Files
houseplan-card/docs/specs/309-junction-visual-limit.md
T
2026-08-25 21:14:01 +03:00

115 lines
15 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.
# ТЗ #309 — Стыковочные узлы: визуальный лимит mitre и устранение паразитных парных патчей
Issue: https://github.com/Matysh/houseplan-card/issues/309
Статус: ревизия 3 (после SPEC-REVIEW-309-r2: явный не-скоуп #249/paper-механизма и touch-impact).
База: #302 (полный mitre, веера узлов, детектор «без дыр», сет junction-сцен), #271 (thickLength-лимиты).
## 0. Сценарий
Владелец дома рисует и правит план в Plan-редакторе и рассматривает результат в View/kiosk. В местах схождения стен — особенно под острыми углами, у толстых стен и при смешанных толщинах — он видит зубцы: шипы за торцом, пики над узлом, ступени. Узлы участвуют в каждом плане, дефект виден на любой поверхности, где рисуется кладка (Plan, View, kiosk, Static, скрытый слой Iso) — геометрия общая, из структурного кэша (docs/WALL-THICKNESS.md §3).
**До:** из узла торчат клинья и пики длиной до 4 полутолщин, у крестов смешанных толщин — уступы в чужих квадрантах.
**После:** узел закрыт по чертёжному: вершины не дальше 1.5 полутолщины, сверх — аккуратная плоская фаска; кресты смешанных толщин идут по граням тонких стен без уступов.
## 1. Проблема
Полный mitre (#302, решение №5) ограничен только сектором и `MITRE_LIMIT = 4·max(h)`. На экспорте владельца три класса артефактов (все воспроизведены исполнением на dev dc68868):
- **Шип** — парный стык 10+20 см под острым углом: mitre-вершина `linearWallJoinPatches` уходит за торец (вылет до 4h допустим).
- **Горб** — узел 3×50 см: веер `junctionNodeGeometry` принимает вершину с вылетом 1.95h — пика над крышей узла.
- **Ступенька** — крест 15/15/30/30: `linearWallJoinPatches` строит mitre-патчи для **всех** пар лучей узла; пара «верхний толстый × правый толстый» кладёт квадрат 62.5×62.5 в противоположный квадрант, где сектор принадлежит тонким лучам (полосы 31.3), — контур получает уступ [x −62.5…−31.3]×[y 31.3…62.5] (подтверждено выводом контура union: вершины (−31.2, 62.5)→(−62.5, 62.5)→(−62.5, 31.3) вокруг узла).
## 2. Решения владельца (2026-08-25)
1. Визуальный порог вылета mitre: **1.5·max(h)** смежных лучей. Прямые/тупые углы (вылет ≤ ~1.41h) не затрагиваются.
2. Сверх порога сектор закрывается **плоской фаской** перпендикулярно биссектрисе сектора на пороге (miter→bevel), не дугой.
3. Ступенька устраняется: угол закрывается **по границам тонких стен** — паразитные патчи не-соседних пар не рисуются.
## 3. Контракт
### 3.1 Визуальный лимит (новая константа)
`VISUAL_MITRE_LIMIT = 1.5` (src/wall-thickness.ts, рядом с `MITRE_LIMIT`). `MITRE_LIMIT = 4` остаётся верхней санитарной границей построения кандидата; форма среза решается по `VISUAL_MITRE_LIMIT`.
### 3.2 Парные стыки (`linearWallJoinPatches`)
Для пары лучей с mitre-кандидатом `hit`:
- `|hit − node| ≤ 1.5·max(hA, hB)` → патч как сейчас: `[node, pA, hit, pB]`.
- иначе → фаска: патч `[node, pA, cA, cB, pB]`, где `cA`, `cB` — пересечения граней (лучей `pA→hit`, `pB→hit`) с прямой, перпендикулярной биссектрисе сектора и проходящей через точку `node + bis·1.5·max(hA, hB)`. Реализация через клип треугольника полуплоскостью допустима.
- Вырожденные случаи (фаска короче epsilon, cA/cB совпали) деградируют к текущему бевелу `[node, pA, pB]`.
### 3.3 Узлы ≥3 лучей: только соседние пары
В точке, где сходятся ≥3 лучей (узел уже покрывается веерами и полосами `junctionNodeGeometry`), `linearWallJoinPatches` не строит патчи для пар, **не соседних по азимуту** в этом узле. Соседние пары либо пропускаются тоже (сектор закрыт веером — предпочтительно, если детектор и golden подтверждают покрытие), либо остаются — решение фиксируется при реализации измерением, лишний слой не оставлять (см. §12 практику #302: мёртвые слои удаляются с доказательством).
### 3.4 Веера узлов (`junctionNodeGeometry`)
Правило принятия mitre в веере дополняется тем же порогом: вылет ≤ `1.5·max(hA, hB)` → mitre; больше → веер срезается фаской перпендикулярно биссектрисе сектора на пороге (клип веера полуплоскостью). Рефлексные (наружные) mitre — то же правило. `thickLength`-лимиты (#271) не ослабляются.
### 3.5 Инварианты
- **Без дыр (#302):** контрактный детектор `junctionContractHoles` остаётся гейтом; фаска не должна открывать зазор между веером/патчем и полосами. Все 16 junction-сцен + `junction-owner-repro-dark` остаются в наборе; изменившиеся принимаются осознанно (`golden:accept -- --reviewed`) с пересмотром каждой.
- **Без фантомов (#271):** нет новых площадей за пределами `thickLength`.
- Превью рисования (drawWallPreviewD и патчи превью) использует ту же парную логику — форма превью и персиста совпадает.
## 3.6 Скоуп и не-скоуп
**Скоуп:** форма патчей парных стыков и вееров узлов ≥3 лучей в рендер-геометрии кладки; фильтр не-соседних пар; golden/юниты/мутанты; docs/WALL-THICKNESS.md §3.
**Не-скоуп:** модель данных и конфиг (толщины, ключи walls, партиции) не меняются; hit-зоны и снап не меняются; политика полного mitre в допустимом пороге (#302 решение №5) сохраняется; #308 (перегородка-дубликат) не трогается.
**Явный не-скоуп — механизм #249:** `MULTI_WALL_JOIN_LIMIT = 1.25` (src/wall-thickness.ts:83) и его потребители — `multiWallBevelCutsAt` (вырез экстерьерного полотна / paper envelope через `node.limit`) и mitre контуров комнат (:1475/:3875) — не изменяются ни численно, ни по форме. #309 правит только веера кладки (`junctionNodeGeometry`) и парные патчи (`linearWallJoinPatches`); близость порогов (1.25 и 1.5) — не повод их объединять. `MultiWallNodeMap` используется совместно, но `node.limit` не переопределяется. AC8 закрепляет это диффом.
## 3.7 UX
Никаких новых контролов, диалогов или настроек. Меняется только форма отрисованной кладки у узлов на всех поверхностях, где она рисуется (Plan/View/kiosk/Static/hidden-Iso — общий структурный кэш). Превью рисования цепочки использует ту же парную логику — форма превью и персиста совпадает.
## 3.8 Модель данных и миграция
Нет. Чистая рендер-геометрия; конфиг, экспорт/импорт и compatibility-поля не затрагиваются.
## 3.8a Touch (docs/TOUCH-SUPPORT.md)
Touch-контракт не затрагивается: меняется только форма отрисованной кладки; hit-зоны (`physical-hit`, touchStroke), жесты, панорама/зум и порядок обработки касаний не изменяются. View/kiosk на сенсорных экранах получают ту же геометрию из общего структурного кэша без каких-либо новых слушателей.
## 3.9 i18n
Нет новых строк.
## 3.10 Риски
1. **Дыры на границе фаски** — фаска среза может разойтись с полосами/веерами: закрывается контрактным детектором (#302) на сценах и на полном экспорте отчёта (AC5).
2. **Регресс утверждённых форм** — прямые углы должны остаться байтово прежними (AC4); прочие изменившиеся golden-сцены пересматриваются поимённо (AC6).
3. **Удаление парных патчей в узлах ≥3 лучей** может вскрыть непокрытые сектора у экзотических конфигураций — решение «пропускать все или только не-соседние» принимается измерением с детектором, мёртвый слой не оставляется (§3.3).
4. **Перф** — клип полуплоскостью на узел добавляет O(1) операций на сектор; замер перф-смока в гейте.
## 3.11 Release-артефакты
Записи в `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` (User-Visible: yes) в коммите реализации; обновление `docs/WALL-THICKNESS.md` §3 (правило порога и фаски, судьба парных патчей в узлах); скриншот-fingerprint при изменении src.
## 4. Затрагиваемые поверхности
`src/wall-thickness.ts` (обе ветки + константа), golden-сет (пересъёмка затронутых junction-сцен + 3 новые сцены), юниты формы, мутационный гейт, `docs/WALL-THICKNESS.md` §3.
## 5. AC
1. **Шип:** на паре 10+20 см из экспорта отчёта вылет тела за торец ≤ 1.5·max(h); сектор закрыт фаской. Доказательство: юнит формы (вершины патча) + golden-сцена `junction-309-spike`.
2. **Горб:** на узле 3×50 вылет веера ≤ 1.5·h; фаска перпендикулярна биссектрисе. Доказательство: юнит формы (все вершины вееров узла в радиусе 1.5·h·(1+eps)) + golden-сцена `junction-309-hump`.
3. **Ступенька:** на кресте 15/15/30/30 контур у узла идёт по граням тонких полос (нет вершин контура в зоне [за гранью тонкой, до грани толстой] «чужого» квадранта). Доказательство: юнит по контуру union + golden-сцена `junction-309-step`.
4. **Прямые углы не изменились:** для пары равных толщин под 90° патч байтово идентичен прежнему (вылет 1.41h < 1.5h). Доказательство: юнит.
5. **Без дыр:** `junctionContractHoles` пуст на всех сценах сета и на полном экспорте отчёта (13 комнат/24 перегородки). Доказательство: юнит на фикстуре экспорта + существующий смок-детектор.
6. **Golden:** весь сет зелёный; изменившиеся сцены перечислены в ревью с обоснованием формы.
7a. **Не-скоуп #249 (AC8):** `MULTI_WALL_JOIN_LIMIT`, `multiWallBevelCutsAt` и mitre контуров комнат не входят в дифф; существующие юниты/сцены paper envelope зелёные. Доказательство: ревью кода по диффу + существующие тесты.
7. **Мутанты:** (a) `VISUAL_MITRE_LIMIT` обратно в 4 — красный; (b) фаска отключена (всегда mitre) — красный; (c) фильтр не-соседних пар отключён — красный; (d) фаска строится не перпендикулярно биссектрисе (например, хорда pA–pB) — красный. Гварды начинаются с типового пролога tsc+fix-test-build.
## 6. План тестов
- Юниты формы: острая пара (шип), 3×50 (горб), крест 15/30 (контур), 90° равных толщин (неизменность), рефлексный сектор.
- Фикстура: `test/fixtures/309-junction-teeth.json` — минимальный вырез трёх узлов из экспорта владельца.
- Golden: 3 новые сцены (билдер junctionNode/сцена из фикстуры, zoomCenter на узлы), пересъёмка затронутых.
- Мутационный гейт: 4 мутанта §5.7.
## 7. Откат
Один revert: только рендер-геометрия, ни конфигов, ни миграций.