# Issue #172 — нулевой Split-разделитель не получает ложную толщину - **Issue:** https://github.com/Matysh/houseplan-card/issues/172 - **Редакция:** первая редакция для независимого ревью; статус определяется только метками issue - **Тип / приоритет:** bug / P2 - **Оценка:** пользовательская ценность 8/10; сложность 6/10; риск 7/10 - **Область:** редактор Плана, каноническая геометрия стен, View/static, hidden Iso, clean floor, Glow и солнце - **Модель данных:** без изменений и миграции - **Связано:** #123, #150, `docs/WALL-THICKNESS.md`, `docs/ARCHITECTURE.md`, `docs/TOUCH-SUPPORT.md` ## 1. Сценарий и персона **Персона:** администратор дома, который строит и уточняет архитектуру плана в desktop-редакторе. **Поверхность и момент:** на плане есть Г-образная комната с заданной толщиной наружных стен. Пользователь выбирает «Разделить», начинает линию во внутреннем углу и заканчивает на другой стене. Линия почти продолжает одно из плеч угла, но отклоняется от него на доли градуса или несколько градусов. Задача поддерживает J4 и J6 из `docs/SCOPE.md`: редактор должен сохранять правдивую геометрию, а штатное разделение комнаты не должно создавать стену, которую пользователь не задавал. Общая физическая модель также сохраняет согласованность J1/J2/J3 между Plan, View и световыми эффектами. ## 2. Что человек увидит до и после **До:** разделитель сохранён без толщины, но визуально вдоль него появляется треугольный клин: на одном конце он почти нулевой, на другом вырастает до толщины примыкающей стены. **После:** нулевой разделитель остаётся без кладки по всей длине; существующие толстые стены заканчиваются локально в точках примыкания и не растягиваются вдоль разделителя. Небольшой нарисованный угол сохраняется без автокоррекции. ## 3. Проблема и подтверждённая причина Дефект воспроизведён на `origin/dev` SHA `a05aa5d` с production-параметрами `pitch = 1/240`, `coordScale = 1000`, `cell_cm = 5`, `GRID_PITCH = 1000/240`. Fixture — Г-образный полигон `[100,100]–[900,100]–[900,800]–[600,800]–[600,400]–[100,400]`, наружные стены `15 см`, Split от `[600,400]` к `[900,402.5]` или `[900,405]`. Слой данных корректен: - оба room-owned интервала общей границы классифицируются как `shared`; - их эффективная толщина равна `0`; - положительной `WallEntry` для разделителя нет; - рендер не должен материализовать такую запись. Ошибка возникает в `insetContour()` / `outsetContour()`. При стыке положительного offset с нулевым и небольшом отклонении от коллинеарности удалённый mitre отбрасывается по `MITRE_LIMIT`. Bevel-ветка сохраняет только смещённую точку толстой грани и теряет исходную вершину нулевой грани. Следующий участок контура соединяет эту точку с дальним концом разделителя и создаёт taper-клин. На красном fixture поперечное сечение ложной кладки растёт вдоль нулевого разделителя приблизительно от `0` до полной глубины стены `15 см`. При точном `0°` специальная коллинеарная ветка уже сохраняет ступень, поэтому дефект легко пропустить тестом только ортогональной геометрии. Это не дубликат: - #123 сохраняет наружный фасад при Split из вершины; - #150 сохраняет breakpoint между коллинеарными внешними интервалами разной толщины; - #172 убирает ложный taper вдоль нулевой внутренней общей границы. ## 4. Решения владельца Владелец принял defaults Q1–Q4 18.08.2026: https://github.com/Matysh/houseplan-card/issues/172#issuecomment-5329384353 1. Split-разделитель с `cm = 0` не содержит masonry по всей длине; толстая примыкающая стена заканчивается локальным стыком без taper. 2. Инвариант применяется ко всем углам, толщинам, направлениям и winding. 3. Фактический угол Split сохраняется; snapping к нормали не добавляется. 4. Исправление едино для Plan, View/static, hidden Iso, clean floor/room fills и препятствий Glow/солнца, без миграции конфигурации. ## 5. Скоуп В задачу входят: 1. переход между положительной толщиной и точным нулевым offset в вершине room wall profile; 2. обе последовательности `h → 0` и `0 → h`, прямой и обратный winding; 3. выпуклые и вогнутые вершины, острые, тупые, почти коллинеарные и ортогональные допустимые углы; 4. все допустимые толщины `1…100 см`; 5. Split с одним или двумя endpoint на толстых стенах; 6. локальный cap/step без кладки в интерьере нулевого разделителя; 7. сохранение существующего mitre/bevel-контракта для пар `h1 ↔ h2`, где обе толщины положительны; 8. единая каноническая геометрия всех перечисленных поверхностей; 9. корректное отображение уже сохранённых планов без записи конфигурации; 10. unit, targeted browser smoke, visual fixture, документация и RU/EN changelog. ## 6. Не входит в задачу - snapping почти перпендикулярного или почти коллинеарного Split; - изменение допустимости, маршрута кликов, диалога или сохранения Split; - автоматическое назначение толщины новой общей границе; - изменение инструмента «Толщина», `Перегородка` или открытых контуров; - переработка модели `rooms`, `walls`, `partitions`, `open_spans` или `wall_columns`; - новые настройки cap/join, пользовательские переключатели или i18n-тексты; - изменение `MITRE_LIMIT` для стыков двух положительных толщин; - миграция, schema version, backend или импорт/экспорт; - публикация изометрии как публичной функции. ## 7. Контракт поведения ### 7.1. Нулевой разделитель Пусть `D` — общая граница двух комнат после Split, а её эффективная толщина равна `0`. - Внутренняя часть `D`, за вычетом локальных областей примыкания существующих толстых стен, не пересекается с канонической masonry geometry. - Толщина вдоль `D` не интерполируется и не образует taper. - В точке перехода `h ↔ 0` сохраняется вершина нулевой грани и формируется локальный cap/step толстой стены. - Локальная область примыкания ограничена физической half-depth примыкающей стены и геометрическим epsilon; она не может расти пропорционально длине `D`. - Правило симметрично относительно порядка комнат, направления границы и winding. ### 7.2. Существующие стены - Толстая наружная или общая стена сохраняет заданную полную глубину до точки примыкания. - Наружный фасад до и после Split сохраняет контракт #123. - Две положительные соседние толщины продолжают использовать действующие mitre/bevel и `MITRE_LIMIT`; исправление нулевого перехода не превращает их в принудительные плоские caps. - Строго коллинеарный переход сохраняет существующую точную ступень. - Проёмы и независимые физические тела не меняют свою ассоциацию или форму. ### 7.3. Геометрия Split и данные - `splitRoomPath()` сохраняет фактически выбранные endpoints и промежуточные точки; угол не округляется к нормали. - Большая и новая комнаты сохраняются по действующему контракту. - Нулевой разделитель не получает `WallEntry` и не материализуется при чтении. - Рендер не мутирует `rooms`, `walls` и другие persisted-поля. - Уже сохранённая конфигурация исправляется вычисляемо сразу после обновления. ### 7.4. Все потребители Один результат wall-body geometry используется в: - Plan editor; - View, kiosk и `houseplan-space-card`; - hidden Iso/floor footprint; - paper, room fills и clean-floor contours; - физических препятствиях Glow и солнечных лучей. Renderer-specific SVG mask, отдельная геометрия только для Plan или подавление штриховки без исправления физического тела не выполняют контракт. ## 8. Архитектурные ограничения реализации 1. Исправление живёт в общей variable-offset логике `src/wall-thickness.ts`, а не в обработчике кликов Split. 2. Для стыка, где ровно один из соседних offsets положителен, контур обязан сохранить как смещённую точку толстой грани, так и исходную вершину нулевой грани в детерминированном порядке для inset и outset. 3. Обе половины кольца вместе формируют локальный cap; ни одна не соединяет offset-точку с дальним узлом нулевой грани. 4. Пары двух положительных offsets проходят прежний bounded-mitre/bevel путь. 5. Результат остаётся валидным для boolean union/difference/intersection и не полагается на порядок полигонов. 6. Canonical wall geometry остаётся единственным источником для drawing, clean-floor и light/sun occlusion. 7. Новых persisted-полей, фоновых writes и renderer-specific caches нет. Предполагаемые файлы: - `src/wall-thickness.ts`; - `test/wall-thickness.test.mjs`; - новый `demo/smoke_zero_divider_taper.mjs` либо эквивалентное расширение узкого Split-smoke; - `demo/golden/matrix.mjs`, fixture/harness и `test/golden-matrix.test.mjs`, если новая visual-сцена оформляется отдельно; - `docs/WALL-THICKNESS.md`, при необходимости `docs/ARCHITECTURE.md`; - `docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`; - три синхронные поставляемые копии bundle. ## 9. Модель данных, compatibility и миграция Форматы `RoomCfg` и `WallEntry` не меняются. Новых ключей, aliases и compatibility-полей нет. - legacy midpoint-only keys продолжают читаться; - lossless `a`/`b` endpoints сохраняют текущий смысл; - чтение и рендер не записывают конфигурацию; - прямая и обратная миграции не нужны; - откат на предыдущую версию не требует восстановления данных, но возвращает визуальный дефект. Это соответствует `docs/CONFIG-COMPATIBILITY.md`: меняется только вычисляемая геометрия существующих данных. ## 10. UX, i18n, accessibility и touch Новых controls, текстов, focus/keyboard semantics или ARIA нет. Новые RU/EN i18n-ключи не требуются. Split остаётся desktop-first. Touch editor: **best effort / intentionally degraded**, но одна и та же сохранённая геометрия не может зависеть от типа указателя. View, kiosk и static являются блокирующими поверхностями и обязаны совпадать с desktop Plan. Темы, `prefers-reduced-motion`, цвет и opacity стен не меняются. ## 11. Критерии приёмки - **AC1 (`unit`):** production-scale fixture из §3 с углами `0,477°` и `0,955°` сохраняет `cm = 0` на обоих room-owned интервалах разделителя, а его внутренняя часть не пересекается с masonry geometry. Тест красный на исходном `dev` из-за taper-клина. - **AC2 (`unit`):** матрица `h = 1, 15, 100 см`, `h → 0` и `0 → h`, углы по обе стороны от точной линии, оба направления и оба winding не создаёт кладку вдоль нулевой грани; размер локального cap ограничен half-depth и epsilon, а не длиной грани. - **AC3 (`unit`):** точный коллинеарный и ортогональный переходы остаются корректными; пары `1 ↔ 15`, `15 ↔ 100` и равные положительные offsets сохраняют действующий mitre/bevel-контракт. - **AC4 (`unit`):** наружная геометрия Г-образной комнаты до/после Split сохраняется, а clean-floor union равен room union минус каноническая кладка; нулевой разделитель не удаляет треугольник пола. - **AC5 (`unit`):** `splitRoomPath()` сохраняет фактический угол fixture без snap, render helpers не меняют сериализованные `rooms`/`walls`, миграция и новые config keys отсутствуют. - **AC6 (`smoke`):** targeted browser-smoke выполняет реальный Split из вогнутого угла с отклонением около `1°`, подтверждает нулевые persisted интервалы и отсутствие taper в SVG Plan. - **AC7 (`smoke`):** тот же smoke доказывает побайтовое/геометрическое совпадение канонической wall-body path в Plan, View/kiosk, static и hidden Iso и совпадение masonry geometry световых препятствий; конфигурация после рендера не меняется. - **AC8 (`golden`):** детерминированная visual-сцена Г-образного Split явно показывает нулевой разделитель без клина и без изменения фасада. Baseline принимается только через `golden:accept -- --reviewed` по полному Linux CI artifact на предрелизном этапе. - **AC9 (`unit` + `smoke`):** существующие регрессии corner Split, wall junctions, wall thickness и opening tunnels остаются зелёными; проёмы и независимые тела не меняются. - **AC10 (`ревью кода`):** исправление находится в общей variable-offset геометрии; renderer-specific masks/branches и изменения persisted schema отсутствуют. - **AC11 (`ревью кода`):** новых DOM-узлов, событий, таймеров, сетевых запросов, HA services и privacy/security surfaces нет. ## 12. План автотестов и гейтов ### Unit В `test/wall-thickness.test.mjs` добавить: 1. точный красный fixture §3 с проверкой данных и площади пересечения masonry с внутренней полосой разделителя; 2. параметрическую матрицу толщин, углов, направлений и winding; 3. negative-regression для двух положительных offsets и exact-collinear step; 4. clean-floor/exterior invariants и immutability. Проверка должна уметь падать: временное возвращение старой bevel-ветки обязано красить как минимум AC1/AC2. Ревьюер фиксирует эту проверку в code review. ### Browser smoke `node demo/smoke_zero_divider_taper.mjs` (имя можно изменить при сохранении однозначной связи с #172) выполняется локально перед `S7-code-review`. Smoke использует `show_borders: true`, чтобы Plan/View/static/Iso сравнивали реально отрисованные стены. ### Golden и pre-release Новая visual-сцена добавляется в матрицу, но baseline не принимается ради зелёной ветки. Capture/diff рассматриваются на предрелизном Linux-гейте; полный golden, smoke и performance выполняются перед бетой по `PROCESS.md` §11.4. ### Обязательный implementation loop ```text npm run typecheck npm test npm run build node demo/smoke_zero_divider_taper.mjs ``` После build три bundle-копии должны совпадать побайтово. Backend/HA harness не нужен: Python и Home Assistant integration не затрагиваются. ## 13. Риски, performance и security | Риск | Мера | |---|---| | Исправление нулевого перехода меняет обычные положительные углы. | AC2/AC3 разделяют `h ↔ 0` и `h1 ↔ h2`; положительные пары сохраняют прежний путь. | | Новый cap создаёт самопересекающийся contour при вогнутом угле. | Матрица convex/concave, оба winding и boolean-validity assertions. | | SVG исправлен, но floor/light продолжают видеть клин. | AC4/AC7 проверяют общий geometry object и потребителей. | | Тест исключает слишком большую endpoint-зону и пропускает taper. | Endpoint allowance ограничен физической half-depth + epsilon, дополнительно проверяются несколько внутренних сечений. | | Golden принимается без review. | Только полный Linux artifact и `--reviewed`; до этого хранится candidate/diff. | Performance-влияние практически отсутствует: число вершин variable-offset контура увеличивается максимум на одну точку в каждом переходе `h ↔ 0`. Асимптотика, cache keys и частота вычислений не меняются. Отдельный performance budget не нужен; общий pre-release performance smoke остаётся обязательным. Security/privacy влияние отсутствует: нет ввода HTML/CSS, сетевых запросов, service calls, новых разрешений или данных HA. ## 14. Откат Исправление откатывается одним frontend-коммитом вместе с тестами и документацией. Persisted schema не меняется, поэтому обратная миграция и cleanup не нужны. Откат возвращает ложный taper, но не повреждает сохранённые планы. Feature flag не добавляется: это исправление общей физической геометрии, а не экспериментальная функция. ## 15. Release-артефакты - пользовательские записи в `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` в том же implementation-коммите (`User-Visible: yes`); - обновление `docs/WALL-THICKNESS.md`; `docs/ARCHITECTURE.md` — только если уточняется общий variable-offset контракт; - targeted unit и browser smoke; - visual fixture/candidate и reviewed golden baseline по правилам §12; - синхронные `dist/houseplan-card.js`, HA frontend и demo bundle; - screenshots вне golden не требуются; - backend, migration и отдельные security/performance artifacts не требуются; - issue закрывается только после включения в опубликованную бету. ## 16. Принятые технические предположения 1. Точный нулевой offset — единственный триггер нового cap/step пути; малые положительные толщины не приравниваются к нулю. 2. Endpoint allowance в тестах вычисляется из half-depth примыкающей стены и общего geometry epsilon, а не задаётся долей длины разделителя. 3. Исправление выполняется симметрично в inset и outset; отдельная post-render маска не допускается. 4. Новая visual-сцена может переиспользовать fixture #123, если остаётся однозначно проверяемой и не скрывает L-shaped near-normal case. 5. Если во время реализации обнаружится отдельный дефект boolean-библиотеки, он оформляется новой issue; #172 не расширяется до общей замены offset engine.