diff --git a/docs/specs/310-pair-apex.md b/docs/specs/310-pair-apex.md new file mode 100644 index 00000000..671eb556 --- /dev/null +++ b/docs/specs/310-pair-apex.md @@ -0,0 +1,70 @@ +# ТЗ #310 — Парный острый стык: полное остриё и срез выступающего торца + +Issue: https://github.com/Matysh/houseplan-card/issues/310 +Статус: ревизия 1. +База: #309 (визуальный лимит и фаска), #302 (веера, детектор дыр), #271 (адресный латеральный трим). + +## 0. Сценарий + +Владелец сводит две стены под острым углом (перегородки, свободные стены). На чертеже такие стены сходятся в точку — остриё легитимно. #309 ошибочно срезал остриё фаской, а настоящий дефект — маленький зубец: прямоугольный торец толстой стены выступает вбок за грань тонкой у самого узла — остался (он виден и на исходном скрине владельца при старом полном mitre, и как ступенька на грани в `junction-309-spike-dark`). + +**До:** остриё срезано плоско (фаска 1.5·h), на грани у узла ступенька-зубец от торца толстой стены. +**После:** чистый клин: остриё доведено до пересечения граней, зубец срезан продолжением грани тонкой стены. Узлы ≥3 лучей не меняются. + +## 1. Решения владельца (2026-08-25, чат) + +1. Узел ровно двух лучей: **полный mitre без визуального лимита** — вершина на пересечении внешних граней. Фаска `VISUAL_MITRE_LIMIT` (#309) остаётся только в веерах узлов ≥3 канонических лучей. +2. **Зубец торца срезается по граням острия**: выступающая за наружную грань тонкой стены часть тела толстой в окрестности узла обрезается продолжением этой грани (столярный mitre-joint), не по биссектрисе. + +## 2. Контракт + +### 2.1 Парная ветка `linearWallJoinPatches` + +Для узла ровно двух лучей (после мультиволл-скипа #309 в эту ветку попадают только такие) mitre-патч строится как до #309: `[node, pA, hit, pB]` при существующем `hit` — **без** проверки `VISUAL_MITRE_LIMIT` и без `chamferApex`; фолбэк `[node, pA, pB]` для параллельных/вырожденных пар сохраняется. `MITRE_LIMIT` в парной ветке не применяется (санитарная граница остаётся только в веерах, где её сопровождает фаска). + +### 2.2 Срез выступающего торца (новый адресный трим) + +В узле ровно двух лучей с принятым mitre для каждой стены пары строится **вычитаемый клин торца**: часть её тела, лежащая снаружи наружной грани соседа (полуплоскость грани, образующей сторону острия) и в окрестности узла — вдоль оси стены не дальше `2·halfDepth(собственной)` от узла. Практически выступ существует только у более толстой стены; правило симметрично и у тонкой даёт пустое множество. Реализация — второе адресное вычитание рядом с тримом #271 (той же фазой в потребителях), с теми же требованиями: строгая локальность, никакого влияния на тела вне окрестности узла. + +Инвариант #302 AC6 обновляется: «вычитания из тела — только два адресных трима: латеральный #271 и торцевой #310»; правка фиксируется в docs/WALL-THICKNESS.md. + +### 2.3 Узлы ≥3 лучей и всё остальное — не меняются + +Веера с фаской 1.5·h (#309), их мутанты и сцены `junction-309-step/hump-dark`, 16 junction-сцен, owner-repro — байтово прежние. Механизм #249 (`MULTI_WALL_JOIN_LIMIT`, `multiWallBevelCutsAt`, mitre контуров комнат) не тронут. Превью цепочки использует ту же парную логику — форма превью и персиста совпадает. + +## 3. Скоуп и не-скоуп + +**Скоуп:** парная ветка `linearWallJoinPatches`, новый торцевой трим и его прокладка в потребителях парных патчей (`physicalBodyParts`, превью, карта), golden `junction-309-spike-dark`, юниты/мутанты, docs/WALL-THICKNESS.md. +**Не-скоуп:** веера и фаска #309, узловая карта, #249-механизм, #271-трим, модель данных, hit-зоны, снап, touch (форма кладки, слушателей нет). + +## 4. UX, данные, i18n + +Нет новых контролов; нет миграций и полей; нет строк i18n. Меняется только форма кладки у парных узлов на всех поверхностях (общий структурный кэш). + +## 5. Риски + +1. **Возврат «хвостов»**: полный mitre на почти-параллельных встречных парах формально даёт очень длинное остриё — это осознанное решение владельца (стены, сходящиеся под 5°, сходятся далеко); вырожденные пары (`hit` нет) сохраняют бевел. +2. **Новое вычитание** — дыры на границе среза: гейт — детектор дыр (#302) на фикстуре и сценах; юнит на неизменность тела вне окрестности узла. +3. **Двойной трим** (#271 + #310) в одном узле — интерференция; покрыть юнитом на коротком толстом саппорте пары. + +## 6. Release-артефакты + +CHANGELOG RU+EN (User-Visible: yes), docs/WALL-THICKNESS.md (§ mitre-joint пары + инвариант о двух тримах), fingerprint скриншотов, пересъёмка `junction-309-spike-dark` (`--reviewed`). + +## 7. AC + +1. **Остриё**: на паре 10+20 (фикстура `309-junction-teeth`) патч — полный mitre `[node, pA, hit, pB]`, вершина на пересечении граней (юнит формы; вылет больше 1.5·h — юнит доказывает отсутствие фаски). +2. **Зубец срезан**: контур узла-двойки — чистый клин; проба в прежней зоне зубца (снаружи грани тонкой, внутри прежнего тела толстой у торца) пуста (юнит по `physicalBodySet.geometry` + пересмотренная сцена `junction-309-spike-dark`). +3. **Адресность**: тело толстой стены на расстоянии > 2·halfDepth от узла байтово прежнее (юнит сравнения колец). +4. **Узлы ≥3 не тронуты**: `junction-309-step/hump-dark` и 16 junction-сцен + owner-repro байтово прежние (golden verify). +5. **Без дыр**: детектор `junctionContractHoles` пуст на фикстуре и сценах (юнит + смок). +6. **90° равных толщин**: патч байтово прежний, трим пуст (юнит #309 AC4 остаётся зелёным). +7. **Мутанты**: (a) фаска возвращена в парную ветку — красный; (b) торцевой трим отключён — красный; (c) трим не ограничен окрестностью узла (режет всё тело) — красный. Гварды с типовым прологом tsc+fix-test-build. + +## 8. План тестов + +Юниты: пара 10+20 (остриё+зубец+адресность), 90°, почти-параллельная пара (бевел-фолбэк), пара с коротким толстым саппортом (#271+#310), детектор на фикстуре. Мутанты §7.7. Golden: пересъёмка spike-сцены, verify остальных. + +## 9. Откат + +Один revert: чистая рендер-геометрия.