Files
houseplan-card/legacy/specs/302-junction-node-material.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

20 KiB
Raw Permalink Blame History

Issue #302 — материал узла стен: переработка на аддитивную логику

  • Issue: #302
  • Класс изменения: A (продукт)
  • Размер: обычный (не small) — переработка подсистемы
  • Автор ТЗ: Codex, 2026-08-25
  • Touch editor: not exposed — меняется только построение геометрии тел стен, одинаковое при любом способе ввода.

1. Сценарий

Владелец рисует двор с двумя пристройками: две комнаты делят острую вершину (~57°), стены 15–70 см на клетке 30 см. На каждом втором стыке — белые треугольные клинья и зазубрины. Артефакты возвращаются из релиза в релиз: 29 коммитов с «junction», четыре ТЗ (141, 197, 249, 279) — и всё равно.

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

До. Клинья и щели в стыках; вид зависит от числа лучей, углов и толщин непредсказуемо. После. Любой узел — сплошная кладка: mitre в пределах лимита, bevel-фаска за ним, без дыр и без «лишнего» материала, одинаково на карте и в статическом рендерере.

3. Подтверждённая причина (всё — исполнением на dev 4b8f17b)

  1. Дыры — в геометрии, не в отрисовке. На живой карте по _wallUnionGeometry().d сеткой проб isPointInPath(evenodd): 154 пробы у стыка не покрыты материалом, будучи окружены кладкой с ≥5 из 8 сторон.

  2. Дыры рождаются в базовой фазе и никем не лечатся. Чистый конвейер wallBodiesGeometry с пофазными снапшотами на репро владельца (5 узлов, сетка проб):

    после фазы дыр
    room-rings + edge-bodies 359
    + unionJunctionPatches 359
    + facade-clip 359
    + exterior-shell 359
    + bevelMultiWallBody 360 (bevel добавил дыру)

    Отключение bevelMultiWallBody, unionJunctionPatches и facade-clip по одному ничего не меняет (359–360) — все «ремонтные» слои для этого класса дыр нерелевантны.

  3. Локализация. Оба кольца комнат строятся и мержатся без исключений (проверено логом), но клинья сидят на внутренних углах колец в окрестностях multi-wall-узлов — там, где outsetContour/insetContour получают multiWallNodes и обрезают угловой материал, а фаски/патчи вниз по конвейеру эти места не накрывают. Визуализация с отмеченными пробами приложена к issue.

Архитектурно: конвейер — 10 фаз, смешивающих аддитивные патчи и вычитающие разрезы с try/catch-фолбэками. Пока в узловой механике есть difference, гарантии «дыр нет» не существует по построению; каждый точечный фикс двигает баланс add/subtract и рождает регресс в соседней конфигурации — что история и показывает.

