mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 19:58:50 +00:00
@@ -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.
|
||||
Reference in New Issue
Block a user