mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 11:49:16 +00:00
docs(spec): require hole-free multi-wall joins
Issue: #272 User-Visible: no
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
# Issue #272 — без белых полостей в multi-wall стыках
|
||||
|
||||
- Дата: 2026-08-23
|
||||
- Тип: bug · приоритет P1
|
||||
- Оценка: пользовательская ценность 10/10 · ценность для разработки 10/10 ·
|
||||
сложность 8/10 · риск 9/10
|
||||
- Issue: [#272](https://github.com/Matysh/houseplan-card/issues/272)
|
||||
- Ветка: `issue/272-no-multiwall-holes`
|
||||
- Статус ТЗ: готово к ревью
|
||||
|
||||
Канонические документы: `docs/SCOPE.md`, `docs/ARCHITECTURE.md`,
|
||||
`docs/WALL-THICKNESS.md`, `docs/USER-GUIDE.ru.md` и `docs/TESTING.md`.
|
||||
|
||||
Связанные задачи:
|
||||
[#249](https://github.com/Matysh/houseplan-card/issues/249),
|
||||
[#258](https://github.com/Matysh/houseplan-card/issues/258),
|
||||
[#261](https://github.com/Matysh/houseplan-card/issues/261),
|
||||
[#270](https://github.com/Matysh/houseplan-card/issues/270) и
|
||||
[#271](https://github.com/Matysh/houseplan-card/issues/271).
|
||||
|
||||
## 1. Сценарий и персона
|
||||
|
||||
Администратор открывает два реальных плана после Optimize в свежей beta.5. В
|
||||
обычных прямоугольных T/X-стыках между штрихованными полосами видны белые
|
||||
треугольники. Они выглядят как отверстия в стене, сохраняются в View и на
|
||||
разных масштабах и не соответствуют ни opening, ни room topology.
|
||||
|
||||
Семья не может доверять главной визуальной поверхности. Это прямое нарушение
|
||||
J1 и J6: физически непрерывный стык должен быть непрерывным у всех consumers,
|
||||
а тест не может принимать белую дыру только потому, что она мала относительно
|
||||
полного кадра.
|
||||
|
||||
## 2. Подтверждённое воспроизведение
|
||||
|
||||
Приватные экспорты `1.json` (`cell_cm: 5`, 12 degree-3+ nodes) и `2.json`
|
||||
(`cell_cm: 1`, 14 nodes) прогнаны точной production geometry. Скриншоты
|
||||
`1-1.png`, `1-3.png`, `2-2.png`, `2-3.png` показывают белые клинья на разных
|
||||
сочетаниях равной и смешанной толщины.
|
||||
|
||||
#261 исправила измеренный сектор около `(887.5, 550)` и добавила один
|
||||
`retainedWedgeProbe`. Но она не проверила полную окрестность каждого node.
|
||||
|
||||
#270 независимо измерила текущее состояние golden-сцены:
|
||||
|
||||
- старый baseline содержит четыре запертые пустые компоненты;
|
||||
- текущий код после #261 содержит две;
|
||||
- pixel diff составляет меньше допуска сцены, поэтому golden проходит;
|
||||
- добавляемый #270 inventory фиксирует `enclosedHoles: 2`, но сознательно не
|
||||
решает, правильны ли они.
|
||||
|
||||
Новый пользовательский репорт даёт продуктовый вердикт: видимых запертых
|
||||
фоновых полостей в непрерывном стыке быть не должно. При этом внешний фон за
|
||||
фаской и существующий discarded-wedge contract #249 должны остаться пустыми.
|
||||
|
||||
Полные пользовательские экспорты не коммитятся. В репозиторий попадают только
|
||||
минимизированные анонимные node fixtures для обоих grid scales.
|
||||
|
||||
## 3. Подтверждённая зона причины
|
||||
|
||||
`multiWallBevelTrianglesAt()` строит pairwise cut для последовательных rays,
|
||||
если старое пересечение offset-lines дальше `R = 1.25 × H`.
|
||||
`bevelMultiWallBody()`:
|
||||
|
||||
1. вычитает full-origin pairwise triangles из исходного body;
|
||||
2. независимо union-ит ray rectangles;
|
||||
3. вычитает bounded excessive triangles;
|
||||
4. добавляет tiny core;
|
||||
5. клипует local result к envelope и подменяет часть исходной masonry в mask.
|
||||
|
||||
`bevelMultiWallPaper()` применяет bounded triangles к paper отдельно. Эти
|
||||
операции локально разумны по точечным probes #249/#261, но не гарантируют, что
|
||||
границы нескольких pairwise cuts образуют один внешний bevel вместо
|
||||
изолированного треугольного hole. Masonry и paper могут также оставить разные
|
||||
локальные компоненты.
|
||||
|
||||
Корневая зона доказана; точная boolean-перестройка остаётся техническим
|
||||
решением реализации и должна выводиться из инварианта §6, а не подгоняться под
|
||||
один пиксельный baseline.
|
||||
|
||||
## 4. Что человек увидит до и после
|
||||
|
||||
**До:** между реальными лучами T/X-стыка виден белый треугольник, иногда с
|
||||
outline по краям, будто кладка разорвана.
|
||||
|
||||
**После:** стык читается как единое сплошное тело. Если внешний угол ограничен
|
||||
фаской #249, пустота лежит снаружи непрерывного контура и соединена с внешним
|
||||
фоном; она не становится запертой дырой внутри masonry/paper.
|
||||
|
||||
Новых UI, настроек и миграции нет. Исправление действует при чтении плана во
|
||||
всех режимах.
|
||||
|
||||
## 5. Scope
|
||||
|
||||
### Входит
|
||||
|
||||
- общий топологический инвариант для непрерывных degree-3+ wall nodes;
|
||||
- согласованная локальная реконструкция body и paper без запертых фоновых
|
||||
компонентов;
|
||||
- T и X, равные/смешанные толщины, `cell_cm: 1/5`, разные winding/order;
|
||||
- сохранение bounded exterior bevel и finite rays;
|
||||
- room masonry, final masonry, paper, clean floors/fills/hover, Static/Iso и
|
||||
light/sun barriers;
|
||||
- unit geometry inventory, browser semantic inventory, mutation, targeted
|
||||
smoke и golden crop;
|
||||
- интеграция с #270 и осознанное принятие новых baselines;
|
||||
- каноническая документация и оба changelog.
|
||||
|
||||
### Не входит
|
||||
|
||||
- изменение сохранённых rooms/walls/open spans/openings;
|
||||
- Optimize data cleanup #273;
|
||||
- удаление independent bodies;
|
||||
- повышение общего pixel-diff порога либо попытка поймать дыру только порогом;
|
||||
- отказ от `R = 1.25 × H` или возврат неограниченного mitre-spike;
|
||||
- округление/скругление bevel, новый визуальный стиль стен;
|
||||
- backend, schema/model version, i18n и UI.
|
||||
|
||||
## 6. Геометрический контракт
|
||||
|
||||
### 6.1 Required solid и внешний фон
|
||||
|
||||
Для canonical node степени `3+` берутся конечные положительные physical rays.
|
||||
Zero-depth, virtual/open rays material не добавляют. Required local solid —
|
||||
связная область конечных ray strips и допустимого node join внутри canonical
|
||||
paper/exterior envelope.
|
||||
|
||||
Из required solid можно удалить excessive mitre area за `R`, только если
|
||||
результат образует внешний bevel: удалённая область должна быть связана с
|
||||
внешним фоном относительно локального physical contour. Cut не может оставить
|
||||
закрытый пустой остров между полосами стен.
|
||||
|
||||
Следствия:
|
||||
|
||||
- node core и каждый incident positive ray area-connected;
|
||||
- local masonry не содержит polygon hole, целиком окружённый required solid;
|
||||
- straight bevel может иметь discarded-wedge probe #249 снаружи contour;
|
||||
- отсутствие hole не означает заливку всего квадратного mask;
|
||||
- область вне finite ray/support #271 не материализуется.
|
||||
|
||||
### 6.2 Masonry и paper
|
||||
|
||||
`roomGeom`, final `geom` и `paperGeom` имеют одну physical boundary около node.
|
||||
Paper покрывает canonical masonry и room centre, но не маскирует masonry-hole
|
||||
белой накладкой. Final independent bodies/opening cuts сохраняют действующий
|
||||
порядок и не используются для сокрытия дефекта room-wall join.
|
||||
|
||||
### 6.3 Семантическое определение hole
|
||||
|
||||
Тест не равняет «любую пустую точку в окне» дыре. Для каждого fixture/node:
|
||||
|
||||
1. задаётся bounded local window, пересекающее все incident strips за join;
|
||||
2. классифицируются physical fill и пустота реальным geometry/SVG API;
|
||||
3. пустые samples, связанные с границей окна или с объявленным exterior
|
||||
sector, считаются внешним фоном;
|
||||
4. оставшиеся связные компоненты внутри required-solid envelope — holes;
|
||||
5. ожидаемое число holes для непрерывного T/X node равно нулю.
|
||||
|
||||
Pure vector test дополнительно проверяет polygon rings/area, browser inventory
|
||||
проверяет фактический path после рендера. Raster pixel threshold не заменяет ни
|
||||
одну из этих проверок.
|
||||
|
||||
### 6.4 Clean floor и physics
|
||||
|
||||
Clean floor не присваивает площадь стены комнате, а light/sun barrier не имеет
|
||||
прохода там, где masonry сплошная. Все consumers используют один canonical
|
||||
node; отдельный white patch поверх SVG запрещён.
|
||||
|
||||
### 6.5 Детерминизм и failure isolation
|
||||
|
||||
Rooms/walls order, reversed endpoints, winding и duplicate shared owner не
|
||||
меняют geometry/hole inventory. Ошибка одного optional node изолируется по
|
||||
контракту #197; обязательный structural boolean failure остаётся fail-dark.
|
||||
|
||||
## 7. Compatibility, UX, touch, security и performance
|
||||
|
||||
- Config/layout/model version не меняются, вход не мутируется.
|
||||
- Legacy/canonical wall keys #258 дают одинаковую topology.
|
||||
- Новых controls, i18n, focus/keyboard/touch/ARIA нет.
|
||||
- Новых HA данных, URL/HTML, permissions и security boundaries нет.
|
||||
- Semantic inventory исполняется в tests/golden harness, а не на каждом
|
||||
production frame. Product geometry остаётся в cached structural pass без
|
||||
нового state-tick traversal и без глобального pairwise `O(E²)`.
|
||||
|
||||
## 8. Acceptance criteria и доказательства
|
||||
|
||||
### AC1. Все реальные классы node не имеют запертых holes
|
||||
|
||||
Minimized fixtures из обоих экспортов содержат как минимум:
|
||||
|
||||
- rectilinear T с тремя равными half-depth;
|
||||
- T со смешанной толщиной;
|
||||
- X/degree-4;
|
||||
- короткий incident ray после/вместе с finite-ray #271;
|
||||
- normalized `cell_cm: 5` и high-resolution `cell_cm: 1`.
|
||||
|
||||
Для каждого node vector inventory даёт `0` enclosed holes в `roomGeom`, final
|
||||
masonry и paper; join связан со всеми rays.
|
||||
|
||||
**Доказательство:** `test/wall-thickness.test.mjs` и анонимная fixture.
|
||||
|
||||
### AC2. #249 остаётся ограниченным внешним bevel
|
||||
|
||||
Existing `discardedWedgeProbe` остаётся пустым; geometry не выходит дальше
|
||||
`R + epsilon`; пустая область probe связана с внешним сектором, а не заперта
|
||||
внутри стены. `MITRE_LIMIT`/`MULTI_WALL_JOIN_LIMIT` не меняются без отдельного
|
||||
решения.
|
||||
|
||||
### AC3. #261 и #271 не регрессируют
|
||||
|
||||
`retainedWedgeProbe` #261 заполнен в masonry/paper и не становится clean floor.
|
||||
Finite short ray не удлиняется ради закрытия hole. Zero-depth interval не
|
||||
материализуется. Existing #197 failure-isolation остаётся зелёным.
|
||||
|
||||
### AC4. Browser surfaces показывают один непрерывный узел
|
||||
|
||||
Targeted production-bundle smoke проверяет hole inventory настоящим
|
||||
`isPointInFill()` для Plan/View/kiosk и Static. Hidden Iso source и light/sun
|
||||
barriers не имеют прохода/разрыва. Theme, hover и HA update не меняют
|
||||
structural fingerprint.
|
||||
|
||||
### AC5. Golden больше не может принять белый клин
|
||||
|
||||
Для `multiwall-junction-bevel-view-dark` и новых minimized real scenes:
|
||||
|
||||
- semantic inventory выполняется до pixel comparison;
|
||||
- `enclosedHoles`/эквивалентный contract равен нулю для перечисленных nodes;
|
||||
- добавление одного локального hole переводит сцену в error независимо от его
|
||||
доли кадра;
|
||||
- stale pre-fix baseline не принимается автоматически.
|
||||
|
||||
Если #270 уже слита, её временное значение `2` заменяется product contract, а
|
||||
не сохраняется ради зелёного теста. Если не слита, реализация переносит её
|
||||
полезный inventory без дублирующих механизмов.
|
||||
|
||||
### AC6. Мутант воспроизводит слепой класс
|
||||
|
||||
Mutation возвращает текущую pairwise реконструкцию/один из holes. AC1 или AC5
|
||||
обязаны падать. Отдельно доказывается, что single retained/discarded probes
|
||||
могут остаться зелёными, то есть новый inventory действительно сильнее.
|
||||
|
||||
### AC7. Privacy и determinism
|
||||
|
||||
Полные пользовательские экспорты и имена комнат не коммитятся. Permutations,
|
||||
reversed intervals, повторный расчёт и оба coordinate scales дают одинаковые
|
||||
semantic outcomes без мутации input.
|
||||
|
||||
### AC8. Локальные гейты реализации
|
||||
|
||||
- `npm run typecheck`;
|
||||
- `npm test`;
|
||||
- `npm run build` и bundle parity;
|
||||
- `node scripts/check-docs.mjs`;
|
||||
- `node scripts/smoke-select.mjs --base origin/dev --head HEAD` и все выбранные
|
||||
targeted smokes;
|
||||
- mutation для enclosed hole;
|
||||
- targeted semantic golden verify.
|
||||
|
||||
Полные smoke/golden/performance и Linux HA harness — prerelease gates.
|
||||
|
||||
## 9. Ожидаемые файлы
|
||||
|
||||
Product code:
|
||||
|
||||
- `src/wall-thickness.ts`.
|
||||
|
||||
Tests/evidence:
|
||||
|
||||
- `test/wall-thickness.test.mjs`, minimized fixture;
|
||||
- targeted multiwall smoke;
|
||||
- `demo/golden/harness.mjs`, `matrix.mjs`, `run.mjs` и matrix unit;
|
||||
- `scripts/mutation-gate.mjs` и registries;
|
||||
- координация с веткой/реализацией #270 без параллельных inventory helpers.
|
||||
|
||||
Документация:
|
||||
|
||||
- `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-коммит имеет `Issue: #272`, `User-Visible: yes` и оба changelog.
|
||||
Golden baseline `multiwall-junction-bevel-view-dark` и новые scenes принимаются
|
||||
только из полного Linux artifact после независимого просмотра. При изменении
|
||||
visual fingerprint docs screenshots также переснимаются Linux workflow и
|
||||
принимаются штатной reviewed-командой. Перед beta обязательны полный golden,
|
||||
smoke, performance и exact-SHA Validate.
|
||||
|
||||
## 11. Риски и меры
|
||||
|
||||
| Риск | Мера |
|
||||
|---|---|
|
||||
| «Закрыть дыру» возвратом длинного spike | AC2 и exterior-connectivity contract. |
|
||||
| Залить внешний фон вокруг T | §6.3 различает exterior-connected emptiness и hole. |
|
||||
| Маскировать дефект только paper | AC1/AC4 проверяют masonry, paper, floor и barriers. |
|
||||
| Golden снова пройдет по общему порогу | Semantic inventory до pixel diff и mutant. |
|
||||
| Конфликт с #270/#271 | Один inventory implementation; рекомендуемый порядок merge: #271, затем #272, затем обновление #270 evidence. |
|
||||
|
||||
## 12. Rollback
|
||||
|
||||
Откатывается product geometry вместе с vector/browser inventory, mutation,
|
||||
golden baselines и документацией. Нельзя оставить baseline нового сплошного
|
||||
стыка при старом коде или временное `enclosedHoles: 2` после нового кода.
|
||||
Миграции данных нет.
|
||||
|
||||
## 13. Принятые технические предположения
|
||||
|
||||
1. Правильный критерий — не «закрасить любой белый пиксель», а исключить
|
||||
interior-connected hole при сохранении внешнего bounded bevel.
|
||||
2. Конкретный boolean algorithm выбирается реализацией; тестовый topology
|
||||
contract обязателен.
|
||||
3. #270 используется как инфраструктурная основа, но её временное ожидаемое
|
||||
число не является продуктовым решением.
|
||||
4. #271 рекомендуется слить первой, чтобы hole repair работал на конечных rays
|
||||
и не закреплял ложные длинные rectangles.
|
||||
5. Продуктовых вопросов нет: владелец явно признал видимые белые треугольники
|
||||
артефактами, а сохранение exterior clamp уже зафиксировано #249.
|
||||
Reference in New Issue
Block a user