Files
houseplan-card/docs/specs/249-multiwall-junction-bevel.md
T
2026-08-23 05:07:48 +03:00

21 KiB
Raw Blame History

Issue #249 — ограниченная геометрия узла из трёх и более стен

  • Дата: 2026-08-23
  • Тип: bug + polish · приоритет P2
  • Ценность: пользовательская 7/10 · для разработки 8/10
  • Сложность/риск: 5/10 и 6/10
  • Issue: #249
  • Ветка: issue/249-multiwall-junction-bevel
  • Статус ТЗ: первая редакция, ожидает ревью

Канонические документы: docs/SCOPE.md, docs/WALL-THICKNESS.md, docs/ARCHITECTURE.md, docs/TOUCH-SUPPORT.md, docs/TESTING.md.

1. Сценарий и персона

Администратор рисует обычную пристройку или несколько комнат, сходящихся в одной точке. У трёх стен могут отличаться толщины. В View, kiosk и Static он видит не единый кладочный узел, а узкий клин, который выступает наружу на десятки сантиметров. Контуры пола рядом визуально наезжают друг на друга, хотя сохранённые оси стен и комнаты корректны.

Это нарушает J1 из docs/SCOPE.md: план перестаёт давать достоверную пространственную картину дома. Дефект постоянно виден, а не ограничен редактором.

2. Воспроизведение и измеренный дефект

Канонический regression fixture минимизируется из приложенного к issue экспорта houseplan-space-space-2026-08-22_21-33-04.json (card_version 1.67.0-beta.2, model_version 6, cell_cm = 30). В узле (0.329166667, 0.141666667) сходятся три положительных физических интервала:

Луч от узла Второй конец Толщина Направление
A (0.308333333, 0.237500000) 50 см 102.3°
B (0.408333333, 0.100000000) 50 см −27.8°
C (0.379166667, 0.191666667) 70 см 45.0°

Углы между соседними лучами — 57.3°, 72.8° и 130.0°. Максимальная полутолщина H = 4.8611 render units. Текущий объединённый wall body содержит join-вершину на расстоянии 8.7312, то есть 1.80 × H, и рисует видимый зубец.

Причина подтверждена production-вызовами spaceModels() и wallBodiesGeometry(): две комнаты независимо строят митры пары 50/70 см через outsetContour()/insetContour(). Обе митры проходят общий MITRE_LIMIT = 4, после чего boolean union сохраняет противоположные выступы. junctionAt() и spaceMergeGeometry() этот узел не формируют.

3. Что человек увидит до и после

До: штриховка у многолучевого узла выходит клином далеко за физическую толщину стен; рядом видны наложенные контуры комнат.

После: те же сохранённые оси образуют один заполненный узел. Корректные короткие митры остаются острыми, а только чрезмерная join-вершина заменяется прямой локальной фаской. В узле нет щели, округления или выступа за установленный габарит. Обычные углы ровно из двух лучей выглядят как раньше.

4. Решения владельца

  1. Fallback чрезмерной митры — прямая локальная фаска. Она образует единый заполненный узел; округление не добавляется.
  2. В узле из 3+ различных физических лучей допустимый радиус join-вершины: R = 1.25 × H, где H — максимальная полутолщина всех сходящихся стен.
  3. Правило применяется независимо от числа комнат и от того, равны ли толщины.
  4. Узлы ровно из двух лучей сохраняют MITRE_LIMIT = 4 и существующий вид.
  5. Исправляется одна каноническая вычисляемая геометрия для Full, Static, hidden Iso и световых барьеров. Persisted rooms/walls не переписываются.

5. Scope

Входит

  • классификация физических узлов по уникальным инцидентным лучам;
  • общий лимит 1.25 × max half-depth для join-геометрии узлов степени 3+;
  • ограниченная митра либо прямая фаска в room inset/outset и в обязательных локальных junction pieces;
  • согласованная каноническая кладка, clean-floor/room-fill границы, бумага, Full/Static/hidden-Iso и Glow/sun occlusion;
  • unit-набор на 3/4 луча, разные толщины, порядок, winding и coordScale;
  • browser smoke и golden-сценарий на минимизированном экспорте владельца;
  • обновление docs/WALL-THICKNESS.md, docs/ARCHITECTURE.md, docs/TESTING.md и двух changelog.

Не входит

  • изменение сохранённых координат комнат, walls, partitions или drafts;
  • привязка узлов к сетке и Optimize-миграция старых планов;
  • изменение толщины стен либо правил shared/outer/virtual interval;
  • новый редакторский инструмент, настройка коэффициента в UI или i18n;
  • округлённые joins, отделочные слои и отдельный материал узла;
  • изменение двухлучевых острых/тупых углов;
  • исправление независимой partition-топологии, уже принадлежащей linearWallJoinPatches().

