diff --git a/docs/specs/173-unified-wall-tool.md b/docs/specs/173-unified-wall-tool.md new file mode 100644 index 00000000..8211fe78 --- /dev/null +++ b/docs/specs/173-unified-wall-tool.md @@ -0,0 +1,692 @@ +# Issue #173 — единый инструмент рисования стен и предложение комнаты по замыканию + +- **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. diff --git a/docs/specs/README.md b/docs/specs/README.md index 65358648..d722648d 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -93,6 +93,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным | [#157](https://github.com/Matysh/houseplan-card/issues/157) Тип проёма «Открытый проём» | [157-open-passage.md](157-open-passage.md) | | [#150](https://github.com/Matysh/houseplan-card/issues/150) Точная геометрия коллинеарного перепада толщины | [150-wall-thickness-transition.md](150-wall-thickness-transition.md) | | [#172](https://github.com/Matysh/houseplan-card/issues/172) Нулевой Split-разделитель не получает ложную толщину | [172-zero-divider-taper.md](172-zero-divider-taper.md) | +| [#173](https://github.com/Matysh/houseplan-card/issues/173) Единый инструмент рисования стен и предложение комнаты по замыканию | [173-unified-wall-tool.md](173-unified-wall-tool.md) | | [#174](https://github.com/Matysh/houseplan-card/issues/174) Связанный виртуальный источник следует реальному контроллеру | [174-linked-virtual-light-controller.md](174-linked-virtual-light-controller.md) | ## Правило актуализации