Files
houseplan-card/legacy/specs/172-zero-divider-taper.md
Claudeandclaude[bot] df46fd1c3e docs(hygiene): ТЗ выпущенных задач без живых ссылок — в legacy/specs (#682)
Волна 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
2026-09-27 22:10:46 +00:00

24 KiB
Raw Permalink Blame History

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

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.