25 KiB
Issue #261 — белые клинья в T-стыках после ограниченного multi-wall bevel
- Дата: 2026-08-23
- Тип: bug · приоритет P1
- Оценка: пользовательская ценность 8/10 · ценность для разработки 8/10 · сложность/риск 7/10
- Issue: #261
- Ветка:
issue/261-white-wedges-root-cause - Статус ТЗ: реализовано, ожидает код-ревью
- Реализация после rebase на текущий
dev:0dc6c39; проверочные артефакты и smoke registry:2e53cb0.
Канонические документы: 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, #249, #253 и #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 независимых физических тела.
Результат сравнения:
wallIntervalsbeta.3/beta.4 совпадает побайтово;wallEdgeBodiesbeta.3/beta.4 совпадает побайтово;buildMultiWallNodeMap()beta.4 находит 12 физических degree-3 узлов;paperGeombeta.3 имеет площадь727303.8201095833, а beta.4/current dev —727248.4381651384;- из beta.4 исчез один реальный треугольник площадью
55.3819444449render-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-формулы:
multiWallBevelTriangles(map)оставляет физическое пересечение лучей доR = 1.25 × Hи возвращает только малый excessive wedge за этим радиусом;- внутренний
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:
- сохраняет complete positive ray strips;
- удаляет только excessive area за
R; - остаётся в допустимой exterior/paper envelope;
- имеет ненулевое заполнение в node centre и area-connected связь с каждым incident ray;
- не зависит от 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, finalgeomи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)через реальный SVGisPointInFill()либо эквивалентный 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. Принятые технические предположения
- Наблюдаемый пользовательский контракт — непрерывная физическая кладка с сохранённым bound #249; точное разбиение функции на helpers не является продуктовым решением.
- Existing fixture #197 является достаточной обезличенной regression fixture; полный реальный export используется только локально для cross-version и model-invariants проверок.
- Probe
(895.5, 556.0)выбран внутри измеренного исчезнувшего треугольника с запасом от его границ; он не является brittle vertex equality. - Paper может отличаться от beta.3 на малую область excessive bevel за
R: задача требует физический контракт, а не полное byte/area равенство старой версии. - Нулевая общая грань остаётся отдельным корректным zero-depth interval. Её материализация для маскировки клина была бы исправлением симптома и выходит из scope.