mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 08:28:31 +00:00
feat v1.33.0: background editor — visual decor layer
- third mode tab: draw lines/rects/ovals/text with grid snap, drag preview; select+move (Delete key), erase, color/width/fill controls; Esc ladder; text dialog with S/M/L sizes and dblclick re-edit - decor renders under rooms, visible in every mode, click-transparent outside its editor; stored in space.decor with backend schema (line/rect/ellipse/text variants), rev/optimistic locking as usual - smoke_decor.mjs (19 checks); smoke_editor_tabs updated to 3 tabs; TESTING/CHANGELOG/UX-MODES same-commit
This commit is contained in:
+308
-7
@@ -32,7 +32,7 @@ import './space-card';
|
||||
import { cardStyles } from './styles';
|
||||
import { langOf, t, type I18nKey } from './i18n';
|
||||
|
||||
const CARD_VERSION = '1.32.1';
|
||||
const CARD_VERSION = '1.33.0';
|
||||
const LS_KEY = 'houseplan_card_layout_v1';
|
||||
const LS_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering
|
||||
const LS_ZOOM = 'houseplan_card_zoom_v1';
|
||||
@@ -86,7 +86,14 @@ class HouseplanCard extends LitElement {
|
||||
/** Interaction mode (docs/UX-MODES.md): view = display only, plan = geometry
|
||||
* editing, devices = marker placement/config. Never persisted — every load
|
||||
* starts in view. */
|
||||
private _mode: 'view' | 'plan' | 'devices' = 'view';
|
||||
private _mode: 'view' | 'plan' | 'devices' | 'decor' = 'view';
|
||||
// ---- decor (background) editor ----
|
||||
private _decorTool: 'select' | 'line' | 'rect' | 'ellipse' | 'text' | 'erase' = 'select';
|
||||
private _decorStyle: { color: string; width: number; fill: boolean } = { color: '#607d8b', width: 3, fill: false };
|
||||
private _decorDraft: { kind: 'line' | 'rect' | 'ellipse'; a: number[]; b: number[]; pid: number } | null = null;
|
||||
private _decorMove: { id: string; start: number[]; orig: any; pid: number; moved: boolean } | null = null;
|
||||
private _decorSel: string | null = null;
|
||||
private _decorTextDialog: { id?: string; x: number; y: number; text: string; size: 's' | 'm' | 'l'; color: string } | null = null;
|
||||
|
||||
/** Edit tabs are offered to admins only (hass.user missing → assume admin). */
|
||||
private get _canEdit(): boolean {
|
||||
@@ -224,6 +231,11 @@ class HouseplanCard extends LitElement {
|
||||
_openingInfo: { state: true },
|
||||
_mergeDialog: { state: true },
|
||||
_splitSel: { state: true },
|
||||
_decorTool: { state: true },
|
||||
_decorStyle: { state: true },
|
||||
_decorDraft: { state: true },
|
||||
_decorSel: { state: true },
|
||||
_decorTextDialog: { state: true },
|
||||
_areaSel: { state: true },
|
||||
_nameSel: { state: true },
|
||||
_roomDialog: { state: true },
|
||||
@@ -265,6 +277,7 @@ class HouseplanCard extends LitElement {
|
||||
if (this._settingsDialog) { this._settingsDialog = null; return; }
|
||||
if (this._markerDialog) { this._markerDialog = null; return; }
|
||||
if (this._openingDialog) { this._openingDialog = null; return; }
|
||||
if (this._decorTextDialog) { this._decorTextDialog = null; return; }
|
||||
if (this._spaceDialog && !this._roomDialog) {
|
||||
// same semantics as the dialog's Cancel: an import queue is abandoned
|
||||
this._spaceDialog = null;
|
||||
@@ -273,6 +286,22 @@ class HouseplanCard extends LitElement {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (this._mode === 'decor') {
|
||||
if ((e.key === 'Delete' || e.key === 'Backspace') && this._decorSel &&
|
||||
!(e.target as HTMLElement)?.closest?.('input, textarea, select')) {
|
||||
e.preventDefault();
|
||||
this._decorDeleteSel();
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
if (this._decorDraft) this._decorDraft = null;
|
||||
else if (this._decorSel) this._decorSel = null;
|
||||
else if (this._decorTool !== 'select') this._decorTool = 'select';
|
||||
else this._setMode('view');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!this._markup) return;
|
||||
const undo = e.key === 'Escape' || ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'z');
|
||||
if (!undo) return;
|
||||
@@ -928,6 +957,7 @@ class HouseplanCard extends LitElement {
|
||||
// do not interfere with icon dragging and markup drawing
|
||||
if (this._drag || this._markup) return;
|
||||
if (this._mode === 'devices' && (ev.target as HTMLElement).closest('.dev')) return;
|
||||
if (this._mode === 'decor' && this._decorPointerDown(ev)) return;
|
||||
this._pointers.set(ev.pointerId, { x: ev.clientX, y: ev.clientY });
|
||||
const v = this._viewOr(this._spaceModel().vb);
|
||||
if (this._pointers.size === 1) {
|
||||
@@ -942,6 +972,14 @@ class HouseplanCard extends LitElement {
|
||||
}
|
||||
|
||||
private _stagePointerMove(ev: PointerEvent): void {
|
||||
if (this._decorDraft?.pid === ev.pointerId) {
|
||||
this._decorDraft = { ...this._decorDraft, b: this._snap(this._svgPoint(ev)) };
|
||||
return;
|
||||
}
|
||||
if (this._decorMove?.pid === ev.pointerId) {
|
||||
this._decorMoveUpdate(ev);
|
||||
return;
|
||||
}
|
||||
if (!this._pointers.has(ev.pointerId)) {
|
||||
this._markupMove(ev);
|
||||
return;
|
||||
@@ -982,6 +1020,15 @@ class HouseplanCard extends LitElement {
|
||||
}
|
||||
|
||||
private _stagePointerUp(ev: PointerEvent): void {
|
||||
if (this._decorDraft?.pid === ev.pointerId) {
|
||||
this._decorCommitDraft();
|
||||
return;
|
||||
}
|
||||
if (this._decorMove?.pid === ev.pointerId) {
|
||||
if (this._decorMove.moved) this._saveConfig();
|
||||
this._decorMove = null;
|
||||
return;
|
||||
}
|
||||
this._pointers.delete(ev.pointerId);
|
||||
if (this._pointers.size < 2) this._pinchStart = null;
|
||||
if (this._pointers.size === 0) {
|
||||
@@ -1103,9 +1150,9 @@ class HouseplanCard extends LitElement {
|
||||
return roomEdges(sp?.rooms || []).map((s) => [s[0] * NORM_W, s[1] * H, s[2] * NORM_W, s[3] * H]);
|
||||
}
|
||||
|
||||
private _setMode(mode: 'view' | 'plan' | 'devices'): void {
|
||||
private _setMode(mode: 'view' | 'plan' | 'devices' | 'decor'): void {
|
||||
if (this._mode === mode) return;
|
||||
if (mode === 'plan' && !this._norm) {
|
||||
if ((mode === 'plan' || mode === 'decor') && !this._norm) {
|
||||
this._showToast(this._t('toast.markup_needs_server'));
|
||||
return;
|
||||
}
|
||||
@@ -1119,6 +1166,9 @@ class HouseplanCard extends LitElement {
|
||||
this._pendingSplit = null;
|
||||
this._selId = null;
|
||||
this._tip = null;
|
||||
this._decorDraft = null;
|
||||
this._decorSel = null;
|
||||
this._decorTool = 'select';
|
||||
}
|
||||
|
||||
|
||||
@@ -1275,6 +1325,255 @@ class HouseplanCard extends LitElement {
|
||||
return (cm / this._cellCm) * this._gridPitch;
|
||||
}
|
||||
|
||||
// ================= decor (background) layer =================
|
||||
|
||||
private get _decorList(): any[] {
|
||||
const sp = this._curSpaceCfg;
|
||||
return Array.isArray(sp?.decor) ? sp.decor : [];
|
||||
}
|
||||
|
||||
private get _decorH(): number {
|
||||
return NORM_W / (this._curSpaceCfg?.aspect || 1);
|
||||
}
|
||||
|
||||
/** Begin a decor gesture. Returns true when the event is consumed (no pan). */
|
||||
private _decorPointerDown(ev: PointerEvent): boolean {
|
||||
const t = this._decorTool;
|
||||
const onShape = (ev.target as HTMLElement).closest?.('.dshape') as SVGElement | null;
|
||||
if (onShape) return true; // the shape's own handler deals with it
|
||||
if (t === 'line' || t === 'rect' || t === 'ellipse') {
|
||||
ev.preventDefault();
|
||||
const p = this._snap(this._svgPoint(ev));
|
||||
this._decorDraft = { kind: t, a: p, b: p, pid: ev.pointerId };
|
||||
(ev.target as HTMLElement).setPointerCapture?.(ev.pointerId);
|
||||
return true;
|
||||
}
|
||||
if (t === 'text') {
|
||||
const p = this._snap(this._svgPoint(ev));
|
||||
this._decorTextDialog = {
|
||||
x: p[0] / NORM_W, y: p[1] / this._decorH,
|
||||
text: '', size: 'm', color: this._decorStyle.color,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
this._decorSel = null; // select/erase on empty space clears the selection
|
||||
return false; // pan is allowed
|
||||
}
|
||||
|
||||
/** Commit the dragged shape (ignore degenerate ones) and persist. */
|
||||
private _decorCommitDraft(): void {
|
||||
const d = this._decorDraft;
|
||||
this._decorDraft = null;
|
||||
if (!d) return;
|
||||
const min = this._gridPitch * 0.5;
|
||||
if (Math.hypot(d.b[0] - d.a[0], d.b[1] - d.a[1]) < min) return;
|
||||
const W = NORM_W, H = this._decorH;
|
||||
const st = this._decorStyle;
|
||||
const id = 'dc' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
|
||||
let shape: any;
|
||||
if (d.kind === 'line') {
|
||||
shape = { id, kind: 'line', x1: d.a[0] / W, y1: d.a[1] / H, x2: d.b[0] / W, y2: d.b[1] / H,
|
||||
color: st.color, width: st.width };
|
||||
} else {
|
||||
const x = Math.min(d.a[0], d.b[0]) / W, y = Math.min(d.a[1], d.b[1]) / H;
|
||||
const w = Math.abs(d.b[0] - d.a[0]) / W, h = Math.abs(d.b[1] - d.a[1]) / H;
|
||||
shape = { id, kind: d.kind, x, y, w, h, color: st.color, width: st.width, fill: st.fill };
|
||||
}
|
||||
const sp = this._curSpaceCfg;
|
||||
sp.decor = [...this._decorList, shape];
|
||||
this._decorSel = id;
|
||||
this._saveConfig();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
/** Select tool: pointerdown on a shape starts moving it. */
|
||||
private _decorShapeDown(ev: PointerEvent, shape: any): void {
|
||||
if (this._mode !== 'decor') return;
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
if (this._decorTool === 'erase') {
|
||||
const sp = this._curSpaceCfg;
|
||||
sp.decor = this._decorList.filter((x) => x.id !== shape.id);
|
||||
if (this._decorSel === shape.id) this._decorSel = null;
|
||||
this._saveConfig();
|
||||
this.requestUpdate();
|
||||
return;
|
||||
}
|
||||
if (this._decorTool !== 'select') return;
|
||||
this._decorSel = shape.id;
|
||||
this._decorMove = {
|
||||
id: shape.id, start: this._svgPoint(ev), orig: JSON.parse(JSON.stringify(shape)),
|
||||
pid: ev.pointerId, moved: false,
|
||||
};
|
||||
(ev.target as HTMLElement).setPointerCapture?.(ev.pointerId);
|
||||
}
|
||||
|
||||
private _decorMoveUpdate(ev: PointerEvent): void {
|
||||
const m = this._decorMove!;
|
||||
const p = this._svgPoint(ev);
|
||||
const g = this._gridPitch;
|
||||
const dx = snapToGrid(p[0] - m.start[0], g) / NORM_W;
|
||||
const dy = snapToGrid(p[1] - m.start[1], g) / this._decorH;
|
||||
if (dx || dy) m.moved = true;
|
||||
const sp = this._curSpaceCfg;
|
||||
sp.decor = this._decorList.map((x) => {
|
||||
if (x.id !== m.id) return x;
|
||||
const o = m.orig;
|
||||
if (x.kind === 'line') return { ...x, x1: o.x1 + dx, y1: o.y1 + dy, x2: o.x2 + dx, y2: o.y2 + dy };
|
||||
return { ...x, x: o.x + dx, y: o.y + dy };
|
||||
});
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
/** Double click on a text shape (select tool) re-opens its dialog. */
|
||||
private _decorShapeDbl(shape: any): void {
|
||||
if (this._mode !== 'decor' || shape.kind !== 'text') return;
|
||||
this._decorTextDialog = { id: shape.id, x: shape.x, y: shape.y,
|
||||
text: shape.text, size: shape.size || 'm', color: shape.color };
|
||||
}
|
||||
|
||||
private _decorSaveText(): void {
|
||||
const d = this._decorTextDialog;
|
||||
if (!d || !d.text.trim()) { this._decorTextDialog = null; return; }
|
||||
const sp = this._curSpaceCfg;
|
||||
if (d.id) {
|
||||
sp.decor = this._decorList.map((x) => x.id === d.id
|
||||
? { ...x, text: d.text.trim(), size: d.size, color: d.color } : x);
|
||||
} else {
|
||||
const id = 'dc' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
|
||||
sp.decor = [...this._decorList, { id, kind: 'text', x: d.x, y: d.y,
|
||||
text: d.text.trim(), size: d.size, color: d.color }];
|
||||
}
|
||||
this._decorTextDialog = null;
|
||||
this._saveConfig();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _decorDeleteSel(): void {
|
||||
if (!this._decorSel) return;
|
||||
const sp = this._curSpaceCfg;
|
||||
sp.decor = this._decorList.filter((x) => x.id !== this._decorSel);
|
||||
this._decorSel = null;
|
||||
this._saveConfig();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _renderDecorLayer(): TemplateResult {
|
||||
const W = NORM_W, H = this._decorH;
|
||||
const TXT = { s: 14, m: 20, l: 30 } as Record<string, number>;
|
||||
const editing = this._mode === 'decor';
|
||||
const shapes = this._decorList.map((sh) => {
|
||||
const cls = 'dshape' + (editing && this._decorSel === sh.id ? ' dsel' : '');
|
||||
const down = (e: PointerEvent) => this._decorShapeDown(e, sh);
|
||||
const dbl = () => this._decorShapeDbl(sh);
|
||||
if (sh.kind === 'line')
|
||||
return svg`<line class="${cls}" x1="${sh.x1 * W}" y1="${sh.y1 * H}" x2="${sh.x2 * W}" y2="${sh.y2 * H}"
|
||||
stroke="${sh.color}" stroke-width="${sh.width}" @pointerdown=${down}></line>`;
|
||||
if (sh.kind === 'rect')
|
||||
return svg`<rect class="${cls}" x="${sh.x * W}" y="${sh.y * H}" width="${sh.w * W}" height="${sh.h * H}"
|
||||
stroke="${sh.color}" stroke-width="${sh.width}"
|
||||
fill="${sh.fill ? sh.color : 'none'}" fill-opacity="${sh.fill ? 0.25 : 0}" @pointerdown=${down}></rect>`;
|
||||
if (sh.kind === 'ellipse')
|
||||
return svg`<ellipse class="${cls}" cx="${(sh.x + sh.w / 2) * W}" cy="${(sh.y + sh.h / 2) * H}"
|
||||
rx="${(sh.w / 2) * W}" ry="${(sh.h / 2) * H}" stroke="${sh.color}" stroke-width="${sh.width}"
|
||||
fill="${sh.fill ? sh.color : 'none'}" fill-opacity="${sh.fill ? 0.25 : 0}" @pointerdown=${down}></ellipse>`;
|
||||
if (sh.kind === 'text')
|
||||
return svg`<text class="${cls} dtext" x="${sh.x * W}" y="${sh.y * H}" fill="${sh.color}"
|
||||
font-size="${TXT[sh.size] || TXT.m}" @pointerdown=${down} @dblclick=${dbl}>${sh.text}</text>`;
|
||||
return nothing;
|
||||
});
|
||||
// живое превью рисуемой фигуры
|
||||
let draft: unknown = nothing;
|
||||
const d = this._decorDraft;
|
||||
if (d) {
|
||||
const st = this._decorStyle;
|
||||
if (d.kind === 'line')
|
||||
draft = svg`<line class="ddraft" x1="${d.a[0]}" y1="${d.a[1]}" x2="${d.b[0]}" y2="${d.b[1]}"
|
||||
stroke="${st.color}" stroke-width="${st.width}"></line>`;
|
||||
else {
|
||||
const x = Math.min(d.a[0], d.b[0]), y = Math.min(d.a[1], d.b[1]);
|
||||
const w = Math.abs(d.b[0] - d.a[0]), h = Math.abs(d.b[1] - d.a[1]);
|
||||
draft = d.kind === 'rect'
|
||||
? svg`<rect class="ddraft" x="${x}" y="${y}" width="${w}" height="${h}" stroke="${st.color}"
|
||||
stroke-width="${st.width}" fill="${st.fill ? st.color : 'none'}" fill-opacity="${st.fill ? 0.15 : 0}"></rect>`
|
||||
: svg`<ellipse class="ddraft" cx="${x + w / 2}" cy="${y + h / 2}" rx="${w / 2}" ry="${h / 2}"
|
||||
stroke="${st.color}" stroke-width="${st.width}" fill="${st.fill ? st.color : 'none'}" fill-opacity="${st.fill ? 0.15 : 0}"></ellipse>`;
|
||||
}
|
||||
}
|
||||
return svg`<g class="decorlayer">${shapes}${draft}</g>` as unknown as TemplateResult;
|
||||
}
|
||||
|
||||
private _renderDecorBar(): TemplateResult {
|
||||
const tools = [
|
||||
['select', 'mdi:cursor-default-outline', 'decor.select'],
|
||||
['line', 'mdi:vector-line', 'decor.line'],
|
||||
['rect', 'mdi:rectangle-outline', 'decor.rect'],
|
||||
['ellipse', 'mdi:ellipse-outline', 'decor.ellipse'],
|
||||
['text', 'mdi:format-text', 'decor.text'],
|
||||
['erase', 'mdi:eraser', 'decor.erase'],
|
||||
] as const;
|
||||
return html`<div class="editbar decorbar">
|
||||
<ha-icon icon="mdi:draw" class="warn"></ha-icon>
|
||||
${tools.map(
|
||||
([t, ic, k]) => html`<button class="btn ${this._decorTool === t ? 'on' : ''}"
|
||||
@click=${() => { this._decorTool = t; this._decorDraft = null; }}
|
||||
title=${this._t(k)}>
|
||||
<ha-icon icon=${ic}></ha-icon><span class="ml">${this._t(k)}</span>
|
||||
</button>`,
|
||||
)}
|
||||
<input type="color" class="dcolor" .value=${this._decorStyle.color}
|
||||
title=${this._t('decor.color')}
|
||||
@input=${(e: Event) => (this._decorStyle = { ...this._decorStyle, color: (e.target as HTMLInputElement).value })} />
|
||||
<select class="dwidth" title=${this._t('decor.width')}
|
||||
@change=${(e: Event) => (this._decorStyle = { ...this._decorStyle, width: Number((e.target as HTMLSelectElement).value) })}>
|
||||
${[[1.5, 'decor.w_thin'], [3, 'decor.w_mid'], [6, 'decor.w_thick']].map(
|
||||
([v, k]) => html`<option value=${v} ?selected=${this._decorStyle.width === v}>${this._t(k as any)}</option>`,
|
||||
)}
|
||||
</select>
|
||||
<label class="dfill"><input type="checkbox" .checked=${this._decorStyle.fill}
|
||||
@change=${(e: Event) => (this._decorStyle = { ...this._decorStyle, fill: (e.target as HTMLInputElement).checked })} />
|
||||
${this._t('decor.fill')}</label>
|
||||
<span class="spacer"></span>
|
||||
<button class="btn barclose" title=${this._t('title.close_editor')}
|
||||
@click=${() => this._setMode('view')}>
|
||||
<ha-icon icon="mdi:close"></ha-icon>
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderDecorTextDialog(): TemplateResult {
|
||||
const d = this._decorTextDialog!;
|
||||
return html`<div class="menuwrap dialogwrap" @click=${() => (this._decorTextDialog = null)}>
|
||||
<div class="dialog" @click=${(e: Event) => e.stopPropagation()}>
|
||||
<div class="hd"><ha-icon icon="mdi:format-text"></ha-icon>${this._t('decor.text_title')}</div>
|
||||
<div class="body">
|
||||
<label>${this._t('decor.text_label')}</label>
|
||||
<input class="namein" .value=${d.text} autofocus
|
||||
@input=${(e: Event) => (this._decorTextDialog = { ...d, text: (e.target as HTMLInputElement).value })}
|
||||
@keydown=${(e: KeyboardEvent) => { if (e.key === 'Enter') this._decorSaveText(); }} />
|
||||
<label>${this._t('decor.text_size')}</label>
|
||||
<div class="radiorow">
|
||||
${(['s', 'm', 'l'] as const).map(
|
||||
(sz) => html`<label class="srcrow inline">
|
||||
<input type="radio" name="dtsize" .checked=${d.size === sz}
|
||||
@change=${() => (this._decorTextDialog = { ...d, size: sz })} />
|
||||
<span>${this._t(('decor.size_' + sz) as any)}</span>
|
||||
</label>`,
|
||||
)}
|
||||
</div>
|
||||
<label>${this._t('decor.color')}</label>
|
||||
<input type="color" .value=${d.color}
|
||||
@input=${(e: Event) => (this._decorTextDialog = { ...d, color: (e.target as HTMLInputElement).value })} />
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="spacer"></span>
|
||||
<button class="btn ghost" @click=${() => (this._decorTextDialog = null)}>${this._t('btn.cancel')}</button>
|
||||
<button class="btn primary" ?disabled=${!d.text.trim()} @click=${() => this._decorSaveText()}>${this._t('btn.save')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/** Opening tool: click an existing opening to edit it, or a wall to place one. */
|
||||
private _openingClick(raw: number[]): void {
|
||||
const eps = this._gridPitch * 1.5;
|
||||
@@ -2515,7 +2814,7 @@ class HouseplanCard extends LitElement {
|
||||
</div>
|
||||
${this._canEdit
|
||||
? html`<div class="modes">
|
||||
${([['plan', 'mdi:floor-plan'], ['devices', 'mdi:tune-variant']] as const).map(
|
||||
${([['plan', 'mdi:floor-plan'], ['devices', 'mdi:tune-variant'], ['decor', 'mdi:draw']] as const).map(
|
||||
([m, ic]) => html`<button class="modetab ${this._mode === m ? 'active' : ''}"
|
||||
title=${this._t(('mode.' + m) as any)}
|
||||
@click=${() => { if (this._mode !== m) this._setMode(m); }}>
|
||||
@@ -2542,10 +2841,10 @@ class HouseplanCard extends LitElement {
|
||||
</button>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._markup ? this._renderMarkupBar() : this._mode === 'devices' ? this._renderDevicesBar() : nothing}
|
||||
${this._markup ? this._renderMarkupBar() : this._mode === 'devices' ? this._renderDevicesBar() : this._mode === 'decor' ? this._renderDecorBar() : nothing}
|
||||
</div>
|
||||
|
||||
<div class="stage ${this._markup ? 'markup tool-' + this._tool + (this._tool === 'split' && !this._splitSel ? ' pickstage' : '') : ''} ${space.bg ? '' : 'noplan'} mode-${this._mode}"
|
||||
<div class="stage ${this._markup ? 'markup tool-' + this._tool + (this._tool === 'split' && !this._splitSel ? ' pickstage' : '') : ''} ${this._mode === 'decor' ? 'dtool-' + this._decorTool : ''} ${space.bg ? '' : 'noplan'} mode-${this._mode}"
|
||||
style="height:calc(100dvh - 118px)"
|
||||
@click=${(e: MouseEvent) => this._markupClick(e)}
|
||||
@wheel=${(e: WheelEvent) => this._onWheel(e)}
|
||||
@@ -2559,6 +2858,7 @@ class HouseplanCard extends LitElement {
|
||||
${space.bg
|
||||
? svg`<image href="${space.bg.href}" x="${space.bg.x}" y="${space.bg.y}" width="${space.bg.w}" height="${space.bg.h}" preserveAspectRatio="none" />`
|
||||
: nothing}
|
||||
${this._renderDecorLayer()}
|
||||
${space.rooms.filter((r) => r.area || this._markup || disp.showBorders).map((r) => {
|
||||
let cls = 'room ' + (space.bg ? 'overlay' : 'yard') + (this._markup ? ' outlined' : '');
|
||||
if (this._markup && (r.id === this._mergeSel || r.id === this._splitSel?.roomId))
|
||||
@@ -2625,6 +2925,7 @@ class HouseplanCard extends LitElement {
|
||||
${this._mergeDialog ? this._renderMergeDialog() : nothing}
|
||||
${this._openingDialog ? this._renderOpeningDialog() : nothing}
|
||||
${this._openingInfo ? this._renderOpeningInfoCard() : nothing}
|
||||
${this._decorTextDialog ? this._renderDecorTextDialog() : nothing}
|
||||
${this._spaceDialog ? this._renderSpaceDialog() : nothing}
|
||||
${this._markerDialog ? this._renderMarkerDialog() : nothing}
|
||||
${this._infoCard ? this._renderInfoCard() : nothing}
|
||||
|
||||
+20
-1
@@ -261,5 +261,24 @@
|
||||
"roomcard.light_on": "On",
|
||||
"roomcard.light_off": "Off",
|
||||
"roomcard.light_partial": "{on} of {total}",
|
||||
"toast.split_pick_inside": "Intermediate cut points must be inside the room"
|
||||
"toast.split_pick_inside": "Intermediate cut points must be inside the room",
|
||||
"mode.decor": "Background editor",
|
||||
"decor.select": "Select",
|
||||
"decor.line": "Line",
|
||||
"decor.rect": "Rectangle",
|
||||
"decor.ellipse": "Oval",
|
||||
"decor.text": "Text",
|
||||
"decor.erase": "Erase",
|
||||
"decor.color": "Color",
|
||||
"decor.width": "Line width",
|
||||
"decor.w_thin": "Thin",
|
||||
"decor.w_mid": "Medium",
|
||||
"decor.w_thick": "Thick",
|
||||
"decor.fill": "Fill",
|
||||
"decor.text_title": "Text label",
|
||||
"decor.text_label": "Text",
|
||||
"decor.text_size": "Size",
|
||||
"decor.size_s": "Small",
|
||||
"decor.size_m": "Medium",
|
||||
"decor.size_l": "Large"
|
||||
}
|
||||
|
||||
+20
-1
@@ -261,5 +261,24 @@
|
||||
"roomcard.light_on": "Вкл",
|
||||
"roomcard.light_off": "Выкл",
|
||||
"roomcard.light_partial": "{on} из {total}",
|
||||
"toast.split_pick_inside": "Промежуточные точки разреза — внутри комнаты"
|
||||
"toast.split_pick_inside": "Промежуточные точки разреза — внутри комнаты",
|
||||
"mode.decor": "Редактор подложки",
|
||||
"decor.select": "Выбрать",
|
||||
"decor.line": "Линия",
|
||||
"decor.rect": "Прямоугольник",
|
||||
"decor.ellipse": "Овал",
|
||||
"decor.text": "Надпись",
|
||||
"decor.erase": "Стереть",
|
||||
"decor.color": "Цвет",
|
||||
"decor.width": "Толщина линии",
|
||||
"decor.w_thin": "Тонкая",
|
||||
"decor.w_mid": "Средняя",
|
||||
"decor.w_thick": "Толстая",
|
||||
"decor.fill": "Залить",
|
||||
"decor.text_title": "Надпись",
|
||||
"decor.text_label": "Текст",
|
||||
"decor.text_size": "Размер",
|
||||
"decor.size_s": "Мелкий",
|
||||
"decor.size_m": "Средний",
|
||||
"decor.size_l": "Крупный"
|
||||
}
|
||||
|
||||
@@ -432,6 +432,48 @@ export const cardStyles = css`
|
||||
user-select: none;
|
||||
z-index: 3;
|
||||
}
|
||||
/* decor (background) layer */
|
||||
.decorlayer .dshape { pointer-events: none; }
|
||||
.stage.mode-decor .decorlayer .dshape {
|
||||
pointer-events: visiblePainted;
|
||||
cursor: pointer;
|
||||
}
|
||||
.stage.mode-decor.dtool-select .decorlayer .dshape { cursor: move; }
|
||||
.decorlayer .dsel {
|
||||
filter: drop-shadow(0 0 3px var(--hp-accent));
|
||||
}
|
||||
.decorlayer .ddraft {
|
||||
opacity: 0.75;
|
||||
stroke-dasharray: 6 5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.decorlayer text {
|
||||
font-weight: 600;
|
||||
user-select: none;
|
||||
dominant-baseline: middle;
|
||||
text-anchor: middle;
|
||||
}
|
||||
.stage.mode-decor {
|
||||
outline: 2px solid #26a69a;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
.stage.mode-decor.dtool-line, .stage.mode-decor.dtool-rect,
|
||||
.stage.mode-decor.dtool-ellipse, .stage.mode-decor.dtool-text {
|
||||
cursor: crosshair;
|
||||
}
|
||||
.stage.mode-decor .room, .stage.mode-decor .devlayer { pointer-events: none; }
|
||||
.stage.mode-decor .oplock { pointer-events: none; }
|
||||
.decorbar .dcolor {
|
||||
width: 30px; height: 26px; padding: 0; border: none; background: none; cursor: pointer;
|
||||
}
|
||||
.decorbar .dwidth {
|
||||
font-family: inherit; font-size: 12px; border-radius: 6px;
|
||||
background: var(--hp-bg2, transparent); color: var(--hp-txt); border: 1px solid var(--hp-muted);
|
||||
padding: 3px 5px;
|
||||
}
|
||||
.decorbar .dfill {
|
||||
display: inline-flex; align-items: center; gap: 4px; font-size: 12px; cursor: pointer;
|
||||
}
|
||||
.opghost {
|
||||
stroke: var(--hp-open, #ff9800);
|
||||
stroke-width: 5;
|
||||
|
||||
Reference in New Issue
Block a user