diff --git a/docs/specs/172-zero-divider-taper.md b/docs/specs/172-zero-divider-taper.md new file mode 100644 index 00000000..b976a210 --- /dev/null +++ b/docs/specs/172-zero-divider-taper.md @@ -0,0 +1,345 @@ +# 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. diff --git a/docs/specs/README.md b/docs/specs/README.md index 28ceb0e0..d506474e 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -92,6 +92,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным | [#141](https://github.com/Matysh/houseplan-card/issues/141) Бесшовные стыки перегородок и открытых контуров | [141-wall-junctions.md](141-wall-junctions.md) | | [#157](https://github.com/Matysh/houseplan-card/issues/157) Тип проёма «Открытый проём» | [157-open-passage.md](157-open-passage.md) | | [#150](https://github.com/Matysh/houseplan-card/issues/150) Точная геометрия коллинеарного перепада толщины | [150-wall-thickness-transition.md](150-wall-thickness-transition.md) | +| [#172](https://github.com/Matysh/houseplan-card/issues/172) Нулевой Split-разделитель не получает ложную толщину | [172-zero-divider-taper.md](172-zero-divider-taper.md) | ## Правило актуализации