mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 16:38:31 +00:00
ux v1.32.1: dashed hover preview for the Opening tool
- _openingPreview getter mirrors the click's snap (same eps, same existing-opening hit test); dashed 90cm ghost + center dot on the wall - smoke_opening_preview.mjs; TESTING/CHANGELOG same-commit
This commit is contained in:
@@ -11,7 +11,7 @@ PLANS_DIR = "houseplan/plans" # relative to the HA configuration directory
|
|||||||
FILES_URL = "/houseplan_files/files"
|
FILES_URL = "/houseplan_files/files"
|
||||||
FILES_DIR = "houseplan/files"
|
FILES_DIR = "houseplan/files"
|
||||||
CONF_ADMIN_ONLY = "admin_only"
|
CONF_ADMIN_ONLY = "admin_only"
|
||||||
VERSION = "1.32.0"
|
VERSION = "1.32.1"
|
||||||
|
|
||||||
DEFAULT_CONFIG: dict = {
|
DEFAULT_CONFIG: dict = {
|
||||||
"spaces": [],
|
"spaces": [],
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -16,5 +16,5 @@
|
|||||||
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
|
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
|
||||||
"requirements": [],
|
"requirements": [],
|
||||||
"single_config_entry": true,
|
"single_config_entry": true,
|
||||||
"version": "1.32.0"
|
"version": "1.32.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { launch } from './serve.mjs';
|
||||||
|
const { page, browser } = await launch();
|
||||||
|
const res = await page.evaluate(async () => {
|
||||||
|
const out = {};
|
||||||
|
const c = window.__card;
|
||||||
|
const sr = () => c.shadowRoot || c.renderRoot;
|
||||||
|
c._setMode('plan'); c._tool = 'opening'; await c.updateComplete;
|
||||||
|
const rooms = c._spaceModel().rooms;
|
||||||
|
const r = rooms.find((x) => x.id === 'r1');
|
||||||
|
const poly = r.poly || [[r.x, r.y], [r.x + r.w, r.y], [r.x + r.w, r.y + r.h], [r.x, r.y + r.h]];
|
||||||
|
const wallMid = [(poly[0][0] + poly[1][0]) / 2, (poly[0][1] + poly[1][1]) / 2];
|
||||||
|
// 1) курсор возле стены → пунктирный призрак есть
|
||||||
|
c._cursorPt = [wallMid[0], wallMid[1] + c._gridPitch * 0.5]; c.requestUpdate(); await c.updateComplete;
|
||||||
|
const ghost = sr().querySelector('.opghost');
|
||||||
|
out.ghostNearWall = !!ghost;
|
||||||
|
if (ghost) {
|
||||||
|
const cs = getComputedStyle(ghost);
|
||||||
|
out.dashed = cs.strokeDasharray !== 'none' && cs.strokeDasharray !== '';
|
||||||
|
// призрак лежит на стене (y координаты концов совпадают с y стены)
|
||||||
|
out.onWall = Math.abs(Number(ghost.getAttribute('y1')) - poly[0][1]) < 0.5
|
||||||
|
&& Math.abs(Number(ghost.getAttribute('y2')) - poly[0][1]) < 0.5;
|
||||||
|
// длина = 90 см в рендер-единицах
|
||||||
|
const len = Math.hypot(ghost.getAttribute('x2') - ghost.getAttribute('x1'), ghost.getAttribute('y2') - ghost.getAttribute('y1'));
|
||||||
|
out.len90cm = Math.abs(len - c._cmToUnits(90)) < 0.5;
|
||||||
|
}
|
||||||
|
// 2) курсор далеко от стен → призрака нет
|
||||||
|
const center = c._roomCenter(r);
|
||||||
|
c._cursorPt = center; c.requestUpdate(); await c.updateComplete;
|
||||||
|
out.noGhostFarFromWall = !sr().querySelector('.opghost');
|
||||||
|
// 3) в других инструментах призрака нет
|
||||||
|
c._tool = 'draw'; c._cursorPt = [wallMid[0], wallMid[1] + 2]; c.requestUpdate(); await c.updateComplete;
|
||||||
|
out.noGhostOtherTool = !sr().querySelector('.opghost');
|
||||||
|
// 4) над существующим проёмом призрак не рисуется (там клик = редактирование)
|
||||||
|
c._serverCfg = { ...c._serverCfg, spaces: c._serverCfg.spaces.map((s) => s.id !== c._space ? s : ({
|
||||||
|
...s, openings: [{ id: 'op9', type: 'door', x: wallMid[0] / 1000, y: wallMid[1] / (1000 / (c._curSpaceCfg.aspect || 1)) * (c._curSpaceCfg.aspect ? 1 : 1), angle: 0, length: 0.09 }] })) };
|
||||||
|
c._tool = 'opening'; c.requestUpdate(); await c.updateComplete;
|
||||||
|
const op = c._openingsR[0];
|
||||||
|
c._cursorPt = [op.rx, op.ry]; c.requestUpdate(); await c.updateComplete;
|
||||||
|
out.noGhostOverExisting = !sr().querySelector('.opghost');
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
console.log(JSON.stringify(res, null, 1));
|
||||||
|
await browser.close();
|
||||||
File diff suppressed because one or more lines are too long
Vendored
+18
-2
File diff suppressed because one or more lines are too long
@@ -1,5 +1,11 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.32.1 — 2026-07-22
|
||||||
|
- Opening tool: hovering near a wall now shows a **dashed preview** of where
|
||||||
|
the opening would land — snapped onto the wall, default door length (90 cm),
|
||||||
|
with a center dot. No preview far from walls or over an existing opening
|
||||||
|
(a click there edits it instead).
|
||||||
|
|
||||||
## v1.32.0 — 2026-07-22 (split polyline, tool cursors, Esc)
|
## v1.32.0 — 2026-07-22 (split polyline, tool cursors, Esc)
|
||||||
- **Split can now cut along a polyline**, not just a straight chord: start on
|
- **Split can now cut along a polyline**, not just a straight chord: start on
|
||||||
a wall, click intermediate points inside the room, finish on another wall.
|
a wall, click intermediate points inside the room, finish on another wall.
|
||||||
|
|||||||
@@ -140,6 +140,10 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
|
|||||||
(explicit ripple color still wins); off/white lights unchanged [auto]
|
(explicit ripple color still wins); off/white lights unchanged [auto]
|
||||||
- [ ] Alarm pulse (v1.27.0): leak/smoke/gas/CO/siren in 'on' pulse a red ring over any
|
- [ ] Alarm pulse (v1.27.0): leak/smoke/gas/CO/siren in 'on' pulse a red ring over any
|
||||||
display mode; clears on 'off'; unavailable never alarms [auto]; reduced-motion static
|
display mode; clears on 'off'; unavailable never alarms [auto]; reduced-motion static
|
||||||
|
- [ ] Opening hover preview (v1.32.1): with the Opening tool, hovering near a
|
||||||
|
wall shows a dashed 90 cm ghost snapped onto the wall (with a center
|
||||||
|
dot); no ghost far from walls, over an existing opening (click = edit),
|
||||||
|
or in other tools [auto]
|
||||||
- [ ] Split polyline + cursors + Esc (v1.32.0): Merge shows a pointer cursor,
|
- [ ] Split polyline + cursors + Esc (v1.32.0): Merge shows a pointer cursor,
|
||||||
Split shows pointer until a room is picked then crosshair; the cut can be
|
Split shows pointer until a room is picked then crosshair; the cut can be
|
||||||
a polyline — start on a wall, intermediate clicks inside the room, finish
|
a polyline — start on a wall, intermediate clicks inside the room, finish
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "houseplan-card",
|
"name": "houseplan-card",
|
||||||
"version": "1.32.0",
|
"version": "1.32.1",
|
||||||
"description": "Interactive house plan Lovelace card for Home Assistant",
|
"description": "Interactive house plan Lovelace card for Home Assistant",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
+31
-1
@@ -32,7 +32,7 @@ import './space-card';
|
|||||||
import { cardStyles } from './styles';
|
import { cardStyles } from './styles';
|
||||||
import { langOf, t, type I18nKey } from './i18n';
|
import { langOf, t, type I18nKey } from './i18n';
|
||||||
|
|
||||||
const CARD_VERSION = '1.32.0';
|
const CARD_VERSION = '1.32.1';
|
||||||
const LS_KEY = 'houseplan_card_layout_v1';
|
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_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering
|
||||||
const LS_ZOOM = 'houseplan_card_zoom_v1';
|
const LS_ZOOM = 'houseplan_card_zoom_v1';
|
||||||
@@ -1535,12 +1535,32 @@ class HouseplanCard extends LitElement {
|
|||||||
|
|
||||||
private _markupMove(ev: MouseEvent): void {
|
private _markupMove(ev: MouseEvent): void {
|
||||||
if (!this._markup) return;
|
if (!this._markup) return;
|
||||||
|
if (this._tool === 'opening') {
|
||||||
|
// hover preview: where the opening would land (raw point, wall-snapped later)
|
||||||
|
this._cursorPt = this._svgPoint(ev);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const drawing = this._tool === 'draw' && this._path.length && !this._contourClosed;
|
const drawing = this._tool === 'draw' && this._path.length && !this._contourClosed;
|
||||||
const cutting = this._tool === 'split' && !!this._splitSel?.pts?.length;
|
const cutting = this._tool === 'split' && !!this._splitSel?.pts?.length;
|
||||||
if (!drawing && !cutting) return;
|
if (!drawing && !cutting) return;
|
||||||
this._cursorPt = this._snap(this._svgPoint(ev));
|
this._cursorPt = this._snap(this._svgPoint(ev));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Dashed hover preview of an opening: same snap and default length as the click. */
|
||||||
|
private get _openingPreview(): { x: number; y: number; angle: number; rlen: number } | null {
|
||||||
|
if (this._tool !== 'opening' || !this._cursorPt) return null;
|
||||||
|
const raw = this._cursorPt;
|
||||||
|
// an existing opening under the cursor will be edited, not added — no preview
|
||||||
|
const eps = this._gridPitch * 1.5;
|
||||||
|
const hit = this._openingsR.find(
|
||||||
|
(o) => Math.hypot(raw[0] - o.rx, raw[1] - o.ry) <= Math.max(o.rlen / 2, eps),
|
||||||
|
);
|
||||||
|
if (hit) return null;
|
||||||
|
const snap = snapToWall(raw, this._spaceModel().rooms, eps);
|
||||||
|
if (!snap) return null;
|
||||||
|
return { ...snap, rlen: this._cmToUnits(90) };
|
||||||
|
}
|
||||||
|
|
||||||
/** Save a room with a mandatory binding to an HA area. */
|
/** Save a room with a mandatory binding to an HA area. */
|
||||||
private _saveRoom(): void {
|
private _saveRoom(): void {
|
||||||
if (!this._areaSel) return;
|
if (!this._areaSel) return;
|
||||||
@@ -3125,6 +3145,16 @@ class HouseplanCard extends LitElement {
|
|||||||
x2="${this._cursorPt[0]}" y2="${this._cursorPt[1]}"></line>`
|
x2="${this._cursorPt[0]}" y2="${this._cursorPt[1]}"></line>`
|
||||||
: nothing}
|
: nothing}
|
||||||
${path.map((p, i) => svg`<circle class="vertex ${i === 0 ? 'first' : ''}" cx="${p[0]}" cy="${p[1]}" r="${g * 0.22}"></circle>`)}
|
${path.map((p, i) => svg`<circle class="vertex ${i === 0 ? 'first' : ''}" cx="${p[0]}" cy="${p[1]}" r="${g * 0.22}"></circle>`)}
|
||||||
|
${(() => {
|
||||||
|
const op = this._openingPreview;
|
||||||
|
if (!op) return nothing;
|
||||||
|
const rad = (op.angle * Math.PI) / 180;
|
||||||
|
const dx = (Math.cos(rad) * op.rlen) / 2;
|
||||||
|
const dy = (Math.sin(rad) * op.rlen) / 2;
|
||||||
|
return svg`<line class="opghost" x1="${op.x - dx}" y1="${op.y - dy}"
|
||||||
|
x2="${op.x + dx}" y2="${op.y + dy}"></line>
|
||||||
|
<circle class="opghost-dot" cx="${op.x}" cy="${op.y}" r="${g * 0.18}"></circle>`;
|
||||||
|
})()}
|
||||||
${this._tool === 'split' && this._splitSel?.pts?.length
|
${this._tool === 'split' && this._splitSel?.pts?.length
|
||||||
? svg`${this._splitSel.pts.length > 1
|
? svg`${this._splitSel.pts.length > 1
|
||||||
? svg`<polyline class="pathline" points="${this._splitSel.pts.map((p) => p.join(',')).join(' ')}"></polyline>`
|
? svg`<polyline class="pathline" points="${this._splitSel.pts.map((p) => p.join(',')).join(' ')}"></polyline>`
|
||||||
|
|||||||
@@ -432,6 +432,19 @@ export const cardStyles = css`
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
|
.opghost {
|
||||||
|
stroke: var(--hp-open, #ff9800);
|
||||||
|
stroke-width: 5;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-dasharray: 7 6;
|
||||||
|
opacity: 0.85;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.opghost-dot {
|
||||||
|
fill: var(--hp-open, #ff9800);
|
||||||
|
opacity: 0.85;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
.rlabel {
|
.rlabel {
|
||||||
fill: var(--hp-muted);
|
fill: var(--hp-muted);
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
|||||||
Reference in New Issue
Block a user