Files
houseplan-card/docs/FURNITURE.md
T
Claude 7df2740f1c feat(furniture): пакет 0.4.0 — 60 дизайнерских символов вместо 44 плюс 12 примитивов
Встроенная библиотека мебели была смешанной: 44 предмета рисовал дизайнер, а
12 — код, прямоугольниками и эллипсами в unit box. Они выбивались из стиля, и
три категории меню (`computer`, `hood`, `oven`) вообще не открывались, потому
что вида сверху для них не было.

Пакет 0.4.0 перерисовывает все 56 существующих публичных ID и добавляет четыре
новых: `computer`, `hood`, `oven`, `cactus`. Каталог становится равен поставке:
60 символов, 32 непустые категории.

Совместимость. Ни один сохранённый план не мигрирует: положение, размер,
поворот, отражение, цвет, прозрачность и толщина линии не трогаются, а
default-размеры всех 56 прежних ID совпадают со старыми до числа — таблица
зашита в `test/furniture.test.mjs`, чтобы сдвиг было видно тестом, а не глазами.

Ловушка, ради которой примитивы удалены целиком. Библиотека собиралась союзом
каталога и `LEGACY_FURNITURE`, и обе половины несли ОДНИ И ТЕ ЖЕ публичные ID.
`BY_ID` — это `Map`, она оставляет последнюю запись, поэтому примитив молча
побеждал дизайнерский рисунок, который его заменял: задача выглядит выполненной,
12 предметов не обновились, ни один существующий тест этого не видит. Поэтому
`LEGACY_FURNITURE`, `RETAINED_IDS`, тип `Prim`, `box()` и `primitivePathD`
удалены, а на дубликаты в каталоге стоит тест и мутант.

