mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 19:58:50 +00:00
@@ -0,0 +1,409 @@
|
||||
# Issue #261 — белые клинья в T-стыках после ограниченного multi-wall bevel
|
||||
|
||||
- Дата: 2026-08-23
|
||||
- Тип: bug · приоритет P1
|
||||
- Оценка: пользовательская ценность 8/10 · ценность для разработки 8/10 ·
|
||||
сложность/риск 7/10
|
||||
- Issue: [#261](https://github.com/Matysh/houseplan-card/issues/261)
|
||||
- Ветка: `issue/261-white-wedges-root-cause`
|
||||
- Статус ТЗ: первая редакция, ожидает ревью
|
||||
|
||||
Канонические документы: `docs/SCOPE.md`, `docs/USER-GUIDE.ru.md`,
|
||||
`docs/ARCHITECTURE.md`, `docs/WALL-THICKNESS.md`,
|
||||
`docs/CONFIG-COMPATIBILITY.md`, `docs/TOUCH-SUPPORT.md` и
|
||||
`docs/TESTING.md`.
|
||||
|
||||
Связанные, но не дублирующие задачи:
|
||||
[#197](https://github.com/Matysh/houseplan-card/issues/197),
|
||||
[#249](https://github.com/Matysh/houseplan-card/issues/249),
|
||||
[#253](https://github.com/Matysh/houseplan-card/issues/253) и
|
||||
[#258](https://github.com/Matysh/houseplan-card/issues/258).
|
||||
|
||||
## 1. Сценарий и персона
|
||||
|
||||
Администратор дома открывает в `v1.67.0-beta.4` обычный сохранённый план с
|
||||
толстыми стенами разной толщины. В нескольких T-образных стыках вместо единого
|
||||
тела кладки видны белые треугольные клинья. Они остаются после reload и видны
|
||||
не только администратору в Plan, но также семье в View, kiosk и Static.
|
||||
|
||||
Это нарушает J1 из `docs/SCOPE.md`: план перестаёт правдиво показывать
|
||||
физические стены дома. Исправление также обслуживает J6: все поверхности должны
|
||||
получать одну каноническую геометрию, а не отдельную визуальную заплатку.
|
||||
|
||||
## 2. Подтверждённое воспроизведение
|
||||
|
||||
Один и тот же реальный экспорт владельца выполнен production-проекцией
|
||||
`spaceModels()` → `prepareSpacePhysicalGeometryInputs()` →
|
||||
`wallBodiesGeometry()` на `v1.67.0-beta.3`, `v1.67.0-beta.4` и текущем `dev`.
|
||||
Вход содержит 8 комнат, 24 записи толщины, 3 виртуальных участка, 15 проёмов и
|
||||
3 независимых физических тела.
|
||||
|
||||
Результат сравнения:
|
||||
|
||||
- `wallIntervals` beta.3/beta.4 совпадает побайтово;
|
||||
- `wallEdgeBodies` beta.3/beta.4 совпадает побайтово;
|
||||
- `buildMultiWallNodeMap()` beta.4 находит 12 физических degree-3 узлов;
|
||||
- `paperGeom` beta.3 имеет площадь `727303.8201095833`, а beta.4/current dev —
|
||||
`727248.4381651384`;
|
||||
- из beta.4 исчез один реальный треугольник площадью `55.3819444449`
|
||||
render-unit² с вершинами около `(887.5, 559.1667)`, `(899.5833, 550)` и
|
||||
`(899.5833, 559.1667)`;
|
||||
- безопасная внутренняя точка исчезнувшего тела: `(895.5, 556.0)` в
|
||||
production render coordinates, или `(0.8955, 0.5560)` в сохранённой
|
||||
нормализованной конфигурации.
|
||||
|
||||
Этот узел уже присутствует в обезличенной фикстуре
|
||||
`test/fixtures/197-junction-patch.json`, поэтому пользовательский экспорт в
|
||||
репозиторий не добавляется. Фикстура #197 содержит 8 комнат, 25 wall entries и
|
||||
3 virtual cuts; дополнительная ULP-запись не меняет сам проблемный узел.
|
||||
|
||||
## 3. Подтверждённая причина
|
||||
|
||||
Первый дефектный SHA — коммит #249 `29904df` (`fix: bound multi-wall junction
|
||||
bevels`). Уже на нём masonry теряет `495.579151` render-unit² относительно
|
||||
beta.3, а paper — указанный треугольник `55.381944`. Изменения #253 на этом SHA
|
||||
ещё отсутствуют и причиной не являются.
|
||||
|
||||
У #249 есть два разных варианта одной bevel-формулы:
|
||||
|
||||
1. `multiWallBevelTriangles(map)` оставляет физическое пересечение лучей до
|
||||
`R = 1.25 × H` и возвращает только малый excessive wedge за этим радиусом;
|
||||
2. внутренний `multiWallBevelTrianglesAt(map, false)` начинает cut от исходных
|
||||
offset-точек граней и возвращает весь треугольник до старой mitre-вершины.
|
||||
|
||||
`bevelMultiWallPaper()` применяет второй вариант, затем делает
|
||||
`union(centre, beveled)`. Room centre возвращает только площадь внутри
|
||||
объединения комнат; наружная половина физической стены не возвращается.
|
||||
`bevelMultiWallBody()` использует тот же полный cut перед локальной
|
||||
реконструкцией, но reconstructed ray strips клипуются к `centre`, поэтому их
|
||||
внешняя часть также теряется. Алгоритм, предназначенный для удаления длинного
|
||||
mitre-зуба, вырезает допустимое пересечение двух положительных физических
|
||||
лучей.
|
||||
|
||||
Общая грань `0.620833,0.545833@1.5706` без толщины из исходной гипотезы не
|
||||
причастна: её `half = 0`, она не входит в degree-3 карту и находится у
|
||||
`(620.833, 545.833)`, а подтверждённый клин — у `(887.5, 550)`.
|
||||
|
||||
## 4. Что человек увидит до и после
|
||||
|
||||
**До:** у T-стыка положительных стен разной толщины штрихованное тело и бумага
|
||||
теряют треугольный сектор между двумя реальными лучами. Через него виден белый
|
||||
фон/пол, словно в кладке есть отверстие.
|
||||
|
||||
**После:** те же сохранённые комнаты и толщины образуют одно непрерывное тело
|
||||
без белого клина. Старый чрезмерный mitre-зуб по-прежнему ограничивается прямой
|
||||
фаской за пределом `1.25 × H`; исправление не возвращает выступ #249 и не
|
||||
округляет стык. Результат одинаков в Plan, View, kiosk, Static и скрытой
|
||||
изометрии, а свет не проходит сквозь восстановленную кладку.
|
||||
|
||||
Новых сообщений, настроек и ремонтных действий нет. План исправляется при
|
||||
чтении без Save или Optimize.
|
||||
|
||||
## 5. Scope
|
||||
|
||||
### Входит
|
||||
|
||||
- устранение полного destructive cut для положительных ray strips в
|
||||
degree-3+ room-wall nodes;
|
||||
- сохранение физического пересечения лучей вплоть до действующего
|
||||
`R = 1.25 × H` внутри и снаружи room-centre union;
|
||||
- согласованные `roomGeom`, final masonry и `paperGeom` без клина;
|
||||
- Full/Plan/View/kiosk, Static, hidden Iso, clean floor/room fill/hover и
|
||||
Glow/sun/light-barrier consumers одной геометрии;
|
||||
- regression на существующей обезличенной фикстуре #197;
|
||||
- усиление unit, browser smoke, golden semantic gate и mutation gate;
|
||||
- обновление канонических документов, пользовательского руководства и двух
|
||||
changelog.
|
||||
|
||||
### Не входит
|
||||
|
||||
- изменение `R = 1.25 × H`, `MITRE_LIMIT = 4` или прямой формы фаски #249;
|
||||
- возвращение старых неограниченных mitre-зубов beta.3;
|
||||
- изменение wall-key/stored-endpoint ремонта #258;
|
||||
- добавление толщины нулевой общей грани у `(620.833, 545.833)`;
|
||||
- изменение interval classification, shared/outer/virtual semantics,
|
||||
толщины стен, openings, partitions, drafts или columns;
|
||||
- изменение сохранённых rooms/walls/open spans либо новый Optimize-pass;
|
||||
- новый UI, i18n, backend, schema/model/store version или WebSocket API.
|
||||
|
||||
## 6. Геометрический контракт
|
||||
|
||||
### 6.1 Физическая область multi-wall node
|
||||
|
||||
Для каждого canonical node степени `3+` используются существующие уникальные
|
||||
положительные rays. Повторы shared interval и сонаправленные дубликаты остаются
|
||||
схлопнутыми; zero-thickness и virtual intervals материала не добавляют.
|
||||
|
||||
Локальное физическое тело node определяется union конечных полос всех rays.
|
||||
Из него разрешено удалить только ту часть pairwise mitre-overlap, которая
|
||||
лежит за `R = 1.25 × H`, где `H` — максимальная incident half-depth.
|
||||
|
||||
Полный треугольник от offset origins до старой mitre-вершины не является
|
||||
допустимым cut: его часть до `R` принадлежит физическим полосам и должна
|
||||
остаться заполненной. Это правило одинаково по обе стороны room-centre union.
|
||||
|
||||
### 6.2 Masonry и exterior
|
||||
|
||||
В bounded mask проблемного node canonical room masonry:
|
||||
|
||||
1. сохраняет complete positive ray strips;
|
||||
2. удаляет только excessive area за `R`;
|
||||
3. остаётся в допустимой exterior/paper envelope;
|
||||
4. имеет ненулевое заполнение в node centre и area-connected связь с каждым
|
||||
incident ray;
|
||||
5. не зависит от rooms/walls order, interval direction и room winding.
|
||||
|
||||
Room-centre union не может быть единственным clip для repaired local strips,
|
||||
потому что физическая outer half-wall по контракту растёт наружу. Одновременно
|
||||
local reconstruction не вправе выходить за каноническую ограниченную exterior
|
||||
envelope и возвращать старый фасадный spike.
|
||||
|
||||
### 6.3 Paper
|
||||
|
||||
`paperGeom` обязан покрывать room centre и всю каноническую room masonry,
|
||||
которой нужен фон плана. Bevel paper удаляет только область за `R`; он не
|
||||
вырезает bounded overlap положительных rays. Внутри node не образуется новый
|
||||
hole/component.
|
||||
|
||||
Открытия и independent physical bodies сохраняют нынешний порядок: openings
|
||||
режут masonry после canonical `roomGeom`, а independent bodies не расширяют
|
||||
room paper.
|
||||
|
||||
### 6.4 Clean floor и physics
|
||||
|
||||
`roomGeom` остаётся единственным pre-opening/pre-independent wall authority.
|
||||
Clean-floor/room-fill/hover subtract именно его; восстановленный wall probe не
|
||||
может стать полом соседней комнаты. Final masonry, hidden Iso и light barriers
|
||||
получают тот же node: визуальная щель не заменяется невидимым occluder и
|
||||
наоборот.
|
||||
|
||||
Структурные caches и fingerprints сохраняют нынешнюю зависимость только от
|
||||
геометрии. Theme, hover и HA state tick не перестраивают topology.
|
||||
|
||||
### 6.5 Failure isolation
|
||||
|
||||
Каждый node остаётся локальной транзакцией по контракту #197: invalid/nonfinite
|
||||
candidate не удаляет успешную геометрию других nodes и не превращает весь план
|
||||
в `null`. Обязательные exterior/body/opening failures продолжают fail-dark;
|
||||
исправление не маскирует их как успешный результат.
|
||||
|
||||
## 7. Данные, compatibility, touch, security и performance
|
||||
|
||||
- Config/layout и `model_version` не меняются; чтение и render не мутируют
|
||||
вход.
|
||||
- Обе сохранённые wall-key формы #258 продолжают давать одну геометрию;
|
||||
Optimize не требуется для визуального исправления.
|
||||
- Legacy key-only records и нулевые/virtual intervals сохраняют действующие
|
||||
правила.
|
||||
- Новых UI, focus, ARIA, клавиатурных, pointer или touch-контрактов нет. View и
|
||||
kiosk получают исправление через общий render path; редакторы остаются
|
||||
desktop-first.
|
||||
- Новых данных, HTML, URL, service calls, permissions и security surfaces нет.
|
||||
- Node map не строится повторно. Локальный repair остаётся в существующем
|
||||
cached structural pass; запрещён новый полный `O(E²)` обход на HA state tick.
|
||||
Отдельный performance budget не нужен, но prerelease performance smoke
|
||||
остаётся обязательным.
|
||||
|
||||
## 8. Acceptance criteria и доказательства
|
||||
|
||||
### AC1. Реальный T-узел сохраняет физический сектор
|
||||
|
||||
На `test/fixtures/197-junction-patch.json` при `coordScale = 1000`:
|
||||
|
||||
- `wallBodiesGeometry()` возвращает непустые `roomGeom`, `geom` и `paperGeom`;
|
||||
- probe `(895.5, 556.0)` находится внутри `roomGeom`, final `geom` и
|
||||
`paperGeom` с устойчивым ненулевым покрытием, а не только на границе;
|
||||
- probe не находится ни в одном clean-floor результате соседних rooms;
|
||||
- локальное тело у `(887.5, 550)` связано со всеми тремя incident rays;
|
||||
- повторный расчёт детерминирован.
|
||||
|
||||
**Доказательство:** unit в `test/wall-thickness.test.mjs`, использующий
|
||||
существующую fixture #197 и polygon-area/point coverage, а не только общую
|
||||
непустоту или brittle SVG-строку.
|
||||
|
||||
### AC2. Ограничение #249 не ослаблено
|
||||
|
||||
На fixture #249 и table-driven 3/4-ray cases:
|
||||
|
||||
- old excessive wedge остаётся пустым;
|
||||
- join не выходит дальше `1.25 × H + epsilon`;
|
||||
- ordinary two-ray joins сохраняют `MITRE_LIMIT = 4` и прежние expected values;
|
||||
- равные/неравные толщины, permutations, reversed intervals, room winding и
|
||||
normalized/production scale дают эквивалентный результат.
|
||||
|
||||
**Доказательство:** существующие и усиленные unit
|
||||
`test/wall-thickness.test.mjs`; прежние expected values #249 не переписываются
|
||||
ради прохождения нового теста.
|
||||
|
||||
### AC3. #197, #258 и zero-depth regressions остаются закрыты
|
||||
|
||||
- forced failure одного optional junction candidate сохраняет остальные nodes
|
||||
и непустую canonical geometry;
|
||||
- affected/canonical wall-key variants #258 дают равные masonry/paper paths;
|
||||
- нулевая общая грань `(620.833, 541.667..550)` не становится физической и не
|
||||
увеличивает degree;
|
||||
- render не мутирует rooms, walls, open spans, openings или extra bodies.
|
||||
|
||||
**Доказательство:** unit #197/#258, deep equality и mutation gate. Новый mutant
|
||||
возвращает full-origin bevel cut в paper/body; AC1 обязан на нём краснеть.
|
||||
|
||||
### AC4. Все browser-поверхности видят один заполненный node
|
||||
|
||||
В `demo/smoke_junction_patch_resilience.mjs` fixture #197:
|
||||
|
||||
- Plan/View/kiosk wall path и paper path содержат normalized probe
|
||||
`(0.8955, 0.5560)` через реальный SVG `isPointInFill()` либо эквивалентный
|
||||
geometry probe;
|
||||
- Static получает тот же canonical wall path и заполненный probe;
|
||||
- hidden Iso source masonry и light-barrier masonry содержат тот же probe;
|
||||
- clean-floor/hover не присваивают probe полу;
|
||||
- theme и HA state tick сохраняют path/fingerprint и повторно используют cache.
|
||||
|
||||
**Доказательство:** targeted browser smoke. Проверка одной лишь непустоты path
|
||||
недостаточна.
|
||||
|
||||
### AC5. Golden защищает видимый клин семантически
|
||||
|
||||
Сценарии `junction-patch-resilience-plan-dark` и
|
||||
`junction-patch-resilience-view-dark` используют существующую полную fixture и
|
||||
получают явный `retainedWedgeProbe: [0.8955, 0.5560]`. Golden capture до записи
|
||||
PNG проверяет, что wall и paper SVG действительно заполняют probe; пустая,
|
||||
неверно кадрированная или сохранившая клин сцена завершается ошибкой.
|
||||
|
||||
Изменившиеся Linux CI PNG и diff просматриваются до принятия baseline.
|
||||
Локальный Windows raster не является каноном.
|
||||
|
||||
**Доказательство:** `demo/golden/matrix.mjs`, `demo/golden/harness.mjs`,
|
||||
`demo/golden/run.mjs`, `test/golden-matrix.test.mjs` и полный
|
||||
`npm run golden:verify`.
|
||||
|
||||
### AC6. Данные и производительность не меняются
|
||||
|
||||
- до/после render входной config побайтово одинаков;
|
||||
- schema/backend/i18n отсутствуют в diff;
|
||||
- structural cache строит node map один раз; state/theme ticks его используют;
|
||||
- `node scripts/model-invariants.mjs --config <реальный экспорт>` не получает
|
||||
новых нарушений.
|
||||
|
||||
**Доказательство:** unit/smoke, review diff и model-invariants на исходном
|
||||
экспорте владельца.
|
||||
|
||||
### AC7. Локальные гейты реализации зелёные
|
||||
|
||||
- `npx tsc --noEmit`;
|
||||
- `npm test`;
|
||||
- `npm run build` с действующей проверкой shipped bundle copies;
|
||||
- `node scripts/check-docs.mjs`;
|
||||
- `node scripts/smoke-select.mjs --base origin/dev --head HEAD` с явным решением
|
||||
по каждой предложенной строке;
|
||||
- targeted `node demo/smoke_junction_patch_resilience.mjs` и
|
||||
`node demo/smoke_multiwall_junction.mjs`;
|
||||
- `npm run golden:verify` для видимого изменения;
|
||||
- `node scripts/model-invariants.mjs --config
|
||||
C:\\Temp\\houseplan-space-1-2026-08-23_02-30-42.json`.
|
||||
|
||||
Полный smoke и performance остаются prerelease gates; Python/backend suite не
|
||||
требуется при отсутствии Python diff. Canonical HA harness остаётся Linux CI.
|
||||
|
||||
## 9. Затронутые файлы и модули
|
||||
|
||||
Ожидаемый product diff:
|
||||
|
||||
- `src/wall-thickness.ts` — canonical multi-wall body/paper repair.
|
||||
|
||||
Ожидаемые доказательства:
|
||||
|
||||
- `test/wall-thickness.test.mjs`;
|
||||
- `demo/smoke_junction_patch_resilience.mjs`;
|
||||
- при необходимости `demo/smoke_multiwall_junction.mjs`;
|
||||
- `demo/golden/matrix.mjs`, `demo/golden/harness.mjs`,
|
||||
`demo/golden/run.mjs`, `test/golden-matrix.test.mjs`;
|
||||
- `scripts/mutation-gate.mjs`.
|
||||
|
||||
Документация:
|
||||
|
||||
- `docs/WALL-THICKNESS.md`, `docs/ARCHITECTURE.md`, `docs/TESTING.md`;
|
||||
- `docs/USER-GUIDE.ru.md` и синхронный `docs/USER-GUIDE.md`;
|
||||
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md`;
|
||||
- docs screenshots/manifest, если обязательный `check-docs` требует их
|
||||
обновления для текущего `src/**` fingerprint.
|
||||
|
||||
Новых i18n keys нет.
|
||||
|
||||
## 10. Release artifacts
|
||||
|
||||
- User-visible bugfix записывается в оба changelog в том же коммите, что
|
||||
`src/**`.
|
||||
- Каноническая wall/architecture/testing документация обновляется тем же
|
||||
коммитом.
|
||||
- Golden baseline меняется только после просмотра полного Linux CI artifact и
|
||||
`npm run golden:accept -- --reviewed`.
|
||||
- Если source fingerprint делает docs screenshots stale, кандидаты снимаются
|
||||
только workflow `Docs screenshots` и принимаются из его полного artifact;
|
||||
локальная Windows-пересъёмка не коммитится.
|
||||
- Перед бетой обязательны полный smoke, golden и performance/Validate exact-SHA
|
||||
по действующему release process.
|
||||
|
||||
## 11. Риски и меры снижения
|
||||
|
||||
### R1. Возврат фасадного spike #249
|
||||
|
||||
Если просто убрать full-origin cut, старый длинный mitre может вернуться.
|
||||
|
||||
**Митигация:** AC2 сохраняет bound `1.25 × H`, discarded-wedge probe и
|
||||
перестановочную матрицу #249; меняется область cut, а не лимит.
|
||||
|
||||
### R2. Ложная кладка снаружи дома
|
||||
|
||||
Если разрешить rebuilt strips без exterior envelope, child-room ray может
|
||||
расширить фасад.
|
||||
|
||||
**Митигация:** §6.2 требует clip к канонической bounded paper/exterior envelope,
|
||||
corner-Split и concave-facade тесты остаются неизменными.
|
||||
|
||||
### R3. Paper, masonry и physics расходятся
|
||||
|
||||
Исправление только SVG path скроет белый клин, но оставит ложный clean floor или
|
||||
проход света.
|
||||
|
||||
**Митигация:** AC1/AC4 проверяют `roomGeom`, final masonry, paper, clean floor,
|
||||
Iso и light barrier одной точкой. Отдельный render-only patch запрещён.
|
||||
|
||||
### R4. Существующая fixture снова проверит только непустоту
|
||||
|
||||
#197 уже содержала проблемный node, но её unit/smoke принимали общую площадь и
|
||||
непустой path, поэтому регрессия прошла.
|
||||
|
||||
**Митигация:** обязательные point-coverage assertions, semantic golden field и
|
||||
mutant полного cut. Тест должен быть продемонстрирован красным на мутанте.
|
||||
|
||||
### R5. Local boolean repair погасит соседние nodes
|
||||
|
||||
Изменение order union/intersection может вернуть failure mode #197.
|
||||
|
||||
**Митигация:** node-by-node transaction сохраняется; forced-failure test и
|
||||
полная fixture проверяются вместе с новым probe.
|
||||
|
||||
## 12. Откат
|
||||
|
||||
Откатывается локальная multi-wall body/paper correction вместе с unit,
|
||||
smoke/golden и документацией. Данные не мигрируются, поэтому отдельного
|
||||
data rollback нет. Golden baseline откатывается только вместе с product
|
||||
geometry; частичный откат оставит ложный зелёный визуальный гейт.
|
||||
|
||||
## 13. Принятые технические предположения
|
||||
|
||||
1. Наблюдаемый пользовательский контракт — непрерывная физическая кладка с
|
||||
сохранённым bound #249; точное разбиение функции на helpers не является
|
||||
продуктовым решением.
|
||||
2. Existing fixture #197 является достаточной обезличенной regression fixture;
|
||||
полный реальный export используется только локально для cross-version и
|
||||
model-invariants проверок.
|
||||
3. Probe `(895.5, 556.0)` выбран внутри измеренного исчезнувшего треугольника с
|
||||
запасом от его границ; он не является brittle vertex equality.
|
||||
4. Paper может отличаться от beta.3 на малую область excessive bevel за `R`:
|
||||
задача требует физический контракт, а не полное byte/area равенство старой
|
||||
версии.
|
||||
5. Нулевая общая грань остаётся отдельным корректным zero-depth interval. Её
|
||||
материализация для маскировки клина была бы исправлением симптома и выходит
|
||||
из scope.
|
||||
Reference in New Issue
Block a user