diff --git a/docs/specs/148-tool-first-plan-editor.md b/docs/specs/148-tool-first-plan-editor.md new file mode 100644 index 00000000..7ad46fce --- /dev/null +++ b/docs/specs/148-tool-first-plan-editor.md @@ -0,0 +1,433 @@ +# Issue #148 — Tool-first редактирование комнаты в Plan Editor + +- **Issue:** https://github.com/Matysh/houseplan-card/issues/148 +- **Редакция:** первая редакция для независимого ревью; статус задачи определяется + только метками issue +- **Тип / приоритет:** feature + polish / P2 +- **Оценка:** пользовательская ценность 8/10; ценность для разработки 7/10; + сложность и риск 8/10 +- **Область:** нижняя панель Plan Editor, шесть инструментов редактирования + комнаты, выбор целей, локальные параметры, Undo/Redo, keyboard и accessibility +- **Модель данных:** сохранённый формат плана не меняется; `ToolSession` — только + runtime-состояние редактора +- **Связано:** `docs/UX-MODES.md`, `docs/CANVAS.md`, `docs/RESIZE.md`, + `docs/WALL-THICKNESS.md`, `docs/TOUCH-SUPPORT.md` + +## 1. Сценарий и продуктовый контекст + +**Персона:** администратор дома, который строит и исправляет план в +desktop-браузере. + +**Поверхность и момент:** пользователь открывает Plan Editor и хочет выполнить +операцию над комнатой, ещё не выбрав комнату или стену. + +**До → после, без терминов реализации:** сейчас состав нижней панели зависит от +того, сколько комнат уже выбрано, и заставляет сначала угадывать нужное +выделение; после изменения пользователь одним и тем же способом сначала +выбирает действие, затем требуемые объекты и только после валидного выбора +настраивает и применяет результат. + +Это поддерживает J4 и J6 из `docs/SCOPE.md`: встроенный редактор становится +предсказуемым инструментом построения и последующего исправления плана. + +## 2. Решение владельца и источник UX-контракта + +Владелец 15.08.2026 отклонил предложенное разбиение на этапы: общий framework и +все шесть действий поставляются **одним релизом**. Каноническая запись: +https://github.com/Matysh/houseplan-card/issues/148#issuecomment-5301106841 + +Приложенный к issue архив `Plan Editor.zip` является UX-референсом. Его +`DECISION.md`, `UX-SPECIFICATION.md`, `WIREFRAMES.md`, +`interactive-prototype.html` и `states.html` уточняют поведение, но не являются +готовым кодом или отдельной системой стилей. При расхождении приоритет таков: + +1. решения владельца в issue; +2. это ТЗ; +3. текстовые документы архива; +4. визуальная реализация прототипа. + +## 3. Скоуп одного релиза + +В задачу входят: + +1. единый runtime lifecycle сессии инструмента; +2. стабильная группа «Редактировать комнату» с действиями «Объединить», + «Разделить», «Размер», «Граница», «Толщина», «Удалить»; +3. перевод на tool-first контракт всех шести действий без сохранения скрытого + preselection-зависимого маршрута; +4. инструкции, счётчики, validation errors и визуальные состояния active, + hover, selected и invalid; +5. локальная панель толщины и контекстные настройки остальных инструментов; +6. единые правила отмены, переключения, фокуса, клавиатуры и Undo/Redo; +7. сохранение верхней навигации и занимаемой нижней панелью геометрии; +8. unit, browser smoke, visual golden, документация и RU/EN changelog. + +Поставка только framework, Merge или Thickness при незавершённых остальных +четырёх инструментах не выполняет задачу. + +## 4. Не входит в задачу + +- новая верхняя навигация, постоянный правый inspector или перенос редактора в + отдельную страницу; +- новые архитектурные операции, которых сейчас нет в Plan Editor; +- изменение результата Merge/Split/Resize/Boundary/Thickness/Delete после + подтверждения, кроме необходимого выравнивания их UX lifecycle; +- изменение моделей комнат, стен, проёмов, перегородок, колонн и HA Areas; +- миграция, schema version или backend API; +- переход Devices/Background editor на новый lifecycle; +- полноценный touch-first Plan Editor: редактор остаётся desktop-first, при + обязательном safety floor из раздела 11; +- изменение View, киоска и верхней segmented navigation; +- новый верхнеуровневый режим «Удаление». + +## 5. Информационная архитектура нижней панели + +### 5.1. Стабильные группы + +В Plan сохраняются существующая верхняя навигация и общий каркас нижней панели. +Панель предоставляет стабильные группы: + +- «Выбрать»; +- «Редактировать комнату»; +- «Контур комнаты»; +- «Конструкции»; +- «Проём». + +Внутри «Редактировать комнату» состав не меняется от выделения на холсте: + +1. Объединить; +2. Разделить; +3. Размер; +4. Граница; +5. Толщина; +6. Удалить. + +Группа доступна без предварительного выбора комнаты. Условия конкретной +операции показывает активная сессия, а не disabled-состав подпанели. + +### 5.2. Шесть обзорных состояний + +Требуемый комплект продуктовых состояний/скриншотов: + +1. без выбора; +2. редактирование комнаты; +3. контур комнаты; +4. конструкции; +5. проём; +6. подтверждение удаления. + +«Подтверждение удаления» — временное destructive-состояние внутри группы +«Редактировать комнату». Оно не добавляет шестую постоянную группу или вкладку. + +### 5.3. Геометрия панели + +- раскрытие группы и смена фазы не меняют размеры stage и не сдвигают холст; +- содержимое остаётся в выделенной нижней области и использует существующий + shared secondary controller; +- overflow прокручивается внутри панели; кнопка подтверждения и текущая + инструкция не уходят за viewport; +- локальная плавающая панель может перекрывать stage, но не участвует в layout. + +## 6. Каноническая сессия инструмента + +Реализация может уточнить имена типов, но обязана иметь один явный источник +runtime-истины со следующим смыслом: + +```ts +interface ToolSession { + tool: 'merge' | 'split' | 'resize' | 'boundary' | 'wallthick' | 'delete' | null; + phase: 'idle' | 'selecting_targets' | 'validating_targets' | 'configuring' | 'applying'; + targets: ToolTarget[]; + anchor?: { x: number; y: number }; + draft: Record; + validationError?: { key: string; targetId?: string }; +} +``` + +Обязательные инварианты: + +1. `tool === null` означает отсутствие незавершённой операции; +2. тип, количество, порядок и совместимость целей задаёт активный инструмент; +3. `selectedRooms.length` и другие глобальные selection counts не определяют + состав подпанели; +4. hover не является target и не меняет конфигурацию; +5. invalid target не добавляется в `targets`; +6. `draft` и targets не пишутся в server config, localStorage или history; +7. единственная граница Undo/Redo — успешно применённая команда; +8. после apply сессия завершается и очищается до `idle`; повтор операции + требует нового явного выбора инструмента; +9. переключение space/mode, закрытие редактора и потеря актуальной цели + безопасно отменяют незавершённую сессию; +10. asynchronous validation/apply проверяют revision сессии: запоздалый ответ + отменённого или заменённого инструмента не меняет UI и конфигурацию. + +Существующие разрозненные поля допустимы как внутренние детали перехода, но не +могут оставаться конкурирующими источниками lifecycle. + +## 7. Контракты шести инструментов + +### 7.1. Объединить + +1. Клик по «Объединить» включает инструмент и показывает «Выберите две + смежные комнаты», счётчик `0 из 2`. +2. Первая валидная комната получает selected-state, счётчик становится + `1 из 2`. +3. Повторный клик по уже выбранной комнате снимает её; порядок остальных целей + сохраняется. +4. Вторая кандидатура валидируется до добавления: это другая существующая + комната того же space/floor с подходящей общей границей, не занятая + конфликтующей операцией. +5. Невалидная комната не заменяет первую и не становится третьей целью. Рядом + с подпанелью показывается конкретная причина, а кандидат получает временный + invalid-state. +6. После второй валидной комнаты автоматически открываются действующие + настройки Merge. Фокус переходит на их заголовок/первое поле. +7. Пользователь может применить, отменить всю сессию или вернуться к выбору + комнат, сохранив первую/обе валидные цели согласно явной кнопке «Назад». +8. Конфигурация и history меняются только после подтверждения. + +### 7.2. Разделить + +1. Инструмент просит выбрать одну существующую комнату. +2. После валидного выбора начинается действующий маршрут задания линии Split; + незавершённые точки являются `draft`, а не history-командами. +3. Начальная/конечная точки, пересечения и результирующие полигоны валидируются + по текущим правилам Split; invalid click не повреждает уже валидный draft. +4. Настройки новой комнаты открываются только после законченной валидной линии. +5. «Назад» из настроек возвращает к редактированию линии; подтверждение + применяет Split атомарно, cancel очищает его полностью. + +### 7.3. Размер + +1. Инструмент просит выбрать одну комнату либо допустимый её сегмент. +2. Первый валидный target включает существующие resize handles и preview. +3. Live preview хранится только в сессии; server config и соседние render paths + не видят частично применённую геометрию. +4. Настройки/контролы показываются после валидного target, а apply фиксирует + одну атомарную history-команду. +5. Cancel восстанавливает точную исходную геометрию без записи и без Undo item. + +### 7.4. Граница + +1. Инструмент объясняет требуемую текущей операцией цель и принимает только + допустимую общую границу/её endpoints. +2. Первая точка или boundary target переводит сессию в выбор следующей цели; + pan/pinch не подтверждает точку. +3. Невалидная цель не заменяет валидный anchor и сопровождается конкретной + причиной. +4. Конфигурация границы применяется одной подтверждённой командой. Esc во + время двухточечного выбора сначала возвращает к выбору целей по правилам + раздела 9, не создавая history. + +### 7.5. Толщина + +1. Инструмент показывает «Выберите стену или несколько стен». +2. Доступный atomic wall segment получает hover-state; клик добавляет/снимает + его из набора targets. Открытый span и другой невалидный объект не + добавляются. +3. После первого выбранного сегмента появляется локальная панель рядом с ним: + поле «Толщина», диапазон `1–100 см` (с действующим imperial-equivalent), + «Применить ко всем стенам комнаты» и кнопка подтверждения с галочкой. +4. При нескольких targets anchor панели — общий bbox выбранных сегментов либо + последний выбранный сегмент; выбор должен быть детерминирован. +5. Предпочтительное положение — под anchor с отступом 8–12 px. Панель + переворачивается/сдвигается, чтобы целиком остаться в card viewport, не + закрывать выбранный сегмент и критичные handles. +6. Поле может содержать draft, но стены не меняются до подтверждения. Ошибка + диапазона показывается рядом с полем и блокирует apply. +7. «Ко всем стенам комнаты» вычисляет цели от комнаты выбранного anchor в + момент подтверждения; неоднозначность shared wall решается действующим + владельцем atomic interval и не распространяется на соседнюю комнату + скрыто. +8. Одна галочка создаёт одну атомарную history-команду для всех targets. + +### 7.6. Удалить + +1. Инструмент просит выбрать одну комнату; hover/selected не удаляют данные. +2. После валидного target открывается встроенное состояние подтверждения с + именем комнаты и явным destructive action. +3. Красный цвет используется для финальной destructive-кнопки и invalid/error, + но не как постоянная окраска всей группы. +4. «Назад» возвращает к выбору комнаты; cancel не создаёт history. +5. Подтверждение удаляет только явно выбранную комнату существующей атомарной + командой и затем очищает сессию. + +## 8. Визуальные состояния и обратная связь + +- **active tool:** постоянный cyan/accent state кнопки и текстовая инструкция; +- **hover target:** лёгкий контур без изменения target count; +- **selected target:** стабильный заметный контур/заливка и текстовый счётчик; +- **invalid target:** краткий красный/not-allowed state вместе с текстовой + причиной; цвет не является единственным каналом; +- **applying:** повторное подтверждение блокируется, но отменённый устаревший + completion не может завершить новую сессию; +- status/instruction использует `aria-live="polite"`, validation error — + `aria-live="assertive"` или эквивалентный alert. + +Стили берутся из текущих токенов редактора. Прототип определяет иерархию и +состояния, но не разрешает вводить параллельную палитру. + +## 9. Отмена, keyboard и focus + +1. Повторный клик по активному инструменту отменяет всю незавершённую сессию. +2. Переключение инструмента атомарно очищает targets, anchors, draft, preview, + validation error и локальную панель предыдущего. +3. Esc в `configuring` закрывает настройки и возвращает к `selecting_targets` + с последним валидным набором targets. +4. Esc в `selecting_targets` очищает targets и выключает инструмент. +5. Для многошагового Split/Boundary первый Esc может снять последнюю + незавершённую точку только если это явно показано пользователю; следующий + следует общему правилу отмены сессии. +6. Enter применяет только валидную configuring-фазу и никогда не подтверждает + destructive Delete без фокуса на явной кнопке. +7. После автоматического открытия настроек фокус переходит на их heading или + первое поле; после «Назад» — на active tool/instruction; после apply/cancel + — на кнопку запуска инструмента. +8. Кнопки имеют `aria-pressed`, локализованное имя и понятный disabled reason. + +## 10. Модель данных, persistence и compatibility + +- форматы `spaces`, `rooms`, `walls`, `open_spans`, openings и физических + объектов не меняются; +- `ToolSession`, targets, anchors и drafts не сериализуются; +- миграции, backfill и schema version нет; +- открытие и отмена любого инструмента не вызывает `_saveConfig()`; +- применённые команды используют действующий config/write contract и остаются + читаемыми предыдущей версией; +- import/export не получает новых полей; +- текущие планы открываются без materialisation и побайтовых изменений. + +## 11. Touch, zoom, resilience и performance + +Plan Editor остаётся desktop-first. На coarse pointer полный ergonomic parity +не требуется, но: + +- второй палец, pan, pinch и pointercancel не могут подтвердить цель или apply; +- touch target controls не меньше действующего minimum; +- cancel/switch/space change не оставляет preview или заблокированный stage; +- zoom между выбором targets не меняет их identity и корректно перепривязывает + локальную панель; +- удаление target внешним config update переводит сессию в безопасную отмену с + сообщением, а не применяет операцию к другому объекту. + +Hover/pointermove меняют только дешёвый preview. Полная геометрия, render model +и secondary-panel composition не пересчитываются на каждый pixel движения. +Target validation и локальная раскладка входят в существующие bounded caches; +HA state ticks не перестраивают tool session без структурной причины. + +## 12. i18n + +Все новые пользовательские строки добавляются одновременно в RU и EN: + +- название группы и шести действий, если действующие ключи не подходят; +- инструкции каждой фазы; +- счётчики целей (`0 из 2`, `1 из 2`, `2 из 2`) через plural/parameter contract; +- конкретные validation reasons Merge и остальных инструментов; +- «Назад», «Применить», «Применить ко всем стенам комнаты»; +- accessible labels/status для панели и targets. + +Нельзя собирать предложения конкатенацией или оставлять prototype-only English. +Термины сверяются с `docs/USER-GUIDE.ru.md`. + +## 13. Acceptance criteria + +1. «Редактировать комнату» доступно без preselection и всегда показывает все + шесть действий в заданном порядке. +2. В основной панели нет компоновок «выбрана 1 комната»/«выбраны 2 комнаты» и + ветвления состава от `selectedRooms.length`. +3. Каждое из шести действий следует фазам tool → targets → validation → + configuring → apply и не пишет draft в config/history. +4. Merge показывает `0/2` и `1/2`, не принимает третью/невалидную комнату и + автоматически открывает настройки после второй валидной. +5. Thickness допускает один/несколько atomic segments, открывает рядом панель, + валидирует `1–100 см`, поддерживает all-room и применяет только по галочке. +6. Split, Resize и Boundary сохраняют действующую геометрическую семантику, но + получают единые выбор, отмену, настройки и apply boundary. +7. Delete сначала выбирает одну комнату и показывает отдельное подтверждение; + до финальной кнопки данные не меняются. +8. Active, hover, selected и invalid различимы визуально и доступны без цвета. +9. Esc, повторный tool click, смена tool/mode/space и stale target очищают + незавершённое состояние по разделам 6 и 9. +10. Undo/Redo содержит только подтверждённые операции — ровно одну запись на + один apply независимо от числа targets. +11. Верхняя navigation не меняется, secondary content не сдвигает stage, а + локальная панель целиком остаётся в viewport при zoom и у всех краёв. +12. RU/EN, keyboard, focus, ARIA status/error и reduced-motion проходят + проверки; destructive Enter не срабатывает неявно. +13. Существующие планы и wire format не меняются; cancel не вызывает save. +14. Все шесть состояний раздела 5.2 обновлены и приняты visual review. + +## 14. Проверки и доказательства + +### Unit + +- переходы state machine для каждого инструмента, повторного клика, switch, + Esc, apply, stale async completion и external target removal; +- target cardinality/identity и invalid-target exclusion; +- Merge same floor/shared boundary и автоматический configuring transition; +- Thickness multi-select, `1–100`, all-room и одна history-команда; +- отсутствие save/history для всех cancel paths. + +### Browser smoke + +- полный happy path и cancel/back path всех шести инструментов мышью; +- keyboard-only Merge, Thickness и Delete safety; +- zoom/pan во время выбора и позиционирование локальной панели у четырёх краёв; +- mode/space switch, pointercancel и structural config update; +- верхняя навигация и размеры stage до/после раскрытия групп. + +### Golden + +Обновляется и reviewится комплект из шести состояний раздела 5.2 минимум в +desktop theme light/dark, плюс узкие golden для: + +- Merge `1 из 2`, invalid second target и configuring; +- Thickness hover, multi-selected и local panel у края; +- Delete confirmation. + +Baseline меняется только после проверки, что diff соответствует ТЗ. Golden, +smoke и performance запускаются в release gate перед бетой; цикл реализации — +`typecheck`, `unit`, `build` по процессу. + +## 15. Release-артефакты + +В том же user-visible коммите реализации обязательны: + +- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md`; +- `docs/USER-GUIDE.ru.md` — новый порядок редактирования всех шести операций; +- `docs/UX-MODES.md` — стабильные группы и tool-first lifecycle; +- при необходимости `docs/CANVAS.md`, `docs/RESIZE.md` и + `docs/WALL-THICKNESS.md`, если реализация уточнит их взаимодействие; +- RU/EN locale files; +- принятые golden/baseline и smoke scenario либо ссылка на канонический + артефакт CI по правилам репозитория; +- `docs/TESTING.md`, если добавлен новый сценарий/gate. + +Security и backend release artifacts не требуются, пока реализация не меняет +права или wire format. Если такой change окажется необходим, задача должна +вернуться к владельцу до кода. + +## 16. Риски и rollback + +Основные риски: два конкурирующих источника selection state, применение stale +draft, потеря Undo boundary, недоступный focus и floating panel вне viewport. +Они закрываются единой сессией, revision guard и проверками раздела 14. + +Rollback — возврат UI/runtime-кода и locale/docs без миграции данных. Уже +сохранённые планы остаются совместимыми. + +## 17. Принятые технические предположения + +1. Shared secondary controller остаётся хозяином нижней contextual surface; + новый lifecycle расширяет его, а не создаёт второй overlay stack. +2. Existing domain helpers Merge/Split/Resize/Boundary/Thickness/Delete + сохраняются; задача унифицирует orchestration и transaction boundaries. +3. Для multi-select Thickness стабильная identity — нормализованный atomic wall + interval вместе с room/space context, а не DOM element или экранные + координаты. +4. «Заблокирована другим процессом» на первом этапе означает локальную + конфликтующую сессию/структурно устаревшую цель; новая distributed locking + модель не вводится. +5. Overview prototype state «Удаление» трактуется как подтверждение внутри + «Редактировать комнату», поскольку это одновременно сохраняет заявленную + стабильную иерархию и шесть продуктовых скриншотов. diff --git a/docs/specs/README.md b/docs/specs/README.md index 14e8aaaa..78fcd978 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -86,6 +86,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным | [#123](https://github.com/Matysh/houseplan-card/issues/123) Split из вершины не меняет наружную геометрию стен | [123-corner-split-wall.md](123-corner-split-wall.md) | | [#137](https://github.com/Matysh/houseplan-card/issues/137) Узлы и линии привязки в редакторе Плана | [137-plan-snap-overlay.md](137-plan-snap-overlay.md) | | [#141](https://github.com/Matysh/houseplan-card/issues/141) Бесшовные стыки перегородок и открытых контуров | [141-wall-junctions.md](141-wall-junctions.md) | +| [#148](https://github.com/Matysh/houseplan-card/issues/148) Tool-first редактирование комнаты | [148-tool-first-plan-editor.md](148-tool-first-plan-editor.md) | ## Правило актуализации