Files
houseplan-card/docs/specs/309-junction-visual-limit.md
T
2026-08-25 20:52:15 +03:00

73 lines
8.9 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
Статус: ревизия 1.
База: #302 (полный mitre, веера узлов, детектор «без дыр», сет junction-сцен), #271 (thickLength-лимиты).
## 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 и патчи превью) использует ту же парную логику — форма превью и персиста совпадает.
## 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:** весь сет зелёный; изменившиеся сцены перечислены в ревью с обоснованием формы.
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: только рендер-геометрия, ни конфигов, ни миграций.