diff --git a/docs/specs/228-plan-drawing-problems.md b/docs/specs/228-plan-drawing-problems.md new file mode 100644 index 00000000..0f72c366 --- /dev/null +++ b/docs/specs/228-plan-drawing-problems.md @@ -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; + открытых продуктовых вопросов нет. diff --git a/docs/specs/README.md b/docs/specs/README.md index d2dbdcf0..f708e3eb 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -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