feat: preview furniture placement

Issue: #359
User-Visible: yes
This commit is contained in:
Matysh
2026-08-29 00:34:06 +03:00
parent 3c281bb12c
commit 8b66d67d3d
29 changed files with 942 additions and 493 deletions
@@ -1,111 +1,111 @@
{
"schema": 1,
"fingerprint": "817110bad40d29ee0aa8e4ef2dadfda9e310dfa6346ddb5d2725ecafb359f073",
"fingerprint": "e6c51f2467ae4e22a792e536ae954291ab5a46915e728b216c8c45bc6816b209",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-BrUCyMoE.js",
"houseplan-assets/houseplan-card-DI__VUPO.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 271841,
"initialViewGzipBytes": 272469,
"lazyFiles": [
"houseplan-assets/de-CndLXoON.js",
"houseplan-assets/editor-H80bjnWS.js",
"houseplan-assets/houseplan-editor-runtime-LLPkWHY0.js",
"houseplan-assets/houseplan-onboarding-runtime-BkWWzfSR.js",
"houseplan-assets/space-deletion-CLA5ezlL.js"
"houseplan-assets/de-gTJXGK7s.js",
"houseplan-assets/editor-LGCA8WjH.js",
"houseplan-assets/houseplan-editor-runtime-Cu2NFP8N.js",
"houseplan-assets/houseplan-onboarding-runtime-B4PXZ4um.js",
"houseplan-assets/space-deletion-Do1Uuvoz.js"
],
"lazyGzipBytes": 165904,
"lazyGzipBytes": 166314,
"lazyEditorFiles": [
"houseplan-assets/editor-H80bjnWS.js",
"houseplan-assets/houseplan-editor-runtime-LLPkWHY0.js",
"houseplan-assets/space-deletion-CLA5ezlL.js"
"houseplan-assets/editor-LGCA8WjH.js",
"houseplan-assets/houseplan-editor-runtime-Cu2NFP8N.js",
"houseplan-assets/space-deletion-Do1Uuvoz.js"
],
"lazyEditorGzipBytes": 136806,
"lazyEditorGzipBytes": 137196,
"lazyOnboardingFiles": [
"houseplan-assets/houseplan-onboarding-runtime-BkWWzfSR.js",
"houseplan-assets/space-deletion-CLA5ezlL.js"
"houseplan-assets/houseplan-onboarding-runtime-B4PXZ4um.js",
"houseplan-assets/space-deletion-Do1Uuvoz.js"
],
"lazyOnboardingGzipBytes": 7193,
"lazyOnboardingGzipBytes": 7204,
"lazyLocaleFiles": [
"houseplan-assets/de-CndLXoON.js"
"houseplan-assets/de-gTJXGK7s.js"
],
"lazyLocaleGzipBytes": 22454,
"lazyLocaleGzipBytes": 22468,
"files": [
{
"path": "houseplan-assets/de-CndLXoON.js",
"sha256": "c3fb30be483664a92a555e2a916d2d6201a5d950d77385a34869445625732be1",
"path": "houseplan-assets/de-gTJXGK7s.js",
"sha256": "2ed2a8aad10d9eaa16fe3fe2e591c2d736d948b2fd35aca01fcffb33264e1284",
"rawBytes": 73919,
"gzipBytes": 22454,
"gzipBytes": 22468,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-H80bjnWS.js",
"sha256": "bcd9f271360a94dfbf63d5bcdf3c965aed1e1b17d0c543ac0b554d5527b90250",
"path": "houseplan-assets/editor-LGCA8WjH.js",
"sha256": "0f4ce2555703ec01ee188cf84badfdee661587e849c1b604ec88354c3f94894f",
"rawBytes": 3831,
"gzipBytes": 1580,
"gzipBytes": 1585,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BrUCyMoE.js"
"houseplan-assets/houseplan-card-DI__VUPO.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-BrUCyMoE.js",
"sha256": "bdcf3f7e9306428b2583f1e662541bebebcf2f106b48a53df117c6c9c54c5cde",
"rawBytes": 958245,
"gzipBytes": 271090,
"path": "houseplan-assets/houseplan-card-DI__VUPO.js",
"sha256": "35a537b289b388f34a5f6efc01a95254207be9880fa9fd931723ccfa258ad602",
"rawBytes": 960284,
"gzipBytes": 271715,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-CndLXoON.js",
"houseplan-assets/editor-H80bjnWS.js",
"houseplan-assets/houseplan-editor-runtime-LLPkWHY0.js",
"houseplan-assets/houseplan-onboarding-runtime-BkWWzfSR.js"
"houseplan-assets/de-gTJXGK7s.js",
"houseplan-assets/editor-LGCA8WjH.js",
"houseplan-assets/houseplan-editor-runtime-Cu2NFP8N.js",
"houseplan-assets/houseplan-onboarding-runtime-B4PXZ4um.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-LLPkWHY0.js",
"sha256": "e49282b51d104baff861b3c21261e66dcbfd58807e0db444b04f1f171499e24d",
"rawBytes": 520292,
"gzipBytes": 134677,
"path": "houseplan-assets/houseplan-editor-runtime-Cu2NFP8N.js",
"sha256": "4eb656ee069026534dace297817908e97d0e82e63ba1059d0c3d6c283d913344",
"rawBytes": 522188,
"gzipBytes": 135057,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BrUCyMoE.js",
"houseplan-assets/space-deletion-CLA5ezlL.js"
"houseplan-assets/houseplan-card-DI__VUPO.js",
"houseplan-assets/space-deletion-Do1Uuvoz.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-BkWWzfSR.js",
"sha256": "5446043fbf2a383f84df3751b1fd97ecac72d8bcf3c254da80ad4176538c91da",
"path": "houseplan-assets/houseplan-onboarding-runtime-B4PXZ4um.js",
"sha256": "73ad6471b2395b982d233754598fcb6fb296c29b01a188cce724c2861e044187",
"rawBytes": 26356,
"gzipBytes": 6644,
"gzipBytes": 6650,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BrUCyMoE.js",
"houseplan-assets/space-deletion-CLA5ezlL.js"
"houseplan-assets/houseplan-card-DI__VUPO.js",
"houseplan-assets/space-deletion-Do1Uuvoz.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/space-deletion-CLA5ezlL.js",
"sha256": "ec0519abd52ab649e2ed8f5744cc8284d96d8a83f3c9517df7f5070be63a66e9",
"rawBytes": 906,
"gzipBytes": 549,
"path": "houseplan-assets/space-deletion-Do1Uuvoz.js",
"sha256": "258cd7b7338ca746122d6afcfc63f81d34be23a7bd31f1599ed8d45ec0daa5b6",
"rawBytes": 911,
"gzipBytes": 554,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "9bb7a0cb60837b9a006d1c0d125c820ab083b270a9c6130147cd5c8c8b47cff1",
"sha256": "031ef9710a85647066f82b62cd7a294c752493c0391cf59b1688380f7cac2a18",
"rawBytes": 1089,
"gzipBytes": 751,
"gzipBytes": 754,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-BrUCyMoE.js"
"houseplan-assets/houseplan-card-DI__VUPO.js"
],
"dynamicImports": []
}
@@ -1,14 +1,14 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="817110bad40d29ee0aa8e4ef2dadfda9e310dfa6346ddb5d2725ecafb359f073";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-BrUCyMoE.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__="e6c51f2467ae4e22a792e536ae954291ab5a46915e728b216c8c45bc6816b209";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-DI__VUPO.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),u=e=>c`<ha-form
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
.computeLabel=${e=>h[e.name]||e.name}
@value-changed=${this._valueChanged}
></ha-form>`;return c`
${d(f.slice(0,3))}
${_?c`<div class="default-floor-error" role="alert"
${u(_.slice(0,3))}
${f?c`<div class="default-floor-error" role="alert"
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
${t(s,"editor.default_floor_missing",{id:_})}
${t(s,"editor.default_floor_missing",{id:f})}
</div>`:a}
${d(f.slice(3))}
${u(_.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__="817110bad40d29ee0aa8e4ef2dadfda9e310dfa6346ddb5d2725ecafb359f073";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-BrUCyMoE.js";import{i as b,c as $,a as v,s as f,t as y}from"./space-deletion-CLA5ezlL.js";const w=1e3,D=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},S="817110bad40d29ee0aa8e4ef2dadfda9e310dfa6346ddb5d2725ecafb359f073";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__="e6c51f2467ae4e22a792e536ae954291ab5a46915e728b216c8c45bc6816b209";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 n,b_ as c,b$ as r,b as p,A as _,cR as d,ap as u,ar as g,cS as m}from"./houseplan-card-DI__VUPO.js";import{i as b,c as $,a as v,s as y,t as f}from"./space-deletion-Do1Uuvoz.js";const w=1e3,D=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},S="e6c51f2467ae4e22a792e536ae954291ab5a46915e728b216c8c45bc6816b209";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__="817110bad40d29ee0aa8e4ef2dadfda9e310
<label>${this.host._t("space.plan_label")}</label>
<label class="srcrow">
<input type="radio" name="plansrc" .checked=${"file"===t.source}
@change=${()=>this.host._spaceDialog=f(t,"file")} />
@change=${()=>this.host._spaceDialog=y(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__="817110bad40d29ee0aa8e4ef2dadfda9e310
${t.pickSaved?this._renderServerPlans(t):_}`:_}
<label class="srcrow">
<input type="radio" name="plansrc" .checked=${"draw"===t.source}
@change=${()=>this.host._spaceDialog=f(t,"draw")} />
@change=${()=>this.host._spaceDialog=y(t,"draw")} />
<span>${this.host._t("space.source_draw")}</span>
</label>
@@ -85,7 +85,7 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="817110bad40d29ee0aa8e4ef2dadfda9e310
<label class="dispsection">${this.host._t("space.display_section")}</label>
<label class="srcrow">
${this._boolInput(t.showBorders,s=>{this.host._spaceDialog=y(t,"showBorders",s)})}
${this._boolInput(t.showBorders,s=>{this.host._spaceDialog=f(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__="817110bad40d29ee0aa8e4ef2dadfda9e310
</select>
<div class="rhint">${this.host._t("space.zero_wall_help")}</div>
<label class="srcrow">
${this._boolInput(t.showNames,s=>{this.host._spaceDialog=y(t,"showNames",s)})}
${this._boolInput(t.showNames,s=>{this.host._spaceDialog=f(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__="817110bad40d29ee0aa8e4ef2dadfda9e310dfa6346ddb5d2725ecafb359f073";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)),a=[...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:a,count:a.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="e6c51f2467ae4e22a792e536ae954291ab5a46915e728b216c8c45bc6816b209";export{t as a,e as c,o as i,r as s,s as t};
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="817110bad40d29ee0aa8e4ef2dadfda9e310dfa6346ddb5d2725ecafb359f073";try{await import("./houseplan-assets/houseplan-card-BrUCyMoE.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__="e6c51f2467ae4e22a792e536ae954291ab5a46915e728b216c8c45bc6816b209";try{await import("./houseplan-assets/houseplan-card-DI__VUPO.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)}
+35
View File
@@ -1274,6 +1274,41 @@ export async function prepareGoldenScenario(page, scenario) {
if (!palette.querySelector(selector))
throw new Error(`golden furniture palette level is empty: ${scenario.id}`);
}
if (scenario.furniturePlacementPreview) {
const { symbol, widthCm, depthCm, pointer, free = false } = scenario.furniturePlacementPreview;
if (typeof symbol !== 'string' || !(widthCm > 0) || !(depthCm > 0)
|| !Array.isArray(pointer) || pointer.length !== 2 || !pointer.every(Number.isFinite)) {
throw new Error(`invalid golden furniturePlacementPreview: ${scenario.id}`);
}
card._decorSel = null;
card._decorTool = 'furniture';
card._furnCategory = null;
card._furnPalette = { symbol, w: widthCm, h: depthCm };
card.requestUpdate();
await card.updateComplete;
await frame();
const svgRoot = card.renderRoot.querySelector('.stage svg');
const stageEl = card.renderRoot.querySelector('.stage');
const screen = new DOMPoint(pointer[0] * 1000, pointer[1] * card._spaceH)
.matrixTransform(svgRoot.getScreenCTM());
stageEl.dispatchEvent(new PointerEvent('pointermove', {
bubbles: true, composed: true, pointerId: 993, pointerType: 'mouse',
clientX: screen.x, clientY: screen.y, shiftKey: free,
}));
await card.updateComplete;
await frame();
const preview = card.renderRoot.querySelector('.furniture-placement-preview');
const wall = card.renderRoot.querySelector('[data-hp="wall"]');
const previewBeforeWall = !!wall
&& !!(preview?.compareDocumentPosition(wall) & Node.DOCUMENT_POSITION_FOLLOWING);
if (!preview || preview.getAttribute('data-symbol') !== symbol
|| preview.getAttribute('aria-hidden') !== 'true'
|| getComputedStyle(preview).pointerEvents !== 'none'
|| Math.abs(Number(getComputedStyle(preview).opacity) - 0.55) > 1e-6
|| !preview.closest('.decorlayer') || !previewBeforeWall) {
throw new Error(`golden furniture placement preview contract is incomplete: ${scenario.id}`);
}
}
if (scenario.hoverRoom) {
const room = card._spaceModel().rooms.find((item) => item.id === scenario.hoverRoom);
if (!room) throw new Error(`golden hover room missing: ${scenario.hoverRoom}`);
+3
View File
@@ -418,6 +418,9 @@ export const GOLDEN_SCENARIOS = Object.freeze([
{ id: 'furniture-variants-light', fixture: 'visual', space: 'golden-geometry', mode: 'decor',
furniturePalette: 'variants', furnitureCategory: 'coffee_table', language: 'ru', theme: 'light',
viewport: { width: 900, height: 820 }, ...page },
{ id: 'furniture-placement-preview-light', fixture: 'visual', space: 'golden-geometry', mode: 'decor',
furniturePlacementPreview: { symbol: 'sofa', widthCm: 180, depthCm: 90, pointer: [0.35, 0.90] },
language: 'ru', theme: 'light', viewport: { width: 1000, height: 900 }, ...stage },
{ id: 'furniture-plan-art-dark', fixture: 'visual', space: 'golden-geometry', mode: 'view',
decorOverride: [
...decorLayerFixture.filter((shape) => shape.kind !== 'furniture'),
+115 -18
View File
@@ -4,12 +4,14 @@
// 2) выбранный символ ставится кликом по плану в РЕАЛЬНОМ размере — через
// cell_cm пространства, и сразу оказывается выделенным в «выбрать»;
// 3) поля ширины/глубины в палитре меняют размер ДО размещения;
// 4) магнит к стене: предмет прижимается к ближайшей стене и доворачивается
// по ней; Shift не отключает обязательную привязку (docs/CANVAS.md §9.4);
// 5) у выделенного предмета та же рамка, что у текстового блока, — угловые
// 4) мышь показывает точный полупрозрачный будущий символ без записи в
// конфиг; touch/pen не рисуют hover и сохраняют только чистый tap;
// 5) магнит к стене: предмет прижимается к ближайшей стене и доворачивается
// по ней; Shift временно обходит магнит, сохраняя grid snap;
// 6) у выделенного предмета та же рамка, что у текстового блока, — угловые
// ручки (НЕзависимые ширина и глубина) и ручка поворота с шагом 5°;
// 6) во время растягивания угла показываются живые плашки размеров;
// 7) фигура пишется в конфиг и переживает пересборку, хуки card-mod на месте.
// 7) во время растягивания угла показываются живые плашки размеров;
// 8) фигура пишется в конфиг и переживает пересборку, хуки card-mod на месте.
// ПАДАЕТ на сборке до этой задачи: инструмента «Мебель» нет, палитры нет,
// kind:'furniture' не рисуется, магнита и плашек не существует. Смок при этом
// null-safe — он показывает СПИСОК провалов, а не одно исключение.
@@ -59,6 +61,16 @@ const res = await page.evaluate(async () => {
await c.updateComplete;
out.toolIsSelected = c._decorTool === 'furniture';
const pal = () => sr().querySelector('.furnpalette');
const openFurniture = async () => {
if (c._decorTool !== 'furniture') {
c._editorSecondary.openPalette();
c._decorTool = 'furniture';
c._furnPalette = null;
c._furnCategory = null;
c.requestUpdate();
await c.updateComplete;
}
};
out.paletteOpens = !!pal();
const groups = [...(pal()?.querySelectorAll('.furngroup') || [])]
.map((g) => g.getAttribute('data-group'));
@@ -126,9 +138,67 @@ const res = await page.evaluate(async () => {
out.fieldsShowMetres = +(wIn()?.value ?? NaN) === 1.8 && +(hIn()?.value ?? NaN) === 0.9;
out.selectedItemIsMarked = !!item('sofa')?.classList.contains('on');
// ================= 3. размещение в реальном размере ======================
// ================= 3. точное превью без мутации ==========================
const ghost = () => sr().querySelector('.furniture-placement-preview');
const beforePreview = {
decor: c._decorList.length,
epoch: c._cfgEpoch,
undo: c._geometryHistory.undoName,
redo: c._geometryHistory.redoName,
};
ev('pointermove', stageEl(), 300, 300, { pointerType: 'mouse' });
await c.updateComplete;
const firstPreview = c._furniturePreviewPlacement
? JSON.parse(JSON.stringify(c._furniturePreviewPlacement)) : null;
const firstTransform = ghost()?.getAttribute('transform');
out.mouseHoverShowsRealSymbol = ghost()?.getAttribute('data-symbol') === 'sofa'
&& (ghost()?.getAttribute('d') || '').length > 20;
out.previewIsTransientAndInert = ghost()?.getAttribute('aria-hidden') === 'true'
&& getComputedStyle(ghost()).pointerEvents === 'none'
&& near(Number(getComputedStyle(ghost()).opacity), 0.55, 1e-6);
out.previewDoesNotMutateConfigOrHistory = c._decorList.length === beforePreview.decor
&& c._cfgEpoch === beforePreview.epoch
&& c._geometryHistory.undoName === beforePreview.undo
&& c._geometryHistory.redoName === beforePreview.redo;
if (wIn()) {
wIn().value = '1.85';
wIn().dispatchEvent(new Event('input', { bubbles: true }));
}
await c.updateComplete;
out.sizeUpdatesPreviewWithoutPointerMove = c._furniturePreviewPlacement?.w > firstPreview?.w
&& ghost()?.getAttribute('transform') !== firstTransform;
if (wIn()) {
wIn().value = '1.8';
wIn().dispatchEvent(new Event('input', { bubbles: true }));
}
await c.updateComplete;
stageEl().dispatchEvent(new PointerEvent('pointerleave', {
bubbles: true, composed: true, pointerId: 11, pointerType: 'mouse',
}));
await c.updateComplete;
out.pointerLeaveClearsPreview = !ghost() && c._furnPreviewInput === null;
// Unknown/stale symbols fail dark: neither an invisible ghost nor a decor
// record is allowed. Restoring a valid selection resumes the same session.
c._furnPalette = { symbol: 'future_unknown_symbol', w: 180, h: 90 };
await c.updateComplete;
ev('pointermove', stageEl(), 300, 300, { pointerType: 'mouse' });
await c.updateComplete;
const beforeUnknown = c._decorList.length;
ev('pointerdown', stageEl(), 300, 300, { pointerType: 'mouse' });
await c.updateComplete;
out.unknownSymbolFailsDark = !ghost() && c._decorList.length === beforeUnknown
&& c._decorTool === 'furniture';
c._furnPalette = { symbol: 'sofa', w: 180, h: 90 };
await c.updateComplete;
// ================= 4. размещение в реальном размере ======================
// середина комнаты r1 (40..550 × 140..580) — до стен дальше порога магнита
ev('pointerdown', stageEl(), 300, 300);
ev('pointermove', stageEl(), 300, 300, { pointerType: 'mouse' });
await c.updateComplete;
const committedPreview = c._furniturePreviewPlacement
? JSON.parse(JSON.stringify(c._furniturePreviewPlacement)) : null;
ev('pointerdown', stageEl(), 300, 300, { pointerType: 'mouse' });
await c.updateComplete;
const sofa = c._decorList.find((s) => s.kind === 'furniture');
const sofaId = sofa?.id ?? null;
@@ -140,6 +210,10 @@ const res = await page.evaluate(async () => {
&& near((sofa.x + sofa.w / 2) * 1000, 300, 1e-6)
&& near((sofa.y + sofa.h / 2) * 1000, 300, 1e-6);
out.noAngleWhenStraight = !!sofa && sofa.angle === undefined;
out.previewAndCommitAreIdentical = !!sofa && !!committedPreview
&& ['x', 'y', 'w', 'h'].every((k) => near(sofa[k], committedPreview[k], 1e-12))
&& (sofa.angle || 0) === committedPreview.angle;
out.previewClearsAfterCommit = !ghost() && c._furnPreviewInput === null;
out.selectedRightAway = !!sofaId && c._decorSel === sofaId;
out.toolWentBackToSelect = c._decorTool === 'select';
out.paletteDisarmed = c._furnPalette === null;
@@ -154,8 +228,8 @@ const res = await page.evaluate(async () => {
&& /scale\(/.test(attr(sofaId, 'transform') || '')
&& attr(sofaId, 'vector-effect') === 'non-scaling-stroke';
// ================= 4. свой размер до размещения ==========================
c._decorTool = 'furniture'; await c.updateComplete;
// ================= 5. свой размер до размещения ==========================
await openFurniture();
category('toilet')?.click(); await c.updateComplete;
pick('toilet'); await c.updateComplete;
if (wIn()) {
@@ -170,8 +244,8 @@ const res = await page.evaluate(async () => {
out.typedSizeIsUsed = near((wc?.w ?? NaN) * 1000, cmToUnits(50), 1e-6)
&& near((wc?.h ?? NaN) * 1000, cmToUnits(70), 1e-6);
// ================= 5. магнит к стене =====================================
c._decorTool = 'furniture'; await c.updateComplete;
// ================= 6. магнит к стене =====================================
await openFurniture();
category('bed')?.click(); await c.updateComplete;
pick('bed_double'); await c.updateComplete;
ev('pointerdown', stageEl(), 300, 150); // 10 единиц от стены y=140
@@ -184,19 +258,42 @@ const res = await page.evaluate(async () => {
out.wallMagnetQuantisesAlongTheWall = !!bed
&& Math.abs((bed.x + bed.w / 2) * 1000 - 300) <= PITCH + 1e-6;
// …Shift больше не отключает магнит
c._decorTool = 'furniture'; await c.updateComplete;
// Shift обходит магнит, но оставляет обычную привязку декора к сетке.
await openFurniture();
category('bed')?.click(); await c.updateComplete;
pick('bed_single'); await c.updateComplete;
ev('pointerdown', stageEl(), 301.7, 151.3, { shiftKey: true });
await c.updateComplete;
const free = c._decorList.find((s) => s.symbol === 'bed_single');
out.shiftKeepsTheMagnet = !!free
out.shiftBypassesTheWallMagnet = !!free
&& !near((free.y + free.h / 2) * 1000, 151.3, 1e-6)
&& free.angle === undefined;
// Coarse pointers have no hover. A cancelled/dragged/second-contact gesture
// must not become an accidental furniture save. Keep this after the palette
// click checks because these synthetic pointer streams deliberately have no
// browser-generated compatibility click tail.
await openFurniture();
category('chair')?.click(); await c.updateComplete;
pick('chair'); await c.updateComplete;
const beforeTouch = c._decorList.length;
ev('pointerdown', stageEl(), 330, 330, { pointerType: 'touch', pointerId: 21 });
ev('pointermove', stageEl(), 350, 350, { pointerType: 'touch', pointerId: 21 });
ev('pointerup', stageEl(), 350, 350, { pointerType: 'touch', pointerId: 21 });
ev('pointerdown', stageEl(), 330, 330, { pointerType: 'touch', pointerId: 22 });
ev('pointercancel', stageEl(), 330, 330, { pointerType: 'touch', pointerId: 22 });
ev('pointerdown', stageEl(), 330, 330, { pointerType: 'touch', pointerId: 23 });
ev('pointerdown', stageEl(), 340, 340, { pointerType: 'touch', pointerId: 24, isPrimary: false });
ev('pointerup', stageEl(), 330, 330, { pointerType: 'touch', pointerId: 23 });
ev('pointerup', stageEl(), 340, 340, { pointerType: 'touch', pointerId: 24, isPrimary: false });
await c.updateComplete;
out.touchCancelMoveAndSecondContactDoNotSave = c._decorList.length === beforeTouch
&& !ghost() && c._furnTouchPending === null;
// …магнит работает и при ПЕРЕТАСКИВАНИИ: тянем диван к левой стене (x=40)
c._decorTool = 'select'; c._decorSel = sofaId; c.requestUpdate();
c._editorRuntime._clearFurniturePreview();
c._decorTool = 'select'; c._furnPalette = null; c._furnCategory = null;
c._decorSel = sofaId; c.requestUpdate();
await c.updateComplete; await sleep(40); await c.updateComplete;
ev('pointerdown', el(sofaId), 300, 300);
out.dragStarted = !!sofaId && c._decorMove?.id === sofaId;
@@ -213,7 +310,7 @@ const res = await page.evaluate(async () => {
ev('pointerup', stageEl(), 300, 300, { shiftKey: true });
await c.updateComplete;
// ================= 6. рамка, углы, плашки, поворот =======================
// ================= 7. рамка, углы, плашки, поворот =======================
c._decorSel = sofaId; c.requestUpdate();
await c.updateComplete; await sleep(60); await c.updateComplete;
const frame = () => sr().querySelector('.dtframe');
@@ -285,7 +382,7 @@ const res = await page.evaluate(async () => {
await c.updateComplete;
out.rotationIsRendered = /rotate\(/.test(attr(sofaId, 'transform') || '');
// ================= 7. конфиг и пересборка ================================
// ================= 8. конфиг и пересборка ================================
const stored = JSON.parse(JSON.stringify(sofaNow()));
out.storedShapeIsComplete = stored.kind === 'furniture' && typeof stored.symbol === 'string'
&& ['x', 'y', 'w', 'h'].every((k) => Number.isFinite(stored[k]));
@@ -305,7 +402,7 @@ const res = await page.evaluate(async () => {
)?.value === stored.symbol;
c._decorShapeDialog = null;
// ================= 8. инертность и удаление ==============================
// ================= 9. инертность и удаление ==============================
c._decorTool = 'furniture'; c._furnPalette = null; await c.updateComplete;
out.inertUnderTheStamp = pe(sofaId) === 'none';
const n0 = c._decorList.length;
+51 -51
View File
@@ -1,111 +1,111 @@
{
"schema": 1,
"fingerprint": "817110bad40d29ee0aa8e4ef2dadfda9e310dfa6346ddb5d2725ecafb359f073",
"fingerprint": "e6c51f2467ae4e22a792e536ae954291ab5a46915e728b216c8c45bc6816b209",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-BrUCyMoE.js",
"houseplan-assets/houseplan-card-DI__VUPO.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 271841,
"initialViewGzipBytes": 272469,
"lazyFiles": [
"houseplan-assets/de-CndLXoON.js",
"houseplan-assets/editor-H80bjnWS.js",
"houseplan-assets/houseplan-editor-runtime-LLPkWHY0.js",
"houseplan-assets/houseplan-onboarding-runtime-BkWWzfSR.js",
"houseplan-assets/space-deletion-CLA5ezlL.js"
"houseplan-assets/de-gTJXGK7s.js",
"houseplan-assets/editor-LGCA8WjH.js",
"houseplan-assets/houseplan-editor-runtime-Cu2NFP8N.js",
"houseplan-assets/houseplan-onboarding-runtime-B4PXZ4um.js",
"houseplan-assets/space-deletion-Do1Uuvoz.js"
],
"lazyGzipBytes": 165904,
"lazyGzipBytes": 166314,
"lazyEditorFiles": [
"houseplan-assets/editor-H80bjnWS.js",
"houseplan-assets/houseplan-editor-runtime-LLPkWHY0.js",
"houseplan-assets/space-deletion-CLA5ezlL.js"
"houseplan-assets/editor-LGCA8WjH.js",
"houseplan-assets/houseplan-editor-runtime-Cu2NFP8N.js",
"houseplan-assets/space-deletion-Do1Uuvoz.js"
],
"lazyEditorGzipBytes": 136806,
"lazyEditorGzipBytes": 137196,
"lazyOnboardingFiles": [
"houseplan-assets/houseplan-onboarding-runtime-BkWWzfSR.js",
"houseplan-assets/space-deletion-CLA5ezlL.js"
"houseplan-assets/houseplan-onboarding-runtime-B4PXZ4um.js",
"houseplan-assets/space-deletion-Do1Uuvoz.js"
],
"lazyOnboardingGzipBytes": 7193,
"lazyOnboardingGzipBytes": 7204,
"lazyLocaleFiles": [
"houseplan-assets/de-CndLXoON.js"
"houseplan-assets/de-gTJXGK7s.js"
],
"lazyLocaleGzipBytes": 22454,
"lazyLocaleGzipBytes": 22468,
"files": [
{
"path": "houseplan-assets/de-CndLXoON.js",
"sha256": "c3fb30be483664a92a555e2a916d2d6201a5d950d77385a34869445625732be1",
"path": "houseplan-assets/de-gTJXGK7s.js",
"sha256": "2ed2a8aad10d9eaa16fe3fe2e591c2d736d948b2fd35aca01fcffb33264e1284",
"rawBytes": 73919,
"gzipBytes": 22454,
"gzipBytes": 22468,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-H80bjnWS.js",
"sha256": "bcd9f271360a94dfbf63d5bcdf3c965aed1e1b17d0c543ac0b554d5527b90250",
"path": "houseplan-assets/editor-LGCA8WjH.js",
"sha256": "0f4ce2555703ec01ee188cf84badfdee661587e849c1b604ec88354c3f94894f",
"rawBytes": 3831,
"gzipBytes": 1580,
"gzipBytes": 1585,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BrUCyMoE.js"
"houseplan-assets/houseplan-card-DI__VUPO.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-BrUCyMoE.js",
"sha256": "bdcf3f7e9306428b2583f1e662541bebebcf2f106b48a53df117c6c9c54c5cde",
"rawBytes": 958245,
"gzipBytes": 271090,
"path": "houseplan-assets/houseplan-card-DI__VUPO.js",
"sha256": "35a537b289b388f34a5f6efc01a95254207be9880fa9fd931723ccfa258ad602",
"rawBytes": 960284,
"gzipBytes": 271715,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-CndLXoON.js",
"houseplan-assets/editor-H80bjnWS.js",
"houseplan-assets/houseplan-editor-runtime-LLPkWHY0.js",
"houseplan-assets/houseplan-onboarding-runtime-BkWWzfSR.js"
"houseplan-assets/de-gTJXGK7s.js",
"houseplan-assets/editor-LGCA8WjH.js",
"houseplan-assets/houseplan-editor-runtime-Cu2NFP8N.js",
"houseplan-assets/houseplan-onboarding-runtime-B4PXZ4um.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-LLPkWHY0.js",
"sha256": "e49282b51d104baff861b3c21261e66dcbfd58807e0db444b04f1f171499e24d",
"rawBytes": 520292,
"gzipBytes": 134677,
"path": "houseplan-assets/houseplan-editor-runtime-Cu2NFP8N.js",
"sha256": "4eb656ee069026534dace297817908e97d0e82e63ba1059d0c3d6c283d913344",
"rawBytes": 522188,
"gzipBytes": 135057,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BrUCyMoE.js",
"houseplan-assets/space-deletion-CLA5ezlL.js"
"houseplan-assets/houseplan-card-DI__VUPO.js",
"houseplan-assets/space-deletion-Do1Uuvoz.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-BkWWzfSR.js",
"sha256": "5446043fbf2a383f84df3751b1fd97ecac72d8bcf3c254da80ad4176538c91da",
"path": "houseplan-assets/houseplan-onboarding-runtime-B4PXZ4um.js",
"sha256": "73ad6471b2395b982d233754598fcb6fb296c29b01a188cce724c2861e044187",
"rawBytes": 26356,
"gzipBytes": 6644,
"gzipBytes": 6650,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BrUCyMoE.js",
"houseplan-assets/space-deletion-CLA5ezlL.js"
"houseplan-assets/houseplan-card-DI__VUPO.js",
"houseplan-assets/space-deletion-Do1Uuvoz.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/space-deletion-CLA5ezlL.js",
"sha256": "ec0519abd52ab649e2ed8f5744cc8284d96d8a83f3c9517df7f5070be63a66e9",
"rawBytes": 906,
"gzipBytes": 549,
"path": "houseplan-assets/space-deletion-Do1Uuvoz.js",
"sha256": "258cd7b7338ca746122d6afcfc63f81d34be23a7bd31f1599ed8d45ec0daa5b6",
"rawBytes": 911,
"gzipBytes": 554,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "9bb7a0cb60837b9a006d1c0d125c820ab083b270a9c6130147cd5c8c8b47cff1",
"sha256": "031ef9710a85647066f82b62cd7a294c752493c0391cf59b1688380f7cac2a18",
"rawBytes": 1089,
"gzipBytes": 751,
"gzipBytes": 754,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-BrUCyMoE.js"
"houseplan-assets/houseplan-card-DI__VUPO.js"
],
"dynamicImports": []
}
File diff suppressed because one or more lines are too long
@@ -1,14 +1,14 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="817110bad40d29ee0aa8e4ef2dadfda9e310dfa6346ddb5d2725ecafb359f073";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-BrUCyMoE.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__="e6c51f2467ae4e22a792e536ae954291ab5a46915e728b216c8c45bc6816b209";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-DI__VUPO.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),u=e=>c`<ha-form
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
.computeLabel=${e=>h[e.name]||e.name}
@value-changed=${this._valueChanged}
></ha-form>`;return c`
${d(f.slice(0,3))}
${_?c`<div class="default-floor-error" role="alert"
${u(_.slice(0,3))}
${f?c`<div class="default-floor-error" role="alert"
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
${t(s,"editor.default_floor_missing",{id:_})}
${t(s,"editor.default_floor_missing",{id:f})}
</div>`:a}
${d(f.slice(3))}
${u(_.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__="817110bad40d29ee0aa8e4ef2dadfda9e310dfa6346ddb5d2725ecafb359f073";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-BrUCyMoE.js";import{i as b,c as $,a as v,s as f,t as y}from"./space-deletion-CLA5ezlL.js";const w=1e3,D=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},S="817110bad40d29ee0aa8e4ef2dadfda9e310dfa6346ddb5d2725ecafb359f073";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__="e6c51f2467ae4e22a792e536ae954291ab5a46915e728b216c8c45bc6816b209";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 n,b_ as c,b$ as r,b as p,A as _,cR as d,ap as u,ar as g,cS as m}from"./houseplan-card-DI__VUPO.js";import{i as b,c as $,a as v,s as y,t as f}from"./space-deletion-Do1Uuvoz.js";const w=1e3,D=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},S="e6c51f2467ae4e22a792e536ae954291ab5a46915e728b216c8c45bc6816b209";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__="817110bad40d29ee0aa8e4ef2dadfda9e310
<label>${this.host._t("space.plan_label")}</label>
<label class="srcrow">
<input type="radio" name="plansrc" .checked=${"file"===t.source}
@change=${()=>this.host._spaceDialog=f(t,"file")} />
@change=${()=>this.host._spaceDialog=y(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__="817110bad40d29ee0aa8e4ef2dadfda9e310
${t.pickSaved?this._renderServerPlans(t):_}`:_}
<label class="srcrow">
<input type="radio" name="plansrc" .checked=${"draw"===t.source}
@change=${()=>this.host._spaceDialog=f(t,"draw")} />
@change=${()=>this.host._spaceDialog=y(t,"draw")} />
<span>${this.host._t("space.source_draw")}</span>
</label>
@@ -85,7 +85,7 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="817110bad40d29ee0aa8e4ef2dadfda9e310
<label class="dispsection">${this.host._t("space.display_section")}</label>
<label class="srcrow">
${this._boolInput(t.showBorders,s=>{this.host._spaceDialog=y(t,"showBorders",s)})}
${this._boolInput(t.showBorders,s=>{this.host._spaceDialog=f(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__="817110bad40d29ee0aa8e4ef2dadfda9e310
</select>
<div class="rhint">${this.host._t("space.zero_wall_help")}</div>
<label class="srcrow">
${this._boolInput(t.showNames,s=>{this.host._spaceDialog=y(t,"showNames",s)})}
${this._boolInput(t.showNames,s=>{this.host._spaceDialog=f(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__="817110bad40d29ee0aa8e4ef2dadfda9e310dfa6346ddb5d2725ecafb359f073";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)),a=[...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:a,count:a.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="e6c51f2467ae4e22a792e536ae954291ab5a46915e728b216c8c45bc6816b209";export{t as a,e as c,o as i,r as s,s as t};
+1 -1
View File
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="817110bad40d29ee0aa8e4ef2dadfda9e310dfa6346ddb5d2725ecafb359f073";try{await import("./houseplan-assets/houseplan-card-BrUCyMoE.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__="e6c51f2467ae4e22a792e536ae954291ab5a46915e728b216c8c45bc6816b209";try{await import("./houseplan-assets/houseplan-card-DI__VUPO.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)}
+6
View File
@@ -2,6 +2,12 @@
## Unreleased
- Furniture placement in the Background editor now shows a translucent mouse
preview with the exact future size, rotation and wall snap. It follows live
size edits and disappears without a config change on cancel; on touch,
movement, cancellation or a second contact cannot create an accidental item
([#359](https://github.com/Matysh/houseplan-card/issues/359)).
- 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
+6
View File
@@ -8,6 +8,12 @@
## Не выпущено
- При размещении мебели в редакторе подложки мышь теперь показывает на плане
полупрозрачный символ с точным будущим размером, поворотом и привязкой к
стене. Предпросмотр обновляется вместе с полями размера и исчезает без
изменений при отмене; на touch движение, отмена или второй палец не создают
случайный предмет ([#359](https://github.com/Matysh/houseplan-card/issues/359)).
- След пылесоса теперь повторяет те же записанные точки с мягко скруглёнными
поворотами вместо заметной ломаной. Текущая и предыдущая уборки используют
одну ограниченную кривую, сохраняют разрывы телеметрии и не отклоняются от
+15 -3
View File
@@ -53,9 +53,21 @@ Furniture is a two-level non-modal palette in the editor context tray:
3. The second level shows the real top-view drawings. Back returns to the
category list and clears any armed symbol.
4. Picking a variant arms one placement and reveals editable Width and Depth
in the Home Assistant length unit.
5. Clicking the plan places the object and returns to Select. Shift keeps the
established free-placement behaviour.
in the Home Assistant length unit. With a mouse, moving over the plan shows
the real top-view symbol at its exact future size, wall magnet, rotation and
canvas clamp. Editing Width or Depth updates that ghost immediately without
requiring another pointer move.
5. Clicking the plan places exactly the previewed object and returns to Select.
Shift keeps the established free-placement behaviour. The ghost is a
transient, 55%-opaque rendering aid: it is never saved and never enters
Undo history.
6. Pointer leave, Escape, palette/tool/editor/space changes and remount clear
the ghost. Unknown or stale symbol ids show and save nothing.
Touch and pen do not emulate hover or show a placement ghost. Their best-effort
editor path saves one object only after a clean tap; movement, pointer cancel
or a second contact cancels that pending stamp so pinch/cancel cannot create
furniture accidentally. View and kiosk touch guarantees remain unchanged.
The properties dialog remains a flat native select, grouped by category. Its
optgroup label includes both the parent group and category because HTML selects
+5 -1
View File
@@ -1302,7 +1302,11 @@ Power=`off`/`unavailable` подавляет даже устаревший ак
### Мебель
Библиотека содержит группы мебели, бытовой техники, сантехники и прочих символов. Размер можно задать в метрах или десятичных футах. При размещении рядом со стеной предмет:
Библиотека содержит группы мебели, бытовой техники, сантехники и прочих символов. Размер можно задать в метрах или десятичных футах. После выбора символа наведите мышь на план: полупрозрачное изображение заранее показывает точное будущее положение, размер и поворот предмета. Изменение ширины или глубины сразу обновляет предпросмотр. Клик размещает предмет именно в показанной позиции; выход курсора с плана, `Esc`, смена инструмента, пространства или редактора убирают предпросмотр без изменения плана.
На сенсорных экранах предпросмотр не показывается: чистый тап размещает предмет, а движение, отмена касания или второй палец отменяют ожидающее размещение. Редактирование на touch-устройствах по-прежнему поддерживается по остаточному принципу; для полноценной работы используйте desktop.
При размещении рядом со стеной предмет:
- прижимается к ней и поворачивается параллельно;
- всегда прижимается к ближайшей стене в радиусе магнита, иначе размещается по сетке;
+65 -1
View File
@@ -28,7 +28,7 @@
* colour, so a plan stays a drawing (owner: «символы рисуются линейно»).
*/
import { NORM_W, GRID_PITCH, CANVAS_LIMIT } from './space-geometry';
import { NORM_W, GRID_PITCH, CANVAS_LIMIT, clampCanvasN } from './space-geometry';
import { GENERATED_FURNITURE_PLAN } from './furniture-plan-art.generated';
/** Groups the palette shows, in the order it shows them. */
@@ -395,6 +395,33 @@ export interface FurnitureSnap {
/** Wall segments (`[x1,y1,x2,y2]`) in whatever units the caller works in. */
export type WallSeg = number[];
/** Final normalised box shared by the placement ghost and the saved decor
* record. Keeping this result pure prevents a preview/commit jump when wall
* magnetism or the canvas guard participates (#359). */
export interface FurniturePlacement {
symbol: string;
x: number;
y: number;
w: number;
h: number;
angle: number;
}
export interface FurniturePlacementInput {
symbol: string;
widthCm: number;
depthCm: number;
/** Already resolved decor/grid snap point, in render units. */
point: readonly [number, number];
canvasW: number;
canvasH: number;
cellCm: number;
gridPitch: number;
walls: WallSeg[];
wallReach: number;
free?: boolean;
}
const norm180 = (a: number): number => {
let x = ((a % 360) + 360) % 360;
if (x > 180) x -= 360;
@@ -458,6 +485,43 @@ export function snapFurnitureToWall(
return best;
}
/**
* Resolve the exact geometry one furniture stamp will own.
*
* The caller owns the higher-level decor/room/grid snap because that geometry
* is editor state. Everything after that point is pure and therefore shared
* byte-for-byte by hover preview and commit. Unknown symbols fail dark: a
* newer config or a stale palette cannot create invisible furniture.
*/
export function resolveFurniturePlacement(input: FurniturePlacementInput): FurniturePlacement | null {
const {
symbol, widthCm, depthCm, point, canvasW, canvasH, cellCm, gridPitch,
walls, wallReach, free = false,
} = input;
if (!furnitureGraphic(symbol) || !(canvasW > 0) || !(canvasH > 0)
|| !Number.isFinite(point[0]) || !Number.isFinite(point[1])) return null;
const w = clampFurnSize(cmToNorm(widthCm, cellCm, gridPitch, canvasW));
const h = clampFurnSize(cmToNorm(depthCm, cellCm, gridPitch, canvasW));
let cx = point[0], cy = point[1];
let angle = 0;
const snap = free ? null : snapFurnitureToWall(
cx, cy, h * canvasH, walls, wallReach, gridPitch,
);
if (snap) {
cx = snap.cx;
cy = snap.cy;
angle = snap.angle;
}
return {
symbol,
x: clampCanvasN(cx / canvasW - w / 2),
y: clampCanvasN(cy / canvasH - h / 2),
w,
h,
angle: Number(angle.toFixed(2)),
};
}
/**
* The four corners of a rotated box, world units, in the order the frame
* draws them: NW, NE, SE, SW. Pure, so the frame, the gestures and the tests
+53 -3
View File
@@ -63,7 +63,8 @@ import { dayCycleStageVars, renderDayCycleEnvironment } from './day-cycle-render
import {
FURNITURE_GROUPS, furnitureOfGroup, furnitureSymbol, furnitureDefaultCm,
furnitureGraphic, furnitureCorners, snapFurnitureToWall,
cmToNorm, clampFurnSize, clampFurnCm, FURN_WALL_CELLS, type FurnitureGroup,
cmToNorm, clampFurnSize, clampFurnCm, FURN_WALL_CELLS,
type FurnitureGroup, type FurniturePlacement,
} from './furniture';
import {
degradeWalls, rekeyWallsAfterMoveChecked, wallRecordCarrierViolations,
@@ -1037,6 +1038,15 @@ export class HouseplanCard extends LitElement {
private _furnPalette: { symbol: string; w: number; h: number } | null = null;
/** Category currently open in the two-level furniture palette. */
private _furnCategory: string | null = null;
/** Last fine-pointer location while one furniture stamp is armed. The final
* geometry is deliberately resolved on render from the live palette so a
* Width/Depth edit updates the ghost without another mouse move (#359). */
private _furnPreviewInput: { raw: [number, number]; free: boolean } | null = null;
/** Touch/pen has no hover. Delay its one-shot stamp until a clean pointerup
* so pointercancel or a second contact cannot save accidental geometry. */
private _furnTouchPending: {
pid: number; sx: number; sy: number; pointerType: string; cancelled: boolean;
} | null = null;
/** The selected shape's own unrotated frame (text is measured from SVG). */
private _dtBox: { id: string; x: number; y: number; w: number; h: number } | null = null;
/**
@@ -1437,6 +1447,7 @@ export class HouseplanCard extends LitElement {
if (id !== this._space) {
this._clearTransientHover(true);
this._cancelDevicePressFeedback();
this._editorRuntime?._clearFurniturePreview();
}
this._space = id;
return true;
@@ -2431,6 +2442,7 @@ export class HouseplanCard extends LitElement {
_backdropDialog: { state: true },
_furnPalette: { state: true },
_furnCategory: { state: true },
_furnPreviewInput: { state: true },
_bdDrag: { state: true },
_dtBox: { state: true },
_dtDrag: { state: true },
@@ -2676,6 +2688,7 @@ export class HouseplanCard extends LitElement {
this._warmRelease();
this._clearPlanSnapHover();
this._clearOpeningPlacement(true);
this._editorRuntime?._clearFurniturePreview();
this._touchContacts.clear();
this._touchSequenceMultitouch = false;
this._touchClickBlockUntil = 0;
@@ -2781,6 +2794,7 @@ export class HouseplanCard extends LitElement {
// The palette is one explicit surface: Escape closes it and returns
// to Select in one step, regardless of whether a symbol was armed.
else if (this._decorTool === 'furniture') {
this._editorRuntime?._clearFurniturePreview();
this._furnPalette = null;
this._furnCategory = null;
this._decorTool = 'select';
@@ -6156,6 +6170,10 @@ export class HouseplanCard extends LitElement {
this._decorMoveUpdate(ev);
return;
}
if (this._mode === 'decor' && this._decorTool === 'furniture' && this._editorRuntime) {
this._notePointer(ev);
if (this._editorRuntime._furnPointerMove(ev, this._pointerModality.hoverEnabled)) return;
}
if (!this._pointers.has(ev.pointerId)) {
this._markupMove(ev);
return;
@@ -6220,7 +6238,8 @@ export class HouseplanCard extends LitElement {
}
}
private _stagePointerLeave(_ev: PointerEvent): void {
private _stagePointerLeave(ev: PointerEvent): void {
if (this._mode === 'decor') this._editorRuntime?._furnPointerLeave(ev);
if (!this._markup) return;
if (this._tool === 'opening') {
this._cursorPt = null;
@@ -6297,6 +6316,7 @@ export class HouseplanCard extends LitElement {
this._decorMove = null;
return;
}
if (this._editorRuntime?._furnPointerUp(ev)) return;
const viewportGestureEnded = !!this._pinchStart || !!this._panStart;
this._pointers.delete(ev.pointerId);
if (this._pointers.size < 2) this._pinchStart = null;
@@ -8018,6 +8038,36 @@ export class HouseplanCard extends LitElement {
</g>` as unknown as TemplateResult;
}
private get _furniturePreviewPlacement(): FurniturePlacement | null {
const input = this._furnPreviewInput;
if (!input || !this._editorRuntime || this._mode !== 'decor'
|| this._decorTool !== 'furniture' || !this._furnPalette
|| !this._pointerModality.hoverEnabled) return null;
return this._editorRuntime._resolveFurniturePlacement(input.raw, input.free, 'mouse');
}
/** 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 {
const placement = this._furniturePreviewPlacement;
if (!placement) return nothing;
const art = furnitureGraphic(placement.symbol);
if (!art) return nothing;
const W = NORM_W, H = this._decorH;
const W2 = placement.w * W, H2 = placement.h * H;
const cx = placement.x * W + W2 / 2, cy = placement.y * H + H2 / 2;
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;
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)}
fill="none" stroke-linecap="round" stroke-linejoin="round"
vector-effect="non-scaling-stroke" aria-hidden="true" pointer-events="none"></path>`;
}
private _renderDecorLayer(): TemplateResult {
const W = NORM_W, H = this._decorH;
const editing = this._mode === 'decor';
@@ -8137,7 +8187,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}</g>` as unknown as TemplateResult;
return svg`<g class="decorlayer">${shapes}${draft}${this._renderFurniturePlacementPreview()}</g>` as unknown as TemplateResult;
}
// ================= shared editor secondary surface =================
+122 -16
View File
@@ -65,7 +65,9 @@ import { dayCycleStageVars, renderDayCycleEnvironment } from './day-cycle-render
import {
FURNITURE_GROUPS, furnitureOfGroup, furnitureDefaultCm,
furnitureGraphic, furnitureCorners, snapFurnitureToWall,
cmToNorm, clampFurnSize, clampFurnCm, FURN_WALL_CELLS, type FurnitureGroup,
resolveFurniturePlacement,
clampFurnCm, FURN_WALL_CELLS,
type FurnitureGroup, type FurniturePlacement,
} from './furniture';
import { GENERATED_FURNITURE_MENU } from './furniture-menu-art.generated';
import {
@@ -917,6 +919,10 @@ export interface HouseplanEditorHostPort {
_fullRegistryHass: any;
_furnPalette: { symbol: string; w: number; h: number; } | null;
_furnCategory: string | null;
_furnPreviewInput: { raw: [number, number]; free: boolean; } | null;
_furnTouchPending: {
pid: number; sx: number; sy: number; pointerType: string; cancelled: boolean;
} | null;
_furnWallReach: number;
_furnWalls: number[][];
_gearPtCache: WeakMap<number[][], number[]>;
@@ -1169,6 +1175,7 @@ public _help(key: Extract<I18nKey, `${string}.help`>): TemplateResult | typeof n
public _setMode(mode: 'view' | 'plan' | 'devices' | 'decor', animate = true): void {
this.host._endTabDrag();
this._clearFurniturePreview();
// A mode command is newer than the editor remembered by a same-route warm
// remount. Clear it before the same-mode early return: while can_write is
// pending, Lit may still be presenting the previous editor DOM even though
@@ -2197,6 +2204,7 @@ public _commitPhysicalGeometry(
}
public _clearGeometryGesture(): void {
this._clearFurniturePreview();
this.host._path = [];
this._clearPlanSnapHover();
this._clearOpeningPlacement(false);
@@ -2226,6 +2234,12 @@ public _clearGeometryGesture(): void {
public _stagePointerCancel(ev: PointerEvent): void {
clearTimeout(this.host._kioskHoldTimer);
if (this.host._swipeStart?.id === ev.pointerId) this.host._swipeStart = null;
if (this.host._furnTouchPending?.pid === ev.pointerId) {
this._clearFurniturePreview();
this.host._pointers.delete(ev.pointerId);
this.host.requestUpdate();
return;
}
if (this.host._physicalDrag?.pid === ev.pointerId || this.host._physicalRotate?.pid === ev.pointerId) {
this._cancelPhysicalGesture();
return;
@@ -4095,7 +4109,29 @@ public _decorPointerDown(ev: PointerEvent): boolean {
// must not silently place whatever was chosen last week.
if (!this.host._furnPalette) return false;
ev.preventDefault();
this._furnPlace(this._svgPoint(ev), ev.shiftKey);
// A mouse has already shown the exact result and keeps the traditional
// one-press stamp. Touch/pen has no hover, so commit only after a clean
// pointerup; pointercancel, movement and a second contact stay fail-dark.
const pointerType = ev.pointerType || 'mouse';
if (pointerType === 'mouse') {
this._furnPlace(this._svgPoint(ev), ev.shiftKey, pointerType);
return true;
}
const pending = this.host._furnTouchPending;
if (pending && pending.pid !== ev.pointerId) {
pending.cancelled = true;
this.host._furnPreviewInput = null;
return true;
}
this.host._furnTouchPending = {
pid: ev.pointerId,
sx: ev.clientX,
sy: ev.clientY,
pointerType,
cancelled: false,
};
this.host._furnPreviewInput = null;
capturePointer(ev);
return true;
}
// Empty-space selection belongs only to Select. Erase is an object
@@ -4639,41 +4675,104 @@ public _furnPick(symbol: string): void {
this.host._furnPalette = { symbol, w: d.w, h: d.h };
}
public _furnPlace(raw: number[], free = false): void {
public _resolveFurniturePlacement(
raw: number[], free = false, pointerType = 'mouse',
): FurniturePlacement | null {
const pal = this.host._furnPalette;
const sp = this.host._curSpaceCfg;
if (!pal || !sp) return;
if (!pal || !sp) return null;
const W = NORM_W, H = this.host._decorH;
const wN = clampFurnSize(cmToNorm(pal.w, this.host._cellCm, this.host._gridPitch, W));
const hN = clampFurnSize(cmToNorm(pal.h, this.host._cellCm, this.host._gridPitch, W));
const snapped = this._decorSnap(raw, pointerType);
return resolveFurniturePlacement({
symbol: pal.symbol,
widthCm: pal.w,
depthCm: pal.h,
point: [snapped[0], snapped[1]],
canvasW: W,
canvasH: H,
cellCm: this.host._cellCm,
gridPitch: this.host._gridPitch,
walls: this.host._furnWalls,
wallReach: this.host._furnWallReach,
free,
});
}
public _furnPlace(raw: number[], free = false, pointerType = 'mouse'): void {
const placement = this._resolveFurniturePlacement(raw, free, pointerType);
if (!placement) return;
const sp = this.host._curSpaceCfg;
if (!sp) return;
const before = this._geometrySnapshot();
const c = this._decorSnap(raw);
let cx = c[0], cy = c[1];
let angle = 0;
const snap = free ? null : snapFurnitureToWall(cx, cy, hN * H, this.host._furnWalls,
this.host._furnWallReach, this.host._gridPitch);
if (snap) { cx = snap.cx; cy = snap.cy; angle = snap.angle; }
const id = 'df' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
const shape: any = {
id, kind: 'furniture', symbol: pal.symbol,
x: clampCanvasN(cx / W - wN / 2), y: clampCanvasN(cy / H - hN / 2),
w: wN, h: hN,
id, kind: 'furniture', symbol: placement.symbol,
x: placement.x, y: placement.y, w: placement.w, h: placement.h,
...decorStylePatch(this.host._decorStyle, false),
};
// a straight piece stores no angle at all, exactly as a straight label
// stores none (docs/LIVE-TEXT.md §3)
if (angle) shape.angle = Number(angle.toFixed(2));
if (placement.angle) shape.angle = placement.angle;
sp.decor = [...this.host._decorList, shape];
this.host._decorSel = id;
// …and the editor goes back to the tool that can move what was just placed
this.host._decorTool = 'select';
this.host._furnPalette = null;
this.host._furnCategory = null;
this._clearFurniturePreview();
this._recordGeometry(this.host._t('history.decor_add'), before);
this._saveConfig();
this.host.requestUpdate();
}
public _clearFurniturePreview(): void {
this.host._furnPreviewInput = null;
this.host._furnTouchPending = null;
}
/** Track only real fine-pointer hover. Touch/pen own a delayed one-tap
* transaction below and must never resurrect a compatibility mouse ghost. */
public _furnPointerMove(ev: PointerEvent, hoverAllowed: boolean): boolean {
const pending = this.host._furnTouchPending;
if (pending?.pid === ev.pointerId) {
if (Math.abs(ev.clientX - pending.sx) + Math.abs(ev.clientY - pending.sy) > 8) {
pending.cancelled = true;
}
this.host._furnPreviewInput = null;
return true;
}
if (this.host._mode !== 'decor' || this.host._decorTool !== 'furniture'
|| !this.host._furnPalette) {
this._clearFurniturePreview();
return false;
}
if (ev.pointerType !== 'mouse' || !hoverAllowed) {
this.host._furnPreviewInput = null;
return false;
}
const raw = this._svgPoint(ev);
this.host._furnPreviewInput = { raw: [raw[0], raw[1]], free: ev.shiftKey };
return true;
}
public _furnPointerLeave(ev: PointerEvent): void {
if (this.host._furnTouchPending?.pid === ev.pointerId) {
this.host._furnTouchPending.cancelled = true;
}
this.host._furnPreviewInput = null;
}
public _furnPointerUp(ev: PointerEvent): boolean {
const pending = this.host._furnTouchPending;
if (!pending || pending.pid !== ev.pointerId) return false;
this.host._furnTouchPending = null;
if (!pending.cancelled && this.host._mode === 'decor'
&& this.host._decorTool === 'furniture' && this.host._furnPalette) {
this._furnPlace(this._svgPoint(ev), ev.shiftKey, pending.pointerType);
}
return true;
}
public _furnMoveUpdate(ev: PointerEvent): void {
const m = this.host._decorMove!;
if (m.orig.kind !== 'furniture') return;
@@ -4765,6 +4864,7 @@ public _renderFurnPalette(): TemplateResult {
<span class="spacer"></span>
<button class="btn furnclose" title=${this.host._t('btn.close')}
@click=${() => {
this._clearFurniturePreview();
this.host._furnPalette = null;
this.host._furnCategory = null;
this.host._decorTool = 'select';
@@ -4775,6 +4875,7 @@ public _renderFurnPalette(): TemplateResult {
<div class="furnbody">
${category ? html`
<button class="btn ghost furnback" @click=${() => {
this._clearFurniturePreview();
this.host._furnPalette = null;
this.host._furnCategory = null;
}}>
@@ -4798,6 +4899,7 @@ public _renderFurnPalette(): TemplateResult {
class="furnitem furncategory" data-category=${item.id}
title=${this.host._t(`furn.cat_${item.id}` as I18nKey)}
@click=${() => {
this._clearFurniturePreview();
this.host._furnPalette = null;
this.host._furnCategory = item.id;
}}>
@@ -5117,6 +5219,7 @@ public _renderDecorSecondary(): EditorSecondaryModel | null {
dismissPolicy: this.host._furnPalette ? 'stay-open-on-canvas' : 'outside',
dismiss: () => {
if (this.host._decorTool !== 'furniture') return;
this._clearFurniturePreview();
this.host._furnPalette = null;
this.host._furnCategory = null;
this.host._decorTool = 'select';
@@ -5239,6 +5342,7 @@ public _renderDecorBar(): TemplateResult {
data-editor-palette=${t === 'furniture' ? 'furniture' : nothing}
@click=${() => {
if (t === 'furniture' && this.host._decorTool === 'furniture') {
this._clearFurniturePreview();
this.host._furnPalette = null;
this.host._furnCategory = null;
this.host._decorTool = 'select';
@@ -5250,9 +5354,11 @@ public _renderDecorBar(): TemplateResult {
// the palette belongs to its tool and to nothing else: leaving the
// tool disarms whatever was chosen, so no later click can stamp it
if (t !== 'furniture') {
this._clearFurniturePreview();
this.host._furnPalette = null;
this.host._furnCategory = null;
} else {
this._clearFurniturePreview();
this.host._furnCategory = null;
}
}}
+4
View File
@@ -737,6 +737,10 @@ export const planStyles = css`
calc(5px * var(--hp-cell-visual-scale, 1));
pointer-events: none;
}
.decorlayer .furniture-placement-preview {
opacity: 0.55;
pointer-events: none;
}
.decorlayer text {
font-weight: 600;
user-select: none;
+34 -1
View File
@@ -6,7 +6,8 @@ import assert from 'node:assert/strict';
import {
FURNITURE, FURNITURE_GROUPS, furnitureSymbol, furnitureOfGroup,
furnitureDefaultCm, furniturePathD, furnitureGraphic, furnitureCorners, furnitureResize,
snapFurnitureToWall, cmToNorm, normToCm, clampFurnSize, clampFurnCm,
snapFurnitureToWall, resolveFurniturePlacement,
cmToNorm, normToCm, clampFurnSize, clampFurnCm,
FURN_MIN_N, FURN_MIN_CM, FURN_MAX_CM, FURN_WALL_CELLS,
} from '../test-build/furniture.js';
import { roomEdges } from '../test-build/logic.js';
@@ -185,6 +186,38 @@ test('the default reach is six cells — thirty centimetres on a default plan',
closeTo(GRID_PITCH * FURN_WALL_CELLS * (5 / GRID_PITCH), 30); // cells x cell_cm
});
test('preview and commit share one deterministic furniture placement resolver', () => {
const input = {
symbol: 'sofa', widthCm: 180, depthCm: 90,
point: [300, 112], canvasW: 1000, canvasH: 1000,
cellCm: 5, gridPitch: GRID_PITCH, walls: EDGES,
wallReach: 30,
};
const preview = resolveFurniturePlacement(input);
const commit = resolveFurniturePlacement({ ...input });
assert.deepEqual(preview, commit);
assert.equal(preview.symbol, 'sofa');
closeTo((preview.x + preview.w / 2) * 1000, 300, GRID_PITCH);
closeTo((preview.y + preview.h / 2) * 1000, 100 + preview.h * 500);
assert.equal(preview.angle, 0);
});
test('the shared placement resolver supports Shift/free, canvas guards and unknown symbols', () => {
const base = {
symbol: 'sofa', widthCm: 180, depthCm: 90,
point: [300, 112], canvasW: 1000, canvasH: 1000,
cellCm: 5, gridPitch: GRID_PITCH, walls: EDGES,
wallReach: 30,
};
const free = resolveFurniturePlacement({ ...base, free: true });
closeTo((free.x + free.w / 2) * 1000, 300);
closeTo((free.y + free.h / 2) * 1000, 112);
const guarded = resolveFurniturePlacement({ ...base, point: [-1e9, -1e9], free: true });
assert.equal(guarded.x, -5000);
assert.equal(guarded.y, -5000);
assert.equal(resolveFurniturePlacement({ ...base, symbol: 'future_unknown_symbol' }), null);
});
// ------------------------------- the frame ----------------------------------
test('the corners of an unrotated box are the box; a rotated one turns about its centre', () => {
+11
View File
@@ -835,6 +835,17 @@ test('issue 159 goldens cover both palette levels and every plan-art compatibili
new Set(['sofa', 'fridge', 'sofa_corner_right']));
});
test('issue 359 has one deterministic light-theme furniture placement preview golden', () => {
const scenario = GOLDEN_SCENARIOS.find((item) => item.id === 'furniture-placement-preview-light');
assert.ok(scenario);
assert.equal(scenario.mode, 'decor');
assert.equal(scenario.theme, 'light');
assert.equal(scenario.capture, 'stage');
assert.deepEqual(scenario.furniturePlacementPreview, {
symbol: 'sofa', widthCm: 180, depthCm: 90, pointer: [0.35, 0.90],
});
});
test('device dialog goldens expose the complete light-source controls at desktop and mobile widths', () => {
const dialogs = GOLDEN_SCENARIOS.filter((scenario) => scenario.id.startsWith('device-dialog-'));
assert.equal(dialogs.length, 3);