mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-04 13:48:57 +00:00
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
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
|
||||
/* Generated by scripts/generate-furniture-assets.mjs. DO NOT EDIT. */
|
||||
export const GENERATED_FURNITURE_CATALOG = [{"id":"coffee_table","group":"furniture","category":"coffee_table","w":120,"h":60},{"id":"coffee_table_round","group":"furniture","category":"coffee_table","w":80,"h":80},{"id":"coffee_table_oval","group":"furniture","category":"coffee_table","w":120,"h":60},{"id":"coffee_table_rounded","group":"furniture","category":"coffee_table","w":120,"h":60},{"id":"table_dining","group":"furniture","category":"dining_table","w":160,"h":90},{"id":"table_round","group":"furniture","category":"dining_table","w":110,"h":110},{"id":"table_dining_oval","group":"furniture","category":"dining_table","w":180,"h":100},{"id":"table_dining_rounded","group":"furniture","category":"dining_table","w":160,"h":90},{"id":"desk","group":"furniture","category":"work_table","w":140,"h":70},{"id":"desk_corner","group":"furniture","category":"work_table","w":160,"h":160},{"id":"chair","group":"furniture","category":"chair","w":50,"h":50},{"id":"chair_bar","group":"furniture","category":"chair","w":45,"h":48},{"id":"armchair","group":"furniture","category":"armchair","w":90,"h":90},{"id":"armchair_office","group":"furniture","category":"armchair","w":65,"h":65},{"id":"sofa","group":"furniture","category":"sofa","w":180,"h":90},{"id":"sofa_three_seat","group":"furniture","category":"sofa","w":240,"h":90},{"id":"sofa_corner_right","group":"furniture","category":"sofa","w":260,"h":170},{"id":"bed_single","group":"furniture","category":"bed","w":90,"h":200},{"id":"bed_double","group":"furniture","category":"bed","w":160,"h":200},{"id":"nightstand","group":"furniture","category":"nightstand","w":50,"h":40},{"id":"cabinet_tv","group":"furniture","category":"nightstand","w":140,"h":45},{"id":"cabinet_shoe","group":"furniture","category":"nightstand","w":80,"h":35},{"id":"cabinet_sink","group":"furniture","category":"nightstand","w":80,"h":50},{"id":"bookshelf","group":"furniture","category":"wardrobe","w":100,"h":35},{"id":"wall_unit","group":"furniture","category":"wardrobe","w":240,"h":45},{"id":"wardrobe","group":"furniture","category":"wardrobe","w":180,"h":60},{"id":"kitchen_floor","group":"furniture","category":"kitchen_cabinet","w":60,"h":60},{"id":"kitchen_floor_corner","group":"furniture","category":"kitchen_cabinet","w":90,"h":90},{"id":"kitchen_wall","group":"furniture","category":"kitchen_cabinet","w":60,"h":35},{"id":"kitchen_wall_corner","group":"furniture","category":"kitchen_cabinet","w":60,"h":60},{"id":"shelf_floor","group":"furniture","category":"shelving","w":100,"h":35},{"id":"shelf_wall","group":"furniture","category":"shelving","w":100,"h":25},{"id":"cooktop_two","group":"appliance","category":"cooktop","w":30,"h":50},{"id":"stove","group":"appliance","category":"cooktop","w":60,"h":60},{"id":"tv","group":"appliance","category":"tv","w":120,"h":28},{"id":"tv_wall","group":"appliance","category":"tv","w":120,"h":15},{"id":"toilet","group":"sanitary","category":"toilet","w":40,"h":70},{"id":"toilet_built_in","group":"sanitary","category":"toilet","w":40,"h":55},{"id":"bathtub","group":"sanitary","category":"bathtub","w":170,"h":75},{"id":"bathtub_corner","group":"sanitary","category":"bathtub","w":140,"h":140},{"id":"bidet","group":"sanitary","category":"bidet","w":40,"h":60},{"id":"bidet_built_in","group":"sanitary","category":"bidet","w":40,"h":50},{"id":"kitchen_sink","group":"sanitary","category":"kitchen_sink","w":60,"h":50},{"id":"kitchen_sink_double","group":"sanitary","category":"kitchen_sink","w":90,"h":50}] as const;
|
||||
export const GENERATED_FURNITURE_CATALOG = [{"id":"coffee_table","group":"furniture","category":"coffee_table","w":120,"h":60},{"id":"coffee_table_round","group":"furniture","category":"coffee_table","w":80,"h":80},{"id":"coffee_table_oval","group":"furniture","category":"coffee_table","w":120,"h":60},{"id":"coffee_table_rounded","group":"furniture","category":"coffee_table","w":120,"h":60},{"id":"table_dining","group":"furniture","category":"dining_table","w":160,"h":90},{"id":"table_round","group":"furniture","category":"dining_table","w":110,"h":110},{"id":"table_dining_oval","group":"furniture","category":"dining_table","w":180,"h":100},{"id":"table_dining_rounded","group":"furniture","category":"dining_table","w":160,"h":90},{"id":"desk","group":"furniture","category":"work_table","w":140,"h":70},{"id":"desk_corner","group":"furniture","category":"work_table","w":160,"h":160},{"id":"chair","group":"furniture","category":"chair","w":50,"h":50},{"id":"chair_bar","group":"furniture","category":"chair","w":45,"h":48},{"id":"armchair","group":"furniture","category":"armchair","w":90,"h":90},{"id":"armchair_office","group":"furniture","category":"armchair","w":65,"h":65},{"id":"sofa","group":"furniture","category":"sofa","w":180,"h":90},{"id":"sofa_three_seat","group":"furniture","category":"sofa","w":240,"h":90},{"id":"sofa_corner_right","group":"furniture","category":"sofa","w":260,"h":170},{"id":"bed_single","group":"furniture","category":"bed","w":90,"h":200},{"id":"bed_double","group":"furniture","category":"bed","w":160,"h":200},{"id":"nightstand","group":"furniture","category":"nightstand","w":50,"h":40},{"id":"cabinet_tv","group":"furniture","category":"nightstand","w":140,"h":45},{"id":"cabinet_shoe","group":"furniture","category":"nightstand","w":80,"h":35},{"id":"cabinet_sink","group":"furniture","category":"nightstand","w":80,"h":50},{"id":"bookshelf","group":"furniture","category":"wardrobe","w":100,"h":35},{"id":"wall_unit","group":"furniture","category":"wardrobe","w":240,"h":45},{"id":"wardrobe","group":"furniture","category":"wardrobe","w":180,"h":60},{"id":"kitchen_floor","group":"furniture","category":"kitchen_cabinet","w":60,"h":60},{"id":"kitchen_floor_corner","group":"furniture","category":"kitchen_cabinet","w":90,"h":90},{"id":"kitchen_wall","group":"furniture","category":"kitchen_cabinet","w":60,"h":35},{"id":"kitchen_wall_corner","group":"furniture","category":"kitchen_cabinet","w":60,"h":60},{"id":"shelf_floor","group":"furniture","category":"shelving","w":100,"h":35},{"id":"shelf_wall","group":"furniture","category":"shelving","w":100,"h":25},{"id":"cooktop_two","group":"appliance","category":"cooktop","w":30,"h":50},{"id":"stove","group":"appliance","category":"cooktop","w":60,"h":60},{"id":"tv","group":"appliance","category":"tv","w":120,"h":28},{"id":"tv_wall","group":"appliance","category":"tv","w":120,"h":15},{"id":"toilet","group":"sanitary","category":"toilet","w":40,"h":70},{"id":"toilet_built_in","group":"sanitary","category":"toilet","w":40,"h":55},{"id":"bathtub","group":"sanitary","category":"bathtub","w":170,"h":75},{"id":"bathtub_corner","group":"sanitary","category":"bathtub","w":140,"h":140},{"id":"bidet","group":"sanitary","category":"bidet","w":40,"h":60},{"id":"bidet_built_in","group":"sanitary","category":"bidet","w":40,"h":50},{"id":"kitchen_sink","group":"sanitary","category":"kitchen_sink","w":60,"h":50},{"id":"kitchen_sink_double","group":"sanitary","category":"kitchen_sink","w":90,"h":50},{"id":"fridge","group":"appliance","category":"fridge","w":60,"h":65},{"id":"dishwasher","group":"appliance","category":"dishwasher","w":60,"h":60},{"id":"washer","group":"appliance","category":"washer","w":60,"h":60},{"id":"dryer","group":"appliance","category":"dryer","w":60,"h":60},{"id":"ac","group":"appliance","category":"air_conditioner","w":90,"h":25},{"id":"water_heater","group":"appliance","category":"boiler","w":45,"h":45},{"id":"shower","group":"sanitary","category":"shower","w":90,"h":90},{"id":"sink","group":"sanitary","category":"sink","w":60,"h":45},{"id":"stairs","group":"other","category":"stairs","w":100,"h":280},{"id":"fireplace","group":"other","category":"fireplace","w":120,"h":40},{"id":"plant","group":"other","category":"plant","w":40,"h":40},{"id":"rug","group":"other","category":"rug","w":200,"h":140},{"id":"computer","group":"appliance","category":"computer","w":120,"h":60},{"id":"hood","group":"appliance","category":"hood","w":60,"h":50},{"id":"oven","group":"appliance","category":"oven","w":60,"h":60},{"id":"cactus","group":"other","category":"plant","w":70,"h":120}] as const;
|
||||
|
||||
+40
-267
@@ -10,17 +10,17 @@
|
||||
* to SCALE: a sofa is a 2.2 x 0.9 m rectangle with a back along one long
|
||||
* side. Putting a 24x24 pictogram into that rectangle stretches a drawing
|
||||
* that was never meant to be stretched, and the result reads as an icon lying
|
||||
* on the floor, not as a piece of furniture. The primary library is therefore
|
||||
* generated from House Plan's own top-view SVG pack. A small compatibility
|
||||
* set that is absent from the pack stays expressed as unit-box primitives.
|
||||
* Both forms resolve to one path plus its native coordinate box; the renderer
|
||||
* scales that box to the stored real-world size. The renderer keeps the
|
||||
* configured decor stroke stable through that local non-uniform transform,
|
||||
* while `furniturePlanScreenScale` restores the outer physical plan zoom.
|
||||
* on the floor, not as a piece of furniture. The library is therefore
|
||||
* generated from House Plan's own top-view SVG pack — all of it, since #593:
|
||||
* the 12 compatibility symbols that used to be drawn from unit-box primitives
|
||||
* got designer artwork under their existing ids. Every symbol resolves to one
|
||||
* path plus its native coordinate box; the renderer scales that box to the
|
||||
* stored real-world size. The renderer keeps the configured decor stroke
|
||||
* stable through that local non-uniform transform, while
|
||||
* `furniturePlanScreenScale` restores the outer physical plan zoom.
|
||||
*
|
||||
* THE CONVENTION EVERY SYMBOL OBEYS:
|
||||
* - x points right and y points DOWN (SVG); generated art keeps its native
|
||||
* viewBox while legacy primitives use `0..1 x 0..1`;
|
||||
* - x points right and y points DOWN (SVG); art keeps its native viewBox;
|
||||
* - `y = 0` is the BACK of the object — the side that goes against a wall.
|
||||
* That is what makes the wall magnet meaningful: a sofa's back, a bed's
|
||||
* headboard, a wardrobe's rear panel and a worktop's edge all mean the
|
||||
@@ -37,23 +37,6 @@ import { FURNITURE_ART_RUNTIME, type FurnitureArtHost } from './furniture-art-ru
|
||||
export const FURNITURE_GROUPS = ['furniture', 'appliance', 'sanitary', 'other'] as const;
|
||||
export type FurnitureGroup = (typeof FURNITURE_GROUPS)[number];
|
||||
|
||||
/**
|
||||
* A drawing primitive in the unit box. Deliberately tiny — four shapes are
|
||||
* enough for a plan symbol, and each one maps to a couple of path commands,
|
||||
* so a symbol costs a few dozen bytes in the bundle instead of a kilobyte of
|
||||
* Inkscape output.
|
||||
*
|
||||
* ['r', x, y, w, h] rectangle
|
||||
* ['l', x1, y1, x2, y2] line
|
||||
* ['e', cx, cy, rx, ry] ellipse
|
||||
* ['p', x1, y1, x2, y2, ...] open polyline (2 points or more)
|
||||
*/
|
||||
export type Prim =
|
||||
| ['r', number, number, number, number]
|
||||
| ['l', number, number, number, number]
|
||||
| ['e', number, number, number, number]
|
||||
| ['p', ...number[]];
|
||||
|
||||
export interface FurnitureSymbol {
|
||||
/** Stable id — it is what the config stores and what `data-symbol` carries. */
|
||||
id: string;
|
||||
@@ -63,10 +46,6 @@ export interface FurnitureSymbol {
|
||||
/** Default REAL size in centimetres: width ALONG the back edge x depth. */
|
||||
w: number;
|
||||
h: number;
|
||||
/** Legacy drawing retained for symbols absent from the designer pack. */
|
||||
g?: Prim[];
|
||||
/** Designer symbol: its drawing lives in the lazy artwork chunk (#474). */
|
||||
designer?: true;
|
||||
}
|
||||
|
||||
export interface FurnitureGraphic {
|
||||
@@ -75,190 +54,27 @@ export interface FurnitureGraphic {
|
||||
viewH: number;
|
||||
}
|
||||
|
||||
// The outline every boxy symbol starts from, named once so the table reads.
|
||||
const box = (): Prim => ['r', 0, 0, 1, 1];
|
||||
|
||||
/**
|
||||
* The library. Sizes are the ones a European flat actually has; they are
|
||||
* DEFAULTS, not limits — the palette lets the user type over both numbers
|
||||
* before placing, and the corner handles change them afterwards.
|
||||
* The public library: every symbol of the designer pack, nothing else.
|
||||
*
|
||||
* Until #593 this was a union of the generated catalogue and 12 symbols drawn
|
||||
* from unit-box primitives, kept because the pack had no top view for them.
|
||||
* Pack 0.4.0 draws all 60, so the union is gone — and with it the trap it
|
||||
* carried: both halves used the SAME public ids, and `BY_ID` below keeps the
|
||||
* LAST entry, so a primitive silently won over the designer drawing it was
|
||||
* supposed to be replaced by.
|
||||
*
|
||||
* Sizes are the ones a European flat actually has; they are DEFAULTS, not
|
||||
* limits — the palette lets the user type over both numbers before placing,
|
||||
* and the corner handles change them afterwards.
|
||||
*/
|
||||
const LEGACY_FURNITURE: FurnitureSymbol[] = [
|
||||
// ------------------------------- мебель --------------------------------
|
||||
{ id: 'sofa', group: 'furniture', category: 'sofa', w: 220, h: 90, g: [
|
||||
box(),
|
||||
['l', 0.09, 0.26, 0.91, 0.26], // the back cushion
|
||||
['l', 0.09, 0.26, 0.09, 1], // armrests
|
||||
['l', 0.91, 0.26, 0.91, 1],
|
||||
['l', 0.5, 0.26, 0.5, 1], // two seats
|
||||
] },
|
||||
{ id: 'armchair', group: 'furniture', category: 'armchair', w: 90, h: 85, g: [
|
||||
box(),
|
||||
['l', 0.14, 0.28, 0.86, 0.28],
|
||||
['l', 0.14, 0.28, 0.14, 1],
|
||||
['l', 0.86, 0.28, 0.86, 1],
|
||||
] },
|
||||
{ id: 'coffee_table', group: 'furniture', category: 'coffee_table', w: 110, h: 60, g: [
|
||||
box(), ['r', 0.08, 0.14, 0.84, 0.72],
|
||||
] },
|
||||
{ id: 'table_dining', group: 'furniture', category: 'dining_table', w: 140, h: 80, g: [
|
||||
box(), ['r', 0.06, 0.11, 0.88, 0.78],
|
||||
] },
|
||||
{ id: 'table_round', group: 'furniture', category: 'dining_table', w: 120, h: 120, g: [
|
||||
['e', 0.5, 0.5, 0.5, 0.5], ['e', 0.5, 0.5, 0.41, 0.41],
|
||||
] },
|
||||
{ id: 'chair', group: 'furniture', category: 'chair', w: 45, h: 45, g: [
|
||||
['r', 0, 0, 1, 0.18], // the back
|
||||
['r', 0.06, 0.18, 0.88, 0.8], // the seat
|
||||
] },
|
||||
{ id: 'desk', group: 'furniture', category: 'work_table', w: 120, h: 60, g: [
|
||||
box(),
|
||||
['r', 0.63, 0.07, 0.31, 0.86], // the drawer pedestal
|
||||
['l', 0.63, 0.5, 0.94, 0.5],
|
||||
] },
|
||||
{ id: 'bed_double', group: 'furniture', category: 'bed', w: 160, h: 200, g: [
|
||||
box(),
|
||||
['r', 0, 0, 1, 0.07], // the headboard, i.e. the back
|
||||
['r', 0.06, 0.1, 0.4, 0.15], // two pillows
|
||||
['r', 0.54, 0.1, 0.4, 0.15],
|
||||
['l', 0, 0.33, 1, 0.33], // the turned-down blanket
|
||||
] },
|
||||
{ id: 'bed_single', group: 'furniture', category: 'bed', w: 90, h: 200, g: [
|
||||
box(),
|
||||
['r', 0, 0, 1, 0.07],
|
||||
['r', 0.15, 0.1, 0.7, 0.15],
|
||||
['l', 0, 0.33, 1, 0.33],
|
||||
] },
|
||||
{ id: 'nightstand', group: 'furniture', category: 'nightstand', w: 45, h: 40, g: [
|
||||
box(), ['r', 0.12, 0.14, 0.76, 0.33], ['r', 0.12, 0.53, 0.76, 0.33],
|
||||
] },
|
||||
{ id: 'wardrobe', group: 'furniture', category: 'wardrobe', w: 100, h: 60, g: [
|
||||
box(),
|
||||
['l', 0, 0.72, 1, 0.72], // the hanging rail
|
||||
['l', 0.5, 0.72, 0.5, 1], // the doors meet here
|
||||
] },
|
||||
{ id: 'bookshelf', group: 'furniture', category: 'wardrobe', w: 80, h: 30, g: [
|
||||
box(), ['l', 0.34, 0, 0.34, 1], ['l', 0.67, 0, 0.67, 1],
|
||||
] },
|
||||
|
||||
// ------------------------------- техника -------------------------------
|
||||
{ id: 'fridge', group: 'appliance', category: 'fridge', w: 60, h: 65, g: [
|
||||
box(),
|
||||
['l', 0, 0.36, 1, 0.36], // freezer / fridge
|
||||
['l', 0.83, 0.44, 0.83, 0.64], // the handle
|
||||
] },
|
||||
{ id: 'stove', group: 'appliance', category: 'cooktop', w: 60, h: 60, g: [
|
||||
box(),
|
||||
['e', 0.29, 0.31, 0.15, 0.15], ['e', 0.71, 0.31, 0.15, 0.15],
|
||||
['e', 0.29, 0.71, 0.15, 0.15], ['e', 0.71, 0.71, 0.15, 0.15],
|
||||
] },
|
||||
{ id: 'dishwasher', group: 'appliance', category: 'dishwasher', w: 60, h: 60, g: [
|
||||
box(),
|
||||
['r', 0.1, 0.12, 0.8, 0.76],
|
||||
['e', 0.5, 0.5, 0.27, 0.27], ['e', 0.5, 0.5, 0.13, 0.13], // plates
|
||||
] },
|
||||
{ id: 'washer', group: 'appliance', category: 'washer', w: 60, h: 60, g: [
|
||||
box(),
|
||||
['l', 0.08, 0.17, 0.92, 0.17], // the control panel
|
||||
['e', 0.5, 0.57, 0.3, 0.3], ['e', 0.5, 0.57, 0.14, 0.14], // the drum
|
||||
] },
|
||||
{ id: 'dryer', group: 'appliance', category: 'dryer', w: 60, h: 60, g: [
|
||||
box(),
|
||||
['l', 0.08, 0.17, 0.92, 0.17],
|
||||
['e', 0.5, 0.57, 0.3, 0.3],
|
||||
['p', 0.36, 0.5, 0.5, 0.64, 0.64, 0.5], // the chevron that is not a drum
|
||||
] },
|
||||
{ id: 'tv', group: 'appliance', category: 'tv', w: 120, h: 30, g: [
|
||||
['r', 0, 0, 1, 0.42], // the screen, seen edge-on
|
||||
['l', 0.5, 0.42, 0.5, 0.72],
|
||||
['l', 0.3, 0.72, 0.7, 0.72], // the stand
|
||||
] },
|
||||
{ id: 'ac', group: 'appliance', category: 'air_conditioner', w: 90, h: 25, g: [
|
||||
box(), ['l', 0.05, 0.55, 0.95, 0.55], ['l', 0.05, 0.79, 0.95, 0.79],
|
||||
] },
|
||||
{ id: 'water_heater', group: 'appliance', category: 'boiler', w: 45, h: 45, g: [
|
||||
['e', 0.5, 0.5, 0.5, 0.5], ['e', 0.5, 0.5, 0.31, 0.31],
|
||||
] },
|
||||
|
||||
// ----------------------------- сантехника ------------------------------
|
||||
{ id: 'toilet', group: 'sanitary', category: 'toilet', w: 40, h: 70, g: [
|
||||
['r', 0.06, 0, 0.88, 0.2], // the cistern, against the wall
|
||||
['e', 0.5, 0.58, 0.37, 0.35], // the bowl
|
||||
['e', 0.5, 0.58, 0.22, 0.2],
|
||||
] },
|
||||
{ id: 'bathtub', group: 'sanitary', category: 'bathtub', w: 170, h: 75, g: [
|
||||
box(),
|
||||
['r', 0.05, 0.11, 0.77, 0.78],
|
||||
['e', 0.89, 0.5, 0.045, 0.1], // the drain end
|
||||
] },
|
||||
{ id: 'shower', group: 'sanitary', category: 'shower', w: 90, h: 90, g: [
|
||||
box(),
|
||||
['l', 0, 0, 1, 1], ['l', 1, 0, 0, 1], // the tray, as every plan draws it
|
||||
['e', 0.5, 0.5, 0.08, 0.08],
|
||||
] },
|
||||
{ id: 'sink', group: 'sanitary', category: 'sink', w: 60, h: 45, g: [
|
||||
box(),
|
||||
['e', 0.5, 0.6, 0.34, 0.3],
|
||||
['e', 0.5, 0.15, 0.07, 0.07], // the tap
|
||||
] },
|
||||
{ id: 'kitchen_sink', group: 'sanitary', category: 'kitchen_sink', w: 80, h: 60, g: [
|
||||
box(),
|
||||
['r', 0.06, 0.24, 0.44, 0.64],
|
||||
['r', 0.54, 0.24, 0.4, 0.64],
|
||||
['e', 0.5, 0.12, 0.06, 0.06],
|
||||
] },
|
||||
{ id: 'bidet', group: 'sanitary', category: 'bidet', w: 40, h: 55, g: [
|
||||
['e', 0.5, 0.5, 0.44, 0.5], ['e', 0.5, 0.5, 0.26, 0.3],
|
||||
] },
|
||||
|
||||
// ------------------------------- прочее --------------------------------
|
||||
{ id: 'stairs', group: 'other', category: 'stairs', w: 100, h: 280, g: [
|
||||
box(),
|
||||
['l', 0, 0.111, 1, 0.111], ['l', 0, 0.222, 1, 0.222], ['l', 0, 0.333, 1, 0.333],
|
||||
['l', 0, 0.444, 1, 0.444], ['l', 0, 0.556, 1, 0.556], ['l', 0, 0.667, 1, 0.667],
|
||||
['l', 0, 0.778, 1, 0.778], ['l', 0, 0.889, 1, 0.889],
|
||||
['l', 0.5, 0.93, 0.5, 0.06], // the "up" arrow
|
||||
['p', 0.38, 0.16, 0.5, 0.06, 0.62, 0.16],
|
||||
] },
|
||||
{ id: 'fireplace', group: 'other', category: 'fireplace', w: 120, h: 40, g: [
|
||||
box(), ['p', 0.22, 1, 0.22, 0.42, 0.78, 0.42, 0.78, 1],
|
||||
] },
|
||||
// #584: единственный legacy-примитив с полями — рисунок занимал 0.02…0.98
|
||||
// unit box, то есть 40-сантиметровое растение выглядело на 4 % меньше
|
||||
// соседей того же размера. Растянуто от центра в 1/0.96: форма прежняя,
|
||||
// габарит стал заявленным.
|
||||
{ id: 'plant', group: 'other', category: 'plant', w: 40, h: 40, g: [
|
||||
['e', 0.5, 0.5, 0.22916667, 0.22916667],
|
||||
['l', 0.5, 0.27083333, 0.5, 0], ['l', 0.5, 0.72916667, 0.5, 1],
|
||||
['l', 0.27083333, 0.5, 0, 0.5], ['l', 0.72916667, 0.5, 1, 0.5],
|
||||
['l', 0.33333333, 0.33333333, 0.11458333, 0.11458333],
|
||||
['l', 0.66666667, 0.66666667, 0.88541667, 0.88541667],
|
||||
['l', 0.66666667, 0.33333333, 0.88541667, 0.11458333],
|
||||
['l', 0.33333333, 0.66666667, 0.11458333, 0.88541667],
|
||||
] },
|
||||
{ id: 'rug', group: 'other', category: 'rug', w: 200, h: 140, g: [
|
||||
box(), ['r', 0.06, 0.09, 0.88, 0.82],
|
||||
] },
|
||||
];
|
||||
|
||||
const RETAINED_IDS = new Set([
|
||||
'fridge', 'dishwasher', 'washer', 'dryer', 'ac', 'water_heater',
|
||||
'shower', 'sink', 'stairs', 'fireplace', 'plant', 'rug',
|
||||
]);
|
||||
|
||||
/** Complete public library: 44 designer symbols plus the 12 useful existing
|
||||
* symbols for which the pack intentionally has no top-view replacement. */
|
||||
export const FURNITURE: FurnitureSymbol[] = [
|
||||
...GENERATED_FURNITURE_CATALOG.map((symbol) => ({
|
||||
id: symbol.id,
|
||||
group: symbol.group as FurnitureGroup,
|
||||
category: symbol.category,
|
||||
w: symbol.w,
|
||||
h: symbol.h,
|
||||
designer: true as const,
|
||||
})),
|
||||
...LEGACY_FURNITURE.filter((symbol) => RETAINED_IDS.has(symbol.id)),
|
||||
];
|
||||
export const FURNITURE: FurnitureSymbol[] = GENERATED_FURNITURE_CATALOG.map((symbol) => ({
|
||||
id: symbol.id,
|
||||
group: symbol.group as FurnitureGroup,
|
||||
category: symbol.category,
|
||||
w: symbol.w,
|
||||
h: symbol.h,
|
||||
}));
|
||||
|
||||
const BY_ID = new Map(FURNITURE.map((s) => [s.id, s]));
|
||||
|
||||
@@ -269,9 +85,11 @@ export function furnitureSymbol(id: string | null | undefined): FurnitureSymbol
|
||||
return (id && BY_ID.get(id)) || null;
|
||||
}
|
||||
|
||||
/** Designer symbol whose drawing is lazy (#474); legacy symbols draw eagerly. */
|
||||
/** Is this a known symbol, and therefore one whose drawing arrives with the
|
||||
* lazy artwork chunk (#474)? Since #593 the two questions have one answer:
|
||||
* every symbol is designer artwork, and an unknown id has no drawing at all. */
|
||||
export function furnitureArtIsLazy(id: string): boolean {
|
||||
return BY_ID.get(id)?.designer === true;
|
||||
return BY_ID.has(id);
|
||||
}
|
||||
|
||||
/** The symbols of one group, in table order. */
|
||||
@@ -328,68 +146,23 @@ export function clampFurnCm(cm: number): number {
|
||||
|
||||
// ------------------------------ the drawing --------------------------------
|
||||
|
||||
const num = (v: number): string => {
|
||||
const r = Math.round(v * 1000) / 1000;
|
||||
return Object.is(r, -0) ? '0' : String(r);
|
||||
};
|
||||
|
||||
/**
|
||||
* The symbol as ONE `d` string, in a box `w x h` whose top-left is `(0,0)`.
|
||||
*
|
||||
* One path per piece, not one element per primitive: the whole symbol then
|
||||
* takes a single stroke, a single pointer target and a single `data-symbol`,
|
||||
* and the decor layer's DOM does not grow by ten nodes per sofa.
|
||||
*/
|
||||
function primitivePathD(s: FurnitureSymbol, w: number, h: number): string {
|
||||
if (!s.g || !(w > 0) || !(h > 0)) return '';
|
||||
const X = (v: number) => num(v * w);
|
||||
const Y = (v: number) => num(v * h);
|
||||
const out: string[] = [];
|
||||
for (const p of s.g) {
|
||||
if (p[0] === 'r') {
|
||||
const [, x, y, pw, ph] = p;
|
||||
out.push(`M${X(x)} ${Y(y)}H${X(x + pw)}V${Y(y + ph)}H${X(x)}Z`);
|
||||
} else if (p[0] === 'l') {
|
||||
const [, x1, y1, x2, y2] = p;
|
||||
out.push(`M${X(x1)} ${Y(y1)}L${X(x2)} ${Y(y2)}`);
|
||||
} else if (p[0] === 'e') {
|
||||
const [, cx, cy, rx, ry] = p;
|
||||
// two half-arcs: a full ellipse cannot be one A command
|
||||
out.push(`M${X(cx - rx)} ${Y(cy)}`
|
||||
+ `A${num(rx * w)} ${num(ry * h)} 0 0 1 ${X(cx + rx)} ${Y(cy)}`
|
||||
+ `A${num(rx * w)} ${num(ry * h)} 0 0 1 ${X(cx - rx)} ${Y(cy)}Z`);
|
||||
} else {
|
||||
const pts = p.slice(1) as number[];
|
||||
if (pts.length < 4) continue;
|
||||
let d = `M${X(pts[0])} ${Y(pts[1])}`;
|
||||
for (let i = 2; i + 1 < pts.length; i += 2) d += `L${X(pts[i])} ${Y(pts[i + 1])}`;
|
||||
out.push(d);
|
||||
}
|
||||
}
|
||||
return out.join('');
|
||||
}
|
||||
|
||||
/** Native one-path artwork and its coordinate box. The renderer scales this
|
||||
* box non-uniformly to the user's stored dimensions; the furniture stroke
|
||||
* helpers below separate that local scale from the outer plan camera. */
|
||||
export function furnitureGraphic(id: string, host?: FurnitureArtHost): FurnitureGraphic | null {
|
||||
const symbol = furnitureSymbol(id);
|
||||
if (!symbol) return null;
|
||||
// Designer artwork is lazy (#474): `undefined` while the chunk is pending or
|
||||
// settled into fallback renders the piece as an unknown symbol — nothing —
|
||||
// and a host passed here is re-rendered once the runtime settles.
|
||||
if (symbol.designer) return FURNITURE_ART_RUNTIME.art(id, host) ?? null;
|
||||
const d = primitivePathD(symbol, 1, 1);
|
||||
return d ? { d, viewW: 1, viewH: 1 } : null;
|
||||
if (!furnitureSymbol(id)) return null;
|
||||
// Artwork is lazy (#474): `undefined` while the chunk is pending or settled
|
||||
// into fallback renders the piece as an unknown symbol — nothing — and a
|
||||
// host passed here is re-rendered once the runtime settles. Since #593 that
|
||||
// is true for all 60 symbols, including the 12 that used to draw eagerly.
|
||||
return FURNITURE_ART_RUNTIME.art(id, host) ?? null;
|
||||
}
|
||||
|
||||
/** Compatibility helper for callers that only need a path string. Designer
|
||||
* artwork remains in its native viewBox; use `furnitureGraphic` when sizing it. */
|
||||
export function furniturePathD(id: string, w: number, h: number): string {
|
||||
const symbol = furnitureSymbol(id);
|
||||
if (!symbol || !(w > 0) || !(h > 0)) return '';
|
||||
if (symbol.designer) return FURNITURE_ART_RUNTIME.art(id)?.d || '';
|
||||
return primitivePathD(symbol, w, h);
|
||||
if (!furnitureSymbol(id) || !(w > 0) || !(h > 0)) return '';
|
||||
return FURNITURE_ART_RUNTIME.art(id)?.d || '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1129,15 +1129,18 @@
|
||||
"furn.cat_boiler": "Warmwasserbereiter",
|
||||
"furn.cat_chair": "Stühle",
|
||||
"furn.cat_coffee_table": "Couchtische",
|
||||
"furn.cat_computer": "Computer",
|
||||
"furn.cat_cooktop": "Kochfelder",
|
||||
"furn.cat_dining_table": "Esstische",
|
||||
"furn.cat_dishwasher": "Geschirrspüler",
|
||||
"furn.cat_dryer": "Wäschetrockner",
|
||||
"furn.cat_fireplace": "Kamine",
|
||||
"furn.cat_fridge": "Kühlschränke",
|
||||
"furn.cat_hood": "Dunstabzugshaube",
|
||||
"furn.cat_kitchen_cabinet": "Küchenschränke",
|
||||
"furn.cat_kitchen_sink": "Küchenspülen",
|
||||
"furn.cat_nightstand": "Schränke",
|
||||
"furn.cat_oven": "Backofen",
|
||||
"furn.cat_plant": "Pflanzen",
|
||||
"furn.cat_rug": "Teppiche",
|
||||
"furn.cat_shelving": "Regale",
|
||||
@@ -1150,6 +1153,10 @@
|
||||
"furn.cat_wardrobe": "Kleiderschränke",
|
||||
"furn.cat_washer": "Waschmaschinen",
|
||||
"furn.cat_work_table": "Schreibtische",
|
||||
"furn.sym_cactus": "Kaktus",
|
||||
"furn.sym_computer": "Computer",
|
||||
"furn.sym_hood": "Dunstabzugshaube",
|
||||
"furn.sym_oven": "Backofen",
|
||||
"furn.sym_sofa": "Zweisitzer-Sofa",
|
||||
"furn.sym_sofa_three_seat": "Dreisitzer-Sofa",
|
||||
"furn.sym_sofa_corner_right": "Ecksofa rechts",
|
||||
|
||||
@@ -1129,15 +1129,18 @@
|
||||
"furn.cat_boiler": "Water heaters",
|
||||
"furn.cat_chair": "Chairs",
|
||||
"furn.cat_coffee_table": "Coffee tables",
|
||||
"furn.cat_computer": "Computer",
|
||||
"furn.cat_cooktop": "Cooktops",
|
||||
"furn.cat_dining_table": "Dining tables",
|
||||
"furn.cat_dishwasher": "Dishwashers",
|
||||
"furn.cat_dryer": "Dryers",
|
||||
"furn.cat_fireplace": "Fireplaces",
|
||||
"furn.cat_fridge": "Refrigerators",
|
||||
"furn.cat_hood": "Cooker hood",
|
||||
"furn.cat_kitchen_cabinet": "Kitchen cabinets",
|
||||
"furn.cat_kitchen_sink": "Kitchen sinks",
|
||||
"furn.cat_nightstand": "Cabinets",
|
||||
"furn.cat_oven": "Oven",
|
||||
"furn.cat_plant": "Plants",
|
||||
"furn.cat_rug": "Rugs",
|
||||
"furn.cat_shelving": "Shelving",
|
||||
@@ -1150,6 +1153,10 @@
|
||||
"furn.cat_wardrobe": "Wardrobes",
|
||||
"furn.cat_washer": "Washing machines",
|
||||
"furn.cat_work_table": "Desks",
|
||||
"furn.sym_cactus": "Cactus",
|
||||
"furn.sym_computer": "Computer",
|
||||
"furn.sym_hood": "Cooker hood",
|
||||
"furn.sym_oven": "Oven",
|
||||
"furn.sym_sofa": "Two-seat sofa",
|
||||
"furn.sym_sofa_three_seat": "Three-seat sofa",
|
||||
"furn.sym_sofa_corner_right": "Right sectional sofa",
|
||||
|
||||
@@ -1129,15 +1129,18 @@
|
||||
"furn.cat_boiler": "Chauffe-eau",
|
||||
"furn.cat_chair": "Chaises",
|
||||
"furn.cat_coffee_table": "Tables basses",
|
||||
"furn.cat_computer": "Ordinateur",
|
||||
"furn.cat_cooktop": "Plaques de cuisson",
|
||||
"furn.cat_dining_table": "Tables à manger",
|
||||
"furn.cat_dishwasher": "Lave-vaisselle",
|
||||
"furn.cat_dryer": "Sèche-linge",
|
||||
"furn.cat_fireplace": "Cheminées",
|
||||
"furn.cat_fridge": "Réfrigérateurs",
|
||||
"furn.cat_hood": "Hotte",
|
||||
"furn.cat_kitchen_cabinet": "Meubles de cuisine",
|
||||
"furn.cat_kitchen_sink": "Éviers de cuisine",
|
||||
"furn.cat_nightstand": "Meubles",
|
||||
"furn.cat_oven": "Four",
|
||||
"furn.cat_plant": "Plantes",
|
||||
"furn.cat_rug": "Tapis",
|
||||
"furn.cat_shelving": "Étagères",
|
||||
@@ -1150,6 +1153,10 @@
|
||||
"furn.cat_wardrobe": "Armoires",
|
||||
"furn.cat_washer": "Lave-linge",
|
||||
"furn.cat_work_table": "Bureaux",
|
||||
"furn.sym_cactus": "Cactus",
|
||||
"furn.sym_computer": "Ordinateur",
|
||||
"furn.sym_hood": "Hotte",
|
||||
"furn.sym_oven": "Four",
|
||||
"furn.sym_sofa": "Canapé",
|
||||
"furn.sym_sofa_three_seat": "Canapé trois places",
|
||||
"furn.sym_sofa_corner_right": "Canapé d'angle droit",
|
||||
|
||||
@@ -1129,15 +1129,18 @@
|
||||
"furn.cat_boiler": "Бойлеры",
|
||||
"furn.cat_chair": "Стулья",
|
||||
"furn.cat_coffee_table": "Журнальные столы",
|
||||
"furn.cat_computer": "Компьютер",
|
||||
"furn.cat_cooktop": "Варочные панели",
|
||||
"furn.cat_dining_table": "Обеденные столы",
|
||||
"furn.cat_dishwasher": "Посудомоечные машины",
|
||||
"furn.cat_dryer": "Сушильные машины",
|
||||
"furn.cat_fireplace": "Камины",
|
||||
"furn.cat_fridge": "Холодильники",
|
||||
"furn.cat_hood": "Вытяжка",
|
||||
"furn.cat_kitchen_cabinet": "Кухонные шкафы",
|
||||
"furn.cat_kitchen_sink": "Кухонные мойки",
|
||||
"furn.cat_nightstand": "Тумбы",
|
||||
"furn.cat_oven": "Духовка",
|
||||
"furn.cat_plant": "Растения",
|
||||
"furn.cat_rug": "Ковры",
|
||||
"furn.cat_shelving": "Стеллажи",
|
||||
@@ -1150,6 +1153,10 @@
|
||||
"furn.cat_wardrobe": "Шкафы",
|
||||
"furn.cat_washer": "Стиральные машины",
|
||||
"furn.cat_work_table": "Рабочие столы",
|
||||
"furn.sym_cactus": "Кактус",
|
||||
"furn.sym_computer": "Компьютер",
|
||||
"furn.sym_hood": "Вытяжка",
|
||||
"furn.sym_oven": "Духовка",
|
||||
"furn.sym_sofa": "Диван двухместный",
|
||||
"furn.sym_sofa_three_seat": "Диван трёхместный",
|
||||
"furn.sym_sofa_corner_right": "Угловой диван, правый",
|
||||
|
||||
Reference in New Issue
Block a user