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

30 KiB
Raw Blame History

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-истины со следующим смыслом:

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