6. Термины и классификация узла

physical interval — атомарный room-wall interval с положительной толщиной, который не является virtual/open. Его полутолщина уже переведена в render units существующим wallCmToUnits().

node — группа совпадающих концов физических интервалов в существующей scale-relative геометрической погрешности (openEps(pitch, coordScale) либо единый эквивалент подсистемы). Координаты не округляются к drawing grid.

ray — направление от node к противоположному концу interval:

  • повтор одного физического shared interval от второй комнаты удаляется по каноническому interval key;
  • совпадающие сонаправленные атомарные фрагменты считаются одним лучом и берут максимальную физическую полутолщину;
  • противоположные направления прямой стены являются двумя разными лучами;
  • zero-thickness и virtual intervals не увеличивают степень узла.

multi-wall node — node с тремя или более различными rays. Для него H = max(ray.halfDepth), R = 1.25 × H. Если конечные значения и положительный H получить нельзя, локальный limiter не создаёт новую геометрию и не должен ронять весь structural pass.

join vertex — точка пересечения смещённых граней либо точка локального junction patch, созданная именно для соединения интервалов в node. Дальние концы самих стен не являются join vertices и радиусом R не ограничиваются.

7. Геометрический контракт

7.1 Единая карта узлов

Один pure structural pass строит детерминированную карту multi-wall nodes из эффективных wallIntervals(). Карта не зависит от порядка rooms/walls, направления сохранённого отрезка или winding комнаты. Она строится один раз для текущей геометрии и передаётся всем contour/junction вычислениям; отдельные рендереры не распознают узел повторно.

7.2 Ограничение join

Когда вершина room profile совпадает с multi-wall node:

  1. существующее пересечение смещённых граней остаётся митрой, если его расстояние от node <= R + epsilon;
  2. если пересечения нет, оно не конечное либо дальше R, join содержит две штатные offset-точки соседних граней — прямую фаску;
  3. каждая созданная для этого node join-вершина остаётся не дальше R + epsilon;
  4. локальное соединение имеет ненулевую площадь, node не попадает в hole wall body, а тела всех инцидентных rays входят в один связный кладочный компонент;
  5. boolean union не вправе вернуть в body отброшенную дальнюю вершину из другого независимого room ring. Все ring/piece producers получают одну карту и лимит до объединения.

Offset-точка фаски находится на расстоянии собственной полутолщины, поэтому для валидного interval автоматически укладывается в H <= R. Круг либо аппроксимация окружности не строятся.

7.3 Двухлучевая совместимость

Для node степени 1 или 2 сигнатуры и результат insetContour(), outsetContour() и существующих bounded junction patches не меняются: MITRE_LIMIT = 4 остаётся единственным лимитом. Новый коэффициент не становится глобальной заменой MITRE_LIMIT.

7.4 Канонические потребители

Исправленная structural geometry является общей для:

  • wall body/hatch в Full, kiosk и Static;
  • Plan preview сохранённой геометрии;
  • hidden isometric masonry;
  • clean floor, room fills и room hover, использующих внутреннюю грань;
  • paper/exterior shell там, где multi-wall node принадлежит внешней границе;
  • Glow/sun/light source occlusion.

Существующие structural caches продолжают ключеваться полной геометрией. Курсор, hover и HA state tick не перестраивают topology. Исправление не вводит расходящиеся SVG-only и physics-only patches.

7.5 Ошибки и fallback

  • Валидный multi-wall node не может молча вернуться к дальнему MITRE_LIMIT = 4 join: если точная митра не укладывается в R, результатом служит фаска.
  • Неконечный или вырожденный локальный candidate отбрасывается изолированно; он не превращает успешную остальную кладку, paper или light barriers в null/пустой план.
  • Существующий fail-dark контракт обязательного structural pass сохраняется: исправление не должно маскировать независимую ошибку exterior/body/opening.

8. Данные, совместимость и производительность

  • Схема и model_version не меняются; config/layout остаются byte-for-byte.
  • Результат вычисляется при чтении и сразу исправляет старые планы без Save.
  • Backend и WebSocket не меняются.
  • Нет новых i18n, настроек, действий, жестов либо различий mouse/touch.
  • Построение node map ограничено structural geometry pass; допускается линейная либо O(E log E) группировка. Новый полный O(E²) обход на каждый render/state tick запрещён.

9. Acceptance criteria и доказательства

AC1. Экспорт владельца больше не даёт шип

