docs: specify plan drawing repairs

Issue: #228
User-Visible: no
This commit is contained in:
Sergey Matyunin
2026-08-22 10:55:08 +03:00
parent 66ffd6fdda
commit e44b3c97b0
2 changed files with 684 additions and 0 deletions
+683
View File
@@ -0,0 +1,683 @@
# Issue #228 — надёжное рисование стен и операции с готовым контуром
- Дата: 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;
открытых продуктовых вопросов нет.
+1
View File
@@ -64,6 +64,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
| [#229](https://github.com/Matysh/houseplan-card/issues/229) Сращивание коллинеарных отрезков стен | [229-merge-collinear-partitions.md](229-merge-collinear-partitions.md) |
| [#226](https://github.com/Matysh/houseplan-card/issues/226) Entity-marker не дублируется родительским HA-устройством | [226-entity-parent-dedup.md](226-entity-parent-dedup.md) |
| [#223](https://github.com/Matysh/houseplan-card/issues/223) Optimize канонизирует координаты без floating-point шума | [223-optimize-coordinate-canonicalization.md](223-optimize-coordinate-canonicalization.md) |
| [#228](https://github.com/Matysh/houseplan-card/issues/228) Надёжное рисование стен и операции с готовым контуром | [228-plan-drawing-problems.md](228-plan-drawing-problems.md) |
## P2