4. Продуктовые решения владельца (2026-08-25, issue #302)

  1. Не следующий точечный фикс, а переработка узловой механики на простую и надёжную логику.
  2. Отдельный полноценный сет тестов со скриншотами стыков крупным планом: разное число лучей, толщины, углы, виртуальные стены и т.д.
  3. Детектор дыр как объективный инвариант — в дополнение к скриншотам.
  4. Фаска #249 на внешних углах узлов сохраняется — решение середины дня, отменено решением №5.
  5. Полный mitre везде (финальное решение 2026-08-25, по визуальному сравнению трёх вариантов на T-90 и Y-60 50 см): узлы смыкаются как обычное пересечение стен на чертеже, фаска #249 демонтируется целиком. Поводом стали артефакты фаски на не-ортогональных узлах в новых эталонах (junction-y-60-equal50 — вырез, junction-acute30-mixed — торчащие углы). Следствия: узловая механика чисто аддитивная, слой bevelMultiWallPaper удаляется, bevelMultiWallBody остаётся только адресным латеральным тримом #271 (уточнение при реализации; §8.2), существующие junction-эталоны переснимаются осознанно.

5. Цели

  • Ни одной внутренней дыры ни в одной конфигурации узла из тестового сета и на репро владельца.
  • Одна механика узла вместо стека ремонтных слоёв; поведение выводимо из короткого контракта §8.
  • Оба рендерера получают результат из одного и того же кода (уже так — wallBodiesUnionPath; сохраняется).

6. Scope

  • src/wall-thickness.ts: новая узловая механика; демонтаж заменённых слоёв (bevelMultiWallBody/bevelMultiWallPaper, вычитающие разрезы узлов, спец-обработка multiWallNodes в outsetContour/insetContour — в той мере, в какой их роль переходит к новой механике).
  • Новый чистый модуль детектора дыр (переиспользуется тестами).
  • demo/golden/matrix.mjs + фикстура: сет узловых сцен крупным планом.
  • Юниты, смок, мутанты.

7. Не входит

  • Модель хранения (docs/ADR: полигоны комнат + walls/partitions) — не меняется.
  • Правило роста ±cm/2 от оси (docs/WALL-THICKNESS.md §2) — не меняется.
  • Проёмы: вычитающий слот проёма остаётся как есть (это не узловая механика).
  • Штриховка, цвета, бумага-подложка вне узлов.

8. Контракт поведения

8.1. Узел

Узел — кластер концов лучей (интервалов стен из wallIntervals) в допуске EPS_NODE = openEps × 4 (текущий допуск buildMultiWallNodeMap — сохраняется). Луч приходит в узел со своей полу-толщиной; лучи упорядочиваются по азимуту.

8.2. Правило узла: аддитивные веера (решение №5)

Для каждой пары соседних по азимуту лучей (сектор ≤ 180°; рефлексные секторы — внешняя сторона выпуклого угла — пропускаются):

Веер (аддитивно): полигон «узел → край полосы A → mitre → край полосы B», где mitre — пересечение обращённых в сектор офсетных прямых. Границы:

  • mitre принимается, пока он в пределах классического MITRE_LIMIT × max(толщин пары) — то же правило, что у обычных углов комнат;
  • и пока он не дальше конца ТОЛСТОГО саппорта каждого луча — иначе веер нарисовал бы латеральный фантом рядом с тонким продолжением (#271);
  • иначе — bevel-хорда: по каждому краю до min(длина толстого саппорта, √(limit² − half²)), замыкание хордой.

Дополнительно узел аддитивно получает точные саппорт-квады своих лучей (каждый ограничен собственной конечной длиной). Все куски клиппуются гладкой фасадной границей junctionNodeBound — узел не растит новый фасад на вогнутой вершине.

Единственное вычитание узловой механики — адресный латеральный трим #271: bevelMultiWallBody вызывается только для узлов с вырожденно-коротким толстым саппортом (короче собственной толщины), где базовые контуры красят лишнюю ширину, которую аддитивно не убрать; его угловые следы на таких узлах перекрываются веерами. На всех остальных узлах механика чисто аддитивная: слой фаски #249 как ВИД демонтирован целиком, дыра между полосами невозможна по построению, «лишний» материал ограничен MITRE_LIMIT и фасадной границей.

8.3. Спец-случаи

  • Луч нулевой толщины (виртуальный участок, осевой черновик): в веерах не участвует, соседями по азимуту становятся его соседи (текущее поведение «zero divider не порождает кладку» сохраняется, docs/specs/172).
  • Два коллинеарных луча одной толщины — вырожденный веер (пустой), стык бесшовный (согласуется с #229).
  • Одинокий конец (degree-1) — плоский торец, веера нет (как сейчас).
  • Колонна в узле — самостоятельное тело, union поверх (как сейчас).

8.4. Инвариант «нет дыр» — формальный

Уточнение по факту реализации (первая формулировка «окружено кладкой с ≥5 из 8 сторон» ложно срабатывала на легитимном полу комнаты в острых внутренних углах — за inset-mitre): детектор проверяет контрактное покрытие. Проба p — «дыра», если p ∈ (полоса какого-либо луча узла ∪ веер узла) и p ∉ тело. Сетка проб шагом 0.2 × gridPitch в радиусе MITRE_LIMIT × halfDepth + halfDepth от узла; принадлежность полосе — строго внутри квада (0 ≤ t ≤ длина, |перпендикуляр| < half − ε). Ноль дыр — обязательство для каждого узла каждой сцены сета §13.

8.5. Что не меняется наружно

  • Форма прямых участков, торцы, проёмные тоннели, бумага по контурам комнат.
  • Ортогональные L/T/X-стыки одинаковой толщины обязаны совпасть с текущим видом попиксельно (golden), кроме сцен, где сегодня есть дефекты.

9. Данные, i18n, a11y, privacy, security

Конфиг не меняется, миграций нет, строк интерфейса нет. Приватность/безопасность не затрагиваются.

10. Performance

Замена вычитающих фаз на аддитивные веера уменьшает число булевых операций на узел. Бюджет: smoke_render_perf не хуже базовой линии; large-house фикстура — без деградации, замер в отчёте на код-ревью.

11. Риски

  1. Golden-переснятие. Существующие сцены стыков изменятся законно там, где сегодня дефект. Каждое расхождение разбирается поимённо; принятие — только golden:accept -- --reviewed отдельным коммитом с Release: + Baseline-Reviewed: (урок #230). Сцены, не связанные с узлами, обязаны совпасть побайтно.
  2. Демонтаж слоёв. Убирая bevelMultiWallBody и вычитающие разрезы, можно потерять их полезную функцию для конфигураций вне сета. Ответ — широта сета §13 и обязательный прогон всех существующих junction-смоков и golden.
  3. Незамеченный «лишний» материал. Аддитивные веера могут закрыть то, что раньше было честной щелью (например, два независимых узла рядом). Допуск EPS_NODE не расширяется, веера строятся только между лучами ОДНОГО узла.

12. Acceptance criteria

  • AC1. Детектор §8.4: ноль внутренних дыр на каждом узле каждой сцены сета §13.
  • AC2. Репро владельца (5 узлов): ноль внутренних дыр; визуально клинья исчезли (golden-сцена репро).
  • AC3. Сцены, не связанные с узлами ≥3 лучей, совпадают с текущими эталонами попиксельно. Junction-сцены меняются законно (решение №5) и переснимаются осознанно с поимённым разбором.
  • AC4. Острый угол 57° со стенами 50/70: сплошная кладка (перекрытие полос не срезается), свободная часть клина срезана по лимиту узла.
  • AC5. Виртуальный участок в узле не порождает кладку; соседние физические лучи смыкаются веером через него.
  • AC6. Единственное вычитание узловой механики — адресный трим #271 на узлах с вырожденно-коротким толстым саппортом; все прочие узлы аддитивны (фаска #249 как вид демонтирована). Мутант возвращает лимит веера к 1.25·h — юнит формы веера обязан покраснеть (вырез Y-60 из отчёта владельца).
  • AC7. Оба рендерера дают идентичную геометрию узла (один вызов wallBodiesUnionPath; смок сверяет пути).
  • AC8. Перф: smoke_render_perf в бюджете; large-house без деградации.
  • AC9. Существующие смоки стыков (wall_junctions, junction_patch_resilience, split_corner_wall, zero_divider_taper, wall_thickness*) зелёные.

13. Тестовый сет (golden крупным планом + детектор)

Каждая сцена: узел занимает весь кадр; тёмная тема; для каждой сцены детектор §8.4 по всем узлам. Матрица:

группа сцены
число лучей L (2), T (3), X (4), звезда (5)
толщины равные 15; смешанные 15/50, 50/70, 15/70
углы 90°, 60°, 45°, 30°, 15°, 170°
виртуальные физический+виртуальный в T; виртуальный сквозь X
комнаты вершина комнаты + перегородка; T в середину стены комнаты; общая острая вершина двух комнат (репро)
прочее колонна в узле; конец черновика; перекрёсток двух перегородок

Комбинации не декартовы — ~20 сцен, отобранных по одному представителю на класс, плюс сцена-репро из issue. Юниты: чистые функции веера (mitre/bevel, пороги, вырожденные случаи) и детектора.

14. Мутационный гейт

id Что ломает Гвард
node-fan-disabled веера не строятся вовсе детектор на сете
node-fan-outer-only веер только с внешней стороны детектор
node-fan-ignores-mitre-limit mitre без лимита юниты веера
node-fan-includes-zero-ray нулевой луч порождает кладку юниты + смок
node-fan-limit-back-to-249 лимит веера снова 1.25·h — вырез Y-60 юниты формы веера
hole-detector-blind детектор всегда зелёный самопроверка детектора на заведомо дырявой фикстуре

Все юнит-гварды — с пересборкой test-build (урок #230/#235).

15. Release-артефакты

User-Visible: yes: обе редакции CHANGELOG; docs/WALL-THICKNESS.md §3 и §9 переписываются под новую механику; строка в USER-GUIDE при необходимости. Golden-переснятие — отдельным коммитом по правилу §11.1.

16. Откат

Один revert продуктового коммита + revert коммита эталонов. Конфиг не меняется, миграций нет.

17. Принятые предположения (техническое, менять свободно)

  • Константы детектора (§8.4) выбраны по факту воспроизведения; могут быть уточнены, но только в сторону строгости.
  • Демонтаж старых слоёв допустимо вести поэтапно (веера поверх текущей базы → снятие bevel-слоя → снятие узловой спец-обработки контуров), если каждый шаг держит AC1–AC9 зелёными; порядок — на усмотрение реализации.