mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 19:58:50 +00:00
688 lines
48 KiB
Markdown
688 lines
48 KiB
Markdown
# 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;
|
||
открытых продуктовых вопросов нет.
|