diff --git a/docs/specs/302-junction-node-material.md b/docs/specs/302-junction-node-material.md index 55d4e17b..58a7c9a2 100644 --- a/docs/specs/302-junction-node-material.md +++ b/docs/specs/302-junction-node-material.md @@ -61,6 +61,12 @@ 2. **Отдельный полноценный сет тестов со скриншотами стыков крупным планом**: разное число лучей, толщины, углы, виртуальные стены и т.д. 3. Детектор дыр как объективный инвариант — в дополнение к скриншотам. +4. **Фаска #249 на внешних углах узлов сохраняется** (решение в ходе + реализации, 2026-08-25): узлы ≥3 лучей остаются чуть срезанными, как + утверждено в #249, а не превращаются в острые mitre-углы. Следствие для + контракта: чисто аддитивной модель быть не может — фаска по природе + вычитающая; гарантию «нет дыр» несёт strip-safe построение среза (§8.2) + плюс детектор, а не отсутствие вычитаний. ## 5. Цели @@ -96,21 +102,27 @@ `EPS_NODE = openEps × 4` (текущий допуск `buildMultiWallNodeMap` — сохраняется). Луч приходит в узел со своей полу-толщиной; лучи упорядочиваются по азимуту. -### 8.2. Аддитивное построение — единственное правило узла +### 8.2. Правило узла: веер + strip-safe фаска из одного углового обхода -Материал узла = union из: +Для каждой пары соседних по азимуту лучей (сектор ≤ 180°; рефлексные секторы — +внешняя сторона выпуклого угла — пропускаются целиком): -1. тел лучей (прямоугольные полосы до узла, как сейчас); -2. **углового веера между каждой парой соседних по азимуту лучей**: пересечение - офсетных прямых двух лучей на обращённой друг к другу стороне даёт - mitre-точку; веер — полигон «узел → конец полосы A → mitre → конец полосы B». - Если mitre-точка дальше `MITRE_LIMIT × max(толщин)` от узла или прямые - параллельны — вместо неё хорда между концами полос (bevel). Правило одно и - то же для внешней и внутренней стороны угла и для любого числа лучей. +1. **Веер (аддитивно):** полигон «узел → край полосы A → mitre → край полосы B», + где mitre — пересечение обращённых в сектор офсетных прямых. Если mitre + дальше **лимита узла** (`node.limit = MULTI_WALL_JOIN_LIMIT × halfDepth`, + утверждённая граница #249) — вместо mitre хорда bevel на радиусе лимита. +2. **Фаска (вычитающе, strip-safe):** клин за хордой (треугольник + «конец A → mitre → конец B») минус **квады всех лучей этого узла** — ещё на + этапе построения, до какого-либо `difference` по телу. Срез физически не + может задеть материал полос: у острых стыков, где клин перекрывается с + полосами, срезается только его свободная часть, и стык остаётся сплошным — + это и есть исправление артефактов из issue. У ортогональных T/X клин лежит + вне полос, и фаска выглядит ровно как в #249. -**В узловой механике нет ни одной операции `difference`.** Дыра между телами -лучей невозможна по построению: каждый сектор между соседними лучами накрыт -веером. «Лишний» материал ограничен тем же `MITRE_LIMIT`, что и сегодня. +Инвариант вместо «нет difference»: **тело ⊇ полосы лучей ∪ веера** — материал +контракта не может быть удалён ничем, включая фаску (она strip-safe по +построению и не пересекается с веером — их разделяет общая хорда). Проверяется +детектором §8.4. ### 8.3. Спец-случаи @@ -124,10 +136,13 @@ ### 8.4. Инвариант «нет дыр» — формальный -Точка p — «внутренняя дыра», если p ∉ материал, и лучи из p в ≥5 из 8 -направлений (шаг 45°) пересекают материал в радиусе `2.2 × gridPitch`. -Детектор: сетка проб шагом `0.2 × gridPitch` в радиусе `5 × gridPitch` от узла. -Ноль внутренних дыр — обязательство для каждого узла каждой сцены сета §13. +Уточнение по факту реализации (первая формулировка «окружено кладкой с ≥5 из +8 сторон» ложно срабатывала на легитимном полу комнаты в острых внутренних +углах — за inset-mitre): детектор проверяет **контрактное покрытие**. Проба p — +«дыра», если p ∈ (полоса какого-либо луча узла ∪ веер узла) и p ∉ тело. Сетка +проб шагом `0.2 × gridPitch` в радиусе `MITRE_LIMIT × halfDepth + halfDepth` от +узла; принадлежность полосе — строго внутри квада (0 ≤ t ≤ длина, |перпендикуляр| +< half − ε). Ноль дыр — обязательство для каждого узла каждой сцены сета §13. ### 8.5. Что не меняется наружно @@ -167,12 +182,13 @@ исчезли (golden-сцена репро). - **AC3.** Ортогональные стыки одинаковой толщины: попиксельное совпадение с текущими эталонами. -- **AC4.** Острый угол 57° со стенами 50/70: сплошная кладка, фаска в пределах - `MITRE_LIMIT`. +- **AC4.** Острый угол 57° со стенами 50/70: сплошная кладка (перекрытие полос + не срезается), свободная часть клина срезана по лимиту узла. - **AC5.** Виртуальный участок в узле не порождает кладку; соседние физические лучи смыкаются веером через него. -- **AC6.** Узловая механика не содержит `difference` — проверяется чтением и - мутантом на возврат вычитающего слоя. +- **AC6.** Единственные вычитания узловой механики — strip-safe фаски §8.2: + срез строится уже за вычетом квадов лучей узла. Мутант убирает вычет квадов + из среза — детектор обязан покраснеть. - **AC7.** Оба рендерера дают идентичную геометрию узла (один вызов `wallBodiesUnionPath`; смок сверяет пути). - **AC8.** Перф: `smoke_render_perf` в бюджете; large-house без деградации. @@ -206,7 +222,7 @@ | `node-fan-outer-only` | веер только с внешней стороны | детектор | | `node-fan-ignores-mitre-limit` | mitre без лимита | юниты веера | | `node-fan-includes-zero-ray` | нулевой луч порождает кладку | юниты + смок | -| `node-difference-back` | возврат вычитающего разреза в узел | детектор + AC6 | +| `node-cut-not-strip-safe` | срез фаски без вычета квадов лучей | детектор + AC6 | | `hole-detector-blind` | детектор всегда зелёный | самопроверка детектора на заведомо дырявой фикстуре | Все юнит-гварды — с пересборкой `test-build` (урок #230/#235).