feat: junction nodes — detector, fans over the approved chamfer, scene set (#302)

Вторая половина переработки узлов поверх ядра из прошлого коммита:

- `junctionContractHoles` — объективный инвариант «тело ⊇ полосы ∪ веера в
  фасадной границе» как экспортная чистая функция; самопроверка на заведомо
  дырявой фикстуре входит в юниты. Первая формулировка детектора из ТЗ
  («окружено кладкой с ≥5 из 8 сторон») уточнена в §8.4 по факту измерения:
  она ложно флагует легитимный пол комнаты в острых внутренних углах.
- `junctionNodeBound` — «гладкая» фасадная граница (конверт с обычными углами)
  экспортирована: ею клиппуются куски узла и ею же пользуются тесты.
- юниты #302: веера/рефлекс/короткий толстый саппорт/детектор/репро
  end-to-end; хелпер тестов старого контракта переведён на strip-safe
  семантику по саппортам и фасадной границе.
- смок `smoke_junction_holes`: контрактные пробы считаются в node из той же
  фикстуры и проверяются в браузере по реальному `d`-пути карточки.
- сет из 16 golden-сцен стыков крупным планом (звёзды лучей: T/X/Y/острые/
  почти коллинеарные/виртуальные/колонна/черновик) + сцена-репро владельца;
  билдер сцен и `zoomCenter` в harness. Контракт сцены
  `multiwall-junction-bevel-view-dark` инвертирован по решению владельца:
  проба в перекрытии полос обязана быть ЗАПОЛНЕНА (strip-safe), а не пустой.
- семь мутантов §14, включая слепоту детектора и невозврат саппортов.
- фикстура #197: площадь кладки выросла на 0.2 юнита² — слайвер вееров вдоль
  хорд фаски; константа обновлена с комментарием.

Эталоны новых сцен идут отдельным коммитом с положенными трейлерами.

Issue: #302
User-Visible: yes
This commit is contained in:
Codex
2026-08-25 17:34:19 +03:00
parent a1361974a3
commit 76d60b3e1b
24 changed files with 1160 additions and 598 deletions
+7
View File
@@ -2,6 +2,13 @@
## Unreleased
- Wall junctions no longer leave white wedges and notches. A node where three
or more walls meet is now completed with sector fans and the exact strips of
its rays on top of the existing chamfer: an acute junction is solid masonry,
and the chamfer cut survives only where it does not touch the walls
themselves. The no-holes invariant is checked automatically on a set of 16
close-up junction scenes and on the plan from the original report
([#302](https://github.com/Matysh/houseplan-card/issues/302)).
- Every Plan editor tool now shows the same complete set of wall axes and
structural endpoints. Switching from Walls to Thickness no longer hides room
axes, while each tool keeps its own hints and live preview
+7
View File
@@ -8,6 +8,13 @@
## Не выпущено
- Стыки стен больше не оставляют белых клиньев и зазубрин. Узел, где сходятся
три и больше стен, теперь достраивается угловыми веерами и точными полосами
своих лучей поверх прежней фаски: острый стык — сплошная кладка, срез фаски
остаётся только там, где он не задевает сами стены. Инвариант «дыр нет»
проверяется автоматически на сете из 16 крупноплановых сцен стыков и на
плане из исходного сообщения
([#302](https://github.com/Matysh/houseplan-card/issues/302)).
- Во всех инструментах редактора Плана теперь отображается один и тот же полный
набор осевых линий и узлов стен. При переходе из «Стены» в «Толщина» оси
комнат больше не исчезают, а подсказки и предпросмотр активного инструмента
+15
View File
@@ -185,6 +185,21 @@ excluded from this shared `roomGeom`, so a door does not change the room fill
and a detached body cannot punch it. Full and Static render paths reuse the same
structural cache instead of rebuilding wall booleans once per room.
**Junction nodes (#302).** A degree-3+ node keeps the approved #249 chamfer
(`bevelMultiWallBody`, bounded by the node's join limit
`MULTI_WALL_JOIN_LIMIT × halfDepth`), and AFTER it the node additively gets
back what a chamfer must never eat: the exact support quads of its rays (each
bounded by its own finite length, so a trimmed lateral phantom — #271 — cannot
come back) and one sector fan per pair of angularly adjacent rays
(`junctionNodeGeometry`): mitre up to the node limit, bevel chord past it,
reflex sectors skipped. The pieces are clipped by the plain-corner facade
bound (`junctionNodeBound`), so a node can never grow new facade at a concave
vertex. The result is strip-safe: an acute junction is solid masonry, and the
objective invariant «body ⊇ support strips ∪ fans, inside the facade bound» is
machine-checked by `junctionContractHoles` in tests and the
`smoke_junction_holes` wiring probe. Degenerate zero-area rings left by
coincident chords are dropped.
**Hatch density is physical (#230).** The pattern step is a distance on the
plan, not a count of coordinate units: `wallHatchStepUnits(cellCm)` returns
`8 × (5 / cell_cm)`, which is 9.6 cm at every grid scale and exactly the
Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 350 KiB

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 KiB

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 KiB

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 319 KiB

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 KiB

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 141 KiB

+21 -21
View File
@@ -2,7 +2,7 @@
"version": 1,
"fixture": "synthetic-only",
"chromium": "151.0.7922.34",
"sourceFingerprint": "03f814f86bf3582d85993d03efe835babf6f5e2d2c6401124588e4eb0a7b9278",
"sourceFingerprint": "4869370390a8aaa4066ea720457ccafc421b87da1879eda2e6273935a3a0f4d9",
"captureScriptSha256": "ce2e9542fed9dade3085be87d16f69adb2ac8262893ad78ad966b1b9673f2983",
"command": "npm run build && node demo/docs/capture.mjs",
"scenarios": {
@@ -14,8 +14,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "03f814f86bf3582d85993d03efe835babf6f5e2d2c6401124588e4eb0a7b9278",
"imageSha256": "c9e2203fe2df5c2627c861f92e9c4dfad585efb101df19e4b6388a6fbc27d01a"
"sourceSha256": "4869370390a8aaa4066ea720457ccafc421b87da1879eda2e6273935a3a0f4d9",
"imageSha256": "51551aa5237090b67ff4c48802b0f2518e884b48cb1b0319e782fced239953ea"
},
"view-touch": {
"file": "02-view-touch.png",
@@ -25,8 +25,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "03f814f86bf3582d85993d03efe835babf6f5e2d2c6401124588e4eb0a7b9278",
"imageSha256": "a544bd821460e5b9b7a83b1636ce6b6a3fc50be68fe4f74779d23ec9e12abe6a"
"sourceSha256": "4869370390a8aaa4066ea720457ccafc421b87da1879eda2e6273935a3a0f4d9",
"imageSha256": "e52f419772f86480b336a0ab63d0c781207101b262fdf73f5fd581527e47dce4"
},
"space-create": {
"file": "03-space-create.png",
@@ -36,8 +36,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "03f814f86bf3582d85993d03efe835babf6f5e2d2c6401124588e4eb0a7b9278",
"imageSha256": "6df7753f00b7d87cd35fd18fb8fbac0a2a7fa2e549902b3afee38f38c93a2ac0"
"sourceSha256": "4869370390a8aaa4066ea720457ccafc421b87da1879eda2e6273935a3a0f4d9",
"imageSha256": "329750c81a7a958c4fd105b2c80f85dce55ed87df9ddf0ea08084e4428e85f04"
},
"room-contour-close": {
"file": "04-room-contour-close.png",
@@ -47,8 +47,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "03f814f86bf3582d85993d03efe835babf6f5e2d2c6401124588e4eb0a7b9278",
"imageSha256": "0129b6a5f1f97fa5d29db0a30bfd899bcfe53308a8ba3872d2a794132fccc9cc"
"sourceSha256": "4869370390a8aaa4066ea720457ccafc421b87da1879eda2e6273935a3a0f4d9",
"imageSha256": "e9a847d17b9bd02ad9fa39a6e7194f980144e90538eb94413f770f9539ceb7bc"
},
"plan-context-tray": {
"file": "05-plan-context-tray.png",
@@ -58,8 +58,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "03f814f86bf3582d85993d03efe835babf6f5e2d2c6401124588e4eb0a7b9278",
"imageSha256": "7f243f578d2d286ae7911363571762e3910fe2117abc5253292d9ef2d578c8dc"
"sourceSha256": "4869370390a8aaa4066ea720457ccafc421b87da1879eda2e6273935a3a0f4d9",
"imageSha256": "7fd57bfc022211c5d37bd7c4a1a72b34ed7651fd577ada1d2314b64ae5061c21"
},
"device-editor": {
"file": "06-device-editor.png",
@@ -69,8 +69,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "03f814f86bf3582d85993d03efe835babf6f5e2d2c6401124588e4eb0a7b9278",
"imageSha256": "24710a46be38418a09c4bdb3db8750611affea593d80c91cfe74c105ef8ed201"
"sourceSha256": "4869370390a8aaa4066ea720457ccafc421b87da1879eda2e6273935a3a0f4d9",
"imageSha256": "0ef6798b79ff0b78e1947b1dba547b92cbdf11b79b6335cffb8a415158c09b5d"
},
"device-display-preview": {
"file": "06-device-display-preview.png",
@@ -80,8 +80,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "03f814f86bf3582d85993d03efe835babf6f5e2d2c6401124588e4eb0a7b9278",
"imageSha256": "1eaedd95180175f3db2c2e052bad70db63f6c70b296ec19c3d152ccbb81520ed"
"sourceSha256": "4869370390a8aaa4066ea720457ccafc421b87da1879eda2e6273935a3a0f4d9",
"imageSha256": "3842b3306d4e73a5b1d3475a96b4c1fd433c5c4334054a34d52f7cc0a20ae931"
},
"background-editor": {
"file": "07-background-editor.png",
@@ -91,8 +91,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "03f814f86bf3582d85993d03efe835babf6f5e2d2c6401124588e4eb0a7b9278",
"imageSha256": "460f098f7761a41c6b30e42940cb5c9d6f96966ba34e4df04ea273575c293e93"
"sourceSha256": "4869370390a8aaa4066ea720457ccafc421b87da1879eda2e6273935a3a0f4d9",
"imageSha256": "f91c9575404abc64c5f03bb7dc9283b80231ada6bfd88774cfdf7d53fb46e0b2"
},
"room-card": {
"file": "08-room-card.png",
@@ -102,8 +102,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "03f814f86bf3582d85993d03efe835babf6f5e2d2c6401124588e4eb0a7b9278",
"imageSha256": "66d8b0a509909822ff6b891a483bad9d2c72b20f08e287355883aa5205c6cee0"
"sourceSha256": "4869370390a8aaa4066ea720457ccafc421b87da1879eda2e6273935a3a0f4d9",
"imageSha256": "48a5685039a5e0de2a28190d857b193c86e2e63606e3c7a8c40fc9d2b190da4e"
},
"device-info": {
"file": "09-device-info.png",
@@ -113,8 +113,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "03f814f86bf3582d85993d03efe835babf6f5e2d2c6401124588e4eb0a7b9278",
"imageSha256": "29b23ec03b320c35b3b6d725b3e69dd7a2cfcd1607cfbbe01dceb37c316964c3"
"sourceSha256": "4869370390a8aaa4066ea720457ccafc421b87da1879eda2e6273935a3a0f4d9",
"imageSha256": "83debe3fc2853406f8bc37df55d162a0b0dadc8c4e997dda4db83c07ca25caab"
}
}
}