Files
houseplan-card/legacy/specs/309-junction-visual-limit.md
Claudeandclaude[bot] df46fd1c3e docs(hygiene): ТЗ выпущенных задач без живых ссылок — в legacy/specs (#682)
Волна 5 эпика #674, перенос ТЗ (класс C). Из 240 файлов `docs/specs/` в
`legacy/specs/` уехали 219: на них не ссылается ни один живой файл (код,
тесты, скрипты, workflow, документы вне архива и ревью). Остались 21 ТЗ —
на которые ссылаются код, ADR, ISOMETRIC, SUN, RADAR, LIGHT (`docs/specs/067`),
DECOR-EDITOR, support-relay, и те, на которые ссылаются они сами; README
каталога объясняет, где искать остальное. Открытых issue с файлом ТЗ среди
перенесённых нет. Относительные ссылки перенесённых файлов переписаны
(`../X` → `../../docs/X`, соседние оставшиеся ТЗ → `../../docs/specs/…`) —
все 26 резолвятся. Попутно: битая ссылка в
`089-isometric-view-stage1.md:8` на удалённый `089-isometric-view.md` —
теперь команда `git show` по истории. Строка в `legacy/README.md`.

Issue: #682
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:10:46 +00:00

15 KiB
Raw Permalink Blame History

ТЗ #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: только рендер-геометрия, ни конфигов, ни миграций.