Минимизированный fixture хранит только необходимые rooms, wall entries и scale-параметры из экспорта. Для node (0.329166667, 0.141666667):

  • wallBodiesGeometry() возвращает успешную непустую геометрию;
  • H = 4.8611, и каждая join-вершина node находится не дальше 1.25 × H + epsilon;
  • node не лежит в hole, локальный wall body имеет ненулевую площадь и один компонент касается тел всех трёх rays;
  • повторный расчёт даёт тот же нормализованный geometry/path.

Доказательство: unit в test/wall-thickness.test.mjs и новый fixture test/fixtures/249-multiwall-junction.json.

AC2. Матрица многолучевых узлов

Те же инварианты выполняются минимум для:

  • трёх стен одинаковой толщины;
  • трёх стен 15/50/70 см;
  • четырёх стен с равными и разными толщинами;
  • перестановки rooms/walls, обратного направления intervals, обоих windings и production coordScale = 1000.

Ни один вариант не создаёт gap, self-intersection, null либо второй локальный кладочный компонент.

Доказательство: table-driven unit с численным bound и сравнением нормализованной геометрии.

AC3. Два луча не меняются

Ровно две стены одинаковой толщины под углами fixture #249 и существующие acute/obtuse contour cases возвращают прежние точки/paths и продолжают использовать MITRE_LIMIT = 4. Новый limiter не превращает корректную митру в фаску.

Доказательство: unit regression на insetContour()/outsetContour() и существующий wall-thickness unit-набор без обновления его двухлучевых expected values.

AC4. Все поверхности используют один body

На fixture #249 Full/View, Plan с сохранённым контуром, kiosk, Static, hidden Iso и light occlusion получают одну исправленную physical geometry: клина нет, а луч света не проходит через заполненный узел. Переключение темы и HA state не меняет structural path и не вызывает его пересчёт.

Доказательство: targeted browser smoke demo/smoke_multiwall_junction.mjs с path/geometry assertions и cache parity.

AC5. Golden фиксирует видимый результат

В golden matrix добавлен отдельный crop узла из минимизированного экспорта: видны три луча, фаска и отсутствие щели. Сценарий имеет semantic assertions на число rays и ограниченный габарит, поэтому пустой или неверно кадрированный PNG не может пройти.

Доказательство: demo/golden/matrix.mjs, harness/fixture и test/golden-matrix.test.mjs. Новый baseline принимается только по полному Linux CI artifact командой npm run golden:accept -- --reviewed перед бетой.

AC6. Данные и смежные контракты не меняются

Расчёт не мутирует rooms/walls/open spans, не меняет schema/model version и не затрагивает independent partition junctions, openings или двухлучевые внешние углы.

Доказательство: deep-equality unit входа до/после и зелёные существующие wall/opening/junction tests.

AC7. Гейты реализации

В цикле реализации зелёные:

  • npm run typecheck;
  • npm test;
  • npm run build;
  • targeted node demo/smoke_multiwall_junction.mjs после копирования свежего bundle в demo.

Полный smoke, golden и performance запускаются перед бетой; полный backend HA-harness не требуется, если Python/backend не меняются, но общий exact-SHA Validate остаётся обязательным.

10. Документация и changelog

  • docs/WALL-THICKNESS.md: степень node, R = 1.25 × H, фаска и сохранение MITRE_LIMIT = 4 для двух лучей;
  • docs/ARCHITECTURE.md: единая node map и потребители canonical masonry;
  • docs/TESTING.md: unit/smoke/golden evidence #249;
  • docs/CHANGELOG.md и docs/CHANGELOG.ru.md: пользовательский bugfix в том же коммите, что продуктовая правка.

11. Откат

Откатывается pure multi-wall node classification/limit и связанные tests/docs. Данные не мигрируются, поэтому отдельный data rollback не нужен. Golden baseline откатывается только вместе с соответствующей product geometry.

12. Принятые технические предположения

  1. Коэффициент 1.25 — именованная константа канонической wall geometry, а не настройка и не литерал в рендерерах.
  2. Shared interval, встреченный из двух room profiles, является одним физическим лучом; иначе обычный T ошибочно классифицировался бы по числу владельцев.
  3. В regression fixture не переносится остальной пользовательский экспорт: сохраняется минимальная обезличенная геометрия, достаточная для численного и визуального воспроизведения.
  4. epsilon берётся из существующей scale-relative геометрической политики и не ослабляется до шага drawing grid.
  5. Если реализация требует отдельного bounded fill patch для положительной связности, он остаётся частью canonical structural geometry, целиком лежит в радиусе R и проходит тот же детерминизм/failure isolation; это не отдельный визуальный слой.