Files
houseplan-card/docs/specs/302-junction-node-material.md
T
2026-08-25 17:34:18 +03:00

232 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Issue #302 — материал узла стен: переработка на аддитивную логику
- Issue: [#302](https://github.com/Matysh/houseplan-card/issues/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. Детектор дыр как объективный инвариант — в дополнение к скриншотам.
## 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. Аддитивное построение — единственное правило узла
Материал узла = union из:
1. тел лучей (прямоугольные полосы до узла, как сейчас);
2. **углового веера между каждой парой соседних по азимуту лучей**: пересечение
офсетных прямых двух лучей на обращённой друг к другу стороне даёт
mitre-точку; веер — полигон «узел → конец полосы A → mitre → конец полосы B».
Если mitre-точка дальше `MITRE_LIMIT × max(толщин)` от узла или прямые
параллельны — вместо неё хорда между концами полос (bevel). Правило одно и
то же для внешней и внутренней стороны угла и для любого числа лучей.
**В узловой механике нет ни одной операции `difference`.** Дыра между телами
лучей невозможна по построению: каждый сектор между соседними лучами накрыт
веером. «Лишний» материал ограничен тем же `MITRE_LIMIT`, что и сегодня.
### 8.3. Спец-случаи
- Луч нулевой толщины (виртуальный участок, осевой черновик): в веерах не
участвует, соседями по азимуту становятся его соседи (текущее поведение
«zero divider не порождает кладку» сохраняется, docs/specs/172).
- Два коллинеарных луча одной толщины — вырожденный веер (пустой), стык
бесшовный (согласуется с #229).
- Одинокий конец (degree-1) — плоский торец, веера нет (как сейчас).
- Колонна в узле — самостоятельное тело, union поверх (как сейчас).
### 8.4. Инвариант «нет дыр» — формальный
Точка p — «внутренняя дыра», если p ∉ материал, и лучи из p в ≥5 из 8
направлений (шаг 45°) пересекают материал в радиусе `2.2 × gridPitch`.
Детектор: сетка проб шагом `0.2 × gridPitch` в радиусе `5 × gridPitch` от узла.
Ноль внутренних дыр — обязательство для каждого узла каждой сцены сета §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.** Ортогональные стыки одинаковой толщины: попиксельное совпадение с
текущими эталонами.
- **AC4.** Острый угол 57° со стенами 50/70: сплошная кладка, фаска в пределах
`MITRE_LIMIT`.
- **AC5.** Виртуальный участок в узле не порождает кладку; соседние физические
лучи смыкаются веером через него.
- **AC6.** Узловая механика не содержит `difference` — проверяется чтением и
мутантом на возврат вычитающего слоя.
- **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-difference-back` | возврат вычитающего разреза в узел | детектор + AC6 |
| `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 зелёными; порядок — на усмотрение реализации.