mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 08:28:31 +00:00
fix v1.37.3: true dashed open boundary, rendered above the glow
- outlineWithout trims the rooms' solid strokes under open stretches (.room.noedge kills the polygon stroke incl. hover; trimmed .room-outline path draws the remaining walls) - dash color follows the space stroke color; openwalls layer moved after the glow layer - +1 unit test (105); smoke_openwall extended; docs same-commit
This commit is contained in:
+22
-6
@@ -14,7 +14,7 @@ import {
|
||||
import {
|
||||
lqiColor, snapToGrid, samePoint, pointInPolygon, markerIdForBinding,
|
||||
segmentCm, formatLength, roomEdges, roomPoly, pointStrictlyInside, roomsOverlap,
|
||||
pointOnBoundary, mergeRooms, splitRoomPath, polygonArea, closestPointOnBoundary, pointStrictlyInside as ptInside, islandsOf, sharedBoundary, openZoneOf, distToSegment,
|
||||
pointOnBoundary, mergeRooms, splitRoomPath, polygonArea, closestPointOnBoundary, pointStrictlyInside as ptInside, islandsOf, sharedBoundary, openZoneOf, distToSegment, outlineWithout,
|
||||
snapToWall, openingAmount,
|
||||
averageLqi, fitView, declump, safeUrl, resolveTapAction, floorsOf, type FloorInfo,
|
||||
stateIcon, lightColorOf, isAlarmState, parseRoomRef, diffNewDevices, glowColorOf, doorSector, hasRoomBehind, controlsAction, isControllable,
|
||||
@@ -32,7 +32,7 @@ import './space-card';
|
||||
import { cardStyles } from './styles';
|
||||
import { langOf, t, type I18nKey } from './i18n';
|
||||
|
||||
const CARD_VERSION = '1.37.2';
|
||||
const CARD_VERSION = '1.37.3';
|
||||
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';
|
||||
@@ -1606,12 +1606,13 @@ class HouseplanCard extends LitElement {
|
||||
}
|
||||
|
||||
/** Dashed strokes over open (virtual) boundaries; highlighted in the tool. */
|
||||
private _renderOpenWalls(): TemplateResult {
|
||||
private _renderOpenWalls(disp?: SpaceDisplay): TemplateResult {
|
||||
const pairs = this._openPairs();
|
||||
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' : ''}">
|
||||
const stroke = disp?.color || 'var(--hp-muted)';
|
||||
return svg`<g class="openwalls ${hot ? 'hot' : ''}" style="--ow-stroke:${stroke}">
|
||||
${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
|
||||
@@ -3152,6 +3153,14 @@ class HouseplanCard extends LitElement {
|
||||
r.area ? areaTemp(this.hass, this._devices, r.area) : null);
|
||||
const label = !space.bg && !disp.showNames && !this._markup;
|
||||
const c = this._roomCenter(r);
|
||||
// open boundaries: this room's solid stroke must not run beneath
|
||||
// the dashed stretches — suppress it and draw a trimmed outline
|
||||
const openCuts = !this._markup && r.id
|
||||
? this._openPairs()
|
||||
.filter((pp) => pp.a.id === r.id || pp.b.id === r.id)
|
||||
.flatMap((pp) => pp.segs)
|
||||
: [];
|
||||
if (openCuts.length) cls += ' noedge';
|
||||
// island rooms punch holes in their parent's fill (evenodd)
|
||||
const myPoly = roomPoly(r);
|
||||
const holes = myPoly
|
||||
@@ -3172,10 +3181,17 @@ class HouseplanCard extends LitElement {
|
||||
x="${r.x}" y="${r.y}" width="${r.w}" height="${r.h}" rx="${Math.min(r.w!, r.h!) * 0.03}"
|
||||
@click=${() => this._clickRoom(r)} @mousemove=${tip}
|
||||
@mouseleave=${() => (this._tip = null)}></rect>`;
|
||||
return svg`${shape}${label ? svg`<text class="rlabel" x="${c[0]}" y="${c[1]}">${r.name}</text>` : nothing}`;
|
||||
const trimmed = openCuts.length && myPoly
|
||||
? outlineWithout(myPoly, openCuts, this._gridPitch * 0.02)
|
||||
: null;
|
||||
const outline = trimmed
|
||||
? svg`<path class="room-outline" d="${trimmed.map((sg) => `M ${sg[0]} ${sg[1]} L ${sg[2]} ${sg[3]}`).join(' ')}"
|
||||
style="stroke:${disp.color};stroke-opacity:${disp.showBorders && !this._markup ? disp.opacity : 0}"></path>`
|
||||
: nothing;
|
||||
return svg`${shape}${outline}${label ? svg`<text class="rlabel" x="${c[0]}" y="${c[1]}">${r.name}</text>` : nothing}`;
|
||||
})}
|
||||
${this._renderOpenWalls()}
|
||||
${disp.fill === 'glow' && !this._markup ? this._renderGlowLayer(space) : nothing}
|
||||
${this._renderOpenWalls(disp)}
|
||||
${this._markup ? this._renderMarkupLayer(vb) : nothing}
|
||||
${this._renderOpenings(disp)}
|
||||
</svg>
|
||||
|
||||
@@ -911,6 +911,47 @@ export function openZoneOf(roomId: string, rooms: { id?: string; open_to?: strin
|
||||
return zone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Room outline pieces with the given collinear stretches removed — used to
|
||||
* draw a TRUE dashed open boundary (the solid stroke must not run beneath).
|
||||
* Returns segments [x1,y1,x2,y2].
|
||||
*/
|
||||
export function outlineWithout(poly: number[][], cuts: number[][], eps = 1e-6): number[][] {
|
||||
const out: number[][] = [];
|
||||
for (let i = 0; i < poly.length; i++) {
|
||||
const p1 = poly[i], p2 = poly[(i + 1) % poly.length];
|
||||
const dx = p2[0] - p1[0], dy = p2[1] - p1[1];
|
||||
const len = Math.hypot(dx, dy);
|
||||
if (len < eps) continue;
|
||||
const ux = dx / len, uy = dy / len;
|
||||
// collect cut intervals on this edge
|
||||
const iv: [number, number][] = [];
|
||||
for (const c of cuts) {
|
||||
const d1 = Math.abs((c[0] - p1[0]) * uy - (c[1] - p1[1]) * ux);
|
||||
const d2 = Math.abs((c[2] - p1[0]) * uy - (c[3] - p1[1]) * ux);
|
||||
const tol = Math.max(eps, len * 1e-6);
|
||||
if (d1 > tol || d2 > tol) continue;
|
||||
const t1 = (c[0] - p1[0]) * ux + (c[1] - p1[1]) * uy;
|
||||
const t2 = (c[2] - p1[0]) * ux + (c[3] - p1[1]) * uy;
|
||||
const lo = Math.max(0, Math.min(t1, t2));
|
||||
const hi = Math.min(len, Math.max(t1, t2));
|
||||
if (hi - lo > eps) iv.push([lo, hi]);
|
||||
}
|
||||
if (!iv.length) {
|
||||
out.push([p1[0], p1[1], p2[0], p2[1]]);
|
||||
continue;
|
||||
}
|
||||
iv.sort((a, b) => a[0] - b[0]);
|
||||
let cur = 0;
|
||||
for (const [lo, hi] of iv) {
|
||||
if (lo - cur > eps) out.push([p1[0] + ux * cur, p1[1] + uy * cur, p1[0] + ux * lo, p1[1] + uy * lo]);
|
||||
cur = Math.max(cur, hi);
|
||||
}
|
||||
if (len - cur > eps) out.push([p1[0] + ux * cur, p1[1] + uy * cur, p2[0], p2[1]]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Distance from a point to a segment [x1,y1,x2,y2]. */
|
||||
export function distToSegment(p: number[], s: number[]): number {
|
||||
const dx = s[2] - s[0], dy = s[3] - s[1];
|
||||
|
||||
+13
-2
@@ -545,12 +545,23 @@ export const cardStyles = css`
|
||||
.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;
|
||||
stroke: var(--ow-stroke, var(--hp-muted));
|
||||
stroke-width: 2.5;
|
||||
stroke-dasharray: 7 7;
|
||||
stroke-linecap: butt;
|
||||
pointer-events: none;
|
||||
opacity: 0.9;
|
||||
}
|
||||
/* rooms with open stretches: the polygon's own stroke is fully off
|
||||
(hover included) — the trimmed .room-outline path draws the walls */
|
||||
.room.noedge {
|
||||
stroke-opacity: 0 !important;
|
||||
}
|
||||
.room-outline {
|
||||
fill: none;
|
||||
stroke-width: 2.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.openwalls.hot .openwall {
|
||||
stroke: #ffc14d;
|
||||
opacity: 1;
|
||||
|
||||
Reference in New Issue
Block a user