fix: scale furniture strokes with plan zoom

Issue: #361
User-Visible: yes
This commit is contained in:
Sergey Matyunin
2026-08-29 07:46:07 +00:00
committed by claude[bot]
parent 8adcd52a1f
commit 6c60848512
36 changed files with 638 additions and 339 deletions
@@ -1,113 +1,113 @@
{
"schema": 1,
"fingerprint": "048a8c9c9ad15757438fe100b5f2afaaa74bd2ddf749b378a7e784e69aaed2bb",
"fingerprint": "2f53621bc0d0f8ce3e04153700c0c17286b37d4dae42519acf74d6023333442a",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-DFgj-WTW.js",
"houseplan-assets/houseplan-card-z66sNkRH.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 273385,
"initialViewGzipBytes": 273551,
"lazyFiles": [
"houseplan-assets/backdrop-pick-BETSWzR3.js",
"houseplan-assets/de-CrxBNzOZ.js",
"houseplan-assets/editor-DEVkkvAS.js",
"houseplan-assets/houseplan-editor-runtime-D8M9Xksp.js",
"houseplan-assets/houseplan-onboarding-runtime-k5k7DpeG.js"
"houseplan-assets/backdrop-pick-Tyu0BfFF.js",
"houseplan-assets/de-nY6CPgRE.js",
"houseplan-assets/editor-Btuj24RH.js",
"houseplan-assets/houseplan-editor-runtime-CztWrxSd.js",
"houseplan-assets/houseplan-onboarding-runtime-G4p88P9H.js"
],
"lazyGzipBytes": 168759,
"lazyGzipBytes": 168764,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-BETSWzR3.js",
"houseplan-assets/editor-DEVkkvAS.js",
"houseplan-assets/houseplan-editor-runtime-D8M9Xksp.js"
"houseplan-assets/backdrop-pick-Tyu0BfFF.js",
"houseplan-assets/editor-Btuj24RH.js",
"houseplan-assets/houseplan-editor-runtime-CztWrxSd.js"
],
"lazyEditorGzipBytes": 139476,
"lazyEditorGzipBytes": 139471,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-BETSWzR3.js",
"houseplan-assets/houseplan-onboarding-runtime-k5k7DpeG.js"
"houseplan-assets/backdrop-pick-Tyu0BfFF.js",
"houseplan-assets/houseplan-onboarding-runtime-G4p88P9H.js"
],
"lazyOnboardingGzipBytes": 9450,
"lazyOnboardingGzipBytes": 9453,
"lazyLocaleFiles": [
"houseplan-assets/de-CrxBNzOZ.js"
"houseplan-assets/de-nY6CPgRE.js"
],
"lazyLocaleGzipBytes": 22788,
"lazyLocaleGzipBytes": 22796,
"files": [
{
"path": "houseplan-assets/backdrop-pick-BETSWzR3.js",
"sha256": "5f6f70b3a6c1fd480dd5523dd7aca0cf1cab222c020c0aeaf7c2486bdbcbc22e",
"path": "houseplan-assets/backdrop-pick-Tyu0BfFF.js",
"sha256": "db0d4c348fee148aff02781bfb2c1d0fa4afb50b7f7c7670768b748675f5389a",
"rawBytes": 6967,
"gzipBytes": 2955,
"gzipBytes": 2956,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DFgj-WTW.js"
"houseplan-assets/houseplan-card-z66sNkRH.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-CrxBNzOZ.js",
"sha256": "bd36f8b22947658266b391181b3c35f8892511fc33d12a0d8b0b676c1066a51d",
"path": "houseplan-assets/de-nY6CPgRE.js",
"sha256": "1ea533abc21c2d2585941122c21212deeec234adf5099ae8afce611bc3a6cd49",
"rawBytes": 74836,
"gzipBytes": 22788,
"gzipBytes": 22796,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-DEVkkvAS.js",
"sha256": "724b393d08bb7b50fb6a55fa84403ac506d49746ad5047d882572d1773b6597a",
"path": "houseplan-assets/editor-Btuj24RH.js",
"sha256": "e462de3e6a34cce0b93fabf0804a430ce81bcfd2fe6c26e65557ec65504cd964",
"rawBytes": 3831,
"gzipBytes": 1583,
"gzipBytes": 1585,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DFgj-WTW.js"
"houseplan-assets/houseplan-card-z66sNkRH.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-DFgj-WTW.js",
"sha256": "f0a394e42255f50ab1e5c89dc3a2424690592df9d51c19ce5326245f79395815",
"rawBytes": 963249,
"gzipBytes": 272633,
"path": "houseplan-assets/houseplan-card-z66sNkRH.js",
"sha256": "33ee209a395a96a13e293ca1d48c2a5af686141b094bb92966d369f9dbcc8813",
"rawBytes": 963751,
"gzipBytes": 272797,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-CrxBNzOZ.js",
"houseplan-assets/editor-DEVkkvAS.js",
"houseplan-assets/houseplan-editor-runtime-D8M9Xksp.js",
"houseplan-assets/houseplan-onboarding-runtime-k5k7DpeG.js"
"houseplan-assets/de-nY6CPgRE.js",
"houseplan-assets/editor-Btuj24RH.js",
"houseplan-assets/houseplan-editor-runtime-CztWrxSd.js",
"houseplan-assets/houseplan-onboarding-runtime-G4p88P9H.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-D8M9Xksp.js",
"sha256": "74278a13fbcd9748c378edb13d4ba954b1f18801e24fa4711fad81c9cf1d4603",
"path": "houseplan-assets/houseplan-editor-runtime-CztWrxSd.js",
"sha256": "ac4ed4d1f057b7bae3ca52141d71beb81fd0ff971fc81e28d71073ad9bd5a5b2",
"rawBytes": 522033,
"gzipBytes": 134938,
"gzipBytes": 134930,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-BETSWzR3.js",
"houseplan-assets/houseplan-card-DFgj-WTW.js"
"houseplan-assets/backdrop-pick-Tyu0BfFF.js",
"houseplan-assets/houseplan-card-z66sNkRH.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-k5k7DpeG.js",
"sha256": "70ac81d086dddda7bb5d634e76bb3b5f46e80b423fb76c5376f5a38ee8a1100e",
"path": "houseplan-assets/houseplan-onboarding-runtime-G4p88P9H.js",
"sha256": "9094f910509b9881e8d7882a9ad1855e39610da15a2a163088af4d1e96c8687f",
"rawBytes": 26195,
"gzipBytes": 6495,
"gzipBytes": 6497,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-BETSWzR3.js",
"houseplan-assets/houseplan-card-DFgj-WTW.js"
"houseplan-assets/backdrop-pick-Tyu0BfFF.js",
"houseplan-assets/houseplan-card-z66sNkRH.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "22e46001f1a6549cb4eb63d779ea0fbe08a609c069ed98364bbcc5d2962e352d",
"sha256": "cdd74054b0eda637b8e8184c96291b395fd558678f8e55af673931615e01b085",
"rawBytes": 1089,
"gzipBytes": 752,
"gzipBytes": 754,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-DFgj-WTW.js"
"houseplan-assets/houseplan-card-z66sNkRH.js"
],
"dynamicImports": []
}
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="048a8c9c9ad15757438fe100b5f2afaaa74bd2ddf749b378a7e784e69aaed2bb";import{c as e,l as o,t,cZ as s,A as a,c_ as i,c$ as l,d0 as n,E as r,b as c}from"./houseplan-card-DFgj-WTW.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="2f53621bc0d0f8ce3e04153700c0c17286b37d4dae42519acf74d6023333442a";import{c as e,l as o,t,cZ as s,A as a,c_ as i,c$ as l,d0 as n,E as r,b as c}from"./houseplan-card-z66sNkRH.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}
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="048a8c9c9ad15757438fe100b5f2afaaa74bd2ddf749b378a7e784e69aaed2bb";import{bT as t,bU as s,bV as e,bW as o,aq as a,bX as l,c0 as i,bY as h,bZ as c,b_ as n,b$ as r,A as p,b as _,cR as d,ap as u,ar as g,cS as m}from"./houseplan-card-DFgj-WTW.js";import{i as b,c as $,e as v,r as f,a as y,b as w,s as D,t as S}from"./backdrop-pick-BETSWzR3.js";const k=1e3,C=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},M="048a8c9c9ad15757438fe100b5f2afaaa74bd2ddf749b378a7e784e69aaed2bb";class F{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),c=i.settings?.custom_fill&&"object"==typeof i.settings.custom_fill?s(i.settings.custom_fill):null,n="none"===h.fill?{...c||e,a:0}:c;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:n,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:n,fillMode:"custom",customFill:{...e,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:c,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;s.value="";const o=await $(e);if("reject"===o.kind)return void this.host._showToast(this.host._t("toast.plan_formats"));if("guard"===o.kind)return void(this.host._backdropGuard=o.state);const a=await v(e,o.ext,e.name);this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:a})}_renderBackdropGuard(){return f(this.host,t=>{this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:t})},()=>{this.host._backdropGuard=null},this.host.hass)??p}_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 _`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return _`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return _`<div class="savedplans">
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="2f53621bc0d0f8ce3e04153700c0c17286b37d4dae42519acf74d6023333442a";import{bT as t,bU as s,bV as e,bW as o,aq as a,bX as l,c0 as i,bY as h,bZ as c,b_ as n,b$ as r,A as p,b as _,cR as d,ap as u,ar as g,cS as m}from"./houseplan-card-z66sNkRH.js";import{i as b,c as $,e as v,r as f,a as y,b as w,s as D,t as S}from"./backdrop-pick-Tyu0BfFF.js";const k=1e3,C=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},M="2f53621bc0d0f8ce3e04153700c0c17286b37d4dae42519acf74d6023333442a";class F{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),c=i.settings?.custom_fill&&"object"==typeof i.settings.custom_fill?s(i.settings.custom_fill):null,n="none"===h.fill?{...c||e,a:0}:c;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:n,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:n,fillMode:"custom",customFill:{...e,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:c,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;s.value="";const o=await $(e);if("reject"===o.kind)return void this.host._showToast(this.host._t("toast.plan_formats"));if("guard"===o.kind)return void(this.host._backdropGuard=o.state);const a=await v(e,o.ext,e.name);this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:a})}_renderBackdropGuard(){return f(this.host,t=>{this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:t})},()=>{this.host._backdropGuard=null},this.host.hass)??p}_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 _`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return _`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return _`<div class="savedplans">
${s.map(s=>{return _`
<div class="savedplan ${s.url===t.planUrl?"cur":""}">
<img src=${this.host._display(s.url)} alt="" loading="lazy" decoding="async" />
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="048a8c9c9ad15757438fe100b5f2afaaa74bd2ddf749b378a7e784e69aaed2bb";try{await import("./houseplan-assets/houseplan-card-DFgj-WTW.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__="2f53621bc0d0f8ce3e04153700c0c17286b37d4dae42519acf74d6023333442a";try{await import("./houseplan-assets/houseplan-card-z66sNkRH.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)}
+136
View File
@@ -198,6 +198,7 @@ const res = await page.evaluate(async () => {
await c.updateComplete;
const committedPreview = c._furniturePreviewPlacement
? JSON.parse(JSON.stringify(c._furniturePreviewPlacement)) : null;
const committedPreviewStroke = Number(ghost()?.getAttribute('stroke-width'));
ev('pointerdown', stageEl(), 300, 300, { pointerType: 'mouse' });
await c.updateComplete;
const sofa = c._decorList.find((s) => s.kind === 'furniture');
@@ -213,6 +214,8 @@ const res = await page.evaluate(async () => {
out.previewAndCommitAreIdentical = !!sofa && !!committedPreview
&& ['x', 'y', 'w', 'h'].every((k) => near(sofa[k], committedPreview[k], 1e-12))
&& (sofa.angle || 0) === committedPreview.angle;
out.previewAndCommitShareThePhysicalStroke = Number.isFinite(committedPreviewStroke)
&& near(Number(attr(sofaId, 'stroke-width')), committedPreviewStroke, 1e-9);
out.previewClearsAfterCommit = !ghost() && c._furnPreviewInput === null;
out.selectedRightAway = !!sofaId && c._decorSel === sofaId;
out.toolWentBackToSelect = c._decorTool === 'select';
@@ -426,5 +429,138 @@ const res = await page.evaluate(async () => {
out.inertInView = !!left && pe(left.id) === 'none';
return out;
});
// #361: this is deliberately a pixel assertion, not another DOM-attribute
// comparison. `vector-effect=non-scaling-stroke` can make the attributes look
// perfectly consistent while Chromium paints furniture at the wrong camera
// zoom. Bright magenta isolates the fixture from the normal plan artwork.
const renderStrokeFixture = async (zoom, angle = 0, viewport = null) => {
if (viewport) await page.setViewportSize(viewport);
await page.evaluate(async ({ zoom, angle }) => {
const c = window.__card;
c._setMode('view');
await c.updateComplete;
for (let i = 0; i < 80 && c._modeTransitionBusy; i++)
await new Promise((resolve) => setTimeout(resolve, 20));
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
c._curSpaceCfg.decor = [
{ id: 'stroke-control', kind: 'line', x1: 0.18, y1: 0.18, x2: 0.50, y2: 0.18,
color: '#ff00ff', opacity: 1, width_cm: 8 },
// Designer artwork: deliberately very wide and shallow so a leaked local
// x/y scale produces obviously different horizontal/vertical strokes.
{ id: 'stroke-designer', kind: 'furniture', symbol: 'coffee_table',
x: 0.18, y: 0.30, w: 0.38, h: 0.09, angle,
color: '#ff00ff', opacity: 1, width_cm: 8 },
// Compatibility artwork: unit-box primitive, exercising the other path
// source retained by the furniture catalogue.
{ id: 'stroke-primitive', kind: 'furniture', symbol: 'fridge',
x: 0.62, y: 0.27, w: 0.11, h: 0.20, angle,
color: '#ff00ff', opacity: 1, width_cm: 8 },
];
c._cfgEpoch++;
c._zoom = 1;
c._view = null;
c.requestUpdate();
await c.updateComplete;
c._applyView(zoom, 450, 350);
c.requestUpdate();
await c.updateComplete;
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
}, { zoom, angle });
const samples = await page.evaluate(() => {
const root = window.__card.shadowRoot || window.__card.renderRoot;
const sample = (id, a, b) => {
const node = root.querySelector(`.decorlayer [data-id="${id}"]`);
const matrix = node?.getScreenCTM?.();
if (!node || !matrix) return null;
const map = ([x, y]) => {
const point = new DOMPoint(x, y).matrixTransform(matrix);
return { x: point.x, y: point.y };
};
return { a: map(a), b: map(b) };
};
const line = root.querySelector('.decorlayer [data-id="stroke-control"]');
const x1 = Number(line?.getAttribute('x1'));
const x2 = Number(line?.getAttribute('x2'));
const y = Number(line?.getAttribute('y1'));
return {
control: sample('stroke-control', [x1 + (x2 - x1) * 0.35, y], [x1 + (x2 - x1) * 0.65, y]),
// coffee_table generated art: long top and right-side straight runs.
designerH: sample('stroke-designer', [30, 3.692], [90, 3.692]),
designerV: sample('stroke-designer', [116.16, 18], [116.16, 45]),
// fridge compatibility art is the canonical unit rectangle.
primitive: sample('stroke-primitive', [0.2, 0], [0.8, 0]),
};
});
const png = (await page.screenshot({ type: 'png' })).toString('base64');
return page.evaluate(async ({ png, samples }) => {
const image = new Image();
image.src = `data:image/png;base64,${png}`;
await image.decode();
const canvas = document.createElement('canvas');
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
ctx.drawImage(image, 0, 0);
const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
const magenta = (x, y) => {
x = Math.round(x); y = Math.round(y);
if (x < 0 || y < 0 || x >= canvas.width || y >= canvas.height) return false;
const i = (y * canvas.width + x) * 4;
return pixels[i] > 150 && pixels[i + 1] < 130 && pixels[i + 2] > 150 && pixels[i + 3] > 80;
};
const thickness = (segment) => {
if (!segment) return NaN;
const dx = segment.b.x - segment.a.x;
const dy = segment.b.y - segment.a.y;
const length = Math.hypot(dx, dy);
if (!(length > 8)) return NaN;
const tx = dx / length, ty = dy / length;
const nx = -ty, ny = tx;
const cx = (segment.a.x + segment.b.x) / 2;
const cy = (segment.a.y + segment.b.y) / 2;
const widths = [];
for (const along of [-8, -4, 0, 4, 8]) {
const hits = [];
for (let normal = -24; normal <= 24; normal++) {
if (magenta(cx + tx * along + nx * normal, cy + ty * along + ny * normal))
hits.push(normal);
}
if (hits.length) widths.push(hits.at(-1) - hits[0] + 1);
}
widths.sort((a, b) => a - b);
return widths.length ? widths[Math.floor(widths.length / 2)] : NaN;
};
return Object.fromEntries(Object.entries(samples).map(([key, value]) => [key, thickness(value)]));
}, { png, samples });
};
const z1 = await renderStrokeFixture(1);
const z2 = await renderStrokeFixture(2);
const rotated = await renderStrokeFixture(1, 30);
const rotatedZ2 = await renderStrokeFixture(2, 30);
const compact = await renderStrokeFixture(1, 0, { width: 620, height: 760 });
const finite = (...values) => values.every((value) => Number.isFinite(value) && value > 0);
const closePx = (a, b, tolerance = 2) => finite(a, b) && Math.abs(a - b) <= tolerance;
const doubles = (a, b) => finite(a, b) && b / a >= 1.6 && b / a <= 2.4;
res.rasterFixturePainted = finite(
z1.control, z1.designerH, z1.designerV, z1.primitive,
z2.control, z2.designerH, z2.primitive, rotated.designerH,
rotatedZ2.designerH, compact.control, compact.designerH, compact.primitive,
);
res.furnitureFollowsPhysicalCameraZoom = doubles(z1.control, z2.control)
&& doubles(z1.designerH, z2.designerH)
&& doubles(z1.primitive, z2.primitive)
&& doubles(rotated.designerH, rotatedZ2.designerH);
res.designerAndPrimitiveMatchOrdinaryDecor = closePx(z1.designerH, z1.control)
&& closePx(z1.primitive, z1.control)
&& closePx(z2.designerH, z2.control)
&& closePx(z2.primitive, z2.control);
res.anisotropicResizeKeepsBothAxesEqual = closePx(z1.designerH, z1.designerV);
res.rotatedArtworkKeepsTheSameThickness = closePx(rotated.designerH, z1.designerH);
res.viewportResizeRecalculatesTheSharedPhysicalStroke = closePx(compact.designerH, compact.control)
&& closePx(compact.primitive, compact.control)
&& Math.abs(compact.control - z1.control) >= 1;
checkAll(res);
await finish(browser, res);
+50 -50
View File
@@ -1,113 +1,113 @@
{
"schema": 1,
"fingerprint": "048a8c9c9ad15757438fe100b5f2afaaa74bd2ddf749b378a7e784e69aaed2bb",
"fingerprint": "2f53621bc0d0f8ce3e04153700c0c17286b37d4dae42519acf74d6023333442a",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-DFgj-WTW.js",
"houseplan-assets/houseplan-card-z66sNkRH.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 273385,
"initialViewGzipBytes": 273551,
"lazyFiles": [
"houseplan-assets/backdrop-pick-BETSWzR3.js",
"houseplan-assets/de-CrxBNzOZ.js",
"houseplan-assets/editor-DEVkkvAS.js",
"houseplan-assets/houseplan-editor-runtime-D8M9Xksp.js",
"houseplan-assets/houseplan-onboarding-runtime-k5k7DpeG.js"
"houseplan-assets/backdrop-pick-Tyu0BfFF.js",
"houseplan-assets/de-nY6CPgRE.js",
"houseplan-assets/editor-Btuj24RH.js",
"houseplan-assets/houseplan-editor-runtime-CztWrxSd.js",
"houseplan-assets/houseplan-onboarding-runtime-G4p88P9H.js"
],
"lazyGzipBytes": 168759,
"lazyGzipBytes": 168764,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-BETSWzR3.js",
"houseplan-assets/editor-DEVkkvAS.js",
"houseplan-assets/houseplan-editor-runtime-D8M9Xksp.js"
"houseplan-assets/backdrop-pick-Tyu0BfFF.js",
"houseplan-assets/editor-Btuj24RH.js",
"houseplan-assets/houseplan-editor-runtime-CztWrxSd.js"
],
"lazyEditorGzipBytes": 139476,
"lazyEditorGzipBytes": 139471,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-BETSWzR3.js",
"houseplan-assets/houseplan-onboarding-runtime-k5k7DpeG.js"
"houseplan-assets/backdrop-pick-Tyu0BfFF.js",
"houseplan-assets/houseplan-onboarding-runtime-G4p88P9H.js"
],
"lazyOnboardingGzipBytes": 9450,
"lazyOnboardingGzipBytes": 9453,
"lazyLocaleFiles": [
"houseplan-assets/de-CrxBNzOZ.js"
"houseplan-assets/de-nY6CPgRE.js"
],
"lazyLocaleGzipBytes": 22788,
"lazyLocaleGzipBytes": 22796,
"files": [
{
"path": "houseplan-assets/backdrop-pick-BETSWzR3.js",
"sha256": "5f6f70b3a6c1fd480dd5523dd7aca0cf1cab222c020c0aeaf7c2486bdbcbc22e",
"path": "houseplan-assets/backdrop-pick-Tyu0BfFF.js",
"sha256": "db0d4c348fee148aff02781bfb2c1d0fa4afb50b7f7c7670768b748675f5389a",
"rawBytes": 6967,
"gzipBytes": 2955,
"gzipBytes": 2956,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DFgj-WTW.js"
"houseplan-assets/houseplan-card-z66sNkRH.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-CrxBNzOZ.js",
"sha256": "bd36f8b22947658266b391181b3c35f8892511fc33d12a0d8b0b676c1066a51d",
"path": "houseplan-assets/de-nY6CPgRE.js",
"sha256": "1ea533abc21c2d2585941122c21212deeec234adf5099ae8afce611bc3a6cd49",
"rawBytes": 74836,
"gzipBytes": 22788,
"gzipBytes": 22796,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-DEVkkvAS.js",
"sha256": "724b393d08bb7b50fb6a55fa84403ac506d49746ad5047d882572d1773b6597a",
"path": "houseplan-assets/editor-Btuj24RH.js",
"sha256": "e462de3e6a34cce0b93fabf0804a430ce81bcfd2fe6c26e65557ec65504cd964",
"rawBytes": 3831,
"gzipBytes": 1583,
"gzipBytes": 1585,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DFgj-WTW.js"
"houseplan-assets/houseplan-card-z66sNkRH.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-DFgj-WTW.js",
"sha256": "f0a394e42255f50ab1e5c89dc3a2424690592df9d51c19ce5326245f79395815",
"rawBytes": 963249,
"gzipBytes": 272633,
"path": "houseplan-assets/houseplan-card-z66sNkRH.js",
"sha256": "33ee209a395a96a13e293ca1d48c2a5af686141b094bb92966d369f9dbcc8813",
"rawBytes": 963751,
"gzipBytes": 272797,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-CrxBNzOZ.js",
"houseplan-assets/editor-DEVkkvAS.js",
"houseplan-assets/houseplan-editor-runtime-D8M9Xksp.js",
"houseplan-assets/houseplan-onboarding-runtime-k5k7DpeG.js"
"houseplan-assets/de-nY6CPgRE.js",
"houseplan-assets/editor-Btuj24RH.js",
"houseplan-assets/houseplan-editor-runtime-CztWrxSd.js",
"houseplan-assets/houseplan-onboarding-runtime-G4p88P9H.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-D8M9Xksp.js",
"sha256": "74278a13fbcd9748c378edb13d4ba954b1f18801e24fa4711fad81c9cf1d4603",
"path": "houseplan-assets/houseplan-editor-runtime-CztWrxSd.js",
"sha256": "ac4ed4d1f057b7bae3ca52141d71beb81fd0ff971fc81e28d71073ad9bd5a5b2",
"rawBytes": 522033,
"gzipBytes": 134938,
"gzipBytes": 134930,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-BETSWzR3.js",
"houseplan-assets/houseplan-card-DFgj-WTW.js"
"houseplan-assets/backdrop-pick-Tyu0BfFF.js",
"houseplan-assets/houseplan-card-z66sNkRH.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-k5k7DpeG.js",
"sha256": "70ac81d086dddda7bb5d634e76bb3b5f46e80b423fb76c5376f5a38ee8a1100e",
"path": "houseplan-assets/houseplan-onboarding-runtime-G4p88P9H.js",
"sha256": "9094f910509b9881e8d7882a9ad1855e39610da15a2a163088af4d1e96c8687f",
"rawBytes": 26195,
"gzipBytes": 6495,
"gzipBytes": 6497,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-BETSWzR3.js",
"houseplan-assets/houseplan-card-DFgj-WTW.js"
"houseplan-assets/backdrop-pick-Tyu0BfFF.js",
"houseplan-assets/houseplan-card-z66sNkRH.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "22e46001f1a6549cb4eb63d779ea0fbe08a609c069ed98364bbcc5d2962e352d",
"sha256": "cdd74054b0eda637b8e8184c96291b395fd558678f8e55af673931615e01b085",
"rawBytes": 1089,
"gzipBytes": 752,
"gzipBytes": 754,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-DFgj-WTW.js"
"houseplan-assets/houseplan-card-z66sNkRH.js"
],
"dynamicImports": []
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="048a8c9c9ad15757438fe100b5f2afaaa74bd2ddf749b378a7e784e69aaed2bb";import{c as e,l as o,t,cZ as s,A as a,c_ as i,c$ as l,d0 as n,E as r,b as c}from"./houseplan-card-DFgj-WTW.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="2f53621bc0d0f8ce3e04153700c0c17286b37d4dae42519acf74d6023333442a";import{c as e,l as o,t,cZ as s,A as a,c_ as i,c$ as l,d0 as n,E as r,b as c}from"./houseplan-card-z66sNkRH.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}
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="048a8c9c9ad15757438fe100b5f2afaaa74bd2ddf749b378a7e784e69aaed2bb";import{bT as t,bU as s,bV as e,bW as o,aq as a,bX as l,c0 as i,bY as h,bZ as c,b_ as n,b$ as r,A as p,b as _,cR as d,ap as u,ar as g,cS as m}from"./houseplan-card-DFgj-WTW.js";import{i as b,c as $,e as v,r as f,a as y,b as w,s as D,t as S}from"./backdrop-pick-BETSWzR3.js";const k=1e3,C=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},M="048a8c9c9ad15757438fe100b5f2afaaa74bd2ddf749b378a7e784e69aaed2bb";class F{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),c=i.settings?.custom_fill&&"object"==typeof i.settings.custom_fill?s(i.settings.custom_fill):null,n="none"===h.fill?{...c||e,a:0}:c;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:n,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:n,fillMode:"custom",customFill:{...e,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:c,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;s.value="";const o=await $(e);if("reject"===o.kind)return void this.host._showToast(this.host._t("toast.plan_formats"));if("guard"===o.kind)return void(this.host._backdropGuard=o.state);const a=await v(e,o.ext,e.name);this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:a})}_renderBackdropGuard(){return f(this.host,t=>{this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:t})},()=>{this.host._backdropGuard=null},this.host.hass)??p}_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 _`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return _`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return _`<div class="savedplans">
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="2f53621bc0d0f8ce3e04153700c0c17286b37d4dae42519acf74d6023333442a";import{bT as t,bU as s,bV as e,bW as o,aq as a,bX as l,c0 as i,bY as h,bZ as c,b_ as n,b$ as r,A as p,b as _,cR as d,ap as u,ar as g,cS as m}from"./houseplan-card-z66sNkRH.js";import{i as b,c as $,e as v,r as f,a as y,b as w,s as D,t as S}from"./backdrop-pick-Tyu0BfFF.js";const k=1e3,C=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},M="2f53621bc0d0f8ce3e04153700c0c17286b37d4dae42519acf74d6023333442a";class F{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),c=i.settings?.custom_fill&&"object"==typeof i.settings.custom_fill?s(i.settings.custom_fill):null,n="none"===h.fill?{...c||e,a:0}:c;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:n,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:n,fillMode:"custom",customFill:{...e,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:c,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;s.value="";const o=await $(e);if("reject"===o.kind)return void this.host._showToast(this.host._t("toast.plan_formats"));if("guard"===o.kind)return void(this.host._backdropGuard=o.state);const a=await v(e,o.ext,e.name);this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:a})}_renderBackdropGuard(){return f(this.host,t=>{this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:t})},()=>{this.host._backdropGuard=null},this.host.hass)??p}_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 _`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return _`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return _`<div class="savedplans">
${s.map(s=>{return _`
<div class="savedplan ${s.url===t.planUrl?"cur":""}">
<img src=${this.host._display(s.url)} alt="" loading="lazy" decoding="async" />
+1 -1
View File
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="048a8c9c9ad15757438fe100b5f2afaaa74bd2ddf749b378a7e784e69aaed2bb";try{await import("./houseplan-assets/houseplan-card-DFgj-WTW.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__="2f53621bc0d0f8ce3e04153700c0c17286b37d4dae42519acf74d6023333442a";try{await import("./houseplan-assets/houseplan-card-z66sNkRH.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)}
+5
View File
@@ -2,6 +2,11 @@
## Unreleased
- Furniture outline width now follows plan zoom like the same physical width
on lines and shapes, while independent width/depth resizing still keeps
horizontal, vertical and diagonal strokes equally thick
([#361](https://github.com/Matysh/houseplan-card/issues/361)).
- Device markers in the Background editor are now passive, translucent
landmarks: their core, ring, value capsule and badges no longer hover, open,
actuate or drag, and a drawing or furniture tool receives a press through
+5
View File
@@ -8,6 +8,11 @@
## Не выпущено
- Толщина контура мебели теперь меняется вместе с масштабом плана, как та же
физическая толщина у линий и фигур, а независимое изменение ширины и глубины
предмета по-прежнему не искажает горизонтальные, вертикальные и диагональные
штрихи ([#361](https://github.com/Matysh/houseplan-card/issues/361)).
- Маркеры устройств в редакторе подложки теперь служат только полупрозрачными
ориентирами: ядро, кольцо, капсула значения и бейджи больше не реагируют на
наведение, не открываются, не выполняют действия и не перетаскиваются, а
+6 -2
View File
@@ -77,10 +77,14 @@ cannot nest optgroups.
Each object is still one `<path>` and one erase hit path. Designer artwork
keeps its native SVG `viewBox`; the renderer applies the user's stored width
and depth with a non-uniform transform and uses
`vector-effect="non-scaling-stroke"`. Consequently:
and depth with a non-uniform transform. `vector-effect="non-scaling-stroke"`
rejects only that local width/depth distortion in the visible result; the
renderer separately applies the outer plan viewBox scale to the stroke width.
Consequently:
- resizing changes the physical object box without distorting line weight;
- camera zoom changes the visible line weight exactly like other physical
decor with the same `width_cm`;
- the user's decor colour, opacity and physical line width remain authoritative;
- `data-hp="decor"`, `data-kind="furniture"`, `data-id` and `data-symbol`
remain stable for card-mod;
+1 -1
View File
@@ -1251,7 +1251,7 @@ Power=`off`/`unavailable` подавляет даже устаревший ак
Картинка подложки интерактивна только в своём инструменте. В остальных инструментах этого редактора она показана с непрозрачностью `0.5`, а в просмотре и других редакторах — `1.0`.
Цвет контура/текста/мебели и его прозрачность задаются общим компактным полем: нажмите на образец цвета, чтобы открыть выбор цвета и прозрачности. Толщина и размер текста хранятся физически и показываются в сантиметрах или дюймах. Для прямоугольника и овала заливка имеет собственные цвет и прозрачность. Большие размеры в свойствах показываются в метрах или футах.
Цвет контура/текста/мебели и его прозрачность задаются общим компактным полем: нажмите на образец цвета, чтобы открыть выбор цвета и прозрачности. Толщина и размер текста хранятся физически и показываются в сантиметрах или дюймах. Поэтому при приближении плана контур мебели становится толще вместе с остальными физическими линиями, а при отдалении — тоньше; изменение ширины и глубины самого предмета толщину не искажает. Для прямоугольника и овала заливка имеет собственные цвет и прозрачность. Большие размеры в свойствах показываются в метрах или футах.
Параметры нового объекта показываются в общей суб-панели над холстом только
для применимого инструмента. При выборе готового объекта эта же панель
Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 343 KiB

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 334 KiB

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 319 KiB

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 317 KiB

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 134 KiB

+21 -21
View File
@@ -3,7 +3,7 @@
"fixture": "synthetic-only",
"chromium": "151.0.7922.34",
"oxipng": null,
"sourceFingerprint": "55e77e97e94985562de2bf89b0732cd429a7ab3c7a53ed62d7fa74bb80d3237e",
"sourceFingerprint": "efd6edaeba9496509727bec13d19495b799339bdfe2176c791d3abcee83f45a5",
"captureScriptSha256": "fe76c8e2e27501e9a377d7aa15dd6cba6a018d55c07deb602dbee097f0a0a5ba",
"command": "npm run build && node demo/docs/capture.mjs",
"scenarios": {
@@ -15,8 +15,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "55e77e97e94985562de2bf89b0732cd429a7ab3c7a53ed62d7fa74bb80d3237e",
"imageSha256": "0ca982495a7749907c48ebbddfd089c54385a4347f79f64e94af2c5a8d5c03eb"
"sourceSha256": "efd6edaeba9496509727bec13d19495b799339bdfe2176c791d3abcee83f45a5",
"imageSha256": "36223106c073f07d8cc3ecf8eaab37192ebb2687daba65c5c21047d0b7890de0"
},
"view-touch": {
"file": "02-view-touch.png",
@@ -26,8 +26,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "55e77e97e94985562de2bf89b0732cd429a7ab3c7a53ed62d7fa74bb80d3237e",
"imageSha256": "3be239a5a494f6776540a9c0049694ee6f9d2ba927c8bcb4b26ee5a69a228a7d"
"sourceSha256": "efd6edaeba9496509727bec13d19495b799339bdfe2176c791d3abcee83f45a5",
"imageSha256": "a4aa3af48578ee41594a9b1a6e98b4d39f3324a949a9fd9e63d5854576d5db85"
},
"space-create": {
"file": "03-space-create.png",
@@ -37,8 +37,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "55e77e97e94985562de2bf89b0732cd429a7ab3c7a53ed62d7fa74bb80d3237e",
"imageSha256": "a55b463055a565892ca139014e314a8f63fb5301c87fff2e48d7e69210fe9b6c"
"sourceSha256": "efd6edaeba9496509727bec13d19495b799339bdfe2176c791d3abcee83f45a5",
"imageSha256": "0dc024726327f60f4a9ceaf3044381691f81f1090af81d1812e870f22d9343ba"
},
"room-contour-close": {
"file": "04-room-contour-close.png",
@@ -48,8 +48,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "55e77e97e94985562de2bf89b0732cd429a7ab3c7a53ed62d7fa74bb80d3237e",
"imageSha256": "dfad9a27371cfa397a3fd8dce7005f42d24649743f5322105ee894067d3a9f16"
"sourceSha256": "efd6edaeba9496509727bec13d19495b799339bdfe2176c791d3abcee83f45a5",
"imageSha256": "44cfaa95bd51e2cd628400e28db0ad8b2f0cd904385845bd402494f3f5c0d93c"
},
"plan-context-tray": {
"file": "05-plan-context-tray.png",
@@ -59,8 +59,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "55e77e97e94985562de2bf89b0732cd429a7ab3c7a53ed62d7fa74bb80d3237e",
"imageSha256": "b75f348c632296da5c2fc142d53df613f757e1516acac315be50f5b4557729cc"
"sourceSha256": "efd6edaeba9496509727bec13d19495b799339bdfe2176c791d3abcee83f45a5",
"imageSha256": "e0662429b423fb74151b583dcc2c8635b001b637d03bbf7a6b16aec46399c3f8"
},
"device-editor": {
"file": "06-device-editor.png",
@@ -70,8 +70,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "55e77e97e94985562de2bf89b0732cd429a7ab3c7a53ed62d7fa74bb80d3237e",
"imageSha256": "8affadfce926fe2f6cf344c33f611c44fcbc79c1969f69c3a6935ff2e99c36c3"
"sourceSha256": "efd6edaeba9496509727bec13d19495b799339bdfe2176c791d3abcee83f45a5",
"imageSha256": "2f861b3e0f3e09cbe462873887fc6a4412940393d37d782bcd0be58c9e82879e"
},
"device-display-preview": {
"file": "06-device-display-preview.png",
@@ -81,8 +81,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "55e77e97e94985562de2bf89b0732cd429a7ab3c7a53ed62d7fa74bb80d3237e",
"imageSha256": "6f628abe6b89d7c4b352e6d73ac0b126526f6fdba5927bb774c5b884e484ca19"
"sourceSha256": "efd6edaeba9496509727bec13d19495b799339bdfe2176c791d3abcee83f45a5",
"imageSha256": "2cdabae1f89c3286e4fac0ce30f757ee1690b707ab8a5488748b7cd420626160"
},
"background-editor": {
"file": "07-background-editor.png",
@@ -92,8 +92,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "55e77e97e94985562de2bf89b0732cd429a7ab3c7a53ed62d7fa74bb80d3237e",
"imageSha256": "a1d1a905f1410f73231d2ea5d1dfbd8ae79c2fe8b0a79cff87ebb977ac7a3103"
"sourceSha256": "efd6edaeba9496509727bec13d19495b799339bdfe2176c791d3abcee83f45a5",
"imageSha256": "30147bb00a90eea7136b4cee30995f6e6a9217b5132f3e8d3ad7471413b1af8a"
},
"room-card": {
"file": "08-room-card.png",
@@ -103,8 +103,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "55e77e97e94985562de2bf89b0732cd429a7ab3c7a53ed62d7fa74bb80d3237e",
"imageSha256": "48a5685039a5e0de2a28190d857b193c86e2e63606e3c7a8c40fc9d2b190da4e"
"sourceSha256": "efd6edaeba9496509727bec13d19495b799339bdfe2176c791d3abcee83f45a5",
"imageSha256": "029a3e69ec647a8a370d99e6bb7f9225833c526739076022f6b52ba54bff30ea"
},
"device-info": {
"file": "09-device-info.png",
@@ -114,8 +114,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "55e77e97e94985562de2bf89b0732cd429a7ab3c7a53ed62d7fa74bb80d3237e",
"imageSha256": "29a342811aaedaebc54ba337792d60cb454a7cf4c3063b210f6c4bafe0a4d87b"
"sourceSha256": "efd6edaeba9496509727bec13d19495b799339bdfe2176c791d3abcee83f45a5",
"imageSha256": "dd492f53150b7149085daada5cce9eeae9bde9e7ea1d86679a54b3041f72f517"
}
}
}
+51 -4
View File
@@ -14,8 +14,9 @@
* generated from House Plan's own top-view SVG pack. A small compatibility
* set that is absent from the pack stays expressed as unit-box primitives.
* Both forms resolve to one path plus its native coordinate box; the renderer
* scales that box to the stored real-world size and keeps the configured decor
* stroke stable with `vector-effect="non-scaling-stroke"`.
* scales that box to the stored real-world size. The renderer keeps the
* configured decor stroke stable through that local non-uniform transform,
* while `furniturePlanScreenScale` restores the outer physical plan zoom.
*
* THE CONVENTION EVERY SYMBOL OBEYS:
* - x points right and y points DOWN (SVG); generated art keeps its native
@@ -357,8 +358,8 @@ function primitivePathD(s: FurnitureSymbol, w: number, h: number): string {
}
/** Native one-path artwork and its coordinate box. The renderer scales this
* box non-uniformly to the user's stored dimensions and keeps the decor stroke
* physical with `vector-effect=non-scaling-stroke`. */
* box non-uniformly to the user's stored dimensions; the furniture stroke
* helpers below separate that local scale from the outer plan camera. */
export function furnitureGraphic(id: string): FurnitureGraphic | null {
const symbol = furnitureSymbol(id);
if (!symbol) return null;
@@ -375,6 +376,52 @@ export function furniturePathD(id: string, w: number, h: number): string {
return symbol.art?.d || primitivePathD(symbol, w, h);
}
/**
* Uniform plan-user-unit -> CSS-pixel scale of the SVG viewport.
*
* Furniture artwork uses a local non-uniform transform for independent width
* and depth. `vector-effect=non-scaling-stroke` correctly rejects that local
* transform, but it also rejects the outer SVG viewBox camera. Supplying this
* one outer scale as the path's stroke width restores the physical zoom while
* keeping horizontal, vertical and diagonal artwork strokes equally thick.
*
* Invalid/zero layout is transient during first mount. Returning 1 is the
* finite old-behaviour fallback; ResizeObserver requests the measured render
* before the boot veil reveals the plan.
*/
export function furniturePlanScreenScale(
viewportWidth: unknown,
viewportHeight: unknown,
viewBoxWidth: unknown,
viewBoxHeight: unknown,
): number {
const viewportW = Number(viewportWidth);
const viewportH = Number(viewportHeight);
const viewW = Number(viewBoxWidth);
const viewH = Number(viewBoxHeight);
if (![viewportW, viewportH, viewW, viewH].every((value) => Number.isFinite(value) && value > 0))
return 1;
const scale = Math.min(viewportW / viewW, viewportH / viewH);
return Number.isFinite(scale) && scale > 0 ? scale : 1;
}
/** Physical furniture stroke expressed in CSS pixels for a non-scaling path. */
export function furnitureStrokePx(
strokeUnits: unknown,
planScreenScale: unknown,
fallbackStrokeUnits = 1,
): number {
const rawStroke = Number(strokeUnits);
const fallback = Number(fallbackStrokeUnits);
const stroke = Number.isFinite(rawStroke) && rawStroke > 0
? rawStroke
: Number.isFinite(fallback) && fallback > 0 ? fallback : 1;
const rawScale = Number(planScreenScale);
const scale = Number.isFinite(rawScale) && rawScale > 0 ? rawScale : 1;
const result = stroke * scale;
return Number.isFinite(result) && result > 0 ? result : stroke;
}
// ------------------------------ the wall magnet -----------------------------
/** How far from a wall the magnet still reaches, in GRID CELLS. Six cells is
+24 -6
View File
@@ -63,6 +63,7 @@ import { dayCycleStageVars, renderDayCycleEnvironment } from './day-cycle-render
import {
FURNITURE_GROUPS, furnitureOfGroup, furnitureSymbol, furnitureDefaultCm,
furnitureGraphic, furnitureCorners, snapFurnitureToWall,
furniturePlanScreenScale, furnitureStrokePx,
cmToNorm, clampFurnSize, clampFurnCm, FURN_WALL_CELLS,
type FurnitureGroup, type FurniturePlacement,
} from './furniture';
@@ -8061,7 +8062,9 @@ export class HouseplanCard extends LitElement {
/** One real furniture path, in the same decor composition group as saved
* shapes. It paints after them (so it is visible) and the whole decor layer
* still paints below physical walls (#359). */
private _renderFurniturePlacementPreview(): TemplateResult | typeof nothing {
private _renderFurniturePlacementPreview(
furnitureScreenScale: number,
): TemplateResult | typeof nothing {
const placement = this._furniturePreviewPlacement;
if (!placement) return nothing;
const art = furnitureGraphic(placement.symbol);
@@ -8072,10 +8075,14 @@ export class HouseplanCard extends LitElement {
const transform = `${placement.angle ? `rotate(${placement.angle} ${cx} ${cy}) ` : ''}`
+ `translate(${placement.x * W} ${placement.y * H}) scale(${W2 / art.viewW} ${H2 / art.viewH})`;
const style = this._decorStyle;
const strokeWidth = furnitureStrokePx(
decorCmToUnits(style.widthCm, this._cellCm, this._gridPitch),
furnitureScreenScale,
);
return svg`<path class="furniture-placement-preview dfurn"
data-symbol=${placement.symbol} d=${art.d} transform=${transform}
stroke=${style.color} stroke-opacity=${style.opacity}
stroke-width=${decorCmToUnits(style.widthCm, this._cellCm, this._gridPitch)}
stroke-width=${strokeWidth}
fill="none" stroke-linecap="round" stroke-linejoin="round"
vector-effect="non-scaling-stroke" aria-hidden="true" pointer-events="none"></path>`;
}
@@ -8084,6 +8091,15 @@ export class HouseplanCard extends LitElement {
const W = NORM_W, H = this._decorH;
const editing = this._mode === 'decor';
const erasing = editing && this._decorTool === 'erase';
// One measured outer viewBox scale for the whole layer. Furniture paths
// keep `non-scaling-stroke` only to reject their own independent width /
// depth transform; multiplying by this factor restores the same physical
// camera zoom ordinary decor gets from the plan SVG (#361).
const stage = this._stageEl;
const planView = this._viewOr(this._baseVb());
const furnitureScreenScale = furniturePlanScreenScale(
stage?.clientWidth, stage?.clientHeight, planView.w, planView.h,
);
const shapes = this._decorList.map((sh) => {
const cls = 'dshape' + (editing && this._decorSel === sh.id ? ' dsel' : '');
const style = this._decorResolvedStyle(sh);
@@ -8135,8 +8151,9 @@ export class HouseplanCard extends LitElement {
}
if (sh.kind === 'furniture') {
// One path per piece. Designer art keeps its native viewBox and is
// scaled to the user's stored box; vector-effect keeps the configured
// decor stroke physical under non-uniform resizing.
// scaled to the user's stored box; vector-effect rejects that local
// non-uniform transform while furnitureScreenScale restores the outer
// physical plan zoom.
// An unknown symbol renders as nothing: a plan from a newer card must
// open in an older one, not break it.
const W2 = sh.w * W, H2 = sh.h * H;
@@ -8147,7 +8164,8 @@ export class HouseplanCard extends LitElement {
const tr = `${ang ? `rotate(${ang} ${cx} ${cy}) ` : ''}translate(${sh.x * W} ${sh.y * H}) scale(${W2 / art.viewW} ${H2 / art.viewH})`;
return svg`<path class="${cls} dfurn" data-hp="decor" data-id="${sh.id}"
data-kind="${sh.kind}" data-symbol="${sh.symbol}" d="${art.d}" transform=${tr}
stroke="${style.color}" stroke-opacity="${style.opacity}" stroke-width="${strokeWidth}" fill="none"
stroke="${style.color}" stroke-opacity="${style.opacity}"
stroke-width="${furnitureStrokePx(strokeWidth, furnitureScreenScale)}" fill="none"
stroke-linecap="round" stroke-linejoin="round" vector-effect="non-scaling-stroke"
@pointerdown=${down} @dblclick=${dbl}></path>
${erasing ? svg`<path class="dshape derasehit" data-hp="decor" data-id="${sh.id}"
@@ -8199,7 +8217,7 @@ export class HouseplanCard extends LitElement {
stroke="${st.color}" stroke-opacity="${st.opacity}" stroke-width="${sw}" fill="${st.fill ? st.fillColor : 'none'}" fill-opacity="${st.fill ? st.fillOpacity : 0}"></ellipse>`;
}
}
return svg`<g class="decorlayer">${shapes}${draft}${this._renderFurniturePlacementPreview()}</g>` as unknown as TemplateResult;
return svg`<g class="decorlayer">${shapes}${draft}${this._renderFurniturePlacementPreview(furnitureScreenScale)}</g>` as unknown as TemplateResult;
}
// ================= shared editor secondary surface =================
+46
View File
@@ -0,0 +1,46 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
const card = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
test('#361 saved furniture and placement preview share the physical screen resolver', () => {
const preview = card.slice(
card.indexOf('private _renderFurniturePlacementPreview('),
card.indexOf('private _renderDecorLayer('),
);
const layer = card.slice(
card.indexOf('private _renderDecorLayer('),
card.indexOf('// ================= shared editor secondary surface'),
);
assert.match(preview, /furnitureStrokePx\([\s\S]*decorCmToUnits\([\s\S]*furnitureScreenScale/);
assert.match(preview, /stroke-width=\$\{strokeWidth\}/);
assert.doesNotMatch(
preview,
/stroke-width=\$\{decorCmToUnits/,
'preview must not bypass the camera-aware furniture resolver',
);
assert.match(layer, /const furnitureScreenScale = furniturePlanScreenScale\(/);
assert.match(layer, /stroke-width="\$\{furnitureStrokePx\(strokeWidth, furnitureScreenScale\)\}"/);
assert.match(layer, /_renderFurniturePlacementPreview\(furnitureScreenScale\)/);
assert.equal(
(layer.match(/const furnitureScreenScale = furniturePlanScreenScale\(/g) || []).length,
1,
'the viewport scale is resolved once for the whole decor layer',
);
});
test('#361 keeps local anisotropic protection and screen-oriented erase hit separate', () => {
const layer = card.slice(
card.indexOf('private _renderDecorLayer('),
card.indexOf('// ================= shared editor secondary surface'),
);
assert.ok(
(layer.match(/vector-effect="non-scaling-stroke"/g) || []).length >= 2,
'saved and erase paths must keep rejecting the local artwork scale',
);
assert.match(layer, /class="dshape derasehit"[\s\S]*vector-effect="non-scaling-stroke"/);
assert.match(layer, /data-hp="decor"[\s\S]*data-kind="\$\{sh\.kind\}"[\s\S]*data-symbol="\$\{sh\.symbol\}"/);
});
+37 -1
View File
@@ -6,7 +6,7 @@ import assert from 'node:assert/strict';
import {
FURNITURE, FURNITURE_GROUPS, furnitureSymbol, furnitureOfGroup,
furnitureDefaultCm, furniturePathD, furnitureGraphic, furnitureCorners, furnitureResize,
snapFurnitureToWall, resolveFurniturePlacement,
snapFurnitureToWall, resolveFurniturePlacement, furniturePlanScreenScale, furnitureStrokePx,
cmToNorm, normToCm, clampFurnSize, clampFurnCm,
FURN_MIN_N, FURN_MIN_CM, FURN_MAX_CM, FURN_WALL_CELLS,
} from '../test-build/furniture.js';
@@ -129,6 +129,42 @@ test('every symbol exposes one finite path and a positive native coordinate box'
}
});
test('furniture stroke follows the outer plan viewBox camera like physical decor', () => {
// 1000 plan units fitted into 500 CSS px: the same 3-unit physical line is
// 1.5 px on screen. Halving the viewBox is a 2x camera zoom and doubles it.
const fit = furniturePlanScreenScale(500, 300, 1000, 600);
const zoom2 = furniturePlanScreenScale(500, 300, 500, 300);
closeTo(fit, 0.5);
closeTo(zoom2, 1);
closeTo(furnitureStrokePx(3, fit), 1.5);
closeTo(furnitureStrokePx(3, zoom2), 3);
closeTo(furnitureStrokePx(3, zoom2) / furnitureStrokePx(3, fit), 2);
});
test('furniture stroke uses the meet scale and stays independent of the local artwork box', () => {
// Width alone would say 0.8; xMidYMid meet is constrained by height at 0.5.
const letterboxed = furniturePlanScreenScale(800, 300, 1000, 600);
closeTo(letterboxed, 0.5);
const stroke = furnitureStrokePx(4, letterboxed);
for (const symbol of ['sofa', 'fridge']) {
assert.ok(furnitureGraphic(symbol), `${symbol}: fixture must render`);
// Designer native viewBox and retained unit-box art receive the same
// physical stroke even if their independent width/depth scales differ.
closeTo(furnitureStrokePx(4, letterboxed), stroke);
}
});
test('furniture stroke layout fallbacks are finite and recover on measured layout', () => {
for (const metrics of [
[0, 300, 1000, 600], [500, NaN, 1000, 600], [500, 300, 0, 600],
[undefined, undefined, undefined, undefined],
]) assert.equal(furniturePlanScreenScale(...metrics), 1);
assert.equal(furnitureStrokePx(NaN, NaN, 2.5), 2.5);
assert.equal(furnitureStrokePx(3, 0), 3);
assert.ok(Number.isFinite(furnitureStrokePx(Infinity, Infinity)));
closeTo(furnitureStrokePx(3, furniturePlanScreenScale(500, 300, 500, 300)), 3);
});
// ------------------------------ the wall magnet -----------------------------
// one 400x300 room, normalised like the config stores it, in render units