Два продуктовых решения владельца (#593):

- `cactus` живёт в категории `plant`, а не `exercise`, как предлагал пакет:
  плитка «Тренажёр» рисует тренажёр, и открывать её на кактус — врать о
  содержимом палитры. `exercise` остаётся скрытой пустой категорией, непустых
  становится 32, а не 33;
- при отказе ленивого чанка с рисунками не рисуется НИЧЕГО. Прежде 12
  примитивов рисовались всегда, и `docs/FURNITURE.md` это обещал; обещание
  снято. Это единственное место, где задача ухудшает наблюдаемое поведение, и
  оно проверяется смоком, а не словом.

Бюджеты. Ревью ТЗ показало, что «бюджет ленивого графа защищён» было
заявлением без гейта: размеры печатались в отчёт и ни с чем не сравнивались.
Добавлены `lazyGraphCeilingViolation` и два настоящих потолка со своим мутантом.
Стартовый граф ушёл ниже полосы, поэтому `INITIAL_VIEW_GZIP_CEILING` опущен
292 400 → 291 400 — вторая половина храповика, без которой выигрыш отыграется.

Провенанс. Документ пакета воссоздан под 0.4.0: грант владельца, имя архива и
его SHA-256. Это единственная исполнимая защита от повторения истории #159, и
она не ослаблена — число проверок в тесте не уменьшилось, а мутант подменяет
хеш и обязан её покраснить.

Свидетели: пять новых мутантов, новая golden-сцена `furniture-new-symbols-light`
для четырёх новых ID, матрица 63 → 64. Шестой мутант правится, а не добавляется:
`furniture-placement-needs-art` ссылался на удалённое поле `g` и переставал
компилироваться — раннер честно отличал это от «пойман» и отчитывался
setup-failure. Ложь мутанта осталась прежней по смыслу, убрана только ветка про
примитив.

i18n: 7 ключей × 4 локали. `furn.cat_exercise` не добавлен — категория скрыта.

Issue: #593
User-Visible: yes
2026-09-19 13:38:49 +03:00

8.1 KiB

Furniture library

Status: implemented and expanded by issues #159 and #383. The Background editor stores furniture as ordinary decor:

{ kind: "furniture", symbol, x, y, w, h, color, opacity, width_cm, angle?, flip_h?, flip_v? }

w and h are always positive physical extents. Optional boolean flip_h and flip_v mirror the drawing inside that unchanged box; absent flags are the original orientation. Existing furniture therefore keeps its position, size, rotation, orientation and styling after an update.

Library and source

The public library contains 60 top-view symbols, all of them designer artwork from assets/furniture/houseplan-0.4.0/svg/plan (#593). The previous split — 44 designer symbols plus 12 built-ins drawn from unit-box primitives — is gone: pack 0.4.0 redraws those 12 ids (refrigerator, dishwasher, washer, dryer, air conditioner, water heater, shower, sink, stairs, fireplace, plant and rug) and adds four new ones: computer, hood, oven and cactus.

The designer pack also contains 33 front-view category illustrations in svg/menu. One of them, exercise, has no top-view symbol and is intentionally hidden instead of opening an empty category, so the palette shows 32 categories. The cactus lives under plant, not under exercise: the exercise tile draws an exercise machine, and opening it onto a cactus would misdescribe the palette (owner's decision in #593).

pack.json is the source of truth for stable ids, categories, names, default centimetre dimensions and SVG paths. Run:

npm run furniture:generate
npm run furniture:check

The generator validates a deliberately small, inert SVG subset and produces:

  • src/furniture-plan-catalog.generated.ts — ids, groups, categories and default sizes; the only part of the pack in the initial View graph;
  • src/furniture-plan-art.generated.ts — the 60 top-view drawings, a lazy chunk (#474) loaded by src/furniture-art-runtime.ts when a plan draws furniture and imported statically by the editor;
  • src/furniture-menu-art.generated.ts for the lazy editor graph only.

Generated files are never edited manually. The 93 drawings were created by Sergey Matyunin (Matysh) and granted to the project under its MIT License in issue #593. No separate attribution is required in the interface. The pack's own README.md records the reviewed archive and its SHA-256.

Palette interaction

Furniture is a two-level non-modal palette in the editor context tray:

  1. The first level shows front-view categories, grouped as Furniture, Appliances, Plumbing and Other.
  2. Every category opens a second level, including categories that have only one variant.
  3. The second level shows the real top-view drawings. Back returns to the category list and clears any armed symbol.
  4. Picking a variant arms one placement and reveals editable Width and Depth in the Home Assistant length unit. With a mouse, moving over the plan shows the real top-view symbol at its exact future size, wall magnet, rotation and canvas clamp. Editing Width or Depth updates that ghost immediately without requiring another pointer move.
  5. Clicking the plan places exactly the previewed object and returns to Select. Shift keeps the established free-placement behaviour. The ghost is a transient, 55%-opaque rendering aid: it is never saved and never enters Undo history.
  6. Pointer leave, Escape, palette/tool/editor/space changes and remount clear the ghost. Unknown or stale symbol ids show and save nothing.

Touch and pen do not emulate hover or show a placement ghost. Their best-effort editor path saves one object only after a clean tap; movement, pointer cancel or a second contact cancels that pending stamp so pinch/cancel cannot create furniture accidentally. View and kiosk touch guarantees remain unchanged.

The properties dialog remains a flat native select, grouped by category. Its optgroup label includes both the parent group and category because HTML selects cannot nest optgroups.

Transform interaction

Selected furniture has four corner and four middle-edge resize handles. Corner resize is continuous and preserves the original aspect ratio; Shift lets width and depth change independently. A middle handle changes only its local axis, including on a rotated object. Dragging any handle across the fixed opposite edge keeps the gesture alive and mirrors the corresponding axis.

Rotation is continuous normally and snaps to the nearest multiple of 45° while Shift is held. The rotation handle uses a circular-arrow cursor. These rules are furniture-only: other decor and the backdrop keep their existing grid and modifier contracts.

Properties show signed width/depth. A negative width is horizontal mirror and a negative depth is vertical mirror; the two adjacent checkboxes are the same state expressed explicitly. Save stores the absolute extents plus optional flags. Zero is not a valid saved dimension.

Rendering contract

Each object is still one visible <path> and one erase hit path. In Select, an additional invisible path follows the same artwork and extends the target 10 physical centimetres beyond each visible stroke edge; empty areas of the bounding box remain non-interactive. Designer artwork keeps its native SVG viewBox; the renderer applies the user's stored width and depth with a non-uniform transform. vector-effect="non-scaling-stroke" rejects only that local width/depth distortion in the visible result; the renderer separately applies the outer plan viewBox scale to the stroke width. Consequently:

  • resizing changes the physical object box without distorting line weight;
  • camera zoom changes the visible line weight exactly like other physical decor with the same width_cm;
  • the user's decor colour, opacity and physical line width remain authoritative;
  • data-hp="decor", data-kind="furniture", data-id and data-symbol remain stable for card-mod;
  • an id unknown to an older card remains valid data and simply renders nothing instead of breaking the plan.

The top edge of every drawing is BACK (y = 0). Placement and dragging put BACK on a physical surface of a wall, including the local atomic half-thickness; the invisible centreline is not the contact surface. An outer wall exposes both its room-facing and exterior surfaces, so raw pointer intent keeps furniture inside or outside instead of pulling it through the masonry. On a shared wall the raw pointer side selects the room. A new placement exactly on an outer-wall axis defaults inside, while an exact-axis drag keeps the piece's current side. The magnet reach is measured from the selected physical surface. Shift keeps free placement and bypasses the wall magnet. Preview, commit and drag share the same surface resolver. Furniture is decor: it has no entity, state, room aggregation, collision model or automatic binding to later wall edits, and already saved coordinates are never migrated.

Compatibility and performance

The backend schema additively accepts optional boolean furniture flip_h and flip_v; no configuration version or migration is needed. Every public id that existed before #593 still exists and keeps its default centimetre dimensions to the number: pack 0.4.0 changes the drawing inside the box, never the box. An already saved object's w and h are never rewritten, and an id unknown to an older card remains valid data that simply renders nothing.

Plan artwork is available in View and kiosk: a plan with furniture requests the artwork chunk once per page before its first frame, and the first-open veil waits for it; a plan without furniture never requests it. If the chunk cannot be loaded, nothing is drawn until the page reloads, and a toast says so once. Before #593 the 12 primitive symbols drew regardless; that promise was withdrawn deliberately when they became designer artwork, so the failure now behaves the same way for all 60 pieces instead of for 48 of them. Front-view menu art is imported only after the editor runtime is requested. Touch View/kiosk support is blocking; editor ergonomics on touch remain best effort under docs/TOUCH-SUPPORT.md.