feat: expand furniture library

Issue: #159
User-Visible: yes
This commit is contained in:
Matysh
2026-08-28 18:52:03 +03:00
parent cc05df0b33
commit 7909ab6eed
120 changed files with 3318 additions and 1591 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+92 -42
View File
@@ -10,14 +10,16 @@
* 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. So the symbols below are drawn
* here, in a UNIT BOX, and generated at the shape's real size — which also
* means the stroke is a plain `stroke-width` in render units, exactly like
* every other decor shape, instead of a `vector-effect` hack fighting a
* non-uniform scale.
* 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 and keeps the configured decor
* stroke stable with `vector-effect="non-scaling-stroke"`.
*
* THE CONVENTION EVERY SYMBOL OBEYS:
* - the box is `0..1 x 0..1`, x to the right, y DOWN (SVG);
* - x points right and y points DOWN (SVG); generated art keeps its native
* viewBox while legacy primitives use `0..1 x 0..1`;
* - `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
@@ -27,6 +29,7 @@
*/
import { NORM_W, GRID_PITCH, CANVAS_LIMIT } from './space-geometry';
import { GENERATED_FURNITURE_PLAN } from './furniture-plan-art.generated';
/** Groups the palette shows, in the order it shows them. */
export const FURNITURE_GROUPS = ['furniture', 'appliance', 'sanitary', 'other'] as const;
@@ -53,11 +56,21 @@ export interface FurnitureSymbol {
/** Stable id — it is what the config stores and what `data-symbol` carries. */
id: string;
group: FurnitureGroup;
/** Category tile in the two-level editor palette. */
category: string;
/** Default REAL size in centimetres: width ALONG the back edge x depth. */
w: number;
h: number;
/** The drawing, in the unit box described above. */
g: Prim[];
/** Legacy drawing retained for symbols absent from the designer pack. */
g?: Prim[];
/** Designer drawing with its native SVG viewBox. */
art?: FurnitureGraphic;
}
export interface FurnitureGraphic {
d: string;
viewW: number;
viewH: number;
}
// The outline every boxy symbol starts from, named once so the table reads.
@@ -68,136 +81,136 @@ const box = (): Prim => ['r', 0, 0, 1, 1];
* DEFAULTS, not limits — the palette lets the user type over both numbers
* before placing, and the corner handles change them afterwards.
*/
export const FURNITURE: FurnitureSymbol[] = [
const LEGACY_FURNITURE: FurnitureSymbol[] = [
// ------------------------------- мебель --------------------------------
{ id: 'sofa', group: 'furniture', w: 220, h: 90, g: [
{ 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', w: 90, h: 85, g: [
{ 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', w: 110, h: 60, g: [
{ 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', w: 140, h: 80, g: [
{ 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', w: 120, h: 120, g: [
{ 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', w: 45, h: 45, g: [
{ 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', w: 120, h: 60, g: [
{ 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', w: 160, h: 200, g: [
{ 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', w: 90, h: 200, g: [
{ 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', w: 45, h: 40, g: [
{ 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', w: 100, h: 60, g: [
{ 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', w: 80, h: 30, g: [
{ 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', w: 60, h: 65, g: [
{ 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', w: 60, h: 60, g: [
{ 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', w: 60, h: 60, g: [
{ 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', w: 60, h: 60, g: [
{ 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', w: 60, h: 60, g: [
{ 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', w: 120, h: 30, g: [
{ 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', w: 90, h: 25, g: [
{ 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', w: 45, h: 45, g: [
{ 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', w: 40, h: 70, g: [
{ 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', w: 170, h: 75, g: [
{ 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', w: 90, h: 90, g: [
{ 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', w: 60, h: 45, g: [
{ 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', w: 80, h: 60, g: [
{ 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', w: 40, h: 55, g: [
{ 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', w: 100, h: 280, g: [
{ 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],
@@ -205,21 +218,40 @@ export const FURNITURE: FurnitureSymbol[] = [
['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', w: 120, h: 40, g: [
{ 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],
] },
{ id: 'plant', group: 'other', w: 40, h: 40, g: [
{ id: 'plant', group: 'other', category: 'plant', w: 40, h: 40, g: [
['e', 0.5, 0.5, 0.22, 0.22],
['l', 0.5, 0.28, 0.5, 0.02], ['l', 0.5, 0.72, 0.5, 0.98],
['l', 0.28, 0.5, 0.02, 0.5], ['l', 0.72, 0.5, 0.98, 0.5],
['l', 0.34, 0.34, 0.13, 0.13], ['l', 0.66, 0.66, 0.87, 0.87],
['l', 0.66, 0.34, 0.87, 0.13], ['l', 0.34, 0.66, 0.13, 0.87],
] },
{ id: 'rug', group: 'other', w: 200, h: 140, g: [
{ 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_PLAN.map((symbol) => ({
id: symbol.id,
group: symbol.group as FurnitureGroup,
category: symbol.category,
w: symbol.w,
h: symbol.h,
art: symbol.art,
})),
...LEGACY_FURNITURE.filter((symbol) => RETAINED_IDS.has(symbol.id)),
];
const BY_ID = new Map(FURNITURE.map((s) => [s.id, s]));
/** The symbol with this id, or null. An unknown id is DATA, not a crash: a
@@ -295,9 +327,8 @@ const num = (v: number): string => {
* 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.
*/
export function furniturePathD(id: string, w: number, h: number): string {
const s = furnitureSymbol(id);
if (!s || !(w > 0) || !(h > 0)) return '';
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[] = [];
@@ -325,6 +356,25 @@ export function furniturePathD(id: string, w: number, h: number): string {
return out.join('');
}
/** Native one-path artwork and its coordinate box. The renderer scales this
* box non-uniformly to the user's stored dimensions and keeps the decor stroke
* physical with `vector-effect=non-scaling-stroke`. */
export function furnitureGraphic(id: string): FurnitureGraphic | null {
const symbol = furnitureSymbol(id);
if (!symbol) return null;
if (symbol.art) return symbol.art;
const d = primitivePathD(symbol, 1, 1);
return d ? { d, viewW: 1, viewH: 1 } : 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 '';
return symbol.art?.d || primitivePathD(symbol, w, h);
}
// ------------------------------ the wall magnet -----------------------------
/** How far from a wall the magnet still reaches, in GRID CELLS. Six cells is
+16 -17
View File
@@ -61,7 +61,7 @@ import {
import { dayCycleStageVars, renderDayCycleEnvironment } from './day-cycle-render';
import {
FURNITURE_GROUPS, furnitureOfGroup, furnitureSymbol, furnitureDefaultCm,
furniturePathD, furnitureCorners, snapFurnitureToWall,
furnitureGraphic, furnitureCorners, snapFurnitureToWall,
cmToNorm, clampFurnSize, clampFurnCm, FURN_WALL_CELLS, type FurnitureGroup,
} from './furniture';
import {
@@ -1033,6 +1033,8 @@ export class HouseplanCard extends LitElement {
* a unit system is a display setting, never a stored one.
*/
private _furnPalette: { symbol: string; w: number; h: number } | null = null;
/** Category currently open in the two-level furniture palette. */
private _furnCategory: string | null = null;
/** The selected shape's own unrotated frame (text is measured from SVG). */
private _dtBox: { id: string; x: number; y: number; w: number; h: number } | null = null;
/**
@@ -2426,6 +2428,7 @@ export class HouseplanCard extends LitElement {
_decorShapeDialog: { state: true },
_backdropDialog: { state: true },
_furnPalette: { state: true },
_furnCategory: { state: true },
_bdDrag: { state: true },
_dtBox: { state: true },
_dtDrag: { state: true },
@@ -2777,6 +2780,7 @@ export class HouseplanCard extends LitElement {
// to Select in one step, regardless of whether a symbol was armed.
else if (this._decorTool === 'furniture') {
this._furnPalette = null;
this._furnCategory = null;
this._decorTool = 'select';
}
else if (this._decorSel) this._decorSel = null;
@@ -7823,13 +7827,7 @@ export class HouseplanCard extends LitElement {
];
}
/**
* The palette: every symbol of the library, grouped, each drawn with the
* very same `furniturePathD` the plan uses — so what the user picks is what
* the user gets, at any size, and there is no second set of preview assets
* to keep in step. Under it the two size fields, prefilled with the chosen
* symbol's real size and shown in metres or feet by the HA unit system.
*/
/** The lazy editor runtime owns the generated two-level furniture palette. */
private _renderFurnPalette(): TemplateResult {
return this._editorRuntimeOrThrow()._renderFurnPalette();
}
@@ -8068,24 +8066,25 @@ export class HouseplanCard extends LitElement {
transform=${ang ? `rotate(${ang} ${cx} ${cy})` : nothing} @pointerdown=${down}></ellipse>` : nothing}`;
}
if (sh.kind === 'furniture') {
// One path per piece, generated at the shape's REAL size, so the
// stroke is an ordinary stroke-width in render units like every other
// decor shape — no non-uniform scale to fight (docs/FURNITURE.md §2).
// One path per piece. Designer art keeps its native viewBox and is
// scaled to the user's stored box; vector-effect keeps the configured
// decor stroke physical under non-uniform resizing.
// An unknown symbol renders as nothing: a plan from a newer card must
// open in an older one, not break it.
const W2 = sh.w * W, H2 = sh.h * H;
const d = furniturePathD(sh.symbol, W2, H2);
if (!d) return nothing;
const art = furnitureGraphic(sh.symbol);
if (!art) return nothing;
const ang = Number(sh.angle) || 0;
const cx = sh.x * W + W2 / 2, cy = sh.y * H + H2 / 2;
const tr = `${ang ? `rotate(${ang} ${cx} ${cy}) ` : ''}translate(${sh.x * W} ${sh.y * H})`;
const tr = `${ang ? `rotate(${ang} ${cx} ${cy}) ` : ''}translate(${sh.x * W} ${sh.y * H}) scale(${W2 / art.viewW} ${H2 / art.viewH})`;
return svg`<path class="${cls} dfurn" data-hp="decor" data-id="${sh.id}"
data-kind="${sh.kind}" data-symbol="${sh.symbol}" d="${d}" transform=${tr}
data-kind="${sh.kind}" data-symbol="${sh.symbol}" d="${art.d}" transform=${tr}
stroke="${style.color}" stroke-opacity="${style.opacity}" stroke-width="${strokeWidth}" fill="none"
stroke-linecap="round" stroke-linejoin="round"
stroke-linecap="round" stroke-linejoin="round" vector-effect="non-scaling-stroke"
@pointerdown=${down} @dblclick=${dbl}></path>
${erasing ? svg`<path class="dshape derasehit" data-hp="decor" data-id="${sh.id}"
data-kind="${sh.kind}" data-symbol="${sh.symbol}" d="${d}" transform=${tr}
data-kind="${sh.kind}" data-symbol="${sh.symbol}" d="${art.d}" transform=${tr}
vector-effect="non-scaling-stroke"
@pointerdown=${down}></path>` : nothing}`;
}
if (sh.kind === 'text') {
+73 -22
View File
@@ -63,10 +63,11 @@ import {
} from './sun';
import { dayCycleStageVars, renderDayCycleEnvironment } from './day-cycle-render';
import {
FURNITURE_GROUPS, furnitureOfGroup, furnitureSymbol, furnitureDefaultCm,
furniturePathD, furnitureCorners, snapFurnitureToWall,
FURNITURE_GROUPS, furnitureOfGroup, furnitureDefaultCm,
furnitureGraphic, furnitureCorners, snapFurnitureToWall,
cmToNorm, clampFurnSize, clampFurnCm, FURN_WALL_CELLS, type FurnitureGroup,
} from './furniture';
import { GENERATED_FURNITURE_MENU } from './furniture-menu-art.generated';
import {
degradeWalls, rekeyWallsAfterMoveChecked, wallRecordCarrierViolations,
setWallThickness, setWallThicknessForRoom, cmToField, wallCmToUnits,
@@ -915,6 +916,7 @@ export interface HouseplanEditorHostPort {
_freeAreas: any[];
_fullRegistryHass: any;
_furnPalette: { symbol: string; w: number; h: number; } | null;
_furnCategory: string | null;
_furnWallReach: number;
_furnWalls: number[][];
_gearPtCache: WeakMap<number[][], number[]>;
@@ -4661,6 +4663,7 @@ public _furnPlace(raw: number[], free = false): void {
// …and the editor goes back to the tool that can move what was just placed
this.host._decorTool = 'select';
this.host._furnPalette = null;
this.host._furnCategory = null;
this._recordGeometry(this.host._t('history.decor_add'), before);
this._saveConfig();
this.host.requestUpdate();
@@ -4726,38 +4729,74 @@ public _decorApplyBox(id: string, box: { x: number; y: number; w: number; h: num
public _renderFurnPalette(): TemplateResult {
const pal = this.host._furnPalette;
const categoryId = this.host._furnCategory;
const allSymbols = FURNITURE_GROUPS.flatMap((group) => furnitureOfGroup(group));
const categories = GENERATED_FURNITURE_MENU.filter((item) =>
allSymbols.some((symbol) => symbol.category === item.id));
const category = categories.find((item) => item.id === categoryId) || null;
const variants = category
? allSymbols.filter((symbol) => symbol.category === category.id)
: [];
// the unit the fields are read in — the same two words the glow radius
// already uses, because a plan has one unit system, not one per control
const unit = this.host._t(this.host._imperial ? 'gs.unit_ft' : 'gs.unit_m');
const preview = (id: string): TemplateResult => {
const s = furnitureSymbol(id)!;
// fit the symbol into a 40×40 box keeping its real proportions, so a
// sofa reads as a sofa and a toilet does not become a square
const k = 36 / Math.max(s.w, s.h);
const w = s.w * k, h = s.h * k;
return svg`<svg class="furnprev" viewBox="0 0 40 40" aria-hidden="true"><g
transform="translate(${(40 - w) / 2} ${(40 - h) / 2})"><path
d=${furniturePathD(id, w, h)} fill="none" stroke="currentColor"
stroke-width="1.2" stroke-linejoin="round"></path></g></svg>` as unknown as TemplateResult;
const art = furnitureGraphic(id);
if (!art) return svg`<svg class="furnprev" aria-hidden="true"></svg>` as unknown as TemplateResult;
return svg`<svg class="furnprev" viewBox=${`0 0 ${art.viewW} ${art.viewH}`}
preserveAspectRatio="xMidYMid meet" aria-hidden="true"><path
d=${art.d} fill="none" stroke="currentColor" stroke-width="1.2"
vector-effect="non-scaling-stroke" stroke-linecap="round" stroke-linejoin="round"></path></svg>` as unknown as TemplateResult;
};
const categoryPreview = (item: (typeof GENERATED_FURNITURE_MENU)[number]): TemplateResult => {
return svg`<svg class="furnprev furncatprev" viewBox=${`0 0 ${item.art.viewW} ${item.art.viewH}`}
preserveAspectRatio="xMidYMid meet" aria-hidden="true"><path
d=${item.art.d} fill="none" stroke="currentColor" stroke-width="1.5"
vector-effect="non-scaling-stroke" stroke-linecap="round" stroke-linejoin="round"></path></svg>` as unknown as TemplateResult;
};
return html`<div class="furnpalette" @pointerdown=${(e: Event) => e.stopPropagation()}>
<div class="furnhd">
<ha-icon icon="mdi:sofa-outline"></ha-icon>${this.host._t('furn.title')}
<span class="spacer"></span>
<button class="btn furnclose" title=${this.host._t('btn.close')}
@click=${() => { this.host._furnPalette = null; this.host._decorTool = 'select'; }}>
@click=${() => {
this.host._furnPalette = null;
this.host._furnCategory = null;
this.host._decorTool = 'select';
}}>
<ha-icon icon="mdi:close"></ha-icon>
</button>
</div>
<div class="furnbody">
${FURNITURE_GROUPS.map((g) => html`
${category ? html`
<button class="btn ghost furnback" @click=${() => {
this.host._furnPalette = null;
this.host._furnCategory = null;
}}>
<ha-icon icon="mdi:arrow-left"></ha-icon>${this.host._t('furn.back_to_categories')}
</button>
<div class="furngroup" data-category=${category.id}>
${this.host._t(`furn.cat_${category.id}` as I18nKey)}
</div>
<div class="furnrow furnvariants">
${variants.map((symbol) => html`<button
class="furnitem ${pal?.symbol === symbol.id ? 'on' : ''}" data-symbol=${symbol.id}
title=${this.host._t(`furn.sym_${symbol.id}` as any)}
@click=${() => this._furnPick(symbol.id)}>
${preview(symbol.id)}<span>${this.host._t(`furn.sym_${symbol.id}` as any)}</span>
</button>`)}
</div>
` : FURNITURE_GROUPS.map((g) => html`
<div class="furngroup" data-group=${g}>${this.host._t(`furn.group_${g}` as any)}</div>
<div class="furnrow">
${furnitureOfGroup(g as FurnitureGroup).map((s) => html`<button
class="furnitem ${pal?.symbol === s.id ? 'on' : ''}" data-symbol=${s.id}
title=${this.host._t(`furn.sym_${s.id}` as any)}
@click=${() => this._furnPick(s.id)}>
${preview(s.id)}<span>${this.host._t(`furn.sym_${s.id}` as any)}</span>
<div class="furnrow furncategories">
${categories.filter((item) => item.group === g).map((item) => html`<button
class="furnitem furncategory" data-category=${item.id}
title=${this.host._t(`furn.cat_${item.id}` as I18nKey)}
@click=${() => {
this.host._furnPalette = null;
this.host._furnCategory = item.id;
}}>
${categoryPreview(item)}<span>${this.host._t(`furn.cat_${item.id}` as I18nKey)}</span>
</button>`)}
</div>`)}
</div>
@@ -5074,6 +5113,7 @@ public _renderDecorSecondary(): EditorSecondaryModel | null {
dismiss: () => {
if (this.host._decorTool !== 'furniture') return;
this.host._furnPalette = null;
this.host._furnCategory = null;
this.host._decorTool = 'select';
this.host.requestUpdate();
},
@@ -5195,6 +5235,7 @@ public _renderDecorBar(): TemplateResult {
@click=${() => {
if (t === 'furniture' && this.host._decorTool === 'furniture') {
this.host._furnPalette = null;
this.host._furnCategory = null;
this.host._decorTool = 'select';
return;
}
@@ -5203,7 +5244,12 @@ public _renderDecorBar(): TemplateResult {
this.host._decorDraft = null;
// the palette belongs to its tool and to nothing else: leaving the
// tool disarms whatever was chosen, so no later click can stamp it
if (t !== 'furniture') this.host._furnPalette = null;
if (t !== 'furniture') {
this.host._furnPalette = null;
this.host._furnCategory = null;
} else {
this.host._furnCategory = null;
}
}}
title=${this.host._t(k)}>
<ha-icon icon=${ic}></ha-icon><span class="ml">${this.host._t(k)}</span>
@@ -5335,8 +5381,13 @@ public _renderDecorShapeDialog(): TemplateResult {
@change=${(e: Event) => (this.host._decorShapeDialog = {
...d, symbol: (e.target as HTMLSelectElement).value,
})}>
${FURNITURE_GROUPS.map((group) => html`<optgroup label=${this.host._t(`furn.group_${group}` as any)}>
${furnitureOfGroup(group).map((symbol) => html`<option value=${symbol.id}
${GENERATED_FURNITURE_MENU.map((category) => ({
category,
symbols: furnitureOfGroup(category.group as FurnitureGroup)
.filter((symbol) => symbol.category === category.id),
})).filter((entry) => entry.symbols.length).map(({ category, symbols }) => html`
<optgroup label=${`${this.host._t(`furn.group_${category.group}` as I18nKey)} · ${this.host._t(`furn.cat_${category.id}` as I18nKey)}`}>
${symbols.map((symbol) => html`<option value=${symbol.id}
?selected=${symbol.id === d.symbol}>
${this.host._t(`furn.sym_${symbol.id}` as any)}
</option>`)}
+69 -13
View File
@@ -900,34 +900,90 @@
"furn.group_other": "Sonstiges",
"furn.width": "Breite",
"furn.depth": "Tiefe",
"furn.pick_hint": "Wählen Sie ein Symbol aus und klicken Sie dann auf den Plan.",
"furn.back_to_categories": "Alle Kategorien",
"furn.pick_hint": "Wählen Sie eine Kategorie und dann ein Symbol.",
"furn.place_hint": "Klicken Sie auf den Plan — das Element wird an der nächsten Wand platziert. Mit Shift wird es frei platziert.",
"furn.sym_sofa": "Sofa",
"furn.sym_armchair": "Sessel",
"furn.sym_coffee_table": "Couchtisch",
"furn.sym_table_dining": "Esstisch",
"furn.cat_air_conditioner": "Klimaanlagen",
"furn.cat_armchair": "Sessel",
"furn.cat_bathtub": "Badewannen",
"furn.cat_bed": "Betten",
"furn.cat_bidet": "Bidets",
"furn.cat_boiler": "Warmwasserbereiter",
"furn.cat_chair": "Stühle",
"furn.cat_coffee_table": "Couchtische",
"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_kitchen_cabinet": "Küchenschränke",
"furn.cat_kitchen_sink": "Küchenspülen",
"furn.cat_nightstand": "Schränke",
"furn.cat_plant": "Pflanzen",
"furn.cat_rug": "Teppiche",
"furn.cat_shelving": "Regale",
"furn.cat_shower": "Duschen",
"furn.cat_sink": "Waschbecken",
"furn.cat_sofa": "Sofas",
"furn.cat_stairs": "Treppen",
"furn.cat_toilet": "Toiletten",
"furn.cat_tv": "Fernseher",
"furn.cat_wardrobe": "Kleiderschränke",
"furn.cat_washer": "Waschmaschinen",
"furn.cat_work_table": "Schreibtische",
"furn.sym_sofa": "Zweisitzer-Sofa",
"furn.sym_sofa_three_seat": "Dreisitzer-Sofa",
"furn.sym_sofa_corner_right": "Ecksofa rechts",
"furn.sym_armchair": "Polstersessel",
"furn.sym_armchair_office": "Bürostuhl",
"furn.sym_coffee_table": "Rechteckiger Couchtisch",
"furn.sym_coffee_table_round": "Runder Couchtisch",
"furn.sym_coffee_table_oval": "Ovaler Couchtisch",
"furn.sym_coffee_table_rounded": "Abgerundeter Couchtisch",
"furn.sym_table_dining": "Rechteckiger Esstisch",
"furn.sym_table_round": "Rundtisch",
"furn.sym_table_dining_oval": "Ovaler Esstisch",
"furn.sym_table_dining_rounded": "Abgerundeter Esstisch",
"furn.sym_chair": "Stuhl",
"furn.sym_desk": "Schreibtisch",
"furn.sym_chair_bar": "Barhocker",
"furn.sym_desk": "Rechteckiger Schreibtisch",
"furn.sym_desk_corner": "Eckschreibtisch",
"furn.sym_bed_double": "Doppelbett",
"furn.sym_bed_single": "Einzelbett",
"furn.sym_nightstand": "Nachttisch",
"furn.sym_cabinet_tv": "TV-Schrank",
"furn.sym_cabinet_shoe": "Schuhschrank",
"furn.sym_cabinet_sink": "Waschtischunterschrank",
"furn.sym_wardrobe": "Kleiderschrank",
"furn.sym_bookshelf": "Bücherregal",
"furn.sym_bookshelf": "Bücherschrank",
"furn.sym_wall_unit": "Schrankwand",
"furn.sym_kitchen_floor": "Küchen-Unterschrank",
"furn.sym_kitchen_floor_corner": "Küchen-Eckunterschrank",
"furn.sym_kitchen_wall": "Küchen-Hängeschrank",
"furn.sym_kitchen_wall_corner": "Küchen-Eckhängeschrank",
"furn.sym_shelf_floor": "Standregal",
"furn.sym_shelf_wall": "Wandregal",
"furn.sym_fridge": "Kühlschrank",
"furn.sym_stove": "Herd",
"furn.sym_stove": "Vierflammiges Kochfeld",
"furn.sym_cooktop_two": "Zweiflammiges Kochfeld",
"furn.sym_dishwasher": "Geschirrspüler",
"furn.sym_washer": "Waschmaschine",
"furn.sym_dryer": "Wäschetrockner",
"furn.sym_tv": "Fernseher",
"furn.sym_tv": "Fernseher mit Standfuß",
"furn.sym_tv_wall": "Wandfernseher",
"furn.sym_ac": "Klimaanlage",
"furn.sym_water_heater": "Wassererhitzer",
"furn.sym_toilet": "Toilette",
"furn.sym_bathtub": "Badewanne",
"furn.sym_toilet": "Stand-WC",
"furn.sym_toilet_built_in": "Wand-WC",
"furn.sym_bathtub": "Rechteckige Badewanne",
"furn.sym_bathtub_corner": "Eckbadewanne",
"furn.sym_shower": "Dusche",
"furn.sym_sink": "Waschbecken",
"furn.sym_kitchen_sink": "Küchenspüle",
"furn.sym_bidet": "Bidet",
"furn.sym_kitchen_sink": "Einzel-Küchenspüle",
"furn.sym_kitchen_sink_double": "Doppel-Küchenspüle",
"furn.sym_bidet": "Stand-Bidet",
"furn.sym_bidet_built_in": "Wand-Bidet",
"furn.sym_stairs": "Treppe",
"furn.sym_fireplace": "Kamin",
"furn.sym_plant": "Pflanze",
+70 -14
View File
@@ -900,34 +900,90 @@
"furn.group_other": "Other",
"furn.width": "Width",
"furn.depth": "Depth",
"furn.pick_hint": "Pick a symbol, then click on the plan.",
"furn.back_to_categories": "All categories",
"furn.pick_hint": "Choose a category, then a symbol.",
"furn.place_hint": "Click on the plan — the piece lands against the nearest wall. Shift places it free.",
"furn.sym_sofa": "Sofa",
"furn.sym_armchair": "Armchair",
"furn.sym_coffee_table": "Coffee table",
"furn.sym_table_dining": "Dining table",
"furn.cat_air_conditioner": "Air conditioners",
"furn.cat_armchair": "Armchairs",
"furn.cat_bathtub": "Bathtubs",
"furn.cat_bed": "Beds",
"furn.cat_bidet": "Bidets",
"furn.cat_boiler": "Water heaters",
"furn.cat_chair": "Chairs",
"furn.cat_coffee_table": "Coffee tables",
"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_kitchen_cabinet": "Kitchen cabinets",
"furn.cat_kitchen_sink": "Kitchen sinks",
"furn.cat_nightstand": "Cabinets",
"furn.cat_plant": "Plants",
"furn.cat_rug": "Rugs",
"furn.cat_shelving": "Shelving",
"furn.cat_shower": "Showers",
"furn.cat_sink": "Sinks",
"furn.cat_sofa": "Sofas",
"furn.cat_stairs": "Stairs",
"furn.cat_toilet": "Toilets",
"furn.cat_tv": "Televisions",
"furn.cat_wardrobe": "Wardrobes",
"furn.cat_washer": "Washing machines",
"furn.cat_work_table": "Desks",
"furn.sym_sofa": "Two-seat sofa",
"furn.sym_sofa_three_seat": "Three-seat sofa",
"furn.sym_sofa_corner_right": "Right sectional sofa",
"furn.sym_armchair": "Soft armchair",
"furn.sym_armchair_office": "Office chair",
"furn.sym_coffee_table": "Rectangular coffee table",
"furn.sym_coffee_table_round": "Round coffee table",
"furn.sym_coffee_table_oval": "Oval coffee table",
"furn.sym_coffee_table_rounded": "Rounded coffee table",
"furn.sym_table_dining": "Rectangular dining table",
"furn.sym_table_round": "Round table",
"furn.sym_table_dining_oval": "Oval dining table",
"furn.sym_table_dining_rounded": "Rounded dining table",
"furn.sym_chair": "Chair",
"furn.sym_desk": "Desk",
"furn.sym_chair_bar": "Bar stool",
"furn.sym_desk": "Rectangular desk",
"furn.sym_desk_corner": "Corner desk",
"furn.sym_bed_double": "Double bed",
"furn.sym_bed_single": "Single bed",
"furn.sym_nightstand": "Nightstand",
"furn.sym_nightstand": "Bedside table",
"furn.sym_cabinet_tv": "TV cabinet",
"furn.sym_cabinet_shoe": "Shoe cabinet",
"furn.sym_cabinet_sink": "Sink cabinet",
"furn.sym_wardrobe": "Wardrobe",
"furn.sym_bookshelf": "Bookshelf",
"furn.sym_bookshelf": "Bookcase",
"furn.sym_wall_unit": "Wall unit",
"furn.sym_kitchen_floor": "Kitchen floor module",
"furn.sym_kitchen_floor_corner": "Kitchen floor corner module",
"furn.sym_kitchen_wall": "Kitchen wall module",
"furn.sym_kitchen_wall_corner": "Kitchen wall corner module",
"furn.sym_shelf_floor": "Floor shelving unit",
"furn.sym_shelf_wall": "Wall shelf",
"furn.sym_fridge": "Fridge",
"furn.sym_stove": "Cooker",
"furn.sym_stove": "Four-burner cooktop",
"furn.sym_cooktop_two": "Two-burner cooktop",
"furn.sym_dishwasher": "Dishwasher",
"furn.sym_washer": "Washing machine",
"furn.sym_dryer": "Tumble dryer",
"furn.sym_tv": "TV",
"furn.sym_tv": "TV on stand",
"furn.sym_tv_wall": "Wall-mounted TV",
"furn.sym_ac": "Air conditioner",
"furn.sym_water_heater": "Water heater",
"furn.sym_toilet": "Toilet",
"furn.sym_bathtub": "Bathtub",
"furn.sym_toilet": "Floor-standing toilet",
"furn.sym_toilet_built_in": "Built-in toilet",
"furn.sym_bathtub": "Rectangular bathtub",
"furn.sym_bathtub_corner": "Corner bathtub",
"furn.sym_shower": "Shower",
"furn.sym_sink": "Washbasin",
"furn.sym_kitchen_sink": "Kitchen sink",
"furn.sym_bidet": "Bidet",
"furn.sym_kitchen_sink": "Single kitchen sink",
"furn.sym_kitchen_sink_double": "Double kitchen sink",
"furn.sym_bidet": "Floor-standing bidet",
"furn.sym_bidet_built_in": "Built-in bidet",
"furn.sym_stairs": "Stairs",
"furn.sym_fireplace": "Fireplace",
"furn.sym_plant": "Plant",
+71 -15
View File
@@ -900,34 +900,90 @@
"furn.group_other": "Прочее",
"furn.width": "Ширина",
"furn.depth": "Глубина",
"furn.pick_hint": "Выберите символ и кликните по плану.",
"furn.back_to_categories": "Все категории",
"furn.pick_hint": "Выберите категорию, затем символ.",
"furn.place_hint": "Кликните по плану — предмет встанет к ближайшей стене. Shift — свободно.",
"furn.sym_sofa": "Диван",
"furn.sym_armchair": "Кресло",
"furn.sym_coffee_table": "Журнальный столик",
"furn.sym_table_dining": "Обеденный стол",
"furn.cat_air_conditioner": "Кондиционеры",
"furn.cat_armchair": "Кресла",
"furn.cat_bathtub": "Ванны",
"furn.cat_bed": "Кровати",
"furn.cat_bidet": "Биде",
"furn.cat_boiler": "Бойлеры",
"furn.cat_chair": "Стулья",
"furn.cat_coffee_table": "Журнальные столы",
"furn.cat_cooktop": "Варочные панели",
"furn.cat_dining_table": "Обеденные столы",
"furn.cat_dishwasher": "Посудомоечные машины",
"furn.cat_dryer": "Сушильные машины",
"furn.cat_fireplace": "Камины",
"furn.cat_fridge": "Холодильники",
"furn.cat_kitchen_cabinet": "Кухонные шкафы",
"furn.cat_kitchen_sink": "Кухонные мойки",
"furn.cat_nightstand": "Тумбы",
"furn.cat_plant": "Растения",
"furn.cat_rug": "Ковры",
"furn.cat_shelving": "Стеллажи",
"furn.cat_shower": "Душевые",
"furn.cat_sink": "Раковины",
"furn.cat_sofa": "Диваны",
"furn.cat_stairs": "Лестницы",
"furn.cat_toilet": "Унитазы",
"furn.cat_tv": "Телевизоры",
"furn.cat_wardrobe": "Шкафы",
"furn.cat_washer": "Стиральные машины",
"furn.cat_work_table": "Рабочие столы",
"furn.sym_sofa": "Диван двухместный",
"furn.sym_sofa_three_seat": "Диван трёхместный",
"furn.sym_sofa_corner_right": "Угловой диван, правый",
"furn.sym_armchair": "Кресло мягкое",
"furn.sym_armchair_office": "Кресло офисное",
"furn.sym_coffee_table": "Журнальный стол, прямоугольный",
"furn.sym_coffee_table_round": "Журнальный стол, круглый",
"furn.sym_coffee_table_oval": "Журнальный стол, овальный",
"furn.sym_coffee_table_rounded": "Журнальный стол, скруглённый",
"furn.sym_table_dining": "Обеденный стол, прямоугольный",
"furn.sym_table_round": "Круглый стол",
"furn.sym_table_dining_oval": "Обеденный стол, овальный",
"furn.sym_table_dining_rounded": "Обеденный стол, скруглённый",
"furn.sym_chair": "Стул",
"furn.sym_desk": "Письменный стол",
"furn.sym_chair_bar": "Барный стул",
"furn.sym_desk": "Рабочий стол, прямоугольный",
"furn.sym_desk_corner": "Рабочий стол, угловой",
"furn.sym_bed_double": "Двуспальная кровать",
"furn.sym_bed_single": "Односпальная кровать",
"furn.sym_nightstand": "Тумбочка",
"furn.sym_wardrobe": "Шкаф",
"furn.sym_bookshelf": "Стеллаж",
"furn.sym_nightstand": "Тумба прикроватная",
"furn.sym_cabinet_tv": "Тумба под телевизор",
"furn.sym_cabinet_shoe": "Тумба для обуви",
"furn.sym_cabinet_sink": "Тумба под раковину",
"furn.sym_wardrobe": "Шкаф гардеробный",
"furn.sym_bookshelf": "Шкаф книжный",
"furn.sym_wall_unit": "Шкаф-стенка",
"furn.sym_kitchen_floor": "Кухонный напольный модуль",
"furn.sym_kitchen_floor_corner": "Кухонный напольный угловой модуль",
"furn.sym_kitchen_wall": "Кухонный навесной модуль",
"furn.sym_kitchen_wall_corner": "Кухонный навесной угловой модуль",
"furn.sym_shelf_floor": "Стеллаж напольный",
"furn.sym_shelf_wall": "Полка настенная",
"furn.sym_fridge": "Холодильник",
"furn.sym_stove": "Плита",
"furn.sym_stove": "Варочная панель, 4 конфорки",
"furn.sym_cooktop_two": "Варочная панель, 2 конфорки",
"furn.sym_dishwasher": "Посудомоечная машина",
"furn.sym_washer": "Стиральная машина",
"furn.sym_dryer": "Сушильная машина",
"furn.sym_tv": "Телевизор",
"furn.sym_tv": "Телевизор на подставке",
"furn.sym_tv_wall": "Телевизор на кронштейне",
"furn.sym_ac": "Кондиционер",
"furn.sym_water_heater": "Бойлер",
"furn.sym_toilet": "Унитаз",
"furn.sym_bathtub": "Ванна",
"furn.sym_toilet": "Унитаз напольный",
"furn.sym_toilet_built_in": "Унитаз встроенный",
"furn.sym_bathtub": "Ванна прямоугольная",
"furn.sym_bathtub_corner": "Ванна угловая",
"furn.sym_shower": "Душ",
"furn.sym_sink": "Раковина",
"furn.sym_kitchen_sink": "Кухонная мойка",
"furn.sym_bidet": "Биде",
"furn.sym_kitchen_sink": "Кухонная мойка одинарная",
"furn.sym_kitchen_sink_double": "Кухонная мойка двойная",
"furn.sym_bidet": "Биде напольное",
"furn.sym_bidet_built_in": "Биде встроенное",
"furn.sym_stairs": "Лестница",
"furn.sym_fireplace": "Камин",
"furn.sym_plant": "Растение",
+8
View File
@@ -695,6 +695,11 @@ export const dialogsStyles = css`
flex-wrap: wrap;
gap: 4px;
}
.furnback {
min-height: 34px;
gap: 5px;
margin: 2px 0 4px;
}
.furnitem {
display: flex;
flex-direction: column;
@@ -722,6 +727,9 @@ export const dialogsStyles = css`
height: 40px;
color: var(--primary-text-color, currentColor);
}
.furncategory { width: 92px; min-height: 76px; }
.furncatprev { width: 48px; height: 48px; }
.furnvariants .furnitem { min-height: 74px; }
.furnsize {
display: flex;
align-items: center;