mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
Код честно держал адресный латеральный трим с самого решения №5, дока после M1 описывала его верно — расходился только текст ТЗ, писавший «демонтирован целиком». §4.5, §8.2 и AC6 приведены к фактическому контракту. Issue: #302 User-Visible: no
262 lines
20 KiB
Markdown
262 lines
20 KiB
Markdown
# 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. Детектор дыр как объективный инвариант — в дополнение к скриншотам.
|
||
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 зелёными; порядок — на усмотрение реализации.
|