Files
houseplan-card/legacy/specs/359-furniture-placement-preview.md
Claudeandclaude[bot] df46fd1c3e docs(hygiene): ТЗ выпущенных задач без живых ссылок — в legacy/specs (#682)
Волна 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
2026-09-27 22:10:46 +00:00

17 KiB
Raw Permalink Blame History

#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 устройствах;
  • изменение рисунков и состава библиотеки мебели.

Контракт поведения

  1. Preview существует только когда одновременно выполнены условия: mode === decor, активен инструмент furniture, выбран валидный символ и последний fine/hover-capable mouse pointer находится над планом.
  2. Preview использует текущий выбранный символ, введённые Width/Depth и текущий стиль декоративного контура. Он рисуется как тот же top-view SVG, но с общей визуальной непрозрачностью 0.55, aria-hidden="true" и pointer-events="none".
  3. Положение сначала проходит существующую сеточную/decor/room привязку. Затем, если Shift не зажат, применяется существующий магнит мебели к стене и его автоматический поворот. При Shift магнит пропускается, а оставшаяся привязка продолжает работать — ровно как в текущем commit.
  4. Габарит целиком ограничивается теми же текущими canvas bounds, что и при размещении. Предмет у края показывает уже скорректированную позицию, а не позицию, которая затем «прыгнет» после клика.
  5. Один и тот же чистый resolver строит итоговую геометрию preview и запись DecorShape. На pointerdown геометрия разрешается для координаты самого события и сразу передаётся commit; сохранённый объект не вычисляет привязку вторым независимым путём.
  6. Preview не меняет space.decor, cfgEpoch, очередь записи, selection и историю Undo/Redo. Движение указателя не вызывает config/set.
  7. После успешного клика сохраняется ровно один предмет, он выделяется, а инструмент, категория и палитра сбрасываются как сейчас; preview исчезает в том же кадре.
  8. Preview очищается при pointerleave и pointercancel, Escape, закрытии палитры, выборе другого инструмента, смене editor/space, потере выбранного символа и teardown/remount. Stale state дополнительно не рендерится, если условия пункта 1 больше не выполняются.
  9. Изменение Width/Depth при указателе над планом пересчитывает preview без необходимости снова двигать мышь. Невалидное/неизвестное изображение не создаёт preview и не ломает редактор.
  10. 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 рядом с существующей мебелью и стеной.

План автотестов

  1. В src/furniture.ts выделить/расширить чистый resolver placement и покрыть его unit-тестами для обычной точки, стены, Shift-пути и clamp у границы.
  2. Расширить demo/smoke_furniture.mjs: mouse preview, размеры без движения, Shift parity, отсутствие мутаций, commit parity, lifecycle clear и touch/pointercancel safety, а также fail-dark неизвестного symbol с последующим восстановлением валидного выбора.
  3. Добавить узкий source-contract test, если lifecycle очистка распределена между card shell и lazy editor runtime и её нельзя надёжно доказать одним smoke.
  4. Добавить в 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.
  5. Перед 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.