mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 08:28:31 +00:00
fix v1.36.3: door sectors no longer punch dark wedges inside the room
- room outline + sectors as separate clipPath children (always union) instead of subpaths of one nonzero path where opposite windings cancelled the overlap; smoke asserts one contour per path - 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.36.2"
|
VERSION = "1.36.3"
|
||||||
|
|
||||||
DEFAULT_CONFIG: dict = {
|
DEFAULT_CONFIG: dict = {
|
||||||
"spaces": [],
|
"spaces": [],
|
||||||
|
|||||||
@@ -1448,14 +1448,14 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
|
|||||||
<input type="range" min="0" max="100" .value=${String(Math.round(100*i.a))}
|
<input type="range" min="0" max="100" .value=${String(Math.round(100*i.a))}
|
||||||
@input=${e=>this._setFillColor(t,{a:Number(e.target.value)/100})} />
|
@input=${e=>this._setFillColor(t,{a:Number(e.target.value)/100})} />
|
||||||
<span class="opv">${Math.round(100*i.a)}%</span>
|
<span class="opv">${Math.round(100*i.a)}%</span>
|
||||||
</div>`}get _glowRadiusCm(){const t=Number(this._settings.glow_radius_cm);return Number.isFinite(t)&&t>0?t:300}get _imperial(){return"mi"===this.hass?.config?.unit_system?.length}get _glowRadiusPlaceholder(){const t=this._glowRadiusCm;return this._imperial?String(Math.round(t/30.48*10)/10):String(t/100)}_renderGlowLayer(t){const e=this._fillColors,i=this._glowRadiusCm/this._cellCm*this._gridPitch,s=this._gridPitch,o=t.rooms.map(t=>({r:t,poly:we(t)})).filter(t=>!!t.poly),n=this._openingsR.filter(t=>"door"===t.type),r=[];for(const a of this._devices){if(a.space!==t.id)continue;const l=a.entities.find(t=>t.startsWith("light.")&&"on"===this.hass.states[t]?.state);if(!l)continue;const c=ei(this.hass.states[l],e.glow_light.c);if(!c)continue;const h=Number(a.marker?.glow_radius_cm),p=Number.isFinite(h)&&h>0?h/this._cellCm*this._gridPitch:i,d=this._pos(a),u=[...o].reverse().find(t=>this._pointInRoom([d.x,d.y],t.r));let _=null;if(u){const t=["M "+u.poly.map(t=>t[0]+" "+t[1]).join(" L ")+" Z"];for(const e of n){const i=Me([e.rx,e.ry],u.poly);if(!i||Math.hypot(i[0]-e.rx,i[1]-e.ry)>.75*s)continue;const n=e.angle*Math.PI/180,r=Math.cos(n)*e.rlen/2,a=Math.sin(n)*e.rlen/2,l=o.filter(t=>t!==u).map(t=>t.poly);if(!si([e.rx,e.ry],e.angle,[d.x,d.y],l,.6*s))continue;const c=ii([d.x,d.y],[e.rx-r,e.ry-a],[e.rx+r,e.ry+a],p);c&&t.push("M "+c.map(t=>t[0]+" "+t[1]).join(" L ")+" Z")}_=t.join(" ")}r.push({pos:d,c:c.c,alpha:e.glow_light.a*c.bri,clip:_,r:p})}return r.length?j`<defs>
|
</div>`}get _glowRadiusCm(){const t=Number(this._settings.glow_radius_cm);return Number.isFinite(t)&&t>0?t:300}get _imperial(){return"mi"===this.hass?.config?.unit_system?.length}get _glowRadiusPlaceholder(){const t=this._glowRadiusCm;return this._imperial?String(Math.round(t/30.48*10)/10):String(t/100)}_renderGlowLayer(t){const e=this._fillColors,i=this._glowRadiusCm/this._cellCm*this._gridPitch,s=this._gridPitch,o=t.rooms.map(t=>({r:t,poly:we(t)})).filter(t=>!!t.poly),n=this._openingsR.filter(t=>"door"===t.type),r=[];for(const a of this._devices){if(a.space!==t.id)continue;const l=a.entities.find(t=>t.startsWith("light.")&&"on"===this.hass.states[t]?.state);if(!l)continue;const c=ei(this.hass.states[l],e.glow_light.c);if(!c)continue;const h=Number(a.marker?.glow_radius_cm),p=Number.isFinite(h)&&h>0?h/this._cellCm*this._gridPitch:i,d=this._pos(a),u=[...o].reverse().find(t=>this._pointInRoom([d.x,d.y],t.r));let _=null;if(u){const t=["M "+u.poly.map(t=>t[0]+" "+t[1]).join(" L ")+" Z"];for(const e of n){const i=Me([e.rx,e.ry],u.poly);if(!i||Math.hypot(i[0]-e.rx,i[1]-e.ry)>.75*s)continue;const n=e.angle*Math.PI/180,r=Math.cos(n)*e.rlen/2,a=Math.sin(n)*e.rlen/2,l=o.filter(t=>t!==u).map(t=>t.poly);if(!si([e.rx,e.ry],e.angle,[d.x,d.y],l,.6*s))continue;const c=ii([d.x,d.y],[e.rx-r,e.ry-a],[e.rx+r,e.ry+a],p);c&&t.push("M "+c.map(t=>t[0]+" "+t[1]).join(" L ")+" Z")}_=t}r.push({pos:d,c:c.c,alpha:e.glow_light.a*c.bri,clip:_,r:p})}return r.length?j`<defs>
|
||||||
${r.map((t,e)=>j`
|
${r.map((t,e)=>j`
|
||||||
<radialGradient id="hp-glow-${e}">
|
<radialGradient id="hp-glow-${e}">
|
||||||
<stop offset="0%" stop-color="${t.c}" stop-opacity="${t.alpha.toFixed(3)}"></stop>
|
<stop offset="0%" stop-color="${t.c}" stop-opacity="${t.alpha.toFixed(3)}"></stop>
|
||||||
<stop offset="55%" stop-color="${t.c}" stop-opacity="${(.45*t.alpha).toFixed(3)}"></stop>
|
<stop offset="55%" stop-color="${t.c}" stop-opacity="${(.45*t.alpha).toFixed(3)}"></stop>
|
||||||
<stop offset="100%" stop-color="${t.c}" stop-opacity="0"></stop>
|
<stop offset="100%" stop-color="${t.c}" stop-opacity="0"></stop>
|
||||||
</radialGradient>
|
</radialGradient>
|
||||||
${t.clip?j`<clipPath id="hp-glowclip-${e}"><path d="${t.clip}"></path></clipPath>`:W}`)}
|
${t.clip?j`<clipPath id="hp-glowclip-${e}">${t.clip.map(t=>j`<path d="${t}"></path>`)}</clipPath>`:W}`)}
|
||||||
</defs>
|
</defs>
|
||||||
<g class="glowlayer">
|
<g class="glowlayer">
|
||||||
${r.map((t,e)=>j`<circle cx="${t.pos.x}" cy="${t.pos.y}" r="${t.r}"
|
${r.map((t,e)=>j`<circle cx="${t.pos.x}" cy="${t.pos.y}" r="${t.r}"
|
||||||
@@ -2195,4 +2195,4 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`}}ji.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}},ji.styles=$i,customElements.get("houseplan-card")||customElements.define("houseplan-card",ji),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.36.2 ","background:#3ea6ff;color:#04121f;font-weight:700","");
|
</div>`}}ji.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}},ji.styles=$i,customElements.get("houseplan-card")||customElements.define("houseplan-card",ji),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.36.3 ","background:#3ea6ff;color:#04121f;font-weight:700","");
|
||||||
|
|||||||
@@ -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.36.2"
|
"version": "1.36.3"
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-4
@@ -63,16 +63,20 @@ const res = await page.evaluate(async () => {
|
|||||||
// радиус 6 м, чтобы дверь заведомо была в зоне досягаемости
|
// радиус 6 м, чтобы дверь заведомо была в зоне досягаемости
|
||||||
c._serverCfg = { ...c._serverCfg, settings: { ...(c._serverCfg.settings || {}), glow_radius_cm: 600 } };
|
c._serverCfg = { ...c._serverCfg, settings: { ...(c._serverCfg.settings || {}), glow_radius_cm: 600 } };
|
||||||
c.requestUpdate(); await c.updateComplete;
|
c.requestUpdate(); await c.updateComplete;
|
||||||
const clips = [...sr().querySelectorAll('defs clipPath[id^="hp-glowclip"] path')];
|
// теперь каждый под-контур — отдельный path внутри clipPath
|
||||||
out.sectorAdded = clips.some((p) => ((p.getAttribute('d') || '').match(/M /g) || []).length >= 2);
|
const clipEls = [...sr().querySelectorAll('defs clipPath[id^="hp-glowclip"]')];
|
||||||
|
out.sectorAdded = clipEls.some((cp) => cp.querySelectorAll('path').length >= 2);
|
||||||
|
// и сектор в том же направлении обхода не выедает комнату: каждый path — один контур
|
||||||
|
out.singleSubpathPerPath = clipEls.every((cp) => [...cp.querySelectorAll('path')]
|
||||||
|
.every((p) => ((p.getAttribute('d') || '').match(/M /g) || []).length === 1));
|
||||||
// у входной двери (наружу) сектора нет: дверь на внешней стене r1 (x=min)
|
// у входной двери (наружу) сектора нет: дверь на внешней стене r1 (x=min)
|
||||||
const minX = Math.min(...poly1.map((p) => p[0]));
|
const minX = Math.min(...poly1.map((p) => p[0]));
|
||||||
const yMid = (Math.min(...poly1.map((p) => p[1])) + Math.max(...poly1.map((p) => p[1]))) / 2;
|
const yMid = (Math.min(...poly1.map((p) => p[1])) + Math.max(...poly1.map((p) => p[1]))) / 2;
|
||||||
c._serverCfg = { ...c._serverCfg, spaces: c._serverCfg.spaces.map((s) => s.id !== spId ? s : ({
|
c._serverCfg = { ...c._serverCfg, spaces: c._serverCfg.spaces.map((s) => s.id !== spId ? s : ({
|
||||||
...s, openings: [{ id: 'gd2', type: 'door', x: minX / 1000, y: yMid / (1000 / aspect), angle: 90, length: 0.09 }] })) };
|
...s, openings: [{ id: 'gd2', type: 'door', x: minX / 1000, y: yMid / (1000 / aspect), angle: 90, length: 0.09 }] })) };
|
||||||
c.requestUpdate(); await c.updateComplete;
|
c.requestUpdate(); await c.updateComplete;
|
||||||
const clips2 = [...sr().querySelectorAll('defs clipPath[id^="hp-glowclip"] path')];
|
const clipEls2 = [...sr().querySelectorAll('defs clipPath[id^="hp-glowclip"]')];
|
||||||
out.entranceNoSector = clips2.every((p) => ((p.getAttribute('d') || '').match(/M /g) || []).length === 1);
|
out.entranceNoSector = clipEls2.every((cp) => cp.querySelectorAll('path').length === 1);
|
||||||
// 7а) персональный радиус источника перекрывает глобальный
|
// 7а) персональный радиус источника перекрывает глобальный
|
||||||
const litMarkerId = litLight.id;
|
const litMarkerId = litLight.id;
|
||||||
c._serverCfg = { ...c._serverCfg, markers: [
|
c._serverCfg = { ...c._serverCfg, markers: [
|
||||||
|
|||||||
@@ -1448,14 +1448,14 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
|
|||||||
<input type="range" min="0" max="100" .value=${String(Math.round(100*i.a))}
|
<input type="range" min="0" max="100" .value=${String(Math.round(100*i.a))}
|
||||||
@input=${e=>this._setFillColor(t,{a:Number(e.target.value)/100})} />
|
@input=${e=>this._setFillColor(t,{a:Number(e.target.value)/100})} />
|
||||||
<span class="opv">${Math.round(100*i.a)}%</span>
|
<span class="opv">${Math.round(100*i.a)}%</span>
|
||||||
</div>`}get _glowRadiusCm(){const t=Number(this._settings.glow_radius_cm);return Number.isFinite(t)&&t>0?t:300}get _imperial(){return"mi"===this.hass?.config?.unit_system?.length}get _glowRadiusPlaceholder(){const t=this._glowRadiusCm;return this._imperial?String(Math.round(t/30.48*10)/10):String(t/100)}_renderGlowLayer(t){const e=this._fillColors,i=this._glowRadiusCm/this._cellCm*this._gridPitch,s=this._gridPitch,o=t.rooms.map(t=>({r:t,poly:we(t)})).filter(t=>!!t.poly),n=this._openingsR.filter(t=>"door"===t.type),r=[];for(const a of this._devices){if(a.space!==t.id)continue;const l=a.entities.find(t=>t.startsWith("light.")&&"on"===this.hass.states[t]?.state);if(!l)continue;const c=ei(this.hass.states[l],e.glow_light.c);if(!c)continue;const h=Number(a.marker?.glow_radius_cm),p=Number.isFinite(h)&&h>0?h/this._cellCm*this._gridPitch:i,d=this._pos(a),u=[...o].reverse().find(t=>this._pointInRoom([d.x,d.y],t.r));let _=null;if(u){const t=["M "+u.poly.map(t=>t[0]+" "+t[1]).join(" L ")+" Z"];for(const e of n){const i=Me([e.rx,e.ry],u.poly);if(!i||Math.hypot(i[0]-e.rx,i[1]-e.ry)>.75*s)continue;const n=e.angle*Math.PI/180,r=Math.cos(n)*e.rlen/2,a=Math.sin(n)*e.rlen/2,l=o.filter(t=>t!==u).map(t=>t.poly);if(!si([e.rx,e.ry],e.angle,[d.x,d.y],l,.6*s))continue;const c=ii([d.x,d.y],[e.rx-r,e.ry-a],[e.rx+r,e.ry+a],p);c&&t.push("M "+c.map(t=>t[0]+" "+t[1]).join(" L ")+" Z")}_=t.join(" ")}r.push({pos:d,c:c.c,alpha:e.glow_light.a*c.bri,clip:_,r:p})}return r.length?j`<defs>
|
</div>`}get _glowRadiusCm(){const t=Number(this._settings.glow_radius_cm);return Number.isFinite(t)&&t>0?t:300}get _imperial(){return"mi"===this.hass?.config?.unit_system?.length}get _glowRadiusPlaceholder(){const t=this._glowRadiusCm;return this._imperial?String(Math.round(t/30.48*10)/10):String(t/100)}_renderGlowLayer(t){const e=this._fillColors,i=this._glowRadiusCm/this._cellCm*this._gridPitch,s=this._gridPitch,o=t.rooms.map(t=>({r:t,poly:we(t)})).filter(t=>!!t.poly),n=this._openingsR.filter(t=>"door"===t.type),r=[];for(const a of this._devices){if(a.space!==t.id)continue;const l=a.entities.find(t=>t.startsWith("light.")&&"on"===this.hass.states[t]?.state);if(!l)continue;const c=ei(this.hass.states[l],e.glow_light.c);if(!c)continue;const h=Number(a.marker?.glow_radius_cm),p=Number.isFinite(h)&&h>0?h/this._cellCm*this._gridPitch:i,d=this._pos(a),u=[...o].reverse().find(t=>this._pointInRoom([d.x,d.y],t.r));let _=null;if(u){const t=["M "+u.poly.map(t=>t[0]+" "+t[1]).join(" L ")+" Z"];for(const e of n){const i=Me([e.rx,e.ry],u.poly);if(!i||Math.hypot(i[0]-e.rx,i[1]-e.ry)>.75*s)continue;const n=e.angle*Math.PI/180,r=Math.cos(n)*e.rlen/2,a=Math.sin(n)*e.rlen/2,l=o.filter(t=>t!==u).map(t=>t.poly);if(!si([e.rx,e.ry],e.angle,[d.x,d.y],l,.6*s))continue;const c=ii([d.x,d.y],[e.rx-r,e.ry-a],[e.rx+r,e.ry+a],p);c&&t.push("M "+c.map(t=>t[0]+" "+t[1]).join(" L ")+" Z")}_=t}r.push({pos:d,c:c.c,alpha:e.glow_light.a*c.bri,clip:_,r:p})}return r.length?j`<defs>
|
||||||
${r.map((t,e)=>j`
|
${r.map((t,e)=>j`
|
||||||
<radialGradient id="hp-glow-${e}">
|
<radialGradient id="hp-glow-${e}">
|
||||||
<stop offset="0%" stop-color="${t.c}" stop-opacity="${t.alpha.toFixed(3)}"></stop>
|
<stop offset="0%" stop-color="${t.c}" stop-opacity="${t.alpha.toFixed(3)}"></stop>
|
||||||
<stop offset="55%" stop-color="${t.c}" stop-opacity="${(.45*t.alpha).toFixed(3)}"></stop>
|
<stop offset="55%" stop-color="${t.c}" stop-opacity="${(.45*t.alpha).toFixed(3)}"></stop>
|
||||||
<stop offset="100%" stop-color="${t.c}" stop-opacity="0"></stop>
|
<stop offset="100%" stop-color="${t.c}" stop-opacity="0"></stop>
|
||||||
</radialGradient>
|
</radialGradient>
|
||||||
${t.clip?j`<clipPath id="hp-glowclip-${e}"><path d="${t.clip}"></path></clipPath>`:W}`)}
|
${t.clip?j`<clipPath id="hp-glowclip-${e}">${t.clip.map(t=>j`<path d="${t}"></path>`)}</clipPath>`:W}`)}
|
||||||
</defs>
|
</defs>
|
||||||
<g class="glowlayer">
|
<g class="glowlayer">
|
||||||
${r.map((t,e)=>j`<circle cx="${t.pos.x}" cy="${t.pos.y}" r="${t.r}"
|
${r.map((t,e)=>j`<circle cx="${t.pos.x}" cy="${t.pos.y}" r="${t.r}"
|
||||||
@@ -2195,4 +2195,4 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`}}ji.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}},ji.styles=$i,customElements.get("houseplan-card")||customElements.define("houseplan-card",ji),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.36.2 ","background:#3ea6ff;color:#04121f;font-weight:700","");
|
</div>`}}ji.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}},ji.styles=$i,customElements.get("houseplan-card")||customElements.define("houseplan-card",ji),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.36.3 ","background:#3ea6ff;color:#04121f;font-weight:700","");
|
||||||
|
|||||||
Vendored
+3
-3
@@ -1448,14 +1448,14 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
|
|||||||
<input type="range" min="0" max="100" .value=${String(Math.round(100*i.a))}
|
<input type="range" min="0" max="100" .value=${String(Math.round(100*i.a))}
|
||||||
@input=${e=>this._setFillColor(t,{a:Number(e.target.value)/100})} />
|
@input=${e=>this._setFillColor(t,{a:Number(e.target.value)/100})} />
|
||||||
<span class="opv">${Math.round(100*i.a)}%</span>
|
<span class="opv">${Math.round(100*i.a)}%</span>
|
||||||
</div>`}get _glowRadiusCm(){const t=Number(this._settings.glow_radius_cm);return Number.isFinite(t)&&t>0?t:300}get _imperial(){return"mi"===this.hass?.config?.unit_system?.length}get _glowRadiusPlaceholder(){const t=this._glowRadiusCm;return this._imperial?String(Math.round(t/30.48*10)/10):String(t/100)}_renderGlowLayer(t){const e=this._fillColors,i=this._glowRadiusCm/this._cellCm*this._gridPitch,s=this._gridPitch,o=t.rooms.map(t=>({r:t,poly:we(t)})).filter(t=>!!t.poly),n=this._openingsR.filter(t=>"door"===t.type),r=[];for(const a of this._devices){if(a.space!==t.id)continue;const l=a.entities.find(t=>t.startsWith("light.")&&"on"===this.hass.states[t]?.state);if(!l)continue;const c=ei(this.hass.states[l],e.glow_light.c);if(!c)continue;const h=Number(a.marker?.glow_radius_cm),p=Number.isFinite(h)&&h>0?h/this._cellCm*this._gridPitch:i,d=this._pos(a),u=[...o].reverse().find(t=>this._pointInRoom([d.x,d.y],t.r));let _=null;if(u){const t=["M "+u.poly.map(t=>t[0]+" "+t[1]).join(" L ")+" Z"];for(const e of n){const i=Me([e.rx,e.ry],u.poly);if(!i||Math.hypot(i[0]-e.rx,i[1]-e.ry)>.75*s)continue;const n=e.angle*Math.PI/180,r=Math.cos(n)*e.rlen/2,a=Math.sin(n)*e.rlen/2,l=o.filter(t=>t!==u).map(t=>t.poly);if(!si([e.rx,e.ry],e.angle,[d.x,d.y],l,.6*s))continue;const c=ii([d.x,d.y],[e.rx-r,e.ry-a],[e.rx+r,e.ry+a],p);c&&t.push("M "+c.map(t=>t[0]+" "+t[1]).join(" L ")+" Z")}_=t.join(" ")}r.push({pos:d,c:c.c,alpha:e.glow_light.a*c.bri,clip:_,r:p})}return r.length?j`<defs>
|
</div>`}get _glowRadiusCm(){const t=Number(this._settings.glow_radius_cm);return Number.isFinite(t)&&t>0?t:300}get _imperial(){return"mi"===this.hass?.config?.unit_system?.length}get _glowRadiusPlaceholder(){const t=this._glowRadiusCm;return this._imperial?String(Math.round(t/30.48*10)/10):String(t/100)}_renderGlowLayer(t){const e=this._fillColors,i=this._glowRadiusCm/this._cellCm*this._gridPitch,s=this._gridPitch,o=t.rooms.map(t=>({r:t,poly:we(t)})).filter(t=>!!t.poly),n=this._openingsR.filter(t=>"door"===t.type),r=[];for(const a of this._devices){if(a.space!==t.id)continue;const l=a.entities.find(t=>t.startsWith("light.")&&"on"===this.hass.states[t]?.state);if(!l)continue;const c=ei(this.hass.states[l],e.glow_light.c);if(!c)continue;const h=Number(a.marker?.glow_radius_cm),p=Number.isFinite(h)&&h>0?h/this._cellCm*this._gridPitch:i,d=this._pos(a),u=[...o].reverse().find(t=>this._pointInRoom([d.x,d.y],t.r));let _=null;if(u){const t=["M "+u.poly.map(t=>t[0]+" "+t[1]).join(" L ")+" Z"];for(const e of n){const i=Me([e.rx,e.ry],u.poly);if(!i||Math.hypot(i[0]-e.rx,i[1]-e.ry)>.75*s)continue;const n=e.angle*Math.PI/180,r=Math.cos(n)*e.rlen/2,a=Math.sin(n)*e.rlen/2,l=o.filter(t=>t!==u).map(t=>t.poly);if(!si([e.rx,e.ry],e.angle,[d.x,d.y],l,.6*s))continue;const c=ii([d.x,d.y],[e.rx-r,e.ry-a],[e.rx+r,e.ry+a],p);c&&t.push("M "+c.map(t=>t[0]+" "+t[1]).join(" L ")+" Z")}_=t}r.push({pos:d,c:c.c,alpha:e.glow_light.a*c.bri,clip:_,r:p})}return r.length?j`<defs>
|
||||||
${r.map((t,e)=>j`
|
${r.map((t,e)=>j`
|
||||||
<radialGradient id="hp-glow-${e}">
|
<radialGradient id="hp-glow-${e}">
|
||||||
<stop offset="0%" stop-color="${t.c}" stop-opacity="${t.alpha.toFixed(3)}"></stop>
|
<stop offset="0%" stop-color="${t.c}" stop-opacity="${t.alpha.toFixed(3)}"></stop>
|
||||||
<stop offset="55%" stop-color="${t.c}" stop-opacity="${(.45*t.alpha).toFixed(3)}"></stop>
|
<stop offset="55%" stop-color="${t.c}" stop-opacity="${(.45*t.alpha).toFixed(3)}"></stop>
|
||||||
<stop offset="100%" stop-color="${t.c}" stop-opacity="0"></stop>
|
<stop offset="100%" stop-color="${t.c}" stop-opacity="0"></stop>
|
||||||
</radialGradient>
|
</radialGradient>
|
||||||
${t.clip?j`<clipPath id="hp-glowclip-${e}"><path d="${t.clip}"></path></clipPath>`:W}`)}
|
${t.clip?j`<clipPath id="hp-glowclip-${e}">${t.clip.map(t=>j`<path d="${t}"></path>`)}</clipPath>`:W}`)}
|
||||||
</defs>
|
</defs>
|
||||||
<g class="glowlayer">
|
<g class="glowlayer">
|
||||||
${r.map((t,e)=>j`<circle cx="${t.pos.x}" cy="${t.pos.y}" r="${t.r}"
|
${r.map((t,e)=>j`<circle cx="${t.pos.x}" cy="${t.pos.y}" r="${t.r}"
|
||||||
@@ -2195,4 +2195,4 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`}}ji.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}},ji.styles=$i,customElements.get("houseplan-card")||customElements.define("houseplan-card",ji),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.36.2 ","background:#3ea6ff;color:#04121f;font-weight:700","");
|
</div>`}}ji.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}},ji.styles=$i,customElements.get("houseplan-card")||customElements.define("houseplan-card",ji),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.36.3 ","background:#3ea6ff;color:#04121f;font-weight:700","");
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.36.3 — 2026-07-23
|
||||||
|
- Glow: fixed dark wedges appearing INSIDE a lit room near some doorways.
|
||||||
|
The room outline and the door sectors were subpaths of a single clip path;
|
||||||
|
with opposite winding directions the nonzero fill rule cancelled their
|
||||||
|
overlap. Each contour is now its own clipPath child (children always
|
||||||
|
union), so sectors only ever ADD light.
|
||||||
|
|
||||||
## v1.36.2 — 2026-07-23
|
## v1.36.2 — 2026-07-23
|
||||||
- **Glow radius is now per source**: every device dialog gained a "Glow
|
- **Glow radius is now per source**: every device dialog gained a "Glow
|
||||||
radius" field (in your HA units; empty = the global default from general
|
radius" field (in your HA units; empty = the global default from general
|
||||||
|
|||||||
@@ -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]
|
(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
|
||||||
|
- [ ] Sector wedge fix (v1.36.3): door sectors never darken the light INSIDE
|
||||||
|
the room — room outline and sectors are separate clipPath children
|
||||||
|
(union), not subpaths of one nonzero path [auto]
|
||||||
- [ ] Per-source glow radius (v1.36.2): the device dialog has a "Glow radius"
|
- [ ] Per-source glow radius (v1.36.2): the device dialog has a "Glow radius"
|
||||||
field (HA units; empty = general-settings default shown as placeholder);
|
field (HA units; empty = general-settings default shown as placeholder);
|
||||||
an override changes that source's pool and door sectors only [auto]
|
an override changes that source's pool and door sectors only [auto]
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "houseplan-card",
|
"name": "houseplan-card",
|
||||||
"version": "1.36.2",
|
"version": "1.36.3",
|
||||||
"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",
|
||||||
|
|||||||
@@ -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.36.2';
|
const CARD_VERSION = '1.36.3';
|
||||||
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';
|
||||||
@@ -2683,7 +2683,7 @@ class HouseplanCard extends LitElement {
|
|||||||
.map((r) => ({ r, poly: roomPoly(r) }))
|
.map((r) => ({ r, poly: roomPoly(r) }))
|
||||||
.filter((x): x is { r: RoomCfg; poly: number[][] } => !!x.poly);
|
.filter((x): x is { r: RoomCfg; poly: number[][] } => !!x.poly);
|
||||||
const doors = this._openingsR.filter((o) => o.type === 'door');
|
const doors = this._openingsR.filter((o) => o.type === 'door');
|
||||||
const spots: { pos: { x: number; y: number }; c: string; alpha: number; clip: string | null; r: number }[] = [];
|
const spots: { pos: { x: number; y: number }; c: string; alpha: number; clip: string[] | null; r: number }[] = [];
|
||||||
for (const d of this._devices) {
|
for (const d of this._devices) {
|
||||||
if (d.space !== space.id) continue;
|
if (d.space !== space.id) continue;
|
||||||
const lightEid = d.entities.find(
|
const lightEid = d.entities.find(
|
||||||
@@ -2698,7 +2698,7 @@ class HouseplanCard extends LitElement {
|
|||||||
const pos = this._pos(d);
|
const pos = this._pos(d);
|
||||||
// innermost room under the source (islands win — reverse order)
|
// innermost room under the source (islands win — reverse order)
|
||||||
const home = [...polys].reverse().find((x) => this._pointInRoom([pos.x, pos.y], x.r));
|
const home = [...polys].reverse().find((x) => this._pointInRoom([pos.x, pos.y], x.r));
|
||||||
let clip: string | null = null;
|
let clip: string[] | null = null;
|
||||||
if (home) {
|
if (home) {
|
||||||
const shapes: string[] = ['M ' + home.poly.map((p) => p[0] + ' ' + p[1]).join(' L ') + ' Z'];
|
const shapes: string[] = ['M ' + home.poly.map((p) => p[0] + ' ' + p[1]).join(' L ') + ' Z'];
|
||||||
// doorways of this room spill light into neighbouring rooms only
|
// doorways of this room spill light into neighbouring rooms only
|
||||||
@@ -2713,7 +2713,11 @@ class HouseplanCard extends LitElement {
|
|||||||
const sector = doorSector([pos.x, pos.y], [o.rx - dx, o.ry - dy], [o.rx + dx, o.ry + dy], R);
|
const sector = doorSector([pos.x, pos.y], [o.rx - dx, o.ry - dy], [o.rx + dx, o.ry + dy], R);
|
||||||
if (sector) shapes.push('M ' + sector.map((p) => p[0] + ' ' + p[1]).join(' L ') + ' Z');
|
if (sector) shapes.push('M ' + sector.map((p) => p[0] + ' ' + p[1]).join(' L ') + ' Z');
|
||||||
}
|
}
|
||||||
clip = shapes.join(' ');
|
// IMPORTANT: separate <path> children — clipPath children always
|
||||||
|
// UNION. Joining the room and a sector into ONE path made the default
|
||||||
|
// nonzero fill-rule cancel their overlap when the windings opposed,
|
||||||
|
// punching a dark wedge INSIDE the room (field report + screenshot).
|
||||||
|
clip = shapes;
|
||||||
}
|
}
|
||||||
spots.push({ pos, c: glow.c, alpha: colors.glow_light.a * glow.bri, clip, r: R });
|
spots.push({ pos, c: glow.c, alpha: colors.glow_light.a * glow.bri, clip, r: R });
|
||||||
}
|
}
|
||||||
@@ -2725,7 +2729,7 @@ class HouseplanCard extends LitElement {
|
|||||||
<stop offset="55%" stop-color="${sp.c}" stop-opacity="${(sp.alpha * 0.45).toFixed(3)}"></stop>
|
<stop offset="55%" stop-color="${sp.c}" stop-opacity="${(sp.alpha * 0.45).toFixed(3)}"></stop>
|
||||||
<stop offset="100%" stop-color="${sp.c}" stop-opacity="0"></stop>
|
<stop offset="100%" stop-color="${sp.c}" stop-opacity="0"></stop>
|
||||||
</radialGradient>
|
</radialGradient>
|
||||||
${sp.clip ? svg`<clipPath id="hp-glowclip-${i}"><path d="${sp.clip}"></path></clipPath>` : nothing}`)}
|
${sp.clip ? svg`<clipPath id="hp-glowclip-${i}">${sp.clip.map((d) => svg`<path d="${d}"></path>`)}</clipPath>` : nothing}`)}
|
||||||
</defs>
|
</defs>
|
||||||
<g class="glowlayer">
|
<g class="glowlayer">
|
||||||
${spots.map((sp, i) => svg`<circle cx="${sp.pos.x}" cy="${sp.pos.y}" r="${sp.r}"
|
${spots.map((sp, i) => svg`<circle cx="${sp.pos.x}" cy="${sp.pos.y}" r="${sp.r}"
|
||||||
|
|||||||
Reference in New Issue
Block a user