mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 08:28:31 +00:00
ux v1.37.1: open-wall tool hover — default cursor, pointer + stretch preview near walls
- _openWallHit shared by click and hover; amber dashed preview of the stretch that would open, red solid when the click would close it; stage.wallhot drives the pointer cursor - smoke_openwall_hover.mjs (9 checks); 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_DIR = "houseplan/files"
|
||||
CONF_ADMIN_ONLY = "admin_only"
|
||||
VERSION = "1.37.0"
|
||||
VERSION = "1.37.1"
|
||||
|
||||
DEFAULT_CONFIG: dict = {
|
||||
"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",
|
||||
"requirements": [],
|
||||
"single_config_entry": true,
|
||||
"version": "1.37.0"
|
||||
"version": "1.37.1"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
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;
|
||||
const stage = () => sr().querySelector('.stage');
|
||||
c._setMode('plan'); c._tool = 'openwall'; await c.updateComplete;
|
||||
const H = 1000 / (c._curSpaceCfg.aspect || 1);
|
||||
// 1) без наведения: курсор default, превью нет
|
||||
c._cursorPt = null; c.requestUpdate(); await c.updateComplete;
|
||||
out.idleCursor = getComputedStyle(stage()).cursor === 'default';
|
||||
out.noPreview = !sr().querySelector('.openwall-preview');
|
||||
// 2) наведение на общую стену r1|r2 (x=550): pointer + янтарное превью
|
||||
c._cursorPt = [550, 0.25 * H]; c.requestUpdate(); await c.updateComplete;
|
||||
out.hotCursor = getComputedStyle(stage()).cursor === 'pointer';
|
||||
const prev = sr().querySelectorAll('.openwall-preview');
|
||||
out.previewShown = prev.length > 0;
|
||||
out.previewAmber = prev.length ? !prev[0].classList.contains('willclose') : null;
|
||||
// превью лежит на стене x=550
|
||||
out.previewOnWall = prev.length ? Math.abs(Number(prev[0].getAttribute('x1')) - 550) < 0.5 : null;
|
||||
// 3) наведение в центр комнаты: снова default, превью исчезло
|
||||
c._cursorPt = [300, 150]; c.requestUpdate(); await c.updateComplete;
|
||||
out.missCursor = getComputedStyle(stage()).cursor === 'default';
|
||||
out.missNoPreview = !sr().querySelector('.openwall-preview');
|
||||
// 4) открыть границу → навести снова: превью красное сплошное (клик закроет)
|
||||
c._openWallClick([550, 0.25 * H]); await c.updateComplete;
|
||||
c._cursorPt = [550, 0.25 * H]; c.requestUpdate(); await c.updateComplete;
|
||||
const prev2 = sr().querySelector('.openwall-preview');
|
||||
out.willclose = prev2?.classList.contains('willclose') === true;
|
||||
// прибрать
|
||||
c._openWallClick([550, 0.25 * H]); await c.updateComplete;
|
||||
return out;
|
||||
});
|
||||
console.log(JSON.stringify(res, null, 1));
|
||||
await browser.close();
|
||||
File diff suppressed because one or more lines are too long
Vendored
+113
-96
File diff suppressed because one or more lines are too long
@@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## v1.37.1 — 2026-07-23
|
||||
- Open-boundary tool polish: the cursor stays default and only turns into a
|
||||
pointer near a wall shared by two rooms; hovering previews the exact
|
||||
stretch that would become open (amber dashed) — or red solid when the
|
||||
boundary is already open and the click would close it.
|
||||
|
||||
## v1.37.0 — 2026-07-23 (open boundaries — virtual walls)
|
||||
- Rooms divided only by zoning can now share an **open boundary**: the new
|
||||
"Open boundary" tool in the Plan editor toggles it with a click on the wall
|
||||
|
||||
@@ -140,6 +140,11 @@ 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
|
||||
- [ ] Open-wall hover (v1.37.1): with the tool active the cursor is default;
|
||||
near a shared wall it turns pointer and the exact stretch that would
|
||||
open is previewed (amber dashed); an already-open boundary previews red
|
||||
solid (the click will close it); preview follows the cursor and clears
|
||||
on miss [auto]
|
||||
- [ ] Open boundaries (v1.37.0): the Plan editor's "Open boundary" tool
|
||||
toggles a virtual wall between two rooms by clicking their shared wall
|
||||
(pull like Split; miss → toast); open stretches render dashed (amber
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "houseplan-card",
|
||||
"version": "1.37.0",
|
||||
"version": "1.37.1",
|
||||
"description": "Interactive house plan Lovelace card for Home Assistant",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
+32
-11
@@ -32,7 +32,7 @@ import './space-card';
|
||||
import { cardStyles } from './styles';
|
||||
import { langOf, t, type I18nKey } from './i18n';
|
||||
|
||||
const CARD_VERSION = '1.37.0';
|
||||
const CARD_VERSION = '1.37.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';
|
||||
@@ -1598,14 +1598,26 @@ class HouseplanCard extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/** Boundary under the cursor in the open-wall tool (hover preview). */
|
||||
private get _openWallHover(): { segs: number[][]; open: boolean } | null {
|
||||
if (!this._markup || this._tool !== 'openwall' || !this._cursorPt) return null;
|
||||
const hit = this._openWallHit(this._cursorPt);
|
||||
return hit ? { segs: hit.segs, open: hit.open } : null;
|
||||
}
|
||||
|
||||
/** Dashed strokes over open (virtual) boundaries; highlighted in the tool. */
|
||||
private _renderOpenWalls(): TemplateResult {
|
||||
const pairs = this._openPairs();
|
||||
if (!pairs.length) return svg`` as unknown as TemplateResult;
|
||||
const hover = this._openWallHover;
|
||||
if (!pairs.length && !hover) return svg`` as unknown as TemplateResult;
|
||||
const hot = this._markup && this._tool === 'openwall';
|
||||
return svg`<g class="openwalls ${hot ? 'hot' : ''}">
|
||||
${pairs.flatMap((p) => p.segs.map((sg) => svg`<line class="openwall"
|
||||
x1="${sg[0]}" y1="${sg[1]}" x2="${sg[2]}" y2="${sg[3]}"></line>`))}
|
||||
${hover
|
||||
? hover.segs.map((sg) => svg`<line class="openwall-preview ${hover.open ? 'willclose' : ''}"
|
||||
x1="${sg[0]}" y1="${sg[1]}" x2="${sg[2]}" y2="${sg[3]}"></line>`)
|
||||
: nothing}
|
||||
</g>` as unknown as TemplateResult;
|
||||
}
|
||||
|
||||
@@ -1626,11 +1638,11 @@ class HouseplanCard extends LitElement {
|
||||
return res;
|
||||
}
|
||||
|
||||
/** Open-boundary tool: a click on a shared wall toggles its "virtual" state. */
|
||||
private _openWallClick(raw: number[]): void {
|
||||
/** The shared boundary nearest to the cursor (both the tool's click and hover). */
|
||||
private _openWallHit(raw: number[]): { a: RoomCfg; b: RoomCfg; segs: number[][]; open: boolean } | null {
|
||||
const rooms = this._spaceModel().rooms.filter((r) => r.id);
|
||||
const pull = this._gridPitch * 6;
|
||||
let best: { a: RoomCfg; b: RoomCfg; d: number } | null = null;
|
||||
let best: { a: RoomCfg; b: RoomCfg; segs: number[][]; d: number } | null = null;
|
||||
for (let i = 0; i < rooms.length; i++)
|
||||
for (let j = i + 1; j < rooms.length; j++) {
|
||||
const pa = roomPoly(rooms[i]), pb = roomPoly(rooms[j]);
|
||||
@@ -1638,16 +1650,25 @@ class HouseplanCard extends LitElement {
|
||||
const segs = sharedBoundary(pa, pb, this._gridPitch * 0.02);
|
||||
for (const seg of segs) {
|
||||
const d = distToSegment(raw, seg);
|
||||
if (d <= pull && (!best || d < best.d)) best = { a: rooms[i], b: rooms[j], d };
|
||||
if (d <= pull && (!best || d < best.d)) best = { a: rooms[i], b: rooms[j], segs, d };
|
||||
}
|
||||
}
|
||||
if (!best) return null;
|
||||
const open = (((best.a as any).open_to || []).includes(best.b.id))
|
||||
|| (((best.b as any).open_to || []).includes(best.a.id));
|
||||
return { a: best.a, b: best.b, segs: best.segs, open };
|
||||
}
|
||||
|
||||
/** Open-boundary tool: a click on a shared wall toggles its "virtual" state. */
|
||||
private _openWallClick(raw: number[]): void {
|
||||
const best = this._openWallHit(raw);
|
||||
if (!best) {
|
||||
this._showToast(this._t('toast.openwall_pick'));
|
||||
return;
|
||||
}
|
||||
const sp = this._curSpaceCfg;
|
||||
const ra = sp.rooms.find((r: any) => r.id === best!.a.id);
|
||||
const rb = sp.rooms.find((r: any) => r.id === best!.b.id);
|
||||
const ra = sp.rooms.find((r: any) => r.id === best.a.id);
|
||||
const rb = sp.rooms.find((r: any) => r.id === best.b.id);
|
||||
if (!ra || !rb) return;
|
||||
const linked = (ra.open_to || []).includes(rb.id) || (rb.open_to || []).includes(ra.id);
|
||||
if (linked) {
|
||||
@@ -1925,8 +1946,8 @@ 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)
|
||||
if (this._tool === 'opening' || this._tool === 'openwall') {
|
||||
// hover preview: raw cursor point; snapping happens in the preview getters
|
||||
this._cursorPt = this._svgPoint(ev);
|
||||
return;
|
||||
}
|
||||
@@ -3077,7 +3098,7 @@ class HouseplanCard extends LitElement {
|
||||
${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' : '') : ''} ${this._mode === 'decor' ? 'dtool-' + this._decorTool : ''} ${space.bg ? '' : 'noplan'} mode-${this._mode}"
|
||||
<div class="stage ${this._markup ? 'markup tool-' + this._tool + (this._tool === 'split' && !this._splitSel ? ' pickstage' : '') + (this._tool === 'openwall' && this._openWallHover ? ' wallhot' : '') : ''} ${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)}
|
||||
|
||||
+16
-1
@@ -538,10 +538,12 @@ export const cardStyles = css`
|
||||
/* room-picking stages: merge (both clicks) and split before a room is chosen */
|
||||
.stage.markup.tool-merge,
|
||||
.stage.markup.tool-split.pickstage,
|
||||
.stage.markup.tool-openwall,
|
||||
.stage.markup.tool-delroom {
|
||||
cursor: pointer;
|
||||
}
|
||||
/* open-wall tool: default until a shared wall is under the cursor */
|
||||
.stage.markup.tool-openwall { cursor: default; }
|
||||
.stage.markup.tool-openwall.wallhot { cursor: pointer; }
|
||||
.openwall {
|
||||
stroke: var(--card-background-color, var(--hp-bg, #fff));
|
||||
stroke-width: 3.2;
|
||||
@@ -553,6 +555,19 @@ export const cardStyles = css`
|
||||
stroke: #ffc14d;
|
||||
opacity: 1;
|
||||
}
|
||||
.openwall-preview {
|
||||
stroke: #ffc14d;
|
||||
stroke-width: 5;
|
||||
stroke-dasharray: 7 7;
|
||||
stroke-linecap: round;
|
||||
pointer-events: none;
|
||||
opacity: 0.95;
|
||||
}
|
||||
/* an already-open boundary under the cursor: the click will CLOSE it */
|
||||
.openwall-preview.willclose {
|
||||
stroke: #f25a4a;
|
||||
stroke-dasharray: none;
|
||||
}
|
||||
.stage.markup .room {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user