Files
houseplan-card/docs/specs/228-plan-drawing-problems.md
T
2026-08-26 13:39:52 +03:00

688 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 #228 — надёжное рисование стен и операции с готовым контуром
> Пункты этого исследования об отдельном редакторе границ (#148) являются
> историческими и заменены #306: отдельного инструмента и сущности виртуальной
> стены больше нет, используется обычная стена `cm:0`.
- Дата: 2026-08-22
- Тип: bug · приоритет P1
- Оценка: пользовательская ценность 9/10 · ценность для разработки 8/10 ·
сложность 9/10 · риск 9/10
- Issue: [#228](https://github.com/Matysh/houseplan-card/issues/228)
- Ветка: `issue/228-plan-drawing-problems`
Канонические документы: `docs/SCOPE.md`, `docs/USER-GUIDE.ru.md`,
`docs/ARCHITECTURE.md`, `docs/CANVAS.md`, `docs/WALL-THICKNESS.md`,
`docs/TOUCH-SUPPORT.md`, `docs/CONFIG-COMPATIBILITY.md`, а также принятые
контракты #137, #141 и #173.
## 1. Сценарий, персона и момент
Домашний администратор рисует или исправляет план в desktop-редакторе Плана
инструментом **«Стены»**. В этот момент ему нужно видеть точную ось активного
отрезка, однозначно понимать будущий узел, сохранять строгий угол с `Shift`,
создать комнату внутри уже готового контура и удалить старую комнату, не теряя
нужную кладку.
Задача закрывает J4 — точный план без внешнего SVG-редактора — и J6 — безопасное
обслуживание плана. View и киоск не получают новых действий, но обязаны без
регрессии показывать результат созданной или удалённой геометрии.
## 2. Что человек увидит до и после
**До:** активная толстая стена скрывает собственную ось и конечный узел;
неразличимые близкие endpoints молча разрешаются в один из них; snap может
сломать зафиксированный `Shift` угол, а подпись 90,1° остаться зелёной; готовый
контур нельзя превратить в комнату без нового отрезка; удаление комнаты всегда
удаляет её эксклюзивные стены.
**После:** активный отрезок показывает ось и точный будущий узел; неоднозначный
узел не записывается; `Shift` означает действительно строгий угол; клик внутри
готовой пустой области предлагает комнату и при подтверждении может исправить
единственный разрыв до 2 см; при удалении комнаты человек явно выбирает,
оставить её физические стены или удалить их.
## 3. Подтверждённое текущее состояние
1. В `_renderMarkupLayer()` толстый `previewD` строится по `path + cursor`, но
`.pathline` и `.vertex` — только по уже поставленному `_path`. Пунктирный
`.preview` существует, однако теряется на штриховке и не даёт конечного узла.
2. `resolvePlanSnap()` возвращает ближайший endpoint внутри 12 CSS px и
детерминированно разрешает равенство. Два разных узла могут быть корректными
кандидатами, но визуально сливаться на текущем масштабе; commit всё равно
молча выбирает один.
3. `wall-face-graph.ts` соединяет только точные endpoint/T/X/collinear узлы с
малым geometry epsilon. Промах 1,2 см из отчёта не образует face, даже когда
тела стен визуально соприкасаются.
4. `_deleteRoomClick()` использует native `confirm()`, удаляет `room` и клипует
open spans. Эксклюзивные room edges не материализуются в `partitions`.
5. #173 ищет только faces, появившиеся после последнего принятого сегмента.
Существующий замкнутый partition/draft ring намеренно не вызывает пассивного
предложения.
6. `_resolvePlanDrawPoint()` безусловно предпочитает любой snap candidate
`_snapDrawPoint(raw, lock45)`. `is45(deg, 0.5)` затем считает 90,1° зелёным.
Это старый контракт #137, который настоящая задача заменяет решением
владельца.
## 4. Решения владельца
22.08.2026 владелец подтвердил все defaults:
1. Визуально неразличимые разные endpoints не выбираются автоматически: commit
блокируется и предлагает увеличить масштаб.
2. Только при явном создании комнаты допустимо точно исправить единственный
промах до **2 см**; исправление и комната входят в один Undo. Больший разрыв
не чинится и подсвечивается.
3. Основное действие удаления — **«Удалить комнату, оставить стены»**; отдельное
опасное действие — **«Удалить комнату и стены»**; общие стены остаются всегда.
Сохраняемые эксклюзивные стены удерживают свою толщину и проёмы.
4. Одиночный клик пустой цепочкой внутри наименьшей подходящей области предлагает
комнату; `Shift+click` начинает новую цепочку и обходит предложение; клик по
оси или узлу сохраняет обычный Walls flow.
5. `Shift` строгий: принимается только совместимый endpoint либо точное
пересечение зафиксированного луча с линией. Зелёная подпись означает
фактически точный кратный угол; 90,1° зелёным не считается.
## 5. Scope
1. Ось и конечный узел активного Walls rubber-band поверх тела стены.
2. Явное состояние неоднозначных близких endpoints, блокирующее commit.
3. Новый strict-Shift resolver для endpoint и line snap без изменения обычного
no-modifier поведения.
4. Строгая семантика зелёной угловой подписи для Walls/Split preview.
5. Поиск наименьшей подходящей уже существующей bounded face по клику внутри неё.
6. `Shift+click` как desktop-bypass предложения комнаты до первой точки.
7. Вычисляемый repair proposal endpoint↔endpoint или endpoint↔solid-line до
2 физических сантиметров, его preview, revalidation и атомарное применение
только вместе с подтверждённым созданием комнаты.
8. Диагностика единственного большего/неоднозначного разрыва без мутации.
9. Диалог удаления комнаты с Keep walls / Delete walls / Cancel.
10. Материализация эксклюзивных физических room-wall intervals в независимые
`partitions`, сохранение толщины и перепривязка проёмов при Keep walls.
11. Каскадное удаление проёмов эксклюзивных удаляемых стен при Delete walls;
сохранение общих стен и их проёмов в обоих вариантах.
12. Один named Undo/Redo и один config transaction на создание комнаты с
repair либо удаление комнаты с выбранным вариантом.
13. RU/EN i18n, документация, unit, targeted smoke, visual/performance coverage.
## 6. Non-scope
- глобальное автоматическое выравнивание, чтение или сохранение старых планов;
- соединение обычного snap с допуском 2 см: вне явного room-create обычная
геометрия остаётся точной;
- слияние разных близких узлов на pointermove, reload, Optimize, View или HA tick;
- repair разрывов больше 2 см либо нескольких неоднозначных разрывов;
- автоматическое перемещение вершин существующих комнат;
- новый общий инструмент редактирования узлов/стен, изменение toolbar #148;
- пассивное предложение комнат при загрузке, Undo/Redo, Resize, move, delete,
thickness, Optimize или внешнем config update;
- изменение 12 CSS px общей snap-зоны, радиусов 5/10 см либо endpoint-first
порядка в однозначном no-Shift случае;
- удаление уже существующих совпадающих partitions по inference;
- новая настройка, feature flag, persisted `node`, `face`, `repair` или draft;
- touch-паритет desktop hover и modifier gestures;
- изменение рендера стыков #141, общей face-модели #173 или задачи #232 за
пределами необходимых точек расширения.
## 7. Термины и инварианты
- **Активная ось** — центрline от последней поставленной точки до текущей
resolved cursor point.
- **Визуально неразличимые endpoints** — два разных endpoint-кандидата внутри
общей snap-зоны, расстояние между центрами которых на текущем view меньше
технического CSS-порога различимости. Точное значение порога не хранится.
- **Existing face** — simple bounded face текущего structural wall graph,
существовавшая до клика и не занятая комнатой.
- **Repair proposal** — immutable список точных endpoint moves, необходимый для
получения одной face и ограниченный 2 см на каждое движение.
- **Физическая стена комнаты** — solid room-wall interval с эффективной
положительной толщиной. Intentional `open_span` и нулевая boundary-линия не
материализуются как новая masonry partition.
- **Эксклюзивный interval** — часть границы удаляемой комнаты, которую после её
удаления не продолжает ни одна другая room boundary.
Инварианты:
1. Hover, face hit-test, ambiguity и repair preview не мутируют config/layout.
2. Обычный click/tap всегда повторно решает актуальный snapshot; hover не
авторитетен.
3. Неоднозначность fail-closed: нет точки, segment, history или save.
4. Repair не остаётся вычисляемой «почти связью»: после Save persisted endpoints
точно совпадают с выбранным endpoint/линией.
5. Repair применяется только после явного подтверждения комнаты; Keep walls,
Cancel, X, Escape и ошибка оставляют исходную геометрию bit-equivalent.
6. Общую стену нельзя удалить удалением одной комнаты.
7. Проём не остаётся orphan по результату обеих веток удаления.
8. Любая успешная операция является одной атомарной geometry-командой.
## 8. UX-контракт
### 8.1 Активная ось и узел
После первой точки и до commit следующей:
- тонкая ось продолжается до resolved cursor point поверх толстого preview;
- на свободном конце виден один контрастный узел;
- при endpoint/line snap роль конечного узла выполняет уже существующий активный
snap marker, второй совпадающий круг не рисуется;
- при miss рисуется обычный live endpoint;
- committed path и active axis выглядят как одна непрерывная конструкция;
- существующий thick fill, bounded join patch, length/angle badge и hit-testing
не меняют источник геометрии.
Layer pointer-inert и `aria-hidden`; анимации нет. Light, dark, forced-colours и
reduced-motion остаются читаемыми.
### 8.2 Неоднозначные endpoints
Resolver получает три исхода endpoint-ветки: `none`, `resolved`, `ambiguous`.
- Exact-dedup endpoints остаются одним endpoint и не создают ambiguity.
- Если два разных endpoint внутри 12 CSS px визуально неразличимы на текущем
view, все конфликтующие точки получают контрастный conflict-state, а активный
resolved endpoint отсутствует.
- Pointermove только показывает конфликт. Click/tap не добавляет точку и один
раз показывает локализованный toast «Увеличьте масштаб, чтобы выбрать узел».
- После zoom, когда центры различимы, обычное nearest-endpoint правило #137
снова действует; tie остаётся детерминированным.
- Endpoint ambiguity сильнее line snap: resolver не падает сквозь конфликт к
более удобной линии.
### 8.3 Строгий `Shift`
При наличии anchor ближайший октант определяется из `anchor → raw pointer`.
1. Совместимый endpoint принимается только если лежит на выбранном ray в
canonical geometry epsilon и не даёт нулевой segment.
2. Для line candidate вычисляется пересечение solid segment с выбранным ray.
Оно принимается, только если лежит на segment, направлено от anchor и само
остаётся в текущей 12 CSS px зоне pointer.
3. Для collinear ray/line используется ближайшая ненулевая wall-bound точка на
ray; для parallel/non-intersecting или несовместимого endpoint кандидат
игнорируется.
4. При отсутствии совместимого snap результат — действующий strict 45°
grid-bound point из `snapSegment45()`.
5. Без `Shift` endpoint-first/line-snap #137 не меняется, кроме ambiguity guard.
First-point `Shift+click` не имеет anchor и поэтому использует обычную grid point,
но служит bypass из §8.5.
### 8.4 Угловая подпись
Для текущего Walls/Split segment зелёное состояние вычисляется по вектору, а не
по широкому допуску `±0,5°`:
- горизонталь, вертикаль и диагональ с `|dx| = |dy|` в geometry epsilon зелёные;
- неизбежный floating-point шум точного пересечения допускается;
- 89,9°/90,1° и другие реально некратные значения красные;
- отображаемое число продолжает округляться до 0,1° и совпадает с цветовым
обещанием.
### 8.5 Создание комнаты из готовой области
При `Walls`, пустой `_path` и обычном click/tap применяется такой порядок:
1. room card, physical editor chrome и другие существующие владельцы события
сохраняют приоритет;
2. ambiguity либо клик в snap-зоне endpoint/line обрабатывается обычным Walls
flow и не предлагает комнату;
3. `Shift+click` на desktop всегда начинает новую цепочку;
4. иначе structural graph ищет existing face, содержащую raw click;
5. exact room duplicate, partial overlap и face, чья внутренняя точка уже
принадлежит любой комнате, исключаются;
6. из оставшихся выбирается наименьшая площадь, затем canonical key;
7. открывается стандартный room dialog/queue #173 с Create room, Keep walls и
Cancel; existing partitions/drafts не удаляются после Create.
Keep walls и Cancel для existing face ничего не пишут. Следующий обычный click
может снова предложить ту же face; новый persisted dismiss-state не вводится.
### 8.6 Допуск 2 см и явный repair
Exact graph проверяется первым. Если exact face под click/последним segment не
найдена, разрешён поиск face с одним однозначным repair proposal:
- endpoint→endpoint либо endpoint→внутренняя точка solid structural line;
- физическое расстояние вычисляется через `cell_cm/gridPitch` и не превышает
2,0 см включительно;
- endpoint room boundary является неподвижным target; двигается endpoint
independent partition/draft/active chain;
- room vertex никогда не двигается;
- если обе стороны independent, выбирается deterministic target по геометрии и
grid-canonicality, не по record order/id;
- несколько равноценных target, несколько необходимых разрывов, zero-length,
self-intersection, open-span bridge или конфликт проёма делают repair
недопустимым;
- opening types не разрывают structural axis, а `open_span` остаётся настоящим
gap и никогда не чинится;
- preview подсвечивает исходный разрыв и точку будущего соединения.
Room dialog не применяет repair заранее. Create повторно проверяет config
revision, face, 2 см, limits, overlap и hosted openings, затем одним commit:
1. двигает только предложенные independent endpoints;
2. обновляет compatibility-проекцию `x/y/angle` проёмов изменённого partition,
сохраняя host id/t, если strict jamb validation остаётся зелёной;
3. создаёт room по уже точному repaired graph;
4. применяет обычный consumed/unconsumed contract #173;
5. записывает одну named Undo-команду.
Если gap больше 2 см, но около click существует единственный диагностируемый
разрыв в screen snap-range, он подсвечивается и room dialog не открывается.
Неоднозначный или недиагностируемый открытый набор остаётся обычным Walls flow;
House Plan не строит комнату из произвольного почти-цикла.
### 8.7 Удаление комнаты
Клик инструментом «Удалить комнату» открывает `hp-dialog`, не native `confirm`:
- title называет комнату;
- основная кнопка **«Удалить комнату, оставить стены»**;
- danger-кнопка **«Удалить комнату и стены»**;
- Cancel/X/Escape ничего не меняют.
**Keep walls:**
1. до удаления материализуется effective room-wall profile;
2. solid positive-thickness exclusive intervals становятся `partitions` с теми
же endpoints и `cm`;
3. совпадающая уже существующая partition переиспользуется, а не дублируется;
4. room-wall opening эксклюзивного interval получает явный partition host и
сохраняет type, length, flips, contact/lock и положение; strict jamb margin
валидируется до commit;
5. shared intervals остаются производными стенами соседей и не получают
partition;
6. virtual/open и zero-thickness boundary intervals не материализуются как
physical partition;
7. target room удаляется, walls/open spans нормализуются действующими helpers.
**Delete walls:**
1. target room удаляется без новых partitions;
2. openings, чья resolved structural ассоциация была только с эксклюзивной
стеной target room, удаляются тем же commit;
3. opening на shared wall сохраняется и после re-resolution принадлежит
surviving room wall;
4. уже существующая совпадающая partition и её hosted opening не удаляются:
это отдельный объект, а не стена комнаты.
Обе ветки сохраняют neighbour rooms/metadata/devices, пересчитывают open spans,
wall intervals и device projection и дают один Undo. Capacity/validation failure
показывается до мутации и оставляет исходный snapshot.
## 9. Модель данных и архитектурный контракт
### 9.1 Snap result
`PlanSnapCandidate` либо соседний pure type расширяется до результата,
различающего `resolved` и `ambiguous`. Список конфликтующих endpoint содержит
только immutable coordinates/keys; CSS threshold передаётся из live view и не
входит в cache key static geometry.
Strict-Shift projection выделяется в pure helper. Card orchestrator передаёт
raw point, anchor, ray, candidate и geometry epsilon; SVG DOM не является
источником координат.
### 9.2 Existing face и repair proposal
`wall-face-graph.ts` получает read-only face-at-point query поверх уже построенного
exact graph. Она не меняет правило #173 «новые faces только после segment»:
новый query вызывается только явным idle click.
Near-junction repair строится отдельным pure planning pass или расширением graph
API с provenance:
- input — room axes после `open_spans`, partitions, inactive drafts и active
chain, physical conversion и click/added-segment context;
- output — candidate face, exact endpoint moves и diagnostics;
- no output при неоднозначности либо необходимости двигать room vertices;
- record order, ids, direction и winding не меняют repaired geometry;
- input objects не мутируются.
### 9.3 Room deletion plan
До показа/commit строится immutable deletion plan:
- exact old room-wall intervals и ownership после atomization;
- shared/exclusive classification;
- partitions to create/reuse;
- room-wall openings to rehost, keep or delete;
- next walls/open spans и capacity/validation result.
Commit revalidates room id и structural fingerprint. UI не повторяет геометрию
планировщика самостоятельно.
### 9.4 Кэши и lifecycle
- Static snap geometry cache сохраняется; ambiguity зависит от event/view и не
создаёт новый O(E) DOM на каждый pointermove.
- Exact face graph использует действующий bounded LRU. Existing-face и repair
query запускаются только по click/accepted segment, не по hover/render/HA tick.
- Mode/tool/space change, pointerleave, Cancel и external config adoption очищают
active/conflict/repair preview.
- Никаких timer, websocket, service, fetch или localStorage путей не добавляется.
## 10. Совместимость и миграция
Новых persisted полей и write-on-read нет.
- `rooms`, `walls`, `room_drafts`, `partitions`, `openings`, `open_spans` и
layout сохраняют текущую schema;
- repair записывает только допустимые текущей schema endpoints и обновлённые
compatibility-поля hosted openings;
- Keep walls создаёт обычные `partitions` с `cm` 1–100; нулевая boundary-линия
не превращается в новый тип данных;
- import/export, model version и backend storage API не меняются;
- backend semantic validation должен принять обе ветки удаления и repaired
partition; для новых schema keys Python-изменений нет;
- downgrade читает созданные rooms/partitions как обычные существующие данные;
- старые планы не меняются до явного Create/Delete;
- no-op/Cancel/Keep existing face возвращают bit-equivalent config.
## 11. UX, i18n, accessibility и touch
### 11.1 RU/EN i18n
Добавляются или уточняются ключи для:
- ambiguous endpoints / рекомендация увеличить масштаб;
- gap больше 2 см либо неоднозначный repair;
- room-delete dialog title/body;
- «Удалить комнату, оставить стены»;
- «Удалить комнату и стены»;
- validation/capacity/opening failure при сохранении стен;
- history «Создать комнату и соединить стены» и две ветки удаления, если для
понятного Undo нужны разные названия.
Термины следуют `docs/USER-GUIDE.ru.md`: **Стены**, **комната**,
**независимая стена**, **проём**, **увеличить масштаб**.
### 11.2 Accessibility
- активная/конфликтная геометрия декоративна, `aria-hidden`, pointer-inert;
- toast получает действующий status/announcement path;
- delete dialog использует `hp-dialog`: title, focus trap, Escape и restore focus;
- primary/danger/Cancel имеют явные текстовые labels и логичный tab order;
- forced colours различает normal active node и conflict;
- новой клавиатурной навигации по plan nodes нет.
### 11.3 Touch
**Touch editor: best effort / intentionally degraded.**
- tap без pointermove повторно решает ambiguity, existing face и repair;
- hover preview не обещается;
- на touch нет modifier-bypass: tap внутри eligible empty face предлагает
комнату; свободную цепочку внутри неё начинают с axis/node либо на desktop;
- pinch, pan, second pointer, pointercancel и suppressed synthetic click не
создают point/room, не применяют repair и не подтверждают delete;
- View/kiosk остаются fully supported и не получают editor DOM/actions.
Эта deliberate degradation описывается в user guide/release notes.
## 12. Производительность и безопасность
- pointermove остаётся O(E) resolver по cached geometry и обновляет только
ограниченное active/conflict состояние;
- face traversal/repair/delete planning не запускаются на pointermove, render
или HA state;
- large-house budgets не ослабляются; отдельные accepted-click measurements
покрывают exact face, one-repair и ambiguity;
- conflict set/repair proposal/deletion plan ограничены текущим числом
архитектурных рёбер и существующими schema limits;
- нет HTML от пользователя, новых URL, network, HA action или permission paths;
- delete/repair mutation проходят существующий config write/CAS и Undo boundary;
- secure opening contact/lock references только сохраняются либо удаляются как
данные; сервисы HA не вызываются.
## 13. Acceptance criteria
- **AC1 (`unit` + `smoke` + pre-beta `golden`; разработчик):** после первой
точки active Walls segment показывает непрерывную ось поверх thick preview и
ровно один конечный узел для miss/endpoint/line snap в light/dark/forced
colours; pointer/ARIA и committed geometry не меняются.
- **AC2 (`unit` + `smoke`; разработчик):** два exact-dedup endpoints остаются
одним, а разные visually-indistinguishable endpoints дают conflict state;
click/tap не пишет point/history/config и показывает zoom toast. После zoom
nearest endpoint снова принимается; line fallback при конфликте отсутствует.
- **AC3 (`unit` + `smoke`; разработчик):** без `Shift` #137 остаётся
pixel/coordinate-compatible; с `Shift` принимаются только endpoint на strict
ray либо точное line/ray intersection в solid interval и hit zone;
несовместимый snap даёт grid-bound strict-45 point.
- **AC4 (`unit` + `smoke`; разработчик):** angle badge зелёный для точных
0/45/90° vectors с численным epsilon и красный для 89,9°/90,1°; показанное
значение и committed endpoint совпадают.
- **AC5 (`unit` + `smoke`; разработчик):** idle ordinary click внутри
наименьшей existing empty bounded face открывает room dialog без нового
segment; snap-zone click рисует стену, `Shift+click` bypass начинает цепочку,
occupied/duplicate/partial face не предлагаются.
- **AC6 (`unit` + `smoke`; разработчик):** unique endpoint↔endpoint и
endpoint↔line gap `<=2,0 cm` создаёт repair proposal; только Create атомарно
записывает exact junction + room и один Undo. Keep/Cancel/X/Escape/error
оставляют config bit-equivalent.
- **AC7 (`unit` + `smoke`; разработчик):** `>2 cm`, two-gap, equal-target,
room-vertex move, open-span bridge, zero/self-intersection и invalid hosted
opening не ремонтируются; доступный больший single gap подсвечивается без
room/save.
- **AC8 (`unit` + `smoke`; разработчик):** room, созданная из existing/repaired
face, соблюдает #173 eligibility/order/limits; existing room walls, drafts и
partitions не удаляются, active consumed/unconsumed cm/provenance сохраняются.
- **AC9 (`smoke`; разработчик):** Delete room открывает `hp-dialog` с primary
Keep walls, danger Delete walls и Cancel; focus/Escape/restore работают,
native `confirm` не используется.
- **AC10 (`unit` + `smoke`; разработчик):** Keep walls материализует только
positive solid exclusive intervals как partitions с exact endpoints/cm,
reuses coincident partition, rehosts valid openings и не дублирует shared,
virtual или zero-thickness intervals.
- **AC11 (`unit` + `smoke`; разработчик):** Delete walls удаляет только target
room и openings эксклюзивных room walls; shared walls/openings, neighbour
rooms и explicit coincident partitions/hosted openings остаются.
- **AC12 (`unit` + `smoke`; разработчик):** обе delete ветки и room+repair —
один named Undo/Redo/config save; capacity, opening validation, stale room и
conflict fail before mutation; Redo детерминированно восстанавливает результат.
- **AC13 (`unit` + targeted smoke + code review; разработчик/ревьюер):**
Plan/View/static/hidden Iso, clean floor, Glow, sun и junction rendering
используют existing canonical consumers; no-op hover/dialog/render не меняют
config, а shared masonry не получает double wall/tooth/gap.
- **AC14 (`smoke` + code review; разработчик/ревьюер):** tap выполняет
authoritative click resolution, но pinch/pan/second pointer/pointercancel/
suppressed click ничего не сохраняют; View/kiosk pixels/actions не меняются.
- **AC15 (`performance` + code review; разработчик/ревьюер):** 60-room/
60-partition profile проходит pointer ambiguity, existing-face click и
one-repair query без ослабления timing/heap/cache/DOM budgets; graph не
запускается на pointermove/HA ticks.
- **AC16 (`unit` + backend/schema review; разработчик/ревьюер):** current
schema/import/export/storage/API/permissions остаются совместимыми; старые
планы не мигрируют, новые rooms/partitions/openings валидны для backend;
no network/security paths.
- **AC17 (`typecheck` + `unit` + `build` + documentation review; разработчик):**
fast gates зелёные, три bundle-копии побайтно одинаковы, named smokes зелёные,
RU/EN i18n, оба changelog и перечисленные документы обновлены в одном
`User-Visible: yes` implementation commit.
## 14. План автотестов и гейтов
### 14.1 Unit
Расширить `test/plan-snap-overlay.test.mjs`, `test/wall-face-graph.test.mjs` и
добавить pure room-deletion suite:
1. active-axis projection: miss/resolved/duplicate-node suppression;
2. ambiguity по screen threshold, exact dedup, zoom separation, endpoint-first;
3. strict ray endpoint, diagonal/vertical/horizontal intersection, collinear,
parallel, outside interval/hit zone, immutability;
4. exact-octant badge против 89,9/90,1 и floating epsilon;
5. exact existing face hit, smallest/nested/order, room occupancy exclusion;
6. 1,2 см endpoint/endpoint и endpoint/line repair при `cell_cm:1`;
7. physical invariance threshold at other `cell_cm`, exact 2,0 / 2,01 boundary;
8. ambiguous/multi-gap/open-span/room-vertex/hosted-opening negative matrix;
9. repair revalidation, Keep/Cancel immutability, one history snapshot;
10. delete classification for exclusive/shared/partial-collinear/open spans;
11. positive cm preservation, coincident partition reuse, limits;
12. room opening rehost with t/x/y/angle and delete cascade;
13. shared opening and explicit hosted partition survival;
14. walls/open spans normalisation, Undo/Redo and record-order invariance.
Каждая новая ветка имеет mutation test: удаление ambiguity guard, поднятие
порога выше 2 см, движение room vertex либо удаление shared opening делает
соответствующий тест красным.
### 14.2 Targeted production-bundle smoke
Добавить `demo/smoke_plan_drawing_repairs.mjs` либо эквивалент и при
необходимости расширить `demo/smoke_plan_snap_overlay.mjs` /
`demo/smoke_unified_wall_tool.mjs`:
1. active thick segment показывает axis/node и сохраняет exact preview point;
2. два близких endpoint блокируют click, zoom разрешает выбранный endpoint;
3. strict Shift endpoint/line и красный 90,1°;
4. existing closed partition ring → click → room dialog → Create/Keep/Cancel;
5. `Shift+click` начинает chain, snap-zone click не открывает room;
6. 1,2 см gap → preview → Create → exact persisted repair + Undo/Redo;
7. 2,01 см/ambiguous/open-span/invalid-opening negative paths;
8. Keep walls: exclusive thick walls/room openings становятся hosted
partitions, shared wall/opening не дублируются;
9. Delete walls: exclusive opening удалён, shared/explicit partition сохранены;
10. pan/pinch/pointercancel/suppressed tap и View/kiosk absence.
Перед `S7-code-review` обязательны точные команды:
```powershell
npm run typecheck
npm test
npm run build
node demo/smoke_plan_snap_overlay.mjs
node demo/smoke_unified_wall_tool.mjs
node demo/smoke_plan_drawing_repairs.mjs
```
После build три bundle-копии синхронизируются и сравниваются побайтно.
### 14.3 Golden и performance
- pre-beta golden review проверяет light/dark Plan с active axis, endpoint
conflict, repair preview и delete dialog;
- существующие View/static/hidden-Iso baselines не меняются только из-за
editor chrome; baseline принимается исключительно из полного reviewed Linux
artifact через `golden:accept -- --reviewed`;
- large-house profile получает accepted-click cases exact/ambiguous/repair и
доказывает отсутствие graph work на pointermove;
- бюджеты не ослабляются; полный golden/performance suite остаётся pre-beta
gate на exact SHA.
### 14.4 Backend
Backend schema не меняется. Pure frontend unit + backend/schema code review
доказывают действующие `partition`/host limits. Полный HA harness выполняется в
Linux CI/WSL; native Windows из-за `fcntl` каноном не является.
## 15. План реализации
1. Расширить pure snap result ambiguity и добавить strict-ray projection.
2. Подключить active axis/node render без дублирования snap marker.
3. Заменить wide angle colour check на exact-vector predicate для editor badge.
4. Добавить exact face-at-point query и idle-click routing.
5. Реализовать provenance-aware near-junction repair planner и atomic apply.
6. Реализовать pure room deletion plan с partition/opening consequences.
7. Заменить native confirm на `hp-dialog` и связать оба commits с history/save.
8. Добавить unit, targeted smokes и performance diagnostics.
9. Обновить i18n, docs, changelogs и три bundle-копии.
Точные helper/file names свободны при соблюдении границ модулей и AC.
## 16. Затронутые файлы и модули
Ожидаемо:
- `src/plan-snap-overlay.ts` — ambiguity и candidate metadata;
- `src/wall-face-graph.ts` — exact face-at-point / repair planning extensions;
- новый pure helper room deletion/repair либо эквивалент;
- `src/houseplan-card.ts` — orchestration, dialog, history, lifecycle;
- `src/styles.ts` — active/conflict/repair visuals;
- `src/i18n/en.json`, `src/i18n/ru.json`;
- `test/plan-snap-overlay.test.mjs`, `test/wall-face-graph.test.mjs`, новый
deletion/repair suite;
- `demo/smoke_plan_snap_overlay.mjs`, `demo/smoke_unified_wall_tool.mjs`, новый
targeted smoke и при необходимости performance fixture;
- `docs/USER-GUIDE.ru.md`, `docs/CANVAS.md`, `docs/ARCHITECTURE.md`,
`docs/WALL-THICKNESS.md`, `docs/TOUCH-SUPPORT.md`, `docs/TESTING.md`,
`docs/STATUS.md`, оба changelog;
- `dist/houseplan-card.js`, `custom_components/houseplan/frontend/houseplan-card.js`,
`demo/srv/assets/houseplan-card.js` как один generated build.
Изменение Python/schema не ожидается. Если реализация потребует новый persisted
field либо расширение допустимых `cm`, работа останавливается и возвращается в
`S3-spec`: это изменение контракта вне утверждённого ТЗ.
## 17. Release-артефакты
Изменение пользовательское. Implementation commit имеет `User-Visible: yes` и
в том же коммите обновляет:
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` со ссылкой на #228;
- `docs/USER-GUIDE.ru.md` — active preview, ambiguity, strict Shift, existing
face, 2 см repair, room delete choices и touch limitation;
- `docs/CANVAS.md` — обновлённый snap/Shift/face contract;
- `docs/ARCHITECTURE.md` — repair/deletion plans и atomic transactions;
- `docs/WALL-THICKNESS.md` — materialized exclusive positive wall intervals;
- `docs/TOUCH-SUPPORT.md` — modifier bypass отсутствует на touch;
- `docs/TESTING.md` и `docs/STATUS.md` после реализации;
- RU/EN i18n;
- named smokes и performance diagnostics;
- три одинаковые bundle-копии.
Перед бетой: полный smoke suite, reviewed Linux golden, performance smoke/Full
Performance, Linux backend harness и Validate на exact candidate SHA. Изменение
проходит beta/RC до stable.
## 18. Риски и снижение
| Риск | Вероятность / ущерб | Снижение |
|---|---|---|
| Ambiguity заблокирует обычный endpoint | средняя / высокий | screen-threshold matrix, zoom smoke, exact dedup |
| Strict Shift потеряет полезный line snap | средняя / средний | exact ray/segment intersection и no-Shift parity |
| Repair соединит намеренный разрыв | средняя / высокий | 2 см, one-gap, explicit Create, open-span/ambiguity guards |
| Repair изменит room polygon | низкая / высокий | room vertices immutable, provenance units |
| Hosted opening перестанет помещаться | средняя / высокий | preflight strict validation, fail before mutation |
| Existing face click мешает начать стену | средняя / средний | snap-zone priority + desktop Shift bypass + docs |
| Keep walls создаст двойную кладку | средняя / высокий | interval ownership и coincident partition reuse |
| Delete walls удалит shared opening | низкая / высокий | old/new association plan, shared regression smoke |
| Multi-action commit сохранится частично | средняя / высокий | immutable plan + revalidation + one history/save |
| Graph попадёт в pointermove | средняя / высокий | click-only API и performance diagnostics |
| Touch tap сохранит лишнюю геометрию | низкая / высокий | gesture safety smoke |
| Визуальные golden изменятся шире scope | средняя / средний | targeted frames, reviewed Linux acceptance only |
## 19. Откат
Откат — revert одного user-visible implementation commit вместе с tests, docs,
i18n, changelogs и bundles. Новых persisted полей нет; rooms, partitions и
hosted openings, уже явно созданные новой версией, остаются валидными для старой.
Автоматическая data rollback не нужна.
Красный targeted smoke/golden/performance/backend gate блокирует бету. Ослабление
budget, удаление теста или silent fallback к приблизительной связи не являются
откатом.
## 20. Принятые технические предположения — можно менять без продуктового ревью
1. Порог screen-различимости endpoints рекомендуется держать около 8 CSS px;
точное значение свободно при выполнении zoom/ambiguity AC.
2. Exact-octant лучше проверять по `dx/dy`, а не по degrees; helper/name свободны.
3. Repair target priority: immutable room axis → более grid-canonical
independent endpoint → stable geometric tie. Persisted id не определяет
координату результата.
4. Диагностика gap больше 2 см может искать один virtual bridge только в
текущей 12 CSS px event-zone; произвольный open-chain solver не требуется.
5. Existing face может переиспользовать `_wallFaceBatch` с новым origin либо
отдельный pending object, если Create/Keep/Cancel остаются эквивалентны.
6. Delete planner может atomize old room edges через `wallIntervals()` и
canonical segment helpers; DOM measurement запрещён.
7. Совпадающая partition переиспользуется только при exact collinearity/coverage
и совместимой толщине; иначе создаётся отдельный interval в пределах limits.
8. Нулевая room boundary — контур/бордер, а не physical masonry partition.
Настоящая поддержка zero-cm independent wall требует отдельного model review.
9. Точные i18n/history keys, названия helper/test/smoke и раскладка файлов
свободны.
10. Все пять продуктовых вопросов закрыты defaults владельца 22.08.2026;
открытых продуктовых вопросов нет.