Files
houseplan-card/docs/specs/173-unified-wall-tool.md
T
2026-08-26 13:39:52 +03:00

699 lines
48 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Issue #173 — единый инструмент рисования стен и предложение комнаты по замыканию
> **Частично заменено #306.** Единый инструмент **«Стены»** остаётся
> каноническим, но все положения этого документа об отдельном инструменте
> **«Граница»**, virtual/open-wall UX и записи `open_spans/open_to` больше не
> действуют. Нулевая толщина создаётся и редактируется как обычная стена по
> `docs/specs/306-zero-thickness-walls.md`; модель v9 хранит только `cm:0`.
- **Issue:** https://github.com/Matysh/houseplan-card/issues/173
- **Редакция:** первая редакция для независимого ревью; статус определяется только метками issue
- **Тип / приоритет:** feature / P1
- **Оценка:** пользовательская ценность 9/10; ценность для разработки 6/10;
сложность/риск 9/10
- **Область:** desktop-first редактор Плана, рисование стен, planar topology,
создание и разделение комнат, Undo/Redo, i18n и release-проверки
- **Модель данных:** существующие `rooms`, `room_drafts`, `partitions` и `walls`;
без новой schema, backend API и миграции
- **Связано:** #137, #138, #141, #150, #172, `docs/SCOPE.md`,
`docs/UX-MODES.md`, `docs/CANVAS.md`, `docs/WALL-THICKNESS.md`,
`docs/TOUCH-SUPPORT.md`
## 1. Сценарий и персона
**Персона:** администратор дома, который рисует новый план либо достраивает
существующий в desktop-редакторе Плана.
**Момент:** пользователь хочет провести стены. Сегодня перед первым кликом ему
надо решить внутренний вопрос модели House Plan: это будущий контур комнаты или
независимая перегородка. При ошибочном выборе приходится менять инструмент либо
перерисовывать геометрию.
Задача поддерживает J4 и J6 из `docs/SCOPE.md`: встроенный GUI должен позволять
нарисовать и поддерживать правдивый план без знания внутренней схемы данных и без
внешнего редактора.
## 2. Что человек увидит до и после
**До:** в панели есть отдельные «Контур комнаты» и «Перегородка». Первый ждёт
замыкания своего draft, второй заканчивается после одного сегмента. Замыкание по
старым стенам, нескольким рёбрам, T- и X-пересечениям обычно не предлагает
комнату.
**После:** в панели есть один инструмент **«Стены»**. Пользователь ведёт одну
ломаную столько, сколько нужно. Если она остаётся открытой и человек меняет
инструмент или выходит из редактора, нарисованное остаётся обычными
перегородками. Если новый сегмент замкнул одну или несколько допустимых областей,
House Plan предлагает создать комнаты; отказ оставляет новые линии стенами.
Никакой отдельной кнопки «Завершить» и клавиши Enter не появляется.
## 3. Проблема и подтверждённое текущее состояние
1. `MarkupTool` содержит разные `draw` и `partition`; toolbar показывает две
кнопки, а warm viewport способен восстановить оба значения.
2. `draw` сохраняет каждый завершённый сегмент в `room_drafts`; `partition`
создаёт одну запись `partitions` после двух кликов и очищает `_path`.
3. `_markupClick()` предлагает комнату при возврате к собственной первой точке,
`Ctrl`/`Cmd` closure либо в узком контракте #138: начало и конец лежат на
одном непрерывном room-owned отрезке.
4. `plan-snap-overlay.ts` уже собирает стены комнат, сохранённые drafts и
partitions, разрешает endpoint-first и line-snap. Он не строит planar faces.
5. `_keepClosedAsPartitions()` умеет преобразовать только собственный замкнутый
draft. Он не знает provenance рёбер, пришедших из старой стены, T/X-узла или
нескольких новых граней.
6. Отдельный Split уже сохраняет большую часть исходной комнаты с прежним id и
метаданными, а меньшую открывает как новую комнату.
7. #141 объединяет физические тела T/endpoint-стыков, но намеренно не создаёт
persisted node для случайного X. В #173 X становится **вычисляемым узлом
только для face topology**; persisted записи и физический join-контракт #141
от этого не меняются.
Следовательно, задача не сводится к удалению кнопки: нужен детерминированный
поиск новых bounded faces и безопасная транзакция между draft, partitions и
rooms.
## 4. Решения владельца
Решения зафиксированы 18.08.2026:
https://github.com/Matysh/houseplan-card/issues/173#issuecomment-5330235454
1. Специальных кнопок завершения и Enter нет. Смена инструмента, пространства
либо выход из редактора завершает открытую цепочку; она становится обычными
partitions и больше не возобновляется как draft.
2. Esc/Ctrl+Z убирает последнюю точку. Pinch, pan и `pointercancel` никогда не
завершают цепочку и не добавляют геометрию.
3. Сохраняется действующий overlap-контракт: partial overlap запрещён, exact
duplicate не предлагается, полное вложение комнат разрешено. Devices, decor,
openings, columns и внутренние стены не блокируют предложение сами по себе.
4. При разделении существующей комнаты большая часть сохраняет id, имя, HA area,
настройки и позиции; диалог настраивает меньшую новую часть. Отказ не меняет
исходную комнату.
5. Если один сегмент замкнул несколько допустимых областей, они предлагаются
последовательно от меньшей площади к большей; решение по каждой независимо.
6. Endpoint, T и геометрический X являются вычисляемыми узлами face graph.
Collinear/coincident edges дедуплицируются. Opening/open-span gap не замыкает
область через отсутствующую стену.
7. Детектор работает только после добавления пользователем нового завершённого
сегмента. Пассивных предложений после загрузки, move/delete/Align/толщины нет;
session/global «не предлагать» не вводится.
8. Отдельный Partition удаляется из toolbar. Отдельный Split остаётся доступен;
возможная дедупликация Split — отдельная будущая задача.
## 5. Scope
В #173 входят:
1. один публичный инструмент «Стены» вместо кнопок «Контур комнаты» и
«Перегородка»;
2. сохранение активной открытой цепочки как crash-safe `room_draft` во время
рисования и её явное завершение по смене контекста;
3. преобразование завершённой незамкнутой цепочки в ordinary `partitions` без
потери координат и толщин;
4. planar graph из solid room-wall intervals, drafts, partitions и нового
сегмента, включая endpoint, T, X и collinear atomization;
5. поиск только новых bounded faces, возникших из последнего принятого сегмента;
6. предложение standalone, adjacent, nested и clean-split комнат по принятому
overlap-контракту;
7. последовательная очередь нескольких кандидатов;
8. действия «Создать комнату», «Оставить стенами» и Cancel без частично
применённой конфигурации;
9. сохранение provenance и толщины новых/существующих рёбер;
10. совместимость старых drafts, partitions и warm session tool;
11. Undo/Redo, optimistic save/conflict, limits, touch safety floor;
12. unit, production-bundle smoke, golden и performance coverage;
13. i18n RU/EN, пользовательские/архитектурные документы и оба changelog.
## 6. Non-scope
В задачу не входят:
- удаление или изменение отдельного Split;
- автоматическое предложение комнат при загрузке, импорте, reload, move, drag,
delete, Resize, Align, изменении толщины, Undo/Redo или внешнем config update;
- фоновая миграция всех старых `room_drafts` в partitions;
- persisted `node`, `junction`, `face` либо новый storage/config field;
- переписывание или дробление **старых** partitions/drafts ради вычисляемого X/T;
- изменение snap tolerance, endpoint-first порядка и overlay-радиусов #137;
- изменение bounded physical join, cap/mitre/bevel и renderer semantics #141;
- создание комнаты через opening/open-span gap;
- колонны, decor, backdrop, furniture и device markers как рёбра graph;
- новая session/global настройка «не предлагать комнату»;
- новый Finish control, Enter/double-click shortcut либо touch-hover parity;
- удаление, перенос или автоматический rebinding устройств;
- backend schema/API, HA service calls, новые зависимости и security permissions;
- принятие golden baseline вне reviewed Linux artifact.
## 7. Термины и базовые инварианты
- **Активная цепочка** — текущая полилиния инструмента «Стены», хранимая во
время работы как один `room_draft` с per-segment `cm`.
- **Исходное ребро** — room-wall solid interval, segment draft/partition либо
новый segment до разрезания пересечениями.
- **Атомарное ребро** — ненулевой отрезок между соседними вычисляемыми graph
vertices после endpoint/T/X/collinear atomization.
- **Face** — простой bounded polygon, найденный обходом half-edges. Внешняя
бесконечная область face не является.
- **Новая face** — face, отсутствовавшая до последнего принятого сегмента и
содержащая хотя бы одно атомарное ребро этого сегмента.
- **Consumed atom** — часть активной цепочки, которая после подтверждения стала
границей созданной комнаты. Остальные части активной цепочки становятся
independent partitions.
Инварианты:
1. Topology вычисляется из координат и canonical cuts, не из SVG DOM, толщины
stroke либо пикселей.
2. Render/hover/topology не мутируют config.
3. Существующая geometry не удаляется и не переписывается по inference.
4. Одна пользовательская координата после snap имеет одну graph-вершину;
record order, id и direction не меняют результат.
5. Комната создаётся только после явного подтверждения в диалоге.
6. Ошибка/Cancel сохраняет уже нарисованные стены как редактируемый draft, но не
создаёт частичную комнату.
## 8. UX единого инструмента
### 8.1 Toolbar и начало цепочки
1. Кнопка `Partition` удаляется из Plan toolbar.
2. Текущая кнопка `draw` становится публичным инструментом **«Стены»** и
остаётся neutral/default tool редактора.
3. Первый click/tap ставит только стартовую точку; одиночная точка не пишется.
4. Каждый следующий отличный resolved point создаёт segment с текущим `cm`,
сохраняет его в активный `room_draft` и продолжает цепочку.
5. Shift/45°, grid, endpoint/line snap, preview, size badge и limits сохраняют
действующие контракты.
6. Повторный клик по уже активной кнопке «Стены» ничего не завершает.
7. Existing `Reset` не становится Finish: он очищает локальное продолжение по
действующему reset-контракту, не конвертируя draft в partitions.
### 8.2 Завершение незамкнутой цепочки
Завершение запускают только:
- переход с «Стены» на другой Plan tool;
- переход в View, Devices или Background;
- смена пространства/этажа, пока активна цепочка.
Reload, route remount/disconnect, external config adoption, pan, pinch,
`pointercancel`, suppressed synthetic click, Escape, Ctrl/Cmd+Z и повторный
выбор «Стены» завершением не являются.
Если есть только стартовая точка, она просто отбрасывается. Если есть один или
больше segment, завершение одной geometry-транзакцией:
1. создаёт ordinary `partitions` из сегментов с теми же endpoints и `cm`;
2. удаляет только соответствующий активный `room_draft`;
3. очищает `_path`, resume/session references и preview;
4. после reload не предлагает эту цепочку для продолжения;
5. записывается именованной Undo/Redo-командой и сохраняется до фактической
смены контекста.
До добавления каждого segment проверяется, что итоговая конвертация не превысит
`MAX_PARTITIONS`/общие limits. Поэтому смена инструмента не может оставить
пользователя в ловушке «переключение невозможно из-за позднего лимита».
### 8.3 Замыкание и диалог
После каждого принятого ненулевого segment строится topology delta. Если новых
eligible faces нет, рисование продолжается. Если они есть:
1. terminal segment остаётся безопасно сохранённым в active draft;
2. открывается стандартный диалог новой комнаты;
3. при нескольких faces диалог показывает понятный прогресс `i из N` и идёт в
стабильном порядке §9.5;
4. **Save/Create room** запоминает настройки текущей face;
5. **Оставить стенами** отклоняет только текущую face;
6. после последнего решения весь batch применяется одной geometry-транзакцией;
7. цепочка завершается: consumed atoms становятся room boundaries, остальные
новые atoms/segments — partitions, active draft удаляется.
Нового предварительного confirm и silent-mode нет.
### 8.4 Cancel, Escape и ошибка
- Cancel, X или Escape в room dialog отменяет **весь ещё не применённый batch**,
включая ранее введённые в очереди ответы, и возвращает terminal active draft
в открытое редактируемое состояние.
- Уже сохранённый terminal segment не пропадает; комнаты/partitions из pending
batch не создаются.
- Validation/limit/boolean/save failure действует так же: no partial room,
no partial conversion, draft остаётся восстанавливаемым.
- Esc вне диалога и Ctrl/Cmd+Z сохраняют действующую семантику удаления последней
точки/segment до shared history.
- Conflict с удалённым config проходит текущий reload/conflict path без
дубликатов rooms/partitions.
## 9. Контракт planar graph и новых faces
### 9.1 Входная геометрия
Graph получает immutable snapshot:
- room boundary axes после canonical opening/open-span cuts #137/#138;
- все валидные inactive `room_drafts`;
- все валидные `partitions`;
- active draft до candidate segment для `before` и с ним для `after`.
Active/saved draft и partition не получают room opening cuts. Columns, wall
thickness offsets, physical body edges, decor, devices и paper не входят.
Malformed/non-finite/zero-length sources игнорируются fail-safe и не ломают
валидный connected component.
### 9.2 Atomization и vertices
1. Endpoint каждого source segment — vertex.
2. Endpoint, точно лежащий на другом solid segment, создаёт T-vertex.
3. Proper X-intersection двух внутренних интервалов создаёт derived vertex в
обоих рёбрах, даже если пользователь там не кликал.
4. Collinear overlap режется по всем конечным точкам и дедуплицируется в
undirected atomic edge; provenance всех источников сохраняется.
5. Near-miss вне canonical epsilon не соединяется.
6. Opening/open-span gap удалён до atomization и не получает искусственных
bridge/endpoints.
7. Persisted old records не дробятся и не переписываются. Atomization является
вычисляемой projection; исключение — новый active segment при финальной
принятой конвертации §10.4.
### 9.3 Face traversal и canonical identity
Outgoing half-edges у vertex сортируются по геометрическому углу со стабильным
tie-break. Обход каждого ещё не посещённого half-edge даёт candidate ring.
Face допускается только если:
- ring имеет минимум три разные vertices и положительную площадь выше
canonical epsilon;
- ring simple после удаления повторов;
- все boundary atomic edges существуют в graph;
- это bounded, а не внешняя face;
- canonical ring identity одинакова при cyclic shift, reverse winding,
перестановке records, id и направлении endpoints.
Точный алгоритм half-edge/DCEL свободен, но pairwise «нашли любой цикл» без
выделения faces запрещён: он создаёт составные внешние циклы поверх внутренних.
### 9.4 Delta: только то, что замкнул пользователь
До append строится или берётся cached `before` component; после append —
`after`. В очередь попадают только faces, которые:
1. отсутствуют по canonical identity в `before`;
2. присутствуют в `after`;
3. содержат хотя бы один atomic interval нового segment;
4. проходят room eligibility §10.
Поэтому уже существующий closed partition ring не открывает диалог при reload
или при несвязанном клике. Detector вызывается только после commit candidate
segment; pointermove/hover строят прежний локальный preview без face traversal.
### 9.5 Порядок и дедупликация кандидатов
Exact room polygon duplicate исключается. Остальные eligible faces
дедуплицируются по canonical identity и сортируются:
1. площадь по возрастанию;
2. canonical ring key как стабильный tie-break.
При равной площади DOM order, room id, source record order и winding не меняют
очередь. Отклонение одной face не удаляет из очереди другую.
## 10. Создание комнат и provenance стен
### 10.1 Standalone, adjacent и nested room
Для face, которая не является clean split:
- partial overlap с любой существующей room polygon отклоняется текущей
validation/toast семантикой;
- exact duplicate не предлагается;
- полное вложение в существующую комнату или охват существующей вложенной комнаты
разрешены как действующий island-room контракт;
- devices, decor, openings, columns и independent walls не блокируют face;
- Save создаёт обычный `room.poly` с выбранными name/HA area/settings;
- существующие room walls, drafts и partitions, использованные как часть
boundary, остаются на месте и не удаляются по inference.
### 10.2 Clean split существующей комнаты
Если new wall path даёт валидное разбиение одной room polygon на две простые
части по действующему `splitRoomPath`/area invariant:
1. диалог относится к меньшей части;
2. большая сохраняет исходные id, name, HA area, settings, label/device positions;
3. при равной площади используется действующий детерминированный tie-break;
4. Save применяет тот же materialize/normalize/open-span порядок, что Split;
5. «Оставить стенами» не меняет room polygon или metadata;
6. отдельный Split остаётся pixel/behavior-compatible.
Если graph face находится внутри комнаты, но не образует представимое clean
split с одной простой remainder polygon, она рассматривается по разрешённому
nested-room контракту либо отклоняется текущей partial-overlap validation; parent
не переписывается эвристически.
### 10.3 Несколько faces
Room settings для каждого принятого кандидата буферизуются до конца очереди.
Одна final transaction валидирует batch повторно на исходном snapshot и только
затем одновременно:
- создаёт принятые rooms;
- применяет clean splits;
- конвертирует не consumed новую геометрию в partitions;
- удаляет active draft;
- обновляет walls/open spans и rebuild устройств по действующим контрактам;
- создаёт одну именованную Undo/Redo-команду и один config save.
Если комбинация независимых ответов образует partial overlap, disconnected
remainder либо превышает limits, batch не применяется: пользователь получает
validation error, а active draft остаётся. Частичное сохранение первых диалогов
запрещено.
### 10.4 Новые рёбра, старые рёбра и толщина
1. Existing room wall сохраняет свою authoritative thickness.
2. Existing partition/draft сохраняет запись, id, selection/history identity и
собственный `cm`; создание room по его оси не удаляет объект.
3. Каждый active-draft segment сохраняет `cm`, выбранный в момент клика.
4. Если только часть нового segment consumed room boundary, segment можно
разрезать **в рамках явной подтверждённой операции** по graph vertices:
consumed atoms переходят в room wall intervals, остальные становятся одной
или несколькими partitions с исходным `cm`.
5. Полностью consumed active atoms не дублируются independent partition.
6. При полном отказе от faces исходные active segments становятся partitions без
лишнего дробления только ради derived X/T.
7. Room-wall normalization #150/#172, joined physical geometry #141, clean
floor, Glow, sun, static и hidden Iso используют существующие canonical
consumers; topology detection не вводит второй renderer.
## 11. Модель данных, compatibility и миграция
Новых persisted полей нет.
- `rooms[].poly`, `walls`, `room_drafts`, `partitions`, `openings` и
`open_spans` сохраняют текущую schema;
- backend validation, storage version, import/export и Python model не меняются;
- старые partitions остаются обычными selectable objects;
- старые room drafts остаются видимыми и могут быть продолжены через единый
инструмент; автоматически при чтении они не конвертируются;
- legacy warm viewport tool `partition` нормализуется в `draw`/«Стены» без
создания geometry;
- reload/remount во время активного рисования восстанавливает безопасный draft,
а не считает технический disconnect выходом из редактора;
- новая версия пишет только уже валидные старой schema rooms/partitions, поэтому
downgrade не требует data rollback;
- topology, hover и dialog preview не пишут config/layout/localStorage.
## 12. UX, i18n, accessibility и touch
### UX и i18n
RU/EN обязательно обновляют:
- toolbar label `Контур комнаты / Room outline` → `Стены / Walls`;
- title и hints: одна цепочка, Shift/45°, Esc/Ctrl+Z, предложение комнаты;
- удаляется toolbar-only copy отдельного Partition, но слово «Перегородка»
сохраняется как label существующего selectable object/properties;
- при queue `N > 1` диалог показывает локализованный `Комната i из N / Room i of
N` либо эквивалентный понятный progress;
- существующие «Оставить стенами», validation toasts, room fields и Save/Cancel
переиспользуются, где их смысл совпадает.
Нового Finish/Enter control, setting или help toggle нет.
### Accessibility
- удалённая кнопка больше не остаётся в tab order или ARIA toolbar;
- «Стены» получает корректные title/aria-label и pressed state;
- room queue остаётся modal, сохраняет focus trap/return и объявляет `i из N` в
title/status без отдельного плавающего слоя;
- keyboard Escape/Undo не меняют смысл;
- forced colours/reduced motion не получают новой анимационной зависимость.
### Touch
**Touch editor: best effort / intentionally degraded.** Desktop — reference.
- tap без pointermove повторно решает snap/topology после commit;
- hover parity не обещается;
- смена инструмента/режима завершает цепочку так же, как desktop;
- pinch, pan, second pointer, pointercancel и suppressed synthetic click не
завершают цепочку и не создают segment/room;
- View и kiosk остаются fully supported и не получают новых interactions.
## 13. Архитектурный и performance-контракт
1. Face detection выделяется в чистый модуль (`wall-face-graph.ts` или
эквивалент), независимый от Lit/DOM и пригодный для exhaustive unit tests.
2. `plan-snap-overlay` остаётся источником canonical architectural axes/cuts
либо делит с detector общий pure collector; второй opening resolver запрещён.
3. Graph строится для connected component нового segment либо использует
bounded structural cache. На каждом pointermove full graph не строится.
4. Structural key включает coordinates/provenance/cuts/space/grid epsilon, но не
HA state, theme, hover, cursor, glow tick и animation.
5. Atomization и half-edge traversal должны иметь документированную сложность;
на валидном input ожидается не хуже `O((E + I) log E)` либо эквивалентного
bounded spatial подхода, где `I` — реальные intersections component.
6. Cache bounded и инвалидируется существующим geometry lifecycle.
7. Нельзя ослаблять `large-house-plan-snap-v1` budgets. Accepted-click/face
traversal получает измерение в этом профиле либо отдельный профиль с теми же
original timing/heap/DOM invariants.
8. Boolean/graph failure fail-safe: draft остаётся, room не создаётся, config не
мутирует. Исчезновение стены или частичная комната как fallback запрещены.
9. Новых network calls, HA services, dynamic code и внешних зависимостей нет.
## 14. Acceptance criteria
- **AC1 (`unit` + `smoke` + `golden`; разработчик):** Plan toolbar содержит одну
кнопку `Стены/Walls` вместо Room outline + Partition; legacy warm tool
`partition` безопасно открывает `Walls`, отдельный Split остаётся доступен и
сохраняет прежний сценарий.
- **AC2 (`unit` + `smoke`; разработчик):** open chain из 1…N segments сохраняет
per-segment endpoints/cm как active draft; смена Plan tool, mode или space
атомарно создаёт ordinary partitions, удаляет draft и исключает auto-resume.
Одна стартовая точка не пишет config.
- **AC3 (`smoke`; разработчик):** Esc/Ctrl+Z убирают последнюю точку, Reset не
становится Finish, а pan/pinch/second pointer/pointercancel/suppressed click,
reload и remount не завершают цепочку и не создают лишнюю geometry.
- **AC4 (`unit`; разработчик):** graph atomizes endpoint, exact T, proper X и
collinear overlaps; near-miss и opening/open-span gaps не соединяются;
malformed/zero input не портит valid component. Persisted old records не
мутируют.
- **AC5 (`unit`; разработчик):** half-edge traversal возвращает только simple
bounded faces с canonical identity; результат и order не зависят от ids,
record order, endpoint direction, winding и cyclic start. Compound cycles и
внешняя face исключены.
- **AC6 (`unit` + `smoke`; разработчик):** dialog появляется только для face,
добавленной последним committed segment и содержащей его atom. Existing
closed ring, reload, move/delete/Align/Resize/thickness/Undo/Redo и unrelated
segment не вызывают пассивное предложение.
- **AC7 (`unit` + `smoke`; разработчик):** новая область замыкается собственной
цепочкой, несколькими room edges, saved draft/partition, endpoint, T и X.
Adjacent scenario #138 остаётся зелёным; gap через opening/open-span не
предлагает room.
- **AC8 (`unit` + `smoke`; разработчик):** exact duplicate room не предлагается,
partial overlap отклоняется без mutation, full nesting в обе стороны
сохраняет island-room contract; devices/decor/openings/columns внутри не
блокируют valid candidate.
- **AC9 (`unit` + `smoke`; разработчик):** clean wall-to-wall split сохраняет
metadata/id/positions большей части и настраивает меньшую; отказ оставляет
original room bit-equivalent. Отдельный Split проходит прежние regression
tests.
- **AC10 (`unit` + `smoke`; разработчик):** несколько faces идут area-ascending
с stable tie-break и progress `i/N`; Create/Keep решения независимы, но final
apply один. Cancel/error/conflict/invalid combination оставляют terminal draft
без частичных rooms/partitions.
- **AC11 (`unit` + `smoke`; разработчик):** accepted batch удаляет только active
draft; existing walls/drafts/partitions не удаляются. Consumed active atoms
получают правильные room-wall cm, unconsumed части становятся partitions с
исходным cm; полный отказ сохраняет исходные segments без лишнего split.
- **AC12 (`unit` + `smoke` + `golden`; разработчик):** Plan/View/static/hidden
Iso, clean floor, Glow и sun используют существующую canonical physical
geometry #141/#150/#172; dialog/topology/render не переписывают config и не
создают визуального jump/double wall.
- **AC13 (`unit` + `smoke`; разработчик):** conversion и accepted room batch
являются именованными Undo/Redo-командами; partition/room limits проверяются
до mutation; save conflict не оставляет duplicate/lost geometry.
- **AC14 (`unit` + backend/schema review; разработчик/ревьюер):** schema,
backend, import/export, старые plans/drafts/partitions и downgrade остаются
совместимыми без migration/write-on-read; новых HA/network/security paths нет.
- **AC15 (`performance` + code review; разработчик/ревьюер):** face traversal не
запускается на pointermove/hover/HA state; large-house snap profile и новый
accepted-click measurement проходят неизменные timing, heap, cache и DOM
budgets.
- **AC16 (`smoke` + code review; разработчик/ревьюер):** touch safety floor и
View/kiosk не регрессируют; no-hover tap может выполнить тот же commit, но
hover parity редактора не обещается.
- **AC17 (`typecheck` + `unit` + `build` + documentation review; разработчик):**
implementation-loop gates зелёные, три bundle-копии побайтно одинаковы, RU/EN
i18n, оба changelog и перечисленные документы обновлены в одном
user-visible implementation commit.
## 15. План автотестов и гейтов
### 15.1 Unit
Добавить отдельный pure suite, предпочтительно
`test/wall-face-graph.test.mjs`, плюс lifecycle tests:
1. endpoint/T/X/collinear atomization, reverse/order/permutation invariance;
2. near-miss, gaps, zero/non-finite, duplicate axes;
3. rectangle, concave face, nested faces, compound cycles и unbounded face;
4. before/after delta и provenance последнего segment;
5. several faces area order и equal-area tie;
6. exact duplicate, partial overlap, inner/outer nesting;
7. clean split metadata и bit-equivalent rejection;
8. active draft → partitions, per-segment cm, limit reservation;
9. consumed/unconsumed atom conversion без mutation старых records;
10. pending batch Cancel/error/conflict/Undo/Redo;
11. legacy `partition` tool normalization и old draft compatibility;
12. structural cache invalidation и immutability.
Минимум один topology test должен краснеть на `origin/dev` без detector, а
ревьюер обязан проверить его mutation discipline.
### 15.2 Targeted production-bundle smoke
Добавить `demo/smoke_unified_wall_tool.mjs` либо эквивалент, который через
реальный UI:
1. подтверждает одну toolbar-кнопку Walls и наличие Split;
2. рисует open polyline, меняет tool/mode/space и проверяет partitions/no draft;
3. проверяет Esc/Undo и pointer safety;
4. создаёт room собственным loop, adjacent multi-edge path и older partition;
5. создаёт T и X closure без persisted rewrite старых records;
6. разделяет room wall-to-wall, проверяет original metadata и Keep walls;
7. проводит segment, замыкающий две faces, проходит progress/order и mixed
Create/Keep;
8. отменяет queue и получает исходный active draft без partial room;
9. выполняет reload/remount и safe resume;
10. сравнивает Plan/View/static/hidden Iso signatures и no-write render.
Named smoke запускается локально перед `S7-code-review` по актуальному процессу.
### 15.3 Golden
Golden matrix получает минимум:
- light/dark Plan toolbar без Partition;
- active Walls chain и T/X snap topology;
- room queue `1/N` и `N/N`;
- before/after open-chain conversion;
- View/static/hidden Iso результата с adjacent/split room.
Existing unrelated baselines не принимаются автоматически. Любой baseline —
только через `golden:accept -- --reviewed` по полному Linux artifact.
### 15.4 Performance и backend
- расширить `large-house-plan-snap-v1` accepted-click diagnostics либо добавить
сопоставимый `large-house-wall-face-v1` без ослабления исходных budgets;
- измерить no-op segment, one-face и multi-intersection component;
- подтвердить отсутствие traversal на серии pointermove и bounded cache growth;
- backend не меняется; отдельный Python test не требуется, но Linux Validate
остаётся release gate.
### 15.5 Implementation loop
Во время реализации:
```powershell
npm run typecheck
npm test
npm run build
```
После build три bundle-копии синхронизируются побайтно. Named smoke выполняется
перед передачей в code review. Полный golden/performance/backend suite — на
предрелизном этапе и Linux CI по действующему процессу.
## 16. План реализации
1. Выделить pure architectural source collector/face graph с provenance.
2. Добавить before/after delta и room eligibility/classification.
3. Свести toolbar/session state к одному Walls tool и legacy normalization.
4. Реализовать безопасное finish active draft → partitions на смене контекста.
5. Ввести pending room-face queue и atomic batch commit.
6. Переиспользовать current room creation/Split/wall normalization для
standalone/nested/split cases.
7. Добавить unit, targeted smoke, golden fixtures и performance diagnostics.
8. Обновить i18n, документы, changelog и три bundle-копии.
Точные имена helpers и раскладка файлов не являются продуктовым контрактом.
## 17. Release-артефакты
Изменение пользовательское. Implementation commit имеет `User-Visible: yes` и
в том же коммите обновляет:
- `docs/CHANGELOG.md`;
- `docs/CHANGELOG.ru.md`;
- `docs/USER-GUIDE.ru.md` — единый Walls flow, finish и room proposal;
- `docs/UX-MODES.md` — Plan toolbar и independent walls;
- `docs/CANVAS.md` — graph/snap/face topology;
- `docs/ARCHITECTURE.md` — session draft, planar graph и batch transaction;
- `docs/WALL-THICKNESS.md` — provenance consumed/unconsumed edges;
- `docs/TESTING.md` — unit/smoke/golden/performance coverage;
- `docs/TOUCH-SUPPORT.md` только если implementation уточняет documented
degradation;
- `docs/STATUS.md` после фактической реализации;
- RU/EN i18n;
- golden matrix/fixtures без непроверенного baseline;
- три поставляемые bundle-копии.
Перед бетой: exact-SHA Linux Validate, полный smoke-suite, reviewed golden,
performance smoke/Full Performance и зелёный code review. Изменение проходит
beta до stable.
## 18. Риски и снижение
| Риск | Вероятность / ущерб | Снижение |
|---|---|---|
| Cycle finder примет составной цикл вместо face | высокая / высокий | half-edge face traversal, compound/unbounded units |
| X/collinear epsilon создаст ложную комнату | средняя / высокий | canonical atomization, near-miss/gap matrix |
| Batch частично изменит несколько rooms | средняя / высокий | buffered decisions, one revalidation + one transaction |
| Split потеряет id/HA area/devices | средняя / высокий | reuse current Split path, metadata regression smoke |
| Active draft исчезнет при Cancel/remount | средняя / высокий | terminal segment persisted first, no partial commit |
| Consumed segment потеряет внешнюю часть | средняя / высокий | provenance atoms, consumed/unconsumed conversion tests |
| Existing partition удалится как «лишний» | низкая / высокий | standing no-delete-on-inference invariant |
| Finish превысит partition limit при смене tool | средняя / высокий | reserve/check capacity before accepting each segment |
| Full graph попадёт в pointermove | средняя / высокий | click-only API, benchmark diagnostics and cache assertions |
| Toolbar cleanup сломает old warm state | средняя / средний | normalize legacy `partition` → Walls unit/smoke |
| Touch gesture сохранит стену | низкая / высокий | pointercancel/pinch/suppressed-click smoke |
| Golden изменятся шире editor toolbar | средняя / средний | matrix audit, reviewed Linux acceptance only |
## 19. Откат
Откат — revert одного user-visible implementation commit вместе с tests, docs,
i18n, changelog и bundles. Новые rooms и partitions используют старую schema и
останутся читаемыми; data migration/rollback не нужны. После отката вернутся две
кнопки и прежний ограниченный closure flow.
Красный topology/smoke/golden/performance gate блокирует выпуск. Feature flag,
ослабление budget или удаление теста как способ отката не используются.
## 20. Принятые технические предположения — можно менять без продуктового ревью
1. Рекомендуется half-edge/DCEL-подобный pure representation; другой алгоритм
допустим при выполнении canonical face AC.
2. Graph epsilon переиспользует существующую geometry/grid precision и не
становится настройкой пользователя.
3. Before/after может пересчитывать только connected component candidate
segment; exact cache structure свободна.
4. Pending queue хранится только в session memory; persisted draft является
crash-safe source of truth до final batch.
5. Exact progress key, history names, helper/file/smoke/golden ids свободны при
наличии RU/EN и выполнении AC.
6. `Reset` может очистить только active session pointer либо оставить saved draft
для явного endpoint resume, но не считается owner-approved Finish trigger.
7. Active new segments разрешено разрезать в persisted partitions только когда
пользователь подтвердил room и это нужно для сохранения unconsumed части;
старые records не дробятся.
8. Clean split classification может переиспользовать `splitRoomPath`; общий face
detector не обязан заменять canonical polygon split helper.
9. Full nested rooms сохраняют действующий smallest/geometry resolution других
подсистем; #173 не вводит новый device-room arbitration.
10. Открытых продуктовых вопросов нет: Q1 принят с правкой владельца, Q2–Q7 —
defaults, 18.08.2026.