From 8e2d995ef6c3d2215db2a69d6e7ecd50986bf8dd Mon Sep 17 00:00:00 2001 From: Matysh Date: Fri, 28 Aug 2026 17:44:22 +0300 Subject: [PATCH] docs: specify furniture pack rollout Issue: #159 User-Visible: no --- docs/specs/159-furniture-pack.md | 365 +++++++++++++++++++++++++++++++ docs/specs/README.md | 1 + 2 files changed, 366 insertions(+) create mode 100644 docs/specs/159-furniture-pack.md diff --git a/docs/specs/159-furniture-pack.md b/docs/specs/159-furniture-pack.md new file mode 100644 index 00000000..8268e9ea --- /dev/null +++ b/docs/specs/159-furniture-pack.md @@ -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`-строку. Один + размещённый предмет остаётся одним интерактивным ``, включая 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; + превышение требует упростить представление либо отдельно пересогласовать + продуктовую цену, а не поднять общий бюджет. + +## Критерии приёмки + +### 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. diff --git a/docs/specs/README.md b/docs/specs/README.md index 2a4ee2ec..cded72b3 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -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) |