ux v1.38.3: true dashed open boundaries in the Plan editor too

- room outline trimming applies in markup (blue .room-outline.outlined);
  merge/split-picked rooms keep the full amber stroke
- smoke_openwall extended (14 checks); TESTING/CHANGELOG same-commit
This commit is contained in:
Matysh
2026-07-23 17:17:01 +03:00
parent 8adb262410
commit 9c92bcdf2f
11 changed files with 64 additions and 24 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.38.2"
VERSION = "1.38.3"
DEFAULT_CONFIG: dict = {
"spaces": [],
@@ -598,6 +598,11 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
stroke-width: 2.5;
pointer-events: none;
}
/* Plan editor: trimmed outlines use the markup blue */
.room-outline.outlined {
stroke: rgba(62, 166, 255, 0.55);
stroke-opacity: 1;
}
.openwalls.hot .openwall {
stroke: #ffc14d;
opacity: 1;
@@ -1693,16 +1698,17 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
${this._editing&&!this._markup?j`<rect x="${i[0]}" y="${i[1]}" width="${i[2]}" height="${i[3]}" fill="url(#hp-grid)" pointer-events="none"></rect>`:G}
${e.bg?j`<image href="${e.bg.href}" x="${e.bg.x}" y="${e.bg.y}" width="${e.bg.w}" height="${e.bg.h}" preserveAspectRatio="none" />`:G}
${this._renderDecorLayer()}
${e.rooms.filter(t=>t.area||this._markup||o.showBorders).map(t=>{let i="room "+(e.bg?"overlay":"yard")+(this._markup?" outlined":"");!this._markup||t.id!==this._mergeSel&&t.id!==this._splitSel?.roomId||(i+=" picked");let s="";if(!this._markup&&(o.showBorders||"none"!==o.fill)){i+=" styled";const e=[];e.push(`--room-stroke:${o.color}`,`--room-stroke-op:${o.showBorders?o.opacity:0}`);const n="glow"===o.fill?this._fillColors.glow_base:t.area?Qe(o.fill,"lqi"===o.fill?this._roomLqi(t.area):null,"light"===o.fill?vi(this.hass,this._devices,t.area):"none","temp"===o.fill?yi(this.hass,this._devices,t.area):null,o.tempMin,o.tempMax,this._fillColors):null;n?(i+=" filled",e.push(`--room-fill:${n.c}`,`--room-fill-op:${n.a.toFixed(3)}`)):e.push("--room-fill:transparent","--room-fill-op:0"),s=e.join(";")}const r=e=>this._showTip(e,t.name,this._t("tip.room"),n?this._roomLqi(t.area):null,t.area?yi(this.hass,this._devices,t.area):null),a=!e.bg&&!o.showNames&&!this._markup,l=this._roomCenter(t),c=!this._markup&&t.id?this._openPairs().filter(e=>e.a.id===t.id||e.b.id===t.id).flatMap(t=>t.segs):[];c.length&&(i+=" noedge");const h=we(t),p=h?function(t,e,i=1e-6){const s=e.filter(e=>Re(t,e,i));return s.filter(t=>!s.some(e=>e!==t&&Re(e,t,i)))}(h,e.rooms.filter(e=>e!==t).map(t=>we(t)).filter(Boolean)):[],d=p.length&&h?j`<path class="${i}" style="${s}" fill-rule="evenodd"
d="${[h,...p].map(t=>"M "+t.map(t=>t[0]+" "+t[1]).join(" L ")+" Z").join(" ")}"
${e.rooms.filter(t=>t.area||this._markup||o.showBorders).map(t=>{let i="room "+(e.bg?"overlay":"yard")+(this._markup?" outlined":"");!this._markup||t.id!==this._mergeSel&&t.id!==this._splitSel?.roomId||(i+=" picked");let s="";if(!this._markup&&(o.showBorders||"none"!==o.fill)){i+=" styled";const e=[];e.push(`--room-stroke:${o.color}`,`--room-stroke-op:${o.showBorders?o.opacity:0}`);const n="glow"===o.fill?this._fillColors.glow_base:t.area?Qe(o.fill,"lqi"===o.fill?this._roomLqi(t.area):null,"light"===o.fill?vi(this.hass,this._devices,t.area):"none","temp"===o.fill?yi(this.hass,this._devices,t.area):null,o.tempMin,o.tempMax,this._fillColors):null;n?(i+=" filled",e.push(`--room-fill:${n.c}`,`--room-fill-op:${n.a.toFixed(3)}`)):e.push("--room-fill:transparent","--room-fill-op:0"),s=e.join(";")}const r=e=>this._showTip(e,t.name,this._t("tip.room"),n?this._roomLqi(t.area):null,t.area?yi(this.hass,this._devices,t.area):null),a=!e.bg&&!o.showNames&&!this._markup,l=this._roomCenter(t),c=this._markup&&(t.id===this._mergeSel||t.id===this._splitSel?.roomId),h=t.id&&!c?this._openPairs().filter(e=>e.a.id===t.id||e.b.id===t.id).flatMap(t=>t.segs):[];h.length&&(i+=" noedge");const p=we(t),d=p?function(t,e,i=1e-6){const s=e.filter(e=>Re(t,e,i));return s.filter(t=>!s.some(e=>e!==t&&Re(e,t,i)))}(p,e.rooms.filter(e=>e!==t).map(t=>we(t)).filter(Boolean)):[],u=d.length&&p?j`<path class="${i}" style="${s}" fill-rule="evenodd"
d="${[p,...d].map(t=>"M "+t.map(t=>t[0]+" "+t[1]).join(" L ")+" Z").join(" ")}"
@click=${()=>this._clickRoom(t)} @mousemove=${r}
@mouseleave=${()=>this._tip=null}></path>`:t.poly?j`<polygon class="${i}" style="${s}" points="${t.poly.map(t=>t.join(",")).join(" ")}"
@click=${()=>this._clickRoom(t)} @mousemove=${r}
@mouseleave=${()=>this._tip=null}></polygon>`:j`<rect class="${i}" style="${s}"
x="${t.x}" y="${t.y}" width="${t.w}" height="${t.h}" rx="${.03*Math.min(t.w,t.h)}"
@click=${()=>this._clickRoom(t)} @mousemove=${r}
@mouseleave=${()=>this._tip=null}></rect>`,u=c.length&&h?function(t,e,i=1e-6){const s=[];for(let o=0;o<t.length;o++){const n=t[o],r=t[(o+1)%t.length],a=r[0]-n[0],l=r[1]-n[1],c=Math.hypot(a,l);if(c<i)continue;const h=a/c,p=l/c,d=[];for(const t of e){const e=Math.abs((t[0]-n[0])*p-(t[1]-n[1])*h),s=Math.abs((t[2]-n[0])*p-(t[3]-n[1])*h),o=Math.max(i,1e-6*c);if(e>o||s>o)continue;const r=(t[0]-n[0])*h+(t[1]-n[1])*p,a=(t[2]-n[0])*h+(t[3]-n[1])*p,l=Math.max(0,Math.min(r,a)),u=Math.min(c,Math.max(r,a));u-l>i&&d.push([l,u])}if(!d.length){s.push([n[0],n[1],r[0],r[1]]);continue}d.sort((t,e)=>t[0]-e[0]);let u=0;for(const[t,e]of d)t-u>i&&s.push([n[0]+h*u,n[1]+p*u,n[0]+h*t,n[1]+p*t]),u=Math.max(u,e);c-u>i&&s.push([n[0]+h*u,n[1]+p*u,r[0],r[1]])}return s}(h,c,.02*this._gridPitch):null,_=u?j`<path class="room-outline" d="${u.map(t=>`M ${t[0]} ${t[1]} L ${t[2]} ${t[3]}`).join(" ")}"
style="stroke:${o.color};stroke-opacity:${o.showBorders&&!this._markup?o.opacity:0}"></path>`:G;return j`${d}${_}${a?j`<text class="rlabel" x="${l[0]}" y="${l[1]}">${t.name}</text>`:G}`})}
@mouseleave=${()=>this._tip=null}></rect>`,_=h.length&&p?function(t,e,i=1e-6){const s=[];for(let o=0;o<t.length;o++){const n=t[o],r=t[(o+1)%t.length],a=r[0]-n[0],l=r[1]-n[1],c=Math.hypot(a,l);if(c<i)continue;const h=a/c,p=l/c,d=[];for(const t of e){const e=Math.abs((t[0]-n[0])*p-(t[1]-n[1])*h),s=Math.abs((t[2]-n[0])*p-(t[3]-n[1])*h),o=Math.max(i,1e-6*c);if(e>o||s>o)continue;const r=(t[0]-n[0])*h+(t[1]-n[1])*p,a=(t[2]-n[0])*h+(t[3]-n[1])*p,l=Math.max(0,Math.min(r,a)),u=Math.min(c,Math.max(r,a));u-l>i&&d.push([l,u])}if(!d.length){s.push([n[0],n[1],r[0],r[1]]);continue}d.sort((t,e)=>t[0]-e[0]);let u=0;for(const[t,e]of d)t-u>i&&s.push([n[0]+h*u,n[1]+p*u,n[0]+h*t,n[1]+p*t]),u=Math.max(u,e);c-u>i&&s.push([n[0]+h*u,n[1]+p*u,r[0],r[1]])}return s}(p,h,.02*this._gridPitch):null,g=_?j`<path class="room-outline ${this._markup?"outlined":""}"
d="${_.map(t=>`M ${t[0]} ${t[1]} L ${t[2]} ${t[3]}`).join(" ")}"
style=${this._markup?G:`stroke:${o.color};stroke-opacity:${o.showBorders?o.opacity:0}`}></path>`:G;return j`${u}${g}${a?j`<text class="rlabel" x="${l[0]}" y="${l[1]}">${t.name}</text>`:G}`})}
${"glow"!==o.fill||this._markup?G:this._renderGlowLayer(e)}
${this._renderOpenWalls(o)}
${this._markup?this._renderMarkupLayer(i):G}
@@ -2300,4 +2306,4 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
</button>
</div>
</div>
</div>`}}Ki.properties={hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_mode:{state:!0},_tool:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_mergeSel:{state:!0},_openingDialog:{state:!0},_openingInfo:{state:!0},_mergeDialog:{state:!0},_splitSel:{state:!0},_decorTool:{state:!0},_decorStyle:{state:!0},_decorDraft:{state:!0},_decorSel:{state:!0},_decorTextDialog:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_rulesDialog:{state:!0},_settingsDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},Ki.styles=Mi,customElements.get("houseplan-card")||customElements.define("houseplan-card",Ki),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info("%c HOUSEPLAN-CARD %c v1.38.2 ","background:#3ea6ff;color:#04121f;font-weight:700","");
</div>`}}Ki.properties={hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_mode:{state:!0},_tool:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_mergeSel:{state:!0},_openingDialog:{state:!0},_openingInfo:{state:!0},_mergeDialog:{state:!0},_splitSel:{state:!0},_decorTool:{state:!0},_decorStyle:{state:!0},_decorDraft:{state:!0},_decorSel:{state:!0},_decorTextDialog:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_rulesDialog:{state:!0},_settingsDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},Ki.styles=Mi,customElements.get("houseplan-card")||customElements.define("houseplan-card",Ki),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info("%c HOUSEPLAN-CARD %c v1.38.3 ","background:#3ea6ff;color:#04121f;font-weight:700","");
+1 -1
View File
@@ -16,5 +16,5 @@
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
"requirements": [],
"single_config_entry": true,
"version": "1.38.2"
"version": "1.38.3"
}
+6 -1
View File
@@ -24,7 +24,12 @@ const res = await page.evaluate(async () => {
const gi = order.indexOf('glowlayer');
const oi = order.indexOf('openwalls');
out.dashAboveGlow = gi === -1 || oi > gi;
c._setMode('plan'); c._tool = 'openwall'; await c.updateComplete;
// в редакторе плана: пунктир виден, штрих комнат вырезан, контур синий
c._setMode('plan'); c._tool = 'draw'; await c.updateComplete;
out.planDashes = sr().querySelectorAll('.openwall').length > 0;
out.planNoedge = sr().querySelectorAll('.room.noedge').length >= 2;
out.planBlueOutline = sr().querySelectorAll('.room-outline.outlined').length >= 2;
c._tool = 'openwall'; await c.updateComplete;
// повторный клик закрывает
c._openWallClick([550, 0.25 * H]); await c.updateComplete;
out.toggledOff = !(c._curSpaceCfg.rooms.find((r) => r.id === 'r1').open_to || []).includes('r2');
+11 -5
View File
@@ -598,6 +598,11 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
stroke-width: 2.5;
pointer-events: none;
}
/* Plan editor: trimmed outlines use the markup blue */
.room-outline.outlined {
stroke: rgba(62, 166, 255, 0.55);
stroke-opacity: 1;
}
.openwalls.hot .openwall {
stroke: #ffc14d;
opacity: 1;
@@ -1693,16 +1698,17 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
${this._editing&&!this._markup?j`<rect x="${i[0]}" y="${i[1]}" width="${i[2]}" height="${i[3]}" fill="url(#hp-grid)" pointer-events="none"></rect>`:G}
${e.bg?j`<image href="${e.bg.href}" x="${e.bg.x}" y="${e.bg.y}" width="${e.bg.w}" height="${e.bg.h}" preserveAspectRatio="none" />`:G}
${this._renderDecorLayer()}
${e.rooms.filter(t=>t.area||this._markup||o.showBorders).map(t=>{let i="room "+(e.bg?"overlay":"yard")+(this._markup?" outlined":"");!this._markup||t.id!==this._mergeSel&&t.id!==this._splitSel?.roomId||(i+=" picked");let s="";if(!this._markup&&(o.showBorders||"none"!==o.fill)){i+=" styled";const e=[];e.push(`--room-stroke:${o.color}`,`--room-stroke-op:${o.showBorders?o.opacity:0}`);const n="glow"===o.fill?this._fillColors.glow_base:t.area?Qe(o.fill,"lqi"===o.fill?this._roomLqi(t.area):null,"light"===o.fill?vi(this.hass,this._devices,t.area):"none","temp"===o.fill?yi(this.hass,this._devices,t.area):null,o.tempMin,o.tempMax,this._fillColors):null;n?(i+=" filled",e.push(`--room-fill:${n.c}`,`--room-fill-op:${n.a.toFixed(3)}`)):e.push("--room-fill:transparent","--room-fill-op:0"),s=e.join(";")}const r=e=>this._showTip(e,t.name,this._t("tip.room"),n?this._roomLqi(t.area):null,t.area?yi(this.hass,this._devices,t.area):null),a=!e.bg&&!o.showNames&&!this._markup,l=this._roomCenter(t),c=!this._markup&&t.id?this._openPairs().filter(e=>e.a.id===t.id||e.b.id===t.id).flatMap(t=>t.segs):[];c.length&&(i+=" noedge");const h=we(t),p=h?function(t,e,i=1e-6){const s=e.filter(e=>Re(t,e,i));return s.filter(t=>!s.some(e=>e!==t&&Re(e,t,i)))}(h,e.rooms.filter(e=>e!==t).map(t=>we(t)).filter(Boolean)):[],d=p.length&&h?j`<path class="${i}" style="${s}" fill-rule="evenodd"
d="${[h,...p].map(t=>"M "+t.map(t=>t[0]+" "+t[1]).join(" L ")+" Z").join(" ")}"
${e.rooms.filter(t=>t.area||this._markup||o.showBorders).map(t=>{let i="room "+(e.bg?"overlay":"yard")+(this._markup?" outlined":"");!this._markup||t.id!==this._mergeSel&&t.id!==this._splitSel?.roomId||(i+=" picked");let s="";if(!this._markup&&(o.showBorders||"none"!==o.fill)){i+=" styled";const e=[];e.push(`--room-stroke:${o.color}`,`--room-stroke-op:${o.showBorders?o.opacity:0}`);const n="glow"===o.fill?this._fillColors.glow_base:t.area?Qe(o.fill,"lqi"===o.fill?this._roomLqi(t.area):null,"light"===o.fill?vi(this.hass,this._devices,t.area):"none","temp"===o.fill?yi(this.hass,this._devices,t.area):null,o.tempMin,o.tempMax,this._fillColors):null;n?(i+=" filled",e.push(`--room-fill:${n.c}`,`--room-fill-op:${n.a.toFixed(3)}`)):e.push("--room-fill:transparent","--room-fill-op:0"),s=e.join(";")}const r=e=>this._showTip(e,t.name,this._t("tip.room"),n?this._roomLqi(t.area):null,t.area?yi(this.hass,this._devices,t.area):null),a=!e.bg&&!o.showNames&&!this._markup,l=this._roomCenter(t),c=this._markup&&(t.id===this._mergeSel||t.id===this._splitSel?.roomId),h=t.id&&!c?this._openPairs().filter(e=>e.a.id===t.id||e.b.id===t.id).flatMap(t=>t.segs):[];h.length&&(i+=" noedge");const p=we(t),d=p?function(t,e,i=1e-6){const s=e.filter(e=>Re(t,e,i));return s.filter(t=>!s.some(e=>e!==t&&Re(e,t,i)))}(p,e.rooms.filter(e=>e!==t).map(t=>we(t)).filter(Boolean)):[],u=d.length&&p?j`<path class="${i}" style="${s}" fill-rule="evenodd"
d="${[p,...d].map(t=>"M "+t.map(t=>t[0]+" "+t[1]).join(" L ")+" Z").join(" ")}"
@click=${()=>this._clickRoom(t)} @mousemove=${r}
@mouseleave=${()=>this._tip=null}></path>`:t.poly?j`<polygon class="${i}" style="${s}" points="${t.poly.map(t=>t.join(",")).join(" ")}"
@click=${()=>this._clickRoom(t)} @mousemove=${r}
@mouseleave=${()=>this._tip=null}></polygon>`:j`<rect class="${i}" style="${s}"
x="${t.x}" y="${t.y}" width="${t.w}" height="${t.h}" rx="${.03*Math.min(t.w,t.h)}"
@click=${()=>this._clickRoom(t)} @mousemove=${r}
@mouseleave=${()=>this._tip=null}></rect>`,u=c.length&&h?function(t,e,i=1e-6){const s=[];for(let o=0;o<t.length;o++){const n=t[o],r=t[(o+1)%t.length],a=r[0]-n[0],l=r[1]-n[1],c=Math.hypot(a,l);if(c<i)continue;const h=a/c,p=l/c,d=[];for(const t of e){const e=Math.abs((t[0]-n[0])*p-(t[1]-n[1])*h),s=Math.abs((t[2]-n[0])*p-(t[3]-n[1])*h),o=Math.max(i,1e-6*c);if(e>o||s>o)continue;const r=(t[0]-n[0])*h+(t[1]-n[1])*p,a=(t[2]-n[0])*h+(t[3]-n[1])*p,l=Math.max(0,Math.min(r,a)),u=Math.min(c,Math.max(r,a));u-l>i&&d.push([l,u])}if(!d.length){s.push([n[0],n[1],r[0],r[1]]);continue}d.sort((t,e)=>t[0]-e[0]);let u=0;for(const[t,e]of d)t-u>i&&s.push([n[0]+h*u,n[1]+p*u,n[0]+h*t,n[1]+p*t]),u=Math.max(u,e);c-u>i&&s.push([n[0]+h*u,n[1]+p*u,r[0],r[1]])}return s}(h,c,.02*this._gridPitch):null,_=u?j`<path class="room-outline" d="${u.map(t=>`M ${t[0]} ${t[1]} L ${t[2]} ${t[3]}`).join(" ")}"
style="stroke:${o.color};stroke-opacity:${o.showBorders&&!this._markup?o.opacity:0}"></path>`:G;return j`${d}${_}${a?j`<text class="rlabel" x="${l[0]}" y="${l[1]}">${t.name}</text>`:G}`})}
@mouseleave=${()=>this._tip=null}></rect>`,_=h.length&&p?function(t,e,i=1e-6){const s=[];for(let o=0;o<t.length;o++){const n=t[o],r=t[(o+1)%t.length],a=r[0]-n[0],l=r[1]-n[1],c=Math.hypot(a,l);if(c<i)continue;const h=a/c,p=l/c,d=[];for(const t of e){const e=Math.abs((t[0]-n[0])*p-(t[1]-n[1])*h),s=Math.abs((t[2]-n[0])*p-(t[3]-n[1])*h),o=Math.max(i,1e-6*c);if(e>o||s>o)continue;const r=(t[0]-n[0])*h+(t[1]-n[1])*p,a=(t[2]-n[0])*h+(t[3]-n[1])*p,l=Math.max(0,Math.min(r,a)),u=Math.min(c,Math.max(r,a));u-l>i&&d.push([l,u])}if(!d.length){s.push([n[0],n[1],r[0],r[1]]);continue}d.sort((t,e)=>t[0]-e[0]);let u=0;for(const[t,e]of d)t-u>i&&s.push([n[0]+h*u,n[1]+p*u,n[0]+h*t,n[1]+p*t]),u=Math.max(u,e);c-u>i&&s.push([n[0]+h*u,n[1]+p*u,r[0],r[1]])}return s}(p,h,.02*this._gridPitch):null,g=_?j`<path class="room-outline ${this._markup?"outlined":""}"
d="${_.map(t=>`M ${t[0]} ${t[1]} L ${t[2]} ${t[3]}`).join(" ")}"
style=${this._markup?G:`stroke:${o.color};stroke-opacity:${o.showBorders?o.opacity:0}`}></path>`:G;return j`${u}${g}${a?j`<text class="rlabel" x="${l[0]}" y="${l[1]}">${t.name}</text>`:G}`})}
${"glow"!==o.fill||this._markup?G:this._renderGlowLayer(e)}
${this._renderOpenWalls(o)}
${this._markup?this._renderMarkupLayer(i):G}
@@ -2300,4 +2306,4 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
</button>
</div>
</div>
</div>`}}Ki.properties={hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_mode:{state:!0},_tool:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_mergeSel:{state:!0},_openingDialog:{state:!0},_openingInfo:{state:!0},_mergeDialog:{state:!0},_splitSel:{state:!0},_decorTool:{state:!0},_decorStyle:{state:!0},_decorDraft:{state:!0},_decorSel:{state:!0},_decorTextDialog:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_rulesDialog:{state:!0},_settingsDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},Ki.styles=Mi,customElements.get("houseplan-card")||customElements.define("houseplan-card",Ki),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info("%c HOUSEPLAN-CARD %c v1.38.2 ","background:#3ea6ff;color:#04121f;font-weight:700","");
</div>`}}Ki.properties={hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_mode:{state:!0},_tool:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_mergeSel:{state:!0},_openingDialog:{state:!0},_openingInfo:{state:!0},_mergeDialog:{state:!0},_splitSel:{state:!0},_decorTool:{state:!0},_decorStyle:{state:!0},_decorDraft:{state:!0},_decorSel:{state:!0},_decorTextDialog:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_rulesDialog:{state:!0},_settingsDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},Ki.styles=Mi,customElements.get("houseplan-card")||customElements.define("houseplan-card",Ki),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info("%c HOUSEPLAN-CARD %c v1.38.3 ","background:#3ea6ff;color:#04121f;font-weight:700","");
+11 -5
View File
@@ -598,6 +598,11 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
stroke-width: 2.5;
pointer-events: none;
}
/* Plan editor: trimmed outlines use the markup blue */
.room-outline.outlined {
stroke: rgba(62, 166, 255, 0.55);
stroke-opacity: 1;
}
.openwalls.hot .openwall {
stroke: #ffc14d;
opacity: 1;
@@ -1693,16 +1698,17 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
${this._editing&&!this._markup?j`<rect x="${i[0]}" y="${i[1]}" width="${i[2]}" height="${i[3]}" fill="url(#hp-grid)" pointer-events="none"></rect>`:G}
${e.bg?j`<image href="${e.bg.href}" x="${e.bg.x}" y="${e.bg.y}" width="${e.bg.w}" height="${e.bg.h}" preserveAspectRatio="none" />`:G}
${this._renderDecorLayer()}
${e.rooms.filter(t=>t.area||this._markup||o.showBorders).map(t=>{let i="room "+(e.bg?"overlay":"yard")+(this._markup?" outlined":"");!this._markup||t.id!==this._mergeSel&&t.id!==this._splitSel?.roomId||(i+=" picked");let s="";if(!this._markup&&(o.showBorders||"none"!==o.fill)){i+=" styled";const e=[];e.push(`--room-stroke:${o.color}`,`--room-stroke-op:${o.showBorders?o.opacity:0}`);const n="glow"===o.fill?this._fillColors.glow_base:t.area?Qe(o.fill,"lqi"===o.fill?this._roomLqi(t.area):null,"light"===o.fill?vi(this.hass,this._devices,t.area):"none","temp"===o.fill?yi(this.hass,this._devices,t.area):null,o.tempMin,o.tempMax,this._fillColors):null;n?(i+=" filled",e.push(`--room-fill:${n.c}`,`--room-fill-op:${n.a.toFixed(3)}`)):e.push("--room-fill:transparent","--room-fill-op:0"),s=e.join(";")}const r=e=>this._showTip(e,t.name,this._t("tip.room"),n?this._roomLqi(t.area):null,t.area?yi(this.hass,this._devices,t.area):null),a=!e.bg&&!o.showNames&&!this._markup,l=this._roomCenter(t),c=!this._markup&&t.id?this._openPairs().filter(e=>e.a.id===t.id||e.b.id===t.id).flatMap(t=>t.segs):[];c.length&&(i+=" noedge");const h=we(t),p=h?function(t,e,i=1e-6){const s=e.filter(e=>Re(t,e,i));return s.filter(t=>!s.some(e=>e!==t&&Re(e,t,i)))}(h,e.rooms.filter(e=>e!==t).map(t=>we(t)).filter(Boolean)):[],d=p.length&&h?j`<path class="${i}" style="${s}" fill-rule="evenodd"
d="${[h,...p].map(t=>"M "+t.map(t=>t[0]+" "+t[1]).join(" L ")+" Z").join(" ")}"
${e.rooms.filter(t=>t.area||this._markup||o.showBorders).map(t=>{let i="room "+(e.bg?"overlay":"yard")+(this._markup?" outlined":"");!this._markup||t.id!==this._mergeSel&&t.id!==this._splitSel?.roomId||(i+=" picked");let s="";if(!this._markup&&(o.showBorders||"none"!==o.fill)){i+=" styled";const e=[];e.push(`--room-stroke:${o.color}`,`--room-stroke-op:${o.showBorders?o.opacity:0}`);const n="glow"===o.fill?this._fillColors.glow_base:t.area?Qe(o.fill,"lqi"===o.fill?this._roomLqi(t.area):null,"light"===o.fill?vi(this.hass,this._devices,t.area):"none","temp"===o.fill?yi(this.hass,this._devices,t.area):null,o.tempMin,o.tempMax,this._fillColors):null;n?(i+=" filled",e.push(`--room-fill:${n.c}`,`--room-fill-op:${n.a.toFixed(3)}`)):e.push("--room-fill:transparent","--room-fill-op:0"),s=e.join(";")}const r=e=>this._showTip(e,t.name,this._t("tip.room"),n?this._roomLqi(t.area):null,t.area?yi(this.hass,this._devices,t.area):null),a=!e.bg&&!o.showNames&&!this._markup,l=this._roomCenter(t),c=this._markup&&(t.id===this._mergeSel||t.id===this._splitSel?.roomId),h=t.id&&!c?this._openPairs().filter(e=>e.a.id===t.id||e.b.id===t.id).flatMap(t=>t.segs):[];h.length&&(i+=" noedge");const p=we(t),d=p?function(t,e,i=1e-6){const s=e.filter(e=>Re(t,e,i));return s.filter(t=>!s.some(e=>e!==t&&Re(e,t,i)))}(p,e.rooms.filter(e=>e!==t).map(t=>we(t)).filter(Boolean)):[],u=d.length&&p?j`<path class="${i}" style="${s}" fill-rule="evenodd"
d="${[p,...d].map(t=>"M "+t.map(t=>t[0]+" "+t[1]).join(" L ")+" Z").join(" ")}"
@click=${()=>this._clickRoom(t)} @mousemove=${r}
@mouseleave=${()=>this._tip=null}></path>`:t.poly?j`<polygon class="${i}" style="${s}" points="${t.poly.map(t=>t.join(",")).join(" ")}"
@click=${()=>this._clickRoom(t)} @mousemove=${r}
@mouseleave=${()=>this._tip=null}></polygon>`:j`<rect class="${i}" style="${s}"
x="${t.x}" y="${t.y}" width="${t.w}" height="${t.h}" rx="${.03*Math.min(t.w,t.h)}"
@click=${()=>this._clickRoom(t)} @mousemove=${r}
@mouseleave=${()=>this._tip=null}></rect>`,u=c.length&&h?function(t,e,i=1e-6){const s=[];for(let o=0;o<t.length;o++){const n=t[o],r=t[(o+1)%t.length],a=r[0]-n[0],l=r[1]-n[1],c=Math.hypot(a,l);if(c<i)continue;const h=a/c,p=l/c,d=[];for(const t of e){const e=Math.abs((t[0]-n[0])*p-(t[1]-n[1])*h),s=Math.abs((t[2]-n[0])*p-(t[3]-n[1])*h),o=Math.max(i,1e-6*c);if(e>o||s>o)continue;const r=(t[0]-n[0])*h+(t[1]-n[1])*p,a=(t[2]-n[0])*h+(t[3]-n[1])*p,l=Math.max(0,Math.min(r,a)),u=Math.min(c,Math.max(r,a));u-l>i&&d.push([l,u])}if(!d.length){s.push([n[0],n[1],r[0],r[1]]);continue}d.sort((t,e)=>t[0]-e[0]);let u=0;for(const[t,e]of d)t-u>i&&s.push([n[0]+h*u,n[1]+p*u,n[0]+h*t,n[1]+p*t]),u=Math.max(u,e);c-u>i&&s.push([n[0]+h*u,n[1]+p*u,r[0],r[1]])}return s}(h,c,.02*this._gridPitch):null,_=u?j`<path class="room-outline" d="${u.map(t=>`M ${t[0]} ${t[1]} L ${t[2]} ${t[3]}`).join(" ")}"
style="stroke:${o.color};stroke-opacity:${o.showBorders&&!this._markup?o.opacity:0}"></path>`:G;return j`${d}${_}${a?j`<text class="rlabel" x="${l[0]}" y="${l[1]}">${t.name}</text>`:G}`})}
@mouseleave=${()=>this._tip=null}></rect>`,_=h.length&&p?function(t,e,i=1e-6){const s=[];for(let o=0;o<t.length;o++){const n=t[o],r=t[(o+1)%t.length],a=r[0]-n[0],l=r[1]-n[1],c=Math.hypot(a,l);if(c<i)continue;const h=a/c,p=l/c,d=[];for(const t of e){const e=Math.abs((t[0]-n[0])*p-(t[1]-n[1])*h),s=Math.abs((t[2]-n[0])*p-(t[3]-n[1])*h),o=Math.max(i,1e-6*c);if(e>o||s>o)continue;const r=(t[0]-n[0])*h+(t[1]-n[1])*p,a=(t[2]-n[0])*h+(t[3]-n[1])*p,l=Math.max(0,Math.min(r,a)),u=Math.min(c,Math.max(r,a));u-l>i&&d.push([l,u])}if(!d.length){s.push([n[0],n[1],r[0],r[1]]);continue}d.sort((t,e)=>t[0]-e[0]);let u=0;for(const[t,e]of d)t-u>i&&s.push([n[0]+h*u,n[1]+p*u,n[0]+h*t,n[1]+p*t]),u=Math.max(u,e);c-u>i&&s.push([n[0]+h*u,n[1]+p*u,r[0],r[1]])}return s}(p,h,.02*this._gridPitch):null,g=_?j`<path class="room-outline ${this._markup?"outlined":""}"
d="${_.map(t=>`M ${t[0]} ${t[1]} L ${t[2]} ${t[3]}`).join(" ")}"
style=${this._markup?G:`stroke:${o.color};stroke-opacity:${o.showBorders?o.opacity:0}`}></path>`:G;return j`${u}${g}${a?j`<text class="rlabel" x="${l[0]}" y="${l[1]}">${t.name}</text>`:G}`})}
${"glow"!==o.fill||this._markup?G:this._renderGlowLayer(e)}
${this._renderOpenWalls(o)}
${this._markup?this._renderMarkupLayer(i):G}
@@ -2300,4 +2306,4 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
</button>
</div>
</div>
</div>`}}Ki.properties={hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_mode:{state:!0},_tool:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_mergeSel:{state:!0},_openingDialog:{state:!0},_openingInfo:{state:!0},_mergeDialog:{state:!0},_splitSel:{state:!0},_decorTool:{state:!0},_decorStyle:{state:!0},_decorDraft:{state:!0},_decorSel:{state:!0},_decorTextDialog:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_rulesDialog:{state:!0},_settingsDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},Ki.styles=Mi,customElements.get("houseplan-card")||customElements.define("houseplan-card",Ki),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info("%c HOUSEPLAN-CARD %c v1.38.2 ","background:#3ea6ff;color:#04121f;font-weight:700","");
</div>`}}Ki.properties={hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_mode:{state:!0},_tool:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_mergeSel:{state:!0},_openingDialog:{state:!0},_openingInfo:{state:!0},_mergeDialog:{state:!0},_splitSel:{state:!0},_decorTool:{state:!0},_decorStyle:{state:!0},_decorDraft:{state:!0},_decorSel:{state:!0},_decorTextDialog:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_rulesDialog:{state:!0},_settingsDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},Ki.styles=Mi,customElements.get("houseplan-card")||customElements.define("houseplan-card",Ki),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info("%c HOUSEPLAN-CARD %c v1.38.3 ","background:#3ea6ff;color:#04121f;font-weight:700","");
+5
View File
@@ -1,5 +1,10 @@
# Changelog
## v1.38.3 — 2026-07-23
- The Plan editor now shows open boundaries as a true dash as well: the blue
markup outlines are trimmed under the open stretches (rooms picked for
merge/split keep their full amber highlight).
## v1.38.2 — 2026-07-23
- The card now **remembers where you were**: the selected space and the active
editor survive navigation and closing the tab (localStorage; edit modes are
+3
View File
@@ -140,6 +140,9 @@ 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
- [ ] Dashed boundaries in the Plan editor (v1.38.3): open stretches render as
a true dash in markup too (blue trimmed outlines); merge/split-picked
rooms keep their full amber highlight [auto]
- [ ] Nav persistence (v1.38.2): closing/reopening the tab restores the last
space AND editor mode (admins; localStorage); a #space= deep link beats
the saved space; a stale cache without the saved space retries after the
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "houseplan-card",
"version": "1.38.2",
"version": "1.38.3",
"description": "Interactive house plan Lovelace card for Home Assistant",
"license": "MIT",
"type": "module",
+9 -5
View File
@@ -32,7 +32,7 @@ import './space-card';
import { cardStyles } from './styles';
import { langOf, t, type I18nKey } from './i18n';
const CARD_VERSION = '1.38.2';
const CARD_VERSION = '1.38.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';
@@ -3201,8 +3201,11 @@ class HouseplanCard extends LitElement {
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
// the dashed stretches — suppress it and draw a trimmed outline.
// Applies in the Plan editor too (picked rooms keep their full
// amber highlight — the merge/split selection must stay visible).
const isPicked = this._markup && (r.id === this._mergeSel || r.id === this._splitSel?.roomId);
const openCuts = r.id && !isPicked
? this._openPairs()
.filter((pp) => pp.a.id === r.id || pp.b.id === r.id)
.flatMap((pp) => pp.segs)
@@ -3232,8 +3235,9 @@ class HouseplanCard extends LitElement {
? 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>`
? svg`<path class="room-outline ${this._markup ? 'outlined' : ''}"
d="${trimmed.map((sg) => `M ${sg[0]} ${sg[1]} L ${sg[2]} ${sg[3]}`).join(' ')}"
style=${this._markup ? nothing : `stroke:${disp.color};stroke-opacity:${disp.showBorders ? disp.opacity : 0}`}></path>`
: nothing;
return svg`${shape}${outline}${label ? svg`<text class="rlabel" x="${c[0]}" y="${c[1]}">${r.name}</text>` : nothing}`;
})}
+5
View File
@@ -595,6 +595,11 @@ export const cardStyles = css`
stroke-width: 2.5;
pointer-events: none;
}
/* Plan editor: trimmed outlines use the markup blue */
.room-outline.outlined {
stroke: rgba(62, 166, 255, 0.55);
stroke-opacity: 1;
}
.openwalls.hot .openwall {
stroke: #ffc14d;
opacity: 1;