fix: address render performance review

Issue: #451
User-Visible: no
This commit is contained in:
Sergey Matyunin
2026-09-04 23:45:57 +03:00
parent f8b3445dc3
commit cb68492c99
41 changed files with 6985 additions and 6600 deletions
@@ -1,182 +1,182 @@
{
"schema": 1,
"fingerprint": "05ce2842760e7918081d8de1db20b0c2687438b9316a6dcdd9591094c2efb298",
"fingerprint": "9cba40b372f43cfad12eb432d5b2ee7847cb3f00f6d91e154ffada6f31df2fd4",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-Cue_bp_z.js",
"houseplan-assets/houseplan-card-ClTiXI3p.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 296492,
"initialViewGzipBytes": 296711,
"lazyFiles": [
"houseplan-assets/backdrop-pick-Rc7DbOnh.js",
"houseplan-assets/de-BsO3XZz2.js",
"houseplan-assets/editor-B5aKTgBM.js",
"houseplan-assets/fr-BND5clgi.js",
"houseplan-assets/houseplan-editor-runtime-Byj_OtmL.js",
"houseplan-assets/houseplan-onboarding-runtime-BPeqwGUM.js",
"houseplan-assets/hp-zigbee-topology-overlay-B4dxvXzw.js",
"houseplan-assets/live-interaction-runtime-BKpy4fJl.js",
"houseplan-assets/zigbee-topology-C9RnitZl.js",
"houseplan-assets/zigbee-topology-runtime-Cjou-8UI.js"
"houseplan-assets/backdrop-pick-DxIrP2TG.js",
"houseplan-assets/de-DrM43PtS.js",
"houseplan-assets/editor-DF8nDJsl.js",
"houseplan-assets/fr-BUknt1Ee.js",
"houseplan-assets/houseplan-editor-runtime-CEjxcGHj.js",
"houseplan-assets/houseplan-onboarding-runtime-CDcvp0Jc.js",
"houseplan-assets/hp-zigbee-topology-overlay-BclGSBEr.js",
"houseplan-assets/live-interaction-runtime-Cm2wMojA.js",
"houseplan-assets/zigbee-topology-Di0_F8nQ.js",
"houseplan-assets/zigbee-topology-runtime-B9aBn1Rm.js"
],
"lazyGzipBytes": 238059,
"lazyGzipBytes": 239429,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-Rc7DbOnh.js",
"houseplan-assets/editor-B5aKTgBM.js",
"houseplan-assets/houseplan-editor-runtime-Byj_OtmL.js",
"houseplan-assets/zigbee-topology-C9RnitZl.js"
"houseplan-assets/backdrop-pick-DxIrP2TG.js",
"houseplan-assets/editor-DF8nDJsl.js",
"houseplan-assets/houseplan-editor-runtime-CEjxcGHj.js",
"houseplan-assets/zigbee-topology-Di0_F8nQ.js"
],
"lazyEditorGzipBytes": 175720,
"lazyEditorGzipBytes": 177118,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-Rc7DbOnh.js",
"houseplan-assets/houseplan-onboarding-runtime-BPeqwGUM.js"
"houseplan-assets/backdrop-pick-DxIrP2TG.js",
"houseplan-assets/houseplan-onboarding-runtime-CDcvp0Jc.js"
],
"lazyOnboardingGzipBytes": 14239,
"lazyOnboardingGzipBytes": 14237,
"lazyLocaleFiles": [
"houseplan-assets/de-BsO3XZz2.js",
"houseplan-assets/fr-BND5clgi.js"
"houseplan-assets/de-DrM43PtS.js",
"houseplan-assets/fr-BUknt1Ee.js"
],
"lazyLocaleGzipBytes": 49117,
"lazyLocaleGzipBytes": 49095,
"files": [
{
"path": "houseplan-assets/backdrop-pick-Rc7DbOnh.js",
"sha256": "49133ce5325444f6d1a288db1d559409a1cbf148530b57b36c3e3b35422b563f",
"path": "houseplan-assets/backdrop-pick-DxIrP2TG.js",
"sha256": "40a2b20a1d2895882f9f5253c23012c4082ff794f59d69d16906cdbd0ef0b81b",
"rawBytes": 21417,
"gzipBytes": 7283,
"gzipBytes": 7279,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Cue_bp_z.js"
"houseplan-assets/houseplan-card-ClTiXI3p.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-BsO3XZz2.js",
"sha256": "ea45e6e4b85447b9d64cebd3e178d2b8d0e956e2b210c540ea0157838848b348",
"path": "houseplan-assets/de-DrM43PtS.js",
"sha256": "8eef78c8bb65d4dd600c779a6d9ab39f8edc2b05dec690624cff32734181f102",
"rawBytes": 82182,
"gzipBytes": 24828,
"gzipBytes": 24817,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-B5aKTgBM.js",
"sha256": "760d90c44a967cb14af4c6eb947cf70460bf2ef5173d7c77874b49f2b10f170b",
"rawBytes": 3826,
"gzipBytes": 1581,
"path": "houseplan-assets/editor-DF8nDJsl.js",
"sha256": "abb853b6f918c548c0cb09101dc9186340fccc5e252a04f28e7ca113e43d04c2",
"rawBytes": 3831,
"gzipBytes": 1580,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Cue_bp_z.js"
"houseplan-assets/houseplan-card-ClTiXI3p.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-BND5clgi.js",
"sha256": "18685ca59a983d0c8bda581f53aed49123fa8f88e450c9e04b387e11427aa761",
"path": "houseplan-assets/fr-BUknt1Ee.js",
"sha256": "54ea601d94848dca21dc418c66be5dd3efed711ddbd167e1b3803aa4eca34816",
"rawBytes": 84312,
"gzipBytes": 24289,
"gzipBytes": 24278,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-Cue_bp_z.js",
"sha256": "8861c1b21b3bfd68b283d1f207924bc8cb24989b32214379afa78afbf848df7d",
"rawBytes": 1053264,
"gzipBytes": 295697,
"path": "houseplan-assets/houseplan-card-ClTiXI3p.js",
"sha256": "23062c58e7a572faeaceaf8690447cf3efc26a35394ca616f95b942fb4ff342a",
"rawBytes": 1053904,
"gzipBytes": 295917,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-BsO3XZz2.js",
"houseplan-assets/editor-B5aKTgBM.js",
"houseplan-assets/fr-BND5clgi.js",
"houseplan-assets/houseplan-editor-runtime-Byj_OtmL.js",
"houseplan-assets/houseplan-onboarding-runtime-BPeqwGUM.js",
"houseplan-assets/hp-zigbee-topology-overlay-B4dxvXzw.js",
"houseplan-assets/live-interaction-runtime-BKpy4fJl.js"
"houseplan-assets/de-DrM43PtS.js",
"houseplan-assets/editor-DF8nDJsl.js",
"houseplan-assets/fr-BUknt1Ee.js",
"houseplan-assets/houseplan-editor-runtime-CEjxcGHj.js",
"houseplan-assets/houseplan-onboarding-runtime-CDcvp0Jc.js",
"houseplan-assets/hp-zigbee-topology-overlay-BclGSBEr.js",
"houseplan-assets/live-interaction-runtime-Cm2wMojA.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-Byj_OtmL.js",
"sha256": "39626dfebe5420ddfafa29bbbce1a50ffc014a638d1dbe808d068ff2b6713f50",
"rawBytes": 614348,
"gzipBytes": 162012,
"path": "houseplan-assets/houseplan-editor-runtime-CEjxcGHj.js",
"sha256": "edd71327a1d38a9175f85f1f4caede3a5ec75cb0e1ee76e02153051149b08296",
"rawBytes": 618575,
"gzipBytes": 163415,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-Rc7DbOnh.js",
"houseplan-assets/houseplan-card-Cue_bp_z.js",
"houseplan-assets/zigbee-topology-C9RnitZl.js"
"houseplan-assets/backdrop-pick-DxIrP2TG.js",
"houseplan-assets/houseplan-card-ClTiXI3p.js",
"houseplan-assets/zigbee-topology-Di0_F8nQ.js"
],
"dynamicImports": [
"houseplan-assets/zigbee-topology-runtime-Cjou-8UI.js"
"houseplan-assets/zigbee-topology-runtime-B9aBn1Rm.js"
]
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-BPeqwGUM.js",
"sha256": "81d4372e2cdd3cafb7d7ae201c7678d4b72b56bfe241b5e35a64f9197f2c9768",
"path": "houseplan-assets/houseplan-onboarding-runtime-CDcvp0Jc.js",
"sha256": "8b98688fdb1c1b0ef8c2e1bdba4e776922a12dad06694df686406d237ddae545",
"rawBytes": 28088,
"gzipBytes": 6956,
"gzipBytes": 6958,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-Rc7DbOnh.js",
"houseplan-assets/houseplan-card-Cue_bp_z.js"
"houseplan-assets/backdrop-pick-DxIrP2TG.js",
"houseplan-assets/houseplan-card-ClTiXI3p.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/hp-zigbee-topology-overlay-B4dxvXzw.js",
"sha256": "e2cd2c36ce28369920428b8aa19975c8cbf91749f52da7fd5a9a1fcc09938e07",
"path": "houseplan-assets/hp-zigbee-topology-overlay-BclGSBEr.js",
"sha256": "b16af400fa662e3f8b583655654a4a51d97e17632ce2609070546049995e9f4e",
"rawBytes": 6006,
"gzipBytes": 2377,
"gzipBytes": 2373,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Cue_bp_z.js",
"houseplan-assets/zigbee-topology-C9RnitZl.js",
"houseplan-assets/zigbee-topology-runtime-Cjou-8UI.js"
"houseplan-assets/houseplan-card-ClTiXI3p.js",
"houseplan-assets/zigbee-topology-Di0_F8nQ.js",
"houseplan-assets/zigbee-topology-runtime-B9aBn1Rm.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/live-interaction-runtime-BKpy4fJl.js",
"sha256": "d06b3e2c1784f55e36581863a3c06d2bcd64e238499e81417afb6cbfdd8e58f9",
"path": "houseplan-assets/live-interaction-runtime-Cm2wMojA.js",
"sha256": "864dbac45c561ca96bd101ad7d02609daca27f2786250607a0f030fdc53c5e12",
"rawBytes": 5196,
"gzipBytes": 2127,
"gzipBytes": 2124,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Cue_bp_z.js"
"houseplan-assets/houseplan-card-ClTiXI3p.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-C9RnitZl.js",
"sha256": "423c2ae23cd2e688411935ace0924868e3cb7654c382614411dd97e548e01bd3",
"path": "houseplan-assets/zigbee-topology-Di0_F8nQ.js",
"sha256": "b2d2fc995f582cc9e11f6b8ee33deff93879bbafaf96900e8ac7ef090f5148a8",
"rawBytes": 12382,
"gzipBytes": 4844,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Cue_bp_z.js"
"houseplan-assets/houseplan-card-ClTiXI3p.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-runtime-Cjou-8UI.js",
"sha256": "40c1b1e0b1367c51296177653b264fd1a1cfb26b4b98eb9d83db216ee626774f",
"path": "houseplan-assets/zigbee-topology-runtime-B9aBn1Rm.js",
"sha256": "7bf47628bde723081c132b9af5172b93f340baf5efb969cb40381d1af2345fce",
"rawBytes": 3941,
"gzipBytes": 1762,
"gzipBytes": 1761,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Cue_bp_z.js",
"houseplan-assets/zigbee-topology-C9RnitZl.js"
"houseplan-assets/houseplan-card-ClTiXI3p.js",
"houseplan-assets/zigbee-topology-Di0_F8nQ.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "93b3e318550fa784c9487b41b67302d6259e44bea8f2407613f9373d1354da61",
"sha256": "ba2206bd6eca725fd19155de72b02ba441ea0aa9f51891f0f7080f7141e9e61c",
"rawBytes": 1183,
"gzipBytes": 795,
"gzipBytes": 794,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-Cue_bp_z.js"
"houseplan-assets/houseplan-card-ClTiXI3p.js"
],
"dynamicImports": []
}
File diff suppressed because one or more lines are too long
@@ -1,14 +0,0 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="05ce2842760e7918081d8de1db20b0c2687438b9316a6dcdd9591094c2efb298";import{k as e,l as o,t,dq as s,A as a,dr as i,ds as l,dt as r,E as n,c}from"./houseplan-card-Cue_bp_z.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 r="string"==typeof this._config?.default_floor?this._config.default_floor:"",n=[...e];return r&&!e.some(e=>e.value===r)&&n.unshift({value:r,label:r}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:n}}}:{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 r();if("warm"===e)return n;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(_.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:f})}
</div>`:a}
${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);
@@ -0,0 +1,14 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="9cba40b372f43cfad12eb432d5b2ee7847cb3f00f6d91e154ffada6f31df2fd4";import{v as e,l as o,z as t,dt as s,A as a,du as i,dv as l,dw as n,E as r,c}from"./houseplan-card-ClTiXI3p.js";class f 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,f={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")},h=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
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
.computeLabel=${e=>f[e.name]||e.name}
@value-changed=${this._valueChanged}
></ha-form>`;return c`
${d(h.slice(0,3))}
${_?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:_})}
</div>`:a}
${d(h.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)}}f.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",f);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="05ce2842760e7918081d8de1db20b0c2687438b9316a6dcdd9591094c2efb298";import{k as e,A as t,du as i,w as s,c as o,l as r,j as n}from"./houseplan-card-Cue_bp_z.js";import{a,r as h,t as d}from"./zigbee-topology-C9RnitZl.js";import{zigbeeTopologyRuntimeSnapshot as p,subscribeZigbeeTopology as c}from"./zigbee-topology-runtime-Cjou-8UI.js";const l={revision:0,topologies:[],states:{}};class v extends e{constructor(){super(...arguments),this.devices=[],this.currentSpace="",this._runtime=l,this._hovered="",this._pointerOver=e=>{if(!this._mouseAllowed(e))return void this._clear();const t=this._deviceFromEvent(e),i=t?.dataset.id||"";i&&i!==this._hovered&&(this._hovered=i,this.requestUpdate())},this._pointerOut=e=>{const t=this._deviceFromEvent(e);if(!t||t.dataset.id!==this._hovered)return;const i=e.relatedTarget instanceof Element?e.relatedTarget.closest('[data-hp="device"]'):null;i?.dataset.id!==this._hovered&&this._clear()},this._pointerDown=e=>{"mouse"!==e.pointerType&&this._clear()}}connectedCallback(){super.connectedCallback();const e=this.getRootNode();e.host&&"undefined"!=typeof MutationObserver&&(this._hoverGateObserver=new MutationObserver(()=>{e.host.hasAttribute("data-pointer-hover")||this._clear()}),this._hoverGateObserver.observe(e.host,{attributes:!0,attributeFilter:["data-pointer-hover"]})),queueMicrotask(()=>this._connectParent())}disconnectedCallback(){this._release?.(),this._release=void 0,this._disconnectParent(),this._hoverGateObserver?.disconnect(),this._hoverGateObserver=void 0,this._hovered="",super.disconnectedCallback()}updated(e){e.has("hass")&&(this._release?.(),this._runtime=p(this.hass),this._mappedMemo=void 0,this._release=c(this.hass,()=>{this._runtime=p(this.hass),this._mappedMemo=void 0,this.requestUpdate()})),(e.has("currentSpace")||e.has("devices")||e.has("registry"))&&(this._mappedMemo=void 0,this.devices.some(e=>e.id===this._hovered&&e.space===this.currentSpace)||(this._hovered=""))}_connectParent(){const e=this.parentElement;e&&e!==this._parent&&(this._disconnectParent(),this._parent=e,e.addEventListener("pointerover",this._pointerOver),e.addEventListener("pointerout",this._pointerOut),e.addEventListener("pointerdown",this._pointerDown,!0))}_disconnectParent(){this._parent?.removeEventListener("pointerover",this._pointerOver),this._parent?.removeEventListener("pointerout",this._pointerOut),this._parent?.removeEventListener("pointerdown",this._pointerDown,!0),this._parent=void 0}_deviceFromEvent(e){for(const t of e.composedPath())if(t instanceof HTMLElement&&"device"===t.dataset.hp)return t;return null}_mouseAllowed(e){const t=this.getRootNode();return"mouse"===e.pointerType&&t.host?.hasAttribute?.("data-pointer-hover")}_clear(){this._hovered&&(this._hovered="",this.requestUpdate())}_position(e){const t=[...this._parent?.querySelectorAll(".dev[data-id]")||[]].find(t=>t.dataset.id===e);if(!t)return null;const i=Number.parseFloat(t.style.left),s=Number.parseFloat(t.style.top),o=t.getBoundingClientRect();return Number.isFinite(i)&&Number.isFinite(s)?{x:i,y:s,width:o.width,height:o.height}:null}render(){if(!this._hovered||!this.registry||!this._runtime.topologies.length)return t;const e=this._mappedMemo,n=e&&e.runtime===this._runtime&&e.devices===this.devices&&e.registry===this.registry?e.mapped:a(this._runtime.topologies,this.devices,this.registry);e&&n===e.mapped||(this._mappedMemo={runtime:this._runtime,devices:this.devices,registry:this.registry,mapped:n});const p=h(n,this.currentSpace,this._hovered),c=this._position(this._hovered);if(!c)return t;const l=p.lines.map(e=>({...e,point:this._position(e.neighborMarkerId)})).filter(e=>!!e.point);return o`
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="9cba40b372f43cfad12eb432d5b2ee7847cb3f00f6d91e154ffada6f31df2fd4";import{v as e,A as t,dx as i,w as s,c as o,l as r,u as n}from"./houseplan-card-ClTiXI3p.js";import{a,r as h,t as d}from"./zigbee-topology-Di0_F8nQ.js";import{zigbeeTopologyRuntimeSnapshot as p,subscribeZigbeeTopology as c}from"./zigbee-topology-runtime-B9aBn1Rm.js";const l={revision:0,topologies:[],states:{}};class v extends e{constructor(){super(...arguments),this.devices=[],this.currentSpace="",this._runtime=l,this._hovered="",this._pointerOver=e=>{if(!this._mouseAllowed(e))return void this._clear();const t=this._deviceFromEvent(e),i=t?.dataset.id||"";i&&i!==this._hovered&&(this._hovered=i,this.requestUpdate())},this._pointerOut=e=>{const t=this._deviceFromEvent(e);if(!t||t.dataset.id!==this._hovered)return;const i=e.relatedTarget instanceof Element?e.relatedTarget.closest('[data-hp="device"]'):null;i?.dataset.id!==this._hovered&&this._clear()},this._pointerDown=e=>{"mouse"!==e.pointerType&&this._clear()}}connectedCallback(){super.connectedCallback();const e=this.getRootNode();e.host&&"undefined"!=typeof MutationObserver&&(this._hoverGateObserver=new MutationObserver(()=>{e.host.hasAttribute("data-pointer-hover")||this._clear()}),this._hoverGateObserver.observe(e.host,{attributes:!0,attributeFilter:["data-pointer-hover"]})),queueMicrotask(()=>this._connectParent())}disconnectedCallback(){this._release?.(),this._release=void 0,this._disconnectParent(),this._hoverGateObserver?.disconnect(),this._hoverGateObserver=void 0,this._hovered="",super.disconnectedCallback()}updated(e){e.has("hass")&&(this._release?.(),this._runtime=p(this.hass),this._mappedMemo=void 0,this._release=c(this.hass,()=>{this._runtime=p(this.hass),this._mappedMemo=void 0,this.requestUpdate()})),(e.has("currentSpace")||e.has("devices")||e.has("registry"))&&(this._mappedMemo=void 0,this.devices.some(e=>e.id===this._hovered&&e.space===this.currentSpace)||(this._hovered=""))}_connectParent(){const e=this.parentElement;e&&e!==this._parent&&(this._disconnectParent(),this._parent=e,e.addEventListener("pointerover",this._pointerOver),e.addEventListener("pointerout",this._pointerOut),e.addEventListener("pointerdown",this._pointerDown,!0))}_disconnectParent(){this._parent?.removeEventListener("pointerover",this._pointerOver),this._parent?.removeEventListener("pointerout",this._pointerOut),this._parent?.removeEventListener("pointerdown",this._pointerDown,!0),this._parent=void 0}_deviceFromEvent(e){for(const t of e.composedPath())if(t instanceof HTMLElement&&"device"===t.dataset.hp)return t;return null}_mouseAllowed(e){const t=this.getRootNode();return"mouse"===e.pointerType&&t.host?.hasAttribute?.("data-pointer-hover")}_clear(){this._hovered&&(this._hovered="",this.requestUpdate())}_position(e){const t=[...this._parent?.querySelectorAll(".dev[data-id]")||[]].find(t=>t.dataset.id===e);if(!t)return null;const i=Number.parseFloat(t.style.left),s=Number.parseFloat(t.style.top),o=t.getBoundingClientRect();return Number.isFinite(i)&&Number.isFinite(s)?{x:i,y:s,width:o.width,height:o.height}:null}render(){if(!this._hovered||!this.registry||!this._runtime.topologies.length)return t;const e=this._mappedMemo,n=e&&e.runtime===this._runtime&&e.devices===this.devices&&e.registry===this.registry?e.mapped:a(this._runtime.topologies,this.devices,this.registry);e&&n===e.mapped||(this._mappedMemo={runtime:this._runtime,devices:this.devices,registry:this.registry,mapped:n});const p=h(n,this.currentSpace,this._hovered),c=this._position(this._hovered);if(!c)return t;const l=p.lines.map(e=>({...e,point:this._position(e.neighborMarkerId)})).filter(e=>!!e.point);return o`
${l.length?s`<svg viewBox="0 0 100 100" preserveAspectRatio="none"
aria-hidden="true" data-hp="zigbee-topology-lines">
${l.map(e=>s`<line x1=${c.x} y1=${c.y}
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="05ce2842760e7918081d8de1db20b0c2687438b9316a6dcdd9591094c2efb298";import{n as t,b as n}from"./zigbee-topology-C9RnitZl.js";import{z as e}from"./houseplan-card-Cue_bp_z.js";const o=new WeakMap;function i(t){const n=function(t){const n=t?.connection||t;return!n||"object"!=typeof n&&"function"!=typeof n?null:n}(t);if(!n)return null;let e=o.get(n);return e||(e={revision:0,topologies:[],states:{},listeners:new Set,inflight:new Map},o.set(n,e)),e}function r(t,n,e){t.states={...t.states,[n]:e},function(t){t.revision++;for(const n of t.listeners)n()}(t)}function s(t){const n=t?.code;if("permission"===n||"unsupported"===n||"timeout"===n||"invalid_topic"===n||"invalid_payload"===n)return n;const e=String(t?.message||"").toLowerCase();return e.includes("unauthor")||e.includes("permission")?"permission":e.includes("unknown_command")||e.includes("not found")?"unsupported":"provider"}function a(t){return Object.assign(new Error(t),{code:t})}function c(t,n,e){const o=t.inflight.get(n);if(o)return o;r(t,n,{phase:"loading"});const i=e().then(n=>{if(!n.nodes.length&&n.warnings.some(t=>"invalid_payload"===t.code))throw a("invalid_payload");!function(t,n){t.topologies=[...t.topologies.filter(t=>!(t.provider===n.provider&&t.instanceId===n.instanceId)),n],r(t,"zha"===n.provider?"zha":`z2m:${n.instanceId}`,{phase:"ready",obtainedAt:n.obtainedAt,partial:n.warnings.length>0})}(t,n)}).catch(e=>{r(t,n,{phase:"error",error:s(e)})}).finally(()=>t.inflight.delete(n));return t.inflight.set(n,i),i}function l(t){const n=i(t);return n?{revision:n.revision,topologies:n.topologies,states:n.states}:{revision:0,topologies:[],states:{}}}function u(t,n){const e=i(t);return e?(e.listeners.add(n),()=>e.listeners.delete(n)):()=>{}}function p(t){if(!0!==t?.user?.is_admin)throw a("permission")}function f(n){const e=i(n);return e?c(e,"zha",async()=>{if(p(n),"function"!=typeof n?.callWS)throw a("unsupported");return t(await n.callWS({type:"zha/devices"}))}):Promise.resolve()}function d(t){return null!==t&&"object"==typeof t?t:null}function h(t){const n=d(t),e=n?.payload??t;if("string"!=typeof e)return e;try{return JSON.parse(e)}catch{return null}}async function m(t,n){let e;try{return await Promise.race([t,new Promise((t,o)=>{e=globalThis.setTimeout(()=>o(a("timeout")),Math.max(1,n))})])}finally{void 0!==e&&globalThis.clearTimeout(e)}}function g(t,o,r=15e4){const s=i(t);if(!s)return Promise.resolve();const l=e(o);return c(s,`z2m:${l||String(o)}`,async()=>{if(p(t),!l)throw a("invalid_topic");const e=t.connection,o=e?.subscribeMessage;if("function"!=typeof o||"function"!=typeof t?.callService)throw a("unsupported");const i=function(){const t=globalThis.crypto;return"function"==typeof t?.randomUUID?`houseplan-${t.randomUUID()}`:`houseplan-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}(),s=Date.now()+Math.max(1,r);let c=!1,u=null,f=null,g=null;const y=new Promise(t=>{u=t}),w=new Promise((t,n)=>{f=t,g=n}),b=[];try{const r=await o.call(e,t=>{!0===d(t)?.retain&&h(t)&&u?.()},{type:"mqtt/subscribe",topic:`${l}/bridge/info`});"function"==typeof r&&b.push(r);const p=await o.call(e,t=>{if(!0===d(t)?.retain)return;const n=h(t);null!==n?n&&function(t){const n=d(t),e=n?.transaction??d(n?.data)?.transaction;return"string"==typeof e||"number"==typeof e?String(e):null}(n)===i&&f?.(n):c&&g?.(a("invalid_payload"))},{type:"mqtt/subscribe",topic:`${l}/bridge/response/networkmap`});"function"==typeof p&&b.push(p),await m(y,Math.min(4e3,Math.max(1,s-Date.now()))),c=!0,await t.callService("mqtt","publish",{topic:`${l}/bridge/request/networkmap`,payload:JSON.stringify({type:"raw",routes:!1,transaction:i}),qos:0,retain:!1});const v=await m(w,s-Date.now()),_=d(v)?.status;if(_&&"ok"!==_)throw a("provider");return n(v,l)}finally{for(const t of b)try{t()}catch{}}})}export{f as readZhaTopology,g as refreshZ2mTopology,u as subscribeZigbeeTopology,l as zigbeeTopologyRuntimeSnapshot};
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="9cba40b372f43cfad12eb432d5b2ee7847cb3f00f6d91e154ffada6f31df2fd4";import{n as t,b as n}from"./zigbee-topology-Di0_F8nQ.js";import{I as e}from"./houseplan-card-ClTiXI3p.js";const o=new WeakMap;function i(t){const n=function(t){const n=t?.connection||t;return!n||"object"!=typeof n&&"function"!=typeof n?null:n}(t);if(!n)return null;let e=o.get(n);return e||(e={revision:0,topologies:[],states:{},listeners:new Set,inflight:new Map},o.set(n,e)),e}function r(t,n,e){t.states={...t.states,[n]:e},function(t){t.revision++;for(const n of t.listeners)n()}(t)}function s(t){const n=t?.code;if("permission"===n||"unsupported"===n||"timeout"===n||"invalid_topic"===n||"invalid_payload"===n)return n;const e=String(t?.message||"").toLowerCase();return e.includes("unauthor")||e.includes("permission")?"permission":e.includes("unknown_command")||e.includes("not found")?"unsupported":"provider"}function a(t){return Object.assign(new Error(t),{code:t})}function c(t,n,e){const o=t.inflight.get(n);if(o)return o;r(t,n,{phase:"loading"});const i=e().then(n=>{if(!n.nodes.length&&n.warnings.some(t=>"invalid_payload"===t.code))throw a("invalid_payload");!function(t,n){t.topologies=[...t.topologies.filter(t=>!(t.provider===n.provider&&t.instanceId===n.instanceId)),n],r(t,"zha"===n.provider?"zha":`z2m:${n.instanceId}`,{phase:"ready",obtainedAt:n.obtainedAt,partial:n.warnings.length>0})}(t,n)}).catch(e=>{r(t,n,{phase:"error",error:s(e)})}).finally(()=>t.inflight.delete(n));return t.inflight.set(n,i),i}function l(t){const n=i(t);return n?{revision:n.revision,topologies:n.topologies,states:n.states}:{revision:0,topologies:[],states:{}}}function u(t,n){const e=i(t);return e?(e.listeners.add(n),()=>e.listeners.delete(n)):()=>{}}function f(t){if(!0!==t?.user?.is_admin)throw a("permission")}function p(n){const e=i(n);return e?c(e,"zha",async()=>{if(f(n),"function"!=typeof n?.callWS)throw a("unsupported");return t(await n.callWS({type:"zha/devices"}))}):Promise.resolve()}function d(t){return null!==t&&"object"==typeof t?t:null}function h(t){const n=d(t),e=n?.payload??t;if("string"!=typeof e)return e;try{return JSON.parse(e)}catch{return null}}async function m(t,n){let e;try{return await Promise.race([t,new Promise((t,o)=>{e=globalThis.setTimeout(()=>o(a("timeout")),Math.max(1,n))})])}finally{void 0!==e&&globalThis.clearTimeout(e)}}function g(t,o,r=15e4){const s=i(t);if(!s)return Promise.resolve();const l=e(o);return c(s,`z2m:${l||String(o)}`,async()=>{if(f(t),!l)throw a("invalid_topic");const e=t.connection,o=e?.subscribeMessage;if("function"!=typeof o||"function"!=typeof t?.callService)throw a("unsupported");const i=function(){const t=globalThis.crypto;return"function"==typeof t?.randomUUID?`houseplan-${t.randomUUID()}`:`houseplan-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}(),s=Date.now()+Math.max(1,r);let c=!1,u=null,p=null,g=null;const y=new Promise(t=>{u=t}),w=new Promise((t,n)=>{p=t,g=n}),b=[];try{const r=await o.call(e,t=>{!0===d(t)?.retain&&h(t)&&u?.()},{type:"mqtt/subscribe",topic:`${l}/bridge/info`});"function"==typeof r&&b.push(r);const f=await o.call(e,t=>{if(!0===d(t)?.retain)return;const n=h(t);null!==n?n&&function(t){const n=d(t),e=n?.transaction??d(n?.data)?.transaction;return"string"==typeof e||"number"==typeof e?String(e):null}(n)===i&&p?.(n):c&&g?.(a("invalid_payload"))},{type:"mqtt/subscribe",topic:`${l}/bridge/response/networkmap`});"function"==typeof f&&b.push(f),await m(y,Math.min(4e3,Math.max(1,s-Date.now()))),c=!0,await t.callService("mqtt","publish",{topic:`${l}/bridge/request/networkmap`,payload:JSON.stringify({type:"raw",routes:!1,transaction:i}),qos:0,retain:!1});const v=await m(w,s-Date.now()),_=d(v)?.status;if(_&&"ok"!==_)throw a("provider");return n(v,l)}finally{for(const t of b)try{t()}catch{}}})}export{p as readZhaTopology,g as refreshZ2mTopology,u as subscribeZigbeeTopology,l as zigbeeTopologyRuntimeSnapshot};
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="05ce2842760e7918081d8de1db20b0c2687438b9316a6dcdd9591094c2efb298";try{await import("./houseplan-assets/houseplan-card-Cue_bp_z.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).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+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__="9cba40b372f43cfad12eb432d5b2ee7847cb3f00f6d91e154ffada6f31df2fd4";try{await import("./houseplan-assets/houseplan-card-ClTiXI3p.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).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+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)}
+13 -2
View File
@@ -477,7 +477,7 @@ try {
deltas.cameraTerminalFullRenders = pinchTerminalRenders;
deltas.cameraFullRenders = fullRenderCount - cameraBefore;
const editorLongTasks = startLongTaskWindow();
const editorLongTaskWindows = [];
const editorConfigBefore = JSON.stringify(card._serverCfg);
const editorCallsBefore = wsCalls;
const editorPaintBefore = card._liveEditorPaintCount || 0;
@@ -499,6 +499,7 @@ try {
clientX: editorRect.left + ((x - editorView.x) / editorView.w) * editorRect.width,
clientY: editorRect.top + ((y - editorView.y) / editorView.h) * editorRect.height,
});
let editorPartLongTasks = startLongTaskWindow();
let partStarted = performance.now();
let editorMoveBefore = fullRenderCount;
for (let index = 0; index < 40; index++) {
@@ -518,6 +519,7 @@ try {
await frame();
editorTerminalRenders.push(fullRenderCount - editorTerminalBefore);
editorElapsed += performance.now() - partStarted;
editorLongTaskWindows.push(await editorPartLongTasks.stop());
card._tool = 'resize';
card.requestUpdate();
@@ -533,6 +535,7 @@ try {
editorStage = card.renderRoot.querySelector('.stage');
editorRect = editorStage.getBoundingClientRect();
editorView = card._viewOr(card._baseVb());
editorPartLongTasks = startLongTaskWindow();
partStarted = performance.now();
editorMoveBefore = fullRenderCount;
for (let index = 0; index < 40; index++) {
@@ -554,6 +557,7 @@ try {
await frame();
editorTerminalRenders.push(fullRenderCount - editorTerminalBefore);
editorElapsed += performance.now() - partStarted;
editorLongTaskWindows.push(await editorPartLongTasks.stop());
card._setMode('decor', false);
card._decorTool = 'select';
@@ -582,6 +586,7 @@ try {
card._dtStart(decorPointer(cornerX, cornerY), 'scale', [1, 1]);
await card.updateComplete;
await frame();
editorPartLongTasks = startLongTaskWindow();
partStarted = performance.now();
editorMoveBefore = fullRenderCount;
for (let index = 0; index < 40; index++) {
@@ -595,8 +600,14 @@ try {
await frame();
editorTerminalRenders.push(fullRenderCount - editorTerminalBefore);
editorElapsed += performance.now() - partStarted;
editorLongTaskWindows.push(await editorPartLongTasks.stop());
timings.editorSeriesMs = editorElapsed;
interactionLongTasks.editorSeries = await editorLongTasks.stop();
interactionLongTasks.editorSeries = {
supported: editorLongTaskWindows.every((window) => window.supported),
count: editorLongTaskWindows.reduce((sum, window) => sum + window.count, 0),
maxMs: Math.max(0, ...editorLongTaskWindows.map((window) => window.maxMs)),
totalMs: editorLongTaskWindows.reduce((sum, window) => sum + window.totalMs, 0),
};
deltas.editorMoveFullRenders = editorMoveRenders;
deltas.editorTerminalFullRenders = editorTerminalRenders;
deltas.editorLivePaints = (card._liveEditorPaintCount || 0) - editorPaintBefore;
+1
View File
@@ -325,6 +325,7 @@ const drawn = await page.evaluate(async () => {
},
options: { minDim: 1, eps: 0.01 }, rooms: resizeRooms, openings: [],
snapshotIdentity: snapshot, before: JSON.parse(snapshot), wallUnionBefore: null,
epochBefore: c._cfgEpoch,
});
c._resize.move({
pointerId: 912, point: [687.5, 250], step: 0.5, snap: (point) => point,
+150
View File
@@ -0,0 +1,150 @@
// #451: production-bundle guard for the boundary between HA intake, full Lit
// renders and RAF-owned interaction layers. The large benchmark owns timings;
// this fast smoke runs on every selected-smoke pass and owns wiring semantics.
import { launch, checkAll, finish } from './serve.mjs';
const { page, browser } = await launch();
const result = await page.evaluate(async () => {
const card = window.__card;
const frame = () => new Promise((done) => requestAnimationFrame(() => requestAnimationFrame(done)));
const until = async (predicate, timeout = 5000) => {
const started = performance.now();
while (!predicate()) {
if (performance.now() - started > timeout) throw new Error('render invalidation smoke timed out');
await new Promise((done) => setTimeout(done, 10));
}
};
const out = {};
card._setMode('view', false);
await card.updateComplete;
if ('_bootSoft' in card) await until(() => card._bootSoft === false);
await frame();
let renders = 0;
let intakes = 0;
let scans = 0;
const originalRenderBody = card._renderBody.bind(card);
const originalObserve = card._renderLife.observe.bind(card._renderLife);
const originalBindingStatus = card._bindingStatus.bind(card);
card._renderBody = (...args) => { renders++; return originalRenderBody(...args); };
card._renderLife.observe = (...args) => { intakes++; return originalObserve(...args); };
card._bindingStatus = (...args) => { scans++; return originalBindingStatus(...args); };
const dependency = [...(card._visibleDeviceSnapshot?.entityIds || [])]
.find((id) => card.hass.states?.[id]);
const originalHass = card.hass;
const stableConfig = JSON.stringify(card._serverCfg);
const stableLayout = JSON.stringify(card._layout);
const withState = (id, state) => ({
...card.hass,
states: { ...card.hass.states, [id]: { ...card.hass.states[id], state } },
});
const irrelevantBefore = renders;
const intakeBefore = intakes;
for (let index = 0; index < 5; index++) {
card.hass = {
...card.hass,
states: {
...card.hass.states,
[`sensor.hp_451_irrelevant_${index}`]: { entity_id: `sensor.hp_451_irrelevant_${index}`, state: String(index), attributes: {} },
},
};
}
await card.updateComplete;
await frame();
out.irrelevantHaHasNoFullRender = renders - irrelevantBefore === 0;
out.irrelevantHaStillIntakes = intakes - intakeBefore === 5;
const relevantBefore = renders;
if (dependency) card.hass = withState(dependency, '451-outside');
await card.updateComplete;
await frame();
out.relevantHaRendersOnce = !!dependency && renders - relevantBefore === 1;
scans = 0;
const heavy = card.renderRoot.querySelector('.wallbodies');
const stage = card.renderRoot.querySelector('.stage');
const rect = stage.getBoundingClientRect();
const hoverBefore = renders;
const device = card.renderRoot.querySelector('[data-hp="device"]');
if (device) {
device.dispatchEvent(new PointerEvent('pointerover', {
bubbles: true, composed: true, pointerId: 450, pointerType: 'mouse', isPrimary: true,
clientX: rect.left + 220, clientY: rect.top + 220,
}));
device.dispatchEvent(new PointerEvent('pointermove', {
bubbles: true, composed: true, pointerId: 450, pointerType: 'mouse', isPrimary: true,
clientX: rect.left + 225, clientY: rect.top + 225,
}));
}
await frame();
const liveTip = card.renderRoot.querySelector('[data-hp-live-tip]');
out.hoverUsesLightweightPaint = !!device && renders === hoverBefore
&& !!liveTip && !liveTip.hidden;
device?.dispatchEvent(new PointerEvent('pointerleave', {
bubbles: false, composed: true, pointerId: 450, pointerType: 'mouse', isPrimary: true,
}));
await frame();
const pointer = (type, x, y, buttons) => stage.dispatchEvent(new PointerEvent(type, {
bubbles: true, composed: true, pointerId: 451, pointerType: 'mouse', isPrimary: true,
button: 0, buttons, clientX: rect.left + x, clientY: rect.top + y,
}));
pointer('pointerdown', 300, 300, 1);
await card.updateComplete;
const panBefore = renders;
const panIntakeBefore = intakes;
for (let index = 0; index < 3; index++) {
pointer('pointermove', 320 + index * 20, 315 + index * 10, 1);
if (dependency) card.hass = withState(dependency, `451-drag-${index}`);
}
await card.updateComplete;
await frame();
out.panAndDeferredHaSkipFullRender = renders === panBefore;
out.deferredHaTicksStillIntake = intakes - panIntakeBefore === 3;
const panTerminalBefore = renders;
pointer('pointerup', 360, 335, 0);
await card.updateComplete;
await frame();
out.panTerminalRendersOnce = renders - panTerminalBefore === 1;
out.deferredHaIsLastWins = !!dependency
&& card._renderPlanHass.states?.[dependency]?.state === '451-drag-2';
card._setMode('plan', false);
card._tool = 'draw';
card._path = [[100, 100]];
card.requestUpdate();
await card.updateComplete;
await until(() => !card._modeTransitionBusy && card._continuity.state === 'steady');
for (let index = 0; index < 5; index++) await frame();
await frame();
const editorBefore = renders;
const editorPaintBefore = card._liveEditorPaintCount || 0;
for (let index = 0; index < 20; index++) {
stage.dispatchEvent(new PointerEvent('pointermove', {
bubbles: true, composed: true, pointerId: 452, pointerType: 'mouse', isPrimary: true,
clientX: rect.left + 110 + index, clientY: rect.top + 120 + index,
}));
}
await frame();
out.editorMovesSkipFullRender = renders === editorBefore;
out.editorMovesUseRafPaint = (card._liveEditorPaintCount || 0) > editorPaintBefore;
card._cursorPt = null;
card._path = [];
const editorTerminalBefore = renders;
card.requestUpdate();
await card.updateComplete;
await frame();
out.editorTerminalHasAtMostOneRender = renders - editorTerminalBefore <= 1;
out.heavySceneIdentityStable = card.renderRoot.querySelector('.wallbodies') === heavy;
out.pointerWindowsSkipDiagnosticScans = scans === 0;
out.interactionsDoNotWriteConfig = JSON.stringify(card._serverCfg) === stableConfig;
out.interactionsDoNotWriteLayout = JSON.stringify(card._layout) === stableLayout;
card.hass = originalHass;
return out;
});
checkAll(result);
await finish(browser, result);
+85 -85
View File
@@ -1,182 +1,182 @@
{
"schema": 1,
"fingerprint": "05ce2842760e7918081d8de1db20b0c2687438b9316a6dcdd9591094c2efb298",
"fingerprint": "9cba40b372f43cfad12eb432d5b2ee7847cb3f00f6d91e154ffada6f31df2fd4",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-Cue_bp_z.js",
"houseplan-assets/houseplan-card-ClTiXI3p.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 296492,
"initialViewGzipBytes": 296711,
"lazyFiles": [
"houseplan-assets/backdrop-pick-Rc7DbOnh.js",
"houseplan-assets/de-BsO3XZz2.js",
"houseplan-assets/editor-B5aKTgBM.js",
"houseplan-assets/fr-BND5clgi.js",
"houseplan-assets/houseplan-editor-runtime-Byj_OtmL.js",
"houseplan-assets/houseplan-onboarding-runtime-BPeqwGUM.js",
"houseplan-assets/hp-zigbee-topology-overlay-B4dxvXzw.js",
"houseplan-assets/live-interaction-runtime-BKpy4fJl.js",
"houseplan-assets/zigbee-topology-C9RnitZl.js",
"houseplan-assets/zigbee-topology-runtime-Cjou-8UI.js"
"houseplan-assets/backdrop-pick-DxIrP2TG.js",
"houseplan-assets/de-DrM43PtS.js",
"houseplan-assets/editor-DF8nDJsl.js",
"houseplan-assets/fr-BUknt1Ee.js",
"houseplan-assets/houseplan-editor-runtime-CEjxcGHj.js",
"houseplan-assets/houseplan-onboarding-runtime-CDcvp0Jc.js",
"houseplan-assets/hp-zigbee-topology-overlay-BclGSBEr.js",
"houseplan-assets/live-interaction-runtime-Cm2wMojA.js",
"houseplan-assets/zigbee-topology-Di0_F8nQ.js",
"houseplan-assets/zigbee-topology-runtime-B9aBn1Rm.js"
],
"lazyGzipBytes": 238059,
"lazyGzipBytes": 239429,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-Rc7DbOnh.js",
"houseplan-assets/editor-B5aKTgBM.js",
"houseplan-assets/houseplan-editor-runtime-Byj_OtmL.js",
"houseplan-assets/zigbee-topology-C9RnitZl.js"
"houseplan-assets/backdrop-pick-DxIrP2TG.js",
"houseplan-assets/editor-DF8nDJsl.js",
"houseplan-assets/houseplan-editor-runtime-CEjxcGHj.js",
"houseplan-assets/zigbee-topology-Di0_F8nQ.js"
],
"lazyEditorGzipBytes": 175720,
"lazyEditorGzipBytes": 177118,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-Rc7DbOnh.js",
"houseplan-assets/houseplan-onboarding-runtime-BPeqwGUM.js"
"houseplan-assets/backdrop-pick-DxIrP2TG.js",
"houseplan-assets/houseplan-onboarding-runtime-CDcvp0Jc.js"
],
"lazyOnboardingGzipBytes": 14239,
"lazyOnboardingGzipBytes": 14237,
"lazyLocaleFiles": [
"houseplan-assets/de-BsO3XZz2.js",
"houseplan-assets/fr-BND5clgi.js"
"houseplan-assets/de-DrM43PtS.js",
"houseplan-assets/fr-BUknt1Ee.js"
],
"lazyLocaleGzipBytes": 49117,
"lazyLocaleGzipBytes": 49095,
"files": [
{
"path": "houseplan-assets/backdrop-pick-Rc7DbOnh.js",
"sha256": "49133ce5325444f6d1a288db1d559409a1cbf148530b57b36c3e3b35422b563f",
"path": "houseplan-assets/backdrop-pick-DxIrP2TG.js",
"sha256": "40a2b20a1d2895882f9f5253c23012c4082ff794f59d69d16906cdbd0ef0b81b",
"rawBytes": 21417,
"gzipBytes": 7283,
"gzipBytes": 7279,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Cue_bp_z.js"
"houseplan-assets/houseplan-card-ClTiXI3p.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-BsO3XZz2.js",
"sha256": "ea45e6e4b85447b9d64cebd3e178d2b8d0e956e2b210c540ea0157838848b348",
"path": "houseplan-assets/de-DrM43PtS.js",
"sha256": "8eef78c8bb65d4dd600c779a6d9ab39f8edc2b05dec690624cff32734181f102",
"rawBytes": 82182,
"gzipBytes": 24828,
"gzipBytes": 24817,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-B5aKTgBM.js",
"sha256": "760d90c44a967cb14af4c6eb947cf70460bf2ef5173d7c77874b49f2b10f170b",
"rawBytes": 3826,
"gzipBytes": 1581,
"path": "houseplan-assets/editor-DF8nDJsl.js",
"sha256": "abb853b6f918c548c0cb09101dc9186340fccc5e252a04f28e7ca113e43d04c2",
"rawBytes": 3831,
"gzipBytes": 1580,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Cue_bp_z.js"
"houseplan-assets/houseplan-card-ClTiXI3p.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-BND5clgi.js",
"sha256": "18685ca59a983d0c8bda581f53aed49123fa8f88e450c9e04b387e11427aa761",
"path": "houseplan-assets/fr-BUknt1Ee.js",
"sha256": "54ea601d94848dca21dc418c66be5dd3efed711ddbd167e1b3803aa4eca34816",
"rawBytes": 84312,
"gzipBytes": 24289,
"gzipBytes": 24278,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-Cue_bp_z.js",
"sha256": "8861c1b21b3bfd68b283d1f207924bc8cb24989b32214379afa78afbf848df7d",
"rawBytes": 1053264,
"gzipBytes": 295697,
"path": "houseplan-assets/houseplan-card-ClTiXI3p.js",
"sha256": "23062c58e7a572faeaceaf8690447cf3efc26a35394ca616f95b942fb4ff342a",
"rawBytes": 1053904,
"gzipBytes": 295917,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-BsO3XZz2.js",
"houseplan-assets/editor-B5aKTgBM.js",
"houseplan-assets/fr-BND5clgi.js",
"houseplan-assets/houseplan-editor-runtime-Byj_OtmL.js",
"houseplan-assets/houseplan-onboarding-runtime-BPeqwGUM.js",
"houseplan-assets/hp-zigbee-topology-overlay-B4dxvXzw.js",
"houseplan-assets/live-interaction-runtime-BKpy4fJl.js"
"houseplan-assets/de-DrM43PtS.js",
"houseplan-assets/editor-DF8nDJsl.js",
"houseplan-assets/fr-BUknt1Ee.js",
"houseplan-assets/houseplan-editor-runtime-CEjxcGHj.js",
"houseplan-assets/houseplan-onboarding-runtime-CDcvp0Jc.js",
"houseplan-assets/hp-zigbee-topology-overlay-BclGSBEr.js",
"houseplan-assets/live-interaction-runtime-Cm2wMojA.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-Byj_OtmL.js",
"sha256": "39626dfebe5420ddfafa29bbbce1a50ffc014a638d1dbe808d068ff2b6713f50",
"rawBytes": 614348,
"gzipBytes": 162012,
"path": "houseplan-assets/houseplan-editor-runtime-CEjxcGHj.js",
"sha256": "edd71327a1d38a9175f85f1f4caede3a5ec75cb0e1ee76e02153051149b08296",
"rawBytes": 618575,
"gzipBytes": 163415,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-Rc7DbOnh.js",
"houseplan-assets/houseplan-card-Cue_bp_z.js",
"houseplan-assets/zigbee-topology-C9RnitZl.js"
"houseplan-assets/backdrop-pick-DxIrP2TG.js",
"houseplan-assets/houseplan-card-ClTiXI3p.js",
"houseplan-assets/zigbee-topology-Di0_F8nQ.js"
],
"dynamicImports": [
"houseplan-assets/zigbee-topology-runtime-Cjou-8UI.js"
"houseplan-assets/zigbee-topology-runtime-B9aBn1Rm.js"
]
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-BPeqwGUM.js",
"sha256": "81d4372e2cdd3cafb7d7ae201c7678d4b72b56bfe241b5e35a64f9197f2c9768",
"path": "houseplan-assets/houseplan-onboarding-runtime-CDcvp0Jc.js",
"sha256": "8b98688fdb1c1b0ef8c2e1bdba4e776922a12dad06694df686406d237ddae545",
"rawBytes": 28088,
"gzipBytes": 6956,
"gzipBytes": 6958,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-Rc7DbOnh.js",
"houseplan-assets/houseplan-card-Cue_bp_z.js"
"houseplan-assets/backdrop-pick-DxIrP2TG.js",
"houseplan-assets/houseplan-card-ClTiXI3p.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/hp-zigbee-topology-overlay-B4dxvXzw.js",
"sha256": "e2cd2c36ce28369920428b8aa19975c8cbf91749f52da7fd5a9a1fcc09938e07",
"path": "houseplan-assets/hp-zigbee-topology-overlay-BclGSBEr.js",
"sha256": "b16af400fa662e3f8b583655654a4a51d97e17632ce2609070546049995e9f4e",
"rawBytes": 6006,
"gzipBytes": 2377,
"gzipBytes": 2373,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Cue_bp_z.js",
"houseplan-assets/zigbee-topology-C9RnitZl.js",
"houseplan-assets/zigbee-topology-runtime-Cjou-8UI.js"
"houseplan-assets/houseplan-card-ClTiXI3p.js",
"houseplan-assets/zigbee-topology-Di0_F8nQ.js",
"houseplan-assets/zigbee-topology-runtime-B9aBn1Rm.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/live-interaction-runtime-BKpy4fJl.js",
"sha256": "d06b3e2c1784f55e36581863a3c06d2bcd64e238499e81417afb6cbfdd8e58f9",
"path": "houseplan-assets/live-interaction-runtime-Cm2wMojA.js",
"sha256": "864dbac45c561ca96bd101ad7d02609daca27f2786250607a0f030fdc53c5e12",
"rawBytes": 5196,
"gzipBytes": 2127,
"gzipBytes": 2124,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Cue_bp_z.js"
"houseplan-assets/houseplan-card-ClTiXI3p.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-C9RnitZl.js",
"sha256": "423c2ae23cd2e688411935ace0924868e3cb7654c382614411dd97e548e01bd3",
"path": "houseplan-assets/zigbee-topology-Di0_F8nQ.js",
"sha256": "b2d2fc995f582cc9e11f6b8ee33deff93879bbafaf96900e8ac7ef090f5148a8",
"rawBytes": 12382,
"gzipBytes": 4844,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Cue_bp_z.js"
"houseplan-assets/houseplan-card-ClTiXI3p.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-runtime-Cjou-8UI.js",
"sha256": "40c1b1e0b1367c51296177653b264fd1a1cfb26b4b98eb9d83db216ee626774f",
"path": "houseplan-assets/zigbee-topology-runtime-B9aBn1Rm.js",
"sha256": "7bf47628bde723081c132b9af5172b93f340baf5efb969cb40381d1af2345fce",
"rawBytes": 3941,
"gzipBytes": 1762,
"gzipBytes": 1761,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Cue_bp_z.js",
"houseplan-assets/zigbee-topology-C9RnitZl.js"
"houseplan-assets/houseplan-card-ClTiXI3p.js",
"houseplan-assets/zigbee-topology-Di0_F8nQ.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "93b3e318550fa784c9487b41b67302d6259e44bea8f2407613f9373d1354da61",
"sha256": "ba2206bd6eca725fd19155de72b02ba441ea0aa9f51891f0f7080f7141e9e61c",
"rawBytes": 1183,
"gzipBytes": 795,
"gzipBytes": 794,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-Cue_bp_z.js"
"houseplan-assets/houseplan-card-ClTiXI3p.js"
],
"dynamicImports": []
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-14
View File
@@ -1,14 +0,0 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="05ce2842760e7918081d8de1db20b0c2687438b9316a6dcdd9591094c2efb298";import{k as e,l as o,t,dq as s,A as a,dr as i,ds as l,dt as r,E as n,c}from"./houseplan-card-Cue_bp_z.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 r="string"==typeof this._config?.default_floor?this._config.default_floor:"",n=[...e];return r&&!e.some(e=>e.value===r)&&n.unshift({value:r,label:r}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:n}}}:{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 r();if("warm"===e)return n;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(_.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:f})}
</div>`:a}
${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);
+14
View File
@@ -0,0 +1,14 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="9cba40b372f43cfad12eb432d5b2ee7847cb3f00f6d91e154ffada6f31df2fd4";import{v as e,l as o,z as t,dt as s,A as a,du as i,dv as l,dw as n,E as r,c}from"./houseplan-card-ClTiXI3p.js";class f 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,f={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")},h=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
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
.computeLabel=${e=>f[e.name]||e.name}
@value-changed=${this._valueChanged}
></ha-form>`;return c`
${d(h.slice(0,3))}
${_?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:_})}
</div>`:a}
${d(h.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)}}f.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",f);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="05ce2842760e7918081d8de1db20b0c2687438b9316a6dcdd9591094c2efb298";import{k as e,A as t,du as i,w as s,c as o,l as r,j as n}from"./houseplan-card-Cue_bp_z.js";import{a,r as h,t as d}from"./zigbee-topology-C9RnitZl.js";import{zigbeeTopologyRuntimeSnapshot as p,subscribeZigbeeTopology as c}from"./zigbee-topology-runtime-Cjou-8UI.js";const l={revision:0,topologies:[],states:{}};class v extends e{constructor(){super(...arguments),this.devices=[],this.currentSpace="",this._runtime=l,this._hovered="",this._pointerOver=e=>{if(!this._mouseAllowed(e))return void this._clear();const t=this._deviceFromEvent(e),i=t?.dataset.id||"";i&&i!==this._hovered&&(this._hovered=i,this.requestUpdate())},this._pointerOut=e=>{const t=this._deviceFromEvent(e);if(!t||t.dataset.id!==this._hovered)return;const i=e.relatedTarget instanceof Element?e.relatedTarget.closest('[data-hp="device"]'):null;i?.dataset.id!==this._hovered&&this._clear()},this._pointerDown=e=>{"mouse"!==e.pointerType&&this._clear()}}connectedCallback(){super.connectedCallback();const e=this.getRootNode();e.host&&"undefined"!=typeof MutationObserver&&(this._hoverGateObserver=new MutationObserver(()=>{e.host.hasAttribute("data-pointer-hover")||this._clear()}),this._hoverGateObserver.observe(e.host,{attributes:!0,attributeFilter:["data-pointer-hover"]})),queueMicrotask(()=>this._connectParent())}disconnectedCallback(){this._release?.(),this._release=void 0,this._disconnectParent(),this._hoverGateObserver?.disconnect(),this._hoverGateObserver=void 0,this._hovered="",super.disconnectedCallback()}updated(e){e.has("hass")&&(this._release?.(),this._runtime=p(this.hass),this._mappedMemo=void 0,this._release=c(this.hass,()=>{this._runtime=p(this.hass),this._mappedMemo=void 0,this.requestUpdate()})),(e.has("currentSpace")||e.has("devices")||e.has("registry"))&&(this._mappedMemo=void 0,this.devices.some(e=>e.id===this._hovered&&e.space===this.currentSpace)||(this._hovered=""))}_connectParent(){const e=this.parentElement;e&&e!==this._parent&&(this._disconnectParent(),this._parent=e,e.addEventListener("pointerover",this._pointerOver),e.addEventListener("pointerout",this._pointerOut),e.addEventListener("pointerdown",this._pointerDown,!0))}_disconnectParent(){this._parent?.removeEventListener("pointerover",this._pointerOver),this._parent?.removeEventListener("pointerout",this._pointerOut),this._parent?.removeEventListener("pointerdown",this._pointerDown,!0),this._parent=void 0}_deviceFromEvent(e){for(const t of e.composedPath())if(t instanceof HTMLElement&&"device"===t.dataset.hp)return t;return null}_mouseAllowed(e){const t=this.getRootNode();return"mouse"===e.pointerType&&t.host?.hasAttribute?.("data-pointer-hover")}_clear(){this._hovered&&(this._hovered="",this.requestUpdate())}_position(e){const t=[...this._parent?.querySelectorAll(".dev[data-id]")||[]].find(t=>t.dataset.id===e);if(!t)return null;const i=Number.parseFloat(t.style.left),s=Number.parseFloat(t.style.top),o=t.getBoundingClientRect();return Number.isFinite(i)&&Number.isFinite(s)?{x:i,y:s,width:o.width,height:o.height}:null}render(){if(!this._hovered||!this.registry||!this._runtime.topologies.length)return t;const e=this._mappedMemo,n=e&&e.runtime===this._runtime&&e.devices===this.devices&&e.registry===this.registry?e.mapped:a(this._runtime.topologies,this.devices,this.registry);e&&n===e.mapped||(this._mappedMemo={runtime:this._runtime,devices:this.devices,registry:this.registry,mapped:n});const p=h(n,this.currentSpace,this._hovered),c=this._position(this._hovered);if(!c)return t;const l=p.lines.map(e=>({...e,point:this._position(e.neighborMarkerId)})).filter(e=>!!e.point);return o`
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="9cba40b372f43cfad12eb432d5b2ee7847cb3f00f6d91e154ffada6f31df2fd4";import{v as e,A as t,dx as i,w as s,c as o,l as r,u as n}from"./houseplan-card-ClTiXI3p.js";import{a,r as h,t as d}from"./zigbee-topology-Di0_F8nQ.js";import{zigbeeTopologyRuntimeSnapshot as p,subscribeZigbeeTopology as c}from"./zigbee-topology-runtime-B9aBn1Rm.js";const l={revision:0,topologies:[],states:{}};class v extends e{constructor(){super(...arguments),this.devices=[],this.currentSpace="",this._runtime=l,this._hovered="",this._pointerOver=e=>{if(!this._mouseAllowed(e))return void this._clear();const t=this._deviceFromEvent(e),i=t?.dataset.id||"";i&&i!==this._hovered&&(this._hovered=i,this.requestUpdate())},this._pointerOut=e=>{const t=this._deviceFromEvent(e);if(!t||t.dataset.id!==this._hovered)return;const i=e.relatedTarget instanceof Element?e.relatedTarget.closest('[data-hp="device"]'):null;i?.dataset.id!==this._hovered&&this._clear()},this._pointerDown=e=>{"mouse"!==e.pointerType&&this._clear()}}connectedCallback(){super.connectedCallback();const e=this.getRootNode();e.host&&"undefined"!=typeof MutationObserver&&(this._hoverGateObserver=new MutationObserver(()=>{e.host.hasAttribute("data-pointer-hover")||this._clear()}),this._hoverGateObserver.observe(e.host,{attributes:!0,attributeFilter:["data-pointer-hover"]})),queueMicrotask(()=>this._connectParent())}disconnectedCallback(){this._release?.(),this._release=void 0,this._disconnectParent(),this._hoverGateObserver?.disconnect(),this._hoverGateObserver=void 0,this._hovered="",super.disconnectedCallback()}updated(e){e.has("hass")&&(this._release?.(),this._runtime=p(this.hass),this._mappedMemo=void 0,this._release=c(this.hass,()=>{this._runtime=p(this.hass),this._mappedMemo=void 0,this.requestUpdate()})),(e.has("currentSpace")||e.has("devices")||e.has("registry"))&&(this._mappedMemo=void 0,this.devices.some(e=>e.id===this._hovered&&e.space===this.currentSpace)||(this._hovered=""))}_connectParent(){const e=this.parentElement;e&&e!==this._parent&&(this._disconnectParent(),this._parent=e,e.addEventListener("pointerover",this._pointerOver),e.addEventListener("pointerout",this._pointerOut),e.addEventListener("pointerdown",this._pointerDown,!0))}_disconnectParent(){this._parent?.removeEventListener("pointerover",this._pointerOver),this._parent?.removeEventListener("pointerout",this._pointerOut),this._parent?.removeEventListener("pointerdown",this._pointerDown,!0),this._parent=void 0}_deviceFromEvent(e){for(const t of e.composedPath())if(t instanceof HTMLElement&&"device"===t.dataset.hp)return t;return null}_mouseAllowed(e){const t=this.getRootNode();return"mouse"===e.pointerType&&t.host?.hasAttribute?.("data-pointer-hover")}_clear(){this._hovered&&(this._hovered="",this.requestUpdate())}_position(e){const t=[...this._parent?.querySelectorAll(".dev[data-id]")||[]].find(t=>t.dataset.id===e);if(!t)return null;const i=Number.parseFloat(t.style.left),s=Number.parseFloat(t.style.top),o=t.getBoundingClientRect();return Number.isFinite(i)&&Number.isFinite(s)?{x:i,y:s,width:o.width,height:o.height}:null}render(){if(!this._hovered||!this.registry||!this._runtime.topologies.length)return t;const e=this._mappedMemo,n=e&&e.runtime===this._runtime&&e.devices===this.devices&&e.registry===this.registry?e.mapped:a(this._runtime.topologies,this.devices,this.registry);e&&n===e.mapped||(this._mappedMemo={runtime:this._runtime,devices:this.devices,registry:this.registry,mapped:n});const p=h(n,this.currentSpace,this._hovered),c=this._position(this._hovered);if(!c)return t;const l=p.lines.map(e=>({...e,point:this._position(e.neighborMarkerId)})).filter(e=>!!e.point);return o`
${l.length?s`<svg viewBox="0 0 100 100" preserveAspectRatio="none"
aria-hidden="true" data-hp="zigbee-topology-lines">
${l.map(e=>s`<line x1=${c.x} y1=${c.y}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="05ce2842760e7918081d8de1db20b0c2687438b9316a6dcdd9591094c2efb298";import{n as t,b as n}from"./zigbee-topology-C9RnitZl.js";import{z as e}from"./houseplan-card-Cue_bp_z.js";const o=new WeakMap;function i(t){const n=function(t){const n=t?.connection||t;return!n||"object"!=typeof n&&"function"!=typeof n?null:n}(t);if(!n)return null;let e=o.get(n);return e||(e={revision:0,topologies:[],states:{},listeners:new Set,inflight:new Map},o.set(n,e)),e}function r(t,n,e){t.states={...t.states,[n]:e},function(t){t.revision++;for(const n of t.listeners)n()}(t)}function s(t){const n=t?.code;if("permission"===n||"unsupported"===n||"timeout"===n||"invalid_topic"===n||"invalid_payload"===n)return n;const e=String(t?.message||"").toLowerCase();return e.includes("unauthor")||e.includes("permission")?"permission":e.includes("unknown_command")||e.includes("not found")?"unsupported":"provider"}function a(t){return Object.assign(new Error(t),{code:t})}function c(t,n,e){const o=t.inflight.get(n);if(o)return o;r(t,n,{phase:"loading"});const i=e().then(n=>{if(!n.nodes.length&&n.warnings.some(t=>"invalid_payload"===t.code))throw a("invalid_payload");!function(t,n){t.topologies=[...t.topologies.filter(t=>!(t.provider===n.provider&&t.instanceId===n.instanceId)),n],r(t,"zha"===n.provider?"zha":`z2m:${n.instanceId}`,{phase:"ready",obtainedAt:n.obtainedAt,partial:n.warnings.length>0})}(t,n)}).catch(e=>{r(t,n,{phase:"error",error:s(e)})}).finally(()=>t.inflight.delete(n));return t.inflight.set(n,i),i}function l(t){const n=i(t);return n?{revision:n.revision,topologies:n.topologies,states:n.states}:{revision:0,topologies:[],states:{}}}function u(t,n){const e=i(t);return e?(e.listeners.add(n),()=>e.listeners.delete(n)):()=>{}}function p(t){if(!0!==t?.user?.is_admin)throw a("permission")}function f(n){const e=i(n);return e?c(e,"zha",async()=>{if(p(n),"function"!=typeof n?.callWS)throw a("unsupported");return t(await n.callWS({type:"zha/devices"}))}):Promise.resolve()}function d(t){return null!==t&&"object"==typeof t?t:null}function h(t){const n=d(t),e=n?.payload??t;if("string"!=typeof e)return e;try{return JSON.parse(e)}catch{return null}}async function m(t,n){let e;try{return await Promise.race([t,new Promise((t,o)=>{e=globalThis.setTimeout(()=>o(a("timeout")),Math.max(1,n))})])}finally{void 0!==e&&globalThis.clearTimeout(e)}}function g(t,o,r=15e4){const s=i(t);if(!s)return Promise.resolve();const l=e(o);return c(s,`z2m:${l||String(o)}`,async()=>{if(p(t),!l)throw a("invalid_topic");const e=t.connection,o=e?.subscribeMessage;if("function"!=typeof o||"function"!=typeof t?.callService)throw a("unsupported");const i=function(){const t=globalThis.crypto;return"function"==typeof t?.randomUUID?`houseplan-${t.randomUUID()}`:`houseplan-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}(),s=Date.now()+Math.max(1,r);let c=!1,u=null,f=null,g=null;const y=new Promise(t=>{u=t}),w=new Promise((t,n)=>{f=t,g=n}),b=[];try{const r=await o.call(e,t=>{!0===d(t)?.retain&&h(t)&&u?.()},{type:"mqtt/subscribe",topic:`${l}/bridge/info`});"function"==typeof r&&b.push(r);const p=await o.call(e,t=>{if(!0===d(t)?.retain)return;const n=h(t);null!==n?n&&function(t){const n=d(t),e=n?.transaction??d(n?.data)?.transaction;return"string"==typeof e||"number"==typeof e?String(e):null}(n)===i&&f?.(n):c&&g?.(a("invalid_payload"))},{type:"mqtt/subscribe",topic:`${l}/bridge/response/networkmap`});"function"==typeof p&&b.push(p),await m(y,Math.min(4e3,Math.max(1,s-Date.now()))),c=!0,await t.callService("mqtt","publish",{topic:`${l}/bridge/request/networkmap`,payload:JSON.stringify({type:"raw",routes:!1,transaction:i}),qos:0,retain:!1});const v=await m(w,s-Date.now()),_=d(v)?.status;if(_&&"ok"!==_)throw a("provider");return n(v,l)}finally{for(const t of b)try{t()}catch{}}})}export{f as readZhaTopology,g as refreshZ2mTopology,u as subscribeZigbeeTopology,l as zigbeeTopologyRuntimeSnapshot};
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="9cba40b372f43cfad12eb432d5b2ee7847cb3f00f6d91e154ffada6f31df2fd4";import{n as t,b as n}from"./zigbee-topology-Di0_F8nQ.js";import{I as e}from"./houseplan-card-ClTiXI3p.js";const o=new WeakMap;function i(t){const n=function(t){const n=t?.connection||t;return!n||"object"!=typeof n&&"function"!=typeof n?null:n}(t);if(!n)return null;let e=o.get(n);return e||(e={revision:0,topologies:[],states:{},listeners:new Set,inflight:new Map},o.set(n,e)),e}function r(t,n,e){t.states={...t.states,[n]:e},function(t){t.revision++;for(const n of t.listeners)n()}(t)}function s(t){const n=t?.code;if("permission"===n||"unsupported"===n||"timeout"===n||"invalid_topic"===n||"invalid_payload"===n)return n;const e=String(t?.message||"").toLowerCase();return e.includes("unauthor")||e.includes("permission")?"permission":e.includes("unknown_command")||e.includes("not found")?"unsupported":"provider"}function a(t){return Object.assign(new Error(t),{code:t})}function c(t,n,e){const o=t.inflight.get(n);if(o)return o;r(t,n,{phase:"loading"});const i=e().then(n=>{if(!n.nodes.length&&n.warnings.some(t=>"invalid_payload"===t.code))throw a("invalid_payload");!function(t,n){t.topologies=[...t.topologies.filter(t=>!(t.provider===n.provider&&t.instanceId===n.instanceId)),n],r(t,"zha"===n.provider?"zha":`z2m:${n.instanceId}`,{phase:"ready",obtainedAt:n.obtainedAt,partial:n.warnings.length>0})}(t,n)}).catch(e=>{r(t,n,{phase:"error",error:s(e)})}).finally(()=>t.inflight.delete(n));return t.inflight.set(n,i),i}function l(t){const n=i(t);return n?{revision:n.revision,topologies:n.topologies,states:n.states}:{revision:0,topologies:[],states:{}}}function u(t,n){const e=i(t);return e?(e.listeners.add(n),()=>e.listeners.delete(n)):()=>{}}function f(t){if(!0!==t?.user?.is_admin)throw a("permission")}function p(n){const e=i(n);return e?c(e,"zha",async()=>{if(f(n),"function"!=typeof n?.callWS)throw a("unsupported");return t(await n.callWS({type:"zha/devices"}))}):Promise.resolve()}function d(t){return null!==t&&"object"==typeof t?t:null}function h(t){const n=d(t),e=n?.payload??t;if("string"!=typeof e)return e;try{return JSON.parse(e)}catch{return null}}async function m(t,n){let e;try{return await Promise.race([t,new Promise((t,o)=>{e=globalThis.setTimeout(()=>o(a("timeout")),Math.max(1,n))})])}finally{void 0!==e&&globalThis.clearTimeout(e)}}function g(t,o,r=15e4){const s=i(t);if(!s)return Promise.resolve();const l=e(o);return c(s,`z2m:${l||String(o)}`,async()=>{if(f(t),!l)throw a("invalid_topic");const e=t.connection,o=e?.subscribeMessage;if("function"!=typeof o||"function"!=typeof t?.callService)throw a("unsupported");const i=function(){const t=globalThis.crypto;return"function"==typeof t?.randomUUID?`houseplan-${t.randomUUID()}`:`houseplan-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}(),s=Date.now()+Math.max(1,r);let c=!1,u=null,p=null,g=null;const y=new Promise(t=>{u=t}),w=new Promise((t,n)=>{p=t,g=n}),b=[];try{const r=await o.call(e,t=>{!0===d(t)?.retain&&h(t)&&u?.()},{type:"mqtt/subscribe",topic:`${l}/bridge/info`});"function"==typeof r&&b.push(r);const f=await o.call(e,t=>{if(!0===d(t)?.retain)return;const n=h(t);null!==n?n&&function(t){const n=d(t),e=n?.transaction??d(n?.data)?.transaction;return"string"==typeof e||"number"==typeof e?String(e):null}(n)===i&&p?.(n):c&&g?.(a("invalid_payload"))},{type:"mqtt/subscribe",topic:`${l}/bridge/response/networkmap`});"function"==typeof f&&b.push(f),await m(y,Math.min(4e3,Math.max(1,s-Date.now()))),c=!0,await t.callService("mqtt","publish",{topic:`${l}/bridge/request/networkmap`,payload:JSON.stringify({type:"raw",routes:!1,transaction:i}),qos:0,retain:!1});const v=await m(w,s-Date.now()),_=d(v)?.status;if(_&&"ok"!==_)throw a("provider");return n(v,l)}finally{for(const t of b)try{t()}catch{}}})}export{p as readZhaTopology,g as refreshZ2mTopology,u as subscribeZigbeeTopology,l as zigbeeTopologyRuntimeSnapshot};
+1 -1
View File
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="05ce2842760e7918081d8de1db20b0c2687438b9316a6dcdd9591094c2efb298";try{await import("./houseplan-assets/houseplan-card-Cue_bp_z.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).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+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__="9cba40b372f43cfad12eb432d5b2ee7847cb3f00f6d91e154ffada6f31df2fd4";try{await import("./houseplan-assets/houseplan-card-ClTiXI3p.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).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+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)}
+8 -1
View File
@@ -108,8 +108,15 @@ export const LOW_HEADROOM_WARNING_BYTES = 15_000;
* после реализации он занимает 295 743 Б gzip. Запас до потолка — 1 257 Б,
* до нижней границы полосы — 743 Б, то есть обе стороны остаются дальше
* наблюдаемого шума. Общий бюджет 300 000 Б и долг #367 не меняются.
*
* 2026-09-04, #451: потолок поднят 297 000 → 298 000. Терминальная доставка
* последнего HA-снимка и явное управление кадром редактора принадлежат
* основному View: без них пропущенный полный render оставлял бы устаревшее
* состояние после pan/resize. После исправления initial-граф занимает
* 296 717 Б gzip: 1 283 Б сверху и 717 Б до нижней границы полосы. Общий
* бюджет 300 000 Б не меняется; долг ленивого выноса #367 остаётся видимым.
*/
export const INITIAL_VIEW_GZIP_CEILING = 297_000;
export const INITIAL_VIEW_GZIP_CEILING = 298_000;
export const INITIAL_VIEW_CEILING_BAND = 2_000;
/**
+11
View File
@@ -105,6 +105,17 @@ function relocateEditorPatch(patch, cardSource, editorSource) {
// `find` обязан встречаться в файле ровно один раз: патч, который ложится «куда
// попало», проверяет не то, что объявлен проверять. Это контролирует --check.
const MUTANT_DEFINITIONS = [
{
id: 'render-invalidation-renders-irrelevant-ha',
guard: 'node demo/smoke_render_invalidation.mjs',
because: 'an irrelevant HA state row must still enter lifecycle intake without scheduling '
+ 'a full Lit render; the production smoke owns this card/runtime wiring (#451)',
patches: [{
file: 'src/houseplan-card.ts',
find: ' if (!render) return;',
replace: ' if (false && !render) return;',
}],
},
{
id: 'draft-delete-drops-the-promise',
guard: 'node demo/smoke_free_walls.mjs',
+16 -11
View File
@@ -668,6 +668,7 @@ export class HouseplanCard extends LitElement {
);
if (!render) return;
}
if (name !== undefined) this._terminalFrame = 0;
if (this._editorRuntime?._routeLiveEditorUpdate(name, oldValue)) return;
this._liveRt?.clear();
super.requestUpdate(name, oldValue, options);
@@ -3797,6 +3798,7 @@ export class HouseplanCard extends LitElement {
private _cfgEpoch = 0;
/** Exact settings-only Area lifecycle replacement which may reuse geometry. */
private _cfgEpochPreservedConfig: ServerConfig | null = null;
private _terminalFrame: 0 | 1 | 2 = 0; // 1=restored cancel, 2=deferred HA
private _modelCache: { key: string; model: SpaceModel[] } | null = null;
private _emptySpaceStateActive = false;
private _decorSnapCache: {
@@ -4516,7 +4518,7 @@ export class HouseplanCard extends LitElement {
private _liveVp(now = false): void { this._liveRt ? this._liveRt.viewport(now) : this.requestUpdate(); }
private _syncLiveHover(): void { this._liveRt ? this._liveRt.hover() : this.requestUpdate(); }
/** Bypass live routing so a gesture terminal publishes the last HA frame. */
private _flushHa(): void { if (this._liveRt?.take()) super.requestUpdate(); }
private _flushHa(): void { if (this._liveRt?.take()) { this._terminalFrame = 2; super.requestUpdate(); } }
private _onHaRegistryUpdate = (): void => {
const snapshot = haRegistrySnapshot(this.hass);
if (snapshot.revision === this._haRegistryRev && this._devices.length) return;
@@ -7787,10 +7789,9 @@ export class HouseplanCard extends LitElement {
}
/** #329 П1-П5 over one space. Pure input, no side effects. */
private _junctionLimitViolations(
config: any, spaceId: string, sharedGeometry?: any,
): JunctionLimitViolation[] {
return this._editorRuntimeOrThrow()._junctionLimitViolations(config, spaceId, sharedGeometry);
private _junctionLimitViolations(config: any, spaceId: string, sharedGeometry?: any,
roomIds?: ReadonlySet<string>): JunctionLimitViolation[] {
return this._editorRuntimeOrThrow()._junctionLimitViolations(config, spaceId, sharedGeometry, roomIds);
}
/** Localised refusal text for the first violation a write introduces. */
@@ -8244,8 +8245,9 @@ export class HouseplanCard extends LitElement {
/** Fixed-topology wall handles. Disabled geometry remains visible and
* explains why it cannot start a gesture; the old corner scale is gone. */
private _renderResizeLayer(view: { x: number; y: number; w: number; h: number }): TemplateResult {
return this._editorRuntimeOrThrow()._renderResizeLayer(view);
private _renderResizeLayer(view: { x: number; y: number; w: number; h: number },
roomIds?: readonly string[]): TemplateResult {
return this._editorRuntimeOrThrow()._renderResizeLayer(view, roomIds);
}
/** Openings of the current space in render units. */
@@ -8766,7 +8768,7 @@ export class HouseplanCard extends LitElement {
</g>` as unknown as TemplateResult;
}
private _renderDecorLayer(): TemplateResult {
private _renderDecorLayer(onlyId?: string | null): TemplateResult {
const W = NORM_W, H = this._decorH;
const editing = this._mode === 'decor';
const erasing = editing && this._decorTool === 'erase';
@@ -8784,7 +8786,7 @@ export class HouseplanCard extends LitElement {
const furnitureScreenScale = this._renderProjection === 'iso' ? 1 : furniturePlanScreenScale(
stage?.clientWidth, stage?.clientHeight, planView.w, planView.h,
);
const shapes = this._decorList.map((sh) => {
const shapes = this._decorList.filter((sh) => onlyId === undefined || sh.id === onlyId).map((sh) => {
const cls = 'dshape' + (editing && this._decorSel === sh.id ? ' dsel' : '');
const style = this._decorResolvedStyle(sh);
const strokeWidth = this._decorWidthUnits(sh);
@@ -11298,6 +11300,9 @@ export class HouseplanCard extends LitElement {
private _renderBody(): TemplateResult | typeof nothing | typeof noChange {
if (!this._config || !this.hass) return nothing;
const terminalFrame = this._terminalFrame;
this._terminalFrame = 0;
if (terminalFrame === 1) return noChange;
const localeGate = this._syncDangerConfirmLocaleGate();
if (localeGate === 'cold') return languageLoadingTemplate();
if (localeGate === 'warm') return noChange;
@@ -13006,8 +13011,8 @@ export class HouseplanCard extends LitElement {
return this._editorRuntimeOrThrow()._renderOpeningPlacementPreview();
}
private _renderOpenings(disp: SpaceDisplay): TemplateResult {
const items = this._openingsR;
private _renderOpenings(disp: SpaceDisplay, onlyIds?: readonly string[]): TemplateResult {
const items = onlyIds ? this._openingsR.filter((item) => onlyIds.includes(item.id)) : this._openingsR;
if (!items.length) return svg``;
const space = this._spaceModel();
if (!space) return svg``;
+72 -72
View File
@@ -104,8 +104,8 @@ import {
innerEdgeSpan, ownEdgeOffsets, thicknessCmAt,
} from './wall-thickness';
import {
checkNodeDistances, checkNodes, checkRoomClearance, checkSegmentLengths,
increasedViolations, type JunctionLimitViolation, type LimitSegment,
junctionLimitViolations, increasedViolations,
type JunctionLimitViolation, type LimitSegment,
} from './junction-limits';
import {
pointOnOpenCut, sanitizeOpenSpans,
@@ -974,7 +974,7 @@ export interface HouseplanEditorHostPort {
_infoCard: DevItem | null;
_innerRoomContour: (space: SpaceModel, roomId: string, openCuts?: number[][], roomWalls?: any, multiWallNodes?: MultiWallNodeMap | null | undefined) => number[][] | null;
_junctionLimitViolations: (
config: unknown, spaceId: string, sharedGeometry?: unknown,
config: unknown, spaceId: string, sharedGeometry?: unknown, roomIds?: ReadonlySet<string>,
) => JunctionLimitViolation[];
_isVacDev: (d: DevItem) => boolean;
_kiosk: boolean;
@@ -1101,6 +1101,7 @@ export interface HouseplanEditorHostPort {
_serverCfg: ServerConfig | null;
_serverStorage: boolean;
_settings: { exclude_integrations?: string[]; group_lights?: boolean; show_all?: boolean; filter_seeded?: boolean; icon_rules?: { pattern: string; icon: string; }[]; show_room_tooltip?: boolean; zigbee_topology?: { enabled?: boolean; z2m_base_topics?: string[] }; };
_terminalFrame: 0 | 1 | 2;
_settingsDialog: { colors: FillColors; glowRadius: number; bgColor: string | null; northDeg: number | null; bgMode: "static" | "daynight"; sunRays: boolean; showRoomTooltip: boolean; zigbeeTopology: ZigbeeTopologySettings; busy: boolean; } | null;
_supportDialog: SupportDialogState | null;
_showAll: boolean;
@@ -1185,10 +1186,11 @@ export interface HouseplanEditorHostPort {
export const EDITOR_RUNTIME_FINGERPRINT = '__HOUSEPLAN_SOURCE_FINGERPRINT__';
export class HouseplanEditorRuntime {
/** #330: baseline limits are stable across one resize gesture. */
private _junctionBaselineCache = new WeakMap<object, {
spaceId: string; fingerprint: string; violations: JunctionLimitViolation[];
}>();
private _resizePreviewNodes: MultiWallNodeMap | null = null;
private _resizeBaselineLimits: JunctionLimitViolation[] = [];
private _supportExpiryTimer?: number;
private _supportPreviewGeneration = 0;
private _decorAssetGuardReplace: boolean | null = null;
@@ -2049,51 +2051,12 @@ public _junctionLimitViolations(
/** #330 §4.7: an already computed masonry pass of THIS config (e.g. the
* resize preflight's artifact) — the union is never paid twice. */
sharedGeometry?: any,
roomIds?: ReadonlySet<string>,
): JunctionLimitViolation[] {
const space = (config?.spaces || []).find((item) => item?.id === spaceId);
if (!space) return [];
const cellCm = Number(space.cell_cm) > 0 ? Number(space.cell_cm) : 5;
const segments = this._limitSegmentsOf(space);
const violations = [
...checkNodes(segments),
...checkSegmentLengths(segments, cellCm, GRID_STEP_N),
...checkNodeDistances(segments, cellCm, GRID_STEP_N),
];
// #330 §4.7: without the shared passes every room recomputed the FULL
// junction topology and masonry union for itself. Compute both at most
// once and hand them to every room clearance check.
let sharedNodes: ReturnType<typeof multiWallNodesForGeometry> | null = null;
try {
sharedNodes = multiWallNodesForGeometry(
space.rooms || [], space.walls || [], [], GRID_STEP_N, cellCm, GRID_STEP_N, 1,
);
} catch { sharedNodes = null; }
let sharedRoomGeometry: any = sharedGeometry?.status === 'ok'
|| sharedGeometry?.status === 'degraded-extra'
? sharedGeometry.roomGeom : null;
if (!sharedRoomGeometry && sharedNodes && sharedNodes.nodes.length) {
try {
const geometry = wallBodiesGeometry(
space.rooms || [], space.walls || [], [], [], GRID_STEP_N, cellCm, GRID_STEP_N, 1,
);
sharedRoomGeometry = geometry?.status === 'ok' || geometry?.status === 'degraded-extra'
? geometry.roomGeom : null;
} catch { sharedRoomGeometry = null; }
}
for (const room of (space.rooms || [])) {
const roomId = String(room?.id || '');
if (!roomId) continue;
let inner: number[][] | null = null;
try {
inner = innerContourForRoom(
space.rooms || [], roomId, space.walls || [], [],
GRID_STEP_N, cellCm, GRID_STEP_N, 1,
sharedRoomGeometry ?? undefined, sharedNodes,
);
} catch { inner = null; }
violations.push(...checkRoomClearance(roomId, inner, cellCm, GRID_STEP_N));
}
return violations;
return space ? junctionLimitViolations(
config, spaceId, this._limitSegmentsOf(space), sharedGeometry, roomIds,
) : [];
}
public _junctionLimitLabel(violation: JunctionLimitViolation): string {
@@ -2106,6 +2069,7 @@ public _junctionLimitLabel(violation: JunctionLimitViolation): string {
public _junctionLimitsIntroduced(
candidate: any, previousConfig: any, spaceId: string,
candidateGeometry?: unknown,
affectedRoomIds?: readonly string[],
): JunctionLimitViolation[] {
// The baseline must be the previous document AS THE CANDIDATE SEES IT: a
// legacy space carries no wall catalogue at all, so comparing it raw with
@@ -2113,13 +2077,14 @@ public _junctionLimitsIntroduced(
// refused legitimate resizes of real plans. Both sides therefore cross
// the same identity barrier first. Current-version documents are already
// materialized, while legacy documents cross that barrier once.
const affected = affectedRoomIds?.length ? new Set(affectedRoomIds) : undefined;
let inherited: JunctionLimitViolation[] | undefined;
const previousSpace = (previousConfig?.spaces || [])
.find((space: any) => space?.id === spaceId);
let fingerprint = '';
try { fingerprint = spacePhysicalGeometryFingerprint(previousSpace); }
catch { fingerprint = ''; }
const cached = previousConfig && typeof previousConfig === 'object'
const cached = !affected && previousConfig && typeof previousConfig === 'object'
? this._junctionBaselineCache.get(previousConfig) : undefined;
if (cached && fingerprint && cached.fingerprint === fingerprint
&& cached.spaceId === spaceId) {
@@ -2130,26 +2095,34 @@ public _junctionLimitsIntroduced(
const baseline = Number(previousConfig?.model_version || 0) >= WALL_SEGMENT_MODEL_VERSION
? previousConfig
: commitWallSegmentModel(previousConfig).config;
inherited = this.host._junctionLimitViolations(baseline, spaceId);
inherited = this.host._junctionLimitViolations(
baseline, spaceId, affected ? null : undefined, affected,
);
} catch {
// An unmigratable baseline proves nothing about inheritance; never
// refuse the write on that basis.
return [];
}
if (previousConfig && typeof previousConfig === 'object' && fingerprint) {
if (!affected && previousConfig && typeof previousConfig === 'object' && fingerprint) {
this._junctionBaselineCache.set(previousConfig, {
spaceId, fingerprint, violations: inherited,
});
}
}
let next: JunctionLimitViolation[] = [];
try { next = this.host._junctionLimitViolations(candidate, spaceId, candidateGeometry); }
let comparison = inherited;
try {
next = this.host._junctionLimitViolations(candidate, spaceId, candidateGeometry, affected);
if (affected) comparison = inherited.filter(
(item) => item.rule !== 'clearance' || affected.has(item.subject),
);
}
catch {
// #331 §2.5: candidate judgment fails closed; only an unprovable
// baseline above is allowed to fail open.
return [{ rule: 'check_failed', subject: spaceId, actual: 0, limit: 0 }];
}
return increasedViolations(next, inherited);
return increasedViolations(next, comparison);
}
public _commitPhysicalGeometry(
@@ -3681,10 +3654,6 @@ public _rszProjectPreview(
if (wallRecordCarrierViolations(
changedWalls, wallCarriers, this.host._wallKeyPitch, NORM_W, s.walls || [],
).length) return { ok: false, reason: 'wall-metadata' };
const preflight = this._rszSpaceCandidateGeometry(this.host._space, sp);
if (!preflight.ok) {
return { ok: false, reason: 'physical-geometry' };
}
// #329 AC7a: a step that would ADD a junction-limit violation is never
// projected, so the drag stops at the last allowed position instead of
// committing an impossible plan.
@@ -3694,11 +3663,23 @@ public _rszProjectPreview(
(space) => (space?.id === this.host._space ? sp : space),
),
};
// #330 §4.7: reuse the masonry pass already computed by the resize
// preflight instead of rebuilding it while checking room clearance.
const limited = this._junctionLimitsIntroduced(
limitCandidate, this.host._serverCfg, this.host._space, preflight.wallGeometry,
try {
this._resizePreviewNodes = multiWallNodesForGeometry(
sp.rooms || [], sp.walls || [], [], GRID_STEP_N,
this.host._cellCm, this.host._gridPitch, NORM_W,
);
} catch { this._resizePreviewNodes = null; }
let limited: JunctionLimitViolation[];
try {
const affected = new Set(changedRoomIds);
const next = this.host._junctionLimitViolations(limitCandidate, this.host._space,
{ status: 'lightweight', multiWallNodes: this._resizePreviewNodes }, affected);
const baseline = this._resizeBaselineLimits.filter(
(item) => item.rule !== 'clearance' || affected.has(item.subject));
limited = increasedViolations(next, baseline);
} catch {
limited = [{ rule: 'check_failed', subject: this.host._space, actual: 0, limit: 0 }];
}
if (limited.length) {
this.host._rszLimitViolation = limited[0];
return { ok: false, reason: 'junction-limit' };
@@ -3709,7 +3690,7 @@ public _rszProjectPreview(
preview: { space: this.host._space, sp },
beforeWalls: s.walls || [],
afterWalls: sp.walls || [],
artifact: preflight.wallGeometry,
artifact: null,
},
};
}
@@ -3718,6 +3699,8 @@ public _rszAcceptPreview(
preview: ResizePreview | null, wallGeometry: ResizeWallArtifact | null,
): void {
this.host._cfgEpoch++;
if (this.host._physicalBodiesCache) this.host._physicalBodiesCache.key =
`${this.host._space}|${this.host._cfgEpoch}|${this.host._cellCm}|${this.host._gridPitch}`;
if (!preview || !wallGeometry) return;
const projected = wallBodiesGeometryPath(wallGeometry);
if (!projected) return;
@@ -3783,6 +3766,10 @@ public _rszEdgeDown(ev: PointerEvent, roomId: string, edge: number): void {
}
capturePointer(ev);
const plan = resolution.plan;
try {
this._resizeBaselineLimits = this.host._junctionLimitViolations(
this.host._serverCfg, this.host._space, null, new Set(plan.roomIds));
} catch { this._resizeBaselineLimits = []; }
const start = this._svgPoint(ev);
const wallUnionKey = `${this.host._space}|${this.host._cfgEpoch}|${rooms.length}`;
const wallUnionBefore = this.host._wallUnionCache?.key === wallUnionKey
@@ -3794,6 +3781,7 @@ public _rszEdgeDown(ev: PointerEvent, roomId: string, edge: number): void {
openings: this._rszOpenings(), snapshotIdentity,
before: JSON.parse(snapshotIdentity) as SpaceGeometryState,
wallUnionBefore,
epochBefore: this.host._cfgEpoch,
});
}
@@ -3899,15 +3887,13 @@ public _rszCancelDrag(pointerId?: number): void {
cancelHouseplanPointerMove(this.host, 'resize');
const result = this.host._resize.cancel(this._rszSnapshot(), pointerId);
if (result.kind === 'no-op') return;
// HP-1550-01/-03: a cancel just drops the overlay — the real config was
// never touched, so there is nothing to restore, no undo step and no write
this.host._cfgEpoch++;
// An identical cancel reuses the pre-drag structural caches and writes nothing.
if (result.restoreEpoch !== null) this.host._cfgEpoch = result.restoreEpoch;
else this.host._cfgEpoch++;
if (this.host._physicalBodiesCache) this.host._physicalBodiesCache.key =
`${this.host._space}|${this.host._cfgEpoch}|${this.host._cellCm}|${this.host._gridPitch}`;
if (result.restoreWallUnion) {
// The server-backed geometry did not change during the gesture. The
// preview advanced the structural epoch and temporarily selected its
// candidate union, but Cancel returns to the byte-identical pre-drag
// config. Alias that already-proved result under the fresh epoch instead
// of paying another O(n log n) polygon union on the next paint.
// Alias the already-proved pre-drag union under the restored epoch.
const space = this.host._spaceModel();
if (space) {
const key = `${this.host._space}|${this.host._cfgEpoch}|${space.rooms.length}`;
@@ -3916,6 +3902,10 @@ public _rszCancelDrag(pointerId?: number): void {
this.host._wallUnionCache = entry;
}
}
// Retain the canonical frame only when the drag was actually painted in
// the isolated plan-editor layer. Programmatic/non-editor callers still
// need a full render to replace their preview DOM.
if (result.restoreEpoch !== null && this.host._mode === 'plan') this.host._terminalFrame = 1;
this.host.requestUpdate();
}
@@ -3989,7 +3979,10 @@ public _rszEdgeLabels(
// masonry union + contour cache that the following render consumes;
// rebuilding both independently here doubled one Resize frame.
const floor = walls.length && space
? (this.host._innerRoomContour(space, id) || poly)
? (innerContourForRoom(
space.rooms, id, walls, this.host._openCuts(), this.host._wallKeyPitch,
this.host._cellCm, this.host._gridPitch, NORM_W, null, this._resizePreviewNodes,
) || poly)
: poly;
const m2 = physical.length
? geometryArea(floorMinusBodies(floor, physical))
@@ -4052,7 +4045,9 @@ public _renderResizeMeasurements(): TemplateResult | typeof nothing {
</g>`;
}
public _renderResizeLayer(view: { x: number; y: number; w: number; h: number }): TemplateResult {
public _renderResizeLayer(
view: { x: number; y: number; w: number; h: number }, roomIds?: readonly string[],
): TemplateResult {
const hr = Math.max(view.w * 0.013, 5); // finger-sized HIT radius on touch, like .vacfithandle
// Wall-handle glyph: half the old circle — a wall segment with two arrows
// pointing perpendicular to it (the directions the wall can be dragged).
@@ -4071,7 +4066,9 @@ public _renderResizeLayer(view: { x: number; y: number; w: number; h: number }):
// never once per room edge. During a drag the immutable gesture snapshot
// remains authoritative inside _rszResolution.
const renderSnapshot = this.host._resize.snapshotIdentity || this._rszSnapshot();
const visibleRooms = roomIds?.length ? new Set(roomIds) : null;
for (const r of rooms) {
if (visibleRooms && !visibleRooms.has(r.id)) continue;
for (let i = 0; i < r.poly.length; i++) {
const a = r.poly[i], b = r.poly[(i + 1) % r.poly.length];
if (Math.hypot(b[0] - a[0], b[1] - a[1]) < this.host._gridPitch) continue;
@@ -4189,11 +4186,14 @@ public _cancelDecorGesture(): void {
for (const key of ['plan_x', 'plan_y', 'plan_scale', 'plan_scale_x', 'plan_scale_y', 'plan_angle'] as const)
delete sp[key];
Object.assign(sp, copy(before.plan_transform || {}));
this.host._cfgEpoch++;
// The restored transform is byte-identical; keep geometry caches.
this.host._decorSnapCache = null;
}
this.host._decorMove = null;
this.host._dtDrag = null;
this.host._bdDrag = null;
// Mark after named terminal setters; only the unnamed cancel frame may be retained.
if (before && sp && this.host._mode === 'decor') this.host._terminalFrame = 1;
this.host.requestUpdate();
}
+57 -1
View File
@@ -10,7 +10,12 @@
*
* Thresholds are absolute (centimetres, degrees) and do not scale with the
* space's `cell_cm` (spec r1-L1).
*/
*/
import { GRID_STEP_N } from './space-geometry';
import {
innerContourForRoom, multiWallNodesForGeometry, wallBodiesGeometry,
} from './wall-thickness';
export const MIN_JUNCTION_ANGLE_DEG = 15;
export const MAX_JUNCTION_VALENCE = 6;
@@ -41,6 +46,57 @@ export interface LimitSegment {
cm?: number;
}
/** Shared full/affected-room validation used by editor writes and lightweight resize previews. */
export function junctionLimitViolations(
config: any,
spaceId: string,
segments: readonly LimitSegment[],
sharedGeometry?: any,
roomIds?: ReadonlySet<string>,
): JunctionLimitViolation[] {
const space = (config?.spaces || []).find((item: any) => item?.id === spaceId);
if (!space) return [];
const cellCm = Number(space.cell_cm) > 0 ? Number(space.cell_cm) : 5;
const violations = [
...checkNodes(segments), ...checkSegmentLengths(segments, cellCm, GRID_STEP_N),
...checkNodeDistances(segments, cellCm, GRID_STEP_N),
];
let nodes: ReturnType<typeof multiWallNodesForGeometry> | null =
sharedGeometry?.multiWallNodes || null;
if (!nodes) {
try {
nodes = multiWallNodesForGeometry(
space.rooms || [], space.walls || [], [], GRID_STEP_N, cellCm, GRID_STEP_N, 1,
);
} catch { nodes = null; }
}
let roomGeometry: any = sharedGeometry?.status === 'ok'
|| sharedGeometry?.status === 'degraded-extra' ? sharedGeometry.roomGeom : null;
const lightweight = sharedGeometry === null || sharedGeometry?.status === 'lightweight';
if (!roomGeometry && !lightweight && nodes?.nodes.length) {
try {
const geometry = wallBodiesGeometry(
space.rooms || [], space.walls || [], [], [], GRID_STEP_N, cellCm, GRID_STEP_N, 1,
);
roomGeometry = geometry?.status === 'ok' || geometry?.status === 'degraded-extra'
? geometry.roomGeom : null;
} catch { roomGeometry = null; }
}
for (const room of space.rooms || []) {
const roomId = String(room?.id || '');
if (!roomId || (roomIds && !roomIds.has(roomId))) continue;
let inner: number[][] | null = null;
try {
inner = innerContourForRoom(
space.rooms || [], roomId, space.walls || [], [], GRID_STEP_N, cellCm, GRID_STEP_N, 1,
lightweight ? null : roomGeometry ?? undefined, nodes,
);
} catch { inner = null; }
violations.push(...checkRoomClearance(roomId, inner, cellCm, GRID_STEP_N));
}
return violations;
}
const EPS = 1e-9;
/**
* #331 §2.1: below this two points are ONE node / a node is ON the wall —
+138 -14
View File
@@ -1,10 +1,18 @@
import { nothing, render, svg, type TemplateResult } from 'lit';
import { cancelHouseplanPointerMove } from './pointer-move-queue';
import { gridVisualUnits } from './grid-scale';
import {
wallBodyNeedsSolid, wallCmToUnits, wallEdgePathD,
wallHatchNeedsSolid, wallHatchStepUnits,
type WallEdgeBody,
} from './wall-thickness';
import { NORM_W } from './space-geometry';
interface LiveEditorState {
raf: number;
hidden: HTMLElement[];
transparent: HTMLElement[];
dimmed: HTMLElement[];
}
interface LiveEditorHost {
@@ -17,11 +25,11 @@ interface LiveEditorHost {
_physicalDrag?: unknown;
_physicalRotate?: unknown;
_decorDraft?: unknown;
_decorMove?: unknown;
_dtDrag?: unknown;
_decorMove?: { id: string } | null;
_dtDrag?: { id: string } | null;
_bdDrag?: unknown;
_opDrag?: unknown;
_resize?: { dragging: boolean };
_resize?: { dragging: boolean; plan?: { roomIds: string[]; movingOpeningIds: string[] } | null };
_layout: unknown;
_dtBox: { id: string; x: number; y: number; w: number; h: number } | null;
_dtSel: { id: string; kind: string } | null;
@@ -33,8 +41,8 @@ interface LiveEditorHost {
_spaceDisplayForRender: () => unknown;
_renderWallBodies: (display: unknown) => unknown;
_renderResizeMeasurements: () => unknown;
_renderOpenings: (display: unknown) => unknown;
_renderResizeLayer: (view: unknown) => unknown;
_renderOpenings: (display: unknown, onlyIds?: readonly string[]) => unknown;
_renderResizeLayer: (view: unknown, roomIds?: readonly string[]) => unknown;
_renderMarkupLayer: (viewBox: number[]) => unknown;
_renderHiddenWallDiagnosticOverlay: () => unknown;
_renderOpeningPlacementPreview: () => unknown;
@@ -43,12 +51,20 @@ interface LiveEditorHost {
_renderActiveChainInk: () => unknown;
_renderPlanSnapOverlay: () => unknown;
_renderWallThickUi: () => unknown;
_renderDecorLayer: () => unknown;
_renderDecorLayer: (onlyId?: string | null) => unknown;
_renderBackdropFrame: (view: unknown) => unknown;
_renderTextFrame: (view: unknown) => unknown;
_renderLiveEditorMeasurements: (view: unknown) => unknown;
_livePos: (device: { id: string }) => { x: number; y: number };
_scenePoint: (point: number[]) => number[];
_renderProjection: string;
_spaceModel: () => any;
_spaceWalls: any[];
_cellCm: number;
_gridPitch: number;
_openingsR: any[];
_stageEl: HTMLElement | null;
_fillColors: { wall_fill: { c: string; a: number } };
}
const states = new WeakMap<object, LiveEditorState>();
@@ -65,7 +81,7 @@ const gestureProperties = new Set<PropertyKey>([
const stateOf = (host: object): LiveEditorState => {
let state = states.get(host);
if (!state) {
state = { raf: 0, hidden: [], transparent: [] };
state = { raf: 0, hidden: [], transparent: [], dimmed: [] };
states.set(host, state);
}
return state;
@@ -132,6 +148,76 @@ const restore = (state: LiveEditorState): void => {
state.hidden.length = 0;
for (const element of state.transparent) element.style.removeProperty('opacity');
state.transparent.length = 0;
for (const element of state.dimmed) element.style.removeProperty('opacity');
state.dimmed.length = 0;
};
/** Resize moves paint individual wall strips; pointerup still validates the canonical union. */
const resizePreviewWalls = (
host: LiveEditorHost, display: any, roomIds: readonly string[] = [],
): TemplateResult => {
if (host._renderProjection === 'iso') return svg``;
const space = host._spaceModel();
if (!space) return svg``;
const affected = new Set(roomIds);
const boundary = (space.rooms || []).filter((room: any) => affected.has(room.id))
.flatMap((room: any) => (room.poly || []).map((point: number[], index: number) => [
point, room.poly[(index + 1) % room.poly.length],
]));
const onSegment = (point: number[], segment: number[][]): boolean => {
const [a, b] = segment;
const dx = b[0] - a[0], dy = b[1] - a[1];
const length2 = dx * dx + dy * dy;
if (!(length2 > 0)) return false;
const t = ((point[0] - a[0]) * dx + (point[1] - a[1]) * dy) / length2;
const clamped = Math.max(0, Math.min(1, t));
return Math.hypot(point[0] - a[0] - dx * clamped, point[1] - a[1] - dy * clamped) < 1e-4;
};
const edge = (a: number[], b: number[], cm: number, key: string): WallEdgeBody | null => {
const length = Math.hypot(b[0] - a[0], b[1] - a[1]);
if (!(length > 1e-9) || !(cm > 0)) return null;
const half = wallCmToUnits(cm, host._cellCm, host._gridPitch) / 2;
const nx = -(b[1] - a[1]) / length * half;
const ny = (b[0] - a[0]) / length * half;
return {
key, kind: 'outer', cm, a, b, depthUnits: half * 2,
quad: [[a[0] + nx, a[1] + ny], [b[0] + nx, b[1] + ny],
[b[0] - nx, b[1] - ny], [a[0] - nx, a[1] - ny]],
};
};
const edges = [
...host._spaceWalls.filter((wall) => {
const a = (wall.a || []).map((value: number) => value * NORM_W);
const b = (wall.b || []).map((value: number) => value * NORM_W);
return boundary.some((segment: number[][]) => onSegment(a, segment) && onSegment(b, segment));
}).map((wall, index) => edge(
(wall.a || []).map((value: number) => value * NORM_W),
(wall.b || []).map((value: number) => value * NORM_W),
wall.cm, wall.key || `wall-${index}`,
)),
].filter((item): item is WallEdgeBody => !!item);
const openings = host._openingsR.map((opening) => ({
x: opening.rx, y: opening.ry, angle: opening.angle, length: opening.rlen,
}));
const paths = edges.map((edge) => wallEdgePathD(edge, openings)).filter(Boolean);
if (!paths.length) return svg``;
const depth = Math.max(...edges.map((edge) => edge.depthUnits), 0);
const view = host._viewOr(host._baseVb());
const px = host._stageEl?.clientWidth && view.w ? host._stageEl.clientWidth / view.w : 1;
const solid = wallBodyNeedsSolid(depth, px)
|| wallHatchNeedsSolid(wallHatchStepUnits(host._cellCm), px);
const stroke = display?.color || '#607d8b';
const fill = host._fillColors.wall_fill;
const d = paths.join(' ');
return svg`<g class="wallbodies hp-resize-preview-walls"
style="--room-stroke:${stroke};--wall-fill:${fill.c};--wall-fill-op:${fill.a}">
<path class="wallbody-fill" data-component="preview" d=${d}
fill=${fill.c} fill-opacity=${fill.a} fill-rule="nonzero" stroke="none"></path>
<path class="wallbody ${solid ? 'solid' : ''}" data-hp="wall"
data-id="preview" data-kind="preview" d=${d}
fill=${solid ? 'none' : 'url(#hp-wall-hatch)'} fill-rule="nonzero"
stroke=${stroke} stroke-width=${gridVisualUnits(0.6, host._cellCm)}></path>
</g>`;
};
const planTemplate = (host: LiveEditorHost): TemplateResult => {
@@ -140,11 +226,12 @@ const planTemplate = (host: LiveEditorHost): TemplateResult => {
const measure = host._opMeasureView;
if (host._resize?.dragging) {
const display = host._spaceDisplayForRender();
const plan = host._resize.plan;
return svg`<g class="hp-live-resize" aria-hidden="true" pointer-events="none">
${host._renderWallBodies(display)}
${resizePreviewWalls(host, display, plan?.roomIds)}
${host._renderResizeMeasurements()}
${host._renderOpenings(display)}
${host._renderResizeLayer(view)}
${host._renderOpenings(display, plan?.movingOpeningIds)}
${host._renderResizeLayer(view, plan?.roomIds)}
</g>`;
}
const display = host._spaceDisplayForRender();
@@ -166,8 +253,9 @@ const editorTemplate = (host: LiveEditorHost): TemplateResult | typeof nothing =
if (host._mode === 'plan') return planTemplate(host);
if (host._mode === 'decor') {
const view = host._viewOr(host._baseVb());
const activeId = host._dtDrag?.id || host._decorMove?.id || null;
return svg`<g class="hp-live-decor" aria-hidden="true" pointer-events="none">
${host._renderDecorLayer()}
${host._renderDecorLayer(activeId)}
${host._renderBackdropFrame(view)}
${host._renderTextFrame(view)}
</g>`;
@@ -189,6 +277,37 @@ const paintDevice = (host: LiveEditorHost, root: ParentNode): void => {
element.style.top = `${((point[1] - view.y) / view.h) * 100}%`;
};
const makeDecorShapeTransparent = (
state: LiveEditorState, root: ParentNode, id: string,
): void => {
for (const element of root.querySelectorAll<HTMLElement>('.decorlayer [data-hp="decor"]')) {
if (element.closest('[data-hp-live-editor]') || element.dataset.id !== id) continue;
if (element.style.opacity === '0') continue;
element.style.opacity = '0';
state.transparent.push(element);
}
};
const makeMovingOpeningsTransparent = (
state: LiveEditorState, root: ParentNode, ids: readonly string[],
): void => {
const moving = new Set(ids);
for (const element of root.querySelectorAll<HTMLElement>('.openinglayer [data-hp="opening"]')) {
if (!element.closest('[data-hp-live-editor]') && moving.has(element.dataset.id || '')) {
element.style.opacity = '0';
state.transparent.push(element);
}
}
};
const dim = (state: LiveEditorState, root: ParentNode, selector: string): void => {
for (const element of root.querySelectorAll<HTMLElement>(selector)) {
if (element.closest('[data-hp-live-editor]')) continue;
element.style.opacity = '0.35';
state.dimmed.push(element);
}
};
/** One editor-only render root per card; the settled scene remains untouched. */
export function paintHouseplanEditor(value: object): void {
const host = value as LiveEditorHost;
@@ -201,11 +320,16 @@ export function paintHouseplanEditor(value: object): void {
if (!target) return;
if (host._mode === 'plan') {
makeTransparent(state, root, '.hp-editor-only-layer:not(.hp-plan-snap-layer)');
if (host._resize?.dragging || host._opDrag) hide(state, root, '.wallbodies');
if (host._resize?.dragging) makeTransparent(state, root, '.resize-layer');
if (host._opDrag) hide(state, root, '.wallbodies');
if (host._resize?.dragging) {
dim(state, root, '.wallbodies');
makeTransparent(state, root, '.resize-layer');
makeMovingOpeningsTransparent(state, root, host._resize.plan?.movingOpeningIds || []);
}
if (host._opDrag) makeTransparent(state, root, '.openinglayer');
} else if (host._mode === 'decor') {
makeTransparent(state, root, '.decorlayer');
const activeId = host._dtDrag?.id || host._decorMove?.id;
if (activeId) makeDecorShapeTransparent(state, root, activeId);
makeTransparent(state, root, '.dtframe, .backdropframe');
}
render(editorTemplate(host), target);
+7 -3
View File
@@ -40,7 +40,7 @@ export type ResizeFinishOutcome<TPreview, TBefore> =
| { kind: 'no-op' };
export type ResizeCancelOutcome<TWallUnion> =
| { kind: 'cancelled'; restoreWallUnion: TWallUnion | null }
| { kind: 'cancelled'; restoreWallUnion: TWallUnion | null; restoreEpoch: number | null }
| { kind: 'no-op' };
export interface ResizeBeginInput<TBefore, TWallUnion> {
@@ -54,6 +54,7 @@ export interface ResizeBeginInput<TBefore, TWallUnion> {
snapshotIdentity: string;
before: TBefore;
wallUnionBefore: TWallUnion | null;
epochBefore: number;
}
interface AcceptedPreview<TPreview, TLabels, TArtifact> {
@@ -79,6 +80,7 @@ interface ResizeSession<TPreview, TLabels, TBefore, TWallUnion, TArtifact> {
changedRoomIds: string[];
rejectionNotified: boolean;
wallUnionBefore: TWallUnion | null;
epochBefore: number;
accepted: AcceptedPreview<TPreview, TLabels, TArtifact> | null;
}
@@ -149,6 +151,7 @@ export class ResizeController<TPreview, TLabels, TBefore, TWallUnion, TArtifact>
changedRoomIds: [...input.plan.roomIds],
rejectionNotified: false,
wallUnionBefore: input.wallUnionBefore,
epochBefore: input.epochBefore,
accepted: null,
};
return true;
@@ -273,10 +276,11 @@ export class ResizeController<TPreview, TLabels, TBefore, TWallUnion, TArtifact>
}
this._session = null;
this._eligibility = null;
const snapshotMatches = currentSnapshotIdentity === session.snapshotIdentity;
return {
kind: 'cancelled',
restoreWallUnion: currentSnapshotIdentity === session.snapshotIdentity
? session.wallUnionBefore : null,
restoreWallUnion: snapshotMatches ? session.wallUnionBefore : null,
restoreEpoch: snapshotMatches ? session.epochBefore : null,
};
}
+3 -3
View File
@@ -27,6 +27,7 @@ const setup = () => {
snapshotIdentity: 'snapshot-a',
before: { id: 'before' },
wallUnionBefore: { path: 'old' },
epochBefore: 42,
}), true);
return { controller, beforeWalls };
};
@@ -128,12 +129,11 @@ test('#264 exact wall profile fails closed on preview and is rechecked on finish
test('#264 cancel restores retained masonry only for the same snapshot', () => {
const same = setup().controller;
assert.deepEqual(same.cancel('snapshot-a'), {
kind: 'cancelled', restoreWallUnion: { path: 'old' },
kind: 'cancelled', restoreWallUnion: { path: 'old' }, restoreEpoch: 42,
});
assert.deepEqual(same.cancel('snapshot-a'), { kind: 'no-op' });
const stale = setup().controller;
assert.deepEqual(stale.cancel('snapshot-b'), {
kind: 'cancelled', restoreWallUnion: null,
kind: 'cancelled', restoreWallUnion: null, restoreEpoch: null,
});
});