# 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 «Удаление» трактуется как подтверждение внутри «Редактировать комнату», поскольку это одновременно сохраняет заявленную стабильную иерархию и шесть продуктовых скриншотов.