@@ -1,111 +1,111 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"fingerprint": "20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019",
|
||||
"fingerprint": "dcade390c5de1a3b8ba29307c6f93fdfd8bda1809b228f1242b561be04d14fe4",
|
||||
"entry": "houseplan-card.js",
|
||||
"initialViewFiles": [
|
||||
"houseplan-assets/houseplan-card-Cjdt1t7f.js",
|
||||
"houseplan-assets/houseplan-card-B_i3R4iG.js",
|
||||
"houseplan-card.js"
|
||||
],
|
||||
"initialViewGzipBytes": 271455,
|
||||
"initialViewGzipBytes": 271846,
|
||||
"lazyFiles": [
|
||||
"houseplan-assets/de-DRFSDdUU.js",
|
||||
"houseplan-assets/editor-Rs89b855.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-BL8faVDv.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-Ci7QymAY.js",
|
||||
"houseplan-assets/space-deletion-1NoE8x_l.js"
|
||||
"houseplan-assets/de-W0NvUh84.js",
|
||||
"houseplan-assets/editor-CyHGDaga.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-o9_82gfm.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BLEW_xwn.js",
|
||||
"houseplan-assets/space-deletion-D2aHGv12.js"
|
||||
],
|
||||
"lazyGzipBytes": 165917,
|
||||
"lazyGzipBytes": 165918,
|
||||
"lazyEditorFiles": [
|
||||
"houseplan-assets/editor-Rs89b855.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-BL8faVDv.js",
|
||||
"houseplan-assets/space-deletion-1NoE8x_l.js"
|
||||
"houseplan-assets/editor-CyHGDaga.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-o9_82gfm.js",
|
||||
"houseplan-assets/space-deletion-D2aHGv12.js"
|
||||
],
|
||||
"lazyEditorGzipBytes": 136808,
|
||||
"lazyEditorGzipBytes": 136811,
|
||||
"lazyOnboardingFiles": [
|
||||
"houseplan-assets/houseplan-onboarding-runtime-Ci7QymAY.js",
|
||||
"houseplan-assets/space-deletion-1NoE8x_l.js"
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BLEW_xwn.js",
|
||||
"houseplan-assets/space-deletion-D2aHGv12.js"
|
||||
],
|
||||
"lazyOnboardingGzipBytes": 7195,
|
||||
"lazyOnboardingGzipBytes": 7199,
|
||||
"lazyLocaleFiles": [
|
||||
"houseplan-assets/de-DRFSDdUU.js"
|
||||
"houseplan-assets/de-W0NvUh84.js"
|
||||
],
|
||||
"lazyLocaleGzipBytes": 22465,
|
||||
"lazyLocaleGzipBytes": 22460,
|
||||
"files": [
|
||||
{
|
||||
"path": "houseplan-assets/de-DRFSDdUU.js",
|
||||
"sha256": "93319bdb17d7b9902672342eb6daa15f32ab09658dd6415deb157decb0edf613",
|
||||
"path": "houseplan-assets/de-W0NvUh84.js",
|
||||
"sha256": "37aaf04bdd37c2780782d25e6167ce5e61e2022f1c30c8febfa01fb63bf7eb0a",
|
||||
"rawBytes": 73919,
|
||||
"gzipBytes": 22465,
|
||||
"gzipBytes": 22460,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/editor-Rs89b855.js",
|
||||
"sha256": "93bd34541456059f9dd5369dd9a51a5bd8a39f99089927d610bf6fe6cfbf87c5",
|
||||
"path": "houseplan-assets/editor-CyHGDaga.js",
|
||||
"sha256": "059234eb256c8ffe07feba0342796637b8b71e49068bc9f5c338a8e3033d96d2",
|
||||
"rawBytes": 3831,
|
||||
"gzipBytes": 1582,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Cjdt1t7f.js"
|
||||
"houseplan-assets/houseplan-card-B_i3R4iG.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-card-Cjdt1t7f.js",
|
||||
"sha256": "10d36979e2f624b0dc5e98a445ea0c8dedec6469857b12dbd99c84f8dded4ee5",
|
||||
"rawBytes": 957284,
|
||||
"gzipBytes": 270703,
|
||||
"path": "houseplan-assets/houseplan-card-B_i3R4iG.js",
|
||||
"sha256": "00bfdc2f641863996f10c6d32da364da10d45d5af2c0dc2dc755e929da771d55",
|
||||
"rawBytes": 958245,
|
||||
"gzipBytes": 271094,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/de-DRFSDdUU.js",
|
||||
"houseplan-assets/editor-Rs89b855.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-BL8faVDv.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-Ci7QymAY.js"
|
||||
"houseplan-assets/de-W0NvUh84.js",
|
||||
"houseplan-assets/editor-CyHGDaga.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-o9_82gfm.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BLEW_xwn.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-BL8faVDv.js",
|
||||
"sha256": "ea20c56b4b365bd0accc4cd07950322b5b9dda70bc15a17b5d69c14df0ed0a55",
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-o9_82gfm.js",
|
||||
"sha256": "c90c6f8d2160755c7931cf80aaedfe988ed5bb5822aa1432eab8944dc700a063",
|
||||
"rawBytes": 520292,
|
||||
"gzipBytes": 134675,
|
||||
"gzipBytes": 134677,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Cjdt1t7f.js",
|
||||
"houseplan-assets/space-deletion-1NoE8x_l.js"
|
||||
"houseplan-assets/houseplan-card-B_i3R4iG.js",
|
||||
"houseplan-assets/space-deletion-D2aHGv12.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-Ci7QymAY.js",
|
||||
"sha256": "f18f2ec9af6091f8f731a2c8d969f05cdd8e34dec1c76c35a11857f7ab1e9d3a",
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-BLEW_xwn.js",
|
||||
"sha256": "a1fafac0f5ffc1839c18f66398c7f397c38feeda8296b6aad2a06d34cf7dedce",
|
||||
"rawBytes": 26356,
|
||||
"gzipBytes": 6644,
|
||||
"gzipBytes": 6647,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Cjdt1t7f.js",
|
||||
"houseplan-assets/space-deletion-1NoE8x_l.js"
|
||||
"houseplan-assets/houseplan-card-B_i3R4iG.js",
|
||||
"houseplan-assets/space-deletion-D2aHGv12.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/space-deletion-1NoE8x_l.js",
|
||||
"sha256": "20448d45c7f3cbeed2ca72852535b30c0b7dd5bf70d6b9c354fd71ca693abf84",
|
||||
"rawBytes": 906,
|
||||
"gzipBytes": 551,
|
||||
"path": "houseplan-assets/space-deletion-D2aHGv12.js",
|
||||
"sha256": "0f0018f508fee171aaa272cd947f303967dfe59f2e283d97ec004fca91f2b0fa",
|
||||
"rawBytes": 911,
|
||||
"gzipBytes": 552,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-card.js",
|
||||
"sha256": "15b38319f0dc22546a9f747ae4c227b49f10b5f21595cdf2ce758e09a7402be5",
|
||||
"sha256": "ff4dc28297338f8b56f26ca60856468c9f3b2bd1b813731da91fc0324a12e4b4",
|
||||
"rawBytes": 1089,
|
||||
"gzipBytes": 752,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Cjdt1t7f.js"
|
||||
"houseplan-assets/houseplan-card-B_i3R4iG.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019";import{c as e,l as o,t,c_ as s,A as a,c$ as i,d0 as l,d1 as n,E as r,b as c}from"./houseplan-card-Cjdt1t7f.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},_=this._schema,f=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="dcade390c5de1a3b8ba29307c6f93fdfd8bda1809b228f1242b561be04d14fe4";import{c as e,l as o,t,c_ as s,A as a,c$ as i,d0 as l,d1 as n,E as r,b as c}from"./houseplan-card-B_i3R4iG.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
.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"
|
||||
${d(f.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:f})}
|
||||
${t(s,"editor.default_floor_missing",{id:_})}
|
||||
</div>`:a}
|
||||
${d(_.slice(3))}
|
||||
${d(f.slice(3))}
|
||||
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",h);
|
||||
@@ -1,4 +1,4 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019";import{bU as t,bV as s,bW as e,bX as o,aq as a,bY as l,c1 as i,bZ as h,b_ as n,b$ as c,c0 as r,b as p,A as _,cS as d,ap as u,ar as g,cT as m}from"./houseplan-card-Cjdt1t7f.js";import{i as b,c as $,a as v,s as y,t as f}from"./space-deletion-1NoE8x_l.js";const w=1e3,D=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},S="20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019";class C{constructor(t){this.host=t,this._toggleServerPlans=async()=>{const t=this.host._spaceDialog;if(t)if(t.pickSaved)this.host._spaceDialog={...t,pickSaved:!1};else{this.host._spaceDialog={...t,pickSaved:!0,savedBusy:!0};try{const t=await this.host.hass.callWS({type:"houseplan/plans/list"}),s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:t?.plans||[],savedBusy:!1})}catch(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:[],savedBusy:!1}),this.host._showToast(this.host._t("toast.plans_list_failed",{err:this.host._errText(t)}))}}}}_openSpaceDialog(p,_){if(!this.host._serverStorage||!this.host._serverCfg)return void this.host._showToast(this.host._t("toast.integration_missing"));if("edit"===p){const i=this.host._serverCfg.spaces.find(t=>t.id===_);if(!i)return;const h=t(i),n=i.settings?.custom_fill&&"object"==typeof i.settings.custom_fill?s(i.settings.custom_fill):null,c="none"===h.fill?{...n||e,a:0}:n;return void(this.host._spaceDialog={mode:p,spaceId:_,title:i.title,planUrl:i.plan_url||null,planFile:null,source:i.plan_url?"file":"draw",showBorders:h.showBorders,showNames:h.showNames,zeroWallStyle:l(i),displayTouched:!0,hideDecor:h.hideDecor,hideOpenings:h.hideOpenings,roomColor:h.color,roomOpacity:h.opacity,fillMode:"none"===h.fill?"custom":h.fill,customFill:c,glowEnabled:h.glow,bgColor:h.bgColor,bgMode:"static"===i.settings?.bg_mode||"daynight"===i.settings?.bg_mode?i.settings.bg_mode:null,northDeg:a({},i.settings),sunRays:"boolean"==typeof i.settings?.sun_rays?i.settings.sun_rays:null,tempMin:h.tempMin,tempMax:h.tempMax,showLqi:h.showLqi??this.host._config?.show_signal??!0,cardFontScale:h.cardFontScale,labelTemp:h.labelTemp,labelHum:h.labelHum,labelLqi:h.labelLqi,labelLight:h.labelLight,cellCm:Number(i.cell_cm)>0?Number(i.cell_cm):5,cellCmInput:o(Number(i.cell_cm)>0?Number(i.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1})}const d=i(this.host._imperial);this.host._spaceDialog={mode:p,title:"",planUrl:null,planFile:null,...b(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:r,roomOpacity:c,fillMode:"custom",customFill:{...e,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:n,tempMax:h,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:d,cellCmInput:o(d,this.host._imperial),cellCmTouched:!1,busy:!1}}async _pickPlanFile(t){const s=t.target,e=s.files?.[0];if(!e||!this.host._spaceDialog)return;const o={"image/svg+xml":"svg","image/png":"png","image/jpeg":"jpg","image/webp":"webp"}[e.type]||(e.name.toLowerCase().endsWith(".svg")?"svg":"");if(!o)return void this.host._showToast(this.host._t("toast.plan_formats"));const a=new Uint8Array(await e.arrayBuffer());let l="";for(let t=0;t<a.length;t+=32768)l+=String.fromCharCode(...a.subarray(t,t+32768));const i=btoa(l),h=URL.createObjectURL(e),n=await new Promise(t=>{const s=new Image;s.onload=()=>t(s.naturalWidth&&s.naturalHeight?s.naturalWidth/s.naturalHeight:1.414),s.onerror=()=>t(1.414),s.src=h});URL.revokeObjectURL(h),this.host._spaceDialog={...this.host._spaceDialog,planFile:{ext:o,b64:i,aspect:n,name:e.name}}}_useServerPlan(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,planUrl:t,planFile:null,pickSaved:!1,savedAspect:void 0},this.host._aspectJob=this._readPlanAspect(t))}async _readPlanAspect(t){for(let s=0;s<40;s++){const s=this.host._display(t);if(s){const e=await new Promise(t=>{const e=new Image;e.onload=()=>t(e.naturalWidth&&e.naturalHeight?e.naturalWidth/e.naturalHeight:0),e.onerror=()=>t(0),e.src=s}),o=this.host._spaceDialog;return o&&o.planUrl===t&&Number.isFinite(e)&&e>0?(this.host._spaceDialog={...o,savedAspect:e},e):0}if(await new Promise(t=>setTimeout(t,150)),this.host._spaceDialog?.planUrl!==t)return 0}return 0}async _deleteServerPlan(t){if(confirm(this.host._t("confirm.delete_plan",{name:t})))try{await this.host.hass.callWS({type:"houseplan/plans/delete",name:t});const s=this.host._spaceDialog;s?.saved&&(this.host._spaceDialog={...s,saved:s.saved.filter(s=>s.name!==t)})}catch(t){this.host._showToast(this.host._t("toast.plan_delete_failed",{err:this.host._errText(t)}))}}_renderServerPlans(t){if(t.savedBusy)return p`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return p`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return p`<div class="savedplans">
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="dcade390c5de1a3b8ba29307c6f93fdfd8bda1809b228f1242b561be04d14fe4";import{bU as t,bV as s,bW as e,bX as o,aq as a,bY as l,c1 as i,bZ as h,b_ as n,b$ as c,c0 as r,b as p,A as _,cS as d,ap as u,ar as g,cT as m}from"./houseplan-card-B_i3R4iG.js";import{i as b,c as $,a as v,s as f,t as y}from"./space-deletion-D2aHGv12.js";const w=1e3,D=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},S="dcade390c5de1a3b8ba29307c6f93fdfd8bda1809b228f1242b561be04d14fe4";class C{constructor(t){this.host=t,this._toggleServerPlans=async()=>{const t=this.host._spaceDialog;if(t)if(t.pickSaved)this.host._spaceDialog={...t,pickSaved:!1};else{this.host._spaceDialog={...t,pickSaved:!0,savedBusy:!0};try{const t=await this.host.hass.callWS({type:"houseplan/plans/list"}),s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:t?.plans||[],savedBusy:!1})}catch(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:[],savedBusy:!1}),this.host._showToast(this.host._t("toast.plans_list_failed",{err:this.host._errText(t)}))}}}}_openSpaceDialog(p,_){if(!this.host._serverStorage||!this.host._serverCfg)return void this.host._showToast(this.host._t("toast.integration_missing"));if("edit"===p){const i=this.host._serverCfg.spaces.find(t=>t.id===_);if(!i)return;const h=t(i),n=i.settings?.custom_fill&&"object"==typeof i.settings.custom_fill?s(i.settings.custom_fill):null,c="none"===h.fill?{...n||e,a:0}:n;return void(this.host._spaceDialog={mode:p,spaceId:_,title:i.title,planUrl:i.plan_url||null,planFile:null,source:i.plan_url?"file":"draw",showBorders:h.showBorders,showNames:h.showNames,zeroWallStyle:l(i),displayTouched:!0,hideDecor:h.hideDecor,hideOpenings:h.hideOpenings,roomColor:h.color,roomOpacity:h.opacity,fillMode:"none"===h.fill?"custom":h.fill,customFill:c,glowEnabled:h.glow,bgColor:h.bgColor,bgMode:"static"===i.settings?.bg_mode||"daynight"===i.settings?.bg_mode?i.settings.bg_mode:null,northDeg:a({},i.settings),sunRays:"boolean"==typeof i.settings?.sun_rays?i.settings.sun_rays:null,tempMin:h.tempMin,tempMax:h.tempMax,showLqi:h.showLqi??this.host._config?.show_signal??!0,cardFontScale:h.cardFontScale,labelTemp:h.labelTemp,labelHum:h.labelHum,labelLqi:h.labelLqi,labelLight:h.labelLight,cellCm:Number(i.cell_cm)>0?Number(i.cell_cm):5,cellCmInput:o(Number(i.cell_cm)>0?Number(i.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1})}const d=i(this.host._imperial);this.host._spaceDialog={mode:p,title:"",planUrl:null,planFile:null,...b(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:r,roomOpacity:c,fillMode:"custom",customFill:{...e,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:n,tempMax:h,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:d,cellCmInput:o(d,this.host._imperial),cellCmTouched:!1,busy:!1}}async _pickPlanFile(t){const s=t.target,e=s.files?.[0];if(!e||!this.host._spaceDialog)return;const o={"image/svg+xml":"svg","image/png":"png","image/jpeg":"jpg","image/webp":"webp"}[e.type]||(e.name.toLowerCase().endsWith(".svg")?"svg":"");if(!o)return void this.host._showToast(this.host._t("toast.plan_formats"));const a=new Uint8Array(await e.arrayBuffer());let l="";for(let t=0;t<a.length;t+=32768)l+=String.fromCharCode(...a.subarray(t,t+32768));const i=btoa(l),h=URL.createObjectURL(e),n=await new Promise(t=>{const s=new Image;s.onload=()=>t(s.naturalWidth&&s.naturalHeight?s.naturalWidth/s.naturalHeight:1.414),s.onerror=()=>t(1.414),s.src=h});URL.revokeObjectURL(h),this.host._spaceDialog={...this.host._spaceDialog,planFile:{ext:o,b64:i,aspect:n,name:e.name}}}_useServerPlan(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,planUrl:t,planFile:null,pickSaved:!1,savedAspect:void 0},this.host._aspectJob=this._readPlanAspect(t))}async _readPlanAspect(t){for(let s=0;s<40;s++){const s=this.host._display(t);if(s){const e=await new Promise(t=>{const e=new Image;e.onload=()=>t(e.naturalWidth&&e.naturalHeight?e.naturalWidth/e.naturalHeight:0),e.onerror=()=>t(0),e.src=s}),o=this.host._spaceDialog;return o&&o.planUrl===t&&Number.isFinite(e)&&e>0?(this.host._spaceDialog={...o,savedAspect:e},e):0}if(await new Promise(t=>setTimeout(t,150)),this.host._spaceDialog?.planUrl!==t)return 0}return 0}async _deleteServerPlan(t){if(confirm(this.host._t("confirm.delete_plan",{name:t})))try{await this.host.hass.callWS({type:"houseplan/plans/delete",name:t});const s=this.host._spaceDialog;s?.saved&&(this.host._spaceDialog={...s,saved:s.saved.filter(s=>s.name!==t)})}catch(t){this.host._showToast(this.host._t("toast.plan_delete_failed",{err:this.host._errText(t)}))}}_renderServerPlans(t){if(t.savedBusy)return p`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return p`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return p`<div class="savedplans">
|
||||
${s.map(s=>{return p`
|
||||
<div class="savedplan ${s.url===t.planUrl?"cur":""}">
|
||||
<img src=${this.host._display(s.url)} alt="" loading="lazy" decoding="async" />
|
||||
@@ -46,7 +46,7 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b
|
||||
<label>${this.host._t("space.plan_label")}</label>
|
||||
<label class="srcrow">
|
||||
<input type="radio" name="plansrc" .checked=${"file"===t.source}
|
||||
@change=${()=>this.host._spaceDialog=y(t,"file")} />
|
||||
@change=${()=>this.host._spaceDialog=f(t,"file")} />
|
||||
<span>${this.host._t("space.source_file")}</span>
|
||||
</label>
|
||||
${"file"===t.source?p`<div class="planrow">
|
||||
@@ -68,7 +68,7 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b
|
||||
${t.pickSaved?this._renderServerPlans(t):_}`:_}
|
||||
<label class="srcrow">
|
||||
<input type="radio" name="plansrc" .checked=${"draw"===t.source}
|
||||
@change=${()=>this.host._spaceDialog=y(t,"draw")} />
|
||||
@change=${()=>this.host._spaceDialog=f(t,"draw")} />
|
||||
<span>${this.host._t("space.source_draw")}</span>
|
||||
</label>
|
||||
|
||||
@@ -85,7 +85,7 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b
|
||||
|
||||
<label class="dispsection">${this.host._t("space.display_section")}</label>
|
||||
<label class="srcrow">
|
||||
${this._boolInput(t.showBorders,s=>{this.host._spaceDialog=f(t,"showBorders",s)})}
|
||||
${this._boolInput(t.showBorders,s=>{this.host._spaceDialog=y(t,"showBorders",s)})}
|
||||
<span>${this.host._t("space.show_borders")}</span>
|
||||
</label>
|
||||
<label>${this.host._t("space.zero_wall_style")}</label>
|
||||
@@ -99,7 +99,7 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b
|
||||
</select>
|
||||
<div class="rhint">${this.host._t("space.zero_wall_help")}</div>
|
||||
<label class="srcrow">
|
||||
${this._boolInput(t.showNames,s=>{this.host._spaceDialog=f(t,"showNames",s)})}
|
||||
${this._boolInput(t.showNames,s=>{this.host._spaceDialog=y(t,"showNames",s)})}
|
||||
<span>${this.host._t("space.show_names")}</span>
|
||||
</label>
|
||||
<label class="srcrow">
|
||||
@@ -1 +1 @@
|
||||
function e(e,o){return{id:e,title:o,plan_url:null,view_box:[0,0,1,1],rooms:[],wall_segments:[]}}function o(e="file"){const o="draw"===e;return{source:e,showBorders:o,showNames:o,displayTouched:!1}}function r(e,o){if(e.displayTouched)return{...e,source:o};const r="draw"===o;return{...e,source:o,showBorders:r,showNames:r}}function s(e,o,r){return{...e,[o]:r,displayTouched:!0}}function a(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),a=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),t=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&a.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:t,count:t.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019";export{a,e as c,o as i,r as s,s as t};
|
||||
function e(e,o){return{id:e,title:o,plan_url:null,view_box:[0,0,1,1],rooms:[],wall_segments:[]}}function o(e="file"){const o="draw"===e;return{source:e,showBorders:o,showNames:o,displayTouched:!1}}function r(e,o){if(e.displayTouched)return{...e,source:o};const r="draw"===o;return{...e,source:o,showBorders:r,showNames:r}}function s(e,o,r){return{...e,[o]:r,displayTouched:!0}}function t(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),t=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),n=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&t.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:n,count:n.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="dcade390c5de1a3b8ba29307c6f93fdfd8bda1809b228f1242b561be04d14fe4";export{t as a,e as c,o as i,r as s,s as t};
|
||||
@@ -1 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019";try{await import("./houseplan-assets/houseplan-card-Cjdt1t7f.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="dcade390c5de1a3b8ba29307c6f93fdfd8bda1809b228f1242b561be04d14fe4";try{await import("./houseplan-assets/houseplan-card-B_i3R4iG.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
|
||||
|
||||
@@ -677,6 +677,44 @@ export function prepareGoldenFixture(scenario) {
|
||||
if (missing.length) throw new Error(`golden layoutOverrides reference missing item(s): ${missing.join(', ')}`);
|
||||
fixture.layout = { ...(fixture.layout || {}), ...structuredClone(scenario.layoutOverrides) };
|
||||
}
|
||||
if (scenario.vacuumTrail) {
|
||||
const deviceId = 'golden-vacuum-trail';
|
||||
const vacuumEntity = 'vacuum.golden_vacuum_trail';
|
||||
const sourceEntity = 'camera.golden_vacuum_trail_map';
|
||||
fixture.devices[deviceId] = {
|
||||
id: deviceId, name: 'Golden vacuum trail', model: 'GOLDEN-VACUUM-TRAIL',
|
||||
area_id: 'golden_geo_sw', identifiers: [['houseplan_golden', deviceId]],
|
||||
config_entries: ['golden_entry'], entry_type: null, via_device_id: null, disabled_by: null,
|
||||
};
|
||||
fixture.entities[vacuumEntity] = {
|
||||
entity_id: vacuumEntity, device_id: deviceId, platform: 'houseplan_golden',
|
||||
config_entry_id: 'golden_entry', disabled_by: null,
|
||||
};
|
||||
fixture.entities[sourceEntity] = {
|
||||
entity_id: sourceEntity, device_id: deviceId, platform: 'houseplan_golden',
|
||||
config_entry_id: 'golden_entry', disabled_by: null,
|
||||
};
|
||||
fixture.states[vacuumEntity] = {
|
||||
entity_id: vacuumEntity, state: 'cleaning',
|
||||
attributes: { friendly_name: 'Golden vacuum trail' },
|
||||
};
|
||||
fixture.states[sourceEntity] = {
|
||||
entity_id: sourceEntity, state: 'idle', attributes: {
|
||||
friendly_name: 'Golden vacuum trail map', map_name: 'golden-trail',
|
||||
vacuum_position: { x: 850, y: 760, a: 0 },
|
||||
path: { path: structuredClone(scenario.vacuumTrail.current) },
|
||||
},
|
||||
};
|
||||
fixture.layout[deviceId] = { s: scenario.space, x: 0.12, y: 0.84 };
|
||||
fixture.config.markers = [...(fixture.config.markers || []), {
|
||||
id: deviceId, binding: `device:${deviceId}`, space: scenario.space,
|
||||
area: 'golden_geo_sw',
|
||||
vacuum: {
|
||||
source: sourceEntity, trail_mode: 'always',
|
||||
calibration: { 'golden-trail': [1, 0, 0, 0, 1, 0] },
|
||||
},
|
||||
}];
|
||||
}
|
||||
|
||||
return fixture;
|
||||
}
|
||||
@@ -740,6 +778,14 @@ export async function prepareGoldenScenario(page, scenario) {
|
||||
return { config: structuredClone(fixture.config), rev: 1, can_write: true };
|
||||
if (message.type === 'houseplan/layout/get')
|
||||
return { layout: structuredClone(fixture.layout || {}), rev: 1 };
|
||||
if (message.type === 'houseplan/trail/get') return scenario.vacuumTrail ? {
|
||||
trails: { 'golden-vacuum-trail': {
|
||||
previous: {
|
||||
map_id: 'golden-trail', started: 1, ended: 2,
|
||||
points: structuredClone(scenario.vacuumTrail.previous),
|
||||
},
|
||||
} },
|
||||
} : { trails: {} };
|
||||
if (message.type === 'config/device_registry/list') return Object.values(fixture.devices || {});
|
||||
if (message.type === 'config/entity_registry/list') return Object.values(fixture.entities || {});
|
||||
if (message.type === 'config_entries/get')
|
||||
@@ -1574,6 +1620,22 @@ export async function prepareGoldenScenario(page, scenario) {
|
||||
throw new Error(`golden day-cycle contract did not render: ${scenario.id}`);
|
||||
}
|
||||
}
|
||||
if (scenario.vacuumTrail) {
|
||||
await until(() => !!card.renderRoot.querySelector('.vactrail > path.case'));
|
||||
const currentCase = card.renderRoot.querySelector('.vactrail > path.case');
|
||||
const currentCore = card.renderRoot.querySelector('.vactrail > path.core');
|
||||
const previousCase = card.renderRoot.querySelector('.vactrail .prev path.case');
|
||||
const previousCore = card.renderRoot.querySelector('.vactrail .prev path.core');
|
||||
const currentD = currentCase?.getAttribute('d') || '';
|
||||
const previousD = previousCase?.getAttribute('d') || '';
|
||||
if ((currentD.match(/M /g) || []).length !== 2
|
||||
|| (currentD.match(/Q /g) || []).length < 4
|
||||
|| (previousD.match(/Q /g) || []).length < 2
|
||||
|| currentD !== currentCore?.getAttribute('d')
|
||||
|| previousD !== previousCore?.getAttribute('d')) {
|
||||
throw new Error(`golden vacuum trail contract is incomplete: ${scenario.id}`);
|
||||
}
|
||||
}
|
||||
await document.fonts?.ready;
|
||||
await frame();
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { fixtureWallKey } from '../fixtures/visual-matrix.mjs';
|
||||
|
||||
/** Data-only HP-QA-01 capture matrix. Bump when framing or scenarios change. */
|
||||
export const GOLDEN_MATRIX_VERSION = 49;
|
||||
export const GOLDEN_MATRIX_VERSION = 50;
|
||||
|
||||
const stage = { capture: 'stage', threshold: { maxChannelDelta: 10, maxDiffRatio: 0.0005 } };
|
||||
const page = { capture: 'page', threshold: { maxChannelDelta: 10, maxDiffRatio: 0.0008 } };
|
||||
@@ -222,6 +222,15 @@ export const GOLDEN_SCENARIOS = Object.freeze([
|
||||
theme: 'dark', viewport: { width: 1000, height: 900 }, ...stage },
|
||||
{ id: 'geometry-view-light-fit', fixture: 'visual', space: 'golden-geometry', mode: 'view',
|
||||
theme: 'light', viewport: { width: 1000, height: 900 }, ...stage },
|
||||
{ id: 'vacuum-trail-smoothing-dark', fixture: 'visual', space: 'golden-geometry', mode: 'view',
|
||||
vacuumTrail: {
|
||||
current: [
|
||||
[[180, 220], [360, 220], [360, 420], [520, 420]],
|
||||
[[650, 240], [780, 240], [780, 500], [650, 620], [850, 760]],
|
||||
],
|
||||
previous: [[160, 760], [300, 650], [420, 740], [560, 600]],
|
||||
},
|
||||
theme: 'dark', viewport: { width: 1000, height: 900 }, ...stage },
|
||||
{ id: 'room-label-parity-view-dark', fixture: 'visual', space: 'golden-lighting', mode: 'view',
|
||||
roomLabelParity: true, theme: 'dark', viewport: { width: 1000, height: 900 }, ...page },
|
||||
{ id: 'room-label-parity-plan-dark', fixture: 'visual', space: 'golden-lighting', mode: 'plan',
|
||||
|
||||
@@ -68,7 +68,7 @@ const out = await page.evaluate(async () => {
|
||||
entity_id: 'camera.static_robot_map', state: 'idle',
|
||||
attributes: {
|
||||
map_name: 'm1', vacuum_position: { x: 650, y: 700, a: 0 },
|
||||
path: [[200, 200], [420, 440], [650, 700]],
|
||||
path: [[200, 200], [420, 200], [420, 440], [650, 700]],
|
||||
},
|
||||
},
|
||||
'sun.sun': {
|
||||
@@ -110,6 +110,7 @@ const out = await page.evaluate(async () => {
|
||||
const before = layers();
|
||||
const beforeSpillParts = [...root(original).querySelectorAll('.glow-pool')]
|
||||
.map((node) => node.getAttribute('data-lit-parts'));
|
||||
const isoVacuumTrailD = root(original).querySelector('.vactrail path.case')?.getAttribute('d') || '';
|
||||
const wallFingerprint = root(original).querySelector('[data-hp="iso-walls"]')?.dataset.fingerprint;
|
||||
const cachedGeometry = original._isoGeometryCache.get(wallFingerprint)?.geometry;
|
||||
const light = original.hass.states['light.ceiling'];
|
||||
@@ -154,6 +155,7 @@ const out = await page.evaluate(async () => {
|
||||
&& before.verticalOpenings === 5
|
||||
&& before.devices > 0 && before.hover > 0 && before.vacuumPucks > 0
|
||||
&& before.vacuumTrails >= 2,
|
||||
vacuumTrailCurved: (isoVacuumTrailD.match(/Q /g) || []).length === 1,
|
||||
liveLayersStable: JSON.stringify(after) === JSON.stringify(before),
|
||||
spillBarrierStable: JSON.stringify(afterSpillParts) === JSON.stringify(beforeSpillParts)
|
||||
&& beforeSpillParts.length >= 2 && beforeSpillParts.every((parts) => Number(parts) > 0),
|
||||
|
||||
@@ -140,6 +140,7 @@ const out = await page.evaluate(async () => {
|
||||
c.hass = { ...c.hass, states: { ...c.hass.states } }; await c.updateComplete;
|
||||
const resumedD = sr().querySelector('.vactrail > path.case')?.getAttribute('d') || '';
|
||||
o.srvResumedCurrentKeepsEarlierPoints = (resumedD.match(/[ML]/g) || []).length === 4;
|
||||
o.srvResumedCurrentSmooth = (resumedD.match(/Q /g) || []).length === 2;
|
||||
|
||||
// ---- server-side runs: current + one previous (owner 2026-07-31) ----
|
||||
cfg.markers.find((m) => m.id === 'e_vacuum_robo').vacuum.trail_mode = 'always';
|
||||
@@ -155,11 +156,17 @@ const out = await page.evaluate(async () => {
|
||||
await c.updateComplete;
|
||||
c.hass = { ...c.hass, states: { ...c.hass.states } }; await c.updateComplete;
|
||||
const prevG = sr().querySelector('.vactrail g.prev');
|
||||
o.srvPrevRunShown = !!prevG && prevG.querySelectorAll('polyline').length === 2
|
||||
&& prevG.querySelector('.case').getAttribute('points').split(' ').length === 3;
|
||||
const prevCase = prevG?.querySelector('path.case');
|
||||
const prevCore = prevG?.querySelector('path.core');
|
||||
const prevD = prevCase?.getAttribute('d') || '';
|
||||
o.srvPrevRunShown = !!prevG && prevG.querySelectorAll('path').length === 2
|
||||
&& prevD === prevCore?.getAttribute('d')
|
||||
&& (prevD.match(/M /g) || []).length === 1
|
||||
&& (prevD.match(/Q /g) || []).length === 1;
|
||||
const cur = sr().querySelector('.vactrail > path.case');
|
||||
// 4 server points, the live tail trimmed while moving -> 3 rendered
|
||||
o.srvCurTrimmed = !!cur && (cur.getAttribute('d').match(/[ML]/g) || []).length === 3;
|
||||
o.srvCurTrimmed = !!cur && (cur.getAttribute('d').match(/[ML]/g) || []).length === 3
|
||||
&& (cur.getAttribute('d').match(/Q /g) || []).length === 1;
|
||||
o.srvPrevFaded = !!prevG && getComputedStyle(prevG).opacity === '0.4';
|
||||
// XCME integration path outranks the simultaneous server run. Its two
|
||||
// subpaths remain two SVG moveto sections: no synthetic bridge over a gap.
|
||||
@@ -280,6 +287,7 @@ checkAll(out, {
|
||||
puckGoneWhenDocked: true, trailHiddenAfterDock: true, hiddenNoPuck: true,
|
||||
unknownMapNoPuck: true,
|
||||
srvEndedCurrentHiddenDefault: true, srvResumedCurrentKeepsEarlierPoints: true,
|
||||
srvResumedCurrentSmooth: true,
|
||||
srvPrevRunShown: true, srvCurTrimmed: true, srvPrevFaded: true,
|
||||
multiSubpathNoBridge: true, integrationPathAlwaysAtRest: true,
|
||||
tipExists: true, tipGluedToPuck: true, neverHidesAll: true,
|
||||
|
||||
@@ -1,111 +1,111 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"fingerprint": "20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019",
|
||||
"fingerprint": "dcade390c5de1a3b8ba29307c6f93fdfd8bda1809b228f1242b561be04d14fe4",
|
||||
"entry": "houseplan-card.js",
|
||||
"initialViewFiles": [
|
||||
"houseplan-assets/houseplan-card-Cjdt1t7f.js",
|
||||
"houseplan-assets/houseplan-card-B_i3R4iG.js",
|
||||
"houseplan-card.js"
|
||||
],
|
||||
"initialViewGzipBytes": 271455,
|
||||
"initialViewGzipBytes": 271846,
|
||||
"lazyFiles": [
|
||||
"houseplan-assets/de-DRFSDdUU.js",
|
||||
"houseplan-assets/editor-Rs89b855.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-BL8faVDv.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-Ci7QymAY.js",
|
||||
"houseplan-assets/space-deletion-1NoE8x_l.js"
|
||||
"houseplan-assets/de-W0NvUh84.js",
|
||||
"houseplan-assets/editor-CyHGDaga.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-o9_82gfm.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BLEW_xwn.js",
|
||||
"houseplan-assets/space-deletion-D2aHGv12.js"
|
||||
],
|
||||
"lazyGzipBytes": 165917,
|
||||
"lazyGzipBytes": 165918,
|
||||
"lazyEditorFiles": [
|
||||
"houseplan-assets/editor-Rs89b855.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-BL8faVDv.js",
|
||||
"houseplan-assets/space-deletion-1NoE8x_l.js"
|
||||
"houseplan-assets/editor-CyHGDaga.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-o9_82gfm.js",
|
||||
"houseplan-assets/space-deletion-D2aHGv12.js"
|
||||
],
|
||||
"lazyEditorGzipBytes": 136808,
|
||||
"lazyEditorGzipBytes": 136811,
|
||||
"lazyOnboardingFiles": [
|
||||
"houseplan-assets/houseplan-onboarding-runtime-Ci7QymAY.js",
|
||||
"houseplan-assets/space-deletion-1NoE8x_l.js"
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BLEW_xwn.js",
|
||||
"houseplan-assets/space-deletion-D2aHGv12.js"
|
||||
],
|
||||
"lazyOnboardingGzipBytes": 7195,
|
||||
"lazyOnboardingGzipBytes": 7199,
|
||||
"lazyLocaleFiles": [
|
||||
"houseplan-assets/de-DRFSDdUU.js"
|
||||
"houseplan-assets/de-W0NvUh84.js"
|
||||
],
|
||||
"lazyLocaleGzipBytes": 22465,
|
||||
"lazyLocaleGzipBytes": 22460,
|
||||
"files": [
|
||||
{
|
||||
"path": "houseplan-assets/de-DRFSDdUU.js",
|
||||
"sha256": "93319bdb17d7b9902672342eb6daa15f32ab09658dd6415deb157decb0edf613",
|
||||
"path": "houseplan-assets/de-W0NvUh84.js",
|
||||
"sha256": "37aaf04bdd37c2780782d25e6167ce5e61e2022f1c30c8febfa01fb63bf7eb0a",
|
||||
"rawBytes": 73919,
|
||||
"gzipBytes": 22465,
|
||||
"gzipBytes": 22460,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/editor-Rs89b855.js",
|
||||
"sha256": "93bd34541456059f9dd5369dd9a51a5bd8a39f99089927d610bf6fe6cfbf87c5",
|
||||
"path": "houseplan-assets/editor-CyHGDaga.js",
|
||||
"sha256": "059234eb256c8ffe07feba0342796637b8b71e49068bc9f5c338a8e3033d96d2",
|
||||
"rawBytes": 3831,
|
||||
"gzipBytes": 1582,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Cjdt1t7f.js"
|
||||
"houseplan-assets/houseplan-card-B_i3R4iG.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-card-Cjdt1t7f.js",
|
||||
"sha256": "10d36979e2f624b0dc5e98a445ea0c8dedec6469857b12dbd99c84f8dded4ee5",
|
||||
"rawBytes": 957284,
|
||||
"gzipBytes": 270703,
|
||||
"path": "houseplan-assets/houseplan-card-B_i3R4iG.js",
|
||||
"sha256": "00bfdc2f641863996f10c6d32da364da10d45d5af2c0dc2dc755e929da771d55",
|
||||
"rawBytes": 958245,
|
||||
"gzipBytes": 271094,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/de-DRFSDdUU.js",
|
||||
"houseplan-assets/editor-Rs89b855.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-BL8faVDv.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-Ci7QymAY.js"
|
||||
"houseplan-assets/de-W0NvUh84.js",
|
||||
"houseplan-assets/editor-CyHGDaga.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-o9_82gfm.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BLEW_xwn.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-BL8faVDv.js",
|
||||
"sha256": "ea20c56b4b365bd0accc4cd07950322b5b9dda70bc15a17b5d69c14df0ed0a55",
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-o9_82gfm.js",
|
||||
"sha256": "c90c6f8d2160755c7931cf80aaedfe988ed5bb5822aa1432eab8944dc700a063",
|
||||
"rawBytes": 520292,
|
||||
"gzipBytes": 134675,
|
||||
"gzipBytes": 134677,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Cjdt1t7f.js",
|
||||
"houseplan-assets/space-deletion-1NoE8x_l.js"
|
||||
"houseplan-assets/houseplan-card-B_i3R4iG.js",
|
||||
"houseplan-assets/space-deletion-D2aHGv12.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-Ci7QymAY.js",
|
||||
"sha256": "f18f2ec9af6091f8f731a2c8d969f05cdd8e34dec1c76c35a11857f7ab1e9d3a",
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-BLEW_xwn.js",
|
||||
"sha256": "a1fafac0f5ffc1839c18f66398c7f397c38feeda8296b6aad2a06d34cf7dedce",
|
||||
"rawBytes": 26356,
|
||||
"gzipBytes": 6644,
|
||||
"gzipBytes": 6647,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Cjdt1t7f.js",
|
||||
"houseplan-assets/space-deletion-1NoE8x_l.js"
|
||||
"houseplan-assets/houseplan-card-B_i3R4iG.js",
|
||||
"houseplan-assets/space-deletion-D2aHGv12.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/space-deletion-1NoE8x_l.js",
|
||||
"sha256": "20448d45c7f3cbeed2ca72852535b30c0b7dd5bf70d6b9c354fd71ca693abf84",
|
||||
"rawBytes": 906,
|
||||
"gzipBytes": 551,
|
||||
"path": "houseplan-assets/space-deletion-D2aHGv12.js",
|
||||
"sha256": "0f0018f508fee171aaa272cd947f303967dfe59f2e283d97ec004fca91f2b0fa",
|
||||
"rawBytes": 911,
|
||||
"gzipBytes": 552,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-card.js",
|
||||
"sha256": "15b38319f0dc22546a9f747ae4c227b49f10b5f21595cdf2ce758e09a7402be5",
|
||||
"sha256": "ff4dc28297338f8b56f26ca60856468c9f3b2bd1b813731da91fc0324a12e4b4",
|
||||
"rawBytes": 1089,
|
||||
"gzipBytes": 752,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Cjdt1t7f.js"
|
||||
"houseplan-assets/houseplan-card-B_i3R4iG.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019";import{c as e,l as o,t,c_ as s,A as a,c$ as i,d0 as l,d1 as n,E as r,b as c}from"./houseplan-card-Cjdt1t7f.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},_=this._schema,f=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="dcade390c5de1a3b8ba29307c6f93fdfd8bda1809b228f1242b561be04d14fe4";import{c as e,l as o,t,c_ as s,A as a,c$ as i,d0 as l,d1 as n,E as r,b as c}from"./houseplan-card-B_i3R4iG.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
.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"
|
||||
${d(f.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:f})}
|
||||
${t(s,"editor.default_floor_missing",{id:_})}
|
||||
</div>`:a}
|
||||
${d(_.slice(3))}
|
||||
${d(f.slice(3))}
|
||||
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",h);
|
||||
@@ -1,4 +1,4 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019";import{bU as t,bV as s,bW as e,bX as o,aq as a,bY as l,c1 as i,bZ as h,b_ as n,b$ as c,c0 as r,b as p,A as _,cS as d,ap as u,ar as g,cT as m}from"./houseplan-card-Cjdt1t7f.js";import{i as b,c as $,a as v,s as y,t as f}from"./space-deletion-1NoE8x_l.js";const w=1e3,D=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},S="20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019";class C{constructor(t){this.host=t,this._toggleServerPlans=async()=>{const t=this.host._spaceDialog;if(t)if(t.pickSaved)this.host._spaceDialog={...t,pickSaved:!1};else{this.host._spaceDialog={...t,pickSaved:!0,savedBusy:!0};try{const t=await this.host.hass.callWS({type:"houseplan/plans/list"}),s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:t?.plans||[],savedBusy:!1})}catch(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:[],savedBusy:!1}),this.host._showToast(this.host._t("toast.plans_list_failed",{err:this.host._errText(t)}))}}}}_openSpaceDialog(p,_){if(!this.host._serverStorage||!this.host._serverCfg)return void this.host._showToast(this.host._t("toast.integration_missing"));if("edit"===p){const i=this.host._serverCfg.spaces.find(t=>t.id===_);if(!i)return;const h=t(i),n=i.settings?.custom_fill&&"object"==typeof i.settings.custom_fill?s(i.settings.custom_fill):null,c="none"===h.fill?{...n||e,a:0}:n;return void(this.host._spaceDialog={mode:p,spaceId:_,title:i.title,planUrl:i.plan_url||null,planFile:null,source:i.plan_url?"file":"draw",showBorders:h.showBorders,showNames:h.showNames,zeroWallStyle:l(i),displayTouched:!0,hideDecor:h.hideDecor,hideOpenings:h.hideOpenings,roomColor:h.color,roomOpacity:h.opacity,fillMode:"none"===h.fill?"custom":h.fill,customFill:c,glowEnabled:h.glow,bgColor:h.bgColor,bgMode:"static"===i.settings?.bg_mode||"daynight"===i.settings?.bg_mode?i.settings.bg_mode:null,northDeg:a({},i.settings),sunRays:"boolean"==typeof i.settings?.sun_rays?i.settings.sun_rays:null,tempMin:h.tempMin,tempMax:h.tempMax,showLqi:h.showLqi??this.host._config?.show_signal??!0,cardFontScale:h.cardFontScale,labelTemp:h.labelTemp,labelHum:h.labelHum,labelLqi:h.labelLqi,labelLight:h.labelLight,cellCm:Number(i.cell_cm)>0?Number(i.cell_cm):5,cellCmInput:o(Number(i.cell_cm)>0?Number(i.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1})}const d=i(this.host._imperial);this.host._spaceDialog={mode:p,title:"",planUrl:null,planFile:null,...b(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:r,roomOpacity:c,fillMode:"custom",customFill:{...e,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:n,tempMax:h,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:d,cellCmInput:o(d,this.host._imperial),cellCmTouched:!1,busy:!1}}async _pickPlanFile(t){const s=t.target,e=s.files?.[0];if(!e||!this.host._spaceDialog)return;const o={"image/svg+xml":"svg","image/png":"png","image/jpeg":"jpg","image/webp":"webp"}[e.type]||(e.name.toLowerCase().endsWith(".svg")?"svg":"");if(!o)return void this.host._showToast(this.host._t("toast.plan_formats"));const a=new Uint8Array(await e.arrayBuffer());let l="";for(let t=0;t<a.length;t+=32768)l+=String.fromCharCode(...a.subarray(t,t+32768));const i=btoa(l),h=URL.createObjectURL(e),n=await new Promise(t=>{const s=new Image;s.onload=()=>t(s.naturalWidth&&s.naturalHeight?s.naturalWidth/s.naturalHeight:1.414),s.onerror=()=>t(1.414),s.src=h});URL.revokeObjectURL(h),this.host._spaceDialog={...this.host._spaceDialog,planFile:{ext:o,b64:i,aspect:n,name:e.name}}}_useServerPlan(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,planUrl:t,planFile:null,pickSaved:!1,savedAspect:void 0},this.host._aspectJob=this._readPlanAspect(t))}async _readPlanAspect(t){for(let s=0;s<40;s++){const s=this.host._display(t);if(s){const e=await new Promise(t=>{const e=new Image;e.onload=()=>t(e.naturalWidth&&e.naturalHeight?e.naturalWidth/e.naturalHeight:0),e.onerror=()=>t(0),e.src=s}),o=this.host._spaceDialog;return o&&o.planUrl===t&&Number.isFinite(e)&&e>0?(this.host._spaceDialog={...o,savedAspect:e},e):0}if(await new Promise(t=>setTimeout(t,150)),this.host._spaceDialog?.planUrl!==t)return 0}return 0}async _deleteServerPlan(t){if(confirm(this.host._t("confirm.delete_plan",{name:t})))try{await this.host.hass.callWS({type:"houseplan/plans/delete",name:t});const s=this.host._spaceDialog;s?.saved&&(this.host._spaceDialog={...s,saved:s.saved.filter(s=>s.name!==t)})}catch(t){this.host._showToast(this.host._t("toast.plan_delete_failed",{err:this.host._errText(t)}))}}_renderServerPlans(t){if(t.savedBusy)return p`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return p`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return p`<div class="savedplans">
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="dcade390c5de1a3b8ba29307c6f93fdfd8bda1809b228f1242b561be04d14fe4";import{bU as t,bV as s,bW as e,bX as o,aq as a,bY as l,c1 as i,bZ as h,b_ as n,b$ as c,c0 as r,b as p,A as _,cS as d,ap as u,ar as g,cT as m}from"./houseplan-card-B_i3R4iG.js";import{i as b,c as $,a as v,s as f,t as y}from"./space-deletion-D2aHGv12.js";const w=1e3,D=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},S="dcade390c5de1a3b8ba29307c6f93fdfd8bda1809b228f1242b561be04d14fe4";class C{constructor(t){this.host=t,this._toggleServerPlans=async()=>{const t=this.host._spaceDialog;if(t)if(t.pickSaved)this.host._spaceDialog={...t,pickSaved:!1};else{this.host._spaceDialog={...t,pickSaved:!0,savedBusy:!0};try{const t=await this.host.hass.callWS({type:"houseplan/plans/list"}),s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:t?.plans||[],savedBusy:!1})}catch(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:[],savedBusy:!1}),this.host._showToast(this.host._t("toast.plans_list_failed",{err:this.host._errText(t)}))}}}}_openSpaceDialog(p,_){if(!this.host._serverStorage||!this.host._serverCfg)return void this.host._showToast(this.host._t("toast.integration_missing"));if("edit"===p){const i=this.host._serverCfg.spaces.find(t=>t.id===_);if(!i)return;const h=t(i),n=i.settings?.custom_fill&&"object"==typeof i.settings.custom_fill?s(i.settings.custom_fill):null,c="none"===h.fill?{...n||e,a:0}:n;return void(this.host._spaceDialog={mode:p,spaceId:_,title:i.title,planUrl:i.plan_url||null,planFile:null,source:i.plan_url?"file":"draw",showBorders:h.showBorders,showNames:h.showNames,zeroWallStyle:l(i),displayTouched:!0,hideDecor:h.hideDecor,hideOpenings:h.hideOpenings,roomColor:h.color,roomOpacity:h.opacity,fillMode:"none"===h.fill?"custom":h.fill,customFill:c,glowEnabled:h.glow,bgColor:h.bgColor,bgMode:"static"===i.settings?.bg_mode||"daynight"===i.settings?.bg_mode?i.settings.bg_mode:null,northDeg:a({},i.settings),sunRays:"boolean"==typeof i.settings?.sun_rays?i.settings.sun_rays:null,tempMin:h.tempMin,tempMax:h.tempMax,showLqi:h.showLqi??this.host._config?.show_signal??!0,cardFontScale:h.cardFontScale,labelTemp:h.labelTemp,labelHum:h.labelHum,labelLqi:h.labelLqi,labelLight:h.labelLight,cellCm:Number(i.cell_cm)>0?Number(i.cell_cm):5,cellCmInput:o(Number(i.cell_cm)>0?Number(i.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1})}const d=i(this.host._imperial);this.host._spaceDialog={mode:p,title:"",planUrl:null,planFile:null,...b(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:r,roomOpacity:c,fillMode:"custom",customFill:{...e,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:n,tempMax:h,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:d,cellCmInput:o(d,this.host._imperial),cellCmTouched:!1,busy:!1}}async _pickPlanFile(t){const s=t.target,e=s.files?.[0];if(!e||!this.host._spaceDialog)return;const o={"image/svg+xml":"svg","image/png":"png","image/jpeg":"jpg","image/webp":"webp"}[e.type]||(e.name.toLowerCase().endsWith(".svg")?"svg":"");if(!o)return void this.host._showToast(this.host._t("toast.plan_formats"));const a=new Uint8Array(await e.arrayBuffer());let l="";for(let t=0;t<a.length;t+=32768)l+=String.fromCharCode(...a.subarray(t,t+32768));const i=btoa(l),h=URL.createObjectURL(e),n=await new Promise(t=>{const s=new Image;s.onload=()=>t(s.naturalWidth&&s.naturalHeight?s.naturalWidth/s.naturalHeight:1.414),s.onerror=()=>t(1.414),s.src=h});URL.revokeObjectURL(h),this.host._spaceDialog={...this.host._spaceDialog,planFile:{ext:o,b64:i,aspect:n,name:e.name}}}_useServerPlan(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,planUrl:t,planFile:null,pickSaved:!1,savedAspect:void 0},this.host._aspectJob=this._readPlanAspect(t))}async _readPlanAspect(t){for(let s=0;s<40;s++){const s=this.host._display(t);if(s){const e=await new Promise(t=>{const e=new Image;e.onload=()=>t(e.naturalWidth&&e.naturalHeight?e.naturalWidth/e.naturalHeight:0),e.onerror=()=>t(0),e.src=s}),o=this.host._spaceDialog;return o&&o.planUrl===t&&Number.isFinite(e)&&e>0?(this.host._spaceDialog={...o,savedAspect:e},e):0}if(await new Promise(t=>setTimeout(t,150)),this.host._spaceDialog?.planUrl!==t)return 0}return 0}async _deleteServerPlan(t){if(confirm(this.host._t("confirm.delete_plan",{name:t})))try{await this.host.hass.callWS({type:"houseplan/plans/delete",name:t});const s=this.host._spaceDialog;s?.saved&&(this.host._spaceDialog={...s,saved:s.saved.filter(s=>s.name!==t)})}catch(t){this.host._showToast(this.host._t("toast.plan_delete_failed",{err:this.host._errText(t)}))}}_renderServerPlans(t){if(t.savedBusy)return p`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return p`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return p`<div class="savedplans">
|
||||
${s.map(s=>{return p`
|
||||
<div class="savedplan ${s.url===t.planUrl?"cur":""}">
|
||||
<img src=${this.host._display(s.url)} alt="" loading="lazy" decoding="async" />
|
||||
@@ -46,7 +46,7 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b
|
||||
<label>${this.host._t("space.plan_label")}</label>
|
||||
<label class="srcrow">
|
||||
<input type="radio" name="plansrc" .checked=${"file"===t.source}
|
||||
@change=${()=>this.host._spaceDialog=y(t,"file")} />
|
||||
@change=${()=>this.host._spaceDialog=f(t,"file")} />
|
||||
<span>${this.host._t("space.source_file")}</span>
|
||||
</label>
|
||||
${"file"===t.source?p`<div class="planrow">
|
||||
@@ -68,7 +68,7 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b
|
||||
${t.pickSaved?this._renderServerPlans(t):_}`:_}
|
||||
<label class="srcrow">
|
||||
<input type="radio" name="plansrc" .checked=${"draw"===t.source}
|
||||
@change=${()=>this.host._spaceDialog=y(t,"draw")} />
|
||||
@change=${()=>this.host._spaceDialog=f(t,"draw")} />
|
||||
<span>${this.host._t("space.source_draw")}</span>
|
||||
</label>
|
||||
|
||||
@@ -85,7 +85,7 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b
|
||||
|
||||
<label class="dispsection">${this.host._t("space.display_section")}</label>
|
||||
<label class="srcrow">
|
||||
${this._boolInput(t.showBorders,s=>{this.host._spaceDialog=f(t,"showBorders",s)})}
|
||||
${this._boolInput(t.showBorders,s=>{this.host._spaceDialog=y(t,"showBorders",s)})}
|
||||
<span>${this.host._t("space.show_borders")}</span>
|
||||
</label>
|
||||
<label>${this.host._t("space.zero_wall_style")}</label>
|
||||
@@ -99,7 +99,7 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b
|
||||
</select>
|
||||
<div class="rhint">${this.host._t("space.zero_wall_help")}</div>
|
||||
<label class="srcrow">
|
||||
${this._boolInput(t.showNames,s=>{this.host._spaceDialog=f(t,"showNames",s)})}
|
||||
${this._boolInput(t.showNames,s=>{this.host._spaceDialog=y(t,"showNames",s)})}
|
||||
<span>${this.host._t("space.show_names")}</span>
|
||||
</label>
|
||||
<label class="srcrow">
|
||||
@@ -1 +1 @@
|
||||
function e(e,o){return{id:e,title:o,plan_url:null,view_box:[0,0,1,1],rooms:[],wall_segments:[]}}function o(e="file"){const o="draw"===e;return{source:e,showBorders:o,showNames:o,displayTouched:!1}}function r(e,o){if(e.displayTouched)return{...e,source:o};const r="draw"===o;return{...e,source:o,showBorders:r,showNames:r}}function s(e,o,r){return{...e,[o]:r,displayTouched:!0}}function a(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),a=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),t=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&a.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:t,count:t.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019";export{a,e as c,o as i,r as s,s as t};
|
||||
function e(e,o){return{id:e,title:o,plan_url:null,view_box:[0,0,1,1],rooms:[],wall_segments:[]}}function o(e="file"){const o="draw"===e;return{source:e,showBorders:o,showNames:o,displayTouched:!1}}function r(e,o){if(e.displayTouched)return{...e,source:o};const r="draw"===o;return{...e,source:o,showBorders:r,showNames:r}}function s(e,o,r){return{...e,[o]:r,displayTouched:!0}}function t(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),t=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),n=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&t.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:n,count:n.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="dcade390c5de1a3b8ba29307c6f93fdfd8bda1809b228f1242b561be04d14fe4";export{t as a,e as c,o as i,r as s,s as t};
|
||||
@@ -1 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019";try{await import("./houseplan-assets/houseplan-card-Cjdt1t7f.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="dcade390c5de1a3b8ba29307c6f93fdfd8bda1809b228f1242b561be04d14fe4";try{await import("./houseplan-assets/houseplan-card-B_i3R4iG.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
|
||||
|
||||
@@ -242,6 +242,14 @@ priority decision. `resolveVacSource()` is sticky for saved sources and limits
|
||||
automatic selection to compatible entities on the same HA device; the card
|
||||
adds registry status through the shared `resolveHaBindingStatus()` authority.
|
||||
|
||||
`smoothVacPath()` is the shared pure presentation step for current and previous
|
||||
runs. It consumes calibrated flat plan coordinates and returns typed
|
||||
`move|line|quadratic` commands with a caller-supplied physical radius; the card
|
||||
then applies flat/isometric scene projection and SVG serialization. Each corner
|
||||
uses a quadratic inside the adjacent-segment convex hull, bounded by half of
|
||||
both segment lengths, so the 17.5 cm product limit, exact endpoints and literal
|
||||
subpath gaps are structural rather than renderer-specific accidents.
|
||||
|
||||
Room auto-calibration uses the same shoelace `areaCentroid()` for plan polygons
|
||||
and robot outlines. Residuals are converted through resolved grid pitch and
|
||||
cell centimetres; matrices above the 40 cm threshold remain proposals until an
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Robot-vacuum trails now follow the same recorded points with softly rounded
|
||||
corners instead of a visibly segmented polyline. Current and previous runs
|
||||
use the same bounded curve, preserve telemetry gaps and never deviate by more
|
||||
than 17.5 cm from the recorded route
|
||||
([#209](https://github.com/Matysh/houseplan-card/issues/209)).
|
||||
|
||||
- A robot vacuum that reports its live position no longer freezes the card
|
||||
on a freshly opened page: the map-id lookup ran inside the render cycle
|
||||
and required editor code that a cold tab has not loaded yet, so the very
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
|
||||
## Не выпущено
|
||||
|
||||
- След пылесоса теперь повторяет те же записанные точки с мягко скруглёнными
|
||||
поворотами вместо заметной ломаной. Текущая и предыдущая уборки используют
|
||||
одну ограниченную кривую, сохраняют разрывы телеметрии и не отклоняются от
|
||||
записанного маршрута более чем на 17,5 см
|
||||
([#209](https://github.com/Matysh/houseplan-card/issues/209)).
|
||||
|
||||
- Робот-пылесос с живой позицией больше не замораживает карточку на только
|
||||
что открытой странице: определение id карты выполнялось внутри цикла
|
||||
отрисовки и требовало код редактора, который холодная вкладка ещё не
|
||||
|
||||
@@ -33,7 +33,7 @@ metadata). Only an explicit owner-approved emergency hotfix may skip this gate.
|
||||
| Furniture | #159 replaces the flat ~30-item picker with a two-level category/variant palette and 56 top-view symbols. The reviewed 77-SVG MIT source pack is vendored under `assets/furniture/houseplan-0.3.0`; plan art stays in the initial View graph, front-view category art stays in the lazy editor graph, and existing saved geometry is unchanged. |
|
||||
| Tests | Four layers: Node unit (`npm test`: frontend pure modules + tooling policy), pure backend (`pytest tests_backend`, runs anywhere), HA-harness backend (same folder, CI only — needs py3.13 + pytest-homeassistant-custom-component), and browser smokes (`demo/smoke_*.mjs`, headless chromium). **Counts are not written down here** — they went stale within two releases while the version line beside them was kept current, which reads as less coverage than exists (review R5-2). Run `npm run inventory` for the current numbers, or read them off the last CI run |
|
||||
| Input support | Owner's rule since 2026-08-08: View and kiosk are fully supported and release-blocking on touch. All three editors are desktop-first; touch editing is best effort and may be awkward, reduced or absent when parity is expensive. `docs/TOUCH-SUPPORT.md` defines the non-negotiable safety floor and documentation/test rules |
|
||||
| Vacuums | Live puck, server-side trails and fit calibration are shipped. The local v1.61 Stage 1 contract in docs/VACUUM.md adds explicit Dreame/XCME/Valetudo coverage, registry-less source selection, capability diagnostics, path-gap preservation and source-health warnings; #205's issue branch resumes one ended same-map run through an inclusive 30-minute station/pause grace. Roomba remains Stage 2 |
|
||||
| Vacuums | Live puck, server-side trails and fit calibration are shipped. The local v1.61 Stage 1 contract in docs/VACUUM.md adds explicit Dreame/XCME/Valetudo coverage, registry-less source selection, capability diagnostics, path-gap preservation and source-health warnings; #205 resumes one ended same-map run through an inclusive 30-minute station/pause grace. #209 renders current and previous trails through the same bounded 17.5 cm rounded-corner curve without changing stored points or gaps. Roomba remains Stage 2 |
|
||||
| Demo stand | **https://demo.houseplan.tech** — public, login `demo`/`demo`, resets to a pristine synthetic home every hour. **https://dev.houseplan.tech** — closed (basic auth), auto-deploys the `dev` branch every 10 min. Host: `ssh -i ~/.ssh/hp_stand hp@135.106.166.146`; layout, seeds and gotchas in the memory note `houseplan-demo-stand`. Since 2026-07-31 the stand covers most of the manual checklist: a scripted robot vacuum (`demo/stand/demo_robot` — Tasshack-shaped map sensor, serpentine run, pre-solved calibration, seeded server trail), Zigbee-style LQI template sensors, hand/auto-triggered leak+smoke alarms, an hvac_action climate marker and working script/scene/automation targets for tap-run. The stand-specific how-to-check guide is **docs/TESTING-DEMO.md** |
|
||||
| Community | **Telegram chat: https://t.me/ha_houseplan** (created 2026-07-27) — the primary user-facing support channel; GitHub issues stay for bugs/features. Link it from any new release notes and posts |
|
||||
| Product scope | `docs/SCOPE.md` is the feature guard rail; `docs/TOUCH-SUPPORT.md` is the input-support contract — check both before accepting interaction work |
|
||||
|
||||
@@ -37,6 +37,26 @@
|
||||
мутантам с браузерным гвардом; компиляция тестов в worktree стартует с
|
||||
тёплого `test-build/` основного дерева.
|
||||
|
||||
## Vacuum trail smoothing (#209)
|
||||
|
||||
- [ ] Pure `smoothVacPath` tests prove exact endpoints, separate subpaths,
|
||||
degenerate/reversal fallbacks, the 17.5 cm sampled deviation bound and
|
||||
the 64-subpath/4000-point `≤2N` command budget
|
||||
[unit: `test/vacuum.test.mjs`].
|
||||
- [ ] The production bundle renders current and previous runs through paired
|
||||
case/core `<path>` elements with quadratic commands, unchanged source
|
||||
authority/modes, live-target trim and puck tip
|
||||
[auto: `demo/smoke_vacuum.mjs`].
|
||||
- [ ] Flat and dormant-Iso projections retain the same curved live layer on
|
||||
touch and across HA updates [auto: `demo/smoke_isometric_live_touch.mjs`].
|
||||
- [ ] `vacuum-trail-smoothing-dark` contains two current subpaths plus a stored
|
||||
previous run; its semantic guard requires two `M` sections, quadratic
|
||||
commands and byte-identical case/core geometry before PNG comparison.
|
||||
The baseline is accepted only from reviewed Linux CI
|
||||
[golden: `demo/golden/matrix.mjs`].
|
||||
- [ ] Replacing the quadratic corner with a straight vertex makes the focused
|
||||
unit guard red [mutation: `vacuum-trail-smoothing-disabled`].
|
||||
|
||||
## Stable wall-segment identity (#282)
|
||||
|
||||
- [ ] Wall junction limits (#329): drawing refuses an apex under 15°, a seventh
|
||||
|
||||
@@ -1414,6 +1414,9 @@ Map Extractor, dreame-vacuum (Tasshack) и Valetudo-подобные камер
|
||||
- Позиция старше 60 секунд визуально считается устаревшей.
|
||||
- Разрывы integration path сохраняются отдельными участками и не соединяются
|
||||
ложной линией. Хранятся максимум 64 рисуемых участка и 4000 точек.
|
||||
- Текущий и предыдущий следы автоматически показываются плавными кривыми. Они
|
||||
сохраняют исходные концы и разрывы маршрута и отклоняются от записанной
|
||||
ломаной не более чем на 17,5 см независимо от масштаба и вида плана.
|
||||
- Для текущего пути приоритет таков: путь интеграции → серверный текущий run →
|
||||
локальный буфер. Пустой или одноточечный участок приоритет не перехватывает.
|
||||
- Остановка, пауза или визит на базу сразу скрывают текущий след в режиме
|
||||
|
||||
@@ -106,6 +106,13 @@ two points are discarded before limits are applied. The newest 64 drawable
|
||||
subpaths are kept, with at most 4000 total points; both endpoints of every kept
|
||||
subpath survive deterministic proportional thinning.
|
||||
|
||||
Current and previous trails use the same bounded rounded-corner curve. The
|
||||
curve preserves every subpath endpoint and never leaves the recorded polyline
|
||||
by more than 17.5 cm in physical plan coordinates. Smoothing happens after
|
||||
vacuum calibration and before flat/isometric projection, so zoom, viewport,
|
||||
DPR and projection do not change that limit. The live target is still trimmed
|
||||
before rendering, and smoothing never bridges an integration data gap.
|
||||
|
||||
| Display mode | While moving | After movement stops |
|
||||
|---|---|---|
|
||||
| `never` | Hidden | Hidden |
|
||||
|
||||
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 336 KiB After Width: | Height: | Size: 288 KiB |
|
Before Width: | Height: | Size: 327 KiB After Width: | Height: | Size: 277 KiB |
|
Before Width: | Height: | Size: 289 KiB After Width: | Height: | Size: 246 KiB |
|
Before Width: | Height: | Size: 291 KiB After Width: | Height: | Size: 244 KiB |
|
Before Width: | Height: | Size: 290 KiB After Width: | Height: | Size: 242 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 116 KiB |
@@ -2,8 +2,8 @@
|
||||
"version": 1,
|
||||
"fixture": "synthetic-only",
|
||||
"chromium": "151.0.7922.34",
|
||||
"oxipng": null,
|
||||
"sourceFingerprint": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
|
||||
"oxipng": "oxipng 10.2.0",
|
||||
"sourceFingerprint": "4b183589451e9a238ceeab91d847fcc1806215e85ee64f2c27b4b1cb167f6181",
|
||||
"captureScriptSha256": "fe76c8e2e27501e9a377d7aa15dd6cba6a018d55c07deb602dbee097f0a0a5ba",
|
||||
"command": "npm run build && node demo/docs/capture.mjs",
|
||||
"scenarios": {
|
||||
@@ -15,8 +15,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
|
||||
"imageSha256": "36223106c073f07d8cc3ecf8eaab37192ebb2687daba65c5c21047d0b7890de0"
|
||||
"sourceSha256": "4b183589451e9a238ceeab91d847fcc1806215e85ee64f2c27b4b1cb167f6181",
|
||||
"imageSha256": "f8c9541c9d7fde462d0efba63dff70944dc03df76ce988eecaf2b269e2863a96"
|
||||
},
|
||||
"view-touch": {
|
||||
"file": "02-view-touch.png",
|
||||
@@ -26,8 +26,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
|
||||
"imageSha256": "a4aa3af48578ee41594a9b1a6e98b4d39f3324a949a9fd9e63d5854576d5db85"
|
||||
"sourceSha256": "4b183589451e9a238ceeab91d847fcc1806215e85ee64f2c27b4b1cb167f6181",
|
||||
"imageSha256": "21c6fe7d79ff5c878ade8e98a64cc90dfd7bcc66338c864fb5a0bd411471313d"
|
||||
},
|
||||
"space-create": {
|
||||
"file": "03-space-create.png",
|
||||
@@ -37,8 +37,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
|
||||
"imageSha256": "0dc024726327f60f4a9ceaf3044381691f81f1090af81d1812e870f22d9343ba"
|
||||
"sourceSha256": "4b183589451e9a238ceeab91d847fcc1806215e85ee64f2c27b4b1cb167f6181",
|
||||
"imageSha256": "f6d190f0b2ac9125cea68aea2ebbcdf9a7081ae2a3565ff253fb1ed0c4a56483"
|
||||
},
|
||||
"room-contour-close": {
|
||||
"file": "04-room-contour-close.png",
|
||||
@@ -48,8 +48,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
|
||||
"imageSha256": "44cfaa95bd51e2cd628400e28db0ad8b2f0cd904385845bd402494f3f5c0d93c"
|
||||
"sourceSha256": "4b183589451e9a238ceeab91d847fcc1806215e85ee64f2c27b4b1cb167f6181",
|
||||
"imageSha256": "ca8bb143d0d6679ff09f8e9c1f5ad68b2ca7cfd96a4740a4a650c251f55dfba5"
|
||||
},
|
||||
"plan-context-tray": {
|
||||
"file": "05-plan-context-tray.png",
|
||||
@@ -59,8 +59,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
|
||||
"imageSha256": "e0662429b423fb74151b583dcc2c8635b001b637d03bbf7a6b16aec46399c3f8"
|
||||
"sourceSha256": "4b183589451e9a238ceeab91d847fcc1806215e85ee64f2c27b4b1cb167f6181",
|
||||
"imageSha256": "06b67c5108f9ee29a226cc2920484ccffe0c79840aec09721072b25b91ef8f03"
|
||||
},
|
||||
"device-editor": {
|
||||
"file": "06-device-editor.png",
|
||||
@@ -70,8 +70,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
|
||||
"imageSha256": "d0ffd31ce80bfde21ab75da356a5fc1af38246f2b301030880320620c228d89d"
|
||||
"sourceSha256": "4b183589451e9a238ceeab91d847fcc1806215e85ee64f2c27b4b1cb167f6181",
|
||||
"imageSha256": "9e435a6fe89564b9215b82a61fb7eae998266dd05483524ffa84160c35630e0c"
|
||||
},
|
||||
"device-display-preview": {
|
||||
"file": "06-device-display-preview.png",
|
||||
@@ -81,8 +81,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
|
||||
"imageSha256": "2cdabae1f89c3286e4fac0ce30f757ee1690b707ab8a5488748b7cd420626160"
|
||||
"sourceSha256": "4b183589451e9a238ceeab91d847fcc1806215e85ee64f2c27b4b1cb167f6181",
|
||||
"imageSha256": "cc5abe1b3364bf9812bea6ea5cfa84f35120ff9639ed3c4c92958cb646a8caf7"
|
||||
},
|
||||
"background-editor": {
|
||||
"file": "07-background-editor.png",
|
||||
@@ -92,8 +92,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
|
||||
"imageSha256": "30147bb00a90eea7136b4cee30995f6e6a9217b5132f3e8d3ad7471413b1af8a"
|
||||
"sourceSha256": "4b183589451e9a238ceeab91d847fcc1806215e85ee64f2c27b4b1cb167f6181",
|
||||
"imageSha256": "7e308a2b105fa6a9f788449ca30b8183375a32f0d0f8726f4a131e4f35527f57"
|
||||
},
|
||||
"room-card": {
|
||||
"file": "08-room-card.png",
|
||||
@@ -103,8 +103,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
|
||||
"imageSha256": "029a3e69ec647a8a370d99e6bb7f9225833c526739076022f6b52ba54bff30ea"
|
||||
"sourceSha256": "4b183589451e9a238ceeab91d847fcc1806215e85ee64f2c27b4b1cb167f6181",
|
||||
"imageSha256": "f1bc10b478f7afc68e70cb4c37148f680b0bbd41d50a7d63a21067fc952ff134"
|
||||
},
|
||||
"device-info": {
|
||||
"file": "09-device-info.png",
|
||||
@@ -114,8 +114,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
|
||||
"imageSha256": "dd492f53150b7149085daada5cce9eeae9bde9e7ea1d86679a54b3041f72f517"
|
||||
"sourceSha256": "4b183589451e9a238ceeab91d847fcc1806215e85ee64f2c27b4b1cb167f6181",
|
||||
"imageSha256": "2634cd7ead5720ce46ea4d6fa8f31006b740c520a73ddc97afe9a6d9d3b49778"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2441,6 +2441,18 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: ' resumed = False',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'vacuum-trail-smoothing-disabled',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test --test-name-pattern="smoothVacPath rounds corners" test/vacuum.test.mjs',
|
||||
because: 'the bounded curve is the user-visible point of #209; restoring a straight vertex '
|
||||
+ 'must be rejected by the focused geometry test rather than blessed by a matching empty golden',
|
||||
patches: [{
|
||||
file: 'src/vacuum.ts',
|
||||
find: " commands.push({ kind: 'quadratic', control: b, point: after });",
|
||||
replace: " commands.push({ kind: 'line', point: b });",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'device-unavailable-hover-restored',
|
||||
guard: 'node demo/smoke_device_icon_design.mjs',
|
||||
|
||||
@@ -104,7 +104,8 @@ import { mdiHomeCityOutline } from '@mdi/js';
|
||||
import {
|
||||
Affine, applyAffine, readVacTelemetry,
|
||||
autoCalibrate, pushTrailPoint, isVacMoving, vacTrailMode, vacMapIdWithFallback,
|
||||
parseVacSourceCandidate, resolveVacSource, resolveCurrentVacPath, trimVacPathTarget, areaCentroid,
|
||||
parseVacSourceCandidate, resolveVacSource, resolveCurrentVacPath, trimVacPathTarget, normalizeVacPath,
|
||||
smoothVacPath, VAC_TRAIL_SMOOTH_RADIUS_CM, areaCentroid,
|
||||
vacCalibrationResidualCm, vacRoomNameMatchCount, VAC_CALIBRATION_WARN_CM,
|
||||
VAC_TELEPORT_GAP_MS, VAC_STALE_MS,
|
||||
FitParams, fitMatrix, fitFromMatrix, initialFit, reanchorFit, VacRoom,
|
||||
@@ -11815,6 +11816,21 @@ export class HouseplanCard extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
/** One bounded current/previous trail serializer for flat and Iso surfaces. */
|
||||
private _vacTrailPathD(path: VacPath, matrix: Affine): string {
|
||||
const calibrated = path.map((segment) => segment.map(([x, y]) => applyAffine(matrix, x, y)));
|
||||
const commands = smoothVacPath(calibrated, this._cmToUnits(VAC_TRAIL_SMOOTH_RADIUS_CM));
|
||||
const pointText = (point: readonly [number, number]): string => {
|
||||
const scene = this._scenePoint(point);
|
||||
return `${scene[0].toFixed(1)} ${scene[1].toFixed(1)}`;
|
||||
};
|
||||
return commands.map((segment) => segment.map((command) => {
|
||||
if (command.kind === 'move') return `M ${pointText(command.point)}`;
|
||||
if (command.kind === 'line') return `L ${pointText(command.point)}`;
|
||||
return `Q ${pointText(command.control)} ${pointText(command.point)}`;
|
||||
}).join(' ')).join(' ');
|
||||
}
|
||||
|
||||
/** Puck + trail for every live vacuum of the space. */
|
||||
private _renderVacuums(devs: DevItem[], view: { x: number; y: number; w: number; h: number }): TemplateResult | typeof nothing {
|
||||
if (this._markup || this._mode === 'decor') return nothing;
|
||||
@@ -11846,13 +11862,12 @@ export class HouseplanCard extends LitElement {
|
||||
const srvPrev = srv?.previous?.map_id === mapNow && Array.isArray(srv.previous.points) ? srv.previous : null;
|
||||
// the PREVIOUS run stays visible even at rest: users compare where the
|
||||
// robot has been against where it has not (owner call 2026-07-31)
|
||||
if (tmode === 'always' && srvPrev && srvPrev.points.length > 1) {
|
||||
const pts = srvPrev.points.map(([x, y]: number[]) => {
|
||||
const [cx2, cy2] = applyAffine(matrix, x, y);
|
||||
const point = this._scenePoint([cx2, cy2]);
|
||||
return point[0].toFixed(1) + ',' + point[1].toFixed(1);
|
||||
}).join(' ');
|
||||
trails.push(svg`<g class="prev"><polyline class="case" points="${pts}"></polyline><polyline class="core" points="${pts}"></polyline></g>`);
|
||||
if (tmode === 'always' && srvPrev) {
|
||||
const previous = normalizeVacPath(srvPrev.points);
|
||||
const pathD = this._vacTrailPathD(previous, matrix);
|
||||
if (pathD) {
|
||||
trails.push(svg`<g class="prev"><path class="case" d="${pathD}"></path><path class="core" d="${pathD}"></path></g>`);
|
||||
}
|
||||
}
|
||||
// One arbitration authority: integration multi-subpath → server run →
|
||||
// local runtime. Only drawable segments participate.
|
||||
@@ -11863,13 +11878,9 @@ export class HouseplanCard extends LitElement {
|
||||
const raw: VacPath = moving && (current.source === 'integration' || current.source === 'server')
|
||||
? trimVacPathTarget(current.path) : current.path;
|
||||
if (raw.length) {
|
||||
const pathD = raw.map((segment) => segment.map(([x, y], index) => {
|
||||
const [cx, cy] = applyAffine(matrix, x, y);
|
||||
const point = this._scenePoint([cx, cy]);
|
||||
return `${index ? 'L' : 'M'} ${point[0].toFixed(1)} ${point[1].toFixed(1)}`;
|
||||
}).join(' ')).join(' ');
|
||||
const pathD = this._vacTrailPathD(raw, matrix);
|
||||
// A single SVG path with several M commands keeps gaps literal.
|
||||
trails.push(svg`<path class="case" d="${pathD}"></path><path class="core" d="${pathD}"></path>`);
|
||||
if (pathD) trails.push(svg`<path class="case" d="${pathD}"></path><path class="core" d="${pathD}"></path>`);
|
||||
const last = raw[raw.length - 1];
|
||||
if (moving && last?.length >= 2) {
|
||||
const anchor = last[last.length - 1];
|
||||
|
||||
@@ -11,6 +11,13 @@ export type Affine = [number, number, number, number, number, number];
|
||||
export type Pt = [number, number];
|
||||
export type VacPath = Pt[][];
|
||||
|
||||
export type VacPathCommand =
|
||||
| { kind: 'move'; point: Pt }
|
||||
| { kind: 'line'; point: Pt }
|
||||
| { kind: 'quadratic'; control: Pt; point: Pt };
|
||||
|
||||
export const VAC_TRAIL_SMOOTH_RADIUS_CM = 17.5;
|
||||
|
||||
export const VAC_PATH_MAX_SEGMENTS = 64;
|
||||
export const VAC_PATH_MAX_POINTS = 4000;
|
||||
|
||||
@@ -228,6 +235,60 @@ export function trimVacPathTarget(path: VacPath): VacPath {
|
||||
return out;
|
||||
}
|
||||
|
||||
const finitePoint = (point: Pt): boolean => Number.isFinite(point[0]) && Number.isFinite(point[1]);
|
||||
const samePoint = (a: Pt, b: Pt): boolean => a[0] === b[0] && a[1] === b[1];
|
||||
|
||||
/**
|
||||
* Turn calibrated plan-space trail points into bounded rounded-corner commands.
|
||||
*
|
||||
* Each corner is trimmed by at most half of either adjacent segment and by the
|
||||
* caller's physical radius. The quadratic lies in the P-B-Q convex hull, so it
|
||||
* cannot be farther from the source polyline than that radius. This function
|
||||
* deliberately knows nothing about SVG, Lit, scale conversion or flat/iso
|
||||
* projection; the renderer serialises and projects the typed commands.
|
||||
*/
|
||||
export function smoothVacPath(path: VacPath, maxRadius: number): VacPathCommand[][] {
|
||||
if (!Number.isFinite(maxRadius) || maxRadius <= 0) return [];
|
||||
const out: VacPathCommand[][] = [];
|
||||
for (const rawSegment of path) {
|
||||
if (!rawSegment.every(finitePoint)) continue;
|
||||
const segment = rawSegment.filter((point, index) => index === 0 || !samePoint(point, rawSegment[index - 1]));
|
||||
if (segment.length < 2) continue;
|
||||
const commands: VacPathCommand[] = [{ kind: 'move', point: segment[0] }];
|
||||
if (segment.length === 2) {
|
||||
commands.push({ kind: 'line', point: segment[1] });
|
||||
out.push(commands);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let index = 1; index < segment.length - 1; index++) {
|
||||
const a = segment[index - 1], b = segment[index], c = segment[index + 1];
|
||||
const inX = b[0] - a[0], inY = b[1] - a[1];
|
||||
const outX = c[0] - b[0], outY = c[1] - b[1];
|
||||
const inLength = Math.hypot(inX, inY), outLength = Math.hypot(outX, outY);
|
||||
const dot = (inX * outX + inY * outY) / (inLength * outLength);
|
||||
// An almost exact reversal would fold a quadratic back onto itself. Keep
|
||||
// that telemetry cusp literal instead of inventing a loop around it.
|
||||
if (!Number.isFinite(dot) || dot <= -0.999) {
|
||||
commands.push({ kind: 'line', point: b });
|
||||
continue;
|
||||
}
|
||||
const radius = Math.min(maxRadius, inLength / 2, outLength / 2);
|
||||
if (!Number.isFinite(radius) || radius <= 0) {
|
||||
commands.push({ kind: 'line', point: b });
|
||||
continue;
|
||||
}
|
||||
const before: Pt = [b[0] - inX * radius / inLength, b[1] - inY * radius / inLength];
|
||||
const after: Pt = [b[0] + outX * radius / outLength, b[1] + outY * radius / outLength];
|
||||
commands.push({ kind: 'line', point: before });
|
||||
commands.push({ kind: 'quadratic', control: b, point: after });
|
||||
}
|
||||
commands.push({ kind: 'line', point: segment[segment.length - 1] });
|
||||
out.push(commands);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map-id normalisation contract, shared with the backend recorder
|
||||
* (custom_components/houseplan/trails.py: resolve_map_id). The FIRST value
|
||||
|
||||
@@ -372,7 +372,7 @@ test('sun-ray golden requires browser-painted light from a state-only sun entity
|
||||
assert.ok(scenario);
|
||||
const fixture = prepareGoldenFixture(scenario);
|
||||
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
|
||||
assert.equal(GOLDEN_MATRIX_VERSION, 49);
|
||||
assert.equal(GOLDEN_MATRIX_VERSION, 50);
|
||||
assert.equal(space.settings.sun_rays, true);
|
||||
assert.equal(scenario.northDeg, 90,
|
||||
'the sign-sensitive golden must keep a non-zero north direction');
|
||||
@@ -390,6 +390,18 @@ test('sun-ray golden requires browser-painted light from a state-only sun entity
|
||||
assert.equal(scenario.sunRayPixels.minChannelDelta >= 4, true);
|
||||
});
|
||||
|
||||
test('vacuum smoothing golden covers current gaps and the previous run', () => {
|
||||
const scenario = GOLDEN_SCENARIOS.find((item) => item.id === 'vacuum-trail-smoothing-dark');
|
||||
assert.ok(scenario);
|
||||
assert.equal(scenario.mode, 'view');
|
||||
assert.equal(scenario.vacuumTrail?.current.length, 2);
|
||||
assert.equal(scenario.vacuumTrail?.current.flat().length >= 8, true);
|
||||
assert.equal(scenario.vacuumTrail?.previous.length >= 4, true);
|
||||
const fixture = prepareGoldenFixture(scenario);
|
||||
assert.equal(fixture.config.markers.some((marker) => marker.id === 'golden-vacuum-trail'), true);
|
||||
assert.equal(fixture.states['vacuum.golden_vacuum_trail']?.state, 'cleaning');
|
||||
});
|
||||
|
||||
test('issue #277 golden pairs safe/disabled handles and an opening clamp in both themes', () => {
|
||||
const scenarios = GOLDEN_SCENARIOS.filter((item) => item.safeResizePreview);
|
||||
assert.deepEqual(scenarios.map((item) => item.theme).sort(), ['dark', 'light']);
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
solveAffine, applyAffine, affineResidual, readVacTelemetry, autoCalibrate,
|
||||
thinPath, pushTrailPoint, isVacMoving, isVacSourceState, vacMapIdFromAttrs,
|
||||
vacMapIdWithFallback, areaCentroid, normalizeVacPath, resolveCurrentVacPath,
|
||||
trimVacPathTarget, parseVacSourceCandidate, resolveVacSource,
|
||||
trimVacPathTarget, smoothVacPath, VAC_TRAIL_SMOOTH_RADIUS_CM,
|
||||
parseVacSourceCandidate, resolveVacSource,
|
||||
vacCalibrationResidualCm, vacRoomNameMatchCount, VAC_CALIBRATION_WARN_CM,
|
||||
} from '../test-build/vacuum.js';
|
||||
|
||||
@@ -181,6 +182,88 @@ test('resolveCurrentVacPath arbitrates only drawable sources and preserves gaps'
|
||||
]);
|
||||
});
|
||||
|
||||
test('smoothVacPath rounds corners within 17.5 cm and preserves exact endpoints', () => {
|
||||
const gridPitch = 100;
|
||||
const cellCm = 5;
|
||||
const radius = (VAC_TRAIL_SMOOTH_RADIUS_CM / cellCm) * gridPitch;
|
||||
const source = [[[0, 0], [900, 0], [900, 500], [1400, 650]]];
|
||||
const [commands] = smoothVacPath(source, radius);
|
||||
assert.deepEqual(commands[0], { kind: 'move', point: source[0][0] });
|
||||
assert.deepEqual(commands.at(-1), { kind: 'line', point: source[0].at(-1) });
|
||||
assert.equal(commands.filter((command) => command.kind === 'quadratic').length, 2);
|
||||
|
||||
const distanceToSegment = (point, a, b) => {
|
||||
const dx = b[0] - a[0], dy = b[1] - a[1];
|
||||
const denominator = dx * dx + dy * dy;
|
||||
const t = denominator ? Math.max(0, Math.min(1,
|
||||
((point[0] - a[0]) * dx + (point[1] - a[1]) * dy) / denominator)) : 0;
|
||||
return Math.hypot(point[0] - (a[0] + dx * t), point[1] - (a[1] + dy * t));
|
||||
};
|
||||
const distanceToSource = (point) => Math.min(...source[0].slice(1).map(
|
||||
(end, index) => distanceToSegment(point, source[0][index], end),
|
||||
));
|
||||
for (let index = 0; index < commands.length; index++) {
|
||||
const command = commands[index];
|
||||
if (command.kind !== 'quadratic') continue;
|
||||
const start = commands[index - 1].point;
|
||||
for (let sample = 0; sample <= 100; sample++) {
|
||||
const t = sample / 100, u = 1 - t;
|
||||
const point = [
|
||||
u * u * start[0] + 2 * u * t * command.control[0] + t * t * command.point[0],
|
||||
u * u * start[1] + 2 * u * t * command.control[1] + t * t * command.point[1],
|
||||
];
|
||||
const deviationCm = (distanceToSource(point) / gridPitch) * cellCm;
|
||||
assert.ok(deviationCm <= VAC_TRAIL_SMOOTH_RADIUS_CM + 1e-9,
|
||||
`curve deviated by ${deviationCm} cm`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('smoothVacPath preserves subpath gaps and bounds short uneven corners', () => {
|
||||
const path = [
|
||||
[[0, 0], [2, 0], [2, 1000]],
|
||||
[[5000, 5000], [5010, 5000], [5010, 5001], [6000, 5001]],
|
||||
];
|
||||
const result = smoothVacPath(path, 100);
|
||||
assert.equal(result.length, 2);
|
||||
assert.deepEqual(result.map((segment) => segment[0]), [
|
||||
{ kind: 'move', point: [0, 0] },
|
||||
{ kind: 'move', point: [5000, 5000] },
|
||||
]);
|
||||
assert.deepEqual(result.map((segment) => segment.at(-1).point), [[2, 1000], [6000, 5001]]);
|
||||
const firstCurve = result[0].find((command) => command.kind === 'quadratic');
|
||||
assert.deepEqual(firstCurve.point, [2, 1]);
|
||||
});
|
||||
|
||||
test('smoothVacPath fails closed for degenerate, non-finite and reversal input', () => {
|
||||
assert.deepEqual(smoothVacPath([], 10), []);
|
||||
assert.deepEqual(smoothVacPath([[[0, 0]]], 10), []);
|
||||
assert.deepEqual(smoothVacPath([[[0, 0], [1, 1]]], 0), []);
|
||||
assert.deepEqual(smoothVacPath([[[0, 0], [NaN, 1], [2, 2]]], 10), []);
|
||||
const duplicate = smoothVacPath([[[0, 0], [0, 0], [10, 0]]], 10);
|
||||
assert.deepEqual(duplicate, [[
|
||||
{ kind: 'move', point: [0, 0] },
|
||||
{ kind: 'line', point: [10, 0] },
|
||||
]]);
|
||||
const reversal = smoothVacPath([[[0, 0], [10, 0], [0, 0]]], 10)[0];
|
||||
assert.equal(reversal.some((command) => command.kind === 'quadratic'), false);
|
||||
assert.equal(JSON.stringify(reversal).includes('null'), false);
|
||||
});
|
||||
|
||||
test('smoothVacPath stays linear and finite at the 64/4000 path budget', () => {
|
||||
const path = Array.from({ length: 64 }, (_, segment) => {
|
||||
const length = segment < 32 ? 63 : 62;
|
||||
return Array.from({ length }, (_, point) => [segment * 1000 + point, point % 2]);
|
||||
});
|
||||
const pointCount = path.reduce((sum, segment) => sum + segment.length, 0);
|
||||
assert.equal(pointCount, 4000);
|
||||
const result = smoothVacPath(path, 17.5);
|
||||
const commandCount = result.reduce((sum, segment) => sum + segment.length, 0);
|
||||
assert.equal(result.length, 64);
|
||||
assert.ok(commandCount <= pointCount * 2);
|
||||
assert.equal(JSON.stringify(result).includes('null'), false);
|
||||
});
|
||||
|
||||
test('parseVacSourceCandidate is deterministic and explains XCME/camera capability', () => {
|
||||
const positioned = parseVacSourceCandidate('camera.map', {
|
||||
attributes: { friendly_name: 'Map', vacuum_position: { x: 1, y: 2 }, rooms: {} },
|
||||
|
||||