From 392ef22c0d3cd6d21ca6f2a3e7bb23d844fad3a8 Mon Sep 17 00:00:00 2001 From: Matysh Date: Sat, 29 Aug 2026 00:00:55 +0300 Subject: [PATCH] docs: specify furniture placement preview Issue: #359 User-Visible: no --- docs/specs/359-furniture-placement-preview.md | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 docs/specs/359-furniture-placement-preview.md diff --git a/docs/specs/359-furniture-placement-preview.md b/docs/specs/359-furniture-placement-preview.md new file mode 100644 index 00000000..f4f38b58 --- /dev/null +++ b/docs/specs/359-furniture-placement-preview.md @@ -0,0 +1,197 @@ +# #359 — Предпросмотр мебели на плане перед размещением + +Issue: [#359](https://github.com/Matysh/houseplan-card/issues/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`. + +## План автотестов + +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. +3. Добавить узкий source-contract test, если lifecycle очистка распределена + между card shell и lazy editor runtime и её нельзя надёжно доказать одним + smoke. +4. Перед `S7`: `npm run typecheck`, `npm test`, `npm run build`, + `npm run bundle:sync`, `npm run bundle:budget`, + `node demo/smoke_furniture.mjs`. + +Новый golden baseline не требуется: smoke проверяет реальный SVG path, +computed style и позиционную геометрию, а принятие нового изображения добавило +бы дорогой платформенный шум к transient editor-only состоянию. + +## Риски + +- **Расхождение 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/screenshots не требуются; +- performance/security/backend artifacts не требуются; +- целевой browser artifact: зелёный `demo/smoke_furniture.mjs`. + +## Принято предположительно, поменять свободно + +- имя и форма transient state; +- конкретный модуль общего resolver и границы между card/runtime; +- способ коалесцирования pointermove; +- набор source-contract assertions сверх обязательного browser smoke.