Files
houseplan-card/docs/specs/172-zero-divider-taper.md
T
2026-08-18 15:05:46 +00:00

346 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.