# #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`-строку. Один размещённый предмет остаётся одним интерактивным ``, включая 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_` для каждой отображаемой категории; - `furn.sym_` для всех 26 новых ID; - существующие 18 `furn.sym_*` получают уточнённые названия manifest; - `furn.back_to_categories` и accessible label для возврата, если общий ключ Back не подходит грамматически. RU/EN берутся из manifest и редакционно нормализуются под текущий UI. DE переводится в том же изменении; fallback на английский для нового каталога не допускается. ## Производительность и bundle - На один предмет остаётся один plan ``; 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.