mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 11:49:16 +00:00
325 lines
18 KiB
Markdown
325 lines
18 KiB
Markdown
# 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. Новых продуктовых вопросов нет: конечное физическое тело обязано соблюдать
|
||
конечный источник.
|