Files
houseplan-card/docs/specs/159-furniture-pack.md
T
2026-08-28 18:50:48 +03:00

376 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# #159 — Новый набор мебели и двухуровневая библиотека
Issue: [#159](https://github.com/Matysh/houseplan-card/issues/159)
Источник набора: [архив в комментарии](https://github.com/Matysh/houseplan-card/issues/159#issuecomment-5449707137), SHA-256 `9E969016EE3B4B4E3DB776FEC53C8B387B91368B118EB5E39911483DEF1B0953`.
## Сценарий
Администратор дома в desktop-редакторе подложки открывает библиотеку мебели,
сначала выбирает понятную категорию по фронтальной иконке, затем — конкретный
вариант по виду сверху, задаёт его реальный размер и ставит на план. Домочадцы
видят обновлённый линейный символ в обычном View и киоске без дополнительных
действий и без изменения поведения плана.
## Что человек увидит до и после
До изменения библиотека показывает 30 условных видов сверху одним длинным
списком. После изменения она показывает компактный каталог категорий с новыми
фронтальными иконками и второй экран с вариантами вида сверху; на план можно
разместить 56 символов, а уже размещённая мебель с прежними ID автоматически
получает новый рисунок там, где дизайнер передал замену.
## Проблема
Текущая библиотека собрана из простых программных примитивов. В ней мало
вариантов, а часть символов недостаточно похожа на реальный предмет. Плоский
список плохо масштабируется: добавление 26 вариантов сделает его длиннее и не
объяснит пользователю связь «Диваны → двухместный / трёхместный / угловой».
Полученный набор решает визуальную часть, но не может быть вставлен как
произвольный SVG из config: House Plan намеренно не принимает пользовательский
markup. Нужен детерминированный compile-time каталог безопасной path-геометрии,
который сохраняет текущий config-контракт `decor[].symbol`.
## Проверенный вход и права
- Архив содержит 33 SVG категорий меню и 44 SVG вида сверху: 18 `replace`,
26 `add`.
- Технический валидатор архива проходит; все 77 SVG состоят только из
автономных `path` с `fill="none"`, `stroke="currentColor"`, без внешних ссылок,
transforms, CSS, растра, текста, script/filter/mask/clipPath.
- Владелец репозитория [публично подтвердил](https://github.com/Matysh/houseplan-card/issues/159#issuecomment-5454085168),
что является автором всех 77 SVG и разрешает House Plan использовать,
изменять и распространять их под MIT License репозитория без обязательной
отдельной атрибуции в UI. В vendored-копию попадают `author: Matysh`,
`license: MIT`; исходный `TBD` не переносится как релизная метаинформация.
- Превью из архива — материал визуального ревью, не runtime-ресурс и не источник
геометрии.
## Scope
1. Вендоринг нормализованного исходного набора в
`assets/furniture/houseplan-0.3.0/`: manifest, README/provenance, MIT license
и 77 SVG. PNG-превью остаются в issue и не дублируются в git.
2. Детерминированный генератор, который валидирует разрешённый SVG-поднабор и
создаёт раздельные TS-каталоги:
- plan-art, нужный View и редактору;
- menu-art, импортируемый только ленивым editor graph.
3. Итоговый каталог из 56 плановых символов: все 30 прежних ID плюс 26 новых.
4. Замена рисунка и default-размера для 18 прежних ID из manifest без миграции
сохранённой мебели.
5. Двухуровневая палитра «категории → варианты» с фронтальными и top-view SVG.
6. Полные EN/RU/DE названия категорий и вариантов.
7. Обновление свойств мебели, тестов, golden/smoke, документации и changelog.
## Не входит
- загрузка пользовательских SVG/паков и выбор пакета;
- удаление либо переименование любого существующего symbol ID;
- 3D/изометрическая мебель, заливки, цвета, тени и материалы;
- новые жесты, wall magnet, resize/rotate, z-order или collision detection;
- новые плановые предметы для четырёх menu-only исходников `computer`, `oven`,
`hood`, `exercise`;
- изменение backend-схемы decor и миграция config;
- обещание удобного редактирования на touch: редактор остаётся desktop-first.
## Каталог и совместимость
### Замены существующих ID
`coffee_table`, `table_dining`, `table_round`, `desk`, `chair`, `armchair`,
`sofa`, `bed_single`, `bed_double`, `nightstand`, `bookshelf`, `wardrobe`,
`stove`, `tv`, `toilet`, `bathtub`, `bidet`, `kitchen_sink`.
Для нового размещения применяются размеры из manifest. Для уже сохранённой
мебели `x/y/w/h/angle` остаются байт-в-байт прежними; меняется только рисунок,
разрешаемый по тому же `symbol`.
### Новые ID
`coffee_table_round`, `coffee_table_oval`, `coffee_table_rounded`,
`table_dining_oval`, `table_dining_rounded`, `desk_corner`, `chair_bar`,
`armchair_office`, `sofa_three_seat`, `sofa_corner_right`, `cabinet_tv`,
`cabinet_shoe`, `cabinet_sink`, `wall_unit`, `kitchen_floor`,
`kitchen_floor_corner`, `kitchen_wall`, `kitchen_wall_corner`, `shelf_floor`,
`shelf_wall`, `cooktop_two`, `tv_wall`, `toilet_built_in`, `bathtub_corner`,
`bidet_built_in`, `kitchen_sink_double`.
### Неперерисованные прежние ID
`fridge`, `dishwasher`, `washer`, `dryer`, `ac`, `water_heater`, `shower`,
`sink`, `stairs`, `fireplace`, `plant`, `rug` сохраняют текущую геометрию,
размеры и поведение. Они получают фронтальные category icons из нового набора.
### Категории
Каждый плановый символ имеет ровно одну категорию. Для неперерисованных ID:
| Symbol | Category |
|---|---|
| `ac` | `air_conditioner` |
| `water_heater` | `boiler` |
| `fridge` | `fridge` |
| `dishwasher` | `dishwasher` |
| `washer` | `washer` |
| `dryer` | `dryer` |
| `shower` | `shower` |
| `sink` | `sink` |
| `stairs` | `stairs` |
| `fireplace` | `fireplace` |
| `plant` | `plant` |
| `rug` | `rug` |
Остальные связи берутся из `symbols[].menu_icon` manifest. Категория без
единого top-view symbol не отображается. Поэтому `computer`, `oven`, `hood`,
`exercise` остаются в vendored source для будущего дополнения, но не создают
пустых или неработающих кнопок.
## Контракт UX
### Первый уровень: категории
- Открытие «Мебели» показывает четыре прежние группы в прежнем порядке:
мебель, техника, сантехника, прочее.
- Внутри группы показывается одна плитка на непустую категорию: новая
фронтальная иконка и локализованное имя.
- Плитка не начинает размещение и не меняет config. Она открывает второй
уровень внутри той же context tray/palette.
- Закрытие палитры сбрасывает навигацию на первый уровень и возвращает инструмент
в Select по существующему контракту.
### Второй уровень: варианты
- Заголовок показывает кнопку «Назад», фронтальную иконку и имя категории.
- Все варианты категории показываются плитками с реальным видом сверху и
локализованным названием. Даже категория с одним вариантом проходит через
этот экран: одинаковый результат клика важнее экономии одного клика.
- Клик по варианту вооружает прежний stamp-flow, подсвечивает вариант и
показывает прежние поля ширины/глубины и подсказку размещения.
- Выбор другого варианта сбрасывает width/depth на defaults нового варианта,
как нынешний выбор другого symbol. Ручные размеры не переносятся между
разными вариантами.
- «Назад» возвращает к категориям и снимает вооружённый symbol, чтобы нажатие
по плану после смены экрана не поставило скрыто выбранный предмет.
- После успешной одиночной установки инструмент возвращается в Select, как
сейчас. Многократный stamp не добавляется.
### Свойства уже размещённого предмета
- Поле «Символ» остаётся одним select без дополнительного шага, но варианты
группируются сначала по четырём product-группам, затем по category label.
- Текущий ID всегда выбран, в том числе для старого или нового символа.
- Смена symbol сохраняет текущую коробку, положение и поворот по действующему
контракту; default-размер используется только при новом размещении.
### Доступность и touch
- Категории/варианты — настоящие `button` с `title`/accessible name; «Назад»
доступен с клавиатуры и имеет локализованную подпись.
- Палитра сохраняет собственный вертикальный/горизонтальный scroll и не
увеличивает рабочую область редактора.
- Touch editor: best effort. Плитки не могут быть меньше действующих touch
targets, а нажатие внутри tray не проходит на план. View/kiosk рендерят
мебель полностью и являются release-blocking.
## Рендер и безопасность
1. Runtime не читает SVG-файлы и не принимает SVG/path из config. Генератор
извлекает только локальные `path[d]` из проверенного vendored набора.
2. Разрешённый source-контракт: корректный XML; точный viewBox; только `svg/g/path`;
`fill=none`, `stroke=currentColor`; без URL, style/class, event attributes,
transform, script, text, foreignObject, image, animation и SVG effects.
Нарушение прерывает генерацию/тест, а не попадает в bundle.
3. Все path одного символа объединяются в одну неизменяемую `d`-строку. Один
размещённый предмет остаётся одним интерактивным `<path>`, включая erase-hit.
4. Source viewBox хранится рядом с `d`. Рендер применяет translate/rotate и
non-uniform scale из source box в сохранённые `w/h`; `vector-effect` не даёт
scale менять пользовательскую физическую толщину контура.
5. Plan path получает текущие `color`, `opacity`, `width_cm`, round cap/join и
все существующие `data-hp/data-id/data-kind/data-symbol` hooks.
6. Menu/variant preview рендерит тот же immutable path в своём viewBox с
`currentColor`, прозрачным фоном и фиксированным non-scaling stroke. Variant
preview сохраняет реальные пропорции в 40×40, как текущая палитра.
7. Неизвестный `decor[].symbol` по-прежнему валиден для backend, не падает и
ничего не рисует в старой карточке.
## Модель данных и миграция
Persisted schema не меняется:
```ts
{ kind: 'furniture', symbol: string, x, y, w, h, angle?, ...style }
```
- Новых config-полей, schema/model version и migration нет.
- `operation`, category ID, source viewBox и SVG path — compile-time metadata,
не пользовательские данные.
- Сохранение старого плана не переписывает symbol или геометрию мебели.
- Backend продолжает валидировать только безопасный формат ID, а не закрытый
список символов — forward compatibility сохраняется.
## Генерация и source of truth
- `assets/furniture/houseplan-0.3.0/manifest.json` — список дизайнерских
замен/добавлений и menu mapping.
- README внутри каталога фиксирует SHA-256 исходного архива, URL issue/Figma,
авторство и отличия нормализованной копии (`author/license/pack_id`).
- `scripts/generate-furniture-assets.mjs` валидирует пакет и атомарно генерирует
два стабильных файла без timestamp/абсолютных путей:
`src/furniture-plan-art.generated.ts` и
`src/furniture-menu-art.generated.ts`.
- `--check` пересобирает в памяти и падает при stale generated output. Он входит
в unit/CI-проверку задачи.
- Plan-art импортируется `src/furniture.ts`, потому что нужен обычному View.
Menu-art импортируется только `src/houseplan-editor-runtime.ts`, чтобы 33
фронтальные иконки не попали в initial View graph.
## i18n
Меняются `src/i18n/en.json`, `ru.json`, `de.json`:
- `furn.category_<id>` для каждой отображаемой категории;
- `furn.sym_<id>` для всех 26 новых ID;
- существующие 18 `furn.sym_*` получают уточнённые названия manifest;
- `furn.back_to_categories` и accessible label для возврата, если общий ключ
Back не подходит грамматически.
RU/EN берутся из manifest и редакционно нормализуются под текущий UI. DE
переводится в том же изменении; fallback на английский для нового каталога не
допускается.
## Производительность и bundle
- На один предмет остаётся один plan `<path>`; DOM-сложность View не растёт от
числа исходных subpaths.
- Категории/варианты создаются только при открытом editor palette.
- `npm run bundle:budget` обязан пройти общий initial View ceiling.
- В handoff записываются gzip-дельты initial View и lazy editor относительно
`origin/dev`. Front-menu art в initial View graph — блокирующая ошибка.
- Дополнительный initial View gzip для 44 plan SVG — не более 18 KiB. Это
блокирующий критерий задачи: при превышении реализация упрощает представление
и повторяет замер. Поднять общий budget или принять превышение в #159 нельзя;
иное решение требует отдельного изменения ТЗ и нового ревью до реализации.
## Критерии приёмки
### AC1 — целостность и provenance пакета (`unit`, ревью кода)
В issue есть публичное подтверждение владельца об авторстве всех 77 SVG и
разрешении использовать, изменять и распространять их под MIT License
репозитория без отдельной UI-атрибуции. Vendored source соответствует
зафиксированному архиву по всем 77 SVG; нормализованы только подтверждённые
метаданные author/license/pack ID и документация. Генератор отклоняет
запрещённый SVG и `--check` подтверждает актуальность двух generated каталогов.
### AC2 — полная совместимость ID (`unit`)
Итоговый каталог содержит ровно 56 уникальных ID: прежние 30 без удаления или
переименования и новые 26. 18 replacement-ID используют новый art/defaults,
12 retained-ID — прежний art/defaults. Unknown ID остаётся безопасным no-op.
### AC3 — категории и варианты (`unit`, `smoke`)
В первом уровне есть только непустые категории с фронтальными иконками; четыре
menu-only категории скрыты. Переход во второй уровень, Back, выбор каждого
варианта, selected state, reset defaults и однократный stamp выполняют UX-контракт.
### AC4 — плановый рендер (`unit`, `smoke`, `golden`)
Новый и replacement symbol одинаково рисуются в Background editor, View,
light/dark theme и при непропорциональном resize; stroke остаётся одинаковой
толщины, цвет/opacity/rotation/hooks/erase-hit не регрессируют. Сохранённый
replacement-предмет сохраняет свою коробку и получает новый рисунок.
### AC5 — свойства и round-trip (`unit`, `smoke`)
Properties select показывает/выбирает все 56 ID; смена symbol сохраняет
геометрию, save/reload не меняет ID, а backend принимает новые IDs без обновления
allow-list.
### AC6 — локализации (`unit`, smoke)
Все видимые category/symbol labels существуют и непусты в EN/RU/DE; ни одна
плитка не показывает ключ или английский fallback в RU/DE.
### AC7 — editor/touch safety (`smoke`, ревью кода)
Pointer внутри обоих уровней палитры не ставит мебель на план и не начинает
pan; закрытие/Back не оставляют скрытый armed stamp. View и kiosk не получают
новых handlers и рендерят те же symbol paths.
### AC8 — bundle и DOM budget (`build`, unit)
Budget проходит; initial View gzip delta ≤18 KiB, menu-art отсутствует в initial
graph; один мебельный объект создаёт один основной plan path и один erase-hit
только в активном erase-mode.
### AC9 — документация и релиз (`review`)
Обновлены `docs/FURNITURE.md`, релевантный user guide/`docs/STATUS.md`,
`docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`; provenance и desktop-first правило
зафиксированы. Golden candidates приняты только из Linux CI по штатному процессу.
## План автотестов
1. Расширить `test/furniture.test.mjs`: множества ID, category mapping,
replacements/retained, source viewBox/path, finite transform, unknown ID,
default sizes и one-path contract.
2. Новый тест generator/integrity: manifest↔SVG↔generated parity, запрет каждого
опасного SVG-класса, orphan menu policy, deterministic output.
3. Расширить `demo/smoke_furniture.mjs`: category → variants → select → size →
place, Back/close safety, properties change, reload, старый/new symbol.
4. Golden matrix: category screen light, category screen dark, variant screen с
несколькими формами и plan scene с replacement + retained + new furniture.
5. i18n parity tests для всех новых ключей EN/RU/DE.
6. Build manifest comparison с `origin/dev` и `npm run bundle:budget`.
## Release-артефакты
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md`: значимое пользовательское
изменение со ссылкой на #159;
- `docs/FURNITURE.md`: каталог, двухуровневый выбор, размеры/совместимость,
provenance;
- user guide/STATUS — только разделы, где перечисляется библиотека мебели;
- обновлённые Linux golden baselines и запись визуального review;
- новые docs screenshots — только если изменяемый сценарий входит в их matrix;
- bundle-size delta initial/lazy в handoff и отчёте code review;
- security artifact отдельно не нужен: безопасность доказывает allow-list
generator и отсутствие runtime SVG input.
## Риски и митигации
| Риск | Митигация |
|---|---|
| Новый рисунок неожиданно меняет старый план | replacement только по явно переданным ID; геометрия config не меняется; golden before/after |
| Menu art раздует View bundle | отдельный lazy import и graph assertion |
| SVG path меняет толщину при resize | `vector-effect: non-scaling-stroke`, экстремальные aspect-ratio tests |
| Пустая category создаёт dead end | category выводится только при ≥1 plan symbol; invariant test |
| Новый symbol ломает backend | backend сохраняет regex-only forward-compatible validation; round-trip smoke |
| Длинные переводы ломают tray | RU/EN/DE golden и внутренний scroll без изменения stage |
| Небезопасный SVG попадёт в runtime | strict compile-time validator; в config нет markup/path |
## Откат
Кодовый откат возвращает прежний каталог и палитру. Persisted schema не
изменялась: старые 30 ID снова рисуются прежними символами. Новые 26 ID в старой
версии остаются валидными данными backend и безопасно не рисуются; после
возврата новой версии появляются снова с сохранённой геометрией. Удалять или
переписывать такие records при откате запрещено.
## Продуктово-правовое решение
Авторство и MIT-разрешение подтверждены владельцем в
[issue comment](https://github.com/Matysh/houseplan-card/issues/159#issuecomment-5454085168).
Открытых продуктовых или правовых вопросов нет.
## Принято предположительно, поменять свободно на ревью
- Generated TS, а не runtime SVG imports, выбран ради CSP/HACS и контроля bundle.
- Четыре menu-only SVG вендорятся для полноты авторского исходника, но скрыты до
появления top-view counterparts.
- Все категории, включая одиночные, используют одинаковый второй уровень;
это осознанно добавляет один клик ради предсказуемого интерфейса.
- Исходные PNG-превью не дублируются в git: issue остаётся immutable source,
а product visuals защищает штатная golden matrix.