Волна 5 эпика #674, перенос ТЗ (класс C). Из 240 файлов `docs/specs/` в `legacy/specs/` уехали 219: на них не ссылается ни один живой файл (код, тесты, скрипты, workflow, документы вне архива и ревью). Остались 21 ТЗ — на которые ссылаются код, ADR, ISOMETRIC, SUN, RADAR, LIGHT (`docs/specs/067`), DECOR-EDITOR, support-relay, и те, на которые ссылаются они сами; README каталога объясняет, где искать остальное. Открытых issue с файлом ТЗ среди перенесённых нет. Относительные ссылки перенесённых файлов переписаны (`../X` → `../../docs/X`, соседние оставшиеся ТЗ → `../../docs/specs/…`) — все 26 резолвятся. Попутно: битая ссылка в `089-isometric-view-stage1.md:8` на удалённый `089-isometric-view.md` — теперь команда `git show` по истории. Строка в `legacy/README.md`. Issue: #682 User-Visible: no Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
17 KiB
#359 — Предпросмотр мебели на плане перед размещением
Issue: #359
Сценарий
Персона администратор дома (J4/J6 из docs/SCOPE.md) работает в
Редакторе подложки на desktop с мышью: открывает библиотеку мебели,
выбирает предмет, при необходимости меняет его ширину и глубину и ведёт
указатель к месту установки на плане.
Touch editor: best effort / intentionally degraded. Безопасное размещение одним тапом сохраняется, но hover-предпросмотр без hover-capable pointer не обещается; View и kiosk не меняются.
Что человек увидит до и после
До изменения предмет появляется только после клика; после изменения до клика виден полупрозрачный предмет ровно в той позиции, размере и ориентации, в которых он будет размещён.
Проблема
Текущее одноразовое размещение мебели скрывает от пользователя три значимых результата до записи: реальный габарит предмета, итог сеточной привязки и срабатывание магнита к стене вместе с автоматическим поворотом. Ошибка видна только после сохранения и требует отдельного перемещения либо удаления.
Скоуп
- transient preview одного выбранного предмета мебели на SVG-плане;
- полное совпадение preview и commit по координатам, размеру, углу, символу и текущему стилю контура;
- немедленное обновление при движении мыши, изменении Width/Depth и изменении
состояния
Shift; - единый resolver геометрии для preview и фактического размещения;
- очистка preview на всех границах жизненного цикла инструмента;
- целевые unit/source checks и браузерный smoke существующего furniture flow;
- пользовательская документация и RU/EN changelog.
Не-скоуп
- multi-stamp и изменение текущего возврата в Выбрать после одного клика;
- размерные плашки во время первоначального размещения;
- collision detection, запрет размещения вне комнат или автоматическое исправление пересечений;
- изменение сохранённой схемы
space.decor, миграция или новые настройки; - изменение перемещения, resize и rotate уже сохранённой мебели;
- гарантированный hover-preview на touch/coarse-pointer устройствах;
- изменение рисунков и состава библиотеки мебели.
Контракт поведения
- Preview существует только когда одновременно выполнены условия:
mode === decor, активен инструментfurniture, выбран валидный символ и последний fine/hover-capable mouse pointer находится над планом. - Preview использует текущий выбранный символ, введённые Width/Depth и
текущий стиль декоративного контура. Он рисуется как тот же top-view SVG,
но с общей визуальной непрозрачностью
0.55,aria-hidden="true"иpointer-events="none". - Положение сначала проходит существующую сеточную/decor/room привязку.
Затем, если
Shiftне зажат, применяется существующий магнит мебели к стене и его автоматический поворот. ПриShiftмагнит пропускается, а оставшаяся привязка продолжает работать — ровно как в текущем commit. - Габарит целиком ограничивается теми же текущими canvas bounds, что и при размещении. Предмет у края показывает уже скорректированную позицию, а не позицию, которая затем «прыгнет» после клика.
- Один и тот же чистый resolver строит итоговую геометрию preview и запись
DecorShape. Наpointerdownгеометрия разрешается для координаты самого события и сразу передаётся commit; сохранённый объект не вычисляет привязку вторым независимым путём. - Preview не меняет
space.decor,cfgEpoch, очередь записи, selection и историю Undo/Redo. Движение указателя не вызываетconfig/set. - После успешного клика сохраняется ровно один предмет, он выделяется, а инструмент, категория и палитра сбрасываются как сейчас; preview исчезает в том же кадре.
- Preview очищается при
pointerleaveиpointercancel, Escape, закрытии палитры, выборе другого инструмента, смене editor/space, потере выбранного символа и teardown/remount. Stale state дополнительно не рендерится, если условия пункта 1 больше не выполняются. - Изменение Width/Depth при указателе над планом пересчитывает preview без необходимости снова двигать мышь. Невалидное/неизвестное изображение не создаёт preview и не ломает редактор.
- Touch/pen не создают hover-preview и очищают возможный mouse-preview.
Один тап сохраняет не более одного предмета; второй pointer, pinch и
pointercancelне должны создавать запись.
UX
- Ghost находится в decor composition layer поверх сохранённого decor и не участвует в hit testing.
- Используется реальный top-view рисунок и реальный будущий размер, а не bounding-box placeholder.
- Сниженная непрозрачность однозначно отделяет ещё не сохранённый объект от сохранённых. Дополнительная рамка, анимация и текст не добавляются.
- Cursor и одноразовая модель инструмента не меняются.
- При отсутствии выбранного варианта палитра ведёт себя как сейчас и ничего на плане не показывает.
Модель данных и миграция
Сохранённая модель не меняется. Preview — экземпляр локального runtime state,
не сериализуется и не восстанавливается после remount. Фактический предмет
продолжает записываться как существующий DecorShape вида
{ kind: "furniture", symbol, x, y, w, h, angle?, ...style }.
Миграция не нужна. Старые и новые конфиги читаются и записываются одинаково.
i18n
Нового текста интерфейса нет. Существующие названия инструмента, палитры и подсказка размещения не меняются; новые ключи локализации не нужны.
Критерии приёмки
- AC1 — появление: после выбора валидного предмета mouse move над планом
показывает один
.furniture-placement-previewс правильнымdata-symbol. Доказательство:demo/smoke_furniture.mjs. - AC2 — геометрический паритет: при одинаковом input resolver возвращает
идентичные
x/y/w/h/angle/symbolдля preview и commit; отдельно покрыты свободное место, магнит к горизонтальной/вертикальной стене,Shiftи край canvas. Доказательство:test/furniture.test.mjsплюс smoke. - AC3 — живые размеры: изменение Width/Depth при неподвижном указателе меняет габарит preview, а последующий клик сохраняет этот габарит. Доказательство: smoke.
- AC4 — чистота preview: pointer move не меняет decor/history и не вызывает сохранение; click добавляет ровно одну запись. Доказательство: smoke с счётчиками конфигурации/истории.
- AC5 — очистка: mouse leave, pointercancel, Escape, закрытие палитры и смена инструмента убирают preview; после commit он также отсутствует. Доказательство: smoke и source-contract test для lifecycle guards.
- AC6 — ввод:
Shiftнемедленно меняет preview на свободное размещение, mouse возвращает его обратно; touch/pen не оставляют ghost и cancel/pinch не создают мебель. Доказательство: smoke. - AC7 — визуальный контракт: preview рисует настоящий path выбранного
символа, имеет opacity
0.55,aria-hiddenиpointer-events="none". Доказательство: smoke DOM/computed-style assertions. - AC8 — совместимость: существующий выбор, commit, selection, wall magnet,
resize/rotate, erase и View-rendering мебели остаются зелёными.
Доказательство:
test/furniture.test.mjs,demo/smoke_furniture.mjs, общиеtypecheck/test/build. - AC9 — неизвестный символ: неизвестный/невалидный
symbolне создаёт.furniture-placement-preview, не добавляет предмет вspace.decorи не вызывает исключение; после выбора валидного символа инструмент продолжает работать. Доказательство: unit-тест resolver на неизвестный id иdemo/smoke_furniture.mjsс принудительным invalid palette state. - AC10 — композиция: реальный рисунок ghost виден поверх уже сохранённого
decor и остаётся ниже физических стен в согласованном decor composition
layer. Доказательство: один детерминированный golden-сценарий
furniture-placement-preview-light, который программно вооружает предмет и фиксирует pointer position рядом с существующей мебелью и стеной.
План автотестов
- В
src/furniture.tsвыделить/расширить чистый resolver placement и покрыть его unit-тестами для обычной точки, стены,Shift-пути и clamp у границы. - Расширить
demo/smoke_furniture.mjs: mouse preview, размеры без движения, Shift parity, отсутствие мутаций, commit parity, lifecycle clear и touch/pointercancel safety, а также fail-dark неизвестного symbol с последующим восстановлением валидного выбора. - Добавить узкий source-contract test, если lifecycle очистка распределена между card shell и lazy editor runtime и её нельзя надёжно доказать одним smoke.
- Добавить в
demo/golden/matrix.mjsодин светлый детерминированный сценарийfurniture-placement-preview-light: состояние preview задаётся программно, без реального hover timing; рядом присутствуют сохранённый decor и стена, чтобы растровое сравнение защищало z-order и композитинг. Новый baseline не принимается локально или «ради зелёного CI»: после реализации используется полный Linux artifact иnpm run golden:accept -- --reviewedпо правиламdemo/golden/README.md. - Перед
S7:npm run typecheck,npm test,npm run build,npm run bundle:sync,npm run bundle:budget,node demo/smoke_furniture.mjs.
Риски
- Расхождение preview/commit. Снижается единым resolver и сравнением результата до/после клика.
- Лишние записи на pointermove. Preview state отделяется от config и проверяется счётчиком сохранений/history.
- Stale ghost после смены состояния. Все выходы очищают state, а renderer имеет независимый mode/tool/palette guard.
- Стоимость render на каждый pointermove. Меняется один SVG path; никакой записи, пересчёта всей модели или DOM-измерений. При необходимости обновление коалесцируется существующим Lit render cycle.
- Touch misclick. Preview ограничен real mouse/fine hover; cancel/second pointer не коммитят объект.
- Неизвестный symbol. Fail dark: нет path/preview/commit, редактор жив.
Откат
Удалить transient preview state, его pointer lifecycle и render path, сохранив общий resolver placement и прежний вызов commit. Схема данных не меняется, поэтому откат не требует миграции и не влияет на уже сохранённую мебель.
Release-артефакты
- обновить
docs/FURNITURE.mdи раздел мебели вdocs/USER-GUIDE.ru.md; - добавить короткие значимые записи со ссылкой на #359 в
docs/CHANGELOG.mdиdocs/CHANGELOG.ru.md; - добавить один детерминированный golden-сценарий
furniture-placement-preview-light; baseline принимается только по полному Linux CI artifact черезnpm run golden:accept -- --reviewedи может быть завершён послеS8в pre-release gate, как требует политика эталонов; - performance/security/backend artifacts не требуются;
- целевой browser artifact: зелёный
demo/smoke_furniture.mjs.
Принято предположительно, поменять свободно
- имя и форма transient state;
- конкретный модуль общего resolver и границы между card/runtime;
- способ коалесцирования pointermove;
- набор source-contract assertions сверх обязательного browser smoke.