35 KiB
Issue #141 — бесшовные стыки перегородок и открытых контуров
Статус: ТЗ на ревью
Дата: 2026-08-14
Тип: bug · приоритет: P2 · оценка ценности: 8/10 · сложность/риск: 7/10
Issue: #141
Ветка: issue/141-wall-joints
Канонические документы: SCOPE,
WALL-THICKNESS, CANVAS,
UX-MODES, LIGHT, SUN,
ISOMETRIC, TOUCH-SUPPORT.
Решения владельца Q1–Q3 и defaults аналитики приняты 2026-08-14 в issue #141.
1. Сценарий и продуктовый контекст
Основная персона — домашний администратор, который в desktop Plan editor с нуля рисует план либо позднее добавляет к нему независимые перегородки. После второго клика он должен оценивать уже финальную форму стены, а не ждать замыкания комнаты или перехода в View, чтобы обнаружить клин, щель либо зуб в стыке.
Это часть J4/J6 из docs/SCOPE.md: GUI должен доводить администратора от пустого
холста до правдивого плана без внешнего редактора и сохранять этот план правдивым
при дальнейших изменениях. View и киоск не получают новых действий, но обязаны
показывать ту же физическую кладку и те же световые препятствия.
2. Что человек увидит до и после
До: два соединённых толстых сегмента выглядят как наложенные прямоугольники: у прямого угла остаётся ступень/зуб, у непрямого — клиновидный разрыв; открытая комнатная стена исправляется только после замыкания контура.
После: намеренно соединённые сегменты сразу, включая live rubber-band, образуют одно ровное тело с тем же ограниченным mitre/bevel, которое человек видит у готовой комнаты; после клика, замыкания или перехода в View форма не прыгает.
3. Проблема и подтверждённая причина
drawWallPreviewD()используетoutset − insetтолько для закрытого контура. Открытый путь создаётся как несколько независимых прямоугольников.partitionBody()правильно создаёт один сегмент с плоскими торцами, ноphysicalBodies()передаёт набор таких прямоугольников без топологии узлов.- Polygon union удаляет перекрытие прямоугольников, но не может догадаться, какой отсутствующий сектор следует достроить в endpoint↔endpoint углу. Поэтому прямой и косой угол показывают один механизм в разном масштабе.
- Закрытая комната проходит другой путь: смежные рёбра одного polygon получают
offset-line intersection с
MITRE_LIMIT, поэтому её углы нормализованы. - Full/static/isometric render уже объединяют независимые тела с кладкой, но Glow и солнце местами обходят исходные прямоугольники отдельно. В результате визуальная щель может стать реальным световым просветом, а внутренняя грань перекрытия — ложным окклюдером.
- Предположенный в исходном описании отдельный дефект свободного торца по приложенному скриншоту не подтверждён: видимый нижний левый зуб находится в стыке двух сегментов. Одиночный корректный сегмент уже имеет плоский cap.
4. Решения владельца
- Исправление охватывает точные endpoint↔endpoint и endpoint↔line (T) соединения, которые могут создать инструменты «Контур» и «Перегородка» после #137, между active/saved draft, partition и готовой комнатной стеной.
- Rubber-band до клика использует тот же join, что сохранённое тело; клик не меняет внешнюю форму стыка.
- Свободный конец остаётся плоским. Отдельные round/square cap, настройка cap или новый UX не вводятся.
- Существующие сегменты не дробятся и сохранённый config не переписывается ради вычисляемого T-узла.
- Случайное X-пересечение без endpoint сохраняет текущую union-семантику и не становится новым persisted node.
5. Scope
В задачу входят:
- вычисляемая топология точных узлов у
room_draftsиpartitions; - бесшовные endpoint↔endpoint углы: прямые, острые и тупые;
- endpoint↔line T-соединения с partition, saved draft и комнатной стеной;
- open-draft preview после размещения сегмента и rubber-band до следующего клика/замыкания;
- собственная толщина каждого уже размещённого сегмента и текущая толщина rubber-band;
- одна joined geometry для full View, Plan, static card, hidden isometric, clean floor/area, Glow, sun и проверки источника внутри кладки;
- сохранение per-object geometry для выбора, hit testing, drag, properties, удаления и Undo/Redo;
- unit, targeted production-bundle smoke, golden и performance regression coverage;
- пользовательская и архитектурная документация плюс RU/EN changelog.
6. Non-scope
В задачу не входят:
- изменение snap tolerance, приоритетов или overlay из #137;
- автоматическое дробление room/draft/partition в persisted config;
- превращение partition в комнатную границу, разбиение комнаты или HA area;
- новый тип persisted node/junction и миграция старых планов;
- новый cap/join selector, round cap, декоративные окончания и материалы;
- изменение opening/open-span, wall thickness, Split или Resize semantics;
- исправление #138 про замыкание контура по углам существующей комнаты;
- изменение furniture magnet, opening placement либо selection UX;
- touch parity редактора сверх действующего safety floor;
- новые backend API, HA service calls, зависимости или i18n-тексты.
7. Контракт геометрии и поведения
7.1. Что считается соединением
- Узел вычисляется только из координат, уже совпавших по действующему grid/wall-bound snap и geometry epsilon. Визуально близкие, но разные точки автоматически не стягиваются.
- Endpoint↔endpoint — общий конец двух или более segment centrelines.
- Endpoint↔line — endpoint одного segment точно лежит на сплошном интервале другого. Для вычисления тела проходящий segment можно временно представить двумя incident rays, но его persisted запись остаётся одной.
- Opening и open-span gap не является физической линией комнатной стены и не создаёт join с ней. Independent partition/draft не получает проём только потому, что совпал с room opening.
- Пересечение двух внутренних точек без endpoint не создаёт node metadata; наложившиеся физические объёмы по-прежнему соединяются обычным boolean union.
7.2. Endpoint↔endpoint corner
- Исходные segment bodies растут на
½ thicknessпо обе стороны centreline. - Внешние offset faces смежных incident rays пересекаются в mitre point.
- Если расстояние до точки пересечения не превышает
MITRE_LIMIT × max(adjacent half-depth), отсутствующий сектор включается в joined body. - Если предел превышен либо устойчивого пересечения нет, применяется bounded bevel. Бесконечный spike, self-intersection, щель и незаполненный зуб недопустимы.
- Для коллинеарных продолжений результат — один прямой wall run без внутреннего торца. Для разворота назад/нулевой длины новая неоднозначная кладка не создаётся.
7.3. T-соединения и комнатные стены
- Branch, endpoint которого лежит на другом solid segment, входит в тело проходящей стены без щели и без видимой внутренней butt-face.
- Для готовой комнатной стены authoritative room ring/exterior shell не
перестраивается по правилам partition. Joined independent geometry
объединяется с уже готовой
wallBodiesGeometry()после opening cuts, как и сейчас; partition не пробивается совпавшим room opening. - Подключение к exterior corner не меняет наружный фасад комнаты за пределами реального independent body и не возвращает регрессию #123.
- Разная толщина incident segments не усредняется и не записывается обратно. Joined contour переходит между их реальными offset faces без прозрачной щели.
7.4. Свободные торцы и сложные узлы
- Узел степени 1 заканчивается текущим плоским cap на исходном endpoint.
- В узле степени 3+ объединяется volume всех incident bodies и только ограниченные join patches. Алгоритм не должен заполнять произвольный круг вокруг узла или расширять кладку дальше bounded mitre/bevel envelope.
- Zero-length, non-finite или уже отклонённый schema segment не создаёт body или join patch и не ломает остальные валидные тела.
- Порядок records, id и направление
a↔bне меняют результат.
7.5. Live preview
- После каждого законченного segment открытый контур показывает joined body немедленно; замыкание не является первым моментом нормализации.
- Если есть cursor candidate, rubber-band присоединяется к предыдущему segment и к точному target endpoint/line тем же bounded join.
- Предыдущие segment используют сохранённые
room_drafts[].segments[].cm, а rubber-band — текущее session thickness. Изменение поля толщины не перерисовывает уже сохранённые segment новой толщиной. - При клике outer contour rubber-band становится committed contour без скачка; допустимо только изменение preview/editor styling.
- При закрытии простой комнаты все уже существовавшие вершины совпадают с нормализованной формой готовой комнаты в пределах geometry epsilon.
7.6. Единая физическая семантика
Для валидного сохранённого плана один structural result определяет:
- full Plan/View wall-body path;
houseplan-space-cardwall-body path;- hidden isometric wall footprint/faces;
- subtraction из clean floor и вычисляемой площади;
- Glow barriers и fail-dark source placement;
- sun occluders.
Ни один consumer не должен заново обходить raw segment rectangles как будто их внутренние butt-faces являются наружными стенами. Raw per-object body остаётся доступным только там, где требуется identity редактируемой записи: hit, selection frame, drag/properties/delete и history snapshot.
7.7. Ошибки вычисления
- На валидных schema/snap inputs joined pass обязан быть детерминированным.
- Malformed legacy input не должен превращать видимую стену в прозрачность: fallback остаётся conservative/opaque и не пишет config.
- Boolean failure не разрешается маскировать исчезновением кладки, световым проходом или миграцией данных. Диагностический fallback может вернуть raw bodies с прежним визуальным дефектом, но plan и light должны fail consistently opaque.
8. Архитектурный контракт реализации
8.1. Structural physical frame
Реализация вводит один чистый вычисляемый frame (точное имя свободно), который получает room walls/open cuts/openings, partitions, room drafts, columns и scale inputs и возвращает как минимум:
- raw bodies с source identity для редактора;
- joined independent geometry;
- объединённую room + independent masonry geometry для presentation/occlusion;
- стабильный structural fingerprint либо данные для существующего cache key.
Join topology строится из centreline segments и их half-depth, а не из SVG DOM, stroke-linejoin или raster measurement. Columns остаются обычными closed bodies и участвуют в union, но не становятся incident wall rays.
8.2. Canonical consumers
wallBodiesGeometry() остаётся канонической точкой room masonry либо получает
эквивалентный единый wrapper; full/static/isometric не создают разные join
алгоритмы. _lightBarriers получает outer/hole rings объединённой opaque geometry,
а не внутренние грани перекрывающихся raw rectangles. Clean-floor и source guard
используют тот же joined result.
Preview использует тот же pure node/join primitive, но может строить только активный path плюс локально необходимые target segments. Он не мутирует и не инвалидирует saved structural frame на каждый pointermove.
8.3. Cache и invalidation
- Saved topology пересчитывается только при structural geometry change:
coordinates, cm, room/opening/open-span topology,
cell_cmили grid/scale. - HA state, theme, hover, cursor, Glow brightness и animation tick не входят в saved geometry key.
- Live rubber-band допускает дешёвый локальный расчёт на pointermove; полный boolean union всего дома на каждый move запрещён.
- Full/static/isometric/light consumers переиспользуют structural result либо эквивалентный immutable geometry, а не копируют O(N²) node search.
- Cache остаётся bounded и очищается существующими lifecycle hooks.
9. Модель данных, compatibility и миграция
rooms,walls,room_drafts,partitions,wall_columns,openingsиopen_spansне меняют schema.- Backend validation, storage version, import/export и Python model не меняются.
- Старые планы получают исправленную вычисляемую форму при чтении без записи.
- Hover, preview, render и cache warm-up не вызывают config/layout/storage write.
- Undo/Redo продолжает хранить существующие geometry snapshots; одна partition остаётся одной записью и одним history action.
- Прямой и обратной миграции нет.
10. UX, i18n, accessibility и touch
Новых controls, диалогов, toasts, focus/keyboard semantics и текстов нет; следовательно, новые RU/EN i18n keys не требуются.
Plan editor остаётся desktop-first. На touch/coarse pointer новый hover parity не обещается, но если существующий tap создаёт segment, его сохранённая geometry обязана быть той же; pinch, pointercancel и synthetic click не могут сохранить лишний segment. View/киоск остаются полностью поддержанными и получают ту же исправленную форму без новых взаимодействий.
Selection frame и handles могут показывать границу конкретной редактируемой
записи поверх общего wall body; это editor chrome, а не альтернативная физика.
Forced colours и prefers-reduced-motion не получают новой ветки поведения.
11. Производительность и безопасность
Производительность: large-house fixture уже содержит 60 partitions. Новая топология не должна пересчитываться на HA state tick, создавать unbounded cache или ослаблять действующие budgets. Перед бетой обязательны exact-SHA performance smoke и Full Performance; изменение budget допускается только отдельным решением процесса, не в #141.
Безопасность: HA permissions, services, locks, network, HTML и внешние данные не меняются. Главный safety invariant — нарисованная кладка, clean floor и окклюдер совпадают. При ошибке вычисления приоритет у непрозрачного fail-closed поведения, а не у сохранения декоративной картинки.
12. Acceptance criteria
- AC1 (
unit+golden; разработчик): open draft из двух segment с общим endpoint при 90°, остром и тупом угле образует один body без gap, tooth, overlap seam или неограниченного spike; до замыкания и после замыкания уже размещённые вершины геометрически совпадают в пределах epsilon. - AC2 (
unit+smoke+golden; разработчик): rubber-band до клика использует тот же bounded mitre/bevel к предыдущему segment и к выбранному endpoint/line target; commit не меняет outer contour, кроме preview styling. - AC3 (
unit; разработчик): каждый segment сохраняет собственную толщину,MITRE_LIMIT = 4ограничивает spike, excess angle переходит в bevel, collinear continuation не имеет внутреннего cap, а degree-1 endpoint остаётся плоским. - AC4 (
unit+smoke; разработчик): две и более saved partitions с точным endpoint↔endpoint соединением дают одинаковую joined geometry для прямого и косого угла независимо от record order, id, направления и допустимой разной толщины. - AC5 (
unit+smoke; разработчик): endpoint↔line соединение с partition, saved draft и solid room wall образует бесшовный T-body; target record не дробится, room/partition/draft config до и после render/hover побайтно эквивалентен. - AC6 (
unit; разработчик): near-miss вне geometry epsilon остаётся раздельным, opening/open-span gap не создаёт room-wall join, incidental X без endpoint сохраняет union semantics, zero-length/non-finite segment не портит валидные neighbours. - AC7 (
unit+smoke+golden; разработчик): full Plan/View,houseplan-space-cardи hidden iso получают один joined footprint: крупный прямой/косой стык не показывает внутреннюю butt-face, зуб или второй hatch. - AC8 (
unit+smoke; разработчик): clean-floor area, Glow barriers, source-inside-body guard и sun occlusion используют joined geometry: свет не проходит через corner/T join и не блокируется внутренней гранью бывшего overlap. - AC9 (
smoke; разработчик): hit/selection/drag/properties/delete и Undo/Redo сохраняют identity отдельных partitions/draft segments; joined render не объединяет записи в config и не меняет их history granularity. - AC10 (
unit+ code review; разработчик/ревьюер): room wall openings, virtual-T, nested/partial walls, exterior shell #123, columns и одиночные partitions сохраняют действующую геометрию; совпавший room opening не режет independent wall. - AC11 (
performance+ code review; разработчик/ревьюер): structural joined pass cached/bounded, HA state и pointer hover не запускают full-house topology, 60-partition fixture проходит существующие budgets без их ослабления. - AC12 (
unit+ backend/schema review; разработчик/ревьюер): schema, serialized config, backend, import/export, i18n, HA/network calls и зависимости не меняются; старый plan исправляется без migration/write. - AC13 (
typecheck+unit+build+ documentation review; разработчик): implementation-loop gates зелёные; три bundle-копии после build побайтно одинаковы; RU/EN changelog и перечисленная документация обновлены в том же user-visible implementation commit.
13. План автотестов
13.1. Unit
Добавить geometry regression tests в test/physical-geometry.test.mjs,
test/wall-thickness.test.mjs либо отдельный узкий файл:
- сравнить joined L-body с ожидаемым offset envelope для 90°, acute и obtuse;
- проверить bounded mitre и bevel threshold вокруг
MITRE_LIMIT; - проверить equal/unequal thickness, reversed endpoints и permutation records;
- проверить collinear run, flat free caps, zero-length и near-miss;
- проверить T partition→partition, draft→partition и partition→solid room wall;
- проверить gap/open span и X-crossing contract;
- сравнить open-preview committed vertices с closed-room ring;
- доказать, что предыдущие draft segment сохраняют свой
cm, когда current session thickness меняется; - проверить clean-floor boolean area и united opaque rings для Glow/source/sun;
- regression: openings do not cut extras, columns unchanged, #123 exterior, virtual-T и nested wall remain green;
- проверить immutability inputs и structural fingerprint/cache invalidation.
Минимум один новый test должен быть запущен/проверен ревьюером на origin/dev и
краснеть на старой прямоугольной geometry, а не только подтверждать новый helper.
13.2. Targeted production-bundle smoke
Добавить demo/smoke_wall_junctions.mjs либо эквивалент:
- открыть Plan editor и нарисовать open draft с прямым и косым join;
- проверить rubber-band path до click и отсутствие geometry jump после click;
- создать две partitions endpoint↔endpoint и новую partition через line-snap #137 к середине существующей wall/partition;
- проверить единый body path, отсутствие split target record и no-write hover;
- переключить Plan → View → static card → hidden iso и сравнить footprint/bbox signatures;
- проверить selection/drag/Undo/Redo отдельных records;
- поставить Glow source и sun case по разные стороны junction и подтвердить opaque/no-false-edge result;
- проверить pointercancel/pinch/suppressed click — новых records нет.
По процессу smoke пишется вместе с кодом, но полный browser-suite запускается только перед бетой.
13.3. Golden
Добавить deterministic joined-wall scenario минимум с кадрами:
- Plan/light: committed 90° и oblique open-draft joins плюс active rubber-band;
- Plan/dark: endpoint-to-line T-preview поверх существующей partition/room wall;
- View/static/iso: saved 90° и oblique partitions с крупно читаемым outer contour;
- lighting при необходимости: junction рядом с Glow pool, чтобы щель не стала световой полосой.
Не связанные View baselines с одиночными partitions должны остаться
pixel-identical. Любые затронутые эталоны принимаются только из полного
просмотренного Linux artifact через
npm run golden:accept -- --reviewed; локальное принятие ради зелёного CI
запрещено.
13.4. Performance и backend
Перед бетой прогнать существующий large-house-v1/performance smoke с 60
partitions и Full Performance на точном SHA. Если для локального join-preview
нужен отдельный профиль, он получает новый id и не переопределяет старый budget.
Backend не меняется; отдельный backend test не требуется. Полный Linux Validate остаётся release gate.
14. План реализации
- Выделить чистое представление linear physical segments и exact junction topology с source identity.
- Построить bounded mitre/bevel node patches и joined independent geometry.
- Разделить raw editable bodies и canonical joined presentation/physics frame.
- Подключить joined frame к full/static/isometric, clean floor, Glow/source guard и sun, сохранив opening ordering.
- Перевести open-draft/rubber-band preview на тот же join primitive и per-segment thickness.
- Добавить unit и targeted smoke; подготовить golden/performance fixtures.
- Обновить документы, оба changelog и поставляемые bundle-копии.
Точные имена helpers/files не являются продуктовым контрактом.
15. Release-артефакты
Изменение пользовательское. Implementation-коммит имеет User-Visible: yes и в
том же коммите обновляет:
docs/CHANGELOG.md;docs/CHANGELOG.ru.md;docs/USER-GUIDE.ru.md— рисование открытых контуров и partitions;docs/WALL-THICKNESS.md— independent joined-body/cap/mitre contract;docs/ARCHITECTURE.md— raw editable bodies против canonical joined frame;docs/LIGHT.mdи при необходимостиdocs/SUN.md/docs/ISOMETRIC.md— единый opaque geometry consumer;docs/TESTING.md— unit/smoke/golden coverage;docs/STATUS.md— релизная линия после фактической реализации;- три поставляемые bundle-копии, создаваемые build/release-процессом.
Перед бетой обязательны exact-SHA Linux Validate, полный smoke-suite, просмотр и reviewed acceptance Linux golden artifact, performance smoke и Full Performance, а также зелёный code review. Отдельный security report не требуется: новых внешних данных, вызовов и разрешений нет. Изменение проходит beta до stable.
16. Риски и снижение
| Риск | Вероятность / ущерб | Снижение |
|---|---|---|
| Pairwise patch переполнит узел степени 3+ | средняя / высокий | radial/node envelope units, T/Y matrix, bounded area assertion |
| Разная толщина усреднится или даст self-intersection | средняя / высокий | per-ray half-depth и boolean difference tests |
| Plan исправится, а Glow/sun сохранят raw butt-faces | средняя / высокий | один canonical joined frame и occlusion smoke |
| Opening случайно прорежет coincident partition | средняя / высокий | сохранить union extras after cuts, regression unit |
| Selection потеряет identity после union | средняя / высокий | raw bodies отдельным editor-only channel, drag/Undo smoke |
| Room exterior изменится при attached partition | низкая / высокий | #123 exterior regression и room-ring authority |
| Live preview начнёт делать full union на pointermove | средняя / высокий | local target subset, performance/code review AC11 |
| Старый near-miss внезапно соединится | средняя / средний | exact epsilon contract и explicit near-miss test |
| Boolean failure сделает стену прозрачной | низкая / высокий | conservative opaque fallback и malformed fixture |
| Golden изменятся шире joined cases | средняя / средний | baseline diff audit; unrelated single partitions pixel-identical |
17. Откат
Откат — revert user-visible implementation commit #141 вместе с тестами, документацией, changelog и bundle-копиями. Schema и persisted data не меняются, поэтому migration/data rollback не нужны; после отката вернётся прежний визуальный дефект прямоугольных bodies.
Красный golden/performance gate перед бетой блокирует выпуск. Feature flag, ослабление budget или принятие baseline без review не используются как обход.
18. Принятые технические предположения — можно менять без продуктового ревью
- Junction epsilon переиспользует существующий grid/geometry tolerance и не вводит пользовательскую настройку.
- Рекомендуется immutable
PhysicalGeometryFrameлибо эквивалент с raw и joined projections; точное имя и раскладка файлов свободны. - Node patches могут строиться вручную через offset intersections или через другую deterministic boolean decomposition, если все AC выполняются.
- Room-wall geometry может оставаться внутри
wallBodiesGeometry()либо быть обёрнута общим helper; второй независимый renderer запрещён. - Preview может добавлять локальный patch поверх existing saved body либо строить небольшой joined subset; DOM decomposition не важна при отсутствии seam/jump.
- Cache может использовать
_cfgEpochплюс structural fingerprint или полный fingerprint; HA state/theme/cursor не входят в saved key. - Selection frame вправе показывать raw body выбранной записи, если base wall body остаётся joined и hit order не меняется.
- Имена smoke/golden scenarios и точные test files не являются продуктовым контрактом.
- Для malformed legacy fallback допустимы raw opaque bodies; валидные schema inputs обязаны проходить joined path без fallback.
- Нет открытых продуктовых вопросов: Q1–Q3 и предложенные 8/10 · P2 приняты владельцем 2026-08-14.