docs: specify furniture pack rollout

Issue: #159
User-Visible: no
This commit is contained in:
Matysh
2026-08-28 18:50:48 +03:00
parent 7c285d25b7
commit 8e2d995ef6
2 changed files with 366 additions and 0 deletions
+365
View File
@@ -0,0 +1,365 @@
# #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.
- Автор и владелец репозитория явно подтвердил в запросе на реализацию, что все
иконки нарисованы им собственноручно. В репозиторий попадает нормализованная
копия с `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;
превышение требует упростить представление либо отдельно пересогласовать
продуктовую цену, а не поднять общий бюджет.
## Критерии приёмки
### AC1 — целостность и provenance пакета (`unit`, ревью кода)
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 при откате запрещено.
## Принято предположительно, поменять свободно на ревью
- Generated TS, а не runtime SVG imports, выбран ради CSP/HACS и контроля bundle.
- Четыре menu-only SVG вендорятся для полноты авторского исходника, но скрыты до
появления top-view counterparts.
- Все категории, включая одиночные, используют одинаковый второй уровень;
это осознанно добавляет один клик ради предсказуемого интерфейса.
- Исходные PNG-превью не дублируются в git: issue остаётся immutable source,
а product visuals защищает штатная golden matrix.
+1
View File
@@ -123,6 +123,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
| [#132](https://github.com/Matysh/houseplan-card/issues/132) Проёмы в независимых стенах (+ bug [#185](https://github.com/Matysh/houseplan-card/issues/185)) | [132-partition-openings.md](132-partition-openings.md) |
| [#137](https://github.com/Matysh/houseplan-card/issues/137) Узлы и линии привязки в редакторе Плана | [137-plan-snap-overlay.md](137-plan-snap-overlay.md) |
| [#141](https://github.com/Matysh/houseplan-card/issues/141) Бесшовные стыки перегородок и открытых контуров | [141-wall-junctions.md](141-wall-junctions.md) |
| [#159](https://github.com/Matysh/houseplan-card/issues/159) Новый набор мебели и двухуровневая библиотека | [159-furniture-pack.md](159-furniture-pack.md) |
| [#157](https://github.com/Matysh/houseplan-card/issues/157) Тип проёма «Открытый проём» | [157-open-passage.md](157-open-passage.md) |
| [#150](https://github.com/Matysh/houseplan-card/issues/150) Точная геометрия коллинеарного перепада толщины | [150-wall-thickness-transition.md](150-wall-thickness-transition.md) |
| [#172](https://github.com/Matysh/houseplan-card/issues/172) Нулевой Split-разделитель не получает ложную толщину | [172-zero-divider-taper.md](172-zero-divider-taper.md) |