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:
Matysh
2026-07-22 14:33:26 +03:00
parent 5e6f9c407c
commit f235c8afc5
11 changed files with 154 additions and 10 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ PLANS_DIR = "houseplan/plans" # relative to the HA configuration directory
FILES_URL = "/houseplan_files/files"
FILES_DIR = "houseplan/files"
CONF_ADMIN_ONLY = "admin_only"
VERSION = "1.32.0"
VERSION = "1.32.1"
DEFAULT_CONFIG: dict = {
"spaces": [],
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -16,5 +16,5 @@
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
"requirements": [],
"single_config_entry": true,
"version": "1.32.0"
"version": "1.32.1"
}
+43
View File
@@ -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
+18 -2
View File
File diff suppressed because one or more lines are too long
+6
View File
@@ -1,5 +1,11 @@
# 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)
- **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.
+4
View File
@@ -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]
- [ ] 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
- [ ] 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 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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "houseplan-card",
"version": "1.32.0",
"version": "1.32.1",
"description": "Interactive house plan Lovelace card for Home Assistant",
"license": "MIT",
"type": "module",
+31 -1
View File
@@ -32,7 +32,7 @@ import './space-card';
import { cardStyles } from './styles';
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_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering
const LS_ZOOM = 'houseplan_card_zoom_v1';
@@ -1535,12 +1535,32 @@ class HouseplanCard extends LitElement {
private _markupMove(ev: MouseEvent): void {
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 cutting = this._tool === 'split' && !!this._splitSel?.pts?.length;
if (!drawing && !cutting) return;
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. */
private _saveRoom(): void {
if (!this._areaSel) return;
@@ -3125,6 +3145,16 @@ class HouseplanCard extends LitElement {
x2="${this._cursorPt[0]}" y2="${this._cursorPt[1]}"></line>`
: nothing}
${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
? svg`${this._splitSel.pts.length > 1
? svg`<polyline class="pathline" points="${this._splitSel.pts.map((p) => p.join(',')).join(' ')}"></polyline>`
+13
View File
@@ -432,6 +432,19 @@ export const cardStyles = css`
user-select: none;
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 {
fill: var(--hp-muted);
font-size: 15px;