8.9 KiB
ТЗ #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)
- Визуальный порог вылета mitre: 1.5·max(h) смежных лучей. Прямые/тупые углы (вылет ≤ ~1.41h) не затрагиваются.
- Сверх порога сектор закрывается плоской фаской перпендикулярно биссектрисе сектора на пороге (miter→bevel), не дугой.
- Ступенька устраняется: угол закрывается по границам тонких стен — паразитные патчи не-соседних пар не рисуются.
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
- Шип: на паре 10+20 см из экспорта отчёта вылет тела за торец ≤ 1.5·max(h); сектор закрыт фаской. Доказательство: юнит формы (вершины патча) + golden-сцена
junction-309-spike. - Горб: на узле 3×50 вылет веера ≤ 1.5·h; фаска перпендикулярна биссектрисе. Доказательство: юнит формы (все вершины вееров узла в радиусе 1.5·h·(1+eps)) + golden-сцена
junction-309-hump. - Ступенька: на кресте 15/15/30/30 контур у узла идёт по граням тонких полос (нет вершин контура в зоне [за гранью тонкой, до грани толстой] «чужого» квадранта). Доказательство: юнит по контуру union + golden-сцена
junction-309-step. - Прямые углы не изменились: для пары равных толщин под 90° патч байтово идентичен прежнему (вылет 1.41h < 1.5h). Доказательство: юнит.
- Без дыр:
junctionContractHolesпуст на всех сценах сета и на полном экспорте отчёта (13 комнат/24 перегородки). Доказательство: юнит на фикстуре экспорта + существующий смок-детектор. - Golden: весь сет зелёный; изменившиеся сцены перечислены в ревью с обоснованием формы.
- Мутанты: (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: только рендер-геометрия, ни конфигов, ни миграций.