Волна 5 эпика #674, перенос ТЗ (класс C). Из 240 файлов `docs/specs/` в `legacy/specs/` уехали 219: на них не ссылается ни один живой файл (код, тесты, скрипты, workflow, документы вне архива и ревью). Остались 21 ТЗ — на которые ссылаются код, ADR, ISOMETRIC, SUN, RADAR, LIGHT (`docs/specs/067`), DECOR-EDITOR, support-relay, и те, на которые ссылаются они сами; README каталога объясняет, где искать остальное. Открытых issue с файлом ТЗ среди перенесённых нет. Относительные ссылки перенесённых файлов переписаны (`../X` → `../../docs/X`, соседние оставшиеся ТЗ → `../../docs/specs/…`) — все 26 резолвятся. Попутно: битая ссылка в `089-isometric-view-stage1.md:8` на удалённый `089-isometric-view.md` — теперь команда `git show` по истории. Строка в `legacy/README.md`. Issue: #682 User-Visible: no Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
24 KiB
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
- Split-разделитель с
cm = 0не содержит masonry по всей длине; толстая примыкающая стена заканчивается локальным стыком без taper. - Инвариант применяется ко всем углам, толщинам, направлениям и winding.
- Фактический угол Split сохраняется; snapping к нормали не добавляется.
- Исправление едино для Plan, View/static, hidden Iso, clean floor/room fills и препятствий Glow/солнца, без миграции конфигурации.
5. Скоуп
В задачу входят:
- переход между положительной толщиной и точным нулевым offset в вершине room wall profile;
- обе последовательности
h → 0и0 → h, прямой и обратный winding; - выпуклые и вогнутые вершины, острые, тупые, почти коллинеарные и ортогональные допустимые углы;
- все допустимые толщины
1…100 см; - Split с одним или двумя endpoint на толстых стенах;
- локальный cap/step без кладки в интерьере нулевого разделителя;
- сохранение существующего mitre/bevel-контракта для пар
h1 ↔ h2, где обе толщины положительны; - единая каноническая геометрия всех перечисленных поверхностей;
- корректное отображение уже сохранённых планов без записи конфигурации;
- 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. Архитектурные ограничения реализации
- Исправление живёт в общей variable-offset логике
src/wall-thickness.ts, а не в обработчике кликов Split. - Для стыка, где ровно один из соседних offsets положителен, контур обязан сохранить как смещённую точку толстой грани, так и исходную вершину нулевой грани в детерминированном порядке для inset и outset.
- Обе половины кольца вместе формируют локальный cap; ни одна не соединяет offset-точку с дальним узлом нулевой грани.
- Пары двух положительных offsets проходят прежний bounded-mitre/bevel путь.
- Результат остаётся валидным для boolean union/difference/intersection и не полагается на порядок полигонов.
- Canonical wall geometry остаётся единственным источником для drawing, clean-floor и light/sun occlusion.
- Новых 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/bendpoints сохраняют текущий смысл; - чтение и рендер не записывают конфигурацию;
- прямая и обратная миграции не нужны;
- откат на предыдущую версию не требует восстановления данных, но возвращает визуальный дефект.
Это соответствует 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 добавить:
- точный красный fixture §3 с проверкой данных и площади пересечения masonry с внутренней полосой разделителя;
- параметрическую матрицу толщин, углов, направлений и winding;
- negative-regression для двух положительных offsets и exact-collinear step;
- 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
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. Принятые технические предположения
- Точный нулевой offset — единственный триггер нового cap/step пути; малые положительные толщины не приравниваются к нулю.
- Endpoint allowance в тестах вычисляется из half-depth примыкающей стены и общего geometry epsilon, а не задаётся долей длины разделителя.
- Исправление выполняется симметрично в inset и outset; отдельная post-render маска не допускается.
- Новая visual-сцена может переиспользовать fixture #123, если остаётся однозначно проверяемой и не скрывает L-shaped near-normal case.
- Если во время реализации обнаружится отдельный дефект boolean-библиотеки, он оформляется новой issue; #172 не расширяется до общей замены offset engine.