Files
houseplan-card/docs/specs/148-tool-first-plan-editor.md
T
2026-08-15 10:36:28 +03:00

434 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<string, unknown>;
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 «Удаление» трактуется как подтверждение внутри
«Редактировать комнату», поскольку это одновременно сохраняет заявленную
стабильную иерархию и шесть продуктовых скриншотов.