mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 11:49:16 +00:00
346 lines
24 KiB
Markdown
346 lines
24 KiB
Markdown
# 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.
|