feat: make Glow follow door opening state (#20)
Issue: #20 User-Visible: yes
@@ -1,111 +1,111 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"fingerprint": "c673cca61c08a44252a2303b34ba28b96411e9d5fddb18c938f79fbbc00ccd7f",
|
||||
"fingerprint": "20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019",
|
||||
"entry": "houseplan-card.js",
|
||||
"initialViewFiles": [
|
||||
"houseplan-assets/houseplan-card-CmrzFyoP.js",
|
||||
"houseplan-assets/houseplan-card-Cjdt1t7f.js",
|
||||
"houseplan-card.js"
|
||||
],
|
||||
"initialViewGzipBytes": 271168,
|
||||
"initialViewGzipBytes": 271455,
|
||||
"lazyFiles": [
|
||||
"houseplan-assets/de-B04VJMDv.js",
|
||||
"houseplan-assets/editor-C0gbLi11.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-BbpRSsiv.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BkJZgs7A.js",
|
||||
"houseplan-assets/space-deletion-BfZFFcpf.js"
|
||||
"houseplan-assets/de-DRFSDdUU.js",
|
||||
"houseplan-assets/editor-Rs89b855.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-BL8faVDv.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-Ci7QymAY.js",
|
||||
"houseplan-assets/space-deletion-1NoE8x_l.js"
|
||||
],
|
||||
"lazyGzipBytes": 165928,
|
||||
"lazyGzipBytes": 165917,
|
||||
"lazyEditorFiles": [
|
||||
"houseplan-assets/editor-C0gbLi11.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-BbpRSsiv.js",
|
||||
"houseplan-assets/space-deletion-BfZFFcpf.js"
|
||||
"houseplan-assets/editor-Rs89b855.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-BL8faVDv.js",
|
||||
"houseplan-assets/space-deletion-1NoE8x_l.js"
|
||||
],
|
||||
"lazyEditorGzipBytes": 136819,
|
||||
"lazyEditorGzipBytes": 136808,
|
||||
"lazyOnboardingFiles": [
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BkJZgs7A.js",
|
||||
"houseplan-assets/space-deletion-BfZFFcpf.js"
|
||||
"houseplan-assets/houseplan-onboarding-runtime-Ci7QymAY.js",
|
||||
"houseplan-assets/space-deletion-1NoE8x_l.js"
|
||||
],
|
||||
"lazyOnboardingGzipBytes": 7201,
|
||||
"lazyOnboardingGzipBytes": 7195,
|
||||
"lazyLocaleFiles": [
|
||||
"houseplan-assets/de-B04VJMDv.js"
|
||||
"houseplan-assets/de-DRFSDdUU.js"
|
||||
],
|
||||
"lazyLocaleGzipBytes": 22462,
|
||||
"lazyLocaleGzipBytes": 22465,
|
||||
"files": [
|
||||
{
|
||||
"path": "houseplan-assets/de-B04VJMDv.js",
|
||||
"sha256": "0b2029018d30a920e040e0e8cc0213031782aecec97550726a2832bcba4c571e",
|
||||
"path": "houseplan-assets/de-DRFSDdUU.js",
|
||||
"sha256": "93319bdb17d7b9902672342eb6daa15f32ab09658dd6415deb157decb0edf613",
|
||||
"rawBytes": 73919,
|
||||
"gzipBytes": 22462,
|
||||
"gzipBytes": 22465,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/editor-C0gbLi11.js",
|
||||
"sha256": "71b26e609aa270fa02c90b56757e82b9a081f23a3605eacd0dd3ae32e3bb5782",
|
||||
"path": "houseplan-assets/editor-Rs89b855.js",
|
||||
"sha256": "93bd34541456059f9dd5369dd9a51a5bd8a39f99089927d610bf6fe6cfbf87c5",
|
||||
"rawBytes": 3831,
|
||||
"gzipBytes": 1583,
|
||||
"gzipBytes": 1582,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-CmrzFyoP.js"
|
||||
"houseplan-assets/houseplan-card-Cjdt1t7f.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-card-CmrzFyoP.js",
|
||||
"sha256": "19d936972988d0561b927cad612ee34675783cf0d0c1bfcd7d796156aedc7305",
|
||||
"rawBytes": 956498,
|
||||
"gzipBytes": 270414,
|
||||
"path": "houseplan-assets/houseplan-card-Cjdt1t7f.js",
|
||||
"sha256": "10d36979e2f624b0dc5e98a445ea0c8dedec6469857b12dbd99c84f8dded4ee5",
|
||||
"rawBytes": 957284,
|
||||
"gzipBytes": 270703,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/de-B04VJMDv.js",
|
||||
"houseplan-assets/editor-C0gbLi11.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-BbpRSsiv.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BkJZgs7A.js"
|
||||
"houseplan-assets/de-DRFSDdUU.js",
|
||||
"houseplan-assets/editor-Rs89b855.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-BL8faVDv.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-Ci7QymAY.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-BbpRSsiv.js",
|
||||
"sha256": "cff3061e998268f86098047985b0c3b4f0d14740e8981b0a46e43ffa98a90590",
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-BL8faVDv.js",
|
||||
"sha256": "ea20c56b4b365bd0accc4cd07950322b5b9dda70bc15a17b5d69c14df0ed0a55",
|
||||
"rawBytes": 520292,
|
||||
"gzipBytes": 134682,
|
||||
"gzipBytes": 134675,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-CmrzFyoP.js",
|
||||
"houseplan-assets/space-deletion-BfZFFcpf.js"
|
||||
"houseplan-assets/houseplan-card-Cjdt1t7f.js",
|
||||
"houseplan-assets/space-deletion-1NoE8x_l.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-BkJZgs7A.js",
|
||||
"sha256": "bbf33f54ddf0b5c9025351822042f17615ef9195a180ddc7616c9c1da7f11142",
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-Ci7QymAY.js",
|
||||
"sha256": "f18f2ec9af6091f8f731a2c8d969f05cdd8e34dec1c76c35a11857f7ab1e9d3a",
|
||||
"rawBytes": 26356,
|
||||
"gzipBytes": 6647,
|
||||
"gzipBytes": 6644,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-CmrzFyoP.js",
|
||||
"houseplan-assets/space-deletion-BfZFFcpf.js"
|
||||
"houseplan-assets/houseplan-card-Cjdt1t7f.js",
|
||||
"houseplan-assets/space-deletion-1NoE8x_l.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/space-deletion-BfZFFcpf.js",
|
||||
"sha256": "ffae5e9921eacc23faf7db2551eb327f76f99677c549adf89ab3b9c7d5de27d3",
|
||||
"rawBytes": 911,
|
||||
"gzipBytes": 554,
|
||||
"path": "houseplan-assets/space-deletion-1NoE8x_l.js",
|
||||
"sha256": "20448d45c7f3cbeed2ca72852535b30c0b7dd5bf70d6b9c354fd71ca693abf84",
|
||||
"rawBytes": 906,
|
||||
"gzipBytes": 551,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-card.js",
|
||||
"sha256": "f1f688dceb0fc517658234151a7999de73f9111b8249b51a8997c6d21915d2b4",
|
||||
"sha256": "15b38319f0dc22546a9f747ae4c227b49f10b5f21595cdf2ce758e09a7402be5",
|
||||
"rawBytes": 1089,
|
||||
"gzipBytes": 754,
|
||||
"gzipBytes": 752,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-CmrzFyoP.js"
|
||||
"houseplan-assets/houseplan-card-Cjdt1t7f.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="c673cca61c08a44252a2303b34ba28b96411e9d5fddb18c938f79fbbc00ccd7f";import{c as e,l as o,t,c_ as s,A as a,c$ as i,d0 as l,d1 as n,E as r,b as c}from"./houseplan-card-CmrzFyoP.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019";import{c as e,l as o,t,c_ as s,A as a,c$ as i,d0 as l,d1 as n,E as r,b as c}from"./houseplan-card-Cjdt1t7f.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},_=this._schema,f=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._formData}
|
||||
.schema=${e}
|
||||
.computeLabel=${e=>h[e.name]||e.name}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>`;return c`
|
||||
${d(f.slice(0,3))}
|
||||
${_?c`<div class="default-floor-error" role="alert"
|
||||
${d(_.slice(0,3))}
|
||||
${f?c`<div class="default-floor-error" role="alert"
|
||||
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
|
||||
${t(s,"editor.default_floor_missing",{id:_})}
|
||||
${t(s,"editor.default_floor_missing",{id:f})}
|
||||
</div>`:a}
|
||||
${d(f.slice(3))}
|
||||
${d(_.slice(3))}
|
||||
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",h);
|
||||
@@ -1 +1 @@
|
||||
function e(e,o){return{id:e,title:o,plan_url:null,view_box:[0,0,1,1],rooms:[],wall_segments:[]}}function o(e="file"){const o="draw"===e;return{source:e,showBorders:o,showNames:o,displayTouched:!1}}function r(e,o){if(e.displayTouched)return{...e,source:o};const r="draw"===o;return{...e,source:o,showBorders:r,showNames:r}}function s(e,o,r){return{...e,[o]:r,displayTouched:!0}}function t(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),t=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),n=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&t.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:n,count:n.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="c673cca61c08a44252a2303b34ba28b96411e9d5fddb18c938f79fbbc00ccd7f";export{t as a,e as c,o as i,r as s,s as t};
|
||||
function e(e,o){return{id:e,title:o,plan_url:null,view_box:[0,0,1,1],rooms:[],wall_segments:[]}}function o(e="file"){const o="draw"===e;return{source:e,showBorders:o,showNames:o,displayTouched:!1}}function r(e,o){if(e.displayTouched)return{...e,source:o};const r="draw"===o;return{...e,source:o,showBorders:r,showNames:r}}function s(e,o,r){return{...e,[o]:r,displayTouched:!0}}function a(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),a=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),t=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&a.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:t,count:t.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019";export{a,e as c,o as i,r as s,s as t};
|
||||
@@ -1 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="c673cca61c08a44252a2303b34ba28b96411e9d5fddb18c938f79fbbc00ccd7f";try{await import("./houseplan-assets/houseplan-card-CmrzFyoP.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019";try{await import("./houseplan-assets/houseplan-card-Cjdt1t7f.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
|
||||
|
||||
@@ -144,6 +144,18 @@ const runtime = (includeAppliance = false) => {
|
||||
entity_id: 'sun.sun', state: 'above_horizon',
|
||||
attributes: { azimuth: 180, elevation: 24 },
|
||||
},
|
||||
// State-only YAML covers deliberately have no registry/device row. They
|
||||
// keep the Glow opening-state golden independent from entity-registry
|
||||
// discovery while exercising the same exact-reference projection as a
|
||||
// user's template cover.
|
||||
'cover.golden_door_closed': {
|
||||
entity_id: 'cover.golden_door_closed', state: 'closed',
|
||||
attributes: { friendly_name: 'Golden closed door', current_position: 0 },
|
||||
},
|
||||
'cover.golden_door_half': {
|
||||
entity_id: 'cover.golden_door_half', state: 'open',
|
||||
attributes: { friendly_name: 'Golden half-open door', current_position: 50 },
|
||||
},
|
||||
};
|
||||
// Keep sun.sun state-only on purpose. Core/runtime entities and YAML
|
||||
// entities without unique_id may have a live state without a registry row.
|
||||
|
||||
@@ -580,11 +580,20 @@ export const GOLDEN_SCENARIOS = Object.freeze([
|
||||
'light.golden_light_one': { state: 'on', attributes: { rgb_color: [255, 196, 112], brightness: 255 } },
|
||||
},
|
||||
extraOpenings: [
|
||||
{ id: 'light-door-second', type: 'door', x: 0.50, y: 0.32, angle: 90, length: 0.13 },
|
||||
{
|
||||
id: 'light-door-second', type: 'door', x: 0.50, y: 0.30,
|
||||
angle: 90, length: 0.13, contact: 'cover.golden_door_closed',
|
||||
},
|
||||
{
|
||||
id: 'light-door-third', type: 'door', x: 0.50, y: 0.76,
|
||||
angle: 90, length: 0.13, contact: 'cover.golden_door_half',
|
||||
},
|
||||
],
|
||||
layoutOverrides: { 'golden-light-one': { s: 'golden-lighting', x: 0.40, y: 0.48 } },
|
||||
// The golden is protection only if the receiving half actually contains
|
||||
// rendered light. A data-only check once let a visually empty baseline pass.
|
||||
// One frame contains the complete #20 matrix: the original unbound door is
|
||||
// fully open, the upper bound door is closed and the lower positional door
|
||||
// exposes 50%. Protection is meaningful only if the receiving half also
|
||||
// contains rendered light; a data-only check once let an empty baseline pass.
|
||||
warmPixelRegion: { x: 0.5, y: 0, w: 0.5, h: 1, minPixels: 2500, minRedBlueDelta: 25 },
|
||||
theme: 'dark', viewport: { width: 1000, height: 900 }, ...stage },
|
||||
{ id: 'lighting-custom-glow-light', fixture: 'visual', space: 'golden-lighting', mode: 'view',
|
||||
|
||||
@@ -310,8 +310,12 @@ const rasterFixture = await page.evaluate(async () => {
|
||||
const walls = [...edges.values()].map(({ a, b }) => ({
|
||||
key: wallKey(a, b), a, b, cm: 15,
|
||||
}));
|
||||
const dynamicContact = 'cover.glow_dynamic_door';
|
||||
const openings = [
|
||||
{ id: 'glow-raster-east', type: 'door', x: 0.55, y: 0.32, angle: 90, length: 0.09 },
|
||||
{
|
||||
id: 'glow-raster-east', type: 'door', x: 0.55, y: 0.32,
|
||||
angle: 90, length: 0.09, contact: dynamicContact,
|
||||
},
|
||||
{ id: 'glow-raster-south', type: 'door', x: 0.32, y: 0.58, angle: 0, length: 0.09 },
|
||||
];
|
||||
const wallSource = [...edges.values()]
|
||||
@@ -350,6 +354,14 @@ const rasterFixture = await page.evaluate(async () => {
|
||||
...states[sourceEid], state: 'on',
|
||||
attributes: { ...(states[sourceEid]?.attributes || {}), brightness: 255, rgb_color: [255, 196, 112] },
|
||||
};
|
||||
states[dynamicContact] = {
|
||||
entity_id: dynamicContact,
|
||||
state: 'open',
|
||||
attributes: { current_position: 100, friendly_name: 'Glow dynamic door' },
|
||||
last_changed: new Date().toISOString(),
|
||||
last_updated: new Date().toISOString(),
|
||||
context: { id: 'glow-dynamic-door', parent_id: null, user_id: null },
|
||||
};
|
||||
c.hass = { ...c.hass, states };
|
||||
c._cfgEpoch++;
|
||||
c._glowClipCache.clear();
|
||||
@@ -473,7 +485,10 @@ const rasterFixture = await page.evaluate(async () => {
|
||||
edge: edgePlan.map(({ offset, point }) => ({ offset, point: toStage(point) })),
|
||||
};
|
||||
}
|
||||
return { ready: true, litParts, sourceEid, sourceId: source.id, wallSource, doors, shadow };
|
||||
return {
|
||||
ready: true, litParts, sourceEid, sourceId: source.id, wallSource,
|
||||
dynamicContact, doors, shadow,
|
||||
};
|
||||
});
|
||||
|
||||
if (rasterFixture.ready) {
|
||||
@@ -580,6 +595,87 @@ if (rasterFixture.ready) {
|
||||
res.shadowEdgeIsCrisp = !!metrics.shadow
|
||||
&& metrics.shadow.high - metrics.shadow.low >= 8
|
||||
&& metrics.shadow.edgeWidth <= 4;
|
||||
// The east opening is a positional cover. Exercise live 100 → 0 → 50
|
||||
// transitions without clearing any Glow cache: the rendered pixels must
|
||||
// prove both state invalidation and the centre-preserving partial aperture.
|
||||
const setDoorPosition = async (position) => page.evaluate(async ({ contact, position: next }) => {
|
||||
const c = window.__card;
|
||||
const previous = c.hass.states[contact];
|
||||
c.hass = {
|
||||
...c.hass,
|
||||
states: {
|
||||
...c.hass.states,
|
||||
[contact]: {
|
||||
...previous,
|
||||
state: next > 0 ? 'open' : 'closed',
|
||||
attributes: { ...(previous?.attributes || {}), current_position: next },
|
||||
},
|
||||
},
|
||||
};
|
||||
await c.updateComplete;
|
||||
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||
}, { contact: rasterFixture.dynamicContact, position });
|
||||
await page.evaluate(async (sourceEid) => {
|
||||
const c = window.__card;
|
||||
const previous = c.hass.states[sourceEid];
|
||||
c.hass = {
|
||||
...c.hass,
|
||||
states: { ...c.hass.states, [sourceEid]: { ...previous, state: 'on' } },
|
||||
};
|
||||
await c.updateComplete;
|
||||
}, rasterFixture.sourceEid);
|
||||
await setDoorPosition(0);
|
||||
const doorClosed = await stage.screenshot({ animations: 'disabled' });
|
||||
await setDoorPosition(50);
|
||||
const doorHalf = await stage.screenshot({ animations: 'disabled' });
|
||||
const dynamicDoor = rasterFixture.doors.find((door) => door.id === 'glow-raster-east');
|
||||
const dynamicMetrics = dynamicDoor ? await page.evaluate(async ({ images64, door }) => {
|
||||
const decode = async (base64) => {
|
||||
const bytes = Uint8Array.from(atob(base64), (char) => char.charCodeAt(0));
|
||||
return createImageBitmap(new Blob([bytes], { type: 'image/png' }));
|
||||
};
|
||||
const images = await Promise.all(images64.map(decode));
|
||||
const width = images[0].width;
|
||||
const height = images[0].height;
|
||||
const pixels = images.map((image) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width; canvas.height = height;
|
||||
const context = canvas.getContext('2d', { willReadFrequently: true });
|
||||
context.drawImage(image, 0, 0);
|
||||
return context.getImageData(0, 0, width, height).data;
|
||||
});
|
||||
const luma = (data, point) => {
|
||||
const x = Math.max(0, Math.min(width - 1, Math.round(point[0])));
|
||||
const y = Math.max(0, Math.min(height - 1, Math.round(point[1])));
|
||||
const offset = (y * width + x) * 4;
|
||||
return data[offset] * 0.2126 + data[offset + 1] * 0.7152 + data[offset + 2] * 0.0722;
|
||||
};
|
||||
const [open, closed, half, off] = pixels;
|
||||
const delta = (data, point) => luma(data, point) - luma(off, point);
|
||||
const widthOf = (data) => {
|
||||
const lit = door.scan.filter(({ point }) => delta(data, point) >= 3);
|
||||
return lit.length ? lit[lit.length - 1].offset - lit[0].offset : 0;
|
||||
};
|
||||
return {
|
||||
openDelta: delta(open, door.sample),
|
||||
closedDelta: delta(closed, door.sample),
|
||||
openWidth: widthOf(open),
|
||||
halfWidth: widthOf(half),
|
||||
};
|
||||
}, {
|
||||
images64: [
|
||||
lightOn.toString('base64'), doorClosed.toString('base64'),
|
||||
doorHalf.toString('base64'), lightOff.toString('base64'),
|
||||
],
|
||||
door: dynamicDoor,
|
||||
}) : null;
|
||||
console.log('Dynamic door Glow metrics:', JSON.stringify(dynamicMetrics));
|
||||
res.closedDoorBlocksGlow = !!dynamicMetrics
|
||||
&& dynamicMetrics.openDelta >= 8 && dynamicMetrics.closedDelta <= 3;
|
||||
res.partialDoorNarrowsGlow = !!dynamicMetrics
|
||||
&& dynamicMetrics.halfWidth > 0
|
||||
&& dynamicMetrics.halfWidth < dynamicMetrics.openWidth;
|
||||
await setDoorPosition(100);
|
||||
const sourceInsideWall = await page.evaluate(async ({ sourceEid, sourceId, wallSource }) => {
|
||||
if (!wallSource) return false;
|
||||
const c = window.__card;
|
||||
@@ -621,6 +717,8 @@ if (rasterFixture.ready) {
|
||||
res.shadowSamplesAreValid = false;
|
||||
res.occluderCastsShadow = false;
|
||||
res.shadowEdgeIsCrisp = false;
|
||||
res.closedDoorBlocksGlow = false;
|
||||
res.partialDoorNarrowsGlow = false;
|
||||
res.sourceInsideWallSuppressed = false;
|
||||
}
|
||||
// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1)
|
||||
|
||||
@@ -1,111 +1,111 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"fingerprint": "c673cca61c08a44252a2303b34ba28b96411e9d5fddb18c938f79fbbc00ccd7f",
|
||||
"fingerprint": "20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019",
|
||||
"entry": "houseplan-card.js",
|
||||
"initialViewFiles": [
|
||||
"houseplan-assets/houseplan-card-CmrzFyoP.js",
|
||||
"houseplan-assets/houseplan-card-Cjdt1t7f.js",
|
||||
"houseplan-card.js"
|
||||
],
|
||||
"initialViewGzipBytes": 271168,
|
||||
"initialViewGzipBytes": 271455,
|
||||
"lazyFiles": [
|
||||
"houseplan-assets/de-B04VJMDv.js",
|
||||
"houseplan-assets/editor-C0gbLi11.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-BbpRSsiv.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BkJZgs7A.js",
|
||||
"houseplan-assets/space-deletion-BfZFFcpf.js"
|
||||
"houseplan-assets/de-DRFSDdUU.js",
|
||||
"houseplan-assets/editor-Rs89b855.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-BL8faVDv.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-Ci7QymAY.js",
|
||||
"houseplan-assets/space-deletion-1NoE8x_l.js"
|
||||
],
|
||||
"lazyGzipBytes": 165928,
|
||||
"lazyGzipBytes": 165917,
|
||||
"lazyEditorFiles": [
|
||||
"houseplan-assets/editor-C0gbLi11.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-BbpRSsiv.js",
|
||||
"houseplan-assets/space-deletion-BfZFFcpf.js"
|
||||
"houseplan-assets/editor-Rs89b855.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-BL8faVDv.js",
|
||||
"houseplan-assets/space-deletion-1NoE8x_l.js"
|
||||
],
|
||||
"lazyEditorGzipBytes": 136819,
|
||||
"lazyEditorGzipBytes": 136808,
|
||||
"lazyOnboardingFiles": [
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BkJZgs7A.js",
|
||||
"houseplan-assets/space-deletion-BfZFFcpf.js"
|
||||
"houseplan-assets/houseplan-onboarding-runtime-Ci7QymAY.js",
|
||||
"houseplan-assets/space-deletion-1NoE8x_l.js"
|
||||
],
|
||||
"lazyOnboardingGzipBytes": 7201,
|
||||
"lazyOnboardingGzipBytes": 7195,
|
||||
"lazyLocaleFiles": [
|
||||
"houseplan-assets/de-B04VJMDv.js"
|
||||
"houseplan-assets/de-DRFSDdUU.js"
|
||||
],
|
||||
"lazyLocaleGzipBytes": 22462,
|
||||
"lazyLocaleGzipBytes": 22465,
|
||||
"files": [
|
||||
{
|
||||
"path": "houseplan-assets/de-B04VJMDv.js",
|
||||
"sha256": "0b2029018d30a920e040e0e8cc0213031782aecec97550726a2832bcba4c571e",
|
||||
"path": "houseplan-assets/de-DRFSDdUU.js",
|
||||
"sha256": "93319bdb17d7b9902672342eb6daa15f32ab09658dd6415deb157decb0edf613",
|
||||
"rawBytes": 73919,
|
||||
"gzipBytes": 22462,
|
||||
"gzipBytes": 22465,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/editor-C0gbLi11.js",
|
||||
"sha256": "71b26e609aa270fa02c90b56757e82b9a081f23a3605eacd0dd3ae32e3bb5782",
|
||||
"path": "houseplan-assets/editor-Rs89b855.js",
|
||||
"sha256": "93bd34541456059f9dd5369dd9a51a5bd8a39f99089927d610bf6fe6cfbf87c5",
|
||||
"rawBytes": 3831,
|
||||
"gzipBytes": 1583,
|
||||
"gzipBytes": 1582,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-CmrzFyoP.js"
|
||||
"houseplan-assets/houseplan-card-Cjdt1t7f.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-card-CmrzFyoP.js",
|
||||
"sha256": "19d936972988d0561b927cad612ee34675783cf0d0c1bfcd7d796156aedc7305",
|
||||
"rawBytes": 956498,
|
||||
"gzipBytes": 270414,
|
||||
"path": "houseplan-assets/houseplan-card-Cjdt1t7f.js",
|
||||
"sha256": "10d36979e2f624b0dc5e98a445ea0c8dedec6469857b12dbd99c84f8dded4ee5",
|
||||
"rawBytes": 957284,
|
||||
"gzipBytes": 270703,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/de-B04VJMDv.js",
|
||||
"houseplan-assets/editor-C0gbLi11.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-BbpRSsiv.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BkJZgs7A.js"
|
||||
"houseplan-assets/de-DRFSDdUU.js",
|
||||
"houseplan-assets/editor-Rs89b855.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-BL8faVDv.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-Ci7QymAY.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-BbpRSsiv.js",
|
||||
"sha256": "cff3061e998268f86098047985b0c3b4f0d14740e8981b0a46e43ffa98a90590",
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-BL8faVDv.js",
|
||||
"sha256": "ea20c56b4b365bd0accc4cd07950322b5b9dda70bc15a17b5d69c14df0ed0a55",
|
||||
"rawBytes": 520292,
|
||||
"gzipBytes": 134682,
|
||||
"gzipBytes": 134675,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-CmrzFyoP.js",
|
||||
"houseplan-assets/space-deletion-BfZFFcpf.js"
|
||||
"houseplan-assets/houseplan-card-Cjdt1t7f.js",
|
||||
"houseplan-assets/space-deletion-1NoE8x_l.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-BkJZgs7A.js",
|
||||
"sha256": "bbf33f54ddf0b5c9025351822042f17615ef9195a180ddc7616c9c1da7f11142",
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-Ci7QymAY.js",
|
||||
"sha256": "f18f2ec9af6091f8f731a2c8d969f05cdd8e34dec1c76c35a11857f7ab1e9d3a",
|
||||
"rawBytes": 26356,
|
||||
"gzipBytes": 6647,
|
||||
"gzipBytes": 6644,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-CmrzFyoP.js",
|
||||
"houseplan-assets/space-deletion-BfZFFcpf.js"
|
||||
"houseplan-assets/houseplan-card-Cjdt1t7f.js",
|
||||
"houseplan-assets/space-deletion-1NoE8x_l.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/space-deletion-BfZFFcpf.js",
|
||||
"sha256": "ffae5e9921eacc23faf7db2551eb327f76f99677c549adf89ab3b9c7d5de27d3",
|
||||
"rawBytes": 911,
|
||||
"gzipBytes": 554,
|
||||
"path": "houseplan-assets/space-deletion-1NoE8x_l.js",
|
||||
"sha256": "20448d45c7f3cbeed2ca72852535b30c0b7dd5bf70d6b9c354fd71ca693abf84",
|
||||
"rawBytes": 906,
|
||||
"gzipBytes": 551,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-card.js",
|
||||
"sha256": "f1f688dceb0fc517658234151a7999de73f9111b8249b51a8997c6d21915d2b4",
|
||||
"sha256": "15b38319f0dc22546a9f747ae4c227b49f10b5f21595cdf2ce758e09a7402be5",
|
||||
"rawBytes": 1089,
|
||||
"gzipBytes": 754,
|
||||
"gzipBytes": 752,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-CmrzFyoP.js"
|
||||
"houseplan-assets/houseplan-card-Cjdt1t7f.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="c673cca61c08a44252a2303b34ba28b96411e9d5fddb18c938f79fbbc00ccd7f";import{c as e,l as o,t,c_ as s,A as a,c$ as i,d0 as l,d1 as n,E as r,b as c}from"./houseplan-card-CmrzFyoP.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019";import{c as e,l as o,t,c_ as s,A as a,c$ as i,d0 as l,d1 as n,E as r,b as c}from"./houseplan-card-Cjdt1t7f.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},_=this._schema,f=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._formData}
|
||||
.schema=${e}
|
||||
.computeLabel=${e=>h[e.name]||e.name}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>`;return c`
|
||||
${d(f.slice(0,3))}
|
||||
${_?c`<div class="default-floor-error" role="alert"
|
||||
${d(_.slice(0,3))}
|
||||
${f?c`<div class="default-floor-error" role="alert"
|
||||
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
|
||||
${t(s,"editor.default_floor_missing",{id:_})}
|
||||
${t(s,"editor.default_floor_missing",{id:f})}
|
||||
</div>`:a}
|
||||
${d(f.slice(3))}
|
||||
${d(_.slice(3))}
|
||||
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",h);
|
||||
@@ -1 +1 @@
|
||||
function e(e,o){return{id:e,title:o,plan_url:null,view_box:[0,0,1,1],rooms:[],wall_segments:[]}}function o(e="file"){const o="draw"===e;return{source:e,showBorders:o,showNames:o,displayTouched:!1}}function r(e,o){if(e.displayTouched)return{...e,source:o};const r="draw"===o;return{...e,source:o,showBorders:r,showNames:r}}function s(e,o,r){return{...e,[o]:r,displayTouched:!0}}function t(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),t=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),n=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&t.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:n,count:n.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="c673cca61c08a44252a2303b34ba28b96411e9d5fddb18c938f79fbbc00ccd7f";export{t as a,e as c,o as i,r as s,s as t};
|
||||
function e(e,o){return{id:e,title:o,plan_url:null,view_box:[0,0,1,1],rooms:[],wall_segments:[]}}function o(e="file"){const o="draw"===e;return{source:e,showBorders:o,showNames:o,displayTouched:!1}}function r(e,o){if(e.displayTouched)return{...e,source:o};const r="draw"===o;return{...e,source:o,showBorders:r,showNames:r}}function s(e,o,r){return{...e,[o]:r,displayTouched:!0}}function a(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),a=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),t=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&a.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:t,count:t.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019";export{a,e as c,o as i,r as s,s as t};
|
||||
@@ -1 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="c673cca61c08a44252a2303b34ba28b96411e9d5fddb18c938f79fbbc00ccd7f";try{await import("./houseplan-assets/houseplan-card-CmrzFyoP.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019";try{await import("./houseplan-assets/houseplan-card-Cjdt1t7f.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
|
||||
|
||||
@@ -1259,10 +1259,11 @@ its configured space.
|
||||
`_lightBarriers` collects everything opaque: the wall bodies exactly as the
|
||||
plan draws them (`wallBodiesGeometry`, real thickness, mitred junctions),
|
||||
every independent body (partition, column, draft), and the bare outline of
|
||||
any edge that carries no thickness. It cuts out the exceptions: doorways
|
||||
and gates — cut through the masonry, so an opening is a real gap
|
||||
between two jamb faces and a beam is narrowed by the returns of a thick wall
|
||||
— plus dashed zero-thickness walls. Solid zero-thickness walls instead add
|
||||
any edge that carries no thickness. It cuts out the exceptions: interior
|
||||
doorways and gates according to their resolved live opening amount — a
|
||||
closed bound opening keeps the masonry, while a positional cover cuts a
|
||||
centre-aligned fraction between the jambs — and saved passages, which remain
|
||||
fully open, plus dashed zero-thickness walls. Solid zero-thickness walls instead add
|
||||
their exact axes as zero-area barriers. Windows stay
|
||||
solid, so an indoor lamp never washes the street; the light's masonry is cut
|
||||
by passages only and therefore differs on purpose from the drawn one. So does
|
||||
@@ -1289,10 +1290,12 @@ its configured space.
|
||||
open-zone graph and no shadow mask left in the light path.
|
||||
|
||||
Barriers are cached per space by a fingerprint of their complete geometry
|
||||
(every body point, both wall endpoints and scale inputs), never by
|
||||
`_cfgEpoch`: the epoch lags behind geometry edited in place, and a stale
|
||||
barrier set is invisible — the plan simply keeps lighting through a wall that
|
||||
now exists. The same fingerprint keys the per-source region cache. A cached per-
|
||||
(every body point, both wall endpoints and scale inputs) plus a sorted
|
||||
signature of bound interior door/gate opening amounts, never by `_cfgEpoch`:
|
||||
the epoch lags behind geometry edited in place, and a stale barrier set is
|
||||
invisible — the plan simply keeps lighting through a wall or closed door that
|
||||
now exists. Unrelated HA updates retain the same signature and reuse the
|
||||
barrier set. The combined fingerprint keys the per-source region cache. A cached per-
|
||||
`Document` raster probe verifies actual SVG screen pixels rather than trusting
|
||||
CSS syntax support; pending/unsupported/error/timeout states render with
|
||||
deterministic normal blending and a successful probe requests one update.
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
first frame was also the last one
|
||||
([#358](https://github.com/Matysh/houseplan-card/issues/358)).
|
||||
|
||||
- Glow now follows the live state of bound interior doors and gates: a closed
|
||||
opening blocks light, an open one passes it, and a positional cover exposes
|
||||
the matching fraction of the aperture. Passages remain open, windows and
|
||||
exterior openings remain opaque, and unrelated Home Assistant updates reuse
|
||||
the existing light geometry ([#20](https://github.com/Matysh/houseplan-card/issues/20)).
|
||||
|
||||
- Tapping a device on a freshly opened page works again without first
|
||||
visiting any editor screen. A wall switch controlling virtual light sources
|
||||
(and every other tap-to-toggle device) silently ignored the tap on a cold
|
||||
|
||||
@@ -14,6 +14,13 @@
|
||||
загрузила, — первый кадр оказывался последним
|
||||
([#358](https://github.com/Matysh/houseplan-card/issues/358)).
|
||||
|
||||
- Glow теперь учитывает текущее состояние привязанных внутренних дверей и
|
||||
ворот: закрытый проём останавливает свет, открытый пропускает его, а cover с
|
||||
позицией открывает соответствующую долю прохода. Открытые проходы остаются
|
||||
прозрачными, окна и наружные проёмы — непрозрачными; посторонние обновления
|
||||
Home Assistant переиспользуют готовую геометрию света
|
||||
([#20](https://github.com/Matysh/houseplan-card/issues/20)).
|
||||
|
||||
- Нажатие на устройство на только что открытой странице снова работает без
|
||||
предварительного захода в какой-либо редактор. Выключатель, управляющий
|
||||
виртуальными источниками света (и любое другое устройство с тап-переключением),
|
||||
|
||||
@@ -20,7 +20,8 @@ all. None of those states is expressible now.
|
||||
## What stops light, and what does not
|
||||
|
||||
`_lightBarriers()` (`src/houseplan-card.ts`) builds the barrier set once per
|
||||
plan geometry and shares it between every lamp in the space.
|
||||
plan geometry and relevant opening-state signature, then shares it between
|
||||
every lamp in the space.
|
||||
|
||||
**Opaque**
|
||||
|
||||
@@ -34,8 +35,11 @@ plan geometry and shares it between every lamp in the space.
|
||||
|
||||
**Transparent**
|
||||
|
||||
- doorways, gates and saved `passage` openings — cut through the masonry, so an opening is a real
|
||||
gap between two jamb faces and a thick wall's returns narrow the beam;
|
||||
- doorways and gates to floor on both sides, but only by their resolved opening
|
||||
amount. An unbound opening is fully transparent, a bound closed one is opaque,
|
||||
and a positional cover cuts a centre-aligned fraction of the full aperture;
|
||||
- saved `passage` openings — always cut through the masonry, so an opening is a
|
||||
real gap between two jamb faces and a thick wall's returns narrow the beam;
|
||||
- every zero-thickness wall when its space uses the **Dashed** style.
|
||||
|
||||
The setting is intentionally one semantic switch, not just paint. Missing or
|
||||
@@ -52,7 +56,12 @@ input is only a v8 read projection and migrates to ordinary `cm:0` atoms.
|
||||
sides are floor; otherwise a front door glows halfway — up to the centreline,
|
||||
where the room polygon ends — and the plan shows a lit doorway to nowhere.
|
||||
|
||||
The classifier is an explicit `door | gate | passage` allowlist. Any unknown
|
||||
The classifier is an explicit `door | gate | passage` allowlist. Door and gate
|
||||
state is resolved by the same `openingAmount()` used for their visible symbol:
|
||||
binary contacts yield zero or full aperture, finite `current_position` yields
|
||||
`clamp(position / 100)`, and `invert` mirrors a known amount. Missing,
|
||||
disabled, `unknown` or `unavailable` contacts preserve the static-plan fallback
|
||||
of a fully open door/gate rather than manufacturing a closure. Any unknown
|
||||
future opening type remains opaque until its physical semantics are reviewed;
|
||||
it never inherits transparency merely by not being a window.
|
||||
|
||||
@@ -129,11 +138,13 @@ pass over the layer costs a fifth of one blurred mask per source.
|
||||
|
||||
## Caching
|
||||
|
||||
Barriers are keyed by a fingerprint of their own geometry, never by
|
||||
`_cfgEpoch`: the epoch lags behind geometry edited in place, and a stale barrier
|
||||
set is invisible — the plan simply keeps lighting through a wall that now
|
||||
exists. The same fingerprint, plus position and radius, keys the per-source
|
||||
region cache (`_glowClipCache`).
|
||||
Barriers are keyed by their geometry fingerprint plus a compact, sorted
|
||||
signature of bound interior door/gate opening amounts, never by `_cfgEpoch`:
|
||||
the epoch lags behind geometry edited in place, and a stale barrier set is
|
||||
invisible — the plan simply keeps lighting through a wall or closed door that
|
||||
now exists. Unrelated HA updates preserve the signature and hit the bounded
|
||||
barrier cache. The combined fingerprint, plus source position and radius, keys
|
||||
the per-source region cache (`_glowClipCache`).
|
||||
|
||||
The masonry boolean receives room walls after passage cuts plus the cached
|
||||
joined independent body set. Its outer/hole rings are the authoritative
|
||||
@@ -216,6 +227,9 @@ The complete UI and runtime truth table lives in
|
||||
exterior opening masonry, wall bodies, partitions and columns are fail-dark,
|
||||
while a real interior passage remains a valid source position; the rendered
|
||||
Glow path is pinned to that shared guard.
|
||||
- `test/logic.test.mjs` and `test/physical-geometry.test.mjs` — the shared
|
||||
opening amount, stable state signature and centre-preserving partial cut for
|
||||
contour and independent walls.
|
||||
- `demo/smoke_glow.mjs` — pixels on a rendered plan: the aperture itself is lit,
|
||||
the floor behind a door is lit, the visible beam is no wider than twice the
|
||||
opening, there is no light behind a column while the floor beside it is lit,
|
||||
|
||||
|
Before Width: | Height: | Size: 198 KiB After Width: | Height: | Size: 192 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 343 KiB After Width: | Height: | Size: 336 KiB |
|
Before Width: | Height: | Size: 334 KiB After Width: | Height: | Size: 327 KiB |
|
Before Width: | Height: | Size: 319 KiB After Width: | Height: | Size: 289 KiB |
|
Before Width: | Height: | Size: 317 KiB After Width: | Height: | Size: 291 KiB |
|
Before Width: | Height: | Size: 296 KiB After Width: | Height: | Size: 290 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 134 KiB |
@@ -3,7 +3,7 @@
|
||||
"fixture": "synthetic-only",
|
||||
"chromium": "151.0.7922.34",
|
||||
"oxipng": null,
|
||||
"sourceFingerprint": "2bb218477956888a84706e4a8a76248b823d70dc26fc80bcc5272c30e4556a38",
|
||||
"sourceFingerprint": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
|
||||
"captureScriptSha256": "fe76c8e2e27501e9a377d7aa15dd6cba6a018d55c07deb602dbee097f0a0a5ba",
|
||||
"command": "npm run build && node demo/docs/capture.mjs",
|
||||
"scenarios": {
|
||||
@@ -15,8 +15,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "2bb218477956888a84706e4a8a76248b823d70dc26fc80bcc5272c30e4556a38",
|
||||
"imageSha256": "78d328498bd17a9ee1ec44b389d244b61fca9ac52c9aad6631aa85c53233fec0"
|
||||
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
|
||||
"imageSha256": "36223106c073f07d8cc3ecf8eaab37192ebb2687daba65c5c21047d0b7890de0"
|
||||
},
|
||||
"view-touch": {
|
||||
"file": "02-view-touch.png",
|
||||
@@ -26,8 +26,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "2bb218477956888a84706e4a8a76248b823d70dc26fc80bcc5272c30e4556a38",
|
||||
"imageSha256": "3be239a5a494f6776540a9c0049694ee6f9d2ba927c8bcb4b26ee5a69a228a7d"
|
||||
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
|
||||
"imageSha256": "a4aa3af48578ee41594a9b1a6e98b4d39f3324a949a9fd9e63d5854576d5db85"
|
||||
},
|
||||
"space-create": {
|
||||
"file": "03-space-create.png",
|
||||
@@ -37,8 +37,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "2bb218477956888a84706e4a8a76248b823d70dc26fc80bcc5272c30e4556a38",
|
||||
"imageSha256": "a55b463055a565892ca139014e314a8f63fb5301c87fff2e48d7e69210fe9b6c"
|
||||
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
|
||||
"imageSha256": "0dc024726327f60f4a9ceaf3044381691f81f1090af81d1812e870f22d9343ba"
|
||||
},
|
||||
"room-contour-close": {
|
||||
"file": "04-room-contour-close.png",
|
||||
@@ -48,8 +48,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "2bb218477956888a84706e4a8a76248b823d70dc26fc80bcc5272c30e4556a38",
|
||||
"imageSha256": "dfad9a27371cfa397a3fd8dce7005f42d24649743f5322105ee894067d3a9f16"
|
||||
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
|
||||
"imageSha256": "44cfaa95bd51e2cd628400e28db0ad8b2f0cd904385845bd402494f3f5c0d93c"
|
||||
},
|
||||
"plan-context-tray": {
|
||||
"file": "05-plan-context-tray.png",
|
||||
@@ -59,8 +59,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "2bb218477956888a84706e4a8a76248b823d70dc26fc80bcc5272c30e4556a38",
|
||||
"imageSha256": "b75f348c632296da5c2fc142d53df613f757e1516acac315be50f5b4557729cc"
|
||||
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
|
||||
"imageSha256": "e0662429b423fb74151b583dcc2c8635b001b637d03bbf7a6b16aec46399c3f8"
|
||||
},
|
||||
"device-editor": {
|
||||
"file": "06-device-editor.png",
|
||||
@@ -70,8 +70,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "2bb218477956888a84706e4a8a76248b823d70dc26fc80bcc5272c30e4556a38",
|
||||
"imageSha256": "6b9cd281450e7782cff292f11efc58d227bedd0df86afc3be987feb2edec9dc3"
|
||||
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
|
||||
"imageSha256": "d0ffd31ce80bfde21ab75da356a5fc1af38246f2b301030880320620c228d89d"
|
||||
},
|
||||
"device-display-preview": {
|
||||
"file": "06-device-display-preview.png",
|
||||
@@ -81,8 +81,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "2bb218477956888a84706e4a8a76248b823d70dc26fc80bcc5272c30e4556a38",
|
||||
"imageSha256": "6f628abe6b89d7c4b352e6d73ac0b126526f6fdba5927bb774c5b884e484ca19"
|
||||
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
|
||||
"imageSha256": "2cdabae1f89c3286e4fac0ce30f757ee1690b707ab8a5488748b7cd420626160"
|
||||
},
|
||||
"background-editor": {
|
||||
"file": "07-background-editor.png",
|
||||
@@ -92,8 +92,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "2bb218477956888a84706e4a8a76248b823d70dc26fc80bcc5272c30e4556a38",
|
||||
"imageSha256": "a1d1a905f1410f73231d2ea5d1dfbd8ae79c2fe8b0a79cff87ebb977ac7a3103"
|
||||
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
|
||||
"imageSha256": "30147bb00a90eea7136b4cee30995f6e6a9217b5132f3e8d3ad7471413b1af8a"
|
||||
},
|
||||
"room-card": {
|
||||
"file": "08-room-card.png",
|
||||
@@ -103,8 +103,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "2bb218477956888a84706e4a8a76248b823d70dc26fc80bcc5272c30e4556a38",
|
||||
"imageSha256": "48a5685039a5e0de2a28190d857b193c86e2e63606e3c7a8c40fc9d2b190da4e"
|
||||
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
|
||||
"imageSha256": "029a3e69ec647a8a370d99e6bb7f9225833c526739076022f6b52ba54bff30ea"
|
||||
},
|
||||
"device-info": {
|
||||
"file": "09-device-info.png",
|
||||
@@ -114,8 +114,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "2bb218477956888a84706e4a8a76248b823d70dc26fc80bcc5272c30e4556a38",
|
||||
"imageSha256": "29a342811aaedaebc54ba337792d60cb454a7cf4c3063b210f6c4bafe0a4d87b"
|
||||
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
|
||||
"imageSha256": "dd492f53150b7149085daada5cce9eeae9bde9e7ea1d86679a54b3041f72f517"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,8 @@ import {
|
||||
segmentCm, formatLength, roomEdges, roomPoly, paperRoomShapes, pointStrictlyInside, roomsOverlap,
|
||||
pointOnBoundary, mergeRooms, splitRoomPath, polygonArea, closestPointOnBoundary, pointStrictlyInside as ptInside, islandsOf, sharedBoundary, distToSegment, outlineWithout, cutSegments, alignGuides, segmentAngle, is45, isExact45Vector, type AlignGuide, swipeTarget, clampScale, migratePdfUrls, roomFillModeOf, roomGlowOf, contentUrl,
|
||||
snapToWall, snapPointAlongPoly, openingAmount, openingShoulders, interiorPoint,
|
||||
isInteriorLightOpeningType, openingEntityReferences, filterOpeningEntityCandidates,
|
||||
isInteriorLightOpeningType, openingLightApertureLength, openingLightStateSignature,
|
||||
openingEntityReferences, filterOpeningEntityCandidates,
|
||||
poleOfInaccessibility, subst,
|
||||
averageLqi, fitView, declump, safeUrl, floorsOf, type FloorInfo,
|
||||
stateIcon, lightColorOf, parseRoomRef, diffNewDevices, resolveGlowValues, resolveGlowAppearance,
|
||||
@@ -156,13 +157,13 @@ import {
|
||||
directionalOccluders, floorMinusBodies, geometryArea, geometryOuterRings,
|
||||
geometryAllRings, intersectionPaths, partitionBody, polyclipPathD,
|
||||
pointInOpaquePlanBody, pointInPhysicalBody, sameColumnPlacement,
|
||||
physicalBodyParts,
|
||||
physicalBodyParts, scalePartitionOpeningCut,
|
||||
type PartitionOpeningCut,
|
||||
} from './physical-geometry';
|
||||
import {
|
||||
hostedOpeningIntervalsOverlap, materializePartitionOpening,
|
||||
partitionOpeningJambMargin, partitionOpeningNeedsStrictValidation,
|
||||
partitionOpeningFace,
|
||||
partitionOpeningCut, partitionOpeningFace,
|
||||
partitionPlacementIntervals, resolvePartitionOpeningCompat, resolvePartitionOpeningStrict,
|
||||
type PartitionOpeningOrphanReason, type ResolvedPartitionOpening,
|
||||
} from './partition-openings';
|
||||
@@ -10260,7 +10261,8 @@ export class HouseplanCard extends LitElement {
|
||||
* indoor lamp must not wash the street, so the light's masonry is cut by
|
||||
* passages only and differs on purpose from the drawn one.
|
||||
*
|
||||
* The result depends only on the plan, so it is shared by every lamp.
|
||||
* The result depends on the plan plus the small signature of bound interior
|
||||
* door/gate states, so it is shared by every lamp and unrelated HA updates.
|
||||
*/
|
||||
private _lightBarriers(
|
||||
space: SpaceModel, polys: { r: RoomCfg; poly: number[][] }[],
|
||||
@@ -10274,15 +10276,38 @@ export class HouseplanCard extends LitElement {
|
||||
// booleans and wall topology. `_cfgEpoch` alone is deliberately not used:
|
||||
// old editor paths and third-party harnesses can still mutate a structure
|
||||
// before advancing it.
|
||||
const fingerprint = contentFingerprint([raw, this._cellCm, this._gridPitch]);
|
||||
const geometryFingerprint = contentFingerprint([raw, this._cellCm, this._gridPitch]);
|
||||
// Gates are door-like: their different symbol must not change how light
|
||||
// crosses the clear opening.
|
||||
// An opening is transparent only where it leads from floor to floor. A
|
||||
// front door has nothing behind it to light, so for light it is masonry
|
||||
// like a window — otherwise its tunnel glows halfway, up to the centreline
|
||||
// where the room polygon ends, and the plan shows a lit doorway to nowhere.
|
||||
const probe = Math.max(this._cmToUnits(10), this._gridPitch * 0.5);
|
||||
const onFloor = (point: number[]): boolean =>
|
||||
polys.some((x) => this._pointInRoom(point, x.r));
|
||||
const passageStates = this._openingsR.flatMap((opening) => {
|
||||
if (!isInteriorLightOpeningType(String(opening.type))) return [];
|
||||
const o = opening;
|
||||
const rad = (o.angle * Math.PI) / 180;
|
||||
const nx = -Math.sin(rad);
|
||||
const ny = Math.cos(rad);
|
||||
const interior = onFloor([o.rx + nx * probe, o.ry + ny * probe])
|
||||
&& onFloor([o.rx - nx * probe, o.ry - ny * probe]);
|
||||
return interior ? [{ opening: o, amount: this._openingAmt(o) }] : [];
|
||||
});
|
||||
const openingStateSignature = openingLightStateSignature(
|
||||
passageStates.map(({ opening, amount }) => ({
|
||||
id: opening.id, type: opening.type, contact: opening.contact, amount,
|
||||
})),
|
||||
);
|
||||
const fingerprint = contentFingerprint([geometryFingerprint, openingStateSignature]);
|
||||
const cacheKey = `${space.id}|${fingerprint}`;
|
||||
const pooled = lruRead(this._lightBarrierPool, cacheKey);
|
||||
if (pooled.hit) {
|
||||
this._lightBarrierCache = pooled.value;
|
||||
return pooled.value.value;
|
||||
}
|
||||
// Gates are door-like: their different symbol must not change how light
|
||||
// crosses the clear opening.
|
||||
const zeroWalls = this._zeroWalls();
|
||||
// Physical masonry has no body at every zero wall, independent of style.
|
||||
// Light transport differs: solid zero walls are added back below as exact
|
||||
@@ -10292,21 +10317,12 @@ export class HouseplanCard extends LitElement {
|
||||
// zero axes stay in the room outline and are also registered below as
|
||||
// exact standalone barriers for independent/outer-wall parity.
|
||||
const cuts: number[][] = [...zeroWalls.transmissive];
|
||||
// An opening is transparent only where it leads from floor to floor. A
|
||||
// front door has nothing behind it to light, so for light it is masonry
|
||||
// like a window — otherwise its tunnel glows halfway, up to the centreline
|
||||
// where the room polygon ends, and the plan shows a lit doorway to nowhere.
|
||||
const probe = Math.max(this._cmToUnits(10), this._gridPitch * 0.5);
|
||||
const onFloor = (point: number[]): boolean =>
|
||||
polys.some((x) => this._pointInRoom(point, x.r));
|
||||
const passages = this._openingsR.filter((o) => {
|
||||
if (!isInteriorLightOpeningType(String(o.type))) return false;
|
||||
const rad = (o.angle * Math.PI) / 180;
|
||||
const nx = -Math.sin(rad);
|
||||
const ny = Math.cos(rad);
|
||||
return onFloor([o.rx + nx * probe, o.ry + ny * probe])
|
||||
&& onFloor([o.rx - nx * probe, o.ry - ny * probe]);
|
||||
});
|
||||
const passages = passageStates
|
||||
.filter(({ amount }) => amount > 0)
|
||||
.map(({ opening, amount }) => ({
|
||||
...opening,
|
||||
rlen: openingLightApertureLength(opening.rlen, amount),
|
||||
}));
|
||||
const roomPassages = this._roomWallOpeningInputs(passages, space);
|
||||
// Openings omitted from `passages` remain part of light's opaque masonry.
|
||||
// Consequently a source centred in an exterior door/gate or any window is
|
||||
@@ -10317,19 +10333,17 @@ export class HouseplanCard extends LitElement {
|
||||
const dy = (Math.sin(rad) * opening.length) / 2;
|
||||
cuts.push([opening.x - dx, opening.y - dy, opening.x + dx, opening.y + dy]);
|
||||
}
|
||||
const transparentHostedIds = new Set(
|
||||
passages.filter((opening) => opening.partitionHost).map((opening) => opening.id),
|
||||
);
|
||||
const lightPhysicalKey = [
|
||||
space.id, this._cfgEpoch, this._cellCm, this._gridPitch,
|
||||
[...transparentHostedIds].sort().join(','),
|
||||
].join('|');
|
||||
const lightPartitionCuts = passageStates.flatMap(({ opening, amount }) =>
|
||||
amount > 0 && opening.partitionHost
|
||||
? [scalePartitionOpeningCut(partitionOpeningCut(opening.partitionHost), amount)]
|
||||
: []);
|
||||
const lightPhysicalKey = cacheKey;
|
||||
if (this._lightPhysicalBodiesCache?.key !== lightPhysicalKey) {
|
||||
this._lightPhysicalBodiesCache = {
|
||||
key: lightPhysicalKey,
|
||||
all: physicalBodyParts(
|
||||
space, this._cellCm, this._gridPitch, this._gridPitch * 0.0002,
|
||||
this._partitionOpeningCuts(space, (opening) => transparentHostedIds.has(opening.id)),
|
||||
lightPartitionCuts,
|
||||
).all,
|
||||
};
|
||||
}
|
||||
@@ -10345,7 +10359,7 @@ export class HouseplanCard extends LitElement {
|
||||
const sharedFingerprint = (sharedWallGeometry as unknown as {
|
||||
sourceFingerprint?: string;
|
||||
} | null)?.sourceFingerprint;
|
||||
const recut = sharedWallGeometry && sharedFingerprint === fingerprint
|
||||
const recut = sharedWallGeometry && sharedFingerprint === geometryFingerprint
|
||||
? recutWallBodiesGeometry(sharedWallGeometry, roomPassages, lightPhysical)
|
||||
: null;
|
||||
const masonry = recut || (walls.length || lightPhysical.length
|
||||
@@ -12316,10 +12330,12 @@ export class HouseplanCard extends LitElement {
|
||||
|
||||
/** Live state of an opening's contact, 0..1 drawn amount. */
|
||||
private _openingAmt(o: OpeningCfg): number {
|
||||
const st = o.contact && this._renderOpeningEntityAvailable(o.contact)
|
||||
? this._renderPlanHass.states[o.contact]?.state
|
||||
const entity = o.contact && this._renderOpeningEntityAvailable(o.contact)
|
||||
? this._renderPlanHass.states[o.contact]
|
||||
: null;
|
||||
return openingAmount(o.type, st, !!o.invert);
|
||||
return openingAmount(
|
||||
o.type, entity?.state, !!o.invert, entity?.attributes?.current_position,
|
||||
);
|
||||
}
|
||||
|
||||
/** Deleted bindings are unavailable to every plan-level consumer. */
|
||||
|
||||
@@ -314,13 +314,19 @@ export function openingShoulders(
|
||||
* `unavailable`/`unknown` freeze the default too: an outage must not fake motion.
|
||||
*/
|
||||
export function openingAmount(
|
||||
type: 'door' | 'window' | 'gate' | 'passage', state: string | null | undefined, invert = false,
|
||||
type: 'door' | 'window' | 'gate' | 'passage', state: string | null | undefined,
|
||||
invert = false, currentPosition?: unknown,
|
||||
): number {
|
||||
if (type === 'passage') return 1;
|
||||
if (state == null || state === 'unavailable' || state === 'unknown')
|
||||
return type === 'window' ? 0 : 1;
|
||||
const open = isActiveState(state) !== !!invert;
|
||||
return open ? 1 : 0;
|
||||
const rawPosition = typeof currentPosition === 'string'
|
||||
? (currentPosition.trim() ? Number(currentPosition) : NaN)
|
||||
: Number(currentPosition);
|
||||
const base = currentPosition != null && Number.isFinite(rawPosition)
|
||||
? Math.max(0, Math.min(1, rawPosition / 100))
|
||||
: (isActiveState(state) ? 1 : 0);
|
||||
return invert ? 1 - base : base;
|
||||
}
|
||||
|
||||
/** Explicit light allowlist. Unknown future opening types stay fail-dark until
|
||||
@@ -329,6 +335,36 @@ export function isInteriorLightOpeningType(type: string): type is 'door' | 'gate
|
||||
return type === 'door' || type === 'gate' || type === 'passage';
|
||||
}
|
||||
|
||||
export interface OpeningLightStateEntry {
|
||||
id: string;
|
||||
type: string;
|
||||
contact?: string | null;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
/** State-only identity for interior light apertures. Structural geometry is
|
||||
* fingerprinted separately, so unrelated HA updates cannot evict barriers. */
|
||||
export function openingLightStateSignature(
|
||||
openings: readonly OpeningLightStateEntry[],
|
||||
): string {
|
||||
return openings
|
||||
.filter((opening) => !!opening.contact
|
||||
&& (opening.type === 'door' || opening.type === 'gate'))
|
||||
.map((opening) => {
|
||||
const amount = Math.max(0, Math.min(1, Number(opening.amount) || 0));
|
||||
return `${opening.id}:${amount.toFixed(3)}`;
|
||||
})
|
||||
.sort()
|
||||
.join('|');
|
||||
}
|
||||
|
||||
/** Centre-preserving aperture length used by room-wall and partition cuts. */
|
||||
export function openingLightApertureLength(length: number, amount: number): number {
|
||||
const safeLength = Number.isFinite(length) && length > 0 ? length : 0;
|
||||
const safeAmount = Number.isFinite(amount) ? Math.max(0, Math.min(1, amount)) : 0;
|
||||
return safeLength * safeAmount;
|
||||
}
|
||||
|
||||
/** Entity bindings that may affect a rendered opening. A passage is purely
|
||||
* architectural, so stale legacy fields must never create HA subscriptions. */
|
||||
export function openingEntityReferences(opening: {
|
||||
|
||||
@@ -149,6 +149,23 @@ export interface PartitionOpeningCut {
|
||||
depth: number;
|
||||
}
|
||||
|
||||
/** Shorten an independent-wall cut around its existing centre. The host and
|
||||
* depth stay authoritative; only the light aperture changes with door state. */
|
||||
export function scalePartitionOpeningCut(
|
||||
cut: PartitionOpeningCut, amount: number,
|
||||
): PartitionOpeningCut {
|
||||
const safeAmount = Number.isFinite(amount) ? Math.max(0, Math.min(1, amount)) : 0;
|
||||
const cx = (cut.a[0] + cut.b[0]) / 2;
|
||||
const cy = (cut.a[1] + cut.b[1]) / 2;
|
||||
const hx = ((cut.b[0] - cut.a[0]) * safeAmount) / 2;
|
||||
const hy = ((cut.b[1] - cut.a[1]) * safeAmount) / 2;
|
||||
return {
|
||||
...cut,
|
||||
a: [cx - hx, cy - hy],
|
||||
b: [cx + hx, cy + hy],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cut only the explicitly hosted independent-wall body. Boolean failure keeps
|
||||
* the original body opaque (fail-dark) instead of manufacturing a light leak.
|
||||
|
||||
@@ -508,9 +508,11 @@ export function renderSpaceStatic(o: StaticRenderOpts): TemplateResult | null {
|
||||
const wallStroke = disp.color || '#607d8b';
|
||||
const hostedOpeningSymbols = disp.hideOpenings ? [] : resolvedHosted.map((resolved) => {
|
||||
const opening = resolved.opening;
|
||||
const state = opening.type === 'passage' || !opening.contact
|
||||
? null : planHass.states?.[opening.contact]?.state;
|
||||
const amount = openingAmount(opening.type, state, !!opening.invert);
|
||||
const entity = opening.type === 'passage' || !opening.contact
|
||||
? null : planHass.states?.[opening.contact];
|
||||
const amount = openingAmount(
|
||||
opening.type, entity?.state, !!opening.invert, entity?.attributes?.current_position,
|
||||
);
|
||||
const active = amount > 0 && !!opening.contact;
|
||||
const faceFlipV = opening.type === 'gate' ? !opening.flip_v : !!opening.flip_v;
|
||||
const spec: OpeningVisibleSpec = {
|
||||
|
||||
@@ -354,7 +354,11 @@ test('doorway spill golden exposes the opaque-fill failure mode from issue 71',
|
||||
assert.equal(scenario.glowEnabled, true);
|
||||
assert.equal(scenario.sunRays, false);
|
||||
assert.equal(scenario.allLightsOff, true);
|
||||
assert.equal(scenario.extraOpenings?.filter((opening) => opening.type === 'door').length, 1);
|
||||
assert.equal(scenario.extraOpenings?.filter((opening) => opening.type === 'door').length, 2);
|
||||
assert.equal(scenario.extraOpenings?.find((opening) => opening.id === 'light-door-second')?.contact,
|
||||
'cover.golden_door_closed');
|
||||
assert.equal(scenario.extraOpenings?.find((opening) => opening.id === 'light-door-third')?.contact,
|
||||
'cover.golden_door_half');
|
||||
assert.equal(scenario.stateOverrides?.['light.golden_light_one']?.state, 'on');
|
||||
assert.deepEqual(scenario.layoutOverrides?.['golden-light-one'], {
|
||||
s: 'golden-lighting', x: 0.40, y: 0.48,
|
||||
@@ -710,7 +714,9 @@ test('golden harness applies doorway, state and layout overrides to a cloned fix
|
||||
const scenario = GOLDEN_SCENARIOS.find((item) => item.id === 'lighting-opaque-glow-two-doorways-dark');
|
||||
const fixture = prepareGoldenFixture(scenario);
|
||||
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
|
||||
assert.equal(space.openings.filter((opening) => opening.type === 'door').length, 2);
|
||||
assert.equal(space.openings.filter((opening) => opening.type === 'door').length, 3);
|
||||
assert.equal(fixture.states['cover.golden_door_closed'].attributes.current_position, 0);
|
||||
assert.equal(fixture.states['cover.golden_door_half'].attributes.current_position, 50);
|
||||
assert.equal(fixture.states['light.golden_light_one'].state, 'on');
|
||||
assert.deepEqual(fixture.layout['golden-light-one'], {
|
||||
s: 'golden-lighting', x: 0.40, y: 0.48,
|
||||
@@ -765,10 +771,10 @@ test('a light source paints exactly one region: the floor it can see', () => {
|
||||
// Barriers are keyed by their own content: `_cfgEpoch` lags behind geometry
|
||||
// edited in place, and a stale barrier set lights straight through a wall.
|
||||
assert.doesNotMatch(glow, /_cfgEpoch/);
|
||||
assert.match(source, /const fingerprint = contentFingerprint\(\[/);
|
||||
assert.match(source, /contentFingerprint\(\[raw, this\._cellCm, this\._gridPitch\]\)/);
|
||||
assert.match(source, /const geometryFingerprint = contentFingerprint\(\[raw, this\._cellCm, this\._gridPitch\]\)/);
|
||||
assert.match(source, /const fingerprint = contentFingerprint\(\[geometryFingerprint, openingStateSignature\]\)/);
|
||||
assert.match(source, /const cacheKey = `\$\{space\.id\}\|\$\{fingerprint\}`/);
|
||||
assert.match(source, /sharedFingerprint === fingerprint/,
|
||||
assert.match(source, /sharedFingerprint === geometryFingerprint/,
|
||||
'an in-place edit must never recut stale shared masonry');
|
||||
});
|
||||
|
||||
|
||||
@@ -16,7 +16,11 @@ import {
|
||||
contentUrl, chunk, referencedContentUrls, MAX_SIGN_PATHS,
|
||||
interiorPoint,
|
||||
segmentCm, formatLength, roomEdges, roomPoly, paperRoomShapes, pointOnBoundary, pointStrictlyInside, roomsOverlap,
|
||||
mergeRooms, splitRoom, polygonArea, closestPointOnBoundary, isActiveState, snapToWall, openingAmount, openingShoulders, openingEntityReferences, fillColorsOf, lerpColor, roomFillStyle, resolveEffectiveRoomFill, stateIcon, lightColorOf, isAlarmState, parseRoomRef, diffNewDevices, poleOfInaccessibility, runServiceFor, coverMoving,
|
||||
mergeRooms, splitRoom, polygonArea, closestPointOnBoundary, isActiveState, snapToWall,
|
||||
openingAmount, openingLightApertureLength, openingLightStateSignature,
|
||||
openingShoulders, openingEntityReferences, fillColorsOf, lerpColor, roomFillStyle,
|
||||
resolveEffectiveRoomFill, stateIcon, lightColorOf, isAlarmState, parseRoomRef,
|
||||
diffNewDevices, poleOfInaccessibility, runServiceFor, coverMoving,
|
||||
normalizeDeviceDisplay, isAlarmCapable,
|
||||
liveText, liveTextValue, liveTextReference, liveTextToken,
|
||||
hassValue, valueWithUnit, decorTextScale, decorTextLines,
|
||||
@@ -492,6 +496,44 @@ test('openingAmount: door-like openings default open, windows closed; outages fr
|
||||
}
|
||||
});
|
||||
|
||||
test('openingAmount shares positioned cover progress with light apertures', () => {
|
||||
assert.equal(openingAmount('gate', 'open', false, 0), 0);
|
||||
assert.equal(openingAmount('gate', 'open', false, 1), 0.01);
|
||||
assert.equal(openingAmount('gate', 'open', false, 50), 0.5);
|
||||
assert.equal(openingAmount('gate', 'open', false, 100), 1);
|
||||
assert.equal(openingAmount('gate', 'open', false, -20), 0);
|
||||
assert.equal(openingAmount('gate', 'open', false, 140), 1);
|
||||
assert.equal(openingAmount('gate', 'open', false, '40'), 0.4);
|
||||
assert.equal(openingAmount('gate', 'open', false, ''), 1, 'empty position falls back to state');
|
||||
assert.equal(openingAmount('gate', 'off', false, 'bad'), 0, 'invalid position falls back to state');
|
||||
assert.equal(openingAmount('gate', 'open', true, 25), 0.75);
|
||||
assert.equal(openingAmount('gate', 'unavailable', true, 25), 1,
|
||||
'outage fallback ignores stale position and invert');
|
||||
assert.equal(openingAmount('passage', 'off', true, 0), 1);
|
||||
|
||||
assert.equal(openingLightApertureLength(80, 0.5), 40);
|
||||
assert.equal(openingLightApertureLength(80, -1), 0);
|
||||
assert.equal(openingLightApertureLength(80, 2), 80);
|
||||
assert.equal(openingLightApertureLength(NaN, 0.5), 0);
|
||||
});
|
||||
|
||||
test('opening light state signature is ordered, bounded and contact-only', () => {
|
||||
const a = [
|
||||
{ id: 'gate-b', type: 'gate', contact: 'cover.gate', amount: 0.5004 },
|
||||
{ id: 'door-a', type: 'door', contact: 'binary_sensor.door', amount: 1 },
|
||||
{ id: 'passage', type: 'passage', contact: null, amount: 1 },
|
||||
{ id: 'window', type: 'window', contact: 'binary_sensor.window', amount: 0 },
|
||||
{ id: 'unbound', type: 'door', contact: '', amount: 0 },
|
||||
];
|
||||
const expected = 'door-a:1.000|gate-b:0.500';
|
||||
assert.equal(openingLightStateSignature(a), expected);
|
||||
assert.equal(openingLightStateSignature([...a].reverse()), expected);
|
||||
assert.equal(openingLightStateSignature([
|
||||
{ id: 'low', type: 'door', contact: 'binary_sensor.low', amount: -2 },
|
||||
{ id: 'high', type: 'gate', contact: 'cover.high', amount: 2 },
|
||||
]), 'high:1.000|low:0.000');
|
||||
});
|
||||
|
||||
test('stale passage bindings stay inert while ordinary opening bindings remain live', () => {
|
||||
const stale = { type: 'passage', contact: 'binary_sensor.door', lock: 'lock.front' };
|
||||
assert.deepEqual(openingEntityReferences(stale), []);
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
BOOLEAN_COORD_QUANTUM, canonicalColumnAngle, columnBody, floorMinusBodies, geometryArea,
|
||||
directionalOccluders, intersectionPaths, partitionBody, pointInPhysicalBody,
|
||||
physicalBodyParts, physicalBodySet, pointInOpaquePlanBody, pointInPhysicalGeometry,
|
||||
normalizeBooleanBody, sameColumnPlacement, unionBodies,
|
||||
normalizeBooleanBody, sameColumnPlacement, scalePartitionOpeningCut, unionBodies,
|
||||
} from '../test-build/physical-geometry.js';
|
||||
import {
|
||||
polygonSegments, splitAtIntersections, visibilityPolygon,
|
||||
@@ -36,6 +36,37 @@ test('partition body keeps the centreline and requested physical width', () => {
|
||||
closeTo(geometryArea([[[...body, body[0]]]]), 0.5);
|
||||
});
|
||||
|
||||
test('partition light aperture scales around the same centre and keeps host depth', () => {
|
||||
const full = { hostId: 'wall', a: [2, 4], b: [10, 8], depth: 3 };
|
||||
assert.deepEqual(scalePartitionOpeningCut(full, 0.5), {
|
||||
hostId: 'wall', a: [4, 5], b: [8, 7], depth: 3,
|
||||
});
|
||||
assert.deepEqual(scalePartitionOpeningCut(full, 0), {
|
||||
hostId: 'wall', a: [6, 6], b: [6, 6], depth: 3,
|
||||
});
|
||||
assert.deepEqual(scalePartitionOpeningCut(full, 2), full);
|
||||
assert.deepEqual(full, { hostId: 'wall', a: [2, 4], b: [10, 8], depth: 3 },
|
||||
'scaling does not mutate canonical geometry');
|
||||
});
|
||||
|
||||
test('partition body uses the same full, partial and closed light aperture', () => {
|
||||
const space = {
|
||||
room_drafts: [], wall_columns: [],
|
||||
partitions: [{ id: 'host', a: [0, 0], b: [10, 0], cm: 10 }],
|
||||
};
|
||||
const full = { hostId: 'host', a: [4, 0], b: [6, 0], depth: 2 };
|
||||
const half = scalePartitionOpeningCut(full, 0.5);
|
||||
const fullBodies = physicalBodyParts(space, 5, 1, 1e-6, [full]).all;
|
||||
const halfBodies = physicalBodyParts(space, 5, 1, 1e-6, [half]).all;
|
||||
const closedBodies = physicalBodyParts(space, 5, 1, 1e-6, []).all;
|
||||
|
||||
assert.equal(pointInOpaquePlanBody([5, 0], [], fullBodies), false, 'full slot centre');
|
||||
assert.equal(pointInOpaquePlanBody([5.75, 0], [], fullBodies), false, 'full slot shoulder');
|
||||
assert.equal(pointInOpaquePlanBody([5, 0], [], halfBodies), false, 'half slot centre');
|
||||
assert.equal(pointInOpaquePlanBody([5.75, 0], [], halfBodies), true, 'half slot shoulder');
|
||||
assert.equal(pointInOpaquePlanBody([5, 0], [], closedBodies), true, 'closed slot is masonry');
|
||||
});
|
||||
|
||||
test('joined partitions fill straight and oblique endpoint teeth without changing flat free caps', () => {
|
||||
const base = {
|
||||
room_drafts: [], wall_columns: [],
|
||||
|
||||