mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-04 05:41:34 +00:00
docs(spec): define finite multi-wall ray contract
Issue: #271 User-Visible: no
This commit is contained in:
@@ -0,0 +1,324 @@
|
||||
# Issue #271 — конечная длина лучей multi-wall узла
|
||||
|
||||
- Дата: 2026-08-23
|
||||
- Тип: bug · приоритет P1
|
||||
- Оценка: пользовательская ценность 10/10 · ценность для разработки 9/10 ·
|
||||
сложность 7/10 · риск 9/10
|
||||
- Issue: [#271](https://github.com/Matysh/houseplan-card/issues/271)
|
||||
- Ветка: `issue/271-finite-multiwall-rays`
|
||||
- Статус ТЗ: готово к ревью
|
||||
|
||||
Канонические документы: `docs/SCOPE.md`, `docs/ARCHITECTURE.md`,
|
||||
`docs/WALL-THICKNESS.md`, `docs/CONFIG-COMPATIBILITY.md`,
|
||||
`docs/USER-GUIDE.ru.md` и `docs/TESTING.md`.
|
||||
|
||||
Связанные, но не дублирующие задачи:
|
||||
[#249](https://github.com/Matysh/houseplan-card/issues/249),
|
||||
[#261](https://github.com/Matysh/houseplan-card/issues/261),
|
||||
[#270](https://github.com/Matysh/houseplan-card/issues/270) и
|
||||
[#273](https://github.com/Matysh/houseplan-card/issues/273).
|
||||
|
||||
## 1. Сценарий и персона
|
||||
|
||||
Администратор нажимает Optimize для реального плана и затем открывает Plan или
|
||||
View. У короткой ступени контура появляется длинный штрихованный торец без
|
||||
соответствующей осевой линии и конечного узла. В другом месте ложная кладка
|
||||
становится окклюдером и даёт тёмное пятно возле корректно вырезанной двери.
|
||||
|
||||
Семья видит архитектуру, которой нет в сохранённом плане. Это нарушает J1:
|
||||
план обязан правдиво показывать дом. Одновременно нарушен J6: одна каноническая
|
||||
геометрия должна обслуживать masonry, paper, floor, Iso и световые барьеры.
|
||||
|
||||
## 2. Подтверждённое воспроизведение
|
||||
|
||||
Два приватных plan-only экспорта владельца из `v1.67.0-beta.5` после Optimize
|
||||
прогнаны через production-цепочку `spaceModels()` →
|
||||
`prepareSpacePhysicalGeometryInputs()` → `wallIntervals()` →
|
||||
`buildMultiWallNodeMap()` → `wallBodiesGeometry()`.
|
||||
|
||||
| Экспорт / node, render units | Реальная длина ray | Текущий rebuild |
|
||||
|---|---:|---:|
|
||||
| 1 / `(420.833, 437.500)` | `12.500` | `110.000` |
|
||||
| 1 / `(887.500, 345.833)` | `1.381904` | `96.667` |
|
||||
| 2 / `(2404.167, 1245.833)` | `20.833` | `500.000` |
|
||||
| 2 / `(2404.167, 2308.333)` | `20.833` | `500.000` |
|
||||
| 2 / `(-354.167, 2087.500)` | `200.000` | `666.667` |
|
||||
| 2 / `(-354.167, 2287.500)` | `200.000` | `333.333` |
|
||||
|
||||
Во втором экспорте все 14 opening slots проверены по центру и по 41 точке вдоль
|
||||
оси. Final masonry в них отсутствует. Поэтому пятно в `2-1.png` не требует
|
||||
отдельного opening-cut бага: его источник — лишняя geometry/occlusion около
|
||||
проёма.
|
||||
|
||||
Первый экспорт также содержит две явные `wall_columns`. Они являются
|
||||
сохранёнными пользовательскими телами и не удаляются этой задачей. Regression
|
||||
фиксирует только площадь, которую multi-wall reconstruction добавляет дальше
|
||||
конца room-wall interval.
|
||||
|
||||
Приватные экспорты не входят в репозиторий. Для тестов из них выделяются
|
||||
минимальные обезличенные room/wall/opening fixtures с теми же длинами и
|
||||
отношениями толщин.
|
||||
|
||||
## 3. Подтверждённая причина
|
||||
|
||||
`buildMultiWallNodeMap()` получает конечные `WallInterval.a/b`, но после
|
||||
дедупликации сохраняет в `MultiWallNodeRay` только:
|
||||
|
||||
```ts
|
||||
{ u, halfDepth }
|
||||
```
|
||||
|
||||
Фактическая длина `hypot(b - a)` теряется. `bevelMultiWallBody()` затем
|
||||
реконструирует каждый ray прямоугольником с общей длиной:
|
||||
|
||||
```ts
|
||||
radius = MITRE_LIMIT * node.halfDepth;
|
||||
extent = radius * 2;
|
||||
```
|
||||
|
||||
При `MITRE_LIMIT = 4` это `8 × H`, где `H` — максимальная half-depth всего
|
||||
узла, а не длина конкретного ray. Mask ограничивает rebuild только квадратом
|
||||
вокруг node, но не реальным endpoint. Поэтому короткий ray становится длиннее,
|
||||
хотя `wallIntervals`, осевой overlay и сохранённый room polygon остаются
|
||||
короткими. Final masonry затем закономерно используется как общий occluder.
|
||||
|
||||
## 4. Что человек увидит до и после
|
||||
|
||||
**До:** короткая ступень или micro-segment может выглядеть как длинная стена;
|
||||
у неё нет оси и конечного snap-node, а её тень может лечь в дверной проём.
|
||||
|
||||
**После:** masonry, hatch, outline, paper и тени заканчиваются там же, где
|
||||
заканчивается реальный входящий wall interval. Сам T/X-стык остаётся замкнутым,
|
||||
а bounded bevel #249 не возвращает длинный mitre-spike.
|
||||
|
||||
Исправление работает при чтении плана; Save и повторный Optimize не требуются.
|
||||
Новых настроек и сообщений нет.
|
||||
|
||||
## 5. Scope
|
||||
|
||||
### Входит
|
||||
|
||||
- сохранение конечной длины каждого canonical ray в multi-wall node map;
|
||||
- детерминированная дедупликация сонаправленных физических intervals;
|
||||
- ограничение local body/paper reconstruction фактическими конечными rays;
|
||||
- согласование room masonry, final masonry, paper, clean floor, Static/Iso и
|
||||
light/sun barriers;
|
||||
- короткие rays рядом с opening, но без изменения opening association/cut;
|
||||
- pure unit, minimized real-topology fixture, mutation, targeted smoke и
|
||||
semantic golden evidence;
|
||||
- документация и оба changelog.
|
||||
|
||||
### Не входит
|
||||
|
||||
- удаление или изменение явных `wall_columns`, partitions и room drafts;
|
||||
- optimizer-cleanup micro-thickness island #273;
|
||||
- изменение `MITRE_LIMIT = 4`, `MULTI_WALL_JOIN_LIMIT = 1.25` или формы
|
||||
чрезмерного bevel #249;
|
||||
- общий фикс белых запертых полостей #272;
|
||||
- изменение room coordinates, snapping, wall keys, opening symbols/placement;
|
||||
- schema/model-version/backend/i18n/UI изменения.
|
||||
|
||||
## 6. Канонический контракт finite ray
|
||||
|
||||
### 6.1 Длина
|
||||
|
||||
Каждый положительный конечный `WallInterval` создаёт directed endpoint ray с
|
||||
`u`, `halfDepth` и `length > epsilon`. `length` измеряется в тех же render units,
|
||||
что `point`, до любого local rebuild. Node map остаётся immutable projection и
|
||||
не мутирует intervals.
|
||||
|
||||
Локальная полоса ray существует только на параметре `t ∈ [0, length]` плюс
|
||||
scale-relative boolean epsilon на границе. Ни mask, ни join radius не дают
|
||||
права продолжить её за `length`.
|
||||
|
||||
### 6.2 Co-directional duplicates
|
||||
|
||||
Shared interval может прийти от двух room owners. Полные физические дубликаты
|
||||
одной оси/endpoint схлопываются как сейчас. Если в одном направлении есть
|
||||
несколько коллинеарных intervals:
|
||||
|
||||
- `halfDepth` берётся как максимальная эффективная положительная толщина;
|
||||
- finite support является union реально существующих `[0, length]`, а не
|
||||
автоматически бесконечным ray;
|
||||
- для непрерывных co-directional supports от одного endpoint достаточно
|
||||
максимального `length`;
|
||||
- зазор нельзя перекрыть только потому, что более короткий/толстый duplicate
|
||||
присутствует рядом;
|
||||
- сортировка, room order, interval direction и duplicate count не меняют
|
||||
результат.
|
||||
|
||||
Точная структура (`length` либо bounded spans) остаётся техническим решением,
|
||||
если этот физический контракт доказан тестами.
|
||||
|
||||
### 6.3 Local reconstruction
|
||||
|
||||
`bevelMultiWallBody()` может перестраивать только intersection finite ray
|
||||
strips с действующей node mask и bounded paper/exterior envelope. Pairwise
|
||||
join material у математического node сохраняется, но никакой rectangle не
|
||||
пересекает плоскость конечного торца ray наружу.
|
||||
|
||||
Если другой независимый ray/body законно занимает точку за этим торцом, final
|
||||
union остаётся заполненным им. Тест finite ray сравнивает provenance/local
|
||||
piece либо fixture без другого владельца, а не требует пустоты там, где есть
|
||||
реальная пересекающаяся стена.
|
||||
|
||||
### 6.4 Paper, floor и physics
|
||||
|
||||
`roomGeom` и `paperGeom` не получают фасад/торец за finite endpoint. Clean floor
|
||||
не теряет площадь из-за выдуманной masonry. Final `geom`, hidden Iso и
|
||||
light/sun occluders используют тот же bounded result. Запрещён render-only или
|
||||
shadow-only workaround.
|
||||
|
||||
### 6.5 Failure isolation
|
||||
|
||||
Невалидная/nonfinite length исключает только кандидат ray/node и не гасит
|
||||
остальной план. Обязательные structural boolean failures продолжают fail-dark
|
||||
по действующему контракту #197/#199.
|
||||
|
||||
## 7. Compatibility, UX, security и performance
|
||||
|
||||
- Persisted config/layout/model version не меняются; вход не мутируется.
|
||||
- Legacy key-only walls и canonical endpoint walls используют одну длину из
|
||||
resolved `WallInterval`.
|
||||
- Optimize не нужен для runtime-исправления и не удаляет independent bodies.
|
||||
- Новых controls, keyboard/touch/focus/ARIA и locale keys нет.
|
||||
- Новых HA calls, permissions, URL/HTML и security surfaces нет.
|
||||
- Node map строится в существующем structural pass. State/theme/hover ticks не
|
||||
пересчитывают topology. Запрещён новый глобальный `O(E²)` проход; bounded
|
||||
supports вычисляются во время уже существующей endpoint aggregation.
|
||||
|
||||
## 8. Acceptance criteria и доказательства
|
||||
|
||||
### AC1. Node map не теряет конечную длину
|
||||
|
||||
Table-driven unit строит nodes с rays длиной `20.833`, `200` и длиннее `8H`.
|
||||
Публичный результат хранит finite support; duplicate owners, reversed
|
||||
intervals и permutations дают deep-equal map. Input arrays неизменны.
|
||||
|
||||
**Доказательство:** `test/wall-thickness.test.mjs`.
|
||||
|
||||
### AC2. Короткий ray не достраивается
|
||||
|
||||
Minimized node `(2404.167, 1245.833)` с vertical ray `20.833` и `H = 62.5`
|
||||
не содержит local/final masonry в устойчивом probe за endpoint, хотя старый
|
||||
`extent = 500` его заполняет. Внутри `[0, 20.833]` полоса и node core остаются.
|
||||
То же проверяется для `12.5 → не 110` при `cell_cm: 5`.
|
||||
|
||||
**Доказательство:** geometry unit с point/area coverage, не SVG string.
|
||||
|
||||
### AC3. Реальные длинные rays и join #249 не обрезаны
|
||||
|
||||
Ray длиннее local mask сохраняет material до границы mask; T/X node связан со
|
||||
всеми incident rays. Existing excessive-wedge probe #249 остаётся пустым,
|
||||
join-radius не увеличивается, zero/open ray не материализуется.
|
||||
|
||||
**Доказательство:** существующие #249/#261 units плюс finite-length matrix.
|
||||
|
||||
### AC4. Проём остаётся настоящим проёмом без ложной тени
|
||||
|
||||
Fixture с коротким perpendicular ray рядом с door подтверждает:
|
||||
|
||||
- весь opening slot пуст в final masonry;
|
||||
- finite ray не входит в slot/local probe, если его endpoint до slot;
|
||||
- light/sun occluder не содержит добавленной за endpoint площади;
|
||||
- opening tunnel/symbol contract не меняется.
|
||||
|
||||
**Доказательство:** geometry unit и targeted browser smoke с реальным SVG/
|
||||
shadow probe.
|
||||
|
||||
### AC5. Все поверхности используют finite result
|
||||
|
||||
Plan, View, kiosk, Static и hidden Iso не показывают stump за endpoint;
|
||||
room paper/fill/hover и clean floor не вырезают там ложную стену; Glow/sun
|
||||
barriers не создают пятно. Theme/HA state update сохраняют structural
|
||||
fingerprint и cache reuse.
|
||||
|
||||
**Доказательство:** targeted production-bundle smoke.
|
||||
|
||||
### AC6. Семантический visual gate
|
||||
|
||||
Golden scene содержит короткую осевую ступень и local crop. До pixel diff
|
||||
harness проверяет, что два probes за finite endpoint пусты в wall/paper и что
|
||||
probe внутри ray заполнен. Общая доля изменённых пикселей не является
|
||||
единственным gate. Linux artifact просматривается до baseline acceptance.
|
||||
|
||||
### AC7. Мутант ловит исходную регрессию
|
||||
|
||||
Mutation entry отбрасывает finite support или возвращает rectangle длиной
|
||||
`8H`. AC2 либо AC4 обязаны краснеть. Мутант выполняется, а не только
|
||||
регистрируется.
|
||||
|
||||
### AC8. Приватность, данные и детерминизм
|
||||
|
||||
Полные `1.json`/`2.json` и пользовательские названия не коммитятся. Fixture
|
||||
содержит только минимальную анонимную topology. Render не меняет config;
|
||||
повторный расчёт и повторный Optimize детерминированы.
|
||||
|
||||
### AC9. Локальные гейты реализации
|
||||
|
||||
- `npm run typecheck`;
|
||||
- `npm test`;
|
||||
- `npm run build` и byte-identical shipped bundles;
|
||||
- `node scripts/check-docs.mjs`;
|
||||
- `node scripts/smoke-select.mjs --base origin/dev --head HEAD` и все выбранные
|
||||
targeted smokes;
|
||||
- целевой mutation;
|
||||
- целевые semantic golden scenes.
|
||||
|
||||
Полные golden/smoke/performance и Linux HA harness остаются prerelease gates.
|
||||
|
||||
## 9. Ожидаемые файлы
|
||||
|
||||
Product code:
|
||||
|
||||
- `src/wall-thickness.ts`.
|
||||
|
||||
Доказательства:
|
||||
|
||||
- `test/wall-thickness.test.mjs` и обезличенная fixture при необходимости;
|
||||
- targeted smoke для finite multi-wall ray/occluder;
|
||||
- `demo/golden/matrix.mjs`, `demo/golden/harness.mjs` и matrix tests;
|
||||
- `scripts/mutation-gate.mjs`, smoke/mutation registries.
|
||||
|
||||
Документация:
|
||||
|
||||
- `docs/WALL-THICKNESS.md`, `docs/ARCHITECTURE.md`, `docs/TESTING.md`;
|
||||
- `docs/USER-GUIDE.md` и `docs/USER-GUIDE.ru.md`;
|
||||
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md`.
|
||||
|
||||
## 10. Release-артефакты
|
||||
|
||||
Implementation-коммит класса A имеет trailers `Issue: #271` и
|
||||
`User-Visible: yes`; оба changelog входят в тот же коммит. Изменившиеся
|
||||
golden/docs screenshots принимаются только из полного Linux CI artifact после
|
||||
визуального review штатными `*:accept -- --reviewed` командами. Перед beta
|
||||
обязательны полный golden, smoke, performance и exact-SHA Validate.
|
||||
|
||||
## 11. Риски и меры
|
||||
|
||||
| Риск | Мера |
|
||||
|---|---|
|
||||
| Короткая толщина одного duplicate обрежет длинную реальную стену | §6.2 и duplicate/permutation matrix. |
|
||||
| Вернётся mitre-spike #249 | Existing radius/excessive-wedge negative tests. |
|
||||
| Починится Plan, но останется тень | AC4/AC5 проверяют общий occluder. |
|
||||
| Внешний probe законно занят другой стеной | Minimized provenance-aware fixture и отдельные local/final assertions. |
|
||||
| Boolean epsilon создаст щель у endpoint | Probes с запасом и area-connected node assertions. |
|
||||
|
||||
## 12. Rollback
|
||||
|
||||
Откатывается finite-ray projection/reconstruction вместе с semantic tests,
|
||||
smoke/golden и документацией. Данных и миграций нет. Partial rollback только
|
||||
тестов запрещён: он снова сделает регрессию невидимой.
|
||||
|
||||
## 13. Принятые технические предположения
|
||||
|
||||
1. Внутреннее представление finite support (`length`, interval либо clipped
|
||||
polygon) выбирает автор реализации; продуктовый контракт задан §6.
|
||||
2. Тёмное пятно `2-1.png` не получает отдельную задачу: opening slot доказанно
|
||||
пуст, а лишняя geometry уже является общим light barrier.
|
||||
3. Явные `wall_columns` первого экспорта остаются данными плана. Их автоматическое
|
||||
удаление было бы отдельным продуктовым решением и не маскирует этот bug.
|
||||
4. Minimized fixtures достаточно для репозитория; приватные экспорты остаются
|
||||
локальным cross-check перед handoff.
|
||||
5. Новых продуктовых вопросов нет: конечное физическое тело обязано соблюдать
|
||||
конечный источник.
|
||||
Reference in New Issue
Block a user