diff --git a/.github/workflows/publish-prerelease.yml b/.github/workflows/publish-prerelease.yml index 19991da3..126e34db 100644 --- a/.github/workflows/publish-prerelease.yml +++ b/.github/workflows/publish-prerelease.yml @@ -82,17 +82,13 @@ jobs: set -euo pipefail npm ci npm run build - cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js + node scripts/bundle-tree.mjs dist custom_components/houseplan/frontend + npm run bundle:budget VERSION=${TAG#v} - grep -Fq "$VERSION" dist/houseplan-card.js + grep -RFq "$VERSION" dist (cd custom_components/houseplan && zip -qr ../../houseplan.zip .) - unzip -l houseplan.zip | grep -q "manifest.json" - ZIP_VERSION=$(unzip -p houseplan.zip manifest.json | node -e \ - "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>process.stdout.write(JSON.parse(s).version))") - test "$ZIP_VERSION" = "$VERSION" || { - echo "::error::houseplan.zip manifest version $ZIP_VERSION != $VERSION" - exit 1 - } + node scripts/verify-houseplan-zip.mjs houseplan.zip \ + custom_components/houseplan/frontend "$VERSION" test -s dist/houseplan-card.js test -s houseplan.zip - name: Create or verify the annotated tag diff --git a/.github/workflows/release-zip.yml b/.github/workflows/release-zip.yml index a2545897..0ed0fed2 100644 --- a/.github/workflows/release-zip.yml +++ b/.github/workflows/release-zip.yml @@ -34,7 +34,7 @@ jobs: - name: Build houseplan.zip (contents of custom_components/houseplan at zip root) run: cd custom_components/houseplan && zip -qr ../../houseplan.zip . - name: Sanity check - run: unzip -l houseplan.zip | grep -q "manifest.json" + run: node scripts/verify-houseplan-zip.mjs houseplan.zip custom_components/houseplan/frontend - name: Upload asset env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 703683cc..ae69e9c1 100755 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,7 +67,8 @@ jobs: with: name: continuity-screencast path: artifacts/continuity-screencast - - run: cp dist/houseplan-card.js custom_components/houseplan/frontend/ + - name: Verify the complete committed frontend tree + run: node scripts/bundle-tree.mjs dist custom_components/houseplan/frontend - name: Attach card to release uses: softprops/action-gh-release@v3 with: diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 7afbe883..72adefc4 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -314,9 +314,11 @@ jobs: run: npm test - name: Build run: npm run build - # Копия стенда больше не коммитится (#255): сверяются две обязательные. - - name: Card bundle snapshots in sync - run: cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js + # Entry, manifest and every content-hashed chunk are one artifact (#337). + - name: Card bundle trees in sync + run: | + node scripts/bundle-tree.mjs dist custom_components/houseplan/frontend + npm run bundle:budget # Бандл собирается один раз и уезжает артефактом (#336). Прежде каждая из # пяти браузерных job собирала его заново: `bundle:sync` — это `tsc # --noEmit` плюс rollup, около минуты на job. @@ -328,7 +330,7 @@ jobs: uses: actions/upload-artifact@v7 with: name: card-bundle - path: dist/houseplan-card.js + path: dist/ retention-days: 1 if-no-files-found: error diff --git a/.gitignore b/.gitignore index 614e6d97..9cbbc397 100755 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ artifacts/ # она только росла — 364 версии по 1.16 МБ за семь недель (#255). Обязательных # копий две: `dist/` (артефакт сборки) и `custom_components/` (её ставит HACS). demo/srv/assets/houseplan-card.js +demo/srv/assets/houseplan-assets.json +demo/srv/assets/houseplan-assets/ diff --git a/AGENTS.md b/AGENTS.md index 899a16b3..ad58e5f5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,8 @@ House Plan is one HACS package with two parts plus a demo harness: -- **Lovelace card** (`src/`, TypeScript + Lit) — the primary product, bundled to `dist/houseplan-card.js`. +- **Lovelace card** (`src/`, TypeScript + Lit) — the primary product, bundled to + the entry, manifest and hashed chunks under `dist/`. - **Storage integration** (`custom_components/houseplan/`, Python) — the Home Assistant backend. - **Demo harness** (`demo/`) — a self-contained Playwright page (`demo/srv/demo.html`) that renders the card against a fake `hass`, used for screenshots and the `smoke_*.mjs` end-to-end suite. @@ -93,7 +94,7 @@ Start with the spec?" is the correct answer, not a smaller patch. | **A — product** | `src/**`, `custom_components/houseplan/**/*.py`, `manifest.json`, `hacs.json`, i18n, `custom_components/**/translations/**` | yes | | **B — gates and tooling** | `test/**`, `tests_backend/**`, `demo/**`, `scripts/**`, `.github/workflows/**`, `rollup.config.mjs`, `tsconfig*.json` | yes; may reuse the issue it covers | | **C — documentation** | `docs/**`, `README*`, `CHANGELOG*`, `AGENTS.md` | not if it is part of its issue's DoD | -| **D — generated** | `dist/**`, `custom_components/houseplan/frontend/**`, `demo/golden/baselines/**` | never changes on its own. The stand copy `demo/srv/assets/houseplan-card.js` is no longer committed (#255): build it with `npm run bundle:sync` | +| **D — generated** | `dist/**`, `custom_components/houseplan/frontend/**`, `demo/golden/baselines/**` | never changes on its own. The stand copy `demo/srv/assets/**` is no longer committed (#255): build the complete tree with `npm run bundle:sync` | The table above is a summary; `PROCESS.md` §1 is the authority and now covers the configuration files this one omits — `package.json`, `package-lock.json`, @@ -325,12 +326,12 @@ npm run inventory # the only correct way to get test counts Never copy test counts into documents by hand; they go stale in days. -After building, keep all three bundle snapshots in sync — CI compares them -byte-for-byte: +After building, keep the complete manifest-driven bundle trees in sync — CI +verifies every listed file byte-for-byte: ``` -cp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js npm run bundle:sync # dist → custom_components + demo/srv/assets (#255) +npm run bundle:budget # initial View graph <= 256000 B gzip (#337) ``` During the implementation cycle the fast gates always run. Since 2026-08-14 the diff --git a/README.md b/README.md index ad0018a5..d488e27f 100755 --- a/README.md +++ b/README.md @@ -108,8 +108,10 @@ serve that path as a JavaScript module. ### Manual installation -Copy `custom_components/houseplan` to `config/custom_components`, restart Home -Assistant, and add the House Plan integration. +Copy the complete `custom_components/houseplan` release folder to +`config/custom_components`, restart Home Assistant, and add the House Plan +integration. Do not copy only `houseplan-card.js`: the card also uses an +internal manifest and content-hashed modules from the same release. ### Add the card diff --git a/README.ru.md b/README.ru.md index bd6723aa..8d8fe7cf 100644 --- a/README.ru.md +++ b/README.ru.md @@ -112,8 +112,10 @@ resources: ### Вручную -Скопируйте `custom_components/houseplan` в `config/custom_components`, -перезапустите Home Assistant и добавьте интеграцию House Plan. +Скопируйте всю папку `custom_components/houseplan` из релиза в +`config/custom_components`, перезапустите Home Assistant и добавьте интеграцию +House Plan. Одного `houseplan-card.js` недостаточно: карточке также нужны +внутренний манифест и хешированные модули из того же релиза. ### Добавление карточки diff --git a/custom_components/houseplan/__init__.py b/custom_components/houseplan/__init__.py index b3e32251..874a1e92 100755 --- a/custom_components/houseplan/__init__.py +++ b/custom_components/houseplan/__init__.py @@ -39,10 +39,12 @@ async def async_setup(hass: HomeAssistant, config) -> bool: hass.data.setdefault(DOMAIN, {}) hp_ws.async_register(hass) from .http_api import HouseplanContentView, HouseplanImportPreviewView, HouseplanUploadView + from .frontend_assets import HouseplanFrontendAssetView hass.http.register_view(HouseplanUploadView()) hass.http.register_view(HouseplanContentView()) hass.http.register_view(HouseplanImportPreviewView()) + hass.http.register_view(HouseplanFrontendAssetView()) return True @@ -98,7 +100,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: HouseplanConfigEntry) -> static_paths.append(StaticPathConfig(FRONTEND_URL, str(card_path), cache_headers=False)) # NOTE (audit B1): plans and marker files are NO LONGER static. # They are served by HouseplanContentView, which requires auth. - # Only the card bundle stays public — Lovelace resources must be. + # Only the entry and manifest-gated JS chunks stay public — + # Lovelace modules must be loadable without an auth header. if static_paths: await hass.http.async_register_static_paths(static_paths) except ImportError: # very old HA versions diff --git a/custom_components/houseplan/const.py b/custom_components/houseplan/const.py index 4636b5c9..0e8ae3ad 100755 --- a/custom_components/houseplan/const.py +++ b/custom_components/houseplan/const.py @@ -7,6 +7,7 @@ STORAGE_VIRTUAL_LIGHTS_KEY = f"{DOMAIN}.virtual_lights" STORAGE_VERSION = 1 STORAGE_MINOR_VERSION = 2 FRONTEND_URL = "/houseplan_files/houseplan-card.js" +FRONTEND_ASSETS_URL = "/houseplan_files/houseplan-assets" PLANS_URL = "/houseplan_files/plans" PLANS_DIR = "houseplan/plans" # relative to the HA configuration directory FILES_URL = "/houseplan_files/files" diff --git a/custom_components/houseplan/frontend/houseplan-assets.json b/custom_components/houseplan/frontend/houseplan-assets.json new file mode 100644 index 00000000..787d910d --- /dev/null +++ b/custom_components/houseplan/frontend/houseplan-assets.json @@ -0,0 +1,98 @@ +{ + "schema": 1, + "fingerprint": "f111431804c4c42c6e35286f29720d089bc8b77ffc364a6517f7a3510fcb11c8", + "entry": "houseplan-card.js", + "initialViewFiles": [ + "houseplan-assets/houseplan-card-Cs2QHIcW.js", + "houseplan-card.js" + ], + "initialViewGzipBytes": 255385, + "lazyFiles": [ + "houseplan-assets/editor-BC5jpMNv.js", + "houseplan-assets/houseplan-editor-runtime-DUuA4F8c.js", + "houseplan-assets/houseplan-onboarding-runtime-Bj55plb1.js", + "houseplan-assets/space-deletion-CJ8Co_o8.js" + ], + "lazyGzipBytes": 138414, + "lazyEditorFiles": [ + "houseplan-assets/editor-BC5jpMNv.js", + "houseplan-assets/houseplan-editor-runtime-DUuA4F8c.js", + "houseplan-assets/space-deletion-CJ8Co_o8.js" + ], + "lazyEditorGzipBytes": 131768, + "lazyOnboardingFiles": [ + "houseplan-assets/houseplan-onboarding-runtime-Bj55plb1.js", + "houseplan-assets/space-deletion-CJ8Co_o8.js" + ], + "lazyOnboardingGzipBytes": 7200, + "files": [ + { + "path": "houseplan-assets/editor-BC5jpMNv.js", + "sha256": "f10e45b5a98ab0efc6ceb3c4e08acc4f57a9cb5f99d178b723077a2cb011e05b", + "rawBytes": 3699, + "gzipBytes": 1534, + "isEntry": false, + "imports": [ + "houseplan-assets/houseplan-card-Cs2QHIcW.js" + ], + "dynamicImports": [] + }, + { + "path": "houseplan-assets/houseplan-card-Cs2QHIcW.js", + "sha256": "caa02ba0654e191f72d9b7c9b139c078bb6edb84df4b3e2ec263be492009ecd2", + "rawBytes": 908483, + "gzipBytes": 255201, + "isEntry": false, + "imports": [], + "dynamicImports": [ + "houseplan-assets/editor-BC5jpMNv.js", + "houseplan-assets/houseplan-editor-runtime-DUuA4F8c.js", + "houseplan-assets/houseplan-onboarding-runtime-Bj55plb1.js" + ] + }, + { + "path": "houseplan-assets/houseplan-editor-runtime-DUuA4F8c.js", + "sha256": "0e053b445f87c6c7fdb2b681863e148976ab703221f4988d93f80ca825bd4051", + "rawBytes": 504648, + "gzipBytes": 129680, + "isEntry": false, + "imports": [ + "houseplan-assets/houseplan-card-Cs2QHIcW.js", + "houseplan-assets/space-deletion-CJ8Co_o8.js" + ], + "dynamicImports": [] + }, + { + "path": "houseplan-assets/houseplan-onboarding-runtime-Bj55plb1.js", + "sha256": "32b8812031403d65f909ca236f431a3eeefc9bde9a07499ee123925331f75982", + "rawBytes": 26356, + "gzipBytes": 6646, + "isEntry": false, + "imports": [ + "houseplan-assets/houseplan-card-Cs2QHIcW.js", + "houseplan-assets/space-deletion-CJ8Co_o8.js" + ], + "dynamicImports": [] + }, + { + "path": "houseplan-assets/space-deletion-CJ8Co_o8.js", + "sha256": "59ab0af663f25f859d7bbe3a8893cd00757a40117ca4b9e08d6846f0b2dfd3fc", + "rawBytes": 911, + "gzipBytes": 554, + "isEntry": false, + "imports": [], + "dynamicImports": [] + }, + { + "path": "houseplan-card.js", + "sha256": "53623e0fa5ef93003fc02a66b9932d847c8c68b5345233f9ed6c61f1e470d86a", + "rawBytes": 190, + "gzipBytes": 184, + "isEntry": true, + "imports": [ + "houseplan-assets/houseplan-card-Cs2QHIcW.js" + ], + "dynamicImports": [] + } + ] +} diff --git a/custom_components/houseplan/frontend/houseplan-assets/editor-BC5jpMNv.js b/custom_components/houseplan/frontend/houseplan-assets/editor-BC5jpMNv.js new file mode 100644 index 00000000..8d27e2b9 --- /dev/null +++ b/custom_components/houseplan/frontend/houseplan-assets/editor-BC5jpMNv.js @@ -0,0 +1,14 @@ +globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="f111431804c4c42c6e35286f29720d089bc8b77ffc364a6517f7a3510fcb11c8";import{c as e,l as o,t,d2 as s,A as a,b as l}from"./houseplan-card-Cs2QHIcW.js";class i 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")}],l=this._floorToken;l&&a.push({value:l,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const i="string"==typeof this._config?.floor?this._config.floor:"";i&&!e.some(e=>e.value===i)&&a.push({value:i,label:i}),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;this._loadSpaces();const e=this._lang,o={title:t(e,"editor.title"),floor:t(e,"editor.floor"),default_floor:t(e,"editor.default_floor"),language:t(e,"editor.language"),icon_size:t(e,"editor.icon_size"),show_temperature:t(e,"editor.show_temperature"),live_states:t(e,"editor.live_states"),show_signal:t(e,"editor.show_signal"),kiosk:t(e,"editor.kiosk"),cycle:t(e,"editor.cycle")},s=this._schema,i=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),n=e=>l`o[e.name]||e.name} + @value-changed=${this._valueChanged} + >`;return l` + ${n(s.slice(0,3))} + ${i?l``:a} + ${n(s.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)}}i.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",i); diff --git a/custom_components/houseplan/frontend/houseplan-assets/houseplan-card-Cs2QHIcW.js b/custom_components/houseplan/frontend/houseplan-assets/houseplan-card-Cs2QHIcW.js new file mode 100644 index 00000000..54fcf18d --- /dev/null +++ b/custom_components/houseplan/frontend/houseplan-assets/houseplan-card-Cs2QHIcW.js @@ -0,0 +1,5221 @@ +globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="f111431804c4c42c6e35286f29720d089bc8b77ffc364a6517f7a3510fcb11c8";const e=globalThis,t=e.ShadowRoot&&(void 0===e.ShadyCSS||e.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,i=Symbol(),n=new WeakMap;let r=class{constructor(e,t,n){if(this._$cssResult$=!0,n!==i)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=t}get styleSheet(){let e=this.o;const i=this.t;if(t&&void 0===e){const t=void 0!==i&&1===i.length;t&&(e=n.get(i)),void 0===e&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),t&&n.set(i,e))}return e}toString(){return this.cssText}};const o=(e,...t)=>{const n=1===e.length?e[0]:t.reduce((t,i,n)=>t+(e=>{if(!0===e._$cssResult$)return e.cssText;if("number"==typeof e)return e;throw Error("Value passed to 'css' function must be a 'css' function result: "+e+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+e[n+1],e[0]);return new r(n,e,i)},s=t?e=>e:e=>e instanceof CSSStyleSheet?(e=>{let t="";for(const i of e.cssRules)t+=i.cssText;return(e=>new r("string"==typeof e?e:e+"",void 0,i))(t)})(e):e,{is:a,defineProperty:l,getOwnPropertyDescriptor:c,getOwnPropertyNames:h,getOwnPropertySymbols:d,getPrototypeOf:u}=Object,p=globalThis,_=p.trustedTypes,m=_?_.emptyScript:"",g=p.reactiveElementPolyfillSupport,f=(e,t)=>e,v={toAttribute(e,t){switch(t){case Boolean:e=e?m:null;break;case Object:case Array:e=null==e?e:JSON.stringify(e)}return e},fromAttribute(e,t){let i=e;switch(t){case Boolean:i=null!==e;break;case Number:i=null===e?null:Number(e);break;case Object:case Array:try{i=JSON.parse(e)}catch(e){i=null}}return i}},y=(e,t)=>!a(e,t),b={attribute:!0,type:String,converter:v,reflect:!1,useDefault:!1,hasChanged:y};Symbol.metadata??=Symbol("metadata"),p.litPropertyMetadata??=new WeakMap;let w=class extends HTMLElement{static addInitializer(e){this._$Ei(),(this.l??=[]).push(e)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(e,t=b){if(t.state&&(t.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(e)&&((t=Object.create(t)).wrapped=!0),this.elementProperties.set(e,t),!t.noAccessor){const i=Symbol(),n=this.getPropertyDescriptor(e,i,t);void 0!==n&&l(this.prototype,e,n)}}static getPropertyDescriptor(e,t,i){const{get:n,set:r}=c(this.prototype,e)??{get(){return this[t]},set(e){this[t]=e}};return{get:n,set(t){const o=n?.call(this);r?.call(this,t),this.requestUpdate(e,o,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this.elementProperties.get(e)??b}static _$Ei(){if(this.hasOwnProperty(f("elementProperties")))return;const e=u(this);e.finalize(),void 0!==e.l&&(this.l=[...e.l]),this.elementProperties=new Map(e.elementProperties)}static finalize(){if(this.hasOwnProperty(f("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(f("properties"))){const e=this.properties,t=[...h(e),...d(e)];for(const i of t)this.createProperty(i,e[i])}const e=this[Symbol.metadata];if(null!==e){const t=litPropertyMetadata.get(e);if(void 0!==t)for(const[e,i]of t)this.elementProperties.set(e,i)}this._$Eh=new Map;for(const[e,t]of this.elementProperties){const i=this._$Eu(e,t);void 0!==i&&this._$Eh.set(i,e)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(e){const t=[];if(Array.isArray(e)){const i=new Set(e.flat(1/0).reverse());for(const e of i)t.unshift(s(e))}else void 0!==e&&t.push(s(e));return t}static _$Eu(e,t){const i=t.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof e?e.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(e=>this.enableUpdating=e),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(e=>e(this))}addController(e){(this._$EO??=new Set).add(e),void 0!==this.renderRoot&&this.isConnected&&e.hostConnected?.()}removeController(e){this._$EO?.delete(e)}_$E_(){const e=new Map,t=this.constructor.elementProperties;for(const i of t.keys())this.hasOwnProperty(i)&&(e.set(i,this[i]),delete this[i]);e.size>0&&(this._$Ep=e)}createRenderRoot(){const i=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return((i,n)=>{if(t)i.adoptedStyleSheets=n.map(e=>e instanceof CSSStyleSheet?e:e.styleSheet);else for(const t of n){const n=document.createElement("style"),r=e.litNonce;void 0!==r&&n.setAttribute("nonce",r),n.textContent=t.cssText,i.appendChild(n)}})(i,this.constructor.elementStyles),i}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(e=>e.hostConnected?.())}enableUpdating(e){}disconnectedCallback(){this._$EO?.forEach(e=>e.hostDisconnected?.())}attributeChangedCallback(e,t,i){this._$AK(e,i)}_$ET(e,t){const i=this.constructor.elementProperties.get(e),n=this.constructor._$Eu(e,i);if(void 0!==n&&!0===i.reflect){const r=(void 0!==i.converter?.toAttribute?i.converter:v).toAttribute(t,i.type);this._$Em=e,null==r?this.removeAttribute(n):this.setAttribute(n,r),this._$Em=null}}_$AK(e,t){const i=this.constructor,n=i._$Eh.get(e);if(void 0!==n&&this._$Em!==n){const e=i.getPropertyOptions(n),r="function"==typeof e.converter?{fromAttribute:e.converter}:void 0!==e.converter?.fromAttribute?e.converter:v;this._$Em=n;const o=r.fromAttribute(t,e.type);this[n]=o??this._$Ej?.get(n)??o,this._$Em=null}}requestUpdate(e,t,i,n=!1,r){if(void 0!==e){const o=this.constructor;if(!1===n&&(r=this[e]),i??=o.getPropertyOptions(e),!((i.hasChanged??y)(r,t)||i.useDefault&&i.reflect&&r===this._$Ej?.get(e)&&!this.hasAttribute(o._$Eu(e,i))))return;this.C(e,t,i)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}C(e,t,{useDefault:i,reflect:n,wrapped:r},o){i&&!(this._$Ej??=new Map).has(e)&&(this._$Ej.set(e,o??t??this[e]),!0!==r||void 0!==o)||(this._$AL.has(e)||(this.hasUpdated||i||(t=void 0),this._$AL.set(e,t)),!0===n&&this._$Em!==e&&(this._$Eq??=new Set).add(e))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(e){Promise.reject(e)}const e=this.scheduleUpdate();return null!=e&&await e,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[e,t]of this._$Ep)this[e]=t;this._$Ep=void 0}const e=this.constructor.elementProperties;if(e.size>0)for(const[t,i]of e){const{wrapped:e}=i,n=this[t];!0!==e||this._$AL.has(t)||void 0===n||this.C(t,void 0,i,n)}}let e=!1;const t=this._$AL;try{e=this.shouldUpdate(t),e?(this.willUpdate(t),this._$EO?.forEach(e=>e.hostUpdate?.()),this.update(t)):this._$EM()}catch(t){throw e=!1,this._$EM(),t}e&&this._$AE(t)}willUpdate(e){}_$AE(e){this._$EO?.forEach(e=>e.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(e)),this.updated(e)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(e){return!0}update(e){this._$Eq&&=this._$Eq.forEach(e=>this._$ET(e,this[e])),this._$EM()}updated(e){}firstUpdated(e){}};w.elementStyles=[],w.shadowRootOptions={mode:"open"},w[f("elementProperties")]=new Map,w[f("finalized")]=new Map,g?.({ReactiveElement:w}),(p.reactiveElementVersions??=[]).push("2.1.2");const k=globalThis,x=e=>e,$=k.trustedTypes,S=$?$.createPolicy("lit-html",{createHTML:e=>e}):void 0,M="$lit$",T=`lit$${Math.random().toFixed(9).slice(2)}$`,R="?"+T,C=`<${R}>`,D=document,A=()=>D.createComment(""),O=e=>null===e||"object"!=typeof e&&"function"!=typeof e,z=Array.isArray,P="[ \t\n\f\r]",F=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,I=/-->/g,E=/>/g,N=RegExp(`>|${P}(?:([^\\s"'>=/]+)(${P}*=${P}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),H=/'/g,L=/"/g,j=/^(?:script|style|textarea|title)$/i,q=e=>(t,...i)=>({_$litType$:e,strings:t,values:i}),B=q(1),W=q(2),U=Symbol.for("lit-noChange"),G=Symbol.for("lit-nothing"),V=new WeakMap,K=D.createTreeWalker(D,129);function Y(e,t){if(!z(e)||!e.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==S?S.createHTML(t):t}const Z=(e,t)=>{const i=e.length-1,n=[];let r,o=2===t?"":3===t?"":"",s=F;for(let t=0;t"===l[0]?(s=r??F,c=-1):void 0===l[1]?c=-2:(c=s.lastIndex-l[2].length,a=l[1],s=void 0===l[3]?N:'"'===l[3]?L:H):s===L||s===H?s=N:s===I||s===E?s=F:(s=N,r=void 0);const d=s===N&&e[t+1].startsWith("/>")?" ":"";o+=s===F?i+C:c>=0?(n.push(a),i.slice(0,c)+M+i.slice(c)+T+d):i+T+(-2===c?t:d)}return[Y(e,o+(e[i]||"")+(2===t?"":3===t?"":"")),n]};class X{constructor({strings:e,_$litType$:t},i){let n;this.parts=[];let r=0,o=0;const s=e.length-1,a=this.parts,[l,c]=Z(e,t);if(this.el=X.createElement(l,i),K.currentNode=this.el.content,2===t||3===t){const e=this.el.content.firstChild;e.replaceWith(...e.childNodes)}for(;null!==(n=K.nextNode())&&a.length0){n.textContent=$?$.emptyScript:"";for(let i=0;iz(e)||"function"==typeof e?.[Symbol.iterator])(e)?this.k(e):this._(e)}O(e){return this._$AA.parentNode.insertBefore(e,this._$AB)}T(e){this._$AH!==e&&(this._$AR(),this._$AH=this.O(e))}_(e){this._$AH!==G&&O(this._$AH)?this._$AA.nextSibling.data=e:this.T(D.createTextNode(e)),this._$AH=e}$(e){const{values:t,_$litType$:i}=e,n="number"==typeof i?this._$AC(e):(void 0===i.el&&(i.el=X.createElement(Y(i.h,i.h[0]),this.options)),i);if(this._$AH?._$AD===n)this._$AH.p(t);else{const e=new Q(n,this),i=e.u(this.options);e.p(t),this.T(i),this._$AH=e}}_$AC(e){let t=V.get(e.strings);return void 0===t&&V.set(e.strings,t=new X(e)),t}k(e){z(this._$AH)||(this._$AH=[],this._$AR());const t=this._$AH;let i,n=0;for(const r of e)n===t.length?t.push(i=new ee(this.O(A()),this.O(A()),this,this.options)):i=t[n],i._$AI(r),n++;n2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=G}_$AI(e,t=this,i,n){const r=this.strings;let o=!1;if(void 0===r)e=J(this,e,t,0),o=!O(e)||e!==this._$AH&&e!==U,o&&(this._$AH=e);else{const n=e;let s,a;for(e=r[0],s=0;s{const n=i?.renderBefore??t;let r=n._$litPart$;if(void 0===r){const e=i?.renderBefore??null;n._$litPart$=r=new ee(t.insertBefore(A(),e),e,void 0,i??{})}return r._$AI(e),r},ce=globalThis;let he=class extends w{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){const e=super.createRenderRoot();return this.renderOptions.renderBefore??=e.firstChild,e}update(e){const t=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(e),this._$Do=le(t,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return U}};he._$litElement$=!0,he.finalized=!0,ce.litElementHydrateSupport?.({LitElement:he});const de=ce.litElementPolyfillSupport;de?.({LitElement:he}),(ce.litElementVersions??=[]).push("4.2.2");const ue=2,pe=e=>(...t)=>({_$litDirective$:e,values:t});let _e=class{constructor(e){}get _$AU(){return this._$AM._$AU}_$AT(e,t,i){this._$Ct=e,this._$AM=t,this._$Ci=i}_$AS(e,t){return this.update(e,t)}update(e,t){return this.render(...t)}};const{I:me}=se,ge=e=>e,fe=()=>document.createComment(""),ve=(e,t,i)=>{const n=e._$AA.parentNode,r=void 0===t?e._$AB:t._$AA;if(void 0===i){const t=n.insertBefore(fe(),r),o=n.insertBefore(fe(),r);i=new me(t,o,e,e.options)}else{const t=i._$AB.nextSibling,o=i._$AM,s=o!==e;if(s){let t;i._$AQ?.(e),i._$AM=e,void 0!==i._$AP&&(t=e._$AU)!==o._$AU&&i._$AP(t)}if(t!==r||s){let e=i._$AA;for(;e!==t;){const t=ge(e).nextSibling;ge(n).insertBefore(e,r),e=t}}}return i},ye=(e,t,i=e)=>(e._$AI(t,i),e),be={},we=(e,t=be)=>e._$AH=t,ke=e=>{e._$AR(),e._$AA.remove()},xe=(e,t,i)=>{const n=new Map;for(let r=t;r<=i;r++)n.set(e[r],r);return n},$e=pe(class extends _e{constructor(e){if(super(e),e.type!==ue)throw Error("repeat() can only be used in text expressions")}dt(e,t,i){let n;void 0===i?i=t:void 0!==t&&(n=t);const r=[],o=[];let s=0;for(const t of e)r[s]=n?n(t,s):s,o[s]=i(t,s),s++;return{values:o,keys:r}}render(e,t,i){return this.dt(e,t,i).values}update(e,[t,i,n]){const r=(e=>e._$AH)(e),{values:o,keys:s}=this.dt(t,i,n);if(!Array.isArray(r))return this.ut=s,o;const a=this.ut??=[],l=[];let c,h,d=0,u=r.length-1,p=0,_=o.length-1;for(;d<=u&&p<=_;)if(null===r[d])d++;else if(null===r[u])u--;else if(a[d]===s[p])l[p]=ye(r[d],o[p]),d++,p++;else if(a[u]===s[_])l[_]=ye(r[u],o[_]),u--,_--;else if(a[d]===s[_])l[_]=ye(r[d],o[_]),ve(e,l[_+1],r[d]),d++,_--;else if(a[u]===s[p])l[p]=ye(r[u],o[p]),ve(e,r[d],r[u]),u--,p++;else if(void 0===c&&(c=xe(s,p,_),h=xe(a,d,u)),c.has(a[d]))if(c.has(a[u])){const t=h.get(s[p]),i=void 0!==t?r[t]:null;if(null===i){const t=ve(e,r[d]);ye(t,o[p]),l[p]=t}else l[p]=ye(i,o[p]),ve(e,r[d],i),r[t]=null;p++}else ke(r[u]),u--;else ke(r[d]),d++;for(;p<=_;){const t=ve(e,l[_+1]);ye(t,o[p]),l[p++]=t}for(;d<=u;){const e=r[d++];null!==e&&ke(e)}return this.ut=s,we(e,l),U}}),Se=new WeakMap;let Me=0;class Te extends he{constructor(){super(...arguments),this.title="",this.icon="",this.wide=!1,this.dismissOnScrim=!1,this.hass=null,this._opener=null,this._focusRoot=null,this._useHaDialog=!1,this._closing=!1,this._overlays=[],this._titleId="hp-dialog-title-"+ ++Me,this._focusInitial=()=>{const e=this._focusableElements(),t=e.find(e=>e.hasAttribute("autofocus"))||e[0]||(this._useHaDialog?null:this.renderRoot.querySelector(".close"))||this.renderRoot.querySelector(".surface")||this.renderRoot.querySelector("ha-dialog");t?.focus({preventScroll:!0})},this._requestClose=()=>{this._closing||(this._closing=!0,this.dispatchEvent(new CustomEvent("hp-close",{bubbles:!0,composed:!0})))},this._onKeyDown=e=>{if("Escape"===e.key){e.preventDefault(),e.stopImmediatePropagation(),this._pruneOverlays();const t=this._overlays[this._overlays.length-1];return t?void this._closeOverlay(t,"escape"):void this._requestClose()}if("Tab"!==e.key||this._useHaDialog)return;const t=this.renderRoot.querySelector(".close"),i=t?[t,...this._focusableElements()]:this._focusableElements();if(!i.length)return e.preventDefault(),void this.renderRoot.querySelector(".surface")?.focus({preventScroll:!0});const n=this._deepActiveElement(),r=i[0],o=i[i.length-1];!e.shiftKey||n!==r&&i.includes(n)?e.shiftKey||n!==o||(e.preventDefault(),r.focus()):(e.preventDefault(),o.focus())},this._onFallbackCancel=e=>{e.preventDefault(),this._requestClose()},this._onFallbackClick=e=>{this.dismissOnScrim&&e.target===e.currentTarget&&this._requestClose()}}connectedCallback(){super.connectedCallback(),this._opener=this._deepActiveElement();const e=this.getRootNode();this._focusRoot=e;const t=Se.get(e)||{dialogs:new Set,opener:this._opener};t.dialogs.add(this),Se.set(e,t),this._useHaDialog=!!customElements.get("ha-dialog"),this.addEventListener("keydown",this._onKeyDown,!0)}disconnectedCallback(){this.removeEventListener("keydown",this._onKeyDown,!0);const e=[...this._overlays];this._overlays=[];for(const t of e.reverse())t.close("disconnect");const t=this._focusRoot,i=this._opener;this._opener=null,this._focusRoot=null;const n=t?Se.get(t):void 0;n?.dialogs.delete(this),super.disconnectedCallback(),t&&n&&requestAnimationFrame(()=>{const e=Se.get(t);if(!e)return;if(!e.dialogs.size)return e.opener?.isConnected&&e.opener.focus({preventScroll:!0}),void Se.delete(t);const n=i?.closest("hp-dialog");i?.isConnected&&n&&e.dialogs.has(n)&&i.focus({preventScroll:!0})})}firstUpdated(e){if(super.firstUpdated(e),!this._useHaDialog){const e=this.renderRoot.querySelector("dialog");e&&!e.open&&e.showModal()}queueMicrotask(()=>this._focusInitial())}_deepActiveElement(){let e=document.activeElement;for(;e?.shadowRoot?.activeElement;)e=e.shadowRoot.activeElement;return e&&e!==document.body?e:null}_focusableElements(){const e=["[autofocus]","a[href]","button:not([disabled])",'input:not([disabled]):not([type="hidden"])',"select:not([disabled])","textarea:not([disabled])",'[contenteditable="true"]','[tabindex]:not([tabindex="-1"])'].join(","),t=[],i=new Set,n=r=>{if(!i.has(r)){if(i.add(r),r instanceof HTMLElement){if(r.matches(e)&&t.push(r),r instanceof HTMLSlotElement){for(const e of r.assignedNodes({flatten:!0}))n(e);return}if(r.shadowRoot){for(const e of r.shadowRoot.childNodes)n(e);return}}for(const e of r.childNodes)n(e)}};for(const e of this.childNodes)n(e);const r=this.overlayPortal();return r&&n(r),t.filter(e=>{let t=e;for(;t;){const e=getComputedStyle(t);if(t.hidden||t.inert||"true"===t.getAttribute("aria-hidden")||"none"===e.display||"hidden"===e.visibility)return!1;if(t=t.assignedSlot||t.parentElement||(t.getRootNode()instanceof ShadowRoot?t.getRootNode().host:null),t===this)break}return!0})}_pruneOverlays(){this._overlays=this._overlays.filter(e=>e.owner.isConnected)}_closeOverlay(e,t){const i=this._overlays.findIndex(t=>t.token===e.token);i>=0&&this._overlays.splice(i,1),e.close(t)}registerOverlay(e){this._pruneOverlays();const t=this._overlays.find(t=>t.owner===e.owner);t&&this._overlays.splice(this._overlays.indexOf(t),1);const i=e.group||"transient";for(const e of[...this._overlays].reverse())e.group===i&&this._closeOverlay(e,"exclusive");const n={...e,group:i,token:Symbol("hp-overlay")};this._overlays.push(n);let r=!1;return()=>{if(r)return;r=!0;const e=this._overlays.findIndex(e=>e.token===n.token);e>=0&&this._overlays.splice(e,1)}}closeTransientOverlays(e="outside"){this._pruneOverlays();const t=[...this._overlays].filter(e=>"transient"===(e.group||"transient"));for(const i of t.reverse())this._closeOverlay(i,e);return t.length>0}overlayPortal(){return this.renderRoot.querySelector(".overlay-portal")}render(){const e=B` + ${this.icon?B``:G} + ${this.title} + `;return this._useHaDialog?B` + ${e} + + +
`:B` +
+
+ ${e} + +
+
+ +
+
+
`}}Te.properties={title:{type:String},icon:{type:String},wide:{type:Boolean,reflect:!0},dismissOnScrim:{type:Boolean,attribute:"dismiss-on-scrim"},hass:{attribute:!1}},Te.styles=o` + :host { + display: contents; + color: var(--primary-text-color, #e6e7eb); + font: inherit; + } + + ha-dialog { + --dialog-content-padding: 0; + --dialog-surface-background: var(--card-background-color, var(--hp-bg, #202126)); + --ha-dialog-border-radius: var(--rad-l, 18px); + /* HA's ha-dialog-header defaults this custom property to a one-line + fixed height. Our localized slot is intentionally allowed to wrap, + so leaving that default in place clips every line after the first at + the bottom of the header. auto is HA's public sizing hook and also + stays harmless on older ha-dialog implementations that do not consume + it. */ + --ha-dialog-header-title-height: auto; + color: inherit; + } + + ha-dialog::part(dialog) { + border: 1px solid var(--hp-accent, #d89300); + box-shadow: var(--shadow-3, 0 18px 48px rgb(0 0 0 / 0.34)); + overflow: hidden; + } + + /* The HA header slot is a flex item with a constrained inline size. The + old inline-flex title kept its min-content width, so HA clipped the last + word instead of giving it a second line. Keep every wrapper shrinkable + and let the text wrap; this applies to every hp-dialog, including long + device names and translated titles. */ + .header-title-slot { + display: block; + flex: 1 1 auto; + width: 100%; + max-width: 100%; + min-width: 0; + box-sizing: border-box; + white-space: normal; + } + + .title { + display: flex; + flex: 1 1 auto; + align-items: center; + gap: var(--sp-4, 12px); + width: 100%; + max-width: 100%; + min-width: 0; + font-weight: 600; + line-height: 1.25; + white-space: normal; + } + + .title ha-icon { + flex: none; + color: var(--hp-accent, #d89300); + } + + .title-text { + flex: 1 1 auto; + min-width: 0; + max-width: 100%; + white-space: normal; + overflow-wrap: anywhere; + word-break: normal; + } + + .footer { + /* ha-dialog lays out its footer slot as a flex row. display: contents + exposed the consumer's action row as a shrink-to-fit flex item, so a + wide device dialog got a half-width divider and its Hide action slid + toward the centre. Keep one full-width slot item in both HA and the + native fallback. */ + display: block; + width: 100%; + min-width: 0; + box-sizing: border-box; + } + + ::slotted([slot='footer']) { + width: 100%; + box-sizing: border-box; + } + + dialog { + width: auto; + max-width: none; + max-height: none; + margin: auto; + padding: 0; + border: 0; + overflow: visible; + color: inherit; + background: transparent; + } + + dialog::backdrop { + background: rgb(0 0 0 / 0.45); + } + + .surface { + width: min(360px, 92vw); + max-height: 92vh; + display: flex; + flex-direction: column; + overflow: hidden; + box-sizing: border-box; + color: inherit; + background: var(--card-background-color, var(--hp-bg, #202126)); + border: 1px solid var(--hp-accent, #d89300); + border-radius: var(--rad-l, 18px); + box-shadow: var(--shadow-3, 0 18px 48px rgb(0 0 0 / 0.34)); + } + + :host([wide]) .surface { + width: min(var(--hp-dialog-wide-width, 500px), 94vw); + } + + .header { + min-height: 56px; + display: flex; + align-items: center; + gap: var(--sp-4, 12px); + padding: var(--sp-4, 12px) var(--sp-5, 16px); + box-sizing: border-box; + border-bottom: 1px solid var(--hp-line, rgb(255 255 255 / 0.12)); + } + + .header .title { + flex: 1; + } + + .close { + width: 40px; + height: 40px; + flex: none; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + color: inherit; + background: transparent; + border: 0; + border-radius: 50%; + cursor: pointer; + } + + :host([data-pointer-hover]) .close:hover, + .close:focus-visible { + background: rgb(127 127 127 / 0.16); + } + + .content { + min-height: 0; + display: flex; + flex-direction: column; + } + + .overlay-portal { + position: fixed; + z-index: 2147483647; + inset: 0; + overflow: visible; + pointer-events: none; + } + + .overlay-portal:empty { + display: none; + } + + .overlay-portal > * { + pointer-events: auto; + } + `,customElements.get("hp-dialog")||customElements.define("hp-dialog",Te);const Re=new Set(["hacs","sun","backup","hassio","met","telegram_bot","mobile_app","systemmonitor","better_thermostat","adaptive_lighting","yandex_pogoda","upnp_serial_number"]),Ce=[{pattern:"протечк|leak|water sensor",icon:"mdi:water-alert"},{pattern:"клапан|valve",icon:"mdi:pipe-valve"},{pattern:"дым|smoke",icon:"mdi:smoke-detector"},{pattern:"термоголов|trv|radiator",icon:"mdi:radiator"},{pattern:"чайник|kettle|термопот",icon:"mdi:kettle"},{pattern:"сауна|sauna|harvia|парная|парилк",icon:"mdi:hot-tub"},{pattern:"температ|temperature|thermometer|climate sensor",icon:"mdi:thermometer"},{pattern:"qingping|air monitor|молекул|air quality",icon:"mdi:air-filter"},{pattern:"штор|curtain|blind|shade",icon:"mdi:roller-shade"},{pattern:"розетк|plug|socket|outlet",icon:"mdi:power-socket-de"},{pattern:"выключат|switch",icon:"mdi:light-switch"},{pattern:"лампа|лампочк|bulb|gx53|светильник|rgb|lamp|light strip",icon:"mdi:lightbulb"},{pattern:"камер|camera",icon:"mdi:cctv"},{pattern:"замок|ttlock|lock|sn609|sn9161",icon:"mdi:lock"},{pattern:"ворота|garage|gate",icon:"mdi:garage-variant"},{pattern:"калитк|door|открыт|contact",icon:"mdi:door"},{pattern:"счётчик|счетчик|kws|meter",icon:"mdi:meter-electric"},{pattern:"вводный автомат|breaker|wifimcbn",icon:"mdi:electric-switch"},{pattern:"myheat|котёл|котел|boiler|отоплен|heating",icon:"mdi:water-boiler"},{pattern:"холодильник|fridge",icon:"mdi:fridge"},{pattern:"стиральн|washer|washing",icon:"mdi:washing-machine"},{pattern:"сушилк|dryer",icon:"mdi:tumble-dryer"},{pattern:"пылесос|vacuum|dreame|roborock",icon:"mdi:robot-vacuum"},{pattern:"soundbar",icon:"mdi:soundbar"},{pattern:"колонк|станц|speaker|яндекс|yandex|алиса|alice",icon:"mdi:speaker"},{pattern:"tv|телевизор|hyundaitv|mitv|television",icon:"mdi:television"},{pattern:"keenetic|роутер|router|mesh|access point",icon:"mdi:router-wireless"},{pattern:"ибп|ups|kirpich",icon:"mdi:battery-charging-high"},{pattern:"slzb|координат|zigbee|coordinator",icon:"mdi:zigbee"},{pattern:"motion|движен|presence|присутств",icon:"mdi:motion-sensor"},{pattern:"humidity|влажн",icon:"mdi:water-percent"}];function De(e){const t=[];for(const i of e)if(i&&"string"==typeof i.pattern&&i.icon)try{t.push({re:new RegExp(i.pattern,"i"),icon:i.icon})}catch{}return t}function Ae(e){try{return new RegExp(e,"i"),!0}catch{return!1}}const Oe=De(Ce),ze={temperature:"mdi:thermometer",humidity:"mdi:water-percent",motion:"mdi:motion-sensor",occupancy:"mdi:motion-sensor",presence:"mdi:motion-sensor",door:"mdi:door",window:"mdi:window-closed",garage_door:"mdi:garage-variant",smoke:"mdi:smoke-detector",moisture:"mdi:water-alert",gas:"mdi:gas-cylinder",power:"mdi:meter-electric",energy:"mdi:meter-electric",illuminance:"mdi:brightness-5",co2:"mdi:molecule-co2",pm25:"mdi:air-filter",battery:"mdi:battery"},Pe="mdi:chip";function Fe(e,t,i){const n=((e||"")+" "+(t||"")).toLowerCase();for(const{re:e,icon:t}of i??Oe)if(e.test(n))return t;return Pe}const Ie=["light","switch","cover","valve","lock","climate","fan","media_player","camera","vacuum","humidifier","water_heater","alarm_control_panel","sensor","binary_sensor","event","button","number","select","update"];var Ee=/^-?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i,Ne=Math.ceil,He=Math.floor,Le="[BigNumber Error] ",je=Le+"Number primitive has more than 15 significant digits: ",qe=1e14,Be=14,We=9007199254740991,Ue=[1,10,100,1e3,1e4,1e5,1e6,1e7,1e8,1e9,1e10,1e11,1e12,1e13],Ge=1e7,Ve=1e9;function Ke(e){var t=0|e;return e>0||e===t?t:t-1}function Ye(e){for(var t,i,n=1,r=e.length,o=e[0]+"";nc^i?1:-1;for(a=(l=r.length)<(c=o.length)?l:c,s=0;so[s]^i?1:-1;return l==c?0:l>c^i?1:-1}function Xe(e,t,i,n){if(ei||e!==He(e))throw Error(Le+(n||"Argument")+("number"==typeof e?ei?" out of range: ":" not an integer: ":" not a primitive number: ")+String(e))}function Je(e){var t=e.c.length-1;return Ke(e.e/Be)==t&&e.c[t]%2!=0}function Qe(e,t){return(e.length>1?e.charAt(0)+"."+e.slice(1):e)+(t<0?"e":"e+")+t}function et(e,t,i){var n,r;if(t<0){for(r=i+".";++t;r+=i);e=r+e}else if(++t>(n=e.length)){for(r=i,t-=n;--t;r+=i);e+=r}else ty?u.c=u.e=null:e.e=10;l/=10,a++);return void(a>y?u.c=u.e=null:(u.e=a,u.c=[e]))}d=String(e)}else{if(!Ee.test(d=String(e)))return r(u,d,c);u.s=45==d.charCodeAt(0)?(d=d.slice(1),-1):1}(a=d.indexOf("."))>-1&&(d=d.replace(".","")),(l=d.search(/e/i))>0?(a<0&&(a=l),a+=+d.slice(l+1),d=d.substring(0,l)):a<0&&(a=d.length)}else{if(Xe(t,2,$.length,"Base"),10==t&&S)return D(u=new M(e),_+u.e+1,m);if(d=String(e),c="number"==typeof e){if(0*e!=0)return r(u,d,c,t);if(u.s=1/e<0?(d=d.slice(1),-1):1,M.DEBUG&&d.replace(/^0\.0*|\./,"").length>15)throw Error(je+e)}else u.s=45===d.charCodeAt(0)?(d=d.slice(1),-1):1;for(i=$.slice(0,t),a=l=0,h=d.length;la){a=h;continue}}else if(!s&&(d==d.toUpperCase()&&(d=d.toLowerCase())||d==d.toLowerCase()&&(d=d.toUpperCase()))){s=!0,l=-1,a=0;continue}return r(u,String(e),c,t)}c=!1,(a=(d=n(d,t,10,u.s)).indexOf("."))>-1?d=d.replace(".",""):a=d.length}for(l=0;48===d.charCodeAt(l);l++);for(h=d.length;48===d.charCodeAt(--h););if(d=d.slice(l,++h)){if(h-=l,c&&M.DEBUG&&h>15&&(e>We||e!==He(e)))throw Error(je+u.s*e);if((a=a-l-1)>y)u.c=u.e=null;else if(a=f)?Qe(l,s):et(l,s,"0");else if(o=(e=D(new M(e),t,i)).e,a=(l=Ye(e.c)).length,1==n||2==n&&(t<=o||o<=g)){for(;as),l=et(l,o,"0"),o+1>a){if(--t>0)for(l+=".";t--;l+="0");}else if((t+=o-a)>0)for(o+1==a&&(l+=".");t--;l+="0");return e.s<0&&r?"-"+l:l}function R(e,t){for(var i,n,r=1,o=new M(e[0]);r=10;r/=10,n++);return(i=n+i*Be-1)>y?e.c=e.e=null:i=10;a/=10,r++);if((o=t-r)<0)o+=Be,s=t,l=d[c=0],h=He(l/u[r-s-1]%10);else if((c=Ne((o+1)/Be))>=d.length){if(!n)break e;for(;d.length<=c;d.push(0));l=h=0,r=1,s=(o%=Be)-Be+1}else{for(l=a=d[c],r=1;a>=10;a/=10,r++);h=(s=(o%=Be)-Be+r)<0?0:He(l/u[r-s-1]%10)}if(n=n||t<0||null!=d[c+1]||(s<0?l:l%u[r-s-1]),n=i<4?(h||n)&&(0==i||i==(e.s<0?3:2)):h>5||5==h&&(4==i||n||6==i&&(o>0?s>0?l/u[r-s]:0:d[c-1])%10&1||i==(e.s<0?8:7)),t<1||!d[0])return d.length=0,n?(t-=e.e+1,d[0]=u[(Be-t%Be)%Be],e.e=-t||0):d[0]=e.e=0,e;if(0==o?(d.length=c,a=1,c--):(d.length=c+1,a=u[Be-o],d[c]=s>0?He(l/u[r-s]%u[s])*a:0),n)for(;;){if(0==c){for(o=1,s=d[0];s>=10;s/=10,o++);for(s=d[0]+=a,a=1;s>=10;s/=10,a++);o!=a&&(e.e++,d[0]==qe&&(d[0]=1));break}if(d[c]+=a,d[c]!=qe)break;d[c--]=0,a=1}for(o=d.length;0===d[--o];d.pop());}e.e>y?e.c=e.e=null:e.e=f?Qe(t,i):et(t,i,"0"),e.s<0?"-"+t:t)}return M.clone=e,M.ROUND_UP=0,M.ROUND_DOWN=1,M.ROUND_CEIL=2,M.ROUND_FLOOR=3,M.ROUND_HALF_UP=4,M.ROUND_HALF_DOWN=5,M.ROUND_HALF_EVEN=6,M.ROUND_HALF_CEIL=7,M.ROUND_HALF_FLOOR=8,M.EUCLID=9,M.config=M.set=function(e){var t,i;if(null!=e){if("object"!=typeof e)throw Error(Le+"Object expected: "+e);if(e.hasOwnProperty(t="DECIMAL_PLACES")&&(Xe(i=e[t],0,Ve,t),_=i),e.hasOwnProperty(t="ROUNDING_MODE")&&(Xe(i=e[t],0,8,t),m=i),e.hasOwnProperty(t="EXPONENTIAL_AT")&&((i=e[t])&&i.pop?(Xe(i[0],-Ve,0,t),Xe(i[1],0,Ve,t),g=i[0],f=i[1]):(Xe(i,-Ve,Ve,t),g=-(f=i<0?-i:i))),e.hasOwnProperty(t="RANGE"))if((i=e[t])&&i.pop)Xe(i[0],-Ve,-1,t),Xe(i[1],1,Ve,t),v=i[0],y=i[1];else{if(Xe(i,-Ve,Ve,t),!i)throw Error(Le+t+" cannot be zero: "+i);v=-(y=i<0?-i:i)}if(e.hasOwnProperty(t="CRYPTO")){if((i=e[t])!==!!i)throw Error(Le+t+" not true or false: "+i);if(i){if("undefined"==typeof crypto||!crypto||!crypto.getRandomValues&&!crypto.randomBytes)throw b=!i,Error(Le+"crypto unavailable");b=i}else b=i}if(e.hasOwnProperty(t="MODULO_MODE")&&(Xe(i=e[t],0,9,t),w=i),e.hasOwnProperty(t="POW_PRECISION")&&(Xe(i=e[t],0,Ve,t),k=i),e.hasOwnProperty(t="FORMAT")){if("object"!=typeof(i=e[t]))throw Error(Le+t+" not an object: "+i);x=i}if(e.hasOwnProperty(t="ALPHABET")){if("string"!=typeof(i=e[t])||/^.?$|[+\-.\s]|(.).*\1/.test(i))throw Error(Le+t+" invalid: "+i);S="0123456789"==i.slice(0,10),$=i}}return{DECIMAL_PLACES:_,ROUNDING_MODE:m,EXPONENTIAL_AT:[g,f],RANGE:[v,y],CRYPTO:b,MODULO_MODE:w,POW_PRECISION:k,FORMAT:x,ALPHABET:$}},M.isBigNumber=function(e){if(!e||!0!==e._isBigNumber)return!1;if(!M.DEBUG)return!0;var t,i,n=e.c,r=e.e,o=e.s;e:if("[object Array]"=={}.toString.call(n)){if((1===o||-1===o)&&r>=-Ve&&r<=Ve&&r===He(r)){if(0===n[0]){if(0===r&&1===n.length)return!0;break e}if((t=(r+1)%Be)<1&&(t+=Be),String(n[0]).length==t){for(t=0;t=qe||i!==He(i))break e;if(0!==i)return!0}}}else if(null===n&&null===r&&(null===o||1===o||-1===o))return!0;throw Error(Le+"Invalid BigNumber: "+e)},M.maximum=M.max=function(){return R(arguments,-1)},M.minimum=M.min=function(){return R(arguments,1)},M.random=(o=9007199254740992,s=Math.random()*o&2097151?function(){return He(Math.random()*o)}:function(){return 8388608*(1073741824*Math.random()|0)+(8388608*Math.random()|0)},function(e){var t,i,n,r,o,a=0,l=[],c=new M(p);if(null==e?e=_:Xe(e,0,Ve),r=Ne(e/Be),b)if(crypto.getRandomValues){for(t=crypto.getRandomValues(new Uint32Array(r*=2));a>>11))>=9e15?(i=crypto.getRandomValues(new Uint32Array(2)),t[a]=i[0],t[a+1]=i[1]):(l.push(o%1e14),a+=2);a=r/2}else{if(!crypto.randomBytes)throw b=!1,Error(Le+"crypto unavailable");for(t=crypto.randomBytes(r*=7);a=9e15?crypto.randomBytes(7).copy(t,a):(l.push(o%1e14),a+=7);a=r/7}if(!b)for(;a=10;o/=10,a++);ai-1&&(null==s[r+1]&&(s[r+1]=0),s[r+1]+=s[r]/i|0,s[r]%=i)}return s.reverse()}return function(n,r,o,s,a){var l,c,h,d,u,p,g,f,v=n.indexOf("."),y=_,b=m;for(v>=0&&(d=k,k=0,n=n.replace(".",""),p=(f=new M(r)).pow(n.length-v),k=d,f.c=t(et(Ye(p.c),p.e,"0"),10,o,e),f.e=f.c.length),h=d=(g=t(n,r,o,a?(l=$,e):(l=e,$))).length;0==g[--d];g.pop());if(!g[0])return l.charAt(0);if(v<0?--h:(p.c=g,p.e=h,p.s=s,g=(p=i(p,f,y,b,o)).c,u=p.r,h=p.e),v=g[c=h+y+1],d=o/2,u=u||c<0||null!=g[c+1],u=b<4?(null!=v||u)&&(0==b||b==(p.s<0?3:2)):v>d||v==d&&(4==b||u||6==b&&1&g[c-1]||b==(p.s<0?8:7)),c<1||!g[0])n=u?et(l.charAt(1),-y,l.charAt(0)):l.charAt(0);else{if(g.length=c,u)for(--o;++g[--c]>o;)g[c]=0,c||(++h,g=[1].concat(g));for(d=g.length;!g[--d];);for(v=0,n="";v<=d;n+=l.charAt(g[v++]));n=et(n,h,l.charAt(0))}return n}}(),i=function(){function e(e,t,i){var n,r,o,s,a=0,l=e.length,c=t%Ge,h=t/Ge|0;for(e=e.slice();l--;)a=((r=c*(o=e[l]%Ge)+(n=h*o+(s=e[l]/Ge|0)*c)%Ge*Ge+a)/i|0)+(n/Ge|0)+h*s,e[l]=r%i;return a&&(e=[a].concat(e)),e}function t(e,t,i,n){var r,o;if(i!=n)o=i>n?1:-1;else for(r=o=0;rt[r]?1:-1;break}return o}function i(e,t,i,n){for(var r=0;i--;)e[i]-=r,r=e[i]1;e.splice(0,1));}return function(n,r,o,s,a){var l,c,h,d,u,p,_,m,g,f,v,y,b,w,k,x,$,S=n.s==r.s?1:-1,T=n.c,R=r.c;if(!(T&&T[0]&&R&&R[0]))return new M(n.s&&r.s&&(T?!R||T[0]!=R[0]:R)?T&&0==T[0]||!R?0*S:S/0:NaN);for(g=(m=new M(S)).c=[],S=o+(c=n.e-r.e)+1,a||(a=qe,c=Ke(n.e/Be)-Ke(r.e/Be),S=S/Be|0),h=0;R[h]==(T[h]||0);h++);if(R[h]>(T[h]||0)&&c--,S<0)g.push(1),d=!0;else{for(w=T.length,x=R.length,h=0,S+=2,(u=He(a/(R[0]+1)))>1&&(R=e(R,u,a),T=e(T,u,a),x=R.length,w=T.length),b=x,v=(f=T.slice(0,x)).length;v=a/2&&k++;do{if(u=0,(l=t(R,f,x,v))<0){if(y=f[0],x!=v&&(y=y*a+(f[1]||0)),(u=He(y/k))>1)for(u>=a&&(u=a-1),_=(p=e(R,u,a)).length,v=f.length;1==t(p,f,_,v);)u--,i(p,x<_?$:R,_,a),_=p.length,l=1;else 0==u&&(l=u=1),_=(p=R.slice()).length;if(_=10;S/=10,h++);D(m,o+(m.e=h+c*Be-1)+1,s,d)}else m.e=c,m.r=+d;return m}}(),a=/^(-?)0([xbo])(?=\w[\w.]*$)/i,l=/^([^.]+)\.$/,c=/^\.([^.]+)$/,h=/^-?(Infinity|NaN)$/,d=/^\s*\+(?=[\w.])|^\s+|\s+$/g,r=function(e,t,i,n){var r,o=i?t:t.replace(d,"");if(h.test(o))e.s=isNaN(o)?null:o<0?-1:1;else{if(!i&&(o=o.replace(a,function(e,t,i){return r="x"==(i=i.toLowerCase())?16:"b"==i?2:8,n&&n!=r?e:t}),n&&(r=n,o=o.replace(l,"$1").replace(c,"0.$1")),t!=o))return new M(o,r);if(M.DEBUG)throw Error(Le+"Not a"+(n?" base "+n:"")+" number: "+t);e.s=null}e.c=e.e=null},u.absoluteValue=u.abs=function(){var e=new M(this);return e.s<0&&(e.s=1),e},u.comparedTo=function(e,t){return Ze(this,new M(e,t))},u.decimalPlaces=u.dp=function(e,t){var i,n,r,o=this;if(null!=e)return Xe(e,0,Ve),null==t?t=m:Xe(t,0,8),D(new M(o),e+o.e+1,t);if(!(i=o.c))return null;if(n=((r=i.length-1)-Ke(this.e/Be))*Be,r=i[r])for(;r%10==0;r/=10,n--);return n<0&&(n=0),n},u.dividedBy=u.div=function(e,t){return i(this,new M(e,t),_,m)},u.dividedToIntegerBy=u.idiv=function(e,t){return i(this,new M(e,t),0,1)},u.exponentiatedBy=u.pow=function(e,t){var i,n,r,o,s,a,l,c,h=this;if((e=new M(e)).c&&!e.isInteger())throw Error(Le+"Exponent not an integer: "+A(e));if(null!=t&&(t=new M(t)),s=e.e>14,!h.c||!h.c[0]||1==h.c[0]&&!h.e&&1==h.c.length||!e.c||!e.c[0])return c=new M(Math.pow(+A(h),s?e.s*(2-Je(e)):+A(e))),t?c.mod(t):c;if(a=e.s<0,t){if(t.c?!t.c[0]:!t.s)return new M(NaN);(n=!a&&h.isInteger()&&t.isInteger())&&(h=h.mod(t))}else{if(e.e>9&&(h.e>0||h.e<-1||(0==h.e?h.c[0]>1||s&&h.c[1]>=24e7:h.c[0]<8e13||s&&h.c[0]<=9999975e7)))return o=h.s<0&&Je(e)?-0:0,h.e>-1&&(o=1/o),new M(a?1/o:o);k&&(o=Ne(k/Be+2))}for(s?(i=new M(.5),a&&(e.s=1),l=Je(e)):l=(r=Math.abs(+A(e)))%2,c=new M(p);;){if(l){if(!(c=c.times(h)).c)break;o?c.c.length>o&&(c.c.length=o):n&&(c=c.mod(t))}if(r){if(0===(r=He(r/2)))break;l=r%2}else if(D(e=e.times(i),e.e+1,1),e.e>14)l=Je(e);else{if(0===(r=+A(e)))break;l=r%2}h=h.times(h),o?h.c&&h.c.length>o&&(h.c.length=o):n&&(h=h.mod(t))}return n?c:(a&&(c=p.div(c)),t?c.mod(t):o?D(c,k,m,void 0):c)},u.integerValue=function(e){var t=new M(this);return null==e?e=m:Xe(e,0,8),D(t,t.e+1,e)},u.isEqualTo=u.eq=function(e,t){return 0===Ze(this,new M(e,t))},u.isFinite=function(){return!!this.c},u.isGreaterThan=u.gt=function(e,t){return Ze(this,new M(e,t))>0},u.isGreaterThanOrEqualTo=u.gte=function(e,t){return 1===(t=Ze(this,new M(e,t)))||0===t},u.isInteger=function(){return!!this.c&&Ke(this.e/Be)>this.c.length-2},u.isLessThan=u.lt=function(e,t){return Ze(this,new M(e,t))<0},u.isLessThanOrEqualTo=u.lte=function(e,t){return-1===(t=Ze(this,new M(e,t)))||0===t},u.isNaN=function(){return!this.s},u.isNegative=function(){return this.s<0},u.isPositive=function(){return this.s>0},u.isZero=function(){return!!this.c&&0==this.c[0]},u.minus=function(e,t){var i,n,r,o,s=this,a=s.s;if(t=(e=new M(e,t)).s,!a||!t)return new M(NaN);if(a!=t)return e.s=-t,s.plus(e);var l=s.e/Be,c=e.e/Be,h=s.c,d=e.c;if(!l||!c){if(!h||!d)return h?(e.s=-t,e):new M(d?s:NaN);if(!h[0]||!d[0])return d[0]?(e.s=-t,e):new M(h[0]?s:3==m?-0:0)}if(l=Ke(l),c=Ke(c),h=h.slice(),a=l-c){for((o=a<0)?(a=-a,r=h):(c=l,r=d),r.reverse(),t=a;t--;r.push(0));r.reverse()}else for(n=(o=(a=h.length)<(t=d.length))?a:t,a=t=0;t0)for(;t--;h[i++]=0);for(t=qe-1;n>a;){if(h[--n]=0;){for(i=0,u=y[r]%g,p=y[r]/g|0,o=r+(s=l);o>r;)i=((c=u*(c=v[--s]%g)+(a=p*c+(h=v[s]/g|0)*u)%g*g+_[o]+i)/m|0)+(a/g|0)+p*h,_[o--]=c%m;_[o]=i}return i?++n:_.splice(0,1),C(e,_,n)},u.negated=function(){var e=new M(this);return e.s=-e.s||null,e},u.plus=function(e,t){var i,n=this,r=n.s;if(t=(e=new M(e,t)).s,!r||!t)return new M(NaN);if(r!=t)return e.s=-t,n.minus(e);var o=n.e/Be,s=e.e/Be,a=n.c,l=e.c;if(!o||!s){if(!a||!l)return new M(r/0);if(!a[0]||!l[0])return l[0]?e:new M(a[0]?n:0*r)}if(o=Ke(o),s=Ke(s),a=a.slice(),r=o-s){for(r>0?(s=o,i=l):(r=-r,i=a),i.reverse();r--;i.push(0));i.reverse()}for((r=a.length)-(t=l.length)<0&&(i=l,l=a,a=i,t=r),r=0;t;)r=(a[--t]=a[t]+l[t]+r)/qe|0,a[t]=qe===a[t]?0:a[t]%qe;return r&&(a=[r].concat(a),++s),C(e,a,s)},u.precision=u.sd=function(e,t){var i,n,r,o=this;if(null!=e&&e!==!!e)return Xe(e,1,Ve),null==t?t=m:Xe(t,0,8),D(new M(o),e,t);if(!(i=o.c))return null;if(n=(r=i.length-1)*Be+1,r=i[r]){for(;r%10==0;r/=10,n--);for(r=i[0];r>=10;r/=10,n++);}return e&&o.e+1>n&&(n=o.e+1),n},u.shiftedBy=function(e){return Xe(e,-9007199254740991,We),this.times("1e"+e)},u.squareRoot=u.sqrt=function(){var e,t,n,r,o,s=this,a=s.c,l=s.s,c=s.e,h=_+4,d=new M("0.5");if(1!==l||!a||!a[0])return new M(!l||l<0&&(!a||a[0])?NaN:a?s:1/0);if(0==(l=Math.sqrt(+A(s)))||l==1/0?(((t=Ye(a)).length+c)%2==0&&(t+="0"),l=Math.sqrt(+t),c=Ke((c+1)/2)-(c<0||c%2),n=new M(t=l==1/0?"5e"+c:(t=l.toExponential()).slice(0,t.indexOf("e")+1)+c)):n=new M(l+""),n.c[0])for((l=(c=n.e)+h)<3&&(l=0);;)if(o=n,n=d.times(o.plus(i(s,o,h,1))),Ye(o.c).slice(0,l)===(t=Ye(n.c)).slice(0,l)){if(n.e0&&_>0){for(o=_%a||a,h=p.substr(0,o);o<_;o+=a)h+=c+p.substr(o,a);l>0&&(h+=c+p.slice(o)),u&&(h="-"+h)}n=d?h+(i.decimalSeparator||"")+((l=+i.fractionGroupSize)?d.replace(new RegExp("\\d{"+l+"}\\B","g"),"$&"+(i.fractionGroupSeparator||"")):d):h}return(i.prefix||"")+n+(i.suffix||"")},u.toFraction=function(e){var t,n,r,o,s,a,l,c,h,d,u,_,g=this,f=g.c;if(null!=e&&(!(l=new M(e)).isInteger()&&(l.c||1!==l.s)||l.lt(p)))throw Error(Le+"Argument "+(l.isInteger()?"out of range: ":"not an integer: ")+A(l));if(!f)return new M(g);for(t=new M(p),h=n=new M(p),r=c=new M(p),_=Ye(f),s=t.e=_.length-g.e-1,t.c[0]=Ue[(a=s%Be)<0?Be+a:a],e=!e||l.comparedTo(t)>0?s>0?t:h:l,a=y,y=1/0,l=new M(_),c.c[0]=0;d=i(l,t,0,1),1!=(o=n.plus(d.times(r))).comparedTo(e);)n=r,r=o,h=c.plus(d.times(o=h)),c=o,t=l.minus(d.times(o=t)),l=o;return o=i(e.minus(n),r,0,1),c=c.plus(o.times(h)),n=n.plus(o.times(r)),c.s=h.s=g.s,u=i(h,r,s*=2,m).minus(g).abs().comparedTo(i(c,n,s,m).minus(g).abs())<1?[h,r]:[c,n],y=a,u},u.toNumber=function(){return+A(this)},u.toPrecision=function(e,t){return null!=e&&Xe(e,1,Ve),T(this,e,t,2)},u.toString=function(e){var t,i=this,r=i.s,o=i.e;return null===o?r?(t="Infinity",r<0&&(t="-"+t)):t="NaN":(null==e?t=o<=g||o>=f?Qe(Ye(i.c),o):et(Ye(i.c),o,"0"):10===e&&S?t=et(Ye((i=D(new M(i),_+o+1,m)).c),i.e,"0"):(Xe(e,2,$.length,"Base"),t=n(et(Ye(i.c),o,"0"),10,e,r,!0)),r<0&&i.c[0]&&(t="-"+t)),t},u.valueOf=u.toJSON=function(){return A(this)},u._isBigNumber=!0,u[Symbol.toStringTag]="BigNumber",u[Symbol.for("nodejs.util.inspect.custom")]=u.valueOf,null!=t&&M.set(t),M}(),it=class{key;left=null;right=null;constructor(e){this.key=e}},nt=class extends it{constructor(e){super(e)}},rt=class{size=0;modificationCount=0;splayCount=0;splay(e){const t=this.root;if(null==t)return this.compare(e,e),-1;let i=null,n=null,r=null,o=null,s=t;const a=this.compare;let l;for(;;)if(l=a(s.key,e),l>0){let t=s.left;if(null==t)break;if(l=a(t.key,e),l>0&&(s.left=t.right,t.right=s,s=t,t=s.left,null==t))break;null==i?n=s:i.left=s,i=s,s=t}else{if(!(l<0))break;{let t=s.right;if(null==t)break;if(l=a(t.key,e),l<0&&(s.right=t.left,t.left=s,s=t,t=s.right,null==t))break;null==r?o=s:r.right=s,r=s,s=t}}return null!=r&&(r.right=s.left,s.left=o),null!=i&&(i.left=s.right,s.right=n),this.root!==s&&(this.root=s,this.splayCount++),l}splayMin(e){let t=e,i=t.left;for(;null!=i;){const e=i;t.left=e.right,e.right=t,t=e,i=t.left}return t}splayMax(e){let t=e,i=t.right;for(;null!=i;){const e=i;t.right=e.left,e.left=t,t=e,i=t.right}return t}_delete(e){if(null==this.root)return null;if(0!=this.splay(e))return null;let t=this.root;const i=t,n=t.left;if(this.size--,null==n)this.root=t.right;else{const e=t.right;t=this.splayMax(n),t.right=e,this.root=t}return this.modificationCount++,i}addNewRoot(e,t){this.size++,this.modificationCount++;const i=this.root;null!=i?(t<0?(e.left=i,e.right=i.right,i.right=null):(e.right=i,e.left=i.left,i.left=null),this.root=e):this.root=e}_first(){const e=this.root;return null==e?null:(this.root=this.splayMin(e),this.root)}_last(){const e=this.root;return null==e?null:(this.root=this.splayMax(e),this.root)}clear(){this.root=null,this.size=0,this.modificationCount++}has(e){return this.validKey(e)&&0==this.splay(e)}defaultCompare(){return(e,t)=>et?1:0}wrap(){return{getRoot:()=>this.root,setRoot:e=>{this.root=e},getSize:()=>this.size,getModificationCount:()=>this.modificationCount,getSplayCount:()=>this.splayCount,setSplayCount:e=>{this.splayCount=e},splay:e=>this.splay(e),has:e=>this.has(e)}}},ot=class e extends rt{root=null;compare;validKey;constructor(e,t){super(),this.compare=e??this.defaultCompare(),this.validKey=t??(e=>null!=e&&null!=e)}delete(e){return!!this.validKey(e)&&null!=this._delete(e)}deleteAll(e){for(const t of e)this.delete(t)}forEach(e){const t=this[Symbol.iterator]();let i;for(;i=t.next(),!i.done;)e(i.value,i.value,this)}add(e){const t=this.splay(e);return 0!=t&&this.addNewRoot(new nt(e),t),this}addAndReturn(e){const t=this.splay(e);return 0!=t&&this.addNewRoot(new nt(e),t),this.root.key}addAll(e){for(const t of e)this.add(t)}isEmpty(){return null==this.root}isNotEmpty(){return null!=this.root}single(){if(0==this.size)throw"Bad state: No element";if(this.size>1)throw"Bad state: Too many element";return this.root.key}first(){if(0==this.size)throw"Bad state: No element";return this._first().key}last(){if(0==this.size)throw"Bad state: No element";return this._last().key}lastBefore(e){if(null==e)throw"Invalid arguments(s)";if(null==this.root)return null;if(this.splay(e)<0)return this.root.key;let t=this.root.left;if(null==t)return null;let i=t.right;for(;null!=i;)t=i,i=t.right;return t.key}firstAfter(e){if(null==e)throw"Invalid arguments(s)";if(null==this.root)return null;if(this.splay(e)>0)return this.root.key;let t=this.root.right;if(null==t)return null;let i=t.left;for(;null!=i;)t=i,i=t.left;return t.key}retainAll(t){const i=new e(this.compare,this.validKey),n=this.modificationCount;for(const e of t){if(n!=this.modificationCount)throw"Concurrent modification during iteration.";this.validKey(e)&&0==this.splay(e)&&i.add(this.root.key)}i.size!=this.size&&(this.root=i.root,this.size=i.size,this.modificationCount++)}lookup(e){if(!this.validKey(e))return null;return 0!=this.splay(e)?null:this.root.key}intersection(t){const i=new e(this.compare,this.validKey);for(const e of this)t.has(e)&&i.add(e);return i}difference(t){const i=new e(this.compare,this.validKey);for(const e of this)t.has(e)||i.add(e);return i}union(e){const t=this.clone();return t.addAll(e),t}clone(){const t=new e(this.compare,this.validKey);return t.size=this.size,t.root=this.copyNode(this.root),t}copyNode(e){if(null==e)return null;const t=new nt(e.key);return function e(t,i){let n,r;do{if(n=t.left,r=t.right,null!=n){const t=new nt(n.key);i.left=t,e(n,t)}if(null!=r){const e=new nt(r.key);i.right=e,t=r,i=e}}while(null!=r)}(e,t),t}toSet(){return this.clone()}entries(){return new lt(this.wrap())}keys(){return this[Symbol.iterator]()}values(){return this[Symbol.iterator]()}[Symbol.iterator](){return new at(this.wrap())}[Symbol.toStringTag]="[object Set]"},st=class{tree;path=new Array;modificationCount=null;splayCount;constructor(e){this.tree=e,this.splayCount=e.getSplayCount()}[Symbol.iterator](){return this}next(){return this.moveNext()?{done:!1,value:this.current()}:{done:!0,value:null}}current(){if(!this.path.length)return null;const e=this.path[this.path.length-1];return this.getValue(e)}rebuildPath(e){this.path.splice(0,this.path.length),this.tree.splay(e),this.path.push(this.tree.getRoot()),this.splayCount=this.tree.getSplayCount()}findLeftMostDescendent(e){for(;null!=e;)this.path.push(e),e=e.left}moveNext(){if(this.modificationCount!=this.tree.getModificationCount()){if(null==this.modificationCount){this.modificationCount=this.tree.getModificationCount();let e=this.tree.getRoot();for(;null!=e;)this.path.push(e),e=e.left;return this.path.length>0}throw"Concurrent modification during iteration."}if(!this.path.length)return!1;this.splayCount!=this.tree.getSplayCount()&&this.rebuildPath(this.path[this.path.length-1].key);let e=this.path[this.path.length-1],t=e.right;if(null!=t){for(;null!=t;)this.path.push(t),t=t.left;return!0}for(this.path.pop();this.path.length&&this.path[this.path.length-1].right===e;)e=this.path.pop();return this.path.length>0}},at=class extends st{getValue(e){return e.key}},lt=class extends st{getValue(e){return[e.key,e.key]}},ct=e=>()=>e,ht=e=>{const t=e?(t,i)=>i.minus(t).abs().isLessThanOrEqualTo(e):ct(!1);return(e,i)=>t(e,i)?0:e.comparedTo(i)};function dt(e){const t=e?(t,i,n,r,o)=>t.exponentiatedBy(2).isLessThanOrEqualTo(r.minus(i).exponentiatedBy(2).plus(o.minus(n).exponentiatedBy(2)).times(e)):ct(!1);return(e,i,n)=>{const r=e.x,o=e.y,s=n.x,a=n.y,l=o.minus(a).times(i.x.minus(s)).minus(r.minus(s).times(i.y.minus(a)));return t(l,r,o,s,a)?0:l.comparedTo(0)}}var ut=e=>e,pt=e=>{if(e){const t=new ot(ht(e)),i=new ot(ht(e)),n=(e,t)=>t.addAndReturn(e),r=e=>({x:n(e.x,t),y:n(e.y,i)});return r({x:new tt(0),y:new tt(0)}),r}return ut},_t=e=>({set:e=>{mt=_t(e)},reset:()=>_t(e),compare:ht(e),snap:pt(e),orient:dt(e)}),mt=_t(),gt=(e,t)=>e.ll.x.isLessThanOrEqualTo(t.x)&&t.x.isLessThanOrEqualTo(e.ur.x)&&e.ll.y.isLessThanOrEqualTo(t.y)&&t.y.isLessThanOrEqualTo(e.ur.y),ft=(e,t)=>{if(t.ur.x.isLessThan(e.ll.x)||e.ur.x.isLessThan(t.ll.x)||t.ur.y.isLessThan(e.ll.y)||e.ur.y.isLessThan(t.ll.y))return null;const i=e.ll.x.isLessThan(t.ll.x)?t.ll.x:e.ll.x,n=e.ur.x.isLessThan(t.ur.x)?e.ur.x:t.ur.x;return{ll:{x:i,y:e.ll.y.isLessThan(t.ll.y)?t.ll.y:e.ll.y},ur:{x:n,y:e.ur.y.isLessThan(t.ur.y)?e.ur.y:t.ur.y}}},vt=(e,t)=>e.x.times(t.y).minus(e.y.times(t.x)),yt=(e,t)=>e.x.times(t.x).plus(e.y.times(t.y)),bt=e=>yt(e,e).sqrt(),wt=(e,t,i)=>{const n={x:t.x.minus(e.x),y:t.y.minus(e.y)},r={x:i.x.minus(e.x),y:i.y.minus(e.y)};return vt(r,n).div(bt(r)).div(bt(n))},kt=(e,t,i)=>{const n={x:t.x.minus(e.x),y:t.y.minus(e.y)},r={x:i.x.minus(e.x),y:i.y.minus(e.y)};return yt(r,n).div(bt(r)).div(bt(n))},xt=(e,t,i)=>t.y.isZero()?null:{x:e.x.plus(t.x.div(t.y).times(i.minus(e.y))),y:i},$t=(e,t,i)=>t.x.isZero()?null:{x:i,y:e.y.plus(t.y.div(t.x).times(i.minus(e.x)))},St=class e{point;isLeft;segment;otherSE;consumedBy;static compare(t,i){const n=e.comparePoints(t.point,i.point);return 0!==n?n:(t.point!==i.point&&t.link(i),t.isLeft!==i.isLeft?t.isLeft?1:-1:zt.compare(t.segment,i.segment))}static comparePoints(e,t){return e.x.isLessThan(t.x)?-1:e.x.isGreaterThan(t.x)?1:e.y.isLessThan(t.y)?-1:e.y.isGreaterThan(t.y)?1:0}constructor(e,t){void 0===e.events?e.events=[this]:e.events.push(this),this.point=e,this.isLeft=t}link(e){if(e.point===this.point)throw new Error("Tried to link already linked events");const t=e.point.events;for(let e=0,i=t.length;e{const n=i.otherSE;t.set(i,{sine:wt(this.point,e.point,n.point),cosine:kt(this.point,e.point,n.point)})};return(e,n)=>{t.has(e)||i(e),t.has(n)||i(n);const{sine:r,cosine:o}=t.get(e),{sine:s,cosine:a}=t.get(n);return r.isGreaterThanOrEqualTo(0)&&s.isGreaterThanOrEqualTo(0)?o.isLessThan(a)?1:o.isGreaterThan(a)?-1:0:r.isLessThan(0)&&s.isLessThan(0)?o.isLessThan(a)?-1:o.isGreaterThan(a)?1:0:s.isLessThan(r)?-1:s.isGreaterThan(r)?1:0}}},Mt=class e{events;poly;_isExteriorRing;_enclosingRing;static factory(t){const i=[];for(let n=0,r=t.length;n0&&(e=i)}let t=e.segment.prevInResult(),i=t?t.prevInResult():null;for(;;){if(!t)return null;if(!i)return t.ringOut;if(i.ringOut!==t.ringOut)return i.ringOut?.enclosingRing()!==t.ringOut?t.ringOut:t.ringOut?.enclosingRing();t=i.prevInResult(),i=t?t.prevInResult():null}}},Tt=class{exteriorRing;interiorRings;constructor(e){this.exteriorRing=e,e.poly=this,this.interiorRings=[]}addInterior(e){this.interiorRings.push(e),e.poly=this}getGeom(){const e=this.exteriorRing.getGeom();if(null===e)return null;const t=[e];for(let e=0,i=this.interiorRings.length;e0?(this.tree.delete(t),i.push(e)):(this.segments.push(t),t.prev=n)}else{if(n&&r){const e=n.getIntersection(r);if(null!==e){if(!n.isAnEndpoint(e)){const t=this._splitSafely(n,e);for(let e=0,n=t.length;e0)return-1;const n=t.comparePoint(e.rightSE.point);return 0!==n?n:-1}if(i.isGreaterThan(n)){if(s.isLessThan(a)&&s.isLessThan(c))return-1;if(s.isGreaterThan(a)&&s.isGreaterThan(c))return 1;const i=t.comparePoint(e.leftSE.point);if(0!==i)return i;const n=e.comparePoint(t.rightSE.point);return n<0?1:n>0?-1:1}if(s.isLessThan(a))return-1;if(s.isGreaterThan(a))return 1;if(r.isLessThan(o)){const i=t.comparePoint(e.rightSE.point);if(0!==i)return i}if(r.isGreaterThan(o)){const i=e.comparePoint(t.rightSE.point);if(i<0)return 1;if(i>0)return-1}if(!r.eq(o)){const e=l.minus(s),t=r.minus(i),h=c.minus(a),d=o.minus(n);if(e.isGreaterThan(t)&&h.isLessThan(d))return 1;if(e.isLessThan(t)&&h.isGreaterThan(d))return-1}return r.isGreaterThan(o)?1:r.isLessThan(o)||l.isLessThan(c)?-1:l.isGreaterThan(c)?1:e.idt.id?1:0}constructor(e,t,i,n){this.id=++Ot,this.leftSE=e,e.segment=this,e.otherSE=t,this.rightSE=t,t.segment=this,t.otherSE=e,this.rings=i,this.windings=n}static fromRing(t,i,n){let r,o,s;const a=St.comparePoints(t,i);if(a<0)r=t,o=i,s=1;else{if(!(a>0))throw new Error(`Tried to create degenerate segment at [${t.x}, ${t.y}]`);r=i,o=t,s=-1}const l=new St(r,!0),c=new St(o,!1);return new e(l,c,[n],[s])}replaceRightSE(e){this.rightSE=e,this.rightSE.segment=this,this.rightSE.otherSE=this.leftSE,this.leftSE.otherSE=this.rightSE}bbox(){const e=this.leftSE.point.y,t=this.rightSE.point.y;return{ll:{x:this.leftSE.point.x,y:e.isLessThan(t)?e:t},ur:{x:this.rightSE.point.x,y:e.isGreaterThan(t)?e:t}}}vector(){return{x:this.rightSE.point.x.minus(this.leftSE.point.x),y:this.rightSE.point.y.minus(this.leftSE.point.y)}}isAnEndpoint(e){return e.x.eq(this.leftSE.point.x)&&e.y.eq(this.leftSE.point.y)||e.x.eq(this.rightSE.point.x)&&e.y.eq(this.rightSE.point.y)}comparePoint(e){return mt.orient(this.leftSE.point,e,this.rightSE.point)}getIntersection(e){const t=this.bbox(),i=e.bbox(),n=ft(t,i);if(null===n)return null;const r=this.leftSE.point,o=this.rightSE.point,s=e.leftSE.point,a=e.rightSE.point,l=gt(t,s)&&0===this.comparePoint(s),c=gt(i,r)&&0===e.comparePoint(r),h=gt(t,a)&&0===this.comparePoint(a),d=gt(i,o)&&0===e.comparePoint(o);if(c&&l)return d&&!h?o:!d&&h?a:null;if(c)return h&&r.x.eq(a.x)&&r.y.eq(a.y)?null:r;if(l)return d&&o.x.eq(s.x)&&o.y.eq(s.y)?null:s;if(d&&h)return null;if(d)return o;if(h)return a;const u=((e,t,i,n)=>{if(t.x.isZero())return $t(i,n,e.x);if(n.x.isZero())return $t(e,t,i.x);if(t.y.isZero())return xt(i,n,e.y);if(n.y.isZero())return xt(e,t,i.y);const r=vt(t,n);if(r.isZero())return null;const o={x:i.x.minus(e.x),y:i.y.minus(e.y)},s=vt(o,t).div(r),a=vt(o,n).div(r),l=e.x.plus(a.times(t.x)),c=i.x.plus(s.times(n.x)),h=e.y.plus(a.times(t.y)),d=i.y.plus(s.times(n.y));return{x:l.plus(c).div(2),y:h.plus(d).div(2)}})(r,this.vector(),s,e.vector());return null===u?null:gt(n,u)?mt.snap(u):null}split(t){const i=[],n=void 0!==t.events,r=new St(t,!0),o=new St(t,!1),s=this.rightSE;this.replaceRightSE(o),i.push(o),i.push(r);const a=new e(r,s,this.rings.slice(),this.windings.slice());return St.comparePoints(a.leftSE.point,a.rightSE.point)>0&&a.swapEvents(),St.comparePoints(this.leftSE.point,this.rightSE.point)>0&&this.swapEvents(),n&&(r.checkForConsuming(),o.checkForConsuming()),i}swapEvents(){const e=this.rightSE;this.rightSE=this.leftSE,this.leftSE=e,this.leftSE.isLeft=!0,this.rightSE.isLeft=!1;for(let e=0,t=this.windings.length;e0){const e=i;i=n,n=e}if(i.prev===n){const e=i;i=n,n=e}for(let e=0,t=n.rings.length;e1===e.length&&e[0].isSubject;this._isInResult=i(e)!==i(t);break}}return this._isInResult}},Pt=class{poly;isExterior;segments;bbox;constructor(e,t,i){if(!Array.isArray(e)||0===e.length)throw new Error("Input geometry is not a valid Polygon or MultiPolygon");if(this.poly=t,this.isExterior=i,this.segments=[],"number"!=typeof e[0][0]||"number"!=typeof e[0][1])throw new Error("Input geometry is not a valid Polygon or MultiPolygon");const n=mt.snap({x:new tt(e[0][0]),y:new tt(e[0][1])});this.bbox={ll:{x:n.x,y:n.y},ur:{x:n.x,y:n.y}};let r=n;for(let t=1,i=e.length;tAt.run("union",e,t),Nt=(e,...t)=>At.run("intersection",e,t),Ht=(e,...t)=>At.run("difference",e,t);mt.set;const Lt=/^#[0-9a-fA-F]{6}$/;function jt(e,t){return"string"==typeof e&&Lt.test(e)?e:t}const qt="(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])",Bt=new RegExp(`^rgb\\(${qt}, ${qt}, ${qt}\\)$`);function Wt(e){if(!Array.isArray(e)||e.length<3)return null;const t=e.slice(0,3);if(!t.every(e=>"number"==typeof e&&Number.isFinite(e)))return null;const[i,n,r]=t.map(e=>Math.round(Math.min(255,Math.max(0,e))));return`rgb(${i}, ${n}, ${r})`}function Ut(e){const t=Math.max(0,Math.min(120,(e-40)/140*120));return`hsl(${Math.round(t)}, 85%, 55%)`}function Gt(e,t){if(!(Number.isFinite(e)&&t>0))return e;const i=Math.round(e/t)*t;return Math.abs(i-e)<=1e-9*t?e:i}function Vt(e,t,i,n=1/0){if(!(i>0&&e?.every(Number.isFinite)&&t?.every(Number.isFinite)))return[t[0],t[1]];const r=t[0]-e[0],o=t[1]-e[1];if(Math.abs(r)+Math.abs(o)<=1e-12)return[e[0],e[1]];const s=Math.PI/4;let a=Math.atan2(o,r);a<0&&(a+=2*Math.PI);const l=Math.floor(a/s+.5)%8,[c,h]=[[1,0],[1,1],[0,1],[-1,1],[-1,0],[-1,-1],[0,-1],[1,-1]][l],d=c*c+h*h;let u=Math.max(0,Math.round((r*c+o*h)/(i*d)));if(Number.isFinite(n)&&n>=0){const t=(e,t)=>{if(!t)return 1/0;const r=t>0?n-e:e+n;return Math.max(0,Math.floor((r+1e-9*i)/i))};u=Math.min(u,t(e[0],c),t(e[1],h))}return[e[0]+c*u*i,e[1]+h*u*i]}function Kt(e,t){if(t){const t=e/2.54;let i=Math.floor(t/12),n=Math.round(t-12*i);return 12===n&&(i+=1,n=0),`${i}′ ${n}″`}return`${(e/100).toFixed(2)} m`}function Yt(e,t,i=1){const n=e[0].toFixed(i),r=e[1].toFixed(i),o=t[0].toFixed(i),s=t[1].toFixed(i),a=n=3?e.poly:e&&null!=e.x&&null!=e.y&&null!=e.w&&null!=e.h?[[e.x,e.y],[e.x+e.w,e.y],[e.x+e.w,e.y+e.h],[e.x,e.y+e.h]]:null}function Xt(e){const t=[];for(const i of e||[])i?.poly?.length>=3?t.push({poly:i.poly.map(e=>e.join(",")).join(" ")}):i&&null!=i.x&&null!=i.y&&null!=i.w&&null!=i.h&&t.push({rect:{x:i.x,y:i.y,w:i.w,h:i.h,rx:.03*Math.min(i.w,i.h)}});return t}function Jt(e){const t=[],i=new Set;for(const n of e||[]){const e=Zt(n);if(e)for(let n=0;n=90?e-=180:e<-90&&(e+=180),n.step&&n.step>0){const i=Math.sqrt(d),o=Math.min(Math.max(n.length||0,0)/2,i/2);let a=Math.round(u*i/n.step)*n.step;Math.abs(u*i-i/2)<=n.step/2&&(a=i/2),a=Math.max(o,Math.min(i-o,a));const l=a/i;r={x:t+l*c,y:s+l*h,angle:e}}else r={x:p[0],y:p[1],angle:e}}}return r}function ei(e,t,i){let n=null,r=1/0;for(let o=0;o=r)continue;r=_;const m=Math.sqrt(u),g=(i>0?Math.max(0,Math.min(m,Math.round(p*m/i)*i)):p*m)/m;n=[s+g*h,a+g*d]}return n}function ti(e,t,i,n,r,o=1){const s=t*Math.PI/180,a=[Math.cos(s),Math.sin(s)];let l=null,c=o;for(const t of Jt(n)){const i=[[t[0],t[1]],[t[2],t[3]]],n=t=>Math.abs(a[0]*(t[1]-e[1])-a[1]*(t[0]-e[0]));if(n(i[0])>o||n(i[1])>o)continue;const r=(i[0][0]-e[0])*a[0]+(i[0][1]-e[1])*a[1],s=(i[1][0]-e[0])*a[0]+(i[1][1]-e[1])*a[1],h=Math.min(r,s),d=Math.max(r,s),u=h>0?h:d<0?-d:0;u[e[0]+a[0]*t,e[1]+a[1]*t],g=(h+d)/2;return{wallA:m(h),wallB:m(d),sideA:p,sideB:_,midA:m((h-u)/2),midB:m((u+d)/2),wallCenter:m(g),centered:Math.abs(g)<=r}}function ii(e,t,i=!1){if("passage"===e)return 1;if(null==t||"unavailable"===t||"unknown"===t)return"window"===e?0:1;const n=function(e){return["on","open","home","detected","playing","cleaning"].includes(String(e))}(t)!==!!i;return n?1:0}function ni(e){return"passage"===e.type?[]:[e.contact,e.lock].filter(e=>"string"==typeof e&&e.length>0)}function ri(e,t,i=200){const n=t.trim().toLowerCase();return(n?e.filter(e=>e.label.toLowerCase().includes(n)||e.value.toLowerCase().includes(n)):e).slice(0,Math.max(0,i))}function oi(e,t,i=.001){return Math.abs(e[0]-t[0])e[1]!=l>e[1]&&e[0]<(a-o)*(e[1]-s)/(l-s)+o&&(i=!i)}return i}function ai(e,t,i){const n=i[0]-t[0],r=i[1]-t[1],o=n*n+r*r;let s=o?((e[0]-t[0])*n+(e[1]-t[1])*r)/o:0;return s=Math.max(0,Math.min(1,s)),Math.hypot(e[0]-(t[0]+s*n),e[1]-(t[1]+s*r))}function li(e,t){if(!t||t.length<2)return null;let i=null,n=1/0;for(let r=0;rr&&s<-r||o<-r&&s>r)&&(a>r&&l<-r||a<-r&&l>r)}function pi(e,t=24){const i=e.map(e=>e[0]),n=e.map(e=>e[1]),r=Math.min(...i),o=Math.max(...i),s=Math.min(...n),a=Math.max(...n),l=Math.max(o-r,a-s)||1;let c=0,h=0,d=0;for(let t=0;t1e-9?[h/(3*c),d/(3*c)]:[(r+o)/2,(s+a)/2],p=(t,i)=>{const n=((t,i)=>{if(!si([t,i],e))return-1/0;let n=1/0;for(let r=0;rm&&(m=c,_=[n,l])}if(_){const[e,i]=_,n=(o-r)/t,l=(a-s)/t;for(let t=-4;t<=4;t++)for(let r=-4;r<=4;r++){const o=e+n*t/4,s=i+l*r/4,a=p(o,s);a>m&&(m=a,_=[o,s])}}return _||_i(e)||e[0]}function _i(e,t=1e-6){if(!e||e.length<3)return null;const i=e.length,n=[e.reduce((e,t)=>e+t[0],0)/i,e.reduce((e,t)=>e+t[1],0)/i];if(hi(n,e,t))return n;for(let n=0;ngi(e,t,i));return n.filter(e=>!n.some(t=>t!==e&&gi(t,e,i)))}function yi(e){if(!e||e.length<3)return 0;let t=0;for(let i=0;i[e[0],e[1]]),[e[0][0],e[0][1]]]]}function wi(e,t){if(!e||!t||e.length<3||t.length<3)return null;const i=Et(bi(e),bi(t));if(1!==i.length)return null;if(1!==i[0].length)return null;const n=i[0][0].slice(0,-1).map(e=>[e[0],e[1]]);return n.length>=3?n:null}function ki(e,t,i){for(let n=0;n1&&oi(i[0],i[i.length-1],t)&&i.pop(),i}function $i(e,t,i=1e-6){if(!e||e.length<3||!t||t.length<2)return null;const n=t[0],r=t[t.length-1];if(oi(n,r,i))return null;const o=ki(e,n,i),s=ki(e,r,i);if(o<0||s<0)return null;const a=t.slice(1,-1);for(const t of a)if(!hi(t,e,i))return null;for(let i=0;i{const s=[t];let a=(n+1)%e.length;for(let t=0;t<=e.length&&(s.push(e[a]),a!==o);t++)a=(a+1)%e.length;return s.push(r),xi(s,i)};let c,h;if(o===s){const s=xi([...t],i);if(s.length<3||yi(s)<=i)return null;const a=[];for(let i=0;i=0?t:[...t].reverse();for(const e of i)a.push(e)}c=xi(a,i),h=s}else c=xi([...l(n,o,r,s),...[...a].reverse()],i),h=xi([...l(r,s,n,o),...a],i);return c.length<3||h.length<3||yi(c)<=i||yi(h)<=i||Math.abs(yi(c)+yi(h)-yi(e))>Math.max(i,1e-6*yi(e))?null:[c,h]}function Si(e,t,i){const[n,r]=e.split(":");return"device"===n?r:"entity"===n?"lg_"+r:t&&t.startsWith("v_")?t:i()}function Mi(e){return e.length?Math.round(e.reduce((e,t)=>e+t,0)/e.length):null}function Ti(e,t){if(t>e[2]/e[3]){const i=e[3],n=e[3]*t;return{x:e[0]-(n-e[2])/2,y:e[1],w:n,h:i}}const i=e[2],n=e[2]/t;return{x:e[0],y:e[1]-(n-e[3])/2,w:i,h:n}}function Ri(e,t,i,n){if(e.length<2)return;const r=t.x+n,o=t.x+t.w-n,s=t.y+n,a=t.y+t.h-n;for(let t=0;t<60;t++){let t=!1;for(let n=0;n=0)i=t.slice(0,r).trim(),n=t.slice(r+1).trim();else{const e=t.split(".");e.length>2&&(i=e.slice(0,2).join("."),n=e.slice(2).join("."))}return/^[a-z0-9_]+\.[a-z0-9_]+$/.test(i)?r>=0&&!n||n&&!/^[a-zA-Z0-9_.-]+$/.test(n)?null:n?{entity:i,attr:n}:{entity:i}:null}function Li(e,t){const i=String(e??"").trim(),n=String(t??"").trim(),r=Hi(n?`${i}:${n}`:i);return r?`{${r.entity}${r.attr?`:${r.attr}`:""}}`:""}function ji(e,t,i){const n=String(t??"").trim();if(!n)return null;const r=e?.states?.[n];if(!r)return null;const o=String(i??"").trim(),s=e=>e.slice(0,60);if(o){const t=function(e){if(null==e)return null;if(Array.isArray(e)){const t=e.map(e=>null==e?"":String(e)).join(", ");return t?t.slice(0,60):null}if("object"==typeof e)return null;const t=String(e);return""===t?null:t.slice(0,60)}(r.attributes?.[o]);if(null===t)return null;const i=e?.formatEntityAttributeValue;if("function"==typeof i)try{const t=i.call(e,r,o);if("string"==typeof t&&""!==t)return{text:s(t),formatted:!0}}catch{}return{text:t,formatted:!1}}const a=r.state;if(null==a||""===a)return null;const l=e?.formatEntityState;if("function"==typeof l)try{const t=l.call(e,r);if("string"==typeof t&&""!==t)return{text:s(t),formatted:!0}}catch{}return{text:s(String(a)),formatted:!1}}function qi(e,t,i){const n=String(t??"").trim(),r=String(i??"").trim()||n;if(!r)return e.text;const o=e.formatted&&n?function(e,t){if(!t)return e;const i=e.replace(/\s+$/,"");return i.endsWith(t)?i.slice(0,i.length-t.length).replace(/\s+$/,""):e}(e.text,n):e.text;return`${o} ${r}`}function Bi(e,t){const i=(t?.entity||"").trim();if(!i)return"";const n=e?.states?.[i],r=n?.state;if(!n||null==r||""===r||"unavailable"===r||"unknown"===r)return Ei;const o=(t?.attr||"").trim(),s=ji(e,i,o||null);if(null===s)return Ei;return qi(s,o?"":String(n.attributes?.unit_of_measurement??"").trim(),t?.unit)}function Wi(e,t,i,n=()=>!0){const r=e??"";let o=!1;const s=r.replace(/\{([^{}\r\n]+)\}/g,(e,t)=>{const r=Hi(t);return r?(o=!0,n(r.entity||"")?Bi(i,r):Ei):e});if(o)return s;const a=(t?.entity||"").trim();if(!a)return r;if(!n(a)){const e=r.indexOf(Ni);return e>=0?r.slice(0,e)+Ei+r.slice(e+2):r?`${r} ${Ei}`:Ei}const l=Bi(i,t),c=r.indexOf(Ni);return c>=0?r.slice(0,c)+l+r.slice(c+2):r?`${r} ${l}`:l}const Ui=20;function Gi(e){const t=Number(e?.scale);if(Number.isFinite(t)&&t>0)return Math.min(20,Math.max(.15,t));return{s:.7,m:1,l:1.5}[String(e?.size??"")]??1}function Vi(e,t){if(!t)return e;let i=e;for(const[e,n]of Object.entries(t))i=i.split("{"+e+"}").join(String(n));return i}const Ki="#55606c",Yi=.55,Zi=20,Xi=25,Ji={c:"#607d8b",a:.18};function Qi(e,t=Ji){const i=e&&"object"==typeof e?e:null,n=i?.a;return{c:jt(i?.c,t.c),a:"number"==typeof n&&Number.isFinite(n)?Math.min(1,Math.max(0,n)):t.a}}function en(e,t){const i=Qi(e),n=t?.settings?.custom_fill;return n&&"object"==typeof n?Qi(n,i):i}function tn(e){const t=e?.settings||{},i=!e?.plan_url,n="glow"===t.fill_mode;return{showBorders:t.show_borders??i,showNames:t.show_names??i,color:jt(t.room_color,Ki),opacity:"number"==typeof t.room_opacity?Math.min(1,Math.max(0,t.room_opacity)):.55,fill:["lqi","light","temp","custom"].includes(t.fill_mode)?t.fill_mode:"none",customFill:Qi(t.custom_fill),glow:"boolean"==typeof t.glow_enabled?t.glow_enabled:n,tempMin:"number"==typeof t.temp_min?t.temp_min:20,tempMax:"number"==typeof t.temp_max?t.temp_max:25,showLqi:"boolean"==typeof t.show_lqi?t.show_lqi:null,cardFontScale:"number"==typeof t.card_font_scale&&t.card_font_scale>0?Math.min(3,Math.max(.5,t.card_font_scale)):1,labelTemp:!0===t.label_temp,labelHum:!0===t.label_hum,labelLqi:!0===t.label_lqi,labelLight:!0===t.label_light,bgColor:jt(t.bg_color,null),hideDecor:!0===t.hide_decor,hideOpenings:!0===t.hide_openings}}function nn(e,t){if(t.bgColor)return t.bgColor;const i=e?.bg_color;return jt(i,"")}const rn={light_on:{c:"#ffd45c",a:.18},light_off:{c:"#9aa0a6",a:.14},light_none:{c:"#6b7480",a:0},temp_cold:{c:"#4fc3f7",a:.18},temp_ok:{c:"#66d17a",a:.18},temp_hot:{c:"#ffd45c",a:.18},lqi_low:{c:"#f25a4a",a:.18},lqi_high:{c:"#4bd28f",a:.18},glow_base:{c:"#0d1b2a",a:.5},glow_light:{c:"#ffd9a0",a:.85},wall_fill:{c:"#ffffff",a:1}};function on(e){const t={},i=e?.fill_colors||{};for(const e of Object.keys(rn)){const n=rn[e],r=i[e];t[e]={c:jt(r?.c,n.c),a:r&&"number"==typeof r.a?Math.min(1,Math.max(0,r.a)):n.a}}return t}function sn(e,t,i){const n=Math.min(1,Math.max(0,i)),r=[1,3,5].map(t=>parseInt(e.slice(t,t+2),16)),o=[1,3,5].map(e=>parseInt(t.slice(e,e+2),16)),s=r.map((e,t)=>Math.round(e+(o[t]-e)*n));return"#"+s.map(e=>e.toString(16).padStart(2,"0")).join("")}function an(e,t,i,n,r,o,s,a=Ji){const l=function(e,t,i,n,r,o,s,a=Ji){if("custom"===e)return Qi(a);if("lqi"===e){if(null==t)return null;const e=(t-40)/140;return{c:sn(s.lqi_low.c,s.lqi_high.c,e),a:s.lqi_low.a+(s.lqi_high.a-s.lqi_low.a)*Math.min(1,Math.max(0,e))}}if("light"===e)return"none"===i?s.light_none.a>0?s.light_none:null:"on"===i?s.light_on:s.light_off;if("temp"===e){if(null==n)return null;const e=Math.min(r,o),t=Math.max(r,o);return nt?s.temp_hot:s.temp_ok}return null}(e,t,i,n,r,o,s,a);return l?{color:l.c,opacity:l.a,mode:e}:null}const ln={blind:["mdi:blinds","mdi:blinds-open"],shade:["mdi:blinds","mdi:blinds-open"],shutter:["mdi:window-shutter","mdi:window-shutter-open"],curtain:["mdi:curtains-closed","mdi:curtains"],window:["mdi:window-closed","mdi:window-open"],awning:["mdi:awning-outline","mdi:awning"],door:["mdi:door-closed","mdi:door-open"],garage:["mdi:garage","mdi:garage-open"],gate:["mdi:gate","mdi:gate-open"],damper:["mdi:circle-slice-8","mdi:circle-outline"]},cn=[["mdi:roller-shade-closed","mdi:roller-shade"],["mdi:blinds-horizontal-closed","mdi:blinds-horizontal"],["mdi:garage-variant","mdi:garage-open-variant"],["mdi:door","mdi:door-open"]];function hn(e){for(const t of[...Object.values(ln),...cn])if(e===t[0]||e===t[1])return t;return null}function dn(e,t,i,n,r){if(!n||"unavailable"===n||"unknown"===n)return e;if(r){const i="cover"===t?hn(e):null;return i?"closed"===n?i[0]:i[1]:e}if("binary_sensor"===t){if("door"===i)return"on"===n?"mdi:door-open":"mdi:door-closed";if("window"===i)return"on"===n?"mdi:window-open":"mdi:window-closed";if("garage_door"===i)return"on"===n?"mdi:garage-open-variant":"mdi:garage-variant"}if("cover"===t){const t=ln[String(i||"")];if(t)return"closed"===n?t[0]:t[1];const r=hn(e);return r?"closed"===n?r[0]:r[1]:e}return"lock"===t?"locked"===n?"mdi:lock":"mdi:lock-open-variant":"light"===t&&"mdi:lightbulb"===e&&"on"===n?"mdi:lightbulb-on":e}function un(e){const t=Math.min(4e4,Math.max(1e3,e))/100,i=t<=66?255:329.698727446*Math.pow(t-60,-.1332047592),n=t<=66?99.4708025861*Math.log(t)-161.1195681661:288.1221695283*Math.pow(t-60,-.0755148492),r=t>=66?255:t<=19?0:138.5177312231*Math.log(t-10)-305.0447927307,o=e=>Math.round(Math.min(255,Math.max(0,e)));return[o(i),o(n),o(r)]}const pn=1/2.2;function _n(e){const t=/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/.exec(e);return t?"#"+t.slice(1).map(e=>Math.min(255,Number(e)).toString(16).padStart(2,"0")).join(""):null}function mn(e){if(!e||"object"!=typeof e||Array.isArray(e))return null;const t=e;if(Object.keys(t).some(e=>"c"!==e&&"bri"!==e))return null;const i=jt(t.c,null);return i?void 0===t.bri||null===t.bri?{c:i}:"number"!=typeof t.bri||!Number.isFinite(t.bri)||t.bri<.01||t.bri>1?null:{c:i,bri:t.bri}:null}function gn(e,t,i){const n=mn(t),r=e?.attributes||{},o=n?.bri??function(e){const t=e?.attributes?.brightness,i="number"==typeof t?t:"string"==typeof t&&""!==t.trim()?Number(t):Number.NaN;return Number.isFinite(i)?Math.max(0,Math.min(1,i/255)):1}(e);if(n)return{c:n.c,bri:o};const s=Wt(r.rgb_color);if(s)return{c:_n(s)||jt(i,"#ffd9a0"),bri:o};const a=Number(r.color_temp_kelvin)||(Number(r.color_temp)>0?1e6/Number(r.color_temp):NaN);return Number.isFinite(a)&&a>0?{c:(l=un(a),"#"+l.slice(0,3).map(e=>Math.min(255,Math.max(0,Math.round(Number(e)||0))).toString(16).padStart(2,"0")).join("")),bri:o}:{c:jt(i,"#ffd9a0"),bri:o};var l}function fn(e,t,i){return"on"===e?.state?gn(e,t,i):null}function vn(e,t=1){const i=Math.max(0,Math.min(1,Number.isFinite(e)?e:1)),n=.7*Math.max(0,Math.min(1,Number.isFinite(t)?t:1))*(.4+.6*Math.pow(i,pn));return Math.max(0,Math.min(1,n))}function yn(e){return e.startsWith("light.")||e.startsWith("switch.")}function bn(e,t,i=1e-6){const n=[];if(!e||!t||e.length<3||t.length<3)return n;for(let r=0;ru||l>u)continue;const p=(r[0]-o[0])*h+(r[1]-o[1])*d,_=(s[0]-o[0])*h+(s[1]-o[1])*d,m=Math.max(0,Math.min(p,_)),g=Math.min(c,Math.max(p,_));g-m>i&&n.push([o[0]+h*m,o[1]+d*m,o[0]+h*g,o[1]+d*g])}}return n}function wn(e,t,i=1e-6){const n=[];for(const r of e){const e=[r[0],r[1]],o=[r[2],r[3]],s=o[0]-e[0],a=o[1]-e[1],l=Math.hypot(s,a);if(lo||r>o)continue;const s=(n[0]-e[0])*c+(n[1]-e[1])*h,a=(n[2]-e[0])*c+(n[3]-e[1])*h,u=Math.max(0,Math.min(s,a)),p=Math.min(l,Math.max(s,a));p-u>i&&d.push([u,p])}if(!d.length){n.push([e[0],e[1],o[0],o[1]]);continue}d.sort((e,t)=>e[0]-t[0]);let u=0;for(const[t,r]of d)t-u>i&&n.push([e[0]+c*u,e[1]+h*u,e[0]+c*t,e[1]+h*t]),u=Math.max(u,r);l-u>i&&n.push([e[0]+c*u,e[1]+h*u,o[0],o[1]])}return n}function kn(e,t,i=1e-6){const n=[];for(let t=0;t{if("string"!=typeof e||!e)return;const i=Mn(e);i.startsWith("/api/houseplan/content/")&&t.add(i)};for(const t of e?.spaces||[]){i(t?.plan_url);for(const e of t?.markers||[])for(const t of e?.pdfs||[])i(t?.url)}for(const t of e?.markers||[])for(const e of t?.pdfs||[])i(e?.url);return t}function Mn(e){return e?e.startsWith("/houseplan_files/plans/")?"/api/houseplan/content/plans/_/"+e.slice(23):e.startsWith("/houseplan_files/files/")?"/api/houseplan/content/files/"+e.slice(23):e:""}function Tn(e,t){const i=t?.settings?.fill_mode;return"none"===i||"lqi"===i||"light"===i||"temp"===i||"custom"===i?i:e}function Rn(e,t){const i=t?.settings;return"boolean"==typeof i?.glow?i.glow:"glow"===i?.fill_mode||e}function Cn(e,t,i,n){if(!t||!i||t===i)return e;const r="/files/"+t+"/",o="/files/"+i+"/";return e.map(e=>{if(!e.url.includes(r))return e;const t=e.url.split(r)[1]||"",[i,s]=[t.split("?")[0],t.includes("?")?"?"+t.split("?")[1]:""];if(n){const t=n[decodeURIComponent(i)]??n[i];return t?{...e,url:e.url.split(r+i)[0]+o+encodeURIComponent(t)+s}:e}return{...e,url:e.url.split(r).join(o)}})}function Dn(e,t=1){const i=Number(e);return Number.isFinite(i)&&i>0?Math.min(3,Math.max(.5,i)):t}function An(e,t,i){let n=null,r=null;for(const o of t){if(!(Math.abs(o[0]-e[0])<1e-6&&Math.abs(o[1]-e[1])<1e-6)){if(Math.abs(o[0]-e[0])<=i){const t=Math.abs(o[1]-e[1]);t>1e-6&&(!n||t1e-6&&(!r||t"number"==typeof e&&Number.isFinite(e),Wn=(e,t,i)=>Math.min(i,Math.max(t,e));function Un(e){return e.elevation<=-6?"night":e.elevation>=6?"day":e.rising?"dawn":"dusk"}function Gn(e){const t=(Math.floor(Number(e)||0)%1440+1440)%1440;return t>=300&&t<480?"dawn":t>=480&&t<1080?"day":t>=1080&&t<1260?"dusk":"night"}function Vn(e){const t=Un(e),i=e.azimuth*Math.PI/180;return{sunX:50-42*Math.sin(i),sunY:78-Wn(e.elevation,0,90)/90*64,sunOpacity:"night"===t?0:Wn((e.elevation+6)/12,0,1)}}function Kn(e){const t=(Math.floor(Number(e)||0)%1440+1440)%1440;if(t<300||t>=1260)return{sunX:50,sunY:78,sunOpacity:0};const i=(t-300)/960,n=(t-300)/120,r=(1260-t)/120;return{sunX:8+84*i,sunY:78-64*Math.sin(i*Math.PI),sunOpacity:Math.max(.18,Math.min(n,r,1))}}function Yn(e,t=new Date){const i=function(e){const t=e?.states?.["sun.sun"]?.attributes;return t&&Bn(t.azimuth)&&Bn(t.elevation)&&"boolean"==typeof t.rising?{azimuth:Ln(t.azimuth),elevation:t.elevation,rising:t.rising}:null}(e);if(i)return{phase:Un(i),source:"sun",...Vn(i)};const n="number"==typeof t?t:function(e=new Date){return 60*e.getHours()+e.getMinutes()}(t);return{phase:Gn(n),source:"clock",...Kn(n)}}function Zn(e){return`${e.source}|${e.phase}|${e.sunX.toFixed(2)}|${e.sunY.toFixed(2)}|`+e.sunOpacity.toFixed(3)}const Xn=[[-90,"#070c14"],[-12,"#070c14"],[-4,"#131a28"],[0,"#4a3527"],[10,"#e8ddcf"],[30,"#ffffff"],[90,"#ffffff"]],Jn=e=>Math.min(1,Math.max(0,e));function Qn(e,t,i=6){const n=e.angle*Math.PI/180,r=[Math.sin(n),-Math.cos(n)],o=n=>{const o=[e.x+r[0]*i*n,e.y+r[1]*i*n];return t.find(e=>e.poly.length>=3&&si(o,e.poly))||null},s=o(1),a=o(-1);return s&&a?null:s||a?s?{normal:[-r[0],-r[1]],roomId:s.id}:{normal:r,roomId:a.id}:null}function er(e,t,i){return i>0&&e[0]*t[0]+e[1]*t[1]>.05}function tr(e,t,i,n){return[[e[0],e[1]],[t[0],t[1]],[t[0]+i[0]*n,t[1]+i[1]*n],[e[0]+i[0]*n,e[1]+i[1]*n]]}function ir(e,t){try{const i=Nt([[...e.map(e=>[e[0],e[1]]),[e[0][0],e[0][1]]]],[[...t.map(e=>[e[0],e[1]]),[t[0][0],t[0][1]]]]),n=[];for(const e of i){const t=e?.[0];!Array.isArray(t)||t.length<4||n.push(t.slice(0,t.length-1).map(e=>[e[0],e[1]]))}return n}catch{return[]}}function nr(e,t,i,n,r,o,s){if(!(n>0))return[];const a=jn(i,r),l=[-a[0],-a[1]],c=function(e){const t=Math.min(90,Math.max(0,e));return.7*(.8+1.7*Math.pow(1-t/90,1.6))}(n),h=[];for(const i of t){if(!(i.length>0))continue;const t=Qn(i,e);if(!t||!er(t.normal,a,n))continue;const r=e.find(e=>e.id===t.roomId);if(!r)continue;const d=o&&o[t.roomId]||r.poly,u=i.angle*Math.PI/180,p=i.length/2,_=[-t.normal[0],-t.normal[1]],m=Math.max(0,s?.[i.id]||0),g=i.x+_[0]*m/2,f=i.y+_[1]*m/2,v=Math.cos(u)*p,y=Math.sin(u)*p,b=[g-v,f-y],w=[g+v,f+y],k=c*i.length,x=ir(tr(b,w,l,k),d);if(!x.length)continue;const $=l[0]*_[0]+l[1]*_[1];h.push({openingId:i.id,roomId:t.roomId,polys:x,a:b,b:w,dir:l,len:k,normal:_,depth:k*$})}return h}const rr=e=>"number"==typeof e&&Number.isInteger(e)&&e>=0&&e<=359?e:null;function or(e,t){const i=rr(t?.north_deg);return null!==i?i:rr(e?.north_deg)}function sr(e,t){const i=e=>"static"===e||"daynight"===e?e:null;return i(t?.bg_mode)??i(e?.bg_mode)??"static"}function ar(e,t){const i=t?.sun_rays;return"boolean"==typeof i?i:!0===e?.sun_rays}function lr(e){const t=e?.states?.["sun.sun"]?.attributes,i=Number(t?.azimuth),n=Number(t?.elevation);return Number.isFinite(i)&&Number.isFinite(n)?{azimuth:i,elevation:n}:null}const cr=["dawn","day","dusk","night"];function hr(e){if(!e)return"";const t=qn[e.phase];return[`--hp-day-cycle-outline-near:${t.outlineNear}`,`--hp-day-cycle-outline-mid:${t.outlineMid}`,`--hp-day-cycle-outline-far:${t.outlineFar}`].join(";")}function dr(e,t=1){if(!e)return G;const i=[`--hp-day-cycle-sun-x:${e.sunX.toFixed(2)}%`,`--hp-day-cycle-sun-y:${e.sunY.toFixed(2)}%`,`--hp-day-cycle-sun-opacity:${e.sunOpacity.toFixed(3)}`,`opacity:${Math.min(1,Math.max(0,t)).toFixed(4)}`].join(";");return B``}const ur={color:"#607d8b",opacity:1,widthCm:3.6,fill:!1,fillColor:"#607d8b",fillOpacity:.25},pr=(e,t=1)=>{const i=Number(e);return Number.isFinite(i)?Math.min(1,Math.max(0,i)):t},_r=e=>{let t=Number(e);return Number.isFinite(t)?(t=(t%360+360)%360,t>180?t-360:t):0};function mr(e,t,i){return _r(t)?[e.x,e.y]:i([e.x,e.y])}function gr(e,t,i,n){const r=Math.abs(i[0]-t[0]),o=Math.abs(i[1]-t[1]);return"line"===e?Math.hypot(r,o)>=n:r>=n&&o>=n}const fr=(e,t,i)=>{const n=Number(e),r=Number.isFinite(t)&&t>0?t:5;return Number.isFinite(n)&&n>0?n/r*i:0},vr=(e,t,i)=>{const n=Number(e),r=Number.isFinite(t)&&t>0?t:5;return Number.isFinite(n)&&i>0?n/i*r:0},yr=(e,t,i,n=ur.widthCm)=>{const r=Number(e?.width_cm);if(Number.isFinite(r)&&r>0)return r;const o=Number(e?.width);return Number.isFinite(o)&&o>0?vr(o,t,i):n};function br(e,t){return{color:e.color,opacity:pr(e.opacity),width_cm:Math.max(.1,Math.min(100,Number(e.widthCm)||.1)),...t?{fill:e.fill,fill_color:e.fillColor,fill_opacity:pr(e.fillOpacity,.25)}:{}}}function wr(e){const t=e.x+e.w/2,i=e.y+e.h/2,n=_r(e.angle)*Math.PI/180,r=Math.cos(n),o=Math.sin(n),s=(e,n)=>{const s=e-t,a=n-i;return[t+s*r-a*o,i+s*o+a*r]};return[s(e.x,e.y),s(e.x+e.w,e.y),s(e.x+e.w,e.y+e.h),s(e.x,e.y+e.h)]}function kr(e){const t=wr(e),i=(e,t)=>[(e[0]+t[0])/2,(e[1]+t[1])/2],n=[(t[0][0]+t[2][0])/2,(t[0][1]+t[2][1])/2];return{points:[...t,i(t[0],t[1]),i(t[1],t[2]),i(t[2],t[3]),i(t[3],t[0]),n],segments:t.map((e,i)=>({a:e,b:t[(i+1)%4]}))}}function xr(e,t,i,n,r,o,s,a){const l=_r(e.angle)*Math.PI/180,c=Math.cos(l),h=Math.sin(l),d=-Math.sin(l),u=Math.cos(l),p=e.x+e.w/2,_=e.y+e.h/2,m=t>0?-e.w/2:e.w/2,g=i>0?-e.h/2:e.h/2,f=p+m*c+g*d,v=_+m*h+g*u,y=n-f,b=r-v;let w=(y*c+b*h)*(t>0?1:-1),k=(y*d+b*u)*(i>0?1:-1),x=null;if(o){const t=w/Math.max(e.w,a),i=k/Math.max(e.h,a);x=Math.max(a/Math.max(e.w,a),a/Math.max(e.h,a),t,i),w=e.w*x,k=e.h*x}if(s>0)if(o){const t=Math.max(1,Math.round(e.w/s)),i=Math.max(1,Math.round(e.h/s));if(Math.abs(e.w-t*s)<1e-6&&Math.abs(e.h-i*s)<1e-6){const n=(e,t)=>{let i=Math.abs(e),n=Math.abs(t);for(;n;)[i,n]=[n,i%n];return Math.max(1,i)},r=1/n(t,i),o=Math.max(a/e.w,a/e.h),s=Math.max(o,Math.round((x??1)/r)*r);w=e.w*s,k=e.h*s}else{e.w>=e.h?(w=Math.round(w/s)*s,k=w*(e.h/Math.max(e.w,a))):(k=Math.round(k/s)*s,w=k*(e.w/Math.max(e.h,a)))}}else w=Math.round(w/s)*s,k=Math.round(k/s)*s;w=Math.max(a,w),k=Math.max(a,k);return{x:f+(t>0?w/2:-w/2)*c+(i>0?k/2:-k/2)*d-w/2,y:v+(t>0?w/2:-w/2)*h+(i>0?k/2:-k/2)*u-k/2,w:w,h:k,angle:_r(e.angle)||void 0}}const $r=(e,t,i)=>{const n=i[0]-t[0],r=i[1]-t[1],o=n*n+r*r;if(o<1e-12)return[...t];const s=Math.max(0,Math.min(1,((e[0]-t[0])*n+(e[1]-t[1])*r)/o));return[t[0]+n*s,t[1]+r*s]};function Sr(e,t,i,n){const r=n(e);let o=null;for(const r of t.points){const t=n(r),s=Math.hypot(t[0]-e[0],t[1]-e[1]);s<=i&&(!o||se.points),segments:e.flatMap(e=>e.segments)}}const Tr=Math.tan(.25*Math.PI/180);function Rr(e,t){const i=Math.abs(Number(t[0])-Number(e[0])),n=Math.abs(Number(t[1])-Number(e[1]));if(!([i,n].every(Number.isFinite)&&i>0&&n>0))return null;const r=i>=n?"horizontal":"vertical",o=Math.max(i,n),s=Math.min(i,n);return s/o>Tr?null:{axis:r,major:o,minor:s,angleDegrees:180*Math.atan2(s,o)/Math.PI}}function Cr(e,t){const i=Rr(e,t);return i?"horizontal"===i.axis?[Number(t[0]),Number(e[1])]:[Number(e[0]),Number(t[1])]:[Number(t[0]),Number(t[1])]}const Dr=e=>`${e[0]},${e[1]}`,Ar=(e,t)=>e[0]===t[0]&&e[1]===t[1],Or=(e,t)=>{const i=Dr(e),n=Dr(t);return i{let t=0;for(let i=0;i{if(e.length<3)return!1;for(let t=0;t1e-12},Fr=e=>{for(let t=0;t+1Ar(e,t.from)?[...t.to]:[...e],Er=(e,t)=>e.map(e=>Array.isArray(e?.poly)?{...e,poly:e.poly.map(e=>Ir(e,t))}:e),Nr=(e,t,i)=>{const n=e.flatMap((e,t)=>Zt(e)?.some(e=>Ar(e,i.from))?[t]:[]);if(!n.length)return!1;for(const i of n){const n=Zt(e[i]),r=Zt(t[i]);if(!n||!r)continue;if(!Pr(r))return!1;const o=Math.sign(zr(n)),s=Math.sign(zr(r));if(o&&s&&o!==s)return!1}const r=new Set;for(const e of n){const i=Zt(t[e]);if(i)for(let n=0;n{let i=0;for(const n of e){const e=Zt(n);if(e)for(let n=0;n{const{a:i,b:n,axis:r}=e,o="horizontal"===r?{from:n,to:[n[0],i[1]]}:{from:n,to:[i[0],n[1]]},s="horizontal"===r?{from:i,to:[i[0],n[1]]}:{from:i,to:[n[0],i[1]]};return[{move:o,preservedDegree:Hr(t,i)},{move:s,preservedDegree:Hr(t,n)}].sort((e,t)=>t.preservedDegree-e.preservedDegree||Dr(e.move.to).localeCompare(Dr(t.move.to))||Dr(e.move.from).localeCompare(Dr(t.move.from))).map(e=>e.move)};function jr(e){const t=JSON.parse(JSON.stringify(e||{}));let i=Array.isArray(t.rooms)?t.rooms:[];const n=new Map;for(const e of i){const t=Zt(e);if(t)for(let e=0;ee.key.localeCompare(t.key))){let t=null;for(const n of Lr(e,i)){const e=a.get(Dr(n.from));if(e&&e!==Dr(n.to))continue;const r=Er(i,n);if(Nr(i,r,n)){t=n,i=r;break}}t?(a.set(Dr(t.from),Dr(t.to)),r++,s=Math.max(s,Math.hypot(t.to[0]-t.from[0],t.to[1]-t.from[1]))):o++}t.rooms=i;for(const e of t.room_drafts||[]){if(!Array.isArray(e?.points)||e.points.length<2)continue;const t=e.points.map(e=>[...e]),i=t.slice(0,-1).flatMap((i,n)=>{const r=t[n+1],o=Rr(i,r);return o?[{key:`${String(e.id||"")}:${n}`,a:[...i],b:[...r],axis:o.axis}]:[]});let n=t;for(const e of i){let t=null;for(const i of Lr(e,[{poly:n}])){const e=n.map(e=>Ir(e,i)),r=e.length>=4&&Ar(e[0],e[e.length-1]),o=r?e.slice(0,-1):e;if(r?Pr(o):Fr(o)){t=i,n=e;break}}t?(r++,s=Math.max(s,Math.hypot(t.to[0]-t.from[0],t.to[1]-t.from[1]))):o++}e.points=n}for(const e of t.partitions||[]){if(!Array.isArray(e?.a)||!Array.isArray(e?.b))continue;const i=Rr(e.a,e.b);if(!i)continue;const n={key:String(e.id||Or(e.a,e.b)),a:[...e.a],b:[...e.b],axis:i.axis};let a=null;for(const i of Lr(n,[{poly:[e.a,e.b]}])){const n=Ir(e.a,i),r=Ir(e.b,i),o=Math.hypot(r[0]-n[0],r[1]-n[1]);if(!(o>0))continue;const s=(t.openings||[]).filter(t=>"partition"===t?.host?.kind&&t.host.id===e.id).every(e=>{const t=Number(e.host.t),i=Number(e.length);return Number.isFinite(t)&&t>=0&&t<=1&&Number.isFinite(i)&&i>0&&t*o-i/2>=-1e-12&&t*o+i/2<=o+1e-12});if(s){a=i,e.a=n,e.b=r;break}}a?(r++,s=Math.max(s,Math.hypot(a.to[0]-a.from[0],a.to[1]-a.from[1]))):o++}return{space:t,report:{wallsStraightened:r,wallsStraightenSkipped:o,maxStraightenShift:s},changed:r>0}}const qr=10**9,Br=240,Wr=1e-4;function Ur(e){if(Array.isArray(e))return e.map(e=>Ur(e));if(null!==e&&"object"==typeof e){const t={};for(const[i,n]of Object.entries(e))t[i]=Ur(n);return t}return e}function Gr(e){if("number"!=typeof e||!Number.isFinite(e))return e;const t=(e<0||Object.is(e,-0)?-1:1)*(Math.floor(Math.abs(e)*qr+.5)/qr);return 0===t?0:t}function Vr(e){if("number"!=typeof e||!Number.isFinite(e))return e;const t=e*Br,i=Math.round(t);if(Math.abs(t-i)null!==Yr(e)):[]}function Xr(e,t){for(const i of t)Object.prototype.hasOwnProperty.call(e,i)&&(e[i]=Gr(e[i]))}function Jr(e,t){for(const i of t)Object.prototype.hasOwnProperty.call(e,i)&&(e[i]=Vr(e[i]))}function Qr(e){if(Array.isArray(e))for(let t=0;t{for(const r of n){const n=t[r];"number"==typeof n&&Number.isFinite(n)&&i(n,e)}},n=t=>{if(Array.isArray(t))for(let n=0;n{if(Array.isArray(e))for(const t of e)n(t)};for(const i of Zr(e.rooms))t(i,["x","y","w","h"]),r(i.poly);for(const t of Zr(e.walls))n(t.a),n(t.b);for(const t of Zr(e.wall_segments))n(t.a),n(t.b);for(const i of Zr(e.openings))t(i,["x","y"]);for(const i of Zr(e.decor))"line"===i.kind?t(i,["x1","y1","x2","y2"]):"rect"===i.kind||"ellipse"===i.kind||"furniture"===i.kind?t(i,["x","y","w","h"]):"text"===i.kind&&t(i,["x","y"]);for(const t of Zr(e.room_drafts))r(t.points);for(const t of Zr(e.partitions))n(t.a),n(t.b);for(const t of Zr(e.wall_columns))n(t.center);for(const t of Zr(e.open_spans))n(t.a),n(t.b)}const r=Yr(t);if(!r)return;const o=new Map;if(n)for(const e of Zr(n.spaces))null!=e.id&&o.set(String(e.id),e);for(const e of Object.values(r)){const t=Yr(e);if(!t)continue;const n=null!=t.s&&o.get(String(t.s))||null;for(const e of["x","y"]){const r=t[e];"number"==typeof r&&Number.isFinite(r)&&i(r,n)}}}function io(e){const t=Number(e?.cell_cm);return t>0?t:5}function no(e,t={}){const i={canonicalized:0,far:0,maxShift:0,maxShiftCm:0,spaces:[],bySpace:new Map},n=Zr(Yr(e)?.spaces);let r=5;for(const e of n)r=Math.max(r,io(e));to(e,t,(e,t)=>{const n=e*Br,o=Math.abs(n-Math.round(n)),s=Vr(e),a=o>0&&o=Wr))return;const l=a&&"number"==typeof s?Math.abs(s-e):0;if(a?i.canonicalized++:i.far++,i.maxShift=Math.max(i.maxShift,l),i.maxShiftCm=Math.max(i.maxShiftCm,l*Br*(t?io(t):r)),!t?.id||!a)return;const c=String(t.id);let h=i.bySpace.get(c);h||(h={spaceId:c,space:String(t.title||c),canonicalized:0,far:0,maxShift:0,maxShiftCm:0},i.bySpace.set(c,h)),h.canonicalized++,h.maxShift=Math.max(h.maxShift,l),h.maxShiftCm=Math.max(h.maxShiftCm,l*Br*io(t))}),i.bySpace.size&&to(e,t,(e,t)=>{if(!t?.id)return;const n=i.bySpace.get(String(t.id));if(!n)return;const r=e*Br;Math.abs(r-Math.round(r))>=Wr&&n.far++}),i.spaces=[...i.bySpace.values()];const{bySpace:o,...s}=i;return s}function ro(e){return function(e){const t=e,i=Yr(t);i&&Jr(i,["x","y"]);return t}(Ur(e))}function oo(e){const t=e,i=Yr(t);if(!i)return t;for(const e of Object.values(i)){const t=Yr(e);t&&Jr(t,["x","y"])}return t}function so(e){return ao(Ur(e))}function ao(e){const t=e,i=Yr(t);if(!i)return t;for(const e of Zr(i.spaces)){Xr(e,["plan_x","plan_y","plan_scale","plan_scale_x","plan_scale_y","plan_angle"]);for(const t of Zr(e.rooms))Jr(t,["x","y","w","h"]),eo(t.poly);for(const t of Zr(e.walls))Qr(t.a),Qr(t.b);for(const t of Zr(e.wall_segments))Qr(t.a),Qr(t.b);for(const t of Zr(e.openings)){Jr(t,["x","y"]),Xr(t,["angle","length"]);const e=Yr(t.host);e&&Xr(e,["t"])}for(const t of Zr(e.decor))"line"===t.kind?Jr(t,["x1","y1","x2","y2"]):"rect"===t.kind||"ellipse"===t.kind||"furniture"===t.kind?(Jr(t,["x","y","w","h"]),Xr(t,["angle"])):"text"===t.kind&&(Jr(t,["x","y"]),Xr(t,["scale","angle"]));for(const t of Zr(e.room_drafts))eo(t.points);for(const t of Zr(e.partitions))Qr(t.a),Qr(t.b);for(const t of Zr(e.wall_columns))Qr(t.center),"square"===t.shape&&Xr(t,["angle"]);for(const t of Zr(e.open_spans))Qr(t.a),Qr(t.b)}for(const e of Zr(i.markers))Xr(e,["angle"]);return t}const lo=15,co=8;function ho(e,t,i,n,r){const o=i[0]-e[0],s=i[1]-e[1],a=Math.hypot(o,s);if(!(a>0)||a<=r+1e-9)return null;const l=o/a,c=s/a,h=t=>{const n=(t[0]-e[0])*l+(t[1]-e[1])*c,o=(r-n)/(a-n);return!Number.isFinite(o)||o<-1e-9||o>1+1e-9?null:[t[0]+(i[0]-t[0])*o,t[1]+(i[1]-t[1])*o]},d=h(t),u=h(n);return d&&u?[[e[0],e[1]],t,d,u,n]:null}const uo=Math.sin(.25*Math.PI/180);function po(e,t){return Number.isFinite(e)&&e>0&&Number.isFinite(t)&&t>0&&e*t<3}function _o(e){const t=Number(e)>0?Number(e):5;if(5===t)return 8;const i=5/t*8;return Math.min(80,Math.max(.5,i))}function mo(e,t){return Number.isFinite(e)&&e>0&&Number.isFinite(t)&&t>0&&e*t<2}function go(e){return Number.isFinite(e)?Math.max(1,Math.min(100,e)):1}function fo(e,t){return!Number.isFinite(e)||e<0?"":String(t?Math.round(e/2.54*100)/100:Math.round(100*e)/100)}function vo(e,t,i){if(!Number.isFinite(e)||e<=0)return 0;const n=Number(t)>0?Number(t):5;return go(e)/n*i}function yo(e,t){return t>0&&Number.isFinite(e)?Math.round(e/t)*t:e}function bo(e){return Math.max(1e-6*Math.abs(e),1e-9)}function wo(e,t){if(!(t>0&&Number.isFinite(e)))return e;const i=yo(e,t);return Math.abs(i-e)<=bo(t)?i:e}function ko(e,t){let i=t[0]-e[0],n=t[1]-e[1];const r=Math.hypot(i,n);return r<1e-12?[1,0]:(i/=r,n/=r,(i<-1e-12||Math.abs(i)<=1e-12&&n<0)&&(i=-i,n=-n),[i,n])}function xo(e,t,i){const n=[wo(e[0],i),wo(e[1],i)],r=[wo(t[0],i),wo(t[1],i)],o=yo((n[0]+r[0])/2,i),s=yo((n[1]+r[1])/2,i),[a,l]=ko(n,r);let c=Math.atan2(l,a);c<0&&(c+=Math.PI);const h=Math.round(1800*c)/1800,d=i>0&&i<.01?6:i<1?4:2;return`${o.toFixed(d)},${s.toFixed(d)}@${h.toFixed(4)}`}function $o(e,t,i,n){return 1===n?xo(e,t,i):xo([e[0]/n,e[1]/n],[t[0]/n,t[1]/n],i)}function So(e,t){if(!Array.isArray(e.a)||!Array.isArray(e.b)||e.a.length<2||e.b.length<2)return null;const i=[Number(e.a[0]),Number(e.a[1]),Number(e.b[0]),Number(e.b[1])];if(!i.every(Number.isFinite))return null;const n=t>0?t:1;return[[i[0]*n,i[1]*n],[i[2]*n,i[3]*n]]}function Mo(e,t,i,n,r){const o=r>0?r:1;return{key:$o(e,t,n,o),cm:go(i),a:[e[0]/o,e[1]/o],b:[t[0]/o,t[1]/o]}}function To(e,t){const i=t>0?t:1,n=[];for(const t of e){const e=t.key.lastIndexOf("@");if(e<0)continue;const[r,o]=t.key.slice(0,e).split(",").map(Number),s=Number(t.key.slice(e+1));[r,o,s].every(Number.isFinite)&&n.push({w:t,x:r*i,y:o*i,ang:s})}return n}function Ro(e,t){const[i,n]=ko(e,t);let r=Math.atan2(n,i);return r<0&&(r+=Math.PI),r}function Co(e,t){let i=Math.abs(e-t);return i>Math.PI/2&&(i=Math.PI-i),i<.02}function Do(e,t,i,n,r=1){if(!e?.length)return null;const o=$o(t,i,n,r),s=e.find(e=>e.key===o);if(s)return s;const a=r>0?r:1,l=bo(n)*a,c=(e,t)=>Math.abs(e[0]-t[0])<=l&&Math.abs(e[1]-t[1])<=l;for(const n of e){const e=So(n,a);if(e&&(c(e[0],t)&&c(e[1],i)||c(e[0],i)&&c(e[1],t)))return n}const h=(t[0]+i[0])/2,d=(t[1]+i[1])/2,u=Ro(t,i),p=Math.max(.5*n,1e-9)*a;for(const t of To(e,a))if(Co(t.ang,u)&&Math.hypot(t.x-h,t.y-d)<=p)return t.w;return null}function Ao(e,t,i,n,r=1){const o=Do(e,t,i,n,r);if(o&&o.cm>0)return go(o.cm);const s=function(e,t,i,n,r){if(!e?.length)return null;const o=r>0?r:1,s=Math.hypot(i[0]-t[0],i[1]-t[1]);if(s<1e-12)return null;const a=Ro(t,i),l=Math.max(.5*n,1e-9)*o;let c=null;for(const n of e){if(!(n.cm>0))continue;const e=So(n,o);if(!e)continue;const r=Math.hypot(e[1][0]-e[0][0],e[1][1]-e[0][1]);if(r<1e-12||!Co(Ro(e[0],e[1]),a))continue;if(bs(t[0],t[1],e[0][0],e[0][1],e[1][0],e[1][1])>l||bs(i[0],i[1],e[0][0],e[0][1],e[1][0],e[1][1])>l)continue;if(r+l(o.has(e.key)||(e=>{const t=So(e,n);if(!t)return!1;const[i,o]=t,a=o[0]-i[0],c=o[1]-i[1],h=Math.hypot(a,c);if(h<=l)return!1;if(!s.some(e=>{const t=[e[0],e[1]],n=[e[2],e[3]];return Co(Ro(i,o),Ro(t,n))&&bs(i[0],i[1],t[0],t[1],n[0],n[1])<=l&&bs(o[0],o[1],t[0],t[1],n[0],n[1])<=l}))return!1;const d=(r||[]).some(e=>{const t=[e[0],e[1]],n=[e[2],e[3]];if(!Co(Ro(i,o),Ro(t,n)))return!1;const r=e=>Math.abs((e[0]-i[0])*c-(e[1]-i[1])*a)/h;if(r(t)>l||r(n)>l)return!1;const s=h*h,d=((t[0]-i[0])*a+(t[1]-i[1])*c)/s,u=((n[0]-i[0])*a+(n[1]-i[1])*c)/s;return Math.min(1,Math.max(d,u))-Math.max(0,Math.min(d,u))>l/h});return!d})(e))&&e.cm>=1&&e.cm<=100)}function zo(e,t,i,n=8){const[r,o]=ko(e,t);let s=Math.atan2(o,r);s<0&&(s+=Math.PI);let a=i*Math.PI/180%Math.PI;a<0&&(a+=Math.PI);let l=Math.abs(s-a);return l>Math.PI/2&&(l=Math.PI-l),l<=n*Math.PI/180}function Po(e,t,i,n,r=1,o="affine",s){if(!e?.length)return[];if(t.length!==i.length)return"fixed-topology"===o&&s?.(),e.slice();const a=r>0?r:1,l=Math.max(.5*n,1e-9)*a,c=Math.max(n*a*1e-6,1e-9),h=[],d=new Map,u=new Map,p=(e,t,i)=>{const n=e.get(t)||new Set;n.add(i),e.set(t,n)};for(let e=0;e[e[0]+(t[0]-e[0])*i,e[1]+(t[1]-e[1])*i],m=(e,t)=>Math.hypot(e[0]-t[0],e[1]-t[1])<=c,g=(e,t)=>{if("fixed-topology"===o){const i=t.na[0]-t.oa[0],n=t.na[1]-t.oa[1],r=t.nb[0]-t.ob[0],o=t.nb[1]-t.ob[1];return Math.hypot(i-r,n-o)<=c?[e[0]+i,e[1]+n]:m(e,t.oa)?[...t.na]:m(e,t.ob)?[...t.nb]:[...e]}const i=Math.max(0,Math.min(1,((e[0]-t.oa[0])*t.dx+(e[1]-t.oa[1])*t.dy)/t.len2));return _(t.na,t.nb,i)},f=(e,t)=>{const[i,n]=ko(e,t);return(t[0]-e[0])*i+(t[1]-e[1])*n>=0?[[...e],[...t]]:[[...t],[...e]]},v=[],y=[],b=(e,t,i)=>{if(Math.hypot(t[0]-e[0],t[1]-e[1])<=c)return;const[r,o]=f(e,t),s=go(i);if(y.some(e=>e.entry.cm===s&&m(e.span[0],r)&&m(e.span[1],o)))return;const l=Mo(r,o,s,n,a);v.push(l),y.push({entry:l,span:[r,o]})};for(const t of e){const e=So(t,a);if(e){const[i,n]=f(e[0],e[1]),r=n[0]-i[0],s=n[1]-i[1],a=r*r+s*s,d=Math.sqrt(a);if(d<=c){v.push({...t,cm:go(t.cm)});continue}const u=[];for(const e of h){if(!Co(Ro(i,n),Ro(e.oa,e.ob)))continue;const t=e=>Math.abs((e[0]-i[0])*s-(e[1]-i[1])*r)/d;if(t(e.oa)>l||t(e.ob)>l)continue;const o=((e.oa[0]-i[0])*r+(e.oa[1]-i[1])*s)/a,h=((e.ob[0]-i[0])*r+(e.ob[1]-i[1])*s)/a,p=Math.max(0,Math.min(o,h)),_=Math.min(1,Math.max(o,h));(_-p)*d>c&&u.push({lo:p,hi:_,move:e})}if(!u.length){"fixed-topology"===o?v.push({...t}):b(i,n,t.cm);continue}const p=[0,1,...u.flatMap(({lo:e,hi:t})=>[e,t])].sort((e,t)=>e-t).filter((e,t,i)=>0===t||Math.abs(e-i[t-1])*d>c),y=[];for(let e=0;e+1a>=e.lo-1e-12&&a<=e.hi+1e-12);if(!l.length){y.push([o,s]);continue}const h=[g(o,l[0].move),g(s,l[0].move)],f=l.slice(1).some(e=>{const t=g(o,e.move),i=g(s,e.move);return!m(h[0],t)||!m(h[1],i)});y.push(f?[o,s]:h)}const w=(e,t)=>{const i=e[1][0]-e[0][0],n=e[1][1]-e[0][1],r=t[1][0]-t[0][0],o=t[1][1]-t[0][1],s=Math.hypot(i,n);return!(s<=c||i*r+n*o<=0)&&Math.abs(i*o-n*r)/s<=c},k=[];for(const e of y){const t=k[k.length-1];t&&m(t[1],e[0])&&w(t,e)?t[1]=e[1]:k.push([[...e[0]],[...e[1]]])}if("fixed-topology"===o&&1===k.length){const[i,n]=k[0];if(m(i,e[0])&&m(n,e[1])||m(i,e[1])&&m(n,e[0])){v.push({...t});continue}}for(const[e,i]of k)b(e,i,t.cm);continue}if("fixed-topology"===o){const e=u.get(t.key);if(1===e?.size){const i=[...e][0];v.push(i===t.key?{...t}:{...t,key:i});continue}const i=[To([t],a)[0]];1!==a&&i.push(To([t],1)[0]);const n=i.filter(Boolean).some(e=>h.some(t=>{if(!Co(e.ang,Ro(t.oa,t.ob)))return!1;const i=((e.x-t.oa[0])*t.dx+(e.y-t.oa[1])*t.dy)/t.len2;return i>=-1e-6&&i<=1.000001&&bs(e.x,e.y,t.oa[0],t.oa[1],t.ob[0],t.ob[1])<=l}));((e?.size||0)>1||n)&&s?.(),v.push({...t});continue}let i="";const r=d.get(t.key);if(1===r?.size&&(i=[...r][0]),!i){const e=To([t],a)[0];if(e){const t=new Set;for(const i of h){if(!Co(e.ang,Ro(i.oa,i.ob)))continue;const r=((e.x-i.oa[0])*i.dx+(e.y-i.oa[1])*i.dy)/i.len2;if(r<-1e-6||r>1.000001)continue;if(bs(e.x,e.y,i.oa[0],i.oa[1],i.ob[0],i.ob[1])>l)continue;const o=g([e.x,e.y],i),[s,c]=ko(i.na,i.nb),h=Math.max(n*a,1e-6);t.add($o([o[0]-s*h,o[1]-c*h],[o[0]+s*h,o[1]+c*h],n,a))}1===t.size&&(i=[...t][0])}}v.push({...t,key:i||t.key,cm:go(t.cm)})}return v}function Fo(e,t,i,n,r=1,o="affine"){return Po(e,t,i,n,r,o)}function Io(e,t,i,n,r=1,o="fixed-topology"){let s=!1;const a=Po(e,t,i,n,r,o,()=>{s=!0});return{walls:s?(e||[]).map(e=>({...e})):a,rejected:s}}function Eo(e,t,i,n=1,r=[]){const o=n>0?n:1,s=Math.abs(i),a=Math.max(s*o*Wr,1e-9),l=(r||[]).flatMap(e=>{const t=So(e,o);return t?t.map(e=>[...e]):[]}),c=e=>e.every((e,t)=>(e=>{if(!(s>0))return!0;const t=e/o/s;return Math.abs(t-Math.round(t))Math.abs(e-i[t])<=a)),h=[],d=e=>JSON.stringify([e.key,e.cm,e.a,e.b]);for(const i of e||[]){const e=So(i,o);if(!e)continue;const[n,r]=e;if(![n[0],n[1],r[0],r[1]].every(Number.isFinite)||!c(n)||!c(r)){h.push(d(i));continue}const s=r[0]-n[0],l=r[1]-n[1],u=Math.hypot(s,l);if(u<=a){h.push(d(i));continue}const p=s/u,_=l/u,m=[];for(const e of t){const[t,i]=e;if(![t?.[0],t?.[1],i?.[0],i?.[1]].every(Number.isFinite))continue;const r=e=>Math.abs((e[0]-n[0])*_-(e[1]-n[1])*p);if(r(t)>a||r(i)>a)continue;const o=(t[0]-n[0])*p+(t[1]-n[1])*_,s=(i[0]-n[0])*p+(i[1]-n[1])*_,l=Math.max(0,Math.min(o,s)),c=Math.min(u,Math.max(o,s));c-l>a&&m.push([l,c])}m.sort((e,t)=>e[0]-t[0]||e[1]-t[1]);let g=0;for(const[e,t]of m){if(e>g+a)break;if(g=Math.max(g,t),g>=u-a)break}ge.key!==s);return null==n||n<1?a:[...a,Mo(t,i,n,r,o)]}function Ho(e,t,i,n,r,o=[],s=1){let a=e?e.slice():[];for(const e of function(e,t,i,n,r=1,o=[]){const s=ss(e,t,i,n,r,o);if(!s)return[];const a=[];for(let e=0;e0||(c=No(c,t,i,n,r,s)))}return c}function jo(e){const{a:t,b:i,halfDepth:n}=e;if(!Array.isArray(t)||!Array.isArray(i)||t.length<2||i.length<2||![t[0],t[1],i[0],i[1]].every(Number.isFinite))return null;const r=i[0]-t[0],o=i[1]-t[1],s=Math.hypot(r,o);if(!(s>1e-9&&n>0&&Number.isFinite(n)))return null;const a=-o/s*n,l=r/s*n;return[[t[0]+a,t[1]+l],[i[0]+a,i[1]+l],[i[0]-a,i[1]-l],[t[0]-a,t[1]-l]]}function qo(e,t,i){return Math.hypot(e[0]-t[0],e[1]-t[1])<=i}function Bo(e,t,i){const n=t.b[0]-t.a[0],r=t.b[1]-t.a[1],o=n*n+r*r;if(!(o>i*i))return!1;const s=((e[0]-t.a[0])*n+(e[1]-t.a[1])*r)/o;if(!(s>0&&s<1))return!1;const a=[t.a[0]+n*s,t.a[1]+r*s];return Math.hypot(e[0]-a[0],e[1]-a[1])<=i}function Wo(e,t,i,n){const r=Math.hypot(t,i);if(!(r>1e-9&&n>0))return;const o=[t/r,i/r],s=e.find(e=>Math.abs(e.u[0]*o[1]-e.u[1]*o[0])<1e-9&&e.u[0]*o[0]+e.u[1]*o[1]>1-1e-9);s?s.halfDepth=Math.max(s.halfDepth,n):e.push({u:o,halfDepth:n})}function Uo(e,t){try{const i=Ht(xs(e),xs(t));let n=null,r=0;for(const e of i||[]){const t=e?.[0]||[],i=Math.abs(Xo(t));t.length>=4&&i>r&&(r=i,n=t.slice(0,-1).map(e=>[e[0],e[1]]))}return n}catch{return null}}function Go(e,t=1e-6){const i=(e||[]).map((e,t)=>({segment:e,index:t})).filter(({segment:e})=>e&&Array.isArray(e.a)&&Array.isArray(e.b)&&e.a.length>=2&&e.b.length>=2&&e.a.every(Number.isFinite)&&e.b.every(Number.isFinite)&&Number.isFinite(e.halfDepth)&&e.halfDepth>0&&Math.hypot(e.b[0]-e.a[0],e.b[1]-e.a[1])>1e-9);if(i.length<2)return[];const n=Math.max(Number.isFinite(t)?t:0,1e-9),r=i.flatMap(({segment:e})=>[e.a,e.b]).map(e=>[e[0],e[1]]).sort((e,t)=>e[0]-t[0]||e[1]-t[1]),o=[];for(const e of r)o.some(t=>qo(t,e,n))||o.push(e);const s=[];for(const e of o){const t=[];let r=!1;for(const{segment:o,index:s}of i){const i=Math.hypot(o.b[0]-o.a[0],o.b[1]-o.a[1]);qo(e,o.a,n)?t.push({u:[(o.b[0]-o.a[0])/i,(o.b[1]-o.a[1])/i],halfDepth:o.halfDepth,length:i,index:s}):qo(e,o.b,n)?t.push({u:[(o.a[0]-o.b[0])/i,(o.a[1]-o.b[1])/i],halfDepth:o.halfDepth,length:i,index:s}):Bo(e,o,n)&&(r=!0)}if(r||2!==t.length)continue;const[o,a]=t,l=o.u[0]*a.u[1]-o.u[1]*a.u[0];if(Math.abs(l)<1e-9)continue;const c=l<0?1:-1,h=[-o.u[1],o.u[0]],d=[-a.u[1],a.u[0]],u=[e[0]+h[0]*o.halfDepth*c,e[1]+h[1]*o.halfDepth*c],p=[e[0]-d[0]*a.halfDepth*c,e[1]-d[1]*a.halfDepth*c];if(!es(u,o.u,p,a.u))continue;const _=[[o,a,p,[d[0]*-c,d[1]*-c]],[a,o,u,[h[0]*c,h[1]*c]]];for(const[t,,i,r]of _){const o=Math.min(2*t.halfDepth,t.length),a=[-t.u[1]*t.halfDepth,t.u[0]*t.halfDepth],l=[[e[0]+a[0],e[1]+a[1]],[e[0]+t.u[0]*o+a[0],e[1]+t.u[1]*o+a[1]],[e[0]+t.u[0]*o-a[0],e[1]+t.u[1]*o-a[1]],[e[0]-a[0],e[1]-a[1]]],c=e=>(e[0]-i[0])*r[0]+(e[1]-i[1])*r[1],h=[];for(let e=0;e=-1e-12&&h.push(t),n>1e-12&&r<-1e-12||n<-1e-12&&r>1e-12){const e=n/(n-r);h.push([t[0]+(i[0]-t[0])*e,t[1]+(i[1]-t[1])*e])}}h.length>=3&&Math.abs(Xo(h))>n*n&&s.push({segmentIndex:t.index,wedge:h})}}return s}function Vo(e,t=1e-6){const i=(e||[]).filter(e=>e&&Array.isArray(e.a)&&Array.isArray(e.b)&&e.a.length>=2&&e.b.length>=2&&e.a.every(Number.isFinite)&&e.b.every(Number.isFinite)&&Number.isFinite(e.halfDepth)&&e.halfDepth>0&&Math.hypot(e.b[0]-e.a[0],e.b[1]-e.a[1])>1e-9);if(i.length<2)return[];const n=Math.max(Number.isFinite(t)?t:0,1e-9),r=i.flatMap(e=>[e.a,e.b]).map(e=>[e[0],e[1]]).sort((e,t)=>e[0]-t[0]||e[1]-t[1]),o=[];for(const e of r)o.some(t=>qo(t,e,n))||o.push(e);const s=[],a=i.map((e,t)=>({roomId:"",a:[e.a[0],e.a[1]],b:[e.b[0],e.b[1]],key:`join-${t}`,kind:"outer",cm:0,open:!1,half:e.halfDepth})),l=ds(a,n);for(const e of us(l).fans)s.push(e);const c=e=>!!ps(l,e);for(const e of o){if(c(e))continue;const t=[];for(const r of i)qo(e,r.a,n)?Wo(t,r.b[0]-r.a[0],r.b[1]-r.a[1],r.halfDepth):qo(e,r.b,n)?Wo(t,r.a[0]-r.b[0],r.a[1]-r.b[1],r.halfDepth):Bo(e,r,n)&&(Wo(t,r.a[0]-e[0],r.a[1]-e[1],r.halfDepth),Wo(t,r.b[0]-e[0],r.b[1]-e[1],r.halfDepth));if(!(t.length<2)){t.sort((e,t)=>Math.atan2(e.u[1],e.u[0])-Math.atan2(t.u[1],t.u[0])||e.halfDepth-t.halfDepth);for(let i=0;in*n&&s.push(m)}}}return s}function Ko(e,t,i,n){if(!(t>0)||!e||e.length<2)return"";if(i&&e.length>=3){let i=e;const r=e[e.length-1];if(e.length>=4&&Math.hypot(e[0][0]-r[0],e[0][1]-r[1])<1e-9&&(i=e.slice(0,-1)),i.length>=3){const e=i.map((e,i)=>n?.[i]||t),r=qs(i,e),o=ns(i,e);if(r&&o)return`${rs(r)} ${rs(os(o))}`}}const r=[];for(let i=0;i=1e-9&&a>0&&r.push({a:o,b:s,halfDepth:a})}const o=r.map(jo);for(const{segmentIndex:e,wedge:t}of Go(r)){const i=o[e];if(!i)continue;const n=Uo(i,t);n&&(o[e]=n)}const s=[...o.filter(e=>!!e),...Vo(r)],a=function(e){let t=null;try{for(const i of e){if(i.length<3)continue;const e=xs(i);t=t?Et(t,e):[e]}return t}catch{return null}}(s);return a?Fs(a):s.map(e=>rs(e)).join(" ")}function Yo(e,t,i,n,r=1){if(!i.length)return!1;const o=Zo(n,r),s=(e[0]+t[0])/2,a=(e[1]+t[1])/2,[l,c]=ko(e,t);for(const e of i){const[t,i]=ko([e[0],e[1]],[e[2],e[3]]);if(!(Math.abs(l*i-c*t)>.05)&&bs(s,a,e[0],e[1],e[2],e[3])<=o)return!0}return!1}function Zo(e,t){return Math.max(e*(t>0?t:1)*.04,1e-9)}function Xo(e){let t=0;for(let i=0;ie[1]!=l>e[1]&&e[0]<(a-o)*(e[1]-s)/(l-s+0)+o&&(i=!i)}return i}([c[0]+.001*a,c[1]+.001*l],e)||(a=-a,l=-l),[a,l]}function Qo(e,t){const i=e[0]*t[1]-e[1]*t[0],n=e[0]*t[0]+e[1]*t[1];return Math.abs(i)<1e-9&&n>0}function es(e,t,i,n){const r=t[0]*n[1]-t[1]*n[0];if(Math.abs(r)<1e-12)return null;const o=[i[0]-e[0],i[1]-e[1]],s=(o[0]*n[1]-o[1]*n[0])/r;return[e[0]+s*t[0],e[1]+s*t[1]]}function ts(e,t,i){const n=e?.length||0;if(n<3||!Array.isArray(i)||i.length!==n)return 0;const r=(Math.trunc(t)%n+n)%n,o=e[r],s=e[(r+1)%n],a=s[0]-o[0],l=s[1]-o[1],c=Math.hypot(a,l);if(!(c>0))return 0;const h=Math.max(0,Number(i[r])||0);if(!(h>0))return c;const d=[a/c,l/c],u=Jo(e,r),p=[o[0]+u[0]*h,o[1]+u[1]*h],_=t=>{const r=Math.max(0,Number(i[t])||0);if(!(r>0))return null;const s=e[t],a=e[(t+1)%n],l=a[0]-s[0],c=a[1]-s[1],h=Math.hypot(l,c);if(!(h>0))return null;const u=Jo(e,t),_=es(p,d,[s[0]+u[0]*r,s[1]+u[1]*r],[l/h,c/h]);return _?(_[0]-o[0])*d[0]+(_[1]-o[1])*d[1]:null},m=_((r-1+n)%n)??0,g=(_((r+1)%n)??c)-m;return g>0?g:0}function is(e,t,i,n,r,o,s,a=1){const l=(e||[]).find(e=>e?.id===t),c=Zt(l);if(!c||c.length<3)return null;const h=ms(e,t,i,n,r,o,s,a);if(!h)return c.map(()=>0);const d=4*Zo(r,a);return c.map((e,t)=>{const i=c[(t+1)%c.length],n=[(e[0]+i[0])/2,(e[1]+i[1])/2];for(let e=0;e!(e>0)))return e.map(e=>[e[0],e[1]]);const r=[];for(let o=0;o0||u>0)){r.push([e[o][0],e[o][1]]);continue}if(d>0!=u>0){const t=e[o],i=d>0?[t[0]+p*d,t[1]+_*d]:[t[0],t[1]],n=u>0?[t[0]+m*u,t[1]+g*u]:[t[0],t[1]];r.push(i),Math.hypot(n[0]-i[0],n[1]-i[1])>1e-9&&r.push(n);continue}if(Qo(w,k)){const t=e[o],i=[t[0]+p*d,t[1]+_*d],n=[t[0]+m*u,t[1]+g*u];r.push(i),Math.hypot(n[0]-i[0],n[1]-i[1])>1e-9&&r.push(n);continue}const S=es(x,w,$,k),M=Math.max(d,u,1e-9),T=ps(i,e[o])?.limit??4*M;if(S){const i=Math.hypot(S[0]-e[o][0],S[1]-e[o][1]);if(Number.isFinite(i)&&i<=T){r.push(S);continue}if(Es(e,t,o)){r.push(S);continue}}d>0&&r.push([e[o][0]+p*d,e[o][1]+_*d]),u>0&&r.push([e[o][0]+m*u,e[o][1]+g*u]),d>0||u>0||r.push([e[o][0],e[o][1]])}return r.length>=3?r:null}function rs(e,t=!0){if(!e.length)return"";let i=`M ${e[0][0]} ${e[0][1]}`;for(let t=1;te?.id===t),a=Zt(s);if(!a||a.length<3)return null;const l=Zo(n,r),c=[];for(const i of e||[]){if(!i||i.id===t)continue;const e=Zt(i);if(e)for(const t of bn(a,e,l))c.push([t[0],t[1]],[t[2],t[3]])}for(const e of i||[])c.push([e[0],e[1]],[e[2],e[3]]);for(const e of o||[]){const t=So(e,r);t&&c.push(t[0],t[1])}const h=[],d=[];for(let e=0;el)continue;const s=((e[0]-t[0])*(i[0]-t[0])+(e[1]-t[1])*(i[1]-t[1]))/(n*n);s<=r||s>=1-r||(o.some(e=>Math.abs(e-s)*n<=2*l)||o.push(s))}o.sort((e,t)=>e-t);for(const n of o)h.push([t[0]+(i[0]-t[0])*n,t[1]+(i[1]-t[1])*n]),d.push(e)}return{poly:h,parent:d,orig:a}}function as(e,t,i,n){const r=t.poly.length,o=new Array(r).fill(0);if(!e?.length)return o;const s=new Set,a=[];for(let l=0;l0?(o[l]=go(c.cm),s.add(c.key)):a.push(l)}if(!a.length)return o;const l=n>0?n:1,c=Math.max(.5*i,1e-9)*l,h=To(e,l).filter(e=>e.w.cm>0);for(let e=a.length-1;e>=0;e--){const i=a[e],n=t.poly[i],s=t.poly[(i+1)%r],d=Ro(n,s);let u=null;for(const e of h){const t=So(e.w,l);if(!t||!Co(Ro(t[0],t[1]),d))continue;if(bs(n[0],n[1],t[0][0],t[0][1],t[1][0],t[1][1])>c||bs(s[0],s[1],t[0][0],t[0][1],t[1][0],t[1][1])>c)continue;const i=Math.hypot(s[0]-n[0],s[1]-n[1]),r=Math.hypot(t[1][0]-t[0][0],t[1][1]-t[0][1]),o=Math.max(0,r-i);(!u||oc||bs(r[0],r[1],t[0][0],t[0][1],t[1][0],t[1][1])>c)continue;i=!0,o=Math.max(0,Math.hypot(t[1][0]-t[0][0],t[1][1]-t[0][1])-_)}else{if(bs(e.x,e.y,n[0],n[1],r[0],r[1])>c)continue;o=Math.hypot(e.x-d,e.y-u)}(!p||i&&!p.exact||i===p.exact&&o0?i:1,o=(e||[]).filter(e=>e&&!e.open&&null!==e.kind&&Number.isFinite(e.half)&&e.half>0&&Array.isArray(e.a)&&Array.isArray(e.b)&&e.a.length>=2&&e.b.length>=2&&[e.a[0],e.a[1],e.b[0],e.b[1]].every(Number.isFinite)&&Math.hypot(e.b[0]-e.a[0],e.b[1]-e.a[1])>n).sort((e,t)=>e.key.localeCompare(t.key)||e.a[0]-t.a[0]||e.a[1]-t.a[1]||e.b[0]-t.b[0]||e.b[1]-t.b[1]||e.half-t.half),s=new Map;for(const e of o){const t=s.get(e.key);t?e.half>t.half&&s.set(e.key,{...t,half:e.half}):s.set(e.key,e)}const a=[...s.values()].flatMap(e=>[{point:[e.a[0],e.a[1]],other:e.b,halfDepth:e.half,kind:e.kind,key:e.key},{point:[e.b[0],e.b[1]],other:e.a,halfDepth:e.half,kind:e.kind,key:e.key}]).sort((e,t)=>e.point[0]-t.point[0]||e.point[1]-t.point[1]||e.other[0]-t.other[0]||e.other[1]-t.other[1]||e.halfDepth-t.halfDepth),l=new Map;for(const e of a){const[t,i]=ls(e.point,n),r=cs(t,i),o=l.get(r)||[];o.push(e),l.set(r,o)}const c=[],h=new Map;for(const e of a){const t=hs(h,e.point,n).filter(t=>Math.hypot(t.point[0]-e.point[0],t.point[1]-e.point[1])<=n).sort((t,i)=>Math.hypot(t.point[0]-e.point[0],t.point[1]-e.point[1])-Math.hypot(i.point[0]-e.point[0],i.point[1]-e.point[1])||t.point[0]-i.point[0]||t.point[1]-i.point[1]);let i=t[0];if(!i){i={point:[...e.point],rays:[]},c.push(i);const[t,r]=ls(i.point,n),o=cs(t,r),s=h.get(o)||[];s.push(i),h.set(o,s)}const r=e.other[0]-e.point[0],o=e.other[1]-e.point[1],s=Math.hypot(r,o);if(!(s>n))continue;const a=[r/s,o/s];let l=Math.atan2(a[1],a[0]);l<0&&(l+=2*Math.PI),i.rays.push({u:a,halfDepth:e.halfDepth,length:s,angle:l})}const d=[],u=e=>{const t=1e-9*Math.max(1,r),i=e.filter(e=>Number.isFinite(e.halfDepth)&&e.halfDepth>0&&Number.isFinite(e.length)&&e.length>n);return i.filter((e,n)=>!i.some((i,r)=>r!==n&&i.halfDepth>=e.halfDepth-t&&i.length>=e.length-t&&(i.halfDepth>e.halfDepth+t||i.length>e.length+t||re.length-t.length||e.halfDepth-t.halfDepth).map(e=>({...e}))};for(const e of c){const t=e.rays.sort((e,t)=>e.angle-t.angle||e.length-t.length||e.halfDepth-t.halfDepth),i=[];for(const e of t){const t=i[i.length-1];t&&Math.abs(e.angle-t.angle)<=1e-9?t.supports.push({halfDepth:e.halfDepth,length:e.length}):i.push({u:[...e.u],angle:e.angle,supports:[{halfDepth:e.halfDepth,length:e.length}]})}if(i.length>1&&2*Math.PI-i[i.length-1].angle+i[0].angle<=1e-9){const e=i.pop();i[0].supports.push(...e.supports)}if(i.length<3)continue;const r=i.map(t=>{const i=u(t.supports),r=new Set,o=[];for(const s of i){const i=[e.point[0]+t.u[0]*s.length,e.point[1]+t.u[1]*s.length];for(const t of hs(l,i,n)){if("shared"!==t.kind||Math.hypot(t.point[0]-i[0],t.point[1]-i[1])>n)continue;const s=t.other[0]-t.point[0],a=t.other[1]-t.point[1],l=Math.hypot(s,a);if(!(l>n))continue;const c=s/l,h=a/l;if(Math.hypot(t.other[0]-e.point[0],t.other[1]-e.point[1])<=n)continue;const d=`${t.key}|${t.point[0]}|${t.point[1]}|${t.other[0]}|${t.other[1]}|${t.halfDepth}`;r.has(d)||(r.add(d),o.push({start:[t.point[0],t.point[1]],u:[c,h],length:l,halfDepth:t.halfDepth}))}}return o.sort((e,t)=>e.start[0]-t.start[0]||e.start[1]-t.start[1]||e.u[0]-t.u[0]||e.u[1]-t.u[1]||e.length-t.length||e.halfDepth-t.halfDepth),{u:[...t.u],halfDepth:Math.max(...i.map(e=>e.halfDepth)),length:Math.max(...i.map(e=>e.length)),supports:i,continuations:o}}).filter(e=>Number.isFinite(e.halfDepth)&&e.halfDepth>0&&Number.isFinite(e.length)&&e.length>n);if(r.length<3)continue;const o=Math.max(...r.map(e=>e.halfDepth));o>0&&Number.isFinite(o)&&d.push({point:[...e.point],rays:r,halfDepth:o,limit:1.25*o})}d.sort((e,t)=>e.point[0]-t.point[0]||e.point[1]-t.point[1]);const p=new Map;for(const e of d){const[t,i]=ls(e.point,n),r=cs(t,i),o=p.get(r)||[];o.push(e),p.set(r,o)}return{epsilon:n,coordinateScale:r,nodes:d,index:p}}function us(e){const t={fans:[],supports:[]};if(!e?.nodes?.length)return t;const i=Math.max(e.epsilon,1e-9)**2;for(const n of e.nodes){const e=n.rays.filter(e=>Number.isFinite(e.halfDepth)&&e.halfDepth>0).map(e=>({...e,thickLength:Math.max(...e.supports.filter(t=>t.halfDepth>=e.halfDepth-1e-12).map(e=>e.length),0),angle:(()=>{const t=Math.atan2(e.u[1],e.u[0]);return t<0?t+2*Math.PI:t})()})).sort((e,t)=>e.angle-t.angle);if(e.length<2)continue;const r=n.point;for(const i of e)for(const e of i.supports){if(!(e.halfDepth>0&&e.length>0))continue;const n=-i.u[1]*e.halfDepth,o=i.u[0]*e.halfDepth,s=[r[0]+i.u[0]*e.length,r[1]+i.u[1]*e.length];t.supports.push([[r[0]+n,r[1]+o],[s[0]+n,s[1]+o],[s[0]-n,s[1]-o],[r[0]-n,r[1]-o]])}for(let n=0;n{const e=s.angle-o.angle;return e>0?e:e+2*Math.PI})();if(a<1e-9)continue;const l=a>Math.PI+1e-9,c=4*Math.max(o.halfDepth,s.halfDepth),h=[r[0]-o.u[1]*o.halfDepth,r[1]+o.u[0]*o.halfDepth],d=[r[0]+s.u[1]*s.halfDepth,r[1]-s.u[0]*s.halfDepth],u=o.u[0]*s.u[1]-o.u[1]*s.u[0],p=e=>{let t=Math.atan2(e[1]-r[1],e[0]-r[0])-o.angle;for(;t<0;)t+=2*Math.PI;return t<=a+1e-9};let _=null;if(Math.abs(u)>1e-9){const e=((d[0]-h[0])*s.u[1]-(d[1]-h[1])*s.u[0])/u,t=((d[0]-h[0])*o.u[1]-(d[1]-h[1])*o.u[0])/u,i=[h[0]+o.u[0]*e,h[1]+o.u[1]*e];(l?e<=1e-9&&t<=1e-9:e>1e-9&&e<=o.thickLength&&t<=s.thickLength)&&Math.hypot(i[0]-r[0],i[1]-r[1])<=c&&p(i)&&(_=i)}const m=e=>{Math.abs(Xo(e))>i&&t.fans.push(e)};if(_){const e=1.5*Math.max(o.halfDepth,s.halfDepth);m(ho([r[0],r[1]],h,_,d,e)??[[r[0],r[1]],h,_,d]);continue}if(l){m([[r[0],r[1]],h,d]);continue}const g=(e,t)=>Math.min(t,Math.sqrt(Math.max(c**2-e**2,0)),2*Math.max(o.halfDepth,s.halfDepth)),f=[h[0]+o.u[0]*g(o.halfDepth,o.thickLength),h[1]+o.u[1]*g(o.halfDepth,o.thickLength)],v=[d[0]+s.u[0]*g(s.halfDepth,s.thickLength),d[1]+s.u[1]*g(s.halfDepth,s.thickLength)];m([[r[0],r[1]],h,f,v,d])}}return t}function ps(e,t){return!e||!Array.isArray(t)||t.length<2||!t.slice(0,2).every(Number.isFinite)?null:hs(e.index,t,e.epsilon).filter(i=>Math.hypot(i.point[0]-t[0],i.point[1]-t[1])<=e.epsilon).sort((e,i)=>Math.hypot(e.point[0]-t[0],e.point[1]-t[1])-Math.hypot(i.point[0]-t[0],i.point[1]-t[1])||e.point[0]-i.point[0]||e.point[1]-i.point[1])[0]||null}function _s(e,t,i,n,r,o,s){return ds(gs(e,t,i,n,r,o,s),4*Zo(n,s),s)}function ms(e,t,i,n,r,o,s,a=1){const l=ss(e,t,n,r,a,i);if(!l)return null;const c=function(e,t,i){const n=(e||[]).find(e=>e?.id===t),r=Zt(n);if(!r)return[];const o=[];for(const n of e||[]){if(!n||n.id===t)continue;const e=Zt(n);if(e)for(const t of bn(r,e,i))o.push(t)}return o}(e,t,Zo(r,a)),h=function(e,t,i,n,r){const o=Zo(n,r),s=[];for(let a=0;abs(h,d,e[0],e[1],e[2],e[3])<=o);s.push(u?"shared":"outer")}return s}(l.poly,c,n,r,a),d=as(i,l,r,a),u=d.map((e,t)=>h[t]&&e>0?vo(e,o,s)/2:0);return{...l,kinds:h,cms:d,offsets:u}}function gs(e,t,i,n,r,o,s=1){const a=[];for(const l of e||[]){if(!l?.id)continue;const c=ms(e,l.id,t,i,n,r,o,s);if(c)for(let e=0;e0&&(a=No(a,e.a,e.b,e.cm,n,s));return a}function vs(e,t,i,n,r,o,s=1){if(!t?.length)return[];const a=gs(e,t,i,n,r,o,s),l=new Map;for(const e of a){if(e.open||!e.kind||!e.roomId)continue;const t=l.get(e.key)||new Set;t.add(e.roomId),l.set(e.key,t)}const c=e=>{const t=[...l.get(e)||[]].sort();return 1!==t.length&&2!==t.length?`ambiguous:${e}`:`${1===t.length?"outer":"shared"}:${t.join("|")}`},h=[],d=new Set;for(const e of a)!e.open&&e.cm>0&&!d.has(e.key)&&(d.add(e.key),h.push({...e,ownerSignature:c(e.key)}));const u=[];for(const a of e||[]){if(!a?.id)continue;const l=ms(e,a.id,t,i,n,r,o,s);if(l)for(let e=0;e0)||null===l.kinds[i]){e++;continue}const o=c($o(l.poly[i],l.poly[(i+1)%l.poly.length],n,s));let a=e;for(;a+10&&u.push({a:[d[0],d[1]],b:[p[0],p[1]],key:$o(d,p,n,s),cm:r,len:_,ownerSignature:o}),e=a+1}}}u.sort((e,t)=>t.len-e.len||e.key.localeCompare(t.key));const p=[],_=new Set,m=new Set,g=4*Zo(n,s);for(const e of u){const t=h.filter(t=>!m.has(t.key)&&t.cm===e.cm&&t.ownerSignature===e.ownerSignature&&Co(Ro(t.a,t.b),Ro(e.a,e.b))&&bs(t.a[0],t.a[1],e.a[0],e.a[1],e.b[0],e.b[1])<=g&&bs(t.b[0],t.b[1],e.a[0],e.a[1],e.b[0],e.b[1])<=g);if(t.length){for(const e of t)m.add(e.key);_.has(e.key)||(_.add(e.key),p.push(Mo(e.a,e.b,e.cm,n,s)))}}for(const e of h)m.has(e.key)||_.has(e.key)||(_.add(e.key),p.push(Mo(e.a,e.b,e.cm,n,s)));return p}function ys(e,t,i,n,r,o,s,a=1){const l=Zo(r,a),c=(n[0]+n[2])/2,h=(n[1]+n[3])/2,d=Ro([n[0],n[1]],[n[2],n[3]]);let u=null;for(const n of gs(e,t,i,r,o,s,a)){if(!Co(Ro(n.a,n.b),d))continue;const e=bs(c,h,n.a[0],n.a[1],n.b[0],n.b[1]);e>4*l||(!u||e[e[0],e[1]]),o=Math.abs(Xo(r));r.length>=3&&o>i&&(t=r,i=o)}return t}function ks(e,t,i,n,r,o,s,a=1,l,c){const h=(e||[]).find(e=>e?.id===t),d=Zt(h);if(!d||d.length<3)return null;if(!i?.length)return d.map(e=>[e[0],e[1]]);const u=ms(e,t,i,n,r,o,s,a);if(!u||!u.offsets.some(e=>e>0))return d.map(e=>[e[0],e[1]]);const p=c||_s(e,i,n,r,o,s,a),_=ns(u.poly,u.offsets,p);if(!_)return d.map(e=>[e[0],e[1]]);if(!p.nodes.length)return _;const m=void 0===l?Ns(e,i,n,[],r,o,s,a):null,g=l??("ok"===m?.status||"degraded-extra"===m?.status?m.roomGeom:void 0);if(g)try{const e=ws(Ht(xs(u.poly),g));if(e)return e}catch{}return function(e,t){try{return ws(Nt(xs(e),xs(t)))}catch{return null}}(_,u.poly)||d.map(e=>[e[0],e[1]])}function xs(e){const t=e.map(e=>[e[0],e[1]]);return t.push([e[0][0],e[0][1]]),[t]}function $s(e){return!(!Array.isArray(e)||!e.length)&&e.every(e=>Array.isArray(e)&&e.length&&e.every(e=>Array.isArray(e)&&e.length>=4&&e.every(e=>Array.isArray(e)&&e.length>=2&&Number.isFinite(e[0])&&Number.isFinite(e[1])))&&Math.abs(yi(e[0]))>1e-9)}function Ss(e,t,i,n){const r=n?new Set(n):null;let o=null;for(let n=0;nt.epsilon))continue;const l=Ds([[e.point[0]+a[0]*n.halfDepth,e.point[1]+a[1]*n.halfDepth],[e.point[0]+s.u[0]*r+a[0]*n.halfDepth,e.point[1]+s.u[1]*r+a[1]*n.halfDepth],[e.point[0]+s.u[0]*r-a[0]*n.halfDepth,e.point[1]+s.u[1]*r-a[1]*n.halfDepth],[e.point[0]-a[0]*n.halfDepth,e.point[1]-a[1]*n.halfDepth]],t.coordinateScale);if(!l)continue;const c=xs(l);o=o?Et(o,c):c}}return o}function Ms(e,t,i){let n=null;for(const r of e.rays)for(const e of r.continuations){const r=[-e.u[1],e.u[0]],o=[e.start[0]+e.u[0]*e.length,e.start[1]+e.u[1]*e.length],s=Ds([[e.start[0]+r[0]*e.halfDepth,e.start[1]+r[1]*e.halfDepth],[o[0]+r[0]*e.halfDepth,o[1]+r[1]*e.halfDepth],[o[0]-r[0]*e.halfDepth,o[1]-r[1]*e.halfDepth],[e.start[0]-r[0]*e.halfDepth,e.start[1]-r[1]*e.halfDepth]],t.coordinateScale);if(!s)continue;const a=Nt(xs(s),i);Array.isArray(a)&&0!==a.length&&(n=n?Et(n,a):a)}return n}function Ts(e,t,i=2*(4*e.halfDepth+2*t.epsilon)){const n=function(e,t=uo){const i=new Set,n=Number.isFinite(t)&&t>=0?t:uo;for(let t=0;te-t)}(e);return n.length?Ss(e,t,i,n):null}function Rs(e,t,i,n,r){const o=function(e){let t=null;for(const i of e){const e=xs(i);try{t=t?Et(t,e):e}catch{}}return t}(function(e,t,i=!1){if(!e)return[];const n=[];for(const r of e.nodes)for(let o=0;o1e-9)||h>=Math.PI-1e-9)continue;const d=[-s.u[1],s.u[0]],u=[-a.u[1],a.u[0]],p=[r.point[0]+d[0]*s.halfDepth,r.point[1]+d[1]*s.halfDepth],_=[r.point[0]-u[0]*a.halfDepth,r.point[1]-u[1]*a.halfDepth],m=es(p,s.u,_,a.u);if(!m)continue;const g=Math.hypot(m[0]-r.point[0],m[1]-r.point[1]);if(!Number.isFinite(g)||g<=r.limit)continue;const f=t?Math.sqrt(Math.max(0,r.limit*r.limit-s.halfDepth*s.halfDepth)):0,v=t?Math.sqrt(Math.max(0,r.limit*r.limit-a.halfDepth*a.halfDepth)):0,y=Ds([[p[0]+s.u[0]*f,p[1]+s.u[1]*f],[_[0]+a.u[0]*v,_[1]+a.u[1]*v],m],e.coordinateScale);if(y&&n.push(y),i){const t=m[0]-r.point[0],i=m[1]-r.point[1],o=Math.hypot(t,i),s=g-r.limit;if(o>e.epsilon&&s>e.epsilon){const a=Math.min(Math.max(8*e.epsilon,.05*r.halfDepth),.25*s),l=t/o,c=i/o,h=-c,d=l,u=Ds([[m[0]-l*a+h*a,m[1]-c*a+d*a],[m[0]-l*a-h*a,m[1]-c*a-d*a],[m[0]+l*a,m[1]+c*a]],e.coordinateScale);u&&n.push(u)}}}return n}({...t,nodes:[e]},i,n));return o&&r?Ht(o,r):o}function Cs(e,t,i,n){if(!e||!t.nodes.length)return e;let r=null;try{r=function(e){let t=null;for(const i of e.nodes){const n=Ts(i,e);n&&(t=t?Et(t,n):n)}return t}(t)}catch{return e}let o=e;for(const e of t.nodes){const s=4*e.halfDepth+2*t.epsilon,a=2*s,l=[[e.point[0]-s,e.point[1]-s],[e.point[0]+s,e.point[1]-s],[e.point[0]+s,e.point[1]+s],[e.point[0]-s,e.point[1]+s]];try{let s=o;const c=Rs(e,t,!1,!0,r);c&&(s=Ht(s,c));let h=Ss(e,t,a);const d=Rs(e,t,!0,!0,r);d&&(h=Ht(h,d)),r&&(h=Et(h,r));const u=.02*Math.min(...e.rays.map(e=>e.halfDepth));if(h=Et(h,xs([[e.point[0]-u,e.point[1]-u],[e.point[0]+u,e.point[1]-u],[e.point[0]+u,e.point[1]+u],[e.point[0]-u,e.point[1]+u]])),!h)continue;let p=Nt(h,xs(l));n?p=Nt(p,n):i&&(p=Nt(p,i));const _=xs(l),m=Ht(s,_),g=i?Ht(Nt(s,_),i):null,f=Ms(e,t,_);o=Et(m,...g?[g]:[],...f?[f]:[],p)}catch{}}if(r)try{let e=r;n?e=Nt(e,n):i&&(e=Nt(e,i)),o=Et(o,e)}catch{}return o}function Ds(e,t=1){if(!Array.isArray(e)||e.length<3)return null;const i=Number.isFinite(t)&&t>0?t:1,n=1e-12*Math.max(1,i),r=[];for(const t of e){if(!Array.isArray(t)||t.length<2)return null;const e=Number(t[0]),i=Number(t[1]);if(!Number.isFinite(e)||!Number.isFinite(i))return null;const o=Math.round(e/n)*n,s=Math.round(i/n)*n;if(!Number.isFinite(o)||!Number.isFinite(s))return null;r.push([Object.is(o,-0)?0:o,Object.is(s,-0)?0:s])}return Math.abs(Xo(r))>n*n?r:null}function As(e,t,i,n){if(bs(e[0],e[1],t[0],t[1],i[0],i[1])>n)return!1;const r=i[0]-t[0],o=i[1]-t[1],s=(e[0]-t[0])*r+(e[1]-t[1])*o,a=r*r+o*o,l=n*Math.sqrt(a);return s>=-l&&s<=a+l}function Os(e,t,i){const n=[],r=[];for(let o=0;oi*i))continue;const d=i/Math.sqrt(h),u=[0,1];for(const e of t)for(const t of[e.a,e.b]){if(!As(t,s,a,i))continue;const e=((t[0]-s[0])*l+(t[1]-s[1])*c)/h;e>d&&e<1-d&&u.push(e)}u.sort((e,t)=>e-t);const p=u.filter((e,t)=>0===t||Math.abs(e-u[t-1])>d);for(let e=0;e=3&&r.length===n.length?{poly:n,offsets:r}:null}function zs(e,t,i,n,r,o,s,a){const l=(e||[]).map(Zt).filter(e=>!!e&&e.length>=3);if(!l.length)return null;let c=Et(xs(l[0]));for(let e=1;e"outer"===e.kind&&e.half>0),u=4*Zo(n,s),p=a||ds(h,u,s);let _=null;for(const e of function(e){const t=[];for(const i of Array.isArray(e)?e:[])if(Array.isArray(i))for(const e of i){if(!Array.isArray(e)||e.length<4)continue;const i=e.slice(0,-1).map(e=>[e[0],e[1]]);i.length>=3&&t.push(i)}return t}(c)){const t=Os(e,d,u);if(!t||!t.offsets.some(e=>e>0))continue;const i=qs(t.poly,t.offsets,p),n=ns(t.poly,t.offsets,p);if(!i||!n)continue;const r=Ht(xs(i),xs(n));_=_?Et(_,r):r}return{centre:c,shell:_||[]}}function Ps(e,t,i,n,r,o,s=1){try{const a=zs(e,t,i,n,r,o,s,_s(e,t,i,n,r,o,s));if(!a)return[];return a.shell?.length?Et(a.centre,a.shell):a.centre}catch{return null}}function Fs(e){if(!e)return"";let t="";for(const i of e)if(Array.isArray(i))for(const e of i){if(!Array.isArray(e)||e.length<4)continue;const i=e.slice(0,e.length-1);i.length<3||(t+=(t?" ":"")+rs(i.map(e=>[e[0],e[1]])))}return t}const Is=15;function Es(e,t,i){const n=e?.length||0;if(n<3||t?.length!==n)return!1;const r=e[(i-1+n)%n],o=e[i],s=e[(i+1)%n],a=[r[0]-o[0],r[1]-o[1]],l=[s[0]-o[0],s[1]-o[1]],c=Math.hypot(a[0],a[1]),h=Math.hypot(l[0],l[1]);if(!(c>1e-9&&h>1e-9))return!1;const d=Math.max(-1,Math.min(1,(a[0]*l[0]+a[1]*l[1])/(c*h))),u=Math.acos(d);if(!(u>1e-9))return!1;if(u>=Is*Math.PI/180)return!1;const p=Math.max(Math.max(0,t[(i-1+n)%n]),Math.max(0,t[i]));if(!(p>0))return!1;const _=p/Math.tan(u/2);return Number.isFinite(_)&&_>0&&_e>0))continue;for(const e of l.offsets)e>0&&(u=Math.max(u,2*e));const c=qs(l.poly,l.offsets,d),p=ns(l.poly,l.offsets,d);c&&h.push({outset:c,inset:p})}for(const e of l){const t=e.map(e=>e[0]),i=e.map(e=>e[1]);if(t.length){const n=Math.min(Math.max(...t)-Math.min(...t),Math.max(...i)-Math.min(...i)),r=Math.min(...e.map((t,i)=>{const n=e[(i+1)%e.length];return Math.hypot(n[0]-t[0],n[1]-t[1])}));u=Math.max(u,e.length>16?n:r)}}const p=function(e,t,i,n,r,o,s,a){if(!t?.length||!i?.length)return[];const l=4*Zo(n,s),c=new Map;for(const a of gs(e,t,i,n,r,o,s))!a.open&&a.half>0&&!c.has(a.key)&&c.set(a.key,a);const h=[...c.values()];if(h.length<2)return[];const d=a||ds(h,l),u=[];for(const e of i)for(const t of[[e[0],e[1]],[e[2],e[3]]])u.some(e=>Math.hypot(e[0]-t[0],e[1]-t[1])<=l)||u.push(t);const p=[],_=(e,t)=>{let i=0,n=0;if(Math.hypot(e.a[0]-t[0],e.a[1]-t[1])<=l)i=e.b[0]-e.a[0],n=e.b[1]-e.a[1];else{if(!(Math.hypot(e.b[0]-t[0],e.b[1]-t[1])<=l))return null;i=e.a[0]-e.b[0],n=e.a[1]-e.b[1]}const r=Math.hypot(i,n);return r>l?[i/r,n/r]:null};for(const e of u){const t=h.map(t=>({iv:t,u:_(t,e)})).filter(e=>!!e.u);for(let i=0;i0?[e.slice(),u,m,_]:[e.slice(),_,m,u];else{if(!f)continue;{const t=[-r.u[1],r.u[0]],i=[-o.u[1],o.u[0]],n=s<0?1:-1,a=[e[0]+t[0]*r.iv.half*n,e[1]+t[1]*r.iv.half*n],l=[e[0]-i[0]*o.iv.half*n,e[1]-i[1]*o.iv.half*n];y=s>0?[e.slice(),a,l]:[e.slice(),l,a]}}Math.abs(Xo(y))>l*l&&p.push(y)}}return p}(e,t,i,r,o,s,a,d),_=n.length?Bs(e,t,i,r,o,s,a):null;let m="exterior";try{const v=zs(e,t,i,r,o,s,a,d);m="paper";const y=v?v.shell?.length?Et(v.centre,v.shell):v.centre:[],b=e=>{const t=xs(e.outset);if(!e.inset)return t;const i=xs(e.inset);return Ht(t,i)};m="room-rings";let w=null;for(const e of h)try{const t=b(e);w=w?Et(w,t):t}catch{}if(m="edge-bodies",v)for(const n of js(e,t,i,r,o,s,a))try{const e=Nt(xs(n.quad),v.centre);w=w?Et(w,e):e}catch{}m="junctions",w=function(e,t,i=1,n=Et){let r=e;for(const e of t||[]){const t=Ds(e,i);if(t)try{const e=xs(t);r=r?n(r,e):e}catch{}}return r}(w,p,a),m="facade-clip",w&&v&&(w=Nt(w,v.centre)),m="exterior-shell";const k=[];let x=0;if(v?.shell?.length)if(w)try{const e=Et(w,v.shell);if(!$s(e))throw new Error("invalid shell union");w=e}catch{if(!$s(w)||!$s(v.shell))throw new Error("invalid shell");k.push({id:"exterior-shell",geom:v.shell}),x++}else w=v.shell;if(m="multi-wall-trim",w&&d.nodes.length){const e=e=>e.rays.some(e=>e.supports.some(e=>e.length<2*e.halfDepth)),t=d.nodes.filter(e);if(t.length){w=Cs(w,{...d,nodes:t},v?.centre,y)}}if(m="junction-corners",d.nodes.length){const n=us(d),l=function(e,t,i,n,r,o,s,a){try{const l=zs(e,t,i,n,r,o,s,{epsilon:a.epsilon,coordinateScale:a.coordinateScale,nodes:[],index:new Map});return l?l.shell?.length?Et(l.centre,l.shell):l.centre:null}catch{return null}}(e,t,i,r,o,s,a,d);for(const e of n.fans)try{let t=[xs(e)];if(l&&(t=Nt(t,l)),!t?.length)continue;w=w?Et(w,t):t}catch{}g=w,f=Math.max(d.epsilon,1e-9)**2,w=Array.isArray(g)?g.map(e=>{if(!Array.isArray(e)||!e.length)return e;const[t,...i]=e;return Math.abs(Xo(t||[]))<=f?null:[t,...i.filter(e=>Math.abs(Xo(e||[]))>f)]}).filter(e=>!!e):g}const $=w||[],S=[...$s($)?[{id:"room-primary",geom:$}]:[],...k.map((e,t)=>({id:`room-isolated-${t}`,geom:e.geom}))];m="openings";for(const e of n){if(!(e.length>0))continue;const t=Gs(_,e,!0);if(!t.negative&&!t.positive)continue;const i=e.angle*Math.PI/180,n=Math.cos(i),o=Math.sin(i),s=-o,l=n,c=e.length/2,h=1.25*Math.max(u,r*a),d=[[e.x-n*c-s*h,e.y-o*c-l*h],[e.x+n*c-s*h,e.y+o*c-l*h],[e.x+n*c+s*h,e.y+o*c+l*h],[e.x-n*c+s*h,e.y-o*c+l*h]];w&&(w=Ht(w,xs(d)));for(const e of k)e.geom=Ht(e.geom,xs(d))}m="extras";const M=[];let T=0;const R=c.mergeExtra||((e,t)=>e?Et(e,t):t);for(let e=0;ee.length>=2&&Number.isFinite(e[0])&&Number.isFinite(e[1]))||Math.abs(yi(t))<=1e-9){T++;continue}const i=[xs(t)];try{const t=R(w,i,e);if(!$s(t))throw new Error("invalid extra union");w=t}catch{T++,$s(i)&&M.push({id:`extra-${e}`,geom:i})}}M.push(...k),M.sort((e,t)=>Fs(e.geom).localeCompare(Fs(t.geom)));const C=w||[];return{status:T||x?"degraded-extra":"ok",geom:C,components:[...$s(C)?[{id:"primary",geom:C}]:[],...M.map((e,t)=>({...e,id:`isolated-${t}`}))],roomGeom:$,roomComponents:S,paperGeom:y,depthUnits:u,openingPadUnits:1.25*Math.max(u,r*a),openingIndex:_,multiWallNodes:d,degradedExtraCount:T+x}}catch{return c.onCoreFailure?.(m),{status:"failed-core",geom:[],components:[],roomGeom:[],paperGeom:[],roomComponents:[],depthUnits:u,openingIndex:null,multiWallNodes:d,degradedExtraCount:0}}var g,f}function Hs(e,t,i,n=[],r,o,s,a=1,l=[],c={}){if(!t?.length&&!l.length)return null;return Ls(Ns(e,t,i,n,r,o,s,a,l,c))}function Ls(e){if("failed-core"===e.status||"not-applicable"===e.status)return null;const t=e.components.map(e=>({id:e.id,d:Fs(e.geom),fillRule:"evenodd"})).filter(e=>!!e.d),i=t[0]?.d||"",n=Fs(e.paperGeom);if(t.length){const r={status:e.status,d:i,paths:t,components:e.components,roomGeom:e.roomGeom,multiWallNodes:e.multiWallNodes,paperD:n,depthUnits:e.depthUnits,fillRule:"evenodd"};return Object.defineProperties(r,{roomComponents:{value:e.roomComponents||[],enumerable:!1},openingIndex:{value:e.openingIndex,enumerable:!1},openingPadUnits:{value:e.openingPadUnits,enumerable:!1}}),r}return null}function js(e,t,i,n,r,o,s=1){if(!t?.length)return[];const a=new Set,l=[];for(const c of e||[]){if(!c?.id)continue;const h=ms(e,c.id,t,i,n,r,o,s);if(!h)continue;const d=h.poly;for(let e=0;e0))continue;const p=$o(t,i,n,s);if(a.has(p))continue;a.add(p);const _=vo(u,r,o),[m,g]=Jo(d,e),f=-m,v=-g,y=_/2,b=[[t[0]+f*y,t[1]+v*y],[i[0]+f*y,i[1]+v*y],[i[0]+m*y,i[1]+g*y],[t[0]+m*y,t[1]+g*y]];l.push({key:p,kind:c,cm:u,quad:b,a:[t[0],t[1]],b:[i[0],i[1]],depthUnits:_})}}return l}function qs(e,t,i){const n=e?.length||0;if(n<3||t.length!==n)return null;if(t.every(e=>!(e>0)))return e.map(e=>[e[0],e[1]]);os(e),t.slice().reverse();const r=[];for(let o=0;o0||l>0)){r.push([e[o][0],e[o][1]]);continue}if(a>0!=l>0){const t=e[o],i=a>0?[t[0]-c*a,t[1]-h*a]:[t[0],t[1]],n=l>0?[t[0]-d*l,t[1]-u*l]:[t[0],t[1]];r.push(i),Math.hypot(n[0]-i[0],n[1]-i[1])>1e-9&&r.push(n);continue}if(Qo(w,k)){const t=e[o],i=[t[0]-c*a,t[1]-h*a],n=[t[0]-d*l,t[1]-u*l];r.push(i),Math.hypot(n[0]-i[0],n[1]-i[1])>1e-9&&r.push(n);continue}const S=es(x,w,$,k),M=Math.max(a,l,1e-9),T=ps(i,e[o])?.limit??4*M;if(S){const i=Math.hypot(S[0]-e[o][0],S[1]-e[o][1]);if(Number.isFinite(i)&&i<=T){r.push(S);continue}if(Es(e,t,o)){r.push([e[o][0],e[o][1]]);continue}}a>0&&r.push([e[o][0]-c*a,e[o][1]-h*a]),l>0&&r.push([e[o][0]-d*l,e[o][1]-u*l])}return r.length>=3?r:null}function Bs(e,t,i,n,r,o,s=1){const a=[];for(const l of e||[]){if(!l?.id)continue;const c=ms(e,l.id,t,i,n,r,o,s);if(!c)continue;const h=Math.abs(yi(c.poly));for(let e=0;e[Math.max(t,e.x0),Math.min(i,e.x1)]).filter(e=>e[1]-e[0]>n).sort((e,t)=>e[0]-t[0]||e[1]-t[1]);if(!r.length)return{coverage:0,full:!1};let o=r[0][0],s=r[0][1],a=0,l=o<=t+n;for(let e=1;e=i-n,{coverage:a,full:l}}function Us(e,t){return Number(t.full)-Number(e.full)||e.faceDistance-t.faceDistance||e.area-t.area||e.roomId.localeCompare(t.roomId)}function Gs(e,t,i=!1){const n=Number(t?.x),r=Number(t?.y),o=Number(t?.angle),s=Number(t?.length);if(!([n,r,o,s].every(Number.isFinite)&&s>0))return{negative:null,positive:null};const a=o*Math.PI/180,l=Math.cos(a),c=Math.sin(a),h=-c,d=l,u=s/2,p=Math.max(1e-9,e.adjacencyEps),_=new Map;let m=0;for(const t of e.edges){if(i&&!(t.half>0))continue;if(!zo(t.a,t.b,o))continue;const[e,s]=ko(t.a,t.b);if(Math.abs((n-t.a[0])*s-(r-t.a[1])*e)>p)continue;const a=(t.a[0]-n)*l+(t.a[1]-r)*c,g=(t.b[0]-n)*l+(t.b[1]-r)*c,f=Math.max(-u,Math.min(a,g)),v=Math.min(u,Math.max(a,g));if(v-f<=p)continue;const y=t.inward[0]*h+t.inward[1]*d>=0?1:-1,b=((t.a[0]+t.b[0])/2-n)*h+((t.a[1]+t.b[1])/2-r)*d,w=Math.abs(b+y*t.half),k=`${y}|${t.roomId}`,x={x0:f,x1:v,half:t.half,cm:t.cm,key:t.key,axis:[e,s]},$=_.get(k);$?($.pieces.push(x),$.faceDistance=Math.min($.faceDistance,w)):_.set(k,{roomId:t.roomId,side:y,order:m++,pieces:[x],faceDistance:w,area:t.area,coverage:0,full:!1})}for(const e of _.values()){const t=Ws(e.pieces,-u,u,p);e.coverage=t.coverage,e.full=t.full}const g=e=>{const t=[..._.values()].filter(t=>t.side===e&&t.coverage>p);return t.sort(Us),t[0]||null};return{negative:g(-1),positive:g(1)}}function Vs(e,t){const i=Gs(e,t),n=[i.negative,i.positive].filter(e=>!!e);if(!n.length)return{ox:0,oy:0,cm:0,side:-1};const r=i.negative&&i.positive?-1:n[0].side,o=t.flip_v?-r:r,s=(-1===o?i.negative:i.positive)||n[0],a=[...s.pieces].sort((e,t)=>(e.x0<=0&&e.x1>=0?0:Math.min(Math.abs(e.x0),Math.abs(e.x1)))-(t.x0<=0&&t.x1>=0?0:Math.min(Math.abs(t.x0),Math.abs(t.x1)))||t.x1-t.x0-(e.x1-e.x0)||e.key.localeCompare(t.key))[0];if(!(a.half>0&&a.cm>0))return{ox:0,oy:0,cm:0,side:o};const l=t.angle*Math.PI/180,c=-Math.sin(l),h=Math.cos(l);return{ox:c*o*a.half,oy:h*o*a.half,cm:a.cm,side:o}}function Ks(e,t){const i=1e-9,n=t.filter(e=>Number.isFinite(e.x0)&&Number.isFinite(e.x1)&&Number.isFinite(e.half)&&e.x1>e.x0&&e.half>0);if(!n.length)return"";const r=n.flatMap(e=>[e.x0,e.x1]).sort((e,t)=>e-t),o=[];for(const e of r){const t=o[o.length-1];(void 0===t||e>t+i)&&o.push(e)}const s=[];for(let e=0;e+1t+i))continue;const a=(t+r)/2,l=n.reduce((e,t)=>a>=t.x0-i&&a<=t.x1+i?Math.max(e,t.half):e,0);if(!(l>0))continue;const c=s[s.length-1];c&&t<=c.x1+i&&Math.abs(l-c.half)<=i?c.x1=r:s.push({x0:t,x1:r,half:l})}const a=[];for(const e of s){const t=a[a.length-1],n=t?.[t.length-1];n&&e.x0<=n.x1+i?(e.x0=n.x1,t.push(e)):a.push([e])}return a.map(t=>{const i=t[0],n=t[t.length-1],r=Math.min(.25*Math.min(...t.map(e=>e.half)),.75),o=-e*r,s=[];if(1===e){s.push(`M ${i.x0} ${o} L ${n.x1} ${o}`);for(let e=t.length-1;e>=0;e--){const i=t[e];s.push(`L ${i.x1} ${i.half} L ${i.x0} ${i.half}`)}}else{s.push(`M ${n.x1} ${o} L ${i.x0} ${o}`);for(const e of t)s.push(`L ${e.x0} ${-e.half} L ${e.x1} ${-e.half}`)}return s.push("Z"),s.join(" ")}).join(" ")}function Ys(e,t,i,n){if(!n)return i;const r=e.angle*Math.PI/180,o=Math.cos(r),s=Math.sin(r),a=[],l=1e-9;for(const r of i){const[i,c]=r.axis,h=o*i+s*c;if(Math.abs(h)<=l)continue;const d=e.x*i+e.y*c,u=d+h*r.x0,p=d+h*r.x1,_=Math.min(u,p),m=Math.max(u,p),g=`${r.key}|${t}`,f=n.get(g)||[];let v=[[_,m]];for(const[e,t]of f){const i=[];for(const[n,r]of v)t<=n+l||e>=r-l?i.push([n,r]):(e>n+l&&i.push([n,Math.min(r,e)]),te[0]-t[0]||e[1]-t[1]),b=[];for(const e of y){const t=b[b.length-1];t&&e[0]<=t[1]+l?t[1]=Math.max(t[1],e[1]):b.push([e[0],e[1]])}n.set(g,b)}return a}function Zs(e,t){const i=new Map;return t.map(t=>function(e,t,i){const n=Gs(e,t,!0),r=n.negative,o=n.positive;if(!r&&!o)return null;let s;if(r&&o)s=[{candidate:r,side:-1},{candidate:o,side:1}];else{const e=r||o;s=[{candidate:e,side:-1},{candidate:e,side:1}]}const a=s.map(({candidate:e,side:n})=>({candidate:e,side:n,pieces:Ys(t,n,e.pieces,i)})),l=a.map(({candidate:e,side:t,pieces:i})=>({side:t,roomId:e.roomId,d:Ks(t,i)})),c=a.flatMap(({pieces:e})=>e);if(!c.length)return null;const h=Math.max(...c.map(e=>e.half));return{faces:l,minY:-h,maxY:h,wallKey:[...new Set(c.map(e=>e.key))].sort().join("|")}}(e,t,i))}const Xs=150,Js=1e-6;function Qs(e){return Number.isFinite(e)?Math.max(1,Math.min(Xs,e)):1}function ea(e){return((Number.isFinite(Number(e))?Number(e):0)%90+90)%90}const ta=e=>{const t=e.map(e=>[e[0],e[1]]);return!t.length||t[0][0]===t[t.length-1][0]&&t[0][1]===t[t.length-1][1]||t.push([...t[0]]),[t]},ia=(e,t)=>e[0]===t[0]&&e[1]===t[1];function na(e,t=1e-6){const i=Number.isFinite(t)&&t>0?t:Js,n=[];for(const t of e||[]){if(!Array.isArray(t)||t.length<2)return null;const e=Number(t[0]),r=Number(t[1]);if(!Number.isFinite(e)||!Number.isFinite(r))return null;const o=Math.round(e/i)*i,s=Math.round(r/i)*i;if(!Number.isFinite(o)||!Number.isFinite(s))return null;const a=[Object.is(o,-0)?0:o,Object.is(s,-0)?0:s];n.length&&ia(n[n.length-1],a)||n.push(a)}return n.length>1&&ia(n[0],n[n.length-1])&&n.pop(),n.length<3||new Set(n.map(e=>`${e[0]},${e[1]}`)).size<3?null:yi(n)>i*i?n:null}function ra(e){const t=[];for(const i of e||[])for(const e of i||[]){const i=(e||[]).filter(e=>Array.isArray(e)&&e.length>=2);i.length<4||t.push(`M ${i.slice(0,-1).map(e=>`${e[0]} ${e[1]}`).join(" L ")} Z`)}return t.join(" ")}function oa(e,t,i,n,r){return jo({a:e,b:t,halfDepth:vo(i,n,r)/2})}function sa(e,t,i){const n=Number(t)>0?Number(t):5,r=Qs(e.cm)/n*i,o=e.center[0],s=e.center[1];if("circle"===e.shape){const e=r/2;return Array.from({length:96},(t,i)=>{const n=i/96*Math.PI*2;return[o+Math.cos(n)*e,s+Math.sin(n)*e]})}const a=r/2,l=ea(e.angle)*Math.PI/180,c=Math.cos(l),h=Math.sin(l);return[[-a,-a],[a,-a],[a,a],[-a,a]].map(([e,t])=>[o+e*c-t*h,s+e*h+t*c])}function aa(e,t,i=1e-9){if(!t.length)return[e];let n=[ta(e)];try{for(const e of t){const t=e.b[0]-e.a[0],r=e.b[1]-e.a[1],o=Math.hypot(t,r);if(!(o>i))continue;const s=t/o,a=r/o,l=-a,c=s,h=1.25*Math.max(Number(e.depth)||0,4*i),d=Math.max(2*i,1e-9*o),u=[[e.a[0]-s*d-l*h,e.a[1]-a*d-c*h],[e.b[0]+s*d-l*h,e.b[1]+a*d-c*h],[e.b[0]+s*d+l*h,e.b[1]+a*d+c*h],[e.a[0]-s*d+l*h,e.a[1]-a*d+c*h]];n=Ht(n,ta(u))}return fa(n)}catch{return[e]}}function la(e,t){try{const i=Ht([[...e.map(e=>[e[0],e[1]]),[e[0][0],e[0][1]]]],[[...t.map(e=>[e[0],e[1]]),[t[0][0],t[0][1]]]]);let n=null,r=0;for(const e of i||[]){const t=e?.[0]||[],i=Math.abs(yi(t));t.length>=4&&i>r&&(r=i,n=t.slice(0,-1).map(e=>[e[0],e[1]]))}return n}catch{return null}}function ca(e,t,i,n=Math.max(2e-4*i,1e-9),r=[]){const o=[],s=[],a=new Map;for(const e of r){const t=a.get(e.hostId)||[];t.push(e),a.set(e.hostId,t)}const l=[],c=[],h=[];for(const n of e.room_drafts||[])for(let e=0;e+10))continue;const a={a:n.points[e],b:n.points[e+1],halfDepth:s},c=jo(a);c&&(o.push(a),l.push(c))}const d=[];for(const n of e.partitions||[]){const e={a:n.a,b:n.b,halfDepth:vo(n.cm,t,i)/2};if(!(e.halfDepth>0))continue;const r=jo(e);r&&(s.push(e),c.push(r),d.push({id:n.id,body:r}))}const u=[...o,...s];for(const{segmentIndex:e,wedge:t}of Go(u,n)){const i=e=o.length&&(d[i.at].body=n))}for(const e of d)h.push(...aa(e.body,a.get(e.id)||[],n));const p=(e.wall_columns||[]).map(e=>sa(e,t,i)),_=Vo([...o,...s],n).flatMap(e=>aa(e,r,n));return{drafts:l,partitions:c,columns:p,patches:_,all:[...l,...h,..._,...p]}}function ha(e){try{const t=e.map(e=>na(e)).filter(e=>!!e).map(e=>ta(e));return t.length?Et(t[0],...t.slice(1)):null}catch{return null}}const da=e=>`M ${e.map(e=>`${e[0]} ${e[1]}`).join(" L ")} Z`;function ua(e){const t=[];for(const i of e||[]){if(ma([i])<=1e-6)continue;const e=[];for(const t of i||[]){const i=(t||[]).filter(e=>Array.isArray(e)&&e.length>=2);i.length<4||e.push(da(i.slice(0,-1)))}e.length&&t.push(e.join(" "))}return t}function pa(e,t,i={}){const n=ha(e.filter(e=>e.length>=3)),r=t.some(e=>!na(e))?null:ha(t);if(!n)return[];if(r)try{return ua(Nt(n,r))}catch{}return function(e,t,i){let n=null;for(let r=0;r=4&&t.push(e.slice(0,-1).map(e=>[e[0],e[1]]));return t}function fa(e){const t=[];for(const i of e||[]){const e=i?.[0];e?.length>=4&&t.push(e.slice(0,-1).map(e=>[e[0],e[1]]))}return t}function va(e,t){let i=!1;for(let n=0,r=t.length-1;ne[1]!=l>e[1]&&e[0]<(a-o)*(e[1]-s)/(l-s||1e-12)+o&&(i=!i)}return i}function ya(e,t,i){return function(e,t){for(const i of t||[]){const t=i?.[0];if(!t?.length||!va(e,t))continue;let n=!1;for(let t=1;tva(e,t))}function ba(e,t,i){if(Math.hypot(e.center[0]-t.center[0],e.center[1]-t.center[1])>i)return!1;if(Math.abs(Qs(e.cm)-Qs(t.cm))>1e-6)return!1;if(e.shape!==t.shape)return!0;if("circle"===e.shape||"circle"===t.shape)return!0;const n=Math.abs(ea(e.angle)-ea(t.angle));return Math.min(n,90-n)<=1e-6}const wa=2.54;function ka(e){const t="number"==typeof e?e:NaN;return!Number.isFinite(t)||t<=0||5===t?1:5/t}function xa(e,t){return e*ka(t)}function $a(e){return e?wa:1}function Sa(e,t){if(!t)return String(e);const i=e/wa;return String(Math.round(1e6*i)/1e6)}function Ma(e,t){return t?e*wa:e}const Ta=1e3;function Ra(e,t){const i=Number(e),n=Number.isFinite(i)&&i>0?i:1,r=n>=1?t:t*n,o=n>=1?t/n:t;return{x:(t-r)/2,y:(t-o)/2,w:r,h:o}}const Ca=.01,Da=100;function Aa(e,t=1e3){const i=Ra(e?.plan_aspect,t),n=Number(e?.plan_scale),r=Number.isFinite(n)&&n>0?Math.min(Da,Math.max(Ca,n)):1,o=Number(e?.plan_scale_x),s=Number(e?.plan_scale_y),a=Number.isFinite(o)&&o>0?Math.min(Da,Math.max(Ca,o)):r,l=Number.isFinite(s)&&s>0?Math.min(Da,Math.max(Ca,s)):r,c=Number(e?.plan_x),h=Number(e?.plan_y),d=_r(e?.plan_angle);return{x:i.x+(Number.isFinite(c)?qa(c):0)*t,y:i.y+(Number.isFinite(h)?qa(h):0)*t,w:i.w*a,h:i.h*l,...d?{angle:d}:{}}}function Oa(e){if(null==e.x||null==e.y)return{x:e.x,y:e.y,w:e.w,h:e.h};const t=Number(e.w)||0,i=Number(e.h)||0;return{x:t<0?e.x+t:e.x,y:i<0?e.y+i:e.y,w:Math.abs(t),h:Math.abs(i)}}function za(e){return e&&Array.isArray(e.spaces)?e.spaces.map(e=>{const t=Ta,i=function(e){return Array.isArray(e)&&4===e.length&&e.every(e=>Number.isFinite(e))&&e[2]>1e-6&&e[3]>1e-6?e:[0,0,1,1]}(e.view_box);return{id:e.id,title:e.title,cellCm:Number.isFinite(Number(e.cell_cm))&&Number(e.cell_cm)>0?Number(e.cell_cm):5,vb:[i[0]*Ta,i[1]*t,i[2]*Ta,i[3]*t],bg:e.plan_url?{href:Mn(e.plan_url),...Aa(e,Ta)}:null,rooms:(e.rooms||[]).map(e=>{const i={...e,...Oa(e)};return{id:i.id,name:i.name,area:i.area??null,open_to:i.open_to||void 0,settings:i.settings||void 0,x:null!=i.x?i.x*Ta:void 0,y:null!=i.y?i.y*t:void 0,w:null!=i.w?i.w*Ta:void 0,h:null!=i.h?i.h*t:void 0,poly:i.poly?i.poly.map(e=>[e[0]*Ta,e[1]*t]):void 0,wall_ids:Array.isArray(i.wall_ids)?[...i.wall_ids]:void 0}}),wall_segments:(e.wall_segments||[]).map(e=>({...e,id:String(e.id),a:[Number(e.a?.[0])*Ta,Number(e.a?.[1])*t],b:[Number(e.b?.[0])*Ta,Number(e.b?.[1])*t],cm:Number(e.cm)})),room_drafts:(e.room_drafts||[]).map(e=>({id:e.id,points:(e.points||[]).map(e=>[e[0]*Ta,e[1]*t]),segments:(e.segments||[]).map(e=>({..."string"==typeof e.id&&e.id?{id:e.id}:{},cm:Number(e.cm)}))})),partitions:(e.partitions||[]).map(e=>({id:e.id,a:[e.a[0]*Ta,e.a[1]*t],b:[e.b[0]*Ta,e.b[1]*t],cm:Number(e.cm)})),wall_columns:(e.wall_columns||[]).map(e=>({id:e.id,shape:"circle"===e.shape?"circle":"square",center:[e.center[0]*Ta,e.center[1]*t],cm:Number(e.cm),..."circle"===e.shape?{}:{angle:ea(e.angle)}}))}}):[]}const Pa=5e3,Fa=Pa*Ta,Ia=240,Ea=Ta/Ia,Na=1/Ia;function Ha(e){if(!Number.isFinite(e))return e;const t=Math.round(e*Ia/Ta)*Ta/Ia;return Math.abs(t-e)<=1e-9*Ea?e:t}function La(e){return{x:Ha(e.x),y:Ha(e.y)}}function ja(e){return Number.isFinite(e)?Math.min(Fa,Math.max(-Fa,e)):0}function qa(e){return Number.isFinite(e)?Math.min(Pa,Math.max(-Pa,e)):0}const Ba=1/3,Wa=200;function Ua(e){let t=1/0,i=1/0,n=-1/0,r=-1/0;for(const o of e){const e=Number(o[0]),s=Number(o[1]);Number.isFinite(e)&&Number.isFinite(s)&&(en&&(n=e),s>r&&(r=s))}return t>n?null:{minX:t,minY:i,maxX:n,maxY:r}}function Ga(e){return e.poly&&e.poly.length?Ua(e.poly):null==e.x||null==e.y?null:Ua([[e.x,e.y],[e.x+(e.w||0),e.y+(e.h||0)]])}function Va(e,t){const i=[];for(const t of e.rooms||[]){const e=Ga(t);e&&i.push(e)}if(e.bg){const t=Ua(wr(e.bg));t&&i.push(t)}for(const e of t||[])if(Array.isArray(e)){const t=Ua([e]);t&&i.push(t)}else i.push(e);return i}const Ka=e=>{if(!e.length)return 0;const t=[...e].sort((e,t)=>e-t),i=t.length>>1;return t.length%2?t[i]:(t[i-1]+t[i])/2},Ya=e=>{let t=1/0,i=1/0,n=-1/0,r=-1/0;for(const o of e)o.minXn&&(n=o.maxX),o.maxY>r&&(r=o.maxY);return t>n||i>r?null:{x:t,y:i,w:n-t,h:r-i}};function Za(e,t){let{x:i,y:n,w:r,h:o}=e;r<30&&(i=i+r/2-100,r=Wa),o<30&&(n=n+o/2-100,o=Wa);const s=Math.max(r,o)*t;return{x:i-s,y:n-s,w:r+2*s,h:o+2*s}}function Xa(e,t={}){const i=t.pad??.05,n=t.k??10,r=t.minSpread??50,o=e.filter(e=>Number.isFinite(e.minX)&&Number.isFinite(e.minY)&&Number.isFinite(e.maxX)&&Number.isFinite(e.maxY)&&Math.abs(e.minX)<=Fa&&Math.abs(e.maxX)<=Fa&&Math.abs(e.minY)<=Fa&&Math.abs(e.maxY)<=Fa);if(!o.length)return{core:null,all:null,outliers:0};const s=Ya(o);if(o.length<4){const e=Za(s,i);return{core:e,all:e,outliers:0}}const a=o.map(e=>(e.minX+e.maxX)/2),l=o.map(e=>(e.minY+e.maxY)/2),c=Ka(a),h=Ka(l),d=o.map((e,t)=>Math.max(Math.abs(a[t]-c),Math.abs(l[t]-h))),u=Math.max(((e,t)=>e.length?e[Math.min(e.length-1,Math.max(0,Math.round(t*(e.length-1))))]:0)([...d].sort((e,t)=>e-t),.75),r),p=d.map(e=>e>n*u),_=p.filter(Boolean).length,m=_&&_<=o.length*Ba?o.filter((e,t)=>!p[t]):o;return{core:Za(Ya(m)||s,i),all:Za(s,i),outliers:m===o?0:_}}function Ja(e,t,i=.05){const n=Xa(Va(e,t),{pad:i});if(n.core)return n.core;const r=e.vb&&4===e.vb.length&&e.vb[2]>0&&e.vb[3]>0?e.vb:[0,0,Ta,Ta];return{x:r[0],y:r[1],w:r[2],h:r[3]}}function Qa(e){const t=Ja(e);return{x:t.x+t.w/2,y:t.y+t.h/2}}function el(e){const t=[];for(const i of e.rooms||[]){const e=Ga(i);e&&t.push(e)}const i=Xa(t,{pad:0}).core,n=Ta*ka(e.cellCm??5);return i?Math.max(n,Math.min(Fa,Math.max(i.w,i.h))):n}function tl(e,t,i,n=1){const r=Number(i),o=Number.isFinite(n)&&n>0?n:1;return!Number.isFinite(r)||r<=0?e*o:e*el(t)*o/r}const il=[1,2,5,10,20,50,100,200,500,1e3];function nl(e,t,i=7){if(!(e>0&&t>0&&Number.isFinite(t)))return null;const n=il.find(n=>e*n*t>=i);if(void 0===n)return null;const r=il.find(e=>e>=5*n)??5*n;return{fine:n,coarse:r}}function rl(e){if(e.poly&&e.poly.length){const t=e.poly.map(e=>e[0]),i=e.poly.map(e=>e[1]),n=Math.min(...t),r=Math.min(...i);return{x:n,y:r,w:Math.max(...t)-n,h:Math.max(...i)-r}}return{x:e.x??0,y:e.y??0,w:e.w??0,h:e.h??0}}function ol(e,t,i,n){const r=i["rl_"+(e.id||"")];if(r&&r.s===t)return{x:r.x*Ta,y:r.y*Ta};const o=function(e){if(e.poly){const t=e.poly.length;return[e.poly.reduce((e,t)=>e+t[0],0)/t,e.poly.reduce((e,t)=>e+t[1],0)/t]}return[e.x+e.w/2,e.y+.1*Math.min(e.w,e.h)]}(e);return La({x:o[0],y:o[1]})}const sl=["furniture","appliance","sanitary","other"],al=[{id:"sofa",group:"furniture",w:220,h:90,g:[["r",0,0,1,1],["l",.09,.26,.91,.26],["l",.09,.26,.09,1],["l",.91,.26,.91,1],["l",.5,.26,.5,1]]},{id:"armchair",group:"furniture",w:90,h:85,g:[["r",0,0,1,1],["l",.14,.28,.86,.28],["l",.14,.28,.14,1],["l",.86,.28,.86,1]]},{id:"coffee_table",group:"furniture",w:110,h:60,g:[["r",0,0,1,1],["r",.08,.14,.84,.72]]},{id:"table_dining",group:"furniture",w:140,h:80,g:[["r",0,0,1,1],["r",.06,.11,.88,.78]]},{id:"table_round",group:"furniture",w:120,h:120,g:[["e",.5,.5,.5,.5],["e",.5,.5,.41,.41]]},{id:"chair",group:"furniture",w:45,h:45,g:[["r",0,0,1,.18],["r",.06,.18,.88,.8]]},{id:"desk",group:"furniture",w:120,h:60,g:[["r",0,0,1,1],["r",.63,.07,.31,.86],["l",.63,.5,.94,.5]]},{id:"bed_double",group:"furniture",w:160,h:200,g:[["r",0,0,1,1],["r",0,0,1,.07],["r",.06,.1,.4,.15],["r",.54,.1,.4,.15],["l",0,.33,1,.33]]},{id:"bed_single",group:"furniture",w:90,h:200,g:[["r",0,0,1,1],["r",0,0,1,.07],["r",.15,.1,.7,.15],["l",0,.33,1,.33]]},{id:"nightstand",group:"furniture",w:45,h:40,g:[["r",0,0,1,1],["r",.12,.14,.76,.33],["r",.12,.53,.76,.33]]},{id:"wardrobe",group:"furniture",w:100,h:60,g:[["r",0,0,1,1],["l",0,.72,1,.72],["l",.5,.72,.5,1]]},{id:"bookshelf",group:"furniture",w:80,h:30,g:[["r",0,0,1,1],["l",.34,0,.34,1],["l",.67,0,.67,1]]},{id:"fridge",group:"appliance",w:60,h:65,g:[["r",0,0,1,1],["l",0,.36,1,.36],["l",.83,.44,.83,.64]]},{id:"stove",group:"appliance",w:60,h:60,g:[["r",0,0,1,1],["e",.29,.31,.15,.15],["e",.71,.31,.15,.15],["e",.29,.71,.15,.15],["e",.71,.71,.15,.15]]},{id:"dishwasher",group:"appliance",w:60,h:60,g:[["r",0,0,1,1],["r",.1,.12,.8,.76],["e",.5,.5,.27,.27],["e",.5,.5,.13,.13]]},{id:"washer",group:"appliance",w:60,h:60,g:[["r",0,0,1,1],["l",.08,.17,.92,.17],["e",.5,.57,.3,.3],["e",.5,.57,.14,.14]]},{id:"dryer",group:"appliance",w:60,h:60,g:[["r",0,0,1,1],["l",.08,.17,.92,.17],["e",.5,.57,.3,.3],["p",.36,.5,.5,.64,.64,.5]]},{id:"tv",group:"appliance",w:120,h:30,g:[["r",0,0,1,.42],["l",.5,.42,.5,.72],["l",.3,.72,.7,.72]]},{id:"ac",group:"appliance",w:90,h:25,g:[["r",0,0,1,1],["l",.05,.55,.95,.55],["l",.05,.79,.95,.79]]},{id:"water_heater",group:"appliance",w:45,h:45,g:[["e",.5,.5,.5,.5],["e",.5,.5,.31,.31]]},{id:"toilet",group:"sanitary",w:40,h:70,g:[["r",.06,0,.88,.2],["e",.5,.58,.37,.35],["e",.5,.58,.22,.2]]},{id:"bathtub",group:"sanitary",w:170,h:75,g:[["r",0,0,1,1],["r",.05,.11,.77,.78],["e",.89,.5,.045,.1]]},{id:"shower",group:"sanitary",w:90,h:90,g:[["r",0,0,1,1],["l",0,0,1,1],["l",1,0,0,1],["e",.5,.5,.08,.08]]},{id:"sink",group:"sanitary",w:60,h:45,g:[["r",0,0,1,1],["e",.5,.6,.34,.3],["e",.5,.15,.07,.07]]},{id:"kitchen_sink",group:"sanitary",w:80,h:60,g:[["r",0,0,1,1],["r",.06,.24,.44,.64],["r",.54,.24,.4,.64],["e",.5,.12,.06,.06]]},{id:"bidet",group:"sanitary",w:40,h:55,g:[["e",.5,.5,.44,.5],["e",.5,.5,.26,.3]]},{id:"stairs",group:"other",w:100,h:280,g:[["r",0,0,1,1],["l",0,.111,1,.111],["l",0,.222,1,.222],["l",0,.333,1,.333],["l",0,.444,1,.444],["l",0,.556,1,.556],["l",0,.667,1,.667],["l",0,.778,1,.778],["l",0,.889,1,.889],["l",.5,.93,.5,.06],["p",.38,.16,.5,.06,.62,.16]]},{id:"fireplace",group:"other",w:120,h:40,g:[["r",0,0,1,1],["p",.22,1,.22,.42,.78,.42,.78,1]]},{id:"plant",group:"other",w:40,h:40,g:[["e",.5,.5,.22,.22],["l",.5,.28,.5,.02],["l",.5,.72,.5,.98],["l",.28,.5,.02,.5],["l",.72,.5,.98,.5],["l",.34,.34,.13,.13],["l",.66,.66,.87,.87],["l",.66,.34,.87,.13],["l",.34,.66,.13,.87]]},{id:"rug",group:"other",w:200,h:140,g:[["r",0,0,1,1],["r",.06,.09,.88,.82]]}],ll=new Map(al.map(e=>[e.id,e]));function cl(e){return e&&ll.get(e)||null}function hl(e){return al.filter(t=>t.group===e)}function dl(e){const t=cl(e);return t?{w:t.w,h:t.h}:{w:60,h:60}}function ul(e,t,i=Ea,n=1e3){const r=Number(t)>0?Number(t):5;return(Number(e)||0)/r*i/n}const pl=5e-4,_l=Pa;function ml(e){return Number.isFinite(e)?Math.max(pl,Math.min(_l,e)):pl}function gl(e){return Number.isFinite(e)?Math.max(1,Math.min(1e4,e)):1}const fl=e=>{const t=Math.round(1e3*e)/1e3;return Object.is(t,-0)?"0":String(t)};function vl(e,t,i){const n=cl(e);if(!(n&&t>0&&i>0))return"";const r=e=>fl(e*t),o=e=>fl(e*i),s=[];for(const e of n.g)if("r"===e[0]){const[,t,i,n,a]=e;s.push(`M${r(t)} ${o(i)}H${r(t+n)}V${o(i+a)}H${r(t)}Z`)}else if("l"===e[0]){const[,t,i,n,a]=e;s.push(`M${r(t)} ${o(i)}L${r(n)} ${o(a)}`)}else if("e"===e[0]){const[,n,a,l,c]=e;s.push(`M${r(n-l)} ${o(a)}A${fl(l*t)} ${fl(c*i)} 0 0 1 ${r(n+l)} ${o(a)}A${fl(l*t)} ${fl(c*i)} 0 0 1 ${r(n-l)} ${o(a)}Z`)}else{const t=e.slice(1);if(t.length<4)continue;let i=`M${r(t[0])} ${o(t[1])}`;for(let e=2;e+1{let t=(e%360+360)%360;return t>180&&(t-=360),t};function bl(e,t,i,n,r,o=0){let s=null,a=r;for(const r of n){const[n,l,c,h]=r,d=c-n,u=h-l,p=d*d+u*u;if(!p)continue;const _=Math.sqrt(p);let m=((e-n)*d+(t-l)*u)/p;m=Math.max(0,Math.min(1,m));let g=n+m*d,f=l+m*u;const v=Math.hypot(e-g,t-f);if(!(v0){const e=Math.round(m*_/o)*o;g=n+e/_*d,f=l+e/_*u}s={cx:g+y*(i/2),cy:f+b*(i/2),angle:yl(180*Math.atan2(-y,b)/Math.PI),dist:v}}return s}const wl=15e3,kl=2e3,xl=new WeakMap;function $l(e,t){let i=xl.get(e);if(!i){const t=new Set;i={hiddenAt:"hidden"===e.visibilityState?Date.now():0,token:0,subscribers:t,onVisibility:()=>{const t=xl.get(e);if(!t)return;const i=Date.now();if("hidden"===e.visibilityState){t.hiddenAt||(t.hiddenAt=i);const e={kind:"hidden",token:t.token,at:i,hiddenFor:0,long:!1};for(const i of[...t.subscribers])i(e);return}const n=t.hiddenAt?Math.max(0,i-t.hiddenAt):0;t.hiddenAt=0,t.token++;const r={kind:"visible",token:t.token,at:i,hiddenFor:n,long:n>=wl};for(const e of[...t.subscribers])e(r)},onPageShow:t=>{const i=xl.get(e);if(!i)return;const n=Date.now();i.token++;const r={kind:"pageshow",token:i.token,at:n,hiddenFor:0,long:!!t.persisted,persisted:!!t.persisted};for(const e of[...i.subscribers])e(r)}},xl.set(e,i),e.addEventListener("visibilitychange",i.onVisibility),e.defaultView?.addEventListener("pageshow",i.onPageShow)}return i.subscribers.add(t),()=>{const i=xl.get(e);i&&(i.subscribers.delete(t),i.subscribers.size||(e.removeEventListener("visibilitychange",i.onVisibility),e.defaultView?.removeEventListener("pageshow",i.onPageShow),xl.delete(e)))}}const Sl={now:()=>Date.now(),setTimeout:(e,t)=>window.setTimeout(e,t),clearTimeout:e=>window.clearTimeout(e),requestAnimationFrame:e=>window.requestAnimationFrame(e),cancelAnimationFrame:e=>window.cancelAnimationFrame(e)};class Ml{constructor(e,t=Sl){this.onChange=e,this.clock=t,this._state="steady",this._token=0,this._frameFingerprint="",this._hasCompleteFrame=!1,this._overlayPhase="none",this._recoveryReason=null,this._overlayTimer=0,this._overlayRaf=0,this._overlayOpaqueAt=0,this._barrierRafs=new Set,this._trace=[],this._disposed=!1}get state(){return this._state}get token(){return this._token}get frameFingerprint(){return this._frameFingerprint}get hasCompleteFrame(){return this._hasCompleteFrame}get overlayPhase(){return this._overlayPhase}get overlayVisible(){return"none"!==this._overlayPhase}get overlayBlocksInteraction(){return this.overlayVisible}get recoveryReason(){return this._recoveryReason}get trace(){return this._trace.map(e=>({...e,...e.stage?{stage:[...e.stage]}:{}}))}note(e,t={}){this.record(e,void 0,t)}record(e,t,i={}){this._trace.push({at:this.clock.now(),token:this._token,event:e,state:this._state,...t?{reason:t}:{},...i}),this._trace.length>80&&this._trace.splice(0,this._trace.length-80)}changed(){this._disposed||this.onChange()}adoptCompleteFrame(e){e&&(this._hasCompleteFrame=!0,this._frameFingerprint=e,this._state="steady",this._recoveryReason=null,this.clearOverlay(),this.record("frame-adopted"),this.changed())}markCompleteFrame(e){e&&(this._hasCompleteFrame=!0,this._frameFingerprint=e,this._state="steady",this._recoveryReason=null,this.clearOverlay(),this.record("frame-complete"),this.changed())}refreshCompleteFrame(e){!this._disposed&&this._hasCompleteFrame&&"steady"===this._state&&e&&e!==this._frameFingerprint&&(this._frameFingerprint=e,this.record("frame-refreshed"))}visibility(e){return"hidden"===e.kind?(this.record("visibility-hidden"),this._token):e.long||"visible"!==e.kind&&"pageshow"!==e.kind?this.beginCandidate("pageshow"===e.kind?"pageshow":"long-resume"):(this.record("pageshow"===e.kind?"pageshow-noop":"visibility-visible-quick"),this._token)}beginCandidate(e,t="plan"){return this._token++,this._recoveryReason=t,this._hasCompleteFrame?(this._state="connection"===t?"offline-stale":"holding",this.clearOverlay()):this.overlayVisible?this._state="overlay-visible":(this._state="overlay-pending",this.scheduleOverlay(t)),this.record("candidate-start",e),this.changed(),this._token}connectionLost(){return this.beginCandidate("connection-lost","connection")}candidateReady(e){return!this._disposed&&e===this._token&&(this._state="candidate-ready",this.record("candidate-ready"),this.changed(),!0)}async commitAfterPaint(e,t){if(this._disposed||e!==this._token)return!1;let i=!0;const n=(async()=>{for(await t.updateComplete();i&&!this._disposed&&e===this._token;)if(t.stageValid()&&t.assetsReady()){if(await this.nextFrame(),e!==this._token||!t.stageValid()||!t.assetsReady())continue;if(await this.nextFrame(),e===this._token&&t.stageValid()&&t.assetsReady())return!0}else await this.nextFrame();return!1})();let r=0;const o=new Promise(e=>{r=this.clock.setTimeout(()=>e({ready:!1,timedOut:!0}),kl)}),s=await Promise.race([n.then(e=>({ready:e,timedOut:!1})),o]);return i=!1,this.clock.clearTimeout(r),!this._disposed&&e===this._token&&(!(!s.ready&&!s.timedOut)&&(s.ready?(this._hasCompleteFrame=!0,this._frameFingerprint=t.frameFingerprint(),this.record("paint-barrier"),this.finishOverlayAfterCommit(),!0):(this.record("paint-barrier-timeout"),this._hasCompleteFrame?(this._state="connection"===this._recoveryReason?"offline-stale":"steady",this.clearOverlay()):(this._state="recovery-error","opaque"!==this._overlayPhase&&this.showOverlayNow()),this.changed(),!1)))}retry(e=this._recoveryReason||"plan"){return this.beginCandidate("retry",e)}nextFrame(){return new Promise(e=>{const t=this.clock.requestAnimationFrame(()=>{this._barrierRafs.delete(t),e()});this._barrierRafs.add(t)})}scheduleOverlay(e){this.clearOverlayTimer(),this._recoveryReason=e,this._overlayTimer=this.clock.setTimeout(()=>{this._overlayTimer=0,this._hasCompleteFrame||"overlay-pending"!==this._state&&"candidate-ready"!==this._state||(this._state="overlay-visible",this._overlayPhase="entering",this.record("overlay-enter"),this.changed(),this._overlayRaf=this.clock.requestAnimationFrame(()=>{this._overlayRaf=0,"entering"===this._overlayPhase&&(this._overlayPhase="fading-in",this.changed(),this._overlayTimer=this.clock.setTimeout(()=>{this._overlayTimer=0,"fading-in"===this._overlayPhase&&(this._overlayPhase="opaque",this._overlayOpaqueAt=this.clock.now(),this.record("overlay-opaque"),this.changed())},150))}))},150)}showOverlayNow(){this.clearOverlayTimer(),this._overlayPhase="opaque",this._overlayOpaqueAt=this.clock.now(),this.record("overlay-error")}finishOverlayAfterCommit(){if(this.clearOverlayTimer(),"none"===this._overlayPhase||"entering"===this._overlayPhase||"fading-in"===this._overlayPhase)return this.clearOverlay(),this._state="steady",this._recoveryReason=null,this.record("candidate-committed"),void this.changed();const e=Math.max(0,this._overlayOpaqueAt+250-this.clock.now());this._overlayTimer=this.clock.setTimeout(()=>{this._overlayTimer=0,this._overlayPhase="leaving",this.record("overlay-leave"),this.changed(),this._overlayTimer=this.clock.setTimeout(()=>{this._overlayTimer=0,this._overlayPhase="none",this._state="steady",this._recoveryReason=null,this.record("candidate-committed"),this.changed()},180)},e)}clearOverlayTimer(){this._overlayTimer&&this.clock.clearTimeout(this._overlayTimer),this._overlayTimer=0}clearOverlay(){this.clearOverlayTimer(),this._overlayRaf&&this.clock.cancelAnimationFrame(this._overlayRaf),this._overlayRaf=0,this._overlayPhase="none",this._overlayOpaqueAt=0}dispose(){this._disposed=!0,this._token++,this.clearOverlay();for(const e of this._barrierRafs)this.clock.cancelAnimationFrame(e);this._barrierRafs.clear()}}function Tl(e){let t=2166136261,i=2654435769;const n=new WeakSet,r=e=>{for(let n=0;n>>0)+(i>>>2)}},o=e=>{if(null===e)return void r("n");const t=typeof e;if("string"===t)return void r(`s${e.length}:${e}`);if("number"===t)return void r(`d${Number.isNaN(e)?"NaN":String(e)}`);if("boolean"===t)return void r(e?"t":"f");if("undefined"===t)return void r("u");if("object"!==t)return void r(`${t}:${String(e)}`);const i=e;if(n.has(i))r("[cycle]");else{if(n.add(i),Array.isArray(e)){r("[");for(const t of e)o(t);return r("]"),void n.delete(i)}r("{");for(const t of Object.keys(e).sort())r(t),o(e[t]);r("}"),n.delete(i)}};return o(e),`${(t>>>0).toString(16).padStart(8,"0")}${(i>>>0).toString(16).padStart(8,"0")}`}function Rl(e){return Tl(e)}const Cl=new WeakMap;class Dl{constructor(e,t=()=>Date.now()){this.onUpdate=e,this.now=t,this.fallbackAuthority={},this.referenceOwner={},this.referenced=new Set,this.queued=new Set,this.disposed=!1,this.sharedUpdate=()=>{this.disposed||this.onUpdate()}}bind(e){const t=((e,t)=>{const i=e?.connection||e?.callWS||t;return!i||"object"!=typeof i&&"function"!=typeof i?t:i})(e,this.fallbackAuthority);return this.shared&&this.authority===t||(this.shared&&(this.shared.listeners.delete(this.sharedUpdate),this.shared.references.delete(this.referenceOwner)),this.authority=t,this.shared=(e=>{let t=Cl.get(e);return t||(t={signed:{},queued:new Set,inFlight:new Map,retry:new Map,listeners:new Set,settlers:new Set,references:new Map},Cl.set(e,t)),t})(t),this.disposed||(this.shared.listeners.add(this.sharedUpdate),this.shared.references.set(this.referenceOwner,new Set(this.referenced)))),this.shared}start(e,t){this.disposed=!1,this.referenced=new Set(t());const i=this.bind(e());i.listeners.add(this.sharedUpdate),i.references.set(this.referenceOwner,new Set(this.referenced)),this.stopTimer(),this.resignTimer=setInterval(()=>this.resign(e(),t()),288e5)}dispose(){if(this.disposed=!0,this.stopTimer(),clearTimeout(this.batchTimer),this.shared){this.shared.listeners.delete(this.sharedUpdate),this.shared.references.delete(this.referenceOwner);for(const e of this.queued)this.shared.queued.delete(e)}this.queued.clear()}invalidate(e){const t=this.bind(e);clearTimeout(this.batchTimer),this.queued.clear(),t.queued.clear(),t.inFlight.clear(),t.retry.clear(),t.signed={}}stopTimer(){void 0!==this.resignTimer&&clearInterval(this.resignTimer),this.resignTimer=void 0}display(e,t){const i=Mn(t);if(!i.startsWith("/api/houseplan/content/"))return i;const n=this.bind(e);this.referenced.add(i),n.references.set(this.referenceOwner,new Set(this.referenced));const r=n.signed[i],o=r?this.now()-r.at:1/0;return o<$n?r.url:o{const t=[...this.queued];this.queued.clear();for(const e of t)i.queued.delete(e);this.sign(e,t)},30))}sign(e,t){if(!t.length||!e?.callWS)return;const i=this.bind(e);for(const n of function(e,t){const i=Math.max(1,Math.floor(t)),n=[];for(let t=0;t{const n=i.inFlight.get(e);return!(void 0!==n&&t-n<15e3)&&(i.inFlight.set(e,t),!0)});r.length&&e.callWS({type:"houseplan/content/sign",paths:r}).then(e=>{const t=this.now();let n=0,o=!1;for(const s of r){const r=e?.urls?.[s];if("string"==typeof r&&r){const e=i.signed[s];e?.loaded&&e.url!==r?(e.pending={url:r,at:t},this.preloadReplacement(i,s,r,t)):(i.signed[s]={url:r,at:t,loaded:e?.url===r&&!!e.loaded},o=!0),i.retry.delete(s),n++}else this.backOff(i,s)}if(n&&(this.trimShared(i),o))for(const e of[...i.listeners])e()}).catch(()=>{for(const e of r)this.backOff(i,e)}).finally(()=>{for(const e of r)i.inFlight.get(e)===t&&i.inFlight.delete(e);for(const e of[...i.settlers])e()})}}preloadReplacement(e,t,i,n){const r=()=>{const r=e.signed[t];if(r?.pending?.url===i){e.signed[t]={url:i,at:n,loaded:!0},e.retry.delete(t),this.trimShared(e);for(const t of[...e.listeners])t()}},o=()=>{const n=e.signed[t];n?.pending?.url===i&&(delete n.pending,this.backOff(e,t))};if("undefined"==typeof Image)return void r();const s=new Image;s.onload=()=>{("function"==typeof s.decode?s.decode():Promise.resolve()).then(r).catch(o)},s.onerror=o,s.src=i}preloadCurrentImage(e,t,i){if(i.loaded)return Promise.resolve(!0);if(i.preload)return i.preload;if("undefined"==typeof Image)return i.loaded=!0,Promise.resolve(!0);const n=i.url;return i.preload=new Promise(r=>{const o=new Image;let s=!1;const a=o=>{if(s)return;s=!0,clearTimeout(l);const a=e.signed[t];if(a===i&&delete a.preload,o&&a===i&&a.url===n){a.loaded=!0;for(const t of[...e.listeners])t();r(!0)}else o||a!==i||this.backOff(e,t),r(!1)};o.onload=()=>{("function"==typeof o.decode?o.decode():Promise.resolve()).then(()=>a(!0)).catch(()=>a(!1))},o.onerror=()=>a(!1);const l=setTimeout(()=>a(!1),kl);o.src=n}),i.preload}prepareImage(e,t){const i=Mn(t);if(!i.startsWith("/api/houseplan/content/"))return Promise.resolve(!0);const n=this.bind(e),r=()=>{const t=n.signed[i];if(t&&this.now()-t.atthis.now()&&!n.inFlight.has(i)&&!n.queued.has(i)?Promise.resolve(!1):null},o=r();return o?(s=o,new Promise(e=>{let t=!1;const i=i=>{t||(t=!0,clearTimeout(n),e(i))},n=setTimeout(()=>i(!1),kl);s.then(i).catch(()=>i(!1))})):new Promise(e=>{let t=!1,i=null;const o=i=>{t||(t=!0,clearTimeout(a),n.settlers.delete(s),e(i))},s=()=>{t||i||(i=r(),i&&i.then(o).catch(()=>o(!1)))},a=setTimeout(()=>o(!1),kl);n.settlers.add(s),s()});var s}backOff(e,t){const i=e.retry.get(t)?.delay||0,n=Math.min(6e4,i?2*i:2e3);e.retry.set(t,{notBefore:this.now()+n,delay:n})}trimShared(e){const t=Object.entries(e.signed);if(t.length<=512)return;const i=new Set(e.inFlight.keys());for(const t of e.references.values())for(const e of t)i.add(e);for(const[e,n]of t)(n.pending||n.preload)&&i.add(e);t.sort((e,t)=>t[1].at-e[1].at);const n=t.filter(([e])=>i.has(e));for(const e of t){if(n.length>=512)break;i.has(e[0])||n.push(e)}e.signed=Object.fromEntries(n)}resign(e,t){const i=this.bind(e);this.referenced=new Set(t),i.references.set(this.referenceOwner,new Set(this.referenced));const n=this.now(),r=new Set;for(const e of i.references.values())for(const t of e)r.add(t);const o={};for(const[e,t]of Object.entries(i.signed))r.has(e)&&n-t.at!!o[e]&&!o[e].pending))}markLoaded(e,t,i){const n=Mn(t);if(!n.startsWith("/api/houseplan/content/"))return;const r=this.bind(e).signed[n];if(r&&(!i||r.url===i)&&!r.loaded){r.loaded=!0;for(const t of[...this.bind(e).listeners])t()}}isReady(e,t){const i=Mn(t);if(!i.startsWith("/api/houseplan/content/"))return!0;const n=this.bind(e).signed[i];return!!n&&this.now()-n.ate.id===t)}var Ol="M11,18H13V16H11V18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,6A4,4 0 0,0 8,10H10A2,2 0 0,1 12,8A2,2 0 0,1 14,10C14,12 11,11.75 11,15H13C13,12.75 16,12.5 16,10A4,4 0 0,0 12,6Z";function zl(e,t,i){return[e[0]*t+e[1]*i+e[2],e[3]*t+e[4]*i+e[5]]}function Pl(e,t){let i=0;for(const[n,r]of t){const t=zl(e,n[0],n[1]);i=Math.max(i,Math.hypot(t[0]-r[0],t[1]-r[1]))}return i}const Fl=e=>{if(null==e||""===e)return null;const t=Number(e);return Number.isFinite(t)?t:null},Il=e=>{const t=e,i=Fl(Array.isArray(t)?t[0]:t?.x),n=Fl(Array.isArray(t)?t[1]:t?.y);return null==i||null==n?null:[i,n]};function El(e){if(!Array.isArray(e))return null;const t=[];for(const i of e){const e=Il(i);if(!e)return null;t.push(e)}if(t.length>1){const e=t[0],i=t[t.length-1];e[0]===i[0]&&e[1]===i[1]&&t.pop()}if(t.length<3)return null;let i=0,n=0,r=0;for(let e=0;ee+t[0],0)/t.length,t.reduce((e,t)=>e+t[1],0)/t.length]:[n/(3*i),r/(3*i)]}function Nl(e){if(!Array.isArray(e))return null;const t=[];for(const i of e){const e=Il(i);if(!e)return null;t.push(e)}if(t.length>1){const e=t[0],i=t[t.length-1];e[0]===i[0]&&e[1]===i[1]&&t.pop()}const i=El(t);if(!i)return null;const n=t.map(e=>e[0]),r=t.map(e=>e[1]);return{centroid:i,bbox:[Math.min(...n),Math.min(...r),Math.max(...n),Math.max(...r)]}}function Hl(e){let t=e;if(t&&"object"==typeof t&&!Array.isArray(t)){const e=t;t=e.path??e.points??t}if(!Array.isArray(t)||!t.length)return[];let i=(t.some(e=>(e=>Array.isArray(e)?e.length>=2&&!Array.isArray(e[0])&&("object"!=typeof e[0]||null==e[0]):!!e&&"object"==typeof e&&("x"in e||"y"in e))(e))?[t]:t.filter(Array.isArray)).map(e=>e.map(Il).filter(e=>null!=e)).filter(e=>e.length>=2);i.length>64&&(i=i.slice(-64));const n=i.reduce((e,t)=>e+t.length,0);if(n<=4e3)return i.map(e=>e.slice());const r=4e3-2*i.length,o=i.map(e=>e.length-2),s=o.reduce((e,t)=>e+t,0),a=o.map(e=>r*e/s),l=a.map(Math.floor);let c=r-l.reduce((e,t)=>e+t,0);const h=a.map((e,t)=>({index:t,fraction:e-Math.floor(e)})).sort((e,t)=>t.fraction-e.fraction||e.index-t.index);for(let e=0;efunction(e,t){if(t>=e.length)return e.slice();if(t<=2)return[e[0],e[e.length-1]];const i=[];for(let n=0;ne.slice()),i=t[t.length-1];return i?(i.length>2?i.pop():t.pop(),t):[]}function ql(e,t){return"default"!==e?e:null!=t?String(t):"default"}function Bl(e){if(!e)return null;const t=e.vacuum_position||e.robot_position||null,i=t&&null!=Fl(t.x)&&null!=Fl(t.y)?{x:Fl(t.x),y:Fl(t.y),a:Fl(t.a??t.angle??t.theta)}:null,n=Hl(e.path?.path??e.path?.points??e.path),r=[],o=e.rooms,s=Array.isArray(o)?o.map((e,t)=>[String(e?.id??t),e]):o&&"object"==typeof o?Object.entries(o):[];for(const[e,t]of s){if(!t||"object"!=typeof t)continue;const i=String(t.name??t.label??"").trim(),n=[[Fl(t.cx),Fl(t.cy)],[Fl(t.center?.x),Fl(t.center?.y)],[Fl(t.x),Fl(t.y)]].find(([e,t])=>null!=e&&null!=t);let o=n?.[0]??null,s=n?.[1]??null;const a=Nl(t.outline),l=Fl(t.x0),c=Fl(t.y0),h=Fl(t.x1),d=Fl(t.y1),u=(null!=l&&null!=c&&null!=h&&null!=d?[Math.min(l,h),Math.min(c,d),Math.max(l,h),Math.max(c,d)]:null)||a?.bbox||null;if(null!=o&&null!=s||!a||([o,s]=a.centroid),null!=o&&null!=s||!u||(o=(u[0]+u[2])/2,s=(u[1]+u[3])/2),i&&null!=o&&null!=s){const t={id:e,name:i,cx:o,cy:s};u&&([t.x0,t.y0,t.x1,t.y1]=u),r.push(t)}}const a=function(e){return String(e.map_name??e.current_map??e.map_index??e.selected_map??"default")}(e);return i||r.length||n.length?{pos:i,path:n,rooms:r,mapId:a}:null}function Wl(e,t,i){return![e,t,i].every(Number.isFinite)||e<0||t<=0||i<=0?Number.POSITIVE_INFINITY:e/t*i}const Ul=40;function Gl(e,t,i){if(!e||!t)return null;const n=t.attributes||{},r=e.split(".")[0]||"",o=null!=i?.platform?String(i.platform):null,s=!!Il(n.vacuum_position??n.robot_position),a=!(!n.rooms||"object"!=typeof n.rooms),l=Hl(n.path?.path??n.path?.points??n.path).length>0,c=[n.map_name,n.current_map,n.map_index,n.selected_map].some(e=>null!=e),h="xiaomi_cloud_map_extractor"===o,d=a||l||c;let u=null;if(s?u="compatible":h?u="known_xcme_incomplete":d?u="partial":"camera"===r&&(u="camera"),!u)return null;const p=s?"camera"===r?300:200:h?100:d?50:0;return{entityId:e,name:String(n.friendly_name||e),platform:o,category:u,hasPosition:s,hasRooms:a,hasPath:l,hasMapId:c,score:p}}const Vl=e=>e.toLowerCase().replace(/[\s_\-.,]+/g,"");function Kl(e,t){const i=new Set(t.map(Vl).filter(Boolean)),n=new Set;for(const t of e){const e=Vl(t.name||"");e&&i.has(e)&&n.add(e)}return n.size}function Yl(e,t){const i=new Map(t.map(e=>[Vl(e.name),e])),n=[],r=[];for(const t of e){const e=i.get(Vl(t.name));e&&(n.push([[t.cx,t.cy],[e.cx,e.cy]]),r.push(t.name))}if(n.length<3)return null;const o=function(e){if(e.length<3)return null;let t=0,i=0,n=0,r=0,o=0,s=0,a=0,l=0,c=0,h=0,d=0,u=0;for(const[[p,_],[m,g]]of e){if(![p,_,m,g].every(Number.isFinite))return null;t+=p*p,i+=p*_,n+=p,r+=_*_,o+=_,s+=1,a+=p*m,l+=_*m,c+=m,h+=p*g,d+=_*g,u+=g}const p=[t,i,n,i,r,o,n,o,s],_=e=>{const[t,i,n,r,o,s,a,l,c]=p,h=t*(o*c-s*l)-i*(r*c-s*a)+n*(r*l-o*a);if(!Number.isFinite(h)||Math.abs(h)<1e-9)return null;const d=[(o*c-s*l)/h,(n*l-i*c)/h,(i*s-n*o)/h,(s*a-r*c)/h,(t*c-n*a)/h,(n*r-t*s)/h,(r*l-o*a)/h,(i*a-t*l)/h,(t*o-i*r)/h];return[d[0]*e[0]+d[1]*e[1]+d[2]*e[2],d[3]*e[0]+d[4]*e[1]+d[5]*e[2],d[6]*e[0]+d[7]*e[1]+d[8]*e[2]]},m=_([a,l,c]),g=_([h,d,u]);if(!m||!g)return null;const f=[m[0],m[1],m[2],g[0],g[1],g[2]];return f.every(Number.isFinite)?f:null}(n);return o?{matrix:o,matched:r,residual:Pl(o,n)}:null}function Zl(e,t,i){const n=e[e.length-1];if(n&&n[0]===t[0]&&n[1]===t[1])return e;if(e.push(t),e.length<=600)return e;let r=function(e,t){if(e.length<3)return e.slice();const i=new Uint8Array(e.length);i[0]=i[e.length-1]=1;const n=[[0,e.length-1]];for(;n.length;){const[r,o]=n.pop(),[s,a]=e[r],[l,c]=e[o],h=l-s,d=c-a,u=Math.hypot(h,d)||1e-9;let p=0,_=-1;for(let t=r+1;tp&&(p=i,_=t)}_>0&&p>t&&(i[_]=1,n.push([r,_],[_,o]))}const r=[];for(let t=0;t600&&(r=r.filter((e,t)=>t%2==0||t===r.length-1)),r}function Xl(e){return"cleaning"===e||"returning"===e||"on"===e}const Jl={0:[1,0],90:[0,1],180:[-1,0],270:[0,-1]};function Ql(e){const[t,i]=Jl[e.rot]||[1,0],n=e.mir?-1:1;return[e.s*t*n,-e.s*i,e.ox,e.s*i*n,e.s*t,e.oy]}function ec(e){const t=e[0]*e[4]-e[1]*e[3];if(!Number.isFinite(t)||Math.abs(t)<1e-12)return null;const i=t<0,n=Math.sqrt(Math.abs(t));let r=180*Math.atan2(-e[1],e[4])/Math.PI;return r=(90*Math.round(r/90)%360+360)%360,{ox:e[2],oy:e[5],s:n,rot:r,mir:i}}function tc(e,t){const i=[],n=[];for(const t of e)null!=t.x0?(i.push(t.x0,t.x1),n.push(t.y0,t.y1)):(i.push(t.cx),n.push(t.cy));if(!i.length)return{ox:t[0]+t[2]/2,oy:t[1]+t[3]/2,s:t[2]/1e4,rot:0,mir:!0};const r=Math.min(...i),o=Math.max(...i),s=Math.min(...n),a=Math.max(...n),l=Math.max(o-r,a-s)||1,c={ox:0,oy:0,s:.6*Math.min(t[2],t[3])/l,rot:0,mir:!0},h=Ql(c),[d,u]=zl(h,(r+o)/2,(s+a)/2);return c.ox=t[0]+t[2]/2-d,c.oy=t[1]+t[3]/2-u,c}function ic(e,t,i,n){const[r,o]=zl(Ql(t),i,n),s=Ql({...e,ox:0,oy:0}),[a,l]=zl(s,i,n);return{...e,ox:r-a,oy:o-l}}function nc(e){const t=e?.trail_mode;return"never"===t||"cleaning"===t||"always"===t?t:!1===e?.trail?"never":"cleaning"}const rc={availability:"available",status:"alarm",activity:"none"},oc=new Set(["motion","vibration","sound"]),sc=new Set(["occupancy","presence"]),ac=new Set(["door","window","garage_door","opening"]),lc=new Set(["running","power"]),cc=new Set(["smoke","gas","carbon_monoxide","moisture","safety","tamper","problem"]);const hc=new Set(["running","working","washing","rinsing","spinning","drying","heating","cooling","cleaning","cooking","playing","recording","pumping","irrigating","humidifying","dehumidifying","fan","preheating","defrosting"]),dc=new Set([...hc,"start","started","run","active","in_progress","wash","rinse","spin","dry"]),uc=new Set(["off","idle","paused","standby","docked","finished","complete","completed","stopped","ready","sleeping","stop","end","done","inactive"]),pc=new Set(["heat","cool","heat_cool","auto","dry","fan_only"]),_c=e=>""===e||"unknown"===e||"unavailable"===e||"__missing__"===e,mc=e=>String(e??"").trim().toLowerCase(),gc=e=>mc(e).replace(/[\s-]+/g,"_").replace(/_+/g,"_").replace(/^_|_$/g,""),fc=new Map([["run_state",0],["job_state",0],["operation_state",0],["activity_state",0],["machine_state",1],["running_state",1],["status",2],["device_status",2],["machine_status",2]]),vc=new Set(["wifi","connection","signal","battery"]);function yc(e,t){const i=e?.entities?.[t]||{};if("config"===i.entity_category)return null;const n=String(t||"").split(".").slice(1).join("."),r=[i.translation_key,i.original_name,i.name],o=[...r,n];r.some(e=>mc(e))||o.push(e?.states?.[t]?.attributes?.friendly_name);const s=o.map(gc).filter(Boolean);if(s.some(e=>e.split("_").some(e=>vc.has(e))))return null;let a=null;for(const e of s)for(const[t,i]of fc)(e===t||e.endsWith(`_${t}`))&&(a=null==a?i:Math.min(a,i));return a}const bc=(e,t)=>null!=yc(e,t);function wc(e,t){if(!t.startsWith("switch."))return!1;const i=e?.entities?.[t]||{},n=e?.states?.[t],r=t.slice(7).toLowerCase();if(/(?:^|_)(?:main_)?power$/.test(r))return!0;if([i.translation_key,i.original_name,i.name].map(mc).some(e=>["power","main power","power switch","питание"].includes(e)))return!0;const o=mc(n?.attributes?.friendly_name);return/(?:^|[\s._-])(?:main[\s._-]+)?power$/.test(o)||/(?:^|[\s._-])питание$/.test(o)}function kc(e){for(const t of["hvac_action","action","current_operation","run_state","job_state","operation","activity"]){const i=mc(e?.[t]);if(hc.has(i)||uc.has(i))return i}return""}function xc(e,t){const i=e?.states?.[t],n=i?mc(i.state):"__missing__",r=String(t||"").split(".")[0],o=mc(i?.attributes?.device_class),s={eid:t,state:n,availability:_c(n)?"unavailable":"available",status:"neutral",activity:"none",edge:"none"};if("unavailable"===s.availability)return s;if(function(e,t,i){return"alarm_control_panel"===e?"triggered"===i:"on"===i&&In(e,t)}(r,o,n))return{...s,status:"alarm"};if("binary_sensor"===r)return oc.has(o)?{...s,edge:"rising"}:sc.has(o)?{...s,activity:"on"===n?"presence":"none"}:ac.has(o)?{...s,status:"on"===n?"open":"neutral",edge:"rising"}:"moving"===o?{...s,activity:"on"===n?"transition":"none"}:lc.has(o)&&"on"===n?{...s,status:"working",activity:"running"}:s;if("cover"===r)return{...s,activity:"opening"===n||"closing"===n?"transition":"none",edge:"terminal_transition"};if("lock"===r)return{...s,status:"unlocked"===n||"open"===n?"open":"neutral",activity:"locking"===n||"unlocking"===n?"transition":"none",edge:"terminal_transition"};if("valve"===r)return{...s,status:["open","opening","closing"].includes(n)?"open":"neutral",activity:"opening"===n||"closing"===n?"transition":"none",edge:"terminal_transition"};if("climate"===r){const e=kc(i.attributes),t=Array.isArray(i.attributes?.hvac_modes)?i.attributes.hvac_modes.map(mc):[],r=!uc.has(n)&&(pc.has(n)||hc.has(n)||t.includes(n));return(e?hc.has(e):r)?{...s,status:"working",activity:"running"}:s}if(["light","switch","fan","humidifier"].includes(r))return"on"===n?{...s,status:"working",activity:"running"}:s;if("media_player"===r)return"off"===n?{...s,availability:"unavailable"}:s;if("vacuum"===r)return"cleaning"===n?{...s,status:"working",activity:"running"}:"returning"===n?{...s,status:"working",activity:"transition"}:s;if("script"===r)return"on"===n?{...s,status:"working",activity:"running"}:s;if("automation"===r)return s;if("button"===r||"event"===r)return{...s,edge:"change"};const a=kc(i.attributes);return hc.has(a)||hc.has(n)&&!uc.has(n)?{...s,status:"working",activity:"running"}:s}function $c(e,t,i){const n=i.filter(t=>t.startsWith("switch.")&&!e?.entities?.[t]?.entity_category),r=t.find(t=>wc(e,t));if(!(n.length>1&&!!r))return t.map(t=>xc(e,t));const o=t.find(t=>t!==r&&bc(e,t)),s=xc(e,r);return"unavailable"===s.availability||"off"===s.state?t.map(t=>({eid:t,state:e?.states?.[t]?mc(e.states[t].state):"__missing__",availability:"unavailable",status:"neutral",activity:"none",edge:"none"})):t.map(t=>{if(t===o)return function(e,t){const i=e?.states?.[t],n=i?mc(i.state):"__missing__",r={eid:t,state:n,availability:_c(n)?"unavailable":"available",status:"neutral",activity:"none",edge:"none"};if("unavailable"===r.availability)return r;const o=kc(i?.attributes);return hc.has(o)||dc.has(n)&&!uc.has(n)?{...r,status:"working",activity:"running"}:r}(e,t);return{...t===r?s:xc(e,t),status:"neutral",activity:"none",edge:"none"}})}function Sc(e){if(!e.length)return{availability:"available",status:"neutral",activity:"none"};const t=e.filter(e=>"available"===e.availability);if(!t.length)return{availability:"unavailable",status:"neutral",activity:"none"};if(t.some(e=>"alarm"===e.status))return rc;const i=t.some(e=>"working"===e.status)?"working":t.some(e=>"open"===e.status)?"open":"neutral",n=t.some(e=>"transition"===e.activity)?"transition":t.some(e=>"presence"===e.activity)?"presence":t.some(e=>"running"===e.activity)?"running":"none";return{availability:"available",status:i,activity:n}}function Mc(e,t){if(!(e=>!!e&&!_c(e))(e)||"unavailable"===t.availability||e===t.state)return null;if("rising"===t.edge)return"off"===e&&"on"===t.state?"event":null;if("change"===t.edge)return"event";if("terminal_transition"===t.edge){const i=new Set([e,t.state]);if(i.has("closed")&&i.has("open")||i.has("locked")&&i.has("unlocked"))return"transition"}return null}const Tc=(e,t=0)=>{const i=Number(e);return Number.isInteger(i)&&i>=0?i:t},Rc=(e,t)=>{const i={rev:0,configRev:Tc(t),off:new Set};if(null==e)return{snapshot:i,valid:!1};const n=e.off;return!Number.isInteger(e.rev)||Number(e.rev)<0||!Number.isInteger(e.config_rev)||Number(e.config_rev)<0||!Array.isArray(n)||n.some(e=>"string"!=typeof e||!e)?{snapshot:i,valid:!1}:{snapshot:{rev:Number(e.rev),configRev:Number(e.config_rev),off:new Set(n)},valid:!0}};function Cc(e,t=0){return Rc(e,t).snapshot}function Dc(e,t,i,n){const r=Rc(t,i);return n&&r.valid&&r.snapshot.configRev===i?e.configRev===i&&r.snapshot.rev{try{const[i,n]=await Promise.all([e.callWS({type:"config/device_registry/list"}),e.callWS({type:"config/entity_registry/list"})]),r=qc(i,"id"),o=qc(n,"entity_id");if(!r||!o)throw new Error("invalid_registry_response");t.devices=r,t.entities=o,t.authoritative=!0,t.access="full",t.lastSuccess=Date.now(),t.error=void 0}catch(e){t.authoritative=!1,t.access="limited",t.error=function(e){if(e&&"object"==typeof e){const t=e;return String(t.message||t.code||t.error||"registry_unavailable")}return String(e||"registry_unavailable")}(e)}finally{t.revision++,t.loading=void 0,function(e){for(const t of[...e.listeners])try{t()}catch{}}(t)}})()),t.loading}function Wc(e,t){void 0===t.reloadTimer&&(t.reloadTimer=globalThis.setTimeout(()=>{t.reloadTimer=void 0,Bc(e,t)},80))}async function Uc(e,t){if(t.subscribing||t.unsubDevice&&t.unsubEntity)return t.subscribing;const i=e?.connection?.subscribeEvents;return"function"==typeof i?(t.subscribing=(async()=>{try{t.unsubDevice||(t.unsubDevice=await i.call(e.connection,()=>Wc(e,t),"device_registry_updated")),t.unsubEntity||(t.unsubEntity=await i.call(e.connection,()=>Wc(e,t),"entity_registry_updated"))}catch{}finally{0===t.refs&&(t.unsubDevice?.(),t.unsubEntity?.(),t.unsubDevice=void 0,t.unsubEntity=void 0),t.subscribing=void 0}})(),t.subscribing):void 0}function Gc(e,t){const i=jc(e);if(!i)return()=>{};const n=0===i.refs;i.refs++,i.listeners.add(t),("pending"===i.access||n)&&Bc(e,i),Uc(e,i);let r=!1;return()=>{r||(r=!0,i.listeners.delete(t),i.refs=Math.max(0,i.refs-1),i.refs>0||(i.unsubDevice?.(),i.unsubEntity?.(),i.unsubDevice=void 0,i.unsubEntity=void 0,void 0!==i.reloadTimer&&globalThis.clearTimeout(i.reloadTimer),i.reloadTimer=void 0))}}function Vc(e){const t=jc(e);if(!t)return{revision:0,authoritative:!1,access:"limited",devices:e?.devices||{},entities:e?.entities||{},lastSuccess:0,error:"registry_unavailable"};let i=e?.devices||{},n=e?.entities||{};if(t.authoritative){const r=t.liveDevices!==i||t.liveEntities!==n,o=void 0!==t.liveDevices||void 0!==t.liveEntities;if(r&&(t.liveDevices=i,t.liveEntities=n,t.projectedRevision=void 0,o&&Wc(e,t)),t.projectedRevision!==t.revision||!t.projectedDevices||!t.projectedEntities){const e={...t.devices},r={...t.entities};for(const[t,n]of Object.entries(i))Object.prototype.hasOwnProperty.call(e,t)||(e[t]=n);for(const[e,t]of Object.entries(n))Object.prototype.hasOwnProperty.call(r,e)||(r[e]=t);t.projectedDevices=e,t.projectedEntities=r,t.projectedRevision=t.revision}i=t.projectedDevices,n=t.projectedEntities}return{revision:t.revision,authoritative:t.authoritative,access:t.access,devices:i,entities:n,lastSuccess:t.lastSuccess,error:t.error}}function Kc(e){if(!e||"object"!=typeof e&&"function"!=typeof e)return 0;let t=Ec.get(e);return t||(t=Nc++,Ec.set(e,t)),t}function Yc(e,t=Vc(e)){return[t.revision,t.access,Kc(t.devices),Kc(t.entities)].join(":")}function Zc(e){return!!e&&null==e.disabled_by}function Xc(e,t=Vc(e)){const i={},n={},r={};for(const[e,n]of Object.entries(t.devices||{}))Zc(n)&&(i[e]=n);for(const[e,i]of Object.entries(t.entities||{})){if(!Zc(i))continue;const r=i.device_id?t.devices?.[i.device_id]:null;t.authoritative&&i.device_id&&!r||(r&&!Zc(r)||(n[e]=i))}for(const[i,n]of Object.entries(e?.states||{})){const e=t.entities?.[i];if(e&&!Zc(e))continue;const o=e?.device_id?t.devices?.[e.device_id]:null;t.authoritative&&e?.device_id&&!o||(o&&!Zc(o)||(r[i]=n))}return{...e,devices:i,entities:n,states:r}}function Jc(e,t=Vc(e)){return{...e,devices:t.devices||{},entities:t.entities||{}}}function Qc(e,t){const i=[];for(const[n,r]of Object.entries(e||{}))r?.device_id===t&&i.push(n);return i}function eh(){if(Lc)return Lc;if("undefined"==typeof localStorage)return{};try{const e=JSON.parse(localStorage.getItem(Hc)||"{}");if(!e||"object"!=typeof e||Array.isArray(e))return{};const t=Date.now(),i={};for(const[n,r]of Object.entries(e))!r||"active"!==r.kind&&"ha_disabled"!==r.kind||!Number.isFinite(r.ts)||t-r.ts>7776e6||(i[n]=r);return Lc=i,Lc}catch{return{}}}function th(e,t,i=Vc(e)){if(!t||"virtual"===t)return{kind:"active",enabledEntityIds:[],allEntityIds:[]};const n=t.indexOf(":");if(n<1)return{kind:"unverified",reason:"registry_unavailable",enabledEntityIds:[],allEntityIds:[]};const r=t.slice(0,n),o=t.slice(n+1);if("device"!==r&&"entity"!==r||!o)return{kind:"unverified",reason:"registry_unavailable",enabledEntityIds:[],allEntityIds:[]};const s=i.devices||{},a=i.entities||{};if(i.authoritative){if("device"===r){const e=s[o];if(!e)return{kind:"orphaned",reason:"device_missing",enabledEntityIds:[],allEntityIds:[]};const t=Qc(a,o);if(!Zc(e))return{kind:"ha_disabled",reason:"device",enabledEntityIds:[],allEntityIds:t};const i=t.filter(e=>Zc(a[e]));return t.length&&!i.length?{kind:"ha_disabled",reason:"all_entities",enabledEntityIds:[],allEntityIds:t}:{kind:"active",enabledEntityIds:i,allEntityIds:t}}const t=a[o];return t&&!Zc(t)?{kind:"ha_disabled",reason:"entity",enabledEntityIds:[],allEntityIds:[o]}:t?.device_id&&!s[t.device_id]?{kind:"orphaned",reason:"device_missing",enabledEntityIds:[],allEntityIds:[o]}:t?.device_id&&!Zc(s[t.device_id])?{kind:"ha_disabled",reason:"device",enabledEntityIds:[],allEntityIds:[o]}:t||e?.states?.[o]?{kind:"active",enabledEntityIds:[o],allEntityIds:[o]}:{kind:"orphaned",reason:"entity_missing",enabledEntityIds:[],allEntityIds:[]}}const l=function(e){return eh()[e]||null}(t);if("device"===r){const t=s[o],i=Qc(a,o);if(null!=t?.disabled_by)return{kind:"ha_disabled",reason:"device",enabledEntityIds:[],allEntityIds:i};const n=i.filter(e=>{const t=a[e];return null==t?.disabled_by&&(!t.device_id||null==s[t.device_id]?.disabled_by)});if(i.length&&!n.length&&i.every(e=>null!=a[e]?.disabled_by))return{kind:"ha_disabled",reason:"all_entities",enabledEntityIds:[],allEntityIds:i};if("ha_disabled"===l?.kind)return{kind:"ha_disabled",reason:l.reason||"device",enabledEntityIds:[],allEntityIds:i};if(t||n.some(t=>!!e?.states?.[t]))return{kind:"active",enabledEntityIds:n,allEntityIds:i}}else{const t=a[o];if(null!=t?.disabled_by)return{kind:"ha_disabled",reason:"entity",enabledEntityIds:[],allEntityIds:[o]};if(t?.device_id&&null!=s[t.device_id]?.disabled_by)return{kind:"ha_disabled",reason:"device",enabledEntityIds:[],allEntityIds:[o]};if("ha_disabled"===l?.kind)return{kind:"ha_disabled",reason:l.reason||"entity",enabledEntityIds:[],allEntityIds:[o]};if(t||e?.states?.[o])return{kind:"active",enabledEntityIds:[o],allEntityIds:[o]}}return{kind:"unverified",reason:"registry_unavailable",enabledEntityIds:[],allEntityIds:[]}}function ih(e){const t={};for(const[i,n]of Object.entries(e.entities)){if(!n?.device_id||!Zc(n))continue;const r=e.devices?.[n.device_id];r&&!Zc(r)||(t[n.device_id]=t[n.device_id]||[]).push(i)}return t}function nh(e,t){const i=new Map;for(const n of e){if(!0===n?.removed)continue;const e=n?.binding||"";if(!e.startsWith("entity:"))continue;const r=e.slice(7);if(!r)continue;const o=t?.entities?.[r]?.device_id;if(!o)continue;const s=i.get(o)||new Set;s.add(r),i.set(o,s)}return{byDevice:i}}function rh(e,t,i,n){const r=n.byDevice.get(t);return r?.size?{partial:!0,entityIds:i.filter(t=>!r.has(t)&&!e?.entities?.[t]?.hidden)}:{partial:!1,entityIds:[...i]}}function oh(e,t,i){if(t.identifiers?.[0]?.[0])return t.identifiers[0][0];for(const t of i){const i=e.entities[t]?.platform;if(i)return i}return""}function sh(e,t){if(/_device_temperature$/.test(t))return!1;if(e.entities?.[t]?.entity_category)return!1;const i=e.states[t];if(!i)return/_temperature$/.test(t);const n=i.attributes||{};return"temperature"===n.device_class||/°C|°F/.test(n.unit_of_measurement||"")||/_temperature$/.test(t)}const ah=["vacuum","lawn_mower","climate","media_player","light","cover","lock","valve","alarm_control_panel","water_heater","fan","humidifier","siren","camera","remote"],lh=e=>[...e.filter(e=>!e.reg?.hidden),...e.filter(e=>!!e.reg?.hidden)];function ch(e,t){const i=t.map(t=>({eid:t,reg:e?.entities?.[t]})).filter(e=>!!e.reg);if(!i.length)return[];const n=i.filter(e=>!e.reg.entity_category),r=n.length?n:i;for(const e of ah){const t=r.filter(t=>t.eid.startsWith(e+"."));if(t.length)return lh(t).map(e=>e.eid)}const o=r.filter(t=>function(e,t){if(!t.startsWith("binary_sensor."))return!1;const i=mc(e?.states?.[t]?.attributes?.device_class||e?.entities?.[t]?.device_class||e?.entities?.[t]?.original_device_class);return oc.has(i)||sc.has(i)||ac.has(i)||lc.has(i)||"moving"===i||cc.has(i)}(e,t.eid));if(o.length)return lh(o).map(e=>e.eid);const s=r.filter(e=>e.eid.startsWith("switch."));if(s.length){const t=lh(s),n=t.find(t=>wc(e,t.eid));if(s.length>1&&n){const t=i.filter(t=>t.eid!==n.eid&&"config"!==t.reg?.entity_category&&null!=yc(e,t.eid)),r=t.filter(e=>!e.reg?.entity_category),o=r.length?r:t;for(const t of[0,1,2]){const i=lh(o.filter(i=>yc(e,i.eid)===t));if(i.length)return[i[0].eid,n.eid]}}return[(n||t[0]).eid]}const a=[];for(const e of Ie)a.push(...lh(r.filter(t=>t.eid.startsWith(e+"."))));return a.push(...lh(r.filter(e=>!Ie.includes(e.eid.split(".")[0])))),a.map(e=>e.eid)}function hh(e,t,i){const n=t.map(t=>({eid:t,reg:e.entities[t],st:e.states[t]})).filter(e=>e.reg),r=[n.filter(e=>!e.reg.hidden&&!e.reg.entity_category),n.filter(e=>!e.reg.entity_category),n.filter(e=>!e.reg.hidden),n];if("mdi:thermometer"===i||"mdi:air-filter"===i)for(const t of r){const i=t.find(t=>sh(e,t.eid));if(i)return i.eid}const o=ch(e,t);return o.find(yn)||o[0]}function dh(e,t,i=[]){return[...new Set(uh(e,t,i))].filter(e=>yn(e))}function uh(e,t,i=[]){const n=e?.startsWith("entity:")?new Set([e.slice(7)]):new Set(i);return(t||[]).filter(e=>"string"==typeof e&&!n.has(e))}function ph(e,t){if("string"==typeof t)return e.area===t;const i=e.marker?.room_id;return i?!!t.id&&i===t.id:!!t.area&&e.area===t.area}function _h(e){const t=e.marker?.binding?.startsWith("entity:")?e.marker.binding.slice(7):null,i=e.entities.filter(yn),n=e.primary&&yn(e.primary)?e.primary:null;return[...new Set([t,n,...i].filter(e=>!!e&&yn(e)))]}function mh(e){const t=_h(e),i=e.marker?.light_entity;return i&&t.includes(i)?i:t[0]||null}function gh(e,t){if(!1===t.marker?.is_light)return[];if(!0===t.marker?.is_light){return[{eid:mh(t),via:"forced"}]}const i=t.primary||ch(e,t.entities)[0];return i&&!i.startsWith("light.")?[]:t.entities.filter(t=>t.startsWith("light.")&&!!e.states?.[t]).map(e=>({eid:e,via:"light"}))}const fh=new WeakMap,vh=new WeakMap;function yh(e){return e.map(e=>[e.id||"",!0===e.hidden?1:0,e.primary||"",[...e.entities].join(","),void 0===e.controls?"":[...e.controls].join(","),e.marker?.id||"",e.marker?.binding||"",e.marker?.is_light,e.marker?.tap_action||"",!0===e.marker?.removed?1:0,e.marker?.light_entity||"",null==e.marker?.controls?"":[...e.marker.controls].join(",")].join("")).join("")}function bh(e){const t=yh(e),i=fh.get(e);if(i?.fingerprint===t)return i;const n=e.filter(e=>!e.hidden),r=new Map,o=new Map;for(const t of e){o.set(t,uh(t.marker?.binding,t.marker?.controls??t.controls,t.entities));const e=t.hidden||!0!==t.marker?.is_light?null:t.marker?.id||t.id;e&&r.set(String(e),t)}const s=new Map;for(const t of e){const e=o.get(t)||[],i=new Set(void 0===t.controls?e.filter(yn):t.controls.filter(yn)),n=mh(t),a=i.size?[...i]:n?[n]:[];for(const i of e){if(!i.startsWith("marker:"))continue;const e=i.slice(7);if(!e||e===String(t.marker?.id||t.id||""))continue;if(!r.has(e))continue;const n=s.get(e)||{markerId:e,controllers:[],driverEids:[]};n.controllers.push({device:t,driverEids:[...a]}),n.driverEids=[...new Set([...n.driverEids,...a])],s.set(e,n)}}const a={fingerprint:t,visible:n,markerById:r,persistedByDevice:o,incomingByMarker:s};return fh.set(e,a),a}function wh(e){return bh(e).incomingByMarker}function kh(e,t){const i=dh(t.marker?.binding,t.controls??t.marker?.controls,t.entities),n=uh(t.marker?.binding,t.marker?.controls??t.controls,t.entities);return(null!=t.marker?.is_light||!i.length&&!n.some(e=>e.startsWith("marker:")))&&gh(e,t).length>0}function xh(e,t){return gh(e,t).some(e=>!!e.eid)}function $h(e,t,i,n){const r="always"===e||"auto"===e&&t,o=r&&!i;return{sourceExists:r,fromSourceEnabled:r&&i,manualEnabled:r,radiusEnabled:r,passive:o,effectiveMode:o&&"auto"===n?"fixed":n}}function Sh(e,t){const i=dh(t.marker?.binding,t.controls??t.marker?.controls,t.entities).map(e=>({eid:e,via:"controls"})),n=kh(e,t)?gh(e,t):[];for(const e of n)e.eid&&i.some(t=>t.eid===e.eid)||i.push(e);return i}function Mh(e,t,i,n){if(null!=i)return Mh(e,t,null,n).filter(e=>ph(e.device,i));const r=yh(t),o=function(e,t){const i=new Set;for(const e of t){for(const t of e.entities)i.add(t);for(const t of e.controls||e.marker?.controls||[])"string"!=typeof t||t.startsWith("marker:")||i.add(t)}return[...i].sort().map(t=>`${t}:${e?.states?.[t]?.state??""}`).join("|")}(e,t),s=Oc(n),a=vh.get(t);if(a?.graphFingerprint===r&&a.stateFingerprint===o&&a.virtualLightFingerprint===s&&a.registry===e?.entities)return a.sources;const{visible:l,markerById:c,persistedByDevice:h,incomingByMarker:d}=bh(t),u=null==i?l:l.filter(e=>ph(e,i)),p=new Map,_=new Map;for(const t of u){const i=Sh(e,t).filter(e=>"controls"!==e.via);for(const n of i){const i=!0===t.marker?.is_light?String(t.marker?.id||t.id||""):"",r=i?`marker:${i}`:`entity:${n.eid}`,o=n.eid?[n.eid]:[],s={key:r,eid:n.eid||"",stateEids:o,serviceEids:o,device:t,via:n.via,castsGlow:!0,passive:!n.eid,on:!n.eid||"on"===e.states?.[n.eid]?.state};p.set(t,[...p.get(t)||[],s]),n.eid&&_.set(n.eid,s)}}const m=new Map;for(const e of t){const t=h.get(e)||[],i=new Set(void 0===e.controls?t.filter(yn):e.controls);m.set(e,i)}for(const t of p.values())for(const i of t){if(!i.passive)continue;const t=i.key.slice(7),r=d.get(t);i.on=!r||r.driverEids.some(t=>"on"===e.states?.[t]?.state),!r&&zc(i.device.marker)&&(i.on=Pc(i.device.marker,n))}const g=new Map,f=e=>{const t=g.get(e.key);(!t||e.castsGlow&&!t.castsGlow)&&g.set(e.key,e)};for(const t of l){const n=h.get(t)||[],r=null==i||ph(t,i);for(const i of n){if(i.startsWith("marker:")){const e=c.get(i.slice(7)),t=e?p.get(e):null;if(t)for(const e of t)f(e);continue}if(!yn(i)||!m.get(t)?.has(i))continue;const n=_.get(i);n?f(n):r&&f({key:`entity:${i}`,eid:i,stateEids:[i],serviceEids:[i],device:t,via:"controls",castsGlow:!1,passive:!1,on:"on"===e.states?.[i]?.state})}for(const e of p.get(t)||[])f(e)}const v=[...g.values()];return vh.set(t,{graphFingerprint:r,stateFingerprint:o,virtualLightFingerprint:s,registry:e?.entities,sources:v}),v}function Th(e){const t=e.filter(e=>e.castsGlow);return t.find(e=>e.on)||t[0]||null}function Rh(e){return e.length?e.some(e=>e.on)?"on":"off":"none"}function Ch(e,t){const i=[];for(const n of t){const t=e.states[n];if(!t)continue;const r=(t.attributes?.unit_of_measurement||"").toLowerCase();if(/_(linkquality|lqi)$/.test(n)||"lqi"===r){const e=parseFloat(t.state);isNaN(e)||i.push(e);continue}const o=t.attributes?.linkquality??t.attributes?.lqi;if(null!=o){const e=parseFloat(o);isNaN(e)||i.push(e)}}return Mi(i)}function Dh(e,t){for(const i of t){if(!sh(e,i))continue;const t=e.states[i];if(!t)continue;const n=parseFloat(t.state);if(!isNaN(n))return Math.round(10*n)/10}return null}function Ah(e,t){for(const i of t){if(!i.startsWith("climate."))continue;const t=e.states[i];if(!t||"unavailable"===t.state||"unknown"===t.state)continue;const n=parseFloat(t.attributes?.current_temperature);if(Number.isFinite(n))return Math.round(10*n)/10}return null}function Oh(e,t){if(e.entities?.[t]?.entity_category)return!1;const i=e.states[t];if(!i)return/_humidity$/.test(t);const n=i.attributes||{};return"humidity"===n.device_class||"%"===n.unit_of_measurement&&/_humidity$/.test(t)||/_humidity$/.test(t)}function zh(e,t){for(const i of t){if(!Oh(e,i))continue;const t=e.states[i];if(!t)continue;const n=parseFloat(t.state);if(!isNaN(n))return Math.round(n)}return null}function Ph(e,t){if(!t)return[];const i=[];for(const[t,n]of Object.entries(e.entities)){if(!t.startsWith("light.")||n.hidden||!Zc(n))continue;let r=null;if("group"===n.platform)r=n.area_id||null;else{if(!n.device_id)continue;{const t=e.devices[n.device_id];if(!Zc(t))continue;if("Group"!==t?.model)continue;r=t.area_id||n.area_id||null}}if(!r)continue;const o=e.states[t];i.push({eid:t,name:n.name||o?.attributes?.friendly_name||t,area:r})}return i}function Fh(e,t,i,n,r){const o=Fe(t,i,r);if(o!==Pe)return o;const s=[];for(const t of n){const i=e.states[t]?.attributes?.device_class;i&&s.push(i)}return function(e){for(const t of e){const e=ze[t];if(e)return e}return null}(s)??Pe}function Ih(e,t){return e.map(e=>{if(!Array.isArray(e.controls))return e;const i=e.controls.filter(e=>!("string"==typeof e&&e.startsWith("marker:")&&t.has(e.slice(7))));return i.length===e.controls.length?e:{...e,controls:i.length?i:null}})}function Eh(e,t,i){if(!t||!i||t===i)return[...e];const n=`marker:${t}`,r=`marker:${i}`;return e.map(e=>{const t=Array.isArray(e.controls)?e.controls.map(e=>e===n?r:e):e.controls,i="derived_marker_state"===e.value_badge?.source?.kind&&e.value_badge.source.ref===n?{...e.value_badge,source:{...e.value_badge.source,ref:r}}:e.value_badge;return t===e.controls&&i===e.value_badge?e:{...e,controls:t,value_badge:i}})}function Nh(e,t,i){if(!t||t===i)return!0;const n=new Map;for(const t of e)n.set(t.id,(t.controls||[]).filter(e=>"string"==typeof e&&e.startsWith("marker:")).map(e=>e.slice(7)));const r=new Set,o=[i];for(;o.length;){const e=o.pop();if(e===t)return!0;r.has(e)||(r.add(e),o.push(...n.get(e)||[]))}return!1}function Hh(e,t,i,n){const r=new Set([t]),o=e.filter(e=>{const o=e.id===t||!n&&e.binding===i;return o&&r.add(e.id),!o});return{markers:n?o:[...o,{id:t,binding:i,removed:!0,hidden:!0}],cleanupIds:r}}function Lh(e){const t=new Set,i=new Set,n=new Set;for(const r of e||[]){const e=String(r.binding||"").indexOf(":");if(e<1)continue;const o=r.binding.slice(0,e),s=r.binding.slice(e+1);s&&(!0===r?.removed?"device"===o?t.add(s):"entity"===o&&i.add(s):"entity"===o&&n.add(s))}return{devices:t,entities:i,liveEntities:n}}function jh(e,t,i){if(i.liveEntities.has(t))return!1;if(i.entities.has(t))return!0;const n=e?.entities?.[t]?.device_id;return!!n&&i.devices.has(n)}function qh(e,t,i,n,r){const o=dh(t.binding,t.controls,e.entities).filter(e=>!jh(i,e,n)).filter(e=>"active"===th(i,"entity:"+e,r).kind);e.marker=t,e.controls=o,e.userHidden=!0===t.hidden,e.hidden=e.userHidden||"ha_disabled"===e.bindingStatus?.kind,t.name&&(e.name=t.name),t.icon&&(e.icon=t.icon),null!=t.model&&(e.model=t.model),e.link=t.link??null,e.description=t.description??null,e.pdfs=t.pdfs||[],e.tapAction=t.tap_action??null}function Bh(e,t,i,n){if("string"==typeof e.room_id&&e.room_id.length>0&&null===e.area)return{area:"",space:e.space||n};const r=e.area||t||"";return{area:r,space:r&&i[r]||e.space||n}}function Wh(e){const t=e.hass,i=e.registry||Vc(t),n=Xc(t,i),r=Jc(t,i),{areaToSpace:o,markers:s,settings:a,excluded:l,showAll:c,firstSpaceId:h,loc:d,iconRules:u}=e,p=!1!==a.group_lights,_=Lh(s),m=Ph(n,p).filter(e=>!jh(n,e.eid,_)),g=new Set(m.map(e=>e.area)),f=ih(n),v=function(e){const t={};for(const[i,n]of Object.entries(e.entities||{}))n?.device_id&&(t[n.device_id]=t[n.device_id]||[]).push(i);return t}(r),y=nh(s,r),b=new Set;for(const e of s){const[t,i]=e.binding.split(":");"device"!==t&&"entity"!==t||!i||b.add(e.binding)}const w=(e,t)=>s.find(i=>i.binding===e+":"+t),k={},x=[];for(const e of Object.values(n.devices)){const r=e.area_id;if(!r||!o[r])continue;if("service"===e.entry_type)continue;if(b.has("device:"+e.id))continue;const s=th(t,"device:"+e.id,i);if("active"!==s.kind)continue;const h=w("device",e.id);if(h&&h.hidden&&!a.filter_seeded)continue;const _=rh(n,e.id,f[e.id]||[],y);if(_.partial&&!_.entityIds.length)continue;const m=_.entityIds,v=_.partial?{kind:"active",enabledEntityIds:m,allEntityIds:m}:s,$=oh(n,e,m),S=!a.filter_seeded;if(S&&!c){if(l.has($))continue;if("Group"===e.model)continue;if(/scene/i.test(e.model||""))continue;if(/bridge/i.test((e.model||"")+(e.name||"")))continue;if("myheat"===$&&e.via_device_id)continue}const M=(e.name_by_user||e.name||d("device.unnamed")).trim(),T=M+"|"+r;let R=Fh(n,M,e.model,m,u);if(m.some(e=>e.startsWith("lock."))&&(R="mdi:lock"),S&&!c&&p&&"mdi:lightbulb"===R&&g.has(r))continue;k[T]=(k[T]||0)+1;const C=k[T]>1?M+" "+k[T]:M,D={id:e.id,name:C,model:e.model||"",area:r,space:o[r],icon:R,entities:m,allEntities:v.allEntityIds,bindingStatus:v,bindingKind:"device",bindingRef:e.id,pdfs:[]};D.primary=hh(n,m,R),"mdi:thermometer"!==R&&"mdi:air-filter"!==R||(D.temp=Dh(n,m)),D.primary&&Oh(n,D.primary)&&(D.hum=zh(n,m)),x.push(D)}for(const e of m)o[e.area]&&(b.has("entity:"+e.eid)||x.push({id:"lg_"+e.eid,name:e.name,model:d("device.light_group"),area:e.area,space:o[e.area],icon:"mdi:lightbulb-group",entities:[e.eid],allEntities:[e.eid],bindingStatus:{kind:"active",enabledEntityIds:[e.eid],allEntityIds:[e.eid]},primary:e.eid,bindingKind:"entity",bindingRef:e.eid,pdfs:[]}));for(const e of s){if(e.removed)continue;const[s,l]=e.binding.split(":"),c="device"===s||"entity"===s?th(t,e.binding,i):null;if(!e.hidden||a.filter_seeded||"ha_disabled"===c?.kind)if("device"===s){const t=c;if("unverified"===t.kind)continue;const s=r.devices[l],{area:a,space:p}=Bh(e,s?.area_id,o,h),m="active"===t.kind?t.enabledEntityIds:[];let g=s?Fh(n,s.name_by_user||s.name||"",s.model,m,u):"mdi:help-circle";m.some(e=>e.startsWith("lock."))&&(g="mdi:lock");const f={id:e.id,name:s?.name_by_user||s?.name||d("device.fallback"),model:s?.model||"",area:a,space:p,icon:g,entities:m,allEntities:t.allEntityIds.length?t.allEntityIds:s&&v[s.id]||[],bindingStatus:t,bindingKind:"device",bindingRef:l};f.primary=hh(n,m,g),"mdi:thermometer"!==g&&"mdi:air-filter"!==g||(f.temp=Dh(n,m)),f.primary&&Oh(n,f.primary)&&(f.hum=zh(n,m)),qh(f,e,r,_,i),x.push(f)}else if("entity"===s){if(jh(r,l,_))continue;const t=c;if("unverified"===t.kind)continue;const s=r.entities[l],a=s?.area_id||s?.device_id&&r.devices[s.device_id]?.area_id||"",{area:d,space:p}=Bh(e,a,o,h),m=n.states[l],g=s?.name||m?.attributes?.friendly_name||l;let f=Fh(n,g,"",[l],u);l.startsWith("lock.")&&(f="mdi:lock");const v={id:e.id,name:g,model:"",area:d,space:p,icon:f,entities:"active"===t.kind?[l]:[],allEntities:[l],bindingStatus:t,primary:"active"===t.kind?l:void 0,bindingKind:"entity",bindingRef:l};"mdi:thermometer"!==f&&"mdi:air-filter"!==f||!v.entities.length||(v.temp=Dh(n,v.entities)),v.entities.length&&Oh(n,l)&&(v.hum=zh(n,v.entities)),qh(v,e,r,_,i),x.push(v)}else{const t=e.area||"",n=e.space||t&&o[t]||h,s={id:e.id,name:e.name||d("device.virtual"),model:e.model||"",area:t,space:n,icon:e.icon||"mdi:map-marker",entities:[],allEntities:[],bindingStatus:{kind:"active",enabledEntityIds:[],allEntityIds:[]},bindingKind:"virtual",virtual:!0};qh(s,e,r,_,i),x.push(s)}}return x}function Uh(e){const{marker:t,siblingMarkers:i=[],...n}=e;return Wh({...n,markers:[...i.filter(e=>e.id!==t.id),t]}).find(e=>e.id===t.id)||null}function Gh(e,t,i,n){if(!t)return null;if(function(e,t,i){if(!t)return!1;const n=t.indexOf(":");if(n<1)return!1;const r=t.slice(0,n),o=t.slice(n+1),s=Lh(i);return"device"===r?s.devices.has(o):"entity"===r&&jh(e,o,s)}(e,t,n))return null;const r=t.indexOf(":");if(r<0)return null;const o=t.slice(0,r),s=t.slice(r+1);if(!s)return null;if("entity"===o){const t=parseFloat(e.states[s]?.state);return Number.isFinite(t)?"temp"===i?Math.round(10*t)/10:Math.round(t):null}if("device"===o){const t=Object.entries(e.entities).filter(([,e])=>e.device_id===s).map(([e])=>e);return"temp"===i?Dh(e,t):zh(e,t)}return null}const Vh=new RegExp(["water","voda","coolant","flow_?temp","return_?temp","target","setpoint","chip","cpu","processor","board","core_temp","device_temp","batter","akkum","freezer","fridge","oven","kettle","boiler"].join("|"),"i");function Kh(e,t,i){const n=[];for(const r of t){if(r.area!==i||r.virtual)continue;const t=Ch(e,r.entities);null!=t&&n.push(t)}return Mi(n)}function Yh(e,t,i){const n=[];for(const r of t){if(r.area!==i)continue;if("mdi:thermometer"!==r.icon&&"mdi:air-filter"!==r.icon)continue;const t=Dh(e,r.entities);null!=t&&n.push(t)}return n.length?Math.round(n.reduce((e,t)=>e+t,0)/n.length*10)/10:null}const Zh={offStates:["off"],unknownUsesToggle:!0},Xh={light:Zh,switch:Zh,fan:Zh,humidifier:Zh,input_boolean:Zh,automation:Zh,remote:Zh,group:Zh,climate:{...Zh,featureMasks:{turn_on:256,turn_off:128,toggle:384}},media_player:{...Zh,featureMasks:{turn_on:128,turn_off:256,toggle:384}},siren:{...Zh,featureMasks:{turn_on:1,turn_off:2,toggle:3}},vacuum:{...Zh,featureMasks:{turn_on:1,turn_off:2,toggle:3}},water_heater:{...Zh,featureMasks:{turn_on:8,turn_off:8,toggle:8}},camera:{...Zh,featureMasks:{turn_on:1,turn_off:1,toggle:1}}};function Jh(e){return e.slice(0,e.indexOf("."))}function Qh(e,t,i){if(!e?.services||"object"!=typeof e.services)return!1;const n=e.services?.[t];return!!n&&Object.prototype.hasOwnProperty.call(n,i)}function ed(e,t,i){return Qh(e,t,i)?{domain:t,service:i}:Qh(e,"homeassistant",i)?{domain:"homeassistant",service:i}:null}function td(e,t){return e?.entities?.[t]||null}function id(e,t,i){const n=e?.states?.[i],r=td(t,i);return n?.attributes?.friendly_name||r?.name||r?.original_name||i}function nd(e,t){const i=td(e,t);if(null!=i?.disabled_by)return!0;const n=i?.device_id?e?.devices?.[i.device_id]:null;return null!=n?.disabled_by}function rd(e,t){const i=e?.attributes?.supported_features;if(null==i||""===i)return!1;const n=Number(i);return Number.isFinite(n)&&(n&t)===t}function od(e,t,i){const n=Jh(i);if("lock"===n||"alarm_control_panel"===n)return!0;if("cover"!==n)return!1;const r=e?.states?.[i],o=td(t,i),s=String(r?.attributes?.device_class||o?.device_class||o?.original_device_class||"");return Fi.has(s)}function sd(e,t,i,n,r){return{ref:i,entityId:n,name:n?id(e,t,n):null,reason:r}}function ad(e,t,i,n,r,o=null){return{target:null,skipped:sd(e,t,i,n,r),semantics:o,nextEffect:null,command:null}}function ld(e,t,i,n,r=i){const o=Jh(i);if("cover"!==o&&"valve"!==o)return function(e,t,i,n,r=i){const o=Jh(i),s=e?.states?.[i];if(nd(t,i))return ad(e,t,r,i,"ha-disabled","power");if(od(e,t,i))return ad(e,t,r,i,"secure");if(!s)return ad(e,t,r,i,"missing","power");if("unavailable"===s.state)return ad(e,t,r,i,"unavailable","power");const a=Xh[o];if(!a)return ad(e,t,r,i,"unsupported");let l,c;if("unknown"===s.state||""===s.state){if(!a.unknownUsesToggle)return ad(e,t,r,i,"unsupported","power");l="toggle",c="toggle"}else a.offStates.includes(String(s.state))?(l="turn-on",c="turn_on"):(l="turn-off",c="turn_off");const h=a.featureMasks?.[c];if(h&&!rd(s,h))return ad(e,t,r,i,"unsupported","power");const d=ed(e,o,c);return d?{target:{entityId:i,name:id(e,t,i),state:String(s.state||""),via:n},skipped:null,semantics:"power",nextEffect:l,command:{...d,data:{entity_id:i}}}:ad(e,t,r,i,"unsupported","power")}(e,t,i,n,r);if(nd(t,i))return ad(e,t,r,i,"ha-disabled",o);if(od(e,t,i))return ad(e,t,r,i,"secure",o);const s=e?.states?.[i];if(!s)return ad(e,t,r,i,"missing",o);if("unavailable"===s.state)return ad(e,t,r,i,"unavailable",o);const a=function(e,t,i){const n=String(i?.state||""),r="closed"===n?{service:`open_${t}`,effect:"open",feature:1}:"open"===n?{service:`close_${t}`,effect:"close",feature:2}:"opening"===n||"closing"===n?{service:`stop_${t}`,effect:"stop",feature:8}:null;return r&&rd(i,r.feature)&&Qh(e,t,r.service)?r:rd(i,3)&&Qh(e,t,"toggle")?{service:"toggle",effect:"toggle"}:null}(e,o,s);if(!a)return ad(e,t,r,i,"unsupported",o);return{target:{entityId:i,name:id(e,t,i),state:String(s.state||""),via:n},skipped:null,semantics:o,nextEffect:a.effect,command:{domain:o,service:a.service,data:{entity_id:i}}}}function cd(e){return"entity"===e.bindingKind?e.bindingRef&&yn(e.bindingRef)?[e.bindingRef]:[]:"virtual"===e.bindingKind?[]:_h(e)}function hd(e){const t=e.marker?.toggle_entity;return t&&cd(e).includes(t)?t:null}function dd(e,t,i){const n=function(e,t){if("entity"===e.bindingKind&&e.bindingRef){const t=e.marker?.light_entity?mh(e):null;return[...new Set([t,e.bindingRef].filter(e=>!!e))]}const i=e.entities.length?e.entities:e.allEntities||[],n=e.marker?.light_entity||e.primary&&yn(e.primary)?mh(e):null;return[...new Set([n,...ch(t,i)].filter(e=>!!e))]}(i,t);if(!n.length)return null;const r="entity"===i.bindingKind?"binding":"device-role";if("binding"===r)return ld(e,t,n[0],r);let o=null,s=null;for(const i of n){const n=ld(e,t,i,r);if(n.command)return n;const a=n.skipped?.reason;if("missing"===a||"unavailable"===a||"secure"===a)return n;"ha-disabled"!==a?o||=n:s||=n}return o||s}function ud(e){const t=e.skipped?.reason;return"missing"===t?"unavailable":t||"unsupported"}function pd(e,t){return{origin:e,kind:"none",semantics:null,targets:[],skippedTargets:[],noneReason:t,nextEffect:null,command:null}}function _d(e,t){return"cover"===e||"toggle"===e?"toggle":"more-info"===e||"run"===e||"info"===e?e:(null==e||""===e)&&"light"===t?"toggle":"info"}function md(e,t){return{origin:e,kind:"single",semantics:t.semantics,targets:t.target?[t.target]:[],skippedTargets:t.skipped?[t.skipped]:[],noneReason:t.command?null:ud(t),nextEffect:t.nextEffect,command:t.command}}function gd(e,t,i,n=[]){const r=new Map,o=[],s=new Set,a=e=>{const t=`${e.ref}\n${e.entityId||""}\n${e.reason}`;s.has(t)||(s.add(t),o.push(e))};for(const e of n)a(e);for(const n of i){const i=ld(e,t,n.entityId,n.via,n.ref);i.target?r.set(n.entityId,r.get(n.entityId)||i.target):i.skipped&&a(i.skipped)}const l=[...r.values()];if(!l.length)return{origin:"explicit-toggle",kind:"group",semantics:"group-power",targets:l,skippedTargets:o,noneReason:o.length&&o.every(e=>"secure"===e.reason)?"secure":"configured-targets-missing",nextEffect:null,command:null};const c=l.some(e=>"on"===e.state)?"turn_off":"turn_on",h=ed(e,"homeassistant",c);return h?{origin:"explicit-toggle",kind:"group",semantics:"group-power",targets:l,skippedTargets:o,noneReason:null,nextEffect:"turn_on"===c?"turn-on":"turn-off",command:{...h,data:{entity_id:l.map(e=>e.entityId)}}}:{origin:"explicit-toggle",kind:"group",semantics:"group-power",targets:[],skippedTargets:[...o,...l.map(i=>sd(e,t,i.entityId,i.entityId,"unsupported"))],noneReason:"unsupported",nextEffect:null,command:null}}function fd(e){return String(e.marker?.id||e.id||"")}function vd(e){const{hass:t,device:i}=e,n=e.registryHass||t,r=function(e){return"toggle"===e.tapAction?"explicit-toggle":"cover"===e.tapAction?"legacy-cover":!e.tapAction&&e.primary?.startsWith("light.")?"default-light":null}(i);if(!r)return null;const o=i.marker;if("explicit-toggle"===r&&zc(o)){const t=String(o?.id||i.id||""),n=wh(e.devices).get(t);if(n)return function(e,t){const i=e.registryHass||e.hass;return gd(e.hass,i,t.driverEids.map(e=>({entityId:e,via:"control-marker-driver",ref:`marker:${t.markerId}`})))}(e,n);const s=Pc(o,e.virtualLights);return{origin:r,kind:"single",semantics:"power",targets:[{entityId:"",name:i.name,state:s?"on":"off",via:"virtual-light"}],skippedTargets:[],noneReason:null,nextEffect:s?"turn-off":"turn-on",command:null,operation:{kind:"virtual-light",markerId:o.id}}}if("explicit-toggle"===r){if(uh(i.marker?.binding,i.marker?.controls??i.controls,i.entities).length)return function(e,t=null){const{hass:i,device:n,devices:r}=e,o=e.registryHass||i,s=uh(n.marker?.binding,n.marker?.controls??n.controls,n.entities),a=e.lightSources||Mh(i,r,null,e.virtualLights),l=new Map;for(const e of a){if(!e.key.startsWith("marker:"))continue;const t=l.get(e.key)||[];t.push(e),l.set(e.key,t)}const c=wh(r),h=new Map;for(const e of r){const t=String(e.marker?.id||e.id||"");t&&h.set(t,e)}const d=[],u=[];t&&d.push({entityId:t,via:"entity"===n.bindingKind?"binding":"device-role",ref:t});for(const e of s){if(!e.startsWith("marker:")){yn(e)?d.push({entityId:e,via:"control-entity",ref:e}):u.push(sd(i,o,e,e.includes(".")?e:null,"unsupported"));continue}const t=l.get(e)||[];if(!t.length){const t=h.get(e.slice(7));if("ha_disabled"===t?.bindingStatus?.kind){const n=t.bindingStatus.allEntityIds[0]||null;u.push({ref:e,entityId:n,name:t.name||(n?id(i,o,n):null),reason:"ha-disabled"})}else u.push(sd(i,o,e,null,"missing"));continue}const r=[...new Set(t.flatMap(e=>e.serviceEids))];if(r.length){for(const t of r)d.push({entityId:t,via:"control-entity",ref:e});continue}const s=t.some(e=>e.passive),a=e.slice(7),p=fd(n),_=c.get(a)?.controllers.find(e=>fd(e.device)===p);if(s&&_?.driverEids.length)for(const t of _.driverEids)d.push({entityId:t,via:"control-marker-driver",ref:e});else u.push(sd(i,o,e,null,s?"missing":"unsupported"))}return gd(i,o,d,u)}(e,hd(i))}if("legacy-cover"===r){const e=i.entities.find(e=>e.startsWith("cover."))||i.allEntities?.find(e=>e.startsWith("cover."));return e?md(r,ld(t,n,e,"device-role")):pd(r,"no-actionable-entity")}const s=hd(i),a=s?ld(t,n,s,"entity"===i.bindingKind?"binding":"device-role"):dd(t,n,i);return a?md(r,a):pd(r,i.virtual||"virtual"===i.bindingKind?"no-actionable-entity":"no-binding")}function yd(e){if(!e)return[];const t=Array.isArray(e.data.entity_id)?e.data.entity_id:[e.data.entity_id];return[...new Set(t)].sort()}function bd(e){return e?e.operation?e.operation:e.command?{kind:"ha-service",command:e.command}:null:null}const wd=new Set(["missing","ha-disabled","unavailable"]);function kd(e,t){if(!bd(e)||!e.nextEffect||!e.targets.length)return[];const i="group"===e.kind,n=i?e.targets.filter(e=>"on"===e.state).length:0,r=i?0===n?t.groupAllOff():t.groupCurrent(n,e.targets.length):t.state(e.targets[0]);let o;return o="toggle"===e.nextEffect?t.expectedByHa():i&&"turn-on"===e.nextEffect?t.groupAllOn():i&&"turn-off"===e.nextEffect?t.groupAllOff():t.effect(e.nextEffect),[t.current(r),t.expected(o),...e.skippedTargets.length?[t.unavailable(e.skippedTargets.length)]:[]]}function xd(e,t){const i=bd(e),n=bd(t);return!(!i||!n||i.kind!==n.kind)&&("virtual-light"===i.kind&&"virtual-light"===n.kind?i.markerId===n.markerId:"ha-service"===i.kind&&"ha-service"===n.kind&&function(e,t){const i=yd(e),n=yd(t);return i.length===n.length&&i.every((e,t)=>e===n[t])}(i.command,n.command))}function $d(e,t){const i=e.targets[0]||e.skippedTargets[0],n=[];return"group"===e.kind?e.targets.length&&n.push(t.group(e.targets)):i&&n.push(t.single(i)),e.nextEffect&&e.targets.length&&n.push("group"===e.kind?t.groupCurrentNext(e.targets,e.nextEffect):t.currentNext(e.targets[0],e.nextEffect)),e.skippedTargets.length&&n.push(t.skipped(e.skippedTargets)),!e.command&&e.noneReason&&n.push(t.none(e.noneReason)),n}const Sd=e=>!!e&&e.length>=2&&Number.isFinite(e[0])&&Number.isFinite(e[1]);function Md(e,t=5,i=5){return vo(e.cm,t,i)/2}function Td(e,t,i=1,n=5,r=5,o=0){const s=e.host;if(!s||"partition"!==s.kind||"string"!=typeof s.id||!s.id)return{resolved:null,reason:"invalid-host"};const a=t.find(e=>e.id===s.id);if(!a)return{resolved:null,reason:"missing-partition"};if(!Sd(a.a)||!Sd(a.b)||!Number.isFinite(s.t)||s.t<0||s.t>1)return{resolved:null,reason:"invalid-position"};const l=a.b[0]-a.a[0],c=a.b[1]-a.a[1],h=Math.hypot(l,c),d=Number(e.length)*i;if(!(h>1e-9&&d>0&&Number.isFinite(d)))return{resolved:null,reason:"invalid-length"};const u=s.t*h;if(u-d/2h-o+1e-9)return{resolved:null,reason:"does-not-fit"};const p=l/h,_=c/h;let m=180*Math.atan2(c,l)/Math.PI;return m>=90?m-=180:m<-90&&(m+=180),{reason:null,resolved:{opening:e,host:s,partition:a,center:[a.a[0]+l*s.t,a.a[1]+c*s.t],angle:m,length:d,depth:vo(a.cm,n,r),t:s.t,axis:{a:[a.a[0],a.a[1]],b:[a.b[0],a.b[1]],ux:p,uy:_,length:h}}}}function Rd(e,t,i=1,n=5,r=5){return Td(e,t,i,n,r,0)}function Cd(e,t,i=1,n=5,r=5){const o=e.host,s="partition"===o?.kind?t.find(e=>e.id===o.id):void 0;if(!s)return Rd(e,t,i,n,r);const a=Td(e,t,i,n,r,Md(s,n,r));return"does-not-fit"===a.reason?{resolved:null,reason:"does-not-fit-jamb"}:a}function Dd(e,t){return!e||(e.length!==t.length||e.host?.kind!==t.host?.kind||e.host?.id!==t.host?.id||e.host?.t!==t.host?.t)}function Ad(e){const{center:t,length:i,axis:n,host:r}=e,o=i/2;return{hostId:r.id,a:[t[0]-n.ux*o,t[1]-n.uy*o],b:[t[0]+n.ux*o,t[1]+n.uy*o],depth:e.depth}}function Od(e,t=!1){const i=t?1:-1,n=e.depth/2;return{ox:-e.axis.uy*i*n,oy:e.axis.ux*i*n,cm:e.partition.cm,side:i}}function zd(e,t,i){return e.flatMap(e=>!Sd(e.a)||!Sd(e.b)||Math.hypot(e.b[0]-e.a[0],e.b[1]-e.a[1])<=1e-9?[]:[{roomId:"",a:[e.a[0],e.a[1]],b:[e.b[0],e.b[1]],key:`partition:${e.id}`,kind:"outer",cm:e.cm,open:!1,half:vo(e.cm,t,i)/2,partitionHost:{kind:"partition",id:e.id}}])}function Pd(e,t,i){const{center:n,length:r,axis:o}=e,s=r/2,a=t.flatMap(e=>{if(!e.kind||e.open)return[];const t=e.b[0]-e.a[0],r=e.b[1]-e.a[1],a=Math.hypot(t,r);if(!(a>i))return[];const l=t/a,c=r/a;if(Math.abs(o.ux*c-o.uy*l)>1e-6)return[];if(Math.abs((n[0]-e.a[0])*c-(n[1]-e.a[1])*l)>i)return[];const h=e=>(e[0]-n[0])*o.ux+(e[1]-n[1])*o.uy,d=h(e.a),u=h(e.b),p=Math.max(-s,Math.min(d,u)),_=Math.min(s,Math.max(d,u));return _>=p-i?[[p,_]]:[]});if(!a.length)return!1;a.sort((e,t)=>e[0]-t[0]||e[1]-t[1]);let l=-s;for(const[e,t]of a){if(e>l+i)return!1;if(l=Math.max(l,t),l>=s-i)return!0}return!1}function Fd(e,t,i=1e-9){const n=e.length/(2*e.axis.length),r=e.t-n,o=e.t+n;return t.some(t=>{if(t.host.id!==e.host.id||t.opening.id===e.opening.id)return!1;const n=t.length/(2*t.axis.length);return Math.max(r,t.t-n)[]);for(let i=0;i=1-1e-9||u<=1e-9||u>=1-1e-9||(t[i].push(d),t[s].push(u))}}const i=[];for(let n=0;ne-t),1];for(let e=1;e{const i=t[2]-t[0],n=t[3]-t[1],r=i*i+n*n;if(!(r>0))return Math.hypot(e[0]-t[0],e[1]-t[1]);const o=Math.max(0,Math.min(1,((e[0]-t[0])*i+(e[1]-t[1])*n)/r));return Math.hypot(e[0]-(t[0]+o*i),e[1]-(t[1]+o*n))},jd=(e,t,i,n)=>{const r=n[2]-n[0],o=n[3]-n[1],s=t*o-i*r;if(Math.abs(s)<1e-12)return 1/0;const a=n[0]-e[0],l=n[1]-e[1],c=(a*o-l*r)/s;if(!(c>1e-9))return 1/0;const h=(a*i-l*t)/s;return h<-1e-9||h>1+1e-9?1/0:c};function qd(e,t,i,n=96){if(!(t>0&&Number.isFinite(e[0])&&Number.isFinite(e[1])))return[];const r=[];for(const n of i){if(!n||n.length<4)continue;if(![n[0],n[1],n[2],n[3]].every(Number.isFinite))continue;const i=Ld(e,n);if(i<1e-7)return[];i>t||r.push(n)}const o=[],s=Math.max(12,Math.round(n));for(let e=0;ee-t);const l=[];let c=Number.NEGATIVE_INFINITY;for(const i of o){if(i-c<1e-9)continue;c=i;const n=Math.cos(i),o=Math.sin(i);let s=t;for(const t of r){const i=jd(e,n,o,t);i=3?l:[]}const Bd=[o` + :host { + --hp-bg: var(--card-background-color, #16212e); + --hp-line: var(--divider-color, #2b3d4f); + --hp-txt: var(--primary-text-color, #e6edf3); + --hp-muted: var(--secondary-text-color, #8aa0b3); + --hp-accent: var(--primary-color, #3ea6ff); + --hp-on: #ffd45c; + --hp-open: #ff9f43; + /* design tokens (UI chrome only). The icon/plan scale math stays on + --icon-size/--dev-size cqw units and never uses these. */ + /* spacing scale, fact-based; stray 3/5/7/9/13/14px values are unified + onto the nearest step (max +-2px, the whole point of the pass) */ + --sp-1: 2px; + --sp-2: 4px; + --sp-3: 6px; + --sp-4: 8px; + --sp-5: 12px; + --sp-6: 16px; + /* px radii of dialogs/buttons/plates (the 22% badge radius is scale math) */ + --rad-s: 6px; + --rad-m: 8px; + --rad-l: 12px; + /* font tiers: fine print+labels / body+buttons / title */ + --fs-s: 12px; + --fs-m: 13px; + --fs-l: 15px; + /* elevation: badge / floating panel (menu, tip, toast) / dialog */ + --shadow-1: 0 1px 3px rgba(0, 0, 0, 0.45); + --shadow-2: 0 6px 22px rgba(0, 0, 0, 0.45); + --shadow-3: 0 8px 30px rgba(0, 0, 0, 0.5); + } + ha-card { + overflow: visible; /* overflow:hidden breaks position:sticky on the header */ + } + .empty { + padding: 40px 24px; + color: var(--hp-txt); + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + gap: var(--sp-4); + } + .empty .big { + --mdc-icon-size: 56px; + color: var(--hp-accent); + opacity: 0.7; + } + .empty .muted { + color: var(--hp-muted); + font-size: var(--fs-m); + margin: 0; + } + .empty .btn { + margin-top: var(--sp-4); + } + @keyframes fixedfloor-spin { + to { transform: rotate(360deg); } + } + .spacer { + flex: 1; + } + .bootveil { + position: absolute; + inset: 0; + z-index: 40; + display: flex; + align-items: center; + justify-content: center; + background: var(--hp-bg, #16212e); + opacity: 1; + transition: opacity 0.15s ease; + pointer-events: none; + } + .bootveil.off { + opacity: 0; + } + .bootveil .boothouse { + position: static; /* the main .zoomwrap SVG is pinned to inset:0 — not this icon */ + width: 56px; + height: 56px; + fill: var(--hp-accent); + opacity: 0.85; + animation: hp-boot-pulse 1.3s ease-in-out infinite; + } + @keyframes hp-boot-pulse { + 0%, 100% { opacity: 0.3; transform: scale(0.94); } + 50% { opacity: 0.9; transform: scale(1); } + } + @media (prefers-reduced-motion: reduce) { + .bootveil .boothouse { + animation: none; + opacity: 0.7; + } + } + @keyframes hp-pulse-short { + 0% { transform: scale(1); opacity: 0.55; } + 70% { opacity: 0.22; } + 100% { transform: scale(var(--ripple-scale, 1.5)); opacity: 0; } + } + /* Alternate identity: a rapid retrigger restarts the browser timeline. */ + @keyframes hp-pulse-short-b { + 0% { transform: scale(1); opacity: 0.55; } + 70% { opacity: 0.22; } + 100% { transform: scale(var(--ripple-scale, 1.5)); opacity: 0; } + } + @keyframes hp-pulse-continuous { + 0% { transform: scale(1); opacity: 0.55; } + 65% { opacity: 0.18; } + 100% { transform: scale(var(--ripple-scale, 1.5)); opacity: 0; } + } + @keyframes hp-pulse-alarm { + 0% { transform: scale(1); opacity: 0.72; } + 100% { transform: scale(1.5); opacity: 0; } + } + /* Space changes keep their direction, but travel only a few percent: the + motion explains what changed without making the whole plan fly around. */ + @keyframes hp-slide-left { + 0% { transform: translateX(4%); opacity: 0.72; } + 100% { transform: translateX(0); opacity: 1; } + } + @keyframes hp-slide-right { + 0% { transform: translateX(-4%); opacity: 0.72; } + 100% { transform: translateX(0); opacity: 1; } + } + @keyframes hp-sunfade-in { + from { opacity: 0; } + to { opacity: 1; } + } + @keyframes hp-sunfade-out { + from { opacity: 1; } + to { opacity: 0; } + } + .sr-only { + position: absolute !important; + width: 1px !important; + height: 1px !important; + padding: 0 !important; + margin: -1px !important; + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + white-space: nowrap !important; + border: 0 !important; + } + .toast { + position: fixed; + pointer-events: none; + left: 50%; + bottom: 22px; + transform: translateX(-50%); + background: var(--hp-bg); + border: 1px solid var(--hp-accent); + color: var(--hp-txt); + padding: var(--sp-4) var(--sp-6); + border-radius: var(--rad-l); + font-size: var(--fs-m); + box-shadow: var(--shadow-2); + z-index: 120; + max-width: 90vw; + } +`,o` + .fixedfloor-loading { + animation: fixedfloor-spin 1.1s linear infinite; + } + .fixedfloor-error p { + max-width: 42rem; + margin: 0; + overflow-wrap: anywhere; + } + @media (prefers-reduced-motion: reduce) { + .fixedfloor-loading { animation: none; } + } + .stage.noplan { + background: #ffffff; + } + /* Editors always keep the white drawing sheet — even when a backdrop + image is loaded. Without this, a picture plan painted the theme card + colour under the grid (owner 2026-08-05). View keeps the historical + theme / .noplan split. */ + .stage.mode-plan, + .stage.mode-devices, + .stage.mode-decor { + background: #ffffff; + } + .stage { + position: relative; + width: 100%; + container-type: inline-size; + overflow: hidden; + touch-action: none; /* custom pinch/pan gestures */ + background: var(--ha-card-background, var(--card-background-color, #111)); + } + .zoomwrap { + position: absolute; + inset: 0; + z-index: 1; + } + .stage.mode-transition, + .stage.mode-transition .zoomwrap { + transition: none !important; + } + .stage.mode-transition .hp-view-only-layer, + .stage.mode-transition .hp-editor-only-layer { + pointer-events: none; + will-change: opacity; + } + .stage.mode-transition .hp-paper { + fill: var(--hp-mode-paper) !important; + } + /* Four-phase environment (#146): constant layers cross-fade because CSS + gradients themselves are not reliably interpolated. Everything stays + behind the plan; the plan tree is never dimmed, tinted or faded. */ + .hp-day-cycle-env { + position: absolute; + inset: 0; + z-index: 0; + overflow: hidden; + pointer-events: none; + isolation: isolate; + } + .hp-day-cycle-bg { + position: absolute; + inset: 0; + opacity: 0; + overflow: hidden; + transition: opacity 1100ms cubic-bezier(.22, .61, .36, 1); + } + .hp-day-cycle-bg.active { opacity: 1; } + .hp-day-cycle-sun { + position: absolute; + left: var(--hp-day-cycle-sun-x); + top: var(--hp-day-cycle-sun-y); + width: clamp(140px, 25cqw, 250px); + height: clamp(140px, 25cqw, 250px); + transform: translate(-50%, -50%); + border-radius: 50%; + filter: blur(10px); + opacity: var(--hp-day-cycle-sun-opacity); + transition: + left 1100ms cubic-bezier(.22, .61, .36, 1), + top 1100ms cubic-bezier(.22, .61, .36, 1), + opacity 1100ms cubic-bezier(.22, .61, .36, 1); + } + .stage.mode-transition .hp-day-cycle-env { transition: none; } + .stage.daycycle .hp-paperg, + .hp-static-stage.daycycle .hp-paperg { + filter: + drop-shadow(0 0 1px var(--hp-day-cycle-outline-near)) + drop-shadow(0 0 5px var(--hp-day-cycle-outline-mid)) + drop-shadow(0 0 10px var(--hp-day-cycle-outline-far)); + transition: filter 1100ms cubic-bezier(.22, .61, .36, 1); + } + @media (prefers-reduced-motion: reduce) { + .hp-day-cycle-bg, + .hp-day-cycle-sun, + .stage.daycycle .hp-paperg, + .hp-static-stage.daycycle .hp-paperg { + transition: none; + } + } + .sunlayer { + pointer-events: none; + } + .compass { + width: 120px; + height: 120px; + flex: none; + touch-action: none; + cursor: grab; + user-select: none; + } + .compass:active { + cursor: grabbing; + } + .compass .cring { + fill: rgba(255, 255, 255, 0.04); + stroke: var(--divider-color, #444); + stroke-width: 2; + } + .compass .ctick { + stroke: var(--secondary-text-color, #9aa4ad); + stroke-width: 2; + } + .compass .ctick.minor { + stroke-width: 1; + opacity: 0.6; + } + .compass .cneedle line { + stroke: var(--primary-color, #3ea6ff); + stroke-width: 2.5; + stroke-linecap: round; + } + .compass .cneedle path { + fill: var(--primary-color, #3ea6ff); + } + .compass .cneedle text { + fill: var(--text-primary-color, #fff); + font-size: 11px; + font-weight: 700; + } + .compass .cdeg { + fill: var(--secondary-text-color, #9aa4ad); + font-size: 13px; + } + .compass.unset .cneedle { + opacity: 0.35; + } + /* HP-1552: first-open boot veil — the plan hides until the stage height settles */ + .stage.hpboot .zoomwrap, + .stage.hpboot .zoombadge { + visibility: hidden; + } + /* AUD-1552-02: post-veil grace — HA chrome landing after the cap moves + the stage height smoothly; the viewport ResizeObserver refits the plan + along the transition, so a late panel glides instead of jumping. */ + .stage.hpsettle { + transition: height 0.25s ease; + } + @media (prefers-reduced-motion: reduce) { + .stage.hpsettle { + transition: none; + } + } + .zoomctl { + display: inline-flex; + gap: var(--sp-1); + background: rgba(127, 127, 127, 0.12); + border-radius: var(--rad-m); + padding: var(--sp-1); + } + .zoomctl .zb { + border: none; + padding: var(--sp-3) var(--sp-4); + } + .zoomctl .zb[disabled] { + opacity: 0.4; + pointer-events: none; + } + .projection-toggle { + min-width: 44px; + min-height: 44px; + justify-content: center; + padding: var(--sp-3); + } + /* docs/CANVAS.md §5: the plane has no edges, so you can pan until nothing + is on screen. One pointer home, one click back. */ + .homearrow { + position: absolute; + z-index: 12; + transform: translate(-50%, -50%); + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + background: var(--card-background-color, var(--hp-bg)); + color: var(--hp-accent); + border: 1px solid var(--hp-accent); + opacity: 0.9; + padding: 0; + } + .homearrow ha-icon { + --mdc-icon-size: 22px; + line-height: 1; + } + .zoombadge { + position: absolute; + z-index: 12; + left: var(--sp-4); + bottom: var(--sp-4); + background: var(--card-background-color, var(--hp-bg)); + opacity: 0.92; + color: var(--hp-txt); + border: 1px solid var(--hp-accent); + border-radius: var(--rad-m); + padding: var(--sp-1) var(--sp-4); + font-size: var(--fs-s); + font-weight: 600; + pointer-events: none; + } + /* Only the plan canvas owns the whole zoom wrapper. Context trays now live + inside .stage as well, and may contain small SVG previews (furniture in + particular); the old descendant-wide stage-SVG selector stretched every + preview over the tray and stacked them into one blocking artefact. The + vacuum trail/fit overlays have their own explicit geometry below. */ + .zoomwrap > svg { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + display: block; + } + .plan-svg { z-index: 1; } + .iso-underlay-svg { z-index: 0; overflow: visible; } + .iso-shadows-svg { z-index: 3; overflow: visible; } + .iso-walls-svg { + z-index: 4; + overflow: visible; + } + .iso-underlay-svg, + .iso-shadows-svg, + .iso-walls-svg, + .iso-underlay, + .iso-shadows, + .iso-walls, + .iso-openings { + pointer-events: none; + } + .iso-side-hi { stop-color: #b9bdbe; } + .iso-side-lo { stop-color: #969c9f; } + .iso-top-hi { stop-color: #fafaf7; } + .iso-top-lo { stop-color: #e2e4e2; } + .iso-wall-side { + fill: url(#hp-iso-wall-side) #a8acae; + stroke: #92989b; + stroke-width: 0.7; + vector-effect: non-scaling-stroke; + } + .iso-wall-top { + fill: url(#hp-iso-wall-top) #f3f3f1; + stroke: #d7d9d8; + stroke-width: 0.8; + vector-effect: non-scaling-stroke; + } + .iso-floor-side { + fill: #858b8d; + stroke: #71787b; + stroke-width: 0.7; + vector-effect: non-scaling-stroke; + } + .iso-opening-panel { + fill: #d7d9d7; + fill-opacity: 0.96; + stroke: #7f878b; + stroke-width: 0.9; + vector-effect: non-scaling-stroke; + } + .iso-opening-panel.iso-window { + fill: #dfeff4; + fill-opacity: 0.72; + stroke: #8aa7b1; + } + .iso-ambient-shadow { + fill: rgba(15, 21, 25, 0.22); + filter: url(#hp-iso-ambient-shadow); + } + .iso-contact-shadow { + fill: none; + stroke: rgba(22, 28, 31, 0.25); + stroke-width: 3; + filter: url(#hp-iso-contact-shadow); + vector-effect: non-scaling-stroke; + } + .iso-leaf-shadow { + fill: none; + stroke: rgba(18, 23, 27, 0.24); + stroke-width: 4; + filter: url(#hp-iso-leaf-shadow); + vector-effect: non-scaling-stroke; + } + @media (prefers-color-scheme: dark) { + .iso-side-hi { stop-color: #4c555a; } + .iso-side-lo { stop-color: #343c40; } + .iso-top-hi { stop-color: #687176; } + .iso-top-lo { stop-color: #50585d; } + .iso-wall-side { stroke: #30373b; } + .iso-wall-top { stroke: #7b858a; } + .iso-floor-side { fill: #2d3438; stroke: #20272a; } + .iso-opening-panel { fill: #626b70; stroke: #899399; } + .iso-opening-panel.iso-window { fill: #75919b; stroke: #abc6ce; } + .iso-ambient-shadow { fill: rgba(0, 0, 0, 0.34); } + .iso-contact-shadow, .iso-leaf-shadow { stroke: rgba(0, 0, 0, 0.38); } + } + @media (forced-colors: active) { + .iso-wall-side, .iso-wall-top, .iso-floor-side, .iso-opening-panel { + fill: Canvas; + stroke: CanvasText; + forced-color-adjust: auto; + } + .iso-ambient-shadow, .iso-contact-shadow, .iso-leaf-shadow { display: none; } + } + @supports not (filter: blur(1px)) { + .iso-ambient-shadow, .iso-contact-shadow, .iso-leaf-shadow { display: none; } + } + /* Opaque plan paper (owner 2026-08-03): the scene bg_color / daynight sky + shows ONLY around the plan, never through it. The colour is the + pre-bg_color canvas — the theme card background under an image plan, + plain white for a hand-drawn one (.stage.noplan). On drawn plans the + paper is per-room shapes following the room contours — fill only, + never a stroke, so the paper cannot poke past a wall. Night dimming + comes from the .zoomwrap brightness filter, never from alpha + (docs/SUN.md). */ + .hp-paper { + fill: var(--ha-card-background, var(--card-background-color, #111)); + stroke: none; + } + .stage.noplan .hp-paper, + .stage.mode-plan .hp-paper, + .stage.mode-devices .hp-paper, + .stage.mode-decor .hp-paper { + fill: #ffffff; + } + /* Owner 2026-08-04: «углы границ комнат всё ещё с зубцами». A miter join + on a 30-45° corner shoots a spike far past the wall (and flips to an + ugly bevel once past the miter limit) — the same defect the decor lines + had before they got round caps. Every room border, in EVERY renderer + that reuses these styles (plan view, plan editor, static space-card), + joins its walls with a ROUND join instead: the corner reads as the + stroke's own radius, never as a tooth. The linecap matters only for the + open outlines below, but it costs nothing to state it here. */ + .room { + transition: 0.12s; + cursor: default; /* v1.40.1: rooms are not clickable — the label's link icon is */ + stroke-linejoin: round; + stroke-linecap: round; + } + .room.overlay { + fill: transparent; + stroke: transparent; + stroke-width: calc(2px * var(--hp-cell-visual-scale, 1)); + } + :host([data-pointer-hover]) .stage.mode-view .room.overlay:not(.styled):hover { + stroke: var(--hp-accent); + stroke-opacity: 1; + } + .room.yard { + fill: rgba(75, 140, 90, 0.14); + stroke: #4b8c5a; + stroke-width: calc(2px * var(--hp-cell-visual-scale, 1)); + } + :host([data-pointer-hover]) .stage.mode-view .room.yard:not(.styled):hover { + stroke: var(--hp-accent); + stroke-opacity: 1; + } + .room.styled { + stroke: var(--room-stroke, transparent); + stroke-opacity: var(--room-stroke-op, 0); + stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); + fill: var(--room-fill, transparent); + fill-opacity: var(--room-fill-op, 0); + } + .glow-base-layer, + .glow-base-tunnels, + .glow-pools-frame, + .glow-pools, + .glow-spot { + pointer-events: none; + } + /* The parent isolates all source spots from the room data fill, Glow base, + paper and backdrop. A spot is one circle clipped to the floor its lamp + can see, so the whole spot screen-blends as a single primitive — there is + no mask left to be dropped on a promoted layer. Per-stop alpha already + contains the shared 0.7 ceiling; the spot opacity below only animates + between 0 and 1 and is never another persistent alpha ceiling. */ + .glow-pools-frame, + .glow-pools, + .glow-spot { + isolation: isolate; + } + .glow-pools.blend-screen .glow-spot { + mix-blend-mode: screen; + } + .glow-spot { + opacity: 1; + transition: opacity 500ms ease; + } + .glow-spot.is-entering, + .glow-spot.is-leaving { + opacity: 0; + } + @media (prefers-reduced-motion: reduce) { + .glow-spot { + transition: none; + } + } + /* The explicit late room-hover layer owns the wash and halo. Keeping CSS + filters off room paths prevents Chromium from recompositing the sibling + screen-blended Glow layer for one bright frame on every hover. */ + :host([data-pointer-hover]) .stage.mode-view .room.styled:hover { + stroke: var(--hp-accent); + stroke-opacity: 1; + } + /* doors, windows & gates */ + .op-leaf { + transition: transform 0.6s ease; + } + .op-arc { + stroke-width: calc(1.5px * var(--hp-cell-visual-scale, 1)); + transition: stroke-dashoffset 0.6s ease; + } + /* hover affordance: a rounded outline hugging the wall strip + a grab cursor */ + .op-outline { + fill: none; + stroke: var(--hp-accent); + stroke-width: calc(1.5px * var(--hp-cell-visual-scale, 1)); + opacity: 0; + transition: opacity 0.15s; + pointer-events: none; + } + :host([data-pointer-hover]) .stage.markup g.opening:hover .op-outline { + opacity: 0.9; + } + /* openings are pure status graphics outside Plan mode: no cursor, no hover, + no hit target — View must not interact with them at all */ + .op-hit { + fill: transparent; + pointer-events: none; + cursor: default; + } + .stage.markup .op-hit { + pointer-events: auto; + cursor: grab; + touch-action: none; /* drags, not scrolls, on touch */ + } + .stage.markup .op-hit:active { + cursor: grabbing; + } + .stage.markup .opening.orphan { + pointer-events: auto; + cursor: pointer; + color: var(--error-color, #db4437); + } + .stage.markup .opening.orphan circle { + fill: var(--hp-bg); + stroke: currentColor; + stroke-width: calc(2px * var(--hp-cell-visual-scale, 1)); + } + .stage.markup .opening.orphan text { + fill: currentColor; + font-weight: 800; + font-size: calc(12px * var(--hp-cell-visual-scale, 1)); + pointer-events: none; + } + /* HP-1550-04: in the resize tool the wall handles own the hit test — the + transparent .op-hit of a door at the midpoint of a wall used to sit ON + TOP of the handle and made that wall ungrabbable for both rooms. + Openings are not editable in this tool (they ride along with the wall), + so their hit area goes fully inert; every other Plan tool is untouched. */ + .stage.markup.tool-resize .op-hit { + pointer-events: none; + cursor: default; + } + .stage.mode-view .oplock { + pointer-events: auto; + cursor: pointer; + } + @media (prefers-reduced-motion: reduce) { + .op-leaf, .op-arc { transition: none; } + } + .roomlabel { + pointer-events: none; /* draggable only in plan mode (rule below) */ + position: absolute; + transform: translate(-50%, -50%); + font-size: calc(var(--rl-icon-size, var(--icon-size, 2.5cqw)) * 0.5 * var(--rl-scale, 1) * var(--rl-font, 1) * var(--rl-space, 1)); + font-weight: 700; + letter-spacing: 0.04em; + white-space: nowrap; + cursor: grab; + user-select: none; + z-index: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.15em; + text-align: center; + } + .rlname { + display: inline-flex; + align-items: center; + gap: 0.25em; + font-size: calc(1em * var(--rl-name, 1)); + } + .zoomwrap.slide-left { animation: hp-slide-left 0.18s cubic-bezier(0.2, 0.7, 0.2, 1); } + .zoomwrap.slide-right { animation: hp-slide-right 0.18s cubic-bezier(0.2, 0.7, 0.2, 1); } + @media (prefers-reduced-motion: reduce) { + .zoomwrap.slide-left, + .zoomwrap.slide-right { animation: none; } + } + /* The name is the anchor: the label box is centred on the room point, so + anything that takes part in its layout SHIFTS THE NAME. The gear button + and the metrics hang below as absolutes — the name renders in exactly + the same place in view mode and in the plan editor (owner's request), + and the button sits at the very bottom of the card. */ + /* Standalone, centred on the room, sized from the device icon: icon-size + already rescales with the view, so the button zooms WITH the plan + instead of keeping a constant screen size (owner's spec). */ + .rlgearbtn { + --gear-h: calc(var(--icon-size, 2.5cqw) * 0.77); /* owner: half the previous size */ + position: absolute; + /* dead-centred on the room, both axes (owner's spec) */ + transform: translate(-50%, -50%); + display: inline-flex; + align-items: center; + gap: 0.35em; + height: var(--gear-h); + padding: 0 calc(var(--gear-h) * 0.38); + border: 0; + border-radius: 999px; + background: var(--hp-accent); + color: var(--text-primary-color, #fff); + font: inherit; + font-size: calc(var(--gear-h) * 0.42); + font-weight: 600; + line-height: 1; + white-space: nowrap; + cursor: pointer; + pointer-events: auto; + opacity: 0.92; + box-shadow: var(--shadow-1); + z-index: 2; + } + .rlgearbtn { transition: opacity 0.15s, filter 0.15s; } + :host([data-pointer-hover]) .rlgearbtn:hover { opacity: 1; filter: brightness(1.18); } + .rlgearbtn ha-icon { --mdc-icon-size: calc(var(--gear-h) * 0.55); display: inline-flex; } + .rlgear { + --mdc-icon-size: 0.9em; + display: inline-flex; + margin-right: 0.2em; + opacity: 0.6; + cursor: pointer; + pointer-events: auto; + } + :host([data-pointer-hover]) .rlgear:hover { opacity: 1; } + .rlgo { + --mdc-icon-size: 0.85em; + display: inline-flex; + opacity: 0.55; + } + .stage.mode-view .rlgo { + pointer-events: auto; + cursor: pointer; + } + :host([data-pointer-hover]) .stage.mode-view .rlgo:hover { opacity: 1; } + .roomlabel .rlmetrics { + position: absolute; /* below the name, outside the centring math */ + top: calc(100% + 0.15em); + left: 50%; + transform: translateX(-50%); + white-space: nowrap; + display: flex; + align-items: center; + gap: 0.55em; + font-size: calc(0.75em * var(--rl-meta, 1)); /* feedback: 0.62 was unreadable on a tablet */ + font-weight: 600; + letter-spacing: 0.02em; + opacity: 0.9; + } + .roomlabel .rlm { + display: inline-flex; + align-items: center; + gap: 0.12em; + } + .roomlabel .rlm ha-icon { + --mdc-icon-size: 1.05em; + display: inline-flex; + } + .roomlabel .rlm.lit { opacity: 1; } + .rlhandle { + display: none; + position: absolute; + width: 9px; + height: 9px; + border-radius: 2px; + background: var(--hp-accent); + border: 1px solid var(--card-background-color, #fff); + z-index: 2; + } + .rlhandle.tl { left: -6px; top: -6px; cursor: nwse-resize; } + .rlhandle.br { right: -6px; bottom: -6px; cursor: nwse-resize; } + .rlhandle.tr { right: -6px; top: -6px; cursor: nesw-resize; } + .rlhandle.bl { left: -6px; bottom: -6px; cursor: nesw-resize; } + :host([data-pointer-hover]) .stage.markup .roomlabel:hover .rlhandle { display: block; } + .stage.markup .roomlabel { pointer-events: auto; } + .roomlabel:active { cursor: grabbing; } + .measurelayer { + position: absolute; + inset: 0; + pointer-events: none; + } + .alignline { + stroke: var(--hp-accent); + stroke-width: calc(1.2px * var(--hp-cell-visual-scale, 1)); + stroke-dasharray: + calc(4px * var(--hp-cell-visual-scale, 1)) + calc(4px * var(--hp-cell-visual-scale, 1)); + pointer-events: none; + opacity: 0.9; + } + .aligndot { + fill: var(--hp-accent); + pointer-events: none; + } + .measurelabel.on45 { + color: #4bd28f; + border-color: #4bd28f; + } + .measurelabel { + position: absolute; + transform: translate(12px, -150%); + font-size: var(--fs-s); + font-weight: 600; + padding: 1px var(--sp-3); + border-radius: var(--rad-s); + background: rgba(0, 0, 0, 0.72); + color: #fff; + white-space: nowrap; + pointer-events: none; + user-select: none; + z-index: 3; + } + .measurelabel.opdimension { + transform: translate( + calc(-50% + var(--op-label-shift-x, 0px)), + calc(-50% + var(--op-label-shift-y, -12px)) + ); + border: 1px solid var(--hp-open, #ff9800); + } + /* decor (background) layer */ + .decorlayer .dshape { pointer-events: none; } + .stage.mode-decor .decorlayer .dshape { + pointer-events: visiblePainted; + cursor: pointer; + } + .stage.mode-decor.dtool-select .decorlayer .dshape { cursor: move; } + /* …but a DRAWING tool owns the canvas: existing shapes stop being targets + entirely, so a new line can start exactly on the end of an old one + instead of grabbing it (owner, 2026-08-04). Same for the picture tool — + a shape lying over the plan must not block the picture's own drag. + Only select (move) and erase (delete) keep the shapes clickable. */ + .stage.mode-decor.dtool-line .decorlayer .dshape, + .stage.mode-decor.dtool-rect .decorlayer .dshape, + .stage.mode-decor.dtool-ellipse .decorlayer .dshape, + .stage.mode-decor.dtool-text .decorlayer .dshape, + .stage.mode-decor.dtool-furniture .decorlayer .dshape, + .stage.mode-decor.dtool-backdrop .decorlayer .dshape { pointer-events: none; } + /* the furniture tool is a stamp: the press must reach the stage even when + it lands on a sofa that is already there (docs/FURNITURE.md §4) */ + .stage.mode-decor.dtool-furniture { cursor: copy; } + /* ONE exception (owner, 2026-08-04): under the TEXT tool an existing LABEL + is a target again — pressing it opens its editor instead of starting a + new label on top of the old one. Only labels: a line or a rectangle + under the text tool stays inert, so the press reaches the stage and a + new label is created there. */ + .stage.mode-decor.dtool-text .decorlayer .dshape.dtext, + .stage.mode-decor.dtool-select .decorlayer .dshape.dtext, + .stage.mode-decor.dtool-erase .decorlayer .dshape.dtext { + /* A label is one logical decor object. SVG's visiblePainted would hit + only the ink of individual glyphs, so spaces, counters and the area + inside its selection glow behaved like empty canvas. */ + pointer-events: visiblePainted; + pointer-events: bounding-box; + } + .stage.mode-decor.dtool-text .decorlayer .dshape.dtext { + cursor: text; + } + /* Erasing a hairline must not require pixel-perfect aim. The duplicate + geometry is invisible and exists only while Erase is active. A + non-scaling stroke keeps the target comfortably wide at every zoom. */ + .decorlayer .derasehit { + fill: none; + stroke: transparent; + stroke-width: 16px; + stroke-linecap: round; + stroke-linejoin: round; + vector-effect: non-scaling-stroke; + pointer-events: none; + } + .stage.mode-decor.dtool-erase .decorlayer .derasehit { + pointer-events: stroke; + } + /* A dashed line must remain selectable across its gaps. This proxy also + makes the only entry to line-style properties — double click in Select — + practical for hairlines at every zoom. */ + .decorlayer .dselecthit { + fill: none; + stroke: transparent; + stroke-width: 16px; + stroke-linecap: round; + vector-effect: non-scaling-stroke; + pointer-events: none; + } + .stage.mode-decor.dtool-select .decorlayer .dselecthit { + pointer-events: stroke; + cursor: move; + } + .decorlayer .dsel { + filter: drop-shadow(0 0 3px var(--hp-accent)); + } + .decorlayer .ddraft { + stroke-dasharray: + calc(6px * var(--hp-cell-visual-scale, 1)) + calc(5px * var(--hp-cell-visual-scale, 1)); + pointer-events: none; + } + .decorlayer text { + font-weight: 600; + user-select: none; + dominant-baseline: middle; + text-anchor: middle; + } + .stage.mode-decor { + outline: 2px solid #26a69a; + outline-offset: -2px; + } + /* backdrop transform frame (docs/BACKDROP.md §2). Editor chrome: the + outline never takes a pointer, the four corner handles do — and they are + finger-sized (r = 2 % of the visible view), because this is dragged on a + tablet as often as with a mouse. */ + .bdframe .bdbox { + fill: none; + stroke: var(--hp-accent); + stroke-width: 2; + stroke-dasharray: 10 7; + vector-effect: non-scaling-stroke; + pointer-events: none; + opacity: 0.9; + } + /* the HIT circle: invisible, finger-sized, the only thing that takes a + pointer. The visible bead is .bdknob, a quarter of its radius — the same + visual/hit split .dthandle + .dtknob and .rszhandle + .rszicon use. */ + .bdframe .bdhandle { + fill: transparent; + stroke: none; + pointer-events: all; + touch-action: none; + } + .bdframe .bdknob { + fill: var(--hp-accent); + stroke: #fff; + stroke-width: 1; + vector-effect: non-scaling-stroke; + pointer-events: none; + } + :host([data-pointer-hover]) .bdframe .bdhandle:hover + .bdknob { + fill: #fff; + stroke: var(--hp-accent); + } + .bdframe .bd-nwse { cursor: nwse-resize; } + .bdframe .bd-nesw { cursor: nesw-resize; } + /* the picture itself is the drag target for a move (grab, then grabbing) */ + .stage.mode-decor.bdgrab { cursor: grab; } + .stage.mode-decor.bdgrabbing, + .stage.mode-decor.bdgrabbing .bdframe .bdhandle { cursor: grabbing; } + .measurelabel.bdmeasure { + transform: translate(-50%, -50%); + border: 1px solid var(--hp-accent); + } + /* the selected decor object's frame — same chrome rules as the backdrop's: + the outline never takes a pointer, the handles always do, and they are + finger-sized because this is dragged on a tablet too */ + .dtframe .dtbox { + fill: none; + stroke: var(--hp-accent); + stroke-width: 1.5; + stroke-dasharray: 7 5; + vector-effect: non-scaling-stroke; + pointer-events: none; + opacity: 0.85; + } + .dtframe .dtstem, + .bdframe .dtstem { + stroke: var(--hp-accent); + stroke-width: 1.5; + vector-effect: non-scaling-stroke; + pointer-events: none; + opacity: 0.85; + } + /* the HIT circle: invisible, finger-sized, the only thing that takes a + pointer. The visible bead is .dtknob, a quarter of its radius — the + same visual/hit split .rszhandle + .rszicon use. */ + .dtframe .dthandle { + fill: transparent; + stroke: none; + pointer-events: all; + touch-action: none; + } + .dtframe .dtknob { + fill: var(--hp-accent); + stroke: #fff; + stroke-width: 1; + vector-effect: non-scaling-stroke; + pointer-events: none; + } + :host([data-pointer-hover]) .dtframe .dthandle:hover + .dtknob { + fill: #fff; + stroke: var(--hp-accent); + } + .dtframe .dt-nwse { cursor: nwse-resize; } + .dtframe .dt-nesw { cursor: nesw-resize; } + .dtframe .dtrot { cursor: grab; } + .dtframe .dtendpoint { cursor: crosshair; } + .bdframe .dtrot { cursor: grab; } + .dtarea { + resize: vertical; + min-height: 3.4em; + font: inherit; + line-height: 1.35; + } + .stage.mode-decor.dtool-line, .stage.mode-decor.dtool-rect, + .stage.mode-decor.dtool-ellipse, .stage.mode-decor.dtool-text { + cursor: crosshair; + } + .stage.mode-decor.dtool-erase, + .stage.mode-decor.dtool-erase .decorlayer .dshape { + cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cg transform='rotate(-45 12 12)'%3E%3Crect x='7' y='2' width='10' height='18' rx='2' fill='%23fff' stroke='%23111' stroke-width='1.5'/%3E%3Cpath d='M7 13h10v5a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2z' fill='%23ff9f43' stroke='%23111' stroke-width='1.5'/%3E%3C/g%3E%3C/svg%3E") 5 22, pointer; + } + .stage.mode-decor .room, .stage.mode-decor .devlayer { pointer-events: none; } + .stage.mode-decor .oplock { pointer-events: none; } + /* Backdrop-editor de-emphasis is a shared mode-transition coordinate. + It multiplies whole presentation groups and never changes Glow source + alpha, additive blending, or the underlying resolved state. */ + .stage .room, + .stage .devlayer, + .stage .opening, + .stage .room-outline, + .stage .wallbodies, + .stage .opening-tunnels, + .stage .glow-base-layer, + .stage .glow-pools-frame, + .stage .zero-walls { + opacity: var(--hp-mode-architecture-opacity, 1); + } + .opening-preview { + opacity: 0.5; + pointer-events: none; + } + .opening-preview[data-kind="passage"] { + opacity: 1; + } + .passage-preview-cut { + fill: var(--wall-fill, #ffffff); + fill-opacity: 0.35; + stroke: none; + pointer-events: none; + } + .passage-preview-boundary { + stroke: var(--hp-open, #ff9800); + stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); + pointer-events: none; + } + .opening-preview .op-leaf, + .opening-preview .op-arc { + transition: none; + } + .opening-preview-dot { + fill: var(--hp-open, #ff9800); + pointer-events: none; + } + .opening-dimensions, + .opening-dimension { + pointer-events: none; + } + .opening-dimension-line, + .opening-dimension-tick { + fill: none; + stroke: var(--hp-open, #ff9800); + stroke-width: 1.5; + vector-effect: non-scaling-stroke; + pointer-events: none; + } + .opening-dimension-tick { + stroke-width: 2; + } + .stage.edit .room { + pointer-events: none; + } + .stage.markup { + cursor: crosshair; + } + /* room-picking stages: merge (both clicks) and split before a room is chosen */ + .stage.markup.tool-merge, + .stage.markup.tool-split.pickstage, + .stage.markup.tool-delroom { + cursor: pointer; + } + .stage.markup.tool-wallthick { cursor: default; } + .stage.markup.tool-wallthick.wallhot { cursor: pointer; } + /* Solid wall colour sits under the hatch (owner: both, not either/or). */ + .wallbody-fill { + fill: var(--wall-fill, #ffffff); + fill-opacity: var(--wall-fill-op, 1); + fill-rule: evenodd; + stroke: none; + pointer-events: none; + } + .wallbody { + fill: url(#hp-wall-hatch); + fill-rule: evenodd; + stroke: var(--room-stroke, var(--hp-muted)); + stroke-width: calc(0.6px * var(--hp-cell-visual-scale, 1)); + pointer-events: none; + } + /* Thin-on-screen: hatch collapses into noise — fill alone, stroke from fill path's sibling. */ + .wallbody.solid { + fill: none; + } + .wallthick-hover { + fill: var(--accent-color, #03a9f4); + fill-opacity: 0.38; + stroke: var(--accent-color, #03a9f4); + stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); + stroke-linejoin: round; + stroke-linecap: round; + pointer-events: none; + } + .wallthick-hover.isopen { + fill: var(--error-color, #f44336); + stroke: var(--error-color, #f44336); + } + .zero-wall { + stroke: var(--zero-wall-stroke, var(--hp-muted)); + stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); + stroke-dasharray: + calc(7px * var(--hp-cell-visual-scale, 1)) + calc(7px * var(--hp-cell-visual-scale, 1)); + stroke-linecap: butt; + pointer-events: none; + opacity: 0.9; + } + .zero-walls.solid .zero-wall { + stroke-dasharray: none; + } + /* Rooms with zero/thick stretches: the polygon's own stroke is fully off. + The trimmed .room-outline draws normal walls; View hover gets its own + top overlay after the wall bodies. */ + .room.noedge { + stroke-opacity: 0 !important; + } + /* rooms with open boundaries draw their walls as separate M..L subpaths, + so a corner between two of them is two stroke ENDS meeting: round caps + fill it in the same way a round join fills a closed contour's corner + (owner 2026-08-04 — no teeth anywhere on a room border). */ + .room-outline { + fill: none; + stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); + stroke-linejoin: round; + stroke-linecap: round; + pointer-events: none; + } + /* Plan editor: trimmed outlines use the markup blue */ + .room-outline.outlined { + stroke: rgba(62, 166, 255, 0.55); + stroke-opacity: 1; + } + /* The wash is rendered directly above the resolved room fill but below + tunnels, Glow, sun and walls. Black at 22% reproduces the old + brightness(.78) contract without filtering/promoting an SVG ancestor or + changing the room's hue. The halo/outline are rendered again late, above + the wall bodies. */ + .room-hover-fill { + fill: #000; + fill-opacity: 0.22; + pointer-events: none; + } + .room-hover-halo { + fill: none; + stroke: var(--hp-accent); + stroke-opacity: 0.28; + stroke-width: calc(8px * var(--hp-cell-visual-scale, 1)); + stroke-linejoin: round; + stroke-linecap: round; + pointer-events: none; + } + .room-hover-outline { + fill: none; + stroke: var(--hp-accent); + stroke-width: calc(3px * var(--hp-cell-visual-scale, 1)); + stroke-linejoin: round; + stroke-linecap: round; + pointer-events: none; + } + .stage.markup .room { + pointer-events: none; + } + .stage.markup .devlayer .dev { + display: none; /* in plan mode the icons do not get in the way; labels stay */ + } + /* mode frames: the edit modes are visible at a glance */ + .stage.mode-plan { + outline: 2px solid #ffc14d; + outline-offset: -2px; + } + .stage.mode-devices { + outline: 2px solid var(--hp-accent); + outline-offset: -2px; + } + .room.outlined { + stroke: rgba(62, 166, 255, 0.55); + fill: rgba(62, 166, 255, 0.06); + } + /* AFTER .outlined: same specificity — source order decides (gotcha x4) */ + .room.picked { + stroke: #ffc14d; + stroke-width: calc(3px * var(--hp-cell-visual-scale, 1)); + fill: rgba(255, 193, 77, 0.25); + } + /* Owner 2026-08-04: the grid is a HINT, not content — at full strength the + dots argued with the plan on white paper. Both levels are muted, the + hierarchy is kept (major still denser than fine). */ + .griddot { + fill: var(--hp-accent); + opacity: 0.35; + stroke: rgba(0, 0, 0, 0.35); + stroke-width: 0.4; + } + /* docs/CANVAS.md §7: every coarse node (5x/10x the live step) keeps a + bigger, more opaque dot, so zoomed far out the grid still reads as a + grid instead of a grey wash. */ + .griddot.major { + opacity: 0.5; + stroke-width: 0; + } + /* the contour being drawn in the Plan editor: each wall is its own , + so the round cap IS the corner (matches the finished .room border) */ + .seg { + stroke: var(--hp-accent); + stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); + stroke-linecap: round; + stroke-linejoin: round; + } + .pathline { + stroke: #ffc14d; + stroke-width: calc(3px * var(--hp-cell-visual-scale, 1)); + fill: none; + stroke-linecap: round; + stroke-linejoin: round; + } + .preview { + stroke: #ffc14d; + stroke-width: calc(2px * var(--hp-cell-visual-scale, 1)); + stroke-dasharray: + calc(6px * var(--hp-cell-visual-scale, 1)) + calc(5px * var(--hp-cell-visual-scale, 1)); + opacity: 0.7; + } + .active-axis { + stroke: #ffc14d; + stroke-width: 2; + stroke-linecap: round; + vector-effect: non-scaling-stroke; + pointer-events: none; + } + .active-vertex { + fill: #ffc14d; + stroke: #171006; + stroke-width: 2; + vector-effect: non-scaling-stroke; + pointer-events: none; + } + .wall-repair-preview { + stroke: #d93025; + stroke-width: 4; + stroke-linecap: round; + stroke-dasharray: 5 4; + vector-effect: non-scaling-stroke; + pointer-events: none; + } + .drawwall-preview-fill { + fill: var(--wall-fill, #ffffff); + fill-opacity: calc(var(--wall-fill-op, 1) * 0.55); + fill-rule: evenodd; + stroke: none; + pointer-events: none; + } + .drawwall-preview { + fill: url(#hp-wall-hatch); + fill-opacity: 0.55; + fill-rule: evenodd; + stroke: var(--accent-color, #03a9f4); + stroke-width: calc(0.5px * var(--hp-cell-visual-scale, 1)); + stroke-opacity: 0.7; + pointer-events: none; + } + .drawwall-zero-preview { + fill: none; + stroke: var(--accent-color, #03a9f4); + stroke-width: calc(3px * var(--hp-cell-visual-scale, 1)); + stroke-linecap: butt; + stroke-linejoin: round; + opacity: 0.72; + pointer-events: none; + } + .drawwall-zero-preview.dashed { + stroke-dasharray: + calc(7px * var(--hp-cell-visual-scale, 1)) + calc(7px * var(--hp-cell-visual-scale, 1)); + } + .vertex { + fill: #ffc14d; + stroke: #4a2800; + stroke-width: calc(1px * var(--hp-cell-visual-scale, 1)); + } + .vertex.first { + fill: #4bd28f; + stroke: #04121f; + } + .plan-snap-overlay, + .plan-snap-overlay *, + .hidden-wall-diagnostic, + .hidden-wall-diagnostic * { + pointer-events: none; + } + .plan-snap-line, + .hidden-wall-line { + fill: none; + stroke: color-mix(in srgb, var(--hp-accent) 82%, white 18%); + /* Explicit non-scaling-stroke in the SVG keeps this one screen pixel. */ + stroke-width: 1; + stroke-linecap: round; + opacity: 0.92; + } + .plan-snap-node, + .hidden-wall-node { + fill: var(--ha-card-background, var(--card-background-color, #fff)); + stroke: color-mix(in srgb, var(--hp-accent) 88%, #07131c 12%); + stroke-width: 1; + vector-effect: non-scaling-stroke; + } + .plan-snap-node.active { + fill: #ffc14d; + stroke: #171006; + stroke-width: 2; + } + .plan-snap-node.active.dynamic { + fill: #4bd28f; + stroke: #04121f; + } + .plan-snap-node.conflict { + fill: #fff; + stroke: #d93025; + stroke-width: 3; + } + @media (prefers-color-scheme: dark) { + .plan-snap-line { + stroke: color-mix(in srgb, var(--hp-accent) 72%, white 28%); + } + .hidden-wall-line { + stroke: color-mix(in srgb, var(--hp-accent) 72%, white 28%); + } + .plan-snap-node, + .hidden-wall-node { + fill: #17242c; + stroke: #9bdcf5; + } + .plan-snap-node.active { + fill: #ffc14d; + stroke: #fff4d6; + } + .plan-snap-node.active.dynamic { + fill: #4bd28f; + stroke: #eafff4; + } + } + @media (forced-colors: active) { + .plan-snap-line, + .hidden-wall-line { + stroke: CanvasText; + opacity: 1; + forced-color-adjust: auto; + } + .plan-snap-node, + .hidden-wall-node { + fill: Canvas; + stroke: CanvasText; + forced-color-adjust: auto; + } + .plan-snap-node.active, + .plan-snap-node.active.dynamic { + fill: Highlight; + stroke: HighlightText; + forced-color-adjust: auto; + } + } + /* RGB lights: the bulb takes the light's actual color */ + /* v1.52.0: the RGB tint of the icon/border is gone — a lamp's colour + lives ONLY in its glow spot (owner's rule). The ripple-color fallback + keeps using the light colour; that is set inline via --ripple-color. */ + /* Sun wedges (docs/SUN.md). The layer is present ONLY above the 3° + threshold; crossing it fades the whole layer in or out over EXACTLY + 2 s (owner 2026-08-03 — RAY_FADE_MS in src/sun.ts must match). The + geometry is untouched: this is a plain opacity animation on the group, + so overlapping wedges keep their own blending while it plays. */ + .sunlayer { + animation: hp-sunfade-in 2s linear both; + } + .sunlayer.out { + animation: hp-sunfade-out 2s linear both; + } + @media (prefers-reduced-motion: reduce) { + /* no fade at all: the rays are simply there or simply gone */ + .sunlayer, .sunlayer.out { animation: none; } + .sunlayer.out { opacity: 0; } + } + .devlayer { + position: absolute; + inset: 0; + pointer-events: none; + z-index: 6; + } + .stage.mode-devices .dev { cursor: grab; } + .stage.mode-devices .dev:active { cursor: grabbing; } + .physical-hit { + fill: transparent; + stroke: transparent; + pointer-events: none; + cursor: grab; + } + .stage.tool-select .physical-hit { pointer-events: all; } + .physical-hit:active { cursor: grabbing; } + line.physical-hit { cursor: pointer; } + .physical-hit.selected { + fill: rgba(255, 193, 77, 0.24); + stroke: transparent; + } + line.physical-hit.selected { + stroke: transparent; + } + .physical-drag { + fill: rgba(255, 193, 77, 0.38); + stroke: #ffc14d; + stroke-width: 2.5; + vector-effect: non-scaling-stroke; + pointer-events: none; + } + .physical-chrome { pointer-events: none; } + .physical-chrome .frame, + .physical-chrome .stem { + fill: none; + stroke: #ffc14d; + stroke-width: 2; + vector-effect: non-scaling-stroke; + } + .physical-chrome .frame { fill: rgba(255, 193, 77, 0.22); } + .physical-chrome polyline.frame { fill: none; } + .physical-chrome .move-dot { + fill: #ffc14d; + stroke: #24262d; + stroke-width: 1.5; + vector-effect: non-scaling-stroke; + } + .physical-chrome .rotate-handle { + fill: #24262d; + stroke: #ffc14d; + stroke-width: 2.5; + vector-effect: non-scaling-stroke; + pointer-events: all; + cursor: crosshair; + } + .drawwall.invalid input { border-color: var(--error-color, #db4437); } + .drawwall .rangehint { margin-inline-start: 4px; font-size: 0.78em; opacity: 0.72; } + .drawwall.invalid .rangehint { color: var(--error-color, #db4437); opacity: 1; } + .stage.markup.tool-partition, + .stage.markup.tool-column { cursor: crosshair; } + /* room resize tool (docs/RESIZE.md) */ + /* wall handle: invisible finger-sized hit circle (HP-1550-04 hit priority + kept), the visible glyph lives in the sibling .rszicon */ + .rszhandle { + fill: transparent; + stroke: none; + pointer-events: all; + cursor: grab; + touch-action: none; + } + .rszhandle:active { cursor: grabbing; } + .rszhandle.disabled, + .rszhandle.disabled:active { + cursor: not-allowed; + } + /* wall-with-arrows glyph: accent ink over a bg halo, readable on any plan */ + .rszicon { pointer-events: none; } + .rszicon path { + fill: none; + stroke-linecap: round; + stroke-linejoin: round; + vector-effect: non-scaling-stroke; + } + .rszhalo { stroke: var(--hp-bg); stroke-width: 6; } + .rszink { stroke: var(--hp-accent); stroke-width: 2; } + :host([data-pointer-hover]) .rszhandle:hover + .rszicon .rszink { stroke-width: 3; } + .rszicon.disabled { opacity: 0.38; } + :host([data-pointer-hover]) .rszhandle.disabled:hover + .rszicon .rszink { stroke-width: 2; } + .rszmeasurelayer, + .rszmeasurelayer * { pointer-events: none; } + .rszmeasurehalo, + .rszmeasureink, + .rszleader { + fill: none; + stroke-linecap: round; + vector-effect: non-scaling-stroke; + } + .rszmeasurehalo { + stroke: var(--hp-bg); + stroke-width: 7px; + opacity: 0.9; + } + .rszmeasureink { + stroke: var(--hp-accent); + stroke-width: 3px; + } + .rszleader { + stroke: var(--hp-accent); + stroke-width: 2px; + opacity: 0.95; + } + /* the decor draft badge rides the MIDDLE of the shape, so it is centred + horizontally and lifted clear of the line instead of trailing the + cursor the way a wall badge does (owner 2026-08-04) */ + .measurelabel.dmeasure { + transform: translate(-50%, -160%); + } + .measurelabel.rszarea { + transform: translate( + calc(-50% + var(--rsz-label-x, 0px)), + calc(-50% + var(--rsz-label-y, 0px)) + ); + background: rgba(0, 0, 0, 0.6); + border: 1px solid var(--hp-accent); + } + /* width and depth of a piece of furniture while its corner is dragged — + centred on the edge they measure (docs/FURNITURE.md §6) */ + .measurelabel.furnmeasure { + transform: translate(-50%, -50%); + border: 1px solid var(--hp-accent); + } + .alignmsg { margin: 0 0 8px; font-size: 13px; line-height: 1.45; } +`,o` + /* Unified device activity: alarm, short event and continuous state all + share this one renderer. No static rings are allowed. */ + .dev ha-icon { + position: relative; + z-index: 1; + } + .device-pulse { + position: absolute; + left: 50%; + top: 50%; + width: var(--dev-size); + height: var(--dev-size); + transform: translate(-50%, -50%); + pointer-events: none; + z-index: 0; + } + .device-pulse i { + position: absolute; + inset: 0; + border-radius: 50%; + border: 2px solid var(--ripple-color, var(--hp-accent)); + opacity: 0; + } + /* A witnessed edge: exactly three waves over the 3.3 s runtime window. */ + .device-pulse.short i { + animation: hp-pulse-short 1.1s cubic-bezier(.22,.61,.36,1) 1 forwards; + } + .device-pulse.short i:nth-child(2) { animation-delay: 1.1s; } + .device-pulse.short i:nth-child(3) { animation-delay: 2.2s; } + .device-pulse.short.gen2 i { animation-name: hp-pulse-short-b; } + .device-pulse.continuous i:nth-child(n + 2), + .device-pulse.alarm i:nth-child(n + 3) { display: none; } + .device-pulse.continuous i:first-child { + animation: hp-pulse-continuous 3.6s cubic-bezier(.45,.05,.55,.95) infinite; + } + .device-pulse.alarm i { + border-width: 3px; + border-color: #F0410C; + animation: hp-pulse-alarm 2.4s cubic-bezier(.22,.61,.36,1) infinite; + } + .device-pulse.alarm i:nth-child(2) { animation-delay: 1.2s; } + .activity-dot { + position: absolute; + right: 8%; + bottom: 8%; + width: 18%; + height: 18%; + min-width: 4px; + min-height: 4px; + border-radius: 50%; + background: var(--ripple-color, var(--hp-accent)); + border: 1px solid color-mix(in srgb, var(--hp-bg) 82%, transparent); + pointer-events: none; + z-index: 3; + } + .kioskdots { + position: absolute; + left: 50%; + bottom: var(--sp-5); + transform: translateX(-50%); + display: flex; + gap: var(--sp-4); + z-index: 5; + pointer-events: none; + } + .kdot { + width: 9px; + height: 9px; + border-radius: 50%; + background: var(--hp-muted); + opacity: 0.55; + } + .kdot.on { background: var(--hp-accent); opacity: 1; } + .dev.valonly { + /* Saved coordinates remain the centre of a Text shell. The shell may + expand, while the invisible anchor stays one core diameter wide. */ + width: var(--dev-size, var(--icon-size, 2.5cqw)); + } + .dev.valonly .device-core { + width: max-content; + min-width: var(--dev-size, var(--icon-size, 2.5cqw)); + padding-inline: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.16); + border-radius: calc(var(--dev-size, var(--icon-size, 2.5cqw)) / 2); + } + .dev .valtext { + overflow: visible; + font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * var(--value-font-scale, .45)); + font-weight: 600; + white-space: nowrap; + } + @media (prefers-reduced-motion: reduce) { + .device-pulse { display: none; } + } + .dev .newdot { + position: absolute; + top: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * -0.12); + right: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * -0.12); + width: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.34); + height: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.34); + border-radius: 50%; + background: #f0301f; + border: 2px solid var(--card-background-color, var(--hp-bg)); + pointer-events: none; + z-index: 2; + } + .dev { + position: absolute; + /* The surface resolves compatibility icon_size units once. The shared + face receives an effective base and only applies the per-device size. */ + --dev-size: calc(var(--device-base-size, 2.25cqw) * var(--dev-scale, 1)); + /* 101.5/80 is the package shell/core ratio, including its stroke. */ + --device-shell-size: calc(var(--dev-size) * 1.26875); + --device-shell-inset: calc(var(--dev-size) * 0.134375); + --device-shell-stroke-ratio: 0.01875; + --device-shell-border-width: max(1px, calc(var(--dev-size) * var(--device-shell-stroke-ratio))); + --device-core-bg: var(--card-background-color, #fff); + --device-core-fg: var(--primary-text-color, #252525); + --device-core-bg: light-dark(#fff, #252525); + --device-core-fg: light-dark(#252525, #fff); + --device-face-bg: var(--device-core-bg); + --device-face-fg: var(--device-core-fg); + --device-shell-base-stroke: light-dark(#BCBCBC, rgb(37 37 37 / 75%)); + --device-shell-stroke: var(--device-shell-base-stroke); + --device-shell-shadow: + 0 calc(var(--dev-size) * .025) calc(var(--dev-size) * .05) rgb(37 40 45 / 12%), + 0 calc(var(--dev-size) * .1) calc(var(--dev-size) * .175) + calc(var(--dev-size) * -.025) rgb(37 40 45 / 18%); + --device-core-inset-shadow: 0 0 0 0 transparent; + --device-ring-color: transparent; + --device-ring-width: 0px; + /* The saved point is the exact centre of the icon core. */ + width: var(--dev-size); + height: var(--dev-size); + margin: calc(var(--dev-size) / -2) 0 0 calc(var(--dev-size) / -2); + border: 0; + background: transparent; + display: block; + color: var(--device-core-fg); + cursor: pointer; + pointer-events: auto; + transition: opacity 0.2s; + box-shadow: none; + outline: none; + z-index: 2; + } + .dev.theme-light { + --device-core-bg: #fff; + --device-core-fg: #000; + --device-shell-base-stroke: #BCBCBC; + --device-shell-stroke: var(--device-shell-base-stroke); + --device-shell-shadow: + 0 calc(var(--dev-size) * .025) calc(var(--dev-size) * .05) rgb(37 40 45 / 12%), + 0 calc(var(--dev-size) * .1) calc(var(--dev-size) * .175) + calc(var(--dev-size) * -.025) rgb(37 40 45 / 18%); + --device-core-inset-shadow: 0 0 0 0 transparent; + } + .dev.theme-dark { + --device-core-bg: #252525; + --device-core-fg: #fff; + --device-shell-base-stroke: rgb(37 37 37 / 75%); + --device-shell-stroke: var(--device-shell-base-stroke); + --device-shell-shadow: + 0 calc(var(--dev-size) * .025) calc(var(--dev-size) * .0375) rgb(37 40 45 / 12%), + 0 calc(var(--dev-size) * .1) calc(var(--dev-size) * .175) + calc(var(--dev-size) * -.025) rgb(37 40 45 / 18%); + --device-core-inset-shadow: + inset 0 calc(var(--dev-size) * 0.0125) calc(var(--dev-size) * 0.0125) rgb(255 255 255 / 70%); + } + .dev::before { + content: ''; + position: absolute; + left: 50%; + top: 50%; + width: max(44px, var(--device-shell-size)); + height: max(44px, var(--device-shell-size)); + transform: translate(-50%, -50%); + border-radius: 50%; + pointer-events: auto; + z-index: 3; + } + .device-shell { + position: absolute; + box-sizing: border-box; + display: flex; + align-items: center; + gap: calc(var(--dev-size) * 0.1); + padding: 0; + min-width: var(--dev-size); + min-height: var(--dev-size); + border: 0; + background: transparent; + box-shadow: none; + transition: opacity .2s; + pointer-events: none; + } + .device-shell-frame { + position: absolute; + z-index: 0; + box-sizing: border-box; + inset: calc(var(--device-shell-inset) / -1); + border: var(--device-shell-border-width) solid var(--device-shell-stroke); + /* A saturating radius is resolved from the final border box. Using a + second fractional length here lets border-box and radius quantise on + different sides of a device pixel at some zoom/DPR combinations. */ + border-radius: 9999px; + background: transparent; + box-shadow: var(--device-shell-shadow); + transition: border-color .15s, box-shadow .15s, opacity .2s; + pointer-events: auto; + /* Normative production fallback: never add a per-marker backdrop blur. */ + backdrop-filter: none; + } + .device-shell:not(.with-values) { + left: 0; + top: 0; + } + .device-shell:not(.with-values):not(.text-shell) .device-shell-frame { + border-radius: 50%; + } + .device-shell.with-values.pos-right { + left: 0; + top: 50%; + transform: translateY(-50%); + } + .device-shell.with-values.pos-left { + right: 0; + top: 50%; + transform: translateY(-50%); + flex-direction: row-reverse; + } + .device-shell.with-values.pos-bottom { + left: 50%; + top: 0; + transform: translateX(-50%); + flex-direction: column; + } + .device-shell.with-values.pos-top { + left: 50%; + bottom: 0; + transform: translateX(-50%); + flex-direction: column-reverse; + } + .device-core { + position: relative; + z-index: 1; + box-sizing: border-box; + flex: 0 0 auto; + width: var(--dev-size); + height: var(--dev-size); + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + background: var(--device-face-bg); + color: var(--device-face-fg); + box-shadow: + var(--device-core-inset-shadow), + 0 0 0 var(--device-ring-width) var(--device-ring-color); + line-height: 0; + transition: background .15s, color .15s, box-shadow .15s, opacity .2s; + pointer-events: none; + } + .device-sections { + display: flex; + flex-direction: column; + align-items: stretch; + justify-content: center; + gap: calc(var(--dev-size) * .08); + min-width: 0; + pointer-events: none; + } + .device-shell.pos-top .device-sections, + .device-shell.pos-bottom .device-sections { + flex-direction: row; + } + .dev ha-icon { + /* from --dev-size, NOT --icon-size: the per-device size multiplier must + scale the GLYPH with its badge. Pinned to the base size, "make this + icon bigger" grew an empty box around a default-size glyph (user + report via the owner, 2026-07-29). */ + --mdc-icon-size: calc(var(--dev-size, var(--device-base-size, 2.25cqw)) * 0.55); + display: flex; + align-items: center; + justify-content: center; + line-height: 0; + } + .dev.on { + --device-face-bg: #F0A00C; + --device-face-fg: light-dark(#fff, #252525); + --device-shell-stroke: #F0A00C; + } + .dev.theme-light.on { --device-face-fg: #fff; } + .dev.theme-dark.on { + --device-face-fg: #252525; + --device-shell-stroke-ratio: .0375; + } + .dev.open { + --device-face-bg: var(--hp-open); + --device-face-fg: light-dark(#fff, #252525); + } + .dev.theme-light.open { --device-face-fg: #fff; } + .dev.theme-dark.open { --device-face-fg: #252525; } + .dev.lock-locked { + --device-face-bg: #66D17A; + --device-face-fg: light-dark(#fff, #252525); + --device-shell-stroke: #66D17A; + } + .dev.theme-light.lock-locked { --device-face-fg: #fff; } + .dev.theme-dark.lock-locked { + --device-face-fg: #252525; + --device-shell-stroke-ratio: .025; + } + .dev.lock-unlocked { + --device-face-bg: #F0410C; + --device-face-fg: light-dark(#fff, #252525); + --device-shell-stroke: #F0410C; + } + .dev.theme-light.lock-unlocked { --device-face-fg: #fff; } + .dev.theme-dark.lock-unlocked { + --device-face-fg: #252525; + --device-shell-stroke-ratio: .025; + } + /* Interaction wins ordinary state colours. Alarm keeps priority through + the more-specific rule below. Unavailable has no visual hover. */ + :host([data-pointer-hover]) .dev:not(.unavail):hover { + --device-face-bg: #0C82F0; + --device-face-fg: light-dark(#fff, #252525); + --device-shell-stroke: var(--device-shell-base-stroke); + } + :host([data-pointer-hover]) .dev.theme-light:not(.unavail):hover { --device-face-fg: #fff; } + :host([data-pointer-hover]) .dev.theme-dark:not(.unavail):hover { --device-face-fg: #252525; } + :host([data-pointer-hover]) .dev:hover, + .dev:focus-visible { z-index: 5; } + .dev.unavail { + opacity: 0.35; + --device-face-bg: #B5BAC1; + --device-shell-stroke: var(--device-shell-base-stroke); + } + .dev.virtual .device-shell-frame { + border-style: dashed; + } + /* "hide from plan" flag, shown only in the device editor with the + "show hidden devices" toggle on (docs/FILTERING.md). BLUE, so a hidden + device cannot be mistaken for an unavailable one (translucent dark) — + and no live-state paint at all: a ghost is configuration, not status + (owner's request). */ + .dev.ghost { + opacity: 0.6; + color: var(--hp-accent); + } + .dev.ghost .device-shell-frame { + border-style: dashed; + border-color: var(--hp-accent); + box-shadow: none; + } + .dev.ghost .device-core { + background: rgba(62, 166, 255, 0.22); /* fallback for old WebViews */ + background: color-mix(in srgb, var(--hp-accent) 30%, var(--card-background-color, #1c2530)); + color: var(--hp-accent); + } + /* HA-disabled is not a user hide: neutral grey + a power-off badge keeps + the two ghosts distinguishable without relying on colour alone. */ + .dev.ghost.ha-disabled { + opacity: 0.62; + color: var(--secondary-text-color, #9aa0aa); + } + .dev.ghost.ha-disabled .device-shell-frame { + border-color: var(--secondary-text-color, #9aa0aa); + } + .dev.ghost.ha-disabled .device-core { + background: rgba(120, 124, 134, 0.2); + background: color-mix(in srgb, var(--secondary-text-color, #9aa0aa) 24%, var(--card-background-color, #1c2530)); + color: var(--secondary-text-color, #9aa0aa); + } + .dev .habadge { + position: absolute; + right: -18%; + bottom: -18%; + display: grid; + place-items: center; + width: 46%; + height: 46%; + min-width: 12px; + min-height: 12px; + border-radius: 50%; + background: var(--card-background-color, #242832); + border: 1px solid currentColor; + } + .dev .habadge ha-icon { width: 72%; height: 72%; } + .dev.sel { + --device-ring-color: #F0A00C; + --device-ring-width: calc(var(--dev-size) * 0.0375); + } + .dev:focus-visible { + --device-ring-color: #0C82F0; + --device-ring-width: calc(var(--dev-size) * 0.0375); + } + .dev:not(.on):not(.open):not(.alarm):not(.lock-locked):not(.lock-unlocked):not(.unavail):focus-visible { + --device-face-fg: #0C82F0; + } + /* Alert stays above focus, selection, hover and ordinary semantic paint. */ + .dev.alarm, + :host([data-pointer-hover]) .dev.alarm:hover, + .dev.alarm:focus-visible { + --device-face-bg: #F0410C; + --device-face-fg: light-dark(#fff, #252525); + --device-shell-stroke: #F0410C; + } + .dev.theme-light.alarm { --device-face-fg: #fff; } + .dev.theme-dark.alarm { + --device-face-fg: #252525; + --device-shell-stroke-ratio: .025; + } + .dev .value-badge { + position: relative; + z-index: 2; + box-sizing: border-box; + width: max-content; + min-width: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * .7875); + height: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * .7875); + display: flex; + align-items: center; + justify-content: center; + overflow: visible; + background: var(--device-core-bg); + border: 0; + border-radius: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.39375); + padding: 0 calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.14); + font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * var(--value-font-scale, .45)); + font-weight: 600; + line-height: 1; + color: var(--device-core-fg); + white-space: nowrap; + pointer-events: none; + } + .dev .value-badge.unavailable, + .dev .value-badge.missing { opacity: 0.66; } + .dev .lqi { + position: absolute; + top: calc(50% + var(--device-shell-size) / 2 + var(--dev-size) * .05); + left: 50%; + transform: translateX(-50%); + margin-top: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.05); + font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.38); + font-weight: 600; + line-height: 1; + text-shadow: 0 0 3px rgba(0, 0, 0, 0.9), 0 0 2px rgba(0, 0, 0, 0.9); + white-space: nowrap; + pointer-events: none; + } + .dev .lqi.below-value-badge { + margin-top: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.8875); + } + .temprange { + display: inline-flex; + align-items: center; + gap: var(--sp-3); + margin-left: auto; + color: var(--hp-muted); + font-size: var(--fs-s); + } + /* live vacuum: a round puck, no badge plate, soft pulse (docs/VACUUM.md) */ + .vacpuck { + position: absolute; + /* the base badge, but round and 20% smaller — the owner's wording: + «иконка похожа на иконку базы, только круглая и чуть меньше» */ + --puck-size: calc(var(--device-base-size, 2.25cqw) * 0.8); + width: var(--puck-size); + height: var(--puck-size); + border-radius: 50%; + transform: translate(-50%, -50%); + --vac-core-bg: light-dark(#fff, #252525); + --vac-core-fg: light-dark(#252525, #fff); + background: var(--vac-core-bg); + border: 1px solid #BCBCBC; + box-shadow: + 0 1px 2px rgb(37 40 45 / 12%), + 0 4px 8px -1.07px rgb(37 40 45 / 18%); + color: var(--vac-core-fg); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 6; + /* glide between sparse position updates; .jump disables it so the robot + never appears to drive through walls after a data gap */ + transition: left 1.2s linear, top 1.2s linear; + animation: vacpulse 2.2s ease-out infinite; + } + .vacpuck.theme-light { --vac-core-bg: #fff; --vac-core-fg: #252525; } + .vacpuck.theme-dark { --vac-core-bg: #252525; --vac-core-fg: #fff; } + .vacpuck.jump { transition: none; } + .vacpuck.stale { opacity: 0.45; animation: none; } + .vacpuck ha-icon { + --mdc-icon-size: calc(var(--puck-size) * 0.68); + color: var(--vac-core-fg); + /* same centering recipe as .dev ha-icon: without flex + line-height 0 + the glyph sits on its text baseline and appears to float around the + circle (owner report 2026-07-31) */ + display: flex; + align-items: center; + justify-content: center; + line-height: 0; + width: var(--mdc-icon-size); + height: var(--mdc-icon-size); + } + @keyframes vacpulse { + 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--hp-accent) 45%, transparent); } + 70% { box-shadow: 0 0 0 12px transparent; } + 100% { box-shadow: 0 0 0 0 transparent; } + } + @media (prefers-reduced-motion: reduce) { + .vacpuck { animation: none; } + } + .vactrail { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 5; + overflow: visible; + } + .vactrail polyline, + .vactrail path { + fill: none; + stroke-linejoin: round; + stroke-linecap: round; + vector-effect: non-scaling-stroke; + } + /* dark halo + light core: neutral, and one of the two always contrasts + with whatever fill is underneath */ + .vactrail g.prev { opacity: 0.4; } + .vactrail .case { + stroke: rgba(0, 0, 0, 0.4); + stroke-width: 2.25; + } + .vactrail .core { + stroke: rgba(255, 255, 255, 0.82); + stroke-width: 0.9; + } + .devicepreview-empty { + min-height: 82px; + margin: 4px 0 14px; + padding: var(--sp-4); + display: flex; + align-items: center; + justify-content: center; + gap: var(--sp-3); + border: 1px dashed var(--hp-line); + border-radius: var(--rad-m); + color: var(--hp-muted); + text-align: center; + } +`,o` + .hdr { + position: sticky; + top: var(--header-height, 56px); + z-index: 20; + background: var(--card-background-color, var(--hp-bg)); + border-radius: var(--ha-card-border-radius, 12px) var(--ha-card-border-radius, 12px) 0 0; + } + .tabs { + display: flex; + gap: var(--sp-2); + background: rgba(127, 127, 127, 0.12); + padding: var(--sp-2); + border-radius: var(--rad-l); + flex-wrap: wrap; + } + .tab { + border: 0; + background: transparent; + color: var(--hp-muted); + display: inline-flex; + align-items: center; + padding: var(--sp-3) var(--sp-5); + border-radius: var(--rad-m); + font-size: var(--fs-m); + font-weight: 600; + cursor: pointer; + transition: 0.15s; + font-family: inherit; + } + :host([data-pointer-hover]) .tab:hover { + color: var(--hp-txt); + } + .tab.active { + background: var(--hp-accent); + color: var(--text-primary-color, #fff); + } + /* docs/CANVAS.md §4.1: objects an order of magnitude away from the plan + do not decide the opening view — one quiet chip says so and offers to + take them in. Never a modal (owner). */ + .farhint { + position: absolute; + left: 50%; + transform: translateX(-50%); + bottom: var(--sp-4); + z-index: 12; + display: flex; + align-items: center; + gap: var(--sp-2); + max-width: calc(100% - var(--sp-8)); + background: var(--card-background-color, var(--hp-bg)); + opacity: 0.94; + color: var(--hp-txt); + border: 1px solid var(--divider-color, #33404d); + border-radius: var(--rad-m); + padding: var(--sp-1) var(--sp-3); + font-size: var(--fs-s); + } + .farhint ha-icon { + --mdc-icon-size: 18px; + color: var(--hp-warn, #e2a03f); + flex: none; + } + .farhint span { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .dropbtn { + display: flex; + align-items: center; + gap: var(--sp-4); + width: 100%; + text-align: left; + border: 1px solid var(--hp-muted); + border-radius: var(--rad-m); + background: transparent; + color: var(--hp-txt); + padding: var(--sp-4) 10px; + cursor: pointer; + font-family: inherit; + font-size: var(--fs-m); + margin-top: var(--sp-3); + } + .dropbtn .ref { color: var(--hp-muted); font-size: var(--fs-s); margin-left: auto; } + .dropbtn ha-icon { --mdc-icon-size: 18px; margin-left: var(--sp-2); } + .dropbtn.open { border-color: var(--hp-accent); } + .droppanel { + border: 1px solid var(--hp-accent); + border-top: none; + border-radius: 0 0 var(--rad-m) var(--rad-m); + padding: var(--sp-3); + margin-top: -4px; + } + .hdr.kioskhide { display: none; } + .decorbar .dcolor { + width: 30px; height: 26px; padding: 0; border: none; background: none; cursor: pointer; + } + .decorbar .dwidth { + font-family: inherit; font-size: var(--fs-s); border-radius: var(--rad-s); + background: var(--hp-bg2, transparent); color: var(--hp-txt); border: 1px solid var(--hp-muted); + padding: var(--sp-2) var(--sp-3); + } + .decorbar .bdhint { + font-size: var(--fs-s); + color: var(--hp-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .decorbar .dfill { + display: inline-flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-s); cursor: pointer; + } + .decorbar .dfill input[type="checkbox"] { + width: 16px; + height: 16px; + flex: none; + margin: 0; + padding: 0; + } + .decorbar hp-color-opacity { flex: 0 0 auto; } + .modes { + display: inline-flex; + gap: var(--sp-1); + background: rgba(127, 127, 127, 0.12); + border-radius: var(--rad-l); + padding: var(--sp-2); + } + .modetab { + display: inline-flex; + align-items: center; + gap: var(--sp-3); + border: 0; + background: transparent; + color: var(--hp-muted); + padding: var(--sp-3) 10px; /* 10px h-padding kept: +2px would wrap the header modes row */ + border-radius: var(--rad-m); + font-size: var(--fs-m); + font-weight: 600; + cursor: pointer; + font-family: inherit; + transition: background-color 0.14s ease, color 0.14s ease, transform 0.14s ease; + } + .modetab:active { transform: scale(0.97); } + .modetab ha-icon { --mdc-icon-size: 15px; } + /* issue #220: a tab can be dragged to a new position in the editors */ + .tab[data-reorderable] { cursor: grab; } + .tab.dragging { cursor: grabbing; opacity: 0.55; } + .tab.drop-before { box-shadow: inset 2px 0 0 0 var(--primary-color, #03a9f4); } + .tab.drop-after { box-shadow: inset -2px 0 0 0 var(--primary-color, #03a9f4); } + .modetab .closex { + --mdc-icon-size: 13px; + box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + min-width: 24px; + min-height: 24px; + /* Keep the old 13 × 13 flex footprint (+2 px left margin) while the + pointer target grows around it. Header width, height and wrapping do + not move; only near-misses that used to hit the active tab reach X. */ + margin: -5.5px -5.5px -5.5px -3.5px; + opacity: 0.75; + cursor: pointer; + border-radius: var(--rad-s); + } + :host([data-pointer-hover]) .modetab .closex:hover { opacity: 1; } + .editbar .barclose { + box-sizing: border-box; + width: 40px; + height: 40px; + padding: 0; + margin: 0; + min-width: 40px; + min-height: 40px; + justify-content: center; + gap: 0; + line-height: 0; + } + .editbar .barclose ha-icon { flex: none; margin: 0; } + .modetab.active { + background: var(--hp-accent); + color: var(--text-primary-color, #fff); + } + @media (max-width: 720px) { + .modetab .ml { display: none; } + } + .editbar { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: stretch; + border-bottom: 1px solid var(--hp-line); + font-size: var(--fs-m); + } + .editbar-tools { + display: flex; + align-items: center; + gap: 10px; + padding: var(--sp-4) var(--sp-5); + flex-wrap: wrap; + min-width: 0; + outline: none; + } + .editbar-end { + display: flex; + align-items: center; + padding: var(--sp-4) var(--sp-5); + border-inline-start: 1px solid var(--hp-line); + background: var(--card-background-color, var(--hp-bg)); + position: relative; + z-index: 2; + } + .tab .tabedit { + --mdc-icon-size: 13px; + display: inline-flex; + align-items: center; + margin-left: var(--sp-3); + opacity: 0.4; + } + :host([data-pointer-hover]) .tab:hover .tabedit { + opacity: 0.9; + } + .tab.tabadd { + padding: var(--sp-3) var(--sp-4); + } + .tab.tabadd ha-icon { + --mdc-icon-size: 15px; + } + .rhint { + font-size: var(--fs-s); + color: var(--hp-muted); + margin-bottom: var(--sp-3); + } + .togglehint { + overflow-wrap: anywhere; + } + .togglehint > div + div { + margin-top: var(--sp-1); + } + /* Keep the last editor bar mounted while the row collapses. This makes + both entering and leaving an editor change the card geometry gradually; + the header ResizeObserver keeps the stage fitted throughout. */ + .editorchrome { + display: block; + height: 0; + opacity: 0; + visibility: hidden; + overflow: hidden; + } + .editorchrome:not(.open) { + pointer-events: none; + } + .editorchrome.open { + height: auto; + opacity: 1; + visibility: visible; + overflow: visible; + } + .editorchrome.transitioning { + overflow: hidden; + will-change: height; + pointer-events: none; + } + /* The toolbar is already visible while its height is interpolating. Its + explicit navigation control must remain usable even though all editing + tools stay frozen until the transition settles. */ + .editorchrome.transitioning .barclose { + pointer-events: auto; + } + .editorchrome-inner { + min-height: 0; + transform-origin: top center; + } + .editorchrome.transitioning .editorchrome-inner { + will-change: opacity; + } + @media (prefers-reduced-motion: reduce) { + .modetab { transition: none; } + } + .editbar .warn { + color: #ffc14d; + } + .editbar .sname { + font-weight: 600; + } + .editbar input { + width: 74px; + background: transparent; + border: 1px solid var(--hp-line); + color: var(--hp-txt); + border-radius: var(--rad-s); + padding: var(--sp-3) var(--sp-4); + font-size: var(--fs-m); + } + .editbar label, + .editbar .hint { + color: var(--hp-muted); + font-size: var(--fs-s); + } + .editbar .drawwall { + display: inline-flex; + align-items: center; + gap: var(--sp-3); + white-space: nowrap; + } + .editbar .wallsgroup { + display: inline-flex; + align-items: center; + gap: 10px; + white-space: nowrap; + } + .editbar .drawwall input { + width: 4.2em; + } + .editbar .drawwall .opl { + color: var(--hp-muted); + font-size: var(--fs-s); + } + .menuwrap { + position: fixed; + inset: 0; + z-index: 80; + } + .menu { + position: fixed; + background: var(--hp-bg); + border: 1px solid var(--hp-accent); + border-radius: var(--rad-l); + box-shadow: var(--shadow-2); + min-width: 210px; + max-width: 300px; + overflow: hidden; + transform: translate(0, 8px); + } + .menu .hd { + padding: var(--sp-4) var(--sp-5); + font-weight: 600; + font-size: var(--fs-m); + border-bottom: 1px solid var(--hp-line); + display: flex; + align-items: center; + gap: var(--sp-3); + } + .menu .hd ha-icon, + .menu .it.all ha-icon { + color: var(--hp-accent); + --mdc-icon-size: 16px; + } + .menu .it { + padding: var(--sp-4) var(--sp-5); + font-size: var(--fs-m); + cursor: pointer; + display: flex; + align-items: center; + gap: var(--sp-4); + } + .menu .it ha-icon { + --mdc-icon-size: 16px; + color: var(--hp-muted); + } + :host([data-pointer-hover]) .menu .it:hover { + background: rgba(127, 127, 127, 0.15); + } + .menu .it.all { + color: var(--hp-accent); + font-weight: 600; + } + .tip { + position: fixed; + pointer-events: none; + background: var(--hp-bg); + border: 1px solid var(--hp-accent); + color: var(--hp-txt); + padding: var(--sp-3) 10px; + border-radius: var(--rad-m); + font-size: var(--fs-m); + box-shadow: var(--shadow-2); + z-index: 60; + max-width: 260px; + } + .tip .m { + color: var(--hp-muted); + font-size: var(--fs-s); + display: block; + } +`,o` + .head { + display: flex; + align-items: center; + gap: 10px; + padding: 10px var(--sp-5); + border-bottom: 1px solid var(--hp-line); + flex-wrap: wrap; + } + .title { + font-size: var(--fs-l); + font-weight: 600; + display: flex; + align-items: center; + gap: var(--sp-3); + white-space: nowrap; + } + .title ha-icon { + color: var(--hp-accent); + --mdc-icon-size: 18px; + } + @media (max-width: 620px) { + .head { gap: var(--sp-3); padding: var(--sp-4) 10px; } + .head .count { display: none; } + .head .title { font-size: var(--fs-m); } + } + .count { + font-size: var(--fs-s); + color: var(--hp-muted); + } + .btn { + display: inline-flex; + align-items: center; + gap: var(--sp-3); + border: 1px solid var(--hp-line); + background: transparent; + color: var(--hp-txt); + padding: var(--sp-3) 10px; + border-radius: var(--rad-m); + cursor: pointer; + transition: 0.15s; + font-family: inherit; + font-size: var(--fs-m); + } + .btn ha-icon { + --mdc-icon-size: 17px; + } + :host([data-pointer-hover]) .btn:hover { + border-color: var(--hp-accent); + } + .btn.on { + background: var(--hp-accent); + color: var(--text-primary-color, #fff); + border-color: var(--hp-accent); + } + .btn.ghost { + border: none; + } + .btn[disabled] { + opacity: 0.5; + pointer-events: none; + } + /* the compass dial in the general settings (docs/SUN.md) */ + .sunrow { + display: flex; + align-items: center; + gap: var(--sp-5); + margin: var(--sp-3) 0; + } + .suncol { + flex: 1; + display: flex; + flex-direction: column; + gap: var(--sp-3); + min-width: 0; + } + .recoveryoverlay { + position: absolute; + inset: 0; + /* Above the contextual editor tray (70): recovery is the one state in + which every stage-editing surface must be inert. Dialogs live outside + the stage and retain their own higher card-level stacking context. */ + z-index: 75; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--sp-4); + padding: var(--sp-6); + box-sizing: border-box; + /* The final solid layer guarantees an opaque recovery surface even when + a custom HA theme exposes its card colour as rgba(). */ + background: + linear-gradient(var(--ha-card-background, var(--card-background-color, #111)), + var(--ha-card-background, var(--card-background-color, #111))), + #111; + color: var(--primary-text-color, #fff); + text-align: center; + pointer-events: auto; + transition: opacity 0.15s ease; + } + .recoveryoverlay.phase-entering, + .recoveryoverlay.phase-leaving { + opacity: 0; + } + .recoveryoverlay.phase-fading-in, + .recoveryoverlay.phase-opaque { + opacity: 1; + } + .recoveryoverlay ha-icon { + --mdc-icon-size: 44px; + color: var(--hp-accent); + } + .editorloading { + position: absolute; + z-index: 74; + left: 50%; + top: 50%; + display: flex; + align-items: center; + gap: var(--sp-3); + padding: var(--sp-3) var(--sp-5); + border: 1px solid color-mix(in srgb, var(--hp-accent) 45%, transparent); + border-radius: 999px; + background: color-mix(in srgb, + var(--ha-card-background, var(--card-background-color, #111)) 92%, transparent); + color: var(--primary-text-color); + box-shadow: 0 6px 22px rgb(0 0 0 / 18%); + transform: translate(-50%, -50%); + pointer-events: none; + animation: editor-loading-in 0.15s ease both; + } + .editorloading ha-icon { + --mdc-icon-size: 22px; + color: var(--hp-accent); + animation: editor-loading-spin 0.9s linear infinite; + } + @keyframes editor-loading-in { + from { opacity: 0; transform: translate(-50%, calc(-50% + 4px)); } + to { opacity: 1; transform: translate(-50%, -50%); } + } + @keyframes editor-loading-spin { to { transform: rotate(360deg); } } + @media (prefers-reduced-motion: reduce) { + .recoveryoverlay, + .editorloading, + .editorloading ha-icon { + transition: none; + animation: none; + } + } + .oplock { + --oplock-size: calc(var(--icon-size, 2.5cqw) * 0.62); + --oplock-core-size: calc(var(--oplock-size) / 1.26875); + --oplock-stroke-ratio: 0.01875; + --oplock-stroke-width: max(1px, calc(var(--oplock-core-size) * var(--oplock-stroke-ratio))); + --oplock-core-bg: light-dark(#fff, #252525); + --oplock-core-fg: light-dark(#252525, #fff); + --oplock-shell-stroke: light-dark(#BCBCBC, rgb(37 37 37 / 75%)); + --oplock-shell-shadow: + 0 calc(var(--oplock-core-size) * .025) calc(var(--oplock-core-size) * .05) rgb(37 40 45 / 12%), + 0 calc(var(--oplock-core-size) * .1) calc(var(--oplock-core-size) * .175) + calc(var(--oplock-core-size) * -.025) rgb(37 40 45 / 18%); + --oplock-core-shadow: 0 0 0 0 transparent; + pointer-events: none; /* inert while editing; clickable in View (rule below) */ + position: absolute; + transform: translate(-50%, -50%); + width: var(--oplock-size); + height: var(--oplock-size); + display: grid; + place-items: center; + border: 0; + background: transparent; + z-index: 1; + } + .oplock.theme-light { + --oplock-core-bg: #fff; + --oplock-core-fg: #252525; + --oplock-shell-stroke: #BCBCBC; + } + .oplock.theme-dark { + --oplock-core-bg: #252525; + --oplock-core-fg: #fff; + --oplock-shell-stroke: rgb(37 37 37 / 75%); + --oplock-core-shadow: + inset 0 calc(var(--oplock-core-size) * .0125) + calc(var(--oplock-core-size) * .0125) rgb(255 255 255 / 70%); + } + .oplock-shell { + box-sizing: border-box; + width: 100%; + height: 100%; + border: var(--oplock-stroke-width) solid var(--oplock-shell-stroke); + border-radius: 50%; + background: transparent; + box-shadow: var(--oplock-shell-shadow); + display: grid; + place-items: center; + pointer-events: none; + backdrop-filter: none; + } + .oplock-core { + width: var(--oplock-core-size); + height: var(--oplock-core-size); + border-radius: 50%; + background: var(--oplock-core-bg); + color: var(--oplock-core-fg); + box-shadow: var(--oplock-core-shadow); + display: grid; + place-items: center; + pointer-events: none; + } + .oplock ha-icon { + --mdc-icon-size: calc(var(--oplock-core-size) * 0.55); + display: flex; + line-height: 0; + } + .oplock.locked { + --oplock-core-bg: #66D17A; + --oplock-core-fg: light-dark(#fff, #252525); + --oplock-shell-stroke: #66D17A; + } + .oplock.theme-light.locked { --oplock-core-fg: #fff; } + .oplock.theme-dark.locked { + --oplock-core-fg: #252525; + --oplock-stroke-ratio: .025; + } + .oplock.unlocked { + --oplock-core-bg: #F0410C; + --oplock-core-fg: light-dark(#fff, #252525); + --oplock-shell-stroke: #F0410C; + } + .oplock.theme-light.unlocked { --oplock-core-fg: #fff; } + .oplock.theme-dark.unlocked { + --oplock-core-fg: #252525; + --oplock-stroke-ratio: .025; + } + .oplock.unknown { --oplock-core-fg: var(--hp-muted); } + .btn.lockact { + width: 100%; + justify-content: center; + display: flex; + align-items: center; + gap: var(--sp-3); + margin-top: var(--sp-4); + } + .btn.lockact.warn { + color: var(--error-color, #d33); + border-color: var(--error-color, #d33); + } + .oprow { + display: flex; + align-items: center; + gap: var(--sp-4); + padding: var(--sp-3) 0; + } + .oprow b { margin-left: auto; } + .oprow.ok b { color: #66d17a; } + .oprow.warn b { color: var(--hp-open); } + .bindharow { + display: flex; + align-items: center; + gap: var(--sp-5); + flex-wrap: wrap; + } + .bindharow .entcheck { opacity: 0.9; } + .ctrlchips { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin: var(--sp-2) 0; } + .ctrlchip { + display: inline-flex; align-items: center; gap: var(--sp-2); + background: var(--hp-accent); color: var(--text-primary-color, #fff); + border-radius: var(--rad-l); padding: var(--sp-2) var(--sp-4); font-size: var(--fs-s); + } + .ctrlchip ha-icon { --mdc-icon-size: 14px; cursor: pointer; } + .ctrllist { display: flex; flex-direction: column; gap: var(--sp-1); margin-top: var(--sp-2); } + .ctrlopt { + display: flex; align-items: center; gap: var(--sp-4); text-align: left; + border: 0; background: transparent; color: var(--hp-txt); + padding: var(--sp-3) var(--sp-4); border-radius: var(--rad-s); cursor: pointer; font-family: inherit; font-size: var(--fs-m); + } + :host([data-pointer-hover]) .ctrlopt:hover { + background: var(--secondary-background-color, rgba(128,128,128,0.15)); + } + .ctrlopt .sub { color: var(--hp-muted); font-size: var(--fs-s); margin-left: auto; } + .ctrlopt ha-icon { --mdc-icon-size: 16px; } + .ctrlstates { display: flex; flex-direction: column; gap: var(--sp-2); } + .ctrlstate { display: inline-flex; align-items: center; gap: var(--sp-3); color: var(--hp-muted); } + .ctrlstate.on { color: var(--hp-txt); } + .ctrlstate ha-icon { --mdc-icon-size: 15px; } + .cardpreview { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--sp-2); + margin: var(--sp-4) 0 var(--sp-1); + padding: 10px; + border: 1px dashed var(--hp-muted); + border-radius: var(--rad-m); + } + .cardpreview .cpname { font-weight: 700; letter-spacing: 0.04em; } + .cardpreview .cpmeta { + display: inline-flex; + align-items: center; + gap: 0.3em; + font-weight: 600; + opacity: 0.85; + } + .cardpreview .cpmeta ha-icon { --mdc-icon-size: 1.05em; } + .iconauto { + display: flex; + align-items: center; + gap: var(--sp-3); + font-size: var(--fs-s); + margin: var(--sp-2) 0 0; + } + .iconauto ha-icon { --mdc-icon-size: 18px; } + .iconauto span { flex: 1; } + .iconauto .btn { min-height: 32px; padding: 0 var(--sp-3); } + hp-dialog .dfill { + display: inline-flex; + align-items: center; + gap: var(--sp-3); + cursor: pointer; + } + .wallthick-dlg { + position: absolute; + z-index: 40; + min-width: 200px; + transform: translate(-50%, 8px); + padding: 10px 12px; + border-radius: 10px; + background: var(--card-background-color, #fff); + box-shadow: 0 8px 28px rgba(0,0,0,.22); + display: flex; + flex-direction: column; + gap: 8px; + pointer-events: auto; + } + .wallthick-dlg .row { + display: flex; + align-items: center; + gap: 8px; + } + .wallthick-dlg input[type="number"] { + width: 5.5em; + padding: 4px 6px; + border: 1px solid var(--divider-color, #ccc); + border-radius: 6px; + background: var(--input-fill, transparent); + color: var(--primary-text-color); + } + .areasel, + .namein { + background: var(--hp-bg); + border: 1px solid var(--hp-line); + color: var(--hp-txt); + border-radius: var(--rad-s); + padding: var(--sp-3) var(--sp-4); + font-size: var(--fs-m); + font-family: inherit; + } + .namein { + width: 130px; + } + .habindingbanner { + display: flex; + align-items: center; + gap: var(--sp-3); + padding: var(--sp-3) var(--sp-4); + margin-bottom: var(--sp-4); + border: 1px solid var(--warning-color, #ff9800); + border-radius: var(--rad-m); + background: color-mix(in srgb, var(--warning-color, #ff9800) 12%, transparent); + color: var(--primary-text-color, #f1f3f6); + } + .habindingbanner > span { flex: 1; min-width: 0; } + .habindingbanner > ha-icon { color: var(--warning-color, #ff9800); flex: 0 0 auto; } + .habindingbanner.limited { + border-color: var(--secondary-text-color, #9aa0aa); + background: color-mix(in srgb, var(--secondary-text-color, #9aa0aa) 10%, transparent); + } + .habindingbanner.limited > ha-icon { color: var(--secondary-text-color, #9aa0aa); } + .srcrow { + display: flex; + align-items: center; + gap: var(--sp-4); + font-size: var(--fs-m); + cursor: pointer; + padding: var(--sp-1) 0; + } + .dispsection { + margin-top: var(--sp-5) !important; + padding-top: var(--sp-4); + border-top: 1px solid var(--hp-line); + font-weight: 600; + color: var(--hp-txt) !important; + } + .colorrow { + display: flex; + align-items: center; + gap: var(--sp-4); + } + .gsrow > hp-color-opacity { + min-width: min(100%, 210px); + justify-content: space-between; + } + .ripple-colorrow > hp-color-opacity { + width: 100%; + justify-content: space-between; + } + .ripple-sizerow > .opl { + min-width: 0; + } + .colorrow input[type='range'] { flex: 1; } + .colorrow .tempin { width: 70px; flex: none; } + /* beat the generic hp-dialog .body .namein { width:100% } rule */ + hp-dialog .body .temprange .tempin { width: 56px; flex: none; padding: var(--sp-2) var(--sp-3); } + hp-dialog .body .colorrow .tempin { width: 72px; flex: none; } + .srcrow { flex-wrap: nowrap; } + /* native HA controls (rendered only when the HA frontend defines them; + old HA and the smoke env keep the plain inputs). ha-switch is taller + than a checkbox - cap its footprint so .srcrow keeps its rhythm. */ + .srcrow ha-switch { flex: none; } + .colorrow ha-slider { flex: 1; min-width: 0; } + .srcrow > span:first-of-type { white-space: nowrap; } + .colorrow .opl { color: var(--hp-muted); font-size: var(--fs-s); } + .colorrow .opv { font-size: var(--fs-s); min-width: 34px; text-align: right; } + .markerlightgroup { + min-width: 0; + margin: var(--sp-5) 0 0; + padding: var(--sp-4); + border: 1px solid var(--hp-line); + border-radius: var(--rad-m); + } + .markerlightgroup legend { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: var(--sp-1); + padding: 0 var(--sp-2); + color: var(--hp-txt); + font-weight: 600; + } + .markerlightgroup legend > span { min-width: 0; overflow-wrap: anywhere; } + .markerlightgroup[disabled] > :not(legend) { opacity: .62; } + .markerhelpfield { margin-top: var(--sp-4); } + .markerhelplabel { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: var(--sp-1); + margin-bottom: var(--sp-1); + } + .markerhelplabel > label { min-width: 0; overflow-wrap: anywhere; } + .markerradios { display: grid; gap: var(--sp-1); min-width: 0; } + .markerlightgroup .srcrow > span:first-of-type { + min-width: 0; + white-space: normal; + overflow-wrap: anywhere; + } + .markerglowvalue { margin: var(--sp-3) 0; flex-wrap: wrap; } + .markerglowvalue hp-color-opacity { flex: none; } + .markerlightdisabled { + display: flex; + align-items: center; + gap: var(--sp-2); + margin-top: var(--sp-2) !important; + } + .markerlightdisabled ha-icon { --mdc-icon-size: 18px; flex: none; } + .markerbadgetechnical { + min-width: 0; + margin: var(--sp-1) 0 var(--sp-2) !important; + overflow-wrap: anywhere; + } + .markerbadgetechnical code { white-space: normal; } + .planrow { + display: flex; + align-items: center; + gap: 10px; + } + /* the "already uploaded" picker: a plan is never deleted for being + unreferenced, so it has to be findable again */ + .savedplans { + display: flex; + flex-direction: column; + gap: var(--sp-3); + max-height: 240px; + overflow: auto; + margin: var(--sp-3) 0 var(--sp-1); + padding: var(--sp-3); + border: 1px solid var(--hp-line); + border-radius: var(--rad-m); + background: var(--hp-bg2, rgba(255, 255, 255, 0.03)); + /* The same collapse that ate .candlist (v1.53.1): a scroll box is a + flex item whose automatic minimum size is ZERO (overflow != visible), + so inside hp-dialog .body — a flex column taller than its 66vh cap — + it shrank to a 14px sliver: the rows were in the DOM, the owner saw + a thin rounded stripe under the "Already uploaded" button. Don't + shrink, and keep a floor even when the box is empty or loading. */ + flex: 0 0 auto; + min-height: 2.6em; + } + .savedplan { + display: flex; + align-items: center; + gap: 10px; + } + .savedplan.cur { outline: 1px solid var(--hp-accent); border-radius: var(--rad-s); } + .savedplan img { + width: 56px; + height: 40px; + object-fit: contain; + border: 1px solid var(--hp-line); + border-radius: var(--rad-s); + background: #fff; + flex: none; + } + .savedmeta { display: flex; flex-direction: column; min-width: 0; flex: 1; } + .savedmeta b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .savedmeta .muted { font-size: var(--fs-s); } + .savedplan .btn.danger ha-icon { color: #f25a4a; } + .savedplan .btn[disabled] { opacity: 0.4; pointer-events: none; } + .planprev { + max-width: 120px; + max-height: 70px; + border: 1px solid var(--hp-line); + border-radius: var(--rad-s); + background: #fff; + } + .planname { + font-size: var(--fs-m); + max-width: 150px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .planname.muted { + color: var(--hp-muted); + } + .filebtn { + cursor: pointer; + } + .fileupload { display: inline-flex; min-width: 0; } + .fileupload > input { display: none; } + .btn.danger { + border-color: #b3402a; + color: #ff7a5c; + } + hp-dialog .row .spacer { + flex: 1; + } + hp-dialog .body { + max-height: 66vh; + overflow-y: auto; + } + .descin { + width: 100%; + box-sizing: border-box; + background: var(--hp-bg); + border: 1px solid var(--hp-line); + color: var(--hp-txt); + border-radius: var(--rad-s); + padding: var(--sp-3) var(--sp-4); + font-size: var(--fs-m); + font-family: inherit; + resize: vertical; + /* flex column of the dialog body squeezes textareas — keep a usable height */ + min-height: 92px; + flex-shrink: 0; + line-height: 1.35; + } + .bindsel { + display: flex; + flex-direction: column; + gap: var(--sp-3); + border: 1px solid var(--hp-line); + border-radius: var(--rad-m); + padding: var(--sp-4); + } + .bindsel .opt { + display: flex; + align-items: center; + gap: var(--sp-3); + border: 1px solid var(--hp-line); + background: transparent; + color: var(--hp-txt); + border-radius: var(--rad-s); + padding: var(--sp-3) var(--sp-4); + cursor: pointer; + font-size: var(--fs-m); + font-family: inherit; + } + .bindsel .opt.on { + background: var(--hp-accent); + color: var(--text-primary-color, #fff); + border-color: var(--hp-accent); + } + .curbind { + display: flex; + align-items: center; + gap: var(--sp-3); + font-size: var(--fs-m); + color: var(--hp-txt); + flex-wrap: wrap; + } + .curbind .ref { + color: var(--hp-muted); + font-size: var(--fs-s); + } + .vacbox .vacbtns { display: flex; gap: var(--sp-4); margin: var(--sp-3) 0; flex-wrap: wrap; } + .vacdiag { display: grid; gap: 4px; margin-bottom: var(--sp-3); } + .vacdiag > div { display: flex; justify-content: space-between; gap: var(--sp-5); } + .vacdiag > div > span { color: var(--secondary-text-color); } + .vacdiag > div > b { text-align: right; overflow-wrap: anywhere; } + .vacpicker { margin: var(--sp-3) 0; } + .vacsource-warning { display: grid; gap: 8px; } + .vacsource-warning .btn { justify-self: start; } + .vacpicker > summary { display: inline-flex; width: auto; list-style: none; cursor: pointer; } + .vacpicker > summary::-webkit-details-marker { display: none; } + .vacsource-list { display: grid; gap: 6px; margin-top: 8px; } + .vacsource-list details { padding: 6px 0 0; } + .vacsource-list details > summary { cursor: pointer; font-weight: 600; } + .vacsource { display: flex; align-items: center; justify-content: space-between; gap: 12px; + width: 100%; min-width: 0; padding: 9px 10px; border: 1px solid var(--divider-color); + border-radius: 10px; color: var(--primary-text-color); background: var(--secondary-background-color); + text-align: left; cursor: pointer; } + .vacsource.on { border-color: var(--accent-color); box-shadow: inset 3px 0 var(--accent-color); } + .vacsource > span:first-child { min-width: 0; display: grid; gap: 2px; } + .vacsource small { color: var(--secondary-text-color); overflow-wrap: anywhere; } + .vacsource-meta { color: var(--secondary-text-color); text-align: right; font-size: 0.82em; } + .vacxcme pre { margin: 8px 0 0; white-space: pre-wrap; user-select: text; } + .vacfit { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + z-index: 12; + overflow: visible; + touch-action: none; + cursor: grab; + /* the devlayer is pointer-events: none and every child opts back in — + without this line real clicks flew straight through the overlay + (owner: «уголки не кликабельны»; synthetic smoke events bypass + hit-testing, which is why they lied) */ + pointer-events: auto; + } + .vacfit:active { cursor: grabbing; } + .vacfit polygon { + fill: color-mix(in srgb, var(--hp-accent) 16%, transparent); + stroke: var(--hp-accent); + stroke-width: 2; + vector-effect: non-scaling-stroke; + stroke-dasharray: 6 4; + } + .vacfit text { + fill: var(--hp-accent); + font-size: 26px; + text-anchor: middle; + dominant-baseline: middle; + pointer-events: none; + user-select: none; + } + /* ---- the furniture palette (docs/FURNITURE.md §3) ------------------- */ + .furnpalette { + display: flex; + flex-direction: column; + max-height: 38vh; + border-top: 1px solid var(--hp-border, rgba(255, 255, 255, 0.12)); + background: var(--card-background-color, var(--hp-bg)); + font-size: 0.85em; + } + .furnhd { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + font-weight: 600; + opacity: 0.9; + } + .furnhd .spacer { flex: 1; } + .furnbody { + overflow: auto; + padding: 0 8px 6px; + } + .furngroup { + margin: 6px 0 3px; + font-size: 0.85em; + text-transform: uppercase; + letter-spacing: 0.04em; + opacity: 0.6; + } + .furnrow { + display: flex; + flex-wrap: wrap; + gap: 4px; + } + .furnitem { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + width: 74px; + padding: 4px 2px; + border: 1px solid transparent; + border-radius: 8px; + background: rgba(127, 127, 127, 0.08); + color: inherit; + font: inherit; + font-size: 0.8em; + line-height: 1.15; + text-align: center; + cursor: pointer; + } + :host([data-pointer-hover]) .furnitem:hover { background: rgba(127, 127, 127, 0.18); } + .furnitem.on { + border-color: var(--hp-accent); + background: rgba(38, 166, 154, 0.18); + } + .furnprev { + width: 40px; + height: 40px; + color: var(--primary-text-color, currentColor); + } + .furnsize { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; + padding: 6px 8px; + border-top: 1px solid var(--hp-border, rgba(255, 255, 255, 0.12)); + } + .furnsize label { + display: flex; + align-items: baseline; + gap: 3px; + opacity: 0.8; + } + .furnsize .furnunit { opacity: 0.6; font-size: 0.85em; } + .furnsize input { + width: 5.5em; + padding: 3px 5px; + } + .furnhint { opacity: 0.6; } + .vacfitdot { fill: var(--hp-accent); pointer-events: none; } + /* hit target: invisible and finger-sized; .vacfitknob is the visible bead */ + .vacfithandle { + fill: transparent; + stroke: none; + cursor: nwse-resize; + } + .vacfitknob { + fill: var(--hp-bg); + stroke: var(--hp-accent); + stroke-width: 2; + vector-effect: non-scaling-stroke; + pointer-events: none; + } + .vaccalbar { + position: fixed; + left: 50%; + bottom: 24px; + transform: translateX(-50%); + display: flex; + gap: var(--sp-5); + align-items: center; + background: var(--hp-bg); + color: var(--hp-txt); + border: 1px solid var(--hp-accent); + border-radius: var(--rad-l); + padding: 10px var(--sp-5); + z-index: 60; + box-shadow: var(--shadow-2); + } + .candlist { + max-height: 160px; + overflow-y: auto; + border-top: 1px solid var(--hp-line); + /* A scrollable box is a flex item that HAPPILY collapses: inside the + dialog body (a flex column) this list rendered its rows into a 1px + sliver — the DOM had 26 candidates and the user saw nothing. In the + binding dropdown it sits inside .droppanel (block context) and never + showed the bug. Field report, 2026-07-30. */ + flex: 0 0 auto; + min-height: 2.6em; + } + .cand { + display: flex; + justify-content: space-between; + gap: var(--sp-4); + padding: var(--sp-3) var(--sp-4); + cursor: pointer; + border-radius: var(--rad-s); + font-size: var(--fs-m); + } + :host([data-pointer-hover]) .cand:hover { + background: rgba(127, 127, 127, 0.15); + } + .cand.sel { + background: var(--hp-accent); + color: var(--text-primary-color, #fff); + } + .cand .cs { + color: var(--hp-muted); + font-size: var(--fs-s); + white-space: nowrap; + } + .opening-entity-candidate { + width: 100%; + border: 0; + background: transparent; + color: inherit; + font: inherit; + text-align: left; + } + .opening-entity-candidate.sel { + background: var(--hp-accent); + color: var(--text-primary-color, #fff); + } + .opening-entity-empty { cursor: default; } + .cand.sel .cs { + color: var(--text-primary-color, #fff); + opacity: 0.85; + } + .cand.muted { + color: var(--hp-muted); + cursor: default; + } + .pdfedit { + display: flex; + flex-wrap: wrap; + gap: var(--sp-3); + align-items: center; + } + .pdftag { + display: inline-flex; + align-items: center; + gap: var(--sp-2); + border: 1px solid var(--hp-line); + border-radius: var(--rad-s); + padding: var(--sp-2) var(--sp-3); + font-size: var(--fs-s); + } + .pdftag a { + color: var(--hp-txt); + text-decoration: none; + max-width: 150px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .pdftag .x { + --mdc-icon-size: 15px; + cursor: pointer; + color: var(--hp-muted); + } + :host([data-pointer-hover]) .pdftag .x:hover { + color: #ff7a5c; + } + .entlist { + display: flex; + flex-direction: column; + gap: var(--sp-2); + margin-bottom: 10px; + } + .entrow { + display: flex; + align-items: center; + gap: var(--sp-4); + padding: var(--sp-3) var(--sp-4); + border-radius: var(--rad-m); + background: var(--secondary-background-color, rgba(128, 128, 128, 0.12)); + } + .entrow ha-icon { --mdc-icon-size: 20px; color: var(--hp-muted); } + .entrow.on ha-icon { color: var(--hp-accent); } + .entrow .en { flex: 1; font-size: var(--fs-m); } + .entrow .ev { font-size: var(--fs-m); color: var(--hp-muted); } + .entbtn { + min-width: 74px; + min-height: 32px; + padding: var(--sp-2) var(--sp-5); + border: 1px solid var(--hp-muted); + border-radius: 999px; + background: transparent; + color: var(--hp-txt); + font: inherit; + font-size: var(--fs-m); + cursor: pointer; + } + .entbtn.on { + background: var(--hp-accent); + border-color: var(--hp-accent); + color: var(--text-primary-color, #fff); + font-weight: 600; + } + .inforow { + display: flex; + gap: 10px; + font-size: var(--fs-m); + margin: var(--sp-2) 0; + } + .inforow .k { + color: var(--hp-muted); + min-width: 84px; + } + .inforow a { + color: var(--hp-accent); + word-break: break-all; + } + .infodesc { + font-size: var(--fs-m); + white-space: pre-wrap; + margin-top: var(--sp-3); + } + .infodesc.muted { + color: var(--hp-muted); + } + .pdflist { + display: flex; + flex-direction: column; + gap: var(--sp-2); + } + .pdf { + display: inline-flex; + align-items: center; + gap: var(--sp-2); + color: var(--hp-accent); + text-decoration: none; + } + ha-icon-picker { + display: block; + } + .floorrow { + display: flex; + align-items: center; + gap: var(--sp-4); + padding: var(--sp-3) var(--sp-2); + font-size: var(--fs-m); + cursor: pointer; + } + .floorrow .floorlvl { + color: var(--hp-muted); + font-size: var(--fs-s); + border: 1px solid var(--hp-line); + border-radius: var(--rad-s); + padding: 0 var(--sp-3); + } + .rtest { + display: flex; + align-items: center; + gap: var(--sp-4); + margin-bottom: var(--sp-4); + } + .rtest .namein { flex: 1; } + .rtest ha-icon { color: var(--hp-accent); } + .rtesticon { font-size: var(--fs-s); color: var(--hp-muted); } + .rrow { + display: flex; + align-items: center; + gap: var(--sp-3); + margin: var(--sp-1) 0; + } + .rrow .rpat { flex: 2; } + .rrow .ricon { flex: 1.4; } + .rrow .rpat.bad { border-color: #ff7a5c; } + .rrow .rprev { --mdc-icon-size: 18px; color: var(--hp-txt); min-width: 18px; } + .rrow .ract { + --mdc-icon-size: 16px; + color: var(--hp-muted); + cursor: pointer; + } + :host([data-pointer-hover]) .rrow .ract:hover { color: var(--hp-txt); } + :host([data-pointer-hover]) .rrow .ract.del:hover { color: #ff7a5c; } + .gsrow .gsl { + min-width: 150px; + font-size: var(--fs-m); + color: var(--hp-muted); + } + .optimize-live { + display: grid; + justify-items: start; + gap: var(--sp-2); + margin-bottom: var(--sp-3); + } + .optimize-live .alignmsg, .optimize-live .rhint { margin-bottom: 0; } + .optimize-cleanup { min-height: 44px; } + .optimize-selected { color: var(--hp-txt); } + .optimize-details { + margin-top: var(--sp-3); + color: var(--hp-muted); + font-size: var(--fs-s); + overflow-wrap: anywhere; + } + .optimize-details > summary { + width: fit-content; + color: var(--hp-txt); + cursor: pointer; + font-weight: 600; + } + .optimize-details > summary:focus-visible { + outline: 2px solid var(--hp-accent); + outline-offset: 3px; + border-radius: var(--rad-s); + } + .optimize-details ul { margin: var(--sp-3) 0; padding-inline-start: 22px; } + .optimize-details li + li { margin-top: var(--sp-1); } + .btn.alignall { width: 100%; justify-content: center; } + .backupactions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--sp-3); + } + .backupactions .btn { justify-content: center; min-width: 0; } + .backupupload { display: inline-flex; min-width: 0; } + .backupupload > .btn { width: 100%; justify-content: center; } + .backupupload input { display: none; } + .backupbody { min-width: 0; } + .backupplanonly { margin-inline-start: var(--sp-4) !important; align-items: flex-start !important; } + .backupplanonly > span:first-of-type { display: grid; gap: 2px; white-space: normal; } + .backupplanonly small { color: var(--secondary-text-color); line-height: 1.35; } + .backupplanonlystatus { color: var(--hp-accent) !important; font-weight: 700; } + .backupfile, .backupsummary, .backupcontent { + display: flex; + flex-direction: column; + min-width: 0; + gap: var(--sp-1); + } + .backupfile b, .backupcontent span { + overflow-wrap: anywhere; + } + .backupfile span, .backupsummary span, .backupcontent span { + color: var(--hp-muted); + font-size: var(--fs-s); + } + .backupcounts { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--sp-2) var(--sp-4); + font-size: var(--fs-s); + } + .backupwarn, .backuperror { + border-radius: var(--rad-s); + padding: var(--sp-3); + font-size: var(--fs-s); + line-height: 1.45; + overflow-wrap: anywhere; + } + .backupwarn { background: color-mix(in srgb, var(--hp-accent) 12%, transparent); } + .backuperror { background: rgba(179, 64, 42, .16); color: #ff7a5c; } + .backupdetails { + border: 1px solid var(--hp-line); + border-radius: var(--rad-s); + padding: var(--sp-2) var(--sp-3); + font-size: var(--fs-s); + } + .backupdetails summary { cursor: pointer; font-weight: 700; } + .backupdetails > div { display: grid; gap: var(--sp-1); padding-block-start: var(--sp-2); } + .backupdetails code { overflow-wrap: anywhere; color: var(--hp-muted); } + .backupchoices { + display: flex; + flex-direction: column; + gap: var(--sp-2); + border: 1px solid var(--hp-line); + border-radius: var(--rad-s); + min-width: 0; + } + .backupchoices label { margin: 0 !important; display: flex; gap: var(--sp-2); } + .backupconfirm { align-items: flex-start !important; } + .backupconfirm > span:first-of-type { + min-width: 0; + white-space: normal; + overflow-wrap: anywhere; + } + @media (max-width: 520px) { + .backupactions, .backupcounts { grid-template-columns: 1fr; } + } + .aboutver { + font-size: var(--fs-s); + color: var(--hp-muted); + margin: var(--sp-2) 0 var(--sp-3); + } + .aboutlink { + display: flex; + align-items: center; + gap: var(--sp-3); + width: fit-content; + color: var(--hp-accent); + text-decoration: none; + font-size: var(--fs-m); + padding: var(--sp-1) 0; + } + :host([data-pointer-hover]) .aboutlink:hover { text-decoration: underline; } + .aboutlink ha-icon { --mdc-icon-size: 18px; line-height: 1; } + hp-dialog .body { + padding: var(--sp-5) var(--sp-6); + display: flex; + flex-direction: column; + gap: var(--sp-3); + } + hp-dialog .tapconfirm-body { + min-width: 0; + overflow-x: hidden; + } + hp-dialog .tapconfirm-body p { + max-width: 100%; + min-width: 0; + margin: 0; + overflow-wrap: anywhere; + white-space: normal; + } + hp-dialog .tapconfirm-line { + color: var(--hp-muted); + } + hp-dialog .body label { + font-size: var(--fs-s); + color: var(--hp-muted); + margin-top: var(--sp-3); + } + hp-dialog .body .namein, + hp-dialog .body .areasel { + width: 100%; + box-sizing: border-box; + } + /* Room settings contains long radio labels, two source pickers and live + previews, so it deliberately uses hp-dialog's medium width. Keep its + content shrinkable as well: the generic srcrow rule is nowrap because + compact switch rows need it, but here that created a horizontal scroll + box even on a wide desktop viewport. */ + hp-dialog.roomdialog .body { + min-width: 0; + overflow-x: hidden; + } + hp-dialog.roomdialog .body > * { + min-width: 0; + max-width: 100%; + box-sizing: border-box; + } + hp-dialog.roomdialog .srcrow { + min-width: 0; + align-items: flex-start; + } + hp-dialog.roomdialog .srcrow > span:first-of-type { + min-width: 0; + white-space: normal; + overflow-wrap: anywhere; + line-height: 1.35; + } + hp-dialog.roomdialog .dropbtn > b, + hp-dialog.roomdialog .dropbtn > .ref { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + hp-dialog .row { + display: flex; + justify-content: flex-end; + gap: var(--sp-4); + width: 100%; + min-width: 0; + box-sizing: border-box; + padding: var(--sp-5) var(--sp-6); + border-top: 1px solid var(--hp-line); + } + /* Stable destructive/commit footer contract. A flex spacer cannot react + when translated labels no longer fit: justify-content then overflows + the destructive button through the left inset. Two real groups wrap + as units instead — destructive actions stay left, while Cancel/Save + move together to a right-aligned second row when necessary. */ + hp-dialog .row.dialog-action-footer { + align-items: center; + justify-content: flex-start; + flex-wrap: wrap; + row-gap: var(--sp-4); + } + hp-dialog .dialog-action-group { + display: flex; + flex: 0 1 auto; + flex-wrap: wrap; + align-items: center; + gap: var(--sp-4); + max-width: 100%; + min-width: 0; + } + hp-dialog .dialog-action-group .btn { + flex: 0 0 auto; + min-height: 44px; + } + hp-dialog .dialog-action-danger { + margin-right: auto; + } + hp-dialog .dialog-action-commit { + margin-left: auto; + justify-content: flex-end; + } + hp-dialog .row.markerfooter { + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + } + hp-dialog .row.roomfooter { + align-items: center; + flex-wrap: wrap; + } + /* Device info can have Edit + Open in HA + Close. It uses a wide dialog; + wrapping remains as a phone fallback, but without a flex spacer (which + used to strand Edit alone on the first line). */ + hp-dialog .row.infofooter { + align-items: center; + justify-content: flex-start; + flex-wrap: wrap; + gap: var(--sp-3); + } + hp-dialog .row.infofooter .btn { + flex-shrink: 0; + } + hp-dialog .row.infofooter .infofooter-close { + margin-left: auto; + } + @media (max-width: 480px) { + hp-dialog .row.infofooter { + padding: var(--sp-4) var(--sp-5); + } + } + .markeractions, + .markersaveactions { + display: flex; + align-items: center; + gap: var(--sp-4); + } + .markeractions:empty { display: none; } + .markersaveactions { margin-left: auto; } + .device-inbox { + display: flex; + flex-direction: column; + gap: var(--sp-4); + padding: var(--sp-5) var(--sp-6); + min-width: 0; + } + .device-inbox-dialog { --hp-dialog-wide-width: 920px; } + .device-inbox-head { + display: grid; + grid-template-columns: minmax(180px, 1fr) auto; + align-items: center; + gap: var(--sp-4); + } + .device-inbox-search { + width: 100%; + min-width: 0; + box-sizing: border-box; + border: 1px solid var(--hp-line); + border-radius: var(--rad-m); + background: transparent; + color: var(--hp-txt); + font: inherit; + padding: 11px 14px; + } + .device-inbox-tabs { + display: flex; + gap: var(--sp-2); + overflow-x: auto; + scrollbar-width: thin; + padding-bottom: var(--sp-1); + } + .device-inbox-tabs button { + flex: 0 0 auto; + border: 1px solid var(--hp-line); + border-radius: 999px; + background: transparent; + color: var(--hp-txt); + font: inherit; + padding: 8px 12px; + cursor: pointer; + } + .device-inbox-tabs button.on { + border-color: var(--hp-accent); + background: color-mix(in srgb, var(--hp-accent) 18%, transparent); + } + .device-inbox-tabs button span { color: var(--hp-muted); margin-inline-start: 4px; } + .device-inbox-filters { + display: flex; + flex-wrap: wrap; + gap: var(--sp-4) var(--sp-6); + color: var(--hp-muted); + } + .device-inbox-filters label { + display: inline-flex; + align-items: center; + gap: var(--sp-2); + cursor: pointer; + } + .device-inbox-results { display: grid; gap: var(--sp-3); min-width: 0; } + .device-inbox-row { + display: grid; + grid-template-columns: 42px minmax(180px, 1fr) minmax(180px, auto); + align-items: center; + gap: var(--sp-4); + min-width: 0; + border: 1px solid var(--hp-line); + border-radius: var(--rad-m); + padding: var(--sp-4); + background: color-mix(in srgb, var(--hp-txt) 3%, transparent); + } + .device-inbox-icon { --mdc-icon-size: 28px; color: var(--hp-txt); justify-self: center; } + .device-inbox-copy { min-width: 0; } + .device-inbox-name { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2); } + .device-inbox-new { + border-radius: 999px; + background: var(--hp-accent); + color: var(--text-primary-color, #fff); + font-size: var(--fs-s); + padding: 2px 7px; + } + .device-inbox-meta, + .device-inbox-reason, + .device-inbox-copy code { + display: block; + color: var(--hp-muted); + font-size: var(--fs-s); + overflow-wrap: anywhere; + white-space: normal; + } + .device-inbox-status { color: var(--error-color, #db4437); margin-inline-start: var(--sp-2); } + .device-inbox-actions { + display: flex; + justify-content: flex-end; + align-items: center; + flex-wrap: wrap; + gap: var(--sp-2); + } + .device-inbox-actions .btn { min-height: 36px; padding: 7px 10px; } + .device-inbox-menu { position: relative; } + .device-inbox-menu summary { list-style: none; cursor: pointer; } + .device-inbox-menu summary::-webkit-details-marker { display: none; } + .device-inbox-menu-items { + position: absolute; + z-index: 2; + inset-inline-end: 0; + top: calc(100% + var(--sp-1)); + display: grid; + gap: var(--sp-1); + min-width: 180px; + padding: var(--sp-2); + border: 1px solid var(--hp-line); + border-radius: var(--rad-m); + background: var(--hp-panel, var(--card-background-color, #fff)); + box-shadow: 0 8px 24px rgba(0, 0, 0, .22); + } + .device-inbox-menu-items .btn { justify-content: flex-start; width: 100%; } + .device-inbox-empty { color: var(--hp-muted); text-align: center; padding: var(--sp-8); } + .device-inbox-more { align-self: center; } + @media (max-width: 680px) { + .device-inbox { padding: var(--sp-4); } + .device-inbox-head { grid-template-columns: minmax(0, 1fr); } + .device-inbox-head .btn { justify-self: stretch; } + .device-inbox-tabs { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + overflow-x: visible; + } + .device-inbox-tabs button { + min-width: 0; + overflow-wrap: anywhere; + } + .device-inbox-row { grid-template-columns: 36px minmax(0, 1fr); } + .device-inbox-actions { grid-column: 1 / -1; justify-content: flex-start; } + } +`];var Wd={"editor.context_actions":"Actions: {object}","editor.tool_options":"Tool options: {tool}","editor.palette":"Palette: {tool}","editor.open_group":"Tool group: {group}","editor.group_active":"{group} — active: {item}","editor.disabled_action":"{action} is unavailable: {reason}","editor.loading":"Loading editor…","editor.loading_aria":"The plan editor is loading","editor.load_failed":"Could not load the editor.","editor.refresh_advice":"Refresh the page and try again.","color_picker.title":"Color picker","color_picker.hue":"Hue","color_picker.saturation":"Saturation","color_picker.value":"Brightness","color_picker.hex":"Hex color","color_picker.invalid_hex":"Enter a 3- or 6-digit hex color","btn.properties":"Properties","btn.keep_as_walls":"Keep as walls","btn.delete_room_keep_walls":"Delete room, keep walls","btn.delete_room_with_walls":"Delete room and walls","title.markup_select":"Select and edit walls, columns and saved outlines","title.markup_column":"Column: click a grid point to place a square column","markup.partition":"Partition","markup.column":"Column","markup.hint_column":"click a grid point to place a square column","history.draft_segment":"Add room-draft segment","history.draft_merge":"Join unfinished room outlines","history.draft_segment_delete":"Remove room-draft segment","history.partition_add":"Add partition","history.column_add":"Add column","history.physical_edit":"Change physical object","history.physical_delete":"Delete physical object","history.physical_move":"Move physical object","history.contour_to_partitions":"Convert contour to closed walls","history.wall_chain_finish":"Finish wall chain","history.wall_face_batch":"Create rooms from walls","toast.column_duplicate":"A column with the same centre and outer size already exists","confirm.delete_draft":"Delete the whole unfinished room contour?","confirm.delete_draft_segment":"Delete this draft segment? The remaining contour may split in two.","physical.partition_properties":"Partition properties","physical.column_properties":"Column properties","physical.draft_properties":"Draft segment properties","physical.shape":"Shape","physical.square":"Square","physical.circle":"Circle","physical.diameter":"Diameter","physical.side":"Side","physical.rotation":"Rotation angle","physical.length":"Length","physical.allowed_range":"Allowed: {min}–{max} {unit}","physical.delete_segment":"Delete segment","physical.delete_draft":"Delete entire outline","physical.column_size_title":"Outer side of the square column placed by the click (1–150 cm).","card.title":"House plan","count.devices":"{n} dev.","empty.no_spaces":"No spaces yet.","empty.add_first":"Add the first space and upload a floor plan.","empty.install":'Install the House Plan integration and add it in "Devices & services".',"btn.add_space":"Add space","btn.cancel":"Cancel","btn.save":"Save","btn.close":"Close","btn.delete":"Delete","btn.edit":"Edit","btn.open_in_ha":"Open in HA","btn.reset":"Reset","btn.attach":"Attach…","btn.upload":"Upload…","btn.replace":"Replace…","title.zoom_in":"Zoom in","title.zoom_out":"Zoom out","title.zoom_fit":"Fit all","title.add_device":"Add a device to the plan","title.show_all":"Show hidden and disabled devices as service markers (this tab only)","title.markup":"Room markup: grid, lines, outlines","title.configure_space":"Configure space","title.add_space":"Add space","title.markup_add":"Walls: draw a continuous chain; Shift locks to 45°. Changing tool or leaving the editor finishes it as independent walls","title.markup_merge":"Merge: click one room, then the neighbour it shares a wall with","title.markup_split":"Split a room: click the room, then two points on its walls","title.markup_delroom":"Delete room: click inside a room and confirm","title.need_plan":"Upload a floor-plan image","markup.add":"Walls","markup.merge":"Merge","markup.split":"Split","markup.resize":"Resize","title.markup_resize":"Resize rooms: drag an available horizontal or vertical wall handle","markup.hint_resize":"drag an available wall handle · the wall stops at the first unsafe position · Esc cancels · Ctrl+Z — undo","resize.disabled.diagonal":"This wall is angled; Resize supports only horizontal and vertical walls","resize.disabled.side-angle":"An adjoining wall is angled; Resize requires both adjoining walls to meet this wall at right angles","resize.disabled.duplicate-physical-wall":"A separate partition, unfinished outline, or column overlaps this wall; remove or move it before resizing","resize.disabled.partial-shared":"A neighbouring room uses only part of this wall, so it cannot be moved safely as one shared wall","resize.disabled.unequal-shared":"The neighbouring room uses different endpoints or wall length, so the walls cannot move together safely","resize.disabled.multiple-rooms":"Moving this wall would affect more than two rooms","resize.disabled.thickness-conflict":"The wall thickness cannot be preserved safely","resize.disabled.opening-conflict":"An opening on this wall cannot be moved safely","resize.disabled.invalid-geometry":"This wall cannot be resized without changing the plan structure","resize.limit_stopped":"Wall stopped: the next step would break a junction limit","resize.commit_failed":"Resize was cancelled because the final plan did not pass the geometry check","resize.preview_failed":"Resize stopped at the last safe position because the plan geometry could not be preserved","markup.opening":"Opening","markup.delete_room":"Delete room","history.undo":"Undo","history.redo":"Redo","history.undo_named":"Undo: {name} (Ctrl+Z)","history.redo_named":"Redo: {name} (Ctrl+Shift+Z / Ctrl+Y)","history.undo_empty":"Nothing to undo","history.redo_empty":"Nothing to redo","history.undone":"Undone: {name}","history.redone":"Redone: {name}","history.add_room":"Create room","history.split_room":"Split room","history.merge_rooms":"Merge rooms","history.resize_room":"Resize room","history.wall_thickness":"Change wall thickness","history.add_opening":"Add door or window opening","history.edit_opening":"Edit opening","history.move_opening":"Move opening","history.delete_opening":"Delete opening","history.delete_room":"Delete room","history.delete_room_keep_walls":"Delete room, keep walls","history.delete_room_with_walls":"Delete room and walls","history.decor_add":"Add decor object","history.decor_edit":"Edit decor object","history.decor_move":"Move decor object","history.decor_transform":"Transform decor object","history.decor_delete":"Delete decor object","history.backdrop_transform":"Transform plan backdrop","title.markup_opening":"Openings: click a wall to place, click an opening to edit","opening.new":"New opening","opening.edit":"Opening","opening.door":"Door","opening.window":"Window","opening.gate":"Gate","opening.passage":"Open passage","opening.passage_binding_warning":"Saving will remove the open/close sensor and lock.","opening.invalid_passage_fields":"The open passage on plan “{room}” has unsupported settings: {fields}.","opening.type_label":"Type","opening.length_label":"Length, cm","opening.contact_label":"Open/close sensor","opening.lock_label":"Lock","opening.none":"— none —","opening.search_ph":"Search: name or entity id…","opening.invert":"Invert open/closed","opening.flip_h":"Hinge on the other jamb","opening.flip_v":"Opens to the other side","opening.open":"Open","opening.closed":"Closed","opening.locked":"Locked","opening.unlocked":"Unlocked","opening.state_unknown":"unavailable","opening.no_entities":"No sensors bound — a static symbol on the plan.","toast.opening_no_wall":"Click next to a room wall or independent wall","opening.host_partition":"Independent wall","opening.partition_orphan":"The independent wall for this opening no longer exists","opening.partition_jamb_margin":"Leave at least {distance} between the opening and the end of the wall","opening.rebind_partition":"Attach to another independent wall","confirm.delete_partition_openings_title":"Delete wall and openings?","confirm.delete_partition_openings_body":"This wall contains {count} opening(s). They will be deleted together.","confirm.delete_partition_openings_item":"• {type}, {length}","markup.delete":"Delete","markup.hint_points":"points: {n} · Shift — 45° steps · Esc — finish chain · Ctrl+Z — undo a point · closing an area offers a room","markup.hint_start":"click a grid dot to start a wall chain","tip.lqi":"average zigbee signal:","tip.area":"area: {value}","info.device_header":"Device on the plan","info.model":"Model","info.state":"State","info.link":"Link","info.manuals":"Manuals","info.none":"No additional information","marker.new_device":"New device","marker.name_label":"Name (shown on the plan)","marker.name_ph":"Name","marker.binding_label":"Bind to an HA device","marker.binding_disabled":"disabled in Home Assistant","marker.virtual_option":"Virtual device (no binding)","marker.search_ph":"Search device / group…","marker.nothing_found":"nothing found","marker.room_label":"Room","marker.room_override":" (override placement)","marker.room_choose":"— select a room —","marker.room_auto":"— by device area (auto) —","marker.icon_label":"Icon","marker.icon_ph":"mdi:… (empty = auto)","marker.display_label":"Display","display.badge":"Icon + state","display.icon_ripple":"Icon + state and activity","display.static_icon":"Always static icon","marker.display_hint":"Icon + state changes the plate for work, open, alarm and unavailable states. Icon + state and activity additionally shows a short pulse for events and a continuous pulse for ongoing work, movement or presence. Value + state replaces the icon with one unambiguous HA value. A static icon never reacts to device states.","marker.display_hint_badge":"The icon and dynamic plate show device state without the ordinary activity pulse. Red alarms remain visible.","marker.display_hint_icon":"The icon and dynamic plate show device state without the ordinary activity pulse. Red alarms remain visible.","marker.display_hint_icon_ripple":"The icon, dynamic plate and pulse show a short pulse for events and a continuous pulse for work, motion or presence. Red alarms have separate priority.","marker.display_hint_value":"One unambiguous Home Assistant value replaces the icon while the plate continues to show state. Red alarms remain visible.","marker.display_hint_static_icon":"The theme-aware shell and icon always stay the same. State, activity, unavailability and alarms do not change the face.","marker.static_alarm_warning":"This device can report alarms. Static display hides the marker's visual alarm indication.","marker.preview.title":"Display preview","marker.preview.actual":"Now","marker.preview.example":"Example","marker.preview.integration":"Provided by","marker.preview.source":"Display source","marker.preview.current_state":"Current state","marker.preview.result":"On the plan","marker.preview.details":"Source details","marker.preview.select_source":"Choose a Home Assistant device or entity to see its actual display.","marker.preview.unknown_provider":"Unknown integration","marker.preview.virtual_provider":"House Plan · virtual device","marker.preview.no_source":"No active source","marker.preview.no_state":"No current state","marker.preview.mixed_states":"Several different states","marker.preview.multiple_sources":"{n} sources","marker.preview.more_sources":"+{n} more","marker.preview.scaled":"The preview is scaled to {n}% to fit. The saved size is unchanged.","marker.preview.demo_activity":"Show activity example","marker.preview.demo_notice":"Temporary activity example; the actual device state is unchanged.","marker.preview.demo_short":"Show short activity","marker.preview.demo_continuous":"Show continuous activity","marker.preview.stop_continuous":"Stop continuous activity","marker.preview.demo_short_notice":"Short activity example; the actual device state is unchanged.","marker.preview.demo_continuous_notice":"Continuous activity example; the actual device state is unchanged.","marker.preview.demo_already_visible":"The current state already shows a real activity or alarm effect.","marker.preview.reduced_motion":"System reduced-motion is enabled, so ordinary activity is shown as a dot.","marker.preview.reason.neutral":"Neutral dark plate","marker.preview.reason.working":"Yellow plate: the device is working now","marker.preview.reason.working_activity":"Yellow plate and activity effect: the device is working now","marker.preview.reason.open":"Orange plate: physically open or unlocked","marker.preview.reason.cover_icon_state":"Neutral plate; the cover entity controls the displayed state and icon","marker.preview.reason.presence":"Activity effect while presence is detected","marker.preview.reason.event":"Short activity effect after a detected event","marker.preview.reason.transition":"Activity effect while the device is moving or changing state","marker.preview.reason.media_neutral":"Media devices stay on a neutral dark plate while available","marker.preview.reason.unavailable":"Subdued neutral plate: unavailable or off","marker.preview.reason.alarm":"Red alarm plate; alarm indication is always shown","marker.preview.reason.live_states_disabled":"Live-state styling is disabled for this card","marker.preview.reason.value_no_state":"No usable state value; the icon is shown instead","marker.preview.reason.value_ambiguous_sources":"Several possible value sources; the icon is shown instead","marker.preview.reason.value_non_scalar":"The source did not return a simple value; the icon is shown instead","marker.preview.reason.value_virtual":"A virtual device has no Home Assistant value; the icon is shown instead","marker.preview.reason.vacuum_live_plan_only":"Live vacuum position and trail are available only on the full plan","marker.preview.reason.hidden_design_preview":"The device is hidden on the plan; this design preview remains visible","marker.preview.reason.composite_power_source":"State comes from the device Power entity; auxiliary switches are ignored","marker.preview.reason.activity_display_disabled":"The selected display mode does not show ordinary activity effects","marker.preview.reason.ha_disabled":"The Home Assistant binding is disabled and will be hidden on the plan","marker.preview.reason.orphaned":"The saved Home Assistant binding can no longer be found","marker.preview.reason.static_icon":"Static mode: device state does not change the icon","marker.activity_color":"Activity pulse color","marker.ripple_size":"Activity pulse size","marker.activity_alarm_note":"Color and size affect ordinary activity only and do not change red alarms.","marker.pulse_a11y_alarm":"Alarm","marker.pulse_a11y_event":"Recent event","marker.pulse_a11y_presence":"Presence detected","marker.pulse_a11y_transition":"Changing state","marker.pulse_a11y_running":"Working","marker.state_a11y_neutral":"Default state","marker.state_a11y_working":"Working","marker.state_a11y_open":"Open","marker.state_a11y_locked":"Locked","marker.state_a11y_unlocked":"Unlocked","marker.state_a11y_alarm":"Alarm","marker.state_a11y_unavailable":"Unavailable","marker.lqi_a11y_low":"LQI {value}, low signal","marker.lqi_a11y_mid":"LQI {value}, medium signal","marker.lqi_a11y_high":"LQI {value}, high signal","marker.size_label":"Icon size / rotation","marker.angle_label":"Rotate","marker.model_label":"Model","marker.model_ph":"e.g. Aqara T&H","marker.link_label":"Link","marker.desc_label":"Description","marker.desc_ph":"Notes, specs…","marker.manuals_label":"Manuals (PDF etc.)","marker.sub_device":"device","marker.sub_z2m_group":" · Z2M group","marker.sub_group":"group","marker.sub_helper":"helper","space.new":"New space","space.header":"Space","space.title_label":"Title","space.title_ph":"e.g. Garage","space.plan_label":"Floor plan (background)","space.no_plan":"no plan image","space.plan_alt":"plan","room.new":"New room","room.name_label":"Display name","room.name_ph":"e.g. Terrace","room.area_label":"Home Assistant area (unassigned)","room.no_area_option":"— no area —","room.default_name":"Room","device.unnamed":"unnamed","device.light_group":"light group","device.fallback":"device","device.virtual":"virtual device","confirm.delete_room":'Delete room "{name}"?',"confirm.delete_room_title":'Delete room "{name}"?',"confirm.delete_room_body":"Choose whether the room's exclusive physical walls should remain. Walls shared with another room are always kept.","confirm.merge_rooms":'Merge rooms "{a}" and "{b}"?',"confirm.remove_marker":'Delete "{name}" from the plan? The device will disappear completely and stop contributing to plan data. You can add it again later.',"confirm.erase_decor":"Erase the {kind} object? You can undo this action from the editor history.","confirm.delete_space":'Delete space "{title}" with all its rooms and markup?',"space.delete_blocked":"This space is still used by {n} device(s). Move them to another space or delete them first.","toast.pos_save_failed":"Failed to save position: {err}","toast.no_entity":"The device has no suitable entity","toast.ha_disabled_action":"A Home Assistant object that is disabled cannot be used on the plan.","toast.ha_disabled_show_device":"A device disabled in Home Assistant cannot be shown on the plan. Enable it in Home Assistant first.","toast.ha_disabled_show_entity":"An entity disabled in Home Assistant cannot be shown on the plan. Enable it in Home Assistant first.","toast.ha_disabled_add":"A disabled Home Assistant object cannot be added to the plan. Enable it in Home Assistant first.","toast.ha_binding_unverified":"The object status could not be verified through the Home Assistant registry. Display and actions are temporarily unavailable.","toast.markup_needs_server":"Markup is available after the config is moved to the server","toast.space_order_changed":"Order changed. If any card pins its floor by number, check those panels.","toast.conflict":"Config was changed in another window — data refreshed, repeat your last action","toast.cfg_save_failed":"Failed to save config: {err}","toast.room_overlap":"The outline overlaps room “{name}” — rooms must not overlap","toast.contour_min_edges":"Draw at least two edges before closing the outline","toast.contour_cannot_close":"The outline cannot close because it is degenerate or intersects itself","toast.merge_not_adjacent":"Only rooms that share a wall can be merged","toast.rooms_merged":"Rooms merged into “{name}”","toast.split_pick_wall":"Start the cut on the room’s wall","toast.split_bad_cut":"The cut must run wall to wall inside the room, without crossing walls or itself","merge.header":"Merge rooms","merge.hint":"The merged room keeps one name and one area. The other area is released — its devices leave the plan until another room claims it.","merge.keep":"Keep","merge.no_area":"no area","room.split_header":"New room from the split","toast.room_saved":"Room saved ({n}). Devices added: {added}. Outline the next one or exit markup.","toast.room_saved_no_area":"Room saved ({n}, no area). Outline the next one or exit markup.","toast.marker_needs_server":"Device editing is available after the config is moved to the server","toast.virtual_name_required":"Enter a name for the virtual device","toast.marker_saved":"Device saved","toast.marker_removed":"Device deleted from the plan","toast.integration_missing":"The House Plan integration is not installed — management unavailable","toast.plan_formats":"Supported formats: SVG, PNG, JPG, WebP","toast.plan_required":"Upload a floor plan — it is required","toast.space_added_onboard":"Space added. Outline the rooms: click grid dots and close the contour.","toast.space_added":"Space added","toast.space_saved":"Space saved","toast.space_deleted":"Space deleted","toast.delete_failed":"Delete failed: {err}","toast.error":"Error: {err}","toast.toggle_target_unavailable":"Target “{name}” is unavailable — no action was performed","toast.toggle_targets_unavailable":"Targets are unavailable: {names}. No action was performed","toast.file_failed":'File "{name}" was not uploaded: {err}',"toast.files_attached":"Files attached: {n}","err.unknown":"unknown error","err.code":"code {code}","err.too_large":"file larger than {mb} MB","err.bad_ext":"unsupported type (PDF/image expected)","err.unauthorized":"administrator rights required","editor.title":"Title","editor.default_floor":"Initial space","editor.default_floor_missing":"Initial space “{id}” no longer exists. Choose another space.","editor.floor":"Fixed space","editor.floor_none":"— not fixed —","editor.floor_index":"YAML index {index} (preserved)","fixed_floor.loading":"Loading the fixed space…","fixed_floor.invalid_title":"The fixed space is unavailable","fixed_floor.invalid_body":"Check floor in the card configuration. The configured value is: {value}","editor.icon_size":"Icon size, % of plan width","editor.show_temperature":"Show temperature","editor.live_states":"Live states (on/off, open…)","editor.show_signal":"Show zigbee signal (LQI)","editor.language":"Interface language","editor.lang_auto":"Auto (HA profile)","editor.lang_en":"English","editor.lang_ru":"Русский","title.icon_rules":"Icon rules: which MDI icon devices get by name","rules.title":"Icon rules","rules.hint":"Rules are checked top-down against “device name + model” (case-insensitive regex); the first match wins. When nothing matches, the entity device class decides, then the generic chip icon.","rules.pattern_ph":"regex, e.g. plug|socket","rules.icon_ph":"mdi:power-socket-de","rules.add":"Add rule","rules.reset":"Reset to defaults","rules.test_ph":"Try a device name…","rules.invalid":"invalid regex","rules.saved":"Icon rules saved","btn.up":"Up","btn.down":"Down","tap.info":"Device card","tap.more_info":"HA more-info dialog","tap.toggle":"Toggle state","marker.tap_label":"Tap action for this device","tap.toggle_note":"The resolved state is shown below. Secure devices are never toggled from the plan.","import.title":"Create spaces from HA floors","import.hint":"Your Home Assistant already knows these floors. Pick the ones to turn into plan spaces — you will upload a floor-plan image for each one next. Rooms are then outlined by hand on the plan.","import.start":"Create {n} space(s)","import.manual":"Start from scratch","import.progress":"Floor {i} of {n}","import.done":"Spaces created. Outline the rooms: click grid dots and close the contour.","btn.skip":"Skip","space.scale_label":"Scale (grid cell size)","space.scale_unit":"cm per cell","space.scale_unit_imperial":"in per cell","space.display_section":"Display","space.show_borders":"Always show room borders","space.zero_wall_style":"Zero-thickness walls","space.zero_wall_dashed":"Dashed","space.zero_wall_solid":"Solid","space.zero_wall_help":"Dashed walls let light through. Solid walls block light even at zero thickness.","space.show_names":"Show room names (drag to move)","space.room_color":"Border & name color","space.opacity":"Opacity","space.fill_label":"Room fill","fill.none":"None","fill.lqi":"Zigbee signal","fill.light":"Lights","fill.custom":"Custom color","space.custom_fill":"Fill color","space.source_file":"I have a floor-plan image","space.source_draw":"No image — I'll outline rooms by hand","space.orientation":"Canvas","orient.landscape":"Landscape","orient.portrait":"Portrait","orient.square":"Square","fill.temp":"Temperature","space.temp_min":"Comfort from","space.temp_max":"to","tip.temp_avg":"average temperature:","tip.hum_avg":"average humidity:","space_card.button":"Open the space plan","space_card.not_found":"Space “{id}” not found","space_card.loading":"Loading…","continuity.restore_plan":"Restoring floor plan…","continuity.restore_connection":"Restoring device connection…","continuity.retry":"Try again","editor.space":"Space","editor.show_button":"Show button","editor.button_label":"Button label","editor.button_target":"Target dashboard path","marker.sub_entity":"entity","title.general_settings":"General settings","gs.title":"General settings","gs.hint":"Fill colors apply to every space; each color has its own opacity. Which fill mode a space uses is set in that space's dialog.","gs.light_group":"Fill: lights","gs.light_on":"Lights on","gs.light_off":"All lights off","gs.temp_group":"Fill: temperature","gs.temp_cold":"Cold","gs.temp_ok":"Comfortable","gs.temp_hot":"Hot","gs.lqi_group":"Fill: zigbee signal","gs.lqi_low":"Weak signal","gs.lqi_high":"Strong signal","gs.reset":"Reset to defaults","gs.saved":"General settings saved","space.show_lqi":"Show zigbee signal (LQI) next to devices","space.hide_decor":"Hide the decorative layer","space.hide_decor_tip":"Lines, shapes, labels and furniture stay where they are — visible in the backdrop editor, not on the plan.","space.hide_openings":"Hide openings","space.hide_openings_tip":"Door, window and gate symbols are not drawn, but the openings keep working: light passes through, the sun comes in at a window, contact sensors still open them. The plan editor always shows them.","gs.light_none":"No light sources","mode.plan":"Plan editor","mode.devices":"Device editor","display.value":"Value + state","marker.subarea":"no area, manual","device.new":"New device — open its editor to dismiss","opening.unlock_action":"Unlock","opening.lock_action":"Lock","opening.lock_pending":"Working…","title.close_editor":"Close editor (back to view)","devbar.add":"Add","devbar.show_all":"Hidden and disabled","devbar.rules":"Icon rules","device_inbox.button":"Devices","device_inbox.title":"Devices on the plan","device_inbox.search":"Search devices, entities and integrations…","device_inbox.add_virtual":"Add virtual device","device_inbox.tab_on_plan":"On plan","device_inbox.tab_available":"Available","device_inbox.tab_hidden":"Hidden","device_inbox.tab_readd":"Available again","device_inbox.only_new":"New only","device_inbox.show_entities":"Show entities","device_inbox.show_hidden":"Show hidden on plan","device_inbox.new":"New","device_inbox.find":"Find on plan","device_inbox.edit":"Edit","device_inbox.hide":"Hide","device_inbox.show":"Show","device_inbox.add":"Add","device_inbox.readd":"Add again","device_inbox.hide_available":"Hide from list","device_inbox.show_more":"Show more","device_inbox.more_actions":"More actions","device_inbox.find_hidden_hint":"Enable “Show hidden on plan” first","device_inbox.show_disabled":"Activate this binding in Home Assistant before showing it","device_inbox.empty_on_plan":"No matching devices are on the plan yet.","device_inbox.empty_available":"No devices are available to add.","device_inbox.empty_hidden":"There are no hidden devices.","device_inbox.empty_readd":"No removed devices are available to add again.","device_inbox.reason_visible_auto":"Discovered automatically","device_inbox.reason_visible_explicit":"Added explicitly","device_inbox.reason_manual_hidden":"Hidden by user","device_inbox.reason_automatic_hidden":"Hidden automatically","device_inbox.reason_service_entry":"Service-only registry entry","device_inbox.reason_excluded_integration":"Integration excluded by device filters","device_inbox.reason_excluded_domain":"Non-spatial entity type","device_inbox.reason_grouped_light":"Represented by a room light group","device_inbox.reason_represented_by_parent":"Represented by its parent device","device_inbox.reason_removed":"Previously removed from the plan","device_inbox.reason_available":"Available to add to the plan","device_inbox.reason_no_bound_room":"HA area is not linked to a plan room","device_inbox.status_ha_disabled":"Disabled in Home Assistant","device_inbox.status_orphaned":"Binding is no longer present in Home Assistant","device_inbox.status_unverified":"Home Assistant registry is temporarily unavailable","device_inbox.saved":"Device list updated","space.roomcard_section":"Room card shows:","space.label_temp":"Temperature","space.label_hum":"Humidity","space.label_lqi":"Average Zigbee signal","space.label_light":"Lights on/off","roomcard.light_on":"On","roomcard.light_off":"Off","roomcard.light_partial":"{on} of {total}","toast.split_pick_inside":"Intermediate cut points must be inside the room","mode.decor":"Background editor","decor.select":"Select","decor.line":"Line","decor.rect":"Rectangle","decor.ellipse":"Oval","decor.text":"Text","decor.erase":"Erase","decor.erase_confirm_title":"Erase object?","decor.color":"Color","decor.width":"Line width","decor.line_style":"Line style","decor.line_style_solid":"Solid","decor.line_style_dashed":"Dashed","decor.fill":"Fill","decor.fill_color":"Fill color","decor.length":"Length","decor.size":"Size","decor.angle":"Rotation angle, °","decor.text_size":"Text size","decor.backdrop_properties":"Plan backdrop properties","decor.text_title":"Text label","decor.object_title":"Edit {kind}","decor.text_label":"Text","decor.live_group":"Insert HA variable","decor.live_entity":"Entity","decor.live_entity_ph":"choose an entity","decor.live_attr":"Value","decor.live_attr_ph":"choose state or attribute","decor.live_state":"State","decor.backdrop":"Backdrop image","decor.backdrop_hint":"Drag to move; pull a corner to resize; use the top handle to rotate. Shift changes proportions or frees the angle.","decor.backdrop_reset":"Reset the picture","decor.backdrop_reset_done":"The picture is back at its original place and size","marker.icon_auto":"Auto: {icon} (by icon rules; pick one to override)","marker.icon_pin_auto":"Pin","mode.plan_tip":"Plan editor — the geometry of the home: draw and split/merge rooms, bind them to HA areas, place doors, windows and gates, move room cards, set the scale","mode.devices_tip":"Device editor — everything about icons: drag to position, click to edit binding/icon/display, add virtual devices, icon rules","mode.decor_tip":"Background editor — purely visual decor under the plan: lines, rectangles, ovals and text labels that never react to clicks","space.glow_enabled":"Light-source glow","gs.glow_group":"Light-source glow","gs.glow_base":"House darkness","gs.glow_light":"Default light color / intensity","gs.wall_group":"Walls","gs.wall_fill":"Wall fill","gs.glow_radius":"Glow radius","gs.unit_m":"m","gs.unit_ft":"ft","marker.controls_label":"Controls other light sources","marker.controls_hint":"With tap action “Toggle”, a click flips every source added here. The marker’s own bound entity is controlled directly and cannot be added; use the switch below to classify it as a light.","marker.controls_filter":"Search lights and switches…","info.controls":"Controls","marker.glow_radius_label":"Glow radius","marker.glow_radius.help":"Sets the glow radius in metres or feet; an empty value uses the radius from general settings.","marker.glow_radius.help.aria":"Help: glow radius","markup.wallthick":"Thickness","markup.select":"Select","title.markup_wallthick":"Thickness — click a wall to set its thickness from 0 to 100 cm.","markup.hint_wallthick":"click a wall · Esc closes without applying","wallthick.field":"Thickness","wallthick.unit_cm":"cm","wallthick.unit_in":"in","wallthick.apply_room":"Apply to all walls of this room","markup.draw_wall_title":"Thickness of each new wall segment (0–100 cm). Shared walls keep the existing value.","room.queue_progress":"Room {current} of {total}","toast.wall_rooms_saved":"Created rooms: {n}","toast.wall_chain_saved":"Wall chain saved","toast.wallthick_pick":"Click a wall","toast.wallthick_set":"Wall thickness set","toast.wallthick_cleared":"Wall thickness removed","toast.physical_range":"Enter a value from {min} to {max} {unit}","toast.zero_wall_opening_conflict":"Remove the opening on this wall segment first.","toast.zero_wall_ambiguous":"The wall segment is ambiguous. Simplify or adjust the junction.","toast.zero_wall_migration_blocked":"The space was not converted: {reason}. No data was changed.","toast.physical_angle":"Enter a rotation angle from 0° up to, but not including, 90°","toast.physical_limit":"The space has reached the limit for this type of geometry","toast.geometry_unsafe":"Change canceled: wall geometry could not be built safely.","junction.limit_angle":"The angle between walls is too sharp: {actual}°, minimum {limit}°.","junction.limit_valence":"Too many walls meet in one node: {actual}, maximum {limit}.","junction.limit_length":"The wall segment is too short: {actual} cm, minimum {limit} cm.","junction.limit_distance":"Walls and nodes are too close: {actual} cm, minimum {limit} cm.","junction.limit_clearance":"No room interior is left: {actual} cm², minimum {limit} cm².","junction.limit_check_failed":"The junction check could not run — the change was not saved","toast.wall_model_migration_blocked":"The wall model could not be updated: {reason}. The plan was not changed. Run “Optimize plans”; if the error repeats, fix the conflicting wall geometry.","toast.wall_model_client_outdated":"Update the card and reload the page before editing the plan.","wall_model.reason.invalid-room":"invalid room contour","wall_model.reason.zero-length":"zero-length wall segment","wall_model.reason.third-owner":"wall shared by more than two rooms","wall_model.reason.duplicate-id":"conflicting wall identifiers","wall_model.reason.thickness-conflict":"conflicting wall thickness values","wall_model.reason.opening-host":"opening cannot be assigned to one wall","toast.delete_room_pick":"Click inside the room you want to delete","toast.plan_snap_ambiguous":"Zoom in to choose the wall node","toast.wall_repair_ambiguous":"The room outline has more than one possible connection. Zoom in and connect it explicitly.","toast.wall_repair_too_large":"The gap is larger than 2 cm. Connect the walls before creating the room.","toast.wall_repair_changed":"The wall geometry changed. Try creating the room again.","toast.opening_on_zero_wall":"Openings cannot sit on a zero-thickness wall","marker.from_ha_option":"Pick from the HA list","marker.show_entities":"Show entities","marker.show_entities_tip":"Adds not only devices to the list, but all their entities too","marker.pick_ph":"Choose a device…","room.open_area":"Open the HA area","view.volumetric":"Volumetric view","view.flat":"Flat view","kiosk.title":"This screen's sizes","kiosk.hint":"Stored on this device only — every wall tablet or TV can have its own comfortable sizes.","kiosk.icon_scale":"Device icon size","kiosk.font_scale":"Room card text size","editor.kiosk":"Wall device (kiosk) mode","editor.cycle":"Auto-switch spaces every N seconds (kiosk, 0 = off)","room.settings_title":"Room settings","room.settings_section":"Room settings (override the space)","room.fill_label":"Fill in THIS room","fill.inherit":"As the space","room.custom_fill_space":"Space color","room.custom_fill_own":"Room color","room.temp_src_label":"Temperature source","room.hum_src_label":"Humidity source","room.src_average":"Average over the room's sensors (default)","room.src_pick":"A specific HA device or entity","room.src_ph":"Choose a source…","toast.room_updated":"Room updated","space.card_font":"Room-card font size (whole space)","room.sizes_section":"Font sizes","room.name_scale":"Room name size","room.label_scale":"Metrics size","preview.room_name":"Living room","toast.cfg_reload_failed":"Could not reload the plan from the server: {err}","room.settings_short":"Room settings","room.unnamed":"Unnamed room","marker.use_climate_temp":"Include the device temperature in the room","marker.use_climate_temp_tip":"Adds an air conditioner or thermostat current_temperature to the room average. Configure the external value badge separately below.","marker.value_badge_title":"Value badge","marker.value_badge.help":"Shows one selected value next to the icon. It does not affect room metrics, light, Glow or the tap action.","marker.value_badge.help.aria":"Help: device value badge","marker.value_badge_enabled":"Show a value badge","marker.value_badge_source":"Value","marker.value_badge_source.help":"Choose a specific state, supported attribute, or derived value of this device.","marker.value_badge_source.help.aria":"Help: value badge source","marker.value_badge_position":"Position","marker.value_badge_position.help":"The chosen side stays fixed and does not flip automatically at a plan edge.","marker.value_badge_position.help.aria":"Help: value badge position","marker.value_badge_right":"Right","marker.value_badge_bottom":"Below","marker.value_badge_left":"Left","marker.value_badge_top":"Above","marker.value_badge_empty":"This device has no available values","marker.value_badge_static":"A static icon does not show live values. The setting is preserved.","marker.value_badge_missing":"Source unavailable","marker.value_badge_missing_hint":"The saved source is currently unavailable. Keep it, disable the badge, or choose another value.","marker.value_badge_duplicate":"The same value is already shown inside the icon.","marker.value_badge_state":"State · {name}","marker.value_badge_marker_state":"Light-source state · {name}","marker.value_badge_lqi":"Average Zigbee signal quality","marker.value_badge_attr_current_temperature":"Current temperature · {name}","marker.value_badge_attr_temperature":"Target temperature · {name}","marker.value_badge_attr_current_humidity":"Current humidity · {name}","marker.value_badge_attr_humidity":"Humidity · {name}","marker.value_badge_attr_current_position":"Position · {name}","marker.value_badge_attr_percentage":"Speed · {name}","marker.value_badge_attr_brightness":"Brightness · {name}","marker.value_badge_attr_volume_level":"Volume · {name}","marker.value_badge_attr_battery_level":"Battery · {name}","marker.value_badge_attr_fan_speed":"Fan speed · {name}","marker.light_role_label":"Is this device a light source?","marker.light_role.help":"Controls whether this marker itself is a spatial light source. Linked lights above remain independent room-light controls.","marker.light_role.help.aria":"Help: whether this device is a light source","marker.light_role_auto_yes":"Auto (light source)","marker.light_role_auto_no":"Auto (not a light source)","marker.light_role_always":"Always a light source","marker.light_role_never":"Never a light source","marker.light_entity_label":"Leading light entity","marker.light_entity.help":"For a composite device, choose the entity whose state, colour and brightness represent this plan source. The automatic fallback remains compatible with older plans.","marker.light_entity.help.aria":"Help: leading light entity","marker.light_entity_auto":"Automatic ({entity})","marker.light_entity_none":"no controllable entity","marker.light_entity_missing":"The saved entity {entity} is unavailable. House Plan temporarily uses {fallback}; the saved choice will be restored if it returns.","marker.toggle_entity_label":"Entity to toggle","marker.toggle_entity.help":"For a composite device, choose the exact own light or switch operated by a tap. Automatic keeps the previous target rules and is independent from the leading light entity.","marker.toggle_entity.help.aria":"Help: entity to toggle","marker.toggle_entity_auto":"Automatic ({entity})","marker.toggle_entity_none":"no own controllable entity","marker.toggle_entity_missing":"The saved entity {entity} is no longer among this marker’s selectable channels. House Plan temporarily uses {fallback}; the saved choice will be restored if it returns.","marker.glow_color_label":"Glow colour and brightness","marker.glow_mode.help":"Use live source values, override only its colour, or fix both colour and brightness. The minimum is 1%; to disable the source, choose “Never”.","marker.glow_mode.help.aria":"Help: glow colour and brightness","marker.glow_mode_auto":"From source","marker.glow_mode_color":"Set colour","marker.glow_mode_fixed":"Set colour and brightness","marker.glow_color":"Glow colour","marker.glow_brightness":"Brightness","marker.glow_disabled_never":"Glow settings are unavailable because this marker is explicitly not a light source.","marker.glow_disabled_auto":"Auto mode found no spatial light source for this marker.","marker.glow_disabled_no_entity":"No active controllable Home Assistant entity is available for this spatial source.","marker.glow_passive_hint":"This source has no own Home Assistant data. Set its colour and brightness manually; its radius remains available.","marker.control_broken":"Saved source is missing or no longer marked as a light","marker.control_missing_label":"Missing plan light","marker.control_passive":"passive source","toast.marker_control_cycle":"This link would create a circular chain of light controls.","toast.marker_binding_required":"Choose a Home Assistant device before linking another light source.","confirm.unlock":"Unlock “{name}”?","toast.files_migrate_failed":"Attachments could not be moved to the new binding, links keep pointing at the old files: {err}","space.pick_saved":"Already uploaded","space.pick_saved_hint":"Plans stored on the server, including ones you detached earlier","space.no_saved":"No plans stored on the server yet.","space.loading":"Loading…","space.used_by":"in use: {list}","space.in_use":"A space still uses this plan — detach it first","btn.use":"Use","confirm.delete_plan":'Delete the plan file "{name}" from the server? This cannot be undone.',"toast.plans_list_failed":"Could not list the stored plans: {err}","toast.plan_delete_failed":"Could not delete the plan: {err}","marker.hide":"Hide","marker.hide_tip":'The device will disappear from the plan after saving but will still count toward the room signal. Restore it through the "Devices" catalog in the device editor.',"marker.show":"Show","marker.show_tip":"The device will appear on the plan again after saving.","marker.hidden_ghost":"Device hidden by the user","marker.ha_disabled_device":"The device is disabled in Home Assistant and hidden from the plan.","marker.ha_disabled_entity":"The entity is disabled in Home Assistant and hidden from the plan.","marker.ha_disabled_all_entities":"The device has no active Home Assistant entities, so it is hidden from the plan.","marker.ha_registry_limited":"The full Home Assistant registry is unavailable to this user. The unverified object cannot be shown or used for now.","marker.delete_tip":"Completely delete the device from the plan and every aggregate. You can add it again later.","tap.run":"Run automation/script/scene","tap.cover":"Open/close (curtains/blinds)","marker.run_target_label":"What to run","marker.run_search_ph":"Search: automation, script or scene…","marker.run_target_gone":"Target {id} not found — pick again","marker.tap_confirm":"Ask for confirmation","marker.tap_confirm_tip":"Show a confirmation dialog before acting — a guard against accidental taps.","marker.toggle_hint_single":"Target: {name} ({id}).","marker.virtual_light_target":"Virtual light: {name}.","marker.virtual_light_current":"Manual state: {state} → {effect}.","marker.virtual_light_state_on":"on","marker.virtual_light_state_off":"off","marker.toggle_hint_group":"Will toggle {count} source(s): {names}.","marker.toggle_hint_current":"Now: {state} → {effect}.","marker.toggle_hint_group_current":"Currently on: {on} of {count} → {effect}.","marker.toggle_hint_skipped":"Will be skipped ({count}): {targets}.","marker.toggle_effect_turn_on":"will turn on","marker.toggle_effect_turn_off":"will turn off","marker.toggle_effect_open":"will open","marker.toggle_effect_close":"will close","marker.toggle_effect_stop":"will stop","marker.toggle_effect_toggle":"state will toggle","marker.toggle_skip_missing":"not found","marker.toggle_skip_ha_disabled":"disabled in HA","marker.toggle_skip_unavailable":"unavailable","marker.toggle_skip_unsupported":"toggle is unsupported","marker.toggle_skip_secure":"blocked for security","marker.toggle_none_no_binding":"This device has no binding, so there is no state to toggle. Tapping will do nothing.","marker.toggle_none_no_actionable_entity":"This device has no state that can be toggled. Tapping will do nothing.","marker.toggle_none_configured_targets_missing":"The configured targets are unavailable. The device's own entity will not be substituted for them.","marker.toggle_none_ha_disabled":"The target is disabled in Home Assistant and cannot be used on the plan.","marker.toggle_none_unavailable":"The target is currently unavailable. Tapping will do nothing.","marker.toggle_none_unsupported":"Home Assistant exposes no safe toggle operation for this entity. Tapping will do nothing.","marker.toggle_none_secure":"Toggling locks, alarms and secure gates from the plan is blocked for security.","run.automation":"automation","run.script":"script","run.scene":"scene","confirm.tap_run":'Run "{name}"?',"confirm.tap_toggle":'Toggle "{name}"?',"confirm.tap_cover":'Open/close "{name}"?',"confirm.current_state":"Current state: {state}","confirm.expected_state":"After switching: {state}","confirm.group_current":"on {on} of {total}","confirm.group_all_on":"all are on","confirm.group_all_off":"all are off","confirm.unavailable_targets":"Unavailable: {count}","confirm.expected_by_ha":"Home Assistant will determine the state","confirm.state_on":"On","confirm.state_off":"Off","confirm.state_open":"Open","confirm.state_closed":"Closed","confirm.state_opening":"Opening","confirm.state_closing":"Closing","confirm.state_stopped":"Stopped","confirm.state_unknown":"Unknown","toast.run_started":"Started: {name}","toast.run_target_missing":"Run target not found — check the device settings","toast.run_target_required":"Pick an automation, script or scene","toast.tap_target_changed":"The action target changed. Try again.","toast.virtual_light_toggle_failed":"Could not toggle the virtual light: {err}","toast.value_badge_source_required":"Choose a value for the badge","btn.run":"Run","vac.section":"Robot vacuum: live position","vac.autocal":"Set up automatically","vac.live":"Live position on the plan","vac.trail":"Show the robot's path","vac.cal_maps":"Calibrated maps: {maps}","vac.autocal_no_rooms":"The integration reports no room list — open “Fit manually”","vac.autocal_no_match":"Room names did not match (need ≥3 in common) — open “Fit manually”","vac.autocal_done":"Done: bound via {rooms} rooms. Start a cleanup and check","vac.cal_need_pos":"The robot is not reporting coordinates — start a cleanup and pause it","vac.cal_done":"Calibration saved. Start a cleanup and check","vac.cal_cancelled":"Calibration cancelled","vac.fit":"Fit manually","vac.fit_hint":"Drag the robot map into place, stretch by the corners","vac.fit_rotate":"Rotate 90°","vac.fit_mirror":"Mirror","vac.trail_never":"Never","vac.trail_cleaning":"While cleaning","vac.trail_always":"Always","gs.bg_group":"Stage background","gs.bg_color":"Background around the plan","gs.bg_default":"Theme default","gs.bg_theme":"theme default","gs.bg_mode":"Plan background","gs.bg_static":"Static color","gs.bg_daynight":"Follows the Sun","gs.bg_daynight_hint":"The background moves through dawn, day, dusk and night using sun.sun, or your local clock when sun data is unavailable. No compass is needed for the background.","gs.sun_group":"Sun","gs.sun_missing":"The sun.sun entity was not found — the background follows your local clock; window rays are unavailable.","gs.north":"North on the plan","gs.north_ph":"not set","gs.north_hint":"Set north only for window rays (1° steps, 15° with Shift); the Sun background works without it.","gs.north_clear":"Clear","gs.north_letter":"N","gs.sun_rays":"Sunlight through windows","gs.about_group":"About","gs.about_version":"Houseplan Card v{v}","gs.about_github":"GitHub · docs & issues","gs.about_telegram":"Telegram chat","space.bg_color":"Background around the plan","space.bg_inherit":"Inherit general","space.bg_inherited":"inherits general settings","space.bg_mode":"Plan background","space.north":"North on the plan (override)","space.north_inherited":"inherited: {v}","space.sun_rays":"Sunlight through windows","space.sun_inherit":"Inherit general","space.sun_on":"On","space.sun_off":"Off","canvas.far_objects":"{n} object(s) far from the plan","canvas.show_far":"Show","canvas.home_tip":"The plan is over there — click to fit it","gs.grid_group":"Plan maintenance","gs.grid_hint":"Updates data models, aligns plan elements to the grid and merges redundant wall fragments. An exact report is shown before anything is stored.","gs.align_all":"Optimize plans","gs.align_title":"Optimize plans","gs.align_none":"All plans already use the current optimized data model.","gs.optimize_no_automatic_changes":"There are no automatic changes to apply. Review the items below.","gs.align_count":"{n} of {total} elements will move, by at most {cm} cm.","gs.align_where":"The largest shift is in “{s}”.","gs.align_turned":"Openings whose angle is corrected: {n}.","gs.align_removed_drafts":"Invalid outlines collapsed by the grid and removed: {n}.","gs.optimize_redundant_drafts":"Saved wall chains hidden by solid room walls and removed: {n}.","gs.align_preflight_failed":"Could not safely verify the geometry of the following spaces: {spaces}{more}.","gs.align_preflight_hint":"Plans were not changed. Copy the diagnostics with the button below and attach them to the bug report together with a space export.","gs.preflight_reason_prepare-exception":"Could not prepare the space geometry (exception while building the model)","gs.preflight_reason_wall-null":"The wall body did not build (the union came back empty)","gs.preflight_reason_wall-degraded-extra":"The wall body degraded with extra geometry","gs.preflight_reason_wall-failed-core":"The wall body core failed to assemble","gs.preflight_reason_wall-exception":"Wall construction threw an exception","gs.preflight_reason_floor-null":"The floor outline did not build","gs.preflight_reason_floor-exception":"Floor construction threw an exception","gs.preflight_copy":"Copy diagnostics","gs.preflight_copied":"Diagnostics copied","gs.preflight_update_hint":"The card and integration versions differ — update House Plan and retry.","gs.align_preflight_space":"Space {n}","gs.align_preflight_more":", and {n} more","gs.optimize_changes":"Model migrations: {m}; spaces updated: {c}; noisy coordinate values removed: {p}; merged real-wall fragments: {w}; merged zero-thickness wall fragments: {s}; independent walls: {i}.","gs.zero_walls_migrated":"Virtual wall spans converted: {n}.","gs.wall_segments_migrated":"Wall segments stabilised: {n}.","gs.optimize_lattice_summary":"Noisy coordinate values canonicalized: {n}; maximum movement: {cm} cm.","gs.optimize_lattice_space":"{space}: coordinate values canonicalized: {n}; off-grid values left unchanged: {far}.","gs.optimize_coincident_partitions":"Hidden independent wall sections absorbed into room walls: {n}.","gs.optimize_openings_rehosted":"Openings reattached to room walls: {n}.","gs.optimize_walls_straightened":"Walls straightened: {n}; maximum movement: {cm} cm.","gs.optimize_walls_straightened_where":"Largest wall correction: {s}.","gs.optimize_walls_straighten_skipped":"Near-axis walls left unchanged because they could not be repaired safely: {n}.","gs.optimize_glow_migration":"Legacy Glow: {spaces} spaces → no data fill + independent Glow; {rooms} rooms → inherited data fill + independent Glow.","gs.optimize_references":"References repaired: spaces — {spaces}; rooms — {rooms}; positions — {positions}; devices detached from missing spaces — {detached}.","gs.optimize_reference_more":", and {n} more","gs.optimize_orphans_removed":"Forgotten records removed: {total} — room labels: {rooms}; devices: {devices}; group markers: {groups}. They belonged to spaces deleted earlier.","gs.optimize_live_positions":"Old positions in deleted spaces belong to existing objects: {n}{names}. They will be kept.","gs.optimize_live_positions_remove":"Old positions in deleted spaces belong to existing objects: {n}{names}. They are selected for removal.","gs.optimize_live_names":": {names}{more}","gs.optimize_live_remove":"Remove old positions","gs.optimize_live_keep":"Keep old positions","gs.optimize_live_selected":"Old positions will be removed after Optimize is applied.","gs.optimize_unverified":"Could not safely verify positions: {n}. They were left unchanged.","gs.optimize_registry_limited":"Full administrator access to the Home Assistant registries is required for a safe check.","gs.optimize_vacuum_warning":"Vacuum room mappings that still need review: {n}.","gs.optimize_details":"Details","gs.optimize_details_more":"And {n} more records.","gs.optimize_detail_removed":"will be removed","gs.optimize_detail_live":"will be kept","gs.optimize_detail_unverified":"not verified","gs.optimize_detail_room_label":"room label","gs.optimize_detail_device":"device","gs.optimize_detail_group":"group marker","gs.optimize_detail_unknown":"unknown owner","gs.optimize_detail_item":"{status}: {kind} {id}; old space {space}","gs.align_warn":"Elements deliberately placed between grid nodes will move. One undo is available after the operation, only until the next plan edit.","gs.align_run":"Optimize","gs.align_done":"Plans optimized: {n} elements moved, {m} records maintained, {r} references repaired","gs.optimize_undo":"Undo last optimization","gs.optimize_undone":"The last optimization was undone","decor.furniture":"Furniture","furn.title":"Furniture library","furn.symbol":"Symbol","furn.group_furniture":"Furniture","furn.group_appliance":"Appliances","furn.group_sanitary":"Plumbing","furn.group_other":"Other","furn.width":"Width","furn.depth":"Depth","furn.pick_hint":"Pick a symbol, then click on the plan.","furn.place_hint":"Click on the plan — the piece lands against the nearest wall. Shift places it free.","furn.sym_sofa":"Sofa","furn.sym_armchair":"Armchair","furn.sym_coffee_table":"Coffee table","furn.sym_table_dining":"Dining table","furn.sym_table_round":"Round table","furn.sym_chair":"Chair","furn.sym_desk":"Desk","furn.sym_bed_double":"Double bed","furn.sym_bed_single":"Single bed","furn.sym_nightstand":"Nightstand","furn.sym_wardrobe":"Wardrobe","furn.sym_bookshelf":"Bookshelf","furn.sym_fridge":"Fridge","furn.sym_stove":"Cooker","furn.sym_dishwasher":"Dishwasher","furn.sym_washer":"Washing machine","furn.sym_dryer":"Tumble dryer","furn.sym_tv":"TV","furn.sym_ac":"Air conditioner","furn.sym_water_heater":"Water heater","furn.sym_toilet":"Toilet","furn.sym_bathtub":"Bathtub","furn.sym_shower":"Shower","furn.sym_sink":"Washbasin","furn.sym_kitchen_sink":"Kitchen sink","furn.sym_bidet":"Bidet","furn.sym_stairs":"Stairs","furn.sym_fireplace":"Fireplace","furn.sym_plant":"Plant","furn.sym_rug":"Rug","common.yes":"Yes","common.no":"No","vac.diag_source":"Source","vac.diag_platform":"Integration","vac.diag_status":"Status","vac.diag_position":"Position","vac.diag_rooms":"Rooms","vac.diag_rooms_value":"{total} · {matched} names match · {readiness}","vac.autocal_ready":"auto-calibration available","vac.autocal_not_ready":"need 3 matching names","vac.diag_path":"Integration path","vac.diag_map":"Map ID","vac.source_none":"not selected","vac.source_status_ok":"Ready","vac.source_status_missing":"Missing","vac.source_status_disabled":"Disabled in Home Assistant","vac.source_status_unavailable":"Unavailable","vac.source_status_unverified":"Cannot verify with current permissions","vac.source_status_unsupported":"No position data","vac.source_status_none":"No source","vac.source_banner_missing":"The saved source no longer exists. It was not replaced automatically; choose another source or restore it in Home Assistant.","vac.source_banner_disabled":"The saved source is disabled in Home Assistant. Enable it there or choose another source.","vac.source_banner_unverified":"Current Home Assistant permissions cannot verify this saved source. It remains pinned and will not be replaced automatically.","vac.choose_source":"Choose source","vac.source_auto":"Automatic","vac.source_auto_hint":"Use a compatible entity from this device","vac.all_cameras":"All cameras","vac.all_cameras_warn":"A camera may not provide robot data. Choose one only if it is your vacuum map.","vac.all_cameras_empty":"No other camera entities found.","vac.platform_unknown":"unknown integration","vac.cap_position":"position","vac.cap_rooms_short":"rooms","vac.cap_path":"path","vac.cap_map":"map ID","vac.cap_none":"no robot data detected","vac.xcme_hint":"Enable these Xiaomi Cloud Map Extractor attributes:","vac.documentation":"Documentation","vac.residual_title":"Check automatic calibration","vac.residual_message":"The matched rooms disagree by up to {error}. Apply this approximate calibration, refine it manually, or cancel without changing the saved setup.","vac.apply_proposal":"Apply","gs.backup_group":"Backup and transfer","gs.backup_hint":"Download a portable JSON backup or preview and import a backup made by House Plan.","backup.export_open":"Export","backup.import_open":"Import","backup.export_title":"Export House Plan","backup.import_title":"Import House Plan","backup.export_hint":"Choose whether the backup should contain the whole House Plan configuration or only the current space.","backup.full":"Full backup","backup.current_space":"Current space","backup.current_space_title":"Current space: {title}","backup.no_current_space":"No current space","backup.plan_only":"Plan only","backup.plan_only_hint":"Keep rooms, walls, openings, decor and room-label positions without devices or Home Assistant bindings.","backup.plan_only_preview":"This file contains the plan only","backup.privacy_warning":"The archive keeps names, Home Assistant identifiers and exact coordinates. Internal plans and attachments are referenced, not embedded; runtime states and vacuum trails are not included.","backup.download":"Download JSON","backup.export_done":"Backup downloaded","backup.reading":"Checking the backup…","backup.revalidated":"The plan changed after this preview. The summary was refreshed; review it and confirm again.","backup.error.unauthorized":"You do not have permission to export or import this plan.","backup.error.not_ready":"House Plan is not ready yet. Try again in a moment.","backup.error.too_large":"The backup exceeds the 8 MiB limit.","backup.error.invalid_json":"The selected file is not valid JSON.","backup.error.invalid_format":"The selected file is not a House Plan backup.","backup.error.unsupported_export_version":"This backup format is not supported by the installed version.","backup.error.future_model":"The backup was created by a newer House Plan data model.","backup.error.invalid_config":"The backup contains an invalid House Plan configuration.","backup.error.wall_model_migration_blocked":"The backup wall model could not be upgraded safely. Optimize the source plan first, then export it again.","backup.error.invalid_layout":"The backup contains invalid object positions.","backup.error.invalid_content":"The backup contains invalid or inconsistent content references.","backup.error.space_not_found":"The selected space no longer exists.","backup.error.capacity_exceeded":"Adding this backup would exceed the plan limits.","backup.error.preview_expired":"The preview expired. Select the backup file again.","backup.error.preview_owner_mismatch":"This preview belongs to another Home Assistant user.","backup.error.conflict":"The plan changed after the preview. Review the refreshed summary.","backup.error.content_confirmation_required":"Confirm that unavailable local content may be detached.","backup.error.commit_failed":"The backup could not be applied safely. The previous plan was restored or is pending recovery.","backup.error.missing_plan":"A referenced plan file disappeared after the preview. Review the backup again.","backup.error.missing_content":"A referenced local attachment disappeared after the preview. Review the backup again.","backup.error.marker_control_missing":"A linked plan light is missing from the backup.","backup.error.marker_control_not_light":"A linked plan target is no longer marked as a light source.","backup.error.marker_control_self":"A light source cannot control itself.","backup.error.marker_control_cycle":"The backup contains a circular chain of light controls.","backup.error.duplicate_marker_control":"The backup contains a duplicate plan-light link.","backup.error.no_backup":"There is no import or optimization snapshot to restore.","backup.same_source":"Created on this Home Assistant instance","backup.foreign_source":"Created on another Home Assistant instance","backup.created":"Created: {value}","backup.versions":"Card {card}; integration {integration}; data model {model}","backup.count_spaces":"Spaces: {n}","backup.count_rooms":"Rooms: {n}","backup.count_walls":"Walls: {n}","backup.count_openings":"Openings: {n}","backup.count_decor":"Decor objects: {n}","backup.count_markers":"Devices: {n}","backup.count_layout":"Positions: {n}","backup.bindings":"Bindings — devices: {device}, entities: {entity}, virtual: {virtual}; positions without a space: {legacy}","backup.binding_status":"Target status — active: {active}, disabled: {disabled}, missing: {missing}","backup.missing_areas":"Areas missing on the target: {areas}","backup.dropped_marker_links":"Plan-light links outside this transferred space were omitted: {n}.","backup.repaired_target_refs":"Existing references restored by this space import: {n}.","backup.preserved_unresolved_refs":"References that could not be restored unambiguously were preserved: {n}.","backup.preserved_unresolved_hint":"No data was guessed or deleted. After the import, run Optimize plans to inspect the remaining references.","backup.import_details":"Import reference details","backup.import_detail.incoming_remapped":"References updated inside the imported copy: {n}","backup.import_detail.target_repaired":"Existing references restored: {n}","backup.import_detail.preserved_unresolved":"Unresolved references preserved: {n}","backup.import_detail.collisions":"Destination conflicts preserved safely: {n}","backup.import_detail.dropped_links":"Incoming links omitted by transfer rules: {n}","backup.import_detail.bounded_lineages":"Overly nested identifiers left bounded: {n}","backup.replace_warning":"This replaces the current configuration and layout. Uploaded files are never deleted. One undo remains available until the next plan edit.","backup.foreign_bookkeeping":"Instance-specific known/new-device bookkeeping will not be imported.","backup.final_name":"New space name","backup.target_settings":"The target instance's global settings remain unchanged.","backup.duplicates":"Bindings already present: {n}","backup.skip":"Skip duplicate devices","backup.virtual_copy":"Add safe static virtual copies","backup.content":"Referenced content","backup.content_available":"available locally","backup.content_external":"external link","backup.content_detach_required":"will be detached","backup.confirm_detach":"I understand that unavailable internal plans and attachments will be detached from the imported configuration.","backup.replace":"Replace","backup.add":"Add space","backup.space_done":"Space imported: {rooms} rooms, {markers} devices, {refs} existing references restored","backup.full_done":"Backup restored: {spaces} spaces, {rooms} rooms, {markers} devices","backup.undo_import":"Undo last full import","backup.import_undone":"Full import undone"};const Ud=[{code:"en",nativeLabel:"English",dictionary:Wd},{code:"ru",nativeLabel:"Русский",dictionary:{"editor.context_actions":"Действия: {object}","editor.tool_options":"Параметры инструмента: {tool}","editor.palette":"Палитра: {tool}","editor.open_group":"Группа инструментов: {group}","editor.group_active":"{group} — активно: {item}","editor.disabled_action":"{action} недоступно: {reason}","editor.loading":"Загружаем редактор…","editor.loading_aria":"Редактор плана загружается","editor.load_failed":"Не удалось загрузить редактор.","editor.refresh_advice":"Обновите страницу и повторите попытку.","color_picker.title":"Выбор цвета","color_picker.hue":"Оттенок","color_picker.saturation":"Насыщенность","color_picker.value":"Яркость","color_picker.hex":"Цвет HEX","color_picker.invalid_hex":"Введите цвет HEX из 3 или 6 цифр","btn.properties":"Свойства","btn.keep_as_walls":"Оставить стенами","btn.delete_room_keep_walls":"Удалить комнату, оставить стены","btn.delete_room_with_walls":"Удалить комнату и стены","title.markup_select":"Выбор и редактирование стен, колонн и сохранённых контуров","title.markup_column":"Колонна: кликните точку сетки для квадратной колонны","markup.partition":"Перегородка","markup.column":"Колонна","markup.hint_column":"кликните точку сетки, чтобы поставить квадратную колонну","history.draft_segment":"Добавление сегмента черновика комнаты","history.draft_merge":"Соединение незавершённых контуров комнаты","history.draft_segment_delete":"Удаление сегмента черновика комнаты","history.partition_add":"Добавление перегородки","history.column_add":"Добавление колонны","history.physical_edit":"Изменение физического объекта","history.physical_delete":"Удаление физического объекта","history.physical_move":"Перемещение физического объекта","history.contour_to_partitions":"Преобразование контура в замкнутые стены","history.wall_chain_finish":"Завершение цепочки стен","history.wall_face_batch":"Создание комнат из стен","toast.column_duplicate":"Колонна с тем же центром и внешним размером уже существует","confirm.delete_draft":"Удалить весь незавершённый контур комнаты?","confirm.delete_draft_segment":"Удалить этот сегмент черновика? Оставшийся контур может разделиться на два.","physical.partition_properties":"Свойства перегородки","physical.column_properties":"Свойства колонны","physical.draft_properties":"Свойства сегмента черновика","physical.shape":"Форма","physical.square":"Квадрат","physical.circle":"Круг","physical.diameter":"Диаметр","physical.side":"Сторона","physical.rotation":"Угол поворота","physical.length":"Длина","physical.allowed_range":"Допустимо: {min}–{max} {unit}","physical.delete_segment":"Удалить сегмент","physical.delete_draft":"Удалить весь контур","physical.column_size_title":"Внешняя сторона квадратной колонны, создаваемой кликом (1–150 см).","card.title":"План дома","count.devices":"{n} устр.","empty.no_spaces":"Пространств пока нет.","empty.add_first":"Добавьте первое пространство и загрузите план этажа.","empty.install":"Установите интеграцию House Plan и добавьте запись в «Устройства и службы».","btn.add_space":"Добавить пространство","btn.cancel":"Отмена","btn.save":"Сохранить","btn.close":"Закрыть","btn.delete":"Удалить","btn.edit":"Редактировать","btn.open_in_ha":"Открыть в HA","btn.reset":"Сброс","btn.attach":"Прикрепить…","btn.upload":"Загрузить…","btn.replace":"Заменить…","title.zoom_in":"Приблизить","title.zoom_out":"Отдалить","title.zoom_fit":"Вписать всё","title.add_device":"Добавить устройство на план","title.show_all":"Показать скрытые и деактивированные устройства служебными маркерами (только в этой вкладке)","title.markup":"Разметка комнат: сетка, линии, контуры","title.configure_space":"Настроить пространство","title.add_space":"Добавить пространство","title.markup_add":"Стены: рисуйте непрерывную цепочку; Shift фиксирует 45°. Смена инструмента или выход завершает её как независимые стены","title.markup_merge":"Объединить: клик по одной комнате, затем по соседней с общей стеной","title.markup_split":"Разделить комнату: клик по комнате, затем две точки на её стенах","title.markup_delroom":"Удалить комнату: кликните внутри комнаты и подтвердите удаление","title.need_plan":"Загрузите подложку (план этажа)","markup.add":"Стены","markup.merge":"Объединить","markup.split":"Разделить","markup.resize":"Размер","title.markup_resize":"Изменение размера комнат: тяните доступную ручку горизонтальной или вертикальной стены","markup.hint_resize":"тяните доступную ручку стены · стена упирается в первую небезопасную позицию · Esc — отмена · Ctrl+Z — отмена шага","resize.disabled.diagonal":"Стена расположена под углом: Resize поддерживает только горизонтальные и вертикальные стены","resize.disabled.side-angle":"Примыкающая стена расположена под углом: для Resize обе примыкающие стены должны образовывать прямой угол","resize.disabled.duplicate-physical-wall":"Стену перекрывает отдельная перегородка, незавершённый контур или колонна; удалите или переместите её перед изменением размера","resize.disabled.partial-shared":"Соседняя комната использует только часть этой стены, поэтому её нельзя безопасно двигать как одну общую стену","resize.disabled.unequal-shared":"У соседней комнаты другие конечные точки или длина стены, поэтому стены нельзя безопасно двигать вместе","resize.disabled.multiple-rooms":"Перемещение затронуло бы больше двух комнат","resize.disabled.thickness-conflict":"Толщину стены нельзя безопасно сохранить","resize.disabled.opening-conflict":"Проём на этой стене нельзя безопасно переместить","resize.disabled.invalid-geometry":"Эту стену нельзя переместить без изменения структуры плана","resize.limit_stopped":"Стена остановлена: дальше нарушается ограничение стыков","resize.commit_failed":"Изменение размера отменено: итоговый план не прошёл проверку геометрии","resize.preview_failed":"Стена остановлена в последней безопасной позиции: геометрию плана нельзя сохранить без потерь","markup.opening":"Проём","markup.delete_room":"Удалить комнату","history.undo":"Отменить","history.redo":"Повторить","history.undo_named":"Отменить: {name} (Ctrl+Z)","history.redo_named":"Повторить: {name} (Ctrl+Shift+Z / Ctrl+Y)","history.undo_empty":"Нет операций для отмены","history.redo_empty":"Нет операций для повтора","history.undone":"Отменено: {name}","history.redone":"Повторено: {name}","history.add_room":"Создание комнаты","history.split_room":"Разделение комнаты","history.merge_rooms":"Объединение комнат","history.resize_room":"Изменение размера комнаты","history.wall_thickness":"Изменение толщины стены","history.add_opening":"Добавление дверного или оконного проёма","history.edit_opening":"Изменение проёма","history.move_opening":"Перемещение проёма","history.delete_opening":"Удаление проёма","history.delete_room":"Удаление комнаты","history.delete_room_keep_walls":"Удаление комнаты с сохранением стен","history.delete_room_with_walls":"Удаление комнаты и её стен","history.decor_add":"Добавление объекта декора","history.decor_edit":"Изменение объекта декора","history.decor_move":"Перемещение объекта декора","history.decor_transform":"Трансформация объекта декора","history.decor_delete":"Удаление объекта декора","history.backdrop_transform":"Трансформация подложки плана","title.markup_opening":"Проёмы: клик по стене — добавить, клик по проёму — редактировать","opening.new":"Новый проём","opening.edit":"Проём","opening.door":"Дверь","opening.window":"Окно","opening.gate":"Ворота","opening.passage":"Открытый проём","opening.passage_binding_warning":"При сохранении датчик открытия и замок будут удалены.","opening.invalid_passage_fields":"У открытого проёма на плане «{room}» есть недопустимые параметры: {fields}.","opening.type_label":"Тип","opening.length_label":"Длина, см","opening.contact_label":"Датчик открытия","opening.lock_label":"Замок","opening.none":"— нет —","opening.search_ph":"Поиск: имя или entity_id…","opening.invert":"Инвертировать открыто/закрыто","opening.flip_h":"Петли с другой стороны","opening.flip_v":"Открывается в другую сторону","opening.open":"Открыто","opening.closed":"Закрыто","opening.locked":"Заперто","opening.unlocked":"Не заперто","opening.state_unknown":"недоступно","opening.no_entities":"Датчики не привязаны — статичный символ на плане.","toast.opening_no_wall":"Кликните рядом со стеной комнаты или независимой стеной","opening.host_partition":"Независимая стена","opening.partition_orphan":"Независимая стена этого проёма больше не существует","opening.partition_jamb_margin":"Оставьте от края проёма до торца стены минимум {distance}","opening.rebind_partition":"Привязать к другой независимой стене","confirm.delete_partition_openings_title":"Удалить стену и проёмы?","confirm.delete_partition_openings_body":"В стене есть проёмы: {count}. Они будут удалены вместе со стеной.","confirm.delete_partition_openings_item":"• {type}, {length}","markup.delete":"Удалить","markup.hint_points":"точек: {n} · Shift — шаг 45° · Esc — завершить цепочку · Ctrl+Z — убрать точку · при замыкании будет предложена комната","markup.hint_start":"кликните точку сетки, чтобы начать цепочку стен","tip.lqi":"средний сигнал zigbee:","tip.area":"площадь: {value}","info.device_header":"Устройство на плане","info.model":"Модель","info.state":"Состояние","info.link":"Ссылка","info.manuals":"Инструкции","info.none":"Нет дополнительной информации","marker.new_device":"Новое устройство","marker.name_label":"Имя (отображается на плане)","marker.name_ph":"Название","marker.binding_label":"Привязка к устройству HA","marker.binding_disabled":"деактивировано в Home Assistant","marker.virtual_option":"Виртуальное устройство (без привязки)","marker.search_ph":"Поиск устройства / группы…","marker.nothing_found":"ничего не найдено","marker.room_label":"Комната","marker.room_override":" (переопределить размещение)","marker.room_choose":"— выберите комнату —","marker.room_auto":"— по зоне устройства (авто) —","marker.icon_label":"Иконка","marker.icon_ph":"mdi:… (пусто = авто)","marker.display_label":"Отображение","display.badge":"Значок + состояние","display.icon_ripple":"Значок + состояние и активность","display.static_icon":"Всегда статичный значок","marker.display_hint":"Значок + состояние меняет подложку при работе, открытии, тревоге и недоступности. Значок + состояние и активность дополнительно показывает короткую пульсацию для событий и постоянную — для работы, движения или присутствия. Значение + состояние заменяет значок одним однозначным значением HA. Статичный значок не реагирует на состояния устройства.","marker.display_hint_badge":"Значок и динамическая подложка показывают состояние устройства без обычной пульсации активности. Красная тревога сохраняется.","marker.display_hint_icon":"Значок и динамическая подложка показывают состояние устройства без обычной пульсации активности. Красная тревога сохраняется.","marker.display_hint_icon_ripple":"Значок, динамическая подложка и пульсация: короткая — для событий, постоянная — для работы, движения или присутствия. Красная тревога имеет отдельный приоритет.","marker.display_hint_value":"Значок заменяется одним однозначным значением Home Assistant; подложка продолжает показывать состояние. Красная тревога сохраняется.","marker.display_hint_static_icon":"Подложка в цветах темы и значок всегда остаются одинаковыми. Состояния, активность, недоступность и тревоги не меняют отображение.","marker.static_alarm_warning":"Это устройство может сообщать о тревогах. Статичный режим скрывает визуальную тревогу маркера.","marker.preview.title":"Предпросмотр отображения","marker.preview.actual":"Сейчас","marker.preview.example":"Пример","marker.preview.integration":"Интеграция","marker.preview.source":"Источник отображения","marker.preview.current_state":"Текущее состояние","marker.preview.result":"На плане","marker.preview.details":"Подробнее об источниках","marker.preview.select_source":"Выберите устройство или сущность Home Assistant, чтобы увидеть фактическое отображение.","marker.preview.unknown_provider":"Интеграция неизвестна","marker.preview.virtual_provider":"House Plan · виртуальное устройство","marker.preview.no_source":"Нет активного источника","marker.preview.no_state":"Нет текущего состояния","marker.preview.mixed_states":"Несколько разных состояний","marker.preview.multiple_sources":"Источников: {n}","marker.preview.more_sources":"ещё {n}","marker.preview.scaled":"Предпросмотр уменьшен до {n}%, чтобы поместиться. Сохранённый размер не изменится.","marker.preview.demo_activity":"Показать пример активности","marker.preview.demo_notice":"Временный пример активности; фактическое состояние устройства не меняется.","marker.preview.demo_short":"Показать краткую активность","marker.preview.demo_continuous":"Показать постоянную активность","marker.preview.stop_continuous":"Остановить постоянную активность","marker.preview.demo_short_notice":"Пример краткой активности; фактическое состояние устройства не меняется.","marker.preview.demo_continuous_notice":"Пример постоянной активности; фактическое состояние устройства не меняется.","marker.preview.demo_already_visible":"Текущее состояние уже показывает реальную активность или тревогу.","marker.preview.reduced_motion":"В системе включено уменьшение движения, поэтому обычная активность показана точкой.","marker.preview.reason.neutral":"Нейтральная тёмная подложка","marker.preview.reason.working":"Жёлтая подложка: устройство сейчас работает","marker.preview.reason.working_activity":"Жёлтая подложка и эффект активности: устройство сейчас работает","marker.preview.reason.open":"Оранжевая подложка: физически открыто или разблокировано","marker.preview.reason.cover_icon_state":"Нейтральная подложка; состояние и значок определяет сущность шторы или заслонки","marker.preview.reason.presence":"Эффект активности, пока обнаружено присутствие","marker.preview.reason.event":"Короткий эффект активности после события","marker.preview.reason.transition":"Эффект активности во время движения или смены состояния","marker.preview.reason.media_neutral":"Медиаустройство остаётся на нейтральной тёмной подложке, пока доступно","marker.preview.reason.unavailable":"Приглушённая нейтральная подложка: отключено или недоступно","marker.preview.reason.alarm":"Красная тревожная подложка; тревога отображается всегда","marker.preview.reason.live_states_disabled":"Оформление по живым состояниям отключено в настройках карточки","marker.preview.reason.value_no_state":"Нет подходящего значения состояния — вместо него показан значок","marker.preview.reason.value_ambiguous_sources":"Подходит несколько источников значения — вместо него показан значок","marker.preview.reason.value_non_scalar":"Источник вернул не простое значение — вместо него показан значок","marker.preview.reason.value_virtual":"У виртуального устройства нет значения HA — вместо него показан значок","marker.preview.reason.vacuum_live_plan_only":"Живая позиция и след пылесоса доступны только на полном плане","marker.preview.reason.hidden_design_preview":"Устройство скрыто на плане, но его оформление видно в предпросмотре","marker.preview.reason.composite_power_source":"Состояние берётся из сущности питания устройства; вспомогательные переключатели не учитываются","marker.preview.reason.activity_display_disabled":"Выбранный режим отображения не показывает обычные эффекты активности","marker.preview.reason.ha_disabled":"Привязка отключена в Home Assistant, поэтому устройство будет скрыто на плане","marker.preview.reason.orphaned":"Сохранённая привязка Home Assistant больше не найдена","marker.preview.reason.static_icon":"Статичный режим: состояние устройства не меняет значок","marker.activity_color":"Цвет пульсации активности","marker.ripple_size":"Размер пульсации активности","marker.activity_alarm_note":"Цвет и размер относятся только к обычной активности и не влияют на красную тревогу.","marker.pulse_a11y_alarm":"Тревога","marker.pulse_a11y_event":"Недавнее событие","marker.pulse_a11y_presence":"Обнаружено присутствие","marker.pulse_a11y_transition":"Изменение состояния","marker.pulse_a11y_running":"Работает","marker.state_a11y_neutral":"Обычное состояние","marker.state_a11y_working":"Работает","marker.state_a11y_open":"Открыто","marker.state_a11y_locked":"Заблокировано","marker.state_a11y_unlocked":"Разблокировано","marker.state_a11y_alarm":"Тревога","marker.state_a11y_unavailable":"Недоступно","marker.lqi_a11y_low":"LQI {value}, слабый сигнал","marker.lqi_a11y_mid":"LQI {value}, средний сигнал","marker.lqi_a11y_high":"LQI {value}, сильный сигнал","marker.size_label":"Размер / поворот значка","marker.angle_label":"Поворот","marker.model_label":"Модель","marker.model_ph":"напр. Aqara T&H","marker.link_label":"Ссылка","marker.desc_label":"Описание","marker.desc_ph":"Заметки, характеристики…","marker.manuals_label":"Инструкции (PDF и т.п.)","marker.sub_device":"устройство","marker.sub_z2m_group":" · Z2M-группа","marker.sub_group":"группа","marker.sub_helper":"хелпер","space.new":"Новое пространство","space.header":"Пространство","space.title_label":"Название","space.title_ph":"Например: Гараж","space.plan_label":"Подложка (план)","space.no_plan":"нет подложки","space.plan_alt":"план","room.new":"Новая комната","room.name_label":"Отображаемое имя","room.name_ph":"Например: Терраса","room.area_label":"Зона Home Assistant (свободные)","room.no_area_option":"— без зоны —","room.default_name":"Комната","device.unnamed":"без имени","device.light_group":"группа света","device.fallback":"устройство","device.virtual":"виртуальное устройство","confirm.delete_room":"Удалить комнату «{name}»?","confirm.delete_room_title":"Удалить комнату «{name}»?","confirm.delete_room_body":"Выберите, нужно ли оставить физические стены, принадлежащие только этой комнате. Общие с другой комнатой стены сохраняются всегда.","confirm.merge_rooms":"Слить комнаты «{a}» и «{b}»?","confirm.remove_marker":"Удалить «{name}» с плана? Устройство исчезнет полностью и перестанет участвовать в данных плана. Позже его можно будет добавить заново.","confirm.erase_decor":"Стереть объект «{kind}»? Действие можно отменить из истории редактора.","confirm.delete_space":"Удалить пространство «{title}» со всеми комнатами и разметкой?","space.delete_blocked":"Это пространство всё ещё используется устройствами: {n}. Сначала перенесите их в другое пространство или удалите.","toast.pos_save_failed":"Не удалось сохранить позицию: {err}","toast.no_entity":"У устройства нет подходящей сущности","toast.ha_disabled_action":"Деактивированный объект Home Assistant нельзя использовать на плане.","toast.ha_disabled_show_device":"Деактивированное в Home Assistant устройство нельзя показать на плане. Сначала активируйте его в Home Assistant.","toast.ha_disabled_show_entity":"Деактивированную в Home Assistant сущность нельзя показать на плане. Сначала активируйте её в Home Assistant.","toast.ha_disabled_add":"Деактивированный объект Home Assistant нельзя добавить на план. Сначала активируйте его в Home Assistant.","toast.ha_binding_unverified":"Статус объекта не удалось подтвердить по реестру Home Assistant. Отображение и действия временно недоступны.","toast.markup_needs_server":"Разметка доступна после переноса конфига на сервер","toast.space_order_changed":"Порядок изменён. Если где-то этаж карточки задан номером, проверьте такие панели.","toast.conflict":"Конфиг изменён в другом окне — данные обновлены, повторите последнее действие","toast.cfg_save_failed":"Не удалось сохранить конфиг: {err}","toast.room_overlap":"Контур накладывается на комнату «{name}» — комнаты не должны накладываться","toast.contour_min_edges":"Чтобы замкнуть контур, сначала нарисуйте минимум две грани","toast.contour_cannot_close":"Контур нельзя замкнуть: он вырожден или пересекает сам себя","toast.merge_not_adjacent":"Объединять можно только комнаты с общей стеной","toast.rooms_merged":"Комнаты объединены в «{name}»","toast.split_pick_wall":"Начните разрез на стене комнаты","toast.split_bad_cut":"Разрез — от стены до стены внутри комнаты, без пересечения стен и самого себя","merge.header":"Объединение комнат","merge.hint":"У объединённой комнаты одно имя и одна зона. Вторая зона освобождается — её устройства уйдут с плана, пока их не заберёт другая комната.","merge.keep":"Оставить","merge.no_area":"без зоны","room.split_header":"Новая комната после разделения","toast.room_saved":"Комната сохранена ({n}). Устройств добавлено: {added}. Обведите следующую или выйдите из разметки.","toast.room_saved_no_area":"Комната сохранена ({n}, без зоны). Обведите следующую или выйдите из разметки.","toast.marker_needs_server":"Редактирование устройств доступно после переноса конфига на сервер","toast.virtual_name_required":"Укажите имя виртуального устройства","toast.marker_saved":"Устройство сохранено","toast.marker_removed":"Устройство удалено с плана","toast.integration_missing":"Интеграция House Plan не установлена — управление недоступно","toast.plan_formats":"Поддерживаются SVG, PNG, JPG, WebP","toast.plan_required":"Загрузите подложку — план этажа обязателен","toast.space_added_onboard":"Пространство добавлено. Обведите комнаты: кликайте по точкам сетки и замкните контур.","toast.space_added":"Пространство добавлено","toast.space_saved":"Пространство сохранено","toast.space_deleted":"Пространство удалено","toast.delete_failed":"Ошибка удаления: {err}","toast.error":"Ошибка: {err}","toast.toggle_target_unavailable":"Цель «{name}» недоступна — действие не выполнено","toast.toggle_targets_unavailable":"Цели недоступны: {names}. Действие не выполнено","toast.file_failed":"Файл «{name}» не загружен: {err}","toast.files_attached":"Прикреплено файлов: {n}","err.unknown":"неизвестная ошибка","err.code":"код {code}","err.too_large":"файл больше {mb} МБ","err.bad_ext":"недопустимый тип (нужен PDF/изображение)","err.unauthorized":"нужны права администратора","editor.title":"Заголовок","editor.default_floor":"Стартовое пространство","editor.default_floor_missing":"Стартовое пространство «{id}» больше не существует. Выберите другое.","editor.floor":"Закреплённое пространство","editor.floor_none":"— не закреплять —","editor.floor_index":"YAML-индекс {index} (сохранён)","fixed_floor.loading":"Загружаем закреплённое пространство…","fixed_floor.invalid_title":"Закреплённое пространство недоступно","fixed_floor.invalid_body":"Проверьте floor в конфигурации карточки. Настроенное значение: {value}","editor.icon_size":"Размер иконок, % ширины плана","editor.show_temperature":"Показывать температуру","editor.live_states":"Живые состояния (вкл/выкл, открыто…)","editor.show_signal":"Показывать сигнал zigbee (LQI)","editor.language":"Язык интерфейса","editor.lang_auto":"Авто (профиль HA)","editor.lang_en":"English","editor.lang_ru":"Русский","title.icon_rules":"Правила иконок: какая MDI-иконка достаётся устройству по имени","rules.title":"Правила иконок","rules.hint":"Правила проверяются сверху вниз по строке «имя устройства + модель» (regex без учёта регистра); срабатывает первое совпадение. Если ничего не подошло — решает device class сущности, затем — иконка-заглушка.","rules.pattern_ph":"regex, напр. розетк|plug","rules.icon_ph":"mdi:power-socket-de","rules.add":"Добавить правило","rules.reset":"Сбросить к умолчаниям","rules.test_ph":"Проверьте имя устройства…","rules.invalid":"некорректный regex","rules.saved":"Правила иконок сохранены","btn.up":"Вверх","btn.down":"Вниз","tap.info":"Карточка устройства","tap.more_info":"Диалог HA (more-info)","tap.toggle":"Переключить состояние","marker.tap_label":"Действие по нажатию для этого устройства","tap.toggle_note":"Под селектором показано, какое состояние изменится. Защищённые устройства не переключаются с плана.","import.title":"Создать пространства из этажей HA","import.hint":"Home Assistant уже знает эти этажи. Отметьте, какие превратить в пространства плана — далее для каждого попросим картинку плана. Комнаты затем обводятся вручную по плану.","import.start":"Создать: {n}","import.manual":"Начать с нуля","import.progress":"Этаж {i} из {n}","import.done":"Пространства созданы. Обведите комнаты: кликайте по точкам сетки и замкните контур.","btn.skip":"Пропустить","space.scale_label":"Масштаб (размер клетки сетки)","space.scale_unit":"см на клетку","space.scale_unit_imperial":"дюйм на клетку","space.display_section":"Отображение","space.show_borders":"Всегда отображать границы комнат","space.zero_wall_style":"Стены нулевой толщины","space.zero_wall_dashed":"Пунктирные","space.zero_wall_solid":"Сплошные","space.zero_wall_help":"Пунктирные стены пропускают свет. Сплошные стены блокируют свет, даже при нулевой толщине.","space.show_names":"Отображать названия комнат (перетаскиваются)","space.room_color":"Цвет границ и названий","space.opacity":"Прозрачность","space.fill_label":"Заливка комнат","fill.none":"Нет","fill.lqi":"По силе зигби-сигнала","fill.light":"По освещению","fill.custom":"Свой цвет","space.custom_fill":"Цвет заливки","space.source_file":"У меня есть картинка плана","space.source_draw":"Нет подложки — нарисую комнаты вручную","space.orientation":"Холст","orient.landscape":"Альбомный","orient.portrait":"Портретный","orient.square":"Квадрат","fill.temp":"По температуре","space.temp_min":"Комфорт от","space.temp_max":"до","tip.temp_avg":"средняя температура:","tip.hum_avg":"средняя влажность:","space_card.button":"Перейти к пространству","space_card.not_found":"Пространство «{id}» не найдено","space_card.loading":"Загрузка…","continuity.restore_plan":"Восстанавливаем план…","continuity.restore_connection":"Восстанавливаем подключение к устройствам…","continuity.retry":"Повторить","editor.space":"Пространство","editor.show_button":"Показывать кнопку","editor.button_label":"Текст кнопки","editor.button_target":"Путь дашборда (куда вести)","marker.sub_entity":"сущность","title.general_settings":"Общие настройки","gs.title":"Общие настройки","gs.hint":"Цвета заливок действуют на все пространства; у каждого цвета своя прозрачность. Какой режим заливки использует пространство — задаётся в его диалоге.","gs.light_group":"Заливка: освещение","gs.light_on":"Свет включён","gs.light_off":"Весь свет выключен","gs.temp_group":"Заливка: температура","gs.temp_cold":"Холодно","gs.temp_ok":"Комфорт","gs.temp_hot":"Жарко","gs.lqi_group":"Заливка: зигби-сигнал","gs.lqi_low":"Слабый сигнал","gs.lqi_high":"Сильный сигнал","gs.reset":"Сбросить к умолчаниям","gs.saved":"Общие настройки сохранены","space.show_lqi":"Показывать зигби-сигнал (LQI) у устройств","space.hide_decor":"Скрыть декоративный слой","space.hide_decor_tip":"Линии, фигуры, надписи и мебель остаются на месте — их видно в редакторе подложки, но не на плане.","space.hide_openings":"Скрыть проёмы","space.hide_openings_tip":"Двери, окна и ворота не рисуются, но продолжают работать: свет проходит, солнце светит в окна, датчики открытия срабатывают. В редакторе плана проёмы видно всегда.","gs.light_none":"Нет источников света","mode.plan":"Редактор плана","mode.devices":"Редактор устройств","display.value":"Значение + состояние","marker.subarea":"без зоны, вручную","device.new":"Новое устройство — откройте его редактор, чтобы снять отметку","opening.unlock_action":"Открыть замок","opening.lock_action":"Закрыть замок","opening.lock_pending":"Выполняется…","title.close_editor":"Закрыть редактор (вернуться к просмотру)","devbar.add":"Добавить","devbar.show_all":"Скрытые и деактивированные","devbar.rules":"Правила иконок","device_inbox.button":"Устройства","device_inbox.title":"Устройства на плане","device_inbox.search":"Поиск по устройствам, сущностям и интеграциям…","device_inbox.add_virtual":"Добавить виртуальное устройство","device_inbox.tab_on_plan":"На плане","device_inbox.tab_available":"Доступны","device_inbox.tab_hidden":"Скрытые","device_inbox.tab_readd":"Доступны снова","device_inbox.only_new":"Только новые","device_inbox.show_entities":"Показывать сущности","device_inbox.show_hidden":"Показывать скрытые на плане","device_inbox.new":"Новое","device_inbox.find":"Найти на плане","device_inbox.edit":"Настроить","device_inbox.hide":"Скрыть","device_inbox.show":"Показать","device_inbox.add":"Добавить","device_inbox.readd":"Добавить заново","device_inbox.hide_available":"Скрыть из списка","device_inbox.show_more":"Показать ещё","device_inbox.more_actions":"Другие действия","device_inbox.find_hidden_hint":"Сначала включите «Показывать скрытые на плане»","device_inbox.show_disabled":"Перед показом активируйте эту привязку в Home Assistant","device_inbox.empty_on_plan":"На плане пока нет подходящих устройств.","device_inbox.empty_available":"Нет доступных устройств для добавления.","device_inbox.empty_hidden":"Скрытых устройств нет.","device_inbox.empty_readd":"Нет удалённых устройств, доступных для повторного добавления.","device_inbox.reason_visible_auto":"Найдено автоматически","device_inbox.reason_visible_explicit":"Добавлено явно","device_inbox.reason_manual_hidden":"Скрыто пользователем","device_inbox.reason_automatic_hidden":"Скрыто автоматически","device_inbox.reason_service_entry":"Служебная запись реестра","device_inbox.reason_excluded_integration":"Интеграция исключена фильтрами устройств","device_inbox.reason_excluded_domain":"Непространственный тип сущности","device_inbox.reason_grouped_light":"Представлено группой света комнаты","device_inbox.reason_represented_by_parent":"Представлено родительским устройством","device_inbox.reason_removed":"Ранее удалено с плана","device_inbox.reason_available":"Можно добавить на план","device_inbox.reason_no_bound_room":"Зона HA не связана с комнатой плана","device_inbox.status_ha_disabled":"Отключено в Home Assistant","device_inbox.status_orphaned":"Привязка больше не найдена в Home Assistant","device_inbox.status_unverified":"Реестр Home Assistant временно недоступен","device_inbox.saved":"Список устройств обновлён","space.roomcard_section":"В карточке комнаты:","space.label_temp":"Температура","space.label_hum":"Влажность","space.label_lqi":"Средний Zigbee-сигнал","space.label_light":"Свет вкл/выкл","roomcard.light_on":"Вкл","roomcard.light_off":"Выкл","roomcard.light_partial":"{on} из {total}","toast.split_pick_inside":"Промежуточные точки разреза — внутри комнаты","mode.decor":"Редактор подложки","decor.select":"Выбрать","decor.line":"Линия","decor.rect":"Прямоугольник","decor.ellipse":"Овал","decor.text":"Надпись","decor.erase":"Стереть","decor.erase_confirm_title":"Стереть объект?","decor.color":"Цвет","decor.width":"Толщина линии","decor.line_style":"Стиль линии","decor.line_style_solid":"Сплошная","decor.line_style_dashed":"Пунктирная","decor.fill":"Заливка","decor.fill_color":"Цвет заливки","decor.length":"Длина","decor.size":"Размер","decor.angle":"Угол поворота, °","decor.text_size":"Размер текста","decor.backdrop_properties":"Свойства картинки-подложки","decor.text_title":"Надпись","decor.object_title":"Редактирование: {kind}","decor.text_label":"Текст","decor.live_group":"Вставить переменную HA","decor.live_entity":"Сущность","decor.live_entity_ph":"выберите сущность","decor.live_attr":"Значение","decor.live_attr_ph":"выберите состояние или атрибут","decor.live_state":"Состояние","decor.backdrop":"Картинка-подложка","decor.backdrop_hint":"Тяните картинку — перемещение; угол — размер; верхнюю ручку — поворот. Shift меняет пропорции или снимает шаг угла.","decor.backdrop_reset":"Вернуть картинку","decor.backdrop_reset_done":"Картинка вернулась на своё место и в свой размер","marker.icon_auto":"Авто: {icon} (по правилам иконок; выберите свою, чтобы заменить)","marker.icon_pin_auto":"Закрепить","mode.plan_tip":"Редактор плана — геометрия дома: рисование и объединение/разделение комнат, привязка к зонам HA, двери, окна и ворота, карточки комнат, масштаб","mode.devices_tip":"Редактор устройств — всё про значки: перетаскивание, клик — настройка привязки/иконки/отображения, виртуальные устройства, правила иконок","mode.decor_tip":"Редактор подложки — чисто визуальный декор под планом: линии, прямоугольники, овалы и надписи, не реагирующие на клики","space.glow_enabled":"Свечение источников света","gs.glow_group":"Свечение источников света","gs.glow_base":"Темнота дома","gs.glow_light":"Цвет света по умолчанию / интенсивность","gs.wall_group":"Стены","gs.wall_fill":"Цвет заливки стен","gs.glow_radius":"Радиус свечения","gs.unit_m":"м","gs.unit_ft":"фут","marker.controls_label":"Управляет другими источниками света","marker.controls_hint":"При действии «Переключить» клик разом переключает все добавленные здесь источники. Собственная сущность маркера управляется напрямую и сюда не добавляется; чтобы считать её светом, включите флаг ниже.","marker.controls_filter":"Поиск ламп и выключателей…","info.controls":"Управляет","marker.glow_radius_label":"Радиус свечения","marker.glow_radius.help":"Задаёт радиус свечения в метрах или футах; пустое значение использует радиус из общих настроек.","marker.glow_radius.help.aria":"Подсказка: радиус свечения","markup.wallthick":"Толщина","markup.select":"Выбрать","title.markup_wallthick":"Толщина — клик по стене задаёт толщину от 0 до 100 см.","markup.hint_wallthick":"клик по стене · Esc закрывает без применения","wallthick.field":"Толщина","wallthick.unit_cm":"см","wallthick.unit_in":"дюйм","wallthick.apply_room":"Применить ко всем стенам комнаты","markup.draw_wall_title":"Толщина каждого нового сегмента стены (0–100 см). Общие стены сохраняют существующее значение.","room.queue_progress":"Комната {current} из {total}","toast.wall_rooms_saved":"Создано комнат: {n}","toast.wall_chain_saved":"Цепочка стен сохранена","toast.wallthick_pick":"Кликните по стене","toast.wallthick_set":"Толщина стены задана","toast.wallthick_cleared":"Толщина стены убрана","toast.physical_range":"Введите значение от {min} до {max} {unit}","toast.zero_wall_opening_conflict":"Сначала удалите проём на этом участке стены.","toast.zero_wall_ambiguous":"Не удалось однозначно выбрать участок стены. Уточните геометрию узла.","toast.zero_wall_migration_blocked":"Пространство не преобразовано: {reason}. Данные не изменены.","toast.physical_angle":"Введите угол поворота от 0° до 90°, не включая 90°","toast.physical_limit":"В пространстве достигнут лимит объектов этого типа","toast.geometry_unsafe":"Изменение отменено: геометрию стен нельзя безопасно построить.","junction.limit_angle":"Слишком острый угол между стенами: {actual}°, минимум {limit}°.","junction.limit_valence":"Слишком много стен в одном узле: {actual}, максимум {limit}.","junction.limit_length":"Слишком короткий участок стены: {actual} см, минимум {limit} см.","junction.limit_distance":"Стены и узлы слишком близко: {actual} см, минимум {limit} см.","junction.limit_clearance":"Внутри комнаты не остаётся места: {actual} см², минимум {limit} см².","junction.limit_check_failed":"Проверку стыков выполнить не удалось — изменение не сохранено","toast.wall_model_migration_blocked":"Не удалось обновить модель стен: {reason}. План не изменён. Запустите «Оптимизировать планы»; если ошибка повторится, исправьте конфликтующую геометрию стен.","toast.wall_model_client_outdated":"Обновите карточку и перезагрузите страницу перед редактированием плана.","wall_model.reason.invalid-room":"некорректный контур комнаты","wall_model.reason.zero-length":"отрезок стены нулевой длины","wall_model.reason.third-owner":"стена принадлежит более чем двум комнатам","wall_model.reason.duplicate-id":"конфликт идентификаторов стен","wall_model.reason.thickness-conflict":"конфликт значений толщины стены","wall_model.reason.opening-host":"проём нельзя однозначно привязать к стене","toast.delete_room_pick":"Кликните внутри комнаты, которую нужно удалить","toast.plan_snap_ambiguous":"Увеличьте масштаб, чтобы выбрать узел стены","toast.wall_repair_ambiguous":"У контура несколько возможных соединений. Увеличьте масштаб и соедините стены явно.","toast.wall_repair_too_large":"Разрыв больше 2 см. Соедините стены перед созданием комнаты.","toast.wall_repair_changed":"Геометрия стен изменилась. Попробуйте создать комнату ещё раз.","toast.opening_on_zero_wall":"Проёмы на стене нулевой толщины запрещены","marker.from_ha_option":"Выбрать из списка HA","marker.show_entities":"Показывать сущности","marker.show_entities_tip":"Добавляет в список не только устройства, но и все их сущности","marker.pick_ph":"Выберите устройство…","room.open_area":"Открыть зону в HA","view.volumetric":"Объёмный вид","view.flat":"Плоский вид","kiosk.title":"Размеры на этом экране","kiosk.hint":"Хранится только на этом устройстве — у каждого настенного планшета или ТВ свои удобные размеры.","kiosk.icon_scale":"Размер значков устройств","kiosk.font_scale":"Размер текста карточек комнат","editor.kiosk":"Режим настенного устройства (киоск)","editor.cycle":"Автосмена пространств каждые N секунд (киоск, 0 = выкл)","room.settings_title":"Настройки комнаты","room.settings_section":"Настройки комнаты (переопределяют пространство)","room.fill_label":"Заливка в ЭТОЙ комнате","fill.inherit":"Как у пространства","room.custom_fill_space":"Цвет пространства","room.custom_fill_own":"Цвет комнаты","room.temp_src_label":"Источник температуры","room.hum_src_label":"Источник влажности","room.src_average":"Средняя по датчикам комнаты (по умолчанию)","room.src_pick":"Конкретное устройство или сущность HA","room.src_ph":"Выберите источник…","toast.room_updated":"Комната обновлена","space.card_font":"Размер шрифта карточек комнат (всё пространство)","room.sizes_section":"Размеры шрифтов","room.name_scale":"Размер названия","room.label_scale":"Размер подписей","preview.room_name":"Гостиная","toast.cfg_reload_failed":"Не удалось перечитать план с сервера: {err}","room.settings_short":"Настройки комнаты","room.unnamed":"Комната без имени","marker.use_climate_temp":"Учитывать температуру устройства в комнате","marker.use_climate_temp_tip":"Добавляет current_temperature кондиционера или термостата в среднюю температуру комнаты. Внешний бейдж со значением настраивается отдельно ниже.","marker.value_badge_title":"Бейдж со значением","marker.value_badge.help":"Показывает одно выбранное значение рядом со значком. Не влияет на комнатные показатели, свет, Glow и действие по нажатию.","marker.value_badge.help.aria":"Подсказка: бейдж со значением устройства","marker.value_badge_enabled":"Отображать бейдж со значением","marker.value_badge_source":"Значение","marker.value_badge_source.help":"Выберите конкретное состояние, поддерживаемый атрибут или производное значение этого устройства.","marker.value_badge_source.help.aria":"Подсказка: источник значения бейджа","marker.value_badge_position":"Расположение","marker.value_badge_position.help":"Выбранная сторона сохраняется постоянно и не переворачивается автоматически у края плана.","marker.value_badge_position.help.aria":"Подсказка: расположение бейджа","marker.value_badge_right":"Справа","marker.value_badge_bottom":"Снизу","marker.value_badge_left":"Слева","marker.value_badge_top":"Сверху","marker.value_badge_empty":"У этого устройства нет доступных значений","marker.value_badge_static":"Статичный значок не показывает живые значения. Настройка сохранится.","marker.value_badge_missing":"Источник недоступен","marker.value_badge_missing_hint":"Сохранённый источник сейчас недоступен. Можно сохранить его, отключить бейдж или выбрать другой.","marker.value_badge_duplicate":"То же значение уже показано внутри значка.","marker.value_badge_state":"Состояние · {name}","marker.value_badge_marker_state":"Состояние источника света · {name}","marker.value_badge_lqi":"Качество Zigbee-сигнала (среднее)","marker.value_badge_attr_current_temperature":"Текущая температура · {name}","marker.value_badge_attr_temperature":"Целевая температура · {name}","marker.value_badge_attr_current_humidity":"Текущая влажность · {name}","marker.value_badge_attr_humidity":"Влажность · {name}","marker.value_badge_attr_current_position":"Положение · {name}","marker.value_badge_attr_percentage":"Скорость · {name}","marker.value_badge_attr_brightness":"Яркость · {name}","marker.value_badge_attr_volume_level":"Громкость · {name}","marker.value_badge_attr_battery_level":"Заряд батареи · {name}","marker.value_badge_attr_fan_speed":"Скорость вентилятора · {name}","marker.light_role_label":"Является источником света","marker.light_role.help":"Определяет, является ли сам маркер пространственным источником света. Привязанные выше лампы остаются независимыми источниками для комнаты.","marker.light_role.help.aria":"Подсказка: является ли устройство источником света","marker.light_role_auto_yes":"Автоматически (источник света)","marker.light_role_auto_no":"Автоматически (не источник света)","marker.light_role_always":"Всегда источник света","marker.light_role_never":"Никогда не источник света","marker.light_entity_label":"Ведущая сущность света","marker.light_entity.help":"Для составного устройства выберите сущность, состояние, цвет и яркость которой представляют этот источник на плане. Автоматический вариант сохраняет совместимость со старыми планами.","marker.light_entity.help.aria":"Подсказка: ведущая сущность света","marker.light_entity_auto":"Автоматически ({entity})","marker.light_entity_none":"нет управляемой сущности","marker.light_entity_missing":"Сохранённая сущность {entity} недоступна. Временно используется {fallback}; выбор восстановится, если сущность вернётся.","marker.toggle_entity_label":"Переключаемая сущность","marker.toggle_entity.help":"Для составного устройства выберите собственную лампу или переключатель, которой управляет нажатие. Автоматический вариант сохраняет прежние правила цели и не зависит от ведущей сущности света.","marker.toggle_entity.help.aria":"Подсказка: переключаемая сущность","marker.toggle_entity_auto":"Автоматически ({entity})","marker.toggle_entity_none":"нет собственной управляемой сущности","marker.toggle_entity_missing":"Сохранённой сущности {entity} больше нет среди выбираемых каналов этого маркера. Временно используется {fallback}; выбор восстановится, если сущность вернётся.","marker.glow_color_label":"Цвет и яркость свечения","marker.glow_mode.help":"Используйте данные источника, задайте только цвет или зафиксируйте цвет и яркость. Минимум — 1%; чтобы отключить источник, выберите «Никогда».","marker.glow_mode.help.aria":"Подсказка: цвет и яркость свечения","marker.glow_mode_auto":"Из источника","marker.glow_mode_color":"Задать цвет","marker.glow_mode_fixed":"Задать цвет и яркость","marker.glow_color":"Цвет свечения","marker.glow_brightness":"Яркость","marker.glow_disabled_never":"Настройки свечения недоступны: маркер явно исключён из источников света.","marker.glow_disabled_auto":"В автоматическом режиме у этого маркера не найден пространственный источник света.","marker.glow_disabled_no_entity":"Для пространственного источника нет активной управляемой сущности Home Assistant.","marker.glow_passive_hint":"У источника нет собственных данных Home Assistant. Задайте цвет и яркость вручную; радиус остаётся доступным.","marker.control_broken":"Сохранённый источник отсутствует или больше не отмечен как источник света","marker.control_missing_label":"Отсутствующий источник на плане","marker.control_passive":"пассивный источник","toast.marker_control_cycle":"Такая связь создаст циклическую цепочку управления светом.","toast.marker_binding_required":"Сначала выберите устройство Home Assistant, затем добавьте управляемый источник света.","confirm.unlock":"Открыть замок «{name}»?","toast.files_migrate_failed":"Не удалось перенести вложения к новой привязке, ссылки остались на старые файлы: {err}","space.pick_saved":"Уже загруженные","space.pick_saved_hint":"Планы, сохранённые на сервере, включая отцеплённые ранее","space.no_saved":"На сервере пока нет сохранённых планов.","space.loading":"Загрузка…","space.used_by":"используется: {list}","space.in_use":"План используется пространством — сначала отцепите его","btn.use":"Выбрать","confirm.delete_plan":"Удалить файл плана «{name}» с сервера? Действие необратимо.","toast.plans_list_failed":"Не удалось получить список планов: {err}","toast.plan_delete_failed":"Не удалось удалить план: {err}","marker.hide":"Скрыть","marker.hide_tip":"Устройство исчезнет с плана после сохранения, но продолжит участвовать в расчёте сигнала комнаты. Вернуть его можно через каталог «Устройства» в редакторе устройств.","marker.show":"Показать","marker.show_tip":"Устройство снова появится на плане после сохранения.","marker.hidden_ghost":"Устройство скрыто пользователем","marker.ha_disabled_device":"Устройство деактивировано в Home Assistant и скрыто с плана.","marker.ha_disabled_entity":"Сущность деактивирована в Home Assistant и скрыта с плана.","marker.ha_disabled_all_entities":"У устройства нет активных сущностей Home Assistant, поэтому оно скрыто с плана.","marker.ha_registry_limited":"Полный реестр Home Assistant недоступен этому пользователю. Неподтверждённый объект временно нельзя показывать или использовать.","marker.delete_tip":"Полностью удалить устройство с плана и из всех расчётов. Позже его можно добавить заново.","tap.run":"Запустить автоматизацию/скрипт/сцену","tap.cover":"Открыть/закрыть (шторы/жалюзи)","marker.run_target_label":"Что запускать","marker.run_search_ph":"Поиск: автоматизация, скрипт или сцена…","marker.run_target_gone":"Цель {id} не найдена — выберите заново","marker.tap_confirm":"Спрашивать подтверждение","marker.tap_confirm_tip":"Перед выполнением показать диалог подтверждения — защита от случайных нажатий.","marker.toggle_hint_single":"Цель: {name} ({id}).","marker.virtual_light_target":"Виртуальный свет: {name}.","marker.virtual_light_current":"Ручное состояние: {state} → {effect}.","marker.virtual_light_state_on":"включён","marker.virtual_light_state_off":"выключен","marker.toggle_hint_group":"Переключатся источники ({count}): {names}.","marker.toggle_hint_current":"Сейчас: {state} → {effect}.","marker.toggle_hint_group_current":"Сейчас включено {on} из {count} → {effect}.","marker.toggle_hint_skipped":"Не будут затронуты ({count}): {targets}.","marker.toggle_effect_turn_on":"включится","marker.toggle_effect_turn_off":"выключится","marker.toggle_effect_open":"откроется","marker.toggle_effect_close":"закроется","marker.toggle_effect_stop":"остановится","marker.toggle_effect_toggle":"состояние переключится","marker.toggle_skip_missing":"не найдено","marker.toggle_skip_ha_disabled":"деактивировано в HA","marker.toggle_skip_unavailable":"недоступно","marker.toggle_skip_unsupported":"переключение не поддерживается","marker.toggle_skip_secure":"заблокировано из соображений безопасности","marker.toggle_none_no_binding":"У устройства нет привязки, состояние переключать нечем. По нажатию ничего не произойдёт.","marker.toggle_none_no_actionable_entity":"У устройства нет состояния, которое можно переключить. По нажатию ничего не произойдёт.","marker.toggle_none_configured_targets_missing":"Настроенные цели сейчас недоступны. Собственная сущность устройства не будет подставлена вместо них.","marker.toggle_none_ha_disabled":"Цель деактивирована в Home Assistant и не может использоваться на плане.","marker.toggle_none_unavailable":"Цель сейчас недоступна. По нажатию ничего не произойдёт.","marker.toggle_none_unsupported":"Для этой сущности Home Assistant не предоставляет безопасного переключения. По нажатию ничего не произойдёт.","marker.toggle_none_secure":"Переключение замков, сигнализации и защитных ворот с плана заблокировано из соображений безопасности.","run.automation":"автоматизация","run.script":"скрипт","run.scene":"сцена","confirm.tap_run":"Запустить «{name}»?","confirm.tap_toggle":"Переключить «{name}»?","confirm.tap_cover":"Открыть/закрыть «{name}»?","confirm.current_state":"Текущее состояние: {state}","confirm.expected_state":"После переключения: {state}","confirm.group_current":"включено {on} из {total}","confirm.group_all_on":"все включены","confirm.group_all_off":"все выключены","confirm.unavailable_targets":"Недоступно: {count}","confirm.expected_by_ha":"Состояние определит Home Assistant","confirm.state_on":"Включено","confirm.state_off":"Выключено","confirm.state_open":"Открыто","confirm.state_closed":"Закрыто","confirm.state_opening":"Открывается","confirm.state_closing":"Закрывается","confirm.state_stopped":"Остановлено","confirm.state_unknown":"Неизвестно","toast.run_started":"Запущено: {name}","toast.run_target_missing":"Цель запуска не найдена — проверьте настройки устройства","toast.run_target_required":"Выберите автоматизацию, скрипт или сцену","toast.tap_target_changed":"Цель действия изменилась. Повторите попытку.","toast.virtual_light_toggle_failed":"Не удалось переключить виртуальный свет: {err}","toast.value_badge_source_required":"Выберите значение для бейджа","btn.run":"Выполнить","vac.section":"Робот-пылесос: живая позиция","vac.autocal":"Настроить автоматически","vac.live":"Живая позиция на плане","vac.trail":"Показывать путь робота","vac.cal_maps":"Откалиброваны карты: {maps}","vac.autocal_no_rooms":"Интеграция не отдаёт список комнат — откройте «Подогнать вручную»","vac.autocal_no_match":"Не совпали имена комнат (нужно ≥3 общих) — откройте «Подогнать вручную»","vac.autocal_done":"Готово: привязка по {rooms} комнатам. Запустите уборку и проверьте","vac.cal_need_pos":"Робот сейчас не отдаёт координаты — запустите уборку и поставьте на паузу","vac.cal_done":"Калибровка сохранена. Запустите уборку и проверьте","vac.cal_cancelled":"Калибровка отменена","vac.fit":"Подогнать вручную","vac.fit_hint":"Перетащите карту робота на место, растяните за уголки","vac.fit_rotate":"Повернуть 90°","vac.fit_mirror":"Отразить","vac.trail_never":"Не показывать никогда","vac.trail_cleaning":"Во время уборки","vac.trail_always":"Показывать всегда","gs.bg_group":"Фон сцены","gs.bg_color":"Цвет фона вокруг плана","gs.bg_default":"Как в теме","gs.bg_theme":"по умолчанию — из темы","gs.bg_mode":"Фон плана","gs.bg_static":"Статичный цвет","gs.bg_daynight":"Следует за Солнцем","gs.bg_daynight_hint":"Фон проходит рассвет, день, закат и ночь по данным sun.sun, а при их недоступности — по локальным часам. Компас для фона не нужен.","gs.sun_group":"Солнце","gs.sun_missing":"Сущность sun.sun не найдена — фон следует локальным часам, а оконные лучи недоступны.","gs.north":"Север на плане","gs.north_ph":"не задан","gs.north_hint":"Север нужен только для оконных лучей (шаг 1°, с Shift 15°); солнечный фон работает без него.","gs.north_clear":"Сбросить","gs.north_letter":"С","gs.sun_rays":"Солнце в окнах","gs.about_group":"О карточке","gs.about_version":"Houseplan Card v{v}","gs.about_github":"GitHub · документация и issues","gs.about_telegram":"Чат в Telegram","space.bg_color":"Цвет фона вокруг плана","space.bg_inherit":"Наследовать общий","space.bg_inherited":"наследуется из общих настроек","space.bg_mode":"Фон плана","space.north":"Север на плане (переопределение)","space.north_inherited":"наследуется: {v}","space.sun_rays":"Солнце в окнах","space.sun_inherit":"Наследовать общий","space.sun_on":"Вкл","space.sun_off":"Выкл","canvas.far_objects":"Объектов далеко от плана: {n}","canvas.show_far":"Показать","canvas.home_tip":"План там — нажмите, чтобы вписать","gs.grid_group":"Обслуживание планов","gs.grid_hint":"Обновляет модели данных, выравнивает элементы по сетке и объединяет лишние фрагменты стен. Перед записью будет показан точный отчёт.","gs.align_all":"Оптимизировать планы","gs.align_title":"Оптимизировать планы","gs.align_none":"Все планы уже используют актуальную и оптимальную модель данных.","gs.optimize_no_automatic_changes":"Автоматических изменений нет. Проверьте пункты ниже.","gs.align_count":"Сдвинется элементов: {n} из {total}, максимум на {cm} см.","gs.align_where":"Наибольший сдвиг — в пространстве «{s}».","gs.align_turned":"Проёмов с исправлением угла: {n}.","gs.align_removed_drafts":"Схлопнувшиеся на сетке некорректные контуры удалены: {n}.","gs.optimize_redundant_drafts":"Сохранённые цепочки стен, полностью скрытые стенами комнат, удалены: {n}.","gs.align_preflight_failed":"Не удалось безопасно проверить геометрию следующих пространств: {spaces}{more}.","gs.align_preflight_hint":"Планы не изменены. Скопируйте диагностику кнопкой ниже и приложите её к отчёту об ошибке вместе с экспортом пространства.","gs.preflight_reason_prepare-exception":"Не удалось подготовить геометрию пространства (исключение при сборке модели)","gs.preflight_reason_wall-null":"Тело стен не построилось (объединение вернуло пустоту)","gs.preflight_reason_wall-degraded-extra":"Тело стен деградировало с лишней геометрией","gs.preflight_reason_wall-failed-core":"Ядро тела стен не собралось","gs.preflight_reason_wall-exception":"Построение стен упало с исключением","gs.preflight_reason_floor-null":"Контур пола не построился","gs.preflight_reason_floor-exception":"Построение пола упало с исключением","gs.preflight_copy":"Скопировать диагностику","gs.preflight_copied":"Диагностика скопирована","gs.preflight_update_hint":"Версии карточки и интеграции различаются — обновите House Plan и повторите.","gs.align_preflight_space":"Пространство {n}","gs.align_preflight_more":" и ещё {n}","gs.optimize_changes":"Миграций модели: {m}; обновлено пространств: {c}; устранён шум координат: {p}; объединено отрезков реальных стен: {w}; объединено отрезков стен нулевой толщины: {s}; независимых: {i}.","gs.zero_walls_migrated":"Преобразовано виртуальных участков: {n}.","gs.wall_segments_migrated":"Стабилизировано сегментов стен: {n}.","gs.optimize_lattice_summary":"Канонизировано шумовых значений координат: {n}; максимальный сдвиг: {cm} см.","gs.optimize_lattice_space":"{space}: канонизировано значений координат: {n}; оставлено значений вне сетки: {far}.","gs.optimize_coincident_partitions":"Скрытые участки независимых стен поглощены стенами комнат: {n}.","gs.optimize_openings_rehosted":"Проёмы перепривязаны к стенам комнат: {n}.","gs.optimize_walls_straightened":"Выпрямлено стен: {n}; максимальное перемещение: {cm} см.","gs.optimize_walls_straightened_where":"Максимальная правка стены: {s}.","gs.optimize_walls_straighten_skipped":"Оставлено почти осевых стен, которые нельзя безопасно выпрямить: {n}.","gs.optimize_glow_migration":"Старый Glow: пространств — {spaces} → без заливки данных + независимый Glow; комнат — {rooms} → наследуемая заливка + независимый Glow.","gs.optimize_references":"Исправлено ссылок: пространства — {spaces}; комнаты — {rooms}; позиции — {positions}; устройств отвязано от отсутствующих пространств — {detached}.","gs.optimize_reference_more":", и ещё {n}","gs.optimize_orphans_removed":"Убрано забытых записей: {total} — подписи комнат: {rooms}; устройства: {devices}; групповые метки: {groups}. Все они принадлежали пространствам, удалённым ранее.","gs.optimize_live_positions":"Старые позиции в удалённых пространствах принадлежат существующим объектам: {n}{names}. Они будут сохранены.","gs.optimize_live_positions_remove":"Старые позиции в удалённых пространствах принадлежат существующим объектам: {n}{names}. Они выбраны для удаления.","gs.optimize_live_names":": {names}{more}","gs.optimize_live_remove":"Убрать старые позиции","gs.optimize_live_keep":"Сохранить старые позиции","gs.optimize_live_selected":"Старые позиции будут убраны после применения оптимизации.","gs.optimize_unverified":"Не удалось безопасно проверить позиций: {n}. Они оставлены без изменений.","gs.optimize_registry_limited":"Для безопасной проверки нужен полный доступ администратора к реестрам Home Assistant.","gs.optimize_vacuum_warning":"Сопоставления комнат пылесоса требуют проверки: {n}.","gs.optimize_details":"Подробности","gs.optimize_details_more":"И ещё записей: {n}.","gs.optimize_detail_removed":"будет удалено","gs.optimize_detail_live":"будет сохранено","gs.optimize_detail_unverified":"не проверено","gs.optimize_detail_room_label":"подпись комнаты","gs.optimize_detail_device":"устройство","gs.optimize_detail_group":"групповая метка","gs.optimize_detail_unknown":"неизвестный владелец","gs.optimize_detail_item":"{status}: {kind} {id}; прежнее пространство {space}","gs.align_warn":"Элементы, намеренно поставленные между узлами, будут сдвинуты. После операции доступна одна отмена — только до следующего изменения плана.","gs.align_run":"Оптимизировать","gs.align_done":"Планы оптимизированы: сдвинуто элементов — {n}, обслужено записей — {m}, исправлено ссылок — {r}","gs.optimize_undo":"Отменить последнюю оптимизацию","gs.optimize_undone":"Последняя оптимизация отменена","decor.furniture":"Мебель","furn.title":"Библиотека мебели","furn.symbol":"Символ","furn.group_furniture":"Мебель","furn.group_appliance":"Техника","furn.group_sanitary":"Сантехника","furn.group_other":"Прочее","furn.width":"Ширина","furn.depth":"Глубина","furn.pick_hint":"Выберите символ и кликните по плану.","furn.place_hint":"Кликните по плану — предмет встанет к ближайшей стене. Shift — свободно.","furn.sym_sofa":"Диван","furn.sym_armchair":"Кресло","furn.sym_coffee_table":"Журнальный столик","furn.sym_table_dining":"Обеденный стол","furn.sym_table_round":"Круглый стол","furn.sym_chair":"Стул","furn.sym_desk":"Письменный стол","furn.sym_bed_double":"Двуспальная кровать","furn.sym_bed_single":"Односпальная кровать","furn.sym_nightstand":"Тумбочка","furn.sym_wardrobe":"Шкаф","furn.sym_bookshelf":"Стеллаж","furn.sym_fridge":"Холодильник","furn.sym_stove":"Плита","furn.sym_dishwasher":"Посудомоечная машина","furn.sym_washer":"Стиральная машина","furn.sym_dryer":"Сушильная машина","furn.sym_tv":"Телевизор","furn.sym_ac":"Кондиционер","furn.sym_water_heater":"Бойлер","furn.sym_toilet":"Унитаз","furn.sym_bathtub":"Ванна","furn.sym_shower":"Душ","furn.sym_sink":"Раковина","furn.sym_kitchen_sink":"Кухонная мойка","furn.sym_bidet":"Биде","furn.sym_stairs":"Лестница","furn.sym_fireplace":"Камин","furn.sym_plant":"Растение","furn.sym_rug":"Ковёр","common.yes":"Да","common.no":"Нет","vac.diag_source":"Источник","vac.diag_platform":"Интеграция","vac.diag_status":"Статус","vac.diag_position":"Позиция","vac.diag_rooms":"Комнаты","vac.diag_rooms_value":"{total} · совпало имён: {matched} · {readiness}","vac.autocal_ready":"автокалибровка доступна","vac.autocal_not_ready":"нужно 3 совпавших имени","vac.diag_path":"Путь интеграции","vac.diag_map":"ID карты","vac.source_none":"не выбран","vac.source_status_ok":"Готов","vac.source_status_missing":"Не найден","vac.source_status_disabled":"Деактивирован в Home Assistant","vac.source_status_unavailable":"Недоступен","vac.source_status_unverified":"Нельзя проверить с текущими правами","vac.source_status_unsupported":"Нет данных позиции","vac.source_status_none":"Нет источника","vac.source_banner_missing":"Сохранённый источник больше не существует. Он не заменён автоматически: выберите другой или восстановите его в Home Assistant.","vac.source_banner_disabled":"Сохранённый источник деактивирован в Home Assistant. Активируйте его там или выберите другой.","vac.source_banner_unverified":"Текущих прав Home Assistant недостаточно для проверки источника. Привязка сохранена и не будет заменена автоматически.","vac.choose_source":"Выбрать источник","vac.source_auto":"Автоматически","vac.source_auto_hint":"Использовать совместимую сущность этого устройства","vac.all_cameras":"Все камеры","vac.all_cameras_warn":"Камера может не отдавать данные робота. Выбирайте её только если это карта вашего пылесоса.","vac.all_cameras_empty":"Других сущностей камеры не найдено.","vac.platform_unknown":"интеграция неизвестна","vac.cap_position":"позиция","vac.cap_rooms_short":"комнаты","vac.cap_path":"путь","vac.cap_map":"ID карты","vac.cap_none":"данные робота не обнаружены","vac.xcme_hint":"Включите атрибуты Xiaomi Cloud Map Extractor:","vac.documentation":"Документация","vac.residual_title":"Проверьте автокалибровку","vac.residual_message":"Совпавшие комнаты расходятся максимум на {error}. Примените приблизительную калибровку, подгоните её вручную либо отмените без изменения сохранённых настроек.","vac.apply_proposal":"Применить","gs.backup_group":"Резервная копия и перенос","gs.backup_hint":"Скачайте переносимую JSON-копию или проверьте и импортируйте копию, созданную House Plan.","backup.export_open":"Экспорт","backup.import_open":"Импорт","backup.export_title":"Экспорт House Plan","backup.import_title":"Импорт House Plan","backup.export_hint":"Выберите, должна ли копия содержать всю конфигурацию House Plan или только текущее пространство.","backup.full":"Полная копия","backup.current_space":"Текущее пространство","backup.current_space_title":"Текущее пространство: {title}","backup.no_current_space":"Нет текущего пространства","backup.plan_only":"Только планировка","backup.plan_only_hint":"Сохранить комнаты, стены, проёмы, декор и позиции подписей комнат без устройств и привязок Home Assistant.","backup.plan_only_preview":"Файл содержит только планировку","backup.privacy_warning":"Архив сохраняет названия, идентификаторы Home Assistant и точные координаты. Внутренние планы и вложения указываются ссылками, но не вкладываются; текущие состояния и маршруты пылесосов не включаются.","backup.download":"Скачать JSON","backup.export_done":"Резервная копия скачана","backup.reading":"Проверяем резервную копию…","backup.revalidated":"После предпросмотра план изменился. Сводка обновлена — проверьте её и подтвердите ещё раз.","backup.error.unauthorized":"У вас нет прав на экспорт или импорт этого плана.","backup.error.not_ready":"House Plan ещё не готов. Повторите попытку через несколько секунд.","backup.error.too_large":"Размер резервной копии превышает 8 МиБ.","backup.error.invalid_json":"Выбранный файл не является корректным JSON.","backup.error.invalid_format":"Выбранный файл не является резервной копией House Plan.","backup.error.unsupported_export_version":"Эта версия формата резервной копии не поддерживается установленной версией.","backup.error.future_model":"Резервная копия создана в более новой версии модели данных House Plan.","backup.error.invalid_config":"Резервная копия содержит некорректную конфигурацию House Plan.","backup.error.wall_model_migration_blocked":"Не удалось безопасно обновить модель стен из резервной копии. Сначала оптимизируйте исходный план и повторите экспорт.","backup.error.invalid_layout":"Резервная копия содержит некорректные позиции объектов.","backup.error.invalid_content":"Резервная копия содержит некорректные или несогласованные ссылки на файлы.","backup.error.space_not_found":"Выбранное пространство больше не существует.","backup.error.capacity_exceeded":"Добавление этой копии превысит ограничения размера плана.","backup.error.preview_expired":"Время предпросмотра истекло. Выберите файл резервной копии заново.","backup.error.preview_owner_mismatch":"Этот предпросмотр принадлежит другому пользователю Home Assistant.","backup.error.conflict":"После предпросмотра план изменился. Проверьте обновлённую сводку.","backup.error.content_confirmation_required":"Подтвердите отсоединение недоступных локальных файлов.","backup.error.commit_failed":"Не удалось безопасно применить копию. Предыдущий план восстановлен или ожидает восстановления.","backup.error.missing_plan":"После предпросмотра исчез связанный файл плана. Проверьте копию ещё раз.","backup.error.missing_content":"После предпросмотра исчез связанный локальный файл. Проверьте копию ещё раз.","backup.error.marker_control_missing":"В резервной копии отсутствует связанный источник света на плане.","backup.error.marker_control_not_light":"Связанная цель больше не отмечена как источник света.","backup.error.marker_control_self":"Источник света не может управлять самим собой.","backup.error.marker_control_cycle":"Резервная копия содержит циклическую цепочку управления светом.","backup.error.duplicate_marker_control":"Резервная копия содержит повторяющуюся связь с источником света.","backup.error.no_backup":"Нет снимка импорта или оптимизации, который можно восстановить.","backup.same_source":"Создано на этом экземпляре Home Assistant","backup.foreign_source":"Создано на другом экземпляре Home Assistant","backup.created":"Создано: {value}","backup.versions":"Карточка {card}; интеграция {integration}; модель данных {model}","backup.count_spaces":"Пространств: {n}","backup.count_rooms":"Комнат: {n}","backup.count_walls":"Стен: {n}","backup.count_openings":"Проёмов: {n}","backup.count_decor":"Объектов декора: {n}","backup.count_markers":"Устройств: {n}","backup.count_layout":"Позиций: {n}","backup.bindings":"Привязки — устройства: {device}, сущности: {entity}, виртуальные: {virtual}; позиций без пространства: {legacy}","backup.binding_status":"Статус на целевом экземпляре — активных: {active}, деактивированных: {disabled}, отсутствующих: {missing}","backup.missing_areas":"На целевом экземпляре отсутствуют зоны: {areas}","backup.dropped_marker_links":"Связи с источниками света вне переносимого пространства пропущены: {n}.","backup.repaired_target_refs":"Существующих ссылок восстановлено этим импортом: {n}.","backup.preserved_unresolved_refs":"Неоднозначные ссылки сохранены без изменений: {n}.","backup.preserved_unresolved_hint":"House Plan ничего не угадывал и не удалял. После импорта запустите «Оптимизировать планы», чтобы проверить оставшиеся ссылки.","backup.import_details":"Подробности восстановления ссылок","backup.import_detail.incoming_remapped":"Ссылок обновлено внутри импортируемой копии: {n}","backup.import_detail.target_repaired":"Существующих ссылок восстановлено: {n}","backup.import_detail.preserved_unresolved":"Неразрешимых ссылок сохранено: {n}","backup.import_detail.collisions":"Конфликтов назначения безопасно сохранено: {n}","backup.import_detail.dropped_links":"Входящих связей пропущено по правилам переноса: {n}","backup.import_detail.bounded_lineages":"Чрезмерно вложенных идентификаторов ограничено: {n}","backup.replace_warning":"Текущая конфигурация и расположение будут заменены. Загруженные файлы не удаляются. Одну отмену можно выполнить до следующего изменения плана.","backup.foreign_bookkeeping":"Служебные списки известных и новых устройств другого экземпляра импортированы не будут.","backup.final_name":"Название нового пространства","backup.target_settings":"Глобальные настройки целевого экземпляра не изменятся.","backup.duplicates":"Уже существующих привязок: {n}","backup.skip":"Пропустить повторяющиеся устройства","backup.virtual_copy":"Добавить безопасные статичные виртуальные копии","backup.content":"Связанное содержимое","backup.content_available":"доступно локально","backup.content_external":"внешняя ссылка","backup.content_detach_required":"будет отвязано","backup.confirm_detach":"Я понимаю, что недоступные внутренние планы и вложения будут отвязаны от импортируемой конфигурации.","backup.replace":"Заменить","backup.add":"Добавить пространство","backup.space_done":"Пространство импортировано: комнат — {rooms}, устройств — {markers}, существующих ссылок восстановлено — {refs}","backup.full_done":"Копия восстановлена: пространств — {spaces}, комнат — {rooms}, устройств — {markers}","backup.undo_import":"Отменить последний полный импорт","backup.import_undone":"Полный импорт отменён"}}],Gd=Wd;function Vd(e){return"string"==typeof e?e.trim().replaceAll("_","-").toLowerCase():""}const Kd=new Map(Ud.map(e=>[Vd(e.code),e]));function Yd(e){return Kd.get(Vd(e))}function Zd(e,t){const i=[{value:"",label:e},...Ud.map(({code:e,nativeLabel:t})=>({value:e,label:t}))],n="string"==typeof t?t:"";return n&&!i.some(e=>e.value===n)&&i.push({value:n,label:Yd(n)?.nativeLabel??n}),i}function Xd(e,t){return function(e,t,i,n){const r=new Map(i.map(e=>[Vd(e),e])),o=r.get(Vd(e));if(o)return o;const s=Vd(t),a=r.get(s);return a||(r.get(s.split("-")[0]||"")??n)}(t,e?.locale?.language||e?.language,Ud.map(({code:e})=>e),"en")}function Jd(e,t,i){const n=Yd(e)?.dictionary;return Vi(n?.[t]??Gd[t]??t,i)}function Qd(e,t){const i=Yd(e)?.dictionary[t]??Gd[t];return"string"==typeof i&&i.trim().length>0}function eu(e){const t=e.length/2,i=e.face.cm>0?e.face.cm/e.cellCm*e.gridPitch/2:xa(4,e.cellCm),n="gate"===e.type?Math.sin(10*Math.PI/180)*t:0;return{half:t,jambHalf:i,gateDepth:n,outlineHalf:Math.max(xa(16,e.cellCm),i+xa(8,e.cellCm),n+xa(8,e.cellCm)),hitHalf:Math.max(xa(20,e.cellCm),i+xa(10,e.cellCm),n+xa(12,e.cellCm))}}function tu(e){if("passage"===e.type)return W``;const t=Math.max(0,Math.min(1,e.amount)),{half:i,jambHalf:n}=eu(e),r=ka(e.cellCm),o=1.75*r,s=e.flipH?-1:1,a=e.flipV?-1:1,l="gate"===e.type?1:a;e.type,e.flipV,e.angle,e.face;let c;if("window"===e.type){const o=Math.PI/2*i,s=e.face.cm>0?W``:W``;c=W` + + + + + + + + + + + + + + ${s} + `}else if("gate"===e.type){const n=10*e.face.side*t;c=W` + + + + + + + + + + + + `}else{const n=Math.PI/2*e.length;c=W` + + + + + + + + `}return W` + + + ${c} + `}const iu={climate:["current_temperature","temperature","current_humidity","humidity"],water_heater:["current_temperature","temperature"],cover:["current_position"],valve:["current_position"],fan:["percentage"],humidifier:["current_humidity","humidity"],light:["brightness"],media_player:["volume_level"],vacuum:["battery_level","fan_speed"],lawn_mower:["battery_level","fan_speed"]};function nu(e){return e?e.sourceLabel?`${e.sourceLabel}: ${e.fullText}`:e.fullText:""}const ru=new WeakMap;function ou(e){return e?"entity_state"===e.kind?`state:${e.entity_id}`:"entity_attribute"===e.kind?`attr:${e.entity_id}:${e.attribute}`:"derived_marker_state"===e.kind?`marker:${e.ref}`:"derived:lqi":""}function su(e){if("derived:lqi"===e)return{kind:"derived_lqi"};if(e.startsWith("state:"))return{kind:"entity_state",entity_id:e.slice(6)};if(e.startsWith("attr:")){const t=e.slice(5),i=t.lastIndexOf(":");if(i>0)return{kind:"entity_attribute",entity_id:t.slice(0,i),attribute:t.slice(i+1)}}return e.startsWith("marker:marker:")?{kind:"derived_marker_state",ref:e.slice(7)}:null}function au(e,t){const i=e?.entities?.[t],n=e?.states?.[t];return String(i?.name||i?.original_name||n?.attributes?.friendly_name||t)}function lu(e){return e.replaceAll("_"," ")}function cu(e,t=1){const i=Number(e);return Number.isFinite(i)?`${Math.round(i*t)} %`:null}function hu(e){if("derived_lqi"===e.kind)return"lqi";if("entity_attribute"===e.kind){if(e.attribute.includes("temperature"))return"temperature";if(e.attribute.includes("humidity"))return"humidity"}return"default"}function du(e,t,i,n=[]){let r=null,o="",s="available";if("entity_state"===i.kind){o=au(e,i.entity_id);const t=e?.states?.[i.entity_id];if(t)if(pu(e,i.entity_id))if(["unknown","unavailable"].includes(String(t.state).toLowerCase()))s="unavailable";else{const n=ji(e,i.entity_id);r=n?qi(n,String(t.attributes?.unit_of_measurement||"")):null,r||(s="unavailable")}else s="unavailable";else s="missing"}else if("entity_attribute"===i.kind){o=`${lu(i.attribute)} · ${au(e,i.entity_id)}`;const t=e?.states?.[i.entity_id];t?pu(e,i.entity_id)?(r=function(e,t,i){const n=e?.states?.[t];if(!n||!(i in(n.attributes||{})))return null;const r=ji(e,t,i);if(r?.formatted)return r.text;const o=n.attributes?.[i];if("brightness"===i)return cu(o,100/255);if("volume_level"===i)return cu(o,100);if(["current_position","percentage","current_humidity","humidity","battery_level"].includes(i))return cu(o);if("current_temperature"===i||"temperature"===i){const t=Number(o);if(!Number.isFinite(t))return null;const i=String(e?.config?.unit_system?.temperature||"°C");return`${Math.round(10*t)/10} ${i}`}return r?.text||(["string","number","boolean"].includes(typeof o)?String(o):null)}(e,i.entity_id,i.attribute),null==r&&(s="unavailable")):s="unavailable":s="missing"}else if("derived_lqi"===i.kind){o="LQI";const i=Ch(e,t.entities);null==i?s="unavailable":r=String(i)}else{o="Light state";const t=n.find(e=>e.ref===i.ref);if(t){const i=t.on?"on":"off",n=e?.localize?.(`state.default.${i}`);r="string"==typeof n&&n?n:i,o=t.name||o}else s="missing"}const a=e?.localize?.("state.default.unavailable")||"Unavailable";return{source:i,sourceLabel:o,text:r??"—",fullText:r??a,availability:s,isLqi:"derived_lqi"===i.kind,tone:hu(i)}}function uu(e,t){if(!0===t.marker?.use_climate_temp){const i=t.entities.find(t=>t.startsWith("climate.")&&Number.isFinite(Number(e?.states?.[t]?.attributes?.current_temperature)));return i?{kind:"entity_attribute",entity_id:i,attribute:"current_temperature"}:null}if("mdi:thermometer"===t.icon||"mdi:air-filter"===t.icon){const i=t.entities.find(t=>sh(e,t)&&Number.isFinite(Number(e?.states?.[t]?.state)));if(i)return{kind:"entity_state",entity_id:i}}return t.primary&&Oh(e,t.primary)&&Number.isFinite(Number(e?.states?.[t.primary]?.state))?{kind:"entity_state",entity_id:t.primary}:null}function pu(e,t){const i=e?.entities?.[t];if(i&&!Zc(i))return!1;const n=i?.device_id?e?.devices?.[i.device_id]:null;return!n||Zc(n)}function _u(e,t){return pu(e,t)&&!!e?.states?.[t]}function mu(e,t,i=[t]){const n=function(e,t){const i=e=>[e.id,e.primary,e.hidden?1:0,e.userHidden?1:0,e.entities.join(","),(e.controls||[]).join(","),e.marker?.binding||"",!0===e.marker?.is_light?1:0,e.marker?.light_entity||"",(e.marker?.controls||[]).join(",")].join("|");return`${i(e)}\n${t.map(i).join("\n")}`}(t,i),r=ru.get(t);if(r&&r.states===(e?.states||null)&&r.entities===(e?.entities||null)&&r.devices===(e?.devices||null)&&r.signature===n)return r.result;const o=new Set(t.entities);for(const e of t.controls||[])e.startsWith("marker:")||o.add(e);const s=[],a=Mh(e,i),l=a.filter(e=>e.key.startsWith("marker:")).map(e=>({ref:e.key,on:e.on,name:e.device?.name||e.key})),c=i=>{const n=ou(i);if(s.some(e=>e.key===n))return;const r=du(e,t,i,l),o="entity_attribute"===i.kind?`${i.entity_id} · ${i.attribute}`:"entity_state"===i.kind?i.entity_id:"derived_marker_state"===i.kind?i.ref:"LQI";s.push({key:n,source:i,label:r.sourceLabel,technical:o,value:r.text,available:"available"===r.availability})};for(const t of[...o].sort()){if(!_u(e,t))continue;const i=t.split(".")[0],n=e?.entities?.[t];if("button"===i||"event"===i||"config"===n?.entity_category)continue;c({kind:"entity_state",entity_id:t});const r=e?.states?.[t];for(const e of iu[i]||[])e in(r?.attributes||{})&&c({kind:"entity_attribute",entity_id:t,attribute:e})}const h=new Set((t.marker?.controls||t.controls||[]).filter(e=>e.startsWith("marker:")));!0===t.marker?.is_light&&t.marker.id&&h.add(`marker:${t.marker.id}`);const d=new Set(a.map(e=>e.key));for(const e of[...h].sort())d.has(e)&&c({kind:"derived_marker_state",ref:e});return t.virtual||null==Ch(e,t.entities)||c({kind:"derived_lqi"}),ru.set(t,{states:e?.states||null,entities:e?.entities||null,devices:e?.devices||null,signature:n,result:s}),s}function gu(e,t,i){const n=uu(e,t);if(n&&i.some(e=>e.key===ou(n)))return n;const r=t.primary&&i.find(e=>e.key===`state:${t.primary}`);if(r)return r.source;const o=[e=>e.technical.includes("temperature"),e=>e.technical.includes("humidity"),e=>e.technical.includes("battery")];for(const e of o){const t=i.find(e);if(t)return t.source}return i.find(e=>"derived_lqi"!==e.source.kind)?.source||i[0]?.source||null}function fu(e){return e.touched?{value_badge:{enabled:e.enabled,source:e.source,position:e.position}}:e.originalHas?{value_badge:e.original}:{}}function vu(e,t,i){return{kind:"none",reason:"none",generation:e,expiresAt:null,color:t,diameterScale:i,animated:!1,reducedMotionIndicator:"none"}}function yu(e,t,i,n,r=null){return{kind:t,reason:"short"===t?"event":"running",generation:Math.max(1,Math.trunc(i)),expiresAt:"short"===t?r:null,color:e.color,diameterScale:n?1:e.diameterScale,animated:!n,reducedMotionIndicator:n?"dot":"none"}}const bu={availability:"available",status:"neutral",activity:"none"};function wu(e){return e<=40?"low":e<180?"mid":"high"}function ku(e){return Ut(e)}function xu(e){return"static_icon"===e.display?"neutral":"alarm"===e.visual.status?"alarm":"unavailable"===e.visual.availability?"unavailable":e.lockState?e.lockState:"working"===e.visual.status?"working":"open"===e.visual.status?"open":"neutral"}const $u={sourceKind:"none",decisionIds:["source.skipped_static_fast_path"],visualSources:[],criticalSources:[],samples:[]};function Su(e,t){const i=e?.entities?.[t],n=e?.states?.[t];return String(i?.name||i?.original_name||n?.attributes?.friendly_name||t)}function Mu(e,t){const i=e?.states?.[t];if(!i)return"";if("function"==typeof e?.formatEntityState)try{const t=e.formatEntityState(i);if("string"==typeof t&&t)return t}catch{}return String(i.state??"")}function Tu(e,t){if(t.virtual||"virtual"===t.bindingKind||"virtual"===t.marker?.binding)return"available";return(t.entities||[]).some(t=>{const i=String(e?.states?.[t]?.state??"").trim().toLowerCase();return""!==i&&"unknown"!==i&&"unavailable"!==i})?"available":"unavailable"}function Ru(e,t,i,n){return{eid:t,role:i,name:Su(e,t),state:String(e?.states?.[t]?.state??""),stateText:Mu(e,t),integrationDomain:e?.entities?.[t]?.platform||null,sample:n||xc(e,t)}}function Cu(e,t,i=[t],n,r=e){const o=t.hidden&&t.userHidden?{...t,hidden:!1}:t;let s="none",a=[];const l=[],c=[...o.entities,...o.allEntities||[]].some(e=>e.startsWith("cover."))?{...o,tapAction:"cover"}:o,h=(d=vd({hass:e,devices:i,device:c,lightSources:n,registryHass:r}),"cover"!==d?.semantics?null:d.targets[0]?.entityId||d.skippedTargets[0]?.entityId||null);var d;const u=Mh(e,[o]),p=uh(o.marker?.binding,o.marker?.controls??o.controls,o.entities),_=new Set(p.filter(e=>e.startsWith("marker:"))),m=!0===o.marker?.is_light||_.size>0?n||Mh(e,i):u,g=m.filter(e=>e.device.id===o.id&&"controls"!==e.via),f=u.filter(e=>"controls"!==e.via),v=g.length?g:f,y=zc(o.marker),b=y?[...v]:[...u.filter(e=>"controls"===e.via),...v];if(_.size)for(const e of m){if(!_.has(e.key))continue;e.stateEids.length>0&&b.some(t=>t.stateEids.some(t=>e.stateEids.includes(t)))||b.some(t=>t.key===e.key)||b.push({...e,via:"controls",castsGlow:!1})}const w=ch(r,o.entities),k=!!h&&("cover"===o.tapAction||o.primary?.startsWith("cover.")||w.some(e=>e.startsWith("cover.")));if(h&&!k&&l.push("source.cover_capability_bypassed"),k)s="cover",l.push("source.cover"),a=[Ru(e,h,"cover")];else if(b.length)s=!y&&b.some(e=>"controls"===e.via)?"controls":"light",y?l.push("source.manual_virtual_light"):l.push("controls"===s?"source.controls":"source.owned_light"),"controls"!==s||!o.virtual&&"virtual"!==o.bindingKind||l.push("source.virtual_controller"),a=b.map(i=>{const n="controls"===i.via?"control":"forced"===i.via?"forced_light":"light";if(!i.passive)return Ru(e,i.eid,n);const r=i.on?"on":"off";return{eid:i.key,role:n,name:t.name,state:r,stateText:r,integrationDomain:null,sample:{eid:i.key,state:r,availability:"available",status:i.on?"working":"neutral",activity:"none",edge:"none"}}});else{const t=ch(r,o.entities),i=t.length?t:o.entities.filter(t=>!!e?.states?.[t]);if(i.length){s="device_role",l.push("source.device_role");const t=$c(r===e?e:{...r,states:e?.states||{}},i,o.entities),n=i.some(e=>bc(r,e));a=t.map(t=>{const i=n?wc(r,t.eid)?"power_gate":bc(r,t.eid)?"lifecycle":"device_role":"device_role";return Ru(e,t.eid,i,t)})}else o.primary?(s="primary",l.push("source.primary_fallback"),a=[Ru(e,o.primary,"primary")]):l.push("source.none")}p.length>0&&"controls"!==s&&"light"!==s&&"cover"!==s&&l.push("source.filtered_saved_controls");const x=[];for(const t of o.entities||[]){const i=xc(e,t);"alarm"!==i.status||a.some(e=>e.eid===t)||x.push(Ru(e,t,"critical",i))}return x.length&&l.push("source.critical_sibling"),{sourceKind:s,decisionIds:l,visualSources:a,criticalSources:x,samples:[...a,...x].map(e=>e.sample)}}function Du(e,t,i,n){if(t.virtual)return{source:null,text:null,fallback:"value_virtual"};if(n){const i=function(e,t){if(!0!==t.marker?.use_climate_temp)return null;for(const i of t.entities){if(!i.startsWith("climate."))continue;const t=Number(e?.states?.[i]?.attributes?.current_temperature);if(Number.isFinite(t))return{eid:i,text:Math.round(10*t)/10+"°"}}return null}(e,t);if(i)return{source:{kind:"temperature",eid:i.eid,attribute:"current_temperature",text:i.text},text:i.text,fallback:null};const n=function(e,t){if("mdi:thermometer"!==t.icon&&"mdi:air-filter"!==t.icon)return null;for(const i of t.entities){if(!sh(e,i))continue;const t=Number(e?.states?.[i]?.state);if(Number.isFinite(t))return{eid:i,text:Math.round(10*t)/10+"°"}}return null}(e,t);if(n)return{source:{kind:"temperature",eid:n.eid,text:n.text},text:n.text,fallback:null};const r=function(e,t){if(!t.primary||!Oh(e,t.primary))return null;const i=Number(e?.states?.[t.primary]?.state);return Number.isFinite(i)?{eid:t.primary,text:`${Math.round(i)}%`}:null}(e,t);if(r)return{source:{kind:"humidity",eid:r.eid,text:r.text},text:r.text,fallback:null}}const r=i.visualSources.filter(e=>!e.eid.startsWith("marker:"));let o=r.map(e=>e.eid);const s=r.find(e=>"power_gate"===e.role);if(s&&(o=[s.eid]),!o.length&&i.visualSources.some(e=>e.eid.startsWith("marker:"))&&(o=ch(e,t.entities),!o.length&&t.primary&&e?.states?.[t.primary]&&(o=[t.primary])),o=[...new Set(o)],1!==o.length)return{source:null,text:null,fallback:o.length?"value_ambiguous_sources":"value_no_state"};const a=o[0],l=function(e,t){const i=e?.states?.[t];if(!i||null==i.state||""===String(i.state).trim())return{text:"",fallback:"value_no_state"};const n=i.state;if(!["string","number","boolean"].includes(typeof n))return{text:"",fallback:"value_non_scalar"};const r=String(n).trim().toLowerCase();if("unknown"===r||"unavailable"===r)return{text:"",fallback:"value_no_state"};const o=ji(e,t);return o?{text:qi(o,String(i.attributes?.unit_of_measurement||"")),fallback:null}:{text:"",fallback:"value_no_state"}}(e,a);return l.fallback?{source:null,text:null,fallback:l.fallback}:{source:{kind:"entity",eid:a,text:l.text},text:l.text,fallback:null}}function Au(e,t,i){const n=e.marker?.binding||(e.bindingKind&&e.bindingRef?`${e.bindingKind}:${e.bindingRef}`:e.virtual?"virtual":""),r=[...t.visualSources.map(e=>`${e.role}:${e.eid}`),...t.criticalSources.map(e=>`critical:${e.eid}`)].sort(),o=i?`${i.kind}:${i.eid}:${i.attribute||""}`:"none";return[n,t.sourceKind,...r,`value:${o}`].join("\n")}function Ou(e,t,i){return Au(t,i||Cu(e,t),null).replace(/\nvalue:none$/,"")}function zu(e){if(e.effectiveHidden)return[];if("static_icon"===e.display)return["static-icon"];const t=[],{visual:i}=e;return"alarm"===e.pulse.kind?t.push("alarm"):"unavailable"===i.availability?t.push("unavail"):"working"===i.status?t.push("on"):"open"===i.status&&t.push("open"),e.lockState&&t.push(`lock-${e.lockState}`),"none"!==e.pulse.reason&&"alarm"!==e.pulse.reason&&t.push("activity-"+e.pulse.reason),e.pulse.generation%2==0&&(t.push("pulse-gen2"),"short"===e.pulse.kind&&t.push("activity-gen2")),t}function Pu(e,t,i){const n=Ai(t.marker?.display),r="static_icon"===n,o=r&&!1===i.sourceDetails?$u:Cu(e,t,i.lightDevices||[t],i.lightSources,i.registryHass||e),s=t.bindingStatus,a="ha_disabled"===s?.kind,l="orphaned"===s?.kind,c=a?"ha_disabled":l?"orphaned":"active",h=!0===t.userHidden||!0===t.marker?.hidden,d=Sc(o.samples),u=o.visualSources.find(e=>e.eid.startsWith("lock.")),p=u?["unlocked","open"].includes(u.state.toLowerCase())?"unlocked":"locked"===u.state.toLowerCase()?"locked":null:null,_=!zc(t.marker)&&uh(t.marker?.binding,t.marker?.controls??t.controls,t.entities).length>0,m="controls"===o.sourceKind||_&&"light"!==o.sourceKind&&"cover"!==o.sourceKind,g=r&&!1===i.sourceDetails?{source:null,text:null,fallback:null}:Du(e,t,o,i.showTemperature),f=Au(t,o,g.source),v=Ou(e,t,o),y=i.activityRuntime,b=i.now??Date.now(),w=y?.expiresAt||(y?.flashTs?y.flashTs+3300:0),k=y?.sources===v&&y.flashTs&&y.flashKind&&w>b?y.flashKind:null,x=function(e){const t=[],i="ha_disabled"===e.bindingLifecycle||"orphaned"===e.bindingLifecycle;let n=!1;"ha_disabled"===e.bindingLifecycle?(n=!0,t.push("lifecycle.ha_disabled_hidden")):e.userHidden&&!e.designPreview?(n=!0,t.push("lifecycle.user_hidden")):e.userHidden?t.push("lifecycle.user_hidden_preview"):"orphaned"===e.bindingLifecycle?t.push("lifecycle.orphaned_diagnostic"):t.push("lifecycle.active");const r="static_icon"===e.display;let o=e.controllerFace?{...e.sourceVisual,availability:e.controllerAvailability}:e.sourceVisual;t.push(e.controllerFace?"available"===e.controllerAvailability?"availability.controller_available":"availability.controller_unavailable":"availability.source"),n?(o=bu,t.push("face.hidden")):r?(o=bu,t.push("face.static")):"alarm"===e.sourceVisual.status||e.liveStates?t.push("face.dynamic"):(o=bu,t.push("face.live_states_disabled")),!r&&!n&&e.liveStates&&"alarm"!==e.sourceVisual.status&&e.shortActivity&&(o={...o,activity:e.shortActivity},t.push(`activity.short_${e.shortActivity}`)),"alarm"===o.status?t.push("status.alarm"):"unavailable"===o.availability?t.push("status.unavailable"):"working"===o.status?t.push("status.working"):"open"===o.status?t.push("status.open"):t.push("status.neutral");const s="value"===e.display&&!n&&e.valueAvailable?"value":"icon";t.push("value"===s?"content.value":"value"===e.display?"content.value_fallback_icon":"content.icon"),"value"===e.display&&!e.valueAvailable&&e.valueFallback&&t.push(`content.${e.valueFallback}`);const a=e.liveStates&&!r&&!n,l=!r&&!n,c=!n&&!i&&!r&&"available"===o.availability&&("alarm"===o.status||e.liveStates&&"icon_ripple"===e.display),h=l&&e.vacuumLiveRequested;return t.push(a?"diagnostics.dynamic_icon":"diagnostics.base_icon"),t.push(l?"diagnostics.metrics_enabled":"diagnostics.metrics_suppressed"),t.push(c?"activity.pulse_eligible":"activity.pulse_suppressed"),t.push(h?"diagnostics.vacuum_live":"diagnostics.vacuum_static"),{effectiveHidden:n,bindingUnavailable:i,visual:o,face:s,dynamicIcon:a,metrics:l,liveColor:a,pulseEligible:c,vacuumLive:h,decisionIds:t}}({bindingLifecycle:c,userHidden:h,designPreview:!0===i.designPreview,display:n,liveStates:i.liveStates,sourceVisual:d,controllerFace:m,controllerAvailability:m?Tu(e,t):"available",shortActivity:k,valueAvailable:null!=g.text,valueFallback:g.fallback,vacuumLiveRequested:!0===t.marker?.vacuum?.live}),{effectiveHidden:$,visual:S}=x,M="icon_ripple"===n&&!$&&i.liveStates&&"alarm"!==S.status?S.activity:"none",T=x.metrics&&i.showTemperature?!0===t.marker?.use_climate_temp?Ah(e,t.entities):"mdi:thermometer"===t.icon||"mdi:air-filter"===t.icon?Dh(e,t.entities):null:null,R=x.metrics&&i.showTemperature&&t.primary&&Oh(e,t.primary)?zh(e,t.entities):null,C=x.metrics&&i.showSignal&&!t.virtual?Ch(e,t.entities):null,D=i.lightSources||("derived_marker_state"===t.marker?.value_badge?.source?.kind?Mh(e,i.lightDevices||[t]):[]),A=function(e,t,i){const n=null!=t.marker?.value_badge;if(i.effectiveHidden||"static_icon"===i.display)return null;const r=t.marker?.value_badge;if(n)return r?.enabled?r.source?{configured:!0,enabled:!0,position:r.position||"right",...du(e,t,r.source,i.markerStates)}:{configured:!0,enabled:!0,source:null,sourceLabel:"",text:"—",fullText:e?.localize?.("state.default.unavailable")||"Unavailable",position:r.position||"right",availability:"missing",isLqi:!1,tone:"default"}:null;if(!i.showTemperature||"value"===i.display)return null;const o=uu(e,t);if(!o)return null;let s=null,a="default";if("entity_attribute"===o.kind&&"current_temperature"===o.attribute){const i=Ah(e,t.entities);null!=i&&(s=`${i}°`),a="temperature"}else if("mdi:thermometer"===t.icon||"mdi:air-filter"===t.icon){const i=Dh(e,t.entities);null!=i&&(s=`${i}°`),a="temperature"}else{const i=zh(e,t.entities);null!=i&&(s=`${i}%`),a="humidity"}return null==s?null:{configured:!1,enabled:!0,source:o,position:"right",sourceLabel:"entity_attribute"===o.kind?`${lu(o.attribute)} · ${au(e,o.entity_id)}`:"entity_state"===o.kind?au(e,o.entity_id):"",text:s,fullText:s,availability:"available",isLqi:!1,tone:a}}(e,t,{showTemperature:i.showTemperature,showSignal:i.showSignal,display:n,effectiveHidden:$,markerStates:D.filter(e=>e.key.startsWith("marker:")).map(e=>({ref:e.key,on:e.on,name:e.device.name}))}),O=o.visualSources.find(e=>!e.eid.startsWith("marker:")),z="cover"===o.sourceKind?O?.eid:t.primary||O?.eid,P=z?e?.states?.[z]:null,F=x.dynamicIcon?dn(t.icon,z?.split(".")[0],P?.attributes?.device_class,P?.state,!!t.marker?.icon):t.icon,I=x.liveColor&&o.visualSources.filter(e=>!e.eid.startsWith("marker:")).map(t=>function(e){return e&&"on"===e.state?Wt(e.attributes?.rgb_color):null}(e?.states?.[t.eid])).find(e=>!!e)||null,E=Number(t.marker?.size)>0?Number(t.marker.size):1,N=Number(t.marker?.angle)||0,H=Number(t.marker?.ripple_size)>0?Number(t.marker.ripple_size):1.5,L=jt(t.marker?.ripple_color,null),j=r?null:L||I||null,q=function(e){const{display:t,visual:i,semanticActivity:n,liveStates:r,effectiveHidden:o,bindingUnavailable:s=!1,reducedMotion:a=!1}=e,l=Math.max(1,Math.trunc(e.shortGeneration||1)),c=Number.isFinite(e.diameterScale)?Math.max(1,Number(e.diameterScale)):1.5,h=e.color||function(e,t){return"presence"===t?"#1DC21D":"running"===t||"working"===e.status||"open"===e.status?"#F0A00C":"#0C82F0"}(i,n);if(o||s||"unavailable"===i.availability||"static_icon"===t)return vu(l,h,c);if("alarm"===i.status)return{kind:"alarm",reason:"alarm",generation:l,expiresAt:null,color:"#F0410C",diameterScale:1.5,animated:!a,reducedMotionIndicator:"none"};if(!r||"icon_ripple"!==t)return vu(l,h,c);const d=e.now??Date.now(),u=e.shortExpiresAt||null;return e.shortReason&&null!=u&&u>d?{kind:"short",reason:e.shortReason,generation:l,expiresAt:u,color:h,diameterScale:a?1:c,animated:!a,reducedMotionIndicator:a?"dot":"none"}:"presence"===n||"transition"===n||"running"===n?{kind:"continuous",reason:n,generation:l,expiresAt:null,color:h,diameterScale:a?1:c,animated:!a,reducedMotionIndicator:a?"dot":"none"}:vu(l,h,c)}({display:n,visual:S,semanticActivity:d.activity,shortReason:y?.sources===v?y?.flashKind:null,shortGeneration:y?.gen,shortExpiresAt:y?.sources===v?w:null,now:b,liveStates:i.liveStates,effectiveHidden:$,bindingUnavailable:x.bindingUnavailable,reducedMotion:i.reducedMotion,color:j,diameterScale:H}),B="value"===x.face?g.text:null,W="ha_disabled"===s?.kind?s.reason:null,U="ha_disabled"===(G={lifecycle:c,display:n,liveStates:i.liveStates,sourceKind:o.sourceKind,primaryDomain:(t.primary||"").split(".")[0],visual:S,activity:M}).lifecycle?"ha_disabled":"orphaned"===G.lifecycle?"orphaned":"static_icon"===G.display?"static_icon":"alarm"===G.visual.status?"alarm":G.liveStates?"unavailable"===G.visual.availability?"unavailable":"cover"===G.sourceKind?"cover_icon_state":"presence"===G.activity?"presence":"event"===G.activity?"event":"transition"===G.activity?"transition":"working"===G.visual.status?"icon_ripple"===G.display&&"none"!==G.activity?"working_activity":"working":"open"===G.visual.status?"open":"media_player"===G.primaryDomain?"media_neutral":"neutral":"live_states_disabled";var G;const V=[];i.designPreview&&h&&V.push("hidden_design_preview"),r||!0!==t.marker?.vacuum?.live||V.push("vacuum_live_plan_only");const K=o.visualSources.some(t=>"power_gate"===t.role||wc(e,t.eid)),Y=t.entities.filter(t=>t.startsWith("switch.")&&!e?.entities?.[t]?.entity_category).length;K&&Y>1&&V.push("composite_power_source"),"icon_ripple"!==n&&"static_icon"!==n&&"alarm"!==d.status&&"none"!==d.activity&&V.push("activity_display_disabled");const Z={binding:t.marker?.binding||(t.bindingKind&&t.bindingRef?`${t.bindingKind}:${t.bindingRef}`:t.virtual?"virtual":""),sourceKind:o.sourceKind,visualSources:o.visualSources,criticalSources:o.criticalSources,valueSource:g.source,sourceSignature:f,decisionIds:[...o.decisionIds,...x.decisionIds,...A?["diagnostics.value_badge"]:[],...null==C?[]:[`diagnostics.lqi_${wu(C)}`],`pulse.${q.kind}_${q.reason}`],visual:S,lockState:p,display:n,icon:F,valueText:B,valueFullText:B,fallbackReason:"value"!==n||B?null:g.fallback,activity:M,activityGeneration:y?.gen||1,pulse:q,classes:[],tempText:null==T?null:String(T),humText:null==R?null:String(R),valueBadge:A,lqiText:null==C||A?.isLqi?null:String(C),lqiColor:null==C?null:ku(C),lqiBand:null==C?null:wu(C),lightColor:I,scale:E,angle:N,rippleScale:H,rippleColor:j,userHidden:h,effectiveHidden:$,haDisabled:a,disabledReason:W,orphaned:l,vacuumLive:x.vacuumLive,explanation:{reason:U,notices:V}};return{...Z,classes:zu(Z)}}function Fu(e,t=new WeakMap){if(null===e||"object"!=typeof e)return e;const i=e,n=t.get(i);if(n)return n;if(Array.isArray(e)){const n=[];t.set(i,n);for(const i of e)n.push(Fu(i,t));return Object.freeze(n)}const r={};t.set(i,r);for(const[i,n]of Object.entries(e))"function"!=typeof n&&(r[i]=Fu(n,t));return Object.freeze(r)}function Iu(e){const t=new Map(e);let i;return i=Object.freeze({get size(){return t.size},get:e=>t.get(e),has:e=>t.has(e),entries:()=>t.entries(),keys:()=>t.keys(),values:()=>t.values(),forEach:(e,n)=>t.forEach((t,r)=>e.call(n,t,r,i)),[Symbol.iterator]:()=>t[Symbol.iterator]()}),i}function Eu(e){const t=new Set(e.entityIds||[]),i=new Set(e.deviceIds||[]),n=new Set(e.areaIds||[]);for(const n of e.devices){for(const e of n.entities||[])t.add(e);n.primary&&t.add(n.primary);for(const e of n.controls||[])t.add(e);n.marker?.vacuum?.source&&t.add(n.marker.vacuum.source),"device"===n.bindingKind&&n.bindingRef&&i.add(n.bindingRef),"entity"===n.bindingKind&&n.bindingRef&&t.add(n.bindingRef)}const r={},o={};for(const[o,s]of Object.entries(e.hass?.entities||{})){const a=s?.device_id?e.hass?.devices?.[s.device_id]:null;(t.has(o)||s?.device_id&&i.has(s.device_id)||n.has(s?.area_id)||n.has(a?.area_id))&&(t.add(o),r[o]=s,s?.device_id&&i.add(s.device_id))}for(const t of i){const i=e.hass?.devices?.[t];i&&(o[t]=i)}const s={};for(const i of t){const t=e.hass?.states?.[i];t&&(s[i]=t)}const a=Object.freeze({states:Fu(s),entities:Fu(r),devices:Fu(o),config:Fu(e.hass?.config),locale:Fu(e.hass?.locale),themes:Fu(e.hass?.themes)});return Object.freeze({sourceSequence:e.sourceSequence,capturedAt:e.capturedAt??Date.now(),hass:a,devices:Fu([...e.devices]),positions:Iu([...e.positions||[]].map(([e,t])=>[e,Object.freeze({x:t.x,y:t.y})])),presentations:Iu([...e.presentations].map(([e,t])=>[e,Fu(t)])),facts:Iu([...e.facts||[]].map(([e,t])=>[e,Fu(t)]))})}const Nu=(e,t)=>`${e}:${t?1:0}`;function Hu(e){return"boolean"==typeof e?.themes?.darkMode?e.themes.darkMode?"theme-dark":"theme-light":""}function Lu(e){const t=[];if(1!==e.scale&&t.push(`--dev-scale:${e.scale}`),"none"!==e.pulse.kind){t.push(`--ripple-scale:${e.pulse.diameterScale}`);const r=(i=e.pulse.color,n=null,"string"==typeof i&&(Lt.test(i)||Bt.test(i))?i:n);r&&t.push(`--ripple-color:${r}`)}var i,n;return t}function ju(e){const t=[...String(e)].reduce((e,t)=>/\s/.test(t)?e+.35:e+(t.codePointAt(0)>255?1:.62),0);return t<=8?.45:Math.max(.25,Math.round(3.6/t*1e3)/1e3)}function qu(e,t){const i=e.pulse,n=i.generation%2==0,r=function(e){const t=e.valueBadge;if(!t||!1!==t.configured)return[];const i=[];return null!=e.tempText&&"temperature"!==t.tone&&i.push({kind:"temperature",text:e.tempText,suffix:"°"}),null!=e.humText&&"humidity"!==t.tone&&i.push({kind:"humidity",text:e.humText,suffix:"%"}),i}(e),o=e.valueBadge,s=o?.position||"right",a=!!o||r.length>0,l=["device-shell",null!=e.valueText?"text-shell":"",a?`with-values pos-${s}`:"",r.length?"with-legacy":""].filter(Boolean).join(" ");return B` + ${"none"!==i.kind&&"dot"!==i.reducedMotionIndicator?B``:G} + ${"dot"===i.reducedMotionIndicator?B``:G} + ${t.newDevice?B``:G} + ${e.haDisabled?B``:G} + + ${null!=e.lqiText?B`${e.lqiText}`:G} + `}function Bu(e){return!Number.isFinite(e)||e<=0?2.25:e/2.5*2.25}function Wu({openings:e,geometries:t,fillsByRoomId:i,idPrefix:n="data",groupClass:r="opening-tunnels",dataLayer:o="data"}){const s=e.map((e,r)=>{const o=t[r];if(!o)return G;const s=o.faces.find(e=>-1===e.side),a=o.faces.find(e=>1===e.side);if(!s||!a)return G;const l=i.get(s.roomId)||null,c=i.get(a.roomId)||null;if(!l&&!c)return G;const h=`${s.d} ${a.d}`,d=!!l&&!!c&&l.color===c.color&&l.opacity===c.opacity,u=`translate(${e.rx} ${e.ry}) rotate(${e.angle})`;if(d)return W``;const p=o.maxY-o.minY;if(!(p>0))return G;const _=`${(100*Math.max(0,Math.min(1,-o.minY/p))).toFixed(6)}%`,m=l||{color:"#000000",opacity:0},g=c||{color:"#000000",opacity:0},f=`hp-opening-tunnel-${n.replace(/[^a-zA-Z0-9_-]/g,"-")}-${r}`;return W``});return W``}function Uu(e,t,i){const n=i>0?i:1;return{a:[e[0]/n,e[1]/n],b:[t[0]/n,t[1]/n]}}function Gu(e,t){const i=t>0?t:1;return[e.a[0]*i,e.a[1]*i,e.b[0]*i,e.b[1]*i]}function Vu(e){return Array.isArray(e)&&e.length>=2&&Number.isFinite(Number(e[0]))&&Number.isFinite(Number(e[1]))}function Ku(e){if(!Array.isArray(e))return[];const t=[];for(const i of e){if(!i||"object"!=typeof i)continue;const e=i;if(!Vu(e.a)||!Vu(e.b))continue;const n=[Number(e.a[0]),Number(e.a[1])],r=[Number(e.b[0]),Number(e.b[1])];Math.hypot(r[0]-n[0],r[1]-n[1])<.001||t.push({a:n,b:r})}return t}function Yu(e,t,i,n,r=!0){const o=(e||[]).filter(e=>e?.id),s=Ku(t);if(s.length)return function(e,t,i,n){const r=Ku(e);if(!r.length)return[];const o=function(e,t){const i=[],n=(e||[]).filter(e=>e?.id);for(let e=0;e4*n||o>4*n)continue;const s=(e[0]-r)*u+(e[1]-l)*p,c=(e[2]-r)*u+(e[3]-l)*p,h=Math.max(0,Math.min(s,c)),m=Math.min(d,Math.max(s,c));if(m-he.lo-t.lo||e.hi-t.hi);const i=[];for(const e of t){const t=i[i.length-1];t&&e.lo<=t.hi+a?t.hi=Math.max(t.hi,e.hi):i.push({...e})}for(const t of i){const i=[r+u*t.lo,l+p*t.lo],n=[r+u*t.hi,l+p*t.hi];Math.hypot(n[0]-i[0],n[1]-i[1])i.pair===e&&Math.abs(i.ux*s-i.uy*o)<=1e-6&&Math.abs((t[0]-i.origin[0])*i.uy-(t[1]-i.origin[1])*i.ux)<=c&&Math.abs((t[2]-i.origin[0])*i.uy-(t[3]-i.origin[1])*i.ux)<=c);h||(h={pair:e,origin:[t[0],t[1]],ux:o,uy:s,ranges:[]},l.push(h));const d=(t[0]-h.origin[0])*h.ux+(t[1]-h.origin[1])*h.uy,u=(t[2]-h.origin[0])*h.ux+(t[3]-h.origin[1])*h.uy;h.ranges.push({lo:Math.min(d,u),hi:Math.max(d,u)})}const h=[];for(const e of l){e.ranges.sort((e,t)=>e.lo-t.lo||e.hi-t.hi);const t=[];for(const i of e.ranges){const e=t[t.length-1];e&&i.lo<=e.hi+a?e.hi=Math.max(e.hi,i.hi):t.push({...i})}for(const n of t){const t=[e.origin[0]+e.ux*n.lo,e.origin[1]+e.uy*n.lo],r=[e.origin[0]+e.ux*n.hi,e.origin[1]+e.uy*n.hi];Math.hypot(r[0]-t[0],r[1]-t[1])>=a&&h.push(Uu(t,r,i))}}return h}(s,e,i,n).map(e=>Gu(e,i));if(!r)return[];const a=[],l=(e,t)=>(e.open_to||[]).includes(t.id)||(t.open_to||[]).includes(e.id);for(let e=0;er)&&zo([o[0],o[1]],[o[2],o[3]],i))return!0;return!1}const Xu=e=>Array.isArray(e)&&e.length>=2&&Number.isFinite(Number(e[0]))&&Number.isFinite(Number(e[1])),Ju=(e,t,i)=>[Number(e[0])*i,Number(e[1])*i,Number(t[0])*i,Number(t[1])*i],Qu=e=>{const t=`${Number(e[0]).toFixed(9)},${Number(e[1]).toFixed(9)}`,i=`${Number(e[2]).toFixed(9)},${Number(e[3]).toFixed(9)}`;return t<=i?`${t}|${i}`:`${i}|${t}`},ep=e=>{const t=new Map;for(const i of e.flat())!Array.isArray(i)||i.length<4||!i.every(Number.isFinite)||Math.hypot(i[2]-i[0],i[3]-i[1])<=1e-9||t.set(Qu(i),[...i]);return[...t.values()]};function tp(e){return"solid"===e?.zero_wall_style?"solid":"dashed"}function ip(e,t=1,i){const n=[];for(const r of Array.isArray(e?.wall_segments)?e.wall_segments:[])0===Number(r?.cm)&&Xu(r?.a)&&Xu(r?.b)&&(i&&!i.has(String(r.id||""))||n.push(Ju(r.a,r.b,t)));return ep([n])}function np(e,t=1){const i=[];for(const n of Array.isArray(e?.partitions)?e.partitions:[])0===Number(n?.cm)&&Xu(n?.a)&&Xu(n?.b)&&i.push(Ju(n.a,n.b,t));for(const n of Array.isArray(e?.room_drafts)?e.room_drafts:[]){const e=Array.isArray(n?.points)?n.points:[];for(let r=0;r+1e?.host?.kind===t.kind&&e.host.id===t.id)}const ap=new WeakMap,lp=new WeakMap;function cp(e){const t=za(e.cfg),i={};for(const t of e.cfg.spaces||[])for(const e of t.rooms||[])e.area&&(i[e.area]=t.id);const n=e.cfg.settings?.exclude_integrations?new Set(e.cfg.settings.exclude_integrations):Re,r=De(e.cfg.settings?.icon_rules?.length?e.cfg.settings.icon_rules:Ce);return Wh({hass:e.hass,registry:e.registry,areaToSpace:i,markers:e.cfg.markers||[],settings:e.cfg.settings||{},excluded:n,showAll:!!e.cfg.settings?.show_all,firstSpaceId:t[0]?.id||"",loc:t=>Jd(e.lang,t),iconRules:r})}function hp(e){const t=za(e.cfg).find(t=>t.id===e.spaceId);if(!t)return null;const i=tn(e.cfg.spaces.find(t=>t.id===e.spaceId)),n=on(e.cfg.settings),r=e.iconSize??2.5,o=r>8?2.5:r,s=Bu(o),a=e.registry?Xc(e.hass,e.registry):e.hass,l=e.registry?Jc(e.hass,e.registry):e.hass,c=(e.devices||cp(e)).filter(t=>t.space===e.spaceId),h=c.filter(e=>!e.hidden),d=function(e,t,i){const n={},r=i/100*el(t)*1.3;for(const i of t.rooms){if(!i.area)continue;const t=e.filter(e=>e.area===i.area);if(!t.length)continue;const o=rl(i),s=.1*Math.min(o.w,o.h),a=o.w-2*s,l=o.h-2*s,c=Math.max(1,Math.round(Math.sqrt(t.length*a/Math.max(l,1)))),h=a/c,d=l/Math.max(Math.ceil(t.length/c),1),u=t.map((e,t)=>({x:o.x+s+h*(t%c+.5),y:o.y+s+d*(Math.floor(t/c)+.5)}));Ri(u,o,r,.5*s),t.forEach((e,t)=>n[e.id]=La(u[t]))}return n}(c,t,o),u=[];for(const t of h){const i=e.layout[t.id];if(i&&i.s===e.spaceId){const e=i.x*Ta,t=i.y*Ta;u.push({minX:e,minY:t,maxX:e,maxY:t})}}const p=e.cfg.spaces.find(t=>t.id===e.spaceId)||{},_=Array.isArray(p.walls)?p.walls:[],m=Number(p.cell_cm)>0?Number(p.cell_cm):5,g=op(p,t,Ta,.02*Ea),f=(p.openings||[]).flatMap(e=>{if("partition"!==e.host?.kind)return[];const i=Rd(e,t.partitions,Ta,m,Ea).resolved;return i?[i]:[]}),v=Tl({partitions:t.partitions,roomDrafts:t.room_drafts,columns:t.wall_columns,cellCm:m,hostedOpenings:f.map(e=>({id:e.opening.id,host:e.host,length:e.length,type:e.opening.type}))}),y=function(e,t,i,n){let r=lp.get(e);r||(r=new Map,lp.set(e,r));const o=r.get(t);if(o?.fingerprint===i)return o.value;const s=n();return r.set(t,{fingerprint:i,value:s}),s}(e.cfg,t.id,v,()=>ca(t,m,Ea,2e-4*Ea,f.map(Ad)).all);for(const e of y){const t=e.map(e=>e[0]),i=e.map(e=>e[1]);t.length&&u.push({minX:Math.min(...t),minY:Math.min(...i),maxX:Math.max(...t),maxY:Math.max(...i)})}for(const e of g.lines)u.push({minX:Math.min(e[0],e[2]),minY:Math.min(e[1],e[3]),maxX:Math.max(e[0],e[2]),maxY:Math.max(e[1],e[3])});const b=Ja(t,u),w=[b.x,b.y,b.w,b.h],k=new Map(t.rooms.map(t=>{const r=Tn(i.fill,t);return[t,an(r,"lqi"===r&&t.area?Kh(a,c,t.area):null,"light"===r?Rh(Mh(a,c,t,e.virtualLights)):"none","temp"===r&&t.area?Yh(a,c,t.area):null,i.tempMin,i.tempMax,n,en(i.customFill,t))]})),x=new Map;for(const e of t.rooms)e.id&&x.set(e.id,k.get(e)||null);const $=function(e,t=1e3){const i=[];for(const[n,r]of(e||[]).entries()){if("passage"!==r?.type)continue;const e={...r,id:String(r.id||`passage-${n}`),type:"passage",rx:Number(r.x)*t,ry:Number(r.y)*t,rlen:Number(r.length)*t};[e.rx,e.ry,e.rlen,e.angle].every(Number.isFinite)&&e.rlen>0&&i.push(e)}return i}((p.openings||[]).flatMap(e=>{if(!e.host||"wall"===e.host.kind)return[e];const t=f.find(t=>t.opening.id===e.id);return t?[Id(e,t,Ta)]:[]}),Ta),S=gs(t.rooms,_,g.contour,Na,m,Ea,Ta),M=f.filter(e=>Pd(e,S,2e-4*Ea)).map(e=>({x:e.center[0],y:e.center[1],angle:e.angle,length:e.length})),T=t.rooms.filter(e=>e.area||i.showBorders||"none"!==Tn(i.fill,e)).map(e=>{let n="room "+(t.bg?"overlay":"yard"),r="";const o=Tn(i.fill,e);if(i.showBorders||"none"!==o){n+=" styled";const t=[`--room-stroke:${i.color}`,`--room-stroke-op:${i.showBorders&&!g.contour.length?i.opacity:0}`],o=k.get(e)||null;o?(n+=" filled",t.push(`--room-fill:${o.color}`,`--room-fill-op:${o.opacity.toFixed(3)}`)):t.push("--room-fill:transparent","--room-fill-op:0"),r=t.join(";")}const s=e.id||G,a=e.area||G,l=e.poly?W``:W``;return l}),R=t.rooms.filter(e=>{const t=k.get(e);return Rn(i.glow,e)&&(!t||t.opacity<=0)}).map(e=>e.poly?W``:W``),C=new Map;for(const e of t.rooms)if(e.id){const t=k.get(e)||null;C.set(e.id,Rn(i.glow,e)&&(!t||t.opacity<=0)?{color:n.glow_base.c,opacity:n.glow_base.a,mode:"glow"}:null)}const D=Mh(a,h,null,e.virtualLights),A=h.map(n=>{const r=function(e,t,i,n,r){const o=t[e.id];return o&&o.s===e.space?{x:o.x*Ta,y:o.y*Ta}:n[e.id]?n[e.id]:La(Qa(r))}(n,e.layout,e.cfg,d,t),o=(r.x-w[0])/w[2]*100,s=(r.y-w[1])/w[3]*100,c=i.showLqi??!1!==e.showSignal,u=e.presentations?.get(Nu(n.id,c))||Pu(a,n,{liveStates:!1!==e.liveStates,showTemperature:!1!==e.showTemperature,showSignal:c,activityRuntime:e.activityRuntime?.get(n.id),sourceDetails:!1,lightDevices:h,lightSources:D,registryHass:l,reducedMotion:e.reducedMotion}),p=[`left:${o}%`,`top:${s}%`,...Lu(u)],_=xu(u),m=[n.name,Jd(e.lang,`marker.state_a11y_${_}`),"none"!==u.pulse.kind?Jd(e.lang,`marker.pulse_a11y_${u.pulse.reason}`):"",u.valueFullText||u.valueText||"",nu(u.valueBadge),null!=u.lqiText&&u.lqiBand?Jd(e.lang,`marker.lqi_a11y_${u.lqiBand}`,{value:u.lqiText}):""].filter(Boolean).join(", ");return B``}),O=i.showNames?t.rooms.filter(e=>e.name).map(n=>{const r=ol(n,t.id,e.layout,e.cfg),o=(r.x-w[0])/w[2]*100,s=(r.y-w[1])/w[3]*100,a=Math.min(1,i.opacity+.25);return B`
${n.name}
`}):[],z=t.bg?e.displayUrl?e.displayUrl(t.bg.href):t.bg.href:"",P=e.cfg.spaces.find(t=>t.id===e.spaceId)?.settings||{},F="daynight"===sr(e.cfg?.settings,P)?Yn(a,e.dayCycleNow??new Date):null,I=nn(e.cfg?.settings,i),E=!!(_.length||y.length&&i.showBorders),N=E?Tl($.length?{rooms:t.rooms,walls:_,extras:y,cellCm:m,zero:g.contour,passages:$.map(e=>({x:e.rx,y:e.ry,angle:e.angle,length:e.rlen})),hostedCompositeOpenings:M}:{rooms:t.rooms,walls:_,extras:y,cellCm:m,zero:g.contour}):"",H=E?function(e,t,i,n){let r=ap.get(e);r||(r=new Map,ap.set(e,r));const o=r.get(t);if(o?.fingerprint===i)return o.value;const s=n();return r.set(t,{fingerprint:i,value:s}),s}(e.cfg,t.id,N,()=>Hs(t.rooms,_,g.contour,[...$.filter(e=>"partition"!==e.host?.kind).map(e=>({x:e.rx,y:e.ry,angle:e.angle,length:e.rlen})),...M],Na,m,Ea,Ta,y)):null,L=$.length&&_.length?function(e,t,i,n,r,o,s,a=1){return[r,o,s,a].every(Number.isFinite)&&o>0&&a>0&&i?.length?Zs(Bs(e,i,n,r,o,s,a),t):t.map(()=>null)}(t.rooms,$.map(e=>({x:e.rx,y:e.ry,angle:e.angle,length:e.rlen})),_,g.contour,Na,m,Ea,Ta):$.map(()=>null),j=$.length?Wu({openings:$,geometries:L,fillsByRoomId:x,idPrefix:`${t.id}-static-data`,groupClass:"opening-tunnels static-opening-tunnels",dataLayer:"data"}):G,q=$.length?Wu({openings:$,geometries:L,fillsByRoomId:C,idPrefix:`${t.id}-static-glow`,groupClass:"opening-tunnels glow-base-tunnels static-opening-tunnels",dataLayer:"glow-base"}):G,U=_.length&&H?.paperD?[{path:H.paperD}]:Xt(t.rooms),V=i.showBorders?H:null,K=e.stageWidth&&w[2]?e.stageWidth/w[2]:1,Y=_o(m),Z=!!V&&(po(V.depthUnits,K)||mo(Y,K)),X=i.color||"#607d8b",J=i.hideOpenings?[]:f.map(e=>{const t=e.opening,i="passage"!==t.type&&t.contact?a.states?.[t.contact]?.state:null,n=ii(t.type,i,!!t.invert),r=n>0&&!!t.contact,o="gate"===t.type?!t.flip_v:!!t.flip_v,s={type:t.type,length:e.length,angle:e.angle,amount:n,flipH:!!t.flip_h,flipV:!!t.flip_v,base:X,tone:r?"var(--hp-open)":X,cellCm:m,gridPitch:Ea,face:Od(e,o)};return W` + ${tu(s)} + `});return B` +
+ ${dr(F)} + + ${V?W` + + + + `:G} + ${U.map(e=>"path"in e?W``:"poly"in e?W``:W``)} + ${z?W`e.assetLoaded?.(t.bg.href,z)} + transform=${t.bg.angle?`rotate(${t.bg.angle} ${t.bg.x+t.bg.w/2} ${t.bg.y+t.bg.h/2})`:G} + preserveAspectRatio="none" />`:G} + ${T} + ${i.showBorders&&g.contour.length?W``:G} + ${j} + ${R.length?W``:G} + ${q} + ${V?W` + ${V.paths.map(e=>W` + + `)} + `:G} + ${i.showBorders&&g.lines.length?W``:G} + ${J} + + ${""} +
${A}${O}
+
+ `}let dp=null,up=null,pp=0,_p=-1,mp=null,gp=[];const fp=new Set,vp=(e,t)=>{"function"==typeof e&&t.push(e)};function yp(){if(dp)return dp;try{const e=JSON.parse(localStorage.getItem("houseplan_card_cfg_v1")||"null");if(e&&e.config&&Array.isArray(e.config.spaces)){const t=e.layout||{};return{config:e.config,rev:e.rev||0,configFingerprint:e.config_fingerprint||Tl(e.config),layout:t,layoutRev:e.layout_rev||0,layoutFingerprint:e.layout_fingerprint||Tl(t),virtualLights:Cc(e.virtual_lights,e.rev||0)}}}catch{}return null}function bp(e,t=!1){if(t&&(dp=null,pp++,up))return up.catch(()=>null).then(()=>bp(e,!1));if(dp)return Promise.resolve(dp);if(up)return _p!==pp?up.catch(()=>null).then(()=>bp(e,!1)):up;return _p=pp,up=async function(e,t){const[i,n]=await Promise.all([e.callWS({type:"houseplan/config/get"}),e.callWS({type:"houseplan/layout/get"})]),r=i?.rev??0,o=dp?Dc(dp.virtualLights,i?.virtual_lights,r,!!i&&"virtual_lights"in i):Cc(i?.virtual_lights,r),s={config:i?.config??null,rev:r,configFingerprint:Tl(i?.config??null),layout:n?.layout??{},layoutRev:n?.rev??0,layoutFingerprint:Tl(n?.layout??{}),virtualLights:o};t===pp&&(dp=s);const a=e.connection;if(a?.subscribeEvents&&mp!==a){for(const e of gp)e();gp=[],mp=a;const e=()=>{dp=null,pp++,fp.forEach(e=>e())},t=[];try{if(vp(await a.subscribeEvents(e,"houseplan_config_updated"),t),vp(await a.subscribeEvents(e,"houseplan_layout_updated"),t),vp(await a.subscribeEvents(e=>{if(dp){const t=Fc(dp.virtualLights,e?.data);if(t===dp.virtualLights)return;dp={...dp,virtualLights:t}}else pp++;fp.forEach(e=>e())},"houseplan_virtual_light_updated"),t),mp===a)gp=t;else for(const e of t)e()}catch{for(const e of t)e();mp===a&&(mp=null)}}return s}(e,pp).finally(()=>{up=null,_p=-1}),up}const wp=3300;function kp(e,t){return{sources:e,last:Object.fromEntries(t.map(e=>[e.eid,e.state])),flashTs:0,flashKind:null,timer:0,gen:0,expiresAt:0,alarmActive:"alarm"===Sc(t).status}}function xp(e,t,i,n){n(e.timer);const r=kp(t,i);Object.assign(e,r)}function $p(e,t,i,n,r){e.flashTs&&i-e.flashTs"transition"===e.activity)&&(i(e.timer),e.flashTs=0,e.flashKind=null,e.expiresAt=0);if("alarm"===Sc(t).status){e.alarmActive||(i(e.timer),e.flashTs=0,e.flashKind=null,e.expiresAt=0);for(const i of t)e.last[i.eid]=i.state;return e.alarmActive=!0,null}if(e.alarmActive){for(const i of t)e.last[i.eid]=i.state;return e.alarmActive=!1,null}let n=null;for(const i of t){const t=Mc(e.last[i.eid],i);("event"===t||!n&&t)&&(n=t),e.last[i.eid]=i.state}return n}class Mp{constructor(e,t=()=>{}){this._host=e,this._onGateChange=t,this._modality="unknown",this._gate=!1,this._media=null,this._connected=!1,this._onMediaChange=()=>this._syncGate()}get modality(){return this._modality}get hoverEnabled(){return this._gate}connect(e=globalThis.window){this._connected||(this._connected=!0,this._modality="unknown",this._media="function"==typeof e?.matchMedia?e.matchMedia("(any-hover: hover) and (any-pointer: fine)"):null,this._media?.addEventListener?.("change",this._onMediaChange),this._setGate(!1))}disconnect(){this._media?.removeEventListener?.("change",this._onMediaChange),this._media=null,this._connected=!1,this._modality="unknown",this._setGate(!1)}note(e){return this._modality=function(e,t){const i="mouse"===(n=t.pointerType)||"touch"===n||"pen"===n?n:null;var n;return i?"mouse"===i&&t.sourceCapabilities?.firesTouchEvents?e:i:e}(this._modality,e),this._syncGate(),this._modality}suspend(){this._setGate(!1)}_syncGate(){var e,t;this._setGate((e=this._modality,t=!!this._media?.matches,"mouse"===e&&t))}_setGate(e){this._gate!==e&&(this._gate=e,this._host.toggleAttribute("data-pointer-hover",e),this._onGateChange(e))}}class Tp extends he{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!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}))}catch{this._spaces=[]}finally{this._spacesLoading=!1}}}get _lang(){return Xd(this.hass,this._config?.language)}get _schema(){const e=this._spaces||[];return[e.length?{name:"space",selector:{select:{mode:"dropdown",options:e}}}:{name:"space",selector:{text:{}}},{name:"title",selector:{text:{}}},{name:"show_button",selector:{boolean:{}}},{name:"button_label",selector:{text:{}}},{name:"button_target",selector:{text:{}}},{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:{}}}]}render(){if(!this.hass||!this._config)return G;this._loadSpaces();const e=this._lang,t={space:Jd(e,"editor.space"),title:Jd(e,"editor.title"),show_button:Jd(e,"editor.show_button"),button_label:Jd(e,"editor.button_label"),button_target:Jd(e,"editor.button_target"),icon_size:Jd(e,"editor.icon_size"),show_temperature:Jd(e,"editor.show_temperature"),live_states:Jd(e,"editor.live_states"),show_signal:Jd(e,"editor.show_signal")};return B`t[e.name]||e.name} + @value-changed=${this._valueChanged} + >`}_valueChanged(e){const t={...this._config||{},...e.detail.value};delete t.aspect_ratio;const i=new Event("config-changed",{bubbles:!0,composed:!0});i.detail={config:t},this.dispatchEvent(i)}}Tp.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-space-card-editor")||customElements.define("houseplan-space-card-editor",Tp);const Rp=e=>{history.pushState(null,"",e),((e,t,i)=>{const n=new Event(t,{bubbles:!0,composed:!0});n.detail=i??{},e.dispatchEvent(n)})(window,"location-changed",{replace:!1})};class Cp extends he{constructor(){super(...arguments),this._snap=null,this._loading=!1,this._reloadQueued=!1,this._forceReloadQueued=!1,this._reloadRetryTimer=0,this._dayCycleTimer=0,this._dayCycleClockKey="",this._stageWidth=0,this._pendingStageWidth=0,this._stageWidthRaf=0,this._haRegistryConnection=null,this._haRegistryRevision=-1,this._devices=[],this._continuity=this._newContinuityController(),this._continuityHistory=[],this._continuityEpoch=0,this._continuityDataReady=!0,this._continuityPaintToken=-1,this._continuityDisposed=!1,this._renderSnapshotAt=Date.now(),this._hassSequence=0,this._connHooked=null,this._connectionWasLost=!1,this._visibleDeviceSnapshot=null,this._candidateDeviceSnapshot=null,this._stagedDeviceSnapshotToken=-1,this._capturedSnapshotSequence=-1,this._capturedSnapshotDevices=null,this._capturedSnapshotActivity="",this._capturedSnapshotVirtual="",this._activityRuntime=new Map,this._reducedMotion=!1,this._pointerModality=new Mp(this),this._onMotionChange=e=>{this._reducedMotion=e.matches,this._capturedSnapshotSequence=-1,this.requestUpdate()},this._onHaRegistryUpdate=()=>{const e=Vc(this.hass).revision;e!==this._haRegistryRevision&&(this._haRegistryRevision=e,this._refreshDevices(),this._capturedSnapshotSequence=-1,this.requestUpdate())},this._dayCycleTick=()=>{if(!this.isConnected||"hidden"===this.ownerDocument.visibilityState)return;const e=this._dayCycleState();if(!e)return this._dayCycleTimer&&(window.clearInterval(this._dayCycleTimer),this._dayCycleTimer=0),void(this._dayCycleClockKey="");const t=Zn(e);t!==this._dayCycleClockKey&&(this._dayCycleClockKey=t,this.requestUpdate())},this._pageVisibility=e=>{if(this._continuity.visibility(e),this._dayCycleVisibility(e),"hidden"!==e.kind){if(!e.long){const e=Date.now();let t=!1;for(const i of this._activityRuntime.values())!i.flashKind||(i.expiresAt||i.flashTs+3300)>e||(i.flashTs=0,i.flashKind=null,i.expiresAt=0,t=!0);return void(t&&(this._capturedSnapshotSequence=-1,this.requestUpdate()))}Date.now()-this._renderSnapshotAt>1e3&&this._continuity.note("device-snapshot-stale"),this._continuityDataReady=!1,this._continuityPaintToken=-1,this._load(!0)}else this._pointerModality.suspend()},this._onConnLost=()=>{this._connectionWasLost=!0,this._continuityDataReady=!1,this._continuityPaintToken=-1,this._continuity.connectionLost()},this._onConnReady=()=>{!this._connectionWasLost&&this._continuity.hasCompleteFrame?this._beginContinuityCandidate("connection-ready",!1,"plan"):(this._continuityDataReady=!1,this._continuityPaintToken=-1),this._load(!0)},this._onAssetLoaded=(e,t)=>{this._signer.markLoaded(this.hass,e,t),this._continuity.note("asset-ready"),this._continuityPaintToken=-1,"steady"!==this._continuity.state&&this.requestUpdate()},this._loadedOnce=!1,this._signer=new Dl(()=>this.requestUpdate()),this._retryContinuity=()=>{this._continuityDataReady=!1,this._continuityPaintToken=-1,this._continuity.retry(this._continuity.recoveryReason||"plan"),this._load(!0)},this._goToSpace=()=>{const e=(this._config?.button_target||"/plan-doma").replace(/#.*$/,"");Rp(`${e}#space=${encodeURIComponent(this._config.space)}`)}}_ensureHaRegistryAuthority(){const e=this.hass?.connection||null;e&&e!==this._haRegistryConnection&&(this._haRegistryRelease?.(),this._haRegistryConnection=e,this._haRegistryRevision=-1,this._haRegistryRelease=Gc(this.hass,this._onHaRegistryUpdate),this._onHaRegistryUpdate())}_newContinuityController(){return new Ml(()=>{this._continuityEpoch++,this.isConnected&&this.requestUpdate()})}_dayCycleState(e=new Date){const t=this._snap?.config;if(!t||!this._config)return null;const i=t.spaces?.find(e=>e.id===this._config?.space)?.settings||{};return"daynight"!==sr(t.settings,i)?null:Yn(this._renderDeviceSnapshot?.hass||this.hass,e)}_syncDayCycleClock(){const e=this._dayCycleState();this._dayCycleClockKey=e?Zn(e):"";const t="clock"===e?.source&&"hidden"!==this.ownerDocument.visibilityState&&this.isConnected;t&&!this._dayCycleTimer?this._dayCycleTimer=window.setInterval(this._dayCycleTick,3e4):!t&&this._dayCycleTimer&&(window.clearInterval(this._dayCycleTimer),this._dayCycleTimer=0)}_dayCycleVisibility(e){"hidden"!==e.kind?(this._dayCycleTick(),this._syncDayCycleClock()):this._dayCycleTimer&&(window.clearInterval(this._dayCycleTimer),this._dayCycleTimer=0)}_hookConnection(){const e=this.hass?.connection;e&&e!==this._connHooked&&(this._connHooked?.removeEventListener?.("ready",this._onConnReady),this._connHooked?.removeEventListener?.("disconnected",this._onConnLost),this._connHooked?.removeEventListener?.("reconnect-error",this._onConnLost),e.addEventListener?.("ready",this._onConnReady),e.addEventListener?.("disconnected",this._onConnLost),e.addEventListener?.("reconnect-error",this._onConnLost),this._connHooked=e)}static getConfigElement(){return document.createElement("houseplan-space-card-editor")}static getStubConfig(e){const t=yp();return{type:"custom:houseplan-space-card",space:za(t?.config||null)[0]?.id||"",show_button:!0,live_states:!0,show_temperature:!0,show_signal:!0}}setConfig(e){if(!e||!e.space)throw new Error('houseplan-space-card: "space" is required');this._config={show_button:!0,button_target:"/plan-doma",live_states:!0,show_temperature:!0,show_signal:!0,...e},this._snap=this._snap||yp()}connectedCallback(){var e;this._continuityDisposed&&(this._continuity=this._newContinuityController(),this._continuityDisposed=!1,this._continuityPaintToken=-1),super.connectedCallback(),this._pointerModality.connect(this.ownerDocument.defaultView),this._motionMedia=window.matchMedia?.("(prefers-reduced-motion: reduce)"),this._reducedMotion=!!this._motionMedia?.matches,this._motionMedia?.addEventListener?.("change",this._onMotionChange),this.hass&&this._ensureHaRegistryAuthority(),this._continuityUnsub?.(),this._continuityUnsub=$l(this.ownerDocument,this._pageVisibility),this._unsub=(e=()=>{this._beginContinuityCandidate("config-event",!1),this._reloadQueued=!0,this._load()},fp.add(e),()=>fp.delete(e)),this._signer.start(()=>this.hass,()=>this._referenced())}disconnectedCallback(){this._pointerModality.disconnect(),this._continuityUnsub?.(),this._continuityUnsub=void 0,this._motionMedia?.removeEventListener?.("change",this._onMotionChange),this._motionMedia=void 0,this._unsub?.(),this._unsub=void 0,window.clearTimeout(this._reloadRetryTimer),this._reloadRetryTimer=0,this._dayCycleTimer&&(window.clearInterval(this._dayCycleTimer),this._dayCycleTimer=0),this._dayCycleClockKey="",this._stageObserver?.disconnect(),this._stageObserver=void 0,this._observedStage=void 0,this._stageWidthRaf&&cancelAnimationFrame(this._stageWidthRaf),this._stageWidthRaf=0,this._pendingStageWidth=0,this._signer.dispose(),this._haRegistryRelease?.(),this._haRegistryRelease=void 0,this._haRegistryConnection=null,this._connHooked?.removeEventListener?.("ready",this._onConnReady),this._connHooked?.removeEventListener?.("disconnected",this._onConnLost),this._connHooked?.removeEventListener?.("reconnect-error",this._onConnLost),this._connHooked=null;for(const e of this._activityRuntime.values())window.clearTimeout(e.timer);this._activityRuntime.clear(),this._continuityHistory=[...this._continuityHistory,...this._continuity.trace].slice(-80),this._continuity.dispose(),this._continuityDisposed=!0,super.disconnectedCallback()}willUpdate(e){e.has("hass")&&this.hass&&(this._hassSequence++,this._renderSnapshotAt=Date.now(),this._continuity.note("hass-snapshot"),this._ensureHaRegistryAuthority(),this._hookConnection()),!this.hass||this._loading||this._snap&&!e.has("hass")||this._snap&&this._loadedOnce||this._load(),(e.has("hass")||e.has("_snap")||e.has("_config")||!this._devices.length)&&this._refreshDevices(),this._captureRenderDeviceSnapshot(),this._continuity.hasCompleteFrame&&"steady"===this._continuity.state&&this._continuity.refreshCompleteFrame(this._frameFingerprint())}_stampActivity(e,t){$p(e,t,Date.now(),window.clearTimeout.bind(window),e=>window.setTimeout(()=>this.requestUpdate(),e))}_syncActivity(e,t,i=this.hass){if(!1===this._config?.live_states){for(const e of this._activityRuntime.values())window.clearTimeout(e.timer);return void this._activityRuntime.clear()}const n=new Set,r=Mh(t,e,null,this._snap?.virtualLights);for(const o of e){if(o.hidden)continue;if("icon_ripple"!==Ai(o.marker?.display))continue;n.add(o.id);const s=Cu(t,o,e,r,i),a=s.samples,l=Ou(t,o,s);let c=this._activityRuntime.get(o.id);if(!c||c.sources!==l){c&&window.clearTimeout(c.timer),c=kp(l,a),this._activityRuntime.set(o.id,c);continue}const h=Sp(c,a,window.clearTimeout.bind(window));h&&this._stampActivity(c,h)}for(const[e,t]of this._activityRuntime)n.has(e)||(window.clearTimeout(t.timer),this._activityRuntime.delete(e))}_refreshDevices(){if(!this.hass||!this._snap?.config||!this._config)return;const e=Vc(this.hass),t=cp({hass:this.hass,registry:e,cfg:this._snap.config,lang:this._lang});this._syncActivity(t,Xc(this.hass,e),this.hass),this._devices=t}_captureRenderDeviceSnapshot(){if(!this.hass)return;const e=Date.now(),t=[...this._activityRuntime.entries()].map(([t,i])=>`${t}:${i.gen}:${i.flashTs}:`+(i.flashKind&&(i.expiresAt||i.flashTs+3300)>e?1:0)).join("|"),i=this._snap?.virtualLights?`${this._snap.virtualLights.configRev}:${this._snap.virtualLights.rev}`:"";if(this._capturedSnapshotSequence===this._hassSequence&&this._capturedSnapshotDevices===this._devices&&this._capturedSnapshotActivity===t&&this._capturedSnapshotVirtual===i)return;const n=Xc(this.hass,Vc(this.hass)),r=new Map,o=new Set(["sun.sun"]),s=new Set,a=new Set,l=e=>{if(!e)return;const t=e.indexOf(":");if(t<0)return void o.add(e);const i=e.slice(0,t),n=e.slice(t+1);"device"===i?s.add(n):"entity"===i&&o.add(n)},c=za(this._snap?.config||null).find(e=>e.id===this._config?.space);for(const e of c?.rooms||[])e.area&&a.add(e.area),l(e.settings?.temp_source),l(e.settings?.hum_source);const h=this._snap?.config?.spaces?.find(e=>e.id===this._config?.space);for(const e of h?.openings||[])for(const t of ni(e))o.add(t);const d=Mh(n,this._devices,null,this._snap?.virtualLights);for(const e of this._devices)for(const t of[!1,!0])r.set(Nu(e.id,t),Pu(n,e,{liveStates:!1!==this._config?.live_states,showTemperature:!1!==this._config?.show_temperature,showSignal:t,activityRuntime:this._activityRuntime.get(e.id),sourceDetails:!1,lightDevices:this._devices,lightSources:d,registryHass:this.hass,reducedMotion:this._reducedMotion}));const u=Eu({sourceSequence:this._hassSequence,hass:n,devices:this._devices,presentations:r,entityIds:o,deviceIds:s,areaIds:a});this._capturedSnapshotSequence=this._hassSequence,this._capturedSnapshotDevices=this._devices,this._capturedSnapshotActivity=t,this._capturedSnapshotVirtual=i,this._visibleDeviceSnapshot&&"steady"!==this._continuity.state?this._candidateDeviceSnapshot=u:(this._visibleDeviceSnapshot=u,this._candidateDeviceSnapshot=null)}get _renderDeviceSnapshot(){return this._stagedDeviceSnapshotToken===this._continuity.token?this._candidateDeviceSnapshot||this._visibleDeviceSnapshot:this._visibleDeviceSnapshot||this._candidateDeviceSnapshot}_beginContinuityCandidate(e,t,i="plan"){return this._continuityDataReady=t,this._continuityPaintToken=-1,this._stagedDeviceSnapshotToken=-1,this._continuity.beginCandidate(e,i)}_backdropRaw(){return this._snap?.config&&this._config&&za(this._snap.config).find(e=>e.id===this._config.space)?.bg?.href||""}_candidateBackdrop(e){return e&&this._config&&za(e).find(e=>e.id===this._config.space)?.bg?.href||""}_assetsReady(){const e=this._backdropRaw();return!e||this._signer.isReady(this.hass,e)}_frameFingerprint(){const e=this._snap;return Rl([e?.rev||0,e?.configFingerprint||Tl(e?.config),e?.layoutRev||0,e?.layoutFingerprint||Tl(e?.layout),e?.virtualLights?`${e.virtualLights.configRev}:${e.virtualLights.rev}`:"",this._config?.space||"",this._stageWidth,this.hass?.themes?.darkMode??this.hass?.themes?.default_theme??""])}_stageValid(){const e=this._observedStage;return!!e&&e.clientWidth>0&&e.clientHeight>0}_settleContinuityFrame(){if(!this._stageValid())return;if(!this._continuity.hasCompleteFrame&&"steady"===this._continuity.state){if(this._assetsReady())return this._renderSnapshotAt=Date.now(),void this._continuity.markCompleteFrame(this._frameFingerprint());this._beginContinuityCandidate("asset-wait",!0,"asset")}if(!this._continuityDataReady)return;if(!["holding","offline-stale","overlay-pending","overlay-visible","candidate-ready"].includes(this._continuity.state))return;const e=this._continuity.token;if(this._candidateDeviceSnapshot&&this._candidateDeviceSnapshot!==this._visibleDeviceSnapshot&&this._stagedDeviceSnapshotToken!==e)return this._stagedDeviceSnapshotToken=e,void this.requestUpdate();this._continuityPaintToken!==e&&(this._continuityPaintToken=e,this._continuity.candidateReady(e)&&this._continuity.commitAfterPaint(e,{updateComplete:()=>this.updateComplete,stageValid:()=>this.isConnected&&this._stageValid(),assetsReady:()=>this._assetsReady(),frameFingerprint:()=>this._frameFingerprint()}).then(t=>{t&&e===this._continuity.token?(this._renderSnapshotAt=Date.now(),this._candidateDeviceSnapshot&&(this._visibleDeviceSnapshot=this._candidateDeviceSnapshot),this._candidateDeviceSnapshot=null,this._stagedDeviceSnapshotToken=-1):e===this._continuity.token&&(this._continuityPaintToken=-1,this._stagedDeviceSnapshotToken=-1,this._candidateDeviceSnapshot=null,this.requestUpdate())}))}updated(){const e=this.renderRoot.querySelector(".hp-static-stage")||void 0;if(e!==this._observedStage)if(this._stageObserver?.disconnect(),this._observedStage=e,e){const t=()=>{const i=e.clientWidth;if(!(i<=0||Math.abs(i-this._stageWidth)<=.5)){if(this._stageWidth<=0)return this._stageWidth=i,void this.requestUpdate();this._pendingStageWidth=i,this._stageWidthRaf||(this._stageWidthRaf=requestAnimationFrame(()=>{this._stageWidthRaf=0;const e=this._pendingStageWidth;this._pendingStageWidth=0,!e||!this._observedStage||this._observedStage.clientWidth<=0||(Math.abs(this._observedStage.clientWidth-e)>.5?t():Math.abs(e-this._stageWidth)<=.5||(this._continuity.hasCompleteFrame&&this._beginContinuityCandidate("stage-resize",!0,"stage-size"),this._stageWidth=e,this.requestUpdate()))}))}};this._stageObserver=new ResizeObserver(t),this._stageObserver.observe(e),t()}else this._stageObserver=void 0;this._syncDayCycleClock(),this._settleContinuityFrame()}async _load(e=!1){if(this.hass){if(this._loading)return this._reloadQueued=!0,void(this._forceReloadQueued||=e);this._loading=!0,this._reloadQueued=!1;try{const t=await bp(this.hass,e),i=!this._snap||this._snap.configFingerprint!==t.configFingerprint,n=!this._snap||this._snap.layoutFingerprint!==t.layoutFingerprint,r=!this._snap||this._snap.virtualLights!==t.virtualLights;if(i&&!await this._signer.prepareImage(this.hass,this._candidateBackdrop(t.config)))return this._continuity.note("asset-failed"),window.clearTimeout(this._reloadRetryTimer),void(this._reloadRetryTimer=window.setTimeout(()=>{this._load(!0)},1e3));window.clearTimeout(this._reloadRetryTimer),this._reloadRetryTimer=0,(i||n)&&this._continuity.hasCompleteFrame&&"steady"===this._continuity.state&&this._beginContinuityCandidate("structural-response",!0),i||n||r?this._snap=t:this._snap&&(this._snap.rev=t.rev,this._snap.layoutRev=t.layoutRev),i&&this._continuity.note("config-candidate",{configRev:t.rev}),n&&this._continuity.note("layout-candidate",{layoutRev:t.layoutRev}),r&&(this._capturedSnapshotSequence=-1),this._loadedOnce=!0,this._connectionWasLost=!1,this._continuityDataReady=!0,this._refreshDevices()}catch{}finally{if(this._loading=!1,this._continuityDataReady=!0,this.requestUpdate(),this._reloadQueued){const e=this._forceReloadQueued;this._reloadQueued=!1,this._forceReloadQueued=!1,this._load(e)}}}}get _lang(){return Xd(this.hass,this._config?.language)}getCardSize(){const e=za(this._snap?.config||null).find(e=>e.id===this._config?.space);if(e){const t=e.vb[3]/e.vb[2];return Math.max(3,Math.round(8*t))+(!1===this._config?.show_button?0:1)}return 6}_errorCard(e){return B` +
${e}
+
`}_referenced(){return Sn(this._snap?.config)}_renderRecoveryOverlay(){if(!this._continuity.overlayVisible&&"recovery-error"!==this._continuity.state)return G;const e="connection"===this._continuity.recoveryReason;return B`
+ + ${Jd(this._lang,e?"continuity.restore_connection":"continuity.restore_plan")} + ${"recovery-error"===this._continuity.state?B``:G} +
`}houseplanContinuityTrace(){return[...this._continuityHistory,...this._continuity.trace].slice(-80).map(e=>({...e,...e.stage?{stage:[...e.stage]}:{}}))}render(){if(!this._config)return G;const e=this._snap?.config;if(!e)return this._errorCard(Jd(this._lang,"space_card.loading"));const t=this._config.space,i=this._renderDeviceSnapshot,n=hp({hass:i?.hass||this.hass,cfg:e,layout:this._snap?.layout||{},spaceId:t,iconSize:this._config.icon_size,stageWidth:this._stageWidth,lang:this._lang,displayUrl:e=>this._signer.display(this.hass,e),assetLoaded:this._onAssetLoaded,registry:i?{revision:i.sourceSequence,authoritative:!1,access:"limited",entities:{...i.hass.entities},devices:{...i.hass.devices},lastSuccess:i.capturedAt}:Vc(this.hass),devices:[...i?.devices||this._devices],presentations:i?.presentations,activityRuntime:this._activityRuntime,reducedMotion:this._reducedMotion,virtualLights:this._snap?.virtualLights,liveStates:!1!==this._config.live_states,showTemperature:!1!==this._config.show_temperature,showSignal:!1!==this._config.show_signal,inert:this._continuity.overlayBlocksInteraction});if(!n)return this._errorCard(Jd(this._lang,"space_card.not_found",{id:t}));const r=za(e).find(e=>e.id===t),o=void 0!==this._config.title?this._config.title:r?.title||"",s=!1!==this._config.show_button,a=this._config.button_label||Jd(this._lang,"space_card.button"),l=this._continuity.overlayVisible||"recovery-error"===this._continuity.state?this._continuity.recoveryReason:null;return B` + this._pointerModality.note(e)} + @pointerdown=${e=>this._pointerModality.note(e)} + @pointermove=${e=>this._pointerModality.note(e)}> + ${o?B`
${o}
`:G} +
+ ${n} + ${this._renderRecoveryOverlay()} +
+ ${s?B`
+ +
`:G} +
+ `}}Cp.properties={hass:{attribute:!1},_config:{state:!0},_snap:{state:!0},_continuityEpoch:{state:!0}},Cp.styles=[Bd,o` + .hp-static-title { + font-weight: 700; + padding: 10px 14px 6px; + font-size: 16px; + color: var(--primary-text-color); + } + .hp-static-body { + position: relative; + } + .hp-static-stage { + position: relative; + width: 100%; + container-type: inline-size; + overflow: hidden; + pointer-events: none; /* kill ALL interaction on the schematic (§4) */ + background: var(--ha-card-background, var(--card-background-color, #111)); + } + .hp-static-stage svg { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + display: block; + z-index: 1; + } + /* Opaque plan paper — the scene bg_color/day-cycle environment shows only AROUND + the plan (owner 2026-08-03). The static card keeps its historical + canvas colour: the theme card background. Drawn plans paper the room + contours (per-room shapes, fill only, no stroke) — same contract as + the full card. */ + .hp-paper { + fill: var(--ha-card-background, var(--card-background-color, #111)); + stroke: none; + } + .wallbody-fill { + fill: var(--wall-fill, #ffffff); + fill-opacity: var(--wall-fill-op, 1); + fill-rule: evenodd; + stroke: none; + pointer-events: none; + } + .wallbody { + fill: url(#hp-wall-hatch); + fill-rule: evenodd; + stroke: var(--room-stroke, var(--hp-muted, #607d8b)); + stroke-width: calc(0.6px * var(--hp-cell-visual-scale, 1)); + pointer-events: none; + } + .wallbody.solid { + fill: none; + } + .hp-static-stage .devlayer { + position: absolute; + inset: 0; + z-index: 2; + } + .hp-static-foot { + padding: 8px 12px 12px; + pointer-events: auto; /* the button stays clickable */ + } + .hp-static-btn { + width: 100%; + padding: 9px 14px; + border: none; + border-radius: 10px; + background: var(--primary-color, #3ea6ff); + color: var(--text-primary-color, #fff); + font-size: 14px; + font-weight: 600; + cursor: pointer; + } + :host([data-pointer-hover]) .hp-static-btn:hover { + filter: brightness(1.08); + } + .hp-static-error { + padding: 24px; + text-align: center; + color: var(--secondary-text-color, #9aa4ad); + } + `],customElements.get("houseplan-space-card")||customElements.define("houseplan-space-card",Cp),window.customCards=window.customCards||[],window.customCards.find(e=>"houseplan-space-card"===e.type)||window.customCards.push({type:"houseplan-space-card",name:"House Plan — Space (static)",description:"Read-only live schematic of a single houseplan space, with a deep-link button.",preview:!1,documentation:"https://github.com/Matysh/houseplan-card"});const Dp=o` + :host { + --hp-editor-tray-bg-fallback: color-mix(in srgb, var(--card-background-color, var(--hp-bg)) 94%, var(--hp-line)); + --hp-editor-tray-bg: color-mix(in srgb, var(--card-background-color, var(--hp-bg)) 86%, transparent); + --hp-editor-tray-border: color-mix(in srgb, var(--hp-line) 82%, var(--hp-accent)); + --hp-editor-tray-blur: 8px; + --hp-editor-tray-shadow: var(--shadow-2); + } + + /* Contextual editor controls float over the stage. Their appearance must + never alter stage height, viewport fitting or the pinned close action. */ + .editor-secondary-host { + position: absolute; + inset: var(--sp-2) var(--sp-4) auto; + z-index: 70; + display: flex; + justify-content: flex-end; + pointer-events: none; + } + .editor-secondary-host.kind-palette, + .editor-secondary-host.kind-group { + justify-content: flex-start; + } + .editor-secondary { + pointer-events: none; + max-width: 100%; + display: flex; + align-items: center; + gap: var(--sp-3); + padding: var(--sp-3) var(--sp-4); + color: var(--hp-txt); + background: var(--card-background-color, var(--hp-bg)); + background: var(--hp-editor-tray-bg-fallback); + border: 1px solid var(--hp-line); + border-color: var(--hp-editor-tray-border); + border-radius: var(--rad-l); + box-shadow: var(--hp-editor-tray-shadow); + opacity: 0; + visibility: hidden; + transform: translateY(-4px); + transition: + opacity 0.1s ease, + transform 0.1s cubic-bezier(0.2, 0.7, 0.2, 1), + visibility 0s linear 0.1s; + touch-action: auto; + } + .editor-secondary-host.open .editor-secondary { + pointer-events: auto; + opacity: 1; + visibility: visible; + transform: translateY(0); + transition-duration: 0.14s, 0.14s, 0s; + transition-delay: 0s; + } + .editor-secondary-host.blocked .editor-secondary { pointer-events: none; } + @supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { + .editor-secondary { + background: var(--hp-editor-tray-bg); + -webkit-backdrop-filter: blur(var(--hp-editor-tray-blur)); + backdrop-filter: blur(var(--hp-editor-tray-blur)); + } + } + .editor-secondary-content, + .editor-group-items { + display: flex; + align-items: center; + gap: var(--sp-3); + min-width: 0; + } + .editor-context-label { + max-width: 14rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding-inline-end: var(--sp-4); + border-inline-end: 1px solid var(--hp-line); + color: var(--hp-muted); + font-size: var(--fs-s); + } + .editor-secondary.kind-palette { + width: min(760px, 100%); + align-items: stretch; + padding: 0; + overflow: hidden; + } + .editor-secondary.kind-palette .editor-secondary-content, + .editor-secondary.kind-palette .furnpalette { + width: 100%; + } + .editor-secondary.kind-palette .furnpalette { + max-height: 38vh; + border: 0; + background: transparent; + } + .editor-group-launcher .group-chevron { + --mdc-icon-size: 16px; + transition: transform 0.14s ease; + } + .editor-group-launcher[aria-expanded="true"] .group-chevron { transform: rotate(180deg); } + + .editor-secondary .bdhint { + font-size: var(--fs-s); + color: var(--hp-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .editor-secondary .dfill { + display: inline-flex; + align-items: center; + gap: var(--sp-2); + font-size: var(--fs-s); + cursor: pointer; + } + .editor-secondary .dfill input[type="checkbox"] { + width: 16px; + height: 16px; + flex: none; + margin: 0; + padding: 0; + } + .editor-secondary hp-color-opacity { flex: 0 0 auto; } + .editor-secondary input { + width: 74px; + background: transparent; + border: 1px solid var(--hp-line); + color: var(--hp-txt); + border-radius: var(--rad-s); + padding: var(--sp-3) var(--sp-4); + font-size: var(--fs-m); + } + .editor-secondary label, + .editor-secondary .hint { + color: var(--hp-muted); + font-size: var(--fs-s); + } + .editor-secondary .drawwall { + display: inline-flex; + align-items: center; + gap: var(--sp-3); + white-space: nowrap; + } + .editor-secondary .drawwall input { width: 4.2em; } + .editor-secondary .drawwall .opl { + color: var(--hp-muted); + font-size: var(--fs-s); + } + + @container (min-width: 560px) and (max-width: 899px) { + .editor-secondary-content, + .editor-group-items { + flex-wrap: wrap; + justify-content: flex-end; + } + } + @container (max-width: 559px) { + .editor-secondary-host { inset-inline: var(--sp-4); } + .editor-secondary { max-width: 100%; } + .editor-secondary-content, + .editor-group-items { + overflow-x: auto; + scrollbar-width: thin; + } + .editor-secondary-content > *, + .editor-group-items > * { flex: none; } + .editor-context-label { display: none; } + } + @media (prefers-reduced-motion: reduce) { + .editor-secondary { transition: none; } + .editor-group-launcher .group-chevron { transition: none; } + } +`;function Ap(e){return e instanceof Error?e.name||"Error":typeof e}function Op(e){return Tl({id:e?.id??"",cell_cm:e?.cell_cm,rooms:e?.rooms||[],walls:e?.walls||[],wall_segments:e?.wall_segments||[],open_spans:e?.open_spans||[],openings:e?.openings||[],partitions:e?.partitions||[],room_drafts:e?.room_drafts||[],wall_columns:e?.wall_columns||[]})}function zp(e,t,i={}){const n=(Array.isArray(e?.spaces)?e.spaces:[]).find(e=>String(e?.id||"")===String(t||"")),r=Op(n);if(!n)return{spaceId:String(t||""),displayName:"",status:"failed",reason:"prepare-exception",fingerprint:r,ok:!1};const o=Hp({...e,spaces:[n]},{...i,fingerprint:()=>r}).spaces[0]||{spaceId:String(t||""),displayName:"",status:"failed",reason:"prepare-exception"};return{...o,fingerprint:r,ok:"failed"!==o.status}}function Pp(e,t,i,n=Ea,r=1e3){return(Array.isArray(e?.openings)?e.openings:[]).flatMap(e=>{const o={...e,rx:Number(e.x)*r,ry:Number(e.y)*r,rlen:Number(e.length)*r};if(!e.host||"wall"===e.host.kind)return[o];const s=Rd(e,t.partitions,r,i,n);return s.resolved?[{...e,rx:s.resolved.center[0],ry:s.resolved.center[1],rlen:s.resolved.length,angle:s.resolved.angle,partitionHost:s.resolved}]:[]})}function Fp(e,t=()=>!0){return e.flatMap(e=>e.host&&e.partitionHost&&t(e)?[Ad(e.partitionHost)]:[])}function Ip(e,t,i,n,r,o,s=Ea,a=1e3){const l=gs(t.rooms,[...i],n,r,o,s,a);return e.flatMap(e=>{const t={x:e.rx,y:e.ry,angle:Number(e.angle)||0,length:e.rlen};return e.host&&"wall"!==e.host.kind?e.partitionHost&&Pd(e.partitionHost,l,2e-4*s)?[t]:[]:[t]})}function Ep(e,t){const i=Na,n=Ea,r=Ta,o=Number.isFinite(Number(t.cellCm))&&Number(t.cellCm)>0?Number(t.cellCm):5,s=Array.isArray(e?.walls)?e.walls:[],a=function(e,t,i=Ea,n=1e3){return Yu(t.rooms,e?.open_spans,n,.02*i)}(e,t,n,r),l=function(e,t,i=Ea){if(!t.length)return[];const n=e.filter(e=>e?.id),r=.02*i,o=[];for(let e=0;e{const t=[(e[0]+e[2])/2,(e[1]+e[3])/2];return h.some(e=>Pn(t,e)<4*r)}).map(e=>[...e]);d.length&&o.push({a:s,b:a,segs:d})}return o}(t.rooms,a,n).flatMap(e=>e.segs),c=Pp(e,t,o,n,r),h=Fp(c),d=ca(t,o,n,2e-4*n,h).all;return{space:t,walls:s,openCuts:l,openings:c,roomOpenings:Ip(c,t,s,l,i,o,n,r),partitionCuts:h,physicalBodies:d,wallKeyPitch:i,cellCm:o,gridPitch:n,coordScale:r}}function Np(e,t,i){const n="string"==typeof e?.title?e.title.trim():"",r=null==e?.id?"":String(e.id).trim();return{spaceId:r,displayName:n||r||i(t+1)}}function Hp(e,t={}){const i=(t.fingerprint||Tl)(e),n=Array.isArray(e?.spaces)?e.spaces:[],r=t.fallbackSpaceName||(e=>`Space ${e}`),o=t.prepareSpace||Ep,s=t.wallPass||Ns,a=t.floorPass||Ps,l=[];for(let i=0;i0||d.physicalBodies.length>0;if(!d.space.rooms.length&&!u){l.push({...h,status:"not-applicable"});continue}let p=null;if(u){try{p=s(d.space.rooms,d.walls,d.openCuts,d.roomOpenings,d.wallKeyPitch,d.cellCm,d.gridPitch,d.coordScale,d.physicalBodies)}catch(e){l.push({...h,status:"failed",reason:"wall-exception",detail:Ap(e)});continue}if(null==p){l.push({...h,status:"failed",reason:"wall-null"});continue}if("degraded-extra"===p.status){l.push({...h,status:"failed",reason:"wall-degraded-extra"});continue}if("failed-core"===p.status){l.push({...h,status:"failed",reason:"wall-failed-core"});continue}t.captureWallGeometry?.(d,p)}if(d.space.rooms.length&&null==p?.paperGeom){let e;try{e=a(d.space.rooms,d.walls,d.openCuts,d.wallKeyPitch,d.cellCm,d.gridPitch,d.coordScale)}catch(e){l.push({...h,status:"failed",reason:"floor-exception",detail:Ap(e)});continue}if(null==e){l.push({...h,status:"failed",reason:"floor-null"});continue}}l.push({...h,status:"ok"})}const c=l.filter(e=>"failed"===e.status);return{fingerprint:i,spaces:l,failures:c,ok:0===c.length}}function Lp(e,t){return e.catch(()=>{}).then(t)}class jp{constructor(e=50){this._undo=[],this._redo=[],this._limit=Math.max(30,Math.floor(e))}get canUndo(){return this._undo.length>0}get canRedo(){return this._redo.length>0}get undoName(){return this._undo[this._undo.length-1]?.name??null}get redoName(){return this._redo[this._redo.length-1]?.name??null}get size(){return this._undo.length}push(e){this._undo.push(e),this._undo.length>this._limit&&this._undo.splice(0,this._undo.length-this._limit),this._redo=[]}undo(){const e=this._undo.pop()??null;return e&&this._redo.push(e),e}redo(){const e=this._redo.pop()??null;return e&&this._undo.push(e),e}clear(){this._undo=[],this._redo=[]}}const qp=new WeakMap;const Bp=(e,t,i)=>255===e[i+3]&&t.every((t,n)=>Math.abs(e[i+n]-t)<=2);async function Wp(e){const t=e.defaultView;if(!t||!t.CSS?.supports?.("mix-blend-mode","screen"))return!1;const i=[128,32,16],n=[16,64,128],r=[9,19,29],o=`\n \n \n \n \n \n `,s=t.URL.createObjectURL(new Blob([o],{type:"image/svg+xml"}));try{const o=await new Promise((e,i)=>{const n=new t.Image,r=t.setTimeout(()=>i(new Error("SVG blend probe timeout")),1e3);n.onload=()=>{t.clearTimeout(r),e(n)},n.onerror=()=>{t.clearTimeout(r),i(new Error("SVG blend probe failed"))},n.src=s}),a=e.createElement("canvas");a.width=4,a.height=1;const l=a.getContext("2d",{willReadFrequently:!0});if(!l)return!1;l.drawImage(o,0,0,4,1);const c=l.getImageData(0,0,4,1).data;return Bp(c,i,0)&&Bp(c,function(e,t){return[0,1,2].map(i=>Math.round(255-(255-e[i])*(255-t[i])/255))}(i,n),4)&&Bp(c,n,8)&&Bp(c,r,12)}catch{return!1}finally{t.URL.revokeObjectURL(s)}}const Up={now:()=>performance.now(),requestFrame:e=>requestAnimationFrame(e),cancelFrame:e=>cancelAnimationFrame(e)},Gp=e=>Math.max(0,Math.min(1,e)),Vp=(e,t,i)=>e+(t-e)*i,Kp=(e,t,i)=>{const n=1-e;return 3*n*n*e*t+3*n*e*e*i+e*e*e};function Yp(e,t){return{centerX:e.x+e.w/2,centerY:e.y+e.h/2,pixelsPerUnit:t>0&&e.w>0?t/e.w:1,viewBox:{...e}}}function Zp(e,t,i){const n=Math.max(1e-9,e.pixelsPerUnit),r=Math.max(1e-9,t/n),o=Math.max(1e-9,i/n);return{x:e.centerX-r/2,y:e.centerY-o/2,w:r,h:o}}function Xp(e){const t=e.trim(),i=/^rgba?\(\s*([\d.]+)[, ]+([\d.]+)[, ]+([\d.]+)(?:\s*[,/]\s*([\d.]+))?\s*\)$/i.exec(t);if(i)return[Number(i[1]),Number(i[2]),Number(i[3]),null==i[4]?1:Number(i[4])];const n=/^#([0-9a-f]{6}|[0-9a-f]{8})$/i.exec(t)?.[1];return n?[parseInt(n.slice(0,2),16),parseInt(n.slice(2,4),16),parseInt(n.slice(4,6),16),8===n.length?parseInt(n.slice(6,8),16)/255:1]:null}function Jp(e,t,i){const n=Xp(e),r=Xp(t);if(!n||!r)return i<.5?e:t;const o=Gp(i);return`rgba(${Math.round(Vp(n[0],r[0],o))}, ${Math.round(Vp(n[1],r[1],o))}, ${Math.round(Vp(n[2],r[2],o))}, ${Vp(n[3],r[3],o).toFixed(3)})`}function Qp(e,t,i){const n=(e=>{const t=Gp(e);if(0===t||1===t)return t;let i=0,n=1,r=t;for(let e=0;e<12;e++)r=(i+n)/2,Kp(r,.2,.2){const t=this._state;if(!t||t.token!==r)return;const i=Gp((e-t.startedAt)/t.duration);t.presented=i>=1?t.to:Qp(t.from,t.to,i),t.phase=i>=1?"settling":"running",this._hooks.frame(t),i<1?this._raf=this._clock.requestFrame(s):(this._raf=0,this._hooks.settled(t),this._state?.token===r&&(this._state=null))};return this._raf=this._clock.requestFrame(s),r}cancel(e=!0){this._raf&&this._clock.cancelFrame(this._raf),this._raf=0;const t=this._state;this._state=null,this._token++,t&&e&&(t.phase="settling",t.presented=t.to,this._hooks.frame(t),this._hooks.settled(t))}dispose(){this.cancel(!1)}}class t_{constructor(e){this.options=e,this._state="idle",this._inFlight=null}get state(){return this._state}ensure(){return"ready"===this._state?Promise.resolve(!0):"failed"===this._state?Promise.resolve(!1):(this._inFlight||(this._setState("loading"),this._inFlight=this._loadWithRetry().finally(()=>{this._inFlight=null})),this._inFlight)}async _loadWithRetry(){let e=new Error("Editor runtime did not load");for(const t of[0,1])try{const e=await this.options.load(t);if(e.fingerprint!==this.options.expectedFingerprint)throw new Error(`Editor runtime fingerprint mismatch: expected ${this.options.expectedFingerprint}, got ${e.fingerprint}`);const i=e.create();return this.options.install(i),this._setState("ready"),!0}catch(t){e=t}return this._setState("failed"),this.options.failed?.(e),!1}_setState(e){e!==this._state&&(this._state=e,this.options.stateChanged?.(e))}}const i_="houseplan_card_labs_v1",n_=Object.freeze([Object.freeze({id:"iso",issue:89,since:"1.62.0",expires:"1.65.0",summary:"Volumetric plan renderer"})]);function r_(e){const t=/^(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?$/.exec(String(e||"").trim());if(!t)return null;const i=t.slice(1).map(Number);return i.every(e=>Number.isSafeInteger(e))?i:null}function o_(e,t){for(let i=0;i<3;i++)if(e[i]!==t[i])return e[i]e.split(",")).map(e=>e.trim()).filter(Boolean)}function d_(e,t,i,n=n_){const r=function(e,t=n_){const i=r_(e),n=new Map;if(!i||!a_(t))return n;for(const e of t){const t=r_(e.since),r=r_(e.expires);o_(i,t)>=0&&o_(i,r)<0&&n.set(e.id,e)}return n}(i,n),o=new Set(a_(n)?n.map(e=>e.id):[]),s=new Set(function(e){if(!e)return[];try{const t=JSON.parse(e);return Array.isArray(t)?t.filter(e=>"string"==typeof e):[]}catch{return[]}}(t).filter(e=>r.has(e)));let a=!1;const l=e=>{if("off"===e)return s.clear(),void(a=!0);const t=e.startsWith("-"),i=t?e.slice(1):e;o.has(i)&&(a=!0,t||!r.has(i)?s.delete(i):s.add(i))};for(const t of h_(new URLSearchParams(String(e.search||"").replace(/^\?/,""))))l(t);for(const t of h_(l_(e.hash)))l(t);const c=Object.freeze([...s].sort());return{active:c,space:c_(e.hash),persist:a?JSON.stringify(c):void 0,knownUrlOperation:a}}let u_={active:Object.freeze([]),space:""},p_="",__=!1,m_="";const g_=new Set;function f_(){if("undefined"==typeof window)return u_;const e=d_(window.location,function(){try{return window.localStorage.getItem(i_)}catch{return null}}(),p_);void 0!==e.persist&&function(e){try{window.localStorage.setItem(i_,e)}catch{}}(e.persist),u_={active:e.active,space:e.space},window.__hpLabs=e.active;for(const e of g_)e(u_);return u_}function v_(){f_()}function y_(e){return p_=e,"undefined"==typeof window?u_:(__||(__=!0,window.addEventListener("hashchange",v_),window.addEventListener("popstate",v_)),f_())}const b_=Object.freeze({rotDeg:0,tiltDeg:20,xyScale:1,zScale:1,origin:Object.freeze([500,500])});function w_(e){return[e.rotDeg,e.tiltDeg,e.xyScale,e.zScale,e.origin[0],e.origin[1]].every(Number.isFinite)&&Math.abs(e.xyScale)>1e-12&&Math.abs(Math.cos(e.tiltDeg*Math.PI/180))>1e-12}function k_(e,t,i=b_){if(!(w_(i)&&Number.isFinite(e[0])&&Number.isFinite(e[1])&&Number.isFinite(t)))throw new Error("invalid isometric projection input");const n=i.rotDeg*Math.PI/180,r=i.tiltDeg*Math.PI/180,o=e[0]-i.origin[0],s=e[1]-i.origin[1],a=(o*Math.cos(n)-s*Math.sin(n))*i.xyScale,l=(o*Math.sin(n)+s*Math.cos(n))*i.xyScale;return[i.origin[0]+a,i.origin[1]+l*Math.cos(r)-t*i.zScale*Math.sin(r)]}function x_(e,t=b_){if(!w_(t)||!Number.isFinite(e[0])||!Number.isFinite(e[1]))throw new Error("invalid isometric projection input");const i=t.rotDeg*Math.PI/180,n=t.tiltDeg*Math.PI/180,r=(e[0]-t.origin[0])/t.xyScale,o=(e[1]-t.origin[1])/(t.xyScale*Math.cos(n));return[t.origin[0]+r*Math.cos(i)+o*Math.sin(i),t.origin[1]-r*Math.sin(i)+o*Math.cos(i)]}function $_(e=b_){return`matrix(${function(e=b_){if(!w_(e))throw new Error("invalid isometric camera");const t=e.rotDeg*Math.PI/180,i=e.tiltDeg*Math.PI/180,n=e.xyScale*Math.cos(t),r=-e.xyScale*Math.sin(t),o=e.xyScale*Math.sin(t)*Math.cos(i),s=e.xyScale*Math.cos(t)*Math.cos(i);return[n,o,r,s,e.origin[0]-n*e.origin[0]-r*e.origin[1],e.origin[1]-o*e.origin[0]-s*e.origin[1]]}(e).map(e=>Number(e.toFixed(12))).join(" ")})`}function S_(e,t=b_){const{rect:i,wallHeight:n}=e,r=e.openingHeight??n,o=e.floorDepth??0;if(!(i.w>=0)||!(i.h>=0)||!Number.isFinite(n)||!Number.isFinite(r)||!Number.isFinite(o)||n<0||r<0||o<0)throw new Error("invalid isometric frame");const s=[[i.x,i.y],[i.x+i.w,i.y],[i.x+i.w,i.y+i.h],[i.x,i.y+i.h]],a=Math.max(n,r),l=s.flatMap(e=>[k_(e,-o,t),k_(e,a,t)]),c=l.map(e=>e[0]),h=l.map(e=>e[1]),d=Math.min(...c),u=Math.min(...h);return{x:d,y:u,w:Math.max(...c)-d,h:Math.max(...h)-u}}const M_=e=>`${Number(e[0].toFixed(4))} ${Number(e[1].toFixed(4))}`;function T_(e){let t=0;for(let i=0;iArray.isArray(e)&&Number.isFinite(e[0])&&Number.isFinite(e[1])).map(e=>[Number(e[0]),Number(e[1])]),i=t[t.length-1];return t.length>1&&t[0][0]===i[0]&&t[0][1]===i[1]&&t.pop(),t}(e);if(i.length<3)return[];const n=!t;return T_(i)>0===n?i:[...i].reverse()}function C_(e,t,i){return e.length?`M ${e.map(e=>M_(k_(e,i,t))).join(" L ")} Z`:""}function D_(e,t,i){const n=t[1]-e[1],r=-(t[0]-e[0]),o=i.rotDeg*Math.PI/180;return n*Math.sin(o)+r*Math.cos(o)}function A_(e,t,i=b_){if(!Number.isFinite(t)||t<0)throw new Error("invalid floor edge height");const n=(e||[]).map(e=>(e||[]).map((e,t)=>function(e,t){const i=R_(e,t);if(!i.length)return i;let n=0;for(let e=1;e0)).filter(e=>e.length>=3&&Math.abs(T_(e))>=1e-9)).filter(e=>e.length&&T_(e[0])>0);n.sort((e,t)=>{const i=e[0][0],n=t[0][0];return i[0]-n[0]||i[1]-n[1]||Math.abs(T_(t[0]))-Math.abs(T_(e[0]))});const r=[],o=[];let s=0;for(let e=0;ee.depth-t.depth||e.component-t.component||e.edge-t.edge),{footprintPath:r.join(" "),sides:o,componentCount:n.length,edgeCount:s}}function O_(e,t){return e^=Math.round(64*(Number.isFinite(t)?t:0)),Math.imul(e,16777619)>>>0}const z_=e=>e.every(Number.isFinite);function P_(e,t,i,n){const r=e[0]*i,o=e[1]*n,s=t*Math.PI/180;return[r*Math.cos(s)-o*Math.sin(s),r*Math.sin(s)+o*Math.cos(s)]}function F_(e,t,i,n,r,o,s){const a=e.flipH?-1:1,l="gate"===e.type?1:e.flipV?-1:1,c=(e.type,e.flipV,e.angle,e.face,{ox:0,oy:0}),h=function(e,t){return[e[0]+t[0],e[1]+t[1]]}([e.x+c.ox,e.y+c.oy],P_(i,e.angle,a,l));return{leaf:t,hinge:h,closedVector:P_(n,e.angle,a,l),quarterVector:P_([-n[1],n[0]],e.angle,a,l),turnDeg:r,bottom:o,top:s}}const I_=e=>`${Number(e[0].toFixed(4))} ${Number(e[1].toFixed(4))}`;function E_(e,t,i=b_){const n=Math.max(0,Math.min(1,Number.isFinite(t)?t:0));return e.leaves.map(t=>{const r=function(e,t){const i=e.turnDeg*t*Math.PI/180;return[e.hinge[0]+e.closedVector[0]*Math.cos(i)+e.quarterVector[0]*Math.sin(i),e.hinge[1]+e.closedVector[1]*Math.cos(i)+e.quarterVector[1]*Math.sin(i)]}(t,n),o=k_(t.hinge,t.bottom,i),s=k_(r,t.bottom,i),a=k_(r,t.top,i),l=k_(t.hinge,t.top,i);return{id:e.id,sourceIndex:e.sourceIndex,type:e.type,leaf:t.leaf,d:`M ${I_(o)} L ${I_(s)} L ${I_(a)} L ${I_(l)} Z`,shadowD:`M ${I_(k_(t.hinge,0,i))} L ${I_(k_(r,0,i))}`,depth:Math.max(o[1],s[1])}})}const N_={window:120,door:90,passage:90,gate:300};function H_(e){return N_[e]}function L_(e,t){return{type:e,lengthCm:H_(e),flipH:!1,flipV:!1,revision:t}}function j_(e,t){const i=Math.max(0,e.renderedLength)/2,n=Math.max(0,e.target.physicalHalfWidth),r=n+.18*Math.max(0,t),o=e=>({x1:e,y1:-r,x2:e,y2:r});return{rect:{x:-i,y:n?-n:0,width:2*i,height:2*n},boundaries:[o(-i),o(i)]}}function q_(e,t){const i=[e[0],e[1]],n=[t[0],t[1]];return function(e,t){return e[0]-t[0]||e[1]-t[1]}(i,n)<=0?{a:i,b:n}:{a:n,b:i}}function B_(e){const t=new Map;return e.forEach((e,i)=>{if(!e.kind||e.open)return;const n=q_(e.a,e.b);if(Math.hypot(n.b[0]-n.a[0],n.b[1]-n.a[1])<=1e-9)return;const r=function(e,t){const i=e=>(Math.abs(e)<=5e-10?0:Math.round(1e6*e)/1e6).toFixed(6);return`${i(e[0])},${i(e[1])}>${i(t[0])},${i(t[1])}`}(n.a,n.b),o=t.get(r);if(o)return o.physicalHalfWidth=Math.max(o.physicalHalfWidth,e.half||0),o.sourceOrder=Math.min(o.sourceOrder,i),void(e.partitionHost&&(o.partitionHost&&o.partitionHost.id!==e.partitionHost.id?o.ambiguousPartitionHost=!0:o.partitionHost=e.partitionHost));t.set(r,{segmentKey:r,a:n.a,b:n.b,physicalHalfWidth:Math.max(0,e.half||0),sourceOrder:i,...e.partitionHost?{partitionHost:e.partitionHost}:{}})}),[...t.values()]}function W_(e,t){if(Math.abs(e.distance-t.distance)>1e-9)return e.distance-t.distance;if(Math.abs(e.perpendicular-t.perpendicular)>1e-9)return e.perpendicular-t.perpendicular;return(e.target.segmentKeyt.target.segmentKey?1:0)||e.target.sourceOrder-t.target.sourceOrder}function U_(e){const t=B_(e.intervals).map(t=>{const i=function(e,t){const i=t.b[0]-t.a[0],n=t.b[1]-t.a[1],r=Math.hypot(i,n),o=i/r,s=n/r,a=(e[0]-t.a[0])*o+(e[1]-t.a[1])*s,l=Math.max(0,Math.min(r,a)),c=t.a[0]+l*o,h=t.a[1]+l*s;return{along:l,length:r,x:c,y:h,distance:Math.hypot(e[0]-c,e[1]-h),perpendicular:Math.abs((e[0]-t.a[0])*s-(e[1]-t.a[1])*o)}}(e.pointer,t);return{target:t,...i,envelope:Math.max(e.baseTolerance,t.physicalHalfWidth+e.bodyPointerPadding)}}).filter(e=>e.distance<=e.envelope+1e-9).filter(t=>!function(e,t,i,n,r){const o=t.b[0]-t.a[0],s=t.b[1]-t.a[1],a=Math.hypot(o,s);if(!(a>1e-9))return!1;const l=o/a,c=s/a;for(const o of i){if(!o.open&&o.kind)continue;const i=q_(o.a,o.b),s=i.b[0]-i.a[0],a=i.b[1]-i.a[1],h=Math.hypot(s,a);if(!(h>1e-9))continue;const d=s/h,u=a/h;if(Math.abs(l*u-c*d)>1e-6)continue;if(Math.abs((i.a[0]-t.a[0])*c-(i.a[1]-t.a[1])*l)>r)continue;const p=(e[0]-i.a[0])*d+(e[1]-i.a[1])*u,_=1e-9;if(!(p<=_||p>=h-_)&&Math.abs((e[0]-i.a[0])*u-(e[1]-i.a[1])*d)<=n+1e-9)return!0}return!1}(e.pointer,t.target,e.intervals,t.envelope,Math.max(1e-9,Math.min(e.baseTolerance,.04*e.gridStep)))),i=t.filter(t=>!!t.target.partitionHost&&e.renderedLength+2*t.target.physicalHalfWidth>t.length+1e-9).sort(W_),n=t.filter(t=>!t.target.partitionHost||e.renderedLength+2*t.target.physicalHalfWidth<=t.length+1e-9).sort(W_);let r=n[0];if(!r)return{candidate:null,jambBlockedTarget:i[0]?.target||null};const o=n.filter(e=>Math.abs(e.distance-r.distance)<=1e-9&&Math.abs(e.perpendicular-r.perpendicular)<=1e-9),s=o.filter(e=>e.target.partitionHost);if(s.length){if(1!==new Set(s.map(e=>e.target.partitionHost.id)).size||s.some(e=>e.target.ambiguousPartitionHost))return{candidate:null,jambBlockedTarget:null};const e=s[0],t=e.target.b[0]-e.target.a[0],i=e.target.b[1]-e.target.a[1],n=Math.hypot(t,i),a=t/n,l=i/n;if(!o.every(t=>{const i=t.target.b[0]-t.target.a[0],n=t.target.b[1]-t.target.a[1],r=Math.hypot(i,n),o=i/r,s=n/r,c=Math.abs(a*s-l*o)<=1e-6,h=Math.abs((t.target.a[0]-e.target.a[0])*l-(t.target.a[1]-e.target.a[1])*a);return c&&h<=1e-9}))return{candidate:null,jambBlockedTarget:null};r=e}if(r.target.ambiguousPartitionHost)return{candidate:null,jambBlockedTarget:null};const{target:a,length:l}=r,c=a.b[0]-a.a[0],h=a.b[1]-a.a[1],d=c/l,u=h/l,p=a.partitionHost?a.physicalHalfWidth:0,_=Math.min(Math.max(0,e.renderedLength)/2+p,l/2);let m=r.along;const g=Math.max(e.gridStep,1e-9),f=l/2;m=Math.abs(m-f)<=g/2?f:Math.round(m/g)*g,m=Math.max(_,Math.min(l-_,m));const v=a.a[0]+d*m,y=a.a[1]+u*m,b=Math.max(0,e.renderedLength)/2,w=m-b,k=m+b,x=Math.max(0,w),$=Math.max(0,l-k),S=[a.a[0]+d*(w-x/2),a.a[1]+u*(w-x/2)],M=[a.a[0]+d*(k+$/2),a.a[1]+u*(k+$/2)];let T=180*Math.atan2(h,c)/Math.PI;T>=90?T-=180:T<-90&&(T+=180);const R=Math.abs(m-f)<=1e-9;return{candidate:{presetRevision:e.preset.revision,geometryRevision:e.geometryRevision,pointer:[e.pointer[0],e.pointer[1]],type:e.preset.type,lengthCm:e.preset.lengthCm,flipH:e.preset.flipH,flipV:e.preset.flipV,x:v,y:y,angle:T,renderedLength:e.renderedLength,target:a,...a.partitionHost?{host:{...a.partitionHost,t:l>0?m/l:0}}:{},measure:{labels:[{distance:x,midpoint:S},{distance:$,midpoint:M}],guide:R?{x:v,y:y,angle:T}:null}},jambBlockedTarget:null}}function G_(e,t,i,n,r=1e-6){return e.presetRevision===i&&e.geometryRevision===n&&Math.hypot(e.pointer[0]-t[0],e.pointer[1]-t[1])<=r}function V_(e){return!(!e.canEdit||e.kiosk||e.fixedFloor)&&("view"!==e.mode&&("mouse"===e.pointerType&&e.spaceCount>1))}const K_="1.68.1",Y_="f111431804c4c42c6e35286f29720d089bc8b77ffc364a6517f7a3510fcb11c8",Z_=1500,X_=(e,t)=>{if(!e.has(t))return{hit:!1};const i=e.get(t);return e.delete(t),e.set(t,i),{hit:!0,value:i}},J_=(e,t,i,n)=>{for(e.delete(t),e.set(t,i);e.size>n;){const t=e.keys().next().value;if(void 0===t)break;e.delete(t)}},Q_=new Map;let em=0;const tm=e=>`${window.innerWidth}x${window.innerHeight}|${location.pathname}|${JSON.stringify(e??{})}`;let im=1e4;const nm="houseplan_card_layout_v1",rm="houseplan_card_cfg_v1",om="houseplan_card_zoom_v1",sm="houseplan_card_nav_v1",am="houseplan_card_view_v1",lm="hp-dialog, hp-help, hp-color-opacity, hp-device-preview",cm=1e3,hm=[[0,1],[45,.88],[70,.62],[86,.32],[100,0]],dm=(e,t)=>{const i=Math.min(e.x,t.x),n=Math.min(e.y,t.y);return{x:i,y:n,w:Math.max(e.x+e.w,t.x+t.w)-i,h:Math.max(e.y+e.h,t.y+t.h)-n}},um=new Set(["select","draw","column","merge","split","resize","opening","wallthick","delroom"]),pm=(e,t,i)=>{const n=new Event(t,{bubbles:!0,composed:!0});n.detail=i??{},e.dispatchEvent(n)},_m=e=>{history.pushState(null,"",e),pm(window,"location-changed",{replace:!1})},mm=(e,t)=>{let i,n=null;const r=(...r)=>{clearTimeout(i),n=r,i=window.setTimeout(()=>{i=void 0;const t=n;n=null,t&&e(...t)},t)};return r.flush=()=>{if(void 0===i)return;clearTimeout(i),i=void 0;const t=n;n=null,t&&e(...t)},r.cancel=()=>{clearTimeout(i),i=void 0,n=null},r.pending=()=>void 0!==i,r},gm=e=>{try{e.target?.setPointerCapture?.(e.pointerId)}catch{}};class fm extends he{constructor(){super(...arguments),this._editorRuntime=null,this._onboardingRuntime=null,this._editorRuntimeLoadingVisible=!1,this._editorModeRequest=0,this._editorRuntimeLoader=new t_({expectedFingerprint:Y_,load:async e=>{const t=0===e?await import("./houseplan-editor-runtime-DUuA4F8c.js"):await import((()=>{const e=new URL("./houseplan-editor-runtime-DUuA4F8c.js",import.meta.url);return e.searchParams.set("hp_retry",K_),e.href})());return{fingerprint:t.EDITOR_RUNTIME_FINGERPRINT,create:()=>new t.HouseplanEditorRuntime(this)}},install:e=>{this._editorRuntime=e},stateChanged:e=>this._editorRuntimeStateChanged(e),failed:e=>{console.error("[houseplan] unable to load editor runtime",e),this._showToast(`${this._t("editor.load_failed")} ${this._t("editor.refresh_advice")}`)}}),this._onboardingRuntimeLoader=new t_({expectedFingerprint:Y_,load:async e=>{const t=0===e?await import("./houseplan-onboarding-runtime-Bj55plb1.js"):await import((()=>{const e=new URL("./houseplan-onboarding-runtime-Bj55plb1.js",import.meta.url);return e.searchParams.set("hp_retry",K_),e.href})());return{fingerprint:t.ONBOARDING_RUNTIME_FINGERPRINT,create:()=>new t.HouseplanOnboardingRuntime(this)}},install:e=>{this._onboardingRuntime=e},failed:e=>{console.error("[houseplan] unable to load onboarding runtime",e),this._showToast(`${this._t("editor.load_failed")} ${this._t("editor.refresh_advice")}`)}}),this._space="f1",this._layout={},this._serverStorage=!1,this._loadOk=!1,this._serverCanWrite=null,this._loading=!1,this._loadTries=0,this._serverCfg=null,this._cfgRev=0,this._cfgContentFingerprint="",this._unsubCfg=null,this._unsubLayout=null,this._unsubVirtual=null,this._liveSyncAttempt=null,this._liveSyncGeneration=0,this._liveSyncConnection=null,this._layoutRev=0,this._layoutContentFingerprint="",this._virtualLights=Cc(null),this._canOptimizeUndo=!1,this._undoKind=null,this._devices=[],this._regSignature="",this._defPos={},this._newSyncKey="",this._tip=null,this._hoverRoom=null,this._pointerModality=new Mp(this,()=>this._syncPointerHoverTargets()),this._devicePressAnimations=new Map,this._selId=null,this._toast="",this._mode="view",this._pendingNavMode=null,this._decorTool="select",this._decorStyle={...ur},this._decorDraft=null,this._decorMove=null,this._decorSel=null,this._decorEraseConfirm=null,this._decorTextDialog=null,this._decorShapeDialog=null,this._backdropDialog=null,this._decorTextSelection={start:0,end:0},this._furnPalette=null,this._dtBox=null,this._dtDrag=null,this._bdDrag=null,this._slide="",this._reducedMotion=!1,this._onMotionChange=e=>{this._reducedMotion=e.matches,this._cancelDevicePressFeedback(),e.matches&&this._modeTransitionPreparing?this._modeTransitionForceAtomic=!0:e.matches&&this._modeTransition.active&&this._cancelModeTransition(!0),this.requestUpdate()},this._editorChromeMode="plan",this._modeTransitionVisual=null,this._modeTransitionPreparing=!1,this._modeTransitionForceAtomic=!1,this._modeTransitionRequest=0,this._modeTransitionTargetZoom=1,this._modeTransitionEditorCamera=null,this._modeTransition=new e_({frame:e=>this._applyModeTransitionFrame(e),settled:e=>this._settleModeTransition(e)}),this._editorSecondaryCopy={groupActive:(e,t)=>this._t("editor.group_active",{group:e,item:t}),openGroup:e=>this._t("editor.open_group",{group:e}),disabledAction:(e,t)=>this._t("editor.disabled_action",{action:e,reason:t})},this._tool="draw",this._geometryHistory=new jp(50),this._wallDialog=null,this._drawWallField=null,this._activeDraftId=null,this._resumeDraftBySpace={},this._physicalSel=null,this._physicalDialog=null,this._partitionDeleteDialog=null,this._roomDeleteDialog=null,this._physicalDrag=null,this._physicalRotate=null,this._physicalLastTap=null,this._physicalPickCycle=null,this._wallUnionCacheValue=null,this._wallUnionPool=new Map,this._isoGeometryCache=new Map,this._isoFallback=new Set,this._openingTunnelCache=null,this._openingWallIndexCache=new Map,this._openingPlacementIntervalsCache=null,this._openingDimensionContextCache=null,this._planSnapGeometryCache=null,this._planStructuralGeometryCache=null,this._hiddenWallDiagnosticCache=null,this._physicalBodiesCache=null,this._lightPhysicalBodiesCache=null,this._cleanFloorCache=new Map,this._innerContourCache=new Map,this._glowClipCache=new Map,this._glowGeometryWarnings=new Set,this._lightBarrierCache=null,this._lightBarrierPool=new Map,this._glowFeatherUnits=null,this._glowRenderedSources=new Map,this._glowLastAppearance=new Map,this._glowEnteringSources=new Set,this._glowEnterRafs=new Map,this._glowFadeTimers=new Map,this._glowFeatherSuspendUntil=0,this._glowFeatherResumeTimer=0,this._glowSourceSeq=0,this._glowScreenBlend=!1,this._duplicateColumnId=null,this._duplicateColumnTimer=0,this._rszLimitViolation=null,this._path=[],this._cursorPt=null,this._planSnapHover=null,this._mergeSel=null,this._openingPreset=null,this._openingRebindId=null,this._openingPresetRevision=0,this._openingHoverCandidate=null,this._openingJambBlockCm=null,this._openingDialog=null,this._openingInfo=null,this._opDrag=null,this._opMeasure=null,this._mergeDialog=null,this._splitSel=null,this._pendingSplit=null,this._wallFaceBatch=null,this._wallRepairDiagnostic=null,this._wallFaceGraphCache=[],this._areaSel="",this._nameSel="",this._roomDialog=!1,this._roomEditId=null,this._roomFill="",this._roomCustomFill=null,this._roomTempSrc="",this._roomHumSrc="",this._roomSrcOpen=null,this._roomSrcFilter="",this._roomNameScale=1,this._roomLabelScale=1,this._zoom=1,this._view=null,this._zoomBySpace={},this._viewModeSnap=null,this._pointers=new Map,this._panStart=null,this._panLock=null,this._pinchStart=null,this._suppressClick=!1,this._touchContacts=new Map,this._touchSequenceMultitouch=!1,this._touchClickBlockUntil=0,this._connectedPath="",this._routeDepartureHandled=!1,this._onLocationChanged=()=>{this._connectedPath&&location.pathname!==this._connectedPath?this._leaveCardRoute():location.pathname===this._connectedPath&&(this._routeDepartureHandled=!1)},this._touchGestureGuard={capture:!0,handleEvent:e=>this._guardTouchGesture(e)},this._hdrH=118,this._booting=!0,this._bootFading=!1,this._bootSettling=!1,this._bootSettleRaf=0,this._bootLastH=-1,this._bootStart=0,this._bootLastChange=0,this._bootSoft=!1,this._tapConfirm=null,this._onboardingShown=!1,this._rulesDialog=null,this._preflightClipboardFallback=null,this._haIntegrationVersion=null,this._alignDialog=null,this._settingsDialog=null,this._backupExportDialog=null,this._backupImportDialog=null,this._sunRaysCache=null,this._dayCycleTimer=0,this._dayCycleClockKey="",this._compassDrag=!1,this._importDialog=null,this._importQueue=[],this._importTotal=0,this._rulesCompiledSrc="",this._infoCard=null,this._nativeMoreInfoEntity=null,this._deviceInbox=null,this._deviceInboxReturn=null,this._deviceInboxMemo=null,this._markerDialog=null,this._spaceDialog=null,this._keyHandler=e=>this._onKey(e),this._warmVp=null,this._warmVpArmed=!1,this._warmLongReturn=!1,this._warmRevivePending=!1,this._warmGen=++em,this._warmKey=null,this._warmSlot=null,this._hashApplied=!1,this._navApplied=!1,this._labs={active:Object.freeze([]),space:""},this._viewPreference={},this._renderProjection="flat",this._kioskScale={icon:1,font:1},this._kioskDialog=!1,this._activityRt=new Map,this._vacRt=new Map,this._vacViewKey="",this._vacLastView=null,this._vacRaf=0,this._vacSrvTrails={},this._vacJumpOnce=!1,this._continuity=this._newContinuityController(),this._continuityHistory=[],this._continuityEpoch=0,this._continuityDataReady=!0,this._continuityPaintToken=-1,this._continuityDisposed=!1,this._renderSnapshotAt=Date.now(),this._hassSequence=0,this._visibleDeviceSnapshot=null,this._candidateDeviceSnapshot=null,this._stagedDeviceSnapshotToken=-1,this._capturedSnapshotSequence=-1,this._capturedSnapshotDevices=null,this._capturedSnapshotLayout=null,this._capturedSnapshotActivity="",this._capturedSnapshotConfigEpoch=-1,this._capturedSnapshotVirtual="",this._lastValidStageSize=null,this._pendingRefitSize=null,this._refitRaf=0,this._pageVisibility=e=>{if(this._continuity.visibility(e),this._dayCycleVisibility(e),"hidden"===e.kind)return this._clearTransientHover(!0),this._cancelDevicePressFeedback(),void(this._modeTransitionPreparing?this._modeTransitionForceAtomic=!0:this._cancelModeTransition(!0));if(this._vacJumpOnce=!0,!e.long){const e=Date.now();let t=!1;for(const i of this._activityRt.values())!i.flashKind||(i.expiresAt||i.flashTs+wp)>e||(i.flashTs=0,i.flashKind=null,i.expiresAt=0,t=!0);return void(t&&this.requestUpdate())}Date.now()-this._renderSnapshotAt>1e3&&this._continuity.note("device-snapshot-stale"),this._continuityDataReady=!1,this._continuityPaintToken=-1,this._resumeSettling=!0,this._loading?this.requestUpdate():this._loadFromServer()},this._resumeSettling=!1,this._viewportInvalidAt=0,this._vacFit=null,this._vacAllCamerasFor=null,this._vacAllCameraCache=null,this._vacCalConfirm=null,this._kioskDots=!1,this._cyclePausedUntil=0,this._swipeStart=null,this._tabDrag=null,this._tabDragRelease=null,this._tabSuppressClick=!1,this._tabOrderWarned=!1,this._lastTap=0,this._onLabsSnapshot=e=>{const t=this._effectiveProjection();this._labs=e;const i=this._effectiveProjection();this._convertProjectionView(t,i),this.requestUpdate()},this._onHashChange=()=>{if(this._hasFixedFloor)return;const e=this._hashSpace();if(e&&this._model.find(t=>t.id===e)&&e!==this._space){if(this._wallFaceBatch&&this._roomDialogCancel(),"plan"===this._mode&&"draw"===this._tool&&!this._finishWallChain())return;this._activeDraftId&&(this._resumeDraftBySpace[this._space]=this._activeDraftId),this._commitSpace(e),this._selId=null,this._path=[],this._clearPlanSnapHover(),this._clearOpeningPlacement(!0),this._tool="draw",this._activeDraftId=null,this._draftSegmentCms=[],this._closingWallCm=null,"plan"===this._mode&&"draw"===this._tool&&this._resumeLastDraft(),this._restoreZoom(),this.requestUpdate()}},this._drag=null,this._rlResize=null,this._holdFired=!1,this._retryContinuity=()=>{this._continuityDataReady=!1,this._continuityPaintToken=-1,this._continuity.retry(this._continuity.recoveryReason||"plan"),this._loading||this._loadFromServer()},this._cfgEpoch=0,this._modelCache=null,this._emptySpaceStateActive=!1,this._decorSnapCache=null,this._markerPreviewMemo=null,this._markerPreviewDevicesMemo=null,this._showHidden=!1,this._connHooked=null,this._connectionWasLost=!1,this._haRegistryConnection=null,this._haRegistryRev=-1,this._haBindingCacheKey="",this._planHassMemo=null,this._onHaRegistryUpdate=()=>{const e=Vc(this.hass);e.revision===this._haRegistryRev&&this._devices.length||(this._haRegistryRev=e.revision,this._planHassMemo=null,this._regSignature="",this._maybeRebuildDevices(),this.requestUpdate())},this._onConnReady=()=>{if(this._loadTries=0,clearTimeout(this._loadRetryTimer),this._loadRetryTimer=void 0,function(e){const t=jc(e);t&&(Wc(e,t),Uc(e,t))}(this.hass),this._saveConfigDebounced.pending()&&this._saveConfigDebounced.flush(),this._cfgWriting)return clearTimeout(this._reloadRetry),void(this._reloadRetry=window.setTimeout(this._onConnReady,400));!this._connectionWasLost&&this._continuity.hasCompleteFrame?this._beginContinuityCandidate("connection-ready",!1,"plan"):(this._continuityDataReady=!1,this._continuityPaintToken=-1),this._loading||this._loadFromServer()},this._onConnLost=()=>{this._booting&&!this._continuity.hasCompleteFrame||(this._connectionWasLost=!0,this._continuityDataReady=!1,this._continuityPaintToken=-1,this._continuity.connectionLost())},this._signer=new Dl(()=>this.requestUpdate()),this._dirtyPos=new Set,this._sentPos=new Map,this._persistLayout=mm(()=>{if(this._serverStorage){const e=[...this._dirtyPos];this._dirtyPos.clear();for(const t of e){const e=ro(this._layout[t]);e&&(this._layout={...this._layout,[t]:e},this._sentPos.set(t,e),this.hass.callWS({type:"houseplan/layout/update",device_id:t,pos:e}).then(e=>this._noteLayoutRev(e)).catch(e=>this._showToast(this._t("toast.pos_save_failed",{err:this._errText(e)}))).finally(()=>{this._sentPos.get(t)===e&&this._sentPos.delete(t)}))}this._cacheSnapshot()}else this._layout=oo(Ur(this._layout)),localStorage.setItem(nm,JSON.stringify(this._layout))},600),this._frame=null,this._showFar=!1,this._writesPending=0,this._writeChain=Promise.resolve(),this._pendingPhysicalWrites=new Map,this._undoGeometry=()=>this._editorRuntimeOrThrow()._undoGeometry(),this._redoGeometry=()=>this._editorRuntimeOrThrow()._redoGeometry(),this._saveConfigDebounced=mm(()=>{this._serverCfg&&this._writeConfig().catch(e=>{const t=!0===e?.physicalGeometryRolledBack;"geometry-unsafe"!==e?.code&&("wall_model_client_outdated"===e?.code?this._showToast(this._t("toast.wall_model_client_outdated")):"conflict"===e?.code?(this._showToast(this._t("toast.conflict")),t||(this._cancelPath(),this._reloadConfigOnly(!0))):this._showToast(this._t("toast.cfg_save_failed",{err:this._errText(e)})),t&&this._reloadRejectedPhysicalWrite())})},500),this._draftSegmentCms=[],this._closingWallCm=null,this._savePhysicalDialog=()=>this._editorRuntimeOrThrow()._savePhysicalDialog(),this._deletePhysicalSelection=()=>this._editorRuntimeOrThrow()._deletePhysicalSelection(),this._confirmPartitionDelete=()=>this._editorRuntimeOrThrow()._confirmPartitionDelete(),this._deleteDraftWhole=()=>this._editorRuntimeOrThrow()._deleteDraftWhole(),this._deleteDraftSegment=()=>this._editorRuntimeOrThrow()._deleteDraftSegment(),this._confirmRoomDelete=e=>this._editorRuntimeOrThrow()._confirmRoomDelete(e),this._rebindPartitionOpening=()=>this._editorRuntimeOrThrow()._rebindPartitionOpening(),this._keepClosedAsPartitions=()=>this._editorRuntimeOrThrow()._keepClosedAsPartitions(),this._toggleServerPlans=async()=>this._onboardingRuntime&&this._spaceDialogUsesOnboardingRuntime("create")?this._onboardingRuntime._toggleServerPlans():this._editorRuntimeOrThrow()._toggleServerPlans(),this._aspectJob=null,this._dayCycleTick=()=>{if(!this.isConnected||"hidden"===this.ownerDocument.visibilityState)return;const e=this._dayCycleState();if(!e)return this._dayCycleTimer&&(clearInterval(this._dayCycleTimer),this._dayCycleTimer=0),void(this._dayCycleClockKey="");const t=Zn(e);t!==this._dayCycleClockKey&&(this._dayCycleClockKey=t,this.requestUpdate())},this._sunShown=!1,this._sunOut=!1,this._sunOutTimer=0,this._openSettingsDialog=()=>{if(this._editorRuntime)return this._editorRuntime._openSettingsDialog();this._ensureEditorRuntime().then(e=>{e&&this._openSettingsDialog()})},this._reportedPreflightFingerprint=null,this._openAlignDialog=()=>this._editorRuntimeOrThrow()._openAlignDialog(),this._toggleOptimizeLivePositions=()=>this._editorRuntimeOrThrow()._toggleOptimizeLivePositions(),this._optimizeUndoBusy=!1,this._openBackupExport=()=>this._editorRuntimeOrThrow()._openBackupExport(),this._openRulesDialog=()=>this._editorRuntimeOrThrow()._openRulesDialog(),this._climateCache=null,this._gearPtCache=new WeakMap}_editorRuntimeOrThrow(){if(!this._editorRuntime)throw new Error("Houseplan editor runtime is not loaded");return this._editorRuntime}async _ensureEditorRuntime(){return this._editorRuntimeLoader.ensure()}async _ensureOnboardingRuntime(){return this._onboardingRuntimeLoader.ensure()}_editorRuntimeStateChanged(e){clearTimeout(this._editorRuntimeLoadingTimer),this._editorRuntimeLoadingTimer=void 0,"loading"!==e?(this._editorRuntimeLoadingVisible=!1,this.requestUpdate()):this._editorRuntimeLoadingTimer=window.setTimeout(()=>{this._editorRuntimeLoadingVisible=!0,this.requestUpdate()},150)}async _requestMode(e,t=!0){const i=++this._editorModeRequest;("view"===e||await this._ensureEditorRuntime())&&i===this._editorModeRequest&&this._setMode(e,t)}get _canEdit(){return!!this._norm&&(!0===this._serverCanWrite||!1!==this._serverCanWrite&&!0===this.hass?.user?.is_admin)}get _kiosk(){return!!this._config?.kiosk}_showKioskDots(){this._kioskDots=!0,clearTimeout(this._kioskDotsTimer),this._kioskDotsTimer=window.setTimeout(()=>this._kioskDots=!1,2500)}get _modeTransitionBusy(){return this._modeTransitionPreparing||this._modeTransition.active}_cssColor(e,t){const i=String(e||"").trim();if(!i)return t;const n=document.createElement("span");n.style.cssText=`position:absolute;visibility:hidden;color:${i}`,this.renderRoot.append(n);const r=getComputedStyle(n).color||t;return n.remove(),r}_currentModeVisual(e=this._mode){const t=this._modeTransition.presented||this._modeTransitionVisual;if(t)return{...t,viewport:{...t.viewport,viewBox:{...t.viewport.viewBox}}};const i=this._stageEl;if(!i||i.clientWidth<=0||i.clientHeight<=0)return null;const n=this.renderRoot.querySelector(".editorchrome"),r=this._viewOr(this._baseVb()),o=this.renderRoot.querySelector(".hp-paper"),s=this.renderRoot.querySelector(".hp-backdrop"),a=this.renderRoot.querySelector(".zoomwrap"),l=a?getComputedStyle(a).filter:"",c=Number(/brightness\(([^)]+)\)/.exec(l)?.[1]);return{presentedMode:e,editorChromeHeight:"view"===e?0:n?.getBoundingClientRect().height||0,stageWidth:i.clientWidth,stageHeight:i.clientHeight,viewport:Yp(r,i.clientWidth),stageColor:getComputedStyle(i).backgroundColor||"rgb(255, 255, 255)",paperColor:o?getComputedStyle(o).fill:"rgb(255, 255, 255)",sceneBrightness:Number.isFinite(c)?c:1,architectureOpacity:"decor"===e?.35:1,backdropOpacity:s&&Number(getComputedStyle(s).opacity)||1,viewWeight:"view"===e?1:0,editorWeight:"view"===e?0:1,toolbarContentOpacity:"view"===e?0:1}}_viewForModeTarget(e,t,i,n,r){const o=Ti(this._baseVb(),n/Math.max(1,r)),s=Math.min(fm.ZOOM_MAX,Math.max(fm.ZOOM_MIN,e)),a=o.w/s,l=o.h/s,c=t??o.x+o.w/2,h=i??o.y+o.h/2;return this._clampView({x:c-a/2,y:h-l/2,w:a,h:l},o)}_targetStageColor(e){return"view"!==e?"rgb(255, 255, 255)":this._cssColor(this._stageBg(this._spaceDisplayForRender()),this._cssColor("var(--ha-card-background, var(--card-background-color, #111))","rgb(17, 17, 17)"))}_targetPaperColor(e){return"view"===e&&this._spaceModel()?.bg?this._cssColor("var(--ha-card-background, var(--card-background-color, #111))","rgb(17, 17, 17)"):"rgb(255, 255, 255)"}_targetBrightness(e){return 1}_applyModeTransitionFrame(e){e.targetMode===this._mode&&(this._modeTransitionVisual=e.presented,this._view={...e.presented.viewport.viewBox},this.requestUpdate())}_settleModeTransition(e){if(e.targetMode!==this._mode)return;const t=this._modeTransitionRequest;this._view={...e.to.viewport.viewBox},this._zoom=this._modeTransitionTargetZoom,this._modeTransitionVisual=null,this._modeTransitionPreparing=!1,this._lastValidStageSize=[e.to.stageWidth,e.to.stageHeight],"view"===this._mode&&(this._saveZoom(),this._viewModeSnap=null,this._modeTransitionEditorCamera=null),this.requestUpdate(),this.updateComplete.then(()=>{if(!this.isConnected||t!==this._modeTransitionRequest||this._modeTransitionBusy||e.targetMode!==this._mode)return;const i=this._stageEl;if(i&&i.clientWidth>0&&i.clientHeight>0&&(Math.abs(i.clientWidth-e.to.stageWidth)>.5||Math.abs(i.clientHeight-e.to.stageHeight)>.5)){const e=this._view;this._lastValidStageSize=[i.clientWidth,i.clientHeight],this._applyView(this._zoom,e?e.x+e.w/2:void 0,e?e.y+e.h/2:void 0),this.requestUpdate()}const n=this.renderRoot.activeElement;(!n||!n.isConnected||!!n.closest?.(".editorchrome, .stage"))&&this.renderRoot.querySelector(".modetab.active")?.focus?.({preventScroll:!0})})}_cancelModeTransition(e=!0){const t=!!this._modeTransition.state;this._modeTransitionRequest++,this._modeTransitionPreparing=!1,this._modeTransitionForceAtomic=!1,this._modeTransition.cancel(e),e&&t||(this._modeTransitionVisual=null)}_adoptMode(e){this._cancelModeTransition(!1),this._mode=e,"view"!==e&&(this._editorChromeMode=e)}_commitViewModeAtomic(e,t,i,n){if(this._modeTransitionPreparing=!1,this._modeTransitionVisual=null,this._modeTransitionForceAtomic=!1,this._zoom=t,e){const r=this._stageEl?.clientWidth||e.stageWidth,o=Math.max(1,e.stageHeight+e.editorChromeHeight);this._view=this._viewForModeTarget(t,i,n,r,o),this._lastValidStageSize=[r,o]}else this._view=null;this._viewModeSnap=null,this._modeTransitionEditorCamera=null,this._saveZoom(),this.requestUpdate()}_prepareModeTransition(e,t,i,n,r,o){this.updateComplete.then(()=>{if(!this.isConnected||e!==this._modeTransitionRequest||this._mode!==i)return void(e===this._modeTransitionRequest&&(this._modeTransitionPreparing=!1,this._modeTransitionVisual=null,this._modeTransitionForceAtomic=!1,this.requestUpdate()));const s=this.renderRoot.querySelector(".editorchrome"),a=s?.querySelector(".editorchrome-inner"),l="view"===i?0:a?.scrollHeight||a?.getBoundingClientRect().height||0,c=Math.max(1,t.stageHeight+t.editorChromeHeight),h=Math.max(1,c-l),d=this._stageEl?.clientWidth||t.stageWidth;if(d<=0||h<=0)return this._modeTransitionPreparing=!1,this._modeTransitionVisual=null,this._applyView(n,r,o),void this.requestUpdate();const u=this._viewForModeTarget(n,r,o,d,h),p={presentedMode:i,editorChromeHeight:l,stageWidth:d,stageHeight:h,viewport:Yp(u,d),stageColor:this._targetStageColor(i),paperColor:this._targetPaperColor(i),sceneBrightness:this._targetBrightness(i),architectureOpacity:"decor"===i?.35:1,backdropOpacity:"decor"===i&&"backdrop"!==this._decorTool?.5:1,viewWeight:"view"===i?1:0,editorWeight:"view"===i?0:1,toolbarContentOpacity:"view"===i?0:1};this._modeTransitionPreparing=!1;const _=this._modeTransitionForceAtomic;this._modeTransitionForceAtomic=!1,this._modeTransition.start(t,p,i,this._reducedMotion||_?0:220)})}get _hasFixedFloor(){return!!this._config&&Object.prototype.hasOwnProperty.call(this._config,"floor")}_fixedFloorState(e=this._model,t=this._loadOk){const i=this._config?.floor,n=function(e){if(!e.hasFloor)return{kind:"absent"};const t=e.floor;if("string"==typeof t)return t.length?e.spaceIds.includes(t)?{kind:"valid",id:t,source:"id"}:{kind:"invalid",reason:"unknown-id",value:t}:{kind:"invalid",reason:"empty-id",value:t};if("number"==typeof t){if(!Number.isFinite(t))return{kind:"invalid",reason:"non-finite-index",value:t};if(!Number.isInteger(t))return{kind:"invalid",reason:"fractional-index",value:t};if(t<0)return{kind:"invalid",reason:"negative-index",value:t};const i=e.spaceIds[t];return void 0===i?{kind:"invalid",reason:"out-of-range-index",value:t}:{kind:"valid",id:i,source:"index"}}return{kind:"invalid",reason:"invalid-type",value:t}}({spaceIds:e.map(e=>e.id),hasFloor:this._hasFixedFloor,floor:i});return!this._hasFixedFloor||t?n:"number"==typeof i?"valid"===n.kind||"invalid"===n.kind&&"out-of-range-index"===n.reason?{kind:"pending",value:i}:n:"valid"===n.kind||"invalid"===n.kind&&"unknown-id"!==n.reason?n:{kind:"pending",value:i}}_canCommitSpace(e,t=!1){if(t||!this._hasFixedFloor)return!0;const i=this._fixedFloorState();return"valid"===i.kind&&i.id===e}_commitSpace(e,t=!1){return!!this._canCommitSpace(e,t)&&(e!==this._space&&(this._clearTransientHover(!0),this._cancelDevicePressFeedback()),this._space=e,!0)}_slideTo(e,t){if(e===this._space)return!0;if(!this._canCommitSpace(e))return!1;if(this._wallFaceBatch&&this._roomDialogCancel(),"plan"===this._mode&&"draw"===this._tool&&!this._finishWallChain())return!1;this._cancelModeTransition(!0);const i=window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches;return this._activeDraftId&&(this._resumeDraftBySpace[this._space]=this._activeDraftId),this._commitSpace(e),this._path=[],this._clearPlanSnapHover(),this._clearOpeningPlacement(!0),this._tool="draw",this._activeDraftId=null,this._draftSegmentCms=[],this._closingWallCm=null,this._selId=null,this._physicalSel=null,this._editorSecondary?.closeForNavigation(),this._physicalDialog=null,this._physicalDrag=null,"plan"===this._mode&&"draw"===this._tool&&this._resumeLastDraft(),this._restoreZoom(),i||(this._slide=t,clearTimeout(this._slideTimer),this._slideTimer=window.setTimeout(()=>{this._slideTimer=void 0,this._slide="",this.requestUpdate()},190),this.requestUpdate()),!0}get _canReorderTabs(){return V_({canEdit:this._canEdit,kiosk:this._kiosk,mode:this._mode,pointerType:"mouse",spaceCount:this._model.length,fixedFloor:this._hasFixedFloor})}_tabPointerDown(e,t){V_({canEdit:this._canEdit,kiosk:this._kiosk,mode:this._mode,pointerType:e.pointerType,spaceCount:this._model.length,fixedFloor:this._hasFixedFloor})&&(gm(e),this._tabDragRelease=e=>this._tabPointerUp(e),window.addEventListener("pointerup",this._tabDragRelease),window.addEventListener("pointercancel",this._tabDragRelease),this._tabDrag={id:t,pointerId:e.pointerId,x:e.clientX,y:e.clientY,moved:!1,targetId:null,placement:null})}_tabDropTargetAt(e,t,i){const n=this._model.map(e=>e.id),r=n.indexOf(i);if(r<0)return null;const o=this.renderRoot.querySelectorAll('[data-hp="space-tab"]');for(const s of o){const o=s.dataset.id||"";if(!o||o===i)continue;const a=s.getBoundingClientRect();if(ea.right||ta.bottom)continue;const l=n.indexOf(o);return l<0?null:{targetId:o,placement:l=4)))return;var i,n;const r=this._tabDropTargetAt(e.clientX,e.clientY,t.id);t.moved&&t.targetId===r?.targetId&&t.placement===r?.placement||(this._tabDrag={...t,moved:!0,targetId:r?.targetId||null,placement:r?.placement||null})}_tabPointerUp(e){const t=this._tabDrag;if(t&&t.pointerId!==e.pointerId)return;const i="pointerup"===e.type&&t?.moved?this._tabDropTargetAt(e.clientX,e.clientY,t.id):null;"pointerup"===e.type&&t?.moved&&this._suppressNextTabClick(),this._endTabDrag(),t?.moved&&i&&this._commitTabOrder(t.id,i.targetId)}_endTabDrag(){this._tabDrag=null,this._tabDragRelease&&(window.removeEventListener("pointerup",this._tabDragRelease),window.removeEventListener("pointercancel",this._tabDragRelease),this._tabDragRelease=null)}_tabClick(e){if(this._tabSuppressClick)return this._tabSuppressClick=!1,clearTimeout(this._tabSuppressClickTimer),void(this._tabSuppressClickTimer=void 0);this._pickSpace(e)}_suppressNextTabClick(){this._tabSuppressClick=!0,clearTimeout(this._tabSuppressClickTimer),this._tabSuppressClickTimer=window.setTimeout(()=>{this._tabSuppressClick=!1,this._tabSuppressClickTimer=void 0},0)}_commitTabOrder(e,t){const i=this._serverCfg;if(!i||!this._canReorderTabs)return;const n=this._model.map(e=>e.id),r=function(e,t,i){const n=e.indexOf(t),r=e.indexOf(i);if(n<0||r<0||n===r)return e;const o=e.slice();return o.splice(n,1),o.splice(r,0,t),o}(n,e,t);if(r===n)return;const o=new Map(this._devices.map(e=>[String(e.id),String(e.area||"")])),s=function(e,t,i,n=()=>""){if(!i)return[];const r=[];for(const o of e){if(!o||!0===o.removed)continue;const e="string"==typeof o.id?o.id:"";if(!e)continue;if("string"==typeof o.space&&o.space)continue;const s=("string"==typeof o.area?o.area:"")||n(e)||"";s&&t[s]||r.push({id:e,space:i})}return r}(i.markers||[],Object.fromEntries(Object.entries(this._areaToSpace).map(([e,t])=>[e,t.space])),n[0]||"",e=>o.get(e)||"");if(s.length){const e=new Map(s.map(e=>[e.id,e.space]));for(const t of i.markers||[]){const i=e.get(String(t.id));i&&(t.space=i)}}i.spaces=function(e,t){const i=new Map(t.map((e,t)=>[e,t]));return e.map((e,t)=>({space:e,index:t})).sort((e,t)=>(i.get(String(e.space?.id))??Number.MAX_SAFE_INTEGER)-(i.get(String(t.space?.id))??Number.MAX_SAFE_INTEGER)||e.index-t.index).map(e=>e.space)}(i.spaces||[],r),this._saveConfig(),this._tabOrderWarned||(this._tabOrderWarned=!0,this._showToast(this._t("toast.space_order_changed")))}_pickSpace(e){if(this._endTabDrag(),e===this._space)return;const t=this._model.map(e=>e.id),i=t.indexOf(this._space),n=t.indexOf(e);this._navApplied=!0,this._showFar=!1,this._frame=null,this._slideTo(e,i>=0&&n0&&Date.now()>=this._cyclePausedUntil&&this._model.length>1&&this._zoom<=1.001){const e=this._model.map(e=>e.id),t=e.indexOf(this._space);this._slideTo(e[(t+1)%e.length],"left"),this._showKioskDots()}}_syncCycleTimer(){clearInterval(this._cycleTimer),this._cycleTimer=void 0,this.isConnected&&!this._hasFixedFloor&&this._config?.kiosk&&Number(this._config.cycle)>0&&(this._cycleTimer=window.setInterval(()=>this._cycleTick(),1e3*Number(this._config.cycle)))}get _editing(){return"plan"===this._mode||"devices"===this._mode||"decor"===this._mode}get _markup(){return"plan"===this._mode}get _wallUnionCache(){return this._wallUnionCacheValue}set _wallUnionCache(e){this._wallUnionCacheValue=e,null===e&&this._wallUnionPool.clear()}_newContinuityController(){return new Ml(()=>{this._resumeSettling="steady"!==this._continuity.state,this._continuityEpoch++,this.isConnected&&this.requestUpdate()})}get _labsIso(){return this._labs.active.includes("iso")}get _desiredProjection(){return"view"===this._mode&&this._labsIso&&"iso"===this._viewPreference[this._space]?"iso":"flat"}_saveViewPreference(){try{localStorage.setItem(am,JSON.stringify(this._viewPreference))}catch{}}_logicalViewCenter(e){const t=this._view;if(!t)return null;const i=[t.x+t.w/2,t.y+t.h/2],n="iso"===e?x_(i):i;return{x:n[0],y:n[1]}}_convertProjectionView(e,t){if(e===t)return;const i=this._logicalViewCenter(e);this._view=null;const n=i?"iso"===t?k_([i.x,i.y],0):[i.x,i.y]:null;this._applyView(this._zoom,n?.[0],n?.[1]),this._warmPatch({vp:this._warmViewportState()}),this.requestUpdate()}_setProjection(e){if(!this._labsIso||"view"!==this._mode)return;const t=this._effectiveProjection();if(this._viewPreference={...this._viewPreference,[this._space]:e},"iso"===e){const e=this._isoSceneKey();e&&this._isoFallback.delete(e)}this._saveViewPreference();const i=this._effectiveProjection();this._convertProjectionView(t,i)}_hashSpace(){return c_(window.location.hash||"")}connectedCallback(){this._connectedPath=location.pathname,this._routeDepartureHandled=!1,window.addEventListener("location-changed",this._onLocationChanged),window.addEventListener("popstate",this._onLocationChanged),this._continuityDisposed&&(this._continuity=this._newContinuityController(),this._continuityDisposed=!1,this._continuityPaintToken=-1);const e=(t=this.ownerDocument,qp.get(t)?.resolved);var t;void 0!==e&&(this._glowScreenBlend=e),this._continuityUnsub?.(),this._continuityUnsub=$l(this.ownerDocument,this._pageVisibility),super.connectedCallback(),this._pointerModality.connect(this.ownerDocument.defaultView);const i=this.ownerDocument.defaultView?.MutationObserver;i&&(this._pointerHoverObserver=new i(e=>{for(const t of e)for(const e of t.addedNodes)this._syncPointerHoverSubtree(e)}),this._pointerHoverObserver.observe(this.renderRoot,{childList:!0,subtree:!0})),this._motionMedia=window.matchMedia?.("(prefers-reduced-motion: reduce)"),this._reducedMotion=!!this._motionMedia?.matches,this._motionMedia?.addEventListener?.("change",this._onMotionChange),function(e){const t=qp.get(e);if(t)return t.promise;const i={promise:Promise.resolve(!1)};return i.promise=Wp(e).catch(()=>!1).then(e=>(i.resolved=e,e)),qp.set(e,i),i.promise}(this.ownerDocument).then(e=>{e!==this._glowScreenBlend&&(this._glowScreenBlend=e,this.isConnected&&this.requestUpdate())}),this.hass&&this._ensureHaRegistryAuthority(),window.addEventListener("keydown",this._keyHandler),this._signer.start(()=>this.hass,()=>Sn(this._serverCfg)),this._syncCycleTimer(),window.addEventListener("hashchange",this._onHashChange),this._labsUnsub?.(),this._labsUnsub=function(e,t){g_.add(t);const i=y_(e);return"undefined"==typeof window&&t(i),()=>{g_.delete(t)}}(K_,this._onLabsSnapshot),this._booting?this._bootWatch():this._bootFading&&(clearTimeout(this._bootTimer),this._bootTimer=window.setTimeout(()=>{this._bootFading=!1},220)),this._bootSoft&&(clearTimeout(this._bootSoftTimer),this._bootSoftTimer=window.setTimeout(()=>{this._bootSoft=!1},Z_)),!this._loadOk&&this._serverCfg&&this.hass&&this._scheduleLoadRetry(),!this._warmSlot&&this._config&&this._warmAdopt(),this._loadOk&&this._ensureLiveSyncSubscriptions(),this._warmVp&&!this._warmRevivePending&&void 0===this._warmReviveTimer&&(this._warmRevivePending=!0,this._warmReviveTimer=window.setTimeout(()=>this._warmReviveDialog(),0)),this._warmLongReturn&&this._beginResumeSettle(),this._warmLongReturn=!1,this.requestUpdate()}disconnectedCallback(){this._connectedPath&&location.pathname!==this._connectedPath&&this._leaveCardRoute(),window.removeEventListener("location-changed",this._onLocationChanged),window.removeEventListener("popstate",this._onLocationChanged),this._continuityUnsub?.(),this._continuityUnsub=void 0,this._motionMedia?.removeEventListener?.("change",this._onMotionChange),this._motionMedia=void 0,this._vacRaf&&(cancelAnimationFrame(this._vacRaf),this._vacRaf=0),this._refitRaf&&(cancelAnimationFrame(this._refitRaf),this._refitRaf=0),this._dayCycleTimer&&(clearInterval(this._dayCycleTimer),this._dayCycleTimer=0),this._dayCycleClockKey="",this._bootSettleRaf&&(cancelAnimationFrame(this._bootSettleRaf),this._bootSettleRaf=0),this._bootSettling=!1;for(const e of this._activityRt.values())clearTimeout(e.timer);window.removeEventListener("keydown",this._keyHandler),this._endTabDrag(),clearTimeout(this._tabSuppressClickTimer),this._tabSuppressClickTimer=void 0,this._tabSuppressClick=!1,clearInterval(this._cycleTimer),clearTimeout(this._kioskDotsTimer),clearTimeout(this._kioskHoldTimer),clearTimeout(this._reloadRetry),clearTimeout(this._loadRetryTimer),this._loadRetryTimer=void 0,this._connHooked?.removeEventListener?.("ready",this._onConnReady),this._connHooked?.removeEventListener?.("disconnected",this._onConnLost),this._connHooked?.removeEventListener?.("reconnect-error",this._onConnLost),this._connHooked=null,this._haRegistryRelease?.(),this._haRegistryRelease=void 0,this._haRegistryConnection=null,this._signer.dispose(),clearTimeout(this._toastTimer),clearTimeout(this._slideTimer),clearTimeout(this._editorRuntimeLoadingTimer),this._editorRuntimeLoadingTimer=void 0,this._editorRuntimeLoadingVisible=!1,this._modeTransition.dispose(),this._modeTransitionVisual=null,this._modeTransitionPreparing=!1,this._modeTransitionForceAtomic=!1,this._modeTransitionRequest++,this._slideTimer=void 0,this._slide="",clearTimeout(this._bootTimer),this._bootTimer=void 0,clearTimeout(this._bootSoftTimer),this._saveConfigDebounced.flush(),window.removeEventListener("hashchange",this._onHashChange),this._labsUnsub?.(),this._labsUnsub=void 0,clearTimeout(this._holdTimer),this._roViewport?.disconnect(),this._roViewport=void 0,this._roHdr?.disconnect(),this._roHdr=void 0,this._onWinResize&&(window.removeEventListener("resize",this._onWinResize),this._onWinResize=void 0),this._unsubCfg&&(this._unsubCfg(),this._unsubCfg=null),this._unsubLayout&&(this._unsubLayout(),this._unsubLayout=null),this._unsubVirtual&&(this._unsubVirtual(),this._unsubVirtual=null),this._unsubTrail&&(this._unsubTrail(),this._unsubTrail=void 0),this._liveSyncGeneration++,this._liveSyncAttempt=null,this._liveSyncConnection=null,clearTimeout(this._layoutSyncTimer),clearTimeout(this._duplicateColumnTimer);for(const e of this._glowFadeTimers.values())clearTimeout(e);for(const e of this._glowEnterRafs.values())cancelAnimationFrame(e);clearTimeout(this._glowFeatherResumeTimer),this._glowFeatherResumeTimer=0,this._glowFeatherSuspendUntil=0,this._glowFadeTimers.clear(),this._glowEnterRafs.clear(),this._glowEnteringSources.clear(),this._glowRenderedSources.clear(),this._glowLastAppearance.clear(),this._glowSourceSeq=0,this._warmSnapshot(),this._warmRevivePending=!1,clearTimeout(this._warmReviveTimer),this._warmReviveTimer=void 0,this._warmRelease(),this._clearPlanSnapHover(),this._clearOpeningPlacement(!0),this._touchContacts.clear(),this._touchSequenceMultitouch=!1,this._touchClickBlockUntil=0,this._clearTransientHover(!0),this._cancelDevicePressFeedback(),this._pointerHoverObserver?.disconnect(),this._pointerHoverObserver=void 0,this._pointerModality.disconnect(),this._editorSecondary?.reset(),this._resumeSettling=!1,this._continuityHistory=[...this._continuityHistory,...this._continuity.trace].slice(-80),this._continuity.dispose(),this._continuityDisposed=!0,super.disconnectedCallback()}_onKey(e){if("Escape"===e.key&&this._vacFit)return this._vacFit=null,this._showToast(this._t("vac.cal_cancelled")),void e.stopPropagation();if("Escape"===e.key){if(this._tapConfirm)return void(this._tapConfirm=null);if(this._vacCalConfirm)return void(this._vacCalConfirm=null);if(this._decorEraseConfirm)return void(this._decorEraseConfirm=null);if(this._openingInfo)return void(this._openingInfo=null);if(this._infoCard)return void this._closeInfoCard();if(this._rulesDialog)return void(this._rulesDialog=null);if(this._alignDialog)return this._alignDialog=null,void(this._preflightClipboardFallback=null);if(this._backupImportDialog)return void(this._backupImportDialog=null);if(this._backupExportDialog)return void(this._backupExportDialog=null);if(this._settingsDialog)return void(this._settingsDialog=null);if(this._markerDialog)return void this._closeMarkerDialog();if(this._deviceInbox)return void(this._deviceInbox=null);if(this._openingDialog)return void(this._openingDialog=null);if(this._physicalDialog)return void(this._physicalDialog=null);if(this._backdropDialog)return void(this._backdropDialog=null);if(this._decorShapeDialog)return void(this._decorShapeDialog=null);if(this._decorTextDialog)return void(this._decorTextDialog=null);if(this._spaceDialog&&!this._roomDialog)return this._spaceDialog=null,this._importQueue=[],void(this._importTotal=0);if(this._editorSecondary?.hasOpenGroup)return e.preventDefault(),void this._editorSecondary?.closeGroup(!0)}const t=e.composedPath?.()||[e.target],i=t.some(e=>e?.matches?.('input, textarea, select, [contenteditable="true"]')),n=t.some(e=>e?.classList?.contains?.("editor-secondary")),r=e.ctrlKey||e.metaKey,o=e.key.toLowerCase(),s=/^[a-z]$/.test(o),a="z"===o||!s&&"KeyZ"===e.code,l="y"===o||!s&&"KeyY"===e.code,c=r&&(a&&e.shiftKey||l),h=r&&a&&!e.shiftKey;if("decor"===this._mode){if((h||c)&&i)return;return c?(e.preventDefault(),void this._redoGeometry()):h?(e.preventDefault(),this._decorDraft?void(this._decorDraft=null):this._decorMove||this._dtDrag||this._bdDrag?void this._cancelDecorGesture():void this._undoGeometry()):"Delete"!==e.key&&"Backspace"!==e.key||!this._decorSel||i||n?void("Escape"===e.key&&(e.preventDefault(),this._decorDraft?this._decorDraft=null:this._decorMove||this._dtDrag||this._bdDrag?this._cancelDecorGesture():"furniture"===this._decorTool?(this._furnPalette=null,this._decorTool="select"):this._decorSel?this._decorSel=null:"select"!==this._decorTool?this._decorTool="select":this._setMode("view"))):(e.preventDefault(),void this._decorDeleteSel())}if(this._markup&&(!h&&!c||!i)){if(("Delete"===e.key||"Backspace"===e.key)&&this._physicalSel&&!i&&!n)return e.preventDefault(),void this._deletePhysicalSelection();if(c)return e.preventDefault(),void this._redoGeometry();if(h)return e.preventDefault(),this._resize?.dragging?void this._rszCancelDrag():this._wallFaceBatch?(this._roomDialogCancel(),void(this._activeDraftId&&this._path.length>1?this._undoActiveDraftPoint():this._undoPoint())):"draw"===this._tool&&this._path.length?void(this._activeDraftId&&this._path.length>1?this._undoActiveDraftPoint():this._undoPoint()):"split"===this._tool&&this._splitSel?.pts?.length?(this._splitSel={...this._splitSel,pts:this._splitSel.pts.slice(0,-1)},void(this._splitSel.pts.length||(this._cursorPt=null))):void this._undoGeometry();if("Escape"===e.key)return this._physicalDrag||this._physicalRotate?(e.preventDefault(),void this._cancelPhysicalGesture()):this._roomDialog?(e.preventDefault(),void this._roomDialogCancel()):"draw"===this._tool&&this._path.length?(e.preventDefault(),void this._finishWallChain()):this._physicalSel?(e.preventDefault(),void(this._physicalSel=null)):"resize"===this._tool?(e.preventDefault(),this._resize?.dragging?void this._rszCancelDrag():("exit-tool"===this._resize?.escapeIdle()&&(this._tool="draw"),void this.requestUpdate())):"split"===this._tool?(e.preventDefault(),void(this._splitSel?.pts?.length?(this._splitSel={...this._splitSel,pts:this._splitSel.pts.slice(0,-1)},this._splitSel.pts.length||(this._cursorPt=null)):this._splitSel?this._splitSel=null:this._tool="draw")):"merge"===this._tool?(e.preventDefault(),void(this._mergeSel?this._mergeSel=null:this._tool="draw")):this._wallDialog?(e.preventDefault(),void(this._wallDialog=null)):void("opening"!==this._tool&&"wallthick"!==this._tool&&"delroom"!==this._tool&&"column"!==this._tool||(e.preventDefault(),"opening"===this._tool&&this._clearOpeningPlacement(!0),this._tool="draw"))}}_undoPoint(){if(this._path.length){if(this._contourClosed)return this._path=this._path.slice(0,-1),void(this._closingWallCm=null);if(this._activeDraftId&&this._path.length>1&&this._curSpaceCfg){const e=this._geometrySnapshot();this._path=this._path.slice(0,-1),this._draftSegmentCms=this._draftSegmentCms.slice(0,-1);const t=this._curSpaceCfg,i=(t.room_drafts||[]).findIndex(e=>e.id===this._activeDraftId);if(i>=0){const n=Array.isArray(t.room_drafts[i]?.segments)?t.room_drafts[i].segments:[];this._path.length<2?(t.room_drafts.splice(i,1),t.room_drafts.length||delete t.room_drafts,this._activeDraftId=null):t.room_drafts[i]={id:this._activeDraftId,points:this._path.map(e=>[e[0]/cm,e[1]/cm]),segments:this._draftSegmentCms.map((e,t)=>({...n[t]||{},cm:e}))},this._commitPhysicalGeometry(this._t("history.draft_segment_delete"),e)}return}this._path=this._path.slice(0,-1)}}_undoActiveDraftPoint(){const e=this._activeDraftId,t=this._path[0]?[...this._path[0]]:null,i=this._geometryHistory.undo();if(!i)return void this._undoPoint();if(!this._applyGeometryState(i.before,!0))return void this._geometryHistory.clear();const n=e?this._spaceModel()?.room_drafts.find(t=>t.id===e):null;n?(this._activeDraftId=n.id,this._path=n.points.map(e=>[...e]),this._draftSegmentCms=this._adoptDraftCms(this._path,n.segments.map(e=>e.cm),n.id),this._resumeDraftBySpace[this._space]=n.id):(this._activeDraftId=null,this._path=t?[t]:[],this._draftSegmentCms=[],e&&this._resumeDraftBySpace[this._space]===e&&delete this._resumeDraftBySpace[this._space]),this._clearPlanSnapHover(),this._showToast(this._t("history.undone",{name:i.name}))}static async getConfigElement(){return await import("./editor-BC5jpMNv.js"),document.createElement("houseplan-card-editor")}static getStubConfig(){return{type:"custom:houseplan-card"}}static _warmBootReset(e){for(const e of Q_.values())for(const t of e)clearTimeout(t.evict);Q_.clear(),im=e&&e>0?e:1e4}static _warmBootStats(){let e=0,t=0;const i=[];for(const n of Q_.values())for(const r of n)e++,r.dlg&&(t++,i.push(r.dlg.kind));return{keys:Q_.size,slots:e,dlgs:t,drafts:i}}setConfig(e){const t=this._config,i=!!t&&Object.prototype.hasOwnProperty.call(t,"floor");this._config={icon_size:2.5,show_temperature:!0,live_states:!0,show_signal:!0,...e};(i!==this._hasFixedFloor||t?.floor!==this._config.floor)&&(this._hashApplied=!1,this._navApplied=!1,this._warmVpArmed=!1),this._config.kiosk&&(this._booting=!1,this._bootFading=!1),!this._hasFixedFloor&&e.default_floor&&this._commitSpace(e.default_floor,!0);try{this._zoomBySpace=JSON.parse(localStorage.getItem(om)||"{}")||{}}catch{this._zoomBySpace={}}try{const e=JSON.parse(localStorage.getItem(am)||"{}")||{};this._viewPreference=Object.fromEntries(Object.entries(e).filter(e=>"flat"===e[1]||"iso"===e[1]))}catch{this._viewPreference={}}this._labs=y_(K_);try{const e=JSON.parse(localStorage.getItem("houseplan_card_kiosk_v1")||"null");this._kioskScale={icon:Dn(e?.icon),font:Dn(e?.font)}}catch{}try{const e=JSON.parse(localStorage.getItem(rm)||"null");e&&e.config&&Array.isArray(e.config.spaces)&&(this._serverCfg=e.config,this._cfgEpoch++,this._cfgRev=e.rev||0,this._cfgContentFingerprint=e.config_fingerprint||Tl(e.config),this._layout=e.layout||{},this._layoutRev=e.layout_rev||0,this._layoutContentFingerprint=e.layout_fingerprint||Tl(this._layout),this._virtualLights=Cc(e.virtual_lights,this._cfgRev),this._serverStorage=!0)}catch{}this._adoptInitialSpace(this._model,this._loadOk),"view"!==this._mode||this._view||(this._zoom=this._zoomBySpace[this._space]||1),this.isConnected&&(this._syncCycleTimer(),this._warmAdopt(),this._warmLongReturn&&this._beginResumeSettle(),this._warmLongReturn=!1)}_warmAdopt(){if(this._config?.kiosk)return;const e=tm(this._config);if(this._warmKey===e&&this._warmSlot)return;this._warmSlot&&this._warmRelease();const t=this.parentNode,i=this._warmIdx(t),n=Q_.get(e);if(!n||!n.length)return;const r=n.find(e=>e.owner===this._warmGen);if(r)return this._warmLongReturn=!!r.freed&&Date.now()-r.freed>=wl,clearTimeout(r.evict),r.evict=0,r.freed=0,r.live=!0,this._warmSlot=r,this._warmKey=e,r.frameFingerprint&&this._continuity.adoptCompleteFrame(r.frameFingerprint),void(!this._devices.length&&r.devices?.length&&(this._devices=[...r.devices]));const{slot:o,sure:s}=((e,t,i,n)=>{const r=e=>{const t=!!i&&e.place?.deref()===i;return t&&e.idx===n?4:e.live?0:t?3:2};let o=null,s=0,a=0,l=null;for(const i of e){if(i.owner===t)continue;l=i;const e=r(i);e<=0||(e>s?(o=i,s=e,a=1):e===s&&a++)}return!o||a>1?{slot:o||l,sure:!1}:{slot:o,sure:!0}})(n,this._warmGen,t,i);o&&(this._warmLongReturn=!!o.freed&&Date.now()-o.freed>=wl,this._booting=!1,this._bootFading=!1,this._hdrH=o.hdrH,this._bootSoft=!0,this.isConnected&&(clearTimeout(this._bootSoftTimer),this._bootSoftTimer=window.setTimeout(()=>{this._bootSoft=!1},Z_)),this._warmKey=e,s?(clearTimeout(o.evict),o.evict=0,o.owner=this._warmGen,o.place=t?new WeakRef(t):null,o.idx=i,o.live=!0,this._warmSlot=o,this._warmVp=o.vp,o.frameFingerprint&&this._continuity.adoptCompleteFrame(o.frameFingerprint),!this._devices.length&&o.devices?.length&&(this._devices=[...o.devices]),this._warmAdoptViewport(this._config)):(this._warmSlot={owner:this._warmGen,path:location.pathname,place:t?new WeakRef(t):null,idx:i,live:!0,hdrH:o.hdrH,stageH:o.stageH,vp:null,frameFingerprint:"",devices:null,dlg:null,freed:0,evict:0},n.push(this._warmSlot),this._warmTrim(n)))}_warmIdx(e){const t=e?.children;if(!t)return-1;for(let e=0;e4;){const t=e.findIndex(e=>!e.live);if(t<0)break;clearTimeout(e[t].evict),e.splice(t,1)}}_warmScheduleEvict(e,t){if(clearTimeout(e.evict),!(e.dlg||e.vp&&"view"!==e.vp.mode))return;const i=e.freed,n=e.owner;e.evict=window.setTimeout(()=>{if(e.evict=0,e.freed!==i||e.owner!==n)return;var r;e.dlg=null,e.vp=(r=e.vp)&&"view"!==r.mode?{...r,mode:"view",zoom:r.snap?.space===r.space?r.snap.zoom:r.zoom,view:null,snap:null,tool:"draw",decorTool:"select",showHidden:!1,selId:null,rszSel:null,decorSel:null}:r,e.frameFingerprint="";const o=Q_.get(t);if(!e.live&&o&&o.length>1){const t=o.indexOf(e);t>=0&&o.splice(t,1)}},im+250)}_warmAdoptViewport(e){const t=this._warmVp;if(!t||this._warmSlot?.path!==location.pathname)return void(this._warmVp=null);const i=this._fixedFloorState();if(this._hashApplied||!this._model.find(e=>e.id===t.space)||"valid"===i.kind&&i.id!==t.space||this._hasFixedFloor&&"valid"!==i.kind)return void(this._warmVp=null);this._commitSpace(t.space,!0),this._navApplied=!0,this._adoptMode("view"!==t.mode&&this._canEdit&&!e.kiosk?t.mode:"view"),this._pendingNavMode="view"===t.mode||this._canEdit||e.kiosk?null:t.mode,this._zoom=t.zoom;const n=this._effectiveProjection(),r=n===t.projection&&this._labsIso===t.activeLabsIso;if(this._view=r&&t.view?{...t.view}:null,this._viewModeSnap=r&&t.snap?{...t.snap}:null,!r&&t.logicalCenter){const e="iso"===n?k_([t.logicalCenter.x,t.logicalCenter.y],0):[t.logicalCenter.x,t.logicalCenter.y];this._applyView(t.zoom,e[0],e[1])}var o;this._tool="opening"===(o=function(e){return"partition"===e?"draw":e}(o=t.tool))?"draw":"string"==typeof o&&um.has(o)?o:"draw",this._decorTool=t.decorTool,this._showHidden=t.showHidden,this._showFar!==t.showFar&&(this._showFar=t.showFar,this._frame=null),this._selId=t.selId,this._resize?.restoreSelection(t.rszSel),this._decorSel=t.decorSel,this._warmVpArmed=!0}_warmPatch(e,t=!1){if(this._config?.kiosk)return;const i=tm(this._config);if(!this._warmSlot||this._warmKey===i){if(!this._warmSlot){if(!t)return;const e=this.parentNode;this._warmKey=i,this._warmSlot={owner:this._warmGen,path:location.pathname,place:e?new WeakRef(e):null,idx:this._warmIdx(e),live:!0,hdrH:this._hdrH,stageH:0,vp:null,frameFingerprint:"",devices:null,dlg:null,freed:0,evict:0};const n=Q_.get(i)||[];for(n.push(this._warmSlot),Q_.set(i,n),this._warmTrim(n);Q_.size>8;){const e=Q_.keys().next().value;if(void 0===e||e===i)break;for(const t of Q_.get(e)||[])clearTimeout(t.evict);Q_.delete(e)}}Object.assign(this._warmSlot,e)}}_warmViewportState(){const e=this._effectiveProjection();return{space:this._space,mode:this._mode,projection:e,activeLabsIso:this._labsIso,logicalCenter:this._logicalViewCenter(e),zoom:this._zoom,view:this._view?{...this._view}:null,snap:this._viewModeSnap?{...this._viewModeSnap}:null,tool:this._tool,decorTool:this._decorTool,showHidden:this._showHidden,showFar:this._showFar,selId:this._selId,rszSel:this._resize?.selectedRoomId,decorSel:this._decorSel}}_warmDialogState(){const e=(e,t)=>({kind:e,space:this._space,mode:this._mode,data:t});return this._tapConfirm||this._alignDialog||this._mergeDialog||this._importDialog||this._backupExportDialog||this._backupImportDialog?null:this._openingInfo?e("openingInfo",this._openingInfo.id):this._infoCard?e("info",this._infoCard.id):this._rulesDialog?this._rulesDialog.busy?null:e("rules",this._rulesDialog):this._settingsDialog?this._settingsDialog.busy?null:e("settings",this._settingsDialog):this._markerDialog?this._markerDialog.busy?null:e("marker",this._markerDialog):this._openingDialog?e("opening",this._openingDialog):this._backdropDialog?e("backdrop",this._backdropDialog):this._decorShapeDialog?e("decorShape",this._decorShapeDialog):this._decorTextDialog?e("decorText",this._decorTextDialog):this._roomDialog?e("room",{editId:this._roomEditId,fill:this._roomFill,customFill:this._roomCustomFill,tempSrc:this._roomTempSrc,humSrc:this._roomHumSrc,srcOpen:this._roomSrcOpen,srcFilter:this._roomSrcFilter,nameScale:this._roomNameScale,labelScale:this._roomLabelScale,areaSel:this._areaSel,nameSel:this._nameSel,pendingSplit:this._pendingSplit,wallFaceBatch:this._wallFaceBatch,path:this._path}):this._spaceDialog?this._spaceDialog.busy?null:e("space",this._spaceDialog):null}_warmSnapshot(){if(this._booting||this._config?.kiosk||"steady"!==this._continuity.state)return;const e={vp:this._warmViewportState(),frameFingerprint:this._continuity.frameFingerprint,devices:this._devices};if(this._warmRevivePending||(e.dlg=this._warmDialogState()),this.isConnected&&this._warmSlot?.owner===this._warmGen){const t=this.parentNode;e.place=t?new WeakRef(t):null,e.idx=this._warmIdx(t)}this._warmPatch(e)}_warmReviveDialog(){this._warmRevivePending=!1;const e=this._warmSlot;if(this._warmReviveTimer=void 0,!e||!e.dlg)return;const t=e.dlg,i=e.freed;if(e.dlg=null,e.freed=0,clearTimeout(e.evict),e.evict=0,i&&!(Date.now()-i>im)&&t.space===this._space&&t.mode===this._mode){switch(t.kind){case"space":this._spaceDialog={...t.data,busy:!1,savedBusy:!1};break;case"marker":this._markerDialog={...t.data,busy:!1};break;case"settings":this._settingsDialog={...t.data,busy:!1};break;case"rules":this._rulesDialog={...t.data,busy:!1};break;case"opening":this._openingDialog={...t.data};break;case"backdrop":this._backdropDialog={...t.data};break;case"decorShape":this._decorShapeDialog={...t.data};break;case"decorText":{this._decorTextDialog={...t.data};const e=String(this._decorTextDialog?.text??"").length;this._decorTextSelection={start:e,end:e};break}case"room":{const e=t.data;this._roomEditId=e.editId,this._roomFill=e.fill,this._roomCustomFill=e.customFill||null,this._roomTempSrc=e.tempSrc,this._roomHumSrc=e.humSrc,this._roomSrcOpen=e.srcOpen,this._roomSrcFilter=e.srcFilter,this._roomNameScale=e.nameScale,this._roomLabelScale=e.labelScale,this._areaSel=e.areaSel,this._nameSel=e.nameSel,this._pendingSplit=e.pendingSplit,this._wallFaceBatch=e.wallFaceBatch||null,this._path=e.path,this._wallFaceBatch&&(this._activeDraftId=this._wallFaceBatch.activeDraftId,this._draftSegmentCms=[...this._wallFaceBatch.activeCms]),this._roomDialog=!0;break}case"info":{const e=this._devices.find(e=>e.id===t.data);e&&(this._infoCard=e);break}case"openingInfo":{const e=(this._curSpaceCfg?.openings||[]).find(e=>e.id===t.data);e&&(this._openingInfo=e);break}}this.requestUpdate()}}_cacheSnapshot(){if(this._serverCfg)try{this._cfgContentFingerprint=Tl(this._serverCfg),this._layoutContentFingerprint=Tl(this._layout),this._virtualLights=function(e,t,i){if(e.configRev===i)return e;const n=new Set((Array.isArray(t?.markers)?t.markers:[]).filter(zc).map(e=>e.id));return{...e,configRev:i,off:new Set([...e.off].filter(e=>n.has(e)))}}(this._virtualLights,this._serverCfg,this._cfgRev),localStorage.setItem(rm,JSON.stringify({config:this._serverCfg,rev:this._cfgRev,config_fingerprint:this._cfgContentFingerprint,layout:this._layout,layout_rev:this._layoutRev,layout_fingerprint:this._layoutContentFingerprint,virtual_lights:Ac(this._virtualLights)}))}catch{}}_beginContinuityCandidate(e,t,i="plan"){return this._booting&&!this._continuity.hasCompleteFrame?this._continuity.token:(this._continuityDataReady=t,this._continuityPaintToken=-1,this._stagedDeviceSnapshotToken=-1,this._resumeSettling=!0,this._continuity.beginCandidate(e,i))}_continuityStageValid(){const e=this._stageEl;return!!e&&e.clientWidth>0&&e.clientHeight>0}_continuityAssetsReady(){if(!this._model.length)return!1;const e=this._model.length?this._spaceModel():null;return!e?.bg?.href||this._signer.isReady(this.hass,e.bg.href)}_initialSpaceSelection(e,t=this._loadOk){const i=this._fixedFloorState(e,t);return"valid"===i.kind?{id:i.id,source:"fixed"}:this._hasFixedFloor?{id:null,source:"none"}:function(e){const t=new Set(e.spaceIds.filter(e=>!!e));if(!t.size)return{id:null,source:"none"};const i=[["hash",!1===e.acceptHash?null:e.hashSpace],["current",e.preserveCurrent?e.currentSpace:null],["saved",e.savedSpace],["default",e.defaultSpace],["first",e.spaceIds[0]]];for(const[e,n]of i)if(n&&t.has(n))return{id:n,source:e};return{id:null,source:"none"}}({spaceIds:e.map(e=>e.id),hashSpace:this._hashSpace(),acceptHash:!this._hashApplied,currentSpace:this._space,preserveCurrent:this._hashApplied||this._navApplied||this._warmVpArmed,savedSpace:this._savedNav()?.space,defaultSpace:this._config?.default_floor})}_adoptInitialSpace(e,t=this._loadOk){const i=this._initialSpaceSelection(e,t);return i.id?(this._commitSpace(i.id,!0),"hash"===i.source&&(this._hashApplied=!0),"saved"===i.source&&(this._navApplied=!0),i):i}_candidateBackdrop(e,t=this._space){const i=za(e),n=this._fixedFloorState(i,!0);if(this._hasFixedFloor&&"valid"!==n.kind)return"";const r=this._initialSpaceSelection(i,!0).id||(i.some(e=>e.id===t)?t:i[0]?.id);return i.find(e=>e.id===r)?.bg?.href||""}_visualFrameFingerprint(){const e=this._stageEl,t=e?[e.clientWidth,e.clientHeight]:[0,0];return Rl([this._cfgRev,this._cfgContentFingerprint||Tl(this._serverCfg),this._layoutRev,this._layoutContentFingerprint||Tl(this._layout),this._space,this._mode,this._view,t,this._glowScreenBlend?"screen":"normal",this.hass?.themes?.darkMode??this.hass?.themes?.default_theme??""])}_settleContinuityFrame(){if(this._booting||!this._continuityStageValid())return;if(!this._continuity.hasCompleteFrame&&"steady"===this._continuity.state)return void(this._continuityAssetsReady()&&(this._renderSnapshotAt=Date.now(),this._continuity.markCompleteFrame(this._visualFrameFingerprint())));if(!this._continuityDataReady)return;if(!["holding","offline-stale","overlay-pending","overlay-visible","candidate-ready"].includes(this._continuity.state))return;const e=this._continuity.token;if(this._candidateDeviceSnapshot&&this._candidateDeviceSnapshot!==this._visibleDeviceSnapshot&&this._stagedDeviceSnapshotToken!==e)return this._stagedDeviceSnapshotToken=e,void this.requestUpdate();this._continuityPaintToken!==e&&(this._continuityPaintToken=e,this._continuity.candidateReady(e)&&this._continuity.commitAfterPaint(e,{updateComplete:()=>this.updateComplete,stageValid:()=>this.isConnected&&this._continuityStageValid(),assetsReady:()=>this._continuityAssetsReady(),frameFingerprint:()=>this._visualFrameFingerprint()}).then(t=>{t&&e===this._continuity.token?(this._resumeSettling=!1,this._renderSnapshotAt=Date.now(),this._candidateDeviceSnapshot&&(this._visibleDeviceSnapshot=this._candidateDeviceSnapshot),this._candidateDeviceSnapshot=null,this._stagedDeviceSnapshotToken=-1,this._warmSnapshot()):e===this._continuity.token&&(this._continuityPaintToken=-1,this._stagedDeviceSnapshotToken=-1,this._candidateDeviceSnapshot=null,this.requestUpdate())}))}_onBackdropLoaded(e,t){this._signer.markLoaded(this.hass,e,t),this._continuity.note("asset-ready"),this._continuityPaintToken=-1,"steady"!==this._continuity.state&&this.requestUpdate()}_renderRecoveryOverlay(){if(!this._continuity.overlayVisible&&"recovery-error"!==this._continuity.state)return G;const e="connection"===this._continuity.recoveryReason;return B`
e.stopPropagation()} + @click=${e=>e.stopPropagation()} + @wheel=${e=>e.stopPropagation()}> + + ${this._t(e?"continuity.restore_connection":"continuity.restore_plan")} + ${"recovery-error"===this._continuity.state?B``:G} +
`}_renderEditorRuntimeLoading(){return this._editorRuntimeLoadingVisible?B`
+ + ${this._t("editor.loading")} +
`:G}houseplanContinuityTrace(){return[...this._continuityHistory,...this._continuity.trace].slice(-80).map(e=>({...e,...e.stage?{stage:[...e.stage]}:{}}))}getCardSize(){return 12}get _norm(){return!(!this._serverCfg||!this._serverCfg.spaces.length)}_cfgFingerprint(){const e=this._serverCfg?.spaces||[];let t=e.length+":";for(const i of e){t+=(i.id||"")+","+(i.plan_aspect||"")+","+(i.plan_url||"").length+","+(i.plan_x??"")+","+(i.plan_y??"")+","+(i.plan_scale??"")+","+(i.plan_scale_x??"")+","+(i.plan_scale_y??"")+","+(i.plan_angle??"")+","+(i.rooms?.length||0)+","+(i.openings?.length||0)+","+(i.decor?.length||0)+";";for(const e of i.rooms||[]){const i=e.poly?.[0],n=e.poly?.[e.poly.length-1];t+=(e.poly?.length||0)+"."+(e.id||"")+"."+(e.open_to||[]).join("+")+"."+(e.area||"")+"."+JSON.stringify(e.settings||0)+"."+(e.x??"")+","+(e.y??"")+","+(e.w??"")+","+(e.h??"")+","+(i?i[0]+"/"+i[1]:"")+","+(n?n[0]+"/"+n[1]:"")+";"}}return t}get _model(){if(!this._serverCfg)return[];const e=this._cfgEpoch+"|"+this._cfgFingerprint();if(this._modelCache&&this._modelCache.key===e)return this._modelCache.model;const t=this._buildModel();return this._modelCache={key:e,model:t},t}_buildModel(){if(!this._serverCfg)return[];const e=this._renderCfg;return za(e).map((t,i)=>{const n=e.spaces[i]?.plan_url;return t.bg&&n?{...t,bg:{...t.bg,href:n}}:t})}_spaceModel(){return this._hasFixedFloor?Al(this._model,this._space):(e=this._model,t=this._space,e.find(e=>e.id===t)??e[0]);var e,t}_spaceModelById(e){return Al(this._model,e)}_syncEmptySpaceState(){if(!!this._serverCfg&&0===this._serverCfg.spaces.length){if(!this._emptySpaceStateActive){this._emptySpaceStateActive=!0;for(const e of this._pointers.keys())for(const t of this.renderRoot.querySelectorAll("*"))try{t.hasPointerCapture?.(e)&&t.releasePointerCapture(e)}catch{}this._pointers.clear(),this._panStart=null,this._panLock=null,this._pinchStart=null,this._swipeStart=null,this._drag=null,this._rlResize=null,this._vacFit=null,this._compassDrag=!1,this._cancelModeTransition(!1),this._mode="view",this._clearGeometryGesture(),this._geometryHistory.clear(),this._resumeDraftBySpace={},this._tip=null,this._hoverRoom=null,this._openingInfo=null,this._closeInfoCard(),this._deviceInbox=null,this._deviceInboxReturn=null,this._markerDialog=null,this._physicalDialog=null,this._backdropDialog=null,this._decorShapeDialog=null,this._decorTextDialog=null,this._roomDialog=!1,"edit"===this._spaceDialog?.mode&&(this._spaceDialog=null),this._editorSecondary?.closeForNavigation(),this._saveConfigDebounced.cancel(),this._frame=null,this._planSnapGeometryCache=null,this._hiddenWallDiagnosticCache=null,this._decorSnapCache=null,this._commitSpace("",!0)}}else this._emptySpaceStateActive=!1}get _areaToSpace(){const e={};for(const t of this._model)for(const i of t.rooms)i.area&&(e[i.area]={space:t.id,room:i});return e}get _settings(){return this._serverCfg?.settings||{}}get _showAll(){return this._showHidden||!this._settings.filter_seeded&&!!this._settings.show_all}_seedHiddenDevices(){if(!this._serverCfg||!this._norm||!this._canEdit)return;const e=this._serverCfg,t=function(e){const t=e.hass,i=e.registry||Vc(t),n=Xc(t,i),r=Jc(t,i),{areaToSpace:o,markers:s,settings:a,excluded:l,iconRules:c}=e,h=!1!==a.group_lights,d=Lh(s),u=Ph(n,h).filter(e=>!jh(n,e.eid,d)),p=new Set(u.map(e=>e.area)),_=ih(n),m=nh(s,r),g=new Set(s.map(e=>e.binding)),f=[];for(const e of Object.values(n.devices)){const r=e.area_id;if(!r||!o[r])continue;if("service"===e.entry_type)continue;if(g.has("device:"+e.id))continue;if("active"!==th(t,"device:"+e.id,i).kind)continue;const s=rh(n,e.id,_[e.id]||[],m);if(s.partial&&!s.entityIds.length)continue;const a=s.entityIds,d=oh(n,e,a);let u=l.has(d)||"Group"===e.model||/scene/i.test(e.model||"")||/bridge/i.test((e.model||"")+(e.name||""))||"myheat"===d&&!!e.via_device_id;if(!u&&h&&p.has(r)){const t=(e.name_by_user||e.name||"").trim();"mdi:lightbulb"===Fh(n,t,e.model,a,c)&&(u=!0)}u&&f.push("device:"+e.id)}return f}({hass:this.hass,registry:this._haRegistry,areaToSpace:Object.fromEntries(Object.entries(this._areaToSpace).map(([e,t])=>[e,t.space])),markers:this._markers,settings:this._settings,excluded:this._excluded,firstSpaceId:this._model[0]?.id||"",iconRules:this._iconRules});if(!t.length&&e.settings?.filter_seeded)return;e.markers=e.markers||[];const i=[];for(const n of t){const t="h"+n.slice(n.indexOf(":")+1);e.markers.push({id:t,binding:n,hidden:!0}),i.push(n.slice(n.indexOf(":")+1))}const n={...e.settings||{},filter_seeded:!0};delete n.show_all,i.length&&Array.isArray(n.new_device_ids)&&(n.new_device_ids=n.new_device_ids.filter(e=>!i.includes(e))),e.settings=n,this._regSignature="",this._maybeRebuildDevices(),this._saveConfig(),this.requestUpdate()}get _iconRules(){const e=this._settings.icon_rules;if(!e||!Array.isArray(e)||!e.length)return;const t=JSON.stringify(e);return t!==this._rulesCompiledSrc&&(this._rulesCompiledSrc=t,this._rulesCompiled=De(e)),this._rulesCompiled}get _fillColors(){return on(this._settings)}get _excluded(){const e=this._settings.exclude_integrations;return e?new Set(e):Re}willUpdate(e){e.has("_serverCfg")&&this._cfgEpoch++,this._syncEmptySpaceState(),e.has("hass")&&this.hass&&(this._hassSequence++,this._renderSnapshotAt=Date.now(),this._continuity.note("hass-snapshot"),this._ensureHaRegistryAuthority(),this._planHassMemo=null,this._hookConnection(),!this._loadOk&&!this._loading&&this._loadTries<8&&this._loadFromServer(),this._maybeRebuildDevices(),this._vacTick(),this._activityTick()),this._continuity.hasCompleteFrame&&"steady"===this._continuity.state&&this._continuity.refreshCompleteFrame(this._visualFrameFingerprint()),this._captureRenderDeviceSnapshot()}updated(){this._pruneDevicePressFeedback(),this._syncDayCycleClock(),this._warmSnapshot(),this._editorRuntime&&this._dtMeasure();const e=this._stageEl;e&&!this._roViewport&&(this._roViewport=new ResizeObserver(()=>this._refitView()),this._roViewport.observe(e)),e&&this._booting&&!this._bootTimer&&this._bootWatch();const t=this.renderRoot.querySelector(".hdr");if(t&&e&&!this._roHdr){const i=()=>{const t=this.renderRoot.querySelector("ha-card");if(!t)return;const i=e.getBoundingClientRect().top-t.getBoundingClientRect().top,n=Math.min(Math.max(t.getBoundingClientRect().top,0),120),r=Math.round(i+n);r>=0&&r!==this._hdrH&&(this._hdrH=r),r>=0&&!this._booting&&!this._config?.kiosk&&e.clientHeight>0&&this._warmPatch({hdrH:r,stageH:e.clientHeight})};this._roHdr=new ResizeObserver(()=>requestAnimationFrame(i)),this._roHdr.observe(t),this._onWinResize=()=>requestAnimationFrame(i),window.addEventListener("resize",this._onWinResize),i()}if(e&&!this._view&&this._refitView(),this._editorSecondary?.afterRender(),this._settleContinuityFrame(),this._serverStorage&&this._loadOk&&0===this._model.length&&!this._spaceDialog&&!this._importDialog&&!this._onboardingShown){this._onboardingShown=!0;const e=function(e){const t=e?.floors;if(!t||"object"!=typeof t)return[];const i=[];for(const e of Object.values(t))e&&e.floor_id&&i.push({id:e.floor_id,name:e.name||e.floor_id,level:e.level??null});return i.sort((e,t)=>{const i=e.level??1e9,n=t.level??1e9;return i!==n?i-n:e.name.localeCompare(t.name)}),i}(this.hass);e.length?this._importDialog={floors:e.map(e=>({...e,checked:!0}))}:this._openSpaceDialog("create")}}_adoptStructuralResponses(e,t,i){const n=e?.config,r=n&&Array.isArray(n.spaces)?n:null,o=Tl(r),s=o!==(this._cfgContentFingerprint||Tl(this._serverCfg));if(s&&(this._geometryHistory.clear(),this._pendingPhysicalWrites.clear(),this._serverCfg&&this._clearGeometryGesture(),this._serverCfg=r,this._cfgContentFingerprint=o),this._cfgRev=e?.rev??this._cfgRev,e&&("virtual_lights"in e||"config"in e)){const t=Dc(this._virtualLights,e.virtual_lights,this._cfgRev,"virtual_lights"in e);Oc(t)!==Oc(this._virtualLights)&&(this._virtualLights=t,this._capturedSnapshotVirtual="")}let a=!1;if(void 0!==t||void 0!==i){const e=i??t?.layout??{},n=Tl(e);a=n!==(this._layoutContentFingerprint||Tl(this._layout)),a&&(this._layout=e,this._layoutContentFingerprint=n),this._layoutRev=t?.rev??this._layoutRev}return this._canOptimizeUndo=!(!e?.can_optimize_undo&&!t?.can_optimize_undo),this._haIntegrationVersion="string"==typeof e?.integration_version?e.integration_version:this._haIntegrationVersion,this._undoKind=e?.undo_kind||t?.undo_kind||null,"boolean"==typeof e?.can_write&&(this._serverCanWrite=e.can_write),s&&this._continuity.note("config-candidate",{configRev:this._cfgRev}),a&&this._continuity.note("layout-candidate",{layoutRev:this._layoutRev}),{configChanged:s,layoutChanged:a}}_resumePendingNavMode(){if(!this._pendingNavMode||!this._canEdit||this._config?.kiosk)return!1;const e=this._pendingNavMode;return this._pendingNavMode=null,this._setMode(e,!1),!0}async _loadFromServer(){this._loading=!0,this._loadTries++;const e=this._space,t=!!this._view;try{const[i,n]=await Promise.all([this.hass.callWS({type:"houseplan/config/get"}),this.hass.callWS({type:"houseplan/layout/get"})]),r=i?.config&&Array.isArray(i.config.spaces)?i.config:null,o=Tl(r)!==(this._cfgContentFingerprint||Tl(this._serverCfg))||Tl(n?.layout??{})!==(this._layoutContentFingerprint||Tl(this._layout));if(o){if(!await this._signer.prepareImage(this.hass,this._candidateBackdrop(r)))return this._continuity.note("asset-failed"),void this._scheduleLoadRetry(!0)}o&&this._continuity.hasCompleteFrame&&"steady"===this._continuity.state&&this._beginContinuityCandidate("structural-response",!0),this._connectionWasLost=!1,this._serverStorage=!0,"boolean"==typeof i?.can_write&&(this._serverCanWrite=i.can_write),this._canOptimizeUndo=!(!i?.can_optimize_undo&&!n?.can_optimize_undo),this._haIntegrationVersion="string"==typeof i?.integration_version?i.integration_version:this._haIntegrationVersion,this._adoptStructuralResponses(i,n),this._adoptInitialSpace(this._model,!0),this._resumePendingNavMode(),this._cacheSnapshot(),this._warmVpArmed&&this._space===this._warmVp?.space?this._warmVpArmed=!1:t&&this._space===e||this._restoreZoom(),this._loadOk=!0,this.hass.callWS({type:"houseplan/trail/get"}).then(e=>{this._vacSrvTrails=e?.trails||{},this.requestUpdate()}).catch(()=>{}),this._ensureLiveSyncSubscriptions()}catch(e){if(this._serverCfg)this._scheduleLoadRetry(!0);else if(this._loadTries>=8){this._serverStorage=!1;try{this._layout=JSON.parse(localStorage.getItem(nm)||"{}")||{}}catch{this._layout={}}}}finally{this._loading=!1,this._continuityDataReady=!0,this._regSignature="",this._maybeRebuildDevices(),this.requestUpdate()}}_ensureLiveSyncSubscriptions(){const e=this.hass?.connection;if(!e)return;if(e!==this._liveSyncConnection&&(this._unsubCfg?.(),this._unsubCfg=null,this._unsubLayout?.(),this._unsubLayout=null,this._unsubTrail?.(),this._unsubTrail=void 0,this._unsubVirtual?.(),this._unsubVirtual=null,this._liveSyncGeneration++,this._liveSyncAttempt=null,this._liveSyncConnection=e),this._liveSyncAttempt)return;const t=this._liveSyncGeneration,i=[],n=(n,r,o,s)=>{n()||i.push(async()=>{const i=await e.subscribeEvents(s,o);t===this._liveSyncGeneration&&this.isConnected&&this.hass?.connection===e&&!n()?r(i):i?.()})};if(n(()=>this._unsubCfg,e=>{this._unsubCfg=e},"houseplan_config_updated",e=>{const t=Number(e?.data?.rev??-1);t!==this._cfgRev&&this._reloadConfigOnly(!1,t)}),n(()=>this._unsubTrail,e=>{this._unsubTrail=e},"houseplan_trail_updated",async()=>{try{const e=await this.hass.callWS({type:"houseplan/trail/get"});this._vacSrvTrails=e?.trails||{},this.requestUpdate()}catch{}}),n(()=>this._unsubLayout,e=>{this._unsubLayout=e},"houseplan_layout_updated",e=>this._onLayoutEvent(Number(e?.data?.rev??-1))),n(()=>this._unsubVirtual,e=>{this._unsubVirtual=e},"houseplan_virtual_light_updated",e=>{const t=Fc(this._virtualLights,e?.data);t!==this._virtualLights&&(this._virtualLights=t,this._capturedSnapshotVirtual="",this._cacheSnapshot(),this.requestUpdate())}),!i.length)return;const r=function(e){return Promise.allSettled(e.map(e=>Promise.resolve().then(e)))}(i).then(()=>{});this._liveSyncAttempt=r,r.finally(()=>{this._liveSyncAttempt===r&&(this._liveSyncAttempt=null)})}async _reloadConfigOnly(e=!1,t){if(!e){if(void 0!==t&&t<=this._cfgRev)return;if(this._saveConfigDebounced.pending()&&this._saveConfigDebounced.flush(),this._cfgWriting)return clearTimeout(this._reloadRetry),void(this._reloadRetry=window.setTimeout(()=>this._reloadConfigOnly(!1,t),400))}this._beginContinuityCandidate("config-reload",!1);try{const e=await this.hass.callWS({type:"houseplan/config/get"}),t=e?.config&&Array.isArray(e.config.spaces)?e.config:null;if(Tl(t)!==(this._cfgContentFingerprint||Tl(this._serverCfg))&&!await this._signer.prepareImage(this.hass,this._candidateBackdrop(t)))return this._continuity.note("asset-failed"),void this._scheduleLoadRetry(!0);const i=this._space;this._adoptStructuralResponses(e),this._adoptInitialSpace(this._model,!0),this._resumePendingNavMode(),this._cacheSnapshot(),this._space!==i&&this._restoreZoom(),this._regSignature="",this._maybeRebuildDevices(),this.requestUpdate()}catch(e){this._showToast(this._t("toast.cfg_reload_failed",{err:this._errText(e)}))}finally{this._continuityDataReady=!0,this.requestUpdate()}}_scheduleLoadRetry(e=!1){if(void 0!==this._loadRetryTimer)return;const t=Math.min(8e3,500*2**Math.min(4,Math.max(1,this._loadTries-7)));this._loadRetryTimer=window.setTimeout(()=>{this._loadRetryTimer=void 0,!e&&this._loadOk||this._loading||!this.hass||this._loadFromServer()},t)}_ensureHaRegistryAuthority(){const e=this.hass?.connection||null;e&&e!==this._haRegistryConnection&&(this._haRegistryRelease?.(),this._haRegistryConnection=e,this._haRegistryRev=-1,this._haBindingCacheKey="",this._planHassMemo=null,this._haRegistryRelease=Gc(this.hass,this._onHaRegistryUpdate),this._onHaRegistryUpdate())}get _haRegistry(){return Vc(this.hass)}get _planHass(){const e=this._haRegistry,t=Yc(this.hass,e),i=this._planHassMemo;if(i&&i.hass===this.hass&&i.sig===t)return i.active;const n=Xc(this.hass,e),r=Jc(this.hass,e);return this._planHassMemo={hass:this.hass,sig:t,active:n,full:r},n}_captureRenderDeviceSnapshot(){if(!this.hass)return;const e=Date.now(),t=[...this._activityRt.entries()].map(([t,i])=>`${t}:${i.gen}:${i.flashTs}:`+(i.flashKind&&(i.expiresAt||i.flashTs+wp)>e?1:0)).join("|"),i=Oc(this._virtualLights);if(this._capturedSnapshotSequence===this._hassSequence&&this._capturedSnapshotDevices===this._devices&&this._capturedSnapshotLayout===this._layout&&this._capturedSnapshotConfigEpoch===this._cfgEpoch&&this._capturedSnapshotVirtual===i&&this._capturedSnapshotActivity===t)return;const n=this._planHass,r=new Map,o=new Map,s=new Set(["sun.sun"]);this._vacFit?.source&&s.add(this._vacFit.source);const a=new Set,l=new Set,c=e=>{if(!e)return;const t=e.indexOf(":");if(t<0)return void s.add(e);const i=e.slice(0,t),n=e.slice(t+1);"device"===i?a.add(n):"entity"===i&&s.add(n)};for(const e of this._model)for(const t of e.rooms)t.area&&l.add(t.area),c(t.settings?.temp_source),c(t.settings?.hum_source);for(const e of this._serverCfg?.spaces||[])for(const t of e.openings||[])for(const e of ni(t))s.add(e);for(const e of this._serverCfg?.spaces||[])for(const t of e.decor||[])if("text"===t.kind){for(const e of String(t.text||"").matchAll(/\{([^{}\r\n]+)\}/g)){const t=Hi(e[1]);t?.entity&&s.add(t.entity)}t.entity&&s.add(t.entity),o.set(`decor:${e.id}:${t.id}`,Wi(t.text,t,n,e=>!!n.entities?.[e]&&!!n.states?.[e]&&!jh(n,e,Lh(this._markers))))}const h=Mh(n,this._devices,null,this._virtualLights);for(const e of this._devices){for(const t of[!1,!0])r.set(Nu(e.id,t),Pu(n,e,{liveStates:!1!==this._config?.live_states,showTemperature:!1!==this._config?.show_temperature,showSignal:t,activityRuntime:this._activityRt.get(e.id),sourceDetails:!1,lightDevices:this._devices,lightSources:h,registryHass:this._fullRegistryHass,reducedMotion:this._reducedMotion}));if(this._isVacDev(e)){const t=this._vacSource(e,n),i=t?Bl(n?.states?.[t]?.attributes):null,r=this._vacRt.get(e.id);o.set(`vacuum:${e.id}`,{source:t,telemetry:i,mapId:i?this._vacMapId(e,i,n):null,runtime:r?{trail:r.trail,lastTs:r.lastTs,moving:r.moving,jump:r.jump}:null,server:this._vacSrvTrails[e.id]||null})}}const d=Eu({sourceSequence:this._hassSequence,hass:n,devices:this._devices,presentations:r,positions:(u=this._model.length>0,p=this._devices,_=e=>this._livePos(e),u?new Map(p.map(e=>[e.id,_(e)])):new Map),facts:o,entityIds:s,deviceIds:a,areaIds:l});var u,p,_;this._capturedSnapshotSequence=this._hassSequence,this._capturedSnapshotDevices=this._devices,this._capturedSnapshotLayout=this._layout,this._capturedSnapshotActivity=t,this._capturedSnapshotConfigEpoch=this._cfgEpoch,this._capturedSnapshotVirtual=i,this._visibleDeviceSnapshot&&"steady"!==this._continuity.state?this._candidateDeviceSnapshot=d:(this._visibleDeviceSnapshot=d,this._candidateDeviceSnapshot=null)}get _renderDeviceSnapshot(){return this._stagedDeviceSnapshotToken===this._continuity.token?this._candidateDeviceSnapshot||this._visibleDeviceSnapshot:this._visibleDeviceSnapshot||this._candidateDeviceSnapshot}get _renderPlanHass(){return this._renderDeviceSnapshot?.hass||this._planHass}get _renderDevices(){return this._renderDeviceSnapshot?.devices||this._devices}get _fullRegistryHass(){return this._planHass,this._planHassMemo?.full||this.hass}_bindingStatus(e){return th(this.hass,e,this._haRegistry)}houseplanDiagnostics(){const e=function(e){const t=Vc(e);return{access:t.access,authoritative:t.authoritative,revision:t.revision,lastSuccess:t.lastSuccess,error:t.error}}(this.hass),t={active:0,ha_disabled:0,orphaned:0,unverified:0};for(const e of this._markers)e.removed||"virtual"===e.binding||t[this._bindingStatus(e.binding).kind]++;return{registry:{...e,lastSuccessAgeMs:e.lastSuccess?Math.max(0,Date.now()-e.lastSuccess):null},bindings:t}}_openBindingInHa(e){const[t,i]=e.split(":");if(i)if("device"!==t){if("entity"===t){const e=this._fullRegistryHass.entities?.[i];e?.device_id&&_m("/config/devices/device/"+encodeURIComponent(e.device_id))}}else _m("/config/devices/device/"+encodeURIComponent(i))}_bindingHasHaPage(e){const[t,i]=e.split(":");return!!i&&("device"===t||"entity"===t&&!!this._fullRegistryHass.entities?.[i]?.device_id)}_toggleMarkerDialogVisibility(){const e=this._markerDialog;if(!e)return;const t="ha"===e.bindingMode?this._bindingStatus(e.binding):null,i=e.hideFromPlan||"ha_disabled"===t?.kind;i&&"ha_disabled"===t?.kind?this._showToast(this._t("entity"===t.reason?"toast.ha_disabled_show_entity":"toast.ha_disabled_show_device")):i&&"unverified"===t?.kind?this._showToast(this._t("toast.ha_binding_unverified")):this._markerDialog={...e,hideFromPlan:!i}}_hookConnection(){const e=this.hass?.connection;e&&e!==this._connHooked&&(this._connHooked?.removeEventListener?.("ready",this._onConnReady),this._connHooked?.removeEventListener?.("disconnected",this._onConnLost),this._connHooked?.removeEventListener?.("reconnect-error",this._onConnLost),e.addEventListener?.("ready",this._onConnReady),e.addEventListener?.("disconnected",this._onConnLost),e.addEventListener?.("reconnect-error",this._onConnLost),this._connHooked=e)}_display(e){return this._signer.display(this.hass,e)}_resign(){this._signer.resign(this.hass,Sn(this._serverCfg))}_onLayoutEvent(e){e<=this._layoutRev||(clearTimeout(this._layoutSyncTimer),this._layoutSyncTimer=window.setTimeout(()=>{e<=this._layoutRev||this._reloadLayoutOnly()},200))}_noteLayoutRev(e){const t=e?.rev;"number"==typeof t&&t>this._layoutRev&&(this._layoutRev=t)}async _reloadLayoutOnly(){if(!this._serverStorage||!this.hass?.callWS)return;this._beginContinuityCandidate("layout-reload",!1);const e=new Map;for(const t of this._dirtyPos)this._layout[t]&&e.set(t,this._layout[t]);this._persistLayout.pending()&&this._persistLayout.flush();for(const[t,i]of this._sentPos)e.set(t,i);try{const t=await this.hass.callWS({type:"houseplan/layout/get"}),i={...t?.layout||{}};for(const[t,n]of e)i[t]=n;const n=Tl(i);n!==(this._layoutContentFingerprint||Tl(this._layout))&&(this._layout=i,this._layoutContentFingerprint=n),this._layoutRev=t?.rev??this._layoutRev,this._canOptimizeUndo=!!t?.can_optimize_undo,this._haIntegrationVersion="string"==typeof t?.integration_version?t.integration_version:this._haIntegrationVersion,this._undoKind=t?.undo_kind||null,this._cacheSnapshot(),this.requestUpdate()}catch{}finally{this._continuityDataReady=!0,this.requestUpdate()}}_maybeRebuildDevices(){const e=this.hass;if(!e?.devices||!e?.entities||!e?.areas)return;const t=this._haRegistry,i=Yc(e,t)+":"+Object.keys(e.areas).length+":"+(this._norm?"n":"l")+":"+Xd(e,this._config?.language);if(i===this._regSignature&&this._devices.length)return;this._regSignature=i;const n=new Map(this._devices.map(e=>[e.id,e.bindingStatus?.kind||"active"]));this._devices=Wh({hass:e,registry:t,areaToSpace:Object.fromEntries(Object.entries(this._areaToSpace).map(([e,t])=>[e,t.space])),markers:this._markers,settings:this._settings,excluded:this._excluded,showAll:this._showAll,firstSpaceId:this._model[0]?.id||"",loc:e=>this._t(e),iconRules:this._iconRules});const r=this._markers.filter(e=>!e.removed&&"virtual"!==e.binding).map(e=>e.binding).sort(),o=t.revision+":"+r.join("|");if(t.authoritative&&o!==this._haBindingCacheKey){const i=new Map;for(const n of r)i.set(n,th(e,n,t));!function(e){if("undefined"==typeof localStorage||!e.size)return;const t=eh(),i=Date.now();for(const[n,r]of e)"active"===r.kind?t[n]={kind:"active",ts:i}:"ha_disabled"===r.kind&&(t[n]={kind:"ha_disabled",reason:r.reason,ts:i});const n=Object.fromEntries(Object.entries(t).sort((e,t)=>t[1].ts-e[1].ts).slice(0,1500));Lc=n;try{localStorage.setItem(Hc,JSON.stringify(n))}catch{}}(i),this._haBindingCacheKey=o}this._defPos=this._defaultPositions(),this._syncNewDevices(),this._seedHiddenDevices(),this._syncActivityRuntime();const s=new Set(this._devices.filter(e=>!e.hidden).map(e=>e.id));for(const e of this._vacRt.keys())s.has(e)||this._vacRt.delete(e);if(this._infoCard){const e=this._devices.find(e=>e.id===this._infoCard.id);this._infoCard=e&&"ha_disabled"!==e.bindingStatus?.kind?e:null}const a=this._devices.some(e=>"ha_disabled"===e.bindingStatus?.kind&&"ha_disabled"!==n.get(e.id));a&&("ha_disabled"!==this._infoCard?.bindingStatus?.kind&&"ha_disabled"!==this._devices.find(e=>e.id===this._infoCard?.id)?.bindingStatus?.kind||this._closeInfoCard(),this._drag&&"ha_disabled"===this._devices.find(e=>e.id===this._drag.id)?.bindingStatus?.kind&&(this._drag=null),clearTimeout(this._holdTimer),this._holdFired=!1,this._tip=null,this._tapConfirm=null),this._nativeMoreInfoEntity&&!this._planEntityAvailable(this._nativeMoreInfoEntity)&&(pm(this,"hass-more-info",{entityId:null}),this._nativeMoreInfoEntity=null)}_syncNewDevices(){if(!this._norm||!this._loadOk||!this._serverCfg)return;const e=this._devices.filter(e=>!e.marker&&!e.virtual).map(e=>e.id).sort(),t=e.join(",");if(t===this._newSyncKey)return;this._newSyncKey=t;const i=this._settings,{fresh:n,known:r}=function(e,t){if(!Array.isArray(t))return{fresh:[],known:[...e]};const i=new Set(t),n=e.filter(e=>!i.has(e));return{fresh:n,known:n.length?[...t,...n]:t}}(e,i.known_devices);if(!Array.isArray(i.known_devices)||n.length){const e=[...new Set([...i.new_device_ids||[],...n])];this._serverCfg={...this._serverCfg,settings:{...i,known_devices:r,new_device_ids:e}},this._saveConfig()}}get _newIds(){const e=this._settings.new_device_ids;return new Set(Array.isArray(e)?e:[])}_ackNewDevice(e){if(!this._newIds.has(e)||!this._serverCfg)return;const t=this._settings;this._serverCfg={...this._serverCfg,settings:{...t,new_device_ids:(t.new_device_ids||[]).filter(t=>t!==e)}},this._saveConfig(),this.requestUpdate()}get _markers(){return this._serverCfg?.markers||[]}_roomLqi(e){if(!e)return null;const t=[];for(const i of this._renderDevices){if(i.area!==e||i.virtual)continue;const n=Ch(this._renderPlanHass,i.entities);null!=n&&t.push(n)}return Mi(t)}_roomBounds(e){if(e.poly&&e.poly.length){const t=e.poly.map(e=>e[0]),i=e.poly.map(e=>e[1]),n=Math.min(...t),r=Math.min(...i);return{x:n,y:r,w:Math.max(...t)-n,h:Math.max(...i)-r}}return{x:e.x??0,y:e.y??0,w:e.w??0,h:e.h??0}}_defaultPositions(){const e={},t=this._config?.icon_size??2.5;for(const i of this._model){const n=t/100*el(i)*1.3;for(const t of i.rooms){if(!t.area)continue;const r=this._devices.filter(e=>e.area===t.area&&e.space===i.id);if(!r.length)continue;const o=this._roomBounds(t),s=.1*Math.min(o.w,o.h),a=o.w-2*s,l=o.h-2*s,c=Math.max(1,Math.round(Math.sqrt(r.length*a/Math.max(l,1)))),h=Math.ceil(r.length/c),d=a/c,u=l/Math.max(h,1),p=r.map((e,t)=>({x:o.x+s+d*(t%c+.5),y:o.y+s+u*(Math.floor(t/c)+.5)}));Ri(p,o,n,.5*s),r.forEach((t,i)=>e[t.id]=La(p[i]))}}return e}_pos(e){const t=this._renderDeviceSnapshot?.positions.get(e.id);return t?{x:t.x,y:t.y}:this._livePos(e)}_livePos(e){const t=this._spaceModelById(e.space),i=this._layout[e.id];if(i)if(this._norm){if(i.s===e.space)return{x:i.x*cm,y:i.y*cm}}else if(void 0===i.s)return{x:i.x,y:i.y};return this._defPos[e.id]?this._defPos[e.id]:t?La(Qa(t)):{x:500,y:500}}_savePos(e,t,i){if(this._spaceModelById(e.space)){if(this._norm){const n=this._gridPitch,r=Math.round(t/n)*n,o=Math.round(i/n)*n,s=this._layout[e.id]?.k;this._layout={...this._layout,[e.id]:{s:e.space,x:qa(r/cm),y:qa(o/cm),...s?{k:s}:{}}}}else this._layout={...this._layout,[e.id]:{x:Math.round(t),y:Math.round(i)}};this._dirtyPos.add(e.id),this._persistLayout()}}_visualSamples(e){return Cu(this._renderPlanHass,e,this._renderDevices,void 0,this._fullRegistryHass).samples}_devicePresentation(e,t=!1!==this._config?.show_signal,i=!1){const n=i?null:this._renderDeviceSnapshot?.presentations.get(Nu(e.id,t));return n||Pu(this._renderPlanHass,e,{liveStates:!1!==this._config?.live_states,showTemperature:!1!==this._config?.show_temperature,showSignal:t,designPreview:i,activityRuntime:this._activityRt.get(e.id),sourceDetails:!1,lightDevices:this._renderDevices,registryHass:this._fullRegistryHass,reducedMotion:this._reducedMotion})}_deviceVisual(e){return this._devicePresentation(e).visual}_stateClass(e,t=this._deviceVisual(e)){const i=this._devicePresentation(e);return i.effectiveHidden?"":("icon_ripple"!==i.display||!1===this._config?.live_states||"alarm"===t.status||t.activity,zu({...i,visual:t}).join(" "))}_liveTemp(e){if(!this._config?.show_temperature)return null;if(!0===e.marker?.use_climate_temp){const t=Ah(this._renderPlanHass,e.entities);if(null!=t)return t}return"mdi:thermometer"!==e.icon&&"mdi:air-filter"!==e.icon?null:Dh(this._renderPlanHass,e.entities)}_bindingEntities(e){if("virtual"===e||!e)return[];const t=this._bindingStatus(e);return"active"===t.kind?t.enabledEntityIds:t.allEntityIds}_bindingHasClimate(e){return this._bindingEntities(e).some(e=>e.startsWith("climate."))}_bindingHasAlarm(e){return this._bindingEntities(e).some(e=>{const t=this._planHass?.states?.[e]||this.hass?.states?.[e],i=this._fullRegistryHass.entities[e]||this.hass?.entities?.[e];return In(e.split(".")[0],t?.attributes?.device_class||i?.device_class||i?.original_device_class)})}_liveHum(e){return this._config?.show_temperature&&e.primary&&Oh(this.hass,e.primary)?zh(this.hass,e.entities):null}_deviceBindingActive(e,t=!0){if(e.virtual||"virtual"===e.bindingKind)return!0;if(!e.bindingKind||!e.bindingRef)return!1;const i=this._bindingStatus(`${e.bindingKind}:${e.bindingRef}`);return"active"===i.kind||(t&&this._showToast(this._t("ha_disabled"===i.kind?"toast.ha_disabled_action":"toast.ha_binding_unverified")),!1)}_openMoreInfo(e){e?this._planEntityAvailable(e)?(this._nativeMoreInfoEntity=e,pm(this,"hass-more-info",{entityId:e})):this._showToast(this._t("toast.ha_disabled_action")):this._showToast(this._t("toast.no_entity"))}_interruptViewGesture(e,t){if(clearTimeout(this._holdTimer),clearTimeout(this._kioskHoldTimer),void 0!==e)for(const i of[t,this._stageEl])try{i?.hasPointerCapture?.(e)&&i.releasePointerCapture(e)}catch{}this._pointers.clear(),this._panStart=null,this._panLock=null,this._pinchStart=null,this._swipeStart=null}_closeInfoCard(){this._interruptViewGesture(),this._holdFired=!1,this._infoCard=null}_ctxDevice(e,t){"view"===this._mode&&(e.preventDefault(),e.stopPropagation(),this._deviceBindingActive(t)&&(t.primary?this._openMoreInfo(t.primary):this._infoCard=t))}_clickDevice(e,t){if(e.stopPropagation(),this._drag?.moved||this._suppressClick||this._holdFired)return;if("plan"===this._mode)return;if("devices"===this._mode)return void this._openMarkerDialog(t);const i=this._devices.find(e=>e.id===t.id);if(!i)return;const n=_d(i.tapAction,i.primary?.split(".")[0]),r=(e,t)=>{i.marker?.tap_confirm?this._tapConfirm={kind:"run",text:e,exec:t}:t()};if("toggle"===n){const e=this._toggleIntent(i);if(!e)return;if(!bd(e))return void this._showUnavailableToggleTargets(e);const t=e=>{const t=bd(e);if(!t)return;if("virtual-light"===t.kind)return this._startDevicePressFeedback(i.id),void this.hass.callWS({type:"houseplan/virtual_light/toggle",marker_id:t.markerId}).then(e=>{const t=Fc(this._virtualLights,e);t!==this._virtualLights&&(this._virtualLights=t,this._capturedSnapshotVirtual="",this._cacheSnapshot(),this.requestUpdate())}).catch(e=>this._showToast(this._t("toast.virtual_light_toggle_failed",{err:this._errText(e)})));const{command:n}=t;this._startDevicePressFeedback(i.id),this.hass.callService(n.domain,n.service,n.data).catch(e=>this._showToast(this._t("toast.error",{err:this._errText(e)})))},n=((o=e).targets.length?o.targets:o.skippedTargets).map(e=>e.name||e.entityId||e.ref).join(", ")||i.name;if(i.marker?.tap_confirm){const r=this._toggleConfirmationLines(e);if(!r.length)return;this._tapConfirm={kind:"toggle",text:this._t("confirm.tap_toggle",{name:n}),lines:r,initialIntent:e,deviceId:i.id,exec:()=>{const n=this._devices.find(e=>e.id===i.id),r=n?this._toggleIntent(n):null;r&&!bd(r)&&this._showUnavailableToggleTargets(r)||(r&&xd(e,r)?t(r):this._showToast(this._t("toast.tap_target_changed")))}}}else t(e);return}var o;if("info"===n)return this._interruptViewGesture(),void(this._infoCard=i);if(this._deviceBindingActive(i)){if("run"===n){const e=i.marker?.tap_target||"",t=function(e){const t=String(e||"").split(".")[0];return"automation"===t?{domain:"automation",service:"trigger"}:"script"===t?{domain:"script",service:"turn_on"}:"scene"===t?{domain:"scene",service:"turn_on"}:null}(e),n=this.hass.states[e];if(!t||!n)return void this._showToast(this._t("toast.run_target_missing"));const o=n.attributes?.friendly_name||e;return void r(this._t("confirm.tap_run",{name:o}),()=>{this._deviceBindingActive(i)&&this._planEntityAvailable(e)&&(this._startDevicePressFeedback(i.id),this.hass.callService(t.domain,t.service,{entity_id:e}).then(()=>{this._stampActivity(i.id,"event",this._activitySourceKey(i)),this.requestUpdate(),this._showToast(this._t("toast.run_started",{name:o}))}).catch(e=>this._showToast(this._t("toast.error",{err:this._errText(e)}))))})}"more-info"===n&&i.primary?this._openMoreInfo(i.primary):this._infoCard=i}}_showUnavailableToggleTargets(e){const t=function(e){if(!e||"group"!==e.kind||bd(e)||"configured-targets-missing"!==e.noneReason)return[];if(!e.skippedTargets.length||e.skippedTargets.some(e=>!wd.has(e.reason)&&"secure"!==e.reason))return[];const t=[],i=new Set;for(const n of e.skippedTargets){if(!wd.has(n.reason))continue;const e=String(n.name||n.entityId||n.ref||"").trim();e&&!i.has(e)&&(i.add(e),t.push(e))}return t}(e);return!!t.length&&(this._showToast(1===t.length?this._t("toast.toggle_target_unavailable",{name:t[0]}):this._t("toast.toggle_targets_unavailable",{names:t.join(", ")})),!0)}_keyDevice(e,t){"Enter"!==e.key&&" "!==e.key||"view"!==this._mode&&"devices"!==this._mode||(e.preventDefault(),this._clickDevice(e,t))}_startDevicePressFeedback(e){const t=[...this.renderRoot.querySelectorAll(".dev[data-id]")].find(t=>t.dataset.id===e),i=t?.querySelector(".device-shell");if(!i||"function"!=typeof i.animate)return;const n=Number.parseFloat(this.ownerDocument.defaultView?.getComputedStyle(i).scale||""),r=Number.isFinite(n)?Math.max(.95,Math.min(1,n)):1;this._devicePressAnimations.get(e)?.cancel();const o=this._reducedMotion?[{outlineColor:"transparent",outlineStyle:"solid",outlineWidth:"0px"},{outlineColor:"var(--hp-accent, #3ea6ff)",outlineStyle:"solid",outlineWidth:"2px",outlineOffset:"2px",offset:.5},{outlineColor:"transparent",outlineStyle:"solid",outlineWidth:"0px"}]:[{scale:String(r)},{scale:"0.95",offset:.5},{scale:"1"}],s=i.animate(o,{duration:200,easing:"cubic-bezier(.22,.61,.36,1)"});this._devicePressAnimations.set(e,s);const a=()=>{this._devicePressAnimations.get(e)===s&&this._devicePressAnimations.delete(e)};s.addEventListener("finish",a,{once:!0}),s.addEventListener("cancel",a,{once:!0})}_cancelDevicePressFeedback(){for(const e of this._devicePressAnimations.values())e.cancel();this._devicePressAnimations.clear()}_pruneDevicePressFeedback(){if(!this._devicePressAnimations.size)return;const e=new Set([...this.renderRoot.querySelectorAll(".dev[data-id]")].map(e=>e.dataset.id||""));for(const[t,i]of this._devicePressAnimations)e.has(t)||(i.cancel(),this._devicePressAnimations.delete(t))}_t(e,t){return Jd(Xd(this.hass,this._config?.language),e,t)}get _colorPickerLabels(){return{title:this._t("color_picker.title"),hue:this._t("color_picker.hue"),saturation:this._t("color_picker.saturation"),value:this._t("color_picker.value"),hex:this._t("color_picker.hex"),invalidHex:this._t("color_picker.invalid_hex")}}_help(e){return this._editorRuntimeOrThrow()._help(e)}get _stageEl(){return this.renderRoot.querySelector(".stage")}_contentItems(e){const t=[];for(const i of this._devices){if(i.space!==e.id||i.hidden)continue;const n=this._pos(i);t.push({minX:n.x,minY:n.y,maxX:n.x,maxY:n.y})}if(e.id===this._space){for(const e of this._openingsR){const i=Number(e.angle)*Math.PI/180,n=Math.cos(i)*e.rlen/2,r=Math.sin(i)*e.rlen/2,o=Ua([[e.rx-n,e.ry-r],[e.rx+n,e.ry+r]]);o&&t.push(o)}const i=this._decorH;for(const e of this._decorList){const n=Ua("line"===e.kind?[[e.x1*cm,e.y1*i],[e.x2*cm,e.y2*i]]:"text"===e.kind?[[e.x*cm,e.y*i]]:wr({x:e.x*cm,y:e.y*i,w:e.w*cm,h:e.h*i,angle:e.angle}));n&&t.push(n)}for(const i of this._physicalBodiesR(e)){const e=Ua(i);e&&t.push(e)}}return Va(e,t)}_frameOf(){const e=this._spaceModel();if(!e){this._frame=null;const e={x:0,y:0,w:cm,h:cm};return{rect:e,all:e,outliers:0}}const t=this._frame,i="view"!==this._mode;if(t&&t.id===e.id&&this._bdDrag)return t;if(t&&t.id===e.id&&t.model===e&&t.layout===this._layout&&t.devs===this._devices&&t.far===this._showFar&&t.grow===i)return t;const n=Xa(this._contentItems(e));let r=n.all||Ja(e),o=this._showFar?r:n.core||Ja(e);return t&&t.id===e.id&&i&&t.grow&&(o=dm(t.rect,o),r=dm(t.all,r)),this._frame={id:e.id,model:e,layout:this._layout,devs:this._devices,far:this._showFar,grow:i,rect:o,all:r,outliers:n.outliers},this._frame}_isoSource(){const e=this._spaceModel();if(!e)return null;const t=this._spaceWalls,i=this._openCuts(),n=this._openingsR.map((e,t)=>({id:String(e.id||t),sourceIndex:t,type:e.type,x:e.rx,y:e.ry,angle:Number(e.angle)||0,length:e.rlen>0?e.rlen:900,flipH:!!e.flip_h,flipV:!!e.flip_v})),r=xa(64,this._cellCm),o=xa(10,this._cellCm),s=`${e.id}|${function(e){let t=2166136261;const i=e=>{if("number"!=typeof e)if("string"!=typeof e)if("boolean"!=typeof e)if(Array.isArray(e)){t=O_(t,e.length);for(const t of e)i(t)}else{if(e&&"object"==typeof e){const t=e;for(const e of Object.keys(t).sort())i(e),i(t[e]);return}t=O_(t,-1)}else t=O_(t,e?1:0);else for(const i of e)t=O_(t,i.codePointAt(0)||0);else t=O_(t,e)};return i(e),t.toString(36)}({rooms:e.rooms,walls:t,openCuts:i,openings:n,partitions:e.partitions,roomDrafts:e.room_drafts,columns:e.wall_columns,cellCm:this._cellCm,gridPitch:this._gridPitch,wallKeyPitch:this._wallKeyPitch,camera:b_,wallHeight:r,floorEdgeHeight:o,algorithm:3})}`;return{key:s,build:()=>{const o=ca(e,this._cellCm,this._gridPitch,2e-4*this._gridPitch,this._partitionOpeningCuts(e)).all,s=this._roomWallOpeningInputs(this._openingsR,e),a=t.length||o.length?Ns(e.rooms,t,i,s,this._wallKeyPitch,this._cellCm,this._gridPitch,cm,o):null;if(a&&("failed-core"===a.status||"not-applicable"===a.status))throw new Error("wall boolean geometry failed");const l=a?.paperGeom??Ps(e.rooms,t,i,this._wallKeyPitch,this._cellCm,this._gridPitch,cm);if(!l)throw new Error("floor boolean geometry failed");const c=a?.openingIndex||Bs(e.rooms,t,i,this._wallKeyPitch,this._cellCm,this._gridPitch,cm),h=n.map(e=>{const i=this._openingsR[e.sourceIndex],n="gate"===e.type?!e.flipV:e.flipV,o=i?.partitionHost?Od(i.partitionHost,n):t.length||"gate"===e.type?Vs(c,{x:e.x,y:e.y,angle:e.angle,length:e.length,flip_v:n}):{ox:0,oy:0,cm:0,side:-1};return function(e,t=64){if(!(z_([e.x,e.y,e.angle,e.length,e.face.ox,e.face.oy,t])&&e.length>0&&t>0))throw new Error("invalid isometric opening input");const i=e.length/2;let n;if("passage"===e.type)n=[];else if("gate"===e.type){const r=10*e.face.side;n=[F_(e,0,[-i,0],[i,0],r,0,.88*t),F_(e,1,[i,0],[-i,0],-r,0,.88*t)]}else n="window"===e.type?[F_(e,0,[-i,0],[i,0],-90,.27*t,.78*t),F_(e,1,[i,0],[-i,0],90,.27*t,.78*t)]:[F_(e,0,[-i,0],[e.length,0],-90,0,.92*t)];return{id:e.id,sourceIndex:e.sourceIndex,type:e.type,leaves:Object.freeze(n.map(e=>Object.freeze(e)))}}({...e,face:o},r)});return{walls:a?.components.flatMap(e=>e.geom)||[],floor:l,openings:Object.freeze(h)}}}}_isoSceneKey(){if(!this._labsIso||"view"!==this._mode)return null;try{return this._isoSource()?.key??null}catch{return`${this._space}|invalid`}}_isoScene(){const e=this._isoSource();if(!e)return null;const t=this._isoGeometryCache.get(e.key);if(t)return{key:e.key,...t};const i=this._frameOf().rect,n=e.build(),r=xa(64,this._cellCm),o=xa(10,this._cellCm),s=function(e){let t=1/0,i=1/0,n=-1/0,r=-1/0;for(const o of e)for(const e of o.leaves){const o=Math.hypot(e.closedVector[0],e.closedVector[1]);t=Math.min(t,e.hinge[0]-o),i=Math.min(i,e.hinge[1]-o),n=Math.max(n,e.hinge[0]+o),r=Math.max(r,e.hinge[1]+o)}return z_([t,i,n,r])?{x:t,y:i,w:n-t,h:r-i}:null}(n.openings),a=S_({rect:s?dm(i,s):i,wallHeight:r,openingHeight:r,floorDepth:o}),l=function(e,t=b_,i=64){if(!Number.isFinite(i)||i<0)throw new Error("invalid wall height");const n=[],r=[],o=[];let s=0;for(let a=0;a<(e||[]).length;a++){const l=e[a];for(let e=0;e<(l||[]).length;e++){const c=R_(l[e],e>0);if(!(c.length<3||Math.abs(T_(c))<1e-9)){n.push(C_(c,t,i)),s+=c.length;for(let n=0;ne.depth-t.depth||e.polygon-t.polygon||e.ring-t.ring||e.edge-t.edge),{topPath:n.join(" "),sides:r,contactPath:o.join(" "),edgeCount:s}}(n.walls,b_,r),c={geometry:l,floor:A_(n.floor,o),openings:n.openings,frame:a};return J_(this._isoGeometryCache,e.key,c,8),{key:e.key,...c}}_effectiveProjection(){if("iso"!==this._desiredProjection||!this._model.length)return"flat";const e=this._isoSceneKey()||`${this._space}|invalid`;if(this._isoFallback.has(e))return"flat";try{const e=this._isoScene();return e?(t="iso",i=e.key,n=this._isoFallback,"iso"===t&&i&&!n.has(i)?"iso":"flat"):"flat"}catch(t){if(!this._isoFallback.has(e)){this._isoFallback.add(e);const i=e.split("|");console.warn(`HOUSEPLAN ISO FALLBACK: #89, space ${this._space}, fingerprint ${i[i.length-1]}, ${t instanceof Error?t.message:"renderer error"}`)}return"flat"}var t,i,n}_scenePoint(e){return"iso"===this._renderProjection?k_(e,0):e}_floorView(e){if("iso"!==this._renderProjection)return e;const t=x_([e.x,e.y]),i=x_([e.x+e.w,e.y+e.h]);return{x:t[0],y:t[1],w:i[0]-t[0],h:i[1]-t[1]}}_baseVb(){if("iso"===this._effectiveProjection()){if(!this._spaceDisplayForRender().showBorders){const e=S_({rect:this._frameOf().rect,wallHeight:xa(64,this._cellCm)});return[e.x,e.y,e.w,e.h]}const e=this._isoScene()?.frame??this._frameOf().rect;return[e.x,e.y,e.w,e.h]}const e=this._frameOf().rect;return[e.x,e.y,e.w,e.h]}get _outliers(){return this._showFar?0:this._frameOf().outliers}_fitFar(){this._showFar=!0,this._frame=null,this._resetZoom()}_fitAll(){this._showFar=!0,this._frame=null,this._resetZoom()}_renderFarHint(){return this._kiosk||"view"!==this._mode||this._booting||!this._outliers?G:B`
+ + ${this._t("canvas.far_objects",{n:this._outliers})} + +
`}_renderHomeArrow(){if(this._booting)return G;const e=this._view;if(!e||!e.w||!e.h)return G;const t=this._baseVb(),i=t[0],n=t[1],r=t[2],o=t[3];if(!(i+r<=e.x||i>=e.x+e.w||n+o<=e.y||n>=e.y+e.h))return G;const s=Math.atan2(n+o/2-(e.y+e.h/2),i+r/2-(e.x+e.w/2)),a=50+38*Math.cos(s),l=50+38*Math.sin(s);return B``}_stageAspect(){const e=this._stageEl,t=this._baseVb();return e&&e.clientHeight?e.clientWidth/e.clientHeight:t[2]/t[3]}_viewOr(e){return this._view&&this._view.w?this._view:Ti(e,this._stageAspect())}_roomLabelReferenceViewWidth(e){if(!this._markup)return e.w;const t=this._stageEl,i=this.renderRoot.querySelector(".editorchrome");if(!t||t.clientWidth<=0||t.clientHeight<=0)return e.w;const n=t.clientHeight+(i?.getBoundingClientRect().height||0);return n<=0?e.w:this._viewForModeTarget(this._zoom,void 0,void 0,t.clientWidth,n).w}_screenToVb(e,t){const i=this._stageEl,n=this._viewOr(this._baseVb()),r=i?.clientWidth||1,o=i?.clientHeight||1;return[n.x+e/r*n.w,n.y+t/o*n.h]}_clampView(e,t){const i=(e,t,i,n)=>{const r=1*Math.max(t,n),o=i-r,s=i+n-t+r;return Math.max(Math.min(o,s),Math.min(Math.max(o,s),e))};return{w:e.w,h:e.h,x:i(e.x,e.w,t.x,t.w),y:i(e.y,e.h,t.y,t.h)}}_applyView(e,t,i){const n=this._baseVb(),r=Ti(n,this._stageAspect()),o=Math.min(fm.ZOOM_MAX,Math.max(fm.ZOOM_MIN,e)),s=r.w/o,a=r.h/o,l=this._viewOr(n),c=t??l.x+l.w/2,h=i??l.y+l.h/2,d=this._clampView({x:c-s/2,y:h-a/2,w:s,h:a},r);return!(this._view&&Math.abs(this._zoom-o)<1e-9&&Math.abs(this._view.x-d.x)<1e-6&&Math.abs(this._view.y-d.y)<1e-6&&Math.abs(this._view.w-d.w)<1e-6&&Math.abs(this._view.h-d.h)<1e-6)&&("opening"===this._tool&&(this._cursorPt=null,this._clearOpeningPlacement(!1)),this._zoom=o,this._view=d,!0)}_bootWatch(){clearTimeout(this._bootTimer),this._bootStart=Date.now(),this._bootLastH=-1,this._bootLastChange=this._bootStart;const e=()=>{if(!this._booting)return;const t=Date.now(),i=this._stageEl?this._stageEl.clientHeight:0;i!==this._bootLastH&&(this._bootLastH=i,this._bootLastChange=t);const n=t-this._bootStart;n>=1200||n>=700&&i>0&&t-this._bootLastChange>=250?this._bootSettled():this._bootTimer=window.setTimeout(e,100)};this._bootTimer=window.setTimeout(e,100)}_bootSettled(){this._booting&&!this._bootSettling&&(this._bootSettling=!0,this._refitView(),this._bootSettleRaf=requestAnimationFrame(()=>{this._bootSettleRaf=0,this._finishBootSettled()}))}_finishBootSettled(){if(!this._booting)return;this._refitView(),this._booting=!1,this._bootSettling=!1;const e=this._stageEl?.clientHeight??0;!this._config?.kiosk&&e>0&&this._warmPatch({hdrH:this._hdrH,stageH:e,vp:this._warmViewportState()},!0),this._bootFading=!0,this._bootTimer=window.setTimeout(()=>{this._bootFading=!1},220),this._bootSoft=!0,clearTimeout(this._bootSoftTimer),this._bootSoftTimer=window.setTimeout(()=>{this._bootSoft=!1},Z_)}_bootSoftCancel(){this._bootSoft&&(clearTimeout(this._bootSoftTimer),this._bootSoft=!1)}_beginResumeSettle(){this._booting||this._resumeSettling||(this._modeTransitionPreparing?this._modeTransitionForceAtomic=!0:this._cancelModeTransition(!0),this._viewportInvalidAt=0,this._beginContinuityCandidate("warm-resume",!1),this._loading?this.requestUpdate():this._loadFromServer())}_refitView(){if(this._modeTransitionBusy)return;const e=this._stageEl;if(!e||"visible"!==document.visibilityState||e.clientWidth<=0||e.clientHeight<=0)return void(this._viewportInvalidAt||(this._viewportInvalidAt=Date.now()));const t=[e.clientWidth,e.clientHeight],i=this._lastValidStageSize,n=!!i&&Math.abs(i[0]-t[0])<=.5&&Math.abs(i[1]-t[1])<=.5,r=this._viewportInvalidAt?Date.now()-this._viewportInvalidAt:0;if(this._viewportInvalidAt=0,!i)return this._lastValidStageSize=t,void(this._view||this._applyView(this._zoom));n?this._pendingRefitSize=null:(this._pendingRefitSize=t,this._refitRaf||(this._refitRaf=requestAnimationFrame(()=>{this._refitRaf=0;const e=this._pendingRefitSize;this._pendingRefitSize=null;const t=this._stageEl;if(!e||!t||t.clientWidth<=0||t.clientHeight<=0)return;if(Math.abs(t.clientWidth-e[0])>.5||Math.abs(t.clientHeight-e[1])>.5)return void this._refitView();const i=this._lastValidStageSize;if(i&&Math.abs(i[0]-e[0])<=.5&&Math.abs(i[1]-e[1])<=.5)return;this._lastValidStageSize=e;const n=this._view;r>=wl?this._beginContinuityCandidate("stage-size-restored",!0,"stage-size"):this._continuity.hasCompleteFrame&&this._beginContinuityCandidate("stage-resize",!0,"stage-size"),this._applyView(this._zoom,n?n.x+n.w/2:void 0,n?n.y+n.h/2:void 0)})))}_zoomAt(e,t,i){const n=this._stageEl;if(!n)return;const r=Ti(this._baseVb(),this._stageAspect()),o=Math.min(fm.ZOOM_MAX,Math.max(fm.ZOOM_MIN,i));"opening"===this._tool&&(this._cursorPt=null,this._clearOpeningPlacement(!1));const s=n.clientWidth,a=n.clientHeight,l=this._screenToVb(e,t),c=r.w/o,h=r.h/o;this._zoom=o,this._view=this._clampView({x:l[0]-e/s*c,y:l[1]-t/a*h,w:c,h:h},r)}_onWheel(e){const t=this._stageEl;if(!t)return;e.preventDefault();const i=t.getBoundingClientRect(),n=e.deltaY<0?1.15:1/1.15;this._zoomAt(e.clientX-i.left,e.clientY-i.top,this._zoom*n),this._saveZoom()}_stepZoom(e){const t=this._stageEl;t&&(this._zoomAt(t.clientWidth/2,t.clientHeight/2,this._zoom*(e>0?1.4:1/1.4)),this._saveZoom())}_resetZoom(){const e=this._baseVb();"opening"===this._tool&&(this._cursorPt=null,this._clearOpeningPlacement(!1)),this._zoom=1,this._view=Ti(e,this._stageAspect()),this._saveZoom()}_saveZoom(){if("view"===this._mode){this._zoomBySpace={...this._zoomBySpace,[this._space]:this._zoom};try{localStorage.setItem(om,JSON.stringify(this._zoomBySpace))}catch{}}}_restoreZoom(){const e=this._zoomBySpace[this._space]||1;this._zoom=e;const t=this._stageEl;if(t&&t.clientHeight){const t=this._baseVb();return this._applyView(e,t[0]+t[2]/2,t[1]+t[3]/2),void this.requestUpdate()}this._view=null,requestAnimationFrame(()=>{if(!this._stageEl)return;const t=this._baseVb();this._applyView(e,t[0]+t[2]/2,t[1]+t[3]/2),this.requestUpdate()})}_stagePointerDown(e){if(this._vacFit)return;if(this._kiosk&&(this._cyclePausedUntil=Date.now()+6e4,0===this._pointers.size?(this._swipeStart={x:e.clientX,y:e.clientY,id:e.pointerId},e.target.closest?.(".dev, .roomlabel, .oplock")||(clearTimeout(this._kioskHoldTimer),this._kioskHoldTimer=window.setTimeout(()=>{this._kioskDialog=!0,this._swipeStart=null},3e3))):(this._swipeStart=null,clearTimeout(this._kioskHoldTimer))),this._drag)return;if(this._markup&&e.target.closest?.(".roomlabel, .rlhandle, .rszhandle, .dev, .oplock, .op-hit, button"))return;if("devices"===this._mode&&e.target.closest(".dev"))return;if("decor"===this._mode&&this._decorPointerDown(e))return;this._pointers.set(e.pointerId,{x:e.clientX,y:e.clientY});const t=this._viewOr(this._baseVb());if(1===this._pointers.size)this._panStart={sx:e.clientX,sy:e.clientY,vx:t.x,vy:t.y},this._panLock=null,this._suppressClick=!1;else if(2===this._pointers.size){"opening"===this._tool?(this._cursorPt=null,this._clearOpeningPlacement(!1)):"draw"===this._tool&&this._clearPlanSnapHover();const e=[...this._pointers.values()],t=Math.hypot(e[0].x-e[1].x,e[0].y-e[1].y);this._pinchStart={dist:t,zoom:this._zoom},this._panStart=null,this._panLock=null}}get _swipeZone(){return!this._hasFixedFloor&&this._kiosk&&this._zoom<=1.001&&this._model.length>1}_stagePointerMove(e){if(this._physicalRotate?.pid!==e.pointerId)if(this._physicalDrag?.pid!==e.pointerId)if(this._dtDrag?.pid!==e.pointerId)if(this._bdDrag?.pid!==e.pointerId){if(this._decorDraft?.pid===e.pointerId){const t=this._decorDraft;let i=this._decorSnap(this._svgPoint(e),e.pointerType);if(e.shiftKey&&("rect"===t.kind||"ellipse"===t.kind)){const e=i[0]-t.a[0],n=i[1]-t.a[1],r=Math.max(Math.abs(e),Math.abs(n));i=this._snap([t.a[0]+(e<0?-r:r),t.a[1]+(n<0?-r:r)])}return void(this._decorDraft={...t,b:i})}if(this._decorMove?.pid!==e.pointerId)if(this._pointers.has(e.pointerId)){if(this._pointers.set(e.pointerId,{x:e.clientX,y:e.clientY}),this._markup&&1===this._pointers.size&&this._markupMove(e),this._pinchStart&&this._pointers.size>=2){"opening"===this._tool?(this._cursorPt=null,this._clearOpeningPlacement(!1)):"draw"===this._tool&&this._clearPlanSnapHover();const e=[...this._pointers.values()],t=Math.hypot(e[0].x-e[1].x,e[0].y-e[1].y)/(this._pinchStart.dist||1),i=this._stageEl.getBoundingClientRect(),n=(e[0].x+e[1].x)/2-i.left,r=(e[0].y+e[1].y)/2-i.top;this._zoomAt(n,r,this._pinchStart.zoom*t),this._suppressClick=!0,this._saveZoom()}else if(this._panStart){const t=e.clientX-this._panStart.sx,i=e.clientY-this._panStart.sy;Math.abs(t)+Math.abs(i)>4&&(this._suppressClick=!0,clearTimeout(this._holdTimer),"opening"===this._tool?(this._cursorPt=null,this._clearOpeningPlacement(!1)):"draw"===this._tool&&this._clearPlanSnapHover()),null===this._panLock&&Math.abs(t)+Math.abs(i)>8&&(this._panLock=this._swipeZone&&Math.abs(t)>1.5*Math.abs(i)?"swipe":"pan");const n=this._stageEl;if("pan"===this._panLock&&n){const e=this._baseVb(),r=this._viewOr(e),o=Ti(e,this._stageAspect());this._view=this._clampView({x:this._panStart.vx-t/(n.clientWidth||1)*r.w,y:this._panStart.vy-i/(n.clientHeight||1)*r.h,w:r.w,h:r.h},o)}}}else this._markupMove(e);else this._decorMoveUpdate(e)}else this._bdMove(e);else this._dtMove(e);else this._physicalMove(e);else this._physicalRotateMove(e)}_stagePointerLeave(e){this._markup&&("opening"===this._tool?(this._cursorPt=null,this._clearOpeningPlacement(!1)):"draw"===this._tool&&this._clearPlanSnapHover())}_stagePointerUp(e){if(this._kiosk){clearTimeout(this._kioskHoldTimer);const t=this._swipeStart;if(this._swipeStart=null,t&&t.id===e.pointerId){const i=e.clientX-t.x,n=e.clientY-t.y;if(Math.abs(i)+Math.abs(n)<8){const e=Date.now();e-this._lastTap<350&&this._resetZoom(),this._lastTap=e}const r="pan"===this._panLock?null:function(e,t,i,n,r,o=60){if(i>1.001||n.length<2)return null;if(Math.abs(e)e.id),this._space);r&&this._slideTo(r,i<0?"left":"right")&&(this._saveNav(),this._suppressClick=!0,setTimeout(()=>this._suppressClick=!1,0),this._showKioskDots())}}if(this._physicalDrag?.pid===e.pointerId)return void this._physicalUp(e);if(this._physicalRotate?.pid===e.pointerId)return void this._physicalRotateUp(e);if(this._dtDrag?.pid===e.pointerId)return void this._dtUp();if(this._bdDrag?.pid===e.pointerId)return void this._bdUp();if(this._decorDraft?.pid===e.pointerId)return void this._decorCommitDraft();if(this._decorMove?.pid===e.pointerId)return this._decorMove.moved&&(this._recordGeometry(this._t("history.decor_move"),this._decorMove.before),this._saveConfig()),void(this._decorMove=null);const t=!!this._pinchStart||!!this._panStart;this._pointers.delete(e.pointerId),this._pointers.size<2&&(this._pinchStart=null),0===this._pointers.size&&(this._panStart=null,this._panLock=null,setTimeout(()=>this._suppressClick=!1,0)),t&&0===this._pointers.size&&this.requestUpdate()}_clickRoom(e){!this._suppressClick&&e.area&&_m("/config/areas/area/"+e.area)}_pointerDown(e,t){if("plan"===this._mode)return;if("view"===this._mode){this._holdFired=!1,clearTimeout(this._holdTimer);const i=e.pointerId,n=e.currentTarget;return void(this._holdTimer=window.setTimeout(()=>{this._holdFired=!0,this._interruptViewGesture(i,n),this._infoCard=t},600))}if("ha_disabled"===t.bindingStatus?.kind)return;e.preventDefault();const i=this._pos(t);this._drag={id:t.id,sx:e.clientX,sy:e.clientY,ox:i.x,oy:i.y,moved:!1},gm(e),this._tip=null}_pointerMove(e,t){if(!this._drag||this._drag.id!==t.id)return;const i=this.renderRoot.querySelector(".stage");if(!i)return;const n=this._baseVb(),r=i.getBoundingClientRect(),o=this._viewOr(n),s=(e.clientX-this._drag.sx)/r.width*o.w,a=(e.clientY-this._drag.sy)/r.height*o.h;Math.abs(e.clientX-this._drag.sx)+Math.abs(e.clientY-this._drag.sy)>3&&(this._drag.moved=!0,clearTimeout(this._holdTimer));const l=ja(this._drag.ox+s),c=ja(this._drag.oy+a);this._savePos(t,l,c)}_pointerUp(e,t){if(clearTimeout(this._holdTimer),!this._drag||this._drag.id!==t.id)return;const i=this._drag.moved;this._drag=i?this._drag:null,i&&(this._selId=t.id,window.setTimeout(()=>this._drag=null,0))}_showToast(e){for(const e of this.renderRoot.querySelectorAll("hp-dialog"))e.closeTransientOverlays("toast");this._toast=e,clearTimeout(this._toastTimer),this._toastTimer=window.setTimeout(()=>{this._toast=""},3500)}_syncPointerHoverTargets(){const e=this._pointerModality.hoverEnabled;for(const t of this.renderRoot.querySelectorAll(lm))t.toggleAttribute("data-pointer-hover",e)}_syncPointerHoverSubtree(e){if(e.nodeType!==Node.ELEMENT_NODE)return;const t=e,i=this._pointerModality.hoverEnabled;t.matches(lm)&&t.toggleAttribute("data-pointer-hover",i);for(const e of t.querySelectorAll(lm))e.toggleAttribute("data-pointer-hover",i)}_clearTransientHover(e=!1){e&&this._pointerModality.suspend(),this._tip&&(this._tip=null),this._hoverRoom&&(this._hoverRoom=null)}_notePointer(e){const t=this._pointerModality.modality,i=this._pointerModality.note(e);"touch"!==i&&"pen"!==i||t===i&&!this._tip&&!this._hoverRoom||this._clearTransientHover()}_guardTouchGesture(e){if(this._editorSecondary?.handleOutsideDismiss(e))return;if("click"===e.type){if(!this._suppressClick&&!this._touchSequenceMultitouch&&Date.now()>this._touchClickBlockUntil)return;return e.preventDefault(),void e.stopImmediatePropagation()}const t=e;if(this._notePointer(t),"touch"===t.pointerType)if("pointerdown"!==e.type){if("pointermove"===e.type){const e=this._touchContacts.get(t.pointerId);if(!e)return;const i={...e,x:t.clientX,y:t.clientY};if(this._touchContacts.set(t.pointerId,i),this._touchSequenceMultitouch&&"view"===this._mode&&!this._vacFit&&this._pinchStart&&[...this._touchContacts.values()].every(e=>e.inStage)){this._pointers.set(t.pointerId,{x:t.clientX,y:t.clientY});const e=[...this._touchContacts.values()];if(e.length>=2&&this._stageEl){const[t,i]=e,n=Math.hypot(t.x-i.x,t.y-i.y)/(this._pinchStart.dist||1),r=this._stageEl.getBoundingClientRect();this._zoomAt((t.x+i.x)/2-r.left,(t.y+i.y)/2-r.top,this._pinchStart.zoom*n),this._saveZoom()}}return}if("pointerup"===e.type||"pointercancel"===e.type||"lostpointercapture"===e.type){if(this._clearTransientHover(),this._touchContacts.delete(t.pointerId),this._touchSequenceMultitouch){if(this._touchClickBlockUntil=Date.now()+500,this._pointers.delete(t.pointerId),!this._vacFit&&this._pointers.size>=2){const[e,t]=[...this._pointers.values()];this._pinchStart={dist:Math.hypot(e.x-t.x,e.y-t.y),zoom:this._zoom}}else this._pinchStart=null;0===this._pointers.size&&(this._panStart=null,this._panLock=null)}0===this._touchContacts.size&&(this._touchSequenceMultitouch=!1)}}else if(this._touchContacts.set(t.pointerId,{x:t.clientX,y:t.clientY,inStage:!!t.target?.closest?.(".stage")}),this._touchContacts.size>=2){this._clearTransientHover(),this._touchSequenceMultitouch=!0,this._touchClickBlockUntil=Number.POSITIVE_INFINITY,clearTimeout(this._holdTimer),clearTimeout(this._kioskHoldTimer),this._swipeStart=null;const e=[...this._touchContacts.values()];if("view"===this._mode&&!this._vacFit&&e.every(e=>e.inStage)){this._pointers=new Map([...this._touchContacts].map(([e,t])=>[e,{x:t.x,y:t.y}]));const[t,i]=e;this._pinchStart={dist:Math.hypot(t.x-i.x,t.y-i.y),zoom:this._zoom},this._panStart=null,this._panLock=null}else this._vacFit&&(this._pointers.clear(),this._pinchStart=null,this._panStart=null,this._panLock=null)}}_showTip(e,t,i,n,r,o){this._notePointer(e),this._pointerModality.hoverEnabled&&(this._drag||(this._tip={x:e.clientX,y:e.clientY,title:t,meta:i,lqi:n,temp:r,hum:o}))}get _gridPitch(){return Ea}get _cellCm(){const e=Number(this._curSpaceCfg?.cell_cm);return Number.isFinite(e)&&e>0?e:5}_fmtLen(e,t){const i=function(e,t,i,n){return Math.hypot(t[0]-e[0],t[1]-e[1])/i*n}(e,t,this._gridPitch,this._cellCm);return Kt(i,"mi"===this.hass?.config?.unit_system?.length)}get _curSpaceCfg(){const e=this._resize?.preview;return e&&e.space===this._space?e.sp:this._serverCfg?.spaces.find(e=>e.id===this._space)}get _renderCfg(){const e=this._resize?.preview;return e&&this._serverCfg?{...this._serverCfg,spaces:this._serverCfg.spaces.map(t=>t.id===e.space?e.sp:t)}:this._serverCfg}get _spaceH(){return this._curSpaceCfg,cm}get _segments(){const e=this._curSpaceCfg,t=this._spaceH;return Jt(e?.rooms||[]).map(e=>[e[0]*cm,e[1]*t,e[2]*cm,e[3]*t])}_savedNav(){if(this._hasFixedFloor)return null;try{return JSON.parse(localStorage.getItem(sm)||"null")}catch{return null}}_saveNav(){if(!this._hasFixedFloor)try{localStorage.setItem(sm,JSON.stringify({space:this._space}))}catch{}}_leaveCardRoute(){if(this._routeDepartureHandled)return;this._routeDepartureHandled=!0,"view"!==this._mode&&this._setMode("view",!1),this._pendingNavMode=null,this._geometryHistory.clear(),this._activeDraftId=null,this._resumeDraftBySpace={},this._draftSegmentCms=[],this._closingWallCm=null,this._drawWallField=null,this._showHidden=!1,this._tapConfirm=null,this._vacCalConfirm=null,this._decorEraseConfirm=null,this._openingInfo=null,this._closeInfoCard(),this._rulesDialog=null,this._alignDialog=null,this._preflightClipboardFallback=null,this._backupImportDialog=null,this._backupExportDialog=null,this._settingsDialog=null,this._deviceInbox=null,this._deviceInboxReturn=null,this._markerDialog=null,this._openingDialog=null,this._physicalDialog=null,this._wallDialog=null,this._backdropDialog=null,this._decorShapeDialog=null,this._decorTextDialog=null,this._mergeDialog=null,this._roomDialog=!1,this._spaceDialog=null,this._importDialog=null;const e=this._warmSlot;e?.owner===this._warmGen&&(e.vp=this._warmViewportState(),e.dlg=null,e.frameFingerprint="",clearTimeout(e.evict),e.evict=0),this._saveNav()}_setMode(e,t=!0){return this._editorRuntime?this._editorRuntime._setMode(e,t):"view"===e?void this._editorModeRequest++:void this._requestMode(e,t)}_primeDrawWallField(){return this._editorRuntimeOrThrow()._primeDrawWallField()}get _drawWallFieldValue(){return null===this._drawWallField?fo(15,this._imperial):this._drawWallField}get _drawWallCm(){const e=(e=>{const t=String(e??"").trim().replace(",",".");if(!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(t))return null;const i=Number(t);return Number.isFinite(i)?i:null})(this._drawWallFieldValue);if(null==e||e<0)return null;const t=this._imperial?2.54*e:e,i="column"===this._tool?Xs:100;return t>=("column"===this._tool?1:0)&&t<=i?t:null}get _drawWallMaxCm(){return"column"===this._tool?Xs:100}_showPhysicalRange(e=this._drawWallMaxCm,t=0){return this._editorRuntimeOrThrow()._showPhysicalRange(e,t)}_draftSegmentCount(e=this._curSpaceCfg){return this._editorRuntimeOrThrow()._draftSegmentCount(e)}_mergeSpacePartitions(e,t){return this._editorRuntimeOrThrow()._mergeSpacePartitions(e,t)}_finishWallChain(){return this._editorRuntimeOrThrow()._finishWallChain()}_activateMarkupTool(e){return this._editorRuntimeOrThrow()._activateMarkupTool(e)}_limitReached(e){return this._editorRuntimeOrThrow()._limitReached(e)}_svgPoint(e){return this._editorRuntimeOrThrow()._svgPoint(e)}_snap(e){return this._editorRuntimeOrThrow()._snap(e)}_snapDrawPoint(e,t=!1){return this._editorRuntimeOrThrow()._snapDrawPoint(e,t)}_planSnapOpeningCuts(e,t){return this._editorRuntimeOrThrow()._planSnapOpeningCuts(e,t)}_planSnapGeometrySnapshot(){return this._editorRuntimeOrThrow()._planSnapGeometrySnapshot()}_hiddenWallDiagnosticSnapshot(){return this._editorRuntimeOrThrow()._hiddenWallDiagnosticSnapshot()}_planStructuralGeometrySnapshot(){return this._editorRuntimeOrThrow()._planStructuralGeometrySnapshot()}_planSnapContextKey(e){return this._editorRuntimeOrThrow()._planSnapContextKey(e)}_resolvePlanDrawPoint(e,t){return this._editorRuntimeOrThrow()._resolvePlanDrawPoint(e,t)}get _activePlanSnapCandidate(){if(!this._markup||"draw"!==this._tool)return null;const e=this._planSnapHover;if(!e)return null;const t=this._planSnapGeometrySnapshot();return e.contextKey===this._planSnapContextKey(t.key)?e.candidate:null}get _activePlanSnapConflicts(){if(!this._markup||"draw"!==this._tool)return[];const e=this._planSnapHover;if(!e)return[];const t=this._planSnapGeometrySnapshot();return e.contextKey===this._planSnapContextKey(t.key)?e.conflicts:[]}_clearPlanSnapHover(e=!0){return this._editorRuntime?this._editorRuntimeOrThrow()._clearPlanSnapHover(e):(this._planSnapHover=null,void(e&&(this._cursorPt=null)))}_samePt(e,t){return oi(e,t)}_dropLegacySegments(){return this._editorRuntimeOrThrow()._dropLegacySegments()}_rollbackRejectedPhysicalWrites(e){return this._editorRuntimeOrThrow()._rollbackRejectedPhysicalWrites(e)}async _reloadRejectedPhysicalWrite(){return this._editorRuntimeOrThrow()._reloadRejectedPhysicalWrite()}get _cfgWriting(){return this._writesPending>0}async _sendConfigCandidate(e){const t=so(e),i=await this.hass.callWS({type:"houseplan/config/set",config:t,expected_rev:this._cfgRev});this._cfgRev=i?.rev??this._cfgRev+1}_writeConfig(){if(this._editorRuntime)return this._editorRuntime._writeConfig();this._writesPending++,this._writeChain=Lp(this._writeChain,async()=>{if(!this._serverCfg)return;const e=so(this._serverCfg),t=Tl(e);t!==Tl(this._serverCfg)&&(this._serverCfg=e),this._cfgContentFingerprint=t,await this._sendConfigCandidate(e)});return this._writeChain.finally(()=>{this._writesPending--})}_saveConfig(){this._cfgEpoch++,this._saveConfigDebounced()}_geometrySnapshotFromConfig(e,t){return this._editorRuntimeOrThrow()._geometrySnapshotFromConfig(e,t)}_geometrySnapshot(e=this._space){return this._editorRuntimeOrThrow()._geometrySnapshot(e)}_recordGeometry(e,t){return this._editorRuntimeOrThrow()._recordGeometry(e,t)}_restoreGeometryStateInConfig(e,t,i=!1){return this._editorRuntimeOrThrow()._restoreGeometryStateInConfig(e,t,i)}_restoreGeometryStateLocal(e){return this._editorRuntimeOrThrow()._restoreGeometryStateLocal(e)}_wallModelBlockerLabel(e){return this._editorRuntimeOrThrow()._wallModelBlockerLabel(e)}_hasLegacyZeroWallFields(e=this._serverCfg){return this._editorRuntimeOrThrow()._hasLegacyZeroWallFields(e)}_showWallModelMigrationBlocked(e){return this._editorRuntimeOrThrow()._showWallModelMigrationBlocked(e)}_limitSegmentsOf(e){return this._editorRuntimeOrThrow()._limitSegmentsOf(e)}_junctionLimitViolations(e,t){return this._editorRuntimeOrThrow()._junctionLimitViolations(e,t)}_junctionLimitLabel(e){return this._editorRuntimeOrThrow()._junctionLimitLabel(e)}_junctionLimitsIntroduced(e,t,i){return this._editorRuntimeOrThrow()._junctionLimitsIntroduced(e,t,i)}_commitPhysicalGeometry(e,t,i=[]){return this._editorRuntimeOrThrow()._commitPhysicalGeometry(e,t,i)}_clearGeometryGesture(){return this._editorRuntime?this._editorRuntimeOrThrow()._clearGeometryGesture():(this._path=[],this._planSnapHover=null,this._cursorPt=null,this._openingHoverCandidate=null,this._openingJambBlockCm=null,this._openingPreset=null,this._openingRebindId=null,this._mergeSel=null,this._mergeDialog=null,this._splitSel=null,this._pendingSplit=null,this._wallFaceBatch=null,this._wallRepairDiagnostic=null,this._roomDeleteDialog=null,this._wallDialog=null,this._physicalDialog=null,this._physicalSel=null,this._physicalDrag=null,this._physicalRotate=null,this._activeDraftId=null,this._draftSegmentCms=[],this._closingWallCm=null,this._openingDialog=null,this._resize?.reset(),this._decorDraft=null,this._decorMove=null,this._dtDrag=null,void(this._bdDrag=null))}_stagePointerCancel(e){if(this._editorRuntime)return this._editorRuntime._stagePointerCancel(e)}_applyGeometryState(e,t=!1){return this._editorRuntimeOrThrow()._applyGeometryState(e,t)}_roomAt(e){return this._editorRuntimeOrThrow()._roomAt(e)}_overlapRoom(e){return this._editorRuntimeOrThrow()._overlapRoom(e)}_pointInRoom(e,t){return t.poly?si(e,t.poly):null!=t.x&&e[0]>=t.x&&e[0]<=t.x+t.w&&e[1]>=t.y&&e[1]<=t.y+t.h}_contourSelfIntersects(e){return this._editorRuntimeOrThrow()._contourSelfIntersects(e)}_canAppendRoomDraftPoint(){return this._editorRuntimeOrThrow()._canAppendRoomDraftPoint()}_markupClick(e){if(this._editorRuntime)return this._editorRuntime._markupClick(e)}_draftEndAt(e,t){return this._editorRuntimeOrThrow()._draftEndAt(e,t)}_mergeDraftEndpoint(e){return this._editorRuntimeOrThrow()._mergeDraftEndpoint(e)}_adoptDraftCms(e,t,i){return this._editorRuntimeOrThrow()._adoptDraftCms(e,t,i)}_draftSegmentsForPath(e,t,i){return this._editorRuntimeOrThrow()._draftSegmentsForPath(e,t,i)}_persistActiveDraftSegment(){return this._editorRuntimeOrThrow()._persistActiveDraftSegment()}_activeWallSourceKey(e){return this._editorRuntimeOrThrow()._activeWallSourceKey(e)}_wallGraphSources(e){return this._editorRuntimeOrThrow()._wallGraphSources(e)}_wallFaceGraph(e,t){return this._editorRuntimeOrThrow()._wallFaceGraph(e,t)}_offerWallFaces(e,t=this._path.length-2,i){return this._editorRuntimeOrThrow()._offerWallFaces(e,t,i)}_beginWallFaceBatch(e){return this._editorRuntimeOrThrow()._beginWallFaceBatch(e)}_offerExistingWallFace(e){return this._editorRuntimeOrThrow()._offerExistingWallFace(e)}_columnClick(e){return this._editorRuntimeOrThrow()._columnClick(e)}_openPhysicalDialog(e,t,i){return this._editorRuntimeOrThrow()._openPhysicalDialog(e,t,i)}_physicalDown(e,t,i){return this._editorRuntimeOrThrow()._physicalDown(e,t,i)}_clampPhysicalDelta(e,t,i){return this._editorRuntimeOrThrow()._clampPhysicalDelta(e,t,i)}_physicalMove(e){return this._editorRuntimeOrThrow()._physicalMove(e)}_physicalUp(e){return this._editorRuntimeOrThrow()._physicalUp(e)}_registerPhysicalTap(e,t,i){return this._editorRuntimeOrThrow()._registerPhysicalTap(e,t,i)}_cancelPhysicalGesture(){return this._editorRuntimeOrThrow()._cancelPhysicalGesture()}_physicalRotateDown(e,t){return this._editorRuntimeOrThrow()._physicalRotateDown(e,t)}_physicalRotateMove(e){return this._editorRuntimeOrThrow()._physicalRotateMove(e)}_physicalRotateUp(e){return this._editorRuntimeOrThrow()._physicalRotateUp(e)}_rszRooms(){return this._editorRuntimeOrThrow()._rszRooms()}_rszOpenings(){return this._editorRuntimeOrThrow()._rszOpenings()}_rszObstacles(){return this._editorRuntimeOrThrow()._rszObstacles()}_rszOptsFor(e,t){return this._editorRuntimeOrThrow()._rszOptsFor(e,t)}_rszResolution(e,t,i){return this._editorRuntimeOrThrow()._rszResolution(e,t,i)}_rszSnapshot(){return this._editorRuntimeOrThrow()._rszSnapshot()}_rszResetController(){return this._editorRuntimeOrThrow()._rszResetController()}_rszProjectPreview(e,t,i,n,r){return this._editorRuntimeOrThrow()._rszProjectPreview(e,t,i,n,r)}_rszAcceptPreview(e,t){return this._editorRuntimeOrThrow()._rszAcceptPreview(e,t)}_rszSpaceCandidateGeometry(e,t){return this._editorRuntimeOrThrow()._rszSpaceCandidateGeometry(e,t)}_rszSpaceCandidateRenderable(e,t){return this._editorRuntimeOrThrow()._rszSpaceCandidateRenderable(e,t)}_rszCandidateRenderable(e){return this._editorRuntimeOrThrow()._rszCandidateRenderable(e)}_rszEdgeDown(e,t,i){return this._editorRuntimeOrThrow()._rszEdgeDown(e,t,i)}_rszReasonText(e){return this._editorRuntimeOrThrow()._rszReasonText(e)}_rszDisabledActivate(e,t){return this._editorRuntimeOrThrow()._rszDisabledActivate(e,t)}_rszDisabledKey(e,t){return this._editorRuntimeOrThrow()._rszDisabledKey(e,t)}_rszMove(e){return this._editorRuntimeOrThrow()._rszMove(e)}_rszUp(e){return this._editorRuntimeOrThrow()._rszUp(e)}_rszCancelDrag(e){return this._editorRuntimeOrThrow()._rszCancelDrag(e)}_rszPointerCancel(e){return this._editorRuntimeOrThrow()._rszPointerCancel(e)}_rszEdgeLabels(e,t,i=this._resize?.rooms){return this._editorRuntimeOrThrow()._rszEdgeLabels(e,t,i)}_rszInnerSpanCms(e,t,i){return this._editorRuntimeOrThrow()._rszInnerSpanCms(e,t,i)}_renderResizeMeasurements(){return this._editorRuntimeOrThrow()._renderResizeMeasurements()}_renderResizeLayer(e){return this._editorRuntimeOrThrow()._renderResizeLayer(e)}get _openingsR(){const e=this._curSpaceCfg,t=this._spaceH,i=this._spaceModel();return i?(e?.openings||[]).flatMap(e=>{const n={...e,rx:e.x*cm,ry:e.y*t,rlen:e.length*cm};if(!e.host||"wall"===e.host.kind)return[n];const r=Rd(e,i.partitions,cm,this._cellCm,this._gridPitch);if(!r.resolved)return"plan"===this._mode?[{...n,orphanReason:r.reason||"invalid-host"}]:[];let[o,s]=r.resolved.center;const a=this._physicalDrag;return a?.moved&&"partition"===a.kind&&a.id===e.host.id&&(o+=a.delta[0],s+=a.delta[1]),[{...e,rx:o,ry:s,rlen:r.resolved.length,angle:r.resolved.angle,partitionHost:r.resolved}]}):[]}_partitionOpeningCuts(e=this._spaceModel(),t=()=>!0){if(!e)return[];return Fp(Pp(this._curSpaceCfg?.id===e.id?this._curSpaceCfg:null,e,this._cellCm,this._gridPitch,cm),t)}_roomWallOpeningInputs(e=this._openingsR,t=this._spaceModel()){if(!t)return[];const i=this._openCuts();return Ip(e,t,this._spaceWalls,i,this._wallKeyPitch,this._cellCm,this._gridPitch,cm)}_openingFace(e,t,i){return e.partitionHost?Od(e.partitionHost,i):Vs(t,{x:e.rx,y:e.ry,angle:e.angle,length:e.rlen,flip_v:i})}_cmToUnits(e){return e/this._cellCm*this._gridPitch}get _decorList(){const e=this._curSpaceCfg;return Array.isArray(e?.decor)?e.decor:[]}get _decorH(){return cm}_decorResolvedStyle(e){return function(e,t,i,n=ur){const r="rect"===e?.kind||"ellipse"===e?.kind,o=e,s=jt(o?.color,n.color);return{color:s,opacity:pr(o?.opacity,n.opacity),widthCm:yr(e,t,i,n.widthCm),fill:!!r&&!0===o?.fill,fillColor:jt(o?.fill_color,o?.fill?s:n.fillColor),fillOpacity:r&&o?.fill?pr(o?.fill_opacity,.25):n.fillOpacity}}(e,this._cellCm,this._gridPitch,ur)}_decorWidthUnits(e){return((e,t,i,n=ur.widthCm)=>{const r=Number(e?.width_cm);if(Number.isFinite(r)&&r>0)return fr(r,t,i);const o=Number(e?.width);return Number.isFinite(o)&&o>0?o:fr(n,t,i)})(e,this._cellCm,this._gridPitch,ur.widthCm)}_decorTextSizeCm(e){if("text"===e?.kind){const t=Number(e.size_cm);return Number.isFinite(t)&&t>0?t:vr(20*Gi(e),this._cellCm,this._gridPitch)}return vr(20,this._cellCm,this._gridPitch)}_decorTextUnits(e){if("text"!==e.kind)return 20;const t=Number(e.size_cm);return Number.isFinite(t)&&t>0?fr(t,this._cellCm,this._gridPitch):20*Gi(e)}_decorSmallField(e){return Math.round(100*(this._imperial?e/2.54:e))/100}_decorSmallCm(e){const t=this._imperial?2.54*e:e;return Number.isFinite(t)?Math.max(.1,Math.min(100,t)):.1}_decorTextCm(e){const t=this._imperial?2.54*e:e;return Number.isFinite(t)?Math.max(.1,Math.min(2e3,t)):.1}_decorLargeField(e){return Math.round(100*(this._imperial?e/30.48:e/100))/100}_decorLargeCm(e){const t=this._imperial?30.48*e:100*e;return Number.isFinite(t)?Math.max(.1,Math.min(Pa*this._cellCm,t)):.1}_angleField(e){const t=Number(e);return Number.isFinite(t)?String(Number(t.toFixed(3))):"0"}_decorBoxOf(e){return"rect"!==e.kind&&"ellipse"!==e.kind&&"furniture"!==e.kind?null:{x:e.x*cm,y:e.y*this._decorH,w:e.w*cm,h:e.h*this._decorH,angle:_r(e.angle)||void 0}}_decorSnapGeometry(e){return this._editorRuntimeOrThrow()._decorSnapGeometry(e)}_decorSnap(e,t="mouse",i){return this._editorRuntimeOrThrow()._decorSnap(e,t,i)}_replaceDecor(e,t){return this._editorRuntimeOrThrow()._replaceDecor(e,t)}_cancelDecorGesture(){return this._editorRuntimeOrThrow()._cancelDecorGesture()}_decorPointerDown(e){return this._editorRuntimeOrThrow()._decorPointerDown(e)}_decorCommitDraft(){return this._editorRuntimeOrThrow()._decorCommitDraft()}_decorShapeDown(e,t){return this._editorRuntimeOrThrow()._decorShapeDown(e,t)}_decorMoveUpdate(e){return this._editorRuntimeOrThrow()._decorMoveUpdate(e)}_decorShapeDbl(e,t){if(this._editorRuntime)return this._editorRuntime._decorShapeDbl(e,t)}_openDecorProperties(e){return this._editorRuntimeOrThrow()._openDecorProperties(e)}_decorOpenText(e){return this._editorRuntimeOrThrow()._decorOpenText(e)}_decorRememberTextSelection(e){return this._editorRuntimeOrThrow()._decorRememberTextSelection(e)}_decorInsertLiveVariable(e){return this._editorRuntimeOrThrow()._decorInsertLiveVariable(e)}_decorSaveText(){return this._editorRuntimeOrThrow()._decorSaveText()}_decorSaveShape(){return this._editorRuntimeOrThrow()._decorSaveShape()}get _dtSel(){return"decor"===this._mode&&"select"===this._decorTool&&this._decorSel&&this._decorList.find(e=>e.id===this._decorSel)||null}_dtPivot(e){return this._editorRuntimeOrThrow()._dtPivot(e)}_dtApply(e,t){return this._editorRuntimeOrThrow()._dtApply(e,t)}_dtStart(e,t,i,n){return this._editorRuntimeOrThrow()._dtStart(e,t,i,n)}_dtMove(e){return this._editorRuntimeOrThrow()._dtMove(e)}_dtUp(){return this._editorRuntimeOrThrow()._dtUp()}_dtMeasure(){return this._editorRuntimeOrThrow()._dtMeasure()}_deleteDecor(e){return this._editorRuntimeOrThrow()._deleteDecor(e)}_decorDeleteSel(){return this._editorRuntimeOrThrow()._decorDeleteSel()}_confirmDecorErase(){return this._editorRuntimeOrThrow()._confirmDecorErase()}get _furnWalls(){const e=this._rawPhysicalBodiesR().flatMap(e=>e.map((t,i)=>{const n=e[(i+1)%e.length];return[t[0],t[1],n[0],n[1]]}));return[...this._segments,...e]}get _furnWallReach(){return 6*this._gridPitch}_furnFieldValue(e){return this._editorRuntimeOrThrow()._furnFieldValue(e)}_furnFieldToCm(e){return this._editorRuntimeOrThrow()._furnFieldToCm(e)}_furnPick(e){return this._editorRuntimeOrThrow()._furnPick(e)}_furnPlace(e,t=!1){return this._editorRuntimeOrThrow()._furnPlace(e,t)}_furnMoveUpdate(e){return this._editorRuntimeOrThrow()._furnMoveUpdate(e)}_decorApplyBox(e,t){return this._editorRuntimeOrThrow()._decorApplyBox(e,t)}get _furnLive(){const e=this._dtDrag;if(!e||"scale"!==e.kind||!e.orig)return null;const t=this._decorList.find(t=>t.id===e.id);if(!t||"furniture"!==t.kind)return null;const i=cm,n=this._decorH,r=t.w*i,o=t.h*n,s=function(e,t,i,n,r){const o=e+i/2,s=t+n/2,a=(Number(r)||0)*Math.PI/180,l=Math.cos(a),c=Math.sin(a),h=(e,t)=>{const i=e-o,n=t-s;return[o+i*l-n*c,s+i*c+n*l]};return[h(e,t),h(e+i,t),h(e+i,t+n),h(e,t+n)]}(t.x*i,t.y*n,r,o,Number(t.angle)||0),a=(e,t)=>[(e[0]+t[0])/2,(e[1]+t[1])/2],l=a(s[0],s[1]),c=a(s[0],s[3]);return[{x:l[0],y:l[1],text:this._fmtLen([0,0],[r,0])},{x:c[0],y:c[1],text:this._fmtLen([0,0],[0,o])}]}_renderFurnPalette(){return this._editorRuntimeOrThrow()._renderFurnPalette()}get _bdBase(){const e=this._curSpaceCfg;return e?.plan_url?{...Ra(e.plan_aspect,cm)}:null}get _bdRect(){const e=this._curSpaceCfg;return e?.plan_url?Aa(e,cm):null}get _bdParams(){const e=this._curSpaceCfg,t=Number(e?.plan_x),i=Number(e?.plan_y),n=Number(e?.plan_scale),r=Number(e?.plan_scale_x),o=Number(e?.plan_scale_y),s=Number.isFinite(n)&&n>0?n:1;return{dx:Number.isFinite(t)?t:0,dy:Number.isFinite(i)?i:0,sx:Number.isFinite(r)&&r>0?r:s,sy:Number.isFinite(o)&&o>0?o:s,angle:_r(e?.plan_angle)}}_openBackdropDialog(e){if(this._editorRuntime)return this._editorRuntime._openBackdropDialog(e)}_saveBackdropDialog(){return this._editorRuntimeOrThrow()._saveBackdropDialog()}get _bdActive(){return"decor"===this._mode&&!!this._bdRect&&"backdrop"===this._decorTool}get _bdMovable(){return"decor"===this._mode&&"backdrop"===this._decorTool&&!!this._bdRect}_bdApply(e,t,i,n,r){return this._editorRuntimeOrThrow()._bdApply(e,t,i,n,r)}_bdStart(e,t,i=!1){return this._editorRuntimeOrThrow()._bdStart(e,t,i)}_bdMove(e){return this._editorRuntimeOrThrow()._bdMove(e)}get _bdMoved(){if("decor"!==this._mode||!this._bdRect)return!1;const e=this._bdParams;return 0!==e.dx||0!==e.dy||1!==e.sx||1!==e.sy||0!==e.angle}_bdReset(){return this._editorRuntimeOrThrow()._bdReset()}_bdUp(){return this._editorRuntimeOrThrow()._bdUp()}get _bdLive(){if(!this._bdDrag)return null;const e=this._bdRect;return e?{x:e.x+e.w/2,y:e.y+e.h/2,text:`${this._fmtLen([0,0],[e.w,0])} × ${this._fmtLen([0,0],[0,e.h])}`}:null}_renderBackdropFrame(e){return this._editorRuntime?this._editorRuntimeOrThrow()._renderBackdropFrame(e):G}_renderTextFrame(e){const t=this._dtSel,i=this._dtBox;if(!t||!i||i.id!==t.id)return G;const n=.018*Math.max(e.w,e.h),r=n/4;if("line"===t.kind){const e=[t.x1*cm,t.y1*this._decorH],i=[t.x2*cm,t.y2*this._decorH];return W` + + ${[e,i].map((e,t)=>W`this._dtStart(e,"scale",void 0,t)}> + `)} + `}const[o,s]=this._dtPivot(t),a=Number(t.angle)||0,l=2.2*n;return W` + + + this._dtStart(e,"rotate")}> + + ${[[-1,-1,"nwse"],[1,-1,"nesw"],[1,1,"nwse"],[-1,1,"nesw"]].map(([e,t,o])=>W`this._dtStart(i,"scale",[e,t])}>`)} + `}_renderDecorLayer(){const e=cm,t=this._decorH,i="decor"===this._mode,n=i&&"erase"===this._decorTool,r=this._decorList.map(r=>{const o="dshape"+(i&&this._decorSel===r.id?" dsel":""),s=this._decorResolvedStyle(r),a=this._decorWidthUnits(r),l=e=>this._decorShapeDown(e,r),c=e=>this._decorShapeDbl(e,r);if("line"===r.kind)return W` + ${i&&"select"===this._decorTool?W``:G} + ${n?W``:G}`;if("rect"===r.kind){const i=(r.x+r.w/2)*e,h=(r.y+r.h/2)*t,d=_r(r.angle);return W` + ${n?W``:G}`}if("ellipse"===r.kind){const i=(r.x+r.w/2)*e,h=(r.y+r.h/2)*t,d=_r(r.angle);return W` + ${n?W``:G}`}if("furniture"===r.kind){const i=r.w*e,h=r.h*t,d=vl(r.symbol,i,h);if(!d)return G;const u=Number(r.angle)||0,p=r.x*e+i/2,_=r.y*t+h/2,m=`${u?`rotate(${u} ${p} ${_}) `:""}translate(${r.x*e} ${r.y*t})`;return W` + ${n?W``:G}`}if("text"===r.kind){const i=this._decorTextUnits(r),n=this._renderDeviceSnapshot?.facts.get(`decor:${this._space}:${r.id}`),a=function(e){return String(e??"").replace(/\r\n?/g,"\n").split("\n")}("string"==typeof n?n:Wi(r.text,r,this._renderPlanHass,e=>this._renderEntityAvailable(e))),h=r.x*e,d=r.y*t,u=Number(r.angle)||0,p=d-(a.length-1)*i*1.2/2;return W`${a.map((e,t)=>W`${e}`)}`}return G});let o=G;const s=this._decorDraft;if(s){const e=this._decorStyle,t=fr(e.widthCm,this._cellCm,this._gridPitch);if("line"===s.kind)o=W``;else{const i=Math.min(s.a[0],s.b[0]),n=Math.min(s.a[1],s.b[1]),r=Math.abs(s.b[0]-s.a[0]),a=Math.abs(s.b[1]-s.a[1]);o="rect"===s.kind?W``:W``}}return W`${r}${o}`}get _editorToolbarGroups(){return"plan"!==this._mode?[]:[{id:"opening",label:this._t("markup.opening"),icon:"mdi:door",activeItemId:"opening"===this._tool?this._openingPreset?.type:void 0,items:[{id:"window",label:this._t("opening.window"),icon:"mdi:window-closed-variant",role:"tool",invoke:()=>this._activateOpeningPlacement("window")},{id:"door",label:this._t("opening.door"),icon:"mdi:door-open",role:"tool",invoke:()=>this._activateOpeningPlacement("door")},{id:"passage",label:this._t("opening.passage"),icon:"mdi:arch",role:"tool",invoke:()=>this._activateOpeningPlacement("passage")},{id:"gate",label:this._t("opening.gate"),icon:"mdi:gate",role:"tool",invoke:()=>this._activateOpeningPlacement("gate")}]}]}_renderEditorGroupLauncher(e){return this._editorRuntimeOrThrow()._renderEditorGroupLauncher(e)}get _editorSecondaryContextId(){const e=`editor:${this._mode}:${this._space}:${this._cfgEpoch}`,t=this._editorSecondary?.activeGroup(this._editorToolbarGroups);if(t)return`${e}:group:${t.id}:${this._editorSecondary?.groupGeneration}`;if("plan"===this._mode){const t=this._physicalSel;return t?`${e}:selection:${t.kind}:${t.id}:${t.segment??""}`:`${e}:tool:${this._tool}:${this._path.length}`}return"decor"===this._mode?"furniture"===this._decorTool?`${e}:palette:furniture:${this._furnPalette?.symbol||"none"}`:"select"===this._decorTool&&this._decorSel?`${e}:selection:decor:${this._decorSel}`:`${e}:tool:${this._decorTool}:${this._bdMoved?1:0}:${this._bdDrag?1:0}`:`${e}:none`}_runEditorContext(e,t){return this._editorRuntimeOrThrow()._runEditorContext(e,t)}_renderEditorGroupModel(e){return this._editorRuntimeOrThrow()._renderEditorGroupModel(e)}_renderDrawWallControl(){return this._editorRuntimeOrThrow()._renderDrawWallControl()}_renderPlanSecondary(){return this._editorRuntimeOrThrow()._renderPlanSecondary()}_renderDecorSecondary(){return this._editorRuntimeOrThrow()._renderDecorSecondary()}_withBackdropReset(e){return this._editorRuntimeOrThrow()._withBackdropReset(e)}get _editorSecondaryDialogBlocked(){return!!(this._tapConfirm||this._vacCalConfirm||this._roomDialog||this._mergeDialog||this._openingDialog||this._physicalDialog||this._openingInfo||this._decorTextDialog||this._decorShapeDialog||this._backdropDialog||this._decorEraseConfirm||this._spaceDialog||this._markerDialog||this._deviceInbox||this._infoCard||this._rulesDialog||this._settingsDialog||this._alignDialog||this._importDialog||this._kioskDialog||this._backupExportDialog||this._backupImportDialog||this._wallDialog)}_renderEditorSecondary(){return this._editorRuntimeOrThrow()._renderEditorSecondary()}_renderDecorBar(){return this._editorRuntimeOrThrow()._renderDecorBar()}_renderDecorEraseConfirm(){return this._editorRuntimeOrThrow()._renderDecorEraseConfirm()}_renderDecorTextDialog(){return this._editorRuntimeOrThrow()._renderDecorTextDialog()}_renderDecorShapeDialog(){return this._editorRuntimeOrThrow()._renderDecorShapeDialog()}_renderBackdropDialog(){return this._editorRuntimeOrThrow()._renderBackdropDialog()}_cssPxToRender(e){return this._editorRuntimeOrThrow()._cssPxToRender(e)}_renderZeroWalls(e){if(e&&!e.showBorders&&!this._editing)return W``;const t=this._zeroWalls();if(!t.lines.length)return W``;const i=e?.color||"var(--hp-muted)";return W` + ${t.lines.map(e=>W``)} + `}_zeroWalls(){const e=this._spaceModel();return e?op(this._curSpaceCfg,e,cm,.02*this._gridPitch):{style:tp(this._curSpaceCfg),lines:[],contour:[],barriers:[],transmissive:[]}}_openCuts(){return this._zeroWalls().contour}_deleteRoomClick(e){return this._editorRuntimeOrThrow()._deleteRoomClick(e)}get _wallKeyPitch(){return Na}get _spaceWalls(){const e=this._curSpaceCfg?.walls;return Array.isArray(e)?e:[]}_cfgOpenCuts(){const e=this._curSpaceCfg,t=this._spaceModel();if(!e||!t)return[];const i=ip(e,1),n=rp(e,t.rooms,cm,.02*this._gridPitch).map(e=>e.map(e=>e/cm)),r=new Set;return[...i,...n].filter(e=>{const t=e.map(e=>Number(e).toFixed(9)).join(","),i=[e[2],e[3],e[0],e[1]].map(e=>Number(e).toFixed(9)).join(","),n=t[e[0]/cm,e[1]/cm,e[2]/cm,e[3]/cm]))}_paperShapes(e){if(!this._spaceWalls.length)return Xt(e);const t=this._wallUnionGeometry();return t?.paperD?[{path:t.paperD}]:Xt(e)}_wallUnionGeometry(){const e=this._spaceModel();if(!e)return null;const t=this._spaceWalls,i=this._physicalBodiesR();if(!t.length&&!i.length)return null;const n=`${this._space}|${this._cfgEpoch}|${e.rooms.length}`;if(!this._wallUnionCache||this._wallUnionCache.key!==n){const r=X_(this._wallUnionPool,n);if(r.hit)this._wallUnionCache=r.value;else{const r=this._openCuts(),o=this._roomWallOpeningInputs(),s=Hs(e.rooms,t,r,o,this._wallKeyPitch,this._cellCm,this._gridPitch,cm,i);s&&Object.defineProperty(s,"sourceFingerprint",{value:Tl([this._curSpaceCfg,this._cellCm,this._gridPitch]),enumerable:!1});const a={key:n,value:s};J_(this._wallUnionPool,n,a,8),this._wallUnionCache=a}}return this._wallUnionCache.value}_thickWallCuts(){const e=this._spaceModel();if(!e)return[];const t=this._spaceWalls;if(!t.length)return[];const i=this._openCuts();return js(e.rooms,t,i,this._wallKeyPitch,this._cellCm,this._gridPitch,cm).map(e=>[e.a[0],e.a[1],e.b[0],e.b[1]])}_innerRoomContour(e,t,i=this._openCuts(),n=this._wallUnionGeometry()?.roomGeom,r=this._wallUnionGeometry()?.multiWallNodes){const o=i.map(e=>e.join(",")).join(";"),s=`${e.id}|${this._cfgEpoch}|${t}|${o}`,a=X_(this._innerContourCache,s);if(a.hit)return a.value;const l=ks(e.rooms,t,this._spaceWalls,i,this._wallKeyPitch,this._cellCm,this._gridPitch,cm,n,r);return J_(this._innerContourCache,s,l,600),l}_wallThickHit(e){return this._editorRuntimeOrThrow()._wallThickHit(e)}get _wallThickHover(){if(!this._markup||"wallthick"!==this._tool||!this._cursorPt||this._wallDialog)return null;const e=this._wallThickHit(this._cursorPt);if(!e)return null;const t=function(e,t,i){const n=Number.isFinite(e)&&e>0?e:0,r=Number.isFinite(t)&&t>0?t:5,o=Number.isFinite(i)&&i>0?i:0;return n>0?vo(n,r,o)/2:xa(1.5*o,r)}(e.cm,this._cellCm,this._gridPitch);let i="";for(const n of e.segs)i+=(i?" ":"")+Ko([[n[0],n[1]],[n[2],n[3]]],t,!1);return{segs:e.segs,open:e.open,d:i}}_wallThickClick(e){return this._editorRuntimeOrThrow()._wallThickClick(e)}_wallThickApply(e){return this._editorRuntimeOrThrow()._wallThickApply(e)}_wallHatchDefs(e){const t=_o(this._cellCm);return W` + + + + `}_resolvedRoomFills(e,t){const i=new Map,n=new Map;for(const r of e.rooms){const e=this._roomDialog&&r.id===this._roomEditId?this._roomFill||t.fill:Tn(t.fill,r),o=this._roomDialog&&r.id===this._roomEditId?this._roomCustomFill||t.customFill:en(t.customFill,r),s=an(e,"lqi"===e&&r.area?this._roomLqi(r.area):null,"light"===e?Rh(Mh(this._renderPlanHass,this._renderDevices,r,this._virtualLights)):"none","temp"===e?this._roomTemp(r):null,t.tempMin,t.tempMax,this._fillColors,o);i.set(r,s),r.id&&n.set(r.id,s)}return{byRoom:i,byId:n}}_spaceDisplayForRender(){const e=tn(this._curSpaceCfg),t=this._spaceDialog;return t&&"edit"===t.mode&&t.spaceId===this._space?{...e,showBorders:t.showBorders,showNames:t.showNames,hideDecor:t.hideDecor,hideOpenings:t.hideOpenings,color:t.roomColor,opacity:t.roomOpacity,fill:t.fillMode,customFill:t.customFill?Qi(t.customFill):Ji,glow:t.glowEnabled,tempMin:t.tempMin,tempMax:t.tempMax,showLqi:t.showLqi,cardFontScale:t.cardFontScale,labelTemp:t.labelTemp,labelHum:t.labelHum,labelLqi:t.labelLqi,labelLight:t.labelLight}:e}_openingWallIndexFor(e,t){const i=e.rooms.map(e=>`${e.id}:${e.poly?.map(e=>e.join(",")).join("/")||`${e.x},${e.y},${e.w},${e.h}`}`).join(";"),n=this._spaceWalls.map(e=>`${e.key}:${e.a?.join(",")||""}:${e.b?.join(",")||""}:${e.cm}`).join(";"),r=t.map(e=>e.join(",")).join(";"),o=[e.id,this._cfgEpoch,this._wallKeyPitch,this._cellCm,this._gridPitch,i,n,r].join("|");let s=this._openingWallIndexCache.get(o);return s||(s=Bs(e.rooms,this._spaceWalls,t,this._wallKeyPitch,this._cellCm,this._gridPitch,cm)),J_(this._openingWallIndexCache,o,s,4),{key:o,value:s}}_renderOpeningTunnelFills(e,t,i="data"){if(this._markup||!this._spaceWalls.length||!this._openingsR.length)return W``;if("glow-base"===i&&![...t.byRoom.values()].some(Boolean))return W``;const n=this._openCuts(),r=this._openingsR.map(e=>({x:e.rx,y:e.ry,angle:e.angle,length:e.rlen})),o=r.map(e=>`${e.x},${e.y},${e.angle},${e.length}`).join(";"),s=this._openingWallIndexFor(e,n),a=`${s.key}|${o}`;return this._openingTunnelCache&&this._openingTunnelCache.key===a||(this._openingTunnelCache={key:a,value:Zs(s.value,r)}),Wu({openings:this._openingsR,geometries:this._openingTunnelCache.value,fillsByRoomId:t.byId,idPrefix:`${e.id}-${i}`,groupClass:"data"===i?"opening-tunnels":"opening-tunnels glow-base-tunnels",dataLayer:i})}_resolvedGlowBase(e,t,i){const n=new Map,r=new Map,o=this._fillColors.glow_base;for(const s of e.rooms){const e=i.byRoom.get(s),a=Rn(t.glow,s)&&(!e||e.opacity<=0)?{color:o.c,opacity:o.a,mode:"glow"}:null;n.set(s,a),s.id&&r.set(s.id,a)}return{byRoom:n,byId:r}}_renderGlowBaseRooms(e,t){if(this._markup||![...t.byRoom.values()].some(Boolean))return W``;const i=new Map(e.rooms.map(e=>[e,Zt(e)])),n=this._openCuts(),r=this._wallUnionGeometry()?.roomGeom,o=e=>"M "+e.map(e=>e[0]+" "+e[1]).join(" L ")+" Z",s=e.rooms.map(s=>{const a=t.byRoom.get(s)||null,l=i.get(s)||null;if(!a||!l)return G;const c=this._spaceWalls.length&&s.id&&this._innerRoomContour(e,s.id,n,r)||l,h=vi(c,e.rooms.filter(e=>e!==s).map(e=>i.get(e)).filter(e=>!!e)),d=this._cleanFloor(s,c,e).path;return d||h.length?W``:s.poly||c!==l?W``:W``});return W``}_renderWallBodies(e){if("iso"===this._renderProjection)return W``;if(e&&!e.showBorders&&("view"===this._mode||"devices"===this._mode))return W``;const t=this._wallUnionGeometry();if(!t)return W``;const i=this._stageEl,n=this._viewOr(this._baseVb()),r=i&&i.clientWidth&&n.w?i.clientWidth/n.w:1,o=e?.color||"#607d8b",s=po(t.depthUnits,r)||mo(_o(this._cellCm),r),a=this._fillColors.wall_fill;return W` + ${t.paths.map(e=>W` + + `)} + `}_isoDecorationLayers(e){const t="undefined"==typeof CSS||"function"!=typeof CSS.supports||CSS.supports("filter","blur(1px)"),i="function"==typeof matchMedia&&matchMedia("(forced-colors: active)").matches;return function(e){const t=!!e.showBorders;return{structural:t,panels:t&&!e.hideOpenings,shadows:t&&e.filtersSupported&&!e.forcedColors,materialNuance:t&&!e.forcedColors,floorSymbols:!e.hideOpenings&&!t}}({showBorders:e.showBorders,hideOpenings:e.hideOpenings,filtersSupported:t,forcedColors:i})}_isoOpeningPanels(e){if(!e.panels)return[];const t=this._isoScene();if(!t)return[];return t.openings.flatMap(e=>{const t=this._openingsR[e.sourceIndex];return t?E_(e,this._openingAmt(t)):[]}).sort((e,t)=>e.depth-t.depth||e.sourceIndex-t.sourceIndex||e.leaf-t.leaf)}_renderIsoDefs(e,t){const i=ka(this._cellCm);return W` + ${"walls"===t&&e.materialNuance?W` + + + + + + `:G} + ${"underlay"===t&&e.shadows?W` + + + `:G} + ${"shadows"===t&&e.shadows?W` + + + + + + `:G} + `}_renderIsoUnderlay(e){if(!e.structural)return W``;const t=this._isoScene()?.floor;return t?W``:W``}_renderIsoShadows(e,t){if(!e.shadows)return W``;const i=this._isoScene()?.geometry;return i?W``:W``}_renderIsoWalls(e,t){if("iso"!==this._renderProjection)return W``;if(!e.structural)return W``;const i=this._isoScene();return i?W` + ${this._renderIsoDefs(e,"walls")} + ${i.geometry.sides.map(e=>W``)} + + ${e.panels?W``:G} + `:W``}_roomHoverPaths(e){const t=this._hoverRoom;if("view"!==this._mode||!t||t.space!==e.id)return null;const i=e.rooms.find(e=>e===t.room||!!e.id&&e.id===t.room.id);if(!i)return null;const n=Zt(i);if(!n)return null;const r=e.rooms.filter(e=>e!==i).map(e=>({room:e,poly:Zt(e)})).filter(e=>!!e.poly),o=vi(n,r.map(e=>e.poly)),s=this._openCuts(),a=.02*this._gridPitch,l=s.filter(e=>{const t=[(e[0]+e[2])/2,(e[1]+e[3])/2];return n.some((e,i)=>Pn(t,[e[0],e[1],...n[(i+1)%n.length]])<=4*a)}),c=this._spaceWalls,h=this._wallUnionGeometry()?.roomGeom,d=c.length&&i.id&&this._innerRoomContour(e,i.id,s,h)||n,u=[{axis:n,face:d}];for(const t of o){const i=r.find(e=>e.poly===t)?.room;let n=t;if(c.length&&i?.id){const r=ms(e.rooms,i.id,c,s,this._wallKeyPitch,this._cellCm,this._gridPitch,cm);r&&(n=qs(r.poly,r.offsets)||t)}u.push({axis:t,face:n})}const p=this._roomWallOpeningInputs(this._openingsR,e).map(e=>{const t=e.angle*Math.PI/180,i=Math.cos(t)*e.length/2,n=Math.sin(t)*e.length/2;return[e.x-i,e.y-n,e.x+i,e.y+n]}),_=l.concat(p),m=u.map(({axis:e,face:t})=>{const i=_.map(i=>((e,t,i)=>{const n=e[2]-e[0],r=e[3]-e[1],o=Math.hypot(n,r);if(o.05)&&Pn([c,h],[i[0],i[1],n[0],n[1]])<=4*a){d=!0;break}}if(!d)return null;let u=null;for(let e=0;e.05)continue;const p=Pn([c,h],[t[0],t[1],n[0],n[1]]);(!u||p!!e);return i.length?kn(t,i,a).map(e=>`M ${e[0]} ${e[1]} L ${e[2]} ${e[3]}`).join(" "):`M ${t.map(e=>`${e[0]} ${e[1]}`).join(" L ")} Z`}).filter(Boolean).join(" ");if(!m)return null;const g=e=>`M ${e.map(e=>`${e[0]} ${e[1]}`).join(" L ")} Z`;return{fillD:[this._cleanFloor(i,d,e).path||g(d),...o.map(g)].join(" "),outlineD:m}}_renderRoomHoverFill(e){return e?W` + + `:W``}_renderRoomHoverOutline(e){return e?W` + + + `:W``}_renderWallThickUi(){return this._editorRuntime?this._editorRuntimeOrThrow()._renderWallThickUi():W``}_renderWallThickDialog(){return this._editorRuntimeOrThrow()._renderWallThickDialog()}_openingAt(e){return this._editorRuntimeOrThrow()._openingAt(e)}_resolveOpeningPlacement(e){return this._editorRuntimeOrThrow()._resolveOpeningPlacement(e)}_activateOpeningPlacement(e){return this._editorRuntimeOrThrow()._activateOpeningPlacement(e)}_clearOpeningPlacement(e){return this._editorRuntime?this._editorRuntimeOrThrow()._clearOpeningPlacement(e):(this._openingHoverCandidate=null,this._openingJambBlockCm=null,void(e&&(this._openingPreset=null,this._openingRebindId=null)))}_openingClick(e){return this._editorRuntimeOrThrow()._openingClick(e)}_editOpening(e){if(this._editorRuntime)return this._editorRuntimeOrThrow()._editOpening(e)}_opPointerDown(e,t){if(this._editorRuntime)return this._editorRuntimeOrThrow()._opPointerDown(e,t)}_opPointerMove(e,t){if(this._editorRuntime)return this._editorRuntimeOrThrow()._opPointerMove(e,t)}_opRuler(e,t){return this._editorRuntimeOrThrow()._opRuler(e,t)}_opPointerUp(e,t){if(this._editorRuntime)return this._editorRuntimeOrThrow()._opPointerUp(e,t)}_opClick(e,t){if(this._editorRuntime)return this._editorRuntimeOrThrow()._opClick(e,t)}_saveOpening(){return this._editorRuntimeOrThrow()._saveOpening()}_deleteOpening(){return this._editorRuntimeOrThrow()._deleteOpening()}_contactCandidates(){return this._editorRuntimeOrThrow()._contactCandidates()}_lockCandidates(){return this._editorRuntimeOrThrow()._lockCandidates()}_toggleOpeningEntityPicker(e){return this._editorRuntimeOrThrow()._toggleOpeningEntityPicker(e)}_filterOpeningEntities(e,t){return this._editorRuntimeOrThrow()._filterOpeningEntities(e,t)}_selectOpeningEntity(e,t){return this._editorRuntimeOrThrow()._selectOpeningEntity(e,t)}_mergeClick(e){return this._editorRuntimeOrThrow()._mergeClick(e)}_commitMerge(){return this._editorRuntimeOrThrow()._commitMerge()}_splitClick(e){return this._editorRuntimeOrThrow()._splitClick(e)}get _contourClosed(){return this._path.length>=4&&this._samePt(this._path[0],this._path[this._path.length-1])&&(null!=this._closingWallCm||!this._activeDraftId)}_markupMove(e){if(this._markup&&this._editorRuntime)return this._editorRuntime._markupMove(e)}get _openingPreview(){const e=this._openingPreset;if("opening"!==this._tool||!e||!this._cursorPt)return null;const t=this._cursorPt,i=this._openingHoverCandidate;if(i&&G_(i,[t[0],t[1]],e.revision,this._cfgEpoch))return i;if(this._openingAt(t))return this._openingHoverCandidate=null,null;const n=this._resolveOpeningPlacement(t);return this._openingHoverCandidate=n,n}get _opMeasureView(){return this._opMeasure||this._openingPreview?.measure||null}_saveRoom(){return this._editorRuntimeOrThrow()._saveRoom()}_decideWallFace(e){return this._editorRuntimeOrThrow()._decideWallFace(e)}_wallSourceCmAt(e,t,i){return this._editorRuntimeOrThrow()._wallSourceCmAt(e,t,i)}_activePathWithRepair(e,t){return this._editorRuntimeOrThrow()._activePathWithRepair(e,t)}_validateWallRepair(e,t){return this._editorRuntimeOrThrow()._validateWallRepair(e,t)}_applyWallRepair(e,t){return this._editorRuntimeOrThrow()._applyWallRepair(e,t)}_applyWallFaceBatch(){return this._editorRuntimeOrThrow()._applyWallFaceBatch()}_commitRoom(){return this._editorRuntimeOrThrow()._commitRoom()}_cancelPath(){return this._editorRuntimeOrThrow()._cancelPath()}_resumeLastDraft(){return this._editorRuntimeOrThrow()._resumeLastDraft()}_roomDialogCancel(){return this._editorRuntimeOrThrow()._roomDialogCancel()}get _freeAreas(){const e=new Set;for(const t of this._serverCfg?.spaces||[])for(const i of t.rooms||[])i.area&&e.add(i.area);for(const t of this._wallFaceBatch?.decisions||[])t.create&&t.area&&e.add(t.area);return Object.values(this.hass?.areas||{}).filter(t=>!e.has(t.area_id)).sort((e,t)=>(e.name||"").localeCompare(t.name||""))}_openDeviceInbox(){return this._editorRuntimeOrThrow()._openDeviceInbox()}_closeMarkerDialog(){return this._editorRuntimeOrThrow()._closeMarkerDialog()}_deviceInboxCandidates(e){return this._editorRuntimeOrThrow()._deviceInboxCandidates(e)}_deviceInboxRows(){return this._editorRuntimeOrThrow()._deviceInboxRows()}_deviceForInboxRow(e){return this._editorRuntimeOrThrow()._deviceForInboxRow(e)}_openInboxMarker(e,t=!1){return this._editorRuntimeOrThrow()._openInboxMarker(e,t)}async _setInboxHidden(e,t){return this._editorRuntimeOrThrow()._setInboxHidden(e,t)}_findInboxDevice(e){return this._editorRuntimeOrThrow()._findInboxDevice(e)}_deviceInboxTabKey(e){return this._editorRuntimeOrThrow()._deviceInboxTabKey(e)}_openMarkerDialog(e){if(this._editorRuntime)return this._editorRuntime._openMarkerDialog(e);this._ensureEditorRuntime().then(t=>{t&&this._openMarkerDialog(e)})}_runCandidates(){return this._editorRuntimeOrThrow()._runCandidates()}_bindingCandidates(){return this._editorRuntimeOrThrow()._bindingCandidates()}_physicalBodiesR(e=this._spaceModel()){if(!e)return[];const t=`${e.id}|${this._cfgEpoch}|${this._cellCm}|${this._gridPitch}`;if(this._physicalBodiesCache?.key===t)return this._physicalBodiesCache.all;const i=ca(e,this._cellCm,this._gridPitch,2e-4*this._gridPitch,this._partitionOpeningCuts(e));return this._physicalBodiesCache={key:t,...i},i.all}_rawPhysicalBodiesR(e=this._spaceModel()){if(!e)return[];this._physicalBodiesR(e);const t=this._physicalBodiesCache;return t?[...t.drafts,...t.partitions,...t.columns]:[]}_cleanFloor(e,t,i=this._spaceModel()){if(!i)return{floor:t,geom:null,path:"",area:ma([[[...t,t[0]]]])};const n=e.id||`#${i.rooms.indexOf(e)}`,r=`${i.id}|${this._cfgEpoch}|${n}`;if(!this._resize?.preview){const e=X_(this._cleanFloorCache,r);if(e.hit)return e.value}const o=t.map(e=>e[0]),s=t.map(e=>e[1]),a=[Math.min(...o),Math.min(...s),Math.max(...o),Math.max(...s)],l=this._physicalBodiesR(i).filter(e=>{const t=e.map(e=>e[0]),i=e.map(e=>e[1]);return Math.max(...t)>=a[0]&&Math.min(...t)<=a[2]&&Math.max(...i)>=a[1]&&Math.min(...i)<=a[3]}),c=l.length?_a(t,l):null,h={floor:t,geom:c,path:c?ra(c):"",area:ma(c||[[[...t,t[0]]]])};return this._resize?.preview||J_(this._cleanFloorCache,r,h,600),h}_autoIconForBinding(e){if("virtual"===e)return"mdi:map-marker";const[t,i]=e.split(":");if(!i)return"";const n=this._fullRegistryHass,r=this._bindingStatus(e),o="active"===r.kind?r.enabledEntityIds:r.allEntityIds;if("device"===t){const e=n.devices?.[i];if(!e)return"mdi:help-circle";const t=o;return t.some(e=>e.startsWith("lock."))?"mdi:lock":Fh(n,e.name_by_user||e.name||"",e.model,t,this._iconRules)}if("entity"===t){const e=n.entities?.[i],t=this.hass.states?.[i],r=e?.name||t?.attributes?.friendly_name||i;return i.startsWith("lock.")?"mdi:lock":Fh(n,r,"",[i],this._iconRules)}return""}_allRoomsFlat(){const e=[];for(const t of this._serverCfg?.spaces||[])for(const i of t.rooms||[])i.area?e.push({value:t.id+"#"+i.area,label:(t.title||t.id)+" · "+i.name}):i.id&&e.push({value:t.id+"#@"+i.id,label:(t.title||t.id)+" · "+i.name+" · "+this._t("marker.subarea")});return e}_errText(e){if(!e)return this._t("err.unknown");if("string"==typeof e)return e;if("invalid_passage_fields"===e.code){const t=String(e.message||e.error||"").match(/space=([^;]*);\s*opening=([^;]*);\s*fields=([^;]*)/);if(t){const e=this._serverCfg?.spaces?.find(e=>String(e.id)===t[1]),i={contact:"opening.contact_label",lock:"opening.lock_label",invert:"opening.invert",flip_h:"opening.flip_h",flip_v:"opening.flip_v"},n=t[3].split(",").filter(Boolean).map(e=>i[e]?this._t(i[e]):e).join(", ");return this._t("opening.invalid_passage_fields",{room:e?.title||t[1],fields:n})}}if("invalid_partition_opening_jamb_margin"===e.code){const t=String(e.message||e.error||"").match(/margin_cm=([^;]*)/),i=t?Number(t[1]):NaN;if(Number.isFinite(i))return this._t("opening.partition_jamb_margin",{distance:Kt(i,this._imperial)})}if(e.message)return e.message;if(e.error)return e.error;if(null!=e.code)return this._t("err.code",{code:e.code});try{return JSON.stringify(e)}catch{return String(e)}}_backupErrorText(e){return this._editorRuntimeOrThrow()._backupErrorText(e)}async _pickMarkerFiles(e){return this._editorRuntimeOrThrow()._pickMarkerFiles(e)}_removeMarkerPdf(e){return this._editorRuntimeOrThrow()._removeMarkerPdf(e)}_markerLightFields(e){return this._editorRuntimeOrThrow()._markerLightFields(e)}_markerTapActionFields(e){return this._editorRuntimeOrThrow()._markerTapActionFields(e)}_markerToggleEntityFields(e){return this._editorRuntimeOrThrow()._markerToggleEntityFields(e)}async _saveMarker(){return this._editorRuntimeOrThrow()._saveMarker()}async _deleteMarker(){return this._editorRuntimeOrThrow()._deleteMarker()}_normPos(e,t,i){return{s:e,x:t/cm,y:i/cm}}_spaceDialogUsesOnboardingRuntime(e){return"create"===e&&!this._editorRuntime&&(!!this._onboardingRuntime||0===(this._serverCfg?.spaces.length||0)||this._importTotal>0||this._importQueue.length>0)}_spaceRuntimeOrThrow(){if(!this._onboardingRuntime)throw new Error("Houseplan onboarding runtime is not loaded");return this._onboardingRuntime}_openSpaceDialog(e,t){return this._spaceDialogUsesOnboardingRuntime(e)?this._onboardingRuntime?void this._onboardingRuntime._openSpaceDialog(e,t):void this._ensureOnboardingRuntime().then(i=>{i&&this._spaceRuntimeOrThrow()._openSpaceDialog(e,t)}):this._editorRuntime?this._editorRuntime._openSpaceDialog(e,t):void this._ensureEditorRuntime().then(i=>{i&&this._openSpaceDialog(e,t)})}async _pickPlanFile(e){return this._onboardingRuntime&&this._spaceDialogUsesOnboardingRuntime("create")?this._onboardingRuntime._pickPlanFile(e):this._editorRuntimeOrThrow()._pickPlanFile(e)}_useServerPlan(e){return this._onboardingRuntime&&this._spaceDialogUsesOnboardingRuntime("create")?this._onboardingRuntime._useServerPlan(e):this._editorRuntimeOrThrow()._useServerPlan(e)}async _readPlanAspect(e){return this._onboardingRuntime&&this._spaceDialogUsesOnboardingRuntime("create")?this._onboardingRuntime._readPlanAspect(e):this._editorRuntimeOrThrow()._readPlanAspect(e)}async _deleteServerPlan(e){return this._onboardingRuntime&&this._spaceDialogUsesOnboardingRuntime("create")?this._onboardingRuntime._deleteServerPlan(e):this._editorRuntimeOrThrow()._deleteServerPlan(e)}_renderServerPlans(e){return this._onboardingRuntime&&this._spaceDialogUsesOnboardingRuntime(e.mode)?this._onboardingRuntime._renderServerPlans(e):this._editorRuntimeOrThrow()._renderServerPlans(e)}async _saveSpaceDialog(){return this._onboardingRuntime&&this._spaceDialogUsesOnboardingRuntime("create")?this._onboardingRuntime._saveSpaceDialog():this._editorRuntimeOrThrow()._saveSpaceDialog()}async _deleteSpace(){return this._editorRuntimeOrThrow()._deleteSpace()}async _saveConfigNow(){return this._editorRuntimeOrThrow()._saveConfigNow()}_startImport(){return this._onboardingRuntime?this._onboardingRuntime._startImport():this._editorRuntimeOrThrow()._startImport()}_openNextImport(){return this._onboardingRuntime?this._onboardingRuntime._openNextImport():this._editorRuntimeOrThrow()._openNextImport()}_skipImport(){return this._onboardingRuntime?this._onboardingRuntime._skipImport():this._editorRuntimeOrThrow()._skipImport()}_renderImportDialog(){return this._onboardingRuntime?this._onboardingRuntime._renderImportDialog():this._editorRuntimeOrThrow()._renderImportDialog()}_sunGlobal(){const e=this._settingsDialog;return e?{...this._settings,north_deg:e.northDeg??void 0,bg_mode:e.bgMode,sun_rays:e.sunRays}:this._settings}_sunSpace(){const e=this._spaceDialog,t=this._curSpaceCfg?.settings||{};return e&&"edit"===e.mode&&e.spaceId===this._space?{...t,north_deg:e.northDeg??void 0,bg_mode:e.bgMode??void 0,sun_rays:e.sunRays??void 0}:t}_effNorth(){return or(this._sunGlobal(),this._sunSpace())}_effBgMode(){return sr(this._sunGlobal(),this._sunSpace())}_dayCycleState(e=new Date){return(this._modeTransitionVisual?.viewWeight??("view"===this._mode?1:0))<=0||"daynight"!==this._effBgMode()?null:Yn(this._renderPlanHass,e)}_syncDayCycleClock(){const e=this._dayCycleState();this._dayCycleClockKey=e?Zn(e):"";const t="clock"===e?.source&&"hidden"!==this.ownerDocument.visibilityState&&this.isConnected;t&&!this._dayCycleTimer?this._dayCycleTimer=window.setInterval(this._dayCycleTick,3e4):!t&&this._dayCycleTimer&&(clearInterval(this._dayCycleTimer),this._dayCycleTimer=0)}_dayCycleVisibility(e){"hidden"!==e.kind?(this._dayCycleTick(),this._syncDayCycleClock()):this._dayCycleTimer&&(clearInterval(this._dayCycleTimer),this._dayCycleTimer=0)}_effSunRays(){return ar(this._sunGlobal(),this._sunSpace())}_sunNow(){return null!==this._effNorth()?lr(this._renderPlanHass):null}_renderSunRays(e){const t=W``,i=this._modeTransitionVisual?.viewWeight??0;if(this._editing&&i<=0||!this._effSunRays())return this._sunFadeReset(),t;const n=this._effNorth(),r=null!==n?lr(this._renderPlanHass):null;if(!r||r.elevation<=0)return this._sunFadeReset(),t;if(o=r.elevation,Number(o)>=3)this._sunOutTimer&&(clearTimeout(this._sunOutTimer),this._sunOutTimer=0),this._sunOut=!1,this._sunShown=!0;else{if(!this._sunShown)return t;this._sunOut||(this._sunOut=!0,this._sunOutTimer=window.setTimeout(()=>{this._sunOutTimer=0,this._sunShown=!1,this._sunOut=!1,this.requestUpdate()},2e3))}var o;const s=this._zeroWalls(),a=s.barriers.map(e=>e.join(",")).join(";"),l=`${e.id}|${r.azimuth}|${r.elevation}|${n}|${this._cfgEpoch}|${s.style}|${a}`;if(!this._sunRaysCache||this._sunRaysCache.key!==l){const t=e.rooms.map(e=>({id:e.id||"",poly:Zt(e)})).filter(e=>!!e.id&&!!e.poly),i=this._openingsR.filter(e=>"window"===e.type&&"partition"!==e.host?.kind).map(e=>({id:e.id,x:e.rx,y:e.ry,angle:e.angle,length:e.rlen})),o=this._spaceWalls,a=this._openCuts(),c=this._openingWallIndexFor(e,a).value,h={},d={},u=this._wallUnionGeometry()?.roomGeom;if(o.length){for(const i of t){const t=this._innerRoomContour(e,i.id,a,u);t&&(h[i.id]=t)}for(const e of i){const t=Vs(c,{x:e.x,y:e.y,angle:e.angle,length:e.length});t.cm>0&&(d[e.id]=vo(t.cm,this._cellCm,this._gridPitch))}}let p=nr(t,i,r.azimuth,r.elevation,n,o.length?h:void 0,o.length?d:void 0);const _=[...this._physicalBodiesR(e),...s.barriers.map(e=>[[e[0],e[1]],[e[2],e[3]]])];_.length&&(p=p.map(e=>{const t=function(e,t,i){return i>0?e.map(e=>function(e){const t=[...e].sort((e,t)=>e[0]-t[0]||e[1]-t[1]);if(t.length<=2)return t;const i=(e,t,i)=>(t[0]-e[0])*(i[1]-e[1])-(t[1]-e[1])*(i[0]-e[0]),n=[];for(const e of t){for(;n.length>=2&&i(n[n.length-2],n[n.length-1],e)<=0;)n.pop();n.push(e)}const r=[];for(let e=t.length-1;e>=0;e--){const n=t[e];for(;r.length>=2&&i(r[r.length-2],r[r.length-1],n)<=0;)r.pop();r.push(n)}return n.slice(0,-1).concat(r.slice(0,-1))}([...e,...e.map(e=>[e[0]+t[0]*i,e[1]+t[1]*i])])).filter(e=>e.length>=3):e}(_,e.dir,e.len),i=e.polys.map(e=>_a(e,t));return{...e,paths:i.map(ra).filter(Boolean),polys:i.flatMap(fa)}}).filter(e=>e.paths?.length||e.polys.length)),this._sunRaysCache={key:l,rays:p,rims:p.map(e=>function(e,t=1e-4){const[i,n]=e.dir,r=-n,o=i,s=[];for(const a of[e.a,e.b]){const l=[];for(const s of e.polys)for(let e=0;et)continue;if(Math.abs((h[0]-a[0])*r+(h[1]-a[1])*o)>t)continue;const d=(c[0]-a[0])*i+(c[1]-a[1])*n,u=(h[0]-a[0])*i+(h[1]-a[1])*n;Math.abs(u-d)<=t||l.push(de[0]-t[0]);const c=[];for(const e of l){const i=c[c.length-1];i&&e[0]<=i[1]+t?i[1]=Math.max(i[1],e[1]):c.push([e[0],e[1]])}for(const[e,t]of c)s.push([[a[0]+i*e,a[1]+n*e],[a[0]+i*t,a[1]+n*t]])}return s}(e))}}const c=this._sunRaysCache.rays,h=this._sunRaysCache.rims;if(!c.length)return t;const d=(u=function(e){const t=Math.min(90,Math.max(-90,Number(e)||0));let i=Xn[Xn.length-1][1];for(let e=1;e + ${c.map((e,t)=>{const i=(e.a[0]+e.b[0])/2,n=(e.a[1]+e.b[1])/2,r=i+e.normal[0]*e.depth,o=n+e.normal[1]*e.depth;return W` + ${p.map(([e,t])=>W``)} + + + ${_.map(([e,t])=>W``)} + `})} + + + ${c.map((e,t)=>e.paths?.length?e.paths.map(e=>W``):e.polys.map(e=>W``))} + ${c.map((e,t)=>(h[t]||[]).map(e=>W``))} + `}_sunFadeReset(){this._sunOutTimer&&(clearTimeout(this._sunOutTimer),this._sunOutTimer=0),this._sunShown=!1,this._sunOut=!1}_compassPoint(e){const t=e.currentTarget.getBoundingClientRect(),i=e.clientX-(t.left+t.width/2),n=e.clientY-(t.top+t.height/2);if(Math.hypot(i,n)<5)return;let r=Math.round(180*Math.atan2(i,-n)/Math.PI);e.shiftKey&&(r=15*Math.round(r/15)),r=(r%360+360)%360,this._settingsDialog={...this._settingsDialog,northDeg:r}}_renderCompass(){const e=this._settingsDialog.northDeg;return B`{e.currentTarget.setPointerCapture(e.pointerId),this._compassDrag=!0,this._compassPoint(e)}} + @pointermove=${e=>{this._compassDrag&&this._compassPoint(e)}} + @pointerup=${()=>this._compassDrag=!1} + @pointercancel=${()=>this._compassDrag=!1}> + + ${[0,45,90,135,180,225,270,315].map(e=>W``)} + + + + ${this._t("gs.north_letter")} + + ${null===e?"—":e+"°"} + `}_stageBg(e){const t=this._settingsDialog,i=this._spaceDialog,n=t?t.bgColor||"":nn(this._settings,{bgColor:null});return(i&&"edit"===i.mode&&i.spaceId===this._space?i.bgColor||"":e.bgColor||"")||n}_stageBgHex(){const e=this._stageEl;if(e){const t=getComputedStyle(e).backgroundColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);if(t)return"#"+t.slice(1,4).map(e=>(+e).toString(16).padStart(2,"0")).join("")}return"#111111"}_preflightDiagnostics(e,t){return this._editorRuntimeOrThrow()._preflightDiagnostics(e,t)}_reportPreflightFailure(e,t){return this._editorRuntimeOrThrow()._reportPreflightFailure(e,t)}_preflightVersionsDiffer(){return this._editorRuntimeOrThrow()._preflightVersionsDiffer()}async _copyPreflightDiagnostics(){return this._editorRuntimeOrThrow()._copyPreflightDiagnostics()}_checkOptimizeGeometry(e){return this._editorRuntimeOrThrow()._checkOptimizeGeometry(e)}_checkSpacePhysicalGeometry(e,t,i){return this._editorRuntimeOrThrow()._checkSpacePhysicalGeometry(e,t,i)}_optimizeReferenceContext(e){return this._editorRuntimeOrThrow()._optimizeReferenceContext(e)}_previewAlignDialog(e){return this._editorRuntimeOrThrow()._previewAlignDialog(e)}async _runAlignToGrid(){return this._editorRuntimeOrThrow()._runAlignToGrid()}async _undoPlanOptimization(){return this._editorRuntimeOrThrow()._undoPlanOptimization()}async _runBackupExport(){return this._editorRuntimeOrThrow()._runBackupExport()}async _pickBackupImport(e){return this._editorRuntimeOrThrow()._pickBackupImport(e)}async _setBackupDuplicatePolicy(e){return this._editorRuntimeOrThrow()._setBackupDuplicatePolicy(e)}async _applyBackupImport(){return this._editorRuntimeOrThrow()._applyBackupImport()}_renderBackupExportDialog(){return this._editorRuntimeOrThrow()._renderBackupExportDialog()}_renderBackupImportDialog(){return this._editorRuntimeOrThrow()._renderBackupImportDialog()}_setFillColor(e,t){return this._editorRuntimeOrThrow()._setFillColor(e,t)}async _saveSettingsDialog(){return this._editorRuntimeOrThrow()._saveSettingsDialog()}_boolInput(e,t,i=!1){return this._editorRuntimeOrThrow()._boolInput(e,t,i)}_rangeInput(e,t,i,n,r,o=!1,s){return this._editorRuntimeOrThrow()._rangeInput(e,t,i,n,r,o,s)}_renderColorRow(e,t){return this._editorRuntimeOrThrow()._renderColorRow(e,t)}get _glowRadiusCm(){const e=Number(this._settings.glow_radius_cm);return Number.isFinite(e)&&e>0?e:300}get _imperial(){return"mi"===this.hass?.config?.unit_system?.length}get _glowRadiusPlaceholder(){const e=this._glowRadiusCm;return this._imperial?String(Math.round(e/30.48*10)/10):String(e/100)}_glowTransition(e,t){let i=this._glowRenderedSources.get(e);if(t){const t=this._glowFadeTimers.get(e);if(null!=t&&(clearTimeout(t),this._glowFadeTimers.delete(e)),null==i){this._suspendGlowFeatherForTransition(),i=++this._glowSourceSeq,this._glowRenderedSources.set(e,i),this._glowEnteringSources.add(e);const t=requestAnimationFrame(()=>{this._glowEnterRafs.get(e)===t&&(this._glowEnterRafs.delete(e),this._glowEnteringSources.delete(e),this.isConnected&&this.requestUpdate())});this._glowEnterRafs.set(e,t)}return{domId:i,entering:this._glowEnteringSources.has(e),leaving:!1}}if(null==i)return null;const n=this._glowEnterRafs.get(e);if(null!=n&&cancelAnimationFrame(n),this._glowEnterRafs.delete(e),this._glowEnteringSources.delete(e),!this._glowFadeTimers.has(e)){this._suspendGlowFeatherForTransition();const t=window.setTimeout(()=>{this._glowFadeTimers.get(e)===t&&(this._glowFadeTimers.delete(e),this._glowRenderedSources.delete(e),this._glowLastAppearance.delete(e),this.isConnected&&this.requestUpdate())},534);this._glowFadeTimers.set(e,t)}return{domId:i,entering:!1,leaving:!0}}_suspendGlowFeatherForTransition(){if(window.matchMedia?.("(prefers-reduced-motion: reduce)").matches)return;this._glowFeatherSuspendUntil=Math.max(this._glowFeatherSuspendUntil,Date.now()+500),clearTimeout(this._glowFeatherResumeTimer);const e=Math.max(0,this._glowFeatherSuspendUntil-Date.now())+17,t=()=>{this._glowFeatherResumeTimer=0,Date.now()=128){const e=this._glowGeometryWarnings.values().next().value;e&&this._glowGeometryWarnings.delete(e)}this._glowGeometryWarnings.add(r),console.warn(`HOUSEPLAN GLOW GEOMETRY FALLBACK: #218, space ${e}, room ${i}, phase ${n}`)}}_lightBarriers(e,t){const i=Tl([this._curSpaceCfg,this._cellCm,this._gridPitch]),n=`${e.id}|${i}`,r=X_(this._lightBarrierPool,n);if(r.hit)return this._lightBarrierCache=r.value,r.value.value;const o=this._zeroWalls(),s=this._openCuts(),a=[...o.transmissive],l=Math.max(this._cmToUnits(10),.5*this._gridPitch),c=e=>t.some(t=>this._pointInRoom(e,t.r)),h=this._openingsR.filter(e=>{if("door"!==(t=String(e.type))&&"gate"!==t&&"passage"!==t)return!1;var t;const i=e.angle*Math.PI/180,n=-Math.sin(i),r=Math.cos(i);return c([e.rx+n*l,e.ry+r*l])&&c([e.rx-n*l,e.ry-r*l])}),d=this._roomWallOpeningInputs(h,e);for(const e of d){const t=e.angle*Math.PI/180,i=Math.cos(t)*e.length/2,n=Math.sin(t)*e.length/2;a.push([e.x-i,e.y-n,e.x+i,e.y+n])}const u=new Set(h.filter(e=>e.partitionHost).map(e=>e.id)),p=[e.id,this._cfgEpoch,this._cellCm,this._gridPitch,[...u].sort().join(",")].join("|");this._lightPhysicalBodiesCache?.key!==p&&(this._lightPhysicalBodiesCache={key:p,all:ca(e,this._cellCm,this._gridPitch,2e-4*this._gridPitch,this._partitionOpeningCuts(e,e=>u.has(e.id))).all});const _=this._lightPhysicalBodiesCache.all,m=this._spaceWalls,g=.02*this._gridPitch,f=[],v=this._wallUnionGeometry(),y=v?.sourceFingerprint,b=v&&y===i?function(e,t=[],i=[],n={}){if("failed-core"===e.status||"not-applicable"===e.status)return null;const r=e.roomComponents?.length?e.roomComponents:$s(e.roomGeom)?[{id:"room-primary",geom:e.roomGeom}]:[];let o=r[0]?.geom||null;const s=r.slice(1).map(e=>({...e}));try{for(const i of t){if(!(i.length>0&&e.openingIndex))continue;const t=Gs(e.openingIndex,i,!0);if(!t.negative&&!t.positive)continue;const n=i.angle*Math.PI/180,r=Math.cos(n),a=Math.sin(n),l=-a,c=r,h=i.length/2,d=e.openingPadUnits??1.25*Math.max(e.depthUnits,1),u=xs([[i.x-r*h-l*d,i.y-a*h-c*d],[i.x+r*h-l*d,i.y+a*h-c*d],[i.x+r*h+l*d,i.y+a*h+c*d],[i.x-r*h+l*d,i.y-a*h+c*d]]);o&&(o=Ht(o,u));for(const e of s)e.geom=Ht(e.geom,u)}const r=[...s];let a=s.length;const l=n.mergeExtra||((e,t)=>e?Et(e,t):t);for(let e=0;ee.length>=2&&Number.isFinite(e[0])&&Number.isFinite(e[1]))||Math.abs(yi(t))<=1e-9){a++;continue}const n=[xs(t)];try{const t=l(o,n,e);if(!$s(t))throw new Error("invalid extra union");o=t}catch{a++,$s(n)&&r.push({id:`policy-extra-${e}`,geom:n})}}r.sort((e,t)=>Fs(e.geom).localeCompare(Fs(t.geom)));const c=o||[];return{status:a?"degraded-extra":"ok",geom:c,components:[...$s(c)?[{id:"primary",geom:c}]:[],...r.map((e,t)=>({...e,id:`isolated-${t}`}))]}}catch{return null}}(v,d,_):null,w=b||(m.length||_.length?Ns(e.rooms,m,s,d,this._wallKeyPitch,this._cellCm,this._gridPitch,cm,_):null);if(!w||"ok"!==w.status&&"degraded-extra"!==w.status)for(const e of _)f.push(...Hd(e));else for(const e of w.components)for(const t of ga(e.geom))f.push(...Hd(t));for(const{poly:e}of t)for(const t of a.length?kn(e,a,g):Hd(e))f.push(t);for(const e of o.barriers)f.push(e);const k={occluders:Nd(f),floor:t.map(e=>e.poly),fingerprint:i,masonryGeometry:!w||"ok"!==w.status&&"degraded-extra"!==w.status?[]:w.components.flatMap(e=>e.geom),opaqueBodies:_},x={key:n,value:k};return J_(this._lightBarrierPool,n,x,8),this._lightBarrierCache=x,k}_renderGlowLayer(e,t){const i=this._fillColors,n=this._glowRadiusCm/this._cellCm*this._gridPitch,r=e.rooms.map(e=>({r:e,poly:Zt(e)})).filter(e=>!!e.poly),o=r.filter(({r:e})=>Rn(t.glow,e));if(!o.length)return this._forgetGlowSpace(e.id),W``;const{occluders:s,floor:a,fingerprint:l,masonryGeometry:c,opaqueBodies:h}=this._lightBarriers(e,r),d=Mh(this._renderPlanHass,this._renderDevices,null,this._virtualLights).filter(t=>t.device.space===e.id),u=new Map;for(const e of d){if(!e.device.id)continue;const t=u.get(e.device.id)||[];t.push(e),u.set(e.device.id,t)}const p=[],_=new Set;for(const t of this._renderDevices){if(t.space!==e.id)continue;const o=Th(u.get(t.id)||[]);if(!o)continue;const d=`${e.id}|${t.id}`;_.add(d);const m=fn(o.passive?{state:o.on?"on":"off",attributes:{}}:this._renderPlanHass.states[o.eid],t.marker?.glow_color,i.glow_light.c),g=Number(t.marker?.glow_radius_cm),f=Number.isFinite(g)&&g>0?g/this._cellCm*this._gridPitch:n,v=this._pos(t);if(ya([v.x,v.y],c,h)){this._forgetGlowSource(d);continue}const y=this._glowTransition(d,!!m);if(!y)continue;m&&this._glowLastAppearance.set(d,{c:m.c,alpha:vn(m.bri,i.glow_light.a)});const b=this._glowLastAppearance.get(d);if(!b)continue;let w=null;const k=`${e.id}|${l}|${v.x.toFixed(4)},${v.y.toFixed(4)}|${f.toFixed(4)}`,x=X_(this._glowClipCache,k);if(x.hit)w=x.value;else{const t=qd([v.x,v.y],f,s,96);w={lit:t.length>=3?pa([t],a,{onBoundsFailure:({boundIndex:t,phase:i})=>{const n=r[t]?.r;this._warnGlowGeometryFallback(e.id,l,n?.id||`#${t}`,i)}}):[]},J_(this._glowClipCache,k,w,256)}p.push({key:d,sourceEid:o.eid,domId:y.domId,entering:y.entering,leaving:y.leaving,pos:v,c:b.c,alpha:b.alpha,geometry:w,r:f})}const m=`${e.id}|`;for(const e of this._glowRenderedSources.keys())e.startsWith(m)&&!_.has(e)&&this._forgetGlowSource(e);if(!p.length)return W``;const g=this._spaceWalls,f=o.length===r.length?[]:this._openCuts(),v=this._wallUnionGeometry()?.roomGeom,y=o.length===r.length?null:o.map(({r:t,poly:i})=>{const n=g.length&&t.id&&this._innerRoomContour(e,t.id,f,v)||i,o=this._cleanFloor(t,n,e).path,s=vi(n,r.filter(e=>e.r!==t).map(e=>e.poly)),a=e=>"M "+e.map(e=>e[0]+" "+e[1]).join(" L ")+" Z";return[o||a(n),...s.map(a)].join(" ")}),b=this._viewOr(this._baseVb()),w=this._stageEl?.clientWidth&&b.w?this._stageEl.clientWidth/b.w:1,k=1/(w>0?w:1),x=!this._pinchStart&&!this._panStart&&Date.now()>=this._glowFeatherSuspendUntil;(null==this._glowFeatherUnits||x)&&(this._glowFeatherUnits=k);const $=this._glowFeatherUnits??k,S=4*$,M=p.reduce((e,t)=>({x:Math.min(e.x,t.pos.x-t.r-S),y:Math.min(e.y,t.pos.y-t.r-S),maxX:Math.max(e.maxX,t.pos.x+t.r+S),maxY:Math.max(e.maxY,t.pos.y+t.r+S),w:0,h:0}),{x:1/0,y:1/0,maxX:-1/0,maxY:-1/0,w:0,h:0});return M.w=M.maxX-M.x,M.h=M.maxY-M.y,W` + ${$e(p,e=>e.key,e=>{const t=e.domId,i=e.geometry;return W` + ${""} + + ${hm.map(([t,i])=>W` + `)} + + ${""} + ${i?W` + + + `:G}`})} + ${y?W`${y.map(e=>W``)}`:G} + ${""} + + + + + ${""} + + + ${$e(p,e=>e.key,e=>{const t=e.domId;return W` + + + `})} + + `}_renderAlignDialog(){return this._editorRuntimeOrThrow()._renderAlignDialog()}_renderSettingsDialog(){return this._editorRuntimeOrThrow()._renderSettingsDialog()}_rulesSet(e){return this._editorRuntimeOrThrow()._rulesSet(e)}async _saveRules(){return this._editorRuntimeOrThrow()._saveRules()}_renderRulesDialog(){return this._editorRuntimeOrThrow()._renderRulesDialog()}_saveKioskScale(e){return this._editorRuntimeOrThrow()._saveKioskScale(e)}_renderKioskDialog(){return this._editorRuntimeOrThrow()._renderKioskDialog()}_fixedFloorValue(e){if("string"==typeof e)return e||"''";try{const t=JSON.stringify(e);return(void 0===t?String(e):t).slice(0,160)}catch{return String(e).slice(0,160)}}render(){const e=!!this._importDialog||!(!this._spaceDialog||!this._spaceDialogUsesOnboardingRuntime(this._spaceDialog.mode)),t="view"!==this._mode||!!(this._roomDialog||this._mergeDialog||this._openingDialog||this._physicalDialog||this._partitionDeleteDialog||this._roomDeleteDialog||this._decorTextDialog||this._decorShapeDialog||this._backdropDialog||this._decorEraseConfirm||this._spaceDialog&&!this._spaceDialogUsesOnboardingRuntime(this._spaceDialog.mode)||this._deviceInbox||this._markerDialog||this._rulesDialog||this._settingsDialog||this._alignDialog||this._backupExportDialog||this._backupImportDialog||this._kioskDialog||this._vacFit||this._vacCalConfirm);if(e&&!this._onboardingRuntime&&this._ensureOnboardingRuntime(),t&&!this._editorRuntime&&this._ensureEditorRuntime(),!this._config||!this.hass)return G;const i=this._model,n=this.houseplanDiagnostics(),r=this._fixedFloorState(i);if("pending"===r.kind)return B` +
+
${this._config.title||this._t("card.title")}
+
+
+ +

${this._t("fixed_floor.loading")}

+
+
`;if("invalid"===r.kind)return B` +
+
${this._config.title||this._t("card.title")}
+
+ +
`;if(!i.length)return B` +
+
${this._config.title||this._t("card.title")}
+
+
+ +

${this._t("empty.no_spaces")}

+ ${this._serverStorage?B`

${this._t("empty.add_first")}

+ `:B`

${this._t("empty.install")}

`} +
+ ${this._spaceDialog&&(this._onboardingRuntime||this._editorRuntime)?this._renderSpaceDialog():G} + ${this._importDialog&&this._onboardingRuntime?this._renderImportDialog():G} + ${this._toast?B``:G} +
`;const o=this._spaceModel();if(!o)return G;const s="valid"===r.kind?[o]:i,a=o.vb,l=this._effectiveProjection();this._renderProjection=l,this._labs.active.length&&function(e){p_||y_(e);const t=u_,i=t.active.join(",");if(!i||i===m_)return;m_=i;const n=new Map(n_.map(e=>[e.id,e])),r=t.active.map(e=>{const t=n.get(e);return`${e} (#${t.issue}, expires ${t.expires})`}).join(", ");console.info(`HOUSEPLAN LABS: ${r}`)}(K_);const c="iso"===l,h="devices"===this._mode&&this._showAll,d=this._renderDevices.filter(e=>e.space===o.id&&(!e.hidden||h)),u=this._renderDeviceSnapshot,p=this._spaceDisplayForRender(),_=c?this._isoDecorationLayers(p):null,m=_?this._isoOpeningPanels(_):[],g=this._resolvedRoomFills(o,p),f=this._resolvedGlowBase(o,p,g),v=p.showLqi??this._config.show_signal??!0,y=this._config.icon_size??2.5,b=y>8?2.5:y,w=Bu(b),k=this._viewOr(this._baseVb()),x=this._floorView(k),$=this._editing?"":this._stageBg(p),S=this._opMeasureView,M=this._decorMeasure,T=this._bdLive,R=this._furnLive,C="view"===this._mode?this._editorChromeMode:this._mode,D=this._roomHoverPaths(o),A=o.bg?this._display(o.bg.href):"",O=this._continuity.overlayVisible||"recovery-error"===this._continuity.state?this._continuity.recoveryReason:null,z=this._modeTransitionVisual,P=this._dayCycleState(),F=z?.viewWeight??("view"===this._mode?1:0),I=this._modeTransition.state?.from.presentedMode,E=!this._markup||!!z&&("view"===z.presentedMode||"devices"===z.presentedMode||"view"===I||"devices"===I),N=z?.stageColor||$,H=z?.sceneBrightness??1;return B` + this._notePointer(e)} + @pointerdown=${this._touchGestureGuard} + @pointermove=${this._touchGestureGuard} + @pointerup=${this._touchGestureGuard} + @pointercancel=${this._touchGestureGuard} + @lostpointercapture=${this._touchGestureGuard} + @click=${this._touchGestureGuard}> +
+
+
+ + ${this._config.title||this._t("card.title")} +
+
this._tabPointerMove(e)}> + ${s.map(e=>B``)} + ${""} + ${!this._canEdit||this._kiosk||this._hasFixedFloor?G:B``} +
+ ${this._canEdit?B`
+ ${[["plan","mdi:floor-plan"],["devices","mdi:tune-variant"],["decor","mdi:draw"]].map(([e,t])=>B``)} +
`:G} + ${this._t("count.devices",{n:d.filter(e=>!e.hidden).length})} + + ${this._labsIso&&"view"===this._mode&&!this._kiosk?B``:G} +
+ + ${""} + + +
+ ${this._norm&&this._canEdit?B``:G} +
+ ${this._canEdit&&!this._kiosk?B`
+
+ ${this._editorRuntime?"plan"===C?this._renderMarkupBar():"devices"===C?this._renderDevicesBar():this._renderDecorBar():G} +
+
`:G} +
+ +
this._markupClick(e)} + @wheel=${e=>this._onWheel(e)} + @pointerdown=${e=>{this._notePointer(e),this._stagePointerDown(e)}} + @pointermove=${e=>this._stagePointerMove(e)} + @pointerleave=${e=>this._stagePointerLeave(e)} + @pointerup=${e=>this._stagePointerUp(e)} + @pointercancel=${e=>this._stagePointerCancel(e)}> + ${dr(P,F)} + ${this._editorRuntime?this._renderEditorSecondary():G} +
+ ${c&&_?.structural?W``:G} + + + ${""} + ${this._wallHatchDefs(p.color)}${W`${this._paperShapes(o.rooms).map(e=>"path"in e?W``:"poly"in e?W``:W``)}`} + ${this._editing?this._renderMarkupDefs(a):G} + ${""} + ${this._editing&&!this._markup&&this._gridLevels()?W``:G} + ${o.bg&&A?W`this._onBackdropLoaded(o.bg.href,A)} + transform=${o.bg.angle?`rotate(${o.bg.angle} ${o.bg.x+o.bg.w/2} ${o.bg.y+o.bg.h/2})`:G} + @dblclick=${e=>this._openBackdropDialog(e)} + preserveAspectRatio="none" />`:G} + ${(()=>{const e=this._openCuts(),t=this._thickWallCuts(),i=this._wallUnionGeometry()?.roomGeom,n=new Map,r=e=>(n.has(e)||n.set(e,Zt(e)),n.get(e));return o.rooms.filter(e=>e.area||"view"===this._mode||this._markup||p.showBorders).map(n=>{let s="room "+(o.bg?"overlay":"yard")+(this._markup?" outlined":"");!this._markup||n.id!==this._mergeSel&&n.id!==this._splitSel?.roomId||(s+=" picked");let a="";const l=Tn(p.fill,n);if(!this._markup&&(p.showBorders||"none"!==l)){s+=" styled";const e=[];e.push(`--room-stroke:${p.color}`,`--room-stroke-op:${p.showBorders?p.opacity:0}`);const t=g.byRoom.get(n)||null;t?(s+=" filled",e.push(`--room-fill:${t.color}`,`--room-fill-op:${t.opacity}`)):e.push("--room-fill:transparent","--room-fill-op:0"),a=e.join(";")}let c;const h=e=>{"view"===this._mode&&(void 0===c&&(c=this._roomArea(n)),this._showTip(e,n.name||this._t("room.unnamed"),c?this._t("tip.area",{value:c}):"",v?this._roomLqi(n.area):null,this._roomTemp(n),this._roomHum(n)))},d=r(n),u=this._markup&&(n.id===this._mergeSel||n.id===this._splitSel?.roomId),_=d&&!u?e.filter(e=>{const t=[(e[0]+e[2])/2,(e[1]+e[3])/2];return d.some((e,i)=>Pn(t,[e[0],e[1],...d[(i+1)%d.length]])<=.08*this._gridPitch)}):[],m=u?[]:t,f=_.concat(m);f.length&&(s+=" noedge");const y=this._spaceWalls.length&&n.id&&d&&this._innerRoomContour(o,n.id,e,i)||d,b=y?vi(y,(w=n,o.rooms.filter(e=>e!==w).map(r).filter(Boolean))):[];var w;const k=e=>"M "+e.map(e=>e[0]+" "+e[1]).join(" L ")+" Z",x=y?this._cleanFloor(n,y,o).path:"",$=n.id||G,S=n.area||G,M=x&&y?W`{this._notePointer(e),this._pointerModality.hoverEnabled&&(this._hoverRoom={space:o.id,room:n})}} + @pointermove=${h} + @pointerleave=${()=>this._clearTransientHover()}>`:b.length&&y?W`{this._notePointer(e),this._pointerModality.hoverEnabled&&(this._hoverRoom={space:o.id,room:n})}} + @pointermove=${h} + @pointerleave=${()=>this._clearTransientHover()}>`:y&&y!==d?W`{this._notePointer(e),this._pointerModality.hoverEnabled&&(this._hoverRoom={space:o.id,room:n})}} + @pointermove=${h} + @pointerleave=${()=>this._clearTransientHover()}>`:n.poly?W`{this._notePointer(e),this._pointerModality.hoverEnabled&&(this._hoverRoom={space:o.id,room:n})}} + @pointermove=${h} + @pointerleave=${()=>this._clearTransientHover()}>`:W`{this._notePointer(e),this._pointerModality.hoverEnabled&&(this._hoverRoom={space:o.id,room:n})}} + @pointermove=${h} + @pointerleave=${()=>this._clearTransientHover()}>`,T=f.length&&d?kn(d,f,.02*this._gridPitch):null,R=T?W``:G;return W`${M}${R}`})})()} + ${this._renderRoomHoverFill(D)} + ${this._renderOpeningTunnelFills(o,g)} + ${this._renderGlowBaseRooms(o,f)} + ${this._renderOpeningTunnelFills(o,f,"glow-base")} + ${""} + ${p.hideDecor&&"decor"!==this._mode?G:this._renderDecorLayer()} + ${E?this._renderGlowLayer(o,p):G} + ${this._renderSunRays(o)} + ${this._editing?W`${this._renderAlignGuides()}`:G} + ${this._markup?W`${this._renderMarkupLayer(a)}`:G} + ${""} + ${""} + ${this._editing?G:this._renderZeroWalls(p)} + ${this._renderWallBodies(p)} + ${this._markup&&"resize"===this._tool?this._renderResizeMeasurements():G} + ${this._renderRoomHoverOutline(D)} + ${""} + ${this._editing?this._renderZeroWalls(p):G} + ${this._markup?W`${this._renderHiddenWallDiagnosticOverlay()}`:G} + ${this._markup?W`${this._renderOpeningPlacementPreview()}`:G} + ${S?this._renderOpeningDimensionGuides(S):G} + ${S?.guide?this._renderOpeningCenterTick(S.guide):G} + ${""} + ${""} + ${this._markup?W`${this._renderActiveChainInk()}`:G} + ${this._markup?W`${this._renderPlanSnapOverlay()}`:G} + ${p.hideOpenings&&!this._markup||_&&!_.floorSymbols?G:this._renderOpenings(p)} + ${this._renderWallThickUi()} + ${this._markup&&"resize"===this._tool?this._renderResizeLayer(k):G} + ${""} + ${this._renderBackdropFrame(k)} + ${this._renderTextFrame(k)} + + + ${c&&_?.structural?W` + `:G} + ${""} +
+ ${d.map(e=>this._renderDevice(e,k,v))} + ${this._renderVacuums(d,k)} + ${this._renderVacFit(k)} + ${this._renderOpeningLocks(k)} + ${p.showNames||this._markup?o.rooms.map(e=>this._renderRoomLabel(e,o,k,p)):G} + ${this._markup?o.rooms.map(e=>this._renderRoomGear(e,o,k)):G} +
+ ${this._measureAnchor?B`
${this._renderMeasureLabel(k)}
`:G} + ${this._resize?.liveLabels?B`
${this._resize?.liveLabels?.map(e=>B`
${e.text}
`)}
`:G} + ${S?B`
${S.labels.map(e=>B`
${e.text}
`)}
`:G} + ${this._wallDialog?B`
${this._renderWallThickDialog()}
`:G} + ${M?B`
${M.text}
`:G} + ${R?B`
${R.map(e=>B`
${e.text}
`)}
`:G} + ${T?B`
${T.text}
`:G} +
+ ${this._zoom>1?B`
${Math.round(100*this._zoom)}%
`:G} + ${this._renderFarHint()} + ${this._renderHomeArrow()} + ${this._renderEditorRuntimeLoading()} + ${this._renderRecoveryOverlay()} + ${this._booting||this._bootFading?B``:G} +
+ + ${this._roomDialog&&this._editorRuntime?this._renderRoomDialog():G} + ${this._mergeDialog&&this._editorRuntime?this._renderMergeDialog():G} + ${this._openingDialog&&this._editorRuntime?this._renderOpeningDialog():G} + ${this._physicalDialog&&this._editorRuntime?this._renderPhysicalDialog():G} + ${this._partitionDeleteDialog&&this._editorRuntime?this._renderPartitionDeleteDialog():G} + ${this._roomDeleteDialog&&this._editorRuntime?this._renderRoomDeleteDialog():G} + ${this._openingInfo?this._renderOpeningInfoCard():G} + ${this._decorTextDialog&&this._editorRuntime?this._renderDecorTextDialog():G} + ${this._decorShapeDialog&&this._editorRuntime?this._renderDecorShapeDialog():G} + ${this._backdropDialog&&this._editorRuntime?this._renderBackdropDialog():G} + ${this._decorEraseConfirm&&this._editorRuntime?this._renderDecorEraseConfirm():G} + ${this._spaceDialog&&(this._onboardingRuntime||this._editorRuntime)?this._renderSpaceDialog():G} + ${this._deviceInbox&&this._editorRuntime?this._renderDeviceInbox():G} + ${this._markerDialog&&this._editorRuntime?this._renderMarkerDialog():G} + ${this._vacCalConfirm?B`this._vacCalConfirm=null}> +
+

${this._t("vac.residual_message",{error:this._vacCalConfirm.error})}

+
+
+ + + + +
+
`:G} + ${this._infoCard?this._renderInfoCard():G} + ${this._rulesDialog&&this._editorRuntime?this._renderRulesDialog():G} + ${this._settingsDialog&&this._editorRuntime?this._renderSettingsDialog():G} + ${this._alignDialog&&this._editorRuntime?this._renderAlignDialog():G} + ${this._backupExportDialog&&this._editorRuntime?this._renderBackupExportDialog():G} + ${this._backupImportDialog&&this._editorRuntime?this._renderBackupImportDialog():G} + ${this._importDialog&&this._onboardingRuntime?this._renderImportDialog():G} + ${this._tip?B`
+ ${this._tip.title}${this._tip.meta?B`${this._tip.meta}`:G} + ${null!=this._tip.temp?B`${this._t("tip.temp_avg")} ${this._tip.temp}°`:G} + ${null!=this._tip.hum?B`${this._t("tip.hum_avg")} ${this._tip.hum}%`:G} + ${null!=this._tip.lqi?B`${this._t("tip.lqi")} + ${this._tip.lqi}`:G} +
`:G} + ${this._kiosk&&!this._hasFixedFloor&&this._kioskDots&&this._model.length>1?B`
+ ${this._model.map(e=>B``)} +
`:G} + ${this._kioskDialog&&this._editorRuntime?this._renderKioskDialog():G} + ${this._vacFit?B`
+ ${this._t("vac.fit_hint")} + + + + +
`:G} + ${this._tapConfirm?B`this._tapConfirm=null}> +
+ ${"run"===this._tapConfirm.kind?B`

${this._tapConfirm.text}

`:this._tapConfirm.lines.map((e,t)=>B` +

${e}

`)} +
+
+ + + +
+
`:G} + ${this._toast?B``:G} +
+ `}_vacCandidateStatus(e,t,i=this._planHass){const n=this._bindingStatus("entity:"+e);if("ha_disabled"===n.kind)return"disabled";if("orphaned"===n.kind)return"missing";if("unverified"===n.kind)return"unverified";const r=i?.states?.[e];return"unavailable"===r?.state?"unavailable":t?.hasPosition?"ok":"unsupported"}_vacOpenAllCameras(e){const t=this._haRegistry.entities||{},i=[];for(const[e,n]of Object.entries(this.hass?.states||{})){if(!e.startsWith("camera."))continue;const r=Gl(e,n,t[e]);r&&i.push(r)}this._vacAllCameraCache={devId:e.id,candidates:i},this._vacAllCamerasFor=e.id}_vacSourceResolution(e,t=!1,i=this._planHass){const n=e.marker?.vacuum,r="string"==typeof n?.source&&!!n.source,o=new Set(e.entities||[]),s=new Set(o);r&&s.add(n.source);const a=i?.entities||{},l=[];for(const e of s){const t=i?.states?.[e],o=Gl(e,t,a[e]);o?l.push(o):r&&e===n.source&&l.push({entityId:e,name:String(t?.attributes?.friendly_name||e),platform:a[e]?.platform?String(a[e].platform):null,category:e.startsWith("camera.")?"camera":"partial",hasPosition:!1,hasRooms:!1,hasPath:!1,hasMapId:!1,score:0})}if(t&&this._vacAllCameraCache?.devId===e.id){const e=new Set(l.map(e=>e.entityId));for(const t of this._vacAllCameraCache.candidates)e.has(t.entityId)||l.push(t)}const c={};for(const e of l)c[e.entityId]=this._vacCandidateStatus(e.entityId,e,i);return function(e,t,i,n){const r="string"==typeof e&&e.length>0,o=new Set(t),s=Array.from(i).sort((t,i)=>r&&t.entityId===e?-1:r&&i.entityId===e?1:i.score-t.score||t.entityId.localeCompare(i.entityId));if(r){const t=s.find(t=>t.entityId===e);return{entityId:e,status:n[e]||(t?.hasPosition?"ok":"unverified"),pinned:!0,candidates:s}}const a=s.find(e=>o.has(e.entityId)&&e.hasPosition&&"ok"===n[e.entityId]);return a?{entityId:a.entityId,status:"ok",pinned:!1,candidates:s}:{entityId:null,status:"none",pinned:!1,candidates:s}}(n?.source,o,l,c)}_vacSource(e,t=this._planHass){if(!1===e.marker?.vacuum?.live)return null;const i=this._vacSourceResolution(e,!1,t);return"ok"===i.status||"unsupported"===i.status?i.entityId:null}_vacEntity(e){return e.primary?.startsWith("vacuum.")?e.primary:(e.entities||[]).find(e=>e.startsWith("vacuum."))||null}_isVacDev(e){return!!this._vacEntity(e)}_activitySourceKey(e){return this._activitySnapshot(e).sourceKey}_activitySnapshot(e,t=Mh(this._planHass,this._devices,null,this._virtualLights)){const i=Cu(this._planHass,e,this._devices,t,this._fullRegistryHass);return{samples:i.samples,sourceKey:Ou(this._planHass,e,i)}}_stampActivity(e,t,i){let n=this._activityRt.get(e);n||(n=kp(i||"",[]),this._activityRt.set(e,n)),null!=i&&(n.sources=i),$p(n,t,Date.now(),window.clearTimeout.bind(window),e=>window.setTimeout(()=>this.requestUpdate(),e))}_syncActivityRuntime(){const e=new Map;if(!this.hass)return e;if(!1===this._config?.live_states){for(const e of this._activityRt.values())clearTimeout(e.timer);return this._activityRt.clear(),e}const t=new Set,i=Mh(this._planHass,this._devices,null,this._virtualLights);for(const n of this._devices){if(n.hidden)continue;if("icon_ripple"!==Ai(n.marker?.display))continue;t.add(n.id);const r=this._activitySnapshot(n,i);e.set(n.id,r);const{samples:o,sourceKey:s}=r;let a=this._activityRt.get(n.id);a?a.sources!==s&&xp(a,s,o,window.clearTimeout.bind(window)):(a=kp(s,o),this._activityRt.set(n.id,a))}for(const[e,i]of this._activityRt)t.has(e)||(clearTimeout(i.timer),this._activityRt.delete(e));return e}_activityTick(){if(!this.hass)return;const e=this._syncActivityRuntime();for(const t of this._devices){if(t.hidden)continue;if("icon_ripple"!==Ai(t.marker?.display))continue;const i=e.get(t.id)||this._activitySnapshot(t),{samples:n,sourceKey:r}=i,o=this._activityRt.get(t.id);if(!o||o.sources!==r)continue;const s=Sp(o,n,window.clearTimeout.bind(window));s&&this._stampActivity(t.id,s,r)}}_vacTick(){if(this.hass)for(const e of this._devices){if(e.hidden||!this._isVacDev(e))continue;if("static_icon"===Ai(e.marker?.display)){this._vacRt.delete(e.id);continue}const t=this._vacSource(e);if(!t)continue;const i=this._vacEntity(e),n=Xl(this.hass.states[i||""]?.state),r=Bl(this.hass.states[t]?.attributes);let o=this._vacRt.get(e.id);o||(o={trail:[],lastKey:"",lastTs:0,moving:!1,jump:!1,endedTs:0,lastPos:null},this._vacRt.set(e.id,o)),n&&!o.moving&&(o.trail=[],o.lastPos=null);const s="never"!==nc(e.marker?.vacuum)&&!r?.path.length;!n&&o.moving&&(o.endedTs=Date.now(),s&&o.lastPos&&(o.trail=Zl(o.trail,o.lastPos,40)),o.lastPos=null),o.moving=n;const a=r?.pos;if(n&&a){const e=a.x+":"+a.y;if(e!==o.lastKey){const t=Date.now();o.jump=o.lastTs>0&&t-o.lastTs>1e4,o.lastKey=e,o.lastTs=t,s&&o.lastPos&&(o.trail=Zl(o.trail,o.lastPos,40)),o.lastPos=[a.x,a.y]}}}}_vacEnsureMarker(e){const t=this._serverCfg;if(!t)return null;t.markers=t.markers||[];const i=t.markers.find(t=>t.id===e.id);if(i)return i;if("device"!==e.bindingKind&&"entity"!==e.bindingKind||!e.bindingRef)return null;const n={id:e.id,binding:e.bindingKind+":"+e.bindingRef,space:e.space||null,area:e.area||null,hidden:!!e.hidden};return t.markers.push(n),n}_renderVacSection(e){return this._editorRuntimeOrThrow()._renderVacSection(e)}_vacMapId(e,t,i=this._planHass){return this._editorRuntimeOrThrow()._vacMapId(e,t,i)}_vacSaveMatrix(e,t,i,n){return this._editorRuntimeOrThrow()._vacSaveMatrix(e,t,i,n)}_vacPlanRoomAnchors(e){return this._editorRuntimeOrThrow()._vacPlanRoomAnchors(e)}_vacAutoCalibrate(e){return this._editorRuntimeOrThrow()._vacAutoCalibrate(e)}_vacApplyCalibrationProposal(e){return this._editorRuntimeOrThrow()._vacApplyCalibrationProposal(e)}_vacStartFit(e){return this._editorRuntimeOrThrow()._vacStartFit(e)}_vacFitSave(){return this._editorRuntimeOrThrow()._vacFitSave()}_vacFitTurn(e){return this._editorRuntimeOrThrow()._vacFitTurn(e)}_vacGhostCentre(e){return this._editorRuntimeOrThrow()._vacGhostCentre(e)}_vacDelta(e,t,i){return this._editorRuntimeOrThrow()._vacDelta(e,t,i)}_vacFitPointer(e,t){return this._editorRuntimeOrThrow()._vacFitPointer(e,t)}_renderVacFit(e){return this._editorRuntime?this._editorRuntimeOrThrow()._renderVacFit(e):G}_vacRafLoop(){this._vacRaf=requestAnimationFrame(()=>{const e=this.renderRoot,t=this._stageEl,i=this._vacLastView,n=e?.querySelectorAll?.(".vacpuck")||[];if(!t||!i||!n.length)return void(this._vacRaf=0);const r=t.getBoundingClientRect();for(const t of n){const n=t.getAttribute("data-mid"),o=t.getBoundingClientRect(),s=i.x+(o.left+o.width/2-r.left)/r.width*i.w,a=i.y+(o.top+o.height/2-r.top)/r.height*i.h;for(const t of e.querySelectorAll(`line.tip[data-mid="${n}"]`))t.setAttribute("x2",s.toFixed(1)),t.setAttribute("y2",a.toFixed(1))}this._vacRafLoop()})}_renderVacuums(e,t){if(this._markup||"decor"===this._mode)return G;const i=this._space+"|"+t.x+"|"+t.y+"|"+t.w+"|"+t.h,n=this._vacJumpOnce||i!==this._vacViewKey;this._vacViewKey=i,this._vacJumpOnce=!1;const r=[],o=[];for(const i of e){if(i.hidden||!this._isVacDev(i))continue;if("static_icon"===Ai(i.marker?.display))continue;const e=this._renderDeviceSnapshot?.facts.get(`vacuum:${i.id}`),s=e?.source??this._vacSource(i,this._renderPlanHass);if(!s)continue;const a=e?.telemetry??Bl(this._renderPlanHass?.states[s]?.attributes);if(!a)continue;const l=String(e?.mapId??this._vacMapId(i,a,this._renderPlanHass)),c=i.marker?.vacuum?.calibration?.[l];if(!c||6!==c.length)continue;const h=e?.runtime??this._vacRt.get(i.id),d=h?.moving??!1,u=nc(i.marker?.vacuum),p="always"===u||"cleaning"===u&&d,_=e?.server??this._vacSrvTrails[i.id],m=_?.current?.map_id===l&&Array.isArray(_.current.points)?_.current:null,g=_?.previous?.map_id===l&&Array.isArray(_.previous.points)?_.previous:null;if("always"===u&&g&&g.points.length>1){const e=g.points.map(([e,t])=>{const[i,n]=zl(c,e,t),r=this._scenePoint([i,n]);return r[0].toFixed(1)+","+r[1].toFixed(1)}).join(" ");o.push(W``)}if(p){const e=Ll(a,m,h?.trail||[]),t=!d||"integration"!==e.source&&"server"!==e.source?e.path:jl(e.path);if(t.length){const e=t.map(e=>e.map(([e,t],i)=>{const[n,r]=zl(c,e,t),o=this._scenePoint([n,r]);return`${i?"L":"M"} ${o[0].toFixed(1)} ${o[1].toFixed(1)}`}).join(" ")).join(" ");o.push(W``);const n=t[t.length-1];if(d&&n?.length>=2){const e=n[n.length-1],[t,r]=zl(c,e[0],e[1]),s=this._scenePoint([t,r]),a=s[0].toFixed(1),l=s[1].toFixed(1);o.push(W``)}}}if(!d||!a.pos)continue;const[f,v]=zl(c,a.pos.x,a.pos.y),y=this._scenePoint([f,v]),b=(y[0]-t.x)/t.w*100,w=(y[1]-t.y)/t.h*100,k=h&&h.lastTs>0&&Date.now()-h.lastTs>6e4,x=i.marker?.icon||i.icon||"mdi:robot-vacuum";r.push(B`
{e.stopPropagation();const t=this._vacEntity(i);t&&this._openMoreInfo(t)}}> + +
`)}return this._vacLastView=t,r.length&&!this._vacRaf&&this._vacRafLoop(),r.length||o.length?B` + ${o.length?W`${o}`:G} + ${r}`:G}_renderDevice(e,t,i=!0){const n=this._pos(e),r=this._scenePoint([n.x,n.y]),o=(r[0]-t.x)/t.w*100,s=(r[1]-t.y)/t.h*100,a=this._devicePresentation(e,i),l=[`left:${o}%`,`top:${s}%`,...Lu(a)],c=a.disabledReason,h=a.haDisabled?this._t(`marker.ha_disabled_${c}`):e.userHidden?this._t("marker.hidden_ghost"):e.name,d=xu(a),u="view"===this._mode||"devices"===this._mode,p=[h,a.haDisabled?"":this._t(`marker.state_a11y_${d}`),"none"!==a.pulse.kind?this._t(`marker.pulse_a11y_${a.pulse.reason}`):"",a.valueFullText||a.valueText||"",nu(a.valueBadge),null!=a.lqiText&&a.lqiBand?this._t(`marker.lqi_a11y_${a.lqiBand}`,{value:a.lqiText}):""].filter(Boolean).join(", "),_=[e.model,a.valueBadge?.fullText||"",null!=a.lqiText?"LQI "+a.lqiText:""].filter(Boolean).join(" · ");return B`
this._clickDevice(t,e)} + @keydown=${t=>this._keyDevice(t,e)} + @contextmenu=${t=>this._ctxDevice(t,e)} + @pointerover=${t=>this._showTip(t,e.name,a.haDisabled?h:_)} + @pointerleave=${()=>this._clearTransientHover()} + @pointerdown=${t=>this._pointerDown(t,e)} + @pointermove=${t=>{this._pointerMove(t,e),this._showTip(t,e.name,a.haDisabled?h:_)}} + @pointerup=${t=>this._pointerUp(t,e)} + @pointercancel=${t=>this._pointerUp(t,e)} + > + ${qu(a,{newDevice:this._newIds.has(e.id),newDeviceTitle:this._t("device.new"),disabledTitle:a.haDisabled?h:""})} +
`}_roomArea(e){const t=Zt(e);if(!t)return null;const i=this._spaceModel();if(!i)return null;const n=this._spaceWalls.length&&e.id&&this._innerRoomContour(i,e.id)||t,r=this._cleanFloor(e,n),o=this._cellCm/this._gridPitch;return Hn(r.area*o*o/1e4,"mi"===this._renderPlanHass?.config?.unit_system?.length)}_roomTemp(e){const t=e.settings?.temp_source;return t?Gh(this._renderPlanHass,t,"temp",this._markers):e.area?this._climate().get(e.area)?.temp??null:null}_roomHum(e){const t=e.settings?.hum_source;return t?Gh(this._renderPlanHass,t,"hum",this._markers):e.area?this._climate().get(e.area)?.hum??null:null}_climate(){const e=this._serverCfg?.markers,t=this._renderPlanHass,i=this._climateCache;if(i&&i.h===t&&i.r===this._iconRules&&i.mk===e)return i.m;const n=function(e,t,i){const n=new Map;if(!e?.entities)return n;const r=Lh(i),o=new Set;for(const e of i||[]){if(e?.removed||!0!==e?.use_climate_temp)continue;const t=(e.binding||"").indexOf(":");t>0&&o.add(e.binding.slice(t+1))}const s=new Map;for(const[t,i]of Object.entries(e.entities)){if(i.device_id&&r.devices.has(i.device_id)&&!r.liveEntities.has(t)||!i.device_id&&r.entities.has(t)&&!r.liveEntities.has(t))continue;const n=i.device_id?e.devices?.[i.device_id]:null,a=i.area_id||n?.area_id||null;if(!a)continue;if(i.entity_category)continue;if(!(o.size>0&&t.startsWith("climate.")&&(o.has(t)||i.device_id&&o.has(i.device_id)))){if(Re.has(i.platform))continue;if(Vh.test(t))continue}let l=s.get(a);l||(l=new Map,s.set(a,l));const c=i.device_id||t;let h=l.get(c);if(!h){const r=e.states?.[t];h={name:(n?n.name_by_user||n.name:i.name||r?.attributes?.friendly_name||t)||t,model:n?.model,ents:[]},l.set(c,h)}h.ents.push(t)}for(const[i,r]of s){const s=[],a=[];for(const[i,n]of r){const r=Fh(e,n.name,n.model,n.ents,t),l="mdi:thermometer"===r||"mdi:air-filter"===r;if(l){const t=Dh(e,n.ents);null!=t&&s.push(t)}if(o.size>0&&(o.has(i)||n.ents.some(e=>o.has(e)))){const t=Ah(e,n.ents);null!=t&&s.push(t)}if(l||"mdi:water-percent"===r){const t=zh(e,n.ents);null!=t&&a.push(t)}}(s.length||a.length)&&n.set(i,{temp:s.length?Math.round(s.reduce((e,t)=>e+t,0)/s.length*10)/10:null,hum:a.length?Math.round(a.reduce((e,t)=>e+t,0)/a.length):null})}return n}(t,this._iconRules,e);return this._climateCache={h:t,r:this._iconRules,mk:e,m:n},n}_resetRoomDialogFields(){return this._editorRuntimeOrThrow()._resetRoomDialogFields()}_openRoomEdit(e){return this._editorRuntimeOrThrow()._openRoomEdit(e)}_roomSettingsFromDialog(){return this._editorRuntimeOrThrow()._roomSettingsFromDialog()}_saveRoomEdit(){return this._editorRuntimeOrThrow()._saveRoomEdit()}_roomSrcCandidates(){return this._editorRuntimeOrThrow()._roomSrcCandidates()}_roomSrcLabel(e){return this._editorRuntimeOrThrow()._roomSrcLabel(e)}_labelPos(e,t){const i=this._layout["rl_"+(e.id||"")];if(i&&i.s===t)return{x:i.x*cm,y:i.y*cm};const n=this._roomCenter(e),r=this._gridPitch;return{x:ja(Gt(n[0],r)),y:ja(Gt(n[1],r))}}_labelDown(e,t,i){if(this._editorRuntime)return this._editorRuntimeOrThrow()._labelDown(e,t,i)}_labelMove(e,t,i){if(this._editorRuntime)return this._editorRuntimeOrThrow()._labelMove(e,t,i)}_labelUp(e){if(this._editorRuntime)return this._editorRuntimeOrThrow()._labelUp(e)}_labelScale(e){const t=this._layout["rl_"+(e.id||"")]?.k;return"number"==typeof t&&Number.isFinite(t)?Math.min(3,Math.max(.5,t)):1}_rlResizeDown(e,t,i){return this._editorRuntimeOrThrow()._rlResizeDown(e,t,i)}_rlResizeMove(e){return this._editorRuntimeOrThrow()._rlResizeMove(e)}_rlResizeUp(){return this._editorRuntimeOrThrow()._rlResizeUp()}_renderRoomGear(e,t,i){return this._editorRuntimeOrThrow()._renderRoomGear(e,t,i)}_renderRoomLabel(e,t,i,n){if(!e.name&&!this._markup)return G;const r=this._labelPos(e,t.id),o=this._scenePoint([r.x,r.y]),s=(o[0]-i.x)/i.w*100,a=(o[1]-i.y)/i.h*100,l=Math.min(1,n.opacity+.25),c=this._labelScale(e),h=[];if(e.area||e.settings?.temp_source||e.settings?.hum_source||n.labelLight){if(n.labelTemp){const t=this._roomTemp(e);null!=t&&h.push(B`${t}°`)}if(n.labelHum){const t=this._roomHum(e);null!=t&&h.push(B`${t}%`)}if(n.labelLqi&&e.area){const t=this._roomLqi(e.area);null!=t&&h.push(B`${t}`)}if(n.labelLight){const t=(d=Mh(this._renderPlanHass,this._renderDevices,e,this._virtualLights)).length?{on:d.filter(e=>e.on).length,total:d.length}:null;if(t){const e=0===t.on?this._t("roomcard.light_off"):t.on===t.total?this._t("roomcard.light_on"):this._t("roomcard.light_partial",{on:t.on,total:t.total});h.push(B`${e}`)}}}var d;const u=!!e.area,p=!this._markup;return B`
this._labelDown(i,e,t.id)} + @pointermove=${i=>this._labelMove(i,e,t.id)} + @pointerup=${()=>this._labelUp(e)} + @pointercancel=${()=>this._labelUp(e)} + >${e.name||(this._markup?this._t("room.unnamed"):"")}${u?B`{t.stopPropagation(),this._clickRoom(e)}:G} + @pointerdown=${p?e=>e.stopPropagation():G}>`:G} + ${h.length?B`${h}`:G} + ${"plan"===this._mode?["tl","tr","bl","br"].map(i=>B`this._rlResizeDown(i,e,t.id)} + @pointermove=${e=>this._rlResizeMove(e)} + @pointerup=${()=>this._rlResizeUp()} + @pointercancel=${()=>this._rlResizeUp()}>`):G} +
`}get _measureAnchor(){return this._markup&&this._cursorPt?"draw"===this._tool&&this._path.length&&!this._contourClosed?this._path[this._path.length-1]:"split"===this._tool&&this._splitSel?.pts?.length?this._splitSel.pts[this._splitSel.pts.length-1]:null:null}_renderMeasureLabel(e){const t=this._measureAnchor,i=this._cursorPt,n=(i[0]-e.x)/e.w*100,r=(i[1]-e.y)/e.h*100,o=On(t,i),s=Math.round(10*o)/10,a=function(e,t,i=.001){if(e.length<2||t.length<2)return!1;const n=Math.abs(t[0]-e[0]),r=Math.abs(t[1]-e[1]);if(!Number.isFinite(n)||!Number.isFinite(r)||Math.hypot(n,r)<=i)return!1;const o=Math.max(n,r,1);return n<=i*o||r<=i*o||Math.abs(n-r)<=i*o}(t,i,2e-4*this._gridPitch);return B`
+ ${this._fmtLen(t,i)} · ${s}°
`}get _decorMeasure(){const e=this._decorDraft;if(!e||"decor"!==this._mode)return null;const[t,i]=e.a,[n,r]=e.b;if(Math.abs(t-n)<1e-6&&Math.abs(i-r)<1e-6)return null;const o=(t+n)/2,s=(i+r)/2;if("line"===e.kind){const t=On(e.a,e.b);return{x:o,y:s,on45:zn(t),text:`${this._fmtLen(e.a,e.b)} · ${Math.round(10*t)/10}°`}}const a=this._fmtLen([t,i],[n,i]),l=this._fmtLen([n,i],[n,r]);if("ellipse"===e.kind){const e=this._fmtLen([0,0],[Math.abs(n-t)/2,0]),a=this._fmtLen([0,0],[0,Math.abs(r-i)/2]);return{x:o,y:s,on45:!1,text:Math.abs(Math.abs(n-t)-Math.abs(r-i))<1e-6?`R ${e}`:`Rx ${e} × Ry ${a}`}}return{x:o,y:s,on45:!1,text:`${a} × ${l} · ${Hn(vr(Math.abs(n-t),this._cellCm,this._gridPitch)*vr(Math.abs(r-i),this._cellCm,this._gridPitch)/1e4,this._imperial)}`}}get _alignPoint(){if(this._markup){if("draw"===this._tool&&this._path.length&&!this._contourClosed&&this._cursorPt)return this._cursorPt;if("split"===this._tool&&this._splitSel?.pts?.length&&this._cursorPt)return this._cursorPt;if(this._drag?.id.startsWith("rl_")&&this._drag.moved){const e=this._drag.id.slice(3),t=this._spaceModel()?.rooms.find(t=>t.id===e);return t?(()=>{const e=this._labelPos(t,this._space);return[e.x,e.y]})():null}return null}if("devices"===this._mode&&this._drag?.moved){const e=this._devices.find(e=>e.id===this._drag.id);return e?(()=>{const t=this._pos(e);return[t.x,t.y]})():null}if("decor"===this._mode){if(this._decorDraft)return this._decorDraft.b;if(this._decorMove){const e=this._decorList.find(e=>e.id===this._decorMove.id);if(!e)return null;const t=cm,i=this._decorH;return"line"===e.kind?[e.x1*t,e.y1*i]:[e.x*t,e.y*i]}return null}return null}_alignCandidates(){return this._editorRuntimeOrThrow()._alignCandidates()}_renderAlignGuides(){return this._editorRuntimeOrThrow()._renderAlignGuides()}_renderOpeningCenterTick(e){return this._editorRuntimeOrThrow()._renderOpeningCenterTick(e)}_renderOpeningDimensionGuides(e){return this._editorRuntimeOrThrow()._renderOpeningDimensionGuides(e)}_roomCenter(e){if(e.poly){const t=e.poly.length;return[e.poly.reduce((e,t)=>e+t[0],0)/t,e.poly.reduce((e,t)=>e+t[1],0)/t]}return[e.x+e.w/2,e.y+.1*Math.min(e.w,e.h)]}_openingAmt(e){const t=e.contact&&this._renderOpeningEntityAvailable(e.contact)?this._renderPlanHass.states[e.contact]?.state:null;return ii(e.type,t,!!e.invert)}_planEntityAvailable(e){return!!e&&(!jh(this._fullRegistryHass,e,Lh(this._markers))&&"active"===this._bindingStatus("entity:"+e).kind)}_renderEntityAvailable(e){return!!e&&(!jh(this._renderPlanHass,e,Lh(this._markers))&&(!!this._renderPlanHass.entities?.[e]&&!!this._renderPlanHass.states?.[e]))}_openingEntityAvailable(e){return function(e,t,i=Vc(e)){return!!t&&"active"===th(e,`entity:${t}`,i).kind}(this.hass,e,this._haRegistry)}_renderOpeningEntityAvailable(e){return t=this._renderPlanHass,!!(i=e)&&!!t?.states?.[i];var t,i}_renderOpeningPlacementPreview(){return this._editorRuntimeOrThrow()._renderOpeningPlacementPreview()}_renderOpenings(e){const t=this._openingsR;if(!t.length)return W``;const i=this._spaceModel();if(!i)return W``;const n=e.color,r=this._spaceWalls,o=this._openCuts(),s=this._openingWallIndexFor(i,o).value;return W`${t.map(e=>{if(e.orphanReason)return W`{t.stopPropagation(),this._editOpening(e)}}> + + ! + `;const t=this._openingAmt(e),i=t>0&&!!e.contact&&this._renderOpeningEntityAvailable(e.contact)?"var(--hp-open)":n,o="gate"===e.type?!e.flip_v:e.flip_v,a=e.partitionHost||r.length||"gate"===e.type?this._openingFace(e,s,!!o):{ox:0,oy:0,cm:0,side:-1},l={type:e.type,length:e.rlen,angle:e.angle,amount:t,flipH:!!e.flip_h,flipV:!!e.flip_v,base:n,tone:i,cellCm:this._cellCm,gridPitch:this._gridPitch,face:a},{half:c,outlineHalf:h,hitHalf:d}=eu(l),u=xa(10,this._cellCm),p=xa(12,this._cellCm);return W` + ${tu(l)} + + this._opClick(t,e)} + @pointerdown=${t=>this._opPointerDown(t,e)} + @pointermove=${t=>this._opPointerMove(t,e)} + @pointerup=${t=>this._opPointerUp(t,e)} + @pointercancel=${t=>this._opPointerUp(t,e)}> + `})}`}_renderOpeningLocks(e){const t=this._openingsR.filter(e=>!e.orphanReason&&("door"===e.type||"gate"===e.type)&&e.lock&&this._renderOpeningEntityAvailable(e.lock));if(!t.length)return B``;const i=this._spaceModel();if(!i)return B``;const n=this._openCuts(),r=this._openingWallIndexFor(i,n).value;return B`${t.map(t=>{const i=this._renderPlanHass.states[t.lock]?.state,n="locked"===i,o=n||["unlocked","open","opening","unlocking","locking"].includes(String(i)),s=(t.angle+90)*Math.PI/180,a="gate"===t.type?this._openingFace(t,r,!t.flip_v):null,l=xa(16,this._cellCm),c=a?-l*a.side:l*(t.flip_v?-1:1),h=t.rx+Math.cos(s)*c,d=t.ry+Math.sin(s)*c,u=this._scenePoint([h,d]),p=(u[0]-e.x)/e.w*100,_=(u[1]-e.y)/e.h*100;return B`
{e.stopPropagation(),"view"===this._mode&&(this._openingInfo=t)}}> + +
`})}`}_lockAction(e,t){if(this._openingEntityAvailable(e)){if("unlock"===t){const t=this.hass?.states?.[e]?.attributes?.friendly_name||e;if(!confirm(this._t("confirm.unlock",{name:t})))return}this.hass?.callService?.("lock",t,{entity_id:e})}}_renderOpeningInfoCard(){const e=this._openingInfo,t="passage"!==e.type&&e.contact&&this._openingEntityAvailable(e.contact)?e.contact:null,i=("door"===e.type||"gate"===e.type)&&e.lock&&this._openingEntityAvailable(e.lock)?e.lock:null,n=t?this.hass.states[t]?.state:null,r=this._openingAmt(e),o=i?this.hass.states[i]?.state:null,s="door"===e.type?"opening.door":"gate"===e.type?"opening.gate":"passage"===e.type?"opening.passage":"opening.window",a="door"===e.type?"mdi:door":"gate"===e.type?"mdi:gate":"passage"===e.type?"mdi:arch":"mdi:window-closed-variant",l="gate"===e.type?r>0?"mdi:gate-open":"mdi:gate":r>0?"mdi:door-open":"mdi:door-closed",c=(e,t,i,n="")=>B`
${t}${i}
`;return B`this._openingInfo=null}> +
+ ${t?c(l,this._t("opening.contact_label"),"unavailable"===n||null==n?this._t("opening.state_unknown"):this._t(r>0?"opening.open":"opening.closed"),r>0?"warn":"ok"):G} + ${i?c("locked"===o?"mdi:lock":"mdi:lock-open-variant",this._t("opening.lock_label"),"locked"===o?this._t("opening.locked"):["unlocked","open"].includes(String(o))?this._t("opening.unlocked"):this._t("opening.state_unknown"),"locked"===o?"ok":"warn"):G} + ${i&&("locked"===o||["unlocked","open"].includes(String(o)))?B``:i&&["locking","unlocking"].includes(String(o))?B``:G} + ${t||i?G:B`

${this._t("opening.no_entities")}

`} +
+
+ + +
+
`}_renderOpeningDialog(){return this._editorRuntimeOrThrow()._renderOpeningDialog()}_gridLevels(){return this._editorRuntimeOrThrow()._gridLevels()}_renderMarkupDefs(e){return this._editorRuntimeOrThrow()._renderMarkupDefs(e)}_renderPhysicalEditorLayer(){return this._editorRuntimeOrThrow()._renderPhysicalEditorLayer()}_renderHiddenWallDiagnosticOverlay(){return this._editorRuntimeOrThrow()._renderHiddenWallDiagnosticOverlay()}_renderPlanSnapOverlay(){return this._editorRuntimeOrThrow()._renderPlanSnapOverlay()}_syncPlanSnapActiveMarker(e){return this._editorRuntimeOrThrow()._syncPlanSnapActiveMarker(e)}_syncPlanSnapConflictMarkers(e){return this._editorRuntimeOrThrow()._syncPlanSnapConflictMarkers(e)}_planSnapPhysicalSegment(e){return this._editorRuntimeOrThrow()._planSnapPhysicalSegment(e)}_drawPreviewJoinPatchD(e,t){return this._editorRuntimeOrThrow()._drawPreviewJoinPatchD(e,t)}_renderMarkupLayer(e){return this._editorRuntimeOrThrow()._renderMarkupLayer(e)}_renderActiveChainInk(){return this._editorRuntimeOrThrow()._renderActiveChainInk()}_renderPartitionDeleteDialog(){return this._editorRuntimeOrThrow()._renderPartitionDeleteDialog()}_renderRoomDeleteDialog(){return this._editorRuntimeOrThrow()._renderRoomDeleteDialog()}_renderPhysicalDialog(){return this._editorRuntimeOrThrow()._renderPhysicalDialog()}_renderMarkupBar(){return this._editorRuntimeOrThrow()._renderMarkupBar()}_renderDevicesBar(){return this._editorRuntimeOrThrow()._renderDevicesBar()}_renderDeviceInbox(){return this._editorRuntimeOrThrow()._renderDeviceInbox()}_cardEntities(e){const t=this._planHass,i=[],n=new Set,r=e=>{if(!e||n.has(e)||!t.states[e])return;const r=t.entities[e];if("config"===r?.entity_category||"diagnostic"===r?.entity_category)return;n.add(e);const o=e.split(".")[0];["light","switch","fan","humidifier","siren","input_boolean"].includes(o)?i.push({eid:e,kind:"toggle"}):["cover","valve","lock","climate","media_player","vacuum","water_heater"].includes(o)?i.push({eid:e,kind:"open"}):["sensor","binary_sensor","number","select"].includes(o)&&i.push({eid:e,kind:"value"})};for(const i of Mh(t,this._devices,null,this._virtualLights))if(i.device.id===e.id)for(const e of[...i.serviceEids,...i.stateEids])r(e);e.primary&&r(e.primary);for(const t of e.entities)r(t);return i.slice(0,12)}_cardToggle(e){const t=e.split(".")[0];"lock"!==t&&"alarm_control_panel"!==t&&this._planEntityAvailable(e)&&this.hass.callService("homeassistant","toggle",{entity_id:e}).catch(e=>this._showToast(this._t("toast.error",{err:this._errText(e)})))}_renderInfoCard(){const e=this._infoCard,t=e.primary?this.hass.states[e.primary]:void 0,i=t?ji(this.hass,e.primary)?.text??t.state:null,n=(e.controls??e.marker?.controls??[]).filter(yn).filter(e=>this._planEntityAvailable(e));return B` +
+ ${(()=>{const t=this._cardEntities(e);return t.length?B`
+ ${t.map(({eid:e,kind:t})=>{const i=this.hass.states[e],n=this.hass.entities[e]?.name||i?.attributes?.friendly_name||e,r=i?ji(this.hass,e)?.text??i.state:"",o="on"===i?.state||["open","unlocked","playing","cleaning"].includes(i?.state);return B`
+ + ${n} + ${"toggle"===t?B``:"open"===t?B``:B`${r}`} +
`})} +
`:G})()} + ${e.model?B`
${this._t("info.model")}${e.model}
`:G} + ${i&&!this._cardEntities(e).length?B`
${this._t("info.state")}${i}
`:G} + ${Ci(e.link)?B`
${this._t("info.link")} + ${e.link}
`:G} + ${e.description?B`
${e.description}
`:G} + ${e.pdfs&&e.pdfs.length?B`
${this._t("info.manuals")} + ${e.pdfs.map(e=>B` + ${e.name}`)}
`:G} + ${n.length?B`
${this._t("info.controls")} + + ${n.map(e=>{const t=this.hass.states[e],i="on"===t?.state;return B` + + ${t?.attributes?.friendly_name||e}`})} +
`:G} + ${e.model||i||e.link||e.description||e.pdfs&&e.pdfs.length||n.length?G:B`
${this._t("info.none")}
`} +
+
+ + ${e.primary?B``:G} + +
+
`}_markerValueBadgeFields(e){return this._editorRuntimeOrThrow()._markerValueBadgeFields(e)}_markerDraft(e){return this._editorRuntimeOrThrow()._markerDraft(e)}_markerPreviewDevice(e){return this._editorRuntimeOrThrow()._markerPreviewDevice(e)}_markerPreviewDevices(e){return this._editorRuntimeOrThrow()._markerPreviewDevices(e)}_toggleIntent(e,t=this._devices){return this._editorRuntimeOrThrow()._toggleIntent(e,t)}_toggleIntentForDialog(e){return this._editorRuntimeOrThrow()._toggleIntentForDialog(e)}_toggleStateText(e,t){return this._editorRuntimeOrThrow()._toggleStateText(e,t)}_toggleConfirmationStateText(e){return this._editorRuntimeOrThrow()._toggleConfirmationStateText(e)}_toggleConfirmationLines(e){return this._editorRuntimeOrThrow()._toggleConfirmationLines(e)}_toggleHintLines(e){return this._editorRuntimeOrThrow()._toggleHintLines(e)}_effectiveStoredTapAction(e,t){return this._editorRuntimeOrThrow()._effectiveStoredTapAction(e,t)}_effectiveMarkerTapAction(e,t=this._markerPreviewDevice(e)){return this._editorRuntimeOrThrow()._effectiveMarkerTapAction(e,t)}_announceToggleDraft(e){return this._editorRuntimeOrThrow()._announceToggleDraft(e)}_valueBadgeForBinding(e,t){return this._editorRuntimeOrThrow()._valueBadgeForBinding(e,t)}_markerSpatialSource(e){return this._editorRuntimeOrThrow()._markerSpatialSource(e)}_markerAutoHasSpatialSource(e){return this._editorRuntimeOrThrow()._markerAutoHasSpatialSource(e)}_setMarkerLightRole(e){return this._editorRuntimeOrThrow()._setMarkerLightRole(e)}_controlRefInfo(e){return this._editorRuntimeOrThrow()._controlRefInfo(e)}_valueBadgeCandidateLabel(e){return this._editorRuntimeOrThrow()._valueBadgeCandidateLabel(e)}_controlCandidates(e){return this._editorRuntimeOrThrow()._controlCandidates(e)}_addControlRef(e,t){return this._editorRuntimeOrThrow()._addControlRef(e,t)}_setMarkerGlowMode(e){return this._editorRuntimeOrThrow()._setMarkerGlowMode(e)}_renderMarkerDialog(){return this._editorRuntimeOrThrow()._renderMarkerDialog()}_renderSpaceDialog(){return this._onboardingRuntime&&this._spaceDialog&&this._spaceDialogUsesOnboardingRuntime(this._spaceDialog.mode)?this._onboardingRuntime._renderSpaceDialog():this._editorRuntimeOrThrow()._renderSpaceDialog()}_renderMergeDialog(){return this._editorRuntimeOrThrow()._renderMergeDialog()}_renderCardPreview(e,t,i){const n=18*e;return B`
+ + ${this._t("preview.room_name")} + + 22.4° · + 45% · + ${this._t("roomcard.light_partial",{on:1,total:3})} + +
`}_renderRoomSource(e){return this._editorRuntimeOrThrow()._renderRoomSource(e)}_renderRoomDialog(){return this._editorRuntimeOrThrow()._renderRoomDialog()}}fm.properties={_tabDrag:{state:!0},_hdrH:{state:!0},_booting:{state:!0},_bootFading:{state:!0},_bootSoft:{state:!0},_continuityEpoch:{state:!0},_editorRuntimeLoadingVisible:{state:!0},_tapConfirm:{state:!0},hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_hoverRoom:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_mode:{state:!0},_tool:{state:!0},_wallDialog:{state:!0},_drawWallField:{state:!0},_activeDraftId:{state:!0},_physicalSel:{state:!0},_physicalDialog:{state:!0},_partitionDeleteDialog:{state:!0},_roomDeleteDialog:{state:!0},_physicalDrag:{state:!0},_physicalRotate:{state:!0},_duplicateColumnId:{state:!0},_opMeasure:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_mergeSel:{state:!0},_openingPreset:{state:!0},_openingDialog:{state:!0},_openingInfo:{state:!0},_mergeDialog:{state:!0},_splitSel:{state:!0},_decorTool:{state:!0},_decorStyle:{state:!0},_decorDraft:{state:!0},_decorSel:{state:!0},_decorEraseConfirm:{state:!0},_decorTextDialog:{state:!0},_decorShapeDialog:{state:!0},_backdropDialog:{state:!0},_furnPalette:{state:!0},_bdDrag:{state:!0},_dtBox:{state:!0},_dtDrag:{state:!0},_kioskDialog:{state:!0},_vacFit:{state:!0},_vacAllCamerasFor:{state:!0},_vacCalConfirm:{state:!0},_kioskDots:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_roomEditId:{state:!0},_roomFill:{state:!0},_roomCustomFill:{state:!0},_roomTempSrc:{state:!0},_roomHumSrc:{state:!0},_roomSrcOpen:{state:!0},_roomSrcFilter:{state:!0},_roomNameScale:{state:!0},_roomLabelScale:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_deviceInbox:{state:!0},_rulesDialog:{state:!0},_settingsDialog:{state:!0},_alignDialog:{state:!0},_preflightClipboardFallback:{state:!0},_backupExportDialog:{state:!0},_backupImportDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},fm.ZOOM_MAX=8,fm.ZOOM_MIN=1/3,fm.styles=[Bd,Dp],customElements.get("houseplan-card")||customElements.define("houseplan-card",fm),window.customCards=window.customCards||[],window.customCards.find(e=>"houseplan-card"===e.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info(`%c HOUSEPLAN-CARD %c v${K_} `,"background:#3ea6ff;color:#04121f;font-weight:700","");export{Ao as $,G as A,Qt as B,Ia as C,le as D,U as E,Zt as F,Na as G,Rd as H,Id as I,gs as J,lo as K,Pd as L,Md as M,No as N,vs as O,Cd as P,Bs as Q,Gs as R,vo as S,oi as T,ao as U,xo as V,Ku as W,Yu as X,ss as Y,Wr as Z,zo as _,o as a,ts as a$,no as a0,oo as a1,jr as a2,rp as a3,Ta as a4,Fo as a5,Ea as a6,Oo as a7,Ca as a8,Da as a9,ea as aA,Al as aB,op as aC,Tl as aD,Lp as aE,_s as aF,Ns as aG,ks as aH,Op as aI,hi as aJ,si as aK,$i as aL,va as aM,sa as aN,oa as aO,Pa as aP,Io as aQ,Eo as aR,Ls as aS,Kt as aT,tl as aU,ma as aV,_a as aW,Nn as aX,Hn as aY,pi as aZ,is as a_,Ui as aa,Gi as ab,Hi as ac,Li as ad,za as ae,ms as af,ns as ag,js as ah,jo as ai,Xs as aj,ba as ak,sp as al,Qs as am,fs as an,ar as ao,sr as ap,or as aq,nn as ar,Ce as as,Qd as at,k_ as au,fo as av,ja as aw,Gt as ax,Vt as ay,Cr as az,B as b,Yi as b$,W as b0,Pp as b1,Fp as b2,kr as b3,Mr as b4,Sr as b5,vr as b6,qa as b7,_r as b8,gr as b9,Fd as bA,wi as bB,li as bC,ei as bD,ci as bE,Lo as bF,Si as bG,mn as bH,mu as bI,gu as bJ,uh as bK,_d as bL,Ai as bM,Ii as bN,En as bO,Cn as bP,Eh as bQ,ro as bR,Hh as bS,Ih as bT,tn as bU,Qi as bV,Ji as bW,Sa as bX,tp as bY,Xi as bZ,Zi as b_,br as ba,On as bb,pr as bc,fr as bd,mr as be,xr as bf,gl as bg,dl as bh,ml as bi,ul as bj,bl as bk,sl as bl,hl as bm,Ho as bn,_o as bo,xa as bp,eu as bq,zd as br,U_ as bs,Od as bt,Vs as bu,L_ as bv,G_ as bw,Zu as bx,ti as by,Dd as bz,he as c,co as c$,Ki as c0,$a as c1,Hp as c2,zp as c3,rn as c4,Kr as c5,lr as c6,De as c7,Fe as c8,Ae as c9,vd as cA,kd as cB,$d as cC,Th as cD,Mh as cE,kh as cF,_h as cG,yn as cH,dh as cI,Nh as cJ,gn as cK,Pu as cL,ou as cM,xh as cN,$h as cO,mh as cP,cd as cQ,Oi as cR,Di as cS,su as cT,Ci as cU,Ma as cV,zi as cW,Pi as cX,so as cY,cl as cZ,vl as c_,Bl as ca,Kl as cb,Vi as cc,nc as cd,ql as ce,El as cf,Yl as cg,Wl as ch,Ul as ci,ec as cj,tc as ck,Ql as cl,ic as cm,zl as cn,Dn as co,An as cp,ii as cq,j_ as cr,tu as cs,H_ as ct,nl as cu,ys as cv,Vo as cw,Ko as cx,fu as cy,Uh as cz,Bd as d,ri as d0,Fa as d1,Zd as d2,fm as d3,pe as e,Hu as f,Lu as g,ui as h,_e as i,Rr as j,fi as k,Xd as l,Ol as m,Nt as n,yi as o,gi as p,Lh as q,qu as r,jt as s,Jd as t,jh as u,nu as v,yu as w,Jt as x,wn as y,Pn as z}; diff --git a/custom_components/houseplan/frontend/houseplan-assets/houseplan-editor-runtime-DUuA4F8c.js b/custom_components/houseplan/frontend/houseplan-assets/houseplan-editor-runtime-DUuA4F8c.js new file mode 100644 index 00000000..26fb1703 --- /dev/null +++ b/custom_components/houseplan/frontend/houseplan-assets/houseplan-editor-runtime-DUuA4F8c.js @@ -0,0 +1,2572 @@ +globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="f111431804c4c42c6e35286f29720d089bc8b77ffc364a6517f7a3510fcb11c8";import{e as t,i as e,E as s,D as i,b as o,A as a,a as n,c as r,s as l,m as h,d as c,l as d,t as p,w as _,f as u,g as m,v as g,r as f,h as b,j as y,p as v,k as $,n as w,o as k,q as x,u as S,x as M,y as D,z as C,B as P,G as I,C as T,F as R,H as F,I as N,J as z,K as A,L as O,M as L,N as E,O as B,P as H,Q as G,R as W,S as j,T as q,U,V as K,W as V,X as J,Y,Z as X,_ as Z,$ as Q,a0 as tt,a1 as et,a2 as st,a3 as it,a4 as ot,a5 as at,a6 as nt,a7 as rt,a8 as lt,a9 as ht,aa as ct,ab as dt,ac as pt,ad as _t,ae as ut,af as mt,ag as gt,ah as ft,ai as bt,aj as yt,ak as vt,al as $t,am as wt,an as kt,ao as xt,ap as St,aq as Mt,ar as Dt,as as Ct,at as Pt,au as It,av as Tt,aw as Rt,ax as Ft,ay as Nt,az as zt,aA as At,aB as Ot,aC as Lt,aD as Et,aE as Bt,aF as Ht,aG as Gt,aH as Wt,aI as jt,aJ as qt,aK as Ut,aL as Kt,aM as Vt,aN as Jt,aO as Yt,aP as Xt,aQ as Zt,aR as Qt,aS as te,aT as ee,aU as se,aV as ie,aW as oe,aX as ae,aY as ne,aZ as re,a_ as le,a$ as he,b0 as ce,b1 as de,b2 as pe,b3 as _e,b4 as ue,b5 as me,b6 as ge,b7 as fe,b8 as be,b9 as ye,ba as ve,bb as $e,bc as we,bd as ke,be as xe,bf as Se,bg as Me,bh as De,bi as Ce,bj as Pe,bk as Ie,bl as Te,bm as Re,bn as Fe,bo as Ne,bp as ze,bq as Ae,br as Oe,bs as Le,bt as Ee,bu as Be,bv as He,bw as Ge,bx as We,by as je,bz as qe,bA as Ue,bB as Ke,bC as Ve,bD as Je,bE as Ye,bF as Xe,bG as Ze,bH as Qe,bI as ts,bJ as es,bK as ss,bL as is,bM as os,bN as as,bO as ns,bP as rs,bQ as ls,bR as hs,bS as cs,bT as ds,bU as ps,bV as _s,bW as us,bX as ms,bY as gs,bZ as fs,b_ as bs,b$ as ys,c0 as vs,c1 as $s,c2 as ws,c3 as ks,c4 as xs,c5 as Ss,c6 as Ms,c7 as Ds,c8 as Cs,c9 as Ps,ca as Is,cb as Ts,cc as Rs,cd as Fs,ce as Ns,cf as zs,cg as As,ch as Os,ci as Ls,cj as Es,ck as Bs,cl as Hs,cm as Gs,cn as Ws,co as js,cp as qs,cq as Us,cr as Ks,cs as Vs,ct as Js,cu as Ys,cv as Xs,cw as Zs,cx as Qs,cy as ti,cz as ei,cA as si,cB as ii,cC as oi,cD as ai,cE as ni,cF as ri,cG as li,cH as hi,cI as ci,cJ as di,cK as pi,cL as _i,cM as ui,cN as mi,cO as gi,cP as fi,cQ as bi,cR as yi,cS as vi,cT as $i,cU as wi,cV as ki,cW as xi,cX as Si,cY as Mi,cZ as Di,c_ as Ci,c$ as Pi,d0 as Ii,d1 as Ti}from"./houseplan-card-Cs2QHIcW.js";import{i as Ri,c as Fi,a as Ni,s as zi,t as Ai}from"./space-deletion-CJ8Co_o8.js";const Oi={},Li=t(class extends e{constructor(){super(...arguments),this.ot=Oi}render(t,e){return e()}update(t,[e,i]){if(Array.isArray(e)){if(Array.isArray(this.ot)&&this.ot.length===e.length&&e.every((t,e)=>t===this.ot[e]))return s}else if(this.ot===e)return s;return this.ot=Array.isArray(e)?Array.from(e):e,this.render(e,i)}}),Ei=(t,e,s)=>Math.min(s,Math.max(e,Number.isFinite(t)?t:e));function Bi(t){return Number.isFinite(t)?(t%360+360)%360:0}function Hi(t){if("string"!=typeof t)return null;const e=t.trim().replace(/^#/,""),s=/^[0-9a-fA-F]{3}$/.test(e)?e.split("").map(t=>`${t}${t}`).join(""):e;return/^[0-9a-fA-F]{6}$/.test(s)?`#${s.toLowerCase()}`:null}function Gi(t){const e=Hi(t);return e?{r:Number.parseInt(e.slice(1,3),16),g:Number.parseInt(e.slice(3,5),16),b:Number.parseInt(e.slice(5,7),16)}:null}function Wi(t){const e=Ei(t.r,0,255)/255,s=Ei(t.g,0,255)/255,i=Ei(t.b,0,255)/255,o=Math.max(e,s,i),a=o-Math.min(e,s,i);let n=0;return a>0&&(n=o===e?(s-i)/a%6*60:o===s?60*((i-e)/a+2):60*((e-s)/a+4)),{h:Bi(n),s:0===o?0:a/o*100,v:100*o}}function ji(t){return function(t){const e=t=>Math.round(Ei(t,0,255)).toString(16).padStart(2,"0");return`#${e(t.r)}${e(t.g)}${e(t.b)}`}(function(t){const e=Bi(t.h),s=Ei(t.s,0,100)/100,i=Ei(t.v,0,100)/100,o=i*s,a=o*(1-Math.abs(e/60%2-1)),n=i-o;let r;return r=e<60?[o,a,0]:e<120?[a,o,0]:e<180?[0,o,a]:e<240?[0,a,o]:e<300?[a,0,o]:[o,0,a],{r:Math.round(255*(r[0]+n)),g:Math.round(255*(r[1]+n)),b:Math.round(255*(r[2]+n))}}(t))}function qi(t){const e=t.visualViewport;return e&&Number.isFinite(e.width)&&Number.isFinite(e.height)?{left:Number(e.offsetLeft)||0,top:Number(e.offsetTop)||0,width:Math.max(0,Number(e.width)||0),height:Math.max(0,Number(e.height)||0)}:{left:0,top:0,width:t.innerWidth,height:t.innerHeight}}function Ui(t,e,s,i=7,o=8){const a=s.left+o,n=s.top+o,r=Math.max(a,s.left+s.width-o),l=Math.max(n,s.top+s.height-o),h=Math.max(0,r-a),c=Math.max(0,l-n),d=Math.min(Math.max(0,e.width),h),p=Math.min(Math.max(0,e.height),c);let _=t.left;_+d>r&&(_=t.right-d),_=Math.min(Math.max(a,_),Math.max(a,r-d));const u=t.bottom+i,m=t.top-i-p;let g="bottom",f=u;return u+p>l&&m>=n?(g="top",f=Math.min(Math.max(n,m),Math.max(n,l-p))):f=Math.min(Math.max(n,u),Math.max(n,l-p)),{left:Math.round(_),top:Math.round(f),side:g,maxWidth:Math.round(h),maxHeight:Math.round(c)}}class Ki{constructor(t,e,s){this.owner=t,this.name=e,this.keydown=s,this.host=null,this.root=null}window(){return this.owner.ownerDocument.defaultView}dialog(){return this.owner.closest("hp-dialog")}usesPopover(t=!1){const e=this.window();return!t&&!!e&&function(t){const e=t.HTMLElement?.prototype;return"function"==typeof e?.showPopover&&"function"==typeof e?.hidePopover}(e)}get hasFallback(){return!!this.host}containsPath(t){return t.includes(this.owner)||!!this.host&&t.includes(this.host)}ownsActiveElement(){let t=this.owner.ownerDocument.activeElement;for(;t?.shadowRoot?.activeElement;)t=t.shadowRoot.activeElement;return!(!t||t!==this.owner&&!this.owner.shadowRoot?.contains(t)&&!this.root?.contains(t))}renderFallback(t,e){const s=this.ensureFallback();return s?(i(o`${t}`,s),s):null}surface(t,e){return e?this.owner.shadowRoot?.querySelector(t)||null:this.root?.querySelector(t)||null}destroy(){this.root&&i(a,this.root),this.host?.remove(),this.host=null,this.root=null}ensureFallback(){if(this.root?.isConnected)return this.root;const t=this.dialog()?.overlayPortal()||this.owner.ownerDocument.body;if(!t)return null;const e=this.owner.ownerDocument.createElement("div");return e.dataset.hpOverlay=this.name,e.style.display="contents",this.keydown&&e.addEventListener("keydown",this.keydown,!0),t.append(e),this.host=e,this.root=e.attachShadow({mode:"open"}),this.root}}const Vi={title:"Color picker",hue:"Hue",saturation:"Saturation",value:"Brightness",hex:"Hex color",invalidHex:"Enter a 3- or 6-digit hex color"};class Ji extends r{constructor(){super(...arguments),this.label="",this.opacityLabel="Opacity",this.color="#607d8b",this.opacity=1,this.disabled=!1,this.showOpacity=!0,this.pickerLabels=Vi,this._open=!1,this._pickerRaf=0,this._emitRaf=0,this._forceFallback=!1,this._overlayDispose=null,this._hue=0,this._saturation=0,this._value=0,this._hexDraft="#607d8b",this._hexInvalid=!1,this._lastValidColor="#607d8b",this._activePointerId=null,this._outsidePointerDown=t=>{if(!this._open)return;const e=t.composedPath();this._floating.containsPath(e)||this._closePicker(!1,"outside")},this._keyDown=t=>{this._open&&"Escape"===t.key&&(t.preventDefault(),t.stopImmediatePropagation(),this._closePicker(this._floating.ownsActiveElement(),"escape"))},this._floating=new Ki(this,"color-opacity",this._keyDown),this._queuePickerPosition=()=>{if(!this._open)return;this._pickerRaf&&cancelAnimationFrame(this._pickerRaf);const t=this._window();t&&(this._pickerRaf=t.requestAnimationFrame(()=>{this._pickerRaf=0,this._positionPicker()||this._closePicker()}))}}connectedCallback(){super.connectedCallback(),this._syncFromColor(this.color,!1),this.ownerDocument.addEventListener("pointerdown",this._outsidePointerDown,!0),this.ownerDocument.addEventListener("scroll",this._queuePickerPosition,!0);const t=this.ownerDocument.defaultView;t?.addEventListener("resize",this._queuePickerPosition),t?.addEventListener("orientationchange",this._queuePickerPosition),t?.visualViewport?.addEventListener("resize",this._queuePickerPosition),t?.visualViewport?.addEventListener("scroll",this._queuePickerPosition),this.addEventListener("keydown",this._keyDown,!0)}disconnectedCallback(){this.ownerDocument.removeEventListener("pointerdown",this._outsidePointerDown,!0),this.ownerDocument.removeEventListener("scroll",this._queuePickerPosition,!0);const t=this.ownerDocument.defaultView;t?.removeEventListener("resize",this._queuePickerPosition),t?.removeEventListener("orientationchange",this._queuePickerPosition),t?.visualViewport?.removeEventListener("resize",this._queuePickerPosition),t?.visualViewport?.removeEventListener("scroll",this._queuePickerPosition),this.removeEventListener("keydown",this._keyDown,!0),this._pickerRaf&&cancelAnimationFrame(this._pickerRaf),this._emitRaf&&cancelAnimationFrame(this._emitRaf),this._pickerRaf=0,this._emitRaf=0,this._closePicker(!1,"disconnect"),super.disconnectedCallback()}updated(t){if(t.has("color")){const t=Hi(this.color);t&&t!==this._lastValidColor&&this._syncFromColor(t,this._open)}this.disabled&&this._open?this._closePicker():this._open&&(this._supportsPopover()||this._renderFallback(),(t.has("color")||t.has("opacity")||t.has("showOpacity")||t.has("label")||t.has("opacityLabel")||t.has("pickerLabels"))&&this._queuePickerPosition())}_window(){return this._floating.window()}_supportsPopover(){return this._floating.usesPopover(this._forceFallback)}_dialog(){return this._floating.dialog()}async _toggle(){this.disabled||(this._open?this._closePicker():(this._forceFallback=!1,this._syncFromColor(this.color,!1),this._hexInvalid=!1,this._activePointerId=null,this._open=!0,await this.updateComplete,this._open&&(this._supportsPopover()||this._renderFallback(),this._positionPicker()?this._overlayDispose=this._dialog()?.registerOverlay({owner:this,group:"transient",close:t=>this._closePicker("escape"===t&&this._floating.ownsActiveElement(),t)})||null:this._closePicker())))}_closePicker(t=!1,e="exclusive"){if(!this._open&&!this._floating.hasFallback)return;const s=this._overlayDispose;this._overlayDispose=null,s?.();const i=this.renderRoot.querySelector(".picker");if(i?.hidePopover)try{i.matches(":popover-open")&&i.hidePopover()}catch{}this._open=!1,this._cancelQueuedEmit(),this._activePointerId=null,this._floating.destroy(),t&&this.updateComplete.then(()=>this.renderRoot.querySelector(".trigger")?.focus())}_surface(){return this._floating.surface(".picker",this._supportsPopover())}_renderFallback(){const t=Ji.styles.cssText;this._floating.renderFallback(this._pickerTemplate(!1),t)}_positionPicker(){if(!this._open)return!1;const t=this._window(),e=this.renderRoot.querySelector(".trigger");let s=this._surface();if(!t||!e?.isConnected||!s?.isConnected)return!1;const i=qi(t);if(s.style.maxWidth=`${Math.max(0,i.width-16)}px`,s.style.maxHeight=`${Math.max(0,i.height-16)}px`,s.style.visibility="hidden",this._supportsPopover()&&s.showPopover)try{s.matches(":popover-open")||s.showPopover()}catch{if(this._forceFallback=!0,this._renderFallback(),s=this._surface(),!s?.isConnected)return!1;s.style.maxWidth=`${Math.max(0,i.width-16)}px`,s.style.maxHeight=`${Math.max(0,i.height-16)}px`,s.style.visibility="hidden"}const o=e.getBoundingClientRect(),a=s.getBoundingClientRect();if(!(o.width&&o.height&&a.width&&a.height))return!1;const n=Ui(o,a,i);return s.style.left=`${n.left}px`,s.style.top=`${n.top}px`,s.dataset.side=n.side,s.style.visibility="",!0}_labels(){const t=this.pickerLabels||Vi;return{title:t.title||Vi.title,hue:t.hue||Vi.hue,saturation:t.saturation||Vi.saturation,value:t.value||Vi.value,hex:t.hex||Vi.hex,invalidHex:t.invalidHex||Vi.invalidHex}}_syncFromColor(t,e){const s=Hi(l(t,"#607d8b"))||"#607d8b",i=Gi(s);if(!i)return;const o=Wi(i);(!e||o.s>1e-4)&&(this._hue=o.h),this._saturation=o.s,this._value=o.v,this._hexDraft=s,this._hexInvalid=!1,this._lastValidColor=s}_cancelQueuedEmit(t=!1){this._emitRaf&&cancelAnimationFrame(this._emitRaf),this._emitRaf=0,t&&this._syncFromColor(this._lastValidColor,!0)}_queueHsvEmit(){if(this._emitRaf)return;const t=this._window();t?this._emitRaf=t.requestAnimationFrame(()=>{this._emitRaf=0,this._emitHsv()}):this._emitHsv()}_emitHsv(){this._emit(ji({h:this._hue,s:this._saturation,v:this._value}),this.opacity)}_setHue(t){this._hue=Math.min(359,Math.max(0,Number.isFinite(t)?t:0)),this._emitHsv()}_setSaturation(t){this._saturation=Math.min(100,Math.max(0,Number.isFinite(t)?t:0)),this._emitHsv()}_setValue(t){this._value=Math.min(100,Math.max(0,Number.isFinite(t)?t:0)),this._emitHsv()}_setOpacity(t){this._emit(this._lastValidColor,t)}_shiftRangeKey(t,e,s,i,o){if(!t.shiftKey)return;const a="ArrowRight"===t.key||"ArrowUp"===t.key?10:"ArrowLeft"===t.key||"ArrowDown"===t.key?-10:0;a&&(t.preventDefault(),t.stopPropagation(),o(Math.min(i,Math.max(s,e+a))))}_setSvFromPointer(t,e){const s=t.currentTarget.getBoundingClientRect();s.width&&s.height&&(this._saturation=Math.min(100,Math.max(0,(t.clientX-s.left)/s.width*100)),this._value=Math.min(100,Math.max(0,100*(1-(t.clientY-s.top)/s.height))),e?(this._cancelQueuedEmit(),this._emitHsv()):this._queueHsvEmit())}_svPointerDown(t){if(t.preventDefault(),t.stopPropagation(),null===this._activePointerId){this._activePointerId=t.pointerId;try{t.currentTarget.setPointerCapture(t.pointerId)}catch{}this._setSvFromPointer(t,!0)}}_svPointerMove(t){t.pointerId===this._activePointerId&&(t.preventDefault(),t.stopPropagation(),this._setSvFromPointer(t,!1))}_svPointerUp(t){if(t.pointerId===this._activePointerId){t.preventDefault(),t.stopPropagation(),this._setSvFromPointer(t,!0),this._activePointerId=null;try{t.currentTarget.releasePointerCapture(t.pointerId)}catch{}}}_svPointerCancel(t){t.pointerId===this._activePointerId&&(t.preventDefault(),t.stopPropagation(),this._activePointerId=null,this._cancelQueuedEmit(!0))}_svLostPointerCapture(t){t.pointerId===this._activePointerId&&(this._activePointerId=null,this._cancelQueuedEmit(!0))}_hexInput(t){const e=t.target.value;this._hexDraft=e,this._hexInvalid=!1;const s=Hi(e),i=s?Gi(s):null;if(!s||!i)return;const o=Wi(i);o.s>1e-4&&(this._hue=o.h),this._saturation=o.s,this._value=o.v,this._emit(s,this.opacity,!0)}_commitHex(){const t=Hi(this._hexDraft);if(!t)return this._hexDraft=this._lastValidColor,void(this._hexInvalid=!0);this._hexDraft=t,this._hexInvalid=!1,t!==this._lastValidColor&&this._emit(t,this.opacity)}_hexKeyDown(t){"Enter"===t.key&&(t.preventDefault(),t.stopPropagation(),this._commitHex())}_emit(t,e,s=!1){const i=Hi(t)||this._lastValidColor,o=Number.isFinite(e)?e:Number(this.opacity)||0,a=Math.min(1,Math.max(0,o));this.color=i,this.opacity=a,this._lastValidColor=i,s||(this._hexDraft=i),this._hexInvalid=!1,this.dispatchEvent(new CustomEvent("hp-color-opacity-change",{detail:{color:i,opacity:a},bubbles:!0,composed:!0}))}_pickerTemplate(t){const e=Math.round(100*Math.min(1,Math.max(0,Number(this.opacity)||0))),s=ji({h:this._hue,s:this._saturation,v:this._value}),i=ji({h:this._hue,s:100,v:100}),n=this._labels(),r=Math.round(this._hue),l=Math.round(this._saturation),h=Math.round(this._value);return o` + `}render(){const t=Math.round(100*Math.min(1,Math.max(0,Number(this.opacity)||0))),e=l(this.color,"#607d8b"),s=this.showOpacity?`${this.label||"Color"}: ${e}, ${t}%`:`${this.label||"Color"}: ${e}`;return o` + ${this.label?o`${this.label}`:a} + + ${this._open&&!this.disabled&&this._supportsPopover()?this._pickerTemplate(!0):a} + `}}Ji.properties={label:{type:String},opacityLabel:{type:String,attribute:"opacity-label"},color:{type:String},opacity:{type:Number},disabled:{type:Boolean,reflect:!0},showOpacity:{type:Boolean,attribute:"show-opacity"},pickerLabels:{attribute:!1},_open:{state:!0},_hue:{state:!0},_saturation:{state:!0},_value:{state:!0},_hexDraft:{state:!0},_hexInvalid:{state:!0}},Ji.styles=n` + :host { + position: relative; + display: inline-flex; + align-items: center; + gap: 6px; + min-width: 0; + } + .label { + font-size: 12px; + color: var(--secondary-text-color, #9aa4ad); + white-space: nowrap; + } + .trigger { + width: 40px; + height: 40px; + flex: none; + box-sizing: border-box; + padding: 3px; + border: 1px solid var(--divider-color, #666); + border-radius: 7px; + background: + linear-gradient(45deg, #b8b8b8 25%, transparent 25%) 0 0 / 10px 10px, + linear-gradient(45deg, transparent 75%, #b8b8b8 75%) 0 0 / 10px 10px, + linear-gradient(45deg, transparent 75%, #b8b8b8 75%) 5px -5px / 10px 10px, + linear-gradient(45deg, #b8b8b8 25%, #eee 25%) 5px 5px / 10px 10px; + cursor: pointer; + } + :host([data-pointer-hover]) .trigger:hover { + border-color: var(--primary-color, #03a9f4); + } + .trigger[aria-expanded='true'] { + border-color: var(--primary-color, #03a9f4); + outline: none; + box-shadow: 0 0 0 1px var(--primary-color, #03a9f4); + } + .trigger:focus-visible { + border-color: var(--primary-color, #03a9f4); + outline: 2px solid var(--primary-color, #03a9f4); + outline-offset: 2px; + } + .swatch { + display: block; + width: 100%; + height: 100%; + border-radius: 3px; + box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.18); + pointer-events: none; + } + .picker { + /* The popover attribute promotes this surface into the browser top layer, outside + hp-dialog's scrolling/clipping body. JS supplies viewport coordinates. */ + position: fixed; + z-index: 2147483647; + inset: auto; + top: 0; + left: 0; + width: min(292px, calc(100vw - 16px)); + max-height: calc(100vh - 16px); + margin: 0; + box-sizing: border-box; + display: grid; + gap: 10px; + padding: 12px; + overflow: auto; + color: var(--primary-text-color, #fff); + background: var(--card-background-color, #202126); + border: 1px solid var(--primary-color, #03a9f4); + border-radius: 10px; + box-shadow: 0 10px 28px rgb(0 0 0 / 0.34); + } + .picker[popover]:not(:popover-open) { + /* Keep the author-level display:grid from exposing the surface for one + frame before showPopover() promotes it into the top layer. */ + display: none; + } + .picker-head, + .row, + .control-head { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + } + .picker-head { + justify-content: space-between; + } + .caption { + flex: 1; + min-width: 0; + overflow: hidden; + color: var(--secondary-text-color, #9aa4ad); + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; + } + .preview { + width: 52px; + height: 30px; + flex: none; + box-sizing: border-box; + padding: 3px; + border: 1px solid var(--divider-color, #666); + border-radius: 7px; + background: + linear-gradient(45deg, #b8b8b8 25%, transparent 25%) 0 0 / 10px 10px, + linear-gradient(45deg, transparent 75%, #b8b8b8 75%) 0 0 / 10px 10px, + linear-gradient(45deg, transparent 75%, #b8b8b8 75%) 5px -5px / 10px 10px, + linear-gradient(45deg, #b8b8b8 25%, #eee 25%) 5px 5px / 10px 10px; + } + .preview > span { + display: block; + width: 100%; + height: 100%; + border-radius: 3px; + } + .sv-field { + position: relative; + width: 100%; + height: 132px; + overflow: hidden; + box-sizing: border-box; + border: 1px solid var(--divider-color, #666); + border-radius: 8px; + background: + linear-gradient(to top, #000, transparent), + linear-gradient(to right, #fff, var(--hp-picker-hue, #f00)); + cursor: crosshair; + touch-action: none; + user-select: none; + } + .sv-thumb { + position: absolute; + left: var(--hp-picker-saturation, 0%); + top: var(--hp-picker-value, 100%); + width: 14px; + height: 14px; + box-sizing: border-box; + border: 2px solid #fff; + border-radius: 50%; + box-shadow: 0 0 0 1px rgb(0 0 0 / .65), 0 1px 3px rgb(0 0 0 / .5); + transform: translate(-50%, -50%); + pointer-events: none; + } + .control { + display: grid; + gap: 2px; + min-width: 0; + } + .control-value { + flex: none; + color: var(--secondary-text-color, #9aa4ad); + font-size: 12px; + } + input[type='range'] { + width: auto; + height: 40px; + min-width: 0; + flex: 1; + margin: -5px 0; + accent-color: var(--primary-color, #03a9f4); + touch-action: none; + } + .hue-range { + accent-color: var(--hp-picker-hue, #f00); + --hp-picker-hue-track: linear-gradient(to right, + #f00 0%, #ff0 16.667%, #0f0 33.333%, #0ff 50%, + #00f 66.667%, #f0f 83.333%, #f00 100%); + --hp-picker-hue-thumb-shadow: + 0 0 0 2px var(--card-background-color, #202126), + 0 0 0 3px var(--primary-text-color, #fff); + } + .hue-range::-webkit-slider-runnable-track { + height: 10px; + box-sizing: border-box; + border: 1px solid var(--divider-color, #666); + border-radius: 999px; + background: var(--hp-picker-hue-track); + } + .hue-range::-moz-range-track { + height: 10px; + box-sizing: border-box; + border: 1px solid var(--divider-color, #666); + border-radius: 999px; + background: var(--hp-picker-hue-track); + } + .hue-range::-moz-range-progress { + height: 10px; + border: 0; + border-radius: 999px; + background: transparent; + } + .hue-range::-webkit-slider-thumb { + border-radius: 50%; + box-shadow: var(--hp-picker-hue-thumb-shadow); + } + .hue-range::-moz-range-thumb { + border-radius: 50%; + box-shadow: var(--hp-picker-hue-thumb-shadow); + } + input[type='text'], + input[type='number'] { + min-height: 40px; + box-sizing: border-box; + border: 1px solid var(--divider-color, #666); + border-radius: 6px; + padding: 6px 8px; + color: var(--primary-text-color, #fff); + background: var(--input-fill-color, transparent); + font: inherit; + } + input[type='text'] { + width: 100%; + } + input[type='number'] { + width: 50px; + flex: none; + } + input:focus-visible, + .sv-field:focus-visible { + outline: 2px solid var(--primary-color, #03a9f4); + outline-offset: 1px; + } + input[aria-invalid='true'] { + border-color: var(--error-color, #db4437); + } + .error { + color: var(--error-color, #db4437); + font-size: 11px; + line-height: 1.25; + } + .pct { + color: var(--secondary-text-color, #9aa4ad); + font-size: 12px; + } + :host([disabled]) { + opacity: .5; + pointer-events: none; + } + @media (prefers-reduced-motion: reduce) { + .picker { transition: none !important; } + } + @media (forced-colors: active) { + .hue-range::-webkit-slider-runnable-track { + border-color: ButtonText; + background: Canvas; + } + .hue-range::-moz-range-track { + border-color: ButtonText; + background: Canvas; + } + .hue-range::-webkit-slider-thumb { + box-shadow: none; + } + .hue-range::-moz-range-thumb { + box-shadow: none; + } + } + `,customElements.get("hp-color-opacity")||customElements.define("hp-color-opacity",Ji);let Yi=0;class Xi extends r{constructor(){super(...arguments),this.text="",this.ariaLabel="",this.pointerHover=!1,this._open=!1,this._openTimer=0,this._closeTimer=0,this._positionRaf=0,this._forceFallback=!1,this._hoverOwned=!1,this._overlayDispose=null,this._scrollDialog=null,this._descriptionId="hp-help-description-"+ ++Yi,this._surfacePointerEnter=()=>{const t=this._window();this._closeTimer&&t?.clearTimeout(this._closeTimer),this._closeTimer=0},this._surfacePointerLeave=()=>{this._scheduleClose()},this._outsidePointerDown=t=>{if(!this._open)return;const e=t.composedPath();this._floating.containsPath(e)||this._closeHelp(!1,"outside")},this._dialogScroll=t=>{const e=this._floating.containsPath(t.composedPath()),s=t.target,i=this._dialog(),o=s instanceof Node&&(s===i||s instanceof Element&&s.contains(this));var a;this._open&&(a=o,!e&&a)&&this._closeHelp(!1,"scroll")},this._keyDown=t=>{if(!this._open||t.defaultPrevented)return;if("Escape"===t.key)return t.preventDefault(),t.stopImmediatePropagation(),void this._closeHelp(this._floating.ownsActiveElement(),"escape");const e=this.renderRoot.querySelector(".trigger");if(!e||!t.composedPath().includes(e))return;const s=this._surface();if(!s||s.scrollHeight<=s.clientHeight)return;const i=Math.max(40,.8*s.clientHeight),o={ArrowDown:40,ArrowUp:-40,PageDown:i,PageUp:-i};if("Home"===t.key)s.scrollTop=0;else if("End"===t.key)s.scrollTop=s.scrollHeight;else{if(!(t.key in o))return;s.scrollBy({top:o[t.key],behavior:"auto"})}t.preventDefault(),t.stopImmediatePropagation()},this._floating=new Ki(this,"help",this._keyDown),this._queuePosition=()=>{if(!this._open||this._positionRaf)return;const t=this._window();t&&(this._positionRaf=t.requestAnimationFrame(()=>{this._positionRaf=0,this._position()||this._closeHelp()}))}}connectedCallback(){super.connectedCallback(),this.addEventListener("keydown",this._keyDown,!0)}willUpdate(){this.toggleAttribute("data-has-content",this._hasContent())}disconnectedCallback(){const t=this.ownerDocument.defaultView;this._unsubscribeOpenListeners(),this.removeEventListener("keydown",this._keyDown,!0),this._clearTimers(),this._positionRaf&&t?.cancelAnimationFrame(this._positionRaf),this._positionRaf=0,this._closeHelp(!1,"disconnect"),super.disconnectedCallback()}updated(t){if(t.has("pointerHover")&&!this.pointerHover){const t=this._window();this._openTimer&&t?.clearTimeout(this._openTimer),this._openTimer=0,this._hoverOwned&&this._closeHelp(!1,"outside")}this._hasContent()||!this._open?this._open&&t.has("text")&&(this._usesPopover()||this._renderFallback(),this._queuePosition()):this._closeHelp()}_window(){return this._floating.window()}_dialog(){return this._floating.dialog()}_usesPopover(){return this._floating.usesPopover(this._forceFallback)}_hasContent(){return t=this.text,e=this.ariaLabel,"string"==typeof t&&t.trim().length>0&&"string"==typeof e&&e.trim().length>0;var t,e}_clearTimers(){const t=this._window();this._openTimer&&t?.clearTimeout(this._openTimer),this._closeTimer&&t?.clearTimeout(this._closeTimer),this._openTimer=0,this._closeTimer=0}_scheduleOpen(){const t=this._window();this.pointerHover&&t&&!this._open&&!this._openTimer&&(this._closeTimer&&t.clearTimeout(this._closeTimer),this._closeTimer=0,this._openTimer=t.setTimeout(()=>{this._openTimer=0,this.isConnected&&(this._hoverOwned=!0,this._openHelp())},300))}_scheduleClose(){const t=this._window(),e=this.renderRoot.querySelector(".trigger");t&&!e?.matches(":focus-visible")&&(this._openTimer&&t.clearTimeout(this._openTimer),this._closeTimer&&t.clearTimeout(this._closeTimer),this._openTimer=0,this._closeTimer=t.setTimeout(()=>{this._closeTimer=0,this.isConnected&&this._closeHelp()},150))}_triggerPointerEnter(t){this.pointerHover&&"mouse"===t.pointerType&&this._scheduleOpen()}_triggerPointerLeave(t){"mouse"===t.pointerType&&this._scheduleClose()}_triggerFocus(){queueMicrotask(()=>{const t=this.renderRoot.querySelector(".trigger");t?.matches(":focus-visible")&&(this._hoverOwned=!1,this._openHelp())})}_triggerBlur(){this._scheduleClose()}_triggerClick(){this._open?this._closeHelp():(this._hoverOwned=!1,this._openHelp())}_subscribeOpenListeners(){this.ownerDocument.addEventListener("pointerdown",this._outsidePointerDown,!0),this.ownerDocument.addEventListener("keydown",this._keyDown,!0);const t=this._window();t?.addEventListener("resize",this._queuePosition),t?.addEventListener("orientationchange",this._queuePosition),t?.visualViewport?.addEventListener("resize",this._queuePosition),t?.visualViewport?.addEventListener("scroll",this._queuePosition),this._scrollDialog=this._dialog(),this._scrollDialog?.addEventListener("scroll",this._dialogScroll,!0)}_unsubscribeOpenListeners(){this.ownerDocument.removeEventListener("pointerdown",this._outsidePointerDown,!0),this.ownerDocument.removeEventListener("keydown",this._keyDown,!0);const t=this._window();t?.removeEventListener("resize",this._queuePosition),t?.removeEventListener("orientationchange",this._queuePosition),t?.visualViewport?.removeEventListener("resize",this._queuePosition),t?.visualViewport?.removeEventListener("scroll",this._queuePosition),this._scrollDialog?.removeEventListener("scroll",this._dialogScroll,!0),this._scrollDialog=null}async _openHelp(){!this._open&&this._hasContent()&&(this._clearTimers(),this._forceFallback=!1,this._open=!0,this._subscribeOpenListeners(),await this.updateComplete,this._open&&(this._usesPopover()||this._renderFallback(),this._position()?this._overlayDispose=this._dialog()?.registerOverlay({owner:this,group:"transient",close:t=>this._closeHelp("escape"===t&&this._floating.ownsActiveElement(),t)})||null:this._closeHelp()))}_closeHelp(t=!1,e="exclusive"){if(!this._open&&!this._floating.hasFallback)return;this._clearTimers();const s=this._overlayDispose;this._overlayDispose=null,s?.();const i=this.renderRoot.querySelector(".tooltip");if(i?.hidePopover)try{i.matches(":popover-open")&&i.hidePopover()}catch{}this._open=!1,this._hoverOwned=!1,this._unsubscribeOpenListeners(),this._floating.destroy(),t&&this.updateComplete.then(()=>this.renderRoot.querySelector(".trigger")?.focus())}_tooltipTemplate(t){return o``}_renderFallback(){const t=Xi.styles.cssText;this._floating.renderFallback(this._tooltipTemplate(!1),t)}_surface(){return this._floating.surface(".tooltip",this._usesPopover())}_position(){if(!this._open)return!1;const t=this._window(),e=this.renderRoot.querySelector(".trigger");let s=this._surface();if(!t||!e?.isConnected||!s?.isConnected)return!1;const i=qi(t);if(s.style.maxWidth=`${Math.max(0,Math.min(320,i.width-16))}px`,s.style.maxHeight=`${Math.max(0,i.height-16)}px`,s.style.visibility="hidden",this._usesPopover()&&s.showPopover)try{s.matches(":popover-open")||s.showPopover()}catch{if(this._forceFallback=!0,this._renderFallback(),s=this._surface(),!s?.isConnected)return!1;s.style.visibility="hidden"}const o=e.getBoundingClientRect(),a=s.getBoundingClientRect();if(!(o.width&&o.height&&a.width&&a.height))return!1;const n=Ui(o,a,i);return s.style.left=`${n.left}px`,s.style.top=`${n.top}px`,s.dataset.side=n.side,s.style.visibility="",!0}render(){if(!this._hasContent())return a;const t=this.ariaLabel.trim();return o` + ${this.text} + + ${this._usesPopover()?this._tooltipTemplate(!0):a} + `}}Xi.properties={text:{type:String},ariaLabel:{type:String,attribute:"aria-label"},pointerHover:{type:Boolean,attribute:"data-pointer-hover",reflect:!0},_open:{state:!0},_forceFallback:{state:!0}},Xi.styles=n` + :host { + display: none; + flex: none; + vertical-align: middle; + } + + :host([data-has-content]) { + display: inline-flex; + } + + .trigger { + width: 32px; + height: 32px; + display: inline-flex; + align-items: center; + justify-content: center; + flex: none; + box-sizing: border-box; + padding: 0; + color: var(--secondary-text-color, #9aa4ad); + background: transparent; + border: 0; + border-radius: 50%; + cursor: help; + -webkit-tap-highlight-color: transparent; + } + + .trigger svg { + display: block; + width: 18px; + height: 18px; + fill: currentColor; + } + + :host([data-pointer-hover]) .trigger:hover, + .trigger:focus-visible, + .trigger[aria-expanded='true'] { + color: var(--primary-text-color, #fff); + background: rgb(127 127 127 / 0.18); + outline: none; + } + + .trigger:focus-visible { + box-shadow: 0 0 0 2px var(--primary-color, #03a9f4); + } + + .tooltip { + position: fixed; + z-index: 2147483647; + inset: auto; + top: 0; + left: 0; + width: max-content; + max-width: min(320px, calc(100vw - 16px)); + max-height: calc(100vh - 16px); + margin: 0; + box-sizing: border-box; + padding: 9px 11px; + overflow: auto; + color: var(--primary-text-color, #fff); + background: var(--card-background-color, #202126); + border: 1px solid var(--divider-color, rgb(255 255 255 / 0.2)); + border-radius: 9px; + box-shadow: 0 8px 24px rgb(0 0 0 / 0.32); + font: 400 13px/1.4 system-ui, sans-serif; + text-align: start; + white-space: normal; + overflow-wrap: anywhere; + opacity: 1; + transform: translateY(0); + transition: opacity 120ms ease, transform 120ms ease; + } + + .tooltip[data-side='top'] { + transform-origin: bottom center; + } + + .tooltip[data-side='bottom'] { + transform-origin: top center; + } + + .tooltip[popover]:not(:popover-open) { + display: none; + } + + @starting-style { + .tooltip { + opacity: 0; + transform: translateY(3px); + } + } + + .sr-only { + /* Keep the accessibility-only description outside every dialog scroll + container's overflow geometry. Toggling the hidden attribute on the former + absolutely-positioned node made some browsers add a vertical + scrollbar while help was open, even though the visible tooltip was + already in the Popover top layer / fixed fallback portal. */ + position: fixed; + inset: 0 auto auto 0; + width: 1px; + height: 1px; + padding: 0; + margin: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + clip-path: inset(50%); + white-space: nowrap; + border: 0; + } + + @media (pointer: coarse) { + .trigger { + width: 40px; + height: 40px; + } + } + + @media (prefers-reduced-motion: reduce) { + .tooltip { + transition: none; + } + } + `,customElements.get("hp-help")||customElements.define("hp-help",Xi);const Zi=new WeakMap;function Qi(t){const e=function(t){const e=t?.connection||t;return!e||"object"!=typeof e&&"function"!=typeof e?null:e}(t);if(!e)return null;let s=Zi.get(e);return s||(s={revision:0,loaded:!1,configEntries:{},manifests:{},listeners:new Set,refs:0},Zi.set(e,s)),s}function to(t){t.revision++;for(const e of[...t.listeners])try{e()}catch{}}function eo(t,e){const s=Qi(t);if(!s)return()=>{};s.refs++,s.listeners.add(e),async function(t,e){e.loading||"function"!=typeof t?.callWS?e.loading:e.loaded&&e.loadedAt&&Date.now()-e.loadedAt<3e5||(e.loading=(async()=>{const[s,i]=await Promise.allSettled([t.callWS({type:"config_entries/get"}),t.callWS({type:"manifest/list"})]);"fulfilled"===s.status&&Array.isArray(s.value)&&(e.configEntries=Object.fromEntries(s.value.filter(t=>"string"==typeof t?.entry_id).map(t=>[t.entry_id,t]))),"fulfilled"===i.status&&Array.isArray(i.value)&&(e.manifests=Object.fromEntries(i.value.filter(t=>"string"==typeof t?.domain).map(t=>[t.domain,t]))),e.loaded=!0,e.loadedAt=Date.now(),e.loading=void 0,to(e)})().catch(()=>{e.loaded=!0,e.loadedAt=Date.now(),e.loading=void 0,to(e)}),e.loading)}(t,s),async function(t,e){if(e.unsubscribe||e.subscribing)return e.subscribing;const s=t?.connection?.subscribeMessage;"function"==typeof s&&(e.subscribing=(async()=>{try{e.unsubscribe=await s.call(t.connection,t=>{const s=Array.isArray(t)?t:Array.isArray(t?.entries)?t.entries:t?[t]:[];let i=!1;const o={...e.configEntries};for(const t of s){const e=t?.entry||t?.data?.entry||("string"==typeof t?.entry_id?t:null),s=e?.entry_id;if("string"!=typeof s)continue;const a=t?.type||t?.action||t?.data?.action;"removed"===a||"remove"===a?delete o[s]:o[s]=e,i=!0}i&&(e.configEntries=o,to(e))},{type:"config_entries/subscribe"})}catch{}finally{0===e.refs&&(e.unsubscribe?.(),e.unsubscribe=void 0),e.subscribing=void 0}})(),e.subscribing)}(t,s);let i=!1;return()=>{i||(i=!0,s.listeners.delete(e),s.refs=Math.max(0,s.refs-1),s.refs||(s.unsubscribe?.(),s.unsubscribe=void 0))}}function so(t){const e=Qi(t);return e?{revision:e.revision,loaded:e.loaded,configEntries:e.configEntries,manifests:e.manifests}:{revision:0,loaded:!1,configEntries:{},manifests:{}}}function io(t,e,s=so(t),i){const o=s.manifests[e];return function(t,e){if("function"!=typeof t?.localize)return"";try{const s=`component.${e}.title`,i=t.localize(s);return"string"==typeof i&&i!==s?i:""}catch{return""}}(t,e)||String(o?.name||"")||String(i||"")||e.replace(/_/g," ")}function oo(t,e,s,i,o){const a=String(e||"").trim();if(!a)return null;const n=o?i.configEntries[o]:null;return{domain:a,label:io(t,a,i,n?.title),configEntryId:o,configEntryTitle:n?.title,confidence:s}}function ao(t){const e={"registry-owner":0,"entity-platform":1,"identifier-fallback":2},s=new Set;return t.filter(t=>!!t).sort((t,s)=>e[t.confidence]-e[s.confidence]||t.label.localeCompare(s.label)||t.domain.localeCompare(s.domain)).filter(t=>{const e=`${t.domain}\n${t.configEntryId||""}`;return!s.has(e)&&(s.add(e),!0)})}function no(t,e,s,i=so(t)){const o=s.entities?.[e]||t?.entities?.[e];if(!o)return null;const a=o.config_entry_id||void 0,n=a?i.configEntries[a]?.domain:"";return oo(t,o.platform||n,"entity-platform",i,a)}class ro extends r{constructor(){super(...arguments),this.deviceName="",this._metadataConnection=null,this._demoUntil=0,this._demoTimer=0,this._demoGeneration=1,this._demoKind=null,this._reducedMotion=!1,this._onMotionChange=t=>{this._reducedMotion=t.matches,this.requestUpdate()},this._startShortDemo=()=>{const t=this.presentation;t&&"icon_ripple"===t.display&&"none"===t.pulse.kind&&(this._demoGeneration++,this._demoKind="short",this._demoUntil=Date.now()+3300,window.clearTimeout(this._demoTimer),this._demoTimer=window.setTimeout(()=>{this._demoUntil=0,this._demoTimer=0,this._demoKind=null,this.requestUpdate()},3330),this.requestUpdate())},this._toggleContinuousDemo=()=>{const t=this.presentation;t&&"icon_ripple"===t.display&&"none"===t.pulse.kind&&("continuous"===this._demoKind?this._clearDemo():(this._clearDemo(),this._demoGeneration++,this._demoKind="continuous",this._demoUntil=Number.POSITIVE_INFINITY),this.requestUpdate())}}connectedCallback(){super.connectedCallback(),this._motionMedia=window.matchMedia?.("(prefers-reduced-motion: reduce)"),this._reducedMotion=!!this._motionMedia?.matches,this._motionMedia?.addEventListener?.("change",this._onMotionChange),this._ensureMetadata()}disconnectedCallback(){this._metadataRelease?.(),this._metadataRelease=void 0,this._metadataConnection=null,this._clearDemo(),this._motionMedia?.removeEventListener?.("change",this._onMotionChange),this._motionMedia=void 0,super.disconnectedCallback()}willUpdate(t){t.has("hass")&&this._ensureMetadata();const e=this.presentation,s=t.get("presentation");t.has("presentation")&&s?.binding!==e?.binding&&this._clearDemo(),e&&this._demoKind&&("icon_ripple"!==e.display||"none"!==e.pulse.kind)&&this._clearDemo()}_ensureMetadata(){const t=this.hass?.connection||null;t&&t!==this._metadataConnection&&(this._metadataRelease?.(),this._metadataConnection=t,this._metadataRelease=eo(this.hass,()=>this.requestUpdate()))}_clearDemo(){window.clearTimeout(this._demoTimer),this._demoTimer=0,this._demoUntil=0,this._demoKind=null}get _lang(){return d(this.hass)}_t(t,e){return p(this._lang,t,e)}_providerText(t){return"houseplan"===t.domain?this._t("marker.preview.virtual_provider"):t.label.toLowerCase()===t.domain.toLowerCase()?t.domain:`${t.label} (${t.domain})`}_providers(){return this.presentation&&this.registry?function(t,e,s,i=so(t)){if("virtual"===e)return[{domain:"houseplan",label:"House Plan",confidence:"registry-owner"}];const o=e.indexOf(":");if(o<1)return[];const a=e.slice(0,o),n=e.slice(o+1);if("entity"===a){const e=no(t,n,s,i);return e?[e]:[]}if("device"!==a)return[];const r=s.devices?.[n]||t?.devices?.[n];if(!r)return[];const l=[r.config_entry_id,...Array.isArray(r.config_entries)?r.config_entries:[]].filter(t=>"string"==typeof t&&!!t).map(e=>{const s=i.configEntries[e];return oo(t,s?.domain,"registry-owner",i,e)});if(l.some(Boolean))return ao(l);const h=[];for(const[e,o]of Object.entries(s.entities||{}))o?.device_id===n&&null==o?.disabled_by&&h.push(no(t,e,s,i));return h.some(Boolean)?ao(h):ao((Array.isArray(r.identifiers)?r.identifiers:[]).map(e=>{const s=Array.isArray(e)?e[0]:null;return oo(t,s,"identifier-fallback",i)}))}(this.hass,this.presentation.binding,this.registry,so(this.hass)):[]}_providerSummary(t){if(!t.length)return this._t("marker.preview.unknown_provider");const e=t.slice(0,2).map(t=>this._providerText(t)),s=t.length-e.length;return e.join(", ")+(s>0?` · ${this._t("marker.preview.more_sources",{n:s})}`:"")}_sourceSummary(t){const e=t.visualSources;return e.length?1===e.length?`${e[0].name} · ${e[0].eid} · ${this._sourceProvider(e[0].eid)}`:2===e.length?e.map(t=>t.name).join(" · "):this._t("marker.preview.multiple_sources",{n:e.length}):this._t("marker.preview.no_source")}_stateSummary(t){if(!t.visualSources.length)return this._t("marker.preview.no_state");if(1===t.visualSources.length)return t.visualSources[0].stateText||this._t("marker.preview.no_state");const e=[...new Set(t.visualSources.map(t=>t.stateText).filter(Boolean))];return 1===e.length?e[0]:this._t("marker.preview.mixed_states")}_reason(t){return this._t(`marker.preview.reason.${t}`)}_sourceProvider(t){if(!this.registry)return this._t("marker.preview.unknown_provider");const e=no(this.hass,t,this.registry,so(this.hass));return e?this._providerText(e):this._t("marker.preview.unknown_provider")}render(){const t=this.presentation;if(!t)return a;const e=!!this._demoKind&&this._demoUntil>Date.now()&&"icon_ripple"===t.display&&"none"===t.pulse.kind,s=e?(()=>{const e=_(t.pulse,this._demoKind,this._demoGeneration,this._reducedMotion,"short"===this._demoKind?this._demoUntil:null);return{...t,pulse:e,classes:[...t.classes.filter(t=>!t.startsWith("pulse-")),`pulse-${e.kind}`,...e.generation%2==0?["pulse-gen2"]:[]]}})():t,i=this._providers(),n="none"!==t.pulse.kind,r=s.scale*(s.pulse.animated?s.pulse.diameterScale:1);let l=-r/2,h=r/2,c=-r/2,d=r/2;if(s.valueBadge){const t=s.scale*Math.min(4,Math.max(.9,.29*s.valueBadge.text.length+.28)),e=.7*s.scale,i=.6*s.scale;"right"===s.valueBadge.position&&(h=Math.max(h,i+t)),"left"===s.valueBadge.position&&(l=Math.min(l,-i-t)),"top"===s.valueBadge.position&&(c=Math.min(c,-i-e)),"bottom"===s.valueBadge.position&&(d=Math.max(d,i+e))}if(null!=s.lqiText){const t=s.scale*("bottom"===s.valueBadge?.position?1.55:.95);d=Math.max(d,t)}const p=Math.min(1,2.35/Math.max(1,h-l),2.35/Math.max(1,d-c)),b=Math.round(100*p),y=[`left:calc(50% - ${54*((l+h)/2)*p}px)`,`top:calc(50% - ${54*((c+d)/2)*p}px)`,p<1?`transform:scale(${p})`:""].filter(Boolean).join(";"),v=["dev",u(this.hass),...s.classes,"virtual"===s.binding?"virtual":"",s.haDisabled?"ghost ha-disabled":"",null!=s.valueText?"valonly":""].filter(Boolean).join(" "),$=m(s).join(";"),w=e?this._t("continuous"===this._demoKind?"marker.preview.demo_continuous_notice":"marker.preview.demo_short_notice"):this._reason(t.explanation.reason),k=[this.deviceName,this._providerSummary(i),this._stateSummary(t),w,t.valueFullText||"",g(t.valueBadge),null!=t.lqiText&&t.lqiBand?this._t(`marker.lqi_a11y_${t.lqiBand}`,{value:t.lqiText}):""].filter(Boolean).join(". ");return o`
+
+ ${this._t("marker.preview.title")} + + ${this._t(e?"marker.preview.example":"marker.preview.actual")} + +
+
+ +
+
${this._t("marker.preview.integration")}${this._providerSummary(i)}
+
${this._t("marker.preview.source")}${this._sourceSummary(t)}
+
${this._t("marker.preview.current_state")}${this._stateSummary(t)}
+
${this._t("marker.preview.result")}${w}
+ ${t.fallbackReason?o`

${this._reason(t.fallbackReason)}

`:a} + ${t.explanation.notices.map(t=>o`

${this._reason(t)}

`)} + ${p<1?o`

${this._t("marker.preview.scaled",{n:b})}

`:a} + ${e&&this._reducedMotion?o`

${this._t("marker.preview.reduced_motion")}

`:a} +
+
+ ${t.visualSources.length||t.criticalSources.length?o`
+ ${this._t("marker.preview.details")} +
+ ${[...t.visualSources,...t.criticalSources].map(t=>o` +
+ ${t.name}${t.eid} + ${t.stateText||t.state} + ${this._sourceProvider(t.eid)} +
`)} +
+
`:a} + ${"icon_ripple"===t.display?o`
+ + +
`:a} +
`}}ro.properties={hass:{attribute:!1},presentation:{attribute:!1},registry:{attribute:!1},deviceName:{attribute:!1}},ro.styles=[c,n` + :host { display: block; min-width: 0; } + .devicepreview { + margin: 4px 0 14px; + padding: 14px; + border: 1px solid var(--divider-color, rgba(127, 127, 127, 0.28)); + border-radius: 14px; + background: color-mix(in srgb, var(--card-background-color, #20232b) 92%, var(--primary-color, #03a9f4)); + color: var(--primary-text-color, #fff); + overflow: hidden; + } + .previewhead { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; } + .previewbadge { + padding: 3px 9px; + border-radius: 999px; + color: var(--secondary-text-color, #9aa0aa); + background: color-mix(in srgb, var(--secondary-text-color, #9aa0aa) 12%, transparent); + font-size: 12px; + font-weight: 700; + } + .previewbadge.example { color: var(--warning-color, #ffb300); } + .previewgrid { display: grid; grid-template-columns: minmax(170px, 0.8fr) minmax(240px, 1.2fr); gap: 16px; align-items: stretch; } + .previewstage { + position: relative; + min-height: 168px; + border-radius: 12px; + background: var(--secondary-background-color, rgba(0, 0, 0, 0.16)); + overflow: hidden; + pointer-events: none; + container-type: inline-size; + --icon-size: 54px; + --device-base-size: 48.6px; + } + .previewfit { position: absolute; left: 50%; top: 50%; transform-origin: center; } + .previewstage .dev { left: 0; top: 0; cursor: default; } + :host([data-pointer-hover]) .previewstage .dev:hover { z-index: 2; } + .previewfacts { min-width: 0; display: grid; align-content: start; gap: 8px; } + .previewfacts > div { display: grid; grid-template-columns: minmax(115px, 0.8fr) minmax(0, 1.3fr); gap: 8px; } + .previewfacts span { color: var(--secondary-text-color, #9aa0aa); } + .previewfacts b { min-width: 0; overflow-wrap: anywhere; } + .previewnotice { margin: 2px 0 0; color: var(--secondary-text-color, #9aa0aa); font-size: 13px; } + .previewdetails { margin-top: 12px; } + .previewdetails summary { cursor: pointer; color: var(--secondary-text-color, #9aa0aa); } + .previewtable { display: grid; gap: 8px; margin-top: 9px; } + .previewsource { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 3px 10px; font-size: 13px; } + .previewsource > * { min-width: 0; overflow-wrap: anywhere; } + .previewsource code { color: var(--secondary-text-color, #9aa0aa); } + .previewdemo { + min-height: 40px; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border: 1px solid var(--divider-color, rgba(127, 127, 127, 0.35)); + border-radius: 10px; + background: transparent; + color: inherit; + cursor: pointer; + } + .previewdemos { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; } + .previewdemo:focus-visible { outline: 2px solid var(--primary-color, #03a9f4); outline-offset: 2px; } + .previewdemo:disabled { opacity: 0.5; cursor: not-allowed; } + @media (max-width: 640px) { + .previewgrid { grid-template-columns: minmax(0, 1fr); } + .previewstage { min-height: 142px; } + .previewfacts > div { grid-template-columns: minmax(0, 1fr); gap: 2px; } + } + `],customElements.get("hp-device-preview")||customElements.define("hp-device-preview",ro);const lo=(t,e)=>[t[0]-e[0],t[1]-e[1]],ho=(t,e)=>[t[0]+e[0],t[1]+e[1]],co=(t,e)=>t[0]*e[0]+t[1]*e[1],po=t=>Math.hypot(t[0],t[1]);function _o(t){let e=0;for(let s=0;st[1]!=l>t[1]&&t[0]<(r-a)*(t[1]-n)/(l-n)+a&&(s=!s)}return s}([r[0]+n[0]*l,r[1]+n[1]*l],t)&&(n=[-n[0],-n[1]]),n}function go(t){const e=t.length;if(e<3)return!1;for(let s=0;s(t[0]-o[0])*r[1]-(t[1]-o[1])*r[0],h=t=>(t[0]-o[0])*r[0]+(t[1]-o[1])*r[1],c=s,d=n-s;for(const e of t){const t=Math.abs(l(e));if(t<=s)continue;const o=h(e);o<=c||o>=d||tMath.abs(n+(t-p)/g*(r-n));i=Math.min(i,f(u),f(m))}}return i}function bo(t,e,s){if($(t,e,s))return!0;if(v(t,e,s)||v(e,t,s))return!1;let i=0;try{const s=w([[...t.map(t=>[t[0],t[1]]),[t[0][0],t[0][1]]]],[[...e.map(t=>[t[0],t[1]]),[e[0][0],e[0][1]]]]);for(const t of s)t?.[0]&&(i+=k(t[0]))}catch{return!1}return i>Math.max(1e-7,s*s)}function yo(t,e,s){for(const i of e)for(let e=0;es)continue;const h=(t.x-o[0])*l[0]+(t.y-o[1])*l[1];if(h-t.length/2>=-s&&h+t.length/2<=r+s)return!0}return!1}function vo(t,e,s){const i=Math.abs(e[0]-t[0]),o=Math.abs(e[1]-t[1]);return i<=s&&o>s?"v":o<=s&&i>s?"h":null}function $o(t,e,s){return Math.abs(t[0]-e[0])<=s&&Math.abs(t[1]-e[1])<=s}function wo(t,e,s,i,o){return $o(t,s,o)&&$o(e,i,o)||$o(t,i,o)&&$o(e,s,o)}function ko(t,e,s,i,o){const a=vo(t,e,o);return a&&vo(s,i,o)===a?"h"===a?Math.abs(t[1]-s[1])>o||Math.abs(t[1]-i[1])>o?0:Math.max(0,Math.min(Math.max(t[0],e[0]),Math.max(s[0],i[0]))-Math.max(Math.min(t[0],e[0]),Math.min(s[0],i[0]))):Math.abs(t[0]-s[0])>o||Math.abs(t[0]-i[0])>o?0:Math.max(0,Math.min(Math.max(t[1],e[1]),Math.max(s[1],i[1]))-Math.max(Math.min(t[1],e[1]),Math.min(s[1],i[1]))):0}function xo(t,e,s,i){const o=t.length,a=vo(t[(e-1+o)%o],t[e],i),n=vo(t[(e+1)%o],t[(e+2)%o],i),r="h"===s?"v":"h";return a===r&&n===r}function So(t){let e=1/0,s=1/0,i=-1/0,o=-1/0;for(const a of t)e=Math.min(e,a[0]),s=Math.min(s,a[1]),i=Math.max(i,a[0]),o=Math.max(o,a[1]);return[e,s,i,o]}function Mo(t,e,s){const i=So(t),o=So(e);return i[2]o}const Co=(t,e)=>"h"===e?t[0]:t[1],Po=(t,e)=>"h"===e?t[1]:t[0];function Io(t,e,s,i,o){const a=t.find(t=>t.id===e);if(!a)return null;const n=a.poly[s],r=a.poly[(s+1)%a.poly.length],l=vo(n,r,o);if(!l)return null;const h=0===i?r:n,c=0===i?n:r,d=Co(h,l),p=Co(c,l),_=Po(h,l),u=function(t,e,s,i){const o=[];for(const a of t)for(let t=0;ti||Math.abs(Po(r,e)-s)>i)continue;const l=Co(n,e),h=Co(r,e),c=Math.min(l,h),d=Math.max(l,h);d-c>i&&o.push({roomId:a.id,edge:t,lo:c,hi:d})}return o.sort((t,e)=>t.lo-e.lo||t.hi-e.hi||t.roomId.localeCompare(e.roomId)||t.edge-e.edge)}(t,l,_,o),m=function(t,e,s,i){const o=[e,s];for(const a of t){const t=Math.max(e,a.lo),n=Math.min(s,a.hi);n-t>i&&o.push(t,n)}const a=[...new Set(o)].sort((t,e)=>t-e),n=[];for(let e=0;e+1r>t.lo-i&&rt.roomId))].sort();n.push({lo:s,hi:o,owners:l})}return n}(u,Math.min(d,p),Math.max(d,p),o);return!m.length||m.some(t=>t.owners.length<1||t.owners.length>2)?null:{roomId:e,edge:s,movedEndpoint:i,axis:l,line:_,fixed:d,moving:p,intervals:u,baseline:m}}function To(t,e,s){const i=[];for(const o of t.intervals){const a=e.polys[o.roomId];if(!a){i.push(o);continue}if(o.edge<0||o.edge>=a.length)continue;const n=a[o.edge],r=a[(o.edge+1)%a.length];if(vo(n,r,s)!==t.axis||Math.abs(Po(n,t.axis)-t.line)>s||Math.abs(Po(r,t.axis)-t.line)>s)continue;const l=Co(n,t.axis),h=Co(r,t.axis),c=Math.min(l,h),d=Math.max(l,h);d-c>s&&i.push({...o,lo:c,hi:d})}return i}function Ro(t,e,s){return new Set(t.filter(t=>e>t.lo-s&&et.roomId)).size}function Fo(t,e,s){for(const i of e.sideOwnership){const e=t.polys[i.roomId];if(!e)return!1;const o=e[i.edge],a=e[(i.edge+1)%e.length];if(vo(o,a,s)!==i.axis)return!1;const n=Co(0===i.movedEndpoint?o:a,i.axis),r=To(i,t,s),l=(i.moving>=i.fixed?i.baseline[i.baseline.length-1]:i.baseline[0]).owners.length;if(l<1||l>2)return!1;const h=Math.min(i.fixed,i.moving,n),c=Math.max(i.fixed,i.moving,n),d=[h,c,i.fixed,i.moving,n];for(const t of i.intervals)d.push(t.lo,t.hi);for(const t of r)d.push(t.lo,t.hi);const p=[...new Set(d.filter(t=>t>=h&&t<=c))].sort((t,e)=>t-e),_=Math.min(i.fixed,i.moving),u=Math.max(i.fixed,i.moving),m=Math.min(i.fixed,n),g=Math.max(i.fixed,n);for(let t=0;t+1_-s&&am-s&&a2||d>2)return!1;if(n&&h){if(c!==d)return!1}else if(!n&&h){if(d!==l||c>0&&c!==d)return!1}else if(2===c?2!==d:d>1)return!1}}return!0}function No(t,e,s,i){const o=[s.n[0]*i,s.n[1]*i],a={polys:{},openings:{},movedSpans:{}};for(const e of s.roomIds){const i=t.find(t=>t.id===e),n=s.edgeByRoom[e];if(!i||null==n)continue;const r=(n+1)%i.poly.length;a.polys[e]=i.poly.map((t,e)=>e===n||e===r?ho(t,o):[...t]),a.movedSpans[e]=[[ho(i.poly[n],o),ho(i.poly[r],o)]]}for(const t of e)s.movingOpeningIds.includes(t.id)&&(a.openings[t.id]=[t.x+o[0],t.y+o[1]]);return a}function zo(t,e,s,i,o,a,n,r){if(!function(t,e,s,i){if(uo([t.x,t.y],e,s)>2*i)return!1;const o=po(lo(s,e));if(o<=i)return!1;const a=[(s[0]-e[0])/o,(s[1]-e[1])/o],n=(t.x-e[0])*a[0]+(t.y-e[1])*a[1];return n>=-i&&n<=o+i}(t,e,s,r))return!0;if(uo([t.x,t.y],i,o)>2*r)return!1;const l=0===a?i:o;return po(lo([t.x,t.y],l))>=t.length/2+n-r}function Ao(t,e,s,i){const o=i.movingHalf||0;return"circle"===t.kind?uo(t.center,e,s)2)return!1;const r=No(t,e,s,i);if(Object.keys(r.polys).length!==s.roomIds.length)return!1;if(!Fo(r,s,a))return!1;const l=new Set(s.roomIds),h=t=>r.polys[t.id]||t.poly;for(const i of s.roomIds){const l=t.find(t=>t.id===i),h=r.polys[i],c=s.edgeByRoom[i];if(!l||!h||h.length!==s.topology[i]||h.length!==l.poly.length||!go(h))return!1;const d=l.poly.length,p=(c-1+d)%d,_=[p,c,(c+1)%d];for(const t of _)if(y(h[t],h[(t+1)%d]))return!1;const u=_o(l.poly),m=_o(h);if(Math.abs(m)t.id===e),i=r.polys[e];for(const o of t){if(o.id===e||l.has(o.id))continue;const t=h(o);if(!Mo(s.poly,o.poly,a)||!Mo(i,t,a))if(v(s.poly,o.poly,a)){if(!v(i,t,a))return!1}else if(v(o.poly,s.poly,a)){if(!v(t,i,a))return!1}else if(v(i,t,a)||v(t,i,a)||bo(i,t,a))return!1}}t.find(t=>t.id===s.roomId);const c=r.polys[s.roomId],d=s.edgeByRoom[s.roomId],p=c[d],_=c[(d+1)%c.length];if(Math.abs(i)>a)for(const t of o.obstacles||[])if(Ao(t,p,_,o))return!1;for(const t of e){if(!s.movingOpeningIds.includes(t.id))continue;const e=r.openings[t.id];if(!e||t.hosted)return!1;if(!yo({...t,x:e[0],y:e[1]},[c],2*a))return!1}return!0}const Lo=new WeakMap,Eo=t=>"number"==typeof t&&Number.isFinite(t),Bo=(t,e)=>{const s=new Map;for(const i of t||[])!Eo(i?.cm)||!e&&i.cm<=0||s.set(i.cm,(s.get(i.cm)||0)+1);return s},Ho=t=>[String(t),"см"].join(" ");function Go(t,e,s={}){if(s.allowClear)return[];const i=!0===s.exactMultiplicity,o=Bo(t,i),a=Bo(e,i),n=[];for(const[t,e]of o){const s=a.get(t)||0;0===s?n.push({invariant:"wall_records",kind:"lost",owner:Ho(t),reference:`было ${e}`,detail:"записи этой толщины исчезли целиком"}):i&&s!==e&&n.push({invariant:"wall_records",kind:"count",owner:Ho(t),reference:`было ${e}, стало ${s}`,detail:"изменилось число записей этой толщины"})}if(i)for(const[t,e]of a)o.has(t)||n.push({invariant:"wall_records",kind:"count",owner:Ho(t),reference:`было 0, стало ${e}`,detail:"появились записи новой толщины"});return n}class Wo{constructor(){this._selectedRoomId=null,this._session=null,this._eligibility=null}get dragging(){return null!==this._session}ownsPointer(t){return this._session?.pointerId===t}get selectedRoomId(){return this._selectedRoomId}get snapshotIdentity(){return this._session?.snapshotIdentity||null}get activePointerId(){return this._session?.pointerId??null}get moved(){return!0===this._session?.moved}get delta(){return this._session?.delta||0}get rooms(){return this._session?.rooms||null}get openings(){return this._session?.openings||null}get plan(){return this._session?.plan||null}get preview(){return this._session?.accepted?.preview||null}get liveLabels(){return this._session?.accepted?.labels||null}selectRoom(t){this._selectedRoomId=t}restoreSelection(t){this._selectedRoomId=t}escapeIdle(){return this._selectedRoomId?(this._selectedRoomId=null,"selection-cleared"):"exit-tool"}resolve(t,e,s){this._eligibility&&this._eligibility.context===t||(this._eligibility={context:t,values:new Map});const i=this._eligibility.values.get(e);if(i)return i;const o=s();return this._eligibility.values.set(e,o),o}begin(t){return!this._session&&(this._session={pointerId:t.pointerId,start:[...t.start],roomId:t.roomId,plan:t.plan,options:t.options,rooms:t.rooms,openings:t.openings,snapshotIdentity:t.snapshotIdentity,before:t.before,moved:!1,delta:0,changedRoomIds:[...t.plan.roomIds],rejectionNotified:!1,wallUnionBefore:t.wallUnionBefore,accepted:null},!0)}move(t){const e=this._session;if(!e||e.pointerId!==t.pointerId)return{kind:"no-op"};const s=(i=e.start,o=t.point,a=e.plan.n,[i?.[0],i?.[1],o?.[0],o?.[1],a?.[0],a?.[1]].every(Number.isFinite)?(o[0]-i[0])*a[0]+(o[1]-i[1])*a[1]:0);var i,o,a;const n=t.snap([e.plan.a[0]+e.plan.n[0]*s,e.plan.a[1]+e.plan.n[1]*s]),r=(n[0]-e.plan.a[0])*e.plan.n[0]+(n[1]-e.plan.a[1])*e.plan.n[1],l=function(t,e,s,i,o,a){if(!Number.isFinite(i)||Math.abs(i)<1e-9)return 0;const n=Math.sign(i),r=Math.abs(i),l=Math.max(Math.abs(o),1e-6);let h=Lo.get(s);h&&h.opts===a||(h={opts:a,values:new Map},Lo.set(s,h));const c=h.values;let d=0;for(let i=Math.min(l,r),o=0;o<4096&&i<=r+1e-9;o++,i=Math.min(r,i+l)){const o=n*i,h=`${o.toFixed(9)}|${l.toFixed(9)}`;let p=c.get(h);if(void 0===p&&(p=Oo(t,e,s,o,a),c.size<4096&&c.set(h,p)),!p)break;if(d=o,Math.abs(i-r)<=1e-9)break}return d}(e.rooms,e.openings,e.plan,r,t.step,e.options);if(e.moved&&l===e.delta)return{kind:"no-op"};const h=No(e.rooms,e.openings,e.plan,l);let c;try{if(c=t.project(e.snapshotIdentity,h.polys,h.openings,e.changedRoomIds,e.rooms),!c.ok)return this._projectionRejected(e);if(Go(c.value.beforeWalls,c.value.afterWalls,{exactMultiplicity:!0}).length)return this._projectionRejected(e)}catch{return this._projectionRejected(e)}const d=e.accepted,p={preview:c.value.preview,labels:null,artifact:c.value.artifact,beforeWalls:c.value.beforeWalls,afterWalls:c.value.afterWalls};e.accepted=p;try{t.publish(p.preview,p.artifact),p.labels=t.measure(h,e.plan)}catch{e.accepted=d;try{t.publish(d?.preview||null,d?.artifact||null)}catch{}return this._projectionRejected(e)}return e.moved=!0,e.delta=l,{kind:"accepted",preview:c.value.preview,labels:p.labels,artifact:c.value.artifact}}finish(t){const e=this._session;if(!e||e.pointerId!==t.pointerId)return{kind:"no-op"};const s=e.accepted;let i;if(e.moved&&Math.abs(e.delta)>1e-9&&s)if(t.currentSnapshotIdentity!==e.snapshotIdentity)i={kind:"rejected",reason:"stale-snapshot"};else if(Oo(e.rooms,e.openings,e.plan,e.delta,e.options))if(Go(s.beforeWalls,s.afterWalls,{exactMultiplicity:!0}).length)i={kind:"rejected",reason:"wall-records"};else{let o=!1;try{o=t.validatePreview(s.preview)}catch{o=!1}i=o?{kind:"commit",preview:s.preview,before:e.before}:{kind:"rejected",reason:"invalid-candidate"}}else i={kind:"rejected",reason:"invalid-topology"};else i={kind:"no-op"};return this._session=null,"commit"===i.kind&&(this._eligibility=null),i}cancel(t,e){const s=this._session;return!s||void 0!==e&&s.pointerId!==e?{kind:"no-op"}:(this._session=null,this._eligibility=null,{kind:"cancelled",restoreWallUnion:t===s.snapshotIdentity?s.wallUnionBefore:null})}reset(){this._selectedRoomId=null,this._session=null,this._eligibility=null}_projectionRejected(t){const e=!t.rejectionNotified;return t.rejectionNotified=!0,{kind:"rejected",notify:e}}}const jo=(t,e)=>[(t[0]-e.x)/Math.max(1e-9,e.w)*e.stageWidth,(t[1]-e.y)/Math.max(1e-9,e.h)*e.stageHeight];function qo(t){const{poly:e,edge:s,text:i,view:o}=t,a=e[s],n=e[(s+1)%e.length],r=[(a[0]+n[0])/2,(a[1]+n[1])/2],l=function(t,e){const s=t[e],i=t[(e+1)%t.length],o=i[0]-s[0],a=i[1]-s[1],n=Math.hypot(o,a)||1;return(t=>{let e=0;for(let s=0;s=0?[-a/n,o/n]:[a/n,-o/n]}(e,s),h=Math.hypot(n[0]-a[0],n[1]-a[1])||1,c=[(n[0]-a[0])/h,(n[1]-a[1])/h],d=jo(r,o),p=jo(t.gearCenter,o),_=Math.max(34,7.2*i.length+12),u=Math.abs(l[0])>=Math.abs(l[1])?Math.max(28,_/2+4):Math.max(28,13),m=d[0]+l[0]*u,g=d[1]+l[1]*u,f=e=>{return s=m+c[0]*e,i=g+c[1]*e,o=_,a=18,n=p[0],r=p[1],l=t.gearWidthPx,h=t.gearHeightPx,2*Math.abs(s-n)=Math.abs(c[1])?c[0]:c[1])>0?-1:1,e=Math.max(o.stageWidth,o.stageHeight,64);for(let s=4;s<=e;s+=4){const e=s*t;if(!f(e)){b=e;break}const i=-e;if(!f(i)){b=i;break}}}const y=l[0]*u+c[0]*b,v=l[1]*u+c[1]*b,$=Math.hypot(y,v)||1,w=v/$*12,k=[r[0]+y/$*12*o.w/Math.max(1,o.stageWidth),r[1]+w*o.h/Math.max(1,o.stageHeight)];return{anchor:r,offsetXPx:y,offsetYPx:v,tangentOffsetPx:b,side:Math.abs(l[0])>=Math.abs(l[1])?l[0]<0?"left":"right":l[1]<0?"above":"below",leader:{a:r,b:k}}}const Uo=1e-9,Ko=1e7,Vo=t=>{const e=Math.sign(t)*Math.floor(Math.abs(t)*Ko+.5)/Ko;return Object.is(e,-0)?0:e},Jo=t=>`${Vo(t[0])},${Vo(t[1])}`,Yo=(t,e)=>Math.hypot(e[0]-t[0],e[1]-t[1]),Xo=(t,e)=>Math.atan2(e[1]-t[1],e[0]-t[0]),Zo=t=>Array.isArray(t)&&t.length>=2&&t.every(t=>Number.isFinite(t)),Qo=t=>(t||[]).filter(t=>Zo(t?.a)&&Zo(t?.b)&&Yo(t.a,t.b)>Uo);function ta(t,{minAngleDeg:e=15,maxValence:s=6}={}){const i=new Map;for(const e of Qo(t))for(const[t,s]of[[e.a,e.b],[e.b,e.a]]){const e=i.get(Jo(t))||[];e.push(Xo(t,s)),i.set(Jo(t),e)}const o=[];for(const[t,a]of i){if(a.length>s&&o.push({rule:"valence",subject:t,actual:a.length,limit:s}),a.length<2)continue;const i=[...a].sort((t,e)=>t-e);let n=1/0;for(let t=0;tUo&&s(180*Math.atan2(t.b[1]-t.a[1],t.b[0]-t.a[0])/Math.PI%180+180)%180,sa=(t,e,s=1)=>{const i=Math.abs(ea(t)-ea(e));return Math.min(i,180-i)<=s},ia=t=>{const e=new Map;for(const s of t)for(const t of[s.a,s.b]){const i=e.get(Jo(t));i?i.push(s):e.set(Jo(t),[s])}return e};function oa(t,e,s){const i=Qo(e),o=s??ia(i),a=new Set([t]);let n=Yo(t.a,t.b);const r=[t.a,t.b];for(;r.length;){const e=r.pop();for(const s of o.get(Jo(e))||[])a.has(s)||sa(s,t)&&Number(s.cm||0)===Number(t.cm||0)&&(a.add(s),n+=Yo(s.a,s.b),r.push(s.a,s.b))}return n}function aa(t,e,s,{minLengthCm:i=20}={}){const o=[],a=Qo(t),n=ia(a);for(const t of a){const r=oa(t,a,n)/s*(e||1),l=Math.max(i,Number(t.cm)>0?Number(t.cm):0);r{const i=s[0]-e[0],o=s[1]-e[1],a=i*i+o*o,n=a<=Uo?0:Math.max(0,Math.min(1,((t[0]-e[0])*i+(t[1]-e[1])*o)/a));return Math.hypot(t[0]-(e[0]+i*n),t[1]-(e[1]+o*n))};function ra(t,e,s,{minDistanceCm:i=5}={}){const o=Qo(t),a=new Map;for(const t of o)a.set(Jo(t.a),t.a),a.set(Jo(t.b),t.b);const n=((t,e,s)=>t/(e||1)*s)(i,e,s),r=n>Uo?n:1,l=(t,e)=>`${Math.floor(t/r)},${Math.floor(e/r)}`,h=new Map;for(const[t,e]of a){const s=l(e[0],e[1]),i=h.get(s);i?i.push([t,e]):h.set(s,[[t,e]])}const c=new Map;for(const t of o){const e=Math.min(t.a[0],t.b[0])-n,s=Math.max(t.a[0],t.b[0])+n,i=Math.min(t.a[1],t.b[1])-n,o=Math.max(t.a[1],t.b[1])+n;for(let a=Math.floor(e/r);a<=Math.floor(s/r);a++)for(let e=Math.floor(i/r);e<=Math.floor(o/r);e++){const s=`${a},${e}`,i=c.get(s);i?i.push(t):c.set(s,[t])}}const d=[];for(const[t,o]of a){const a=Math.floor(o[0]/r),l=Math.floor(o[1]/r);for(let r=-1;r<=1;r++)for(let c=-1;c<=1;c++)for(const[p,_]of h.get(`${a+r},${l+c}`)||[]){if(t>=p)continue;const a=Yo(o,_);a<=2e-7||a{const i=a[(s+1)%a.length];return t+(e[0]*i[1]-i[0]*e[1])},0))/2,r=(s||1)/i,l=n*r*r;return lt.removed).map(t=>t.binding)),c=new Set;for(const t of s)t.id!==n&&("device"===t.bindingKind&&t.bindingRef&&c.add(`device:${t.bindingRef}`),"entity"===t.bindingKind&&t.bindingRef&&c.add(`entity:${t.bindingRef}`));const d=new Set;for(const t of s)"device"===t.bindingKind&&t.name&&d.add(`${t.name.trim()}|${t.area||""}`);const p=[];for(const t of Object.values(e?.devices||{})){if(!t||"service"===t.entry_type)continue;const e=`device:${t.id}`;if(c.has(e))continue;const s=String(t.name_by_user||t.name||t.id).trim();e!==a&&!h.has(e)&&d.has(`${s}|${t.area_id||""}`)||p.push({value:e,label:s,sub:(t.model||r.device)+("Group"===t.model?r.z2mGroup:""),kind:"device",ref:t.id,areaId:t.area_id||"",model:t.model||""})}for(const[t,s]of Object.entries(e?.entities||{})){const i=`entity:${t}`;if(c.has(i))continue;if(S(e,t,l)&&!h.has(i))continue;const o=ha.has(s?.platform),a="group"===s?.platform;if(!o&&!a)continue;if(s?.hidden&&!h.has(i))continue;const n=e?.states?.[t];p.push({value:i,label:s?.name||n?.attributes?.friendly_name||t,sub:`${t.split(".")[0]} · ${a?r.group:r.helper}`,kind:"entity",ref:t,areaId:s?.area_id||s?.device_id&&e?.devices?.[s.device_id]?.area_id||"",model:"",parentDeviceId:s?.device_id||void 0})}if(o){const t=new Set(p.map(t=>t.value));for(const[s,i]of Object.entries(e?.entities||{})){const o=`entity:${s}`;if(c.has(o)||t.has(o)||i?.hidden&&!h.has(o))continue;const a=!!i?.device_id&&l.devices.has(i.device_id);if(S(e,s,l)&&!h.has(o)&&!a)continue;const n=e?.states?.[s],d=i?.device_id?e?.devices?.[i.device_id]:null,_=d?String(d.name_by_user||d.name||""):"";p.push({value:o,label:i?.name||n?.attributes?.friendly_name||s,sub:`${s.split(".")[0]} · ${r.entity}${_?` · ${_}`:""}`,kind:"entity",ref:s,areaId:i?.area_id||d?.area_id||"",model:"",parentDeviceId:i?.device_id||void 0})}}return p.sort((t,e)=>t.label.localeCompare(e.label)||t.value.localeCompare(e.value))}const da={kind:"active",enabledEntityIds:[],allEntityIds:[]};function pa(t){return t.startsWith("entity:")?"entity":"device"}function _a(t){const e=t.binding.indexOf(":"),s=e>=0?t.binding.slice(e+1):"",i=Object.keys(t).filter(t=>!["id","binding","hidden"].includes(t));return t.id===`h${s}`&&0===i.length}const ua=.001;function ma(t){return!!t&&t.length>=2&&Number.isFinite(t[0])&&Number.isFinite(t[1])}function ga(t){return`${t[0].toFixed(6)},${t[1].toFixed(6)}`}function fa(t,e,s){return Math.abs(t[0]-e[0])"room"!==t.kind&&s.some(s=>s!==t&&function(t,e,s){const i=t.b[0]-t.a[0],o=t.b[1]-t.a[1],a=Math.hypot(i,o);if(!(a>s))return!1;const n=i/a,r=o/a,l=e=>Math.abs((e[0]-t.a[0])*r-(e[1]-t.a[1])*n);if(l(e.a)>s||l(e.b)>s)return!1;const h=(e.a[0]-t.a[0])*n+(e.a[1]-t.a[1])*r,c=(e.b[0]-t.a[0])*n+(e.b[1]-t.a[1])*r;return Math.min(a,Math.max(h,c))-Math.max(0,Math.min(h,c))>s}(t,s,e))),o=i.map(t=>{const[e,s]=ba(t.a,t.b);return{a:e,b:s,key:`hidden|${va(t,e,s)}`,sourceKind:t.kind,sourceId:t.id}}).sort((t,e)=>t.key.localeCompare(e.key)),a=i.flatMap(t=>[t.a,t.b].map((e,s)=>({point:[e[0],e[1]],key:`hidden|${ya(t)}|endpoint-${s}`,sourceKind:t.kind,sourceId:t.id}))).sort((t,e)=>t.key.localeCompare(e.key));return{segments:o,endpoints:a}}function xa(t){const e=t.epsilon??ua,s=t.roomCuts||[],i=new Map;for(const e of t.partitionCuts||[]){if("string"!=typeof e.hostId||!e.hostId||!ma(e.a)||!ma(e.b))continue;const t=i.get(e.hostId)||[];t.push([e.a[0],e.a[1],e.b[0],e.b[1]]),i.set(e.hostId,t)}const o=[];for(const[e,i]of M(t.space.rooms).entries())i.length<4||o.push({a:[i[0],i[1]],b:[i[2],i[3]],kind:"room",id:`room-edge-${e}`,cuts:s});for(const e of t.space.room_drafts||[])if(e.id!==t.activeDraftId)for(let t=0;t+1wa(s,t,e)))continue;const t=ga(s);n.has(t)||n.set(t,{point:[s[0],s[1]],key:t})}}}return{segments:[...a.values()].sort((t,e)=>t.key.localeCompare(e.key)),endpoints:[...n.values()].sort((t,e)=>t.key.localeCompare(e.key))}}function Sa(t,e,s){return e.some(e=>ma(e)&&fa(t,e,s))}function Ma(t,e,s){if(!s)return!0;const i=t-s.distance;return i<-1e-9||Math.abs(i)<=1e-9&&e.localeCompare(s.key)<0}function Da(t,e,s){const i=s.epsilon??ua,o=s.excludePoints||[],a=[...t.endpoints,...(s.extraEndpoints||[]).filter(t=>ma(t.point)).map(t=>({point:[t.point[0],t.point[1]],key:t.key}))],n=new Map;for(const t of a){if(Sa(t.point,o,i))continue;const e=ga(t.point),s=n.get(e);(!s||t.key.localeCompare(s.key)<0)&&n.set(e,t)}return[...n.values()].map(t=>({kind:"endpoint",point:[...t.point],key:t.key,distance:Math.hypot(e[0]-t.point[0],e[1]-t.point[1])})).filter(t=>t.distance<=s.tolerance).sort((t,e)=>t.distance-e.distance||t.key.localeCompare(e.key))}function Ca(t,e){if(!t.length)return null;if(e>0){const s=new Set;for(let i=0;i1)return{kind:"ambiguous",candidate:null,conflicts:t.filter(t=>s.has(t.key)).map(t=>({point:[...t.point],key:t.key}))}}return{kind:"resolved",candidate:t[0],conflicts:[]}}function Pa(t,e,s){const i=t.b[0]-t.a[0],o=t.b[1]-t.a[1],a=Math.hypot(i,o);if(!(a>0))return[...t.a];const n=i/a,r=o/a,l=Math.max(0,Math.min(a,(e[0]-t.a[0])*n+(e[1]-t.a[1])*r)),h=s>0?Math.max(0,Math.min(a,Math.round(l/s)*s)):l;return[t.a[0]+n*h,t.a[1]+r*h]}function Ia(t,e,s){if(!(ma(e)&&ma(s.anchor)&&s.tolerance>=0))return{kind:"none",candidate:null,conflicts:[]};const i=s.epsilon??ua,o=function(t,e){const s=e[0]-t[0],i=e[1]-t[1];if(!(Math.hypot(s,i)>Number.EPSILON))return null;const o=Math.round(Math.atan2(i,s)/(Math.PI/4))*(Math.PI/4);return[Math.cos(o),Math.sin(o)]}(s.anchor,e);if(!o)return{kind:"none",candidate:null,conflicts:[]};const a=Da(t,e,s).filter(t=>function(t,e,s,i){const o=t[0]-e[0],a=t[1]-e[1];return o*s[0]+a*s[1]>i&&Math.abs(o*s[1]-a*s[0])<=i*Math.max(Math.hypot(o,a),1)}(t.point,s.anchor,o,i)),n=Ca(a,s.distinguishTolerance||0);if(n)return n;let r=null;for(const a of t.segments){const t=a.b[0]-a.a[0],n=a.b[1]-a.a[1],l=a.a[0]-s.anchor[0],h=a.a[1]-s.anchor[1],c=o[0]*n-o[1]*t;let d=null;if(Math.abs(c)<=i*Math.max(Math.hypot(t,n),1)){if(Math.abs(l*o[1]-h*o[0])>i*Math.max(Math.hypot(l,h),1))continue;const t=[a.a,a.b].map(t=>({point:t,along:(t[0]-s.anchor[0])*o[0]+(t[1]-s.anchor[1])*o[1]})).filter(t=>t.along>i).sort((t,e)=>t.along-e.along);t.length&&(d=[...t[0].point])}else{const e=(l*n-h*t)/c,a=(l*o[1]-h*o[0])/c;e>i&&a>=-i&&a<=1+i&&(d=[s.anchor[0]+o[0]*e,s.anchor[1]+o[1]*e])}if(!d)continue;const p=Math.hypot(e[0]-d[0],e[1]-d[1]);p>s.tolerance||Sa(d,s.excludePoints||[],i)||!Ma(p,a.key,r)||(r={kind:"line",point:d,key:a.key,distance:p,segment:a})}return r?{kind:"resolved",candidate:r,conflicts:[]}:{kind:"none",candidate:null,conflicts:[]}}const Ta=.001;function Ra(t,e,s,i){const o=Number.isFinite(t)&&t>0?Math.floor(t):0,a=t=>"number"==typeof t&&Number.isFinite(t)&&t>=0?t:null,n=a(s),r=a(i)??0,l=[];let h=null;for(let t=0;t=2&&Number.isFinite(t[0])&&Number.isFinite(t[1])}function za(t,e,s,i){return t*i-e*s}function Aa(t){let e=0;for(let s=0;st.localeCompare(e)),e[0]}function Ha(t,e,s){const i=[...t];for(let t=!0;t&&i.length>=3;){t=!1;for(let o=0;o=0){i.splice(o,1),t=!0;break}}}return i}function Ga(t,e){const s=t.map(t=>Math.max(0,Math.min(1,t))).sort((t,e)=>t-e),i=[];for(const t of s)(!i.length||Math.abs(t-i[i.length-1])>e)&&i.push(t);return i}function Wa(t,e){return[t.a[0]+(t.b[0]-t.a[0])*e,t.a[1]+(t.b[1]-t.a[1])*e]}function ja(t,e,s){const i=function(t,e){const s=e.b[0]-e.a[0],i=e.b[1]-e.a[1],o=s*s+i*i;return o>0?((t[0]-e.a[0])*s+(t[1]-e.a[1])*i)/o:0}(t,e);if(i<-s||i>1+s)return null;const o=Wa(e,i);return Math.hypot(o[0]-t[0],o[1]-t[1])<=s?Math.max(0,Math.min(1,i)):null}function qa(t,e,s,i,o){const a=t.b[0]-t.a[0],n=t.b[1]-t.a[1],r=e.b[0]-e.a[0],l=e.b[1]-e.a[1],h=e.a[0]-t.a[0],c=e.a[1]-t.a[1],d=za(a,n,r,l),p=Math.max(Math.hypot(a,n),Math.hypot(r,l),1);if(Math.abs(d)>o*p){const t=za(h,c,r,l)/d,e=za(h,c,a,n)/d;return void(t>=-o&&t<=1+o&&e>=-o&&e<=1+o&&(s.push(Math.max(0,Math.min(1,t))),i.push(Math.max(0,Math.min(1,e)))))}if(!(Math.abs(za(h,c,a,n))>o*p)){for(const i of[e.a,e.b]){const e=ja(i,t,o);null!=e&&s.push(e)}for(const s of[t.a,t.b]){const t=ja(s,e,o);null!=t&&i.push(t)}}}function Ua(t){let e=t+1|0;return e^=e<<13,e^=e>>>17,e^=e<<5,e>>>0}function Ka(t,e){return t.minY-e.minY||t.index-e.index}function Va(t){return t.subtreeMaxY=Math.max(t.item.maxY,t.left?.subtreeMaxY??-1/0,t.right?.subtreeMaxY??-1/0),t}function Ja(t){const e=t.right;return t.right=e.left,e.left=Va(t),Va(e)}function Ya(t){const e=t.left;return t.left=e.right,e.right=Va(t),Va(e)}function Xa(t,e){return t?(Ka(e,t.item)<0?(t.left=Xa(t.left,e),t.left.priority0)t.right=Za(t.right,e);else{if(!t.left)return t.right;if(!t.right)return t.left;t.left.priority=e&&Qa(t.left,e,s,i),t.item.minY<=s&&t.item.maxY>=e&&i.push(t.item),t.item.minY<=s&&Qa(t.right,e,s,i))}function tn(t,e=.001){const s=Number.isFinite(e)&&e>0?e:Ta,i=t.filter(t=>Na(t.a)&&Na(t.b)&&"string"==typeof t.key&&t.key.length>0&&Math.hypot(t.b[0]-t.a[0],t.b[1]-t.a[1])>s),o=i.map(()=>[0,1]);!function(t,e,s){const i=t.map((t,e)=>({index:e,minX:Math.min(t.a[0],t.b[0])-s,maxX:Math.max(t.a[0],t.b[0])+s,minY:Math.min(t.a[1],t.b[1])-s,maxY:Math.max(t.a[1],t.b[1])+s})),o=[...i].sort((t,e)=>t.minX-e.minX||t.minY-e.minY||t.index-e.index),a=[...i].sort((t,e)=>t.maxX-e.maxX||t.index-e.index),n=new Set;let r=null,l=0;for(const i of o){for(;lt.index-e.index);for(const a of o)qa(t[a.index],t[i.index],e[a.index],e[i.index],s);n.add(i.index),r=Xa(r,i)}}(i,o,s);const a=new Map;for(let t=0;t({key:t,a:e.a,b:e.b,sourceKeys:[...e.sourceKeys].sort((t,e)=>t.localeCompare(e))})).sort((t,e)=>t.key.localeCompare(e.key))}function en(t,e=.001){const s=Number.isFinite(e)&&e>0?e:Ta,i=tn(t,s),o=new Map,a=new Map,n=new Map(i.map(t=>[t.key,t]));for(const t of i){const e=La(t.a,s),i=La(t.b,s);o.set(e,t.a),o.set(i,t.b),a.has(e)||a.set(e,new Set),a.has(i)||a.set(i,new Set),a.get(e).add(i),a.get(i).add(e)}const r=new Map;for(const[t,e]of a){const s=o.get(t);r.set(t,[...e].sort((t,e)=>{const i=o.get(t),a=o.get(e);return Math.atan2(i[1]-s[1],i[0]-s[0])-Math.atan2(a[1]-s[1],a[0]-s[0])||t.localeCompare(e)}))}const l=new Set,h=new Map,c=(t,e)=>`${t}>${e}`;for(const t of i){const e=[La(t.a,s),La(t.b,s)];for(const[t,a]of[e,[e[1],e[0]]]){if(l.has(c(t,a)))continue;const e=[],d=[];let p=t,_=a,u=!1;for(let s=0;s<=2*i.length+2;s++){const s=c(p,_);if(l.has(s)){u=p===t&&_===a;break}l.add(s),e.push(p),d.push(Ea(p,_));const i=r.get(_)||[],o=i.indexOf(p);if(o<0||!i.length)break;if(p=_,_=i[(o-1+i.length)%i.length],p===t&&_===a){u=!0;break}}if(!u||new Set(e).size<3||new Set(e).size!==e.length)continue;const m=e.map(t=>o.get(t)),g=Aa(m);if(!(g>s*s))continue;const f=Ba(Ha(e,o,s)),b=new Set;for(const t of d)for(const e of n.get(t)?.sourceKeys||[])b.add(e);const y={ring:m,key:f,area:g,atomKeys:[...d],sourceKeys:[...b].sort((t,e)=>t.localeCompare(e))};h.has(f)||h.set(f,y)}}return{atoms:i,faces:[...h.values()].sort((t,e)=>t.area-e.area||t.key.localeCompare(e.key))}}function sn(t,e,s=.001){if(!Na(e))return null;const i=t.faces.filter(t=>{if(t.ring.some((i,o)=>function(t,e,s,i){const o=s[0]-e[0],a=s[1]-e[1],n=o*o+a*a;if(!(n>0))return Math.hypot(t[0]-e[0],t[1]-e[1])<=i;const r=((t[0]-e[0])*o+(t[1]-e[1])*a)/n;return!(r<0||r>1)&&Math.hypot(e[0]+o*r-t[0],e[1]+a*r-t[1])<=i}(e,i,t.ring[(o+1)%t.ring.length],s)))return!1;let i=!1;for(let s=0,o=t.ring.length-1;se[1]!=n[1]>e[1]&&e[0]<(n[0]-a[0])*(e[1]-a[1])/(n[1]-a[1])+a[0]&&(i=!i)}return i});return[...i].sort((t,e)=>t.area-e.area||t.key.localeCompare(e.key))[0]||null}function on(t,e,s){const i=s[0]-e[0],o=s[1]-e[1],a=i*i+o*o,n=a>0?Math.max(0,Math.min(1,((t[0]-e[0])*i+(t[1]-e[1])*o)/a)):0;return Math.hypot(e[0]+i*n-t[0],e[1]+o*n-t[1])}function an(t,e,s=.5){let i=Math.abs(t-e)%180;return i>90&&(i=180-i),i<=s}function nn(t,e,s,i){const o=t.filter(t=>"outer"===t.kind&&!t.open&&t.cm>0).map(t=>({interval:t,reusePartitionId:e.find(e=>function(t,e,s){return!(Math.abs(t.cm-e.cm)>1e-6)&&(an(180*Math.atan2(t.b[1]-t.a[1],t.b[0]-t.a[0])/Math.PI,180*Math.atan2(e.b[1]-e.a[1],e.b[0]-e.a[0])/Math.PI,.001)&&on(e.a,t.a,t.b)<=s&&on(e.b,t.a,t.b)<=s)}(e,t,i))?.id||null})),a=new Map;for(const e of s){if("partition"===e.host?.kind)continue;const s=t.map(t=>{const s=180*Math.atan2(t.b[1]-t.a[1],t.b[0]-t.a[0])/Math.PI;return{interval:t,distance:an(e.angle,s)?on([e.x,e.y],t.a,t.b):1/0}}).filter(t=>t.distance<=i).sort((t,e)=>t.distance-e.distance||t.interval.key.localeCompare(e.interval.key)),o=s[0]?.interval;"outer"===o?.kind&&!o.open&&o.cm>0&&a.set(e.id,o.key)}return{materialize:o,openingIntervals:a,removeOpeningIds:[...a.keys()].sort((t,e)=>t.localeCompare(e))}}function rn(t,e){const s=e.b[0]-e.a[0],i=e.b[1]-e.a[1],o=s*s+i*i;return o>0?Math.max(0,Math.min(1,((t[0]-e.a[0])*s+(t[1]-e.a[1])*i)/o)):0}const ln=t=>!t.key.startsWith("static:room|");function hn(t,e){const s=e.b[0]-e.a[0],i=e.b[1]-e.a[1],o=s*s+i*i;if(!(o>0))return null;const a=((t[0]-e.a[0])*s+(t[1]-e.a[1])*i)/o,n=[e.a[0]+s*a,e.a[1]+i*a];return{point:n,t:a,distance:Math.hypot(n[0]-t[0],n[1]-t[1])}}function cn(t,e){return e>0?Math.hypot(t[0]-Math.round(t[0]/e)*e,t[1]-Math.round(t[1]/e)*e):0}function dn(t,e,s){if(ln(t.source)!==ln(e.source))return ln(t.source)?t:e;const i=cn(t.point,s),o=cn(e.point,s);return Math.abs(i-o)>1e-9?i>o?t:e:function(t,e){return t[0]-e[0]||t[1]-e[1]}(t.point,e.point)>0?t:e}function pn(t){return[t.from[0],t.from[1],t.to[0],t.to[1],t.targetKind].map(t=>"number"==typeof t?t.toFixed(6):t).join("|")}function _n(t,e){return t.map(t=>t.key!==e.sourceKey?t:{...t,[e.endpoint]:[...e.to]})}function un(t,e){const s=e.epsilon??.001;if(!(e.maxDistance>=0)||!e.point&&!e.requiredSourceKey)return{kind:"none"};const i=new Map,o=t.flatMap(t=>[{source:t,endpoint:"a",point:t.a},{source:t,endpoint:"b",point:t.b}]);for(let t=0;ts)||l>e.maxDistance)continue;const h=dn(n,r,e.gridStep||0),c=h===n?r:n;if(!ln(h.source))continue;const d={sourceKey:h.source.key,endpoint:h.endpoint,from:[h.point[0],h.point[1]],to:[c.point[0],c.point[1]],targetSourceKey:c.source.key,targetKind:"endpoint",distance:l};i.set(pn(d),d)}for(const a of o)if(ln(a.source))for(const o of t){if(o===a.source)continue;const t=hn(a.point,o);if(!t||t.t<=s||t.t>=1-s||!(t.distance>s)||t.distance>e.maxDistance)continue;const n={sourceKey:a.source.key,endpoint:a.endpoint,from:[a.point[0],a.point[1]],to:t.point,targetSourceKey:o.key,targetKind:"line",distance:t.distance};i.set(pn(n),n)}const a=[];for(const o of i.values()){const i=en(_n(t,o),s),n=e.point?sn(i,e.point,s):i.faces.find(t=>!e.requiredSourceKey||t.sourceKeys.includes(e.requiredSourceKey))||null;n&&a.push({proposal:o,face:n})}if(a.sort((t,e)=>t.proposal.distance-e.proposal.distance||pn(t.proposal).localeCompare(pn(e.proposal))),!a.length)return{kind:"none"};return new Set(a.map(t=>`${pn(t.proposal)}|${t.face.key}`)).size>1?{kind:"ambiguous",proposals:a.map(t=>t.proposal)}:{kind:"repair",...a[0]}}const mn=(t,e)=>(t.composedPath?.()||[t.target]).some(t=>(t=>!!t&&"function"==typeof t.matches)(t)&&e(t));class gn{constructor(t){this.host=t,this._openGroupId=null,this._groupGeneration=0,this._renderedContext="",this._focusOwned=!1,this._currentModel=null,this._blocked=!1,this._dismissPointerTarget=null,this._globalDismissListening=!1,this._globalDismissGuard=t=>{this.handleOutsideDismiss(t)}}get hasOpenGroup(){return null!==this._openGroupId}get groupGeneration(){return this._groupGeneration}activeGroup(t){return this._openGroupId&&t.find(t=>t.id===this._openGroupId)||null}reset(){this._openGroupId=null,this._contentAnimation?.cancel(),this._contentAnimation=void 0,this._renderedContext="",this._focusOwned=!1,this._currentModel=null,this._blocked=!1,this._dismissPointerTarget=null,clearTimeout(this._dismissTimer),this._dismissTimer=void 0,this._syncGlobalDismissListener()}closeForNavigation(){const t="palette"===this._currentModel?.kind?this._currentModel.dismiss:void 0,e=!!this._openGroupId||!!t;this._openGroupId=null,t?.(),t&&(this._currentModel=null),this._clearDismissClick(),e&&this.host.requestUpdate(),this._syncGlobalDismissListener()}openPalette(){this._openGroupId&&this.closeGroup(!1)}toggleGroup(t,e){this._openGroupId!==e?t.some(t=>t.id===e)&&("palette"===this._currentModel?.kind&&this._currentModel.dismiss?.(),this._groupGeneration+=1,this._openGroupId=e,this.host.clearTip(),this.host.requestUpdate(),this._syncGlobalDismissListener()):this.closeGroup(!1)}closeGroup(t){const e=this._openGroupId;this._openGroupId=null,this.host.requestUpdate(),this._syncGlobalDismissListener(),t&&e&&this.host.updateComplete().then(()=>{const t=this.host.root(),s=[...t.querySelectorAll("[data-editor-group]")].find(t=>t.dataset.editorGroup===e&&null!==t.offsetParent);(s||t.querySelector(".editbar-tools"))?.focus?.()})}renderGroupLauncher(t,e,s){const i=this._openGroupId===t.id,a=t.items.find(e=>e.id===t.activeItemId&&!e.disabled);return o``}renderGroupModel(t,e,s){const i=t.items.find(t=>!t.disabled)?.id,n=t.items.some(e=>e.id===t.activeItemId&&!e.disabled)?t.activeItemId:void 0;return{contextId:e,kind:"group",ariaLabel:s.openGroup(t.label),visibleLabel:t.label,content:o`
this._groupKeydown(t)}> + ${t.items.map(e=>o``)} +
`}}runContext(t,e,s){t===e&&s()}render(t,e){this._currentModel=t,this._blocked=e;const s=t?.kind||"hidden";return o`
+ +
`}afterRender(){const t=this.host.root();this._blocked&&(this._openGroupId?this.closeGroup(!1):"palette"===this._currentModel?.kind&&this._currentModel.dismiss?.()),this._syncGlobalDismissListener();const e=t.querySelector(".editor-secondary"),s=this._renderedContext,i=e?.dataset.contextId||"";if(i&&s&&i!==s&&!window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches&&(this._contentAnimation?.cancel(),this._contentAnimation=e?.querySelector(".editor-secondary-content")?.animate([{opacity:.35,transform:"translateY(-4px)"},{opacity:1,transform:"translateY(0)"}],{duration:100,easing:"ease-out"})),this._renderedContext=i,s&&i!==s&&this._focusOwned){const s=i?e?.querySelector('[tabindex="0"], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled])'):t.querySelector(".editbar-tools");s?.focus?.(),this._focusOwned=!!i}if("palette"===this._currentModel?.kind&&this._currentModel.launcherId){const e=[...t.querySelectorAll("[data-editor-palette]")].find(t=>t.dataset.editorPalette===this._currentModel?.launcherId);e&&null!==e.offsetParent||this._currentModel.dismiss?.()}if(!this._openGroupId)return;const o=[...t.querySelectorAll("[data-editor-group]")].find(t=>t.dataset.editorGroup===this._openGroupId);o&&null!==o.offsetParent||this.closeGroup(!0)}handleOutsideDismiss(t){if(mn(t,t=>t.hasAttribute("data-editor-navigation")))return"pointerdown"===t.type?this.closeForNavigation():"click"===t.type&&this._dismissPointerTarget&&this._clearDismissClick(),!1;if("click"===t.type&&this._dismissPointerTarget){return!!(t.composedPath?.()||[t.target]).includes(this._dismissPointerTarget)&&(this._clearDismissClick(),t.preventDefault(),t.stopImmediatePropagation(),!0)}const e=this._openGroupId||this._blocked||"palette"!==this._currentModel?.kind?null:this._currentModel;if("pointerdown"===t.type&&(this._openGroupId||e)){const s=mn(t,t=>t.classList.contains("editor-secondary")),i=mn(t,t=>!!t.dataset.editorGroup),o=mn(t,t=>!!t.dataset.editorPalette),a=mn(t,t=>t.classList.contains("stage"));if(!(s||i||o||"stay-open-on-canvas"===e?.dismissPolicy&&a))return t.preventDefault(),t.stopImmediatePropagation(),this._dismissPointerTarget=t.target,clearTimeout(this._dismissTimer),this._dismissTimer=window.setTimeout(()=>{this._clearDismissClick()},800),this._openGroupId?this.closeGroup(!1):e?.dismiss?.(),this._syncGlobalDismissListener(),!0}return!1}_activateGroupItem(t,e,s){if(t.id!==this._openGroupId)return;const i=t.items.find(t=>t.id===e);if(!i||i.disabled)return;const o="toggle"===i.role&&"stay-open"===i.closePolicy,a=0===s.detail;o||(this._openGroupId=null,this.host.requestUpdate(),this._syncGlobalDismissListener()),i.invoke(),!o&&a&&this.host.updateComplete().then(()=>{const e=[...this.host.root().querySelectorAll("[data-editor-group]")].find(e=>e.dataset.editorGroup===t.id&&null!==e.offsetParent);e?.focus()})}_groupKeydown(t){if(!["ArrowLeft","ArrowRight","Home","End"].includes(t.key))return;const e=[...t.currentTarget.querySelectorAll(".editor-group-item:not([disabled])")];if(!e.length)return;t.preventDefault();const s=e.indexOf(this.host.root().activeElement),i="Home"===t.key?0:"End"===t.key?e.length-1:"ArrowLeft"===t.key?s<=0?e.length-1:s-1:(s+1)%e.length;e[i]?.focus()}_clearDismissClick(){this._dismissPointerTarget=null,clearTimeout(this._dismissTimer),this._dismissTimer=void 0,this._syncGlobalDismissListener()}_syncGlobalDismissListener(){const t=!this._blocked&&"palette"===this._currentModel?.kind,e=!!this._openGroupId||t||!!this._dismissPointerTarget;e!==this._globalDismissListening&&(this._globalDismissListening=e,e?(window.addEventListener("pointerdown",this._globalDismissGuard,!0),window.addEventListener("click",this._globalDismissGuard,!0)):(window.removeEventListener("pointerdown",this._globalDismissGuard,!0),window.removeEventListener("click",this._globalDismissGuard,!0)))}}const fn=t=>t.stopPropagation(),bn=1e-6*I,yn=6*I;function vn(t){return Number.isFinite(t)?Math.round(t/I)*I:t}const $n=(t,e,s,i)=>Math.hypot(s-t,i-e),wn=(t,e,s,i,o,a,n,r)=>Math.hypot(Math.max(Math.abs(o-t),Math.abs(o+n-(t+s))),Math.max(Math.abs(a-e),Math.abs(a+r-(e+i)))),kn=(t,e,s,i,o,a,n)=>{const r=Math.max(Number(i)||0,0)/2,l=Math.PI/180,h=Math.cos(s*l)*r,c=Math.sin(s*l)*r,d=Math.cos(n*l)*r,p=Math.sin(n*l)*r,_=Math.max($n(t+h,e+c,o+d,a+p),$n(t-h,e-c,o-d,a-p)),u=Math.max($n(t+h,e+c,o-d,a-p),$n(t-h,e-c,o+d,a+p));return Math.min(_,u)},xn=t=>{const e=Number(t?.cell_cm);return e>0?e:5};function Sn(t,e){const s=JSON.parse(JSON.stringify(t||[])),i=JSON.parse(JSON.stringify(e||{}));let o=0,a=0,n=0,r=0,l="",h=0,c=0,d=0;const p=(t,e)=>{Number.isFinite(t)&&Number.isFinite(e)&&t!==e&&Math.abs(e-t)<=bn&&c++},_=(t,e)=>{p(t[0],e[0]),p(t[1],e[1])},u={};let m=5;for(const t of s){const e=xn(t);null!=t?.id&&(u[String(t.id)]=e),e>m&&(m=e)}const g=(t,e,s,i=!1)=>{if(!(t>bn||i))return;o++,t>n&&(n=t);const a=t*T*e;a>r&&(r=a,l=s)};for(const t of s){const e=xn(t),s=null!=t?.id?String(t.id):"";for(const i of t.rooms||[]){a++;let t=0;if(i.poly?.length)i.poly=i.poly.map(e=>{const s=[vn(e[0]),vn(e[1])];return t=Math.max(t,$n(e[0],e[1],s[0],s[1])),_(e,s),s});else if(null!=i.x&&null!=i.y){const e=i.x,s=i.y,o=i.w||0,a=i.h||0,n=vn(e+o),r=vn(s+a),l=vn(e),h=vn(s),c=Math.max(I,n-l),d=Math.max(I,r-h);t=wn(e,s,o,a,l,h,c,d),i.x=l,i.y=h,i.w=c,i.h=d,p(e,l),p(s,h),p(e+o,l+c),p(s+a,h+d)}g(t,e,s)}for(const i of t.room_drafts||[]){a++;let t=0;const o=i.points||[],n=o.map(e=>{const s=[vn(e[0]),vn(e[1])];return t=Math.max(t,$n(e[0],e[1],s[0],s[1])),s}),r=n.length?[n[0]]:[],l=n.length?[[o[0],n[0]]]:[],h=[];for(let t=0;t+1=2)for(const[t,e]of l)_(t,e);g(t,e,s)}if(Array.isArray(t.room_drafts)){const e=t.room_drafts.length;t.room_drafts=t.room_drafts.filter(t=>t.points?.length>=2),d+=e-t.room_drafts.length,t.room_drafts.length||delete t.room_drafts}for(const i of t.partitions||[]){a++;const o=[i.a[0],i.a[1]],n=[i.b[0],i.b[1]],r=[vn(o[0]),vn(o[1])],l=[vn(n[0]),vn(n[1])];let h=Math.max($n(o[0],o[1],r[0],r[1]),$n(n[0],n[1],l[0],l[1]));const c=$n(r[0],r[1],l[0],l[1]),d=(t.openings||[]).filter(t=>"partition"===t.host?.kind&&t.host.id===i.id).every(t=>{const e=Number(t.length),s=Number(t.host.t),i=s*c;return Number.isFinite(e)&&e>0&&Number.isFinite(s)&&s>=0&&s<=1&&i-e/2>=-bn&&i+e/2<=c+bn});c>bn&&d?(i.a=r,i.b=l,_(o,r),_(n,l)):h=0,g(h,e,s)}for(const i of t.wall_columns||[]){a++;const t=[i.center[0],i.center[1]],o=[vn(t[0]),vn(t[1])],n=$n(t[0],t[1],o[0],o[1]);i.center=o,_(t,o),g(n,e,s)}for(const i of t.decor||[]){a++;let t=0;if("line"===i.kind){const e=[i.x1,i.y1],s=[i.x2,i.y2],o=[vn(e[0]),vn(e[1])],a=[vn(s[0]),vn(s[1])];t=Math.max($n(e[0],e[1],o[0],o[1]),$n(s[0],s[1],a[0],a[1])),i.x1=o[0],i.y1=o[1],i.x2=a[0],i.y2=a[1],_(e,o),_(s,a)}else{const e=i.x,s=i.y,o=i.w,a=i.h,n=vn(e),r=vn(s);if(null!=i.w&&null!=i.h){const l=vn(e+o),h=vn(s+a),c=Math.max(I,l-n),d=Math.max(I,h-r);t=wn(e,s,o,a,n,r,c,d),i.w=c,i.h=d,p(e+o,n+c),p(s+a,r+d)}else t=$n(e,s,n,r);i.x=n,i.y=r,p(e,n),p(s,r)}g(t,e,s)}for(const i of t.openings||[]){if(a++,"partition"===i.host?.kind){const o=(t.partitions||[]).find(t=>t.id===i.host.id),a=Number(i.host.t);if(!o||!Number.isFinite(a)||a<0||a>1)continue;const n=o.b[0]-o.a[0],r=o.b[1]-o.a[1];if(Math.hypot(n,r)<=bn)continue;const l=o.a[0]+n*a,c=o.a[1]+r*a;let d=180*Math.atan2(r,n)/Math.PI;d>=90?d-=180:d<-90&&(d+=180);const p=Number(i.angle),_=!(Number.isFinite(p)&&p===d),u=kn(i.x,i.y,Number.isFinite(p)?p:d,Number(i.length)||0,l,c,d);i.x=l,i.y=c,i.angle=d,_&&h++,g(u,e,s,_);continue}const o=P([i.x,i.y],t.rooms||[],yn,{step:I,length:Number(i.length)||0});if(!o)continue;const n=Number(i.angle),r=!(Number.isFinite(n)&&n===o.angle),l=kn(i.x,i.y,Number.isFinite(n)?n:o.angle,Number(i.length)||0,o.x,o.y,o.angle);i.x=o.x,i.y=o.y,i.angle=o.angle,r&&h++,g(l,e,s,r)}}for(const[t,e]of Object.entries(i)){if(!e||"object"!=typeof e)continue;const s=e;if("number"!=typeof s.x||"number"!=typeof s.y)continue;a++;const o=vn(s.x),n=vn(s.y),r=$n(s.x,s.y,o,n);i[t]={...s,x:o,y:n},p(s.x,o),p(s.y,n);const l="string"==typeof s.s?s.s:"";g(r,u[l]??m,l)}return{spaces:s,layout:i,report:{moved:o,coordsCanonicalized:c,total:a,maxShift:n,maxShiftCm:r,maxSpace:l,rotated:h,removedDrafts:d},changed:o>0||c>0}}function Mn(t,e){const s=e.base+t*e.span;return Math.min(1,Math.max(0,s))}const Dn=t=>Array.isArray(t)&&t.length>=2&&Number.isFinite(t[0])&&Number.isFinite(t[1]),Cn=(t,e)=>Math.hypot(t[0]-e[0],t[1]-e[1]);function Pn(t,e,s){const i=s[0]-e[0],o=s[1]-e[1],a=i*i+o*o;if(a<=0)return Cn(t,e);let n=((t[0]-e[0])*i+(t[1]-e[1])*o)/a;return n=Math.max(0,Math.min(1,n)),Math.hypot(t[0]-(e[0]+i*n),t[1]-(e[1]+o*n))}function In(t,e,s,i,o){for(const i of e)if(i&&!s.has(i.id)&&Dn(i.a)&&Dn(i.b)&&(Cn(t,i.a)<=o||Cn(t,i.b)<=o))return!0;for(const e of i?.roomPolygons||[])for(let s=0;s0&&r>0))return null;if(Math.abs(o[0]/n*(a[1]/r)-o[1]/n*(a[0]/r))>s)return null;for(const[s,o]of[[t.a,e.a],[t.a,e.b],[t.b,e.a],[t.b,e.b]])if(Cn(s,o)<=i)return[(s[0]+o[0])/2,(s[1]+o[1])/2];return null}function Rn(t,e){const s=.05*(Number(e.pitch)>0?Number(e.pitch):1),i=new Map;let o=(t||[]).filter(t=>!!t&&"string"==typeof t.id&&Dn(t.a)&&Dn(t.b));if(o.length<2)return{partitions:[...t||[]],merged:0,openingMoves:[]};const a=e.seedIds&&e.seedIds.length?new Set(e.seedIds):null;let n=0;for(let t=0;tCn(e,t.at)>s);let c=h[0]??r.a,d=h[h.length-1]??l.b;if(h.length<2&&(c=r.a,d=l.b),d[0]0?(d[0]-c[0])/_:0,m=_>0?(d[1]-c[1])/_:0,g=t=>_>0?((t[0]-c[0])*u+(t[1]-c[1])*m)/_:0;for(const t of[r,l]){const e=g(t.a),s=g(t.b)-e;for(const[o,a]of i)a.toId===t.id&&i.set(o,{fromId:a.fromId,toId:p.id,base:e+a.base*s,span:a.span*s});i.set(t.id,{fromId:t.id,toId:p.id,base:e,span:s})}a&&a.add(p.id),o=o.filter((e,s)=>s!==t.i&&s!==t.j),o.splice(t.i,0,p),n++}return{partitions:o,merged:n,openingMoves:[...i.values()]}}function Fn(t,e,s,i){if(!t?.length||!s.length)return 0;const o=new Map(s.map(t=>[t.fromId,t]));let a=0;for(const s of t){const t=s?.host;if(!t||"partition"!==t.kind)continue;const n=o.get(t.id);if(!n)continue;s.host={...t,id:n.toId,t:Mn(t.t,n)},a++;const r=F(s,e,i.coordScale,i.cellCm,i.gridPitch).resolved;r&&Object.assign(s,N(s,r,i.coordScale))}return a}function Nn(t,e){const s=e?.excludeDraftId||null;return{roomPolygons:(t?.rooms||[]).map(t=>R(t)).filter(t=>!!t),columns:t?.wall_columns||[],draftEnds:(t?.room_drafts||[]).flatMap(t=>{if(s&&t?.id===s)return[];const e=t?.points||[];return e.length?[e[0],e[e.length-1]]:[]})}}const zn=t=>!!t&&t.length>=2&&Number.isFinite(t[0])&&Number.isFinite(t[1]),An=(t,e)=>t[0]-e[0]||t[1]-e[1],On=(t,e,s,i)=>[t[0]+e*i,t[1]+s*i],Ln=(t,e,s,i)=>(t[0]-e[0])*s+(t[1]-e[1])*i,En=(t,e,s,i,o)=>{const a=e[0]-t[0],n=e[1]-t[1],r=Math.hypot(a,n);if(!(r>o))return null;const l=a/r,h=n/r,c=e=>Math.abs((e[0]-t[0])*h-(e[1]-t[1])*l);if(c(s)>o||c(i)>o)return null;const d=Ln(s,t,l,h),p=Ln(i,t,l,h),_=Math.max(0,Math.min(d,p)),u=Math.min(r,Math.max(d,p));return u-_>o?{lo:_,hi:u}:null},Bn=(t,e,s)=>Math.min(t.hi,e.hi)-Math.max(t.lo,e.lo)>s,Hn=t=>{const e=new Set(["id","a","b","cm"]);return Object.keys(t||{}).every(t=>e.has(t))},Gn=(t,e,s,i)=>{const o=t.b[0]-t.a[0],a=t.b[1]-t.a[1],n=Math.hypot(o,a);if(!(n>i))return!0;const r=o/n,l=a/n,h=j(t.cm,s.cellCm,s.gridPitch)/2;return e.some(e=>{const o=e?.center;if(!zn(o))return!0;const a=Ln(o,t.a,r,l),c=Math.abs((o[0]-t.a[0])*l-(o[1]-t.a[1])*r),d=j(Number(e.cm),s.cellCm,s.gridPitch)*("square"===e.shape?Math.SQRT1_2:.5);return!(Number.isFinite(d)&&d>0)||a>=-d-i&&a<=n+d+i&&c<=h+d+i})},Wn=(t,e,s)=>{if(t.host){const i=F(t,e,s.coordScale,s.cellCm,s.gridPitch).resolved;return i?{center:i.center,angle:i.angle,length:i.length}:null}const i=Number(t.x)*s.coordScale,o=Number(t.y)*s.coordScale,a=Number(t.angle),n=Number(t.length)*s.coordScale;return[i,o,a,n].every(Number.isFinite)&&n>0?{center:[i,o],angle:a,length:n}:null},jn=(t,e,s)=>{const i=t.angle*Math.PI/180,o=Math.cos(i),a=Math.sin(i),n=e.angle*Math.PI/180,r=Math.cos(n),l=Math.sin(n);if(Math.abs(o*l-a*r)>1e-6)return!1;const h=e.center[0]-t.center[0],c=e.center[1]-t.center[1];if(Math.abs(h*a-c*o)>s)return!1;const d=h*o+c*a;return Math.max(-t.length/2,d-e.length/2)({...t,a:[t.a[0]*e,t.a[1]*e],b:[t.b[0]*e,t.b[1]*e]}),Un=(t,e,s,i)=>{const o=`~r-${(t=>{let e=2166136261;for(let s=0;s>>0;return e.toString(36)})(`${t}|${e[0].toFixed(9)},${e[1].toFixed(9)}|${s[0].toFixed(9)},${s[1].toFixed(9)}`)}`,a=`${t.slice(0,Math.max(1,64-o.length))}${o}`;let n=a;for(let t=2;i.has(n);t++){const e=`-${t}`;n=`${a.slice(0,64-e.length)}${e}`}return i.add(n),n},Kn=(t,e)=>{const s=[];for(const i of t){const t=s[s.length-1];!(t&&Math.abs(t.hi-i.lo)<=e&&t.safe===i.safe)||i.safe&&t.signature!==i.signature?s.push({lo:i.lo,hi:i.hi,a:i.a,b:i.b,safe:i.safe,signature:i.signature,roomIds:i.roomIds,finalCm:i.finalCm}):(t.hi=i.hi,t.b=i.b)}return s};function Vn(t,e,s,i,o){const a=s||[],n=Array.isArray(t?.partitions)?t.partitions:[],r=Array.isArray(t?.openings)?t.openings:[],l=Array.isArray(t?.room_drafts)?t.room_drafts:[],h={walls:a,partitions:n,openings:r,roomDrafts:l,partitionsReconciled:0,openingsRehosted:0,removedDrafts:0};if(!n.length&&!l.length)return h;const c=Math.max(2e-4*o.gridPitch,1e-9);let d=a,p=n,_=r;const u=z(e.rooms,d,i,o.pitch,o.cellCm,o.gridPitch,o.coordScale),m=((t,e,s,i)=>{const o=new Map(t.map(t=>[t.id,t])),a=new Set;for(const t of e){if(!o.get(t.id)||!Array.isArray(t.points)||t.points.length<2||t.segments.length!==t.points.length-1)continue;let e=!0;for(let o=0;o+10)){e=!1;break}const l=Math.hypot(n[0]-a[0],n[1]-a[1]);if(!(l>i)){e=!1;break}const h=s.flatMap(t=>{const e=t.cm>0?t.cm:A;if(t.open||"outer"!==t.kind&&"shared"!==t.kind||e+it.lo-e.lo||t.hi-e.hi);let c=0;for(const t of h){if(t.lo>c+i)break;if(c=Math.max(c,t.hi),c>=l-i)break}c!m.has(t.id));let f=0,b=0;const y=new Map(e.partitions.map(t=>[t.id,t]));for(const t of[...n].sort((t,e)=>t.id.localeCompare(e.id))){if(!p.some(e=>e.id===t.id))continue;if(!Hn(t))continue;const s=y.get(t.id);if(!(s&&zn(s.a)&&zn(s.b)&&Number.isFinite(s.cm)&&s.cm>0))continue;const[a,n]=An(s.a,s.b)<=0?[[s.a[0],s.a[1]],[s.b[0],s.b[1]]]:[[s.b[0],s.b[1]],[s.a[0],s.a[1]]],r=n[0]-a[0],l=n[1]-a[1],h=Math.hypot(r,l);if(!(h>c))continue;const u=r/h,g=l/h,v=z(e.rooms,d,i,o.pitch,o.cellCm,o.gridPitch,o.coordScale),$=v.flatMap(t=>{if(t.open||"outer"!==t.kind&&"shared"!==t.kind)return[];const e=En(a,n,t.a,t.b,c);return e?[{interval:t,range:e}]:[]});if(!$.length)continue;const w=_.filter(t=>"partition"===t.host?.kind&&t.host.id===s.id).map(t=>({opening:t,resolved:F(t,e.partitions,o.coordScale,o.cellCm,o.gridPitch).resolved}));if(w.some(t=>!t.resolved))continue;const k=[0,h];for(const{range:t}of $)k.push(t.lo,t.hi);const x=k.slice(),S=new Map;for(const{opening:t,resolved:e}of w){const s=Ln(e.center,a,u,g),i={lo:s-e.length/2,hi:s+e.length/2};S.set(t.id,i),x.push(Math.max(0,i.lo),Math.min(h,i.hi))}const M=[...new Set(x.filter(t=>Number.isFinite(t)&&t>=-c&&t<=h+c).map(t=>Math.max(0,Math.min(h,t)).toFixed(9)))].map(Number).sort((t,e)=>t-e),D=p.map(t=>qn(t,o.coordScale)).filter(t=>t.id!==s.id),C=e.room_drafts.filter(t=>!m.has(t.id)),P=[];for(let t=0;t+1c))continue;const r=On(a,u,g,i),l=On(a,u,g,n),h=$.filter(({range:t})=>t.lo<=i+c&&t.hi>=n-c).map(({interval:t})=>t),d=new Map;for(const t of h)d.set(t.roomId,t);const p=[...d.values()],_=new Set(p.map(t=>t.kind)),m=new Set(p.map(t=>t.cm>0?t.cm:A)),f=p[0]?.kind,b=[...d.keys()].sort(),y=1===_.size&&1===m.size&&("outer"===f&&1===b.length||"shared"===f&&2===b.length),v={id:s.id,a:r,b:l,cm:s.cm},w=D.some(t=>!!En(r,l,t.a,t.b,c)),k=C.some(t=>t.points.some((e,s)=>s+10?p[0].cm:A:0,I=y?Math.max(S,s.cm):s.cm;P.push({lo:i,hi:n,a:r,b:l,safe:x,signature:x?`${f}|${b.join(",")}|${I}`:"",roomIds:b,roomCm:S,finalCm:I})}if(!P.some(t=>t.safe))continue;const I=new Map;for(const{opening:t,resolved:e}of w){const i=S.get(t.id),a=k.some(t=>t>i.lo+c&&tBn(t,i,c)),r=new Set(n.filter(t=>t.safe).map(t=>t.signature));if(!a&&n.length>0&&n.every(t=>t.safe)&&1===r.size&&O(e,v,c)){I.set(t.id,{resolved:e,signature:[...r][0]});continue}const l=L(s,o.cellCm,o.gridPitch),h={lo:i.lo-l,hi:i.hi+l};for(const t of P)Bn(t,h,c)&&(t.safe=!1,t.signature="")}let T=!0;for(;T;){T=!1;for(const[t,e]of[...I.entries()]){const i=S.get(t),a=P.filter(t=>Bn(t,i,c));if(a.length>0&&a.every(t=>t.safe)&&a.every(t=>t.signature===e.signature))continue;I.delete(t);const n=L(s,o.cellCm,o.gridPitch),r={lo:i.lo-n,hi:i.hi+n};for(const t of P)Bn(t,r,c)&&(t.safe=!1,t.signature="");T=!0}}const R=Kn(P,c),j=R.filter(t=>t.safe),q=R.filter(t=>!t.safe);if(!j.length)continue;if(p.length-1+q.length>2e3)continue;let U=d;for(const t of j){const[e,i]=An(s.a,s.b)<=0?[t.a,t.b]:[t.b,t.a];U=E(U,e,i,t.finalCm,o.pitch,o.coordScale)}if(U=B(e.rooms,U,i,o.pitch,o.cellCm,o.gridPitch,o.coordScale),U.length>500)continue;const K=new Set(p.map(t=>t.id));K.delete(s.id);const V=q.map((t,e)=>{const i=[t.a[0]/o.coordScale,t.a[1]/o.coordScale],a=[t.b[0]/o.coordScale,t.b[1]/o.coordScale],n=0===e?s.id:Un(s.id,i,a,K);return K.add(n),{id:n,a:i,b:a,cm:s.cm}}),J=p.findIndex(t=>t.id===s.id),Y=p.slice();Y.splice(J,1,...V);const X=Y.map(t=>qn(t,o.coordScale)),Z=new Map;let Q=!0;for(const{opening:t,resolved:e}of w){const s=I.get(t.id);if(s){const e=N(t,s.resolved,o.coordScale),{host:i,...a}=e;Z.set(t.id,a);continue}const i=S.get(t.id),a=V[q.findIndex(t=>t.lo<=i.lo+c&&t.hi>=i.hi-c)],n=a&&X.find(t=>t.id===a.id);if(!a||!n){Q=!1;break}const r=n.b[0]-n.a[0],l=n.b[1]-n.a[1],h=Math.hypot(r,l),d=Ln(e.center,n.a,r/h,l/h),p={...N(t,e,o.coordScale),host:{kind:"partition",id:a.id,t:d/h}};if(!H(p,X,o.coordScale,o.cellCm,o.gridPitch).resolved){Q=!1;break}Z.set(t.id,p)}if(!Q)continue;const tt=_.map(t=>Z.get(t.id)||t),et=[...I.keys()].map(t=>Z.get(t)),st=et.map(t=>Wn(t,X,o));if(st.some(t=>!t))continue;let it=!1;for(let t=0;tt.id));for(const t of tt){if(ot.has(t.id))continue;const e=Wn(t,X,o);e&&st.some(t=>jn(t,e,c))&&(it=!0)}if(it)continue;const at=G(e.rooms,U,i,o.pitch,o.cellCm,o.gridPitch,o.coordScale);[...I.entries()].every(([t,e])=>{const s=Z.get(t),i=W(at,{x:s.x*o.coordScale,y:s.y*o.coordScale,angle:s.angle,length:s.length*o.coordScale},!0),a=[i.negative,i.positive].filter(t=>!!t?.full),n=e.signature.split("|")[1].split(","),r=new Set(a.map(t=>t.roomId));return a.length===n.length&&r.size===n.length&&n.every(t=>r.has(t))})&&(d=U,p=Y,_=tt,f+=j.length,b+=I.size)}return{walls:d,partitions:p,openings:_,roomDrafts:g,partitionsReconciled:f,openingsRehosted:b,removedDrafts:m.size}}const Jn=t=>JSON.parse(JSON.stringify(t)),Yn=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),Xn=(t,e)=>e.length>0&&e.length<=35&&("space"===t?/^[a-z0-9_-]+$/.test(e):/^[A-Za-z0-9_-]+$/.test(e));function Zn(t,e){let s=String(e??"");const i=t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),o=new RegExp(`^${i}_(.+)_([0-9a-f]{8})$`);let a=0;for(;a<16;a++){const t=o.exec(s);if(!t)return{root:s,layers:a,bounded:!1};s=t[1]}return{root:s,layers:a,bounded:o.test(s)}}const Qn=(t,e,s)=>{const i=t.get(e)||[];i.push(s),t.set(e,i)};const tr=1e-9;class er extends Error{constructor(t,e=""){super(e?`${t}: ${e}`:t),this.reason=t,this.name="WallSegmentModelError"}}const sr=t=>JSON.parse(JSON.stringify(t)),ir=t=>Array.isArray(t)&&t.length>=2&&Number.isFinite(Number(t[0]))&&Number.isFinite(Number(t[1])),or=t=>[Number(t[0]),Number(t[1])],ar=t=>`${Number(t[0]).toFixed(12)},${Number(t[1]).toFixed(12)}`,nr=(t,e)=>{const s=ar(t),i=ar(e);return sar(t)<=ar(e)?[or(t),or(e)]:[or(e),or(t)],lr=(t,e)=>Math.hypot(e[0]-t[0],e[1]-t[1]),hr=t=>{const e=[[Number(t.points[0][0]),Number(t.points[0][1])]],s=[];for(let i=1;i{const s=new Set,i=t=>{const e=Number(t);if(!Number.isFinite(e))return;const i=e/I;Math.abs(i-Math.round(i))>=X&&s.add(e.toFixed(12))};for(const e of Array.isArray(t?.rooms)?t.rooms:[])for(const t of R(e)||[])i(t[0]),i(t[1]);for(const e of[t?.wall_segments,t?.walls])for(const t of Array.isArray(e)?e:[])i(t?.a?.[0]),i(t?.a?.[1]),i(t?.b?.[0]),i(t?.b?.[1]);for(const t of e)i(t?.[0]),i(t?.[1]);return s.size},dr=(t,e,s)=>{const i=s[0]-e[0],o=s[1]-e[1],a=i*i+o*o;return a>1e-18?((t[0]-e[0])*i+(t[1]-e[1])*o)/a:0},pr=(t,e,s)=>{const i=Math.max(0,Math.min(1,dr(t,e,s)));return Math.hypot(t[0]-(e[0]+(s[0]-e[0])*i),t[1]-(e[1]+(s[1]-e[1])*i))},_r=(t,e,s,i,o=tr)=>{const a=e[0]-t[0],n=e[1]-t[1],r=Math.hypot(a,n);if(r<=o)return 0;const l=e=>Math.abs((e[0]-t[0])*n-(e[1]-t[1])*a)/r;if(l(s)>o||l(i)>o)return 0;const h=dr(s,t,e),c=dr(i,t,e);return Math.max(0,Math.min(1,Math.max(h,c))-Math.max(0,Math.min(h,c)))*r},ur=t=>{const e=(new TextEncoder).encode(t),s=8*e.length,i=64*Math.ceil((e.length+9)/64),o=new Uint8Array(i);o.set(e),o[e.length]=128;const a=new DataView(o.buffer);a.setUint32(i-8,Math.floor(s/4294967296),!1),a.setUint32(i-4,s>>>0,!1);const n=new Uint32Array([1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298]),r=new Uint32Array([1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225]),l=new Uint32Array(64),h=(t,e)=>t>>>e|t<<32-e;for(let t=0;t>>3,o=h(s,17)^h(s,19)^s>>>10;l[t]=l[t-16]+i+l[t-7]+o>>>0}let[e,s,i,o,c,d,p,_]=r;for(let t=0;t<64;t++){const a=_+(h(c,6)^h(c,11)^h(c,25))+(c&d^~c&p)+n[t]+l[t]>>>0,r=e&s^e&i^s&i;_=p,p=d,d=c,c=o+a>>>0,o=i,i=s,s=e,e=a+((h(e,2)^h(e,13)^h(e,22))+r>>>0)>>>0}r[0]=r[0]+e>>>0,r[1]=r[1]+s>>>0,r[2]=r[2]+i>>>0,r[3]=r[3]+o>>>0,r[4]=r[4]+c>>>0,r[5]=r[5]+d>>>0,r[6]=r[6]+p>>>0,r[7]=r[7]+_>>>0}const c=new Uint8Array(32),d=new DataView(c.buffer);for(let t=0;t{const e="abcdefghijklmnopqrstuvwxyz234567";let s=0,i=0,o="";for(const a of t)for(i=i<<8|a,s+=8;s>=5;)o+=e[i>>>s-5&31],s-=5;return s&&(o+=e[i<<5-s&31]),o},gr=(t,e,s,i,o="")=>{const[a,n]=rr(e,s),r=`${t}|${ar(a)}|${ar(n)}|${[...i].sort().join(",")}${o}`;return`wall-${mr(ur(r)).slice(0,20)}`};let fr=0;const br=t=>{for(let e=0;e<1e3;e++){const e=globalThis.crypto?.randomUUID?.(),s=`wall-${e||mr(ur(`${Date.now()}|${fr++}|${Math.random()}`)).slice(0,26)}`;if(!t.has(s))return s}throw new er("duplicate-id","id factory exhausted")},yr=t=>{const e=new Map;for(const s of Array.isArray(t?.wall_segments)?t.wall_segments:[])if(s&&"string"==typeof s.id&&ir(s.a)&&ir(s.b)){if(e.has(s.id))throw new er("duplicate-id",s.id);e.set(s.id,s)}return e},vr=(t,e,s)=>{const i=(s,i)=>{const o=t[0]-s[0],a=t[1]-s[1];return Math.abs(e[0]-i[0]-o)<=tr&&Math.abs(e[1]-i[1]-a)<=tr?[o,a]:null};return i(s.a,s.b)||i(s.b,s.a)},$r=(t,e)=>{const s=Array.isArray(t.rooms)?t.rooms:[],i=V(t.open_spans).map(t=>[t.a[0],t.a[1],t.b[0],t.b[1]]),o=i.length?i:J(s,null,1,.04*I,!0),a=[];for(const t of s){const s=R(t),i=Array.isArray(t?.wall_ids)?t.wall_ids:[];if(s&&i.length===s.length)for(let t=0;t"partition"!==t.host?.kind&&[Number(t.x),Number(t.y)].every(Number.isFinite)&&Number.isFinite(Number(t.angle))&&Number(t.length)>0&&o.some(e=>pr([Number(t.x),Number(t.y)],[e[0],e[1]],[e[2],e[3]])<=.04*I)):[],l=r.map(t=>{const e=Number(t.angle)*Math.PI/180,s=[Math.cos(e),Math.sin(e)],i=Number(t.length)/2,o=[Number(t.x),Number(t.y)];return{a:[o[0]-s[0]*i,o[1]-s[1]*i],b:[o[0]+s[0]*i,o[1]+s[1]*i]}}),h=(t,e)=>r.some(s=>{const i=[Number(s.x),Number(s.y)];if(!Z(t,e,Number(s.angle)))return!1;if(pr(i,t,e)>.02*I)return!1;const o=lr(t,e),a=dr(i,t,e)*o,n=Number(s.length)/2;return a+n>=-tr&&a-n<=o+tr}),c=new Map,d=[];for(const i of s){const r=String(i?.id||""),p=R(i);if(!r||!p||p.length<3)throw new er("invalid-room",r);const _=Y(s,r,n,I,1,[...t.walls||[],...l]);if(!_||_.poly.length<3)throw new er("invalid-room",r);const u=Array.isArray(i.wall_ids)?i.wall_ids:[],m=u.length===p.length;let g=null,f=m;if(f)for(let t=0;ttr||Math.abs(i[1]-g[1])>tr)){f=!1;break}g=i}const b=[];for(let t=0;t<_.poly.length;t++){const s=_.poly[t],i=_.poly[(t+1)%_.poly.length];if(lr(s,i)<=tr)throw new er("zero-length",r);const n=nr(s,i);let l=c.get(n);if(!l){const[t,e]=rr(s,i);l={key:n,a:t,b:e,owners:new Set,preferredIds:new Set,positionalIds:new Set,preferredCarriers:new Map,parentKeys:new Set,zeroWall:!1},c.set(n,l)}if(l.owners.add(r),l.owners.size>2)throw new er("third-owner",n);const d=m?u[_.parent[t]]:void 0;if("string"==typeof d&&d){const o=e.get(d);f||!o||_r(s,i,o.a,o.b)>tr?l.preferredIds.add(d):l.positionalIds.add(d),l.preferredCarriers.set(d,{a:or(p[_.parent[t]]),b:or(p[(_.parent[t]+1)%p.length])})}const g=_.parent[t];l.parentKeys.add(K(p[g],p[(g+1)%p.length],I));const y=[(s[0]+i[0])/2,(s[1]+i[1])/2],v=t=>t.some(t=>pr(y,[t[0],t[1]],[t[2],t[3]])<=.04*I);l.zeroWall||=v(a)||v(o)&&!h(s,i),b.push(n)}const y={...i};delete y.wall_ids,d.push({...y,poly:_.poly.map(t=>[t[0],t[1]]),wall_ids:b})}return{atoms:[...c.values()].sort((t,e)=>t.key.localeCompare(e.key)),rooms:d}},wr=(t,e,s,i,o)=>{const a=new Map([...s.values()].map(t=>[nr(t.a,t.b),t])),n=(t=>{const e=new Map;for(const s of t)"wall"===s.host?.kind&&e.set(s.host.id,(e.get(s.host.id)||0)+1);return e})(t.openings||[]),r=new Map;for(const t of e){const e=o?.get(t.key);if(e){const i=s.get(e);if(!i)throw new er("duplicate-id",e);r.set(t,i);continue}if(t.preferredIds.size>1)throw new er("duplicate-id",[...t.preferredIds].sort().join(","));const i=[...t.preferredIds][0],l=i?s.get(i):void 0;if(l){r.set(t,l);continue}const h=i?t.preferredCarriers.get(i):void 0;if(i&&h){r.set(t,{id:i,a:h.a,b:h.b,cm:0});continue}const c=a.get(t.key);if(c){r.set(t,c);continue}const d=[...s.values()].filter(e=>_r(t.a,t.b,e.a,e.b)>tr),p=[...new Map(d.map(t=>[t.id,t])).values()];if(p.sort((t,e)=>(n.get(e.id)||0)-(n.get(t.id)||0)||lr(e.a,e.b)-lr(t.a,t.b)||t.id.localeCompare(e.id)),p[0])r.set(t,p[0]);else if(1===t.positionalIds.size){const e=s.get([...t.positionalIds][0]);e&&r.set(t,e)}}const l=new Map;for(const[t,e]of r){const s=l.get(e.id)||[];s.push(t),l.set(e.id,s)}for(const[t,e]of l){const i=s.get(t)||r.get(e[0]);if(!i)throw new er("duplicate-id",t);const o=[(i.a[0]+i.b[0])/2,(i.a[1]+i.b[1])/2];e.sort((t,e)=>{const s=pr(o,t.a,t.b)<=tr?0:1,a=pr(o,e.a,e.b)<=tr?0:1;if(s!==a)return s-a;return(pr(i.a,t.a,t.b)<=tr?0:1)-(pr(i.a,e.a,e.b)<=tr?0:1)||t.key.localeCompare(e.key)}),e[0].id=t}const h=(t=>{const e=new Set;for(const s of[t.rooms,t.openings,t.decor,t.room_drafts,t.partitions,t.wall_columns])for(const t of Array.isArray(s)?s:[])"string"==typeof t?.id&&t.id&&e.add(t.id);for(const s of Array.isArray(t.room_drafts)?t.room_drafts:[])for(const t of Array.isArray(s?.segments)?s.segments:[])"string"==typeof t?.id&&t.id&&e.add(t.id);return e})(t);for(const t of e)if(t.id){if(h.has(t.id))throw new er("duplicate-id",t.id);h.add(t.id)}const c=e.filter(t=>!t.id);if(i){const e=c.map(e=>{const[s,i]=rr(e.a,e.b),o=`${String(t.id||"")}|${ar(s)}|${ar(i)}|${[...e.owners].sort().join(",")}`,a=mr(ur(o));return{atom:e,seed:o,digest:a,base:`wall-${a.slice(0,20)}`}}).sort((t,e)=>t.digest.localeCompare(e.digest)||t.atom.key.localeCompare(e.atom.key)),s=new Map;for(const t of e){const e=s.get(t.digest);if(e&&e!==t.seed)throw new er("duplicate-id",t.digest);s.set(t.digest,t.seed);let i=1,o=t.base;for(;h.has(o);)o=`${t.base}-${++i}`;t.atom.id=o,h.add(o)}}else for(const t of c)t.id=br(h),h.add(t.id)},kr=(t,e)=>{if("partition"===t.host?.kind)return null;const s=[Number(t.x),Number(t.y)];if(!s.every(Number.isFinite))return null;const i=e=>{if(!(Number(e.cm)>0))return!1;const i=dr(s,e.a,e.b),o=lr(e.a,e.b),a=Number(t.length)/2;return i>=-tr&&i<=1+tr&&pr(s,e.a,e.b)<=.02*I&&Z(e.a,e.b,Number(t.angle))&&Number.isFinite(a)&&a>=0&&i*o-a>=-tr&&i*o+a<=o+tr},o="wall"===t.host?.kind?e.find(e=>e.id===t.host.id):null,a=o&&i(o)?[o]:e.filter(i);if(1!==a.length)return null;const n=a[0];return{kind:"wall",id:n.id,t:Math.max(0,Math.min(1,dr(s,n.a,n.b)))}},xr=(t,e)=>{const s=[Number(t.x),Number(t.y)];if(!s.every(Number.isFinite))return null;const i=Number(t.length)/2,o=e.filter(e=>{if(!(Number(e.cm)>0))return!1;const o=dr(s,e.a,e.b),a=lr(e.a,e.b);return o>=-tr&&o<=1+tr&&pr(s,e.a,e.b)<=.02*I&&Z(e.a,e.b,Number(t.angle))&&Number.isFinite(i)&&i>=0&&o*a-i>=-tr&&o*a+i<=a+tr}),a=(e=>{if(!e.length)return null;const i="wall"===t.host?.kind?e.find(e=>e.id===t.host.id):null;return i||[...e].sort((t,e)=>pr(s,t.a,t.b)-pr(s,e.a,e.b)||Number(e.cm)-Number(t.cm)||(t.ide.id?1:0))[0]})(o);return a?{kind:"wall",id:a.id,t:Math.max(0,Math.min(1,dr(s,a.a,a.b)))}:null},Sr=(t,e,s)=>{const i=yr(t),{atoms:o,rooms:a}=$r(t,i);wr(t,o,i,e,s);const n=o.map(e=>{const s=e.id?i.get(e.id):void 0,o=((t,e,s)=>{if(e.zeroWall)return 0;const i=Array.isArray(t.walls)?t.walls:[],o=K(e.a,e.b,I),a=i.filter(t=>Number(t.cm)>0&&(!(t.key!==o&&!e.parentKeys.has(t.key))||!(!ir(t.a)||!ir(t.b))&&_r(e.a,e.b,t.a,t.b)>=lr(e.a,e.b)-tr)).map(t=>Math.max(1,Math.min(100,Number(t.cm))));if(new Set(a.map(t=>t.toFixed(9))).size>1)throw new er("thickness-conflict",e.key);if(a.length)return a[0];const n=Q(i,e.a,e.b,I,1);return n>0?n:Number(s?.cm)>0?Number(s.cm):0})(t,e,s);return{...s||{},id:e.id,a:[...e.a],b:[...e.b],cm:o}}),r=new Map(o.map(t=>[t.key,t.id]));for(const t of a)t.wall_ids=t.wall_ids.map(t=>r.get(t));t.rooms=a,t.wall_segments=n,t.walls=n.filter(t=>t.cm>0).map(t=>({key:K(t.a,t.b,I),cm:t.cm,a:[...t.a],b:[...t.b]})),t.walls.length||delete t.walls,delete t.open_spans;for(const e of t.rooms)delete e.open_to;return((t,e)=>{const s=new Set;for(const e of[t.rooms,t.openings,t.decor,t.room_drafts,t.partitions,t.wall_columns,t.wall_segments])for(const t of Array.isArray(e)?e:[])"string"==typeof t?.id&&t.id&&s.add(t.id);for(const i of Array.isArray(t.room_drafts)?t.room_drafts:[]){const o=Array.isArray(i.segments)?i.segments:[];for(let a=0;a{const i=Array.isArray(t.openings)?t.openings:[];for(const t of i){if("partition"===t.host?.kind)continue;if(!t.host&&!s){const s=kr(t,e);s&&(t.host=s);continue}const i=kr(t,e)??(s?xr(t,e):null);if(i)t.host=i;else{if(!s)throw new er("opening-host",t.id);delete t.host}}})(t,n,e),n.reduce((t,e)=>t+(i.has(e.id)?0:1),0)};function Mr(t,e={}){const s=JSON.stringify(t),i=sr(t);U(i);let o=0;const a=Number(i?.model_version||0)<9;for(const t of Array.isArray(i?.spaces)?i.spaces:[]){const s=e.lineageSpaceId===String(t?.id||"")?e.lineageHints:void 0;o+=Sr(t,a,s)}return i.model_version=9,U(i),{config:i,changed:s!==JSON.stringify(i),migratedSegments:o}}const Dr=t=>!!t&&"object"==typeof t&&!Array.isArray(t);function Cr(t,e){const s=(t,e)=>{if(Array.isArray(t)&&Array.isArray(e)){const i=new Map;for(const e of t)Dr(e)&&"string"==typeof e.id&&e.id&&i.set(e.id,e);const o=e.map((e,o)=>{if(Dr(e)&&"string"==typeof e.id&&e.id){const t=i.get(e.id);return t?s(t,e):sr(e)}return o[e,e in t?s(t[e],i):sr(i)]);for(const e of Object.keys(t))delete t[e];for(const[e,s]of i)t[e]=s;return t}return sr(e)};return s(t,e)}const Pr=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),Ir=(t,e,s)=>Math.min(s,Math.max(e,t)),Tr=t=>ut({spaces:[t]})[0],Rr=(t,e,s)=>{const i=s>0?s:1,o=t=>`${(t[0]/i).toFixed(12)},${(t[1]/i).toFixed(12)}`,a=o(t),n=o(e);return a0?n:1,l=Math.max(i*r*.02,1e-9),h=[];for(const e of t||[])for(const t of R(e)||[])h.push([t[0],t[1]]);const c=[];for(const t of s||[])Array.isArray(t)&&t.length>=4&&t.slice(0,4).every(Number.isFinite)&&c.push([t[0],t[1]],[t[2],t[3]]);const d=(t,e)=>e.some(e=>Math.hypot(t[0]-e[0],t[1]-e[1])<=2*l),p=new Map;for(const n of t||[]){if(!n?.id)continue;const _=mt(t,n.id,e,s,i,o,a,r);if(_)for(let t=0;t<_.orig.length;t++){const e=[];for(let s=0;s<_.parent.length;s++)_.parent[s]===t&&e.push(s);for(let t=1;t+10&&u>0&&n===m&&u!==n))continue;const g=_.poly[i],f=_.poly[(i+1)%_.poly.length],b=Math.hypot(f[0]-g[0],f[1]-g[1]);if(!(b>l&&b<.5*a-1e-9*a))continue;if(d(g,c)||d(f,c))continue;if(d(g,h)&&d(f,h))continue;const y=Rr(g,f,r),v=p.get(y);v?v.targets.add(n):p.set(y,{a:[g[0],g[1]],b:[f[0],f[1]],targets:new Set([n])})}}}let _=e.slice();const u=(t,e)=>Math.hypot(t[0]-e[0],t[1]-e[1])<=2*l,m=(t,e)=>{if(!Array.isArray(t.a)||!Array.isArray(t.b)||t.a.length<2||t.b.length<2)return!1;const s=[Number(t.a[0])*r,Number(t.a[1])*r],i=[Number(t.b[0])*r,Number(t.b[1])*r];return!![...s,...i].every(Number.isFinite)&&(u(s,e.a)&&u(i,e.b)||u(s,e.b)&&u(i,e.a))};for(const t of[...p.values()].filter(t=>1===t.targets.size).sort((t,e)=>Rr(t.a,t.b,r).localeCompare(Rr(e.a,e.b,r)))){const e=[...t.targets][0],s=_.filter(e=>m(e,t));if(new Set(s.map(t=>t.cm)).size>1)continue;let o=!1;_=_.map(s=>m(s,t)?(o=!0,s.cm===e?s:{...s,cm:e}):s),o||(_=E(_,t.a,t.b,e,i,r))}return _}const Nr=t=>{const e=[];for(const s of t||[]){const t=R(s);if(t?.length)for(let s=0;s"string"==typeof t?.id?t.id:"").filter(Boolean),c=new Set(h),d=new Map;for(const t of h){const e=Zn("space",t);e.layers>0&&Xn("space",e.root)&&Qn(d,e.root,t)}const p=new Map,_=new Map,u=new Map,m=new Map;for(const t of r){const e="string"==typeof t?.id?t.id:"";if(!e)continue;const s=new Map;for(const i of Array.isArray(t.rooms)?t.rooms:[]){const t="string"==typeof i?.id?i.id:"";if(!t)continue;const o=Zn("room",t);o.layers>0&&Xn("room",o.root)&&Qn(s,o.root,t),_.has(t)||_.set(t,e),u.has(t)||u.set(t,String(i.name||""));const a="string"==typeof i.area?i.area:"";if(a){const s=m.get(a)||[];s.push({spaceId:e,roomId:t}),m.set(a,s)}}p.set(e,s)}const g=new Set(_.keys()),f=new Map,b=new Map;for(const t of l)"string"==typeof t?.id&&t.id&&(!0===t.removed?b.set(t.id,t):f.set(t.id,t));const y={spaceRefsRemapped:0,roomRefsRemapped:0,positionsRemapped:0,markersDetached:0,positionsUnresolved:0,nestedRefsUnresolved:0,deadSpaceIds:[],orphanRoomLabelsRemoved:0,orphanDevicePositionsRemoved:0,orphanGroupPositionsRemoved:0,liveMissingPositionsRemoved:0,removedPositions:[],liveMissingPositions:[],unverifiedPositions:[]},v=new Set,$=t=>c.has(t)?null:(()=>{const e=Zn("space",t).root;if(!Xn("space",e))return null;const s=d.get(e)||[];return 1===s.length?s[0]:null})(),w=(t,e)=>{const s=Zn("room",t).root;if(!Xn("room",s))return null;const i=p.get(e)?.get(s)||[];return 1===i.length?i[0]:null},k=t=>{const e=s.effectiveAreaByMarker?.[t]||"",i=e&&m.get(e)||[];return 1===i.length?i[0]:null},x=(t,e,s)=>{if("string"==typeof t.room_id&&t.room_id&&!g.has(t.room_id)){const i=e?w(t.room_id,e):null;i?(t.room_id=i,y.roomRefsRemapped++):s?(t.room_id=s.roomId,y.roomRefsRemapped++):!0!==t.removed&&delete t.room_id}const i=t.vacuum?.segment_map;if(i&&"object"==typeof i&&!Array.isArray(i))for(const t of Object.keys(i)){const s=i[t];if("string"!=typeof s||!s||g.has(s))continue;const o=e?w(s,e):null;o?(i[t]=o,y.roomRefsRemapped++):y.nestedRefsUnresolved++}};for(const t of l){const e="string"==typeof t?.id?t.id:"";if(!e)continue;const s="string"==typeof t.space?t.space:"",i=s&&!c.has(s),a=o[e],n="string"==typeof a?.s?a.s:"",r=n&&!c.has(n),l=!0===t.removed;let h=s&&c.has(s)?s:null,d=null,p=!1;if(i){const i=$(s);i?(t.space=i,h=i,p=!0,y.spaceRefsRemapped++):l||"virtual"===t.binding?l||(delete t.space,h=null,y.markersDetached++):(d=k(e),d?(t.space=d.spaceId,h=d.spaceId,y.spaceRefsRemapped++):(delete t.space,h=null,y.markersDetached++))}l&&!p||x(t,h,d),r&&p&&n===s&&h&&(o[e]={...a,s:h},y.positionsRemapped++,v.add(e))}for(const t of Object.keys(o)){if(v.has(t))continue;const e=o[t],s="string"==typeof e?.s?e.s:"";if(!s||c.has(s))continue;const i=$(s);if(i){if(t.startsWith("rl_")){const s=t.slice(3),a=w(s,i);if(a){const s=`rl_${a}`;Yn(o,s)||(o[s]={...e,s:i}),delete o[t],y.roomRefsRemapped++,y.positionsRemapped++;continue}}o[t]={...e,s:i},y.positionsRemapped++}}const S=s.ownerRoster,M=!0===S?.authoritative,D=new Set(S?.deviceIds||[]),C=new Set(S?.entityIds||[]),P=new Set(Array.isArray(i.settings?.known_devices)?i.settings.known_devices.filter(t=>"string"==typeof t):[]),I=(t,e="")=>{const s=String(S?.names?.[t]||e||"").trim();return s&&s!==t?s:""},T=(t,e,s,i="",o)=>({id:t,spaceId:e,kind:s,name:I(t,i),...o?{reason:o}:{}}),R=t=>{"room_label"===t?y.orphanRoomLabelsRemoved++:"group"===t?y.orphanGroupPositionsRemoved++:y.orphanDevicePositionsRemoved++},F=new Set;for(const[t,e]of Object.entries(o)){const i="string"==typeof e?.s?e.s:"";if(!i||c.has(i))continue;let a,n;const r=f.get(t),l=b.get(t);if(r)a=T(t,i,"device",String(r.name||"")),n="live";else if(l)a=T(t,i,"device",String(l.name||"")),n="absent";else if(t.startsWith("rl_")){const e=t.slice(3);a=T(t,i,"room_label",u.get(e)||""),n=g.has(e)?"live":"absent"}else if(t.startsWith("lg_")){const e=t.slice(3);a=T(t,i,"group"),n=C.has(e)?"live":M?"absent":"unverified","unverified"===n&&(a.reason="registry_unavailable")}else D.has(t)?(a=T(t,i,"device"),n="live"):P.has(t)?(a=T(t,i,"device"),n=M?"absent":"unverified","unverified"===n&&(a.reason="registry_unavailable")):(a=T(t,i,"unknown","","unknown_owner"),n="unverified");if("absent"!==n){if("live"===n){if(y.liveMissingPositions.push(a),!0===s.removeLiveMissingPositions){delete o[t],y.liveMissingPositionsRemoved++,R(a.kind),y.removedPositions.push(a);continue}}else y.unverifiedPositions.push(a);y.positionsUnresolved++,F.add(i)}else delete o[t],R(a.kind),y.removedPositions.push(a)}return y.deadSpaceIds=[...F].sort((t,e)=>t.localeCompare(e)),{config:i,layout:o,report:y,changed:JSON.stringify(i)!==a||JSON.stringify(o)!==n}}(t,e,s),n=a.config,r=tt(n,a.layout);U(n),et(a.layout);const l=JSON.stringify(t||{}),h=JSON.stringify(e||{}),c=Number.isInteger(Number(n.model_version))?Number(n.model_version):0,d=(t=>{let e=0,s=0,i=0;for(const s of t.markers||[]){if("ripple"===s.display&&(s.display="icon_ripple",e++),Array.isArray(s.controls)){const t="string"==typeof s.binding&&s.binding.startsWith("entity:")?s.binding.slice(7):"",i=t?s.controls.filter(e=>e!==t):s.controls;JSON.stringify(i)!==JSON.stringify(s.controls)&&(s.controls=i.length?i:null,e++)}const t=s.vacuum;t&&Pr(t,"trail")&&(["never","cleaning","always"].includes(t.trail_mode)||(t.trail_mode=!1===t.trail?"never":"cleaning"),delete t.trail,e++)}for(const o of t.spaces||[]){const t=o.settings;"glow"===t?.fill_mode&&("boolean"!=typeof t.glow_enabled&&(t.glow_enabled=!0),t.fill_mode="none",e++,s++);for(const t of o.rooms||[]){const s=t.settings;"glow"===s?.fill_mode&&("boolean"!=typeof s.glow&&(s.glow=!0),delete s.fill_mode,e++,i++)}if(Pr(o,"segments")&&(delete o.segments,e++),Pr(o,"plan_scale")){const t=Number(o.plan_scale);Number.isFinite(t)&&t>=lt&&t<=ht&&(Pr(o,"plan_scale_x")||(o.plan_scale_x=t),Pr(o,"plan_scale_y")||(o.plan_scale_y=t),delete o.plan_scale,e++)}const a=Number(o.cell_cm);Pr(o,"cell_cm")&&(!Number.isFinite(a)||a<.1||a>1e3)&&(o.cell_cm=Number.isFinite(a)&&a>0?Ir(a,.1,1e3):5,e++);const n=Number(o.cell_cm),r=Number.isFinite(n)&&n>0?n:5;for(const t of o.wall_columns||[])if("circle"===t.shape)Pr(t,"angle")&&(delete t.angle,e++);else if("square"===t.shape&&Pr(t,"angle")){const s=((Number(t.angle)||0)%90+90)%90;t.angle!==s&&(t.angle=s,e++)}for(const t of o.decor||[]){if(Pr(t,"width")){const s=Number(t.width);(Pr(t,"width_cm")||Number.isFinite(s))&&(Pr(t,"width_cm")||(t.width_cm=Number(Ir(s/nt*r,.1,100).toFixed(6))),delete t.width,e++)}if("rect"!==t?.kind&&"ellipse"!==t?.kind||!0!==t.fill||(Pr(t,"fill_color")||(t.fill_color=t.color||"#607d8b",e++),Pr(t,"fill_opacity")||(t.fill_opacity=.25,e++)),"text"!==t?.kind)continue;void 0===t.size_cm?(t.size_cm=Number(Ir(ct*dt(t)/nt*r,.1,2e3).toFixed(6)),delete t.scale,delete t.size,e++):(Pr(t,"scale")||Pr(t,"size"))&&(delete t.scale,delete t.size,e++);let s=String(t.text??"");const i=[...s.matchAll(/\{([^{}\r\n]+)\}/g)].some(t=>!!pt(t[1]));if(!(Pr(t,"entity")||Pr(t,"attr")||Pr(t,"unit")))continue;const o=String(t.unit??"").trim(),a="state"===String(t.attr??"").trim().toLowerCase()?null:t.attr,n=i||o?"":_t(t.entity,a);if(i||n){if(n){const e=s.indexOf("{}");s=e>=0?s.slice(0,e)+n+s.slice(e+2):`${s}${s?" ":""}${n}`,t.text=s}delete t.entity,delete t.attr,delete t.unit,e++}}}return{total:e,glowSpaces:s,glowRooms:i}})(n);let p=d.total;const _=(u=n.spaces||[],JSON.parse(JSON.stringify(u)));var u;const m=Sn(n.spaces||[],a.layout);let g=0,f=0,b=0,y="";const v=m.spaces.map(t=>{const e=st(t);g+=e.report.wallsStraightened,f+=e.report.wallsStraightenSkipped;const s=Number(t?.cell_cm)>0?Number(t.cell_cm):5,i=e.report.maxStraightenShift/I*s;return i>b&&(b=i,y=String(t?.id||"")),e.space}),$=g?Sn(v,m.layout):{...m,spaces:v};n.spaces=$.spaces;const w=g?{...m.report,moved:m.report.moved+$.report.moved,coordsCanonicalized:m.report.coordsCanonicalized+$.report.coordsCanonicalized,maxShift:Math.max(m.report.maxShift,$.report.maxShift),maxShiftCm:Math.max(m.report.maxShiftCm,$.report.maxShiftCm),maxSpace:$.report.maxShiftCm>m.report.maxShiftCm?$.report.maxSpace:m.report.maxSpace,rotated:m.report.rotated+$.report.rotated,removedDrafts:m.report.removedDrafts+$.report.removedDrafts}:{...m.report};let k=0,x=0,S=0,M=0,D=0,C=0,P=0,T=0;for(let t=0;t0===Number(t.cm)),walls:e.walls||[]}),a=Tr(e),r=Tr(s);if(!a||!r)continue;const l=Nr(a.rooms),h=Nr(r.rooms),c=.02*nt,d=(e.wall_segments||[]).filter(t=>0===Number(t.cm)).map(t=>({...t,key:K(t.a,t.b,I),cm:0})),p=new Set(d.map(t=>t.key)),u=it(e,a.rooms,ot,c);x+=u.length;for(const t of u){const e=[t[0]/ot,t[1]/ot],s=[t[2]/ot,t[3]/ot],i=K(e,s,I);p.has(i)||(p.add(i),d.push({key:i,a:e,b:s,cm:0}))}const m=d.length,g=at(d,l,h,I,ot),f=(t,e,s)=>{const i=s[0]-e[0],o=s[1]-e[1],a=i*i+o*o;if(!(a>0))return Math.hypot(t[0]-e[0],t[1]-e[1]);const n=Math.max(0,Math.min(1,((t[0]-e[0])*i+(t[1]-e[1])*o)/a));return Math.hypot(t[0]-(e[0]+i*n),t[1]-(e[1]+o*n))},b=g.filter(t=>{if(!Array.isArray(t.a)||!Array.isArray(t.b))return!1;const e=t.a.map(t=>t*ot),s=t.b.map(t=>t*ot);return h.some(([t,i])=>f(e,t,i)<=4*c&&f(s,t,i)<=4*c)});S+=Math.max(0,m-b.length);const y=(s.wall_segments||[]).filter(t=>Number(t.cm)>0);s.wall_segments=[...y,...b.map(({key:t,...e})=>e)];const v=b.map(t=>[t.a[0]*ot,t.a[1]*ot,t.b[0]*ot,t.b[1]*ot]),$=Array.isArray(e.walls)?e.walls.length:0;let w=at(e.walls,l,h,I,ot);w=Fr(r.rooms,w,v,I,Number(s.cell_cm)>0?Number(s.cell_cm):5,nt,ot),w=B(r.rooms,w,v,I,Number(s.cell_cm)>0?Number(s.cell_cm):5,nt,ot),w=rt(w,s.rooms||[],I,1,v.map(t=>[t[0]/ot,t[1]/ot,t[2]/ot,t[3]/ot])),k+=Math.max(0,$-w.length),w.length?s.walls=w:delete s.walls;const R=Rn(s.partitions||[],{pitch:I,geometry:Nn(s)});R.merged&&(M+=R.merged,s.partitions=R.partitions,Fn(s.openings,s.partitions,R.openingMoves,{coordScale:ot,cellCm:Number(s.cell_cm)>0?Number(s.cell_cm):5,gridPitch:nt}));const F=Tr(s);if(F){const t=o(s,F,s.walls||[],v,{pitch:I,cellCm:Number(s.cell_cm)>0?Number(s.cell_cm):5,gridPitch:nt,coordScale:ot});t.partitionsReconciled&&(D+=t.partitionsReconciled,C+=t.openingsRehosted,t.partitions.length?s.partitions=t.partitions:delete s.partitions,t.openings.length?s.openings=t.openings:delete s.openings,t.walls.length?s.walls=t.walls:delete s.walls),t.removedDrafts&&(P+=t.removedDrafts,t.roomDrafts.length?s.room_drafts=t.roomDrafts:delete s.room_drafts)}JSON.stringify({zero:(s.wall_segments||[]).filter(t=>0===Number(t.cm)),walls:s.walls||[]})!==i&&T++}let R=0;c<=9&&(R=function(t){const e=Mr(t);return Cr(t,e.config),{...e,config:t}}(n).migratedSegments);const F=U(n),N=et($.layout),z=JSON.stringify(F)!==l||JSON.stringify(N)!==h;c<9&&z&&(F.model_version=9);const A=JSON.stringify(F)!==l||JSON.stringify(N)!==h,O=Number.isInteger(Number(F.model_version))?Number(F.model_version):c,L=A?w:{...w,moved:0,coordsCanonicalized:0,maxShift:0,maxShiftCm:0,maxSpace:"",rotated:0,removedDrafts:0},E=A?a.report:{...a.report,spaceRefsRemapped:0,roomRefsRemapped:0,positionsRemapped:0,markersDetached:0,orphanRoomLabelsRemoved:0,orphanDevicePositionsRemoved:0,orphanGroupPositionsRemoved:0,liveMissingPositionsRemoved:0};return{config:F,layout:N,report:{...L,modelFrom:c,modelTo:O,migrated:A?p:0,glowSpacesMigrated:A?d.glowSpaces:0,glowRoomsMigrated:A?d.glowRooms:0,canonicalized:A?T:0,wallSegmentsMigrated:A?R:0,legacyZeroWallsMigrated:A?x:0,wallsMerged:A?k:0,spansMerged:A?S:0,partitionsMerged:A?M:0,partitionsReconciled:A?D:0,openingsRehosted:A?C:0,redundantDraftsRemoved:A?P:0,wallsStraightened:A?g:0,wallsStraightenSkipped:f,maxStraightenShiftCm:A?b:0,maxStraightenSpace:A?y:"",latticeCoordinatesCanonicalized:A?r.canonicalized:0,latticeCoordinatesFar:A?r.far:0,latticeMaxShift:A?r.maxShift:0,latticeMaxShiftCm:A?r.maxShiftCm:0,latticeSpaces:A?r.spaces:[],...E},changed:A}}const Ar=(t,e)=>t[0]*e[1]-t[1]*e[0],Or=(t,e)=>t[0]*e[0]+t[1]*e[1],Lr=(t,e)=>[t[0]-e[0],t[1]-e[1]],Er=(t,e,s)=>[t[0]+e[0]*s,t[1]+e[1]*s];function Br(t,e,s){return Math.abs(Ar(t,e))<=s}function Hr(t,e,s){return Math.abs(Ar(Lr(t,e),s))}function Gr(t,e,s,i,o){const a=e[0]-t[0],n=e[1]-t[1],r=Math.hypot(a,n);if(!(r>o&&s>0))return[];const l=[a/r,n/r];return function(t,e,s){const i=e.map(([e,s])=>[Math.max(0,Math.min(e,s)),Math.min(t,Math.max(e,s))]).filter(([t,e])=>e>t+s).sort((t,e)=>t[0]-e[0]||t[1]-e[1]),o=[];for(const t of i){const e=o[o.length-1];!e||t[0]>e[1]+s?o.push([...t]):e[1]=Math.max(e[1],t[1])}const a=[];let n=0;for(const[t,e]of o)t>n+s&&a.push([n,t]),n=Math.max(n,e);return t>n+s&&a.push([n,t]),a}(r,i.flatMap(e=>{const i=e.b[0]-e.a[0],a=e.b[1]-e.a[1],n=Math.hypot(i,a);if(!(n>o&&Br(l,[i/n,a/n],1e-6)))return[];if(Hr(e.a,t,l)>1.1*Math.max(s,o)||Hr(e.b,t,l)>1.1*Math.max(s,o))return[];return[[Or(Lr(e.a,t),l),Or(Lr(e.b,t),l)]]}),o).flatMap(([e,i])=>{const o=bt({a:Er(t,l,e),b:Er(t,l,i),halfDepth:s});return o?[o]:[]})}function Wr(t){const e=t.coordScale??1,s=Math.max(t.epsilon??2e-4*t.gridPitch,1e-9),i=[];for(const s of t.rooms||[]){if(!s?.id)continue;const o=mt(t.rooms,s.id,t.walls,t.openCuts,t.pitch,t.cellCm,t.gridPitch,e);if(!o||o.poly.length<3)continue;const a=gt(o.poly,o.offsets);!a||a.length<3||i.push({roomId:s.id,profile:o,inner:a})}i.sort((t,e)=>t.roomId.localeCompare(e.roomId));const o=(t.roomOpenings||[]).map(t=>{const e=t.angle*Math.PI/180,s=[Math.cos(e),Math.sin(e)],i=Math.max(0,t.length)/2;return{a:Er([t.x,t.y],s,-i),b:Er([t.x,t.y],s,i)}}),a=[];for(const i of ft(t.rooms,t.walls,t.openCuts,t.pitch,t.cellCm,t.gridPitch,e))for(const t of Gr(i.a,i.b,i.depthUnits/2,o,s))a.push({body:t});const n=new Map;for(const e of t.partitionCuts||[]){const t=n.get(e.hostId)||[];t.push(e),n.set(e.hostId,t)}for(const e of t.partitions||[]){const i=j(e.cm,t.cellCm,t.gridPitch)/2;for(const t of Gr(e.a,e.b,i,n.get(e.id)||[],s))a.push({body:t,partitionId:e.id})}return{rooms:i,boundaries:a,epsilon:s}}function jr(t,e,s,i){const o=t.profile.offsets[e];if(!(o>0))return null;const a=function(t,e){const s=t[e],i=t[(e+1)%t.length],o=i[0]-s[0],a=i[1]-s[1],n=Math.hypot(o,a)||1;return function(t){let e=0;for(let s=0;s=0?[-a/n,o/n]:[a/n,-o/n]}(t.profile.poly,e),n=Er(s.center,a,o),r=[];for(let e=0;ei&&Br(s.axis,[l[0]/h,l[1]/h],1e-6)))continue;if(Hr(o,n,s.axis)>i||Hr(a,n,s.axis)>i)continue;const c=Or(Lr(o,n),s.axis),d=Or(Lr(a,n),s.axis);r.push([Math.min(c,d),Math.max(c,d)])}const l=function(t,e){t.sort((t,e)=>t[0]-e[0]||t[1]-e[1]);const s=[];for(const i of t){const t=s[s.length-1];!t||i[0]>t[1]+e?s.push([...i]):t[1]=Math.max(t[1],i[1])}return s}(r,i).find(([t,e])=>t<=i&&e>=-i);if(!l)return null;const[h,c]=l,d=Er(n,s.axis,-s.half),p=Er(n,s.axis,s.half),_=Math.max(0,-s.half-h),u=Math.max(0,c-s.half),m=_>0?Er(n,s.axis,h):d,g=u>0?Er(n,s.axis,c):p,f=Or(a,s.normal)>=0?1:-1,b=(e,i,o)=>({from:e,to:i,label:[(e[0]+i[0])/2,(e[1]+i[1])/2],axis:s.axis,labelNormal:a,distance:o,roomId:t.roomId,roomSide:f,source:"room-face"});return{left:b(d,m,_),right:b(p,g,u)}}function qr(t){const e=Er(t.center,t.axis,-t.half),s=Er(t.center,t.axis,t.half),i=Er(t.center,t.axis,Math.min(-t.half,t.targetLo)),o=Er(t.center,t.axis,Math.max(t.half,t.targetHi)),a=(e,s)=>({from:e,to:s,label:[(e[0]+s[0])/2,(e[1]+s[1])/2],axis:t.axis,labelNormal:t.normal,distance:Math.hypot(s[0]-e[0],s[1]-e[1]),source:"host-end"});return[a(e,i),a(s,o)]}function Ur(t,e,s,i){const o=Lr(s,e),a=Or(o,o);if(!(a>i*i))return Math.hypot(...Lr(t,e))<=i;const n=Or(Lr(t,e),o)/a;if(n<-i||n>1+i)return!1;const r=Er(e,o,Math.max(0,Math.min(1,n)));return Math.hypot(...Lr(t,r))<=i}function Kr(t,e,s){let i=!1;for(let o=0,a=e.length-1;ot[1]!=h>t[1]&&t[0]<(l-n)*(t[1]-r)/(h-r)+n&&(i=!i)}return i}function Vr(t,e,s,i,o,a){const n=Lr(i,s),r=Ar(e,n);if(Math.abs(r)<=a)return null;const l=Lr(s,t),h=Ar(l,n)/r,c=Ar(l,e)/r;return h<-a||h>o+a||c<-a||c>1+a?null:Math.max(0,Math.min(o,h))}function Jr(t,e,s){const i=qr(s),o="partition"===t.host?.kind?t.host.id:void 0;return[{base:i[0],direction:[-s.axis[0],-s.axis[1]]},{base:i[1],direction:s.axis}].map(({base:t,direction:s})=>{const i=t.distance,a=function(t,e,s,i,o,a){let n=null;for(const r of i)if(!o||r.partitionId!==o){if(Kr(t,r.body,a))return 0;for(let i=0;iOr(Lr(t,o),s));return{center:o,axis:s,normal:i,half:Math.max(0,t.renderedLength)/2,targetLo:Math.min(...a),targetHi:Math.max(...a)}}(t),i=e.rooms.flatMap(t=>{const i=function(t,e,s){const{profile:i}=t;let o=null;for(let t=0;t0))continue;const a=i.poly[t],n=i.poly[(t+1)%i.poly.length],r=Lr(n,a),l=Math.hypot(r[0],r[1]);if(!(l>s&&Br(e.axis,[r[0]/l,r[1]/l],1e-6)))continue;if(Hr(a,e.center,e.axis)>s||Hr(n,e.center,e.axis)>s)continue;const h=Or(Lr(a,e.center),e.axis),c=Or(Lr(n,e.center),e.axis),d=Math.min(h,c),p=Math.max(h,c);if(d>-e.half+s||p0?d:p<0?-p:0);(!o||_2)return qr(s);if(i.length){const t=i.map(({room:t,edge:i})=>jr(t,i,s,e.epsilon));if(t.some(t=>!t))return qr(s);const o=t;return[...o.map(t=>t.left),...o.map(t=>t.right)]}return Jr(t,e,s)}const Xr="1.68.1",Zr={badge:"display.badge",icon_ripple:"display.icon_ripple",value:"display.value",static_icon:"display.static_icon"},Qr={badge:"marker.display_hint_badge",icon_ripple:"marker.display_hint_icon_ripple",value:"marker.display_hint_value",static_icon:"marker.display_hint_static_icon"},tl=2e3,el=1e3,sl=t=>{const e=String(t??"").trim().replace(",",".");if(!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(e))return null;const s=Number(e);return Number.isFinite(s)?s:null},il=(t,e,s,i)=>{for(t.delete(e),t.set(e,s);t.size>i;){const e=t.keys().next().value;if(void 0===e)break;t.delete(e)}},ol=1e3,al=2e3,nl=t=>{try{t.target?.setPointerCapture?.(t.pointerId)}catch{}},rl="f111431804c4c42c6e35286f29720d089bc8b77ffc364a6517f7a3510fcb11c8";class ll{constructor(t){this.host=t,this._junctionBaselineCache=new WeakMap,this._undoGeometry=()=>{if(this.host._physicalDrag||this.host._physicalRotate)return void this._cancelPhysicalGesture();if(this.host._decorDraft)return this.host._decorDraft=null,void this.host.requestUpdate();if(this.host._decorMove||this.host._dtDrag||this.host._bdDrag)return void this._cancelDecorGesture();if(this.host._resize.dragging)return void this._rszCancelDrag();const t=this.host._geometryHistory.undo();t&&(this._applyGeometryState(t.before,!0)?this.host._showToast(this.host._t("history.undone",{name:t.name})):this.host._geometryHistory.clear())},this._redoGeometry=()=>{if(this.host._physicalDrag||this.host._physicalRotate)return void this._cancelPhysicalGesture();if(this.host._decorDraft)return this.host._decorDraft=null,void this.host.requestUpdate();if(this.host._decorMove||this.host._dtDrag||this.host._bdDrag)return void this._cancelDecorGesture();if(this.host._resize.dragging)return void this._rszCancelDrag();const t=this.host._geometryHistory.redo();t&&(this._applyGeometryState(t.after,!0)?this.host._showToast(this.host._t("history.redone",{name:t.name})):this.host._geometryHistory.clear())},this._savePhysicalDialog=()=>{const t=this.host._physicalDialog,e=this.host._curSpaceCfg,s=this.host._spaceModel();if(!t||!e||!s)return;const i=sl(t.cm);if(null==i)return void this._showPhysicalRange("column"===t.kind?yt:100,"column"===t.kind?1:0);const o=this.host._imperial?2.54*i:i,a="column"===t.kind?yt:100,n="column"===t.kind?1:0;if(!Number.isFinite(o)||oa)return void this._showPhysicalRange(a,n);if("column"===t.kind){const e=s.wall_columns.find(e=>e.id===t.id);if(!e)return;const i=sl(t.angle||"0");if("circle"!==t.shape&&(null==i||i<0||i>=90))return void this.host._showToast(this.host._t("toast.physical_angle"));const a="circle"===t.shape?{id:t.id,shape:"circle",center:e.center,cm:o}:{id:t.id,shape:"square",center:e.center,cm:o,angle:i};if(s.wall_columns.some(e=>e.id!==t.id&&vt(e,a,.02*this.host._gridPitch)))return void this.host._showToast(this.host._t("toast.column_duplicate"))}const r=this._geometrySnapshot();if("partition"===t.kind){const s=(e.partitions||[]).find(e=>e.id===t.id);if(s&&0===o&&$t(e.openings,{kind:"partition",id:t.id}))return void this.host._showToast(this.host._t("toast.zero_wall_opening_conflict"));s&&(s.cm=o)}else if("column"===t.kind){const s=(e.wall_columns||[]).find(e=>e.id===t.id);s&&(s.cm=wt(o),s.shape="circle"===t.shape?"circle":"square","square"===s.shape?s.angle=sl(t.angle||"0"):delete s.angle)}else{const s=(e.room_drafts||[]).find(e=>e.id===t.id);s?.segments?.[t.segment||0]&&(s.segments[t.segment||0].cm=o)}const l=this._commitPhysicalGeometry(this.host._t("history.physical_edit"),r);this.host._physicalDialog=null,l||this.host.requestUpdate()},this._deletePhysicalSelection=()=>{const t=this.host._physicalSel,e=this.host._curSpaceCfg;if(!t||!e)return;if("draft"===t.kind)return void this._deleteDraftWhole();const s=this._geometrySnapshot();if("partition"===t.kind){const s=(e.openings||[]).filter(e=>"partition"===e.host?.kind&&e.host.id===t.id).sort((t,e)=>(t.host?.t||0)-(e.host?.t||0));if(s.length)return void(this.host._partitionDeleteDialog={id:t.id,openings:s.map(t=>JSON.parse(JSON.stringify(t)))})}const i="partition"===t.kind?"partitions":"column"===t.kind?"wall_columns":"room_drafts";e[i]=(e[i]||[]).filter(e=>e.id!==t.id),e[i].length||delete e[i],this.host._activeDraftId===t.id&&this._cancelPath(),this.host._physicalSel=null,this.host._physicalDialog=null,this._commitPhysicalGeometry(this.host._t("history.physical_delete"),s)},this._confirmPartitionDelete=()=>{const t=this.host._partitionDeleteDialog,e=this.host._curSpaceCfg;if(!t||!e)return;const s=this._geometrySnapshot();e.partitions=(e.partitions||[]).filter(e=>e.id!==t.id),e.partitions.length||delete e.partitions,e.openings=(e.openings||[]).filter(e=>"partition"!==e.host?.kind||e.host.id!==t.id),e.openings.length||delete e.openings,this.host._partitionDeleteDialog=null,this.host._physicalSel=null,this.host._physicalDialog=null,this._commitPhysicalGeometry(this.host._t("history.physical_delete"),s)},this._deleteDraftWhole=()=>{const t="draft"===this.host._physicalDialog?.kind?this.host._physicalDialog.id:"draft"===this.host._physicalSel?.kind?this.host._physicalSel.id:null,e=this.host._curSpaceCfg;if(!t||!e||!confirm(this.host._t("confirm.delete_draft")))return;const s=this._geometrySnapshot();e.room_drafts=(e.room_drafts||[]).filter(e=>e.id!==t),e.room_drafts.length||delete e.room_drafts,this.host._activeDraftId===t&&this._cancelPath(),this.host._physicalSel=null,this.host._physicalDialog=null,this._commitPhysicalGeometry(this.host._t("history.physical_delete"),s)},this._deleteDraftSegment=()=>{const t=this.host._physicalDialog,e=this.host._curSpaceCfg;if(!t||"draft"!==t.kind||!e)return;if(!confirm(this.host._t("confirm.delete_draft_segment")))return;const s=(e.room_drafts||[]).findIndex(e=>e.id===t.id);if(s<0)return;const i=e.room_drafts[s],o=Math.max(0,Math.min(i.segments.length-1,t.segment||0)),a=[],n=i.points.slice(0,o+1),r=i.points.slice(o+1);if(n.length>=2&&a.push({id:i.id,points:n,segments:i.segments.slice(0,o)}),r.length>=2&&a.push({id:a.length?`${i.id}-${Date.now().toString(36)}`:i.id,points:r,segments:i.segments.slice(o+1)}),2===a.length&&e.room_drafts.length>=200)return void this.host._showToast(this.host._t("toast.physical_limit"));const l=this._geometrySnapshot();e.room_drafts.splice(s,1,...a),e.room_drafts.length||delete e.room_drafts,this.host._activeDraftId===i.id&&this._cancelPath(),this.host._physicalDialog=null,this.host._physicalSel=a.length?{kind:"draft",id:a[0].id}:null,this._commitPhysicalGeometry(this.host._t("history.draft_segment_delete"),l)},this._confirmRoomDelete=t=>{const e=this.host._roomDeleteDialog,s=this.host._curSpaceCfg,i=this.host._spaceModel();if(!e||!s||!i)return;const o=i.rooms.find(t=>t.id===e.roomId);if(!o)return void(this.host._roomDeleteDialog=null);const a=this.host._openCuts(),n=nn(z(i.rooms,this.host._spaceWalls,a,this.host._wallKeyPitch,this.host._cellCm,this.host._gridPitch,ol).filter(t=>t.roomId===o.id),i.partitions,this.host._openingsR.map(t=>({...t,x:t.rx,y:t.ry})),.02*this.host._gridPitch),r=new Set(n.materialize.filter(t=>!t.reusePartitionId).map(t=>t.interval.key)).size;if(t&&(s.partitions||[]).length+r>al)return void this.host._showToast(this.host._t("toast.physical_limit"));const l=this._geometrySnapshot(),h=kt(i.rooms,this.host._spaceWalls,a,this.host._wallKeyPitch,this.host._cellCm,this.host._gridPitch,ol);if(t){s.partitions||=[];const t=new Map,e=new Map,i=t=>{const e=(s.wall_segments||[]).find(e=>{const s=[Number(e.a?.[0])*ol,Number(e.a?.[1])*ol],i=[Number(e.b?.[0])*ol,Number(e.b?.[1])*ol];return this._samePt(t.a,s)&&this._samePt(t.b,i)||this._samePt(t.a,i)&&this._samePt(t.b,s)});return"string"==typeof e?.id&&e.id?e.id:null},o=Date.now().toString(36);for(const a of n.materialize){let n=a.reusePartitionId?s.partitions.find(t=>t.id===a.reusePartitionId):e.get(a.interval.key);n||(n={id:i(a.interval)||`partition-room-${o}-${e.size}`,a:[a.interval.a[0]/ol,a.interval.a[1]/ol],b:[a.interval.b[0]/ol,a.interval.b[1]/ol],cm:a.interval.cm},s.partitions.push(n),e.set(a.interval.key,n)),t.set(a.interval.key,n)}for(const e of s.openings||[]){const s=n.openingIntervals.get(e.id),i=s?t.get(s):null,o=this.host._openingsR.find(t=>t.id===e.id);if(!i||!o)continue;const a={a:[i.a[0]*ol,i.a[1]*ol],b:[i.b[0]*ol,i.b[1]*ol]};e.host={kind:"partition",id:i.id,t:rn([o.rx,o.ry],a)}}}else if(n.removeOpeningIds.length){const t=new Set(n.removeOpeningIds);s.openings=(s.openings||[]).filter(e=>!t.has(e.id)),s.openings.length||delete s.openings}s.rooms=s.rooms.filter(t=>t.id!==o.id),this.host._cfgEpoch++;const c=this.host._normalizeWalls(h,this.host._openCuts());c.length?s.walls=c:delete s.walls,this.host._roomDeleteDialog=null,this._commitPhysicalGeometry(this.host._t(t?"history.delete_room_keep_walls":"history.delete_room_with_walls"),l),this.host._regSignature="",this.host._maybeRebuildDevices(),this.host.requestUpdate()},this._rebindPartitionOpening=()=>{const t=this.host._openingDialog;if(!t?.id)return;const e=t.id;this._activateMarkupTool("opening"),"opening"===this.host._tool&&(this.host._openingRebindId=e,this.host._openingPreset={type:t.type,lengthCm:t.lengthCm,flipH:t.flipH,flipV:t.flipV,revision:++this.host._openingPresetRevision},this.host._openingDialog=null,this.host._openingHoverCandidate=null)},this._keepClosedAsPartitions=()=>{if(this.host._wallFaceBatch)return void this._decideWallFace(!1);if(!this.host._contourClosed||this.host._pendingSplit||!this.host._curSpaceCfg)return;const t=this.host._curSpaceCfg,e=this.host._path.slice(0,-1);if((t.partitions||[]).length+e.length>al)return void this.host._showToast(this.host._t("toast.physical_limit"));const s=this._geometrySnapshot(),i=Ra(e.length,[...this.host._draftSegmentCms,this.host._closingWallCm??void 0],this.host._drawWallCm,A);t.partitions||=[];const o=Date.now().toString(36),a=this.host._activeDraftId?(t.room_drafts||[]).find(t=>t.id===this.host._activeDraftId):null,n=this._draftSegmentsForPath(this.host._path,a,i);for(let s=0;st.id!==this.host._activeDraftId),t.room_drafts.length||delete t.room_drafts),this._commitPhysicalGeometry(this.host._t("history.contour_to_partitions"),s),this.host._roomDialog=!1,this.host._path=[],delete this.host._resumeDraftBySpace[this.host._space],this.host._activeDraftId=null,this.host._draftSegmentCms=[],this.host._closingWallCm=null},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"}),e=this.host._spaceDialog;e&&(this.host._spaceDialog={...e,saved:t?.plans||[],savedBusy:!1})}catch(t){const e=this.host._spaceDialog;e&&(this.host._spaceDialog={...e,saved:[],savedBusy:!1}),this.host._showToast(this.host._t("toast.plans_list_failed",{err:this.host._errText(t)}))}}},this._openSettingsDialog=()=>{if(!this.host._norm)return;const t=this.host._glowRadiusCm,e=this.host._imperial?Math.round(t/30.48*10)/10:Math.round(t)/100;this.host._settingsDialog={colors:JSON.parse(JSON.stringify(this.host._fillColors)),glowRadius:e,bgColor:Dt(this.host._settings,{bgColor:null})||null,northDeg:Mt(this.host._settings,{}),bgMode:St(this.host._settings,{}),sunRays:xt(this.host._settings,{}),busy:!1}},this._openAlignDialog=()=>this._previewAlignDialog(!1),this._toggleOptimizeLivePositions=()=>{const t=this.host._alignDialog;t&&!t.busy&&t.report.liveMissingPositions.length&&this._previewAlignDialog(!t.removeLiveMissingPositions)},this._openBackupExport=()=>{this.host._settingsDialog=null,this.host._backupExportDialog={kind:"full",planOnly:!1,busy:!1,error:""}},this._openRulesDialog=()=>{if(!this.host._norm)return;const t=this.host._settings.icon_rules,e=(t&&t.length?t:Ct).map(t=>({...t}));this.host._rulesDialog={rules:e,test:"",busy:!1}},t._editorSecondary=new gn({root:()=>t.renderRoot,requestUpdate:()=>t.requestUpdate(),updateComplete:()=>t.updateComplete,clearTip:()=>{t._tip=null}}),t._resize=new Wo}_help(t){const e=`${t}.aria`,s=d(this.host.hass,this.host._config?.language);return Pt(s,t)&&Pt(s,e)?o``:a}_setMode(t,e=!0){this.host._endTabDrag(),this.host._pendingNavMode=null;const s=this.host._spaceModel();if(!s)return this.host._cancelModeTransition(!1),void(this.host._mode="view");if(this.host._kiosk&&"view"!==t)return;if(this.host._mode===t){if(!e&&"view"===t&&this.host._modeTransitionBusy){const e=this.host._currentModeVisual(t),s=this.host._modeTransitionTargetZoom,i=this.host._modeTransitionTargetCenterX,o=this.host._modeTransitionTargetCenterY;this.host._cancelModeTransition(!1),this.host._commitViewModeAtomic(e,s,i,o)}return}if(this.host._bootSoftCancel(),("plan"===t||"decor"===t)&&!this.host._norm)return void this.host._showToast(this.host._t("toast.markup_needs_server"));if(this.host._wallFaceBatch&&this._roomDialogCancel(),"plan"===this.host._mode&&"draw"===this.host._tool&&!this._finishWallChain())return;this.host._clearTransientHover(!0),this.host._cancelDevicePressFeedback();const i=this.host._mode,o=this.host._effectiveProjection(),a=this.host._currentModeVisual(i),n=this.host._modeTransitionBusy,r=a&&"view"!==i&&"view"!==t?{...a,toolbarContentOpacity:this.host._reducedMotion?1:.35}:a;this.host._cancelModeTransition(!1),this.host._editorSecondary.closeForNavigation(),(this.host._decorMove||this.host._dtDrag||this.host._bdDrag)&&this._cancelDecorGesture();const l=!s.bg&&"view"===t!=("view"===i),h=n&&"view"===i&&"view"!==t?this.host._modeTransitionEditorCamera:null;let c=h?.zoom??(n?this.host._modeTransitionTargetZoom:this.host._zoom),d=h?h.centerX:n?this.host._modeTransitionTargetCenterX:r?.viewport.centerX,p=h?h.centerY:n?this.host._modeTransitionTargetCenterY:r?.viewport.centerY;if("view"===i&&"view"!==t&&!n){const e=this.host._view;if(this.host._viewModeSnap={space:this.host._space,zoom:this.host._zoom,cx:e?this.host._logicalViewCenter(o)?.x:void 0,cy:e?this.host._logicalViewCenter(o)?.y:void 0},"iso"===o){const e=this.host._logicalViewCenter("iso");d=e?.x,p=e?.y,this.host._view=null,this.host._mode=t,this.host._applyView(this.host._zoom,e?.x,e?.y)}l&&(c=1,d=void 0,p=void 0)}if("plan"===i&&this.host._activeDraftId&&(this.host._resumeDraftBySpace[this.host._space]=this.host._activeDraftId),this.host._mode=t,"devices"===i&&"devices"!==t&&(this.host._showHidden=!1,this.host._deviceInbox=null,this.host._deviceInboxReturn=null,this.host._deviceInboxMemo=null),this.host._editorChromeMode="view"===t?i:t,"view"===t){"view"===i||n||(this.host._modeTransitionEditorCamera={zoom:this.host._zoom,centerX:r?.viewport.centerX,centerY:r?.viewport.centerY});const t=this.host._viewModeSnap;if(t&&t.space===this.host._space){c=t.zoom;const e=this.host._labsIso&&"iso"===this.host._viewPreference[this.host._space]?"iso":"flat",s=null!=t.cx&&null!=t.cy&&"iso"===e?It([t.cx,t.cy],0):null;d=s?.[0]??t.cx,p=s?.[1]??t.cy}else t&&(c=this.host._zoomBySpace[this.host._space]||1,d=void 0,p=void 0)}else this.host._modeTransitionEditorCamera={zoom:c,centerX:d,centerY:p};this.host._modeTransitionTargetZoom=c,this.host._modeTransitionTargetCenterX=d,this.host._modeTransitionTargetCenterY=p,("iso"===o||"view"===t&&this.host._labsIso&&"iso"===this.host._viewPreference[this.host._space])&&(this.host._modeTransitionForceAtomic=!0);const _=++this.host._modeTransitionRequest;e?r?(this.host._modeTransitionPreparing=!0,this.host._modeTransitionVisual=r,this.host._prepareModeTransition(_,r,t,c,d,p)):(this.host._modeTransitionPreparing=!1,this.host._modeTransitionVisual=null,this.host._zoom=c,this.host._view=null,requestAnimationFrame(()=>{this.host.isConnected&&_===this.host._modeTransitionRequest&&this.host._mode===t&&(this.host._applyView(c,d,p),"view"===t&&(this.host._viewModeSnap=null,this.host._modeTransitionEditorCamera=null),this.host.requestUpdate())})):"view"===t&&this.host._commitViewModeAtomic(r,c,d,p),this.host._path=[],this._clearPlanSnapHover(),this._clearOpeningPlacement(!0),this.host._tool="draw",this.host._mergeSel=null,this.host._mergeDialog=null,this.host._splitSel=null,this.host._pendingSplit=null,this.host._selId=null,this.host._physicalSel=null,this.host._physicalDialog=null,this.host._physicalDrag=null,this._rszResetController(),this.host._tip=null,this.host._hoverRoom=null,this.host._decorDraft=null,this.host._decorSel=null,this.host._decorMove=null,this.host._backdropDialog=null,this.host._decorTool="select",this.host._bdDrag=null,this.host._dtDrag=null,this.host._dtBox=null,"plan"===t&&(this._primeDrawWallField(),this._resumeLastDraft()),this.host._saveNav()}_primeDrawWallField(){null===this.host._drawWallField&&(this.host._drawWallField=Tt(A,this.host._imperial))}_showPhysicalRange(t=this.host._drawWallMaxCm,e=0){this.host._showToast(this.host._t("toast.physical_range",{min:Tt(e,this.host._imperial),max:Tt(t,this.host._imperial),unit:this.host._t(this.host._imperial?"wallthick.unit_in":"wallthick.unit_cm")}))}_draftSegmentCount(t=this.host._curSpaceCfg){return(t?.room_drafts||[]).reduce((t,e)=>t+(Array.isArray(e.segments)?e.segments.length:0),0)}_mergeSpacePartitions(t,e){const s=t?.partitions||[];if(s.length<2)return 0;const i=Rn(s,{pitch:I,seedIds:e,geometry:Nn(t,{excludeDraftId:this.host._activeDraftId})});return i.merged?(t.partitions=i.partitions,Fn(t.openings,t.partitions,i.openingMoves,{coordScale:ol,cellCm:this.host._cellCm,gridPitch:this.host._gridPitch}),i.merged):0}_finishWallChain(){if("draw"!==this.host._tool||this.host._wallFaceBatch||this.host._roomDialog)return!0;const t=this.host._curSpaceCfg;if(!t||this.host._path.length<2)return this.host._path=[],this.host._activeDraftId=null,this.host._draftSegmentCms=[],this.host._closingWallCm=null,this._clearPlanSnapHover(),!0;const e=Fa(this.host._path,Ra(this.host._path.length-1,this.host._draftSegmentCms,this.host._drawWallCm,A)),s=e.length;if((t.partitions||[]).length+s>al)return this.host._showToast(this.host._t("toast.physical_limit")),!1;const i=this._geometrySnapshot();t.partitions||=[];const o=Date.now().toString(36),a=[],n=this.host._activeDraftId?(t.room_drafts||[]).find(t=>t.id===this.host._activeDraftId):null,r=this._draftSegmentsForPath(this.host._path,n,e.map(t=>t.cm));for(let i=0;it.id!==this.host._activeDraftId),t.room_drafts.length||delete t.room_drafts),this._commitPhysicalGeometry(this.host._t("history.wall_chain_finish"),i),delete this.host._resumeDraftBySpace[this.host._space],this.host._path=[],this.host._activeDraftId=null,this.host._draftSegmentCms=[],this.host._closingWallCm=null,this._clearPlanSnapHover(),!0}_activateMarkupTool(t){t!==this.host._tool&&(this.host._wallFaceBatch&&this._roomDialogCancel(),("draw"!==this.host._tool||this._finishWallChain())&&(this._cancelPath(),"resize"===this.host._tool&&(this.host._resize.dragging?this._rszCancelDrag():this.host._resize.reset()),this.host._tool=t,"draw"===t&&this._resumeLastDraft(),"resize"===t&&this.host._resize.selectRoom(null),"wallthick"===t&&(this.host._wallDialog=null)))}_limitReached(t){const e=this.host._curSpaceCfg;if(!e)return!0;const s="draft"===t?(e.room_drafts||[]).length>=200:"partition"===t?(e.partitions||[]).length>=al:(e.wall_columns||[]).length>=500;return s&&this.host._showToast(this.host._t("toast.physical_limit")),s}_svgPoint(t){const e=this.host.renderRoot.querySelector(".stage").getBoundingClientRect();return this.host._screenToVb(t.clientX-e.left,t.clientY-e.top)}_snap(t){const e=this.host._gridPitch;return[Rt(Ft(t[0],e)),Rt(Ft(t[1],e))]}_snapDrawPoint(t,e=!1){const s=this.host._path[this.host._path.length-1],i=e&&s?Nt(s,t,this.host._gridPitch,Ti):t;return this._snap(i)}_planSnapOpeningCuts(t,e){if(!this.host._openingsR.length)return[];const s=this.host._openingWallIndexFor(t,e).value,i=[];for(const e of this.host._roomWallOpeningInputs(this.host._openingsR,t)){const t=W(s,e);if(!t.negative&&!t.positive)continue;const o=e.angle*Math.PI/180,a=Math.cos(o)*e.length/2,n=Math.sin(o)*e.length/2;i.push([e.x-a,e.y-n,e.x+a,e.y+n])}return i}_planSnapGeometrySnapshot(){const t=this.host._spaceModel();if(!t)return{key:`${this.host._space}|empty`,value:{segments:[],endpoints:[]}};const e=[this.host._space,this.host._cfgEpoch,this.host._activeDraftId||"",t.rooms.length,t.room_drafts.length,t.partitions.length].join("|");if(this.host._planSnapGeometryCache?.key===e)return this.host._planSnapGeometryCache;const s=this.host._openCuts(),i=xa({space:t,activeDraftId:this.host._activeDraftId,roomCuts:this._planSnapOpeningCuts(t,s),partitionCuts:this._partitionOpeningCuts(t),epsilon:2e-4*this.host._gridPitch});return this.host._planSnapGeometryCache={key:e,value:i},this.host._planSnapGeometryCache}_hiddenWallDiagnosticSnapshot(){const t=this.host._spaceModel();if(!t)return{key:`${this.host._space}|hidden-empty`,value:{segments:[],endpoints:[]}};const e=["hidden",this.host._space,this.host._cfgEpoch,this.host._activeDraftId||"",t.rooms.length,t.room_drafts.length,t.partitions.length].join("|");if(this.host._hiddenWallDiagnosticCache?.key===e)return this.host._hiddenWallDiagnosticCache;const s=ka({space:t,activeDraftId:this.host._activeDraftId,epsilon:2e-4*this.host._gridPitch});return this.host._hiddenWallDiagnosticCache={key:e,value:s},this.host._hiddenWallDiagnosticCache}_planStructuralGeometrySnapshot(){const t=this.host._spaceModel();if(!t)return{key:`${this.host._space}|structural-empty`,value:{segments:[],endpoints:[]}};const e=["structural",this.host._space,this.host._cfgEpoch,this.host._activeDraftId||"",t.rooms.length,t.room_drafts.length,t.partitions.length].join("|");if(this.host._planStructuralGeometryCache?.key===e)return this.host._planStructuralGeometryCache;const s=xa({space:t,activeDraftId:this.host._activeDraftId,epsilon:2e-4*this.host._gridPitch});return this.host._planStructuralGeometryCache={key:e,value:s},this.host._planStructuralGeometryCache}_planSnapContextKey(t){const e=this.host._path[0],s=this.host._path[this.host._path.length-1];return[t,this.host._tool,this.host._path.length,e?`${e[0]},${e[1]}`:"",s?`${s[0]},${s[1]}`:""].join("|")}_resolvePlanDrawPoint(t,e){const s=this._planSnapGeometrySnapshot(),i=this.host._path[this.host._path.length-1],o="draw"===this.host._tool&&this.host._path.length>=3?[{point:this.host._path[0],key:"closure:first-point"}]:[],a={tolerance:this._cssPxToRender(12),distinguishTolerance:this._cssPxToRender(8),gridStep:this.host._gridPitch,excludePoints:i?[i]:[],extraEndpoints:o,epsilon:2e-4*this.host._gridPitch},n=e&&i?Ia(s.value,t,{...a,anchor:i}):function(t,e,s){if(!(ma(e)&&s.tolerance>=0))return{kind:"none",candidate:null,conflicts:[]};const i=s.epsilon??ua,o=s.excludePoints||[],a=Ca(Da(t,e,s),s.distinguishTolerance||0);if(a)return a;let n=null;for(const a of t.segments){const t=[a.a[0],a.a[1],a.b[0],a.b[1]],r=C([e[0],e[1]],t);if(r>s.tolerance)continue;const l=Pa(a,e,s.gridStep);!Sa(l,o,i)&&Ma(r,a.key,n)&&(n={kind:"line",point:l,key:a.key,distance:r,segment:a})}return n?{kind:"resolved",candidate:n,conflicts:[]}:{kind:"none",candidate:null,conflicts:[]}}(s.value,t,a),r="resolved"===n.kind?n.candidate:null,l=r?[...r.point]:this._snapDrawPoint(t,e),h=i?zt(i,l):l;return{point:h,candidate:r&&q(h,r.point)?r:null,conflicts:"ambiguous"===n.kind?n.conflicts:[],ambiguous:"ambiguous"===n.kind,contextKey:this._planSnapContextKey(s.key)}}_clearPlanSnapHover(t=!0){this.host._planSnapHover=null,this._syncPlanSnapActiveMarker(null),this._syncPlanSnapConflictMarkers([]),t&&(this.host._cursorPt=null)}_samePt(t,e){return q(t,e)}_dropLegacySegments(){for(const t of this.host._serverCfg?.markers||[])"ripple"===t.display&&(t.display="icon_ripple");for(const t of this.host._serverCfg?.spaces||[]){delete t.segments;const e=new Set,s=t=>"string"==typeof t&&t.length>=1&&t.length<=64&&!e.has(t),i=t=>(e.add(t),!0),o=t=>Array.isArray(t)&&2===t.length&&t.every(t=>Number.isFinite(Number(t))&&Math.abs(Number(t))<=Xt);if(Array.isArray(t.partitions)&&(t.partitions=t.partitions.filter(t=>t&&s(t.id)&&o(t.a)&&o(t.b)&&Math.hypot(t.a[0]-t.b[0],t.a[1]-t.b[1])>1e-9&&i(t.id)).map(t=>({...t,cm:Number.isFinite(Number(t.cm))?Math.max(0,Math.min(100,Number(t.cm))):15})),t.partitions.length||delete t.partitions),Array.isArray(t.wall_columns)&&(t.wall_columns=t.wall_columns.filter(t=>t&&s(t.id)&&o(t.center)&&i(t.id)).map(t=>({id:t.id,shape:"circle"===t.shape?"circle":"square",center:[Number(t.center[0]),Number(t.center[1])],cm:wt(Number(t.cm)||15),..."circle"===t.shape?{}:{angle:At(t.angle)}})),t.wall_columns.length||delete t.wall_columns),Array.isArray(t.room_drafts)&&(t.room_drafts=t.room_drafts.filter(t=>t&&s(t.id)&&Array.isArray(t.points)&&t.points.length>=2&&t.points.every(o)&&i(t.id)).map(t=>{const e=hr(t);return{id:t.id,...e}}).filter(t=>t.points.length>=2),t.room_drafts.length||delete t.room_drafts),Array.isArray(t.walls)){const e=Ot(ut({spaces:[t]}),t.id),s=e?Lt(t,e,ol,.02*nt).contour.map(t=>t.map(t=>t/ol)):V(t.open_spans).map(t=>[t.a[0],t.a[1],t.b[0],t.b[1]]);t.walls=rt(t.walls,t.rooms||[],I,1,s),t.walls.length||delete t.walls}}}_rollbackRejectedPhysicalWrites(t){if(!this.host._serverCfg||!t.length)return!1;let e=!1;for(const[s]of t){const t=this.host._pendingPhysicalWrites.get(s);t&&(e=this._restoreGeometryStateInConfig(this.host._serverCfg,t.before)||e,this.host._pendingPhysicalWrites.delete(s))}return!!e&&(this._clearGeometryGesture(),this.host._geometryHistory.clear(),this.host._cfgEpoch++,this.host._modelCache=null,this.host._wallUnionCache=null,this.host._physicalBodiesCache=null,this.host._frame=null,this.host._regSignature="",this.host._cfgContentFingerprint=Et(this.host._serverCfg),this.host._maybeRebuildDevices(),this.host.requestUpdate(),!0)}async _reloadRejectedPhysicalWrite(){const t=this.host._writeChain;await t.catch(()=>{}),await this.host._reloadConfigOnly(!0)}_writeConfig(){this.host._writesPending++,this.host._writeChain=Bt(this.host._writeChain,async()=>{if(!this.host._serverCfg)return;this._dropLegacySegments();const t=Mi(this.host._serverCfg),e=Et(t),s=[...this.host._pendingPhysicalWrites.entries()];for(const[e,i]of s){const s=t.spaces.find(t=>t.id===e),o=jt(s);if(o===i.fingerprint)continue;let a=!1;try{a=this._checkSpacePhysicalGeometry(t,e).ok}catch{a=!1}if(!a)throw this._restoreGeometryStateLocal(i.before),this.host._pendingPhysicalWrites.delete(e),this.host._geometryHistory.clear(),this.host._showToast(this.host._t("toast.geometry_unsafe")),Object.assign(new Error("unsafe wall geometry"),{code:"geometry-unsafe"});i.fingerprint=o}e!==Et(this.host._serverCfg)&&(this.host._serverCfg=t),this.host._cfgContentFingerprint=e;try{await this.host._sendConfigCandidate(t)}catch(t){if(!this._rollbackRejectedPhysicalWrites(s))throw t;const e=t&&"object"==typeof t?t:Object.assign(new Error(String(t)),{cause:t});throw e.physicalGeometryRolledBack=!0,e}for(const[t,e]of s)this.host._pendingPhysicalWrites.get(t)?.fingerprint===e.fingerprint&&this.host._pendingPhysicalWrites.delete(t)});return this.host._writeChain.finally(()=>{this.host._writesPending--})}_saveConfig(){this.host._cfgEpoch++,this.host._saveConfigDebounced()}_geometrySnapshotFromConfig(t,e){const s=t?.spaces?.find(t=>t.id===e);if(!s)return null;const i=t=>JSON.parse(JSON.stringify(t)),o={};for(const t of["plan_x","plan_y","plan_scale","plan_scale_x","plan_scale_y","plan_angle"])void 0!==s[t]&&(o[t]=s[t]);return{spaceId:e,rooms:i(s.rooms||[]),...Array.isArray(s.openings)?{openings:i(s.openings)}:{},...Array.isArray(s.walls)?{walls:i(s.walls)}:{},...Array.isArray(s.wall_segments)?{wall_segments:i(s.wall_segments)}:{},...Array.isArray(s.open_spans)?{open_spans:i(s.open_spans)}:{},...Array.isArray(s.room_drafts)?{room_drafts:i(s.room_drafts)}:{},...Array.isArray(s.partitions)?{partitions:i(s.partitions)}:{},...Array.isArray(s.wall_columns)?{wall_columns:i(s.wall_columns)}:{},...Array.isArray(s.decor)?{decor:i(s.decor)}:{},plan_transform:o}}_geometrySnapshot(t=this.host._space){return this._geometrySnapshotFromConfig(this.host._serverCfg,t)}_recordGeometry(t,e){if(!e)return;const s=this._geometrySnapshot(e.spaceId);s&&JSON.stringify(e)!==JSON.stringify(s)&&(this.host._geometryHistory.push({name:t,before:e,after:s}),this.host.requestUpdate())}_restoreGeometryStateInConfig(t,e,s=!1){const i=t?.spaces?.find(t=>t.id===e.spaceId);if(!i)return!1;const o=t=>JSON.parse(JSON.stringify(t)),a=new Map((i.rooms||[]).map(t=>[t.id,t]));if(i.rooms=o(e.rooms),s)for(const t of i.rooms){const e=a.get(t.id);!Array.isArray(t.wall_ids)&&Array.isArray(e?.wall_ids)&&e.wall_ids.length===t.poly?.length&&(t.wall_ids=o(e.wall_ids))}const n=(t,e)=>{void 0!==e?i[t]=o(e):s&&"wall_segments"===t||delete i[t]},r=new Map((i.openings||[]).map(t=>[t.id,t])),l=new Map((i.room_drafts||[]).map(t=>[t.id,t]));if(n("openings",e.openings),n("walls",e.walls),n("wall_segments",e.wall_segments),n("open_spans",e.open_spans),n("room_drafts",e.room_drafts),n("partitions",e.partitions),n("wall_columns",e.wall_columns),n("decor",e.decor),s){for(const t of i.openings||[]){const e=r.get(t.id);t.host||"wall"!==e?.host?.kind||(t.host=o(e.host))}for(const t of i.room_drafts||[]){const e=l.get(t.id);for(let s=0;s<(t.segments||[]).length;s++)!t.segments[s]?.id&&e?.segments?.[s]?.id&&(t.segments[s].id=e.segments[s].id)}}for(const t of["plan_x","plan_y","plan_scale","plan_scale_x","plan_scale_y","plan_angle"])delete i[t];return Object.assign(i,o(e.plan_transform||{})),!0}_restoreGeometryStateLocal(t){return!!this._restoreGeometryStateInConfig(this.host._serverCfg,t)&&(this.host._cfgEpoch++,this.host._modelCache=null,this.host._wallUnionCache=null,this.host._physicalBodiesCache=null,this.host._frame=null,this.host.requestUpdate(),!0)}_wallModelBlockerLabel(t){const e=t instanceof er?t.reason:"invalid-room";return this.host._t(`wall_model.reason.${e}`)}_hasLegacyZeroWallFields(t=this.host._serverCfg){return(t?.spaces||[]).some(t=>Array.isArray(t?.open_spans)&&t.open_spans.length>0||(t?.rooms||[]).some(t=>Array.isArray(t?.open_to)&&t.open_to.length>0))}_showWallModelMigrationBlocked(t){const e=this._hasLegacyZeroWallFields()?"toast.zero_wall_migration_blocked":"toast.wall_model_migration_blocked";this.host._showToast(this.host._t(e,{reason:this._wallModelBlockerLabel(t)}))}_limitSegmentsOf(t){const e=[];for(const s of t?.wall_segments||[])s?.a&&s?.b&&e.push({id:String(s.id||""),a:s.a,b:s.b,cm:Number(s.cm)});for(const s of t?.partitions||[])s?.a&&s?.b&&e.push({id:String(s.id||""),a:s.a,b:s.b,cm:Number(s.cm)});for(const s of t?.room_drafts||[]){const t=Array.isArray(s?.points)?s.points:[],i=Array.isArray(s?.segments)?s.segments:[];for(let o=0;o+1t?.id===e);if(!i)return[];const o=Number(i.cell_cm)>0?Number(i.cell_cm):5,a=this._limitSegmentsOf(i),n=[...ta(a),...aa(a,o,I),...ra(a,o,I)];let r=null;try{r=Ht(i.rooms||[],i.walls||[],[],I,o,I,1)}catch{r=null}let l="ok"===s?.status||"degraded-extra"===s?.status?s.roomGeom:null;if(!l&&r&&r.nodes.length)try{const t=Gt(i.rooms||[],i.walls||[],[],[],I,o,I,1);l="ok"===t?.status||"degraded-extra"===t?.status?t.roomGeom:null}catch{l=null}for(const t of i.rooms||[]){const e=String(t?.id||"");if(!e)continue;let s=null;try{s=Wt(i.rooms||[],e,i.walls||[],[],I,o,I,1,l??void 0,r)}catch{s=null}n.push(...la(e,s,o,I))}return n}_junctionLimitLabel(t){const e=t=>String(Math.round(10*t)/10);return this.host._t(`junction.limit_${t.rule}`,{actual:e(t.actual),limit:e(t.limit)})}_junctionLimitsIntroduced(t,e,s,i){let o;const a=(e?.spaces||[]).find(t=>t?.id===s);let n="";try{n=jt(a)}catch{n=""}const r=e&&"object"==typeof e?this._junctionBaselineCache.get(e):void 0;if(r&&n&&r.fingerprint===n&&r.spaceId===s&&(o=r.violations),!o){try{const t=Number(e?.model_version||0)>=9?e:Mr(e).config;o=this._junctionLimitViolations(t,s)}catch{return[]}e&&"object"==typeof e&&n&&this._junctionBaselineCache.set(e,{spaceId:s,fingerprint:n,violations:o})}let l=[];try{l=this._junctionLimitViolations(t,s,i)}catch{return[{rule:"check_failed",subject:s,actual:0,limit:0}]}return function(t,e){const s=new Map;for(const t of e||[])s.set(t.rule,(s.get(t.rule)||0)+1);const i=new Map;for(const e of t||[])i.set(e.rule,[...i.get(e.rule)||[],e]);const o=[];for(const[t,e]of i){const i=e.length-(s.get(t)||0);i>0&&o.push(...e.slice(0,i))}return o}(l,o)}_commitPhysicalGeometry(t,e,s=[]){if(!e||!this.host._serverCfg)return!1;const i=this.host._serverCfg,o=this._geometrySnapshotFromConfig(i,e.spaceId),a=i.spaces.find(t=>t.id===e.spaceId);if(!a||jt(e)===jt(a))return!1;let n,r=!1;try{r=this._checkSpacePhysicalGeometry(i,e.spaceId).ok}catch{r=!1}if(!r)return this._clearGeometryGesture(),this._restoreGeometryStateLocal(e),this.host._showToast(this.host._t("toast.geometry_unsafe")),!1;let l=e;try{if(Number(i.model_version||0)<9){const t=JSON.parse(JSON.stringify(i));if(!this._restoreGeometryStateInConfig(t,e))throw new er("invalid-room",e.spaceId);const s=Mr(t).config;l=this._geometrySnapshotFromConfig(s,e.spaceId)||e;const a=JSON.parse(JSON.stringify(s));if(!o||!this._restoreGeometryStateInConfig(a,o,!0))throw new er("invalid-room",e.spaceId);const r=s.spaces.find(t=>t.id===e.spaceId),h=a.spaces.find(t=>t.id===e.spaceId);if(!Array.isArray(o.wall_segments)&&JSON.stringify(e.rooms)===JSON.stringify(o.rooms))for(const t of h?.wall_segments||[])t.cm=Q(o.walls,t.a,t.b,I,1);const c=((t,e,s)=>{const i=new Map,o=new Map((t?.rooms||[]).map(t=>[t.id,t])),a=new Map((e||[]).map(t=>[t.id,t])),{rooms:n}=$r(s,yr(t));for(const t of n){const e=a.get(t.id),n=o.get(t.id),r=R(e),l=(s.rooms||[]).find(e=>e.id===t.id),h=R(l),c=R(n);if(r&&h&&c&&r.length===h.length&&c.length===n?.wall_ids?.length&&t.poly.length===t.wall_ids.length)for(let e=0;e{const o=[];for(let a=0;atr||pr(r,s,i)>tr)continue;const l=dr(n,s,i),h=dr(r,s,i);l<-tr||h>1+tr||h<=l+tr||o.push({start:l,id:e[a]})}return o.sort((t,e)=>t.start-e.start)},o=s(c,n.wall_ids,r[e],r[(e+1)%r.length]),a=s(t.poly,t.wall_ids,h[e],h[(e+1)%h.length]);if(o.length&&o.length===a.length)for(let t=0;tt.id===e.spaceId),i=this.host._path.length>=2?this.host._path.map(t=>[t[0]/ol,t[1]/ol]):[];i.push(...s.map(t=>[t[0],t[1]])),h=cr(t)<=cr(l,i)&&this._checkSpacePhysicalGeometry(n,e.spaceId).ok}catch{h=!1}if(!h)return this._clearGeometryGesture(),this._restoreGeometryStateLocal(e),this.host._showToast(this.host._t("toast.geometry_unsafe")),!1;const c=JSON.parse(JSON.stringify(i));this._restoreGeometryStateInConfig(c,e);const d=this._junctionLimitsIntroduced(n,c,e.spaceId);if(d.length)return this._clearGeometryGesture(),this._restoreGeometryStateLocal(e),this.host._showToast(this._junctionLimitLabel(d[0])),!1;Cr(i,n),this._recordGeometry(t,l);const p=i.spaces.find(t=>t.id===e.spaceId),_=this.host._pendingPhysicalWrites.get(e.spaceId);return this.host._pendingPhysicalWrites.set(e.spaceId,{before:_?.before||l,fingerprint:jt(p)}),this._saveConfig(),!0}_clearGeometryGesture(){this.host._path=[],this._clearPlanSnapHover(),this._clearOpeningPlacement(!1),this.host._mergeSel=null,this.host._mergeDialog=null,this.host._splitSel=null,this.host._pendingSplit=null,this.host._wallFaceBatch=null,this.host._wallRepairDiagnostic=null,this.host._roomDeleteDialog=null,this.host._wallDialog=null,this.host._physicalDialog=null,this.host._physicalSel=null,this.host._physicalDrag=null,this.host._physicalRotate=null,this.host._activeDraftId=null,this.host._draftSegmentCms=[],this.host._closingWallCm=null,this.host._openingDialog=null,this._rszResetController(),this.host._decorDraft=null,this.host._decorMove=null,this.host._dtDrag=null,this.host._bdDrag=null}_stagePointerCancel(t){if(clearTimeout(this.host._kioskHoldTimer),this.host._swipeStart?.id===t.pointerId&&(this.host._swipeStart=null),this.host._physicalDrag?.pid===t.pointerId||this.host._physicalRotate?.pid===t.pointerId)return void this._cancelPhysicalGesture();if(this.host._decorDraft?.pid===t.pointerId)return this.host._decorDraft=null,void this.host.requestUpdate();if(this.host._decorMove?.pid===t.pointerId||this.host._dtDrag?.pid===t.pointerId||this.host._bdDrag?.pid===t.pointerId)return void this._cancelDecorGesture();"opening"===this.host._tool?(this.host._cursorPt=null,this._clearOpeningPlacement(!1)):"draw"===this.host._tool&&this._clearPlanSnapHover();const e=!!this.host._pinchStart||!!this.host._panStart;this.host._pointers.delete(t.pointerId),this.host._pointers.size<2&&(this.host._pinchStart=null),0===this.host._pointers.size&&(this.host._panStart=null,this.host._panLock=null),e&&0===this.host._pointers.size&&this.host.requestUpdate()}_applyGeometryState(t,e=!1){if(!this.host._canCommitSpace(t.spaceId))return!1;const s=this._geometrySnapshot(t.spaceId);if(!s||!this._restoreGeometryStateLocal(t))return!1;const i=this.host._serverCfg,o=jt(s)!==jt(t);if(o){let o=!1;try{const s=i?this._checkSpacePhysicalGeometry(i,t.spaceId):null;o=!!s?.ok||!(!e||"wall-degraded-extra"!==s?.reason)}catch{o=!1}if(!o)return this._restoreGeometryStateLocal(s),this.host._showToast(this.host._t("toast.geometry_unsafe")),!1}let a;try{a=Mr(i).config}catch(t){return this._restoreGeometryStateLocal(s),this._showWallModelMigrationBlocked(t),!1}if(o){let o=!1;try{const s=a?this._checkSpacePhysicalGeometry(a,t.spaceId):null;o=!!s?.ok||!(!e||"wall-degraded-extra"!==s?.reason)}catch{o=!1}if(!o)return this._restoreGeometryStateLocal(s),this.host._showToast(this.host._t("toast.geometry_unsafe")),!1;Cr(i,a);const n=i?.spaces.find(e=>e.id===t.spaceId),r=this.host._pendingPhysicalWrites.get(t.spaceId);this.host._pendingPhysicalWrites.set(t.spaceId,{before:r?.before||s,fingerprint:jt(n)})}else Cr(i,a);return this._clearGeometryGesture(),this.host._space!==t.spaceId&&(this.host._commitSpace(t.spaceId),this.host._saveNav(),this.host._restoreZoom()),this.host._modelCache=null,this.host._frame=null,this.host._regSignature="",this.host._maybeRebuildDevices(),this._saveConfig(),this.host.requestUpdate(),!0}_roomAt(t){return this.host._spaceModel()?.rooms.find(e=>{const s=R(e);return!!s&&qt(t,s)})}_overlapRoom(t){return this.host._spaceModel()?.rooms.find(e=>{const s=R(e);return!!s&&$(t,s)})}_pointInRoom(t,e){return e.poly?Ut(t,e.poly):null!=e.x&&t[0]>=e.x&&t[0]<=e.x+e.w&&t[1]>=e.y&&t[1]<=e.y+e.h}_contourSelfIntersects(t){if(!go(t))return!0;const e=t.length,s=.001;for(let i=0;i=500)return this.host._showToast(this.host._t("toast.physical_limit")),!1;const t=this.host._curSpaceCfg;return!(!this.host._activeDraftId&&(t?.room_drafts||[]).length>=200||this._draftSegmentCount(t)>=2e3||(t?.partitions||[]).length+this.host._path.length>al)||(this.host._showToast(this.host._t("toast.physical_limit")),!1)}_markupClick(t){if(this.host._vacFit)return;if(!this.host._markup)return;const e=this.host._spaceModel();if(!e)return void this._clearGeometryGesture();if(this.host._suppressClick)return;if(this.host._drag||this.host._rlResize)return;const s=t.composedPath?.()||[];if(s.some(t=>t?.classList?.contains?.("roomlabel")||t?.classList?.contains?.("rlhandle")))return;if(s.some(t=>t?.classList?.contains?.("physical-hit")))return;const i=this._svgPoint(t);if("select"===this.host._tool)return void(this.host._physicalSel=null);if("resize"===this.host._tool){if(this.host._resize.dragging||s.some(t=>t?.classList?.contains?.("rszhandle")))return;const t=[...e.rooms].reverse().find(t=>this._pointInRoom(i,t));return this.host._resize.selectRoom(t?.id||null),void this.host.requestUpdate()}if("delroom"===this.host._tool)return void this._deleteRoomClick(i);if("opening"===this.host._tool)return void this._openingClick(i);if("merge"===this.host._tool)return void this._mergeClick(i);if("wallthick"===this.host._tool)return void this._wallThickClick(i);if("split"===this.host._tool)return void this._splitClick(i);if("column"===this.host._tool)return void this._columnClick(i);this.host._wallRepairDiagnostic=null;const o=this._resolvePlanDrawPoint(i,t.shiftKey);if(o.ambiguous)return this.host._planSnapHover={contextKey:o.contextKey,candidate:null,conflicts:o.conflicts},this._syncPlanSnapActiveMarker(null),this._syncPlanSnapConflictMarkers(o.conflicts),void this.host._showToast(this.host._t("toast.plan_snap_ambiguous"));let a=o.point;if(t.ctrlKey||t.metaKey){if(t.preventDefault(),this.host._path.length<3)return;a=zt(this.host._path[this.host._path.length-1],this.host._path[0])}if(!this.host._path.length){if(!t.shiftKey&&!o.candidate&&this._offerExistingWallFace(i))return;const e=this._draftEndAt(a);return e?(this.host._activeDraftId=e.draft.id,this.host._resumeDraftBySpace[this.host._space]=e.draft.id,this.host._path=e.reverse?[...e.draft.points].reverse().map(t=>[...t]):e.draft.points.map(t=>[...t]),void(this.host._draftSegmentCms=this._adoptDraftCms(this.host._path,e.reverse?[...e.draft.segments].reverse().map(t=>t.cm):e.draft.segments.map(t=>t.cm),e.draft.id))):(this.host._activeDraftId=null,this.host._draftSegmentCms=[],void(this.host._path=[a]))}const n=this.host._path[this.host._path.length-1];if(this._samePt(a,n))return;const r=this._draftEndAt(a,this.host._activeDraftId||void 0);if(r)return void this._mergeDraftEndpoint(r);if(!this._canAppendRoomDraftPoint())return;const l=this.host._drawWallCm;if(null==l)return void this._showPhysicalRange(100);const h=this.host._path.map(t=>[...t]);this.host._path=[...this.host._path,a],this.host._draftSegmentCms=[...this.host._draftSegmentCms,l],this._persistActiveDraftSegment(),this._offerWallFaces(h)}_draftEndAt(t,e){const s=this.host._spaceModel();if(!s)return null;const i=this.host._viewOr(this.host._baseVb()),o=Math.max(.15*this.host._gridPitch,this.host._stageEl?.clientWidth?i.w/this.host._stageEl.clientWidth*12:0);for(const i of s.room_drafts||[]){if(i.id===e)continue;if(i.points.length<2)continue;const s=i.points[0],a=i.points[i.points.length-1];if(Math.hypot(t[0]-a[0],t[1]-a[1])<=o)return{draft:i,reverse:!1};if(Math.hypot(t[0]-s[0],t[1]-s[1])<=o)return{draft:i,reverse:!0}}return null}_mergeDraftEndpoint(t){const e=this.host._curSpaceCfg;if(!e||!this.host._path.length)return;const s=this.host._path.map(t=>[...t]),i=this._wallGraphSources(s),o=Array.isArray(e.room_drafts)?e.room_drafts:[],a=this.host._activeDraftId?o.find(t=>t.id===this.host._activeDraftId):null,n=o.find(e=>e.id===t.draft.id);if(!n)return;const r=t.reverse?t.draft.points.map(t=>[...t]):[...t.draft.points].reverse().map(t=>[...t]),l=t.reverse?(n.segments||[]).map(t=>({...t})):[...n.segments||[]].reverse().map(t=>({...t})),h=this.host._path[this.host._path.length-1],c=this._samePt(h,r[0]),d=c?null:this.host._drawWallCm;if(!c&&null==d)return void this._showPhysicalRange(100);const p=this._draftSegmentsForPath(this.host._path,a,this.host._draftSegmentCms),_=[...this.host._path.map(t=>[...t]),...c?r.slice(1):r],u=[...p,...null==d?[]:[{cm:d}],...l],m=_.length>=4&&this._samePt(_[0],_[_.length-1]),g=_,f=u;if(g.length>500)return void this.host._showToast(this.host._t("toast.physical_limit"));const b=(a?.segments?.length||0)+(n.segments?.length||0);if(this._draftSegmentCount(e)-b+f.length>2e3)return void this.host._showToast(this.host._t("toast.physical_limit"));if(m){const t=g.slice(0,-1);if(this._contourSelfIntersects(t)||k(t)<=1e-6)return void this.host._showToast(this.host._t("toast.contour_cannot_close"));const e=this._overlapRoom(t);if(e)return void this.host._showToast(this.host._t("toast.room_overlap",{name:e.name||""}))}const y=this._geometrySnapshot(),v=this.host._activeDraftId||t.draft.id,$={...a||n,id:v,points:g.map(t=>[t[0]/ol,t[1]/ol]),segments:f};e.room_drafts=o.filter(e=>e.id!==t.draft.id&&(!this.host._activeDraftId||e.id!==this.host._activeDraftId)),e.room_drafts.push($),this.host._activeDraftId=v,this.host._resumeDraftBySpace[this.host._space]=v,this.host._path=g,this.host._draftSegmentCms=this._adoptDraftCms(g,f.map(t=>Number(t.cm)),v),this.host._physicalSel=null,this._commitPhysicalGeometry(this.host._t("history.draft_merge"),y),null!=d&&this._offerWallFaces(s,s.length-1,i)}_adoptDraftCms(t,e,s){const i=Math.max(0,t.length-1),o=Ra(i,e,this.host._drawWallCm,A);return e.length!==i&&console.debug(`[houseplan] draft ${s??"?"}: восстановлено толщин ${i-e.length} (#234)`),o}_draftSegmentsForPath(t,e,s){const i=Array.isArray(e?.points)?e.points.map(t=>[Number(t?.[0])*ol,Number(t?.[1])*ol]):[],o=Array.isArray(e?.segments)?e.segments:[];return Ra(Math.max(0,t.length-1),s,this.host._drawWallCm,A).map((e,s)=>{const a=t[s],n=t[s+1],r=i.findIndex((t,e)=>{if(e+1>=i.length)return!1;const s=i[e+1];return this._samePt(a,t)&&this._samePt(n,s)||this._samePt(a,s)&&this._samePt(n,t)});return{...r>=0?o[r]:{},cm:e}})}_persistActiveDraftSegment(){if(this.host._path.length<2||!this.host._curSpaceCfg)return;const t=this._geometrySnapshot(),e=this.host._curSpaceCfg;e.room_drafts||=[],this.host._activeDraftId||(this.host._activeDraftId="draft-"+Date.now().toString(36)),this.host._resumeDraftBySpace[this.host._space]=this.host._activeDraftId;const s=e.room_drafts.findIndex(t=>t.id===this.host._activeDraftId),i={...s>=0?e.room_drafts[s]:{},id:this.host._activeDraftId,points:this.host._path.map(t=>[t[0]/ol,t[1]/ol]),segments:this._draftSegmentsForPath(this.host._path,s>=0?e.room_drafts[s]:null,this.host._draftSegmentCms)};s>=0?e.room_drafts[s]=i:e.room_drafts.push(i),this._commitPhysicalGeometry(this.host._t("history.draft_segment"),t)}_activeWallSourceKey(t){return`active:${this.host._activeDraftId||"session"}:${t}`}_wallGraphSources(t){const e=this._planStructuralGeometrySnapshot().value.segments.map(t=>({a:t.a,b:t.b,key:`static:${t.key}`}));for(let s=0;s+1t.key===s);if(i>=0){const[t]=this.host._wallFaceGraphCache.splice(i,1);return this.host._wallFaceGraphCache.push(t),t.value}const o=en(t,e);return this.host._wallFaceGraphCache.push({key:s,value:o}),this.host._wallFaceGraphCache.length>4&&this.host._wallFaceGraphCache.shift(),o}_offerWallFaces(t,e=this.host._path.length-2,s){if(this.host._path.length<2||this.host._wallFaceBatch||this.host._roomDialog)return;const i=this.host._spaceModel();if(!i)return;const o=this._activeWallSourceKey(e),a=2e-4*this.host._gridPitch;let n,r;try{const e=s||this._wallGraphSources(t);r=this._wallGraphSources(this.host._path),n=function(t,e,s){const i=new Set(t.faces.map(t=>t.key));return e.faces.filter(t=>!i.has(t.key)&&t.sourceKeys.includes(s))}(this._wallFaceGraph(e,a),this._wallFaceGraph(r,a),o)}catch{return}if(!n.length){const t=un(r,{requiredSourceKey:o,maxDistance:j(2,this.host._cellCm,this.host._gridPitch),gridStep:this.host._gridPitch,epsilon:a});return"ambiguous"===t.kind?(this.host._wallRepairDiagnostic=t.proposals[0]||null,void this.host._showToast(this.host._t(0===this.host._drawWallCm?"toast.zero_wall_ambiguous":"toast.wall_repair_ambiguous"))):void("repair"!==t.kind||this._overlapRoom(t.face.ring)||this._beginWallFaceBatch([{...t.face,repair:t.proposal}]))}for(const t of i.rooms){const e=R(t);if(!t.id||!e)continue;const s=Kt(e,this.host._path,.02*this.host._gridPitch);if(!s)continue;const[i,o]=s,a=k(i)>=k(o)?i:o,r=a===i?o:i,l=[...n].sort((t,e)=>Math.abs(t.area-k(r))-Math.abs(e.area-k(r))||t.key.localeCompare(e.key))[0];if(!l)return;return void this._beginWallFaceBatch([{...l,ring:r.map(t=>[t[0],t[1]]),split:{roomId:t.id,mainPoly:a,newPoly:r},consumeAllActive:!0}])}const l=n.filter(t=>!(t.ring.length<3||this._contourSelfIntersects(t.ring))&&!this._overlapRoom(t.ring));l.length&&this._beginWallFaceBatch(l)}_beginWallFaceBatch(t){this.host._wallRepairDiagnostic=null,this.host._wallFaceBatch={candidates:[...t].sort((t,e)=>t.area-e.area||t.key.localeCompare(e.key)),index:0,decisions:[],activePath:this.host._path.map(t=>[...t]),activeCms:[...this.host._draftSegmentCms],activeDraftId:this.host._activeDraftId},this._clearPlanSnapHover(),this.host._nameSel="",this.host._areaSel="",this._resetRoomDialogFields(),this.host._roomDialog=!0}_offerExistingWallFace(t){if(this.host._path.length||this.host._wallFaceBatch||this.host._roomDialog)return!1;const e=2e-4*this.host._gridPitch;try{let s,i=sn(this._wallFaceGraph(this._wallGraphSources([]),e),t,e);if(!i){const o=un(this._wallGraphSources([]),{point:t,maxDistance:j(2,this.host._cellCm,this.host._gridPitch),gridStep:this.host._gridPitch,epsilon:e});if("ambiguous"===o.kind)return this.host._wallRepairDiagnostic=o.proposals[0]||null,this.host._showToast(this.host._t(0===this.host._drawWallCm?"toast.zero_wall_ambiguous":"toast.wall_repair_ambiguous")),!0;if("repair"===o.kind)i=o.face,s=o.proposal;else if("none"===o.kind){const s=un(this._wallGraphSources([]),{point:t,maxDistance:this._cssPxToRender(12),gridStep:this.host._gridPitch,epsilon:e});if("repair"===s.kind)return this.host._wallRepairDiagnostic=s.proposal,this.host._showToast(this.host._t("toast.wall_repair_too_large")),!0}}return!(!i||i.ring.length<3||this._contourSelfIntersects(i.ring)||this._overlapRoom(i.ring))&&(this._beginWallFaceBatch([{...i,existing:!s,repair:s}]),!0)}catch{return!1}}_columnClick(t){const e=this._snap(t),s=this.host._drawWallCm;if(null==s)return void this._showPhysicalRange(yt,1);if(!this.host._curSpaceCfg||this._limitReached("column"))return;const i=this.host._spaceModel();if(!i)return;const o={id:"column-"+Date.now().toString(36),shape:"square",center:e,cm:wt(s),angle:0},a=(i.wall_columns||[]).find(t=>vt(t,o,.02*this.host._gridPitch));if(a)return clearTimeout(this.host._duplicateColumnTimer),this.host._duplicateColumnId=a.id,this.host._duplicateColumnTimer=window.setTimeout(()=>{this.host._duplicateColumnId=null},900),void this.host._showToast(this.host._t("toast.column_duplicate"));const n=this._geometrySnapshot(),r=this.host._curSpaceCfg;r.wall_columns||=[],r.wall_columns.push({...o,center:[e[0]/ol,e[1]/ol]}),this._commitPhysicalGeometry(this.host._t("history.column_add"),n)}_openPhysicalDialog(t,e,s){const i=this.host._spaceModel();if(i)if("partition"===t){const s=i.partitions.find(t=>t.id===e);s&&(this.host._physicalDialog={kind:t,id:e,cm:Tt(s.cm,this.host._imperial),length:this.host._fmtLen(s.a,s.b)})}else if("column"===t){const s=i.wall_columns.find(t=>t.id===e);s&&(this.host._physicalDialog={kind:t,id:e,cm:Tt(s.cm,this.host._imperial),shape:s.shape,angle:this.host._angleField("square"===s.shape?At(s.angle):0)})}else{const o=i.room_drafts.find(t=>t.id===e),a=Math.max(0,Math.min(o?.segments.length?o.segments.length-1:0,s||0));o?.segments[a]&&(this.host._physicalDialog={kind:t,id:e,segment:a,cm:Tt(o.segments[a].cm,this.host._imperial),length:this.host._fmtLen(o.points[a],o.points[a+1])})}}_physicalDown(t,e,s){const i=this.host._spaceModel();if(!i)return;t.stopPropagation(),nl(t);const o=this._svgPoint(t),a=[];for(const t of[...i.wall_columns].reverse())Vt(o,Jt(t,this.host._cellCm,this.host._gridPitch))&&a.push({kind:"column",id:t.id});for(const t of[...i.partitions].reverse()){const e=Yt(t.a,t.b,t.cm,this.host._cellCm,this.host._gridPitch);e&&Vt(o,e)&&a.push({kind:"partition",id:t.id})}a.some(t=>t.kind===e&&t.id===s)||a.unshift({kind:e,id:s});const n=a.map(t=>`${t.kind}:${t.id}`).sort().join("|"),r=performance.now(),l=this.host._physicalPickCycle,h=a.length>1&&l?.signature===n&&r-l.at>380&&r-l.at<=1200&&Math.hypot(t.clientX-l.x,t.clientY-l.y)<=10?(l.index+1)%a.length:Math.max(0,a.findIndex(t=>t.kind===e&&t.id===s));this.host._physicalPickCycle={signature:n,index:h,x:t.clientX,y:t.clientY,at:r},e=a[h].kind,s=a[h].id;const c="partition"===e?i.partitions.find(t=>t.id===s):i.wall_columns.find(t=>t.id===s);c&&(this.host._physicalSel={kind:e,id:s},this.host._physicalDrag={pid:t.pointerId,kind:e,id:s,start:this._svgPoint(t),startClient:[t.clientX,t.clientY],before:this._geometrySnapshot(),moved:!1,base:JSON.parse(JSON.stringify(c)),delta:[0,0]})}_clampPhysicalDelta(t,e,s){const i="partition"===t?[e.a,e.b]:[e.center],o=i.map(t=>t[0]),a=i.map(t=>t[1]);return[Math.max(-Xt*ol-Math.min(...o),Math.min(Xt*ol-Math.max(...o),s[0])),Math.max(-Xt*ol-Math.min(...a),Math.min(Xt*ol-Math.max(...a),s[1]))]}_physicalMove(t){const e=this.host._physicalDrag;if(!e||e.pid!==t.pointerId)return;t.stopPropagation();const s=this._svgPoint(t),i="partition"===e.kind?e.base.a:e.base.center,o=this._snap([i[0]+s[0]-e.start[0],i[1]+s[1]-e.start[1]]),a=this._clampPhysicalDelta(e.kind,e.base,[o[0]-i[0],o[1]-i[1]]);this.host._physicalDrag={...e,delta:a,moved:e.moved||Math.hypot(t.clientX-e.startClient[0],t.clientY-e.startClient[1])>=5}}_physicalUp(t){const e=this.host._physicalDrag;if(!e||e.pid!==t.pointerId)return;if(t.stopPropagation(),this.host._physicalDrag=null,!e.moved)return void this._registerPhysicalTap(e.kind,e.id);const s=this.host._spaceModel();if(!this.host._curSpaceCfg||!s)return;const i=this.host._curSpaceCfg;if("partition"===e.kind){const t=(i.partitions||[]).find(t=>t.id===e.id),s=e.base;if(t){const o={...s,a:[s.a[0]+e.delta[0],s.a[1]+e.delta[1]],b:[s.b[0]+e.delta[0],s.b[1]+e.delta[1]]};t.a=[o.a[0]/ol,o.a[1]/ol],t.b=[o.b[0]/ol,o.b[1]/ol];for(const t of i.openings||[]){if("partition"!==t.host?.kind||t.host.id!==e.id)continue;const s=F(t,[o],ol,this.host._cellCm,this.host._gridPitch).resolved;s&&Object.assign(t,N(t,s,ol))}}}else{const t=(i.wall_columns||[]).find(t=>t.id===e.id),o=e.base,a={...o,center:[o.center[0]+e.delta[0],o.center[1]+e.delta[1]]};if(s.wall_columns.some(t=>t.id!==e.id&&vt(t,a,.02*this.host._gridPitch)))return void this.host._showToast(this.host._t("toast.column_duplicate"));t&&(t.center=[(o.center[0]+e.delta[0])/ol,(o.center[1]+e.delta[1])/ol])}this._commitPhysicalGeometry(this.host._t("history.physical_move"),e.before)}_registerPhysicalTap(t,e,s){const i=performance.now(),o=this.host._physicalLastTap?.kind===t&&this.host._physicalLastTap.id===e&&this.host._physicalLastTap.segment===s&&i-this.host._physicalLastTap.at<=360;this.host._physicalLastTap={kind:t,id:e,segment:s,at:i},o&&(this.host._physicalLastTap=null,this._openPhysicalDialog(t,e,s))}_cancelPhysicalGesture(){this.host._physicalDrag=null,this.host._physicalRotate=null,this.host.requestUpdate()}_physicalRotateDown(t,e){if("square"!==e.shape||!this.host._spaceModel())return;t.preventDefault(),t.stopPropagation(),nl(t);const s=this._svgPoint(t);this.host._physicalSel={kind:"column",id:e.id},this.host._physicalRotate={pid:t.pointerId,id:e.id,center:[...e.center],startAngle:180*Math.atan2(s[1]-e.center[1],s[0]-e.center[0])/Math.PI,baseAngle:At(e.angle),angle:At(e.angle),before:this._geometrySnapshot(),moved:!1}}_physicalRotateMove(t){const e=this.host._physicalRotate;if(!e||e.pid!==t.pointerId)return;t.preventDefault(),t.stopPropagation();const s=this._svgPoint(t),i=180*Math.atan2(s[1]-e.center[1],s[0]-e.center[0])/Math.PI,o=e.baseAngle+i-e.startAngle,a=At(t.shiftKey?o:5*Math.round(o/5));this.host._physicalRotate={...e,angle:a,moved:e.moved||Math.abs(o-e.baseAngle)>=.5}}_physicalRotateUp(t){const e=this.host._physicalRotate;if(!e||e.pid!==t.pointerId)return;t.preventDefault(),t.stopPropagation(),this.host._physicalRotate=null;const s=this.host._spaceModel();if(!e.moved||!this.host._curSpaceCfg||!s)return;const i=s.wall_columns.find(t=>t.id===e.id);if(!i||"square"!==i.shape)return;const o={...i,angle:e.angle};if(s.wall_columns.some(t=>t.id!==e.id&&vt(t,o,.02*this.host._gridPitch)))return void this.host._showToast(this.host._t("toast.column_duplicate"));const a=this.host._curSpaceCfg.wall_columns?.find(t=>t.id===e.id);a&&(a.angle=e.angle,this._commitPhysicalGeometry(this.host._t("history.physical_edit"),e.before))}_rszRooms(){const t=[],e=this.host._spaceModel();if(!e)return t;for(const s of e.rooms){const e=s.id?R(s):null;e&&t.push({id:s.id,poly:e,wall_ids:Array.isArray(s.wall_ids)?[...s.wall_ids]:void 0})}return s=t,i=Math.max(1e-12,1e-12*this.host._gridPitch),s.map(t=>{if(!Array.isArray(t.wall_ids)||t.wall_ids.length!==t.poly.length)return{...t,poly:t.poly.map(t=>[...t])};const e=t.poly.map(t=>[...t]);let s=!0;for(;s&&e.length>3;){s=!1;for(let t=0;t0){e.splice(t,1),s=!0;break}}}return{...t,poly:e}});var s,i}_rszOpenings(){return this.host._openingsR.map(t=>({id:t.id,x:t.rx,y:t.ry,length:t.rlen,hosted:!!t.host,angle:t.angle,type:t.type}))}_rszObstacles(){const t=this.host._spaceModel();if(!t)return[];const e=[];for(const s of t.partitions||[])e.push({kind:"segment",a:[...s.a],b:[...s.b],half:j(s.cm,this.host._cellCm,this.host._gridPitch)/2});for(const s of t.room_drafts||[])for(let t=0;t+1Array.isArray(t.a)&&Array.isArray(t.b)&&t.a.length>=2&&t.b.length>=2).map(t=>({wall:t,a:[t.a[0]*ol,t.a[1]*this.host._spaceH],b:[t.b[0]*ol,t.b[1]*this.host._spaceH]})),o=Math.abs(t[0]-e[0])<=.05*this.host._gridPitch?"v":Math.abs(t[1]-e[1])<=.05*this.host._gridPitch?"h":null,a=i.filter(s=>!!o&&("h"===o?!(Math.abs(s.a[1]-t[1])>.05*this.host._gridPitch||Math.abs(s.b[1]-t[1])>.05*this.host._gridPitch)&&Math.min(Math.max(t[0],e[0]),Math.max(s.a[0],s.b[0]))-Math.max(Math.min(t[0],e[0]),Math.min(s.a[0],s.b[0]))>.05*this.host._gridPitch:!(Math.abs(s.a[0]-t[0])>.05*this.host._gridPitch||Math.abs(s.b[0]-t[0])>.05*this.host._gridPitch)&&Math.min(Math.max(t[1],e[1]),Math.max(s.a[1],s.b[1]))-Math.max(Math.min(t[1],e[1]),Math.min(s.a[1],s.b[1]))>.05*this.host._gridPitch)),n=new Set(a.map(t=>Number(t.wall.cm)).filter(t=>t>0));return{minDim:this.host._cmToUnits(30),eps:.05*this.host._gridPitch,step:this.host._gridPitch,movingHalf:s>0?j(s,this.host._cellCm,this.host._gridPitch)/2:0,obstacles:this._rszObstacles(),thicknessConflict:n.size>1}}_rszResolution(t,e,s){const i=this.host._resize.snapshotIdentity||s||this._rszSnapshot(),o=`${this.host._space}|${this.host._cellCm}|${this.host._gridPitch}|${i}`,a=`${t}:${e}`;return this.host._resize.resolve(o,a,()=>{const s=[...this.host._resize.rooms||this._rszRooms()],i=s.find(e=>e.id===t),o=i?.poly?.[e]||[0,0],a=i?.poly?.[(e+1)%(i?.poly?.length||1)]||[0,0];return function(t,e,s,i,o){const{eps:a}=o,n=t.find(t=>t.id===s);if(!n||i<0||i>=(n.poly?.length||0)||n.poly.length<4||!go(n.poly))return{enabled:!1,reason:"invalid-geometry"};const r=n.poly[i],l=n.poly[(i+1)%n.poly.length],h=vo(r,l,a);if(!h)return{enabled:!1,reason:"diagonal"};if(!xo(n.poly,i,h,a))return{enabled:!1,reason:"side-angle"};if(o.thicknessConflict)return{enabled:!1,reason:"thickness-conflict"};for(const t of o.obstacles||[])if(Do(t,r,l,o))return{enabled:!1,reason:"duplicate-physical-wall"};const c=po(lo(l,r)),d=[],p=new Set;let _=!1,u=!1;for(const e of t)if(e.id!==s)for(let t=0;t1||d.length>1)return{enabled:!1,reason:"multiple-rooms"};const m={[s]:i},g=[s];if(1===d.length){const t=d[0];if(!go(t.room.poly))return{enabled:!1,reason:"invalid-geometry"};if(!xo(t.room.poly,t.edge,h,a))return{enabled:!1,reason:"side-angle"};g.push(t.room.id),m[t.room.id]=t.edge}const f=[];for(const t of e)if(!(uo([t.x,t.y],r,l)>2*a)){if(t.hosted||t.length>c+2*a)return{enabled:!1,reason:"opening-conflict"};f.push(t.id)}const b=Object.fromEntries(g.map(e=>[e,t.find(t=>t.id===e).poly.length])),y=[];for(const e of g){const s=t.find(t=>t.id===e),i=m[e],o=(i-1+s.poly.length)%s.poly.length,n=(i+1)%s.poly.length,r=Io(t,e,o,1,a),l=Io(t,e,n,0,a);if(!r||!l)return{enabled:!1,reason:"partial-shared"};y.push(r,l)}const v={roomId:s,edge:i,a:[...r],b:[...l],n:mo(n.poly,i),roomIds:g,edgeByRoom:m,topology:b,movingOpeningIds:f,sideOwnership:y};if(!Oo(t,e,v,0,o))return{enabled:!1,reason:"invalid-geometry"};const $=Math.abs(Number(o.step));if(Number.isFinite($)&&$>a){const s=[-$,$];if(!s.some(s=>Oo(t,e,v,s,o))){if(!s.some(s=>Fo(No(t,e,v,s),v,a)))return{enabled:!1,reason:"partial-shared"};const i={...o,obstacles:[]};return s.some(s=>Oo(t,e,v,s,i))?{enabled:!1,reason:"duplicate-physical-wall"}:e.length&&s.some(e=>Oo(t,[],v,e,o))?{enabled:!1,reason:"opening-conflict"}:{enabled:!1,reason:"invalid-geometry"}}}return{enabled:!0,plan:v}}(s,[...this.host._resize.openings||this._rszOpenings()],t,e,this._rszOptsFor(o,a))})}_rszSnapshot(){return JSON.stringify(this._geometrySnapshot()||{spaceId:this.host._space,rooms:[],openings:[],walls:[],open_spans:[]})}_rszResetController(){const t=null!==this.host._resize.preview;this.host._resize.reset(),t&&this.host._cfgEpoch++}_rszProjectPreview(t,e,s,i,o){this.host._rszLimitViolation=null;const a=this.host._serverCfg?.spaces.find(t=>t.id===this.host._space);if(!a||!this.host._serverCfg)return{ok:!1,reason:"missing-context"};const n=JSON.parse(t),r={...a,rooms:n.rooms,openings:n.openings||[]};for(const t of["walls","wall_segments","open_spans","room_drafts","partitions","wall_columns","decor"])void 0!==n[t]?r[t]=n[t]:delete r[t];for(const t of["plan_x","plan_y","plan_scale","plan_scale_x","plan_scale_y","plan_angle"])void 0!==n.plan_transform?.[t]?r[t]=n.plan_transform[t]:delete r[t];const l=this.host._spaceH;for(const[t,s]of Object.entries(e)){const e=r.rooms.find(e=>e.id===t);e&&(e.poly=s.map(t=>[t[0]/ol,t[1]/l]),delete e.x,delete e.y,delete e.w,delete e.h)}for(const[t,e]of Object.entries(s)){const s=(r.openings||[]).find(e=>e.id===t);s&&(s.x=e[0]/ol,s.y=e[1]/l)}const h=[],c=[];for(const t of i){const e=o.find(e=>e.id===t),s=r.rooms.find(e=>e.id===t);if(!e||!s?.poly)continue;const i=s.poly.map(t=>[t[0]*ol,t[1]*l]);if(e.poly.length!==i.length)continue;const a=Array.isArray(e.wall_ids)?e.wall_ids:[];for(let t=0;tt.id===s):null;o&&(o.a=[i[t][0]/ol,i[t][1]/ol],o.b=[i[(t+1)%i.length][0]/ol,i[(t+1)%i.length][1]/ol])}}if(h.length&&Array.isArray(r.walls)&&r.walls.length){const t=Zt(r.walls,h,c,this.host._wallKeyPitch,ol,"fixed-topology");if(t.rejected)return{ok:!1,reason:"wall-metadata"};r.walls=t.walls}const d=[];for(const t of r.rooms||[]){const e=R(t);if(e&&!(e.length<2))for(let t=0;tJSON.stringify([t?.key,t?.cm,t?.a,t?.b]),_=new Map;for(const t of n.walls||[]){const e=p(t);_.set(e,(_.get(e)||0)+1)}const u=[];for(const t of r.walls||[]){const e=p(t),s=_.get(e)||0;s?_.set(e,s-1):u.push(t)}if(Qt(u,d,this.host._wallKeyPitch,ol,n.walls||[]).length)return{ok:!1,reason:"wall-metadata"};const m=this._rszSpaceCandidateGeometry(this.host._space,r);if(!m.ok)return{ok:!1,reason:"physical-geometry"};const g={...this.host._serverCfg,spaces:(this.host._serverCfg?.spaces||[]).map(t=>t?.id===this.host._space?r:t)},f=this._junctionLimitsIntroduced(g,this.host._serverCfg,this.host._space,m.wallGeometry);return f.length?(this.host._rszLimitViolation=f[0],{ok:!1,reason:"junction-limit"}):{ok:!0,value:{preview:{space:this.host._space,sp:r},beforeWalls:n.walls||[],afterWalls:r.walls||[],artifact:m.wallGeometry}}}_rszAcceptPreview(t,e){if(this.host._cfgEpoch++,!t||!e)return;const s=te(e);if(!s)return;const i=`${this.host._space}|${this.host._cfgEpoch}|${t.sp.rooms.length}`;Object.defineProperty(s,"sourceFingerprint",{value:Et([t.sp,this.host._cellCm,this.host._gridPitch]),enumerable:!1});const o={key:i,value:s};il(this.host._wallUnionPool,i,o,8),this.host._wallUnionCache=o}_rszSpaceCandidateGeometry(t,e){if(!this.host._serverCfg)return{ok:!1,wallGeometry:null};const s={...this.host._serverCfg,spaces:this.host._serverCfg.spaces.map(s=>s.id===t?e:s)};let i=null;try{return{ok:this._checkSpacePhysicalGeometry(s,t,t=>{i=t}).ok,wallGeometry:i}}catch{return{ok:!1,wallGeometry:null}}}_rszSpaceCandidateRenderable(t,e){if(!this.host._serverCfg)return!1;try{const s={...this.host._serverCfg,spaces:this.host._serverCfg.spaces.map(s=>s.id===t?e:s)};return this._checkSpacePhysicalGeometry(s,t).ok}catch{return!1}}_rszCandidateRenderable(t){return!!t&&t.space===this.host._space&&this._rszSpaceCandidateRenderable(t.space,t.sp)}_rszEdgeDown(t,e,s){if("resize"!==this.host._tool||this.host._resize.dragging)return;t.stopPropagation(),t.preventDefault();const i=this._rszRooms(),o=this._rszResolution(e,s);if(!o.enabled)return void this.host._showToast(this._rszReasonText(o.reason));nl(t);const a=o.plan,n=this._svgPoint(t),r=`${this.host._space}|${this.host._cfgEpoch}|${i.length}`,l=this.host._wallUnionCache?.key===r?this.host._wallUnionCache.value:null,h=this._rszSnapshot();this.host._resize.begin({pointerId:t.pointerId,start:[n[0],n[1]],roomId:e,plan:a,options:this._rszOptsFor(a.a,a.b),rooms:i,openings:this._rszOpenings(),snapshotIdentity:h,before:JSON.parse(h),wallUnionBefore:l})}_rszReasonText(t){return this.host._t(`resize.disabled.${t}`)}_rszDisabledActivate(t,e){t.stopPropagation(),t.preventDefault(),this.host._showToast(this._rszReasonText(e))}_rszDisabledKey(t,e){"Enter"!==t.key&&" "!==t.key||this._rszDisabledActivate(t,e)}_rszMove(t){if(!this.host._resize.ownsPointer(t.pointerId))return;t.stopPropagation();const e=this._svgPoint(t),s=this.host._resize.move({pointerId:t.pointerId,point:[e[0],e[1]],step:this.host._gridPitch,snap:t=>{const e=this._snap(t);return[e[0],e[1]]},project:(t,e,s,i,o)=>this._rszProjectPreview(t,e,s,i,o),publish:(t,e)=>this._rszAcceptPreview(t,e),measure:(t,e)=>this._rszEdgeLabels(t,e)});if("rejected"===s.kind)return s.notify&&this.host._showToast(this.host._rszLimitViolation?`${this.host._t("resize.limit_stopped")} — `+this._junctionLimitLabel(this.host._rszLimitViolation):this.host._t("resize.preview_failed")),void this.host.requestUpdate();"accepted"===s.kind&&this.host.requestUpdate()}_rszUp(t){if(!this.host._resize.ownsPointer(t.pointerId))return;t.stopPropagation();const e=this.host._resize.finish({pointerId:t.pointerId,currentSnapshotIdentity:this._rszSnapshot(),validatePreview:t=>this._rszCandidateRenderable(t)});if("no-op"===e.kind)return this.host._cfgEpoch++,void this.host.requestUpdate();if("rejected"===e.kind)return this.host._cfgEpoch++,this.host._showToast(this.host._t("resize.commit_failed")),void this.host.requestUpdate();const s=e.preview,i=this.host._serverCfg?.spaces.find(t=>t.id===s.space);i&&(i.rooms=s.sp.rooms,i.openings=s.sp.openings,Array.isArray(s.sp.walls)&&(s.sp.walls.length?i.walls=s.sp.walls:delete i.walls),Array.isArray(s.sp.wall_segments)&&(i.wall_segments=s.sp.wall_segments)),this.host._suppressClick=!0,setTimeout(()=>this.host._suppressClick=!1,0),this._commitPhysicalGeometry(this.host._t("history.resize_room"),e.before),this.host.requestUpdate()}_rszCancelDrag(t){const e=this.host._resize.cancel(this._rszSnapshot(),t);if("no-op"!==e.kind){if(this.host._cfgEpoch++,e.restoreWallUnion){const t=this.host._spaceModel();if(t){const s=`${this.host._space}|${this.host._cfgEpoch}|${t.rooms.length}`,i={key:s,value:e.restoreWallUnion};il(this.host._wallUnionPool,s,i,8),this.host._wallUnionCache=i}}this.host.requestUpdate()}}_rszPointerCancel(t){this.host._resize.ownsPointer(t.pointerId)&&(t.stopPropagation(),this._rszCancelDrag(t.pointerId))}_rszEdgeLabels(t,e,s=this.host._resize.rooms){const i=s,o=[],a=t.polys[e.roomId]||i?.find(t=>t.id===e.roomId).poly;if(!a||!i)return o;const n=a.length,r=this._rszInnerSpanCms(e.roomId,a,t.polys);for(const t of function(t,e){const s=t.length;return[(e-1+s)%s,(e+1)%s]}(a,e.edge)){const e=a[t],s=a[(t+1)%n],i=r?.[t];o.push({kind:"length",x:(e[0]+s[0])/2,y:(e[1]+s[1])/2,text:null==i?this.host._fmtLen(e,s):ee(i,"mi"===this.host.hass?.config?.unit_system?.length),edge:{a:[e[0],e[1]],b:[s[0],s[1]]}})}const l="mi"===this.host.hass?.config?.unit_system?.length,h=e.roomIds,c=this.host._spaceWalls,d=this.host._physicalBodiesR(),p=this.host._baseVb(),_=this.host._view&&this.host._view.w>0&&this.host._view.h>0?this.host._view:{x:p[0],y:p[1],w:p[2],h:p[3]},[u,m]=this.host._lastValidStageSize||[_.w,_.h],g={..._,stageWidth:Math.max(1,u),stageHeight:Math.max(1,m)},f=this.host._spaceModel(),b=this.host._config?.icon_size??2.5,y=f?se(b>8?2.5:b,f,_.w,this.host._kiosk?this.host._kioskScale.icon:1)*g.stageWidth/100:24,v=Math.max(10,.77*y),$=this.host._t("room.settings_short"),w=v*(1.457+.66*Math.max(1,$.length)*.42);for(const s of h){const a=t.polys[s]||i.find(t=>t.id===s).poly,n=c.length&&f&&this.host._innerRoomContour(f,s)||a,r=d.length?ie(oe(n,d))*Math.pow(this.host._cellCm/this.host._gridPitch,2)/1e4:ae(n,this.host._gridPitch,this.host._cellCm),h=ne(r,l),p=qo({poly:a,edge:e.edgeByRoom[s],text:h,view:g,gearCenter:re(a),gearWidthPx:w,gearHeightPx:v});o.push({kind:"area",roomId:s,x:p.anchor[0],y:p.anchor[1],text:h,placement:p})}return o}_rszInnerSpanCms(t,e,s){const i=Object.keys(s).length?Object.entries(s).map(([t,e])=>({id:t,poly:e})):this.host._spaceModel()?.rooms;if(!i?.length)return null;const o=this.host._spaceWalls;if(!o.length)return null;const a=this.host._openCuts(),n=le(i,t,o,a,this.host._wallKeyPitch,this.host._cellCm,this.host._gridPitch,ol);if(!n||n.length!==e.length)return null;const r=this.host._cellCm/this.host._gridPitch;return e.map((t,s)=>he(e,s,n)*r)}_renderResizeMeasurements(){const t=this.host._resize.liveLabels;if(!t?.length)return a;const e=t.filter(t=>"length"===t.kind),s=t.filter(t=>"area"===t.kind);return ce``}_renderResizeLayer(t){const e=Math.max(.013*t.w,5),s=e/2,i=t=>t.toFixed(1),o=`M ${i(-.7*s)} 0 H ${i(.7*s)} M 0 ${i(-.22*s)} V ${i(-s)} M ${i(-.32*s)} ${i(-.6*s)} L 0 ${i(-s)} L ${i(.32*s)} ${i(-.6*s)} M 0 ${i(.22*s)} V ${i(s)} M ${i(-.32*s)} ${i(.6*s)} L 0 ${i(s)} L ${i(.32*s)} ${i(.6*s)}`,a=[],n=this._rszRooms(),r=this.host._resize.snapshotIdentity||this._rszSnapshot();for(const t of n)for(let s=0;sthis._rszEdgeDown(e,t.id,s)} + @pointermove=${t=>this._rszMove(t)} + @pointerup=${t=>this._rszUp(t)} + @pointercancel=${t=>this._rszPointerCancel(t)} + @lostpointercapture=${t=>this._rszPointerCancel(t)} + @click=${_?t=>this._rszDisabledActivate(t,p.reason):null} + @keydown=${_?t=>this._rszDisabledKey(t,p.reason):null}> + ${u} + `),a.push(ce``)}return ce`${a}`}_partitionOpeningCuts(t=this.host._spaceModel(),e=()=>!0){if(!t)return[];const s=this.host._curSpaceCfg?.id===t.id?this.host._curSpaceCfg:null,i=de(s,t,this.host._cellCm,this.host._gridPitch,ol);return pe(i,e)}_decorSnapGeometry(t){const e=this.host._spaceModel();if(!e)return{points:[],segments:[]};const s=t||"",i=this.host._decorSnapCache;if(i&&i.epoch===this.host._cfgEpoch&&i.space===this.host._space&&i.height===this.host._decorH&&i.exclude===s)return i.geometry;const o=[];for(const e of this.host._decorList)if(e.id!==t)if("line"===e.kind){const t=[e.x1*ol,e.y1*this.host._decorH],s=[e.x2*ol,e.y2*this.host._decorH];o.push({points:[t,s,[(t[0]+s[0])/2,(t[1]+s[1])/2]],segments:[{a:t,b:s}]})}else if("text"===e.kind)o.push({points:[[e.x*ol,e.y*this.host._decorH]],segments:[]});else{const t=this.host._decorBoxOf(e);t&&o.push(_e(t))}for(const t of e.rooms){const e=R(t);e?.length&&o.push({points:e.flatMap((t,s)=>{const i=e[(s+1)%e.length];return[t,[(t[0]+i[0])/2,(t[1]+i[1])/2]]}),segments:e.map((t,s)=>({a:t,b:e[(s+1)%e.length]}))})}const a=ue(o);return this.host._decorSnapCache={epoch:this.host._cfgEpoch,space:this.host._space,height:this.host._decorH,exclude:s,geometry:a},a}_decorSnap(t,e="mouse",s){const i=this.host._stageEl,o=this.host._viewOr(this.host._baseVb()),a="touch"===e||"pen"===e?14:8,n=i?o.w/Math.max(1,i.clientWidth)*a:this.host._gridPitch;return me(t,this._decorSnapGeometry(s),n,t=>this._snap(t)).point}_replaceDecor(t,e){const s=this.host._curSpaceCfg;s&&(s.decor=this.host._decorList.map(s=>s.id===t?{...s,...e}:s),this.host.requestUpdate())}_cancelDecorGesture(){const t=this.host._decorMove?.before||this.host._dtDrag?.before||this.host._bdDrag?.before,e=t&&this.host._serverCfg?.spaces.find(e=>e.id===t.spaceId);if(t&&e){const s=t=>JSON.parse(JSON.stringify(t));void 0!==t.decor?e.decor=s(t.decor):delete e.decor;for(const t of["plan_x","plan_y","plan_scale","plan_scale_x","plan_scale_y","plan_angle"])delete e[t];Object.assign(e,s(t.plan_transform||{})),this.host._cfgEpoch++}this.host._decorMove=null,this.host._dtDrag=null,this.host._bdDrag=null,this.host.requestUpdate()}_decorPointerDown(t){const e=this.host._decorTool;if("select"===e||"erase"===e?t.target.closest?.(".dshape"):null)return!0;if("line"===e||"rect"===e||"ellipse"===e){t.preventDefault();const s=this._decorSnap(this._svgPoint(t),t.pointerType);return this.host._decorDraft={kind:e,a:s,b:s,pid:t.pointerId},nl(t),!0}if("text"===e){const e=this._decorSnap(this._svgPoint(t),t.pointerType);return this.host._decorTextDialog={x:fe(e[0]/ol),y:fe(e[1]/this.host._decorH),text:"",color:this.host._decorStyle.color,opacity:this.host._decorStyle.opacity,angle:"0",sizeCm:ge(ct,this.host._cellCm,this.host._gridPitch)},this.host._decorTextSelection={start:0,end:0},!0}if("furniture"===e)return!!this.host._furnPalette&&(t.preventDefault(),this._furnPlace(this._svgPoint(t),t.shiftKey),!0);if("select"===e&&(this.host._decorSel=null),this.host._bdMovable){const e=this.host._bdRect,s=this._svgPoint(t),i=e.x+e.w/2,o=e.y+e.h/2,a=-be(e.angle)*Math.PI/180,n=s[0]-i,r=s[1]-o,l=[i+n*Math.cos(a)-r*Math.sin(a),o+n*Math.sin(a)+r*Math.cos(a)];if(l[0]>=e.x&&l[0]<=e.x+e.w&&l[1]>=e.y&&l[1]<=e.y+e.h)return t.preventDefault(),this._bdStart(t)}return!1}_decorCommitDraft(){const t=this.host._decorDraft;if(this.host._decorDraft=null,!t)return;const e=.5*this.host._gridPitch;if(!ye(t.kind,t.a,t.b,e))return;const s=ol,i=this.host._decorH,o=this.host._decorStyle,a=this._geometrySnapshot(),n="dc"+Date.now().toString(36)+Math.random().toString(36).slice(2,5),r=fe;let l;if("line"===t.kind)l={id:n,kind:"line",x1:r(t.a[0]/s),y1:r(t.a[1]/i),x2:r(t.b[0]/s),y2:r(t.b[1]/i),...ve(o,!1)};else{const e=r(Math.min(t.a[0],t.b[0])/s),a=r(Math.min(t.a[1],t.b[1])/i),h=Math.abs(t.b[0]-t.a[0])/s,c=Math.abs(t.b[1]-t.a[1])/i;l={id:n,kind:t.kind,x:e,y:a,w:h,h:c,...ve(o,!0)}}this.host._curSpaceCfg.decor=[...this.host._decorList,l],this.host._decorSel=n,this._recordGeometry(this.host._t("history.decor_add"),a),this._saveConfig(),this.host.requestUpdate()}_decorShapeDown(t,e){if("decor"!==this.host._mode)return;const s=this.host._decorTool;if("text"===s){if("text"!==e.kind)return;return t.stopPropagation(),t.preventDefault(),void this._decorOpenText(e)}"select"!==s&&"erase"!==s||(t.stopPropagation(),t.preventDefault(),"erase"!==s?(this.host._decorSel=e.id,this.host._decorMove={id:e.id,start:this._svgPoint(t),orig:JSON.parse(JSON.stringify(e)),pid:t.pointerId,moved:!1,before:this._geometrySnapshot()},nl(t)):this.host._decorEraseConfirm={id:e.id,kind:e.kind})}_decorMoveUpdate(t){const e=this.host._decorMove;if("furniture"===e.orig?.kind)return void this._furnMoveUpdate(t);const s=this._svgPoint(t),i=e.orig,o=("line"===i.kind?i.x1:i.x)*ol,a=("line"===i.kind?i.y1:i.y)*this.host._decorH,n=this._decorSnap([o+(s[0]-e.start[0]),a+(s[1]-e.start[1])],t.pointerType,e.id);let r=(n[0]-o)/ol,l=(n[1]-a)/this.host._decorH;const h=e.orig,c="line"===h.kind?Math.min(h.x1,h.x2):h.x,d="line"===h.kind?Math.min(h.y1,h.y2):h.y,p="line"===h.kind?Math.abs(h.x2-h.x1):h.w||0,_="line"===h.kind?Math.abs(h.y2-h.y1):h.h||0,u=Xt;r=Math.max(-u-c,Math.min(u-c-p,r)),l=Math.max(-u-d,Math.min(u-d-_,l)),(r||l)&&(e.moved=!0);this.host._curSpaceCfg.decor=this.host._decorList.map(t=>{if(t.id!==e.id)return t;const s=e.orig;return"line"===t.kind?{...t,x1:s.x1+r,y1:s.y1+l,x2:s.x2+r,y2:s.y2+l}:{...t,x:s.x+r,y:s.y+l}}),this.host.requestUpdate()}_decorShapeDbl(t,e){"decor"===this.host._mode&&"select"===this.host._decorTool&&(t.preventDefault(),t.stopPropagation(),this.host._decorMove=null,this.host._decorSel=e.id,this._openDecorProperties(e))}_openDecorProperties(t){if("decor"!==this.host._mode||"select"!==this.host._decorTool)return;if("text"===t.kind)return void this._decorOpenText(t);if(!["line","rect","ellipse","furniture"].includes(t.kind))return;const e=this.host._decorResolvedStyle(t),s="line"===t.kind?t:null,i=this.host._decorBoxOf(t);this.host._decorShapeDialog={id:t.id,kind:t.kind,color:e.color,opacity:e.opacity,widthCm:e.widthCm,angle:this.host._angleField(be(s?$e([s.x1*ol,s.y1*this.host._decorH],[s.x2*ol,s.y2*this.host._decorH]):t.angle)),...s?{lengthCm:ge(Math.hypot((s.x2-s.x1)*ol,(s.y2-s.y1)*this.host._decorH),this.host._cellCm,this.host._gridPitch),lineStyle:"dashed"===s.line_style?"dashed":"solid"}:{},...i?{sizeWCm:ge(i.w,this.host._cellCm,this.host._gridPitch),sizeHCm:ge(i.h,this.host._cellCm,this.host._gridPitch)}:{},..."furniture"===t.kind?{symbol:t.symbol}:{},..."rect"===t.kind||"ellipse"===t.kind?{fill:e.fill,fillColor:e.fillColor,fillOpacity:e.fillOpacity}:{}}}_decorOpenText(t){if("text"!==t.kind)return;let e=String(t.text??"");const s=[...e.matchAll(/\{([^{}\r\n]+)\}/g)].some(t=>!!pt(t[1])),i=String(t.unit??"").trim(),o="state"===String(t.attr??"").trim().toLowerCase()?null:t.attr,a=s||i?"":_t(t.entity,o),n=!(!String(t.entity??"").trim()||s||a&&!i);if(a&&!n){const t=e.indexOf("{}");e=t>=0?e.slice(0,t)+a+e.slice(t+2):`${e}${e?" ":""}${a}`}this.host._decorTextDialog={id:t.id,x:t.x,y:t.y,text:e,color:l(t.color,this.host._decorStyle.color),opacity:we(t.opacity,this.host._decorStyle.opacity),angle:this.host._angleField(t.angle),sizeCm:this.host._decorTextSizeCm(t),pickerEntity:t.entity||"",preserveLegacy:n||void 0},this.host._decorTextSelection={start:e.length,end:e.length}}_decorRememberTextSelection(t){this.host._decorTextSelection={start:t.selectionStart??t.value.length,end:t.selectionEnd??t.value.length}}_decorInsertLiveVariable(t){const e=this.host._decorTextDialog;if(!e)return;const s=_t(e.pickerEntity,t);if(!s)return;const i=e.text,o=Math.max(0,Math.min(i.length,this.host._decorTextSelection.start)),a=Math.max(o,Math.min(i.length,this.host._decorTextSelection.end));if(i.length-(a-o)+s.length>200)return;const n=i.slice(0,o)+s+i.slice(a),r=o+s.length;this.host._decorTextSelection={start:r,end:r},this.host._decorTextDialog={...e,text:n,preserveLegacy:void 0},this.host.updateComplete.then(()=>{const t=this.host.renderRoot.querySelector("textarea.dtarea");t&&(t.focus(),t.setSelectionRange(r,r))})}_decorSaveText(){const t=this.host._decorTextDialog,e=String(t?.text??"").replace(/\r\n?/g,"\n").trim();if(!t||!e)return void(this.host._decorTextDialog=null);const s=this._geometrySnapshot(),i=this.host._curSpaceCfg,o={color:t.color,opacity:we(t.opacity),size_cm:Number(Math.max(.1,Math.min(tl,t.sizeCm)).toFixed(4)),...be(t.angle)?{angle:be(t.angle)}:{}};if(t.id)i.decor=this.host._decorList.map(s=>{if(s.id!==t.id)return s;if("text"!==s.kind)return s;if(t.preserveLegacy){const{angle:t,size:i,scale:a,...n}=s;return{...n,text:e,...o}}const{entity:i,attr:a,unit:n,...r}=s,{angle:l,size:h,scale:c,...d}=r;return{...d,text:e,...o}});else{const s="dc"+Date.now().toString(36)+Math.random().toString(36).slice(2,5);i.decor=[...this.host._decorList,{id:s,kind:"text",x:t.x,y:t.y,text:e,...o}],this.host._decorSel=s}this.host._decorTextDialog=null,this._recordGeometry(this.host._t(t.id?"history.decor_edit":"history.decor_add"),s),this._saveConfig(),this.host.requestUpdate()}_decorSaveShape(){const t=this.host._decorShapeDialog;if(!t)return;const e=this._geometrySnapshot(),s={color:t.color,opacity:we(t.opacity),widthCm:Math.max(.1,Math.min(100,Number(t.widthCm)||.1)),fill:!!t.fill,fillColor:t.fillColor||t.color,fillOpacity:we(t.fillOpacity,.25)};this.host._curSpaceCfg.decor=this.host._decorList.map(e=>{if(e.id!==t.id)return e;const i="rect"===t.kind||"ellipse"===t.kind,o=ve(s,i);if("line"===e.kind){const s=(e.x1+e.x2)/2*ol,i=(e.y1+e.y2)/2*this.host._decorH,a=ke(Math.max(.1,Number(t.lengthCm)||.1),this.host._cellCm,this.host._gridPitch),n=be(t.angle)*Math.PI/180,r=Math.cos(n)*a/2,l=Math.sin(n)*a/2,h=this._snap([s-r,i-l]),c=this._snap([s+r,i+l]),{width:d,line_style:p,..._}=e;return{..._,...o,x1:fe(h[0]/ol),y1:fe(h[1]/this.host._decorH),x2:fe(c[0]/ol),y2:fe(c[1]/this.host._decorH),..."dashed"===t.lineStyle?{line_style:"dashed"}:{}}}if("rect"===e.kind||"ellipse"===e.kind||"furniture"===e.kind){const s=e.w*ol,i=e.h*this.host._decorH,a=Math.max(this.host._gridPitch,Ft(ke(Number(t.sizeWCm),this.host._cellCm,this.host._gridPitch),this.host._gridPitch)),n=Math.max(this.host._gridPitch,Ft(ke(Number(t.sizeHCm),this.host._cellCm,this.host._gridPitch),this.host._gridPitch)),r=e.x*ol+s/2,l=e.y*this.host._decorH+i/2,h=be(t.angle),c=xe({x:r-a/2,y:l-n/2},h,t=>this._snap(t)),{width:d,angle:p,..._}=e;return{..._,...o,x:fe(c[0]/ol),y:fe(c[1]/this.host._decorH),w:a/ol,h:n/this.host._decorH,..."furniture"===e.kind&&t.symbol?{symbol:t.symbol}:{},...h?{angle:h}:{}}}return e}),this.host._decorStyle={...s,fill:"rect"===t.kind||"ellipse"===t.kind?s.fill:this.host._decorStyle.fill,fillColor:"rect"===t.kind||"ellipse"===t.kind?s.fillColor:this.host._decorStyle.fillColor,fillOpacity:"rect"===t.kind||"ellipse"===t.kind?s.fillOpacity:this.host._decorStyle.fillOpacity},this.host._decorShapeDialog=null,this._recordGeometry(this.host._t("history.decor_edit"),e),this._saveConfig(),this.host.requestUpdate()}_dtPivot(t){return"line"===t.kind?[(t.x1+t.x2)/2*ol,(t.y1+t.y2)/2*this.host._decorH]:"furniture"===t.kind||"rect"===t.kind||"ellipse"===t.kind?[(t.x+t.w/2)*ol,(t.y+t.h/2)*this.host._decorH]:[t.x*ol,t.y*this.host._decorH]}_dtApply(t,e){const s=this.host._curSpaceCfg;s&&(s.decor=this.host._decorList.map(s=>{if(s.id!==t)return s;const i={...s};"text"===s.kind&&void 0!==e.textSizeCm&&(delete i.size,delete i.scale);const o={...i};return void 0!==e.textSizeCm&&(o.size_cm=Number(Math.max(.1,Math.min(tl,e.textSizeCm)).toFixed(4))),void 0!==e.angle&&(e.angle?o.angle=Number(e.angle.toFixed(2)):delete o.angle),o}),this.host._cfgEpoch++,this.host.requestUpdate())}_dtStart(t,e,s,i){const o=this.host._dtSel;if(!o)return;t.stopPropagation(),t.preventDefault();const[a,n]=this._dtPivot(o),r=this._svgPoint(t),l=this.host._decorBoxOf(o);this.host._dtDrag={id:o.id,kind:e,pid:t.pointerId,ax:a,ay:n,r0:Math.hypot(r[0]-a,r[1]-n),a0:180*Math.atan2(r[1]-n,r[0]-a)/Math.PI,textSizeCm0:"text"===o.kind?this.host._decorTextSizeCm(o):1,angle0:"line"===o.kind?0:Number(o.angle)||0,sgx:s?.[0],sgy:s?.[1],orig:l||void 0,origShape:JSON.parse(JSON.stringify(o)),before:this._geometrySnapshot(),lineEnd:i,moved:!1},nl(t)}_dtMove(t){const e=this.host._dtDrag;if(!e)return;const s=this._svgPoint(t);if(void 0!==e.lineEnd&&"line"===e.origShape.kind){const i=this._decorSnap(s,t.pointerType,e.id),o=fe(i[0]/ol),a=fe(i[1]/this.host._decorH),n=e.origShape,r=0===e.lineEnd?n.x1:n.x2,l=0===e.lineEnd?n.y1:n.y2;return(Math.abs(o-r)>1e-9||Math.abs(a-l)>1e-9)&&(e.moved=!0),void this._replaceDecor(e.id,0===e.lineEnd?{x1:o,y1:a}:{x2:o,y2:a})}if("scale"===e.kind&&e.orig){const i=Se(e.orig,e.sgx??1,e.sgy??1,s[0],s[1],!t.shiftKey,this.host._gridPitch,this.host._gridPitch),o=Math.abs(i.x-e.orig.x)>1e-6||Math.abs(i.y-e.orig.y)>1e-6||Math.abs(i.w-e.orig.w)>1e-6||Math.abs(i.h-e.orig.h)>1e-6;if(!o&&!e.moved)return;return e.moved||=o,void this._decorApplyBox(e.id,i)}if("scale"===e.kind){const t=Math.hypot(s[0]-e.ax,s[1]-e.ay);if(e.r0<1e-6)return;const i=Math.max(.1,Math.min(tl,e.textSizeCm0*(t/e.r0))),o=Math.abs(i-e.textSizeCm0)>1e-6;if(!o&&!e.moved)return;return e.moved||=o,void this._dtApply(e.id,{textSizeCm:i})}const i=180*Math.atan2(s[1]-e.ay,s[0]-e.ax)/Math.PI;let o=e.angle0+(i-e.a0);t.shiftKey||(o=5*Math.round(o/5)),o=(o%360+360)%360,o>180&&(o-=360);const a=Math.abs(o-e.angle0)>1e-6;(a||e.moved)&&(e.moved||=a,this._dtApply(e.id,{angle:o}))}_dtUp(){const t=this.host._dtDrag;this.host._dtDrag=null,t?.moved&&(this._recordGeometry(this.host._t("history.decor_transform"),t.before),this._saveConfig()),this.host.requestUpdate()}_dtMeasure(){const t=this.host._dtSel;if(!t)return void(this.host._dtBox&&(this.host._dtBox=null,this.host.requestUpdate()));let e;if("line"===t.kind){const s=t.x1*ol,i=t.y1*this.host._decorH,o=t.x2*ol,a=t.y2*this.host._decorH;e={id:t.id,x:Math.min(s,o),y:Math.min(i,a),w:Math.abs(o-s),h:Math.abs(a-i)}}else if("furniture"===t.kind||"rect"===t.kind||"ellipse"===t.kind)e={id:t.id,x:t.x*ol,y:t.y*this.host._decorH,w:t.w*ol,h:t.h*this.host._decorH};else{const s=this.host.renderRoot.querySelector(`text.dtext[data-id="${t.id}"]`);if(!s||"function"!=typeof s.getBBox)return;let i;try{i=s.getBBox()}catch{return}if(!i||!i.width&&!i.height)return;e={id:t.id,x:i.x,y:i.y,w:i.width,h:i.height}}const s=this.host._dtBox;s&&s.id===e.id&&Math.abs(s.x-e.x)<.01&&Math.abs(s.y-e.y)<.01&&Math.abs(s.w-e.w)<.01&&Math.abs(s.h-e.h)<.01||(this.host._dtBox=e,this.host.requestUpdate())}_deleteDecor(t){if(!this.host._decorList.some(e=>e.id===t))return;const e=this._geometrySnapshot();this.host._curSpaceCfg.decor=this.host._decorList.filter(e=>e.id!==t),this.host._decorSel===t&&(this.host._decorSel=null),this._recordGeometry(this.host._t("history.decor_delete"),e),this._saveConfig(),this.host.requestUpdate()}_decorDeleteSel(){this.host._decorSel&&this._deleteDecor(this.host._decorSel)}_confirmDecorErase(){const t=this.host._decorEraseConfirm;this.host._decorEraseConfirm=null,t&&this._deleteDecor(t.id)}_furnFieldValue(t){return Math.round(100*(this.host._imperial?t/30.48:t/100))/100}_furnFieldToCm(t){return Me(this.host._imperial?30.48*t:100*t)}_furnPick(t){const e=De(t);this.host._furnPalette={symbol:t,w:e.w,h:e.h}}_furnPlace(t,e=!1){const s=this.host._furnPalette,i=this.host._curSpaceCfg;if(!s||!i)return;const o=ol,a=this.host._decorH,n=Ce(Pe(s.w,this.host._cellCm,this.host._gridPitch,o)),r=Ce(Pe(s.h,this.host._cellCm,this.host._gridPitch,o)),l=this._geometrySnapshot(),h=this._decorSnap(t);let c=h[0],d=h[1],p=0;const _=e?null:Ie(c,d,r*a,this.host._furnWalls,this.host._furnWallReach,this.host._gridPitch);_&&(c=_.cx,d=_.cy,p=_.angle);const u="df"+Date.now().toString(36)+Math.random().toString(36).slice(2,5),m={id:u,kind:"furniture",symbol:s.symbol,x:fe(c/o-n/2),y:fe(d/a-r/2),w:n,h:r,...ve(this.host._decorStyle,!1)};p&&(m.angle=Number(p.toFixed(2))),i.decor=[...this.host._decorList,m],this.host._decorSel=u,this.host._decorTool="select",this.host._furnPalette=null,this._recordGeometry(this.host._t("history.decor_add"),l),this._saveConfig(),this.host.requestUpdate()}_furnMoveUpdate(t){const e=this.host._decorMove;if("furniture"!==e.orig.kind)return;const s=e.orig,i=this.host._curSpaceCfg;if(!i)return;const o=ol,a=this.host._decorH,n=this._svgPoint(t),r=(s.x+s.w/2)*o+(n[0]-e.start[0]),l=(s.y+s.h/2)*a+(n[1]-e.start[1]);let h,c,d=Number(s.angle)||0;const p=t.shiftKey?null:Ie(r,l,s.h*a,this.host._furnWalls,this.host._furnWallReach,this.host._gridPitch);if(p)h=p.cx/o-s.w/2,c=p.cy/a-s.h/2,d=p.angle;else{const i=this._decorSnap([r-s.w/2*o,l-s.h/2*a],t.pointerType,e.id);h=i[0]/o,c=i[1]/a}h=fe(h),c=fe(c),(Math.abs(h-s.x)>1e-9||Math.abs(c-s.y)>1e-9||Math.abs(d-(Number(s.angle)||0))>1e-9)&&(e.moved=!0),i.decor=this.host._decorList.map(t=>{if(t.id!==e.id)return t;const s={...t,x:h,y:c};return d?s.angle=Number(d.toFixed(2)):delete s.angle,s}),this.host.requestUpdate()}_decorApplyBox(t,e){const s=this.host._curSpaceCfg;if(!s)return;const i=ol,o=this.host._decorH;s.decor=this.host._decorList.map(s=>{if(s.id!==t)return s;const a=xe(e,s.angle,t=>this._snap(t));return{...s,x:fe(a[0]/i),y:fe(a[1]/o),w:Math.max(this.host._gridPitch/i,Math.min(2*Xt,e.w/i)),h:Math.max(this.host._gridPitch/o,Math.min(2*Xt,e.h/o))}}),this.host._cfgEpoch++,this.host.requestUpdate()}_renderFurnPalette(){const t=this.host._furnPalette,e=this.host._t(this.host._imperial?"gs.unit_ft":"gs.unit_m");return o`
t.stopPropagation()}> +
+ ${this.host._t("furn.title")} + + +
+
+ ${Te.map(e=>o` +
${this.host._t(`furn.group_${e}`)}
+
+ ${Re(e).map(e=>o``)} +
`)} +
+ ${t?o`
+ + this.host._furnPalette={...t,w:this._furnFieldToCm(Number(e.target.value))}} /> + + this.host._furnPalette={...t,h:this._furnFieldToCm(Number(e.target.value))}} /> + ${this.host._t("furn.place_hint")} +
`:o`
${this.host._t("furn.pick_hint")}
`} +
`}_openBackdropDialog(t){if(!this.host._bdMovable||!this.host._bdRect)return;t?.preventDefault(),t?.stopPropagation(),this.host._bdDrag=null;const e=this.host._bdRect;this.host._backdropDialog={widthCm:ge(e.w,this.host._cellCm,this.host._gridPitch),heightCm:ge(e.h,this.host._cellCm,this.host._gridPitch),angle:this.host._angleField(e.angle)}}_saveBackdropDialog(){const t=this.host._backdropDialog,e=this.host._bdBase,s=this.host._bdRect;if(!t||!e||!s)return;const i=be(t.angle),o=this._geometrySnapshot(),a=Math.min(e.w*ht,Math.max(e.w*lt,Ft(ke(t.widthCm,this.host._cellCm,this.host._gridPitch),this.host._gridPitch))),n=Math.min(e.h*ht,Math.max(e.h*lt,Ft(ke(t.heightCm,this.host._cellCm,this.host._gridPitch),this.host._gridPitch))),r=s.x+s.w/2,l=s.y+s.h/2,h=xe({x:r-a/2,y:l-n/2},i,t=>this._snap(t));this._bdApply((h[0]-e.x)/ol,(h[1]-e.y)/ol,a/e.w,n/e.h,i),this.host._backdropDialog=null,this._recordGeometry(this.host._t("history.backdrop_transform"),o),this._saveConfig()}_bdApply(t,e,s,i,o){const a=this.host._curSpaceCfg;if(!a)return;a.plan_x=Number(fe(t).toFixed(6)),a.plan_y=Number(fe(e).toFixed(6)),delete a.plan_scale,a.plan_scale_x=Number(Math.min(ht,Math.max(lt,s)).toFixed(6)),a.plan_scale_y=Number(Math.min(ht,Math.max(lt,i)).toFixed(6));const n=be(o);n?a.plan_angle=Number(n.toFixed(2)):delete a.plan_angle,this.host._cfgEpoch++,this.host.requestUpdate()}_bdStart(t,e,s=!1){const i=this.host._bdBase,o=this.host._bdRect;if(!i||!o)return!1;const a=this._svgPoint(t),n=e?e[0]:0,r=e?e[1]:0,l=n>0?o.x:o.x+o.w,h=r>0?o.y:o.y+o.h;return this.host._bdDrag={kind:s?"rotate":e?"scale":"move",pid:t.pointerId,sx:a[0],sy:a[1],base:i,p0:this.host._bdParams,fx:l,fy:h,sgx:n,sgy:r,rect0:{x:o.x,y:o.y,w:o.w,h:o.h,angle:o.angle},before:this._geometrySnapshot(),moved:!1},nl(t),!0}_bdMove(t){const e=this.host._bdDrag;if(!e)return;const s=this._svgPoint(t),i=e.base;if("move"===e.kind){const t=e.rect0.x,o=e.rect0.y,a=this._snap([t+(s[0]-e.sx),o+(s[1]-e.sy)]),n=Math.abs(a[0]-t)>1e-9||Math.abs(a[1]-o)>1e-9;if(!n&&!e.moved)return;return e.moved||=n,void this._bdApply((a[0]-i.x)/ol,(a[1]-i.y)/ol,e.p0.sx,e.p0.sy,e.p0.angle)}if("rotate"===e.kind){const i=e.rect0.x+e.rect0.w/2,o=e.rect0.y+e.rect0.h/2,a=180*Math.atan2(e.sy-o,e.sx-i)/Math.PI;let n=e.p0.angle+(180*Math.atan2(s[1]-o,s[0]-i)/Math.PI-a);t.shiftKey||(n=5*Math.round(n/5)),n=be(n);const r=Math.abs(n-e.p0.angle)>1e-9;if(!r&&!e.moved)return;return e.moved||=r,void this._bdApply(e.p0.dx,e.p0.dy,e.p0.sx,e.p0.sy,n)}const o=Se(e.rect0,e.sgx,e.sgy,s[0],s[1],!t.shiftKey,this.host._gridPitch,Math.min(i.w,i.h)*lt),a=o.w/Math.max(1e-9,i.w),n=o.h/Math.max(1e-9,i.h),r=Math.abs(a-e.p0.sx)>1e-9||Math.abs(n-e.p0.sy)>1e-9||Math.abs(o.x-e.rect0.x)>1e-9||Math.abs(o.y-e.rect0.y)>1e-9;if(!r&&!e.moved)return;e.moved||=r;const l=xe(o,e.p0.angle,t=>this._snap(t));this._bdApply((l[0]-i.x)/ol,(l[1]-i.y)/ol,a,n,e.p0.angle)}_bdReset(){const t=this.host._curSpaceCfg;if(!t)return;const e=this._geometrySnapshot();delete t.plan_x,delete t.plan_y,delete t.plan_scale,delete t.plan_scale_x,delete t.plan_scale_y,delete t.plan_angle,this.host._bdDrag=null,this._recordGeometry(this.host._t("history.backdrop_transform"),e),this._saveConfig(),this.host._showToast(this.host._t("decor.backdrop_reset_done")),this.host.requestUpdate()}_bdUp(){const t=this.host._bdDrag;this.host._bdDrag=null,t?.moved&&(this._recordGeometry(this.host._t("history.backdrop_transform"),t.before),this._saveConfig()),this.host.requestUpdate()}_renderBackdropFrame(t){const e=this.host._bdRect;if(!this.host._bdActive||!e)return a;const s=.02*Math.max(t.w,t.h),i=s/4,o=e.x+e.w/2,n=e.y+e.h/2,r=be(e.angle),l=2.2*s;return ce` + + + {t.stopPropagation(),t.preventDefault(),this._bdStart(t,void 0,!0)}}> + + ${[[-1,-1,"nwse"],[1,-1,"nesw"],[1,1,"nwse"],[-1,1,"nesw"]].map(([t,o,a])=>{const n=t<0?e.x:e.x+e.w,r=o<0?e.y:e.y+e.h;return ce`{e.stopPropagation(),e.preventDefault(),this._bdStart(e,[t,o])}}>`})} + `}_renderEditorGroupLauncher(t){return this.host._editorSecondary.renderGroupLauncher(t,this.host._editorToolbarGroups,this.host._editorSecondaryCopy)}_runEditorContext(t,e){this.host._editorSecondary.runContext(t,this.host._editorSecondaryContextId,e)}_renderEditorGroupModel(t){return this.host._editorSecondary.renderGroupModel(t,this.host._editorSecondaryContextId,this.host._editorSecondaryCopy)}_renderDrawWallControl(){const t="column"===this.host._tool?1:0;return o``}_renderPlanSecondary(){const t=this.host._editorSecondaryContextId,e=this.host._physicalSel;if(e){if(!("partition"===e.kind?!!this.host._curSpaceCfg.partitions?.some(t=>t.id===e.id):"column"===e.kind?!!this.host._curSpaceCfg.wall_columns?.some(t=>t.id===e.id):!!this.host._curSpaceCfg.room_drafts?.some(t=>t.id===e.id)))return null;const s="partition"===e.kind?this.host._t("markup.partition"):"column"===e.kind?this.host._t("markup.column"):this.host._t("markup.add");return{contextId:t,kind:"selection",ariaLabel:this.host._t("editor.context_actions",{object:s}),visibleLabel:s,content:o` + + `}}const s="draw"===this.host._tool||"column"===this.host._tool,i="column"===this.host._tool?"markup.hint_column":"resize"===this.host._tool?"markup.hint_resize":"wallthick"===this.host._tool?"markup.hint_wallthick":null,n="draw"===this.host._tool?this.host._t(this.host._path.length?"markup.hint_points":"markup.hint_start",this.host._path.length?{n:this.host._path.length}:void 0):"";if(!s&&!i&&!n)return null;const r="draw"===this.host._tool&&this.host._path.length>0;return{contextId:t,kind:r?"operation":"tool",ariaLabel:this.host._t("editor.tool_options",{tool:this.host._t("draw"===this.host._tool?"markup.add":"column"===this.host._tool?"markup.column":"resize"===this.host._tool?"markup.resize":"markup.wallthick")}),content:o` + ${s?this._renderDrawWallControl():a} + ${n?o`${n}`:a} + ${i?o`${this.host._t(i)}`:a} + ${"draw"===this.host._tool&&this.host._path.length?o``:a}`}}_renderDecorSecondary(){const t=this.host._editorSecondaryContextId;if("furniture"===this.host._decorTool)return{contextId:t,kind:"palette",ariaLabel:this.host._t("editor.palette",{tool:this.host._t("decor.furniture")}),launcherId:"furniture",dismissPolicy:this.host._furnPalette?"stay-open-on-canvas":"outside",dismiss:()=>{"furniture"===this.host._decorTool&&(this.host._furnPalette=null,this.host._decorTool="select",this.host.requestUpdate())},content:this._renderFurnPalette()};const e=this.host._dtSel;if(e){const s=this.host._t(`decor.${e.kind}`);return{contextId:t,kind:"selection",ariaLabel:this.host._t("editor.context_actions",{object:s}),visibleLabel:s,content:o` + + `}}const s="line"===this.host._decorTool||"rect"===this.host._decorTool||"ellipse"===this.host._decorTool,i="rect"===this.host._decorTool||"ellipse"===this.host._decorTool,n="backdrop"===this.host._decorTool&&!!this.host._bdRect;return s||n?{contextId:t,kind:"tool",ariaLabel:this.host._t("editor.tool_options",{tool:this.host._t(s?`decor.${this.host._decorTool}`:"decor.backdrop")}),content:o` + ${s?o` + this.host._decorStyle={...this.host._decorStyle,...t.detail}}> + + ${i?o` + this.host._decorStyle={...this.host._decorStyle,fillColor:t.detail.color,fillOpacity:t.detail.opacity}}>`:a} + `:a} + ${n?o`${this.host._t("decor.backdrop_hint")}`:a}`}:null}_withBackdropReset(t){if(!this.host._bdMoved||this.host._bdDrag||"palette"===t?.kind)return t;const e=this.host._editorSecondaryContextId,s=o``;return t?{...t,kind:"selection"===t.kind?"mixed":t.kind,content:o`${t.content}${s}`}:{contextId:e,kind:"tool",ariaLabel:this.host._t("editor.tool_options",{tool:this.host._t("decor.backdrop")}),content:s}}_renderEditorSecondary(){if(!this.host._editing)return a;const t=this.host._editorSecondary.activeGroup(this.host._editorToolbarGroups),e=t?this._renderEditorGroupModel(t):"plan"===this.host._mode?this._renderPlanSecondary():"decor"===this.host._mode?this._withBackdropReset(this._renderDecorSecondary()):null;return this.host._editorSecondary.render(e,this.host._editorSecondaryDialogBlocked)}_renderDecorBar(){const t=[["select","mdi:cursor-default-outline","decor.select"],...this.host._bdRect?[["backdrop","mdi:image-move","decor.backdrop"]]:[],["line","mdi:vector-line","decor.line"],["rect","mdi:rectangle-outline","decor.rect"],["ellipse","mdi:ellipse-outline","decor.ellipse"],["text","mdi:format-text","decor.text"],["furniture","mdi:sofa-outline","decor.furniture"],["erase","mdi:eraser","decor.erase"]],e=this.host._geometryHistory.undoName,s=this.host._geometryHistory.redoName;return o`
+
+ ${t.map(([t,e,s])=>o``)} + ${this.host._editorToolbarGroups.map(t=>this._renderEditorGroupLauncher(t))} + + +
+
+ +
+
`}_renderDecorEraseConfirm(){const t=this.host._decorEraseConfirm,e=this.host._t(`decor.${t.kind}`);return o`this.host._decorEraseConfirm=null}> +

${this.host._t("confirm.erase_decor",{kind:e})}

+
+ + + +
+
`}_renderDecorTextDialog(){const t=this.host._decorTextDialog,e=(t.pickerEntity||"").trim(),s=e?this.host.hass?.states?.[e]:null;return o`this.host._decorTextDialog=null}> +
+ + ${""} + + this.host._decorTextDialog={...t,...e.detail}}> + +
this.host._decorTextDialog={...t,sizeCm:this.host._decorTextCm(Number(e.target.value))}} /> + ${this.host._t(this.host._imperial?"wallthick.unit_in":"wallthick.unit_cm")}
+ + this.host._decorTextDialog={...t,angle:e.target.value}} /> + + + this.host._decorTextDialog={...t,pickerEntity:e.target.value}} /> + + ${Object.keys(this.host.hass?.states||{}).map(t=>o``)} + + ${e?o` + + + `:a} +
+
+ + + +
+
`}_renderDecorShapeDialog(){const t=this.host._decorShapeDialog,e="rect"===t.kind||"ellipse"===t.kind,s=this.host._t("decor."+t.kind),i=this.host._t(this.host._imperial?"gs.unit_ft":"gs.unit_m");return o`this.host._decorShapeDialog=null}> +
+ ${"furniture"===t.kind?o` + + `:a} + this.host._decorShapeDialog={...t,...e.detail}}> + +
this.host._decorShapeDialog={...t,widthCm:this.host._decorSmallCm(Number(e.target.value))}} />${this.host._t(this.host._imperial?"wallthick.unit_in":"wallthick.unit_cm")}
+ ${"line"===t.kind?o` + +
+ + +
+ +
this.host._decorShapeDialog={...t,lengthCm:this.host._decorLargeCm(Number(e.target.value))}} /> + ${i}
`:o` + +
this.host._decorShapeDialog={...t,sizeWCm:this.host._decorLargeCm(Number(e.target.value))}} /> + ×this.host._decorShapeDialog={...t,sizeHCm:this.host._decorLargeCm(Number(e.target.value))}} /> + ${i}
`} + + this.host._decorShapeDialog={...t,angle:e.target.value}} /> + ${e?o` + this.host._decorShapeDialog={...t,fillColor:e.detail.color,fillOpacity:e.detail.opacity}}>`:a} +
+
+ + + +
+
`}_renderBackdropDialog(){const t=this.host._backdropDialog,e=this.host._t(this.host._imperial?"gs.unit_ft":"gs.unit_m");return o`this.host._backdropDialog=null}> +
+ +
this.host._backdropDialog={...t,widthCm:this.host._decorLargeCm(Number(e.target.value))}} /> + ×this.host._backdropDialog={...t,heightCm:this.host._decorLargeCm(Number(e.target.value))}} /> + ${e}
+ + this.host._backdropDialog={...t,angle:e.target.value}} /> +
+
+ + +
+
`}_cssPxToRender(t){const e=this.host._stageEl,s=this.host._viewOr(this.host._baseVb());return e?.clientWidth&&e.clientHeight?Math.max(s.w/e.clientWidth,s.h/e.clientHeight)*t:this.host._gridPitch/8*t}_deleteRoomClick(t){const e=this.host._spaceModel();if(!e)return;const s=[...e.rooms].reverse().find(e=>this._pointInRoom(t,e));s?s.id&&(this.host._roomDeleteDialog={roomId:s.id,name:s.name}):this.host._showToast(this.host._t("toast.delete_room_pick"))}_wallThickHit(t){const e=this.host._spaceModel();if(!e)return null;const s=6*this.host._gridPitch,i=this.host._openCuts();let o=null;const a=(t,e,i)=>{e>s||(!o||e100)return void this._showPhysicalRange(100);if("room"!==e.source.kind){const t=this._geometrySnapshot();if("partition"===e.source.kind){const t=(s.partitions||[]).find(t=>t.id===e.source.id);if(!t)return;if(0===n&&$t(s.openings,{kind:"partition",id:t.id}))return void this.host._showToast(this.host._t("toast.zero_wall_opening_conflict"));t.cm=n}else{const t=(s.room_drafts||[]).find(t=>t.id===e.source.id),i=t?.segments?.[e.source.segment];if(!i)return;i.cm=n}this.host._wallDialog=null;return this._commitPhysicalGeometry(this.host._t("history.wall_thickness"),t)&&this.host._showToast(this.host._t("toast.wallthick_set")),void this.host.requestUpdate()}const r=this._geometrySnapshot(),l=n>0?n:null;if(0===n){const i=.02*this.host._gridPitch,o=[e.a[0],e.a[1],e.b[0],e.b[1]],a=(s.rooms||[]).find(t=>t.id===e.roomId),n=new Set(t&&a&&Array.isArray(a.wall_ids)?a.wall_ids:(s.wall_segments||[]).filter(t=>{const e=[Number(t.a?.[0])*ol,Number(t.a?.[1])*ol],s=[Number(t.b?.[0])*ol,Number(t.b?.[1])*ol];return C(e,o)<=i&&C(s,o)<=i}).map(t=>String(t.id))),r=(s.openings||[]).some(t=>{if("partition"===t.host?.kind)return!1;if("wall"===t.host?.kind&&n.has(t.host.id))return!0;if(t.host)return!1;const e=[Number(t.x)*ol,Number(t.y)*ol];return e.every(Number.isFinite)&&C(e,o)<=i});if(r)return void this.host._showToast(this.host._t("toast.zero_wall_opening_conflict"))}let h,c=this.host._openCuts();if(n>0&&c.length){const o=[];if(t&&e.roomId){const t=(s.rooms||[]).find(t=>t.id===e.roomId),a=new Set(Array.isArray(t?.wall_ids)?t.wall_ids:[]);for(const t of s.wall_segments||[])a.has(t.id)&&o.push([Number(t.a?.[0])*ol,Number(t.a?.[1])*ol,Number(t.b?.[0])*ol,Number(t.b?.[1])*ol]);if(!o.length){const t=i.rooms.find(t=>t.id===e.roomId),s=t?R(t):null;for(let t=0;s&&t!o.some(e=>C([t[0],t[1]],e)<=a&&C([t[2],t[3]],e)<=a&&C([e[0],e[1]],t)<=a&&C([e[2],e[3]],t)<=a))}h=t&&e.roomId?Fe(s.walls,i.rooms,e.roomId,l,this.host._wallKeyPitch,c,ol):E(s.walls,e.a,e.b,l,this.host._wallKeyPitch,ol),h=this.host._normalizeWalls(h,c);for(const t of s.wall_segments||[])t.cm=Q(h,t.a,t.b,I,1);h.length?s.walls=h:delete s.walls,this.host._wallDialog=null,this._commitPhysicalGeometry(this.host._t("history.wall_thickness"),r)&&this.host._showToast(this.host._t(0===n?"toast.wallthick_cleared":"toast.wallthick_set")),this.host.requestUpdate()}_wallHatchDefs(t){if(!this.host._spaceWalls.length&&!this.host._physicalBodiesR().length&&!this.host._markup)return ce``;const e=Ne(this.host._cellCm);return ce` + + + + `}_renderWallThickUi(){const t=this.host._wallThickHover;return t&&t.d?ce``:ce``}_renderWallThickDialog(){const t=this.host._wallDialog;return t?o`
t.stopPropagation()}> +
+ + {this.host._wallDialog={...t,value:e.target.value}}} + @keydown=${t=>{"Enter"===t.key&&(t.preventDefault(),this._wallThickApply(!1))}} /> + ${this.host._t(this.host._imperial?"wallthick.unit_in":"wallthick.unit_cm")} +
+
+ ${"room"===t.source.kind?o``:a} + + +
+
`:o``}_openingAt(t){if(!this.host._openingsR.length)return null;const e=this.host._openingsR.flatMap(e=>{const s=e.angle*Math.PI/180,i=t[0]-e.rx,o=t[1]-e.ry,a=i*Math.cos(s)+o*Math.sin(s);return Math.abs(a)>e.rlen/2+ze(12,this.host._cellCm)?[]:[{o:e,localY:-i*Math.sin(s)+o*Math.cos(s)}]});if(!e.length)return null;const s=this.host._spaceModel();if(!s)return null;const i=this.host._openCuts(),o=this.host._openingWallIndexFor(s,i).value;return e.find(({o:t,localY:e})=>{const s="gate"===t.type?!t.flip_v:t.flip_v,i=t.partitionHost||this.host._spaceWalls.length||"gate"===t.type?this.host._openingFace(t,o,!!s):{ox:0,oy:0,cm:0,side:-1},a=Ae({type:t.type,length:t.rlen,angle:t.angle,amount:this.host._openingAmt(t),flipH:!!t.flip_h,flipV:!!t.flip_v,cellCm:this.host._cellCm,gridPitch:this.host._gridPitch,face:i});return Math.abs(e)<=a.hitHalf})?.o||null}_resolveOpeningPlacement(t){const e=this.host._openingPreset;if("opening"!==this.host._tool||!e)return null;const s=this.host._spaceModel();if(!s)return null;const i=this.host._openCuts(),o=this.host._openingWallIndexFor(s,i),a=`${o.key}|partitions:${this.host._cfgEpoch}`;this.host._openingPlacementIntervalsCache&&this.host._openingPlacementIntervalsCache.key===a||(this.host._openingPlacementIntervalsCache={key:a,value:[...z(s.rooms,this.host._spaceWalls,i,this.host._wallKeyPitch,this.host._cellCm,this.host._gridPitch,ol),...Oe(s.partitions,this.host._cellCm,this.host._gridPitch)]}),this.host._openingDimensionContextCache&&this.host._openingDimensionContextCache.key===a||(this.host._openingDimensionContextCache={key:a,value:Wr({rooms:s.rooms,walls:this.host._spaceWalls,openCuts:i,partitions:s.partitions,roomOpenings:this.host._roomWallOpeningInputs(),partitionCuts:this._partitionOpeningCuts(s),pitch:this.host._wallKeyPitch,cellCm:this.host._cellCm,gridPitch:this.host._gridPitch,coordScale:ol,epsilon:2e-4*this.host._gridPitch})});const n=Le({pointer:[t[0],t[1]],preset:e,geometryRevision:this.host._cfgEpoch,renderedLength:this.host._cmToUnits(e.lengthCm),intervals:this.host._openingPlacementIntervalsCache.value,baseTolerance:1.5*this.host._gridPitch,bodyPointerPadding:this._cssPxToRender("touch"===this.host._pointerModality.modality||"pen"===this.host._pointerModality.modality?10:6),gridStep:this.host._gridPitch});this.host._openingJambBlockCm=n.jambBlockedTarget?n.jambBlockedTarget.physicalHalfWidth/this.host._gridPitch*this.host._cellCm:null;const r=n.candidate;if(!r)return null;const l="gate"===r.type?!r.flipV:r.flipV;let h;if(r.host){const t=F({id:"preview",type:r.type,x:r.x/ol,y:r.y/ol,angle:r.angle,length:r.renderedLength/ol,host:r.host},s.partitions,ol,this.host._cellCm,this.host._gridPitch).resolved;h=t?Ee(t,l):{ox:0,oy:0,cm:0,side:-1}}else h=Be(o.value,{x:r.x,y:r.y,angle:r.angle,length:r.renderedLength,flip_v:l});const c="mi"===this.host.hass?.config?.unit_system?.length,d=Yr(r,this.host._openingDimensionContextCache.value).map(t=>({x:t.label[0],y:t.label[1],text:ee(t.distance/this.host._gridPitch*this.host._cellCm,c),dimension:t}));return{...r,face:h,measure:{labels:d,guide:r.measure.guide}}}_activateOpeningPlacement(t){this._activateMarkupTool("opening"),"opening"===this.host._tool&&(this.host._openingPreset=He(t,++this.host._openingPresetRevision),this.host._openingHoverCandidate=null,this.host._cursorPt=null)}_clearOpeningPlacement(t){this.host._openingHoverCandidate=null,this.host._openingJambBlockCm=null,t&&(this.host._openingPreset=null,this.host._openingRebindId=null)}_openingClick(t){const e=this.host._spaceModel();if(!e)return;const s=this.host._openingRebindId?null:this._openingAt(t);if(s)return void this._editOpening(s);const i=this.host._openingPreset;if(!i)return;const o=this.host._openingHoverCandidate,a=o&&Ge(o,[t[0],t[1]],i.revision,this.host._cfgEpoch)?o:this._resolveOpeningPlacement(t);if(!a){if(null!=this.host._openingJambBlockCm)return void this.host._showToast(this.host._t("opening.partition_jamb_margin",{distance:ee(this.host._openingJambBlockCm,this.host._imperial)}));const s=1.5*this.host._gridPitch,i=P(t,e.rooms,s);return i&&We(i.x,i.y,i.angle,this.host._openCuts(),s)?void this.host._showToast(this.host._t("toast.opening_on_zero_wall")):void this.host._showToast(this.host._t("toast.opening_no_wall"))}const n=this.host._openingRebindId?this.host._curSpaceCfg?.openings?.find(t=>t.id===this.host._openingRebindId):null;this.host._openingDialog={...n?{id:n.id,type:n.type,lengthCm:Math.round(n.length*ol/this.host._gridPitch*this.host._cellCm),lengthTouched:!1,contact:n.contact||"",lock:n.lock||"",invert:!!n.invert,flipH:!!n.flip_h,flipV:!!n.flip_v}:{type:a.type,lengthCm:a.lengthCm,contact:"",lock:"",invert:!1,flipH:a.flipH,flipV:a.flipV},...a.host?{host:a.host}:{},x:a.x,y:a.y,angle:a.angle},this.host._openingRebindId=null,this.host._openingHoverCandidate=null,this.host._cursorPt=null}_editOpening(t){this.host._openingDialog={id:t.id,type:t.type,lengthCm:Math.round(t.rlen/this.host._gridPitch*this.host._cellCm),lengthTouched:!1,contact:t.contact||"",lock:t.lock||"",invert:!!t.invert,flipH:!!t.flip_h,flipV:!!t.flip_v,..."partition"===t.host?.kind?{host:{...t.host}}:{},x:t.rx,y:t.ry,angle:t.angle}}_opPointerDown(t,e){if("plan"===this.host._mode&&this.host._spaceModel()&&"resize"!==this.host._tool){t.preventDefault(),t.stopPropagation();try{nl(t)}catch{}this.host._opDrag={id:e.id,moved:!1,sx:t.clientX,sy:t.clientY,dirty:!1,before:this._geometrySnapshot()}}}_opPointerMove(t,e){if(!this.host._opDrag||this.host._opDrag.id!==e.id)return;if(Math.abs(t.clientX-this.host._opDrag.sx)+Math.abs(t.clientY-this.host._opDrag.sy)<=3)return;const s=this.host._spaceModel();if(!s)return;const i=this._svgPoint(t),o=this.host._curSpaceCfg,a=o?.openings?.find(t=>t.id===e.id);if(!a)return;if("partition"===a.host?.kind){const t=s.partitions.find(t=>t.id===a.host.id);if(!t)return;const e=t.b[0]-t.a[0],o=t.b[1]-t.a[1],n=Math.hypot(e,o);if(!(n>1e-9))return;const r=e/n,l=o/n;if(Math.abs((i[0]-t.a[0])*l-(i[1]-t.a[1])*r)>4*this.host._gridPitch)return;const h=a.length*ol/2+L(t,this.host._cellCm,this.host._gridPitch);let c=(i[0]-t.a[0])*r+(i[1]-t.a[1])*l;if(c=Math.round(c/this.host._gridPitch)*this.host._gridPitch,c=Math.max(h,Math.min(n-h,c)),n<2*h-1e-9)return;const d=c/n,p=(t.a[0]+r*c)/ol,_=(t.a[1]+l*c)/this.host._spaceH;let u=180*Math.atan2(o,e)/Math.PI;u>=90?u-=180:u<-90&&(u+=180);const m=a.host.t!==d||a.x!==p||a.y!==_||a.angle!==u;return this.host._opDrag.moved=!0,m&&(this.host._opDrag.dirty=!0),a.host={...a.host,t:d},a.x=p,a.y=_,a.angle=u,this.host._opMeasure=null,m&&this.host._cfgEpoch++,void this.host.requestUpdate()}const n=P(i,s.rooms,4*this.host._gridPitch);if(!n)return;this.host._opDrag.moved=!0;const r=this._opRuler(n,a.length*ol);this.host._opMeasure=r.measure;const l=r.x/ol,h=r.y/this.host._spaceH,c=a.x!==l||a.y!==h||a.angle!==n.angle;if(c&&(this.host._opDrag.dirty=!0),a.x=l,a.y=h,a.angle=n.angle,Number(this.host._serverCfg?.model_version||0)>=9){const t=kr(a,o?.wall_segments||[]);t?a.host=t:delete a.host}c&&this.host._cfgEpoch++,this.host.requestUpdate()}_opRuler(t,e){const s=this.host._spaceModel()?.rooms||[],i=this.host._gridPitch/2;let o=t.x,a=t.y,n=je([o,a],t.angle,e,s,i);if(n&&n.centered&&(o!==n.wallCenter[0]||a!==n.wallCenter[1]))[o,a]=n.wallCenter,n=je([o,a],t.angle,e,s,i);else if(n){const[r,l]=n.wallA,[h,c]=n.wallB,d=h-r,p=c-l,_=Math.hypot(d,p);if(_>0){const h=this.host._gridPitch,c=Math.min(e/2,_/2);let u=Math.round(((o-r)*d+(a-l)*p)/_/h)*h;u=Math.max(c,Math.min(_-c,u)),o=r+u/_*d,a=l+u/_*p,n=je([o,a],t.angle,e,s,i)||n}}if(!n)return{x:o,y:a,angle:t.angle,measure:null};const r="mi"===this.host.hass?.config?.unit_system?.length,l=(t,e)=>({x:e[0],y:e[1],text:ee(t/this.host._gridPitch*this.host._cellCm,r)});return{x:o,y:a,angle:t.angle,measure:{labels:[l(n.sideA,n.midA),l(n.sideB,n.midB)],guide:n.centered?{x:n.wallCenter[0],y:n.wallCenter[1],angle:t.angle}:null}}}_opPointerUp(t,e){if(!this.host._opDrag||this.host._opDrag.id!==e.id)return;const s=this.host._opDrag,i=s.moved;this.host._opMeasure=null,i&&s.dirty&&this._commitPhysicalGeometry(this.host._t("history.move_opening"),s.before),i?window.setTimeout(()=>this.host._opDrag=null,0):this.host._opDrag=null}_opClick(t,e){"plan"===this.host._mode&&"resize"===this.host._tool||(t.stopPropagation(),this.host._opDrag?.moved||"plan"===this.host._mode&&this._editOpening(e))}_saveOpening(){const t=this.host._openingDialog,e=this.host._curSpaceCfg,s=this.host._spaceModel();if(!t||!e||!s)return;const i=this._geometrySnapshot(),o=this.host._spaceH,a=(e.openings||[]).find(e=>e.id===t.id),n={...a||{},id:t.id||"o"+Date.now().toString(36),type:t.type,x:t.x/ol,y:t.y/o,angle:t.angle,length:a&&!t.lengthTouched?a.length:this.host._cmToUnits(Math.max(20,t.lengthCm))/ol,...t.host?{host:{...t.host}}:{}};if("partition"===n.host?.kind){const t=qe(a,n)?H(n,s.partitions,ol,this.host._cellCm,this.host._gridPitch):F(n,s.partitions,ol,this.host._cellCm,this.host._gridPitch);if(!t.resolved){const e=s.partitions.find(t=>t.id===n.host.id);if("does-not-fit-jamb"===t.reason&&e){const t=L(e,this.host._cellCm,this.host._gridPitch);this.host._showToast(this.host._t("opening.partition_jamb_margin",{distance:ee(t/this.host._gridPitch*this.host._cellCm,this.host._imperial)}))}else this.host._showToast(this.host._t("opening.partition_orphan"));return}const i=(e.openings||[]).flatMap(t=>{if(!t.host||t.id===n.id)return[];const e=F(t,s.partitions,ol,this.host._cellCm,this.host._gridPitch).resolved;return e?[e]:[]});if(Ue(t.resolved,i))return void this.host._showToast(this.host._t("toast.opening_no_wall"));Object.assign(n,N(n,t.resolved,ol))}else if(Number(this.host._serverCfg?.model_version||0)>=9){const t=kr(n,e.wall_segments||[]);if(!t)return void this.host._showToast(this.host._t("toast.opening_no_wall"));n.host=t}else delete n.host;"passage"===t.type?(delete n.contact,delete n.lock,delete n.invert,delete n.flip_h,delete n.flip_v):(n.contact=t.contact||null,n.lock=("door"===t.type||"gate"===t.type)&&t.lock||null,n.invert=t.invert||void 0,n.flip_h="gate"!==t.type&&t.flipH||void 0,n.flip_v=t.flipV||void 0),e.openings=e.openings||[];const r=e.openings.findIndex(t=>t.id===n.id);r>=0?e.openings[r]=n:e.openings.push(n),this._commitPhysicalGeometry(this.host._t(t.id?"history.edit_opening":"history.add_opening"),i),this.host._openingDialog=null,this.host.requestUpdate()}_deleteOpening(){const t=this.host._openingDialog,e=this.host._curSpaceCfg;if(!t?.id||!e?.openings)return;const s=this._geometrySnapshot();e.openings=e.openings.filter(e=>e.id!==t.id),this._commitPhysicalGeometry(this.host._t("history.delete_opening"),s),this.host._openingDialog=null,this.host.requestUpdate()}_contactCandidates(){const t=[];for(const e of Object.keys(this.host.hass.states)){if(!this.host._openingEntityAvailable(e))continue;const s=e.split(".")[0];if("binary_sensor"!==s&&"cover"!==s)continue;const i=this.host.hass.states[e],o=["door","window","opening","garage_door","garage"].includes(i?.attributes?.device_class||"");("cover"!==s||o)&&t.push([e,i?.attributes?.friendly_name||e,o?0:1])}return t.sort((t,e)=>t[2]-e[2]||t[1].localeCompare(e[1])).map(([t,e])=>({value:t,label:e}))}_lockCandidates(){return Object.keys(this.host.hass.states).filter(t=>t.startsWith("lock.")&&this.host._openingEntityAvailable(t)).map(t=>({value:t,label:this.host.hass.states[t]?.attributes?.friendly_name||t})).sort((t,e)=>t.label.localeCompare(e.label))}_toggleOpeningEntityPicker(t){const e=this.host._openingDialog;if(!e)return;const s="contact"===t?!e.contactOpen:!e.lockOpen;this.host._openingDialog={...e,contactOpen:"contact"===t&&s,lockOpen:"lock"===t&&s}}_filterOpeningEntities(t,e){const s=this.host._openingDialog;s&&(this.host._openingDialog="contact"===t?{...s,contactFilter:e}:{...s,lockFilter:e})}_selectOpeningEntity(t,e){const s=this.host._openingDialog;s&&(this.host._openingDialog="contact"===t?{...s,contact:e,contactOpen:!1,contactFilter:""}:{...s,lock:e,lockOpen:!1,lockFilter:""})}_mergeClick(t){const e=this.host._spaceModel();if(!e)return;const s=e.rooms,i=[...s].reverse().find(e=>this._pointInRoom(t,e));if(!i?.id)return;const o=i.id;if(!this.host._mergeSel||this.host._mergeSel===o)return void(this.host._mergeSel=this.host._mergeSel===o?null:o);const a=s.find(t=>t.id===this.host._mergeSel),n=a?R(a):null,r=R(i),l=n&&r?Ke(n,r):null;if(!l)return this.host._showToast(this.host._t("toast.merge_not_adjacent")),void(this.host._mergeSel=null);this.host._mergeDialog={aId:this.host._mergeSel,bId:o,poly:l,pick:"a"},this.host._mergeSel=null}_commitMerge(){const t=this.host._mergeDialog,e=this.host._curSpaceCfg;if(!t||!e||!this.host._spaceModel())return;const s=this._geometrySnapshot(),i=this.host._spaceH,o="a"===t.pick?t.aId:t.bId,a="a"===t.pick?t.bId:t.aId,n=e.rooms.find(t=>t.id===o);if(!n)return void(this.host._mergeDialog=null);n.poly=t.poly.map(t=>[t[0]/ol,t[1]/i]),delete n.x,delete n.y,delete n.w,delete n.h,e.rooms=e.rooms.filter(t=>t.id!==a);const r=this._commitPhysicalGeometry(this.host._t("history.merge_rooms"),s);this.host._mergeDialog=null,this.host._regSignature="",this.host._maybeRebuildDevices(),r&&this.host._showToast(this.host._t("toast.rooms_merged",{name:n.name||""}))}_splitClick(t){const e=this.host._spaceModel();if(!e)return;const s=e.rooms;if(!this.host._splitSel){const e=[...s].reverse().find(e=>this._pointInRoom(t,e));if(!e?.id)return;return void(this.host._splitSel={roomId:e.id,pts:[]})}const i=s.find(t=>t.id===this.host._splitSel.roomId),o=i?R(i):null;if(!i||!o)return void(this.host._splitSel=null);const a=.02*this.host._gridPitch,n=6*this.host._gridPitch,r=Ve(t,o),l=r&&Je(r,o,this.host._gridPitch)||r,h=r&&l&&Math.hypot(r[0]-t[0],r[1]-t[1])<=n?l:null,c=!!h&&Ye(h,o,a),d=this.host._splitSel.pts;if(!d.length)return c?void(this.host._splitSel={...this.host._splitSel,pts:[h]}):void this.host._showToast(this.host._t("toast.split_pick_wall"));if(!c){const e=this._snap(t);return qt(e,o,a)?void(this.host._splitSel={...this.host._splitSel,pts:[...d,e]}):void this.host._showToast(this.host._t("toast.split_pick_inside"))}const p=Kt(o,[...d,h],a);if(!p)return void this.host._showToast(this.host._t("toast.split_bad_cut"));this._resetRoomDialogFields();const[_,u]=p,m=k(_)>=k(u)?_:u,g=m===_?u:_;this.host._pendingSplit={roomId:i.id,mainPoly:m,newPoly:g},this.host._cursorPt=null,this.host._nameSel="",this.host._areaSel="",this.host._roomDialog=!0}_markupMove(t){if(!this.host._markup)return;if("column"===this.host._tool)return void(this.host._cursorPt=this._snap(this._svgPoint(t)));if("opening"===this.host._tool||"wallthick"===this.host._tool)return void(this.host._cursorPt=this._svgPoint(t));const e="draw"===this.host._tool&&!this.host._contourClosed,s="split"===this.host._tool&&!!this.host._splitSel?.pts?.length;if(!e&&!s)return;const i=this._svgPoint(t);if(e){const e=this._resolvePlanDrawPoint(i,t.shiftKey);return this.host._planSnapHover={contextKey:e.contextKey,candidate:e.candidate,conflicts:e.conflicts},this._syncPlanSnapConflictMarkers(e.conflicts),this.host._path.length?void(this.host._cursorPt=e.point):void this._syncPlanSnapActiveMarker(e.candidate)}this.host._cursorPt=this._snap(i)}_saveRoom(){(this.host._areaSel||this.host._nameSel.trim())&&(this.host._wallFaceBatch?this._decideWallFace(!0):this._commitRoom())}_decideWallFace(t){const e=this.host._wallFaceBatch;if(!e)return;const s=e.candidates[e.index];if(!s)return;if(!t&&(s.existing||s.repair)&&1===e.candidates.length)return this.host._wallFaceBatch=null,this.host._roomDialog=!1,this.host._nameSel="",this.host._areaSel="",void this.host.requestUpdate();const i=t?{candidate:s,create:!0,name:this.host._nameSel.trim(),area:this.host._areaSel||null,settings:this._roomSettingsFromDialog()?JSON.parse(JSON.stringify(this._roomSettingsFromDialog())):null}:{candidate:s,create:!1},o=[...e.decisions,i],a=e.index+1;if(a"number"==typeof t&&Number.isFinite(t)&&t>=0?t:null,o=.02*this.host._gridPitch,a=this.host._spaceModel();if(!a)return A;for(const e of this._planSnapGeometrySnapshot().value.segments){if("room"===e.sourceKind||C(t,[e.a[0],e.a[1],e.b[0],e.b[1]])>o)continue;if("partition"===e.sourceKind)return i(a.partitions.find(t=>t.id===e.sourceId)?.cm)??A;const s=e.sourceId.lastIndexOf(":"),n=s>=0?e.sourceId.slice(0,s):e.sourceId,r=s>=0?Number(e.sourceId.slice(s+1)):-1;return i(a.room_drafts.find(t=>t.id===n)?.segments[r]?.cm)??A}const n=Ra(Math.max(0,e.length-1),s,this.host._drawWallCm,A);for(let s=0;s+1[...t]);if(!e?.sourceKey.startsWith("active:"))return s;const i=/:(\d+)$/.exec(e.sourceKey),o=i?Number(i[1])+("b"===e.endpoint?1:0):-1;return o>=0&&oe.key===t.sourceKey),a=i.find(e=>e.key===t.targetSourceKey);if(!o||!a)return!1;const n="a"===t.endpoint?o.a:o.b;return!(Math.hypot(n[0]-t.from[0],n[1]-t.from[1])>s)&&(!(C(t.to,[a.a[0],a.a[1],a.b[0],a.b[1]])>s)&&!function(t,e){if(!t.sourceKey.startsWith("static:partition|"))return!1;const s=t.sourceKey.slice(17).split("|")[0];return!!s&&e.some(t=>"partition"===t.host?.kind&&t.host.id===s)}(t,this.host._curSpaceCfg?.openings||[]))}_applyWallRepair(t,e){const s=this.host._curSpaceCfg;if(!s||!this._validateWallRepair(t,e.activePath))return!1;const i=e=>{e[0]=t.to[0]/ol,e[1]=t.to[1]/ol};if(t.sourceKey.startsWith("active:")){const o=/:(\d+)$/.exec(t.sourceKey),a=o?Number(o[1])+("b"===t.endpoint?1:0):-1;if(a<0||a>=e.activePath.length)return!1;e.activePath[a]=[...t.to];const n=(s.room_drafts||[]).find(t=>t.id===e.activeDraftId);return n?.points?.[a]&&i(n.points[a]),!0}if(!t.sourceKey.startsWith("static:"))return!1;const[o,a]=t.sourceKey.slice(7).split("|");if("partition"===o){const e=(s.partitions||[]).find(t=>t.id===a);if(!e)return!1;const o=[e.a[0]*ol,e.a[1]*ol],n=[e.b[0]*ol,e.b[1]*ol],r=Math.hypot(o[0]-t.from[0],o[1]-t.from[1])<=2e-4*this.host._gridPitch?e.a:Math.hypot(n[0]-t.from[0],n[1]-t.from[1])<=2e-4*this.host._gridPitch?e.b:null;return!!r&&(i(r),!0)}if("draft"===o){const e=a.lastIndexOf(":"),o=e>=0?a.slice(0,e):"",n=e>=0?Number(a.slice(e+1)):-1,r=(s.room_drafts||[]).find(t=>t.id===o);if(!r?.points?.[n]||!r?.points?.[n+1])return!1;const l=[n,n+1].find(e=>Math.hypot(r.points[e][0]*ol-t.from[0],r.points[e][1]*ol-t.from[1])<=2e-4*this.host._gridPitch);return null!=l&&(i(r.points[l]),!0)}return!1}_applyWallFaceBatch(){const t=this.host._wallFaceBatch,e=this.host._curSpaceCfg,s=this.host._spaceModel();if(!t||!e||!s)return;const i=(t,e)=>{this.host._showToast(this.host._t(t,e)),this._roomDialogCancel()},o=t.decisions.filter(t=>t.create),a=o.map(t=>t.candidate.ring),n=s.rooms;for(let t=0;ts.id!==e&&!!R(s)&&$(a[t],R(s)));if(s)return void i("toast.room_overlap",{name:s.name||""})}if((e.rooms||[]).length+o.length>400)return void i("toast.physical_limit");if(o.some(t=>t.candidate.split&&!e.rooms.some(e=>e.id===t.candidate.split.roomId)))return void i("toast.contour_cannot_close");const r=o.map(t=>t.candidate.repair).filter(t=>!!t);if(r.length>1||r[0]&&!this._validateWallRepair(r[0],t.activePath))return void i("toast.wall_repair_changed");const l=this._activePathWithRepair(t.activePath,r[0]),h=2e-4*this.host._gridPitch,c=new Map;for(let e=0;et.candidate.atomKeys)),e=o.some(t=>t.candidate.consumeAllActive);for(const s of tn(this._wallGraphSources(l),h)){const i=s.sourceKeys.find(t=>c.has(t));!i||e||t.has(s.key)||d.push({a:s.a,b:s.b,cm:c.get(i)})}}else d.push(...Fa(l,Ra(l.length-1,t.activeCms,this.host._drawWallCm,A)));if((e.partitions||[]).length+d.length>al)return void i("toast.physical_limit");const p=o.map(t=>t.candidate.ring.map(()=>"")),_=t.activeDraftId?(e.room_drafts||[]).find(e=>e.id===t.activeDraftId):null,u=this._draftSegmentsForPath(t.activePath,_,t.activeCms),m=(t,e)=>{const s=`${t[0].toFixed(9)},${t[1].toFixed(9)}`,i=`${e[0].toFixed(9)},${e[1].toFixed(9)}`;return s<=i?`${s}|${i}`:`${i}|${s}`};for(let e=0;et.candidate.ring.forEach((s,i)=>{const o=t.candidate.ring[(i+1)%t.candidate.ring.length];C(s,n)<=h&&C(o,n)<=h&&r.push({kind:"room",owner:e,edge:i,a:s,b:o})})),d.forEach((t,e)=>{C(t.a,n)<=h&&C(t.b,n)<=h&&r.push({kind:"partition",owner:e,edge:0,a:t.a,b:t.b})});const l=[(i[0]+a[0])/2,(i[1]+a[1])/2];r.sort((t,e)=>{const s=[t.a[0],t.a[1],t.b[0],t.b[1]],o=[e.a[0],e.a[1],e.b[0],e.b[1]];return(C(l,s)<=h?0:1)-(C(l,o)<=h?0:1)||(C(i,s)<=h?0:1)-(C(i,o)<=h?0:1)||m(t.a,t.b).localeCompare(m(e.a,e.b))});const c=r[0];if(!c)continue;const _=m(c.a,c.b);for(const t of r)m(t.a,t.b)===_&&("room"===t.kind?p[t.owner][t.edge]=s:d[t.owner].id=s)}const g=this._geometrySnapshot();if(r[0]&&!this._applyWallRepair(r[0],t))return void i("toast.wall_repair_changed");const f=o.some(t=>!!t.candidate.split),b=f?kt(s.rooms,e.walls,this.host._openCuts(),this.host._wallKeyPitch,this.host._cellCm,this.host._gridPitch,ol):null,y=f&&Array.isArray(e.walls)?e.walls:null;for(const t of o){const s=t.candidate.split;if(!s)continue;const o=e.rooms.find(t=>t.id===s.roomId);if(!o)return void i("toast.contour_cannot_close");o.poly=s.mainPoly.map(t=>[t[0]/ol,t[1]/this.host._spaceH]),delete o.x,delete o.y,delete o.w,delete o.h}const v=Date.now().toString(36),w=[];if(o.forEach((t,s)=>{const i=t.area?this.host.hass.areas[t.area]?.name:"",o={id:`r${v}-${s}`,name:t.name||i||this.host._t("room.default_name"),area:t.area||null,poly:t.candidate.ring.map(t=>[t[0]/ol,t[1]/this.host._spaceH]),...p[s].some(Boolean)?{wall_ids:p[s]}:{},...t.settings?{settings:JSON.parse(JSON.stringify(t.settings))}:{}};e.rooms.push(o),w.push({room:o,decision:t})}),t.activeDraftId&&Array.isArray(e.room_drafts)&&(e.room_drafts=e.room_drafts.filter(e=>e.id!==t.activeDraftId),e.room_drafts.length||delete e.room_drafts),d.length&&(e.partitions||=[],d.forEach((t,s)=>e.partitions.push({id:t.id||`partition-${v}-${s}`,a:[t.a[0]/ol,t.a[1]/ol],b:[t.b[0]/ol,t.b[1]/ol],cm:t.cm}))),f){const t=this.host._normalizeWalls(b,this.host._openCuts());t.length?e.walls=t:y?.length?e.walls=y:delete e.walls}if(w.length){this.host._cfgEpoch++;const s=this.host._openCuts(),i=this.host._spaceModel();if(!i)return;let o=e.walls;for(const{room:e,decision:a}of w){const n=a.candidate.ring,r=this._wallSourceCmAt(n[0],t.activePath,t.activeCms);o=Xe(o,i.rooms,e.id,r,this.host._wallKeyPitch,s,ol);for(const a of z(i.rooms,o,s,this.host._wallKeyPitch,this.host._cellCm,this.host._gridPitch,ol)){if(a.roomId!==e.id||"outer"!==a.kind)continue;const s=[(a.a[0]+a.b[0])/2,(a.a[1]+a.b[1])/2];o=E(o,a.a,a.b,this._wallSourceCmAt(s,t.activePath,t.activeCms),this.host._wallKeyPitch,ol)}}const a=this.host._normalizeWalls(o,s);a.length?e.walls=a:delete e.walls}if(!this._commitPhysicalGeometry(this.host._t("history.wall_face_batch"),g))return;delete this.host._resumeDraftBySpace[this.host._space],this.host._path=[],this.host._activeDraftId=null,this.host._draftSegmentCms=[],this.host._closingWallCm=null,this.host._wallFaceBatch=null,this.host._roomDialog=!1,this.host._nameSel="",this.host._areaSel="",this.host._regSignature="",this.host._maybeRebuildDevices();const x=new Set(o.map(t=>t.area).filter(t=>!!t));if(x.size){const t={...this.host._layout};for(const e of this.host._devices){if(!x.has(e.area||"")||e.space!==this.host._space)continue;if(this.host._layout[e.id])continue;const s=this.host._defPos[e.id];s&&(t[e.id]={s:this.host._space,x:s.x/ol,y:s.y/ol},this.host._dirtyPos.add(e.id))}this.host._layout=t,this.host._persistLayout()}this.host._showToast(this.host._t(o.length?"toast.wall_rooms_saved":"toast.wall_chain_saved",{n:o.length}))}_commitRoom(){const t=this.host._curSpaceCfg,e=this.host._spaceModel();if(!t||!e)return;const s=this._geometrySnapshot(),i=this.host._spaceH,o=!!this.host._pendingSplit,a=o?kt(e.rooms,t.walls,this.host._openCuts(),this.host._wallKeyPitch,this.host._cellCm,this.host._gridPitch,ol):null,n=o&&Array.isArray(t.walls)?t.walls:null;let r;if(this.host._pendingSplit){const e=t.rooms.find(t=>t.id===this.host._pendingSplit.roomId);if(!e)return this.host._pendingSplit=null,this.host._splitSel=null,void(this.host._roomDialog=!1);e.poly=this.host._pendingSplit.mainPoly.map(t=>[t[0]/ol,t[1]/i]),delete e.x,delete e.y,delete e.w,delete e.h,r=this.host._pendingSplit.newPoly}else{if(!this.host._contourClosed)return;r=this.host._path.slice(0,-1)}const l=this.host._areaSel?this.host.hass.areas[this.host._areaSel]?.name:"",h={id:"r"+Date.now().toString(36),name:this.host._nameSel||l||this.host._t("room.default_name"),area:this.host._areaSel||null,poly:r.map(t=>[t[0]/ol,t[1]/i]),...this._roomSettingsFromDialog()?{settings:this._roomSettingsFromDialog()}:{}};if(!o&&this.host._activeDraftId){const e=Array.isArray(t.room_drafts)?t.room_drafts.find(t=>t.id===this.host._activeDraftId):null,s=Ra(r.length,[...this.host._draftSegmentCms,this.host._closingWallCm??void 0],this.host._drawWallCm,A),i=this._draftSegmentsForPath(this.host._path,e,s);i.length===r.length&&i.every(t=>!!t.id)&&(h.wall_ids=i.map(t=>t.id))}if(t.rooms.push(h),!o&&this.host._activeDraftId&&Array.isArray(t.room_drafts)&&(t.room_drafts=t.room_drafts.filter(t=>t.id!==this.host._activeDraftId),t.room_drafts.length||delete t.room_drafts),o){const e=this.host._normalizeWalls(a,this.host._openCuts());e.length?t.walls=e:n?.length?t.walls=n:delete t.walls}if(!o){const e=Ra(r.length,[...this.host._draftSegmentCms,this.host._closingWallCm??void 0],this.host._drawWallCm,A),s=e[0];if(null!=s){this.host._cfgEpoch++;const i=this.host._openCuts(),o=this.host._spaceModel();if(!o)return;let a=Xe(t.walls,o.rooms,h.id,s,this.host._wallKeyPitch,i,ol);for(const t of z(o.rooms,a,i,this.host._wallKeyPitch,this.host._cellCm,this.host._gridPitch,ol)){if(t.roomId!==h.id||"outer"!==t.kind)continue;const s=[(t.a[0]+t.b[0])/2,(t.a[1]+t.b[1])/2],i=r.findIndex((t,e)=>{const i=r[(e+1)%r.length];return C(s,[t[0],t[1],i[0],i[1]])<=.02*this.host._gridPitch});i>=0&&(a=E(a,t.a,t.b,e[i],this.host._wallKeyPitch,ol))}a=this.host._normalizeWalls(a,i),a.length?t.walls=a:delete t.walls}}const c=this._commitPhysicalGeometry(this.host._t(o?"history.split_room":"history.add_room"),s);this.host._path=[],delete this.host._resumeDraftBySpace[this.host._space],this.host._activeDraftId=null,this.host._draftSegmentCms=[],this.host._closingWallCm=null,this.host._pendingSplit=null,this.host._splitSel=null;const d=this.host._areaSel;if(this.host._areaSel="",this.host._nameSel="",this.host._roomDialog=!1,this.host._regSignature="",this.host._maybeRebuildDevices(),!c)return;let p=0;if(d){const t=ol,e={...this.host._layout};for(const s of this.host._devices){if(s.area!==d||s.space!==this.host._space)continue;if(p++,this.host._layout[s.id])continue;const i=this.host._defPos[s.id];i&&(e[s.id]={s:this.host._space,x:i.x/ol,y:i.y/t},this.host._dirtyPos.add(s.id))}this.host._layout=e,this.host._persistLayout()}const _=this.host._model.find(t=>t.id===this.host._space)?.rooms.length||0;this.host._showToast(d?this.host._t("toast.room_saved",{n:_,added:p}):this.host._t("toast.room_saved_no_area",{n:_}))}_cancelPath(){this.host._wallRepairDiagnostic=null,this.host._activeDraftId&&(this.host._resumeDraftBySpace[this.host._space]=this.host._activeDraftId),this.host._path=[],this.host._activeDraftId=null,this.host._draftSegmentCms=[],this.host._closingWallCm=null,this._clearPlanSnapHover(),this.host._roomDialog=!1,this.host._pendingSplit=null,this.host._wallFaceBatch=null,this.host._splitSel=null,this.host._mergeSel=null,this.host._mergeDialog=null,this.host._physicalSel=null,this.host._physicalDrag=null,this.host._physicalRotate=null,this._clearOpeningPlacement(!0)}_resumeLastDraft(){const t=this.host._resumeDraftBySpace[this.host._space];if(!t)return;const e=this.host._spaceModel()?.room_drafts.find(e=>e.id===t);e?(this.host._activeDraftId=t,this.host._path=e.points.map(t=>[...t]),this.host._draftSegmentCms=this._adoptDraftCms(this.host._path,e.segments.map(t=>t.cm),e.id),this._clearPlanSnapHover()):delete this.host._resumeDraftBySpace[this.host._space]}_roomDialogCancel(){if(this.host._roomDialog=!1,this.host._roomEditId)return this.host._roomEditId=null,this.host._nameSel="",void(this.host._areaSel="");if(this.host._wallFaceBatch){const t=this.host._wallFaceBatch;return this.host._wallFaceBatch=null,this.host._path=t.activePath.map(t=>[...t]),this.host._draftSegmentCms=[...t.activeCms],this.host._activeDraftId=t.activeDraftId,this.host._nameSel="",this.host._areaSel="",void this.host.requestUpdate()}if(this.host._pendingSplit)return this.host._pendingSplit=null,void(this.host._splitSel=null);this.host._undoPoint()}_openDeviceInbox(){this.host._deviceInboxReturn=null,this.host._deviceInbox=this.host._deviceInbox||{tab:"on_plan",search:"",showEntities:!1,onlyNew:!1,limit:100}}_closeMarkerDialog(){if(this.host._markerDialog=null,this.host._deviceInboxReturn){const t={...this.host._deviceInboxReturn};this.host._deviceInbox=t,this.host._deviceInboxReturn=null,t.anchor&&this.host.updateComplete.then(()=>requestAnimationFrame(()=>{const e=`.device-inbox-row[data-binding="${CSS.escape(t.anchor)}"]`;this.host.renderRoot.querySelector(e)?.scrollIntoView({block:"nearest"})}))}}_deviceInboxCandidates(t){return ca({hass:this.host._planHass,devices:this.host._devices,markers:this.host._markers,showEntities:t,labels:{device:this.host._t("marker.sub_device"),z2mGroup:this.host._t("marker.sub_z2m_group"),group:this.host._t("marker.sub_group"),helper:this.host._t("marker.sub_helper"),entity:this.host._t("marker.sub_entity")}})}_deviceInboxRows(){const t=this.host._deviceInbox||this.host._deviceInboxReturn,e=!!t?.showEntities,s=[this.host._haRegistry.revision,this.host._cfgRev,this.host._cfgEpoch,this.host._regSignature,this.host._newSyncKey,e?1:0,this.host._showAll?1:0,d(this.host.hass,this.host._config?.language)].join("|");if(this.host._deviceInboxMemo?.key===s)return this.host._deviceInboxMemo.rows;const i=this._deviceInboxCandidates(e),o=new Set(i.map(t=>t.value));for(const t of this.host._markers)t.binding&&"virtual"!==t.binding&&o.add(t.binding);for(const t of this.host._devices)t.bindingKind&&"virtual"!==t.bindingKind&&t.bindingRef&&o.add(`${t.bindingKind}:${t.bindingRef}`);const a=new Map;for(const t of o)a.set(t,this.host._bindingStatus(t));const n=this.host._areaToSpace,r={};for(const[t,e]of Object.entries(this.host.hass?.areas||{}))r[t]=n[t]?.room?.name||e?.name||t;for(const[t,e]of Object.entries(n))r[t]=e.room.name||r[t]||t;const l=Object.fromEntries(this.host._model.map(t=>[t.id,t.title])),h=Object.fromEntries(Object.entries(n).map(([t,e])=>[t,e.space])),c={},p=new Map;for(const[t,e]of Object.entries(this.host._fullRegistryHass.entities||{})){const s=String(e?.platform||"").trim();if(s&&(c[`entity:${t}`]=s),s&&e?.device_id){const t=p.get(e.device_id)||new Set;t.add(s),p.set(e.device_id,t)}}for(const[t,e]of p)c[`device:${t}`]=[...e].sort().join(", ");const _={};for(const[t,e]of Object.entries(this.host._fullRegistryHass.devices||{})){const s=`device:${t}`,i=p.get(t)||new Set,o=Array.isArray(e?.identifiers?.[0])?String(e.identifiers[0][0]||""):"",a=[o,...i].some(t=>this.host._excluded.has(t));"service"===e?.entry_type?_[s]="service_entry":a?_[s]="excluded_integration":"Group"===e?.model?_[s]="grouped_light":/scene/i.test(e?.model||"")?_[s]="excluded_domain":(/bridge/i.test(`${e?.model||""}${e?.name||""}`)||"myheat"===o&&e?.via_device_id)&&(_[s]="represented_by_parent")}const u=function(t){const{devices:e,markers:s,candidates:i,statuses:o,newDeviceIds:a,showHiddenOnPlan:n,areaNames:r={},spaceNames:l={},spaceByArea:h={},integrationByBinding:c={},reasonByBinding:d={}}=t,p=new Map;for(const t of e)!t.virtual&&t.bindingKind&&"virtual"!==t.bindingKind&&t.bindingRef&&p.set(`${t.bindingKind}:${t.bindingRef}`,t);const _=new Map,u=new Map;for(const t of s)t?.binding&&"virtual"!==t.binding&&(t.removed?u.set(t.binding,t):_.set(t.binding,t));const m=new Map(i.map(t=>[t.value,t])),g=new Set([...p.keys(),..._.keys(),...m.keys(),...[...u.keys()].filter(t=>m.has(t))]),f=[];for(const t of g){const e=p.get(t),s=_.get(t),i=s?void 0:u.get(t),g=m.get(t);let b;if(i&&g)b="readd";else if(!0===s?.hidden)b="hidden";else if(e||s)b="on_plan";else{if(!g)continue;b="available"}const y=e?.bindingStatus||o.get(t)||da,v=s||i,$=pa(t),w=e?.area||g?.areaId||v?.area||"",k=e?.space||v?.space||h[w]||"",x=e?.name||v?.name||g?.label||t,S=e?.model||v?.model||g?.model||"",M=!!g?.parentDeviceId&&p.has(`device:${g.parentDeviceId}`),D="readd"===b?"removed":"hidden"===b?s&&_a(s)?d[t]||"automatic_hidden":"manual_hidden":"on_plan"===b?s?"visible_explicit":"visible_auto":d[t]||(M?"represented_by_parent":w&&k?"available":"no_bound_room"),C=!!e&&(!e.hidden||n),P="active"===y.kind,I=C&&(P||"ha_disabled"===y.kind&&n),T=[x,S,c[t],r[w],l[k],t,g?.sub].filter(Boolean).join(" ").toLocaleLowerCase();f.push({key:t,binding:t,category:b,status:y,reason:D,deviceId:e?.id,markerId:v?.id,name:x,icon:e?.icon||v?.icon||("entity"===$?"mdi:code-braces":"mdi:devices"),model:S,integration:c[t]||"",areaId:w,areaName:r[w]||"",spaceId:k,spaceName:l[k]||"",kind:$,isNew:!!e&&a.has(e.id),searchText:T,canFind:I,canEdit:!!e||!!s,canHide:"on_plan"===b&&P,canShow:"hidden"===b&&P,canAdd:("available"===b||"readd"===b)&&P})}const b=t=>"active"===t.status.kind?1:0;return f.sort((t,e)=>t.category.localeCompare(e.category)||Number(e.isNew)-Number(t.isNew)||b(t)-b(e)||t.name.localeCompare(e.name)||t.binding.localeCompare(e.binding))}({devices:this.host._devices,markers:this.host._markers,candidates:i,statuses:a,newDeviceIds:this.host._newIds,showHiddenOnPlan:this.host._showAll,areaNames:r,spaceNames:l,spaceByArea:h,integrationByBinding:c,reasonByBinding:_});return this.host._deviceInboxMemo={key:s,rows:u},u}_deviceForInboxRow(t){const e=t.deviceId?this.host._devices.find(e=>e.id===t.deviceId):null;if(e)return e;const s=t.markerId?this.host._markers.find(e=>e.id===t.markerId):null;return!s||s.removed?null:{id:s.id,name:t.name,model:t.model,area:t.areaId,space:t.spaceId||this.host._space,hidden:!0===s.hidden||"ha_disabled"===t.status.kind,userHidden:!0===s.hidden,bindingStatus:t.status,icon:t.icon,entities:"active"===t.status.kind?t.status.enabledEntityIds:[],allEntities:t.status.allEntityIds,primary:"active"===t.status.kind?t.status.enabledEntityIds[0]:void 0,marker:s,bindingKind:t.kind,bindingRef:t.binding.slice(t.binding.indexOf(":")+1),pdfs:s.pdfs||[]}}_openInboxMarker(t,e=!1){const s=this.host._deviceInbox;if(s){if(this.host._deviceInboxReturn={...s,anchor:t.key},this.host._deviceInbox=null,!e){const e=this._deviceForInboxRow(t);return void(e?this._openMarkerDialog(e):this._closeMarkerDialog())}this._openMarkerDialog(),this.host._markerDialog?this.host._markerDialog={...this.host._markerDialog,bindingMode:"ha",binding:t.binding,bindingOpen:!1,showEntities:"entity"===t.kind,name:""}:this._closeMarkerDialog()}}async _setInboxHidden(t,e){const s=this.host._deviceInbox,i=this.host._serverCfg;if(!s||!i||s.busy||"active"!==t.status.kind)return;const o=i.markers||[],a=o.find(e=>!e.removed&&e.binding===t.binding);if(!e&&!a)return;const n=a?.id||Ze(t.binding,t.markerId,()=>`m_${Date.now().toString(36)}`),r=a?{...a,hidden:e}:{id:n,binding:t.binding,hidden:!0};i.markers=[...o.filter(e=>e.id!==n&&(e.binding!==t.binding||!0===e.removed)),r],this.host._deviceInbox={...s,busy:t.key,anchor:t.key};try{await this._saveConfigNow(),this.host._regSignature="",this.host._deviceInboxMemo=null,this.host._maybeRebuildDevices(),this.host._deviceInbox&&(this.host._deviceInbox={...this.host._deviceInbox,busy:void 0}),this.host._showToast(this.host._t("device_inbox.saved"))}catch(t){this.host._serverCfg===i&&(i.markers=o),this.host._deviceInbox&&(this.host._deviceInbox={...this.host._deviceInbox,busy:void 0}),this.host._showToast(this.host._t("toast.error",{err:this.host._errText(t)}))}}_findInboxDevice(t){if(!t.canFind)return;const e=this._deviceForInboxRow(t);if(!e)return;if(this.host._deviceInbox=null,e.space&&e.space!==this.host._space){if(!this.host._commitSpace(e.space))return;this.host._restoreZoom()}const s=()=>{const t=this.host._devices.find(t=>t.id===e.id)||e,s=this.host._pos(t);this.host._applyView(this.host._zoom,s.x,s.y),this.host._selId=t.id,this.host.requestUpdate(),window.setTimeout(()=>{this.host._selId===t.id&&(this.host._selId=null,this.host.requestUpdate())},1500)};requestAnimationFrame(()=>requestAnimationFrame(s))}_deviceInboxTabKey(t){if("ArrowLeft"!==t.key&&"ArrowRight"!==t.key)return;const e=this.host._deviceInbox;if(!e)return;const s=["on_plan","available","hidden","readd"],i="ArrowRight"===t.key?1:-1,o=(s.indexOf(e.tab)+i+s.length)%s.length;this.host._deviceInbox={...e,tab:s[o],limit:100,onlyNew:!1},t.preventDefault()}_openMarkerDialog(t){if(this.host._vacAllCamerasFor=null,this.host._vacAllCameraCache=null,t&&this.host._ackNewDevice(t.id),this.host._norm)if(t){const e=t.marker,s=Object.prototype.hasOwnProperty.call(e||{},"is_light"),i=Object.prototype.hasOwnProperty.call(e||{},"light_entity"),o=Object.prototype.hasOwnProperty.call(e||{},"glow_color"),a=Object.prototype.hasOwnProperty.call(e||{},"value_badge"),n=Object.prototype.hasOwnProperty.call(e||{},"tap_action"),r=Object.prototype.hasOwnProperty.call(e||{},"toggle_entity"),h=Qe(e?.glow_color),c=this.host._devicePresentation(t,!0).valueBadge,d=ts(this.host._planHass,t,this.host._devices),p=es(this.host._planHass,t,d);this.host._markerDialog={devId:t.id,name:t.name,binding:"virtual"===t.bindingKind?"virtual":t.bindingKind+":"+t.bindingRef,bindingMode:"virtual"===t.bindingKind?"virtual":"ha",bindingOpen:!1,showEntities:"entity"===t.bindingKind&&!!this.host._fullRegistryHass.entities[t.bindingRef||""]?.device_id,bindingFilter:"",icon:t.marker?.icon||"",autoIcon:t.icon||"",display:os(t.marker?.display),rippleColor:l(t.marker?.ripple_color,""),rippleSize:Number(t.marker?.ripple_size)>0?Number(t.marker.ripple_size):1.5,size:Number(t.marker?.size)>0?Number(t.marker.size):1,angle:Number(t.marker?.angle)||0,tapAction:is(t.marker?.tap_action,t.primary?.split(".")[0]),tapActionTouched:!1,originalHasTapAction:n,originalTapAction:t.marker?.tap_action,tapHintAnnouncement:"",toggleEntity:e?.toggle_entity||"",toggleEntityTouched:!1,originalHasToggleEntity:r,originalToggleEntity:e?.toggle_entity,tapTarget:t.marker?.tap_target||"",tapConfirm:!0===t.marker?.tap_confirm,runFilter:"",controls:ss(t.marker?.binding,t.marker?.controls,t.entities),controlsFilter:"",lightRole:!0===e?.is_light?"always":!1===e?.is_light?"never":"auto",lightRoleTouched:!1,originalHasIsLight:s,originalIsLight:e?.is_light,lightEntity:e?.light_entity||"",lightEntityTouched:!1,originalHasLightEntity:i,originalLightEntity:e?.light_entity,glowMode:null!=h?.bri?"fixed":h?"color":"auto",glowColor:h?.c||this.host._fillColors.glow_light.c,glowBrightness:Math.max(1,Math.round(100*(h?.bri??1))),glowColorDrafted:!!h,glowBrightnessDrafted:null!=h?.bri,glowTouched:!1,originalHasGlowColor:o&&(!!h||null===e?.glow_color),originalGlowColor:h||(null===e?.glow_color?null:void 0),valueBadgeEnabled:a?!0===e?.value_badge?.enabled:!!c,valueBadgeSource:e?.value_badge?.source||c?.source||p,valueBadgePosition:e?.value_badge?.position||c?.position||"right",valueBadgeTouched:!1,originalHasValueBadge:a,originalValueBadge:e?.value_badge,useClimateTemp:!0===t.marker?.use_climate_temp,glowRadius:Number(t.marker?.glow_radius_cm)>0?String(this.host._imperial?Math.round(Number(t.marker.glow_radius_cm)/30.48*10)/10:Math.round(Number(t.marker.glow_radius_cm))/100):"",model:t.model||"",link:t.link||"",description:t.description||"",pdfs:[...t.pdfs||[]],room:t.marker?.room_id?t.space+"#@"+t.marker.room_id:t.space&&t.area?t.space+"#"+t.area:"",hideFromPlan:!0===t.marker?.hidden,busy:!1}}else this.host._markerDialog={name:"",binding:"virtual",bindingMode:"virtual",bindingOpen:!1,showEntities:!1,bindingFilter:"",icon:"",autoIcon:"",display:"badge",rippleColor:"",rippleSize:1.5,size:1,angle:0,tapAction:"info",tapActionTouched:!1,originalHasTapAction:!1,originalTapAction:void 0,tapHintAnnouncement:"",toggleEntity:"",toggleEntityTouched:!1,originalHasToggleEntity:!1,originalToggleEntity:void 0,tapTarget:"",tapConfirm:!1,runFilter:"",controls:[],controlsFilter:"",lightRole:"auto",lightRoleTouched:!1,originalHasIsLight:!1,originalIsLight:void 0,lightEntity:"",lightEntityTouched:!1,originalHasLightEntity:!1,originalLightEntity:void 0,glowMode:"auto",glowColor:this.host._fillColors.glow_light.c,glowBrightness:100,glowColorDrafted:!1,glowBrightnessDrafted:!1,glowTouched:!1,originalHasGlowColor:!1,originalGlowColor:void 0,valueBadgeEnabled:!1,valueBadgeSource:null,valueBadgePosition:"right",valueBadgeTouched:!1,originalHasValueBadge:!1,originalValueBadge:void 0,useClimateTemp:!1,glowRadius:"",model:"",link:"",description:"",pdfs:[],room:"",hideFromPlan:!1,busy:!1,uploadId:"up_"+Date.now().toString(36)+Math.random().toString(36).slice(2,6)};else this.host._showToast(this.host._t("toast.marker_needs_server"))}_runCandidates(){const t=[];for(const e of as)for(const[s,i]of Object.entries(this.host.hass.states))s.startsWith(e+".")&&this.host._planEntityAvailable(s)&&t.push({value:s,label:i?.attributes?.friendly_name||s,sub:this.host._t("run."+e)});return t.sort((t,e)=>t.sub.localeCompare(e.sub)||t.label.localeCompare(e.label))}_bindingCandidates(){const t=ca({hass:this.host._planHass,devices:this.host._devices,markers:this.host._markers,showEntities:!!this.host._markerDialog?.showEntities,currentBinding:this.host._markerDialog?.binding,currentDeviceId:this.host._markerDialog?.devId,labels:{device:this.host._t("marker.sub_device"),z2mGroup:this.host._t("marker.sub_z2m_group"),group:this.host._t("marker.sub_group"),helper:this.host._t("marker.sub_helper"),entity:this.host._t("marker.sub_entity")}}),e=(this.host._markerDialog?.bindingFilter||"").toLowerCase().trim(),s=e?t.filter(t=>(t.label+" "+t.sub+" "+t.value).toLowerCase().includes(e)):t;return s.sort((t,e)=>t.label.localeCompare(e.label)),s.slice(0,200)}_backupErrorText(t){const e=t?.code??t?.error;if("string"==typeof e){const t=`backup.error.${e}`,s=this.host._t(t);if(s!==t)return s}return this.host._errText(t)}async _pickMarkerFiles(t){const e=t.target,s=e.files?[...e.files]:[];if(e.value="",!s.length||!this.host._markerDialog)return;const i=this.host._markerDialog.uploadId||this.host._markerDialog.devId||"new",o=[];for(const t of s)try{const e=new FormData;e.append("marker_id",i),e.append("file",t,t.name);const s=this.host.hass?.fetchWithAuth?await this.host.hass.fetchWithAuth("/api/houseplan/upload",{method:"POST",body:e}):await fetch("/api/houseplan/upload",{method:"POST",body:e,headers:this.host.hass?.auth?.data?.access_token?{authorization:`Bearer ${this.host.hass.auth.data.access_token}`}:{}}),a=await s.json().catch(()=>({}));if(!s.ok||a.error){const t={too_large:this.host._t("err.too_large",{mb:a.max_mb||50}),bad_ext:this.host._t("err.bad_ext"),unauthorized:this.host._t("err.unauthorized")};throw new Error(t[a.error]||a.error||"HTTP "+s.status)}o.push({name:a.name||t.name,url:a.url})}catch(e){this.host._showToast(this.host._t("toast.file_failed",{name:t.name,err:this.host._errText(e)}))}o.length&&this.host._markerDialog&&(this.host._markerDialog={...this.host._markerDialog,pdfs:[...this.host._markerDialog.pdfs,...o]},this.host._showToast(this.host._t("toast.files_attached",{n:o.length})))}_removeMarkerPdf(t){this.host._markerDialog&&(this.host._markerDialog={...this.host._markerDialog,pdfs:this.host._markerDialog.pdfs.filter(e=>e.url!==t)})}_markerLightFields(t){const e={};if(t.lightRoleTouched?"always"===t.lightRole?e.is_light=!0:"never"===t.lightRole&&(e.is_light=!1):t.originalHasIsLight&&(e.is_light=t.originalIsLight??null),t.lightEntityTouched?t.lightEntity&&(e.light_entity=t.lightEntity):t.originalHasLightEntity&&(e.light_entity=t.originalLightEntity??null),t.glowTouched){if("auto"!==t.glowMode){const s={c:l(t.glowColor,this.host._fillColors.glow_light.c)};"fixed"===t.glowMode&&(s.bri=Math.max(.01,Math.min(1,Math.round(t.glowBrightness)/100))),e.glow_color=s}}else t.originalHasGlowColor&&(e.glow_color=t.originalGlowColor??null);return e}_markerTapActionFields(t){return t.tapActionTouched?{tap_action:t.tapAction||null}:t.originalHasTapAction?{tap_action:t.originalTapAction??null}:{}}_markerToggleEntityFields(t){return(e={touched:t.toggleEntityTouched,originalHas:t.originalHasToggleEntity,original:t.originalToggleEntity,value:t.toggleEntity}).touched?e.value?{toggle_entity:e.value}:{}:e.originalHas?{toggle_entity:e.original??null}:{};var e}async _saveMarker(){const t=this.host._markerDialog;if(!t||t.busy)return;const e=this._effectiveMarkerTapAction(t);if("ha"===t.bindingMode&&(!t.binding||"virtual"===t.binding))return;if("virtual"===t.binding&&!t.name.trim())return void this.host._showToast(this.host._t("toast.virtual_name_required"));if("run"===e&&!t.tapTarget)return void this.host._showToast(this.host._t("toast.run_target_required"));if(t.valueBadgeTouched&&t.valueBadgeEnabled&&!t.valueBadgeSource)return void this.host._showToast(this.host._t("toast.value_badge_source_required"));if("ha"===t.bindingMode){const e=this.host._bindingStatus(t.binding),s=t.devId?this.host._markers.find(e=>e.id===t.devId):null,i=!s||s.binding!==t.binding;if("active"!==e.kind&&i)return void this.host._showToast(this.host._t("ha_disabled"===e.kind?"toast.ha_disabled_add":"toast.ha_binding_unverified"));if("ha_disabled"===e.kind&&!0===s?.hidden&&!t.hideFromPlan)return this.host._markerDialog={...t,hideFromPlan:!0},void this.host._showToast(this.host._t("entity"===e.reason?"toast.ha_disabled_show_entity":"toast.ha_disabled_show_device"))}const s=this.host._serverCfg;if(!s)return;const i=s.markers||[],o=ns(t.room);let a=o?.space||null;const n=o?.area||null,r=o?.roomId||null,l=Ze(t.binding,t.devId,()=>"v_"+Date.now().toString(36)),h=t.devId,c=h?this.host._devices.find(t=>t.id===h):null,d=o?.space||c?.space||null,p=d?this.host._spaceModelById(d):this.host._spaceModel();if(!p)return;const _=p.id;"virtual"!==t.binding||a||(a=_),this.host._markerDialog={...t,busy:!0};try{const o="virtual"===t.binding?[]:i.filter(e=>e.removed&&e.binding===t.binding).map(t=>t.id),d=o.length>0,u=ss(t.binding,t.controls,this.host._bindingEntities(t.binding)),m=i.find(t=>t.id===l||t.id===h)?.vacuum||null,g={id:l,vacuum:m,binding:t.binding,name:t.name.trim()||null,icon:t.icon||null,display:"badge"!==t.display?t.display:null,ripple_color:"icon_ripple"===t.display&&t.rippleColor?t.rippleColor:null,ripple_size:"icon_ripple"===t.display&&1.5!==t.rippleSize?t.rippleSize:null,size:1!==t.size?t.size:null,angle:t.angle?t.angle:null,...this._markerTapActionFields(t),...this._markerToggleEntityFields(t),tap_target:"run"===e&&t.tapTarget||null,tap_confirm:!!t.tapConfirm||null,controls:u.length?u:null,...this._markerLightFields(t),...this._markerValueBadgeFields(t),use_climate_temp:!!t.useClimateTemp||null,glow_radius_cm:(()=>{const e=sl(t.glowRadius);return null==e||e<=0?null:Math.round(this.host._imperial?30.48*e:100*e)})(),model:t.model.trim()||null,link:t.link.trim()||null,description:t.description.trim()||null,pdfs:t.pdfs,hidden:!!t.hideFromPlan};("virtual"===t.binding||t.room)&&(g.space=a,g.area=n,g.room_id=r);const f=c?.marker?.room_id??null,b=!!t.room&&null!=c&&(c.space!==a||c.area!==n||f!==r);let y=!1;const v=t.uploadId||h;if(v&&v!==l&&g.pdfs?.length)try{const t=await this.host.hass.callWS({type:"houseplan/files/migrate",from_id:v,to_id:l}),e=t?.mapping||{};g.pdfs=rs(g.pdfs,v,l,e),y=Object.keys(e).length>0}catch(t){this.host._showToast(this.host._t("toast.files_migrate_failed",{err:this.host._errText(t)}))}s.markers=i,h&&h!==l&&(s.markers=ls(s.markers,h,l)),h&&h!==l&&"derived_marker_state"===g.value_badge?.source?.kind&&g.value_badge.source.ref===`marker:${h}`&&(g.value_badge.source={kind:"derived_marker_state",ref:`marker:${l}`}),s.markers=s.markers.filter(t=>t.id!==l&&t.id!==h&&("virtual"===g.binding||t.binding!==g.binding)),s.markers.push(g);let $=null;const w=h?this.host._layout[h]:null,k=w?{s:w.s||c?.space||this.host._space,x:w.x,y:w.y}:h&&c&&this.host._defPos[h]?this.host._normPos(c.space,this.host._defPos[h].x,this.host._defPos[h].y):null;if(!d&&k&&k.s===_)l===h&&this.host._layout[l]&&!b||($={s:k.s,x:k.x,y:k.y},this.host._layout={...this.host._layout,[l]:$});else if(d||!this.host._layout[l]||b){let t=p.vb[0]+p.vb[2]/2,e=p.vb[1]+p.vb[3]/2;const s=r?p.rooms.find(t=>t.id===r):n?p.rooms.find(t=>t.area===n):void 0;s&&([t,e]=this.host._roomCenter(s)),$=this.host._normPos(_,t,e),this.host._layout={...this.host._layout,[l]:$}}if(await this._saveConfigNow(),$){const t=hs($);this.host._layout={...this.host._layout,[l]:t},this.host._noteLayoutRev(await this.host.hass.callWS({type:"houseplan/layout/update",device_id:l,pos:t}))}const x=new Set(o);h&&h!==l&&x.add(h),x.delete(l);for(const t of x)delete this.host._layout[t],await this.host.hass.callWS({type:"houseplan/layout/delete",device_id:t}).then(t=>this.host._noteLayoutRev(t)).catch(()=>{});y&&v&&await this.host.hass.callWS({type:"houseplan/files/cleanup",marker_id:v}).catch(()=>{}),this._closeMarkerDialog(),this.host._regSignature="",this.host._maybeRebuildDevices(),this.host._showToast(this.host._t("toast.marker_saved"))}catch(t){this.host._markerDialog&&(this.host._markerDialog={...this.host._markerDialog,busy:!1}),this.host._showToast(this.host._t("toast.error",{err:this.host._errText(t)}))}}async _deleteMarker(){const t=this.host._markerDialog;if(!t||t.busy||!t.devId)return;const e=t.devId?this.host._devices.find(e=>e.id===t.devId):null,s=this.host._markers.find(e=>e.id===t.devId);if(!e&&!s)return;const i=t.name||this.host._t("device.fallback");if(!confirm(this.host._t("confirm.remove_marker",{name:i})))return;const o=this.host._serverCfg;o.markers=o.markers||[];const a=o.markers,n=e?.id||s.id,r=e?"virtual"===e.bindingKind?"virtual":e.bindingKind&&e.bindingRef?`${e.bindingKind}:${e.bindingRef}`:"":s.binding;if(!r)return;const l=cs(o.markers,n,r,"virtual"===r);o.markers=ds(l.markers,l.cleanupIds);const h=l.cleanupIds;this.host._markerDialog={...t,busy:!0};try{await this._saveConfigNow();for(const t of h){delete this.host._layout[t],delete this.host._defPos[t],this.host._dirtyPos.delete(t),this.host._sentPos.delete(t);const e=this.host._activityRt.get(t);e&&clearTimeout(e.timer),this.host._activityRt.delete(t),this.host._vacRt.delete(t),delete this.host._vacSrvTrails[t],await this.host.hass.callWS({type:"houseplan/layout/delete",device_id:t}).then(t=>this.host._noteLayoutRev(t)).catch(()=>{}),await this.host.hass.callWS({type:"houseplan/files/cleanup",marker_id:t}).catch(()=>{}),await this.host.hass.callWS({type:"houseplan/trail/delete",marker_id:t}).catch(()=>{})}this.host._deviceInboxReturn&&(this.host._deviceInboxReturn={...this.host._deviceInboxReturn,tab:"readd",anchor:r}),this._closeMarkerDialog(),this.host._infoCard?.id===n&&this.host._closeInfoCard(),this.host._selId===n&&(this.host._selId=null),this.host._drag?.id===n&&(this.host._drag=null),this.host._regSignature="",this.host._maybeRebuildDevices(),this.host._showToast(this.host._t("toast.marker_removed"))}catch(t){this.host._serverCfg===o&&(o.markers=a),this.host._markerDialog&&(this.host._markerDialog={...this.host._markerDialog,busy:!1}),this.host._showToast(this.host._t("toast.error",{err:this.host._errText(t)}))}}_openSpaceDialog(t,e){if(this.host._serverStorage&&this.host._serverCfg)if("edit"===t){const s=this.host._serverCfg.spaces.find(t=>t.id===e);if(!s)return;const i=ps(s),o=s.settings?.custom_fill&&"object"==typeof s.settings.custom_fill?_s(s.settings.custom_fill):null,a="none"===i.fill?{...o||us,a:0}:o;this.host._spaceDialog={mode:t,spaceId:e,title:s.title,planUrl:s.plan_url||null,planFile:null,source:s.plan_url?"file":"draw",showBorders:i.showBorders,showNames:i.showNames,zeroWallStyle:gs(s),displayTouched:!0,hideDecor:i.hideDecor,hideOpenings:i.hideOpenings,roomColor:i.color,roomOpacity:i.opacity,fillMode:"none"===i.fill?"custom":i.fill,customFill:a,glowEnabled:i.glow,bgColor:i.bgColor,bgMode:"static"===s.settings?.bg_mode||"daynight"===s.settings?.bg_mode?s.settings.bg_mode:null,northDeg:Mt({},s.settings),sunRays:"boolean"==typeof s.settings?.sun_rays?s.settings.sun_rays:null,tempMin:i.tempMin,tempMax:i.tempMax,showLqi:i.showLqi??this.host._config?.show_signal??!0,cardFontScale:i.cardFontScale,labelTemp:i.labelTemp,labelHum:i.labelHum,labelLqi:i.labelLqi,labelLight:i.labelLight,cellCm:Number(s.cell_cm)>0?Number(s.cell_cm):5,cellCmInput:ms(Number(s.cell_cm)>0?Number(s.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1}}else this.host._spaceDialog={mode:t,title:"",planUrl:null,planFile:null,...Ri(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:vs,roomOpacity:ys,fillMode:"custom",customFill:{...us,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:bs,tempMax:fs,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:$s(this.host._imperial),cellCmInput:ms($s(this.host._imperial),this.host._imperial),cellCmTouched:!1,busy:!1};else this.host._showToast(this.host._t("toast.integration_missing"))}async _pickPlanFile(t){const e=t.target,s=e.files?.[0];if(!s||!this.host._spaceDialog)return;const i={"image/svg+xml":"svg","image/png":"png","image/jpeg":"jpg","image/webp":"webp"}[s.type]||(s.name.toLowerCase().endsWith(".svg")?"svg":"");if(!i)return void this.host._showToast(this.host._t("toast.plan_formats"));const o=new Uint8Array(await s.arrayBuffer());let a="";for(let t=0;t{const e=new Image;e.onload=()=>t(e.naturalWidth&&e.naturalHeight?e.naturalWidth/e.naturalHeight:1.414),e.onerror=()=>t(1.414),e.src=r});URL.revokeObjectURL(r),this.host._spaceDialog={...this.host._spaceDialog,planFile:{ext:i,b64:n,aspect:l,name:s.name}}}_useServerPlan(t){const e=this.host._spaceDialog;e&&(this.host._spaceDialog={...e,planUrl:t,planFile:null,pickSaved:!1,savedAspect:void 0},this.host._aspectJob=this._readPlanAspect(t))}async _readPlanAspect(t){for(let e=0;e<40;e++){const e=this.host._display(t);if(e){const s=await new Promise(t=>{const s=new Image;s.onload=()=>t(s.naturalWidth&&s.naturalHeight?s.naturalWidth/s.naturalHeight:0),s.onerror=()=>t(0),s.src=e}),i=this.host._spaceDialog;return i&&i.planUrl===t&&Number.isFinite(s)&&s>0?(this.host._spaceDialog={...i,savedAspect:s},s):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 e=this.host._spaceDialog;e?.saved&&(this.host._spaceDialog={...e,saved:e.saved.filter(e=>e.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 o`
${this.host._t("space.loading")}
`;const e=t.saved||[];if(!e.length)return o`
${this.host._t("space.no_saved")}
`;return o`
+ ${e.map(e=>{return o` +
+ +
+ ${e.name} + ${s=e.size,s>=1048576?(s/1048576).toFixed(1)+" MB":Math.round(s/1024)+" KB"}${e.used_by.length?" · "+this.host._t("space.used_by",{list:e.used_by.join(", ")}):""} +
+ + +
`;var s})} +
`}async _saveSpaceDialog(){const t=this.host._spaceDialog;if(!t||t.busy||!t.title.trim())return;if("file"===t.source&&!t.planFile&&!t.planUrl)return void this.host._showToast(this.host._t("toast.plan_required"));const e="create"===t.mode&&0===(this.host._serverCfg?.spaces.length||0);this.host._spaceDialog={...t,busy:!0};try{const s="create"===t.mode?"s"+Date.now().toString(36):t.spaceId;let i=null;if("file"===t.source&&t.planFile){i={url:(await this.host.hass.callWS({type:"houseplan/plan/set",space_id:s,ext:t.planFile.ext,data:t.planFile.b64})).url,aspect:t.planFile.aspect}}let o=t.savedAspect||null;!i&&"file"===t.source&&t.planUrl&&!o&&this.host._aspectJob&&(o=await this.host._aspectJob||null);const a=this.host._serverCfg;let n;if("create"===t.mode)n=Fi(s,t.title.trim()),a.spaces.push(n);else{if(n=a.spaces.find(t=>t.id===s),!n)throw new Error("space "+s+" is gone from the config");n.title=t.title.trim()}i?(n.plan_url=i.url,n.plan_aspect=i.aspect):"file"===t.source&&t.planUrl&&t.planUrl!==n.plan_url&&(n.plan_url=t.planUrl,n.plan_aspect=o),"draw"===t.source&&(n.plan_url=null,n.plan_aspect=null,delete n.plan_x,delete n.plan_y,delete n.plan_scale,delete n.plan_scale_x,delete n.plan_scale_y,delete n.plan_angle),n.settings={...n.settings||{},show_borders:t.showBorders,show_names:t.showNames,hide_decor:t.hideDecor||void 0,hide_openings:t.hideOpenings||void 0,room_color:t.roomColor,room_opacity:t.roomOpacity,bg_color:t.bgColor||void 0,bg_mode:t.bgMode||void 0,north_deg:t.northDeg??void 0,sun_rays:t.sunRays??void 0,fill_mode:t.fillMode,custom_fill:t.customFill||void 0,glow_enabled:t.glowEnabled,temp_min:Number.isFinite(t.tempMin)?Math.min(t.tempMin,t.tempMax):bs,temp_max:Number.isFinite(t.tempMax)?Math.max(t.tempMin,t.tempMax):fs,show_lqi:t.showLqi,card_font_scale:1!==t.cardFontScale?t.cardFontScale:void 0,label_temp:t.labelTemp,label_hum:t.labelHum,label_lqi:t.labelLqi,label_light:t.labelLight},n.zero_wall_style=t.zeroWallStyle,n.cell_cm=Number.isFinite(t.cellCm)&&t.cellCm>0?Math.max(.1,Math.min(el,t.cellCm)):5,await this._saveConfigNow(),this.host._spaceDialog=null,"create"===t.mode&&this.host._commitSpace(n.id),this.host._regSignature="",this.host._maybeRebuildDevices(),this.host._importQueue.length?this._openNextImport():e||this.host._importTotal>0?(this.host._importTotal=0,this.host._commitSpace(this.host._serverCfg.spaces[0]?.id||this.host._space),this._setMode("plan"),this.host._tool="draw",this.host._path=[],this.host._cursorPt=null,this._primeDrawWallField(),this.host._showToast(this.host._t(e&&!this.host._importTotal?"toast.space_added_onboard":"import.done"))):(this.host._showToast("create"===t.mode?this.host._t("toast.space_added"):this.host._t("toast.space_saved")),"create"===t.mode&&("plan"!==this.host._mode?this._setMode("plan"):(this.host._tool="draw",this.host._path=[],this.host._cursorPt=null,this._primeDrawWallField(),this.host._saveNav())))}catch(t){"conflict"!==t?.code&&await this.host._reloadConfigOnly(!0),this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,busy:!1}),this.host._showToast(this.host._t("toast.error",{err:this.host._errText(t)}))}}async _deleteSpace(){const t=this.host._spaceDialog;if(!t||"edit"!==t.mode)return;const e=this.host._serverCfg;if(!e)return;const s=e.spaces.find(e=>e.id===t.spaceId),i=Ni(e,this.host._layout||{},t.spaceId||""),o=1===e.spaces.length&&e.spaces[0]?.id===t.spaceId;if(!i.count||o){if(confirm(this.host._t("confirm.delete_space",{title:s.title}))){this.host._spaceDialog={...t,deleteBlockers:0,busy:!0};try{this.host._saveConfigDebounced.pending()&&this.host._saveConfigDebounced.flush(),this.host._persistLayout.pending()&&this.host._persistLayout.flush(),await this.host._writeChain;const e=await this.host.hass.callWS({type:"houseplan/space/delete",space_id:t.spaceId,expected_config_rev:this.host._cfgRev,expected_layout_rev:this.host._layoutRev}),[s,i]=await Promise.all([this.host.hass.callWS({type:"houseplan/config/get"}),this.host.hass.callWS({type:"houseplan/layout/get"})]);this.host._adoptStructuralResponses(s,i),this.host._cfgRev=e?.config_rev??this.host._cfgRev,this.host._layoutRev=e?.layout_rev??this.host._layoutRev,this.host._spaceDialog=null,this.host._space===t.spaceId&&this.host._commitSpace(this.host._serverCfg.spaces[0]?.id||""),this.host._regSignature="",this.host._maybeRebuildDevices(),this.host._showToast(this.host._t("toast.space_deleted"))}catch(e){"conflict"!==e?.code&&"space_in_use"!==e?.code||await Promise.all([this.host._reloadConfigOnly(!0),this.host._reloadLayoutOnly()]);const s=this.host._serverCfg;if(this.host._spaceDialog&&s){const e=Ni(s,this.host._layout||{},t.spaceId||""),i=1===s.spaces.length&&s.spaces[0]?.id===t.spaceId;this.host._spaceDialog={...this.host._spaceDialog,busy:!1,deleteBlockers:i?0:e.count}}this.host._showToast(this.host._t("toast.delete_failed",{err:this.host._errText(e)}))}}}else this.host._spaceDialog={...t,deleteBlockers:i.count}}async _saveConfigNow(){this.host._cfgEpoch++;try{await this._writeConfig()}catch(t){throw t?.physicalGeometryRolledBack?await this._reloadRejectedPhysicalWrite():"conflict"===t?.code&&await this.host._reloadConfigOnly(),t}}_startImport(){const t=this.host._importDialog;if(!t)return;const e=t.floors.filter(t=>t.checked).map(t=>t.name);this.host._importDialog=null,e.length?(this.host._importQueue=e,this.host._importTotal=e.length,this._openNextImport()):this._openSpaceDialog("create")}_openNextImport(){const t=this.host._importQueue.shift();void 0!==t&&(this.host._spaceDialog={mode:"create",title:t,planUrl:null,planFile:null,...Ri(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:vs,roomOpacity:ys,fillMode:"custom",customFill:null,glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:bs,tempMax:fs,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:$s(this.host._imperial),cellCmInput:ms($s(this.host._imperial),this.host._imperial),cellCmTouched:!1,busy:!1})}_skipImport(){this.host._spaceDialog=null,this.host._importQueue.length?this._openNextImport():this.host._importTotal>0&&this.host._model.length&&(this.host._importTotal=0,this.host._commitSpace(this.host._serverCfg.spaces[0]?.id||this.host._space),this._setMode("plan"),this.host._showToast(this.host._t("import.done")))}_renderImportDialog(){const t=this.host._importDialog,e=t.floors.filter(t=>t.checked).length;return o`this.host._importDialog=null}> +
+
${this.host._t("import.hint")}
+ ${t.floors.map((e,s)=>o``)} +
+
+ + + +
+
`}_preflightDiagnostics(t,e){const s=new Map((e?.spaces||[]).map(t=>[String(t?.id||""),t]));return{kind:"houseplan-optimize-preflight",origin:"runtime",cardVersion:Xr,checkedAt:(new Date).toISOString(),preflightFingerprint:t.fingerprint,failures:t.failures.map(t=>({spaceId:t.spaceId,displayName:t.displayName,reason:t.reason,detail:t.detail??null,spaceGeometryFingerprint:s.has(t.spaceId)?jt(s.get(t.spaceId)):null}))}}_reportPreflightFailure(t,e){t.ok||t.fingerprint===this.host._reportedPreflightFingerprint||(this.host._reportedPreflightFingerprint=t.fingerprint,console.warn("[houseplan] optimize preflight failed",this._preflightDiagnostics(t,e)))}_preflightVersionsDiffer(){const t=this.host._haIntegrationVersion;return"string"==typeof t&&t.length>0&&t!==Xr}async _copyPreflightDiagnostics(){const t=this.host._alignDialog?.preflight;if(!t||t.ok)return;const e=JSON.stringify(this._preflightDiagnostics(t,this.host._alignDialog?.config??null),null,2);try{await navigator.clipboard.writeText(e),this.host._preflightClipboardFallback=null,this.host._showToast(this.host._t("gs.preflight_copied"))}catch{this.host._preflightClipboardFallback=e}}_checkOptimizeGeometry(t){return ws(t,{fallbackSpaceName:t=>this.host._t("gs.align_preflight_space",{n:String(t)})})}_checkSpacePhysicalGeometry(t,e,s){return ks(t,e,{fallbackSpaceName:t=>this.host._t("gs.align_preflight_space",{n:String(t)}),captureWallGeometry:s?(t,e)=>s(e):void 0})}_optimizeReferenceContext(t){const e=this.host._haRegistry,s=this.host._fullRegistryHass,i={},o=(...t)=>{for(const e of t){const t=String(e||"").trim();if(t)return t}return""};for(const[t,s]of Object.entries(e.devices||{}))i[t]=o(s?.name_by_user,s?.name,s?.model);for(const[t,s]of Object.entries(e.entities||{}))i[`lg_${t}`]=o(this.host.hass?.states?.[t]?.attributes?.friendly_name,s?.name,s?.original_name);for(const t of this.host._devices)i[t.id]=o(t.name,i[t.id]);for(const t of this.host._serverCfg?.markers||[]){const e=String(t.binding||"").indexOf(":"),a=e>0?t.binding.slice(0,e):"",n=e>0?t.binding.slice(e+1):"",r="device"===a?s?.devices?.[n]:"entity"===a?s?.entities?.[n]:null;i[t.id]=o(t.name,"device"===a?r?.name_by_user:null,r?.name,"entity"===a?this.host.hass?.states?.[n]?.attributes?.friendly_name:null,i[t.id])}return{effectiveAreaByMarker:Object.fromEntries(this.host._devices.filter(t=>!t.virtual&&!!t.area).map(t=>[t.id,t.area])),ownerRoster:{authoritative:e.authoritative,deviceIds:Object.keys(e.devices||{}),entityIds:[...new Set([...Object.keys(e.entities||{}),...Object.keys(this.host.hass?.states||{})])],names:i},removeLiveMissingPositions:t}}_previewAlignDialog(t){if(!this.host._norm||!this.host._serverCfg)return;const e=this.host._serverCfg.spaces||[];let s;try{s=zr(this.host._serverCfg,this.host._layout||{},this._optimizeReferenceContext(t))}catch(t){return void this._showWallModelMigrationBlocked(t)}const i=s.changed?this._checkOptimizeGeometry(s.config):null;i&&this._reportPreflightFailure(i,s.config);const o=Math.ceil(10*s.report.maxShiftCm)/10,a=e.find(t=>null!=t?.id&&String(t.id)===s.report.maxSpace),n=e.length>1&&a?String(a.title||a.id):"";this.host._preflightClipboardFallback=null,this.host._alignDialog={report:s.report,config:s.config,layout:s.layout,cm:o,where:n,preflight:i,changed:s.changed,busy:!1,removeLiveMissingPositions:t}}async _runAlignToGrid(){let t=this.host._alignDialog;if(!t||t.busy||!this.host._serverCfg||!t.changed||!t.preflight?.ok)return;const e=Et(t.config);if(t.preflight.fingerprint!==e){const e=this._checkOptimizeGeometry(t.config);if(this._reportPreflightFailure(e,t.config),t={...t,preflight:e},this.host._alignDialog=t,!e.ok)return}this._clearGeometryGesture(),this.host._alignDialog={...t,busy:!0};try{this.host._saveConfigDebounced.pending()&&this.host._saveConfigDebounced.flush(),await this.host._writeChain;const e=await this.host.hass.callWS({type:"houseplan/plan/optimize",config:t.config,layout:t.layout,expected_config_rev:this.host._cfgRev,expected_layout_rev:this.host._layoutRev});this.host._serverCfg=t.config,this.host._layout=t.layout,this.host._geometryHistory.clear(),this.host._cfgRev=e?.config_rev??this.host._cfgRev+1,this.host._layoutRev=e?.layout_rev??this.host._layoutRev+1,this.host._canOptimizeUndo=!!e?.can_undo,this.host._undoKind=e?.can_undo?"optimize":null,this.host._dirtyPos.clear(),this.host._sentPos.clear(),this.host._cfgEpoch++,this.host._modelCache=null,this.host._frame=null,this.host._regSignature="",this.host._maybeRebuildDevices(),this.host._cacheSnapshot(),this.host._alignDialog=null,this.host._preflightClipboardFallback=null,this.host.requestUpdate(),this.host._showToast(this.host._t("gs.align_done",{n:String(t.report.moved),m:String(t.report.migrated+t.report.canonicalized+t.report.coordsCanonicalized+t.report.latticeCoordinatesCanonicalized+t.report.wallsMerged+t.report.spansMerged+t.report.partitionsMerged+t.report.partitionsReconciled+t.report.openingsRehosted+t.report.wallsStraightened),r:String(t.report.spaceRefsRemapped+t.report.roomRefsRemapped+t.report.positionsRemapped+t.report.markersDetached+t.report.orphanRoomLabelsRemoved+t.report.orphanDevicePositionsRemoved+t.report.orphanGroupPositionsRemoved)}))}catch(t){if(this.host._alignDialog&&(this.host._alignDialog={...this.host._alignDialog,busy:!1}),"wall_model_client_outdated"===t?.code)return void this.host._showToast(this.host._t("toast.wall_model_client_outdated"));"conflict"===t?.code&&await Promise.all([this.host._reloadConfigOnly(!0),this.host._reloadLayoutOnly()]),this.host._showToast(this.host._t("toast.error",{err:this.host._errText(t)}))}}async _undoPlanOptimization(){if(!this.host._canOptimizeUndo||this.host._optimizeUndoBusy)return;const t=this.host._undoKind;this._clearGeometryGesture(),this.host._optimizeUndoBusy=!0,this.host.requestUpdate();try{await this.host.hass.callWS({type:"houseplan/plan/optimize_undo",expected_config_rev:this.host._cfgRev,expected_layout_rev:this.host._layoutRev});const[e,s]=await Promise.all([this.host.hass.callWS({type:"houseplan/config/get"}),this.host.hass.callWS({type:"houseplan/layout/get"})]);this.host._serverCfg=e?.config||this.host._serverCfg,this.host._cfgRev=e?.rev??this.host._cfgRev,this.host._layout=s?.layout||this.host._layout,this.host._geometryHistory.clear(),this.host._layoutRev=s?.rev??this.host._layoutRev,this.host._canOptimizeUndo=!1,this.host._undoKind=null,this.host._cfgEpoch++,this.host._modelCache=null,this.host._frame=null,this.host._regSignature="",this.host._maybeRebuildDevices(),this.host._cacheSnapshot(),this.host.requestUpdate(),this.host._showToast(this.host._t("import"===t?"backup.import_undone":"gs.optimize_undone"))}catch(t){this.host._canOptimizeUndo=!1,this.host._undoKind=null,this.host._showToast(this.host._t("toast.error",{err:this.host._errText(t)}))}finally{this.host._optimizeUndoBusy=!1,this.host.requestUpdate()}}async _runBackupExport(){const t=this.host._backupExportDialog;if(t&&!t.busy){this.host._backupExportDialog={...t,busy:!0,error:""};try{const e=await this.host.hass.callWS({type:"houseplan/export/create",kind:t.kind,space_id:"space"===t.kind?this.host._space:void 0,..."space"===t.kind&&t.planOnly?{plan_only:!0}:{},card_version:Xr}),s=new Blob([JSON.stringify(e.document,null,2)+"\n"],{type:"application/json;charset=utf-8"}),i=URL.createObjectURL(s),o=document.createElement("a");o.href=i,o.download=e.filename||`houseplan-${t.kind}.json`,o.style.display="none",document.body.append(o),o.click(),o.remove(),window.setTimeout(()=>URL.revokeObjectURL(i),0),this.host._backupExportDialog=null,this.host._showToast(this.host._t("backup.export_done"))}catch(t){this.host._backupExportDialog&&(this.host._backupExportDialog={...this.host._backupExportDialog,busy:!1,error:this._backupErrorText(t)})}}}async _pickBackupImport(t){const e=t.target,s=e.files?.[0];if(e.value="",s){this.host._settingsDialog=null,this.host._backupImportDialog={filename:s.name,size:s.size,token:"",preview:null,expectedConfigRev:this.host._cfgRev,expectedLayoutRev:this.host._layoutRev,duplicatePolicy:"skip",confirmMissing:!1,busy:!0,error:""};try{this.host._saveConfigDebounced.pending()&&this.host._saveConfigDebounced.flush(),await this.host._writeChain,this.host._persistLayout.pending()&&this.host._persistLayout.flush();const t="/api/houseplan/import/preview?duplicate_policy=skip",e={method:"POST",body:s,headers:{"content-type":"application/json"}},i=this.host.hass?.fetchWithAuth?await this.host.hass.fetchWithAuth(t,e):await fetch(t,{...e,headers:{...e.headers,...this.host.hass?.auth?.data?.access_token?{authorization:`Bearer ${this.host.hass.auth.data.access_token}`}:{}}}),o=await i.json().catch(()=>({}));if(!i.ok||o.error)throw Object.assign(new Error(o.message||o.error||`HTTP ${i.status}`),{code:o.error});this.host._backupImportDialog&&(this.host._backupImportDialog={...this.host._backupImportDialog,token:o.token,preview:o.preview,expectedConfigRev:o.expected_config_rev,expectedLayoutRev:o.expected_layout_rev,busy:!1})}catch(t){this.host._backupImportDialog&&(this.host._backupImportDialog={...this.host._backupImportDialog,busy:!1,error:this._backupErrorText(t)})}}}async _setBackupDuplicatePolicy(t){const e=this.host._backupImportDialog;if(e&&!e.busy&&e.token&&t!==e.duplicatePolicy){this.host._backupImportDialog={...e,duplicatePolicy:t,busy:!0,error:""};try{const s=await this.host.hass.callWS({type:"houseplan/import/revalidate",token:e.token,duplicate_policy:t});this.host._backupImportDialog&&(this.host._backupImportDialog={...this.host._backupImportDialog,preview:{...e.preview,...s.preview},expectedConfigRev:s.expected_config_rev,expectedLayoutRev:s.expected_layout_rev,confirmMissing:!1,busy:!1})}catch(t){this.host._backupImportDialog&&(this.host._backupImportDialog={...this.host._backupImportDialog,duplicatePolicy:e.duplicatePolicy,preview:e.preview,confirmMissing:e.confirmMissing,busy:!1,error:this._backupErrorText(t)})}}}async _applyBackupImport(){const t=this.host._backupImportDialog;if(!t||t.busy||!t.token||!t.preview)return;if(t.preview.confirmation_required&&!t.confirmMissing)return;this.host._backupImportDialog={...t,busy:!0,error:""};const e=this.host._space;try{const s=await this.host.hass.callWS({type:"houseplan/import/apply",token:t.token,expected_config_rev:t.expectedConfigRev,expected_layout_rev:t.expectedLayoutRev,duplicate_policy:t.duplicatePolicy,confirm_missing_content:t.confirmMissing}),[i,o]=await Promise.all([this.host.hass.callWS({type:"houseplan/config/get"}),this.host.hass.callWS({type:"houseplan/layout/get"})]);this.host._adoptStructuralResponses(i,o),this.host._geometryHistory.clear(),this.host._dirtyPos.clear(),this.host._sentPos.clear(),this.host._defPos={},this.host._cfgEpoch++,this.host._modelCache=null,this.host._frame=null,this.host._visibleDeviceSnapshot=null,this.host._candidateDeviceSnapshot=null,this.host._stagedDeviceSnapshotToken=-1,this.host._capturedSnapshotConfigEpoch=-1,this.host._regSignature="",this.host._signer.invalidate(this.host.hass),this.host._resign(),this.host._maybeRebuildDevices();const a=this.host._serverCfg?.spaces||[],n="space"===s.kind&&s.space_id?s.space_id:a.some(t=>t.id===e)?e:a[0]?.id||this.host._space;this.host._hasFixedFloor?this.host._adoptInitialSpace(this.host._model,!0):this.host._commitSpace(n),this.host._backupImportDialog=null,this.host._cacheSnapshot(),this.host.requestUpdate();const r="space"===s.kind?t.preview.counts:s.counts;this.host._showToast(this.host._t("space"===s.kind?"backup.space_done":"backup.full_done",{spaces:String(r?.spaces||0),rooms:String(r?.rooms||0),markers:String(r?.markers||0),refs:String(s.repaired_target_refs||0)}))}catch(t){if("conflict"===t?.code&&this.host._backupImportDialog?.token)try{const t=this.host._backupImportDialog,e=await this.host.hass.callWS({type:"houseplan/import/revalidate",token:t.token,duplicate_policy:t.duplicatePolicy});return void(this.host._backupImportDialog={...t,preview:{...t.preview,...e.preview},expectedConfigRev:e.expected_config_rev,expectedLayoutRev:e.expected_layout_rev,confirmMissing:!1,busy:!1,error:this.host._t("backup.revalidated")})}catch(e){t=e}this.host._backupImportDialog&&(this.host._backupImportDialog={...this.host._backupImportDialog,busy:!1,error:this._backupErrorText(t)})}}_renderBackupExportDialog(){const t=this.host._backupExportDialog,e=(this.host._serverCfg?.spaces||[]).find(t=>t.id===this.host._space);return o`this.host._backupExportDialog=null}> +
+
${this.host._t("backup.export_hint")}
+ + + ${"space"===t.kind&&e?o``:a} +
${this.host._t("backup.privacy_warning")}
+ ${t.error?o``:a} +
+
+ + + +
+
`}_renderBackupImportDialog(){const t=this.host._backupImportDialog,e=t.preview,s=e?.counts||{},i=e?.reference_report||{},n=t=>Object.values(t||{}).reduce((t,e)=>t+(Number(e)||0),0),r=[["incoming_remapped",n(i.remapped?.incoming)],["target_repaired",n(i.remapped?.target)],["preserved_unresolved",n(i.preservedUnresolved)],["collisions",n(i.collisions)],["dropped_links",n(i.droppedIncomingLinks)],["bounded_lineages",Number(i.boundedLineages)||0]].filter(([,t])=>t>0);return o`this.host._backupImportDialog=null}> +
+
${t.filename}${(t.size/1024).toFixed(1)} KB
+ ${t.busy&&!e?o`
${this.host._t("backup.reading")}
`:a} + ${t.error?o``:a} + ${e?o` +
+ ${this.host._t("full"===e.kind?"backup.full":"backup.current_space")} + ${e.plan_only?o`${this.host._t("backup.plan_only_preview")}`:a} + ${this.host._t("same"===e.source?"backup.same_source":"backup.foreign_source")} + ${this.host._t("backup.created",{value:e.created_at||"—"})} + ${this.host._t("backup.versions",{card:e.card_version||"—",integration:e.integration_version||"—",model:String(e.model_version??"—")})} +
+
+ ${this.host._t("backup.count_spaces",{n:"full"===e.kind?`${e.current_counts?.spaces||0} → ${s.spaces||0}`:String(s.spaces||0)})} + ${this.host._t("backup.count_rooms",{n:"full"===e.kind?`${e.current_counts?.rooms||0} → ${s.rooms||0}`:String(s.rooms||0)})} + ${this.host._t("backup.count_walls",{n:"full"===e.kind?`${e.current_counts?.walls||0} → ${s.walls||0}`:String(s.walls||0)})} + ${this.host._t("backup.count_openings",{n:"full"===e.kind?`${e.current_counts?.openings||0} → ${s.openings||0}`:String(s.openings||0)})} + ${this.host._t("backup.count_decor",{n:"full"===e.kind?`${e.current_counts?.decor||0} → ${s.decor||0}`:String(s.decor||0)})} + ${this.host._t("backup.count_markers",{n:"full"===e.kind?`${e.current_counts?.markers||0} → ${s.markers||0}`:String(s.markers||0)})} + ${this.host._t("backup.count_layout",{n:"full"===e.kind?`${e.current_counts?.layout||0} → ${s.layout||0}`:String(s.layout||0)})} +
+ ${e.bindings?o`
${this.host._t("backup.bindings",{device:String(e.bindings.device||0),entity:String(e.bindings.entity||0),virtual:String(e.bindings.virtual||0),legacy:String(e.legacy_positions||0)})}
${this.host._t("backup.binding_status",{active:String(e.bindings.active||0),disabled:String(e.bindings.disabled||0),missing:String(e.bindings.missing||0)})}
`:a} + ${e.missing_areas?.length?o`
${this.host._t("backup.missing_areas",{areas:e.missing_areas.join(", ")})}
`:a} + ${e.dropped_marker_links?o`
${this.host._t("backup.dropped_marker_links",{n:String(e.dropped_marker_links)})}
`:a} + ${e.repaired_target_refs?o`
${this.host._t("backup.repaired_target_refs",{n:String(e.repaired_target_refs)})}
`:a} + ${e.preserved_unresolved_refs?o` +
${this.host._t("backup.preserved_unresolved_refs",{n:String(e.preserved_unresolved_refs)})}
${this.host._t("backup.preserved_unresolved_hint")}
+ `:a} + ${r.length?o`
+ ${this.host._t("backup.import_details")} +
+ ${r.map(([t,e])=>o`${this.host._t(`backup.import_detail.${t}`,{n:String(e)})}`)} + ${(i.examples||[]).slice(0,8).map(t=>o` + ${t.owner} → ${t.reference} + `)} +
+
`:a} + ${"full"===e.kind?o` +
${this.host._t("backup.replace_warning")}
+ ${"foreign"===e.source?o`
${this.host._t("backup.foreign_bookkeeping")}
`:a}`:o` +
${this.host._t("backup.final_name")}${e.space_title}
+
${this.host._t("backup.target_settings")}
+ ${e.duplicates?o`
${this.host._t("backup.duplicates",{n:String(e.duplicates)})} + + +
`:a}`} + ${e.content?.length?o`
+ ${this.host._t("backup.content")} + ${e.content.map(t=>o`${t.url} · ${this.host._t("available"===t.state?"backup.content_available":"external"===t.state?"backup.content_external":"backup.content_detach_required")}`)} +
`:a} + ${e.confirmation_required?o``:a} + `:a} +
+
+ + + ${e?o``:a} +
+
`}_setFillColor(t,e){const s=this.host._settingsDialog;this.host._settingsDialog={...s,colors:{...s.colors,[t]:{...s.colors[t],...e}}}}async _saveSettingsDialog(){const t=this.host._settingsDialog;if(t&&!t.busy){this.host._settingsDialog={...t,busy:!0};try{const e=this.host._serverCfg,s=JSON.stringify(t.colors)===JSON.stringify(xs),i={...e.settings};s?delete i.fill_colors:i.fill_colors=t.colors;const o=this.host._imperial?30.48*t.glowRadius:100*t.glowRadius;Number.isFinite(o)&&o>0&&300!==Math.round(o)?i.glow_radius_cm=Math.round(o):delete i.glow_radius_cm,t.bgColor?i.bg_color=t.bgColor:delete i.bg_color,null!==t.northDeg&&Number.isInteger(t.northDeg)&&t.northDeg>=0&&t.northDeg<=359?i.north_deg=t.northDeg:delete i.north_deg,i.bg_mode=t.bgMode,t.sunRays?i.sun_rays=!0:delete i.sun_rays,delete i.weather_entity,this.host._serverCfg={...e,settings:i},await this._saveConfigNow(),this.host._settingsDialog=null,this.host.requestUpdate(),this.host._showToast(this.host._t("gs.saved"))}catch(t){this.host._settingsDialog&&(this.host._settingsDialog={...this.host._settingsDialog,busy:!1}),this.host._showToast(this.host._t("toast.error",{err:this.host._errText(t)}))}}}_boolInput(t,e,s=!1){const i=t=>e(!!t.target.checked);return customElements.get("ha-switch")?o``:o``}_rangeInput(t,e,s,i,n,r=!1,l){const h=t=>{const e=Number(t.target.value);Number.isFinite(e)&&n(e)};return customElements.get("ha-slider")?o``:o``}_renderColorRow(t,e){const s=this.host._settingsDialog.colors[t];return o`
+ {this._setFillColor(t,{c:e.detail.color,a:e.detail.opacity})}}> +
`}_renderAlignDialog(){const t=this.host._alignDialog,e=t.report,s=t.changed&&!t.preflight?.ok,i=t.preflight?.failures||[],n=i.slice(0,3).map(t=>t.displayName),r=n.length?n.join(", "):this.host._t("gs.align_preflight_space",{n:"1"}),l=Math.max(0,i.length-n.length),h=l?this.host._t("gs.align_preflight_more",{n:String(l)}):"",c=e.spaceRefsRemapped+e.roomRefsRemapped+e.positionsRemapped+e.markersDetached,d=e.migrated+e.canonicalized+e.coordsCanonicalized+e.wallSegmentsMigrated+e.wallsMerged+e.spansMerged+e.partitionsMerged+e.partitionsReconciled+e.openingsRehosted+e.redundantDraftsRemoved,p=e.moved+e.rotated+e.removedDrafts+e.coordsCanonicalized+e.wallsStraightened,_=Math.ceil(10*e.maxStraightenShiftCm)/10,u=(this.host._serverCfg?.spaces||[]).find(t=>String(t?.id||"")===e.maxStraightenSpace),m=(this.host._serverCfg?.spaces||[]).length>1&&u?String(u.title||u.id):"",g=e.orphanRoomLabelsRemoved+e.orphanDevicePositionsRemoved+e.orphanGroupPositionsRemoved,f=e.liveMissingPositions.map(t=>t.name).filter(Boolean),b=f.slice(0,3).join(", "),y=Math.max(0,f.length-3),v=b?this.host._t("gs.optimize_live_names",{names:b,more:y?this.host._t("gs.optimize_reference_more",{n:String(y)}):""}):"",$=e.unverifiedPositions.some(t=>"registry_unavailable"===t.reason),w=t=>e.removedPositions.some(e=>e.id===t.id)?this.host._t("gs.optimize_detail_removed"):e.liveMissingPositions.some(e=>e.id===t.id)?this.host._t("gs.optimize_detail_live"):this.host._t("gs.optimize_detail_unverified"),k=t=>this.host._t("room_label"===t?"gs.optimize_detail_room_label":"group"===t?"gs.optimize_detail_group":"device"===t?"gs.optimize_detail_device":"gs.optimize_detail_unknown"),x=[...e.removedPositions,...e.liveMissingPositions.filter(t=>!e.removedPositions.some(e=>e.id===t.id)),...e.unverifiedPositions],S=x.slice(0,10),M=Math.max(0,x.length-S.length);return o`{this.host._alignDialog=null,this.host._preflightClipboardFallback=null}}> +
+ ${s?o` +

${this.host._t("gs.align_preflight_failed",{spaces:r,more:h})}

+ ${i.slice(0,10).map(t=>o`

+ ${t.displayName}: ${this.host._t(`gs.preflight_reason_${t.reason}`)} +

`)} + ${i.length>10?o`

+ ${this.host._t("gs.align_preflight_more",{n:String(i.length-10)})} +

`:a} +
${this.host._t("gs.align_preflight_hint")}
+ ${this._preflightVersionsDiffer()?o` +
${this.host._t("gs.preflight_update_hint")}
`:a} +
+ +
+ ${this.host._preflightClipboardFallback?o`
+ ${this.host._t("gs.preflight_copy")} +
${this.host._preflightClipboardFallback}
+
`:a}`:t.changed?o` + ${e.moved?o`

${this.host._t("gs.align_count",{n:String(e.moved),total:String(e.total),cm:String(t.cm)})}

`:a} + ${e.latticeCoordinatesCanonicalized?o` +

${this.host._t("gs.optimize_lattice_summary",{n:String(e.latticeCoordinatesCanonicalized),cm:Ss(e.latticeMaxShiftCm)})}

+ ${e.latticeSpaces.map(t=>o`

${this.host._t("gs.optimize_lattice_space",{space:t.space,n:String(t.canonicalized),far:String(t.far)})}

`)} + `:a} + ${t.where?o`

${this.host._t("gs.align_where",{s:t.where})}

`:a} + ${e.rotated?o`

${this.host._t("gs.align_turned",{n:String(e.rotated)})}

`:a} + ${e.removedDrafts?o`

${this.host._t("gs.align_removed_drafts",{n:String(e.removedDrafts)})}

`:a} + ${e.redundantDraftsRemoved?o`

${this.host._t("gs.optimize_redundant_drafts",{n:String(e.redundantDraftsRemoved)})}

`:a} + ${e.wallSegmentsMigrated?o`

${this.host._t("gs.wall_segments_migrated",{n:String(e.wallSegmentsMigrated)})}

`:a} + ${e.legacyZeroWallsMigrated?o`

${this.host._t("gs.zero_walls_migrated",{n:String(e.legacyZeroWallsMigrated)})}

`:a} + ${d?o`

${this.host._t("gs.optimize_changes",{m:String(e.migrated),c:String(e.canonicalized),p:String(e.coordsCanonicalized),w:String(e.wallsMerged),s:String(e.spansMerged),i:String(e.partitionsMerged)})}

`:a} + ${e.partitionsReconciled?o`

${this.host._t("gs.optimize_coincident_partitions",{n:String(e.partitionsReconciled)})}

`:a} + ${e.openingsRehosted?o`

${this.host._t("gs.optimize_openings_rehosted",{n:String(e.openingsRehosted)})}

`:a} + ${e.wallsStraightened?o`

${this.host._t("gs.optimize_walls_straightened",{n:String(e.wallsStraightened),cm:String(_)})}

`:a} + ${m?o`

${this.host._t("gs.optimize_walls_straightened_where",{s:m})}

`:a} + ${e.glowSpacesMigrated||e.glowRoomsMigrated?o`

${this.host._t("gs.optimize_glow_migration",{spaces:String(e.glowSpacesMigrated),rooms:String(e.glowRoomsMigrated)})}

`:a} + ${p?o`
${this.host._t("gs.align_warn")}
`:a}`:o`

${this.host._t(e.liveMissingPositions.length||e.unverifiedPositions.length||e.nestedRefsUnresolved?"gs.optimize_no_automatic_changes":"gs.align_none")}

`} + ${!s&&e.wallsStraightenSkipped?o`

${this.host._t("gs.optimize_walls_straighten_skipped",{n:String(e.wallsStraightenSkipped)})}

`:a} + ${c?o`

${this.host._t("gs.optimize_references",{spaces:String(e.spaceRefsRemapped),rooms:String(e.roomRefsRemapped),positions:String(e.positionsRemapped),detached:String(e.markersDetached)})}

`:a} + ${g?o`

${this.host._t("gs.optimize_orphans_removed",{total:String(g),rooms:String(e.orphanRoomLabelsRemoved),devices:String(e.orphanDevicePositionsRemoved),groups:String(e.orphanGroupPositionsRemoved)})}

`:a} + ${e.liveMissingPositions.length?o`
+

${this.host._t(t.removeLiveMissingPositions?"gs.optimize_live_positions_remove":"gs.optimize_live_positions",{n:String(e.liveMissingPositions.length),names:v})}

+ + ${t.removeLiveMissingPositions?o`
+ ${this.host._t("gs.optimize_live_selected")} +
`:a} +
`:a} + ${e.unverifiedPositions.length?o``:a} + ${e.nestedRefsUnresolved?o``:a} + ${x.length?o`
+ ${this.host._t("gs.optimize_details")} +
    + ${S.map(t=>o`
  • ${this.host._t("gs.optimize_detail_item",{status:w(t),kind:k(t.kind),id:t.id,space:t.spaceId})}
  • `)} +
+ ${M?o`
${this.host._t("gs.optimize_details_more",{n:String(M)})}
`:a} +
`:a} +
+
+ + + ${t.changed&&t.preflight?.ok?o` + `:a} +
+
`}_renderSettingsDialog(){return o`this.host._settingsDialog=null}> +
+
${this.host._t("gs.hint")}
+ + ${this._renderColorRow("light_on","gs.light_on")} + ${this._renderColorRow("light_off","gs.light_off")} + ${this._renderColorRow("light_none","gs.light_none")} + + ${this._renderColorRow("temp_cold","gs.temp_cold")} + ${this._renderColorRow("temp_ok","gs.temp_ok")} + ${this._renderColorRow("temp_hot","gs.temp_hot")} + + ${this._renderColorRow("lqi_low","gs.lqi_low")} + ${this._renderColorRow("lqi_high","gs.lqi_high")} + + ${this._renderColorRow("glow_base","gs.glow_base")} + ${this._renderColorRow("glow_light","gs.glow_light")} +
+ ${this.host._t("gs.glow_radius")} + {const e=sl(t.target.value);null!=e&&e>0&&(this.host._settingsDialog={...this.host._settingsDialog,glowRadius:e})}} /> + ${this.host._imperial?this.host._t("gs.unit_ft"):this.host._t("gs.unit_m")} +
+ + ${this._renderColorRow("wall_fill","gs.wall_fill")} + +
+ ${this.host._t("gs.bg_mode")} + +
+ ${"static"===this.host._settingsDialog.bgMode?o`
+ {this.host._settingsDialog={...this.host._settingsDialog,bgColor:t.detail.color}}}> + ${this.host._settingsDialog.bgColor?o``:o`${this.host._t("gs.bg_theme")}`} +
`:o`
${this.host._t("gs.bg_daynight_hint")}
`} + + ${Ms(this.host.hass)?a:o`
${this.host._t("gs.sun_missing")}
`} +
+ ${this.host._renderCompass()} +
+ ${this.host._t("gs.north")} +
+ {const e=t.target.value.trim(),s=""===e?null:Math.round(Number(e));this.host._settingsDialog={...this.host._settingsDialog,northDeg:null!==s&&Number.isFinite(s)?Math.min(359,Math.max(0,s)):null}}} /> + ${null!==this.host._settingsDialog.northDeg?o``:a} +
+ ${null===this.host._settingsDialog.northDeg?o`
${this.host._t("gs.north_hint")}
`:a} +
+
+ + ${this.host._canEdit?o` + +
${this.host._t("gs.backup_hint")}
+
+ + + + + + ${this.host._canOptimizeUndo&&"import"===this.host._undoKind?o` + `:a} +
`:a} + +
${this.host._t("gs.grid_hint")}
+
+ +
+ ${this.host._canOptimizeUndo&&"import"!==this.host._undoKind?o`
+ +
`:a} + +
${this.host._t("gs.about_version",{v:Xr})}
+ + ${this.host._t("gs.about_github")} + + ${this.host._t("gs.about_telegram")} +
+
+ + + + +
+
`}_rulesSet(t){this.host._rulesDialog={...this.host._rulesDialog,rules:t}}async _saveRules(){const t=this.host._rulesDialog;if(!t||t.busy)return;const e=t.rules.filter(t=>t.pattern.trim()&&t.icon.trim());this.host._rulesDialog={...t,busy:!0};try{const t=this.host._serverCfg,s=JSON.stringify(e)===JSON.stringify(Ct),i={...t.settings};s?delete i.icon_rules:i.icon_rules=e,this.host._serverCfg={...t,settings:i},await this._saveConfigNow(),this.host._rulesDialog=null,this.host._regSignature="",this.host._maybeRebuildDevices(),this.host._showToast(this.host._t("rules.saved"))}catch(t){this.host._rulesDialog&&(this.host._rulesDialog={...this.host._rulesDialog,busy:!1}),this.host._showToast(this.host._t("toast.error",{err:this.host._errText(t)}))}}_renderRulesDialog(){const t=this.host._rulesDialog,e=Ds(t.rules),s=t.test.trim()?Cs(t.test,"",e):null,i=(e,s)=>{const i=[...t.rules],o=e+s;o<0||o>=i.length||([i[e],i[o]]=[i[o],i[e]],this._rulesSet(i))};return o`this.host._rulesDialog=null}> +
+
${this.host._t("rules.hint")}
+
+ this.host._rulesDialog={...t,test:e.target.value}} /> + ${s?o`${s}`:a} +
+ ${t.rules.map((e,s)=>{const a=""!==e.pattern.trim()&&!Ps(e.pattern);return o`
+ {const o=[...t.rules];o[s]={...e,pattern:i.target.value},this._rulesSet(o)}} /> + {const o=[...t.rules];o[s]={...e,icon:i.target.value},this._rulesSet(o)}} /> + + i(s,-1)}> + i(s,1)}> + this._rulesSet(t.rules.filter((t,e)=>e!==s))}> +
`})} + +
+
+ + + + +
+
`}_saveKioskScale(t){this.host._kioskScale={...this.host._kioskScale,...t};try{localStorage.setItem("houseplan_card_kiosk_v1",JSON.stringify(this.host._kioskScale))}catch{}this.host.requestUpdate()}_renderKioskDialog(){const t=this.host._kioskScale,e=(e,s)=>o` +
+ ${this._rangeInput(50,300,5,Math.round(100*t[e]),t=>this._saveKioskScale({[e]:t/100}))} + ${Math.round(100*t[e])}% +
`;return o`this.host._kioskDialog=!1}> +
+
${this.host._t("kiosk.hint")}
+ ${e("icon",this.host._t("kiosk.icon_scale"))} + ${e("font",this.host._t("kiosk.font_scale"))} +
+
+ + + +
+
`}_renderVacSection(t){const e=this.host._devices.find(e=>e.id===t.devId);if(!e||!this.host._isVacDev(e))return a;const s=e.marker?.vacuum||{},i=this.host._vacAllCamerasFor===e.id,n=this.host._vacSourceResolution(e,i),r=n.entityId,l=n.candidates.find(t=>t.entityId===r)||null,h=r?Is(this.host.hass?.states[r]?.attributes):null,c=!1!==s.live&&("ok"===n.status||"unsupported"===n.status),d=this._vacPlanRoomAnchors(e.space),p=h?Ts(h.rooms,d.map(t=>t.name)):0,_=!!(c&&h&&p>=3),u=Object.keys(s.calibration||{}),m=t=>{const s=this.host._vacEnsureMarker(e);s&&(s.vacuum={...s.vacuum||{},...t},this.host._regSignature="",this.host._maybeRebuildDevices(),this._saveConfig(),this.host.requestUpdate())},g=new Set(e.entities||[]),f=n.candidates.filter(t=>g.has(t.entityId)||t.entityId===s.source),b=i?n.candidates.filter(t=>t.entityId.startsWith("camera.")&&!f.some(e=>e.entityId===t.entityId)):[],y=t=>[t.hasPosition?this.host._t("vac.cap_position"):"",t.hasRooms?this.host._t("vac.cap_rooms_short"):"",t.hasPath?this.host._t("vac.cap_path"):"",t.hasMapId?this.host._t("vac.cap_map"):""].filter(Boolean).join(" · ")||this.host._t("vac.cap_none"),v=t=>o` + `,$=n.candidates.some(t=>g.has(t.entityId)&&"known_xcme_incomplete"===t.category)||!!n.pinned&&!!r?.startsWith("camera.")&&!l?.hasPosition;return o` + +
+
+
${this.host._t("vac.diag_source")}${r||this.host._t("vac.source_none")}
+ ${l?.platform?o`
${this.host._t("vac.diag_platform")}${l.platform}
`:a} +
${this.host._t("vac.diag_status")}${this.host._t(`vac.source_status_${n.status}`)}
+
${this.host._t("vac.diag_position")}${h?.pos?this.host._t("common.yes"):this.host._t("common.no")}
+
${this.host._t("vac.diag_rooms")}${Rs(this.host._t("vac.diag_rooms_value"),{total:String(h?.rooms.length||0),matched:String(p),readiness:this.host._t(p>=3?"vac.autocal_ready":"vac.autocal_not_ready")})}
+
${this.host._t("vac.diag_path")}${h?.path.length?this.host._t("common.yes"):this.host._t("common.no")}
+
${this.host._t("vac.diag_map")}${h?.mapId??"default"}
+
+ ${"missing"===n.status||"disabled"===n.status||"unverified"===n.status?o`
+ ${this.host._t(`vac.source_banner_${n.status}`)} + +
`:a} + ${$?o`
+ ${this.host._t("vac.xcme_hint")} +
attributes:
+  - vacuum_position
+  - rooms
+  - path
+  - map_name
+
`:a} +
+ ${this.host._t("vac.choose_source")} +
+ + ${f.map(v)} +
{t.currentTarget.open?this.host._vacOpenAllCameras(e):(this.host._vacAllCamerasFor=null,this.host._vacAllCameraCache=null)}}> + ${this.host._t("vac.all_cameras")} +
${this.host._t("vac.all_cameras_warn")}
+ ${i?b.length?b.map(v):o`
${this.host._t("vac.all_cameras_empty")}
`:a} +
+
+
+
+ ${_?o``:a} + ${c?o``:a} + ${this.host._t("vac.documentation")} +
+ ${h?o` + + + + ${u.length?o`
${Rs(this.host._t("vac.cal_maps"),{maps:u.join(", ")})}
`:a} + `:a} +
`}_vacMapId(t,e,s=this.host._planHass){const i=this.host._vacEntity(t),o=i?s?.states?.[i]?.attributes?.selected_map:null;return Ns(e.mapId,o)}_vacSaveMatrix(t,e,s,i){const o=this.host._devices.find(e=>e.id===t),a=o?this.host._vacEnsureMarker(o):this.host._serverCfg?.markers?.find(e=>e.id===t);if(!a)return!1;const n={...a.vacuum||{}};return n.source=e,n.calibration={...n.calibration||{},[s]:i.map(t=>Number(t.toFixed(6)))},a.vacuum=n,this.host._regSignature="",this.host._maybeRebuildDevices(),this._saveConfig(),this.host.requestUpdate(),!0}_vacPlanRoomAnchors(t){return(this.host._spaceModelById(t)?.rooms||[]).map(t=>({room:t,poly:R(t)})).filter(({room:t,poly:e})=>t.name&&e).map(({room:t,poly:e})=>{const s=zs(e);return s?{name:String(t.name),cx:s[0],cy:s[1]}:null}).filter(Boolean)}_vacAutoCalibrate(t){const e=this.host._vacSource(t),s=e?Is(this.host.hass?.states[e]?.attributes):null;if(!e||!s||s.rooms.length<3)return void this.host._showToast(this.host._t("vac.autocal_no_rooms"));const i=this._vacPlanRoomAnchors(t.space),o=As(s.rooms,i);if(!o)return void this.host._showToast(this.host._t("vac.autocal_no_match"));const a=this.host._serverCfg?.spaces?.find(e=>e.id===t.space),n=Number(a?.cell_cm),r=Number.isFinite(n)&&n>0?n:5,l=Os(o.residual,this.host._gridPitch,r),h=this._vacMapId(t,s);l>Ls?this.host._vacCalConfirm={markerId:t.id,source:e,mapId:h,matrix:o.matrix,rooms:o.matched.length,error:ee(l,"mi"===this.host.hass?.config?.unit_system?.length)}:this._vacSaveMatrix(t.id,e,h,o.matrix)&&this.host._showToast(Rs(this.host._t("vac.autocal_done"),{rooms:String(o.matched.length)}))}_vacApplyCalibrationProposal(t){const e=this.host._vacCalConfirm;if(e){if(this.host._vacCalConfirm=null,t){const t=this.host._devices.find(t=>t.id===e.markerId),s=Es(e.matrix);if(!t||!s)return;if(this.host._markerDialog=null,t.space!==this.host._space&&!this.host._commitSpace(t.space))return;return void(this.host._vacFit={markerId:e.markerId,source:e.source,mapId:e.mapId,p:s,drag:null})}this._vacSaveMatrix(e.markerId,e.source,e.mapId,e.matrix)&&this.host._showToast(Rs(this.host._t("vac.autocal_done"),{rooms:String(e.rooms)}))}}_vacStartFit(t){const e=this.host._vacSource(t),s=e?Is(this.host.hass?.states[e]?.attributes):null;if(!e||!s)return void this.host._showToast(this.host._t("vac.cal_need_pos"));const i=this._vacMapId(t,s),o=t.marker?.vacuum?.calibration?.[i],a=this.host._spaceModelById(t.space);if(!a)return;const n=a.vb,r=o&&6===o.length&&Es(o)||Bs(s.rooms,n);this.host._markerDialog=null,(t.space===this.host._space||this.host._commitSpace(t.space))&&(this.host._vacFit={markerId:t.id,source:e,mapId:i,p:r,drag:null})}_vacFitSave(){const t=this.host._vacFit;if(!t)return;const e=this._vacSaveMatrix(t.markerId,t.source,t.mapId,Hs(t.p));this.host._vacFit=null,e&&this.host._showToast(this.host._t("vac.cal_done"))}_vacFitTurn(t){const e=this.host._vacFit;if(!e)return;const s=Is(this.host.hass?.states[e.source]?.attributes),i=this._vacGhostCentre(s?.rooms||[]),o={...e.p,...t};this.host._vacFit={...e,p:Gs(o,e.p,i[0],i[1])}}_vacGhostCentre(t){const e=[],s=[];for(const i of t)e.push(i.x0??i.cx,i.x1??i.cx),s.push(i.y0??i.cy,i.y1??i.cy);return e.length?[(Math.min(...e)+Math.max(...e))/2,(Math.min(...s)+Math.max(...s))/2]:[0,0]}_vacDelta(t,e,s){const i=this.host._stageEl,o=i?.clientWidth||1,a=i?.clientHeight||1;return[e/o*t.w,s/a*t.h]}_vacFitPointer(t,e){const s=this.host._vacFit;if(!s)return;if(t.stopPropagation(),"pointerdown"===t.type){const e=t.target,i=e.getAttribute?.("data-corner");try{t.currentTarget.setPointerCapture?.(t.pointerId)}catch{}return void(this.host._vacFit={...s,drag:i?{kind:"scale",sx:t.clientX,sy:t.clientY,p0:{...s.p},fx:Number(i.split(",")[0]),fy:Number(i.split(",")[1])}:{kind:"move",sx:t.clientX,sy:t.clientY,p0:{...s.p},fx:0,fy:0}})}const i=s.drag;if(i){if("pointermove"===t.type){const[o,a]=this._vacDelta(e,t.clientX-i.sx,t.clientY-i.sy);if("move"===i.kind)this.host._vacFit={...s,p:{...i.p0,ox:i.p0.ox+o,oy:i.p0.oy+a}};else{const t=Is(this.host.hass?.states[s.source]?.attributes),e=this._vacGhostCentre(t?.rooms||[]),n=Hs(i.p0),[r,l]=Ws(n,e[0],e[1]),[h,c]=Ws(n,i.fx,i.fy),d=Math.hypot(r-h,l-c)||1,[p,_]=[2*r-h,2*l-c],u=Math.hypot(p+2*o-h,_+2*a-c)/2,m=Math.max(.05,u/d),g={...i.p0,s:i.p0.s*m};this.host._vacFit={...s,p:Gs(g,i.p0,i.fx,i.fy)}}return}"pointerup"!==t.type&&"pointercancel"!==t.type||(this.host._vacFit={...s,drag:null})}}_renderVacFit(t){const e=this.host._vacFit;if(!e)return a;const s=Is(this.host._renderPlanHass?.states?.[e.source]?.attributes);if(!s)return a;const i=Hs(e.p),n=[],r=[],l=[];for(const t of s.rooms){if(null==t.x0)continue;const e=[[t.x0,t.y0],[t.x1,t.y0],[t.x1,t.y1],[t.x0,t.y1]].map(([t,e])=>Ws(i,t,e));e.forEach(([t,e])=>{r.push(t),l.push(e)});const[s,o]=Ws(i,t.cx,t.cy);n.push(ce` + ${t.name}`)}let h=a;if(s.pos){const[e,o]=Ws(i,s.pos.x,s.pos.y);h=ce``}const c=[];if(r.length){const e=(()=>{const t=i[0]*i[4]-i[1]*i[3];return(e,s)=>[(i[4]*(e-i[2])-i[1]*(s-i[5]))/t,(-i[3]*(e-i[2])+i[0]*(s-i[5]))/t]})(),s=Math.min(...r),o=Math.max(...r),a=Math.min(...l),n=Math.max(...l),h=.022*t.w,d=h/4;for(const[t,i,r,l]of[[s,a,o,n],[o,a,s,n],[o,n,s,a],[s,n,o,a]]){const s=e(r,l);c.push(ce` + `)}}return o`this._vacFitPointer(e,t)} + @pointermove=${e=>this._vacFitPointer(e,t)} + @pointerup=${e=>this._vacFitPointer(e,t)} + @pointercancel=${e=>this._vacFitPointer(e,t)}>${n}${h}${c}`}_resetRoomDialogFields(){this.host._roomEditId=null,this.host._roomFill="",this.host._roomCustomFill=null,this.host._roomTempSrc="",this.host._roomHumSrc="",this.host._roomSrcOpen=null,this.host._roomSrcFilter="",this.host._roomNameScale=1,this.host._roomLabelScale=1}_openRoomEdit(t){if(!t.id)return;this.host._roomEditId=t.id,this.host._nameSel=t.name||"",this.host._areaSel=t.area||"",this.host._roomFill="glow"===t.settings?.fill_mode?"":t.settings?.fill_mode||"";const e=t.settings?.custom_fill;this.host._roomCustomFill=e&&"object"==typeof e?_s(e,ps(this.host._curSpaceCfg).customFill):null,this.host._roomTempSrc=t.settings?.temp_source||"",this.host._roomHumSrc=t.settings?.hum_source||"",this.host._roomNameScale=js(t.settings?.name_scale),this.host._roomLabelScale=js(t.settings?.label_scale),this.host._roomSrcOpen=null,this.host._roomSrcFilter="",this.host._roomDialog=!0}_roomSettingsFromDialog(){const t={};return this.host._roomFill&&(t.fill_mode=this.host._roomFill),this.host._roomCustomFill&&(t.custom_fill=this.host._roomCustomFill),this.host._roomTempSrc&&(t.temp_source=this.host._roomTempSrc),this.host._roomHumSrc&&(t.hum_source=this.host._roomHumSrc),1!==this.host._roomNameScale&&(t.name_scale=this.host._roomNameScale),1!==this.host._roomLabelScale&&(t.label_scale=this.host._roomLabelScale),Object.keys(t).length?t:null}_saveRoomEdit(){const t=this.host._curSpaceCfg,e=t?.rooms.find(t=>t.id===this.host._roomEditId);if(!e)return this.host._roomDialog=!1,void(this.host._roomEditId=null);e.name=this.host._nameSel.trim()||e.name,e.area=this.host._areaSel||null;const s=e.settings||{},i={...s};this.host._roomFill?i.fill_mode=this.host._roomFill:delete i.fill_mode,this.host._roomCustomFill?i.custom_fill=this.host._roomCustomFill:delete i.custom_fill,"glow"===s.fill_mode&&"boolean"!=typeof s.glow&&(i.glow=!0),this.host._roomTempSrc?i.temp_source=this.host._roomTempSrc:delete i.temp_source,this.host._roomHumSrc?i.hum_source=this.host._roomHumSrc:delete i.hum_source,1!==this.host._roomNameScale?i.name_scale=this.host._roomNameScale:delete i.name_scale,1!==this.host._roomLabelScale?i.label_scale=this.host._roomLabelScale:delete i.label_scale,Object.keys(i).length?e.settings=i:delete e.settings,this._saveConfig(),this.host._roomDialog=!1,this.host._roomEditId=null,this.host._nameSel="",this.host._areaSel="",this.host._regSignature="",this.host._maybeRebuildDevices(),this.host.requestUpdate(),this.host._showToast(this.host._t("toast.room_updated"))}_roomSrcCandidates(){const t=this.host._planHass,e=x(this.host._markers),s=this.host._roomSrcFilter.trim().toLowerCase(),i=[];for(const o of Object.values(t.devices)){if("service"===o.entry_type)continue;if(e.devices.has(o.id))continue;const t=(o.name_by_user||o.name||o.id).trim();s&&!t.toLowerCase().includes(s)||i.push({value:"device:"+o.id,label:t,sub:o.model||this.host._t("marker.sub_device")})}for(const[o,a]of Object.entries(t.entities)){if(!o.startsWith("sensor.")||a.hidden)continue;if(S(t,o,e))continue;const n=a.name||t.states[o]?.attributes?.friendly_name||o;s&&!(n+" "+o).toLowerCase().includes(s)||i.push({value:"entity:"+o,label:n,sub:o})}return i.sort((t,e)=>t.label.localeCompare(e.label)),i.slice(0,200)}_roomSrcLabel(t){const e=t.indexOf(":"),s=t.slice(0,e),i=t.slice(e+1);return"device"===s?this.host._fullRegistryHass.devices[i]?.name_by_user||this.host._fullRegistryHass.devices[i]?.name||i:this.host._fullRegistryHass.entities[i]?.name||this.host.hass.states[i]?.attributes?.friendly_name||i}_labelPos(t,e){const s=this.host._layout["rl_"+(t.id||"")];if(s&&s.s===e)return{x:s.x*ol,y:s.y*ol};const i=this._snap(this.host._roomCenter(t));return{x:i[0],y:i[1]}}_labelDown(t,e,s){if("plan"!==this.host._mode)return;t.preventDefault(),t.stopPropagation();const i=this._labelPos(e,s);this.host._drag={id:"rl_"+(e.id||""),sx:t.clientX,sy:t.clientY,ox:i.x,oy:i.y,moved:!1},nl(t),this.host._tip=null}_labelMove(t,e,s){const i="rl_"+(e.id||"");if(!this.host._drag||this.host._drag.id!==i)return;const o=this.host._stageEl;if(!o)return;const a=this.host._spaceModelById(s);if(!a)return;const n=a.vb,r=o.getBoundingClientRect(),l=this.host._viewOr(n),h=(t.clientX-this.host._drag.sx)/r.width*l.w,c=(t.clientY-this.host._drag.sy)/r.height*l.h;Math.abs(t.clientX-this.host._drag.sx)+Math.abs(t.clientY-this.host._drag.sy)>3&&(this.host._drag.moved=!0);const d=Rt(this.host._drag.ox+h),p=Rt(this.host._drag.oy+c);this.host._savePos({id:i,space:s},d,p)}_labelUp(t){const e="rl_"+(t.id||"");if(!this.host._drag||this.host._drag.id!==e)return;const s=this.host._drag.moved;this.host._drag=s?this.host._drag:null,s&&window.setTimeout(()=>this.host._drag=null,0)}_labelScale(t){const e=this.host._layout["rl_"+(t.id||"")]?.k;return"number"==typeof e&&Number.isFinite(e)?Math.min(3,Math.max(.5,e)):1}_rlResizeDown(t,e,s){if("plan"!==this.host._mode)return;t.preventDefault(),t.stopPropagation();const i=t.target.closest(".roomlabel");if(!i)return;const o=i.getBoundingClientRect(),a=o.left+o.width/2,n=o.top+o.height/2,r=Math.max(8,Math.hypot(t.clientX-a,t.clientY-n));this.host._rlResize={id:"rl_"+(e.id||""),space:s,k0:this._labelScale(e),cx:a,cy:n,d0:r},nl(t)}_rlResizeMove(t){const e=this.host._rlResize;if(!e)return;t.stopPropagation();const s=Math.max(8,Math.hypot(t.clientX-e.cx,t.clientY-e.cy)),i=Math.min(3,Math.max(.5,e.k0*(s/e.d0))),o=this.host._layout[e.id];if(o)this.host._layout={...this.host._layout,[e.id]:{...o,k:i}};else{const t=e.id.slice(3),s=this.host._spaceModelById(e.space);if(!s)return;const o=s.rooms.find(e=>e.id===t);if(!o)return;const a=this._labelPos(o,e.space);this.host._layout={...this.host._layout,[e.id]:{s:e.space,x:a.x/ol,y:a.y/ol,k:i}}}this.host._dirtyPos.add(e.id)}_rlResizeUp(){this.host._rlResize&&(this.host._rlResize=null,this.host._persistLayout())}_renderRoomGear(t,e,s){if(!t.id)return a;let i=null;if(t.poly?(i=this.host._gearPtCache.get(t.poly)||null,i||(i=re(t.poly),this.host._gearPtCache.set(t.poly,i))):null!=t.x&&null!=t.y&&(i=[t.x+(t.w||0)/2,t.y+(t.h||0)/2]),!i)return a;const n=(i[0]-s.x)/s.w*100,r=(i[1]-s.y)/s.h*100;return o``}_alignCandidates(){const t=[],e=this.host._spaceModel();if(this.host._markup){if(!e)return t;if(this.host._drag?.id.startsWith("rl_")){const s=this.host._drag.id.slice(3);for(const i of e.rooms){if(!i.name||i.id===s)continue;const e=this._labelPos(i,this.host._space);t.push([e.x,e.y])}return t}for(const s of e.rooms){const e=R(s);if(e)for(const s of e)t.push(s)}if("draw"===this.host._tool)for(const e of this.host._path)t.push(e);if("split"===this.host._tool&&this.host._splitSel?.pts)for(const e of this.host._splitSel.pts)t.push(e);return t}if("devices"===this.host._mode){for(const e of this.host._devices){if(e.space!==this.host._space||e.id===this.host._drag?.id||"ha_disabled"===e.bindingStatus?.kind)continue;const s=this.host._pos(e);t.push([s.x,s.y])}return t}if("decor"===this.host._mode){const e=this.host._decorMove?.id;return t.push(...this._decorSnapGeometry(e).points),this.host._decorDraft&&t.push(this.host._decorDraft.a),t}return t}_renderAlignGuides(){const t=this.host._alignPoint;if(!t)return ce``;const e=this.host._drag?.id.startsWith("rl_")?.5*this.host._gridPitch:.05*this.host._gridPitch,s=qs(t,this._alignCandidates(),e);if(!s.length)return ce``;const i=this.host._gridPitch,o=1.5*i;return ce` + ${s.map(e=>{const[s,a,n,r]="x"===e.axis?[e.at,e.from[1],e.at,t[1]+Math.sign(t[1]-e.from[1])*o]:[e.from[0],e.at,t[0]+Math.sign(t[0]-e.from[0])*o,e.at];return ce` + `})} + `}_renderOpeningCenterTick(t){const e=(t.angle+90)*Math.PI/180,s=ze(15,this.host._cellCm);return ce``}_renderOpeningDimensionGuides(t){const e=t.labels.flatMap(t=>t.dimension?[t.dimension]:[]);if(!e.length)return ce``;const s=this._cssPxToRender(4);return ce``}_renderOpeningPlacementPreview(){const t=this.host._openingPreview;if(!t)return ce``;const e={type:t.type,length:t.renderedLength,angle:t.angle,amount:Us(t.type,null),flipH:t.flipH,flipV:t.flipV,base:"var(--hp-open)",tone:"var(--hp-open)",cellCm:this.host._cellCm,gridPitch:this.host._gridPitch,face:t.face},s="passage"===t.type?Ks(t,ze(this.host._gridPitch,this.host._cellCm)):null;return ce` + `}_renderOpeningDialog(){const t=this.host._openingDialog,e=this.host._spaceModel()?.partitions||[],s=t.id?this.host._curSpaceCfg?.openings?.find(e=>e.id===t.id):null,i={id:t.id||"preview",type:t.type,x:t.x/ol,y:t.y/this.host._spaceH,angle:t.angle,length:s&&!t.lengthTouched?s.length:this.host._cmToUnits(Math.max(20,t.lengthCm))/ol,...t.host?{host:t.host}:{}},n=qe(s,i),r=t.host?n?H(i,e,ol,this.host._cellCm,this.host._gridPitch):F(i,e,ol,this.host._cellCm,this.host._gridPitch):null,l="does-not-fit-jamb"===r?.reason,h=t.host?e.find(e=>e.id===t.host.id):null,c=h?ee(h.cm/2,this.host._imperial):"",d=!!t.host&&!r?.resolved&&!l,p="gate"===t.type?"mdi:gate":"window"===t.type?"mdi:window-closed-variant":"passage"===t.type?"mdi:arch":"mdi:door",_=(e,s)=>{const i="contact"===e?t.contact:t.lock,n="contact"===e?!!t.contactOpen:!!t.lockOpen,r="contact"===e?t.contactFilter||"":t.lockFilter||"",l=s.find(t=>t.value===i),h=l?.label||this.host.hass.states[i]?.attributes?.friendly_name||this.host._fullRegistryHass.entities[i]?.name||i,c=Ii(s,r);return o` + + ${n?o`
+ this._filterOpeningEntities(e,t.target.value)} /> +
+ + ${c.map(t=>o` + `)} + ${c.length?a:o`
${this.host._t("marker.nothing_found")}
`} +
+
`:a}`};return o`this.host._openingDialog=null}> +
+ ${t.host?o` +
+ ${d||l?o``:a} + ${d?this.host._t("opening.partition_orphan"):l?this.host._t("opening.partition_jamb_margin",{distance:c}):this.host._t("opening.host_partition")} +
`:a} + + + + + + + + {const s=sl(e.target.value);null!=s&&(this.host._openingDialog={...t,lengthCm:s,lengthTouched:!0})}} /> + + ${"passage"===t.type&&(t.contact||t.lock)?o`
+ + ${this.host._t("opening.passage_binding_warning")} +
`:a} + + ${"passage"!==t.type?o` + ${_("contact",this._contactCandidates())} + ${t.contact?o``:a}`:a} + + ${"door"===t.type||"gate"===t.type?o` + ${_("lock",this._lockCandidates())}`:a} + + ${"gate"!==t.type&&"passage"!==t.type?o``:a} + ${"passage"!==t.type?o``:a} +
+ +
`}_gridLevels(){const t=this.host._stageEl,e=this.host._viewOr(this.host._baseVb()),s=t&&t.clientWidth&&e.w?t.clientWidth/e.w:1;return Ys(this.host._gridPitch,s)}_renderMarkupDefs(t){const e=this._gridLevels();if(!e)return ce``;const s=this.host._gridPitch*e.fine,i=this.host._gridPitch*e.coarse,o=this.host._gridPitch*e.fine*.14;return ce` + + + + + + + + + + + + + + `}_renderPhysicalEditorLayer(){const t=this.host._spaceModel();if(!t)return ce``;const e=this.host._gridPitch,s=this.host._viewOr(this.host._baseVb()),i=this.host._stageEl,o=i?.clientWidth?s.w/i.clientWidth:e/8,n=Math.max(.22*e,8*o),r=Math.max(n,12*o),l=t=>`M ${t.map(t=>`${t[0]} ${t[1]}`).join(" L ")} Z`,h=(t,e)=>this.host._physicalSel?.kind===t&&this.host._physicalSel.id===e||"column"===t&&this.host._duplicateColumnId===e,c=(t.room_drafts||[]).flatMap(t=>t.points.slice(0,-1).map((e,s)=>{const i=t.points[s+1],a=Number(t.segments[s]?.cm),n=j(Number.isFinite(a)?a:15,this.host._cellCm,this.host._gridPitch);return ce`{this.host._physicalSel={kind:"draft",id:t.id,segment:s},this._registerPhysicalTap("draft",t.id,s)}}>`})),d=(t.partitions||[]).map(t=>{const e=Yt(t.a,t.b,t.cm,this.host._cellCm,this.host._gridPitch);return e?ce`this._physicalDown(e,"partition",t.id)} + @pointermove=${t=>this._physicalMove(t)} + @pointerup=${t=>this._physicalUp(t)}>`:ce`this._physicalDown(e,"partition",t.id)} + @pointermove=${t=>this._physicalMove(t)} + @pointerup=${t=>this._physicalUp(t)}>`}),p=(t.wall_columns||[]).map(t=>{const e=this.host._physicalRotate?.id===t.id?{...t,angle:this.host._physicalRotate.angle}:t,s=Jt(e,this.host._cellCm,this.host._gridPitch);return ce`this._physicalDown(e,"column",t.id)} + @pointermove=${t=>this._physicalMove(t)} + @pointerup=${t=>this._physicalUp(t)}>`}),_=this.host._physicalDrag,u=(()=>{if(!_?.moved)return a;if("partition"===_.kind&&0===Number(_.base.cm)){const t=_.base;return ce``}const t="partition"===_.kind?Yt(_.base.a,_.base.b,_.base.cm,this.host._cellCm,this.host._gridPitch):Jt(_.base,this.host._cellCm,this.host._gridPitch);return t?ce``:a})(),m=(()=>{const s=this.host._physicalSel;if(!s)return a;if("draft"===s.kind){const e=t.room_drafts.find(t=>t.id===s.id);return e?ce` + t.join(",")).join(" ")}> + ${e.points.map(t=>ce``)} + `:a}if("partition"===s.kind){const e=t.partitions.find(t=>t.id===s.id);if(!e)return a;const i=Yt(e.a,e.b,e.cm,this.host._cellCm,this.host._gridPitch),o=(e.a[0]+e.b[0])/2,r=(e.a[1]+e.b[1])/2;return ce` + ${i?ce``:ce``} + + + + `}const i=t.wall_columns.find(t=>t.id===s.id);if(!i)return a;const h=this.host._physicalRotate?.id===i.id?{...i,angle:this.host._physicalRotate.angle}:i,c=Jt(h,this.host._cellCm,this.host._gridPitch);if("square"!==h.shape)return ce` + + + `;const d=j(h.cm,this.host._cellCm,this.host._gridPitch),p=At(h.angle)*Math.PI/180,_=Math.sin(p),u=-Math.cos(p),m=[h.center[0]+_*d/2,h.center[1]+u*d/2],g=[m[0]+_*Math.max(e,24*o),m[1]+u*Math.max(e,24*o)];return ce` + + + + this._physicalRotateDown(t,i)} + @pointermove=${t=>this._physicalRotateMove(t)} + @pointerup=${t=>this._physicalRotateUp(t)}> + `})();return ce`${c}${d}${p}${u}${m}`}_renderHiddenWallDiagnosticOverlay(){if(!this.host._markup)return ce``;const t=this._hiddenWallDiagnosticSnapshot().value;if(!t.segments.length)return ce``;const e=j(5,this.host._cellCm,this.host._gridPitch);return ce``}_renderPlanSnapOverlay(){if(!this.host._markup)return ce``;const t=this._planSnapGeometrySnapshot().value,e=this.host._activePlanSnapCandidate,s=new Set(this.host._activePlanSnapConflicts.map(t=>t.key)),i=j(5,this.host._cellCm,this.host._gridPitch),o=j(10,this.host._cellCm,this.host._gridPitch);return ce``}_syncPlanSnapActiveMarker(t){const e=this.host.renderRoot?.querySelector('[data-hp="plan-snap-active-marker"]');if(e){if(e.setAttribute("class",`plan-snap-node${t?" active":""}${"line"===t?.kind?" dynamic":""}`),e.setAttribute("data-active",t?"true":"false"),e.setAttribute("visibility",t?"visible":"hidden"),!t)return e.removeAttribute("data-kind"),void e.removeAttribute("data-key");e.setAttribute("data-kind",t.kind),e.setAttribute("data-key",t.key),e.setAttribute("cx",String(t.point[0])),e.setAttribute("cy",String(t.point[1])),e.setAttribute("r",String(j(10,this.host._cellCm,this.host._gridPitch)))}}_syncPlanSnapConflictMarkers(t){const e=new Set(t.map(t=>t.key));for(const t of this.host.renderRoot?.querySelectorAll('.plan-snap-node[data-kind="endpoint"]')||[])t.setAttribute("class","plan-snap-node"+(e.has(t.dataset.key||"")?" conflict":""))}_planSnapPhysicalSegment(t){let e=0;const s=this.host._spaceModel();if(!s)return null;if("partition"===t.sourceKind)e=Number(s.partitions.find(e=>e.id===t.sourceId)?.cm)||0;else if("draft"===t.sourceKind){const i=/^(.*):(\d+)$/.exec(t.sourceId),o=i?s.room_drafts.find(t=>t.id===i[1]):null;e=o&&i&&Number(o.segments[Number(i[2])]?.cm)||0}else e=Xs(s.rooms,this.host._spaceWalls,this.host._openCuts(),[t.a[0],t.a[1],t.b[0],t.b[1]],this.host._wallKeyPitch,this.host._cellCm,this.host._gridPitch,ol);return e>0?{a:[...t.a],b:[...t.b],halfDepth:j(e,this.host._cellCm,this.host._gridPitch)/2}:null}_drawPreviewJoinPatchD(t,e){if(t.length<2)return"";const s=[];for(let i=0;i+10&&s.push({a:t[i],b:t[i+1],halfDepth:e[i]});if(!s.length)return"";const i=2e-4*this.host._gridPitch,o=this._planSnapGeometrySnapshot().value.segments.filter(e=>t.some(t=>C(t,[e.a[0],e.a[1],e.b[0],e.b[1]])<=i)).map(t=>this._planSnapPhysicalSegment(t)).filter(t=>!!t);return Zs([...s,...o],i).map(t=>`M ${t.map(t=>`${t[0]} ${t[1]}`).join(" L ")} Z`).join(" ")}_renderMarkupLayer(t){const e=this.host._openCuts(),s=this.host._thickWallCuts(),i=e.concat(s),o=i.length?D(this.host._segments,i,.02*this.host._gridPitch):this.host._segments,n=this.host._path,r=this.host._gridPitch,l=this.host._viewOr(this.host._baseVb()),h="draw"===this.host._tool?this.host._drawWallCm:null,c=(()=>"draw"===this.host._tool&&n.length&&null!=h?this.host._contourClosed?n:this.host._cursorPt?[...n,this.host._cursorPt]:n.length>=2?n:null:null)(),d=c?Ra(c.length-1,this.host._contourClosed?[...this.host._draftSegmentCms,this.host._closingWallCm??void 0]:this.host._draftSegmentCms,h,A).map(t=>j(t,this.host._cellCm,this.host._gridPitch)/2):[],p=c?Qs(c,j(h,this.host._cellCm,this.host._gridPitch)/2,this.host._contourClosed,d):"",_=c?this._drawPreviewJoinPatchD(c,d):"",u=this.host._wallFaceBatch?.candidates[this.host._wallFaceBatch.index]?.repair||this.host._wallRepairDiagnostic;return ce` + ${this._gridLevels()?ce``:a} + ${o.map(t=>ce``)} + ${this._renderPhysicalEditorLayer()} + ${"column"===this.host._tool&&this.host._cursorPt&&this.host._drawWallCm?ce`{const t={shape:"square",center:this.host._cursorPt,cm:this.host._drawWallCm};return`M ${Jt(t,this.host._cellCm,this.host._gridPitch).map(t=>`${t[0]} ${t[1]}`).join(" L ")} Z`})()}>`:a} + ${p?ce` + `:a} + ${c&&0===h?ce`t.join(",")).join(" ")}>`:a} + ${_?ce` + `:a} + ${u?ce``:a} + ${"split"===this.host._tool&&this.host._splitSel?.pts?.length?ce`${this.host._splitSel.pts.length>1?ce``:a} + ${this.host._splitSel.pts.map((t,e)=>ce``)} + ${this.host._cursorPt?ce``:a}`:a} + `}_renderActiveChainInk(){const t=this.host._path,e=this.host._gridPitch;return ce` + ${t.length>1?ce``:a} + ${t.length&&this.host._cursorPt&&"draw"===this.host._tool&&!this.host._contourClosed?ce` + ${this.host._activePlanSnapCandidate||this.host._activePlanSnapConflicts.length?a:ce``}`:a} + ${t.map((t,s)=>ce``)} + `}_renderPartitionDeleteDialog(){const t=this.host._partitionDeleteDialog,e="mi"===this.host.hass?.config?.unit_system?.length;return o`this.host._partitionDeleteDialog=null}> +
+

${this.host._t("confirm.delete_partition_openings_body",{count:t.openings.length})}

+
    + ${t.openings.map(t=>o`
  • ${this.host._t("confirm.delete_partition_openings_item",{type:this.host._t(`opening.${t.type}`),length:ee(t.length*ol/this.host._gridPitch*this.host._cellCm,e)})}
  • `)} +
+
+
+ + + +
+
`}_renderRoomDeleteDialog(){const t=this.host._roomDeleteDialog;return o`this.host._roomDeleteDialog=null}> +
+

${this.host._t("confirm.delete_room_body")}

+
+
+ + + + +
+
`}_renderPhysicalDialog(){const t=this.host._physicalDialog,e="column"===t.kind;return o`this.host._physicalDialog=null}> +
+ ${e?o` + `:a} + +
this.host._physicalDialog={...t,cm:e.target.value}} /> + ${this.host._t(this.host._imperial?"wallthick.unit_in":"wallthick.unit_cm")}
+ ${e&&"square"===t.shape?o` + + this.host._physicalDialog={...t,angle:e.target.value}} />`:a} + ${t.length?o`
${this.host._t("physical.length")}: ${t.length}
`:a} +
+ +
`}_renderMarkupBar(){const t=this.host._geometryHistory.undoName,e=this.host._geometryHistory.redoName,s=t?this.host._t("history.undo_named",{name:t}):this.host._t("history.undo_empty"),i=e?this.host._t("history.redo_named",{name:e}):this.host._t("history.redo_empty");return o`
+
+ + + + + + + + + + ${this.host._editorToolbarGroups.map(t=>this._renderEditorGroupLauncher(t))} + + + + +
+
+ +
+
`}_renderDevicesBar(){return o`
+
+ + + + ${this.host._editorToolbarGroups.map(t=>this._renderEditorGroupLauncher(t))} +
+
+ +
+
`}_renderDeviceInbox(){const t=this.host._deviceInbox,e=this._deviceInboxRows(),s=Object.fromEntries(["on_plan","available","hidden","readd"].map(t=>[t,e.filter(e=>e.category===t).length])),i=function(t,e,s,i=!1){const o=s.trim().toLocaleLowerCase();return t.filter(t=>t.category===e&&(!i||t.isNew)&&(!o||t.searchText.includes(o)))}(e,t.tab,t.search,"on_plan"===t.tab&&t.onlyNew),n=i.slice(0,t.limit),r=t=>this.host._t(`device_inbox.tab_${t}`),l=`device_inbox.empty_${t.tab}`;return o`this.host._deviceInbox=null}> +
+
+ this.host._deviceInbox={...t,search:e.target.value,limit:100}} /> + +
+
+ ${["on_plan","available","hidden","readd"].map(e=>o` + `)} +
+
+ ${"on_plan"===t.tab?o``:a} + ${"available"===t.tab?o``:a} + +
+
+ ${n.length?n.map(t=>{const e="on_plan"===t.category?t.canFind?o``:o``:"hidden"===t.category?o``:o``,s="active"===t.status.kind?"":this.host._t(`device_inbox.status_${t.status.kind}`);return o`
+ +
+
${t.name} + ${t.isNew?o`${this.host._t("device_inbox.new")}`:a} +
+
+ ${[t.model,t.integration,t.spaceName,t.areaName].filter(Boolean).join(" · ")} +
+
+ ${this.host._t(`device_inbox.reason_${t.reason}`)} + ${s?o`${s}`:a} +
+ ${t.binding} +
+
+ ${e} + ${t.canEdit||t.canHide||"available"===t.category||"hidden"===t.category||this.host._bindingHasHaPage(t.binding)?o` +
+ + + +
+ ${t.canEdit&&("on_plan"!==t.category||t.canFind)?o``:a} + ${t.canHide?o``:a} + ${"available"===t.category?o``:a} + ${"hidden"===t.category?o``:a} + ${this.host._bindingHasHaPage(t.binding)?o``:a} +
+
`:a} +
+
`}):o`
${this.host._t(l)}
`} +
+ ${i.length>n.length?o``:a} +
+
+ +
+
`}_markerValueBadgeFields(t){return ti({touched:t.valueBadgeTouched,originalHas:t.originalHasValueBadge,original:t.originalValueBadge,enabled:t.valueBadgeEnabled,source:t.valueBadgeSource,position:t.valueBadgePosition})}_markerDraft(t){if("ha"===t.bindingMode&&(!t.binding||"virtual"===t.binding))return null;const e=ns(t.room),s=Ze(t.binding,t.devId,()=>"__hp_device_preview__"),i=this.host._markers.find(e=>e.id===s||e.id===t.devId),o=ss(t.binding,t.controls,this.host._bindingEntities(t.binding)),a=this._effectiveStoredTapAction(t),n={id:s,binding:t.binding,name:t.name.trim()||null,icon:t.icon||null,display:"badge"!==t.display?t.display:null,ripple_color:"icon_ripple"===t.display&&t.rippleColor?t.rippleColor:null,ripple_size:"icon_ripple"===t.display&&1.5!==t.rippleSize?t.rippleSize:null,size:1!==t.size?t.size:null,angle:t.angle||null,...this._markerTapActionFields(t),...this._markerToggleEntityFields(t),tap_target:"run"===a&&t.tapTarget||null,tap_confirm:!!t.tapConfirm||null,controls:o.length?o:null,...this._markerLightFields(t),...this._markerValueBadgeFields(t),use_climate_temp:!!t.useClimateTemp||null,glow_radius_cm:(()=>{const e=sl(t.glowRadius);return null==e||e<=0?null:Math.round(this.host._imperial?30.48*e:100*e)})(),model:t.model.trim()||null,link:t.link.trim()||null,description:t.description.trim()||null,pdfs:t.pdfs,hidden:t.hideFromPlan,vacuum:i?.vacuum||null};return("virtual"===t.binding||t.room)&&(n.space=e?.space||("virtual"===t.binding?this.host._space:null),n.area=e?.area||null,n.room_id=e?.roomId||null),n}_markerPreviewDevice(t){const e=this._markerDraft(t);if(!e)return null;const s=`${this.host._haRegistry.revision}\n${Et(this.host._markers)}\n${JSON.stringify(e)}`;if(this.host._markerPreviewMemo?.key===s)return this.host._markerPreviewMemo.device;const i=ei({hass:this.host.hass,registry:this.host._haRegistry,areaToSpace:Object.fromEntries(Object.entries(this.host._areaToSpace).map(([t,e])=>[t,e.space])),marker:e,siblingMarkers:this.host._markers,settings:this.host._settings,excluded:this.host._excluded,showAll:this.host._showAll,firstSpaceId:this.host._model[0]?.id||this.host._space,loc:t=>this.host._t(t),iconRules:this.host._iconRules});return this.host._markerPreviewMemo={key:s,device:i},i}_markerPreviewDevices(t){const e=this.host._markerPreviewDevicesMemo;if(e?.base===this.host._devices&&e.preview===t)return e.devices;const s=[...this.host._devices.filter(e=>e.id!==t.id),t];return this.host._markerPreviewDevicesMemo={base:this.host._devices,preview:t,devices:s},s}_toggleIntent(t,e=this.host._devices){return si({hass:this.host._planHass,registryHass:this.host._fullRegistryHass,devices:e,device:t,virtualLights:this.host._virtualLights})}_toggleIntentForDialog(t){const e=this._markerPreviewDevice(t);if(!e)return null;const s=this._markerPreviewDevices(e);return this._toggleIntent(e,s)}_toggleStateText(t,e){const s=this.host._planHass?.states?.[t]||this.host.hass?.states?.[t];try{return s&&"function"==typeof this.host.hass?.formatEntityState?this.host.hass.formatEntityState(s):e}catch{return e}}_toggleConfirmationStateText(t){const e=String(t.state||"unknown"),s=t.entityId?this._toggleStateText(t.entityId,e):e;if(s.trim().toLocaleLowerCase()!==e.trim().toLocaleLowerCase())return s;const i={on:"confirm.state_on",off:"confirm.state_off",open:"confirm.state_open",closed:"confirm.state_closed",opening:"confirm.state_opening",closing:"confirm.state_closing",unknown:"confirm.state_unknown"}[e];return i?this.host._t(i):e.replaceAll("_"," ").replaceAll("-"," ")}_toggleConfirmationLines(t){const e={"turn-on":"confirm.state_on","turn-off":"confirm.state_off",open:"confirm.state_open",close:"confirm.state_closed",stop:"confirm.state_stopped"};return ii(t,{state:t=>this._toggleConfirmationStateText(t),current:t=>this.host._t("confirm.current_state",{state:t}),expected:t=>this.host._t("confirm.expected_state",{state:t}),groupCurrent:(t,e)=>this.host._t("confirm.group_current",{on:t,total:e}),groupAllOn:()=>this.host._t("confirm.group_all_on"),groupAllOff:()=>this.host._t("confirm.group_all_off"),unavailable:t=>this.host._t("confirm.unavailable_targets",{count:t}),effect:t=>this.host._t(e[t]),expectedByHa:()=>this.host._t("confirm.expected_by_ha")})}_toggleHintLines(t){if(!t)return[];const e=t=>this.host._t(`marker.toggle_effect_${t.replace("-","_")}`),s=t=>this.host._t(`marker.toggle_skip_${t.replace("-","_")}`);return oi(t,{single:t=>{if("via"in t&&"virtual-light"===t.via)return this.host._t("marker.virtual_light_target",{name:t.name});const e=t.entityId||("ref"in t?t.ref:""),s=t.name||e;return this.host._t("marker.toggle_hint_single",{name:s,id:e})},group:t=>this.host._t("marker.toggle_hint_group",{count:t.length,names:t.map(t=>`${t.name} (${t.entityId})`).join(", ")}),currentNext:(t,s)=>"virtual-light"===t.via?this.host._t("marker.virtual_light_current",{state:this.host._t("on"===t.state?"marker.virtual_light_state_on":"marker.virtual_light_state_off"),effect:e(s)}):this.host._t("marker.toggle_hint_current",{state:this._toggleStateText(t.entityId,t.state),effect:e(s)}),groupCurrentNext:(t,s)=>this.host._t("marker.toggle_hint_group_current",{on:t.filter(t=>"on"===t.state).length,count:t.length,effect:e(s)}),skipped:t=>this.host._t("marker.toggle_hint_skipped",{count:t.length,targets:t.map(t=>{const e=t.entityId||t.ref;return`${t.name||e} (${e}: ${s(t.reason)})`}).join(", ")}),none:t=>this.host._t(`marker.toggle_none_${t.replaceAll("-","_")}`)})}_effectiveStoredTapAction(t,e){return t.tapActionTouched?t.tapAction:is(t.originalHasTapAction?t.originalTapAction:null,e)}_effectiveMarkerTapAction(t,e=this._markerPreviewDevice(t)){return this._effectiveStoredTapAction(t,e?.primary?.split(".")[0])}_announceToggleDraft(t){const e=this._markerPreviewDevice(t),s=t.tapActionTouched?t:{...t,tapAction:this._effectiveMarkerTapAction(t,e)},i="toggle"===s.tapAction?this._toggleHintLines(this._toggleIntentForDialog(s)).join(" "):"";return{...s,tapHintAnnouncement:i}}_valueBadgeForBinding(t,e){const s={...t,binding:e,valueBadgeEnabled:!1,valueBadgeSource:null,valueBadgeTouched:!0},i=this._markerPreviewDevice(s);if(!i)return{valueBadgeEnabled:!1,valueBadgeSource:null,valueBadgeTouched:!0};const o=[...this.host._devices.filter(t=>t.id!==i.id),i],a=ts(this.host._planHass,i,o),n=es(this.host._planHass,i,a);return{valueBadgeEnabled:t.valueBadgeEnabled&&!!n,valueBadgeSource:n,valueBadgeTouched:!0}}_markerSpatialSource(t){const e=this._markerPreviewDevice(t);if(!e)return null;const s={...e,hidden:!1};return ai(ni(this.host._planHass,[...this.host._devices.filter(t=>t.id!==s.id),s],null,this.host._virtualLights).filter(t=>t.device.id===s.id))}_markerAutoHasSpatialSource(t){const e={...t,lightRole:"auto",lightRoleTouched:!0},s=this._markerPreviewDevice(e);return!!s&&ri(this.host._planHass,{...s})}_setMarkerLightRole(t){const e=this.host._markerDialog;e&&(this.host._markerDialog={...e,lightRole:t,lightRoleTouched:!0})}_controlRefInfo(t){if(!t.startsWith("marker:"))return{label:this.host.hass.states[t]?.attributes?.friendly_name||t,sub:t,icon:t.startsWith("light.")?"mdi:lightbulb":"mdi:toggle-switch",warning:!this.host._planEntityAvailable(t)};const e=t.slice(7),s=this.host._markers.find(t=>t.id===e),i=this.host._devices.find(t=>t.id===e);if(!s||s.removed||!0!==s.is_light)return{label:this.host._t("marker.control_missing_label"),sub:`${this.host._t("marker.control_broken")} (${t})`,icon:"mdi:alert-outline",warning:!0};const o=this.host._serverCfg?.spaces.find(t=>t.id===(i?.space||s.space)),a=!!i&&li(i).length>0;return{label:s.name||i?.name||e,sub:[o?.title||i?.space||s.space,s.room_id||i?.area,a?"":this.host._t("marker.control_passive")].filter(Boolean).join(" · "),icon:s.icon||i?.icon||"mdi:lightbulb-outline",warning:!0===s.hidden||!0===i?.hidden}}_valueBadgeCandidateLabel(t){const e=t.source;if("derived_lqi"===e.kind)return this.host._t("marker.value_badge_lqi");if("derived_marker_state"===e.kind)return this.host._t("marker.value_badge_marker_state",{name:t.label});if("entity_state"===e.kind)return this.host._t("marker.value_badge_state",{name:t.label});const s=this.host.hass.states[e.entity_id]?.attributes?.friendly_name||this.host._fullRegistryHass.entities[e.entity_id]?.name||e.entity_id;return this.host._t(`marker.value_badge_attr_${e.attribute}`,{name:s})}_controlCandidates(t){const e=this._markerDraft(t)?.id||t.devId||"",s=[],i=new Set;for(const t of this.host._markers){if(t.id===e||t.removed||t.hidden||!0!==t.is_light)continue;if("virtual"!==t.binding&&"active"!==this.host._bindingStatus(t.binding).kind)continue;const o=this.host._devices.find(e=>e.id===t.id);if(!o||o.hidden)continue;const a=this._controlRefInfo(`marker:${t.id}`);for(const t of li(o))i.add(t);s.push({value:`marker:${t.id}`,label:a.label,sub:a.sub,icon:a.icon,search:`${a.label} ${a.sub} ${t.id} ${li(o).join(" ")}`.toLowerCase()})}const o=Object.keys(this.host.hass.states||{}).filter(e=>hi(e)&&!i.has(e)&&ci(t.binding,[e],this.host._bindingEntities(t.binding)).length>0&&this.host._planEntityAvailable(e)).map(t=>{const e=this._controlRefInfo(t);return{value:t,label:e.label,sub:t,icon:e.icon,search:`${e.label} ${t}`.toLowerCase()}}),a=t.controlsFilter.trim().toLowerCase();return[...s,...o].filter(e=>!t.controls.includes(e.value)&&(!a||e.search.includes(a))).slice(0,12).map(({search:t,...e})=>e)}_addControlRef(t,e){if(e.startsWith("marker:")){const s=this._markerDraft(t)?.id||t.devId||"";if(!s)return void this.host._showToast(this.host._t("toast.marker_binding_required"));const i=e.slice(7),o=this._markerDraft(t),a=o?[...this.host._markers.filter(t=>t.id!==s),o]:this.host._markers;if(di(a,s,i))return void this.host._showToast(this.host._t("toast.marker_control_cycle"))}this.host._markerDialog=this._announceToggleDraft({...t,controls:[...t.controls,e],controlsFilter:""})}_setMarkerGlowMode(t){const e=this.host._markerDialog;if(!e)return;if("auto"===t)return void(this.host._markerDialog={...e,glowMode:t,glowTouched:!0});const s=this._markerSpatialSource(e),i=pi(s?this.host._planHass.states[s.eid]:void 0,null,this.host._fillColors.glow_light.c),o=!e.glowColorDrafted,a="fixed"===t&&!e.glowBrightnessDrafted;this.host._markerDialog={...e,glowMode:t,glowColor:o?i.c:e.glowColor,glowBrightness:a?Math.max(1,Math.round(100*i.bri)):e.glowBrightness,glowColorDrafted:!0,glowBrightnessDrafted:e.glowBrightnessDrafted||"fixed"===t,glowTouched:!0}}_renderMarkerDialog(){const t=this.host._markerDialog,e="virtual"===t.bindingMode,s=this._bindingCandidates();this.host._bindingEntities(t.binding);const i=e?null:this.host._bindingStatus(t.binding),n=!e&&this.host._bindingHasHaPage(t.binding),r=this._markerPreviewDevice(t),l=this._effectiveMarkerTapAction(t,r),h=r?ps(this.host._serverCfg?.spaces.find(t=>t.id===r.space)):null,c=r?this._markerPreviewDevices(r):this.host._devices,d="toggle"===l&&r?this._toggleIntent(r,c):null,p=this._toggleHintLines(d),_=r?_i(this.host._planHass,r,{liveStates:!1!==this.host._config?.live_states,showTemperature:!1!==this.host._config?.show_temperature,showSignal:h?.showLqi??!1!==this.host._config?.show_signal,designPreview:!0,activityRuntime:this.host._activityRt.get(r.id),lightDevices:c,registryHass:this.host._fullRegistryHass,reducedMotion:this.host._reducedMotion}):null,u=r?ts(this.host._planHass,r,c):[],m=r?es(this.host._planHass,r,u):null,g=t.valueBadgeTouched?t.valueBadgeEnabled:!!_?.valueBadge,f=t.valueBadgeTouched?t.valueBadgeSource:_?.valueBadge?.source||t.valueBadgeSource,b=t.valueBadgeTouched?t.valueBadgePosition:_?.valueBadge?.position||t.valueBadgePosition,y=ui(f),v=!!f&&!u.some(t=>t.key===y),$=u.find(t=>t.key===y),w=_?.valueSource?_.valueSource.attribute?`attr:${_.valueSource.eid}:${_.valueSource.attribute}`:`state:${_.valueSource.eid}`:"",k=this._markerAutoHasSpatialSource(t),x=!!r&&mi(this.host._planHass,{...r}),S=gi(t.lightRole,k,x,t.glowMode),M=!S.sourceExists,D=!S.fromSourceEnabled,C=S.passive,P=S.effectiveMode,I="never"===t.lightRole?this.host._t("marker.glow_disabled_never"):"auto"!==t.lightRole||k?C?this.host._t("marker.glow_passive_hint"):this.host._t("marker.glow_disabled_no_entity"):this.host._t("marker.glow_disabled_auto"),T=r?li(r):[],R=r&&fi(r)||"",F=!!t.lightEntity&&!T.includes(t.lightEntity),N=r?bi(r):[],z=!!t.toggleEntity&&!N.includes(t.toggleEntity),A="toggle"===l?this._toggleIntentForDialog({...t,toggleEntity:"",toggleEntityTouched:!0}):null,O=A?[...A.targets,...A.skippedTargets].map(t=>t.entityId||("ref"in t?t.ref:"")).filter(Boolean).join(", "):"",L=(()=>{if(e)return null;const i=s.find(e=>e.value===t.binding);if(i)return i.label;const[o,a]=t.binding.split(":");return"device"===o?this.host._fullRegistryHass.devices[a]?.name_by_user||this.host._fullRegistryHass.devices[a]?.name||a:this.host._fullRegistryHass.entities[a]?.name||this.host.hass.states[a]?.attributes?.friendly_name||a})();return o` +
+ ${"ha_disabled"===i?.kind?o`
+ + ${this.host._t(`marker.ha_disabled_${i.reason}`)} + ${n?o``:a} +
`:"unverified"===i?.kind&&t.binding?o`
+ + ${this.host._t("marker.ha_registry_limited")} +
`:a} + + this.host._markerDialog={...t,name:e.target.value}} /> + + +
+ +
+ + +
+ ${"ha"===t.bindingMode?o` + ${t.bindingOpen?o`
+ this.host._markerDialog={...t,bindingFilter:e.target.value}} /> +
+ ${s.map(e=>o`
{const s={...t,binding:e.value,bindingOpen:!1,controls:ss(e.value,t.controls,this.host._bindingEntities(e.value)),autoIcon:this.host._autoIconForBinding(e.value)};this.host._markerDialog=this._announceToggleDraft({...s,...this._valueBadgeForBinding(s,e.value)})}}> + ${e.label}${e.sub} +
`)} + ${s.length?a:o`
${this.host._t("marker.nothing_found")}
`} +
+
`:a}`:a} +
+ + + + + ${this._renderVacSection(t)} + + + + ${"toggle"===l&&(N.length>1||z)?o`
+
+ + ${this._help("marker.toggle_entity.help")} +
+ + ${z?o`

+ + ${this.host._t("marker.toggle_entity_missing",{entity:t.toggleEntity,fallback:O||this.host._t("marker.toggle_entity_none")})} +

`:a} +
`:a} + ${"toggle"===l?o`
+ ${p.map(t=>o`
${t}
`)} +
+
${t.tapHintAnnouncement}
`:a} + ${"run"===l?(()=>{const e=t.runFilter.trim().toLowerCase(),s=this._runCandidates().filter(t=>!e||t.label.toLowerCase().includes(e)||t.value.includes(e)),i=t.tapTarget?this._runCandidates().find(e=>e.value===t.tapTarget):null;return o` + + ${t.tapTarget&&!i?o`
${this.host._t("marker.run_target_gone",{id:t.tapTarget})}
`:a} + {t.target.select()}} + @input=${e=>this.host._markerDialog={...t,runFilter:e.target.value,tapTarget:""}} /> + ${i?a:o`
+ ${s.slice(0,40).map(e=>o`
this.host._markerDialog={...t,tapTarget:e.value,runFilter:""}}> + ${e.label}${e.sub} +
`)} + ${s.length?a:o`
${this.host._t("marker.nothing_found")}
`} +
`}`})():a} + ${"run"===l||"toggle"===l?o``:a} + + +
${this.host._t("marker.controls_hint")}
+ ${t.controls.length?o`
+ ${t.controls.map(e=>{const s=this._controlRefInfo(e);return o` + ${s.label} + this.host._markerDialog=this._announceToggleDraft({...t,controls:t.controls.filter(t=>t!==e)})}> + `})} +
`:a} + this.host._markerDialog={...t,controlsFilter:e.target.value}} /> + ${t.controlsFilter.trim()?o`
+ ${this._controlCandidates(t).map(e=>o``)} +
`:a} + + ${this.host._bindingHasClimate(t.binding)?o``:a} +
+ ${this.host._t("marker.light_role_label")}${this._help("marker.light_role.help")} +
+ + + +
+
+ + ${"always"===t.lightRole&&(T.length>1||F)?o`
+
+ + ${this._help("marker.light_entity.help")} +
+ + ${F?o`

+ + ${this.host._t("marker.light_entity_missing",{entity:t.lightEntity,fallback:R||"—"})} +

`:a} +
`:a} + +
+ ${this.host._t("marker.glow_color_label")}${this._help("marker.glow_mode.help")} +
+ + + +
+ ${"auto"!==P?o`
+ {this.host._markerDialog={...t,glowMode:P,glowColor:e.detail.color,glowColorDrafted:!0,glowTouched:!0}}}> + ${"fixed"===P?o` + ${this.host._t("marker.glow_brightness")} + ${this._rangeInput(1,100,1,t.glowBrightness,e=>{this.host._markerDialog={...t,glowMode:P,glowBrightness:e,glowBrightnessDrafted:!0,glowTouched:!0}},M,this.host._t("marker.glow_brightness"))} + ${Math.round(t.glowBrightness)}%`:a} +
`:a} +
+
+
+ + ${this._help("marker.glow_radius.help")} +
+
+ this.host._markerDialog={...t,glowRadius:e.target.value}} /> + ${this.host._imperial?this.host._t("gs.unit_ft"):this.host._t("gs.unit_m")} +
+
+ ${M||C?o`

+ ${I} +

`:a} + + + ${customElements.get("ha-icon-picker")?o`{const s=e.detail.value||"";(t.icon||s!==t.autoIcon)&&(this.host._markerDialog={...t,icon:s})}}>`:o`this.host._markerDialog={...t,icon:e.target.value}} />`} + ${!t.icon&&t.autoIcon?o`

+ ${this.host._t("marker.icon_auto",{icon:t.autoIcon})} +

`:a} + + + +

${this.host._t(Qr[t.display])}

+ ${"static_icon"===t.display&&this.host._bindingHasAlarm(t.binding)?o`
+ + ${this.host._t("marker.static_alarm_warning")} +
`:a} +
+ ${this.host._t("marker.value_badge_title")}${this._help("marker.value_badge.help")} + + ${"static_icon"===t.display?o`

+ ${this.host._t("marker.value_badge_static")} +

`:u.length||t.valueBadgeSource?a:o`

+ ${this.host._t("marker.value_badge_empty")} +

`} + ${g?o` +
+ + ${this._help("marker.value_badge_source.help")} +
+ + ${$?o`

${$.technical}

`:a} + ${v?o`

+ ${this.host._t("marker.value_badge_missing_hint")} +

`:a} + ${"value"===t.display&&y===w?o`

+ ${this.host._t("marker.value_badge_duplicate")} +

`:a} +
+ + ${this._help("marker.value_badge_position.help")} +
+ + `:a} +
+ ${_?o` + `:o`
+ + ${this.host._t("marker.preview.select_source")} +
`} + ${"icon_ripple"===t.display?o`
+ {this.host._markerDialog={...t,rippleColor:e.detail.color}}}> +
+
+ ${this.host._t("marker.ripple_size")} + ${this._rangeInput(1,8,.5,t.rippleSize,e=>this.host._markerDialog={...t,rippleSize:e})} + ×${t.rippleSize} +
+

${this.host._t("marker.activity_alarm_note")}

`:a} + + +
+ ${this._rangeInput(.5,3,.1,t.size,e=>this.host._markerDialog={...t,size:e})} + ×${t.size.toFixed(1)} + ${this.host._t("marker.angle_label")} + ${""} + ${this._rangeInput(0,355,5,t.angle,e=>this.host._markerDialog={...t,angle:e})} + ${t.angle}° +
+ + + this.host._markerDialog={...t,model:e.target.value}} /> + + + this.host._markerDialog={...t,link:e.target.value}} /> + + + + + +
+ ${t.pdfs.map(t=>o` + ${t.name} + this._removeMarkerPdf(t.url)}>`)} + + + this._pickMarkerFiles(t)} /> + +
+
+
+
+ ${t.devId?o``:a} + ${t.devId?o``:a} +
+
+ + +
+
+
`}_renderSpaceDialog(){const t=this.host._spaceDialog,e=this.host._importTotal>0&&"create"===t.mode?this.host._t("import.progress",{i:this.host._importTotal-this.host._importQueue.length,n:this.host._importTotal}):"",s=()=>{this.host._spaceDialog=null,this.host._importQueue=[],this.host._importTotal=0};return o` +
+ + this.host._spaceDialog={...t,title:e.target.value}} /> + + + ${"file"===t.source?o`
+ ${t.planFile?o`${t.planFile.name}`:t.planUrl?o`${this.host._t("space.plan_alt")}`:o`${this.host._t("space.no_plan")}`} + + + this._pickPlanFile(t)} /> + + +
+ ${t.pickSaved?this._renderServerPlans(t):a}`:a} + + + +
+ {const s=e.target.value,i=sl(s),o=null==i?null:ki(i,this.host._imperial);this.host._spaceDialog={...t,cellCmInput:s,cellCmTouched:!0,cellCm:null!=o&&o>0?Math.max(.1,Math.min(el,o)):t.cellCm}}} /> + ${this.host._t(this.host._imperial?"space.scale_unit_imperial":"space.scale_unit")} +
+ + + + + +
${this.host._t("space.zero_wall_help")}
+ + + ${""} + +
${this.host._t("space.hide_decor_tip")}
+ +
${this.host._t("space.hide_openings_tip")}
+ + ${[["labelTemp","space.label_temp"],["labelHum","space.label_hum"],["labelLqi","space.label_lqi"],["labelLight","space.label_light"]].map(([e,s])=>o``)} + +
+ ${this._rangeInput(50,300,5,Math.round(100*t.cardFontScale),e=>this.host._spaceDialog={...t,cardFontScale:e/100})} + ${Math.round(100*t.cardFontScale)}% +
+ ${this.host._renderCardPreview(t.cardFontScale,1,1)} +
+ {this.host._spaceDialog={...t,roomColor:e.detail.color,roomOpacity:e.detail.opacity}}}> +
+ + + ${"static"===(t.bgMode??St(this.host._settings,{}))?o`
+ {this.host._spaceDialog={...t,bgColor:e.detail.color}}}> + ${t.bgColor?o``:o`${this.host._t("space.bg_inherited")}`} +
`:a} + +
+ {const s=e.target.value.trim(),i=""===s?null:Math.round(Number(s));this.host._spaceDialog={...t,northDeg:null!==i&&Number.isFinite(i)?Math.min(359,Math.max(0,i)):null}}} /> + ${null===t.northDeg?this.host._t("space.north_inherited",{v:null===Mt(this.host._settings,{})?"—":String(Mt(this.host._settings,{}))+"°"}):"°"} +
+ + + + ${xi.map(t=>[t,"fill."+t]).map(([e,s])=>o` + ${"custom"===e&&"custom"===t.fillMode?o`
+ ${this.host._t("space.custom_fill")} + {this.host._spaceDialog={...t,customFill:{c:e.detail.color,a:e.detail.opacity}}}}> + ${t.customFill?o``:a} +
`:a}`)} + + ${t.deleteBlockers?o``:a} +
+ +
`}_renderMergeDialog(){const t=this.host._mergeDialog,e=this.host._spaceModel()?.rooms||[],s=(s,i)=>{const a=e.find(t=>t.id===s),n=a?.area?this.host.hass.areas[a.area]?.name:null;return o``};return o`this.host._mergeDialog=null}> +
+

${this.host._t("merge.hint")}

+ + ${s(t.aId,"a")} + ${s(t.bId,"b")} +
+
+ + + +
+
`}_renderRoomSource(t){const e="temp"===t?this.host._roomTempSrc:this.host._roomHumSrc,s=e=>{"temp"===t?this.host._roomTempSrc=e:this.host._roomHumSrc=e,this.host.requestUpdate()},i=this.host._roomSrcOpen===t;return o` + + + + ${e||i?o` + ${i?o`
+ {this.host._roomSrcFilter=t.target.value,this.host.requestUpdate()}} /> +
+ ${this._roomSrcCandidates().map(t=>o`
{s(t.value),this.host._roomSrcOpen=null}}> + ${t.label}${t.sub} +
`)} +
+
`:a}`:a}`}_renderRoomDialog(){const t=!!this.host._roomEditId,e=t?null:this.host._wallFaceBatch,s=e&&e.candidates.length>1?this.host._t("room.queue_progress",{current:e.index+1,total:e.candidates.length}):"",i=!!this.host._areaSel||!!this.host._nameSel.trim(),n=ps(this.host._curSpaceCfg),r=this.host._roomFill||n.fill,l=this.host._roomCustomFill||n.customFill,h=[...this.host._freeAreas];if(t&&this.host._areaSel&&!h.some(t=>t.area_id===this.host._areaSel)){const t=this.host.hass.areas[this.host._areaSel];t&&h.unshift(t)}return o` +
+ ${s?o`

+ ${s} +

`:a} + + this.host._nameSel=t.target.value} /> + + + + + + ${[["","fill.inherit"],...Si.map(t=>[t,"fill."+t])].map(([t,e])=>o``)} + ${"custom"===r?o`
+ ${this.host._roomCustomFill?this.host._t("room.custom_fill_own"):this.host._t("room.custom_fill_space")} + {this.host._roomCustomFill={c:t.detail.color,a:t.detail.opacity}}}> + ${this.host._roomCustomFill?o``:a} +
`:a} + ${this._renderRoomSource("temp")} + ${this._renderRoomSource("hum")} + + + +
+ ${this._rangeInput(50,300,5,Math.round(100*this.host._roomNameScale),t=>{this.host._roomNameScale=t/100,this.host.requestUpdate()})} + ${Math.round(100*this.host._roomNameScale)}% +
+ +
+ ${this._rangeInput(50,300,5,Math.round(100*this.host._roomLabelScale),t=>{this.host._roomLabelScale=t/100,this.host.requestUpdate()})} + ${Math.round(100*this.host._roomLabelScale)}% +
+ ${this.host._renderCardPreview(ps(this.host._curSpaceCfg).cardFontScale,this.host._roomNameScale,this.host._roomLabelScale)} +
+
+ + + ${t?o``:o`${this.host._pendingSplit?a:o``} + `} +
+
`}}export{rl as EDITOR_RUNTIME_FINGERPRINT,ll as HouseplanEditorRuntime}; diff --git a/custom_components/houseplan/frontend/houseplan-assets/houseplan-onboarding-runtime-Bj55plb1.js b/custom_components/houseplan/frontend/houseplan-assets/houseplan-onboarding-runtime-Bj55plb1.js new file mode 100644 index 00000000..05f8b521 --- /dev/null +++ b/custom_components/houseplan/frontend/houseplan-assets/houseplan-onboarding-runtime-Bj55plb1.js @@ -0,0 +1,217 @@ +globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="f111431804c4c42c6e35286f29720d089bc8b77ffc364a6517f7a3510fcb11c8";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 c,b$ as n,c0 as r,b as p,A as _,cV as d,ap as u,ar as g,cW as m}from"./houseplan-card-Cs2QHIcW.js";import{i as b,c as $,a as v,s as f,t as y}from"./space-deletion-CJ8Co_o8.js";const w=1e3,D=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},S="f111431804c4c42c6e35286f29720d089bc8b77ffc364a6517f7a3510fcb11c8";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),c=i.settings?.custom_fill&&"object"==typeof i.settings.custom_fill?s(i.settings.custom_fill):null,n="none"===h.fill?{...c||e,a:0}:c;return void(this.host._spaceDialog={mode:p,spaceId:_,title:i.title,planUrl:i.plan_url||null,planFile:null,source:i.plan_url?"file":"draw",showBorders:h.showBorders,showNames:h.showNames,zeroWallStyle:l(i),displayTouched:!0,hideDecor:h.hideDecor,hideOpenings:h.hideOpenings,roomColor:h.color,roomOpacity:h.opacity,fillMode:"none"===h.fill?"custom":h.fill,customFill:n,glowEnabled:h.glow,bgColor:h.bgColor,bgMode:"static"===i.settings?.bg_mode||"daynight"===i.settings?.bg_mode?i.settings.bg_mode:null,northDeg:a({},i.settings),sunRays:"boolean"==typeof i.settings?.sun_rays?i.settings.sun_rays:null,tempMin:h.tempMin,tempMax:h.tempMax,showLqi:h.showLqi??this.host._config?.show_signal??!0,cardFontScale:h.cardFontScale,labelTemp:h.labelTemp,labelHum:h.labelHum,labelLqi:h.labelLqi,labelLight:h.labelLight,cellCm:Number(i.cell_cm)>0?Number(i.cell_cm):5,cellCmInput:o(Number(i.cell_cm)>0?Number(i.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1})}const d=i(this.host._imperial);this.host._spaceDialog={mode:p,title:"",planUrl:null,planFile:null,...b(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:r,roomOpacity:n,fillMode:"custom",customFill:{...e,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:c,tempMax:h,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:d,cellCmInput:o(d,this.host._imperial),cellCmTouched:!1,busy:!1}}async _pickPlanFile(t){const s=t.target,e=s.files?.[0];if(!e||!this.host._spaceDialog)return;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{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:c,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`
${this.host._t("space.loading")}
`;const s=t.saved||[];if(!s.length)return p`
${this.host._t("space.no_saved")}
`;return p`
+ ${s.map(s=>{return p` +
+ +
+ ${s.name} + ${e=s.size,e>=1048576?`${(e/1048576).toFixed(1)} MB`:`${Math.round(e/1024)} KB`}${s.used_by.length?` · ${this.host._t("space.used_by",{list:s.used_by.join(", ")})}`:""} +
+ + +
`;var e})} +
`}async _saveConfigNow(){this.host._cfgEpoch++;try{await this.host._writeConfig()}catch(t){const s=t;throw s?.physicalGeometryRolledBack?await this.host._reloadRejectedPhysicalWrite():"conflict"===s?.code&&await this.host._reloadConfigOnly(),t}}async _saveSpaceDialog(){const t=this.host._spaceDialog;if(!t||t.busy||!t.title.trim())return;if("file"===t.source&&!t.planFile&&!t.planUrl)return void this.host._showToast(this.host._t("toast.plan_required"));const s="create"===t.mode&&0===(this.host._serverCfg?.spaces.length||0);this.host._spaceDialog={...t,busy:!0};try{const e="create"===t.mode?`s${Date.now().toString(36)}`:t.spaceId;let o=null;if("file"===t.source&&t.planFile){o={url:(await this.host.hass.callWS({type:"houseplan/plan/set",space_id:e,ext:t.planFile.ext,data:t.planFile.b64})).url,aspect:t.planFile.aspect}}let a=t.savedAspect||null;!o&&"file"===t.source&&t.planUrl&&!a&&this.host._aspectJob&&(a=await this.host._aspectJob||null);const l=this.host._serverCfg;let i=l.spaces.find(t=>t.id===e);if("create"===t.mode)i=$(e,t.title.trim()),l.spaces.push(i);else{if(!i)throw new Error(`space ${e} is gone from the config`);i.title=t.title.trim()}if(!i)throw new Error(`space ${e} is unavailable`);if(o?(i.plan_url=o.url,i.plan_aspect=o.aspect):"file"===t.source&&t.planUrl&&t.planUrl!==i.plan_url&&(i.plan_url=t.planUrl,i.plan_aspect=a),"draw"===t.source&&(i.plan_url=null,i.plan_aspect=null,delete i.plan_x,delete i.plan_y,delete i.plan_scale,delete i.plan_scale_x,delete i.plan_scale_y,delete i.plan_angle),i.settings={...i.settings||{},show_borders:t.showBorders,show_names:t.showNames,hide_decor:t.hideDecor||void 0,hide_openings:t.hideOpenings||void 0,room_color:t.roomColor,room_opacity:t.roomOpacity,bg_color:t.bgColor||void 0,bg_mode:t.bgMode||void 0,north_deg:t.northDeg??void 0,sun_rays:t.sunRays??void 0,fill_mode:t.fillMode,custom_fill:t.customFill||void 0,glow_enabled:t.glowEnabled,temp_min:Number.isFinite(t.tempMin)?Math.min(t.tempMin,t.tempMax):c,temp_max:Number.isFinite(t.tempMax)?Math.max(t.tempMin,t.tempMax):h,show_lqi:t.showLqi,card_font_scale:1!==t.cardFontScale?t.cardFontScale:void 0,label_temp:t.labelTemp,label_hum:t.labelHum,label_lqi:t.labelLqi,label_light:t.labelLight},i.zero_wall_style=t.zeroWallStyle,i.cell_cm=Number.isFinite(t.cellCm)&&t.cellCm>0?Math.max(.1,Math.min(w,t.cellCm)):5,await this._saveConfigNow(),this.host._spaceDialog=null,"create"===t.mode&&this.host._commitSpace(i.id),this.host._regSignature="",this.host._maybeRebuildDevices(),this.host._importQueue.length)this._openNextImport();else if(s||this.host._importTotal>0){const t=this.host._importTotal>0;this.host._importTotal=0,this.host._commitSpace(this.host._serverCfg.spaces[0]?.id||this.host._space),await this.host._requestMode("plan"),this.host._tool="draw",this.host._path=[],this.host._cursorPt=null,this.host._primeDrawWallField(),this.host._showToast(this.host._t(s&&!t?"toast.space_added_onboard":"import.done"))}else this.host._showToast(this.host._t("create"===t.mode?"toast.space_added":"toast.space_saved")),"create"===t.mode&&("plan"!==this.host._mode?await this.host._requestMode("plan"):(this.host._tool="draw",this.host._path=[],this.host._cursorPt=null,this.host._primeDrawWallField(),this.host._saveNav()))}catch(t){const s=t;"conflict"!==s?.code&&await this.host._reloadConfigOnly(!0),this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,busy:!1}),this.host._showToast(this.host._t("toast.error",{err:this.host._errText(t)}))}}async _deleteSpace(){const t=this.host._spaceDialog;if(!t||"edit"!==t.mode)return;const s=this.host._serverCfg;if(!s)return;const e=s.spaces.find(s=>s.id===t.spaceId);if(!e)return;const o=v(s,this.host._layout||{},t.spaceId||""),a=1===s.spaces.length&&s.spaces[0]?.id===t.spaceId;if(!o.count||a){if(confirm(this.host._t("confirm.delete_space",{title:e.title}))){this.host._spaceDialog={...t,deleteBlockers:0,busy:!0};try{this.host._saveConfigDebounced.pending()&&this.host._saveConfigDebounced.flush(),this.host._persistLayout.pending()&&this.host._persistLayout.flush(),await this.host._writeChain;const s=await this.host.hass.callWS({type:"houseplan/space/delete",space_id:t.spaceId,expected_config_rev:this.host._cfgRev,expected_layout_rev:this.host._layoutRev}),[e,o]=await Promise.all([this.host.hass.callWS({type:"houseplan/config/get"}),this.host.hass.callWS({type:"houseplan/layout/get"})]);this.host._adoptStructuralResponses(e,o),this.host._cfgRev=s?.config_rev??this.host._cfgRev,this.host._layoutRev=s?.layout_rev??this.host._layoutRev,this.host._spaceDialog=null,this.host._space===t.spaceId&&this.host._commitSpace(this.host._serverCfg.spaces[0]?.id||""),this.host._regSignature="",this.host._maybeRebuildDevices(),this.host._showToast(this.host._t("toast.space_deleted"))}catch(s){const e=s;"conflict"!==e?.code&&"space_in_use"!==e?.code||await Promise.all([this.host._reloadConfigOnly(!0),this.host._reloadLayoutOnly()]);const o=this.host._serverCfg;if(this.host._spaceDialog&&o){const s=v(o,this.host._layout||{},t.spaceId||""),e=1===o.spaces.length&&o.spaces[0]?.id===t.spaceId;this.host._spaceDialog={...this.host._spaceDialog,busy:!1,deleteBlockers:e?0:s.count}}this.host._showToast(this.host._t("toast.delete_failed",{err:this.host._errText(s)}))}}}else this.host._spaceDialog={...t,deleteBlockers:o.count}}_startImport(){const t=this.host._importDialog;if(!t)return;const s=t.floors.filter(t=>t.checked).map(t=>t.name);this.host._importDialog=null,s.length?(this.host._importQueue=s,this.host._importTotal=s.length,this._openNextImport()):this._openSpaceDialog("create")}_openNextImport(){const t=this.host._importQueue.shift();if(void 0===t)return;const s=i(this.host._imperial);this.host._spaceDialog={mode:"create",title:t,planUrl:null,planFile:null,...b(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:r,roomOpacity:n,fillMode:"custom",customFill:null,glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:c,tempMax:h,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:s,cellCmInput:o(s,this.host._imperial),cellCmTouched:!1,busy:!1}}_skipImport(){this.host._spaceDialog=null,this.host._importQueue.length?this._openNextImport():this.host._importTotal>0&&this.host._model.length&&(this.host._importTotal=0,this.host._commitSpace(this.host._serverCfg.spaces[0]?.id||this.host._space),this.host._requestMode("plan").then(()=>{this.host._showToast(this.host._t("import.done"))}))}_renderImportDialog(){const t=this.host._importDialog,s=t.floors.filter(t=>t.checked).length;return p`this.host._importDialog=null}> +
+
${this.host._t("import.hint")}
+ ${t.floors.map((s,e)=>p``)} +
+
+ + + +
+
`}_boolInput(t,s){return p`s(t.target.checked)} />`}_rangeInput(t,s,e,o,a){return p`a(Number(t.target.value))} />`}_renderSpaceDialog(){return this._renderSpaceDialogBody(this.host._spaceDialog)}_renderSpaceDialogBody(t){const s=this.host._importTotal>0&&"create"===t.mode?this.host._t("import.progress",{i:this.host._importTotal-this.host._importQueue.length,n:this.host._importTotal}):"",l=()=>{this.host._spaceDialog=null,this.host._importQueue=[],this.host._importTotal=0};return p` +
+ + this.host._spaceDialog={...t,title:s.target.value}} /> + + + ${"file"===t.source?p`
+ ${t.planFile?p`${t.planFile.name}`:t.planUrl?p`${this.host._t("space.plan_alt")}`:p`${this.host._t("space.no_plan")}`} + + + this._pickPlanFile(t)} /> + + +
+ ${t.pickSaved?this._renderServerPlans(t):_}`:_} + + + +
+ {const e=s.target.value,o=D(e),a=null==o?null:d(o,this.host._imperial);this.host._spaceDialog={...t,cellCmInput:e,cellCmTouched:!0,cellCm:null!=a&&a>0?Math.max(.1,Math.min(w,a)):t.cellCm}}} /> + ${this.host._t(this.host._imperial?"space.scale_unit_imperial":"space.scale_unit")} +
+ + + + + +
${this.host._t("space.zero_wall_help")}
+ + + +
${this.host._t("space.hide_decor_tip")}
+ +
${this.host._t("space.hide_openings_tip")}
+ + ${[["labelTemp","space.label_temp"],["labelHum","space.label_hum"],["labelLqi","space.label_lqi"],["labelLight","space.label_light"]].map(([s,e])=>p``)} + +
+ ${this._rangeInput(50,300,5,Math.round(100*t.cardFontScale),s=>{this.host._spaceDialog={...t,cardFontScale:s/100}})} + ${Math.round(100*t.cardFontScale)}% +
+ ${this.host._renderCardPreview(t.cardFontScale,1,1)} +
+ {this.host._spaceDialog={...t,roomColor:s.detail.color,roomOpacity:s.detail.opacity}}}> +
+ + + ${"static"===(t.bgMode??u(this.host._settings,{}))?p` +
+ {this.host._spaceDialog={...t,bgColor:s.detail.color}}}> + ${t.bgColor?p``:p`${this.host._t("space.bg_inherited")}`} +
`:_} + +
+ {const e=s.target.value.trim(),o=""===e?null:Math.round(Number(e));this.host._spaceDialog={...t,northDeg:null!==o&&Number.isFinite(o)?Math.min(359,Math.max(0,o)):null}}} /> + ${null===t.northDeg?this.host._t("space.north_inherited",{v:null===a(this.host._settings,{})?"—":`${a(this.host._settings,{})}°`}):"°"} +
+ + + + ${m.map(t=>[t,`fill.${t}`]).map(([s,o])=>p` + ${"custom"===s&&"custom"===t.fillMode?p` +
+ ${this.host._t("space.custom_fill")} + {this.host._spaceDialog={...t,customFill:{c:s.detail.color,a:s.detail.opacity}}}}> + ${t.customFill?p``:_} +
`:_}`)} + + ${t.deleteBlockers?p``:_} +
+ +
`}}export{C as HouseplanOnboardingRuntime,S as ONBOARDING_RUNTIME_FINGERPRINT}; diff --git a/custom_components/houseplan/frontend/houseplan-assets/space-deletion-CJ8Co_o8.js b/custom_components/houseplan/frontend/houseplan-assets/space-deletion-CJ8Co_o8.js new file mode 100644 index 00000000..d474a3b9 --- /dev/null +++ b/custom_components/houseplan/frontend/houseplan-assets/space-deletion-CJ8Co_o8.js @@ -0,0 +1 @@ +function e(e,o){return{id:e,title:o,plan_url:null,view_box:[0,0,1,1],rooms:[],wall_segments:[]}}function o(e="file"){const o="draw"===e;return{source:e,showBorders:o,showNames:o,displayTouched:!1}}function r(e,o){if(e.displayTouched)return{...e,source:o};const r="draw"===o;return{...e,source:o,showBorders:r,showNames:r}}function s(e,o,r){return{...e,[o]:r,displayTouched:!0}}function t(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),t=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),n=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&t.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:n,count:n.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="f111431804c4c42c6e35286f29720d089bc8b77ffc364a6517f7a3510fcb11c8";export{t as a,e as c,o as i,r as s,s as t}; diff --git a/custom_components/houseplan/frontend/houseplan-card.js b/custom_components/houseplan/frontend/houseplan-card.js index c03e74c3..90b0b423 100755 --- a/custom_components/houseplan/frontend/houseplan-card.js +++ b/custom_components/houseplan/frontend/houseplan-card.js @@ -1,7763 +1 @@ -globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="130005653425b19cc41a05c9a1f2a786b6c7fb1f6ed052e2e552de8fd5dfc524";const e=globalThis,t=e.ShadowRoot&&(void 0===e.ShadyCSS||e.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,i=Symbol(),s=new WeakMap;let n=class{constructor(e,t,s){if(this._$cssResult$=!0,s!==i)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=t}get styleSheet(){let e=this.o;const i=this.t;if(t&&void 0===e){const t=void 0!==i&&1===i.length;t&&(e=s.get(i)),void 0===e&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),t&&s.set(i,e))}return e}toString(){return this.cssText}};const o=(e,...t)=>{const s=1===e.length?e[0]:t.reduce((t,i,s)=>t+(e=>{if(!0===e._$cssResult$)return e.cssText;if("number"==typeof e)return e;throw Error("Value passed to 'css' function must be a 'css' function result: "+e+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+e[s+1],e[0]);return new n(s,e,i)},a=t?e=>e:e=>e instanceof CSSStyleSheet?(e=>{let t="";for(const i of e.cssRules)t+=i.cssText;return(e=>new n("string"==typeof e?e:e+"",void 0,i))(t)})(e):e,{is:r,defineProperty:l,getOwnPropertyDescriptor:c,getOwnPropertyNames:h,getOwnPropertySymbols:d,getPrototypeOf:p}=Object,u=globalThis,_=u.trustedTypes,m=_?_.emptyScript:"",g=u.reactiveElementPolyfillSupport,f=(e,t)=>e,v={toAttribute(e,t){switch(t){case Boolean:e=e?m:null;break;case Object:case Array:e=null==e?e:JSON.stringify(e)}return e},fromAttribute(e,t){let i=e;switch(t){case Boolean:i=null!==e;break;case Number:i=null===e?null:Number(e);break;case Object:case Array:try{i=JSON.parse(e)}catch(e){i=null}}return i}},b=(e,t)=>!r(e,t),y={attribute:!0,type:String,converter:v,reflect:!1,useDefault:!1,hasChanged:b};Symbol.metadata??=Symbol("metadata"),u.litPropertyMetadata??=new WeakMap;let w=class extends HTMLElement{static addInitializer(e){this._$Ei(),(this.l??=[]).push(e)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(e,t=y){if(t.state&&(t.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(e)&&((t=Object.create(t)).wrapped=!0),this.elementProperties.set(e,t),!t.noAccessor){const i=Symbol(),s=this.getPropertyDescriptor(e,i,t);void 0!==s&&l(this.prototype,e,s)}}static getPropertyDescriptor(e,t,i){const{get:s,set:n}=c(this.prototype,e)??{get(){return this[t]},set(e){this[t]=e}};return{get:s,set(t){const o=s?.call(this);n?.call(this,t),this.requestUpdate(e,o,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this.elementProperties.get(e)??y}static _$Ei(){if(this.hasOwnProperty(f("elementProperties")))return;const e=p(this);e.finalize(),void 0!==e.l&&(this.l=[...e.l]),this.elementProperties=new Map(e.elementProperties)}static finalize(){if(this.hasOwnProperty(f("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(f("properties"))){const e=this.properties,t=[...h(e),...d(e)];for(const i of t)this.createProperty(i,e[i])}const e=this[Symbol.metadata];if(null!==e){const t=litPropertyMetadata.get(e);if(void 0!==t)for(const[e,i]of t)this.elementProperties.set(e,i)}this._$Eh=new Map;for(const[e,t]of this.elementProperties){const i=this._$Eu(e,t);void 0!==i&&this._$Eh.set(i,e)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(e){const t=[];if(Array.isArray(e)){const i=new Set(e.flat(1/0).reverse());for(const e of i)t.unshift(a(e))}else void 0!==e&&t.push(a(e));return t}static _$Eu(e,t){const i=t.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof e?e.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(e=>this.enableUpdating=e),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(e=>e(this))}addController(e){(this._$EO??=new Set).add(e),void 0!==this.renderRoot&&this.isConnected&&e.hostConnected?.()}removeController(e){this._$EO?.delete(e)}_$E_(){const e=new Map,t=this.constructor.elementProperties;for(const i of t.keys())this.hasOwnProperty(i)&&(e.set(i,this[i]),delete this[i]);e.size>0&&(this._$Ep=e)}createRenderRoot(){const i=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return((i,s)=>{if(t)i.adoptedStyleSheets=s.map(e=>e instanceof CSSStyleSheet?e:e.styleSheet);else for(const t of s){const s=document.createElement("style"),n=e.litNonce;void 0!==n&&s.setAttribute("nonce",n),s.textContent=t.cssText,i.appendChild(s)}})(i,this.constructor.elementStyles),i}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(e=>e.hostConnected?.())}enableUpdating(e){}disconnectedCallback(){this._$EO?.forEach(e=>e.hostDisconnected?.())}attributeChangedCallback(e,t,i){this._$AK(e,i)}_$ET(e,t){const i=this.constructor.elementProperties.get(e),s=this.constructor._$Eu(e,i);if(void 0!==s&&!0===i.reflect){const n=(void 0!==i.converter?.toAttribute?i.converter:v).toAttribute(t,i.type);this._$Em=e,null==n?this.removeAttribute(s):this.setAttribute(s,n),this._$Em=null}}_$AK(e,t){const i=this.constructor,s=i._$Eh.get(e);if(void 0!==s&&this._$Em!==s){const e=i.getPropertyOptions(s),n="function"==typeof e.converter?{fromAttribute:e.converter}:void 0!==e.converter?.fromAttribute?e.converter:v;this._$Em=s;const o=n.fromAttribute(t,e.type);this[s]=o??this._$Ej?.get(s)??o,this._$Em=null}}requestUpdate(e,t,i,s=!1,n){if(void 0!==e){const o=this.constructor;if(!1===s&&(n=this[e]),i??=o.getPropertyOptions(e),!((i.hasChanged??b)(n,t)||i.useDefault&&i.reflect&&n===this._$Ej?.get(e)&&!this.hasAttribute(o._$Eu(e,i))))return;this.C(e,t,i)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}C(e,t,{useDefault:i,reflect:s,wrapped:n},o){i&&!(this._$Ej??=new Map).has(e)&&(this._$Ej.set(e,o??t??this[e]),!0!==n||void 0!==o)||(this._$AL.has(e)||(this.hasUpdated||i||(t=void 0),this._$AL.set(e,t)),!0===s&&this._$Em!==e&&(this._$Eq??=new Set).add(e))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(e){Promise.reject(e)}const e=this.scheduleUpdate();return null!=e&&await e,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[e,t]of this._$Ep)this[e]=t;this._$Ep=void 0}const e=this.constructor.elementProperties;if(e.size>0)for(const[t,i]of e){const{wrapped:e}=i,s=this[t];!0!==e||this._$AL.has(t)||void 0===s||this.C(t,void 0,i,s)}}let e=!1;const t=this._$AL;try{e=this.shouldUpdate(t),e?(this.willUpdate(t),this._$EO?.forEach(e=>e.hostUpdate?.()),this.update(t)):this._$EM()}catch(t){throw e=!1,this._$EM(),t}e&&this._$AE(t)}willUpdate(e){}_$AE(e){this._$EO?.forEach(e=>e.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(e)),this.updated(e)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(e){return!0}update(e){this._$Eq&&=this._$Eq.forEach(e=>this._$ET(e,this[e])),this._$EM()}updated(e){}firstUpdated(e){}};w.elementStyles=[],w.shadowRootOptions={mode:"open"},w[f("elementProperties")]=new Map,w[f("finalized")]=new Map,g?.({ReactiveElement:w}),(u.reactiveElementVersions??=[]).push("2.1.2");const k=globalThis,x=e=>e,$=k.trustedTypes,S=$?$.createPolicy("lit-html",{createHTML:e=>e}):void 0,M="$lit$",C=`lit$${Math.random().toFixed(9).slice(2)}$`,D="?"+C,P=`<${D}>`,T=document,I=()=>T.createComment(""),R=e=>null===e||"object"!=typeof e&&"function"!=typeof e,A=Array.isArray,z="[ \t\n\f\r]",F=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,N=/-->/g,E=/>/g,O=RegExp(`>|${z}(?:([^\\s"'>=/]+)(${z}*=${z}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),H=/'/g,L=/"/g,B=/^(?:script|style|textarea|title)$/i,j=e=>(t,...i)=>({_$litType$:e,strings:t,values:i}),q=j(1),W=j(2),U=Symbol.for("lit-noChange"),G=Symbol.for("lit-nothing"),V=new WeakMap,K=T.createTreeWalker(T,129);function Y(e,t){if(!A(e)||!e.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==S?S.createHTML(t):t}const J=(e,t)=>{const i=e.length-1,s=[];let n,o=2===t?"":3===t?"":"",a=F;for(let t=0;t"===l[0]?(a=n??F,c=-1):void 0===l[1]?c=-2:(c=a.lastIndex-l[2].length,r=l[1],a=void 0===l[3]?O:'"'===l[3]?L:H):a===L||a===H?a=O:a===N||a===E?a=F:(a=O,n=void 0);const d=a===O&&e[t+1].startsWith("/>")?" ":"";o+=a===F?i+P:c>=0?(s.push(r),i.slice(0,c)+M+i.slice(c)+C+d):i+C+(-2===c?t:d)}return[Y(e,o+(e[i]||"")+(2===t?"":3===t?"":"")),s]};class X{constructor({strings:e,_$litType$:t},i){let s;this.parts=[];let n=0,o=0;const a=e.length-1,r=this.parts,[l,c]=J(e,t);if(this.el=X.createElement(l,i),K.currentNode=this.el.content,2===t||3===t){const e=this.el.content.firstChild;e.replaceWith(...e.childNodes)}for(;null!==(s=K.nextNode())&&r.length0){s.textContent=$?$.emptyScript:"";for(let i=0;iA(e)||"function"==typeof e?.[Symbol.iterator])(e)?this.k(e):this._(e)}O(e){return this._$AA.parentNode.insertBefore(e,this._$AB)}T(e){this._$AH!==e&&(this._$AR(),this._$AH=this.O(e))}_(e){this._$AH!==G&&R(this._$AH)?this._$AA.nextSibling.data=e:this.T(T.createTextNode(e)),this._$AH=e}$(e){const{values:t,_$litType$:i}=e,s="number"==typeof i?this._$AC(e):(void 0===i.el&&(i.el=X.createElement(Y(i.h,i.h[0]),this.options)),i);if(this._$AH?._$AD===s)this._$AH.p(t);else{const e=new Q(s,this),i=e.u(this.options);e.p(t),this.T(i),this._$AH=e}}_$AC(e){let t=V.get(e.strings);return void 0===t&&V.set(e.strings,t=new X(e)),t}k(e){A(this._$AH)||(this._$AH=[],this._$AR());const t=this._$AH;let i,s=0;for(const n of e)s===t.length?t.push(i=new ee(this.O(I()),this.O(I()),this,this.options)):i=t[s],i._$AI(n),s++;s2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=G}_$AI(e,t=this,i,s){const n=this.strings;let o=!1;if(void 0===n)e=Z(this,e,t,0),o=!R(e)||e!==this._$AH&&e!==U,o&&(this._$AH=e);else{const s=e;let a,r;for(e=n[0],a=0;a{const s=i?.renderBefore??t;let n=s._$litPart$;if(void 0===n){const e=i?.renderBefore??null;s._$litPart$=n=new ee(t.insertBefore(I(),e),e,void 0,i??{})}return n._$AI(e),n},ce=globalThis;let he=class extends w{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){const e=super.createRenderRoot();return this.renderOptions.renderBefore??=e.firstChild,e}update(e){const t=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(e),this._$Do=le(t,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return U}};he._$litElement$=!0,he.finalized=!0,ce.litElementHydrateSupport?.({LitElement:he});const de=ce.litElementPolyfillSupport;de?.({LitElement:he}),(ce.litElementVersions??=[]).push("4.2.2");const pe=2,ue=e=>(...t)=>({_$litDirective$:e,values:t});let _e=class{constructor(e){}get _$AU(){return this._$AM._$AU}_$AT(e,t,i){this._$Ct=e,this._$AM=t,this._$Ci=i}_$AS(e,t){return this.update(e,t)}update(e,t){return this.render(...t)}};const me={},ge=ue(class extends _e{constructor(){super(...arguments),this.ot=me}render(e,t){return t()}update(e,[t,i]){if(Array.isArray(t)){if(Array.isArray(this.ot)&&this.ot.length===t.length&&t.every((e,t)=>e===this.ot[t]))return U}else if(this.ot===t)return U;return this.ot=Array.isArray(t)?Array.from(t):t,this.render(t,i)}}),{I:fe}=ae,ve=e=>e,be=()=>document.createComment(""),ye=(e,t,i)=>{const s=e._$AA.parentNode,n=void 0===t?e._$AB:t._$AA;if(void 0===i){const t=s.insertBefore(be(),n),o=s.insertBefore(be(),n);i=new fe(t,o,e,e.options)}else{const t=i._$AB.nextSibling,o=i._$AM,a=o!==e;if(a){let t;i._$AQ?.(e),i._$AM=e,void 0!==i._$AP&&(t=e._$AU)!==o._$AU&&i._$AP(t)}if(t!==n||a){let e=i._$AA;for(;e!==t;){const t=ve(e).nextSibling;ve(s).insertBefore(e,n),e=t}}}return i},we=(e,t,i=e)=>(e._$AI(t,i),e),ke={},xe=(e,t=ke)=>e._$AH=t,$e=e=>{e._$AR(),e._$AA.remove()},Se=(e,t,i)=>{const s=new Map;for(let n=t;n<=i;n++)s.set(e[n],n);return s},Me=ue(class extends _e{constructor(e){if(super(e),e.type!==pe)throw Error("repeat() can only be used in text expressions")}dt(e,t,i){let s;void 0===i?i=t:void 0!==t&&(s=t);const n=[],o=[];let a=0;for(const t of e)n[a]=s?s(t,a):a,o[a]=i(t,a),a++;return{values:o,keys:n}}render(e,t,i){return this.dt(e,t,i).values}update(e,[t,i,s]){const n=(e=>e._$AH)(e),{values:o,keys:a}=this.dt(t,i,s);if(!Array.isArray(n))return this.ut=a,o;const r=this.ut??=[],l=[];let c,h,d=0,p=n.length-1,u=0,_=o.length-1;for(;d<=p&&u<=_;)if(null===n[d])d++;else if(null===n[p])p--;else if(r[d]===a[u])l[u]=we(n[d],o[u]),d++,u++;else if(r[p]===a[_])l[_]=we(n[p],o[_]),p--,_--;else if(r[d]===a[_])l[_]=we(n[d],o[_]),ye(e,l[_+1],n[d]),d++,_--;else if(r[p]===a[u])l[u]=we(n[p],o[u]),ye(e,n[d],n[p]),p--,u++;else if(void 0===c&&(c=Se(a,u,_),h=Se(r,d,p)),c.has(r[d]))if(c.has(r[p])){const t=h.get(a[u]),i=void 0!==t?n[t]:null;if(null===i){const t=ye(e,n[d]);we(t,o[u]),l[u]=t}else l[u]=we(i,o[u]),ye(e,n[d],i),n[t]=null;u++}else $e(n[p]),p--;else $e(n[d]),d++;for(;u<=_;){const t=ye(e,l[_+1]);we(t,o[u]),l[u++]=t}for(;d<=p;){const e=n[d++];null!==e&&$e(e)}return this.ut=a,xe(e,l),U}}),Ce=new WeakMap;let De=0;class Pe extends he{constructor(){super(...arguments),this.title="",this.icon="",this.wide=!1,this.dismissOnScrim=!1,this.hass=null,this._opener=null,this._focusRoot=null,this._useHaDialog=!1,this._closing=!1,this._overlays=[],this._titleId="hp-dialog-title-"+ ++De,this._focusInitial=()=>{const e=this._focusableElements(),t=e.find(e=>e.hasAttribute("autofocus"))||e[0]||(this._useHaDialog?null:this.renderRoot.querySelector(".close"))||this.renderRoot.querySelector(".surface")||this.renderRoot.querySelector("ha-dialog");t?.focus({preventScroll:!0})},this._requestClose=()=>{this._closing||(this._closing=!0,this.dispatchEvent(new CustomEvent("hp-close",{bubbles:!0,composed:!0})))},this._onKeyDown=e=>{if("Escape"===e.key){e.preventDefault(),e.stopImmediatePropagation(),this._pruneOverlays();const t=this._overlays[this._overlays.length-1];return t?void this._closeOverlay(t,"escape"):void this._requestClose()}if("Tab"!==e.key||this._useHaDialog)return;const t=this.renderRoot.querySelector(".close"),i=t?[t,...this._focusableElements()]:this._focusableElements();if(!i.length)return e.preventDefault(),void this.renderRoot.querySelector(".surface")?.focus({preventScroll:!0});const s=this._deepActiveElement(),n=i[0],o=i[i.length-1];!e.shiftKey||s!==n&&i.includes(s)?e.shiftKey||s!==o||(e.preventDefault(),n.focus()):(e.preventDefault(),o.focus())},this._onFallbackCancel=e=>{e.preventDefault(),this._requestClose()},this._onFallbackClick=e=>{this.dismissOnScrim&&e.target===e.currentTarget&&this._requestClose()}}connectedCallback(){super.connectedCallback(),this._opener=this._deepActiveElement();const e=this.getRootNode();this._focusRoot=e;const t=Ce.get(e)||{dialogs:new Set,opener:this._opener};t.dialogs.add(this),Ce.set(e,t),this._useHaDialog=!!customElements.get("ha-dialog"),this.addEventListener("keydown",this._onKeyDown,!0)}disconnectedCallback(){this.removeEventListener("keydown",this._onKeyDown,!0);const e=[...this._overlays];this._overlays=[];for(const t of e.reverse())t.close("disconnect");const t=this._focusRoot,i=this._opener;this._opener=null,this._focusRoot=null;const s=t?Ce.get(t):void 0;s?.dialogs.delete(this),super.disconnectedCallback(),t&&s&&requestAnimationFrame(()=>{const e=Ce.get(t);if(!e)return;if(!e.dialogs.size)return e.opener?.isConnected&&e.opener.focus({preventScroll:!0}),void Ce.delete(t);const s=i?.closest("hp-dialog");i?.isConnected&&s&&e.dialogs.has(s)&&i.focus({preventScroll:!0})})}firstUpdated(e){if(super.firstUpdated(e),!this._useHaDialog){const e=this.renderRoot.querySelector("dialog");e&&!e.open&&e.showModal()}queueMicrotask(()=>this._focusInitial())}_deepActiveElement(){let e=document.activeElement;for(;e?.shadowRoot?.activeElement;)e=e.shadowRoot.activeElement;return e&&e!==document.body?e:null}_focusableElements(){const e=["[autofocus]","a[href]","button:not([disabled])",'input:not([disabled]):not([type="hidden"])',"select:not([disabled])","textarea:not([disabled])",'[contenteditable="true"]','[tabindex]:not([tabindex="-1"])'].join(","),t=[],i=new Set,s=n=>{if(!i.has(n)){if(i.add(n),n instanceof HTMLElement){if(n.matches(e)&&t.push(n),n instanceof HTMLSlotElement){for(const e of n.assignedNodes({flatten:!0}))s(e);return}if(n.shadowRoot){for(const e of n.shadowRoot.childNodes)s(e);return}}for(const e of n.childNodes)s(e)}};for(const e of this.childNodes)s(e);const n=this.overlayPortal();return n&&s(n),t.filter(e=>{let t=e;for(;t;){const e=getComputedStyle(t);if(t.hidden||t.inert||"true"===t.getAttribute("aria-hidden")||"none"===e.display||"hidden"===e.visibility)return!1;if(t=t.assignedSlot||t.parentElement||(t.getRootNode()instanceof ShadowRoot?t.getRootNode().host:null),t===this)break}return!0})}_pruneOverlays(){this._overlays=this._overlays.filter(e=>e.owner.isConnected)}_closeOverlay(e,t){const i=this._overlays.findIndex(t=>t.token===e.token);i>=0&&this._overlays.splice(i,1),e.close(t)}registerOverlay(e){this._pruneOverlays();const t=this._overlays.find(t=>t.owner===e.owner);t&&this._overlays.splice(this._overlays.indexOf(t),1);const i=e.group||"transient";for(const e of[...this._overlays].reverse())e.group===i&&this._closeOverlay(e,"exclusive");const s={...e,group:i,token:Symbol("hp-overlay")};this._overlays.push(s);let n=!1;return()=>{if(n)return;n=!0;const e=this._overlays.findIndex(e=>e.token===s.token);e>=0&&this._overlays.splice(e,1)}}closeTransientOverlays(e="outside"){this._pruneOverlays();const t=[...this._overlays].filter(e=>"transient"===(e.group||"transient"));for(const i of t.reverse())this._closeOverlay(i,e);return t.length>0}overlayPortal(){return this.renderRoot.querySelector(".overlay-portal")}render(){const e=q` - ${this.icon?q``:G} - ${this.title} - `;return this._useHaDialog?q` - ${e} - - -
`:q` -
-
- ${e} - -
-
- -
-
-
`}}Pe.properties={title:{type:String},icon:{type:String},wide:{type:Boolean,reflect:!0},dismissOnScrim:{type:Boolean,attribute:"dismiss-on-scrim"},hass:{attribute:!1}},Pe.styles=o` - :host { - display: contents; - color: var(--primary-text-color, #e6e7eb); - font: inherit; - } - - ha-dialog { - --dialog-content-padding: 0; - --dialog-surface-background: var(--card-background-color, var(--hp-bg, #202126)); - --ha-dialog-border-radius: var(--rad-l, 18px); - /* HA's ha-dialog-header defaults this custom property to a one-line - fixed height. Our localized slot is intentionally allowed to wrap, - so leaving that default in place clips every line after the first at - the bottom of the header. auto is HA's public sizing hook and also - stays harmless on older ha-dialog implementations that do not consume - it. */ - --ha-dialog-header-title-height: auto; - color: inherit; - } - - ha-dialog::part(dialog) { - border: 1px solid var(--hp-accent, #d89300); - box-shadow: var(--shadow-3, 0 18px 48px rgb(0 0 0 / 0.34)); - overflow: hidden; - } - - /* The HA header slot is a flex item with a constrained inline size. The - old inline-flex title kept its min-content width, so HA clipped the last - word instead of giving it a second line. Keep every wrapper shrinkable - and let the text wrap; this applies to every hp-dialog, including long - device names and translated titles. */ - .header-title-slot { - display: block; - flex: 1 1 auto; - width: 100%; - max-width: 100%; - min-width: 0; - box-sizing: border-box; - white-space: normal; - } - - .title { - display: flex; - flex: 1 1 auto; - align-items: center; - gap: var(--sp-4, 12px); - width: 100%; - max-width: 100%; - min-width: 0; - font-weight: 600; - line-height: 1.25; - white-space: normal; - } - - .title ha-icon { - flex: none; - color: var(--hp-accent, #d89300); - } - - .title-text { - flex: 1 1 auto; - min-width: 0; - max-width: 100%; - white-space: normal; - overflow-wrap: anywhere; - word-break: normal; - } - - .footer { - /* ha-dialog lays out its footer slot as a flex row. display: contents - exposed the consumer's action row as a shrink-to-fit flex item, so a - wide device dialog got a half-width divider and its Hide action slid - toward the centre. Keep one full-width slot item in both HA and the - native fallback. */ - display: block; - width: 100%; - min-width: 0; - box-sizing: border-box; - } - - ::slotted([slot='footer']) { - width: 100%; - box-sizing: border-box; - } - - dialog { - width: auto; - max-width: none; - max-height: none; - margin: auto; - padding: 0; - border: 0; - overflow: visible; - color: inherit; - background: transparent; - } - - dialog::backdrop { - background: rgb(0 0 0 / 0.45); - } - - .surface { - width: min(360px, 92vw); - max-height: 92vh; - display: flex; - flex-direction: column; - overflow: hidden; - box-sizing: border-box; - color: inherit; - background: var(--card-background-color, var(--hp-bg, #202126)); - border: 1px solid var(--hp-accent, #d89300); - border-radius: var(--rad-l, 18px); - box-shadow: var(--shadow-3, 0 18px 48px rgb(0 0 0 / 0.34)); - } - - :host([wide]) .surface { - width: min(var(--hp-dialog-wide-width, 500px), 94vw); - } - - .header { - min-height: 56px; - display: flex; - align-items: center; - gap: var(--sp-4, 12px); - padding: var(--sp-4, 12px) var(--sp-5, 16px); - box-sizing: border-box; - border-bottom: 1px solid var(--hp-line, rgb(255 255 255 / 0.12)); - } - - .header .title { - flex: 1; - } - - .close { - width: 40px; - height: 40px; - flex: none; - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0; - color: inherit; - background: transparent; - border: 0; - border-radius: 50%; - cursor: pointer; - } - - :host([data-pointer-hover]) .close:hover, - .close:focus-visible { - background: rgb(127 127 127 / 0.16); - } - - .content { - min-height: 0; - display: flex; - flex-direction: column; - } - - .overlay-portal { - position: fixed; - z-index: 2147483647; - inset: 0; - overflow: visible; - pointer-events: none; - } - - .overlay-portal:empty { - display: none; - } - - .overlay-portal > * { - pointer-events: auto; - } - `,customElements.get("hp-dialog")||customElements.define("hp-dialog",Pe);const Te=/^#[0-9a-fA-F]{6}$/;function Ie(e,t){return"string"==typeof e&&Te.test(e)?e:t}const Re="(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])",Ae=new RegExp(`^rgb\\(${Re}, ${Re}, ${Re}\\)$`);function ze(e){if(!Array.isArray(e)||e.length<3)return null;const t=e.slice(0,3);if(!t.every(e=>"number"==typeof e&&Number.isFinite(e)))return null;const[i,s,n]=t.map(e=>Math.round(Math.min(255,Math.max(0,e))));return`rgb(${i}, ${s}, ${n})`}const Fe=(e,t,i)=>Math.min(i,Math.max(t,Number.isFinite(e)?e:t));function Ne(e){return Number.isFinite(e)?(e%360+360)%360:0}function Ee(e){if("string"!=typeof e)return null;const t=e.trim().replace(/^#/,""),i=/^[0-9a-fA-F]{3}$/.test(t)?t.split("").map(e=>`${e}${e}`).join(""):t;return/^[0-9a-fA-F]{6}$/.test(i)?`#${i.toLowerCase()}`:null}function Oe(e){const t=Ee(e);return t?{r:Number.parseInt(t.slice(1,3),16),g:Number.parseInt(t.slice(3,5),16),b:Number.parseInt(t.slice(5,7),16)}:null}function He(e){const t=Fe(e.r,0,255)/255,i=Fe(e.g,0,255)/255,s=Fe(e.b,0,255)/255,n=Math.max(t,i,s),o=n-Math.min(t,i,s);let a=0;return o>0&&(a=n===t?(i-s)/o%6*60:n===i?60*((s-t)/o+2):60*((t-i)/o+4)),{h:Ne(a),s:0===n?0:o/n*100,v:100*n}}function Le(e){return function(e){const t=e=>Math.round(Fe(e,0,255)).toString(16).padStart(2,"0");return`#${t(e.r)}${t(e.g)}${t(e.b)}`}(function(e){const t=Ne(e.h),i=Fe(e.s,0,100)/100,s=Fe(e.v,0,100)/100,n=s*i,o=n*(1-Math.abs(t/60%2-1)),a=s-n;let r;return r=t<60?[n,o,0]:t<120?[o,n,0]:t<180?[0,n,o]:t<240?[0,o,n]:t<300?[o,0,n]:[n,0,o],{r:Math.round(255*(r[0]+a)),g:Math.round(255*(r[1]+a)),b:Math.round(255*(r[2]+a))}}(e))}function Be(e){const t=e.visualViewport;return t&&Number.isFinite(t.width)&&Number.isFinite(t.height)?{left:Number(t.offsetLeft)||0,top:Number(t.offsetTop)||0,width:Math.max(0,Number(t.width)||0),height:Math.max(0,Number(t.height)||0)}:{left:0,top:0,width:e.innerWidth,height:e.innerHeight}}function je(e,t,i,s=7,n=8){const o=i.left+n,a=i.top+n,r=Math.max(o,i.left+i.width-n),l=Math.max(a,i.top+i.height-n),c=Math.max(0,r-o),h=Math.max(0,l-a),d=Math.min(Math.max(0,t.width),c),p=Math.min(Math.max(0,t.height),h);let u=e.left;u+d>r&&(u=e.right-d),u=Math.min(Math.max(o,u),Math.max(o,r-d));const _=e.bottom+s,m=e.top-s-p;let g="bottom",f=_;return _+p>l&&m>=a?(g="top",f=Math.min(Math.max(a,m),Math.max(a,l-p))):f=Math.min(Math.max(a,_),Math.max(a,l-p)),{left:Math.round(u),top:Math.round(f),side:g,maxWidth:Math.round(c),maxHeight:Math.round(h)}}class qe{constructor(e,t,i){this.owner=e,this.name=t,this.keydown=i,this.host=null,this.root=null}window(){return this.owner.ownerDocument.defaultView}dialog(){return this.owner.closest("hp-dialog")}usesPopover(e=!1){const t=this.window();return!e&&!!t&&function(e){const t=e.HTMLElement?.prototype;return"function"==typeof t?.showPopover&&"function"==typeof t?.hidePopover}(t)}get hasFallback(){return!!this.host}containsPath(e){return e.includes(this.owner)||!!this.host&&e.includes(this.host)}ownsActiveElement(){let e=this.owner.ownerDocument.activeElement;for(;e?.shadowRoot?.activeElement;)e=e.shadowRoot.activeElement;return!(!e||e!==this.owner&&!this.owner.shadowRoot?.contains(e)&&!this.root?.contains(e))}renderFallback(e,t){const i=this.ensureFallback();return i?(le(q`${e}`,i),i):null}surface(e,t){return t?this.owner.shadowRoot?.querySelector(e)||null:this.root?.querySelector(e)||null}destroy(){this.root&&le(G,this.root),this.host?.remove(),this.host=null,this.root=null}ensureFallback(){if(this.root?.isConnected)return this.root;const e=this.dialog()?.overlayPortal()||this.owner.ownerDocument.body;if(!e)return null;const t=this.owner.ownerDocument.createElement("div");return t.dataset.hpOverlay=this.name,t.style.display="contents",this.keydown&&t.addEventListener("keydown",this.keydown,!0),e.append(t),this.host=t,this.root=t.attachShadow({mode:"open"}),this.root}}const We={title:"Color picker",hue:"Hue",saturation:"Saturation",value:"Brightness",hex:"Hex color",invalidHex:"Enter a 3- or 6-digit hex color"};class Ue extends he{constructor(){super(...arguments),this.label="",this.opacityLabel="Opacity",this.color="#607d8b",this.opacity=1,this.disabled=!1,this.showOpacity=!0,this.pickerLabels=We,this._open=!1,this._pickerRaf=0,this._emitRaf=0,this._forceFallback=!1,this._overlayDispose=null,this._hue=0,this._saturation=0,this._value=0,this._hexDraft="#607d8b",this._hexInvalid=!1,this._lastValidColor="#607d8b",this._activePointerId=null,this._outsidePointerDown=e=>{if(!this._open)return;const t=e.composedPath();this._floating.containsPath(t)||this._closePicker(!1,"outside")},this._keyDown=e=>{this._open&&"Escape"===e.key&&(e.preventDefault(),e.stopImmediatePropagation(),this._closePicker(this._floating.ownsActiveElement(),"escape"))},this._floating=new qe(this,"color-opacity",this._keyDown),this._queuePickerPosition=()=>{if(!this._open)return;this._pickerRaf&&cancelAnimationFrame(this._pickerRaf);const e=this._window();e&&(this._pickerRaf=e.requestAnimationFrame(()=>{this._pickerRaf=0,this._positionPicker()||this._closePicker()}))}}connectedCallback(){super.connectedCallback(),this._syncFromColor(this.color,!1),this.ownerDocument.addEventListener("pointerdown",this._outsidePointerDown,!0),this.ownerDocument.addEventListener("scroll",this._queuePickerPosition,!0);const e=this.ownerDocument.defaultView;e?.addEventListener("resize",this._queuePickerPosition),e?.addEventListener("orientationchange",this._queuePickerPosition),e?.visualViewport?.addEventListener("resize",this._queuePickerPosition),e?.visualViewport?.addEventListener("scroll",this._queuePickerPosition),this.addEventListener("keydown",this._keyDown,!0)}disconnectedCallback(){this.ownerDocument.removeEventListener("pointerdown",this._outsidePointerDown,!0),this.ownerDocument.removeEventListener("scroll",this._queuePickerPosition,!0);const e=this.ownerDocument.defaultView;e?.removeEventListener("resize",this._queuePickerPosition),e?.removeEventListener("orientationchange",this._queuePickerPosition),e?.visualViewport?.removeEventListener("resize",this._queuePickerPosition),e?.visualViewport?.removeEventListener("scroll",this._queuePickerPosition),this.removeEventListener("keydown",this._keyDown,!0),this._pickerRaf&&cancelAnimationFrame(this._pickerRaf),this._emitRaf&&cancelAnimationFrame(this._emitRaf),this._pickerRaf=0,this._emitRaf=0,this._closePicker(!1,"disconnect"),super.disconnectedCallback()}updated(e){if(e.has("color")){const e=Ee(this.color);e&&e!==this._lastValidColor&&this._syncFromColor(e,this._open)}this.disabled&&this._open?this._closePicker():this._open&&(this._supportsPopover()||this._renderFallback(),(e.has("color")||e.has("opacity")||e.has("showOpacity")||e.has("label")||e.has("opacityLabel")||e.has("pickerLabels"))&&this._queuePickerPosition())}_window(){return this._floating.window()}_supportsPopover(){return this._floating.usesPopover(this._forceFallback)}_dialog(){return this._floating.dialog()}async _toggle(){this.disabled||(this._open?this._closePicker():(this._forceFallback=!1,this._syncFromColor(this.color,!1),this._hexInvalid=!1,this._activePointerId=null,this._open=!0,await this.updateComplete,this._open&&(this._supportsPopover()||this._renderFallback(),this._positionPicker()?this._overlayDispose=this._dialog()?.registerOverlay({owner:this,group:"transient",close:e=>this._closePicker("escape"===e&&this._floating.ownsActiveElement(),e)})||null:this._closePicker())))}_closePicker(e=!1,t="exclusive"){if(!this._open&&!this._floating.hasFallback)return;const i=this._overlayDispose;this._overlayDispose=null,i?.();const s=this.renderRoot.querySelector(".picker");if(s?.hidePopover)try{s.matches(":popover-open")&&s.hidePopover()}catch{}this._open=!1,this._cancelQueuedEmit(),this._activePointerId=null,this._floating.destroy(),e&&this.updateComplete.then(()=>this.renderRoot.querySelector(".trigger")?.focus())}_surface(){return this._floating.surface(".picker",this._supportsPopover())}_renderFallback(){const e=Ue.styles.cssText;this._floating.renderFallback(this._pickerTemplate(!1),e)}_positionPicker(){if(!this._open)return!1;const e=this._window(),t=this.renderRoot.querySelector(".trigger");let i=this._surface();if(!e||!t?.isConnected||!i?.isConnected)return!1;const s=Be(e);if(i.style.maxWidth=`${Math.max(0,s.width-16)}px`,i.style.maxHeight=`${Math.max(0,s.height-16)}px`,i.style.visibility="hidden",this._supportsPopover()&&i.showPopover)try{i.matches(":popover-open")||i.showPopover()}catch{if(this._forceFallback=!0,this._renderFallback(),i=this._surface(),!i?.isConnected)return!1;i.style.maxWidth=`${Math.max(0,s.width-16)}px`,i.style.maxHeight=`${Math.max(0,s.height-16)}px`,i.style.visibility="hidden"}const n=t.getBoundingClientRect(),o=i.getBoundingClientRect();if(!(n.width&&n.height&&o.width&&o.height))return!1;const a=je(n,o,s);return i.style.left=`${a.left}px`,i.style.top=`${a.top}px`,i.dataset.side=a.side,i.style.visibility="",!0}_labels(){const e=this.pickerLabels||We;return{title:e.title||We.title,hue:e.hue||We.hue,saturation:e.saturation||We.saturation,value:e.value||We.value,hex:e.hex||We.hex,invalidHex:e.invalidHex||We.invalidHex}}_syncFromColor(e,t){const i=Ee(Ie(e,"#607d8b"))||"#607d8b",s=Oe(i);if(!s)return;const n=He(s);(!t||n.s>1e-4)&&(this._hue=n.h),this._saturation=n.s,this._value=n.v,this._hexDraft=i,this._hexInvalid=!1,this._lastValidColor=i}_cancelQueuedEmit(e=!1){this._emitRaf&&cancelAnimationFrame(this._emitRaf),this._emitRaf=0,e&&this._syncFromColor(this._lastValidColor,!0)}_queueHsvEmit(){if(this._emitRaf)return;const e=this._window();e?this._emitRaf=e.requestAnimationFrame(()=>{this._emitRaf=0,this._emitHsv()}):this._emitHsv()}_emitHsv(){this._emit(Le({h:this._hue,s:this._saturation,v:this._value}),this.opacity)}_setHue(e){this._hue=Math.min(359,Math.max(0,Number.isFinite(e)?e:0)),this._emitHsv()}_setSaturation(e){this._saturation=Math.min(100,Math.max(0,Number.isFinite(e)?e:0)),this._emitHsv()}_setValue(e){this._value=Math.min(100,Math.max(0,Number.isFinite(e)?e:0)),this._emitHsv()}_setOpacity(e){this._emit(this._lastValidColor,e)}_shiftRangeKey(e,t,i,s,n){if(!e.shiftKey)return;const o="ArrowRight"===e.key||"ArrowUp"===e.key?10:"ArrowLeft"===e.key||"ArrowDown"===e.key?-10:0;o&&(e.preventDefault(),e.stopPropagation(),n(Math.min(s,Math.max(i,t+o))))}_setSvFromPointer(e,t){const i=e.currentTarget.getBoundingClientRect();i.width&&i.height&&(this._saturation=Math.min(100,Math.max(0,(e.clientX-i.left)/i.width*100)),this._value=Math.min(100,Math.max(0,100*(1-(e.clientY-i.top)/i.height))),t?(this._cancelQueuedEmit(),this._emitHsv()):this._queueHsvEmit())}_svPointerDown(e){if(e.preventDefault(),e.stopPropagation(),null===this._activePointerId){this._activePointerId=e.pointerId;try{e.currentTarget.setPointerCapture(e.pointerId)}catch{}this._setSvFromPointer(e,!0)}}_svPointerMove(e){e.pointerId===this._activePointerId&&(e.preventDefault(),e.stopPropagation(),this._setSvFromPointer(e,!1))}_svPointerUp(e){if(e.pointerId===this._activePointerId){e.preventDefault(),e.stopPropagation(),this._setSvFromPointer(e,!0),this._activePointerId=null;try{e.currentTarget.releasePointerCapture(e.pointerId)}catch{}}}_svPointerCancel(e){e.pointerId===this._activePointerId&&(e.preventDefault(),e.stopPropagation(),this._activePointerId=null,this._cancelQueuedEmit(!0))}_svLostPointerCapture(e){e.pointerId===this._activePointerId&&(this._activePointerId=null,this._cancelQueuedEmit(!0))}_hexInput(e){const t=e.target.value;this._hexDraft=t,this._hexInvalid=!1;const i=Ee(t),s=i?Oe(i):null;if(!i||!s)return;const n=He(s);n.s>1e-4&&(this._hue=n.h),this._saturation=n.s,this._value=n.v,this._emit(i,this.opacity,!0)}_commitHex(){const e=Ee(this._hexDraft);if(!e)return this._hexDraft=this._lastValidColor,void(this._hexInvalid=!0);this._hexDraft=e,this._hexInvalid=!1,e!==this._lastValidColor&&this._emit(e,this.opacity)}_hexKeyDown(e){"Enter"===e.key&&(e.preventDefault(),e.stopPropagation(),this._commitHex())}_emit(e,t,i=!1){const s=Ee(e)||this._lastValidColor,n=Number.isFinite(t)?t:Number(this.opacity)||0,o=Math.min(1,Math.max(0,n));this.color=s,this.opacity=o,this._lastValidColor=s,i||(this._hexDraft=s),this._hexInvalid=!1,this.dispatchEvent(new CustomEvent("hp-color-opacity-change",{detail:{color:s,opacity:o},bubbles:!0,composed:!0}))}_pickerTemplate(e){const t=Math.round(100*Math.min(1,Math.max(0,Number(this.opacity)||0))),i=Le({h:this._hue,s:this._saturation,v:this._value}),s=Le({h:this._hue,s:100,v:100}),n=this._labels(),o=Math.round(this._hue),a=Math.round(this._saturation),r=Math.round(this._value);return q` - `}render(){const e=Math.round(100*Math.min(1,Math.max(0,Number(this.opacity)||0))),t=Ie(this.color,"#607d8b"),i=this.showOpacity?`${this.label||"Color"}: ${t}, ${e}%`:`${this.label||"Color"}: ${t}`;return q` - ${this.label?q`${this.label}`:G} - - ${this._open&&!this.disabled&&this._supportsPopover()?this._pickerTemplate(!0):G} - `}}Ue.properties={label:{type:String},opacityLabel:{type:String,attribute:"opacity-label"},color:{type:String},opacity:{type:Number},disabled:{type:Boolean,reflect:!0},showOpacity:{type:Boolean,attribute:"show-opacity"},pickerLabels:{attribute:!1},_open:{state:!0},_hue:{state:!0},_saturation:{state:!0},_value:{state:!0},_hexDraft:{state:!0},_hexInvalid:{state:!0}},Ue.styles=o` - :host { - position: relative; - display: inline-flex; - align-items: center; - gap: 6px; - min-width: 0; - } - .label { - font-size: 12px; - color: var(--secondary-text-color, #9aa4ad); - white-space: nowrap; - } - .trigger { - width: 40px; - height: 40px; - flex: none; - box-sizing: border-box; - padding: 3px; - border: 1px solid var(--divider-color, #666); - border-radius: 7px; - background: - linear-gradient(45deg, #b8b8b8 25%, transparent 25%) 0 0 / 10px 10px, - linear-gradient(45deg, transparent 75%, #b8b8b8 75%) 0 0 / 10px 10px, - linear-gradient(45deg, transparent 75%, #b8b8b8 75%) 5px -5px / 10px 10px, - linear-gradient(45deg, #b8b8b8 25%, #eee 25%) 5px 5px / 10px 10px; - cursor: pointer; - } - :host([data-pointer-hover]) .trigger:hover { - border-color: var(--primary-color, #03a9f4); - } - .trigger[aria-expanded='true'] { - border-color: var(--primary-color, #03a9f4); - outline: none; - box-shadow: 0 0 0 1px var(--primary-color, #03a9f4); - } - .trigger:focus-visible { - border-color: var(--primary-color, #03a9f4); - outline: 2px solid var(--primary-color, #03a9f4); - outline-offset: 2px; - } - .swatch { - display: block; - width: 100%; - height: 100%; - border-radius: 3px; - box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.18); - pointer-events: none; - } - .picker { - /* The popover attribute promotes this surface into the browser top layer, outside - hp-dialog's scrolling/clipping body. JS supplies viewport coordinates. */ - position: fixed; - z-index: 2147483647; - inset: auto; - top: 0; - left: 0; - width: min(292px, calc(100vw - 16px)); - max-height: calc(100vh - 16px); - margin: 0; - box-sizing: border-box; - display: grid; - gap: 10px; - padding: 12px; - overflow: auto; - color: var(--primary-text-color, #fff); - background: var(--card-background-color, #202126); - border: 1px solid var(--primary-color, #03a9f4); - border-radius: 10px; - box-shadow: 0 10px 28px rgb(0 0 0 / 0.34); - } - .picker[popover]:not(:popover-open) { - /* Keep the author-level display:grid from exposing the surface for one - frame before showPopover() promotes it into the top layer. */ - display: none; - } - .picker-head, - .row, - .control-head { - display: flex; - align-items: center; - gap: 8px; - min-width: 0; - } - .picker-head { - justify-content: space-between; - } - .caption { - flex: 1; - min-width: 0; - overflow: hidden; - color: var(--secondary-text-color, #9aa4ad); - font-size: 12px; - text-overflow: ellipsis; - white-space: nowrap; - } - .preview { - width: 52px; - height: 30px; - flex: none; - box-sizing: border-box; - padding: 3px; - border: 1px solid var(--divider-color, #666); - border-radius: 7px; - background: - linear-gradient(45deg, #b8b8b8 25%, transparent 25%) 0 0 / 10px 10px, - linear-gradient(45deg, transparent 75%, #b8b8b8 75%) 0 0 / 10px 10px, - linear-gradient(45deg, transparent 75%, #b8b8b8 75%) 5px -5px / 10px 10px, - linear-gradient(45deg, #b8b8b8 25%, #eee 25%) 5px 5px / 10px 10px; - } - .preview > span { - display: block; - width: 100%; - height: 100%; - border-radius: 3px; - } - .sv-field { - position: relative; - width: 100%; - height: 132px; - overflow: hidden; - box-sizing: border-box; - border: 1px solid var(--divider-color, #666); - border-radius: 8px; - background: - linear-gradient(to top, #000, transparent), - linear-gradient(to right, #fff, var(--hp-picker-hue, #f00)); - cursor: crosshair; - touch-action: none; - user-select: none; - } - .sv-thumb { - position: absolute; - left: var(--hp-picker-saturation, 0%); - top: var(--hp-picker-value, 100%); - width: 14px; - height: 14px; - box-sizing: border-box; - border: 2px solid #fff; - border-radius: 50%; - box-shadow: 0 0 0 1px rgb(0 0 0 / .65), 0 1px 3px rgb(0 0 0 / .5); - transform: translate(-50%, -50%); - pointer-events: none; - } - .control { - display: grid; - gap: 2px; - min-width: 0; - } - .control-value { - flex: none; - color: var(--secondary-text-color, #9aa4ad); - font-size: 12px; - } - input[type='range'] { - width: auto; - height: 40px; - min-width: 0; - flex: 1; - margin: -5px 0; - accent-color: var(--primary-color, #03a9f4); - touch-action: none; - } - .hue-range { - accent-color: var(--hp-picker-hue, #f00); - --hp-picker-hue-track: linear-gradient(to right, - #f00 0%, #ff0 16.667%, #0f0 33.333%, #0ff 50%, - #00f 66.667%, #f0f 83.333%, #f00 100%); - --hp-picker-hue-thumb-shadow: - 0 0 0 2px var(--card-background-color, #202126), - 0 0 0 3px var(--primary-text-color, #fff); - } - .hue-range::-webkit-slider-runnable-track { - height: 10px; - box-sizing: border-box; - border: 1px solid var(--divider-color, #666); - border-radius: 999px; - background: var(--hp-picker-hue-track); - } - .hue-range::-moz-range-track { - height: 10px; - box-sizing: border-box; - border: 1px solid var(--divider-color, #666); - border-radius: 999px; - background: var(--hp-picker-hue-track); - } - .hue-range::-moz-range-progress { - height: 10px; - border: 0; - border-radius: 999px; - background: transparent; - } - .hue-range::-webkit-slider-thumb { - border-radius: 50%; - box-shadow: var(--hp-picker-hue-thumb-shadow); - } - .hue-range::-moz-range-thumb { - border-radius: 50%; - box-shadow: var(--hp-picker-hue-thumb-shadow); - } - input[type='text'], - input[type='number'] { - min-height: 40px; - box-sizing: border-box; - border: 1px solid var(--divider-color, #666); - border-radius: 6px; - padding: 6px 8px; - color: var(--primary-text-color, #fff); - background: var(--input-fill-color, transparent); - font: inherit; - } - input[type='text'] { - width: 100%; - } - input[type='number'] { - width: 50px; - flex: none; - } - input:focus-visible, - .sv-field:focus-visible { - outline: 2px solid var(--primary-color, #03a9f4); - outline-offset: 1px; - } - input[aria-invalid='true'] { - border-color: var(--error-color, #db4437); - } - .error { - color: var(--error-color, #db4437); - font-size: 11px; - line-height: 1.25; - } - .pct { - color: var(--secondary-text-color, #9aa4ad); - font-size: 12px; - } - :host([disabled]) { - opacity: .5; - pointer-events: none; - } - @media (prefers-reduced-motion: reduce) { - .picker { transition: none !important; } - } - @media (forced-colors: active) { - .hue-range::-webkit-slider-runnable-track { - border-color: ButtonText; - background: Canvas; - } - .hue-range::-moz-range-track { - border-color: ButtonText; - background: Canvas; - } - .hue-range::-webkit-slider-thumb { - box-shadow: none; - } - .hue-range::-moz-range-thumb { - box-shadow: none; - } - } - `,customElements.get("hp-color-opacity")||customElements.define("hp-color-opacity",Ue);let Ge=0;class Ve extends he{constructor(){super(...arguments),this.text="",this.ariaLabel="",this.pointerHover=!1,this._open=!1,this._openTimer=0,this._closeTimer=0,this._positionRaf=0,this._forceFallback=!1,this._hoverOwned=!1,this._overlayDispose=null,this._scrollDialog=null,this._descriptionId="hp-help-description-"+ ++Ge,this._surfacePointerEnter=()=>{const e=this._window();this._closeTimer&&e?.clearTimeout(this._closeTimer),this._closeTimer=0},this._surfacePointerLeave=()=>{this._scheduleClose()},this._outsidePointerDown=e=>{if(!this._open)return;const t=e.composedPath();this._floating.containsPath(t)||this._closeHelp(!1,"outside")},this._dialogScroll=e=>{const t=this._floating.containsPath(e.composedPath()),i=e.target,s=this._dialog(),n=i instanceof Node&&(i===s||i instanceof Element&&i.contains(this));var o;this._open&&(o=n,!t&&o)&&this._closeHelp(!1,"scroll")},this._keyDown=e=>{if(!this._open||e.defaultPrevented)return;if("Escape"===e.key)return e.preventDefault(),e.stopImmediatePropagation(),void this._closeHelp(this._floating.ownsActiveElement(),"escape");const t=this.renderRoot.querySelector(".trigger");if(!t||!e.composedPath().includes(t))return;const i=this._surface();if(!i||i.scrollHeight<=i.clientHeight)return;const s=Math.max(40,.8*i.clientHeight),n={ArrowDown:40,ArrowUp:-40,PageDown:s,PageUp:-s};if("Home"===e.key)i.scrollTop=0;else if("End"===e.key)i.scrollTop=i.scrollHeight;else{if(!(e.key in n))return;i.scrollBy({top:n[e.key],behavior:"auto"})}e.preventDefault(),e.stopImmediatePropagation()},this._floating=new qe(this,"help",this._keyDown),this._queuePosition=()=>{if(!this._open||this._positionRaf)return;const e=this._window();e&&(this._positionRaf=e.requestAnimationFrame(()=>{this._positionRaf=0,this._position()||this._closeHelp()}))}}connectedCallback(){super.connectedCallback(),this.addEventListener("keydown",this._keyDown,!0)}willUpdate(){this.toggleAttribute("data-has-content",this._hasContent())}disconnectedCallback(){const e=this.ownerDocument.defaultView;this._unsubscribeOpenListeners(),this.removeEventListener("keydown",this._keyDown,!0),this._clearTimers(),this._positionRaf&&e?.cancelAnimationFrame(this._positionRaf),this._positionRaf=0,this._closeHelp(!1,"disconnect"),super.disconnectedCallback()}updated(e){if(e.has("pointerHover")&&!this.pointerHover){const e=this._window();this._openTimer&&e?.clearTimeout(this._openTimer),this._openTimer=0,this._hoverOwned&&this._closeHelp(!1,"outside")}this._hasContent()||!this._open?this._open&&e.has("text")&&(this._usesPopover()||this._renderFallback(),this._queuePosition()):this._closeHelp()}_window(){return this._floating.window()}_dialog(){return this._floating.dialog()}_usesPopover(){return this._floating.usesPopover(this._forceFallback)}_hasContent(){return e=this.text,t=this.ariaLabel,"string"==typeof e&&e.trim().length>0&&"string"==typeof t&&t.trim().length>0;var e,t}_clearTimers(){const e=this._window();this._openTimer&&e?.clearTimeout(this._openTimer),this._closeTimer&&e?.clearTimeout(this._closeTimer),this._openTimer=0,this._closeTimer=0}_scheduleOpen(){const e=this._window();this.pointerHover&&e&&!this._open&&!this._openTimer&&(this._closeTimer&&e.clearTimeout(this._closeTimer),this._closeTimer=0,this._openTimer=e.setTimeout(()=>{this._openTimer=0,this.isConnected&&(this._hoverOwned=!0,this._openHelp())},300))}_scheduleClose(){const e=this._window(),t=this.renderRoot.querySelector(".trigger");e&&!t?.matches(":focus-visible")&&(this._openTimer&&e.clearTimeout(this._openTimer),this._closeTimer&&e.clearTimeout(this._closeTimer),this._openTimer=0,this._closeTimer=e.setTimeout(()=>{this._closeTimer=0,this.isConnected&&this._closeHelp()},150))}_triggerPointerEnter(e){this.pointerHover&&"mouse"===e.pointerType&&this._scheduleOpen()}_triggerPointerLeave(e){"mouse"===e.pointerType&&this._scheduleClose()}_triggerFocus(){queueMicrotask(()=>{const e=this.renderRoot.querySelector(".trigger");e?.matches(":focus-visible")&&(this._hoverOwned=!1,this._openHelp())})}_triggerBlur(){this._scheduleClose()}_triggerClick(){this._open?this._closeHelp():(this._hoverOwned=!1,this._openHelp())}_subscribeOpenListeners(){this.ownerDocument.addEventListener("pointerdown",this._outsidePointerDown,!0),this.ownerDocument.addEventListener("keydown",this._keyDown,!0);const e=this._window();e?.addEventListener("resize",this._queuePosition),e?.addEventListener("orientationchange",this._queuePosition),e?.visualViewport?.addEventListener("resize",this._queuePosition),e?.visualViewport?.addEventListener("scroll",this._queuePosition),this._scrollDialog=this._dialog(),this._scrollDialog?.addEventListener("scroll",this._dialogScroll,!0)}_unsubscribeOpenListeners(){this.ownerDocument.removeEventListener("pointerdown",this._outsidePointerDown,!0),this.ownerDocument.removeEventListener("keydown",this._keyDown,!0);const e=this._window();e?.removeEventListener("resize",this._queuePosition),e?.removeEventListener("orientationchange",this._queuePosition),e?.visualViewport?.removeEventListener("resize",this._queuePosition),e?.visualViewport?.removeEventListener("scroll",this._queuePosition),this._scrollDialog?.removeEventListener("scroll",this._dialogScroll,!0),this._scrollDialog=null}async _openHelp(){!this._open&&this._hasContent()&&(this._clearTimers(),this._forceFallback=!1,this._open=!0,this._subscribeOpenListeners(),await this.updateComplete,this._open&&(this._usesPopover()||this._renderFallback(),this._position()?this._overlayDispose=this._dialog()?.registerOverlay({owner:this,group:"transient",close:e=>this._closeHelp("escape"===e&&this._floating.ownsActiveElement(),e)})||null:this._closeHelp()))}_closeHelp(e=!1,t="exclusive"){if(!this._open&&!this._floating.hasFallback)return;this._clearTimers();const i=this._overlayDispose;this._overlayDispose=null,i?.();const s=this.renderRoot.querySelector(".tooltip");if(s?.hidePopover)try{s.matches(":popover-open")&&s.hidePopover()}catch{}this._open=!1,this._hoverOwned=!1,this._unsubscribeOpenListeners(),this._floating.destroy(),e&&this.updateComplete.then(()=>this.renderRoot.querySelector(".trigger")?.focus())}_tooltipTemplate(e){return q``}_renderFallback(){const e=Ve.styles.cssText;this._floating.renderFallback(this._tooltipTemplate(!1),e)}_surface(){return this._floating.surface(".tooltip",this._usesPopover())}_position(){if(!this._open)return!1;const e=this._window(),t=this.renderRoot.querySelector(".trigger");let i=this._surface();if(!e||!t?.isConnected||!i?.isConnected)return!1;const s=Be(e);if(i.style.maxWidth=`${Math.max(0,Math.min(320,s.width-16))}px`,i.style.maxHeight=`${Math.max(0,s.height-16)}px`,i.style.visibility="hidden",this._usesPopover()&&i.showPopover)try{i.matches(":popover-open")||i.showPopover()}catch{if(this._forceFallback=!0,this._renderFallback(),i=this._surface(),!i?.isConnected)return!1;i.style.visibility="hidden"}const n=t.getBoundingClientRect(),o=i.getBoundingClientRect();if(!(n.width&&n.height&&o.width&&o.height))return!1;const a=je(n,o,s);return i.style.left=`${a.left}px`,i.style.top=`${a.top}px`,i.dataset.side=a.side,i.style.visibility="",!0}render(){if(!this._hasContent())return G;const e=this.ariaLabel.trim();return q` - ${this.text} - - ${this._usesPopover()?this._tooltipTemplate(!0):G} - `}}Ve.properties={text:{type:String},ariaLabel:{type:String,attribute:"aria-label"},pointerHover:{type:Boolean,attribute:"data-pointer-hover",reflect:!0},_open:{state:!0},_forceFallback:{state:!0}},Ve.styles=o` - :host { - display: none; - flex: none; - vertical-align: middle; - } - - :host([data-has-content]) { - display: inline-flex; - } - - .trigger { - width: 32px; - height: 32px; - display: inline-flex; - align-items: center; - justify-content: center; - flex: none; - box-sizing: border-box; - padding: 0; - color: var(--secondary-text-color, #9aa4ad); - background: transparent; - border: 0; - border-radius: 50%; - cursor: help; - -webkit-tap-highlight-color: transparent; - } - - .trigger svg { - display: block; - width: 18px; - height: 18px; - fill: currentColor; - } - - :host([data-pointer-hover]) .trigger:hover, - .trigger:focus-visible, - .trigger[aria-expanded='true'] { - color: var(--primary-text-color, #fff); - background: rgb(127 127 127 / 0.18); - outline: none; - } - - .trigger:focus-visible { - box-shadow: 0 0 0 2px var(--primary-color, #03a9f4); - } - - .tooltip { - position: fixed; - z-index: 2147483647; - inset: auto; - top: 0; - left: 0; - width: max-content; - max-width: min(320px, calc(100vw - 16px)); - max-height: calc(100vh - 16px); - margin: 0; - box-sizing: border-box; - padding: 9px 11px; - overflow: auto; - color: var(--primary-text-color, #fff); - background: var(--card-background-color, #202126); - border: 1px solid var(--divider-color, rgb(255 255 255 / 0.2)); - border-radius: 9px; - box-shadow: 0 8px 24px rgb(0 0 0 / 0.32); - font: 400 13px/1.4 system-ui, sans-serif; - text-align: start; - white-space: normal; - overflow-wrap: anywhere; - opacity: 1; - transform: translateY(0); - transition: opacity 120ms ease, transform 120ms ease; - } - - .tooltip[data-side='top'] { - transform-origin: bottom center; - } - - .tooltip[data-side='bottom'] { - transform-origin: top center; - } - - .tooltip[popover]:not(:popover-open) { - display: none; - } - - @starting-style { - .tooltip { - opacity: 0; - transform: translateY(3px); - } - } - - .sr-only { - /* Keep the accessibility-only description outside every dialog scroll - container's overflow geometry. Toggling the hidden attribute on the former - absolutely-positioned node made some browsers add a vertical - scrollbar while help was open, even though the visible tooltip was - already in the Popover top layer / fixed fallback portal. */ - position: fixed; - inset: 0 auto auto 0; - width: 1px; - height: 1px; - padding: 0; - margin: 0; - overflow: hidden; - clip: rect(0, 0, 0, 0); - clip-path: inset(50%); - white-space: nowrap; - border: 0; - } - - @media (pointer: coarse) { - .trigger { - width: 40px; - height: 40px; - } - } - - @media (prefers-reduced-motion: reduce) { - .tooltip { - transition: none; - } - } - `,customElements.get("hp-help")||customElements.define("hp-help",Ve);const Ke=[o` - :host { - --hp-bg: var(--card-background-color, #16212e); - --hp-line: var(--divider-color, #2b3d4f); - --hp-txt: var(--primary-text-color, #e6edf3); - --hp-muted: var(--secondary-text-color, #8aa0b3); - --hp-accent: var(--primary-color, #3ea6ff); - --hp-on: #ffd45c; - --hp-open: #ff9f43; - /* design tokens (UI chrome only). The icon/plan scale math stays on - --icon-size/--dev-size cqw units and never uses these. */ - /* spacing scale, fact-based; stray 3/5/7/9/13/14px values are unified - onto the nearest step (max +-2px, the whole point of the pass) */ - --sp-1: 2px; - --sp-2: 4px; - --sp-3: 6px; - --sp-4: 8px; - --sp-5: 12px; - --sp-6: 16px; - /* px radii of dialogs/buttons/plates (the 22% badge radius is scale math) */ - --rad-s: 6px; - --rad-m: 8px; - --rad-l: 12px; - /* font tiers: fine print+labels / body+buttons / title */ - --fs-s: 12px; - --fs-m: 13px; - --fs-l: 15px; - /* elevation: badge / floating panel (menu, tip, toast) / dialog */ - --shadow-1: 0 1px 3px rgba(0, 0, 0, 0.45); - --shadow-2: 0 6px 22px rgba(0, 0, 0, 0.45); - --shadow-3: 0 8px 30px rgba(0, 0, 0, 0.5); - } - ha-card { - overflow: visible; /* overflow:hidden breaks position:sticky on the header */ - } - .empty { - padding: 40px 24px; - color: var(--hp-txt); - text-align: center; - display: flex; - flex-direction: column; - align-items: center; - gap: var(--sp-4); - } - .empty .big { - --mdc-icon-size: 56px; - color: var(--hp-accent); - opacity: 0.7; - } - .empty .muted { - color: var(--hp-muted); - font-size: var(--fs-m); - margin: 0; - } - .empty .btn { - margin-top: var(--sp-4); - } - @keyframes fixedfloor-spin { - to { transform: rotate(360deg); } - } - .spacer { - flex: 1; - } - .bootveil { - position: absolute; - inset: 0; - z-index: 40; - display: flex; - align-items: center; - justify-content: center; - background: var(--hp-bg, #16212e); - opacity: 1; - transition: opacity 0.15s ease; - pointer-events: none; - } - .bootveil.off { - opacity: 0; - } - .bootveil .boothouse { - position: static; /* the main .zoomwrap SVG is pinned to inset:0 — not this icon */ - width: 56px; - height: 56px; - fill: var(--hp-accent); - opacity: 0.85; - animation: hp-boot-pulse 1.3s ease-in-out infinite; - } - @keyframes hp-boot-pulse { - 0%, 100% { opacity: 0.3; transform: scale(0.94); } - 50% { opacity: 0.9; transform: scale(1); } - } - @media (prefers-reduced-motion: reduce) { - .bootveil .boothouse { - animation: none; - opacity: 0.7; - } - } - @keyframes hp-pulse-short { - 0% { transform: scale(1); opacity: 0.55; } - 70% { opacity: 0.22; } - 100% { transform: scale(var(--ripple-scale, 1.5)); opacity: 0; } - } - /* Alternate identity: a rapid retrigger restarts the browser timeline. */ - @keyframes hp-pulse-short-b { - 0% { transform: scale(1); opacity: 0.55; } - 70% { opacity: 0.22; } - 100% { transform: scale(var(--ripple-scale, 1.5)); opacity: 0; } - } - @keyframes hp-pulse-continuous { - 0% { transform: scale(1); opacity: 0.55; } - 65% { opacity: 0.18; } - 100% { transform: scale(var(--ripple-scale, 1.5)); opacity: 0; } - } - @keyframes hp-pulse-alarm { - 0% { transform: scale(1); opacity: 0.72; } - 100% { transform: scale(1.5); opacity: 0; } - } - /* Space changes keep their direction, but travel only a few percent: the - motion explains what changed without making the whole plan fly around. */ - @keyframes hp-slide-left { - 0% { transform: translateX(4%); opacity: 0.72; } - 100% { transform: translateX(0); opacity: 1; } - } - @keyframes hp-slide-right { - 0% { transform: translateX(-4%); opacity: 0.72; } - 100% { transform: translateX(0); opacity: 1; } - } - @keyframes hp-sunfade-in { - from { opacity: 0; } - to { opacity: 1; } - } - @keyframes hp-sunfade-out { - from { opacity: 1; } - to { opacity: 0; } - } - .sr-only { - position: absolute !important; - width: 1px !important; - height: 1px !important; - padding: 0 !important; - margin: -1px !important; - overflow: hidden !important; - clip: rect(0, 0, 0, 0) !important; - white-space: nowrap !important; - border: 0 !important; - } - .toast { - position: fixed; - pointer-events: none; - left: 50%; - bottom: 22px; - transform: translateX(-50%); - background: var(--hp-bg); - border: 1px solid var(--hp-accent); - color: var(--hp-txt); - padding: var(--sp-4) var(--sp-6); - border-radius: var(--rad-l); - font-size: var(--fs-m); - box-shadow: var(--shadow-2); - z-index: 120; - max-width: 90vw; - } -`,o` - .fixedfloor-loading { - animation: fixedfloor-spin 1.1s linear infinite; - } - .fixedfloor-error p { - max-width: 42rem; - margin: 0; - overflow-wrap: anywhere; - } - @media (prefers-reduced-motion: reduce) { - .fixedfloor-loading { animation: none; } - } - .stage.noplan { - background: #ffffff; - } - /* Editors always keep the white drawing sheet — even when a backdrop - image is loaded. Without this, a picture plan painted the theme card - colour under the grid (owner 2026-08-05). View keeps the historical - theme / .noplan split. */ - .stage.mode-plan, - .stage.mode-devices, - .stage.mode-decor { - background: #ffffff; - } - .stage { - position: relative; - width: 100%; - container-type: inline-size; - overflow: hidden; - touch-action: none; /* custom pinch/pan gestures */ - background: var(--ha-card-background, var(--card-background-color, #111)); - } - .zoomwrap { - position: absolute; - inset: 0; - z-index: 1; - } - .stage.mode-transition, - .stage.mode-transition .zoomwrap { - transition: none !important; - } - .stage.mode-transition .hp-view-only-layer, - .stage.mode-transition .hp-editor-only-layer { - pointer-events: none; - will-change: opacity; - } - .stage.mode-transition .hp-paper { - fill: var(--hp-mode-paper) !important; - } - /* Four-phase environment (#146): constant layers cross-fade because CSS - gradients themselves are not reliably interpolated. Everything stays - behind the plan; the plan tree is never dimmed, tinted or faded. */ - .hp-day-cycle-env { - position: absolute; - inset: 0; - z-index: 0; - overflow: hidden; - pointer-events: none; - isolation: isolate; - } - .hp-day-cycle-bg { - position: absolute; - inset: 0; - opacity: 0; - overflow: hidden; - transition: opacity 1100ms cubic-bezier(.22, .61, .36, 1); - } - .hp-day-cycle-bg.active { opacity: 1; } - .hp-day-cycle-sun { - position: absolute; - left: var(--hp-day-cycle-sun-x); - top: var(--hp-day-cycle-sun-y); - width: clamp(140px, 25cqw, 250px); - height: clamp(140px, 25cqw, 250px); - transform: translate(-50%, -50%); - border-radius: 50%; - filter: blur(10px); - opacity: var(--hp-day-cycle-sun-opacity); - transition: - left 1100ms cubic-bezier(.22, .61, .36, 1), - top 1100ms cubic-bezier(.22, .61, .36, 1), - opacity 1100ms cubic-bezier(.22, .61, .36, 1); - } - .stage.mode-transition .hp-day-cycle-env { transition: none; } - .stage.daycycle .hp-paperg, - .hp-static-stage.daycycle .hp-paperg { - filter: - drop-shadow(0 0 1px var(--hp-day-cycle-outline-near)) - drop-shadow(0 0 5px var(--hp-day-cycle-outline-mid)) - drop-shadow(0 0 10px var(--hp-day-cycle-outline-far)); - transition: filter 1100ms cubic-bezier(.22, .61, .36, 1); - } - @media (prefers-reduced-motion: reduce) { - .hp-day-cycle-bg, - .hp-day-cycle-sun, - .stage.daycycle .hp-paperg, - .hp-static-stage.daycycle .hp-paperg { - transition: none; - } - } - .sunlayer { - pointer-events: none; - } - .compass { - width: 120px; - height: 120px; - flex: none; - touch-action: none; - cursor: grab; - user-select: none; - } - .compass:active { - cursor: grabbing; - } - .compass .cring { - fill: rgba(255, 255, 255, 0.04); - stroke: var(--divider-color, #444); - stroke-width: 2; - } - .compass .ctick { - stroke: var(--secondary-text-color, #9aa4ad); - stroke-width: 2; - } - .compass .ctick.minor { - stroke-width: 1; - opacity: 0.6; - } - .compass .cneedle line { - stroke: var(--primary-color, #3ea6ff); - stroke-width: 2.5; - stroke-linecap: round; - } - .compass .cneedle path { - fill: var(--primary-color, #3ea6ff); - } - .compass .cneedle text { - fill: var(--text-primary-color, #fff); - font-size: 11px; - font-weight: 700; - } - .compass .cdeg { - fill: var(--secondary-text-color, #9aa4ad); - font-size: 13px; - } - .compass.unset .cneedle { - opacity: 0.35; - } - /* HP-1552: first-open boot veil — the plan hides until the stage height settles */ - .stage.hpboot .zoomwrap, - .stage.hpboot .zoombadge { - visibility: hidden; - } - /* AUD-1552-02: post-veil grace — HA chrome landing after the cap moves - the stage height smoothly; the viewport ResizeObserver refits the plan - along the transition, so a late panel glides instead of jumping. */ - .stage.hpsettle { - transition: height 0.25s ease; - } - @media (prefers-reduced-motion: reduce) { - .stage.hpsettle { - transition: none; - } - } - .zoomctl { - display: inline-flex; - gap: var(--sp-1); - background: rgba(127, 127, 127, 0.12); - border-radius: var(--rad-m); - padding: var(--sp-1); - } - .zoomctl .zb { - border: none; - padding: var(--sp-3) var(--sp-4); - } - .zoomctl .zb[disabled] { - opacity: 0.4; - pointer-events: none; - } - .projection-toggle { - min-width: 44px; - min-height: 44px; - justify-content: center; - padding: var(--sp-3); - } - /* docs/CANVAS.md §5: the plane has no edges, so you can pan until nothing - is on screen. One pointer home, one click back. */ - .homearrow { - position: absolute; - z-index: 12; - transform: translate(-50%, -50%); - width: 40px; - height: 40px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - background: var(--card-background-color, var(--hp-bg)); - color: var(--hp-accent); - border: 1px solid var(--hp-accent); - opacity: 0.9; - padding: 0; - } - .homearrow ha-icon { - --mdc-icon-size: 22px; - line-height: 1; - } - .zoombadge { - position: absolute; - z-index: 12; - left: var(--sp-4); - bottom: var(--sp-4); - background: var(--card-background-color, var(--hp-bg)); - opacity: 0.92; - color: var(--hp-txt); - border: 1px solid var(--hp-accent); - border-radius: var(--rad-m); - padding: var(--sp-1) var(--sp-4); - font-size: var(--fs-s); - font-weight: 600; - pointer-events: none; - } - /* Only the plan canvas owns the whole zoom wrapper. Context trays now live - inside .stage as well, and may contain small SVG previews (furniture in - particular); the old descendant-wide stage-SVG selector stretched every - preview over the tray and stacked them into one blocking artefact. The - vacuum trail/fit overlays have their own explicit geometry below. */ - .zoomwrap > svg { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - display: block; - } - .plan-svg { z-index: 1; } - .iso-underlay-svg { z-index: 0; overflow: visible; } - .iso-shadows-svg { z-index: 3; overflow: visible; } - .iso-walls-svg { - z-index: 4; - overflow: visible; - } - .iso-underlay-svg, - .iso-shadows-svg, - .iso-walls-svg, - .iso-underlay, - .iso-shadows, - .iso-walls, - .iso-openings { - pointer-events: none; - } - .iso-side-hi { stop-color: #b9bdbe; } - .iso-side-lo { stop-color: #969c9f; } - .iso-top-hi { stop-color: #fafaf7; } - .iso-top-lo { stop-color: #e2e4e2; } - .iso-wall-side { - fill: url(#hp-iso-wall-side) #a8acae; - stroke: #92989b; - stroke-width: 0.7; - vector-effect: non-scaling-stroke; - } - .iso-wall-top { - fill: url(#hp-iso-wall-top) #f3f3f1; - stroke: #d7d9d8; - stroke-width: 0.8; - vector-effect: non-scaling-stroke; - } - .iso-floor-side { - fill: #858b8d; - stroke: #71787b; - stroke-width: 0.7; - vector-effect: non-scaling-stroke; - } - .iso-opening-panel { - fill: #d7d9d7; - fill-opacity: 0.96; - stroke: #7f878b; - stroke-width: 0.9; - vector-effect: non-scaling-stroke; - } - .iso-opening-panel.iso-window { - fill: #dfeff4; - fill-opacity: 0.72; - stroke: #8aa7b1; - } - .iso-ambient-shadow { - fill: rgba(15, 21, 25, 0.22); - filter: url(#hp-iso-ambient-shadow); - } - .iso-contact-shadow { - fill: none; - stroke: rgba(22, 28, 31, 0.25); - stroke-width: 3; - filter: url(#hp-iso-contact-shadow); - vector-effect: non-scaling-stroke; - } - .iso-leaf-shadow { - fill: none; - stroke: rgba(18, 23, 27, 0.24); - stroke-width: 4; - filter: url(#hp-iso-leaf-shadow); - vector-effect: non-scaling-stroke; - } - @media (prefers-color-scheme: dark) { - .iso-side-hi { stop-color: #4c555a; } - .iso-side-lo { stop-color: #343c40; } - .iso-top-hi { stop-color: #687176; } - .iso-top-lo { stop-color: #50585d; } - .iso-wall-side { stroke: #30373b; } - .iso-wall-top { stroke: #7b858a; } - .iso-floor-side { fill: #2d3438; stroke: #20272a; } - .iso-opening-panel { fill: #626b70; stroke: #899399; } - .iso-opening-panel.iso-window { fill: #75919b; stroke: #abc6ce; } - .iso-ambient-shadow { fill: rgba(0, 0, 0, 0.34); } - .iso-contact-shadow, .iso-leaf-shadow { stroke: rgba(0, 0, 0, 0.38); } - } - @media (forced-colors: active) { - .iso-wall-side, .iso-wall-top, .iso-floor-side, .iso-opening-panel { - fill: Canvas; - stroke: CanvasText; - forced-color-adjust: auto; - } - .iso-ambient-shadow, .iso-contact-shadow, .iso-leaf-shadow { display: none; } - } - @supports not (filter: blur(1px)) { - .iso-ambient-shadow, .iso-contact-shadow, .iso-leaf-shadow { display: none; } - } - /* Opaque plan paper (owner 2026-08-03): the scene bg_color / daynight sky - shows ONLY around the plan, never through it. The colour is the - pre-bg_color canvas — the theme card background under an image plan, - plain white for a hand-drawn one (.stage.noplan). On drawn plans the - paper is per-room shapes following the room contours — fill only, - never a stroke, so the paper cannot poke past a wall. Night dimming - comes from the .zoomwrap brightness filter, never from alpha - (docs/SUN.md). */ - .hp-paper { - fill: var(--ha-card-background, var(--card-background-color, #111)); - stroke: none; - } - .stage.noplan .hp-paper, - .stage.mode-plan .hp-paper, - .stage.mode-devices .hp-paper, - .stage.mode-decor .hp-paper { - fill: #ffffff; - } - /* Owner 2026-08-04: «углы границ комнат всё ещё с зубцами». A miter join - on a 30-45° corner shoots a spike far past the wall (and flips to an - ugly bevel once past the miter limit) — the same defect the decor lines - had before they got round caps. Every room border, in EVERY renderer - that reuses these styles (plan view, plan editor, static space-card), - joins its walls with a ROUND join instead: the corner reads as the - stroke's own radius, never as a tooth. The linecap matters only for the - open outlines below, but it costs nothing to state it here. */ - .room { - transition: 0.12s; - cursor: default; /* v1.40.1: rooms are not clickable — the label's link icon is */ - stroke-linejoin: round; - stroke-linecap: round; - } - .room.overlay { - fill: transparent; - stroke: transparent; - stroke-width: calc(2px * var(--hp-cell-visual-scale, 1)); - } - :host([data-pointer-hover]) .stage.mode-view .room.overlay:not(.styled):hover { - stroke: var(--hp-accent); - stroke-opacity: 1; - } - .room.yard { - fill: rgba(75, 140, 90, 0.14); - stroke: #4b8c5a; - stroke-width: calc(2px * var(--hp-cell-visual-scale, 1)); - } - :host([data-pointer-hover]) .stage.mode-view .room.yard:not(.styled):hover { - stroke: var(--hp-accent); - stroke-opacity: 1; - } - .room.styled { - stroke: var(--room-stroke, transparent); - stroke-opacity: var(--room-stroke-op, 0); - stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); - fill: var(--room-fill, transparent); - fill-opacity: var(--room-fill-op, 0); - } - .glow-base-layer, - .glow-base-tunnels, - .glow-pools-frame, - .glow-pools, - .glow-spot { - pointer-events: none; - } - /* The parent isolates all source spots from the room data fill, Glow base, - paper and backdrop. A spot is one circle clipped to the floor its lamp - can see, so the whole spot screen-blends as a single primitive — there is - no mask left to be dropped on a promoted layer. Per-stop alpha already - contains the shared 0.7 ceiling; the spot opacity below only animates - between 0 and 1 and is never another persistent alpha ceiling. */ - .glow-pools-frame, - .glow-pools, - .glow-spot { - isolation: isolate; - } - .glow-pools.blend-screen .glow-spot { - mix-blend-mode: screen; - } - .glow-spot { - opacity: 1; - transition: opacity 500ms ease; - } - .glow-spot.is-entering, - .glow-spot.is-leaving { - opacity: 0; - } - @media (prefers-reduced-motion: reduce) { - .glow-spot { - transition: none; - } - } - /* The explicit late room-hover layer owns the wash and halo. Keeping CSS - filters off room paths prevents Chromium from recompositing the sibling - screen-blended Glow layer for one bright frame on every hover. */ - :host([data-pointer-hover]) .stage.mode-view .room.styled:hover { - stroke: var(--hp-accent); - stroke-opacity: 1; - } - /* doors, windows & gates */ - .op-leaf { - transition: transform 0.6s ease; - } - .op-arc { - stroke-width: calc(1.5px * var(--hp-cell-visual-scale, 1)); - transition: stroke-dashoffset 0.6s ease; - } - /* hover affordance: a rounded outline hugging the wall strip + a grab cursor */ - .op-outline { - fill: none; - stroke: var(--hp-accent); - stroke-width: calc(1.5px * var(--hp-cell-visual-scale, 1)); - opacity: 0; - transition: opacity 0.15s; - pointer-events: none; - } - :host([data-pointer-hover]) .stage.markup g.opening:hover .op-outline { - opacity: 0.9; - } - /* openings are pure status graphics outside Plan mode: no cursor, no hover, - no hit target — View must not interact with them at all */ - .op-hit { - fill: transparent; - pointer-events: none; - cursor: default; - } - .stage.markup .op-hit { - pointer-events: auto; - cursor: grab; - touch-action: none; /* drags, not scrolls, on touch */ - } - .stage.markup .op-hit:active { - cursor: grabbing; - } - .stage.markup .opening.orphan { - pointer-events: auto; - cursor: pointer; - color: var(--error-color, #db4437); - } - .stage.markup .opening.orphan circle { - fill: var(--hp-bg); - stroke: currentColor; - stroke-width: calc(2px * var(--hp-cell-visual-scale, 1)); - } - .stage.markup .opening.orphan text { - fill: currentColor; - font-weight: 800; - font-size: calc(12px * var(--hp-cell-visual-scale, 1)); - pointer-events: none; - } - /* HP-1550-04: in the resize tool the wall handles own the hit test — the - transparent .op-hit of a door at the midpoint of a wall used to sit ON - TOP of the handle and made that wall ungrabbable for both rooms. - Openings are not editable in this tool (they ride along with the wall), - so their hit area goes fully inert; every other Plan tool is untouched. */ - .stage.markup.tool-resize .op-hit { - pointer-events: none; - cursor: default; - } - .stage.mode-view .oplock { - pointer-events: auto; - cursor: pointer; - } - @media (prefers-reduced-motion: reduce) { - .op-leaf, .op-arc { transition: none; } - } - .roomlabel { - pointer-events: none; /* draggable only in plan mode (rule below) */ - position: absolute; - transform: translate(-50%, -50%); - font-size: calc(var(--rl-icon-size, var(--icon-size, 2.5cqw)) * 0.5 * var(--rl-scale, 1) * var(--rl-font, 1) * var(--rl-space, 1)); - font-weight: 700; - letter-spacing: 0.04em; - white-space: nowrap; - cursor: grab; - user-select: none; - z-index: 1; - display: flex; - flex-direction: column; - align-items: center; - gap: 0.15em; - text-align: center; - } - .rlname { - display: inline-flex; - align-items: center; - gap: 0.25em; - font-size: calc(1em * var(--rl-name, 1)); - } - .zoomwrap.slide-left { animation: hp-slide-left 0.18s cubic-bezier(0.2, 0.7, 0.2, 1); } - .zoomwrap.slide-right { animation: hp-slide-right 0.18s cubic-bezier(0.2, 0.7, 0.2, 1); } - @media (prefers-reduced-motion: reduce) { - .zoomwrap.slide-left, - .zoomwrap.slide-right { animation: none; } - } - /* The name is the anchor: the label box is centred on the room point, so - anything that takes part in its layout SHIFTS THE NAME. The gear button - and the metrics hang below as absolutes — the name renders in exactly - the same place in view mode and in the plan editor (owner's request), - and the button sits at the very bottom of the card. */ - /* Standalone, centred on the room, sized from the device icon: icon-size - already rescales with the view, so the button zooms WITH the plan - instead of keeping a constant screen size (owner's spec). */ - .rlgearbtn { - --gear-h: calc(var(--icon-size, 2.5cqw) * 0.77); /* owner: half the previous size */ - position: absolute; - /* dead-centred on the room, both axes (owner's spec) */ - transform: translate(-50%, -50%); - display: inline-flex; - align-items: center; - gap: 0.35em; - height: var(--gear-h); - padding: 0 calc(var(--gear-h) * 0.38); - border: 0; - border-radius: 999px; - background: var(--hp-accent); - color: var(--text-primary-color, #fff); - font: inherit; - font-size: calc(var(--gear-h) * 0.42); - font-weight: 600; - line-height: 1; - white-space: nowrap; - cursor: pointer; - pointer-events: auto; - opacity: 0.92; - box-shadow: var(--shadow-1); - z-index: 2; - } - .rlgearbtn { transition: opacity 0.15s, filter 0.15s; } - :host([data-pointer-hover]) .rlgearbtn:hover { opacity: 1; filter: brightness(1.18); } - .rlgearbtn ha-icon { --mdc-icon-size: calc(var(--gear-h) * 0.55); display: inline-flex; } - .rlgear { - --mdc-icon-size: 0.9em; - display: inline-flex; - margin-right: 0.2em; - opacity: 0.6; - cursor: pointer; - pointer-events: auto; - } - :host([data-pointer-hover]) .rlgear:hover { opacity: 1; } - .rlgo { - --mdc-icon-size: 0.85em; - display: inline-flex; - opacity: 0.55; - } - .stage.mode-view .rlgo { - pointer-events: auto; - cursor: pointer; - } - :host([data-pointer-hover]) .stage.mode-view .rlgo:hover { opacity: 1; } - .roomlabel .rlmetrics { - position: absolute; /* below the name, outside the centring math */ - top: calc(100% + 0.15em); - left: 50%; - transform: translateX(-50%); - white-space: nowrap; - display: flex; - align-items: center; - gap: 0.55em; - font-size: calc(0.75em * var(--rl-meta, 1)); /* feedback: 0.62 was unreadable on a tablet */ - font-weight: 600; - letter-spacing: 0.02em; - opacity: 0.9; - } - .roomlabel .rlm { - display: inline-flex; - align-items: center; - gap: 0.12em; - } - .roomlabel .rlm ha-icon { - --mdc-icon-size: 1.05em; - display: inline-flex; - } - .roomlabel .rlm.lit { opacity: 1; } - .rlhandle { - display: none; - position: absolute; - width: 9px; - height: 9px; - border-radius: 2px; - background: var(--hp-accent); - border: 1px solid var(--card-background-color, #fff); - z-index: 2; - } - .rlhandle.tl { left: -6px; top: -6px; cursor: nwse-resize; } - .rlhandle.br { right: -6px; bottom: -6px; cursor: nwse-resize; } - .rlhandle.tr { right: -6px; top: -6px; cursor: nesw-resize; } - .rlhandle.bl { left: -6px; bottom: -6px; cursor: nesw-resize; } - :host([data-pointer-hover]) .stage.markup .roomlabel:hover .rlhandle { display: block; } - .stage.markup .roomlabel { pointer-events: auto; } - .roomlabel:active { cursor: grabbing; } - .measurelayer { - position: absolute; - inset: 0; - pointer-events: none; - } - .alignline { - stroke: var(--hp-accent); - stroke-width: calc(1.2px * var(--hp-cell-visual-scale, 1)); - stroke-dasharray: - calc(4px * var(--hp-cell-visual-scale, 1)) - calc(4px * var(--hp-cell-visual-scale, 1)); - pointer-events: none; - opacity: 0.9; - } - .aligndot { - fill: var(--hp-accent); - pointer-events: none; - } - .measurelabel.on45 { - color: #4bd28f; - border-color: #4bd28f; - } - .measurelabel { - position: absolute; - transform: translate(12px, -150%); - font-size: var(--fs-s); - font-weight: 600; - padding: 1px var(--sp-3); - border-radius: var(--rad-s); - background: rgba(0, 0, 0, 0.72); - color: #fff; - white-space: nowrap; - pointer-events: none; - user-select: none; - z-index: 3; - } - .measurelabel.opdimension { - transform: translate( - calc(-50% + var(--op-label-shift-x, 0px)), - calc(-50% + var(--op-label-shift-y, -12px)) - ); - border: 1px solid var(--hp-open, #ff9800); - } - /* decor (background) layer */ - .decorlayer .dshape { pointer-events: none; } - .stage.mode-decor .decorlayer .dshape { - pointer-events: visiblePainted; - cursor: pointer; - } - .stage.mode-decor.dtool-select .decorlayer .dshape { cursor: move; } - /* …but a DRAWING tool owns the canvas: existing shapes stop being targets - entirely, so a new line can start exactly on the end of an old one - instead of grabbing it (owner, 2026-08-04). Same for the picture tool — - a shape lying over the plan must not block the picture's own drag. - Only select (move) and erase (delete) keep the shapes clickable. */ - .stage.mode-decor.dtool-line .decorlayer .dshape, - .stage.mode-decor.dtool-rect .decorlayer .dshape, - .stage.mode-decor.dtool-ellipse .decorlayer .dshape, - .stage.mode-decor.dtool-text .decorlayer .dshape, - .stage.mode-decor.dtool-furniture .decorlayer .dshape, - .stage.mode-decor.dtool-backdrop .decorlayer .dshape { pointer-events: none; } - /* the furniture tool is a stamp: the press must reach the stage even when - it lands on a sofa that is already there (docs/FURNITURE.md §4) */ - .stage.mode-decor.dtool-furniture { cursor: copy; } - /* ONE exception (owner, 2026-08-04): under the TEXT tool an existing LABEL - is a target again — pressing it opens its editor instead of starting a - new label on top of the old one. Only labels: a line or a rectangle - under the text tool stays inert, so the press reaches the stage and a - new label is created there. */ - .stage.mode-decor.dtool-text .decorlayer .dshape.dtext, - .stage.mode-decor.dtool-select .decorlayer .dshape.dtext, - .stage.mode-decor.dtool-erase .decorlayer .dshape.dtext { - /* A label is one logical decor object. SVG's visiblePainted would hit - only the ink of individual glyphs, so spaces, counters and the area - inside its selection glow behaved like empty canvas. */ - pointer-events: visiblePainted; - pointer-events: bounding-box; - } - .stage.mode-decor.dtool-text .decorlayer .dshape.dtext { - cursor: text; - } - /* Erasing a hairline must not require pixel-perfect aim. The duplicate - geometry is invisible and exists only while Erase is active. A - non-scaling stroke keeps the target comfortably wide at every zoom. */ - .decorlayer .derasehit { - fill: none; - stroke: transparent; - stroke-width: 16px; - stroke-linecap: round; - stroke-linejoin: round; - vector-effect: non-scaling-stroke; - pointer-events: none; - } - .stage.mode-decor.dtool-erase .decorlayer .derasehit { - pointer-events: stroke; - } - /* A dashed line must remain selectable across its gaps. This proxy also - makes the only entry to line-style properties — double click in Select — - practical for hairlines at every zoom. */ - .decorlayer .dselecthit { - fill: none; - stroke: transparent; - stroke-width: 16px; - stroke-linecap: round; - vector-effect: non-scaling-stroke; - pointer-events: none; - } - .stage.mode-decor.dtool-select .decorlayer .dselecthit { - pointer-events: stroke; - cursor: move; - } - .decorlayer .dsel { - filter: drop-shadow(0 0 3px var(--hp-accent)); - } - .decorlayer .ddraft { - stroke-dasharray: - calc(6px * var(--hp-cell-visual-scale, 1)) - calc(5px * var(--hp-cell-visual-scale, 1)); - pointer-events: none; - } - .decorlayer text { - font-weight: 600; - user-select: none; - dominant-baseline: middle; - text-anchor: middle; - } - .stage.mode-decor { - outline: 2px solid #26a69a; - outline-offset: -2px; - } - /* backdrop transform frame (docs/BACKDROP.md §2). Editor chrome: the - outline never takes a pointer, the four corner handles do — and they are - finger-sized (r = 2 % of the visible view), because this is dragged on a - tablet as often as with a mouse. */ - .bdframe .bdbox { - fill: none; - stroke: var(--hp-accent); - stroke-width: 2; - stroke-dasharray: 10 7; - vector-effect: non-scaling-stroke; - pointer-events: none; - opacity: 0.9; - } - /* the HIT circle: invisible, finger-sized, the only thing that takes a - pointer. The visible bead is .bdknob, a quarter of its radius — the same - visual/hit split .dthandle + .dtknob and .rszhandle + .rszicon use. */ - .bdframe .bdhandle { - fill: transparent; - stroke: none; - pointer-events: all; - touch-action: none; - } - .bdframe .bdknob { - fill: var(--hp-accent); - stroke: #fff; - stroke-width: 1; - vector-effect: non-scaling-stroke; - pointer-events: none; - } - :host([data-pointer-hover]) .bdframe .bdhandle:hover + .bdknob { - fill: #fff; - stroke: var(--hp-accent); - } - .bdframe .bd-nwse { cursor: nwse-resize; } - .bdframe .bd-nesw { cursor: nesw-resize; } - /* the picture itself is the drag target for a move (grab, then grabbing) */ - .stage.mode-decor.bdgrab { cursor: grab; } - .stage.mode-decor.bdgrabbing, - .stage.mode-decor.bdgrabbing .bdframe .bdhandle { cursor: grabbing; } - .measurelabel.bdmeasure { - transform: translate(-50%, -50%); - border: 1px solid var(--hp-accent); - } - /* the selected decor object's frame — same chrome rules as the backdrop's: - the outline never takes a pointer, the handles always do, and they are - finger-sized because this is dragged on a tablet too */ - .dtframe .dtbox { - fill: none; - stroke: var(--hp-accent); - stroke-width: 1.5; - stroke-dasharray: 7 5; - vector-effect: non-scaling-stroke; - pointer-events: none; - opacity: 0.85; - } - .dtframe .dtstem, - .bdframe .dtstem { - stroke: var(--hp-accent); - stroke-width: 1.5; - vector-effect: non-scaling-stroke; - pointer-events: none; - opacity: 0.85; - } - /* the HIT circle: invisible, finger-sized, the only thing that takes a - pointer. The visible bead is .dtknob, a quarter of its radius — the - same visual/hit split .rszhandle + .rszicon use. */ - .dtframe .dthandle { - fill: transparent; - stroke: none; - pointer-events: all; - touch-action: none; - } - .dtframe .dtknob { - fill: var(--hp-accent); - stroke: #fff; - stroke-width: 1; - vector-effect: non-scaling-stroke; - pointer-events: none; - } - :host([data-pointer-hover]) .dtframe .dthandle:hover + .dtknob { - fill: #fff; - stroke: var(--hp-accent); - } - .dtframe .dt-nwse { cursor: nwse-resize; } - .dtframe .dt-nesw { cursor: nesw-resize; } - .dtframe .dtrot { cursor: grab; } - .dtframe .dtendpoint { cursor: crosshair; } - .bdframe .dtrot { cursor: grab; } - .dtarea { - resize: vertical; - min-height: 3.4em; - font: inherit; - line-height: 1.35; - } - .stage.mode-decor.dtool-line, .stage.mode-decor.dtool-rect, - .stage.mode-decor.dtool-ellipse, .stage.mode-decor.dtool-text { - cursor: crosshair; - } - .stage.mode-decor.dtool-erase, - .stage.mode-decor.dtool-erase .decorlayer .dshape { - cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cg transform='rotate(-45 12 12)'%3E%3Crect x='7' y='2' width='10' height='18' rx='2' fill='%23fff' stroke='%23111' stroke-width='1.5'/%3E%3Cpath d='M7 13h10v5a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2z' fill='%23ff9f43' stroke='%23111' stroke-width='1.5'/%3E%3C/g%3E%3C/svg%3E") 5 22, pointer; - } - .stage.mode-decor .room, .stage.mode-decor .devlayer { pointer-events: none; } - .stage.mode-decor .oplock { pointer-events: none; } - /* Backdrop-editor de-emphasis is a shared mode-transition coordinate. - It multiplies whole presentation groups and never changes Glow source - alpha, additive blending, or the underlying resolved state. */ - .stage .room, - .stage .devlayer, - .stage .opening, - .stage .room-outline, - .stage .wallbodies, - .stage .opening-tunnels, - .stage .glow-base-layer, - .stage .glow-pools-frame, - .stage .zero-walls { - opacity: var(--hp-mode-architecture-opacity, 1); - } - .opening-preview { - opacity: 0.5; - pointer-events: none; - } - .opening-preview[data-kind="passage"] { - opacity: 1; - } - .passage-preview-cut { - fill: var(--wall-fill, #ffffff); - fill-opacity: 0.35; - stroke: none; - pointer-events: none; - } - .passage-preview-boundary { - stroke: var(--hp-open, #ff9800); - stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); - pointer-events: none; - } - .opening-preview .op-leaf, - .opening-preview .op-arc { - transition: none; - } - .opening-preview-dot { - fill: var(--hp-open, #ff9800); - pointer-events: none; - } - .opening-dimensions, - .opening-dimension { - pointer-events: none; - } - .opening-dimension-line, - .opening-dimension-tick { - fill: none; - stroke: var(--hp-open, #ff9800); - stroke-width: 1.5; - vector-effect: non-scaling-stroke; - pointer-events: none; - } - .opening-dimension-tick { - stroke-width: 2; - } - .stage.edit .room { - pointer-events: none; - } - .stage.markup { - cursor: crosshair; - } - /* room-picking stages: merge (both clicks) and split before a room is chosen */ - .stage.markup.tool-merge, - .stage.markup.tool-split.pickstage, - .stage.markup.tool-delroom { - cursor: pointer; - } - .stage.markup.tool-wallthick { cursor: default; } - .stage.markup.tool-wallthick.wallhot { cursor: pointer; } - /* Solid wall colour sits under the hatch (owner: both, not either/or). */ - .wallbody-fill { - fill: var(--wall-fill, #ffffff); - fill-opacity: var(--wall-fill-op, 1); - fill-rule: evenodd; - stroke: none; - pointer-events: none; - } - .wallbody { - fill: url(#hp-wall-hatch); - fill-rule: evenodd; - stroke: var(--room-stroke, var(--hp-muted)); - stroke-width: calc(0.6px * var(--hp-cell-visual-scale, 1)); - pointer-events: none; - } - /* Thin-on-screen: hatch collapses into noise — fill alone, stroke from fill path's sibling. */ - .wallbody.solid { - fill: none; - } - .wallthick-hover { - fill: var(--accent-color, #03a9f4); - fill-opacity: 0.38; - stroke: var(--accent-color, #03a9f4); - stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); - stroke-linejoin: round; - stroke-linecap: round; - pointer-events: none; - } - .wallthick-hover.isopen { - fill: var(--error-color, #f44336); - stroke: var(--error-color, #f44336); - } - .zero-wall { - stroke: var(--zero-wall-stroke, var(--hp-muted)); - stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); - stroke-dasharray: - calc(7px * var(--hp-cell-visual-scale, 1)) - calc(7px * var(--hp-cell-visual-scale, 1)); - stroke-linecap: butt; - pointer-events: none; - opacity: 0.9; - } - .zero-walls.solid .zero-wall { - stroke-dasharray: none; - } - /* Rooms with zero/thick stretches: the polygon's own stroke is fully off. - The trimmed .room-outline draws normal walls; View hover gets its own - top overlay after the wall bodies. */ - .room.noedge { - stroke-opacity: 0 !important; - } - /* rooms with open boundaries draw their walls as separate M..L subpaths, - so a corner between two of them is two stroke ENDS meeting: round caps - fill it in the same way a round join fills a closed contour's corner - (owner 2026-08-04 — no teeth anywhere on a room border). */ - .room-outline { - fill: none; - stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); - stroke-linejoin: round; - stroke-linecap: round; - pointer-events: none; - } - /* Plan editor: trimmed outlines use the markup blue */ - .room-outline.outlined { - stroke: rgba(62, 166, 255, 0.55); - stroke-opacity: 1; - } - /* The wash is rendered directly above the resolved room fill but below - tunnels, Glow, sun and walls. Black at 22% reproduces the old - brightness(.78) contract without filtering/promoting an SVG ancestor or - changing the room's hue. The halo/outline are rendered again late, above - the wall bodies. */ - .room-hover-fill { - fill: #000; - fill-opacity: 0.22; - pointer-events: none; - } - .room-hover-halo { - fill: none; - stroke: var(--hp-accent); - stroke-opacity: 0.28; - stroke-width: calc(8px * var(--hp-cell-visual-scale, 1)); - stroke-linejoin: round; - stroke-linecap: round; - pointer-events: none; - } - .room-hover-outline { - fill: none; - stroke: var(--hp-accent); - stroke-width: calc(3px * var(--hp-cell-visual-scale, 1)); - stroke-linejoin: round; - stroke-linecap: round; - pointer-events: none; - } - .stage.markup .room { - pointer-events: none; - } - .stage.markup .devlayer .dev { - display: none; /* in plan mode the icons do not get in the way; labels stay */ - } - /* mode frames: the edit modes are visible at a glance */ - .stage.mode-plan { - outline: 2px solid #ffc14d; - outline-offset: -2px; - } - .stage.mode-devices { - outline: 2px solid var(--hp-accent); - outline-offset: -2px; - } - .room.outlined { - stroke: rgba(62, 166, 255, 0.55); - fill: rgba(62, 166, 255, 0.06); - } - /* AFTER .outlined: same specificity — source order decides (gotcha x4) */ - .room.picked { - stroke: #ffc14d; - stroke-width: calc(3px * var(--hp-cell-visual-scale, 1)); - fill: rgba(255, 193, 77, 0.25); - } - /* Owner 2026-08-04: the grid is a HINT, not content — at full strength the - dots argued with the plan on white paper. Both levels are muted, the - hierarchy is kept (major still denser than fine). */ - .griddot { - fill: var(--hp-accent); - opacity: 0.35; - stroke: rgba(0, 0, 0, 0.35); - stroke-width: 0.4; - } - /* docs/CANVAS.md §7: every coarse node (5x/10x the live step) keeps a - bigger, more opaque dot, so zoomed far out the grid still reads as a - grid instead of a grey wash. */ - .griddot.major { - opacity: 0.5; - stroke-width: 0; - } - /* the contour being drawn in the Plan editor: each wall is its own , - so the round cap IS the corner (matches the finished .room border) */ - .seg { - stroke: var(--hp-accent); - stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); - stroke-linecap: round; - stroke-linejoin: round; - } - .pathline { - stroke: #ffc14d; - stroke-width: calc(3px * var(--hp-cell-visual-scale, 1)); - fill: none; - stroke-linecap: round; - stroke-linejoin: round; - } - .preview { - stroke: #ffc14d; - stroke-width: calc(2px * var(--hp-cell-visual-scale, 1)); - stroke-dasharray: - calc(6px * var(--hp-cell-visual-scale, 1)) - calc(5px * var(--hp-cell-visual-scale, 1)); - opacity: 0.7; - } - .active-axis { - stroke: #ffc14d; - stroke-width: 2; - stroke-linecap: round; - vector-effect: non-scaling-stroke; - pointer-events: none; - } - .active-vertex { - fill: #ffc14d; - stroke: #171006; - stroke-width: 2; - vector-effect: non-scaling-stroke; - pointer-events: none; - } - .wall-repair-preview { - stroke: #d93025; - stroke-width: 4; - stroke-linecap: round; - stroke-dasharray: 5 4; - vector-effect: non-scaling-stroke; - pointer-events: none; - } - .drawwall-preview-fill { - fill: var(--wall-fill, #ffffff); - fill-opacity: calc(var(--wall-fill-op, 1) * 0.55); - fill-rule: evenodd; - stroke: none; - pointer-events: none; - } - .drawwall-preview { - fill: url(#hp-wall-hatch); - fill-opacity: 0.55; - fill-rule: evenodd; - stroke: var(--accent-color, #03a9f4); - stroke-width: calc(0.5px * var(--hp-cell-visual-scale, 1)); - stroke-opacity: 0.7; - pointer-events: none; - } - .drawwall-zero-preview { - fill: none; - stroke: var(--accent-color, #03a9f4); - stroke-width: calc(3px * var(--hp-cell-visual-scale, 1)); - stroke-linecap: butt; - stroke-linejoin: round; - opacity: 0.72; - pointer-events: none; - } - .drawwall-zero-preview.dashed { - stroke-dasharray: - calc(7px * var(--hp-cell-visual-scale, 1)) - calc(7px * var(--hp-cell-visual-scale, 1)); - } - .vertex { - fill: #ffc14d; - stroke: #4a2800; - stroke-width: calc(1px * var(--hp-cell-visual-scale, 1)); - } - .vertex.first { - fill: #4bd28f; - stroke: #04121f; - } - .plan-snap-overlay, - .plan-snap-overlay *, - .hidden-wall-diagnostic, - .hidden-wall-diagnostic * { - pointer-events: none; - } - .plan-snap-line, - .hidden-wall-line { - fill: none; - stroke: color-mix(in srgb, var(--hp-accent) 82%, white 18%); - /* Explicit non-scaling-stroke in the SVG keeps this one screen pixel. */ - stroke-width: 1; - stroke-linecap: round; - opacity: 0.92; - } - .plan-snap-node, - .hidden-wall-node { - fill: var(--ha-card-background, var(--card-background-color, #fff)); - stroke: color-mix(in srgb, var(--hp-accent) 88%, #07131c 12%); - stroke-width: 1; - vector-effect: non-scaling-stroke; - } - .plan-snap-node.active { - fill: #ffc14d; - stroke: #171006; - stroke-width: 2; - } - .plan-snap-node.active.dynamic { - fill: #4bd28f; - stroke: #04121f; - } - .plan-snap-node.conflict { - fill: #fff; - stroke: #d93025; - stroke-width: 3; - } - @media (prefers-color-scheme: dark) { - .plan-snap-line { - stroke: color-mix(in srgb, var(--hp-accent) 72%, white 28%); - } - .hidden-wall-line { - stroke: color-mix(in srgb, var(--hp-accent) 72%, white 28%); - } - .plan-snap-node, - .hidden-wall-node { - fill: #17242c; - stroke: #9bdcf5; - } - .plan-snap-node.active { - fill: #ffc14d; - stroke: #fff4d6; - } - .plan-snap-node.active.dynamic { - fill: #4bd28f; - stroke: #eafff4; - } - } - @media (forced-colors: active) { - .plan-snap-line, - .hidden-wall-line { - stroke: CanvasText; - opacity: 1; - forced-color-adjust: auto; - } - .plan-snap-node, - .hidden-wall-node { - fill: Canvas; - stroke: CanvasText; - forced-color-adjust: auto; - } - .plan-snap-node.active, - .plan-snap-node.active.dynamic { - fill: Highlight; - stroke: HighlightText; - forced-color-adjust: auto; - } - } - /* RGB lights: the bulb takes the light's actual color */ - /* v1.52.0: the RGB tint of the icon/border is gone — a lamp's colour - lives ONLY in its glow spot (owner's rule). The ripple-color fallback - keeps using the light colour; that is set inline via --ripple-color. */ - /* Sun wedges (docs/SUN.md). The layer is present ONLY above the 3° - threshold; crossing it fades the whole layer in or out over EXACTLY - 2 s (owner 2026-08-03 — RAY_FADE_MS in src/sun.ts must match). The - geometry is untouched: this is a plain opacity animation on the group, - so overlapping wedges keep their own blending while it plays. */ - .sunlayer { - animation: hp-sunfade-in 2s linear both; - } - .sunlayer.out { - animation: hp-sunfade-out 2s linear both; - } - @media (prefers-reduced-motion: reduce) { - /* no fade at all: the rays are simply there or simply gone */ - .sunlayer, .sunlayer.out { animation: none; } - .sunlayer.out { opacity: 0; } - } - .devlayer { - position: absolute; - inset: 0; - pointer-events: none; - z-index: 6; - } - .stage.mode-devices .dev { cursor: grab; } - .stage.mode-devices .dev:active { cursor: grabbing; } - .physical-hit { - fill: transparent; - stroke: transparent; - pointer-events: none; - cursor: grab; - } - .stage.tool-select .physical-hit { pointer-events: all; } - .physical-hit:active { cursor: grabbing; } - line.physical-hit { cursor: pointer; } - .physical-hit.selected { - fill: rgba(255, 193, 77, 0.24); - stroke: transparent; - } - line.physical-hit.selected { - stroke: transparent; - } - .physical-drag { - fill: rgba(255, 193, 77, 0.38); - stroke: #ffc14d; - stroke-width: 2.5; - vector-effect: non-scaling-stroke; - pointer-events: none; - } - .physical-chrome { pointer-events: none; } - .physical-chrome .frame, - .physical-chrome .stem { - fill: none; - stroke: #ffc14d; - stroke-width: 2; - vector-effect: non-scaling-stroke; - } - .physical-chrome .frame { fill: rgba(255, 193, 77, 0.22); } - .physical-chrome polyline.frame { fill: none; } - .physical-chrome .move-dot { - fill: #ffc14d; - stroke: #24262d; - stroke-width: 1.5; - vector-effect: non-scaling-stroke; - } - .physical-chrome .rotate-handle { - fill: #24262d; - stroke: #ffc14d; - stroke-width: 2.5; - vector-effect: non-scaling-stroke; - pointer-events: all; - cursor: crosshair; - } - .drawwall.invalid input { border-color: var(--error-color, #db4437); } - .drawwall .rangehint { margin-inline-start: 4px; font-size: 0.78em; opacity: 0.72; } - .drawwall.invalid .rangehint { color: var(--error-color, #db4437); opacity: 1; } - .stage.markup.tool-partition, - .stage.markup.tool-column { cursor: crosshair; } - /* room resize tool (docs/RESIZE.md) */ - /* wall handle: invisible finger-sized hit circle (HP-1550-04 hit priority - kept), the visible glyph lives in the sibling .rszicon */ - .rszhandle { - fill: transparent; - stroke: none; - pointer-events: all; - cursor: grab; - touch-action: none; - } - .rszhandle:active { cursor: grabbing; } - .rszhandle.disabled, - .rszhandle.disabled:active { - cursor: not-allowed; - } - /* wall-with-arrows glyph: accent ink over a bg halo, readable on any plan */ - .rszicon { pointer-events: none; } - .rszicon path { - fill: none; - stroke-linecap: round; - stroke-linejoin: round; - vector-effect: non-scaling-stroke; - } - .rszhalo { stroke: var(--hp-bg); stroke-width: 6; } - .rszink { stroke: var(--hp-accent); stroke-width: 2; } - :host([data-pointer-hover]) .rszhandle:hover + .rszicon .rszink { stroke-width: 3; } - .rszicon.disabled { opacity: 0.38; } - :host([data-pointer-hover]) .rszhandle.disabled:hover + .rszicon .rszink { stroke-width: 2; } - .rszmeasurelayer, - .rszmeasurelayer * { pointer-events: none; } - .rszmeasurehalo, - .rszmeasureink, - .rszleader { - fill: none; - stroke-linecap: round; - vector-effect: non-scaling-stroke; - } - .rszmeasurehalo { - stroke: var(--hp-bg); - stroke-width: 7px; - opacity: 0.9; - } - .rszmeasureink { - stroke: var(--hp-accent); - stroke-width: 3px; - } - .rszleader { - stroke: var(--hp-accent); - stroke-width: 2px; - opacity: 0.95; - } - /* the decor draft badge rides the MIDDLE of the shape, so it is centred - horizontally and lifted clear of the line instead of trailing the - cursor the way a wall badge does (owner 2026-08-04) */ - .measurelabel.dmeasure { - transform: translate(-50%, -160%); - } - .measurelabel.rszarea { - transform: translate( - calc(-50% + var(--rsz-label-x, 0px)), - calc(-50% + var(--rsz-label-y, 0px)) - ); - background: rgba(0, 0, 0, 0.6); - border: 1px solid var(--hp-accent); - } - /* width and depth of a piece of furniture while its corner is dragged — - centred on the edge they measure (docs/FURNITURE.md §6) */ - .measurelabel.furnmeasure { - transform: translate(-50%, -50%); - border: 1px solid var(--hp-accent); - } - .alignmsg { margin: 0 0 8px; font-size: 13px; line-height: 1.45; } -`,o` - /* Unified device activity: alarm, short event and continuous state all - share this one renderer. No static rings are allowed. */ - .dev ha-icon { - position: relative; - z-index: 1; - } - .device-pulse { - position: absolute; - left: 50%; - top: 50%; - width: var(--dev-size); - height: var(--dev-size); - transform: translate(-50%, -50%); - pointer-events: none; - z-index: 0; - } - .device-pulse i { - position: absolute; - inset: 0; - border-radius: 50%; - border: 2px solid var(--ripple-color, var(--hp-accent)); - opacity: 0; - } - /* A witnessed edge: exactly three waves over the 3.3 s runtime window. */ - .device-pulse.short i { - animation: hp-pulse-short 1.1s cubic-bezier(.22,.61,.36,1) 1 forwards; - } - .device-pulse.short i:nth-child(2) { animation-delay: 1.1s; } - .device-pulse.short i:nth-child(3) { animation-delay: 2.2s; } - .device-pulse.short.gen2 i { animation-name: hp-pulse-short-b; } - .device-pulse.continuous i:nth-child(n + 2), - .device-pulse.alarm i:nth-child(n + 3) { display: none; } - .device-pulse.continuous i:first-child { - animation: hp-pulse-continuous 3.6s cubic-bezier(.45,.05,.55,.95) infinite; - } - .device-pulse.alarm i { - border-width: 3px; - border-color: #F0410C; - animation: hp-pulse-alarm 2.4s cubic-bezier(.22,.61,.36,1) infinite; - } - .device-pulse.alarm i:nth-child(2) { animation-delay: 1.2s; } - .activity-dot { - position: absolute; - right: 8%; - bottom: 8%; - width: 18%; - height: 18%; - min-width: 4px; - min-height: 4px; - border-radius: 50%; - background: var(--ripple-color, var(--hp-accent)); - border: 1px solid color-mix(in srgb, var(--hp-bg) 82%, transparent); - pointer-events: none; - z-index: 3; - } - .kioskdots { - position: absolute; - left: 50%; - bottom: var(--sp-5); - transform: translateX(-50%); - display: flex; - gap: var(--sp-4); - z-index: 5; - pointer-events: none; - } - .kdot { - width: 9px; - height: 9px; - border-radius: 50%; - background: var(--hp-muted); - opacity: 0.55; - } - .kdot.on { background: var(--hp-accent); opacity: 1; } - .dev.valonly { - /* Saved coordinates remain the centre of a Text shell. The shell may - expand, while the invisible anchor stays one core diameter wide. */ - width: var(--dev-size, var(--icon-size, 2.5cqw)); - } - .dev.valonly .device-core { - width: max-content; - min-width: var(--dev-size, var(--icon-size, 2.5cqw)); - padding-inline: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.16); - border-radius: calc(var(--dev-size, var(--icon-size, 2.5cqw)) / 2); - } - .dev .valtext { - overflow: visible; - font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * var(--value-font-scale, .45)); - font-weight: 600; - white-space: nowrap; - } - @media (prefers-reduced-motion: reduce) { - .device-pulse { display: none; } - } - .dev .newdot { - position: absolute; - top: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * -0.12); - right: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * -0.12); - width: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.34); - height: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.34); - border-radius: 50%; - background: #f0301f; - border: 2px solid var(--card-background-color, var(--hp-bg)); - pointer-events: none; - z-index: 2; - } - .dev { - position: absolute; - /* The surface resolves compatibility icon_size units once. The shared - face receives an effective base and only applies the per-device size. */ - --dev-size: calc(var(--device-base-size, 2.25cqw) * var(--dev-scale, 1)); - /* 101.5/80 is the package shell/core ratio, including its stroke. */ - --device-shell-size: calc(var(--dev-size) * 1.26875); - --device-shell-inset: calc(var(--dev-size) * 0.134375); - --device-shell-stroke-ratio: 0.01875; - --device-shell-border-width: max(1px, calc(var(--dev-size) * var(--device-shell-stroke-ratio))); - --device-core-bg: var(--card-background-color, #fff); - --device-core-fg: var(--primary-text-color, #252525); - --device-core-bg: light-dark(#fff, #252525); - --device-core-fg: light-dark(#252525, #fff); - --device-face-bg: var(--device-core-bg); - --device-face-fg: var(--device-core-fg); - --device-shell-base-stroke: light-dark(#BCBCBC, rgb(37 37 37 / 75%)); - --device-shell-stroke: var(--device-shell-base-stroke); - --device-shell-shadow: - 0 calc(var(--dev-size) * .025) calc(var(--dev-size) * .05) rgb(37 40 45 / 12%), - 0 calc(var(--dev-size) * .1) calc(var(--dev-size) * .175) - calc(var(--dev-size) * -.025) rgb(37 40 45 / 18%); - --device-core-inset-shadow: 0 0 0 0 transparent; - --device-ring-color: transparent; - --device-ring-width: 0px; - /* The saved point is the exact centre of the icon core. */ - width: var(--dev-size); - height: var(--dev-size); - margin: calc(var(--dev-size) / -2) 0 0 calc(var(--dev-size) / -2); - border: 0; - background: transparent; - display: block; - color: var(--device-core-fg); - cursor: pointer; - pointer-events: auto; - transition: opacity 0.2s; - box-shadow: none; - outline: none; - z-index: 2; - } - .dev.theme-light { - --device-core-bg: #fff; - --device-core-fg: #000; - --device-shell-base-stroke: #BCBCBC; - --device-shell-stroke: var(--device-shell-base-stroke); - --device-shell-shadow: - 0 calc(var(--dev-size) * .025) calc(var(--dev-size) * .05) rgb(37 40 45 / 12%), - 0 calc(var(--dev-size) * .1) calc(var(--dev-size) * .175) - calc(var(--dev-size) * -.025) rgb(37 40 45 / 18%); - --device-core-inset-shadow: 0 0 0 0 transparent; - } - .dev.theme-dark { - --device-core-bg: #252525; - --device-core-fg: #fff; - --device-shell-base-stroke: rgb(37 37 37 / 75%); - --device-shell-stroke: var(--device-shell-base-stroke); - --device-shell-shadow: - 0 calc(var(--dev-size) * .025) calc(var(--dev-size) * .0375) rgb(37 40 45 / 12%), - 0 calc(var(--dev-size) * .1) calc(var(--dev-size) * .175) - calc(var(--dev-size) * -.025) rgb(37 40 45 / 18%); - --device-core-inset-shadow: - inset 0 calc(var(--dev-size) * 0.0125) calc(var(--dev-size) * 0.0125) rgb(255 255 255 / 70%); - } - .dev::before { - content: ''; - position: absolute; - left: 50%; - top: 50%; - width: max(44px, var(--device-shell-size)); - height: max(44px, var(--device-shell-size)); - transform: translate(-50%, -50%); - border-radius: 50%; - pointer-events: auto; - z-index: 3; - } - .device-shell { - position: absolute; - box-sizing: border-box; - display: flex; - align-items: center; - gap: calc(var(--dev-size) * 0.1); - padding: 0; - min-width: var(--dev-size); - min-height: var(--dev-size); - border: 0; - background: transparent; - box-shadow: none; - transition: opacity .2s; - pointer-events: none; - } - .device-shell-frame { - position: absolute; - z-index: 0; - box-sizing: border-box; - inset: calc(var(--device-shell-inset) / -1); - border: var(--device-shell-border-width) solid var(--device-shell-stroke); - /* A saturating radius is resolved from the final border box. Using a - second fractional length here lets border-box and radius quantise on - different sides of a device pixel at some zoom/DPR combinations. */ - border-radius: 9999px; - background: transparent; - box-shadow: var(--device-shell-shadow); - transition: border-color .15s, box-shadow .15s, opacity .2s; - pointer-events: auto; - /* Normative production fallback: never add a per-marker backdrop blur. */ - backdrop-filter: none; - } - .device-shell:not(.with-values) { - left: 0; - top: 0; - } - .device-shell:not(.with-values):not(.text-shell) .device-shell-frame { - border-radius: 50%; - } - .device-shell.with-values.pos-right { - left: 0; - top: 50%; - transform: translateY(-50%); - } - .device-shell.with-values.pos-left { - right: 0; - top: 50%; - transform: translateY(-50%); - flex-direction: row-reverse; - } - .device-shell.with-values.pos-bottom { - left: 50%; - top: 0; - transform: translateX(-50%); - flex-direction: column; - } - .device-shell.with-values.pos-top { - left: 50%; - bottom: 0; - transform: translateX(-50%); - flex-direction: column-reverse; - } - .device-core { - position: relative; - z-index: 1; - box-sizing: border-box; - flex: 0 0 auto; - width: var(--dev-size); - height: var(--dev-size); - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - background: var(--device-face-bg); - color: var(--device-face-fg); - box-shadow: - var(--device-core-inset-shadow), - 0 0 0 var(--device-ring-width) var(--device-ring-color); - line-height: 0; - transition: background .15s, color .15s, box-shadow .15s, opacity .2s; - pointer-events: none; - } - .device-sections { - display: flex; - flex-direction: column; - align-items: stretch; - justify-content: center; - gap: calc(var(--dev-size) * .08); - min-width: 0; - pointer-events: none; - } - .device-shell.pos-top .device-sections, - .device-shell.pos-bottom .device-sections { - flex-direction: row; - } - .dev ha-icon { - /* from --dev-size, NOT --icon-size: the per-device size multiplier must - scale the GLYPH with its badge. Pinned to the base size, "make this - icon bigger" grew an empty box around a default-size glyph (user - report via the owner, 2026-07-29). */ - --mdc-icon-size: calc(var(--dev-size, var(--device-base-size, 2.25cqw)) * 0.55); - display: flex; - align-items: center; - justify-content: center; - line-height: 0; - } - .dev.on { - --device-face-bg: #F0A00C; - --device-face-fg: light-dark(#fff, #252525); - --device-shell-stroke: #F0A00C; - } - .dev.theme-light.on { --device-face-fg: #fff; } - .dev.theme-dark.on { - --device-face-fg: #252525; - --device-shell-stroke-ratio: .0375; - } - .dev.open { - --device-face-bg: var(--hp-open); - --device-face-fg: light-dark(#fff, #252525); - } - .dev.theme-light.open { --device-face-fg: #fff; } - .dev.theme-dark.open { --device-face-fg: #252525; } - .dev.lock-locked { - --device-face-bg: #66D17A; - --device-face-fg: light-dark(#fff, #252525); - --device-shell-stroke: #66D17A; - } - .dev.theme-light.lock-locked { --device-face-fg: #fff; } - .dev.theme-dark.lock-locked { - --device-face-fg: #252525; - --device-shell-stroke-ratio: .025; - } - .dev.lock-unlocked { - --device-face-bg: #F0410C; - --device-face-fg: light-dark(#fff, #252525); - --device-shell-stroke: #F0410C; - } - .dev.theme-light.lock-unlocked { --device-face-fg: #fff; } - .dev.theme-dark.lock-unlocked { - --device-face-fg: #252525; - --device-shell-stroke-ratio: .025; - } - /* Interaction wins ordinary state colours. Alarm keeps priority through - the more-specific rule below. Unavailable has no visual hover. */ - :host([data-pointer-hover]) .dev:not(.unavail):hover { - --device-face-bg: #0C82F0; - --device-face-fg: light-dark(#fff, #252525); - --device-shell-stroke: var(--device-shell-base-stroke); - } - :host([data-pointer-hover]) .dev.theme-light:not(.unavail):hover { --device-face-fg: #fff; } - :host([data-pointer-hover]) .dev.theme-dark:not(.unavail):hover { --device-face-fg: #252525; } - :host([data-pointer-hover]) .dev:hover, - .dev:focus-visible { z-index: 5; } - .dev.unavail { - opacity: 0.35; - --device-face-bg: #B5BAC1; - --device-shell-stroke: var(--device-shell-base-stroke); - } - .dev.virtual .device-shell-frame { - border-style: dashed; - } - /* "hide from plan" flag, shown only in the device editor with the - "show hidden devices" toggle on (docs/FILTERING.md). BLUE, so a hidden - device cannot be mistaken for an unavailable one (translucent dark) — - and no live-state paint at all: a ghost is configuration, not status - (owner's request). */ - .dev.ghost { - opacity: 0.6; - color: var(--hp-accent); - } - .dev.ghost .device-shell-frame { - border-style: dashed; - border-color: var(--hp-accent); - box-shadow: none; - } - .dev.ghost .device-core { - background: rgba(62, 166, 255, 0.22); /* fallback for old WebViews */ - background: color-mix(in srgb, var(--hp-accent) 30%, var(--card-background-color, #1c2530)); - color: var(--hp-accent); - } - /* HA-disabled is not a user hide: neutral grey + a power-off badge keeps - the two ghosts distinguishable without relying on colour alone. */ - .dev.ghost.ha-disabled { - opacity: 0.62; - color: var(--secondary-text-color, #9aa0aa); - } - .dev.ghost.ha-disabled .device-shell-frame { - border-color: var(--secondary-text-color, #9aa0aa); - } - .dev.ghost.ha-disabled .device-core { - background: rgba(120, 124, 134, 0.2); - background: color-mix(in srgb, var(--secondary-text-color, #9aa0aa) 24%, var(--card-background-color, #1c2530)); - color: var(--secondary-text-color, #9aa0aa); - } - .dev .habadge { - position: absolute; - right: -18%; - bottom: -18%; - display: grid; - place-items: center; - width: 46%; - height: 46%; - min-width: 12px; - min-height: 12px; - border-radius: 50%; - background: var(--card-background-color, #242832); - border: 1px solid currentColor; - } - .dev .habadge ha-icon { width: 72%; height: 72%; } - .dev.sel { - --device-ring-color: #F0A00C; - --device-ring-width: calc(var(--dev-size) * 0.0375); - } - .dev:focus-visible { - --device-ring-color: #0C82F0; - --device-ring-width: calc(var(--dev-size) * 0.0375); - } - .dev:not(.on):not(.open):not(.alarm):not(.lock-locked):not(.lock-unlocked):not(.unavail):focus-visible { - --device-face-fg: #0C82F0; - } - /* Alert stays above focus, selection, hover and ordinary semantic paint. */ - .dev.alarm, - :host([data-pointer-hover]) .dev.alarm:hover, - .dev.alarm:focus-visible { - --device-face-bg: #F0410C; - --device-face-fg: light-dark(#fff, #252525); - --device-shell-stroke: #F0410C; - } - .dev.theme-light.alarm { --device-face-fg: #fff; } - .dev.theme-dark.alarm { - --device-face-fg: #252525; - --device-shell-stroke-ratio: .025; - } - .dev .value-badge { - position: relative; - z-index: 2; - box-sizing: border-box; - width: max-content; - min-width: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * .7875); - height: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * .7875); - display: flex; - align-items: center; - justify-content: center; - overflow: visible; - background: var(--device-core-bg); - border: 0; - border-radius: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.39375); - padding: 0 calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.14); - font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * var(--value-font-scale, .45)); - font-weight: 600; - line-height: 1; - color: var(--device-core-fg); - white-space: nowrap; - pointer-events: none; - } - .dev .value-badge.unavailable, - .dev .value-badge.missing { opacity: 0.66; } - .dev .lqi { - position: absolute; - top: calc(50% + var(--device-shell-size) / 2 + var(--dev-size) * .05); - left: 50%; - transform: translateX(-50%); - margin-top: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.05); - font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.38); - font-weight: 600; - line-height: 1; - text-shadow: 0 0 3px rgba(0, 0, 0, 0.9), 0 0 2px rgba(0, 0, 0, 0.9); - white-space: nowrap; - pointer-events: none; - } - .dev .lqi.below-value-badge { - margin-top: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.8875); - } - .temprange { - display: inline-flex; - align-items: center; - gap: var(--sp-3); - margin-left: auto; - color: var(--hp-muted); - font-size: var(--fs-s); - } - /* live vacuum: a round puck, no badge plate, soft pulse (docs/VACUUM.md) */ - .vacpuck { - position: absolute; - /* the base badge, but round and 20% smaller — the owner's wording: - «иконка похожа на иконку базы, только круглая и чуть меньше» */ - --puck-size: calc(var(--device-base-size, 2.25cqw) * 0.8); - width: var(--puck-size); - height: var(--puck-size); - border-radius: 50%; - transform: translate(-50%, -50%); - --vac-core-bg: light-dark(#fff, #252525); - --vac-core-fg: light-dark(#252525, #fff); - background: var(--vac-core-bg); - border: 1px solid #BCBCBC; - box-shadow: - 0 1px 2px rgb(37 40 45 / 12%), - 0 4px 8px -1.07px rgb(37 40 45 / 18%); - color: var(--vac-core-fg); - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - z-index: 6; - /* glide between sparse position updates; .jump disables it so the robot - never appears to drive through walls after a data gap */ - transition: left 1.2s linear, top 1.2s linear; - animation: vacpulse 2.2s ease-out infinite; - } - .vacpuck.theme-light { --vac-core-bg: #fff; --vac-core-fg: #252525; } - .vacpuck.theme-dark { --vac-core-bg: #252525; --vac-core-fg: #fff; } - .vacpuck.jump { transition: none; } - .vacpuck.stale { opacity: 0.45; animation: none; } - .vacpuck ha-icon { - --mdc-icon-size: calc(var(--puck-size) * 0.68); - color: var(--vac-core-fg); - /* same centering recipe as .dev ha-icon: without flex + line-height 0 - the glyph sits on its text baseline and appears to float around the - circle (owner report 2026-07-31) */ - display: flex; - align-items: center; - justify-content: center; - line-height: 0; - width: var(--mdc-icon-size); - height: var(--mdc-icon-size); - } - @keyframes vacpulse { - 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--hp-accent) 45%, transparent); } - 70% { box-shadow: 0 0 0 12px transparent; } - 100% { box-shadow: 0 0 0 0 transparent; } - } - @media (prefers-reduced-motion: reduce) { - .vacpuck { animation: none; } - } - .vactrail { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - pointer-events: none; - z-index: 5; - overflow: visible; - } - .vactrail polyline, - .vactrail path { - fill: none; - stroke-linejoin: round; - stroke-linecap: round; - vector-effect: non-scaling-stroke; - } - /* dark halo + light core: neutral, and one of the two always contrasts - with whatever fill is underneath */ - .vactrail g.prev { opacity: 0.4; } - .vactrail .case { - stroke: rgba(0, 0, 0, 0.4); - stroke-width: 2.25; - } - .vactrail .core { - stroke: rgba(255, 255, 255, 0.82); - stroke-width: 0.9; - } - .devicepreview-empty { - min-height: 82px; - margin: 4px 0 14px; - padding: var(--sp-4); - display: flex; - align-items: center; - justify-content: center; - gap: var(--sp-3); - border: 1px dashed var(--hp-line); - border-radius: var(--rad-m); - color: var(--hp-muted); - text-align: center; - } -`,o` - .hdr { - position: sticky; - top: var(--header-height, 56px); - z-index: 20; - background: var(--card-background-color, var(--hp-bg)); - border-radius: var(--ha-card-border-radius, 12px) var(--ha-card-border-radius, 12px) 0 0; - } - .tabs { - display: flex; - gap: var(--sp-2); - background: rgba(127, 127, 127, 0.12); - padding: var(--sp-2); - border-radius: var(--rad-l); - flex-wrap: wrap; - } - .tab { - border: 0; - background: transparent; - color: var(--hp-muted); - display: inline-flex; - align-items: center; - padding: var(--sp-3) var(--sp-5); - border-radius: var(--rad-m); - font-size: var(--fs-m); - font-weight: 600; - cursor: pointer; - transition: 0.15s; - font-family: inherit; - } - :host([data-pointer-hover]) .tab:hover { - color: var(--hp-txt); - } - .tab.active { - background: var(--hp-accent); - color: var(--text-primary-color, #fff); - } - /* docs/CANVAS.md §4.1: objects an order of magnitude away from the plan - do not decide the opening view — one quiet chip says so and offers to - take them in. Never a modal (owner). */ - .farhint { - position: absolute; - left: 50%; - transform: translateX(-50%); - bottom: var(--sp-4); - z-index: 12; - display: flex; - align-items: center; - gap: var(--sp-2); - max-width: calc(100% - var(--sp-8)); - background: var(--card-background-color, var(--hp-bg)); - opacity: 0.94; - color: var(--hp-txt); - border: 1px solid var(--divider-color, #33404d); - border-radius: var(--rad-m); - padding: var(--sp-1) var(--sp-3); - font-size: var(--fs-s); - } - .farhint ha-icon { - --mdc-icon-size: 18px; - color: var(--hp-warn, #e2a03f); - flex: none; - } - .farhint span { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - .dropbtn { - display: flex; - align-items: center; - gap: var(--sp-4); - width: 100%; - text-align: left; - border: 1px solid var(--hp-muted); - border-radius: var(--rad-m); - background: transparent; - color: var(--hp-txt); - padding: var(--sp-4) 10px; - cursor: pointer; - font-family: inherit; - font-size: var(--fs-m); - margin-top: var(--sp-3); - } - .dropbtn .ref { color: var(--hp-muted); font-size: var(--fs-s); margin-left: auto; } - .dropbtn ha-icon { --mdc-icon-size: 18px; margin-left: var(--sp-2); } - .dropbtn.open { border-color: var(--hp-accent); } - .droppanel { - border: 1px solid var(--hp-accent); - border-top: none; - border-radius: 0 0 var(--rad-m) var(--rad-m); - padding: var(--sp-3); - margin-top: -4px; - } - .hdr.kioskhide { display: none; } - .decorbar .dcolor { - width: 30px; height: 26px; padding: 0; border: none; background: none; cursor: pointer; - } - .decorbar .dwidth { - font-family: inherit; font-size: var(--fs-s); border-radius: var(--rad-s); - background: var(--hp-bg2, transparent); color: var(--hp-txt); border: 1px solid var(--hp-muted); - padding: var(--sp-2) var(--sp-3); - } - .decorbar .bdhint { - font-size: var(--fs-s); - color: var(--hp-muted); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - .decorbar .dfill { - display: inline-flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-s); cursor: pointer; - } - .decorbar .dfill input[type="checkbox"] { - width: 16px; - height: 16px; - flex: none; - margin: 0; - padding: 0; - } - .decorbar hp-color-opacity { flex: 0 0 auto; } - .modes { - display: inline-flex; - gap: var(--sp-1); - background: rgba(127, 127, 127, 0.12); - border-radius: var(--rad-l); - padding: var(--sp-2); - } - .modetab { - display: inline-flex; - align-items: center; - gap: var(--sp-3); - border: 0; - background: transparent; - color: var(--hp-muted); - padding: var(--sp-3) 10px; /* 10px h-padding kept: +2px would wrap the header modes row */ - border-radius: var(--rad-m); - font-size: var(--fs-m); - font-weight: 600; - cursor: pointer; - font-family: inherit; - transition: background-color 0.14s ease, color 0.14s ease, transform 0.14s ease; - } - .modetab:active { transform: scale(0.97); } - .modetab ha-icon { --mdc-icon-size: 15px; } - /* issue #220: a tab can be dragged to a new position in the editors */ - .tab[data-reorderable] { cursor: grab; } - .tab.dragging { cursor: grabbing; opacity: 0.55; } - .tab.drop-before { box-shadow: inset 2px 0 0 0 var(--primary-color, #03a9f4); } - .tab.drop-after { box-shadow: inset -2px 0 0 0 var(--primary-color, #03a9f4); } - .modetab .closex { - --mdc-icon-size: 13px; - box-sizing: border-box; - display: inline-flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - min-width: 24px; - min-height: 24px; - /* Keep the old 13 × 13 flex footprint (+2 px left margin) while the - pointer target grows around it. Header width, height and wrapping do - not move; only near-misses that used to hit the active tab reach X. */ - margin: -5.5px -5.5px -5.5px -3.5px; - opacity: 0.75; - cursor: pointer; - border-radius: var(--rad-s); - } - :host([data-pointer-hover]) .modetab .closex:hover { opacity: 1; } - .editbar .barclose { - box-sizing: border-box; - width: 40px; - height: 40px; - padding: 0; - margin: 0; - min-width: 40px; - min-height: 40px; - justify-content: center; - gap: 0; - line-height: 0; - } - .editbar .barclose ha-icon { flex: none; margin: 0; } - .modetab.active { - background: var(--hp-accent); - color: var(--text-primary-color, #fff); - } - @media (max-width: 720px) { - .modetab .ml { display: none; } - } - .editbar { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - align-items: stretch; - border-bottom: 1px solid var(--hp-line); - font-size: var(--fs-m); - } - .editbar-tools { - display: flex; - align-items: center; - gap: 10px; - padding: var(--sp-4) var(--sp-5); - flex-wrap: wrap; - min-width: 0; - outline: none; - } - .editbar-end { - display: flex; - align-items: center; - padding: var(--sp-4) var(--sp-5); - border-inline-start: 1px solid var(--hp-line); - background: var(--card-background-color, var(--hp-bg)); - position: relative; - z-index: 2; - } - .tab .tabedit { - --mdc-icon-size: 13px; - display: inline-flex; - align-items: center; - margin-left: var(--sp-3); - opacity: 0.4; - } - :host([data-pointer-hover]) .tab:hover .tabedit { - opacity: 0.9; - } - .tab.tabadd { - padding: var(--sp-3) var(--sp-4); - } - .tab.tabadd ha-icon { - --mdc-icon-size: 15px; - } - .rhint { - font-size: var(--fs-s); - color: var(--hp-muted); - margin-bottom: var(--sp-3); - } - .togglehint { - overflow-wrap: anywhere; - } - .togglehint > div + div { - margin-top: var(--sp-1); - } - /* Keep the last editor bar mounted while the row collapses. This makes - both entering and leaving an editor change the card geometry gradually; - the header ResizeObserver keeps the stage fitted throughout. */ - .editorchrome { - display: block; - height: 0; - opacity: 0; - visibility: hidden; - overflow: hidden; - } - .editorchrome:not(.open) { - pointer-events: none; - } - .editorchrome.open { - height: auto; - opacity: 1; - visibility: visible; - overflow: visible; - } - .editorchrome.transitioning { - overflow: hidden; - will-change: height; - pointer-events: none; - } - /* The toolbar is already visible while its height is interpolating. Its - explicit navigation control must remain usable even though all editing - tools stay frozen until the transition settles. */ - .editorchrome.transitioning .barclose { - pointer-events: auto; - } - .editorchrome-inner { - min-height: 0; - transform-origin: top center; - } - .editorchrome.transitioning .editorchrome-inner { - will-change: opacity; - } - @media (prefers-reduced-motion: reduce) { - .modetab { transition: none; } - } - .editbar .warn { - color: #ffc14d; - } - .editbar .sname { - font-weight: 600; - } - .editbar input { - width: 74px; - background: transparent; - border: 1px solid var(--hp-line); - color: var(--hp-txt); - border-radius: var(--rad-s); - padding: var(--sp-3) var(--sp-4); - font-size: var(--fs-m); - } - .editbar label, - .editbar .hint { - color: var(--hp-muted); - font-size: var(--fs-s); - } - .editbar .drawwall { - display: inline-flex; - align-items: center; - gap: var(--sp-3); - white-space: nowrap; - } - .editbar .wallsgroup { - display: inline-flex; - align-items: center; - gap: 10px; - white-space: nowrap; - } - .editbar .drawwall input { - width: 4.2em; - } - .editbar .drawwall .opl { - color: var(--hp-muted); - font-size: var(--fs-s); - } - .menuwrap { - position: fixed; - inset: 0; - z-index: 80; - } - .menu { - position: fixed; - background: var(--hp-bg); - border: 1px solid var(--hp-accent); - border-radius: var(--rad-l); - box-shadow: var(--shadow-2); - min-width: 210px; - max-width: 300px; - overflow: hidden; - transform: translate(0, 8px); - } - .menu .hd { - padding: var(--sp-4) var(--sp-5); - font-weight: 600; - font-size: var(--fs-m); - border-bottom: 1px solid var(--hp-line); - display: flex; - align-items: center; - gap: var(--sp-3); - } - .menu .hd ha-icon, - .menu .it.all ha-icon { - color: var(--hp-accent); - --mdc-icon-size: 16px; - } - .menu .it { - padding: var(--sp-4) var(--sp-5); - font-size: var(--fs-m); - cursor: pointer; - display: flex; - align-items: center; - gap: var(--sp-4); - } - .menu .it ha-icon { - --mdc-icon-size: 16px; - color: var(--hp-muted); - } - :host([data-pointer-hover]) .menu .it:hover { - background: rgba(127, 127, 127, 0.15); - } - .menu .it.all { - color: var(--hp-accent); - font-weight: 600; - } - .tip { - position: fixed; - pointer-events: none; - background: var(--hp-bg); - border: 1px solid var(--hp-accent); - color: var(--hp-txt); - padding: var(--sp-3) 10px; - border-radius: var(--rad-m); - font-size: var(--fs-m); - box-shadow: var(--shadow-2); - z-index: 60; - max-width: 260px; - } - .tip .m { - color: var(--hp-muted); - font-size: var(--fs-s); - display: block; - } -`,o` - .head { - display: flex; - align-items: center; - gap: 10px; - padding: 10px var(--sp-5); - border-bottom: 1px solid var(--hp-line); - flex-wrap: wrap; - } - .title { - font-size: var(--fs-l); - font-weight: 600; - display: flex; - align-items: center; - gap: var(--sp-3); - white-space: nowrap; - } - .title ha-icon { - color: var(--hp-accent); - --mdc-icon-size: 18px; - } - @media (max-width: 620px) { - .head { gap: var(--sp-3); padding: var(--sp-4) 10px; } - .head .count { display: none; } - .head .title { font-size: var(--fs-m); } - } - .count { - font-size: var(--fs-s); - color: var(--hp-muted); - } - .btn { - display: inline-flex; - align-items: center; - gap: var(--sp-3); - border: 1px solid var(--hp-line); - background: transparent; - color: var(--hp-txt); - padding: var(--sp-3) 10px; - border-radius: var(--rad-m); - cursor: pointer; - transition: 0.15s; - font-family: inherit; - font-size: var(--fs-m); - } - .btn ha-icon { - --mdc-icon-size: 17px; - } - :host([data-pointer-hover]) .btn:hover { - border-color: var(--hp-accent); - } - .btn.on { - background: var(--hp-accent); - color: var(--text-primary-color, #fff); - border-color: var(--hp-accent); - } - .btn.ghost { - border: none; - } - .btn[disabled] { - opacity: 0.5; - pointer-events: none; - } - /* the compass dial in the general settings (docs/SUN.md) */ - .sunrow { - display: flex; - align-items: center; - gap: var(--sp-5); - margin: var(--sp-3) 0; - } - .suncol { - flex: 1; - display: flex; - flex-direction: column; - gap: var(--sp-3); - min-width: 0; - } - .recoveryoverlay { - position: absolute; - inset: 0; - /* Above the contextual editor tray (70): recovery is the one state in - which every stage-editing surface must be inert. Dialogs live outside - the stage and retain their own higher card-level stacking context. */ - z-index: 75; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: var(--sp-4); - padding: var(--sp-6); - box-sizing: border-box; - /* The final solid layer guarantees an opaque recovery surface even when - a custom HA theme exposes its card colour as rgba(). */ - background: - linear-gradient(var(--ha-card-background, var(--card-background-color, #111)), - var(--ha-card-background, var(--card-background-color, #111))), - #111; - color: var(--primary-text-color, #fff); - text-align: center; - pointer-events: auto; - transition: opacity 0.15s ease; - } - .recoveryoverlay.phase-entering, - .recoveryoverlay.phase-leaving { - opacity: 0; - } - .recoveryoverlay.phase-fading-in, - .recoveryoverlay.phase-opaque { - opacity: 1; - } - .recoveryoverlay ha-icon { - --mdc-icon-size: 44px; - color: var(--hp-accent); - } - @media (prefers-reduced-motion: reduce) { - .recoveryoverlay { - transition: none; - } - } - .oplock { - --oplock-size: calc(var(--icon-size, 2.5cqw) * 0.62); - --oplock-core-size: calc(var(--oplock-size) / 1.26875); - --oplock-stroke-ratio: 0.01875; - --oplock-stroke-width: max(1px, calc(var(--oplock-core-size) * var(--oplock-stroke-ratio))); - --oplock-core-bg: light-dark(#fff, #252525); - --oplock-core-fg: light-dark(#252525, #fff); - --oplock-shell-stroke: light-dark(#BCBCBC, rgb(37 37 37 / 75%)); - --oplock-shell-shadow: - 0 calc(var(--oplock-core-size) * .025) calc(var(--oplock-core-size) * .05) rgb(37 40 45 / 12%), - 0 calc(var(--oplock-core-size) * .1) calc(var(--oplock-core-size) * .175) - calc(var(--oplock-core-size) * -.025) rgb(37 40 45 / 18%); - --oplock-core-shadow: 0 0 0 0 transparent; - pointer-events: none; /* inert while editing; clickable in View (rule below) */ - position: absolute; - transform: translate(-50%, -50%); - width: var(--oplock-size); - height: var(--oplock-size); - display: grid; - place-items: center; - border: 0; - background: transparent; - z-index: 1; - } - .oplock.theme-light { - --oplock-core-bg: #fff; - --oplock-core-fg: #252525; - --oplock-shell-stroke: #BCBCBC; - } - .oplock.theme-dark { - --oplock-core-bg: #252525; - --oplock-core-fg: #fff; - --oplock-shell-stroke: rgb(37 37 37 / 75%); - --oplock-core-shadow: - inset 0 calc(var(--oplock-core-size) * .0125) - calc(var(--oplock-core-size) * .0125) rgb(255 255 255 / 70%); - } - .oplock-shell { - box-sizing: border-box; - width: 100%; - height: 100%; - border: var(--oplock-stroke-width) solid var(--oplock-shell-stroke); - border-radius: 50%; - background: transparent; - box-shadow: var(--oplock-shell-shadow); - display: grid; - place-items: center; - pointer-events: none; - backdrop-filter: none; - } - .oplock-core { - width: var(--oplock-core-size); - height: var(--oplock-core-size); - border-radius: 50%; - background: var(--oplock-core-bg); - color: var(--oplock-core-fg); - box-shadow: var(--oplock-core-shadow); - display: grid; - place-items: center; - pointer-events: none; - } - .oplock ha-icon { - --mdc-icon-size: calc(var(--oplock-core-size) * 0.55); - display: flex; - line-height: 0; - } - .oplock.locked { - --oplock-core-bg: #66D17A; - --oplock-core-fg: light-dark(#fff, #252525); - --oplock-shell-stroke: #66D17A; - } - .oplock.theme-light.locked { --oplock-core-fg: #fff; } - .oplock.theme-dark.locked { - --oplock-core-fg: #252525; - --oplock-stroke-ratio: .025; - } - .oplock.unlocked { - --oplock-core-bg: #F0410C; - --oplock-core-fg: light-dark(#fff, #252525); - --oplock-shell-stroke: #F0410C; - } - .oplock.theme-light.unlocked { --oplock-core-fg: #fff; } - .oplock.theme-dark.unlocked { - --oplock-core-fg: #252525; - --oplock-stroke-ratio: .025; - } - .oplock.unknown { --oplock-core-fg: var(--hp-muted); } - .btn.lockact { - width: 100%; - justify-content: center; - display: flex; - align-items: center; - gap: var(--sp-3); - margin-top: var(--sp-4); - } - .btn.lockact.warn { - color: var(--error-color, #d33); - border-color: var(--error-color, #d33); - } - .oprow { - display: flex; - align-items: center; - gap: var(--sp-4); - padding: var(--sp-3) 0; - } - .oprow b { margin-left: auto; } - .oprow.ok b { color: #66d17a; } - .oprow.warn b { color: var(--hp-open); } - .bindharow { - display: flex; - align-items: center; - gap: var(--sp-5); - flex-wrap: wrap; - } - .bindharow .entcheck { opacity: 0.9; } - .ctrlchips { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin: var(--sp-2) 0; } - .ctrlchip { - display: inline-flex; align-items: center; gap: var(--sp-2); - background: var(--hp-accent); color: var(--text-primary-color, #fff); - border-radius: var(--rad-l); padding: var(--sp-2) var(--sp-4); font-size: var(--fs-s); - } - .ctrlchip ha-icon { --mdc-icon-size: 14px; cursor: pointer; } - .ctrllist { display: flex; flex-direction: column; gap: var(--sp-1); margin-top: var(--sp-2); } - .ctrlopt { - display: flex; align-items: center; gap: var(--sp-4); text-align: left; - border: 0; background: transparent; color: var(--hp-txt); - padding: var(--sp-3) var(--sp-4); border-radius: var(--rad-s); cursor: pointer; font-family: inherit; font-size: var(--fs-m); - } - :host([data-pointer-hover]) .ctrlopt:hover { - background: var(--secondary-background-color, rgba(128,128,128,0.15)); - } - .ctrlopt .sub { color: var(--hp-muted); font-size: var(--fs-s); margin-left: auto; } - .ctrlopt ha-icon { --mdc-icon-size: 16px; } - .ctrlstates { display: flex; flex-direction: column; gap: var(--sp-2); } - .ctrlstate { display: inline-flex; align-items: center; gap: var(--sp-3); color: var(--hp-muted); } - .ctrlstate.on { color: var(--hp-txt); } - .ctrlstate ha-icon { --mdc-icon-size: 15px; } - .cardpreview { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--sp-2); - margin: var(--sp-4) 0 var(--sp-1); - padding: 10px; - border: 1px dashed var(--hp-muted); - border-radius: var(--rad-m); - } - .cardpreview .cpname { font-weight: 700; letter-spacing: 0.04em; } - .cardpreview .cpmeta { - display: inline-flex; - align-items: center; - gap: 0.3em; - font-weight: 600; - opacity: 0.85; - } - .cardpreview .cpmeta ha-icon { --mdc-icon-size: 1.05em; } - .iconauto { - display: flex; - align-items: center; - gap: var(--sp-3); - font-size: var(--fs-s); - margin: var(--sp-2) 0 0; - } - .iconauto ha-icon { --mdc-icon-size: 18px; } - .iconauto span { flex: 1; } - .iconauto .btn { min-height: 32px; padding: 0 var(--sp-3); } - hp-dialog .dfill { - display: inline-flex; - align-items: center; - gap: var(--sp-3); - cursor: pointer; - } - .wallthick-dlg { - position: absolute; - z-index: 40; - min-width: 200px; - transform: translate(-50%, 8px); - padding: 10px 12px; - border-radius: 10px; - background: var(--card-background-color, #fff); - box-shadow: 0 8px 28px rgba(0,0,0,.22); - display: flex; - flex-direction: column; - gap: 8px; - pointer-events: auto; - } - .wallthick-dlg .row { - display: flex; - align-items: center; - gap: 8px; - } - .wallthick-dlg input[type="number"] { - width: 5.5em; - padding: 4px 6px; - border: 1px solid var(--divider-color, #ccc); - border-radius: 6px; - background: var(--input-fill, transparent); - color: var(--primary-text-color); - } - .areasel, - .namein { - background: var(--hp-bg); - border: 1px solid var(--hp-line); - color: var(--hp-txt); - border-radius: var(--rad-s); - padding: var(--sp-3) var(--sp-4); - font-size: var(--fs-m); - font-family: inherit; - } - .namein { - width: 130px; - } - .habindingbanner { - display: flex; - align-items: center; - gap: var(--sp-3); - padding: var(--sp-3) var(--sp-4); - margin-bottom: var(--sp-4); - border: 1px solid var(--warning-color, #ff9800); - border-radius: var(--rad-m); - background: color-mix(in srgb, var(--warning-color, #ff9800) 12%, transparent); - color: var(--primary-text-color, #f1f3f6); - } - .habindingbanner > span { flex: 1; min-width: 0; } - .habindingbanner > ha-icon { color: var(--warning-color, #ff9800); flex: 0 0 auto; } - .habindingbanner.limited { - border-color: var(--secondary-text-color, #9aa0aa); - background: color-mix(in srgb, var(--secondary-text-color, #9aa0aa) 10%, transparent); - } - .habindingbanner.limited > ha-icon { color: var(--secondary-text-color, #9aa0aa); } - .srcrow { - display: flex; - align-items: center; - gap: var(--sp-4); - font-size: var(--fs-m); - cursor: pointer; - padding: var(--sp-1) 0; - } - .dispsection { - margin-top: var(--sp-5) !important; - padding-top: var(--sp-4); - border-top: 1px solid var(--hp-line); - font-weight: 600; - color: var(--hp-txt) !important; - } - .colorrow { - display: flex; - align-items: center; - gap: var(--sp-4); - } - .gsrow > hp-color-opacity { - min-width: min(100%, 210px); - justify-content: space-between; - } - .ripple-colorrow > hp-color-opacity { - width: 100%; - justify-content: space-between; - } - .ripple-sizerow > .opl { - min-width: 0; - } - .colorrow input[type='range'] { flex: 1; } - .colorrow .tempin { width: 70px; flex: none; } - /* beat the generic hp-dialog .body .namein { width:100% } rule */ - hp-dialog .body .temprange .tempin { width: 56px; flex: none; padding: var(--sp-2) var(--sp-3); } - hp-dialog .body .colorrow .tempin { width: 72px; flex: none; } - .srcrow { flex-wrap: nowrap; } - /* native HA controls (rendered only when the HA frontend defines them; - old HA and the smoke env keep the plain inputs). ha-switch is taller - than a checkbox - cap its footprint so .srcrow keeps its rhythm. */ - .srcrow ha-switch { flex: none; } - .colorrow ha-slider { flex: 1; min-width: 0; } - .srcrow > span:first-of-type { white-space: nowrap; } - .colorrow .opl { color: var(--hp-muted); font-size: var(--fs-s); } - .colorrow .opv { font-size: var(--fs-s); min-width: 34px; text-align: right; } - .markerlightgroup { - min-width: 0; - margin: var(--sp-5) 0 0; - padding: var(--sp-4); - border: 1px solid var(--hp-line); - border-radius: var(--rad-m); - } - .markerlightgroup legend { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: var(--sp-1); - padding: 0 var(--sp-2); - color: var(--hp-txt); - font-weight: 600; - } - .markerlightgroup legend > span { min-width: 0; overflow-wrap: anywhere; } - .markerlightgroup[disabled] > :not(legend) { opacity: .62; } - .markerhelpfield { margin-top: var(--sp-4); } - .markerhelplabel { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: var(--sp-1); - margin-bottom: var(--sp-1); - } - .markerhelplabel > label { min-width: 0; overflow-wrap: anywhere; } - .markerradios { display: grid; gap: var(--sp-1); min-width: 0; } - .markerlightgroup .srcrow > span:first-of-type { - min-width: 0; - white-space: normal; - overflow-wrap: anywhere; - } - .markerglowvalue { margin: var(--sp-3) 0; flex-wrap: wrap; } - .markerglowvalue hp-color-opacity { flex: none; } - .markerlightdisabled { - display: flex; - align-items: center; - gap: var(--sp-2); - margin-top: var(--sp-2) !important; - } - .markerlightdisabled ha-icon { --mdc-icon-size: 18px; flex: none; } - .markerbadgetechnical { - min-width: 0; - margin: var(--sp-1) 0 var(--sp-2) !important; - overflow-wrap: anywhere; - } - .markerbadgetechnical code { white-space: normal; } - .planrow { - display: flex; - align-items: center; - gap: 10px; - } - /* the "already uploaded" picker: a plan is never deleted for being - unreferenced, so it has to be findable again */ - .savedplans { - display: flex; - flex-direction: column; - gap: var(--sp-3); - max-height: 240px; - overflow: auto; - margin: var(--sp-3) 0 var(--sp-1); - padding: var(--sp-3); - border: 1px solid var(--hp-line); - border-radius: var(--rad-m); - background: var(--hp-bg2, rgba(255, 255, 255, 0.03)); - /* The same collapse that ate .candlist (v1.53.1): a scroll box is a - flex item whose automatic minimum size is ZERO (overflow != visible), - so inside hp-dialog .body — a flex column taller than its 66vh cap — - it shrank to a 14px sliver: the rows were in the DOM, the owner saw - a thin rounded stripe under the "Already uploaded" button. Don't - shrink, and keep a floor even when the box is empty or loading. */ - flex: 0 0 auto; - min-height: 2.6em; - } - .savedplan { - display: flex; - align-items: center; - gap: 10px; - } - .savedplan.cur { outline: 1px solid var(--hp-accent); border-radius: var(--rad-s); } - .savedplan img { - width: 56px; - height: 40px; - object-fit: contain; - border: 1px solid var(--hp-line); - border-radius: var(--rad-s); - background: #fff; - flex: none; - } - .savedmeta { display: flex; flex-direction: column; min-width: 0; flex: 1; } - .savedmeta b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } - .savedmeta .muted { font-size: var(--fs-s); } - .savedplan .btn.danger ha-icon { color: #f25a4a; } - .savedplan .btn[disabled] { opacity: 0.4; pointer-events: none; } - .planprev { - max-width: 120px; - max-height: 70px; - border: 1px solid var(--hp-line); - border-radius: var(--rad-s); - background: #fff; - } - .planname { - font-size: var(--fs-m); - max-width: 150px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - .planname.muted { - color: var(--hp-muted); - } - .filebtn { - cursor: pointer; - } - .fileupload { display: inline-flex; min-width: 0; } - .fileupload > input { display: none; } - .btn.danger { - border-color: #b3402a; - color: #ff7a5c; - } - hp-dialog .row .spacer { - flex: 1; - } - hp-dialog .body { - max-height: 66vh; - overflow-y: auto; - } - .descin { - width: 100%; - box-sizing: border-box; - background: var(--hp-bg); - border: 1px solid var(--hp-line); - color: var(--hp-txt); - border-radius: var(--rad-s); - padding: var(--sp-3) var(--sp-4); - font-size: var(--fs-m); - font-family: inherit; - resize: vertical; - /* flex column of the dialog body squeezes textareas — keep a usable height */ - min-height: 92px; - flex-shrink: 0; - line-height: 1.35; - } - .bindsel { - display: flex; - flex-direction: column; - gap: var(--sp-3); - border: 1px solid var(--hp-line); - border-radius: var(--rad-m); - padding: var(--sp-4); - } - .bindsel .opt { - display: flex; - align-items: center; - gap: var(--sp-3); - border: 1px solid var(--hp-line); - background: transparent; - color: var(--hp-txt); - border-radius: var(--rad-s); - padding: var(--sp-3) var(--sp-4); - cursor: pointer; - font-size: var(--fs-m); - font-family: inherit; - } - .bindsel .opt.on { - background: var(--hp-accent); - color: var(--text-primary-color, #fff); - border-color: var(--hp-accent); - } - .curbind { - display: flex; - align-items: center; - gap: var(--sp-3); - font-size: var(--fs-m); - color: var(--hp-txt); - flex-wrap: wrap; - } - .curbind .ref { - color: var(--hp-muted); - font-size: var(--fs-s); - } - .vacbox .vacbtns { display: flex; gap: var(--sp-4); margin: var(--sp-3) 0; flex-wrap: wrap; } - .vacdiag { display: grid; gap: 4px; margin-bottom: var(--sp-3); } - .vacdiag > div { display: flex; justify-content: space-between; gap: var(--sp-5); } - .vacdiag > div > span { color: var(--secondary-text-color); } - .vacdiag > div > b { text-align: right; overflow-wrap: anywhere; } - .vacpicker { margin: var(--sp-3) 0; } - .vacsource-warning { display: grid; gap: 8px; } - .vacsource-warning .btn { justify-self: start; } - .vacpicker > summary { display: inline-flex; width: auto; list-style: none; cursor: pointer; } - .vacpicker > summary::-webkit-details-marker { display: none; } - .vacsource-list { display: grid; gap: 6px; margin-top: 8px; } - .vacsource-list details { padding: 6px 0 0; } - .vacsource-list details > summary { cursor: pointer; font-weight: 600; } - .vacsource { display: flex; align-items: center; justify-content: space-between; gap: 12px; - width: 100%; min-width: 0; padding: 9px 10px; border: 1px solid var(--divider-color); - border-radius: 10px; color: var(--primary-text-color); background: var(--secondary-background-color); - text-align: left; cursor: pointer; } - .vacsource.on { border-color: var(--accent-color); box-shadow: inset 3px 0 var(--accent-color); } - .vacsource > span:first-child { min-width: 0; display: grid; gap: 2px; } - .vacsource small { color: var(--secondary-text-color); overflow-wrap: anywhere; } - .vacsource-meta { color: var(--secondary-text-color); text-align: right; font-size: 0.82em; } - .vacxcme pre { margin: 8px 0 0; white-space: pre-wrap; user-select: text; } - .vacfit { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - z-index: 12; - overflow: visible; - touch-action: none; - cursor: grab; - /* the devlayer is pointer-events: none and every child opts back in — - without this line real clicks flew straight through the overlay - (owner: «уголки не кликабельны»; synthetic smoke events bypass - hit-testing, which is why they lied) */ - pointer-events: auto; - } - .vacfit:active { cursor: grabbing; } - .vacfit polygon { - fill: color-mix(in srgb, var(--hp-accent) 16%, transparent); - stroke: var(--hp-accent); - stroke-width: 2; - vector-effect: non-scaling-stroke; - stroke-dasharray: 6 4; - } - .vacfit text { - fill: var(--hp-accent); - font-size: 26px; - text-anchor: middle; - dominant-baseline: middle; - pointer-events: none; - user-select: none; - } - /* ---- the furniture palette (docs/FURNITURE.md §3) ------------------- */ - .furnpalette { - display: flex; - flex-direction: column; - max-height: 38vh; - border-top: 1px solid var(--hp-border, rgba(255, 255, 255, 0.12)); - background: var(--card-background-color, var(--hp-bg)); - font-size: 0.85em; - } - .furnhd { - display: flex; - align-items: center; - gap: 6px; - padding: 4px 8px; - font-weight: 600; - opacity: 0.9; - } - .furnhd .spacer { flex: 1; } - .furnbody { - overflow: auto; - padding: 0 8px 6px; - } - .furngroup { - margin: 6px 0 3px; - font-size: 0.85em; - text-transform: uppercase; - letter-spacing: 0.04em; - opacity: 0.6; - } - .furnrow { - display: flex; - flex-wrap: wrap; - gap: 4px; - } - .furnitem { - display: flex; - flex-direction: column; - align-items: center; - gap: 2px; - width: 74px; - padding: 4px 2px; - border: 1px solid transparent; - border-radius: 8px; - background: rgba(127, 127, 127, 0.08); - color: inherit; - font: inherit; - font-size: 0.8em; - line-height: 1.15; - text-align: center; - cursor: pointer; - } - :host([data-pointer-hover]) .furnitem:hover { background: rgba(127, 127, 127, 0.18); } - .furnitem.on { - border-color: var(--hp-accent); - background: rgba(38, 166, 154, 0.18); - } - .furnprev { - width: 40px; - height: 40px; - color: var(--primary-text-color, currentColor); - } - .furnsize { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 6px; - padding: 6px 8px; - border-top: 1px solid var(--hp-border, rgba(255, 255, 255, 0.12)); - } - .furnsize label { - display: flex; - align-items: baseline; - gap: 3px; - opacity: 0.8; - } - .furnsize .furnunit { opacity: 0.6; font-size: 0.85em; } - .furnsize input { - width: 5.5em; - padding: 3px 5px; - } - .furnhint { opacity: 0.6; } - .vacfitdot { fill: var(--hp-accent); pointer-events: none; } - /* hit target: invisible and finger-sized; .vacfitknob is the visible bead */ - .vacfithandle { - fill: transparent; - stroke: none; - cursor: nwse-resize; - } - .vacfitknob { - fill: var(--hp-bg); - stroke: var(--hp-accent); - stroke-width: 2; - vector-effect: non-scaling-stroke; - pointer-events: none; - } - .vaccalbar { - position: fixed; - left: 50%; - bottom: 24px; - transform: translateX(-50%); - display: flex; - gap: var(--sp-5); - align-items: center; - background: var(--hp-bg); - color: var(--hp-txt); - border: 1px solid var(--hp-accent); - border-radius: var(--rad-l); - padding: 10px var(--sp-5); - z-index: 60; - box-shadow: var(--shadow-2); - } - .candlist { - max-height: 160px; - overflow-y: auto; - border-top: 1px solid var(--hp-line); - /* A scrollable box is a flex item that HAPPILY collapses: inside the - dialog body (a flex column) this list rendered its rows into a 1px - sliver — the DOM had 26 candidates and the user saw nothing. In the - binding dropdown it sits inside .droppanel (block context) and never - showed the bug. Field report, 2026-07-30. */ - flex: 0 0 auto; - min-height: 2.6em; - } - .cand { - display: flex; - justify-content: space-between; - gap: var(--sp-4); - padding: var(--sp-3) var(--sp-4); - cursor: pointer; - border-radius: var(--rad-s); - font-size: var(--fs-m); - } - :host([data-pointer-hover]) .cand:hover { - background: rgba(127, 127, 127, 0.15); - } - .cand.sel { - background: var(--hp-accent); - color: var(--text-primary-color, #fff); - } - .cand .cs { - color: var(--hp-muted); - font-size: var(--fs-s); - white-space: nowrap; - } - .opening-entity-candidate { - width: 100%; - border: 0; - background: transparent; - color: inherit; - font: inherit; - text-align: left; - } - .opening-entity-candidate.sel { - background: var(--hp-accent); - color: var(--text-primary-color, #fff); - } - .opening-entity-empty { cursor: default; } - .cand.sel .cs { - color: var(--text-primary-color, #fff); - opacity: 0.85; - } - .cand.muted { - color: var(--hp-muted); - cursor: default; - } - .pdfedit { - display: flex; - flex-wrap: wrap; - gap: var(--sp-3); - align-items: center; - } - .pdftag { - display: inline-flex; - align-items: center; - gap: var(--sp-2); - border: 1px solid var(--hp-line); - border-radius: var(--rad-s); - padding: var(--sp-2) var(--sp-3); - font-size: var(--fs-s); - } - .pdftag a { - color: var(--hp-txt); - text-decoration: none; - max-width: 150px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - .pdftag .x { - --mdc-icon-size: 15px; - cursor: pointer; - color: var(--hp-muted); - } - :host([data-pointer-hover]) .pdftag .x:hover { - color: #ff7a5c; - } - .entlist { - display: flex; - flex-direction: column; - gap: var(--sp-2); - margin-bottom: 10px; - } - .entrow { - display: flex; - align-items: center; - gap: var(--sp-4); - padding: var(--sp-3) var(--sp-4); - border-radius: var(--rad-m); - background: var(--secondary-background-color, rgba(128, 128, 128, 0.12)); - } - .entrow ha-icon { --mdc-icon-size: 20px; color: var(--hp-muted); } - .entrow.on ha-icon { color: var(--hp-accent); } - .entrow .en { flex: 1; font-size: var(--fs-m); } - .entrow .ev { font-size: var(--fs-m); color: var(--hp-muted); } - .entbtn { - min-width: 74px; - min-height: 32px; - padding: var(--sp-2) var(--sp-5); - border: 1px solid var(--hp-muted); - border-radius: 999px; - background: transparent; - color: var(--hp-txt); - font: inherit; - font-size: var(--fs-m); - cursor: pointer; - } - .entbtn.on { - background: var(--hp-accent); - border-color: var(--hp-accent); - color: var(--text-primary-color, #fff); - font-weight: 600; - } - .inforow { - display: flex; - gap: 10px; - font-size: var(--fs-m); - margin: var(--sp-2) 0; - } - .inforow .k { - color: var(--hp-muted); - min-width: 84px; - } - .inforow a { - color: var(--hp-accent); - word-break: break-all; - } - .infodesc { - font-size: var(--fs-m); - white-space: pre-wrap; - margin-top: var(--sp-3); - } - .infodesc.muted { - color: var(--hp-muted); - } - .pdflist { - display: flex; - flex-direction: column; - gap: var(--sp-2); - } - .pdf { - display: inline-flex; - align-items: center; - gap: var(--sp-2); - color: var(--hp-accent); - text-decoration: none; - } - ha-icon-picker { - display: block; - } - .floorrow { - display: flex; - align-items: center; - gap: var(--sp-4); - padding: var(--sp-3) var(--sp-2); - font-size: var(--fs-m); - cursor: pointer; - } - .floorrow .floorlvl { - color: var(--hp-muted); - font-size: var(--fs-s); - border: 1px solid var(--hp-line); - border-radius: var(--rad-s); - padding: 0 var(--sp-3); - } - .rtest { - display: flex; - align-items: center; - gap: var(--sp-4); - margin-bottom: var(--sp-4); - } - .rtest .namein { flex: 1; } - .rtest ha-icon { color: var(--hp-accent); } - .rtesticon { font-size: var(--fs-s); color: var(--hp-muted); } - .rrow { - display: flex; - align-items: center; - gap: var(--sp-3); - margin: var(--sp-1) 0; - } - .rrow .rpat { flex: 2; } - .rrow .ricon { flex: 1.4; } - .rrow .rpat.bad { border-color: #ff7a5c; } - .rrow .rprev { --mdc-icon-size: 18px; color: var(--hp-txt); min-width: 18px; } - .rrow .ract { - --mdc-icon-size: 16px; - color: var(--hp-muted); - cursor: pointer; - } - :host([data-pointer-hover]) .rrow .ract:hover { color: var(--hp-txt); } - :host([data-pointer-hover]) .rrow .ract.del:hover { color: #ff7a5c; } - .gsrow .gsl { - min-width: 150px; - font-size: var(--fs-m); - color: var(--hp-muted); - } - .optimize-live { - display: grid; - justify-items: start; - gap: var(--sp-2); - margin-bottom: var(--sp-3); - } - .optimize-live .alignmsg, .optimize-live .rhint { margin-bottom: 0; } - .optimize-cleanup { min-height: 44px; } - .optimize-selected { color: var(--hp-txt); } - .optimize-details { - margin-top: var(--sp-3); - color: var(--hp-muted); - font-size: var(--fs-s); - overflow-wrap: anywhere; - } - .optimize-details > summary { - width: fit-content; - color: var(--hp-txt); - cursor: pointer; - font-weight: 600; - } - .optimize-details > summary:focus-visible { - outline: 2px solid var(--hp-accent); - outline-offset: 3px; - border-radius: var(--rad-s); - } - .optimize-details ul { margin: var(--sp-3) 0; padding-inline-start: 22px; } - .optimize-details li + li { margin-top: var(--sp-1); } - .btn.alignall { width: 100%; justify-content: center; } - .backupactions { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: var(--sp-3); - } - .backupactions .btn { justify-content: center; min-width: 0; } - .backupupload { display: inline-flex; min-width: 0; } - .backupupload > .btn { width: 100%; justify-content: center; } - .backupupload input { display: none; } - .backupbody { min-width: 0; } - .backupplanonly { margin-inline-start: var(--sp-4) !important; align-items: flex-start !important; } - .backupplanonly > span:first-of-type { display: grid; gap: 2px; white-space: normal; } - .backupplanonly small { color: var(--secondary-text-color); line-height: 1.35; } - .backupplanonlystatus { color: var(--hp-accent) !important; font-weight: 700; } - .backupfile, .backupsummary, .backupcontent { - display: flex; - flex-direction: column; - min-width: 0; - gap: var(--sp-1); - } - .backupfile b, .backupcontent span { - overflow-wrap: anywhere; - } - .backupfile span, .backupsummary span, .backupcontent span { - color: var(--hp-muted); - font-size: var(--fs-s); - } - .backupcounts { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: var(--sp-2) var(--sp-4); - font-size: var(--fs-s); - } - .backupwarn, .backuperror { - border-radius: var(--rad-s); - padding: var(--sp-3); - font-size: var(--fs-s); - line-height: 1.45; - overflow-wrap: anywhere; - } - .backupwarn { background: color-mix(in srgb, var(--hp-accent) 12%, transparent); } - .backuperror { background: rgba(179, 64, 42, .16); color: #ff7a5c; } - .backupdetails { - border: 1px solid var(--hp-line); - border-radius: var(--rad-s); - padding: var(--sp-2) var(--sp-3); - font-size: var(--fs-s); - } - .backupdetails summary { cursor: pointer; font-weight: 700; } - .backupdetails > div { display: grid; gap: var(--sp-1); padding-block-start: var(--sp-2); } - .backupdetails code { overflow-wrap: anywhere; color: var(--hp-muted); } - .backupchoices { - display: flex; - flex-direction: column; - gap: var(--sp-2); - border: 1px solid var(--hp-line); - border-radius: var(--rad-s); - min-width: 0; - } - .backupchoices label { margin: 0 !important; display: flex; gap: var(--sp-2); } - .backupconfirm { align-items: flex-start !important; } - .backupconfirm > span:first-of-type { - min-width: 0; - white-space: normal; - overflow-wrap: anywhere; - } - @media (max-width: 520px) { - .backupactions, .backupcounts { grid-template-columns: 1fr; } - } - .aboutver { - font-size: var(--fs-s); - color: var(--hp-muted); - margin: var(--sp-2) 0 var(--sp-3); - } - .aboutlink { - display: flex; - align-items: center; - gap: var(--sp-3); - width: fit-content; - color: var(--hp-accent); - text-decoration: none; - font-size: var(--fs-m); - padding: var(--sp-1) 0; - } - :host([data-pointer-hover]) .aboutlink:hover { text-decoration: underline; } - .aboutlink ha-icon { --mdc-icon-size: 18px; line-height: 1; } - hp-dialog .body { - padding: var(--sp-5) var(--sp-6); - display: flex; - flex-direction: column; - gap: var(--sp-3); - } - hp-dialog .tapconfirm-body { - min-width: 0; - overflow-x: hidden; - } - hp-dialog .tapconfirm-body p { - max-width: 100%; - min-width: 0; - margin: 0; - overflow-wrap: anywhere; - white-space: normal; - } - hp-dialog .tapconfirm-line { - color: var(--hp-muted); - } - hp-dialog .body label { - font-size: var(--fs-s); - color: var(--hp-muted); - margin-top: var(--sp-3); - } - hp-dialog .body .namein, - hp-dialog .body .areasel { - width: 100%; - box-sizing: border-box; - } - /* Room settings contains long radio labels, two source pickers and live - previews, so it deliberately uses hp-dialog's medium width. Keep its - content shrinkable as well: the generic srcrow rule is nowrap because - compact switch rows need it, but here that created a horizontal scroll - box even on a wide desktop viewport. */ - hp-dialog.roomdialog .body { - min-width: 0; - overflow-x: hidden; - } - hp-dialog.roomdialog .body > * { - min-width: 0; - max-width: 100%; - box-sizing: border-box; - } - hp-dialog.roomdialog .srcrow { - min-width: 0; - align-items: flex-start; - } - hp-dialog.roomdialog .srcrow > span:first-of-type { - min-width: 0; - white-space: normal; - overflow-wrap: anywhere; - line-height: 1.35; - } - hp-dialog.roomdialog .dropbtn > b, - hp-dialog.roomdialog .dropbtn > .ref { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - hp-dialog .row { - display: flex; - justify-content: flex-end; - gap: var(--sp-4); - width: 100%; - min-width: 0; - box-sizing: border-box; - padding: var(--sp-5) var(--sp-6); - border-top: 1px solid var(--hp-line); - } - /* Stable destructive/commit footer contract. A flex spacer cannot react - when translated labels no longer fit: justify-content then overflows - the destructive button through the left inset. Two real groups wrap - as units instead — destructive actions stay left, while Cancel/Save - move together to a right-aligned second row when necessary. */ - hp-dialog .row.dialog-action-footer { - align-items: center; - justify-content: flex-start; - flex-wrap: wrap; - row-gap: var(--sp-4); - } - hp-dialog .dialog-action-group { - display: flex; - flex: 0 1 auto; - flex-wrap: wrap; - align-items: center; - gap: var(--sp-4); - max-width: 100%; - min-width: 0; - } - hp-dialog .dialog-action-group .btn { - flex: 0 0 auto; - min-height: 44px; - } - hp-dialog .dialog-action-danger { - margin-right: auto; - } - hp-dialog .dialog-action-commit { - margin-left: auto; - justify-content: flex-end; - } - hp-dialog .row.markerfooter { - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - } - hp-dialog .row.roomfooter { - align-items: center; - flex-wrap: wrap; - } - /* Device info can have Edit + Open in HA + Close. It uses a wide dialog; - wrapping remains as a phone fallback, but without a flex spacer (which - used to strand Edit alone on the first line). */ - hp-dialog .row.infofooter { - align-items: center; - justify-content: flex-start; - flex-wrap: wrap; - gap: var(--sp-3); - } - hp-dialog .row.infofooter .btn { - flex-shrink: 0; - } - hp-dialog .row.infofooter .infofooter-close { - margin-left: auto; - } - @media (max-width: 480px) { - hp-dialog .row.infofooter { - padding: var(--sp-4) var(--sp-5); - } - } - .markeractions, - .markersaveactions { - display: flex; - align-items: center; - gap: var(--sp-4); - } - .markeractions:empty { display: none; } - .markersaveactions { margin-left: auto; } - .device-inbox { - display: flex; - flex-direction: column; - gap: var(--sp-4); - padding: var(--sp-5) var(--sp-6); - min-width: 0; - } - .device-inbox-dialog { --hp-dialog-wide-width: 920px; } - .device-inbox-head { - display: grid; - grid-template-columns: minmax(180px, 1fr) auto; - align-items: center; - gap: var(--sp-4); - } - .device-inbox-search { - width: 100%; - min-width: 0; - box-sizing: border-box; - border: 1px solid var(--hp-line); - border-radius: var(--rad-m); - background: transparent; - color: var(--hp-txt); - font: inherit; - padding: 11px 14px; - } - .device-inbox-tabs { - display: flex; - gap: var(--sp-2); - overflow-x: auto; - scrollbar-width: thin; - padding-bottom: var(--sp-1); - } - .device-inbox-tabs button { - flex: 0 0 auto; - border: 1px solid var(--hp-line); - border-radius: 999px; - background: transparent; - color: var(--hp-txt); - font: inherit; - padding: 8px 12px; - cursor: pointer; - } - .device-inbox-tabs button.on { - border-color: var(--hp-accent); - background: color-mix(in srgb, var(--hp-accent) 18%, transparent); - } - .device-inbox-tabs button span { color: var(--hp-muted); margin-inline-start: 4px; } - .device-inbox-filters { - display: flex; - flex-wrap: wrap; - gap: var(--sp-4) var(--sp-6); - color: var(--hp-muted); - } - .device-inbox-filters label { - display: inline-flex; - align-items: center; - gap: var(--sp-2); - cursor: pointer; - } - .device-inbox-results { display: grid; gap: var(--sp-3); min-width: 0; } - .device-inbox-row { - display: grid; - grid-template-columns: 42px minmax(180px, 1fr) minmax(180px, auto); - align-items: center; - gap: var(--sp-4); - min-width: 0; - border: 1px solid var(--hp-line); - border-radius: var(--rad-m); - padding: var(--sp-4); - background: color-mix(in srgb, var(--hp-txt) 3%, transparent); - } - .device-inbox-icon { --mdc-icon-size: 28px; color: var(--hp-txt); justify-self: center; } - .device-inbox-copy { min-width: 0; } - .device-inbox-name { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2); } - .device-inbox-new { - border-radius: 999px; - background: var(--hp-accent); - color: var(--text-primary-color, #fff); - font-size: var(--fs-s); - padding: 2px 7px; - } - .device-inbox-meta, - .device-inbox-reason, - .device-inbox-copy code { - display: block; - color: var(--hp-muted); - font-size: var(--fs-s); - overflow-wrap: anywhere; - white-space: normal; - } - .device-inbox-status { color: var(--error-color, #db4437); margin-inline-start: var(--sp-2); } - .device-inbox-actions { - display: flex; - justify-content: flex-end; - align-items: center; - flex-wrap: wrap; - gap: var(--sp-2); - } - .device-inbox-actions .btn { min-height: 36px; padding: 7px 10px; } - .device-inbox-menu { position: relative; } - .device-inbox-menu summary { list-style: none; cursor: pointer; } - .device-inbox-menu summary::-webkit-details-marker { display: none; } - .device-inbox-menu-items { - position: absolute; - z-index: 2; - inset-inline-end: 0; - top: calc(100% + var(--sp-1)); - display: grid; - gap: var(--sp-1); - min-width: 180px; - padding: var(--sp-2); - border: 1px solid var(--hp-line); - border-radius: var(--rad-m); - background: var(--hp-panel, var(--card-background-color, #fff)); - box-shadow: 0 8px 24px rgba(0, 0, 0, .22); - } - .device-inbox-menu-items .btn { justify-content: flex-start; width: 100%; } - .device-inbox-empty { color: var(--hp-muted); text-align: center; padding: var(--sp-8); } - .device-inbox-more { align-self: center; } - @media (max-width: 680px) { - .device-inbox { padding: var(--sp-4); } - .device-inbox-head { grid-template-columns: minmax(0, 1fr); } - .device-inbox-head .btn { justify-self: stretch; } - .device-inbox-tabs { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - overflow-x: visible; - } - .device-inbox-tabs button { - min-width: 0; - overflow-wrap: anywhere; - } - .device-inbox-row { grid-template-columns: 36px minmax(0, 1fr); } - .device-inbox-actions { grid-column: 1 / -1; justify-content: flex-start; } - } -`],Ye=new Set(["hacs","sun","backup","hassio","met","telegram_bot","mobile_app","systemmonitor","better_thermostat","adaptive_lighting","yandex_pogoda","upnp_serial_number"]),Je=[{pattern:"протечк|leak|water sensor",icon:"mdi:water-alert"},{pattern:"клапан|valve",icon:"mdi:pipe-valve"},{pattern:"дым|smoke",icon:"mdi:smoke-detector"},{pattern:"термоголов|trv|radiator",icon:"mdi:radiator"},{pattern:"чайник|kettle|термопот",icon:"mdi:kettle"},{pattern:"сауна|sauna|harvia|парная|парилк",icon:"mdi:hot-tub"},{pattern:"температ|temperature|thermometer|climate sensor",icon:"mdi:thermometer"},{pattern:"qingping|air monitor|молекул|air quality",icon:"mdi:air-filter"},{pattern:"штор|curtain|blind|shade",icon:"mdi:roller-shade"},{pattern:"розетк|plug|socket|outlet",icon:"mdi:power-socket-de"},{pattern:"выключат|switch",icon:"mdi:light-switch"},{pattern:"лампа|лампочк|bulb|gx53|светильник|rgb|lamp|light strip",icon:"mdi:lightbulb"},{pattern:"камер|camera",icon:"mdi:cctv"},{pattern:"замок|ttlock|lock|sn609|sn9161",icon:"mdi:lock"},{pattern:"ворота|garage|gate",icon:"mdi:garage-variant"},{pattern:"калитк|door|открыт|contact",icon:"mdi:door"},{pattern:"счётчик|счетчик|kws|meter",icon:"mdi:meter-electric"},{pattern:"вводный автомат|breaker|wifimcbn",icon:"mdi:electric-switch"},{pattern:"myheat|котёл|котел|boiler|отоплен|heating",icon:"mdi:water-boiler"},{pattern:"холодильник|fridge",icon:"mdi:fridge"},{pattern:"стиральн|washer|washing",icon:"mdi:washing-machine"},{pattern:"сушилк|dryer",icon:"mdi:tumble-dryer"},{pattern:"пылесос|vacuum|dreame|roborock",icon:"mdi:robot-vacuum"},{pattern:"soundbar",icon:"mdi:soundbar"},{pattern:"колонк|станц|speaker|яндекс|yandex|алиса|alice",icon:"mdi:speaker"},{pattern:"tv|телевизор|hyundaitv|mitv|television",icon:"mdi:television"},{pattern:"keenetic|роутер|router|mesh|access point",icon:"mdi:router-wireless"},{pattern:"ибп|ups|kirpich",icon:"mdi:battery-charging-high"},{pattern:"slzb|координат|zigbee|coordinator",icon:"mdi:zigbee"},{pattern:"motion|движен|presence|присутств",icon:"mdi:motion-sensor"},{pattern:"humidity|влажн",icon:"mdi:water-percent"}];function Xe(e){const t=[];for(const i of e)if(i&&"string"==typeof i.pattern&&i.icon)try{t.push({re:new RegExp(i.pattern,"i"),icon:i.icon})}catch{}return t}const Ze=Xe(Je),Qe={temperature:"mdi:thermometer",humidity:"mdi:water-percent",motion:"mdi:motion-sensor",occupancy:"mdi:motion-sensor",presence:"mdi:motion-sensor",door:"mdi:door",window:"mdi:window-closed",garage_door:"mdi:garage-variant",smoke:"mdi:smoke-detector",moisture:"mdi:water-alert",gas:"mdi:gas-cylinder",power:"mdi:meter-electric",energy:"mdi:meter-electric",illuminance:"mdi:brightness-5",co2:"mdi:molecule-co2",pm25:"mdi:air-filter",battery:"mdi:battery"},et="mdi:chip";function tt(e,t,i){const s=((e||"")+" "+(t||"")).toLowerCase();for(const{re:e,icon:t}of i??Ze)if(e.test(s))return t;return et}const it=["light","switch","cover","valve","lock","climate","fan","media_player","camera","vacuum","humidifier","water_heater","alarm_control_panel","sensor","binary_sensor","event","button","number","select","update"];var st=/^-?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i,nt=Math.ceil,ot=Math.floor,at="[BigNumber Error] ",rt=at+"Number primitive has more than 15 significant digits: ",lt=1e14,ct=14,ht=9007199254740991,dt=[1,10,100,1e3,1e4,1e5,1e6,1e7,1e8,1e9,1e10,1e11,1e12,1e13],pt=1e7,ut=1e9;function _t(e){var t=0|e;return e>0||e===t?t:t-1}function mt(e){for(var t,i,s=1,n=e.length,o=e[0]+"";sc^i?1:-1;for(r=(l=n.length)<(c=o.length)?l:c,a=0;ao[a]^i?1:-1;return l==c?0:l>c^i?1:-1}function ft(e,t,i,s){if(ei||e!==ot(e))throw Error(at+(s||"Argument")+("number"==typeof e?ei?" out of range: ":" not an integer: ":" not a primitive number: ")+String(e))}function vt(e){var t=e.c.length-1;return _t(e.e/ct)==t&&e.c[t]%2!=0}function bt(e,t){return(e.length>1?e.charAt(0)+"."+e.slice(1):e)+(t<0?"e":"e+")+t}function yt(e,t,i){var s,n;if(t<0){for(n=i+".";++t;n+=i);e=n+e}else if(++t>(s=e.length)){for(n=i,t-=s;--t;n+=i);e+=n}else tb?p.c=p.e=null:e.e=10;l/=10,r++);return void(r>b?p.c=p.e=null:(p.e=r,p.c=[e]))}d=String(e)}else{if(!st.test(d=String(e)))return n(p,d,c);p.s=45==d.charCodeAt(0)?(d=d.slice(1),-1):1}(r=d.indexOf("."))>-1&&(d=d.replace(".","")),(l=d.search(/e/i))>0?(r<0&&(r=l),r+=+d.slice(l+1),d=d.substring(0,l)):r<0&&(r=d.length)}else{if(ft(t,2,$.length,"Base"),10==t&&S)return T(p=new M(e),_+p.e+1,m);if(d=String(e),c="number"==typeof e){if(0*e!=0)return n(p,d,c,t);if(p.s=1/e<0?(d=d.slice(1),-1):1,M.DEBUG&&d.replace(/^0\.0*|\./,"").length>15)throw Error(rt+e)}else p.s=45===d.charCodeAt(0)?(d=d.slice(1),-1):1;for(i=$.slice(0,t),r=l=0,h=d.length;lr){r=h;continue}}else if(!a&&(d==d.toUpperCase()&&(d=d.toLowerCase())||d==d.toLowerCase()&&(d=d.toUpperCase()))){a=!0,l=-1,r=0;continue}return n(p,String(e),c,t)}c=!1,(r=(d=s(d,t,10,p.s)).indexOf("."))>-1?d=d.replace(".",""):r=d.length}for(l=0;48===d.charCodeAt(l);l++);for(h=d.length;48===d.charCodeAt(--h););if(d=d.slice(l,++h)){if(h-=l,c&&M.DEBUG&&h>15&&(e>ht||e!==ot(e)))throw Error(rt+p.s*e);if((r=r-l-1)>b)p.c=p.e=null;else if(r=f)?bt(l,a):yt(l,a,"0");else if(o=(e=T(new M(e),t,i)).e,r=(l=mt(e.c)).length,1==s||2==s&&(t<=o||o<=g)){for(;ra),l=yt(l,o,"0"),o+1>r){if(--t>0)for(l+=".";t--;l+="0");}else if((t+=o-r)>0)for(o+1==r&&(l+=".");t--;l+="0");return e.s<0&&n?"-"+l:l}function D(e,t){for(var i,s,n=1,o=new M(e[0]);n=10;n/=10,s++);return(i=s+i*ct-1)>b?e.c=e.e=null:i=10;r/=10,n++);if((o=t-n)<0)o+=ct,a=t,l=d[c=0],h=ot(l/p[n-a-1]%10);else if((c=nt((o+1)/ct))>=d.length){if(!s)break e;for(;d.length<=c;d.push(0));l=h=0,n=1,a=(o%=ct)-ct+1}else{for(l=r=d[c],n=1;r>=10;r/=10,n++);h=(a=(o%=ct)-ct+n)<0?0:ot(l/p[n-a-1]%10)}if(s=s||t<0||null!=d[c+1]||(a<0?l:l%p[n-a-1]),s=i<4?(h||s)&&(0==i||i==(e.s<0?3:2)):h>5||5==h&&(4==i||s||6==i&&(o>0?a>0?l/p[n-a]:0:d[c-1])%10&1||i==(e.s<0?8:7)),t<1||!d[0])return d.length=0,s?(t-=e.e+1,d[0]=p[(ct-t%ct)%ct],e.e=-t||0):d[0]=e.e=0,e;if(0==o?(d.length=c,r=1,c--):(d.length=c+1,r=p[ct-o],d[c]=a>0?ot(l/p[n-a]%p[a])*r:0),s)for(;;){if(0==c){for(o=1,a=d[0];a>=10;a/=10,o++);for(a=d[0]+=r,r=1;a>=10;a/=10,r++);o!=r&&(e.e++,d[0]==lt&&(d[0]=1));break}if(d[c]+=r,d[c]!=lt)break;d[c--]=0,r=1}for(o=d.length;0===d[--o];d.pop());}e.e>b?e.c=e.e=null:e.e=f?bt(t,i):yt(t,i,"0"),e.s<0?"-"+t:t)}return M.clone=e,M.ROUND_UP=0,M.ROUND_DOWN=1,M.ROUND_CEIL=2,M.ROUND_FLOOR=3,M.ROUND_HALF_UP=4,M.ROUND_HALF_DOWN=5,M.ROUND_HALF_EVEN=6,M.ROUND_HALF_CEIL=7,M.ROUND_HALF_FLOOR=8,M.EUCLID=9,M.config=M.set=function(e){var t,i;if(null!=e){if("object"!=typeof e)throw Error(at+"Object expected: "+e);if(e.hasOwnProperty(t="DECIMAL_PLACES")&&(ft(i=e[t],0,ut,t),_=i),e.hasOwnProperty(t="ROUNDING_MODE")&&(ft(i=e[t],0,8,t),m=i),e.hasOwnProperty(t="EXPONENTIAL_AT")&&((i=e[t])&&i.pop?(ft(i[0],-ut,0,t),ft(i[1],0,ut,t),g=i[0],f=i[1]):(ft(i,-ut,ut,t),g=-(f=i<0?-i:i))),e.hasOwnProperty(t="RANGE"))if((i=e[t])&&i.pop)ft(i[0],-ut,-1,t),ft(i[1],1,ut,t),v=i[0],b=i[1];else{if(ft(i,-ut,ut,t),!i)throw Error(at+t+" cannot be zero: "+i);v=-(b=i<0?-i:i)}if(e.hasOwnProperty(t="CRYPTO")){if((i=e[t])!==!!i)throw Error(at+t+" not true or false: "+i);if(i){if("undefined"==typeof crypto||!crypto||!crypto.getRandomValues&&!crypto.randomBytes)throw y=!i,Error(at+"crypto unavailable");y=i}else y=i}if(e.hasOwnProperty(t="MODULO_MODE")&&(ft(i=e[t],0,9,t),w=i),e.hasOwnProperty(t="POW_PRECISION")&&(ft(i=e[t],0,ut,t),k=i),e.hasOwnProperty(t="FORMAT")){if("object"!=typeof(i=e[t]))throw Error(at+t+" not an object: "+i);x=i}if(e.hasOwnProperty(t="ALPHABET")){if("string"!=typeof(i=e[t])||/^.?$|[+\-.\s]|(.).*\1/.test(i))throw Error(at+t+" invalid: "+i);S="0123456789"==i.slice(0,10),$=i}}return{DECIMAL_PLACES:_,ROUNDING_MODE:m,EXPONENTIAL_AT:[g,f],RANGE:[v,b],CRYPTO:y,MODULO_MODE:w,POW_PRECISION:k,FORMAT:x,ALPHABET:$}},M.isBigNumber=function(e){if(!e||!0!==e._isBigNumber)return!1;if(!M.DEBUG)return!0;var t,i,s=e.c,n=e.e,o=e.s;e:if("[object Array]"=={}.toString.call(s)){if((1===o||-1===o)&&n>=-ut&&n<=ut&&n===ot(n)){if(0===s[0]){if(0===n&&1===s.length)return!0;break e}if((t=(n+1)%ct)<1&&(t+=ct),String(s[0]).length==t){for(t=0;t=lt||i!==ot(i))break e;if(0!==i)return!0}}}else if(null===s&&null===n&&(null===o||1===o||-1===o))return!0;throw Error(at+"Invalid BigNumber: "+e)},M.maximum=M.max=function(){return D(arguments,-1)},M.minimum=M.min=function(){return D(arguments,1)},M.random=(o=9007199254740992,a=Math.random()*o&2097151?function(){return ot(Math.random()*o)}:function(){return 8388608*(1073741824*Math.random()|0)+(8388608*Math.random()|0)},function(e){var t,i,s,n,o,r=0,l=[],c=new M(u);if(null==e?e=_:ft(e,0,ut),n=nt(e/ct),y)if(crypto.getRandomValues){for(t=crypto.getRandomValues(new Uint32Array(n*=2));r>>11))>=9e15?(i=crypto.getRandomValues(new Uint32Array(2)),t[r]=i[0],t[r+1]=i[1]):(l.push(o%1e14),r+=2);r=n/2}else{if(!crypto.randomBytes)throw y=!1,Error(at+"crypto unavailable");for(t=crypto.randomBytes(n*=7);r=9e15?crypto.randomBytes(7).copy(t,r):(l.push(o%1e14),r+=7);r=n/7}if(!y)for(;r=10;o/=10,r++);ri-1&&(null==a[n+1]&&(a[n+1]=0),a[n+1]+=a[n]/i|0,a[n]%=i)}return a.reverse()}return function(s,n,o,a,r){var l,c,h,d,p,u,g,f,v=s.indexOf("."),b=_,y=m;for(v>=0&&(d=k,k=0,s=s.replace(".",""),u=(f=new M(n)).pow(s.length-v),k=d,f.c=t(yt(mt(u.c),u.e,"0"),10,o,e),f.e=f.c.length),h=d=(g=t(s,n,o,r?(l=$,e):(l=e,$))).length;0==g[--d];g.pop());if(!g[0])return l.charAt(0);if(v<0?--h:(u.c=g,u.e=h,u.s=a,g=(u=i(u,f,b,y,o)).c,p=u.r,h=u.e),v=g[c=h+b+1],d=o/2,p=p||c<0||null!=g[c+1],p=y<4?(null!=v||p)&&(0==y||y==(u.s<0?3:2)):v>d||v==d&&(4==y||p||6==y&&1&g[c-1]||y==(u.s<0?8:7)),c<1||!g[0])s=p?yt(l.charAt(1),-b,l.charAt(0)):l.charAt(0);else{if(g.length=c,p)for(--o;++g[--c]>o;)g[c]=0,c||(++h,g=[1].concat(g));for(d=g.length;!g[--d];);for(v=0,s="";v<=d;s+=l.charAt(g[v++]));s=yt(s,h,l.charAt(0))}return s}}(),i=function(){function e(e,t,i){var s,n,o,a,r=0,l=e.length,c=t%pt,h=t/pt|0;for(e=e.slice();l--;)r=((n=c*(o=e[l]%pt)+(s=h*o+(a=e[l]/pt|0)*c)%pt*pt+r)/i|0)+(s/pt|0)+h*a,e[l]=n%i;return r&&(e=[r].concat(e)),e}function t(e,t,i,s){var n,o;if(i!=s)o=i>s?1:-1;else for(n=o=0;nt[n]?1:-1;break}return o}function i(e,t,i,s){for(var n=0;i--;)e[i]-=n,n=e[i]1;e.splice(0,1));}return function(s,n,o,a,r){var l,c,h,d,p,u,_,m,g,f,v,b,y,w,k,x,$,S=s.s==n.s?1:-1,C=s.c,D=n.c;if(!(C&&C[0]&&D&&D[0]))return new M(s.s&&n.s&&(C?!D||C[0]!=D[0]:D)?C&&0==C[0]||!D?0*S:S/0:NaN);for(g=(m=new M(S)).c=[],S=o+(c=s.e-n.e)+1,r||(r=lt,c=_t(s.e/ct)-_t(n.e/ct),S=S/ct|0),h=0;D[h]==(C[h]||0);h++);if(D[h]>(C[h]||0)&&c--,S<0)g.push(1),d=!0;else{for(w=C.length,x=D.length,h=0,S+=2,(p=ot(r/(D[0]+1)))>1&&(D=e(D,p,r),C=e(C,p,r),x=D.length,w=C.length),y=x,v=(f=C.slice(0,x)).length;v=r/2&&k++;do{if(p=0,(l=t(D,f,x,v))<0){if(b=f[0],x!=v&&(b=b*r+(f[1]||0)),(p=ot(b/k))>1)for(p>=r&&(p=r-1),_=(u=e(D,p,r)).length,v=f.length;1==t(u,f,_,v);)p--,i(u,x<_?$:D,_,r),_=u.length,l=1;else 0==p&&(l=p=1),_=(u=D.slice()).length;if(_=10;S/=10,h++);T(m,o+(m.e=h+c*ct-1)+1,a,d)}else m.e=c,m.r=+d;return m}}(),r=/^(-?)0([xbo])(?=\w[\w.]*$)/i,l=/^([^.]+)\.$/,c=/^\.([^.]+)$/,h=/^-?(Infinity|NaN)$/,d=/^\s*\+(?=[\w.])|^\s+|\s+$/g,n=function(e,t,i,s){var n,o=i?t:t.replace(d,"");if(h.test(o))e.s=isNaN(o)?null:o<0?-1:1;else{if(!i&&(o=o.replace(r,function(e,t,i){return n="x"==(i=i.toLowerCase())?16:"b"==i?2:8,s&&s!=n?e:t}),s&&(n=s,o=o.replace(l,"$1").replace(c,"0.$1")),t!=o))return new M(o,n);if(M.DEBUG)throw Error(at+"Not a"+(s?" base "+s:"")+" number: "+t);e.s=null}e.c=e.e=null},p.absoluteValue=p.abs=function(){var e=new M(this);return e.s<0&&(e.s=1),e},p.comparedTo=function(e,t){return gt(this,new M(e,t))},p.decimalPlaces=p.dp=function(e,t){var i,s,n,o=this;if(null!=e)return ft(e,0,ut),null==t?t=m:ft(t,0,8),T(new M(o),e+o.e+1,t);if(!(i=o.c))return null;if(s=((n=i.length-1)-_t(this.e/ct))*ct,n=i[n])for(;n%10==0;n/=10,s--);return s<0&&(s=0),s},p.dividedBy=p.div=function(e,t){return i(this,new M(e,t),_,m)},p.dividedToIntegerBy=p.idiv=function(e,t){return i(this,new M(e,t),0,1)},p.exponentiatedBy=p.pow=function(e,t){var i,s,n,o,a,r,l,c,h=this;if((e=new M(e)).c&&!e.isInteger())throw Error(at+"Exponent not an integer: "+I(e));if(null!=t&&(t=new M(t)),a=e.e>14,!h.c||!h.c[0]||1==h.c[0]&&!h.e&&1==h.c.length||!e.c||!e.c[0])return c=new M(Math.pow(+I(h),a?e.s*(2-vt(e)):+I(e))),t?c.mod(t):c;if(r=e.s<0,t){if(t.c?!t.c[0]:!t.s)return new M(NaN);(s=!r&&h.isInteger()&&t.isInteger())&&(h=h.mod(t))}else{if(e.e>9&&(h.e>0||h.e<-1||(0==h.e?h.c[0]>1||a&&h.c[1]>=24e7:h.c[0]<8e13||a&&h.c[0]<=9999975e7)))return o=h.s<0&&vt(e)?-0:0,h.e>-1&&(o=1/o),new M(r?1/o:o);k&&(o=nt(k/ct+2))}for(a?(i=new M(.5),r&&(e.s=1),l=vt(e)):l=(n=Math.abs(+I(e)))%2,c=new M(u);;){if(l){if(!(c=c.times(h)).c)break;o?c.c.length>o&&(c.c.length=o):s&&(c=c.mod(t))}if(n){if(0===(n=ot(n/2)))break;l=n%2}else if(T(e=e.times(i),e.e+1,1),e.e>14)l=vt(e);else{if(0===(n=+I(e)))break;l=n%2}h=h.times(h),o?h.c&&h.c.length>o&&(h.c.length=o):s&&(h=h.mod(t))}return s?c:(r&&(c=u.div(c)),t?c.mod(t):o?T(c,k,m,void 0):c)},p.integerValue=function(e){var t=new M(this);return null==e?e=m:ft(e,0,8),T(t,t.e+1,e)},p.isEqualTo=p.eq=function(e,t){return 0===gt(this,new M(e,t))},p.isFinite=function(){return!!this.c},p.isGreaterThan=p.gt=function(e,t){return gt(this,new M(e,t))>0},p.isGreaterThanOrEqualTo=p.gte=function(e,t){return 1===(t=gt(this,new M(e,t)))||0===t},p.isInteger=function(){return!!this.c&&_t(this.e/ct)>this.c.length-2},p.isLessThan=p.lt=function(e,t){return gt(this,new M(e,t))<0},p.isLessThanOrEqualTo=p.lte=function(e,t){return-1===(t=gt(this,new M(e,t)))||0===t},p.isNaN=function(){return!this.s},p.isNegative=function(){return this.s<0},p.isPositive=function(){return this.s>0},p.isZero=function(){return!!this.c&&0==this.c[0]},p.minus=function(e,t){var i,s,n,o,a=this,r=a.s;if(t=(e=new M(e,t)).s,!r||!t)return new M(NaN);if(r!=t)return e.s=-t,a.plus(e);var l=a.e/ct,c=e.e/ct,h=a.c,d=e.c;if(!l||!c){if(!h||!d)return h?(e.s=-t,e):new M(d?a:NaN);if(!h[0]||!d[0])return d[0]?(e.s=-t,e):new M(h[0]?a:3==m?-0:0)}if(l=_t(l),c=_t(c),h=h.slice(),r=l-c){for((o=r<0)?(r=-r,n=h):(c=l,n=d),n.reverse(),t=r;t--;n.push(0));n.reverse()}else for(s=(o=(r=h.length)<(t=d.length))?r:t,r=t=0;t0)for(;t--;h[i++]=0);for(t=lt-1;s>r;){if(h[--s]=0;){for(i=0,p=b[n]%g,u=b[n]/g|0,o=n+(a=l);o>n;)i=((c=p*(c=v[--a]%g)+(r=u*c+(h=v[a]/g|0)*p)%g*g+_[o]+i)/m|0)+(r/g|0)+u*h,_[o--]=c%m;_[o]=i}return i?++s:_.splice(0,1),P(e,_,s)},p.negated=function(){var e=new M(this);return e.s=-e.s||null,e},p.plus=function(e,t){var i,s=this,n=s.s;if(t=(e=new M(e,t)).s,!n||!t)return new M(NaN);if(n!=t)return e.s=-t,s.minus(e);var o=s.e/ct,a=e.e/ct,r=s.c,l=e.c;if(!o||!a){if(!r||!l)return new M(n/0);if(!r[0]||!l[0])return l[0]?e:new M(r[0]?s:0*n)}if(o=_t(o),a=_t(a),r=r.slice(),n=o-a){for(n>0?(a=o,i=l):(n=-n,i=r),i.reverse();n--;i.push(0));i.reverse()}for((n=r.length)-(t=l.length)<0&&(i=l,l=r,r=i,t=n),n=0;t;)n=(r[--t]=r[t]+l[t]+n)/lt|0,r[t]=lt===r[t]?0:r[t]%lt;return n&&(r=[n].concat(r),++a),P(e,r,a)},p.precision=p.sd=function(e,t){var i,s,n,o=this;if(null!=e&&e!==!!e)return ft(e,1,ut),null==t?t=m:ft(t,0,8),T(new M(o),e,t);if(!(i=o.c))return null;if(s=(n=i.length-1)*ct+1,n=i[n]){for(;n%10==0;n/=10,s--);for(n=i[0];n>=10;n/=10,s++);}return e&&o.e+1>s&&(s=o.e+1),s},p.shiftedBy=function(e){return ft(e,-9007199254740991,ht),this.times("1e"+e)},p.squareRoot=p.sqrt=function(){var e,t,s,n,o,a=this,r=a.c,l=a.s,c=a.e,h=_+4,d=new M("0.5");if(1!==l||!r||!r[0])return new M(!l||l<0&&(!r||r[0])?NaN:r?a:1/0);if(0==(l=Math.sqrt(+I(a)))||l==1/0?(((t=mt(r)).length+c)%2==0&&(t+="0"),l=Math.sqrt(+t),c=_t((c+1)/2)-(c<0||c%2),s=new M(t=l==1/0?"5e"+c:(t=l.toExponential()).slice(0,t.indexOf("e")+1)+c)):s=new M(l+""),s.c[0])for((l=(c=s.e)+h)<3&&(l=0);;)if(o=s,s=d.times(o.plus(i(a,o,h,1))),mt(o.c).slice(0,l)===(t=mt(s.c)).slice(0,l)){if(s.e0&&_>0){for(o=_%r||r,h=u.substr(0,o);o<_;o+=r)h+=c+u.substr(o,r);l>0&&(h+=c+u.slice(o)),p&&(h="-"+h)}s=d?h+(i.decimalSeparator||"")+((l=+i.fractionGroupSize)?d.replace(new RegExp("\\d{"+l+"}\\B","g"),"$&"+(i.fractionGroupSeparator||"")):d):h}return(i.prefix||"")+s+(i.suffix||"")},p.toFraction=function(e){var t,s,n,o,a,r,l,c,h,d,p,_,g=this,f=g.c;if(null!=e&&(!(l=new M(e)).isInteger()&&(l.c||1!==l.s)||l.lt(u)))throw Error(at+"Argument "+(l.isInteger()?"out of range: ":"not an integer: ")+I(l));if(!f)return new M(g);for(t=new M(u),h=s=new M(u),n=c=new M(u),_=mt(f),a=t.e=_.length-g.e-1,t.c[0]=dt[(r=a%ct)<0?ct+r:r],e=!e||l.comparedTo(t)>0?a>0?t:h:l,r=b,b=1/0,l=new M(_),c.c[0]=0;d=i(l,t,0,1),1!=(o=s.plus(d.times(n))).comparedTo(e);)s=n,n=o,h=c.plus(d.times(o=h)),c=o,t=l.minus(d.times(o=t)),l=o;return o=i(e.minus(s),n,0,1),c=c.plus(o.times(h)),s=s.plus(o.times(n)),c.s=h.s=g.s,p=i(h,n,a*=2,m).minus(g).abs().comparedTo(i(c,s,a,m).minus(g).abs())<1?[h,n]:[c,s],b=r,p},p.toNumber=function(){return+I(this)},p.toPrecision=function(e,t){return null!=e&&ft(e,1,ut),C(this,e,t,2)},p.toString=function(e){var t,i=this,n=i.s,o=i.e;return null===o?n?(t="Infinity",n<0&&(t="-"+t)):t="NaN":(null==e?t=o<=g||o>=f?bt(mt(i.c),o):yt(mt(i.c),o,"0"):10===e&&S?t=yt(mt((i=T(new M(i),_+o+1,m)).c),i.e,"0"):(ft(e,2,$.length,"Base"),t=s(yt(mt(i.c),o,"0"),10,e,n,!0)),n<0&&i.c[0]&&(t="-"+t)),t},p.valueOf=p.toJSON=function(){return I(this)},p._isBigNumber=!0,p[Symbol.toStringTag]="BigNumber",p[Symbol.for("nodejs.util.inspect.custom")]=p.valueOf,null!=t&&M.set(t),M}(),kt=class{key;left=null;right=null;constructor(e){this.key=e}},xt=class extends kt{constructor(e){super(e)}},$t=class{size=0;modificationCount=0;splayCount=0;splay(e){const t=this.root;if(null==t)return this.compare(e,e),-1;let i=null,s=null,n=null,o=null,a=t;const r=this.compare;let l;for(;;)if(l=r(a.key,e),l>0){let t=a.left;if(null==t)break;if(l=r(t.key,e),l>0&&(a.left=t.right,t.right=a,a=t,t=a.left,null==t))break;null==i?s=a:i.left=a,i=a,a=t}else{if(!(l<0))break;{let t=a.right;if(null==t)break;if(l=r(t.key,e),l<0&&(a.right=t.left,t.left=a,a=t,t=a.right,null==t))break;null==n?o=a:n.right=a,n=a,a=t}}return null!=n&&(n.right=a.left,a.left=o),null!=i&&(i.left=a.right,a.right=s),this.root!==a&&(this.root=a,this.splayCount++),l}splayMin(e){let t=e,i=t.left;for(;null!=i;){const e=i;t.left=e.right,e.right=t,t=e,i=t.left}return t}splayMax(e){let t=e,i=t.right;for(;null!=i;){const e=i;t.right=e.left,e.left=t,t=e,i=t.right}return t}_delete(e){if(null==this.root)return null;if(0!=this.splay(e))return null;let t=this.root;const i=t,s=t.left;if(this.size--,null==s)this.root=t.right;else{const e=t.right;t=this.splayMax(s),t.right=e,this.root=t}return this.modificationCount++,i}addNewRoot(e,t){this.size++,this.modificationCount++;const i=this.root;null!=i?(t<0?(e.left=i,e.right=i.right,i.right=null):(e.right=i,e.left=i.left,i.left=null),this.root=e):this.root=e}_first(){const e=this.root;return null==e?null:(this.root=this.splayMin(e),this.root)}_last(){const e=this.root;return null==e?null:(this.root=this.splayMax(e),this.root)}clear(){this.root=null,this.size=0,this.modificationCount++}has(e){return this.validKey(e)&&0==this.splay(e)}defaultCompare(){return(e,t)=>et?1:0}wrap(){return{getRoot:()=>this.root,setRoot:e=>{this.root=e},getSize:()=>this.size,getModificationCount:()=>this.modificationCount,getSplayCount:()=>this.splayCount,setSplayCount:e=>{this.splayCount=e},splay:e=>this.splay(e),has:e=>this.has(e)}}},St=class e extends $t{root=null;compare;validKey;constructor(e,t){super(),this.compare=e??this.defaultCompare(),this.validKey=t??(e=>null!=e&&null!=e)}delete(e){return!!this.validKey(e)&&null!=this._delete(e)}deleteAll(e){for(const t of e)this.delete(t)}forEach(e){const t=this[Symbol.iterator]();let i;for(;i=t.next(),!i.done;)e(i.value,i.value,this)}add(e){const t=this.splay(e);return 0!=t&&this.addNewRoot(new xt(e),t),this}addAndReturn(e){const t=this.splay(e);return 0!=t&&this.addNewRoot(new xt(e),t),this.root.key}addAll(e){for(const t of e)this.add(t)}isEmpty(){return null==this.root}isNotEmpty(){return null!=this.root}single(){if(0==this.size)throw"Bad state: No element";if(this.size>1)throw"Bad state: Too many element";return this.root.key}first(){if(0==this.size)throw"Bad state: No element";return this._first().key}last(){if(0==this.size)throw"Bad state: No element";return this._last().key}lastBefore(e){if(null==e)throw"Invalid arguments(s)";if(null==this.root)return null;if(this.splay(e)<0)return this.root.key;let t=this.root.left;if(null==t)return null;let i=t.right;for(;null!=i;)t=i,i=t.right;return t.key}firstAfter(e){if(null==e)throw"Invalid arguments(s)";if(null==this.root)return null;if(this.splay(e)>0)return this.root.key;let t=this.root.right;if(null==t)return null;let i=t.left;for(;null!=i;)t=i,i=t.left;return t.key}retainAll(t){const i=new e(this.compare,this.validKey),s=this.modificationCount;for(const e of t){if(s!=this.modificationCount)throw"Concurrent modification during iteration.";this.validKey(e)&&0==this.splay(e)&&i.add(this.root.key)}i.size!=this.size&&(this.root=i.root,this.size=i.size,this.modificationCount++)}lookup(e){if(!this.validKey(e))return null;return 0!=this.splay(e)?null:this.root.key}intersection(t){const i=new e(this.compare,this.validKey);for(const e of this)t.has(e)&&i.add(e);return i}difference(t){const i=new e(this.compare,this.validKey);for(const e of this)t.has(e)||i.add(e);return i}union(e){const t=this.clone();return t.addAll(e),t}clone(){const t=new e(this.compare,this.validKey);return t.size=this.size,t.root=this.copyNode(this.root),t}copyNode(e){if(null==e)return null;const t=new xt(e.key);return function e(t,i){let s,n;do{if(s=t.left,n=t.right,null!=s){const t=new xt(s.key);i.left=t,e(s,t)}if(null!=n){const e=new xt(n.key);i.right=e,t=n,i=e}}while(null!=n)}(e,t),t}toSet(){return this.clone()}entries(){return new Dt(this.wrap())}keys(){return this[Symbol.iterator]()}values(){return this[Symbol.iterator]()}[Symbol.iterator](){return new Ct(this.wrap())}[Symbol.toStringTag]="[object Set]"},Mt=class{tree;path=new Array;modificationCount=null;splayCount;constructor(e){this.tree=e,this.splayCount=e.getSplayCount()}[Symbol.iterator](){return this}next(){return this.moveNext()?{done:!1,value:this.current()}:{done:!0,value:null}}current(){if(!this.path.length)return null;const e=this.path[this.path.length-1];return this.getValue(e)}rebuildPath(e){this.path.splice(0,this.path.length),this.tree.splay(e),this.path.push(this.tree.getRoot()),this.splayCount=this.tree.getSplayCount()}findLeftMostDescendent(e){for(;null!=e;)this.path.push(e),e=e.left}moveNext(){if(this.modificationCount!=this.tree.getModificationCount()){if(null==this.modificationCount){this.modificationCount=this.tree.getModificationCount();let e=this.tree.getRoot();for(;null!=e;)this.path.push(e),e=e.left;return this.path.length>0}throw"Concurrent modification during iteration."}if(!this.path.length)return!1;this.splayCount!=this.tree.getSplayCount()&&this.rebuildPath(this.path[this.path.length-1].key);let e=this.path[this.path.length-1],t=e.right;if(null!=t){for(;null!=t;)this.path.push(t),t=t.left;return!0}for(this.path.pop();this.path.length&&this.path[this.path.length-1].right===e;)e=this.path.pop();return this.path.length>0}},Ct=class extends Mt{getValue(e){return e.key}},Dt=class extends Mt{getValue(e){return[e.key,e.key]}},Pt=e=>()=>e,Tt=e=>{const t=e?(t,i)=>i.minus(t).abs().isLessThanOrEqualTo(e):Pt(!1);return(e,i)=>t(e,i)?0:e.comparedTo(i)};function It(e){const t=e?(t,i,s,n,o)=>t.exponentiatedBy(2).isLessThanOrEqualTo(n.minus(i).exponentiatedBy(2).plus(o.minus(s).exponentiatedBy(2)).times(e)):Pt(!1);return(e,i,s)=>{const n=e.x,o=e.y,a=s.x,r=s.y,l=o.minus(r).times(i.x.minus(a)).minus(n.minus(a).times(i.y.minus(r)));return t(l,n,o,a,r)?0:l.comparedTo(0)}}var Rt=e=>e,At=e=>{if(e){const t=new St(Tt(e)),i=new St(Tt(e)),s=(e,t)=>t.addAndReturn(e),n=e=>({x:s(e.x,t),y:s(e.y,i)});return n({x:new wt(0),y:new wt(0)}),n}return Rt},zt=e=>({set:e=>{Ft=zt(e)},reset:()=>zt(e),compare:Tt(e),snap:At(e),orient:It(e)}),Ft=zt(),Nt=(e,t)=>e.ll.x.isLessThanOrEqualTo(t.x)&&t.x.isLessThanOrEqualTo(e.ur.x)&&e.ll.y.isLessThanOrEqualTo(t.y)&&t.y.isLessThanOrEqualTo(e.ur.y),Et=(e,t)=>{if(t.ur.x.isLessThan(e.ll.x)||e.ur.x.isLessThan(t.ll.x)||t.ur.y.isLessThan(e.ll.y)||e.ur.y.isLessThan(t.ll.y))return null;const i=e.ll.x.isLessThan(t.ll.x)?t.ll.x:e.ll.x,s=e.ur.x.isLessThan(t.ur.x)?e.ur.x:t.ur.x;return{ll:{x:i,y:e.ll.y.isLessThan(t.ll.y)?t.ll.y:e.ll.y},ur:{x:s,y:e.ur.y.isLessThan(t.ur.y)?e.ur.y:t.ur.y}}},Ot=(e,t)=>e.x.times(t.y).minus(e.y.times(t.x)),Ht=(e,t)=>e.x.times(t.x).plus(e.y.times(t.y)),Lt=e=>Ht(e,e).sqrt(),Bt=(e,t,i)=>{const s={x:t.x.minus(e.x),y:t.y.minus(e.y)},n={x:i.x.minus(e.x),y:i.y.minus(e.y)};return Ot(n,s).div(Lt(n)).div(Lt(s))},jt=(e,t,i)=>{const s={x:t.x.minus(e.x),y:t.y.minus(e.y)},n={x:i.x.minus(e.x),y:i.y.minus(e.y)};return Ht(n,s).div(Lt(n)).div(Lt(s))},qt=(e,t,i)=>t.y.isZero()?null:{x:e.x.plus(t.x.div(t.y).times(i.minus(e.y))),y:i},Wt=(e,t,i)=>t.x.isZero()?null:{x:i,y:e.y.plus(t.y.div(t.x).times(i.minus(e.x)))},Ut=class e{point;isLeft;segment;otherSE;consumedBy;static compare(t,i){const s=e.comparePoints(t.point,i.point);return 0!==s?s:(t.point!==i.point&&t.link(i),t.isLeft!==i.isLeft?t.isLeft?1:-1:Qt.compare(t.segment,i.segment))}static comparePoints(e,t){return e.x.isLessThan(t.x)?-1:e.x.isGreaterThan(t.x)?1:e.y.isLessThan(t.y)?-1:e.y.isGreaterThan(t.y)?1:0}constructor(e,t){void 0===e.events?e.events=[this]:e.events.push(this),this.point=e,this.isLeft=t}link(e){if(e.point===this.point)throw new Error("Tried to link already linked events");const t=e.point.events;for(let e=0,i=t.length;e{const s=i.otherSE;t.set(i,{sine:Bt(this.point,e.point,s.point),cosine:jt(this.point,e.point,s.point)})};return(e,s)=>{t.has(e)||i(e),t.has(s)||i(s);const{sine:n,cosine:o}=t.get(e),{sine:a,cosine:r}=t.get(s);return n.isGreaterThanOrEqualTo(0)&&a.isGreaterThanOrEqualTo(0)?o.isLessThan(r)?1:o.isGreaterThan(r)?-1:0:n.isLessThan(0)&&a.isLessThan(0)?o.isLessThan(r)?-1:o.isGreaterThan(r)?1:0:a.isLessThan(n)?-1:a.isGreaterThan(n)?1:0}}},Gt=class e{events;poly;_isExteriorRing;_enclosingRing;static factory(t){const i=[];for(let s=0,n=t.length;s0&&(e=i)}let t=e.segment.prevInResult(),i=t?t.prevInResult():null;for(;;){if(!t)return null;if(!i)return t.ringOut;if(i.ringOut!==t.ringOut)return i.ringOut?.enclosingRing()!==t.ringOut?t.ringOut:t.ringOut?.enclosingRing();t=i.prevInResult(),i=t?t.prevInResult():null}}},Vt=class{exteriorRing;interiorRings;constructor(e){this.exteriorRing=e,e.poly=this,this.interiorRings=[]}addInterior(e){this.interiorRings.push(e),e.poly=this}getGeom(){const e=this.exteriorRing.getGeom();if(null===e)return null;const t=[e];for(let e=0,i=this.interiorRings.length;e0?(this.tree.delete(t),i.push(e)):(this.segments.push(t),t.prev=s)}else{if(s&&n){const e=s.getIntersection(n);if(null!==e){if(!s.isAnEndpoint(e)){const t=this._splitSafely(s,e);for(let e=0,s=t.length;e0)return-1;const s=t.comparePoint(e.rightSE.point);return 0!==s?s:-1}if(i.isGreaterThan(s)){if(a.isLessThan(r)&&a.isLessThan(c))return-1;if(a.isGreaterThan(r)&&a.isGreaterThan(c))return 1;const i=t.comparePoint(e.leftSE.point);if(0!==i)return i;const s=e.comparePoint(t.rightSE.point);return s<0?1:s>0?-1:1}if(a.isLessThan(r))return-1;if(a.isGreaterThan(r))return 1;if(n.isLessThan(o)){const i=t.comparePoint(e.rightSE.point);if(0!==i)return i}if(n.isGreaterThan(o)){const i=e.comparePoint(t.rightSE.point);if(i<0)return 1;if(i>0)return-1}if(!n.eq(o)){const e=l.minus(a),t=n.minus(i),h=c.minus(r),d=o.minus(s);if(e.isGreaterThan(t)&&h.isLessThan(d))return 1;if(e.isLessThan(t)&&h.isGreaterThan(d))return-1}return n.isGreaterThan(o)?1:n.isLessThan(o)||l.isLessThan(c)?-1:l.isGreaterThan(c)?1:e.idt.id?1:0}constructor(e,t,i,s){this.id=++Zt,this.leftSE=e,e.segment=this,e.otherSE=t,this.rightSE=t,t.segment=this,t.otherSE=e,this.rings=i,this.windings=s}static fromRing(t,i,s){let n,o,a;const r=Ut.comparePoints(t,i);if(r<0)n=t,o=i,a=1;else{if(!(r>0))throw new Error(`Tried to create degenerate segment at [${t.x}, ${t.y}]`);n=i,o=t,a=-1}const l=new Ut(n,!0),c=new Ut(o,!1);return new e(l,c,[s],[a])}replaceRightSE(e){this.rightSE=e,this.rightSE.segment=this,this.rightSE.otherSE=this.leftSE,this.leftSE.otherSE=this.rightSE}bbox(){const e=this.leftSE.point.y,t=this.rightSE.point.y;return{ll:{x:this.leftSE.point.x,y:e.isLessThan(t)?e:t},ur:{x:this.rightSE.point.x,y:e.isGreaterThan(t)?e:t}}}vector(){return{x:this.rightSE.point.x.minus(this.leftSE.point.x),y:this.rightSE.point.y.minus(this.leftSE.point.y)}}isAnEndpoint(e){return e.x.eq(this.leftSE.point.x)&&e.y.eq(this.leftSE.point.y)||e.x.eq(this.rightSE.point.x)&&e.y.eq(this.rightSE.point.y)}comparePoint(e){return Ft.orient(this.leftSE.point,e,this.rightSE.point)}getIntersection(e){const t=this.bbox(),i=e.bbox(),s=Et(t,i);if(null===s)return null;const n=this.leftSE.point,o=this.rightSE.point,a=e.leftSE.point,r=e.rightSE.point,l=Nt(t,a)&&0===this.comparePoint(a),c=Nt(i,n)&&0===e.comparePoint(n),h=Nt(t,r)&&0===this.comparePoint(r),d=Nt(i,o)&&0===e.comparePoint(o);if(c&&l)return d&&!h?o:!d&&h?r:null;if(c)return h&&n.x.eq(r.x)&&n.y.eq(r.y)?null:n;if(l)return d&&o.x.eq(a.x)&&o.y.eq(a.y)?null:a;if(d&&h)return null;if(d)return o;if(h)return r;const p=((e,t,i,s)=>{if(t.x.isZero())return Wt(i,s,e.x);if(s.x.isZero())return Wt(e,t,i.x);if(t.y.isZero())return qt(i,s,e.y);if(s.y.isZero())return qt(e,t,i.y);const n=Ot(t,s);if(n.isZero())return null;const o={x:i.x.minus(e.x),y:i.y.minus(e.y)},a=Ot(o,t).div(n),r=Ot(o,s).div(n),l=e.x.plus(r.times(t.x)),c=i.x.plus(a.times(s.x)),h=e.y.plus(r.times(t.y)),d=i.y.plus(a.times(s.y));return{x:l.plus(c).div(2),y:h.plus(d).div(2)}})(n,this.vector(),a,e.vector());return null===p?null:Nt(s,p)?Ft.snap(p):null}split(t){const i=[],s=void 0!==t.events,n=new Ut(t,!0),o=new Ut(t,!1),a=this.rightSE;this.replaceRightSE(o),i.push(o),i.push(n);const r=new e(n,a,this.rings.slice(),this.windings.slice());return Ut.comparePoints(r.leftSE.point,r.rightSE.point)>0&&r.swapEvents(),Ut.comparePoints(this.leftSE.point,this.rightSE.point)>0&&this.swapEvents(),s&&(n.checkForConsuming(),o.checkForConsuming()),i}swapEvents(){const e=this.rightSE;this.rightSE=this.leftSE,this.leftSE=e,this.leftSE.isLeft=!0,this.rightSE.isLeft=!1;for(let e=0,t=this.windings.length;e0){const e=i;i=s,s=e}if(i.prev===s){const e=i;i=s,s=e}for(let e=0,t=s.rings.length;e1===e.length&&e[0].isSubject;this._isInResult=i(e)!==i(t);break}}return this._isInResult}},ei=class{poly;isExterior;segments;bbox;constructor(e,t,i){if(!Array.isArray(e)||0===e.length)throw new Error("Input geometry is not a valid Polygon or MultiPolygon");if(this.poly=t,this.isExterior=i,this.segments=[],"number"!=typeof e[0][0]||"number"!=typeof e[0][1])throw new Error("Input geometry is not a valid Polygon or MultiPolygon");const s=Ft.snap({x:new wt(e[0][0]),y:new wt(e[0][1])});this.bbox={ll:{x:s.x,y:s.y},ur:{x:s.x,y:s.y}};let n=s;for(let t=1,i=e.length;tXt.run("union",e,t),ni=(e,...t)=>Xt.run("intersection",e,t),oi=(e,...t)=>Xt.run("difference",e,t);function ai(e){const t=Math.max(0,Math.min(120,(e-40)/140*120));return`hsl(${Math.round(t)}, 85%, 55%)`}function ri(e,t){if(!(Number.isFinite(e)&&t>0))return e;const i=Math.round(e/t)*t;return Math.abs(i-e)<=1e-9*t?e:i}function li(e,t){if(t){const t=e/2.54;let i=Math.floor(t/12),s=Math.round(t-12*i);return 12===s&&(i+=1,s=0),`${i}′ ${s}″`}return`${(e/100).toFixed(2)} m`}function ci(e,t,i=1){const s=e[0].toFixed(i),n=e[1].toFixed(i),o=t[0].toFixed(i),a=t[1].toFixed(i),r=s=3?e.poly:e&&null!=e.x&&null!=e.y&&null!=e.w&&null!=e.h?[[e.x,e.y],[e.x+e.w,e.y],[e.x+e.w,e.y+e.h],[e.x,e.y+e.h]]:null}function di(e){const t=[];for(const i of e||[])i?.poly?.length>=3?t.push({poly:i.poly.map(e=>e.join(",")).join(" ")}):i&&null!=i.x&&null!=i.y&&null!=i.w&&null!=i.h&&t.push({rect:{x:i.x,y:i.y,w:i.w,h:i.h,rx:.03*Math.min(i.w,i.h)}});return t}function pi(e){const t=[],i=new Set;for(const s of e||[]){const e=hi(s);if(e)for(let s=0;s=90?e-=180:e<-90&&(e+=180),s.step&&s.step>0){const i=Math.sqrt(d),o=Math.min(Math.max(s.length||0,0)/2,i/2);let r=Math.round(p*i/s.step)*s.step;Math.abs(p*i-i/2)<=s.step/2&&(r=i/2),r=Math.max(o,Math.min(i-o,r));const l=r/i;n={x:t+l*c,y:a+l*h,angle:e}}else n={x:u[0],y:u[1],angle:e}}}return n}function _i(e,t,i,s,n,o=1){const a=t*Math.PI/180,r=[Math.cos(a),Math.sin(a)];let l=null,c=o;for(const t of pi(s)){const i=[[t[0],t[1]],[t[2],t[3]]],s=t=>Math.abs(r[0]*(t[1]-e[1])-r[1]*(t[0]-e[0]));if(s(i[0])>o||s(i[1])>o)continue;const n=(i[0][0]-e[0])*r[0]+(i[0][1]-e[1])*r[1],a=(i[1][0]-e[0])*r[0]+(i[1][1]-e[1])*r[1],h=Math.min(n,a),d=Math.max(n,a),p=h>0?h:d<0?-d:0;p[e[0]+r[0]*t,e[1]+r[1]*t],g=(h+d)/2;return{wallA:m(h),wallB:m(d),sideA:u,sideB:_,midA:m((h-p)/2),midB:m((p+d)/2),wallCenter:m(g),centered:Math.abs(g)<=n}}function mi(e,t,i=!1){if("passage"===e)return 1;if(null==t||"unavailable"===t||"unknown"===t)return"window"===e?0:1;const s=function(e){return["on","open","home","detected","playing","cleaning"].includes(String(e))}(t)!==!!i;return s?1:0}function gi(e){return"passage"===e.type?[]:[e.contact,e.lock].filter(e=>"string"==typeof e&&e.length>0)}function fi(e,t,i=.001){return Math.abs(e[0]-t[0])e[1]!=l>e[1]&&e[0]<(r-o)*(e[1]-a)/(l-a)+o&&(i=!i)}return i}function bi(e,t,i){const s=i[0]-t[0],n=i[1]-t[1],o=s*s+n*n;let a=o?((e[0]-t[0])*s+(e[1]-t[1])*n)/o:0;return a=Math.max(0,Math.min(1,a)),Math.hypot(e[0]-(t[0]+a*s),e[1]-(t[1]+a*n))}function yi(e,t,i=1e-6){if(!t||t.length<2)return!1;for(let s=0;sn&&a<-n||o<-n&&a>n)&&(r>n&&l<-n||r<-n&&l>n)}function $i(e,t=24){const i=e.map(e=>e[0]),s=e.map(e=>e[1]),n=Math.min(...i),o=Math.max(...i),a=Math.min(...s),r=Math.max(...s),l=Math.max(o-n,r-a)||1;let c=0,h=0,d=0;for(let t=0;t1e-9?[h/(3*c),d/(3*c)]:[(n+o)/2,(a+r)/2],u=(t,i)=>{const s=((t,i)=>{if(!vi([t,i],e))return-1/0;let s=1/0;for(let n=0;nm&&(m=c,_=[s,l])}if(_){const[e,i]=_,s=(o-n)/t,l=(r-a)/t;for(let t=-4;t<=4;t++)for(let n=-4;n<=4;n++){const o=e+s*t/4,a=i+l*n/4,r=u(o,a);r>m&&(m=r,_=[o,a])}}return _||Si(e)||e[0]}function Si(e,t=1e-6){if(!e||e.length<3)return null;const i=e.length,s=[e.reduce((e,t)=>e+t[0],0)/i,e.reduce((e,t)=>e+t[1],0)/i];if(wi(s,e,t))return s;for(let s=0;sCi(e,t,i));return s.filter(e=>!s.some(t=>t!==e&&Ci(t,e,i)))}function Ti(e){if(!e||e.length<3)return 0;let t=0;for(let i=0;i[e[0],e[1]]),[e[0][0],e[0][1]]]]}function Ri(e,t,i){for(let s=0;s1&&fi(i[0],i[i.length-1],t)&&i.pop(),i}function zi(e,t,i=1e-6){if(!e||e.length<3||!t||t.length<2)return null;const s=t[0],n=t[t.length-1];if(fi(s,n,i))return null;const o=Ri(e,s,i),a=Ri(e,n,i);if(o<0||a<0)return null;const r=t.slice(1,-1);for(const t of r)if(!wi(t,e,i))return null;for(let i=0;i{const a=[t];let r=(s+1)%e.length;for(let t=0;t<=e.length&&(a.push(e[r]),r!==o);t++)r=(r+1)%e.length;return a.push(n),Ai(a,i)};let c,h;if(o===a){const a=Ai([...t],i);if(a.length<3||Ti(a)<=i)return null;const r=[];for(let i=0;i=0?t:[...t].reverse();for(const e of i)r.push(e)}c=Ai(r,i),h=a}else c=Ai([...l(s,o,n,a),...[...r].reverse()],i),h=Ai([...l(n,a,s,o),...r],i);return c.length<3||h.length<3||Ti(c)<=i||Ti(h)<=i||Math.abs(Ti(c)+Ti(h)-Ti(e))>Math.max(i,1e-6*Ti(e))?null:[c,h]}function Fi(e,t,i){const[s,n]=e.split(":");return"device"===s?n:"entity"===s?"lg_"+n:t&&t.startsWith("v_")?t:i()}function Ni(e){return e.length?Math.round(e.reduce((e,t)=>e+t,0)/e.length):null}function Ei(e,t){if(t>e[2]/e[3]){const i=e[3],s=e[3]*t;return{x:e[0]-(s-e[2])/2,y:e[1],w:s,h:i}}const i=e[2],s=e[2]/t;return{x:e[0],y:e[1]-(s-e[3])/2,w:i,h:s}}function Oi(e,t,i,s){if(e.length<2)return;const n=t.x+s,o=t.x+t.w-s,a=t.y+s,r=t.y+t.h-s;for(let t=0;t<60;t++){let t=!1;for(let s=0;s=0)i=t.slice(0,n).trim(),s=t.slice(n+1).trim();else{const e=t.split(".");e.length>2&&(i=e.slice(0,2).join("."),s=e.slice(2).join("."))}return/^[a-z0-9_]+\.[a-z0-9_]+$/.test(i)?n>=0&&!s||s&&!/^[a-zA-Z0-9_.-]+$/.test(s)?null:s?{entity:i,attr:s}:{entity:i}:null}function Ji(e,t){const i=String(e??"").trim(),s=String(t??"").trim(),n=Yi(s?`${i}:${s}`:i);return n?`{${n.entity}${n.attr?`:${n.attr}`:""}}`:""}function Xi(e,t,i){const s=String(t??"").trim();if(!s)return null;const n=e?.states?.[s];if(!n)return null;const o=String(i??"").trim(),a=e=>e.slice(0,60);if(o){const t=function(e){if(null==e)return null;if(Array.isArray(e)){const t=e.map(e=>null==e?"":String(e)).join(", ");return t?t.slice(0,60):null}if("object"==typeof e)return null;const t=String(e);return""===t?null:t.slice(0,60)}(n.attributes?.[o]);if(null===t)return null;const i=e?.formatEntityAttributeValue;if("function"==typeof i)try{const t=i.call(e,n,o);if("string"==typeof t&&""!==t)return{text:a(t),formatted:!0}}catch{}return{text:t,formatted:!1}}const r=n.state;if(null==r||""===r)return null;const l=e?.formatEntityState;if("function"==typeof l)try{const t=l.call(e,n);if("string"==typeof t&&""!==t)return{text:a(t),formatted:!0}}catch{}return{text:a(String(r)),formatted:!1}}function Zi(e,t,i){const s=String(t??"").trim(),n=String(i??"").trim()||s;if(!n)return e.text;const o=e.formatted&&s?function(e,t){if(!t)return e;const i=e.replace(/\s+$/,"");return i.endsWith(t)?i.slice(0,i.length-t.length).replace(/\s+$/,""):e}(e.text,s):e.text;return`${o} ${n}`}function Qi(e,t){const i=(t?.entity||"").trim();if(!i)return"";const s=e?.states?.[i],n=s?.state;if(!s||null==n||""===n||"unavailable"===n||"unknown"===n)return Vi;const o=(t?.attr||"").trim(),a=Xi(e,i,o||null);if(null===a)return Vi;return Zi(a,o?"":String(s.attributes?.unit_of_measurement??"").trim(),t?.unit)}function es(e,t,i,s=()=>!0){const n=e??"";let o=!1;const a=n.replace(/\{([^{}\r\n]+)\}/g,(e,t)=>{const n=Yi(t);return n?(o=!0,s(n.entity||"")?Qi(i,n):Vi):e});if(o)return a;const r=(t?.entity||"").trim();if(!r)return n;if(!s(r)){const e=n.indexOf(Ki);return e>=0?n.slice(0,e)+Vi+n.slice(e+2):n?`${n} ${Vi}`:Vi}const l=Qi(i,t),c=n.indexOf(Ki);return c>=0?n.slice(0,c)+l+n.slice(c+2):n?`${n} ${l}`:l}const ts=20;function is(e){const t=Number(e?.scale);if(Number.isFinite(t)&&t>0)return Math.min(20,Math.max(.15,t));return{s:.7,m:1,l:1.5}[String(e?.size??"")]??1}function ss(e,t){if(!t)return e;let i=e;for(const[e,s]of Object.entries(t))i=i.split("{"+e+"}").join(String(s));return i}const ns="#55606c",os=.55,as={c:"#607d8b",a:.18};function rs(e,t=as){const i=e&&"object"==typeof e?e:null,s=i?.a;return{c:Ie(i?.c,t.c),a:"number"==typeof s&&Number.isFinite(s)?Math.min(1,Math.max(0,s)):t.a}}function ls(e,t){const i=rs(e),s=t?.settings?.custom_fill;return s&&"object"==typeof s?rs(s,i):i}function cs(e){const t=e?.settings||{},i=!e?.plan_url,s="glow"===t.fill_mode;return{showBorders:t.show_borders??i,showNames:t.show_names??i,color:Ie(t.room_color,ns),opacity:"number"==typeof t.room_opacity?Math.min(1,Math.max(0,t.room_opacity)):os,fill:["lqi","light","temp","custom"].includes(t.fill_mode)?t.fill_mode:"none",customFill:rs(t.custom_fill),glow:"boolean"==typeof t.glow_enabled?t.glow_enabled:s,tempMin:"number"==typeof t.temp_min?t.temp_min:20,tempMax:"number"==typeof t.temp_max?t.temp_max:25,showLqi:"boolean"==typeof t.show_lqi?t.show_lqi:null,cardFontScale:"number"==typeof t.card_font_scale&&t.card_font_scale>0?Math.min(3,Math.max(.5,t.card_font_scale)):1,labelTemp:!0===t.label_temp,labelHum:!0===t.label_hum,labelLqi:!0===t.label_lqi,labelLight:!0===t.label_light,bgColor:Ie(t.bg_color,null),hideDecor:!0===t.hide_decor,hideOpenings:!0===t.hide_openings}}function hs(e,t){if(t.bgColor)return t.bgColor;const i=e?.bg_color;return Ie(i,"")}const ds={light_on:{c:"#ffd45c",a:.18},light_off:{c:"#9aa0a6",a:.14},light_none:{c:"#6b7480",a:0},temp_cold:{c:"#4fc3f7",a:.18},temp_ok:{c:"#66d17a",a:.18},temp_hot:{c:"#ffd45c",a:.18},lqi_low:{c:"#f25a4a",a:.18},lqi_high:{c:"#4bd28f",a:.18},glow_base:{c:"#0d1b2a",a:.5},glow_light:{c:"#ffd9a0",a:.85},wall_fill:{c:"#ffffff",a:1}};function ps(e){const t={},i=e?.fill_colors||{};for(const e of Object.keys(ds)){const s=ds[e],n=i[e];t[e]={c:Ie(n?.c,s.c),a:n&&"number"==typeof n.a?Math.min(1,Math.max(0,n.a)):s.a}}return t}function us(e,t,i){const s=Math.min(1,Math.max(0,i)),n=[1,3,5].map(t=>parseInt(e.slice(t,t+2),16)),o=[1,3,5].map(e=>parseInt(t.slice(e,e+2),16)),a=n.map((e,t)=>Math.round(e+(o[t]-e)*s));return"#"+a.map(e=>e.toString(16).padStart(2,"0")).join("")}function _s(e,t,i,s,n,o,a,r=as){const l=function(e,t,i,s,n,o,a,r=as){if("custom"===e)return rs(r);if("lqi"===e){if(null==t)return null;const e=(t-40)/140;return{c:us(a.lqi_low.c,a.lqi_high.c,e),a:a.lqi_low.a+(a.lqi_high.a-a.lqi_low.a)*Math.min(1,Math.max(0,e))}}if("light"===e)return"none"===i?a.light_none.a>0?a.light_none:null:"on"===i?a.light_on:a.light_off;if("temp"===e){if(null==s)return null;const e=Math.min(n,o),t=Math.max(n,o);return st?a.temp_hot:a.temp_ok}return null}(e,t,i,s,n,o,a,r);return l?{color:l.c,opacity:l.a,mode:e}:null}const ms={blind:["mdi:blinds","mdi:blinds-open"],shade:["mdi:blinds","mdi:blinds-open"],shutter:["mdi:window-shutter","mdi:window-shutter-open"],curtain:["mdi:curtains-closed","mdi:curtains"],window:["mdi:window-closed","mdi:window-open"],awning:["mdi:awning-outline","mdi:awning"],door:["mdi:door-closed","mdi:door-open"],garage:["mdi:garage","mdi:garage-open"],gate:["mdi:gate","mdi:gate-open"],damper:["mdi:circle-slice-8","mdi:circle-outline"]},gs=[["mdi:roller-shade-closed","mdi:roller-shade"],["mdi:blinds-horizontal-closed","mdi:blinds-horizontal"],["mdi:garage-variant","mdi:garage-open-variant"],["mdi:door","mdi:door-open"]];function fs(e){for(const t of[...Object.values(ms),...gs])if(e===t[0]||e===t[1])return t;return null}function vs(e,t,i,s,n){if(!s||"unavailable"===s||"unknown"===s)return e;if(n){const i="cover"===t?fs(e):null;return i?"closed"===s?i[0]:i[1]:e}if("binary_sensor"===t){if("door"===i)return"on"===s?"mdi:door-open":"mdi:door-closed";if("window"===i)return"on"===s?"mdi:window-open":"mdi:window-closed";if("garage_door"===i)return"on"===s?"mdi:garage-open-variant":"mdi:garage-variant"}if("cover"===t){const t=ms[String(i||"")];if(t)return"closed"===s?t[0]:t[1];const n=fs(e);return n?"closed"===s?n[0]:n[1]:e}return"lock"===t?"locked"===s?"mdi:lock":"mdi:lock-open-variant":"light"===t&&"mdi:lightbulb"===e&&"on"===s?"mdi:lightbulb-on":e}function bs(e){const t=Math.min(4e4,Math.max(1e3,e))/100,i=t<=66?255:329.698727446*Math.pow(t-60,-.1332047592),s=t<=66?99.4708025861*Math.log(t)-161.1195681661:288.1221695283*Math.pow(t-60,-.0755148492),n=t>=66?255:t<=19?0:138.5177312231*Math.log(t-10)-305.0447927307,o=e=>Math.round(Math.min(255,Math.max(0,e)));return[o(i),o(s),o(n)]}const ys=1/2.2;function ws(e){const t=/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/.exec(e);return t?"#"+t.slice(1).map(e=>Math.min(255,Number(e)).toString(16).padStart(2,"0")).join(""):null}function ks(e){if(!e||"object"!=typeof e||Array.isArray(e))return null;const t=e;if(Object.keys(t).some(e=>"c"!==e&&"bri"!==e))return null;const i=Ie(t.c,null);return i?void 0===t.bri||null===t.bri?{c:i}:"number"!=typeof t.bri||!Number.isFinite(t.bri)||t.bri<.01||t.bri>1?null:{c:i,bri:t.bri}:null}function xs(e,t,i){const s=ks(t),n=e?.attributes||{},o=s?.bri??function(e){const t=e?.attributes?.brightness,i="number"==typeof t?t:"string"==typeof t&&""!==t.trim()?Number(t):Number.NaN;return Number.isFinite(i)?Math.max(0,Math.min(1,i/255)):1}(e);if(s)return{c:s.c,bri:o};const a=ze(n.rgb_color);if(a)return{c:ws(a)||Ie(i,"#ffd9a0"),bri:o};const r=Number(n.color_temp_kelvin)||(Number(n.color_temp)>0?1e6/Number(n.color_temp):NaN);return Number.isFinite(r)&&r>0?{c:(l=bs(r),"#"+l.slice(0,3).map(e=>Math.min(255,Math.max(0,Math.round(Number(e)||0))).toString(16).padStart(2,"0")).join("")),bri:o}:{c:Ie(i,"#ffd9a0"),bri:o};var l}function $s(e,t,i){return"on"===e?.state?xs(e,t,i):null}function Ss(e,t=1){const i=Math.max(0,Math.min(1,Number.isFinite(e)?e:1)),s=.7*Math.max(0,Math.min(1,Number.isFinite(t)?t:1))*(.4+.6*Math.pow(i,ys));return Math.max(0,Math.min(1,s))}function Ms(e){return e.startsWith("light.")||e.startsWith("switch.")}function Cs(e,t,i=1e-6){const s=[];if(!e||!t||e.length<3||t.length<3)return s;for(let n=0;np||l>p)continue;const u=(n[0]-o[0])*h+(n[1]-o[1])*d,_=(a[0]-o[0])*h+(a[1]-o[1])*d,m=Math.max(0,Math.min(u,_)),g=Math.min(c,Math.max(u,_));g-m>i&&s.push([o[0]+h*m,o[1]+d*m,o[0]+h*g,o[1]+d*g])}}return s}function Ds(e,t,i=1e-6){const s=[];for(const n of e){const e=[n[0],n[1]],o=[n[2],n[3]],a=o[0]-e[0],r=o[1]-e[1],l=Math.hypot(a,r);if(lo||n>o)continue;const a=(s[0]-e[0])*c+(s[1]-e[1])*h,r=(s[2]-e[0])*c+(s[3]-e[1])*h,p=Math.max(0,Math.min(a,r)),u=Math.min(l,Math.max(a,r));u-p>i&&d.push([p,u])}if(!d.length){s.push([e[0],e[1],o[0],o[1]]);continue}d.sort((e,t)=>e[0]-t[0]);let p=0;for(const[t,n]of d)t-p>i&&s.push([e[0]+c*p,e[1]+h*p,e[0]+c*t,e[1]+h*t]),p=Math.max(p,n);l-p>i&&s.push([e[0]+c*p,e[1]+h*p,o[0],o[1]])}return s}function Ps(e,t,i=1e-6){const s=[];for(let t=0;t{if("string"!=typeof e||!e)return;const i=As(e);i.startsWith("/api/houseplan/content/")&&t.add(i)};for(const t of e?.spaces||[]){i(t?.plan_url);for(const e of t?.markers||[])for(const t of e?.pdfs||[])i(t?.url)}for(const t of e?.markers||[])for(const e of t?.pdfs||[])i(e?.url);return t}function As(e){return e?e.startsWith("/houseplan_files/plans/")?"/api/houseplan/content/plans/_/"+e.slice(23):e.startsWith("/houseplan_files/files/")?"/api/houseplan/content/files/"+e.slice(23):e:""}function zs(e,t){const i=t?.settings?.fill_mode;return"none"===i||"lqi"===i||"light"===i||"temp"===i||"custom"===i?i:e}function Fs(e,t){const i=t?.settings;return"boolean"==typeof i?.glow?i.glow:"glow"===i?.fill_mode||e}function Ns(e,t=1){const i=Number(e);return Number.isFinite(i)&&i>0?Math.min(3,Math.max(.5,i)):t}function Es(e,t){let i=180*Math.atan2(t[1]-e[1],t[0]-e[0])/Math.PI;return i<0&&(i+=360),i}function Os(e,t=.5){const i=(e%45+45)%45;return i<=t||45-i<=t}function Hs(e,t){const i=t[2]-t[0],s=t[3]-t[1],n=i*i+s*s;if(!n)return Math.hypot(e[0]-t[0],e[1]-t[1]);let o=((e[0]-t[0])*i+(e[1]-t[1])*s)/n;return o=Math.max(0,Math.min(1,o)),Math.hypot(e[0]-(t[0]+o*i),e[1]-(t[1]+o*s))}const Ls=new Set(["smoke","gas","carbon_monoxide","moisture","safety","tamper","problem"]);function Bs(e,t){return"alarm_control_panel"===e||"siren"===e||"binary_sensor"===e&&!!t&&Ls.has(t)}function js(e){if(!e)return null;const t=e.indexOf("#");if(t<=0)return null;const i=e.slice(0,t),s=e.slice(t+1);if(!s)return null;if(s.startsWith("@")){const e=s.slice(1);return e?{space:i,area:null,roomId:e}:null}return{space:i,area:s,roomId:null}}const qs={availability:"available",status:"alarm",activity:"none"},Ws=new Set(["motion","vibration","sound"]),Us=new Set(["occupancy","presence"]),Gs=new Set(["door","window","garage_door","opening"]),Vs=new Set(["running","power"]),Ks=new Set(["smoke","gas","carbon_monoxide","moisture","safety","tamper","problem"]);const Ys=new Set(["running","working","washing","rinsing","spinning","drying","heating","cooling","cleaning","cooking","playing","recording","pumping","irrigating","humidifying","dehumidifying","fan","preheating","defrosting"]),Js=new Set([...Ys,"start","started","run","active","in_progress","wash","rinse","spin","dry"]),Xs=new Set(["off","idle","paused","standby","docked","finished","complete","completed","stopped","ready","sleeping","stop","end","done","inactive"]),Zs=new Set(["heat","cool","heat_cool","auto","dry","fan_only"]),Qs=e=>""===e||"unknown"===e||"unavailable"===e||"__missing__"===e,en=e=>String(e??"").trim().toLowerCase(),tn=e=>en(e).replace(/[\s-]+/g,"_").replace(/_+/g,"_").replace(/^_|_$/g,""),sn=new Map([["run_state",0],["job_state",0],["operation_state",0],["activity_state",0],["machine_state",1],["running_state",1],["status",2],["device_status",2],["machine_status",2]]),nn=new Set(["wifi","connection","signal","battery"]);function on(e,t){const i=e?.entities?.[t]||{};if("config"===i.entity_category)return null;const s=String(t||"").split(".").slice(1).join("."),n=[i.translation_key,i.original_name,i.name],o=[...n,s];n.some(e=>en(e))||o.push(e?.states?.[t]?.attributes?.friendly_name);const a=o.map(tn).filter(Boolean);if(a.some(e=>e.split("_").some(e=>nn.has(e))))return null;let r=null;for(const e of a)for(const[t,i]of sn)(e===t||e.endsWith(`_${t}`))&&(r=null==r?i:Math.min(r,i));return r}const an=(e,t)=>null!=on(e,t);function rn(e,t){if(!t.startsWith("switch."))return!1;const i=e?.entities?.[t]||{},s=e?.states?.[t],n=t.slice(7).toLowerCase();if(/(?:^|_)(?:main_)?power$/.test(n))return!0;if([i.translation_key,i.original_name,i.name].map(en).some(e=>["power","main power","power switch","питание"].includes(e)))return!0;const o=en(s?.attributes?.friendly_name);return/(?:^|[\s._-])(?:main[\s._-]+)?power$/.test(o)||/(?:^|[\s._-])питание$/.test(o)}function ln(e){for(const t of["hvac_action","action","current_operation","run_state","job_state","operation","activity"]){const i=en(e?.[t]);if(Ys.has(i)||Xs.has(i))return i}return""}function cn(e,t){const i=e?.states?.[t],s=i?en(i.state):"__missing__",n=String(t||"").split(".")[0],o=en(i?.attributes?.device_class),a={eid:t,state:s,availability:Qs(s)?"unavailable":"available",status:"neutral",activity:"none",edge:"none"};if("unavailable"===a.availability)return a;if(function(e,t,i){return"alarm_control_panel"===e?"triggered"===i:"on"===i&&Bs(e,t)}(n,o,s))return{...a,status:"alarm"};if("binary_sensor"===n)return Ws.has(o)?{...a,edge:"rising"}:Us.has(o)?{...a,activity:"on"===s?"presence":"none"}:Gs.has(o)?{...a,status:"on"===s?"open":"neutral",edge:"rising"}:"moving"===o?{...a,activity:"on"===s?"transition":"none"}:Vs.has(o)&&"on"===s?{...a,status:"working",activity:"running"}:a;if("cover"===n)return{...a,activity:"opening"===s||"closing"===s?"transition":"none",edge:"terminal_transition"};if("lock"===n)return{...a,status:"unlocked"===s||"open"===s?"open":"neutral",activity:"locking"===s||"unlocking"===s?"transition":"none",edge:"terminal_transition"};if("valve"===n)return{...a,status:["open","opening","closing"].includes(s)?"open":"neutral",activity:"opening"===s||"closing"===s?"transition":"none",edge:"terminal_transition"};if("climate"===n){const e=ln(i.attributes),t=Array.isArray(i.attributes?.hvac_modes)?i.attributes.hvac_modes.map(en):[],n=!Xs.has(s)&&(Zs.has(s)||Ys.has(s)||t.includes(s));return(e?Ys.has(e):n)?{...a,status:"working",activity:"running"}:a}if(["light","switch","fan","humidifier"].includes(n))return"on"===s?{...a,status:"working",activity:"running"}:a;if("media_player"===n)return"off"===s?{...a,availability:"unavailable"}:a;if("vacuum"===n)return"cleaning"===s?{...a,status:"working",activity:"running"}:"returning"===s?{...a,status:"working",activity:"transition"}:a;if("script"===n)return"on"===s?{...a,status:"working",activity:"running"}:a;if("automation"===n)return a;if("button"===n||"event"===n)return{...a,edge:"change"};const r=ln(i.attributes);return Ys.has(r)||Ys.has(s)&&!Xs.has(s)?{...a,status:"working",activity:"running"}:a}function hn(e,t,i){const s=i.filter(t=>t.startsWith("switch.")&&!e?.entities?.[t]?.entity_category),n=t.find(t=>rn(e,t));if(!(s.length>1&&!!n))return t.map(t=>cn(e,t));const o=t.find(t=>t!==n&&an(e,t)),a=cn(e,n);return"unavailable"===a.availability||"off"===a.state?t.map(t=>({eid:t,state:e?.states?.[t]?en(e.states[t].state):"__missing__",availability:"unavailable",status:"neutral",activity:"none",edge:"none"})):t.map(t=>{if(t===o)return function(e,t){const i=e?.states?.[t],s=i?en(i.state):"__missing__",n={eid:t,state:s,availability:Qs(s)?"unavailable":"available",status:"neutral",activity:"none",edge:"none"};if("unavailable"===n.availability)return n;const o=ln(i?.attributes);return Ys.has(o)||Js.has(s)&&!Xs.has(s)?{...n,status:"working",activity:"running"}:n}(e,t);return{...t===n?a:cn(e,t),status:"neutral",activity:"none",edge:"none"}})}function dn(e){if(!e.length)return{availability:"available",status:"neutral",activity:"none"};const t=e.filter(e=>"available"===e.availability);if(!t.length)return{availability:"unavailable",status:"neutral",activity:"none"};if(t.some(e=>"alarm"===e.status))return qs;const i=t.some(e=>"working"===e.status)?"working":t.some(e=>"open"===e.status)?"open":"neutral",s=t.some(e=>"transition"===e.activity)?"transition":t.some(e=>"presence"===e.activity)?"presence":t.some(e=>"running"===e.activity)?"running":"none";return{availability:"available",status:i,activity:s}}function pn(e,t){if(!(e=>!!e&&!Qs(e))(e)||"unavailable"===t.availability||e===t.state)return null;if("rising"===t.edge)return"off"===e&&"on"===t.state?"event":null;if("change"===t.edge)return"event";if("terminal_transition"===t.edge){const i=new Set([e,t.state]);if(i.has("closed")&&i.has("open")||i.has("locked")&&i.has("unlocked"))return"transition"}return null}const un=(e,t=0)=>{const i=Number(e);return Number.isInteger(i)&&i>=0?i:t},_n=(e,t)=>{const i={rev:0,configRev:un(t),off:new Set};if(null==e)return{snapshot:i,valid:!1};const s=e.off;return!Number.isInteger(e.rev)||Number(e.rev)<0||!Number.isInteger(e.config_rev)||Number(e.config_rev)<0||!Array.isArray(s)||s.some(e=>"string"!=typeof e||!e)?{snapshot:i,valid:!1}:{snapshot:{rev:Number(e.rev),configRev:Number(e.config_rev),off:new Set(s)},valid:!0}};function mn(e,t=0){return _n(e,t).snapshot}function gn(e,t,i,s){const n=_n(t,i);return s&&n.valid&&n.snapshot.configRev===i?e.configRev===i&&n.snapshot.rev{try{const[i,s]=await Promise.all([e.callWS({type:"config/device_registry/list"}),e.callWS({type:"config/entity_registry/list"})]),n=Dn(i,"id"),o=Dn(s,"entity_id");if(!n||!o)throw new Error("invalid_registry_response");t.devices=n,t.entities=o,t.authoritative=!0,t.access="full",t.lastSuccess=Date.now(),t.error=void 0}catch(e){t.authoritative=!1,t.access="limited",t.error=function(e){if(e&&"object"==typeof e){const t=e;return String(t.message||t.code||t.error||"registry_unavailable")}return String(e||"registry_unavailable")}(e)}finally{t.revision++,t.loading=void 0,function(e){for(const t of[...e.listeners])try{t()}catch{}}(t)}})()),t.loading}function Tn(e,t){void 0===t.reloadTimer&&(t.reloadTimer=globalThis.setTimeout(()=>{t.reloadTimer=void 0,Pn(e,t)},80))}async function In(e,t){if(t.subscribing||t.unsubDevice&&t.unsubEntity)return t.subscribing;const i=e?.connection?.subscribeEvents;return"function"==typeof i?(t.subscribing=(async()=>{try{t.unsubDevice||(t.unsubDevice=await i.call(e.connection,()=>Tn(e,t),"device_registry_updated")),t.unsubEntity||(t.unsubEntity=await i.call(e.connection,()=>Tn(e,t),"entity_registry_updated"))}catch{}finally{0===t.refs&&(t.unsubDevice?.(),t.unsubEntity?.(),t.unsubDevice=void 0,t.unsubEntity=void 0),t.subscribing=void 0}})(),t.subscribing):void 0}function Rn(e,t){const i=Cn(e);if(!i)return()=>{};const s=0===i.refs;i.refs++,i.listeners.add(t),("pending"===i.access||s)&&Pn(e,i),In(e,i);let n=!1;return()=>{n||(n=!0,i.listeners.delete(t),i.refs=Math.max(0,i.refs-1),i.refs>0||(i.unsubDevice?.(),i.unsubEntity?.(),i.unsubDevice=void 0,i.unsubEntity=void 0,void 0!==i.reloadTimer&&globalThis.clearTimeout(i.reloadTimer),i.reloadTimer=void 0))}}function An(e){const t=Cn(e);if(!t)return{revision:0,authoritative:!1,access:"limited",devices:e?.devices||{},entities:e?.entities||{},lastSuccess:0,error:"registry_unavailable"};let i=e?.devices||{},s=e?.entities||{};if(t.authoritative){const n=t.liveDevices!==i||t.liveEntities!==s,o=void 0!==t.liveDevices||void 0!==t.liveEntities;if(n&&(t.liveDevices=i,t.liveEntities=s,t.projectedRevision=void 0,o&&Tn(e,t)),t.projectedRevision!==t.revision||!t.projectedDevices||!t.projectedEntities){const e={...t.devices},n={...t.entities};for(const[t,s]of Object.entries(i))Object.prototype.hasOwnProperty.call(e,t)||(e[t]=s);for(const[e,t]of Object.entries(s))Object.prototype.hasOwnProperty.call(n,e)||(n[e]=t);t.projectedDevices=e,t.projectedEntities=n,t.projectedRevision=t.revision}i=t.projectedDevices,s=t.projectedEntities}return{revision:t.revision,authoritative:t.authoritative,access:t.access,devices:i,entities:s,lastSuccess:t.lastSuccess,error:t.error}}function zn(e){if(!e||"object"!=typeof e&&"function"!=typeof e)return 0;let t=xn.get(e);return t||(t=$n++,xn.set(e,t)),t}function Fn(e,t=An(e)){return[t.revision,t.access,zn(t.devices),zn(t.entities)].join(":")}function Nn(e){return!!e&&null==e.disabled_by}function En(e,t=An(e)){const i={},s={},n={};for(const[e,s]of Object.entries(t.devices||{}))Nn(s)&&(i[e]=s);for(const[e,i]of Object.entries(t.entities||{})){if(!Nn(i))continue;const n=i.device_id?t.devices?.[i.device_id]:null;t.authoritative&&i.device_id&&!n||(n&&!Nn(n)||(s[e]=i))}for(const[i,s]of Object.entries(e?.states||{})){const e=t.entities?.[i];if(e&&!Nn(e))continue;const o=e?.device_id?t.devices?.[e.device_id]:null;t.authoritative&&e?.device_id&&!o||(o&&!Nn(o)||(n[i]=s))}return{...e,devices:i,entities:s,states:n}}function On(e,t=An(e)){return{...e,devices:t.devices||{},entities:t.entities||{}}}function Hn(e,t){const i=[];for(const[s,n]of Object.entries(e||{}))n?.device_id===t&&i.push(s);return i}function Ln(){if(Mn)return Mn;if("undefined"==typeof localStorage)return{};try{const e=JSON.parse(localStorage.getItem(Sn)||"{}");if(!e||"object"!=typeof e||Array.isArray(e))return{};const t=Date.now(),i={};for(const[s,n]of Object.entries(e))!n||"active"!==n.kind&&"ha_disabled"!==n.kind||!Number.isFinite(n.ts)||t-n.ts>7776e6||(i[s]=n);return Mn=i,Mn}catch{return{}}}function Bn(e,t,i=An(e)){if(!t||"virtual"===t)return{kind:"active",enabledEntityIds:[],allEntityIds:[]};const s=t.indexOf(":");if(s<1)return{kind:"unverified",reason:"registry_unavailable",enabledEntityIds:[],allEntityIds:[]};const n=t.slice(0,s),o=t.slice(s+1);if("device"!==n&&"entity"!==n||!o)return{kind:"unverified",reason:"registry_unavailable",enabledEntityIds:[],allEntityIds:[]};const a=i.devices||{},r=i.entities||{};if(i.authoritative){if("device"===n){const e=a[o];if(!e)return{kind:"orphaned",reason:"device_missing",enabledEntityIds:[],allEntityIds:[]};const t=Hn(r,o);if(!Nn(e))return{kind:"ha_disabled",reason:"device",enabledEntityIds:[],allEntityIds:t};const i=t.filter(e=>Nn(r[e]));return t.length&&!i.length?{kind:"ha_disabled",reason:"all_entities",enabledEntityIds:[],allEntityIds:t}:{kind:"active",enabledEntityIds:i,allEntityIds:t}}const t=r[o];return t&&!Nn(t)?{kind:"ha_disabled",reason:"entity",enabledEntityIds:[],allEntityIds:[o]}:t?.device_id&&!a[t.device_id]?{kind:"orphaned",reason:"device_missing",enabledEntityIds:[],allEntityIds:[o]}:t?.device_id&&!Nn(a[t.device_id])?{kind:"ha_disabled",reason:"device",enabledEntityIds:[],allEntityIds:[o]}:t||e?.states?.[o]?{kind:"active",enabledEntityIds:[o],allEntityIds:[o]}:{kind:"orphaned",reason:"entity_missing",enabledEntityIds:[],allEntityIds:[]}}const l=function(e){return Ln()[e]||null}(t);if("device"===n){const t=a[o],i=Hn(r,o);if(null!=t?.disabled_by)return{kind:"ha_disabled",reason:"device",enabledEntityIds:[],allEntityIds:i};const s=i.filter(e=>{const t=r[e];return null==t?.disabled_by&&(!t.device_id||null==a[t.device_id]?.disabled_by)});if(i.length&&!s.length&&i.every(e=>null!=r[e]?.disabled_by))return{kind:"ha_disabled",reason:"all_entities",enabledEntityIds:[],allEntityIds:i};if("ha_disabled"===l?.kind)return{kind:"ha_disabled",reason:l.reason||"device",enabledEntityIds:[],allEntityIds:i};if(t||s.some(t=>!!e?.states?.[t]))return{kind:"active",enabledEntityIds:s,allEntityIds:i}}else{const t=r[o];if(null!=t?.disabled_by)return{kind:"ha_disabled",reason:"entity",enabledEntityIds:[],allEntityIds:[o]};if(t?.device_id&&null!=a[t.device_id]?.disabled_by)return{kind:"ha_disabled",reason:"device",enabledEntityIds:[],allEntityIds:[o]};if("ha_disabled"===l?.kind)return{kind:"ha_disabled",reason:l.reason||"entity",enabledEntityIds:[],allEntityIds:[o]};if(t||e?.states?.[o])return{kind:"active",enabledEntityIds:[o],allEntityIds:[o]}}return{kind:"unverified",reason:"registry_unavailable",enabledEntityIds:[],allEntityIds:[]}}function jn(e){const t={};for(const[i,s]of Object.entries(e.entities)){if(!s?.device_id||!Nn(s))continue;const n=e.devices?.[s.device_id];n&&!Nn(n)||(t[s.device_id]=t[s.device_id]||[]).push(i)}return t}function qn(e,t){const i=new Map;for(const s of e){if(!0===s?.removed)continue;const e=s?.binding||"";if(!e.startsWith("entity:"))continue;const n=e.slice(7);if(!n)continue;const o=t?.entities?.[n]?.device_id;if(!o)continue;const a=i.get(o)||new Set;a.add(n),i.set(o,a)}return{byDevice:i}}function Wn(e,t,i,s){const n=s.byDevice.get(t);return n?.size?{partial:!0,entityIds:i.filter(t=>!n.has(t)&&!e?.entities?.[t]?.hidden)}:{partial:!1,entityIds:[...i]}}function Un(e,t,i){if(t.identifiers?.[0]?.[0])return t.identifiers[0][0];for(const t of i){const i=e.entities[t]?.platform;if(i)return i}return""}function Gn(e,t){if(/_device_temperature$/.test(t))return!1;if(e.entities?.[t]?.entity_category)return!1;const i=e.states[t];if(!i)return/_temperature$/.test(t);const s=i.attributes||{};return"temperature"===s.device_class||/°C|°F/.test(s.unit_of_measurement||"")||/_temperature$/.test(t)}const Vn=["vacuum","lawn_mower","climate","media_player","light","cover","lock","valve","alarm_control_panel","water_heater","fan","humidifier","siren","camera","remote"],Kn=e=>[...e.filter(e=>!e.reg?.hidden),...e.filter(e=>!!e.reg?.hidden)];function Yn(e,t){const i=t.map(t=>({eid:t,reg:e?.entities?.[t]})).filter(e=>!!e.reg);if(!i.length)return[];const s=i.filter(e=>!e.reg.entity_category),n=s.length?s:i;for(const e of Vn){const t=n.filter(t=>t.eid.startsWith(e+"."));if(t.length)return Kn(t).map(e=>e.eid)}const o=n.filter(t=>function(e,t){if(!t.startsWith("binary_sensor."))return!1;const i=en(e?.states?.[t]?.attributes?.device_class||e?.entities?.[t]?.device_class||e?.entities?.[t]?.original_device_class);return Ws.has(i)||Us.has(i)||Gs.has(i)||Vs.has(i)||"moving"===i||Ks.has(i)}(e,t.eid));if(o.length)return Kn(o).map(e=>e.eid);const a=n.filter(e=>e.eid.startsWith("switch."));if(a.length){const t=Kn(a),s=t.find(t=>rn(e,t.eid));if(a.length>1&&s){const t=i.filter(t=>t.eid!==s.eid&&"config"!==t.reg?.entity_category&&null!=on(e,t.eid)),n=t.filter(e=>!e.reg?.entity_category),o=n.length?n:t;for(const t of[0,1,2]){const i=Kn(o.filter(i=>on(e,i.eid)===t));if(i.length)return[i[0].eid,s.eid]}}return[(s||t[0]).eid]}const r=[];for(const e of it)r.push(...Kn(n.filter(t=>t.eid.startsWith(e+"."))));return r.push(...Kn(n.filter(e=>!it.includes(e.eid.split(".")[0])))),r.map(e=>e.eid)}function Jn(e,t,i){const s=t.map(t=>({eid:t,reg:e.entities[t],st:e.states[t]})).filter(e=>e.reg),n=[s.filter(e=>!e.reg.hidden&&!e.reg.entity_category),s.filter(e=>!e.reg.entity_category),s.filter(e=>!e.reg.hidden),s];if("mdi:thermometer"===i||"mdi:air-filter"===i)for(const t of n){const i=t.find(t=>Gn(e,t.eid));if(i)return i.eid}const o=Yn(e,t);return o.find(Ms)||o[0]}function Xn(e,t,i=[]){return[...new Set(Zn(e,t,i))].filter(e=>Ms(e))}function Zn(e,t,i=[]){const s=e?.startsWith("entity:")?new Set([e.slice(7)]):new Set(i);return(t||[]).filter(e=>"string"==typeof e&&!s.has(e))}function Qn(e,t){if("string"==typeof t)return e.area===t;const i=e.marker?.room_id;return i?!!t.id&&i===t.id:!!t.area&&e.area===t.area}function eo(e){const t=e.marker?.binding?.startsWith("entity:")?e.marker.binding.slice(7):null,i=e.entities.filter(Ms),s=e.primary&&Ms(e.primary)?e.primary:null;return[...new Set([t,s,...i].filter(e=>!!e&&Ms(e)))]}function to(e){const t=eo(e),i=e.marker?.light_entity;return i&&t.includes(i)?i:t[0]||null}function io(e,t){if(!1===t.marker?.is_light)return[];if(!0===t.marker?.is_light){return[{eid:to(t),via:"forced"}]}const i=t.primary||Yn(e,t.entities)[0];return i&&!i.startsWith("light.")?[]:t.entities.filter(t=>t.startsWith("light.")&&!!e.states?.[t]).map(e=>({eid:e,via:"light"}))}const so=new WeakMap,no=new WeakMap;function oo(e){return e.map(e=>[e.id||"",!0===e.hidden?1:0,e.primary||"",[...e.entities].join(","),void 0===e.controls?"":[...e.controls].join(","),e.marker?.id||"",e.marker?.binding||"",e.marker?.is_light,e.marker?.tap_action||"",!0===e.marker?.removed?1:0,e.marker?.light_entity||"",null==e.marker?.controls?"":[...e.marker.controls].join(",")].join("")).join("")}function ao(e){const t=oo(e),i=so.get(e);if(i?.fingerprint===t)return i;const s=e.filter(e=>!e.hidden),n=new Map,o=new Map;for(const t of e){o.set(t,Zn(t.marker?.binding,t.marker?.controls??t.controls,t.entities));const e=t.hidden||!0!==t.marker?.is_light?null:t.marker?.id||t.id;e&&n.set(String(e),t)}const a=new Map;for(const t of e){const e=o.get(t)||[],i=new Set(void 0===t.controls?e.filter(Ms):t.controls.filter(Ms)),s=to(t),r=i.size?[...i]:s?[s]:[];for(const i of e){if(!i.startsWith("marker:"))continue;const e=i.slice(7);if(!e||e===String(t.marker?.id||t.id||""))continue;if(!n.has(e))continue;const s=a.get(e)||{markerId:e,controllers:[],driverEids:[]};s.controllers.push({device:t,driverEids:[...r]}),s.driverEids=[...new Set([...s.driverEids,...r])],a.set(e,s)}}const r={fingerprint:t,visible:s,markerById:n,persistedByDevice:o,incomingByMarker:a};return so.set(e,r),r}function ro(e){return ao(e).incomingByMarker}function lo(e,t){const i=Xn(t.marker?.binding,t.controls??t.marker?.controls,t.entities),s=Zn(t.marker?.binding,t.marker?.controls??t.controls,t.entities);return(null!=t.marker?.is_light||!i.length&&!s.some(e=>e.startsWith("marker:")))&&io(e,t).length>0}function co(e,t){const i=Xn(t.marker?.binding,t.controls??t.marker?.controls,t.entities).map(e=>({eid:e,via:"controls"})),s=lo(e,t)?io(e,t):[];for(const e of s)e.eid&&i.some(t=>t.eid===e.eid)||i.push(e);return i}function ho(e,t,i,s){if(null!=i)return ho(e,t,null,s).filter(e=>Qn(e.device,i));const n=oo(t),o=function(e,t){const i=new Set;for(const e of t){for(const t of e.entities)i.add(t);for(const t of e.controls||e.marker?.controls||[])"string"!=typeof t||t.startsWith("marker:")||i.add(t)}return[...i].sort().map(t=>`${t}:${e?.states?.[t]?.state??""}`).join("|")}(e,t),a=vn(s),r=no.get(t);if(r?.graphFingerprint===n&&r.stateFingerprint===o&&r.virtualLightFingerprint===a&&r.registry===e?.entities)return r.sources;const{visible:l,markerById:c,persistedByDevice:h,incomingByMarker:d}=ao(t),p=null==i?l:l.filter(e=>Qn(e,i)),u=new Map,_=new Map;for(const t of p){const i=co(e,t).filter(e=>"controls"!==e.via);for(const s of i){const i=!0===t.marker?.is_light?String(t.marker?.id||t.id||""):"",n=i?`marker:${i}`:`entity:${s.eid}`,o=s.eid?[s.eid]:[],a={key:n,eid:s.eid||"",stateEids:o,serviceEids:o,device:t,via:s.via,castsGlow:!0,passive:!s.eid,on:!s.eid||"on"===e.states?.[s.eid]?.state};u.set(t,[...u.get(t)||[],a]),s.eid&&_.set(s.eid,a)}}const m=new Map;for(const e of t){const t=h.get(e)||[],i=new Set(void 0===e.controls?t.filter(Ms):e.controls);m.set(e,i)}for(const t of u.values())for(const i of t){if(!i.passive)continue;const t=i.key.slice(7),n=d.get(t);i.on=!n||n.driverEids.some(t=>"on"===e.states?.[t]?.state),!n&&bn(i.device.marker)&&(i.on=yn(i.device.marker,s))}const g=new Map,f=e=>{const t=g.get(e.key);(!t||e.castsGlow&&!t.castsGlow)&&g.set(e.key,e)};for(const t of l){const s=h.get(t)||[],n=null==i||Qn(t,i);for(const i of s){if(i.startsWith("marker:")){const e=c.get(i.slice(7)),t=e?u.get(e):null;if(t)for(const e of t)f(e);continue}if(!Ms(i)||!m.get(t)?.has(i))continue;const s=_.get(i);s?f(s):n&&f({key:`entity:${i}`,eid:i,stateEids:[i],serviceEids:[i],device:t,via:"controls",castsGlow:!1,passive:!1,on:"on"===e.states?.[i]?.state})}for(const e of u.get(t)||[])f(e)}const v=[...g.values()];return no.set(t,{graphFingerprint:n,stateFingerprint:o,virtualLightFingerprint:a,registry:e?.entities,sources:v}),v}function po(e){const t=e.filter(e=>e.castsGlow);return t.find(e=>e.on)||t[0]||null}function uo(e){return e.length?e.some(e=>e.on)?"on":"off":"none"}function _o(e,t){const i=[];for(const s of t){const t=e.states[s];if(!t)continue;const n=(t.attributes?.unit_of_measurement||"").toLowerCase();if(/_(linkquality|lqi)$/.test(s)||"lqi"===n){const e=parseFloat(t.state);isNaN(e)||i.push(e);continue}const o=t.attributes?.linkquality??t.attributes?.lqi;if(null!=o){const e=parseFloat(o);isNaN(e)||i.push(e)}}return Ni(i)}function mo(e,t){for(const i of t){if(!Gn(e,i))continue;const t=e.states[i];if(!t)continue;const s=parseFloat(t.state);if(!isNaN(s))return Math.round(10*s)/10}return null}function go(e,t){for(const i of t){if(!i.startsWith("climate."))continue;const t=e.states[i];if(!t||"unavailable"===t.state||"unknown"===t.state)continue;const s=parseFloat(t.attributes?.current_temperature);if(Number.isFinite(s))return Math.round(10*s)/10}return null}function fo(e,t){if(e.entities?.[t]?.entity_category)return!1;const i=e.states[t];if(!i)return/_humidity$/.test(t);const s=i.attributes||{};return"humidity"===s.device_class||"%"===s.unit_of_measurement&&/_humidity$/.test(t)||/_humidity$/.test(t)}function vo(e,t){for(const i of t){if(!fo(e,i))continue;const t=e.states[i];if(!t)continue;const s=parseFloat(t.state);if(!isNaN(s))return Math.round(s)}return null}function bo(e,t){if(!t)return[];const i=[];for(const[t,s]of Object.entries(e.entities)){if(!t.startsWith("light.")||s.hidden||!Nn(s))continue;let n=null;if("group"===s.platform)n=s.area_id||null;else{if(!s.device_id)continue;{const t=e.devices[s.device_id];if(!Nn(t))continue;if("Group"!==t?.model)continue;n=t.area_id||s.area_id||null}}if(!n)continue;const o=e.states[t];i.push({eid:t,name:s.name||o?.attributes?.friendly_name||t,area:n})}return i}function yo(e,t,i,s,n){const o=tt(t,i,n);if(o!==et)return o;const a=[];for(const t of s){const i=e.states[t]?.attributes?.device_class;i&&a.push(i)}return function(e){for(const t of e){const e=Qe[t];if(e)return e}return null}(a)??et}function wo(e){const t=new Set,i=new Set,s=new Set;for(const n of e||[]){const e=String(n.binding||"").indexOf(":");if(e<1)continue;const o=n.binding.slice(0,e),a=n.binding.slice(e+1);a&&(!0===n?.removed?"device"===o?t.add(a):"entity"===o&&i.add(a):"entity"===o&&s.add(a))}return{devices:t,entities:i,liveEntities:s}}function ko(e,t,i){if(i.liveEntities.has(t))return!1;if(i.entities.has(t))return!0;const s=e?.entities?.[t]?.device_id;return!!s&&i.devices.has(s)}function xo(e,t,i,s,n){const o=Xn(t.binding,t.controls,e.entities).filter(e=>!ko(i,e,s)).filter(e=>"active"===Bn(i,"entity:"+e,n).kind);e.marker=t,e.controls=o,e.userHidden=!0===t.hidden,e.hidden=e.userHidden||"ha_disabled"===e.bindingStatus?.kind,t.name&&(e.name=t.name),t.icon&&(e.icon=t.icon),null!=t.model&&(e.model=t.model),e.link=t.link??null,e.description=t.description??null,e.pdfs=t.pdfs||[],e.tapAction=t.tap_action??null}function $o(e,t,i,s){if("string"==typeof e.room_id&&e.room_id.length>0&&null===e.area)return{area:"",space:e.space||s};const n=e.area||t||"";return{area:n,space:n&&i[n]||e.space||s}}function So(e){const t=e.hass,i=e.registry||An(t),s=En(t,i),n=On(t,i),{areaToSpace:o,markers:a,settings:r,excluded:l,showAll:c,firstSpaceId:h,loc:d,iconRules:p}=e,u=!1!==r.group_lights,_=wo(a),m=bo(s,u).filter(e=>!ko(s,e.eid,_)),g=new Set(m.map(e=>e.area)),f=jn(s),v=function(e){const t={};for(const[i,s]of Object.entries(e.entities||{}))s?.device_id&&(t[s.device_id]=t[s.device_id]||[]).push(i);return t}(n),b=qn(a,n),y=new Set;for(const e of a){const[t,i]=e.binding.split(":");"device"!==t&&"entity"!==t||!i||y.add(e.binding)}const w=(e,t)=>a.find(i=>i.binding===e+":"+t),k={},x=[];for(const e of Object.values(s.devices)){const n=e.area_id;if(!n||!o[n])continue;if("service"===e.entry_type)continue;if(y.has("device:"+e.id))continue;const a=Bn(t,"device:"+e.id,i);if("active"!==a.kind)continue;const h=w("device",e.id);if(h&&h.hidden&&!r.filter_seeded)continue;const _=Wn(s,e.id,f[e.id]||[],b);if(_.partial&&!_.entityIds.length)continue;const m=_.entityIds,v=_.partial?{kind:"active",enabledEntityIds:m,allEntityIds:m}:a,$=Un(s,e,m),S=!r.filter_seeded;if(S&&!c){if(l.has($))continue;if("Group"===e.model)continue;if(/scene/i.test(e.model||""))continue;if(/bridge/i.test((e.model||"")+(e.name||"")))continue;if("myheat"===$&&e.via_device_id)continue}const M=(e.name_by_user||e.name||d("device.unnamed")).trim(),C=M+"|"+n;let D=yo(s,M,e.model,m,p);if(m.some(e=>e.startsWith("lock."))&&(D="mdi:lock"),S&&!c&&u&&"mdi:lightbulb"===D&&g.has(n))continue;k[C]=(k[C]||0)+1;const P=k[C]>1?M+" "+k[C]:M,T={id:e.id,name:P,model:e.model||"",area:n,space:o[n],icon:D,entities:m,allEntities:v.allEntityIds,bindingStatus:v,bindingKind:"device",bindingRef:e.id,pdfs:[]};T.primary=Jn(s,m,D),"mdi:thermometer"!==D&&"mdi:air-filter"!==D||(T.temp=mo(s,m)),T.primary&&fo(s,T.primary)&&(T.hum=vo(s,m)),x.push(T)}for(const e of m)o[e.area]&&(y.has("entity:"+e.eid)||x.push({id:"lg_"+e.eid,name:e.name,model:d("device.light_group"),area:e.area,space:o[e.area],icon:"mdi:lightbulb-group",entities:[e.eid],allEntities:[e.eid],bindingStatus:{kind:"active",enabledEntityIds:[e.eid],allEntityIds:[e.eid]},primary:e.eid,bindingKind:"entity",bindingRef:e.eid,pdfs:[]}));for(const e of a){if(e.removed)continue;const[a,l]=e.binding.split(":"),c="device"===a||"entity"===a?Bn(t,e.binding,i):null;if(!e.hidden||r.filter_seeded||"ha_disabled"===c?.kind)if("device"===a){const t=c;if("unverified"===t.kind)continue;const a=n.devices[l],{area:r,space:u}=$o(e,a?.area_id,o,h),m="active"===t.kind?t.enabledEntityIds:[];let g=a?yo(s,a.name_by_user||a.name||"",a.model,m,p):"mdi:help-circle";m.some(e=>e.startsWith("lock."))&&(g="mdi:lock");const f={id:e.id,name:a?.name_by_user||a?.name||d("device.fallback"),model:a?.model||"",area:r,space:u,icon:g,entities:m,allEntities:t.allEntityIds.length?t.allEntityIds:a&&v[a.id]||[],bindingStatus:t,bindingKind:"device",bindingRef:l};f.primary=Jn(s,m,g),"mdi:thermometer"!==g&&"mdi:air-filter"!==g||(f.temp=mo(s,m)),f.primary&&fo(s,f.primary)&&(f.hum=vo(s,m)),xo(f,e,n,_,i),x.push(f)}else if("entity"===a){if(ko(n,l,_))continue;const t=c;if("unverified"===t.kind)continue;const a=n.entities[l],r=a?.area_id||a?.device_id&&n.devices[a.device_id]?.area_id||"",{area:d,space:u}=$o(e,r,o,h),m=s.states[l],g=a?.name||m?.attributes?.friendly_name||l;let f=yo(s,g,"",[l],p);l.startsWith("lock.")&&(f="mdi:lock");const v={id:e.id,name:g,model:"",area:d,space:u,icon:f,entities:"active"===t.kind?[l]:[],allEntities:[l],bindingStatus:t,primary:"active"===t.kind?l:void 0,bindingKind:"entity",bindingRef:l};"mdi:thermometer"!==f&&"mdi:air-filter"!==f||!v.entities.length||(v.temp=mo(s,v.entities)),v.entities.length&&fo(s,l)&&(v.hum=vo(s,v.entities)),xo(v,e,n,_,i),x.push(v)}else{const t=e.area||"",s=e.space||t&&o[t]||h,a={id:e.id,name:e.name||d("device.virtual"),model:e.model||"",area:t,space:s,icon:e.icon||"mdi:map-marker",entities:[],allEntities:[],bindingStatus:{kind:"active",enabledEntityIds:[],allEntityIds:[]},bindingKind:"virtual",virtual:!0};xo(a,e,n,_,i),x.push(a)}}return x}function Mo(e,t,i,s){if(!t)return null;if(function(e,t,i){if(!t)return!1;const s=t.indexOf(":");if(s<1)return!1;const n=t.slice(0,s),o=t.slice(s+1),a=wo(i);return"device"===n?a.devices.has(o):"entity"===n&&ko(e,o,a)}(e,t,s))return null;const n=t.indexOf(":");if(n<0)return null;const o=t.slice(0,n),a=t.slice(n+1);if(!a)return null;if("entity"===o){const t=parseFloat(e.states[a]?.state);return Number.isFinite(t)?"temp"===i?Math.round(10*t)/10:Math.round(t):null}if("device"===o){const t=Object.entries(e.entities).filter(([,e])=>e.device_id===a).map(([e])=>e);return"temp"===i?mo(e,t):vo(e,t)}return null}const Co=new RegExp(["water","voda","coolant","flow_?temp","return_?temp","target","setpoint","chip","cpu","processor","board","core_temp","device_temp","batter","akkum","freezer","fridge","oven","kettle","boiler"].join("|"),"i");function Do(e,t,i){const s=[];for(const n of t){if(n.area!==i||n.virtual)continue;const t=_o(e,n.entities);null!=t&&s.push(t)}return Ni(s)}function Po(e,t,i){const s=[];for(const n of t){if(n.area!==i)continue;if("mdi:thermometer"!==n.icon&&"mdi:air-filter"!==n.icon)continue;const t=mo(e,n.entities);null!=t&&s.push(t)}return s.length?Math.round(s.reduce((e,t)=>e+t,0)/s.length*10)/10:null}const To={climate:["current_temperature","temperature","current_humidity","humidity"],water_heater:["current_temperature","temperature"],cover:["current_position"],valve:["current_position"],fan:["percentage"],humidifier:["current_humidity","humidity"],light:["brightness"],media_player:["volume_level"],vacuum:["battery_level","fan_speed"],lawn_mower:["battery_level","fan_speed"]};function Io(e){return e?e.sourceLabel?`${e.sourceLabel}: ${e.fullText}`:e.fullText:""}const Ro=new WeakMap;function Ao(e){return e?"entity_state"===e.kind?`state:${e.entity_id}`:"entity_attribute"===e.kind?`attr:${e.entity_id}:${e.attribute}`:"derived_marker_state"===e.kind?`marker:${e.ref}`:"derived:lqi":""}function zo(e){if("derived:lqi"===e)return{kind:"derived_lqi"};if(e.startsWith("state:"))return{kind:"entity_state",entity_id:e.slice(6)};if(e.startsWith("attr:")){const t=e.slice(5),i=t.lastIndexOf(":");if(i>0)return{kind:"entity_attribute",entity_id:t.slice(0,i),attribute:t.slice(i+1)}}return e.startsWith("marker:marker:")?{kind:"derived_marker_state",ref:e.slice(7)}:null}function Fo(e,t){const i=e?.entities?.[t],s=e?.states?.[t];return String(i?.name||i?.original_name||s?.attributes?.friendly_name||t)}function No(e){return e.replaceAll("_"," ")}function Eo(e,t=1){const i=Number(e);return Number.isFinite(i)?`${Math.round(i*t)} %`:null}function Oo(e){if("derived_lqi"===e.kind)return"lqi";if("entity_attribute"===e.kind){if(e.attribute.includes("temperature"))return"temperature";if(e.attribute.includes("humidity"))return"humidity"}return"default"}function Ho(e,t,i,s=[]){let n=null,o="",a="available";if("entity_state"===i.kind){o=Fo(e,i.entity_id);const t=e?.states?.[i.entity_id];if(t)if(Bo(e,i.entity_id))if(["unknown","unavailable"].includes(String(t.state).toLowerCase()))a="unavailable";else{const s=Xi(e,i.entity_id);n=s?Zi(s,String(t.attributes?.unit_of_measurement||"")):null,n||(a="unavailable")}else a="unavailable";else a="missing"}else if("entity_attribute"===i.kind){o=`${No(i.attribute)} · ${Fo(e,i.entity_id)}`;const t=e?.states?.[i.entity_id];t?Bo(e,i.entity_id)?(n=function(e,t,i){const s=e?.states?.[t];if(!s||!(i in(s.attributes||{})))return null;const n=Xi(e,t,i);if(n?.formatted)return n.text;const o=s.attributes?.[i];if("brightness"===i)return Eo(o,100/255);if("volume_level"===i)return Eo(o,100);if(["current_position","percentage","current_humidity","humidity","battery_level"].includes(i))return Eo(o);if("current_temperature"===i||"temperature"===i){const t=Number(o);if(!Number.isFinite(t))return null;const i=String(e?.config?.unit_system?.temperature||"°C");return`${Math.round(10*t)/10} ${i}`}return n?.text||(["string","number","boolean"].includes(typeof o)?String(o):null)}(e,i.entity_id,i.attribute),null==n&&(a="unavailable")):a="unavailable":a="missing"}else if("derived_lqi"===i.kind){o="LQI";const i=_o(e,t.entities);null==i?a="unavailable":n=String(i)}else{o="Light state";const t=s.find(e=>e.ref===i.ref);if(t){const i=t.on?"on":"off",s=e?.localize?.(`state.default.${i}`);n="string"==typeof s&&s?s:i,o=t.name||o}else a="missing"}const r=e?.localize?.("state.default.unavailable")||"Unavailable";return{source:i,sourceLabel:o,text:n??"—",fullText:n??r,availability:a,isLqi:"derived_lqi"===i.kind,tone:Oo(i)}}function Lo(e,t){if(!0===t.marker?.use_climate_temp){const i=t.entities.find(t=>t.startsWith("climate.")&&Number.isFinite(Number(e?.states?.[t]?.attributes?.current_temperature)));return i?{kind:"entity_attribute",entity_id:i,attribute:"current_temperature"}:null}if("mdi:thermometer"===t.icon||"mdi:air-filter"===t.icon){const i=t.entities.find(t=>Gn(e,t)&&Number.isFinite(Number(e?.states?.[t]?.state)));if(i)return{kind:"entity_state",entity_id:i}}return t.primary&&fo(e,t.primary)&&Number.isFinite(Number(e?.states?.[t.primary]?.state))?{kind:"entity_state",entity_id:t.primary}:null}function Bo(e,t){const i=e?.entities?.[t];if(i&&!Nn(i))return!1;const s=i?.device_id?e?.devices?.[i.device_id]:null;return!s||Nn(s)}function jo(e,t){return Bo(e,t)&&!!e?.states?.[t]}function qo(e,t,i=[t]){const s=function(e,t){const i=e=>[e.id,e.primary,e.hidden?1:0,e.userHidden?1:0,e.entities.join(","),(e.controls||[]).join(","),e.marker?.binding||"",!0===e.marker?.is_light?1:0,e.marker?.light_entity||"",(e.marker?.controls||[]).join(",")].join("|");return`${i(e)}\n${t.map(i).join("\n")}`}(t,i),n=Ro.get(t);if(n&&n.states===(e?.states||null)&&n.entities===(e?.entities||null)&&n.devices===(e?.devices||null)&&n.signature===s)return n.result;const o=new Set(t.entities);for(const e of t.controls||[])e.startsWith("marker:")||o.add(e);const a=[],r=ho(e,i),l=r.filter(e=>e.key.startsWith("marker:")).map(e=>({ref:e.key,on:e.on,name:e.device?.name||e.key})),c=i=>{const s=Ao(i);if(a.some(e=>e.key===s))return;const n=Ho(e,t,i,l),o="entity_attribute"===i.kind?`${i.entity_id} · ${i.attribute}`:"entity_state"===i.kind?i.entity_id:"derived_marker_state"===i.kind?i.ref:"LQI";a.push({key:s,source:i,label:n.sourceLabel,technical:o,value:n.text,available:"available"===n.availability})};for(const t of[...o].sort()){if(!jo(e,t))continue;const i=t.split(".")[0],s=e?.entities?.[t];if("button"===i||"event"===i||"config"===s?.entity_category)continue;c({kind:"entity_state",entity_id:t});const n=e?.states?.[t];for(const e of To[i]||[])e in(n?.attributes||{})&&c({kind:"entity_attribute",entity_id:t,attribute:e})}const h=new Set((t.marker?.controls||t.controls||[]).filter(e=>e.startsWith("marker:")));!0===t.marker?.is_light&&t.marker.id&&h.add(`marker:${t.marker.id}`);const d=new Set(r.map(e=>e.key));for(const e of[...h].sort())d.has(e)&&c({kind:"derived_marker_state",ref:e});return t.virtual||null==_o(e,t.entities)||c({kind:"derived_lqi"}),Ro.set(t,{states:e?.states||null,entities:e?.entities||null,devices:e?.devices||null,signature:s,result:a}),a}function Wo(e,t,i){const s=Lo(e,t);if(s&&i.some(e=>e.key===Ao(s)))return s;const n=t.primary&&i.find(e=>e.key===`state:${t.primary}`);if(n)return n.source;const o=[e=>e.technical.includes("temperature"),e=>e.technical.includes("humidity"),e=>e.technical.includes("battery")];for(const e of o){const t=i.find(e);if(t)return t.source}return i.find(e=>"derived_lqi"!==e.source.kind)?.source||i[0]?.source||null}function Uo(e){return"boolean"==typeof e?.themes?.darkMode?e.themes.darkMode?"theme-dark":"theme-light":""}function Go(e){const t=[];if(1!==e.scale&&t.push(`--dev-scale:${e.scale}`),"none"!==e.pulse.kind){t.push(`--ripple-scale:${e.pulse.diameterScale}`);const n=(i=e.pulse.color,s=null,"string"==typeof i&&(Te.test(i)||Ae.test(i))?i:s);n&&t.push(`--ripple-color:${n}`)}var i,s;return t}function Vo(e){const t=[...String(e)].reduce((e,t)=>/\s/.test(t)?e+.35:e+(t.codePointAt(0)>255?1:.62),0);return t<=8?.45:Math.max(.25,Math.round(3.6/t*1e3)/1e3)}function Ko(e,t){const i=e.pulse,s=i.generation%2==0,n=function(e){const t=e.valueBadge;if(!t||!1!==t.configured)return[];const i=[];return null!=e.tempText&&"temperature"!==t.tone&&i.push({kind:"temperature",text:e.tempText,suffix:"°"}),null!=e.humText&&"humidity"!==t.tone&&i.push({kind:"humidity",text:e.humText,suffix:"%"}),i}(e),o=e.valueBadge,a=o?.position||"right",r=!!o||n.length>0,l=["device-shell",null!=e.valueText?"text-shell":"",r?`with-values pos-${a}`:"",n.length?"with-legacy":""].filter(Boolean).join(" ");return q` - ${"none"!==i.kind&&"dot"!==i.reducedMotionIndicator?q``:G} - ${"dot"===i.reducedMotionIndicator?q``:G} - ${t.newDevice?q``:G} - ${e.haDisabled?q``:G} - - ${null!=e.lqiText?q`${e.lqiText}`:G} - `}function Yo(e,t,i){return{kind:"none",reason:"none",generation:e,expiresAt:null,color:t,diameterScale:i,animated:!1,reducedMotionIndicator:"none"}}const Jo=new WeakMap;function Xo(e){const t=function(e){const t=e?.connection||e;return!t||"object"!=typeof t&&"function"!=typeof t?null:t}(e);if(!t)return null;let i=Jo.get(t);return i||(i={revision:0,loaded:!1,configEntries:{},manifests:{},listeners:new Set,refs:0},Jo.set(t,i)),i}function Zo(e){e.revision++;for(const t of[...e.listeners])try{t()}catch{}}function Qo(e,t){const i=Xo(e);if(!i)return()=>{};i.refs++,i.listeners.add(t),async function(e,t){t.loading||"function"!=typeof e?.callWS?t.loading:t.loaded&&t.loadedAt&&Date.now()-t.loadedAt<3e5||(t.loading=(async()=>{const[i,s]=await Promise.allSettled([e.callWS({type:"config_entries/get"}),e.callWS({type:"manifest/list"})]);"fulfilled"===i.status&&Array.isArray(i.value)&&(t.configEntries=Object.fromEntries(i.value.filter(e=>"string"==typeof e?.entry_id).map(e=>[e.entry_id,e]))),"fulfilled"===s.status&&Array.isArray(s.value)&&(t.manifests=Object.fromEntries(s.value.filter(e=>"string"==typeof e?.domain).map(e=>[e.domain,e]))),t.loaded=!0,t.loadedAt=Date.now(),t.loading=void 0,Zo(t)})().catch(()=>{t.loaded=!0,t.loadedAt=Date.now(),t.loading=void 0,Zo(t)}),t.loading)}(e,i),async function(e,t){if(t.unsubscribe||t.subscribing)return t.subscribing;const i=e?.connection?.subscribeMessage;"function"==typeof i&&(t.subscribing=(async()=>{try{t.unsubscribe=await i.call(e.connection,e=>{const i=Array.isArray(e)?e:Array.isArray(e?.entries)?e.entries:e?[e]:[];let s=!1;const n={...t.configEntries};for(const e of i){const t=e?.entry||e?.data?.entry||("string"==typeof e?.entry_id?e:null),i=t?.entry_id;if("string"!=typeof i)continue;const o=e?.type||e?.action||e?.data?.action;"removed"===o||"remove"===o?delete n[i]:n[i]=t,s=!0}s&&(t.configEntries=n,Zo(t))},{type:"config_entries/subscribe"})}catch{}finally{0===t.refs&&(t.unsubscribe?.(),t.unsubscribe=void 0),t.subscribing=void 0}})(),t.subscribing)}(e,i);let s=!1;return()=>{s||(s=!0,i.listeners.delete(t),i.refs=Math.max(0,i.refs-1),i.refs||(i.unsubscribe?.(),i.unsubscribe=void 0))}}function ea(e){const t=Xo(e);return t?{revision:t.revision,loaded:t.loaded,configEntries:t.configEntries,manifests:t.manifests}:{revision:0,loaded:!1,configEntries:{},manifests:{}}}function ta(e,t,i=ea(e),s){const n=i.manifests[t];return function(e,t){if("function"!=typeof e?.localize)return"";try{const i=`component.${t}.title`,s=e.localize(i);return"string"==typeof s&&s!==i?s:""}catch{return""}}(e,t)||String(n?.name||"")||String(s||"")||t.replace(/_/g," ")}function ia(e,t,i,s,n){const o=String(t||"").trim();if(!o)return null;const a=n?s.configEntries[n]:null;return{domain:o,label:ta(e,o,s,a?.title),configEntryId:n,configEntryTitle:a?.title,confidence:i}}function sa(e){const t={"registry-owner":0,"entity-platform":1,"identifier-fallback":2},i=new Set;return e.filter(e=>!!e).sort((e,i)=>t[e.confidence]-t[i.confidence]||e.label.localeCompare(i.label)||e.domain.localeCompare(i.domain)).filter(e=>{const t=`${e.domain}\n${e.configEntryId||""}`;return!i.has(t)&&(i.add(t),!0)})}function na(e,t,i,s=ea(e)){const n=i.entities?.[t]||e?.entities?.[t];if(!n)return null;const o=n.config_entry_id||void 0,a=o?s.configEntries[o]?.domain:"";return ia(e,n.platform||a,"entity-platform",s,o)}var oa={"editor.context_actions":"Actions: {object}","editor.tool_options":"Tool options: {tool}","editor.palette":"Palette: {tool}","editor.open_group":"Tool group: {group}","editor.group_active":"{group} — active: {item}","editor.disabled_action":"{action} is unavailable: {reason}","color_picker.title":"Color picker","color_picker.hue":"Hue","color_picker.saturation":"Saturation","color_picker.value":"Brightness","color_picker.hex":"Hex color","color_picker.invalid_hex":"Enter a 3- or 6-digit hex color","btn.properties":"Properties","btn.keep_as_walls":"Keep as walls","btn.delete_room_keep_walls":"Delete room, keep walls","btn.delete_room_with_walls":"Delete room and walls","title.markup_select":"Select and edit walls, columns and saved outlines","title.markup_column":"Column: click a grid point to place a square column","markup.partition":"Partition","markup.column":"Column","markup.hint_column":"click a grid point to place a square column","history.draft_segment":"Add room-draft segment","history.draft_merge":"Join unfinished room outlines","history.draft_segment_delete":"Remove room-draft segment","history.partition_add":"Add partition","history.column_add":"Add column","history.physical_edit":"Change physical object","history.physical_delete":"Delete physical object","history.physical_move":"Move physical object","history.contour_to_partitions":"Convert contour to closed walls","history.wall_chain_finish":"Finish wall chain","history.wall_face_batch":"Create rooms from walls","toast.column_duplicate":"A column with the same centre and outer size already exists","confirm.delete_draft":"Delete the whole unfinished room contour?","confirm.delete_draft_segment":"Delete this draft segment? The remaining contour may split in two.","physical.partition_properties":"Partition properties","physical.column_properties":"Column properties","physical.draft_properties":"Draft segment properties","physical.shape":"Shape","physical.square":"Square","physical.circle":"Circle","physical.diameter":"Diameter","physical.side":"Side","physical.rotation":"Rotation angle","physical.length":"Length","physical.allowed_range":"Allowed: {min}–{max} {unit}","physical.delete_segment":"Delete segment","physical.delete_draft":"Delete entire outline","physical.column_size_title":"Outer side of the square column placed by the click (1–150 cm).","card.title":"House plan","count.devices":"{n} dev.","empty.no_spaces":"No spaces yet.","empty.add_first":"Add the first space and upload a floor plan.","empty.install":'Install the House Plan integration and add it in "Devices & services".',"btn.add_space":"Add space","btn.cancel":"Cancel","btn.save":"Save","btn.close":"Close","btn.delete":"Delete","btn.edit":"Edit","btn.open_in_ha":"Open in HA","btn.reset":"Reset","btn.attach":"Attach…","btn.upload":"Upload…","btn.replace":"Replace…","title.zoom_in":"Zoom in","title.zoom_out":"Zoom out","title.zoom_fit":"Fit all","title.add_device":"Add a device to the plan","title.show_all":"Show hidden and disabled devices as service markers (this tab only)","title.markup":"Room markup: grid, lines, outlines","title.configure_space":"Configure space","title.add_space":"Add space","title.markup_add":"Walls: draw a continuous chain; Shift locks to 45°. Changing tool or leaving the editor finishes it as independent walls","title.markup_merge":"Merge: click one room, then the neighbour it shares a wall with","title.markup_split":"Split a room: click the room, then two points on its walls","title.markup_delroom":"Delete room: click inside a room and confirm","title.need_plan":"Upload a floor-plan image","markup.add":"Walls","markup.merge":"Merge","markup.split":"Split","markup.resize":"Resize","title.markup_resize":"Resize rooms: drag an available horizontal or vertical wall handle","markup.hint_resize":"drag an available wall handle · the wall stops at the first unsafe position · Esc cancels · Ctrl+Z — undo","resize.disabled.diagonal":"This wall is angled; Resize supports only horizontal and vertical walls","resize.disabled.side-angle":"An adjoining wall is angled; Resize requires both adjoining walls to meet this wall at right angles","resize.disabled.duplicate-physical-wall":"A separate partition, unfinished outline, or column overlaps this wall; remove or move it before resizing","resize.disabled.partial-shared":"A neighbouring room uses only part of this wall, so it cannot be moved safely as one shared wall","resize.disabled.unequal-shared":"The neighbouring room uses different endpoints or wall length, so the walls cannot move together safely","resize.disabled.multiple-rooms":"Moving this wall would affect more than two rooms","resize.disabled.thickness-conflict":"The wall thickness cannot be preserved safely","resize.disabled.opening-conflict":"An opening on this wall cannot be moved safely","resize.disabled.invalid-geometry":"This wall cannot be resized without changing the plan structure","resize.limit_stopped":"Wall stopped: the next step would break a junction limit","resize.commit_failed":"Resize was cancelled because the final plan did not pass the geometry check","resize.preview_failed":"Resize stopped at the last safe position because the plan geometry could not be preserved","markup.opening":"Opening","markup.delete_room":"Delete room","history.undo":"Undo","history.redo":"Redo","history.undo_named":"Undo: {name} (Ctrl+Z)","history.redo_named":"Redo: {name} (Ctrl+Shift+Z / Ctrl+Y)","history.undo_empty":"Nothing to undo","history.redo_empty":"Nothing to redo","history.undone":"Undone: {name}","history.redone":"Redone: {name}","history.add_room":"Create room","history.split_room":"Split room","history.merge_rooms":"Merge rooms","history.resize_room":"Resize room","history.wall_thickness":"Change wall thickness","history.add_opening":"Add door or window opening","history.edit_opening":"Edit opening","history.move_opening":"Move opening","history.delete_opening":"Delete opening","history.delete_room":"Delete room","history.delete_room_keep_walls":"Delete room, keep walls","history.delete_room_with_walls":"Delete room and walls","history.decor_add":"Add decor object","history.decor_edit":"Edit decor object","history.decor_move":"Move decor object","history.decor_transform":"Transform decor object","history.decor_delete":"Delete decor object","history.backdrop_transform":"Transform plan backdrop","title.markup_opening":"Openings: click a wall to place, click an opening to edit","opening.new":"New opening","opening.edit":"Opening","opening.door":"Door","opening.window":"Window","opening.gate":"Gate","opening.passage":"Open passage","opening.passage_binding_warning":"Saving will remove the open/close sensor and lock.","opening.invalid_passage_fields":"The open passage on plan “{room}” has unsupported settings: {fields}.","opening.type_label":"Type","opening.length_label":"Length, cm","opening.contact_label":"Open/close sensor","opening.lock_label":"Lock","opening.none":"— none —","opening.search_ph":"Search: name or entity id…","opening.invert":"Invert open/closed","opening.flip_h":"Hinge on the other jamb","opening.flip_v":"Opens to the other side","opening.open":"Open","opening.closed":"Closed","opening.locked":"Locked","opening.unlocked":"Unlocked","opening.state_unknown":"unavailable","opening.no_entities":"No sensors bound — a static symbol on the plan.","toast.opening_no_wall":"Click next to a room wall or independent wall","opening.host_partition":"Independent wall","opening.partition_orphan":"The independent wall for this opening no longer exists","opening.partition_jamb_margin":"Leave at least {distance} between the opening and the end of the wall","opening.rebind_partition":"Attach to another independent wall","confirm.delete_partition_openings_title":"Delete wall and openings?","confirm.delete_partition_openings_body":"This wall contains {count} opening(s). They will be deleted together.","confirm.delete_partition_openings_item":"• {type}, {length}","markup.delete":"Delete","markup.hint_points":"points: {n} · Shift — 45° steps · Esc — finish chain · Ctrl+Z — undo a point · closing an area offers a room","markup.hint_start":"click a grid dot to start a wall chain","tip.lqi":"average zigbee signal:","tip.area":"area: {value}","info.device_header":"Device on the plan","info.model":"Model","info.state":"State","info.link":"Link","info.manuals":"Manuals","info.none":"No additional information","marker.new_device":"New device","marker.name_label":"Name (shown on the plan)","marker.name_ph":"Name","marker.binding_label":"Bind to an HA device","marker.binding_disabled":"disabled in Home Assistant","marker.virtual_option":"Virtual device (no binding)","marker.search_ph":"Search device / group…","marker.nothing_found":"nothing found","marker.room_label":"Room","marker.room_override":" (override placement)","marker.room_choose":"— select a room —","marker.room_auto":"— by device area (auto) —","marker.icon_label":"Icon","marker.icon_ph":"mdi:… (empty = auto)","marker.display_label":"Display","display.badge":"Icon + state","display.icon_ripple":"Icon + state and activity","display.static_icon":"Always static icon","marker.display_hint":"Icon + state changes the plate for work, open, alarm and unavailable states. Icon + state and activity additionally shows a short pulse for events and a continuous pulse for ongoing work, movement or presence. Value + state replaces the icon with one unambiguous HA value. A static icon never reacts to device states.","marker.display_hint_badge":"The icon and dynamic plate show device state without the ordinary activity pulse. Red alarms remain visible.","marker.display_hint_icon":"The icon and dynamic plate show device state without the ordinary activity pulse. Red alarms remain visible.","marker.display_hint_icon_ripple":"The icon, dynamic plate and pulse show a short pulse for events and a continuous pulse for work, motion or presence. Red alarms have separate priority.","marker.display_hint_value":"One unambiguous Home Assistant value replaces the icon while the plate continues to show state. Red alarms remain visible.","marker.display_hint_static_icon":"The theme-aware shell and icon always stay the same. State, activity, unavailability and alarms do not change the face.","marker.static_alarm_warning":"This device can report alarms. Static display hides the marker's visual alarm indication.","marker.preview.title":"Display preview","marker.preview.actual":"Now","marker.preview.example":"Example","marker.preview.integration":"Provided by","marker.preview.source":"Display source","marker.preview.current_state":"Current state","marker.preview.result":"On the plan","marker.preview.details":"Source details","marker.preview.select_source":"Choose a Home Assistant device or entity to see its actual display.","marker.preview.unknown_provider":"Unknown integration","marker.preview.virtual_provider":"House Plan · virtual device","marker.preview.no_source":"No active source","marker.preview.no_state":"No current state","marker.preview.mixed_states":"Several different states","marker.preview.multiple_sources":"{n} sources","marker.preview.more_sources":"+{n} more","marker.preview.scaled":"The preview is scaled to {n}% to fit. The saved size is unchanged.","marker.preview.demo_activity":"Show activity example","marker.preview.demo_notice":"Temporary activity example; the actual device state is unchanged.","marker.preview.demo_short":"Show short activity","marker.preview.demo_continuous":"Show continuous activity","marker.preview.stop_continuous":"Stop continuous activity","marker.preview.demo_short_notice":"Short activity example; the actual device state is unchanged.","marker.preview.demo_continuous_notice":"Continuous activity example; the actual device state is unchanged.","marker.preview.demo_already_visible":"The current state already shows a real activity or alarm effect.","marker.preview.reduced_motion":"System reduced-motion is enabled, so ordinary activity is shown as a dot.","marker.preview.reason.neutral":"Neutral dark plate","marker.preview.reason.working":"Yellow plate: the device is working now","marker.preview.reason.working_activity":"Yellow plate and activity effect: the device is working now","marker.preview.reason.open":"Orange plate: physically open or unlocked","marker.preview.reason.cover_icon_state":"Neutral plate; the cover entity controls the displayed state and icon","marker.preview.reason.presence":"Activity effect while presence is detected","marker.preview.reason.event":"Short activity effect after a detected event","marker.preview.reason.transition":"Activity effect while the device is moving or changing state","marker.preview.reason.media_neutral":"Media devices stay on a neutral dark plate while available","marker.preview.reason.unavailable":"Subdued neutral plate: unavailable or off","marker.preview.reason.alarm":"Red alarm plate; alarm indication is always shown","marker.preview.reason.live_states_disabled":"Live-state styling is disabled for this card","marker.preview.reason.value_no_state":"No usable state value; the icon is shown instead","marker.preview.reason.value_ambiguous_sources":"Several possible value sources; the icon is shown instead","marker.preview.reason.value_non_scalar":"The source did not return a simple value; the icon is shown instead","marker.preview.reason.value_virtual":"A virtual device has no Home Assistant value; the icon is shown instead","marker.preview.reason.vacuum_live_plan_only":"Live vacuum position and trail are available only on the full plan","marker.preview.reason.hidden_design_preview":"The device is hidden on the plan; this design preview remains visible","marker.preview.reason.composite_power_source":"State comes from the device Power entity; auxiliary switches are ignored","marker.preview.reason.activity_display_disabled":"The selected display mode does not show ordinary activity effects","marker.preview.reason.ha_disabled":"The Home Assistant binding is disabled and will be hidden on the plan","marker.preview.reason.orphaned":"The saved Home Assistant binding can no longer be found","marker.preview.reason.static_icon":"Static mode: device state does not change the icon","marker.activity_color":"Activity pulse color","marker.ripple_size":"Activity pulse size","marker.activity_alarm_note":"Color and size affect ordinary activity only and do not change red alarms.","marker.pulse_a11y_alarm":"Alarm","marker.pulse_a11y_event":"Recent event","marker.pulse_a11y_presence":"Presence detected","marker.pulse_a11y_transition":"Changing state","marker.pulse_a11y_running":"Working","marker.state_a11y_neutral":"Default state","marker.state_a11y_working":"Working","marker.state_a11y_open":"Open","marker.state_a11y_locked":"Locked","marker.state_a11y_unlocked":"Unlocked","marker.state_a11y_alarm":"Alarm","marker.state_a11y_unavailable":"Unavailable","marker.lqi_a11y_low":"LQI {value}, low signal","marker.lqi_a11y_mid":"LQI {value}, medium signal","marker.lqi_a11y_high":"LQI {value}, high signal","marker.size_label":"Icon size / rotation","marker.angle_label":"Rotate","marker.model_label":"Model","marker.model_ph":"e.g. Aqara T&H","marker.link_label":"Link","marker.desc_label":"Description","marker.desc_ph":"Notes, specs…","marker.manuals_label":"Manuals (PDF etc.)","marker.sub_device":"device","marker.sub_z2m_group":" · Z2M group","marker.sub_group":"group","marker.sub_helper":"helper","space.new":"New space","space.header":"Space","space.title_label":"Title","space.title_ph":"e.g. Garage","space.plan_label":"Floor plan (background)","space.no_plan":"no plan image","space.plan_alt":"plan","room.new":"New room","room.name_label":"Display name","room.name_ph":"e.g. Terrace","room.area_label":"Home Assistant area (unassigned)","room.no_area_option":"— no area —","room.default_name":"Room","device.unnamed":"unnamed","device.light_group":"light group","device.fallback":"device","device.virtual":"virtual device","confirm.delete_room":'Delete room "{name}"?',"confirm.delete_room_title":'Delete room "{name}"?',"confirm.delete_room_body":"Choose whether the room's exclusive physical walls should remain. Walls shared with another room are always kept.","confirm.merge_rooms":'Merge rooms "{a}" and "{b}"?',"confirm.remove_marker":'Delete "{name}" from the plan? The device will disappear completely and stop contributing to plan data. You can add it again later.',"confirm.erase_decor":"Erase the {kind} object? You can undo this action from the editor history.","confirm.delete_space":'Delete space "{title}" with all its rooms and markup?',"space.delete_blocked":"This space is still used by {n} device(s). Move them to another space or delete them first.","toast.pos_save_failed":"Failed to save position: {err}","toast.no_entity":"The device has no suitable entity","toast.ha_disabled_action":"A Home Assistant object that is disabled cannot be used on the plan.","toast.ha_disabled_show_device":"A device disabled in Home Assistant cannot be shown on the plan. Enable it in Home Assistant first.","toast.ha_disabled_show_entity":"An entity disabled in Home Assistant cannot be shown on the plan. Enable it in Home Assistant first.","toast.ha_disabled_add":"A disabled Home Assistant object cannot be added to the plan. Enable it in Home Assistant first.","toast.ha_binding_unverified":"The object status could not be verified through the Home Assistant registry. Display and actions are temporarily unavailable.","toast.markup_needs_server":"Markup is available after the config is moved to the server","toast.space_order_changed":"Order changed. If any card pins its floor by number, check those panels.","toast.conflict":"Config was changed in another window — data refreshed, repeat your last action","toast.cfg_save_failed":"Failed to save config: {err}","toast.room_overlap":"The outline overlaps room “{name}” — rooms must not overlap","toast.contour_min_edges":"Draw at least two edges before closing the outline","toast.contour_cannot_close":"The outline cannot close because it is degenerate or intersects itself","toast.merge_not_adjacent":"Only rooms that share a wall can be merged","toast.rooms_merged":"Rooms merged into “{name}”","toast.split_pick_wall":"Start the cut on the room’s wall","toast.split_bad_cut":"The cut must run wall to wall inside the room, without crossing walls or itself","merge.header":"Merge rooms","merge.hint":"The merged room keeps one name and one area. The other area is released — its devices leave the plan until another room claims it.","merge.keep":"Keep","merge.no_area":"no area","room.split_header":"New room from the split","toast.room_saved":"Room saved ({n}). Devices added: {added}. Outline the next one or exit markup.","toast.room_saved_no_area":"Room saved ({n}, no area). Outline the next one or exit markup.","toast.marker_needs_server":"Device editing is available after the config is moved to the server","toast.virtual_name_required":"Enter a name for the virtual device","toast.marker_saved":"Device saved","toast.marker_removed":"Device deleted from the plan","toast.integration_missing":"The House Plan integration is not installed — management unavailable","toast.plan_formats":"Supported formats: SVG, PNG, JPG, WebP","toast.plan_required":"Upload a floor plan — it is required","toast.space_added_onboard":"Space added. Outline the rooms: click grid dots and close the contour.","toast.space_added":"Space added","toast.space_saved":"Space saved","toast.space_deleted":"Space deleted","toast.delete_failed":"Delete failed: {err}","toast.error":"Error: {err}","toast.toggle_target_unavailable":"Target “{name}” is unavailable — no action was performed","toast.toggle_targets_unavailable":"Targets are unavailable: {names}. No action was performed","toast.file_failed":'File "{name}" was not uploaded: {err}',"toast.files_attached":"Files attached: {n}","err.unknown":"unknown error","err.code":"code {code}","err.too_large":"file larger than {mb} MB","err.bad_ext":"unsupported type (PDF/image expected)","err.unauthorized":"administrator rights required","editor.title":"Title","editor.default_floor":"Initial space","editor.default_floor_missing":"Initial space “{id}” no longer exists. Choose another space.","editor.floor":"Fixed space","editor.floor_none":"— not fixed —","editor.floor_index":"YAML index {index} (preserved)","fixed_floor.loading":"Loading the fixed space…","fixed_floor.invalid_title":"The fixed space is unavailable","fixed_floor.invalid_body":"Check floor in the card configuration. The configured value is: {value}","editor.icon_size":"Icon size, % of plan width","editor.show_temperature":"Show temperature","editor.live_states":"Live states (on/off, open…)","editor.show_signal":"Show zigbee signal (LQI)","editor.language":"Interface language","editor.lang_auto":"Auto (HA profile)","editor.lang_en":"English","editor.lang_ru":"Русский","title.icon_rules":"Icon rules: which MDI icon devices get by name","rules.title":"Icon rules","rules.hint":"Rules are checked top-down against “device name + model” (case-insensitive regex); the first match wins. When nothing matches, the entity device class decides, then the generic chip icon.","rules.pattern_ph":"regex, e.g. plug|socket","rules.icon_ph":"mdi:power-socket-de","rules.add":"Add rule","rules.reset":"Reset to defaults","rules.test_ph":"Try a device name…","rules.invalid":"invalid regex","rules.saved":"Icon rules saved","btn.up":"Up","btn.down":"Down","tap.info":"Device card","tap.more_info":"HA more-info dialog","tap.toggle":"Toggle state","marker.tap_label":"Tap action for this device","tap.toggle_note":"The resolved state is shown below. Secure devices are never toggled from the plan.","import.title":"Create spaces from HA floors","import.hint":"Your Home Assistant already knows these floors. Pick the ones to turn into plan spaces — you will upload a floor-plan image for each one next. Rooms are then outlined by hand on the plan.","import.start":"Create {n} space(s)","import.manual":"Start from scratch","import.progress":"Floor {i} of {n}","import.done":"Spaces created. Outline the rooms: click grid dots and close the contour.","btn.skip":"Skip","space.scale_label":"Scale (grid cell size)","space.scale_unit":"cm per cell","space.scale_unit_imperial":"in per cell","space.display_section":"Display","space.show_borders":"Always show room borders","space.zero_wall_style":"Zero-thickness walls","space.zero_wall_dashed":"Dashed","space.zero_wall_solid":"Solid","space.zero_wall_help":"Dashed walls let light through. Solid walls block light even at zero thickness.","space.show_names":"Show room names (drag to move)","space.room_color":"Border & name color","space.opacity":"Opacity","space.fill_label":"Room fill","fill.none":"None","fill.lqi":"Zigbee signal","fill.light":"Lights","fill.custom":"Custom color","space.custom_fill":"Fill color","space.source_file":"I have a floor-plan image","space.source_draw":"No image — I'll outline rooms by hand","space.orientation":"Canvas","orient.landscape":"Landscape","orient.portrait":"Portrait","orient.square":"Square","fill.temp":"Temperature","space.temp_min":"Comfort from","space.temp_max":"to","tip.temp_avg":"average temperature:","tip.hum_avg":"average humidity:","space_card.button":"Open the space plan","space_card.not_found":"Space “{id}” not found","space_card.loading":"Loading…","continuity.restore_plan":"Restoring floor plan…","continuity.restore_connection":"Restoring device connection…","continuity.retry":"Try again","editor.space":"Space","editor.show_button":"Show button","editor.button_label":"Button label","editor.button_target":"Target dashboard path","marker.sub_entity":"entity","title.general_settings":"General settings","gs.title":"General settings","gs.hint":"Fill colors apply to every space; each color has its own opacity. Which fill mode a space uses is set in that space's dialog.","gs.light_group":"Fill: lights","gs.light_on":"Lights on","gs.light_off":"All lights off","gs.temp_group":"Fill: temperature","gs.temp_cold":"Cold","gs.temp_ok":"Comfortable","gs.temp_hot":"Hot","gs.lqi_group":"Fill: zigbee signal","gs.lqi_low":"Weak signal","gs.lqi_high":"Strong signal","gs.reset":"Reset to defaults","gs.saved":"General settings saved","space.show_lqi":"Show zigbee signal (LQI) next to devices","space.hide_decor":"Hide the decorative layer","space.hide_decor_tip":"Lines, shapes, labels and furniture stay where they are — visible in the backdrop editor, not on the plan.","space.hide_openings":"Hide openings","space.hide_openings_tip":"Door, window and gate symbols are not drawn, but the openings keep working: light passes through, the sun comes in at a window, contact sensors still open them. The plan editor always shows them.","gs.light_none":"No light sources","mode.plan":"Plan editor","mode.devices":"Device editor","display.value":"Value + state","marker.subarea":"no area, manual","device.new":"New device — open its editor to dismiss","opening.unlock_action":"Unlock","opening.lock_action":"Lock","opening.lock_pending":"Working…","title.close_editor":"Close editor (back to view)","devbar.add":"Add","devbar.show_all":"Hidden and disabled","devbar.rules":"Icon rules","device_inbox.button":"Devices","device_inbox.title":"Devices on the plan","device_inbox.search":"Search devices, entities and integrations…","device_inbox.add_virtual":"Add virtual device","device_inbox.tab_on_plan":"On plan","device_inbox.tab_available":"Available","device_inbox.tab_hidden":"Hidden","device_inbox.tab_readd":"Available again","device_inbox.only_new":"New only","device_inbox.show_entities":"Show entities","device_inbox.show_hidden":"Show hidden on plan","device_inbox.new":"New","device_inbox.find":"Find on plan","device_inbox.edit":"Edit","device_inbox.hide":"Hide","device_inbox.show":"Show","device_inbox.add":"Add","device_inbox.readd":"Add again","device_inbox.hide_available":"Hide from list","device_inbox.show_more":"Show more","device_inbox.more_actions":"More actions","device_inbox.find_hidden_hint":"Enable “Show hidden on plan” first","device_inbox.show_disabled":"Activate this binding in Home Assistant before showing it","device_inbox.empty_on_plan":"No matching devices are on the plan yet.","device_inbox.empty_available":"No devices are available to add.","device_inbox.empty_hidden":"There are no hidden devices.","device_inbox.empty_readd":"No removed devices are available to add again.","device_inbox.reason_visible_auto":"Discovered automatically","device_inbox.reason_visible_explicit":"Added explicitly","device_inbox.reason_manual_hidden":"Hidden by user","device_inbox.reason_automatic_hidden":"Hidden automatically","device_inbox.reason_service_entry":"Service-only registry entry","device_inbox.reason_excluded_integration":"Integration excluded by device filters","device_inbox.reason_excluded_domain":"Non-spatial entity type","device_inbox.reason_grouped_light":"Represented by a room light group","device_inbox.reason_represented_by_parent":"Represented by its parent device","device_inbox.reason_removed":"Previously removed from the plan","device_inbox.reason_available":"Available to add to the plan","device_inbox.reason_no_bound_room":"HA area is not linked to a plan room","device_inbox.status_ha_disabled":"Disabled in Home Assistant","device_inbox.status_orphaned":"Binding is no longer present in Home Assistant","device_inbox.status_unverified":"Home Assistant registry is temporarily unavailable","device_inbox.saved":"Device list updated","space.roomcard_section":"Room card shows:","space.label_temp":"Temperature","space.label_hum":"Humidity","space.label_lqi":"Average Zigbee signal","space.label_light":"Lights on/off","roomcard.light_on":"On","roomcard.light_off":"Off","roomcard.light_partial":"{on} of {total}","toast.split_pick_inside":"Intermediate cut points must be inside the room","mode.decor":"Background editor","decor.select":"Select","decor.line":"Line","decor.rect":"Rectangle","decor.ellipse":"Oval","decor.text":"Text","decor.erase":"Erase","decor.erase_confirm_title":"Erase object?","decor.color":"Color","decor.width":"Line width","decor.line_style":"Line style","decor.line_style_solid":"Solid","decor.line_style_dashed":"Dashed","decor.fill":"Fill","decor.fill_color":"Fill color","decor.length":"Length","decor.size":"Size","decor.angle":"Rotation angle, °","decor.text_size":"Text size","decor.backdrop_properties":"Plan backdrop properties","decor.text_title":"Text label","decor.object_title":"Edit {kind}","decor.text_label":"Text","decor.live_group":"Insert HA variable","decor.live_entity":"Entity","decor.live_entity_ph":"choose an entity","decor.live_attr":"Value","decor.live_attr_ph":"choose state or attribute","decor.live_state":"State","decor.backdrop":"Backdrop image","decor.backdrop_hint":"Drag to move; pull a corner to resize; use the top handle to rotate. Shift changes proportions or frees the angle.","decor.backdrop_reset":"Reset the picture","decor.backdrop_reset_done":"The picture is back at its original place and size","marker.icon_auto":"Auto: {icon} (by icon rules; pick one to override)","marker.icon_pin_auto":"Pin","mode.plan_tip":"Plan editor — the geometry of the home: draw and split/merge rooms, bind them to HA areas, place doors, windows and gates, move room cards, set the scale","mode.devices_tip":"Device editor — everything about icons: drag to position, click to edit binding/icon/display, add virtual devices, icon rules","mode.decor_tip":"Background editor — purely visual decor under the plan: lines, rectangles, ovals and text labels that never react to clicks","space.glow_enabled":"Light-source glow","gs.glow_group":"Light-source glow","gs.glow_base":"House darkness","gs.glow_light":"Default light color / intensity","gs.wall_group":"Walls","gs.wall_fill":"Wall fill","gs.glow_radius":"Glow radius","gs.unit_m":"m","gs.unit_ft":"ft","marker.controls_label":"Controls other light sources","marker.controls_hint":"With tap action “Toggle”, a click flips every source added here. The marker’s own bound entity is controlled directly and cannot be added; use the switch below to classify it as a light.","marker.controls_filter":"Search lights and switches…","info.controls":"Controls","marker.glow_radius_label":"Glow radius","marker.glow_radius.help":"Sets the glow radius in metres or feet; an empty value uses the radius from general settings.","marker.glow_radius.help.aria":"Help: glow radius","markup.wallthick":"Thickness","markup.select":"Select","title.markup_wallthick":"Thickness — click a wall to set its thickness from 0 to 100 cm.","markup.hint_wallthick":"click a wall · Esc closes without applying","wallthick.field":"Thickness","wallthick.unit_cm":"cm","wallthick.unit_in":"in","wallthick.apply_room":"Apply to all walls of this room","markup.draw_wall_title":"Thickness of each new wall segment (0–100 cm). Shared walls keep the existing value.","room.queue_progress":"Room {current} of {total}","toast.wall_rooms_saved":"Created rooms: {n}","toast.wall_chain_saved":"Wall chain saved","toast.wallthick_pick":"Click a wall","toast.wallthick_set":"Wall thickness set","toast.wallthick_cleared":"Wall thickness removed","toast.physical_range":"Enter a value from {min} to {max} {unit}","toast.zero_wall_opening_conflict":"Remove the opening on this wall segment first.","toast.zero_wall_ambiguous":"The wall segment is ambiguous. Simplify or adjust the junction.","toast.zero_wall_migration_blocked":"The space was not converted: {reason}. No data was changed.","toast.physical_angle":"Enter a rotation angle from 0° up to, but not including, 90°","toast.physical_limit":"The space has reached the limit for this type of geometry","toast.geometry_unsafe":"Change canceled: wall geometry could not be built safely.","junction.limit_angle":"The angle between walls is too sharp: {actual}°, minimum {limit}°.","junction.limit_valence":"Too many walls meet in one node: {actual}, maximum {limit}.","junction.limit_length":"The wall segment is too short: {actual} cm, minimum {limit} cm.","junction.limit_distance":"Walls and nodes are too close: {actual} cm, minimum {limit} cm.","junction.limit_clearance":"No room interior is left: {actual} cm², minimum {limit} cm².","junction.limit_check_failed":"The junction check could not run — the change was not saved","toast.wall_model_migration_blocked":"The wall model could not be updated: {reason}. The plan was not changed. Run “Optimize plans”; if the error repeats, fix the conflicting wall geometry.","toast.wall_model_client_outdated":"Update the card and reload the page before editing the plan.","wall_model.reason.invalid-room":"invalid room contour","wall_model.reason.zero-length":"zero-length wall segment","wall_model.reason.third-owner":"wall shared by more than two rooms","wall_model.reason.duplicate-id":"conflicting wall identifiers","wall_model.reason.thickness-conflict":"conflicting wall thickness values","wall_model.reason.opening-host":"opening cannot be assigned to one wall","toast.delete_room_pick":"Click inside the room you want to delete","toast.plan_snap_ambiguous":"Zoom in to choose the wall node","toast.wall_repair_ambiguous":"The room outline has more than one possible connection. Zoom in and connect it explicitly.","toast.wall_repair_too_large":"The gap is larger than 2 cm. Connect the walls before creating the room.","toast.wall_repair_changed":"The wall geometry changed. Try creating the room again.","toast.opening_on_zero_wall":"Openings cannot sit on a zero-thickness wall","marker.from_ha_option":"Pick from the HA list","marker.show_entities":"Show entities","marker.show_entities_tip":"Adds not only devices to the list, but all their entities too","marker.pick_ph":"Choose a device…","room.open_area":"Open the HA area","view.volumetric":"Volumetric view","view.flat":"Flat view","kiosk.title":"This screen's sizes","kiosk.hint":"Stored on this device only — every wall tablet or TV can have its own comfortable sizes.","kiosk.icon_scale":"Device icon size","kiosk.font_scale":"Room card text size","editor.kiosk":"Wall device (kiosk) mode","editor.cycle":"Auto-switch spaces every N seconds (kiosk, 0 = off)","room.settings_title":"Room settings","room.settings_section":"Room settings (override the space)","room.fill_label":"Fill in THIS room","fill.inherit":"As the space","room.custom_fill_space":"Space color","room.custom_fill_own":"Room color","room.temp_src_label":"Temperature source","room.hum_src_label":"Humidity source","room.src_average":"Average over the room's sensors (default)","room.src_pick":"A specific HA device or entity","room.src_ph":"Choose a source…","toast.room_updated":"Room updated","space.card_font":"Room-card font size (whole space)","room.sizes_section":"Font sizes","room.name_scale":"Room name size","room.label_scale":"Metrics size","preview.room_name":"Living room","toast.cfg_reload_failed":"Could not reload the plan from the server: {err}","room.settings_short":"Room settings","room.unnamed":"Unnamed room","marker.use_climate_temp":"Include the device temperature in the room","marker.use_climate_temp_tip":"Adds an air conditioner or thermostat current_temperature to the room average. Configure the external value badge separately below.","marker.value_badge_title":"Value badge","marker.value_badge.help":"Shows one selected value next to the icon. It does not affect room metrics, light, Glow or the tap action.","marker.value_badge.help.aria":"Help: device value badge","marker.value_badge_enabled":"Show a value badge","marker.value_badge_source":"Value","marker.value_badge_source.help":"Choose a specific state, supported attribute, or derived value of this device.","marker.value_badge_source.help.aria":"Help: value badge source","marker.value_badge_position":"Position","marker.value_badge_position.help":"The chosen side stays fixed and does not flip automatically at a plan edge.","marker.value_badge_position.help.aria":"Help: value badge position","marker.value_badge_right":"Right","marker.value_badge_bottom":"Below","marker.value_badge_left":"Left","marker.value_badge_top":"Above","marker.value_badge_empty":"This device has no available values","marker.value_badge_static":"A static icon does not show live values. The setting is preserved.","marker.value_badge_missing":"Source unavailable","marker.value_badge_missing_hint":"The saved source is currently unavailable. Keep it, disable the badge, or choose another value.","marker.value_badge_duplicate":"The same value is already shown inside the icon.","marker.value_badge_state":"State · {name}","marker.value_badge_marker_state":"Light-source state · {name}","marker.value_badge_lqi":"Average Zigbee signal quality","marker.value_badge_attr_current_temperature":"Current temperature · {name}","marker.value_badge_attr_temperature":"Target temperature · {name}","marker.value_badge_attr_current_humidity":"Current humidity · {name}","marker.value_badge_attr_humidity":"Humidity · {name}","marker.value_badge_attr_current_position":"Position · {name}","marker.value_badge_attr_percentage":"Speed · {name}","marker.value_badge_attr_brightness":"Brightness · {name}","marker.value_badge_attr_volume_level":"Volume · {name}","marker.value_badge_attr_battery_level":"Battery · {name}","marker.value_badge_attr_fan_speed":"Fan speed · {name}","marker.light_role_label":"Is this device a light source?","marker.light_role.help":"Controls whether this marker itself is a spatial light source. Linked lights above remain independent room-light controls.","marker.light_role.help.aria":"Help: whether this device is a light source","marker.light_role_auto_yes":"Auto (light source)","marker.light_role_auto_no":"Auto (not a light source)","marker.light_role_always":"Always a light source","marker.light_role_never":"Never a light source","marker.light_entity_label":"Leading light entity","marker.light_entity.help":"For a composite device, choose the entity whose state, colour and brightness represent this plan source. The automatic fallback remains compatible with older plans.","marker.light_entity.help.aria":"Help: leading light entity","marker.light_entity_auto":"Automatic ({entity})","marker.light_entity_none":"no controllable entity","marker.light_entity_missing":"The saved entity {entity} is unavailable. House Plan temporarily uses {fallback}; the saved choice will be restored if it returns.","marker.toggle_entity_label":"Entity to toggle","marker.toggle_entity.help":"For a composite device, choose the exact own light or switch operated by a tap. Automatic keeps the previous target rules and is independent from the leading light entity.","marker.toggle_entity.help.aria":"Help: entity to toggle","marker.toggle_entity_auto":"Automatic ({entity})","marker.toggle_entity_none":"no own controllable entity","marker.toggle_entity_missing":"The saved entity {entity} is no longer among this marker’s selectable channels. House Plan temporarily uses {fallback}; the saved choice will be restored if it returns.","marker.glow_color_label":"Glow colour and brightness","marker.glow_mode.help":"Use live source values, override only its colour, or fix both colour and brightness. The minimum is 1%; to disable the source, choose “Never”.","marker.glow_mode.help.aria":"Help: glow colour and brightness","marker.glow_mode_auto":"From source","marker.glow_mode_color":"Set colour","marker.glow_mode_fixed":"Set colour and brightness","marker.glow_color":"Glow colour","marker.glow_brightness":"Brightness","marker.glow_disabled_never":"Glow settings are unavailable because this marker is explicitly not a light source.","marker.glow_disabled_auto":"Auto mode found no spatial light source for this marker.","marker.glow_disabled_no_entity":"No active controllable Home Assistant entity is available for this spatial source.","marker.glow_passive_hint":"This source has no own Home Assistant data. Set its colour and brightness manually; its radius remains available.","marker.control_broken":"Saved source is missing or no longer marked as a light","marker.control_missing_label":"Missing plan light","marker.control_passive":"passive source","toast.marker_control_cycle":"This link would create a circular chain of light controls.","toast.marker_binding_required":"Choose a Home Assistant device before linking another light source.","confirm.unlock":"Unlock “{name}”?","toast.files_migrate_failed":"Attachments could not be moved to the new binding, links keep pointing at the old files: {err}","space.pick_saved":"Already uploaded","space.pick_saved_hint":"Plans stored on the server, including ones you detached earlier","space.no_saved":"No plans stored on the server yet.","space.loading":"Loading…","space.used_by":"in use: {list}","space.in_use":"A space still uses this plan — detach it first","btn.use":"Use","confirm.delete_plan":'Delete the plan file "{name}" from the server? This cannot be undone.',"toast.plans_list_failed":"Could not list the stored plans: {err}","toast.plan_delete_failed":"Could not delete the plan: {err}","marker.hide":"Hide","marker.hide_tip":'The device will disappear from the plan after saving but will still count toward the room signal. Restore it through the "Devices" catalog in the device editor.',"marker.show":"Show","marker.show_tip":"The device will appear on the plan again after saving.","marker.hidden_ghost":"Device hidden by the user","marker.ha_disabled_device":"The device is disabled in Home Assistant and hidden from the plan.","marker.ha_disabled_entity":"The entity is disabled in Home Assistant and hidden from the plan.","marker.ha_disabled_all_entities":"The device has no active Home Assistant entities, so it is hidden from the plan.","marker.ha_registry_limited":"The full Home Assistant registry is unavailable to this user. The unverified object cannot be shown or used for now.","marker.delete_tip":"Completely delete the device from the plan and every aggregate. You can add it again later.","tap.run":"Run automation/script/scene","tap.cover":"Open/close (curtains/blinds)","marker.run_target_label":"What to run","marker.run_search_ph":"Search: automation, script or scene…","marker.run_target_gone":"Target {id} not found — pick again","marker.tap_confirm":"Ask for confirmation","marker.tap_confirm_tip":"Show a confirmation dialog before acting — a guard against accidental taps.","marker.toggle_hint_single":"Target: {name} ({id}).","marker.virtual_light_target":"Virtual light: {name}.","marker.virtual_light_current":"Manual state: {state} → {effect}.","marker.virtual_light_state_on":"on","marker.virtual_light_state_off":"off","marker.toggle_hint_group":"Will toggle {count} source(s): {names}.","marker.toggle_hint_current":"Now: {state} → {effect}.","marker.toggle_hint_group_current":"Currently on: {on} of {count} → {effect}.","marker.toggle_hint_skipped":"Will be skipped ({count}): {targets}.","marker.toggle_effect_turn_on":"will turn on","marker.toggle_effect_turn_off":"will turn off","marker.toggle_effect_open":"will open","marker.toggle_effect_close":"will close","marker.toggle_effect_stop":"will stop","marker.toggle_effect_toggle":"state will toggle","marker.toggle_skip_missing":"not found","marker.toggle_skip_ha_disabled":"disabled in HA","marker.toggle_skip_unavailable":"unavailable","marker.toggle_skip_unsupported":"toggle is unsupported","marker.toggle_skip_secure":"blocked for security","marker.toggle_none_no_binding":"This device has no binding, so there is no state to toggle. Tapping will do nothing.","marker.toggle_none_no_actionable_entity":"This device has no state that can be toggled. Tapping will do nothing.","marker.toggle_none_configured_targets_missing":"The configured targets are unavailable. The device's own entity will not be substituted for them.","marker.toggle_none_ha_disabled":"The target is disabled in Home Assistant and cannot be used on the plan.","marker.toggle_none_unavailable":"The target is currently unavailable. Tapping will do nothing.","marker.toggle_none_unsupported":"Home Assistant exposes no safe toggle operation for this entity. Tapping will do nothing.","marker.toggle_none_secure":"Toggling locks, alarms and secure gates from the plan is blocked for security.","run.automation":"automation","run.script":"script","run.scene":"scene","confirm.tap_run":'Run "{name}"?',"confirm.tap_toggle":'Toggle "{name}"?',"confirm.tap_cover":'Open/close "{name}"?',"confirm.current_state":"Current state: {state}","confirm.expected_state":"After switching: {state}","confirm.group_current":"on {on} of {total}","confirm.group_all_on":"all are on","confirm.group_all_off":"all are off","confirm.unavailable_targets":"Unavailable: {count}","confirm.expected_by_ha":"Home Assistant will determine the state","confirm.state_on":"On","confirm.state_off":"Off","confirm.state_open":"Open","confirm.state_closed":"Closed","confirm.state_opening":"Opening","confirm.state_closing":"Closing","confirm.state_stopped":"Stopped","confirm.state_unknown":"Unknown","toast.run_started":"Started: {name}","toast.run_target_missing":"Run target not found — check the device settings","toast.run_target_required":"Pick an automation, script or scene","toast.tap_target_changed":"The action target changed. Try again.","toast.virtual_light_toggle_failed":"Could not toggle the virtual light: {err}","toast.value_badge_source_required":"Choose a value for the badge","btn.run":"Run","vac.section":"Robot vacuum: live position","vac.autocal":"Set up automatically","vac.live":"Live position on the plan","vac.trail":"Show the robot's path","vac.cal_maps":"Calibrated maps: {maps}","vac.autocal_no_rooms":"The integration reports no room list — open “Fit manually”","vac.autocal_no_match":"Room names did not match (need ≥3 in common) — open “Fit manually”","vac.autocal_done":"Done: bound via {rooms} rooms. Start a cleanup and check","vac.cal_need_pos":"The robot is not reporting coordinates — start a cleanup and pause it","vac.cal_done":"Calibration saved. Start a cleanup and check","vac.cal_cancelled":"Calibration cancelled","vac.fit":"Fit manually","vac.fit_hint":"Drag the robot map into place, stretch by the corners","vac.fit_rotate":"Rotate 90°","vac.fit_mirror":"Mirror","vac.trail_never":"Never","vac.trail_cleaning":"While cleaning","vac.trail_always":"Always","gs.bg_group":"Stage background","gs.bg_color":"Background around the plan","gs.bg_default":"Theme default","gs.bg_theme":"theme default","gs.bg_mode":"Plan background","gs.bg_static":"Static color","gs.bg_daynight":"Follows the Sun","gs.bg_daynight_hint":"The background moves through dawn, day, dusk and night using sun.sun, or your local clock when sun data is unavailable. No compass is needed for the background.","gs.sun_group":"Sun","gs.sun_missing":"The sun.sun entity was not found — the background follows your local clock; window rays are unavailable.","gs.north":"North on the plan","gs.north_ph":"not set","gs.north_hint":"Set north only for window rays (1° steps, 15° with Shift); the Sun background works without it.","gs.north_clear":"Clear","gs.north_letter":"N","gs.sun_rays":"Sunlight through windows","gs.about_group":"About","gs.about_version":"Houseplan Card v{v}","gs.about_github":"GitHub · docs & issues","gs.about_telegram":"Telegram chat","space.bg_color":"Background around the plan","space.bg_inherit":"Inherit general","space.bg_inherited":"inherits general settings","space.bg_mode":"Plan background","space.north":"North on the plan (override)","space.north_inherited":"inherited: {v}","space.sun_rays":"Sunlight through windows","space.sun_inherit":"Inherit general","space.sun_on":"On","space.sun_off":"Off","canvas.far_objects":"{n} object(s) far from the plan","canvas.show_far":"Show","canvas.home_tip":"The plan is over there — click to fit it","gs.grid_group":"Plan maintenance","gs.grid_hint":"Updates data models, aligns plan elements to the grid and merges redundant wall fragments. An exact report is shown before anything is stored.","gs.align_all":"Optimize plans","gs.align_title":"Optimize plans","gs.align_none":"All plans already use the current optimized data model.","gs.optimize_no_automatic_changes":"There are no automatic changes to apply. Review the items below.","gs.align_count":"{n} of {total} elements will move, by at most {cm} cm.","gs.align_where":"The largest shift is in “{s}”.","gs.align_turned":"Openings whose angle is corrected: {n}.","gs.align_removed_drafts":"Invalid outlines collapsed by the grid and removed: {n}.","gs.optimize_redundant_drafts":"Saved wall chains hidden by solid room walls and removed: {n}.","gs.align_preflight_failed":"Could not safely verify the geometry of the following spaces: {spaces}{more}.","gs.align_preflight_hint":"Plans were not changed. Copy the diagnostics with the button below and attach them to the bug report together with a space export.","gs.preflight_reason_prepare-exception":"Could not prepare the space geometry (exception while building the model)","gs.preflight_reason_wall-null":"The wall body did not build (the union came back empty)","gs.preflight_reason_wall-degraded-extra":"The wall body degraded with extra geometry","gs.preflight_reason_wall-failed-core":"The wall body core failed to assemble","gs.preflight_reason_wall-exception":"Wall construction threw an exception","gs.preflight_reason_floor-null":"The floor outline did not build","gs.preflight_reason_floor-exception":"Floor construction threw an exception","gs.preflight_copy":"Copy diagnostics","gs.preflight_copied":"Diagnostics copied","gs.preflight_update_hint":"The card and integration versions differ — update House Plan and retry.","gs.align_preflight_space":"Space {n}","gs.align_preflight_more":", and {n} more","gs.optimize_changes":"Model migrations: {m}; spaces updated: {c}; noisy coordinate values removed: {p}; merged real-wall fragments: {w}; merged zero-thickness wall fragments: {s}; independent walls: {i}.","gs.zero_walls_migrated":"Virtual wall spans converted: {n}.","gs.wall_segments_migrated":"Wall segments stabilised: {n}.","gs.optimize_lattice_summary":"Noisy coordinate values canonicalized: {n}; maximum movement: {cm} cm.","gs.optimize_lattice_space":"{space}: coordinate values canonicalized: {n}; off-grid values left unchanged: {far}.","gs.optimize_coincident_partitions":"Hidden independent wall sections absorbed into room walls: {n}.","gs.optimize_openings_rehosted":"Openings reattached to room walls: {n}.","gs.optimize_walls_straightened":"Walls straightened: {n}; maximum movement: {cm} cm.","gs.optimize_walls_straightened_where":"Largest wall correction: {s}.","gs.optimize_walls_straighten_skipped":"Near-axis walls left unchanged because they could not be repaired safely: {n}.","gs.optimize_glow_migration":"Legacy Glow: {spaces} spaces → no data fill + independent Glow; {rooms} rooms → inherited data fill + independent Glow.","gs.optimize_references":"References repaired: spaces — {spaces}; rooms — {rooms}; positions — {positions}; devices detached from missing spaces — {detached}.","gs.optimize_reference_more":", and {n} more","gs.optimize_orphans_removed":"Forgotten records removed: {total} — room labels: {rooms}; devices: {devices}; group markers: {groups}. They belonged to spaces deleted earlier.","gs.optimize_live_positions":"Old positions in deleted spaces belong to existing objects: {n}{names}. They will be kept.","gs.optimize_live_positions_remove":"Old positions in deleted spaces belong to existing objects: {n}{names}. They are selected for removal.","gs.optimize_live_names":": {names}{more}","gs.optimize_live_remove":"Remove old positions","gs.optimize_live_keep":"Keep old positions","gs.optimize_live_selected":"Old positions will be removed after Optimize is applied.","gs.optimize_unverified":"Could not safely verify positions: {n}. They were left unchanged.","gs.optimize_registry_limited":"Full administrator access to the Home Assistant registries is required for a safe check.","gs.optimize_vacuum_warning":"Vacuum room mappings that still need review: {n}.","gs.optimize_details":"Details","gs.optimize_details_more":"And {n} more records.","gs.optimize_detail_removed":"will be removed","gs.optimize_detail_live":"will be kept","gs.optimize_detail_unverified":"not verified","gs.optimize_detail_room_label":"room label","gs.optimize_detail_device":"device","gs.optimize_detail_group":"group marker","gs.optimize_detail_unknown":"unknown owner","gs.optimize_detail_item":"{status}: {kind} {id}; old space {space}","gs.align_warn":"Elements deliberately placed between grid nodes will move. One undo is available after the operation, only until the next plan edit.","gs.align_run":"Optimize","gs.align_done":"Plans optimized: {n} elements moved, {m} records maintained, {r} references repaired","gs.optimize_undo":"Undo last optimization","gs.optimize_undone":"The last optimization was undone","decor.furniture":"Furniture","furn.title":"Furniture library","furn.symbol":"Symbol","furn.group_furniture":"Furniture","furn.group_appliance":"Appliances","furn.group_sanitary":"Plumbing","furn.group_other":"Other","furn.width":"Width","furn.depth":"Depth","furn.pick_hint":"Pick a symbol, then click on the plan.","furn.place_hint":"Click on the plan — the piece lands against the nearest wall. Shift places it free.","furn.sym_sofa":"Sofa","furn.sym_armchair":"Armchair","furn.sym_coffee_table":"Coffee table","furn.sym_table_dining":"Dining table","furn.sym_table_round":"Round table","furn.sym_chair":"Chair","furn.sym_desk":"Desk","furn.sym_bed_double":"Double bed","furn.sym_bed_single":"Single bed","furn.sym_nightstand":"Nightstand","furn.sym_wardrobe":"Wardrobe","furn.sym_bookshelf":"Bookshelf","furn.sym_fridge":"Fridge","furn.sym_stove":"Cooker","furn.sym_dishwasher":"Dishwasher","furn.sym_washer":"Washing machine","furn.sym_dryer":"Tumble dryer","furn.sym_tv":"TV","furn.sym_ac":"Air conditioner","furn.sym_water_heater":"Water heater","furn.sym_toilet":"Toilet","furn.sym_bathtub":"Bathtub","furn.sym_shower":"Shower","furn.sym_sink":"Washbasin","furn.sym_kitchen_sink":"Kitchen sink","furn.sym_bidet":"Bidet","furn.sym_stairs":"Stairs","furn.sym_fireplace":"Fireplace","furn.sym_plant":"Plant","furn.sym_rug":"Rug","common.yes":"Yes","common.no":"No","vac.diag_source":"Source","vac.diag_platform":"Integration","vac.diag_status":"Status","vac.diag_position":"Position","vac.diag_rooms":"Rooms","vac.diag_rooms_value":"{total} · {matched} names match · {readiness}","vac.autocal_ready":"auto-calibration available","vac.autocal_not_ready":"need 3 matching names","vac.diag_path":"Integration path","vac.diag_map":"Map ID","vac.source_none":"not selected","vac.source_status_ok":"Ready","vac.source_status_missing":"Missing","vac.source_status_disabled":"Disabled in Home Assistant","vac.source_status_unavailable":"Unavailable","vac.source_status_unverified":"Cannot verify with current permissions","vac.source_status_unsupported":"No position data","vac.source_status_none":"No source","vac.source_banner_missing":"The saved source no longer exists. It was not replaced automatically; choose another source or restore it in Home Assistant.","vac.source_banner_disabled":"The saved source is disabled in Home Assistant. Enable it there or choose another source.","vac.source_banner_unverified":"Current Home Assistant permissions cannot verify this saved source. It remains pinned and will not be replaced automatically.","vac.choose_source":"Choose source","vac.source_auto":"Automatic","vac.source_auto_hint":"Use a compatible entity from this device","vac.all_cameras":"All cameras","vac.all_cameras_warn":"A camera may not provide robot data. Choose one only if it is your vacuum map.","vac.all_cameras_empty":"No other camera entities found.","vac.platform_unknown":"unknown integration","vac.cap_position":"position","vac.cap_rooms_short":"rooms","vac.cap_path":"path","vac.cap_map":"map ID","vac.cap_none":"no robot data detected","vac.xcme_hint":"Enable these Xiaomi Cloud Map Extractor attributes:","vac.documentation":"Documentation","vac.residual_title":"Check automatic calibration","vac.residual_message":"The matched rooms disagree by up to {error}. Apply this approximate calibration, refine it manually, or cancel without changing the saved setup.","vac.apply_proposal":"Apply","gs.backup_group":"Backup and transfer","gs.backup_hint":"Download a portable JSON backup or preview and import a backup made by House Plan.","backup.export_open":"Export","backup.import_open":"Import","backup.export_title":"Export House Plan","backup.import_title":"Import House Plan","backup.export_hint":"Choose whether the backup should contain the whole House Plan configuration or only the current space.","backup.full":"Full backup","backup.current_space":"Current space","backup.current_space_title":"Current space: {title}","backup.no_current_space":"No current space","backup.plan_only":"Plan only","backup.plan_only_hint":"Keep rooms, walls, openings, decor and room-label positions without devices or Home Assistant bindings.","backup.plan_only_preview":"This file contains the plan only","backup.privacy_warning":"The archive keeps names, Home Assistant identifiers and exact coordinates. Internal plans and attachments are referenced, not embedded; runtime states and vacuum trails are not included.","backup.download":"Download JSON","backup.export_done":"Backup downloaded","backup.reading":"Checking the backup…","backup.revalidated":"The plan changed after this preview. The summary was refreshed; review it and confirm again.","backup.error.unauthorized":"You do not have permission to export or import this plan.","backup.error.not_ready":"House Plan is not ready yet. Try again in a moment.","backup.error.too_large":"The backup exceeds the 8 MiB limit.","backup.error.invalid_json":"The selected file is not valid JSON.","backup.error.invalid_format":"The selected file is not a House Plan backup.","backup.error.unsupported_export_version":"This backup format is not supported by the installed version.","backup.error.future_model":"The backup was created by a newer House Plan data model.","backup.error.invalid_config":"The backup contains an invalid House Plan configuration.","backup.error.wall_model_migration_blocked":"The backup wall model could not be upgraded safely. Optimize the source plan first, then export it again.","backup.error.invalid_layout":"The backup contains invalid object positions.","backup.error.invalid_content":"The backup contains invalid or inconsistent content references.","backup.error.space_not_found":"The selected space no longer exists.","backup.error.capacity_exceeded":"Adding this backup would exceed the plan limits.","backup.error.preview_expired":"The preview expired. Select the backup file again.","backup.error.preview_owner_mismatch":"This preview belongs to another Home Assistant user.","backup.error.conflict":"The plan changed after the preview. Review the refreshed summary.","backup.error.content_confirmation_required":"Confirm that unavailable local content may be detached.","backup.error.commit_failed":"The backup could not be applied safely. The previous plan was restored or is pending recovery.","backup.error.missing_plan":"A referenced plan file disappeared after the preview. Review the backup again.","backup.error.missing_content":"A referenced local attachment disappeared after the preview. Review the backup again.","backup.error.marker_control_missing":"A linked plan light is missing from the backup.","backup.error.marker_control_not_light":"A linked plan target is no longer marked as a light source.","backup.error.marker_control_self":"A light source cannot control itself.","backup.error.marker_control_cycle":"The backup contains a circular chain of light controls.","backup.error.duplicate_marker_control":"The backup contains a duplicate plan-light link.","backup.error.no_backup":"There is no import or optimization snapshot to restore.","backup.same_source":"Created on this Home Assistant instance","backup.foreign_source":"Created on another Home Assistant instance","backup.created":"Created: {value}","backup.versions":"Card {card}; integration {integration}; data model {model}","backup.count_spaces":"Spaces: {n}","backup.count_rooms":"Rooms: {n}","backup.count_walls":"Walls: {n}","backup.count_openings":"Openings: {n}","backup.count_decor":"Decor objects: {n}","backup.count_markers":"Devices: {n}","backup.count_layout":"Positions: {n}","backup.bindings":"Bindings — devices: {device}, entities: {entity}, virtual: {virtual}; positions without a space: {legacy}","backup.binding_status":"Target status — active: {active}, disabled: {disabled}, missing: {missing}","backup.missing_areas":"Areas missing on the target: {areas}","backup.dropped_marker_links":"Plan-light links outside this transferred space were omitted: {n}.","backup.repaired_target_refs":"Existing references restored by this space import: {n}.","backup.preserved_unresolved_refs":"References that could not be restored unambiguously were preserved: {n}.","backup.preserved_unresolved_hint":"No data was guessed or deleted. After the import, run Optimize plans to inspect the remaining references.","backup.import_details":"Import reference details","backup.import_detail.incoming_remapped":"References updated inside the imported copy: {n}","backup.import_detail.target_repaired":"Existing references restored: {n}","backup.import_detail.preserved_unresolved":"Unresolved references preserved: {n}","backup.import_detail.collisions":"Destination conflicts preserved safely: {n}","backup.import_detail.dropped_links":"Incoming links omitted by transfer rules: {n}","backup.import_detail.bounded_lineages":"Overly nested identifiers left bounded: {n}","backup.replace_warning":"This replaces the current configuration and layout. Uploaded files are never deleted. One undo remains available until the next plan edit.","backup.foreign_bookkeeping":"Instance-specific known/new-device bookkeeping will not be imported.","backup.final_name":"New space name","backup.target_settings":"The target instance's global settings remain unchanged.","backup.duplicates":"Bindings already present: {n}","backup.skip":"Skip duplicate devices","backup.virtual_copy":"Add safe static virtual copies","backup.content":"Referenced content","backup.content_available":"available locally","backup.content_external":"external link","backup.content_detach_required":"will be detached","backup.confirm_detach":"I understand that unavailable internal plans and attachments will be detached from the imported configuration.","backup.replace":"Replace","backup.add":"Add space","backup.space_done":"Space imported: {rooms} rooms, {markers} devices, {refs} existing references restored","backup.full_done":"Backup restored: {spaces} spaces, {rooms} rooms, {markers} devices","backup.undo_import":"Undo last full import","backup.import_undone":"Full import undone"};const aa=[{code:"en",nativeLabel:"English",dictionary:oa},{code:"ru",nativeLabel:"Русский",dictionary:{"editor.context_actions":"Действия: {object}","editor.tool_options":"Параметры инструмента: {tool}","editor.palette":"Палитра: {tool}","editor.open_group":"Группа инструментов: {group}","editor.group_active":"{group} — активно: {item}","editor.disabled_action":"{action} недоступно: {reason}","color_picker.title":"Выбор цвета","color_picker.hue":"Оттенок","color_picker.saturation":"Насыщенность","color_picker.value":"Яркость","color_picker.hex":"Цвет HEX","color_picker.invalid_hex":"Введите цвет HEX из 3 или 6 цифр","btn.properties":"Свойства","btn.keep_as_walls":"Оставить стенами","btn.delete_room_keep_walls":"Удалить комнату, оставить стены","btn.delete_room_with_walls":"Удалить комнату и стены","title.markup_select":"Выбор и редактирование стен, колонн и сохранённых контуров","title.markup_column":"Колонна: кликните точку сетки для квадратной колонны","markup.partition":"Перегородка","markup.column":"Колонна","markup.hint_column":"кликните точку сетки, чтобы поставить квадратную колонну","history.draft_segment":"Добавление сегмента черновика комнаты","history.draft_merge":"Соединение незавершённых контуров комнаты","history.draft_segment_delete":"Удаление сегмента черновика комнаты","history.partition_add":"Добавление перегородки","history.column_add":"Добавление колонны","history.physical_edit":"Изменение физического объекта","history.physical_delete":"Удаление физического объекта","history.physical_move":"Перемещение физического объекта","history.contour_to_partitions":"Преобразование контура в замкнутые стены","history.wall_chain_finish":"Завершение цепочки стен","history.wall_face_batch":"Создание комнат из стен","toast.column_duplicate":"Колонна с тем же центром и внешним размером уже существует","confirm.delete_draft":"Удалить весь незавершённый контур комнаты?","confirm.delete_draft_segment":"Удалить этот сегмент черновика? Оставшийся контур может разделиться на два.","physical.partition_properties":"Свойства перегородки","physical.column_properties":"Свойства колонны","physical.draft_properties":"Свойства сегмента черновика","physical.shape":"Форма","physical.square":"Квадрат","physical.circle":"Круг","physical.diameter":"Диаметр","physical.side":"Сторона","physical.rotation":"Угол поворота","physical.length":"Длина","physical.allowed_range":"Допустимо: {min}–{max} {unit}","physical.delete_segment":"Удалить сегмент","physical.delete_draft":"Удалить весь контур","physical.column_size_title":"Внешняя сторона квадратной колонны, создаваемой кликом (1–150 см).","card.title":"План дома","count.devices":"{n} устр.","empty.no_spaces":"Пространств пока нет.","empty.add_first":"Добавьте первое пространство и загрузите план этажа.","empty.install":"Установите интеграцию House Plan и добавьте запись в «Устройства и службы».","btn.add_space":"Добавить пространство","btn.cancel":"Отмена","btn.save":"Сохранить","btn.close":"Закрыть","btn.delete":"Удалить","btn.edit":"Редактировать","btn.open_in_ha":"Открыть в HA","btn.reset":"Сброс","btn.attach":"Прикрепить…","btn.upload":"Загрузить…","btn.replace":"Заменить…","title.zoom_in":"Приблизить","title.zoom_out":"Отдалить","title.zoom_fit":"Вписать всё","title.add_device":"Добавить устройство на план","title.show_all":"Показать скрытые и деактивированные устройства служебными маркерами (только в этой вкладке)","title.markup":"Разметка комнат: сетка, линии, контуры","title.configure_space":"Настроить пространство","title.add_space":"Добавить пространство","title.markup_add":"Стены: рисуйте непрерывную цепочку; Shift фиксирует 45°. Смена инструмента или выход завершает её как независимые стены","title.markup_merge":"Объединить: клик по одной комнате, затем по соседней с общей стеной","title.markup_split":"Разделить комнату: клик по комнате, затем две точки на её стенах","title.markup_delroom":"Удалить комнату: кликните внутри комнаты и подтвердите удаление","title.need_plan":"Загрузите подложку (план этажа)","markup.add":"Стены","markup.merge":"Объединить","markup.split":"Разделить","markup.resize":"Размер","title.markup_resize":"Изменение размера комнат: тяните доступную ручку горизонтальной или вертикальной стены","markup.hint_resize":"тяните доступную ручку стены · стена упирается в первую небезопасную позицию · Esc — отмена · Ctrl+Z — отмена шага","resize.disabled.diagonal":"Стена расположена под углом: Resize поддерживает только горизонтальные и вертикальные стены","resize.disabled.side-angle":"Примыкающая стена расположена под углом: для Resize обе примыкающие стены должны образовывать прямой угол","resize.disabled.duplicate-physical-wall":"Стену перекрывает отдельная перегородка, незавершённый контур или колонна; удалите или переместите её перед изменением размера","resize.disabled.partial-shared":"Соседняя комната использует только часть этой стены, поэтому её нельзя безопасно двигать как одну общую стену","resize.disabled.unequal-shared":"У соседней комнаты другие конечные точки или длина стены, поэтому стены нельзя безопасно двигать вместе","resize.disabled.multiple-rooms":"Перемещение затронуло бы больше двух комнат","resize.disabled.thickness-conflict":"Толщину стены нельзя безопасно сохранить","resize.disabled.opening-conflict":"Проём на этой стене нельзя безопасно переместить","resize.disabled.invalid-geometry":"Эту стену нельзя переместить без изменения структуры плана","resize.limit_stopped":"Стена остановлена: дальше нарушается ограничение стыков","resize.commit_failed":"Изменение размера отменено: итоговый план не прошёл проверку геометрии","resize.preview_failed":"Стена остановлена в последней безопасной позиции: геометрию плана нельзя сохранить без потерь","markup.opening":"Проём","markup.delete_room":"Удалить комнату","history.undo":"Отменить","history.redo":"Повторить","history.undo_named":"Отменить: {name} (Ctrl+Z)","history.redo_named":"Повторить: {name} (Ctrl+Shift+Z / Ctrl+Y)","history.undo_empty":"Нет операций для отмены","history.redo_empty":"Нет операций для повтора","history.undone":"Отменено: {name}","history.redone":"Повторено: {name}","history.add_room":"Создание комнаты","history.split_room":"Разделение комнаты","history.merge_rooms":"Объединение комнат","history.resize_room":"Изменение размера комнаты","history.wall_thickness":"Изменение толщины стены","history.add_opening":"Добавление дверного или оконного проёма","history.edit_opening":"Изменение проёма","history.move_opening":"Перемещение проёма","history.delete_opening":"Удаление проёма","history.delete_room":"Удаление комнаты","history.delete_room_keep_walls":"Удаление комнаты с сохранением стен","history.delete_room_with_walls":"Удаление комнаты и её стен","history.decor_add":"Добавление объекта декора","history.decor_edit":"Изменение объекта декора","history.decor_move":"Перемещение объекта декора","history.decor_transform":"Трансформация объекта декора","history.decor_delete":"Удаление объекта декора","history.backdrop_transform":"Трансформация подложки плана","title.markup_opening":"Проёмы: клик по стене — добавить, клик по проёму — редактировать","opening.new":"Новый проём","opening.edit":"Проём","opening.door":"Дверь","opening.window":"Окно","opening.gate":"Ворота","opening.passage":"Открытый проём","opening.passage_binding_warning":"При сохранении датчик открытия и замок будут удалены.","opening.invalid_passage_fields":"У открытого проёма на плане «{room}» есть недопустимые параметры: {fields}.","opening.type_label":"Тип","opening.length_label":"Длина, см","opening.contact_label":"Датчик открытия","opening.lock_label":"Замок","opening.none":"— нет —","opening.search_ph":"Поиск: имя или entity_id…","opening.invert":"Инвертировать открыто/закрыто","opening.flip_h":"Петли с другой стороны","opening.flip_v":"Открывается в другую сторону","opening.open":"Открыто","opening.closed":"Закрыто","opening.locked":"Заперто","opening.unlocked":"Не заперто","opening.state_unknown":"недоступно","opening.no_entities":"Датчики не привязаны — статичный символ на плане.","toast.opening_no_wall":"Кликните рядом со стеной комнаты или независимой стеной","opening.host_partition":"Независимая стена","opening.partition_orphan":"Независимая стена этого проёма больше не существует","opening.partition_jamb_margin":"Оставьте от края проёма до торца стены минимум {distance}","opening.rebind_partition":"Привязать к другой независимой стене","confirm.delete_partition_openings_title":"Удалить стену и проёмы?","confirm.delete_partition_openings_body":"В стене есть проёмы: {count}. Они будут удалены вместе со стеной.","confirm.delete_partition_openings_item":"• {type}, {length}","markup.delete":"Удалить","markup.hint_points":"точек: {n} · Shift — шаг 45° · Esc — завершить цепочку · Ctrl+Z — убрать точку · при замыкании будет предложена комната","markup.hint_start":"кликните точку сетки, чтобы начать цепочку стен","tip.lqi":"средний сигнал zigbee:","tip.area":"площадь: {value}","info.device_header":"Устройство на плане","info.model":"Модель","info.state":"Состояние","info.link":"Ссылка","info.manuals":"Инструкции","info.none":"Нет дополнительной информации","marker.new_device":"Новое устройство","marker.name_label":"Имя (отображается на плане)","marker.name_ph":"Название","marker.binding_label":"Привязка к устройству HA","marker.binding_disabled":"деактивировано в Home Assistant","marker.virtual_option":"Виртуальное устройство (без привязки)","marker.search_ph":"Поиск устройства / группы…","marker.nothing_found":"ничего не найдено","marker.room_label":"Комната","marker.room_override":" (переопределить размещение)","marker.room_choose":"— выберите комнату —","marker.room_auto":"— по зоне устройства (авто) —","marker.icon_label":"Иконка","marker.icon_ph":"mdi:… (пусто = авто)","marker.display_label":"Отображение","display.badge":"Значок + состояние","display.icon_ripple":"Значок + состояние и активность","display.static_icon":"Всегда статичный значок","marker.display_hint":"Значок + состояние меняет подложку при работе, открытии, тревоге и недоступности. Значок + состояние и активность дополнительно показывает короткую пульсацию для событий и постоянную — для работы, движения или присутствия. Значение + состояние заменяет значок одним однозначным значением HA. Статичный значок не реагирует на состояния устройства.","marker.display_hint_badge":"Значок и динамическая подложка показывают состояние устройства без обычной пульсации активности. Красная тревога сохраняется.","marker.display_hint_icon":"Значок и динамическая подложка показывают состояние устройства без обычной пульсации активности. Красная тревога сохраняется.","marker.display_hint_icon_ripple":"Значок, динамическая подложка и пульсация: короткая — для событий, постоянная — для работы, движения или присутствия. Красная тревога имеет отдельный приоритет.","marker.display_hint_value":"Значок заменяется одним однозначным значением Home Assistant; подложка продолжает показывать состояние. Красная тревога сохраняется.","marker.display_hint_static_icon":"Подложка в цветах темы и значок всегда остаются одинаковыми. Состояния, активность, недоступность и тревоги не меняют отображение.","marker.static_alarm_warning":"Это устройство может сообщать о тревогах. Статичный режим скрывает визуальную тревогу маркера.","marker.preview.title":"Предпросмотр отображения","marker.preview.actual":"Сейчас","marker.preview.example":"Пример","marker.preview.integration":"Интеграция","marker.preview.source":"Источник отображения","marker.preview.current_state":"Текущее состояние","marker.preview.result":"На плане","marker.preview.details":"Подробнее об источниках","marker.preview.select_source":"Выберите устройство или сущность Home Assistant, чтобы увидеть фактическое отображение.","marker.preview.unknown_provider":"Интеграция неизвестна","marker.preview.virtual_provider":"House Plan · виртуальное устройство","marker.preview.no_source":"Нет активного источника","marker.preview.no_state":"Нет текущего состояния","marker.preview.mixed_states":"Несколько разных состояний","marker.preview.multiple_sources":"Источников: {n}","marker.preview.more_sources":"ещё {n}","marker.preview.scaled":"Предпросмотр уменьшен до {n}%, чтобы поместиться. Сохранённый размер не изменится.","marker.preview.demo_activity":"Показать пример активности","marker.preview.demo_notice":"Временный пример активности; фактическое состояние устройства не меняется.","marker.preview.demo_short":"Показать краткую активность","marker.preview.demo_continuous":"Показать постоянную активность","marker.preview.stop_continuous":"Остановить постоянную активность","marker.preview.demo_short_notice":"Пример краткой активности; фактическое состояние устройства не меняется.","marker.preview.demo_continuous_notice":"Пример постоянной активности; фактическое состояние устройства не меняется.","marker.preview.demo_already_visible":"Текущее состояние уже показывает реальную активность или тревогу.","marker.preview.reduced_motion":"В системе включено уменьшение движения, поэтому обычная активность показана точкой.","marker.preview.reason.neutral":"Нейтральная тёмная подложка","marker.preview.reason.working":"Жёлтая подложка: устройство сейчас работает","marker.preview.reason.working_activity":"Жёлтая подложка и эффект активности: устройство сейчас работает","marker.preview.reason.open":"Оранжевая подложка: физически открыто или разблокировано","marker.preview.reason.cover_icon_state":"Нейтральная подложка; состояние и значок определяет сущность шторы или заслонки","marker.preview.reason.presence":"Эффект активности, пока обнаружено присутствие","marker.preview.reason.event":"Короткий эффект активности после события","marker.preview.reason.transition":"Эффект активности во время движения или смены состояния","marker.preview.reason.media_neutral":"Медиаустройство остаётся на нейтральной тёмной подложке, пока доступно","marker.preview.reason.unavailable":"Приглушённая нейтральная подложка: отключено или недоступно","marker.preview.reason.alarm":"Красная тревожная подложка; тревога отображается всегда","marker.preview.reason.live_states_disabled":"Оформление по живым состояниям отключено в настройках карточки","marker.preview.reason.value_no_state":"Нет подходящего значения состояния — вместо него показан значок","marker.preview.reason.value_ambiguous_sources":"Подходит несколько источников значения — вместо него показан значок","marker.preview.reason.value_non_scalar":"Источник вернул не простое значение — вместо него показан значок","marker.preview.reason.value_virtual":"У виртуального устройства нет значения HA — вместо него показан значок","marker.preview.reason.vacuum_live_plan_only":"Живая позиция и след пылесоса доступны только на полном плане","marker.preview.reason.hidden_design_preview":"Устройство скрыто на плане, но его оформление видно в предпросмотре","marker.preview.reason.composite_power_source":"Состояние берётся из сущности питания устройства; вспомогательные переключатели не учитываются","marker.preview.reason.activity_display_disabled":"Выбранный режим отображения не показывает обычные эффекты активности","marker.preview.reason.ha_disabled":"Привязка отключена в Home Assistant, поэтому устройство будет скрыто на плане","marker.preview.reason.orphaned":"Сохранённая привязка Home Assistant больше не найдена","marker.preview.reason.static_icon":"Статичный режим: состояние устройства не меняет значок","marker.activity_color":"Цвет пульсации активности","marker.ripple_size":"Размер пульсации активности","marker.activity_alarm_note":"Цвет и размер относятся только к обычной активности и не влияют на красную тревогу.","marker.pulse_a11y_alarm":"Тревога","marker.pulse_a11y_event":"Недавнее событие","marker.pulse_a11y_presence":"Обнаружено присутствие","marker.pulse_a11y_transition":"Изменение состояния","marker.pulse_a11y_running":"Работает","marker.state_a11y_neutral":"Обычное состояние","marker.state_a11y_working":"Работает","marker.state_a11y_open":"Открыто","marker.state_a11y_locked":"Заблокировано","marker.state_a11y_unlocked":"Разблокировано","marker.state_a11y_alarm":"Тревога","marker.state_a11y_unavailable":"Недоступно","marker.lqi_a11y_low":"LQI {value}, слабый сигнал","marker.lqi_a11y_mid":"LQI {value}, средний сигнал","marker.lqi_a11y_high":"LQI {value}, сильный сигнал","marker.size_label":"Размер / поворот значка","marker.angle_label":"Поворот","marker.model_label":"Модель","marker.model_ph":"напр. Aqara T&H","marker.link_label":"Ссылка","marker.desc_label":"Описание","marker.desc_ph":"Заметки, характеристики…","marker.manuals_label":"Инструкции (PDF и т.п.)","marker.sub_device":"устройство","marker.sub_z2m_group":" · Z2M-группа","marker.sub_group":"группа","marker.sub_helper":"хелпер","space.new":"Новое пространство","space.header":"Пространство","space.title_label":"Название","space.title_ph":"Например: Гараж","space.plan_label":"Подложка (план)","space.no_plan":"нет подложки","space.plan_alt":"план","room.new":"Новая комната","room.name_label":"Отображаемое имя","room.name_ph":"Например: Терраса","room.area_label":"Зона Home Assistant (свободные)","room.no_area_option":"— без зоны —","room.default_name":"Комната","device.unnamed":"без имени","device.light_group":"группа света","device.fallback":"устройство","device.virtual":"виртуальное устройство","confirm.delete_room":"Удалить комнату «{name}»?","confirm.delete_room_title":"Удалить комнату «{name}»?","confirm.delete_room_body":"Выберите, нужно ли оставить физические стены, принадлежащие только этой комнате. Общие с другой комнатой стены сохраняются всегда.","confirm.merge_rooms":"Слить комнаты «{a}» и «{b}»?","confirm.remove_marker":"Удалить «{name}» с плана? Устройство исчезнет полностью и перестанет участвовать в данных плана. Позже его можно будет добавить заново.","confirm.erase_decor":"Стереть объект «{kind}»? Действие можно отменить из истории редактора.","confirm.delete_space":"Удалить пространство «{title}» со всеми комнатами и разметкой?","space.delete_blocked":"Это пространство всё ещё используется устройствами: {n}. Сначала перенесите их в другое пространство или удалите.","toast.pos_save_failed":"Не удалось сохранить позицию: {err}","toast.no_entity":"У устройства нет подходящей сущности","toast.ha_disabled_action":"Деактивированный объект Home Assistant нельзя использовать на плане.","toast.ha_disabled_show_device":"Деактивированное в Home Assistant устройство нельзя показать на плане. Сначала активируйте его в Home Assistant.","toast.ha_disabled_show_entity":"Деактивированную в Home Assistant сущность нельзя показать на плане. Сначала активируйте её в Home Assistant.","toast.ha_disabled_add":"Деактивированный объект Home Assistant нельзя добавить на план. Сначала активируйте его в Home Assistant.","toast.ha_binding_unverified":"Статус объекта не удалось подтвердить по реестру Home Assistant. Отображение и действия временно недоступны.","toast.markup_needs_server":"Разметка доступна после переноса конфига на сервер","toast.space_order_changed":"Порядок изменён. Если где-то этаж карточки задан номером, проверьте такие панели.","toast.conflict":"Конфиг изменён в другом окне — данные обновлены, повторите последнее действие","toast.cfg_save_failed":"Не удалось сохранить конфиг: {err}","toast.room_overlap":"Контур накладывается на комнату «{name}» — комнаты не должны накладываться","toast.contour_min_edges":"Чтобы замкнуть контур, сначала нарисуйте минимум две грани","toast.contour_cannot_close":"Контур нельзя замкнуть: он вырожден или пересекает сам себя","toast.merge_not_adjacent":"Объединять можно только комнаты с общей стеной","toast.rooms_merged":"Комнаты объединены в «{name}»","toast.split_pick_wall":"Начните разрез на стене комнаты","toast.split_bad_cut":"Разрез — от стены до стены внутри комнаты, без пересечения стен и самого себя","merge.header":"Объединение комнат","merge.hint":"У объединённой комнаты одно имя и одна зона. Вторая зона освобождается — её устройства уйдут с плана, пока их не заберёт другая комната.","merge.keep":"Оставить","merge.no_area":"без зоны","room.split_header":"Новая комната после разделения","toast.room_saved":"Комната сохранена ({n}). Устройств добавлено: {added}. Обведите следующую или выйдите из разметки.","toast.room_saved_no_area":"Комната сохранена ({n}, без зоны). Обведите следующую или выйдите из разметки.","toast.marker_needs_server":"Редактирование устройств доступно после переноса конфига на сервер","toast.virtual_name_required":"Укажите имя виртуального устройства","toast.marker_saved":"Устройство сохранено","toast.marker_removed":"Устройство удалено с плана","toast.integration_missing":"Интеграция House Plan не установлена — управление недоступно","toast.plan_formats":"Поддерживаются SVG, PNG, JPG, WebP","toast.plan_required":"Загрузите подложку — план этажа обязателен","toast.space_added_onboard":"Пространство добавлено. Обведите комнаты: кликайте по точкам сетки и замкните контур.","toast.space_added":"Пространство добавлено","toast.space_saved":"Пространство сохранено","toast.space_deleted":"Пространство удалено","toast.delete_failed":"Ошибка удаления: {err}","toast.error":"Ошибка: {err}","toast.toggle_target_unavailable":"Цель «{name}» недоступна — действие не выполнено","toast.toggle_targets_unavailable":"Цели недоступны: {names}. Действие не выполнено","toast.file_failed":"Файл «{name}» не загружен: {err}","toast.files_attached":"Прикреплено файлов: {n}","err.unknown":"неизвестная ошибка","err.code":"код {code}","err.too_large":"файл больше {mb} МБ","err.bad_ext":"недопустимый тип (нужен PDF/изображение)","err.unauthorized":"нужны права администратора","editor.title":"Заголовок","editor.default_floor":"Стартовое пространство","editor.default_floor_missing":"Стартовое пространство «{id}» больше не существует. Выберите другое.","editor.floor":"Закреплённое пространство","editor.floor_none":"— не закреплять —","editor.floor_index":"YAML-индекс {index} (сохранён)","fixed_floor.loading":"Загружаем закреплённое пространство…","fixed_floor.invalid_title":"Закреплённое пространство недоступно","fixed_floor.invalid_body":"Проверьте floor в конфигурации карточки. Настроенное значение: {value}","editor.icon_size":"Размер иконок, % ширины плана","editor.show_temperature":"Показывать температуру","editor.live_states":"Живые состояния (вкл/выкл, открыто…)","editor.show_signal":"Показывать сигнал zigbee (LQI)","editor.language":"Язык интерфейса","editor.lang_auto":"Авто (профиль HA)","editor.lang_en":"English","editor.lang_ru":"Русский","title.icon_rules":"Правила иконок: какая MDI-иконка достаётся устройству по имени","rules.title":"Правила иконок","rules.hint":"Правила проверяются сверху вниз по строке «имя устройства + модель» (regex без учёта регистра); срабатывает первое совпадение. Если ничего не подошло — решает device class сущности, затем — иконка-заглушка.","rules.pattern_ph":"regex, напр. розетк|plug","rules.icon_ph":"mdi:power-socket-de","rules.add":"Добавить правило","rules.reset":"Сбросить к умолчаниям","rules.test_ph":"Проверьте имя устройства…","rules.invalid":"некорректный regex","rules.saved":"Правила иконок сохранены","btn.up":"Вверх","btn.down":"Вниз","tap.info":"Карточка устройства","tap.more_info":"Диалог HA (more-info)","tap.toggle":"Переключить состояние","marker.tap_label":"Действие по нажатию для этого устройства","tap.toggle_note":"Под селектором показано, какое состояние изменится. Защищённые устройства не переключаются с плана.","import.title":"Создать пространства из этажей HA","import.hint":"Home Assistant уже знает эти этажи. Отметьте, какие превратить в пространства плана — далее для каждого попросим картинку плана. Комнаты затем обводятся вручную по плану.","import.start":"Создать: {n}","import.manual":"Начать с нуля","import.progress":"Этаж {i} из {n}","import.done":"Пространства созданы. Обведите комнаты: кликайте по точкам сетки и замкните контур.","btn.skip":"Пропустить","space.scale_label":"Масштаб (размер клетки сетки)","space.scale_unit":"см на клетку","space.scale_unit_imperial":"дюйм на клетку","space.display_section":"Отображение","space.show_borders":"Всегда отображать границы комнат","space.zero_wall_style":"Стены нулевой толщины","space.zero_wall_dashed":"Пунктирные","space.zero_wall_solid":"Сплошные","space.zero_wall_help":"Пунктирные стены пропускают свет. Сплошные стены блокируют свет, даже при нулевой толщине.","space.show_names":"Отображать названия комнат (перетаскиваются)","space.room_color":"Цвет границ и названий","space.opacity":"Прозрачность","space.fill_label":"Заливка комнат","fill.none":"Нет","fill.lqi":"По силе зигби-сигнала","fill.light":"По освещению","fill.custom":"Свой цвет","space.custom_fill":"Цвет заливки","space.source_file":"У меня есть картинка плана","space.source_draw":"Нет подложки — нарисую комнаты вручную","space.orientation":"Холст","orient.landscape":"Альбомный","orient.portrait":"Портретный","orient.square":"Квадрат","fill.temp":"По температуре","space.temp_min":"Комфорт от","space.temp_max":"до","tip.temp_avg":"средняя температура:","tip.hum_avg":"средняя влажность:","space_card.button":"Перейти к пространству","space_card.not_found":"Пространство «{id}» не найдено","space_card.loading":"Загрузка…","continuity.restore_plan":"Восстанавливаем план…","continuity.restore_connection":"Восстанавливаем подключение к устройствам…","continuity.retry":"Повторить","editor.space":"Пространство","editor.show_button":"Показывать кнопку","editor.button_label":"Текст кнопки","editor.button_target":"Путь дашборда (куда вести)","marker.sub_entity":"сущность","title.general_settings":"Общие настройки","gs.title":"Общие настройки","gs.hint":"Цвета заливок действуют на все пространства; у каждого цвета своя прозрачность. Какой режим заливки использует пространство — задаётся в его диалоге.","gs.light_group":"Заливка: освещение","gs.light_on":"Свет включён","gs.light_off":"Весь свет выключен","gs.temp_group":"Заливка: температура","gs.temp_cold":"Холодно","gs.temp_ok":"Комфорт","gs.temp_hot":"Жарко","gs.lqi_group":"Заливка: зигби-сигнал","gs.lqi_low":"Слабый сигнал","gs.lqi_high":"Сильный сигнал","gs.reset":"Сбросить к умолчаниям","gs.saved":"Общие настройки сохранены","space.show_lqi":"Показывать зигби-сигнал (LQI) у устройств","space.hide_decor":"Скрыть декоративный слой","space.hide_decor_tip":"Линии, фигуры, надписи и мебель остаются на месте — их видно в редакторе подложки, но не на плане.","space.hide_openings":"Скрыть проёмы","space.hide_openings_tip":"Двери, окна и ворота не рисуются, но продолжают работать: свет проходит, солнце светит в окна, датчики открытия срабатывают. В редакторе плана проёмы видно всегда.","gs.light_none":"Нет источников света","mode.plan":"Редактор плана","mode.devices":"Редактор устройств","display.value":"Значение + состояние","marker.subarea":"без зоны, вручную","device.new":"Новое устройство — откройте его редактор, чтобы снять отметку","opening.unlock_action":"Открыть замок","opening.lock_action":"Закрыть замок","opening.lock_pending":"Выполняется…","title.close_editor":"Закрыть редактор (вернуться к просмотру)","devbar.add":"Добавить","devbar.show_all":"Скрытые и деактивированные","devbar.rules":"Правила иконок","device_inbox.button":"Устройства","device_inbox.title":"Устройства на плане","device_inbox.search":"Поиск по устройствам, сущностям и интеграциям…","device_inbox.add_virtual":"Добавить виртуальное устройство","device_inbox.tab_on_plan":"На плане","device_inbox.tab_available":"Доступны","device_inbox.tab_hidden":"Скрытые","device_inbox.tab_readd":"Доступны снова","device_inbox.only_new":"Только новые","device_inbox.show_entities":"Показывать сущности","device_inbox.show_hidden":"Показывать скрытые на плане","device_inbox.new":"Новое","device_inbox.find":"Найти на плане","device_inbox.edit":"Настроить","device_inbox.hide":"Скрыть","device_inbox.show":"Показать","device_inbox.add":"Добавить","device_inbox.readd":"Добавить заново","device_inbox.hide_available":"Скрыть из списка","device_inbox.show_more":"Показать ещё","device_inbox.more_actions":"Другие действия","device_inbox.find_hidden_hint":"Сначала включите «Показывать скрытые на плане»","device_inbox.show_disabled":"Перед показом активируйте эту привязку в Home Assistant","device_inbox.empty_on_plan":"На плане пока нет подходящих устройств.","device_inbox.empty_available":"Нет доступных устройств для добавления.","device_inbox.empty_hidden":"Скрытых устройств нет.","device_inbox.empty_readd":"Нет удалённых устройств, доступных для повторного добавления.","device_inbox.reason_visible_auto":"Найдено автоматически","device_inbox.reason_visible_explicit":"Добавлено явно","device_inbox.reason_manual_hidden":"Скрыто пользователем","device_inbox.reason_automatic_hidden":"Скрыто автоматически","device_inbox.reason_service_entry":"Служебная запись реестра","device_inbox.reason_excluded_integration":"Интеграция исключена фильтрами устройств","device_inbox.reason_excluded_domain":"Непространственный тип сущности","device_inbox.reason_grouped_light":"Представлено группой света комнаты","device_inbox.reason_represented_by_parent":"Представлено родительским устройством","device_inbox.reason_removed":"Ранее удалено с плана","device_inbox.reason_available":"Можно добавить на план","device_inbox.reason_no_bound_room":"Зона HA не связана с комнатой плана","device_inbox.status_ha_disabled":"Отключено в Home Assistant","device_inbox.status_orphaned":"Привязка больше не найдена в Home Assistant","device_inbox.status_unverified":"Реестр Home Assistant временно недоступен","device_inbox.saved":"Список устройств обновлён","space.roomcard_section":"В карточке комнаты:","space.label_temp":"Температура","space.label_hum":"Влажность","space.label_lqi":"Средний Zigbee-сигнал","space.label_light":"Свет вкл/выкл","roomcard.light_on":"Вкл","roomcard.light_off":"Выкл","roomcard.light_partial":"{on} из {total}","toast.split_pick_inside":"Промежуточные точки разреза — внутри комнаты","mode.decor":"Редактор подложки","decor.select":"Выбрать","decor.line":"Линия","decor.rect":"Прямоугольник","decor.ellipse":"Овал","decor.text":"Надпись","decor.erase":"Стереть","decor.erase_confirm_title":"Стереть объект?","decor.color":"Цвет","decor.width":"Толщина линии","decor.line_style":"Стиль линии","decor.line_style_solid":"Сплошная","decor.line_style_dashed":"Пунктирная","decor.fill":"Заливка","decor.fill_color":"Цвет заливки","decor.length":"Длина","decor.size":"Размер","decor.angle":"Угол поворота, °","decor.text_size":"Размер текста","decor.backdrop_properties":"Свойства картинки-подложки","decor.text_title":"Надпись","decor.object_title":"Редактирование: {kind}","decor.text_label":"Текст","decor.live_group":"Вставить переменную HA","decor.live_entity":"Сущность","decor.live_entity_ph":"выберите сущность","decor.live_attr":"Значение","decor.live_attr_ph":"выберите состояние или атрибут","decor.live_state":"Состояние","decor.backdrop":"Картинка-подложка","decor.backdrop_hint":"Тяните картинку — перемещение; угол — размер; верхнюю ручку — поворот. Shift меняет пропорции или снимает шаг угла.","decor.backdrop_reset":"Вернуть картинку","decor.backdrop_reset_done":"Картинка вернулась на своё место и в свой размер","marker.icon_auto":"Авто: {icon} (по правилам иконок; выберите свою, чтобы заменить)","marker.icon_pin_auto":"Закрепить","mode.plan_tip":"Редактор плана — геометрия дома: рисование и объединение/разделение комнат, привязка к зонам HA, двери, окна и ворота, карточки комнат, масштаб","mode.devices_tip":"Редактор устройств — всё про значки: перетаскивание, клик — настройка привязки/иконки/отображения, виртуальные устройства, правила иконок","mode.decor_tip":"Редактор подложки — чисто визуальный декор под планом: линии, прямоугольники, овалы и надписи, не реагирующие на клики","space.glow_enabled":"Свечение источников света","gs.glow_group":"Свечение источников света","gs.glow_base":"Темнота дома","gs.glow_light":"Цвет света по умолчанию / интенсивность","gs.wall_group":"Стены","gs.wall_fill":"Цвет заливки стен","gs.glow_radius":"Радиус свечения","gs.unit_m":"м","gs.unit_ft":"фут","marker.controls_label":"Управляет другими источниками света","marker.controls_hint":"При действии «Переключить» клик разом переключает все добавленные здесь источники. Собственная сущность маркера управляется напрямую и сюда не добавляется; чтобы считать её светом, включите флаг ниже.","marker.controls_filter":"Поиск ламп и выключателей…","info.controls":"Управляет","marker.glow_radius_label":"Радиус свечения","marker.glow_radius.help":"Задаёт радиус свечения в метрах или футах; пустое значение использует радиус из общих настроек.","marker.glow_radius.help.aria":"Подсказка: радиус свечения","markup.wallthick":"Толщина","markup.select":"Выбрать","title.markup_wallthick":"Толщина — клик по стене задаёт толщину от 0 до 100 см.","markup.hint_wallthick":"клик по стене · Esc закрывает без применения","wallthick.field":"Толщина","wallthick.unit_cm":"см","wallthick.unit_in":"дюйм","wallthick.apply_room":"Применить ко всем стенам комнаты","markup.draw_wall_title":"Толщина каждого нового сегмента стены (0–100 см). Общие стены сохраняют существующее значение.","room.queue_progress":"Комната {current} из {total}","toast.wall_rooms_saved":"Создано комнат: {n}","toast.wall_chain_saved":"Цепочка стен сохранена","toast.wallthick_pick":"Кликните по стене","toast.wallthick_set":"Толщина стены задана","toast.wallthick_cleared":"Толщина стены убрана","toast.physical_range":"Введите значение от {min} до {max} {unit}","toast.zero_wall_opening_conflict":"Сначала удалите проём на этом участке стены.","toast.zero_wall_ambiguous":"Не удалось однозначно выбрать участок стены. Уточните геометрию узла.","toast.zero_wall_migration_blocked":"Пространство не преобразовано: {reason}. Данные не изменены.","toast.physical_angle":"Введите угол поворота от 0° до 90°, не включая 90°","toast.physical_limit":"В пространстве достигнут лимит объектов этого типа","toast.geometry_unsafe":"Изменение отменено: геометрию стен нельзя безопасно построить.","junction.limit_angle":"Слишком острый угол между стенами: {actual}°, минимум {limit}°.","junction.limit_valence":"Слишком много стен в одном узле: {actual}, максимум {limit}.","junction.limit_length":"Слишком короткий участок стены: {actual} см, минимум {limit} см.","junction.limit_distance":"Стены и узлы слишком близко: {actual} см, минимум {limit} см.","junction.limit_clearance":"Внутри комнаты не остаётся места: {actual} см², минимум {limit} см².","junction.limit_check_failed":"Проверку стыков выполнить не удалось — изменение не сохранено","toast.wall_model_migration_blocked":"Не удалось обновить модель стен: {reason}. План не изменён. Запустите «Оптимизировать планы»; если ошибка повторится, исправьте конфликтующую геометрию стен.","toast.wall_model_client_outdated":"Обновите карточку и перезагрузите страницу перед редактированием плана.","wall_model.reason.invalid-room":"некорректный контур комнаты","wall_model.reason.zero-length":"отрезок стены нулевой длины","wall_model.reason.third-owner":"стена принадлежит более чем двум комнатам","wall_model.reason.duplicate-id":"конфликт идентификаторов стен","wall_model.reason.thickness-conflict":"конфликт значений толщины стены","wall_model.reason.opening-host":"проём нельзя однозначно привязать к стене","toast.delete_room_pick":"Кликните внутри комнаты, которую нужно удалить","toast.plan_snap_ambiguous":"Увеличьте масштаб, чтобы выбрать узел стены","toast.wall_repair_ambiguous":"У контура несколько возможных соединений. Увеличьте масштаб и соедините стены явно.","toast.wall_repair_too_large":"Разрыв больше 2 см. Соедините стены перед созданием комнаты.","toast.wall_repair_changed":"Геометрия стен изменилась. Попробуйте создать комнату ещё раз.","toast.opening_on_zero_wall":"Проёмы на стене нулевой толщины запрещены","marker.from_ha_option":"Выбрать из списка HA","marker.show_entities":"Показывать сущности","marker.show_entities_tip":"Добавляет в список не только устройства, но и все их сущности","marker.pick_ph":"Выберите устройство…","room.open_area":"Открыть зону в HA","view.volumetric":"Объёмный вид","view.flat":"Плоский вид","kiosk.title":"Размеры на этом экране","kiosk.hint":"Хранится только на этом устройстве — у каждого настенного планшета или ТВ свои удобные размеры.","kiosk.icon_scale":"Размер значков устройств","kiosk.font_scale":"Размер текста карточек комнат","editor.kiosk":"Режим настенного устройства (киоск)","editor.cycle":"Автосмена пространств каждые N секунд (киоск, 0 = выкл)","room.settings_title":"Настройки комнаты","room.settings_section":"Настройки комнаты (переопределяют пространство)","room.fill_label":"Заливка в ЭТОЙ комнате","fill.inherit":"Как у пространства","room.custom_fill_space":"Цвет пространства","room.custom_fill_own":"Цвет комнаты","room.temp_src_label":"Источник температуры","room.hum_src_label":"Источник влажности","room.src_average":"Средняя по датчикам комнаты (по умолчанию)","room.src_pick":"Конкретное устройство или сущность HA","room.src_ph":"Выберите источник…","toast.room_updated":"Комната обновлена","space.card_font":"Размер шрифта карточек комнат (всё пространство)","room.sizes_section":"Размеры шрифтов","room.name_scale":"Размер названия","room.label_scale":"Размер подписей","preview.room_name":"Гостиная","toast.cfg_reload_failed":"Не удалось перечитать план с сервера: {err}","room.settings_short":"Настройки комнаты","room.unnamed":"Комната без имени","marker.use_climate_temp":"Учитывать температуру устройства в комнате","marker.use_climate_temp_tip":"Добавляет current_temperature кондиционера или термостата в среднюю температуру комнаты. Внешний бейдж со значением настраивается отдельно ниже.","marker.value_badge_title":"Бейдж со значением","marker.value_badge.help":"Показывает одно выбранное значение рядом со значком. Не влияет на комнатные показатели, свет, Glow и действие по нажатию.","marker.value_badge.help.aria":"Подсказка: бейдж со значением устройства","marker.value_badge_enabled":"Отображать бейдж со значением","marker.value_badge_source":"Значение","marker.value_badge_source.help":"Выберите конкретное состояние, поддерживаемый атрибут или производное значение этого устройства.","marker.value_badge_source.help.aria":"Подсказка: источник значения бейджа","marker.value_badge_position":"Расположение","marker.value_badge_position.help":"Выбранная сторона сохраняется постоянно и не переворачивается автоматически у края плана.","marker.value_badge_position.help.aria":"Подсказка: расположение бейджа","marker.value_badge_right":"Справа","marker.value_badge_bottom":"Снизу","marker.value_badge_left":"Слева","marker.value_badge_top":"Сверху","marker.value_badge_empty":"У этого устройства нет доступных значений","marker.value_badge_static":"Статичный значок не показывает живые значения. Настройка сохранится.","marker.value_badge_missing":"Источник недоступен","marker.value_badge_missing_hint":"Сохранённый источник сейчас недоступен. Можно сохранить его, отключить бейдж или выбрать другой.","marker.value_badge_duplicate":"То же значение уже показано внутри значка.","marker.value_badge_state":"Состояние · {name}","marker.value_badge_marker_state":"Состояние источника света · {name}","marker.value_badge_lqi":"Качество Zigbee-сигнала (среднее)","marker.value_badge_attr_current_temperature":"Текущая температура · {name}","marker.value_badge_attr_temperature":"Целевая температура · {name}","marker.value_badge_attr_current_humidity":"Текущая влажность · {name}","marker.value_badge_attr_humidity":"Влажность · {name}","marker.value_badge_attr_current_position":"Положение · {name}","marker.value_badge_attr_percentage":"Скорость · {name}","marker.value_badge_attr_brightness":"Яркость · {name}","marker.value_badge_attr_volume_level":"Громкость · {name}","marker.value_badge_attr_battery_level":"Заряд батареи · {name}","marker.value_badge_attr_fan_speed":"Скорость вентилятора · {name}","marker.light_role_label":"Является источником света","marker.light_role.help":"Определяет, является ли сам маркер пространственным источником света. Привязанные выше лампы остаются независимыми источниками для комнаты.","marker.light_role.help.aria":"Подсказка: является ли устройство источником света","marker.light_role_auto_yes":"Автоматически (источник света)","marker.light_role_auto_no":"Автоматически (не источник света)","marker.light_role_always":"Всегда источник света","marker.light_role_never":"Никогда не источник света","marker.light_entity_label":"Ведущая сущность света","marker.light_entity.help":"Для составного устройства выберите сущность, состояние, цвет и яркость которой представляют этот источник на плане. Автоматический вариант сохраняет совместимость со старыми планами.","marker.light_entity.help.aria":"Подсказка: ведущая сущность света","marker.light_entity_auto":"Автоматически ({entity})","marker.light_entity_none":"нет управляемой сущности","marker.light_entity_missing":"Сохранённая сущность {entity} недоступна. Временно используется {fallback}; выбор восстановится, если сущность вернётся.","marker.toggle_entity_label":"Переключаемая сущность","marker.toggle_entity.help":"Для составного устройства выберите собственную лампу или переключатель, которой управляет нажатие. Автоматический вариант сохраняет прежние правила цели и не зависит от ведущей сущности света.","marker.toggle_entity.help.aria":"Подсказка: переключаемая сущность","marker.toggle_entity_auto":"Автоматически ({entity})","marker.toggle_entity_none":"нет собственной управляемой сущности","marker.toggle_entity_missing":"Сохранённой сущности {entity} больше нет среди выбираемых каналов этого маркера. Временно используется {fallback}; выбор восстановится, если сущность вернётся.","marker.glow_color_label":"Цвет и яркость свечения","marker.glow_mode.help":"Используйте данные источника, задайте только цвет или зафиксируйте цвет и яркость. Минимум — 1%; чтобы отключить источник, выберите «Никогда».","marker.glow_mode.help.aria":"Подсказка: цвет и яркость свечения","marker.glow_mode_auto":"Из источника","marker.glow_mode_color":"Задать цвет","marker.glow_mode_fixed":"Задать цвет и яркость","marker.glow_color":"Цвет свечения","marker.glow_brightness":"Яркость","marker.glow_disabled_never":"Настройки свечения недоступны: маркер явно исключён из источников света.","marker.glow_disabled_auto":"В автоматическом режиме у этого маркера не найден пространственный источник света.","marker.glow_disabled_no_entity":"Для пространственного источника нет активной управляемой сущности Home Assistant.","marker.glow_passive_hint":"У источника нет собственных данных Home Assistant. Задайте цвет и яркость вручную; радиус остаётся доступным.","marker.control_broken":"Сохранённый источник отсутствует или больше не отмечен как источник света","marker.control_missing_label":"Отсутствующий источник на плане","marker.control_passive":"пассивный источник","toast.marker_control_cycle":"Такая связь создаст циклическую цепочку управления светом.","toast.marker_binding_required":"Сначала выберите устройство Home Assistant, затем добавьте управляемый источник света.","confirm.unlock":"Открыть замок «{name}»?","toast.files_migrate_failed":"Не удалось перенести вложения к новой привязке, ссылки остались на старые файлы: {err}","space.pick_saved":"Уже загруженные","space.pick_saved_hint":"Планы, сохранённые на сервере, включая отцеплённые ранее","space.no_saved":"На сервере пока нет сохранённых планов.","space.loading":"Загрузка…","space.used_by":"используется: {list}","space.in_use":"План используется пространством — сначала отцепите его","btn.use":"Выбрать","confirm.delete_plan":"Удалить файл плана «{name}» с сервера? Действие необратимо.","toast.plans_list_failed":"Не удалось получить список планов: {err}","toast.plan_delete_failed":"Не удалось удалить план: {err}","marker.hide":"Скрыть","marker.hide_tip":"Устройство исчезнет с плана после сохранения, но продолжит участвовать в расчёте сигнала комнаты. Вернуть его можно через каталог «Устройства» в редакторе устройств.","marker.show":"Показать","marker.show_tip":"Устройство снова появится на плане после сохранения.","marker.hidden_ghost":"Устройство скрыто пользователем","marker.ha_disabled_device":"Устройство деактивировано в Home Assistant и скрыто с плана.","marker.ha_disabled_entity":"Сущность деактивирована в Home Assistant и скрыта с плана.","marker.ha_disabled_all_entities":"У устройства нет активных сущностей Home Assistant, поэтому оно скрыто с плана.","marker.ha_registry_limited":"Полный реестр Home Assistant недоступен этому пользователю. Неподтверждённый объект временно нельзя показывать или использовать.","marker.delete_tip":"Полностью удалить устройство с плана и из всех расчётов. Позже его можно добавить заново.","tap.run":"Запустить автоматизацию/скрипт/сцену","tap.cover":"Открыть/закрыть (шторы/жалюзи)","marker.run_target_label":"Что запускать","marker.run_search_ph":"Поиск: автоматизация, скрипт или сцена…","marker.run_target_gone":"Цель {id} не найдена — выберите заново","marker.tap_confirm":"Спрашивать подтверждение","marker.tap_confirm_tip":"Перед выполнением показать диалог подтверждения — защита от случайных нажатий.","marker.toggle_hint_single":"Цель: {name} ({id}).","marker.virtual_light_target":"Виртуальный свет: {name}.","marker.virtual_light_current":"Ручное состояние: {state} → {effect}.","marker.virtual_light_state_on":"включён","marker.virtual_light_state_off":"выключен","marker.toggle_hint_group":"Переключатся источники ({count}): {names}.","marker.toggle_hint_current":"Сейчас: {state} → {effect}.","marker.toggle_hint_group_current":"Сейчас включено {on} из {count} → {effect}.","marker.toggle_hint_skipped":"Не будут затронуты ({count}): {targets}.","marker.toggle_effect_turn_on":"включится","marker.toggle_effect_turn_off":"выключится","marker.toggle_effect_open":"откроется","marker.toggle_effect_close":"закроется","marker.toggle_effect_stop":"остановится","marker.toggle_effect_toggle":"состояние переключится","marker.toggle_skip_missing":"не найдено","marker.toggle_skip_ha_disabled":"деактивировано в HA","marker.toggle_skip_unavailable":"недоступно","marker.toggle_skip_unsupported":"переключение не поддерживается","marker.toggle_skip_secure":"заблокировано из соображений безопасности","marker.toggle_none_no_binding":"У устройства нет привязки, состояние переключать нечем. По нажатию ничего не произойдёт.","marker.toggle_none_no_actionable_entity":"У устройства нет состояния, которое можно переключить. По нажатию ничего не произойдёт.","marker.toggle_none_configured_targets_missing":"Настроенные цели сейчас недоступны. Собственная сущность устройства не будет подставлена вместо них.","marker.toggle_none_ha_disabled":"Цель деактивирована в Home Assistant и не может использоваться на плане.","marker.toggle_none_unavailable":"Цель сейчас недоступна. По нажатию ничего не произойдёт.","marker.toggle_none_unsupported":"Для этой сущности Home Assistant не предоставляет безопасного переключения. По нажатию ничего не произойдёт.","marker.toggle_none_secure":"Переключение замков, сигнализации и защитных ворот с плана заблокировано из соображений безопасности.","run.automation":"автоматизация","run.script":"скрипт","run.scene":"сцена","confirm.tap_run":"Запустить «{name}»?","confirm.tap_toggle":"Переключить «{name}»?","confirm.tap_cover":"Открыть/закрыть «{name}»?","confirm.current_state":"Текущее состояние: {state}","confirm.expected_state":"После переключения: {state}","confirm.group_current":"включено {on} из {total}","confirm.group_all_on":"все включены","confirm.group_all_off":"все выключены","confirm.unavailable_targets":"Недоступно: {count}","confirm.expected_by_ha":"Состояние определит Home Assistant","confirm.state_on":"Включено","confirm.state_off":"Выключено","confirm.state_open":"Открыто","confirm.state_closed":"Закрыто","confirm.state_opening":"Открывается","confirm.state_closing":"Закрывается","confirm.state_stopped":"Остановлено","confirm.state_unknown":"Неизвестно","toast.run_started":"Запущено: {name}","toast.run_target_missing":"Цель запуска не найдена — проверьте настройки устройства","toast.run_target_required":"Выберите автоматизацию, скрипт или сцену","toast.tap_target_changed":"Цель действия изменилась. Повторите попытку.","toast.virtual_light_toggle_failed":"Не удалось переключить виртуальный свет: {err}","toast.value_badge_source_required":"Выберите значение для бейджа","btn.run":"Выполнить","vac.section":"Робот-пылесос: живая позиция","vac.autocal":"Настроить автоматически","vac.live":"Живая позиция на плане","vac.trail":"Показывать путь робота","vac.cal_maps":"Откалиброваны карты: {maps}","vac.autocal_no_rooms":"Интеграция не отдаёт список комнат — откройте «Подогнать вручную»","vac.autocal_no_match":"Не совпали имена комнат (нужно ≥3 общих) — откройте «Подогнать вручную»","vac.autocal_done":"Готово: привязка по {rooms} комнатам. Запустите уборку и проверьте","vac.cal_need_pos":"Робот сейчас не отдаёт координаты — запустите уборку и поставьте на паузу","vac.cal_done":"Калибровка сохранена. Запустите уборку и проверьте","vac.cal_cancelled":"Калибровка отменена","vac.fit":"Подогнать вручную","vac.fit_hint":"Перетащите карту робота на место, растяните за уголки","vac.fit_rotate":"Повернуть 90°","vac.fit_mirror":"Отразить","vac.trail_never":"Не показывать никогда","vac.trail_cleaning":"Во время уборки","vac.trail_always":"Показывать всегда","gs.bg_group":"Фон сцены","gs.bg_color":"Цвет фона вокруг плана","gs.bg_default":"Как в теме","gs.bg_theme":"по умолчанию — из темы","gs.bg_mode":"Фон плана","gs.bg_static":"Статичный цвет","gs.bg_daynight":"Следует за Солнцем","gs.bg_daynight_hint":"Фон проходит рассвет, день, закат и ночь по данным sun.sun, а при их недоступности — по локальным часам. Компас для фона не нужен.","gs.sun_group":"Солнце","gs.sun_missing":"Сущность sun.sun не найдена — фон следует локальным часам, а оконные лучи недоступны.","gs.north":"Север на плане","gs.north_ph":"не задан","gs.north_hint":"Север нужен только для оконных лучей (шаг 1°, с Shift 15°); солнечный фон работает без него.","gs.north_clear":"Сбросить","gs.north_letter":"С","gs.sun_rays":"Солнце в окнах","gs.about_group":"О карточке","gs.about_version":"Houseplan Card v{v}","gs.about_github":"GitHub · документация и issues","gs.about_telegram":"Чат в Telegram","space.bg_color":"Цвет фона вокруг плана","space.bg_inherit":"Наследовать общий","space.bg_inherited":"наследуется из общих настроек","space.bg_mode":"Фон плана","space.north":"Север на плане (переопределение)","space.north_inherited":"наследуется: {v}","space.sun_rays":"Солнце в окнах","space.sun_inherit":"Наследовать общий","space.sun_on":"Вкл","space.sun_off":"Выкл","canvas.far_objects":"Объектов далеко от плана: {n}","canvas.show_far":"Показать","canvas.home_tip":"План там — нажмите, чтобы вписать","gs.grid_group":"Обслуживание планов","gs.grid_hint":"Обновляет модели данных, выравнивает элементы по сетке и объединяет лишние фрагменты стен. Перед записью будет показан точный отчёт.","gs.align_all":"Оптимизировать планы","gs.align_title":"Оптимизировать планы","gs.align_none":"Все планы уже используют актуальную и оптимальную модель данных.","gs.optimize_no_automatic_changes":"Автоматических изменений нет. Проверьте пункты ниже.","gs.align_count":"Сдвинется элементов: {n} из {total}, максимум на {cm} см.","gs.align_where":"Наибольший сдвиг — в пространстве «{s}».","gs.align_turned":"Проёмов с исправлением угла: {n}.","gs.align_removed_drafts":"Схлопнувшиеся на сетке некорректные контуры удалены: {n}.","gs.optimize_redundant_drafts":"Сохранённые цепочки стен, полностью скрытые стенами комнат, удалены: {n}.","gs.align_preflight_failed":"Не удалось безопасно проверить геометрию следующих пространств: {spaces}{more}.","gs.align_preflight_hint":"Планы не изменены. Скопируйте диагностику кнопкой ниже и приложите её к отчёту об ошибке вместе с экспортом пространства.","gs.preflight_reason_prepare-exception":"Не удалось подготовить геометрию пространства (исключение при сборке модели)","gs.preflight_reason_wall-null":"Тело стен не построилось (объединение вернуло пустоту)","gs.preflight_reason_wall-degraded-extra":"Тело стен деградировало с лишней геометрией","gs.preflight_reason_wall-failed-core":"Ядро тела стен не собралось","gs.preflight_reason_wall-exception":"Построение стен упало с исключением","gs.preflight_reason_floor-null":"Контур пола не построился","gs.preflight_reason_floor-exception":"Построение пола упало с исключением","gs.preflight_copy":"Скопировать диагностику","gs.preflight_copied":"Диагностика скопирована","gs.preflight_update_hint":"Версии карточки и интеграции различаются — обновите House Plan и повторите.","gs.align_preflight_space":"Пространство {n}","gs.align_preflight_more":" и ещё {n}","gs.optimize_changes":"Миграций модели: {m}; обновлено пространств: {c}; устранён шум координат: {p}; объединено отрезков реальных стен: {w}; объединено отрезков стен нулевой толщины: {s}; независимых: {i}.","gs.zero_walls_migrated":"Преобразовано виртуальных участков: {n}.","gs.wall_segments_migrated":"Стабилизировано сегментов стен: {n}.","gs.optimize_lattice_summary":"Канонизировано шумовых значений координат: {n}; максимальный сдвиг: {cm} см.","gs.optimize_lattice_space":"{space}: канонизировано значений координат: {n}; оставлено значений вне сетки: {far}.","gs.optimize_coincident_partitions":"Скрытые участки независимых стен поглощены стенами комнат: {n}.","gs.optimize_openings_rehosted":"Проёмы перепривязаны к стенам комнат: {n}.","gs.optimize_walls_straightened":"Выпрямлено стен: {n}; максимальное перемещение: {cm} см.","gs.optimize_walls_straightened_where":"Максимальная правка стены: {s}.","gs.optimize_walls_straighten_skipped":"Оставлено почти осевых стен, которые нельзя безопасно выпрямить: {n}.","gs.optimize_glow_migration":"Старый Glow: пространств — {spaces} → без заливки данных + независимый Glow; комнат — {rooms} → наследуемая заливка + независимый Glow.","gs.optimize_references":"Исправлено ссылок: пространства — {spaces}; комнаты — {rooms}; позиции — {positions}; устройств отвязано от отсутствующих пространств — {detached}.","gs.optimize_reference_more":", и ещё {n}","gs.optimize_orphans_removed":"Убрано забытых записей: {total} — подписи комнат: {rooms}; устройства: {devices}; групповые метки: {groups}. Все они принадлежали пространствам, удалённым ранее.","gs.optimize_live_positions":"Старые позиции в удалённых пространствах принадлежат существующим объектам: {n}{names}. Они будут сохранены.","gs.optimize_live_positions_remove":"Старые позиции в удалённых пространствах принадлежат существующим объектам: {n}{names}. Они выбраны для удаления.","gs.optimize_live_names":": {names}{more}","gs.optimize_live_remove":"Убрать старые позиции","gs.optimize_live_keep":"Сохранить старые позиции","gs.optimize_live_selected":"Старые позиции будут убраны после применения оптимизации.","gs.optimize_unverified":"Не удалось безопасно проверить позиций: {n}. Они оставлены без изменений.","gs.optimize_registry_limited":"Для безопасной проверки нужен полный доступ администратора к реестрам Home Assistant.","gs.optimize_vacuum_warning":"Сопоставления комнат пылесоса требуют проверки: {n}.","gs.optimize_details":"Подробности","gs.optimize_details_more":"И ещё записей: {n}.","gs.optimize_detail_removed":"будет удалено","gs.optimize_detail_live":"будет сохранено","gs.optimize_detail_unverified":"не проверено","gs.optimize_detail_room_label":"подпись комнаты","gs.optimize_detail_device":"устройство","gs.optimize_detail_group":"групповая метка","gs.optimize_detail_unknown":"неизвестный владелец","gs.optimize_detail_item":"{status}: {kind} {id}; прежнее пространство {space}","gs.align_warn":"Элементы, намеренно поставленные между узлами, будут сдвинуты. После операции доступна одна отмена — только до следующего изменения плана.","gs.align_run":"Оптимизировать","gs.align_done":"Планы оптимизированы: сдвинуто элементов — {n}, обслужено записей — {m}, исправлено ссылок — {r}","gs.optimize_undo":"Отменить последнюю оптимизацию","gs.optimize_undone":"Последняя оптимизация отменена","decor.furniture":"Мебель","furn.title":"Библиотека мебели","furn.symbol":"Символ","furn.group_furniture":"Мебель","furn.group_appliance":"Техника","furn.group_sanitary":"Сантехника","furn.group_other":"Прочее","furn.width":"Ширина","furn.depth":"Глубина","furn.pick_hint":"Выберите символ и кликните по плану.","furn.place_hint":"Кликните по плану — предмет встанет к ближайшей стене. Shift — свободно.","furn.sym_sofa":"Диван","furn.sym_armchair":"Кресло","furn.sym_coffee_table":"Журнальный столик","furn.sym_table_dining":"Обеденный стол","furn.sym_table_round":"Круглый стол","furn.sym_chair":"Стул","furn.sym_desk":"Письменный стол","furn.sym_bed_double":"Двуспальная кровать","furn.sym_bed_single":"Односпальная кровать","furn.sym_nightstand":"Тумбочка","furn.sym_wardrobe":"Шкаф","furn.sym_bookshelf":"Стеллаж","furn.sym_fridge":"Холодильник","furn.sym_stove":"Плита","furn.sym_dishwasher":"Посудомоечная машина","furn.sym_washer":"Стиральная машина","furn.sym_dryer":"Сушильная машина","furn.sym_tv":"Телевизор","furn.sym_ac":"Кондиционер","furn.sym_water_heater":"Бойлер","furn.sym_toilet":"Унитаз","furn.sym_bathtub":"Ванна","furn.sym_shower":"Душ","furn.sym_sink":"Раковина","furn.sym_kitchen_sink":"Кухонная мойка","furn.sym_bidet":"Биде","furn.sym_stairs":"Лестница","furn.sym_fireplace":"Камин","furn.sym_plant":"Растение","furn.sym_rug":"Ковёр","common.yes":"Да","common.no":"Нет","vac.diag_source":"Источник","vac.diag_platform":"Интеграция","vac.diag_status":"Статус","vac.diag_position":"Позиция","vac.diag_rooms":"Комнаты","vac.diag_rooms_value":"{total} · совпало имён: {matched} · {readiness}","vac.autocal_ready":"автокалибровка доступна","vac.autocal_not_ready":"нужно 3 совпавших имени","vac.diag_path":"Путь интеграции","vac.diag_map":"ID карты","vac.source_none":"не выбран","vac.source_status_ok":"Готов","vac.source_status_missing":"Не найден","vac.source_status_disabled":"Деактивирован в Home Assistant","vac.source_status_unavailable":"Недоступен","vac.source_status_unverified":"Нельзя проверить с текущими правами","vac.source_status_unsupported":"Нет данных позиции","vac.source_status_none":"Нет источника","vac.source_banner_missing":"Сохранённый источник больше не существует. Он не заменён автоматически: выберите другой или восстановите его в Home Assistant.","vac.source_banner_disabled":"Сохранённый источник деактивирован в Home Assistant. Активируйте его там или выберите другой.","vac.source_banner_unverified":"Текущих прав Home Assistant недостаточно для проверки источника. Привязка сохранена и не будет заменена автоматически.","vac.choose_source":"Выбрать источник","vac.source_auto":"Автоматически","vac.source_auto_hint":"Использовать совместимую сущность этого устройства","vac.all_cameras":"Все камеры","vac.all_cameras_warn":"Камера может не отдавать данные робота. Выбирайте её только если это карта вашего пылесоса.","vac.all_cameras_empty":"Других сущностей камеры не найдено.","vac.platform_unknown":"интеграция неизвестна","vac.cap_position":"позиция","vac.cap_rooms_short":"комнаты","vac.cap_path":"путь","vac.cap_map":"ID карты","vac.cap_none":"данные робота не обнаружены","vac.xcme_hint":"Включите атрибуты Xiaomi Cloud Map Extractor:","vac.documentation":"Документация","vac.residual_title":"Проверьте автокалибровку","vac.residual_message":"Совпавшие комнаты расходятся максимум на {error}. Примените приблизительную калибровку, подгоните её вручную либо отмените без изменения сохранённых настроек.","vac.apply_proposal":"Применить","gs.backup_group":"Резервная копия и перенос","gs.backup_hint":"Скачайте переносимую JSON-копию или проверьте и импортируйте копию, созданную House Plan.","backup.export_open":"Экспорт","backup.import_open":"Импорт","backup.export_title":"Экспорт House Plan","backup.import_title":"Импорт House Plan","backup.export_hint":"Выберите, должна ли копия содержать всю конфигурацию House Plan или только текущее пространство.","backup.full":"Полная копия","backup.current_space":"Текущее пространство","backup.current_space_title":"Текущее пространство: {title}","backup.no_current_space":"Нет текущего пространства","backup.plan_only":"Только планировка","backup.plan_only_hint":"Сохранить комнаты, стены, проёмы, декор и позиции подписей комнат без устройств и привязок Home Assistant.","backup.plan_only_preview":"Файл содержит только планировку","backup.privacy_warning":"Архив сохраняет названия, идентификаторы Home Assistant и точные координаты. Внутренние планы и вложения указываются ссылками, но не вкладываются; текущие состояния и маршруты пылесосов не включаются.","backup.download":"Скачать JSON","backup.export_done":"Резервная копия скачана","backup.reading":"Проверяем резервную копию…","backup.revalidated":"После предпросмотра план изменился. Сводка обновлена — проверьте её и подтвердите ещё раз.","backup.error.unauthorized":"У вас нет прав на экспорт или импорт этого плана.","backup.error.not_ready":"House Plan ещё не готов. Повторите попытку через несколько секунд.","backup.error.too_large":"Размер резервной копии превышает 8 МиБ.","backup.error.invalid_json":"Выбранный файл не является корректным JSON.","backup.error.invalid_format":"Выбранный файл не является резервной копией House Plan.","backup.error.unsupported_export_version":"Эта версия формата резервной копии не поддерживается установленной версией.","backup.error.future_model":"Резервная копия создана в более новой версии модели данных House Plan.","backup.error.invalid_config":"Резервная копия содержит некорректную конфигурацию House Plan.","backup.error.wall_model_migration_blocked":"Не удалось безопасно обновить модель стен из резервной копии. Сначала оптимизируйте исходный план и повторите экспорт.","backup.error.invalid_layout":"Резервная копия содержит некорректные позиции объектов.","backup.error.invalid_content":"Резервная копия содержит некорректные или несогласованные ссылки на файлы.","backup.error.space_not_found":"Выбранное пространство больше не существует.","backup.error.capacity_exceeded":"Добавление этой копии превысит ограничения размера плана.","backup.error.preview_expired":"Время предпросмотра истекло. Выберите файл резервной копии заново.","backup.error.preview_owner_mismatch":"Этот предпросмотр принадлежит другому пользователю Home Assistant.","backup.error.conflict":"После предпросмотра план изменился. Проверьте обновлённую сводку.","backup.error.content_confirmation_required":"Подтвердите отсоединение недоступных локальных файлов.","backup.error.commit_failed":"Не удалось безопасно применить копию. Предыдущий план восстановлен или ожидает восстановления.","backup.error.missing_plan":"После предпросмотра исчез связанный файл плана. Проверьте копию ещё раз.","backup.error.missing_content":"После предпросмотра исчез связанный локальный файл. Проверьте копию ещё раз.","backup.error.marker_control_missing":"В резервной копии отсутствует связанный источник света на плане.","backup.error.marker_control_not_light":"Связанная цель больше не отмечена как источник света.","backup.error.marker_control_self":"Источник света не может управлять самим собой.","backup.error.marker_control_cycle":"Резервная копия содержит циклическую цепочку управления светом.","backup.error.duplicate_marker_control":"Резервная копия содержит повторяющуюся связь с источником света.","backup.error.no_backup":"Нет снимка импорта или оптимизации, который можно восстановить.","backup.same_source":"Создано на этом экземпляре Home Assistant","backup.foreign_source":"Создано на другом экземпляре Home Assistant","backup.created":"Создано: {value}","backup.versions":"Карточка {card}; интеграция {integration}; модель данных {model}","backup.count_spaces":"Пространств: {n}","backup.count_rooms":"Комнат: {n}","backup.count_walls":"Стен: {n}","backup.count_openings":"Проёмов: {n}","backup.count_decor":"Объектов декора: {n}","backup.count_markers":"Устройств: {n}","backup.count_layout":"Позиций: {n}","backup.bindings":"Привязки — устройства: {device}, сущности: {entity}, виртуальные: {virtual}; позиций без пространства: {legacy}","backup.binding_status":"Статус на целевом экземпляре — активных: {active}, деактивированных: {disabled}, отсутствующих: {missing}","backup.missing_areas":"На целевом экземпляре отсутствуют зоны: {areas}","backup.dropped_marker_links":"Связи с источниками света вне переносимого пространства пропущены: {n}.","backup.repaired_target_refs":"Существующих ссылок восстановлено этим импортом: {n}.","backup.preserved_unresolved_refs":"Неоднозначные ссылки сохранены без изменений: {n}.","backup.preserved_unresolved_hint":"House Plan ничего не угадывал и не удалял. После импорта запустите «Оптимизировать планы», чтобы проверить оставшиеся ссылки.","backup.import_details":"Подробности восстановления ссылок","backup.import_detail.incoming_remapped":"Ссылок обновлено внутри импортируемой копии: {n}","backup.import_detail.target_repaired":"Существующих ссылок восстановлено: {n}","backup.import_detail.preserved_unresolved":"Неразрешимых ссылок сохранено: {n}","backup.import_detail.collisions":"Конфликтов назначения безопасно сохранено: {n}","backup.import_detail.dropped_links":"Входящих связей пропущено по правилам переноса: {n}","backup.import_detail.bounded_lineages":"Чрезмерно вложенных идентификаторов ограничено: {n}","backup.replace_warning":"Текущая конфигурация и расположение будут заменены. Загруженные файлы не удаляются. Одну отмену можно выполнить до следующего изменения плана.","backup.foreign_bookkeeping":"Служебные списки известных и новых устройств другого экземпляра импортированы не будут.","backup.final_name":"Название нового пространства","backup.target_settings":"Глобальные настройки целевого экземпляра не изменятся.","backup.duplicates":"Уже существующих привязок: {n}","backup.skip":"Пропустить повторяющиеся устройства","backup.virtual_copy":"Добавить безопасные статичные виртуальные копии","backup.content":"Связанное содержимое","backup.content_available":"доступно локально","backup.content_external":"внешняя ссылка","backup.content_detach_required":"будет отвязано","backup.confirm_detach":"Я понимаю, что недоступные внутренние планы и вложения будут отвязаны от импортируемой конфигурации.","backup.replace":"Заменить","backup.add":"Добавить пространство","backup.space_done":"Пространство импортировано: комнат — {rooms}, устройств — {markers}, существующих ссылок восстановлено — {refs}","backup.full_done":"Копия восстановлена: пространств — {spaces}, комнат — {rooms}, устройств — {markers}","backup.undo_import":"Отменить последний полный импорт","backup.import_undone":"Полный импорт отменён"}}],ra=oa;function la(e){return"string"==typeof e?e.trim().replaceAll("_","-").toLowerCase():""}const ca=new Map(aa.map(e=>[la(e.code),e]));function ha(e){return ca.get(la(e))}function da(e,t){const i=[{value:"",label:e},...aa.map(({code:e,nativeLabel:t})=>({value:e,label:t}))],s="string"==typeof t?t:"";return s&&!i.some(e=>e.value===s)&&i.push({value:s,label:ha(s)?.nativeLabel??s}),i}function pa(e,t){return function(e,t,i,s){const n=new Map(i.map(e=>[la(e),e])),o=n.get(la(e));if(o)return o;const a=la(t),r=n.get(a);return r||(n.get(a.split("-")[0]||"")??s)}(t,e?.locale?.language||e?.language,aa.map(({code:e})=>e),"en")}function ua(e,t,i){const s=ha(e)?.dictionary;return ss(s?.[t]??ra[t]??t,i)}function _a(e,t){const i=ha(e)?.dictionary[t]??ra[t];return"string"==typeof i&&i.trim().length>0}class ma extends he{constructor(){super(...arguments),this.deviceName="",this._metadataConnection=null,this._demoUntil=0,this._demoTimer=0,this._demoGeneration=1,this._demoKind=null,this._reducedMotion=!1,this._onMotionChange=e=>{this._reducedMotion=e.matches,this.requestUpdate()},this._startShortDemo=()=>{const e=this.presentation;e&&"icon_ripple"===e.display&&"none"===e.pulse.kind&&(this._demoGeneration++,this._demoKind="short",this._demoUntil=Date.now()+3300,window.clearTimeout(this._demoTimer),this._demoTimer=window.setTimeout(()=>{this._demoUntil=0,this._demoTimer=0,this._demoKind=null,this.requestUpdate()},3330),this.requestUpdate())},this._toggleContinuousDemo=()=>{const e=this.presentation;e&&"icon_ripple"===e.display&&"none"===e.pulse.kind&&("continuous"===this._demoKind?this._clearDemo():(this._clearDemo(),this._demoGeneration++,this._demoKind="continuous",this._demoUntil=Number.POSITIVE_INFINITY),this.requestUpdate())}}connectedCallback(){super.connectedCallback(),this._motionMedia=window.matchMedia?.("(prefers-reduced-motion: reduce)"),this._reducedMotion=!!this._motionMedia?.matches,this._motionMedia?.addEventListener?.("change",this._onMotionChange),this._ensureMetadata()}disconnectedCallback(){this._metadataRelease?.(),this._metadataRelease=void 0,this._metadataConnection=null,this._clearDemo(),this._motionMedia?.removeEventListener?.("change",this._onMotionChange),this._motionMedia=void 0,super.disconnectedCallback()}willUpdate(e){e.has("hass")&&this._ensureMetadata();const t=this.presentation,i=e.get("presentation");e.has("presentation")&&i?.binding!==t?.binding&&this._clearDemo(),t&&this._demoKind&&("icon_ripple"!==t.display||"none"!==t.pulse.kind)&&this._clearDemo()}_ensureMetadata(){const e=this.hass?.connection||null;e&&e!==this._metadataConnection&&(this._metadataRelease?.(),this._metadataConnection=e,this._metadataRelease=Qo(this.hass,()=>this.requestUpdate()))}_clearDemo(){window.clearTimeout(this._demoTimer),this._demoTimer=0,this._demoUntil=0,this._demoKind=null}get _lang(){return pa(this.hass)}_t(e,t){return ua(this._lang,e,t)}_providerText(e){return"houseplan"===e.domain?this._t("marker.preview.virtual_provider"):e.label.toLowerCase()===e.domain.toLowerCase()?e.domain:`${e.label} (${e.domain})`}_providers(){return this.presentation&&this.registry?function(e,t,i,s=ea(e)){if("virtual"===t)return[{domain:"houseplan",label:"House Plan",confidence:"registry-owner"}];const n=t.indexOf(":");if(n<1)return[];const o=t.slice(0,n),a=t.slice(n+1);if("entity"===o){const t=na(e,a,i,s);return t?[t]:[]}if("device"!==o)return[];const r=i.devices?.[a]||e?.devices?.[a];if(!r)return[];const l=[r.config_entry_id,...Array.isArray(r.config_entries)?r.config_entries:[]].filter(e=>"string"==typeof e&&!!e).map(t=>{const i=s.configEntries[t];return ia(e,i?.domain,"registry-owner",s,t)});if(l.some(Boolean))return sa(l);const c=[];for(const[t,n]of Object.entries(i.entities||{}))n?.device_id===a&&null==n?.disabled_by&&c.push(na(e,t,i,s));return c.some(Boolean)?sa(c):sa((Array.isArray(r.identifiers)?r.identifiers:[]).map(t=>{const i=Array.isArray(t)?t[0]:null;return ia(e,i,"identifier-fallback",s)}))}(this.hass,this.presentation.binding,this.registry,ea(this.hass)):[]}_providerSummary(e){if(!e.length)return this._t("marker.preview.unknown_provider");const t=e.slice(0,2).map(e=>this._providerText(e)),i=e.length-t.length;return t.join(", ")+(i>0?` · ${this._t("marker.preview.more_sources",{n:i})}`:"")}_sourceSummary(e){const t=e.visualSources;return t.length?1===t.length?`${t[0].name} · ${t[0].eid} · ${this._sourceProvider(t[0].eid)}`:2===t.length?t.map(e=>e.name).join(" · "):this._t("marker.preview.multiple_sources",{n:t.length}):this._t("marker.preview.no_source")}_stateSummary(e){if(!e.visualSources.length)return this._t("marker.preview.no_state");if(1===e.visualSources.length)return e.visualSources[0].stateText||this._t("marker.preview.no_state");const t=[...new Set(e.visualSources.map(e=>e.stateText).filter(Boolean))];return 1===t.length?t[0]:this._t("marker.preview.mixed_states")}_reason(e){return this._t(`marker.preview.reason.${e}`)}_sourceProvider(e){if(!this.registry)return this._t("marker.preview.unknown_provider");const t=na(this.hass,e,this.registry,ea(this.hass));return t?this._providerText(t):this._t("marker.preview.unknown_provider")}render(){const e=this.presentation;if(!e)return G;const t=!!this._demoKind&&this._demoUntil>Date.now()&&"icon_ripple"===e.display&&"none"===e.pulse.kind,i=t?(()=>{const t=function(e,t,i,s,n=null){return{kind:t,reason:"short"===t?"event":"running",generation:Math.max(1,Math.trunc(i)),expiresAt:"short"===t?n:null,color:e.color,diameterScale:s?1:e.diameterScale,animated:!s,reducedMotionIndicator:s?"dot":"none"}}(e.pulse,this._demoKind,this._demoGeneration,this._reducedMotion,"short"===this._demoKind?this._demoUntil:null);return{...e,pulse:t,classes:[...e.classes.filter(e=>!e.startsWith("pulse-")),`pulse-${t.kind}`,...t.generation%2==0?["pulse-gen2"]:[]]}})():e,s=this._providers(),n="none"!==e.pulse.kind,o=i.scale*(i.pulse.animated?i.pulse.diameterScale:1);let a=-o/2,r=o/2,l=-o/2,c=o/2;if(i.valueBadge){const e=i.scale*Math.min(4,Math.max(.9,.29*i.valueBadge.text.length+.28)),t=.7*i.scale,s=.6*i.scale;"right"===i.valueBadge.position&&(r=Math.max(r,s+e)),"left"===i.valueBadge.position&&(a=Math.min(a,-s-e)),"top"===i.valueBadge.position&&(l=Math.min(l,-s-t)),"bottom"===i.valueBadge.position&&(c=Math.max(c,s+t))}if(null!=i.lqiText){const e=i.scale*("bottom"===i.valueBadge?.position?1.55:.95);c=Math.max(c,e)}const h=Math.min(1,2.35/Math.max(1,r-a),2.35/Math.max(1,c-l)),d=Math.round(100*h),p=[`left:calc(50% - ${54*((a+r)/2)*h}px)`,`top:calc(50% - ${54*((l+c)/2)*h}px)`,h<1?`transform:scale(${h})`:""].filter(Boolean).join(";"),u=["dev",Uo(this.hass),...i.classes,"virtual"===i.binding?"virtual":"",i.haDisabled?"ghost ha-disabled":"",null!=i.valueText?"valonly":""].filter(Boolean).join(" "),_=Go(i).join(";"),m=t?this._t("continuous"===this._demoKind?"marker.preview.demo_continuous_notice":"marker.preview.demo_short_notice"):this._reason(e.explanation.reason),g=[this.deviceName,this._providerSummary(s),this._stateSummary(e),m,e.valueFullText||"",Io(e.valueBadge),null!=e.lqiText&&e.lqiBand?this._t(`marker.lqi_a11y_${e.lqiBand}`,{value:e.lqiText}):""].filter(Boolean).join(". ");return q`
-
- ${this._t("marker.preview.title")} - - ${this._t(t?"marker.preview.example":"marker.preview.actual")} - -
-
- -
-
${this._t("marker.preview.integration")}${this._providerSummary(s)}
-
${this._t("marker.preview.source")}${this._sourceSummary(e)}
-
${this._t("marker.preview.current_state")}${this._stateSummary(e)}
-
${this._t("marker.preview.result")}${m}
- ${e.fallbackReason?q`

${this._reason(e.fallbackReason)}

`:G} - ${e.explanation.notices.map(e=>q`

${this._reason(e)}

`)} - ${h<1?q`

${this._t("marker.preview.scaled",{n:d})}

`:G} - ${t&&this._reducedMotion?q`

${this._t("marker.preview.reduced_motion")}

`:G} -
-
- ${e.visualSources.length||e.criticalSources.length?q`
- ${this._t("marker.preview.details")} -
- ${[...e.visualSources,...e.criticalSources].map(e=>q` -
- ${e.name}${e.eid} - ${e.stateText||e.state} - ${this._sourceProvider(e.eid)} -
`)} -
-
`:G} - ${"icon_ripple"===e.display?q`
- - -
`:G} -
`}}ma.properties={hass:{attribute:!1},presentation:{attribute:!1},registry:{attribute:!1},deviceName:{attribute:!1}},ma.styles=[Ke,o` - :host { display: block; min-width: 0; } - .devicepreview { - margin: 4px 0 14px; - padding: 14px; - border: 1px solid var(--divider-color, rgba(127, 127, 127, 0.28)); - border-radius: 14px; - background: color-mix(in srgb, var(--card-background-color, #20232b) 92%, var(--primary-color, #03a9f4)); - color: var(--primary-text-color, #fff); - overflow: hidden; - } - .previewhead { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; } - .previewbadge { - padding: 3px 9px; - border-radius: 999px; - color: var(--secondary-text-color, #9aa0aa); - background: color-mix(in srgb, var(--secondary-text-color, #9aa0aa) 12%, transparent); - font-size: 12px; - font-weight: 700; - } - .previewbadge.example { color: var(--warning-color, #ffb300); } - .previewgrid { display: grid; grid-template-columns: minmax(170px, 0.8fr) minmax(240px, 1.2fr); gap: 16px; align-items: stretch; } - .previewstage { - position: relative; - min-height: 168px; - border-radius: 12px; - background: var(--secondary-background-color, rgba(0, 0, 0, 0.16)); - overflow: hidden; - pointer-events: none; - container-type: inline-size; - --icon-size: 54px; - --device-base-size: 48.6px; - } - .previewfit { position: absolute; left: 50%; top: 50%; transform-origin: center; } - .previewstage .dev { left: 0; top: 0; cursor: default; } - :host([data-pointer-hover]) .previewstage .dev:hover { z-index: 2; } - .previewfacts { min-width: 0; display: grid; align-content: start; gap: 8px; } - .previewfacts > div { display: grid; grid-template-columns: minmax(115px, 0.8fr) minmax(0, 1.3fr); gap: 8px; } - .previewfacts span { color: var(--secondary-text-color, #9aa0aa); } - .previewfacts b { min-width: 0; overflow-wrap: anywhere; } - .previewnotice { margin: 2px 0 0; color: var(--secondary-text-color, #9aa0aa); font-size: 13px; } - .previewdetails { margin-top: 12px; } - .previewdetails summary { cursor: pointer; color: var(--secondary-text-color, #9aa0aa); } - .previewtable { display: grid; gap: 8px; margin-top: 9px; } - .previewsource { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 3px 10px; font-size: 13px; } - .previewsource > * { min-width: 0; overflow-wrap: anywhere; } - .previewsource code { color: var(--secondary-text-color, #9aa0aa); } - .previewdemo { - min-height: 40px; - display: inline-flex; - align-items: center; - gap: 8px; - padding: 8px 12px; - border: 1px solid var(--divider-color, rgba(127, 127, 127, 0.35)); - border-radius: 10px; - background: transparent; - color: inherit; - cursor: pointer; - } - .previewdemos { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; } - .previewdemo:focus-visible { outline: 2px solid var(--primary-color, #03a9f4); outline-offset: 2px; } - .previewdemo:disabled { opacity: 0.5; cursor: not-allowed; } - @media (max-width: 640px) { - .previewgrid { grid-template-columns: minmax(0, 1fr); } - .previewstage { min-height: 142px; } - .previewfacts > div { grid-template-columns: minmax(0, 1fr); gap: 2px; } - } - `],customElements.get("hp-device-preview")||customElements.define("hp-device-preview",ma);const ga=Math.tan(.25*Math.PI/180);function fa(e,t){const i=Math.abs(Number(t[0])-Number(e[0])),s=Math.abs(Number(t[1])-Number(e[1]));if(!([i,s].every(Number.isFinite)&&i>0&&s>0))return null;const n=i>=s?"horizontal":"vertical",o=Math.max(i,s),a=Math.min(i,s);return a/o>ga?null:{axis:n,major:o,minor:a,angleDegrees:180*Math.atan2(a,o)/Math.PI}}function va(e,t){const i=fa(e,t);return i?"horizontal"===i.axis?[Number(t[0]),Number(e[1])]:[Number(e[0]),Number(t[1])]:[Number(t[0]),Number(t[1])]}const ba=e=>`${e[0]},${e[1]}`,ya=(e,t)=>e[0]===t[0]&&e[1]===t[1],wa=(e,t)=>{const i=ba(e),s=ba(t);return i{let t=0;for(let i=0;i{if(e.length<3)return!1;for(let t=0;t1e-12},$a=e=>{for(let t=0;t+1ya(e,t.from)?[...t.to]:[...e],Ma=(e,t)=>e.map(e=>Array.isArray(e?.poly)?{...e,poly:e.poly.map(e=>Sa(e,t))}:e),Ca=(e,t,i)=>{const s=e.flatMap((e,t)=>hi(e)?.some(e=>ya(e,i.from))?[t]:[]);if(!s.length)return!1;for(const i of s){const s=hi(e[i]),n=hi(t[i]);if(!s||!n)continue;if(!xa(n))return!1;const o=Math.sign(ka(s)),a=Math.sign(ka(n));if(o&&a&&o!==a)return!1}const n=new Set;for(const e of s){const i=hi(t[e]);if(i)for(let s=0;s{let i=0;for(const s of e){const e=hi(s);if(e)for(let s=0;s{const{a:i,b:s,axis:n}=e,o="horizontal"===n?{from:s,to:[s[0],i[1]]}:{from:s,to:[i[0],s[1]]},a="horizontal"===n?{from:i,to:[i[0],s[1]]}:{from:i,to:[s[0],i[1]]};return[{move:o,preservedDegree:Da(t,i)},{move:a,preservedDegree:Da(t,s)}].sort((e,t)=>t.preservedDegree-e.preservedDegree||ba(e.move.to).localeCompare(ba(t.move.to))||ba(e.move.from).localeCompare(ba(t.move.from))).map(e=>e.move)};const Ta=(e,t)=>[e[0]-t[0],e[1]-t[1]],Ia=(e,t)=>[e[0]+t[0],e[1]+t[1]],Ra=(e,t)=>e[0]*t[0]+e[1]*t[1],Aa=e=>Math.hypot(e[0],e[1]);function za(e){let t=0;for(let i=0;ie[1]!=l>e[1]&&e[0]<(r-o)*(e[1]-a)/(l-a)+o&&(i=!i)}return i}([r[0]+a[0]*l,r[1]+a[1]*l],e)&&(a=[-a[0],-a[1]]),a}function Ea(e){const t=e.length;if(t<3)return!1;for(let i=0;i(e[0]-n[0])*r[1]-(e[1]-n[1])*r[0],c=e=>(e[0]-n[0])*r[0]+(e[1]-n[1])*r[1],h=i,d=a-i;for(const t of e){const e=Math.abs(l(t));if(e<=i)continue;const n=c(t);n<=h||n>=d||eMath.abs(a+(e-p)/g*(r-a));s=Math.min(s,f(_),f(m))}}return s}function Ha(e,t,i){if(Di(e,t,i))return!0;if(Ci(e,t,i)||Ci(t,e,i))return!1;let s=0;try{const i=ni([[...e.map(e=>[e[0],e[1]]),[e[0][0],e[0][1]]]],[[...t.map(e=>[e[0],e[1]]),[t[0][0],t[0][1]]]]);for(const e of i)e?.[0]&&(s+=Ti(e[0]))}catch{return!1}return s>Math.max(1e-7,i*i)}function La(e,t,i){for(const s of t)for(let t=0;ti)continue;const c=(e.x-n[0])*l[0]+(e.y-n[1])*l[1];if(c-e.length/2>=-i&&c+e.length/2<=r+i)return!0}return!1}function Ba(e,t,i){const s=Math.abs(t[0]-e[0]),n=Math.abs(t[1]-e[1]);return s<=i&&n>i?"v":n<=i&&s>i?"h":null}function ja(e,t,i){return Math.abs(e[0]-t[0])<=i&&Math.abs(e[1]-t[1])<=i}function qa(e,t,i,s,n){return ja(e,i,n)&&ja(t,s,n)||ja(e,s,n)&&ja(t,i,n)}function Wa(e,t,i,s,n){const o=Ba(e,t,n);return o&&Ba(i,s,n)===o?"h"===o?Math.abs(e[1]-i[1])>n||Math.abs(e[1]-s[1])>n?0:Math.max(0,Math.min(Math.max(e[0],t[0]),Math.max(i[0],s[0]))-Math.max(Math.min(e[0],t[0]),Math.min(i[0],s[0]))):Math.abs(e[0]-i[0])>n||Math.abs(e[0]-s[0])>n?0:Math.max(0,Math.min(Math.max(e[1],t[1]),Math.max(i[1],s[1]))-Math.max(Math.min(e[1],t[1]),Math.min(i[1],s[1]))):0}function Ua(e,t,i,s){const n=e.length,o=Ba(e[(t-1+n)%n],e[t],s),a=Ba(e[(t+1)%n],e[(t+2)%n],s),r="h"===i?"v":"h";return o===r&&a===r}function Ga(e){let t=1/0,i=1/0,s=-1/0,n=-1/0;for(const o of e)t=Math.min(t,o[0]),i=Math.min(i,o[1]),s=Math.max(s,o[0]),n=Math.max(n,o[1]);return[t,i,s,n]}function Va(e,t,i){const s=Ga(e),n=Ga(t);return s[2]n}const Ya=(e,t)=>"h"===t?e[0]:e[1],Ja=(e,t)=>"h"===t?e[1]:e[0];function Xa(e,t,i,s,n){const o=e.find(e=>e.id===t);if(!o)return null;const a=o.poly[i],r=o.poly[(i+1)%o.poly.length],l=Ba(a,r,n);if(!l)return null;const c=0===s?r:a,h=0===s?a:r,d=Ya(c,l),p=Ya(h,l),u=Ja(c,l),_=function(e,t,i,s){const n=[];for(const o of e)for(let e=0;es||Math.abs(Ja(r,t)-i)>s)continue;const l=Ya(a,t),c=Ya(r,t),h=Math.min(l,c),d=Math.max(l,c);d-h>s&&n.push({roomId:o.id,edge:e,lo:h,hi:d})}return n.sort((e,t)=>e.lo-t.lo||e.hi-t.hi||e.roomId.localeCompare(t.roomId)||e.edge-t.edge)}(e,l,u,n),m=function(e,t,i,s){const n=[t,i];for(const o of e){const e=Math.max(t,o.lo),a=Math.min(i,o.hi);a-e>s&&n.push(e,a)}const o=[...new Set(n)].sort((e,t)=>e-t),a=[];for(let t=0;t+1r>e.lo-s&&re.roomId))].sort();a.push({lo:i,hi:n,owners:l})}return a}(_,Math.min(d,p),Math.max(d,p),n);return!m.length||m.some(e=>e.owners.length<1||e.owners.length>2)?null:{roomId:t,edge:i,movedEndpoint:s,axis:l,line:u,fixed:d,moving:p,intervals:_,baseline:m}}function Za(e,t,i){const s=[];for(const n of e.intervals){const o=t.polys[n.roomId];if(!o){s.push(n);continue}if(n.edge<0||n.edge>=o.length)continue;const a=o[n.edge],r=o[(n.edge+1)%o.length];if(Ba(a,r,i)!==e.axis||Math.abs(Ja(a,e.axis)-e.line)>i||Math.abs(Ja(r,e.axis)-e.line)>i)continue;const l=Ya(a,e.axis),c=Ya(r,e.axis),h=Math.min(l,c),d=Math.max(l,c);d-h>i&&s.push({...n,lo:h,hi:d})}return s}function Qa(e,t,i){return new Set(e.filter(e=>t>e.lo-i&&te.roomId)).size}function er(e,t,i){for(const s of t.sideOwnership){const t=e.polys[s.roomId];if(!t)return!1;const n=t[s.edge],o=t[(s.edge+1)%t.length];if(Ba(n,o,i)!==s.axis)return!1;const a=Ya(0===s.movedEndpoint?n:o,s.axis),r=Za(s,e,i),l=(s.moving>=s.fixed?s.baseline[s.baseline.length-1]:s.baseline[0]).owners.length;if(l<1||l>2)return!1;const c=Math.min(s.fixed,s.moving,a),h=Math.max(s.fixed,s.moving,a),d=[c,h,s.fixed,s.moving,a];for(const e of s.intervals)d.push(e.lo,e.hi);for(const e of r)d.push(e.lo,e.hi);const p=[...new Set(d.filter(e=>e>=c&&e<=h))].sort((e,t)=>e-t),u=Math.min(s.fixed,s.moving),_=Math.max(s.fixed,s.moving),m=Math.min(s.fixed,a),g=Math.max(s.fixed,a);for(let e=0;e+1u-i&&o<_+i,c=o>m-i&&o2||d>2)return!1;if(a&&c){if(h!==d)return!1}else if(!a&&c){if(d!==l||h>0&&h!==d)return!1}else if(2===h?2!==d:d>1)return!1}}return!0}function tr(e,t,i,s){const n=[i.n[0]*s,i.n[1]*s],o={polys:{},openings:{},movedSpans:{}};for(const t of i.roomIds){const s=e.find(e=>e.id===t),a=i.edgeByRoom[t];if(!s||null==a)continue;const r=(a+1)%s.poly.length;o.polys[t]=s.poly.map((e,t)=>t===a||t===r?Ia(e,n):[...e]),o.movedSpans[t]=[[Ia(s.poly[a],n),Ia(s.poly[r],n)]]}for(const e of t)i.movingOpeningIds.includes(e.id)&&(o.openings[e.id]=[e.x+n[0],e.y+n[1]]);return o}function ir(e,t,i,s,n,o,a,r){if(!function(e,t,i,s){if(Fa([e.x,e.y],t,i)>2*s)return!1;const n=Aa(Ta(i,t));if(n<=s)return!1;const o=[(i[0]-t[0])/n,(i[1]-t[1])/n],a=(e.x-t[0])*o[0]+(e.y-t[1])*o[1];return a>=-s&&a<=n+s}(e,t,i,r))return!0;if(Fa([e.x,e.y],s,n)>2*r)return!1;const l=0===o?s:n;return Aa(Ta([e.x,e.y],l))>=e.length/2+a-r}function sr(e,t,i,s){const n=s.movingHalf||0;return"circle"===e.kind?Fa(e.center,t,i)2)return!1;const r=tr(e,t,i,s);if(Object.keys(r.polys).length!==i.roomIds.length)return!1;if(!er(r,i,o))return!1;const l=new Set(i.roomIds),c=e=>r.polys[e.id]||e.poly;for(const s of i.roomIds){const l=e.find(e=>e.id===s),c=r.polys[s],h=i.edgeByRoom[s];if(!l||!c||c.length!==i.topology[s]||c.length!==l.poly.length||!Ea(c))return!1;const d=l.poly.length,p=(h-1+d)%d,u=[p,h,(h+1)%d];for(const e of u)if(fa(c[e],c[(e+1)%d]))return!1;const _=za(l.poly),m=za(c);if(Math.abs(m)e.id===t),s=r.polys[t];for(const n of e){if(n.id===t||l.has(n.id))continue;const e=c(n);if(!Va(i.poly,n.poly,o)||!Va(s,e,o))if(Ci(i.poly,n.poly,o)){if(!Ci(s,e,o))return!1}else if(Ci(n.poly,i.poly,o)){if(!Ci(e,s,o))return!1}else if(Ci(s,e,o)||Ci(e,s,o)||Ha(s,e,o))return!1}}e.find(e=>e.id===i.roomId);const h=r.polys[i.roomId],d=i.edgeByRoom[i.roomId],p=h[d],u=h[(d+1)%h.length];if(Math.abs(s)>o)for(const e of n.obstacles||[])if(sr(e,p,u,n))return!1;for(const e of t){if(!i.movingOpeningIds.includes(e.id))continue;const t=r.openings[e.id];if(!t||e.hosted)return!1;if(!La({...e,x:t[0],y:t[1]},[h],2*o))return!1}return!0}const or=new WeakMap;function ar(e,t,i){const s=i/t;return Ti(e)*s*s/1e4}function rr(e,t){return t?`${Math.round(10.7639*e)} ft²`:`${(Math.round(10*e)/10).toFixed(1)} m²`}const lr=e=>"number"==typeof e&&Number.isFinite(e),cr=(e,t)=>{const i=new Map;for(const s of e||[])!lr(s?.cm)||!t&&s.cm<=0||i.set(s.cm,(i.get(s.cm)||0)+1);return i},hr=e=>[String(e),"см"].join(" ");function dr(e,t,i={}){if(i.allowClear)return[];const s=!0===i.exactMultiplicity,n=cr(e,s),o=cr(t,s),a=[];for(const[e,t]of n){const i=o.get(e)||0;0===i?a.push({invariant:"wall_records",kind:"lost",owner:hr(e),reference:`было ${t}`,detail:"записи этой толщины исчезли целиком"}):s&&i!==t&&a.push({invariant:"wall_records",kind:"count",owner:hr(e),reference:`было ${t}, стало ${i}`,detail:"изменилось число записей этой толщины"})}if(s)for(const[e,t]of o)n.has(e)||a.push({invariant:"wall_records",kind:"count",owner:hr(e),reference:`было 0, стало ${t}`,detail:"появились записи новой толщины"});return a}class pr{constructor(){this._selectedRoomId=null,this._session=null,this._eligibility=null}get dragging(){return null!==this._session}ownsPointer(e){return this._session?.pointerId===e}get selectedRoomId(){return this._selectedRoomId}get snapshotIdentity(){return this._session?.snapshotIdentity||null}get activePointerId(){return this._session?.pointerId??null}get moved(){return!0===this._session?.moved}get delta(){return this._session?.delta||0}get rooms(){return this._session?.rooms||null}get openings(){return this._session?.openings||null}get plan(){return this._session?.plan||null}get preview(){return this._session?.accepted?.preview||null}get liveLabels(){return this._session?.accepted?.labels||null}selectRoom(e){this._selectedRoomId=e}restoreSelection(e){this._selectedRoomId=e}escapeIdle(){return this._selectedRoomId?(this._selectedRoomId=null,"selection-cleared"):"exit-tool"}resolve(e,t,i){this._eligibility&&this._eligibility.context===e||(this._eligibility={context:e,values:new Map});const s=this._eligibility.values.get(t);if(s)return s;const n=i();return this._eligibility.values.set(t,n),n}begin(e){return!this._session&&(this._session={pointerId:e.pointerId,start:[...e.start],roomId:e.roomId,plan:e.plan,options:e.options,rooms:e.rooms,openings:e.openings,snapshotIdentity:e.snapshotIdentity,before:e.before,moved:!1,delta:0,changedRoomIds:[...e.plan.roomIds],rejectionNotified:!1,wallUnionBefore:e.wallUnionBefore,accepted:null},!0)}move(e){const t=this._session;if(!t||t.pointerId!==e.pointerId)return{kind:"no-op"};const i=(s=t.start,n=e.point,o=t.plan.n,[s?.[0],s?.[1],n?.[0],n?.[1],o?.[0],o?.[1]].every(Number.isFinite)?(n[0]-s[0])*o[0]+(n[1]-s[1])*o[1]:0);var s,n,o;const a=e.snap([t.plan.a[0]+t.plan.n[0]*i,t.plan.a[1]+t.plan.n[1]*i]),r=(a[0]-t.plan.a[0])*t.plan.n[0]+(a[1]-t.plan.a[1])*t.plan.n[1],l=function(e,t,i,s,n,o){if(!Number.isFinite(s)||Math.abs(s)<1e-9)return 0;const a=Math.sign(s),r=Math.abs(s),l=Math.max(Math.abs(n),1e-6);let c=or.get(i);c&&c.opts===o||(c={opts:o,values:new Map},or.set(i,c));const h=c.values;let d=0;for(let s=Math.min(l,r),n=0;n<4096&&s<=r+1e-9;n++,s=Math.min(r,s+l)){const n=a*s,c=`${n.toFixed(9)}|${l.toFixed(9)}`;let p=h.get(c);if(void 0===p&&(p=nr(e,t,i,n,o),h.size<4096&&h.set(c,p)),!p)break;if(d=n,Math.abs(s-r)<=1e-9)break}return d}(t.rooms,t.openings,t.plan,r,e.step,t.options);if(t.moved&&l===t.delta)return{kind:"no-op"};const c=tr(t.rooms,t.openings,t.plan,l);let h;try{if(h=e.project(t.snapshotIdentity,c.polys,c.openings,t.changedRoomIds,t.rooms),!h.ok)return this._projectionRejected(t);if(dr(h.value.beforeWalls,h.value.afterWalls,{exactMultiplicity:!0}).length)return this._projectionRejected(t)}catch{return this._projectionRejected(t)}const d=t.accepted,p={preview:h.value.preview,labels:null,artifact:h.value.artifact,beforeWalls:h.value.beforeWalls,afterWalls:h.value.afterWalls};t.accepted=p;try{e.publish(p.preview,p.artifact),p.labels=e.measure(c,t.plan)}catch{t.accepted=d;try{e.publish(d?.preview||null,d?.artifact||null)}catch{}return this._projectionRejected(t)}return t.moved=!0,t.delta=l,{kind:"accepted",preview:h.value.preview,labels:p.labels,artifact:h.value.artifact}}finish(e){const t=this._session;if(!t||t.pointerId!==e.pointerId)return{kind:"no-op"};const i=t.accepted;let s;if(t.moved&&Math.abs(t.delta)>1e-9&&i)if(e.currentSnapshotIdentity!==t.snapshotIdentity)s={kind:"rejected",reason:"stale-snapshot"};else if(nr(t.rooms,t.openings,t.plan,t.delta,t.options))if(dr(i.beforeWalls,i.afterWalls,{exactMultiplicity:!0}).length)s={kind:"rejected",reason:"wall-records"};else{let n=!1;try{n=e.validatePreview(i.preview)}catch{n=!1}s=n?{kind:"commit",preview:i.preview,before:t.before}:{kind:"rejected",reason:"invalid-candidate"}}else s={kind:"rejected",reason:"invalid-topology"};else s={kind:"no-op"};return this._session=null,"commit"===s.kind&&(this._eligibility=null),s}cancel(e,t){const i=this._session;return!i||void 0!==t&&i.pointerId!==t?{kind:"no-op"}:(this._session=null,this._eligibility=null,{kind:"cancelled",restoreWallUnion:e===i.snapshotIdentity?i.wallUnionBefore:null})}reset(){this._selectedRoomId=null,this._session=null,this._eligibility=null}_projectionRejected(e){const t=!e.rejectionNotified;return e.rejectionNotified=!0,{kind:"rejected",notify:t}}}const ur=(e,t)=>[(e[0]-t.x)/Math.max(1e-9,t.w)*t.stageWidth,(e[1]-t.y)/Math.max(1e-9,t.h)*t.stageHeight];function _r(e){const{poly:t,edge:i,text:s,view:n}=e,o=t[i],a=t[(i+1)%t.length],r=[(o[0]+a[0])/2,(o[1]+a[1])/2],l=function(e,t){const i=e[t],s=e[(t+1)%e.length],n=s[0]-i[0],o=s[1]-i[1],a=Math.hypot(n,o)||1;return(e=>{let t=0;for(let i=0;i=0?[-o/a,n/a]:[o/a,-n/a]}(t,i),c=Math.hypot(a[0]-o[0],a[1]-o[1])||1,h=[(a[0]-o[0])/c,(a[1]-o[1])/c],d=ur(r,n),p=ur(e.gearCenter,n),u=Math.max(34,7.2*s.length+12),_=Math.abs(l[0])>=Math.abs(l[1])?Math.max(28,u/2+4):Math.max(28,13),m=d[0]+l[0]*_,g=d[1]+l[1]*_,f=t=>{return i=m+h[0]*t,s=g+h[1]*t,n=u,o=18,a=p[0],r=p[1],l=e.gearWidthPx,c=e.gearHeightPx,2*Math.abs(i-a)=Math.abs(h[1])?h[0]:h[1])>0?-1:1,t=Math.max(n.stageWidth,n.stageHeight,64);for(let i=4;i<=t;i+=4){const t=i*e;if(!f(t)){v=t;break}const s=-t;if(!f(s)){v=s;break}}}const b=l[0]*_+h[0]*v,y=l[1]*_+h[1]*v,w=Math.hypot(b,y)||1,k=y/w*12,x=[r[0]+b/w*12*n.w/Math.max(1,n.stageWidth),r[1]+k*n.h/Math.max(1,n.stageHeight)];return{anchor:r,offsetXPx:b,offsetYPx:y,tangentOffsetPx:v,side:Math.abs(l[0])>=Math.abs(l[1])?l[0]<0?"left":"right":l[1]<0?"above":"below",leader:{a:r,b:x}}}function mr(e){const t=e%360;return t<0?t+360:t}function gr(e,t){const i=function(e,t){return mr(e+t)}(e,t)*Math.PI/180;return[Math.sin(i),-Math.cos(i)]}const fr=Object.freeze({dawn:Object.freeze({top:"#aabdd1",bottom:"#e8c8b7",horizon:"rgba(255,201,156,.56)",sun:"rgba(255,188,125,.78)",vignette:"rgba(65,72,99,.21)",outlineNear:"rgba(74,57,61,.25)",outlineMid:"rgba(255,238,224,.40)",outlineFar:"rgba(255,224,202,.18)"}),day:Object.freeze({top:"#dce9ef",bottom:"#cbdce3",horizon:"rgba(255,245,220,.45)",sun:"rgba(255,239,190,.72)",vignette:"rgba(65,91,105,.16)",outlineNear:"rgba(45,62,71,.28)",outlineMid:"rgba(255,255,255,.42)",outlineFar:"rgba(255,255,255,.20)"}),dusk:Object.freeze({top:"#48536c",bottom:"#9a7380",horizon:"rgba(242,156,114,.34)",sun:"rgba(255,167,113,.55)",vignette:"rgba(20,26,44,.39)",outlineNear:"rgba(238,219,225,.40)",outlineMid:"rgba(229,207,218,.26)",outlineFar:"rgba(215,190,205,.12)"}),night:Object.freeze({top:"#111a27",bottom:"#1f2f3e",horizon:"rgba(79,120,151,.16)",sun:"rgba(169,208,231,0)",vignette:"rgba(3,8,14,.58)",outlineNear:"rgba(218,238,249,.56)",outlineMid:"rgba(174,215,238,.30)",outlineFar:"rgba(136,194,226,.14)"})}),vr=e=>"number"==typeof e&&Number.isFinite(e),br=(e,t,i)=>Math.min(i,Math.max(t,e));function yr(e){return e.elevation<=-6?"night":e.elevation>=6?"day":e.rising?"dawn":"dusk"}function wr(e){const t=(Math.floor(Number(e)||0)%1440+1440)%1440;return t>=300&&t<480?"dawn":t>=480&&t<1080?"day":t>=1080&&t<1260?"dusk":"night"}function kr(e){const t=yr(e),i=e.azimuth*Math.PI/180;return{sunX:50-42*Math.sin(i),sunY:78-br(e.elevation,0,90)/90*64,sunOpacity:"night"===t?0:br((e.elevation+6)/12,0,1)}}function xr(e){const t=(Math.floor(Number(e)||0)%1440+1440)%1440;if(t<300||t>=1260)return{sunX:50,sunY:78,sunOpacity:0};const i=(t-300)/960,s=(t-300)/120,n=(1260-t)/120;return{sunX:8+84*i,sunY:78-64*Math.sin(i*Math.PI),sunOpacity:Math.max(.18,Math.min(s,n,1))}}function $r(e,t=new Date){const i=function(e){const t=e?.states?.["sun.sun"]?.attributes;return t&&vr(t.azimuth)&&vr(t.elevation)&&"boolean"==typeof t.rising?{azimuth:mr(t.azimuth),elevation:t.elevation,rising:t.rising}:null}(e);if(i)return{phase:yr(i),source:"sun",...kr(i)};const s="number"==typeof t?t:function(e=new Date){return 60*e.getHours()+e.getMinutes()}(t);return{phase:wr(s),source:"clock",...xr(s)}}function Sr(e){return`${e.source}|${e.phase}|${e.sunX.toFixed(2)}|${e.sunY.toFixed(2)}|`+e.sunOpacity.toFixed(3)}const Mr=[[-90,"#070c14"],[-12,"#070c14"],[-4,"#131a28"],[0,"#4a3527"],[10,"#e8ddcf"],[30,"#ffffff"],[90,"#ffffff"]],Cr=e=>Math.min(1,Math.max(0,e));function Dr(e,t,i=6){const s=e.angle*Math.PI/180,n=[Math.sin(s),-Math.cos(s)],o=s=>{const o=[e.x+n[0]*i*s,e.y+n[1]*i*s];return t.find(e=>e.poly.length>=3&&vi(o,e.poly))||null},a=o(1),r=o(-1);return a&&r?null:a||r?a?{normal:[-n[0],-n[1]],roomId:a.id}:{normal:n,roomId:r.id}:null}function Pr(e,t,i){return i>0&&e[0]*t[0]+e[1]*t[1]>.05}function Tr(e,t,i,s){return[[e[0],e[1]],[t[0],t[1]],[t[0]+i[0]*s,t[1]+i[1]*s],[e[0]+i[0]*s,e[1]+i[1]*s]]}function Ir(e,t){try{const i=ni([[...e.map(e=>[e[0],e[1]]),[e[0][0],e[0][1]]]],[[...t.map(e=>[e[0],e[1]]),[t[0][0],t[0][1]]]]),s=[];for(const e of i){const t=e?.[0];!Array.isArray(t)||t.length<4||s.push(t.slice(0,t.length-1).map(e=>[e[0],e[1]]))}return s}catch{return[]}}function Rr(e,t,i,s,n,o,a){if(!(s>0))return[];const r=gr(i,n),l=[-r[0],-r[1]],c=function(e){const t=Math.min(90,Math.max(0,e));return.7*(.8+1.7*Math.pow(1-t/90,1.6))}(s),h=[];for(const i of t){if(!(i.length>0))continue;const t=Dr(i,e);if(!t||!Pr(t.normal,r,s))continue;const n=e.find(e=>e.id===t.roomId);if(!n)continue;const d=o&&o[t.roomId]||n.poly,p=i.angle*Math.PI/180,u=i.length/2,_=[-t.normal[0],-t.normal[1]],m=Math.max(0,a?.[i.id]||0),g=i.x+_[0]*m/2,f=i.y+_[1]*m/2,v=Math.cos(p)*u,b=Math.sin(p)*u,y=[g-v,f-b],w=[g+v,f+b],k=c*i.length,x=Ir(Tr(y,w,l,k),d);if(!x.length)continue;const $=l[0]*_[0]+l[1]*_[1];h.push({openingId:i.id,roomId:t.roomId,polys:x,a:y,b:w,dir:l,len:k,normal:_,depth:k*$})}return h}const Ar=e=>"number"==typeof e&&Number.isInteger(e)&&e>=0&&e<=359?e:null;function zr(e,t){const i=Ar(t?.north_deg);return null!==i?i:Ar(e?.north_deg)}function Fr(e,t){const i=e=>"static"===e||"daynight"===e?e:null;return i(t?.bg_mode)??i(e?.bg_mode)??"static"}function Nr(e,t){const i=t?.sun_rays;return"boolean"==typeof i?i:!0===e?.sun_rays}function Er(e){const t=e?.states?.["sun.sun"]?.attributes,i=Number(t?.azimuth),s=Number(t?.elevation);return Number.isFinite(i)&&Number.isFinite(s)?{azimuth:i,elevation:s}:null}const Or=["dawn","day","dusk","night"];function Hr(e){if(!e)return"";const t=fr[e.phase];return[`--hp-day-cycle-outline-near:${t.outlineNear}`,`--hp-day-cycle-outline-mid:${t.outlineMid}`,`--hp-day-cycle-outline-far:${t.outlineFar}`].join(";")}function Lr(e,t=1){if(!e)return G;const i=[`--hp-day-cycle-sun-x:${e.sunX.toFixed(2)}%`,`--hp-day-cycle-sun-y:${e.sunY.toFixed(2)}%`,`--hp-day-cycle-sun-opacity:${e.sunOpacity.toFixed(3)}`,`opacity:${Math.min(1,Math.max(0,t)).toFixed(4)}`].join(";");return q``}const Br={color:"#607d8b",opacity:1,widthCm:3.6,fill:!1,fillColor:"#607d8b",fillOpacity:.25},jr=(e,t=1)=>{const i=Number(e);return Number.isFinite(i)?Math.min(1,Math.max(0,i)):t},qr=e=>{let t=Number(e);return Number.isFinite(t)?(t=(t%360+360)%360,t>180?t-360:t):0};function Wr(e,t,i){return qr(t)?[e.x,e.y]:i([e.x,e.y])}const Ur=(e,t,i)=>{const s=Number(e),n=Number.isFinite(t)&&t>0?t:5;return Number.isFinite(s)&&s>0?s/n*i:0},Gr=(e,t,i)=>{const s=Number(e),n=Number.isFinite(t)&&t>0?t:5;return Number.isFinite(s)&&i>0?s/i*n:0},Vr=(e,t,i,s=Br.widthCm)=>{const n=Number(e?.width_cm);if(Number.isFinite(n)&&n>0)return n;const o=Number(e?.width);return Number.isFinite(o)&&o>0?Gr(o,t,i):s};function Kr(e,t){return{color:e.color,opacity:jr(e.opacity),width_cm:Math.max(.1,Math.min(100,Number(e.widthCm)||.1)),...t?{fill:e.fill,fill_color:e.fillColor,fill_opacity:jr(e.fillOpacity,.25)}:{}}}function Yr(e){const t=e.x+e.w/2,i=e.y+e.h/2,s=qr(e.angle)*Math.PI/180,n=Math.cos(s),o=Math.sin(s),a=(e,s)=>{const a=e-t,r=s-i;return[t+a*n-r*o,i+a*o+r*n]};return[a(e.x,e.y),a(e.x+e.w,e.y),a(e.x+e.w,e.y+e.h),a(e.x,e.y+e.h)]}function Jr(e){const t=Yr(e),i=(e,t)=>[(e[0]+t[0])/2,(e[1]+t[1])/2],s=[(t[0][0]+t[2][0])/2,(t[0][1]+t[2][1])/2];return{points:[...t,i(t[0],t[1]),i(t[1],t[2]),i(t[2],t[3]),i(t[3],t[0]),s],segments:t.map((e,i)=>({a:e,b:t[(i+1)%4]}))}}function Xr(e,t,i,s,n,o,a,r){const l=qr(e.angle)*Math.PI/180,c=Math.cos(l),h=Math.sin(l),d=-Math.sin(l),p=Math.cos(l),u=e.x+e.w/2,_=e.y+e.h/2,m=t>0?-e.w/2:e.w/2,g=i>0?-e.h/2:e.h/2,f=u+m*c+g*d,v=_+m*h+g*p,b=s-f,y=n-v;let w=(b*c+y*h)*(t>0?1:-1),k=(b*d+y*p)*(i>0?1:-1),x=null;if(o){const t=w/Math.max(e.w,r),i=k/Math.max(e.h,r);x=Math.max(r/Math.max(e.w,r),r/Math.max(e.h,r),t,i),w=e.w*x,k=e.h*x}if(a>0)if(o){const t=Math.max(1,Math.round(e.w/a)),i=Math.max(1,Math.round(e.h/a));if(Math.abs(e.w-t*a)<1e-6&&Math.abs(e.h-i*a)<1e-6){const s=(e,t)=>{let i=Math.abs(e),s=Math.abs(t);for(;s;)[i,s]=[s,i%s];return Math.max(1,i)},n=1/s(t,i),o=Math.max(r/e.w,r/e.h),a=Math.max(o,Math.round((x??1)/n)*n);w=e.w*a,k=e.h*a}else{e.w>=e.h?(w=Math.round(w/a)*a,k=w*(e.h/Math.max(e.w,r))):(k=Math.round(k/a)*a,w=k*(e.w/Math.max(e.h,r)))}}else w=Math.round(w/a)*a,k=Math.round(k/a)*a;w=Math.max(r,w),k=Math.max(r,k);return{x:f+(t>0?w/2:-w/2)*c+(i>0?k/2:-k/2)*d-w/2,y:v+(t>0?w/2:-w/2)*h+(i>0?k/2:-k/2)*p-k/2,w:w,h:k,angle:qr(e.angle)||void 0}}const Zr=(e,t,i)=>{const s=i[0]-t[0],n=i[1]-t[1],o=s*s+n*n;if(o<1e-12)return[...t];const a=Math.max(0,Math.min(1,((e[0]-t[0])*s+(e[1]-t[1])*n)/o));return[t[0]+s*a,t[1]+n*a]};const Qr=10**9,el=240,tl=1e-4;function il(e){if(Array.isArray(e))return e.map(e=>il(e));if(null!==e&&"object"==typeof e){const t={};for(const[i,s]of Object.entries(e))t[i]=il(s);return t}return e}function sl(e){if("number"!=typeof e||!Number.isFinite(e))return e;const t=(e<0||Object.is(e,-0)?-1:1)*(Math.floor(Math.abs(e)*Qr+.5)/Qr);return 0===t?0:t}function nl(e){if("number"!=typeof e||!Number.isFinite(e))return e;const t=e*el,i=Math.round(t);if(Math.abs(t-i)null!==al(e)):[]}function ll(e,t){for(const i of t)Object.prototype.hasOwnProperty.call(e,i)&&(e[i]=sl(e[i]))}function cl(e,t){for(const i of t)Object.prototype.hasOwnProperty.call(e,i)&&(e[i]=nl(e[i]))}function hl(e){if(Array.isArray(e))for(let t=0;t{for(const n of s){const s=t[n];"number"==typeof s&&Number.isFinite(s)&&i(s,e)}},s=t=>{if(Array.isArray(t))for(let s=0;s{if(Array.isArray(e))for(const t of e)s(t)};for(const i of rl(e.rooms))t(i,["x","y","w","h"]),n(i.poly);for(const t of rl(e.walls))s(t.a),s(t.b);for(const t of rl(e.wall_segments))s(t.a),s(t.b);for(const i of rl(e.openings))t(i,["x","y"]);for(const i of rl(e.decor))"line"===i.kind?t(i,["x1","y1","x2","y2"]):"rect"===i.kind||"ellipse"===i.kind||"furniture"===i.kind?t(i,["x","y","w","h"]):"text"===i.kind&&t(i,["x","y"]);for(const t of rl(e.room_drafts))n(t.points);for(const t of rl(e.partitions))s(t.a),s(t.b);for(const t of rl(e.wall_columns))s(t.center);for(const t of rl(e.open_spans))s(t.a),s(t.b)}const n=al(t);if(!n)return;const o=new Map;if(s)for(const e of rl(s.spaces))null!=e.id&&o.set(String(e.id),e);for(const e of Object.values(n)){const t=al(e);if(!t)continue;const s=null!=t.s&&o.get(String(t.s))||null;for(const e of["x","y"]){const n=t[e];"number"==typeof n&&Number.isFinite(n)&&i(n,s)}}}function ul(e){const t=Number(e?.cell_cm);return t>0?t:5}function _l(e,t={}){const i={canonicalized:0,far:0,maxShift:0,maxShiftCm:0,spaces:[],bySpace:new Map},s=rl(al(e)?.spaces);let n=5;for(const e of s)n=Math.max(n,ul(e));pl(e,t,(e,t)=>{const s=e*el,o=Math.abs(s-Math.round(s)),a=nl(e),r=o>0&&o=tl))return;const l=r&&"number"==typeof a?Math.abs(a-e):0;if(r?i.canonicalized++:i.far++,i.maxShift=Math.max(i.maxShift,l),i.maxShiftCm=Math.max(i.maxShiftCm,l*el*(t?ul(t):n)),!t?.id||!r)return;const c=String(t.id);let h=i.bySpace.get(c);h||(h={spaceId:c,space:String(t.title||c),canonicalized:0,far:0,maxShift:0,maxShiftCm:0},i.bySpace.set(c,h)),h.canonicalized++,h.maxShift=Math.max(h.maxShift,l),h.maxShiftCm=Math.max(h.maxShiftCm,l*el*ul(t))}),i.bySpace.size&&pl(e,t,(e,t)=>{if(!t?.id)return;const s=i.bySpace.get(String(t.id));if(!s)return;const n=e*el;Math.abs(n-Math.round(n))>=tl&&s.far++}),i.spaces=[...i.bySpace.values()];const{bySpace:o,...a}=i;return a}function ml(e){return function(e){const t=e,i=al(t);i&&cl(i,["x","y"]);return t}(il(e))}function gl(e){const t=e,i=al(t);if(!i)return t;for(const e of Object.values(i)){const t=al(e);t&&cl(t,["x","y"])}return t}function fl(e){const t=e,i=al(t);if(!i)return t;for(const e of rl(i.spaces)){ll(e,["plan_x","plan_y","plan_scale","plan_scale_x","plan_scale_y","plan_angle"]);for(const t of rl(e.rooms))cl(t,["x","y","w","h"]),dl(t.poly);for(const t of rl(e.walls))hl(t.a),hl(t.b);for(const t of rl(e.wall_segments))hl(t.a),hl(t.b);for(const t of rl(e.openings)){cl(t,["x","y"]),ll(t,["angle","length"]);const e=al(t.host);e&&ll(e,["t"])}for(const t of rl(e.decor))"line"===t.kind?cl(t,["x1","y1","x2","y2"]):"rect"===t.kind||"ellipse"===t.kind||"furniture"===t.kind?(cl(t,["x","y","w","h"]),ll(t,["angle"])):"text"===t.kind&&(cl(t,["x","y"]),ll(t,["scale","angle"]));for(const t of rl(e.room_drafts))dl(t.points);for(const t of rl(e.partitions))hl(t.a),hl(t.b);for(const t of rl(e.wall_columns))hl(t.center),"square"===t.shape&&ll(t,["angle"]);for(const t of rl(e.open_spans))hl(t.a),hl(t.b)}for(const e of rl(i.markers))ll(e,["angle"]);return t}const vl=15;function bl(e,t,i,s,n){const o=i[0]-e[0],a=i[1]-e[1],r=Math.hypot(o,a);if(!(r>0)||r<=n+1e-9)return null;const l=o/r,c=a/r,h=t=>{const s=(t[0]-e[0])*l+(t[1]-e[1])*c,o=(n-s)/(r-s);return!Number.isFinite(o)||o<-1e-9||o>1+1e-9?null:[t[0]+(i[0]-t[0])*o,t[1]+(i[1]-t[1])*o]},d=h(t),p=h(s);return d&&p?[[e[0],e[1]],t,d,p,s]:null}const yl=Math.sin(.25*Math.PI/180);function wl(e,t){return Number.isFinite(e)&&e>0&&Number.isFinite(t)&&t>0&&e*t<3}function kl(e){const t=Number(e)>0?Number(e):5;if(5===t)return 8;const i=5/t*8;return Math.min(80,Math.max(.5,i))}function xl(e,t){return Number.isFinite(e)&&e>0&&Number.isFinite(t)&&t>0&&e*t<2}function $l(e){return Number.isFinite(e)?Math.max(1,Math.min(100,e)):1}function Sl(e,t){return!Number.isFinite(e)||e<0?"":String(t?Math.round(e/2.54*100)/100:Math.round(100*e)/100)}function Ml(e,t,i){if(!Number.isFinite(e)||e<=0)return 0;const s=Number(t)>0?Number(t):5;return $l(e)/s*i}function Cl(e,t){return t>0&&Number.isFinite(e)?Math.round(e/t)*t:e}function Dl(e){return Math.max(1e-6*Math.abs(e),1e-9)}function Pl(e,t){if(!(t>0&&Number.isFinite(e)))return e;const i=Cl(e,t);return Math.abs(i-e)<=Dl(t)?i:e}function Tl(e,t){let i=t[0]-e[0],s=t[1]-e[1];const n=Math.hypot(i,s);return n<1e-12?[1,0]:(i/=n,s/=n,(i<-1e-12||Math.abs(i)<=1e-12&&s<0)&&(i=-i,s=-s),[i,s])}function Il(e,t,i){const s=[Pl(e[0],i),Pl(e[1],i)],n=[Pl(t[0],i),Pl(t[1],i)],o=Cl((s[0]+n[0])/2,i),a=Cl((s[1]+n[1])/2,i),[r,l]=Tl(s,n);let c=Math.atan2(l,r);c<0&&(c+=Math.PI);const h=Math.round(1800*c)/1800,d=i>0&&i<.01?6:i<1?4:2;return`${o.toFixed(d)},${a.toFixed(d)}@${h.toFixed(4)}`}function Rl(e,t,i,s){return 1===s?Il(e,t,i):Il([e[0]/s,e[1]/s],[t[0]/s,t[1]/s],i)}function Al(e,t){if(!Array.isArray(e.a)||!Array.isArray(e.b)||e.a.length<2||e.b.length<2)return null;const i=[Number(e.a[0]),Number(e.a[1]),Number(e.b[0]),Number(e.b[1])];if(!i.every(Number.isFinite))return null;const s=t>0?t:1;return[[i[0]*s,i[1]*s],[i[2]*s,i[3]*s]]}function zl(e,t,i,s,n){const o=n>0?n:1;return{key:Rl(e,t,s,o),cm:$l(i),a:[e[0]/o,e[1]/o],b:[t[0]/o,t[1]/o]}}function Fl(e,t){const i=t>0?t:1,s=[];for(const t of e){const e=t.key.lastIndexOf("@");if(e<0)continue;const[n,o]=t.key.slice(0,e).split(",").map(Number),a=Number(t.key.slice(e+1));[n,o,a].every(Number.isFinite)&&s.push({w:t,x:n*i,y:o*i,ang:a})}return s}function Nl(e,t){const[i,s]=Tl(e,t);let n=Math.atan2(s,i);return n<0&&(n+=Math.PI),n}function El(e,t){let i=Math.abs(e-t);return i>Math.PI/2&&(i=Math.PI-i),i<.02}function Ol(e,t,i,s,n=1){if(!e?.length)return null;const o=Rl(t,i,s,n),a=e.find(e=>e.key===o);if(a)return a;const r=n>0?n:1,l=Dl(s)*r,c=(e,t)=>Math.abs(e[0]-t[0])<=l&&Math.abs(e[1]-t[1])<=l;for(const s of e){const e=Al(s,r);if(e&&(c(e[0],t)&&c(e[1],i)||c(e[0],i)&&c(e[1],t)))return s}const h=(t[0]+i[0])/2,d=(t[1]+i[1])/2,p=Nl(t,i),u=Math.max(.5*s,1e-9)*r;for(const t of Fl(e,r))if(El(t.ang,p)&&Math.hypot(t.x-h,t.y-d)<=u)return t.w;return null}function Hl(e,t,i,s,n=1){const o=Ol(e,t,i,s,n);if(o&&o.cm>0)return $l(o.cm);const a=function(e,t,i,s,n){if(!e?.length)return null;const o=n>0?n:1,a=Math.hypot(i[0]-t[0],i[1]-t[1]);if(a<1e-12)return null;const r=Nl(t,i),l=Math.max(.5*s,1e-9)*o;let c=null;for(const s of e){if(!(s.cm>0))continue;const e=Al(s,o);if(!e)continue;const n=Math.hypot(e[1][0]-e[0][0],e[1][1]-e[0][1]);if(n<1e-12||!El(Nl(e[0],e[1]),r))continue;if(Sc(t[0],t[1],e[0][0],e[0][1],e[1][0],e[1][1])>l||Sc(i[0],i[1],e[0][0],e[0][1],e[1][0],e[1][1])>l)continue;if(n+l(o.has(e.key)||(e=>{const t=Al(e,s);if(!t)return!1;const[i,o]=t,r=o[0]-i[0],c=o[1]-i[1],h=Math.hypot(r,c);if(h<=l)return!1;if(!a.some(e=>{const t=[e[0],e[1]],s=[e[2],e[3]];return El(Nl(i,o),Nl(t,s))&&Sc(i[0],i[1],t[0],t[1],s[0],s[1])<=l&&Sc(o[0],o[1],t[0],t[1],s[0],s[1])<=l}))return!1;const d=(n||[]).some(e=>{const t=[e[0],e[1]],s=[e[2],e[3]];if(!El(Nl(i,o),Nl(t,s)))return!1;const n=e=>Math.abs((e[0]-i[0])*c-(e[1]-i[1])*r)/h;if(n(t)>l||n(s)>l)return!1;const a=h*h,d=((t[0]-i[0])*r+(t[1]-i[1])*c)/a,p=((s[0]-i[0])*r+(s[1]-i[1])*c)/a;return Math.min(1,Math.max(d,p))-Math.max(0,Math.min(d,p))>l/h});return!d})(e))&&e.cm>=1&&e.cm<=100)}function Bl(e,t,i,s=8){const[n,o]=Tl(e,t);let a=Math.atan2(o,n);a<0&&(a+=Math.PI);let r=i*Math.PI/180%Math.PI;r<0&&(r+=Math.PI);let l=Math.abs(a-r);return l>Math.PI/2&&(l=Math.PI-l),l<=s*Math.PI/180}function jl(e,t,i,s,n=1,o="affine",a){if(!e?.length)return[];if(t.length!==i.length)return"fixed-topology"===o&&a?.(),e.slice();const r=n>0?n:1,l=Math.max(.5*s,1e-9)*r,c=Math.max(s*r*1e-6,1e-9),h=[],d=new Map,p=new Map,u=(e,t,i)=>{const s=e.get(t)||new Set;s.add(i),e.set(t,s)};for(let e=0;e[e[0]+(t[0]-e[0])*i,e[1]+(t[1]-e[1])*i],m=(e,t)=>Math.hypot(e[0]-t[0],e[1]-t[1])<=c,g=(e,t)=>{if("fixed-topology"===o){const i=t.na[0]-t.oa[0],s=t.na[1]-t.oa[1],n=t.nb[0]-t.ob[0],o=t.nb[1]-t.ob[1];return Math.hypot(i-n,s-o)<=c?[e[0]+i,e[1]+s]:m(e,t.oa)?[...t.na]:m(e,t.ob)?[...t.nb]:[...e]}const i=Math.max(0,Math.min(1,((e[0]-t.oa[0])*t.dx+(e[1]-t.oa[1])*t.dy)/t.len2));return _(t.na,t.nb,i)},f=(e,t)=>{const[i,s]=Tl(e,t);return(t[0]-e[0])*i+(t[1]-e[1])*s>=0?[[...e],[...t]]:[[...t],[...e]]},v=[],b=[],y=(e,t,i)=>{if(Math.hypot(t[0]-e[0],t[1]-e[1])<=c)return;const[n,o]=f(e,t),a=$l(i);if(b.some(e=>e.entry.cm===a&&m(e.span[0],n)&&m(e.span[1],o)))return;const l=zl(n,o,a,s,r);v.push(l),b.push({entry:l,span:[n,o]})};for(const t of e){const e=Al(t,r);if(e){const[i,s]=f(e[0],e[1]),n=s[0]-i[0],a=s[1]-i[1],r=n*n+a*a,d=Math.sqrt(r);if(d<=c){v.push({...t,cm:$l(t.cm)});continue}const p=[];for(const e of h){if(!El(Nl(i,s),Nl(e.oa,e.ob)))continue;const t=e=>Math.abs((e[0]-i[0])*a-(e[1]-i[1])*n)/d;if(t(e.oa)>l||t(e.ob)>l)continue;const o=((e.oa[0]-i[0])*n+(e.oa[1]-i[1])*a)/r,h=((e.ob[0]-i[0])*n+(e.ob[1]-i[1])*a)/r,u=Math.max(0,Math.min(o,h)),_=Math.min(1,Math.max(o,h));(_-u)*d>c&&p.push({lo:u,hi:_,move:e})}if(!p.length){"fixed-topology"===o?v.push({...t}):y(i,s,t.cm);continue}const u=[0,1,...p.flatMap(({lo:e,hi:t})=>[e,t])].sort((e,t)=>e-t).filter((e,t,i)=>0===t||Math.abs(e-i[t-1])*d>c),b=[];for(let e=0;e+1r>=e.lo-1e-12&&r<=e.hi+1e-12);if(!l.length){b.push([o,a]);continue}const h=[g(o,l[0].move),g(a,l[0].move)],f=l.slice(1).some(e=>{const t=g(o,e.move),i=g(a,e.move);return!m(h[0],t)||!m(h[1],i)});b.push(f?[o,a]:h)}const w=(e,t)=>{const i=e[1][0]-e[0][0],s=e[1][1]-e[0][1],n=t[1][0]-t[0][0],o=t[1][1]-t[0][1],a=Math.hypot(i,s);return!(a<=c||i*n+s*o<=0)&&Math.abs(i*o-s*n)/a<=c},k=[];for(const e of b){const t=k[k.length-1];t&&m(t[1],e[0])&&w(t,e)?t[1]=e[1]:k.push([[...e[0]],[...e[1]]])}if("fixed-topology"===o&&1===k.length){const[i,s]=k[0];if(m(i,e[0])&&m(s,e[1])||m(i,e[1])&&m(s,e[0])){v.push({...t});continue}}for(const[e,i]of k)y(e,i,t.cm);continue}if("fixed-topology"===o){const e=p.get(t.key);if(1===e?.size){const i=[...e][0];v.push(i===t.key?{...t}:{...t,key:i});continue}const i=[Fl([t],r)[0]];1!==r&&i.push(Fl([t],1)[0]);const s=i.filter(Boolean).some(e=>h.some(t=>{if(!El(e.ang,Nl(t.oa,t.ob)))return!1;const i=((e.x-t.oa[0])*t.dx+(e.y-t.oa[1])*t.dy)/t.len2;return i>=-1e-6&&i<=1.000001&&Sc(e.x,e.y,t.oa[0],t.oa[1],t.ob[0],t.ob[1])<=l}));((e?.size||0)>1||s)&&a?.(),v.push({...t});continue}let i="";const n=d.get(t.key);if(1===n?.size&&(i=[...n][0]),!i){const e=Fl([t],r)[0];if(e){const t=new Set;for(const i of h){if(!El(e.ang,Nl(i.oa,i.ob)))continue;const n=((e.x-i.oa[0])*i.dx+(e.y-i.oa[1])*i.dy)/i.len2;if(n<-1e-6||n>1.000001)continue;if(Sc(e.x,e.y,i.oa[0],i.oa[1],i.ob[0],i.ob[1])>l)continue;const o=g([e.x,e.y],i),[a,c]=Tl(i.na,i.nb),h=Math.max(s*r,1e-6);t.add(Rl([o[0]-a*h,o[1]-c*h],[o[0]+a*h,o[1]+c*h],s,r))}1===t.size&&(i=[...t][0])}}v.push({...t,key:i||t.key,cm:$l(t.cm)})}return v}function ql(e,t,i,s,n=1,o="affine"){return jl(e,t,i,s,n,o)}function Wl(e,t,i,s=1,n=[]){const o=s>0?s:1,a=Math.abs(i),r=Math.max(a*o*tl,1e-9),l=(n||[]).flatMap(e=>{const t=Al(e,o);return t?t.map(e=>[...e]):[]}),c=e=>e.every((e,t)=>(e=>{if(!(a>0))return!0;const t=e/o/a;return Math.abs(t-Math.round(t))Math.abs(e-i[t])<=r)),h=[],d=e=>JSON.stringify([e.key,e.cm,e.a,e.b]);for(const i of e||[]){const e=Al(i,o);if(!e)continue;const[s,n]=e;if(![s[0],s[1],n[0],n[1]].every(Number.isFinite)||!c(s)||!c(n)){h.push(d(i));continue}const a=n[0]-s[0],l=n[1]-s[1],p=Math.hypot(a,l);if(p<=r){h.push(d(i));continue}const u=a/p,_=l/p,m=[];for(const e of t){const[t,i]=e;if(![t?.[0],t?.[1],i?.[0],i?.[1]].every(Number.isFinite))continue;const n=e=>Math.abs((e[0]-s[0])*_-(e[1]-s[1])*u);if(n(t)>r||n(i)>r)continue;const o=(t[0]-s[0])*u+(t[1]-s[1])*_,a=(i[0]-s[0])*u+(i[1]-s[1])*_,l=Math.max(0,Math.min(o,a)),c=Math.min(p,Math.max(o,a));c-l>r&&m.push([l,c])}m.sort((e,t)=>e[0]-t[0]||e[1]-t[1]);let g=0;for(const[e,t]of m){if(e>g+r)break;if(g=Math.max(g,t),g>=p-r)break}ge.key!==a);return null==s||s<1?r:[...r,zl(t,i,s,n,o)]}function Gl(e,t,i,s,n,o=[],a=1){let r=e?e.slice():[];for(const e of function(e,t,i,s,n=1,o=[]){const a=dc(e,t,i,s,n,o);if(!a)return[];const r=[];for(let e=0;e0||(c=Ul(c,t,i,s,n,a)))}return c}function Kl(e){const{a:t,b:i,halfDepth:s}=e;if(!Array.isArray(t)||!Array.isArray(i)||t.length<2||i.length<2||![t[0],t[1],i[0],i[1]].every(Number.isFinite))return null;const n=i[0]-t[0],o=i[1]-t[1],a=Math.hypot(n,o);if(!(a>1e-9&&s>0&&Number.isFinite(s)))return null;const r=-o/a*s,l=n/a*s;return[[t[0]+r,t[1]+l],[i[0]+r,i[1]+l],[i[0]-r,i[1]-l],[t[0]-r,t[1]-l]]}function Yl(e,t,i){return Math.hypot(e[0]-t[0],e[1]-t[1])<=i}function Jl(e,t,i){const s=t.b[0]-t.a[0],n=t.b[1]-t.a[1],o=s*s+n*n;if(!(o>i*i))return!1;const a=((e[0]-t.a[0])*s+(e[1]-t.a[1])*n)/o;if(!(a>0&&a<1))return!1;const r=[t.a[0]+s*a,t.a[1]+n*a];return Math.hypot(e[0]-r[0],e[1]-r[1])<=i}function Xl(e,t,i,s){const n=Math.hypot(t,i);if(!(n>1e-9&&s>0))return;const o=[t/n,i/n],a=e.find(e=>Math.abs(e.u[0]*o[1]-e.u[1]*o[0])<1e-9&&e.u[0]*o[0]+e.u[1]*o[1]>1-1e-9);a?a.halfDepth=Math.max(a.halfDepth,s):e.push({u:o,halfDepth:s})}function Zl(e,t){try{const i=oi(Dc(e),Dc(t));let s=null,n=0;for(const e of i||[]){const t=e?.[0]||[],i=Math.abs(nc(t));t.length>=4&&i>n&&(n=i,s=t.slice(0,-1).map(e=>[e[0],e[1]]))}return s}catch{return null}}function Ql(e,t=1e-6){const i=(e||[]).map((e,t)=>({segment:e,index:t})).filter(({segment:e})=>e&&Array.isArray(e.a)&&Array.isArray(e.b)&&e.a.length>=2&&e.b.length>=2&&e.a.every(Number.isFinite)&&e.b.every(Number.isFinite)&&Number.isFinite(e.halfDepth)&&e.halfDepth>0&&Math.hypot(e.b[0]-e.a[0],e.b[1]-e.a[1])>1e-9);if(i.length<2)return[];const s=Math.max(Number.isFinite(t)?t:0,1e-9),n=i.flatMap(({segment:e})=>[e.a,e.b]).map(e=>[e[0],e[1]]).sort((e,t)=>e[0]-t[0]||e[1]-t[1]),o=[];for(const e of n)o.some(t=>Yl(t,e,s))||o.push(e);const a=[];for(const e of o){const t=[];let n=!1;for(const{segment:o,index:a}of i){const i=Math.hypot(o.b[0]-o.a[0],o.b[1]-o.a[1]);Yl(e,o.a,s)?t.push({u:[(o.b[0]-o.a[0])/i,(o.b[1]-o.a[1])/i],halfDepth:o.halfDepth,length:i,index:a}):Yl(e,o.b,s)?t.push({u:[(o.a[0]-o.b[0])/i,(o.a[1]-o.b[1])/i],halfDepth:o.halfDepth,length:i,index:a}):Jl(e,o,s)&&(n=!0)}if(n||2!==t.length)continue;const[o,r]=t,l=o.u[0]*r.u[1]-o.u[1]*r.u[0];if(Math.abs(l)<1e-9)continue;const c=l<0?1:-1,h=[-o.u[1],o.u[0]],d=[-r.u[1],r.u[0]],p=[e[0]+h[0]*o.halfDepth*c,e[1]+h[1]*o.halfDepth*c],u=[e[0]-d[0]*r.halfDepth*c,e[1]-d[1]*r.halfDepth*c];if(!rc(p,o.u,u,r.u))continue;const _=[[o,r,u,[d[0]*-c,d[1]*-c]],[r,o,p,[h[0]*c,h[1]*c]]];for(const[t,,i,n]of _){const o=Math.min(2*t.halfDepth,t.length),r=[-t.u[1]*t.halfDepth,t.u[0]*t.halfDepth],l=[[e[0]+r[0],e[1]+r[1]],[e[0]+t.u[0]*o+r[0],e[1]+t.u[1]*o+r[1]],[e[0]+t.u[0]*o-r[0],e[1]+t.u[1]*o-r[1]],[e[0]-r[0],e[1]-r[1]]],c=e=>(e[0]-i[0])*n[0]+(e[1]-i[1])*n[1],h=[];for(let e=0;e=-1e-12&&h.push(t),s>1e-12&&n<-1e-12||s<-1e-12&&n>1e-12){const e=s/(s-n);h.push([t[0]+(i[0]-t[0])*e,t[1]+(i[1]-t[1])*e])}}h.length>=3&&Math.abs(nc(h))>s*s&&a.push({segmentIndex:t.index,wedge:h})}}return a}function ec(e,t=1e-6){const i=(e||[]).filter(e=>e&&Array.isArray(e.a)&&Array.isArray(e.b)&&e.a.length>=2&&e.b.length>=2&&e.a.every(Number.isFinite)&&e.b.every(Number.isFinite)&&Number.isFinite(e.halfDepth)&&e.halfDepth>0&&Math.hypot(e.b[0]-e.a[0],e.b[1]-e.a[1])>1e-9);if(i.length<2)return[];const s=Math.max(Number.isFinite(t)?t:0,1e-9),n=i.flatMap(e=>[e.a,e.b]).map(e=>[e[0],e[1]]).sort((e,t)=>e[0]-t[0]||e[1]-t[1]),o=[];for(const e of n)o.some(t=>Yl(t,e,s))||o.push(e);const a=[],r=i.map((e,t)=>({roomId:"",a:[e.a[0],e.a[1]],b:[e.b[0],e.b[1]],key:`join-${t}`,kind:"outer",cm:0,open:!1,half:e.halfDepth})),l=gc(r,s);for(const e of fc(l).fans)a.push(e);const c=e=>!!vc(l,e);for(const e of o){if(c(e))continue;const t=[];for(const n of i)Yl(e,n.a,s)?Xl(t,n.b[0]-n.a[0],n.b[1]-n.a[1],n.halfDepth):Yl(e,n.b,s)?Xl(t,n.a[0]-n.b[0],n.a[1]-n.b[1],n.halfDepth):Jl(e,n,s)&&(Xl(t,n.a[0]-e[0],n.a[1]-e[1],n.halfDepth),Xl(t,n.b[0]-e[0],n.b[1]-e[1],n.halfDepth));if(!(t.length<2)){t.sort((e,t)=>Math.atan2(e.u[1],e.u[0])-Math.atan2(t.u[1],t.u[0])||e.halfDepth-t.halfDepth);for(let i=0;is*s&&a.push(m)}}}return a}function tc(e,t,i,s){if(!(t>0)||!e||e.length<2)return"";if(i&&e.length>=3){let i=e;const n=e[e.length-1];if(e.length>=4&&Math.hypot(e[0][0]-n[0],e[0][1]-n[1])<1e-9&&(i=e.slice(0,-1)),i.length>=3){const e=i.map((e,i)=>s?.[i]||t),n=Vc(i,e),o=lc(i,e);if(n&&o)return`${cc(n)} ${cc(hc(o))}`}}const n=[];for(let i=0;i=1e-9&&r>0&&n.push({a:o,b:a,halfDepth:r})}const o=n.map(Kl);for(const{segmentIndex:e,wedge:t}of Ql(n)){const i=o[e];if(!i)continue;const s=Zl(i,t);s&&(o[e]=s)}const a=[...o.filter(e=>!!e),...ec(n)],r=function(e){let t=null;try{for(const i of e){if(i.length<3)continue;const e=Dc(i);t=t?si(t,e):[e]}return t}catch{return null}}(a);return r?Lc(r):a.map(e=>cc(e)).join(" ")}function ic(e,t,i,s,n=1){if(!i.length)return!1;const o=sc(s,n),a=(e[0]+t[0])/2,r=(e[1]+t[1])/2,[l,c]=Tl(e,t);for(const e of i){const[t,i]=Tl([e[0],e[1]],[e[2],e[3]]);if(!(Math.abs(l*i-c*t)>.05)&&Sc(a,r,e[0],e[1],e[2],e[3])<=o)return!0}return!1}function sc(e,t){return Math.max(e*(t>0?t:1)*.04,1e-9)}function nc(e){let t=0;for(let i=0;ie[1]!=l>e[1]&&e[0]<(r-o)*(e[1]-a)/(l-a+0)+o&&(i=!i)}return i}([c[0]+.001*r,c[1]+.001*l],e)||(r=-r,l=-l),[r,l]}function ac(e,t){const i=e[0]*t[1]-e[1]*t[0],s=e[0]*t[0]+e[1]*t[1];return Math.abs(i)<1e-9&&s>0}function rc(e,t,i,s){const n=t[0]*s[1]-t[1]*s[0];if(Math.abs(n)<1e-12)return null;const o=[i[0]-e[0],i[1]-e[1]],a=(o[0]*s[1]-o[1]*s[0])/n;return[e[0]+a*t[0],e[1]+a*t[1]]}function lc(e,t,i){const s=e?.length||0;if(s<3||t.length!==s)return null;if(t.every(e=>!(e>0)))return e.map(e=>[e[0],e[1]]);const n=[];for(let o=0;o0||p>0)){n.push([e[o][0],e[o][1]]);continue}if(d>0!=p>0){const t=e[o],i=d>0?[t[0]+u*d,t[1]+_*d]:[t[0],t[1]],s=p>0?[t[0]+m*p,t[1]+g*p]:[t[0],t[1]];n.push(i),Math.hypot(s[0]-i[0],s[1]-i[1])>1e-9&&n.push(s);continue}if(ac(w,k)){const t=e[o],i=[t[0]+u*d,t[1]+_*d],s=[t[0]+m*p,t[1]+g*p];n.push(i),Math.hypot(s[0]-i[0],s[1]-i[1])>1e-9&&n.push(s);continue}const S=rc(x,w,$,k),M=Math.max(d,p,1e-9),C=vc(i,e[o])?.limit??4*M;if(S){const i=Math.hypot(S[0]-e[o][0],S[1]-e[o][1]);if(Number.isFinite(i)&&i<=C){n.push(S);continue}if(jc(e,t,o)){n.push(S);continue}}d>0&&n.push([e[o][0]+u*d,e[o][1]+_*d]),p>0&&n.push([e[o][0]+m*p,e[o][1]+g*p]),d>0||p>0||n.push([e[o][0],e[o][1]])}return n.length>=3?n:null}function cc(e,t=!0){if(!e.length)return"";let i=`M ${e[0][0]} ${e[0][1]}`;for(let t=1;te?.id===t),r=hi(a);if(!r||r.length<3)return null;const l=sc(s,n),c=[];for(const i of e||[]){if(!i||i.id===t)continue;const e=hi(i);if(e)for(const t of Cs(r,e,l))c.push([t[0],t[1]],[t[2],t[3]])}for(const e of i||[])c.push([e[0],e[1]],[e[2],e[3]]);for(const e of o||[]){const t=Al(e,n);t&&c.push(t[0],t[1])}const h=[],d=[];for(let e=0;el)continue;const a=((e[0]-t[0])*(i[0]-t[0])+(e[1]-t[1])*(i[1]-t[1]))/(s*s);a<=n||a>=1-n||(o.some(e=>Math.abs(e-a)*s<=2*l)||o.push(a))}o.sort((e,t)=>e-t);for(const s of o)h.push([t[0]+(i[0]-t[0])*s,t[1]+(i[1]-t[1])*s]),d.push(e)}return{poly:h,parent:d,orig:r}}function pc(e,t,i,s){const n=t.poly.length,o=new Array(n).fill(0);if(!e?.length)return o;const a=new Set,r=[];for(let l=0;l0?(o[l]=$l(c.cm),a.add(c.key)):r.push(l)}if(!r.length)return o;const l=s>0?s:1,c=Math.max(.5*i,1e-9)*l,h=Fl(e,l).filter(e=>e.w.cm>0);for(let e=r.length-1;e>=0;e--){const i=r[e],s=t.poly[i],a=t.poly[(i+1)%n],d=Nl(s,a);let p=null;for(const e of h){const t=Al(e.w,l);if(!t||!El(Nl(t[0],t[1]),d))continue;if(Sc(s[0],s[1],t[0][0],t[0][1],t[1][0],t[1][1])>c||Sc(a[0],a[1],t[0][0],t[0][1],t[1][0],t[1][1])>c)continue;const i=Math.hypot(a[0]-s[0],a[1]-s[1]),n=Math.hypot(t[1][0]-t[0][0],t[1][1]-t[0][1]),o=Math.max(0,n-i);(!p||oc||Sc(n[0],n[1],t[0][0],t[0][1],t[1][0],t[1][1])>c)continue;i=!0,o=Math.max(0,Math.hypot(t[1][0]-t[0][0],t[1][1]-t[0][1])-_)}else{if(Sc(e.x,e.y,s[0],s[1],n[0],n[1])>c)continue;o=Math.hypot(e.x-d,e.y-p)}(!u||i&&!u.exact||i===u.exact&&o0?i:1,o=(e||[]).filter(e=>e&&!e.open&&null!==e.kind&&Number.isFinite(e.half)&&e.half>0&&Array.isArray(e.a)&&Array.isArray(e.b)&&e.a.length>=2&&e.b.length>=2&&[e.a[0],e.a[1],e.b[0],e.b[1]].every(Number.isFinite)&&Math.hypot(e.b[0]-e.a[0],e.b[1]-e.a[1])>s).sort((e,t)=>e.key.localeCompare(t.key)||e.a[0]-t.a[0]||e.a[1]-t.a[1]||e.b[0]-t.b[0]||e.b[1]-t.b[1]||e.half-t.half),a=new Map;for(const e of o){const t=a.get(e.key);t?e.half>t.half&&a.set(e.key,{...t,half:e.half}):a.set(e.key,e)}const r=[...a.values()].flatMap(e=>[{point:[e.a[0],e.a[1]],other:e.b,halfDepth:e.half,kind:e.kind,key:e.key},{point:[e.b[0],e.b[1]],other:e.a,halfDepth:e.half,kind:e.kind,key:e.key}]).sort((e,t)=>e.point[0]-t.point[0]||e.point[1]-t.point[1]||e.other[0]-t.other[0]||e.other[1]-t.other[1]||e.halfDepth-t.halfDepth),l=new Map;for(const e of r){const[t,i]=uc(e.point,s),n=_c(t,i),o=l.get(n)||[];o.push(e),l.set(n,o)}const c=[],h=new Map;for(const e of r){const t=mc(h,e.point,s).filter(t=>Math.hypot(t.point[0]-e.point[0],t.point[1]-e.point[1])<=s).sort((t,i)=>Math.hypot(t.point[0]-e.point[0],t.point[1]-e.point[1])-Math.hypot(i.point[0]-e.point[0],i.point[1]-e.point[1])||t.point[0]-i.point[0]||t.point[1]-i.point[1]);let i=t[0];if(!i){i={point:[...e.point],rays:[]},c.push(i);const[t,n]=uc(i.point,s),o=_c(t,n),a=h.get(o)||[];a.push(i),h.set(o,a)}const n=e.other[0]-e.point[0],o=e.other[1]-e.point[1],a=Math.hypot(n,o);if(!(a>s))continue;const r=[n/a,o/a];let l=Math.atan2(r[1],r[0]);l<0&&(l+=2*Math.PI),i.rays.push({u:r,halfDepth:e.halfDepth,length:a,angle:l})}const d=[],p=e=>{const t=1e-9*Math.max(1,n),i=e.filter(e=>Number.isFinite(e.halfDepth)&&e.halfDepth>0&&Number.isFinite(e.length)&&e.length>s);return i.filter((e,s)=>!i.some((i,n)=>n!==s&&i.halfDepth>=e.halfDepth-t&&i.length>=e.length-t&&(i.halfDepth>e.halfDepth+t||i.length>e.length+t||ne.length-t.length||e.halfDepth-t.halfDepth).map(e=>({...e}))};for(const e of c){const t=e.rays.sort((e,t)=>e.angle-t.angle||e.length-t.length||e.halfDepth-t.halfDepth),i=[];for(const e of t){const t=i[i.length-1];t&&Math.abs(e.angle-t.angle)<=1e-9?t.supports.push({halfDepth:e.halfDepth,length:e.length}):i.push({u:[...e.u],angle:e.angle,supports:[{halfDepth:e.halfDepth,length:e.length}]})}if(i.length>1&&2*Math.PI-i[i.length-1].angle+i[0].angle<=1e-9){const e=i.pop();i[0].supports.push(...e.supports)}if(i.length<3)continue;const n=i.map(t=>{const i=p(t.supports),n=new Set,o=[];for(const a of i){const i=[e.point[0]+t.u[0]*a.length,e.point[1]+t.u[1]*a.length];for(const t of mc(l,i,s)){if("shared"!==t.kind||Math.hypot(t.point[0]-i[0],t.point[1]-i[1])>s)continue;const a=t.other[0]-t.point[0],r=t.other[1]-t.point[1],l=Math.hypot(a,r);if(!(l>s))continue;const c=a/l,h=r/l;if(Math.hypot(t.other[0]-e.point[0],t.other[1]-e.point[1])<=s)continue;const d=`${t.key}|${t.point[0]}|${t.point[1]}|${t.other[0]}|${t.other[1]}|${t.halfDepth}`;n.has(d)||(n.add(d),o.push({start:[t.point[0],t.point[1]],u:[c,h],length:l,halfDepth:t.halfDepth}))}}return o.sort((e,t)=>e.start[0]-t.start[0]||e.start[1]-t.start[1]||e.u[0]-t.u[0]||e.u[1]-t.u[1]||e.length-t.length||e.halfDepth-t.halfDepth),{u:[...t.u],halfDepth:Math.max(...i.map(e=>e.halfDepth)),length:Math.max(...i.map(e=>e.length)),supports:i,continuations:o}}).filter(e=>Number.isFinite(e.halfDepth)&&e.halfDepth>0&&Number.isFinite(e.length)&&e.length>s);if(n.length<3)continue;const o=Math.max(...n.map(e=>e.halfDepth));o>0&&Number.isFinite(o)&&d.push({point:[...e.point],rays:n,halfDepth:o,limit:1.25*o})}d.sort((e,t)=>e.point[0]-t.point[0]||e.point[1]-t.point[1]);const u=new Map;for(const e of d){const[t,i]=uc(e.point,s),n=_c(t,i),o=u.get(n)||[];o.push(e),u.set(n,o)}return{epsilon:s,coordinateScale:n,nodes:d,index:u}}function fc(e){const t={fans:[],supports:[]};if(!e?.nodes?.length)return t;const i=Math.max(e.epsilon,1e-9)**2;for(const s of e.nodes){const e=s.rays.filter(e=>Number.isFinite(e.halfDepth)&&e.halfDepth>0).map(e=>({...e,thickLength:Math.max(...e.supports.filter(t=>t.halfDepth>=e.halfDepth-1e-12).map(e=>e.length),0),angle:(()=>{const t=Math.atan2(e.u[1],e.u[0]);return t<0?t+2*Math.PI:t})()})).sort((e,t)=>e.angle-t.angle);if(e.length<2)continue;const n=s.point;for(const i of e)for(const e of i.supports){if(!(e.halfDepth>0&&e.length>0))continue;const s=-i.u[1]*e.halfDepth,o=i.u[0]*e.halfDepth,a=[n[0]+i.u[0]*e.length,n[1]+i.u[1]*e.length];t.supports.push([[n[0]+s,n[1]+o],[a[0]+s,a[1]+o],[a[0]-s,a[1]-o],[n[0]-s,n[1]-o]])}for(let s=0;s{const e=a.angle-o.angle;return e>0?e:e+2*Math.PI})();if(r<1e-9)continue;const l=r>Math.PI+1e-9,c=4*Math.max(o.halfDepth,a.halfDepth),h=[n[0]-o.u[1]*o.halfDepth,n[1]+o.u[0]*o.halfDepth],d=[n[0]+a.u[1]*a.halfDepth,n[1]-a.u[0]*a.halfDepth],p=o.u[0]*a.u[1]-o.u[1]*a.u[0],u=e=>{let t=Math.atan2(e[1]-n[1],e[0]-n[0])-o.angle;for(;t<0;)t+=2*Math.PI;return t<=r+1e-9};let _=null;if(Math.abs(p)>1e-9){const e=((d[0]-h[0])*a.u[1]-(d[1]-h[1])*a.u[0])/p,t=((d[0]-h[0])*o.u[1]-(d[1]-h[1])*o.u[0])/p,i=[h[0]+o.u[0]*e,h[1]+o.u[1]*e];(l?e<=1e-9&&t<=1e-9:e>1e-9&&e<=o.thickLength&&t<=a.thickLength)&&Math.hypot(i[0]-n[0],i[1]-n[1])<=c&&u(i)&&(_=i)}const m=e=>{Math.abs(nc(e))>i&&t.fans.push(e)};if(_){const e=1.5*Math.max(o.halfDepth,a.halfDepth);m(bl([n[0],n[1]],h,_,d,e)??[[n[0],n[1]],h,_,d]);continue}if(l){m([[n[0],n[1]],h,d]);continue}const g=(e,t)=>Math.min(t,Math.sqrt(Math.max(c**2-e**2,0)),2*Math.max(o.halfDepth,a.halfDepth)),f=[h[0]+o.u[0]*g(o.halfDepth,o.thickLength),h[1]+o.u[1]*g(o.halfDepth,o.thickLength)],v=[d[0]+a.u[0]*g(a.halfDepth,a.thickLength),d[1]+a.u[1]*g(a.halfDepth,a.thickLength)];m([[n[0],n[1]],h,f,v,d])}}return t}function vc(e,t){return!e||!Array.isArray(t)||t.length<2||!t.slice(0,2).every(Number.isFinite)?null:mc(e.index,t,e.epsilon).filter(i=>Math.hypot(i.point[0]-t[0],i.point[1]-t[1])<=e.epsilon).sort((e,i)=>Math.hypot(e.point[0]-t[0],e.point[1]-t[1])-Math.hypot(i.point[0]-t[0],i.point[1]-t[1])||e.point[0]-i.point[0]||e.point[1]-i.point[1])[0]||null}function bc(e,t,i,s,n,o,a){return gc(wc(e,t,i,s,n,o,a),4*sc(s,a),a)}function yc(e,t,i,s,n,o,a,r=1){const l=dc(e,t,s,n,r,i);if(!l)return null;const c=function(e,t,i){const s=(e||[]).find(e=>e?.id===t),n=hi(s);if(!n)return[];const o=[];for(const s of e||[]){if(!s||s.id===t)continue;const e=hi(s);if(e)for(const t of Cs(n,e,i))o.push(t)}return o}(e,t,sc(n,r)),h=function(e,t,i,s,n){const o=sc(s,n),a=[];for(let r=0;rSc(h,d,e[0],e[1],e[2],e[3])<=o);a.push(p?"shared":"outer")}return a}(l.poly,c,s,n,r),d=pc(i,l,n,r),p=d.map((e,t)=>h[t]&&e>0?Ml(e,o,a)/2:0);return{...l,kinds:h,cms:d,offsets:p}}function wc(e,t,i,s,n,o,a=1){const r=[];for(const l of e||[]){if(!l?.id)continue;const c=yc(e,l.id,t,i,s,n,o,a);if(c)for(let e=0;e0&&(r=Ul(r,e.a,e.b,e.cm,s,a));return r}function xc(e,t,i,s,n,o,a=1){if(!t?.length)return[];const r=wc(e,t,i,s,n,o,a),l=new Map;for(const e of r){if(e.open||!e.kind||!e.roomId)continue;const t=l.get(e.key)||new Set;t.add(e.roomId),l.set(e.key,t)}const c=e=>{const t=[...l.get(e)||[]].sort();return 1!==t.length&&2!==t.length?`ambiguous:${e}`:`${1===t.length?"outer":"shared"}:${t.join("|")}`},h=[],d=new Set;for(const e of r)!e.open&&e.cm>0&&!d.has(e.key)&&(d.add(e.key),h.push({...e,ownerSignature:c(e.key)}));const p=[];for(const r of e||[]){if(!r?.id)continue;const l=yc(e,r.id,t,i,s,n,o,a);if(l)for(let e=0;e0)||null===l.kinds[i]){e++;continue}const o=c(Rl(l.poly[i],l.poly[(i+1)%l.poly.length],s,a));let r=e;for(;r+10&&p.push({a:[d[0],d[1]],b:[u[0],u[1]],key:Rl(d,u,s,a),cm:n,len:_,ownerSignature:o}),e=r+1}}}p.sort((e,t)=>t.len-e.len||e.key.localeCompare(t.key));const u=[],_=new Set,m=new Set,g=4*sc(s,a);for(const e of p){const t=h.filter(t=>!m.has(t.key)&&t.cm===e.cm&&t.ownerSignature===e.ownerSignature&&El(Nl(t.a,t.b),Nl(e.a,e.b))&&Sc(t.a[0],t.a[1],e.a[0],e.a[1],e.b[0],e.b[1])<=g&&Sc(t.b[0],t.b[1],e.a[0],e.a[1],e.b[0],e.b[1])<=g);if(t.length){for(const e of t)m.add(e.key);_.has(e.key)||(_.add(e.key),u.push(zl(e.a,e.b,e.cm,s,a)))}}for(const e of h)m.has(e.key)||_.has(e.key)||(_.add(e.key),u.push(zl(e.a,e.b,e.cm,s,a)));return u}function $c(e,t,i,s,n,o,a,r=1){const l=sc(n,r),c=(s[0]+s[2])/2,h=(s[1]+s[3])/2,d=Nl([s[0],s[1]],[s[2],s[3]]);let p=null;for(const s of wc(e,t,i,n,o,a,r)){if(!El(Nl(s.a,s.b),d))continue;const e=Sc(c,h,s.a[0],s.a[1],s.b[0],s.b[1]);e>4*l||(!p||e[e[0],e[1]]),o=Math.abs(nc(n));n.length>=3&&o>i&&(t=n,i=o)}return t}function Cc(e,t,i,s,n,o,a,r=1,l,c){const h=(e||[]).find(e=>e?.id===t),d=hi(h);if(!d||d.length<3)return null;if(!i?.length)return d.map(e=>[e[0],e[1]]);const p=yc(e,t,i,s,n,o,a,r);if(!p||!p.offsets.some(e=>e>0))return d.map(e=>[e[0],e[1]]);const u=c||bc(e,i,s,n,o,a,r),_=lc(p.poly,p.offsets,u);if(!_)return d.map(e=>[e[0],e[1]]);if(!u.nodes.length)return _;const m=void 0===l?qc(e,i,s,[],n,o,a,r):null,g=l??("ok"===m?.status||"degraded-extra"===m?.status?m.roomGeom:void 0);if(g)try{const e=Mc(oi(Dc(p.poly),g));if(e)return e}catch{}return function(e,t){try{return Mc(ni(Dc(e),Dc(t)))}catch{return null}}(_,p.poly)||d.map(e=>[e[0],e[1]])}function Dc(e){const t=e.map(e=>[e[0],e[1]]);return t.push([e[0][0],e[0][1]]),[t]}function Pc(e){return!(!Array.isArray(e)||!e.length)&&e.every(e=>Array.isArray(e)&&e.length&&e.every(e=>Array.isArray(e)&&e.length>=4&&e.every(e=>Array.isArray(e)&&e.length>=2&&Number.isFinite(e[0])&&Number.isFinite(e[1])))&&Math.abs(Ti(e[0]))>1e-9)}function Tc(e,t,i,s){const n=s?new Set(s):null;let o=null;for(let s=0;st.epsilon))continue;const l=Fc([[e.point[0]+r[0]*s.halfDepth,e.point[1]+r[1]*s.halfDepth],[e.point[0]+a.u[0]*n+r[0]*s.halfDepth,e.point[1]+a.u[1]*n+r[1]*s.halfDepth],[e.point[0]+a.u[0]*n-r[0]*s.halfDepth,e.point[1]+a.u[1]*n-r[1]*s.halfDepth],[e.point[0]-r[0]*s.halfDepth,e.point[1]-r[1]*s.halfDepth]],t.coordinateScale);if(!l)continue;const c=Dc(l);o=o?si(o,c):c}}return o}function Ic(e,t,i){let s=null;for(const n of e.rays)for(const e of n.continuations){const n=[-e.u[1],e.u[0]],o=[e.start[0]+e.u[0]*e.length,e.start[1]+e.u[1]*e.length],a=Fc([[e.start[0]+n[0]*e.halfDepth,e.start[1]+n[1]*e.halfDepth],[o[0]+n[0]*e.halfDepth,o[1]+n[1]*e.halfDepth],[o[0]-n[0]*e.halfDepth,o[1]-n[1]*e.halfDepth],[e.start[0]-n[0]*e.halfDepth,e.start[1]-n[1]*e.halfDepth]],t.coordinateScale);if(!a)continue;const r=ni(Dc(a),i);Array.isArray(r)&&0!==r.length&&(s=s?si(s,r):r)}return s}function Rc(e,t,i=2*(4*e.halfDepth+2*t.epsilon)){const s=function(e,t=yl){const i=new Set,s=Number.isFinite(t)&&t>=0?t:yl;for(let t=0;te-t)}(e);return s.length?Tc(e,t,i,s):null}function Ac(e,t,i,s,n){const o=function(e){let t=null;for(const i of e){const e=Dc(i);try{t=t?si(t,e):e}catch{}}return t}(function(e,t,i=!1){if(!e)return[];const s=[];for(const n of e.nodes)for(let o=0;o1e-9)||h>=Math.PI-1e-9)continue;const d=[-a.u[1],a.u[0]],p=[-r.u[1],r.u[0]],u=[n.point[0]+d[0]*a.halfDepth,n.point[1]+d[1]*a.halfDepth],_=[n.point[0]-p[0]*r.halfDepth,n.point[1]-p[1]*r.halfDepth],m=rc(u,a.u,_,r.u);if(!m)continue;const g=Math.hypot(m[0]-n.point[0],m[1]-n.point[1]);if(!Number.isFinite(g)||g<=n.limit)continue;const f=t?Math.sqrt(Math.max(0,n.limit*n.limit-a.halfDepth*a.halfDepth)):0,v=t?Math.sqrt(Math.max(0,n.limit*n.limit-r.halfDepth*r.halfDepth)):0,b=Fc([[u[0]+a.u[0]*f,u[1]+a.u[1]*f],[_[0]+r.u[0]*v,_[1]+r.u[1]*v],m],e.coordinateScale);if(b&&s.push(b),i){const t=m[0]-n.point[0],i=m[1]-n.point[1],o=Math.hypot(t,i),a=g-n.limit;if(o>e.epsilon&&a>e.epsilon){const r=Math.min(Math.max(8*e.epsilon,.05*n.halfDepth),.25*a),l=t/o,c=i/o,h=-c,d=l,p=Fc([[m[0]-l*r+h*r,m[1]-c*r+d*r],[m[0]-l*r-h*r,m[1]-c*r-d*r],[m[0]+l*r,m[1]+c*r]],e.coordinateScale);p&&s.push(p)}}}return s}({...t,nodes:[e]},i,s));return o&&n?oi(o,n):o}function zc(e,t,i,s){if(!e||!t.nodes.length)return e;let n=null;try{n=function(e){let t=null;for(const i of e.nodes){const s=Rc(i,e);s&&(t=t?si(t,s):s)}return t}(t)}catch{return e}let o=e;for(const e of t.nodes){const a=4*e.halfDepth+2*t.epsilon,r=2*a,l=[[e.point[0]-a,e.point[1]-a],[e.point[0]+a,e.point[1]-a],[e.point[0]+a,e.point[1]+a],[e.point[0]-a,e.point[1]+a]];try{let a=o;const c=Ac(e,t,!1,!0,n);c&&(a=oi(a,c));let h=Tc(e,t,r);const d=Ac(e,t,!0,!0,n);d&&(h=oi(h,d)),n&&(h=si(h,n));const p=.02*Math.min(...e.rays.map(e=>e.halfDepth));if(h=si(h,Dc([[e.point[0]-p,e.point[1]-p],[e.point[0]+p,e.point[1]-p],[e.point[0]+p,e.point[1]+p],[e.point[0]-p,e.point[1]+p]])),!h)continue;let u=ni(h,Dc(l));s?u=ni(u,s):i&&(u=ni(u,i));const _=Dc(l),m=oi(a,_),g=i?oi(ni(a,_),i):null,f=Ic(e,t,_);o=si(m,...g?[g]:[],...f?[f]:[],u)}catch{}}if(n)try{let e=n;s?e=ni(e,s):i&&(e=ni(e,i)),o=si(o,e)}catch{}return o}function Fc(e,t=1){if(!Array.isArray(e)||e.length<3)return null;const i=Number.isFinite(t)&&t>0?t:1,s=1e-12*Math.max(1,i),n=[];for(const t of e){if(!Array.isArray(t)||t.length<2)return null;const e=Number(t[0]),i=Number(t[1]);if(!Number.isFinite(e)||!Number.isFinite(i))return null;const o=Math.round(e/s)*s,a=Math.round(i/s)*s;if(!Number.isFinite(o)||!Number.isFinite(a))return null;n.push([Object.is(o,-0)?0:o,Object.is(a,-0)?0:a])}return Math.abs(nc(n))>s*s?n:null}function Nc(e,t,i,s){if(Sc(e[0],e[1],t[0],t[1],i[0],i[1])>s)return!1;const n=i[0]-t[0],o=i[1]-t[1],a=(e[0]-t[0])*n+(e[1]-t[1])*o,r=n*n+o*o,l=s*Math.sqrt(r);return a>=-l&&a<=r+l}function Ec(e,t,i){const s=[],n=[];for(let o=0;oi*i))continue;const d=i/Math.sqrt(h),p=[0,1];for(const e of t)for(const t of[e.a,e.b]){if(!Nc(t,a,r,i))continue;const e=((t[0]-a[0])*l+(t[1]-a[1])*c)/h;e>d&&e<1-d&&p.push(e)}p.sort((e,t)=>e-t);const u=p.filter((e,t)=>0===t||Math.abs(e-p[t-1])>d);for(let e=0;e=3&&n.length===s.length?{poly:s,offsets:n}:null}function Oc(e,t,i,s,n,o,a,r){const l=(e||[]).map(hi).filter(e=>!!e&&e.length>=3);if(!l.length)return null;let c=si(Dc(l[0]));for(let e=1;e"outer"===e.kind&&e.half>0),p=4*sc(s,a),u=r||gc(h,p,a);let _=null;for(const e of function(e){const t=[];for(const i of Array.isArray(e)?e:[])if(Array.isArray(i))for(const e of i){if(!Array.isArray(e)||e.length<4)continue;const i=e.slice(0,-1).map(e=>[e[0],e[1]]);i.length>=3&&t.push(i)}return t}(c)){const t=Ec(e,d,p);if(!t||!t.offsets.some(e=>e>0))continue;const i=Vc(t.poly,t.offsets,u),s=lc(t.poly,t.offsets,u);if(!i||!s)continue;const n=oi(Dc(i),Dc(s));_=_?si(_,n):n}return{centre:c,shell:_||[]}}function Hc(e,t,i,s,n,o,a=1){try{const r=Oc(e,t,i,s,n,o,a,bc(e,t,i,s,n,o,a));if(!r)return[];return r.shell?.length?si(r.centre,r.shell):r.centre}catch{return null}}function Lc(e){if(!e)return"";let t="";for(const i of e)if(Array.isArray(i))for(const e of i){if(!Array.isArray(e)||e.length<4)continue;const i=e.slice(0,e.length-1);i.length<3||(t+=(t?" ":"")+cc(i.map(e=>[e[0],e[1]])))}return t}const Bc=15;function jc(e,t,i){const s=e?.length||0;if(s<3||t?.length!==s)return!1;const n=e[(i-1+s)%s],o=e[i],a=e[(i+1)%s],r=[n[0]-o[0],n[1]-o[1]],l=[a[0]-o[0],a[1]-o[1]],c=Math.hypot(r[0],r[1]),h=Math.hypot(l[0],l[1]);if(!(c>1e-9&&h>1e-9))return!1;const d=Math.max(-1,Math.min(1,(r[0]*l[0]+r[1]*l[1])/(c*h))),p=Math.acos(d);if(!(p>1e-9))return!1;if(p>=Bc*Math.PI/180)return!1;const u=Math.max(Math.max(0,t[(i-1+s)%s]),Math.max(0,t[i]));if(!(u>0))return!1;const _=u/Math.tan(p/2);return Number.isFinite(_)&&_>0&&_e>0))continue;for(const e of l.offsets)e>0&&(p=Math.max(p,2*e));const c=Vc(l.poly,l.offsets,d),u=lc(l.poly,l.offsets,d);c&&h.push({outset:c,inset:u})}for(const e of l){const t=e.map(e=>e[0]),i=e.map(e=>e[1]);if(t.length){const s=Math.min(Math.max(...t)-Math.min(...t),Math.max(...i)-Math.min(...i)),n=Math.min(...e.map((t,i)=>{const s=e[(i+1)%e.length];return Math.hypot(s[0]-t[0],s[1]-t[1])}));p=Math.max(p,e.length>16?s:n)}}const u=function(e,t,i,s,n,o,a,r){if(!t?.length||!i?.length)return[];const l=4*sc(s,a),c=new Map;for(const r of wc(e,t,i,s,n,o,a))!r.open&&r.half>0&&!c.has(r.key)&&c.set(r.key,r);const h=[...c.values()];if(h.length<2)return[];const d=r||gc(h,l),p=[];for(const e of i)for(const t of[[e[0],e[1]],[e[2],e[3]]])p.some(e=>Math.hypot(e[0]-t[0],e[1]-t[1])<=l)||p.push(t);const u=[],_=(e,t)=>{let i=0,s=0;if(Math.hypot(e.a[0]-t[0],e.a[1]-t[1])<=l)i=e.b[0]-e.a[0],s=e.b[1]-e.a[1];else{if(!(Math.hypot(e.b[0]-t[0],e.b[1]-t[1])<=l))return null;i=e.a[0]-e.b[0],s=e.a[1]-e.b[1]}const n=Math.hypot(i,s);return n>l?[i/n,s/n]:null};for(const e of p){const t=h.map(t=>({iv:t,u:_(t,e)})).filter(e=>!!e.u);for(let i=0;i0?[e.slice(),p,m,_]:[e.slice(),_,m,p];else{if(!f)continue;{const t=[-n.u[1],n.u[0]],i=[-o.u[1],o.u[0]],s=a<0?1:-1,r=[e[0]+t[0]*n.iv.half*s,e[1]+t[1]*n.iv.half*s],l=[e[0]-i[0]*o.iv.half*s,e[1]-i[1]*o.iv.half*s];b=a>0?[e.slice(),r,l]:[e.slice(),l,r]}}Math.abs(nc(b))>l*l&&u.push(b)}}return u}(e,t,i,n,o,a,r,d),_=s.length?Kc(e,t,i,n,o,a,r):null;let m="exterior";try{const v=Oc(e,t,i,n,o,a,r,d);m="paper";const b=v?v.shell?.length?si(v.centre,v.shell):v.centre:[],y=e=>{const t=Dc(e.outset);if(!e.inset)return t;const i=Dc(e.inset);return oi(t,i)};m="room-rings";let w=null;for(const e of h)try{const t=y(e);w=w?si(w,t):t}catch{}if(m="edge-bodies",v)for(const s of Gc(e,t,i,n,o,a,r))try{const e=ni(Dc(s.quad),v.centre);w=w?si(w,e):e}catch{}m="junctions",w=function(e,t,i=1,s=si){let n=e;for(const e of t||[]){const t=Fc(e,i);if(t)try{const e=Dc(t);n=n?s(n,e):e}catch{}}return n}(w,u,r),m="facade-clip",w&&v&&(w=ni(w,v.centre)),m="exterior-shell";const k=[];let x=0;if(v?.shell?.length)if(w)try{const e=si(w,v.shell);if(!Pc(e))throw new Error("invalid shell union");w=e}catch{if(!Pc(w)||!Pc(v.shell))throw new Error("invalid shell");k.push({id:"exterior-shell",geom:v.shell}),x++}else w=v.shell;if(m="multi-wall-trim",w&&d.nodes.length){const e=e=>e.rays.some(e=>e.supports.some(e=>e.length<2*e.halfDepth)),t=d.nodes.filter(e);if(t.length){w=zc(w,{...d,nodes:t},v?.centre,b)}}if(m="junction-corners",d.nodes.length){const s=fc(d),l=function(e,t,i,s,n,o,a,r){try{const l=Oc(e,t,i,s,n,o,a,{epsilon:r.epsilon,coordinateScale:r.coordinateScale,nodes:[],index:new Map});return l?l.shell?.length?si(l.centre,l.shell):l.centre:null}catch{return null}}(e,t,i,n,o,a,r,d);for(const e of s.fans)try{let t=[Dc(e)];if(l&&(t=ni(t,l)),!t?.length)continue;w=w?si(w,t):t}catch{}g=w,f=Math.max(d.epsilon,1e-9)**2,w=Array.isArray(g)?g.map(e=>{if(!Array.isArray(e)||!e.length)return e;const[t,...i]=e;return Math.abs(nc(t||[]))<=f?null:[t,...i.filter(e=>Math.abs(nc(e||[]))>f)]}).filter(e=>!!e):g}const $=w||[],S=[...Pc($)?[{id:"room-primary",geom:$}]:[],...k.map((e,t)=>({id:`room-isolated-${t}`,geom:e.geom}))];m="openings";for(const e of s){if(!(e.length>0))continue;const t=Xc(_,e,!0);if(!t.negative&&!t.positive)continue;const i=e.angle*Math.PI/180,s=Math.cos(i),o=Math.sin(i),a=-o,l=s,c=e.length/2,h=1.25*Math.max(p,n*r),d=[[e.x-s*c-a*h,e.y-o*c-l*h],[e.x+s*c-a*h,e.y+o*c-l*h],[e.x+s*c+a*h,e.y+o*c+l*h],[e.x-s*c+a*h,e.y-o*c+l*h]];w&&(w=oi(w,Dc(d)));for(const e of k)e.geom=oi(e.geom,Dc(d))}m="extras";const M=[];let C=0;const D=c.mergeExtra||((e,t)=>e?si(e,t):t);for(let e=0;ee.length>=2&&Number.isFinite(e[0])&&Number.isFinite(e[1]))||Math.abs(Ti(t))<=1e-9){C++;continue}const i=[Dc(t)];try{const t=D(w,i,e);if(!Pc(t))throw new Error("invalid extra union");w=t}catch{C++,Pc(i)&&M.push({id:`extra-${e}`,geom:i})}}M.push(...k),M.sort((e,t)=>Lc(e.geom).localeCompare(Lc(t.geom)));const P=w||[];return{status:C||x?"degraded-extra":"ok",geom:P,components:[...Pc(P)?[{id:"primary",geom:P}]:[],...M.map((e,t)=>({...e,id:`isolated-${t}`}))],roomGeom:$,roomComponents:S,paperGeom:b,depthUnits:p,openingPadUnits:1.25*Math.max(p,n*r),openingIndex:_,multiWallNodes:d,degradedExtraCount:C+x}}catch{return c.onCoreFailure?.(m),{status:"failed-core",geom:[],components:[],roomGeom:[],paperGeom:[],roomComponents:[],depthUnits:p,openingIndex:null,multiWallNodes:d,degradedExtraCount:0}}var g,f}function Wc(e,t,i,s=[],n,o,a,r=1,l=[],c={}){if(!t?.length&&!l.length)return null;return Uc(qc(e,t,i,s,n,o,a,r,l,c))}function Uc(e){if("failed-core"===e.status||"not-applicable"===e.status)return null;const t=e.components.map(e=>({id:e.id,d:Lc(e.geom),fillRule:"evenodd"})).filter(e=>!!e.d),i=t[0]?.d||"",s=Lc(e.paperGeom);if(t.length){const n={status:e.status,d:i,paths:t,components:e.components,roomGeom:e.roomGeom,multiWallNodes:e.multiWallNodes,paperD:s,depthUnits:e.depthUnits,fillRule:"evenodd"};return Object.defineProperties(n,{roomComponents:{value:e.roomComponents||[],enumerable:!1},openingIndex:{value:e.openingIndex,enumerable:!1},openingPadUnits:{value:e.openingPadUnits,enumerable:!1}}),n}return null}function Gc(e,t,i,s,n,o,a=1){if(!t?.length)return[];const r=new Set,l=[];for(const c of e||[]){if(!c?.id)continue;const h=yc(e,c.id,t,i,s,n,o,a);if(!h)continue;const d=h.poly;for(let e=0;e0))continue;const u=Rl(t,i,s,a);if(r.has(u))continue;r.add(u);const _=Ml(p,n,o),[m,g]=oc(d,e),f=-m,v=-g,b=_/2,y=[[t[0]+f*b,t[1]+v*b],[i[0]+f*b,i[1]+v*b],[i[0]+m*b,i[1]+g*b],[t[0]+m*b,t[1]+g*b]];l.push({key:u,kind:c,cm:p,quad:y,a:[t[0],t[1]],b:[i[0],i[1]],depthUnits:_})}}return l}function Vc(e,t,i){const s=e?.length||0;if(s<3||t.length!==s)return null;if(t.every(e=>!(e>0)))return e.map(e=>[e[0],e[1]]);hc(e),t.slice().reverse();const n=[];for(let o=0;o0||l>0)){n.push([e[o][0],e[o][1]]);continue}if(r>0!=l>0){const t=e[o],i=r>0?[t[0]-c*r,t[1]-h*r]:[t[0],t[1]],s=l>0?[t[0]-d*l,t[1]-p*l]:[t[0],t[1]];n.push(i),Math.hypot(s[0]-i[0],s[1]-i[1])>1e-9&&n.push(s);continue}if(ac(w,k)){const t=e[o],i=[t[0]-c*r,t[1]-h*r],s=[t[0]-d*l,t[1]-p*l];n.push(i),Math.hypot(s[0]-i[0],s[1]-i[1])>1e-9&&n.push(s);continue}const S=rc(x,w,$,k),M=Math.max(r,l,1e-9),C=vc(i,e[o])?.limit??4*M;if(S){const i=Math.hypot(S[0]-e[o][0],S[1]-e[o][1]);if(Number.isFinite(i)&&i<=C){n.push(S);continue}if(jc(e,t,o)){n.push([e[o][0],e[o][1]]);continue}}r>0&&n.push([e[o][0]-c*r,e[o][1]-h*r]),l>0&&n.push([e[o][0]-d*l,e[o][1]-p*l])}return n.length>=3?n:null}function Kc(e,t,i,s,n,o,a=1){const r=[];for(const l of e||[]){if(!l?.id)continue;const c=yc(e,l.id,t,i,s,n,o,a);if(!c)continue;const h=Math.abs(Ti(c.poly));for(let e=0;e[Math.max(t,e.x0),Math.min(i,e.x1)]).filter(e=>e[1]-e[0]>s).sort((e,t)=>e[0]-t[0]||e[1]-t[1]);if(!n.length)return{coverage:0,full:!1};let o=n[0][0],a=n[0][1],r=0,l=o<=t+s;for(let e=1;e=i-s,{coverage:r,full:l}}function Jc(e,t){return Number(t.full)-Number(e.full)||e.faceDistance-t.faceDistance||e.area-t.area||e.roomId.localeCompare(t.roomId)}function Xc(e,t,i=!1){const s=Number(t?.x),n=Number(t?.y),o=Number(t?.angle),a=Number(t?.length);if(!([s,n,o,a].every(Number.isFinite)&&a>0))return{negative:null,positive:null};const r=o*Math.PI/180,l=Math.cos(r),c=Math.sin(r),h=-c,d=l,p=a/2,u=Math.max(1e-9,e.adjacencyEps),_=new Map;let m=0;for(const t of e.edges){if(i&&!(t.half>0))continue;if(!Bl(t.a,t.b,o))continue;const[e,a]=Tl(t.a,t.b);if(Math.abs((s-t.a[0])*a-(n-t.a[1])*e)>u)continue;const r=(t.a[0]-s)*l+(t.a[1]-n)*c,g=(t.b[0]-s)*l+(t.b[1]-n)*c,f=Math.max(-p,Math.min(r,g)),v=Math.min(p,Math.max(r,g));if(v-f<=u)continue;const b=t.inward[0]*h+t.inward[1]*d>=0?1:-1,y=((t.a[0]+t.b[0])/2-s)*h+((t.a[1]+t.b[1])/2-n)*d,w=Math.abs(y+b*t.half),k=`${b}|${t.roomId}`,x={x0:f,x1:v,half:t.half,cm:t.cm,key:t.key,axis:[e,a]},$=_.get(k);$?($.pieces.push(x),$.faceDistance=Math.min($.faceDistance,w)):_.set(k,{roomId:t.roomId,side:b,order:m++,pieces:[x],faceDistance:w,area:t.area,coverage:0,full:!1})}for(const e of _.values()){const t=Yc(e.pieces,-p,p,u);e.coverage=t.coverage,e.full=t.full}const g=e=>{const t=[..._.values()].filter(t=>t.side===e&&t.coverage>u);return t.sort(Jc),t[0]||null};return{negative:g(-1),positive:g(1)}}function Zc(e,t){const i=Xc(e,t),s=[i.negative,i.positive].filter(e=>!!e);if(!s.length)return{ox:0,oy:0,cm:0,side:-1};const n=i.negative&&i.positive?-1:s[0].side,o=t.flip_v?-n:n,a=(-1===o?i.negative:i.positive)||s[0],r=[...a.pieces].sort((e,t)=>(e.x0<=0&&e.x1>=0?0:Math.min(Math.abs(e.x0),Math.abs(e.x1)))-(t.x0<=0&&t.x1>=0?0:Math.min(Math.abs(t.x0),Math.abs(t.x1)))||t.x1-t.x0-(e.x1-e.x0)||e.key.localeCompare(t.key))[0];if(!(r.half>0&&r.cm>0))return{ox:0,oy:0,cm:0,side:o};const l=t.angle*Math.PI/180,c=-Math.sin(l),h=Math.cos(l);return{ox:c*o*r.half,oy:h*o*r.half,cm:r.cm,side:o}}function Qc(e,t){const i=1e-9,s=t.filter(e=>Number.isFinite(e.x0)&&Number.isFinite(e.x1)&&Number.isFinite(e.half)&&e.x1>e.x0&&e.half>0);if(!s.length)return"";const n=s.flatMap(e=>[e.x0,e.x1]).sort((e,t)=>e-t),o=[];for(const e of n){const t=o[o.length-1];(void 0===t||e>t+i)&&o.push(e)}const a=[];for(let e=0;e+1t+i))continue;const r=(t+n)/2,l=s.reduce((e,t)=>r>=t.x0-i&&r<=t.x1+i?Math.max(e,t.half):e,0);if(!(l>0))continue;const c=a[a.length-1];c&&t<=c.x1+i&&Math.abs(l-c.half)<=i?c.x1=n:a.push({x0:t,x1:n,half:l})}const r=[];for(const e of a){const t=r[r.length-1],s=t?.[t.length-1];s&&e.x0<=s.x1+i?(e.x0=s.x1,t.push(e)):r.push([e])}return r.map(t=>{const i=t[0],s=t[t.length-1],n=Math.min(.25*Math.min(...t.map(e=>e.half)),.75),o=-e*n,a=[];if(1===e){a.push(`M ${i.x0} ${o} L ${s.x1} ${o}`);for(let e=t.length-1;e>=0;e--){const i=t[e];a.push(`L ${i.x1} ${i.half} L ${i.x0} ${i.half}`)}}else{a.push(`M ${s.x1} ${o} L ${i.x0} ${o}`);for(const e of t)a.push(`L ${e.x0} ${-e.half} L ${e.x1} ${-e.half}`)}return a.push("Z"),a.join(" ")}).join(" ")}function eh(e,t,i,s){if(!s)return i;const n=e.angle*Math.PI/180,o=Math.cos(n),a=Math.sin(n),r=[],l=1e-9;for(const n of i){const[i,c]=n.axis,h=o*i+a*c;if(Math.abs(h)<=l)continue;const d=e.x*i+e.y*c,p=d+h*n.x0,u=d+h*n.x1,_=Math.min(p,u),m=Math.max(p,u),g=`${n.key}|${t}`,f=s.get(g)||[];let v=[[_,m]];for(const[e,t]of f){const i=[];for(const[s,n]of v)t<=s+l||e>=n-l?i.push([s,n]):(e>s+l&&i.push([s,Math.min(n,e)]),te[0]-t[0]||e[1]-t[1]),y=[];for(const e of b){const t=y[y.length-1];t&&e[0]<=t[1]+l?t[1]=Math.max(t[1],e[1]):y.push([e[0],e[1]])}s.set(g,y)}return r}function th(e,t){const i=new Map;return t.map(t=>function(e,t,i){const s=Xc(e,t,!0),n=s.negative,o=s.positive;if(!n&&!o)return null;let a;if(n&&o)a=[{candidate:n,side:-1},{candidate:o,side:1}];else{const e=n||o;a=[{candidate:e,side:-1},{candidate:e,side:1}]}const r=a.map(({candidate:e,side:s})=>({candidate:e,side:s,pieces:eh(t,s,e.pieces,i)})),l=r.map(({candidate:e,side:t,pieces:i})=>({side:t,roomId:e.roomId,d:Qc(t,i)})),c=r.flatMap(({pieces:e})=>e);if(!c.length)return null;const h=Math.max(...c.map(e=>e.half));return{faces:l,minY:-h,maxY:h,wallKey:[...new Set(c.map(e=>e.key))].sort().join("|")}}(e,t,i))}const ih=150,sh=1e-6;function nh(e){return Number.isFinite(e)?Math.max(1,Math.min(ih,e)):1}function oh(e){return((Number.isFinite(Number(e))?Number(e):0)%90+90)%90}const ah=e=>{const t=e.map(e=>[e[0],e[1]]);return!t.length||t[0][0]===t[t.length-1][0]&&t[0][1]===t[t.length-1][1]||t.push([...t[0]]),[t]},rh=(e,t)=>e[0]===t[0]&&e[1]===t[1];function lh(e,t=1e-6){const i=Number.isFinite(t)&&t>0?t:sh,s=[];for(const t of e||[]){if(!Array.isArray(t)||t.length<2)return null;const e=Number(t[0]),n=Number(t[1]);if(!Number.isFinite(e)||!Number.isFinite(n))return null;const o=Math.round(e/i)*i,a=Math.round(n/i)*i;if(!Number.isFinite(o)||!Number.isFinite(a))return null;const r=[Object.is(o,-0)?0:o,Object.is(a,-0)?0:a];s.length&&rh(s[s.length-1],r)||s.push(r)}return s.length>1&&rh(s[0],s[s.length-1])&&s.pop(),s.length<3||new Set(s.map(e=>`${e[0]},${e[1]}`)).size<3?null:Ti(s)>i*i?s:null}function ch(e){const t=[];for(const i of e||[])for(const e of i||[]){const i=(e||[]).filter(e=>Array.isArray(e)&&e.length>=2);i.length<4||t.push(`M ${i.slice(0,-1).map(e=>`${e[0]} ${e[1]}`).join(" L ")} Z`)}return t.join(" ")}function hh(e,t,i,s,n){return Kl({a:e,b:t,halfDepth:Ml(i,s,n)/2})}function dh(e,t,i){const s=Number(t)>0?Number(t):5,n=nh(e.cm)/s*i,o=e.center[0],a=e.center[1];if("circle"===e.shape){const e=n/2;return Array.from({length:96},(t,i)=>{const s=i/96*Math.PI*2;return[o+Math.cos(s)*e,a+Math.sin(s)*e]})}const r=n/2,l=oh(e.angle)*Math.PI/180,c=Math.cos(l),h=Math.sin(l);return[[-r,-r],[r,-r],[r,r],[-r,r]].map(([e,t])=>[o+e*c-t*h,a+e*h+t*c])}function ph(e,t,i=1e-9){if(!t.length)return[e];let s=[ah(e)];try{for(const e of t){const t=e.b[0]-e.a[0],n=e.b[1]-e.a[1],o=Math.hypot(t,n);if(!(o>i))continue;const a=t/o,r=n/o,l=-r,c=a,h=1.25*Math.max(Number(e.depth)||0,4*i),d=Math.max(2*i,1e-9*o),p=[[e.a[0]-a*d-l*h,e.a[1]-r*d-c*h],[e.b[0]+a*d-l*h,e.b[1]+r*d-c*h],[e.b[0]+a*d+l*h,e.b[1]+r*d+c*h],[e.a[0]-a*d+l*h,e.a[1]-r*d+c*h]];s=oi(s,ah(p))}return kh(s)}catch{return[e]}}function uh(e,t){try{const i=oi([[...e.map(e=>[e[0],e[1]]),[e[0][0],e[0][1]]]],[[...t.map(e=>[e[0],e[1]]),[t[0][0],t[0][1]]]]);let s=null,n=0;for(const e of i||[]){const t=e?.[0]||[],i=Math.abs(Ti(t));t.length>=4&&i>n&&(n=i,s=t.slice(0,-1).map(e=>[e[0],e[1]]))}return s}catch{return null}}function _h(e,t,i,s=Math.max(2e-4*i,1e-9),n=[]){const o=[],a=[],r=new Map;for(const e of n){const t=r.get(e.hostId)||[];t.push(e),r.set(e.hostId,t)}const l=[],c=[],h=[];for(const s of e.room_drafts||[])for(let e=0;e+10))continue;const r={a:s.points[e],b:s.points[e+1],halfDepth:a},c=Kl(r);c&&(o.push(r),l.push(c))}const d=[];for(const s of e.partitions||[]){const e={a:s.a,b:s.b,halfDepth:Ml(s.cm,t,i)/2};if(!(e.halfDepth>0))continue;const n=Kl(e);n&&(a.push(e),c.push(n),d.push({id:s.id,body:n}))}const p=[...o,...a];for(const{segmentIndex:e,wedge:t}of Ql(p,s)){const i=e=o.length&&(d[i.at].body=s))}for(const e of d)h.push(...ph(e.body,r.get(e.id)||[],s));const u=(e.wall_columns||[]).map(e=>dh(e,t,i)),_=ec([...o,...a],s).flatMap(e=>ph(e,n,s));return{drafts:l,partitions:c,columns:u,patches:_,all:[...l,...h,..._,...u]}}function mh(e){try{const t=e.map(e=>lh(e)).filter(e=>!!e).map(e=>ah(e));return t.length?si(t[0],...t.slice(1)):null}catch{return null}}const gh=e=>`M ${e.map(e=>`${e[0]} ${e[1]}`).join(" L ")} Z`;function fh(e){const t=[];for(const i of e||[]){if(yh([i])<=1e-6)continue;const e=[];for(const t of i||[]){const i=(t||[]).filter(e=>Array.isArray(e)&&e.length>=2);i.length<4||e.push(gh(i.slice(0,-1)))}e.length&&t.push(e.join(" "))}return t}function vh(e,t,i={}){const s=mh(e.filter(e=>e.length>=3)),n=t.some(e=>!lh(e))?null:mh(t);if(!s)return[];if(n)try{return fh(ni(s,n))}catch{}return function(e,t,i){let s=null;for(let n=0;n=4&&t.push(e.slice(0,-1).map(e=>[e[0],e[1]]));return t}function kh(e){const t=[];for(const i of e||[]){const e=i?.[0];e?.length>=4&&t.push(e.slice(0,-1).map(e=>[e[0],e[1]]))}return t}function xh(e,t){let i=!1;for(let s=0,n=t.length-1;se[1]!=l>e[1]&&e[0]<(r-o)*(e[1]-a)/(l-a||1e-12)+o&&(i=!i)}return i}function $h(e,t,i){return function(e,t){for(const i of t||[]){const t=i?.[0];if(!t?.length||!xh(e,t))continue;let s=!1;for(let t=1;txh(e,t))}function Sh(e,t,i){if(Math.hypot(e.center[0]-t.center[0],e.center[1]-t.center[1])>i)return!1;if(Math.abs(nh(e.cm)-nh(t.cm))>1e-6)return!1;if(e.shape!==t.shape)return!0;if("circle"===e.shape||"circle"===t.shape)return!0;const s=Math.abs(oh(e.angle)-oh(t.angle));return Math.min(s,90-s)<=1e-6}const Mh=2.54;function Ch(e){const t="number"==typeof e?e:NaN;return!Number.isFinite(t)||t<=0||5===t?1:5/t}function Dh(e,t){return e*Ch(t)}function Ph(e){return e?Mh:1}function Th(e,t){if(!t)return String(e);const i=e/Mh;return String(Math.round(1e6*i)/1e6)}const Ih=1e3;function Rh(e,t){const i=Number(e),s=Number.isFinite(i)&&i>0?i:1,n=s>=1?t:t*s,o=s>=1?t/s:t;return{x:(t-n)/2,y:(t-o)/2,w:n,h:o}}const Ah=.01,zh=100;function Fh(e,t=1e3){const i=Rh(e?.plan_aspect,t),s=Number(e?.plan_scale),n=Number.isFinite(s)&&s>0?Math.min(zh,Math.max(Ah,s)):1,o=Number(e?.plan_scale_x),a=Number(e?.plan_scale_y),r=Number.isFinite(o)&&o>0?Math.min(zh,Math.max(Ah,o)):n,l=Number.isFinite(a)&&a>0?Math.min(zh,Math.max(Ah,a)):n,c=Number(e?.plan_x),h=Number(e?.plan_y),d=qr(e?.plan_angle);return{x:i.x+(Number.isFinite(c)?Gh(c):0)*t,y:i.y+(Number.isFinite(h)?Gh(h):0)*t,w:i.w*r,h:i.h*l,...d?{angle:d}:{}}}function Nh(e){if(null==e.x||null==e.y)return{x:e.x,y:e.y,w:e.w,h:e.h};const t=Number(e.w)||0,i=Number(e.h)||0;return{x:t<0?e.x+t:e.x,y:i<0?e.y+i:e.y,w:Math.abs(t),h:Math.abs(i)}}function Eh(e){return e&&Array.isArray(e.spaces)?e.spaces.map(e=>{const t=Ih,i=function(e){return Array.isArray(e)&&4===e.length&&e.every(e=>Number.isFinite(e))&&e[2]>1e-6&&e[3]>1e-6?e:[0,0,1,1]}(e.view_box);return{id:e.id,title:e.title,cellCm:Number.isFinite(Number(e.cell_cm))&&Number(e.cell_cm)>0?Number(e.cell_cm):5,vb:[i[0]*Ih,i[1]*t,i[2]*Ih,i[3]*t],bg:e.plan_url?{href:As(e.plan_url),...Fh(e,Ih)}:null,rooms:(e.rooms||[]).map(e=>{const i={...e,...Nh(e)};return{id:i.id,name:i.name,area:i.area??null,open_to:i.open_to||void 0,settings:i.settings||void 0,x:null!=i.x?i.x*Ih:void 0,y:null!=i.y?i.y*t:void 0,w:null!=i.w?i.w*Ih:void 0,h:null!=i.h?i.h*t:void 0,poly:i.poly?i.poly.map(e=>[e[0]*Ih,e[1]*t]):void 0,wall_ids:Array.isArray(i.wall_ids)?[...i.wall_ids]:void 0}}),wall_segments:(e.wall_segments||[]).map(e=>({...e,id:String(e.id),a:[Number(e.a?.[0])*Ih,Number(e.a?.[1])*t],b:[Number(e.b?.[0])*Ih,Number(e.b?.[1])*t],cm:Number(e.cm)})),room_drafts:(e.room_drafts||[]).map(e=>({id:e.id,points:(e.points||[]).map(e=>[e[0]*Ih,e[1]*t]),segments:(e.segments||[]).map(e=>({..."string"==typeof e.id&&e.id?{id:e.id}:{},cm:Number(e.cm)}))})),partitions:(e.partitions||[]).map(e=>({id:e.id,a:[e.a[0]*Ih,e.a[1]*t],b:[e.b[0]*Ih,e.b[1]*t],cm:Number(e.cm)})),wall_columns:(e.wall_columns||[]).map(e=>({id:e.id,shape:"circle"===e.shape?"circle":"square",center:[e.center[0]*Ih,e.center[1]*t],cm:Number(e.cm),..."circle"===e.shape?{}:{angle:oh(e.angle)}}))}}):[]}const Oh=5e3,Hh=Oh*Ih,Lh=240,Bh=Ih/Lh,jh=1/Lh;function qh(e){if(!Number.isFinite(e))return e;const t=Math.round(e*Lh/Ih)*Ih/Lh;return Math.abs(t-e)<=1e-9*Bh?e:t}function Wh(e){return{x:qh(e.x),y:qh(e.y)}}function Uh(e){return Number.isFinite(e)?Math.min(Hh,Math.max(-Hh,e)):0}function Gh(e){return Number.isFinite(e)?Math.min(Oh,Math.max(-Oh,e)):0}const Vh=1/3,Kh=200;function Yh(e){let t=1/0,i=1/0,s=-1/0,n=-1/0;for(const o of e){const e=Number(o[0]),a=Number(o[1]);Number.isFinite(e)&&Number.isFinite(a)&&(es&&(s=e),a>n&&(n=a))}return t>s?null:{minX:t,minY:i,maxX:s,maxY:n}}function Jh(e){return e.poly&&e.poly.length?Yh(e.poly):null==e.x||null==e.y?null:Yh([[e.x,e.y],[e.x+(e.w||0),e.y+(e.h||0)]])}function Xh(e,t){const i=[];for(const t of e.rooms||[]){const e=Jh(t);e&&i.push(e)}if(e.bg){const t=Yh(Yr(e.bg));t&&i.push(t)}for(const e of t||[])if(Array.isArray(e)){const t=Yh([e]);t&&i.push(t)}else i.push(e);return i}const Zh=e=>{if(!e.length)return 0;const t=[...e].sort((e,t)=>e-t),i=t.length>>1;return t.length%2?t[i]:(t[i-1]+t[i])/2},Qh=e=>{let t=1/0,i=1/0,s=-1/0,n=-1/0;for(const o of e)o.minXs&&(s=o.maxX),o.maxY>n&&(n=o.maxY);return t>s||i>n?null:{x:t,y:i,w:s-t,h:n-i}};function ed(e,t){let{x:i,y:s,w:n,h:o}=e;n<30&&(i=i+n/2-100,n=Kh),o<30&&(s=s+o/2-100,o=Kh);const a=Math.max(n,o)*t;return{x:i-a,y:s-a,w:n+2*a,h:o+2*a}}function td(e,t={}){const i=t.pad??.05,s=t.k??10,n=t.minSpread??50,o=e.filter(e=>Number.isFinite(e.minX)&&Number.isFinite(e.minY)&&Number.isFinite(e.maxX)&&Number.isFinite(e.maxY)&&Math.abs(e.minX)<=Hh&&Math.abs(e.maxX)<=Hh&&Math.abs(e.minY)<=Hh&&Math.abs(e.maxY)<=Hh);if(!o.length)return{core:null,all:null,outliers:0};const a=Qh(o);if(o.length<4){const e=ed(a,i);return{core:e,all:e,outliers:0}}const r=o.map(e=>(e.minX+e.maxX)/2),l=o.map(e=>(e.minY+e.maxY)/2),c=Zh(r),h=Zh(l),d=o.map((e,t)=>Math.max(Math.abs(r[t]-c),Math.abs(l[t]-h))),p=Math.max(((e,t)=>e.length?e[Math.min(e.length-1,Math.max(0,Math.round(t*(e.length-1))))]:0)([...d].sort((e,t)=>e-t),.75),n),u=d.map(e=>e>s*p),_=u.filter(Boolean).length,m=_&&_<=o.length*Vh?o.filter((e,t)=>!u[t]):o;return{core:ed(Qh(m)||a,i),all:ed(a,i),outliers:m===o?0:_}}function id(e,t,i=.05){const s=td(Xh(e,t),{pad:i});if(s.core)return s.core;const n=e.vb&&4===e.vb.length&&e.vb[2]>0&&e.vb[3]>0?e.vb:[0,0,Ih,Ih];return{x:n[0],y:n[1],w:n[2],h:n[3]}}function sd(e){const t=id(e);return{x:t.x+t.w/2,y:t.y+t.h/2}}function nd(e){const t=[];for(const i of e.rooms||[]){const e=Jh(i);e&&t.push(e)}const i=td(t,{pad:0}).core,s=Ih*Ch(e.cellCm??5);return i?Math.max(s,Math.min(Hh,Math.max(i.w,i.h))):s}function od(e,t,i,s=1){const n=Number(i),o=Number.isFinite(s)&&s>0?s:1;return!Number.isFinite(n)||n<=0?e*o:e*nd(t)*o/n}const ad=[1,2,5,10,20,50,100,200,500,1e3];function rd(e){if(e.poly&&e.poly.length){const t=e.poly.map(e=>e[0]),i=e.poly.map(e=>e[1]),s=Math.min(...t),n=Math.min(...i);return{x:s,y:n,w:Math.max(...t)-s,h:Math.max(...i)-n}}return{x:e.x??0,y:e.y??0,w:e.w??0,h:e.h??0}}function ld(e,t,i,s){const n=i["rl_"+(e.id||"")];if(n&&n.s===t)return{x:n.x*Ih,y:n.y*Ih};const o=function(e){if(e.poly){const t=e.poly.length;return[e.poly.reduce((e,t)=>e+t[0],0)/t,e.poly.reduce((e,t)=>e+t[1],0)/t]}return[e.x+e.w/2,e.y+.1*Math.min(e.w,e.h)]}(e);return Wh({x:o[0],y:o[1]})}const cd=["furniture","appliance","sanitary","other"],hd=[{id:"sofa",group:"furniture",w:220,h:90,g:[["r",0,0,1,1],["l",.09,.26,.91,.26],["l",.09,.26,.09,1],["l",.91,.26,.91,1],["l",.5,.26,.5,1]]},{id:"armchair",group:"furniture",w:90,h:85,g:[["r",0,0,1,1],["l",.14,.28,.86,.28],["l",.14,.28,.14,1],["l",.86,.28,.86,1]]},{id:"coffee_table",group:"furniture",w:110,h:60,g:[["r",0,0,1,1],["r",.08,.14,.84,.72]]},{id:"table_dining",group:"furniture",w:140,h:80,g:[["r",0,0,1,1],["r",.06,.11,.88,.78]]},{id:"table_round",group:"furniture",w:120,h:120,g:[["e",.5,.5,.5,.5],["e",.5,.5,.41,.41]]},{id:"chair",group:"furniture",w:45,h:45,g:[["r",0,0,1,.18],["r",.06,.18,.88,.8]]},{id:"desk",group:"furniture",w:120,h:60,g:[["r",0,0,1,1],["r",.63,.07,.31,.86],["l",.63,.5,.94,.5]]},{id:"bed_double",group:"furniture",w:160,h:200,g:[["r",0,0,1,1],["r",0,0,1,.07],["r",.06,.1,.4,.15],["r",.54,.1,.4,.15],["l",0,.33,1,.33]]},{id:"bed_single",group:"furniture",w:90,h:200,g:[["r",0,0,1,1],["r",0,0,1,.07],["r",.15,.1,.7,.15],["l",0,.33,1,.33]]},{id:"nightstand",group:"furniture",w:45,h:40,g:[["r",0,0,1,1],["r",.12,.14,.76,.33],["r",.12,.53,.76,.33]]},{id:"wardrobe",group:"furniture",w:100,h:60,g:[["r",0,0,1,1],["l",0,.72,1,.72],["l",.5,.72,.5,1]]},{id:"bookshelf",group:"furniture",w:80,h:30,g:[["r",0,0,1,1],["l",.34,0,.34,1],["l",.67,0,.67,1]]},{id:"fridge",group:"appliance",w:60,h:65,g:[["r",0,0,1,1],["l",0,.36,1,.36],["l",.83,.44,.83,.64]]},{id:"stove",group:"appliance",w:60,h:60,g:[["r",0,0,1,1],["e",.29,.31,.15,.15],["e",.71,.31,.15,.15],["e",.29,.71,.15,.15],["e",.71,.71,.15,.15]]},{id:"dishwasher",group:"appliance",w:60,h:60,g:[["r",0,0,1,1],["r",.1,.12,.8,.76],["e",.5,.5,.27,.27],["e",.5,.5,.13,.13]]},{id:"washer",group:"appliance",w:60,h:60,g:[["r",0,0,1,1],["l",.08,.17,.92,.17],["e",.5,.57,.3,.3],["e",.5,.57,.14,.14]]},{id:"dryer",group:"appliance",w:60,h:60,g:[["r",0,0,1,1],["l",.08,.17,.92,.17],["e",.5,.57,.3,.3],["p",.36,.5,.5,.64,.64,.5]]},{id:"tv",group:"appliance",w:120,h:30,g:[["r",0,0,1,.42],["l",.5,.42,.5,.72],["l",.3,.72,.7,.72]]},{id:"ac",group:"appliance",w:90,h:25,g:[["r",0,0,1,1],["l",.05,.55,.95,.55],["l",.05,.79,.95,.79]]},{id:"water_heater",group:"appliance",w:45,h:45,g:[["e",.5,.5,.5,.5],["e",.5,.5,.31,.31]]},{id:"toilet",group:"sanitary",w:40,h:70,g:[["r",.06,0,.88,.2],["e",.5,.58,.37,.35],["e",.5,.58,.22,.2]]},{id:"bathtub",group:"sanitary",w:170,h:75,g:[["r",0,0,1,1],["r",.05,.11,.77,.78],["e",.89,.5,.045,.1]]},{id:"shower",group:"sanitary",w:90,h:90,g:[["r",0,0,1,1],["l",0,0,1,1],["l",1,0,0,1],["e",.5,.5,.08,.08]]},{id:"sink",group:"sanitary",w:60,h:45,g:[["r",0,0,1,1],["e",.5,.6,.34,.3],["e",.5,.15,.07,.07]]},{id:"kitchen_sink",group:"sanitary",w:80,h:60,g:[["r",0,0,1,1],["r",.06,.24,.44,.64],["r",.54,.24,.4,.64],["e",.5,.12,.06,.06]]},{id:"bidet",group:"sanitary",w:40,h:55,g:[["e",.5,.5,.44,.5],["e",.5,.5,.26,.3]]},{id:"stairs",group:"other",w:100,h:280,g:[["r",0,0,1,1],["l",0,.111,1,.111],["l",0,.222,1,.222],["l",0,.333,1,.333],["l",0,.444,1,.444],["l",0,.556,1,.556],["l",0,.667,1,.667],["l",0,.778,1,.778],["l",0,.889,1,.889],["l",.5,.93,.5,.06],["p",.38,.16,.5,.06,.62,.16]]},{id:"fireplace",group:"other",w:120,h:40,g:[["r",0,0,1,1],["p",.22,1,.22,.42,.78,.42,.78,1]]},{id:"plant",group:"other",w:40,h:40,g:[["e",.5,.5,.22,.22],["l",.5,.28,.5,.02],["l",.5,.72,.5,.98],["l",.28,.5,.02,.5],["l",.72,.5,.98,.5],["l",.34,.34,.13,.13],["l",.66,.66,.87,.87],["l",.66,.34,.87,.13],["l",.34,.66,.13,.87]]},{id:"rug",group:"other",w:200,h:140,g:[["r",0,0,1,1],["r",.06,.09,.88,.82]]}],dd=new Map(hd.map(e=>[e.id,e]));function pd(e){return e&&dd.get(e)||null}function ud(e){return hd.filter(t=>t.group===e)}function _d(e,t,i=Bh,s=1e3){const n=Number(t)>0?Number(t):5;return(Number(e)||0)/n*i/s}const md=5e-4,gd=Oh;function fd(e){return Number.isFinite(e)?Math.max(md,Math.min(gd,e)):md}const vd=e=>{const t=Math.round(1e3*e)/1e3;return Object.is(t,-0)?"0":String(t)};function bd(e,t,i){const s=pd(e);if(!(s&&t>0&&i>0))return"";const n=e=>vd(e*t),o=e=>vd(e*i),a=[];for(const e of s.g)if("r"===e[0]){const[,t,i,s,r]=e;a.push(`M${n(t)} ${o(i)}H${n(t+s)}V${o(i+r)}H${n(t)}Z`)}else if("l"===e[0]){const[,t,i,s,r]=e;a.push(`M${n(t)} ${o(i)}L${n(s)} ${o(r)}`)}else if("e"===e[0]){const[,s,r,l,c]=e;a.push(`M${n(s-l)} ${o(r)}A${vd(l*t)} ${vd(c*i)} 0 0 1 ${n(s+l)} ${o(r)}A${vd(l*t)} ${vd(c*i)} 0 0 1 ${n(s-l)} ${o(r)}Z`)}else{const t=e.slice(1);if(t.length<4)continue;let i=`M${n(t[0])} ${o(t[1])}`;for(let e=2;e+1{let t=(e%360+360)%360;return t>180&&(t-=360),t};function wd(e,t,i,s,n,o=0){let a=null,r=n;for(const n of s){const[s,l,c,h]=n,d=c-s,p=h-l,u=d*d+p*p;if(!u)continue;const _=Math.sqrt(u);let m=((e-s)*d+(t-l)*p)/u;m=Math.max(0,Math.min(1,m));let g=s+m*d,f=l+m*p;const v=Math.hypot(e-g,t-f);if(!(v0){const e=Math.round(m*_/o)*o;g=s+e/_*d,f=l+e/_*p}a={cx:g+b*(i/2),cy:f+y*(i/2),angle:yd(180*Math.atan2(-b,y)/Math.PI),dist:v}}return a}const kd=1e-9,xd=1e7,$d=e=>{const t=Math.sign(e)*Math.floor(Math.abs(e)*xd+.5)/xd;return Object.is(t,-0)?0:t},Sd=e=>`${$d(e[0])},${$d(e[1])}`,Md=(e,t)=>Math.hypot(t[0]-e[0],t[1]-e[1]),Cd=(e,t)=>Math.atan2(t[1]-e[1],t[0]-e[0]),Dd=e=>Array.isArray(e)&&e.length>=2&&e.every(e=>Number.isFinite(e)),Pd=e=>(e||[]).filter(e=>Dd(e?.a)&&Dd(e?.b)&&Md(e.a,e.b)>kd);function Td(e,{minAngleDeg:t=15,maxValence:i=6}={}){const s=new Map;for(const t of Pd(e))for(const[e,i]of[[t.a,t.b],[t.b,t.a]]){const t=s.get(Sd(e))||[];t.push(Cd(e,i)),s.set(Sd(e),t)}const n=[];for(const[e,o]of s){if(o.length>i&&n.push({rule:"valence",subject:e,actual:o.length,limit:i}),o.length<2)continue;const s=[...o].sort((e,t)=>e-t);let a=1/0;for(let e=0;ekd&&i(180*Math.atan2(e.b[1]-e.a[1],e.b[0]-e.a[0])/Math.PI%180+180)%180,Rd=(e,t,i=1)=>{const s=Math.abs(Id(e)-Id(t));return Math.min(s,180-s)<=i},Ad=e=>{const t=new Map;for(const i of e)for(const e of[i.a,i.b]){const s=t.get(Sd(e));s?s.push(i):t.set(Sd(e),[i])}return t};function zd(e,t,i){const s=Pd(t),n=i??Ad(s),o=new Set([e]);let a=Md(e.a,e.b);const r=[e.a,e.b];for(;r.length;){const t=r.pop();for(const i of n.get(Sd(t))||[])o.has(i)||Rd(i,e)&&Number(i.cm||0)===Number(e.cm||0)&&(o.add(i),a+=Md(i.a,i.b),r.push(i.a,i.b))}return a}function Fd(e,t,i,{minLengthCm:s=20}={}){const n=[],o=Pd(e),a=Ad(o);for(const e of o){const r=zd(e,o,a)/i*(t||1),l=Math.max(s,Number(e.cm)>0?Number(e.cm):0);r{const s=i[0]-t[0],n=i[1]-t[1],o=s*s+n*n,a=o<=kd?0:Math.max(0,Math.min(1,((e[0]-t[0])*s+(e[1]-t[1])*n)/o));return Math.hypot(e[0]-(t[0]+s*a),e[1]-(t[1]+n*a))};function Ed(e,t,i,{minDistanceCm:s=5}={}){const n=Pd(e),o=new Map;for(const e of n)o.set(Sd(e.a),e.a),o.set(Sd(e.b),e.b);const a=((e,t,i)=>e/(t||1)*i)(s,t,i),r=a>kd?a:1,l=(e,t)=>`${Math.floor(e/r)},${Math.floor(t/r)}`,c=new Map;for(const[e,t]of o){const i=l(t[0],t[1]),s=c.get(i);s?s.push([e,t]):c.set(i,[[e,t]])}const h=new Map;for(const e of n){const t=Math.min(e.a[0],e.b[0])-a,i=Math.max(e.a[0],e.b[0])+a,s=Math.min(e.a[1],e.b[1])-a,n=Math.max(e.a[1],e.b[1])+a;for(let o=Math.floor(t/r);o<=Math.floor(i/r);o++)for(let t=Math.floor(s/r);t<=Math.floor(n/r);t++){const i=`${o},${t}`,s=h.get(i);s?s.push(e):h.set(i,[e])}}const d=[];for(const[e,n]of o){const o=Math.floor(n[0]/r),l=Math.floor(n[1]/r);for(let r=-1;r<=1;r++)for(let h=-1;h<=1;h++)for(const[p,u]of c.get(`${o+r},${l+h}`)||[]){if(e>=p)continue;const o=Md(n,u);o<=2e-7||o{const s=o[(i+1)%o.length];return e+(t[0]*s[1]-s[0]*t[1])},0))/2,r=(i||1)/s,l=a*r*r;return l0?i:1;return{a:[e[0]/s,e[1]/s],b:[t[0]/s,t[1]/s]}}function Ld(e,t){const i=t>0?t:1;return[e.a[0]*i,e.a[1]*i,e.b[0]*i,e.b[1]*i]}function Bd(e){return Array.isArray(e)&&e.length>=2&&Number.isFinite(Number(e[0]))&&Number.isFinite(Number(e[1]))}function jd(e){if(!Array.isArray(e))return[];const t=[];for(const i of e){if(!i||"object"!=typeof i)continue;const e=i;if(!Bd(e.a)||!Bd(e.b))continue;const s=[Number(e.a[0]),Number(e.a[1])],n=[Number(e.b[0]),Number(e.b[1])];Math.hypot(n[0]-s[0],n[1]-s[1])<.001||t.push({a:s,b:n})}return t}function qd(e,t,i,s,n=!0){const o=(e||[]).filter(e=>e?.id),a=jd(t);if(a.length)return function(e,t,i,s){const n=jd(e);if(!n.length)return[];const o=function(e,t){const i=[],s=(e||[]).filter(e=>e?.id);for(let e=0;e4*s||o>4*s)continue;const a=(e[0]-n)*p+(e[1]-l)*u,c=(e[2]-n)*p+(e[3]-l)*u,h=Math.max(0,Math.min(a,c)),m=Math.min(d,Math.max(a,c));if(m-he.lo-t.lo||e.hi-t.hi);const i=[];for(const e of t){const t=i[i.length-1];t&&e.lo<=t.hi+r?t.hi=Math.max(t.hi,e.hi):i.push({...e})}for(const t of i){const i=[n+p*t.lo,l+u*t.lo],s=[n+p*t.hi,l+u*t.hi];Math.hypot(s[0]-i[0],s[1]-i[1])i.pair===e&&Math.abs(i.ux*a-i.uy*o)<=1e-6&&Math.abs((t[0]-i.origin[0])*i.uy-(t[1]-i.origin[1])*i.ux)<=c&&Math.abs((t[2]-i.origin[0])*i.uy-(t[3]-i.origin[1])*i.ux)<=c);h||(h={pair:e,origin:[t[0],t[1]],ux:o,uy:a,ranges:[]},l.push(h));const d=(t[0]-h.origin[0])*h.ux+(t[1]-h.origin[1])*h.uy,p=(t[2]-h.origin[0])*h.ux+(t[3]-h.origin[1])*h.uy;h.ranges.push({lo:Math.min(d,p),hi:Math.max(d,p)})}const h=[];for(const e of l){e.ranges.sort((e,t)=>e.lo-t.lo||e.hi-t.hi);const t=[];for(const i of e.ranges){const e=t[t.length-1];e&&i.lo<=e.hi+r?e.hi=Math.max(e.hi,i.hi):t.push({...i})}for(const s of t){const t=[e.origin[0]+e.ux*s.lo,e.origin[1]+e.uy*s.lo],n=[e.origin[0]+e.ux*s.hi,e.origin[1]+e.uy*s.hi];Math.hypot(n[0]-t[0],n[1]-t[1])>=r&&h.push(Hd(t,n,i))}}return h}(a,e,i,s).map(e=>Ld(e,i));if(!n)return[];const r=[],l=(e,t)=>(e.open_to||[]).includes(t.id)||(t.open_to||[]).includes(e.id);for(let e=0;e{const t=Gd.get(e);if(!t)return;const i=Date.now();if("hidden"===e.visibilityState){t.hiddenAt||(t.hiddenAt=i);const e={kind:"hidden",token:t.token,at:i,hiddenFor:0,long:!1};for(const i of[...t.subscribers])i(e);return}const s=t.hiddenAt?Math.max(0,i-t.hiddenAt):0;t.hiddenAt=0,t.token++;const n={kind:"visible",token:t.token,at:i,hiddenFor:s,long:s>=Wd};for(const e of[...t.subscribers])e(n)},onPageShow:t=>{const i=Gd.get(e);if(!i)return;const s=Date.now();i.token++;const n={kind:"pageshow",token:i.token,at:s,hiddenFor:0,long:!!t.persisted,persisted:!!t.persisted};for(const e of[...i.subscribers])e(n)}},Gd.set(e,i),e.addEventListener("visibilitychange",i.onVisibility),e.defaultView?.addEventListener("pageshow",i.onPageShow)}return i.subscribers.add(t),()=>{const i=Gd.get(e);i&&(i.subscribers.delete(t),i.subscribers.size||(e.removeEventListener("visibilitychange",i.onVisibility),e.defaultView?.removeEventListener("pageshow",i.onPageShow),Gd.delete(e)))}}const Kd={now:()=>Date.now(),setTimeout:(e,t)=>window.setTimeout(e,t),clearTimeout:e=>window.clearTimeout(e),requestAnimationFrame:e=>window.requestAnimationFrame(e),cancelAnimationFrame:e=>window.cancelAnimationFrame(e)};class Yd{constructor(e,t=Kd){this.onChange=e,this.clock=t,this._state="steady",this._token=0,this._frameFingerprint="",this._hasCompleteFrame=!1,this._overlayPhase="none",this._recoveryReason=null,this._overlayTimer=0,this._overlayRaf=0,this._overlayOpaqueAt=0,this._barrierRafs=new Set,this._trace=[],this._disposed=!1}get state(){return this._state}get token(){return this._token}get frameFingerprint(){return this._frameFingerprint}get hasCompleteFrame(){return this._hasCompleteFrame}get overlayPhase(){return this._overlayPhase}get overlayVisible(){return"none"!==this._overlayPhase}get overlayBlocksInteraction(){return this.overlayVisible}get recoveryReason(){return this._recoveryReason}get trace(){return this._trace.map(e=>({...e,...e.stage?{stage:[...e.stage]}:{}}))}note(e,t={}){this.record(e,void 0,t)}record(e,t,i={}){this._trace.push({at:this.clock.now(),token:this._token,event:e,state:this._state,...t?{reason:t}:{},...i}),this._trace.length>80&&this._trace.splice(0,this._trace.length-80)}changed(){this._disposed||this.onChange()}adoptCompleteFrame(e){e&&(this._hasCompleteFrame=!0,this._frameFingerprint=e,this._state="steady",this._recoveryReason=null,this.clearOverlay(),this.record("frame-adopted"),this.changed())}markCompleteFrame(e){e&&(this._hasCompleteFrame=!0,this._frameFingerprint=e,this._state="steady",this._recoveryReason=null,this.clearOverlay(),this.record("frame-complete"),this.changed())}refreshCompleteFrame(e){!this._disposed&&this._hasCompleteFrame&&"steady"===this._state&&e&&e!==this._frameFingerprint&&(this._frameFingerprint=e,this.record("frame-refreshed"))}visibility(e){return"hidden"===e.kind?(this.record("visibility-hidden"),this._token):e.long||"visible"!==e.kind&&"pageshow"!==e.kind?this.beginCandidate("pageshow"===e.kind?"pageshow":"long-resume"):(this.record("pageshow"===e.kind?"pageshow-noop":"visibility-visible-quick"),this._token)}beginCandidate(e,t="plan"){return this._token++,this._recoveryReason=t,this._hasCompleteFrame?(this._state="connection"===t?"offline-stale":"holding",this.clearOverlay()):this.overlayVisible?this._state="overlay-visible":(this._state="overlay-pending",this.scheduleOverlay(t)),this.record("candidate-start",e),this.changed(),this._token}connectionLost(){return this.beginCandidate("connection-lost","connection")}candidateReady(e){return!this._disposed&&e===this._token&&(this._state="candidate-ready",this.record("candidate-ready"),this.changed(),!0)}async commitAfterPaint(e,t){if(this._disposed||e!==this._token)return!1;let i=!0;const s=(async()=>{for(await t.updateComplete();i&&!this._disposed&&e===this._token;)if(t.stageValid()&&t.assetsReady()){if(await this.nextFrame(),e!==this._token||!t.stageValid()||!t.assetsReady())continue;if(await this.nextFrame(),e===this._token&&t.stageValid()&&t.assetsReady())return!0}else await this.nextFrame();return!1})();let n=0;const o=new Promise(e=>{n=this.clock.setTimeout(()=>e({ready:!1,timedOut:!0}),Ud)}),a=await Promise.race([s.then(e=>({ready:e,timedOut:!1})),o]);return i=!1,this.clock.clearTimeout(n),!this._disposed&&e===this._token&&(!(!a.ready&&!a.timedOut)&&(a.ready?(this._hasCompleteFrame=!0,this._frameFingerprint=t.frameFingerprint(),this.record("paint-barrier"),this.finishOverlayAfterCommit(),!0):(this.record("paint-barrier-timeout"),this._hasCompleteFrame?(this._state="connection"===this._recoveryReason?"offline-stale":"steady",this.clearOverlay()):(this._state="recovery-error","opaque"!==this._overlayPhase&&this.showOverlayNow()),this.changed(),!1)))}retry(e=this._recoveryReason||"plan"){return this.beginCandidate("retry",e)}nextFrame(){return new Promise(e=>{const t=this.clock.requestAnimationFrame(()=>{this._barrierRafs.delete(t),e()});this._barrierRafs.add(t)})}scheduleOverlay(e){this.clearOverlayTimer(),this._recoveryReason=e,this._overlayTimer=this.clock.setTimeout(()=>{this._overlayTimer=0,this._hasCompleteFrame||"overlay-pending"!==this._state&&"candidate-ready"!==this._state||(this._state="overlay-visible",this._overlayPhase="entering",this.record("overlay-enter"),this.changed(),this._overlayRaf=this.clock.requestAnimationFrame(()=>{this._overlayRaf=0,"entering"===this._overlayPhase&&(this._overlayPhase="fading-in",this.changed(),this._overlayTimer=this.clock.setTimeout(()=>{this._overlayTimer=0,"fading-in"===this._overlayPhase&&(this._overlayPhase="opaque",this._overlayOpaqueAt=this.clock.now(),this.record("overlay-opaque"),this.changed())},150))}))},150)}showOverlayNow(){this.clearOverlayTimer(),this._overlayPhase="opaque",this._overlayOpaqueAt=this.clock.now(),this.record("overlay-error")}finishOverlayAfterCommit(){if(this.clearOverlayTimer(),"none"===this._overlayPhase||"entering"===this._overlayPhase||"fading-in"===this._overlayPhase)return this.clearOverlay(),this._state="steady",this._recoveryReason=null,this.record("candidate-committed"),void this.changed();const e=Math.max(0,this._overlayOpaqueAt+250-this.clock.now());this._overlayTimer=this.clock.setTimeout(()=>{this._overlayTimer=0,this._overlayPhase="leaving",this.record("overlay-leave"),this.changed(),this._overlayTimer=this.clock.setTimeout(()=>{this._overlayTimer=0,this._overlayPhase="none",this._state="steady",this._recoveryReason=null,this.record("candidate-committed"),this.changed()},180)},e)}clearOverlayTimer(){this._overlayTimer&&this.clock.clearTimeout(this._overlayTimer),this._overlayTimer=0}clearOverlay(){this.clearOverlayTimer(),this._overlayRaf&&this.clock.cancelAnimationFrame(this._overlayRaf),this._overlayRaf=0,this._overlayPhase="none",this._overlayOpaqueAt=0}dispose(){this._disposed=!0,this._token++,this.clearOverlay();for(const e of this._barrierRafs)this.clock.cancelAnimationFrame(e);this._barrierRafs.clear()}}function Jd(e){let t=2166136261,i=2654435769;const s=new WeakSet,n=e=>{for(let s=0;s>>0)+(i>>>2)}},o=e=>{if(null===e)return void n("n");const t=typeof e;if("string"===t)return void n(`s${e.length}:${e}`);if("number"===t)return void n(`d${Number.isNaN(e)?"NaN":String(e)}`);if("boolean"===t)return void n(e?"t":"f");if("undefined"===t)return void n("u");if("object"!==t)return void n(`${t}:${String(e)}`);const i=e;if(s.has(i))n("[cycle]");else{if(s.add(i),Array.isArray(e)){n("[");for(const t of e)o(t);return n("]"),void s.delete(i)}n("{");for(const t of Object.keys(e).sort())n(t),o(e[t]);n("}"),s.delete(i)}};return o(e),`${(t>>>0).toString(16).padStart(8,"0")}${(i>>>0).toString(16).padStart(8,"0")}`}function Xd(e){return Jd(e)}const Zd=new WeakMap;class Qd{constructor(e,t=()=>Date.now()){this.onUpdate=e,this.now=t,this.fallbackAuthority={},this.referenceOwner={},this.referenced=new Set,this.queued=new Set,this.disposed=!1,this.sharedUpdate=()=>{this.disposed||this.onUpdate()}}bind(e){const t=((e,t)=>{const i=e?.connection||e?.callWS||t;return!i||"object"!=typeof i&&"function"!=typeof i?t:i})(e,this.fallbackAuthority);return this.shared&&this.authority===t||(this.shared&&(this.shared.listeners.delete(this.sharedUpdate),this.shared.references.delete(this.referenceOwner)),this.authority=t,this.shared=(e=>{let t=Zd.get(e);return t||(t={signed:{},queued:new Set,inFlight:new Map,retry:new Map,listeners:new Set,settlers:new Set,references:new Map},Zd.set(e,t)),t})(t),this.disposed||(this.shared.listeners.add(this.sharedUpdate),this.shared.references.set(this.referenceOwner,new Set(this.referenced)))),this.shared}start(e,t){this.disposed=!1,this.referenced=new Set(t());const i=this.bind(e());i.listeners.add(this.sharedUpdate),i.references.set(this.referenceOwner,new Set(this.referenced)),this.stopTimer(),this.resignTimer=setInterval(()=>this.resign(e(),t()),288e5)}dispose(){if(this.disposed=!0,this.stopTimer(),clearTimeout(this.batchTimer),this.shared){this.shared.listeners.delete(this.sharedUpdate),this.shared.references.delete(this.referenceOwner);for(const e of this.queued)this.shared.queued.delete(e)}this.queued.clear()}invalidate(e){const t=this.bind(e);clearTimeout(this.batchTimer),this.queued.clear(),t.queued.clear(),t.inFlight.clear(),t.retry.clear(),t.signed={}}stopTimer(){void 0!==this.resignTimer&&clearInterval(this.resignTimer),this.resignTimer=void 0}display(e,t){const i=As(t);if(!i.startsWith("/api/houseplan/content/"))return i;const s=this.bind(e);this.referenced.add(i),s.references.set(this.referenceOwner,new Set(this.referenced));const n=s.signed[i],o=n?this.now()-n.at:1/0;return o{const t=[...this.queued];this.queued.clear();for(const e of t)i.queued.delete(e);this.sign(e,t)},30))}sign(e,t){if(!t.length||!e?.callWS)return;const i=this.bind(e);for(const s of function(e,t){const i=Math.max(1,Math.floor(t)),s=[];for(let t=0;t{const s=i.inFlight.get(e);return!(void 0!==s&&t-s<15e3)&&(i.inFlight.set(e,t),!0)});n.length&&e.callWS({type:"houseplan/content/sign",paths:n}).then(e=>{const t=this.now();let s=0,o=!1;for(const a of n){const n=e?.urls?.[a];if("string"==typeof n&&n){const e=i.signed[a];e?.loaded&&e.url!==n?(e.pending={url:n,at:t},this.preloadReplacement(i,a,n,t)):(i.signed[a]={url:n,at:t,loaded:e?.url===n&&!!e.loaded},o=!0),i.retry.delete(a),s++}else this.backOff(i,a)}if(s&&(this.trimShared(i),o))for(const e of[...i.listeners])e()}).catch(()=>{for(const e of n)this.backOff(i,e)}).finally(()=>{for(const e of n)i.inFlight.get(e)===t&&i.inFlight.delete(e);for(const e of[...i.settlers])e()})}}preloadReplacement(e,t,i,s){const n=()=>{const n=e.signed[t];if(n?.pending?.url===i){e.signed[t]={url:i,at:s,loaded:!0},e.retry.delete(t),this.trimShared(e);for(const t of[...e.listeners])t()}},o=()=>{const s=e.signed[t];s?.pending?.url===i&&(delete s.pending,this.backOff(e,t))};if("undefined"==typeof Image)return void n();const a=new Image;a.onload=()=>{("function"==typeof a.decode?a.decode():Promise.resolve()).then(n).catch(o)},a.onerror=o,a.src=i}preloadCurrentImage(e,t,i){if(i.loaded)return Promise.resolve(!0);if(i.preload)return i.preload;if("undefined"==typeof Image)return i.loaded=!0,Promise.resolve(!0);const s=i.url;return i.preload=new Promise(n=>{const o=new Image;let a=!1;const r=o=>{if(a)return;a=!0,clearTimeout(l);const r=e.signed[t];if(r===i&&delete r.preload,o&&r===i&&r.url===s){r.loaded=!0;for(const t of[...e.listeners])t();n(!0)}else o||r!==i||this.backOff(e,t),n(!1)};o.onload=()=>{("function"==typeof o.decode?o.decode():Promise.resolve()).then(()=>r(!0)).catch(()=>r(!1))},o.onerror=()=>r(!1);const l=setTimeout(()=>r(!1),Ud);o.src=s}),i.preload}prepareImage(e,t){const i=As(t);if(!i.startsWith("/api/houseplan/content/"))return Promise.resolve(!0);const s=this.bind(e),n=()=>{const t=s.signed[i];if(t&&this.now()-t.atthis.now()&&!s.inFlight.has(i)&&!s.queued.has(i)?Promise.resolve(!1):null},o=n();return o?(a=o,new Promise(e=>{let t=!1;const i=i=>{t||(t=!0,clearTimeout(s),e(i))},s=setTimeout(()=>i(!1),Ud);a.then(i).catch(()=>i(!1))})):new Promise(e=>{let t=!1,i=null;const o=i=>{t||(t=!0,clearTimeout(r),s.settlers.delete(a),e(i))},a=()=>{t||i||(i=n(),i&&i.then(o).catch(()=>o(!1)))},r=setTimeout(()=>o(!1),Ud);s.settlers.add(a),a()});var a}backOff(e,t){const i=e.retry.get(t)?.delay||0,s=Math.min(6e4,i?2*i:2e3);e.retry.set(t,{notBefore:this.now()+s,delay:s})}trimShared(e){const t=Object.entries(e.signed);if(t.length<=512)return;const i=new Set(e.inFlight.keys());for(const t of e.references.values())for(const e of t)i.add(e);for(const[e,s]of t)(s.pending||s.preload)&&i.add(e);t.sort((e,t)=>t[1].at-e[1].at);const s=t.filter(([e])=>i.has(e));for(const e of t){if(s.length>=512)break;i.has(e[0])||s.push(e)}e.signed=Object.fromEntries(s)}resign(e,t){const i=this.bind(e);this.referenced=new Set(t),i.references.set(this.referenceOwner,new Set(this.referenced));const s=this.now(),n=new Set;for(const e of i.references.values())for(const t of e)n.add(t);const o={};for(const[e,t]of Object.entries(i.signed))n.has(e)&&s-t.at!!o[e]&&!o[e].pending))}markLoaded(e,t,i){const s=As(t);if(!s.startsWith("/api/houseplan/content/"))return;const n=this.bind(e).signed[s];if(n&&(!i||n.url===i)&&!n.loaded){n.loaded=!0;for(const t of[...this.bind(e).listeners])t()}}isReady(e,t){const i=As(t);if(!i.startsWith("/api/houseplan/content/"))return!0;const s=this.bind(e).signed[i];return!!s&&this.now()-s.ate.id===t)}function tp(e="file"){const t="draw"===e;return{source:e,showBorders:t,showNames:t,displayTouched:!1}}function ip(e,t){if(e.displayTouched)return{...e,source:t};const i="draw"===t;return{...e,source:t,showBorders:i,showNames:i}}function sp(e,t,i){return{...e,[t]:i,displayTouched:!0}}function np(e,t,i){return[e[0]*t+e[1]*i+e[2],e[3]*t+e[4]*i+e[5]]}function op(e,t){let i=0;for(const[s,n]of t){const t=np(e,s[0],s[1]);i=Math.max(i,Math.hypot(t[0]-n[0],t[1]-n[1]))}return i}const ap=e=>{if(null==e||""===e)return null;const t=Number(e);return Number.isFinite(t)?t:null},rp=e=>{const t=e,i=ap(Array.isArray(t)?t[0]:t?.x),s=ap(Array.isArray(t)?t[1]:t?.y);return null==i||null==s?null:[i,s]};function lp(e){if(!Array.isArray(e))return null;const t=[];for(const i of e){const e=rp(i);if(!e)return null;t.push(e)}if(t.length>1){const e=t[0],i=t[t.length-1];e[0]===i[0]&&e[1]===i[1]&&t.pop()}if(t.length<3)return null;let i=0,s=0,n=0;for(let e=0;ee+t[0],0)/t.length,t.reduce((e,t)=>e+t[1],0)/t.length]:[s/(3*i),n/(3*i)]}function cp(e){if(!Array.isArray(e))return null;const t=[];for(const i of e){const e=rp(i);if(!e)return null;t.push(e)}if(t.length>1){const e=t[0],i=t[t.length-1];e[0]===i[0]&&e[1]===i[1]&&t.pop()}const i=lp(t);if(!i)return null;const s=t.map(e=>e[0]),n=t.map(e=>e[1]);return{centroid:i,bbox:[Math.min(...s),Math.min(...n),Math.max(...s),Math.max(...n)]}}function hp(e){let t=e;if(t&&"object"==typeof t&&!Array.isArray(t)){const e=t;t=e.path??e.points??t}if(!Array.isArray(t)||!t.length)return[];let i=(t.some(e=>(e=>Array.isArray(e)?e.length>=2&&!Array.isArray(e[0])&&("object"!=typeof e[0]||null==e[0]):!!e&&"object"==typeof e&&("x"in e||"y"in e))(e))?[t]:t.filter(Array.isArray)).map(e=>e.map(rp).filter(e=>null!=e)).filter(e=>e.length>=2);i.length>64&&(i=i.slice(-64));const s=i.reduce((e,t)=>e+t.length,0);if(s<=4e3)return i.map(e=>e.slice());const n=4e3-2*i.length,o=i.map(e=>e.length-2),a=o.reduce((e,t)=>e+t,0),r=o.map(e=>n*e/a),l=r.map(Math.floor);let c=n-l.reduce((e,t)=>e+t,0);const h=r.map((e,t)=>({index:t,fraction:e-Math.floor(e)})).sort((e,t)=>t.fraction-e.fraction||e.index-t.index);for(let e=0;efunction(e,t){if(t>=e.length)return e.slice();if(t<=2)return[e[0],e[e.length-1]];const i=[];for(let s=0;se.slice()),i=t[t.length-1];return i?(i.length>2?i.pop():t.pop(),t):[]}function up(e){if(!e)return null;const t=e.vacuum_position||e.robot_position||null,i=t&&null!=ap(t.x)&&null!=ap(t.y)?{x:ap(t.x),y:ap(t.y),a:ap(t.a??t.angle??t.theta)}:null,s=hp(e.path?.path??e.path?.points??e.path),n=[],o=e.rooms,a=Array.isArray(o)?o.map((e,t)=>[String(e?.id??t),e]):o&&"object"==typeof o?Object.entries(o):[];for(const[e,t]of a){if(!t||"object"!=typeof t)continue;const i=String(t.name??t.label??"").trim(),s=[[ap(t.cx),ap(t.cy)],[ap(t.center?.x),ap(t.center?.y)],[ap(t.x),ap(t.y)]].find(([e,t])=>null!=e&&null!=t);let o=s?.[0]??null,a=s?.[1]??null;const r=cp(t.outline),l=ap(t.x0),c=ap(t.y0),h=ap(t.x1),d=ap(t.y1),p=(null!=l&&null!=c&&null!=h&&null!=d?[Math.min(l,h),Math.min(c,d),Math.max(l,h),Math.max(c,d)]:null)||r?.bbox||null;if(null!=o&&null!=a||!r||([o,a]=r.centroid),null!=o&&null!=a||!p||(o=(p[0]+p[2])/2,a=(p[1]+p[3])/2),i&&null!=o&&null!=a){const t={id:e,name:i,cx:o,cy:a};p&&([t.x0,t.y0,t.x1,t.y1]=p),n.push(t)}}const r=function(e){return String(e.map_name??e.current_map??e.map_index??e.selected_map??"default")}(e);return i||n.length||s.length?{pos:i,path:s,rooms:n,mapId:r}:null}function _p(e,t,i){if(!e||!t)return null;const s=t.attributes||{},n=e.split(".")[0]||"",o=null!=i?.platform?String(i.platform):null,a=!!rp(s.vacuum_position??s.robot_position),r=!(!s.rooms||"object"!=typeof s.rooms),l=hp(s.path?.path??s.path?.points??s.path).length>0,c=[s.map_name,s.current_map,s.map_index,s.selected_map].some(e=>null!=e),h="xiaomi_cloud_map_extractor"===o,d=r||l||c;let p=null;if(a?p="compatible":h?p="known_xcme_incomplete":d?p="partial":"camera"===n&&(p="camera"),!p)return null;const u=a?"camera"===n?300:200:h?100:d?50:0;return{entityId:e,name:String(s.friendly_name||e),platform:o,category:p,hasPosition:a,hasRooms:r,hasPath:l,hasMapId:c,score:u}}const mp=e=>e.toLowerCase().replace(/[\s_\-.,]+/g,"");function gp(e,t){const i=new Map(t.map(e=>[mp(e.name),e])),s=[],n=[];for(const t of e){const e=i.get(mp(t.name));e&&(s.push([[t.cx,t.cy],[e.cx,e.cy]]),n.push(t.name))}if(s.length<3)return null;const o=function(e){if(e.length<3)return null;let t=0,i=0,s=0,n=0,o=0,a=0,r=0,l=0,c=0,h=0,d=0,p=0;for(const[[u,_],[m,g]]of e){if(![u,_,m,g].every(Number.isFinite))return null;t+=u*u,i+=u*_,s+=u,n+=_*_,o+=_,a+=1,r+=u*m,l+=_*m,c+=m,h+=u*g,d+=_*g,p+=g}const u=[t,i,s,i,n,o,s,o,a],_=e=>{const[t,i,s,n,o,a,r,l,c]=u,h=t*(o*c-a*l)-i*(n*c-a*r)+s*(n*l-o*r);if(!Number.isFinite(h)||Math.abs(h)<1e-9)return null;const d=[(o*c-a*l)/h,(s*l-i*c)/h,(i*a-s*o)/h,(a*r-n*c)/h,(t*c-s*r)/h,(s*n-t*a)/h,(n*l-o*r)/h,(i*r-t*l)/h,(t*o-i*n)/h];return[d[0]*e[0]+d[1]*e[1]+d[2]*e[2],d[3]*e[0]+d[4]*e[1]+d[5]*e[2],d[6]*e[0]+d[7]*e[1]+d[8]*e[2]]},m=_([r,l,c]),g=_([h,d,p]);if(!m||!g)return null;const f=[m[0],m[1],m[2],g[0],g[1],g[2]];return f.every(Number.isFinite)?f:null}(s);return o?{matrix:o,matched:n,residual:op(o,s)}:null}function fp(e,t,i){const s=e[e.length-1];if(s&&s[0]===t[0]&&s[1]===t[1])return e;if(e.push(t),e.length<=600)return e;let n=function(e,t){if(e.length<3)return e.slice();const i=new Uint8Array(e.length);i[0]=i[e.length-1]=1;const s=[[0,e.length-1]];for(;s.length;){const[n,o]=s.pop(),[a,r]=e[n],[l,c]=e[o],h=l-a,d=c-r,p=Math.hypot(h,d)||1e-9;let u=0,_=-1;for(let t=n+1;tu&&(u=i,_=t)}_>0&&u>t&&(i[_]=1,s.push([n,_],[_,o]))}const n=[];for(let t=0;t600&&(n=n.filter((e,t)=>t%2==0||t===n.length-1)),n}function vp(e){return"cleaning"===e||"returning"===e||"on"===e}const bp={0:[1,0],90:[0,1],180:[-1,0],270:[0,-1]};function yp(e){const[t,i]=bp[e.rot]||[1,0],s=e.mir?-1:1;return[e.s*t*s,-e.s*i,e.ox,e.s*i*s,e.s*t,e.oy]}function wp(e){const t=e[0]*e[4]-e[1]*e[3];if(!Number.isFinite(t)||Math.abs(t)<1e-12)return null;const i=t<0,s=Math.sqrt(Math.abs(t));let n=180*Math.atan2(-e[1],e[4])/Math.PI;return n=(90*Math.round(n/90)%360+360)%360,{ox:e[2],oy:e[5],s:s,rot:n,mir:i}}function kp(e,t,i,s){const[n,o]=np(yp(t),i,s),a=yp({...e,ox:0,oy:0}),[r,l]=np(a,i,s);return{...e,ox:n-r,oy:o-l}}function xp(e){const t=e?.trail_mode;return"never"===t||"cleaning"===t||"always"===t?t:!1===e?.trail?"never":"cleaning"}const $p=new Set(["group","template","derivative","min_max","threshold","integration","statistics","trend","utility_meter","tod","switch_as_x","schedule"]);function Sp(e){const{hass:t,devices:i,markers:s,showEntities:n,currentBinding:o,currentDeviceId:a,labels:r}=e,l=wo(s),c=new Set(s.filter(e=>e.removed).map(e=>e.binding)),h=new Set;for(const e of i)e.id!==a&&("device"===e.bindingKind&&e.bindingRef&&h.add(`device:${e.bindingRef}`),"entity"===e.bindingKind&&e.bindingRef&&h.add(`entity:${e.bindingRef}`));const d=new Set;for(const e of i)"device"===e.bindingKind&&e.name&&d.add(`${e.name.trim()}|${e.area||""}`);const p=[];for(const e of Object.values(t?.devices||{})){if(!e||"service"===e.entry_type)continue;const t=`device:${e.id}`;if(h.has(t))continue;const i=String(e.name_by_user||e.name||e.id).trim();t!==o&&!c.has(t)&&d.has(`${i}|${e.area_id||""}`)||p.push({value:t,label:i,sub:(e.model||r.device)+("Group"===e.model?r.z2mGroup:""),kind:"device",ref:e.id,areaId:e.area_id||"",model:e.model||""})}for(const[e,i]of Object.entries(t?.entities||{})){const s=`entity:${e}`;if(h.has(s))continue;if(ko(t,e,l)&&!c.has(s))continue;const n=$p.has(i?.platform),o="group"===i?.platform;if(!n&&!o)continue;if(i?.hidden&&!c.has(s))continue;const a=t?.states?.[e];p.push({value:s,label:i?.name||a?.attributes?.friendly_name||e,sub:`${e.split(".")[0]} · ${o?r.group:r.helper}`,kind:"entity",ref:e,areaId:i?.area_id||i?.device_id&&t?.devices?.[i.device_id]?.area_id||"",model:"",parentDeviceId:i?.device_id||void 0})}if(n){const e=new Set(p.map(e=>e.value));for(const[i,s]of Object.entries(t?.entities||{})){const n=`entity:${i}`;if(h.has(n)||e.has(n)||s?.hidden&&!c.has(n))continue;const o=!!s?.device_id&&l.devices.has(s.device_id);if(ko(t,i,l)&&!c.has(n)&&!o)continue;const a=t?.states?.[i],d=s?.device_id?t?.devices?.[s.device_id]:null,u=d?String(d.name_by_user||d.name||""):"";p.push({value:n,label:s?.name||a?.attributes?.friendly_name||i,sub:`${i.split(".")[0]} · ${r.entity}${u?` · ${u}`:""}`,kind:"entity",ref:i,areaId:s?.area_id||d?.area_id||"",model:"",parentDeviceId:s?.device_id||void 0})}}return p.sort((e,t)=>e.label.localeCompare(t.label)||e.value.localeCompare(t.value))}const Mp={kind:"active",enabledEntityIds:[],allEntityIds:[]};function Cp(e){return e.startsWith("entity:")?"entity":"device"}function Dp(e){const t=e.binding.indexOf(":"),i=t>=0?e.binding.slice(t+1):"",s=Object.keys(e).filter(e=>!["id","binding","hidden"].includes(e));return e.id===`h${i}`&&0===s.length}const Pp={offStates:["off"],unknownUsesToggle:!0},Tp={light:Pp,switch:Pp,fan:Pp,humidifier:Pp,input_boolean:Pp,automation:Pp,remote:Pp,group:Pp,climate:{...Pp,featureMasks:{turn_on:256,turn_off:128,toggle:384}},media_player:{...Pp,featureMasks:{turn_on:128,turn_off:256,toggle:384}},siren:{...Pp,featureMasks:{turn_on:1,turn_off:2,toggle:3}},vacuum:{...Pp,featureMasks:{turn_on:1,turn_off:2,toggle:3}},water_heater:{...Pp,featureMasks:{turn_on:8,turn_off:8,toggle:8}},camera:{...Pp,featureMasks:{turn_on:1,turn_off:1,toggle:1}}};function Ip(e){return e.slice(0,e.indexOf("."))}function Rp(e,t,i){if(!e?.services||"object"!=typeof e.services)return!1;const s=e.services?.[t];return!!s&&Object.prototype.hasOwnProperty.call(s,i)}function Ap(e,t,i){return Rp(e,t,i)?{domain:t,service:i}:Rp(e,"homeassistant",i)?{domain:"homeassistant",service:i}:null}function zp(e,t){return e?.entities?.[t]||null}function Fp(e,t,i){const s=e?.states?.[i],n=zp(t,i);return s?.attributes?.friendly_name||n?.name||n?.original_name||i}function Np(e,t){const i=zp(e,t);if(null!=i?.disabled_by)return!0;const s=i?.device_id?e?.devices?.[i.device_id]:null;return null!=s?.disabled_by}function Ep(e,t){const i=e?.attributes?.supported_features;if(null==i||""===i)return!1;const s=Number(i);return Number.isFinite(s)&&(s&t)===t}function Op(e,t,i){const s=Ip(i);if("lock"===s||"alarm_control_panel"===s)return!0;if("cover"!==s)return!1;const n=e?.states?.[i],o=zp(t,i),a=String(n?.attributes?.device_class||o?.device_class||o?.original_device_class||"");return Ui.has(a)}function Hp(e,t,i,s,n){return{ref:i,entityId:s,name:s?Fp(e,t,s):null,reason:n}}function Lp(e,t,i,s,n,o=null){return{target:null,skipped:Hp(e,t,i,s,n),semantics:o,nextEffect:null,command:null}}function Bp(e,t,i,s,n=i){const o=Ip(i);if("cover"!==o&&"valve"!==o)return function(e,t,i,s,n=i){const o=Ip(i),a=e?.states?.[i];if(Np(t,i))return Lp(e,t,n,i,"ha-disabled","power");if(Op(e,t,i))return Lp(e,t,n,i,"secure");if(!a)return Lp(e,t,n,i,"missing","power");if("unavailable"===a.state)return Lp(e,t,n,i,"unavailable","power");const r=Tp[o];if(!r)return Lp(e,t,n,i,"unsupported");let l,c;if("unknown"===a.state||""===a.state){if(!r.unknownUsesToggle)return Lp(e,t,n,i,"unsupported","power");l="toggle",c="toggle"}else r.offStates.includes(String(a.state))?(l="turn-on",c="turn_on"):(l="turn-off",c="turn_off");const h=r.featureMasks?.[c];if(h&&!Ep(a,h))return Lp(e,t,n,i,"unsupported","power");const d=Ap(e,o,c);return d?{target:{entityId:i,name:Fp(e,t,i),state:String(a.state||""),via:s},skipped:null,semantics:"power",nextEffect:l,command:{...d,data:{entity_id:i}}}:Lp(e,t,n,i,"unsupported","power")}(e,t,i,s,n);if(Np(t,i))return Lp(e,t,n,i,"ha-disabled",o);if(Op(e,t,i))return Lp(e,t,n,i,"secure",o);const a=e?.states?.[i];if(!a)return Lp(e,t,n,i,"missing",o);if("unavailable"===a.state)return Lp(e,t,n,i,"unavailable",o);const r=function(e,t,i){const s=String(i?.state||""),n="closed"===s?{service:`open_${t}`,effect:"open",feature:1}:"open"===s?{service:`close_${t}`,effect:"close",feature:2}:"opening"===s||"closing"===s?{service:`stop_${t}`,effect:"stop",feature:8}:null;return n&&Ep(i,n.feature)&&Rp(e,t,n.service)?n:Ep(i,3)&&Rp(e,t,"toggle")?{service:"toggle",effect:"toggle"}:null}(e,o,a);if(!r)return Lp(e,t,n,i,"unsupported",o);return{target:{entityId:i,name:Fp(e,t,i),state:String(a.state||""),via:s},skipped:null,semantics:o,nextEffect:r.effect,command:{domain:o,service:r.service,data:{entity_id:i}}}}function jp(e){return"entity"===e.bindingKind?e.bindingRef&&Ms(e.bindingRef)?[e.bindingRef]:[]:"virtual"===e.bindingKind?[]:eo(e)}function qp(e){const t=e.marker?.toggle_entity;return t&&jp(e).includes(t)?t:null}function Wp(e,t,i){const s=function(e,t){if("entity"===e.bindingKind&&e.bindingRef){const t=e.marker?.light_entity?to(e):null;return[...new Set([t,e.bindingRef].filter(e=>!!e))]}const i=e.entities.length?e.entities:e.allEntities||[],s=e.marker?.light_entity||e.primary&&Ms(e.primary)?to(e):null;return[...new Set([s,...Yn(t,i)].filter(e=>!!e))]}(i,t);if(!s.length)return null;const n="entity"===i.bindingKind?"binding":"device-role";if("binding"===n)return Bp(e,t,s[0],n);let o=null,a=null;for(const i of s){const s=Bp(e,t,i,n);if(s.command)return s;const r=s.skipped?.reason;if("missing"===r||"unavailable"===r||"secure"===r)return s;"ha-disabled"!==r?o||=s:a||=s}return o||a}function Up(e){const t=e.skipped?.reason;return"missing"===t?"unavailable":t||"unsupported"}function Gp(e,t){return{origin:e,kind:"none",semantics:null,targets:[],skippedTargets:[],noneReason:t,nextEffect:null,command:null}}function Vp(e,t){return"cover"===e||"toggle"===e?"toggle":"more-info"===e||"run"===e||"info"===e?e:(null==e||""===e)&&"light"===t?"toggle":"info"}function Kp(e,t){return{origin:e,kind:"single",semantics:t.semantics,targets:t.target?[t.target]:[],skippedTargets:t.skipped?[t.skipped]:[],noneReason:t.command?null:Up(t),nextEffect:t.nextEffect,command:t.command}}function Yp(e,t,i,s=[]){const n=new Map,o=[],a=new Set,r=e=>{const t=`${e.ref}\n${e.entityId||""}\n${e.reason}`;a.has(t)||(a.add(t),o.push(e))};for(const e of s)r(e);for(const s of i){const i=Bp(e,t,s.entityId,s.via,s.ref);i.target?n.set(s.entityId,n.get(s.entityId)||i.target):i.skipped&&r(i.skipped)}const l=[...n.values()];if(!l.length)return{origin:"explicit-toggle",kind:"group",semantics:"group-power",targets:l,skippedTargets:o,noneReason:o.length&&o.every(e=>"secure"===e.reason)?"secure":"configured-targets-missing",nextEffect:null,command:null};const c=l.some(e=>"on"===e.state)?"turn_off":"turn_on",h=Ap(e,"homeassistant",c);return h?{origin:"explicit-toggle",kind:"group",semantics:"group-power",targets:l,skippedTargets:o,noneReason:null,nextEffect:"turn_on"===c?"turn-on":"turn-off",command:{...h,data:{entity_id:l.map(e=>e.entityId)}}}:{origin:"explicit-toggle",kind:"group",semantics:"group-power",targets:[],skippedTargets:[...o,...l.map(i=>Hp(e,t,i.entityId,i.entityId,"unsupported"))],noneReason:"unsupported",nextEffect:null,command:null}}function Jp(e){return String(e.marker?.id||e.id||"")}function Xp(e){const{hass:t,device:i}=e,s=e.registryHass||t,n=function(e){return"toggle"===e.tapAction?"explicit-toggle":"cover"===e.tapAction?"legacy-cover":!e.tapAction&&e.primary?.startsWith("light.")?"default-light":null}(i);if(!n)return null;const o=i.marker;if("explicit-toggle"===n&&bn(o)){const t=String(o?.id||i.id||""),s=ro(e.devices).get(t);if(s)return function(e,t){const i=e.registryHass||e.hass;return Yp(e.hass,i,t.driverEids.map(e=>({entityId:e,via:"control-marker-driver",ref:`marker:${t.markerId}`})))}(e,s);const a=yn(o,e.virtualLights);return{origin:n,kind:"single",semantics:"power",targets:[{entityId:"",name:i.name,state:a?"on":"off",via:"virtual-light"}],skippedTargets:[],noneReason:null,nextEffect:a?"turn-off":"turn-on",command:null,operation:{kind:"virtual-light",markerId:o.id}}}if("explicit-toggle"===n){if(Zn(i.marker?.binding,i.marker?.controls??i.controls,i.entities).length)return function(e,t=null){const{hass:i,device:s,devices:n}=e,o=e.registryHass||i,a=Zn(s.marker?.binding,s.marker?.controls??s.controls,s.entities),r=e.lightSources||ho(i,n,null,e.virtualLights),l=new Map;for(const e of r){if(!e.key.startsWith("marker:"))continue;const t=l.get(e.key)||[];t.push(e),l.set(e.key,t)}const c=ro(n),h=new Map;for(const e of n){const t=String(e.marker?.id||e.id||"");t&&h.set(t,e)}const d=[],p=[];t&&d.push({entityId:t,via:"entity"===s.bindingKind?"binding":"device-role",ref:t});for(const e of a){if(!e.startsWith("marker:")){Ms(e)?d.push({entityId:e,via:"control-entity",ref:e}):p.push(Hp(i,o,e,e.includes(".")?e:null,"unsupported"));continue}const t=l.get(e)||[];if(!t.length){const t=h.get(e.slice(7));if("ha_disabled"===t?.bindingStatus?.kind){const s=t.bindingStatus.allEntityIds[0]||null;p.push({ref:e,entityId:s,name:t.name||(s?Fp(i,o,s):null),reason:"ha-disabled"})}else p.push(Hp(i,o,e,null,"missing"));continue}const n=[...new Set(t.flatMap(e=>e.serviceEids))];if(n.length){for(const t of n)d.push({entityId:t,via:"control-entity",ref:e});continue}const a=t.some(e=>e.passive),r=e.slice(7),u=Jp(s),_=c.get(r)?.controllers.find(e=>Jp(e.device)===u);if(a&&_?.driverEids.length)for(const t of _.driverEids)d.push({entityId:t,via:"control-marker-driver",ref:e});else p.push(Hp(i,o,e,null,a?"missing":"unsupported"))}return Yp(i,o,d,p)}(e,qp(i))}if("legacy-cover"===n){const e=i.entities.find(e=>e.startsWith("cover."))||i.allEntities?.find(e=>e.startsWith("cover."));return e?Kp(n,Bp(t,s,e,"device-role")):Gp(n,"no-actionable-entity")}const a=qp(i),r=a?Bp(t,s,a,"entity"===i.bindingKind?"binding":"device-role"):Wp(t,s,i);return r?Kp(n,r):Gp(n,i.virtual||"virtual"===i.bindingKind?"no-actionable-entity":"no-binding")}function Zp(e){if(!e)return[];const t=Array.isArray(e.data.entity_id)?e.data.entity_id:[e.data.entity_id];return[...new Set(t)].sort()}function Qp(e){return e?e.operation?e.operation:e.command?{kind:"ha-service",command:e.command}:null:null}const eu=new Set(["missing","ha-disabled","unavailable"]);function tu(e,t){const i=Qp(e),s=Qp(t);return!(!i||!s||i.kind!==s.kind)&&("virtual-light"===i.kind&&"virtual-light"===s.kind?i.markerId===s.markerId:"ha-service"===i.kind&&"ha-service"===s.kind&&function(e,t){const i=Zp(e),s=Zp(t);return i.length===s.length&&i.every((e,t)=>e===s[t])}(i.command,s.command))}const iu=e=>!!e&&e.length>=2&&Number.isFinite(e[0])&&Number.isFinite(e[1]);function su(e,t=5,i=5){return Ml(e.cm,t,i)/2}function nu(e,t,i=1,s=5,n=5,o=0){const a=e.host;if(!a||"partition"!==a.kind||"string"!=typeof a.id||!a.id)return{resolved:null,reason:"invalid-host"};const r=t.find(e=>e.id===a.id);if(!r)return{resolved:null,reason:"missing-partition"};if(!iu(r.a)||!iu(r.b)||!Number.isFinite(a.t)||a.t<0||a.t>1)return{resolved:null,reason:"invalid-position"};const l=r.b[0]-r.a[0],c=r.b[1]-r.a[1],h=Math.hypot(l,c),d=Number(e.length)*i;if(!(h>1e-9&&d>0&&Number.isFinite(d)))return{resolved:null,reason:"invalid-length"};const p=a.t*h;if(p-d/2h-o+1e-9)return{resolved:null,reason:"does-not-fit"};const u=l/h,_=c/h;let m=180*Math.atan2(c,l)/Math.PI;return m>=90?m-=180:m<-90&&(m+=180),{reason:null,resolved:{opening:e,host:a,partition:r,center:[r.a[0]+l*a.t,r.a[1]+c*a.t],angle:m,length:d,depth:Ml(r.cm,s,n),t:a.t,axis:{a:[r.a[0],r.a[1]],b:[r.b[0],r.b[1]],ux:u,uy:_,length:h}}}}function ou(e,t,i=1,s=5,n=5){return nu(e,t,i,s,n,0)}function au(e,t,i=1,s=5,n=5){const o=e.host,a="partition"===o?.kind?t.find(e=>e.id===o.id):void 0;if(!a)return ou(e,t,i,s,n);const r=nu(e,t,i,s,n,su(a,s,n));return"does-not-fit"===r.reason?{resolved:null,reason:"does-not-fit-jamb"}:r}function ru(e,t){return!e||(e.length!==t.length||e.host?.kind!==t.host?.kind||e.host?.id!==t.host?.id||e.host?.t!==t.host?.t)}function lu(e){const{center:t,length:i,axis:s,host:n}=e,o=i/2;return{hostId:n.id,a:[t[0]-s.ux*o,t[1]-s.uy*o],b:[t[0]+s.ux*o,t[1]+s.uy*o],depth:e.depth}}function cu(e,t=!1){const i=t?1:-1,s=e.depth/2;return{ox:-e.axis.uy*i*s,oy:e.axis.ux*i*s,cm:e.partition.cm,side:i}}function hu(e,t,i){return e.flatMap(e=>!iu(e.a)||!iu(e.b)||Math.hypot(e.b[0]-e.a[0],e.b[1]-e.a[1])<=1e-9?[]:[{roomId:"",a:[e.a[0],e.a[1]],b:[e.b[0],e.b[1]],key:`partition:${e.id}`,kind:"outer",cm:e.cm,open:!1,half:Ml(e.cm,t,i)/2,partitionHost:{kind:"partition",id:e.id}}])}function du(e,t,i){const{center:s,length:n,axis:o}=e,a=n/2,r=t.flatMap(e=>{if(!e.kind||e.open)return[];const t=e.b[0]-e.a[0],n=e.b[1]-e.a[1],r=Math.hypot(t,n);if(!(r>i))return[];const l=t/r,c=n/r;if(Math.abs(o.ux*c-o.uy*l)>1e-6)return[];if(Math.abs((s[0]-e.a[0])*c-(s[1]-e.a[1])*l)>i)return[];const h=e=>(e[0]-s[0])*o.ux+(e[1]-s[1])*o.uy,d=h(e.a),p=h(e.b),u=Math.max(-a,Math.min(d,p)),_=Math.min(a,Math.max(d,p));return _>=u-i?[[u,_]]:[]});if(!r.length)return!1;r.sort((e,t)=>e[0]-t[0]||e[1]-t[1]);let l=-a;for(const[e,t]of r){if(e>l+i)return!1;if(l=Math.max(l,t),l>=a-i)return!0}return!1}function pu(e,t,i){return{...e,x:t.center[0]/i,y:t.center[1]/i,angle:t.angle}}const uu=.001;function _u(e){return!!e&&e.length>=2&&Number.isFinite(e[0])&&Number.isFinite(e[1])}function mu(e){return`${e[0].toFixed(6)},${e[1].toFixed(6)}`}function gu(e,t,i){return Math.abs(e[0]-t[0])"room"!==e.kind&&i.some(i=>i!==e&&function(e,t,i){const s=e.b[0]-e.a[0],n=e.b[1]-e.a[1],o=Math.hypot(s,n);if(!(o>i))return!1;const a=s/o,r=n/o,l=t=>Math.abs((t[0]-e.a[0])*r-(t[1]-e.a[1])*a);if(l(t.a)>i||l(t.b)>i)return!1;const c=(t.a[0]-e.a[0])*a+(t.a[1]-e.a[1])*r,h=(t.b[0]-e.a[0])*a+(t.b[1]-e.a[1])*r;return Math.min(o,Math.max(c,h))-Math.max(0,Math.min(c,h))>i}(e,i,t))),n=s.map(e=>{const[t,i]=fu(e.a,e.b);return{a:t,b:i,key:`hidden|${bu(e,t,i)}`,sourceKind:e.kind,sourceId:e.id}}).sort((e,t)=>e.key.localeCompare(t.key)),o=s.flatMap(e=>[e.a,e.b].map((t,i)=>({point:[t[0],t[1]],key:`hidden|${vu(e)}|endpoint-${i}`,sourceKind:e.kind,sourceId:e.id}))).sort((e,t)=>e.key.localeCompare(t.key));return{segments:n,endpoints:o}}function xu(e){const t=e.epsilon??uu,i=e.roomCuts||[],s=new Map;for(const t of e.partitionCuts||[]){if("string"!=typeof t.hostId||!t.hostId||!_u(t.a)||!_u(t.b))continue;const e=s.get(t.hostId)||[];e.push([t.a[0],t.a[1],t.b[0],t.b[1]]),s.set(t.hostId,e)}const n=[];for(const[t,s]of pi(e.space.rooms).entries())s.length<4||n.push({a:[s[0],s[1]],b:[s[2],s[3]],kind:"room",id:`room-edge-${t}`,cuts:i});for(const t of e.space.room_drafts||[])if(t.id!==e.activeDraftId)for(let e=0;e+1wu(s,e,t)))continue;const e=mu(s);a.has(e)||a.set(e,{point:[s[0],s[1]],key:e})}}}return{segments:[...o.values()].sort((e,t)=>e.key.localeCompare(t.key)),endpoints:[...a.values()].sort((e,t)=>e.key.localeCompare(t.key))}}function $u(e,t,i){return t.some(t=>_u(t)&&gu(e,t,i))}function Su(e,t,i){if(!i)return!0;const s=e-i.distance;return s<-1e-9||Math.abs(s)<=1e-9&&t.localeCompare(i.key)<0}function Mu(e,t,i){const s=i.epsilon??uu,n=i.excludePoints||[],o=[...e.endpoints,...(i.extraEndpoints||[]).filter(e=>_u(e.point)).map(e=>({point:[e.point[0],e.point[1]],key:e.key}))],a=new Map;for(const e of o){if($u(e.point,n,s))continue;const t=mu(e.point),i=a.get(t);(!i||e.key.localeCompare(i.key)<0)&&a.set(t,e)}return[...a.values()].map(e=>({kind:"endpoint",point:[...e.point],key:e.key,distance:Math.hypot(t[0]-e.point[0],t[1]-e.point[1])})).filter(e=>e.distance<=i.tolerance).sort((e,t)=>e.distance-t.distance||e.key.localeCompare(t.key))}function Cu(e,t){if(!e.length)return null;if(t>0){const i=new Set;for(let s=0;s1)return{kind:"ambiguous",candidate:null,conflicts:e.filter(e=>i.has(e.key)).map(e=>({point:[...e.point],key:e.key}))}}return{kind:"resolved",candidate:e[0],conflicts:[]}}function Du(e,t,i){const s=e.b[0]-e.a[0],n=e.b[1]-e.a[1],o=Math.hypot(s,n);if(!(o>0))return[...e.a];const a=s/o,r=n/o,l=Math.max(0,Math.min(o,(t[0]-e.a[0])*a+(t[1]-e.a[1])*r)),c=i>0?Math.max(0,Math.min(o,Math.round(l/i)*i)):l;return[e.a[0]+a*c,e.a[1]+r*c]}function Pu(e,t,i){if(!(_u(t)&&_u(i.anchor)&&i.tolerance>=0))return{kind:"none",candidate:null,conflicts:[]};const s=i.epsilon??uu,n=function(e,t){const i=t[0]-e[0],s=t[1]-e[1];if(!(Math.hypot(i,s)>Number.EPSILON))return null;const n=Math.round(Math.atan2(s,i)/(Math.PI/4))*(Math.PI/4);return[Math.cos(n),Math.sin(n)]}(i.anchor,t);if(!n)return{kind:"none",candidate:null,conflicts:[]};const o=Mu(e,t,i).filter(e=>function(e,t,i,s){const n=e[0]-t[0],o=e[1]-t[1];return n*i[0]+o*i[1]>s&&Math.abs(n*i[1]-o*i[0])<=s*Math.max(Math.hypot(n,o),1)}(e.point,i.anchor,n,s)),a=Cu(o,i.distinguishTolerance||0);if(a)return a;let r=null;for(const o of e.segments){const e=o.b[0]-o.a[0],a=o.b[1]-o.a[1],l=o.a[0]-i.anchor[0],c=o.a[1]-i.anchor[1],h=n[0]*a-n[1]*e;let d=null;if(Math.abs(h)<=s*Math.max(Math.hypot(e,a),1)){if(Math.abs(l*n[1]-c*n[0])>s*Math.max(Math.hypot(l,c),1))continue;const e=[o.a,o.b].map(e=>({point:e,along:(e[0]-i.anchor[0])*n[0]+(e[1]-i.anchor[1])*n[1]})).filter(e=>e.along>s).sort((e,t)=>e.along-t.along);e.length&&(d=[...e[0].point])}else{const t=(l*a-c*e)/h,o=(l*n[1]-c*n[0])/h;t>s&&o>=-s&&o<=1+s&&(d=[i.anchor[0]+n[0]*t,i.anchor[1]+n[1]*t])}if(!d)continue;const p=Math.hypot(t[0]-d[0],t[1]-d[1]);p>i.tolerance||$u(d,i.excludePoints||[],s)||!Su(p,o.key,r)||(r={kind:"line",point:d,key:o.key,distance:p,segment:o})}return r?{kind:"resolved",candidate:r,conflicts:[]}:{kind:"none",candidate:null,conflicts:[]}}const Tu=.001;function Iu(e,t,i,s){const n=Number.isFinite(e)&&e>0?Math.floor(e):0,o=e=>"number"==typeof e&&Number.isFinite(e)&&e>=0?e:null,a=o(i),r=o(s)??0,l=[];let c=null;for(let e=0;e=2&&Number.isFinite(e[0])&&Number.isFinite(e[1])}function zu(e,t,i,s){return e*s-t*i}function Fu(e){let t=0;for(let i=0;ie.localeCompare(t)),t[0]}function Lu(e,t,i){const s=[...e];for(let e=!0;e&&s.length>=3;){e=!1;for(let n=0;n=0){s.splice(n,1),e=!0;break}}}return s}function Bu(e,t){const i=e.map(e=>Math.max(0,Math.min(1,e))).sort((e,t)=>e-t),s=[];for(const e of i)(!s.length||Math.abs(e-s[s.length-1])>t)&&s.push(e);return s}function ju(e,t){return[e.a[0]+(e.b[0]-e.a[0])*t,e.a[1]+(e.b[1]-e.a[1])*t]}function qu(e,t,i){const s=function(e,t){const i=t.b[0]-t.a[0],s=t.b[1]-t.a[1],n=i*i+s*s;return n>0?((e[0]-t.a[0])*i+(e[1]-t.a[1])*s)/n:0}(e,t);if(s<-i||s>1+i)return null;const n=ju(t,s);return Math.hypot(n[0]-e[0],n[1]-e[1])<=i?Math.max(0,Math.min(1,s)):null}function Wu(e,t,i,s,n){const o=e.b[0]-e.a[0],a=e.b[1]-e.a[1],r=t.b[0]-t.a[0],l=t.b[1]-t.a[1],c=t.a[0]-e.a[0],h=t.a[1]-e.a[1],d=zu(o,a,r,l),p=Math.max(Math.hypot(o,a),Math.hypot(r,l),1);if(Math.abs(d)>n*p){const e=zu(c,h,r,l)/d,t=zu(c,h,o,a)/d;return void(e>=-n&&e<=1+n&&t>=-n&&t<=1+n&&(i.push(Math.max(0,Math.min(1,e))),s.push(Math.max(0,Math.min(1,t)))))}if(!(Math.abs(zu(c,h,o,a))>n*p)){for(const s of[t.a,t.b]){const t=qu(s,e,n);null!=t&&i.push(t)}for(const i of[e.a,e.b]){const e=qu(i,t,n);null!=e&&s.push(e)}}}function Uu(e){let t=e+1|0;return t^=t<<13,t^=t>>>17,t^=t<<5,t>>>0}function Gu(e,t){return e.minY-t.minY||e.index-t.index}function Vu(e){return e.subtreeMaxY=Math.max(e.item.maxY,e.left?.subtreeMaxY??-1/0,e.right?.subtreeMaxY??-1/0),e}function Ku(e){const t=e.right;return e.right=t.left,t.left=Vu(e),Vu(t)}function Yu(e){const t=e.left;return e.left=t.right,t.right=Vu(e),Vu(t)}function Ju(e,t){return e?(Gu(t,e.item)<0?(e.left=Ju(e.left,t),e.left.priority0)e.right=Xu(e.right,t);else{if(!e.left)return e.right;if(!e.right)return e.left;e.left.priority=t&&Zu(e.left,t,i,s),e.item.minY<=i&&e.item.maxY>=t&&s.push(e.item),e.item.minY<=i&&Zu(e.right,t,i,s))}function Qu(e,t=.001){const i=Number.isFinite(t)&&t>0?t:Tu,s=e.filter(e=>Au(e.a)&&Au(e.b)&&"string"==typeof e.key&&e.key.length>0&&Math.hypot(e.b[0]-e.a[0],e.b[1]-e.a[1])>i),n=s.map(()=>[0,1]);!function(e,t,i){const s=e.map((e,t)=>({index:t,minX:Math.min(e.a[0],e.b[0])-i,maxX:Math.max(e.a[0],e.b[0])+i,minY:Math.min(e.a[1],e.b[1])-i,maxY:Math.max(e.a[1],e.b[1])+i})),n=[...s].sort((e,t)=>e.minX-t.minX||e.minY-t.minY||e.index-t.index),o=[...s].sort((e,t)=>e.maxX-t.maxX||e.index-t.index),a=new Set;let r=null,l=0;for(const s of n){for(;le.index-t.index);for(const o of n)Wu(e[o.index],e[s.index],t[o.index],t[s.index],i);a.add(s.index),r=Ju(r,s)}}(s,n,i);const o=new Map;for(let e=0;e({key:e,a:t.a,b:t.b,sourceKeys:[...t.sourceKeys].sort((e,t)=>e.localeCompare(t))})).sort((e,t)=>e.key.localeCompare(t.key))}function e_(e,t=.001){const i=Number.isFinite(t)&&t>0?t:Tu,s=Qu(e,i),n=new Map,o=new Map,a=new Map(s.map(e=>[e.key,e]));for(const e of s){const t=Eu(e.a,i),s=Eu(e.b,i);n.set(t,e.a),n.set(s,e.b),o.has(t)||o.set(t,new Set),o.has(s)||o.set(s,new Set),o.get(t).add(s),o.get(s).add(t)}const r=new Map;for(const[e,t]of o){const i=n.get(e);r.set(e,[...t].sort((e,t)=>{const s=n.get(e),o=n.get(t);return Math.atan2(s[1]-i[1],s[0]-i[0])-Math.atan2(o[1]-i[1],o[0]-i[0])||e.localeCompare(t)}))}const l=new Set,c=new Map,h=(e,t)=>`${e}>${t}`;for(const e of s){const t=[Eu(e.a,i),Eu(e.b,i)];for(const[e,o]of[t,[t[1],t[0]]]){if(l.has(h(e,o)))continue;const t=[],d=[];let p=e,u=o,_=!1;for(let i=0;i<=2*s.length+2;i++){const i=h(p,u);if(l.has(i)){_=p===e&&u===o;break}l.add(i),t.push(p),d.push(Ou(p,u));const s=r.get(u)||[],n=s.indexOf(p);if(n<0||!s.length)break;if(p=u,u=s[(n-1+s.length)%s.length],p===e&&u===o){_=!0;break}}if(!_||new Set(t).size<3||new Set(t).size!==t.length)continue;const m=t.map(e=>n.get(e)),g=Fu(m);if(!(g>i*i))continue;const f=Hu(Lu(t,n,i)),v=new Set;for(const e of d)for(const t of a.get(e)?.sourceKeys||[])v.add(t);const b={ring:m,key:f,area:g,atomKeys:[...d],sourceKeys:[...v].sort((e,t)=>e.localeCompare(t))};c.has(f)||c.set(f,b)}}return{atoms:s,faces:[...c.values()].sort((e,t)=>e.area-t.area||e.key.localeCompare(t.key))}}function t_(e,t,i=.001){if(!Au(t))return null;const s=e.faces.filter(e=>{if(e.ring.some((s,n)=>function(e,t,i,s){const n=i[0]-t[0],o=i[1]-t[1],a=n*n+o*o;if(!(a>0))return Math.hypot(e[0]-t[0],e[1]-t[1])<=s;const r=((e[0]-t[0])*n+(e[1]-t[1])*o)/a;return!(r<0||r>1)&&Math.hypot(t[0]+n*r-e[0],t[1]+o*r-e[1])<=s}(t,s,e.ring[(n+1)%e.ring.length],i)))return!1;let s=!1;for(let i=0,n=e.ring.length-1;it[1]!=a[1]>t[1]&&t[0]<(a[0]-o[0])*(t[1]-o[1])/(a[1]-o[1])+o[0]&&(s=!s)}return s});return[...s].sort((e,t)=>e.area-t.area||e.key.localeCompare(t.key))[0]||null}function i_(e,t,i){const s=i[0]-t[0],n=i[1]-t[1],o=s*s+n*n,a=o>0?Math.max(0,Math.min(1,((e[0]-t[0])*s+(e[1]-t[1])*n)/o)):0;return Math.hypot(t[0]+s*a-e[0],t[1]+n*a-e[1])}function s_(e,t,i=.5){let s=Math.abs(e-t)%180;return s>90&&(s=180-s),s<=i}function n_(e,t,i,s){const n=e.filter(e=>"outer"===e.kind&&!e.open&&e.cm>0).map(e=>({interval:e,reusePartitionId:t.find(t=>function(e,t,i){return!(Math.abs(e.cm-t.cm)>1e-6)&&(s_(180*Math.atan2(e.b[1]-e.a[1],e.b[0]-e.a[0])/Math.PI,180*Math.atan2(t.b[1]-t.a[1],t.b[0]-t.a[0])/Math.PI,.001)&&i_(t.a,e.a,e.b)<=i&&i_(t.b,e.a,e.b)<=i)}(t,e,s))?.id||null})),o=new Map;for(const t of i){if("partition"===t.host?.kind)continue;const i=e.map(e=>{const i=180*Math.atan2(e.b[1]-e.a[1],e.b[0]-e.a[0])/Math.PI;return{interval:e,distance:s_(t.angle,i)?i_([t.x,t.y],e.a,e.b):1/0}}).filter(e=>e.distance<=s).sort((e,t)=>e.distance-t.distance||e.interval.key.localeCompare(t.interval.key)),n=i[0]?.interval;"outer"===n?.kind&&!n.open&&n.cm>0&&o.set(t.id,n.key)}return{materialize:n,openingIntervals:o,removeOpeningIds:[...o.keys()].sort((e,t)=>e.localeCompare(t))}}function o_(e,t){const i=t.b[0]-t.a[0],s=t.b[1]-t.a[1],n=i*i+s*s;return n>0?Math.max(0,Math.min(1,((e[0]-t.a[0])*i+(e[1]-t.a[1])*s)/n)):0}const a_=e=>!e.key.startsWith("static:room|");function r_(e,t){const i=t.b[0]-t.a[0],s=t.b[1]-t.a[1],n=i*i+s*s;if(!(n>0))return null;const o=((e[0]-t.a[0])*i+(e[1]-t.a[1])*s)/n,a=[t.a[0]+i*o,t.a[1]+s*o];return{point:a,t:o,distance:Math.hypot(a[0]-e[0],a[1]-e[1])}}function l_(e,t){return t>0?Math.hypot(e[0]-Math.round(e[0]/t)*t,e[1]-Math.round(e[1]/t)*t):0}function c_(e,t,i){if(a_(e.source)!==a_(t.source))return a_(e.source)?e:t;const s=l_(e.point,i),n=l_(t.point,i);return Math.abs(s-n)>1e-9?s>n?e:t:function(e,t){return e[0]-t[0]||e[1]-t[1]}(e.point,t.point)>0?e:t}function h_(e){return[e.from[0],e.from[1],e.to[0],e.to[1],e.targetKind].map(e=>"number"==typeof e?e.toFixed(6):e).join("|")}function d_(e,t){return e.map(e=>e.key!==t.sourceKey?e:{...e,[t.endpoint]:[...t.to]})}function p_(e,t){const i=t.epsilon??.001;if(!(t.maxDistance>=0)||!t.point&&!t.requiredSourceKey)return{kind:"none"};const s=new Map,n=e.flatMap(e=>[{source:e,endpoint:"a",point:e.a},{source:e,endpoint:"b",point:e.b}]);for(let e=0;ei)||l>t.maxDistance)continue;const c=c_(a,r,t.gridStep||0),h=c===a?r:a;if(!a_(c.source))continue;const d={sourceKey:c.source.key,endpoint:c.endpoint,from:[c.point[0],c.point[1]],to:[h.point[0],h.point[1]],targetSourceKey:h.source.key,targetKind:"endpoint",distance:l};s.set(h_(d),d)}for(const o of n)if(a_(o.source))for(const n of e){if(n===o.source)continue;const e=r_(o.point,n);if(!e||e.t<=i||e.t>=1-i||!(e.distance>i)||e.distance>t.maxDistance)continue;const a={sourceKey:o.source.key,endpoint:o.endpoint,from:[o.point[0],o.point[1]],to:e.point,targetSourceKey:n.key,targetKind:"line",distance:e.distance};s.set(h_(a),a)}const o=[];for(const n of s.values()){const s=e_(d_(e,n),i),a=t.point?t_(s,t.point,i):s.faces.find(e=>!t.requiredSourceKey||e.sourceKeys.includes(t.requiredSourceKey))||null;a&&o.push({proposal:n,face:a})}if(o.sort((e,t)=>e.proposal.distance-t.proposal.distance||h_(e.proposal).localeCompare(h_(t.proposal))),!o.length)return{kind:"none"};return new Set(o.map(e=>`${h_(e.proposal)}|${e.face.key}`)).size>1?{kind:"ambiguous",proposals:o.map(e=>e.proposal)}:{kind:"repair",...o[0]}}const u_=1e-5;function __(e){const t=e.map(()=>[]);for(let i=0;i=1-1e-9||p<=1e-9||p>=1-1e-9||(t[i].push(d),t[a].push(p))}}const i=[];for(let s=0;se-t),1];for(let e=1;e{const i=t[2]-t[0],s=t[3]-t[1],n=i*i+s*s;if(!(n>0))return Math.hypot(e[0]-t[0],e[1]-t[1]);const o=Math.max(0,Math.min(1,((e[0]-t[0])*i+(e[1]-t[1])*s)/n));return Math.hypot(e[0]-(t[0]+o*i),e[1]-(t[1]+o*s))},f_=(e,t,i,s)=>{const n=s[2]-s[0],o=s[3]-s[1],a=t*o-i*n;if(Math.abs(a)<1e-12)return 1/0;const r=s[0]-e[0],l=s[1]-e[1],c=(r*o-l*n)/a;if(!(c>1e-9))return 1/0;const h=(r*i-l*t)/a;return h<-1e-9||h>1+1e-9?1/0:c};function v_(e,t,i,s=96){if(!(t>0&&Number.isFinite(e[0])&&Number.isFinite(e[1])))return[];const n=[];for(const s of i){if(!s||s.length<4)continue;if(![s[0],s[1],s[2],s[3]].every(Number.isFinite))continue;const i=g_(e,s);if(i<1e-7)return[];i>t||n.push(s)}const o=[],a=Math.max(12,Math.round(s));for(let e=0;ee-t);const l=[];let c=Number.NEGATIVE_INFINITY;for(const i of o){if(i-c<1e-9)continue;c=i;const s=Math.cos(i),o=Math.sin(i);let a=t;for(const t of n){const i=f_(e,s,o,t);i=3?l:[]}class b_ extends he{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 pa(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},t=this._floorToken;return t?e.floor=t:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],t=this._lang,i=[{value:"",label:ua(t,"editor.floor_none")}],s=this._floorToken;s&&i.push({value:s,label:ua(t,"editor.floor_index",{index:String(this._config?.floor)})});const n="string"==typeof this._config?.floor?this._config.floor:"";n&&!e.some(e=>e.value===n)&&i.push({value:n,label:n}),i.push(...e);const o="string"==typeof this._config?.default_floor?this._config.default_floor:"",a=[...e];return o&&!e.some(e=>e.value===o)&&a.unshift({value:o,label:o}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:i}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:a}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:da(ua(t,"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 G;this._loadSpaces();const e=this._lang,t={title:ua(e,"editor.title"),floor:ua(e,"editor.floor"),default_floor:ua(e,"editor.default_floor"),language:ua(e,"editor.language"),icon_size:ua(e,"editor.icon_size"),show_temperature:ua(e,"editor.show_temperature"),live_states:ua(e,"editor.live_states"),show_signal:ua(e,"editor.show_signal"),kiosk:ua(e,"editor.kiosk"),cycle:ua(e,"editor.cycle")},i=this._schema,s=function(e,t,i){if(!i||null===t)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||t.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),n=e=>q`t[e.name]||e.name} - @value-changed=${this._valueChanged} - >`;return q` - ${n(i.slice(0,3))} - ${s?q``:G} - ${n(i.slice(3))} - `}_valueChanged(e){const t={...this._config,...e.detail.value};""===t.floor?delete t.floor:t.floor===this._floorToken&&(t.floor=this._config?.floor);const i=new Event("config-changed",{bubbles:!0,composed:!0});i.detail={config:t},this.dispatchEvent(i)}}function y_(e){const t=e.length/2,i=e.face.cm>0?e.face.cm/e.cellCm*e.gridPitch/2:Dh(4,e.cellCm),s="gate"===e.type?Math.sin(10*Math.PI/180)*t:0;return{half:t,jambHalf:i,gateDepth:s,outlineHalf:Math.max(Dh(16,e.cellCm),i+Dh(8,e.cellCm),s+Dh(8,e.cellCm)),hitHalf:Math.max(Dh(20,e.cellCm),i+Dh(10,e.cellCm),s+Dh(12,e.cellCm))}}function w_(e){if("passage"===e.type)return W``;const t=Math.max(0,Math.min(1,e.amount)),{half:i,jambHalf:s}=y_(e),n=Ch(e.cellCm),o=1.75*n,a=e.flipH?-1:1,r=e.flipV?-1:1,l="gate"===e.type?1:r;e.type,e.flipV,e.angle,e.face;let c;if("window"===e.type){const o=Math.PI/2*i,a=e.face.cm>0?W``:W``;c=W` - - - - - - - - - - - - - - ${a} - `}else if("gate"===e.type){const s=10*e.face.side*t;c=W` - - - - - - - - - - - - `}else{const s=Math.PI/2*e.length;c=W` - - - - - - - - `}return W` - - - ${c} - `}b_.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",b_);const k_={availability:"available",status:"neutral",activity:"none"};function x_(e){return e<=40?"low":e<180?"mid":"high"}function $_(e){return ai(e)}function S_(e){return"static_icon"===e.display?"neutral":"alarm"===e.visual.status?"alarm":"unavailable"===e.visual.availability?"unavailable":e.lockState?e.lockState:"working"===e.visual.status?"working":"open"===e.visual.status?"open":"neutral"}const M_={sourceKind:"none",decisionIds:["source.skipped_static_fast_path"],visualSources:[],criticalSources:[],samples:[]};function C_(e,t){const i=e?.entities?.[t],s=e?.states?.[t];return String(i?.name||i?.original_name||s?.attributes?.friendly_name||t)}function D_(e,t){const i=e?.states?.[t];if(!i)return"";if("function"==typeof e?.formatEntityState)try{const t=e.formatEntityState(i);if("string"==typeof t&&t)return t}catch{}return String(i.state??"")}function P_(e,t){if(t.virtual||"virtual"===t.bindingKind||"virtual"===t.marker?.binding)return"available";return(t.entities||[]).some(t=>{const i=String(e?.states?.[t]?.state??"").trim().toLowerCase();return""!==i&&"unknown"!==i&&"unavailable"!==i})?"available":"unavailable"}function T_(e,t,i,s){return{eid:t,role:i,name:C_(e,t),state:String(e?.states?.[t]?.state??""),stateText:D_(e,t),integrationDomain:e?.entities?.[t]?.platform||null,sample:s||cn(e,t)}}function I_(e,t,i=[t],s,n=e){const o=t.hidden&&t.userHidden?{...t,hidden:!1}:t;let a="none",r=[];const l=[],c=[...o.entities,...o.allEntities||[]].some(e=>e.startsWith("cover."))?{...o,tapAction:"cover"}:o,h=(d=Xp({hass:e,devices:i,device:c,lightSources:s,registryHass:n}),"cover"!==d?.semantics?null:d.targets[0]?.entityId||d.skippedTargets[0]?.entityId||null);var d;const p=ho(e,[o]),u=Zn(o.marker?.binding,o.marker?.controls??o.controls,o.entities),_=new Set(u.filter(e=>e.startsWith("marker:"))),m=!0===o.marker?.is_light||_.size>0?s||ho(e,i):p,g=m.filter(e=>e.device.id===o.id&&"controls"!==e.via),f=p.filter(e=>"controls"!==e.via),v=g.length?g:f,b=bn(o.marker),y=b?[...v]:[...p.filter(e=>"controls"===e.via),...v];if(_.size)for(const e of m){if(!_.has(e.key))continue;e.stateEids.length>0&&y.some(t=>t.stateEids.some(t=>e.stateEids.includes(t)))||y.some(t=>t.key===e.key)||y.push({...e,via:"controls",castsGlow:!1})}const w=Yn(n,o.entities),k=!!h&&("cover"===o.tapAction||o.primary?.startsWith("cover.")||w.some(e=>e.startsWith("cover.")));if(h&&!k&&l.push("source.cover_capability_bypassed"),k)a="cover",l.push("source.cover"),r=[T_(e,h,"cover")];else if(y.length)a=!b&&y.some(e=>"controls"===e.via)?"controls":"light",b?l.push("source.manual_virtual_light"):l.push("controls"===a?"source.controls":"source.owned_light"),"controls"!==a||!o.virtual&&"virtual"!==o.bindingKind||l.push("source.virtual_controller"),r=y.map(i=>{const s="controls"===i.via?"control":"forced"===i.via?"forced_light":"light";if(!i.passive)return T_(e,i.eid,s);const n=i.on?"on":"off";return{eid:i.key,role:s,name:t.name,state:n,stateText:n,integrationDomain:null,sample:{eid:i.key,state:n,availability:"available",status:i.on?"working":"neutral",activity:"none",edge:"none"}}});else{const t=Yn(n,o.entities),i=t.length?t:o.entities.filter(t=>!!e?.states?.[t]);if(i.length){a="device_role",l.push("source.device_role");const t=hn(n===e?e:{...n,states:e?.states||{}},i,o.entities),s=i.some(e=>an(n,e));r=t.map(t=>{const i=s?rn(n,t.eid)?"power_gate":an(n,t.eid)?"lifecycle":"device_role":"device_role";return T_(e,t.eid,i,t)})}else o.primary?(a="primary",l.push("source.primary_fallback"),r=[T_(e,o.primary,"primary")]):l.push("source.none")}u.length>0&&"controls"!==a&&"light"!==a&&"cover"!==a&&l.push("source.filtered_saved_controls");const x=[];for(const t of o.entities||[]){const i=cn(e,t);"alarm"!==i.status||r.some(e=>e.eid===t)||x.push(T_(e,t,"critical",i))}return x.length&&l.push("source.critical_sibling"),{sourceKind:a,decisionIds:l,visualSources:r,criticalSources:x,samples:[...r,...x].map(e=>e.sample)}}function R_(e,t,i,s){if(t.virtual)return{source:null,text:null,fallback:"value_virtual"};if(s){const i=function(e,t){if(!0!==t.marker?.use_climate_temp)return null;for(const i of t.entities){if(!i.startsWith("climate."))continue;const t=Number(e?.states?.[i]?.attributes?.current_temperature);if(Number.isFinite(t))return{eid:i,text:Math.round(10*t)/10+"°"}}return null}(e,t);if(i)return{source:{kind:"temperature",eid:i.eid,attribute:"current_temperature",text:i.text},text:i.text,fallback:null};const s=function(e,t){if("mdi:thermometer"!==t.icon&&"mdi:air-filter"!==t.icon)return null;for(const i of t.entities){if(!Gn(e,i))continue;const t=Number(e?.states?.[i]?.state);if(Number.isFinite(t))return{eid:i,text:Math.round(10*t)/10+"°"}}return null}(e,t);if(s)return{source:{kind:"temperature",eid:s.eid,text:s.text},text:s.text,fallback:null};const n=function(e,t){if(!t.primary||!fo(e,t.primary))return null;const i=Number(e?.states?.[t.primary]?.state);return Number.isFinite(i)?{eid:t.primary,text:`${Math.round(i)}%`}:null}(e,t);if(n)return{source:{kind:"humidity",eid:n.eid,text:n.text},text:n.text,fallback:null}}const n=i.visualSources.filter(e=>!e.eid.startsWith("marker:"));let o=n.map(e=>e.eid);const a=n.find(e=>"power_gate"===e.role);if(a&&(o=[a.eid]),!o.length&&i.visualSources.some(e=>e.eid.startsWith("marker:"))&&(o=Yn(e,t.entities),!o.length&&t.primary&&e?.states?.[t.primary]&&(o=[t.primary])),o=[...new Set(o)],1!==o.length)return{source:null,text:null,fallback:o.length?"value_ambiguous_sources":"value_no_state"};const r=o[0],l=function(e,t){const i=e?.states?.[t];if(!i||null==i.state||""===String(i.state).trim())return{text:"",fallback:"value_no_state"};const s=i.state;if(!["string","number","boolean"].includes(typeof s))return{text:"",fallback:"value_non_scalar"};const n=String(s).trim().toLowerCase();if("unknown"===n||"unavailable"===n)return{text:"",fallback:"value_no_state"};const o=Xi(e,t);return o?{text:Zi(o,String(i.attributes?.unit_of_measurement||"")),fallback:null}:{text:"",fallback:"value_no_state"}}(e,r);return l.fallback?{source:null,text:null,fallback:l.fallback}:{source:{kind:"entity",eid:r,text:l.text},text:l.text,fallback:null}}function A_(e,t,i){const s=e.marker?.binding||(e.bindingKind&&e.bindingRef?`${e.bindingKind}:${e.bindingRef}`:e.virtual?"virtual":""),n=[...t.visualSources.map(e=>`${e.role}:${e.eid}`),...t.criticalSources.map(e=>`critical:${e.eid}`)].sort(),o=i?`${i.kind}:${i.eid}:${i.attribute||""}`:"none";return[s,t.sourceKind,...n,`value:${o}`].join("\n")}function z_(e,t,i){return A_(t,i||I_(e,t),null).replace(/\nvalue:none$/,"")}function F_(e){if(e.effectiveHidden)return[];if("static_icon"===e.display)return["static-icon"];const t=[],{visual:i}=e;return"alarm"===e.pulse.kind?t.push("alarm"):"unavailable"===i.availability?t.push("unavail"):"working"===i.status?t.push("on"):"open"===i.status&&t.push("open"),e.lockState&&t.push(`lock-${e.lockState}`),"none"!==e.pulse.reason&&"alarm"!==e.pulse.reason&&t.push("activity-"+e.pulse.reason),e.pulse.generation%2==0&&(t.push("pulse-gen2"),"short"===e.pulse.kind&&t.push("activity-gen2")),t}function N_(e,t,i){const s=Bi(t.marker?.display),n="static_icon"===s,o=n&&!1===i.sourceDetails?M_:I_(e,t,i.lightDevices||[t],i.lightSources,i.registryHass||e),a=t.bindingStatus,r="ha_disabled"===a?.kind,l="orphaned"===a?.kind,c=r?"ha_disabled":l?"orphaned":"active",h=!0===t.userHidden||!0===t.marker?.hidden,d=dn(o.samples),p=o.visualSources.find(e=>e.eid.startsWith("lock.")),u=p?["unlocked","open"].includes(p.state.toLowerCase())?"unlocked":"locked"===p.state.toLowerCase()?"locked":null:null,_=!bn(t.marker)&&Zn(t.marker?.binding,t.marker?.controls??t.controls,t.entities).length>0,m="controls"===o.sourceKind||_&&"light"!==o.sourceKind&&"cover"!==o.sourceKind,g=n&&!1===i.sourceDetails?{source:null,text:null,fallback:null}:R_(e,t,o,i.showTemperature),f=A_(t,o,g.source),v=z_(e,t,o),b=i.activityRuntime,y=i.now??Date.now(),w=b?.expiresAt||(b?.flashTs?b.flashTs+3300:0),k=b?.sources===v&&b.flashTs&&b.flashKind&&w>y?b.flashKind:null,x=function(e){const t=[],i="ha_disabled"===e.bindingLifecycle||"orphaned"===e.bindingLifecycle;let s=!1;"ha_disabled"===e.bindingLifecycle?(s=!0,t.push("lifecycle.ha_disabled_hidden")):e.userHidden&&!e.designPreview?(s=!0,t.push("lifecycle.user_hidden")):e.userHidden?t.push("lifecycle.user_hidden_preview"):"orphaned"===e.bindingLifecycle?t.push("lifecycle.orphaned_diagnostic"):t.push("lifecycle.active");const n="static_icon"===e.display;let o=e.controllerFace?{...e.sourceVisual,availability:e.controllerAvailability}:e.sourceVisual;t.push(e.controllerFace?"available"===e.controllerAvailability?"availability.controller_available":"availability.controller_unavailable":"availability.source"),s?(o=k_,t.push("face.hidden")):n?(o=k_,t.push("face.static")):"alarm"===e.sourceVisual.status||e.liveStates?t.push("face.dynamic"):(o=k_,t.push("face.live_states_disabled")),!n&&!s&&e.liveStates&&"alarm"!==e.sourceVisual.status&&e.shortActivity&&(o={...o,activity:e.shortActivity},t.push(`activity.short_${e.shortActivity}`)),"alarm"===o.status?t.push("status.alarm"):"unavailable"===o.availability?t.push("status.unavailable"):"working"===o.status?t.push("status.working"):"open"===o.status?t.push("status.open"):t.push("status.neutral");const a="value"===e.display&&!s&&e.valueAvailable?"value":"icon";t.push("value"===a?"content.value":"value"===e.display?"content.value_fallback_icon":"content.icon"),"value"===e.display&&!e.valueAvailable&&e.valueFallback&&t.push(`content.${e.valueFallback}`);const r=e.liveStates&&!n&&!s,l=!n&&!s,c=!s&&!i&&!n&&"available"===o.availability&&("alarm"===o.status||e.liveStates&&"icon_ripple"===e.display),h=l&&e.vacuumLiveRequested;return t.push(r?"diagnostics.dynamic_icon":"diagnostics.base_icon"),t.push(l?"diagnostics.metrics_enabled":"diagnostics.metrics_suppressed"),t.push(c?"activity.pulse_eligible":"activity.pulse_suppressed"),t.push(h?"diagnostics.vacuum_live":"diagnostics.vacuum_static"),{effectiveHidden:s,bindingUnavailable:i,visual:o,face:a,dynamicIcon:r,metrics:l,liveColor:r,pulseEligible:c,vacuumLive:h,decisionIds:t}}({bindingLifecycle:c,userHidden:h,designPreview:!0===i.designPreview,display:s,liveStates:i.liveStates,sourceVisual:d,controllerFace:m,controllerAvailability:m?P_(e,t):"available",shortActivity:k,valueAvailable:null!=g.text,valueFallback:g.fallback,vacuumLiveRequested:!0===t.marker?.vacuum?.live}),{effectiveHidden:$,visual:S}=x,M="icon_ripple"===s&&!$&&i.liveStates&&"alarm"!==S.status?S.activity:"none",C=x.metrics&&i.showTemperature?!0===t.marker?.use_climate_temp?go(e,t.entities):"mdi:thermometer"===t.icon||"mdi:air-filter"===t.icon?mo(e,t.entities):null:null,D=x.metrics&&i.showTemperature&&t.primary&&fo(e,t.primary)?vo(e,t.entities):null,P=x.metrics&&i.showSignal&&!t.virtual?_o(e,t.entities):null,T=i.lightSources||("derived_marker_state"===t.marker?.value_badge?.source?.kind?ho(e,i.lightDevices||[t]):[]),I=function(e,t,i){const s=null!=t.marker?.value_badge;if(i.effectiveHidden||"static_icon"===i.display)return null;const n=t.marker?.value_badge;if(s)return n?.enabled?n.source?{configured:!0,enabled:!0,position:n.position||"right",...Ho(e,t,n.source,i.markerStates)}:{configured:!0,enabled:!0,source:null,sourceLabel:"",text:"—",fullText:e?.localize?.("state.default.unavailable")||"Unavailable",position:n.position||"right",availability:"missing",isLqi:!1,tone:"default"}:null;if(!i.showTemperature||"value"===i.display)return null;const o=Lo(e,t);if(!o)return null;let a=null,r="default";if("entity_attribute"===o.kind&&"current_temperature"===o.attribute){const i=go(e,t.entities);null!=i&&(a=`${i}°`),r="temperature"}else if("mdi:thermometer"===t.icon||"mdi:air-filter"===t.icon){const i=mo(e,t.entities);null!=i&&(a=`${i}°`),r="temperature"}else{const i=vo(e,t.entities);null!=i&&(a=`${i}%`),r="humidity"}return null==a?null:{configured:!1,enabled:!0,source:o,position:"right",sourceLabel:"entity_attribute"===o.kind?`${No(o.attribute)} · ${Fo(e,o.entity_id)}`:"entity_state"===o.kind?Fo(e,o.entity_id):"",text:a,fullText:a,availability:"available",isLqi:!1,tone:r}}(e,t,{showTemperature:i.showTemperature,showSignal:i.showSignal,display:s,effectiveHidden:$,markerStates:T.filter(e=>e.key.startsWith("marker:")).map(e=>({ref:e.key,on:e.on,name:e.device.name}))}),R=o.visualSources.find(e=>!e.eid.startsWith("marker:")),A="cover"===o.sourceKind?R?.eid:t.primary||R?.eid,z=A?e?.states?.[A]:null,F=x.dynamicIcon?vs(t.icon,A?.split(".")[0],z?.attributes?.device_class,z?.state,!!t.marker?.icon):t.icon,N=x.liveColor&&o.visualSources.filter(e=>!e.eid.startsWith("marker:")).map(t=>function(e){return e&&"on"===e.state?ze(e.attributes?.rgb_color):null}(e?.states?.[t.eid])).find(e=>!!e)||null,E=Number(t.marker?.size)>0?Number(t.marker.size):1,O=Number(t.marker?.angle)||0,H=Number(t.marker?.ripple_size)>0?Number(t.marker.ripple_size):1.5,L=Ie(t.marker?.ripple_color,null),B=n?null:L||N||null,j=function(e){const{display:t,visual:i,semanticActivity:s,liveStates:n,effectiveHidden:o,bindingUnavailable:a=!1,reducedMotion:r=!1}=e,l=Math.max(1,Math.trunc(e.shortGeneration||1)),c=Number.isFinite(e.diameterScale)?Math.max(1,Number(e.diameterScale)):1.5,h=e.color||function(e,t){return"presence"===t?"#1DC21D":"running"===t||"working"===e.status||"open"===e.status?"#F0A00C":"#0C82F0"}(i,s);if(o||a||"unavailable"===i.availability||"static_icon"===t)return Yo(l,h,c);if("alarm"===i.status)return{kind:"alarm",reason:"alarm",generation:l,expiresAt:null,color:"#F0410C",diameterScale:1.5,animated:!r,reducedMotionIndicator:"none"};if(!n||"icon_ripple"!==t)return Yo(l,h,c);const d=e.now??Date.now(),p=e.shortExpiresAt||null;return e.shortReason&&null!=p&&p>d?{kind:"short",reason:e.shortReason,generation:l,expiresAt:p,color:h,diameterScale:r?1:c,animated:!r,reducedMotionIndicator:r?"dot":"none"}:"presence"===s||"transition"===s||"running"===s?{kind:"continuous",reason:s,generation:l,expiresAt:null,color:h,diameterScale:r?1:c,animated:!r,reducedMotionIndicator:r?"dot":"none"}:Yo(l,h,c)}({display:s,visual:S,semanticActivity:d.activity,shortReason:b?.sources===v?b?.flashKind:null,shortGeneration:b?.gen,shortExpiresAt:b?.sources===v?w:null,now:y,liveStates:i.liveStates,effectiveHidden:$,bindingUnavailable:x.bindingUnavailable,reducedMotion:i.reducedMotion,color:B,diameterScale:H}),q="value"===x.face?g.text:null,W="ha_disabled"===a?.kind?a.reason:null,U="ha_disabled"===(G={lifecycle:c,display:s,liveStates:i.liveStates,sourceKind:o.sourceKind,primaryDomain:(t.primary||"").split(".")[0],visual:S,activity:M}).lifecycle?"ha_disabled":"orphaned"===G.lifecycle?"orphaned":"static_icon"===G.display?"static_icon":"alarm"===G.visual.status?"alarm":G.liveStates?"unavailable"===G.visual.availability?"unavailable":"cover"===G.sourceKind?"cover_icon_state":"presence"===G.activity?"presence":"event"===G.activity?"event":"transition"===G.activity?"transition":"working"===G.visual.status?"icon_ripple"===G.display&&"none"!==G.activity?"working_activity":"working":"open"===G.visual.status?"open":"media_player"===G.primaryDomain?"media_neutral":"neutral":"live_states_disabled";var G;const V=[];i.designPreview&&h&&V.push("hidden_design_preview"),n||!0!==t.marker?.vacuum?.live||V.push("vacuum_live_plan_only");const K=o.visualSources.some(t=>"power_gate"===t.role||rn(e,t.eid)),Y=t.entities.filter(t=>t.startsWith("switch.")&&!e?.entities?.[t]?.entity_category).length;K&&Y>1&&V.push("composite_power_source"),"icon_ripple"!==s&&"static_icon"!==s&&"alarm"!==d.status&&"none"!==d.activity&&V.push("activity_display_disabled");const J={binding:t.marker?.binding||(t.bindingKind&&t.bindingRef?`${t.bindingKind}:${t.bindingRef}`:t.virtual?"virtual":""),sourceKind:o.sourceKind,visualSources:o.visualSources,criticalSources:o.criticalSources,valueSource:g.source,sourceSignature:f,decisionIds:[...o.decisionIds,...x.decisionIds,...I?["diagnostics.value_badge"]:[],...null==P?[]:[`diagnostics.lqi_${x_(P)}`],`pulse.${j.kind}_${j.reason}`],visual:S,lockState:u,display:s,icon:F,valueText:q,valueFullText:q,fallbackReason:"value"!==s||q?null:g.fallback,activity:M,activityGeneration:b?.gen||1,pulse:j,classes:[],tempText:null==C?null:String(C),humText:null==D?null:String(D),valueBadge:I,lqiText:null==P||I?.isLqi?null:String(P),lqiColor:null==P?null:$_(P),lqiBand:null==P?null:x_(P),lightColor:N,scale:E,angle:O,rippleScale:H,rippleColor:B,userHidden:h,effectiveHidden:$,haDisabled:r,disabledReason:W,orphaned:l,vacuumLive:x.vacuumLive,explanation:{reason:U,notices:V}};return{...J,classes:F_(J)}}function E_(e,t=new WeakMap){if(null===e||"object"!=typeof e)return e;const i=e,s=t.get(i);if(s)return s;if(Array.isArray(e)){const s=[];t.set(i,s);for(const i of e)s.push(E_(i,t));return Object.freeze(s)}const n={};t.set(i,n);for(const[i,s]of Object.entries(e))"function"!=typeof s&&(n[i]=E_(s,t));return Object.freeze(n)}function O_(e){const t=new Map(e);let i;return i=Object.freeze({get size(){return t.size},get:e=>t.get(e),has:e=>t.has(e),entries:()=>t.entries(),keys:()=>t.keys(),values:()=>t.values(),forEach:(e,s)=>t.forEach((t,n)=>e.call(s,t,n,i)),[Symbol.iterator]:()=>t[Symbol.iterator]()}),i}function H_(e){const t=new Set(e.entityIds||[]),i=new Set(e.deviceIds||[]),s=new Set(e.areaIds||[]);for(const s of e.devices){for(const e of s.entities||[])t.add(e);s.primary&&t.add(s.primary);for(const e of s.controls||[])t.add(e);s.marker?.vacuum?.source&&t.add(s.marker.vacuum.source),"device"===s.bindingKind&&s.bindingRef&&i.add(s.bindingRef),"entity"===s.bindingKind&&s.bindingRef&&t.add(s.bindingRef)}const n={},o={};for(const[o,a]of Object.entries(e.hass?.entities||{})){const r=a?.device_id?e.hass?.devices?.[a.device_id]:null;(t.has(o)||a?.device_id&&i.has(a.device_id)||s.has(a?.area_id)||s.has(r?.area_id))&&(t.add(o),n[o]=a,a?.device_id&&i.add(a.device_id))}for(const t of i){const i=e.hass?.devices?.[t];i&&(o[t]=i)}const a={};for(const i of t){const t=e.hass?.states?.[i];t&&(a[i]=t)}const r=Object.freeze({states:E_(a),entities:E_(n),devices:E_(o),config:E_(e.hass?.config),locale:E_(e.hass?.locale),themes:E_(e.hass?.themes)});return Object.freeze({sourceSequence:e.sourceSequence,capturedAt:e.capturedAt??Date.now(),hass:r,devices:E_([...e.devices]),positions:O_([...e.positions||[]].map(([e,t])=>[e,Object.freeze({x:t.x,y:t.y})])),presentations:O_([...e.presentations].map(([e,t])=>[e,E_(t)])),facts:O_([...e.facts||[]].map(([e,t])=>[e,E_(t)]))})}const L_=(e,t)=>`${e}:${t?1:0}`;function B_(e){return!Number.isFinite(e)||e<=0?2.25:e/2.5*2.25}function j_({openings:e,geometries:t,fillsByRoomId:i,idPrefix:s="data",groupClass:n="opening-tunnels",dataLayer:o="data"}){const a=e.map((e,n)=>{const o=t[n];if(!o)return G;const a=o.faces.find(e=>-1===e.side),r=o.faces.find(e=>1===e.side);if(!a||!r)return G;const l=i.get(a.roomId)||null,c=i.get(r.roomId)||null;if(!l&&!c)return G;const h=`${a.d} ${r.d}`,d=!!l&&!!c&&l.color===c.color&&l.opacity===c.opacity,p=`translate(${e.rx} ${e.ry}) rotate(${e.angle})`;if(d)return W``;const u=o.maxY-o.minY;if(!(u>0))return G;const _=`${(100*Math.max(0,Math.min(1,-o.minY/u))).toFixed(6)}%`,m=l||{color:"#000000",opacity:0},g=c||{color:"#000000",opacity:0},f=`hp-opening-tunnel-${s.replace(/[^a-zA-Z0-9_-]/g,"-")}-${n}`;return W``});return W``}const q_=e=>Array.isArray(e)&&e.length>=2&&Number.isFinite(Number(e[0]))&&Number.isFinite(Number(e[1])),W_=(e,t,i)=>[Number(e[0])*i,Number(e[1])*i,Number(t[0])*i,Number(t[1])*i],U_=e=>{const t=`${Number(e[0]).toFixed(9)},${Number(e[1]).toFixed(9)}`,i=`${Number(e[2]).toFixed(9)},${Number(e[3]).toFixed(9)}`;return t<=i?`${t}|${i}`:`${i}|${t}`},G_=e=>{const t=new Map;for(const i of e.flat())!Array.isArray(i)||i.length<4||!i.every(Number.isFinite)||Math.hypot(i[2]-i[0],i[3]-i[1])<=1e-9||t.set(U_(i),[...i]);return[...t.values()]};function V_(e){return"solid"===e?.zero_wall_style?"solid":"dashed"}function K_(e,t=1,i){const s=[];for(const n of Array.isArray(e?.wall_segments)?e.wall_segments:[])0===Number(n?.cm)&&q_(n?.a)&&q_(n?.b)&&(i&&!i.has(String(n.id||""))||s.push(W_(n.a,n.b,t)));return G_([s])}function Y_(e,t=1){const i=[];for(const s of Array.isArray(e?.partitions)?e.partitions:[])0===Number(s?.cm)&&q_(s?.a)&&q_(s?.b)&&i.push(W_(s.a,s.b,t));for(const s of Array.isArray(e?.room_drafts)?e.room_drafts:[]){const e=Array.isArray(s?.points)?s.points:[];for(let n=0;n+1e?.host?.kind===t.kind&&e.host.id===t.id)}const Q_=new WeakMap,em=new WeakMap;function tm(e){const t=Eh(e.cfg),i={};for(const t of e.cfg.spaces||[])for(const e of t.rooms||[])e.area&&(i[e.area]=t.id);const s=e.cfg.settings?.exclude_integrations?new Set(e.cfg.settings.exclude_integrations):Ye,n=Xe(e.cfg.settings?.icon_rules?.length?e.cfg.settings.icon_rules:Je);return So({hass:e.hass,registry:e.registry,areaToSpace:i,markers:e.cfg.markers||[],settings:e.cfg.settings||{},excluded:s,showAll:!!e.cfg.settings?.show_all,firstSpaceId:t[0]?.id||"",loc:t=>ua(e.lang,t),iconRules:n})}function im(e){const t=Eh(e.cfg).find(t=>t.id===e.spaceId);if(!t)return null;const i=cs(e.cfg.spaces.find(t=>t.id===e.spaceId)),s=ps(e.cfg.settings),n=e.iconSize??2.5,o=n>8?2.5:n,a=B_(o),r=e.registry?En(e.hass,e.registry):e.hass,l=e.registry?On(e.hass,e.registry):e.hass,c=(e.devices||tm(e)).filter(t=>t.space===e.spaceId),h=c.filter(e=>!e.hidden),d=function(e,t,i){const s={},n=i/100*nd(t)*1.3;for(const i of t.rooms){if(!i.area)continue;const t=e.filter(e=>e.area===i.area);if(!t.length)continue;const o=rd(i),a=.1*Math.min(o.w,o.h),r=o.w-2*a,l=o.h-2*a,c=Math.max(1,Math.round(Math.sqrt(t.length*r/Math.max(l,1)))),h=r/c,d=l/Math.max(Math.ceil(t.length/c),1),p=t.map((e,t)=>({x:o.x+a+h*(t%c+.5),y:o.y+a+d*(Math.floor(t/c)+.5)}));Oi(p,o,n,.5*a),t.forEach((e,t)=>s[e.id]=Wh(p[t]))}return s}(c,t,o),p=[];for(const t of h){const i=e.layout[t.id];if(i&&i.s===e.spaceId){const e=i.x*Ih,t=i.y*Ih;p.push({minX:e,minY:t,maxX:e,maxY:t})}}const u=e.cfg.spaces.find(t=>t.id===e.spaceId)||{},_=Array.isArray(u.walls)?u.walls:[],m=Number(u.cell_cm)>0?Number(u.cell_cm):5,g=X_(u,t,Ih,.02*Bh),f=(u.openings||[]).flatMap(e=>{if("partition"!==e.host?.kind)return[];const i=ou(e,t.partitions,Ih,m,Bh).resolved;return i?[i]:[]}),v=Jd({partitions:t.partitions,roomDrafts:t.room_drafts,columns:t.wall_columns,cellCm:m,hostedOpenings:f.map(e=>({id:e.opening.id,host:e.host,length:e.length,type:e.opening.type}))}),b=function(e,t,i,s){let n=em.get(e);n||(n=new Map,em.set(e,n));const o=n.get(t);if(o?.fingerprint===i)return o.value;const a=s();return n.set(t,{fingerprint:i,value:a}),a}(e.cfg,t.id,v,()=>_h(t,m,Bh,2e-4*Bh,f.map(lu)).all);for(const e of b){const t=e.map(e=>e[0]),i=e.map(e=>e[1]);t.length&&p.push({minX:Math.min(...t),minY:Math.min(...i),maxX:Math.max(...t),maxY:Math.max(...i)})}for(const e of g.lines)p.push({minX:Math.min(e[0],e[2]),minY:Math.min(e[1],e[3]),maxX:Math.max(e[0],e[2]),maxY:Math.max(e[1],e[3])});const y=id(t,p),w=[y.x,y.y,y.w,y.h],k=new Map(t.rooms.map(t=>{const n=zs(i.fill,t);return[t,_s(n,"lqi"===n&&t.area?Do(r,c,t.area):null,"light"===n?uo(ho(r,c,t,e.virtualLights)):"none","temp"===n&&t.area?Po(r,c,t.area):null,i.tempMin,i.tempMax,s,ls(i.customFill,t))]})),x=new Map;for(const e of t.rooms)e.id&&x.set(e.id,k.get(e)||null);const $=function(e,t=1e3){const i=[];for(const[s,n]of(e||[]).entries()){if("passage"!==n?.type)continue;const e={...n,id:String(n.id||`passage-${s}`),type:"passage",rx:Number(n.x)*t,ry:Number(n.y)*t,rlen:Number(n.length)*t};[e.rx,e.ry,e.rlen,e.angle].every(Number.isFinite)&&e.rlen>0&&i.push(e)}return i}((u.openings||[]).flatMap(e=>{if(!e.host||"wall"===e.host.kind)return[e];const t=f.find(t=>t.opening.id===e.id);return t?[pu(e,t,Ih)]:[]}),Ih),S=wc(t.rooms,_,g.contour,jh,m,Bh,Ih),M=f.filter(e=>du(e,S,2e-4*Bh)).map(e=>({x:e.center[0],y:e.center[1],angle:e.angle,length:e.length})),C=t.rooms.filter(e=>e.area||i.showBorders||"none"!==zs(i.fill,e)).map(e=>{let s="room "+(t.bg?"overlay":"yard"),n="";const o=zs(i.fill,e);if(i.showBorders||"none"!==o){s+=" styled";const t=[`--room-stroke:${i.color}`,`--room-stroke-op:${i.showBorders&&!g.contour.length?i.opacity:0}`],o=k.get(e)||null;o?(s+=" filled",t.push(`--room-fill:${o.color}`,`--room-fill-op:${o.opacity.toFixed(3)}`)):t.push("--room-fill:transparent","--room-fill-op:0"),n=t.join(";")}const a=e.id||G,r=e.area||G,l=e.poly?W``:W``;return l}),D=t.rooms.filter(e=>{const t=k.get(e);return Fs(i.glow,e)&&(!t||t.opacity<=0)}).map(e=>e.poly?W``:W``),P=new Map;for(const e of t.rooms)if(e.id){const t=k.get(e)||null;P.set(e.id,Fs(i.glow,e)&&(!t||t.opacity<=0)?{color:s.glow_base.c,opacity:s.glow_base.a,mode:"glow"}:null)}const T=ho(r,h,null,e.virtualLights),I=h.map(s=>{const n=function(e,t,i,s,n){const o=t[e.id];return o&&o.s===e.space?{x:o.x*Ih,y:o.y*Ih}:s[e.id]?s[e.id]:Wh(sd(n))}(s,e.layout,e.cfg,d,t),o=(n.x-w[0])/w[2]*100,a=(n.y-w[1])/w[3]*100,c=i.showLqi??!1!==e.showSignal,p=e.presentations?.get(L_(s.id,c))||N_(r,s,{liveStates:!1!==e.liveStates,showTemperature:!1!==e.showTemperature,showSignal:c,activityRuntime:e.activityRuntime?.get(s.id),sourceDetails:!1,lightDevices:h,lightSources:T,registryHass:l,reducedMotion:e.reducedMotion}),u=[`left:${o}%`,`top:${a}%`,...Go(p)],_=S_(p),m=[s.name,ua(e.lang,`marker.state_a11y_${_}`),"none"!==p.pulse.kind?ua(e.lang,`marker.pulse_a11y_${p.pulse.reason}`):"",p.valueFullText||p.valueText||"",Io(p.valueBadge),null!=p.lqiText&&p.lqiBand?ua(e.lang,`marker.lqi_a11y_${p.lqiBand}`,{value:p.lqiText}):""].filter(Boolean).join(", ");return q``}),R=i.showNames?t.rooms.filter(e=>e.name).map(s=>{const n=ld(s,t.id,e.layout,e.cfg),o=(n.x-w[0])/w[2]*100,a=(n.y-w[1])/w[3]*100,r=Math.min(1,i.opacity+.25);return q`
${s.name}
`}):[],A=t.bg?e.displayUrl?e.displayUrl(t.bg.href):t.bg.href:"",z=e.cfg.spaces.find(t=>t.id===e.spaceId)?.settings||{},F="daynight"===Fr(e.cfg?.settings,z)?$r(r,e.dayCycleNow??new Date):null,N=hs(e.cfg?.settings,i),E=!!(_.length||b.length&&i.showBorders),O=E?Jd($.length?{rooms:t.rooms,walls:_,extras:b,cellCm:m,zero:g.contour,passages:$.map(e=>({x:e.rx,y:e.ry,angle:e.angle,length:e.rlen})),hostedCompositeOpenings:M}:{rooms:t.rooms,walls:_,extras:b,cellCm:m,zero:g.contour}):"",H=E?function(e,t,i,s){let n=Q_.get(e);n||(n=new Map,Q_.set(e,n));const o=n.get(t);if(o?.fingerprint===i)return o.value;const a=s();return n.set(t,{fingerprint:i,value:a}),a}(e.cfg,t.id,O,()=>Wc(t.rooms,_,g.contour,[...$.filter(e=>"partition"!==e.host?.kind).map(e=>({x:e.rx,y:e.ry,angle:e.angle,length:e.rlen})),...M],jh,m,Bh,Ih,b)):null,L=$.length&&_.length?function(e,t,i,s,n,o,a,r=1){return[n,o,a,r].every(Number.isFinite)&&o>0&&r>0&&i?.length?th(Kc(e,i,s,n,o,a,r),t):t.map(()=>null)}(t.rooms,$.map(e=>({x:e.rx,y:e.ry,angle:e.angle,length:e.rlen})),_,g.contour,jh,m,Bh,Ih):$.map(()=>null),B=$.length?j_({openings:$,geometries:L,fillsByRoomId:x,idPrefix:`${t.id}-static-data`,groupClass:"opening-tunnels static-opening-tunnels",dataLayer:"data"}):G,j=$.length?j_({openings:$,geometries:L,fillsByRoomId:P,idPrefix:`${t.id}-static-glow`,groupClass:"opening-tunnels glow-base-tunnels static-opening-tunnels",dataLayer:"glow-base"}):G,U=_.length&&H?.paperD?[{path:H.paperD}]:di(t.rooms),V=i.showBorders?H:null,K=e.stageWidth&&w[2]?e.stageWidth/w[2]:1,Y=kl(m),J=!!V&&(wl(V.depthUnits,K)||xl(Y,K)),X=i.color||"#607d8b",Z=i.hideOpenings?[]:f.map(e=>{const t=e.opening,i="passage"!==t.type&&t.contact?r.states?.[t.contact]?.state:null,s=mi(t.type,i,!!t.invert),n=s>0&&!!t.contact,o="gate"===t.type?!t.flip_v:!!t.flip_v,a={type:t.type,length:e.length,angle:e.angle,amount:s,flipH:!!t.flip_h,flipV:!!t.flip_v,base:X,tone:n?"var(--hp-open)":X,cellCm:m,gridPitch:Bh,face:cu(e,o)};return W` - ${w_(a)} - `});return q` -
- ${Lr(F)} - - ${V?W` - - - - `:G} - ${U.map(e=>"path"in e?W``:"poly"in e?W``:W``)} - ${A?W`e.assetLoaded?.(t.bg.href,A)} - transform=${t.bg.angle?`rotate(${t.bg.angle} ${t.bg.x+t.bg.w/2} ${t.bg.y+t.bg.h/2})`:G} - preserveAspectRatio="none" />`:G} - ${C} - ${i.showBorders&&g.contour.length?W``:G} - ${B} - ${D.length?W``:G} - ${j} - ${V?W` - ${V.paths.map(e=>W` - - `)} - `:G} - ${i.showBorders&&g.lines.length?W``:G} - ${Z} - - ${""} -
${I}${R}
-
- `}let sm=null,nm=null,om=0,am=-1,rm=null,lm=[];const cm=new Set,hm=(e,t)=>{"function"==typeof e&&t.push(e)};function dm(){if(sm)return sm;try{const e=JSON.parse(localStorage.getItem("houseplan_card_cfg_v1")||"null");if(e&&e.config&&Array.isArray(e.config.spaces)){const t=e.layout||{};return{config:e.config,rev:e.rev||0,configFingerprint:e.config_fingerprint||Jd(e.config),layout:t,layoutRev:e.layout_rev||0,layoutFingerprint:e.layout_fingerprint||Jd(t),virtualLights:mn(e.virtual_lights,e.rev||0)}}}catch{}return null}function pm(e,t=!1){if(t&&(sm=null,om++,nm))return nm.catch(()=>null).then(()=>pm(e,!1));if(sm)return Promise.resolve(sm);if(nm)return am!==om?nm.catch(()=>null).then(()=>pm(e,!1)):nm;return am=om,nm=async function(e,t){const[i,s]=await Promise.all([e.callWS({type:"houseplan/config/get"}),e.callWS({type:"houseplan/layout/get"})]),n=i?.rev??0,o=sm?gn(sm.virtualLights,i?.virtual_lights,n,!!i&&"virtual_lights"in i):mn(i?.virtual_lights,n),a={config:i?.config??null,rev:n,configFingerprint:Jd(i?.config??null),layout:s?.layout??{},layoutRev:s?.rev??0,layoutFingerprint:Jd(s?.layout??{}),virtualLights:o};t===om&&(sm=a);const r=e.connection;if(r?.subscribeEvents&&rm!==r){for(const e of lm)e();lm=[],rm=r;const e=()=>{sm=null,om++,cm.forEach(e=>e())},t=[];try{if(hm(await r.subscribeEvents(e,"houseplan_config_updated"),t),hm(await r.subscribeEvents(e,"houseplan_layout_updated"),t),hm(await r.subscribeEvents(e=>{if(sm){const t=wn(sm.virtualLights,e?.data);if(t===sm.virtualLights)return;sm={...sm,virtualLights:t}}else om++;cm.forEach(e=>e())},"houseplan_virtual_light_updated"),t),rm===r)lm=t;else for(const e of t)e()}catch{for(const e of t)e();rm===r&&(rm=null)}}return a}(e,om).finally(()=>{nm=null,am=-1}),nm}const um=3300;function _m(e,t){return{sources:e,last:Object.fromEntries(t.map(e=>[e.eid,e.state])),flashTs:0,flashKind:null,timer:0,gen:0,expiresAt:0,alarmActive:"alarm"===dn(t).status}}function mm(e,t,i,s){s(e.timer);const n=_m(t,i);Object.assign(e,n)}function gm(e,t,i,s,n){e.flashTs&&i-e.flashTs"transition"===e.activity)&&(i(e.timer),e.flashTs=0,e.flashKind=null,e.expiresAt=0);if("alarm"===dn(t).status){e.alarmActive||(i(e.timer),e.flashTs=0,e.flashKind=null,e.expiresAt=0);for(const i of t)e.last[i.eid]=i.state;return e.alarmActive=!0,null}if(e.alarmActive){for(const i of t)e.last[i.eid]=i.state;return e.alarmActive=!1,null}let s=null;for(const i of t){const t=pn(e.last[i.eid],i);("event"===t||!s&&t)&&(s=t),e.last[i.eid]=i.state}return s}class vm{constructor(e,t=()=>{}){this._host=e,this._onGateChange=t,this._modality="unknown",this._gate=!1,this._media=null,this._connected=!1,this._onMediaChange=()=>this._syncGate()}get modality(){return this._modality}get hoverEnabled(){return this._gate}connect(e=globalThis.window){this._connected||(this._connected=!0,this._modality="unknown",this._media="function"==typeof e?.matchMedia?e.matchMedia("(any-hover: hover) and (any-pointer: fine)"):null,this._media?.addEventListener?.("change",this._onMediaChange),this._setGate(!1))}disconnect(){this._media?.removeEventListener?.("change",this._onMediaChange),this._media=null,this._connected=!1,this._modality="unknown",this._setGate(!1)}note(e){return this._modality=function(e,t){const i="mouse"===(s=t.pointerType)||"touch"===s||"pen"===s?s:null;var s;return i?"mouse"===i&&t.sourceCapabilities?.firesTouchEvents?e:i:e}(this._modality,e),this._syncGate(),this._modality}suspend(){this._setGate(!1)}_syncGate(){var e,t;this._setGate((e=this._modality,t=!!this._media?.matches,"mouse"===e&&t))}_setGate(e){this._gate!==e&&(this._gate=e,this._host.toggleAttribute("data-pointer-hover",e),this._onGateChange(e))}}class bm extends he{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!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}))}catch{this._spaces=[]}finally{this._spacesLoading=!1}}}get _lang(){return pa(this.hass,this._config?.language)}get _schema(){const e=this._spaces||[];return[e.length?{name:"space",selector:{select:{mode:"dropdown",options:e}}}:{name:"space",selector:{text:{}}},{name:"title",selector:{text:{}}},{name:"show_button",selector:{boolean:{}}},{name:"button_label",selector:{text:{}}},{name:"button_target",selector:{text:{}}},{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:{}}}]}render(){if(!this.hass||!this._config)return G;this._loadSpaces();const e=this._lang,t={space:ua(e,"editor.space"),title:ua(e,"editor.title"),show_button:ua(e,"editor.show_button"),button_label:ua(e,"editor.button_label"),button_target:ua(e,"editor.button_target"),icon_size:ua(e,"editor.icon_size"),show_temperature:ua(e,"editor.show_temperature"),live_states:ua(e,"editor.live_states"),show_signal:ua(e,"editor.show_signal")};return q`t[e.name]||e.name} - @value-changed=${this._valueChanged} - >`}_valueChanged(e){const t={...this._config||{},...e.detail.value};delete t.aspect_ratio;const i=new Event("config-changed",{bubbles:!0,composed:!0});i.detail={config:t},this.dispatchEvent(i)}}bm.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-space-card-editor")||customElements.define("houseplan-space-card-editor",bm);const ym=e=>{history.pushState(null,"",e),((e,t,i)=>{const s=new Event(t,{bubbles:!0,composed:!0});s.detail=i??{},e.dispatchEvent(s)})(window,"location-changed",{replace:!1})};class wm extends he{constructor(){super(...arguments),this._snap=null,this._loading=!1,this._reloadQueued=!1,this._forceReloadQueued=!1,this._reloadRetryTimer=0,this._dayCycleTimer=0,this._dayCycleClockKey="",this._stageWidth=0,this._pendingStageWidth=0,this._stageWidthRaf=0,this._haRegistryConnection=null,this._haRegistryRevision=-1,this._devices=[],this._continuity=this._newContinuityController(),this._continuityHistory=[],this._continuityEpoch=0,this._continuityDataReady=!0,this._continuityPaintToken=-1,this._continuityDisposed=!1,this._renderSnapshotAt=Date.now(),this._hassSequence=0,this._connHooked=null,this._connectionWasLost=!1,this._visibleDeviceSnapshot=null,this._candidateDeviceSnapshot=null,this._stagedDeviceSnapshotToken=-1,this._capturedSnapshotSequence=-1,this._capturedSnapshotDevices=null,this._capturedSnapshotActivity="",this._capturedSnapshotVirtual="",this._activityRuntime=new Map,this._reducedMotion=!1,this._pointerModality=new vm(this),this._onMotionChange=e=>{this._reducedMotion=e.matches,this._capturedSnapshotSequence=-1,this.requestUpdate()},this._onHaRegistryUpdate=()=>{const e=An(this.hass).revision;e!==this._haRegistryRevision&&(this._haRegistryRevision=e,this._refreshDevices(),this._capturedSnapshotSequence=-1,this.requestUpdate())},this._dayCycleTick=()=>{if(!this.isConnected||"hidden"===this.ownerDocument.visibilityState)return;const e=this._dayCycleState();if(!e)return this._dayCycleTimer&&(window.clearInterval(this._dayCycleTimer),this._dayCycleTimer=0),void(this._dayCycleClockKey="");const t=Sr(e);t!==this._dayCycleClockKey&&(this._dayCycleClockKey=t,this.requestUpdate())},this._pageVisibility=e=>{if(this._continuity.visibility(e),this._dayCycleVisibility(e),"hidden"!==e.kind){if(!e.long){const e=Date.now();let t=!1;for(const i of this._activityRuntime.values())!i.flashKind||(i.expiresAt||i.flashTs+3300)>e||(i.flashTs=0,i.flashKind=null,i.expiresAt=0,t=!0);return void(t&&(this._capturedSnapshotSequence=-1,this.requestUpdate()))}Date.now()-this._renderSnapshotAt>1e3&&this._continuity.note("device-snapshot-stale"),this._continuityDataReady=!1,this._continuityPaintToken=-1,this._load(!0)}else this._pointerModality.suspend()},this._onConnLost=()=>{this._connectionWasLost=!0,this._continuityDataReady=!1,this._continuityPaintToken=-1,this._continuity.connectionLost()},this._onConnReady=()=>{!this._connectionWasLost&&this._continuity.hasCompleteFrame?this._beginContinuityCandidate("connection-ready",!1,"plan"):(this._continuityDataReady=!1,this._continuityPaintToken=-1),this._load(!0)},this._onAssetLoaded=(e,t)=>{this._signer.markLoaded(this.hass,e,t),this._continuity.note("asset-ready"),this._continuityPaintToken=-1,"steady"!==this._continuity.state&&this.requestUpdate()},this._loadedOnce=!1,this._signer=new Qd(()=>this.requestUpdate()),this._retryContinuity=()=>{this._continuityDataReady=!1,this._continuityPaintToken=-1,this._continuity.retry(this._continuity.recoveryReason||"plan"),this._load(!0)},this._goToSpace=()=>{const e=(this._config?.button_target||"/plan-doma").replace(/#.*$/,"");ym(`${e}#space=${encodeURIComponent(this._config.space)}`)}}_ensureHaRegistryAuthority(){const e=this.hass?.connection||null;e&&e!==this._haRegistryConnection&&(this._haRegistryRelease?.(),this._haRegistryConnection=e,this._haRegistryRevision=-1,this._haRegistryRelease=Rn(this.hass,this._onHaRegistryUpdate),this._onHaRegistryUpdate())}_newContinuityController(){return new Yd(()=>{this._continuityEpoch++,this.isConnected&&this.requestUpdate()})}_dayCycleState(e=new Date){const t=this._snap?.config;if(!t||!this._config)return null;const i=t.spaces?.find(e=>e.id===this._config?.space)?.settings||{};return"daynight"!==Fr(t.settings,i)?null:$r(this._renderDeviceSnapshot?.hass||this.hass,e)}_syncDayCycleClock(){const e=this._dayCycleState();this._dayCycleClockKey=e?Sr(e):"";const t="clock"===e?.source&&"hidden"!==this.ownerDocument.visibilityState&&this.isConnected;t&&!this._dayCycleTimer?this._dayCycleTimer=window.setInterval(this._dayCycleTick,3e4):!t&&this._dayCycleTimer&&(window.clearInterval(this._dayCycleTimer),this._dayCycleTimer=0)}_dayCycleVisibility(e){"hidden"!==e.kind?(this._dayCycleTick(),this._syncDayCycleClock()):this._dayCycleTimer&&(window.clearInterval(this._dayCycleTimer),this._dayCycleTimer=0)}_hookConnection(){const e=this.hass?.connection;e&&e!==this._connHooked&&(this._connHooked?.removeEventListener?.("ready",this._onConnReady),this._connHooked?.removeEventListener?.("disconnected",this._onConnLost),this._connHooked?.removeEventListener?.("reconnect-error",this._onConnLost),e.addEventListener?.("ready",this._onConnReady),e.addEventListener?.("disconnected",this._onConnLost),e.addEventListener?.("reconnect-error",this._onConnLost),this._connHooked=e)}static getConfigElement(){return document.createElement("houseplan-space-card-editor")}static getStubConfig(e){const t=dm();return{type:"custom:houseplan-space-card",space:Eh(t?.config||null)[0]?.id||"",show_button:!0,live_states:!0,show_temperature:!0,show_signal:!0}}setConfig(e){if(!e||!e.space)throw new Error('houseplan-space-card: "space" is required');this._config={show_button:!0,button_target:"/plan-doma",live_states:!0,show_temperature:!0,show_signal:!0,...e},this._snap=this._snap||dm()}connectedCallback(){var e;this._continuityDisposed&&(this._continuity=this._newContinuityController(),this._continuityDisposed=!1,this._continuityPaintToken=-1),super.connectedCallback(),this._pointerModality.connect(this.ownerDocument.defaultView),this._motionMedia=window.matchMedia?.("(prefers-reduced-motion: reduce)"),this._reducedMotion=!!this._motionMedia?.matches,this._motionMedia?.addEventListener?.("change",this._onMotionChange),this.hass&&this._ensureHaRegistryAuthority(),this._continuityUnsub?.(),this._continuityUnsub=Vd(this.ownerDocument,this._pageVisibility),this._unsub=(e=()=>{this._beginContinuityCandidate("config-event",!1),this._reloadQueued=!0,this._load()},cm.add(e),()=>cm.delete(e)),this._signer.start(()=>this.hass,()=>this._referenced())}disconnectedCallback(){this._pointerModality.disconnect(),this._continuityUnsub?.(),this._continuityUnsub=void 0,this._motionMedia?.removeEventListener?.("change",this._onMotionChange),this._motionMedia=void 0,this._unsub?.(),this._unsub=void 0,window.clearTimeout(this._reloadRetryTimer),this._reloadRetryTimer=0,this._dayCycleTimer&&(window.clearInterval(this._dayCycleTimer),this._dayCycleTimer=0),this._dayCycleClockKey="",this._stageObserver?.disconnect(),this._stageObserver=void 0,this._observedStage=void 0,this._stageWidthRaf&&cancelAnimationFrame(this._stageWidthRaf),this._stageWidthRaf=0,this._pendingStageWidth=0,this._signer.dispose(),this._haRegistryRelease?.(),this._haRegistryRelease=void 0,this._haRegistryConnection=null,this._connHooked?.removeEventListener?.("ready",this._onConnReady),this._connHooked?.removeEventListener?.("disconnected",this._onConnLost),this._connHooked?.removeEventListener?.("reconnect-error",this._onConnLost),this._connHooked=null;for(const e of this._activityRuntime.values())window.clearTimeout(e.timer);this._activityRuntime.clear(),this._continuityHistory=[...this._continuityHistory,...this._continuity.trace].slice(-80),this._continuity.dispose(),this._continuityDisposed=!0,super.disconnectedCallback()}willUpdate(e){e.has("hass")&&this.hass&&(this._hassSequence++,this._renderSnapshotAt=Date.now(),this._continuity.note("hass-snapshot"),this._ensureHaRegistryAuthority(),this._hookConnection()),!this.hass||this._loading||this._snap&&!e.has("hass")||this._snap&&this._loadedOnce||this._load(),(e.has("hass")||e.has("_snap")||e.has("_config")||!this._devices.length)&&this._refreshDevices(),this._captureRenderDeviceSnapshot(),this._continuity.hasCompleteFrame&&"steady"===this._continuity.state&&this._continuity.refreshCompleteFrame(this._frameFingerprint())}_stampActivity(e,t){gm(e,t,Date.now(),window.clearTimeout.bind(window),e=>window.setTimeout(()=>this.requestUpdate(),e))}_syncActivity(e,t,i=this.hass){if(!1===this._config?.live_states){for(const e of this._activityRuntime.values())window.clearTimeout(e.timer);return void this._activityRuntime.clear()}const s=new Set,n=ho(t,e,null,this._snap?.virtualLights);for(const o of e){if(o.hidden)continue;if("icon_ripple"!==Bi(o.marker?.display))continue;s.add(o.id);const a=I_(t,o,e,n,i),r=a.samples,l=z_(t,o,a);let c=this._activityRuntime.get(o.id);if(!c||c.sources!==l){c&&window.clearTimeout(c.timer),c=_m(l,r),this._activityRuntime.set(o.id,c);continue}const h=fm(c,r,window.clearTimeout.bind(window));h&&this._stampActivity(c,h)}for(const[e,t]of this._activityRuntime)s.has(e)||(window.clearTimeout(t.timer),this._activityRuntime.delete(e))}_refreshDevices(){if(!this.hass||!this._snap?.config||!this._config)return;const e=An(this.hass),t=tm({hass:this.hass,registry:e,cfg:this._snap.config,lang:this._lang});this._syncActivity(t,En(this.hass,e),this.hass),this._devices=t}_captureRenderDeviceSnapshot(){if(!this.hass)return;const e=Date.now(),t=[...this._activityRuntime.entries()].map(([t,i])=>`${t}:${i.gen}:${i.flashTs}:`+(i.flashKind&&(i.expiresAt||i.flashTs+3300)>e?1:0)).join("|"),i=this._snap?.virtualLights?`${this._snap.virtualLights.configRev}:${this._snap.virtualLights.rev}`:"";if(this._capturedSnapshotSequence===this._hassSequence&&this._capturedSnapshotDevices===this._devices&&this._capturedSnapshotActivity===t&&this._capturedSnapshotVirtual===i)return;const s=En(this.hass,An(this.hass)),n=new Map,o=new Set(["sun.sun"]),a=new Set,r=new Set,l=e=>{if(!e)return;const t=e.indexOf(":");if(t<0)return void o.add(e);const i=e.slice(0,t),s=e.slice(t+1);"device"===i?a.add(s):"entity"===i&&o.add(s)},c=Eh(this._snap?.config||null).find(e=>e.id===this._config?.space);for(const e of c?.rooms||[])e.area&&r.add(e.area),l(e.settings?.temp_source),l(e.settings?.hum_source);const h=this._snap?.config?.spaces?.find(e=>e.id===this._config?.space);for(const e of h?.openings||[])for(const t of gi(e))o.add(t);const d=ho(s,this._devices,null,this._snap?.virtualLights);for(const e of this._devices)for(const t of[!1,!0])n.set(L_(e.id,t),N_(s,e,{liveStates:!1!==this._config?.live_states,showTemperature:!1!==this._config?.show_temperature,showSignal:t,activityRuntime:this._activityRuntime.get(e.id),sourceDetails:!1,lightDevices:this._devices,lightSources:d,registryHass:this.hass,reducedMotion:this._reducedMotion}));const p=H_({sourceSequence:this._hassSequence,hass:s,devices:this._devices,presentations:n,entityIds:o,deviceIds:a,areaIds:r});this._capturedSnapshotSequence=this._hassSequence,this._capturedSnapshotDevices=this._devices,this._capturedSnapshotActivity=t,this._capturedSnapshotVirtual=i,this._visibleDeviceSnapshot&&"steady"!==this._continuity.state?this._candidateDeviceSnapshot=p:(this._visibleDeviceSnapshot=p,this._candidateDeviceSnapshot=null)}get _renderDeviceSnapshot(){return this._stagedDeviceSnapshotToken===this._continuity.token?this._candidateDeviceSnapshot||this._visibleDeviceSnapshot:this._visibleDeviceSnapshot||this._candidateDeviceSnapshot}_beginContinuityCandidate(e,t,i="plan"){return this._continuityDataReady=t,this._continuityPaintToken=-1,this._stagedDeviceSnapshotToken=-1,this._continuity.beginCandidate(e,i)}_backdropRaw(){return this._snap?.config&&this._config&&Eh(this._snap.config).find(e=>e.id===this._config.space)?.bg?.href||""}_candidateBackdrop(e){return e&&this._config&&Eh(e).find(e=>e.id===this._config.space)?.bg?.href||""}_assetsReady(){const e=this._backdropRaw();return!e||this._signer.isReady(this.hass,e)}_frameFingerprint(){const e=this._snap;return Xd([e?.rev||0,e?.configFingerprint||Jd(e?.config),e?.layoutRev||0,e?.layoutFingerprint||Jd(e?.layout),e?.virtualLights?`${e.virtualLights.configRev}:${e.virtualLights.rev}`:"",this._config?.space||"",this._stageWidth,this.hass?.themes?.darkMode??this.hass?.themes?.default_theme??""])}_stageValid(){const e=this._observedStage;return!!e&&e.clientWidth>0&&e.clientHeight>0}_settleContinuityFrame(){if(!this._stageValid())return;if(!this._continuity.hasCompleteFrame&&"steady"===this._continuity.state){if(this._assetsReady())return this._renderSnapshotAt=Date.now(),void this._continuity.markCompleteFrame(this._frameFingerprint());this._beginContinuityCandidate("asset-wait",!0,"asset")}if(!this._continuityDataReady)return;if(!["holding","offline-stale","overlay-pending","overlay-visible","candidate-ready"].includes(this._continuity.state))return;const e=this._continuity.token;if(this._candidateDeviceSnapshot&&this._candidateDeviceSnapshot!==this._visibleDeviceSnapshot&&this._stagedDeviceSnapshotToken!==e)return this._stagedDeviceSnapshotToken=e,void this.requestUpdate();this._continuityPaintToken!==e&&(this._continuityPaintToken=e,this._continuity.candidateReady(e)&&this._continuity.commitAfterPaint(e,{updateComplete:()=>this.updateComplete,stageValid:()=>this.isConnected&&this._stageValid(),assetsReady:()=>this._assetsReady(),frameFingerprint:()=>this._frameFingerprint()}).then(t=>{t&&e===this._continuity.token?(this._renderSnapshotAt=Date.now(),this._candidateDeviceSnapshot&&(this._visibleDeviceSnapshot=this._candidateDeviceSnapshot),this._candidateDeviceSnapshot=null,this._stagedDeviceSnapshotToken=-1):e===this._continuity.token&&(this._continuityPaintToken=-1,this._stagedDeviceSnapshotToken=-1,this._candidateDeviceSnapshot=null,this.requestUpdate())}))}updated(){const e=this.renderRoot.querySelector(".hp-static-stage")||void 0;if(e!==this._observedStage)if(this._stageObserver?.disconnect(),this._observedStage=e,e){const t=()=>{const i=e.clientWidth;if(!(i<=0||Math.abs(i-this._stageWidth)<=.5)){if(this._stageWidth<=0)return this._stageWidth=i,void this.requestUpdate();this._pendingStageWidth=i,this._stageWidthRaf||(this._stageWidthRaf=requestAnimationFrame(()=>{this._stageWidthRaf=0;const e=this._pendingStageWidth;this._pendingStageWidth=0,!e||!this._observedStage||this._observedStage.clientWidth<=0||(Math.abs(this._observedStage.clientWidth-e)>.5?t():Math.abs(e-this._stageWidth)<=.5||(this._continuity.hasCompleteFrame&&this._beginContinuityCandidate("stage-resize",!0,"stage-size"),this._stageWidth=e,this.requestUpdate()))}))}};this._stageObserver=new ResizeObserver(t),this._stageObserver.observe(e),t()}else this._stageObserver=void 0;this._syncDayCycleClock(),this._settleContinuityFrame()}async _load(e=!1){if(this.hass){if(this._loading)return this._reloadQueued=!0,void(this._forceReloadQueued||=e);this._loading=!0,this._reloadQueued=!1;try{const t=await pm(this.hass,e),i=!this._snap||this._snap.configFingerprint!==t.configFingerprint,s=!this._snap||this._snap.layoutFingerprint!==t.layoutFingerprint,n=!this._snap||this._snap.virtualLights!==t.virtualLights;if(i&&!await this._signer.prepareImage(this.hass,this._candidateBackdrop(t.config)))return this._continuity.note("asset-failed"),window.clearTimeout(this._reloadRetryTimer),void(this._reloadRetryTimer=window.setTimeout(()=>{this._load(!0)},1e3));window.clearTimeout(this._reloadRetryTimer),this._reloadRetryTimer=0,(i||s)&&this._continuity.hasCompleteFrame&&"steady"===this._continuity.state&&this._beginContinuityCandidate("structural-response",!0),i||s||n?this._snap=t:this._snap&&(this._snap.rev=t.rev,this._snap.layoutRev=t.layoutRev),i&&this._continuity.note("config-candidate",{configRev:t.rev}),s&&this._continuity.note("layout-candidate",{layoutRev:t.layoutRev}),n&&(this._capturedSnapshotSequence=-1),this._loadedOnce=!0,this._connectionWasLost=!1,this._continuityDataReady=!0,this._refreshDevices()}catch{}finally{if(this._loading=!1,this._continuityDataReady=!0,this.requestUpdate(),this._reloadQueued){const e=this._forceReloadQueued;this._reloadQueued=!1,this._forceReloadQueued=!1,this._load(e)}}}}get _lang(){return pa(this.hass,this._config?.language)}getCardSize(){const e=Eh(this._snap?.config||null).find(e=>e.id===this._config?.space);if(e){const t=e.vb[3]/e.vb[2];return Math.max(3,Math.round(8*t))+(!1===this._config?.show_button?0:1)}return 6}_errorCard(e){return q` -
${e}
-
`}_referenced(){return Rs(this._snap?.config)}_renderRecoveryOverlay(){if(!this._continuity.overlayVisible&&"recovery-error"!==this._continuity.state)return G;const e="connection"===this._continuity.recoveryReason;return q`
- - ${ua(this._lang,e?"continuity.restore_connection":"continuity.restore_plan")} - ${"recovery-error"===this._continuity.state?q``:G} -
`}houseplanContinuityTrace(){return[...this._continuityHistory,...this._continuity.trace].slice(-80).map(e=>({...e,...e.stage?{stage:[...e.stage]}:{}}))}render(){if(!this._config)return G;const e=this._snap?.config;if(!e)return this._errorCard(ua(this._lang,"space_card.loading"));const t=this._config.space,i=this._renderDeviceSnapshot,s=im({hass:i?.hass||this.hass,cfg:e,layout:this._snap?.layout||{},spaceId:t,iconSize:this._config.icon_size,stageWidth:this._stageWidth,lang:this._lang,displayUrl:e=>this._signer.display(this.hass,e),assetLoaded:this._onAssetLoaded,registry:i?{revision:i.sourceSequence,authoritative:!1,access:"limited",entities:{...i.hass.entities},devices:{...i.hass.devices},lastSuccess:i.capturedAt}:An(this.hass),devices:[...i?.devices||this._devices],presentations:i?.presentations,activityRuntime:this._activityRuntime,reducedMotion:this._reducedMotion,virtualLights:this._snap?.virtualLights,liveStates:!1!==this._config.live_states,showTemperature:!1!==this._config.show_temperature,showSignal:!1!==this._config.show_signal,inert:this._continuity.overlayBlocksInteraction});if(!s)return this._errorCard(ua(this._lang,"space_card.not_found",{id:t}));const n=Eh(e).find(e=>e.id===t),o=void 0!==this._config.title?this._config.title:n?.title||"",a=!1!==this._config.show_button,r=this._config.button_label||ua(this._lang,"space_card.button"),l=this._continuity.overlayVisible||"recovery-error"===this._continuity.state?this._continuity.recoveryReason:null;return q` - this._pointerModality.note(e)} - @pointerdown=${e=>this._pointerModality.note(e)} - @pointermove=${e=>this._pointerModality.note(e)}> - ${o?q`
${o}
`:G} -
- ${s} - ${this._renderRecoveryOverlay()} -
- ${a?q`
- -
`:G} -
- `}}wm.properties={hass:{attribute:!1},_config:{state:!0},_snap:{state:!0},_continuityEpoch:{state:!0}},wm.styles=[Ke,o` - .hp-static-title { - font-weight: 700; - padding: 10px 14px 6px; - font-size: 16px; - color: var(--primary-text-color); - } - .hp-static-body { - position: relative; - } - .hp-static-stage { - position: relative; - width: 100%; - container-type: inline-size; - overflow: hidden; - pointer-events: none; /* kill ALL interaction on the schematic (§4) */ - background: var(--ha-card-background, var(--card-background-color, #111)); - } - .hp-static-stage svg { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - display: block; - z-index: 1; - } - /* Opaque plan paper — the scene bg_color/day-cycle environment shows only AROUND - the plan (owner 2026-08-03). The static card keeps its historical - canvas colour: the theme card background. Drawn plans paper the room - contours (per-room shapes, fill only, no stroke) — same contract as - the full card. */ - .hp-paper { - fill: var(--ha-card-background, var(--card-background-color, #111)); - stroke: none; - } - .wallbody-fill { - fill: var(--wall-fill, #ffffff); - fill-opacity: var(--wall-fill-op, 1); - fill-rule: evenodd; - stroke: none; - pointer-events: none; - } - .wallbody { - fill: url(#hp-wall-hatch); - fill-rule: evenodd; - stroke: var(--room-stroke, var(--hp-muted, #607d8b)); - stroke-width: calc(0.6px * var(--hp-cell-visual-scale, 1)); - pointer-events: none; - } - .wallbody.solid { - fill: none; - } - .hp-static-stage .devlayer { - position: absolute; - inset: 0; - z-index: 2; - } - .hp-static-foot { - padding: 8px 12px 12px; - pointer-events: auto; /* the button stays clickable */ - } - .hp-static-btn { - width: 100%; - padding: 9px 14px; - border: none; - border-radius: 10px; - background: var(--primary-color, #3ea6ff); - color: var(--text-primary-color, #fff); - font-size: 14px; - font-weight: 600; - cursor: pointer; - } - :host([data-pointer-hover]) .hp-static-btn:hover { - filter: brightness(1.08); - } - .hp-static-error { - padding: 24px; - text-align: center; - color: var(--secondary-text-color, #9aa4ad); - } - `],customElements.get("houseplan-space-card")||customElements.define("houseplan-space-card",wm),window.customCards=window.customCards||[],window.customCards.find(e=>"houseplan-space-card"===e.type)||window.customCards.push({type:"houseplan-space-card",name:"House Plan — Space (static)",description:"Read-only live schematic of a single houseplan space, with a deep-link button.",preview:!1,documentation:"https://github.com/Matysh/houseplan-card"});const km=o` - :host { - --hp-editor-tray-bg-fallback: color-mix(in srgb, var(--card-background-color, var(--hp-bg)) 94%, var(--hp-line)); - --hp-editor-tray-bg: color-mix(in srgb, var(--card-background-color, var(--hp-bg)) 86%, transparent); - --hp-editor-tray-border: color-mix(in srgb, var(--hp-line) 82%, var(--hp-accent)); - --hp-editor-tray-blur: 8px; - --hp-editor-tray-shadow: var(--shadow-2); - } - - /* Contextual editor controls float over the stage. Their appearance must - never alter stage height, viewport fitting or the pinned close action. */ - .editor-secondary-host { - position: absolute; - inset: var(--sp-2) var(--sp-4) auto; - z-index: 70; - display: flex; - justify-content: flex-end; - pointer-events: none; - } - .editor-secondary-host.kind-palette, - .editor-secondary-host.kind-group { - justify-content: flex-start; - } - .editor-secondary { - pointer-events: none; - max-width: 100%; - display: flex; - align-items: center; - gap: var(--sp-3); - padding: var(--sp-3) var(--sp-4); - color: var(--hp-txt); - background: var(--card-background-color, var(--hp-bg)); - background: var(--hp-editor-tray-bg-fallback); - border: 1px solid var(--hp-line); - border-color: var(--hp-editor-tray-border); - border-radius: var(--rad-l); - box-shadow: var(--hp-editor-tray-shadow); - opacity: 0; - visibility: hidden; - transform: translateY(-4px); - transition: - opacity 0.1s ease, - transform 0.1s cubic-bezier(0.2, 0.7, 0.2, 1), - visibility 0s linear 0.1s; - touch-action: auto; - } - .editor-secondary-host.open .editor-secondary { - pointer-events: auto; - opacity: 1; - visibility: visible; - transform: translateY(0); - transition-duration: 0.14s, 0.14s, 0s; - transition-delay: 0s; - } - .editor-secondary-host.blocked .editor-secondary { pointer-events: none; } - @supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { - .editor-secondary { - background: var(--hp-editor-tray-bg); - -webkit-backdrop-filter: blur(var(--hp-editor-tray-blur)); - backdrop-filter: blur(var(--hp-editor-tray-blur)); - } - } - .editor-secondary-content, - .editor-group-items { - display: flex; - align-items: center; - gap: var(--sp-3); - min-width: 0; - } - .editor-context-label { - max-width: 14rem; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - padding-inline-end: var(--sp-4); - border-inline-end: 1px solid var(--hp-line); - color: var(--hp-muted); - font-size: var(--fs-s); - } - .editor-secondary.kind-palette { - width: min(760px, 100%); - align-items: stretch; - padding: 0; - overflow: hidden; - } - .editor-secondary.kind-palette .editor-secondary-content, - .editor-secondary.kind-palette .furnpalette { - width: 100%; - } - .editor-secondary.kind-palette .furnpalette { - max-height: 38vh; - border: 0; - background: transparent; - } - .editor-group-launcher .group-chevron { - --mdc-icon-size: 16px; - transition: transform 0.14s ease; - } - .editor-group-launcher[aria-expanded="true"] .group-chevron { transform: rotate(180deg); } - - .editor-secondary .bdhint { - font-size: var(--fs-s); - color: var(--hp-muted); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - .editor-secondary .dfill { - display: inline-flex; - align-items: center; - gap: var(--sp-2); - font-size: var(--fs-s); - cursor: pointer; - } - .editor-secondary .dfill input[type="checkbox"] { - width: 16px; - height: 16px; - flex: none; - margin: 0; - padding: 0; - } - .editor-secondary hp-color-opacity { flex: 0 0 auto; } - .editor-secondary input { - width: 74px; - background: transparent; - border: 1px solid var(--hp-line); - color: var(--hp-txt); - border-radius: var(--rad-s); - padding: var(--sp-3) var(--sp-4); - font-size: var(--fs-m); - } - .editor-secondary label, - .editor-secondary .hint { - color: var(--hp-muted); - font-size: var(--fs-s); - } - .editor-secondary .drawwall { - display: inline-flex; - align-items: center; - gap: var(--sp-3); - white-space: nowrap; - } - .editor-secondary .drawwall input { width: 4.2em; } - .editor-secondary .drawwall .opl { - color: var(--hp-muted); - font-size: var(--fs-s); - } - - @container (min-width: 560px) and (max-width: 899px) { - .editor-secondary-content, - .editor-group-items { - flex-wrap: wrap; - justify-content: flex-end; - } - } - @container (max-width: 559px) { - .editor-secondary-host { inset-inline: var(--sp-4); } - .editor-secondary { max-width: 100%; } - .editor-secondary-content, - .editor-group-items { - overflow-x: auto; - scrollbar-width: thin; - } - .editor-secondary-content > *, - .editor-group-items > * { flex: none; } - .editor-context-label { display: none; } - } - @media (prefers-reduced-motion: reduce) { - .editor-secondary { transition: none; } - .editor-group-launcher .group-chevron { transition: none; } - } -`,xm=(e,t)=>(e.composedPath?.()||[e.target]).some(e=>(e=>!!e&&"function"==typeof e.matches)(e)&&t(e));class $m{constructor(e){this.host=e,this._openGroupId=null,this._groupGeneration=0,this._renderedContext="",this._focusOwned=!1,this._currentModel=null,this._blocked=!1,this._dismissPointerTarget=null,this._globalDismissListening=!1,this._globalDismissGuard=e=>{this.handleOutsideDismiss(e)}}get hasOpenGroup(){return null!==this._openGroupId}get groupGeneration(){return this._groupGeneration}activeGroup(e){return this._openGroupId&&e.find(e=>e.id===this._openGroupId)||null}reset(){this._openGroupId=null,this._contentAnimation?.cancel(),this._contentAnimation=void 0,this._renderedContext="",this._focusOwned=!1,this._currentModel=null,this._blocked=!1,this._dismissPointerTarget=null,clearTimeout(this._dismissTimer),this._dismissTimer=void 0,this._syncGlobalDismissListener()}closeForNavigation(){const e="palette"===this._currentModel?.kind?this._currentModel.dismiss:void 0,t=!!this._openGroupId||!!e;this._openGroupId=null,e?.(),e&&(this._currentModel=null),this._clearDismissClick(),t&&this.host.requestUpdate(),this._syncGlobalDismissListener()}openPalette(){this._openGroupId&&this.closeGroup(!1)}toggleGroup(e,t){this._openGroupId!==t?e.some(e=>e.id===t)&&("palette"===this._currentModel?.kind&&this._currentModel.dismiss?.(),this._groupGeneration+=1,this._openGroupId=t,this.host.clearTip(),this.host.requestUpdate(),this._syncGlobalDismissListener()):this.closeGroup(!1)}closeGroup(e){const t=this._openGroupId;this._openGroupId=null,this.host.requestUpdate(),this._syncGlobalDismissListener(),e&&t&&this.host.updateComplete().then(()=>{const e=this.host.root(),i=[...e.querySelectorAll("[data-editor-group]")].find(e=>e.dataset.editorGroup===t&&null!==e.offsetParent);(i||e.querySelector(".editbar-tools"))?.focus?.()})}renderGroupLauncher(e,t,i){const s=this._openGroupId===e.id,n=e.items.find(t=>t.id===e.activeItemId&&!t.disabled);return q``}renderGroupModel(e,t,i){const s=e.items.find(e=>!e.disabled)?.id,n=e.items.some(t=>t.id===e.activeItemId&&!t.disabled)?e.activeItemId:void 0;return{contextId:t,kind:"group",ariaLabel:i.openGroup(e.label),visibleLabel:e.label,content:q`
this._groupKeydown(e)}> - ${e.items.map(t=>q``)} -
`}}runContext(e,t,i){e===t&&i()}render(e,t){this._currentModel=e,this._blocked=t;const i=e?.kind||"hidden";return q`
- -
`}afterRender(){const e=this.host.root();this._blocked&&(this._openGroupId?this.closeGroup(!1):"palette"===this._currentModel?.kind&&this._currentModel.dismiss?.()),this._syncGlobalDismissListener();const t=e.querySelector(".editor-secondary"),i=this._renderedContext,s=t?.dataset.contextId||"";if(s&&i&&s!==i&&!window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches&&(this._contentAnimation?.cancel(),this._contentAnimation=t?.querySelector(".editor-secondary-content")?.animate([{opacity:.35,transform:"translateY(-4px)"},{opacity:1,transform:"translateY(0)"}],{duration:100,easing:"ease-out"})),this._renderedContext=s,i&&s!==i&&this._focusOwned){const i=s?t?.querySelector('[tabindex="0"], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled])'):e.querySelector(".editbar-tools");i?.focus?.(),this._focusOwned=!!s}if("palette"===this._currentModel?.kind&&this._currentModel.launcherId){const t=[...e.querySelectorAll("[data-editor-palette]")].find(e=>e.dataset.editorPalette===this._currentModel?.launcherId);t&&null!==t.offsetParent||this._currentModel.dismiss?.()}if(!this._openGroupId)return;const n=[...e.querySelectorAll("[data-editor-group]")].find(e=>e.dataset.editorGroup===this._openGroupId);n&&null!==n.offsetParent||this.closeGroup(!0)}handleOutsideDismiss(e){if(xm(e,e=>e.hasAttribute("data-editor-navigation")))return"pointerdown"===e.type?this.closeForNavigation():"click"===e.type&&this._dismissPointerTarget&&this._clearDismissClick(),!1;if("click"===e.type&&this._dismissPointerTarget){return!!(e.composedPath?.()||[e.target]).includes(this._dismissPointerTarget)&&(this._clearDismissClick(),e.preventDefault(),e.stopImmediatePropagation(),!0)}const t=this._openGroupId||this._blocked||"palette"!==this._currentModel?.kind?null:this._currentModel;if("pointerdown"===e.type&&(this._openGroupId||t)){const i=xm(e,e=>e.classList.contains("editor-secondary")),s=xm(e,e=>!!e.dataset.editorGroup),n=xm(e,e=>!!e.dataset.editorPalette),o=xm(e,e=>e.classList.contains("stage"));if(!(i||s||n||"stay-open-on-canvas"===t?.dismissPolicy&&o))return e.preventDefault(),e.stopImmediatePropagation(),this._dismissPointerTarget=e.target,clearTimeout(this._dismissTimer),this._dismissTimer=window.setTimeout(()=>{this._clearDismissClick()},800),this._openGroupId?this.closeGroup(!1):t?.dismiss?.(),this._syncGlobalDismissListener(),!0}return!1}_activateGroupItem(e,t,i){if(e.id!==this._openGroupId)return;const s=e.items.find(e=>e.id===t);if(!s||s.disabled)return;const n="toggle"===s.role&&"stay-open"===s.closePolicy,o=0===i.detail;n||(this._openGroupId=null,this.host.requestUpdate(),this._syncGlobalDismissListener()),s.invoke(),!n&&o&&this.host.updateComplete().then(()=>{const t=[...this.host.root().querySelectorAll("[data-editor-group]")].find(t=>t.dataset.editorGroup===e.id&&null!==t.offsetParent);t?.focus()})}_groupKeydown(e){if(!["ArrowLeft","ArrowRight","Home","End"].includes(e.key))return;const t=[...e.currentTarget.querySelectorAll(".editor-group-item:not([disabled])")];if(!t.length)return;e.preventDefault();const i=t.indexOf(this.host.root().activeElement),s="Home"===e.key?0:"End"===e.key?t.length-1:"ArrowLeft"===e.key?i<=0?t.length-1:i-1:(i+1)%t.length;t[s]?.focus()}_clearDismissClick(){this._dismissPointerTarget=null,clearTimeout(this._dismissTimer),this._dismissTimer=void 0,this._syncGlobalDismissListener()}_syncGlobalDismissListener(){const e=!this._blocked&&"palette"===this._currentModel?.kind,t=!!this._openGroupId||e||!!this._dismissPointerTarget;t!==this._globalDismissListening&&(this._globalDismissListening=t,t?(window.addEventListener("pointerdown",this._globalDismissGuard,!0),window.addEventListener("click",this._globalDismissGuard,!0)):(window.removeEventListener("pointerdown",this._globalDismissGuard,!0),window.removeEventListener("click",this._globalDismissGuard,!0)))}}const Sm=e=>e.stopPropagation(),Mm=4.166666666666667e-9;function Cm(e){return Number.isFinite(e)?Math.round(e/jh)*jh:e}const Dm=(e,t,i,s)=>Math.hypot(i-e,s-t),Pm=(e,t,i,s,n,o,a,r)=>Math.hypot(Math.max(Math.abs(n-e),Math.abs(n+a-(e+i))),Math.max(Math.abs(o-t),Math.abs(o+r-(t+s)))),Tm=(e,t,i,s,n,o,a)=>{const r=Math.max(Number(s)||0,0)/2,l=Math.PI/180,c=Math.cos(i*l)*r,h=Math.sin(i*l)*r,d=Math.cos(a*l)*r,p=Math.sin(a*l)*r,u=Math.max(Dm(e+c,t+h,n+d,o+p),Dm(e-c,t-h,n-d,o-p)),_=Math.max(Dm(e+c,t+h,n-d,o-p),Dm(e-c,t-h,n+d,o+p));return Math.min(u,_)},Im=e=>{const t=Number(e?.cell_cm);return t>0?t:5};function Rm(e,t){const i=JSON.parse(JSON.stringify(e||[])),s=JSON.parse(JSON.stringify(t||{}));let n=0,o=0,a=0,r=0,l="",c=0,h=0,d=0;const p=(e,t)=>{Number.isFinite(e)&&Number.isFinite(t)&&e!==t&&Math.abs(t-e)<=Mm&&h++},u=(e,t)=>{p(e[0],t[0]),p(e[1],t[1])},_={};let m=5;for(const e of i){const t=Im(e);null!=e?.id&&(_[String(e.id)]=t),t>m&&(m=t)}const g=(e,t,i,s=!1)=>{if(!(e>Mm||s))return;n++,e>a&&(a=e);const o=e*Lh*t;o>r&&(r=o,l=i)};for(const e of i){const t=Im(e),i=null!=e?.id?String(e.id):"";for(const s of e.rooms||[]){o++;let e=0;if(s.poly?.length)s.poly=s.poly.map(t=>{const i=[Cm(t[0]),Cm(t[1])];return e=Math.max(e,Dm(t[0],t[1],i[0],i[1])),u(t,i),i});else if(null!=s.x&&null!=s.y){const t=s.x,i=s.y,n=s.w||0,o=s.h||0,a=Cm(t+n),r=Cm(i+o),l=Cm(t),c=Cm(i),h=Math.max(jh,a-l),d=Math.max(jh,r-c);e=Pm(t,i,n,o,l,c,h,d),s.x=l,s.y=c,s.w=h,s.h=d,p(t,l),p(i,c),p(t+n,l+h),p(i+o,c+d)}g(e,t,i)}for(const s of e.room_drafts||[]){o++;let e=0;const n=s.points||[],a=n.map(t=>{const i=[Cm(t[0]),Cm(t[1])];return e=Math.max(e,Dm(t[0],t[1],i[0],i[1])),i}),r=a.length?[a[0]]:[],l=a.length?[[n[0],a[0]]]:[],c=[];for(let e=0;e+1=2)for(const[e,t]of l)u(e,t);g(e,t,i)}if(Array.isArray(e.room_drafts)){const t=e.room_drafts.length;e.room_drafts=e.room_drafts.filter(e=>e.points?.length>=2),d+=t-e.room_drafts.length,e.room_drafts.length||delete e.room_drafts}for(const s of e.partitions||[]){o++;const n=[s.a[0],s.a[1]],a=[s.b[0],s.b[1]],r=[Cm(n[0]),Cm(n[1])],l=[Cm(a[0]),Cm(a[1])];let c=Math.max(Dm(n[0],n[1],r[0],r[1]),Dm(a[0],a[1],l[0],l[1]));const h=Dm(r[0],r[1],l[0],l[1]),d=(e.openings||[]).filter(e=>"partition"===e.host?.kind&&e.host.id===s.id).every(e=>{const t=Number(e.length),i=Number(e.host.t),s=i*h;return Number.isFinite(t)&&t>0&&Number.isFinite(i)&&i>=0&&i<=1&&s-t/2>=-Mm&&s+t/2<=h+Mm});h>Mm&&d?(s.a=r,s.b=l,u(n,r),u(a,l)):c=0,g(c,t,i)}for(const s of e.wall_columns||[]){o++;const e=[s.center[0],s.center[1]],n=[Cm(e[0]),Cm(e[1])],a=Dm(e[0],e[1],n[0],n[1]);s.center=n,u(e,n),g(a,t,i)}for(const s of e.decor||[]){o++;let e=0;if("line"===s.kind){const t=[s.x1,s.y1],i=[s.x2,s.y2],n=[Cm(t[0]),Cm(t[1])],o=[Cm(i[0]),Cm(i[1])];e=Math.max(Dm(t[0],t[1],n[0],n[1]),Dm(i[0],i[1],o[0],o[1])),s.x1=n[0],s.y1=n[1],s.x2=o[0],s.y2=o[1],u(t,n),u(i,o)}else{const t=s.x,i=s.y,n=s.w,o=s.h,a=Cm(t),r=Cm(i);if(null!=s.w&&null!=s.h){const l=Cm(t+n),c=Cm(i+o),h=Math.max(jh,l-a),d=Math.max(jh,c-r);e=Pm(t,i,n,o,a,r,h,d),s.w=h,s.h=d,p(t+n,a+h),p(i+o,r+d)}else e=Dm(t,i,a,r);s.x=a,s.y=r,p(t,a),p(i,r)}g(e,t,i)}for(const s of e.openings||[]){if(o++,"partition"===s.host?.kind){const n=(e.partitions||[]).find(e=>e.id===s.host.id),o=Number(s.host.t);if(!n||!Number.isFinite(o)||o<0||o>1)continue;const a=n.b[0]-n.a[0],r=n.b[1]-n.a[1];if(Math.hypot(a,r)<=Mm)continue;const l=n.a[0]+a*o,h=n.a[1]+r*o;let d=180*Math.atan2(r,a)/Math.PI;d>=90?d-=180:d<-90&&(d+=180);const p=Number(s.angle),u=!(Number.isFinite(p)&&p===d),_=Tm(s.x,s.y,Number.isFinite(p)?p:d,Number(s.length)||0,l,h,d);s.x=l,s.y=h,s.angle=d,u&&c++,g(_,t,i,u);continue}const n=ui([s.x,s.y],e.rooms||[],.025,{step:jh,length:Number(s.length)||0});if(!n)continue;const a=Number(s.angle),r=!(Number.isFinite(a)&&a===n.angle),l=Tm(s.x,s.y,Number.isFinite(a)?a:n.angle,Number(s.length)||0,n.x,n.y,n.angle);s.x=n.x,s.y=n.y,s.angle=n.angle,r&&c++,g(l,t,i,r)}}for(const[e,t]of Object.entries(s)){if(!t||"object"!=typeof t)continue;const i=t;if("number"!=typeof i.x||"number"!=typeof i.y)continue;o++;const n=Cm(i.x),a=Cm(i.y),r=Dm(i.x,i.y,n,a);s[e]={...i,x:n,y:a},p(i.x,n),p(i.y,a);const l="string"==typeof i.s?i.s:"";g(r,_[l]??m,l)}return{spaces:i,layout:s,report:{moved:n,coordsCanonicalized:h,total:o,maxShift:a,maxShiftCm:r,maxSpace:l,rotated:c,removedDrafts:d},changed:n>0||h>0}}function Am(e,t){const i=t.base+e*t.span;return Math.min(1,Math.max(0,i))}const zm=e=>Array.isArray(e)&&e.length>=2&&Number.isFinite(e[0])&&Number.isFinite(e[1]),Fm=(e,t)=>Math.hypot(e[0]-t[0],e[1]-t[1]);function Nm(e,t,i){const s=i[0]-t[0],n=i[1]-t[1],o=s*s+n*n;if(o<=0)return Fm(e,t);let a=((e[0]-t[0])*s+(e[1]-t[1])*n)/o;return a=Math.max(0,Math.min(1,a)),Math.hypot(e[0]-(t[0]+s*a),e[1]-(t[1]+n*a))}function Em(e,t,i,s,n){for(const s of t)if(s&&!i.has(s.id)&&zm(s.a)&&zm(s.b)&&(Fm(e,s.a)<=n||Fm(e,s.b)<=n))return!0;for(const t of s?.roomPolygons||[])for(let i=0;i0&&r>0))return null;if(Math.abs(n[0]/a*(o[1]/r)-n[1]/a*(o[0]/r))>i)return null;for(const[i,n]of[[e.a,t.a],[e.a,t.b],[e.b,t.a],[e.b,t.b]])if(Fm(i,n)<=s)return[(i[0]+n[0])/2,(i[1]+n[1])/2];return null}function Hm(e,t){const i=.05*(Number(t.pitch)>0?Number(t.pitch):1),s=new Map;let n=(e||[]).filter(e=>!!e&&"string"==typeof e.id&&zm(e.a)&&zm(e.b));if(n.length<2)return{partitions:[...e||[]],merged:0,openingMoves:[]};const o=t.seedIds&&t.seedIds.length?new Set(t.seedIds):null;let a=0;for(let e=0;eFm(t,e.at)>i);let h=c[0]??r.a,d=c[c.length-1]??l.b;if(c.length<2&&(h=r.a,d=l.b),d[0]0?(d[0]-h[0])/u:0,m=u>0?(d[1]-h[1])/u:0,g=e=>u>0?((e[0]-h[0])*_+(e[1]-h[1])*m)/u:0;for(const e of[r,l]){const t=g(e.a),i=g(e.b)-t;for(const[n,o]of s)o.toId===e.id&&s.set(n,{fromId:o.fromId,toId:p.id,base:t+o.base*i,span:o.span*i});s.set(e.id,{fromId:e.id,toId:p.id,base:t,span:i})}o&&o.add(p.id),n=n.filter((t,i)=>i!==e.i&&i!==e.j),n.splice(e.i,0,p),a++}return{partitions:n,merged:a,openingMoves:[...s.values()]}}function Lm(e,t,i,s){if(!e?.length||!i.length)return 0;const n=new Map(i.map(e=>[e.fromId,e]));let o=0;for(const i of e){const e=i?.host;if(!e||"partition"!==e.kind)continue;const a=n.get(e.id);if(!a)continue;i.host={...e,id:a.toId,t:Am(e.t,a)},o++;const r=ou(i,t,s.coordScale,s.cellCm,s.gridPitch).resolved;r&&Object.assign(i,pu(i,r,s.coordScale))}return o}function Bm(e,t){const i=t?.excludeDraftId||null;return{roomPolygons:(e?.rooms||[]).map(e=>hi(e)).filter(e=>!!e),columns:e?.wall_columns||[],draftEnds:(e?.room_drafts||[]).flatMap(e=>{if(i&&e?.id===i)return[];const t=e?.points||[];return t.length?[t[0],t[t.length-1]]:[]})}}const jm=e=>!!e&&e.length>=2&&Number.isFinite(e[0])&&Number.isFinite(e[1]),qm=(e,t)=>e[0]-t[0]||e[1]-t[1],Wm=(e,t,i,s)=>[e[0]+t*s,e[1]+i*s],Um=(e,t,i,s)=>(e[0]-t[0])*i+(e[1]-t[1])*s,Gm=(e,t,i,s,n)=>{const o=t[0]-e[0],a=t[1]-e[1],r=Math.hypot(o,a);if(!(r>n))return null;const l=o/r,c=a/r,h=t=>Math.abs((t[0]-e[0])*c-(t[1]-e[1])*l);if(h(i)>n||h(s)>n)return null;const d=Um(i,e,l,c),p=Um(s,e,l,c),u=Math.max(0,Math.min(d,p)),_=Math.min(r,Math.max(d,p));return _-u>n?{lo:u,hi:_}:null},Vm=(e,t,i)=>Math.min(e.hi,t.hi)-Math.max(e.lo,t.lo)>i,Km=e=>{const t=new Set(["id","a","b","cm"]);return Object.keys(e||{}).every(e=>t.has(e))},Ym=(e,t,i,s)=>{const n=e.b[0]-e.a[0],o=e.b[1]-e.a[1],a=Math.hypot(n,o);if(!(a>s))return!0;const r=n/a,l=o/a,c=Ml(e.cm,i.cellCm,i.gridPitch)/2;return t.some(t=>{const n=t?.center;if(!jm(n))return!0;const o=Um(n,e.a,r,l),h=Math.abs((n[0]-e.a[0])*l-(n[1]-e.a[1])*r),d=Ml(Number(t.cm),i.cellCm,i.gridPitch)*("square"===t.shape?Math.SQRT1_2:.5);return!(Number.isFinite(d)&&d>0)||o>=-d-s&&o<=a+d+s&&h<=c+d+s})},Jm=(e,t,i)=>{if(e.host){const s=ou(e,t,i.coordScale,i.cellCm,i.gridPitch).resolved;return s?{center:s.center,angle:s.angle,length:s.length}:null}const s=Number(e.x)*i.coordScale,n=Number(e.y)*i.coordScale,o=Number(e.angle),a=Number(e.length)*i.coordScale;return[s,n,o,a].every(Number.isFinite)&&a>0?{center:[s,n],angle:o,length:a}:null},Xm=(e,t,i)=>{const s=e.angle*Math.PI/180,n=Math.cos(s),o=Math.sin(s),a=t.angle*Math.PI/180,r=Math.cos(a),l=Math.sin(a);if(Math.abs(n*l-o*r)>1e-6)return!1;const c=t.center[0]-e.center[0],h=t.center[1]-e.center[1];if(Math.abs(c*o-h*n)>i)return!1;const d=c*n+h*o;return Math.max(-e.length/2,d-t.length/2)({...e,a:[e.a[0]*t,e.a[1]*t],b:[e.b[0]*t,e.b[1]*t]}),Qm=(e,t,i,s)=>{const n=`~r-${(e=>{let t=2166136261;for(let i=0;i>>0;return t.toString(36)})(`${e}|${t[0].toFixed(9)},${t[1].toFixed(9)}|${i[0].toFixed(9)},${i[1].toFixed(9)}`)}`,o=`${e.slice(0,Math.max(1,64-n.length))}${n}`;let a=o;for(let e=2;s.has(a);e++){const t=`-${e}`;a=`${o.slice(0,64-t.length)}${t}`}return s.add(a),a},eg=(e,t)=>{const i=[];for(const s of e){const e=i[i.length-1];!(e&&Math.abs(e.hi-s.lo)<=t&&e.safe===s.safe)||s.safe&&e.signature!==s.signature?i.push({lo:s.lo,hi:s.hi,a:s.a,b:s.b,safe:s.safe,signature:s.signature,roomIds:s.roomIds,finalCm:s.finalCm}):(e.hi=s.hi,e.b=s.b)}return i};function tg(e,t,i,s,n){const o=i||[],a=Array.isArray(e?.partitions)?e.partitions:[],r=Array.isArray(e?.openings)?e.openings:[],l=Array.isArray(e?.room_drafts)?e.room_drafts:[],c={walls:o,partitions:a,openings:r,roomDrafts:l,partitionsReconciled:0,openingsRehosted:0,removedDrafts:0};if(!a.length&&!l.length)return c;const h=Math.max(2e-4*n.gridPitch,1e-9);let d=o,p=a,u=r;const _=wc(t.rooms,d,s,n.pitch,n.cellCm,n.gridPitch,n.coordScale),m=((e,t,i,s)=>{const n=new Map(e.map(e=>[e.id,e])),o=new Set;for(const e of t){if(!n.get(e.id)||!Array.isArray(e.points)||e.points.length<2||e.segments.length!==e.points.length-1)continue;let t=!0;for(let n=0;n+10)){t=!1;break}const l=Math.hypot(a[0]-o[0],a[1]-o[1]);if(!(l>s)){t=!1;break}const c=i.flatMap(e=>{const t=e.cm>0?e.cm:vl;if(e.open||"outer"!==e.kind&&"shared"!==e.kind||t+se.lo-t.lo||e.hi-t.hi);let h=0;for(const e of c){if(e.lo>h+s)break;if(h=Math.max(h,e.hi),h>=l-s)break}h!m.has(e.id));let f=0,v=0;const b=new Map(t.partitions.map(e=>[e.id,e]));for(const e of[...a].sort((e,t)=>e.id.localeCompare(t.id))){if(!p.some(t=>t.id===e.id))continue;if(!Km(e))continue;const i=b.get(e.id);if(!(i&&jm(i.a)&&jm(i.b)&&Number.isFinite(i.cm)&&i.cm>0))continue;const[o,a]=qm(i.a,i.b)<=0?[[i.a[0],i.a[1]],[i.b[0],i.b[1]]]:[[i.b[0],i.b[1]],[i.a[0],i.a[1]]],r=a[0]-o[0],l=a[1]-o[1],c=Math.hypot(r,l);if(!(c>h))continue;const _=r/c,g=l/c,y=wc(t.rooms,d,s,n.pitch,n.cellCm,n.gridPitch,n.coordScale),w=y.flatMap(e=>{if(e.open||"outer"!==e.kind&&"shared"!==e.kind)return[];const t=Gm(o,a,e.a,e.b,h);return t?[{interval:e,range:t}]:[]});if(!w.length)continue;const k=u.filter(e=>"partition"===e.host?.kind&&e.host.id===i.id).map(e=>({opening:e,resolved:ou(e,t.partitions,n.coordScale,n.cellCm,n.gridPitch).resolved}));if(k.some(e=>!e.resolved))continue;const x=[0,c];for(const{range:e}of w)x.push(e.lo,e.hi);const $=x.slice(),S=new Map;for(const{opening:e,resolved:t}of k){const i=Um(t.center,o,_,g),s={lo:i-t.length/2,hi:i+t.length/2};S.set(e.id,s),$.push(Math.max(0,s.lo),Math.min(c,s.hi))}const M=[...new Set($.filter(e=>Number.isFinite(e)&&e>=-h&&e<=c+h).map(e=>Math.max(0,Math.min(c,e)).toFixed(9)))].map(Number).sort((e,t)=>e-t),C=p.map(e=>Zm(e,n.coordScale)).filter(e=>e.id!==i.id),D=t.room_drafts.filter(e=>!m.has(e.id)),P=[];for(let e=0;e+1h))continue;const r=Wm(o,_,g,s),l=Wm(o,_,g,a),c=w.filter(({range:e})=>e.lo<=s+h&&e.hi>=a-h).map(({interval:e})=>e),d=new Map;for(const e of c)d.set(e.roomId,e);const p=[...d.values()],u=new Set(p.map(e=>e.kind)),m=new Set(p.map(e=>e.cm>0?e.cm:vl)),f=p[0]?.kind,v=[...d.keys()].sort(),b=1===u.size&&1===m.size&&("outer"===f&&1===v.length||"shared"===f&&2===v.length),y={id:i.id,a:r,b:l,cm:i.cm},k=C.some(e=>!!Gm(r,l,e.a,e.b,h)),x=D.some(e=>e.points.some((t,i)=>i+10?p[0].cm:vl:0,T=b?Math.max(S,i.cm):i.cm;P.push({lo:s,hi:a,a:r,b:l,safe:$,signature:$?`${f}|${v.join(",")}|${T}`:"",roomIds:v,roomCm:S,finalCm:T})}if(!P.some(e=>e.safe))continue;const T=new Map;for(const{opening:e,resolved:t}of k){const s=S.get(e.id),o=x.some(e=>e>s.lo+h&&eVm(e,s,h)),r=new Set(a.filter(e=>e.safe).map(e=>e.signature));if(!o&&a.length>0&&a.every(e=>e.safe)&&1===r.size&&du(t,y,h)){T.set(e.id,{resolved:t,signature:[...r][0]});continue}const l=su(i,n.cellCm,n.gridPitch),c={lo:s.lo-l,hi:s.hi+l};for(const e of P)Vm(e,c,h)&&(e.safe=!1,e.signature="")}let I=!0;for(;I;){I=!1;for(const[e,t]of[...T.entries()]){const s=S.get(e),o=P.filter(e=>Vm(e,s,h));if(o.length>0&&o.every(e=>e.safe)&&o.every(e=>e.signature===t.signature))continue;T.delete(e);const a=su(i,n.cellCm,n.gridPitch),r={lo:s.lo-a,hi:s.hi+a};for(const e of P)Vm(e,r,h)&&(e.safe=!1,e.signature="");I=!0}}const R=eg(P,h),A=R.filter(e=>e.safe),z=R.filter(e=>!e.safe);if(!A.length)continue;if(p.length-1+z.length>2e3)continue;let F=d;for(const e of A){const[t,s]=qm(i.a,i.b)<=0?[e.a,e.b]:[e.b,e.a];F=Ul(F,t,s,e.finalCm,n.pitch,n.coordScale)}if(F=xc(t.rooms,F,s,n.pitch,n.cellCm,n.gridPitch,n.coordScale),F.length>500)continue;const N=new Set(p.map(e=>e.id));N.delete(i.id);const E=z.map((e,t)=>{const s=[e.a[0]/n.coordScale,e.a[1]/n.coordScale],o=[e.b[0]/n.coordScale,e.b[1]/n.coordScale],a=0===t?i.id:Qm(i.id,s,o,N);return N.add(a),{id:a,a:s,b:o,cm:i.cm}}),O=p.findIndex(e=>e.id===i.id),H=p.slice();H.splice(O,1,...E);const L=H.map(e=>Zm(e,n.coordScale)),B=new Map;let j=!0;for(const{opening:e,resolved:t}of k){const i=T.get(e.id);if(i){const t=pu(e,i.resolved,n.coordScale),{host:s,...o}=t;B.set(e.id,o);continue}const s=S.get(e.id),o=E[z.findIndex(e=>e.lo<=s.lo+h&&e.hi>=s.hi-h)],a=o&&L.find(e=>e.id===o.id);if(!o||!a){j=!1;break}const r=a.b[0]-a.a[0],l=a.b[1]-a.a[1],c=Math.hypot(r,l),d=Um(t.center,a.a,r/c,l/c),p={...pu(e,t,n.coordScale),host:{kind:"partition",id:o.id,t:d/c}};if(!au(p,L,n.coordScale,n.cellCm,n.gridPitch).resolved){j=!1;break}B.set(e.id,p)}if(!j)continue;const q=u.map(e=>B.get(e.id)||e),W=[...T.keys()].map(e=>B.get(e)),U=W.map(e=>Jm(e,L,n));if(U.some(e=>!e))continue;let G=!1;for(let e=0;ee.id));for(const e of q){if(V.has(e.id))continue;const t=Jm(e,L,n);t&&U.some(e=>Xm(e,t,h))&&(G=!0)}if(G)continue;const K=Kc(t.rooms,F,s,n.pitch,n.cellCm,n.gridPitch,n.coordScale);[...T.entries()].every(([e,t])=>{const i=B.get(e),s=Xc(K,{x:i.x*n.coordScale,y:i.y*n.coordScale,angle:i.angle,length:i.length*n.coordScale},!0),o=[s.negative,s.positive].filter(e=>!!e?.full),a=t.signature.split("|")[1].split(","),r=new Set(o.map(e=>e.roomId));return o.length===a.length&&r.size===a.length&&a.every(e=>r.has(e))})&&(d=F,p=H,u=q,f+=A.length,v+=T.size)}return{walls:d,partitions:p,openings:u,roomDrafts:g,partitionsReconciled:f,openingsRehosted:v,removedDrafts:m.size}}const ig=e=>JSON.parse(JSON.stringify(e)),sg=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),ng=(e,t)=>t.length>0&&t.length<=35&&("space"===e?/^[a-z0-9_-]+$/.test(t):/^[A-Za-z0-9_-]+$/.test(t));function og(e,t){let i=String(t??"");const s=e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),n=new RegExp(`^${s}_(.+)_([0-9a-f]{8})$`);let o=0;for(;o<16;o++){const e=n.exec(i);if(!e)return{root:i,layers:o,bounded:!1};i=e[1]}return{root:i,layers:o,bounded:n.test(i)}}const ag=(e,t,i)=>{const s=e.get(t)||[];s.push(i),e.set(t,s)};const rg=1e-9;class lg extends Error{constructor(e,t=""){super(t?`${e}: ${t}`:e),this.reason=e,this.name="WallSegmentModelError"}}const cg=e=>JSON.parse(JSON.stringify(e)),hg=e=>Array.isArray(e)&&e.length>=2&&Number.isFinite(Number(e[0]))&&Number.isFinite(Number(e[1])),dg=e=>[Number(e[0]),Number(e[1])],pg=e=>`${Number(e[0]).toFixed(12)},${Number(e[1]).toFixed(12)}`,ug=(e,t)=>{const i=pg(e),s=pg(t);return ipg(e)<=pg(t)?[dg(e),dg(t)]:[dg(t),dg(e)],mg=(e,t)=>Math.hypot(t[0]-e[0],t[1]-e[1]),gg=e=>{const t=[[Number(e.points[0][0]),Number(e.points[0][1])]],i=[];for(let s=1;s{const i=new Set,s=e=>{const t=Number(e);if(!Number.isFinite(t))return;const s=t/jh;Math.abs(s-Math.round(s))>=tl&&i.add(t.toFixed(12))};for(const t of Array.isArray(e?.rooms)?e.rooms:[])for(const e of hi(t)||[])s(e[0]),s(e[1]);for(const t of[e?.wall_segments,e?.walls])for(const e of Array.isArray(t)?t:[])s(e?.a?.[0]),s(e?.a?.[1]),s(e?.b?.[0]),s(e?.b?.[1]);for(const e of t)s(e?.[0]),s(e?.[1]);return i.size},vg=(e,t,i)=>{const s=i[0]-t[0],n=i[1]-t[1],o=s*s+n*n;return o>1e-18?((e[0]-t[0])*s+(e[1]-t[1])*n)/o:0},bg=(e,t,i)=>{const s=Math.max(0,Math.min(1,vg(e,t,i)));return Math.hypot(e[0]-(t[0]+(i[0]-t[0])*s),e[1]-(t[1]+(i[1]-t[1])*s))},yg=(e,t,i,s,n=rg)=>{const o=t[0]-e[0],a=t[1]-e[1],r=Math.hypot(o,a);if(r<=n)return 0;const l=t=>Math.abs((t[0]-e[0])*a-(t[1]-e[1])*o)/r;if(l(i)>n||l(s)>n)return 0;const c=vg(i,e,t),h=vg(s,e,t);return Math.max(0,Math.min(1,Math.max(c,h))-Math.max(0,Math.min(c,h)))*r},wg=e=>{const t=(new TextEncoder).encode(e),i=8*t.length,s=64*Math.ceil((t.length+9)/64),n=new Uint8Array(s);n.set(t),n[t.length]=128;const o=new DataView(n.buffer);o.setUint32(s-8,Math.floor(i/4294967296),!1),o.setUint32(s-4,i>>>0,!1);const a=new Uint32Array([1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298]),r=new Uint32Array([1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225]),l=new Uint32Array(64),c=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=c(i,17)^c(i,19)^i>>>10;l[e]=l[e-16]+s+l[e-7]+n>>>0}let[t,i,s,n,h,d,p,u]=r;for(let e=0;e<64;e++){const o=u+(c(h,6)^c(h,11)^c(h,25))+(h&d^~h&p)+a[e]+l[e]>>>0,r=t&i^t&s^i&s;u=p,p=d,d=h,h=n+o>>>0,n=s,s=i,i=t,t=o+((c(t,2)^c(t,13)^c(t,22))+r>>>0)>>>0}r[0]=r[0]+t>>>0,r[1]=r[1]+i>>>0,r[2]=r[2]+s>>>0,r[3]=r[3]+n>>>0,r[4]=r[4]+h>>>0,r[5]=r[5]+d>>>0,r[6]=r[6]+p>>>0,r[7]=r[7]+u>>>0}const h=new Uint8Array(32),d=new DataView(h.buffer);for(let e=0;e{const t="abcdefghijklmnopqrstuvwxyz234567";let i=0,s=0,n="";for(const o of e)for(s=s<<8|o,i+=8;i>=5;)n+=t[s>>>i-5&31],i-=5;return i&&(n+=t[s<<5-i&31]),n},xg=(e,t,i,s,n="")=>{const[o,a]=_g(t,i),r=`${e}|${pg(o)}|${pg(a)}|${[...s].sort().join(",")}${n}`;return`wall-${kg(wg(r)).slice(0,20)}`};let $g=0;const Sg=e=>{for(let t=0;t<1e3;t++){const t=globalThis.crypto?.randomUUID?.(),i=`wall-${t||kg(wg(`${Date.now()}|${$g++}|${Math.random()}`)).slice(0,26)}`;if(!e.has(i))return i}throw new lg("duplicate-id","id factory exhausted")},Mg=e=>{const t=new Map;for(const i of Array.isArray(e?.wall_segments)?e.wall_segments:[])if(i&&"string"==typeof i.id&&hg(i.a)&&hg(i.b)){if(t.has(i.id))throw new lg("duplicate-id",i.id);t.set(i.id,i)}return t},Cg=(e,t,i)=>{const s=(i,s)=>{const n=e[0]-i[0],o=e[1]-i[1];return Math.abs(t[0]-s[0]-n)<=rg&&Math.abs(t[1]-s[1]-o)<=rg?[n,o]:null};return s(i.a,i.b)||s(i.b,i.a)},Dg=(e,t)=>{const i=Array.isArray(e.rooms)?e.rooms:[],s=jd(e.open_spans).map(e=>[e.a[0],e.a[1],e.b[0],e.b[1]]),n=s.length?s:qd(i,null,1,.04*jh,!0),o=[];for(const e of i){const i=hi(e),s=Array.isArray(e?.wall_ids)?e.wall_ids:[];if(i&&s.length===i.length)for(let e=0;e"partition"!==e.host?.kind&&[Number(e.x),Number(e.y)].every(Number.isFinite)&&Number.isFinite(Number(e.angle))&&Number(e.length)>0&&n.some(t=>bg([Number(e.x),Number(e.y)],[t[0],t[1]],[t[2],t[3]])<=.04*jh)):[],l=r.map(e=>{const t=Number(e.angle)*Math.PI/180,i=[Math.cos(t),Math.sin(t)],s=Number(e.length)/2,n=[Number(e.x),Number(e.y)];return{a:[n[0]-i[0]*s,n[1]-i[1]*s],b:[n[0]+i[0]*s,n[1]+i[1]*s]}}),c=(e,t)=>r.some(i=>{const s=[Number(i.x),Number(i.y)];if(!Bl(e,t,Number(i.angle)))return!1;if(bg(s,e,t)>.02*jh)return!1;const n=mg(e,t),o=vg(s,e,t)*n,a=Number(i.length)/2;return o+a>=-rg&&o-a<=n+rg}),h=new Map,d=[];for(const s of i){const r=String(s?.id||""),p=hi(s);if(!r||!p||p.length<3)throw new lg("invalid-room",r);const u=dc(i,r,a,jh,1,[...e.walls||[],...l]);if(!u||u.poly.length<3)throw new lg("invalid-room",r);const _=Array.isArray(s.wall_ids)?s.wall_ids:[],m=_.length===p.length;let g=null,f=m;if(f)for(let e=0;erg||Math.abs(s[1]-g[1])>rg)){f=!1;break}g=s}const v=[];for(let e=0;e2)throw new lg("third-owner",a);const d=m?_[u.parent[e]]:void 0;if("string"==typeof d&&d){const n=t.get(d);f||!n||yg(i,s,n.a,n.b)>rg?l.preferredIds.add(d):l.positionalIds.add(d),l.preferredCarriers.set(d,{a:dg(p[u.parent[e]]),b:dg(p[(u.parent[e]+1)%p.length])})}const g=u.parent[e];l.parentKeys.add(Il(p[g],p[(g+1)%p.length],jh));const b=[(i[0]+s[0])/2,(i[1]+s[1])/2],y=e=>e.some(e=>bg(b,[e[0],e[1]],[e[2],e[3]])<=.04*jh);l.zeroWall||=y(o)||y(n)&&!c(i,s),v.push(a)}const b={...s};delete b.wall_ids,d.push({...b,poly:u.poly.map(e=>[e[0],e[1]]),wall_ids:v})}return{atoms:[...h.values()].sort((e,t)=>e.key.localeCompare(t.key)),rooms:d}},Pg=(e,t,i,s,n)=>{const o=new Map([...i.values()].map(e=>[ug(e.a,e.b),e])),a=(e=>{const t=new Map;for(const i of e)"wall"===i.host?.kind&&t.set(i.host.id,(t.get(i.host.id)||0)+1);return t})(e.openings||[]),r=new Map;for(const e of t){const t=n?.get(e.key);if(t){const s=i.get(t);if(!s)throw new lg("duplicate-id",t);r.set(e,s);continue}if(e.preferredIds.size>1)throw new lg("duplicate-id",[...e.preferredIds].sort().join(","));const s=[...e.preferredIds][0],l=s?i.get(s):void 0;if(l){r.set(e,l);continue}const c=s?e.preferredCarriers.get(s):void 0;if(s&&c){r.set(e,{id:s,a:c.a,b:c.b,cm:0});continue}const h=o.get(e.key);if(h){r.set(e,h);continue}const d=[...i.values()].filter(t=>yg(e.a,e.b,t.a,t.b)>rg),p=[...new Map(d.map(e=>[e.id,e])).values()];if(p.sort((e,t)=>(a.get(t.id)||0)-(a.get(e.id)||0)||mg(t.a,t.b)-mg(e.a,e.b)||e.id.localeCompare(t.id)),p[0])r.set(e,p[0]);else if(1===e.positionalIds.size){const t=i.get([...e.positionalIds][0]);t&&r.set(e,t)}}const l=new Map;for(const[e,t]of r){const i=l.get(t.id)||[];i.push(e),l.set(t.id,i)}for(const[e,t]of l){const s=i.get(e)||r.get(t[0]);if(!s)throw new lg("duplicate-id",e);const n=[(s.a[0]+s.b[0])/2,(s.a[1]+s.b[1])/2];t.sort((e,t)=>{const i=bg(n,e.a,e.b)<=rg?0:1,o=bg(n,t.a,t.b)<=rg?0:1;if(i!==o)return i-o;return(bg(s.a,e.a,e.b)<=rg?0:1)-(bg(s.a,t.a,t.b)<=rg?0:1)||e.key.localeCompare(t.key)}),t[0].id=e}const c=(e=>{const t=new Set;for(const i of[e.rooms,e.openings,e.decor,e.room_drafts,e.partitions,e.wall_columns])for(const e of Array.isArray(i)?i:[])"string"==typeof e?.id&&e.id&&t.add(e.id);for(const i of Array.isArray(e.room_drafts)?e.room_drafts:[])for(const e of Array.isArray(i?.segments)?i.segments:[])"string"==typeof e?.id&&e.id&&t.add(e.id);return t})(e);for(const e of t)if(e.id){if(c.has(e.id))throw new lg("duplicate-id",e.id);c.add(e.id)}const h=t.filter(e=>!e.id);if(s){const t=h.map(t=>{const[i,s]=_g(t.a,t.b),n=`${String(e.id||"")}|${pg(i)}|${pg(s)}|${[...t.owners].sort().join(",")}`,o=kg(wg(n));return{atom:t,seed:n,digest:o,base:`wall-${o.slice(0,20)}`}}).sort((e,t)=>e.digest.localeCompare(t.digest)||e.atom.key.localeCompare(t.atom.key)),i=new Map;for(const e of t){const t=i.get(e.digest);if(t&&t!==e.seed)throw new lg("duplicate-id",e.digest);i.set(e.digest,e.seed);let s=1,n=e.base;for(;c.has(n);)n=`${e.base}-${++s}`;e.atom.id=n,c.add(n)}}else for(const e of h)e.id=Sg(c),c.add(e.id)},Tg=(e,t)=>{if("partition"===e.host?.kind)return null;const i=[Number(e.x),Number(e.y)];if(!i.every(Number.isFinite))return null;const s=t=>{if(!(Number(t.cm)>0))return!1;const s=vg(i,t.a,t.b),n=mg(t.a,t.b),o=Number(e.length)/2;return s>=-rg&&s<=1+rg&&bg(i,t.a,t.b)<=.02*jh&&Bl(t.a,t.b,Number(e.angle))&&Number.isFinite(o)&&o>=0&&s*n-o>=-rg&&s*n+o<=n+rg},n="wall"===e.host?.kind?t.find(t=>t.id===e.host.id):null,o=n&&s(n)?[n]:t.filter(s);if(1!==o.length)return null;const a=o[0];return{kind:"wall",id:a.id,t:Math.max(0,Math.min(1,vg(i,a.a,a.b)))}},Ig=(e,t)=>{const i=[Number(e.x),Number(e.y)];if(!i.every(Number.isFinite))return null;const s=Number(e.length)/2,n=t.filter(t=>{if(!(Number(t.cm)>0))return!1;const n=vg(i,t.a,t.b),o=mg(t.a,t.b);return n>=-rg&&n<=1+rg&&bg(i,t.a,t.b)<=.02*jh&&Bl(t.a,t.b,Number(e.angle))&&Number.isFinite(s)&&s>=0&&n*o-s>=-rg&&n*o+s<=o+rg}),o=(t=>{if(!t.length)return null;const s="wall"===e.host?.kind?t.find(t=>t.id===e.host.id):null;return s||[...t].sort((e,t)=>bg(i,e.a,e.b)-bg(i,t.a,t.b)||Number(t.cm)-Number(e.cm)||(e.idt.id?1:0))[0]})(n);return o?{kind:"wall",id:o.id,t:Math.max(0,Math.min(1,vg(i,o.a,o.b)))}:null},Rg=(e,t,i)=>{const s=Mg(e),{atoms:n,rooms:o}=Dg(e,s);Pg(e,n,s,t,i);const a=n.map(t=>{const i=t.id?s.get(t.id):void 0,n=((e,t,i)=>{if(t.zeroWall)return 0;const s=Array.isArray(e.walls)?e.walls:[],n=Il(t.a,t.b,jh),o=s.filter(e=>Number(e.cm)>0&&(!(e.key!==n&&!t.parentKeys.has(e.key))||!(!hg(e.a)||!hg(e.b))&&yg(t.a,t.b,e.a,e.b)>=mg(t.a,t.b)-rg)).map(e=>Math.max(1,Math.min(100,Number(e.cm))));if(new Set(o.map(e=>e.toFixed(9))).size>1)throw new lg("thickness-conflict",t.key);if(o.length)return o[0];const a=Hl(s,t.a,t.b,jh,1);return a>0?a:Number(i?.cm)>0?Number(i.cm):0})(e,t,i);return{...i||{},id:t.id,a:[...t.a],b:[...t.b],cm:n}}),r=new Map(n.map(e=>[e.key,e.id]));for(const e of o)e.wall_ids=e.wall_ids.map(e=>r.get(e));e.rooms=o,e.wall_segments=a,e.walls=a.filter(e=>e.cm>0).map(e=>({key:Il(e.a,e.b,jh),cm:e.cm,a:[...e.a],b:[...e.b]})),e.walls.length||delete e.walls,delete e.open_spans;for(const t of e.rooms)delete t.open_to;return((e,t)=>{const i=new Set;for(const t of[e.rooms,e.openings,e.decor,e.room_drafts,e.partitions,e.wall_columns,e.wall_segments])for(const e of Array.isArray(t)?t:[])"string"==typeof e?.id&&e.id&&i.add(e.id);for(const s of Array.isArray(e.room_drafts)?e.room_drafts:[]){const n=Array.isArray(s.segments)?s.segments:[];for(let o=0;o{const s=Array.isArray(e.openings)?e.openings:[];for(const e of s){if("partition"===e.host?.kind)continue;if(!e.host&&!i){const i=Tg(e,t);i&&(e.host=i);continue}const s=Tg(e,t)??(i?Ig(e,t):null);if(s)e.host=s;else{if(!i)throw new lg("opening-host",e.id);delete e.host}}})(e,a,t),a.reduce((e,t)=>e+(s.has(t.id)?0:1),0)};function Ag(e,t={}){const i=JSON.stringify(e),s=cg(e);fl(s);let n=0;const o=Number(s?.model_version||0)<9;for(const e of Array.isArray(s?.spaces)?s.spaces:[]){const i=t.lineageSpaceId===String(e?.id||"")?t.lineageHints:void 0;n+=Rg(e,o,i)}return s.model_version=9,fl(s),{config:s,changed:i!==JSON.stringify(s),migratedSegments:n}}const zg=e=>!!e&&"object"==typeof e&&!Array.isArray(e);function Fg(e,t){const i=(e,t)=>{if(Array.isArray(e)&&Array.isArray(t)){const s=new Map;for(const t of e)zg(t)&&"string"==typeof t.id&&t.id&&s.set(t.id,t);const n=t.map((t,n)=>{if(zg(t)&&"string"==typeof t.id&&t.id){const e=s.get(t.id);return e?i(e,t):cg(t)}return n[t,t in e?i(e[t],s):cg(s)]);for(const t of Object.keys(e))delete e[t];for(const[t,i]of s)e[t]=i;return e}return cg(t)};return i(e,t)}const Ng=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),Eg=(e,t,i)=>Math.min(i,Math.max(t,e)),Og=e=>Eh({spaces:[e]})[0],Hg=(e,t,i)=>{const s=i>0?i:1,n=e=>`${(e[0]/s).toFixed(12)},${(e[1]/s).toFixed(12)}`,o=n(e),a=n(t);return o0?a:1,l=Math.max(s*r*.02,1e-9),c=[];for(const t of e||[])for(const e of hi(t)||[])c.push([e[0],e[1]]);const h=[];for(const e of i||[])Array.isArray(e)&&e.length>=4&&e.slice(0,4).every(Number.isFinite)&&h.push([e[0],e[1]],[e[2],e[3]]);const d=(e,t)=>t.some(t=>Math.hypot(e[0]-t[0],e[1]-t[1])<=2*l),p=new Map;for(const a of e||[]){if(!a?.id)continue;const u=yc(e,a.id,t,i,s,n,o,r);if(u)for(let e=0;e0&&_>0&&a===m&&_!==a))continue;const g=u.poly[s],f=u.poly[(s+1)%u.poly.length],v=Math.hypot(f[0]-g[0],f[1]-g[1]);if(!(v>l&&v<.5*o-1e-9*o))continue;if(d(g,h)||d(f,h))continue;if(d(g,c)&&d(f,c))continue;const b=Hg(g,f,r),y=p.get(b);y?y.targets.add(a):p.set(b,{a:[g[0],g[1]],b:[f[0],f[1]],targets:new Set([a])})}}}let u=t.slice();const _=(e,t)=>Math.hypot(e[0]-t[0],e[1]-t[1])<=2*l,m=(e,t)=>{if(!Array.isArray(e.a)||!Array.isArray(e.b)||e.a.length<2||e.b.length<2)return!1;const i=[Number(e.a[0])*r,Number(e.a[1])*r],s=[Number(e.b[0])*r,Number(e.b[1])*r];return!![...i,...s].every(Number.isFinite)&&(_(i,t.a)&&_(s,t.b)||_(i,t.b)&&_(s,t.a))};for(const e of[...p.values()].filter(e=>1===e.targets.size).sort((e,t)=>Hg(e.a,e.b,r).localeCompare(Hg(t.a,t.b,r)))){const t=[...e.targets][0],i=u.filter(t=>m(t,e));if(new Set(i.map(e=>e.cm)).size>1)continue;let n=!1;u=u.map(i=>m(i,e)?(n=!0,i.cm===t?i:{...i,cm:t}):i),n||(u=Ul(u,e.a,e.b,t,s,r))}return u}const Bg=e=>{const t=[];for(const i of e||[]){const e=hi(i);if(e?.length)for(let i=0;i"string"==typeof e?.id?e.id:"").filter(Boolean),h=new Set(c),d=new Map;for(const e of c){const t=og("space",e);t.layers>0&&ng("space",t.root)&&ag(d,t.root,e)}const p=new Map,u=new Map,_=new Map,m=new Map;for(const e of r){const t="string"==typeof e?.id?e.id:"";if(!t)continue;const i=new Map;for(const s of Array.isArray(e.rooms)?e.rooms:[]){const e="string"==typeof s?.id?s.id:"";if(!e)continue;const n=og("room",e);n.layers>0&&ng("room",n.root)&&ag(i,n.root,e),u.has(e)||u.set(e,t),_.has(e)||_.set(e,String(s.name||""));const o="string"==typeof s.area?s.area:"";if(o){const i=m.get(o)||[];i.push({spaceId:t,roomId:e}),m.set(o,i)}}p.set(t,i)}const g=new Set(u.keys()),f=new Map,v=new Map;for(const e of l)"string"==typeof e?.id&&e.id&&(!0===e.removed?v.set(e.id,e):f.set(e.id,e));const b={spaceRefsRemapped:0,roomRefsRemapped:0,positionsRemapped:0,markersDetached:0,positionsUnresolved:0,nestedRefsUnresolved:0,deadSpaceIds:[],orphanRoomLabelsRemoved:0,orphanDevicePositionsRemoved:0,orphanGroupPositionsRemoved:0,liveMissingPositionsRemoved:0,removedPositions:[],liveMissingPositions:[],unverifiedPositions:[]},y=new Set,w=e=>h.has(e)?null:(()=>{const t=og("space",e).root;if(!ng("space",t))return null;const i=d.get(t)||[];return 1===i.length?i[0]:null})(),k=(e,t)=>{const i=og("room",e).root;if(!ng("room",i))return null;const s=p.get(t)?.get(i)||[];return 1===s.length?s[0]:null},x=e=>{const t=i.effectiveAreaByMarker?.[e]||"",s=t&&m.get(t)||[];return 1===s.length?s[0]:null},$=(e,t,i)=>{if("string"==typeof e.room_id&&e.room_id&&!g.has(e.room_id)){const s=t?k(e.room_id,t):null;s?(e.room_id=s,b.roomRefsRemapped++):i?(e.room_id=i.roomId,b.roomRefsRemapped++):!0!==e.removed&&delete e.room_id}const s=e.vacuum?.segment_map;if(s&&"object"==typeof s&&!Array.isArray(s))for(const e of Object.keys(s)){const i=s[e];if("string"!=typeof i||!i||g.has(i))continue;const n=t?k(i,t):null;n?(s[e]=n,b.roomRefsRemapped++):b.nestedRefsUnresolved++}};for(const e of l){const t="string"==typeof e?.id?e.id:"";if(!t)continue;const i="string"==typeof e.space?e.space:"",s=i&&!h.has(i),o=n[t],a="string"==typeof o?.s?o.s:"",r=a&&!h.has(a),l=!0===e.removed;let c=i&&h.has(i)?i:null,d=null,p=!1;if(s){const s=w(i);s?(e.space=s,c=s,p=!0,b.spaceRefsRemapped++):l||"virtual"===e.binding?l||(delete e.space,c=null,b.markersDetached++):(d=x(t),d?(e.space=d.spaceId,c=d.spaceId,b.spaceRefsRemapped++):(delete e.space,c=null,b.markersDetached++))}l&&!p||$(e,c,d),r&&p&&a===i&&c&&(n[t]={...o,s:c},b.positionsRemapped++,y.add(t))}for(const e of Object.keys(n)){if(y.has(e))continue;const t=n[e],i="string"==typeof t?.s?t.s:"";if(!i||h.has(i))continue;const s=w(i);if(s){if(e.startsWith("rl_")){const i=e.slice(3),o=k(i,s);if(o){const i=`rl_${o}`;sg(n,i)||(n[i]={...t,s:s}),delete n[e],b.roomRefsRemapped++,b.positionsRemapped++;continue}}n[e]={...t,s:s},b.positionsRemapped++}}const S=i.ownerRoster,M=!0===S?.authoritative,C=new Set(S?.deviceIds||[]),D=new Set(S?.entityIds||[]),P=new Set(Array.isArray(s.settings?.known_devices)?s.settings.known_devices.filter(e=>"string"==typeof e):[]),T=(e,t="")=>{const i=String(S?.names?.[e]||t||"").trim();return i&&i!==e?i:""},I=(e,t,i,s="",n)=>({id:e,spaceId:t,kind:i,name:T(e,s),...n?{reason:n}:{}}),R=e=>{"room_label"===e?b.orphanRoomLabelsRemoved++:"group"===e?b.orphanGroupPositionsRemoved++:b.orphanDevicePositionsRemoved++},A=new Set;for(const[e,t]of Object.entries(n)){const s="string"==typeof t?.s?t.s:"";if(!s||h.has(s))continue;let o,a;const r=f.get(e),l=v.get(e);if(r)o=I(e,s,"device",String(r.name||"")),a="live";else if(l)o=I(e,s,"device",String(l.name||"")),a="absent";else if(e.startsWith("rl_")){const t=e.slice(3);o=I(e,s,"room_label",_.get(t)||""),a=g.has(t)?"live":"absent"}else if(e.startsWith("lg_")){const t=e.slice(3);o=I(e,s,"group"),a=D.has(t)?"live":M?"absent":"unverified","unverified"===a&&(o.reason="registry_unavailable")}else C.has(e)?(o=I(e,s,"device"),a="live"):P.has(e)?(o=I(e,s,"device"),a=M?"absent":"unverified","unverified"===a&&(o.reason="registry_unavailable")):(o=I(e,s,"unknown","","unknown_owner"),a="unverified");if("absent"!==a){if("live"===a){if(b.liveMissingPositions.push(o),!0===i.removeLiveMissingPositions){delete n[e],b.liveMissingPositionsRemoved++,R(o.kind),b.removedPositions.push(o);continue}}else b.unverifiedPositions.push(o);b.positionsUnresolved++,A.add(s)}else delete n[e],R(o.kind),b.removedPositions.push(o)}return b.deadSpaceIds=[...A].sort((e,t)=>e.localeCompare(t)),{config:s,layout:n,report:b,changed:JSON.stringify(s)!==o||JSON.stringify(n)!==a}}(e,t,i),a=o.config,r=_l(a,o.layout);fl(a),gl(o.layout);const l=JSON.stringify(e||{}),c=JSON.stringify(t||{}),h=Number.isInteger(Number(a.model_version))?Number(a.model_version):0,d=(e=>{let t=0,i=0,s=0;for(const i of e.markers||[]){if("ripple"===i.display&&(i.display="icon_ripple",t++),Array.isArray(i.controls)){const e="string"==typeof i.binding&&i.binding.startsWith("entity:")?i.binding.slice(7):"",s=e?i.controls.filter(t=>t!==e):i.controls;JSON.stringify(s)!==JSON.stringify(i.controls)&&(i.controls=s.length?s:null,t++)}const e=i.vacuum;e&&Ng(e,"trail")&&(["never","cleaning","always"].includes(e.trail_mode)||(e.trail_mode=!1===e.trail?"never":"cleaning"),delete e.trail,t++)}for(const n of e.spaces||[]){const e=n.settings;"glow"===e?.fill_mode&&("boolean"!=typeof e.glow_enabled&&(e.glow_enabled=!0),e.fill_mode="none",t++,i++);for(const e of n.rooms||[]){const i=e.settings;"glow"===i?.fill_mode&&("boolean"!=typeof i.glow&&(i.glow=!0),delete i.fill_mode,t++,s++)}if(Ng(n,"segments")&&(delete n.segments,t++),Ng(n,"plan_scale")){const e=Number(n.plan_scale);Number.isFinite(e)&&e>=Ah&&e<=zh&&(Ng(n,"plan_scale_x")||(n.plan_scale_x=e),Ng(n,"plan_scale_y")||(n.plan_scale_y=e),delete n.plan_scale,t++)}const o=Number(n.cell_cm);Ng(n,"cell_cm")&&(!Number.isFinite(o)||o<.1||o>1e3)&&(n.cell_cm=Number.isFinite(o)&&o>0?Eg(o,.1,1e3):5,t++);const a=Number(n.cell_cm),r=Number.isFinite(a)&&a>0?a:5;for(const e of n.wall_columns||[])if("circle"===e.shape)Ng(e,"angle")&&(delete e.angle,t++);else if("square"===e.shape&&Ng(e,"angle")){const i=((Number(e.angle)||0)%90+90)%90;e.angle!==i&&(e.angle=i,t++)}for(const e of n.decor||[]){if(Ng(e,"width")){const i=Number(e.width);(Ng(e,"width_cm")||Number.isFinite(i))&&(Ng(e,"width_cm")||(e.width_cm=Number(Eg(i/Bh*r,.1,100).toFixed(6))),delete e.width,t++)}if("rect"!==e?.kind&&"ellipse"!==e?.kind||!0!==e.fill||(Ng(e,"fill_color")||(e.fill_color=e.color||"#607d8b",t++),Ng(e,"fill_opacity")||(e.fill_opacity=.25,t++)),"text"!==e?.kind)continue;void 0===e.size_cm?(e.size_cm=Number(Eg(ts*is(e)/Bh*r,.1,2e3).toFixed(6)),delete e.scale,delete e.size,t++):(Ng(e,"scale")||Ng(e,"size"))&&(delete e.scale,delete e.size,t++);let i=String(e.text??"");const s=[...i.matchAll(/\{([^{}\r\n]+)\}/g)].some(e=>!!Yi(e[1]));if(!(Ng(e,"entity")||Ng(e,"attr")||Ng(e,"unit")))continue;const n=String(e.unit??"").trim(),o="state"===String(e.attr??"").trim().toLowerCase()?null:e.attr,a=s||n?"":Ji(e.entity,o);if(s||a){if(a){const t=i.indexOf("{}");i=t>=0?i.slice(0,t)+a+i.slice(t+2):`${i}${i?" ":""}${a}`,e.text=i}delete e.entity,delete e.attr,delete e.unit,t++}}}return{total:t,glowSpaces:i,glowRooms:s}})(a);let p=d.total;const u=(_=a.spaces||[],JSON.parse(JSON.stringify(_)));var _;const m=Rm(a.spaces||[],o.layout);let g=0,f=0,v=0,b="";const y=m.spaces.map(e=>{const t=function(e){const t=JSON.parse(JSON.stringify(e||{}));let i=Array.isArray(t.rooms)?t.rooms:[];const s=new Map;for(const e of i){const t=hi(e);if(t)for(let e=0;ee.key.localeCompare(t.key))){let t=null;for(const s of Pa(e,i)){const e=r.get(ba(s.from));if(e&&e!==ba(s.to))continue;const n=Ma(i,s);if(Ca(i,n,s)){t=s,i=n;break}}t?(r.set(ba(t.from),ba(t.to)),n++,a=Math.max(a,Math.hypot(t.to[0]-t.from[0],t.to[1]-t.from[1]))):o++}t.rooms=i;for(const e of t.room_drafts||[]){if(!Array.isArray(e?.points)||e.points.length<2)continue;const t=e.points.map(e=>[...e]),i=t.slice(0,-1).flatMap((i,s)=>{const n=t[s+1],o=fa(i,n);return o?[{key:`${String(e.id||"")}:${s}`,a:[...i],b:[...n],axis:o.axis}]:[]});let s=t;for(const e of i){let t=null;for(const i of Pa(e,[{poly:s}])){const e=s.map(e=>Sa(e,i)),n=e.length>=4&&ya(e[0],e[e.length-1]),o=n?e.slice(0,-1):e;if(n?xa(o):$a(o)){t=i,s=e;break}}t?(n++,a=Math.max(a,Math.hypot(t.to[0]-t.from[0],t.to[1]-t.from[1]))):o++}e.points=s}for(const e of t.partitions||[]){if(!Array.isArray(e?.a)||!Array.isArray(e?.b))continue;const i=fa(e.a,e.b);if(!i)continue;const s={key:String(e.id||wa(e.a,e.b)),a:[...e.a],b:[...e.b],axis:i.axis};let r=null;for(const i of Pa(s,[{poly:[e.a,e.b]}])){const s=Sa(e.a,i),n=Sa(e.b,i),o=Math.hypot(n[0]-s[0],n[1]-s[1]);if(!(o>0))continue;const a=(t.openings||[]).filter(t=>"partition"===t?.host?.kind&&t.host.id===e.id).every(e=>{const t=Number(e.host.t),i=Number(e.length);return Number.isFinite(t)&&t>=0&&t<=1&&Number.isFinite(i)&&i>0&&t*o-i/2>=-1e-12&&t*o+i/2<=o+1e-12});if(a){r=i,e.a=s,e.b=n;break}}r?(n++,a=Math.max(a,Math.hypot(r.to[0]-r.from[0],r.to[1]-r.from[1]))):o++}return{space:t,report:{wallsStraightened:n,wallsStraightenSkipped:o,maxStraightenShift:a},changed:n>0}}(e);g+=t.report.wallsStraightened,f+=t.report.wallsStraightenSkipped;const i=Number(e?.cell_cm)>0?Number(e.cell_cm):5,s=t.report.maxStraightenShift/jh*i;return s>v&&(v=s,b=String(e?.id||"")),t.space}),w=g?Rm(y,m.layout):{...m,spaces:y};a.spaces=w.spaces;const k=g?{...m.report,moved:m.report.moved+w.report.moved,coordsCanonicalized:m.report.coordsCanonicalized+w.report.coordsCanonicalized,maxShift:Math.max(m.report.maxShift,w.report.maxShift),maxShiftCm:Math.max(m.report.maxShiftCm,w.report.maxShiftCm),maxSpace:w.report.maxShiftCm>m.report.maxShiftCm?w.report.maxSpace:m.report.maxSpace,rotated:m.report.rotated+w.report.rotated,removedDrafts:m.report.removedDrafts+w.report.removedDrafts}:{...m.report};let x=0,$=0,S=0,M=0,C=0,D=0,P=0,T=0;for(let e=0;e0===Number(e.cm)),walls:t.walls||[]}),o=Og(t),r=Og(i);if(!o||!r)continue;const l=Bg(o.rooms),c=Bg(r.rooms),h=.02*Bh,d=(t.wall_segments||[]).filter(e=>0===Number(e.cm)).map(e=>({...e,key:Il(e.a,e.b,jh),cm:0})),p=new Set(d.map(e=>e.key)),_=J_(t,o.rooms,Ih,h);$+=_.length;for(const e of _){const t=[e[0]/Ih,e[1]/Ih],i=[e[2]/Ih,e[3]/Ih],s=Il(t,i,jh);p.has(s)||(p.add(s),d.push({key:s,a:t,b:i,cm:0}))}const m=d.length,g=ql(d,l,c,jh,Ih),f=(e,t,i)=>{const s=i[0]-t[0],n=i[1]-t[1],o=s*s+n*n;if(!(o>0))return Math.hypot(e[0]-t[0],e[1]-t[1]);const a=Math.max(0,Math.min(1,((e[0]-t[0])*s+(e[1]-t[1])*n)/o));return Math.hypot(e[0]-(t[0]+s*a),e[1]-(t[1]+n*a))},v=g.filter(e=>{if(!Array.isArray(e.a)||!Array.isArray(e.b))return!1;const t=e.a.map(e=>e*Ih),i=e.b.map(e=>e*Ih);return c.some(([e,s])=>f(t,e,s)<=4*h&&f(i,e,s)<=4*h)});S+=Math.max(0,m-v.length);const b=(i.wall_segments||[]).filter(e=>Number(e.cm)>0);i.wall_segments=[...b,...v.map(({key:e,...t})=>t)];const y=v.map(e=>[e.a[0]*Ih,e.a[1]*Ih,e.b[0]*Ih,e.b[1]*Ih]),w=Array.isArray(t.walls)?t.walls.length:0;let k=ql(t.walls,l,c,jh,Ih);k=Lg(r.rooms,k,y,jh,Number(i.cell_cm)>0?Number(i.cell_cm):5,Bh,Ih),k=xc(r.rooms,k,y,jh,Number(i.cell_cm)>0?Number(i.cell_cm):5,Bh,Ih),k=Ll(k,i.rooms||[],jh,1,y.map(e=>[e[0]/Ih,e[1]/Ih,e[2]/Ih,e[3]/Ih])),x+=Math.max(0,w-k.length),k.length?i.walls=k:delete i.walls;const I=Hm(i.partitions||[],{pitch:jh,geometry:Bm(i)});I.merged&&(M+=I.merged,i.partitions=I.partitions,Lm(i.openings,i.partitions,I.openingMoves,{coordScale:Ih,cellCm:Number(i.cell_cm)>0?Number(i.cell_cm):5,gridPitch:Bh}));const R=Og(i);if(R){const e=n(i,R,i.walls||[],y,{pitch:jh,cellCm:Number(i.cell_cm)>0?Number(i.cell_cm):5,gridPitch:Bh,coordScale:Ih});e.partitionsReconciled&&(C+=e.partitionsReconciled,D+=e.openingsRehosted,e.partitions.length?i.partitions=e.partitions:delete i.partitions,e.openings.length?i.openings=e.openings:delete i.openings,e.walls.length?i.walls=e.walls:delete i.walls),e.removedDrafts&&(P+=e.removedDrafts,e.roomDrafts.length?i.room_drafts=e.roomDrafts:delete i.room_drafts)}JSON.stringify({zero:(i.wall_segments||[]).filter(e=>0===Number(e.cm)),walls:i.walls||[]})!==s&&T++}let I=0;h<=9&&(I=function(e){const t=Ag(e);return Fg(e,t.config),{...t,config:e}}(a).migratedSegments);const R=fl(a),A=gl(w.layout),z=JSON.stringify(R)!==l||JSON.stringify(A)!==c;h<9&&z&&(R.model_version=9);const F=JSON.stringify(R)!==l||JSON.stringify(A)!==c,N=Number.isInteger(Number(R.model_version))?Number(R.model_version):h,E=F?k:{...k,moved:0,coordsCanonicalized:0,maxShift:0,maxShiftCm:0,maxSpace:"",rotated:0,removedDrafts:0},O=F?o.report:{...o.report,spaceRefsRemapped:0,roomRefsRemapped:0,positionsRemapped:0,markersDetached:0,orphanRoomLabelsRemoved:0,orphanDevicePositionsRemoved:0,orphanGroupPositionsRemoved:0,liveMissingPositionsRemoved:0};return{config:R,layout:A,report:{...E,modelFrom:h,modelTo:N,migrated:F?p:0,glowSpacesMigrated:F?d.glowSpaces:0,glowRoomsMigrated:F?d.glowRooms:0,canonicalized:F?T:0,wallSegmentsMigrated:F?I:0,legacyZeroWallsMigrated:F?$:0,wallsMerged:F?x:0,spansMerged:F?S:0,partitionsMerged:F?M:0,partitionsReconciled:F?C:0,openingsRehosted:F?D:0,redundantDraftsRemoved:F?P:0,wallsStraightened:F?g:0,wallsStraightenSkipped:f,maxStraightenShiftCm:F?v:0,maxStraightenSpace:F?b:"",latticeCoordinatesCanonicalized:F?r.canonicalized:0,latticeCoordinatesFar:F?r.far:0,latticeMaxShift:F?r.maxShift:0,latticeMaxShiftCm:F?r.maxShiftCm:0,latticeSpaces:F?r.spaces:[],...O},changed:F}}function qg(e,t,i){const s=(e?.spaces||[]).find(e=>e?.id===i),n=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),o=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===i||"string"==typeof e.room_id&&n.has(e.room_id)||t?.[e.id]?.s===i).map(e=>e.id))].sort((e,t)=>e.localeCompare(t));return{markerIds:o,count:o.length}}function Wg(e){return e instanceof Error?e.name||"Error":typeof e}function Ug(e){return Jd({id:e?.id??"",cell_cm:e?.cell_cm,rooms:e?.rooms||[],walls:e?.walls||[],wall_segments:e?.wall_segments||[],open_spans:e?.open_spans||[],openings:e?.openings||[],partitions:e?.partitions||[],room_drafts:e?.room_drafts||[],wall_columns:e?.wall_columns||[]})}function Gg(e,t,i,s=Bh,n=1e3){return(Array.isArray(e?.openings)?e.openings:[]).flatMap(e=>{const o={...e,rx:Number(e.x)*n,ry:Number(e.y)*n,rlen:Number(e.length)*n};if(!e.host||"wall"===e.host.kind)return[o];const a=ou(e,t.partitions,n,i,s);return a.resolved?[{...e,rx:a.resolved.center[0],ry:a.resolved.center[1],rlen:a.resolved.length,angle:a.resolved.angle,partitionHost:a.resolved}]:[]})}function Vg(e,t=()=>!0){return e.flatMap(e=>e.host&&e.partitionHost&&t(e)?[lu(e.partitionHost)]:[])}function Kg(e,t,i,s,n,o,a=Bh,r=1e3){const l=wc(t.rooms,[...i],s,n,o,a,r);return e.flatMap(e=>{const t={x:e.rx,y:e.ry,angle:Number(e.angle)||0,length:e.rlen};return e.host&&"wall"!==e.host.kind?e.partitionHost&&du(e.partitionHost,l,2e-4*a)?[t]:[]:[t]})}function Yg(e,t){const i=jh,s=Bh,n=Ih,o=Number.isFinite(Number(t.cellCm))&&Number(t.cellCm)>0?Number(t.cellCm):5,a=Array.isArray(e?.walls)?e.walls:[],r=function(e,t,i=Bh,s=1e3){return qd(t.rooms,e?.open_spans,s,.02*i)}(e,t,s,n),l=function(e,t,i=Bh){if(!t.length)return[];const s=e.filter(e=>e?.id),n=.02*i,o=[];for(let e=0;e{const t=[(e[0]+e[2])/2,(e[1]+e[3])/2];return h.some(e=>Hs(t,e)<4*n)}).map(e=>[...e]);d.length&&o.push({a:a,b:r,segs:d})}return o}(t.rooms,r,s).flatMap(e=>e.segs),c=Gg(e,t,o,s,n),h=Vg(c),d=_h(t,o,s,2e-4*s,h).all;return{space:t,walls:a,openCuts:l,openings:c,roomOpenings:Kg(c,t,a,l,i,o,s,n),partitionCuts:h,physicalBodies:d,wallKeyPitch:i,cellCm:o,gridPitch:s,coordScale:n}}function Jg(e,t,i){const s="string"==typeof e?.title?e.title.trim():"",n=null==e?.id?"":String(e.id).trim();return{spaceId:n,displayName:s||n||i(t+1)}}function Xg(e,t={}){const i=(t.fingerprint||Jd)(e),s=Array.isArray(e?.spaces)?e.spaces:[],n=t.fallbackSpaceName||(e=>`Space ${e}`),o=t.prepareSpace||Yg,a=t.wallPass||qc,r=t.floorPass||Hc,l=[];for(let i=0;i0||d.physicalBodies.length>0;if(!d.space.rooms.length&&!p){l.push({...h,status:"not-applicable"});continue}let u=null;if(p){try{u=a(d.space.rooms,d.walls,d.openCuts,d.roomOpenings,d.wallKeyPitch,d.cellCm,d.gridPitch,d.coordScale,d.physicalBodies)}catch(e){l.push({...h,status:"failed",reason:"wall-exception",detail:Wg(e)});continue}if(null==u){l.push({...h,status:"failed",reason:"wall-null"});continue}if("degraded-extra"===u.status){l.push({...h,status:"failed",reason:"wall-degraded-extra"});continue}if("failed-core"===u.status){l.push({...h,status:"failed",reason:"wall-failed-core"});continue}t.captureWallGeometry?.(d,u)}if(d.space.rooms.length&&null==u?.paperGeom){let e;try{e=r(d.space.rooms,d.walls,d.openCuts,d.wallKeyPitch,d.cellCm,d.gridPitch,d.coordScale)}catch(e){l.push({...h,status:"failed",reason:"floor-exception",detail:Wg(e)});continue}if(null==e){l.push({...h,status:"failed",reason:"floor-null"});continue}}l.push({...h,status:"ok"})}const c=l.filter(e=>"failed"===e.status);return{fingerprint:i,spaces:l,failures:c,ok:0===c.length}}class Zg{constructor(e=50){this._undo=[],this._redo=[],this._limit=Math.max(30,Math.floor(e))}get canUndo(){return this._undo.length>0}get canRedo(){return this._redo.length>0}get undoName(){return this._undo[this._undo.length-1]?.name??null}get redoName(){return this._redo[this._redo.length-1]?.name??null}get size(){return this._undo.length}push(e){this._undo.push(e),this._undo.length>this._limit&&this._undo.splice(0,this._undo.length-this._limit),this._redo=[]}undo(){const e=this._undo.pop()??null;return e&&this._redo.push(e),e}redo(){const e=this._redo.pop()??null;return e&&this._undo.push(e),e}clear(){this._undo=[],this._redo=[]}}const Qg=new WeakMap;const ef=(e,t,i)=>255===e[i+3]&&t.every((t,s)=>Math.abs(e[i+s]-t)<=2);async function tf(e){const t=e.defaultView;if(!t||!t.CSS?.supports?.("mix-blend-mode","screen"))return!1;const i=[128,32,16],s=[16,64,128],n=[9,19,29],o=`\n \n \n \n \n \n `,a=t.URL.createObjectURL(new Blob([o],{type:"image/svg+xml"}));try{const o=await new Promise((e,i)=>{const s=new t.Image,n=t.setTimeout(()=>i(new Error("SVG blend probe timeout")),1e3);s.onload=()=>{t.clearTimeout(n),e(s)},s.onerror=()=>{t.clearTimeout(n),i(new Error("SVG blend probe failed"))},s.src=a}),r=e.createElement("canvas");r.width=4,r.height=1;const l=r.getContext("2d",{willReadFrequently:!0});if(!l)return!1;l.drawImage(o,0,0,4,1);const c=l.getImageData(0,0,4,1).data;return ef(c,i,0)&&ef(c,function(e,t){return[0,1,2].map(i=>Math.round(255-(255-e[i])*(255-t[i])/255))}(i,s),4)&&ef(c,s,8)&&ef(c,n,12)}catch{return!1}finally{t.URL.revokeObjectURL(a)}}const sf={now:()=>performance.now(),requestFrame:e=>requestAnimationFrame(e),cancelFrame:e=>cancelAnimationFrame(e)},nf=e=>Math.max(0,Math.min(1,e)),of=(e,t,i)=>e+(t-e)*i,af=(e,t,i)=>{const s=1-e;return 3*s*s*e*t+3*s*e*e*i+e*e*e};function rf(e,t){return{centerX:e.x+e.w/2,centerY:e.y+e.h/2,pixelsPerUnit:t>0&&e.w>0?t/e.w:1,viewBox:{...e}}}function lf(e,t,i){const s=Math.max(1e-9,e.pixelsPerUnit),n=Math.max(1e-9,t/s),o=Math.max(1e-9,i/s);return{x:e.centerX-n/2,y:e.centerY-o/2,w:n,h:o}}function cf(e){const t=e.trim(),i=/^rgba?\(\s*([\d.]+)[, ]+([\d.]+)[, ]+([\d.]+)(?:\s*[,/]\s*([\d.]+))?\s*\)$/i.exec(t);if(i)return[Number(i[1]),Number(i[2]),Number(i[3]),null==i[4]?1:Number(i[4])];const s=/^#([0-9a-f]{6}|[0-9a-f]{8})$/i.exec(t)?.[1];return s?[parseInt(s.slice(0,2),16),parseInt(s.slice(2,4),16),parseInt(s.slice(4,6),16),8===s.length?parseInt(s.slice(6,8),16)/255:1]:null}function hf(e,t,i){const s=cf(e),n=cf(t);if(!s||!n)return i<.5?e:t;const o=nf(i);return`rgba(${Math.round(of(s[0],n[0],o))}, ${Math.round(of(s[1],n[1],o))}, ${Math.round(of(s[2],n[2],o))}, ${of(s[3],n[3],o).toFixed(3)})`}function df(e,t,i){const s=(e=>{const t=nf(e);if(0===t||1===t)return t;let i=0,s=1,n=t;for(let e=0;e<12;e++)n=(i+s)/2,af(n,.2,.2){const t=this._state;if(!t||t.token!==n)return;const i=nf((e-t.startedAt)/t.duration);t.presented=i>=1?t.to:df(t.from,t.to,i),t.phase=i>=1?"settling":"running",this._hooks.frame(t),i<1?this._raf=this._clock.requestFrame(a):(this._raf=0,this._hooks.settled(t),this._state?.token===n&&(this._state=null))};return this._raf=this._clock.requestFrame(a),n}cancel(e=!0){this._raf&&this._clock.cancelFrame(this._raf),this._raf=0;const t=this._state;this._state=null,this._token++,t&&e&&(t.phase="settling",t.presented=t.to,this._hooks.frame(t),this._hooks.settled(t))}dispose(){this.cancel(!1)}}const uf="houseplan_card_labs_v1",_f=Object.freeze([Object.freeze({id:"iso",issue:89,since:"1.62.0",expires:"1.65.0",summary:"Volumetric plan renderer"})]);function mf(e){const t=/^(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?$/.exec(String(e||"").trim());if(!t)return null;const i=t.slice(1).map(Number);return i.every(e=>Number.isSafeInteger(e))?i:null}function gf(e,t){for(let i=0;i<3;i++)if(e[i]!==t[i])return e[i]e.split(",")).map(e=>e.trim()).filter(Boolean)}function kf(e,t,i,s=_f){const n=function(e,t=_f){const i=mf(e),s=new Map;if(!i||!vf(t))return s;for(const e of t){const t=mf(e.since),n=mf(e.expires);gf(i,t)>=0&&gf(i,n)<0&&s.set(e.id,e)}return s}(i,s),o=new Set(vf(s)?s.map(e=>e.id):[]),a=new Set(function(e){if(!e)return[];try{const t=JSON.parse(e);return Array.isArray(t)?t.filter(e=>"string"==typeof e):[]}catch{return[]}}(t).filter(e=>n.has(e)));let r=!1;const l=e=>{if("off"===e)return a.clear(),void(r=!0);const t=e.startsWith("-"),i=t?e.slice(1):e;o.has(i)&&(r=!0,t||!n.has(i)?a.delete(i):a.add(i))};for(const t of wf(new URLSearchParams(String(e.search||"").replace(/^\?/,""))))l(t);for(const t of wf(bf(e.hash)))l(t);const c=Object.freeze([...a].sort());return{active:c,space:yf(e.hash),persist:r?JSON.stringify(c):void 0,knownUrlOperation:r}}let xf={active:Object.freeze([]),space:""},$f="",Sf=!1,Mf="";const Cf=new Set;function Df(){if("undefined"==typeof window)return xf;const e=kf(window.location,function(){try{return window.localStorage.getItem(uf)}catch{return null}}(),$f);void 0!==e.persist&&function(e){try{window.localStorage.setItem(uf,e)}catch{}}(e.persist),xf={active:e.active,space:e.space},window.__hpLabs=e.active;for(const e of Cf)e(xf);return xf}function Pf(){Df()}function Tf(e){return $f=e,"undefined"==typeof window?xf:(Sf||(Sf=!0,window.addEventListener("hashchange",Pf),window.addEventListener("popstate",Pf)),Df())}const If=Object.freeze({rotDeg:0,tiltDeg:20,xyScale:1,zScale:1,origin:Object.freeze([500,500])});function Rf(e){return[e.rotDeg,e.tiltDeg,e.xyScale,e.zScale,e.origin[0],e.origin[1]].every(Number.isFinite)&&Math.abs(e.xyScale)>1e-12&&Math.abs(Math.cos(e.tiltDeg*Math.PI/180))>1e-12}function Af(e,t,i=If){if(!(Rf(i)&&Number.isFinite(e[0])&&Number.isFinite(e[1])&&Number.isFinite(t)))throw new Error("invalid isometric projection input");const s=i.rotDeg*Math.PI/180,n=i.tiltDeg*Math.PI/180,o=e[0]-i.origin[0],a=e[1]-i.origin[1],r=(o*Math.cos(s)-a*Math.sin(s))*i.xyScale,l=(o*Math.sin(s)+a*Math.cos(s))*i.xyScale;return[i.origin[0]+r,i.origin[1]+l*Math.cos(n)-t*i.zScale*Math.sin(n)]}function zf(e,t=If){if(!Rf(t)||!Number.isFinite(e[0])||!Number.isFinite(e[1]))throw new Error("invalid isometric projection input");const i=t.rotDeg*Math.PI/180,s=t.tiltDeg*Math.PI/180,n=(e[0]-t.origin[0])/t.xyScale,o=(e[1]-t.origin[1])/(t.xyScale*Math.cos(s));return[t.origin[0]+n*Math.cos(i)+o*Math.sin(i),t.origin[1]-n*Math.sin(i)+o*Math.cos(i)]}function Ff(e=If){return`matrix(${function(e=If){if(!Rf(e))throw new Error("invalid isometric camera");const t=e.rotDeg*Math.PI/180,i=e.tiltDeg*Math.PI/180,s=e.xyScale*Math.cos(t),n=-e.xyScale*Math.sin(t),o=e.xyScale*Math.sin(t)*Math.cos(i),a=e.xyScale*Math.cos(t)*Math.cos(i);return[s,o,n,a,e.origin[0]-s*e.origin[0]-n*e.origin[1],e.origin[1]-o*e.origin[0]-a*e.origin[1]]}(e).map(e=>Number(e.toFixed(12))).join(" ")})`}function Nf(e,t=If){const{rect:i,wallHeight:s}=e,n=e.openingHeight??s,o=e.floorDepth??0;if(!(i.w>=0)||!(i.h>=0)||!Number.isFinite(s)||!Number.isFinite(n)||!Number.isFinite(o)||s<0||n<0||o<0)throw new Error("invalid isometric frame");const a=[[i.x,i.y],[i.x+i.w,i.y],[i.x+i.w,i.y+i.h],[i.x,i.y+i.h]],r=Math.max(s,n),l=a.flatMap(e=>[Af(e,-o,t),Af(e,r,t)]),c=l.map(e=>e[0]),h=l.map(e=>e[1]),d=Math.min(...c),p=Math.min(...h);return{x:d,y:p,w:Math.max(...c)-d,h:Math.max(...h)-p}}const Ef=e=>`${Number(e[0].toFixed(4))} ${Number(e[1].toFixed(4))}`;function Of(e){let t=0;for(let i=0;iArray.isArray(e)&&Number.isFinite(e[0])&&Number.isFinite(e[1])).map(e=>[Number(e[0]),Number(e[1])]),i=t[t.length-1];return t.length>1&&t[0][0]===i[0]&&t[0][1]===i[1]&&t.pop(),t}(e);if(i.length<3)return[];const s=!t;return Of(i)>0===s?i:[...i].reverse()}function Lf(e,t,i){return e.length?`M ${e.map(e=>Ef(Af(e,i,t))).join(" L ")} Z`:""}function Bf(e,t,i){const s=t[1]-e[1],n=-(t[0]-e[0]),o=i.rotDeg*Math.PI/180;return s*Math.sin(o)+n*Math.cos(o)}function jf(e,t,i=If){if(!Number.isFinite(t)||t<0)throw new Error("invalid floor edge height");const s=(e||[]).map(e=>(e||[]).map((e,t)=>function(e,t){const i=Hf(e,t);if(!i.length)return i;let s=0;for(let e=1;e0)).filter(e=>e.length>=3&&Math.abs(Of(e))>=1e-9)).filter(e=>e.length&&Of(e[0])>0);s.sort((e,t)=>{const i=e[0][0],s=t[0][0];return i[0]-s[0]||i[1]-s[1]||Math.abs(Of(t[0]))-Math.abs(Of(e[0]))});const n=[],o=[];let a=0;for(let e=0;ee.depth-t.depth||e.component-t.component||e.edge-t.edge),{footprintPath:n.join(" "),sides:o,componentCount:s.length,edgeCount:a}}function qf(e,t){return e^=Math.round(64*(Number.isFinite(t)?t:0)),Math.imul(e,16777619)>>>0}const Wf=e=>e.every(Number.isFinite);function Uf(e,t,i,s){const n=e[0]*i,o=e[1]*s,a=t*Math.PI/180;return[n*Math.cos(a)-o*Math.sin(a),n*Math.sin(a)+o*Math.cos(a)]}function Gf(e,t,i,s,n,o,a){const r=e.flipH?-1:1,l="gate"===e.type?1:e.flipV?-1:1,c=(e.type,e.flipV,e.angle,e.face,{ox:0,oy:0}),h=function(e,t){return[e[0]+t[0],e[1]+t[1]]}([e.x+c.ox,e.y+c.oy],Uf(i,e.angle,r,l));return{leaf:t,hinge:h,closedVector:Uf(s,e.angle,r,l),quarterVector:Uf([-s[1],s[0]],e.angle,r,l),turnDeg:n,bottom:o,top:a}}const Vf=e=>`${Number(e[0].toFixed(4))} ${Number(e[1].toFixed(4))}`;function Kf(e,t,i=If){const s=Math.max(0,Math.min(1,Number.isFinite(t)?t:0));return e.leaves.map(t=>{const n=function(e,t){const i=e.turnDeg*t*Math.PI/180;return[e.hinge[0]+e.closedVector[0]*Math.cos(i)+e.quarterVector[0]*Math.sin(i),e.hinge[1]+e.closedVector[1]*Math.cos(i)+e.quarterVector[1]*Math.sin(i)]}(t,s),o=Af(t.hinge,t.bottom,i),a=Af(n,t.bottom,i),r=Af(n,t.top,i),l=Af(t.hinge,t.top,i);return{id:e.id,sourceIndex:e.sourceIndex,type:e.type,leaf:t.leaf,d:`M ${Vf(o)} L ${Vf(a)} L ${Vf(r)} L ${Vf(l)} Z`,shadowD:`M ${Vf(Af(t.hinge,0,i))} L ${Vf(Af(n,0,i))}`,depth:Math.max(o[1],a[1])}})}const Yf={window:120,door:90,passage:90,gate:300};function Jf(e){return Yf[e]}function Xf(e,t){const i=[e[0],e[1]],s=[t[0],t[1]];return function(e,t){return e[0]-t[0]||e[1]-t[1]}(i,s)<=0?{a:i,b:s}:{a:s,b:i}}function Zf(e){const t=new Map;return e.forEach((e,i)=>{if(!e.kind||e.open)return;const s=Xf(e.a,e.b);if(Math.hypot(s.b[0]-s.a[0],s.b[1]-s.a[1])<=1e-9)return;const n=function(e,t){const i=e=>(Math.abs(e)<=5e-10?0:Math.round(1e6*e)/1e6).toFixed(6);return`${i(e[0])},${i(e[1])}>${i(t[0])},${i(t[1])}`}(s.a,s.b),o=t.get(n);if(o)return o.physicalHalfWidth=Math.max(o.physicalHalfWidth,e.half||0),o.sourceOrder=Math.min(o.sourceOrder,i),void(e.partitionHost&&(o.partitionHost&&o.partitionHost.id!==e.partitionHost.id?o.ambiguousPartitionHost=!0:o.partitionHost=e.partitionHost));t.set(n,{segmentKey:n,a:s.a,b:s.b,physicalHalfWidth:Math.max(0,e.half||0),sourceOrder:i,...e.partitionHost?{partitionHost:e.partitionHost}:{}})}),[...t.values()]}function Qf(e,t){if(Math.abs(e.distance-t.distance)>1e-9)return e.distance-t.distance;if(Math.abs(e.perpendicular-t.perpendicular)>1e-9)return e.perpendicular-t.perpendicular;return(e.target.segmentKeyt.target.segmentKey?1:0)||e.target.sourceOrder-t.target.sourceOrder}function ev(e){const t=Zf(e.intervals).map(t=>{const i=function(e,t){const i=t.b[0]-t.a[0],s=t.b[1]-t.a[1],n=Math.hypot(i,s),o=i/n,a=s/n,r=(e[0]-t.a[0])*o+(e[1]-t.a[1])*a,l=Math.max(0,Math.min(n,r)),c=t.a[0]+l*o,h=t.a[1]+l*a;return{along:l,length:n,x:c,y:h,distance:Math.hypot(e[0]-c,e[1]-h),perpendicular:Math.abs((e[0]-t.a[0])*a-(e[1]-t.a[1])*o)}}(e.pointer,t);return{target:t,...i,envelope:Math.max(e.baseTolerance,t.physicalHalfWidth+e.bodyPointerPadding)}}).filter(e=>e.distance<=e.envelope+1e-9).filter(t=>!function(e,t,i,s,n){const o=t.b[0]-t.a[0],a=t.b[1]-t.a[1],r=Math.hypot(o,a);if(!(r>1e-9))return!1;const l=o/r,c=a/r;for(const o of i){if(!o.open&&o.kind)continue;const i=Xf(o.a,o.b),a=i.b[0]-i.a[0],r=i.b[1]-i.a[1],h=Math.hypot(a,r);if(!(h>1e-9))continue;const d=a/h,p=r/h;if(Math.abs(l*p-c*d)>1e-6)continue;if(Math.abs((i.a[0]-t.a[0])*c-(i.a[1]-t.a[1])*l)>n)continue;const u=(e[0]-i.a[0])*d+(e[1]-i.a[1])*p,_=1e-9;if(!(u<=_||u>=h-_)&&Math.abs((e[0]-i.a[0])*p-(e[1]-i.a[1])*d)<=s+1e-9)return!0}return!1}(e.pointer,t.target,e.intervals,t.envelope,Math.max(1e-9,Math.min(e.baseTolerance,.04*e.gridStep)))),i=t.filter(t=>!!t.target.partitionHost&&e.renderedLength+2*t.target.physicalHalfWidth>t.length+1e-9).sort(Qf),s=t.filter(t=>!t.target.partitionHost||e.renderedLength+2*t.target.physicalHalfWidth<=t.length+1e-9).sort(Qf);let n=s[0];if(!n)return{candidate:null,jambBlockedTarget:i[0]?.target||null};const o=s.filter(e=>Math.abs(e.distance-n.distance)<=1e-9&&Math.abs(e.perpendicular-n.perpendicular)<=1e-9),a=o.filter(e=>e.target.partitionHost);if(a.length){if(1!==new Set(a.map(e=>e.target.partitionHost.id)).size||a.some(e=>e.target.ambiguousPartitionHost))return{candidate:null,jambBlockedTarget:null};const e=a[0],t=e.target.b[0]-e.target.a[0],i=e.target.b[1]-e.target.a[1],s=Math.hypot(t,i),r=t/s,l=i/s,c=o.every(t=>{const i=t.target.b[0]-t.target.a[0],s=t.target.b[1]-t.target.a[1],n=Math.hypot(i,s),o=i/n,a=s/n,c=Math.abs(r*a-l*o)<=1e-6,h=Math.abs((t.target.a[0]-e.target.a[0])*l-(t.target.a[1]-e.target.a[1])*r);return c&&h<=1e-9});if(!c)return{candidate:null,jambBlockedTarget:null};n=e}if(n.target.ambiguousPartitionHost)return{candidate:null,jambBlockedTarget:null};const{target:r,length:l}=n,c=r.b[0]-r.a[0],h=r.b[1]-r.a[1],d=c/l,p=h/l,u=r.partitionHost?r.physicalHalfWidth:0,_=Math.min(Math.max(0,e.renderedLength)/2+u,l/2);let m=n.along;const g=Math.max(e.gridStep,1e-9),f=l/2;m=Math.abs(m-f)<=g/2?f:Math.round(m/g)*g,m=Math.max(_,Math.min(l-_,m));const v=r.a[0]+d*m,b=r.a[1]+p*m,y=Math.max(0,e.renderedLength)/2,w=m-y,k=m+y,x=Math.max(0,w),$=Math.max(0,l-k),S=[r.a[0]+d*(w-x/2),r.a[1]+p*(w-x/2)],M=[r.a[0]+d*(k+$/2),r.a[1]+p*(k+$/2)];let C=180*Math.atan2(h,c)/Math.PI;C>=90?C-=180:C<-90&&(C+=180);const D=Math.abs(m-f)<=1e-9;return{candidate:{presetRevision:e.preset.revision,geometryRevision:e.geometryRevision,pointer:[e.pointer[0],e.pointer[1]],type:e.preset.type,lengthCm:e.preset.lengthCm,flipH:e.preset.flipH,flipV:e.preset.flipV,x:v,y:b,angle:C,renderedLength:e.renderedLength,target:r,...r.partitionHost?{host:{...r.partitionHost,t:l>0?m/l:0}}:{},measure:{labels:[{distance:x,midpoint:S},{distance:$,midpoint:M}],guide:D?{x:v,y:b,angle:C}:null}},jambBlockedTarget:null}}function tv(e,t,i,s,n=1e-6){return e.presetRevision===i&&e.geometryRevision===s&&Math.hypot(e.pointer[0]-t[0],e.pointer[1]-t[1])<=n}const iv=(e,t)=>e[0]*t[1]-e[1]*t[0],sv=(e,t)=>e[0]*t[0]+e[1]*t[1],nv=(e,t)=>[e[0]-t[0],e[1]-t[1]],ov=(e,t,i)=>[e[0]+t[0]*i,e[1]+t[1]*i];function av(e,t,i){return Math.abs(iv(e,t))<=i}function rv(e,t,i){return Math.abs(iv(nv(e,t),i))}function lv(e,t,i,s,n){const o=t[0]-e[0],a=t[1]-e[1],r=Math.hypot(o,a);if(!(r>n&&i>0))return[];const l=[o/r,a/r];return function(e,t,i){const s=t.map(([t,i])=>[Math.max(0,Math.min(t,i)),Math.min(e,Math.max(t,i))]).filter(([e,t])=>t>e+i).sort((e,t)=>e[0]-t[0]||e[1]-t[1]),n=[];for(const e of s){const t=n[n.length-1];!t||e[0]>t[1]+i?n.push([...e]):t[1]=Math.max(t[1],e[1])}const o=[];let a=0;for(const[e,t]of n)e>a+i&&o.push([a,e]),a=Math.max(a,t);return e>a+i&&o.push([a,e]),o}(r,s.flatMap(t=>{const s=t.b[0]-t.a[0],o=t.b[1]-t.a[1],a=Math.hypot(s,o);if(!(a>n&&av(l,[s/a,o/a],1e-6)))return[];if(rv(t.a,e,l)>1.1*Math.max(i,n)||rv(t.b,e,l)>1.1*Math.max(i,n))return[];return[[sv(nv(t.a,e),l),sv(nv(t.b,e),l)]]}),n).flatMap(([t,s])=>{const n=Kl({a:ov(e,l,t),b:ov(e,l,s),halfDepth:i});return n?[n]:[]})}function cv(e){const t=e.coordScale??1,i=Math.max(e.epsilon??2e-4*e.gridPitch,1e-9),s=[];for(const i of e.rooms||[]){if(!i?.id)continue;const n=yc(e.rooms,i.id,e.walls,e.openCuts,e.pitch,e.cellCm,e.gridPitch,t);if(!n||n.poly.length<3)continue;const o=lc(n.poly,n.offsets);!o||o.length<3||s.push({roomId:i.id,profile:n,inner:o})}s.sort((e,t)=>e.roomId.localeCompare(t.roomId));const n=(e.roomOpenings||[]).map(e=>{const t=e.angle*Math.PI/180,i=[Math.cos(t),Math.sin(t)],s=Math.max(0,e.length)/2;return{a:ov([e.x,e.y],i,-s),b:ov([e.x,e.y],i,s)}}),o=[];for(const s of Gc(e.rooms,e.walls,e.openCuts,e.pitch,e.cellCm,e.gridPitch,t))for(const e of lv(s.a,s.b,s.depthUnits/2,n,i))o.push({body:e});const a=new Map;for(const t of e.partitionCuts||[]){const e=a.get(t.hostId)||[];e.push(t),a.set(t.hostId,e)}for(const t of e.partitions||[]){const s=Ml(t.cm,e.cellCm,e.gridPitch)/2;for(const e of lv(t.a,t.b,s,a.get(t.id)||[],i))o.push({body:e,partitionId:t.id})}return{rooms:s,boundaries:o,epsilon:i}}function hv(e,t,i,s){const n=e.profile.offsets[t];if(!(n>0))return null;const o=function(e,t){const i=e[t],s=e[(t+1)%e.length],n=s[0]-i[0],o=s[1]-i[1],a=Math.hypot(n,o)||1;return function(e){let t=0;for(let i=0;i=0?[-o/a,n/a]:[o/a,-n/a]}(e.profile.poly,t),a=ov(i.center,o,n),r=[];for(let t=0;ts&&av(i.axis,[l[0]/c,l[1]/c],1e-6)))continue;if(rv(n,a,i.axis)>s||rv(o,a,i.axis)>s)continue;const h=sv(nv(n,a),i.axis),d=sv(nv(o,a),i.axis);r.push([Math.min(h,d),Math.max(h,d)])}const l=function(e,t){e.sort((e,t)=>e[0]-t[0]||e[1]-t[1]);const i=[];for(const s of e){const e=i[i.length-1];!e||s[0]>e[1]+t?i.push([...s]):e[1]=Math.max(e[1],s[1])}return i}(r,s).find(([e,t])=>e<=s&&t>=-s);if(!l)return null;const[c,h]=l,d=ov(a,i.axis,-i.half),p=ov(a,i.axis,i.half),u=Math.max(0,-i.half-c),_=Math.max(0,h-i.half),m=u>0?ov(a,i.axis,c):d,g=_>0?ov(a,i.axis,h):p,f=sv(o,i.normal)>=0?1:-1,v=(t,s,n)=>({from:t,to:s,label:[(t[0]+s[0])/2,(t[1]+s[1])/2],axis:i.axis,labelNormal:o,distance:n,roomId:e.roomId,roomSide:f,source:"room-face"});return{left:v(d,m,u),right:v(p,g,_)}}function dv(e){const t=ov(e.center,e.axis,-e.half),i=ov(e.center,e.axis,e.half),s=ov(e.center,e.axis,Math.min(-e.half,e.targetLo)),n=ov(e.center,e.axis,Math.max(e.half,e.targetHi)),o=(t,i)=>({from:t,to:i,label:[(t[0]+i[0])/2,(t[1]+i[1])/2],axis:e.axis,labelNormal:e.normal,distance:Math.hypot(i[0]-t[0],i[1]-t[1]),source:"host-end"});return[o(t,s),o(i,n)]}function pv(e,t,i,s){const n=nv(i,t),o=sv(n,n);if(!(o>s*s))return Math.hypot(...nv(e,t))<=s;const a=sv(nv(e,t),n)/o;if(a<-s||a>1+s)return!1;const r=ov(t,n,Math.max(0,Math.min(1,a)));return Math.hypot(...nv(e,r))<=s}function uv(e,t,i){let s=!1;for(let n=0,o=t.length-1;ne[1]!=c>e[1]&&e[0]<(l-a)*(e[1]-r)/(c-r)+a&&(s=!s)}return s}function _v(e,t,i,s,n,o){const a=nv(s,i),r=iv(t,a);if(Math.abs(r)<=o)return null;const l=nv(i,e),c=iv(l,a)/r,h=iv(l,t)/r;return c<-o||c>n+o||h<-o||h>1+o?null:Math.max(0,Math.min(n,c))}function mv(e,t,i){const s=dv(i),n="partition"===e.host?.kind?e.host.id:void 0;return[{base:s[0],direction:[-i.axis[0],-i.axis[1]]},{base:s[1],direction:i.axis}].map(({base:e,direction:i})=>{const s=e.distance,o=function(e,t,i,s,n,o){let a=null;for(const r of s)if(!n||r.partitionId!==n){if(uv(e,r.body,o))return 0;for(let s=0;ssv(nv(e,n),i));return{center:n,axis:i,normal:s,half:Math.max(0,e.renderedLength)/2,targetLo:Math.min(...o),targetHi:Math.max(...o)}}(e),s=t.rooms.flatMap(e=>{const s=function(e,t,i){const{profile:s}=e;let n=null;for(let e=0;e0))continue;const o=s.poly[e],a=s.poly[(e+1)%s.poly.length],r=nv(a,o),l=Math.hypot(r[0],r[1]);if(!(l>i&&av(t.axis,[r[0]/l,r[1]/l],1e-6)))continue;if(rv(o,t.center,t.axis)>i||rv(a,t.center,t.axis)>i)continue;const c=sv(nv(o,t.center),t.axis),h=sv(nv(a,t.center),t.axis),d=Math.min(c,h),p=Math.max(c,h);if(d>-t.half+i||p0?d:p<0?-p:0);(!n||u2)return dv(i);if(s.length){const e=s.map(({room:e,edge:s})=>hv(e,s,i,t.epsilon));if(e.some(e=>!e))return dv(i);const n=e;return[...n.map(e=>e.left),...n.map(e=>e.right)]}return mv(e,t,i)}function fv(e){return!(!e.canEdit||e.kiosk||e.fixedFloor)&&("view"!==e.mode&&("mouse"===e.pointerType&&e.spaceCount>1))}const vv="1.68.1",bv={badge:"display.badge",icon_ripple:"display.icon_ripple",value:"display.value",static_icon:"display.static_icon"},yv={badge:"marker.display_hint_badge",icon_ripple:"marker.display_hint_icon_ripple",value:"marker.display_hint_value",static_icon:"marker.display_hint_static_icon"},wv=2e3,kv=1e3,xv=1500,$v=e=>{const t=String(e??"").trim().replace(",",".");if(!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(t))return null;const i=Number(t);return Number.isFinite(i)?i:null},Sv=(e,t)=>{if(!e.has(t))return{hit:!1};const i=e.get(t);return e.delete(t),e.set(t,i),{hit:!0,value:i}},Mv=(e,t,i,s)=>{for(e.delete(t),e.set(t,i);e.size>s;){const t=e.keys().next().value;if(void 0===t)break;e.delete(t)}},Cv=new Map;let Dv=0;const Pv=e=>`${window.innerWidth}x${window.innerHeight}|${location.pathname}|${JSON.stringify(e??{})}`;let Tv=1e4;const Iv="houseplan_card_layout_v1",Rv="houseplan_card_cfg_v1",Av="houseplan_card_zoom_v1",zv="houseplan_card_nav_v1",Fv="houseplan_card_kiosk_v1",Nv="houseplan_card_view_v1",Ev="hp-dialog, hp-help, hp-color-opacity, hp-device-preview",Ov=1e3,Hv=[[0,1],[45,.88],[70,.62],[86,.32],[100,0]],Lv=(e,t)=>{const i=Math.min(e.x,t.x),s=Math.min(e.y,t.y);return{x:i,y:s,w:Math.max(e.x+e.w,t.x+t.w)-i,h:Math.max(e.y+e.h,t.y+t.h)-s}},Bv=new Set(["select","draw","column","merge","split","resize","opening","wallthick","delroom"]),jv=2e3,qv=(e,t,i)=>{const s=new Event(t,{bubbles:!0,composed:!0});s.detail=i??{},e.dispatchEvent(s)},Wv=e=>{history.pushState(null,"",e),qv(window,"location-changed",{replace:!1})},Uv=(e,t)=>{let i,s=null;const n=(...n)=>{clearTimeout(i),s=n,i=window.setTimeout(()=>{i=void 0;const t=s;s=null,t&&e(...t)},t)};return n.flush=()=>{if(void 0===i)return;clearTimeout(i),i=void 0;const t=s;s=null,t&&e(...t)},n.cancel=()=>{clearTimeout(i),i=void 0,s=null},n.pending=()=>void 0!==i,n},Gv=e=>{try{e.target?.setPointerCapture?.(e.pointerId)}catch{}};class Vv extends he{constructor(){super(...arguments),this._space="f1",this._layout={},this._serverStorage=!1,this._loadOk=!1,this._serverCanWrite=null,this._loading=!1,this._loadTries=0,this._serverCfg=null,this._cfgRev=0,this._cfgContentFingerprint="",this._unsubCfg=null,this._unsubLayout=null,this._unsubVirtual=null,this._liveSyncAttempt=null,this._liveSyncGeneration=0,this._liveSyncConnection=null,this._layoutRev=0,this._layoutContentFingerprint="",this._virtualLights=mn(null),this._canOptimizeUndo=!1,this._undoKind=null,this._devices=[],this._regSignature="",this._defPos={},this._newSyncKey="",this._tip=null,this._hoverRoom=null,this._pointerModality=new vm(this,()=>this._syncPointerHoverTargets()),this._devicePressAnimations=new Map,this._selId=null,this._toast="",this._mode="view",this._pendingNavMode=null,this._decorTool="select",this._decorStyle={...Br},this._decorDraft=null,this._decorMove=null,this._decorSel=null,this._decorEraseConfirm=null,this._decorTextDialog=null,this._decorShapeDialog=null,this._backdropDialog=null,this._decorTextSelection={start:0,end:0},this._furnPalette=null,this._dtBox=null,this._dtDrag=null,this._bdDrag=null,this._slide="",this._reducedMotion=!1,this._onMotionChange=e=>{this._reducedMotion=e.matches,this._cancelDevicePressFeedback(),e.matches&&this._modeTransitionPreparing?this._modeTransitionForceAtomic=!0:e.matches&&this._modeTransition.active&&this._cancelModeTransition(!0),this.requestUpdate()},this._editorChromeMode="plan",this._modeTransitionVisual=null,this._modeTransitionPreparing=!1,this._modeTransitionForceAtomic=!1,this._modeTransitionRequest=0,this._modeTransitionTargetZoom=1,this._modeTransitionEditorCamera=null,this._modeTransition=new pf({frame:e=>this._applyModeTransitionFrame(e),settled:e=>this._settleModeTransition(e)}),this._editorSecondary=new $m({root:()=>this.renderRoot,requestUpdate:()=>this.requestUpdate(),updateComplete:()=>this.updateComplete,clearTip:()=>{this._tip=null}}),this._editorSecondaryCopy={groupActive:(e,t)=>this._t("editor.group_active",{group:e,item:t}),openGroup:e=>this._t("editor.open_group",{group:e}),disabledAction:(e,t)=>this._t("editor.disabled_action",{action:e,reason:t})},this._tool="draw",this._geometryHistory=new Zg(50),this._wallDialog=null,this._drawWallField=null,this._activeDraftId=null,this._resumeDraftBySpace={},this._physicalSel=null,this._physicalDialog=null,this._partitionDeleteDialog=null,this._roomDeleteDialog=null,this._physicalDrag=null,this._physicalRotate=null,this._physicalLastTap=null,this._physicalPickCycle=null,this._wallUnionCacheValue=null,this._wallUnionPool=new Map,this._isoGeometryCache=new Map,this._isoFallback=new Set,this._openingTunnelCache=null,this._openingWallIndexCache=new Map,this._openingPlacementIntervalsCache=null,this._openingDimensionContextCache=null,this._planSnapGeometryCache=null,this._planStructuralGeometryCache=null,this._hiddenWallDiagnosticCache=null,this._physicalBodiesCache=null,this._lightPhysicalBodiesCache=null,this._cleanFloorCache=new Map,this._innerContourCache=new Map,this._glowClipCache=new Map,this._glowGeometryWarnings=new Set,this._lightBarrierCache=null,this._lightBarrierPool=new Map,this._glowFeatherUnits=null,this._glowRenderedSources=new Map,this._glowLastAppearance=new Map,this._glowEnteringSources=new Set,this._glowEnterRafs=new Map,this._glowFadeTimers=new Map,this._glowFeatherSuspendUntil=0,this._glowFeatherResumeTimer=0,this._glowSourceSeq=0,this._glowScreenBlend=!1,this._duplicateColumnId=null,this._duplicateColumnTimer=0,this._resize=new pr,this._rszLimitViolation=null,this._path=[],this._cursorPt=null,this._planSnapHover=null,this._mergeSel=null,this._openingPreset=null,this._openingRebindId=null,this._openingPresetRevision=0,this._openingHoverCandidate=null,this._openingJambBlockCm=null,this._openingDialog=null,this._openingInfo=null,this._opDrag=null,this._opMeasure=null,this._mergeDialog=null,this._splitSel=null,this._pendingSplit=null,this._wallFaceBatch=null,this._wallRepairDiagnostic=null,this._wallFaceGraphCache=[],this._areaSel="",this._nameSel="",this._roomDialog=!1,this._roomEditId=null,this._roomFill="",this._roomCustomFill=null,this._roomTempSrc="",this._roomHumSrc="",this._roomSrcOpen=null,this._roomSrcFilter="",this._roomNameScale=1,this._roomLabelScale=1,this._zoom=1,this._view=null,this._zoomBySpace={},this._viewModeSnap=null,this._pointers=new Map,this._panStart=null,this._panLock=null,this._pinchStart=null,this._suppressClick=!1,this._touchContacts=new Map,this._touchSequenceMultitouch=!1,this._touchClickBlockUntil=0,this._connectedPath="",this._routeDepartureHandled=!1,this._onLocationChanged=()=>{this._connectedPath&&location.pathname!==this._connectedPath?this._leaveCardRoute():location.pathname===this._connectedPath&&(this._routeDepartureHandled=!1)},this._touchGestureGuard={capture:!0,handleEvent:e=>this._guardTouchGesture(e)},this._hdrH=118,this._booting=!0,this._bootFading=!1,this._bootSettling=!1,this._bootSettleRaf=0,this._bootLastH=-1,this._bootStart=0,this._bootLastChange=0,this._bootSoft=!1,this._tapConfirm=null,this._onboardingShown=!1,this._rulesDialog=null,this._preflightClipboardFallback=null,this._haIntegrationVersion=null,this._alignDialog=null,this._settingsDialog=null,this._backupExportDialog=null,this._backupImportDialog=null,this._sunRaysCache=null,this._dayCycleTimer=0,this._dayCycleClockKey="",this._compassDrag=!1,this._importDialog=null,this._importQueue=[],this._importTotal=0,this._rulesCompiledSrc="",this._infoCard=null,this._nativeMoreInfoEntity=null,this._deviceInbox=null,this._deviceInboxReturn=null,this._deviceInboxMemo=null,this._markerDialog=null,this._spaceDialog=null,this._keyHandler=e=>this._onKey(e),this._warmVp=null,this._warmVpArmed=!1,this._warmLongReturn=!1,this._warmRevivePending=!1,this._warmGen=++Dv,this._warmKey=null,this._warmSlot=null,this._hashApplied=!1,this._navApplied=!1,this._labs={active:Object.freeze([]),space:""},this._viewPreference={},this._renderProjection="flat",this._kioskScale={icon:1,font:1},this._kioskDialog=!1,this._activityRt=new Map,this._vacRt=new Map,this._vacViewKey="",this._vacLastView=null,this._vacRaf=0,this._vacSrvTrails={},this._vacJumpOnce=!1,this._continuity=this._newContinuityController(),this._continuityHistory=[],this._continuityEpoch=0,this._continuityDataReady=!0,this._continuityPaintToken=-1,this._continuityDisposed=!1,this._renderSnapshotAt=Date.now(),this._hassSequence=0,this._visibleDeviceSnapshot=null,this._candidateDeviceSnapshot=null,this._stagedDeviceSnapshotToken=-1,this._capturedSnapshotSequence=-1,this._capturedSnapshotDevices=null,this._capturedSnapshotLayout=null,this._capturedSnapshotActivity="",this._capturedSnapshotConfigEpoch=-1,this._capturedSnapshotVirtual="",this._lastValidStageSize=null,this._pendingRefitSize=null,this._refitRaf=0,this._pageVisibility=e=>{if(this._continuity.visibility(e),this._dayCycleVisibility(e),"hidden"===e.kind)return this._clearTransientHover(!0),this._cancelDevicePressFeedback(),void(this._modeTransitionPreparing?this._modeTransitionForceAtomic=!0:this._cancelModeTransition(!0));if(this._vacJumpOnce=!0,!e.long){const e=Date.now();let t=!1;for(const i of this._activityRt.values())!i.flashKind||(i.expiresAt||i.flashTs+um)>e||(i.flashTs=0,i.flashKind=null,i.expiresAt=0,t=!0);return void(t&&this.requestUpdate())}Date.now()-this._renderSnapshotAt>1e3&&this._continuity.note("device-snapshot-stale"),this._continuityDataReady=!1,this._continuityPaintToken=-1,this._resumeSettling=!0,this._loading?this.requestUpdate():this._loadFromServer()},this._resumeSettling=!1,this._viewportInvalidAt=0,this._vacFit=null,this._vacAllCamerasFor=null,this._vacAllCameraCache=null,this._vacCalConfirm=null,this._kioskDots=!1,this._cyclePausedUntil=0,this._swipeStart=null,this._tabDrag=null,this._tabDragRelease=null,this._tabSuppressClick=!1,this._tabOrderWarned=!1,this._lastTap=0,this._onLabsSnapshot=e=>{const t=this._effectiveProjection();this._labs=e;const i=this._effectiveProjection();this._convertProjectionView(t,i),this.requestUpdate()},this._onHashChange=()=>{if(this._hasFixedFloor)return;const e=this._hashSpace();if(e&&this._model.find(t=>t.id===e)&&e!==this._space){if(this._wallFaceBatch&&this._roomDialogCancel(),"plan"===this._mode&&"draw"===this._tool&&!this._finishWallChain())return;this._activeDraftId&&(this._resumeDraftBySpace[this._space]=this._activeDraftId),this._commitSpace(e),this._selId=null,this._path=[],this._clearPlanSnapHover(),this._clearOpeningPlacement(!0),this._tool="draw",this._activeDraftId=null,this._draftSegmentCms=[],this._closingWallCm=null,"plan"===this._mode&&"draw"===this._tool&&this._resumeLastDraft(),this._restoreZoom(),this.requestUpdate()}},this._drag=null,this._rlResize=null,this._holdFired=!1,this._retryContinuity=()=>{this._continuityDataReady=!1,this._continuityPaintToken=-1,this._continuity.retry(this._continuity.recoveryReason||"plan"),this._loading||this._loadFromServer()},this._cfgEpoch=0,this._modelCache=null,this._emptySpaceStateActive=!1,this._decorSnapCache=null,this._markerPreviewMemo=null,this._markerPreviewDevicesMemo=null,this._showHidden=!1,this._connHooked=null,this._connectionWasLost=!1,this._haRegistryConnection=null,this._haRegistryRev=-1,this._haBindingCacheKey="",this._planHassMemo=null,this._onHaRegistryUpdate=()=>{const e=An(this.hass);e.revision===this._haRegistryRev&&this._devices.length||(this._haRegistryRev=e.revision,this._planHassMemo=null,this._regSignature="",this._maybeRebuildDevices(),this.requestUpdate())},this._onConnReady=()=>{if(this._loadTries=0,clearTimeout(this._loadRetryTimer),this._loadRetryTimer=void 0,function(e){const t=Cn(e);t&&(Tn(e,t),In(e,t))}(this.hass),this._saveConfigDebounced.pending()&&this._saveConfigDebounced.flush(),this._cfgWriting)return clearTimeout(this._reloadRetry),void(this._reloadRetry=window.setTimeout(this._onConnReady,400));!this._connectionWasLost&&this._continuity.hasCompleteFrame?this._beginContinuityCandidate("connection-ready",!1,"plan"):(this._continuityDataReady=!1,this._continuityPaintToken=-1),this._loading||this._loadFromServer()},this._onConnLost=()=>{this._booting&&!this._continuity.hasCompleteFrame||(this._connectionWasLost=!0,this._continuityDataReady=!1,this._continuityPaintToken=-1,this._continuity.connectionLost())},this._signer=new Qd(()=>this.requestUpdate()),this._dirtyPos=new Set,this._sentPos=new Map,this._persistLayout=Uv(()=>{if(this._serverStorage){const e=[...this._dirtyPos];this._dirtyPos.clear();for(const t of e){const e=ml(this._layout[t]);e&&(this._layout={...this._layout,[t]:e},this._sentPos.set(t,e),this.hass.callWS({type:"houseplan/layout/update",device_id:t,pos:e}).then(e=>this._noteLayoutRev(e)).catch(e=>this._showToast(this._t("toast.pos_save_failed",{err:this._errText(e)}))).finally(()=>{this._sentPos.get(t)===e&&this._sentPos.delete(t)}))}this._cacheSnapshot()}else this._layout=gl(il(this._layout)),localStorage.setItem(Iv,JSON.stringify(this._layout))},600),this._frame=null,this._showFar=!1,this._writesPending=0,this._writeChain=Promise.resolve(),this._pendingPhysicalWrites=new Map,this._junctionBaselineCache=new WeakMap,this._undoGeometry=()=>{if(this._physicalDrag||this._physicalRotate)return void this._cancelPhysicalGesture();if(this._decorDraft)return this._decorDraft=null,void this.requestUpdate();if(this._decorMove||this._dtDrag||this._bdDrag)return void this._cancelDecorGesture();if(this._resize.dragging)return void this._rszCancelDrag();const e=this._geometryHistory.undo();e&&(this._applyGeometryState(e.before,!0)?this._showToast(this._t("history.undone",{name:e.name})):this._geometryHistory.clear())},this._redoGeometry=()=>{if(this._physicalDrag||this._physicalRotate)return void this._cancelPhysicalGesture();if(this._decorDraft)return this._decorDraft=null,void this.requestUpdate();if(this._decorMove||this._dtDrag||this._bdDrag)return void this._cancelDecorGesture();if(this._resize.dragging)return void this._rszCancelDrag();const e=this._geometryHistory.redo();e&&(this._applyGeometryState(e.after,!0)?this._showToast(this._t("history.redone",{name:e.name})):this._geometryHistory.clear())},this._saveConfigDebounced=Uv(()=>{this._serverCfg&&this._writeConfig().catch(e=>{const t=!0===e?.physicalGeometryRolledBack;"geometry-unsafe"!==e?.code&&("wall_model_client_outdated"===e?.code?this._showToast(this._t("toast.wall_model_client_outdated")):"conflict"===e?.code?(this._showToast(this._t("toast.conflict")),t||(this._cancelPath(),this._reloadConfigOnly(!0))):this._showToast(this._t("toast.cfg_save_failed",{err:this._errText(e)})),t&&this._reloadRejectedPhysicalWrite())})},500),this._draftSegmentCms=[],this._closingWallCm=null,this._savePhysicalDialog=()=>{const e=this._physicalDialog,t=this._curSpaceCfg,i=this._spaceModel();if(!e||!t||!i)return;const s=$v(e.cm);if(null==s)return void this._showPhysicalRange("column"===e.kind?ih:100,"column"===e.kind?1:0);const n=this._imperial?2.54*s:s,o="column"===e.kind?ih:100,a="column"===e.kind?1:0;if(!Number.isFinite(n)||no)return void this._showPhysicalRange(o,a);if("column"===e.kind){const t=i.wall_columns.find(t=>t.id===e.id);if(!t)return;const s=$v(e.angle||"0");if("circle"!==e.shape&&(null==s||s<0||s>=90))return void this._showToast(this._t("toast.physical_angle"));const o="circle"===e.shape?{id:e.id,shape:"circle",center:t.center,cm:n}:{id:e.id,shape:"square",center:t.center,cm:n,angle:s};if(i.wall_columns.some(t=>t.id!==e.id&&Sh(t,o,.02*this._gridPitch)))return void this._showToast(this._t("toast.column_duplicate"))}const r=this._geometrySnapshot();if("partition"===e.kind){const i=(t.partitions||[]).find(t=>t.id===e.id);if(i&&0===n&&Z_(t.openings,{kind:"partition",id:e.id}))return void this._showToast(this._t("toast.zero_wall_opening_conflict"));i&&(i.cm=n)}else if("column"===e.kind){const i=(t.wall_columns||[]).find(t=>t.id===e.id);i&&(i.cm=nh(n),i.shape="circle"===e.shape?"circle":"square","square"===i.shape?i.angle=$v(e.angle||"0"):delete i.angle)}else{const i=(t.room_drafts||[]).find(t=>t.id===e.id);i?.segments?.[e.segment||0]&&(i.segments[e.segment||0].cm=n)}const l=this._commitPhysicalGeometry(this._t("history.physical_edit"),r);this._physicalDialog=null,l||this.requestUpdate()},this._deletePhysicalSelection=()=>{const e=this._physicalSel,t=this._curSpaceCfg;if(!e||!t)return;if("draft"===e.kind)return void this._deleteDraftWhole();const i=this._geometrySnapshot();if("partition"===e.kind){const i=(t.openings||[]).filter(t=>"partition"===t.host?.kind&&t.host.id===e.id).sort((e,t)=>(e.host?.t||0)-(t.host?.t||0));if(i.length)return void(this._partitionDeleteDialog={id:e.id,openings:i.map(e=>JSON.parse(JSON.stringify(e)))})}const s="partition"===e.kind?"partitions":"column"===e.kind?"wall_columns":"room_drafts";t[s]=(t[s]||[]).filter(t=>t.id!==e.id),t[s].length||delete t[s],this._activeDraftId===e.id&&this._cancelPath(),this._physicalSel=null,this._physicalDialog=null,this._commitPhysicalGeometry(this._t("history.physical_delete"),i)},this._confirmPartitionDelete=()=>{const e=this._partitionDeleteDialog,t=this._curSpaceCfg;if(!e||!t)return;const i=this._geometrySnapshot();t.partitions=(t.partitions||[]).filter(t=>t.id!==e.id),t.partitions.length||delete t.partitions,t.openings=(t.openings||[]).filter(t=>"partition"!==t.host?.kind||t.host.id!==e.id),t.openings.length||delete t.openings,this._partitionDeleteDialog=null,this._physicalSel=null,this._physicalDialog=null,this._commitPhysicalGeometry(this._t("history.physical_delete"),i)},this._deleteDraftWhole=()=>{const e="draft"===this._physicalDialog?.kind?this._physicalDialog.id:"draft"===this._physicalSel?.kind?this._physicalSel.id:null,t=this._curSpaceCfg;if(!e||!t||!confirm(this._t("confirm.delete_draft")))return;const i=this._geometrySnapshot();t.room_drafts=(t.room_drafts||[]).filter(t=>t.id!==e),t.room_drafts.length||delete t.room_drafts,this._activeDraftId===e&&this._cancelPath(),this._physicalSel=null,this._physicalDialog=null,this._commitPhysicalGeometry(this._t("history.physical_delete"),i)},this._deleteDraftSegment=()=>{const e=this._physicalDialog,t=this._curSpaceCfg;if(!e||"draft"!==e.kind||!t)return;if(!confirm(this._t("confirm.delete_draft_segment")))return;const i=(t.room_drafts||[]).findIndex(t=>t.id===e.id);if(i<0)return;const s=t.room_drafts[i],n=Math.max(0,Math.min(s.segments.length-1,e.segment||0)),o=[],a=s.points.slice(0,n+1),r=s.points.slice(n+1);if(a.length>=2&&o.push({id:s.id,points:a,segments:s.segments.slice(0,n)}),r.length>=2&&o.push({id:o.length?`${s.id}-${Date.now().toString(36)}`:s.id,points:r,segments:s.segments.slice(n+1)}),2===o.length&&t.room_drafts.length>=200)return void this._showToast(this._t("toast.physical_limit"));const l=this._geometrySnapshot();t.room_drafts.splice(i,1,...o),t.room_drafts.length||delete t.room_drafts,this._activeDraftId===s.id&&this._cancelPath(),this._physicalDialog=null,this._physicalSel=o.length?{kind:"draft",id:o[0].id}:null,this._commitPhysicalGeometry(this._t("history.draft_segment_delete"),l)},this._confirmRoomDelete=e=>{const t=this._roomDeleteDialog,i=this._curSpaceCfg,s=this._spaceModel();if(!t||!i||!s)return;const n=s.rooms.find(e=>e.id===t.roomId);if(!n)return void(this._roomDeleteDialog=null);const o=this._openCuts(),a=n_(wc(s.rooms,this._spaceWalls,o,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov).filter(e=>e.roomId===n.id),s.partitions,this._openingsR.map(e=>({...e,x:e.rx,y:e.ry})),.02*this._gridPitch),r=new Set(a.materialize.filter(e=>!e.reusePartitionId).map(e=>e.interval.key)).size;if(e&&(i.partitions||[]).length+r>jv)return void this._showToast(this._t("toast.physical_limit"));const l=this._geometrySnapshot(),c=kc(s.rooms,this._spaceWalls,o,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov);if(e){i.partitions||=[];const e=new Map,t=new Map,s=e=>{const t=(i.wall_segments||[]).find(t=>{const i=[Number(t.a?.[0])*Ov,Number(t.a?.[1])*Ov],s=[Number(t.b?.[0])*Ov,Number(t.b?.[1])*Ov];return this._samePt(e.a,i)&&this._samePt(e.b,s)||this._samePt(e.a,s)&&this._samePt(e.b,i)});return"string"==typeof t?.id&&t.id?t.id:null},n=Date.now().toString(36);for(const o of a.materialize){let a=o.reusePartitionId?i.partitions.find(e=>e.id===o.reusePartitionId):t.get(o.interval.key);a||(a={id:s(o.interval)||`partition-room-${n}-${t.size}`,a:[o.interval.a[0]/Ov,o.interval.a[1]/Ov],b:[o.interval.b[0]/Ov,o.interval.b[1]/Ov],cm:o.interval.cm},i.partitions.push(a),t.set(o.interval.key,a)),e.set(o.interval.key,a)}for(const t of i.openings||[]){const i=a.openingIntervals.get(t.id),s=i?e.get(i):null,n=this._openingsR.find(e=>e.id===t.id);if(!s||!n)continue;const o={a:[s.a[0]*Ov,s.a[1]*Ov],b:[s.b[0]*Ov,s.b[1]*Ov]};t.host={kind:"partition",id:s.id,t:o_([n.rx,n.ry],o)}}}else if(a.removeOpeningIds.length){const e=new Set(a.removeOpeningIds);i.openings=(i.openings||[]).filter(t=>!e.has(t.id)),i.openings.length||delete i.openings}i.rooms=i.rooms.filter(e=>e.id!==n.id),this._cfgEpoch++;const h=this._normalizeWalls(c,this._openCuts());h.length?i.walls=h:delete i.walls,this._roomDeleteDialog=null,this._commitPhysicalGeometry(this._t(e?"history.delete_room_keep_walls":"history.delete_room_with_walls"),l),this._regSignature="",this._maybeRebuildDevices(),this.requestUpdate()},this._rebindPartitionOpening=()=>{const e=this._openingDialog;if(!e?.id)return;const t=e.id;this._activateMarkupTool("opening"),"opening"===this._tool&&(this._openingRebindId=t,this._openingPreset={type:e.type,lengthCm:e.lengthCm,flipH:e.flipH,flipV:e.flipV,revision:++this._openingPresetRevision},this._openingDialog=null,this._openingHoverCandidate=null)},this._keepClosedAsPartitions=()=>{if(this._wallFaceBatch)return void this._decideWallFace(!1);if(!this._contourClosed||this._pendingSplit||!this._curSpaceCfg)return;const e=this._curSpaceCfg,t=this._path.slice(0,-1);if((e.partitions||[]).length+t.length>jv)return void this._showToast(this._t("toast.physical_limit"));const i=this._geometrySnapshot(),s=Iu(t.length,[...this._draftSegmentCms,this._closingWallCm??void 0],this._drawWallCm,vl);e.partitions||=[];const n=Date.now().toString(36),o=this._activeDraftId?(e.room_drafts||[]).find(e=>e.id===this._activeDraftId):null,a=this._draftSegmentsForPath(this._path,o,s);for(let i=0;ie.id!==this._activeDraftId),e.room_drafts.length||delete e.room_drafts),this._commitPhysicalGeometry(this._t("history.contour_to_partitions"),i),this._roomDialog=!1,this._path=[],delete this._resumeDraftBySpace[this._space],this._activeDraftId=null,this._draftSegmentCms=[],this._closingWallCm=null},this._toggleServerPlans=async()=>{const e=this._spaceDialog;if(e)if(e.pickSaved)this._spaceDialog={...e,pickSaved:!1};else{this._spaceDialog={...e,pickSaved:!0,savedBusy:!0};try{const e=await this.hass.callWS({type:"houseplan/plans/list"}),t=this._spaceDialog;t&&(this._spaceDialog={...t,saved:e?.plans||[],savedBusy:!1})}catch(e){const t=this._spaceDialog;t&&(this._spaceDialog={...t,saved:[],savedBusy:!1}),this._showToast(this._t("toast.plans_list_failed",{err:this._errText(e)}))}}},this._aspectJob=null,this._dayCycleTick=()=>{if(!this.isConnected||"hidden"===this.ownerDocument.visibilityState)return;const e=this._dayCycleState();if(!e)return this._dayCycleTimer&&(clearInterval(this._dayCycleTimer),this._dayCycleTimer=0),void(this._dayCycleClockKey="");const t=Sr(e);t!==this._dayCycleClockKey&&(this._dayCycleClockKey=t,this.requestUpdate())},this._sunShown=!1,this._sunOut=!1,this._sunOutTimer=0,this._openSettingsDialog=()=>{if(!this._norm)return;const e=this._glowRadiusCm,t=this._imperial?Math.round(e/30.48*10)/10:Math.round(e)/100;this._settingsDialog={colors:JSON.parse(JSON.stringify(this._fillColors)),glowRadius:t,bgColor:hs(this._settings,{bgColor:null})||null,northDeg:zr(this._settings,{}),bgMode:Fr(this._settings,{}),sunRays:Nr(this._settings,{}),busy:!1}},this._reportedPreflightFingerprint=null,this._openAlignDialog=()=>this._previewAlignDialog(!1),this._toggleOptimizeLivePositions=()=>{const e=this._alignDialog;e&&!e.busy&&e.report.liveMissingPositions.length&&this._previewAlignDialog(!e.removeLiveMissingPositions)},this._optimizeUndoBusy=!1,this._openBackupExport=()=>{this._settingsDialog=null,this._backupExportDialog={kind:"full",planOnly:!1,busy:!1,error:""}},this._openRulesDialog=()=>{if(!this._norm)return;const e=this._settings.icon_rules,t=(e&&e.length?e:Je).map(e=>({...e}));this._rulesDialog={rules:t,test:"",busy:!1}},this._climateCache=null,this._gearPtCache=new WeakMap}get _canEdit(){return!!this._norm&&(!0===this._serverCanWrite||!1!==this._serverCanWrite&&!0===this.hass?.user?.is_admin)}get _kiosk(){return!!this._config?.kiosk}_showKioskDots(){this._kioskDots=!0,clearTimeout(this._kioskDotsTimer),this._kioskDotsTimer=window.setTimeout(()=>this._kioskDots=!1,2500)}get _modeTransitionBusy(){return this._modeTransitionPreparing||this._modeTransition.active}_cssColor(e,t){const i=String(e||"").trim();if(!i)return t;const s=document.createElement("span");s.style.cssText=`position:absolute;visibility:hidden;color:${i}`,this.renderRoot.append(s);const n=getComputedStyle(s).color||t;return s.remove(),n}_currentModeVisual(e=this._mode){const t=this._modeTransition.presented||this._modeTransitionVisual;if(t)return{...t,viewport:{...t.viewport,viewBox:{...t.viewport.viewBox}}};const i=this._stageEl;if(!i||i.clientWidth<=0||i.clientHeight<=0)return null;const s=this.renderRoot.querySelector(".editorchrome"),n=this._viewOr(this._baseVb()),o=this.renderRoot.querySelector(".hp-paper"),a=this.renderRoot.querySelector(".hp-backdrop"),r=this.renderRoot.querySelector(".zoomwrap"),l=r?getComputedStyle(r).filter:"",c=Number(/brightness\(([^)]+)\)/.exec(l)?.[1]);return{presentedMode:e,editorChromeHeight:"view"===e?0:s?.getBoundingClientRect().height||0,stageWidth:i.clientWidth,stageHeight:i.clientHeight,viewport:rf(n,i.clientWidth),stageColor:getComputedStyle(i).backgroundColor||"rgb(255, 255, 255)",paperColor:o?getComputedStyle(o).fill:"rgb(255, 255, 255)",sceneBrightness:Number.isFinite(c)?c:1,architectureOpacity:"decor"===e?.35:1,backdropOpacity:a&&Number(getComputedStyle(a).opacity)||1,viewWeight:"view"===e?1:0,editorWeight:"view"===e?0:1,toolbarContentOpacity:"view"===e?0:1}}_viewForModeTarget(e,t,i,s,n){const o=Ei(this._baseVb(),s/Math.max(1,n)),a=Math.min(Vv.ZOOM_MAX,Math.max(Vv.ZOOM_MIN,e)),r=o.w/a,l=o.h/a,c=t??o.x+o.w/2,h=i??o.y+o.h/2;return this._clampView({x:c-r/2,y:h-l/2,w:r,h:l},o)}_targetStageColor(e){return"view"!==e?"rgb(255, 255, 255)":this._cssColor(this._stageBg(this._spaceDisplayForRender()),this._cssColor("var(--ha-card-background, var(--card-background-color, #111))","rgb(17, 17, 17)"))}_targetPaperColor(e){return"view"===e&&this._spaceModel()?.bg?this._cssColor("var(--ha-card-background, var(--card-background-color, #111))","rgb(17, 17, 17)"):"rgb(255, 255, 255)"}_targetBrightness(e){return 1}_applyModeTransitionFrame(e){e.targetMode===this._mode&&(this._modeTransitionVisual=e.presented,this._view={...e.presented.viewport.viewBox},this.requestUpdate())}_settleModeTransition(e){if(e.targetMode!==this._mode)return;const t=this._modeTransitionRequest;this._view={...e.to.viewport.viewBox},this._zoom=this._modeTransitionTargetZoom,this._modeTransitionVisual=null,this._modeTransitionPreparing=!1,this._lastValidStageSize=[e.to.stageWidth,e.to.stageHeight],"view"===this._mode&&(this._saveZoom(),this._viewModeSnap=null,this._modeTransitionEditorCamera=null),this.requestUpdate(),this.updateComplete.then(()=>{if(!this.isConnected||t!==this._modeTransitionRequest||this._modeTransitionBusy||e.targetMode!==this._mode)return;const i=this._stageEl;if(i&&i.clientWidth>0&&i.clientHeight>0&&(Math.abs(i.clientWidth-e.to.stageWidth)>.5||Math.abs(i.clientHeight-e.to.stageHeight)>.5)){const e=this._view;this._lastValidStageSize=[i.clientWidth,i.clientHeight],this._applyView(this._zoom,e?e.x+e.w/2:void 0,e?e.y+e.h/2:void 0),this.requestUpdate()}const s=this.renderRoot.activeElement;(!s||!s.isConnected||!!s.closest?.(".editorchrome, .stage"))&&this.renderRoot.querySelector(".modetab.active")?.focus?.({preventScroll:!0})})}_cancelModeTransition(e=!0){const t=!!this._modeTransition.state;this._modeTransitionRequest++,this._modeTransitionPreparing=!1,this._modeTransitionForceAtomic=!1,this._modeTransition.cancel(e),e&&t||(this._modeTransitionVisual=null)}_adoptMode(e){this._cancelModeTransition(!1),this._mode=e,"view"!==e&&(this._editorChromeMode=e)}_commitViewModeAtomic(e,t,i,s){if(this._modeTransitionPreparing=!1,this._modeTransitionVisual=null,this._modeTransitionForceAtomic=!1,this._zoom=t,e){const n=this._stageEl?.clientWidth||e.stageWidth,o=Math.max(1,e.stageHeight+e.editorChromeHeight);this._view=this._viewForModeTarget(t,i,s,n,o),this._lastValidStageSize=[n,o]}else this._view=null;this._viewModeSnap=null,this._modeTransitionEditorCamera=null,this._saveZoom(),this.requestUpdate()}_prepareModeTransition(e,t,i,s,n,o){this.updateComplete.then(()=>{if(!this.isConnected||e!==this._modeTransitionRequest||this._mode!==i)return void(e===this._modeTransitionRequest&&(this._modeTransitionPreparing=!1,this._modeTransitionVisual=null,this._modeTransitionForceAtomic=!1,this.requestUpdate()));const a=this.renderRoot.querySelector(".editorchrome"),r=a?.querySelector(".editorchrome-inner"),l="view"===i?0:r?.scrollHeight||r?.getBoundingClientRect().height||0,c=Math.max(1,t.stageHeight+t.editorChromeHeight),h=Math.max(1,c-l),d=this._stageEl?.clientWidth||t.stageWidth;if(d<=0||h<=0)return this._modeTransitionPreparing=!1,this._modeTransitionVisual=null,this._applyView(s,n,o),void this.requestUpdate();const p=this._viewForModeTarget(s,n,o,d,h),u={presentedMode:i,editorChromeHeight:l,stageWidth:d,stageHeight:h,viewport:rf(p,d),stageColor:this._targetStageColor(i),paperColor:this._targetPaperColor(i),sceneBrightness:this._targetBrightness(i),architectureOpacity:"decor"===i?.35:1,backdropOpacity:"decor"===i&&"backdrop"!==this._decorTool?.5:1,viewWeight:"view"===i?1:0,editorWeight:"view"===i?0:1,toolbarContentOpacity:"view"===i?0:1};this._modeTransitionPreparing=!1;const _=this._modeTransitionForceAtomic;this._modeTransitionForceAtomic=!1,this._modeTransition.start(t,u,i,this._reducedMotion||_?0:220)})}get _hasFixedFloor(){return!!this._config&&Object.prototype.hasOwnProperty.call(this._config,"floor")}_fixedFloorState(e=this._model,t=this._loadOk){const i=this._config?.floor,s=function(e){if(!e.hasFloor)return{kind:"absent"};const t=e.floor;if("string"==typeof t)return t.length?e.spaceIds.includes(t)?{kind:"valid",id:t,source:"id"}:{kind:"invalid",reason:"unknown-id",value:t}:{kind:"invalid",reason:"empty-id",value:t};if("number"==typeof t){if(!Number.isFinite(t))return{kind:"invalid",reason:"non-finite-index",value:t};if(!Number.isInteger(t))return{kind:"invalid",reason:"fractional-index",value:t};if(t<0)return{kind:"invalid",reason:"negative-index",value:t};const i=e.spaceIds[t];return void 0===i?{kind:"invalid",reason:"out-of-range-index",value:t}:{kind:"valid",id:i,source:"index"}}return{kind:"invalid",reason:"invalid-type",value:t}}({spaceIds:e.map(e=>e.id),hasFloor:this._hasFixedFloor,floor:i});return!this._hasFixedFloor||t?s:"number"==typeof i?"valid"===s.kind||"invalid"===s.kind&&"out-of-range-index"===s.reason?{kind:"pending",value:i}:s:"valid"===s.kind||"invalid"===s.kind&&"unknown-id"!==s.reason?s:{kind:"pending",value:i}}_canCommitSpace(e,t=!1){if(t||!this._hasFixedFloor)return!0;const i=this._fixedFloorState();return"valid"===i.kind&&i.id===e}_commitSpace(e,t=!1){return!!this._canCommitSpace(e,t)&&(e!==this._space&&(this._clearTransientHover(!0),this._cancelDevicePressFeedback()),this._space=e,!0)}_slideTo(e,t){if(e===this._space)return!0;if(!this._canCommitSpace(e))return!1;if(this._wallFaceBatch&&this._roomDialogCancel(),"plan"===this._mode&&"draw"===this._tool&&!this._finishWallChain())return!1;this._cancelModeTransition(!0);const i=window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches;return this._activeDraftId&&(this._resumeDraftBySpace[this._space]=this._activeDraftId),this._commitSpace(e),this._path=[],this._clearPlanSnapHover(),this._clearOpeningPlacement(!0),this._tool="draw",this._activeDraftId=null,this._draftSegmentCms=[],this._closingWallCm=null,this._selId=null,this._physicalSel=null,this._editorSecondary.closeForNavigation(),this._physicalDialog=null,this._physicalDrag=null,"plan"===this._mode&&"draw"===this._tool&&this._resumeLastDraft(),this._restoreZoom(),i||(this._slide=t,clearTimeout(this._slideTimer),this._slideTimer=window.setTimeout(()=>{this._slideTimer=void 0,this._slide="",this.requestUpdate()},190),this.requestUpdate()),!0}get _canReorderTabs(){return fv({canEdit:this._canEdit,kiosk:this._kiosk,mode:this._mode,pointerType:"mouse",spaceCount:this._model.length,fixedFloor:this._hasFixedFloor})}_tabPointerDown(e,t){fv({canEdit:this._canEdit,kiosk:this._kiosk,mode:this._mode,pointerType:e.pointerType,spaceCount:this._model.length,fixedFloor:this._hasFixedFloor})&&(Gv(e),this._tabDragRelease=e=>this._tabPointerUp(e),window.addEventListener("pointerup",this._tabDragRelease),window.addEventListener("pointercancel",this._tabDragRelease),this._tabDrag={id:t,pointerId:e.pointerId,x:e.clientX,y:e.clientY,moved:!1,targetId:null,placement:null})}_tabDropTargetAt(e,t,i){const s=this._model.map(e=>e.id),n=s.indexOf(i);if(n<0)return null;const o=this.renderRoot.querySelectorAll('[data-hp="space-tab"]');for(const a of o){const o=a.dataset.id||"";if(!o||o===i)continue;const r=a.getBoundingClientRect();if(er.right||tr.bottom)continue;const l=s.indexOf(o);return l<0?null:{targetId:o,placement:l=4)))return;var i,s;const n=this._tabDropTargetAt(e.clientX,e.clientY,t.id);t.moved&&t.targetId===n?.targetId&&t.placement===n?.placement||(this._tabDrag={...t,moved:!0,targetId:n?.targetId||null,placement:n?.placement||null})}_tabPointerUp(e){const t=this._tabDrag;if(t&&t.pointerId!==e.pointerId)return;const i="pointerup"===e.type&&t?.moved?this._tabDropTargetAt(e.clientX,e.clientY,t.id):null;"pointerup"===e.type&&t?.moved&&this._suppressNextTabClick(),this._endTabDrag(),t?.moved&&i&&this._commitTabOrder(t.id,i.targetId)}_endTabDrag(){this._tabDrag=null,this._tabDragRelease&&(window.removeEventListener("pointerup",this._tabDragRelease),window.removeEventListener("pointercancel",this._tabDragRelease),this._tabDragRelease=null)}_tabClick(e){if(this._tabSuppressClick)return this._tabSuppressClick=!1,clearTimeout(this._tabSuppressClickTimer),void(this._tabSuppressClickTimer=void 0);this._pickSpace(e)}_suppressNextTabClick(){this._tabSuppressClick=!0,clearTimeout(this._tabSuppressClickTimer),this._tabSuppressClickTimer=window.setTimeout(()=>{this._tabSuppressClick=!1,this._tabSuppressClickTimer=void 0},0)}_commitTabOrder(e,t){const i=this._serverCfg;if(!i||!this._canReorderTabs)return;const s=this._model.map(e=>e.id),n=function(e,t,i){const s=e.indexOf(t),n=e.indexOf(i);if(s<0||n<0||s===n)return e;const o=e.slice();return o.splice(s,1),o.splice(n,0,t),o}(s,e,t);if(n===s)return;const o=new Map(this._devices.map(e=>[String(e.id),String(e.area||"")])),a=function(e,t,i,s=()=>""){if(!i)return[];const n=[];for(const o of e){if(!o||!0===o.removed)continue;const e="string"==typeof o.id?o.id:"";if(!e)continue;if("string"==typeof o.space&&o.space)continue;const a=("string"==typeof o.area?o.area:"")||s(e)||"";a&&t[a]||n.push({id:e,space:i})}return n}(i.markers||[],Object.fromEntries(Object.entries(this._areaToSpace).map(([e,t])=>[e,t.space])),s[0]||"",e=>o.get(e)||"");if(a.length){const e=new Map(a.map(e=>[e.id,e.space]));for(const t of i.markers||[]){const i=e.get(String(t.id));i&&(t.space=i)}}i.spaces=function(e,t){const i=new Map(t.map((e,t)=>[e,t]));return e.map((e,t)=>({space:e,index:t})).sort((e,t)=>(i.get(String(e.space?.id))??Number.MAX_SAFE_INTEGER)-(i.get(String(t.space?.id))??Number.MAX_SAFE_INTEGER)||e.index-t.index).map(e=>e.space)}(i.spaces||[],n),this._saveConfig(),this._tabOrderWarned||(this._tabOrderWarned=!0,this._showToast(this._t("toast.space_order_changed")))}_pickSpace(e){if(this._endTabDrag(),e===this._space)return;const t=this._model.map(e=>e.id),i=t.indexOf(this._space),s=t.indexOf(e);this._navApplied=!0,this._showFar=!1,this._frame=null,this._slideTo(e,i>=0&&s0&&Date.now()>=this._cyclePausedUntil&&this._model.length>1&&this._zoom<=1.001){const e=this._model.map(e=>e.id),t=e.indexOf(this._space);this._slideTo(e[(t+1)%e.length],"left"),this._showKioskDots()}}_syncCycleTimer(){clearInterval(this._cycleTimer),this._cycleTimer=void 0,this.isConnected&&!this._hasFixedFloor&&this._config?.kiosk&&Number(this._config.cycle)>0&&(this._cycleTimer=window.setInterval(()=>this._cycleTick(),1e3*Number(this._config.cycle)))}get _editing(){return"plan"===this._mode||"devices"===this._mode||"decor"===this._mode}get _markup(){return"plan"===this._mode}get _wallUnionCache(){return this._wallUnionCacheValue}set _wallUnionCache(e){this._wallUnionCacheValue=e,null===e&&this._wallUnionPool.clear()}_newContinuityController(){return new Yd(()=>{this._resumeSettling="steady"!==this._continuity.state,this._continuityEpoch++,this.isConnected&&this.requestUpdate()})}get _labsIso(){return this._labs.active.includes("iso")}get _desiredProjection(){return"view"===this._mode&&this._labsIso&&"iso"===this._viewPreference[this._space]?"iso":"flat"}_saveViewPreference(){try{localStorage.setItem(Nv,JSON.stringify(this._viewPreference))}catch{}}_logicalViewCenter(e){const t=this._view;if(!t)return null;const i=[t.x+t.w/2,t.y+t.h/2],s="iso"===e?zf(i):i;return{x:s[0],y:s[1]}}_convertProjectionView(e,t){if(e===t)return;const i=this._logicalViewCenter(e);this._view=null;const s=i?"iso"===t?Af([i.x,i.y],0):[i.x,i.y]:null;this._applyView(this._zoom,s?.[0],s?.[1]),this._warmPatch({vp:this._warmViewportState()}),this.requestUpdate()}_setProjection(e){if(!this._labsIso||"view"!==this._mode)return;const t=this._effectiveProjection();if(this._viewPreference={...this._viewPreference,[this._space]:e},"iso"===e){const e=this._isoSceneKey();e&&this._isoFallback.delete(e)}this._saveViewPreference();const i=this._effectiveProjection();this._convertProjectionView(t,i)}_hashSpace(){return yf(window.location.hash||"")}connectedCallback(){this._connectedPath=location.pathname,this._routeDepartureHandled=!1,window.addEventListener("location-changed",this._onLocationChanged),window.addEventListener("popstate",this._onLocationChanged),this._continuityDisposed&&(this._continuity=this._newContinuityController(),this._continuityDisposed=!1,this._continuityPaintToken=-1);const e=(t=this.ownerDocument,Qg.get(t)?.resolved);var t;void 0!==e&&(this._glowScreenBlend=e),this._continuityUnsub?.(),this._continuityUnsub=Vd(this.ownerDocument,this._pageVisibility),super.connectedCallback(),this._pointerModality.connect(this.ownerDocument.defaultView);const i=this.ownerDocument.defaultView?.MutationObserver;i&&(this._pointerHoverObserver=new i(e=>{for(const t of e)for(const e of t.addedNodes)this._syncPointerHoverSubtree(e)}),this._pointerHoverObserver.observe(this.renderRoot,{childList:!0,subtree:!0})),this._motionMedia=window.matchMedia?.("(prefers-reduced-motion: reduce)"),this._reducedMotion=!!this._motionMedia?.matches,this._motionMedia?.addEventListener?.("change",this._onMotionChange),function(e){const t=Qg.get(e);if(t)return t.promise;const i={promise:Promise.resolve(!1)};return i.promise=tf(e).catch(()=>!1).then(e=>(i.resolved=e,e)),Qg.set(e,i),i.promise}(this.ownerDocument).then(e=>{e!==this._glowScreenBlend&&(this._glowScreenBlend=e,this.isConnected&&this.requestUpdate())}),this.hass&&this._ensureHaRegistryAuthority(),window.addEventListener("keydown",this._keyHandler),this._signer.start(()=>this.hass,()=>Rs(this._serverCfg)),this._syncCycleTimer(),window.addEventListener("hashchange",this._onHashChange),this._labsUnsub?.(),this._labsUnsub=function(e,t){Cf.add(t);const i=Tf(e);return"undefined"==typeof window&&t(i),()=>{Cf.delete(t)}}(vv,this._onLabsSnapshot),this._booting?this._bootWatch():this._bootFading&&(clearTimeout(this._bootTimer),this._bootTimer=window.setTimeout(()=>{this._bootFading=!1},220)),this._bootSoft&&(clearTimeout(this._bootSoftTimer),this._bootSoftTimer=window.setTimeout(()=>{this._bootSoft=!1},xv)),!this._loadOk&&this._serverCfg&&this.hass&&this._scheduleLoadRetry(),!this._warmSlot&&this._config&&this._warmAdopt(),this._loadOk&&this._ensureLiveSyncSubscriptions(),this._warmVp&&!this._warmRevivePending&&void 0===this._warmReviveTimer&&(this._warmRevivePending=!0,this._warmReviveTimer=window.setTimeout(()=>this._warmReviveDialog(),0)),this._warmLongReturn&&this._beginResumeSettle(),this._warmLongReturn=!1,this.requestUpdate()}disconnectedCallback(){this._connectedPath&&location.pathname!==this._connectedPath&&this._leaveCardRoute(),window.removeEventListener("location-changed",this._onLocationChanged),window.removeEventListener("popstate",this._onLocationChanged),this._continuityUnsub?.(),this._continuityUnsub=void 0,this._motionMedia?.removeEventListener?.("change",this._onMotionChange),this._motionMedia=void 0,this._vacRaf&&(cancelAnimationFrame(this._vacRaf),this._vacRaf=0),this._refitRaf&&(cancelAnimationFrame(this._refitRaf),this._refitRaf=0),this._dayCycleTimer&&(clearInterval(this._dayCycleTimer),this._dayCycleTimer=0),this._dayCycleClockKey="",this._bootSettleRaf&&(cancelAnimationFrame(this._bootSettleRaf),this._bootSettleRaf=0),this._bootSettling=!1;for(const e of this._activityRt.values())clearTimeout(e.timer);window.removeEventListener("keydown",this._keyHandler),this._endTabDrag(),clearTimeout(this._tabSuppressClickTimer),this._tabSuppressClickTimer=void 0,this._tabSuppressClick=!1,clearInterval(this._cycleTimer),clearTimeout(this._kioskDotsTimer),clearTimeout(this._kioskHoldTimer),clearTimeout(this._reloadRetry),clearTimeout(this._loadRetryTimer),this._loadRetryTimer=void 0,this._connHooked?.removeEventListener?.("ready",this._onConnReady),this._connHooked?.removeEventListener?.("disconnected",this._onConnLost),this._connHooked?.removeEventListener?.("reconnect-error",this._onConnLost),this._connHooked=null,this._haRegistryRelease?.(),this._haRegistryRelease=void 0,this._haRegistryConnection=null,this._signer.dispose(),clearTimeout(this._toastTimer),clearTimeout(this._slideTimer),this._modeTransition.dispose(),this._modeTransitionVisual=null,this._modeTransitionPreparing=!1,this._modeTransitionForceAtomic=!1,this._modeTransitionRequest++,this._slideTimer=void 0,this._slide="",clearTimeout(this._bootTimer),this._bootTimer=void 0,clearTimeout(this._bootSoftTimer),this._saveConfigDebounced.flush(),window.removeEventListener("hashchange",this._onHashChange),this._labsUnsub?.(),this._labsUnsub=void 0,clearTimeout(this._holdTimer),this._roViewport?.disconnect(),this._roViewport=void 0,this._roHdr?.disconnect(),this._roHdr=void 0,this._onWinResize&&(window.removeEventListener("resize",this._onWinResize),this._onWinResize=void 0),this._unsubCfg&&(this._unsubCfg(),this._unsubCfg=null),this._unsubLayout&&(this._unsubLayout(),this._unsubLayout=null),this._unsubVirtual&&(this._unsubVirtual(),this._unsubVirtual=null),this._unsubTrail&&(this._unsubTrail(),this._unsubTrail=void 0),this._liveSyncGeneration++,this._liveSyncAttempt=null,this._liveSyncConnection=null,clearTimeout(this._layoutSyncTimer),clearTimeout(this._duplicateColumnTimer);for(const e of this._glowFadeTimers.values())clearTimeout(e);for(const e of this._glowEnterRafs.values())cancelAnimationFrame(e);clearTimeout(this._glowFeatherResumeTimer),this._glowFeatherResumeTimer=0,this._glowFeatherSuspendUntil=0,this._glowFadeTimers.clear(),this._glowEnterRafs.clear(),this._glowEnteringSources.clear(),this._glowRenderedSources.clear(),this._glowLastAppearance.clear(),this._glowSourceSeq=0,this._warmSnapshot(),this._warmRevivePending=!1,clearTimeout(this._warmReviveTimer),this._warmReviveTimer=void 0,this._warmRelease(),this._clearPlanSnapHover(),this._clearOpeningPlacement(!0),this._touchContacts.clear(),this._touchSequenceMultitouch=!1,this._touchClickBlockUntil=0,this._clearTransientHover(!0),this._cancelDevicePressFeedback(),this._pointerHoverObserver?.disconnect(),this._pointerHoverObserver=void 0,this._pointerModality.disconnect(),this._editorSecondary.reset(),this._resumeSettling=!1,this._continuityHistory=[...this._continuityHistory,...this._continuity.trace].slice(-80),this._continuity.dispose(),this._continuityDisposed=!0,super.disconnectedCallback()}_onKey(e){if("Escape"===e.key&&this._vacFit)return this._vacFit=null,this._showToast(this._t("vac.cal_cancelled")),void e.stopPropagation();if("Escape"===e.key){if(this._tapConfirm)return void(this._tapConfirm=null);if(this._vacCalConfirm)return void(this._vacCalConfirm=null);if(this._decorEraseConfirm)return void(this._decorEraseConfirm=null);if(this._openingInfo)return void(this._openingInfo=null);if(this._infoCard)return void this._closeInfoCard();if(this._rulesDialog)return void(this._rulesDialog=null);if(this._alignDialog)return this._alignDialog=null,void(this._preflightClipboardFallback=null);if(this._backupImportDialog)return void(this._backupImportDialog=null);if(this._backupExportDialog)return void(this._backupExportDialog=null);if(this._settingsDialog)return void(this._settingsDialog=null);if(this._markerDialog)return void this._closeMarkerDialog();if(this._deviceInbox)return void(this._deviceInbox=null);if(this._openingDialog)return void(this._openingDialog=null);if(this._physicalDialog)return void(this._physicalDialog=null);if(this._backdropDialog)return void(this._backdropDialog=null);if(this._decorShapeDialog)return void(this._decorShapeDialog=null);if(this._decorTextDialog)return void(this._decorTextDialog=null);if(this._spaceDialog&&!this._roomDialog)return this._spaceDialog=null,this._importQueue=[],void(this._importTotal=0);if(this._editorSecondary.hasOpenGroup)return e.preventDefault(),void this._editorSecondary.closeGroup(!0)}const t=e.composedPath?.()||[e.target],i=t.some(e=>e?.matches?.('input, textarea, select, [contenteditable="true"]')),s=t.some(e=>e?.classList?.contains?.("editor-secondary")),n=e.ctrlKey||e.metaKey,o=e.key.toLowerCase(),a=/^[a-z]$/.test(o),r="z"===o||!a&&"KeyZ"===e.code,l="y"===o||!a&&"KeyY"===e.code,c=n&&(r&&e.shiftKey||l),h=n&&r&&!e.shiftKey;if("decor"===this._mode){if((h||c)&&i)return;return c?(e.preventDefault(),void this._redoGeometry()):h?(e.preventDefault(),this._decorDraft?void(this._decorDraft=null):this._decorMove||this._dtDrag||this._bdDrag?void this._cancelDecorGesture():void this._undoGeometry()):"Delete"!==e.key&&"Backspace"!==e.key||!this._decorSel||i||s?void("Escape"===e.key&&(e.preventDefault(),this._decorDraft?this._decorDraft=null:this._decorMove||this._dtDrag||this._bdDrag?this._cancelDecorGesture():"furniture"===this._decorTool?(this._furnPalette=null,this._decorTool="select"):this._decorSel?this._decorSel=null:"select"!==this._decorTool?this._decorTool="select":this._setMode("view"))):(e.preventDefault(),void this._decorDeleteSel())}if(this._markup&&(!h&&!c||!i)){if(("Delete"===e.key||"Backspace"===e.key)&&this._physicalSel&&!i&&!s)return e.preventDefault(),void this._deletePhysicalSelection();if(c)return e.preventDefault(),void this._redoGeometry();if(h)return e.preventDefault(),this._resize.dragging?void this._rszCancelDrag():this._wallFaceBatch?(this._roomDialogCancel(),void(this._activeDraftId&&this._path.length>1?this._undoActiveDraftPoint():this._undoPoint())):"draw"===this._tool&&this._path.length?void(this._activeDraftId&&this._path.length>1?this._undoActiveDraftPoint():this._undoPoint()):"split"===this._tool&&this._splitSel?.pts?.length?(this._splitSel={...this._splitSel,pts:this._splitSel.pts.slice(0,-1)},void(this._splitSel.pts.length||(this._cursorPt=null))):void this._undoGeometry();if("Escape"===e.key)return this._physicalDrag||this._physicalRotate?(e.preventDefault(),void this._cancelPhysicalGesture()):this._roomDialog?(e.preventDefault(),void this._roomDialogCancel()):"draw"===this._tool&&this._path.length?(e.preventDefault(),void this._finishWallChain()):this._physicalSel?(e.preventDefault(),void(this._physicalSel=null)):"resize"===this._tool?(e.preventDefault(),this._resize.dragging?void this._rszCancelDrag():("exit-tool"===this._resize.escapeIdle()&&(this._tool="draw"),void this.requestUpdate())):"split"===this._tool?(e.preventDefault(),void(this._splitSel?.pts?.length?(this._splitSel={...this._splitSel,pts:this._splitSel.pts.slice(0,-1)},this._splitSel.pts.length||(this._cursorPt=null)):this._splitSel?this._splitSel=null:this._tool="draw")):"merge"===this._tool?(e.preventDefault(),void(this._mergeSel?this._mergeSel=null:this._tool="draw")):this._wallDialog?(e.preventDefault(),void(this._wallDialog=null)):void("opening"!==this._tool&&"wallthick"!==this._tool&&"delroom"!==this._tool&&"column"!==this._tool||(e.preventDefault(),"opening"===this._tool&&this._clearOpeningPlacement(!0),this._tool="draw"))}}_undoPoint(){if(this._path.length){if(this._contourClosed)return this._path=this._path.slice(0,-1),void(this._closingWallCm=null);if(this._activeDraftId&&this._path.length>1&&this._curSpaceCfg){const e=this._geometrySnapshot();this._path=this._path.slice(0,-1),this._draftSegmentCms=this._draftSegmentCms.slice(0,-1);const t=this._curSpaceCfg,i=(t.room_drafts||[]).findIndex(e=>e.id===this._activeDraftId);if(i>=0){const s=Array.isArray(t.room_drafts[i]?.segments)?t.room_drafts[i].segments:[];this._path.length<2?(t.room_drafts.splice(i,1),t.room_drafts.length||delete t.room_drafts,this._activeDraftId=null):t.room_drafts[i]={id:this._activeDraftId,points:this._path.map(e=>[e[0]/Ov,e[1]/Ov]),segments:this._draftSegmentCms.map((e,t)=>({...s[t]||{},cm:e}))},this._commitPhysicalGeometry(this._t("history.draft_segment_delete"),e)}return}this._path=this._path.slice(0,-1)}}_undoActiveDraftPoint(){const e=this._activeDraftId,t=this._path[0]?[...this._path[0]]:null,i=this._geometryHistory.undo();if(!i)return void this._undoPoint();if(!this._applyGeometryState(i.before,!0))return void this._geometryHistory.clear();const s=e?this._spaceModel()?.room_drafts.find(t=>t.id===e):null;s?(this._activeDraftId=s.id,this._path=s.points.map(e=>[...e]),this._draftSegmentCms=this._adoptDraftCms(this._path,s.segments.map(e=>e.cm),s.id),this._resumeDraftBySpace[this._space]=s.id):(this._activeDraftId=null,this._path=t?[t]:[],this._draftSegmentCms=[],e&&this._resumeDraftBySpace[this._space]===e&&delete this._resumeDraftBySpace[this._space]),this._clearPlanSnapHover(),this._showToast(this._t("history.undone",{name:i.name}))}static getConfigElement(){return document.createElement("houseplan-card-editor")}static getStubConfig(){return{type:"custom:houseplan-card"}}static _warmBootReset(e){for(const e of Cv.values())for(const t of e)clearTimeout(t.evict);Cv.clear(),Tv=e&&e>0?e:1e4}static _warmBootStats(){let e=0,t=0;const i=[];for(const s of Cv.values())for(const n of s)e++,n.dlg&&(t++,i.push(n.dlg.kind));return{keys:Cv.size,slots:e,dlgs:t,drafts:i}}setConfig(e){const t=this._config,i=!!t&&Object.prototype.hasOwnProperty.call(t,"floor");this._config={icon_size:2.5,show_temperature:!0,live_states:!0,show_signal:!0,...e};(i!==this._hasFixedFloor||t?.floor!==this._config.floor)&&(this._hashApplied=!1,this._navApplied=!1,this._warmVpArmed=!1),this._config.kiosk&&(this._booting=!1,this._bootFading=!1),!this._hasFixedFloor&&e.default_floor&&this._commitSpace(e.default_floor,!0);try{this._zoomBySpace=JSON.parse(localStorage.getItem(Av)||"{}")||{}}catch{this._zoomBySpace={}}try{const e=JSON.parse(localStorage.getItem(Nv)||"{}")||{};this._viewPreference=Object.fromEntries(Object.entries(e).filter(e=>"flat"===e[1]||"iso"===e[1]))}catch{this._viewPreference={}}this._labs=Tf(vv);try{const e=JSON.parse(localStorage.getItem(Fv)||"null");this._kioskScale={icon:Ns(e?.icon),font:Ns(e?.font)}}catch{}try{const e=JSON.parse(localStorage.getItem(Rv)||"null");e&&e.config&&Array.isArray(e.config.spaces)&&(this._serverCfg=e.config,this._cfgEpoch++,this._cfgRev=e.rev||0,this._cfgContentFingerprint=e.config_fingerprint||Jd(e.config),this._layout=e.layout||{},this._layoutRev=e.layout_rev||0,this._layoutContentFingerprint=e.layout_fingerprint||Jd(this._layout),this._virtualLights=mn(e.virtual_lights,this._cfgRev),this._serverStorage=!0)}catch{}this._adoptInitialSpace(this._model,this._loadOk),"view"!==this._mode||this._view||(this._zoom=this._zoomBySpace[this._space]||1),this.isConnected&&(this._syncCycleTimer(),this._warmAdopt(),this._warmLongReturn&&this._beginResumeSettle(),this._warmLongReturn=!1)}_warmAdopt(){if(this._config?.kiosk)return;const e=Pv(this._config);if(this._warmKey===e&&this._warmSlot)return;this._warmSlot&&this._warmRelease();const t=this.parentNode,i=this._warmIdx(t),s=Cv.get(e);if(!s||!s.length)return;const n=s.find(e=>e.owner===this._warmGen);if(n)return this._warmLongReturn=!!n.freed&&Date.now()-n.freed>=Wd,clearTimeout(n.evict),n.evict=0,n.freed=0,n.live=!0,this._warmSlot=n,this._warmKey=e,n.frameFingerprint&&this._continuity.adoptCompleteFrame(n.frameFingerprint),void(!this._devices.length&&n.devices?.length&&(this._devices=[...n.devices]));const{slot:o,sure:a}=((e,t,i,s)=>{const n=e=>{const t=!!i&&e.place?.deref()===i;return t&&e.idx===s?4:e.live?0:t?3:2};let o=null,a=0,r=0,l=null;for(const i of e){if(i.owner===t)continue;l=i;const e=n(i);e<=0||(e>a?(o=i,a=e,r=1):e===a&&r++)}return!o||r>1?{slot:o||l,sure:!1}:{slot:o,sure:!0}})(s,this._warmGen,t,i);o&&(this._warmLongReturn=!!o.freed&&Date.now()-o.freed>=Wd,this._booting=!1,this._bootFading=!1,this._hdrH=o.hdrH,this._bootSoft=!0,this.isConnected&&(clearTimeout(this._bootSoftTimer),this._bootSoftTimer=window.setTimeout(()=>{this._bootSoft=!1},xv)),this._warmKey=e,a?(clearTimeout(o.evict),o.evict=0,o.owner=this._warmGen,o.place=t?new WeakRef(t):null,o.idx=i,o.live=!0,this._warmSlot=o,this._warmVp=o.vp,o.frameFingerprint&&this._continuity.adoptCompleteFrame(o.frameFingerprint),!this._devices.length&&o.devices?.length&&(this._devices=[...o.devices]),this._warmAdoptViewport(this._config)):(this._warmSlot={owner:this._warmGen,path:location.pathname,place:t?new WeakRef(t):null,idx:i,live:!0,hdrH:o.hdrH,stageH:o.stageH,vp:null,frameFingerprint:"",devices:null,dlg:null,freed:0,evict:0},s.push(this._warmSlot),this._warmTrim(s)))}_warmIdx(e){const t=e?.children;if(!t)return-1;for(let e=0;e4;){const t=e.findIndex(e=>!e.live);if(t<0)break;clearTimeout(e[t].evict),e.splice(t,1)}}_warmScheduleEvict(e,t){if(clearTimeout(e.evict),!(e.dlg||e.vp&&"view"!==e.vp.mode))return;const i=e.freed,s=e.owner;e.evict=window.setTimeout(()=>{if(e.evict=0,e.freed!==i||e.owner!==s)return;var n;e.dlg=null,e.vp=(n=e.vp)&&"view"!==n.mode?{...n,mode:"view",zoom:n.snap?.space===n.space?n.snap.zoom:n.zoom,view:null,snap:null,tool:"draw",decorTool:"select",showHidden:!1,selId:null,rszSel:null,decorSel:null}:n,e.frameFingerprint="";const o=Cv.get(t);if(!e.live&&o&&o.length>1){const t=o.indexOf(e);t>=0&&o.splice(t,1)}},Tv+250)}_warmAdoptViewport(e){const t=this._warmVp;if(!t||this._warmSlot?.path!==location.pathname)return void(this._warmVp=null);const i=this._fixedFloorState();if(this._hashApplied||!this._model.find(e=>e.id===t.space)||"valid"===i.kind&&i.id!==t.space||this._hasFixedFloor&&"valid"!==i.kind)return void(this._warmVp=null);this._commitSpace(t.space,!0),this._navApplied=!0,this._adoptMode("view"!==t.mode&&this._canEdit&&!e.kiosk?t.mode:"view"),this._pendingNavMode="view"===t.mode||this._canEdit||e.kiosk?null:t.mode,this._zoom=t.zoom;const s=this._effectiveProjection(),n=s===t.projection&&this._labsIso===t.activeLabsIso;if(this._view=n&&t.view?{...t.view}:null,this._viewModeSnap=n&&t.snap?{...t.snap}:null,!n&&t.logicalCenter){const e="iso"===s?Af([t.logicalCenter.x,t.logicalCenter.y],0):[t.logicalCenter.x,t.logicalCenter.y];this._applyView(t.zoom,e[0],e[1])}var o;this._tool="opening"===(o=function(e){return"partition"===e?"draw":e}(o=t.tool))?"draw":"string"==typeof o&&Bv.has(o)?o:"draw",this._decorTool=t.decorTool,this._showHidden=t.showHidden,this._showFar!==t.showFar&&(this._showFar=t.showFar,this._frame=null),this._selId=t.selId,this._resize.restoreSelection(t.rszSel),this._decorSel=t.decorSel,this._warmVpArmed=!0}_warmPatch(e,t=!1){if(this._config?.kiosk)return;const i=Pv(this._config);if(!this._warmSlot||this._warmKey===i){if(!this._warmSlot){if(!t)return;const e=this.parentNode;this._warmKey=i,this._warmSlot={owner:this._warmGen,path:location.pathname,place:e?new WeakRef(e):null,idx:this._warmIdx(e),live:!0,hdrH:this._hdrH,stageH:0,vp:null,frameFingerprint:"",devices:null,dlg:null,freed:0,evict:0};const s=Cv.get(i)||[];for(s.push(this._warmSlot),Cv.set(i,s),this._warmTrim(s);Cv.size>8;){const e=Cv.keys().next().value;if(void 0===e||e===i)break;for(const t of Cv.get(e)||[])clearTimeout(t.evict);Cv.delete(e)}}Object.assign(this._warmSlot,e)}}_warmViewportState(){const e=this._effectiveProjection();return{space:this._space,mode:this._mode,projection:e,activeLabsIso:this._labsIso,logicalCenter:this._logicalViewCenter(e),zoom:this._zoom,view:this._view?{...this._view}:null,snap:this._viewModeSnap?{...this._viewModeSnap}:null,tool:this._tool,decorTool:this._decorTool,showHidden:this._showHidden,showFar:this._showFar,selId:this._selId,rszSel:this._resize.selectedRoomId,decorSel:this._decorSel}}_warmDialogState(){const e=(e,t)=>({kind:e,space:this._space,mode:this._mode,data:t});return this._tapConfirm||this._alignDialog||this._mergeDialog||this._importDialog||this._backupExportDialog||this._backupImportDialog?null:this._openingInfo?e("openingInfo",this._openingInfo.id):this._infoCard?e("info",this._infoCard.id):this._rulesDialog?this._rulesDialog.busy?null:e("rules",this._rulesDialog):this._settingsDialog?this._settingsDialog.busy?null:e("settings",this._settingsDialog):this._markerDialog?this._markerDialog.busy?null:e("marker",this._markerDialog):this._openingDialog?e("opening",this._openingDialog):this._backdropDialog?e("backdrop",this._backdropDialog):this._decorShapeDialog?e("decorShape",this._decorShapeDialog):this._decorTextDialog?e("decorText",this._decorTextDialog):this._roomDialog?e("room",{editId:this._roomEditId,fill:this._roomFill,customFill:this._roomCustomFill,tempSrc:this._roomTempSrc,humSrc:this._roomHumSrc,srcOpen:this._roomSrcOpen,srcFilter:this._roomSrcFilter,nameScale:this._roomNameScale,labelScale:this._roomLabelScale,areaSel:this._areaSel,nameSel:this._nameSel,pendingSplit:this._pendingSplit,wallFaceBatch:this._wallFaceBatch,path:this._path}):this._spaceDialog?this._spaceDialog.busy?null:e("space",this._spaceDialog):null}_warmSnapshot(){if(this._booting||this._config?.kiosk||"steady"!==this._continuity.state)return;const e={vp:this._warmViewportState(),frameFingerprint:this._continuity.frameFingerprint,devices:this._devices};if(this._warmRevivePending||(e.dlg=this._warmDialogState()),this.isConnected&&this._warmSlot?.owner===this._warmGen){const t=this.parentNode;e.place=t?new WeakRef(t):null,e.idx=this._warmIdx(t)}this._warmPatch(e)}_warmReviveDialog(){this._warmRevivePending=!1;const e=this._warmSlot;if(this._warmReviveTimer=void 0,!e||!e.dlg)return;const t=e.dlg,i=e.freed;if(e.dlg=null,e.freed=0,clearTimeout(e.evict),e.evict=0,i&&!(Date.now()-i>Tv)&&t.space===this._space&&t.mode===this._mode){switch(t.kind){case"space":this._spaceDialog={...t.data,busy:!1,savedBusy:!1};break;case"marker":this._markerDialog={...t.data,busy:!1};break;case"settings":this._settingsDialog={...t.data,busy:!1};break;case"rules":this._rulesDialog={...t.data,busy:!1};break;case"opening":this._openingDialog={...t.data};break;case"backdrop":this._backdropDialog={...t.data};break;case"decorShape":this._decorShapeDialog={...t.data};break;case"decorText":{this._decorTextDialog={...t.data};const e=String(this._decorTextDialog?.text??"").length;this._decorTextSelection={start:e,end:e};break}case"room":{const e=t.data;this._roomEditId=e.editId,this._roomFill=e.fill,this._roomCustomFill=e.customFill||null,this._roomTempSrc=e.tempSrc,this._roomHumSrc=e.humSrc,this._roomSrcOpen=e.srcOpen,this._roomSrcFilter=e.srcFilter,this._roomNameScale=e.nameScale,this._roomLabelScale=e.labelScale,this._areaSel=e.areaSel,this._nameSel=e.nameSel,this._pendingSplit=e.pendingSplit,this._wallFaceBatch=e.wallFaceBatch||null,this._path=e.path,this._wallFaceBatch&&(this._activeDraftId=this._wallFaceBatch.activeDraftId,this._draftSegmentCms=[...this._wallFaceBatch.activeCms]),this._roomDialog=!0;break}case"info":{const e=this._devices.find(e=>e.id===t.data);e&&(this._infoCard=e);break}case"openingInfo":{const e=(this._curSpaceCfg?.openings||[]).find(e=>e.id===t.data);e&&(this._openingInfo=e);break}}this.requestUpdate()}}_cacheSnapshot(){if(this._serverCfg)try{this._cfgContentFingerprint=Jd(this._serverCfg),this._layoutContentFingerprint=Jd(this._layout),this._virtualLights=function(e,t,i){if(e.configRev===i)return e;const s=new Set((Array.isArray(t?.markers)?t.markers:[]).filter(bn).map(e=>e.id));return{...e,configRev:i,off:new Set([...e.off].filter(e=>s.has(e)))}}(this._virtualLights,this._serverCfg,this._cfgRev),localStorage.setItem(Rv,JSON.stringify({config:this._serverCfg,rev:this._cfgRev,config_fingerprint:this._cfgContentFingerprint,layout:this._layout,layout_rev:this._layoutRev,layout_fingerprint:this._layoutContentFingerprint,virtual_lights:fn(this._virtualLights)}))}catch{}}_beginContinuityCandidate(e,t,i="plan"){return this._booting&&!this._continuity.hasCompleteFrame?this._continuity.token:(this._continuityDataReady=t,this._continuityPaintToken=-1,this._stagedDeviceSnapshotToken=-1,this._resumeSettling=!0,this._continuity.beginCandidate(e,i))}_continuityStageValid(){const e=this._stageEl;return!!e&&e.clientWidth>0&&e.clientHeight>0}_continuityAssetsReady(){if(!this._model.length)return!1;const e=this._model.length?this._spaceModel():null;return!e?.bg?.href||this._signer.isReady(this.hass,e.bg.href)}_initialSpaceSelection(e,t=this._loadOk){const i=this._fixedFloorState(e,t);return"valid"===i.kind?{id:i.id,source:"fixed"}:this._hasFixedFloor?{id:null,source:"none"}:function(e){const t=new Set(e.spaceIds.filter(e=>!!e));if(!t.size)return{id:null,source:"none"};const i=[["hash",!1===e.acceptHash?null:e.hashSpace],["current",e.preserveCurrent?e.currentSpace:null],["saved",e.savedSpace],["default",e.defaultSpace],["first",e.spaceIds[0]]];for(const[e,s]of i)if(s&&t.has(s))return{id:s,source:e};return{id:null,source:"none"}}({spaceIds:e.map(e=>e.id),hashSpace:this._hashSpace(),acceptHash:!this._hashApplied,currentSpace:this._space,preserveCurrent:this._hashApplied||this._navApplied||this._warmVpArmed,savedSpace:this._savedNav()?.space,defaultSpace:this._config?.default_floor})}_adoptInitialSpace(e,t=this._loadOk){const i=this._initialSpaceSelection(e,t);return i.id?(this._commitSpace(i.id,!0),"hash"===i.source&&(this._hashApplied=!0),"saved"===i.source&&(this._navApplied=!0),i):i}_candidateBackdrop(e,t=this._space){const i=Eh(e),s=this._fixedFloorState(i,!0);if(this._hasFixedFloor&&"valid"!==s.kind)return"";const n=this._initialSpaceSelection(i,!0).id||(i.some(e=>e.id===t)?t:i[0]?.id);return i.find(e=>e.id===n)?.bg?.href||""}_visualFrameFingerprint(){const e=this._stageEl,t=e?[e.clientWidth,e.clientHeight]:[0,0];return Xd([this._cfgRev,this._cfgContentFingerprint||Jd(this._serverCfg),this._layoutRev,this._layoutContentFingerprint||Jd(this._layout),this._space,this._mode,this._view,t,this._glowScreenBlend?"screen":"normal",this.hass?.themes?.darkMode??this.hass?.themes?.default_theme??""])}_settleContinuityFrame(){if(this._booting||!this._continuityStageValid())return;if(!this._continuity.hasCompleteFrame&&"steady"===this._continuity.state)return void(this._continuityAssetsReady()&&(this._renderSnapshotAt=Date.now(),this._continuity.markCompleteFrame(this._visualFrameFingerprint())));if(!this._continuityDataReady)return;if(!["holding","offline-stale","overlay-pending","overlay-visible","candidate-ready"].includes(this._continuity.state))return;const e=this._continuity.token;if(this._candidateDeviceSnapshot&&this._candidateDeviceSnapshot!==this._visibleDeviceSnapshot&&this._stagedDeviceSnapshotToken!==e)return this._stagedDeviceSnapshotToken=e,void this.requestUpdate();this._continuityPaintToken!==e&&(this._continuityPaintToken=e,this._continuity.candidateReady(e)&&this._continuity.commitAfterPaint(e,{updateComplete:()=>this.updateComplete,stageValid:()=>this.isConnected&&this._continuityStageValid(),assetsReady:()=>this._continuityAssetsReady(),frameFingerprint:()=>this._visualFrameFingerprint()}).then(t=>{t&&e===this._continuity.token?(this._resumeSettling=!1,this._renderSnapshotAt=Date.now(),this._candidateDeviceSnapshot&&(this._visibleDeviceSnapshot=this._candidateDeviceSnapshot),this._candidateDeviceSnapshot=null,this._stagedDeviceSnapshotToken=-1,this._warmSnapshot()):e===this._continuity.token&&(this._continuityPaintToken=-1,this._stagedDeviceSnapshotToken=-1,this._candidateDeviceSnapshot=null,this.requestUpdate())}))}_onBackdropLoaded(e,t){this._signer.markLoaded(this.hass,e,t),this._continuity.note("asset-ready"),this._continuityPaintToken=-1,"steady"!==this._continuity.state&&this.requestUpdate()}_renderRecoveryOverlay(){if(!this._continuity.overlayVisible&&"recovery-error"!==this._continuity.state)return G;const e="connection"===this._continuity.recoveryReason;return q`
e.stopPropagation()} - @click=${e=>e.stopPropagation()} - @wheel=${e=>e.stopPropagation()}> - - ${this._t(e?"continuity.restore_connection":"continuity.restore_plan")} - ${"recovery-error"===this._continuity.state?q``:G} -
`}houseplanContinuityTrace(){return[...this._continuityHistory,...this._continuity.trace].slice(-80).map(e=>({...e,...e.stage?{stage:[...e.stage]}:{}}))}getCardSize(){return 12}get _norm(){return!(!this._serverCfg||!this._serverCfg.spaces.length)}_cfgFingerprint(){const e=this._serverCfg?.spaces||[];let t=e.length+":";for(const i of e){t+=(i.id||"")+","+(i.plan_aspect||"")+","+(i.plan_url||"").length+","+(i.plan_x??"")+","+(i.plan_y??"")+","+(i.plan_scale??"")+","+(i.plan_scale_x??"")+","+(i.plan_scale_y??"")+","+(i.plan_angle??"")+","+(i.rooms?.length||0)+","+(i.openings?.length||0)+","+(i.decor?.length||0)+";";for(const e of i.rooms||[]){const i=e.poly?.[0],s=e.poly?.[e.poly.length-1];t+=(e.poly?.length||0)+"."+(e.id||"")+"."+(e.open_to||[]).join("+")+"."+(e.area||"")+"."+JSON.stringify(e.settings||0)+"."+(e.x??"")+","+(e.y??"")+","+(e.w??"")+","+(e.h??"")+","+(i?i[0]+"/"+i[1]:"")+","+(s?s[0]+"/"+s[1]:"")+";"}}return t}get _model(){if(!this._serverCfg)return[];const e=this._cfgEpoch+"|"+this._cfgFingerprint();if(this._modelCache&&this._modelCache.key===e)return this._modelCache.model;const t=this._buildModel();return this._modelCache={key:e,model:t},t}_buildModel(){if(!this._serverCfg)return[];const e=this._renderCfg;return Eh(e).map((t,i)=>{const s=e.spaces[i]?.plan_url;return t.bg&&s?{...t,bg:{...t.bg,href:s}}:t})}_spaceModel(){return this._hasFixedFloor?ep(this._model,this._space):(e=this._model,t=this._space,e.find(e=>e.id===t)??e[0]);var e,t}_spaceModelById(e){return ep(this._model,e)}_syncEmptySpaceState(){if(!!this._serverCfg&&0===this._serverCfg.spaces.length){if(!this._emptySpaceStateActive){this._emptySpaceStateActive=!0;for(const e of this._pointers.keys())for(const t of this.renderRoot.querySelectorAll("*"))try{t.hasPointerCapture?.(e)&&t.releasePointerCapture(e)}catch{}this._pointers.clear(),this._panStart=null,this._panLock=null,this._pinchStart=null,this._swipeStart=null,this._drag=null,this._rlResize=null,this._vacFit=null,this._compassDrag=!1,this._cancelModeTransition(!1),this._mode="view",this._clearGeometryGesture(),this._geometryHistory.clear(),this._resumeDraftBySpace={},this._tip=null,this._hoverRoom=null,this._openingInfo=null,this._closeInfoCard(),this._deviceInbox=null,this._deviceInboxReturn=null,this._markerDialog=null,this._physicalDialog=null,this._backdropDialog=null,this._decorShapeDialog=null,this._decorTextDialog=null,this._roomDialog=!1,"edit"===this._spaceDialog?.mode&&(this._spaceDialog=null),this._editorSecondary.closeForNavigation(),this._saveConfigDebounced.cancel(),this._frame=null,this._planSnapGeometryCache=null,this._hiddenWallDiagnosticCache=null,this._decorSnapCache=null,this._commitSpace("",!0)}}else this._emptySpaceStateActive=!1}get _areaToSpace(){const e={};for(const t of this._model)for(const i of t.rooms)i.area&&(e[i.area]={space:t.id,room:i});return e}get _settings(){return this._serverCfg?.settings||{}}get _showAll(){return this._showHidden||!this._settings.filter_seeded&&!!this._settings.show_all}_seedHiddenDevices(){if(!this._serverCfg||!this._norm||!this._canEdit)return;const e=this._serverCfg,t=function(e){const t=e.hass,i=e.registry||An(t),s=En(t,i),n=On(t,i),{areaToSpace:o,markers:a,settings:r,excluded:l,iconRules:c}=e,h=!1!==r.group_lights,d=wo(a),p=bo(s,h).filter(e=>!ko(s,e.eid,d)),u=new Set(p.map(e=>e.area)),_=jn(s),m=qn(a,n),g=new Set(a.map(e=>e.binding)),f=[];for(const e of Object.values(s.devices)){const n=e.area_id;if(!n||!o[n])continue;if("service"===e.entry_type)continue;if(g.has("device:"+e.id))continue;if("active"!==Bn(t,"device:"+e.id,i).kind)continue;const a=Wn(s,e.id,_[e.id]||[],m);if(a.partial&&!a.entityIds.length)continue;const r=a.entityIds,d=Un(s,e,r);let p=l.has(d)||"Group"===e.model||/scene/i.test(e.model||"")||/bridge/i.test((e.model||"")+(e.name||""))||"myheat"===d&&!!e.via_device_id;if(!p&&h&&u.has(n)){const t=(e.name_by_user||e.name||"").trim();"mdi:lightbulb"===yo(s,t,e.model,r,c)&&(p=!0)}p&&f.push("device:"+e.id)}return f}({hass:this.hass,registry:this._haRegistry,areaToSpace:Object.fromEntries(Object.entries(this._areaToSpace).map(([e,t])=>[e,t.space])),markers:this._markers,settings:this._settings,excluded:this._excluded,firstSpaceId:this._model[0]?.id||"",iconRules:this._iconRules});if(!t.length&&e.settings?.filter_seeded)return;e.markers=e.markers||[];const i=[];for(const s of t){const t="h"+s.slice(s.indexOf(":")+1);e.markers.push({id:t,binding:s,hidden:!0}),i.push(s.slice(s.indexOf(":")+1))}const s={...e.settings||{},filter_seeded:!0};delete s.show_all,i.length&&Array.isArray(s.new_device_ids)&&(s.new_device_ids=s.new_device_ids.filter(e=>!i.includes(e))),e.settings=s,this._regSignature="",this._maybeRebuildDevices(),this._saveConfig(),this.requestUpdate()}get _iconRules(){const e=this._settings.icon_rules;if(!e||!Array.isArray(e)||!e.length)return;const t=JSON.stringify(e);return t!==this._rulesCompiledSrc&&(this._rulesCompiledSrc=t,this._rulesCompiled=Xe(e)),this._rulesCompiled}get _fillColors(){return ps(this._settings)}get _excluded(){const e=this._settings.exclude_integrations;return e?new Set(e):Ye}willUpdate(e){e.has("_serverCfg")&&this._cfgEpoch++,this._syncEmptySpaceState(),e.has("hass")&&this.hass&&(this._hassSequence++,this._renderSnapshotAt=Date.now(),this._continuity.note("hass-snapshot"),this._ensureHaRegistryAuthority(),this._planHassMemo=null,this._hookConnection(),!this._loadOk&&!this._loading&&this._loadTries<8&&this._loadFromServer(),this._maybeRebuildDevices(),this._vacTick(),this._activityTick()),this._continuity.hasCompleteFrame&&"steady"===this._continuity.state&&this._continuity.refreshCompleteFrame(this._visualFrameFingerprint()),this._captureRenderDeviceSnapshot()}updated(){this._pruneDevicePressFeedback(),this._syncDayCycleClock(),this._warmSnapshot(),this._dtMeasure();const e=this._stageEl;e&&!this._roViewport&&(this._roViewport=new ResizeObserver(()=>this._refitView()),this._roViewport.observe(e)),e&&this._booting&&!this._bootTimer&&this._bootWatch();const t=this.renderRoot.querySelector(".hdr");if(t&&e&&!this._roHdr){const i=()=>{const t=this.renderRoot.querySelector("ha-card");if(!t)return;const i=e.getBoundingClientRect().top-t.getBoundingClientRect().top,s=Math.min(Math.max(t.getBoundingClientRect().top,0),120),n=Math.round(i+s);n>=0&&n!==this._hdrH&&(this._hdrH=n),n>=0&&!this._booting&&!this._config?.kiosk&&e.clientHeight>0&&this._warmPatch({hdrH:n,stageH:e.clientHeight})};this._roHdr=new ResizeObserver(()=>requestAnimationFrame(i)),this._roHdr.observe(t),this._onWinResize=()=>requestAnimationFrame(i),window.addEventListener("resize",this._onWinResize),i()}if(e&&!this._view&&this._refitView(),this._editorSecondary.afterRender(),this._settleContinuityFrame(),this._serverStorage&&this._loadOk&&0===this._model.length&&!this._spaceDialog&&!this._importDialog&&!this._onboardingShown){this._onboardingShown=!0;const e=function(e){const t=e?.floors;if(!t||"object"!=typeof t)return[];const i=[];for(const e of Object.values(t))e&&e.floor_id&&i.push({id:e.floor_id,name:e.name||e.floor_id,level:e.level??null});return i.sort((e,t)=>{const i=e.level??1e9,s=t.level??1e9;return i!==s?i-s:e.name.localeCompare(t.name)}),i}(this.hass);e.length?this._importDialog={floors:e.map(e=>({...e,checked:!0}))}:this._openSpaceDialog("create")}}_adoptStructuralResponses(e,t,i){const s=e?.config,n=s&&Array.isArray(s.spaces)?s:null,o=Jd(n),a=o!==(this._cfgContentFingerprint||Jd(this._serverCfg));if(a&&(this._geometryHistory.clear(),this._pendingPhysicalWrites.clear(),this._serverCfg&&this._clearGeometryGesture(),this._serverCfg=n,this._cfgContentFingerprint=o),this._cfgRev=e?.rev??this._cfgRev,e&&("virtual_lights"in e||"config"in e)){const t=gn(this._virtualLights,e.virtual_lights,this._cfgRev,"virtual_lights"in e);vn(t)!==vn(this._virtualLights)&&(this._virtualLights=t,this._capturedSnapshotVirtual="")}let r=!1;if(void 0!==t||void 0!==i){const e=i??t?.layout??{},s=Jd(e);r=s!==(this._layoutContentFingerprint||Jd(this._layout)),r&&(this._layout=e,this._layoutContentFingerprint=s),this._layoutRev=t?.rev??this._layoutRev}return this._canOptimizeUndo=!(!e?.can_optimize_undo&&!t?.can_optimize_undo),this._haIntegrationVersion="string"==typeof e?.integration_version?e.integration_version:this._haIntegrationVersion,this._undoKind=e?.undo_kind||t?.undo_kind||null,"boolean"==typeof e?.can_write&&(this._serverCanWrite=e.can_write),a&&this._continuity.note("config-candidate",{configRev:this._cfgRev}),r&&this._continuity.note("layout-candidate",{layoutRev:this._layoutRev}),{configChanged:a,layoutChanged:r}}_resumePendingNavMode(){if(!this._pendingNavMode||!this._canEdit||this._config?.kiosk)return!1;const e=this._pendingNavMode;return this._pendingNavMode=null,this._setMode(e,!1),!0}async _loadFromServer(){this._loading=!0,this._loadTries++;const e=this._space,t=!!this._view;try{const[i,s]=await Promise.all([this.hass.callWS({type:"houseplan/config/get"}),this.hass.callWS({type:"houseplan/layout/get"})]),n=i?.config&&Array.isArray(i.config.spaces)?i.config:null,o=Jd(n)!==(this._cfgContentFingerprint||Jd(this._serverCfg))||Jd(s?.layout??{})!==(this._layoutContentFingerprint||Jd(this._layout));if(o){if(!await this._signer.prepareImage(this.hass,this._candidateBackdrop(n)))return this._continuity.note("asset-failed"),void this._scheduleLoadRetry(!0)}o&&this._continuity.hasCompleteFrame&&"steady"===this._continuity.state&&this._beginContinuityCandidate("structural-response",!0),this._connectionWasLost=!1,this._serverStorage=!0,"boolean"==typeof i?.can_write&&(this._serverCanWrite=i.can_write),this._canOptimizeUndo=!(!i?.can_optimize_undo&&!s?.can_optimize_undo),this._haIntegrationVersion="string"==typeof i?.integration_version?i.integration_version:this._haIntegrationVersion,this._adoptStructuralResponses(i,s),this._adoptInitialSpace(this._model,!0),this._resumePendingNavMode(),this._cacheSnapshot(),this._warmVpArmed&&this._space===this._warmVp?.space?this._warmVpArmed=!1:t&&this._space===e||this._restoreZoom(),this._loadOk=!0,this.hass.callWS({type:"houseplan/trail/get"}).then(e=>{this._vacSrvTrails=e?.trails||{},this.requestUpdate()}).catch(()=>{}),this._ensureLiveSyncSubscriptions()}catch(e){if(this._serverCfg)this._scheduleLoadRetry(!0);else if(this._loadTries>=8){this._serverStorage=!1;try{this._layout=JSON.parse(localStorage.getItem(Iv)||"{}")||{}}catch{this._layout={}}}}finally{this._loading=!1,this._continuityDataReady=!0,this._regSignature="",this._maybeRebuildDevices(),this.requestUpdate()}}_ensureLiveSyncSubscriptions(){const e=this.hass?.connection;if(!e)return;if(e!==this._liveSyncConnection&&(this._unsubCfg?.(),this._unsubCfg=null,this._unsubLayout?.(),this._unsubLayout=null,this._unsubTrail?.(),this._unsubTrail=void 0,this._unsubVirtual?.(),this._unsubVirtual=null,this._liveSyncGeneration++,this._liveSyncAttempt=null,this._liveSyncConnection=e),this._liveSyncAttempt)return;const t=this._liveSyncGeneration,i=[],s=(s,n,o,a)=>{s()||i.push(async()=>{const i=await e.subscribeEvents(a,o);t===this._liveSyncGeneration&&this.isConnected&&this.hass?.connection===e&&!s()?n(i):i?.()})};if(s(()=>this._unsubCfg,e=>{this._unsubCfg=e},"houseplan_config_updated",e=>{const t=Number(e?.data?.rev??-1);t!==this._cfgRev&&this._reloadConfigOnly(!1,t)}),s(()=>this._unsubTrail,e=>{this._unsubTrail=e},"houseplan_trail_updated",async()=>{try{const e=await this.hass.callWS({type:"houseplan/trail/get"});this._vacSrvTrails=e?.trails||{},this.requestUpdate()}catch{}}),s(()=>this._unsubLayout,e=>{this._unsubLayout=e},"houseplan_layout_updated",e=>this._onLayoutEvent(Number(e?.data?.rev??-1))),s(()=>this._unsubVirtual,e=>{this._unsubVirtual=e},"houseplan_virtual_light_updated",e=>{const t=wn(this._virtualLights,e?.data);t!==this._virtualLights&&(this._virtualLights=t,this._capturedSnapshotVirtual="",this._cacheSnapshot(),this.requestUpdate())}),!i.length)return;const n=function(e){return Promise.allSettled(e.map(e=>Promise.resolve().then(e)))}(i).then(()=>{});this._liveSyncAttempt=n,n.finally(()=>{this._liveSyncAttempt===n&&(this._liveSyncAttempt=null)})}async _reloadConfigOnly(e=!1,t){if(!e){if(void 0!==t&&t<=this._cfgRev)return;if(this._saveConfigDebounced.pending()&&this._saveConfigDebounced.flush(),this._cfgWriting)return clearTimeout(this._reloadRetry),void(this._reloadRetry=window.setTimeout(()=>this._reloadConfigOnly(!1,t),400))}this._beginContinuityCandidate("config-reload",!1);try{const e=await this.hass.callWS({type:"houseplan/config/get"}),t=e?.config&&Array.isArray(e.config.spaces)?e.config:null;if(Jd(t)!==(this._cfgContentFingerprint||Jd(this._serverCfg))&&!await this._signer.prepareImage(this.hass,this._candidateBackdrop(t)))return this._continuity.note("asset-failed"),void this._scheduleLoadRetry(!0);const i=this._space;this._adoptStructuralResponses(e),this._adoptInitialSpace(this._model,!0),this._resumePendingNavMode(),this._cacheSnapshot(),this._space!==i&&this._restoreZoom(),this._regSignature="",this._maybeRebuildDevices(),this.requestUpdate()}catch(e){this._showToast(this._t("toast.cfg_reload_failed",{err:this._errText(e)}))}finally{this._continuityDataReady=!0,this.requestUpdate()}}_scheduleLoadRetry(e=!1){if(void 0!==this._loadRetryTimer)return;const t=Math.min(8e3,500*2**Math.min(4,Math.max(1,this._loadTries-7)));this._loadRetryTimer=window.setTimeout(()=>{this._loadRetryTimer=void 0,!e&&this._loadOk||this._loading||!this.hass||this._loadFromServer()},t)}_ensureHaRegistryAuthority(){const e=this.hass?.connection||null;e&&e!==this._haRegistryConnection&&(this._haRegistryRelease?.(),this._haRegistryConnection=e,this._haRegistryRev=-1,this._haBindingCacheKey="",this._planHassMemo=null,this._haRegistryRelease=Rn(this.hass,this._onHaRegistryUpdate),this._onHaRegistryUpdate())}get _haRegistry(){return An(this.hass)}get _planHass(){const e=this._haRegistry,t=Fn(this.hass,e),i=this._planHassMemo;if(i&&i.hass===this.hass&&i.sig===t)return i.active;const s=En(this.hass,e),n=On(this.hass,e);return this._planHassMemo={hass:this.hass,sig:t,active:s,full:n},s}_captureRenderDeviceSnapshot(){if(!this.hass)return;const e=Date.now(),t=[...this._activityRt.entries()].map(([t,i])=>`${t}:${i.gen}:${i.flashTs}:`+(i.flashKind&&(i.expiresAt||i.flashTs+um)>e?1:0)).join("|"),i=vn(this._virtualLights);if(this._capturedSnapshotSequence===this._hassSequence&&this._capturedSnapshotDevices===this._devices&&this._capturedSnapshotLayout===this._layout&&this._capturedSnapshotConfigEpoch===this._cfgEpoch&&this._capturedSnapshotVirtual===i&&this._capturedSnapshotActivity===t)return;const s=this._planHass,n=new Map,o=new Map,a=new Set(["sun.sun"]);this._vacFit?.source&&a.add(this._vacFit.source);const r=new Set,l=new Set,c=e=>{if(!e)return;const t=e.indexOf(":");if(t<0)return void a.add(e);const i=e.slice(0,t),s=e.slice(t+1);"device"===i?r.add(s):"entity"===i&&a.add(s)};for(const e of this._model)for(const t of e.rooms)t.area&&l.add(t.area),c(t.settings?.temp_source),c(t.settings?.hum_source);for(const e of this._serverCfg?.spaces||[])for(const t of e.openings||[])for(const e of gi(t))a.add(e);for(const e of this._serverCfg?.spaces||[])for(const t of e.decor||[])if("text"===t.kind){for(const e of String(t.text||"").matchAll(/\{([^{}\r\n]+)\}/g)){const t=Yi(e[1]);t?.entity&&a.add(t.entity)}t.entity&&a.add(t.entity),o.set(`decor:${e.id}:${t.id}`,es(t.text,t,s,e=>!!s.entities?.[e]&&!!s.states?.[e]&&!ko(s,e,wo(this._markers))))}const h=ho(s,this._devices,null,this._virtualLights);for(const e of this._devices){for(const t of[!1,!0])n.set(L_(e.id,t),N_(s,e,{liveStates:!1!==this._config?.live_states,showTemperature:!1!==this._config?.show_temperature,showSignal:t,activityRuntime:this._activityRt.get(e.id),sourceDetails:!1,lightDevices:this._devices,lightSources:h,registryHass:this._fullRegistryHass,reducedMotion:this._reducedMotion}));if(this._isVacDev(e)){const t=this._vacSource(e,s),i=t?up(s?.states?.[t]?.attributes):null,n=this._vacRt.get(e.id);o.set(`vacuum:${e.id}`,{source:t,telemetry:i,mapId:i?this._vacMapId(e,i,s):null,runtime:n?{trail:n.trail,lastTs:n.lastTs,moving:n.moving,jump:n.jump}:null,server:this._vacSrvTrails[e.id]||null})}}const d=H_({sourceSequence:this._hassSequence,hass:s,devices:this._devices,presentations:n,positions:(p=this._model.length>0,u=this._devices,_=e=>this._livePos(e),p?new Map(u.map(e=>[e.id,_(e)])):new Map),facts:o,entityIds:a,deviceIds:r,areaIds:l});var p,u,_;this._capturedSnapshotSequence=this._hassSequence,this._capturedSnapshotDevices=this._devices,this._capturedSnapshotLayout=this._layout,this._capturedSnapshotActivity=t,this._capturedSnapshotConfigEpoch=this._cfgEpoch,this._capturedSnapshotVirtual=i,this._visibleDeviceSnapshot&&"steady"!==this._continuity.state?this._candidateDeviceSnapshot=d:(this._visibleDeviceSnapshot=d,this._candidateDeviceSnapshot=null)}get _renderDeviceSnapshot(){return this._stagedDeviceSnapshotToken===this._continuity.token?this._candidateDeviceSnapshot||this._visibleDeviceSnapshot:this._visibleDeviceSnapshot||this._candidateDeviceSnapshot}get _renderPlanHass(){return this._renderDeviceSnapshot?.hass||this._planHass}get _renderDevices(){return this._renderDeviceSnapshot?.devices||this._devices}get _fullRegistryHass(){return this._planHass,this._planHassMemo?.full||this.hass}_bindingStatus(e){return Bn(this.hass,e,this._haRegistry)}houseplanDiagnostics(){const e=function(e){const t=An(e);return{access:t.access,authoritative:t.authoritative,revision:t.revision,lastSuccess:t.lastSuccess,error:t.error}}(this.hass),t={active:0,ha_disabled:0,orphaned:0,unverified:0};for(const e of this._markers)e.removed||"virtual"===e.binding||t[this._bindingStatus(e.binding).kind]++;return{registry:{...e,lastSuccessAgeMs:e.lastSuccess?Math.max(0,Date.now()-e.lastSuccess):null},bindings:t}}_openBindingInHa(e){const[t,i]=e.split(":");if(i)if("device"!==t){if("entity"===t){const e=this._fullRegistryHass.entities?.[i];e?.device_id&&Wv("/config/devices/device/"+encodeURIComponent(e.device_id))}}else Wv("/config/devices/device/"+encodeURIComponent(i))}_bindingHasHaPage(e){const[t,i]=e.split(":");return!!i&&("device"===t||"entity"===t&&!!this._fullRegistryHass.entities?.[i]?.device_id)}_toggleMarkerDialogVisibility(){const e=this._markerDialog;if(!e)return;const t="ha"===e.bindingMode?this._bindingStatus(e.binding):null,i=e.hideFromPlan||"ha_disabled"===t?.kind;i&&"ha_disabled"===t?.kind?this._showToast(this._t("entity"===t.reason?"toast.ha_disabled_show_entity":"toast.ha_disabled_show_device")):i&&"unverified"===t?.kind?this._showToast(this._t("toast.ha_binding_unverified")):this._markerDialog={...e,hideFromPlan:!i}}_hookConnection(){const e=this.hass?.connection;e&&e!==this._connHooked&&(this._connHooked?.removeEventListener?.("ready",this._onConnReady),this._connHooked?.removeEventListener?.("disconnected",this._onConnLost),this._connHooked?.removeEventListener?.("reconnect-error",this._onConnLost),e.addEventListener?.("ready",this._onConnReady),e.addEventListener?.("disconnected",this._onConnLost),e.addEventListener?.("reconnect-error",this._onConnLost),this._connHooked=e)}_display(e){return this._signer.display(this.hass,e)}_resign(){this._signer.resign(this.hass,Rs(this._serverCfg))}_onLayoutEvent(e){e<=this._layoutRev||(clearTimeout(this._layoutSyncTimer),this._layoutSyncTimer=window.setTimeout(()=>{e<=this._layoutRev||this._reloadLayoutOnly()},200))}_noteLayoutRev(e){const t=e?.rev;"number"==typeof t&&t>this._layoutRev&&(this._layoutRev=t)}async _reloadLayoutOnly(){if(!this._serverStorage||!this.hass?.callWS)return;this._beginContinuityCandidate("layout-reload",!1);const e=new Map;for(const t of this._dirtyPos)this._layout[t]&&e.set(t,this._layout[t]);this._persistLayout.pending()&&this._persistLayout.flush();for(const[t,i]of this._sentPos)e.set(t,i);try{const t=await this.hass.callWS({type:"houseplan/layout/get"}),i={...t?.layout||{}};for(const[t,s]of e)i[t]=s;const s=Jd(i);s!==(this._layoutContentFingerprint||Jd(this._layout))&&(this._layout=i,this._layoutContentFingerprint=s),this._layoutRev=t?.rev??this._layoutRev,this._canOptimizeUndo=!!t?.can_optimize_undo,this._haIntegrationVersion="string"==typeof t?.integration_version?t.integration_version:this._haIntegrationVersion,this._undoKind=t?.undo_kind||null,this._cacheSnapshot(),this.requestUpdate()}catch{}finally{this._continuityDataReady=!0,this.requestUpdate()}}_maybeRebuildDevices(){const e=this.hass;if(!e?.devices||!e?.entities||!e?.areas)return;const t=this._haRegistry,i=Fn(e,t)+":"+Object.keys(e.areas).length+":"+(this._norm?"n":"l")+":"+pa(e,this._config?.language);if(i===this._regSignature&&this._devices.length)return;this._regSignature=i;const s=new Map(this._devices.map(e=>[e.id,e.bindingStatus?.kind||"active"]));this._devices=So({hass:e,registry:t,areaToSpace:Object.fromEntries(Object.entries(this._areaToSpace).map(([e,t])=>[e,t.space])),markers:this._markers,settings:this._settings,excluded:this._excluded,showAll:this._showAll,firstSpaceId:this._model[0]?.id||"",loc:e=>this._t(e),iconRules:this._iconRules});const n=this._markers.filter(e=>!e.removed&&"virtual"!==e.binding).map(e=>e.binding).sort(),o=t.revision+":"+n.join("|");if(t.authoritative&&o!==this._haBindingCacheKey){const i=new Map;for(const s of n)i.set(s,Bn(e,s,t));!function(e){if("undefined"==typeof localStorage||!e.size)return;const t=Ln(),i=Date.now();for(const[s,n]of e)"active"===n.kind?t[s]={kind:"active",ts:i}:"ha_disabled"===n.kind&&(t[s]={kind:"ha_disabled",reason:n.reason,ts:i});const s=Object.fromEntries(Object.entries(t).sort((e,t)=>t[1].ts-e[1].ts).slice(0,1500));Mn=s;try{localStorage.setItem(Sn,JSON.stringify(s))}catch{}}(i),this._haBindingCacheKey=o}this._defPos=this._defaultPositions(),this._syncNewDevices(),this._seedHiddenDevices(),this._syncActivityRuntime();const a=new Set(this._devices.filter(e=>!e.hidden).map(e=>e.id));for(const e of this._vacRt.keys())a.has(e)||this._vacRt.delete(e);if(this._infoCard){const e=this._devices.find(e=>e.id===this._infoCard.id);this._infoCard=e&&"ha_disabled"!==e.bindingStatus?.kind?e:null}const r=this._devices.some(e=>"ha_disabled"===e.bindingStatus?.kind&&"ha_disabled"!==s.get(e.id));r&&("ha_disabled"!==this._infoCard?.bindingStatus?.kind&&"ha_disabled"!==this._devices.find(e=>e.id===this._infoCard?.id)?.bindingStatus?.kind||this._closeInfoCard(),this._drag&&"ha_disabled"===this._devices.find(e=>e.id===this._drag.id)?.bindingStatus?.kind&&(this._drag=null),clearTimeout(this._holdTimer),this._holdFired=!1,this._tip=null,this._tapConfirm=null),this._nativeMoreInfoEntity&&!this._planEntityAvailable(this._nativeMoreInfoEntity)&&(qv(this,"hass-more-info",{entityId:null}),this._nativeMoreInfoEntity=null)}_syncNewDevices(){if(!this._norm||!this._loadOk||!this._serverCfg)return;const e=this._devices.filter(e=>!e.marker&&!e.virtual).map(e=>e.id).sort(),t=e.join(",");if(t===this._newSyncKey)return;this._newSyncKey=t;const i=this._settings,{fresh:s,known:n}=function(e,t){if(!Array.isArray(t))return{fresh:[],known:[...e]};const i=new Set(t),s=e.filter(e=>!i.has(e));return{fresh:s,known:s.length?[...t,...s]:t}}(e,i.known_devices);if(!Array.isArray(i.known_devices)||s.length){const e=[...new Set([...i.new_device_ids||[],...s])];this._serverCfg={...this._serverCfg,settings:{...i,known_devices:n,new_device_ids:e}},this._saveConfig()}}get _newIds(){const e=this._settings.new_device_ids;return new Set(Array.isArray(e)?e:[])}_ackNewDevice(e){if(!this._newIds.has(e)||!this._serverCfg)return;const t=this._settings;this._serverCfg={...this._serverCfg,settings:{...t,new_device_ids:(t.new_device_ids||[]).filter(t=>t!==e)}},this._saveConfig(),this.requestUpdate()}get _markers(){return this._serverCfg?.markers||[]}_roomLqi(e){if(!e)return null;const t=[];for(const i of this._renderDevices){if(i.area!==e||i.virtual)continue;const s=_o(this._renderPlanHass,i.entities);null!=s&&t.push(s)}return Ni(t)}_roomBounds(e){if(e.poly&&e.poly.length){const t=e.poly.map(e=>e[0]),i=e.poly.map(e=>e[1]),s=Math.min(...t),n=Math.min(...i);return{x:s,y:n,w:Math.max(...t)-s,h:Math.max(...i)-n}}return{x:e.x??0,y:e.y??0,w:e.w??0,h:e.h??0}}_defaultPositions(){const e={},t=this._config?.icon_size??2.5;for(const i of this._model){const s=t/100*nd(i)*1.3;for(const t of i.rooms){if(!t.area)continue;const n=this._devices.filter(e=>e.area===t.area&&e.space===i.id);if(!n.length)continue;const o=this._roomBounds(t),a=.1*Math.min(o.w,o.h),r=o.w-2*a,l=o.h-2*a,c=Math.max(1,Math.round(Math.sqrt(n.length*r/Math.max(l,1)))),h=Math.ceil(n.length/c),d=r/c,p=l/Math.max(h,1),u=n.map((e,t)=>({x:o.x+a+d*(t%c+.5),y:o.y+a+p*(Math.floor(t/c)+.5)}));Oi(u,o,s,.5*a),n.forEach((t,i)=>e[t.id]=Wh(u[i]))}}return e}_pos(e){const t=this._renderDeviceSnapshot?.positions.get(e.id);return t?{x:t.x,y:t.y}:this._livePos(e)}_livePos(e){const t=this._spaceModelById(e.space),i=this._layout[e.id];if(i)if(this._norm){if(i.s===e.space)return{x:i.x*Ov,y:i.y*Ov}}else if(void 0===i.s)return{x:i.x,y:i.y};return this._defPos[e.id]?this._defPos[e.id]:t?Wh(sd(t)):{x:500,y:500}}_savePos(e,t,i){if(this._spaceModelById(e.space)){if(this._norm){const s=this._gridPitch,n=Math.round(t/s)*s,o=Math.round(i/s)*s,a=this._layout[e.id]?.k;this._layout={...this._layout,[e.id]:{s:e.space,x:Gh(n/Ov),y:Gh(o/Ov),...a?{k:a}:{}}}}else this._layout={...this._layout,[e.id]:{x:Math.round(t),y:Math.round(i)}};this._dirtyPos.add(e.id),this._persistLayout()}}_visualSamples(e){return I_(this._renderPlanHass,e,this._renderDevices,void 0,this._fullRegistryHass).samples}_devicePresentation(e,t=!1!==this._config?.show_signal,i=!1){const s=i?null:this._renderDeviceSnapshot?.presentations.get(L_(e.id,t));return s||N_(this._renderPlanHass,e,{liveStates:!1!==this._config?.live_states,showTemperature:!1!==this._config?.show_temperature,showSignal:t,designPreview:i,activityRuntime:this._activityRt.get(e.id),sourceDetails:!1,lightDevices:this._renderDevices,registryHass:this._fullRegistryHass,reducedMotion:this._reducedMotion})}_deviceVisual(e){return this._devicePresentation(e).visual}_stateClass(e,t=this._deviceVisual(e)){const i=this._devicePresentation(e);return i.effectiveHidden?"":("icon_ripple"!==i.display||!1===this._config?.live_states||"alarm"===t.status||t.activity,F_({...i,visual:t}).join(" "))}_liveTemp(e){if(!this._config?.show_temperature)return null;if(!0===e.marker?.use_climate_temp){const t=go(this._renderPlanHass,e.entities);if(null!=t)return t}return"mdi:thermometer"!==e.icon&&"mdi:air-filter"!==e.icon?null:mo(this._renderPlanHass,e.entities)}_bindingEntities(e){if("virtual"===e||!e)return[];const t=this._bindingStatus(e);return"active"===t.kind?t.enabledEntityIds:t.allEntityIds}_bindingHasClimate(e){return this._bindingEntities(e).some(e=>e.startsWith("climate."))}_bindingHasAlarm(e){return this._bindingEntities(e).some(e=>{const t=this._planHass?.states?.[e]||this.hass?.states?.[e],i=this._fullRegistryHass.entities[e]||this.hass?.entities?.[e];return Bs(e.split(".")[0],t?.attributes?.device_class||i?.device_class||i?.original_device_class)})}_liveHum(e){return this._config?.show_temperature&&e.primary&&fo(this.hass,e.primary)?vo(this.hass,e.entities):null}_deviceBindingActive(e,t=!0){if(e.virtual||"virtual"===e.bindingKind)return!0;if(!e.bindingKind||!e.bindingRef)return!1;const i=this._bindingStatus(`${e.bindingKind}:${e.bindingRef}`);return"active"===i.kind||(t&&this._showToast(this._t("ha_disabled"===i.kind?"toast.ha_disabled_action":"toast.ha_binding_unverified")),!1)}_openMoreInfo(e){e?this._planEntityAvailable(e)?(this._nativeMoreInfoEntity=e,qv(this,"hass-more-info",{entityId:e})):this._showToast(this._t("toast.ha_disabled_action")):this._showToast(this._t("toast.no_entity"))}_interruptViewGesture(e,t){if(clearTimeout(this._holdTimer),clearTimeout(this._kioskHoldTimer),void 0!==e)for(const i of[t,this._stageEl])try{i?.hasPointerCapture?.(e)&&i.releasePointerCapture(e)}catch{}this._pointers.clear(),this._panStart=null,this._panLock=null,this._pinchStart=null,this._swipeStart=null}_closeInfoCard(){this._interruptViewGesture(),this._holdFired=!1,this._infoCard=null}_ctxDevice(e,t){"view"===this._mode&&(e.preventDefault(),e.stopPropagation(),this._deviceBindingActive(t)&&(t.primary?this._openMoreInfo(t.primary):this._infoCard=t))}_clickDevice(e,t){if(e.stopPropagation(),this._drag?.moved||this._suppressClick||this._holdFired)return;if("plan"===this._mode)return;if("devices"===this._mode)return void this._openMarkerDialog(t);const i=this._devices.find(e=>e.id===t.id);if(!i)return;const s=Vp(i.tapAction,i.primary?.split(".")[0]),n=(e,t)=>{i.marker?.tap_confirm?this._tapConfirm={kind:"run",text:e,exec:t}:t()};if("toggle"===s){const e=this._toggleIntent(i);if(!e)return;if(!Qp(e))return void this._showUnavailableToggleTargets(e);const t=e=>{const t=Qp(e);if(!t)return;if("virtual-light"===t.kind)return this._startDevicePressFeedback(i.id),void this.hass.callWS({type:"houseplan/virtual_light/toggle",marker_id:t.markerId}).then(e=>{const t=wn(this._virtualLights,e);t!==this._virtualLights&&(this._virtualLights=t,this._capturedSnapshotVirtual="",this._cacheSnapshot(),this.requestUpdate())}).catch(e=>this._showToast(this._t("toast.virtual_light_toggle_failed",{err:this._errText(e)})));const{command:s}=t;this._startDevicePressFeedback(i.id),this.hass.callService(s.domain,s.service,s.data).catch(e=>this._showToast(this._t("toast.error",{err:this._errText(e)})))},s=((o=e).targets.length?o.targets:o.skippedTargets).map(e=>e.name||e.entityId||e.ref).join(", ")||i.name;if(i.marker?.tap_confirm){const n=this._toggleConfirmationLines(e);if(!n.length)return;this._tapConfirm={kind:"toggle",text:this._t("confirm.tap_toggle",{name:s}),lines:n,initialIntent:e,deviceId:i.id,exec:()=>{const s=this._devices.find(e=>e.id===i.id),n=s?this._toggleIntent(s):null;n&&!Qp(n)&&this._showUnavailableToggleTargets(n)||(n&&tu(e,n)?t(n):this._showToast(this._t("toast.tap_target_changed")))}}}else t(e);return}var o;if("info"===s)return this._interruptViewGesture(),void(this._infoCard=i);if(this._deviceBindingActive(i)){if("run"===s){const e=i.marker?.tap_target||"",t=function(e){const t=String(e||"").split(".")[0];return"automation"===t?{domain:"automation",service:"trigger"}:"script"===t?{domain:"script",service:"turn_on"}:"scene"===t?{domain:"scene",service:"turn_on"}:null}(e),s=this.hass.states[e];if(!t||!s)return void this._showToast(this._t("toast.run_target_missing"));const o=s.attributes?.friendly_name||e;return void n(this._t("confirm.tap_run",{name:o}),()=>{this._deviceBindingActive(i)&&this._planEntityAvailable(e)&&(this._startDevicePressFeedback(i.id),this.hass.callService(t.domain,t.service,{entity_id:e}).then(()=>{this._stampActivity(i.id,"event",this._activitySourceKey(i)),this.requestUpdate(),this._showToast(this._t("toast.run_started",{name:o}))}).catch(e=>this._showToast(this._t("toast.error",{err:this._errText(e)}))))})}"more-info"===s&&i.primary?this._openMoreInfo(i.primary):this._infoCard=i}}_showUnavailableToggleTargets(e){const t=function(e){if(!e||"group"!==e.kind||Qp(e)||"configured-targets-missing"!==e.noneReason)return[];if(!e.skippedTargets.length||e.skippedTargets.some(e=>!eu.has(e.reason)&&"secure"!==e.reason))return[];const t=[],i=new Set;for(const s of e.skippedTargets){if(!eu.has(s.reason))continue;const e=String(s.name||s.entityId||s.ref||"").trim();e&&!i.has(e)&&(i.add(e),t.push(e))}return t}(e);return!!t.length&&(this._showToast(1===t.length?this._t("toast.toggle_target_unavailable",{name:t[0]}):this._t("toast.toggle_targets_unavailable",{names:t.join(", ")})),!0)}_keyDevice(e,t){"Enter"!==e.key&&" "!==e.key||"view"!==this._mode&&"devices"!==this._mode||(e.preventDefault(),this._clickDevice(e,t))}_startDevicePressFeedback(e){const t=[...this.renderRoot.querySelectorAll(".dev[data-id]")].find(t=>t.dataset.id===e),i=t?.querySelector(".device-shell");if(!i||"function"!=typeof i.animate)return;const s=Number.parseFloat(this.ownerDocument.defaultView?.getComputedStyle(i).scale||""),n=Number.isFinite(s)?Math.max(.95,Math.min(1,s)):1;this._devicePressAnimations.get(e)?.cancel();const o=this._reducedMotion?[{outlineColor:"transparent",outlineStyle:"solid",outlineWidth:"0px"},{outlineColor:"var(--hp-accent, #3ea6ff)",outlineStyle:"solid",outlineWidth:"2px",outlineOffset:"2px",offset:.5},{outlineColor:"transparent",outlineStyle:"solid",outlineWidth:"0px"}]:[{scale:String(n)},{scale:"0.95",offset:.5},{scale:"1"}],a=i.animate(o,{duration:200,easing:"cubic-bezier(.22,.61,.36,1)"});this._devicePressAnimations.set(e,a);const r=()=>{this._devicePressAnimations.get(e)===a&&this._devicePressAnimations.delete(e)};a.addEventListener("finish",r,{once:!0}),a.addEventListener("cancel",r,{once:!0})}_cancelDevicePressFeedback(){for(const e of this._devicePressAnimations.values())e.cancel();this._devicePressAnimations.clear()}_pruneDevicePressFeedback(){if(!this._devicePressAnimations.size)return;const e=new Set([...this.renderRoot.querySelectorAll(".dev[data-id]")].map(e=>e.dataset.id||""));for(const[t,i]of this._devicePressAnimations)e.has(t)||(i.cancel(),this._devicePressAnimations.delete(t))}_t(e,t){return ua(pa(this.hass,this._config?.language),e,t)}get _colorPickerLabels(){return{title:this._t("color_picker.title"),hue:this._t("color_picker.hue"),saturation:this._t("color_picker.saturation"),value:this._t("color_picker.value"),hex:this._t("color_picker.hex"),invalidHex:this._t("color_picker.invalid_hex")}}_help(e){const t=`${e}.aria`,i=pa(this.hass,this._config?.language);return _a(i,e)&&_a(i,t)?q``:G}get _stageEl(){return this.renderRoot.querySelector(".stage")}_contentItems(e){const t=[];for(const i of this._devices){if(i.space!==e.id||i.hidden)continue;const s=this._pos(i);t.push({minX:s.x,minY:s.y,maxX:s.x,maxY:s.y})}if(e.id===this._space){for(const e of this._openingsR){const i=Number(e.angle)*Math.PI/180,s=Math.cos(i)*e.rlen/2,n=Math.sin(i)*e.rlen/2,o=Yh([[e.rx-s,e.ry-n],[e.rx+s,e.ry+n]]);o&&t.push(o)}const i=this._decorH;for(const e of this._decorList){const s=Yh("line"===e.kind?[[e.x1*Ov,e.y1*i],[e.x2*Ov,e.y2*i]]:"text"===e.kind?[[e.x*Ov,e.y*i]]:Yr({x:e.x*Ov,y:e.y*i,w:e.w*Ov,h:e.h*i,angle:e.angle}));s&&t.push(s)}for(const i of this._physicalBodiesR(e)){const e=Yh(i);e&&t.push(e)}}return Xh(e,t)}_frameOf(){const e=this._spaceModel();if(!e){this._frame=null;const e={x:0,y:0,w:Ov,h:Ov};return{rect:e,all:e,outliers:0}}const t=this._frame,i="view"!==this._mode;if(t&&t.id===e.id&&this._bdDrag)return t;if(t&&t.id===e.id&&t.model===e&&t.layout===this._layout&&t.devs===this._devices&&t.far===this._showFar&&t.grow===i)return t;const s=td(this._contentItems(e));let n=s.all||id(e),o=this._showFar?n:s.core||id(e);return t&&t.id===e.id&&i&&t.grow&&(o=Lv(t.rect,o),n=Lv(t.all,n)),this._frame={id:e.id,model:e,layout:this._layout,devs:this._devices,far:this._showFar,grow:i,rect:o,all:n,outliers:s.outliers},this._frame}_isoSource(){const e=this._spaceModel();if(!e)return null;const t=this._spaceWalls,i=this._openCuts(),s=this._openingsR.map((e,t)=>({id:String(e.id||t),sourceIndex:t,type:e.type,x:e.rx,y:e.ry,angle:Number(e.angle)||0,length:e.rlen>0?e.rlen:900,flipH:!!e.flip_h,flipV:!!e.flip_v})),n=Dh(64,this._cellCm),o=Dh(10,this._cellCm),a=`${e.id}|${function(e){let t=2166136261;const i=e=>{if("number"!=typeof e)if("string"!=typeof e)if("boolean"!=typeof e)if(Array.isArray(e)){t=qf(t,e.length);for(const t of e)i(t)}else{if(e&&"object"==typeof e){const t=e;for(const e of Object.keys(t).sort())i(e),i(t[e]);return}t=qf(t,-1)}else t=qf(t,e?1:0);else for(const i of e)t=qf(t,i.codePointAt(0)||0);else t=qf(t,e)};return i(e),t.toString(36)}({rooms:e.rooms,walls:t,openCuts:i,openings:s,partitions:e.partitions,roomDrafts:e.room_drafts,columns:e.wall_columns,cellCm:this._cellCm,gridPitch:this._gridPitch,wallKeyPitch:this._wallKeyPitch,camera:If,wallHeight:n,floorEdgeHeight:o,algorithm:3})}`;return{key:a,build:()=>{const o=_h(e,this._cellCm,this._gridPitch,2e-4*this._gridPitch,this._partitionOpeningCuts(e)).all,a=this._roomWallOpeningInputs(this._openingsR,e),r=t.length||o.length?qc(e.rooms,t,i,a,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov,o):null;if(r&&("failed-core"===r.status||"not-applicable"===r.status))throw new Error("wall boolean geometry failed");const l=r?.paperGeom??Hc(e.rooms,t,i,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov);if(!l)throw new Error("floor boolean geometry failed");const c=r?.openingIndex||Kc(e.rooms,t,i,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov),h=s.map(e=>{const i=this._openingsR[e.sourceIndex],s="gate"===e.type?!e.flipV:e.flipV,o=i?.partitionHost?cu(i.partitionHost,s):t.length||"gate"===e.type?Zc(c,{x:e.x,y:e.y,angle:e.angle,length:e.length,flip_v:s}):{ox:0,oy:0,cm:0,side:-1};return function(e,t=64){if(!(Wf([e.x,e.y,e.angle,e.length,e.face.ox,e.face.oy,t])&&e.length>0&&t>0))throw new Error("invalid isometric opening input");const i=e.length/2;let s;if("passage"===e.type)s=[];else if("gate"===e.type){const n=10*e.face.side;s=[Gf(e,0,[-i,0],[i,0],n,0,.88*t),Gf(e,1,[i,0],[-i,0],-n,0,.88*t)]}else s="window"===e.type?[Gf(e,0,[-i,0],[i,0],-90,.27*t,.78*t),Gf(e,1,[i,0],[-i,0],90,.27*t,.78*t)]:[Gf(e,0,[-i,0],[e.length,0],-90,0,.92*t)];return{id:e.id,sourceIndex:e.sourceIndex,type:e.type,leaves:Object.freeze(s.map(e=>Object.freeze(e)))}}({...e,face:o},n)});return{walls:r?.components.flatMap(e=>e.geom)||[],floor:l,openings:Object.freeze(h)}}}}_isoSceneKey(){if(!this._labsIso||"view"!==this._mode)return null;try{return this._isoSource()?.key??null}catch{return`${this._space}|invalid`}}_isoScene(){const e=this._isoSource();if(!e)return null;const t=this._isoGeometryCache.get(e.key);if(t)return{key:e.key,...t};const i=this._frameOf().rect,s=e.build(),n=Dh(64,this._cellCm),o=Dh(10,this._cellCm),a=function(e){let t=1/0,i=1/0,s=-1/0,n=-1/0;for(const o of e)for(const e of o.leaves){const o=Math.hypot(e.closedVector[0],e.closedVector[1]);t=Math.min(t,e.hinge[0]-o),i=Math.min(i,e.hinge[1]-o),s=Math.max(s,e.hinge[0]+o),n=Math.max(n,e.hinge[1]+o)}return Wf([t,i,s,n])?{x:t,y:i,w:s-t,h:n-i}:null}(s.openings),r=Nf({rect:a?Lv(i,a):i,wallHeight:n,openingHeight:n,floorDepth:o}),l=function(e,t=If,i=64){if(!Number.isFinite(i)||i<0)throw new Error("invalid wall height");const s=[],n=[],o=[];let a=0;for(let r=0;r<(e||[]).length;r++){const l=e[r];for(let e=0;e<(l||[]).length;e++){const c=Hf(l[e],e>0);if(!(c.length<3||Math.abs(Of(c))<1e-9)){s.push(Lf(c,t,i)),a+=c.length;for(let s=0;se.depth-t.depth||e.polygon-t.polygon||e.ring-t.ring||e.edge-t.edge),{topPath:s.join(" "),sides:n,contactPath:o.join(" "),edgeCount:a}}(s.walls,If,n),c={geometry:l,floor:jf(s.floor,o),openings:s.openings,frame:r};return Mv(this._isoGeometryCache,e.key,c,8),{key:e.key,...c}}_effectiveProjection(){if("iso"!==this._desiredProjection||!this._model.length)return"flat";const e=this._isoSceneKey()||`${this._space}|invalid`;if(this._isoFallback.has(e))return"flat";try{const e=this._isoScene();return e?(t="iso",i=e.key,s=this._isoFallback,"iso"===t&&i&&!s.has(i)?"iso":"flat"):"flat"}catch(t){if(!this._isoFallback.has(e)){this._isoFallback.add(e);const i=e.split("|");console.warn(`HOUSEPLAN ISO FALLBACK: #89, space ${this._space}, fingerprint ${i[i.length-1]}, ${t instanceof Error?t.message:"renderer error"}`)}return"flat"}var t,i,s}_scenePoint(e){return"iso"===this._renderProjection?Af(e,0):e}_floorView(e){if("iso"!==this._renderProjection)return e;const t=zf([e.x,e.y]),i=zf([e.x+e.w,e.y+e.h]);return{x:t[0],y:t[1],w:i[0]-t[0],h:i[1]-t[1]}}_baseVb(){if("iso"===this._effectiveProjection()){if(!this._spaceDisplayForRender().showBorders){const e=Nf({rect:this._frameOf().rect,wallHeight:Dh(64,this._cellCm)});return[e.x,e.y,e.w,e.h]}const e=this._isoScene()?.frame??this._frameOf().rect;return[e.x,e.y,e.w,e.h]}const e=this._frameOf().rect;return[e.x,e.y,e.w,e.h]}get _outliers(){return this._showFar?0:this._frameOf().outliers}_fitFar(){this._showFar=!0,this._frame=null,this._resetZoom()}_fitAll(){this._showFar=!0,this._frame=null,this._resetZoom()}_renderFarHint(){return this._kiosk||"view"!==this._mode||this._booting||!this._outliers?G:q`
- - ${this._t("canvas.far_objects",{n:this._outliers})} - -
`}_renderHomeArrow(){if(this._booting)return G;const e=this._view;if(!e||!e.w||!e.h)return G;const t=this._baseVb(),i=t[0],s=t[1],n=t[2],o=t[3];if(!(i+n<=e.x||i>=e.x+e.w||s+o<=e.y||s>=e.y+e.h))return G;const a=Math.atan2(s+o/2-(e.y+e.h/2),i+n/2-(e.x+e.w/2)),r=50+38*Math.cos(a),l=50+38*Math.sin(a);return q``}_stageAspect(){const e=this._stageEl,t=this._baseVb();return e&&e.clientHeight?e.clientWidth/e.clientHeight:t[2]/t[3]}_viewOr(e){return this._view&&this._view.w?this._view:Ei(e,this._stageAspect())}_roomLabelReferenceViewWidth(e){if(!this._markup)return e.w;const t=this._stageEl,i=this.renderRoot.querySelector(".editorchrome");if(!t||t.clientWidth<=0||t.clientHeight<=0)return e.w;const s=t.clientHeight+(i?.getBoundingClientRect().height||0);return s<=0?e.w:this._viewForModeTarget(this._zoom,void 0,void 0,t.clientWidth,s).w}_screenToVb(e,t){const i=this._stageEl,s=this._viewOr(this._baseVb()),n=i?.clientWidth||1,o=i?.clientHeight||1;return[s.x+e/n*s.w,s.y+t/o*s.h]}_clampView(e,t){const i=(e,t,i,s)=>{const n=1*Math.max(t,s),o=i-n,a=i+s-t+n;return Math.max(Math.min(o,a),Math.min(Math.max(o,a),e))};return{w:e.w,h:e.h,x:i(e.x,e.w,t.x,t.w),y:i(e.y,e.h,t.y,t.h)}}_applyView(e,t,i){const s=this._baseVb(),n=Ei(s,this._stageAspect()),o=Math.min(Vv.ZOOM_MAX,Math.max(Vv.ZOOM_MIN,e)),a=n.w/o,r=n.h/o,l=this._viewOr(s),c=t??l.x+l.w/2,h=i??l.y+l.h/2,d=this._clampView({x:c-a/2,y:h-r/2,w:a,h:r},n);return!(this._view&&Math.abs(this._zoom-o)<1e-9&&Math.abs(this._view.x-d.x)<1e-6&&Math.abs(this._view.y-d.y)<1e-6&&Math.abs(this._view.w-d.w)<1e-6&&Math.abs(this._view.h-d.h)<1e-6)&&("opening"===this._tool&&(this._cursorPt=null,this._clearOpeningPlacement(!1)),this._zoom=o,this._view=d,!0)}_bootWatch(){clearTimeout(this._bootTimer),this._bootStart=Date.now(),this._bootLastH=-1,this._bootLastChange=this._bootStart;const e=()=>{if(!this._booting)return;const t=Date.now(),i=this._stageEl?this._stageEl.clientHeight:0;i!==this._bootLastH&&(this._bootLastH=i,this._bootLastChange=t);const s=t-this._bootStart;s>=1200||s>=700&&i>0&&t-this._bootLastChange>=250?this._bootSettled():this._bootTimer=window.setTimeout(e,100)};this._bootTimer=window.setTimeout(e,100)}_bootSettled(){this._booting&&!this._bootSettling&&(this._bootSettling=!0,this._refitView(),this._bootSettleRaf=requestAnimationFrame(()=>{this._bootSettleRaf=0,this._finishBootSettled()}))}_finishBootSettled(){if(!this._booting)return;this._refitView(),this._booting=!1,this._bootSettling=!1;const e=this._stageEl?.clientHeight??0;!this._config?.kiosk&&e>0&&this._warmPatch({hdrH:this._hdrH,stageH:e,vp:this._warmViewportState()},!0),this._bootFading=!0,this._bootTimer=window.setTimeout(()=>{this._bootFading=!1},220),this._bootSoft=!0,clearTimeout(this._bootSoftTimer),this._bootSoftTimer=window.setTimeout(()=>{this._bootSoft=!1},xv)}_bootSoftCancel(){this._bootSoft&&(clearTimeout(this._bootSoftTimer),this._bootSoft=!1)}_beginResumeSettle(){this._booting||this._resumeSettling||(this._modeTransitionPreparing?this._modeTransitionForceAtomic=!0:this._cancelModeTransition(!0),this._viewportInvalidAt=0,this._beginContinuityCandidate("warm-resume",!1),this._loading?this.requestUpdate():this._loadFromServer())}_refitView(){if(this._modeTransitionBusy)return;const e=this._stageEl;if(!e||"visible"!==document.visibilityState||e.clientWidth<=0||e.clientHeight<=0)return void(this._viewportInvalidAt||(this._viewportInvalidAt=Date.now()));const t=[e.clientWidth,e.clientHeight],i=this._lastValidStageSize,s=!!i&&Math.abs(i[0]-t[0])<=.5&&Math.abs(i[1]-t[1])<=.5,n=this._viewportInvalidAt?Date.now()-this._viewportInvalidAt:0;if(this._viewportInvalidAt=0,!i)return this._lastValidStageSize=t,void(this._view||this._applyView(this._zoom));s?this._pendingRefitSize=null:(this._pendingRefitSize=t,this._refitRaf||(this._refitRaf=requestAnimationFrame(()=>{this._refitRaf=0;const e=this._pendingRefitSize;this._pendingRefitSize=null;const t=this._stageEl;if(!e||!t||t.clientWidth<=0||t.clientHeight<=0)return;if(Math.abs(t.clientWidth-e[0])>.5||Math.abs(t.clientHeight-e[1])>.5)return void this._refitView();const i=this._lastValidStageSize;if(i&&Math.abs(i[0]-e[0])<=.5&&Math.abs(i[1]-e[1])<=.5)return;this._lastValidStageSize=e;const s=this._view;n>=Wd?this._beginContinuityCandidate("stage-size-restored",!0,"stage-size"):this._continuity.hasCompleteFrame&&this._beginContinuityCandidate("stage-resize",!0,"stage-size"),this._applyView(this._zoom,s?s.x+s.w/2:void 0,s?s.y+s.h/2:void 0)})))}_zoomAt(e,t,i){const s=this._stageEl;if(!s)return;const n=Ei(this._baseVb(),this._stageAspect()),o=Math.min(Vv.ZOOM_MAX,Math.max(Vv.ZOOM_MIN,i));"opening"===this._tool&&(this._cursorPt=null,this._clearOpeningPlacement(!1));const a=s.clientWidth,r=s.clientHeight,l=this._screenToVb(e,t),c=n.w/o,h=n.h/o;this._zoom=o,this._view=this._clampView({x:l[0]-e/a*c,y:l[1]-t/r*h,w:c,h:h},n)}_onWheel(e){const t=this._stageEl;if(!t)return;e.preventDefault();const i=t.getBoundingClientRect(),s=e.deltaY<0?1.15:1/1.15;this._zoomAt(e.clientX-i.left,e.clientY-i.top,this._zoom*s),this._saveZoom()}_stepZoom(e){const t=this._stageEl;t&&(this._zoomAt(t.clientWidth/2,t.clientHeight/2,this._zoom*(e>0?1.4:1/1.4)),this._saveZoom())}_resetZoom(){const e=this._baseVb();"opening"===this._tool&&(this._cursorPt=null,this._clearOpeningPlacement(!1)),this._zoom=1,this._view=Ei(e,this._stageAspect()),this._saveZoom()}_saveZoom(){if("view"===this._mode){this._zoomBySpace={...this._zoomBySpace,[this._space]:this._zoom};try{localStorage.setItem(Av,JSON.stringify(this._zoomBySpace))}catch{}}}_restoreZoom(){const e=this._zoomBySpace[this._space]||1;this._zoom=e;const t=this._stageEl;if(t&&t.clientHeight){const t=this._baseVb();return this._applyView(e,t[0]+t[2]/2,t[1]+t[3]/2),void this.requestUpdate()}this._view=null,requestAnimationFrame(()=>{if(!this._stageEl)return;const t=this._baseVb();this._applyView(e,t[0]+t[2]/2,t[1]+t[3]/2),this.requestUpdate()})}_stagePointerDown(e){if(this._vacFit)return;if(this._kiosk&&(this._cyclePausedUntil=Date.now()+6e4,0===this._pointers.size?(this._swipeStart={x:e.clientX,y:e.clientY,id:e.pointerId},e.target.closest?.(".dev, .roomlabel, .oplock")||(clearTimeout(this._kioskHoldTimer),this._kioskHoldTimer=window.setTimeout(()=>{this._kioskDialog=!0,this._swipeStart=null},3e3))):(this._swipeStart=null,clearTimeout(this._kioskHoldTimer))),this._drag)return;if(this._markup&&e.target.closest?.(".roomlabel, .rlhandle, .rszhandle, .dev, .oplock, .op-hit, button"))return;if("devices"===this._mode&&e.target.closest(".dev"))return;if("decor"===this._mode&&this._decorPointerDown(e))return;this._pointers.set(e.pointerId,{x:e.clientX,y:e.clientY});const t=this._viewOr(this._baseVb());if(1===this._pointers.size)this._panStart={sx:e.clientX,sy:e.clientY,vx:t.x,vy:t.y},this._panLock=null,this._suppressClick=!1;else if(2===this._pointers.size){"opening"===this._tool?(this._cursorPt=null,this._clearOpeningPlacement(!1)):"draw"===this._tool&&this._clearPlanSnapHover();const e=[...this._pointers.values()],t=Math.hypot(e[0].x-e[1].x,e[0].y-e[1].y);this._pinchStart={dist:t,zoom:this._zoom},this._panStart=null,this._panLock=null}}get _swipeZone(){return!this._hasFixedFloor&&this._kiosk&&this._zoom<=1.001&&this._model.length>1}_stagePointerMove(e){if(this._physicalRotate?.pid!==e.pointerId)if(this._physicalDrag?.pid!==e.pointerId)if(this._dtDrag?.pid!==e.pointerId)if(this._bdDrag?.pid!==e.pointerId){if(this._decorDraft?.pid===e.pointerId){const t=this._decorDraft;let i=this._decorSnap(this._svgPoint(e),e.pointerType);if(e.shiftKey&&("rect"===t.kind||"ellipse"===t.kind)){const e=i[0]-t.a[0],s=i[1]-t.a[1],n=Math.max(Math.abs(e),Math.abs(s));i=this._snap([t.a[0]+(e<0?-n:n),t.a[1]+(s<0?-n:n)])}return void(this._decorDraft={...t,b:i})}if(this._decorMove?.pid!==e.pointerId)if(this._pointers.has(e.pointerId)){if(this._pointers.set(e.pointerId,{x:e.clientX,y:e.clientY}),this._markup&&1===this._pointers.size&&this._markupMove(e),this._pinchStart&&this._pointers.size>=2){"opening"===this._tool?(this._cursorPt=null,this._clearOpeningPlacement(!1)):"draw"===this._tool&&this._clearPlanSnapHover();const e=[...this._pointers.values()],t=Math.hypot(e[0].x-e[1].x,e[0].y-e[1].y)/(this._pinchStart.dist||1),i=this._stageEl.getBoundingClientRect(),s=(e[0].x+e[1].x)/2-i.left,n=(e[0].y+e[1].y)/2-i.top;this._zoomAt(s,n,this._pinchStart.zoom*t),this._suppressClick=!0,this._saveZoom()}else if(this._panStart){const t=e.clientX-this._panStart.sx,i=e.clientY-this._panStart.sy;Math.abs(t)+Math.abs(i)>4&&(this._suppressClick=!0,clearTimeout(this._holdTimer),"opening"===this._tool?(this._cursorPt=null,this._clearOpeningPlacement(!1)):"draw"===this._tool&&this._clearPlanSnapHover()),null===this._panLock&&Math.abs(t)+Math.abs(i)>8&&(this._panLock=this._swipeZone&&Math.abs(t)>1.5*Math.abs(i)?"swipe":"pan");const s=this._stageEl;if("pan"===this._panLock&&s){const e=this._baseVb(),n=this._viewOr(e),o=Ei(e,this._stageAspect());this._view=this._clampView({x:this._panStart.vx-t/(s.clientWidth||1)*n.w,y:this._panStart.vy-i/(s.clientHeight||1)*n.h,w:n.w,h:n.h},o)}}}else this._markupMove(e);else this._decorMoveUpdate(e)}else this._bdMove(e);else this._dtMove(e);else this._physicalMove(e);else this._physicalRotateMove(e)}_stagePointerLeave(e){this._markup&&("opening"===this._tool?(this._cursorPt=null,this._clearOpeningPlacement(!1)):"draw"===this._tool&&this._clearPlanSnapHover())}_stagePointerUp(e){if(this._kiosk){clearTimeout(this._kioskHoldTimer);const t=this._swipeStart;if(this._swipeStart=null,t&&t.id===e.pointerId){const i=e.clientX-t.x,s=e.clientY-t.y;if(Math.abs(i)+Math.abs(s)<8){const e=Date.now();e-this._lastTap<350&&this._resetZoom(),this._lastTap=e}const n="pan"===this._panLock?null:function(e,t,i,s,n,o=60){if(i>1.001||s.length<2)return null;if(Math.abs(e)e.id),this._space);n&&this._slideTo(n,i<0?"left":"right")&&(this._saveNav(),this._suppressClick=!0,setTimeout(()=>this._suppressClick=!1,0),this._showKioskDots())}}if(this._physicalDrag?.pid===e.pointerId)return void this._physicalUp(e);if(this._physicalRotate?.pid===e.pointerId)return void this._physicalRotateUp(e);if(this._dtDrag?.pid===e.pointerId)return void this._dtUp();if(this._bdDrag?.pid===e.pointerId)return void this._bdUp();if(this._decorDraft?.pid===e.pointerId)return void this._decorCommitDraft();if(this._decorMove?.pid===e.pointerId)return this._decorMove.moved&&(this._recordGeometry(this._t("history.decor_move"),this._decorMove.before),this._saveConfig()),void(this._decorMove=null);const t=!!this._pinchStart||!!this._panStart;this._pointers.delete(e.pointerId),this._pointers.size<2&&(this._pinchStart=null),0===this._pointers.size&&(this._panStart=null,this._panLock=null,setTimeout(()=>this._suppressClick=!1,0)),t&&0===this._pointers.size&&this.requestUpdate()}_clickRoom(e){!this._suppressClick&&e.area&&Wv("/config/areas/area/"+e.area)}_pointerDown(e,t){if("plan"===this._mode)return;if("view"===this._mode){this._holdFired=!1,clearTimeout(this._holdTimer);const i=e.pointerId,s=e.currentTarget;return void(this._holdTimer=window.setTimeout(()=>{this._holdFired=!0,this._interruptViewGesture(i,s),this._infoCard=t},600))}if("ha_disabled"===t.bindingStatus?.kind)return;e.preventDefault();const i=this._pos(t);this._drag={id:t.id,sx:e.clientX,sy:e.clientY,ox:i.x,oy:i.y,moved:!1},Gv(e),this._tip=null}_pointerMove(e,t){if(!this._drag||this._drag.id!==t.id)return;const i=this.renderRoot.querySelector(".stage");if(!i)return;const s=this._baseVb(),n=i.getBoundingClientRect(),o=this._viewOr(s),a=(e.clientX-this._drag.sx)/n.width*o.w,r=(e.clientY-this._drag.sy)/n.height*o.h;Math.abs(e.clientX-this._drag.sx)+Math.abs(e.clientY-this._drag.sy)>3&&(this._drag.moved=!0,clearTimeout(this._holdTimer));const l=Uh(this._drag.ox+a),c=Uh(this._drag.oy+r);this._savePos(t,l,c)}_pointerUp(e,t){if(clearTimeout(this._holdTimer),!this._drag||this._drag.id!==t.id)return;const i=this._drag.moved;this._drag=i?this._drag:null,i&&(this._selId=t.id,window.setTimeout(()=>this._drag=null,0))}_showToast(e){for(const e of this.renderRoot.querySelectorAll("hp-dialog"))e.closeTransientOverlays("toast");this._toast=e,clearTimeout(this._toastTimer),this._toastTimer=window.setTimeout(()=>{this._toast=""},3500)}_syncPointerHoverTargets(){const e=this._pointerModality.hoverEnabled;for(const t of this.renderRoot.querySelectorAll(Ev))t.toggleAttribute("data-pointer-hover",e)}_syncPointerHoverSubtree(e){if(e.nodeType!==Node.ELEMENT_NODE)return;const t=e,i=this._pointerModality.hoverEnabled;t.matches(Ev)&&t.toggleAttribute("data-pointer-hover",i);for(const e of t.querySelectorAll(Ev))e.toggleAttribute("data-pointer-hover",i)}_clearTransientHover(e=!1){e&&this._pointerModality.suspend(),this._tip&&(this._tip=null),this._hoverRoom&&(this._hoverRoom=null)}_notePointer(e){const t=this._pointerModality.modality,i=this._pointerModality.note(e);"touch"!==i&&"pen"!==i||t===i&&!this._tip&&!this._hoverRoom||this._clearTransientHover()}_guardTouchGesture(e){if(this._editorSecondary.handleOutsideDismiss(e))return;if("click"===e.type){if(!this._suppressClick&&!this._touchSequenceMultitouch&&Date.now()>this._touchClickBlockUntil)return;return e.preventDefault(),void e.stopImmediatePropagation()}const t=e;if(this._notePointer(t),"touch"===t.pointerType)if("pointerdown"!==e.type){if("pointermove"===e.type){const e=this._touchContacts.get(t.pointerId);if(!e)return;const i={...e,x:t.clientX,y:t.clientY};if(this._touchContacts.set(t.pointerId,i),this._touchSequenceMultitouch&&"view"===this._mode&&!this._vacFit&&this._pinchStart&&[...this._touchContacts.values()].every(e=>e.inStage)){this._pointers.set(t.pointerId,{x:t.clientX,y:t.clientY});const e=[...this._touchContacts.values()];if(e.length>=2&&this._stageEl){const[t,i]=e,s=Math.hypot(t.x-i.x,t.y-i.y)/(this._pinchStart.dist||1),n=this._stageEl.getBoundingClientRect();this._zoomAt((t.x+i.x)/2-n.left,(t.y+i.y)/2-n.top,this._pinchStart.zoom*s),this._saveZoom()}}return}if("pointerup"===e.type||"pointercancel"===e.type||"lostpointercapture"===e.type){if(this._clearTransientHover(),this._touchContacts.delete(t.pointerId),this._touchSequenceMultitouch){if(this._touchClickBlockUntil=Date.now()+500,this._pointers.delete(t.pointerId),!this._vacFit&&this._pointers.size>=2){const[e,t]=[...this._pointers.values()];this._pinchStart={dist:Math.hypot(e.x-t.x,e.y-t.y),zoom:this._zoom}}else this._pinchStart=null;0===this._pointers.size&&(this._panStart=null,this._panLock=null)}0===this._touchContacts.size&&(this._touchSequenceMultitouch=!1)}}else if(this._touchContacts.set(t.pointerId,{x:t.clientX,y:t.clientY,inStage:!!t.target?.closest?.(".stage")}),this._touchContacts.size>=2){this._clearTransientHover(),this._touchSequenceMultitouch=!0,this._touchClickBlockUntil=Number.POSITIVE_INFINITY,clearTimeout(this._holdTimer),clearTimeout(this._kioskHoldTimer),this._swipeStart=null;const e=[...this._touchContacts.values()];if("view"===this._mode&&!this._vacFit&&e.every(e=>e.inStage)){this._pointers=new Map([...this._touchContacts].map(([e,t])=>[e,{x:t.x,y:t.y}]));const[t,i]=e;this._pinchStart={dist:Math.hypot(t.x-i.x,t.y-i.y),zoom:this._zoom},this._panStart=null,this._panLock=null}else this._vacFit&&(this._pointers.clear(),this._pinchStart=null,this._panStart=null,this._panLock=null)}}_showTip(e,t,i,s,n,o){this._notePointer(e),this._pointerModality.hoverEnabled&&(this._drag||(this._tip={x:e.clientX,y:e.clientY,title:t,meta:i,lqi:s,temp:n,hum:o}))}get _gridPitch(){return Bh}get _cellCm(){const e=Number(this._curSpaceCfg?.cell_cm);return Number.isFinite(e)&&e>0?e:5}_fmtLen(e,t){const i=function(e,t,i,s){return Math.hypot(t[0]-e[0],t[1]-e[1])/i*s}(e,t,this._gridPitch,this._cellCm);return li(i,"mi"===this.hass?.config?.unit_system?.length)}get _curSpaceCfg(){const e=this._resize.preview;return e&&e.space===this._space?e.sp:this._serverCfg?.spaces.find(e=>e.id===this._space)}get _renderCfg(){const e=this._resize.preview;return e&&this._serverCfg?{...this._serverCfg,spaces:this._serverCfg.spaces.map(t=>t.id===e.space?e.sp:t)}:this._serverCfg}get _spaceH(){return this._curSpaceCfg,Ov}get _segments(){const e=this._curSpaceCfg,t=this._spaceH;return pi(e?.rooms||[]).map(e=>[e[0]*Ov,e[1]*t,e[2]*Ov,e[3]*t])}_savedNav(){if(this._hasFixedFloor)return null;try{return JSON.parse(localStorage.getItem(zv)||"null")}catch{return null}}_saveNav(){if(!this._hasFixedFloor)try{localStorage.setItem(zv,JSON.stringify({space:this._space}))}catch{}}_leaveCardRoute(){if(this._routeDepartureHandled)return;this._routeDepartureHandled=!0,"view"!==this._mode&&this._setMode("view",!1),this._pendingNavMode=null,this._geometryHistory.clear(),this._activeDraftId=null,this._resumeDraftBySpace={},this._draftSegmentCms=[],this._closingWallCm=null,this._drawWallField=null,this._showHidden=!1,this._tapConfirm=null,this._vacCalConfirm=null,this._decorEraseConfirm=null,this._openingInfo=null,this._closeInfoCard(),this._rulesDialog=null,this._alignDialog=null,this._preflightClipboardFallback=null,this._backupImportDialog=null,this._backupExportDialog=null,this._settingsDialog=null,this._deviceInbox=null,this._deviceInboxReturn=null,this._markerDialog=null,this._openingDialog=null,this._physicalDialog=null,this._wallDialog=null,this._backdropDialog=null,this._decorShapeDialog=null,this._decorTextDialog=null,this._mergeDialog=null,this._roomDialog=!1,this._spaceDialog=null,this._importDialog=null;const e=this._warmSlot;e?.owner===this._warmGen&&(e.vp=this._warmViewportState(),e.dlg=null,e.frameFingerprint="",clearTimeout(e.evict),e.evict=0),this._saveNav()}_setMode(e,t=!0){this._endTabDrag(),this._pendingNavMode=null;const i=this._spaceModel();if(!i)return this._cancelModeTransition(!1),void(this._mode="view");if(this._kiosk&&"view"!==e)return;if(this._mode===e){if(!t&&"view"===e&&this._modeTransitionBusy){const t=this._currentModeVisual(e),i=this._modeTransitionTargetZoom,s=this._modeTransitionTargetCenterX,n=this._modeTransitionTargetCenterY;this._cancelModeTransition(!1),this._commitViewModeAtomic(t,i,s,n)}return}if(this._bootSoftCancel(),("plan"===e||"decor"===e)&&!this._norm)return void this._showToast(this._t("toast.markup_needs_server"));if(this._wallFaceBatch&&this._roomDialogCancel(),"plan"===this._mode&&"draw"===this._tool&&!this._finishWallChain())return;this._clearTransientHover(!0),this._cancelDevicePressFeedback();const s=this._mode,n=this._effectiveProjection(),o=this._currentModeVisual(s),a=this._modeTransitionBusy,r=o&&"view"!==s&&"view"!==e?{...o,toolbarContentOpacity:this._reducedMotion?1:.35}:o;this._cancelModeTransition(!1),this._editorSecondary.closeForNavigation(),(this._decorMove||this._dtDrag||this._bdDrag)&&this._cancelDecorGesture();const l=!i.bg&&"view"===e!=("view"===s),c=a&&"view"===s&&"view"!==e?this._modeTransitionEditorCamera:null;let h=c?.zoom??(a?this._modeTransitionTargetZoom:this._zoom),d=c?c.centerX:a?this._modeTransitionTargetCenterX:r?.viewport.centerX,p=c?c.centerY:a?this._modeTransitionTargetCenterY:r?.viewport.centerY;if("view"===s&&"view"!==e&&!a){const t=this._view;if(this._viewModeSnap={space:this._space,zoom:this._zoom,cx:t?this._logicalViewCenter(n)?.x:void 0,cy:t?this._logicalViewCenter(n)?.y:void 0},"iso"===n){const t=this._logicalViewCenter("iso");d=t?.x,p=t?.y,this._view=null,this._mode=e,this._applyView(this._zoom,t?.x,t?.y)}l&&(h=1,d=void 0,p=void 0)}if("plan"===s&&this._activeDraftId&&(this._resumeDraftBySpace[this._space]=this._activeDraftId),this._mode=e,"devices"===s&&"devices"!==e&&(this._showHidden=!1,this._deviceInbox=null,this._deviceInboxReturn=null,this._deviceInboxMemo=null),this._editorChromeMode="view"===e?s:e,"view"===e){"view"===s||a||(this._modeTransitionEditorCamera={zoom:this._zoom,centerX:r?.viewport.centerX,centerY:r?.viewport.centerY});const e=this._viewModeSnap;if(e&&e.space===this._space){h=e.zoom;const t=this._labsIso&&"iso"===this._viewPreference[this._space]?"iso":"flat",i=null!=e.cx&&null!=e.cy&&"iso"===t?Af([e.cx,e.cy],0):null;d=i?.[0]??e.cx,p=i?.[1]??e.cy}else e&&(h=this._zoomBySpace[this._space]||1,d=void 0,p=void 0)}else this._modeTransitionEditorCamera={zoom:h,centerX:d,centerY:p};this._modeTransitionTargetZoom=h,this._modeTransitionTargetCenterX=d,this._modeTransitionTargetCenterY=p,("iso"===n||"view"===e&&this._labsIso&&"iso"===this._viewPreference[this._space])&&(this._modeTransitionForceAtomic=!0);const u=++this._modeTransitionRequest;t?r?(this._modeTransitionPreparing=!0,this._modeTransitionVisual=r,this._prepareModeTransition(u,r,e,h,d,p)):(this._modeTransitionPreparing=!1,this._modeTransitionVisual=null,this._zoom=h,this._view=null,requestAnimationFrame(()=>{this.isConnected&&u===this._modeTransitionRequest&&this._mode===e&&(this._applyView(h,d,p),"view"===e&&(this._viewModeSnap=null,this._modeTransitionEditorCamera=null),this.requestUpdate())})):"view"===e&&this._commitViewModeAtomic(r,h,d,p),this._path=[],this._clearPlanSnapHover(),this._clearOpeningPlacement(!0),this._tool="draw",this._mergeSel=null,this._mergeDialog=null,this._splitSel=null,this._pendingSplit=null,this._selId=null,this._physicalSel=null,this._physicalDialog=null,this._physicalDrag=null,this._rszResetController(),this._tip=null,this._hoverRoom=null,this._decorDraft=null,this._decorSel=null,this._decorMove=null,this._backdropDialog=null,this._decorTool="select",this._bdDrag=null,this._dtDrag=null,this._dtBox=null,"plan"===e&&(this._primeDrawWallField(),this._resumeLastDraft()),this._saveNav()}_primeDrawWallField(){null===this._drawWallField&&(this._drawWallField=Sl(vl,this._imperial))}get _drawWallFieldValue(){return null===this._drawWallField?Sl(vl,this._imperial):this._drawWallField}get _drawWallCm(){const e=$v(this._drawWallFieldValue);if(null==e||e<0)return null;const t=this._imperial?2.54*e:e,i="column"===this._tool?ih:100;return t>=("column"===this._tool?1:0)&&t<=i?t:null}get _drawWallMaxCm(){return"column"===this._tool?ih:100}_showPhysicalRange(e=this._drawWallMaxCm,t=0){this._showToast(this._t("toast.physical_range",{min:Sl(t,this._imperial),max:Sl(e,this._imperial),unit:this._t(this._imperial?"wallthick.unit_in":"wallthick.unit_cm")}))}_draftSegmentCount(e=this._curSpaceCfg){return(e?.room_drafts||[]).reduce((e,t)=>e+(Array.isArray(t.segments)?t.segments.length:0),0)}_mergeSpacePartitions(e,t){const i=e?.partitions||[];if(i.length<2)return 0;const s=Hm(i,{pitch:jh,seedIds:t,geometry:Bm(e,{excludeDraftId:this._activeDraftId})});return s.merged?(e.partitions=s.partitions,Lm(e.openings,e.partitions,s.openingMoves,{coordScale:Ov,cellCm:this._cellCm,gridPitch:this._gridPitch}),s.merged):0}_finishWallChain(){if("draw"!==this._tool||this._wallFaceBatch||this._roomDialog)return!0;const e=this._curSpaceCfg;if(!e||this._path.length<2)return this._path=[],this._activeDraftId=null,this._draftSegmentCms=[],this._closingWallCm=null,this._clearPlanSnapHover(),!0;const t=Ru(this._path,Iu(this._path.length-1,this._draftSegmentCms,this._drawWallCm,vl)),i=t.length;if((e.partitions||[]).length+i>jv)return this._showToast(this._t("toast.physical_limit")),!1;const s=this._geometrySnapshot();e.partitions||=[];const n=Date.now().toString(36),o=[],a=this._activeDraftId?(e.room_drafts||[]).find(e=>e.id===this._activeDraftId):null,r=this._draftSegmentsForPath(this._path,a,t.map(e=>e.cm));for(let s=0;se.id!==this._activeDraftId),e.room_drafts.length||delete e.room_drafts),this._commitPhysicalGeometry(this._t("history.wall_chain_finish"),s),delete this._resumeDraftBySpace[this._space],this._path=[],this._activeDraftId=null,this._draftSegmentCms=[],this._closingWallCm=null,this._clearPlanSnapHover(),!0}_activateMarkupTool(e){e!==this._tool&&(this._wallFaceBatch&&this._roomDialogCancel(),("draw"!==this._tool||this._finishWallChain())&&(this._cancelPath(),"resize"===this._tool&&(this._resize.dragging?this._rszCancelDrag():this._resize.reset()),this._tool=e,"draw"===e&&this._resumeLastDraft(),"resize"===e&&this._resize.selectRoom(null),"wallthick"===e&&(this._wallDialog=null)))}_limitReached(e){const t=this._curSpaceCfg;if(!t)return!0;const i="draft"===e?(t.room_drafts||[]).length>=200:"partition"===e?(t.partitions||[]).length>=jv:(t.wall_columns||[]).length>=500;return i&&this._showToast(this._t("toast.physical_limit")),i}_svgPoint(e){const t=this.renderRoot.querySelector(".stage").getBoundingClientRect();return this._screenToVb(e.clientX-t.left,e.clientY-t.top)}_snap(e){const t=this._gridPitch;return[Uh(ri(e[0],t)),Uh(ri(e[1],t))]}_snapDrawPoint(e,t=!1){const i=this._path[this._path.length-1],s=t&&i?function(e,t,i,s=1/0){if(!(i>0&&e?.every(Number.isFinite)&&t?.every(Number.isFinite)))return[t[0],t[1]];const n=t[0]-e[0],o=t[1]-e[1];if(Math.abs(n)+Math.abs(o)<=1e-12)return[e[0],e[1]];const a=Math.PI/4;let r=Math.atan2(o,n);r<0&&(r+=2*Math.PI);const l=Math.floor(r/a+.5)%8,[c,h]=[[1,0],[1,1],[0,1],[-1,1],[-1,0],[-1,-1],[0,-1],[1,-1]][l],d=c*c+h*h;let p=Math.max(0,Math.round((n*c+o*h)/(i*d)));if(Number.isFinite(s)&&s>=0){const t=(e,t)=>{if(!t)return 1/0;const n=t>0?s-e:e+s;return Math.max(0,Math.floor((n+1e-9*i)/i))};p=Math.min(p,t(e[0],c),t(e[1],h))}return[e[0]+c*p*i,e[1]+h*p*i]}(i,e,this._gridPitch,Hh):e;return this._snap(s)}_planSnapOpeningCuts(e,t){if(!this._openingsR.length)return[];const i=this._openingWallIndexFor(e,t).value,s=[];for(const t of this._roomWallOpeningInputs(this._openingsR,e)){const e=Xc(i,t);if(!e.negative&&!e.positive)continue;const n=t.angle*Math.PI/180,o=Math.cos(n)*t.length/2,a=Math.sin(n)*t.length/2;s.push([t.x-o,t.y-a,t.x+o,t.y+a])}return s}_planSnapGeometrySnapshot(){const e=this._spaceModel();if(!e)return{key:`${this._space}|empty`,value:{segments:[],endpoints:[]}};const t=[this._space,this._cfgEpoch,this._activeDraftId||"",e.rooms.length,e.room_drafts.length,e.partitions.length].join("|");if(this._planSnapGeometryCache?.key===t)return this._planSnapGeometryCache;const i=this._openCuts(),s=xu({space:e,activeDraftId:this._activeDraftId,roomCuts:this._planSnapOpeningCuts(e,i),partitionCuts:this._partitionOpeningCuts(e),epsilon:2e-4*this._gridPitch});return this._planSnapGeometryCache={key:t,value:s},this._planSnapGeometryCache}_hiddenWallDiagnosticSnapshot(){const e=this._spaceModel();if(!e)return{key:`${this._space}|hidden-empty`,value:{segments:[],endpoints:[]}};const t=["hidden",this._space,this._cfgEpoch,this._activeDraftId||"",e.rooms.length,e.room_drafts.length,e.partitions.length].join("|");if(this._hiddenWallDiagnosticCache?.key===t)return this._hiddenWallDiagnosticCache;const i=ku({space:e,activeDraftId:this._activeDraftId,epsilon:2e-4*this._gridPitch});return this._hiddenWallDiagnosticCache={key:t,value:i},this._hiddenWallDiagnosticCache}_planStructuralGeometrySnapshot(){const e=this._spaceModel();if(!e)return{key:`${this._space}|structural-empty`,value:{segments:[],endpoints:[]}};const t=["structural",this._space,this._cfgEpoch,this._activeDraftId||"",e.rooms.length,e.room_drafts.length,e.partitions.length].join("|");if(this._planStructuralGeometryCache?.key===t)return this._planStructuralGeometryCache;const i=xu({space:e,activeDraftId:this._activeDraftId,epsilon:2e-4*this._gridPitch});return this._planStructuralGeometryCache={key:t,value:i},this._planStructuralGeometryCache}_planSnapContextKey(e){const t=this._path[0],i=this._path[this._path.length-1];return[e,this._tool,this._path.length,t?`${t[0]},${t[1]}`:"",i?`${i[0]},${i[1]}`:""].join("|")}_resolvePlanDrawPoint(e,t){const i=this._planSnapGeometrySnapshot(),s=this._path[this._path.length-1],n="draw"===this._tool&&this._path.length>=3?[{point:this._path[0],key:"closure:first-point"}]:[],o={tolerance:this._cssPxToRender(12),distinguishTolerance:this._cssPxToRender(8),gridStep:this._gridPitch,excludePoints:s?[s]:[],extraEndpoints:n,epsilon:2e-4*this._gridPitch},a=t&&s?Pu(i.value,e,{...o,anchor:s}):function(e,t,i){if(!(_u(t)&&i.tolerance>=0))return{kind:"none",candidate:null,conflicts:[]};const s=i.epsilon??uu,n=i.excludePoints||[],o=Cu(Mu(e,t,i),i.distinguishTolerance||0);if(o)return o;let a=null;for(const o of e.segments){const e=[o.a[0],o.a[1],o.b[0],o.b[1]],r=Hs([t[0],t[1]],e);if(r>i.tolerance)continue;const l=Du(o,t,i.gridStep);!$u(l,n,s)&&Su(r,o.key,a)&&(a={kind:"line",point:l,key:o.key,distance:r,segment:o})}return a?{kind:"resolved",candidate:a,conflicts:[]}:{kind:"none",candidate:null,conflicts:[]}}(i.value,e,o),r="resolved"===a.kind?a.candidate:null,l=r?[...r.point]:this._snapDrawPoint(e,t),c=s?va(s,l):l;return{point:c,candidate:r&&fi(c,r.point)?r:null,conflicts:"ambiguous"===a.kind?a.conflicts:[],ambiguous:"ambiguous"===a.kind,contextKey:this._planSnapContextKey(i.key)}}get _activePlanSnapCandidate(){if(!this._markup||"draw"!==this._tool)return null;const e=this._planSnapHover;if(!e)return null;const t=this._planSnapGeometrySnapshot();return e.contextKey===this._planSnapContextKey(t.key)?e.candidate:null}get _activePlanSnapConflicts(){if(!this._markup||"draw"!==this._tool)return[];const e=this._planSnapHover;if(!e)return[];const t=this._planSnapGeometrySnapshot();return e.contextKey===this._planSnapContextKey(t.key)?e.conflicts:[]}_clearPlanSnapHover(e=!0){this._planSnapHover=null,this._syncPlanSnapActiveMarker(null),this._syncPlanSnapConflictMarkers([]),e&&(this._cursorPt=null)}_samePt(e,t){return fi(e,t)}_dropLegacySegments(){for(const e of this._serverCfg?.markers||[])"ripple"===e.display&&(e.display="icon_ripple");for(const e of this._serverCfg?.spaces||[]){delete e.segments;const t=new Set,i=e=>"string"==typeof e&&e.length>=1&&e.length<=64&&!t.has(e),s=e=>(t.add(e),!0),n=e=>Array.isArray(e)&&2===e.length&&e.every(e=>Number.isFinite(Number(e))&&Math.abs(Number(e))<=Oh);if(Array.isArray(e.partitions)&&(e.partitions=e.partitions.filter(e=>e&&i(e.id)&&n(e.a)&&n(e.b)&&Math.hypot(e.a[0]-e.b[0],e.a[1]-e.b[1])>1e-9&&s(e.id)).map(e=>({...e,cm:Number.isFinite(Number(e.cm))?Math.max(0,Math.min(100,Number(e.cm))):15})),e.partitions.length||delete e.partitions),Array.isArray(e.wall_columns)&&(e.wall_columns=e.wall_columns.filter(e=>e&&i(e.id)&&n(e.center)&&s(e.id)).map(e=>({id:e.id,shape:"circle"===e.shape?"circle":"square",center:[Number(e.center[0]),Number(e.center[1])],cm:nh(Number(e.cm)||15),..."circle"===e.shape?{}:{angle:oh(e.angle)}})),e.wall_columns.length||delete e.wall_columns),Array.isArray(e.room_drafts)&&(e.room_drafts=e.room_drafts.filter(e=>e&&i(e.id)&&Array.isArray(e.points)&&e.points.length>=2&&e.points.every(n)&&s(e.id)).map(e=>{const t=gg(e);return{id:e.id,...t}}).filter(e=>e.points.length>=2),e.room_drafts.length||delete e.room_drafts),Array.isArray(e.walls)){const t=ep(Eh({spaces:[e]}),e.id),i=t?X_(e,t,Ov,.02*Bh).contour.map(e=>e.map(e=>e/Ov)):jd(e.open_spans).map(e=>[e.a[0],e.a[1],e.b[0],e.b[1]]);e.walls=Ll(e.walls,e.rooms||[],jh,1,i),e.walls.length||delete e.walls}}}_rollbackRejectedPhysicalWrites(e){if(!this._serverCfg||!e.length)return!1;let t=!1;for(const[i]of e){const e=this._pendingPhysicalWrites.get(i);e&&(t=this._restoreGeometryStateInConfig(this._serverCfg,e.before)||t,this._pendingPhysicalWrites.delete(i))}return!!t&&(this._clearGeometryGesture(),this._geometryHistory.clear(),this._cfgEpoch++,this._modelCache=null,this._wallUnionCache=null,this._physicalBodiesCache=null,this._frame=null,this._regSignature="",this._cfgContentFingerprint=Jd(this._serverCfg),this._maybeRebuildDevices(),this.requestUpdate(),!0)}async _reloadRejectedPhysicalWrite(){const e=this._writeChain;await e.catch(()=>{}),await this._reloadConfigOnly(!0)}get _cfgWriting(){return this._writesPending>0}_writeConfig(){var e,t;this._writesPending++,this._writeChain=(e=this._writeChain,t=async()=>{if(!this._serverCfg)return;this._dropLegacySegments();const e=fl(il(this._serverCfg)),t=Jd(e),i=[...this._pendingPhysicalWrites.entries()];for(const[t,s]of i){const i=Ug(e.spaces.find(e=>e.id===t));if(i===s.fingerprint)continue;let n=!1;try{n=this._checkSpacePhysicalGeometry(e,t).ok}catch{n=!1}if(!n)throw this._restoreGeometryStateLocal(s.before),this._pendingPhysicalWrites.delete(t),this._geometryHistory.clear(),this._showToast(this._t("toast.geometry_unsafe")),Object.assign(new Error("unsafe wall geometry"),{code:"geometry-unsafe"});s.fingerprint=i}let s;t!==Jd(this._serverCfg)&&(this._serverCfg=e),this._cfgContentFingerprint=t;try{s=await this.hass.callWS({type:"houseplan/config/set",config:e,expected_rev:this._cfgRev})}catch(e){if(!this._rollbackRejectedPhysicalWrites(i))throw e;const t=e&&"object"==typeof e?e:Object.assign(new Error(String(e)),{cause:e});throw t.physicalGeometryRolledBack=!0,t}this._cfgRev=s?.rev??this._cfgRev+1;for(const[e,t]of i)this._pendingPhysicalWrites.get(e)?.fingerprint===t.fingerprint&&this._pendingPhysicalWrites.delete(e)},e.catch(()=>{}).then(t));return this._writeChain.finally(()=>{this._writesPending--})}_saveConfig(){this._cfgEpoch++,this._saveConfigDebounced()}_geometrySnapshotFromConfig(e,t){const i=e?.spaces?.find(e=>e.id===t);if(!i)return null;const s=e=>JSON.parse(JSON.stringify(e)),n={};for(const e of["plan_x","plan_y","plan_scale","plan_scale_x","plan_scale_y","plan_angle"])void 0!==i[e]&&(n[e]=i[e]);return{spaceId:t,rooms:s(i.rooms||[]),...Array.isArray(i.openings)?{openings:s(i.openings)}:{},...Array.isArray(i.walls)?{walls:s(i.walls)}:{},...Array.isArray(i.wall_segments)?{wall_segments:s(i.wall_segments)}:{},...Array.isArray(i.open_spans)?{open_spans:s(i.open_spans)}:{},...Array.isArray(i.room_drafts)?{room_drafts:s(i.room_drafts)}:{},...Array.isArray(i.partitions)?{partitions:s(i.partitions)}:{},...Array.isArray(i.wall_columns)?{wall_columns:s(i.wall_columns)}:{},...Array.isArray(i.decor)?{decor:s(i.decor)}:{},plan_transform:n}}_geometrySnapshot(e=this._space){return this._geometrySnapshotFromConfig(this._serverCfg,e)}_recordGeometry(e,t){if(!t)return;const i=this._geometrySnapshot(t.spaceId);i&&JSON.stringify(t)!==JSON.stringify(i)&&(this._geometryHistory.push({name:e,before:t,after:i}),this.requestUpdate())}_restoreGeometryStateInConfig(e,t,i=!1){const s=e?.spaces?.find(e=>e.id===t.spaceId);if(!s)return!1;const n=e=>JSON.parse(JSON.stringify(e)),o=new Map((s.rooms||[]).map(e=>[e.id,e]));if(s.rooms=n(t.rooms),i)for(const e of s.rooms){const t=o.get(e.id);!Array.isArray(e.wall_ids)&&Array.isArray(t?.wall_ids)&&t.wall_ids.length===e.poly?.length&&(e.wall_ids=n(t.wall_ids))}const a=(e,t)=>{void 0!==t?s[e]=n(t):i&&"wall_segments"===e||delete s[e]},r=new Map((s.openings||[]).map(e=>[e.id,e])),l=new Map((s.room_drafts||[]).map(e=>[e.id,e]));if(a("openings",t.openings),a("walls",t.walls),a("wall_segments",t.wall_segments),a("open_spans",t.open_spans),a("room_drafts",t.room_drafts),a("partitions",t.partitions),a("wall_columns",t.wall_columns),a("decor",t.decor),i){for(const e of s.openings||[]){const t=r.get(e.id);e.host||"wall"!==t?.host?.kind||(e.host=n(t.host))}for(const e of s.room_drafts||[]){const t=l.get(e.id);for(let i=0;i<(e.segments||[]).length;i++)!e.segments[i]?.id&&t?.segments?.[i]?.id&&(e.segments[i].id=t.segments[i].id)}}for(const e of["plan_x","plan_y","plan_scale","plan_scale_x","plan_scale_y","plan_angle"])delete s[e];return Object.assign(s,n(t.plan_transform||{})),!0}_restoreGeometryStateLocal(e){return!!this._restoreGeometryStateInConfig(this._serverCfg,e)&&(this._cfgEpoch++,this._modelCache=null,this._wallUnionCache=null,this._physicalBodiesCache=null,this._frame=null,this.requestUpdate(),!0)}_wallModelBlockerLabel(e){const t=e instanceof lg?e.reason:"invalid-room";return this._t(`wall_model.reason.${t}`)}_hasLegacyZeroWallFields(e=this._serverCfg){return(e?.spaces||[]).some(e=>Array.isArray(e?.open_spans)&&e.open_spans.length>0||(e?.rooms||[]).some(e=>Array.isArray(e?.open_to)&&e.open_to.length>0))}_showWallModelMigrationBlocked(e){const t=this._hasLegacyZeroWallFields()?"toast.zero_wall_migration_blocked":"toast.wall_model_migration_blocked";this._showToast(this._t(t,{reason:this._wallModelBlockerLabel(e)}))}_limitSegmentsOf(e){const t=[];for(const i of e?.wall_segments||[])i?.a&&i?.b&&t.push({id:String(i.id||""),a:i.a,b:i.b,cm:Number(i.cm)});for(const i of e?.partitions||[])i?.a&&i?.b&&t.push({id:String(i.id||""),a:i.a,b:i.b,cm:Number(i.cm)});for(const i of e?.room_drafts||[]){const e=Array.isArray(i?.points)?i.points:[],s=Array.isArray(i?.segments)?i.segments:[];for(let n=0;n+1e?.id===t);if(!s)return[];const n=Number(s.cell_cm)>0?Number(s.cell_cm):5,o=this._limitSegmentsOf(s),a=[...Td(o),...Fd(o,n,jh),...Ed(o,n,jh)];let r=null;try{r=bc(s.rooms||[],s.walls||[],[],jh,n,jh,1)}catch{r=null}let l="ok"===i?.status||"degraded-extra"===i?.status?i.roomGeom:null;if(!l&&r&&r.nodes.length)try{const e=qc(s.rooms||[],s.walls||[],[],[],jh,n,jh,1);l="ok"===e?.status||"degraded-extra"===e?.status?e.roomGeom:null}catch{l=null}for(const e of s.rooms||[]){const t=String(e?.id||"");if(!t)continue;let i=null;try{i=Cc(s.rooms||[],t,s.walls||[],[],jh,n,jh,1,l??void 0,r)}catch{i=null}a.push(...Od(t,i,n,jh))}return a}_junctionLimitLabel(e){const t=e=>String(Math.round(10*e)/10);return this._t(`junction.limit_${e.rule}`,{actual:t(e.actual),limit:t(e.limit)})}_junctionLimitsIntroduced(e,t,i,s){let n;const o=(t?.spaces||[]).find(e=>e?.id===i);let a="";try{a=Ug(o)}catch{a=""}const r=t&&"object"==typeof t?this._junctionBaselineCache.get(t):void 0;if(r&&a&&r.fingerprint===a&&r.spaceId===i&&(n=r.violations),!n){try{const e=Number(t?.model_version||0)>=9?t:Ag(t).config;n=this._junctionLimitViolations(e,i)}catch{return[]}t&&"object"==typeof t&&a&&this._junctionBaselineCache.set(t,{spaceId:i,fingerprint:a,violations:n})}let l=[];try{l=this._junctionLimitViolations(e,i,s)}catch{return[{rule:"check_failed",subject:i,actual:0,limit:0}]}return function(e,t){const i=new Map;for(const e of t||[])i.set(e.rule,(i.get(e.rule)||0)+1);const s=new Map;for(const t of e||[])s.set(t.rule,[...s.get(t.rule)||[],t]);const n=[];for(const[e,t]of s){const s=t.length-(i.get(e)||0);s>0&&n.push(...t.slice(0,s))}return n}(l,n)}_commitPhysicalGeometry(e,t,i=[]){if(!t||!this._serverCfg)return!1;const s=this._serverCfg,n=this._geometrySnapshotFromConfig(s,t.spaceId),o=s.spaces.find(e=>e.id===t.spaceId);if(!o||Ug(t)===Ug(o))return!1;let a,r=!1;try{r=this._checkSpacePhysicalGeometry(s,t.spaceId).ok}catch{r=!1}if(!r)return this._clearGeometryGesture(),this._restoreGeometryStateLocal(t),this._showToast(this._t("toast.geometry_unsafe")),!1;let l=t;try{if(Number(s.model_version||0)<9){const e=JSON.parse(JSON.stringify(s));if(!this._restoreGeometryStateInConfig(e,t))throw new lg("invalid-room",t.spaceId);const i=Ag(e).config;l=this._geometrySnapshotFromConfig(i,t.spaceId)||t;const o=JSON.parse(JSON.stringify(i));if(!n||!this._restoreGeometryStateInConfig(o,n,!0))throw new lg("invalid-room",t.spaceId);const r=i.spaces.find(e=>e.id===t.spaceId),c=o.spaces.find(e=>e.id===t.spaceId);if(!Array.isArray(n.wall_segments)&&JSON.stringify(t.rooms)===JSON.stringify(n.rooms))for(const e of c?.wall_segments||[])e.cm=Hl(n.walls,e.a,e.b,jh,1);const h=((e,t,i)=>{const s=new Map,n=new Map((e?.rooms||[]).map(e=>[e.id,e])),o=new Map((t||[]).map(e=>[e.id,e])),{rooms:a}=Dg(i,Mg(e));for(const e of a){const t=o.get(e.id),a=n.get(e.id),r=hi(t),l=hi((i.rooms||[]).find(t=>t.id===e.id)),c=hi(a);if(r&&l&&c&&r.length===l.length&&c.length===a?.wall_ids?.length&&e.poly.length===e.wall_ids.length)for(let t=0;t{const n=[];for(let o=0;org||bg(r,i,s)>rg)continue;const l=vg(a,i,s),c=vg(r,i,s);l<-rg||c>1+rg||c<=l+rg||n.push({start:l,id:t[o]})}return n.sort((e,t)=>e.start-t.start)},n=i(c,a.wall_ids,r[t],r[(t+1)%r.length]),o=i(e.poly,e.wall_ids,l[t],l[(t+1)%l.length]);if(n.length&&n.length===o.length)for(let e=0;ee.id===t.spaceId),s=this._path.length>=2?this._path.map(e=>[e[0]/Ov,e[1]/Ov]):[];s.push(...i.map(e=>[e[0],e[1]])),c=fg(e)<=fg(l,s)&&this._checkSpacePhysicalGeometry(a,t.spaceId).ok}catch{c=!1}if(!c)return this._clearGeometryGesture(),this._restoreGeometryStateLocal(t),this._showToast(this._t("toast.geometry_unsafe")),!1;const h=JSON.parse(JSON.stringify(s));this._restoreGeometryStateInConfig(h,t);const d=this._junctionLimitsIntroduced(a,h,t.spaceId);if(d.length)return this._clearGeometryGesture(),this._restoreGeometryStateLocal(t),this._showToast(this._junctionLimitLabel(d[0])),!1;Fg(s,a),this._recordGeometry(e,l);const p=s.spaces.find(e=>e.id===t.spaceId),u=this._pendingPhysicalWrites.get(t.spaceId);return this._pendingPhysicalWrites.set(t.spaceId,{before:u?.before||l,fingerprint:Ug(p)}),this._saveConfig(),!0}_clearGeometryGesture(){this._path=[],this._clearPlanSnapHover(),this._clearOpeningPlacement(!1),this._mergeSel=null,this._mergeDialog=null,this._splitSel=null,this._pendingSplit=null,this._wallFaceBatch=null,this._wallRepairDiagnostic=null,this._roomDeleteDialog=null,this._wallDialog=null,this._physicalDialog=null,this._physicalSel=null,this._physicalDrag=null,this._physicalRotate=null,this._activeDraftId=null,this._draftSegmentCms=[],this._closingWallCm=null,this._openingDialog=null,this._rszResetController(),this._decorDraft=null,this._decorMove=null,this._dtDrag=null,this._bdDrag=null}_stagePointerCancel(e){if(clearTimeout(this._kioskHoldTimer),this._swipeStart?.id===e.pointerId&&(this._swipeStart=null),this._physicalDrag?.pid===e.pointerId||this._physicalRotate?.pid===e.pointerId)return void this._cancelPhysicalGesture();if(this._decorDraft?.pid===e.pointerId)return this._decorDraft=null,void this.requestUpdate();if(this._decorMove?.pid===e.pointerId||this._dtDrag?.pid===e.pointerId||this._bdDrag?.pid===e.pointerId)return void this._cancelDecorGesture();"opening"===this._tool?(this._cursorPt=null,this._clearOpeningPlacement(!1)):"draw"===this._tool&&this._clearPlanSnapHover();const t=!!this._pinchStart||!!this._panStart;this._pointers.delete(e.pointerId),this._pointers.size<2&&(this._pinchStart=null),0===this._pointers.size&&(this._panStart=null,this._panLock=null),t&&0===this._pointers.size&&this.requestUpdate()}_applyGeometryState(e,t=!1){if(!this._canCommitSpace(e.spaceId))return!1;const i=this._geometrySnapshot(e.spaceId);if(!i||!this._restoreGeometryStateLocal(e))return!1;const s=this._serverCfg,n=Ug(i)!==Ug(e);if(n){let n=!1;try{const i=s?this._checkSpacePhysicalGeometry(s,e.spaceId):null;n=!!i?.ok||!(!t||"wall-degraded-extra"!==i?.reason)}catch{n=!1}if(!n)return this._restoreGeometryStateLocal(i),this._showToast(this._t("toast.geometry_unsafe")),!1}let o;try{o=Ag(s).config}catch(e){return this._restoreGeometryStateLocal(i),this._showWallModelMigrationBlocked(e),!1}if(n){let n=!1;try{const i=o?this._checkSpacePhysicalGeometry(o,e.spaceId):null;n=!!i?.ok||!(!t||"wall-degraded-extra"!==i?.reason)}catch{n=!1}if(!n)return this._restoreGeometryStateLocal(i),this._showToast(this._t("toast.geometry_unsafe")),!1;Fg(s,o);const a=s?.spaces.find(t=>t.id===e.spaceId),r=this._pendingPhysicalWrites.get(e.spaceId);this._pendingPhysicalWrites.set(e.spaceId,{before:r?.before||i,fingerprint:Ug(a)})}else Fg(s,o);return this._clearGeometryGesture(),this._space!==e.spaceId&&(this._commitSpace(e.spaceId),this._saveNav(),this._restoreZoom()),this._modelCache=null,this._frame=null,this._regSignature="",this._maybeRebuildDevices(),this._saveConfig(),this.requestUpdate(),!0}_roomAt(e){return this._spaceModel()?.rooms.find(t=>{const i=hi(t);return!!i&&wi(e,i)})}_overlapRoom(e){return this._spaceModel()?.rooms.find(t=>{const i=hi(t);return!!i&&Di(e,i)})}_pointInRoom(e,t){return t.poly?vi(e,t.poly):null!=t.x&&e[0]>=t.x&&e[0]<=t.x+t.w&&e[1]>=t.y&&e[1]<=t.y+t.h}_contourSelfIntersects(e){if(!Ea(e))return!0;const t=e.length,i=.001;for(let s=0;s=500)return this._showToast(this._t("toast.physical_limit")),!1;const e=this._curSpaceCfg;return!(!this._activeDraftId&&(e?.room_drafts||[]).length>=200||this._draftSegmentCount(e)>=2e3||(e?.partitions||[]).length+this._path.length>jv)||(this._showToast(this._t("toast.physical_limit")),!1)}_markupClick(e){if(this._vacFit)return;if(!this._markup)return;const t=this._spaceModel();if(!t)return void this._clearGeometryGesture();if(this._suppressClick)return;if(this._drag||this._rlResize)return;const i=e.composedPath?.()||[];if(i.some(e=>e?.classList?.contains?.("roomlabel")||e?.classList?.contains?.("rlhandle")))return;if(i.some(e=>e?.classList?.contains?.("physical-hit")))return;const s=this._svgPoint(e);if("select"===this._tool)return void(this._physicalSel=null);if("resize"===this._tool){if(this._resize.dragging||i.some(e=>e?.classList?.contains?.("rszhandle")))return;const e=[...t.rooms].reverse().find(e=>this._pointInRoom(s,e));return this._resize.selectRoom(e?.id||null),void this.requestUpdate()}if("delroom"===this._tool)return void this._deleteRoomClick(s);if("opening"===this._tool)return void this._openingClick(s);if("merge"===this._tool)return void this._mergeClick(s);if("wallthick"===this._tool)return void this._wallThickClick(s);if("split"===this._tool)return void this._splitClick(s);if("column"===this._tool)return void this._columnClick(s);this._wallRepairDiagnostic=null;const n=this._resolvePlanDrawPoint(s,e.shiftKey);if(n.ambiguous)return this._planSnapHover={contextKey:n.contextKey,candidate:null,conflicts:n.conflicts},this._syncPlanSnapActiveMarker(null),this._syncPlanSnapConflictMarkers(n.conflicts),void this._showToast(this._t("toast.plan_snap_ambiguous"));let o=n.point;if(e.ctrlKey||e.metaKey){if(e.preventDefault(),this._path.length<3)return;o=va(this._path[this._path.length-1],this._path[0])}if(!this._path.length){if(!e.shiftKey&&!n.candidate&&this._offerExistingWallFace(s))return;const t=this._draftEndAt(o);return t?(this._activeDraftId=t.draft.id,this._resumeDraftBySpace[this._space]=t.draft.id,this._path=t.reverse?[...t.draft.points].reverse().map(e=>[...e]):t.draft.points.map(e=>[...e]),void(this._draftSegmentCms=this._adoptDraftCms(this._path,t.reverse?[...t.draft.segments].reverse().map(e=>e.cm):t.draft.segments.map(e=>e.cm),t.draft.id))):(this._activeDraftId=null,this._draftSegmentCms=[],void(this._path=[o]))}const a=this._path[this._path.length-1];if(this._samePt(o,a))return;const r=this._draftEndAt(o,this._activeDraftId||void 0);if(r)return void this._mergeDraftEndpoint(r);if(!this._canAppendRoomDraftPoint())return;const l=this._drawWallCm;if(null==l)return void this._showPhysicalRange(100);const c=this._path.map(e=>[...e]);this._path=[...this._path,o],this._draftSegmentCms=[...this._draftSegmentCms,l],this._persistActiveDraftSegment(),this._offerWallFaces(c)}_draftEndAt(e,t){const i=this._spaceModel();if(!i)return null;const s=this._viewOr(this._baseVb()),n=Math.max(.15*this._gridPitch,this._stageEl?.clientWidth?s.w/this._stageEl.clientWidth*12:0);for(const s of i.room_drafts||[]){if(s.id===t)continue;if(s.points.length<2)continue;const i=s.points[0],o=s.points[s.points.length-1];if(Math.hypot(e[0]-o[0],e[1]-o[1])<=n)return{draft:s,reverse:!1};if(Math.hypot(e[0]-i[0],e[1]-i[1])<=n)return{draft:s,reverse:!0}}return null}_mergeDraftEndpoint(e){const t=this._curSpaceCfg;if(!t||!this._path.length)return;const i=this._path.map(e=>[...e]),s=this._wallGraphSources(i),n=Array.isArray(t.room_drafts)?t.room_drafts:[],o=this._activeDraftId?n.find(e=>e.id===this._activeDraftId):null,a=n.find(t=>t.id===e.draft.id);if(!a)return;const r=e.reverse?e.draft.points.map(e=>[...e]):[...e.draft.points].reverse().map(e=>[...e]),l=e.reverse?(a.segments||[]).map(e=>({...e})):[...a.segments||[]].reverse().map(e=>({...e})),c=this._path[this._path.length-1],h=this._samePt(c,r[0]),d=h?null:this._drawWallCm;if(!h&&null==d)return void this._showPhysicalRange(100);const p=this._draftSegmentsForPath(this._path,o,this._draftSegmentCms),u=[...this._path.map(e=>[...e]),...h?r.slice(1):r],_=[...p,...null==d?[]:[{cm:d}],...l],m=u.length>=4&&this._samePt(u[0],u[u.length-1]),g=u,f=_;if(g.length>500)return void this._showToast(this._t("toast.physical_limit"));const v=(o?.segments?.length||0)+(a.segments?.length||0);if(this._draftSegmentCount(t)-v+f.length>2e3)return void this._showToast(this._t("toast.physical_limit"));if(m){const e=g.slice(0,-1);if(this._contourSelfIntersects(e)||Ti(e)<=1e-6)return void this._showToast(this._t("toast.contour_cannot_close"));const t=this._overlapRoom(e);if(t)return void this._showToast(this._t("toast.room_overlap",{name:t.name||""}))}const b=this._geometrySnapshot(),y=this._activeDraftId||e.draft.id,w={...o||a,id:y,points:g.map(e=>[e[0]/Ov,e[1]/Ov]),segments:f};t.room_drafts=n.filter(t=>t.id!==e.draft.id&&(!this._activeDraftId||t.id!==this._activeDraftId)),t.room_drafts.push(w),this._activeDraftId=y,this._resumeDraftBySpace[this._space]=y,this._path=g,this._draftSegmentCms=this._adoptDraftCms(g,f.map(e=>Number(e.cm)),y),this._physicalSel=null,this._commitPhysicalGeometry(this._t("history.draft_merge"),b),null!=d&&this._offerWallFaces(i,i.length-1,s)}_adoptDraftCms(e,t,i){const s=Math.max(0,e.length-1),n=Iu(s,t,this._drawWallCm,vl);return t.length!==s&&console.debug(`[houseplan] draft ${i??"?"}: восстановлено толщин ${s-t.length} (#234)`),n}_draftSegmentsForPath(e,t,i){const s=Array.isArray(t?.points)?t.points.map(e=>[Number(e?.[0])*Ov,Number(e?.[1])*Ov]):[],n=Array.isArray(t?.segments)?t.segments:[];return Iu(Math.max(0,e.length-1),i,this._drawWallCm,vl).map((t,i)=>{const o=e[i],a=e[i+1],r=s.findIndex((e,t)=>{if(t+1>=s.length)return!1;const i=s[t+1];return this._samePt(o,e)&&this._samePt(a,i)||this._samePt(o,i)&&this._samePt(a,e)});return{...r>=0?n[r]:{},cm:t}})}_persistActiveDraftSegment(){if(this._path.length<2||!this._curSpaceCfg)return;const e=this._geometrySnapshot(),t=this._curSpaceCfg;t.room_drafts||=[],this._activeDraftId||(this._activeDraftId="draft-"+Date.now().toString(36)),this._resumeDraftBySpace[this._space]=this._activeDraftId;const i=t.room_drafts.findIndex(e=>e.id===this._activeDraftId),s={...i>=0?t.room_drafts[i]:{},id:this._activeDraftId,points:this._path.map(e=>[e[0]/Ov,e[1]/Ov]),segments:this._draftSegmentsForPath(this._path,i>=0?t.room_drafts[i]:null,this._draftSegmentCms)};i>=0?t.room_drafts[i]=s:t.room_drafts.push(s),this._commitPhysicalGeometry(this._t("history.draft_segment"),e)}_activeWallSourceKey(e){return`active:${this._activeDraftId||"session"}:${e}`}_wallGraphSources(e){const t=this._planStructuralGeometrySnapshot().value.segments.map(e=>({a:e.a,b:e.b,key:`static:${e.key}`}));for(let i=0;i+1e.key===i);if(s>=0){const[e]=this._wallFaceGraphCache.splice(s,1);return this._wallFaceGraphCache.push(e),e.value}const n=e_(e,t);return this._wallFaceGraphCache.push({key:i,value:n}),this._wallFaceGraphCache.length>4&&this._wallFaceGraphCache.shift(),n}_offerWallFaces(e,t=this._path.length-2,i){if(this._path.length<2||this._wallFaceBatch||this._roomDialog)return;const s=this._spaceModel();if(!s)return;const n=this._activeWallSourceKey(t),o=2e-4*this._gridPitch;let a,r;try{const t=i||this._wallGraphSources(e);r=this._wallGraphSources(this._path),a=function(e,t,i){const s=new Set(e.faces.map(e=>e.key));return t.faces.filter(e=>!s.has(e.key)&&e.sourceKeys.includes(i))}(this._wallFaceGraph(t,o),this._wallFaceGraph(r,o),n)}catch{return}if(!a.length){const e=p_(r,{requiredSourceKey:n,maxDistance:Ml(2,this._cellCm,this._gridPitch),gridStep:this._gridPitch,epsilon:o});return"ambiguous"===e.kind?(this._wallRepairDiagnostic=e.proposals[0]||null,void this._showToast(this._t(0===this._drawWallCm?"toast.zero_wall_ambiguous":"toast.wall_repair_ambiguous"))):void("repair"!==e.kind||this._overlapRoom(e.face.ring)||this._beginWallFaceBatch([{...e.face,repair:e.proposal}]))}for(const e of s.rooms){const t=hi(e);if(!e.id||!t)continue;const i=zi(t,this._path,.02*this._gridPitch);if(!i)continue;const[s,n]=i,o=Ti(s)>=Ti(n)?s:n,r=o===s?n:s,l=[...a].sort((e,t)=>Math.abs(e.area-Ti(r))-Math.abs(t.area-Ti(r))||e.key.localeCompare(t.key))[0];if(!l)return;return void this._beginWallFaceBatch([{...l,ring:r.map(e=>[e[0],e[1]]),split:{roomId:e.id,mainPoly:o,newPoly:r},consumeAllActive:!0}])}const l=a.filter(e=>!(e.ring.length<3||this._contourSelfIntersects(e.ring))&&!this._overlapRoom(e.ring));l.length&&this._beginWallFaceBatch(l)}_beginWallFaceBatch(e){this._wallRepairDiagnostic=null,this._wallFaceBatch={candidates:[...e].sort((e,t)=>e.area-t.area||e.key.localeCompare(t.key)),index:0,decisions:[],activePath:this._path.map(e=>[...e]),activeCms:[...this._draftSegmentCms],activeDraftId:this._activeDraftId},this._clearPlanSnapHover(),this._nameSel="",this._areaSel="",this._resetRoomDialogFields(),this._roomDialog=!0}_offerExistingWallFace(e){if(this._path.length||this._wallFaceBatch||this._roomDialog)return!1;const t=2e-4*this._gridPitch;try{let i,s=t_(this._wallFaceGraph(this._wallGraphSources([]),t),e,t);if(!s){const n=p_(this._wallGraphSources([]),{point:e,maxDistance:Ml(2,this._cellCm,this._gridPitch),gridStep:this._gridPitch,epsilon:t});if("ambiguous"===n.kind)return this._wallRepairDiagnostic=n.proposals[0]||null,this._showToast(this._t(0===this._drawWallCm?"toast.zero_wall_ambiguous":"toast.wall_repair_ambiguous")),!0;if("repair"===n.kind)s=n.face,i=n.proposal;else if("none"===n.kind){const i=p_(this._wallGraphSources([]),{point:e,maxDistance:this._cssPxToRender(12),gridStep:this._gridPitch,epsilon:t});if("repair"===i.kind)return this._wallRepairDiagnostic=i.proposal,this._showToast(this._t("toast.wall_repair_too_large")),!0}}return!(!s||s.ring.length<3||this._contourSelfIntersects(s.ring)||this._overlapRoom(s.ring))&&(this._beginWallFaceBatch([{...s,existing:!i,repair:i}]),!0)}catch{return!1}}_columnClick(e){const t=this._snap(e),i=this._drawWallCm;if(null==i)return void this._showPhysicalRange(ih,1);if(!this._curSpaceCfg||this._limitReached("column"))return;const s=this._spaceModel();if(!s)return;const n={id:"column-"+Date.now().toString(36),shape:"square",center:t,cm:nh(i),angle:0},o=(s.wall_columns||[]).find(e=>Sh(e,n,.02*this._gridPitch));if(o)return clearTimeout(this._duplicateColumnTimer),this._duplicateColumnId=o.id,this._duplicateColumnTimer=window.setTimeout(()=>{this._duplicateColumnId=null},900),void this._showToast(this._t("toast.column_duplicate"));const a=this._geometrySnapshot(),r=this._curSpaceCfg;r.wall_columns||=[],r.wall_columns.push({...n,center:[t[0]/Ov,t[1]/Ov]}),this._commitPhysicalGeometry(this._t("history.column_add"),a)}_openPhysicalDialog(e,t,i){const s=this._spaceModel();if(s)if("partition"===e){const i=s.partitions.find(e=>e.id===t);i&&(this._physicalDialog={kind:e,id:t,cm:Sl(i.cm,this._imperial),length:this._fmtLen(i.a,i.b)})}else if("column"===e){const i=s.wall_columns.find(e=>e.id===t);i&&(this._physicalDialog={kind:e,id:t,cm:Sl(i.cm,this._imperial),shape:i.shape,angle:this._angleField("square"===i.shape?oh(i.angle):0)})}else{const n=s.room_drafts.find(e=>e.id===t),o=Math.max(0,Math.min(n?.segments.length?n.segments.length-1:0,i||0));n?.segments[o]&&(this._physicalDialog={kind:e,id:t,segment:o,cm:Sl(n.segments[o].cm,this._imperial),length:this._fmtLen(n.points[o],n.points[o+1])})}}_physicalDown(e,t,i){const s=this._spaceModel();if(!s)return;e.stopPropagation(),Gv(e);const n=this._svgPoint(e),o=[];for(const e of[...s.wall_columns].reverse())xh(n,dh(e,this._cellCm,this._gridPitch))&&o.push({kind:"column",id:e.id});for(const e of[...s.partitions].reverse()){const t=hh(e.a,e.b,e.cm,this._cellCm,this._gridPitch);t&&xh(n,t)&&o.push({kind:"partition",id:e.id})}o.some(e=>e.kind===t&&e.id===i)||o.unshift({kind:t,id:i});const a=o.map(e=>`${e.kind}:${e.id}`).sort().join("|"),r=performance.now(),l=this._physicalPickCycle,c=o.length>1&&l?.signature===a&&r-l.at>380&&r-l.at<=1200&&Math.hypot(e.clientX-l.x,e.clientY-l.y)<=10?(l.index+1)%o.length:Math.max(0,o.findIndex(e=>e.kind===t&&e.id===i));this._physicalPickCycle={signature:a,index:c,x:e.clientX,y:e.clientY,at:r},t=o[c].kind,i=o[c].id;const h="partition"===t?s.partitions.find(e=>e.id===i):s.wall_columns.find(e=>e.id===i);h&&(this._physicalSel={kind:t,id:i},this._physicalDrag={pid:e.pointerId,kind:t,id:i,start:this._svgPoint(e),startClient:[e.clientX,e.clientY],before:this._geometrySnapshot(),moved:!1,base:JSON.parse(JSON.stringify(h)),delta:[0,0]})}_clampPhysicalDelta(e,t,i){const s="partition"===e?[t.a,t.b]:[t.center],n=s.map(e=>e[0]),o=s.map(e=>e[1]);return[Math.max(-Oh*Ov-Math.min(...n),Math.min(Oh*Ov-Math.max(...n),i[0])),Math.max(-Oh*Ov-Math.min(...o),Math.min(Oh*Ov-Math.max(...o),i[1]))]}_physicalMove(e){const t=this._physicalDrag;if(!t||t.pid!==e.pointerId)return;e.stopPropagation();const i=this._svgPoint(e),s="partition"===t.kind?t.base.a:t.base.center,n=this._snap([s[0]+i[0]-t.start[0],s[1]+i[1]-t.start[1]]),o=this._clampPhysicalDelta(t.kind,t.base,[n[0]-s[0],n[1]-s[1]]);this._physicalDrag={...t,delta:o,moved:t.moved||Math.hypot(e.clientX-t.startClient[0],e.clientY-t.startClient[1])>=5}}_physicalUp(e){const t=this._physicalDrag;if(!t||t.pid!==e.pointerId)return;if(e.stopPropagation(),this._physicalDrag=null,!t.moved)return void this._registerPhysicalTap(t.kind,t.id);const i=this._spaceModel();if(!this._curSpaceCfg||!i)return;const s=this._curSpaceCfg;if("partition"===t.kind){const e=(s.partitions||[]).find(e=>e.id===t.id),i=t.base;if(e){const n={...i,a:[i.a[0]+t.delta[0],i.a[1]+t.delta[1]],b:[i.b[0]+t.delta[0],i.b[1]+t.delta[1]]};e.a=[n.a[0]/Ov,n.a[1]/Ov],e.b=[n.b[0]/Ov,n.b[1]/Ov];for(const e of s.openings||[]){if("partition"!==e.host?.kind||e.host.id!==t.id)continue;const i=ou(e,[n],Ov,this._cellCm,this._gridPitch).resolved;i&&Object.assign(e,pu(e,i,Ov))}}}else{const e=(s.wall_columns||[]).find(e=>e.id===t.id),n=t.base,o={...n,center:[n.center[0]+t.delta[0],n.center[1]+t.delta[1]]};if(i.wall_columns.some(e=>e.id!==t.id&&Sh(e,o,.02*this._gridPitch)))return void this._showToast(this._t("toast.column_duplicate"));e&&(e.center=[(n.center[0]+t.delta[0])/Ov,(n.center[1]+t.delta[1])/Ov])}this._commitPhysicalGeometry(this._t("history.physical_move"),t.before)}_registerPhysicalTap(e,t,i){const s=performance.now(),n=this._physicalLastTap?.kind===e&&this._physicalLastTap.id===t&&this._physicalLastTap.segment===i&&s-this._physicalLastTap.at<=360;this._physicalLastTap={kind:e,id:t,segment:i,at:s},n&&(this._physicalLastTap=null,this._openPhysicalDialog(e,t,i))}_cancelPhysicalGesture(){this._physicalDrag=null,this._physicalRotate=null,this.requestUpdate()}_physicalRotateDown(e,t){if("square"!==t.shape||!this._spaceModel())return;e.preventDefault(),e.stopPropagation(),Gv(e);const i=this._svgPoint(e);this._physicalSel={kind:"column",id:t.id},this._physicalRotate={pid:e.pointerId,id:t.id,center:[...t.center],startAngle:180*Math.atan2(i[1]-t.center[1],i[0]-t.center[0])/Math.PI,baseAngle:oh(t.angle),angle:oh(t.angle),before:this._geometrySnapshot(),moved:!1}}_physicalRotateMove(e){const t=this._physicalRotate;if(!t||t.pid!==e.pointerId)return;e.preventDefault(),e.stopPropagation();const i=this._svgPoint(e),s=180*Math.atan2(i[1]-t.center[1],i[0]-t.center[0])/Math.PI,n=t.baseAngle+s-t.startAngle,o=oh(e.shiftKey?n:5*Math.round(n/5));this._physicalRotate={...t,angle:o,moved:t.moved||Math.abs(n-t.baseAngle)>=.5}}_physicalRotateUp(e){const t=this._physicalRotate;if(!t||t.pid!==e.pointerId)return;e.preventDefault(),e.stopPropagation(),this._physicalRotate=null;const i=this._spaceModel();if(!t.moved||!this._curSpaceCfg||!i)return;const s=i.wall_columns.find(e=>e.id===t.id);if(!s||"square"!==s.shape)return;const n={...s,angle:t.angle};if(i.wall_columns.some(e=>e.id!==t.id&&Sh(e,n,.02*this._gridPitch)))return void this._showToast(this._t("toast.column_duplicate"));const o=this._curSpaceCfg.wall_columns?.find(e=>e.id===t.id);o&&(o.angle=t.angle,this._commitPhysicalGeometry(this._t("history.physical_edit"),t.before))}_rszRooms(){const e=[],t=this._spaceModel();if(!t)return e;for(const i of t.rooms){const t=i.id?hi(i):null;t&&e.push({id:i.id,poly:t,wall_ids:Array.isArray(i.wall_ids)?[...i.wall_ids]:void 0})}return i=e,s=Math.max(1e-12,1e-12*this._gridPitch),i.map(e=>{if(!Array.isArray(e.wall_ids)||e.wall_ids.length!==e.poly.length)return{...e,poly:e.poly.map(e=>[...e])};const t=e.poly.map(e=>[...e]);let i=!0;for(;i&&t.length>3;){i=!1;for(let e=0;e0){t.splice(e,1),i=!0;break}}}return{...e,poly:t}});var i,s}_rszOpenings(){return this._openingsR.map(e=>({id:e.id,x:e.rx,y:e.ry,length:e.rlen,hosted:!!e.host,angle:e.angle,type:e.type}))}_rszObstacles(){const e=this._spaceModel();if(!e)return[];const t=[];for(const i of e.partitions||[])t.push({kind:"segment",a:[...i.a],b:[...i.b],half:Ml(i.cm,this._cellCm,this._gridPitch)/2});for(const i of e.room_drafts||[])for(let e=0;e+1Array.isArray(e.a)&&Array.isArray(e.b)&&e.a.length>=2&&e.b.length>=2).map(e=>({wall:e,a:[e.a[0]*Ov,e.a[1]*this._spaceH],b:[e.b[0]*Ov,e.b[1]*this._spaceH]})),n=Math.abs(e[0]-t[0])<=.05*this._gridPitch?"v":Math.abs(e[1]-t[1])<=.05*this._gridPitch?"h":null,o=s.filter(i=>!!n&&("h"===n?!(Math.abs(i.a[1]-e[1])>.05*this._gridPitch||Math.abs(i.b[1]-e[1])>.05*this._gridPitch)&&Math.min(Math.max(e[0],t[0]),Math.max(i.a[0],i.b[0]))-Math.max(Math.min(e[0],t[0]),Math.min(i.a[0],i.b[0]))>.05*this._gridPitch:!(Math.abs(i.a[0]-e[0])>.05*this._gridPitch||Math.abs(i.b[0]-e[0])>.05*this._gridPitch)&&Math.min(Math.max(e[1],t[1]),Math.max(i.a[1],i.b[1]))-Math.max(Math.min(e[1],t[1]),Math.min(i.a[1],i.b[1]))>.05*this._gridPitch)),a=new Set(o.map(e=>Number(e.wall.cm)).filter(e=>e>0));return{minDim:this._cmToUnits(30),eps:.05*this._gridPitch,step:this._gridPitch,movingHalf:i>0?Ml(i,this._cellCm,this._gridPitch)/2:0,obstacles:this._rszObstacles(),thicknessConflict:a.size>1}}_rszResolution(e,t,i){const s=this._resize.snapshotIdentity||i||this._rszSnapshot(),n=`${this._space}|${this._cellCm}|${this._gridPitch}|${s}`,o=`${e}:${t}`;return this._resize.resolve(n,o,()=>{const i=[...this._resize.rooms||this._rszRooms()],s=i.find(t=>t.id===e),n=s?.poly?.[t]||[0,0],o=s?.poly?.[(t+1)%(s?.poly?.length||1)]||[0,0];return function(e,t,i,s,n){const{eps:o}=n,a=e.find(e=>e.id===i);if(!a||s<0||s>=(a.poly?.length||0)||a.poly.length<4||!Ea(a.poly))return{enabled:!1,reason:"invalid-geometry"};const r=a.poly[s],l=a.poly[(s+1)%a.poly.length],c=Ba(r,l,o);if(!c)return{enabled:!1,reason:"diagonal"};if(!Ua(a.poly,s,c,o))return{enabled:!1,reason:"side-angle"};if(n.thicknessConflict)return{enabled:!1,reason:"thickness-conflict"};for(const e of n.obstacles||[])if(Ka(e,r,l,n))return{enabled:!1,reason:"duplicate-physical-wall"};const h=Aa(Ta(l,r)),d=[],p=new Set;let u=!1,_=!1;for(const t of e)if(t.id!==i)for(let e=0;e1||d.length>1)return{enabled:!1,reason:"multiple-rooms"};const m={[i]:s},g=[i];if(1===d.length){const e=d[0];if(!Ea(e.room.poly))return{enabled:!1,reason:"invalid-geometry"};if(!Ua(e.room.poly,e.edge,c,o))return{enabled:!1,reason:"side-angle"};g.push(e.room.id),m[e.room.id]=e.edge}const f=[];for(const e of t)if(!(Fa([e.x,e.y],r,l)>2*o)){if(e.hosted||e.length>h+2*o)return{enabled:!1,reason:"opening-conflict"};f.push(e.id)}const v=Object.fromEntries(g.map(t=>[t,e.find(e=>e.id===t).poly.length])),b=[];for(const t of g){const i=e.find(e=>e.id===t),s=m[t],n=(s-1+i.poly.length)%i.poly.length,a=(s+1)%i.poly.length,r=Xa(e,t,n,1,o),l=Xa(e,t,a,0,o);if(!r||!l)return{enabled:!1,reason:"partial-shared"};b.push(r,l)}const y={roomId:i,edge:s,a:[...r],b:[...l],n:Na(a.poly,s),roomIds:g,edgeByRoom:m,topology:v,movingOpeningIds:f,sideOwnership:b};if(!nr(e,t,y,0,n))return{enabled:!1,reason:"invalid-geometry"};const w=Math.abs(Number(n.step));if(Number.isFinite(w)&&w>o){const i=[-w,w];if(!i.some(i=>nr(e,t,y,i,n))){if(!i.some(i=>er(tr(e,t,y,i),y,o)))return{enabled:!1,reason:"partial-shared"};const s={...n,obstacles:[]};return i.some(i=>nr(e,t,y,i,s))?{enabled:!1,reason:"duplicate-physical-wall"}:t.length&&i.some(t=>nr(e,[],y,t,n))?{enabled:!1,reason:"opening-conflict"}:{enabled:!1,reason:"invalid-geometry"}}}return{enabled:!0,plan:y}}(i,[...this._resize.openings||this._rszOpenings()],e,t,this._rszOptsFor(n,o))})}_rszSnapshot(){return JSON.stringify(this._geometrySnapshot()||{spaceId:this._space,rooms:[],openings:[],walls:[],open_spans:[]})}_rszResetController(){const e=null!==this._resize.preview;this._resize.reset(),e&&this._cfgEpoch++}_rszProjectPreview(e,t,i,s,n){this._rszLimitViolation=null;const o=this._serverCfg?.spaces.find(e=>e.id===this._space);if(!o||!this._serverCfg)return{ok:!1,reason:"missing-context"};const a=JSON.parse(e),r={...o,rooms:a.rooms,openings:a.openings||[]};for(const e of["walls","wall_segments","open_spans","room_drafts","partitions","wall_columns","decor"])void 0!==a[e]?r[e]=a[e]:delete r[e];for(const e of["plan_x","plan_y","plan_scale","plan_scale_x","plan_scale_y","plan_angle"])void 0!==a.plan_transform?.[e]?r[e]=a.plan_transform[e]:delete r[e];const l=this._spaceH;for(const[e,i]of Object.entries(t)){const t=r.rooms.find(t=>t.id===e);t&&(t.poly=i.map(e=>[e[0]/Ov,e[1]/l]),delete t.x,delete t.y,delete t.w,delete t.h)}for(const[e,t]of Object.entries(i)){const i=(r.openings||[]).find(t=>t.id===e);i&&(i.x=t[0]/Ov,i.y=t[1]/l)}const c=[],h=[];for(const e of s){const t=n.find(t=>t.id===e),i=r.rooms.find(t=>t.id===e);if(!t||!i?.poly)continue;const s=i.poly.map(e=>[e[0]*Ov,e[1]*l]);if(t.poly.length!==s.length)continue;const o=Array.isArray(t.wall_ids)?t.wall_ids:[];for(let e=0;ee.id===i):null;n&&(n.a=[s[e][0]/Ov,s[e][1]/Ov],n.b=[s[(e+1)%s.length][0]/Ov,s[(e+1)%s.length][1]/Ov])}}if(c.length&&Array.isArray(r.walls)&&r.walls.length){const e=function(e,t,i,s,n=1,o="fixed-topology"){let a=!1;const r=jl(e,t,i,s,n,o,()=>{a=!0});return{walls:a?(e||[]).map(e=>({...e})):r,rejected:a}}(r.walls,c,h,this._wallKeyPitch,Ov,"fixed-topology");if(e.rejected)return{ok:!1,reason:"wall-metadata"};r.walls=e.walls}const d=[];for(const e of r.rooms||[]){const t=hi(e);if(t&&!(t.length<2))for(let e=0;eJSON.stringify([e?.key,e?.cm,e?.a,e?.b]),u=new Map;for(const e of a.walls||[]){const t=p(e);u.set(t,(u.get(t)||0)+1)}const _=[];for(const e of r.walls||[]){const t=p(e),i=u.get(t)||0;i?u.set(t,i-1):_.push(e)}if(Wl(_,d,this._wallKeyPitch,Ov,a.walls||[]).length)return{ok:!1,reason:"wall-metadata"};const m=this._rszSpaceCandidateGeometry(this._space,r);if(!m.ok)return{ok:!1,reason:"physical-geometry"};const g={...this._serverCfg,spaces:(this._serverCfg?.spaces||[]).map(e=>e?.id===this._space?r:e)},f=this._junctionLimitsIntroduced(g,this._serverCfg,this._space,m.wallGeometry);return f.length?(this._rszLimitViolation=f[0],{ok:!1,reason:"junction-limit"}):{ok:!0,value:{preview:{space:this._space,sp:r},beforeWalls:a.walls||[],afterWalls:r.walls||[],artifact:m.wallGeometry}}}_rszAcceptPreview(e,t){if(this._cfgEpoch++,!e||!t)return;const i=Uc(t);if(!i)return;const s=`${this._space}|${this._cfgEpoch}|${e.sp.rooms.length}`;Object.defineProperty(i,"sourceFingerprint",{value:Jd([e.sp,this._cellCm,this._gridPitch]),enumerable:!1});const n={key:s,value:i};Mv(this._wallUnionPool,s,n,8),this._wallUnionCache=n}_rszSpaceCandidateGeometry(e,t){if(!this._serverCfg)return{ok:!1,wallGeometry:null};const i={...this._serverCfg,spaces:this._serverCfg.spaces.map(i=>i.id===e?t:i)};let s=null;try{return{ok:this._checkSpacePhysicalGeometry(i,e,e=>{s=e}).ok,wallGeometry:s}}catch{return{ok:!1,wallGeometry:null}}}_rszSpaceCandidateRenderable(e,t){if(!this._serverCfg)return!1;try{const i={...this._serverCfg,spaces:this._serverCfg.spaces.map(i=>i.id===e?t:i)};return this._checkSpacePhysicalGeometry(i,e).ok}catch{return!1}}_rszCandidateRenderable(e){return!!e&&e.space===this._space&&this._rszSpaceCandidateRenderable(e.space,e.sp)}_rszEdgeDown(e,t,i){if("resize"!==this._tool||this._resize.dragging)return;e.stopPropagation(),e.preventDefault();const s=this._rszRooms(),n=this._rszResolution(t,i);if(!n.enabled)return void this._showToast(this._rszReasonText(n.reason));Gv(e);const o=n.plan,a=this._svgPoint(e),r=`${this._space}|${this._cfgEpoch}|${s.length}`,l=this._wallUnionCache?.key===r?this._wallUnionCache.value:null,c=this._rszSnapshot();this._resize.begin({pointerId:e.pointerId,start:[a[0],a[1]],roomId:t,plan:o,options:this._rszOptsFor(o.a,o.b),rooms:s,openings:this._rszOpenings(),snapshotIdentity:c,before:JSON.parse(c),wallUnionBefore:l})}_rszReasonText(e){return this._t(`resize.disabled.${e}`)}_rszDisabledActivate(e,t){e.stopPropagation(),e.preventDefault(),this._showToast(this._rszReasonText(t))}_rszDisabledKey(e,t){"Enter"!==e.key&&" "!==e.key||this._rszDisabledActivate(e,t)}_rszMove(e){if(!this._resize.ownsPointer(e.pointerId))return;e.stopPropagation();const t=this._svgPoint(e),i=this._resize.move({pointerId:e.pointerId,point:[t[0],t[1]],step:this._gridPitch,snap:e=>{const t=this._snap(e);return[t[0],t[1]]},project:(e,t,i,s,n)=>this._rszProjectPreview(e,t,i,s,n),publish:(e,t)=>this._rszAcceptPreview(e,t),measure:(e,t)=>this._rszEdgeLabels(e,t)});if("rejected"===i.kind)return i.notify&&this._showToast(this._rszLimitViolation?`${this._t("resize.limit_stopped")} — `+this._junctionLimitLabel(this._rszLimitViolation):this._t("resize.preview_failed")),void this.requestUpdate();"accepted"===i.kind&&this.requestUpdate()}_rszUp(e){if(!this._resize.ownsPointer(e.pointerId))return;e.stopPropagation();const t=this._resize.finish({pointerId:e.pointerId,currentSnapshotIdentity:this._rszSnapshot(),validatePreview:e=>this._rszCandidateRenderable(e)});if("no-op"===t.kind)return this._cfgEpoch++,void this.requestUpdate();if("rejected"===t.kind)return this._cfgEpoch++,this._showToast(this._t("resize.commit_failed")),void this.requestUpdate();const i=t.preview,s=this._serverCfg?.spaces.find(e=>e.id===i.space);s&&(s.rooms=i.sp.rooms,s.openings=i.sp.openings,Array.isArray(i.sp.walls)&&(i.sp.walls.length?s.walls=i.sp.walls:delete s.walls),Array.isArray(i.sp.wall_segments)&&(s.wall_segments=i.sp.wall_segments)),this._suppressClick=!0,setTimeout(()=>this._suppressClick=!1,0),this._commitPhysicalGeometry(this._t("history.resize_room"),t.before),this.requestUpdate()}_rszCancelDrag(e){const t=this._resize.cancel(this._rszSnapshot(),e);if("no-op"!==t.kind){if(this._cfgEpoch++,t.restoreWallUnion){const e=this._spaceModel();if(e){const i=`${this._space}|${this._cfgEpoch}|${e.rooms.length}`,s={key:i,value:t.restoreWallUnion};Mv(this._wallUnionPool,i,s,8),this._wallUnionCache=s}}this.requestUpdate()}}_rszPointerCancel(e){this._resize.ownsPointer(e.pointerId)&&(e.stopPropagation(),this._rszCancelDrag(e.pointerId))}_rszEdgeLabels(e,t,i=this._resize.rooms){const s=i,n=[],o=e.polys[t.roomId]||s?.find(e=>e.id===t.roomId).poly;if(!o||!s)return n;const a=o.length,r=this._rszInnerSpanCms(t.roomId,o,e.polys);for(const e of function(e,t){const i=e.length;return[(t-1+i)%i,(t+1)%i]}(o,t.edge)){const t=o[e],i=o[(e+1)%a],s=r?.[e];n.push({kind:"length",x:(t[0]+i[0])/2,y:(t[1]+i[1])/2,text:null==s?this._fmtLen(t,i):li(s,"mi"===this.hass?.config?.unit_system?.length),edge:{a:[t[0],t[1]],b:[i[0],i[1]]}})}const l="mi"===this.hass?.config?.unit_system?.length,c=t.roomIds,h=this._spaceWalls,d=this._physicalBodiesR(),p=this._baseVb(),u=this._view&&this._view.w>0&&this._view.h>0?this._view:{x:p[0],y:p[1],w:p[2],h:p[3]},[_,m]=this._lastValidStageSize||[u.w,u.h],g={...u,stageWidth:Math.max(1,_),stageHeight:Math.max(1,m)},f=this._spaceModel(),v=this._config?.icon_size??2.5,b=f?od(v>8?2.5:v,f,u.w,this._kiosk?this._kioskScale.icon:1)*g.stageWidth/100:24,y=Math.max(10,.77*b),w=this._t("room.settings_short"),k=y*(1.457+.66*Math.max(1,w.length)*.42);for(const i of c){const o=e.polys[i]||s.find(e=>e.id===i).poly,a=h.length&&f&&this._innerRoomContour(f,i)||o,r=rr(d.length?yh(bh(a,d))*Math.pow(this._cellCm/this._gridPitch,2)/1e4:ar(a,this._gridPitch,this._cellCm),l),c=_r({poly:o,edge:t.edgeByRoom[i],text:r,view:g,gearCenter:$i(o),gearWidthPx:k,gearHeightPx:y});n.push({kind:"area",roomId:i,x:c.anchor[0],y:c.anchor[1],text:r,placement:c})}return n}_rszInnerSpanCms(e,t,i){const s=Object.keys(i).length?Object.entries(i).map(([e,t])=>({id:e,poly:t})):this._spaceModel()?.rooms;if(!s?.length)return null;const n=this._spaceWalls;if(!n.length)return null;const o=function(e,t,i,s,n,o,a,r=1){const l=(e||[]).find(e=>e?.id===t),c=hi(l);if(!c||c.length<3)return null;const h=yc(e,t,i,s,n,o,a,r);if(!h)return c.map(()=>0);const d=4*sc(n,r);return c.map((e,t)=>{const i=c[(t+1)%c.length],s=[(e[0]+i[0])/2,(e[1]+i[1])/2];for(let e=0;efunction(e,t,i){const s=e?.length||0;if(s<3||!Array.isArray(i)||i.length!==s)return 0;const n=(Math.trunc(t)%s+s)%s,o=e[n],a=e[(n+1)%s],r=a[0]-o[0],l=a[1]-o[1],c=Math.hypot(r,l);if(!(c>0))return 0;const h=Math.max(0,Number(i[n])||0);if(!(h>0))return c;const d=[r/c,l/c],p=oc(e,n),u=[o[0]+p[0]*h,o[1]+p[1]*h],_=t=>{const n=Math.max(0,Number(i[t])||0);if(!(n>0))return null;const a=e[t],r=e[(t+1)%s],l=r[0]-a[0],c=r[1]-a[1],h=Math.hypot(l,c);if(!(h>0))return null;const p=oc(e,t),_=rc(u,d,[a[0]+p[0]*n,a[1]+p[1]*n],[l/h,c/h]);return _?(_[0]-o[0])*d[0]+(_[1]-o[1])*d[1]:null},m=_((n-1+s)%s)??0,g=(_((n+1)%s)??c)-m;return g>0?g:0}(t,i,o)*a)}_renderResizeMeasurements(){const e=this._resize.liveLabels;if(!e?.length)return G;const t=e.filter(e=>"length"===e.kind),i=e.filter(e=>"area"===e.kind);return W``}_renderResizeLayer(e){const t=Math.max(.013*e.w,5),i=t/2,s=e=>e.toFixed(1),n=`M ${s(-.7*i)} 0 H ${s(.7*i)} M 0 ${s(-.22*i)} V ${s(-i)} M ${s(-.32*i)} ${s(-.6*i)} L 0 ${s(-i)} L ${s(.32*i)} ${s(-.6*i)} M 0 ${s(.22*i)} V ${s(i)} M ${s(-.32*i)} ${s(.6*i)} L 0 ${s(i)} L ${s(.32*i)} ${s(.6*i)}`,o=[],a=this._rszRooms(),r=this._resize.snapshotIdentity||this._rszSnapshot();for(const e of a)for(let i=0;ithis._rszEdgeDown(t,e.id,i)} - @pointermove=${e=>this._rszMove(e)} - @pointerup=${e=>this._rszUp(e)} - @pointercancel=${e=>this._rszPointerCancel(e)} - @lostpointercapture=${e=>this._rszPointerCancel(e)} - @click=${u?e=>this._rszDisabledActivate(e,p.reason):null} - @keydown=${u?e=>this._rszDisabledKey(e,p.reason):null}> - ${_} - `),o.push(W``)}return W`${o}`}get _openingsR(){const e=this._curSpaceCfg,t=this._spaceH,i=this._spaceModel();return i?(e?.openings||[]).flatMap(e=>{const s={...e,rx:e.x*Ov,ry:e.y*t,rlen:e.length*Ov};if(!e.host||"wall"===e.host.kind)return[s];const n=ou(e,i.partitions,Ov,this._cellCm,this._gridPitch);if(!n.resolved)return"plan"===this._mode?[{...s,orphanReason:n.reason||"invalid-host"}]:[];let[o,a]=n.resolved.center;const r=this._physicalDrag;return r?.moved&&"partition"===r.kind&&r.id===e.host.id&&(o+=r.delta[0],a+=r.delta[1]),[{...e,rx:o,ry:a,rlen:n.resolved.length,angle:n.resolved.angle,partitionHost:n.resolved}]}):[]}_partitionOpeningCuts(e=this._spaceModel(),t=()=>!0){if(!e)return[];return Vg(Gg(this._curSpaceCfg?.id===e.id?this._curSpaceCfg:null,e,this._cellCm,this._gridPitch,Ov),t)}_roomWallOpeningInputs(e=this._openingsR,t=this._spaceModel()){if(!t)return[];const i=this._openCuts();return Kg(e,t,this._spaceWalls,i,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov)}_openingFace(e,t,i){return e.partitionHost?cu(e.partitionHost,i):Zc(t,{x:e.rx,y:e.ry,angle:e.angle,length:e.rlen,flip_v:i})}_cmToUnits(e){return e/this._cellCm*this._gridPitch}get _decorList(){const e=this._curSpaceCfg;return Array.isArray(e?.decor)?e.decor:[]}get _decorH(){return Ov}_decorResolvedStyle(e){return function(e,t,i,s=Br){const n="rect"===e?.kind||"ellipse"===e?.kind,o=e,a=Ie(o?.color,s.color);return{color:a,opacity:jr(o?.opacity,s.opacity),widthCm:Vr(e,t,i,s.widthCm),fill:!!n&&!0===o?.fill,fillColor:Ie(o?.fill_color,o?.fill?a:s.fillColor),fillOpacity:n&&o?.fill?jr(o?.fill_opacity,.25):s.fillOpacity}}(e,this._cellCm,this._gridPitch,Br)}_decorWidthUnits(e){return((e,t,i,s=Br.widthCm)=>{const n=Number(e?.width_cm);if(Number.isFinite(n)&&n>0)return Ur(n,t,i);const o=Number(e?.width);return Number.isFinite(o)&&o>0?o:Ur(s,t,i)})(e,this._cellCm,this._gridPitch,Br.widthCm)}_decorTextSizeCm(e){if("text"===e?.kind){const t=Number(e.size_cm);return Number.isFinite(t)&&t>0?t:Gr(ts*is(e),this._cellCm,this._gridPitch)}return Gr(ts,this._cellCm,this._gridPitch)}_decorTextUnits(e){if("text"!==e.kind)return ts;const t=Number(e.size_cm);return Number.isFinite(t)&&t>0?Ur(t,this._cellCm,this._gridPitch):ts*is(e)}_decorSmallField(e){return Math.round(100*(this._imperial?e/2.54:e))/100}_decorSmallCm(e){const t=this._imperial?2.54*e:e;return Number.isFinite(t)?Math.max(.1,Math.min(100,t)):.1}_decorTextCm(e){const t=this._imperial?2.54*e:e;return Number.isFinite(t)?Math.max(.1,Math.min(wv,t)):.1}_decorLargeField(e){return Math.round(100*(this._imperial?e/30.48:e/100))/100}_decorLargeCm(e){const t=this._imperial?30.48*e:100*e;return Number.isFinite(t)?Math.max(.1,Math.min(Oh*this._cellCm,t)):.1}_angleField(e){const t=Number(e);return Number.isFinite(t)?String(Number(t.toFixed(3))):"0"}_decorBoxOf(e){return"rect"!==e.kind&&"ellipse"!==e.kind&&"furniture"!==e.kind?null:{x:e.x*Ov,y:e.y*this._decorH,w:e.w*Ov,h:e.h*this._decorH,angle:qr(e.angle)||void 0}}_decorSnapGeometry(e){const t=this._spaceModel();if(!t)return{points:[],segments:[]};const i=e||"",s=this._decorSnapCache;if(s&&s.epoch===this._cfgEpoch&&s.space===this._space&&s.height===this._decorH&&s.exclude===i)return s.geometry;const n=[];for(const t of this._decorList)if(t.id!==e)if("line"===t.kind){const e=[t.x1*Ov,t.y1*this._decorH],i=[t.x2*Ov,t.y2*this._decorH];n.push({points:[e,i,[(e[0]+i[0])/2,(e[1]+i[1])/2]],segments:[{a:e,b:i}]})}else if("text"===t.kind)n.push({points:[[t.x*Ov,t.y*this._decorH]],segments:[]});else{const e=this._decorBoxOf(t);e&&n.push(Jr(e))}for(const e of t.rooms){const t=hi(e);t?.length&&n.push({points:t.flatMap((e,i)=>{const s=t[(i+1)%t.length];return[e,[(e[0]+s[0])/2,(e[1]+s[1])/2]]}),segments:t.map((e,i)=>({a:e,b:t[(i+1)%t.length]}))})}const o=function(e){return{points:e.flatMap(e=>e.points),segments:e.flatMap(e=>e.segments)}}(n);return this._decorSnapCache={epoch:this._cfgEpoch,space:this._space,height:this._decorH,exclude:i,geometry:o},o}_decorSnap(e,t="mouse",i){const s=this._stageEl,n=this._viewOr(this._baseVb()),o="touch"===t||"pen"===t?14:8,a=s?n.w/Math.max(1,s.clientWidth)*o:this._gridPitch;return function(e,t,i,s){const n=s(e);let o=null;for(const n of t.points){const t=s(n),a=Math.hypot(t[0]-e[0],t[1]-e[1]);a<=i&&(!o||athis._snap(e)).point}_replaceDecor(e,t){const i=this._curSpaceCfg;i&&(i.decor=this._decorList.map(i=>i.id===e?{...i,...t}:i),this.requestUpdate())}_cancelDecorGesture(){const e=this._decorMove?.before||this._dtDrag?.before||this._bdDrag?.before,t=e&&this._serverCfg?.spaces.find(t=>t.id===e.spaceId);if(e&&t){const i=e=>JSON.parse(JSON.stringify(e));void 0!==e.decor?t.decor=i(e.decor):delete t.decor;for(const e of["plan_x","plan_y","plan_scale","plan_scale_x","plan_scale_y","plan_angle"])delete t[e];Object.assign(t,i(e.plan_transform||{})),this._cfgEpoch++}this._decorMove=null,this._dtDrag=null,this._bdDrag=null,this.requestUpdate()}_decorPointerDown(e){const t=this._decorTool;if("select"===t||"erase"===t?e.target.closest?.(".dshape"):null)return!0;if("line"===t||"rect"===t||"ellipse"===t){e.preventDefault();const i=this._decorSnap(this._svgPoint(e),e.pointerType);return this._decorDraft={kind:t,a:i,b:i,pid:e.pointerId},Gv(e),!0}if("text"===t){const t=this._decorSnap(this._svgPoint(e),e.pointerType);return this._decorTextDialog={x:Gh(t[0]/Ov),y:Gh(t[1]/this._decorH),text:"",color:this._decorStyle.color,opacity:this._decorStyle.opacity,angle:"0",sizeCm:Gr(ts,this._cellCm,this._gridPitch)},this._decorTextSelection={start:0,end:0},!0}if("furniture"===t)return!!this._furnPalette&&(e.preventDefault(),this._furnPlace(this._svgPoint(e),e.shiftKey),!0);if("select"===t&&(this._decorSel=null),this._bdMovable){const t=this._bdRect,i=this._svgPoint(e),s=t.x+t.w/2,n=t.y+t.h/2,o=-qr(t.angle)*Math.PI/180,a=i[0]-s,r=i[1]-n,l=[s+a*Math.cos(o)-r*Math.sin(o),n+a*Math.sin(o)+r*Math.cos(o)];if(l[0]>=t.x&&l[0]<=t.x+t.w&&l[1]>=t.y&&l[1]<=t.y+t.h)return e.preventDefault(),this._bdStart(e)}return!1}_decorCommitDraft(){const e=this._decorDraft;if(this._decorDraft=null,!e)return;const t=.5*this._gridPitch;if(!function(e,t,i,s){const n=Math.abs(i[0]-t[0]),o=Math.abs(i[1]-t[1]);return"line"===e?Math.hypot(n,o)>=s:n>=s&&o>=s}(e.kind,e.a,e.b,t))return;const i=Ov,s=this._decorH,n=this._decorStyle,o=this._geometrySnapshot(),a="dc"+Date.now().toString(36)+Math.random().toString(36).slice(2,5),r=Gh;let l;if("line"===e.kind)l={id:a,kind:"line",x1:r(e.a[0]/i),y1:r(e.a[1]/s),x2:r(e.b[0]/i),y2:r(e.b[1]/s),...Kr(n,!1)};else{const t=r(Math.min(e.a[0],e.b[0])/i),o=r(Math.min(e.a[1],e.b[1])/s),c=Math.abs(e.b[0]-e.a[0])/i,h=Math.abs(e.b[1]-e.a[1])/s;l={id:a,kind:e.kind,x:t,y:o,w:c,h:h,...Kr(n,!0)}}this._curSpaceCfg.decor=[...this._decorList,l],this._decorSel=a,this._recordGeometry(this._t("history.decor_add"),o),this._saveConfig(),this.requestUpdate()}_decorShapeDown(e,t){if("decor"!==this._mode)return;const i=this._decorTool;if("text"===i){if("text"!==t.kind)return;return e.stopPropagation(),e.preventDefault(),void this._decorOpenText(t)}"select"!==i&&"erase"!==i||(e.stopPropagation(),e.preventDefault(),"erase"!==i?(this._decorSel=t.id,this._decorMove={id:t.id,start:this._svgPoint(e),orig:JSON.parse(JSON.stringify(t)),pid:e.pointerId,moved:!1,before:this._geometrySnapshot()},Gv(e)):this._decorEraseConfirm={id:t.id,kind:t.kind})}_decorMoveUpdate(e){const t=this._decorMove;if("furniture"===t.orig?.kind)return void this._furnMoveUpdate(e);const i=this._svgPoint(e),s=t.orig,n=("line"===s.kind?s.x1:s.x)*Ov,o=("line"===s.kind?s.y1:s.y)*this._decorH,a=this._decorSnap([n+(i[0]-t.start[0]),o+(i[1]-t.start[1])],e.pointerType,t.id);let r=(a[0]-n)/Ov,l=(a[1]-o)/this._decorH;const c=t.orig,h="line"===c.kind?Math.min(c.x1,c.x2):c.x,d="line"===c.kind?Math.min(c.y1,c.y2):c.y,p="line"===c.kind?Math.abs(c.x2-c.x1):c.w||0,u="line"===c.kind?Math.abs(c.y2-c.y1):c.h||0,_=Oh;r=Math.max(-_-h,Math.min(_-h-p,r)),l=Math.max(-_-d,Math.min(_-d-u,l)),(r||l)&&(t.moved=!0);this._curSpaceCfg.decor=this._decorList.map(e=>{if(e.id!==t.id)return e;const i=t.orig;return"line"===e.kind?{...e,x1:i.x1+r,y1:i.y1+l,x2:i.x2+r,y2:i.y2+l}:{...e,x:i.x+r,y:i.y+l}}),this.requestUpdate()}_decorShapeDbl(e,t){"decor"===this._mode&&"select"===this._decorTool&&(e.preventDefault(),e.stopPropagation(),this._decorMove=null,this._decorSel=t.id,this._openDecorProperties(t))}_openDecorProperties(e){if("decor"!==this._mode||"select"!==this._decorTool)return;if("text"===e.kind)return void this._decorOpenText(e);if(!["line","rect","ellipse","furniture"].includes(e.kind))return;const t=this._decorResolvedStyle(e),i="line"===e.kind?e:null,s=this._decorBoxOf(e);this._decorShapeDialog={id:e.id,kind:e.kind,color:t.color,opacity:t.opacity,widthCm:t.widthCm,angle:this._angleField(qr(i?Es([i.x1*Ov,i.y1*this._decorH],[i.x2*Ov,i.y2*this._decorH]):e.angle)),...i?{lengthCm:Gr(Math.hypot((i.x2-i.x1)*Ov,(i.y2-i.y1)*this._decorH),this._cellCm,this._gridPitch),lineStyle:"dashed"===i.line_style?"dashed":"solid"}:{},...s?{sizeWCm:Gr(s.w,this._cellCm,this._gridPitch),sizeHCm:Gr(s.h,this._cellCm,this._gridPitch)}:{},..."furniture"===e.kind?{symbol:e.symbol}:{},..."rect"===e.kind||"ellipse"===e.kind?{fill:t.fill,fillColor:t.fillColor,fillOpacity:t.fillOpacity}:{}}}_decorOpenText(e){if("text"!==e.kind)return;let t=String(e.text??"");const i=[...t.matchAll(/\{([^{}\r\n]+)\}/g)].some(e=>!!Yi(e[1])),s=String(e.unit??"").trim(),n="state"===String(e.attr??"").trim().toLowerCase()?null:e.attr,o=i||s?"":Ji(e.entity,n),a=!(!String(e.entity??"").trim()||i||o&&!s);if(o&&!a){const e=t.indexOf("{}");t=e>=0?t.slice(0,e)+o+t.slice(e+2):`${t}${t?" ":""}${o}`}this._decorTextDialog={id:e.id,x:e.x,y:e.y,text:t,color:Ie(e.color,this._decorStyle.color),opacity:jr(e.opacity,this._decorStyle.opacity),angle:this._angleField(e.angle),sizeCm:this._decorTextSizeCm(e),pickerEntity:e.entity||"",preserveLegacy:a||void 0},this._decorTextSelection={start:t.length,end:t.length}}_decorRememberTextSelection(e){this._decorTextSelection={start:e.selectionStart??e.value.length,end:e.selectionEnd??e.value.length}}_decorInsertLiveVariable(e){const t=this._decorTextDialog;if(!t)return;const i=Ji(t.pickerEntity,e);if(!i)return;const s=t.text,n=Math.max(0,Math.min(s.length,this._decorTextSelection.start)),o=Math.max(n,Math.min(s.length,this._decorTextSelection.end));if(s.length-(o-n)+i.length>200)return;const a=s.slice(0,n)+i+s.slice(o),r=n+i.length;this._decorTextSelection={start:r,end:r},this._decorTextDialog={...t,text:a,preserveLegacy:void 0},this.updateComplete.then(()=>{const e=this.renderRoot.querySelector("textarea.dtarea");e&&(e.focus(),e.setSelectionRange(r,r))})}_decorSaveText(){const e=this._decorTextDialog,t=String(e?.text??"").replace(/\r\n?/g,"\n").trim();if(!e||!t)return void(this._decorTextDialog=null);const i=this._geometrySnapshot(),s=this._curSpaceCfg,n={color:e.color,opacity:jr(e.opacity),size_cm:Number(Math.max(.1,Math.min(wv,e.sizeCm)).toFixed(4)),...qr(e.angle)?{angle:qr(e.angle)}:{}};if(e.id)s.decor=this._decorList.map(i=>{if(i.id!==e.id)return i;if("text"!==i.kind)return i;if(e.preserveLegacy){const{angle:e,size:s,scale:o,...a}=i;return{...a,text:t,...n}}const{entity:s,attr:o,unit:a,...r}=i,{angle:l,size:c,scale:h,...d}=r;return{...d,text:t,...n}});else{const i="dc"+Date.now().toString(36)+Math.random().toString(36).slice(2,5);s.decor=[...this._decorList,{id:i,kind:"text",x:e.x,y:e.y,text:t,...n}],this._decorSel=i}this._decorTextDialog=null,this._recordGeometry(this._t(e.id?"history.decor_edit":"history.decor_add"),i),this._saveConfig(),this.requestUpdate()}_decorSaveShape(){const e=this._decorShapeDialog;if(!e)return;const t=this._geometrySnapshot(),i={color:e.color,opacity:jr(e.opacity),widthCm:Math.max(.1,Math.min(100,Number(e.widthCm)||.1)),fill:!!e.fill,fillColor:e.fillColor||e.color,fillOpacity:jr(e.fillOpacity,.25)};this._curSpaceCfg.decor=this._decorList.map(t=>{if(t.id!==e.id)return t;const s="rect"===e.kind||"ellipse"===e.kind,n=Kr(i,s);if("line"===t.kind){const i=(t.x1+t.x2)/2*Ov,s=(t.y1+t.y2)/2*this._decorH,o=Ur(Math.max(.1,Number(e.lengthCm)||.1),this._cellCm,this._gridPitch),a=qr(e.angle)*Math.PI/180,r=Math.cos(a)*o/2,l=Math.sin(a)*o/2,c=this._snap([i-r,s-l]),h=this._snap([i+r,s+l]),{width:d,line_style:p,...u}=t;return{...u,...n,x1:Gh(c[0]/Ov),y1:Gh(c[1]/this._decorH),x2:Gh(h[0]/Ov),y2:Gh(h[1]/this._decorH),..."dashed"===e.lineStyle?{line_style:"dashed"}:{}}}if("rect"===t.kind||"ellipse"===t.kind||"furniture"===t.kind){const i=t.w*Ov,s=t.h*this._decorH,o=Math.max(this._gridPitch,ri(Ur(Number(e.sizeWCm),this._cellCm,this._gridPitch),this._gridPitch)),a=Math.max(this._gridPitch,ri(Ur(Number(e.sizeHCm),this._cellCm,this._gridPitch),this._gridPitch)),r=t.x*Ov+i/2,l=t.y*this._decorH+s/2,c=qr(e.angle),h=Wr({x:r-o/2,y:l-a/2},c,e=>this._snap(e)),{width:d,angle:p,...u}=t;return{...u,...n,x:Gh(h[0]/Ov),y:Gh(h[1]/this._decorH),w:o/Ov,h:a/this._decorH,..."furniture"===t.kind&&e.symbol?{symbol:e.symbol}:{},...c?{angle:c}:{}}}return t}),this._decorStyle={...i,fill:"rect"===e.kind||"ellipse"===e.kind?i.fill:this._decorStyle.fill,fillColor:"rect"===e.kind||"ellipse"===e.kind?i.fillColor:this._decorStyle.fillColor,fillOpacity:"rect"===e.kind||"ellipse"===e.kind?i.fillOpacity:this._decorStyle.fillOpacity},this._decorShapeDialog=null,this._recordGeometry(this._t("history.decor_edit"),t),this._saveConfig(),this.requestUpdate()}get _dtSel(){return"decor"===this._mode&&"select"===this._decorTool&&this._decorSel&&this._decorList.find(e=>e.id===this._decorSel)||null}_dtPivot(e){return"line"===e.kind?[(e.x1+e.x2)/2*Ov,(e.y1+e.y2)/2*this._decorH]:"furniture"===e.kind||"rect"===e.kind||"ellipse"===e.kind?[(e.x+e.w/2)*Ov,(e.y+e.h/2)*this._decorH]:[e.x*Ov,e.y*this._decorH]}_dtApply(e,t){const i=this._curSpaceCfg;i&&(i.decor=this._decorList.map(i=>{if(i.id!==e)return i;const s={...i};"text"===i.kind&&void 0!==t.textSizeCm&&(delete s.size,delete s.scale);const n={...s};return void 0!==t.textSizeCm&&(n.size_cm=Number(Math.max(.1,Math.min(wv,t.textSizeCm)).toFixed(4))),void 0!==t.angle&&(t.angle?n.angle=Number(t.angle.toFixed(2)):delete n.angle),n}),this._cfgEpoch++,this.requestUpdate())}_dtStart(e,t,i,s){const n=this._dtSel;if(!n)return;e.stopPropagation(),e.preventDefault();const[o,a]=this._dtPivot(n),r=this._svgPoint(e),l=this._decorBoxOf(n);this._dtDrag={id:n.id,kind:t,pid:e.pointerId,ax:o,ay:a,r0:Math.hypot(r[0]-o,r[1]-a),a0:180*Math.atan2(r[1]-a,r[0]-o)/Math.PI,textSizeCm0:"text"===n.kind?this._decorTextSizeCm(n):1,angle0:"line"===n.kind?0:Number(n.angle)||0,sgx:i?.[0],sgy:i?.[1],orig:l||void 0,origShape:JSON.parse(JSON.stringify(n)),before:this._geometrySnapshot(),lineEnd:s,moved:!1},Gv(e)}_dtMove(e){const t=this._dtDrag;if(!t)return;const i=this._svgPoint(e);if(void 0!==t.lineEnd&&"line"===t.origShape.kind){const s=this._decorSnap(i,e.pointerType,t.id),n=Gh(s[0]/Ov),o=Gh(s[1]/this._decorH),a=t.origShape,r=0===t.lineEnd?a.x1:a.x2,l=0===t.lineEnd?a.y1:a.y2;return(Math.abs(n-r)>1e-9||Math.abs(o-l)>1e-9)&&(t.moved=!0),void this._replaceDecor(t.id,0===t.lineEnd?{x1:n,y1:o}:{x2:n,y2:o})}if("scale"===t.kind&&t.orig){const s=Xr(t.orig,t.sgx??1,t.sgy??1,i[0],i[1],!e.shiftKey,this._gridPitch,this._gridPitch),n=Math.abs(s.x-t.orig.x)>1e-6||Math.abs(s.y-t.orig.y)>1e-6||Math.abs(s.w-t.orig.w)>1e-6||Math.abs(s.h-t.orig.h)>1e-6;if(!n&&!t.moved)return;return t.moved||=n,void this._decorApplyBox(t.id,s)}if("scale"===t.kind){const e=Math.hypot(i[0]-t.ax,i[1]-t.ay);if(t.r0<1e-6)return;const s=Math.max(.1,Math.min(wv,t.textSizeCm0*(e/t.r0))),n=Math.abs(s-t.textSizeCm0)>1e-6;if(!n&&!t.moved)return;return t.moved||=n,void this._dtApply(t.id,{textSizeCm:s})}const s=180*Math.atan2(i[1]-t.ay,i[0]-t.ax)/Math.PI;let n=t.angle0+(s-t.a0);e.shiftKey||(n=5*Math.round(n/5)),n=(n%360+360)%360,n>180&&(n-=360);const o=Math.abs(n-t.angle0)>1e-6;(o||t.moved)&&(t.moved||=o,this._dtApply(t.id,{angle:n}))}_dtUp(){const e=this._dtDrag;this._dtDrag=null,e?.moved&&(this._recordGeometry(this._t("history.decor_transform"),e.before),this._saveConfig()),this.requestUpdate()}_dtMeasure(){const e=this._dtSel;if(!e)return void(this._dtBox&&(this._dtBox=null,this.requestUpdate()));let t;if("line"===e.kind){const i=e.x1*Ov,s=e.y1*this._decorH,n=e.x2*Ov,o=e.y2*this._decorH;t={id:e.id,x:Math.min(i,n),y:Math.min(s,o),w:Math.abs(n-i),h:Math.abs(o-s)}}else if("furniture"===e.kind||"rect"===e.kind||"ellipse"===e.kind)t={id:e.id,x:e.x*Ov,y:e.y*this._decorH,w:e.w*Ov,h:e.h*this._decorH};else{const i=this.renderRoot.querySelector(`text.dtext[data-id="${e.id}"]`);if(!i||"function"!=typeof i.getBBox)return;let s;try{s=i.getBBox()}catch{return}if(!s||!s.width&&!s.height)return;t={id:e.id,x:s.x,y:s.y,w:s.width,h:s.height}}const i=this._dtBox;i&&i.id===t.id&&Math.abs(i.x-t.x)<.01&&Math.abs(i.y-t.y)<.01&&Math.abs(i.w-t.w)<.01&&Math.abs(i.h-t.h)<.01||(this._dtBox=t,this.requestUpdate())}_deleteDecor(e){if(!this._decorList.some(t=>t.id===e))return;const t=this._geometrySnapshot();this._curSpaceCfg.decor=this._decorList.filter(t=>t.id!==e),this._decorSel===e&&(this._decorSel=null),this._recordGeometry(this._t("history.decor_delete"),t),this._saveConfig(),this.requestUpdate()}_decorDeleteSel(){this._decorSel&&this._deleteDecor(this._decorSel)}_confirmDecorErase(){const e=this._decorEraseConfirm;this._decorEraseConfirm=null,e&&this._deleteDecor(e.id)}get _furnWalls(){const e=this._rawPhysicalBodiesR().flatMap(e=>e.map((t,i)=>{const s=e[(i+1)%e.length];return[t[0],t[1],s[0],s[1]]}));return[...this._segments,...e]}get _furnWallReach(){return 6*this._gridPitch}_furnFieldValue(e){return Math.round(100*(this._imperial?e/30.48:e/100))/100}_furnFieldToCm(e){return t=this._imperial?30.48*e:100*e,Number.isFinite(t)?Math.max(1,Math.min(1e4,t)):1;var t}_furnPick(e){const t=function(e){const t=pd(e);return t?{w:t.w,h:t.h}:{w:60,h:60}}(e);this._furnPalette={symbol:e,w:t.w,h:t.h}}_furnPlace(e,t=!1){const i=this._furnPalette,s=this._curSpaceCfg;if(!i||!s)return;const n=Ov,o=this._decorH,a=fd(_d(i.w,this._cellCm,this._gridPitch,n)),r=fd(_d(i.h,this._cellCm,this._gridPitch,n)),l=this._geometrySnapshot(),c=this._decorSnap(e);let h=c[0],d=c[1],p=0;const u=t?null:wd(h,d,r*o,this._furnWalls,this._furnWallReach,this._gridPitch);u&&(h=u.cx,d=u.cy,p=u.angle);const _="df"+Date.now().toString(36)+Math.random().toString(36).slice(2,5),m={id:_,kind:"furniture",symbol:i.symbol,x:Gh(h/n-a/2),y:Gh(d/o-r/2),w:a,h:r,...Kr(this._decorStyle,!1)};p&&(m.angle=Number(p.toFixed(2))),s.decor=[...this._decorList,m],this._decorSel=_,this._decorTool="select",this._furnPalette=null,this._recordGeometry(this._t("history.decor_add"),l),this._saveConfig(),this.requestUpdate()}_furnMoveUpdate(e){const t=this._decorMove;if("furniture"!==t.orig.kind)return;const i=t.orig,s=this._curSpaceCfg;if(!s)return;const n=Ov,o=this._decorH,a=this._svgPoint(e),r=(i.x+i.w/2)*n+(a[0]-t.start[0]),l=(i.y+i.h/2)*o+(a[1]-t.start[1]);let c,h,d=Number(i.angle)||0;const p=e.shiftKey?null:wd(r,l,i.h*o,this._furnWalls,this._furnWallReach,this._gridPitch);if(p)c=p.cx/n-i.w/2,h=p.cy/o-i.h/2,d=p.angle;else{const s=this._decorSnap([r-i.w/2*n,l-i.h/2*o],e.pointerType,t.id);c=s[0]/n,h=s[1]/o}c=Gh(c),h=Gh(h),(Math.abs(c-i.x)>1e-9||Math.abs(h-i.y)>1e-9||Math.abs(d-(Number(i.angle)||0))>1e-9)&&(t.moved=!0),s.decor=this._decorList.map(e=>{if(e.id!==t.id)return e;const i={...e,x:c,y:h};return d?i.angle=Number(d.toFixed(2)):delete i.angle,i}),this.requestUpdate()}_decorApplyBox(e,t){const i=this._curSpaceCfg;if(!i)return;const s=Ov,n=this._decorH;i.decor=this._decorList.map(i=>{if(i.id!==e)return i;const o=Wr(t,i.angle,e=>this._snap(e));return{...i,x:Gh(o[0]/s),y:Gh(o[1]/n),w:Math.max(this._gridPitch/s,Math.min(2*Oh,t.w/s)),h:Math.max(this._gridPitch/n,Math.min(2*Oh,t.h/n))}}),this._cfgEpoch++,this.requestUpdate()}get _furnLive(){const e=this._dtDrag;if(!e||"scale"!==e.kind||!e.orig)return null;const t=this._decorList.find(t=>t.id===e.id);if(!t||"furniture"!==t.kind)return null;const i=Ov,s=this._decorH,n=t.w*i,o=t.h*s,a=function(e,t,i,s,n){const o=e+i/2,a=t+s/2,r=(Number(n)||0)*Math.PI/180,l=Math.cos(r),c=Math.sin(r),h=(e,t)=>{const i=e-o,s=t-a;return[o+i*l-s*c,a+i*c+s*l]};return[h(e,t),h(e+i,t),h(e+i,t+s),h(e,t+s)]}(t.x*i,t.y*s,n,o,Number(t.angle)||0),r=(e,t)=>[(e[0]+t[0])/2,(e[1]+t[1])/2],l=r(a[0],a[1]),c=r(a[0],a[3]);return[{x:l[0],y:l[1],text:this._fmtLen([0,0],[n,0])},{x:c[0],y:c[1],text:this._fmtLen([0,0],[0,o])}]}_renderFurnPalette(){const e=this._furnPalette,t=this._t(this._imperial?"gs.unit_ft":"gs.unit_m");return q`
e.stopPropagation()}> -
- ${this._t("furn.title")} - - -
-
- ${cd.map(t=>q` -
${this._t(`furn.group_${t}`)}
-
- ${ud(t).map(t=>q``)} -
`)} -
- ${e?q`
- - this._furnPalette={...e,w:this._furnFieldToCm(Number(t.target.value))}} /> - - this._furnPalette={...e,h:this._furnFieldToCm(Number(t.target.value))}} /> - ${this._t("furn.place_hint")} -
`:q`
${this._t("furn.pick_hint")}
`} -
`}get _bdBase(){const e=this._curSpaceCfg;return e?.plan_url?{...Rh(e.plan_aspect,Ov)}:null}get _bdRect(){const e=this._curSpaceCfg;return e?.plan_url?Fh(e,Ov):null}get _bdParams(){const e=this._curSpaceCfg,t=Number(e?.plan_x),i=Number(e?.plan_y),s=Number(e?.plan_scale),n=Number(e?.plan_scale_x),o=Number(e?.plan_scale_y),a=Number.isFinite(s)&&s>0?s:1;return{dx:Number.isFinite(t)?t:0,dy:Number.isFinite(i)?i:0,sx:Number.isFinite(n)&&n>0?n:a,sy:Number.isFinite(o)&&o>0?o:a,angle:qr(e?.plan_angle)}}_openBackdropDialog(e){if(!this._bdMovable||!this._bdRect)return;e?.preventDefault(),e?.stopPropagation(),this._bdDrag=null;const t=this._bdRect;this._backdropDialog={widthCm:Gr(t.w,this._cellCm,this._gridPitch),heightCm:Gr(t.h,this._cellCm,this._gridPitch),angle:this._angleField(t.angle)}}_saveBackdropDialog(){const e=this._backdropDialog,t=this._bdBase,i=this._bdRect;if(!e||!t||!i)return;const s=qr(e.angle),n=this._geometrySnapshot(),o=Math.min(t.w*zh,Math.max(t.w*Ah,ri(Ur(e.widthCm,this._cellCm,this._gridPitch),this._gridPitch))),a=Math.min(t.h*zh,Math.max(t.h*Ah,ri(Ur(e.heightCm,this._cellCm,this._gridPitch),this._gridPitch))),r=Wr({x:i.x+i.w/2-o/2,y:i.y+i.h/2-a/2},s,e=>this._snap(e));this._bdApply((r[0]-t.x)/Ov,(r[1]-t.y)/Ov,o/t.w,a/t.h,s),this._backdropDialog=null,this._recordGeometry(this._t("history.backdrop_transform"),n),this._saveConfig()}get _bdActive(){return"decor"===this._mode&&!!this._bdRect&&"backdrop"===this._decorTool}get _bdMovable(){return"decor"===this._mode&&"backdrop"===this._decorTool&&!!this._bdRect}_bdApply(e,t,i,s,n){const o=this._curSpaceCfg;if(!o)return;o.plan_x=Number(Gh(e).toFixed(6)),o.plan_y=Number(Gh(t).toFixed(6)),delete o.plan_scale,o.plan_scale_x=Number(Math.min(zh,Math.max(Ah,i)).toFixed(6)),o.plan_scale_y=Number(Math.min(zh,Math.max(Ah,s)).toFixed(6));const a=qr(n);a?o.plan_angle=Number(a.toFixed(2)):delete o.plan_angle,this._cfgEpoch++,this.requestUpdate()}_bdStart(e,t,i=!1){const s=this._bdBase,n=this._bdRect;if(!s||!n)return!1;const o=this._svgPoint(e),a=t?t[0]:0,r=t?t[1]:0,l=a>0?n.x:n.x+n.w,c=r>0?n.y:n.y+n.h;return this._bdDrag={kind:i?"rotate":t?"scale":"move",pid:e.pointerId,sx:o[0],sy:o[1],base:s,p0:this._bdParams,fx:l,fy:c,sgx:a,sgy:r,rect0:{x:n.x,y:n.y,w:n.w,h:n.h,angle:n.angle},before:this._geometrySnapshot(),moved:!1},Gv(e),!0}_bdMove(e){const t=this._bdDrag;if(!t)return;const i=this._svgPoint(e),s=t.base;if("move"===t.kind){const e=t.rect0.x,n=t.rect0.y,o=this._snap([e+(i[0]-t.sx),n+(i[1]-t.sy)]),a=Math.abs(o[0]-e)>1e-9||Math.abs(o[1]-n)>1e-9;if(!a&&!t.moved)return;return t.moved||=a,void this._bdApply((o[0]-s.x)/Ov,(o[1]-s.y)/Ov,t.p0.sx,t.p0.sy,t.p0.angle)}if("rotate"===t.kind){const s=t.rect0.x+t.rect0.w/2,n=t.rect0.y+t.rect0.h/2,o=180*Math.atan2(t.sy-n,t.sx-s)/Math.PI;let a=t.p0.angle+(180*Math.atan2(i[1]-n,i[0]-s)/Math.PI-o);e.shiftKey||(a=5*Math.round(a/5)),a=qr(a);const r=Math.abs(a-t.p0.angle)>1e-9;if(!r&&!t.moved)return;return t.moved||=r,void this._bdApply(t.p0.dx,t.p0.dy,t.p0.sx,t.p0.sy,a)}const n=Xr(t.rect0,t.sgx,t.sgy,i[0],i[1],!e.shiftKey,this._gridPitch,Math.min(s.w,s.h)*Ah),o=n.w/Math.max(1e-9,s.w),a=n.h/Math.max(1e-9,s.h),r=Math.abs(o-t.p0.sx)>1e-9||Math.abs(a-t.p0.sy)>1e-9||Math.abs(n.x-t.rect0.x)>1e-9||Math.abs(n.y-t.rect0.y)>1e-9;if(!r&&!t.moved)return;t.moved||=r;const l=Wr(n,t.p0.angle,e=>this._snap(e));this._bdApply((l[0]-s.x)/Ov,(l[1]-s.y)/Ov,o,a,t.p0.angle)}get _bdMoved(){if("decor"!==this._mode||!this._bdRect)return!1;const e=this._bdParams;return 0!==e.dx||0!==e.dy||1!==e.sx||1!==e.sy||0!==e.angle}_bdReset(){const e=this._curSpaceCfg;if(!e)return;const t=this._geometrySnapshot();delete e.plan_x,delete e.plan_y,delete e.plan_scale,delete e.plan_scale_x,delete e.plan_scale_y,delete e.plan_angle,this._bdDrag=null,this._recordGeometry(this._t("history.backdrop_transform"),t),this._saveConfig(),this._showToast(this._t("decor.backdrop_reset_done")),this.requestUpdate()}_bdUp(){const e=this._bdDrag;this._bdDrag=null,e?.moved&&(this._recordGeometry(this._t("history.backdrop_transform"),e.before),this._saveConfig()),this.requestUpdate()}get _bdLive(){if(!this._bdDrag)return null;const e=this._bdRect;return e?{x:e.x+e.w/2,y:e.y+e.h/2,text:`${this._fmtLen([0,0],[e.w,0])} × ${this._fmtLen([0,0],[0,e.h])}`}:null}_renderBackdropFrame(e){const t=this._bdRect;if(!this._bdActive||!t)return G;const i=.02*Math.max(e.w,e.h),s=i/4,n=t.x+t.w/2,o=t.y+t.h/2,a=qr(t.angle),r=2.2*i;return W` - - - {e.stopPropagation(),e.preventDefault(),this._bdStart(e,void 0,!0)}}> - - ${[[-1,-1,"nwse"],[1,-1,"nesw"],[1,1,"nwse"],[-1,1,"nesw"]].map(([e,n,o])=>{const a=e<0?t.x:t.x+t.w,r=n<0?t.y:t.y+t.h;return W`{t.stopPropagation(),t.preventDefault(),this._bdStart(t,[e,n])}}>`})} - `}_renderTextFrame(e){const t=this._dtSel,i=this._dtBox;if(!t||!i||i.id!==t.id)return G;const s=.018*Math.max(e.w,e.h),n=s/4;if("line"===t.kind){const e=[t.x1*Ov,t.y1*this._decorH],i=[t.x2*Ov,t.y2*this._decorH];return W` - - ${[e,i].map((e,t)=>W`this._dtStart(e,"scale",void 0,t)}> - `)} - `}const[o,a]=this._dtPivot(t),r=Number(t.angle)||0,l=2.2*s;return W` - - - this._dtStart(e,"rotate")}> - - ${[[-1,-1,"nwse"],[1,-1,"nesw"],[1,1,"nwse"],[-1,1,"nesw"]].map(([e,t,o])=>W`this._dtStart(i,"scale",[e,t])}>`)} - `}_renderDecorLayer(){const e=Ov,t=this._decorH,i="decor"===this._mode,s=i&&"erase"===this._decorTool,n=this._decorList.map(n=>{const o="dshape"+(i&&this._decorSel===n.id?" dsel":""),a=this._decorResolvedStyle(n),r=this._decorWidthUnits(n),l=e=>this._decorShapeDown(e,n),c=e=>this._decorShapeDbl(e,n);if("line"===n.kind)return W` - ${i&&"select"===this._decorTool?W``:G} - ${s?W``:G}`;if("rect"===n.kind){const i=(n.x+n.w/2)*e,h=(n.y+n.h/2)*t,d=qr(n.angle);return W` - ${s?W``:G}`}if("ellipse"===n.kind){const i=(n.x+n.w/2)*e,h=(n.y+n.h/2)*t,d=qr(n.angle);return W` - ${s?W``:G}`}if("furniture"===n.kind){const i=n.w*e,h=n.h*t,d=bd(n.symbol,i,h);if(!d)return G;const p=Number(n.angle)||0,u=n.x*e+i/2,_=n.y*t+h/2,m=`${p?`rotate(${p} ${u} ${_}) `:""}translate(${n.x*e} ${n.y*t})`;return W` - ${s?W``:G}`}if("text"===n.kind){const i=this._decorTextUnits(n),s=this._renderDeviceSnapshot?.facts.get(`decor:${this._space}:${n.id}`),r=function(e){return String(e??"").replace(/\r\n?/g,"\n").split("\n")}("string"==typeof s?s:es(n.text,n,this._renderPlanHass,e=>this._renderEntityAvailable(e))),h=n.x*e,d=n.y*t,p=Number(n.angle)||0,u=d-(r.length-1)*i*1.2/2;return W`${r.map((e,t)=>W`${e}`)}`}return G});let o=G;const a=this._decorDraft;if(a){const e=this._decorStyle,t=Ur(e.widthCm,this._cellCm,this._gridPitch);if("line"===a.kind)o=W``;else{const i=Math.min(a.a[0],a.b[0]),s=Math.min(a.a[1],a.b[1]),n=Math.abs(a.b[0]-a.a[0]),r=Math.abs(a.b[1]-a.a[1]);o="rect"===a.kind?W``:W``}}return W`${n}${o}`}get _editorToolbarGroups(){return"plan"!==this._mode?[]:[{id:"opening",label:this._t("markup.opening"),icon:"mdi:door",activeItemId:"opening"===this._tool?this._openingPreset?.type:void 0,items:[{id:"window",label:this._t("opening.window"),icon:"mdi:window-closed-variant",role:"tool",invoke:()=>this._activateOpeningPlacement("window")},{id:"door",label:this._t("opening.door"),icon:"mdi:door-open",role:"tool",invoke:()=>this._activateOpeningPlacement("door")},{id:"passage",label:this._t("opening.passage"),icon:"mdi:arch",role:"tool",invoke:()=>this._activateOpeningPlacement("passage")},{id:"gate",label:this._t("opening.gate"),icon:"mdi:gate",role:"tool",invoke:()=>this._activateOpeningPlacement("gate")}]}]}_renderEditorGroupLauncher(e){return this._editorSecondary.renderGroupLauncher(e,this._editorToolbarGroups,this._editorSecondaryCopy)}get _editorSecondaryContextId(){const e=`editor:${this._mode}:${this._space}:${this._cfgEpoch}`,t=this._editorSecondary.activeGroup(this._editorToolbarGroups);if(t)return`${e}:group:${t.id}:${this._editorSecondary.groupGeneration}`;if("plan"===this._mode){const t=this._physicalSel;return t?`${e}:selection:${t.kind}:${t.id}:${t.segment??""}`:`${e}:tool:${this._tool}:${this._path.length}`}return"decor"===this._mode?"furniture"===this._decorTool?`${e}:palette:furniture:${this._furnPalette?.symbol||"none"}`:"select"===this._decorTool&&this._decorSel?`${e}:selection:decor:${this._decorSel}`:`${e}:tool:${this._decorTool}:${this._bdMoved?1:0}:${this._bdDrag?1:0}`:`${e}:none`}_runEditorContext(e,t){this._editorSecondary.runContext(e,this._editorSecondaryContextId,t)}_renderEditorGroupModel(e){return this._editorSecondary.renderGroupModel(e,this._editorSecondaryContextId,this._editorSecondaryCopy)}_renderDrawWallControl(){const e="column"===this._tool?1:0;return q``}_renderPlanSecondary(){const e=this._editorSecondaryContextId,t=this._physicalSel;if(t){if(!("partition"===t.kind?!!this._curSpaceCfg.partitions?.some(e=>e.id===t.id):"column"===t.kind?!!this._curSpaceCfg.wall_columns?.some(e=>e.id===t.id):!!this._curSpaceCfg.room_drafts?.some(e=>e.id===t.id)))return null;const i="partition"===t.kind?this._t("markup.partition"):"column"===t.kind?this._t("markup.column"):this._t("markup.add");return{contextId:e,kind:"selection",ariaLabel:this._t("editor.context_actions",{object:i}),visibleLabel:i,content:q` - - `}}const i="draw"===this._tool||"column"===this._tool,s="column"===this._tool?"markup.hint_column":"resize"===this._tool?"markup.hint_resize":"wallthick"===this._tool?"markup.hint_wallthick":null,n="draw"===this._tool?this._t(this._path.length?"markup.hint_points":"markup.hint_start",this._path.length?{n:this._path.length}:void 0):"";if(!i&&!s&&!n)return null;const o="draw"===this._tool&&this._path.length>0;return{contextId:e,kind:o?"operation":"tool",ariaLabel:this._t("editor.tool_options",{tool:this._t("draw"===this._tool?"markup.add":"column"===this._tool?"markup.column":"resize"===this._tool?"markup.resize":"markup.wallthick")}),content:q` - ${i?this._renderDrawWallControl():G} - ${n?q`${n}`:G} - ${s?q`${this._t(s)}`:G} - ${"draw"===this._tool&&this._path.length?q``:G}`}}_renderDecorSecondary(){const e=this._editorSecondaryContextId;if("furniture"===this._decorTool)return{contextId:e,kind:"palette",ariaLabel:this._t("editor.palette",{tool:this._t("decor.furniture")}),launcherId:"furniture",dismissPolicy:this._furnPalette?"stay-open-on-canvas":"outside",dismiss:()=>{"furniture"===this._decorTool&&(this._furnPalette=null,this._decorTool="select",this.requestUpdate())},content:this._renderFurnPalette()};const t=this._dtSel;if(t){const i=this._t(`decor.${t.kind}`);return{contextId:e,kind:"selection",ariaLabel:this._t("editor.context_actions",{object:i}),visibleLabel:i,content:q` - - `}}const i="line"===this._decorTool||"rect"===this._decorTool||"ellipse"===this._decorTool,s="rect"===this._decorTool||"ellipse"===this._decorTool,n="backdrop"===this._decorTool&&!!this._bdRect;return i||n?{contextId:e,kind:"tool",ariaLabel:this._t("editor.tool_options",{tool:this._t(i?`decor.${this._decorTool}`:"decor.backdrop")}),content:q` - ${i?q` - this._decorStyle={...this._decorStyle,...e.detail}}> - - ${s?q` - this._decorStyle={...this._decorStyle,fillColor:e.detail.color,fillOpacity:e.detail.opacity}}>`:G} - `:G} - ${n?q`${this._t("decor.backdrop_hint")}`:G}`}:null}_withBackdropReset(e){if(!this._bdMoved||this._bdDrag||"palette"===e?.kind)return e;const t=this._editorSecondaryContextId,i=q``;return e?{...e,kind:"selection"===e.kind?"mixed":e.kind,content:q`${e.content}${i}`}:{contextId:t,kind:"tool",ariaLabel:this._t("editor.tool_options",{tool:this._t("decor.backdrop")}),content:i}}get _editorSecondaryDialogBlocked(){return!!(this._tapConfirm||this._vacCalConfirm||this._roomDialog||this._mergeDialog||this._openingDialog||this._physicalDialog||this._openingInfo||this._decorTextDialog||this._decorShapeDialog||this._backdropDialog||this._decorEraseConfirm||this._spaceDialog||this._markerDialog||this._deviceInbox||this._infoCard||this._rulesDialog||this._settingsDialog||this._alignDialog||this._importDialog||this._kioskDialog||this._backupExportDialog||this._backupImportDialog||this._wallDialog)}_renderEditorSecondary(){if(!this._editing)return G;const e=this._editorSecondary.activeGroup(this._editorToolbarGroups),t=e?this._renderEditorGroupModel(e):"plan"===this._mode?this._renderPlanSecondary():"decor"===this._mode?this._withBackdropReset(this._renderDecorSecondary()):null;return this._editorSecondary.render(t,this._editorSecondaryDialogBlocked)}_renderDecorBar(){const e=[["select","mdi:cursor-default-outline","decor.select"],...this._bdRect?[["backdrop","mdi:image-move","decor.backdrop"]]:[],["line","mdi:vector-line","decor.line"],["rect","mdi:rectangle-outline","decor.rect"],["ellipse","mdi:ellipse-outline","decor.ellipse"],["text","mdi:format-text","decor.text"],["furniture","mdi:sofa-outline","decor.furniture"],["erase","mdi:eraser","decor.erase"]],t=this._geometryHistory.undoName,i=this._geometryHistory.redoName;return q`
-
- ${e.map(([e,t,i])=>q``)} - ${this._editorToolbarGroups.map(e=>this._renderEditorGroupLauncher(e))} - - -
-
- -
-
`}_renderDecorEraseConfirm(){const e=this._decorEraseConfirm,t=this._t(`decor.${e.kind}`);return q`this._decorEraseConfirm=null}> -

${this._t("confirm.erase_decor",{kind:t})}

-
- - - -
-
`}_renderDecorTextDialog(){const e=this._decorTextDialog,t=(e.pickerEntity||"").trim(),i=t?this.hass?.states?.[t]:null;return q`this._decorTextDialog=null}> -
- - ${""} - - this._decorTextDialog={...e,...t.detail}}> - -
this._decorTextDialog={...e,sizeCm:this._decorTextCm(Number(t.target.value))}} /> - ${this._t(this._imperial?"wallthick.unit_in":"wallthick.unit_cm")}
- - this._decorTextDialog={...e,angle:t.target.value}} /> - - - this._decorTextDialog={...e,pickerEntity:t.target.value}} /> - - ${Object.keys(this.hass?.states||{}).map(e=>q``)} - - ${t?q` - - - `:G} -
-
- - - -
-
`}_renderDecorShapeDialog(){const e=this._decorShapeDialog,t="rect"===e.kind||"ellipse"===e.kind,i=this._t("decor."+e.kind),s=this._t(this._imperial?"gs.unit_ft":"gs.unit_m");return q`this._decorShapeDialog=null}> -
- ${"furniture"===e.kind?q` - - `:G} - this._decorShapeDialog={...e,...t.detail}}> - -
this._decorShapeDialog={...e,widthCm:this._decorSmallCm(Number(t.target.value))}} />${this._t(this._imperial?"wallthick.unit_in":"wallthick.unit_cm")}
- ${"line"===e.kind?q` - -
- - -
- -
this._decorShapeDialog={...e,lengthCm:this._decorLargeCm(Number(t.target.value))}} /> - ${s}
`:q` - -
this._decorShapeDialog={...e,sizeWCm:this._decorLargeCm(Number(t.target.value))}} /> - ×this._decorShapeDialog={...e,sizeHCm:this._decorLargeCm(Number(t.target.value))}} /> - ${s}
`} - - this._decorShapeDialog={...e,angle:t.target.value}} /> - ${t?q` - this._decorShapeDialog={...e,fillColor:t.detail.color,fillOpacity:t.detail.opacity}}>`:G} -
-
- - - -
-
`}_renderBackdropDialog(){const e=this._backdropDialog,t=this._t(this._imperial?"gs.unit_ft":"gs.unit_m");return q`this._backdropDialog=null}> -
- -
this._backdropDialog={...e,widthCm:this._decorLargeCm(Number(t.target.value))}} /> - ×this._backdropDialog={...e,heightCm:this._decorLargeCm(Number(t.target.value))}} /> - ${t}
- - this._backdropDialog={...e,angle:t.target.value}} /> -
-
- - -
-
`}_cssPxToRender(e){const t=this._stageEl,i=this._viewOr(this._baseVb());return t?.clientWidth&&t.clientHeight?Math.max(i.w/t.clientWidth,i.h/t.clientHeight)*e:this._gridPitch/8*e}_renderZeroWalls(e){if(e&&!e.showBorders&&!this._editing)return W``;const t=this._zeroWalls();if(!t.lines.length)return W``;const i=e?.color||"var(--hp-muted)";return W` - ${t.lines.map(e=>W``)} - `}_zeroWalls(){const e=this._spaceModel();return e?X_(this._curSpaceCfg,e,Ov,.02*this._gridPitch):{style:V_(this._curSpaceCfg),lines:[],contour:[],barriers:[],transmissive:[]}}_openCuts(){return this._zeroWalls().contour}_deleteRoomClick(e){const t=this._spaceModel();if(!t)return;const i=[...t.rooms].reverse().find(t=>this._pointInRoom(e,t));i?i.id&&(this._roomDeleteDialog={roomId:i.id,name:i.name}):this._showToast(this._t("toast.delete_room_pick"))}get _wallKeyPitch(){return jh}get _spaceWalls(){const e=this._curSpaceCfg?.walls;return Array.isArray(e)?e:[]}_cfgOpenCuts(){const e=this._curSpaceCfg,t=this._spaceModel();if(!e||!t)return[];const i=K_(e,1),s=J_(e,t.rooms,Ov,.02*this._gridPitch).map(e=>e.map(e=>e/Ov)),n=new Set;return[...i,...s].filter(e=>{const t=e.map(e=>Number(e).toFixed(9)).join(","),i=[e[2],e[3],e[0],e[1]].map(e=>Number(e).toFixed(9)).join(","),s=t[e[0]/Ov,e[1]/Ov,e[2]/Ov,e[3]/Ov]))}_paperShapes(e){if(!this._spaceWalls.length)return di(e);const t=this._wallUnionGeometry();return t?.paperD?[{path:t.paperD}]:di(e)}_wallUnionGeometry(){const e=this._spaceModel();if(!e)return null;const t=this._spaceWalls,i=this._physicalBodiesR();if(!t.length&&!i.length)return null;const s=`${this._space}|${this._cfgEpoch}|${e.rooms.length}`;if(!this._wallUnionCache||this._wallUnionCache.key!==s){const n=Sv(this._wallUnionPool,s);if(n.hit)this._wallUnionCache=n.value;else{const n=this._openCuts(),o=this._roomWallOpeningInputs(),a=Wc(e.rooms,t,n,o,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov,i);a&&Object.defineProperty(a,"sourceFingerprint",{value:Jd([this._curSpaceCfg,this._cellCm,this._gridPitch]),enumerable:!1});const r={key:s,value:a};Mv(this._wallUnionPool,s,r,8),this._wallUnionCache=r}}return this._wallUnionCache.value}_thickWallCuts(){const e=this._spaceModel();if(!e)return[];const t=this._spaceWalls;if(!t.length)return[];const i=this._openCuts();return Gc(e.rooms,t,i,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov).map(e=>[e.a[0],e.a[1],e.b[0],e.b[1]])}_innerRoomContour(e,t,i=this._openCuts(),s=this._wallUnionGeometry()?.roomGeom,n=this._wallUnionGeometry()?.multiWallNodes){const o=i.map(e=>e.join(",")).join(";"),a=`${e.id}|${this._cfgEpoch}|${t}|${o}`,r=Sv(this._innerContourCache,a);if(r.hit)return r.value;const l=Cc(e.rooms,t,this._spaceWalls,i,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov,s,n);return Mv(this._innerContourCache,a,l,600),l}_wallThickHit(e){const t=this._spaceModel();if(!t)return null;const i=6*this._gridPitch,s=this._openCuts();let n=null;const o=(e,t,s)=>{t>i||(!n||t0?e:0,n=Number.isFinite(t)&&t>0?t:5,o=Number.isFinite(i)&&i>0?i:0;return s>0?Ml(s,n,o)/2:Dh(1.5*o,n)}(e.cm,this._cellCm,this._gridPitch);let i="";for(const s of e.segs)i+=(i?" ":"")+tc([[s[0],s[1]],[s[2],s[3]]],t,!1);return{segs:e.segs,open:e.open,d:i}}_wallThickClick(e){const t=this._wallThickHit(e);if(!t)return void this._showToast(this._t("toast.wallthick_pick"));const i=t.cm,s=this._viewOr(this._baseVb()),n=(t.a[0]+t.b[0])/2,o=(t.a[1]+t.b[1])/2;this._wallDialog={a:t.a,b:t.b,value:Sl(i,this._imperial),roomId:t.roomId,source:t.source,sx:(n-s.x)/s.w*100,sy:(o-s.y)/s.h*100}}_wallThickApply(e){const t=this._wallDialog;if(!t)return;const i=this._curSpaceCfg,s=this._spaceModel();if(!i||!s)return;const n=t.value.trim(),o=n?$v(n):null;if(null==o)return void this._showPhysicalRange(100);const a=this._imperial?2.54*o:o;if(!Number.isFinite(a)||a<0||a>100)return void this._showPhysicalRange(100);if("room"!==t.source.kind){const e=this._geometrySnapshot();if("partition"===t.source.kind){const e=(i.partitions||[]).find(e=>e.id===t.source.id);if(!e)return;if(0===a&&Z_(i.openings,{kind:"partition",id:e.id}))return void this._showToast(this._t("toast.zero_wall_opening_conflict"));e.cm=a}else{const e=(i.room_drafts||[]).find(e=>e.id===t.source.id),s=e?.segments?.[t.source.segment];if(!s)return;s.cm=a}this._wallDialog=null;return this._commitPhysicalGeometry(this._t("history.wall_thickness"),e)&&this._showToast(this._t("toast.wallthick_set")),void this.requestUpdate()}const r=this._geometrySnapshot(),l=a>0?a:null;if(0===a){const s=.02*this._gridPitch,n=[t.a[0],t.a[1],t.b[0],t.b[1]],o=(i.rooms||[]).find(e=>e.id===t.roomId),a=new Set(e&&o&&Array.isArray(o.wall_ids)?o.wall_ids:(i.wall_segments||[]).filter(e=>{const t=[Number(e.a?.[0])*Ov,Number(e.a?.[1])*Ov],i=[Number(e.b?.[0])*Ov,Number(e.b?.[1])*Ov];return Hs(t,n)<=s&&Hs(i,n)<=s}).map(e=>String(e.id))),r=(i.openings||[]).some(e=>{if("partition"===e.host?.kind)return!1;if("wall"===e.host?.kind&&a.has(e.host.id))return!0;if(e.host)return!1;const t=[Number(e.x)*Ov,Number(e.y)*Ov];return t.every(Number.isFinite)&&Hs(t,n)<=s});if(r)return void this._showToast(this._t("toast.zero_wall_opening_conflict"))}let c,h=this._openCuts();if(a>0&&h.length){const n=[];if(e&&t.roomId){const e=(i.rooms||[]).find(e=>e.id===t.roomId),o=new Set(Array.isArray(e?.wall_ids)?e.wall_ids:[]);for(const e of i.wall_segments||[])o.has(e.id)&&n.push([Number(e.a?.[0])*Ov,Number(e.a?.[1])*Ov,Number(e.b?.[0])*Ov,Number(e.b?.[1])*Ov]);if(!n.length){const e=s.rooms.find(e=>e.id===t.roomId),i=e?hi(e):null;for(let e=0;i&&e!n.some(t=>Hs([e[0],e[1]],t)<=o&&Hs([e[2],e[3]],t)<=o&&Hs([t[0],t[1]],e)<=o&&Hs([t[2],t[3]],e)<=o))}c=e&&t.roomId?Gl(i.walls,s.rooms,t.roomId,l,this._wallKeyPitch,h,Ov):Ul(i.walls,t.a,t.b,l,this._wallKeyPitch,Ov),c=this._normalizeWalls(c,h);for(const e of i.wall_segments||[])e.cm=Hl(c,e.a,e.b,jh,1);c.length?i.walls=c:delete i.walls,this._wallDialog=null,this._commitPhysicalGeometry(this._t("history.wall_thickness"),r)&&this._showToast(this._t(0===a?"toast.wallthick_cleared":"toast.wallthick_set")),this.requestUpdate()}_wallHatchDefs(e){if(!this._spaceWalls.length&&!this._physicalBodiesR().length&&!this._markup)return W``;const t=kl(this._cellCm);return W` - - - - `}_resolvedRoomFills(e,t){const i=new Map,s=new Map;for(const n of e.rooms){const e=this._roomDialog&&n.id===this._roomEditId?this._roomFill||t.fill:zs(t.fill,n),o=this._roomDialog&&n.id===this._roomEditId?this._roomCustomFill||t.customFill:ls(t.customFill,n),a=_s(e,"lqi"===e&&n.area?this._roomLqi(n.area):null,"light"===e?uo(ho(this._renderPlanHass,this._renderDevices,n,this._virtualLights)):"none","temp"===e?this._roomTemp(n):null,t.tempMin,t.tempMax,this._fillColors,o);i.set(n,a),n.id&&s.set(n.id,a)}return{byRoom:i,byId:s}}_spaceDisplayForRender(){const e=cs(this._curSpaceCfg),t=this._spaceDialog;return t&&"edit"===t.mode&&t.spaceId===this._space?{...e,showBorders:t.showBorders,showNames:t.showNames,hideDecor:t.hideDecor,hideOpenings:t.hideOpenings,color:t.roomColor,opacity:t.roomOpacity,fill:t.fillMode,customFill:t.customFill?rs(t.customFill):as,glow:t.glowEnabled,tempMin:t.tempMin,tempMax:t.tempMax,showLqi:t.showLqi,cardFontScale:t.cardFontScale,labelTemp:t.labelTemp,labelHum:t.labelHum,labelLqi:t.labelLqi,labelLight:t.labelLight}:e}_openingWallIndexFor(e,t){const i=e.rooms.map(e=>`${e.id}:${e.poly?.map(e=>e.join(",")).join("/")||`${e.x},${e.y},${e.w},${e.h}`}`).join(";"),s=this._spaceWalls.map(e=>`${e.key}:${e.a?.join(",")||""}:${e.b?.join(",")||""}:${e.cm}`).join(";"),n=t.map(e=>e.join(",")).join(";"),o=[e.id,this._cfgEpoch,this._wallKeyPitch,this._cellCm,this._gridPitch,i,s,n].join("|");let a=this._openingWallIndexCache.get(o);return a||(a=Kc(e.rooms,this._spaceWalls,t,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov)),Mv(this._openingWallIndexCache,o,a,4),{key:o,value:a}}_renderOpeningTunnelFills(e,t,i="data"){if(this._markup||!this._spaceWalls.length||!this._openingsR.length)return W``;if("glow-base"===i&&![...t.byRoom.values()].some(Boolean))return W``;const s=this._openCuts(),n=this._openingsR.map(e=>({x:e.rx,y:e.ry,angle:e.angle,length:e.rlen})),o=n.map(e=>`${e.x},${e.y},${e.angle},${e.length}`).join(";"),a=this._openingWallIndexFor(e,s),r=`${a.key}|${o}`;return this._openingTunnelCache&&this._openingTunnelCache.key===r||(this._openingTunnelCache={key:r,value:th(a.value,n)}),j_({openings:this._openingsR,geometries:this._openingTunnelCache.value,fillsByRoomId:t.byId,idPrefix:`${e.id}-${i}`,groupClass:"data"===i?"opening-tunnels":"opening-tunnels glow-base-tunnels",dataLayer:i})}_resolvedGlowBase(e,t,i){const s=new Map,n=new Map,o=this._fillColors.glow_base;for(const a of e.rooms){const e=i.byRoom.get(a),r=Fs(t.glow,a)&&(!e||e.opacity<=0)?{color:o.c,opacity:o.a,mode:"glow"}:null;s.set(a,r),a.id&&n.set(a.id,r)}return{byRoom:s,byId:n}}_renderGlowBaseRooms(e,t){if(this._markup||![...t.byRoom.values()].some(Boolean))return W``;const i=new Map(e.rooms.map(e=>[e,hi(e)])),s=this._openCuts(),n=this._wallUnionGeometry()?.roomGeom,o=e=>"M "+e.map(e=>e[0]+" "+e[1]).join(" L ")+" Z",a=e.rooms.map(a=>{const r=t.byRoom.get(a)||null,l=i.get(a)||null;if(!r||!l)return G;const c=this._spaceWalls.length&&a.id&&this._innerRoomContour(e,a.id,s,n)||l,h=Pi(c,e.rooms.filter(e=>e!==a).map(e=>i.get(e)).filter(e=>!!e)),d=this._cleanFloor(a,c,e).path;return d||h.length?W``:a.poly||c!==l?W``:W``});return W``}_renderWallBodies(e){if("iso"===this._renderProjection)return W``;if(e&&!e.showBorders&&("view"===this._mode||"devices"===this._mode))return W``;const t=this._wallUnionGeometry();if(!t)return W``;const i=this._stageEl,s=this._viewOr(this._baseVb()),n=i&&i.clientWidth&&s.w?i.clientWidth/s.w:1,o=e?.color||"#607d8b",a=wl(t.depthUnits,n)||xl(kl(this._cellCm),n),r=this._fillColors.wall_fill;return W` - ${t.paths.map(e=>W` - - `)} - `}_isoDecorationLayers(e){const t="undefined"==typeof CSS||"function"!=typeof CSS.supports||CSS.supports("filter","blur(1px)"),i="function"==typeof matchMedia&&matchMedia("(forced-colors: active)").matches;return function(e){const t=!!e.showBorders;return{structural:t,panels:t&&!e.hideOpenings,shadows:t&&e.filtersSupported&&!e.forcedColors,materialNuance:t&&!e.forcedColors,floorSymbols:!e.hideOpenings&&!t}}({showBorders:e.showBorders,hideOpenings:e.hideOpenings,filtersSupported:t,forcedColors:i})}_isoOpeningPanels(e){if(!e.panels)return[];const t=this._isoScene();if(!t)return[];return t.openings.flatMap(e=>{const t=this._openingsR[e.sourceIndex];return t?Kf(e,this._openingAmt(t)):[]}).sort((e,t)=>e.depth-t.depth||e.sourceIndex-t.sourceIndex||e.leaf-t.leaf)}_renderIsoDefs(e,t){const i=Ch(this._cellCm);return W` - ${"walls"===t&&e.materialNuance?W` - - - - - - `:G} - ${"underlay"===t&&e.shadows?W` - - - `:G} - ${"shadows"===t&&e.shadows?W` - - - - - - `:G} - `}_renderIsoUnderlay(e){if(!e.structural)return W``;const t=this._isoScene()?.floor;return t?W``:W``}_renderIsoShadows(e,t){if(!e.shadows)return W``;const i=this._isoScene()?.geometry;return i?W``:W``}_renderIsoWalls(e,t){if("iso"!==this._renderProjection)return W``;if(!e.structural)return W``;const i=this._isoScene();return i?W` - ${this._renderIsoDefs(e,"walls")} - ${i.geometry.sides.map(e=>W``)} - - ${e.panels?W``:G} - `:W``}_roomHoverPaths(e){const t=this._hoverRoom;if("view"!==this._mode||!t||t.space!==e.id)return null;const i=e.rooms.find(e=>e===t.room||!!e.id&&e.id===t.room.id);if(!i)return null;const s=hi(i);if(!s)return null;const n=e.rooms.filter(e=>e!==i).map(e=>({room:e,poly:hi(e)})).filter(e=>!!e.poly),o=Pi(s,n.map(e=>e.poly)),a=this._openCuts(),r=.02*this._gridPitch,l=a.filter(e=>{const t=[(e[0]+e[2])/2,(e[1]+e[3])/2];return s.some((e,i)=>Hs(t,[e[0],e[1],...s[(i+1)%s.length]])<=4*r)}),c=this._spaceWalls,h=this._wallUnionGeometry()?.roomGeom,d=c.length&&i.id&&this._innerRoomContour(e,i.id,a,h)||s,p=[{axis:s,face:d}];for(const t of o){const i=n.find(e=>e.poly===t)?.room;let s=t;if(c.length&&i?.id){const n=yc(e.rooms,i.id,c,a,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov);n&&(s=Vc(n.poly,n.offsets)||t)}p.push({axis:t,face:s})}const u=this._roomWallOpeningInputs(this._openingsR,e).map(e=>{const t=e.angle*Math.PI/180,i=Math.cos(t)*e.length/2,s=Math.sin(t)*e.length/2;return[e.x-i,e.y-s,e.x+i,e.y+s]}),_=l.concat(u),m=p.map(({axis:e,face:t})=>{const i=_.map(i=>((e,t,i)=>{const s=e[2]-e[0],n=e[3]-e[1],o=Math.hypot(s,n);if(o.05)&&Hs([c,h],[i[0],i[1],s[0],s[1]])<=4*r){d=!0;break}}if(!d)return null;let p=null;for(let e=0;e.05)continue;const u=Hs([c,h],[t[0],t[1],s[0],s[1]]);(!p||u!!e);return i.length?Ps(t,i,r).map(e=>`M ${e[0]} ${e[1]} L ${e[2]} ${e[3]}`).join(" "):`M ${t.map(e=>`${e[0]} ${e[1]}`).join(" L ")} Z`}).filter(Boolean).join(" ");if(!m)return null;const g=e=>`M ${e.map(e=>`${e[0]} ${e[1]}`).join(" L ")} Z`;return{fillD:[this._cleanFloor(i,d,e).path||g(d),...o.map(g)].join(" "),outlineD:m}}_renderRoomHoverFill(e){return e?W` - - `:W``}_renderRoomHoverOutline(e){return e?W` - - - `:W``}_renderWallThickUi(){const e=this._wallThickHover;return e&&e.d?W``:W``}_renderWallThickDialog(){const e=this._wallDialog;return e?q`
e.stopPropagation()}> -
- - {this._wallDialog={...e,value:t.target.value}}} - @keydown=${e=>{"Enter"===e.key&&(e.preventDefault(),this._wallThickApply(!1))}} /> - ${this._t(this._imperial?"wallthick.unit_in":"wallthick.unit_cm")} -
-
- ${"room"===e.source.kind?q``:G} - - -
-
`:q``}_openingAt(e){if(!this._openingsR.length)return null;const t=this._openingsR.flatMap(t=>{const i=t.angle*Math.PI/180,s=e[0]-t.rx,n=e[1]-t.ry,o=s*Math.cos(i)+n*Math.sin(i);return Math.abs(o)>t.rlen/2+Dh(12,this._cellCm)?[]:[{o:t,localY:-s*Math.sin(i)+n*Math.cos(i)}]});if(!t.length)return null;const i=this._spaceModel();if(!i)return null;const s=this._openCuts(),n=this._openingWallIndexFor(i,s).value;return t.find(({o:e,localY:t})=>{const i="gate"===e.type?!e.flip_v:e.flip_v,s=e.partitionHost||this._spaceWalls.length||"gate"===e.type?this._openingFace(e,n,!!i):{ox:0,oy:0,cm:0,side:-1},o=y_({type:e.type,length:e.rlen,angle:e.angle,amount:this._openingAmt(e),flipH:!!e.flip_h,flipV:!!e.flip_v,cellCm:this._cellCm,gridPitch:this._gridPitch,face:s});return Math.abs(t)<=o.hitHalf})?.o||null}_resolveOpeningPlacement(e){const t=this._openingPreset;if("opening"!==this._tool||!t)return null;const i=this._spaceModel();if(!i)return null;const s=this._openCuts(),n=this._openingWallIndexFor(i,s),o=`${n.key}|partitions:${this._cfgEpoch}`;this._openingPlacementIntervalsCache&&this._openingPlacementIntervalsCache.key===o||(this._openingPlacementIntervalsCache={key:o,value:[...wc(i.rooms,this._spaceWalls,s,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov),...hu(i.partitions,this._cellCm,this._gridPitch)]}),this._openingDimensionContextCache&&this._openingDimensionContextCache.key===o||(this._openingDimensionContextCache={key:o,value:cv({rooms:i.rooms,walls:this._spaceWalls,openCuts:s,partitions:i.partitions,roomOpenings:this._roomWallOpeningInputs(),partitionCuts:this._partitionOpeningCuts(i),pitch:this._wallKeyPitch,cellCm:this._cellCm,gridPitch:this._gridPitch,coordScale:Ov,epsilon:2e-4*this._gridPitch})});const a=ev({pointer:[e[0],e[1]],preset:t,geometryRevision:this._cfgEpoch,renderedLength:this._cmToUnits(t.lengthCm),intervals:this._openingPlacementIntervalsCache.value,baseTolerance:1.5*this._gridPitch,bodyPointerPadding:this._cssPxToRender("touch"===this._pointerModality.modality||"pen"===this._pointerModality.modality?10:6),gridStep:this._gridPitch});this._openingJambBlockCm=a.jambBlockedTarget?a.jambBlockedTarget.physicalHalfWidth/this._gridPitch*this._cellCm:null;const r=a.candidate;if(!r)return null;const l="gate"===r.type?!r.flipV:r.flipV;let c;if(r.host){const e=ou({id:"preview",type:r.type,x:r.x/Ov,y:r.y/Ov,angle:r.angle,length:r.renderedLength/Ov,host:r.host},i.partitions,Ov,this._cellCm,this._gridPitch).resolved;c=e?cu(e,l):{ox:0,oy:0,cm:0,side:-1}}else c=Zc(n.value,{x:r.x,y:r.y,angle:r.angle,length:r.renderedLength,flip_v:l});const h="mi"===this.hass?.config?.unit_system?.length,d=gv(r,this._openingDimensionContextCache.value).map(e=>({x:e.label[0],y:e.label[1],text:li(e.distance/this._gridPitch*this._cellCm,h),dimension:e}));return{...r,face:c,measure:{labels:d,guide:r.measure.guide}}}_activateOpeningPlacement(e){this._activateMarkupTool("opening"),"opening"===this._tool&&(this._openingPreset=function(e,t){return{type:e,lengthCm:Jf(e),flipH:!1,flipV:!1,revision:t}}(e,++this._openingPresetRevision),this._openingHoverCandidate=null,this._cursorPt=null)}_clearOpeningPlacement(e){this._openingHoverCandidate=null,this._openingJambBlockCm=null,e&&(this._openingPreset=null,this._openingRebindId=null)}_openingClick(e){const t=this._spaceModel();if(!t)return;const i=this._openingRebindId?null:this._openingAt(e);if(i)return void this._editOpening(i);const s=this._openingPreset;if(!s)return;const n=this._openingHoverCandidate,o=n&&tv(n,[e[0],e[1]],s.revision,this._cfgEpoch)?n:this._resolveOpeningPlacement(e);if(!o){if(null!=this._openingJambBlockCm)return void this._showToast(this._t("opening.partition_jamb_margin",{distance:li(this._openingJambBlockCm,this._imperial)}));const i=1.5*this._gridPitch,s=ui(e,t.rooms,i);return s&&function(e,t,i,s,n){for(const o of s)if(!(Hs([e,t],o)>n)&&Bl([o[0],o[1]],[o[2],o[3]],i))return!0;return!1}(s.x,s.y,s.angle,this._openCuts(),i)?void this._showToast(this._t("toast.opening_on_zero_wall")):void this._showToast(this._t("toast.opening_no_wall"))}const a=this._openingRebindId?this._curSpaceCfg?.openings?.find(e=>e.id===this._openingRebindId):null;this._openingDialog={...a?{id:a.id,type:a.type,lengthCm:Math.round(a.length*Ov/this._gridPitch*this._cellCm),lengthTouched:!1,contact:a.contact||"",lock:a.lock||"",invert:!!a.invert,flipH:!!a.flip_h,flipV:!!a.flip_v}:{type:o.type,lengthCm:o.lengthCm,contact:"",lock:"",invert:!1,flipH:o.flipH,flipV:o.flipV},...o.host?{host:o.host}:{},x:o.x,y:o.y,angle:o.angle},this._openingRebindId=null,this._openingHoverCandidate=null,this._cursorPt=null}_editOpening(e){this._openingDialog={id:e.id,type:e.type,lengthCm:Math.round(e.rlen/this._gridPitch*this._cellCm),lengthTouched:!1,contact:e.contact||"",lock:e.lock||"",invert:!!e.invert,flipH:!!e.flip_h,flipV:!!e.flip_v,..."partition"===e.host?.kind?{host:{...e.host}}:{},x:e.rx,y:e.ry,angle:e.angle}}_opPointerDown(e,t){if("plan"===this._mode&&this._spaceModel()&&"resize"!==this._tool){e.preventDefault(),e.stopPropagation();try{Gv(e)}catch{}this._opDrag={id:t.id,moved:!1,sx:e.clientX,sy:e.clientY,dirty:!1,before:this._geometrySnapshot()}}}_opPointerMove(e,t){if(!this._opDrag||this._opDrag.id!==t.id)return;if(Math.abs(e.clientX-this._opDrag.sx)+Math.abs(e.clientY-this._opDrag.sy)<=3)return;const i=this._spaceModel();if(!i)return;const s=this._svgPoint(e),n=this._curSpaceCfg,o=n?.openings?.find(e=>e.id===t.id);if(!o)return;if("partition"===o.host?.kind){const e=i.partitions.find(e=>e.id===o.host.id);if(!e)return;const t=e.b[0]-e.a[0],n=e.b[1]-e.a[1],a=Math.hypot(t,n);if(!(a>1e-9))return;const r=t/a,l=n/a;if(Math.abs((s[0]-e.a[0])*l-(s[1]-e.a[1])*r)>4*this._gridPitch)return;const c=o.length*Ov/2+su(e,this._cellCm,this._gridPitch);let h=(s[0]-e.a[0])*r+(s[1]-e.a[1])*l;if(h=Math.round(h/this._gridPitch)*this._gridPitch,h=Math.max(c,Math.min(a-c,h)),a<2*c-1e-9)return;const d=h/a,p=(e.a[0]+r*h)/Ov,u=(e.a[1]+l*h)/this._spaceH;let _=180*Math.atan2(n,t)/Math.PI;_>=90?_-=180:_<-90&&(_+=180);const m=o.host.t!==d||o.x!==p||o.y!==u||o.angle!==_;return this._opDrag.moved=!0,m&&(this._opDrag.dirty=!0),o.host={...o.host,t:d},o.x=p,o.y=u,o.angle=_,this._opMeasure=null,m&&this._cfgEpoch++,void this.requestUpdate()}const a=ui(s,i.rooms,4*this._gridPitch);if(!a)return;this._opDrag.moved=!0;const r=this._opRuler(a,o.length*Ov);this._opMeasure=r.measure;const l=r.x/Ov,c=r.y/this._spaceH,h=o.x!==l||o.y!==c||o.angle!==a.angle;if(h&&(this._opDrag.dirty=!0),o.x=l,o.y=c,o.angle=a.angle,Number(this._serverCfg?.model_version||0)>=9){const e=Tg(o,n?.wall_segments||[]);e?o.host=e:delete o.host}h&&this._cfgEpoch++,this.requestUpdate()}_opRuler(e,t){const i=this._spaceModel()?.rooms||[],s=this._gridPitch/2;let n=e.x,o=e.y,a=_i([n,o],e.angle,t,i,s);if(a&&a.centered&&(n!==a.wallCenter[0]||o!==a.wallCenter[1]))[n,o]=a.wallCenter,a=_i([n,o],e.angle,t,i,s);else if(a){const[r,l]=a.wallA,[c,h]=a.wallB,d=c-r,p=h-l,u=Math.hypot(d,p);if(u>0){const c=this._gridPitch,h=Math.min(t/2,u/2);let _=Math.round(((n-r)*d+(o-l)*p)/u/c)*c;_=Math.max(h,Math.min(u-h,_)),n=r+_/u*d,o=l+_/u*p,a=_i([n,o],e.angle,t,i,s)||a}}if(!a)return{x:n,y:o,angle:e.angle,measure:null};const r="mi"===this.hass?.config?.unit_system?.length,l=(e,t)=>({x:t[0],y:t[1],text:li(e/this._gridPitch*this._cellCm,r)});return{x:n,y:o,angle:e.angle,measure:{labels:[l(a.sideA,a.midA),l(a.sideB,a.midB)],guide:a.centered?{x:a.wallCenter[0],y:a.wallCenter[1],angle:e.angle}:null}}}_opPointerUp(e,t){if(!this._opDrag||this._opDrag.id!==t.id)return;const i=this._opDrag,s=i.moved;this._opMeasure=null,s&&i.dirty&&this._commitPhysicalGeometry(this._t("history.move_opening"),i.before),s?window.setTimeout(()=>this._opDrag=null,0):this._opDrag=null}_opClick(e,t){"plan"===this._mode&&"resize"===this._tool||(e.stopPropagation(),this._opDrag?.moved||"plan"===this._mode&&this._editOpening(t))}_saveOpening(){const e=this._openingDialog,t=this._curSpaceCfg,i=this._spaceModel();if(!e||!t||!i)return;const s=this._geometrySnapshot(),n=this._spaceH,o=(t.openings||[]).find(t=>t.id===e.id),a={...o||{},id:e.id||"o"+Date.now().toString(36),type:e.type,x:e.x/Ov,y:e.y/n,angle:e.angle,length:o&&!e.lengthTouched?o.length:this._cmToUnits(Math.max(20,e.lengthCm))/Ov,...e.host?{host:{...e.host}}:{}};if("partition"===a.host?.kind){const e=ru(o,a)?au(a,i.partitions,Ov,this._cellCm,this._gridPitch):ou(a,i.partitions,Ov,this._cellCm,this._gridPitch);if(!e.resolved){const t=i.partitions.find(e=>e.id===a.host.id);if("does-not-fit-jamb"===e.reason&&t){const e=su(t,this._cellCm,this._gridPitch);this._showToast(this._t("opening.partition_jamb_margin",{distance:li(e/this._gridPitch*this._cellCm,this._imperial)}))}else this._showToast(this._t("opening.partition_orphan"));return}const s=(t.openings||[]).flatMap(e=>{if(!e.host||e.id===a.id)return[];const t=ou(e,i.partitions,Ov,this._cellCm,this._gridPitch).resolved;return t?[t]:[]});if(function(e,t,i=1e-9){const s=e.length/(2*e.axis.length),n=e.t-s,o=e.t+s;return t.some(t=>{if(t.host.id!==e.host.id||t.opening.id===e.opening.id)return!1;const s=t.length/(2*t.axis.length);return Math.max(n,t.t-s)=9){const e=Tg(a,t.wall_segments||[]);if(!e)return void this._showToast(this._t("toast.opening_no_wall"));a.host=e}else delete a.host;"passage"===e.type?(delete a.contact,delete a.lock,delete a.invert,delete a.flip_h,delete a.flip_v):(a.contact=e.contact||null,a.lock=("door"===e.type||"gate"===e.type)&&e.lock||null,a.invert=e.invert||void 0,a.flip_h="gate"!==e.type&&e.flipH||void 0,a.flip_v=e.flipV||void 0),t.openings=t.openings||[];const r=t.openings.findIndex(e=>e.id===a.id);r>=0?t.openings[r]=a:t.openings.push(a),this._commitPhysicalGeometry(this._t(e.id?"history.edit_opening":"history.add_opening"),s),this._openingDialog=null,this.requestUpdate()}_deleteOpening(){const e=this._openingDialog,t=this._curSpaceCfg;if(!e?.id||!t?.openings)return;const i=this._geometrySnapshot();t.openings=t.openings.filter(t=>t.id!==e.id),this._commitPhysicalGeometry(this._t("history.delete_opening"),i),this._openingDialog=null,this.requestUpdate()}_contactCandidates(){const e=[];for(const t of Object.keys(this.hass.states)){if(!this._openingEntityAvailable(t))continue;const i=t.split(".")[0];if("binary_sensor"!==i&&"cover"!==i)continue;const s=this.hass.states[t],n=["door","window","opening","garage_door","garage"].includes(s?.attributes?.device_class||"");("cover"!==i||n)&&e.push([t,s?.attributes?.friendly_name||t,n?0:1])}return e.sort((e,t)=>e[2]-t[2]||e[1].localeCompare(t[1])).map(([e,t])=>({value:e,label:t}))}_lockCandidates(){return Object.keys(this.hass.states).filter(e=>e.startsWith("lock.")&&this._openingEntityAvailable(e)).map(e=>({value:e,label:this.hass.states[e]?.attributes?.friendly_name||e})).sort((e,t)=>e.label.localeCompare(t.label))}_toggleOpeningEntityPicker(e){const t=this._openingDialog;if(!t)return;const i="contact"===e?!t.contactOpen:!t.lockOpen;this._openingDialog={...t,contactOpen:"contact"===e&&i,lockOpen:"lock"===e&&i}}_filterOpeningEntities(e,t){const i=this._openingDialog;i&&(this._openingDialog="contact"===e?{...i,contactFilter:t}:{...i,lockFilter:t})}_selectOpeningEntity(e,t){const i=this._openingDialog;i&&(this._openingDialog="contact"===e?{...i,contact:t,contactOpen:!1,contactFilter:""}:{...i,lock:t,lockOpen:!1,lockFilter:""})}_mergeClick(e){const t=this._spaceModel();if(!t)return;const i=t.rooms,s=[...i].reverse().find(t=>this._pointInRoom(e,t));if(!s?.id)return;const n=s.id;if(!this._mergeSel||this._mergeSel===n)return void(this._mergeSel=this._mergeSel===n?null:n);const o=i.find(e=>e.id===this._mergeSel),a=o?hi(o):null,r=hi(s),l=a&&r?function(e,t){if(!e||!t||e.length<3||t.length<3)return null;const i=si(Ii(e),Ii(t));if(1!==i.length)return null;if(1!==i[0].length)return null;const s=i[0][0].slice(0,-1).map(e=>[e[0],e[1]]);return s.length>=3?s:null}(a,r):null;if(!l)return this._showToast(this._t("toast.merge_not_adjacent")),void(this._mergeSel=null);this._mergeDialog={aId:this._mergeSel,bId:n,poly:l,pick:"a"},this._mergeSel=null}_commitMerge(){const e=this._mergeDialog,t=this._curSpaceCfg;if(!e||!t||!this._spaceModel())return;const i=this._geometrySnapshot(),s=this._spaceH,n="a"===e.pick?e.aId:e.bId,o="a"===e.pick?e.bId:e.aId,a=t.rooms.find(e=>e.id===n);if(!a)return void(this._mergeDialog=null);a.poly=e.poly.map(e=>[e[0]/Ov,e[1]/s]),delete a.x,delete a.y,delete a.w,delete a.h,t.rooms=t.rooms.filter(e=>e.id!==o);const r=this._commitPhysicalGeometry(this._t("history.merge_rooms"),i);this._mergeDialog=null,this._regSignature="",this._maybeRebuildDevices(),r&&this._showToast(this._t("toast.rooms_merged",{name:a.name||""}))}_splitClick(e){const t=this._spaceModel();if(!t)return;const i=t.rooms;if(!this._splitSel){const t=[...i].reverse().find(t=>this._pointInRoom(e,t));if(!t?.id)return;return void(this._splitSel={roomId:t.id,pts:[]})}const s=i.find(e=>e.id===this._splitSel.roomId),n=s?hi(s):null;if(!s||!n)return void(this._splitSel=null);const o=.02*this._gridPitch,a=6*this._gridPitch,r=function(e,t){if(!t||t.length<2)return null;let i=null,s=1/0;for(let n=0;n=n)continue;n=_;const m=Math.sqrt(p),g=(i>0?Math.max(0,Math.min(m,Math.round(u*m/i)*i)):u*m)/m;s=[a+g*h,r+g*d]}return s}(r,n,this._gridPitch)||r,c=r&&l&&Math.hypot(r[0]-e[0],r[1]-e[1])<=a?l:null,h=!!c&&yi(c,n,o),d=this._splitSel.pts;if(!d.length)return h?void(this._splitSel={...this._splitSel,pts:[c]}):void this._showToast(this._t("toast.split_pick_wall"));if(!h){const t=this._snap(e);return wi(t,n,o)?void(this._splitSel={...this._splitSel,pts:[...d,t]}):void this._showToast(this._t("toast.split_pick_inside"))}const p=zi(n,[...d,c],o);if(!p)return void this._showToast(this._t("toast.split_bad_cut"));this._resetRoomDialogFields();const[u,_]=p,m=Ti(u)>=Ti(_)?u:_,g=m===u?_:u;this._pendingSplit={roomId:s.id,mainPoly:m,newPoly:g},this._cursorPt=null,this._nameSel="",this._areaSel="",this._roomDialog=!0}get _contourClosed(){return this._path.length>=4&&this._samePt(this._path[0],this._path[this._path.length-1])&&(null!=this._closingWallCm||!this._activeDraftId)}_markupMove(e){if(!this._markup)return;if("column"===this._tool)return void(this._cursorPt=this._snap(this._svgPoint(e)));if("opening"===this._tool||"wallthick"===this._tool)return void(this._cursorPt=this._svgPoint(e));const t="draw"===this._tool&&!this._contourClosed,i="split"===this._tool&&!!this._splitSel?.pts?.length;if(!t&&!i)return;const s=this._svgPoint(e);if(t){const t=this._resolvePlanDrawPoint(s,e.shiftKey);return this._planSnapHover={contextKey:t.contextKey,candidate:t.candidate,conflicts:t.conflicts},this._syncPlanSnapConflictMarkers(t.conflicts),this._path.length?void(this._cursorPt=t.point):void this._syncPlanSnapActiveMarker(t.candidate)}this._cursorPt=this._snap(s)}get _openingPreview(){const e=this._openingPreset;if("opening"!==this._tool||!e||!this._cursorPt)return null;const t=this._cursorPt,i=this._openingHoverCandidate;if(i&&tv(i,[t[0],t[1]],e.revision,this._cfgEpoch))return i;if(this._openingAt(t))return this._openingHoverCandidate=null,null;const s=this._resolveOpeningPlacement(t);return this._openingHoverCandidate=s,s}get _opMeasureView(){return this._opMeasure||this._openingPreview?.measure||null}_saveRoom(){(this._areaSel||this._nameSel.trim())&&(this._wallFaceBatch?this._decideWallFace(!0):this._commitRoom())}_decideWallFace(e){const t=this._wallFaceBatch;if(!t)return;const i=t.candidates[t.index];if(!i)return;if(!e&&(i.existing||i.repair)&&1===t.candidates.length)return this._wallFaceBatch=null,this._roomDialog=!1,this._nameSel="",this._areaSel="",void this.requestUpdate();const s=e?{candidate:i,create:!0,name:this._nameSel.trim(),area:this._areaSel||null,settings:this._roomSettingsFromDialog()?JSON.parse(JSON.stringify(this._roomSettingsFromDialog())):null}:{candidate:i,create:!1},n=[...t.decisions,s],o=t.index+1;if(o"number"==typeof e&&Number.isFinite(e)&&e>=0?e:null,n=.02*this._gridPitch,o=this._spaceModel();if(!o)return vl;for(const t of this._planSnapGeometrySnapshot().value.segments){if("room"===t.sourceKind||Hs(e,[t.a[0],t.a[1],t.b[0],t.b[1]])>n)continue;if("partition"===t.sourceKind)return s(o.partitions.find(e=>e.id===t.sourceId)?.cm)??vl;const i=t.sourceId.lastIndexOf(":"),a=i>=0?t.sourceId.slice(0,i):t.sourceId,r=i>=0?Number(t.sourceId.slice(i+1)):-1;return s(o.room_drafts.find(e=>e.id===a)?.segments[r]?.cm)??vl}const a=Iu(Math.max(0,t.length-1),i,this._drawWallCm,vl);for(let i=0;i+1[...e]);if(!t?.sourceKey.startsWith("active:"))return i;const s=/:(\d+)$/.exec(t.sourceKey),n=s?Number(s[1])+("b"===t.endpoint?1:0):-1;return n>=0&&nt.key===e.sourceKey),o=s.find(t=>t.key===e.targetSourceKey);if(!n||!o)return!1;const a="a"===e.endpoint?n.a:n.b;return!(Math.hypot(a[0]-e.from[0],a[1]-e.from[1])>i)&&(!(Hs(e.to,[o.a[0],o.a[1],o.b[0],o.b[1]])>i)&&!function(e,t){if(!e.sourceKey.startsWith("static:partition|"))return!1;const i=e.sourceKey.slice(17).split("|")[0];return!!i&&t.some(e=>"partition"===e.host?.kind&&e.host.id===i)}(e,this._curSpaceCfg?.openings||[]))}_applyWallRepair(e,t){const i=this._curSpaceCfg;if(!i||!this._validateWallRepair(e,t.activePath))return!1;const s=t=>{t[0]=e.to[0]/Ov,t[1]=e.to[1]/Ov};if(e.sourceKey.startsWith("active:")){const n=/:(\d+)$/.exec(e.sourceKey),o=n?Number(n[1])+("b"===e.endpoint?1:0):-1;if(o<0||o>=t.activePath.length)return!1;t.activePath[o]=[...e.to];const a=(i.room_drafts||[]).find(e=>e.id===t.activeDraftId);return a?.points?.[o]&&s(a.points[o]),!0}if(!e.sourceKey.startsWith("static:"))return!1;const[n,o]=e.sourceKey.slice(7).split("|");if("partition"===n){const t=(i.partitions||[]).find(e=>e.id===o);if(!t)return!1;const n=[t.a[0]*Ov,t.a[1]*Ov],a=[t.b[0]*Ov,t.b[1]*Ov],r=Math.hypot(n[0]-e.from[0],n[1]-e.from[1])<=2e-4*this._gridPitch?t.a:Math.hypot(a[0]-e.from[0],a[1]-e.from[1])<=2e-4*this._gridPitch?t.b:null;return!!r&&(s(r),!0)}if("draft"===n){const t=o.lastIndexOf(":"),n=t>=0?o.slice(0,t):"",a=t>=0?Number(o.slice(t+1)):-1,r=(i.room_drafts||[]).find(e=>e.id===n);if(!r?.points?.[a]||!r?.points?.[a+1])return!1;const l=[a,a+1].find(t=>Math.hypot(r.points[t][0]*Ov-e.from[0],r.points[t][1]*Ov-e.from[1])<=2e-4*this._gridPitch);return null!=l&&(s(r.points[l]),!0)}return!1}_applyWallFaceBatch(){const e=this._wallFaceBatch,t=this._curSpaceCfg,i=this._spaceModel();if(!e||!t||!i)return;const s=(e,t)=>{this._showToast(this._t(e,t)),this._roomDialogCancel()},n=e.decisions.filter(e=>e.create),o=n.map(e=>e.candidate.ring),a=i.rooms;for(let e=0;ei.id!==t&&!!hi(i)&&Di(o[e],hi(i)));if(i)return void s("toast.room_overlap",{name:i.name||""})}if((t.rooms||[]).length+n.length>400)return void s("toast.physical_limit");if(n.some(e=>e.candidate.split&&!t.rooms.some(t=>t.id===e.candidate.split.roomId)))return void s("toast.contour_cannot_close");const r=n.map(e=>e.candidate.repair).filter(e=>!!e);if(r.length>1||r[0]&&!this._validateWallRepair(r[0],e.activePath))return void s("toast.wall_repair_changed");const l=this._activePathWithRepair(e.activePath,r[0]),c=2e-4*this._gridPitch,h=new Map;for(let t=0;te.candidate.atomKeys)),t=n.some(e=>e.candidate.consumeAllActive);for(const i of Qu(this._wallGraphSources(l),c)){const s=i.sourceKeys.find(e=>h.has(e));!s||t||e.has(i.key)||d.push({a:i.a,b:i.b,cm:h.get(s)})}}else d.push(...Ru(l,Iu(l.length-1,e.activeCms,this._drawWallCm,vl)));if((t.partitions||[]).length+d.length>jv)return void s("toast.physical_limit");const p=n.map(e=>e.candidate.ring.map(()=>"")),u=e.activeDraftId?(t.room_drafts||[]).find(t=>t.id===e.activeDraftId):null,_=this._draftSegmentsForPath(e.activePath,u,e.activeCms),m=(e,t)=>{const i=`${e[0].toFixed(9)},${e[1].toFixed(9)}`,s=`${t[0].toFixed(9)},${t[1].toFixed(9)}`;return i<=s?`${i}|${s}`:`${s}|${i}`};for(let t=0;t<_.length;t++){const i=_[t]?.id;if(!i)continue;const s=e.activePath[t],o=e.activePath[t+1],a=[s[0],s[1],o[0],o[1]],r=[];n.forEach((e,t)=>e.candidate.ring.forEach((i,s)=>{const n=e.candidate.ring[(s+1)%e.candidate.ring.length];Hs(i,a)<=c&&Hs(n,a)<=c&&r.push({kind:"room",owner:t,edge:s,a:i,b:n})})),d.forEach((e,t)=>{Hs(e.a,a)<=c&&Hs(e.b,a)<=c&&r.push({kind:"partition",owner:t,edge:0,a:e.a,b:e.b})});const l=[(s[0]+o[0])/2,(s[1]+o[1])/2];r.sort((e,t)=>{const i=[e.a[0],e.a[1],e.b[0],e.b[1]],n=[t.a[0],t.a[1],t.b[0],t.b[1]];return(Hs(l,i)<=c?0:1)-(Hs(l,n)<=c?0:1)||(Hs(s,i)<=c?0:1)-(Hs(s,n)<=c?0:1)||m(e.a,e.b).localeCompare(m(t.a,t.b))});const h=r[0];if(!h)continue;const u=m(h.a,h.b);for(const e of r)m(e.a,e.b)===u&&("room"===e.kind?p[e.owner][e.edge]=i:d[e.owner].id=i)}const g=this._geometrySnapshot();if(r[0]&&!this._applyWallRepair(r[0],e))return void s("toast.wall_repair_changed");const f=n.some(e=>!!e.candidate.split),v=f?kc(i.rooms,t.walls,this._openCuts(),this._wallKeyPitch,this._cellCm,this._gridPitch,Ov):null,b=f&&Array.isArray(t.walls)?t.walls:null;for(const e of n){const i=e.candidate.split;if(!i)continue;const n=t.rooms.find(e=>e.id===i.roomId);if(!n)return void s("toast.contour_cannot_close");n.poly=i.mainPoly.map(e=>[e[0]/Ov,e[1]/this._spaceH]),delete n.x,delete n.y,delete n.w,delete n.h}const y=Date.now().toString(36),w=[];if(n.forEach((e,i)=>{const s=e.area?this.hass.areas[e.area]?.name:"",n={id:`r${y}-${i}`,name:e.name||s||this._t("room.default_name"),area:e.area||null,poly:e.candidate.ring.map(e=>[e[0]/Ov,e[1]/this._spaceH]),...p[i].some(Boolean)?{wall_ids:p[i]}:{},...e.settings?{settings:JSON.parse(JSON.stringify(e.settings))}:{}};t.rooms.push(n),w.push({room:n,decision:e})}),e.activeDraftId&&Array.isArray(t.room_drafts)&&(t.room_drafts=t.room_drafts.filter(t=>t.id!==e.activeDraftId),t.room_drafts.length||delete t.room_drafts),d.length&&(t.partitions||=[],d.forEach((e,i)=>t.partitions.push({id:e.id||`partition-${y}-${i}`,a:[e.a[0]/Ov,e.a[1]/Ov],b:[e.b[0]/Ov,e.b[1]/Ov],cm:e.cm}))),f){const e=this._normalizeWalls(v,this._openCuts());e.length?t.walls=e:b?.length?t.walls=b:delete t.walls}if(w.length){this._cfgEpoch++;const i=this._openCuts(),s=this._spaceModel();if(!s)return;let n=t.walls;for(const{room:t,decision:o}of w){const a=o.candidate.ring,r=this._wallSourceCmAt(a[0],e.activePath,e.activeCms);n=Vl(n,s.rooms,t.id,r,this._wallKeyPitch,i,Ov);for(const o of wc(s.rooms,n,i,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov)){if(o.roomId!==t.id||"outer"!==o.kind)continue;const i=[(o.a[0]+o.b[0])/2,(o.a[1]+o.b[1])/2];n=Ul(n,o.a,o.b,this._wallSourceCmAt(i,e.activePath,e.activeCms),this._wallKeyPitch,Ov)}}const o=this._normalizeWalls(n,i);o.length?t.walls=o:delete t.walls}if(!this._commitPhysicalGeometry(this._t("history.wall_face_batch"),g))return;delete this._resumeDraftBySpace[this._space],this._path=[],this._activeDraftId=null,this._draftSegmentCms=[],this._closingWallCm=null,this._wallFaceBatch=null,this._roomDialog=!1,this._nameSel="",this._areaSel="",this._regSignature="",this._maybeRebuildDevices();const k=new Set(n.map(e=>e.area).filter(e=>!!e));if(k.size){const e={...this._layout};for(const t of this._devices){if(!k.has(t.area||"")||t.space!==this._space)continue;if(this._layout[t.id])continue;const i=this._defPos[t.id];i&&(e[t.id]={s:this._space,x:i.x/Ov,y:i.y/Ov},this._dirtyPos.add(t.id))}this._layout=e,this._persistLayout()}this._showToast(this._t(n.length?"toast.wall_rooms_saved":"toast.wall_chain_saved",{n:n.length}))}_commitRoom(){const e=this._curSpaceCfg,t=this._spaceModel();if(!e||!t)return;const i=this._geometrySnapshot(),s=this._spaceH,n=!!this._pendingSplit,o=n?kc(t.rooms,e.walls,this._openCuts(),this._wallKeyPitch,this._cellCm,this._gridPitch,Ov):null,a=n&&Array.isArray(e.walls)?e.walls:null;let r;if(this._pendingSplit){const t=e.rooms.find(e=>e.id===this._pendingSplit.roomId);if(!t)return this._pendingSplit=null,this._splitSel=null,void(this._roomDialog=!1);t.poly=this._pendingSplit.mainPoly.map(e=>[e[0]/Ov,e[1]/s]),delete t.x,delete t.y,delete t.w,delete t.h,r=this._pendingSplit.newPoly}else{if(!this._contourClosed)return;r=this._path.slice(0,-1)}const l=this._areaSel?this.hass.areas[this._areaSel]?.name:"",c={id:"r"+Date.now().toString(36),name:this._nameSel||l||this._t("room.default_name"),area:this._areaSel||null,poly:r.map(e=>[e[0]/Ov,e[1]/s]),...this._roomSettingsFromDialog()?{settings:this._roomSettingsFromDialog()}:{}};if(!n&&this._activeDraftId){const t=Array.isArray(e.room_drafts)?e.room_drafts.find(e=>e.id===this._activeDraftId):null,i=Iu(r.length,[...this._draftSegmentCms,this._closingWallCm??void 0],this._drawWallCm,vl),s=this._draftSegmentsForPath(this._path,t,i);s.length===r.length&&s.every(e=>!!e.id)&&(c.wall_ids=s.map(e=>e.id))}if(e.rooms.push(c),!n&&this._activeDraftId&&Array.isArray(e.room_drafts)&&(e.room_drafts=e.room_drafts.filter(e=>e.id!==this._activeDraftId),e.room_drafts.length||delete e.room_drafts),n){const t=this._normalizeWalls(o,this._openCuts());t.length?e.walls=t:a?.length?e.walls=a:delete e.walls}if(!n){const t=Iu(r.length,[...this._draftSegmentCms,this._closingWallCm??void 0],this._drawWallCm,vl),i=t[0];if(null!=i){this._cfgEpoch++;const s=this._openCuts(),n=this._spaceModel();if(!n)return;let o=Vl(e.walls,n.rooms,c.id,i,this._wallKeyPitch,s,Ov);for(const e of wc(n.rooms,o,s,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov)){if(e.roomId!==c.id||"outer"!==e.kind)continue;const i=[(e.a[0]+e.b[0])/2,(e.a[1]+e.b[1])/2],s=r.findIndex((e,t)=>{const s=r[(t+1)%r.length];return Hs(i,[e[0],e[1],s[0],s[1]])<=.02*this._gridPitch});s>=0&&(o=Ul(o,e.a,e.b,t[s],this._wallKeyPitch,Ov))}o=this._normalizeWalls(o,s),o.length?e.walls=o:delete e.walls}}const h=this._commitPhysicalGeometry(this._t(n?"history.split_room":"history.add_room"),i);this._path=[],delete this._resumeDraftBySpace[this._space],this._activeDraftId=null,this._draftSegmentCms=[],this._closingWallCm=null,this._pendingSplit=null,this._splitSel=null;const d=this._areaSel;if(this._areaSel="",this._nameSel="",this._roomDialog=!1,this._regSignature="",this._maybeRebuildDevices(),!h)return;let p=0;if(d){const e=Ov,t={...this._layout};for(const i of this._devices){if(i.area!==d||i.space!==this._space)continue;if(p++,this._layout[i.id])continue;const s=this._defPos[i.id];s&&(t[i.id]={s:this._space,x:s.x/Ov,y:s.y/e},this._dirtyPos.add(i.id))}this._layout=t,this._persistLayout()}const u=this._model.find(e=>e.id===this._space)?.rooms.length||0;this._showToast(d?this._t("toast.room_saved",{n:u,added:p}):this._t("toast.room_saved_no_area",{n:u}))}_cancelPath(){this._wallRepairDiagnostic=null,this._activeDraftId&&(this._resumeDraftBySpace[this._space]=this._activeDraftId),this._path=[],this._activeDraftId=null,this._draftSegmentCms=[],this._closingWallCm=null,this._clearPlanSnapHover(),this._roomDialog=!1,this._pendingSplit=null,this._wallFaceBatch=null,this._splitSel=null,this._mergeSel=null,this._mergeDialog=null,this._physicalSel=null,this._physicalDrag=null,this._physicalRotate=null,this._clearOpeningPlacement(!0)}_resumeLastDraft(){const e=this._resumeDraftBySpace[this._space];if(!e)return;const t=this._spaceModel()?.room_drafts.find(t=>t.id===e);t?(this._activeDraftId=e,this._path=t.points.map(e=>[...e]),this._draftSegmentCms=this._adoptDraftCms(this._path,t.segments.map(e=>e.cm),t.id),this._clearPlanSnapHover()):delete this._resumeDraftBySpace[this._space]}_roomDialogCancel(){if(this._roomDialog=!1,this._roomEditId)return this._roomEditId=null,this._nameSel="",void(this._areaSel="");if(this._wallFaceBatch){const e=this._wallFaceBatch;return this._wallFaceBatch=null,this._path=e.activePath.map(e=>[...e]),this._draftSegmentCms=[...e.activeCms],this._activeDraftId=e.activeDraftId,this._nameSel="",this._areaSel="",void this.requestUpdate()}if(this._pendingSplit)return this._pendingSplit=null,void(this._splitSel=null);this._undoPoint()}get _freeAreas(){const e=new Set;for(const t of this._serverCfg?.spaces||[])for(const i of t.rooms||[])i.area&&e.add(i.area);for(const t of this._wallFaceBatch?.decisions||[])t.create&&t.area&&e.add(t.area);return Object.values(this.hass?.areas||{}).filter(t=>!e.has(t.area_id)).sort((e,t)=>(e.name||"").localeCompare(t.name||""))}_openDeviceInbox(){this._deviceInboxReturn=null,this._deviceInbox=this._deviceInbox||{tab:"on_plan",search:"",showEntities:!1,onlyNew:!1,limit:100}}_closeMarkerDialog(){if(this._markerDialog=null,this._deviceInboxReturn){const e={...this._deviceInboxReturn};this._deviceInbox=e,this._deviceInboxReturn=null,e.anchor&&this.updateComplete.then(()=>requestAnimationFrame(()=>{const t=`.device-inbox-row[data-binding="${CSS.escape(e.anchor)}"]`;this.renderRoot.querySelector(t)?.scrollIntoView({block:"nearest"})}))}}_deviceInboxCandidates(e){return Sp({hass:this._planHass,devices:this._devices,markers:this._markers,showEntities:e,labels:{device:this._t("marker.sub_device"),z2mGroup:this._t("marker.sub_z2m_group"),group:this._t("marker.sub_group"),helper:this._t("marker.sub_helper"),entity:this._t("marker.sub_entity")}})}_deviceInboxRows(){const e=this._deviceInbox||this._deviceInboxReturn,t=!!e?.showEntities,i=[this._haRegistry.revision,this._cfgRev,this._cfgEpoch,this._regSignature,this._newSyncKey,t?1:0,this._showAll?1:0,pa(this.hass,this._config?.language)].join("|");if(this._deviceInboxMemo?.key===i)return this._deviceInboxMemo.rows;const s=this._deviceInboxCandidates(t),n=new Set(s.map(e=>e.value));for(const e of this._markers)e.binding&&"virtual"!==e.binding&&n.add(e.binding);for(const e of this._devices)e.bindingKind&&"virtual"!==e.bindingKind&&e.bindingRef&&n.add(`${e.bindingKind}:${e.bindingRef}`);const o=new Map;for(const e of n)o.set(e,this._bindingStatus(e));const a=this._areaToSpace,r={};for(const[e,t]of Object.entries(this.hass?.areas||{}))r[e]=a[e]?.room?.name||t?.name||e;for(const[e,t]of Object.entries(a))r[e]=t.room.name||r[e]||e;const l=Object.fromEntries(this._model.map(e=>[e.id,e.title])),c=Object.fromEntries(Object.entries(a).map(([e,t])=>[e,t.space])),h={},d=new Map;for(const[e,t]of Object.entries(this._fullRegistryHass.entities||{})){const i=String(t?.platform||"").trim();if(i&&(h[`entity:${e}`]=i),i&&t?.device_id){const e=d.get(t.device_id)||new Set;e.add(i),d.set(t.device_id,e)}}for(const[e,t]of d)h[`device:${e}`]=[...t].sort().join(", ");const p={};for(const[e,t]of Object.entries(this._fullRegistryHass.devices||{})){const i=`device:${e}`,s=d.get(e)||new Set,n=Array.isArray(t?.identifiers?.[0])?String(t.identifiers[0][0]||""):"",o=[n,...s].some(e=>this._excluded.has(e));"service"===t?.entry_type?p[i]="service_entry":o?p[i]="excluded_integration":"Group"===t?.model?p[i]="grouped_light":/scene/i.test(t?.model||"")?p[i]="excluded_domain":(/bridge/i.test(`${t?.model||""}${t?.name||""}`)||"myheat"===n&&t?.via_device_id)&&(p[i]="represented_by_parent")}const u=function(e){const{devices:t,markers:i,candidates:s,statuses:n,newDeviceIds:o,showHiddenOnPlan:a,areaNames:r={},spaceNames:l={},spaceByArea:c={},integrationByBinding:h={},reasonByBinding:d={}}=e,p=new Map;for(const e of t)!e.virtual&&e.bindingKind&&"virtual"!==e.bindingKind&&e.bindingRef&&p.set(`${e.bindingKind}:${e.bindingRef}`,e);const u=new Map,_=new Map;for(const e of i)e?.binding&&"virtual"!==e.binding&&(e.removed?_.set(e.binding,e):u.set(e.binding,e));const m=new Map(s.map(e=>[e.value,e])),g=new Set([...p.keys(),...u.keys(),...m.keys(),...[..._.keys()].filter(e=>m.has(e))]),f=[];for(const e of g){const t=p.get(e),i=u.get(e),s=i?void 0:_.get(e),g=m.get(e);let v;if(s&&g)v="readd";else if(!0===i?.hidden)v="hidden";else if(t||i)v="on_plan";else{if(!g)continue;v="available"}const b=t?.bindingStatus||n.get(e)||Mp,y=i||s,w=Cp(e),k=t?.area||g?.areaId||y?.area||"",x=t?.space||y?.space||c[k]||"",$=t?.name||y?.name||g?.label||e,S=t?.model||y?.model||g?.model||"",M=!!g?.parentDeviceId&&p.has(`device:${g.parentDeviceId}`),C="readd"===v?"removed":"hidden"===v?i&&Dp(i)?d[e]||"automatic_hidden":"manual_hidden":"on_plan"===v?i?"visible_explicit":"visible_auto":d[e]||(M?"represented_by_parent":k&&x?"available":"no_bound_room"),D=!!t&&(!t.hidden||a),P="active"===b.kind,T=D&&(P||"ha_disabled"===b.kind&&a),I=[$,S,h[e],r[k],l[x],e,g?.sub].filter(Boolean).join(" ").toLocaleLowerCase();f.push({key:e,binding:e,category:v,status:b,reason:C,deviceId:t?.id,markerId:y?.id,name:$,icon:t?.icon||y?.icon||("entity"===w?"mdi:code-braces":"mdi:devices"),model:S,integration:h[e]||"",areaId:k,areaName:r[k]||"",spaceId:x,spaceName:l[x]||"",kind:w,isNew:!!t&&o.has(t.id),searchText:I,canFind:T,canEdit:!!t||!!i,canHide:"on_plan"===v&&P,canShow:"hidden"===v&&P,canAdd:("available"===v||"readd"===v)&&P})}const v=e=>"active"===e.status.kind?1:0;return f.sort((e,t)=>e.category.localeCompare(t.category)||Number(t.isNew)-Number(e.isNew)||v(e)-v(t)||e.name.localeCompare(t.name)||e.binding.localeCompare(t.binding))}({devices:this._devices,markers:this._markers,candidates:s,statuses:o,newDeviceIds:this._newIds,showHiddenOnPlan:this._showAll,areaNames:r,spaceNames:l,spaceByArea:c,integrationByBinding:h,reasonByBinding:p});return this._deviceInboxMemo={key:i,rows:u},u}_deviceForInboxRow(e){const t=e.deviceId?this._devices.find(t=>t.id===e.deviceId):null;if(t)return t;const i=e.markerId?this._markers.find(t=>t.id===e.markerId):null;return!i||i.removed?null:{id:i.id,name:e.name,model:e.model,area:e.areaId,space:e.spaceId||this._space,hidden:!0===i.hidden||"ha_disabled"===e.status.kind,userHidden:!0===i.hidden,bindingStatus:e.status,icon:e.icon,entities:"active"===e.status.kind?e.status.enabledEntityIds:[],allEntities:e.status.allEntityIds,primary:"active"===e.status.kind?e.status.enabledEntityIds[0]:void 0,marker:i,bindingKind:e.kind,bindingRef:e.binding.slice(e.binding.indexOf(":")+1),pdfs:i.pdfs||[]}}_openInboxMarker(e,t=!1){const i=this._deviceInbox;if(i){if(this._deviceInboxReturn={...i,anchor:e.key},this._deviceInbox=null,!t){const t=this._deviceForInboxRow(e);return void(t?this._openMarkerDialog(t):this._closeMarkerDialog())}this._openMarkerDialog(),this._markerDialog?this._markerDialog={...this._markerDialog,bindingMode:"ha",binding:e.binding,bindingOpen:!1,showEntities:"entity"===e.kind,name:""}:this._closeMarkerDialog()}}async _setInboxHidden(e,t){const i=this._deviceInbox,s=this._serverCfg;if(!i||!s||i.busy||"active"!==e.status.kind)return;const n=s.markers||[],o=n.find(t=>!t.removed&&t.binding===e.binding);if(!t&&!o)return;const a=o?.id||Fi(e.binding,e.markerId,()=>`m_${Date.now().toString(36)}`),r=o?{...o,hidden:t}:{id:a,binding:e.binding,hidden:!0};s.markers=[...n.filter(t=>t.id!==a&&(t.binding!==e.binding||!0===t.removed)),r],this._deviceInbox={...i,busy:e.key,anchor:e.key};try{await this._saveConfigNow(),this._regSignature="",this._deviceInboxMemo=null,this._maybeRebuildDevices(),this._deviceInbox&&(this._deviceInbox={...this._deviceInbox,busy:void 0}),this._showToast(this._t("device_inbox.saved"))}catch(e){this._serverCfg===s&&(s.markers=n),this._deviceInbox&&(this._deviceInbox={...this._deviceInbox,busy:void 0}),this._showToast(this._t("toast.error",{err:this._errText(e)}))}}_findInboxDevice(e){if(!e.canFind)return;const t=this._deviceForInboxRow(e);if(!t)return;if(this._deviceInbox=null,t.space&&t.space!==this._space){if(!this._commitSpace(t.space))return;this._restoreZoom()}const i=()=>{const e=this._devices.find(e=>e.id===t.id)||t,i=this._pos(e);this._applyView(this._zoom,i.x,i.y),this._selId=e.id,this.requestUpdate(),window.setTimeout(()=>{this._selId===e.id&&(this._selId=null,this.requestUpdate())},1500)};requestAnimationFrame(()=>requestAnimationFrame(i))}_deviceInboxTabKey(e){if("ArrowLeft"!==e.key&&"ArrowRight"!==e.key)return;const t=this._deviceInbox;if(!t)return;const i=["on_plan","available","hidden","readd"],s="ArrowRight"===e.key?1:-1,n=(i.indexOf(t.tab)+s+i.length)%i.length;this._deviceInbox={...t,tab:i[n],limit:100,onlyNew:!1},e.preventDefault()}_openMarkerDialog(e){if(this._vacAllCamerasFor=null,this._vacAllCameraCache=null,e&&this._ackNewDevice(e.id),this._norm)if(e){const t=e.marker,i=Object.prototype.hasOwnProperty.call(t||{},"is_light"),s=Object.prototype.hasOwnProperty.call(t||{},"light_entity"),n=Object.prototype.hasOwnProperty.call(t||{},"glow_color"),o=Object.prototype.hasOwnProperty.call(t||{},"value_badge"),a=Object.prototype.hasOwnProperty.call(t||{},"tap_action"),r=Object.prototype.hasOwnProperty.call(t||{},"toggle_entity"),l=ks(t?.glow_color),c=this._devicePresentation(e,!0).valueBadge,h=qo(this._planHass,e,this._devices),d=Wo(this._planHass,e,h);this._markerDialog={devId:e.id,name:e.name,binding:"virtual"===e.bindingKind?"virtual":e.bindingKind+":"+e.bindingRef,bindingMode:"virtual"===e.bindingKind?"virtual":"ha",bindingOpen:!1,showEntities:"entity"===e.bindingKind&&!!this._fullRegistryHass.entities[e.bindingRef||""]?.device_id,bindingFilter:"",icon:e.marker?.icon||"",autoIcon:e.icon||"",display:Bi(e.marker?.display),rippleColor:Ie(e.marker?.ripple_color,""),rippleSize:Number(e.marker?.ripple_size)>0?Number(e.marker.ripple_size):1.5,size:Number(e.marker?.size)>0?Number(e.marker.size):1,angle:Number(e.marker?.angle)||0,tapAction:Vp(e.marker?.tap_action,e.primary?.split(".")[0]),tapActionTouched:!1,originalHasTapAction:a,originalTapAction:e.marker?.tap_action,tapHintAnnouncement:"",toggleEntity:t?.toggle_entity||"",toggleEntityTouched:!1,originalHasToggleEntity:r,originalToggleEntity:t?.toggle_entity,tapTarget:e.marker?.tap_target||"",tapConfirm:!0===e.marker?.tap_confirm,runFilter:"",controls:Zn(e.marker?.binding,e.marker?.controls,e.entities),controlsFilter:"",lightRole:!0===t?.is_light?"always":!1===t?.is_light?"never":"auto",lightRoleTouched:!1,originalHasIsLight:i,originalIsLight:t?.is_light,lightEntity:t?.light_entity||"",lightEntityTouched:!1,originalHasLightEntity:s,originalLightEntity:t?.light_entity,glowMode:null!=l?.bri?"fixed":l?"color":"auto",glowColor:l?.c||this._fillColors.glow_light.c,glowBrightness:Math.max(1,Math.round(100*(l?.bri??1))),glowColorDrafted:!!l,glowBrightnessDrafted:null!=l?.bri,glowTouched:!1,originalHasGlowColor:n&&(!!l||null===t?.glow_color),originalGlowColor:l||(null===t?.glow_color?null:void 0),valueBadgeEnabled:o?!0===t?.value_badge?.enabled:!!c,valueBadgeSource:t?.value_badge?.source||c?.source||d,valueBadgePosition:t?.value_badge?.position||c?.position||"right",valueBadgeTouched:!1,originalHasValueBadge:o,originalValueBadge:t?.value_badge,useClimateTemp:!0===e.marker?.use_climate_temp,glowRadius:Number(e.marker?.glow_radius_cm)>0?String(this._imperial?Math.round(Number(e.marker.glow_radius_cm)/30.48*10)/10:Math.round(Number(e.marker.glow_radius_cm))/100):"",model:e.model||"",link:e.link||"",description:e.description||"",pdfs:[...e.pdfs||[]],room:e.marker?.room_id?e.space+"#@"+e.marker.room_id:e.space&&e.area?e.space+"#"+e.area:"",hideFromPlan:!0===e.marker?.hidden,busy:!1}}else this._markerDialog={name:"",binding:"virtual",bindingMode:"virtual",bindingOpen:!1,showEntities:!1,bindingFilter:"",icon:"",autoIcon:"",display:"badge",rippleColor:"",rippleSize:1.5,size:1,angle:0,tapAction:"info",tapActionTouched:!1,originalHasTapAction:!1,originalTapAction:void 0,tapHintAnnouncement:"",toggleEntity:"",toggleEntityTouched:!1,originalHasToggleEntity:!1,originalToggleEntity:void 0,tapTarget:"",tapConfirm:!1,runFilter:"",controls:[],controlsFilter:"",lightRole:"auto",lightRoleTouched:!1,originalHasIsLight:!1,originalIsLight:void 0,lightEntity:"",lightEntityTouched:!1,originalHasLightEntity:!1,originalLightEntity:void 0,glowMode:"auto",glowColor:this._fillColors.glow_light.c,glowBrightness:100,glowColorDrafted:!1,glowBrightnessDrafted:!1,glowTouched:!1,originalHasGlowColor:!1,originalGlowColor:void 0,valueBadgeEnabled:!1,valueBadgeSource:null,valueBadgePosition:"right",valueBadgeTouched:!1,originalHasValueBadge:!1,originalValueBadge:void 0,useClimateTemp:!1,glowRadius:"",model:"",link:"",description:"",pdfs:[],room:"",hideFromPlan:!1,busy:!1,uploadId:"up_"+Date.now().toString(36)+Math.random().toString(36).slice(2,6)};else this._showToast(this._t("toast.marker_needs_server"))}_runCandidates(){const e=[];for(const t of Gi)for(const[i,s]of Object.entries(this.hass.states))i.startsWith(t+".")&&this._planEntityAvailable(i)&&e.push({value:i,label:s?.attributes?.friendly_name||i,sub:this._t("run."+t)});return e.sort((e,t)=>e.sub.localeCompare(t.sub)||e.label.localeCompare(t.label))}_bindingCandidates(){const e=Sp({hass:this._planHass,devices:this._devices,markers:this._markers,showEntities:!!this._markerDialog?.showEntities,currentBinding:this._markerDialog?.binding,currentDeviceId:this._markerDialog?.devId,labels:{device:this._t("marker.sub_device"),z2mGroup:this._t("marker.sub_z2m_group"),group:this._t("marker.sub_group"),helper:this._t("marker.sub_helper"),entity:this._t("marker.sub_entity")}}),t=(this._markerDialog?.bindingFilter||"").toLowerCase().trim(),i=t?e.filter(e=>(e.label+" "+e.sub+" "+e.value).toLowerCase().includes(t)):e;return i.sort((e,t)=>e.label.localeCompare(t.label)),i.slice(0,200)}_physicalBodiesR(e=this._spaceModel()){if(!e)return[];const t=`${e.id}|${this._cfgEpoch}|${this._cellCm}|${this._gridPitch}`;if(this._physicalBodiesCache?.key===t)return this._physicalBodiesCache.all;const i=_h(e,this._cellCm,this._gridPitch,2e-4*this._gridPitch,this._partitionOpeningCuts(e));return this._physicalBodiesCache={key:t,...i},i.all}_rawPhysicalBodiesR(e=this._spaceModel()){if(!e)return[];this._physicalBodiesR(e);const t=this._physicalBodiesCache;return t?[...t.drafts,...t.partitions,...t.columns]:[]}_cleanFloor(e,t,i=this._spaceModel()){if(!i)return{floor:t,geom:null,path:"",area:yh([[[...t,t[0]]]])};const s=e.id||`#${i.rooms.indexOf(e)}`,n=`${i.id}|${this._cfgEpoch}|${s}`;if(!this._resize.preview){const e=Sv(this._cleanFloorCache,n);if(e.hit)return e.value}const o=t.map(e=>e[0]),a=t.map(e=>e[1]),r=[Math.min(...o),Math.min(...a),Math.max(...o),Math.max(...a)],l=this._physicalBodiesR(i).filter(e=>{const t=e.map(e=>e[0]),i=e.map(e=>e[1]);return Math.max(...t)>=r[0]&&Math.min(...t)<=r[2]&&Math.max(...i)>=r[1]&&Math.min(...i)<=r[3]}),c=l.length?bh(t,l):null,h={floor:t,geom:c,path:c?ch(c):"",area:yh(c||[[[...t,t[0]]]])};return this._resize.preview||Mv(this._cleanFloorCache,n,h,600),h}_autoIconForBinding(e){if("virtual"===e)return"mdi:map-marker";const[t,i]=e.split(":");if(!i)return"";const s=this._fullRegistryHass,n=this._bindingStatus(e),o="active"===n.kind?n.enabledEntityIds:n.allEntityIds;if("device"===t){const e=s.devices?.[i];if(!e)return"mdi:help-circle";const t=o;return t.some(e=>e.startsWith("lock."))?"mdi:lock":yo(s,e.name_by_user||e.name||"",e.model,t,this._iconRules)}if("entity"===t){const e=s.entities?.[i],t=this.hass.states?.[i],n=e?.name||t?.attributes?.friendly_name||i;return i.startsWith("lock.")?"mdi:lock":yo(s,n,"",[i],this._iconRules)}return""}_allRoomsFlat(){const e=[];for(const t of this._serverCfg?.spaces||[])for(const i of t.rooms||[])i.area?e.push({value:t.id+"#"+i.area,label:(t.title||t.id)+" · "+i.name}):i.id&&e.push({value:t.id+"#@"+i.id,label:(t.title||t.id)+" · "+i.name+" · "+this._t("marker.subarea")});return e}_errText(e){if(!e)return this._t("err.unknown");if("string"==typeof e)return e;if("invalid_passage_fields"===e.code){const t=String(e.message||e.error||"").match(/space=([^;]*);\s*opening=([^;]*);\s*fields=([^;]*)/);if(t){const e=this._serverCfg?.spaces?.find(e=>String(e.id)===t[1]),i={contact:"opening.contact_label",lock:"opening.lock_label",invert:"opening.invert",flip_h:"opening.flip_h",flip_v:"opening.flip_v"},s=t[3].split(",").filter(Boolean).map(e=>i[e]?this._t(i[e]):e).join(", ");return this._t("opening.invalid_passage_fields",{room:e?.title||t[1],fields:s})}}if("invalid_partition_opening_jamb_margin"===e.code){const t=String(e.message||e.error||"").match(/margin_cm=([^;]*)/),i=t?Number(t[1]):NaN;if(Number.isFinite(i))return this._t("opening.partition_jamb_margin",{distance:li(i,this._imperial)})}if(e.message)return e.message;if(e.error)return e.error;if(null!=e.code)return this._t("err.code",{code:e.code});try{return JSON.stringify(e)}catch{return String(e)}}_backupErrorText(e){const t=e?.code??e?.error;if("string"==typeof t){const e=`backup.error.${t}`,i=this._t(e);if(i!==e)return i}return this._errText(e)}async _pickMarkerFiles(e){const t=e.target,i=t.files?[...t.files]:[];if(t.value="",!i.length||!this._markerDialog)return;const s=this._markerDialog.uploadId||this._markerDialog.devId||"new",n=[];for(const e of i)try{const t=new FormData;t.append("marker_id",s),t.append("file",e,e.name);const i=this.hass?.fetchWithAuth?await this.hass.fetchWithAuth("/api/houseplan/upload",{method:"POST",body:t}):await fetch("/api/houseplan/upload",{method:"POST",body:t,headers:this.hass?.auth?.data?.access_token?{authorization:`Bearer ${this.hass.auth.data.access_token}`}:{}}),o=await i.json().catch(()=>({}));if(!i.ok||o.error){const e={too_large:this._t("err.too_large",{mb:o.max_mb||50}),bad_ext:this._t("err.bad_ext"),unauthorized:this._t("err.unauthorized")};throw new Error(e[o.error]||o.error||"HTTP "+i.status)}n.push({name:o.name||e.name,url:o.url})}catch(t){this._showToast(this._t("toast.file_failed",{name:e.name,err:this._errText(t)}))}n.length&&this._markerDialog&&(this._markerDialog={...this._markerDialog,pdfs:[...this._markerDialog.pdfs,...n]},this._showToast(this._t("toast.files_attached",{n:n.length})))}_removeMarkerPdf(e){this._markerDialog&&(this._markerDialog={...this._markerDialog,pdfs:this._markerDialog.pdfs.filter(t=>t.url!==e)})}_markerLightFields(e){const t={};if(e.lightRoleTouched?"always"===e.lightRole?t.is_light=!0:"never"===e.lightRole&&(t.is_light=!1):e.originalHasIsLight&&(t.is_light=e.originalIsLight??null),e.lightEntityTouched?e.lightEntity&&(t.light_entity=e.lightEntity):e.originalHasLightEntity&&(t.light_entity=e.originalLightEntity??null),e.glowTouched){if("auto"!==e.glowMode){const i={c:Ie(e.glowColor,this._fillColors.glow_light.c)};"fixed"===e.glowMode&&(i.bri=Math.max(.01,Math.min(1,Math.round(e.glowBrightness)/100))),t.glow_color=i}}else e.originalHasGlowColor&&(t.glow_color=e.originalGlowColor??null);return t}_markerTapActionFields(e){return e.tapActionTouched?{tap_action:e.tapAction||null}:e.originalHasTapAction?{tap_action:e.originalTapAction??null}:{}}_markerToggleEntityFields(e){return(t={touched:e.toggleEntityTouched,originalHas:e.originalHasToggleEntity,original:e.originalToggleEntity,value:e.toggleEntity}).touched?t.value?{toggle_entity:t.value}:{}:t.originalHas?{toggle_entity:t.original??null}:{};var t}async _saveMarker(){const e=this._markerDialog;if(!e||e.busy)return;const t=this._effectiveMarkerTapAction(e);if("ha"===e.bindingMode&&(!e.binding||"virtual"===e.binding))return;if("virtual"===e.binding&&!e.name.trim())return void this._showToast(this._t("toast.virtual_name_required"));if("run"===t&&!e.tapTarget)return void this._showToast(this._t("toast.run_target_required"));if(e.valueBadgeTouched&&e.valueBadgeEnabled&&!e.valueBadgeSource)return void this._showToast(this._t("toast.value_badge_source_required"));if("ha"===e.bindingMode){const t=this._bindingStatus(e.binding),i=e.devId?this._markers.find(t=>t.id===e.devId):null,s=!i||i.binding!==e.binding;if("active"!==t.kind&&s)return void this._showToast(this._t("ha_disabled"===t.kind?"toast.ha_disabled_add":"toast.ha_binding_unverified"));if("ha_disabled"===t.kind&&!0===i?.hidden&&!e.hideFromPlan)return this._markerDialog={...e,hideFromPlan:!0},void this._showToast(this._t("entity"===t.reason?"toast.ha_disabled_show_entity":"toast.ha_disabled_show_device"))}const i=this._serverCfg;if(!i)return;const s=i.markers||[],n=js(e.room);let o=n?.space||null;const a=n?.area||null,r=n?.roomId||null,l=Fi(e.binding,e.devId,()=>"v_"+Date.now().toString(36)),c=e.devId,h=c?this._devices.find(e=>e.id===c):null,d=n?.space||h?.space||null,p=d?this._spaceModelById(d):this._spaceModel();if(!p)return;const u=p.id;"virtual"!==e.binding||o||(o=u),this._markerDialog={...e,busy:!0};try{const n="virtual"===e.binding?[]:s.filter(t=>t.removed&&t.binding===e.binding).map(e=>e.id),d=n.length>0,_=Zn(e.binding,e.controls,this._bindingEntities(e.binding)),m=s.find(e=>e.id===l||e.id===c)?.vacuum||null,g={id:l,vacuum:m,binding:e.binding,name:e.name.trim()||null,icon:e.icon||null,display:"badge"!==e.display?e.display:null,ripple_color:"icon_ripple"===e.display&&e.rippleColor?e.rippleColor:null,ripple_size:"icon_ripple"===e.display&&1.5!==e.rippleSize?e.rippleSize:null,size:1!==e.size?e.size:null,angle:e.angle?e.angle:null,...this._markerTapActionFields(e),...this._markerToggleEntityFields(e),tap_target:"run"===t&&e.tapTarget||null,tap_confirm:!!e.tapConfirm||null,controls:_.length?_:null,...this._markerLightFields(e),...this._markerValueBadgeFields(e),use_climate_temp:!!e.useClimateTemp||null,glow_radius_cm:(()=>{const t=$v(e.glowRadius);return null==t||t<=0?null:Math.round(this._imperial?30.48*t:100*t)})(),model:e.model.trim()||null,link:e.link.trim()||null,description:e.description.trim()||null,pdfs:e.pdfs,hidden:!!e.hideFromPlan};("virtual"===e.binding||e.room)&&(g.space=o,g.area=a,g.room_id=r);const f=h?.marker?.room_id??null,v=!!e.room&&null!=h&&(h.space!==o||h.area!==a||f!==r);let b=!1;const y=e.uploadId||c;if(y&&y!==l&&g.pdfs?.length)try{const e=await this.hass.callWS({type:"houseplan/files/migrate",from_id:y,to_id:l}),t=e?.mapping||{};g.pdfs=function(e,t,i,s){if(!t||!i||t===i)return e;const n="/files/"+t+"/",o="/files/"+i+"/";return e.map(e=>{if(!e.url.includes(n))return e;const t=e.url.split(n)[1]||"",[i,a]=[t.split("?")[0],t.includes("?")?"?"+t.split("?")[1]:""];if(s){const t=s[decodeURIComponent(i)]??s[i];return t?{...e,url:e.url.split(n+i)[0]+o+encodeURIComponent(t)+a}:e}return{...e,url:e.url.split(n).join(o)}})}(g.pdfs,y,l,t),b=Object.keys(t).length>0}catch(e){this._showToast(this._t("toast.files_migrate_failed",{err:this._errText(e)}))}i.markers=s,c&&c!==l&&(i.markers=function(e,t,i){if(!t||!i||t===i)return[...e];const s=`marker:${t}`,n=`marker:${i}`;return e.map(e=>{const t=Array.isArray(e.controls)?e.controls.map(e=>e===s?n:e):e.controls,i="derived_marker_state"===e.value_badge?.source?.kind&&e.value_badge.source.ref===s?{...e.value_badge,source:{...e.value_badge.source,ref:n}}:e.value_badge;return t===e.controls&&i===e.value_badge?e:{...e,controls:t,value_badge:i}})}(i.markers,c,l)),c&&c!==l&&"derived_marker_state"===g.value_badge?.source?.kind&&g.value_badge.source.ref===`marker:${c}`&&(g.value_badge.source={kind:"derived_marker_state",ref:`marker:${l}`}),i.markers=i.markers.filter(e=>e.id!==l&&e.id!==c&&("virtual"===g.binding||e.binding!==g.binding)),i.markers.push(g);let w=null;const k=c?this._layout[c]:null,x=k?{s:k.s||h?.space||this._space,x:k.x,y:k.y}:c&&h&&this._defPos[c]?this._normPos(h.space,this._defPos[c].x,this._defPos[c].y):null;if(!d&&x&&x.s===u)l===c&&this._layout[l]&&!v||(w={s:x.s,x:x.x,y:x.y},this._layout={...this._layout,[l]:w});else if(d||!this._layout[l]||v){let e=p.vb[0]+p.vb[2]/2,t=p.vb[1]+p.vb[3]/2;const i=r?p.rooms.find(e=>e.id===r):a?p.rooms.find(e=>e.area===a):void 0;i&&([e,t]=this._roomCenter(i)),w=this._normPos(u,e,t),this._layout={...this._layout,[l]:w}}if(await this._saveConfigNow(),w){const e=ml(w);this._layout={...this._layout,[l]:e},this._noteLayoutRev(await this.hass.callWS({type:"houseplan/layout/update",device_id:l,pos:e}))}const $=new Set(n);c&&c!==l&&$.add(c),$.delete(l);for(const e of $)delete this._layout[e],await this.hass.callWS({type:"houseplan/layout/delete",device_id:e}).then(e=>this._noteLayoutRev(e)).catch(()=>{});b&&y&&await this.hass.callWS({type:"houseplan/files/cleanup",marker_id:y}).catch(()=>{}),this._closeMarkerDialog(),this._regSignature="",this._maybeRebuildDevices(),this._showToast(this._t("toast.marker_saved"))}catch(e){this._markerDialog&&(this._markerDialog={...this._markerDialog,busy:!1}),this._showToast(this._t("toast.error",{err:this._errText(e)}))}}async _deleteMarker(){const e=this._markerDialog;if(!e||e.busy||!e.devId)return;const t=e.devId?this._devices.find(t=>t.id===e.devId):null,i=this._markers.find(t=>t.id===e.devId);if(!t&&!i)return;const s=e.name||this._t("device.fallback");if(!confirm(this._t("confirm.remove_marker",{name:s})))return;const n=this._serverCfg;n.markers=n.markers||[];const o=n.markers,a=t?.id||i.id,r=t?"virtual"===t.bindingKind?"virtual":t.bindingKind&&t.bindingRef?`${t.bindingKind}:${t.bindingRef}`:"":i.binding;if(!r)return;const l=function(e,t,i,s){const n=new Set([t]),o=e.filter(e=>{const o=e.id===t||!s&&e.binding===i;return o&&n.add(e.id),!o});return{markers:s?o:[...o,{id:t,binding:i,removed:!0,hidden:!0}],cleanupIds:n}}(n.markers,a,r,"virtual"===r);var c,h;n.markers=(c=l.markers,h=l.cleanupIds,c.map(e=>{if(!Array.isArray(e.controls))return e;const t=e.controls.filter(e=>!("string"==typeof e&&e.startsWith("marker:")&&h.has(e.slice(7))));return t.length===e.controls.length?e:{...e,controls:t.length?t:null}}));const d=l.cleanupIds;this._markerDialog={...e,busy:!0};try{await this._saveConfigNow();for(const e of d){delete this._layout[e],delete this._defPos[e],this._dirtyPos.delete(e),this._sentPos.delete(e);const t=this._activityRt.get(e);t&&clearTimeout(t.timer),this._activityRt.delete(e),this._vacRt.delete(e),delete this._vacSrvTrails[e],await this.hass.callWS({type:"houseplan/layout/delete",device_id:e}).then(e=>this._noteLayoutRev(e)).catch(()=>{}),await this.hass.callWS({type:"houseplan/files/cleanup",marker_id:e}).catch(()=>{}),await this.hass.callWS({type:"houseplan/trail/delete",marker_id:e}).catch(()=>{})}this._deviceInboxReturn&&(this._deviceInboxReturn={...this._deviceInboxReturn,tab:"readd",anchor:r}),this._closeMarkerDialog(),this._infoCard?.id===a&&this._closeInfoCard(),this._selId===a&&(this._selId=null),this._drag?.id===a&&(this._drag=null),this._regSignature="",this._maybeRebuildDevices(),this._showToast(this._t("toast.marker_removed"))}catch(e){this._serverCfg===n&&(n.markers=o),this._markerDialog&&(this._markerDialog={...this._markerDialog,busy:!1}),this._showToast(this._t("toast.error",{err:this._errText(e)}))}}_normPos(e,t,i){return{s:e,x:t/Ov,y:i/Ov}}_openSpaceDialog(e,t){if(this._serverStorage&&this._serverCfg)if("edit"===e){const i=this._serverCfg.spaces.find(e=>e.id===t);if(!i)return;const s=cs(i),n=i.settings?.custom_fill&&"object"==typeof i.settings.custom_fill?rs(i.settings.custom_fill):null,o="none"===s.fill?{...n||as,a:0}:n;this._spaceDialog={mode:e,spaceId:t,title:i.title,planUrl:i.plan_url||null,planFile:null,source:i.plan_url?"file":"draw",showBorders:s.showBorders,showNames:s.showNames,zeroWallStyle:V_(i),displayTouched:!0,hideDecor:s.hideDecor,hideOpenings:s.hideOpenings,roomColor:s.color,roomOpacity:s.opacity,fillMode:"none"===s.fill?"custom":s.fill,customFill:o,glowEnabled:s.glow,bgColor:s.bgColor,bgMode:"static"===i.settings?.bg_mode||"daynight"===i.settings?.bg_mode?i.settings.bg_mode:null,northDeg:zr({},i.settings),sunRays:"boolean"==typeof i.settings?.sun_rays?i.settings.sun_rays:null,tempMin:s.tempMin,tempMax:s.tempMax,showLqi:s.showLqi??this._config?.show_signal??!0,cardFontScale:s.cardFontScale,labelTemp:s.labelTemp,labelHum:s.labelHum,labelLqi:s.labelLqi,labelLight:s.labelLight,cellCm:Number(i.cell_cm)>0?Number(i.cell_cm):5,cellCmInput:Th(Number(i.cell_cm)>0?Number(i.cell_cm):5,this._imperial),cellCmTouched:!1,busy:!1}}else this._spaceDialog={mode:e,title:"",planUrl:null,planFile:null,...tp(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:ns,roomOpacity:os,fillMode:"custom",customFill:{...as,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:20,tempMax:25,showLqi:this._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:Ph(this._imperial),cellCmInput:Th(Ph(this._imperial),this._imperial),cellCmTouched:!1,busy:!1};else this._showToast(this._t("toast.integration_missing"))}async _pickPlanFile(e){const t=e.target,i=t.files?.[0];if(!i||!this._spaceDialog)return;const s={"image/svg+xml":"svg","image/png":"png","image/jpeg":"jpg","image/webp":"webp"}[i.type]||(i.name.toLowerCase().endsWith(".svg")?"svg":"");if(!s)return void this._showToast(this._t("toast.plan_formats"));const n=new Uint8Array(await i.arrayBuffer());let o="";for(let e=0;e{const t=new Image;t.onload=()=>e(t.naturalWidth&&t.naturalHeight?t.naturalWidth/t.naturalHeight:1.414),t.onerror=()=>e(1.414),t.src=r});URL.revokeObjectURL(r),this._spaceDialog={...this._spaceDialog,planFile:{ext:s,b64:a,aspect:l,name:i.name}}}_useServerPlan(e){const t=this._spaceDialog;t&&(this._spaceDialog={...t,planUrl:e,planFile:null,pickSaved:!1,savedAspect:void 0},this._aspectJob=this._readPlanAspect(e))}async _readPlanAspect(e){for(let t=0;t<40;t++){const t=this._display(e);if(t){const i=await new Promise(e=>{const i=new Image;i.onload=()=>e(i.naturalWidth&&i.naturalHeight?i.naturalWidth/i.naturalHeight:0),i.onerror=()=>e(0),i.src=t}),s=this._spaceDialog;return s&&s.planUrl===e&&Number.isFinite(i)&&i>0?(this._spaceDialog={...s,savedAspect:i},i):0}if(await new Promise(e=>setTimeout(e,150)),this._spaceDialog?.planUrl!==e)return 0}return 0}async _deleteServerPlan(e){if(confirm(this._t("confirm.delete_plan",{name:e})))try{await this.hass.callWS({type:"houseplan/plans/delete",name:e});const t=this._spaceDialog;t?.saved&&(this._spaceDialog={...t,saved:t.saved.filter(t=>t.name!==e)})}catch(e){this._showToast(this._t("toast.plan_delete_failed",{err:this._errText(e)}))}}_renderServerPlans(e){if(e.savedBusy)return q`
${this._t("space.loading")}
`;const t=e.saved||[];if(!t.length)return q`
${this._t("space.no_saved")}
`;return q`
- ${t.map(t=>q` -
- -
- ${t.name} - ${(e=>e>=1048576?(e/1048576).toFixed(1)+" MB":Math.round(e/1024)+" KB")(t.size)}${t.used_by.length?" · "+this._t("space.used_by",{list:t.used_by.join(", ")}):""} -
- - -
`)} -
`}async _saveSpaceDialog(){const e=this._spaceDialog;if(!e||e.busy||!e.title.trim())return;if("file"===e.source&&!e.planFile&&!e.planUrl)return void this._showToast(this._t("toast.plan_required"));const t="create"===e.mode&&0===(this._serverCfg?.spaces.length||0);this._spaceDialog={...e,busy:!0};try{const n="create"===e.mode?"s"+Date.now().toString(36):e.spaceId;let o=null;if("file"===e.source&&e.planFile){o={url:(await this.hass.callWS({type:"houseplan/plan/set",space_id:n,ext:e.planFile.ext,data:e.planFile.b64})).url,aspect:e.planFile.aspect}}let a=e.savedAspect||null;!o&&"file"===e.source&&e.planUrl&&!a&&this._aspectJob&&(a=await this._aspectJob||null);const r=this._serverCfg;let l;if("create"===e.mode)i=n,s=e.title.trim(),l={id:i,title:s,plan_url:null,view_box:[0,0,1,1],rooms:[],wall_segments:[]},r.spaces.push(l);else{if(l=r.spaces.find(e=>e.id===n),!l)throw new Error("space "+n+" is gone from the config");l.title=e.title.trim()}o?(l.plan_url=o.url,l.plan_aspect=o.aspect):"file"===e.source&&e.planUrl&&e.planUrl!==l.plan_url&&(l.plan_url=e.planUrl,l.plan_aspect=a),"draw"===e.source&&(l.plan_url=null,l.plan_aspect=null,delete l.plan_x,delete l.plan_y,delete l.plan_scale,delete l.plan_scale_x,delete l.plan_scale_y,delete l.plan_angle),l.settings={...l.settings||{},show_borders:e.showBorders,show_names:e.showNames,hide_decor:e.hideDecor||void 0,hide_openings:e.hideOpenings||void 0,room_color:e.roomColor,room_opacity:e.roomOpacity,bg_color:e.bgColor||void 0,bg_mode:e.bgMode||void 0,north_deg:e.northDeg??void 0,sun_rays:e.sunRays??void 0,fill_mode:e.fillMode,custom_fill:e.customFill||void 0,glow_enabled:e.glowEnabled,temp_min:Number.isFinite(e.tempMin)?Math.min(e.tempMin,e.tempMax):20,temp_max:Number.isFinite(e.tempMax)?Math.max(e.tempMin,e.tempMax):25,show_lqi:e.showLqi,card_font_scale:1!==e.cardFontScale?e.cardFontScale:void 0,label_temp:e.labelTemp,label_hum:e.labelHum,label_lqi:e.labelLqi,label_light:e.labelLight},l.zero_wall_style=e.zeroWallStyle,l.cell_cm=Number.isFinite(e.cellCm)&&e.cellCm>0?Math.max(.1,Math.min(kv,e.cellCm)):5,await this._saveConfigNow(),this._spaceDialog=null,"create"===e.mode&&this._commitSpace(l.id),this._regSignature="",this._maybeRebuildDevices(),this._importQueue.length?this._openNextImport():t||this._importTotal>0?(this._importTotal=0,this._commitSpace(this._serverCfg.spaces[0]?.id||this._space),this._setMode("plan"),this._tool="draw",this._path=[],this._cursorPt=null,this._primeDrawWallField(),this._showToast(this._t(t&&!this._importTotal?"toast.space_added_onboard":"import.done"))):(this._showToast("create"===e.mode?this._t("toast.space_added"):this._t("toast.space_saved")),"create"===e.mode&&("plan"!==this._mode?this._setMode("plan"):(this._tool="draw",this._path=[],this._cursorPt=null,this._primeDrawWallField(),this._saveNav())))}catch(e){"conflict"!==e?.code&&await this._reloadConfigOnly(!0),this._spaceDialog&&(this._spaceDialog={...this._spaceDialog,busy:!1}),this._showToast(this._t("toast.error",{err:this._errText(e)}))}var i,s}async _deleteSpace(){const e=this._spaceDialog;if(!e||"edit"!==e.mode)return;const t=this._serverCfg;if(!t)return;const i=t.spaces.find(t=>t.id===e.spaceId),s=qg(t,this._layout||{},e.spaceId||""),n=1===t.spaces.length&&t.spaces[0]?.id===e.spaceId;if(!s.count||n){if(confirm(this._t("confirm.delete_space",{title:i.title}))){this._spaceDialog={...e,deleteBlockers:0,busy:!0};try{this._saveConfigDebounced.pending()&&this._saveConfigDebounced.flush(),this._persistLayout.pending()&&this._persistLayout.flush(),await this._writeChain;const t=await this.hass.callWS({type:"houseplan/space/delete",space_id:e.spaceId,expected_config_rev:this._cfgRev,expected_layout_rev:this._layoutRev}),[i,s]=await Promise.all([this.hass.callWS({type:"houseplan/config/get"}),this.hass.callWS({type:"houseplan/layout/get"})]);this._adoptStructuralResponses(i,s),this._cfgRev=t?.config_rev??this._cfgRev,this._layoutRev=t?.layout_rev??this._layoutRev,this._spaceDialog=null,this._space===e.spaceId&&this._commitSpace(this._serverCfg.spaces[0]?.id||""),this._regSignature="",this._maybeRebuildDevices(),this._showToast(this._t("toast.space_deleted"))}catch(t){"conflict"!==t?.code&&"space_in_use"!==t?.code||await Promise.all([this._reloadConfigOnly(!0),this._reloadLayoutOnly()]);const i=this._serverCfg;if(this._spaceDialog&&i){const t=qg(i,this._layout||{},e.spaceId||""),s=1===i.spaces.length&&i.spaces[0]?.id===e.spaceId;this._spaceDialog={...this._spaceDialog,busy:!1,deleteBlockers:s?0:t.count}}this._showToast(this._t("toast.delete_failed",{err:this._errText(t)}))}}}else this._spaceDialog={...e,deleteBlockers:s.count}}async _saveConfigNow(){this._cfgEpoch++;try{await this._writeConfig()}catch(e){throw e?.physicalGeometryRolledBack?await this._reloadRejectedPhysicalWrite():"conflict"===e?.code&&await this._reloadConfigOnly(),e}}_startImport(){const e=this._importDialog;if(!e)return;const t=e.floors.filter(e=>e.checked).map(e=>e.name);this._importDialog=null,t.length?(this._importQueue=t,this._importTotal=t.length,this._openNextImport()):this._openSpaceDialog("create")}_openNextImport(){const e=this._importQueue.shift();void 0!==e&&(this._spaceDialog={mode:"create",title:e,planUrl:null,planFile:null,...tp(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:ns,roomOpacity:os,fillMode:"custom",customFill:null,glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:20,tempMax:25,showLqi:this._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:Ph(this._imperial),cellCmInput:Th(Ph(this._imperial),this._imperial),cellCmTouched:!1,busy:!1})}_skipImport(){this._spaceDialog=null,this._importQueue.length?this._openNextImport():this._importTotal>0&&this._model.length&&(this._importTotal=0,this._commitSpace(this._serverCfg.spaces[0]?.id||this._space),this._setMode("plan"),this._showToast(this._t("import.done")))}_renderImportDialog(){const e=this._importDialog,t=e.floors.filter(e=>e.checked).length;return q`this._importDialog=null}> -
-
${this._t("import.hint")}
- ${e.floors.map((t,i)=>q``)} -
-
- - - -
-
`}_sunGlobal(){const e=this._settingsDialog;return e?{...this._settings,north_deg:e.northDeg??void 0,bg_mode:e.bgMode,sun_rays:e.sunRays}:this._settings}_sunSpace(){const e=this._spaceDialog,t=this._curSpaceCfg?.settings||{};return e&&"edit"===e.mode&&e.spaceId===this._space?{...t,north_deg:e.northDeg??void 0,bg_mode:e.bgMode??void 0,sun_rays:e.sunRays??void 0}:t}_effNorth(){return zr(this._sunGlobal(),this._sunSpace())}_effBgMode(){return Fr(this._sunGlobal(),this._sunSpace())}_dayCycleState(e=new Date){return(this._modeTransitionVisual?.viewWeight??("view"===this._mode?1:0))<=0||"daynight"!==this._effBgMode()?null:$r(this._renderPlanHass,e)}_syncDayCycleClock(){const e=this._dayCycleState();this._dayCycleClockKey=e?Sr(e):"";const t="clock"===e?.source&&"hidden"!==this.ownerDocument.visibilityState&&this.isConnected;t&&!this._dayCycleTimer?this._dayCycleTimer=window.setInterval(this._dayCycleTick,3e4):!t&&this._dayCycleTimer&&(clearInterval(this._dayCycleTimer),this._dayCycleTimer=0)}_dayCycleVisibility(e){"hidden"!==e.kind?(this._dayCycleTick(),this._syncDayCycleClock()):this._dayCycleTimer&&(clearInterval(this._dayCycleTimer),this._dayCycleTimer=0)}_effSunRays(){return Nr(this._sunGlobal(),this._sunSpace())}_sunNow(){return null!==this._effNorth()?Er(this._renderPlanHass):null}_renderSunRays(e){const t=W``,i=this._modeTransitionVisual?.viewWeight??0;if(this._editing&&i<=0||!this._effSunRays())return this._sunFadeReset(),t;const s=this._effNorth(),n=null!==s?Er(this._renderPlanHass):null;if(!n||n.elevation<=0)return this._sunFadeReset(),t;if(o=n.elevation,Number(o)>=3)this._sunOutTimer&&(clearTimeout(this._sunOutTimer),this._sunOutTimer=0),this._sunOut=!1,this._sunShown=!0;else{if(!this._sunShown)return t;this._sunOut||(this._sunOut=!0,this._sunOutTimer=window.setTimeout(()=>{this._sunOutTimer=0,this._sunShown=!1,this._sunOut=!1,this.requestUpdate()},2e3))}var o;const a=this._zeroWalls(),r=a.barriers.map(e=>e.join(",")).join(";"),l=`${e.id}|${n.azimuth}|${n.elevation}|${s}|${this._cfgEpoch}|${a.style}|${r}`;if(!this._sunRaysCache||this._sunRaysCache.key!==l){const t=e.rooms.map(e=>({id:e.id||"",poly:hi(e)})).filter(e=>!!e.id&&!!e.poly),i=this._openingsR.filter(e=>"window"===e.type&&"partition"!==e.host?.kind).map(e=>({id:e.id,x:e.rx,y:e.ry,angle:e.angle,length:e.rlen})),o=this._spaceWalls,r=this._openCuts(),c=this._openingWallIndexFor(e,r).value,h={},d={},p=this._wallUnionGeometry()?.roomGeom;if(o.length){for(const i of t){const t=this._innerRoomContour(e,i.id,r,p);t&&(h[i.id]=t)}for(const e of i){const t=Zc(c,{x:e.x,y:e.y,angle:e.angle,length:e.length});t.cm>0&&(d[e.id]=Ml(t.cm,this._cellCm,this._gridPitch))}}let u=Rr(t,i,n.azimuth,n.elevation,s,o.length?h:void 0,o.length?d:void 0);const _=[...this._physicalBodiesR(e),...a.barriers.map(e=>[[e[0],e[1]],[e[2],e[3]]])];_.length&&(u=u.map(e=>{const t=function(e,t,i){return i>0?e.map(e=>function(e){const t=[...e].sort((e,t)=>e[0]-t[0]||e[1]-t[1]);if(t.length<=2)return t;const i=(e,t,i)=>(t[0]-e[0])*(i[1]-e[1])-(t[1]-e[1])*(i[0]-e[0]),s=[];for(const e of t){for(;s.length>=2&&i(s[s.length-2],s[s.length-1],e)<=0;)s.pop();s.push(e)}const n=[];for(let e=t.length-1;e>=0;e--){const s=t[e];for(;n.length>=2&&i(n[n.length-2],n[n.length-1],s)<=0;)n.pop();n.push(s)}return s.slice(0,-1).concat(n.slice(0,-1))}([...e,...e.map(e=>[e[0]+t[0]*i,e[1]+t[1]*i])])).filter(e=>e.length>=3):e}(_,e.dir,e.len),i=e.polys.map(e=>bh(e,t));return{...e,paths:i.map(ch).filter(Boolean),polys:i.flatMap(kh)}}).filter(e=>e.paths?.length||e.polys.length)),this._sunRaysCache={key:l,rays:u,rims:u.map(e=>function(e,t=1e-4){const[i,s]=e.dir,n=-s,o=i,a=[];for(const r of[e.a,e.b]){const l=[];for(const a of e.polys)for(let e=0;et)continue;if(Math.abs((h[0]-r[0])*n+(h[1]-r[1])*o)>t)continue;const d=(c[0]-r[0])*i+(c[1]-r[1])*s,p=(h[0]-r[0])*i+(h[1]-r[1])*s;Math.abs(p-d)<=t||l.push(de[0]-t[0]);const c=[];for(const e of l){const i=c[c.length-1];i&&e[0]<=i[1]+t?i[1]=Math.max(i[1],e[1]):c.push([e[0],e[1]])}for(const[e,t]of c)a.push([[r[0]+i*e,r[1]+s*e],[r[0]+i*t,r[1]+s*t]])}return a}(e))}}const c=this._sunRaysCache.rays,h=this._sunRaysCache.rims;if(!c.length)return t;const d=(p=function(e){const t=Math.min(90,Math.max(-90,Number(e)||0));let i=Mr[Mr.length-1][1];for(let e=1;e - ${c.map((e,t)=>{const i=(e.a[0]+e.b[0])/2,s=(e.a[1]+e.b[1])/2,n=i+e.normal[0]*e.depth,o=s+e.normal[1]*e.depth;return W` - ${u.map(([e,t])=>W``)} - - - ${_.map(([e,t])=>W``)} - `})} - - - ${c.map((e,t)=>e.paths?.length?e.paths.map(e=>W``):e.polys.map(e=>W``))} - ${c.map((e,t)=>(h[t]||[]).map(e=>W``))} - `}_sunFadeReset(){this._sunOutTimer&&(clearTimeout(this._sunOutTimer),this._sunOutTimer=0),this._sunShown=!1,this._sunOut=!1}_compassPoint(e){const t=e.currentTarget.getBoundingClientRect(),i=e.clientX-(t.left+t.width/2),s=e.clientY-(t.top+t.height/2);if(Math.hypot(i,s)<5)return;let n=Math.round(180*Math.atan2(i,-s)/Math.PI);e.shiftKey&&(n=15*Math.round(n/15)),n=(n%360+360)%360,this._settingsDialog={...this._settingsDialog,northDeg:n}}_renderCompass(){const e=this._settingsDialog.northDeg;return q`{e.currentTarget.setPointerCapture(e.pointerId),this._compassDrag=!0,this._compassPoint(e)}} - @pointermove=${e=>{this._compassDrag&&this._compassPoint(e)}} - @pointerup=${()=>this._compassDrag=!1} - @pointercancel=${()=>this._compassDrag=!1}> - - ${[0,45,90,135,180,225,270,315].map(e=>W``)} - - - - ${this._t("gs.north_letter")} - - ${null===e?"—":e+"°"} - `}_stageBg(e){const t=this._settingsDialog,i=this._spaceDialog,s=t?t.bgColor||"":hs(this._settings,{bgColor:null});return(i&&"edit"===i.mode&&i.spaceId===this._space?i.bgColor||"":e.bgColor||"")||s}_stageBgHex(){const e=this._stageEl;if(e){const t=getComputedStyle(e).backgroundColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);if(t)return"#"+t.slice(1,4).map(e=>(+e).toString(16).padStart(2,"0")).join("")}return"#111111"}_preflightDiagnostics(e,t){const i=new Map((t?.spaces||[]).map(e=>[String(e?.id||""),e]));return{kind:"houseplan-optimize-preflight",origin:"runtime",cardVersion:vv,checkedAt:(new Date).toISOString(),preflightFingerprint:e.fingerprint,failures:e.failures.map(e=>({spaceId:e.spaceId,displayName:e.displayName,reason:e.reason,detail:e.detail??null,spaceGeometryFingerprint:i.has(e.spaceId)?Ug(i.get(e.spaceId)):null}))}}_reportPreflightFailure(e,t){e.ok||e.fingerprint===this._reportedPreflightFingerprint||(this._reportedPreflightFingerprint=e.fingerprint,console.warn("[houseplan] optimize preflight failed",this._preflightDiagnostics(e,t)))}_preflightVersionsDiffer(){const e=this._haIntegrationVersion;return"string"==typeof e&&e.length>0&&e!==vv}async _copyPreflightDiagnostics(){const e=this._alignDialog?.preflight;if(!e||e.ok)return;const t=JSON.stringify(this._preflightDiagnostics(e,this._alignDialog?.config??null),null,2);try{await navigator.clipboard.writeText(t),this._preflightClipboardFallback=null,this._showToast(this._t("gs.preflight_copied"))}catch{this._preflightClipboardFallback=t}}_checkOptimizeGeometry(e){return Xg(e,{fallbackSpaceName:e=>this._t("gs.align_preflight_space",{n:String(e)})})}_checkSpacePhysicalGeometry(e,t,i){return function(e,t,i={}){const s=(Array.isArray(e?.spaces)?e.spaces:[]).find(e=>String(e?.id||"")===String(t||"")),n=Ug(s);if(!s)return{spaceId:String(t||""),displayName:"",status:"failed",reason:"prepare-exception",fingerprint:n,ok:!1};const o=Xg({...e,spaces:[s]},{...i,fingerprint:()=>n}).spaces[0]||{spaceId:String(t||""),displayName:"",status:"failed",reason:"prepare-exception"};return{...o,fingerprint:n,ok:"failed"!==o.status}}(e,t,{fallbackSpaceName:e=>this._t("gs.align_preflight_space",{n:String(e)}),captureWallGeometry:i?(e,t)=>i(t):void 0})}_optimizeReferenceContext(e){const t=this._haRegistry,i=this._fullRegistryHass,s={},n=(...e)=>{for(const t of e){const e=String(t||"").trim();if(e)return e}return""};for(const[e,i]of Object.entries(t.devices||{}))s[e]=n(i?.name_by_user,i?.name,i?.model);for(const[e,i]of Object.entries(t.entities||{}))s[`lg_${e}`]=n(this.hass?.states?.[e]?.attributes?.friendly_name,i?.name,i?.original_name);for(const e of this._devices)s[e.id]=n(e.name,s[e.id]);for(const e of this._serverCfg?.markers||[]){const t=String(e.binding||"").indexOf(":"),o=t>0?e.binding.slice(0,t):"",a=t>0?e.binding.slice(t+1):"",r="device"===o?i?.devices?.[a]:"entity"===o?i?.entities?.[a]:null;s[e.id]=n(e.name,"device"===o?r?.name_by_user:null,r?.name,"entity"===o?this.hass?.states?.[a]?.attributes?.friendly_name:null,s[e.id])}return{effectiveAreaByMarker:Object.fromEntries(this._devices.filter(e=>!e.virtual&&!!e.area).map(e=>[e.id,e.area])),ownerRoster:{authoritative:t.authoritative,deviceIds:Object.keys(t.devices||{}),entityIds:[...new Set([...Object.keys(t.entities||{}),...Object.keys(this.hass?.states||{})])],names:s},removeLiveMissingPositions:e}}_previewAlignDialog(e){if(!this._norm||!this._serverCfg)return;const t=this._serverCfg.spaces||[];let i;try{i=jg(this._serverCfg,this._layout||{},this._optimizeReferenceContext(e))}catch(e){return void this._showWallModelMigrationBlocked(e)}const s=i.changed?this._checkOptimizeGeometry(i.config):null;s&&this._reportPreflightFailure(s,i.config);const n=Math.ceil(10*i.report.maxShiftCm)/10,o=t.find(e=>null!=e?.id&&String(e.id)===i.report.maxSpace),a=t.length>1&&o?String(o.title||o.id):"";this._preflightClipboardFallback=null,this._alignDialog={report:i.report,config:i.config,layout:i.layout,cm:n,where:a,preflight:s,changed:i.changed,busy:!1,removeLiveMissingPositions:e}}async _runAlignToGrid(){let e=this._alignDialog;if(!e||e.busy||!this._serverCfg||!e.changed||!e.preflight?.ok)return;const t=Jd(e.config);if(e.preflight.fingerprint!==t){const t=this._checkOptimizeGeometry(e.config);if(this._reportPreflightFailure(t,e.config),e={...e,preflight:t},this._alignDialog=e,!t.ok)return}this._clearGeometryGesture(),this._alignDialog={...e,busy:!0};try{this._saveConfigDebounced.pending()&&this._saveConfigDebounced.flush(),await this._writeChain;const t=await this.hass.callWS({type:"houseplan/plan/optimize",config:e.config,layout:e.layout,expected_config_rev:this._cfgRev,expected_layout_rev:this._layoutRev});this._serverCfg=e.config,this._layout=e.layout,this._geometryHistory.clear(),this._cfgRev=t?.config_rev??this._cfgRev+1,this._layoutRev=t?.layout_rev??this._layoutRev+1,this._canOptimizeUndo=!!t?.can_undo,this._undoKind=t?.can_undo?"optimize":null,this._dirtyPos.clear(),this._sentPos.clear(),this._cfgEpoch++,this._modelCache=null,this._frame=null,this._regSignature="",this._maybeRebuildDevices(),this._cacheSnapshot(),this._alignDialog=null,this._preflightClipboardFallback=null,this.requestUpdate(),this._showToast(this._t("gs.align_done",{n:String(e.report.moved),m:String(e.report.migrated+e.report.canonicalized+e.report.coordsCanonicalized+e.report.latticeCoordinatesCanonicalized+e.report.wallsMerged+e.report.spansMerged+e.report.partitionsMerged+e.report.partitionsReconciled+e.report.openingsRehosted+e.report.wallsStraightened),r:String(e.report.spaceRefsRemapped+e.report.roomRefsRemapped+e.report.positionsRemapped+e.report.markersDetached+e.report.orphanRoomLabelsRemoved+e.report.orphanDevicePositionsRemoved+e.report.orphanGroupPositionsRemoved)}))}catch(e){if(this._alignDialog&&(this._alignDialog={...this._alignDialog,busy:!1}),"wall_model_client_outdated"===e?.code)return void this._showToast(this._t("toast.wall_model_client_outdated"));"conflict"===e?.code&&await Promise.all([this._reloadConfigOnly(!0),this._reloadLayoutOnly()]),this._showToast(this._t("toast.error",{err:this._errText(e)}))}}async _undoPlanOptimization(){if(!this._canOptimizeUndo||this._optimizeUndoBusy)return;const e=this._undoKind;this._clearGeometryGesture(),this._optimizeUndoBusy=!0,this.requestUpdate();try{await this.hass.callWS({type:"houseplan/plan/optimize_undo",expected_config_rev:this._cfgRev,expected_layout_rev:this._layoutRev});const[t,i]=await Promise.all([this.hass.callWS({type:"houseplan/config/get"}),this.hass.callWS({type:"houseplan/layout/get"})]);this._serverCfg=t?.config||this._serverCfg,this._cfgRev=t?.rev??this._cfgRev,this._layout=i?.layout||this._layout,this._geometryHistory.clear(),this._layoutRev=i?.rev??this._layoutRev,this._canOptimizeUndo=!1,this._undoKind=null,this._cfgEpoch++,this._modelCache=null,this._frame=null,this._regSignature="",this._maybeRebuildDevices(),this._cacheSnapshot(),this.requestUpdate(),this._showToast(this._t("import"===e?"backup.import_undone":"gs.optimize_undone"))}catch(e){this._canOptimizeUndo=!1,this._undoKind=null,this._showToast(this._t("toast.error",{err:this._errText(e)}))}finally{this._optimizeUndoBusy=!1,this.requestUpdate()}}async _runBackupExport(){const e=this._backupExportDialog;if(e&&!e.busy){this._backupExportDialog={...e,busy:!0,error:""};try{const t=await this.hass.callWS({type:"houseplan/export/create",kind:e.kind,space_id:"space"===e.kind?this._space:void 0,..."space"===e.kind&&e.planOnly?{plan_only:!0}:{},card_version:vv}),i=new Blob([JSON.stringify(t.document,null,2)+"\n"],{type:"application/json;charset=utf-8"}),s=URL.createObjectURL(i),n=document.createElement("a");n.href=s,n.download=t.filename||`houseplan-${e.kind}.json`,n.style.display="none",document.body.append(n),n.click(),n.remove(),window.setTimeout(()=>URL.revokeObjectURL(s),0),this._backupExportDialog=null,this._showToast(this._t("backup.export_done"))}catch(e){this._backupExportDialog&&(this._backupExportDialog={...this._backupExportDialog,busy:!1,error:this._backupErrorText(e)})}}}async _pickBackupImport(e){const t=e.target,i=t.files?.[0];if(t.value="",i){this._settingsDialog=null,this._backupImportDialog={filename:i.name,size:i.size,token:"",preview:null,expectedConfigRev:this._cfgRev,expectedLayoutRev:this._layoutRev,duplicatePolicy:"skip",confirmMissing:!1,busy:!0,error:""};try{this._saveConfigDebounced.pending()&&this._saveConfigDebounced.flush(),await this._writeChain,this._persistLayout.pending()&&this._persistLayout.flush();const e="/api/houseplan/import/preview?duplicate_policy=skip",t={method:"POST",body:i,headers:{"content-type":"application/json"}},s=this.hass?.fetchWithAuth?await this.hass.fetchWithAuth(e,t):await fetch(e,{...t,headers:{...t.headers,...this.hass?.auth?.data?.access_token?{authorization:`Bearer ${this.hass.auth.data.access_token}`}:{}}}),n=await s.json().catch(()=>({}));if(!s.ok||n.error)throw Object.assign(new Error(n.message||n.error||`HTTP ${s.status}`),{code:n.error});this._backupImportDialog&&(this._backupImportDialog={...this._backupImportDialog,token:n.token,preview:n.preview,expectedConfigRev:n.expected_config_rev,expectedLayoutRev:n.expected_layout_rev,busy:!1})}catch(e){this._backupImportDialog&&(this._backupImportDialog={...this._backupImportDialog,busy:!1,error:this._backupErrorText(e)})}}}async _setBackupDuplicatePolicy(e){const t=this._backupImportDialog;if(t&&!t.busy&&t.token&&e!==t.duplicatePolicy){this._backupImportDialog={...t,duplicatePolicy:e,busy:!0,error:""};try{const i=await this.hass.callWS({type:"houseplan/import/revalidate",token:t.token,duplicate_policy:e});this._backupImportDialog&&(this._backupImportDialog={...this._backupImportDialog,preview:{...t.preview,...i.preview},expectedConfigRev:i.expected_config_rev,expectedLayoutRev:i.expected_layout_rev,confirmMissing:!1,busy:!1})}catch(e){this._backupImportDialog&&(this._backupImportDialog={...this._backupImportDialog,duplicatePolicy:t.duplicatePolicy,preview:t.preview,confirmMissing:t.confirmMissing,busy:!1,error:this._backupErrorText(e)})}}}async _applyBackupImport(){const e=this._backupImportDialog;if(!e||e.busy||!e.token||!e.preview)return;if(e.preview.confirmation_required&&!e.confirmMissing)return;this._backupImportDialog={...e,busy:!0,error:""};const t=this._space;try{const i=await this.hass.callWS({type:"houseplan/import/apply",token:e.token,expected_config_rev:e.expectedConfigRev,expected_layout_rev:e.expectedLayoutRev,duplicate_policy:e.duplicatePolicy,confirm_missing_content:e.confirmMissing}),[s,n]=await Promise.all([this.hass.callWS({type:"houseplan/config/get"}),this.hass.callWS({type:"houseplan/layout/get"})]);this._adoptStructuralResponses(s,n),this._geometryHistory.clear(),this._dirtyPos.clear(),this._sentPos.clear(),this._defPos={},this._cfgEpoch++,this._modelCache=null,this._frame=null,this._visibleDeviceSnapshot=null,this._candidateDeviceSnapshot=null,this._stagedDeviceSnapshotToken=-1,this._capturedSnapshotConfigEpoch=-1,this._regSignature="",this._signer.invalidate(this.hass),this._resign(),this._maybeRebuildDevices();const o=this._serverCfg?.spaces||[],a="space"===i.kind&&i.space_id?i.space_id:o.some(e=>e.id===t)?t:o[0]?.id||this._space;this._hasFixedFloor?this._adoptInitialSpace(this._model,!0):this._commitSpace(a),this._backupImportDialog=null,this._cacheSnapshot(),this.requestUpdate();const r="space"===i.kind?e.preview.counts:i.counts;this._showToast(this._t("space"===i.kind?"backup.space_done":"backup.full_done",{spaces:String(r?.spaces||0),rooms:String(r?.rooms||0),markers:String(r?.markers||0),refs:String(i.repaired_target_refs||0)}))}catch(e){if("conflict"===e?.code&&this._backupImportDialog?.token)try{const e=this._backupImportDialog,t=await this.hass.callWS({type:"houseplan/import/revalidate",token:e.token,duplicate_policy:e.duplicatePolicy});return void(this._backupImportDialog={...e,preview:{...e.preview,...t.preview},expectedConfigRev:t.expected_config_rev,expectedLayoutRev:t.expected_layout_rev,confirmMissing:!1,busy:!1,error:this._t("backup.revalidated")})}catch(t){e=t}this._backupImportDialog&&(this._backupImportDialog={...this._backupImportDialog,busy:!1,error:this._backupErrorText(e)})}}_renderBackupExportDialog(){const e=this._backupExportDialog,t=(this._serverCfg?.spaces||[]).find(e=>e.id===this._space);return q`this._backupExportDialog=null}> -
-
${this._t("backup.export_hint")}
- - - ${"space"===e.kind&&t?q``:G} -
${this._t("backup.privacy_warning")}
- ${e.error?q``:G} -
-
- - - -
-
`}_renderBackupImportDialog(){const e=this._backupImportDialog,t=e.preview,i=t?.counts||{},s=t?.reference_report||{},n=e=>Object.values(e||{}).reduce((e,t)=>e+(Number(t)||0),0),o=[["incoming_remapped",n(s.remapped?.incoming)],["target_repaired",n(s.remapped?.target)],["preserved_unresolved",n(s.preservedUnresolved)],["collisions",n(s.collisions)],["dropped_links",n(s.droppedIncomingLinks)],["bounded_lineages",Number(s.boundedLineages)||0]].filter(([,e])=>e>0);return q`this._backupImportDialog=null}> -
-
${e.filename}${(e.size/1024).toFixed(1)} KB
- ${e.busy&&!t?q`
${this._t("backup.reading")}
`:G} - ${e.error?q``:G} - ${t?q` -
- ${this._t("full"===t.kind?"backup.full":"backup.current_space")} - ${t.plan_only?q`${this._t("backup.plan_only_preview")}`:G} - ${this._t("same"===t.source?"backup.same_source":"backup.foreign_source")} - ${this._t("backup.created",{value:t.created_at||"—"})} - ${this._t("backup.versions",{card:t.card_version||"—",integration:t.integration_version||"—",model:String(t.model_version??"—")})} -
-
- ${this._t("backup.count_spaces",{n:"full"===t.kind?`${t.current_counts?.spaces||0} → ${i.spaces||0}`:String(i.spaces||0)})} - ${this._t("backup.count_rooms",{n:"full"===t.kind?`${t.current_counts?.rooms||0} → ${i.rooms||0}`:String(i.rooms||0)})} - ${this._t("backup.count_walls",{n:"full"===t.kind?`${t.current_counts?.walls||0} → ${i.walls||0}`:String(i.walls||0)})} - ${this._t("backup.count_openings",{n:"full"===t.kind?`${t.current_counts?.openings||0} → ${i.openings||0}`:String(i.openings||0)})} - ${this._t("backup.count_decor",{n:"full"===t.kind?`${t.current_counts?.decor||0} → ${i.decor||0}`:String(i.decor||0)})} - ${this._t("backup.count_markers",{n:"full"===t.kind?`${t.current_counts?.markers||0} → ${i.markers||0}`:String(i.markers||0)})} - ${this._t("backup.count_layout",{n:"full"===t.kind?`${t.current_counts?.layout||0} → ${i.layout||0}`:String(i.layout||0)})} -
- ${t.bindings?q`
${this._t("backup.bindings",{device:String(t.bindings.device||0),entity:String(t.bindings.entity||0),virtual:String(t.bindings.virtual||0),legacy:String(t.legacy_positions||0)})}
${this._t("backup.binding_status",{active:String(t.bindings.active||0),disabled:String(t.bindings.disabled||0),missing:String(t.bindings.missing||0)})}
`:G} - ${t.missing_areas?.length?q`
${this._t("backup.missing_areas",{areas:t.missing_areas.join(", ")})}
`:G} - ${t.dropped_marker_links?q`
${this._t("backup.dropped_marker_links",{n:String(t.dropped_marker_links)})}
`:G} - ${t.repaired_target_refs?q`
${this._t("backup.repaired_target_refs",{n:String(t.repaired_target_refs)})}
`:G} - ${t.preserved_unresolved_refs?q` -
${this._t("backup.preserved_unresolved_refs",{n:String(t.preserved_unresolved_refs)})}
${this._t("backup.preserved_unresolved_hint")}
- `:G} - ${o.length?q`
- ${this._t("backup.import_details")} -
- ${o.map(([e,t])=>q`${this._t(`backup.import_detail.${e}`,{n:String(t)})}`)} - ${(s.examples||[]).slice(0,8).map(e=>q` - ${e.owner} → ${e.reference} - `)} -
-
`:G} - ${"full"===t.kind?q` -
${this._t("backup.replace_warning")}
- ${"foreign"===t.source?q`
${this._t("backup.foreign_bookkeeping")}
`:G}`:q` -
${this._t("backup.final_name")}${t.space_title}
-
${this._t("backup.target_settings")}
- ${t.duplicates?q`
${this._t("backup.duplicates",{n:String(t.duplicates)})} - - -
`:G}`} - ${t.content?.length?q`
- ${this._t("backup.content")} - ${t.content.map(e=>q`${e.url} · ${this._t("available"===e.state?"backup.content_available":"external"===e.state?"backup.content_external":"backup.content_detach_required")}`)} -
`:G} - ${t.confirmation_required?q``:G} - `:G} -
-
- - - ${t?q``:G} -
-
`}_setFillColor(e,t){const i=this._settingsDialog;this._settingsDialog={...i,colors:{...i.colors,[e]:{...i.colors[e],...t}}}}async _saveSettingsDialog(){const e=this._settingsDialog;if(e&&!e.busy){this._settingsDialog={...e,busy:!0};try{const t=this._serverCfg,i=JSON.stringify(e.colors)===JSON.stringify(ds),s={...t.settings};i?delete s.fill_colors:s.fill_colors=e.colors;const n=this._imperial?30.48*e.glowRadius:100*e.glowRadius;Number.isFinite(n)&&n>0&&300!==Math.round(n)?s.glow_radius_cm=Math.round(n):delete s.glow_radius_cm,e.bgColor?s.bg_color=e.bgColor:delete s.bg_color,null!==e.northDeg&&Number.isInteger(e.northDeg)&&e.northDeg>=0&&e.northDeg<=359?s.north_deg=e.northDeg:delete s.north_deg,s.bg_mode=e.bgMode,e.sunRays?s.sun_rays=!0:delete s.sun_rays,delete s.weather_entity,this._serverCfg={...t,settings:s},await this._saveConfigNow(),this._settingsDialog=null,this.requestUpdate(),this._showToast(this._t("gs.saved"))}catch(e){this._settingsDialog&&(this._settingsDialog={...this._settingsDialog,busy:!1}),this._showToast(this._t("toast.error",{err:this._errText(e)}))}}}_boolInput(e,t,i=!1){const s=e=>t(!!e.target.checked);return customElements.get("ha-switch")?q``:q``}_rangeInput(e,t,i,s,n,o=!1,a){const r=e=>{const t=Number(e.target.value);Number.isFinite(t)&&n(t)};return customElements.get("ha-slider")?q``:q``}_renderColorRow(e,t){const i=this._settingsDialog.colors[e];return q`
- {this._setFillColor(e,{c:t.detail.color,a:t.detail.opacity})}}> -
`}get _glowRadiusCm(){const e=Number(this._settings.glow_radius_cm);return Number.isFinite(e)&&e>0?e:300}get _imperial(){return"mi"===this.hass?.config?.unit_system?.length}get _glowRadiusPlaceholder(){const e=this._glowRadiusCm;return this._imperial?String(Math.round(e/30.48*10)/10):String(e/100)}_glowTransition(e,t){let i=this._glowRenderedSources.get(e);if(t){const t=this._glowFadeTimers.get(e);if(null!=t&&(clearTimeout(t),this._glowFadeTimers.delete(e)),null==i){this._suspendGlowFeatherForTransition(),i=++this._glowSourceSeq,this._glowRenderedSources.set(e,i),this._glowEnteringSources.add(e);const t=requestAnimationFrame(()=>{this._glowEnterRafs.get(e)===t&&(this._glowEnterRafs.delete(e),this._glowEnteringSources.delete(e),this.isConnected&&this.requestUpdate())});this._glowEnterRafs.set(e,t)}return{domId:i,entering:this._glowEnteringSources.has(e),leaving:!1}}if(null==i)return null;const s=this._glowEnterRafs.get(e);if(null!=s&&cancelAnimationFrame(s),this._glowEnterRafs.delete(e),this._glowEnteringSources.delete(e),!this._glowFadeTimers.has(e)){this._suspendGlowFeatherForTransition();const t=window.setTimeout(()=>{this._glowFadeTimers.get(e)===t&&(this._glowFadeTimers.delete(e),this._glowRenderedSources.delete(e),this._glowLastAppearance.delete(e),this.isConnected&&this.requestUpdate())},534);this._glowFadeTimers.set(e,t)}return{domId:i,entering:!1,leaving:!0}}_suspendGlowFeatherForTransition(){if(window.matchMedia?.("(prefers-reduced-motion: reduce)").matches)return;this._glowFeatherSuspendUntil=Math.max(this._glowFeatherSuspendUntil,Date.now()+500),clearTimeout(this._glowFeatherResumeTimer);const e=Math.max(0,this._glowFeatherSuspendUntil-Date.now())+17,t=()=>{this._glowFeatherResumeTimer=0,Date.now()=128){const e=this._glowGeometryWarnings.values().next().value;e&&this._glowGeometryWarnings.delete(e)}this._glowGeometryWarnings.add(n),console.warn(`HOUSEPLAN GLOW GEOMETRY FALLBACK: #218, space ${e}, room ${i}, phase ${s}`)}}_lightBarriers(e,t){const i=Jd([this._curSpaceCfg,this._cellCm,this._gridPitch]),s=`${e.id}|${i}`,n=Sv(this._lightBarrierPool,s);if(n.hit)return this._lightBarrierCache=n.value,n.value.value;const o=this._zeroWalls(),a=this._openCuts(),r=[...o.transmissive],l=Math.max(this._cmToUnits(10),.5*this._gridPitch),c=e=>t.some(t=>this._pointInRoom(e,t.r)),h=this._openingsR.filter(e=>{if("door"!==(t=String(e.type))&&"gate"!==t&&"passage"!==t)return!1;var t;const i=e.angle*Math.PI/180,s=-Math.sin(i),n=Math.cos(i);return c([e.rx+s*l,e.ry+n*l])&&c([e.rx-s*l,e.ry-n*l])}),d=this._roomWallOpeningInputs(h,e);for(const e of d){const t=e.angle*Math.PI/180,i=Math.cos(t)*e.length/2,s=Math.sin(t)*e.length/2;r.push([e.x-i,e.y-s,e.x+i,e.y+s])}const p=new Set(h.filter(e=>e.partitionHost).map(e=>e.id)),u=[e.id,this._cfgEpoch,this._cellCm,this._gridPitch,[...p].sort().join(",")].join("|");this._lightPhysicalBodiesCache?.key!==u&&(this._lightPhysicalBodiesCache={key:u,all:_h(e,this._cellCm,this._gridPitch,2e-4*this._gridPitch,this._partitionOpeningCuts(e,e=>p.has(e.id))).all});const _=this._lightPhysicalBodiesCache.all,m=this._spaceWalls,g=.02*this._gridPitch,f=[],v=this._wallUnionGeometry(),b=v?.sourceFingerprint,y=v&&b===i?function(e,t=[],i=[],s={}){if("failed-core"===e.status||"not-applicable"===e.status)return null;const n=e.roomComponents?.length?e.roomComponents:Pc(e.roomGeom)?[{id:"room-primary",geom:e.roomGeom}]:[];let o=n[0]?.geom||null;const a=n.slice(1).map(e=>({...e}));try{for(const i of t){if(!(i.length>0&&e.openingIndex))continue;const t=Xc(e.openingIndex,i,!0);if(!t.negative&&!t.positive)continue;const s=i.angle*Math.PI/180,n=Math.cos(s),r=Math.sin(s),l=-r,c=n,h=i.length/2,d=e.openingPadUnits??1.25*Math.max(e.depthUnits,1),p=Dc([[i.x-n*h-l*d,i.y-r*h-c*d],[i.x+n*h-l*d,i.y+r*h-c*d],[i.x+n*h+l*d,i.y+r*h+c*d],[i.x-n*h+l*d,i.y-r*h+c*d]]);o&&(o=oi(o,p));for(const e of a)e.geom=oi(e.geom,p)}const n=[...a];let r=a.length;const l=s.mergeExtra||((e,t)=>e?si(e,t):t);for(let e=0;ee.length>=2&&Number.isFinite(e[0])&&Number.isFinite(e[1]))||Math.abs(Ti(t))<=1e-9){r++;continue}const s=[Dc(t)];try{const t=l(o,s,e);if(!Pc(t))throw new Error("invalid extra union");o=t}catch{r++,Pc(s)&&n.push({id:`policy-extra-${e}`,geom:s})}}n.sort((e,t)=>Lc(e.geom).localeCompare(Lc(t.geom)));const c=o||[];return{status:r?"degraded-extra":"ok",geom:c,components:[...Pc(c)?[{id:"primary",geom:c}]:[],...n.map((e,t)=>({...e,id:`isolated-${t}`}))]}}catch{return null}}(v,d,_):null,w=y||(m.length||_.length?qc(e.rooms,m,a,d,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov,_):null);if(!w||"ok"!==w.status&&"degraded-extra"!==w.status)for(const e of _)f.push(...m_(e));else for(const e of w.components)for(const t of wh(e.geom))f.push(...m_(t));for(const{poly:e}of t)for(const t of r.length?Ps(e,r,g):m_(e))f.push(t);for(const e of o.barriers)f.push(e);const k={occluders:__(f),floor:t.map(e=>e.poly),fingerprint:i,masonryGeometry:!w||"ok"!==w.status&&"degraded-extra"!==w.status?[]:w.components.flatMap(e=>e.geom),opaqueBodies:_},x={key:s,value:k};return Mv(this._lightBarrierPool,s,x,8),this._lightBarrierCache=x,k}_renderGlowLayer(e,t){const i=this._fillColors,s=this._glowRadiusCm/this._cellCm*this._gridPitch,n=e.rooms.map(e=>({r:e,poly:hi(e)})).filter(e=>!!e.poly),o=n.filter(({r:e})=>Fs(t.glow,e));if(!o.length)return this._forgetGlowSpace(e.id),W``;const{occluders:a,floor:r,fingerprint:l,masonryGeometry:c,opaqueBodies:h}=this._lightBarriers(e,n),d=ho(this._renderPlanHass,this._renderDevices,null,this._virtualLights).filter(t=>t.device.space===e.id),p=new Map;for(const e of d){if(!e.device.id)continue;const t=p.get(e.device.id)||[];t.push(e),p.set(e.device.id,t)}const u=[],_=new Set;for(const t of this._renderDevices){if(t.space!==e.id)continue;const o=po(p.get(t.id)||[]);if(!o)continue;const d=`${e.id}|${t.id}`;_.add(d);const m=$s(o.passive?{state:o.on?"on":"off",attributes:{}}:this._renderPlanHass.states[o.eid],t.marker?.glow_color,i.glow_light.c),g=Number(t.marker?.glow_radius_cm),f=Number.isFinite(g)&&g>0?g/this._cellCm*this._gridPitch:s,v=this._pos(t);if($h([v.x,v.y],c,h)){this._forgetGlowSource(d);continue}const b=this._glowTransition(d,!!m);if(!b)continue;m&&this._glowLastAppearance.set(d,{c:m.c,alpha:Ss(m.bri,i.glow_light.a)});const y=this._glowLastAppearance.get(d);if(!y)continue;let w=null;const k=`${e.id}|${l}|${v.x.toFixed(4)},${v.y.toFixed(4)}|${f.toFixed(4)}`,x=Sv(this._glowClipCache,k);if(x.hit)w=x.value;else{const t=v_([v.x,v.y],f,a,96);w={lit:t.length>=3?vh([t],r,{onBoundsFailure:({boundIndex:t,phase:i})=>{const s=n[t]?.r;this._warnGlowGeometryFallback(e.id,l,s?.id||`#${t}`,i)}}):[]},Mv(this._glowClipCache,k,w,256)}u.push({key:d,sourceEid:o.eid,domId:b.domId,entering:b.entering,leaving:b.leaving,pos:v,c:y.c,alpha:y.alpha,geometry:w,r:f})}const m=`${e.id}|`;for(const e of this._glowRenderedSources.keys())e.startsWith(m)&&!_.has(e)&&this._forgetGlowSource(e);if(!u.length)return W``;const g=this._spaceWalls,f=o.length===n.length?[]:this._openCuts(),v=this._wallUnionGeometry()?.roomGeom,b=o.length===n.length?null:o.map(({r:t,poly:i})=>{const s=g.length&&t.id&&this._innerRoomContour(e,t.id,f,v)||i,o=this._cleanFloor(t,s,e).path,a=Pi(s,n.filter(e=>e.r!==t).map(e=>e.poly)),r=e=>"M "+e.map(e=>e[0]+" "+e[1]).join(" L ")+" Z";return[o||r(s),...a.map(r)].join(" ")}),y=this._viewOr(this._baseVb()),w=this._stageEl?.clientWidth&&y.w?this._stageEl.clientWidth/y.w:1,k=1/(w>0?w:1),x=!this._pinchStart&&!this._panStart&&Date.now()>=this._glowFeatherSuspendUntil;(null==this._glowFeatherUnits||x)&&(this._glowFeatherUnits=k);const $=this._glowFeatherUnits??k,S=4*$,M=u.reduce((e,t)=>({x:Math.min(e.x,t.pos.x-t.r-S),y:Math.min(e.y,t.pos.y-t.r-S),maxX:Math.max(e.maxX,t.pos.x+t.r+S),maxY:Math.max(e.maxY,t.pos.y+t.r+S),w:0,h:0}),{x:1/0,y:1/0,maxX:-1/0,maxY:-1/0,w:0,h:0});return M.w=M.maxX-M.x,M.h=M.maxY-M.y,W` - ${Me(u,e=>e.key,e=>{const t=e.domId,i=e.geometry;return W` - ${""} - - ${Hv.map(([t,i])=>W` - `)} - - ${""} - ${i?W` - - - `:G}`})} - ${b?W`${b.map(e=>W``)}`:G} - ${""} - - - - - ${""} - - - ${Me(u,e=>e.key,e=>{const t=e.domId;return W` - - - `})} - - `}_renderAlignDialog(){const e=this._alignDialog,t=e.report,i=e.changed&&!e.preflight?.ok,s=e.preflight?.failures||[],n=s.slice(0,3).map(e=>e.displayName),o=n.length?n.join(", "):this._t("gs.align_preflight_space",{n:"1"}),a=Math.max(0,s.length-n.length),r=a?this._t("gs.align_preflight_more",{n:String(a)}):"",l=t.spaceRefsRemapped+t.roomRefsRemapped+t.positionsRemapped+t.markersDetached,c=t.migrated+t.canonicalized+t.coordsCanonicalized+t.wallSegmentsMigrated+t.wallsMerged+t.spansMerged+t.partitionsMerged+t.partitionsReconciled+t.openingsRehosted+t.redundantDraftsRemoved,h=t.moved+t.rotated+t.removedDrafts+t.coordsCanonicalized+t.wallsStraightened,d=Math.ceil(10*t.maxStraightenShiftCm)/10,p=(this._serverCfg?.spaces||[]).find(e=>String(e?.id||"")===t.maxStraightenSpace),u=(this._serverCfg?.spaces||[]).length>1&&p?String(p.title||p.id):"",_=t.orphanRoomLabelsRemoved+t.orphanDevicePositionsRemoved+t.orphanGroupPositionsRemoved,m=t.liveMissingPositions.map(e=>e.name).filter(Boolean),g=m.slice(0,3).join(", "),f=Math.max(0,m.length-3),v=g?this._t("gs.optimize_live_names",{names:g,more:f?this._t("gs.optimize_reference_more",{n:String(f)}):""}):"",b=t.unverifiedPositions.some(e=>"registry_unavailable"===e.reason),y=e=>t.removedPositions.some(t=>t.id===e.id)?this._t("gs.optimize_detail_removed"):t.liveMissingPositions.some(t=>t.id===e.id)?this._t("gs.optimize_detail_live"):this._t("gs.optimize_detail_unverified"),w=e=>this._t("room_label"===e?"gs.optimize_detail_room_label":"group"===e?"gs.optimize_detail_group":"device"===e?"gs.optimize_detail_device":"gs.optimize_detail_unknown"),k=[...t.removedPositions,...t.liveMissingPositions.filter(e=>!t.removedPositions.some(t=>t.id===e.id)),...t.unverifiedPositions],x=k.slice(0,10),$=Math.max(0,k.length-x.length);return q`{this._alignDialog=null,this._preflightClipboardFallback=null}}> -
- ${i?q` -

${this._t("gs.align_preflight_failed",{spaces:o,more:r})}

- ${s.slice(0,10).map(e=>q`

- ${e.displayName}: ${this._t(`gs.preflight_reason_${e.reason}`)} -

`)} - ${s.length>10?q`

- ${this._t("gs.align_preflight_more",{n:String(s.length-10)})} -

`:G} -
${this._t("gs.align_preflight_hint")}
- ${this._preflightVersionsDiffer()?q` -
${this._t("gs.preflight_update_hint")}
`:G} -
- -
- ${this._preflightClipboardFallback?q`
- ${this._t("gs.preflight_copy")} -
${this._preflightClipboardFallback}
-
`:G}`:e.changed?q` - ${t.moved?q`

${this._t("gs.align_count",{n:String(t.moved),total:String(t.total),cm:String(e.cm)})}

`:G} - ${t.latticeCoordinatesCanonicalized?q` -

${this._t("gs.optimize_lattice_summary",{n:String(t.latticeCoordinatesCanonicalized),cm:ol(t.latticeMaxShiftCm)})}

- ${t.latticeSpaces.map(e=>q`

${this._t("gs.optimize_lattice_space",{space:e.space,n:String(e.canonicalized),far:String(e.far)})}

`)} - `:G} - ${e.where?q`

${this._t("gs.align_where",{s:e.where})}

`:G} - ${t.rotated?q`

${this._t("gs.align_turned",{n:String(t.rotated)})}

`:G} - ${t.removedDrafts?q`

${this._t("gs.align_removed_drafts",{n:String(t.removedDrafts)})}

`:G} - ${t.redundantDraftsRemoved?q`

${this._t("gs.optimize_redundant_drafts",{n:String(t.redundantDraftsRemoved)})}

`:G} - ${t.wallSegmentsMigrated?q`

${this._t("gs.wall_segments_migrated",{n:String(t.wallSegmentsMigrated)})}

`:G} - ${t.legacyZeroWallsMigrated?q`

${this._t("gs.zero_walls_migrated",{n:String(t.legacyZeroWallsMigrated)})}

`:G} - ${c?q`

${this._t("gs.optimize_changes",{m:String(t.migrated),c:String(t.canonicalized),p:String(t.coordsCanonicalized),w:String(t.wallsMerged),s:String(t.spansMerged),i:String(t.partitionsMerged)})}

`:G} - ${t.partitionsReconciled?q`

${this._t("gs.optimize_coincident_partitions",{n:String(t.partitionsReconciled)})}

`:G} - ${t.openingsRehosted?q`

${this._t("gs.optimize_openings_rehosted",{n:String(t.openingsRehosted)})}

`:G} - ${t.wallsStraightened?q`

${this._t("gs.optimize_walls_straightened",{n:String(t.wallsStraightened),cm:String(d)})}

`:G} - ${u?q`

${this._t("gs.optimize_walls_straightened_where",{s:u})}

`:G} - ${t.glowSpacesMigrated||t.glowRoomsMigrated?q`

${this._t("gs.optimize_glow_migration",{spaces:String(t.glowSpacesMigrated),rooms:String(t.glowRoomsMigrated)})}

`:G} - ${h?q`
${this._t("gs.align_warn")}
`:G}`:q`

${this._t(t.liveMissingPositions.length||t.unverifiedPositions.length||t.nestedRefsUnresolved?"gs.optimize_no_automatic_changes":"gs.align_none")}

`} - ${!i&&t.wallsStraightenSkipped?q`

${this._t("gs.optimize_walls_straighten_skipped",{n:String(t.wallsStraightenSkipped)})}

`:G} - ${l?q`

${this._t("gs.optimize_references",{spaces:String(t.spaceRefsRemapped),rooms:String(t.roomRefsRemapped),positions:String(t.positionsRemapped),detached:String(t.markersDetached)})}

`:G} - ${_?q`

${this._t("gs.optimize_orphans_removed",{total:String(_),rooms:String(t.orphanRoomLabelsRemoved),devices:String(t.orphanDevicePositionsRemoved),groups:String(t.orphanGroupPositionsRemoved)})}

`:G} - ${t.liveMissingPositions.length?q`
-

${this._t(e.removeLiveMissingPositions?"gs.optimize_live_positions_remove":"gs.optimize_live_positions",{n:String(t.liveMissingPositions.length),names:v})}

- - ${e.removeLiveMissingPositions?q`
- ${this._t("gs.optimize_live_selected")} -
`:G} -
`:G} - ${t.unverifiedPositions.length?q``:G} - ${t.nestedRefsUnresolved?q``:G} - ${k.length?q`
- ${this._t("gs.optimize_details")} -
    - ${x.map(e=>q`
  • ${this._t("gs.optimize_detail_item",{status:y(e),kind:w(e.kind),id:e.id,space:e.spaceId})}
  • `)} -
- ${$?q`
${this._t("gs.optimize_details_more",{n:String($)})}
`:G} -
`:G} -
-
- - - ${e.changed&&e.preflight?.ok?q` - `:G} -
-
`}_renderSettingsDialog(){return q`this._settingsDialog=null}> -
-
${this._t("gs.hint")}
- - ${this._renderColorRow("light_on","gs.light_on")} - ${this._renderColorRow("light_off","gs.light_off")} - ${this._renderColorRow("light_none","gs.light_none")} - - ${this._renderColorRow("temp_cold","gs.temp_cold")} - ${this._renderColorRow("temp_ok","gs.temp_ok")} - ${this._renderColorRow("temp_hot","gs.temp_hot")} - - ${this._renderColorRow("lqi_low","gs.lqi_low")} - ${this._renderColorRow("lqi_high","gs.lqi_high")} - - ${this._renderColorRow("glow_base","gs.glow_base")} - ${this._renderColorRow("glow_light","gs.glow_light")} -
- ${this._t("gs.glow_radius")} - {const t=$v(e.target.value);null!=t&&t>0&&(this._settingsDialog={...this._settingsDialog,glowRadius:t})}} /> - ${this._imperial?this._t("gs.unit_ft"):this._t("gs.unit_m")} -
- - ${this._renderColorRow("wall_fill","gs.wall_fill")} - -
- ${this._t("gs.bg_mode")} - -
- ${"static"===this._settingsDialog.bgMode?q`
- {this._settingsDialog={...this._settingsDialog,bgColor:e.detail.color}}}> - ${this._settingsDialog.bgColor?q``:q`${this._t("gs.bg_theme")}`} -
`:q`
${this._t("gs.bg_daynight_hint")}
`} - - ${Er(this.hass)?G:q`
${this._t("gs.sun_missing")}
`} -
- ${this._renderCompass()} -
- ${this._t("gs.north")} -
- {const t=e.target.value.trim(),i=""===t?null:Math.round(Number(t));this._settingsDialog={...this._settingsDialog,northDeg:null!==i&&Number.isFinite(i)?Math.min(359,Math.max(0,i)):null}}} /> - ${null!==this._settingsDialog.northDeg?q``:G} -
- ${null===this._settingsDialog.northDeg?q`
${this._t("gs.north_hint")}
`:G} -
-
- - ${this._canEdit?q` - -
${this._t("gs.backup_hint")}
-
- - - - - - ${this._canOptimizeUndo&&"import"===this._undoKind?q` - `:G} -
`:G} - -
${this._t("gs.grid_hint")}
-
- -
- ${this._canOptimizeUndo&&"import"!==this._undoKind?q`
- -
`:G} - -
${this._t("gs.about_version",{v:vv})}
- - ${this._t("gs.about_github")} - - ${this._t("gs.about_telegram")} -
-
- - - - -
-
`}_rulesSet(e){this._rulesDialog={...this._rulesDialog,rules:e}}async _saveRules(){const e=this._rulesDialog;if(!e||e.busy)return;const t=e.rules.filter(e=>e.pattern.trim()&&e.icon.trim());this._rulesDialog={...e,busy:!0};try{const e=this._serverCfg,i=JSON.stringify(t)===JSON.stringify(Je),s={...e.settings};i?delete s.icon_rules:s.icon_rules=t,this._serverCfg={...e,settings:s},await this._saveConfigNow(),this._rulesDialog=null,this._regSignature="",this._maybeRebuildDevices(),this._showToast(this._t("rules.saved"))}catch(e){this._rulesDialog&&(this._rulesDialog={...this._rulesDialog,busy:!1}),this._showToast(this._t("toast.error",{err:this._errText(e)}))}}_renderRulesDialog(){const e=this._rulesDialog,t=Xe(e.rules),i=e.test.trim()?tt(e.test,"",t):null,s=(t,i)=>{const s=[...e.rules],n=t+i;n<0||n>=s.length||([s[t],s[n]]=[s[n],s[t]],this._rulesSet(s))};return q`this._rulesDialog=null}> -
-
${this._t("rules.hint")}
-
- this._rulesDialog={...e,test:t.target.value}} /> - ${i?q`${i}`:G} -
- ${e.rules.map((t,i)=>{const n=""!==t.pattern.trim()&&!function(e){try{return new RegExp(e,"i"),!0}catch{return!1}}(t.pattern);return q`
- {const n=[...e.rules];n[i]={...t,pattern:s.target.value},this._rulesSet(n)}} /> - {const n=[...e.rules];n[i]={...t,icon:s.target.value},this._rulesSet(n)}} /> - - s(i,-1)}> - s(i,1)}> - this._rulesSet(e.rules.filter((e,t)=>t!==i))}> -
`})} - -
-
- - - - -
-
`}_saveKioskScale(e){this._kioskScale={...this._kioskScale,...e};try{localStorage.setItem(Fv,JSON.stringify(this._kioskScale))}catch{}this.requestUpdate()}_renderKioskDialog(){const e=this._kioskScale,t=(t,i)=>q` -
- ${this._rangeInput(50,300,5,Math.round(100*e[t]),e=>this._saveKioskScale({[t]:e/100}))} - ${Math.round(100*e[t])}% -
`;return q`this._kioskDialog=!1}> -
-
${this._t("kiosk.hint")}
- ${t("icon",this._t("kiosk.icon_scale"))} - ${t("font",this._t("kiosk.font_scale"))} -
-
- - - -
-
`}_fixedFloorValue(e){if("string"==typeof e)return e||"''";try{const t=JSON.stringify(e);return(void 0===t?String(e):t).slice(0,160)}catch{return String(e).slice(0,160)}}render(){if(!this._config||!this.hass)return G;const e=this._model,t=this.houseplanDiagnostics(),i=this._fixedFloorState(e);if("pending"===i.kind)return q` -
-
${this._config.title||this._t("card.title")}
-
-
- -

${this._t("fixed_floor.loading")}

-
-
`;if("invalid"===i.kind)return q` -
-
${this._config.title||this._t("card.title")}
-
- -
`;if(!e.length)return q` -
-
${this._config.title||this._t("card.title")}
-
-
- -

${this._t("empty.no_spaces")}

- ${this._serverStorage?q`

${this._t("empty.add_first")}

- `:q`

${this._t("empty.install")}

`} -
- ${this._spaceDialog?this._renderSpaceDialog():G} - ${this._importDialog?this._renderImportDialog():G} - ${this._toast?q``:G} -
`;const s=this._spaceModel();if(!s)return G;const n="valid"===i.kind?[s]:e,o=s.vb,a=this._effectiveProjection();this._renderProjection=a,this._labs.active.length&&function(e){$f||Tf(e);const t=xf,i=t.active.join(",");if(!i||i===Mf)return;Mf=i;const s=new Map(_f.map(e=>[e.id,e])),n=t.active.map(e=>{const t=s.get(e);return`${e} (#${t.issue}, expires ${t.expires})`}).join(", ");console.info(`HOUSEPLAN LABS: ${n}`)}(vv);const r="iso"===a,l="devices"===this._mode&&this._showAll,c=this._renderDevices.filter(e=>e.space===s.id&&(!e.hidden||l)),h=this._renderDeviceSnapshot,d=this._spaceDisplayForRender(),p=r?this._isoDecorationLayers(d):null,u=p?this._isoOpeningPanels(p):[],_=this._resolvedRoomFills(s,d),m=this._resolvedGlowBase(s,d,_),g=d.showLqi??this._config.show_signal??!0,f=this._config.icon_size??2.5,v=f>8?2.5:f,b=B_(v),y=this._viewOr(this._baseVb()),w=this._floorView(y),k=this._editing?"":this._stageBg(d),x=this._opMeasureView,$=this._decorMeasure,S=this._bdLive,M=this._furnLive,C="view"===this._mode?this._editorChromeMode:this._mode,D=this._roomHoverPaths(s),P=s.bg?this._display(s.bg.href):"",T=this._continuity.overlayVisible||"recovery-error"===this._continuity.state?this._continuity.recoveryReason:null,I=this._modeTransitionVisual,R=this._dayCycleState(),A=I?.viewWeight??("view"===this._mode?1:0),z=this._modeTransition.state?.from.presentedMode,F=!this._markup||!!I&&("view"===I.presentedMode||"devices"===I.presentedMode||"view"===z||"devices"===z),N=I?.stageColor||k,E=I?.sceneBrightness??1;return q` - this._notePointer(e)} - @pointerdown=${this._touchGestureGuard} - @pointermove=${this._touchGestureGuard} - @pointerup=${this._touchGestureGuard} - @pointercancel=${this._touchGestureGuard} - @lostpointercapture=${this._touchGestureGuard} - @click=${this._touchGestureGuard}> -
-
-
- - ${this._config.title||this._t("card.title")} -
-
this._tabPointerMove(e)}> - ${n.map(e=>q``)} - ${""} - ${!this._canEdit||this._kiosk||this._hasFixedFloor?G:q``} -
- ${this._canEdit?q`
- ${[["plan","mdi:floor-plan"],["devices","mdi:tune-variant"],["decor","mdi:draw"]].map(([e,t])=>q``)} -
`:G} - ${this._t("count.devices",{n:c.filter(e=>!e.hidden).length})} - - ${this._labsIso&&"view"===this._mode&&!this._kiosk?q``:G} -
- - ${""} - - -
- ${this._norm&&this._canEdit?q``:G} -
- ${this._canEdit&&!this._kiosk?q`
-
- ${"plan"===C?this._renderMarkupBar():"devices"===C?this._renderDevicesBar():this._renderDecorBar()} -
-
`:G} -
- -
this._markupClick(e)} - @wheel=${e=>this._onWheel(e)} - @pointerdown=${e=>{this._notePointer(e),this._stagePointerDown(e)}} - @pointermove=${e=>this._stagePointerMove(e)} - @pointerleave=${e=>this._stagePointerLeave(e)} - @pointerup=${e=>this._stagePointerUp(e)} - @pointercancel=${e=>this._stagePointerCancel(e)}> - ${Lr(R,A)} - ${this._renderEditorSecondary()} -
- ${r&&p?.structural?W``:G} - - - ${""} - ${this._wallHatchDefs(d.color)}${W`${this._paperShapes(s.rooms).map(e=>"path"in e?W``:"poly"in e?W``:W``)}`} - ${this._editing?this._renderMarkupDefs(o):G} - ${""} - ${this._editing&&!this._markup&&this._gridLevels()?W``:G} - ${s.bg&&P?W`this._onBackdropLoaded(s.bg.href,P)} - transform=${s.bg.angle?`rotate(${s.bg.angle} ${s.bg.x+s.bg.w/2} ${s.bg.y+s.bg.h/2})`:G} - @dblclick=${e=>this._openBackdropDialog(e)} - preserveAspectRatio="none" />`:G} - ${(()=>{const e=this._openCuts(),t=this._thickWallCuts(),i=this._wallUnionGeometry()?.roomGeom,n=new Map,o=e=>(n.has(e)||n.set(e,hi(e)),n.get(e));return s.rooms.filter(e=>e.area||"view"===this._mode||this._markup||d.showBorders).map(n=>{let a="room "+(s.bg?"overlay":"yard")+(this._markup?" outlined":"");!this._markup||n.id!==this._mergeSel&&n.id!==this._splitSel?.roomId||(a+=" picked");let r="";const l=zs(d.fill,n);if(!this._markup&&(d.showBorders||"none"!==l)){a+=" styled";const e=[];e.push(`--room-stroke:${d.color}`,`--room-stroke-op:${d.showBorders?d.opacity:0}`);const t=_.byRoom.get(n)||null;t?(a+=" filled",e.push(`--room-fill:${t.color}`,`--room-fill-op:${t.opacity}`)):e.push("--room-fill:transparent","--room-fill-op:0"),r=e.join(";")}let c;const h=e=>{"view"===this._mode&&(void 0===c&&(c=this._roomArea(n)),this._showTip(e,n.name||this._t("room.unnamed"),c?this._t("tip.area",{value:c}):"",g?this._roomLqi(n.area):null,this._roomTemp(n),this._roomHum(n)))},p=o(n),u=this._markup&&(n.id===this._mergeSel||n.id===this._splitSel?.roomId),m=p&&!u?e.filter(e=>{const t=[(e[0]+e[2])/2,(e[1]+e[3])/2];return p.some((e,i)=>Hs(t,[e[0],e[1],...p[(i+1)%p.length]])<=.08*this._gridPitch)}):[],f=u?[]:t,v=m.concat(f);v.length&&(a+=" noedge");const b=this._spaceWalls.length&&n.id&&p&&this._innerRoomContour(s,n.id,e,i)||p,y=b?Pi(b,(w=n,s.rooms.filter(e=>e!==w).map(o).filter(Boolean))):[];var w;const k=e=>"M "+e.map(e=>e[0]+" "+e[1]).join(" L ")+" Z",x=b?this._cleanFloor(n,b,s).path:"",$=n.id||G,S=n.area||G,M=x&&b?W`{this._notePointer(e),this._pointerModality.hoverEnabled&&(this._hoverRoom={space:s.id,room:n})}} - @pointermove=${h} - @pointerleave=${()=>this._clearTransientHover()}>`:y.length&&b?W`{this._notePointer(e),this._pointerModality.hoverEnabled&&(this._hoverRoom={space:s.id,room:n})}} - @pointermove=${h} - @pointerleave=${()=>this._clearTransientHover()}>`:b&&b!==p?W`{this._notePointer(e),this._pointerModality.hoverEnabled&&(this._hoverRoom={space:s.id,room:n})}} - @pointermove=${h} - @pointerleave=${()=>this._clearTransientHover()}>`:n.poly?W`{this._notePointer(e),this._pointerModality.hoverEnabled&&(this._hoverRoom={space:s.id,room:n})}} - @pointermove=${h} - @pointerleave=${()=>this._clearTransientHover()}>`:W`{this._notePointer(e),this._pointerModality.hoverEnabled&&(this._hoverRoom={space:s.id,room:n})}} - @pointermove=${h} - @pointerleave=${()=>this._clearTransientHover()}>`,C=v.length&&p?Ps(p,v,.02*this._gridPitch):null,D=C?W``:G;return W`${M}${D}`})})()} - ${this._renderRoomHoverFill(D)} - ${this._renderOpeningTunnelFills(s,_)} - ${this._renderGlowBaseRooms(s,m)} - ${this._renderOpeningTunnelFills(s,m,"glow-base")} - ${""} - ${d.hideDecor&&"decor"!==this._mode?G:this._renderDecorLayer()} - ${F?this._renderGlowLayer(s,d):G} - ${this._renderSunRays(s)} - ${this._editing?W`${this._renderAlignGuides()}`:G} - ${this._markup?W`${this._renderMarkupLayer(o)}`:G} - ${""} - ${""} - ${this._editing?G:this._renderZeroWalls(d)} - ${this._renderWallBodies(d)} - ${this._markup&&"resize"===this._tool?this._renderResizeMeasurements():G} - ${this._renderRoomHoverOutline(D)} - ${""} - ${this._editing?this._renderZeroWalls(d):G} - ${this._markup?W`${this._renderHiddenWallDiagnosticOverlay()}`:G} - ${this._markup?W`${this._renderOpeningPlacementPreview()}`:G} - ${x?this._renderOpeningDimensionGuides(x):G} - ${x?.guide?this._renderOpeningCenterTick(x.guide):G} - ${""} - ${""} - ${this._markup?W`${this._renderActiveChainInk()}`:G} - ${this._markup?W`${this._renderPlanSnapOverlay()}`:G} - ${d.hideOpenings&&!this._markup||p&&!p.floorSymbols?G:this._renderOpenings(d)} - ${this._renderWallThickUi()} - ${this._markup&&"resize"===this._tool?this._renderResizeLayer(y):G} - ${""} - ${this._renderBackdropFrame(y)} - ${this._renderTextFrame(y)} - - - ${r&&p?.structural?W` - `:G} - ${""} -
- ${c.map(e=>this._renderDevice(e,y,g))} - ${this._renderVacuums(c,y)} - ${this._renderVacFit(y)} - ${this._renderOpeningLocks(y)} - ${d.showNames||this._markup?s.rooms.map(e=>this._renderRoomLabel(e,s,y,d)):G} - ${this._markup?s.rooms.map(e=>this._renderRoomGear(e,s,y)):G} -
- ${this._measureAnchor?q`
${this._renderMeasureLabel(y)}
`:G} - ${this._resize.liveLabels?q`
${this._resize.liveLabels?.map(e=>q`
${e.text}
`)}
`:G} - ${x?q`
${x.labels.map(e=>q`
${e.text}
`)}
`:G} - ${this._wallDialog?q`
${this._renderWallThickDialog()}
`:G} - ${$?q`
${$.text}
`:G} - ${M?q`
${M.map(e=>q`
${e.text}
`)}
`:G} - ${S?q`
${S.text}
`:G} -
- ${this._zoom>1?q`
${Math.round(100*this._zoom)}%
`:G} - ${this._renderFarHint()} - ${this._renderHomeArrow()} - ${this._renderRecoveryOverlay()} - ${this._booting||this._bootFading?q``:G} -
- - ${this._roomDialog?this._renderRoomDialog():G} - ${this._mergeDialog?this._renderMergeDialog():G} - ${this._openingDialog?this._renderOpeningDialog():G} - ${this._physicalDialog?this._renderPhysicalDialog():G} - ${this._partitionDeleteDialog?this._renderPartitionDeleteDialog():G} - ${this._roomDeleteDialog?this._renderRoomDeleteDialog():G} - ${this._openingInfo?this._renderOpeningInfoCard():G} - ${this._decorTextDialog?this._renderDecorTextDialog():G} - ${this._decorShapeDialog?this._renderDecorShapeDialog():G} - ${this._backdropDialog?this._renderBackdropDialog():G} - ${this._decorEraseConfirm?this._renderDecorEraseConfirm():G} - ${this._spaceDialog?this._renderSpaceDialog():G} - ${this._deviceInbox?this._renderDeviceInbox():G} - ${this._markerDialog?this._renderMarkerDialog():G} - ${this._vacCalConfirm?q`this._vacCalConfirm=null}> -
-

${this._t("vac.residual_message",{error:this._vacCalConfirm.error})}

-
-
- - - - -
-
`:G} - ${this._infoCard?this._renderInfoCard():G} - ${this._rulesDialog?this._renderRulesDialog():G} - ${this._settingsDialog?this._renderSettingsDialog():G} - ${this._alignDialog?this._renderAlignDialog():G} - ${this._backupExportDialog?this._renderBackupExportDialog():G} - ${this._backupImportDialog?this._renderBackupImportDialog():G} - ${this._importDialog?this._renderImportDialog():G} - ${this._tip?q`
- ${this._tip.title}${this._tip.meta?q`${this._tip.meta}`:G} - ${null!=this._tip.temp?q`${this._t("tip.temp_avg")} ${this._tip.temp}°`:G} - ${null!=this._tip.hum?q`${this._t("tip.hum_avg")} ${this._tip.hum}%`:G} - ${null!=this._tip.lqi?q`${this._t("tip.lqi")} - ${this._tip.lqi}`:G} -
`:G} - ${this._kiosk&&!this._hasFixedFloor&&this._kioskDots&&this._model.length>1?q`
- ${this._model.map(e=>q``)} -
`:G} - ${this._kioskDialog?this._renderKioskDialog():G} - ${this._vacFit?q`
- ${this._t("vac.fit_hint")} - - - - -
`:G} - ${this._tapConfirm?q`this._tapConfirm=null}> -
- ${"run"===this._tapConfirm.kind?q`

${this._tapConfirm.text}

`:this._tapConfirm.lines.map((e,t)=>q` -

${e}

`)} -
-
- - - -
-
`:G} - ${this._toast?q``:G} -
- `}_vacCandidateStatus(e,t,i=this._planHass){const s=this._bindingStatus("entity:"+e);if("ha_disabled"===s.kind)return"disabled";if("orphaned"===s.kind)return"missing";if("unverified"===s.kind)return"unverified";const n=i?.states?.[e];return"unavailable"===n?.state?"unavailable":t?.hasPosition?"ok":"unsupported"}_vacOpenAllCameras(e){const t=this._haRegistry.entities||{},i=[];for(const[e,s]of Object.entries(this.hass?.states||{})){if(!e.startsWith("camera."))continue;const n=_p(e,s,t[e]);n&&i.push(n)}this._vacAllCameraCache={devId:e.id,candidates:i},this._vacAllCamerasFor=e.id}_vacSourceResolution(e,t=!1,i=this._planHass){const s=e.marker?.vacuum,n="string"==typeof s?.source&&!!s.source,o=new Set(e.entities||[]),a=new Set(o);n&&a.add(s.source);const r=i?.entities||{},l=[];for(const e of a){const t=i?.states?.[e],o=_p(e,t,r[e]);o?l.push(o):n&&e===s.source&&l.push({entityId:e,name:String(t?.attributes?.friendly_name||e),platform:r[e]?.platform?String(r[e].platform):null,category:e.startsWith("camera.")?"camera":"partial",hasPosition:!1,hasRooms:!1,hasPath:!1,hasMapId:!1,score:0})}if(t&&this._vacAllCameraCache?.devId===e.id){const e=new Set(l.map(e=>e.entityId));for(const t of this._vacAllCameraCache.candidates)e.has(t.entityId)||l.push(t)}const c={};for(const e of l)c[e.entityId]=this._vacCandidateStatus(e.entityId,e,i);return function(e,t,i,s){const n="string"==typeof e&&e.length>0,o=new Set(t),a=Array.from(i).sort((t,i)=>n&&t.entityId===e?-1:n&&i.entityId===e?1:i.score-t.score||t.entityId.localeCompare(i.entityId));if(n){const t=a.find(t=>t.entityId===e);return{entityId:e,status:s[e]||(t?.hasPosition?"ok":"unverified"),pinned:!0,candidates:a}}const r=a.find(e=>o.has(e.entityId)&&e.hasPosition&&"ok"===s[e.entityId]);return r?{entityId:r.entityId,status:"ok",pinned:!1,candidates:a}:{entityId:null,status:"none",pinned:!1,candidates:a}}(s?.source,o,l,c)}_vacSource(e,t=this._planHass){if(!1===e.marker?.vacuum?.live)return null;const i=this._vacSourceResolution(e,!1,t);return"ok"===i.status||"unsupported"===i.status?i.entityId:null}_vacEntity(e){return e.primary?.startsWith("vacuum.")?e.primary:(e.entities||[]).find(e=>e.startsWith("vacuum."))||null}_isVacDev(e){return!!this._vacEntity(e)}_activitySourceKey(e){return this._activitySnapshot(e).sourceKey}_activitySnapshot(e,t=ho(this._planHass,this._devices,null,this._virtualLights)){const i=I_(this._planHass,e,this._devices,t,this._fullRegistryHass);return{samples:i.samples,sourceKey:z_(this._planHass,e,i)}}_stampActivity(e,t,i){let s=this._activityRt.get(e);s||(s=_m(i||"",[]),this._activityRt.set(e,s)),null!=i&&(s.sources=i),gm(s,t,Date.now(),window.clearTimeout.bind(window),e=>window.setTimeout(()=>this.requestUpdate(),e))}_syncActivityRuntime(){const e=new Map;if(!this.hass)return e;if(!1===this._config?.live_states){for(const e of this._activityRt.values())clearTimeout(e.timer);return this._activityRt.clear(),e}const t=new Set,i=ho(this._planHass,this._devices,null,this._virtualLights);for(const s of this._devices){if(s.hidden)continue;if("icon_ripple"!==Bi(s.marker?.display))continue;t.add(s.id);const n=this._activitySnapshot(s,i);e.set(s.id,n);const{samples:o,sourceKey:a}=n;let r=this._activityRt.get(s.id);r?r.sources!==a&&mm(r,a,o,window.clearTimeout.bind(window)):(r=_m(a,o),this._activityRt.set(s.id,r))}for(const[e,i]of this._activityRt)t.has(e)||(clearTimeout(i.timer),this._activityRt.delete(e));return e}_activityTick(){if(!this.hass)return;const e=this._syncActivityRuntime();for(const t of this._devices){if(t.hidden)continue;if("icon_ripple"!==Bi(t.marker?.display))continue;const i=e.get(t.id)||this._activitySnapshot(t),{samples:s,sourceKey:n}=i,o=this._activityRt.get(t.id);if(!o||o.sources!==n)continue;const a=fm(o,s,window.clearTimeout.bind(window));a&&this._stampActivity(t.id,a,n)}}_vacTick(){if(this.hass)for(const e of this._devices){if(e.hidden||!this._isVacDev(e))continue;if("static_icon"===Bi(e.marker?.display)){this._vacRt.delete(e.id);continue}const t=this._vacSource(e);if(!t)continue;const i=this._vacEntity(e),s=vp(this.hass.states[i||""]?.state),n=up(this.hass.states[t]?.attributes);let o=this._vacRt.get(e.id);o||(o={trail:[],lastKey:"",lastTs:0,moving:!1,jump:!1,endedTs:0,lastPos:null},this._vacRt.set(e.id,o)),s&&!o.moving&&(o.trail=[],o.lastPos=null);const a="never"!==xp(e.marker?.vacuum)&&!n?.path.length;!s&&o.moving&&(o.endedTs=Date.now(),a&&o.lastPos&&(o.trail=fp(o.trail,o.lastPos,40)),o.lastPos=null),o.moving=s;const r=n?.pos;if(s&&r){const e=r.x+":"+r.y;if(e!==o.lastKey){const t=Date.now();o.jump=o.lastTs>0&&t-o.lastTs>1e4,o.lastKey=e,o.lastTs=t,a&&o.lastPos&&(o.trail=fp(o.trail,o.lastPos,40)),o.lastPos=[r.x,r.y]}}}}_vacEnsureMarker(e){const t=this._serverCfg;if(!t)return null;t.markers=t.markers||[];const i=t.markers.find(t=>t.id===e.id);if(i)return i;if("device"!==e.bindingKind&&"entity"!==e.bindingKind||!e.bindingRef)return null;const s={id:e.id,binding:e.bindingKind+":"+e.bindingRef,space:e.space||null,area:e.area||null,hidden:!!e.hidden};return t.markers.push(s),s}_renderVacSection(e){const t=this._devices.find(t=>t.id===e.devId);if(!t||!this._isVacDev(t))return G;const i=t.marker?.vacuum||{},s=this._vacAllCamerasFor===t.id,n=this._vacSourceResolution(t,s),o=n.entityId,a=n.candidates.find(e=>e.entityId===o)||null,r=o?up(this.hass?.states[o]?.attributes):null,l=!1!==i.live&&("ok"===n.status||"unsupported"===n.status),c=this._vacPlanRoomAnchors(t.space),h=r?function(e,t){const i=new Set(t.map(mp).filter(Boolean)),s=new Set;for(const t of e){const e=mp(t.name||"");e&&i.has(e)&&s.add(e)}return s.size}(r.rooms,c.map(e=>e.name)):0,d=!!(l&&r&&h>=3),p=Object.keys(i.calibration||{}),u=e=>{const i=this._vacEnsureMarker(t);i&&(i.vacuum={...i.vacuum||{},...e},this._regSignature="",this._maybeRebuildDevices(),this._saveConfig(),this.requestUpdate())},_=new Set(t.entities||[]),m=n.candidates.filter(e=>_.has(e.entityId)||e.entityId===i.source),g=s?n.candidates.filter(e=>e.entityId.startsWith("camera.")&&!m.some(t=>t.entityId===e.entityId)):[],f=e=>[e.hasPosition?this._t("vac.cap_position"):"",e.hasRooms?this._t("vac.cap_rooms_short"):"",e.hasPath?this._t("vac.cap_path"):"",e.hasMapId?this._t("vac.cap_map"):""].filter(Boolean).join(" · ")||this._t("vac.cap_none"),v=e=>q` - `,b=n.candidates.some(e=>_.has(e.entityId)&&"known_xcme_incomplete"===e.category)||!!n.pinned&&!!o?.startsWith("camera.")&&!a?.hasPosition;return q` - -
-
-
${this._t("vac.diag_source")}${o||this._t("vac.source_none")}
- ${a?.platform?q`
${this._t("vac.diag_platform")}${a.platform}
`:G} -
${this._t("vac.diag_status")}${this._t(`vac.source_status_${n.status}`)}
-
${this._t("vac.diag_position")}${r?.pos?this._t("common.yes"):this._t("common.no")}
-
${this._t("vac.diag_rooms")}${ss(this._t("vac.diag_rooms_value"),{total:String(r?.rooms.length||0),matched:String(h),readiness:this._t(h>=3?"vac.autocal_ready":"vac.autocal_not_ready")})}
-
${this._t("vac.diag_path")}${r?.path.length?this._t("common.yes"):this._t("common.no")}
-
${this._t("vac.diag_map")}${r?.mapId??"default"}
-
- ${"missing"===n.status||"disabled"===n.status||"unverified"===n.status?q`
- ${this._t(`vac.source_banner_${n.status}`)} - -
`:G} - ${b?q`
- ${this._t("vac.xcme_hint")} -
attributes:
-  - vacuum_position
-  - rooms
-  - path
-  - map_name
-
`:G} -
- ${this._t("vac.choose_source")} -
- - ${m.map(v)} -
{e.currentTarget.open?this._vacOpenAllCameras(t):(this._vacAllCamerasFor=null,this._vacAllCameraCache=null)}}> - ${this._t("vac.all_cameras")} -
${this._t("vac.all_cameras_warn")}
- ${s?g.length?g.map(v):q`
${this._t("vac.all_cameras_empty")}
`:G} -
-
-
-
- ${d?q``:G} - ${l?q``:G} - ${this._t("vac.documentation")} -
- ${r?q` - - - - ${p.length?q`
${ss(this._t("vac.cal_maps"),{maps:p.join(", ")})}
`:G} - `:G} -
`}_vacMapId(e,t,i=this._planHass){const s=this._vacEntity(e),n=s?i?.states?.[s]?.attributes?.selected_map:null;return o=t.mapId,a=n,"default"!==o?o:null!=a?String(a):"default";var o,a}_vacSaveMatrix(e,t,i,s){const n=this._devices.find(t=>t.id===e),o=n?this._vacEnsureMarker(n):this._serverCfg?.markers?.find(t=>t.id===e);if(!o)return!1;const a={...o.vacuum||{}};return a.source=t,a.calibration={...a.calibration||{},[i]:s.map(e=>Number(e.toFixed(6)))},o.vacuum=a,this._regSignature="",this._maybeRebuildDevices(),this._saveConfig(),this.requestUpdate(),!0}_vacPlanRoomAnchors(e){return(this._spaceModelById(e)?.rooms||[]).map(e=>({room:e,poly:hi(e)})).filter(({room:e,poly:t})=>e.name&&t).map(({room:e,poly:t})=>{const i=lp(t);return i?{name:String(e.name),cx:i[0],cy:i[1]}:null}).filter(Boolean)}_vacAutoCalibrate(e){const t=this._vacSource(e),i=t?up(this.hass?.states[t]?.attributes):null;if(!t||!i||i.rooms.length<3)return void this._showToast(this._t("vac.autocal_no_rooms"));const s=this._vacPlanRoomAnchors(e.space),n=gp(i.rooms,s);if(!n)return void this._showToast(this._t("vac.autocal_no_match"));const o=this._serverCfg?.spaces?.find(t=>t.id===e.space),a=Number(o?.cell_cm),r=Number.isFinite(a)&&a>0?a:5,l=function(e,t,i){return![e,t,i].every(Number.isFinite)||e<0||t<=0||i<=0?Number.POSITIVE_INFINITY:e/t*i}(n.residual,this._gridPitch,r),c=this._vacMapId(e,i);l>40?this._vacCalConfirm={markerId:e.id,source:t,mapId:c,matrix:n.matrix,rooms:n.matched.length,error:li(l,"mi"===this.hass?.config?.unit_system?.length)}:this._vacSaveMatrix(e.id,t,c,n.matrix)&&this._showToast(ss(this._t("vac.autocal_done"),{rooms:String(n.matched.length)}))}_vacApplyCalibrationProposal(e){const t=this._vacCalConfirm;if(t){if(this._vacCalConfirm=null,e){const e=this._devices.find(e=>e.id===t.markerId),i=wp(t.matrix);if(!e||!i)return;if(this._markerDialog=null,e.space!==this._space&&!this._commitSpace(e.space))return;return void(this._vacFit={markerId:t.markerId,source:t.source,mapId:t.mapId,p:i,drag:null})}this._vacSaveMatrix(t.markerId,t.source,t.mapId,t.matrix)&&this._showToast(ss(this._t("vac.autocal_done"),{rooms:String(t.rooms)}))}}_vacStartFit(e){const t=this._vacSource(e),i=t?up(this.hass?.states[t]?.attributes):null;if(!t||!i)return void this._showToast(this._t("vac.cal_need_pos"));const s=this._vacMapId(e,i),n=e.marker?.vacuum?.calibration?.[s],o=this._spaceModelById(e.space);if(!o)return;const a=o.vb,r=n&&6===n.length&&wp(n)||function(e,t){const i=[],s=[];for(const t of e)null!=t.x0?(i.push(t.x0,t.x1),s.push(t.y0,t.y1)):(i.push(t.cx),s.push(t.cy));if(!i.length)return{ox:t[0]+t[2]/2,oy:t[1]+t[3]/2,s:t[2]/1e4,rot:0,mir:!0};const n=Math.min(...i),o=Math.max(...i),a=Math.min(...s),r=Math.max(...s),l=Math.max(o-n,r-a)||1,c={ox:0,oy:0,s:.6*Math.min(t[2],t[3])/l,rot:0,mir:!0},h=yp(c),[d,p]=np(h,(n+o)/2,(a+r)/2);return c.ox=t[0]+t[2]/2-d,c.oy=t[1]+t[3]/2-p,c}(i.rooms,a);this._markerDialog=null,(e.space===this._space||this._commitSpace(e.space))&&(this._vacFit={markerId:e.id,source:t,mapId:s,p:r,drag:null})}_vacFitSave(){const e=this._vacFit;if(!e)return;const t=this._vacSaveMatrix(e.markerId,e.source,e.mapId,yp(e.p));this._vacFit=null,t&&this._showToast(this._t("vac.cal_done"))}_vacFitTurn(e){const t=this._vacFit;if(!t)return;const i=up(this.hass?.states[t.source]?.attributes),s=this._vacGhostCentre(i?.rooms||[]),n={...t.p,...e};this._vacFit={...t,p:kp(n,t.p,s[0],s[1])}}_vacGhostCentre(e){const t=[],i=[];for(const s of e)t.push(s.x0??s.cx,s.x1??s.cx),i.push(s.y0??s.cy,s.y1??s.cy);return t.length?[(Math.min(...t)+Math.max(...t))/2,(Math.min(...i)+Math.max(...i))/2]:[0,0]}_vacDelta(e,t,i){const s=this._stageEl,n=s?.clientWidth||1,o=s?.clientHeight||1;return[t/n*e.w,i/o*e.h]}_vacFitPointer(e,t){const i=this._vacFit;if(!i)return;if(e.stopPropagation(),"pointerdown"===e.type){const t=e.target,s=t.getAttribute?.("data-corner");try{e.currentTarget.setPointerCapture?.(e.pointerId)}catch{}return void(this._vacFit={...i,drag:s?{kind:"scale",sx:e.clientX,sy:e.clientY,p0:{...i.p},fx:Number(s.split(",")[0]),fy:Number(s.split(",")[1])}:{kind:"move",sx:e.clientX,sy:e.clientY,p0:{...i.p},fx:0,fy:0}})}const s=i.drag;if(s){if("pointermove"===e.type){const[n,o]=this._vacDelta(t,e.clientX-s.sx,e.clientY-s.sy);if("move"===s.kind)this._vacFit={...i,p:{...s.p0,ox:s.p0.ox+n,oy:s.p0.oy+o}};else{const e=up(this.hass?.states[i.source]?.attributes),t=this._vacGhostCentre(e?.rooms||[]),a=yp(s.p0),[r,l]=np(a,t[0],t[1]),[c,h]=np(a,s.fx,s.fy),d=Math.hypot(r-c,l-h)||1,[p,u]=[2*r-c,2*l-h],_=Math.hypot(p+2*n-c,u+2*o-h)/2,m=Math.max(.05,_/d),g={...s.p0,s:s.p0.s*m};this._vacFit={...i,p:kp(g,s.p0,s.fx,s.fy)}}return}"pointerup"!==e.type&&"pointercancel"!==e.type||(this._vacFit={...i,drag:null})}}_renderVacFit(e){const t=this._vacFit;if(!t)return G;const i=up(this._renderPlanHass?.states?.[t.source]?.attributes);if(!i)return G;const s=yp(t.p),n=[],o=[],a=[];for(const e of i.rooms){if(null==e.x0)continue;const t=[[e.x0,e.y0],[e.x1,e.y0],[e.x1,e.y1],[e.x0,e.y1]].map(([e,t])=>np(s,e,t));t.forEach(([e,t])=>{o.push(e),a.push(t)});const[i,r]=np(s,e.cx,e.cy);n.push(W` - ${e.name}`)}let r=G;if(i.pos){const[t,n]=np(s,i.pos.x,i.pos.y);r=W``}const l=[];if(o.length){const t=(()=>{const e=s[0]*s[4]-s[1]*s[3];return(t,i)=>[(s[4]*(t-s[2])-s[1]*(i-s[5]))/e,(-s[3]*(t-s[2])+s[0]*(i-s[5]))/e]})(),i=Math.min(...o),n=Math.max(...o),r=Math.min(...a),c=Math.max(...a),h=.022*e.w,d=h/4;for(const[e,s,o,a]of[[i,r,n,c],[n,r,i,c],[n,c,i,r],[i,c,n,r]]){const i=t(o,a);l.push(W` - `)}}return q`this._vacFitPointer(t,e)} - @pointermove=${t=>this._vacFitPointer(t,e)} - @pointerup=${t=>this._vacFitPointer(t,e)} - @pointercancel=${t=>this._vacFitPointer(t,e)}>${n}${r}${l}`}_vacRafLoop(){this._vacRaf=requestAnimationFrame(()=>{const e=this.renderRoot,t=this._stageEl,i=this._vacLastView,s=e?.querySelectorAll?.(".vacpuck")||[];if(!t||!i||!s.length)return void(this._vacRaf=0);const n=t.getBoundingClientRect();for(const t of s){const s=t.getAttribute("data-mid"),o=t.getBoundingClientRect(),a=i.x+(o.left+o.width/2-n.left)/n.width*i.w,r=i.y+(o.top+o.height/2-n.top)/n.height*i.h;for(const t of e.querySelectorAll(`line.tip[data-mid="${s}"]`))t.setAttribute("x2",a.toFixed(1)),t.setAttribute("y2",r.toFixed(1))}this._vacRafLoop()})}_renderVacuums(e,t){if(this._markup||"decor"===this._mode)return G;const i=this._space+"|"+t.x+"|"+t.y+"|"+t.w+"|"+t.h,s=this._vacJumpOnce||i!==this._vacViewKey;this._vacViewKey=i,this._vacJumpOnce=!1;const n=[],o=[];for(const i of e){if(i.hidden||!this._isVacDev(i))continue;if("static_icon"===Bi(i.marker?.display))continue;const e=this._renderDeviceSnapshot?.facts.get(`vacuum:${i.id}`),a=e?.source??this._vacSource(i,this._renderPlanHass);if(!a)continue;const r=e?.telemetry??up(this._renderPlanHass?.states[a]?.attributes);if(!r)continue;const l=String(e?.mapId??this._vacMapId(i,r,this._renderPlanHass)),c=i.marker?.vacuum?.calibration?.[l];if(!c||6!==c.length)continue;const h=e?.runtime??this._vacRt.get(i.id),d=h?.moving??!1,p=xp(i.marker?.vacuum),u="always"===p||"cleaning"===p&&d,_=e?.server??this._vacSrvTrails[i.id],m=_?.current?.map_id===l&&Array.isArray(_.current.points)?_.current:null,g=_?.previous?.map_id===l&&Array.isArray(_.previous.points)?_.previous:null;if("always"===p&&g&&g.points.length>1){const e=g.points.map(([e,t])=>{const[i,s]=np(c,e,t),n=this._scenePoint([i,s]);return n[0].toFixed(1)+","+n[1].toFixed(1)}).join(" ");o.push(W``)}if(u){const e=dp(r,m,h?.trail||[]),t=!d||"integration"!==e.source&&"server"!==e.source?e.path:pp(e.path);if(t.length){const e=t.map(e=>e.map(([e,t],i)=>{const[s,n]=np(c,e,t),o=this._scenePoint([s,n]);return`${i?"L":"M"} ${o[0].toFixed(1)} ${o[1].toFixed(1)}`}).join(" ")).join(" ");o.push(W``);const s=t[t.length-1];if(d&&s?.length>=2){const e=s[s.length-1],[t,n]=np(c,e[0],e[1]),a=this._scenePoint([t,n]),r=a[0].toFixed(1),l=a[1].toFixed(1);o.push(W``)}}}if(!d||!r.pos)continue;const[f,v]=np(c,r.pos.x,r.pos.y),b=this._scenePoint([f,v]),y=(b[0]-t.x)/t.w*100,w=(b[1]-t.y)/t.h*100,k=h&&h.lastTs>0&&Date.now()-h.lastTs>6e4,x=i.marker?.icon||i.icon||"mdi:robot-vacuum";n.push(q`
{e.stopPropagation();const t=this._vacEntity(i);t&&this._openMoreInfo(t)}}> - -
`)}return this._vacLastView=t,n.length&&!this._vacRaf&&this._vacRafLoop(),n.length||o.length?q` - ${o.length?W`${o}`:G} - ${n}`:G}_renderDevice(e,t,i=!0){const s=this._pos(e),n=this._scenePoint([s.x,s.y]),o=(n[0]-t.x)/t.w*100,a=(n[1]-t.y)/t.h*100,r=this._devicePresentation(e,i),l=[`left:${o}%`,`top:${a}%`,...Go(r)],c=r.disabledReason,h=r.haDisabled?this._t(`marker.ha_disabled_${c}`):e.userHidden?this._t("marker.hidden_ghost"):e.name,d=S_(r),p="view"===this._mode||"devices"===this._mode,u=[h,r.haDisabled?"":this._t(`marker.state_a11y_${d}`),"none"!==r.pulse.kind?this._t(`marker.pulse_a11y_${r.pulse.reason}`):"",r.valueFullText||r.valueText||"",Io(r.valueBadge),null!=r.lqiText&&r.lqiBand?this._t(`marker.lqi_a11y_${r.lqiBand}`,{value:r.lqiText}):""].filter(Boolean).join(", "),_=[e.model,r.valueBadge?.fullText||"",null!=r.lqiText?"LQI "+r.lqiText:""].filter(Boolean).join(" · ");return q`
this._clickDevice(t,e)} - @keydown=${t=>this._keyDevice(t,e)} - @contextmenu=${t=>this._ctxDevice(t,e)} - @pointerover=${t=>this._showTip(t,e.name,r.haDisabled?h:_)} - @pointerleave=${()=>this._clearTransientHover()} - @pointerdown=${t=>this._pointerDown(t,e)} - @pointermove=${t=>{this._pointerMove(t,e),this._showTip(t,e.name,r.haDisabled?h:_)}} - @pointerup=${t=>this._pointerUp(t,e)} - @pointercancel=${t=>this._pointerUp(t,e)} - > - ${Ko(r,{newDevice:this._newIds.has(e.id),newDeviceTitle:this._t("device.new"),disabledTitle:r.haDisabled?h:""})} -
`}_roomArea(e){const t=hi(e);if(!t)return null;const i=this._spaceModel();if(!i)return null;const s=this._spaceWalls.length&&e.id&&this._innerRoomContour(i,e.id)||t,n=this._cleanFloor(e,s),o=this._cellCm/this._gridPitch;return rr(n.area*o*o/1e4,"mi"===this._renderPlanHass?.config?.unit_system?.length)}_roomTemp(e){const t=e.settings?.temp_source;return t?Mo(this._renderPlanHass,t,"temp",this._markers):e.area?this._climate().get(e.area)?.temp??null:null}_roomHum(e){const t=e.settings?.hum_source;return t?Mo(this._renderPlanHass,t,"hum",this._markers):e.area?this._climate().get(e.area)?.hum??null:null}_climate(){const e=this._serverCfg?.markers,t=this._renderPlanHass,i=this._climateCache;if(i&&i.h===t&&i.r===this._iconRules&&i.mk===e)return i.m;const s=function(e,t,i){const s=new Map;if(!e?.entities)return s;const n=wo(i),o=new Set;for(const e of i||[]){if(e?.removed||!0!==e?.use_climate_temp)continue;const t=(e.binding||"").indexOf(":");t>0&&o.add(e.binding.slice(t+1))}const a=new Map;for(const[t,i]of Object.entries(e.entities)){if(i.device_id&&n.devices.has(i.device_id)&&!n.liveEntities.has(t)||!i.device_id&&n.entities.has(t)&&!n.liveEntities.has(t))continue;const s=i.device_id?e.devices?.[i.device_id]:null,r=i.area_id||s?.area_id||null;if(!r)continue;if(i.entity_category)continue;if(!(o.size>0&&t.startsWith("climate.")&&(o.has(t)||i.device_id&&o.has(i.device_id)))){if(Ye.has(i.platform))continue;if(Co.test(t))continue}let l=a.get(r);l||(l=new Map,a.set(r,l));const c=i.device_id||t;let h=l.get(c);if(!h){const n=e.states?.[t];h={name:(s?s.name_by_user||s.name:i.name||n?.attributes?.friendly_name||t)||t,model:s?.model,ents:[]},l.set(c,h)}h.ents.push(t)}for(const[i,n]of a){const a=[],r=[];for(const[i,s]of n){const n=yo(e,s.name,s.model,s.ents,t),l="mdi:thermometer"===n||"mdi:air-filter"===n;if(l){const t=mo(e,s.ents);null!=t&&a.push(t)}if(o.size>0&&(o.has(i)||s.ents.some(e=>o.has(e)))){const t=go(e,s.ents);null!=t&&a.push(t)}if(l||"mdi:water-percent"===n){const t=vo(e,s.ents);null!=t&&r.push(t)}}(a.length||r.length)&&s.set(i,{temp:a.length?Math.round(a.reduce((e,t)=>e+t,0)/a.length*10)/10:null,hum:r.length?Math.round(r.reduce((e,t)=>e+t,0)/r.length):null})}return s}(t,this._iconRules,e);return this._climateCache={h:t,r:this._iconRules,mk:e,m:s},s}_resetRoomDialogFields(){this._roomEditId=null,this._roomFill="",this._roomCustomFill=null,this._roomTempSrc="",this._roomHumSrc="",this._roomSrcOpen=null,this._roomSrcFilter="",this._roomNameScale=1,this._roomLabelScale=1}_openRoomEdit(e){if(!e.id)return;this._roomEditId=e.id,this._nameSel=e.name||"",this._areaSel=e.area||"",this._roomFill="glow"===e.settings?.fill_mode?"":e.settings?.fill_mode||"";const t=e.settings?.custom_fill;this._roomCustomFill=t&&"object"==typeof t?rs(t,cs(this._curSpaceCfg).customFill):null,this._roomTempSrc=e.settings?.temp_source||"",this._roomHumSrc=e.settings?.hum_source||"",this._roomNameScale=Ns(e.settings?.name_scale),this._roomLabelScale=Ns(e.settings?.label_scale),this._roomSrcOpen=null,this._roomSrcFilter="",this._roomDialog=!0}_roomSettingsFromDialog(){const e={};return this._roomFill&&(e.fill_mode=this._roomFill),this._roomCustomFill&&(e.custom_fill=this._roomCustomFill),this._roomTempSrc&&(e.temp_source=this._roomTempSrc),this._roomHumSrc&&(e.hum_source=this._roomHumSrc),1!==this._roomNameScale&&(e.name_scale=this._roomNameScale),1!==this._roomLabelScale&&(e.label_scale=this._roomLabelScale),Object.keys(e).length?e:null}_saveRoomEdit(){const e=this._curSpaceCfg,t=e?.rooms.find(e=>e.id===this._roomEditId);if(!t)return this._roomDialog=!1,void(this._roomEditId=null);t.name=this._nameSel.trim()||t.name,t.area=this._areaSel||null;const i=t.settings||{},s={...i};this._roomFill?s.fill_mode=this._roomFill:delete s.fill_mode,this._roomCustomFill?s.custom_fill=this._roomCustomFill:delete s.custom_fill,"glow"===i.fill_mode&&"boolean"!=typeof i.glow&&(s.glow=!0),this._roomTempSrc?s.temp_source=this._roomTempSrc:delete s.temp_source,this._roomHumSrc?s.hum_source=this._roomHumSrc:delete s.hum_source,1!==this._roomNameScale?s.name_scale=this._roomNameScale:delete s.name_scale,1!==this._roomLabelScale?s.label_scale=this._roomLabelScale:delete s.label_scale,Object.keys(s).length?t.settings=s:delete t.settings,this._saveConfig(),this._roomDialog=!1,this._roomEditId=null,this._nameSel="",this._areaSel="",this._regSignature="",this._maybeRebuildDevices(),this.requestUpdate(),this._showToast(this._t("toast.room_updated"))}_roomSrcCandidates(){const e=this._planHass,t=wo(this._markers),i=this._roomSrcFilter.trim().toLowerCase(),s=[];for(const n of Object.values(e.devices)){if("service"===n.entry_type)continue;if(t.devices.has(n.id))continue;const e=(n.name_by_user||n.name||n.id).trim();i&&!e.toLowerCase().includes(i)||s.push({value:"device:"+n.id,label:e,sub:n.model||this._t("marker.sub_device")})}for(const[n,o]of Object.entries(e.entities)){if(!n.startsWith("sensor.")||o.hidden)continue;if(ko(e,n,t))continue;const a=o.name||e.states[n]?.attributes?.friendly_name||n;i&&!(a+" "+n).toLowerCase().includes(i)||s.push({value:"entity:"+n,label:a,sub:n})}return s.sort((e,t)=>e.label.localeCompare(t.label)),s.slice(0,200)}_roomSrcLabel(e){const t=e.indexOf(":"),i=e.slice(0,t),s=e.slice(t+1);return"device"===i?this._fullRegistryHass.devices[s]?.name_by_user||this._fullRegistryHass.devices[s]?.name||s:this._fullRegistryHass.entities[s]?.name||this.hass.states[s]?.attributes?.friendly_name||s}_labelPos(e,t){const i=this._layout["rl_"+(e.id||"")];if(i&&i.s===t)return{x:i.x*Ov,y:i.y*Ov};const s=this._snap(this._roomCenter(e));return{x:s[0],y:s[1]}}_labelDown(e,t,i){if("plan"!==this._mode)return;e.preventDefault(),e.stopPropagation();const s=this._labelPos(t,i);this._drag={id:"rl_"+(t.id||""),sx:e.clientX,sy:e.clientY,ox:s.x,oy:s.y,moved:!1},Gv(e),this._tip=null}_labelMove(e,t,i){const s="rl_"+(t.id||"");if(!this._drag||this._drag.id!==s)return;const n=this._stageEl;if(!n)return;const o=this._spaceModelById(i);if(!o)return;const a=o.vb,r=n.getBoundingClientRect(),l=this._viewOr(a),c=(e.clientX-this._drag.sx)/r.width*l.w,h=(e.clientY-this._drag.sy)/r.height*l.h;Math.abs(e.clientX-this._drag.sx)+Math.abs(e.clientY-this._drag.sy)>3&&(this._drag.moved=!0);const d=Uh(this._drag.ox+c),p=Uh(this._drag.oy+h);this._savePos({id:s,space:i},d,p)}_labelUp(e){const t="rl_"+(e.id||"");if(!this._drag||this._drag.id!==t)return;const i=this._drag.moved;this._drag=i?this._drag:null,i&&window.setTimeout(()=>this._drag=null,0)}_labelScale(e){const t=this._layout["rl_"+(e.id||"")]?.k;return"number"==typeof t&&Number.isFinite(t)?Math.min(3,Math.max(.5,t)):1}_rlResizeDown(e,t,i){if("plan"!==this._mode)return;e.preventDefault(),e.stopPropagation();const s=e.target.closest(".roomlabel");if(!s)return;const n=s.getBoundingClientRect(),o=n.left+n.width/2,a=n.top+n.height/2,r=Math.max(8,Math.hypot(e.clientX-o,e.clientY-a));this._rlResize={id:"rl_"+(t.id||""),space:i,k0:this._labelScale(t),cx:o,cy:a,d0:r},Gv(e)}_rlResizeMove(e){const t=this._rlResize;if(!t)return;e.stopPropagation();const i=Math.max(8,Math.hypot(e.clientX-t.cx,e.clientY-t.cy)),s=Math.min(3,Math.max(.5,t.k0*(i/t.d0))),n=this._layout[t.id];if(n)this._layout={...this._layout,[t.id]:{...n,k:s}};else{const e=t.id.slice(3),i=this._spaceModelById(t.space);if(!i)return;const n=i.rooms.find(t=>t.id===e);if(!n)return;const o=this._labelPos(n,t.space);this._layout={...this._layout,[t.id]:{s:t.space,x:o.x/Ov,y:o.y/Ov,k:s}}}this._dirtyPos.add(t.id)}_rlResizeUp(){this._rlResize&&(this._rlResize=null,this._persistLayout())}_renderRoomGear(e,t,i){if(!e.id)return G;let s=null;if(e.poly?(s=this._gearPtCache.get(e.poly)||null,s||(s=$i(e.poly),this._gearPtCache.set(e.poly,s))):null!=e.x&&null!=e.y&&(s=[e.x+(e.w||0)/2,e.y+(e.h||0)/2]),!s)return G;const n=(s[0]-i.x)/i.w*100,o=(s[1]-i.y)/i.h*100;return q``}_renderRoomLabel(e,t,i,s){if(!e.name&&!this._markup)return G;const n=this._labelPos(e,t.id),o=this._scenePoint([n.x,n.y]),a=(o[0]-i.x)/i.w*100,r=(o[1]-i.y)/i.h*100,l=Math.min(1,s.opacity+.25),c=this._labelScale(e),h=[];if(e.area||e.settings?.temp_source||e.settings?.hum_source||s.labelLight){if(s.labelTemp){const t=this._roomTemp(e);null!=t&&h.push(q`${t}°`)}if(s.labelHum){const t=this._roomHum(e);null!=t&&h.push(q`${t}%`)}if(s.labelLqi&&e.area){const t=this._roomLqi(e.area);null!=t&&h.push(q`${t}`)}if(s.labelLight){const t=(d=ho(this._renderPlanHass,this._renderDevices,e,this._virtualLights)).length?{on:d.filter(e=>e.on).length,total:d.length}:null;if(t){const e=0===t.on?this._t("roomcard.light_off"):t.on===t.total?this._t("roomcard.light_on"):this._t("roomcard.light_partial",{on:t.on,total:t.total});h.push(q`${e}`)}}}var d;const p=!!e.area,u=!this._markup;return q`
this._labelDown(i,e,t.id)} - @pointermove=${i=>this._labelMove(i,e,t.id)} - @pointerup=${()=>this._labelUp(e)} - @pointercancel=${()=>this._labelUp(e)} - >${e.name||(this._markup?this._t("room.unnamed"):"")}${p?q`{t.stopPropagation(),this._clickRoom(e)}:G} - @pointerdown=${u?e=>e.stopPropagation():G}>`:G} - ${h.length?q`${h}`:G} - ${"plan"===this._mode?["tl","tr","bl","br"].map(i=>q`this._rlResizeDown(i,e,t.id)} - @pointermove=${e=>this._rlResizeMove(e)} - @pointerup=${()=>this._rlResizeUp()} - @pointercancel=${()=>this._rlResizeUp()}>`):G} -
`}get _measureAnchor(){return this._markup&&this._cursorPt?"draw"===this._tool&&this._path.length&&!this._contourClosed?this._path[this._path.length-1]:"split"===this._tool&&this._splitSel?.pts?.length?this._splitSel.pts[this._splitSel.pts.length-1]:null:null}_renderMeasureLabel(e){const t=this._measureAnchor,i=this._cursorPt,s=(i[0]-e.x)/e.w*100,n=(i[1]-e.y)/e.h*100,o=Es(t,i),a=Math.round(10*o)/10,r=function(e,t,i=.001){if(e.length<2||t.length<2)return!1;const s=Math.abs(t[0]-e[0]),n=Math.abs(t[1]-e[1]);if(!Number.isFinite(s)||!Number.isFinite(n)||Math.hypot(s,n)<=i)return!1;const o=Math.max(s,n,1);return s<=i*o||n<=i*o||Math.abs(s-n)<=i*o}(t,i,2e-4*this._gridPitch);return q`
- ${this._fmtLen(t,i)} · ${a}°
`}get _decorMeasure(){const e=this._decorDraft;if(!e||"decor"!==this._mode)return null;const[t,i]=e.a,[s,n]=e.b;if(Math.abs(t-s)<1e-6&&Math.abs(i-n)<1e-6)return null;const o=(t+s)/2,a=(i+n)/2;if("line"===e.kind){const t=Es(e.a,e.b);return{x:o,y:a,on45:Os(t),text:`${this._fmtLen(e.a,e.b)} · ${Math.round(10*t)/10}°`}}const r=this._fmtLen([t,i],[s,i]),l=this._fmtLen([s,i],[s,n]);if("ellipse"===e.kind){const e=this._fmtLen([0,0],[Math.abs(s-t)/2,0]),r=this._fmtLen([0,0],[0,Math.abs(n-i)/2]);return{x:o,y:a,on45:!1,text:Math.abs(Math.abs(s-t)-Math.abs(n-i))<1e-6?`R ${e}`:`Rx ${e} × Ry ${r}`}}return{x:o,y:a,on45:!1,text:`${r} × ${l} · ${rr(Gr(Math.abs(s-t),this._cellCm,this._gridPitch)*Gr(Math.abs(n-i),this._cellCm,this._gridPitch)/1e4,this._imperial)}`}}get _alignPoint(){if(this._markup){if("draw"===this._tool&&this._path.length&&!this._contourClosed&&this._cursorPt)return this._cursorPt;if("split"===this._tool&&this._splitSel?.pts?.length&&this._cursorPt)return this._cursorPt;if(this._drag?.id.startsWith("rl_")&&this._drag.moved){const e=this._drag.id.slice(3),t=this._spaceModel()?.rooms.find(t=>t.id===e);return t?(()=>{const e=this._labelPos(t,this._space);return[e.x,e.y]})():null}return null}if("devices"===this._mode&&this._drag?.moved){const e=this._devices.find(e=>e.id===this._drag.id);return e?(()=>{const t=this._pos(e);return[t.x,t.y]})():null}if("decor"===this._mode){if(this._decorDraft)return this._decorDraft.b;if(this._decorMove){const e=this._decorList.find(e=>e.id===this._decorMove.id);if(!e)return null;const t=Ov,i=this._decorH;return"line"===e.kind?[e.x1*t,e.y1*i]:[e.x*t,e.y*i]}return null}return null}_alignCandidates(){const e=[],t=this._spaceModel();if(this._markup){if(!t)return e;if(this._drag?.id.startsWith("rl_")){const i=this._drag.id.slice(3);for(const s of t.rooms){if(!s.name||s.id===i)continue;const t=this._labelPos(s,this._space);e.push([t.x,t.y])}return e}for(const i of t.rooms){const t=hi(i);if(t)for(const i of t)e.push(i)}if("draw"===this._tool)for(const t of this._path)e.push(t);if("split"===this._tool&&this._splitSel?.pts)for(const t of this._splitSel.pts)e.push(t);return e}if("devices"===this._mode){for(const t of this._devices){if(t.space!==this._space||t.id===this._drag?.id||"ha_disabled"===t.bindingStatus?.kind)continue;const i=this._pos(t);e.push([i.x,i.y])}return e}if("decor"===this._mode){const t=this._decorMove?.id;return e.push(...this._decorSnapGeometry(t).points),this._decorDraft&&e.push(this._decorDraft.a),e}return e}_renderAlignGuides(){const e=this._alignPoint;if(!e)return W``;const t=this._drag?.id.startsWith("rl_")?.5*this._gridPitch:.05*this._gridPitch,i=function(e,t,i){let s=null,n=null;for(const o of t)if(!(Math.abs(o[0]-e[0])<1e-6&&Math.abs(o[1]-e[1])<1e-6)){if(Math.abs(o[0]-e[0])<=i){const t=Math.abs(o[1]-e[1]);t>1e-6&&(!s||t1e-6&&(!n||t - ${i.map(t=>{const[i,o,a,r]="x"===t.axis?[t.at,t.from[1],t.at,e[1]+Math.sign(e[1]-t.from[1])*n]:[t.from[0],t.at,e[0]+Math.sign(e[0]-t.from[0])*n,t.at];return W` - `})} - `}_renderOpeningCenterTick(e){const t=(e.angle+90)*Math.PI/180,i=Dh(15,this._cellCm);return W``}_renderOpeningDimensionGuides(e){const t=e.labels.flatMap(e=>e.dimension?[e.dimension]:[]);if(!t.length)return W``;const i=this._cssPxToRender(4);return W``}_roomCenter(e){if(e.poly){const t=e.poly.length;return[e.poly.reduce((e,t)=>e+t[0],0)/t,e.poly.reduce((e,t)=>e+t[1],0)/t]}return[e.x+e.w/2,e.y+.1*Math.min(e.w,e.h)]}_openingAmt(e){const t=e.contact&&this._renderOpeningEntityAvailable(e.contact)?this._renderPlanHass.states[e.contact]?.state:null;return mi(e.type,t,!!e.invert)}_planEntityAvailable(e){return!!e&&(!ko(this._fullRegistryHass,e,wo(this._markers))&&"active"===this._bindingStatus("entity:"+e).kind)}_renderEntityAvailable(e){return!!e&&(!ko(this._renderPlanHass,e,wo(this._markers))&&(!!this._renderPlanHass.entities?.[e]&&!!this._renderPlanHass.states?.[e]))}_openingEntityAvailable(e){return function(e,t,i=An(e)){return!!t&&"active"===Bn(e,`entity:${t}`,i).kind}(this.hass,e,this._haRegistry)}_renderOpeningEntityAvailable(e){return t=this._renderPlanHass,!!(i=e)&&!!t?.states?.[i];var t,i}_renderOpeningPlacementPreview(){const e=this._openingPreview;if(!e)return W``;const t={type:e.type,length:e.renderedLength,angle:e.angle,amount:mi(e.type,null),flipH:e.flipH,flipV:e.flipV,base:"var(--hp-open)",tone:"var(--hp-open)",cellCm:this._cellCm,gridPitch:this._gridPitch,face:e.face},i="passage"===e.type?function(e,t){const i=Math.max(0,e.renderedLength)/2,s=Math.max(0,e.target.physicalHalfWidth),n=s+.18*Math.max(0,t),o=e=>({x1:e,y1:-n,x2:e,y2:n});return{rect:{x:-i,y:s?-s:0,width:2*i,height:2*s},boundaries:[o(-i),o(i)]}}(e,Dh(this._gridPitch,this._cellCm)):null;return W` - `}_renderOpenings(e){const t=this._openingsR;if(!t.length)return W``;const i=this._spaceModel();if(!i)return W``;const s=e.color,n=this._spaceWalls,o=this._openCuts(),a=this._openingWallIndexFor(i,o).value;return W`${t.map(e=>{if(e.orphanReason)return W`{t.stopPropagation(),this._editOpening(e)}}> - - ! - `;const t=this._openingAmt(e),i=t>0&&!!e.contact&&this._renderOpeningEntityAvailable(e.contact)?"var(--hp-open)":s,o="gate"===e.type?!e.flip_v:e.flip_v,r=e.partitionHost||n.length||"gate"===e.type?this._openingFace(e,a,!!o):{ox:0,oy:0,cm:0,side:-1},l={type:e.type,length:e.rlen,angle:e.angle,amount:t,flipH:!!e.flip_h,flipV:!!e.flip_v,base:s,tone:i,cellCm:this._cellCm,gridPitch:this._gridPitch,face:r},{half:c,outlineHalf:h,hitHalf:d}=y_(l),p=Dh(10,this._cellCm),u=Dh(12,this._cellCm);return W` - ${w_(l)} - - this._opClick(t,e)} - @pointerdown=${t=>this._opPointerDown(t,e)} - @pointermove=${t=>this._opPointerMove(t,e)} - @pointerup=${t=>this._opPointerUp(t,e)} - @pointercancel=${t=>this._opPointerUp(t,e)}> - `})}`}_renderOpeningLocks(e){const t=this._openingsR.filter(e=>!e.orphanReason&&("door"===e.type||"gate"===e.type)&&e.lock&&this._renderOpeningEntityAvailable(e.lock));if(!t.length)return q``;const i=this._spaceModel();if(!i)return q``;const s=this._openCuts(),n=this._openingWallIndexFor(i,s).value;return q`${t.map(t=>{const i=this._renderPlanHass.states[t.lock]?.state,s="locked"===i,o=s||["unlocked","open","opening","unlocking","locking"].includes(String(i)),a=(t.angle+90)*Math.PI/180,r="gate"===t.type?this._openingFace(t,n,!t.flip_v):null,l=Dh(16,this._cellCm),c=r?-l*r.side:l*(t.flip_v?-1:1),h=t.rx+Math.cos(a)*c,d=t.ry+Math.sin(a)*c,p=this._scenePoint([h,d]),u=(p[0]-e.x)/e.w*100,_=(p[1]-e.y)/e.h*100;return q`
{e.stopPropagation(),"view"===this._mode&&(this._openingInfo=t)}}> - -
`})}`}_lockAction(e,t){if(this._openingEntityAvailable(e)){if("unlock"===t){const t=this.hass?.states?.[e]?.attributes?.friendly_name||e;if(!confirm(this._t("confirm.unlock",{name:t})))return}this.hass?.callService?.("lock",t,{entity_id:e})}}_renderOpeningInfoCard(){const e=this._openingInfo,t="passage"!==e.type&&e.contact&&this._openingEntityAvailable(e.contact)?e.contact:null,i=("door"===e.type||"gate"===e.type)&&e.lock&&this._openingEntityAvailable(e.lock)?e.lock:null,s=t?this.hass.states[t]?.state:null,n=this._openingAmt(e),o=i?this.hass.states[i]?.state:null,a="door"===e.type?"opening.door":"gate"===e.type?"opening.gate":"passage"===e.type?"opening.passage":"opening.window",r="door"===e.type?"mdi:door":"gate"===e.type?"mdi:gate":"passage"===e.type?"mdi:arch":"mdi:window-closed-variant",l="gate"===e.type?n>0?"mdi:gate-open":"mdi:gate":n>0?"mdi:door-open":"mdi:door-closed",c=(e,t,i,s="")=>q`
${t}${i}
`;return q`this._openingInfo=null}> -
- ${t?c(l,this._t("opening.contact_label"),"unavailable"===s||null==s?this._t("opening.state_unknown"):this._t(n>0?"opening.open":"opening.closed"),n>0?"warn":"ok"):G} - ${i?c("locked"===o?"mdi:lock":"mdi:lock-open-variant",this._t("opening.lock_label"),"locked"===o?this._t("opening.locked"):["unlocked","open"].includes(String(o))?this._t("opening.unlocked"):this._t("opening.state_unknown"),"locked"===o?"ok":"warn"):G} - ${i&&("locked"===o||["unlocked","open"].includes(String(o)))?q``:i&&["locking","unlocking"].includes(String(o))?q``:G} - ${t||i?G:q`

${this._t("opening.no_entities")}

`} -
-
- - -
-
`}_renderOpeningDialog(){const e=this._openingDialog,t=this._spaceModel()?.partitions||[],i=e.id?this._curSpaceCfg?.openings?.find(t=>t.id===e.id):null,s={id:e.id||"preview",type:e.type,x:e.x/Ov,y:e.y/this._spaceH,angle:e.angle,length:i&&!e.lengthTouched?i.length:this._cmToUnits(Math.max(20,e.lengthCm))/Ov,...e.host?{host:e.host}:{}},n=ru(i,s),o=e.host?n?au(s,t,Ov,this._cellCm,this._gridPitch):ou(s,t,Ov,this._cellCm,this._gridPitch):null,a="does-not-fit-jamb"===o?.reason,r=e.host?t.find(t=>t.id===e.host.id):null,l=r?li(r.cm/2,this._imperial):"",c=!!e.host&&!o?.resolved&&!a,h="gate"===e.type?"mdi:gate":"window"===e.type?"mdi:window-closed-variant":"passage"===e.type?"mdi:arch":"mdi:door",d=(t,i)=>{const s="contact"===t?e.contact:e.lock,n="contact"===t?!!e.contactOpen:!!e.lockOpen,o="contact"===t?e.contactFilter||"":e.lockFilter||"",a=i.find(e=>e.value===s),r=a?.label||this.hass.states[s]?.attributes?.friendly_name||this._fullRegistryHass.entities[s]?.name||s,l=function(e,t,i=200){const s=t.trim().toLowerCase();return(s?e.filter(e=>e.label.toLowerCase().includes(s)||e.value.toLowerCase().includes(s)):e).slice(0,Math.max(0,i))}(i,o);return q` - - ${n?q`
- this._filterOpeningEntities(t,e.target.value)} /> -
- - ${l.map(e=>q` - `)} - ${l.length?G:q`
${this._t("marker.nothing_found")}
`} -
-
`:G}`};return q`this._openingDialog=null}> -
- ${e.host?q` -
- ${c||a?q``:G} - ${c?this._t("opening.partition_orphan"):a?this._t("opening.partition_jamb_margin",{distance:l}):this._t("opening.host_partition")} -
`:G} - - - - - - - - {const i=$v(t.target.value);null!=i&&(this._openingDialog={...e,lengthCm:i,lengthTouched:!0})}} /> - - ${"passage"===e.type&&(e.contact||e.lock)?q`
- - ${this._t("opening.passage_binding_warning")} -
`:G} - - ${"passage"!==e.type?q` - ${d("contact",this._contactCandidates())} - ${e.contact?q``:G}`:G} - - ${"door"===e.type||"gate"===e.type?q` - ${d("lock",this._lockCandidates())}`:G} - - ${"gate"!==e.type&&"passage"!==e.type?q``:G} - ${"passage"!==e.type?q``:G} -
- -
`}_gridLevels(){const e=this._stageEl,t=this._viewOr(this._baseVb()),i=e&&e.clientWidth&&t.w?e.clientWidth/t.w:1;return function(e,t,i=7){if(!(e>0&&t>0&&Number.isFinite(t)))return null;const s=ad.find(s=>e*s*t>=i);if(void 0===s)return null;const n=ad.find(e=>e>=5*s)??5*s;return{fine:s,coarse:n}}(this._gridPitch,i)}_renderMarkupDefs(e){const t=this._gridLevels();if(!t)return W``;const i=this._gridPitch*t.fine,s=this._gridPitch*t.coarse,n=this._gridPitch*t.fine*.14;return W` - - - - - - - - - - - - - - `}_renderPhysicalEditorLayer(){const e=this._spaceModel();if(!e)return W``;const t=this._gridPitch,i=this._viewOr(this._baseVb()),s=this._stageEl,n=s?.clientWidth?i.w/s.clientWidth:t/8,o=Math.max(.22*t,8*n),a=Math.max(o,12*n),r=e=>`M ${e.map(e=>`${e[0]} ${e[1]}`).join(" L ")} Z`,l=(e,t)=>this._physicalSel?.kind===e&&this._physicalSel.id===t||"column"===e&&this._duplicateColumnId===t,c=(e.room_drafts||[]).flatMap(e=>e.points.slice(0,-1).map((t,i)=>{const s=e.points[i+1],o=Number(e.segments[i]?.cm),a=Ml(Number.isFinite(o)?o:15,this._cellCm,this._gridPitch);return W`{this._physicalSel={kind:"draft",id:e.id,segment:i},this._registerPhysicalTap("draft",e.id,i)}}>`})),h=(e.partitions||[]).map(e=>{const t=hh(e.a,e.b,e.cm,this._cellCm,this._gridPitch);return t?W`this._physicalDown(t,"partition",e.id)} - @pointermove=${e=>this._physicalMove(e)} - @pointerup=${e=>this._physicalUp(e)}>`:W`this._physicalDown(t,"partition",e.id)} - @pointermove=${e=>this._physicalMove(e)} - @pointerup=${e=>this._physicalUp(e)}>`}),d=(e.wall_columns||[]).map(e=>{const t=dh(this._physicalRotate?.id===e.id?{...e,angle:this._physicalRotate.angle}:e,this._cellCm,this._gridPitch);return W`this._physicalDown(t,"column",e.id)} - @pointermove=${e=>this._physicalMove(e)} - @pointerup=${e=>this._physicalUp(e)}>`}),p=this._physicalDrag,u=(()=>{if(!p?.moved)return G;if("partition"===p.kind&&0===Number(p.base.cm)){const e=p.base;return W``}const e="partition"===p.kind?hh(p.base.a,p.base.b,p.base.cm,this._cellCm,this._gridPitch):dh(p.base,this._cellCm,this._gridPitch);return e?W``:G})(),_=(()=>{const i=this._physicalSel;if(!i)return G;if("draft"===i.kind){const t=e.room_drafts.find(e=>e.id===i.id);return t?W` - e.join(",")).join(" ")}> - ${t.points.map(e=>W``)} - `:G}if("partition"===i.kind){const t=e.partitions.find(e=>e.id===i.id);if(!t)return G;const s=hh(t.a,t.b,t.cm,this._cellCm,this._gridPitch),n=(t.a[0]+t.b[0])/2,a=(t.a[1]+t.b[1])/2;return W` - ${s?W``:W``} - - - - `}const s=e.wall_columns.find(e=>e.id===i.id);if(!s)return G;const l=this._physicalRotate?.id===s.id?{...s,angle:this._physicalRotate.angle}:s,c=dh(l,this._cellCm,this._gridPitch);if("square"!==l.shape)return W` - - - `;const h=Ml(l.cm,this._cellCm,this._gridPitch),d=oh(l.angle)*Math.PI/180,p=Math.sin(d),u=-Math.cos(d),_=[l.center[0]+p*h/2,l.center[1]+u*h/2],m=[_[0]+p*Math.max(t,24*n),_[1]+u*Math.max(t,24*n)];return W` - - - - this._physicalRotateDown(e,s)} - @pointermove=${e=>this._physicalRotateMove(e)} - @pointerup=${e=>this._physicalRotateUp(e)}> - `})();return W`${c}${h}${d}${u}${_}`}_renderHiddenWallDiagnosticOverlay(){if(!this._markup)return W``;const e=this._hiddenWallDiagnosticSnapshot().value;if(!e.segments.length)return W``;const t=Ml(5,this._cellCm,this._gridPitch);return W``}_renderPlanSnapOverlay(){if(!this._markup)return W``;const e=this._planSnapGeometrySnapshot().value,t=this._activePlanSnapCandidate,i=new Set(this._activePlanSnapConflicts.map(e=>e.key)),s=Ml(5,this._cellCm,this._gridPitch),n=Ml(10,this._cellCm,this._gridPitch);return W``}_syncPlanSnapActiveMarker(e){const t=this.renderRoot?.querySelector('[data-hp="plan-snap-active-marker"]');if(t){if(t.setAttribute("class",`plan-snap-node${e?" active":""}${"line"===e?.kind?" dynamic":""}`),t.setAttribute("data-active",e?"true":"false"),t.setAttribute("visibility",e?"visible":"hidden"),!e)return t.removeAttribute("data-kind"),void t.removeAttribute("data-key");t.setAttribute("data-kind",e.kind),t.setAttribute("data-key",e.key),t.setAttribute("cx",String(e.point[0])),t.setAttribute("cy",String(e.point[1])),t.setAttribute("r",String(Ml(10,this._cellCm,this._gridPitch)))}}_syncPlanSnapConflictMarkers(e){const t=new Set(e.map(e=>e.key));for(const e of this.renderRoot?.querySelectorAll('.plan-snap-node[data-kind="endpoint"]')||[])e.setAttribute("class","plan-snap-node"+(t.has(e.dataset.key||"")?" conflict":""))}_planSnapPhysicalSegment(e){let t=0;const i=this._spaceModel();if(!i)return null;if("partition"===e.sourceKind)t=Number(i.partitions.find(t=>t.id===e.sourceId)?.cm)||0;else if("draft"===e.sourceKind){const s=/^(.*):(\d+)$/.exec(e.sourceId),n=s?i.room_drafts.find(e=>e.id===s[1]):null;t=n&&s&&Number(n.segments[Number(s[2])]?.cm)||0}else t=$c(i.rooms,this._spaceWalls,this._openCuts(),[e.a[0],e.a[1],e.b[0],e.b[1]],this._wallKeyPitch,this._cellCm,this._gridPitch,Ov);return t>0?{a:[...e.a],b:[...e.b],halfDepth:Ml(t,this._cellCm,this._gridPitch)/2}:null}_drawPreviewJoinPatchD(e,t){if(e.length<2)return"";const i=[];for(let s=0;s+10&&i.push({a:e[s],b:e[s+1],halfDepth:t[s]});if(!i.length)return"";const s=2e-4*this._gridPitch,n=this._planSnapGeometrySnapshot().value.segments.filter(t=>e.some(e=>Hs(e,[t.a[0],t.a[1],t.b[0],t.b[1]])<=s)).map(e=>this._planSnapPhysicalSegment(e)).filter(e=>!!e);return ec([...i,...n],s).map(e=>`M ${e.map(e=>`${e[0]} ${e[1]}`).join(" L ")} Z`).join(" ")}_renderMarkupLayer(e){const t=this._openCuts(),i=this._thickWallCuts(),s=t.concat(i),n=s.length?Ds(this._segments,s,.02*this._gridPitch):this._segments,o=this._path,a=this._gridPitch,r=this._viewOr(this._baseVb()),l="draw"===this._tool?this._drawWallCm:null,c=(()=>"draw"===this._tool&&o.length&&null!=l?this._contourClosed?o:this._cursorPt?[...o,this._cursorPt]:o.length>=2?o:null:null)(),h=c?Iu(c.length-1,this._contourClosed?[...this._draftSegmentCms,this._closingWallCm??void 0]:this._draftSegmentCms,l,vl).map(e=>Ml(e,this._cellCm,this._gridPitch)/2):[],d=c?tc(c,Ml(l,this._cellCm,this._gridPitch)/2,this._contourClosed,h):"",p=c?this._drawPreviewJoinPatchD(c,h):"",u=this._wallFaceBatch?.candidates[this._wallFaceBatch.index]?.repair||this._wallRepairDiagnostic;return W` - ${this._gridLevels()?W``:G} - ${n.map(e=>W``)} - ${this._renderPhysicalEditorLayer()} - ${"column"===this._tool&&this._cursorPt&&this._drawWallCm?W``M ${dh({shape:"square",center:this._cursorPt,cm:this._drawWallCm},this._cellCm,this._gridPitch).map(e=>`${e[0]} ${e[1]}`).join(" L ")} Z`)()}>`:G} - ${d?W` - `:G} - ${c&&0===l?W`e.join(",")).join(" ")}>`:G} - ${p?W` - `:G} - ${u?W``:G} - ${"split"===this._tool&&this._splitSel?.pts?.length?W`${this._splitSel.pts.length>1?W``:G} - ${this._splitSel.pts.map((e,t)=>W``)} - ${this._cursorPt?W``:G}`:G} - `}_renderActiveChainInk(){const e=this._path,t=this._gridPitch;return W` - ${e.length>1?W``:G} - ${e.length&&this._cursorPt&&"draw"===this._tool&&!this._contourClosed?W` - ${this._activePlanSnapCandidate||this._activePlanSnapConflicts.length?G:W``}`:G} - ${e.map((e,i)=>W``)} - `}_renderPartitionDeleteDialog(){const e=this._partitionDeleteDialog,t="mi"===this.hass?.config?.unit_system?.length;return q`this._partitionDeleteDialog=null}> -
-

${this._t("confirm.delete_partition_openings_body",{count:e.openings.length})}

-
    - ${e.openings.map(e=>q`
  • ${this._t("confirm.delete_partition_openings_item",{type:this._t(`opening.${e.type}`),length:li(e.length*Ov/this._gridPitch*this._cellCm,t)})}
  • `)} -
-
-
- - - -
-
`}_renderRoomDeleteDialog(){const e=this._roomDeleteDialog;return q`this._roomDeleteDialog=null}> -
-

${this._t("confirm.delete_room_body")}

-
-
- - - - -
-
`}_renderPhysicalDialog(){const e=this._physicalDialog,t="column"===e.kind;return q`this._physicalDialog=null}> -
- ${t?q` - `:G} - -
this._physicalDialog={...e,cm:t.target.value}} /> - ${this._t(this._imperial?"wallthick.unit_in":"wallthick.unit_cm")}
- ${t&&"square"===e.shape?q` - - this._physicalDialog={...e,angle:t.target.value}} />`:G} - ${e.length?q`
${this._t("physical.length")}: ${e.length}
`:G} -
- -
`}_renderMarkupBar(){const e=this._geometryHistory.undoName,t=this._geometryHistory.redoName,i=e?this._t("history.undo_named",{name:e}):this._t("history.undo_empty"),s=t?this._t("history.redo_named",{name:t}):this._t("history.redo_empty");return q`
-
- - - - - - - - - - ${this._editorToolbarGroups.map(e=>this._renderEditorGroupLauncher(e))} - - - - -
-
- -
-
`}_renderDevicesBar(){return q`
-
- - - - ${this._editorToolbarGroups.map(e=>this._renderEditorGroupLauncher(e))} -
-
- -
-
`}_renderDeviceInbox(){const e=this._deviceInbox,t=this._deviceInboxRows(),i=Object.fromEntries(["on_plan","available","hidden","readd"].map(e=>[e,t.filter(t=>t.category===e).length])),s=function(e,t,i,s=!1){const n=i.trim().toLocaleLowerCase();return e.filter(e=>e.category===t&&(!s||e.isNew)&&(!n||e.searchText.includes(n)))}(t,e.tab,e.search,"on_plan"===e.tab&&e.onlyNew),n=s.slice(0,e.limit),o=e=>this._t(`device_inbox.tab_${e}`),a=`device_inbox.empty_${e.tab}`;return q`this._deviceInbox=null}> -
-
- this._deviceInbox={...e,search:t.target.value,limit:100}} /> - -
-
- ${["on_plan","available","hidden","readd"].map(t=>q` - `)} -
-
- ${"on_plan"===e.tab?q``:G} - ${"available"===e.tab?q``:G} - -
-
- ${n.length?n.map(e=>{const t="on_plan"===e.category?e.canFind?q``:q``:"hidden"===e.category?q``:q``,i="active"===e.status.kind?"":this._t(`device_inbox.status_${e.status.kind}`);return q`
- -
-
${e.name} - ${e.isNew?q`${this._t("device_inbox.new")}`:G} -
-
- ${[e.model,e.integration,e.spaceName,e.areaName].filter(Boolean).join(" · ")} -
-
- ${this._t(`device_inbox.reason_${e.reason}`)} - ${i?q`${i}`:G} -
- ${e.binding} -
-
- ${t} - ${e.canEdit||e.canHide||"available"===e.category||"hidden"===e.category||this._bindingHasHaPage(e.binding)?q` -
- - - -
- ${e.canEdit&&("on_plan"!==e.category||e.canFind)?q``:G} - ${e.canHide?q``:G} - ${"available"===e.category?q``:G} - ${"hidden"===e.category?q``:G} - ${this._bindingHasHaPage(e.binding)?q``:G} -
-
`:G} -
-
`}):q`
${this._t(a)}
`} -
- ${s.length>n.length?q``:G} -
-
- -
-
`}_cardEntities(e){const t=this._planHass,i=[],s=new Set,n=e=>{if(!e||s.has(e)||!t.states[e])return;const n=t.entities[e];if("config"===n?.entity_category||"diagnostic"===n?.entity_category)return;s.add(e);const o=e.split(".")[0];["light","switch","fan","humidifier","siren","input_boolean"].includes(o)?i.push({eid:e,kind:"toggle"}):["cover","valve","lock","climate","media_player","vacuum","water_heater"].includes(o)?i.push({eid:e,kind:"open"}):["sensor","binary_sensor","number","select"].includes(o)&&i.push({eid:e,kind:"value"})};for(const i of ho(t,this._devices,null,this._virtualLights))if(i.device.id===e.id)for(const e of[...i.serviceEids,...i.stateEids])n(e);e.primary&&n(e.primary);for(const t of e.entities)n(t);return i.slice(0,12)}_cardToggle(e){const t=e.split(".")[0];"lock"!==t&&"alarm_control_panel"!==t&&this._planEntityAvailable(e)&&this.hass.callService("homeassistant","toggle",{entity_id:e}).catch(e=>this._showToast(this._t("toast.error",{err:this._errText(e)})))}_renderInfoCard(){const e=this._infoCard,t=e.primary?this.hass.states[e.primary]:void 0,i=t?Xi(this.hass,e.primary)?.text??t.state:null,s=(e.controls??e.marker?.controls??[]).filter(Ms).filter(e=>this._planEntityAvailable(e));return q` -
- ${(()=>{const t=this._cardEntities(e);return t.length?q`
- ${t.map(({eid:e,kind:t})=>{const i=this.hass.states[e],s=this.hass.entities[e]?.name||i?.attributes?.friendly_name||e,n=i?Xi(this.hass,e)?.text??i.state:"",o="on"===i?.state||["open","unlocked","playing","cleaning"].includes(i?.state);return q`
- - ${s} - ${"toggle"===t?q``:"open"===t?q``:q`${n}`} -
`})} -
`:G})()} - ${e.model?q`
${this._t("info.model")}${e.model}
`:G} - ${i&&!this._cardEntities(e).length?q`
${this._t("info.state")}${i}
`:G} - ${Hi(e.link)?q`
${this._t("info.link")} - ${e.link}
`:G} - ${e.description?q`
${e.description}
`:G} - ${e.pdfs&&e.pdfs.length?q`
${this._t("info.manuals")} - ${e.pdfs.map(e=>q` - ${e.name}`)}
`:G} - ${s.length?q`
${this._t("info.controls")} - - ${s.map(e=>{const t=this.hass.states[e],i="on"===t?.state;return q` - - ${t?.attributes?.friendly_name||e}`})} -
`:G} - ${e.model||i||e.link||e.description||e.pdfs&&e.pdfs.length||s.length?G:q`
${this._t("info.none")}
`} -
-
- - ${e.primary?q``:G} - -
-
`}_markerValueBadgeFields(e){return(t={touched:e.valueBadgeTouched,originalHas:e.originalHasValueBadge,original:e.originalValueBadge,enabled:e.valueBadgeEnabled,source:e.valueBadgeSource,position:e.valueBadgePosition}).touched?{value_badge:{enabled:t.enabled,source:t.source,position:t.position}}:t.originalHas?{value_badge:t.original}:{};var t}_markerDraft(e){if("ha"===e.bindingMode&&(!e.binding||"virtual"===e.binding))return null;const t=js(e.room),i=Fi(e.binding,e.devId,()=>"__hp_device_preview__"),s=this._markers.find(t=>t.id===i||t.id===e.devId),n=Zn(e.binding,e.controls,this._bindingEntities(e.binding)),o=this._effectiveStoredTapAction(e),a={id:i,binding:e.binding,name:e.name.trim()||null,icon:e.icon||null,display:"badge"!==e.display?e.display:null,ripple_color:"icon_ripple"===e.display&&e.rippleColor?e.rippleColor:null,ripple_size:"icon_ripple"===e.display&&1.5!==e.rippleSize?e.rippleSize:null,size:1!==e.size?e.size:null,angle:e.angle||null,...this._markerTapActionFields(e),...this._markerToggleEntityFields(e),tap_target:"run"===o&&e.tapTarget||null,tap_confirm:!!e.tapConfirm||null,controls:n.length?n:null,...this._markerLightFields(e),...this._markerValueBadgeFields(e),use_climate_temp:!!e.useClimateTemp||null,glow_radius_cm:(()=>{const t=$v(e.glowRadius);return null==t||t<=0?null:Math.round(this._imperial?30.48*t:100*t)})(),model:e.model.trim()||null,link:e.link.trim()||null,description:e.description.trim()||null,pdfs:e.pdfs,hidden:e.hideFromPlan,vacuum:s?.vacuum||null};return("virtual"===e.binding||e.room)&&(a.space=t?.space||("virtual"===e.binding?this._space:null),a.area=t?.area||null,a.room_id=t?.roomId||null),a}_markerPreviewDevice(e){const t=this._markerDraft(e);if(!t)return null;const i=`${this._haRegistry.revision}\n${Jd(this._markers)}\n${JSON.stringify(t)}`;if(this._markerPreviewMemo?.key===i)return this._markerPreviewMemo.device;const s=function(e){const{marker:t,siblingMarkers:i=[],...s}=e;return So({...s,markers:[...i.filter(e=>e.id!==t.id),t]}).find(e=>e.id===t.id)||null}({hass:this.hass,registry:this._haRegistry,areaToSpace:Object.fromEntries(Object.entries(this._areaToSpace).map(([e,t])=>[e,t.space])),marker:t,siblingMarkers:this._markers,settings:this._settings,excluded:this._excluded,showAll:this._showAll,firstSpaceId:this._model[0]?.id||this._space,loc:e=>this._t(e),iconRules:this._iconRules});return this._markerPreviewMemo={key:i,device:s},s}_markerPreviewDevices(e){const t=this._markerPreviewDevicesMemo;if(t?.base===this._devices&&t.preview===e)return t.devices;const i=[...this._devices.filter(t=>t.id!==e.id),e];return this._markerPreviewDevicesMemo={base:this._devices,preview:e,devices:i},i}_toggleIntent(e,t=this._devices){return Xp({hass:this._planHass,registryHass:this._fullRegistryHass,devices:t,device:e,virtualLights:this._virtualLights})}_toggleIntentForDialog(e){const t=this._markerPreviewDevice(e);if(!t)return null;const i=this._markerPreviewDevices(t);return this._toggleIntent(t,i)}_toggleStateText(e,t){const i=this._planHass?.states?.[e]||this.hass?.states?.[e];try{return i&&"function"==typeof this.hass?.formatEntityState?this.hass.formatEntityState(i):t}catch{return t}}_toggleConfirmationStateText(e){const t=String(e.state||"unknown"),i=e.entityId?this._toggleStateText(e.entityId,t):t;if(i.trim().toLocaleLowerCase()!==t.trim().toLocaleLowerCase())return i;const s={on:"confirm.state_on",off:"confirm.state_off",open:"confirm.state_open",closed:"confirm.state_closed",opening:"confirm.state_opening",closing:"confirm.state_closing",unknown:"confirm.state_unknown"}[t];return s?this._t(s):t.replaceAll("_"," ").replaceAll("-"," ")}_toggleConfirmationLines(e){const t={"turn-on":"confirm.state_on","turn-off":"confirm.state_off",open:"confirm.state_open",close:"confirm.state_closed",stop:"confirm.state_stopped"};return function(e,t){if(!Qp(e)||!e.nextEffect||!e.targets.length)return[];const i="group"===e.kind,s=i?e.targets.filter(e=>"on"===e.state).length:0,n=i?0===s?t.groupAllOff():t.groupCurrent(s,e.targets.length):t.state(e.targets[0]);let o;return o="toggle"===e.nextEffect?t.expectedByHa():i&&"turn-on"===e.nextEffect?t.groupAllOn():i&&"turn-off"===e.nextEffect?t.groupAllOff():t.effect(e.nextEffect),[t.current(n),t.expected(o),...e.skippedTargets.length?[t.unavailable(e.skippedTargets.length)]:[]]}(e,{state:e=>this._toggleConfirmationStateText(e),current:e=>this._t("confirm.current_state",{state:e}),expected:e=>this._t("confirm.expected_state",{state:e}),groupCurrent:(e,t)=>this._t("confirm.group_current",{on:e,total:t}),groupAllOn:()=>this._t("confirm.group_all_on"),groupAllOff:()=>this._t("confirm.group_all_off"),unavailable:e=>this._t("confirm.unavailable_targets",{count:e}),effect:e=>this._t(t[e]),expectedByHa:()=>this._t("confirm.expected_by_ha")})}_toggleHintLines(e){if(!e)return[];const t=e=>this._t(`marker.toggle_effect_${e.replace("-","_")}`),i=e=>this._t(`marker.toggle_skip_${e.replace("-","_")}`);return function(e,t){const i=e.targets[0]||e.skippedTargets[0],s=[];return"group"===e.kind?e.targets.length&&s.push(t.group(e.targets)):i&&s.push(t.single(i)),e.nextEffect&&e.targets.length&&s.push("group"===e.kind?t.groupCurrentNext(e.targets,e.nextEffect):t.currentNext(e.targets[0],e.nextEffect)),e.skippedTargets.length&&s.push(t.skipped(e.skippedTargets)),!e.command&&e.noneReason&&s.push(t.none(e.noneReason)),s}(e,{single:e=>{if("via"in e&&"virtual-light"===e.via)return this._t("marker.virtual_light_target",{name:e.name});const t=e.entityId||("ref"in e?e.ref:""),i=e.name||t;return this._t("marker.toggle_hint_single",{name:i,id:t})},group:e=>this._t("marker.toggle_hint_group",{count:e.length,names:e.map(e=>`${e.name} (${e.entityId})`).join(", ")}),currentNext:(e,i)=>"virtual-light"===e.via?this._t("marker.virtual_light_current",{state:this._t("on"===e.state?"marker.virtual_light_state_on":"marker.virtual_light_state_off"),effect:t(i)}):this._t("marker.toggle_hint_current",{state:this._toggleStateText(e.entityId,e.state),effect:t(i)}),groupCurrentNext:(e,i)=>this._t("marker.toggle_hint_group_current",{on:e.filter(e=>"on"===e.state).length,count:e.length,effect:t(i)}),skipped:e=>this._t("marker.toggle_hint_skipped",{count:e.length,targets:e.map(e=>{const t=e.entityId||e.ref;return`${e.name||t} (${t}: ${i(e.reason)})`}).join(", ")}),none:e=>this._t(`marker.toggle_none_${e.replaceAll("-","_")}`)})}_effectiveStoredTapAction(e,t){return e.tapActionTouched?e.tapAction:Vp(e.originalHasTapAction?e.originalTapAction:null,t)}_effectiveMarkerTapAction(e,t=this._markerPreviewDevice(e)){return this._effectiveStoredTapAction(e,t?.primary?.split(".")[0])}_announceToggleDraft(e){const t=this._markerPreviewDevice(e),i=e.tapActionTouched?e:{...e,tapAction:this._effectiveMarkerTapAction(e,t)},s="toggle"===i.tapAction?this._toggleHintLines(this._toggleIntentForDialog(i)).join(" "):"";return{...i,tapHintAnnouncement:s}}_valueBadgeForBinding(e,t){const i={...e,binding:t,valueBadgeEnabled:!1,valueBadgeSource:null,valueBadgeTouched:!0},s=this._markerPreviewDevice(i);if(!s)return{valueBadgeEnabled:!1,valueBadgeSource:null,valueBadgeTouched:!0};const n=[...this._devices.filter(e=>e.id!==s.id),s],o=qo(this._planHass,s,n),a=Wo(this._planHass,s,o);return{valueBadgeEnabled:e.valueBadgeEnabled&&!!a,valueBadgeSource:a,valueBadgeTouched:!0}}_markerSpatialSource(e){const t=this._markerPreviewDevice(e);if(!t)return null;const i={...t,hidden:!1};return po(ho(this._planHass,[...this._devices.filter(e=>e.id!==i.id),i],null,this._virtualLights).filter(e=>e.device.id===i.id))}_markerAutoHasSpatialSource(e){const t={...e,lightRole:"auto",lightRoleTouched:!0},i=this._markerPreviewDevice(t);return!!i&&lo(this._planHass,{...i})}_setMarkerLightRole(e){const t=this._markerDialog;t&&(this._markerDialog={...t,lightRole:e,lightRoleTouched:!0})}_controlRefInfo(e){if(!e.startsWith("marker:"))return{label:this.hass.states[e]?.attributes?.friendly_name||e,sub:e,icon:e.startsWith("light.")?"mdi:lightbulb":"mdi:toggle-switch",warning:!this._planEntityAvailable(e)};const t=e.slice(7),i=this._markers.find(e=>e.id===t),s=this._devices.find(e=>e.id===t);if(!i||i.removed||!0!==i.is_light)return{label:this._t("marker.control_missing_label"),sub:`${this._t("marker.control_broken")} (${e})`,icon:"mdi:alert-outline",warning:!0};const n=this._serverCfg?.spaces.find(e=>e.id===(s?.space||i.space)),o=!!s&&eo(s).length>0;return{label:i.name||s?.name||t,sub:[n?.title||s?.space||i.space,i.room_id||s?.area,o?"":this._t("marker.control_passive")].filter(Boolean).join(" · "),icon:i.icon||s?.icon||"mdi:lightbulb-outline",warning:!0===i.hidden||!0===s?.hidden}}_valueBadgeCandidateLabel(e){const t=e.source;if("derived_lqi"===t.kind)return this._t("marker.value_badge_lqi");if("derived_marker_state"===t.kind)return this._t("marker.value_badge_marker_state",{name:e.label});if("entity_state"===t.kind)return this._t("marker.value_badge_state",{name:e.label});const i=this.hass.states[t.entity_id]?.attributes?.friendly_name||this._fullRegistryHass.entities[t.entity_id]?.name||t.entity_id;return this._t(`marker.value_badge_attr_${t.attribute}`,{name:i})}_controlCandidates(e){const t=this._markerDraft(e)?.id||e.devId||"",i=[],s=new Set;for(const e of this._markers){if(e.id===t||e.removed||e.hidden||!0!==e.is_light)continue;if("virtual"!==e.binding&&"active"!==this._bindingStatus(e.binding).kind)continue;const n=this._devices.find(t=>t.id===e.id);if(!n||n.hidden)continue;const o=this._controlRefInfo(`marker:${e.id}`);for(const e of eo(n))s.add(e);i.push({value:`marker:${e.id}`,label:o.label,sub:o.sub,icon:o.icon,search:`${o.label} ${o.sub} ${e.id} ${eo(n).join(" ")}`.toLowerCase()})}const n=Object.keys(this.hass.states||{}).filter(t=>Ms(t)&&!s.has(t)&&Xn(e.binding,[t],this._bindingEntities(e.binding)).length>0&&this._planEntityAvailable(t)).map(e=>{const t=this._controlRefInfo(e);return{value:e,label:t.label,sub:e,icon:t.icon,search:`${t.label} ${e}`.toLowerCase()}}),o=e.controlsFilter.trim().toLowerCase();return[...i,...n].filter(t=>!e.controls.includes(t.value)&&(!o||t.search.includes(o))).slice(0,12).map(({search:e,...t})=>t)}_addControlRef(e,t){if(t.startsWith("marker:")){const i=this._markerDraft(e)?.id||e.devId||"";if(!i)return void this._showToast(this._t("toast.marker_binding_required"));const s=t.slice(7),n=this._markerDraft(e);if(function(e,t,i){if(!t||t===i)return!0;const s=new Map;for(const t of e)s.set(t.id,(t.controls||[]).filter(e=>"string"==typeof e&&e.startsWith("marker:")).map(e=>e.slice(7)));const n=new Set,o=[i];for(;o.length;){const e=o.pop();if(e===t)return!0;n.has(e)||(n.add(e),o.push(...s.get(e)||[]))}return!1}(n?[...this._markers.filter(e=>e.id!==i),n]:this._markers,i,s))return void this._showToast(this._t("toast.marker_control_cycle"))}this._markerDialog=this._announceToggleDraft({...e,controls:[...e.controls,t],controlsFilter:""})}_setMarkerGlowMode(e){const t=this._markerDialog;if(!t)return;if("auto"===e)return void(this._markerDialog={...t,glowMode:e,glowTouched:!0});const i=this._markerSpatialSource(t),s=xs(i?this._planHass.states[i.eid]:void 0,null,this._fillColors.glow_light.c),n=!t.glowColorDrafted,o="fixed"===e&&!t.glowBrightnessDrafted;this._markerDialog={...t,glowMode:e,glowColor:n?s.c:t.glowColor,glowBrightness:o?Math.max(1,Math.round(100*s.bri)):t.glowBrightness,glowColorDrafted:!0,glowBrightnessDrafted:t.glowBrightnessDrafted||"fixed"===e,glowTouched:!0}}_renderMarkerDialog(){const e=this._markerDialog,t="virtual"===e.bindingMode,i=this._bindingCandidates();this._bindingEntities(e.binding);const s=t?null:this._bindingStatus(e.binding),n=!t&&this._bindingHasHaPage(e.binding),o=this._markerPreviewDevice(e),a=this._effectiveMarkerTapAction(e,o),r=o?cs(this._serverCfg?.spaces.find(e=>e.id===o.space)):null,l=o?this._markerPreviewDevices(o):this._devices,c="toggle"===a&&o?this._toggleIntent(o,l):null,h=this._toggleHintLines(c),d=o?N_(this._planHass,o,{liveStates:!1!==this._config?.live_states,showTemperature:!1!==this._config?.show_temperature,showSignal:r?.showLqi??!1!==this._config?.show_signal,designPreview:!0,activityRuntime:this._activityRt.get(o.id),lightDevices:l,registryHass:this._fullRegistryHass,reducedMotion:this._reducedMotion}):null,p=o?qo(this._planHass,o,l):[],u=o?Wo(this._planHass,o,p):null,_=e.valueBadgeTouched?e.valueBadgeEnabled:!!d?.valueBadge,m=e.valueBadgeTouched?e.valueBadgeSource:d?.valueBadge?.source||e.valueBadgeSource,g=e.valueBadgeTouched?e.valueBadgePosition:d?.valueBadge?.position||e.valueBadgePosition,f=Ao(m),v=!!m&&!p.some(e=>e.key===f),b=p.find(e=>e.key===f),y=d?.valueSource?d.valueSource.attribute?`attr:${d.valueSource.eid}:${d.valueSource.attribute}`:`state:${d.valueSource.eid}`:"",w=this._markerAutoHasSpatialSource(e),k=!!o&&function(e,t){return io(e,t).some(e=>!!e.eid)}(this._planHass,{...o}),x=function(e,t,i,s){const n="always"===e||"auto"===e&&t,o=n&&!i;return{sourceExists:n,fromSourceEnabled:n&&i,manualEnabled:n,radiusEnabled:n,passive:o,effectiveMode:o&&"auto"===s?"fixed":s}}(e.lightRole,w,k,e.glowMode),$=!x.sourceExists,S=!x.fromSourceEnabled,M=x.passive,C=x.effectiveMode,D="never"===e.lightRole?this._t("marker.glow_disabled_never"):"auto"!==e.lightRole||w?M?this._t("marker.glow_passive_hint"):this._t("marker.glow_disabled_no_entity"):this._t("marker.glow_disabled_auto"),P=o?eo(o):[],T=o&&to(o)||"",I=!!e.lightEntity&&!P.includes(e.lightEntity),R=o?jp(o):[],A=!!e.toggleEntity&&!R.includes(e.toggleEntity),z="toggle"===a?this._toggleIntentForDialog({...e,toggleEntity:"",toggleEntityTouched:!0}):null,F=z?[...z.targets,...z.skippedTargets].map(e=>e.entityId||("ref"in e?e.ref:"")).filter(Boolean).join(", "):"",N=(()=>{if(t)return null;const s=i.find(t=>t.value===e.binding);if(s)return s.label;const[n,o]=e.binding.split(":");return"device"===n?this._fullRegistryHass.devices[o]?.name_by_user||this._fullRegistryHass.devices[o]?.name||o:this._fullRegistryHass.entities[o]?.name||this.hass.states[o]?.attributes?.friendly_name||o})();return q` -
- ${"ha_disabled"===s?.kind?q`
- - ${this._t(`marker.ha_disabled_${s.reason}`)} - ${n?q``:G} -
`:"unverified"===s?.kind&&e.binding?q`
- - ${this._t("marker.ha_registry_limited")} -
`:G} - - this._markerDialog={...e,name:t.target.value}} /> - - -
- -
- - -
- ${"ha"===e.bindingMode?q` - ${e.bindingOpen?q`
- this._markerDialog={...e,bindingFilter:t.target.value}} /> -
- ${i.map(t=>q`
{const i={...e,binding:t.value,bindingOpen:!1,controls:Zn(t.value,e.controls,this._bindingEntities(t.value)),autoIcon:this._autoIconForBinding(t.value)};this._markerDialog=this._announceToggleDraft({...i,...this._valueBadgeForBinding(i,t.value)})}}> - ${t.label}${t.sub} -
`)} - ${i.length?G:q`
${this._t("marker.nothing_found")}
`} -
-
`:G}`:G} -
- - - - - ${this._renderVacSection(e)} - - - - ${"toggle"===a&&(R.length>1||A)?q`
-
- - ${this._help("marker.toggle_entity.help")} -
- - ${A?q`

- - ${this._t("marker.toggle_entity_missing",{entity:e.toggleEntity,fallback:F||this._t("marker.toggle_entity_none")})} -

`:G} -
`:G} - ${"toggle"===a?q`
- ${h.map(e=>q`
${e}
`)} -
-
${e.tapHintAnnouncement}
`:G} - ${"run"===a?(()=>{const t=e.runFilter.trim().toLowerCase(),i=this._runCandidates().filter(e=>!t||e.label.toLowerCase().includes(t)||e.value.includes(t)),s=e.tapTarget?this._runCandidates().find(t=>t.value===e.tapTarget):null;return q` - - ${e.tapTarget&&!s?q`
${this._t("marker.run_target_gone",{id:e.tapTarget})}
`:G} - {e.target.select()}} - @input=${t=>this._markerDialog={...e,runFilter:t.target.value,tapTarget:""}} /> - ${s?G:q`
- ${i.slice(0,40).map(t=>q`
this._markerDialog={...e,tapTarget:t.value,runFilter:""}}> - ${t.label}${t.sub} -
`)} - ${i.length?G:q`
${this._t("marker.nothing_found")}
`} -
`}`})():G} - ${"run"===a||"toggle"===a?q``:G} - - -
${this._t("marker.controls_hint")}
- ${e.controls.length?q`
- ${e.controls.map(t=>{const i=this._controlRefInfo(t);return q` - ${i.label} - this._markerDialog=this._announceToggleDraft({...e,controls:e.controls.filter(e=>e!==t)})}> - `})} -
`:G} - this._markerDialog={...e,controlsFilter:t.target.value}} /> - ${e.controlsFilter.trim()?q`
- ${this._controlCandidates(e).map(t=>q``)} -
`:G} - - ${this._bindingHasClimate(e.binding)?q``:G} -
- ${this._t("marker.light_role_label")}${this._help("marker.light_role.help")} -
- - - -
-
- - ${"always"===e.lightRole&&(P.length>1||I)?q`
-
- - ${this._help("marker.light_entity.help")} -
- - ${I?q`

- - ${this._t("marker.light_entity_missing",{entity:e.lightEntity,fallback:T||"—"})} -

`:G} -
`:G} - -
- ${this._t("marker.glow_color_label")}${this._help("marker.glow_mode.help")} -
- - - -
- ${"auto"!==C?q`
- {this._markerDialog={...e,glowMode:C,glowColor:t.detail.color,glowColorDrafted:!0,glowTouched:!0}}}> - ${"fixed"===C?q` - ${this._t("marker.glow_brightness")} - ${this._rangeInput(1,100,1,e.glowBrightness,t=>{this._markerDialog={...e,glowMode:C,glowBrightness:t,glowBrightnessDrafted:!0,glowTouched:!0}},$,this._t("marker.glow_brightness"))} - ${Math.round(e.glowBrightness)}%`:G} -
`:G} -
-
-
- - ${this._help("marker.glow_radius.help")} -
-
- this._markerDialog={...e,glowRadius:t.target.value}} /> - ${this._imperial?this._t("gs.unit_ft"):this._t("gs.unit_m")} -
-
- ${$||M?q`

- ${D} -

`:G} - - - ${customElements.get("ha-icon-picker")?q`{const i=t.detail.value||"";(e.icon||i!==e.autoIcon)&&(this._markerDialog={...e,icon:i})}}>`:q`this._markerDialog={...e,icon:t.target.value}} />`} - ${!e.icon&&e.autoIcon?q`

- ${this._t("marker.icon_auto",{icon:e.autoIcon})} -

`:G} - - - -

${this._t(yv[e.display])}

- ${"static_icon"===e.display&&this._bindingHasAlarm(e.binding)?q`
- - ${this._t("marker.static_alarm_warning")} -
`:G} -
- ${this._t("marker.value_badge_title")}${this._help("marker.value_badge.help")} - - ${"static_icon"===e.display?q`

- ${this._t("marker.value_badge_static")} -

`:p.length||e.valueBadgeSource?G:q`

- ${this._t("marker.value_badge_empty")} -

`} - ${_?q` -
- - ${this._help("marker.value_badge_source.help")} -
- - ${b?q`

${b.technical}

`:G} - ${v?q`

- ${this._t("marker.value_badge_missing_hint")} -

`:G} - ${"value"===e.display&&f===y?q`

- ${this._t("marker.value_badge_duplicate")} -

`:G} -
- - ${this._help("marker.value_badge_position.help")} -
- - `:G} -
- ${d?q` - `:q`
- - ${this._t("marker.preview.select_source")} -
`} - ${"icon_ripple"===e.display?q`
- {this._markerDialog={...e,rippleColor:t.detail.color}}}> -
-
- ${this._t("marker.ripple_size")} - ${this._rangeInput(1,8,.5,e.rippleSize,t=>this._markerDialog={...e,rippleSize:t})} - ×${e.rippleSize} -
-

${this._t("marker.activity_alarm_note")}

`:G} - - -
- ${this._rangeInput(.5,3,.1,e.size,t=>this._markerDialog={...e,size:t})} - ×${e.size.toFixed(1)} - ${this._t("marker.angle_label")} - ${""} - ${this._rangeInput(0,355,5,e.angle,t=>this._markerDialog={...e,angle:t})} - ${e.angle}° -
- - - this._markerDialog={...e,model:t.target.value}} /> - - - this._markerDialog={...e,link:t.target.value}} /> - - - - - -
- ${e.pdfs.map(e=>q` - ${e.name} - this._removeMarkerPdf(e.url)}>`)} - - - this._pickMarkerFiles(e)} /> - -
-
-
-
- ${e.devId?q``:G} - ${e.devId?q``:G} -
-
- - -
-
-
`}_renderSpaceDialog(){const e=this._spaceDialog,t=this._importTotal>0&&"create"===e.mode?this._t("import.progress",{i:this._importTotal-this._importQueue.length,n:this._importTotal}):"",i=()=>{this._spaceDialog=null,this._importQueue=[],this._importTotal=0};return q` -
- - this._spaceDialog={...e,title:t.target.value}} /> - - - ${"file"===e.source?q`
- ${e.planFile?q`${e.planFile.name}`:e.planUrl?q`${this._t("space.plan_alt")}`:q`${this._t("space.no_plan")}`} - - - this._pickPlanFile(e)} /> - - -
- ${e.pickSaved?this._renderServerPlans(e):G}`:G} - - - -
- {const i=t.target.value,s=$v(i),n=null==s?null:(o=s,this._imperial?o*Mh:o);var o;this._spaceDialog={...e,cellCmInput:i,cellCmTouched:!0,cellCm:null!=n&&n>0?Math.max(.1,Math.min(kv,n)):e.cellCm}}} /> - ${this._t(this._imperial?"space.scale_unit_imperial":"space.scale_unit")} -
- - - - - -
${this._t("space.zero_wall_help")}
- - - ${""} - -
${this._t("space.hide_decor_tip")}
- -
${this._t("space.hide_openings_tip")}
- - ${[["labelTemp","space.label_temp"],["labelHum","space.label_hum"],["labelLqi","space.label_lqi"],["labelLight","space.label_light"]].map(([t,i])=>q``)} - -
- ${this._rangeInput(50,300,5,Math.round(100*e.cardFontScale),t=>this._spaceDialog={...e,cardFontScale:t/100})} - ${Math.round(100*e.cardFontScale)}% -
- ${this._renderCardPreview(e.cardFontScale,1,1)} -
- {this._spaceDialog={...e,roomColor:t.detail.color,roomOpacity:t.detail.opacity}}}> -
- - - ${"static"===(e.bgMode??Fr(this._settings,{}))?q`
- {this._spaceDialog={...e,bgColor:t.detail.color}}}> - ${e.bgColor?q``:q`${this._t("space.bg_inherited")}`} -
`:G} - -
- {const i=t.target.value.trim(),s=""===i?null:Math.round(Number(i));this._spaceDialog={...e,northDeg:null!==s&&Number.isFinite(s)?Math.min(359,Math.max(0,s)):null}}} /> - ${null===e.northDeg?this._t("space.north_inherited",{v:null===zr(this._settings,{})?"—":String(zr(this._settings,{}))+"°"}):"°"} -
- - - - ${qi.map(e=>[e,"fill."+e]).map(([t,i])=>q` - ${"custom"===t&&"custom"===e.fillMode?q`
- ${this._t("space.custom_fill")} - {this._spaceDialog={...e,customFill:{c:t.detail.color,a:t.detail.opacity}}}}> - ${e.customFill?q``:G} -
`:G}`)} - - ${e.deleteBlockers?q``:G} -
- -
`}_renderMergeDialog(){const e=this._mergeDialog,t=this._spaceModel()?.rooms||[],i=(i,s)=>{const n=t.find(e=>e.id===i),o=n?.area?this.hass.areas[n.area]?.name:null;return q``};return q`this._mergeDialog=null}> -
-

${this._t("merge.hint")}

- - ${i(e.aId,"a")} - ${i(e.bId,"b")} -
-
- - - -
-
`}_renderCardPreview(e,t,i){const s=18*e;return q`
- - ${this._t("preview.room_name")} - - 22.4° · - 45% · - ${this._t("roomcard.light_partial",{on:1,total:3})} - -
`}_renderRoomSource(e){const t="temp"===e?this._roomTempSrc:this._roomHumSrc,i=t=>{"temp"===e?this._roomTempSrc=t:this._roomHumSrc=t,this.requestUpdate()},s=this._roomSrcOpen===e;return q` - - - - ${t||s?q` - ${s?q`
- {this._roomSrcFilter=e.target.value,this.requestUpdate()}} /> -
- ${this._roomSrcCandidates().map(e=>q`
{i(e.value),this._roomSrcOpen=null}}> - ${e.label}${e.sub} -
`)} -
-
`:G}`:G}`}_renderRoomDialog(){const e=!!this._roomEditId,t=e?null:this._wallFaceBatch,i=t&&t.candidates.length>1?this._t("room.queue_progress",{current:t.index+1,total:t.candidates.length}):"",s=!!this._areaSel||!!this._nameSel.trim(),n=cs(this._curSpaceCfg),o=this._roomFill||n.fill,a=this._roomCustomFill||n.customFill,r=[...this._freeAreas];if(e&&this._areaSel&&!r.some(e=>e.area_id===this._areaSel)){const e=this.hass.areas[this._areaSel];e&&r.unshift(e)}return q` -
- ${i?q`

- ${i} -

`:G} - - this._nameSel=e.target.value} /> - - - - - - ${[["","fill.inherit"],...Wi.map(e=>[e,"fill."+e])].map(([e,t])=>q``)} - ${"custom"===o?q`
- ${this._roomCustomFill?this._t("room.custom_fill_own"):this._t("room.custom_fill_space")} - {this._roomCustomFill={c:e.detail.color,a:e.detail.opacity}}}> - ${this._roomCustomFill?q``:G} -
`:G} - ${this._renderRoomSource("temp")} - ${this._renderRoomSource("hum")} - - - -
- ${this._rangeInput(50,300,5,Math.round(100*this._roomNameScale),e=>{this._roomNameScale=e/100,this.requestUpdate()})} - ${Math.round(100*this._roomNameScale)}% -
- -
- ${this._rangeInput(50,300,5,Math.round(100*this._roomLabelScale),e=>{this._roomLabelScale=e/100,this.requestUpdate()})} - ${Math.round(100*this._roomLabelScale)}% -
- ${this._renderCardPreview(cs(this._curSpaceCfg).cardFontScale,this._roomNameScale,this._roomLabelScale)} -
-
- - - ${e?q``:q`${this._pendingSplit?G:q``} - `} -
-
`}}Vv.properties={_tabDrag:{state:!0},_hdrH:{state:!0},_booting:{state:!0},_bootFading:{state:!0},_bootSoft:{state:!0},_continuityEpoch:{state:!0},_tapConfirm:{state:!0},hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_hoverRoom:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_mode:{state:!0},_tool:{state:!0},_wallDialog:{state:!0},_drawWallField:{state:!0},_activeDraftId:{state:!0},_physicalSel:{state:!0},_physicalDialog:{state:!0},_partitionDeleteDialog:{state:!0},_roomDeleteDialog:{state:!0},_physicalDrag:{state:!0},_physicalRotate:{state:!0},_duplicateColumnId:{state:!0},_opMeasure:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_mergeSel:{state:!0},_openingPreset:{state:!0},_openingDialog:{state:!0},_openingInfo:{state:!0},_mergeDialog:{state:!0},_splitSel:{state:!0},_decorTool:{state:!0},_decorStyle:{state:!0},_decorDraft:{state:!0},_decorSel:{state:!0},_decorEraseConfirm:{state:!0},_decorTextDialog:{state:!0},_decorShapeDialog:{state:!0},_backdropDialog:{state:!0},_furnPalette:{state:!0},_bdDrag:{state:!0},_dtBox:{state:!0},_dtDrag:{state:!0},_kioskDialog:{state:!0},_vacFit:{state:!0},_vacAllCamerasFor:{state:!0},_vacCalConfirm:{state:!0},_kioskDots:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_roomEditId:{state:!0},_roomFill:{state:!0},_roomCustomFill:{state:!0},_roomTempSrc:{state:!0},_roomHumSrc:{state:!0},_roomSrcOpen:{state:!0},_roomSrcFilter:{state:!0},_roomNameScale:{state:!0},_roomLabelScale:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_deviceInbox:{state:!0},_rulesDialog:{state:!0},_settingsDialog:{state:!0},_alignDialog:{state:!0},_preflightClipboardFallback:{state:!0},_backupExportDialog:{state:!0},_backupImportDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},Vv.ZOOM_MAX=8,Vv.ZOOM_MIN=1/3,Vv.styles=[Ke,km],customElements.get("houseplan-card")||customElements.define("houseplan-card",Vv),window.customCards=window.customCards||[],window.customCards.find(e=>"houseplan-card"===e.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info(`%c HOUSEPLAN-CARD %c v${vv} `,"background:#3ea6ff;color:#04121f;font-weight:700",""); +globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="f111431804c4c42c6e35286f29720d089bc8b77ffc364a6517f7a3510fcb11c8";export{d3 as HouseplanCard}from"./houseplan-assets/houseplan-card-Cs2QHIcW.js"; diff --git a/custom_components/houseplan/frontend_asset_manifest.py b/custom_components/houseplan/frontend_asset_manifest.py new file mode 100644 index 00000000..dc87e905 --- /dev/null +++ b/custom_components/houseplan/frontend_asset_manifest.py @@ -0,0 +1,37 @@ +"""Pure manifest/path resolver for generated frontend assets.""" +from __future__ import annotations + +import json +from pathlib import Path + +_MANIFEST = "houseplan-assets.json" +_ASSET_DIR = "houseplan-assets" + + +def resolve_frontend_asset(frontend_root: Path, filename: object) -> Path | None: + """Resolve one generated JS asset only when the current manifest lists it.""" + if not isinstance(filename, str) or not filename or filename != Path(filename).name: + return None + if "/" in filename or "\\" in filename or filename in {".", ".."}: + return None + if not filename.endswith(".js"): + return None + try: + manifest = json.loads((frontend_root / _MANIFEST).read_text(encoding="utf-8")) + except (OSError, ValueError, TypeError): + return None + if not isinstance(manifest, dict) or manifest.get("schema") != 1: + return None + expected = f"{_ASSET_DIR}/{filename}" + listed = { + item.get("path") + for item in manifest.get("files", []) + if isinstance(item, dict) + } + if expected not in listed: + return None + asset_root = (frontend_root / _ASSET_DIR).resolve() + candidate = (asset_root / filename).resolve() + if candidate.parent != asset_root or not candidate.is_file(): + return None + return candidate diff --git a/custom_components/houseplan/frontend_assets.py b/custom_components/houseplan/frontend_assets.py new file mode 100644 index 00000000..bb3956a5 --- /dev/null +++ b/custom_components/houseplan/frontend_assets.py @@ -0,0 +1,36 @@ +"""Public, manifest-gated JavaScript chunks for the Lovelace card.""" +from __future__ import annotations + +from pathlib import Path + +from aiohttp import web +from homeassistant.components.http import HomeAssistantView +try: + from homeassistant.components.http import KEY_HASS +except ImportError: # pragma: no cover - old HA compatibility + KEY_HASS = "hass" # type: ignore[assignment] + +from .const import FRONTEND_ASSETS_URL +from .frontend_asset_manifest import resolve_frontend_asset + +_FRONTEND_ROOT = Path(__file__).parent / "frontend" + + +class HouseplanFrontendAssetView(HomeAssistantView): + """Serve only generated chunks named by the current build manifest.""" + + url = f"{FRONTEND_ASSETS_URL}/{{filename}}" + name = "api:houseplan:frontend-asset" + requires_auth = False + + async def get(self, request: web.Request, filename: str) -> web.StreamResponse: + hass = request.app[KEY_HASS] + path = await hass.async_add_executor_job( + resolve_frontend_asset, _FRONTEND_ROOT, filename + ) + if path is None: + raise web.HTTPNotFound() + return web.FileResponse( + path, + headers={"Cache-Control": "no-cache", "X-Content-Type-Options": "nosniff"}, + ) diff --git a/demo/bundle-freshness.mjs b/demo/bundle-freshness.mjs index 811776fb..42c56b37 100644 --- a/demo/bundle-freshness.mjs +++ b/demo/bundle-freshness.mjs @@ -1,5 +1,6 @@ -import { existsSync } from 'node:fs'; -import { resolve } from 'node:path'; +import { createHash } from 'node:crypto'; +import { existsSync, readFileSync } from 'node:fs'; +import { relative, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; import { sourceFingerprint } from '../scripts/source-fingerprint.mjs'; @@ -13,17 +14,39 @@ const fingerprintForTree = async (root) => { return module.sourceFingerprint(root); }; +const verifyManifestTree = (root, expected) => { + const assetRoot = resolve(root, 'demo/srv/assets'); + const manifestPath = resolve(assetRoot, 'houseplan-assets.json'); + // Comparative benchmarks can target releases from before the multi-asset + // contract. Their own embedded fingerprint remains the legacy authority. + if (!existsSync(manifestPath)) return; + const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')); + if (manifest?.schema !== 1 || manifest.fingerprint !== expected + || !Array.isArray(manifest.files)) { + throw new Error('demo bundle manifest is stale or malformed; run npm run bundle:sync'); + } + for (const file of manifest.files) { + const path = resolve(assetRoot, String(file?.path || '')); + const rel = relative(assetRoot, path); + if (!rel || rel.startsWith('..') || rel.includes(':') || !existsSync(path)) { + throw new Error(`demo bundle manifest asset is missing or unsafe: ${file?.path}`); + } + const hash = createHash('sha256').update(readFileSync(path)).digest('hex'); + if (hash !== file.sha256) throw new Error(`demo bundle asset hash mismatch: ${file.path}`); + } +}; + /** Refuse measurements/screenshots made by a committed bundle from old source. */ export async function assertFreshDemoBundle(page, root = process.cwd()) { // A comparative performance run may load an older tree whose fingerprint // contract is intentionally different from the candidate's. Validate that // tree with the implementation that built it, not with today's algorithm. const expected = await fingerprintForTree(root); + verifyManifestTree(root, expected); const loaded = await page.evaluate(() => globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__ ?? null); if (loaded !== expected) { throw new Error( - 'demo/srv/assets/houseplan-card.js is stale. Run npm run build and copy ' - + 'dist/houseplan-card.js to demo/srv/assets/houseplan-card.js first. ' + 'demo/srv/assets bundle tree is stale. Run npm run bundle:sync first. ' + `Expected ${expected}, loaded ${loaded || 'no fingerprint'}.`, ); } diff --git a/demo/golden/harness.mjs b/demo/golden/harness.mjs index 117e1601..a54aa07c 100644 --- a/demo/golden/harness.mjs +++ b/demo/golden/harness.mjs @@ -773,6 +773,12 @@ export async function prepareGoldenScenario(page, scenario) { const expectedDevices = Object.keys(fixture.devices || {}).length; if (expectedDevices) await until(() => card._devices?.length >= expectedDevices); await until(() => card._booting === false); + // Golden scenarios intentionally call internal editor commands directly. + // Preload the lazy runtime for that legacy harness contract; cold-View + // loading and retry semantics are covered by smoke_lazy_editor_chunk. + if (!(await card._ensureEditorRuntime())) { + throw new Error(`golden editor runtime failed to load: ${scenario.id}`); + } await frame(); return card; }; @@ -1492,7 +1498,8 @@ export async function prepareGoldenScenario(page, scenario) { }); } const raw = scenario.cardEditorInvalidDefaultFloor; - const editor = document.createElement('houseplan-card-editor'); + const cardClass = customElements.get('houseplan-card'); + const editor = await cardClass.getConfigElement(); editor.setConfig({ type: 'custom:houseplan-card', title: 'Golden invalid floor', language: scenario.language || 'en', default_floor: raw, diff --git a/demo/serve.mjs b/demo/serve.mjs index 5471ebc0..0819b192 100644 --- a/demo/serve.mjs +++ b/demo/serve.mjs @@ -44,7 +44,8 @@ export async function finish(browser, out) { } } -export async function launch( +async function launchInternal( + preloadEditorRuntime, viewport = { width: 820, height: 760 }, scale = 1, browserArgs = [], @@ -57,7 +58,7 @@ export async function launch( viewport, deviceScaleFactor: scale, ...contextOptions, })).newPage(); // audit T1: an exception inside the card used to be logged and ignored - page.on('pageerror', (e) => { _pageErrors++; console.log('EXC', e.message); }); + page.on('pageerror', (e) => { _pageErrors++; console.log('EXC', e.stack || e.message); }); await page.route('**/*', (r) => { const u = new URL(r.request().url()); let p = decodeURIComponent(u.pathname); @@ -84,5 +85,15 @@ export async function launch( // hass flows continuously in production; the stub sets it once — nudge a rebuild await page.evaluate(() => { const c = window.__card; c.hass = { ...c.hass }; }); await page.waitForFunction(() => window.__card._devices.length > 0, { timeout: 9000 }); + // Existing product smokes exercise editor internals directly. They preload + // the new #337 runtime without changing mode; the dedicated network smoke + // uses launchColdView() and proves the real cold-View boundary separately. + if (preloadEditorRuntime) { + const ready = await page.evaluate(() => window.__card._ensureEditorRuntime()); + if (!ready) throw new Error('editor runtime did not preload for browser smoke'); + } return { page, browser }; } + +export const launch = (...args) => launchInternal(true, ...args); +export const launchColdView = (...args) => launchInternal(false, ...args); diff --git a/demo/smoke_lazy_editor_chunk.mjs b/demo/smoke_lazy_editor_chunk.mjs new file mode 100644 index 00000000..3cd51243 --- /dev/null +++ b/demo/smoke_lazy_editor_chunk.mjs @@ -0,0 +1,150 @@ +// #337: the display-only card must stay independent from the editor runtime. +// Exercise the production bundle, including its content-hashed retry URL. +import { readFileSync } from 'node:fs'; +import { launchColdView, checkAll, finish } from './serve.mjs'; + +const manifest = JSON.parse(readFileSync('dist/houseplan-assets.json', 'utf8')); +const runtimePath = manifest.files + .map((file) => file.path) + .find((path) => /houseplan-editor-runtime-[^/]+\.js$/.test(path)); +if (!runtimePath) throw new Error('editor runtime is absent from the bundle manifest'); +const runtimeName = runtimePath.split('/').at(-1); +const runtimeUrlPattern = `**/${runtimeName}*`; +const runtimeFile = `demo/srv/assets/${runtimePath}`; +const onboardingPath = manifest.files + .map((file) => file.path) + .find((path) => /houseplan-onboarding-runtime-[^/]+\.js$/.test(path)); +if (!onboardingPath) throw new Error('onboarding runtime is absent from the bundle manifest'); +const onboardingName = onboardingPath.split('/').at(-1); + +const clickEditor = async (page, index, mode) => { + await page.locator('houseplan-card').evaluate((card, tabIndex) => { + const root = card.shadowRoot || card.renderRoot; + root.querySelectorAll('.modetab')[tabIndex]?.click(); + }, index); + await page.waitForFunction((expected) => window.__card._mode === expected, mode); + await page.waitForFunction(() => window.__card._modeTransitionBusy === false); +}; + +const { page, browser } = await launchColdView(); +const requested = []; +page.on('request', (request) => requested.push(new URL(request.url()).pathname)); +const initialResources = await page.evaluate(() => performance.getEntriesByType('resource') + .map((entry) => new URL(entry.name).pathname)); +const out = { + editorAbsentBeforeIntent: !initialResources.some((path) => path.endsWith(`/${runtimeName}`)), +}; + +await clickEditor(page, 0, 'plan'); +await clickEditor(page, 1, 'devices'); +await clickEditor(page, 2, 'decor'); +out.oneRuntimeRequestForAllEditors = requested.filter((path) => path.endsWith(`/${runtimeName}`)).length === 1; +out.allEditorsUseInstalledRuntime = await page.evaluate(() => + window.__card._editorRuntimeLoader.state === 'ready' && window.__card._mode === 'decor'); + +// Empty-install onboarding is a separate lazy surface. It may fetch its own +// dialog chunk, but must not fetch or install the editor until Save explicitly +// continues into Plan mode. +const onboarding = await launchColdView(); +const onboardingRequests = []; +onboarding.page.on('request', (request) => { + onboardingRequests.push(new URL(request.url()).pathname); +}); +await onboarding.page.evaluate(async () => { + const card = window.__card; + card._onboardingShown = false; + card._serverCfg = { ...card._serverCfg, spaces: [] }; + card._model = []; + card.hass = { ...card.hass, floors: {}, areas: {} }; + card.requestUpdate(); + await card.updateComplete; +}); +await onboarding.page.waitForFunction(() => { + const card = window.__card; + return card._onboardingRuntime && card.renderRoot.querySelector('hp-dialog'); +}); +out.onboardingUsesOwnChunk = onboardingRequests + .filter((path) => path.endsWith(`/${onboardingName}`)).length === 1; +out.onboardingDoesNotLoadEditor = onboardingRequests + .every((path) => !path.endsWith(`/${runtimeName}`)) + && await onboarding.page.evaluate(() => !window.__card._editorRuntime); +await onboarding.page.evaluate(async () => { + const card = window.__card; + const root = card.renderRoot; + const title = root.querySelector('hp-dialog input.namein'); + title.value = 'Cold onboarding'; + title.dispatchEvent(new InputEvent('input', { bubbles: true })); + await card.updateComplete; + const sources = root.querySelectorAll('hp-dialog input[name="plansrc"]'); + sources[1].click(); + await card.updateComplete; + const buttons = [...root.querySelectorAll('hp-dialog button')]; + buttons.find((button) => button.textContent.includes(card._t('btn.save')))?.click(); +}); +await onboarding.page.waitForFunction(() => { + const card = window.__card; + return card._editorRuntime && card._mode === 'plan' + && card._serverCfg.spaces.some((space) => space.title === 'Cold onboarding'); +}); +out.onboardingSaveContinuesToPlan = onboardingRequests + .filter((path) => path.endsWith(`/${runtimeName}`)).length === 1; + +const gui = await launchColdView(); +out.guiEditorLoadsAsynchronously = await gui.page.evaluate(async () => { + const ctor = customElements.get('houseplan-card'); + const editor = await ctor.getConfigElement(); + return editor?.localName === 'houseplan-card-editor'; +}); + +// Two network failures are terminal for this page. View stays live and the +// retry is the same immutable chunk with a cache-busting query string. +const failed = await launchColdView(); +let failedRequests = 0; +await failed.page.route(runtimeUrlPattern, async (route) => { + failedRequests += 1; + await route.abort('failed'); +}); +await failed.page.locator('houseplan-card').evaluate((card) => { + const root = card.shadowRoot || card.renderRoot; + root.querySelectorAll('.modetab')[0]?.click(); +}); +await failed.page.waitForFunction(() => window.__card._editorRuntimeLoader.state === 'failed'); +out.networkFailureRetriesExactlyOnce = failedRequests === 2; +out.networkFailureKeepsView = await failed.page.evaluate(() => { + const card = window.__card; + return card._mode === 'view' + && !card._editorRuntime + && card._toast.includes(card._t('editor.load_failed')) + && card._toast.includes(card._t('editor.refresh_advice')); +}); + +// A valid module from a different build is no safer than a 404. Both attempts +// are fulfilled deliberately so this checks the fingerprint handshake rather +// than the network branch above. +const mismatch = await launchColdView(); +let mismatchRequests = 0; +const incompatibleRuntime = readFileSync(runtimeFile, 'utf8') + .replaceAll(manifest.fingerprint, `${manifest.fingerprint}-mismatch`); +await mismatch.page.route(runtimeUrlPattern, async (route) => { + mismatchRequests += 1; + await route.fulfill({ + status: 200, + contentType: 'text/javascript', + body: incompatibleRuntime, + }); +}); +await mismatch.page.locator('houseplan-card').evaluate((card) => { + const root = card.shadowRoot || card.renderRoot; + root.querySelectorAll('.modetab')[1]?.click(); +}); +await mismatch.page.waitForFunction(() => window.__card._editorRuntimeLoader.state === 'failed'); +out.fingerprintMismatchRetriesExactlyOnce = mismatchRequests === 2; +out.fingerprintMismatchKeepsView = await mismatch.page.evaluate(() => + window.__card._mode === 'view' && !window.__card._editorRuntime); + +await failed.browser.close(); +await mismatch.browser.close(); +await onboarding.browser.close(); +await gui.browser.close(); +checkAll(out); +await finish(browser, out); diff --git a/dist/houseplan-assets.json b/dist/houseplan-assets.json new file mode 100644 index 00000000..787d910d --- /dev/null +++ b/dist/houseplan-assets.json @@ -0,0 +1,98 @@ +{ + "schema": 1, + "fingerprint": "f111431804c4c42c6e35286f29720d089bc8b77ffc364a6517f7a3510fcb11c8", + "entry": "houseplan-card.js", + "initialViewFiles": [ + "houseplan-assets/houseplan-card-Cs2QHIcW.js", + "houseplan-card.js" + ], + "initialViewGzipBytes": 255385, + "lazyFiles": [ + "houseplan-assets/editor-BC5jpMNv.js", + "houseplan-assets/houseplan-editor-runtime-DUuA4F8c.js", + "houseplan-assets/houseplan-onboarding-runtime-Bj55plb1.js", + "houseplan-assets/space-deletion-CJ8Co_o8.js" + ], + "lazyGzipBytes": 138414, + "lazyEditorFiles": [ + "houseplan-assets/editor-BC5jpMNv.js", + "houseplan-assets/houseplan-editor-runtime-DUuA4F8c.js", + "houseplan-assets/space-deletion-CJ8Co_o8.js" + ], + "lazyEditorGzipBytes": 131768, + "lazyOnboardingFiles": [ + "houseplan-assets/houseplan-onboarding-runtime-Bj55plb1.js", + "houseplan-assets/space-deletion-CJ8Co_o8.js" + ], + "lazyOnboardingGzipBytes": 7200, + "files": [ + { + "path": "houseplan-assets/editor-BC5jpMNv.js", + "sha256": "f10e45b5a98ab0efc6ceb3c4e08acc4f57a9cb5f99d178b723077a2cb011e05b", + "rawBytes": 3699, + "gzipBytes": 1534, + "isEntry": false, + "imports": [ + "houseplan-assets/houseplan-card-Cs2QHIcW.js" + ], + "dynamicImports": [] + }, + { + "path": "houseplan-assets/houseplan-card-Cs2QHIcW.js", + "sha256": "caa02ba0654e191f72d9b7c9b139c078bb6edb84df4b3e2ec263be492009ecd2", + "rawBytes": 908483, + "gzipBytes": 255201, + "isEntry": false, + "imports": [], + "dynamicImports": [ + "houseplan-assets/editor-BC5jpMNv.js", + "houseplan-assets/houseplan-editor-runtime-DUuA4F8c.js", + "houseplan-assets/houseplan-onboarding-runtime-Bj55plb1.js" + ] + }, + { + "path": "houseplan-assets/houseplan-editor-runtime-DUuA4F8c.js", + "sha256": "0e053b445f87c6c7fdb2b681863e148976ab703221f4988d93f80ca825bd4051", + "rawBytes": 504648, + "gzipBytes": 129680, + "isEntry": false, + "imports": [ + "houseplan-assets/houseplan-card-Cs2QHIcW.js", + "houseplan-assets/space-deletion-CJ8Co_o8.js" + ], + "dynamicImports": [] + }, + { + "path": "houseplan-assets/houseplan-onboarding-runtime-Bj55plb1.js", + "sha256": "32b8812031403d65f909ca236f431a3eeefc9bde9a07499ee123925331f75982", + "rawBytes": 26356, + "gzipBytes": 6646, + "isEntry": false, + "imports": [ + "houseplan-assets/houseplan-card-Cs2QHIcW.js", + "houseplan-assets/space-deletion-CJ8Co_o8.js" + ], + "dynamicImports": [] + }, + { + "path": "houseplan-assets/space-deletion-CJ8Co_o8.js", + "sha256": "59ab0af663f25f859d7bbe3a8893cd00757a40117ca4b9e08d6846f0b2dfd3fc", + "rawBytes": 911, + "gzipBytes": 554, + "isEntry": false, + "imports": [], + "dynamicImports": [] + }, + { + "path": "houseplan-card.js", + "sha256": "53623e0fa5ef93003fc02a66b9932d847c8c68b5345233f9ed6c61f1e470d86a", + "rawBytes": 190, + "gzipBytes": 184, + "isEntry": true, + "imports": [ + "houseplan-assets/houseplan-card-Cs2QHIcW.js" + ], + "dynamicImports": [] + } + ] +} diff --git a/dist/houseplan-assets/editor-BC5jpMNv.js b/dist/houseplan-assets/editor-BC5jpMNv.js new file mode 100644 index 00000000..8d27e2b9 --- /dev/null +++ b/dist/houseplan-assets/editor-BC5jpMNv.js @@ -0,0 +1,14 @@ +globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="f111431804c4c42c6e35286f29720d089bc8b77ffc364a6517f7a3510fcb11c8";import{c as e,l as o,t,d2 as s,A as a,b as l}from"./houseplan-card-Cs2QHIcW.js";class i 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")}],l=this._floorToken;l&&a.push({value:l,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const i="string"==typeof this._config?.floor?this._config.floor:"";i&&!e.some(e=>e.value===i)&&a.push({value:i,label:i}),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;this._loadSpaces();const e=this._lang,o={title:t(e,"editor.title"),floor:t(e,"editor.floor"),default_floor:t(e,"editor.default_floor"),language:t(e,"editor.language"),icon_size:t(e,"editor.icon_size"),show_temperature:t(e,"editor.show_temperature"),live_states:t(e,"editor.live_states"),show_signal:t(e,"editor.show_signal"),kiosk:t(e,"editor.kiosk"),cycle:t(e,"editor.cycle")},s=this._schema,i=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),n=e=>l`o[e.name]||e.name} + @value-changed=${this._valueChanged} + >`;return l` + ${n(s.slice(0,3))} + ${i?l``:a} + ${n(s.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)}}i.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",i); diff --git a/dist/houseplan-assets/houseplan-card-Cs2QHIcW.js b/dist/houseplan-assets/houseplan-card-Cs2QHIcW.js new file mode 100644 index 00000000..54fcf18d --- /dev/null +++ b/dist/houseplan-assets/houseplan-card-Cs2QHIcW.js @@ -0,0 +1,5221 @@ +globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="f111431804c4c42c6e35286f29720d089bc8b77ffc364a6517f7a3510fcb11c8";const e=globalThis,t=e.ShadowRoot&&(void 0===e.ShadyCSS||e.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,i=Symbol(),n=new WeakMap;let r=class{constructor(e,t,n){if(this._$cssResult$=!0,n!==i)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=t}get styleSheet(){let e=this.o;const i=this.t;if(t&&void 0===e){const t=void 0!==i&&1===i.length;t&&(e=n.get(i)),void 0===e&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),t&&n.set(i,e))}return e}toString(){return this.cssText}};const o=(e,...t)=>{const n=1===e.length?e[0]:t.reduce((t,i,n)=>t+(e=>{if(!0===e._$cssResult$)return e.cssText;if("number"==typeof e)return e;throw Error("Value passed to 'css' function must be a 'css' function result: "+e+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+e[n+1],e[0]);return new r(n,e,i)},s=t?e=>e:e=>e instanceof CSSStyleSheet?(e=>{let t="";for(const i of e.cssRules)t+=i.cssText;return(e=>new r("string"==typeof e?e:e+"",void 0,i))(t)})(e):e,{is:a,defineProperty:l,getOwnPropertyDescriptor:c,getOwnPropertyNames:h,getOwnPropertySymbols:d,getPrototypeOf:u}=Object,p=globalThis,_=p.trustedTypes,m=_?_.emptyScript:"",g=p.reactiveElementPolyfillSupport,f=(e,t)=>e,v={toAttribute(e,t){switch(t){case Boolean:e=e?m:null;break;case Object:case Array:e=null==e?e:JSON.stringify(e)}return e},fromAttribute(e,t){let i=e;switch(t){case Boolean:i=null!==e;break;case Number:i=null===e?null:Number(e);break;case Object:case Array:try{i=JSON.parse(e)}catch(e){i=null}}return i}},y=(e,t)=>!a(e,t),b={attribute:!0,type:String,converter:v,reflect:!1,useDefault:!1,hasChanged:y};Symbol.metadata??=Symbol("metadata"),p.litPropertyMetadata??=new WeakMap;let w=class extends HTMLElement{static addInitializer(e){this._$Ei(),(this.l??=[]).push(e)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(e,t=b){if(t.state&&(t.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(e)&&((t=Object.create(t)).wrapped=!0),this.elementProperties.set(e,t),!t.noAccessor){const i=Symbol(),n=this.getPropertyDescriptor(e,i,t);void 0!==n&&l(this.prototype,e,n)}}static getPropertyDescriptor(e,t,i){const{get:n,set:r}=c(this.prototype,e)??{get(){return this[t]},set(e){this[t]=e}};return{get:n,set(t){const o=n?.call(this);r?.call(this,t),this.requestUpdate(e,o,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this.elementProperties.get(e)??b}static _$Ei(){if(this.hasOwnProperty(f("elementProperties")))return;const e=u(this);e.finalize(),void 0!==e.l&&(this.l=[...e.l]),this.elementProperties=new Map(e.elementProperties)}static finalize(){if(this.hasOwnProperty(f("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(f("properties"))){const e=this.properties,t=[...h(e),...d(e)];for(const i of t)this.createProperty(i,e[i])}const e=this[Symbol.metadata];if(null!==e){const t=litPropertyMetadata.get(e);if(void 0!==t)for(const[e,i]of t)this.elementProperties.set(e,i)}this._$Eh=new Map;for(const[e,t]of this.elementProperties){const i=this._$Eu(e,t);void 0!==i&&this._$Eh.set(i,e)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(e){const t=[];if(Array.isArray(e)){const i=new Set(e.flat(1/0).reverse());for(const e of i)t.unshift(s(e))}else void 0!==e&&t.push(s(e));return t}static _$Eu(e,t){const i=t.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof e?e.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(e=>this.enableUpdating=e),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(e=>e(this))}addController(e){(this._$EO??=new Set).add(e),void 0!==this.renderRoot&&this.isConnected&&e.hostConnected?.()}removeController(e){this._$EO?.delete(e)}_$E_(){const e=new Map,t=this.constructor.elementProperties;for(const i of t.keys())this.hasOwnProperty(i)&&(e.set(i,this[i]),delete this[i]);e.size>0&&(this._$Ep=e)}createRenderRoot(){const i=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return((i,n)=>{if(t)i.adoptedStyleSheets=n.map(e=>e instanceof CSSStyleSheet?e:e.styleSheet);else for(const t of n){const n=document.createElement("style"),r=e.litNonce;void 0!==r&&n.setAttribute("nonce",r),n.textContent=t.cssText,i.appendChild(n)}})(i,this.constructor.elementStyles),i}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(e=>e.hostConnected?.())}enableUpdating(e){}disconnectedCallback(){this._$EO?.forEach(e=>e.hostDisconnected?.())}attributeChangedCallback(e,t,i){this._$AK(e,i)}_$ET(e,t){const i=this.constructor.elementProperties.get(e),n=this.constructor._$Eu(e,i);if(void 0!==n&&!0===i.reflect){const r=(void 0!==i.converter?.toAttribute?i.converter:v).toAttribute(t,i.type);this._$Em=e,null==r?this.removeAttribute(n):this.setAttribute(n,r),this._$Em=null}}_$AK(e,t){const i=this.constructor,n=i._$Eh.get(e);if(void 0!==n&&this._$Em!==n){const e=i.getPropertyOptions(n),r="function"==typeof e.converter?{fromAttribute:e.converter}:void 0!==e.converter?.fromAttribute?e.converter:v;this._$Em=n;const o=r.fromAttribute(t,e.type);this[n]=o??this._$Ej?.get(n)??o,this._$Em=null}}requestUpdate(e,t,i,n=!1,r){if(void 0!==e){const o=this.constructor;if(!1===n&&(r=this[e]),i??=o.getPropertyOptions(e),!((i.hasChanged??y)(r,t)||i.useDefault&&i.reflect&&r===this._$Ej?.get(e)&&!this.hasAttribute(o._$Eu(e,i))))return;this.C(e,t,i)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}C(e,t,{useDefault:i,reflect:n,wrapped:r},o){i&&!(this._$Ej??=new Map).has(e)&&(this._$Ej.set(e,o??t??this[e]),!0!==r||void 0!==o)||(this._$AL.has(e)||(this.hasUpdated||i||(t=void 0),this._$AL.set(e,t)),!0===n&&this._$Em!==e&&(this._$Eq??=new Set).add(e))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(e){Promise.reject(e)}const e=this.scheduleUpdate();return null!=e&&await e,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[e,t]of this._$Ep)this[e]=t;this._$Ep=void 0}const e=this.constructor.elementProperties;if(e.size>0)for(const[t,i]of e){const{wrapped:e}=i,n=this[t];!0!==e||this._$AL.has(t)||void 0===n||this.C(t,void 0,i,n)}}let e=!1;const t=this._$AL;try{e=this.shouldUpdate(t),e?(this.willUpdate(t),this._$EO?.forEach(e=>e.hostUpdate?.()),this.update(t)):this._$EM()}catch(t){throw e=!1,this._$EM(),t}e&&this._$AE(t)}willUpdate(e){}_$AE(e){this._$EO?.forEach(e=>e.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(e)),this.updated(e)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(e){return!0}update(e){this._$Eq&&=this._$Eq.forEach(e=>this._$ET(e,this[e])),this._$EM()}updated(e){}firstUpdated(e){}};w.elementStyles=[],w.shadowRootOptions={mode:"open"},w[f("elementProperties")]=new Map,w[f("finalized")]=new Map,g?.({ReactiveElement:w}),(p.reactiveElementVersions??=[]).push("2.1.2");const k=globalThis,x=e=>e,$=k.trustedTypes,S=$?$.createPolicy("lit-html",{createHTML:e=>e}):void 0,M="$lit$",T=`lit$${Math.random().toFixed(9).slice(2)}$`,R="?"+T,C=`<${R}>`,D=document,A=()=>D.createComment(""),O=e=>null===e||"object"!=typeof e&&"function"!=typeof e,z=Array.isArray,P="[ \t\n\f\r]",F=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,I=/-->/g,E=/>/g,N=RegExp(`>|${P}(?:([^\\s"'>=/]+)(${P}*=${P}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),H=/'/g,L=/"/g,j=/^(?:script|style|textarea|title)$/i,q=e=>(t,...i)=>({_$litType$:e,strings:t,values:i}),B=q(1),W=q(2),U=Symbol.for("lit-noChange"),G=Symbol.for("lit-nothing"),V=new WeakMap,K=D.createTreeWalker(D,129);function Y(e,t){if(!z(e)||!e.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==S?S.createHTML(t):t}const Z=(e,t)=>{const i=e.length-1,n=[];let r,o=2===t?"":3===t?"":"",s=F;for(let t=0;t"===l[0]?(s=r??F,c=-1):void 0===l[1]?c=-2:(c=s.lastIndex-l[2].length,a=l[1],s=void 0===l[3]?N:'"'===l[3]?L:H):s===L||s===H?s=N:s===I||s===E?s=F:(s=N,r=void 0);const d=s===N&&e[t+1].startsWith("/>")?" ":"";o+=s===F?i+C:c>=0?(n.push(a),i.slice(0,c)+M+i.slice(c)+T+d):i+T+(-2===c?t:d)}return[Y(e,o+(e[i]||"")+(2===t?"":3===t?"":"")),n]};class X{constructor({strings:e,_$litType$:t},i){let n;this.parts=[];let r=0,o=0;const s=e.length-1,a=this.parts,[l,c]=Z(e,t);if(this.el=X.createElement(l,i),K.currentNode=this.el.content,2===t||3===t){const e=this.el.content.firstChild;e.replaceWith(...e.childNodes)}for(;null!==(n=K.nextNode())&&a.length0){n.textContent=$?$.emptyScript:"";for(let i=0;iz(e)||"function"==typeof e?.[Symbol.iterator])(e)?this.k(e):this._(e)}O(e){return this._$AA.parentNode.insertBefore(e,this._$AB)}T(e){this._$AH!==e&&(this._$AR(),this._$AH=this.O(e))}_(e){this._$AH!==G&&O(this._$AH)?this._$AA.nextSibling.data=e:this.T(D.createTextNode(e)),this._$AH=e}$(e){const{values:t,_$litType$:i}=e,n="number"==typeof i?this._$AC(e):(void 0===i.el&&(i.el=X.createElement(Y(i.h,i.h[0]),this.options)),i);if(this._$AH?._$AD===n)this._$AH.p(t);else{const e=new Q(n,this),i=e.u(this.options);e.p(t),this.T(i),this._$AH=e}}_$AC(e){let t=V.get(e.strings);return void 0===t&&V.set(e.strings,t=new X(e)),t}k(e){z(this._$AH)||(this._$AH=[],this._$AR());const t=this._$AH;let i,n=0;for(const r of e)n===t.length?t.push(i=new ee(this.O(A()),this.O(A()),this,this.options)):i=t[n],i._$AI(r),n++;n2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=G}_$AI(e,t=this,i,n){const r=this.strings;let o=!1;if(void 0===r)e=J(this,e,t,0),o=!O(e)||e!==this._$AH&&e!==U,o&&(this._$AH=e);else{const n=e;let s,a;for(e=r[0],s=0;s{const n=i?.renderBefore??t;let r=n._$litPart$;if(void 0===r){const e=i?.renderBefore??null;n._$litPart$=r=new ee(t.insertBefore(A(),e),e,void 0,i??{})}return r._$AI(e),r},ce=globalThis;let he=class extends w{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){const e=super.createRenderRoot();return this.renderOptions.renderBefore??=e.firstChild,e}update(e){const t=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(e),this._$Do=le(t,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return U}};he._$litElement$=!0,he.finalized=!0,ce.litElementHydrateSupport?.({LitElement:he});const de=ce.litElementPolyfillSupport;de?.({LitElement:he}),(ce.litElementVersions??=[]).push("4.2.2");const ue=2,pe=e=>(...t)=>({_$litDirective$:e,values:t});let _e=class{constructor(e){}get _$AU(){return this._$AM._$AU}_$AT(e,t,i){this._$Ct=e,this._$AM=t,this._$Ci=i}_$AS(e,t){return this.update(e,t)}update(e,t){return this.render(...t)}};const{I:me}=se,ge=e=>e,fe=()=>document.createComment(""),ve=(e,t,i)=>{const n=e._$AA.parentNode,r=void 0===t?e._$AB:t._$AA;if(void 0===i){const t=n.insertBefore(fe(),r),o=n.insertBefore(fe(),r);i=new me(t,o,e,e.options)}else{const t=i._$AB.nextSibling,o=i._$AM,s=o!==e;if(s){let t;i._$AQ?.(e),i._$AM=e,void 0!==i._$AP&&(t=e._$AU)!==o._$AU&&i._$AP(t)}if(t!==r||s){let e=i._$AA;for(;e!==t;){const t=ge(e).nextSibling;ge(n).insertBefore(e,r),e=t}}}return i},ye=(e,t,i=e)=>(e._$AI(t,i),e),be={},we=(e,t=be)=>e._$AH=t,ke=e=>{e._$AR(),e._$AA.remove()},xe=(e,t,i)=>{const n=new Map;for(let r=t;r<=i;r++)n.set(e[r],r);return n},$e=pe(class extends _e{constructor(e){if(super(e),e.type!==ue)throw Error("repeat() can only be used in text expressions")}dt(e,t,i){let n;void 0===i?i=t:void 0!==t&&(n=t);const r=[],o=[];let s=0;for(const t of e)r[s]=n?n(t,s):s,o[s]=i(t,s),s++;return{values:o,keys:r}}render(e,t,i){return this.dt(e,t,i).values}update(e,[t,i,n]){const r=(e=>e._$AH)(e),{values:o,keys:s}=this.dt(t,i,n);if(!Array.isArray(r))return this.ut=s,o;const a=this.ut??=[],l=[];let c,h,d=0,u=r.length-1,p=0,_=o.length-1;for(;d<=u&&p<=_;)if(null===r[d])d++;else if(null===r[u])u--;else if(a[d]===s[p])l[p]=ye(r[d],o[p]),d++,p++;else if(a[u]===s[_])l[_]=ye(r[u],o[_]),u--,_--;else if(a[d]===s[_])l[_]=ye(r[d],o[_]),ve(e,l[_+1],r[d]),d++,_--;else if(a[u]===s[p])l[p]=ye(r[u],o[p]),ve(e,r[d],r[u]),u--,p++;else if(void 0===c&&(c=xe(s,p,_),h=xe(a,d,u)),c.has(a[d]))if(c.has(a[u])){const t=h.get(s[p]),i=void 0!==t?r[t]:null;if(null===i){const t=ve(e,r[d]);ye(t,o[p]),l[p]=t}else l[p]=ye(i,o[p]),ve(e,r[d],i),r[t]=null;p++}else ke(r[u]),u--;else ke(r[d]),d++;for(;p<=_;){const t=ve(e,l[_+1]);ye(t,o[p]),l[p++]=t}for(;d<=u;){const e=r[d++];null!==e&&ke(e)}return this.ut=s,we(e,l),U}}),Se=new WeakMap;let Me=0;class Te extends he{constructor(){super(...arguments),this.title="",this.icon="",this.wide=!1,this.dismissOnScrim=!1,this.hass=null,this._opener=null,this._focusRoot=null,this._useHaDialog=!1,this._closing=!1,this._overlays=[],this._titleId="hp-dialog-title-"+ ++Me,this._focusInitial=()=>{const e=this._focusableElements(),t=e.find(e=>e.hasAttribute("autofocus"))||e[0]||(this._useHaDialog?null:this.renderRoot.querySelector(".close"))||this.renderRoot.querySelector(".surface")||this.renderRoot.querySelector("ha-dialog");t?.focus({preventScroll:!0})},this._requestClose=()=>{this._closing||(this._closing=!0,this.dispatchEvent(new CustomEvent("hp-close",{bubbles:!0,composed:!0})))},this._onKeyDown=e=>{if("Escape"===e.key){e.preventDefault(),e.stopImmediatePropagation(),this._pruneOverlays();const t=this._overlays[this._overlays.length-1];return t?void this._closeOverlay(t,"escape"):void this._requestClose()}if("Tab"!==e.key||this._useHaDialog)return;const t=this.renderRoot.querySelector(".close"),i=t?[t,...this._focusableElements()]:this._focusableElements();if(!i.length)return e.preventDefault(),void this.renderRoot.querySelector(".surface")?.focus({preventScroll:!0});const n=this._deepActiveElement(),r=i[0],o=i[i.length-1];!e.shiftKey||n!==r&&i.includes(n)?e.shiftKey||n!==o||(e.preventDefault(),r.focus()):(e.preventDefault(),o.focus())},this._onFallbackCancel=e=>{e.preventDefault(),this._requestClose()},this._onFallbackClick=e=>{this.dismissOnScrim&&e.target===e.currentTarget&&this._requestClose()}}connectedCallback(){super.connectedCallback(),this._opener=this._deepActiveElement();const e=this.getRootNode();this._focusRoot=e;const t=Se.get(e)||{dialogs:new Set,opener:this._opener};t.dialogs.add(this),Se.set(e,t),this._useHaDialog=!!customElements.get("ha-dialog"),this.addEventListener("keydown",this._onKeyDown,!0)}disconnectedCallback(){this.removeEventListener("keydown",this._onKeyDown,!0);const e=[...this._overlays];this._overlays=[];for(const t of e.reverse())t.close("disconnect");const t=this._focusRoot,i=this._opener;this._opener=null,this._focusRoot=null;const n=t?Se.get(t):void 0;n?.dialogs.delete(this),super.disconnectedCallback(),t&&n&&requestAnimationFrame(()=>{const e=Se.get(t);if(!e)return;if(!e.dialogs.size)return e.opener?.isConnected&&e.opener.focus({preventScroll:!0}),void Se.delete(t);const n=i?.closest("hp-dialog");i?.isConnected&&n&&e.dialogs.has(n)&&i.focus({preventScroll:!0})})}firstUpdated(e){if(super.firstUpdated(e),!this._useHaDialog){const e=this.renderRoot.querySelector("dialog");e&&!e.open&&e.showModal()}queueMicrotask(()=>this._focusInitial())}_deepActiveElement(){let e=document.activeElement;for(;e?.shadowRoot?.activeElement;)e=e.shadowRoot.activeElement;return e&&e!==document.body?e:null}_focusableElements(){const e=["[autofocus]","a[href]","button:not([disabled])",'input:not([disabled]):not([type="hidden"])',"select:not([disabled])","textarea:not([disabled])",'[contenteditable="true"]','[tabindex]:not([tabindex="-1"])'].join(","),t=[],i=new Set,n=r=>{if(!i.has(r)){if(i.add(r),r instanceof HTMLElement){if(r.matches(e)&&t.push(r),r instanceof HTMLSlotElement){for(const e of r.assignedNodes({flatten:!0}))n(e);return}if(r.shadowRoot){for(const e of r.shadowRoot.childNodes)n(e);return}}for(const e of r.childNodes)n(e)}};for(const e of this.childNodes)n(e);const r=this.overlayPortal();return r&&n(r),t.filter(e=>{let t=e;for(;t;){const e=getComputedStyle(t);if(t.hidden||t.inert||"true"===t.getAttribute("aria-hidden")||"none"===e.display||"hidden"===e.visibility)return!1;if(t=t.assignedSlot||t.parentElement||(t.getRootNode()instanceof ShadowRoot?t.getRootNode().host:null),t===this)break}return!0})}_pruneOverlays(){this._overlays=this._overlays.filter(e=>e.owner.isConnected)}_closeOverlay(e,t){const i=this._overlays.findIndex(t=>t.token===e.token);i>=0&&this._overlays.splice(i,1),e.close(t)}registerOverlay(e){this._pruneOverlays();const t=this._overlays.find(t=>t.owner===e.owner);t&&this._overlays.splice(this._overlays.indexOf(t),1);const i=e.group||"transient";for(const e of[...this._overlays].reverse())e.group===i&&this._closeOverlay(e,"exclusive");const n={...e,group:i,token:Symbol("hp-overlay")};this._overlays.push(n);let r=!1;return()=>{if(r)return;r=!0;const e=this._overlays.findIndex(e=>e.token===n.token);e>=0&&this._overlays.splice(e,1)}}closeTransientOverlays(e="outside"){this._pruneOverlays();const t=[...this._overlays].filter(e=>"transient"===(e.group||"transient"));for(const i of t.reverse())this._closeOverlay(i,e);return t.length>0}overlayPortal(){return this.renderRoot.querySelector(".overlay-portal")}render(){const e=B` + ${this.icon?B``:G} + ${this.title} + `;return this._useHaDialog?B` + ${e} + + +
`:B` +
+
+ ${e} + +
+
+ +
+
+
`}}Te.properties={title:{type:String},icon:{type:String},wide:{type:Boolean,reflect:!0},dismissOnScrim:{type:Boolean,attribute:"dismiss-on-scrim"},hass:{attribute:!1}},Te.styles=o` + :host { + display: contents; + color: var(--primary-text-color, #e6e7eb); + font: inherit; + } + + ha-dialog { + --dialog-content-padding: 0; + --dialog-surface-background: var(--card-background-color, var(--hp-bg, #202126)); + --ha-dialog-border-radius: var(--rad-l, 18px); + /* HA's ha-dialog-header defaults this custom property to a one-line + fixed height. Our localized slot is intentionally allowed to wrap, + so leaving that default in place clips every line after the first at + the bottom of the header. auto is HA's public sizing hook and also + stays harmless on older ha-dialog implementations that do not consume + it. */ + --ha-dialog-header-title-height: auto; + color: inherit; + } + + ha-dialog::part(dialog) { + border: 1px solid var(--hp-accent, #d89300); + box-shadow: var(--shadow-3, 0 18px 48px rgb(0 0 0 / 0.34)); + overflow: hidden; + } + + /* The HA header slot is a flex item with a constrained inline size. The + old inline-flex title kept its min-content width, so HA clipped the last + word instead of giving it a second line. Keep every wrapper shrinkable + and let the text wrap; this applies to every hp-dialog, including long + device names and translated titles. */ + .header-title-slot { + display: block; + flex: 1 1 auto; + width: 100%; + max-width: 100%; + min-width: 0; + box-sizing: border-box; + white-space: normal; + } + + .title { + display: flex; + flex: 1 1 auto; + align-items: center; + gap: var(--sp-4, 12px); + width: 100%; + max-width: 100%; + min-width: 0; + font-weight: 600; + line-height: 1.25; + white-space: normal; + } + + .title ha-icon { + flex: none; + color: var(--hp-accent, #d89300); + } + + .title-text { + flex: 1 1 auto; + min-width: 0; + max-width: 100%; + white-space: normal; + overflow-wrap: anywhere; + word-break: normal; + } + + .footer { + /* ha-dialog lays out its footer slot as a flex row. display: contents + exposed the consumer's action row as a shrink-to-fit flex item, so a + wide device dialog got a half-width divider and its Hide action slid + toward the centre. Keep one full-width slot item in both HA and the + native fallback. */ + display: block; + width: 100%; + min-width: 0; + box-sizing: border-box; + } + + ::slotted([slot='footer']) { + width: 100%; + box-sizing: border-box; + } + + dialog { + width: auto; + max-width: none; + max-height: none; + margin: auto; + padding: 0; + border: 0; + overflow: visible; + color: inherit; + background: transparent; + } + + dialog::backdrop { + background: rgb(0 0 0 / 0.45); + } + + .surface { + width: min(360px, 92vw); + max-height: 92vh; + display: flex; + flex-direction: column; + overflow: hidden; + box-sizing: border-box; + color: inherit; + background: var(--card-background-color, var(--hp-bg, #202126)); + border: 1px solid var(--hp-accent, #d89300); + border-radius: var(--rad-l, 18px); + box-shadow: var(--shadow-3, 0 18px 48px rgb(0 0 0 / 0.34)); + } + + :host([wide]) .surface { + width: min(var(--hp-dialog-wide-width, 500px), 94vw); + } + + .header { + min-height: 56px; + display: flex; + align-items: center; + gap: var(--sp-4, 12px); + padding: var(--sp-4, 12px) var(--sp-5, 16px); + box-sizing: border-box; + border-bottom: 1px solid var(--hp-line, rgb(255 255 255 / 0.12)); + } + + .header .title { + flex: 1; + } + + .close { + width: 40px; + height: 40px; + flex: none; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + color: inherit; + background: transparent; + border: 0; + border-radius: 50%; + cursor: pointer; + } + + :host([data-pointer-hover]) .close:hover, + .close:focus-visible { + background: rgb(127 127 127 / 0.16); + } + + .content { + min-height: 0; + display: flex; + flex-direction: column; + } + + .overlay-portal { + position: fixed; + z-index: 2147483647; + inset: 0; + overflow: visible; + pointer-events: none; + } + + .overlay-portal:empty { + display: none; + } + + .overlay-portal > * { + pointer-events: auto; + } + `,customElements.get("hp-dialog")||customElements.define("hp-dialog",Te);const Re=new Set(["hacs","sun","backup","hassio","met","telegram_bot","mobile_app","systemmonitor","better_thermostat","adaptive_lighting","yandex_pogoda","upnp_serial_number"]),Ce=[{pattern:"протечк|leak|water sensor",icon:"mdi:water-alert"},{pattern:"клапан|valve",icon:"mdi:pipe-valve"},{pattern:"дым|smoke",icon:"mdi:smoke-detector"},{pattern:"термоголов|trv|radiator",icon:"mdi:radiator"},{pattern:"чайник|kettle|термопот",icon:"mdi:kettle"},{pattern:"сауна|sauna|harvia|парная|парилк",icon:"mdi:hot-tub"},{pattern:"температ|temperature|thermometer|climate sensor",icon:"mdi:thermometer"},{pattern:"qingping|air monitor|молекул|air quality",icon:"mdi:air-filter"},{pattern:"штор|curtain|blind|shade",icon:"mdi:roller-shade"},{pattern:"розетк|plug|socket|outlet",icon:"mdi:power-socket-de"},{pattern:"выключат|switch",icon:"mdi:light-switch"},{pattern:"лампа|лампочк|bulb|gx53|светильник|rgb|lamp|light strip",icon:"mdi:lightbulb"},{pattern:"камер|camera",icon:"mdi:cctv"},{pattern:"замок|ttlock|lock|sn609|sn9161",icon:"mdi:lock"},{pattern:"ворота|garage|gate",icon:"mdi:garage-variant"},{pattern:"калитк|door|открыт|contact",icon:"mdi:door"},{pattern:"счётчик|счетчик|kws|meter",icon:"mdi:meter-electric"},{pattern:"вводный автомат|breaker|wifimcbn",icon:"mdi:electric-switch"},{pattern:"myheat|котёл|котел|boiler|отоплен|heating",icon:"mdi:water-boiler"},{pattern:"холодильник|fridge",icon:"mdi:fridge"},{pattern:"стиральн|washer|washing",icon:"mdi:washing-machine"},{pattern:"сушилк|dryer",icon:"mdi:tumble-dryer"},{pattern:"пылесос|vacuum|dreame|roborock",icon:"mdi:robot-vacuum"},{pattern:"soundbar",icon:"mdi:soundbar"},{pattern:"колонк|станц|speaker|яндекс|yandex|алиса|alice",icon:"mdi:speaker"},{pattern:"tv|телевизор|hyundaitv|mitv|television",icon:"mdi:television"},{pattern:"keenetic|роутер|router|mesh|access point",icon:"mdi:router-wireless"},{pattern:"ибп|ups|kirpich",icon:"mdi:battery-charging-high"},{pattern:"slzb|координат|zigbee|coordinator",icon:"mdi:zigbee"},{pattern:"motion|движен|presence|присутств",icon:"mdi:motion-sensor"},{pattern:"humidity|влажн",icon:"mdi:water-percent"}];function De(e){const t=[];for(const i of e)if(i&&"string"==typeof i.pattern&&i.icon)try{t.push({re:new RegExp(i.pattern,"i"),icon:i.icon})}catch{}return t}function Ae(e){try{return new RegExp(e,"i"),!0}catch{return!1}}const Oe=De(Ce),ze={temperature:"mdi:thermometer",humidity:"mdi:water-percent",motion:"mdi:motion-sensor",occupancy:"mdi:motion-sensor",presence:"mdi:motion-sensor",door:"mdi:door",window:"mdi:window-closed",garage_door:"mdi:garage-variant",smoke:"mdi:smoke-detector",moisture:"mdi:water-alert",gas:"mdi:gas-cylinder",power:"mdi:meter-electric",energy:"mdi:meter-electric",illuminance:"mdi:brightness-5",co2:"mdi:molecule-co2",pm25:"mdi:air-filter",battery:"mdi:battery"},Pe="mdi:chip";function Fe(e,t,i){const n=((e||"")+" "+(t||"")).toLowerCase();for(const{re:e,icon:t}of i??Oe)if(e.test(n))return t;return Pe}const Ie=["light","switch","cover","valve","lock","climate","fan","media_player","camera","vacuum","humidifier","water_heater","alarm_control_panel","sensor","binary_sensor","event","button","number","select","update"];var Ee=/^-?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i,Ne=Math.ceil,He=Math.floor,Le="[BigNumber Error] ",je=Le+"Number primitive has more than 15 significant digits: ",qe=1e14,Be=14,We=9007199254740991,Ue=[1,10,100,1e3,1e4,1e5,1e6,1e7,1e8,1e9,1e10,1e11,1e12,1e13],Ge=1e7,Ve=1e9;function Ke(e){var t=0|e;return e>0||e===t?t:t-1}function Ye(e){for(var t,i,n=1,r=e.length,o=e[0]+"";nc^i?1:-1;for(a=(l=r.length)<(c=o.length)?l:c,s=0;so[s]^i?1:-1;return l==c?0:l>c^i?1:-1}function Xe(e,t,i,n){if(ei||e!==He(e))throw Error(Le+(n||"Argument")+("number"==typeof e?ei?" out of range: ":" not an integer: ":" not a primitive number: ")+String(e))}function Je(e){var t=e.c.length-1;return Ke(e.e/Be)==t&&e.c[t]%2!=0}function Qe(e,t){return(e.length>1?e.charAt(0)+"."+e.slice(1):e)+(t<0?"e":"e+")+t}function et(e,t,i){var n,r;if(t<0){for(r=i+".";++t;r+=i);e=r+e}else if(++t>(n=e.length)){for(r=i,t-=n;--t;r+=i);e+=r}else ty?u.c=u.e=null:e.e=10;l/=10,a++);return void(a>y?u.c=u.e=null:(u.e=a,u.c=[e]))}d=String(e)}else{if(!Ee.test(d=String(e)))return r(u,d,c);u.s=45==d.charCodeAt(0)?(d=d.slice(1),-1):1}(a=d.indexOf("."))>-1&&(d=d.replace(".","")),(l=d.search(/e/i))>0?(a<0&&(a=l),a+=+d.slice(l+1),d=d.substring(0,l)):a<0&&(a=d.length)}else{if(Xe(t,2,$.length,"Base"),10==t&&S)return D(u=new M(e),_+u.e+1,m);if(d=String(e),c="number"==typeof e){if(0*e!=0)return r(u,d,c,t);if(u.s=1/e<0?(d=d.slice(1),-1):1,M.DEBUG&&d.replace(/^0\.0*|\./,"").length>15)throw Error(je+e)}else u.s=45===d.charCodeAt(0)?(d=d.slice(1),-1):1;for(i=$.slice(0,t),a=l=0,h=d.length;la){a=h;continue}}else if(!s&&(d==d.toUpperCase()&&(d=d.toLowerCase())||d==d.toLowerCase()&&(d=d.toUpperCase()))){s=!0,l=-1,a=0;continue}return r(u,String(e),c,t)}c=!1,(a=(d=n(d,t,10,u.s)).indexOf("."))>-1?d=d.replace(".",""):a=d.length}for(l=0;48===d.charCodeAt(l);l++);for(h=d.length;48===d.charCodeAt(--h););if(d=d.slice(l,++h)){if(h-=l,c&&M.DEBUG&&h>15&&(e>We||e!==He(e)))throw Error(je+u.s*e);if((a=a-l-1)>y)u.c=u.e=null;else if(a=f)?Qe(l,s):et(l,s,"0");else if(o=(e=D(new M(e),t,i)).e,a=(l=Ye(e.c)).length,1==n||2==n&&(t<=o||o<=g)){for(;as),l=et(l,o,"0"),o+1>a){if(--t>0)for(l+=".";t--;l+="0");}else if((t+=o-a)>0)for(o+1==a&&(l+=".");t--;l+="0");return e.s<0&&r?"-"+l:l}function R(e,t){for(var i,n,r=1,o=new M(e[0]);r=10;r/=10,n++);return(i=n+i*Be-1)>y?e.c=e.e=null:i=10;a/=10,r++);if((o=t-r)<0)o+=Be,s=t,l=d[c=0],h=He(l/u[r-s-1]%10);else if((c=Ne((o+1)/Be))>=d.length){if(!n)break e;for(;d.length<=c;d.push(0));l=h=0,r=1,s=(o%=Be)-Be+1}else{for(l=a=d[c],r=1;a>=10;a/=10,r++);h=(s=(o%=Be)-Be+r)<0?0:He(l/u[r-s-1]%10)}if(n=n||t<0||null!=d[c+1]||(s<0?l:l%u[r-s-1]),n=i<4?(h||n)&&(0==i||i==(e.s<0?3:2)):h>5||5==h&&(4==i||n||6==i&&(o>0?s>0?l/u[r-s]:0:d[c-1])%10&1||i==(e.s<0?8:7)),t<1||!d[0])return d.length=0,n?(t-=e.e+1,d[0]=u[(Be-t%Be)%Be],e.e=-t||0):d[0]=e.e=0,e;if(0==o?(d.length=c,a=1,c--):(d.length=c+1,a=u[Be-o],d[c]=s>0?He(l/u[r-s]%u[s])*a:0),n)for(;;){if(0==c){for(o=1,s=d[0];s>=10;s/=10,o++);for(s=d[0]+=a,a=1;s>=10;s/=10,a++);o!=a&&(e.e++,d[0]==qe&&(d[0]=1));break}if(d[c]+=a,d[c]!=qe)break;d[c--]=0,a=1}for(o=d.length;0===d[--o];d.pop());}e.e>y?e.c=e.e=null:e.e=f?Qe(t,i):et(t,i,"0"),e.s<0?"-"+t:t)}return M.clone=e,M.ROUND_UP=0,M.ROUND_DOWN=1,M.ROUND_CEIL=2,M.ROUND_FLOOR=3,M.ROUND_HALF_UP=4,M.ROUND_HALF_DOWN=5,M.ROUND_HALF_EVEN=6,M.ROUND_HALF_CEIL=7,M.ROUND_HALF_FLOOR=8,M.EUCLID=9,M.config=M.set=function(e){var t,i;if(null!=e){if("object"!=typeof e)throw Error(Le+"Object expected: "+e);if(e.hasOwnProperty(t="DECIMAL_PLACES")&&(Xe(i=e[t],0,Ve,t),_=i),e.hasOwnProperty(t="ROUNDING_MODE")&&(Xe(i=e[t],0,8,t),m=i),e.hasOwnProperty(t="EXPONENTIAL_AT")&&((i=e[t])&&i.pop?(Xe(i[0],-Ve,0,t),Xe(i[1],0,Ve,t),g=i[0],f=i[1]):(Xe(i,-Ve,Ve,t),g=-(f=i<0?-i:i))),e.hasOwnProperty(t="RANGE"))if((i=e[t])&&i.pop)Xe(i[0],-Ve,-1,t),Xe(i[1],1,Ve,t),v=i[0],y=i[1];else{if(Xe(i,-Ve,Ve,t),!i)throw Error(Le+t+" cannot be zero: "+i);v=-(y=i<0?-i:i)}if(e.hasOwnProperty(t="CRYPTO")){if((i=e[t])!==!!i)throw Error(Le+t+" not true or false: "+i);if(i){if("undefined"==typeof crypto||!crypto||!crypto.getRandomValues&&!crypto.randomBytes)throw b=!i,Error(Le+"crypto unavailable");b=i}else b=i}if(e.hasOwnProperty(t="MODULO_MODE")&&(Xe(i=e[t],0,9,t),w=i),e.hasOwnProperty(t="POW_PRECISION")&&(Xe(i=e[t],0,Ve,t),k=i),e.hasOwnProperty(t="FORMAT")){if("object"!=typeof(i=e[t]))throw Error(Le+t+" not an object: "+i);x=i}if(e.hasOwnProperty(t="ALPHABET")){if("string"!=typeof(i=e[t])||/^.?$|[+\-.\s]|(.).*\1/.test(i))throw Error(Le+t+" invalid: "+i);S="0123456789"==i.slice(0,10),$=i}}return{DECIMAL_PLACES:_,ROUNDING_MODE:m,EXPONENTIAL_AT:[g,f],RANGE:[v,y],CRYPTO:b,MODULO_MODE:w,POW_PRECISION:k,FORMAT:x,ALPHABET:$}},M.isBigNumber=function(e){if(!e||!0!==e._isBigNumber)return!1;if(!M.DEBUG)return!0;var t,i,n=e.c,r=e.e,o=e.s;e:if("[object Array]"=={}.toString.call(n)){if((1===o||-1===o)&&r>=-Ve&&r<=Ve&&r===He(r)){if(0===n[0]){if(0===r&&1===n.length)return!0;break e}if((t=(r+1)%Be)<1&&(t+=Be),String(n[0]).length==t){for(t=0;t=qe||i!==He(i))break e;if(0!==i)return!0}}}else if(null===n&&null===r&&(null===o||1===o||-1===o))return!0;throw Error(Le+"Invalid BigNumber: "+e)},M.maximum=M.max=function(){return R(arguments,-1)},M.minimum=M.min=function(){return R(arguments,1)},M.random=(o=9007199254740992,s=Math.random()*o&2097151?function(){return He(Math.random()*o)}:function(){return 8388608*(1073741824*Math.random()|0)+(8388608*Math.random()|0)},function(e){var t,i,n,r,o,a=0,l=[],c=new M(p);if(null==e?e=_:Xe(e,0,Ve),r=Ne(e/Be),b)if(crypto.getRandomValues){for(t=crypto.getRandomValues(new Uint32Array(r*=2));a>>11))>=9e15?(i=crypto.getRandomValues(new Uint32Array(2)),t[a]=i[0],t[a+1]=i[1]):(l.push(o%1e14),a+=2);a=r/2}else{if(!crypto.randomBytes)throw b=!1,Error(Le+"crypto unavailable");for(t=crypto.randomBytes(r*=7);a=9e15?crypto.randomBytes(7).copy(t,a):(l.push(o%1e14),a+=7);a=r/7}if(!b)for(;a=10;o/=10,a++);ai-1&&(null==s[r+1]&&(s[r+1]=0),s[r+1]+=s[r]/i|0,s[r]%=i)}return s.reverse()}return function(n,r,o,s,a){var l,c,h,d,u,p,g,f,v=n.indexOf("."),y=_,b=m;for(v>=0&&(d=k,k=0,n=n.replace(".",""),p=(f=new M(r)).pow(n.length-v),k=d,f.c=t(et(Ye(p.c),p.e,"0"),10,o,e),f.e=f.c.length),h=d=(g=t(n,r,o,a?(l=$,e):(l=e,$))).length;0==g[--d];g.pop());if(!g[0])return l.charAt(0);if(v<0?--h:(p.c=g,p.e=h,p.s=s,g=(p=i(p,f,y,b,o)).c,u=p.r,h=p.e),v=g[c=h+y+1],d=o/2,u=u||c<0||null!=g[c+1],u=b<4?(null!=v||u)&&(0==b||b==(p.s<0?3:2)):v>d||v==d&&(4==b||u||6==b&&1&g[c-1]||b==(p.s<0?8:7)),c<1||!g[0])n=u?et(l.charAt(1),-y,l.charAt(0)):l.charAt(0);else{if(g.length=c,u)for(--o;++g[--c]>o;)g[c]=0,c||(++h,g=[1].concat(g));for(d=g.length;!g[--d];);for(v=0,n="";v<=d;n+=l.charAt(g[v++]));n=et(n,h,l.charAt(0))}return n}}(),i=function(){function e(e,t,i){var n,r,o,s,a=0,l=e.length,c=t%Ge,h=t/Ge|0;for(e=e.slice();l--;)a=((r=c*(o=e[l]%Ge)+(n=h*o+(s=e[l]/Ge|0)*c)%Ge*Ge+a)/i|0)+(n/Ge|0)+h*s,e[l]=r%i;return a&&(e=[a].concat(e)),e}function t(e,t,i,n){var r,o;if(i!=n)o=i>n?1:-1;else for(r=o=0;rt[r]?1:-1;break}return o}function i(e,t,i,n){for(var r=0;i--;)e[i]-=r,r=e[i]1;e.splice(0,1));}return function(n,r,o,s,a){var l,c,h,d,u,p,_,m,g,f,v,y,b,w,k,x,$,S=n.s==r.s?1:-1,T=n.c,R=r.c;if(!(T&&T[0]&&R&&R[0]))return new M(n.s&&r.s&&(T?!R||T[0]!=R[0]:R)?T&&0==T[0]||!R?0*S:S/0:NaN);for(g=(m=new M(S)).c=[],S=o+(c=n.e-r.e)+1,a||(a=qe,c=Ke(n.e/Be)-Ke(r.e/Be),S=S/Be|0),h=0;R[h]==(T[h]||0);h++);if(R[h]>(T[h]||0)&&c--,S<0)g.push(1),d=!0;else{for(w=T.length,x=R.length,h=0,S+=2,(u=He(a/(R[0]+1)))>1&&(R=e(R,u,a),T=e(T,u,a),x=R.length,w=T.length),b=x,v=(f=T.slice(0,x)).length;v=a/2&&k++;do{if(u=0,(l=t(R,f,x,v))<0){if(y=f[0],x!=v&&(y=y*a+(f[1]||0)),(u=He(y/k))>1)for(u>=a&&(u=a-1),_=(p=e(R,u,a)).length,v=f.length;1==t(p,f,_,v);)u--,i(p,x<_?$:R,_,a),_=p.length,l=1;else 0==u&&(l=u=1),_=(p=R.slice()).length;if(_=10;S/=10,h++);D(m,o+(m.e=h+c*Be-1)+1,s,d)}else m.e=c,m.r=+d;return m}}(),a=/^(-?)0([xbo])(?=\w[\w.]*$)/i,l=/^([^.]+)\.$/,c=/^\.([^.]+)$/,h=/^-?(Infinity|NaN)$/,d=/^\s*\+(?=[\w.])|^\s+|\s+$/g,r=function(e,t,i,n){var r,o=i?t:t.replace(d,"");if(h.test(o))e.s=isNaN(o)?null:o<0?-1:1;else{if(!i&&(o=o.replace(a,function(e,t,i){return r="x"==(i=i.toLowerCase())?16:"b"==i?2:8,n&&n!=r?e:t}),n&&(r=n,o=o.replace(l,"$1").replace(c,"0.$1")),t!=o))return new M(o,r);if(M.DEBUG)throw Error(Le+"Not a"+(n?" base "+n:"")+" number: "+t);e.s=null}e.c=e.e=null},u.absoluteValue=u.abs=function(){var e=new M(this);return e.s<0&&(e.s=1),e},u.comparedTo=function(e,t){return Ze(this,new M(e,t))},u.decimalPlaces=u.dp=function(e,t){var i,n,r,o=this;if(null!=e)return Xe(e,0,Ve),null==t?t=m:Xe(t,0,8),D(new M(o),e+o.e+1,t);if(!(i=o.c))return null;if(n=((r=i.length-1)-Ke(this.e/Be))*Be,r=i[r])for(;r%10==0;r/=10,n--);return n<0&&(n=0),n},u.dividedBy=u.div=function(e,t){return i(this,new M(e,t),_,m)},u.dividedToIntegerBy=u.idiv=function(e,t){return i(this,new M(e,t),0,1)},u.exponentiatedBy=u.pow=function(e,t){var i,n,r,o,s,a,l,c,h=this;if((e=new M(e)).c&&!e.isInteger())throw Error(Le+"Exponent not an integer: "+A(e));if(null!=t&&(t=new M(t)),s=e.e>14,!h.c||!h.c[0]||1==h.c[0]&&!h.e&&1==h.c.length||!e.c||!e.c[0])return c=new M(Math.pow(+A(h),s?e.s*(2-Je(e)):+A(e))),t?c.mod(t):c;if(a=e.s<0,t){if(t.c?!t.c[0]:!t.s)return new M(NaN);(n=!a&&h.isInteger()&&t.isInteger())&&(h=h.mod(t))}else{if(e.e>9&&(h.e>0||h.e<-1||(0==h.e?h.c[0]>1||s&&h.c[1]>=24e7:h.c[0]<8e13||s&&h.c[0]<=9999975e7)))return o=h.s<0&&Je(e)?-0:0,h.e>-1&&(o=1/o),new M(a?1/o:o);k&&(o=Ne(k/Be+2))}for(s?(i=new M(.5),a&&(e.s=1),l=Je(e)):l=(r=Math.abs(+A(e)))%2,c=new M(p);;){if(l){if(!(c=c.times(h)).c)break;o?c.c.length>o&&(c.c.length=o):n&&(c=c.mod(t))}if(r){if(0===(r=He(r/2)))break;l=r%2}else if(D(e=e.times(i),e.e+1,1),e.e>14)l=Je(e);else{if(0===(r=+A(e)))break;l=r%2}h=h.times(h),o?h.c&&h.c.length>o&&(h.c.length=o):n&&(h=h.mod(t))}return n?c:(a&&(c=p.div(c)),t?c.mod(t):o?D(c,k,m,void 0):c)},u.integerValue=function(e){var t=new M(this);return null==e?e=m:Xe(e,0,8),D(t,t.e+1,e)},u.isEqualTo=u.eq=function(e,t){return 0===Ze(this,new M(e,t))},u.isFinite=function(){return!!this.c},u.isGreaterThan=u.gt=function(e,t){return Ze(this,new M(e,t))>0},u.isGreaterThanOrEqualTo=u.gte=function(e,t){return 1===(t=Ze(this,new M(e,t)))||0===t},u.isInteger=function(){return!!this.c&&Ke(this.e/Be)>this.c.length-2},u.isLessThan=u.lt=function(e,t){return Ze(this,new M(e,t))<0},u.isLessThanOrEqualTo=u.lte=function(e,t){return-1===(t=Ze(this,new M(e,t)))||0===t},u.isNaN=function(){return!this.s},u.isNegative=function(){return this.s<0},u.isPositive=function(){return this.s>0},u.isZero=function(){return!!this.c&&0==this.c[0]},u.minus=function(e,t){var i,n,r,o,s=this,a=s.s;if(t=(e=new M(e,t)).s,!a||!t)return new M(NaN);if(a!=t)return e.s=-t,s.plus(e);var l=s.e/Be,c=e.e/Be,h=s.c,d=e.c;if(!l||!c){if(!h||!d)return h?(e.s=-t,e):new M(d?s:NaN);if(!h[0]||!d[0])return d[0]?(e.s=-t,e):new M(h[0]?s:3==m?-0:0)}if(l=Ke(l),c=Ke(c),h=h.slice(),a=l-c){for((o=a<0)?(a=-a,r=h):(c=l,r=d),r.reverse(),t=a;t--;r.push(0));r.reverse()}else for(n=(o=(a=h.length)<(t=d.length))?a:t,a=t=0;t0)for(;t--;h[i++]=0);for(t=qe-1;n>a;){if(h[--n]=0;){for(i=0,u=y[r]%g,p=y[r]/g|0,o=r+(s=l);o>r;)i=((c=u*(c=v[--s]%g)+(a=p*c+(h=v[s]/g|0)*u)%g*g+_[o]+i)/m|0)+(a/g|0)+p*h,_[o--]=c%m;_[o]=i}return i?++n:_.splice(0,1),C(e,_,n)},u.negated=function(){var e=new M(this);return e.s=-e.s||null,e},u.plus=function(e,t){var i,n=this,r=n.s;if(t=(e=new M(e,t)).s,!r||!t)return new M(NaN);if(r!=t)return e.s=-t,n.minus(e);var o=n.e/Be,s=e.e/Be,a=n.c,l=e.c;if(!o||!s){if(!a||!l)return new M(r/0);if(!a[0]||!l[0])return l[0]?e:new M(a[0]?n:0*r)}if(o=Ke(o),s=Ke(s),a=a.slice(),r=o-s){for(r>0?(s=o,i=l):(r=-r,i=a),i.reverse();r--;i.push(0));i.reverse()}for((r=a.length)-(t=l.length)<0&&(i=l,l=a,a=i,t=r),r=0;t;)r=(a[--t]=a[t]+l[t]+r)/qe|0,a[t]=qe===a[t]?0:a[t]%qe;return r&&(a=[r].concat(a),++s),C(e,a,s)},u.precision=u.sd=function(e,t){var i,n,r,o=this;if(null!=e&&e!==!!e)return Xe(e,1,Ve),null==t?t=m:Xe(t,0,8),D(new M(o),e,t);if(!(i=o.c))return null;if(n=(r=i.length-1)*Be+1,r=i[r]){for(;r%10==0;r/=10,n--);for(r=i[0];r>=10;r/=10,n++);}return e&&o.e+1>n&&(n=o.e+1),n},u.shiftedBy=function(e){return Xe(e,-9007199254740991,We),this.times("1e"+e)},u.squareRoot=u.sqrt=function(){var e,t,n,r,o,s=this,a=s.c,l=s.s,c=s.e,h=_+4,d=new M("0.5");if(1!==l||!a||!a[0])return new M(!l||l<0&&(!a||a[0])?NaN:a?s:1/0);if(0==(l=Math.sqrt(+A(s)))||l==1/0?(((t=Ye(a)).length+c)%2==0&&(t+="0"),l=Math.sqrt(+t),c=Ke((c+1)/2)-(c<0||c%2),n=new M(t=l==1/0?"5e"+c:(t=l.toExponential()).slice(0,t.indexOf("e")+1)+c)):n=new M(l+""),n.c[0])for((l=(c=n.e)+h)<3&&(l=0);;)if(o=n,n=d.times(o.plus(i(s,o,h,1))),Ye(o.c).slice(0,l)===(t=Ye(n.c)).slice(0,l)){if(n.e0&&_>0){for(o=_%a||a,h=p.substr(0,o);o<_;o+=a)h+=c+p.substr(o,a);l>0&&(h+=c+p.slice(o)),u&&(h="-"+h)}n=d?h+(i.decimalSeparator||"")+((l=+i.fractionGroupSize)?d.replace(new RegExp("\\d{"+l+"}\\B","g"),"$&"+(i.fractionGroupSeparator||"")):d):h}return(i.prefix||"")+n+(i.suffix||"")},u.toFraction=function(e){var t,n,r,o,s,a,l,c,h,d,u,_,g=this,f=g.c;if(null!=e&&(!(l=new M(e)).isInteger()&&(l.c||1!==l.s)||l.lt(p)))throw Error(Le+"Argument "+(l.isInteger()?"out of range: ":"not an integer: ")+A(l));if(!f)return new M(g);for(t=new M(p),h=n=new M(p),r=c=new M(p),_=Ye(f),s=t.e=_.length-g.e-1,t.c[0]=Ue[(a=s%Be)<0?Be+a:a],e=!e||l.comparedTo(t)>0?s>0?t:h:l,a=y,y=1/0,l=new M(_),c.c[0]=0;d=i(l,t,0,1),1!=(o=n.plus(d.times(r))).comparedTo(e);)n=r,r=o,h=c.plus(d.times(o=h)),c=o,t=l.minus(d.times(o=t)),l=o;return o=i(e.minus(n),r,0,1),c=c.plus(o.times(h)),n=n.plus(o.times(r)),c.s=h.s=g.s,u=i(h,r,s*=2,m).minus(g).abs().comparedTo(i(c,n,s,m).minus(g).abs())<1?[h,r]:[c,n],y=a,u},u.toNumber=function(){return+A(this)},u.toPrecision=function(e,t){return null!=e&&Xe(e,1,Ve),T(this,e,t,2)},u.toString=function(e){var t,i=this,r=i.s,o=i.e;return null===o?r?(t="Infinity",r<0&&(t="-"+t)):t="NaN":(null==e?t=o<=g||o>=f?Qe(Ye(i.c),o):et(Ye(i.c),o,"0"):10===e&&S?t=et(Ye((i=D(new M(i),_+o+1,m)).c),i.e,"0"):(Xe(e,2,$.length,"Base"),t=n(et(Ye(i.c),o,"0"),10,e,r,!0)),r<0&&i.c[0]&&(t="-"+t)),t},u.valueOf=u.toJSON=function(){return A(this)},u._isBigNumber=!0,u[Symbol.toStringTag]="BigNumber",u[Symbol.for("nodejs.util.inspect.custom")]=u.valueOf,null!=t&&M.set(t),M}(),it=class{key;left=null;right=null;constructor(e){this.key=e}},nt=class extends it{constructor(e){super(e)}},rt=class{size=0;modificationCount=0;splayCount=0;splay(e){const t=this.root;if(null==t)return this.compare(e,e),-1;let i=null,n=null,r=null,o=null,s=t;const a=this.compare;let l;for(;;)if(l=a(s.key,e),l>0){let t=s.left;if(null==t)break;if(l=a(t.key,e),l>0&&(s.left=t.right,t.right=s,s=t,t=s.left,null==t))break;null==i?n=s:i.left=s,i=s,s=t}else{if(!(l<0))break;{let t=s.right;if(null==t)break;if(l=a(t.key,e),l<0&&(s.right=t.left,t.left=s,s=t,t=s.right,null==t))break;null==r?o=s:r.right=s,r=s,s=t}}return null!=r&&(r.right=s.left,s.left=o),null!=i&&(i.left=s.right,s.right=n),this.root!==s&&(this.root=s,this.splayCount++),l}splayMin(e){let t=e,i=t.left;for(;null!=i;){const e=i;t.left=e.right,e.right=t,t=e,i=t.left}return t}splayMax(e){let t=e,i=t.right;for(;null!=i;){const e=i;t.right=e.left,e.left=t,t=e,i=t.right}return t}_delete(e){if(null==this.root)return null;if(0!=this.splay(e))return null;let t=this.root;const i=t,n=t.left;if(this.size--,null==n)this.root=t.right;else{const e=t.right;t=this.splayMax(n),t.right=e,this.root=t}return this.modificationCount++,i}addNewRoot(e,t){this.size++,this.modificationCount++;const i=this.root;null!=i?(t<0?(e.left=i,e.right=i.right,i.right=null):(e.right=i,e.left=i.left,i.left=null),this.root=e):this.root=e}_first(){const e=this.root;return null==e?null:(this.root=this.splayMin(e),this.root)}_last(){const e=this.root;return null==e?null:(this.root=this.splayMax(e),this.root)}clear(){this.root=null,this.size=0,this.modificationCount++}has(e){return this.validKey(e)&&0==this.splay(e)}defaultCompare(){return(e,t)=>et?1:0}wrap(){return{getRoot:()=>this.root,setRoot:e=>{this.root=e},getSize:()=>this.size,getModificationCount:()=>this.modificationCount,getSplayCount:()=>this.splayCount,setSplayCount:e=>{this.splayCount=e},splay:e=>this.splay(e),has:e=>this.has(e)}}},ot=class e extends rt{root=null;compare;validKey;constructor(e,t){super(),this.compare=e??this.defaultCompare(),this.validKey=t??(e=>null!=e&&null!=e)}delete(e){return!!this.validKey(e)&&null!=this._delete(e)}deleteAll(e){for(const t of e)this.delete(t)}forEach(e){const t=this[Symbol.iterator]();let i;for(;i=t.next(),!i.done;)e(i.value,i.value,this)}add(e){const t=this.splay(e);return 0!=t&&this.addNewRoot(new nt(e),t),this}addAndReturn(e){const t=this.splay(e);return 0!=t&&this.addNewRoot(new nt(e),t),this.root.key}addAll(e){for(const t of e)this.add(t)}isEmpty(){return null==this.root}isNotEmpty(){return null!=this.root}single(){if(0==this.size)throw"Bad state: No element";if(this.size>1)throw"Bad state: Too many element";return this.root.key}first(){if(0==this.size)throw"Bad state: No element";return this._first().key}last(){if(0==this.size)throw"Bad state: No element";return this._last().key}lastBefore(e){if(null==e)throw"Invalid arguments(s)";if(null==this.root)return null;if(this.splay(e)<0)return this.root.key;let t=this.root.left;if(null==t)return null;let i=t.right;for(;null!=i;)t=i,i=t.right;return t.key}firstAfter(e){if(null==e)throw"Invalid arguments(s)";if(null==this.root)return null;if(this.splay(e)>0)return this.root.key;let t=this.root.right;if(null==t)return null;let i=t.left;for(;null!=i;)t=i,i=t.left;return t.key}retainAll(t){const i=new e(this.compare,this.validKey),n=this.modificationCount;for(const e of t){if(n!=this.modificationCount)throw"Concurrent modification during iteration.";this.validKey(e)&&0==this.splay(e)&&i.add(this.root.key)}i.size!=this.size&&(this.root=i.root,this.size=i.size,this.modificationCount++)}lookup(e){if(!this.validKey(e))return null;return 0!=this.splay(e)?null:this.root.key}intersection(t){const i=new e(this.compare,this.validKey);for(const e of this)t.has(e)&&i.add(e);return i}difference(t){const i=new e(this.compare,this.validKey);for(const e of this)t.has(e)||i.add(e);return i}union(e){const t=this.clone();return t.addAll(e),t}clone(){const t=new e(this.compare,this.validKey);return t.size=this.size,t.root=this.copyNode(this.root),t}copyNode(e){if(null==e)return null;const t=new nt(e.key);return function e(t,i){let n,r;do{if(n=t.left,r=t.right,null!=n){const t=new nt(n.key);i.left=t,e(n,t)}if(null!=r){const e=new nt(r.key);i.right=e,t=r,i=e}}while(null!=r)}(e,t),t}toSet(){return this.clone()}entries(){return new lt(this.wrap())}keys(){return this[Symbol.iterator]()}values(){return this[Symbol.iterator]()}[Symbol.iterator](){return new at(this.wrap())}[Symbol.toStringTag]="[object Set]"},st=class{tree;path=new Array;modificationCount=null;splayCount;constructor(e){this.tree=e,this.splayCount=e.getSplayCount()}[Symbol.iterator](){return this}next(){return this.moveNext()?{done:!1,value:this.current()}:{done:!0,value:null}}current(){if(!this.path.length)return null;const e=this.path[this.path.length-1];return this.getValue(e)}rebuildPath(e){this.path.splice(0,this.path.length),this.tree.splay(e),this.path.push(this.tree.getRoot()),this.splayCount=this.tree.getSplayCount()}findLeftMostDescendent(e){for(;null!=e;)this.path.push(e),e=e.left}moveNext(){if(this.modificationCount!=this.tree.getModificationCount()){if(null==this.modificationCount){this.modificationCount=this.tree.getModificationCount();let e=this.tree.getRoot();for(;null!=e;)this.path.push(e),e=e.left;return this.path.length>0}throw"Concurrent modification during iteration."}if(!this.path.length)return!1;this.splayCount!=this.tree.getSplayCount()&&this.rebuildPath(this.path[this.path.length-1].key);let e=this.path[this.path.length-1],t=e.right;if(null!=t){for(;null!=t;)this.path.push(t),t=t.left;return!0}for(this.path.pop();this.path.length&&this.path[this.path.length-1].right===e;)e=this.path.pop();return this.path.length>0}},at=class extends st{getValue(e){return e.key}},lt=class extends st{getValue(e){return[e.key,e.key]}},ct=e=>()=>e,ht=e=>{const t=e?(t,i)=>i.minus(t).abs().isLessThanOrEqualTo(e):ct(!1);return(e,i)=>t(e,i)?0:e.comparedTo(i)};function dt(e){const t=e?(t,i,n,r,o)=>t.exponentiatedBy(2).isLessThanOrEqualTo(r.minus(i).exponentiatedBy(2).plus(o.minus(n).exponentiatedBy(2)).times(e)):ct(!1);return(e,i,n)=>{const r=e.x,o=e.y,s=n.x,a=n.y,l=o.minus(a).times(i.x.minus(s)).minus(r.minus(s).times(i.y.minus(a)));return t(l,r,o,s,a)?0:l.comparedTo(0)}}var ut=e=>e,pt=e=>{if(e){const t=new ot(ht(e)),i=new ot(ht(e)),n=(e,t)=>t.addAndReturn(e),r=e=>({x:n(e.x,t),y:n(e.y,i)});return r({x:new tt(0),y:new tt(0)}),r}return ut},_t=e=>({set:e=>{mt=_t(e)},reset:()=>_t(e),compare:ht(e),snap:pt(e),orient:dt(e)}),mt=_t(),gt=(e,t)=>e.ll.x.isLessThanOrEqualTo(t.x)&&t.x.isLessThanOrEqualTo(e.ur.x)&&e.ll.y.isLessThanOrEqualTo(t.y)&&t.y.isLessThanOrEqualTo(e.ur.y),ft=(e,t)=>{if(t.ur.x.isLessThan(e.ll.x)||e.ur.x.isLessThan(t.ll.x)||t.ur.y.isLessThan(e.ll.y)||e.ur.y.isLessThan(t.ll.y))return null;const i=e.ll.x.isLessThan(t.ll.x)?t.ll.x:e.ll.x,n=e.ur.x.isLessThan(t.ur.x)?e.ur.x:t.ur.x;return{ll:{x:i,y:e.ll.y.isLessThan(t.ll.y)?t.ll.y:e.ll.y},ur:{x:n,y:e.ur.y.isLessThan(t.ur.y)?e.ur.y:t.ur.y}}},vt=(e,t)=>e.x.times(t.y).minus(e.y.times(t.x)),yt=(e,t)=>e.x.times(t.x).plus(e.y.times(t.y)),bt=e=>yt(e,e).sqrt(),wt=(e,t,i)=>{const n={x:t.x.minus(e.x),y:t.y.minus(e.y)},r={x:i.x.minus(e.x),y:i.y.minus(e.y)};return vt(r,n).div(bt(r)).div(bt(n))},kt=(e,t,i)=>{const n={x:t.x.minus(e.x),y:t.y.minus(e.y)},r={x:i.x.minus(e.x),y:i.y.minus(e.y)};return yt(r,n).div(bt(r)).div(bt(n))},xt=(e,t,i)=>t.y.isZero()?null:{x:e.x.plus(t.x.div(t.y).times(i.minus(e.y))),y:i},$t=(e,t,i)=>t.x.isZero()?null:{x:i,y:e.y.plus(t.y.div(t.x).times(i.minus(e.x)))},St=class e{point;isLeft;segment;otherSE;consumedBy;static compare(t,i){const n=e.comparePoints(t.point,i.point);return 0!==n?n:(t.point!==i.point&&t.link(i),t.isLeft!==i.isLeft?t.isLeft?1:-1:zt.compare(t.segment,i.segment))}static comparePoints(e,t){return e.x.isLessThan(t.x)?-1:e.x.isGreaterThan(t.x)?1:e.y.isLessThan(t.y)?-1:e.y.isGreaterThan(t.y)?1:0}constructor(e,t){void 0===e.events?e.events=[this]:e.events.push(this),this.point=e,this.isLeft=t}link(e){if(e.point===this.point)throw new Error("Tried to link already linked events");const t=e.point.events;for(let e=0,i=t.length;e{const n=i.otherSE;t.set(i,{sine:wt(this.point,e.point,n.point),cosine:kt(this.point,e.point,n.point)})};return(e,n)=>{t.has(e)||i(e),t.has(n)||i(n);const{sine:r,cosine:o}=t.get(e),{sine:s,cosine:a}=t.get(n);return r.isGreaterThanOrEqualTo(0)&&s.isGreaterThanOrEqualTo(0)?o.isLessThan(a)?1:o.isGreaterThan(a)?-1:0:r.isLessThan(0)&&s.isLessThan(0)?o.isLessThan(a)?-1:o.isGreaterThan(a)?1:0:s.isLessThan(r)?-1:s.isGreaterThan(r)?1:0}}},Mt=class e{events;poly;_isExteriorRing;_enclosingRing;static factory(t){const i=[];for(let n=0,r=t.length;n0&&(e=i)}let t=e.segment.prevInResult(),i=t?t.prevInResult():null;for(;;){if(!t)return null;if(!i)return t.ringOut;if(i.ringOut!==t.ringOut)return i.ringOut?.enclosingRing()!==t.ringOut?t.ringOut:t.ringOut?.enclosingRing();t=i.prevInResult(),i=t?t.prevInResult():null}}},Tt=class{exteriorRing;interiorRings;constructor(e){this.exteriorRing=e,e.poly=this,this.interiorRings=[]}addInterior(e){this.interiorRings.push(e),e.poly=this}getGeom(){const e=this.exteriorRing.getGeom();if(null===e)return null;const t=[e];for(let e=0,i=this.interiorRings.length;e0?(this.tree.delete(t),i.push(e)):(this.segments.push(t),t.prev=n)}else{if(n&&r){const e=n.getIntersection(r);if(null!==e){if(!n.isAnEndpoint(e)){const t=this._splitSafely(n,e);for(let e=0,n=t.length;e0)return-1;const n=t.comparePoint(e.rightSE.point);return 0!==n?n:-1}if(i.isGreaterThan(n)){if(s.isLessThan(a)&&s.isLessThan(c))return-1;if(s.isGreaterThan(a)&&s.isGreaterThan(c))return 1;const i=t.comparePoint(e.leftSE.point);if(0!==i)return i;const n=e.comparePoint(t.rightSE.point);return n<0?1:n>0?-1:1}if(s.isLessThan(a))return-1;if(s.isGreaterThan(a))return 1;if(r.isLessThan(o)){const i=t.comparePoint(e.rightSE.point);if(0!==i)return i}if(r.isGreaterThan(o)){const i=e.comparePoint(t.rightSE.point);if(i<0)return 1;if(i>0)return-1}if(!r.eq(o)){const e=l.minus(s),t=r.minus(i),h=c.minus(a),d=o.minus(n);if(e.isGreaterThan(t)&&h.isLessThan(d))return 1;if(e.isLessThan(t)&&h.isGreaterThan(d))return-1}return r.isGreaterThan(o)?1:r.isLessThan(o)||l.isLessThan(c)?-1:l.isGreaterThan(c)?1:e.idt.id?1:0}constructor(e,t,i,n){this.id=++Ot,this.leftSE=e,e.segment=this,e.otherSE=t,this.rightSE=t,t.segment=this,t.otherSE=e,this.rings=i,this.windings=n}static fromRing(t,i,n){let r,o,s;const a=St.comparePoints(t,i);if(a<0)r=t,o=i,s=1;else{if(!(a>0))throw new Error(`Tried to create degenerate segment at [${t.x}, ${t.y}]`);r=i,o=t,s=-1}const l=new St(r,!0),c=new St(o,!1);return new e(l,c,[n],[s])}replaceRightSE(e){this.rightSE=e,this.rightSE.segment=this,this.rightSE.otherSE=this.leftSE,this.leftSE.otherSE=this.rightSE}bbox(){const e=this.leftSE.point.y,t=this.rightSE.point.y;return{ll:{x:this.leftSE.point.x,y:e.isLessThan(t)?e:t},ur:{x:this.rightSE.point.x,y:e.isGreaterThan(t)?e:t}}}vector(){return{x:this.rightSE.point.x.minus(this.leftSE.point.x),y:this.rightSE.point.y.minus(this.leftSE.point.y)}}isAnEndpoint(e){return e.x.eq(this.leftSE.point.x)&&e.y.eq(this.leftSE.point.y)||e.x.eq(this.rightSE.point.x)&&e.y.eq(this.rightSE.point.y)}comparePoint(e){return mt.orient(this.leftSE.point,e,this.rightSE.point)}getIntersection(e){const t=this.bbox(),i=e.bbox(),n=ft(t,i);if(null===n)return null;const r=this.leftSE.point,o=this.rightSE.point,s=e.leftSE.point,a=e.rightSE.point,l=gt(t,s)&&0===this.comparePoint(s),c=gt(i,r)&&0===e.comparePoint(r),h=gt(t,a)&&0===this.comparePoint(a),d=gt(i,o)&&0===e.comparePoint(o);if(c&&l)return d&&!h?o:!d&&h?a:null;if(c)return h&&r.x.eq(a.x)&&r.y.eq(a.y)?null:r;if(l)return d&&o.x.eq(s.x)&&o.y.eq(s.y)?null:s;if(d&&h)return null;if(d)return o;if(h)return a;const u=((e,t,i,n)=>{if(t.x.isZero())return $t(i,n,e.x);if(n.x.isZero())return $t(e,t,i.x);if(t.y.isZero())return xt(i,n,e.y);if(n.y.isZero())return xt(e,t,i.y);const r=vt(t,n);if(r.isZero())return null;const o={x:i.x.minus(e.x),y:i.y.minus(e.y)},s=vt(o,t).div(r),a=vt(o,n).div(r),l=e.x.plus(a.times(t.x)),c=i.x.plus(s.times(n.x)),h=e.y.plus(a.times(t.y)),d=i.y.plus(s.times(n.y));return{x:l.plus(c).div(2),y:h.plus(d).div(2)}})(r,this.vector(),s,e.vector());return null===u?null:gt(n,u)?mt.snap(u):null}split(t){const i=[],n=void 0!==t.events,r=new St(t,!0),o=new St(t,!1),s=this.rightSE;this.replaceRightSE(o),i.push(o),i.push(r);const a=new e(r,s,this.rings.slice(),this.windings.slice());return St.comparePoints(a.leftSE.point,a.rightSE.point)>0&&a.swapEvents(),St.comparePoints(this.leftSE.point,this.rightSE.point)>0&&this.swapEvents(),n&&(r.checkForConsuming(),o.checkForConsuming()),i}swapEvents(){const e=this.rightSE;this.rightSE=this.leftSE,this.leftSE=e,this.leftSE.isLeft=!0,this.rightSE.isLeft=!1;for(let e=0,t=this.windings.length;e0){const e=i;i=n,n=e}if(i.prev===n){const e=i;i=n,n=e}for(let e=0,t=n.rings.length;e1===e.length&&e[0].isSubject;this._isInResult=i(e)!==i(t);break}}return this._isInResult}},Pt=class{poly;isExterior;segments;bbox;constructor(e,t,i){if(!Array.isArray(e)||0===e.length)throw new Error("Input geometry is not a valid Polygon or MultiPolygon");if(this.poly=t,this.isExterior=i,this.segments=[],"number"!=typeof e[0][0]||"number"!=typeof e[0][1])throw new Error("Input geometry is not a valid Polygon or MultiPolygon");const n=mt.snap({x:new tt(e[0][0]),y:new tt(e[0][1])});this.bbox={ll:{x:n.x,y:n.y},ur:{x:n.x,y:n.y}};let r=n;for(let t=1,i=e.length;tAt.run("union",e,t),Nt=(e,...t)=>At.run("intersection",e,t),Ht=(e,...t)=>At.run("difference",e,t);mt.set;const Lt=/^#[0-9a-fA-F]{6}$/;function jt(e,t){return"string"==typeof e&&Lt.test(e)?e:t}const qt="(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])",Bt=new RegExp(`^rgb\\(${qt}, ${qt}, ${qt}\\)$`);function Wt(e){if(!Array.isArray(e)||e.length<3)return null;const t=e.slice(0,3);if(!t.every(e=>"number"==typeof e&&Number.isFinite(e)))return null;const[i,n,r]=t.map(e=>Math.round(Math.min(255,Math.max(0,e))));return`rgb(${i}, ${n}, ${r})`}function Ut(e){const t=Math.max(0,Math.min(120,(e-40)/140*120));return`hsl(${Math.round(t)}, 85%, 55%)`}function Gt(e,t){if(!(Number.isFinite(e)&&t>0))return e;const i=Math.round(e/t)*t;return Math.abs(i-e)<=1e-9*t?e:i}function Vt(e,t,i,n=1/0){if(!(i>0&&e?.every(Number.isFinite)&&t?.every(Number.isFinite)))return[t[0],t[1]];const r=t[0]-e[0],o=t[1]-e[1];if(Math.abs(r)+Math.abs(o)<=1e-12)return[e[0],e[1]];const s=Math.PI/4;let a=Math.atan2(o,r);a<0&&(a+=2*Math.PI);const l=Math.floor(a/s+.5)%8,[c,h]=[[1,0],[1,1],[0,1],[-1,1],[-1,0],[-1,-1],[0,-1],[1,-1]][l],d=c*c+h*h;let u=Math.max(0,Math.round((r*c+o*h)/(i*d)));if(Number.isFinite(n)&&n>=0){const t=(e,t)=>{if(!t)return 1/0;const r=t>0?n-e:e+n;return Math.max(0,Math.floor((r+1e-9*i)/i))};u=Math.min(u,t(e[0],c),t(e[1],h))}return[e[0]+c*u*i,e[1]+h*u*i]}function Kt(e,t){if(t){const t=e/2.54;let i=Math.floor(t/12),n=Math.round(t-12*i);return 12===n&&(i+=1,n=0),`${i}′ ${n}″`}return`${(e/100).toFixed(2)} m`}function Yt(e,t,i=1){const n=e[0].toFixed(i),r=e[1].toFixed(i),o=t[0].toFixed(i),s=t[1].toFixed(i),a=n=3?e.poly:e&&null!=e.x&&null!=e.y&&null!=e.w&&null!=e.h?[[e.x,e.y],[e.x+e.w,e.y],[e.x+e.w,e.y+e.h],[e.x,e.y+e.h]]:null}function Xt(e){const t=[];for(const i of e||[])i?.poly?.length>=3?t.push({poly:i.poly.map(e=>e.join(",")).join(" ")}):i&&null!=i.x&&null!=i.y&&null!=i.w&&null!=i.h&&t.push({rect:{x:i.x,y:i.y,w:i.w,h:i.h,rx:.03*Math.min(i.w,i.h)}});return t}function Jt(e){const t=[],i=new Set;for(const n of e||[]){const e=Zt(n);if(e)for(let n=0;n=90?e-=180:e<-90&&(e+=180),n.step&&n.step>0){const i=Math.sqrt(d),o=Math.min(Math.max(n.length||0,0)/2,i/2);let a=Math.round(u*i/n.step)*n.step;Math.abs(u*i-i/2)<=n.step/2&&(a=i/2),a=Math.max(o,Math.min(i-o,a));const l=a/i;r={x:t+l*c,y:s+l*h,angle:e}}else r={x:p[0],y:p[1],angle:e}}}return r}function ei(e,t,i){let n=null,r=1/0;for(let o=0;o=r)continue;r=_;const m=Math.sqrt(u),g=(i>0?Math.max(0,Math.min(m,Math.round(p*m/i)*i)):p*m)/m;n=[s+g*h,a+g*d]}return n}function ti(e,t,i,n,r,o=1){const s=t*Math.PI/180,a=[Math.cos(s),Math.sin(s)];let l=null,c=o;for(const t of Jt(n)){const i=[[t[0],t[1]],[t[2],t[3]]],n=t=>Math.abs(a[0]*(t[1]-e[1])-a[1]*(t[0]-e[0]));if(n(i[0])>o||n(i[1])>o)continue;const r=(i[0][0]-e[0])*a[0]+(i[0][1]-e[1])*a[1],s=(i[1][0]-e[0])*a[0]+(i[1][1]-e[1])*a[1],h=Math.min(r,s),d=Math.max(r,s),u=h>0?h:d<0?-d:0;u[e[0]+a[0]*t,e[1]+a[1]*t],g=(h+d)/2;return{wallA:m(h),wallB:m(d),sideA:p,sideB:_,midA:m((h-u)/2),midB:m((u+d)/2),wallCenter:m(g),centered:Math.abs(g)<=r}}function ii(e,t,i=!1){if("passage"===e)return 1;if(null==t||"unavailable"===t||"unknown"===t)return"window"===e?0:1;const n=function(e){return["on","open","home","detected","playing","cleaning"].includes(String(e))}(t)!==!!i;return n?1:0}function ni(e){return"passage"===e.type?[]:[e.contact,e.lock].filter(e=>"string"==typeof e&&e.length>0)}function ri(e,t,i=200){const n=t.trim().toLowerCase();return(n?e.filter(e=>e.label.toLowerCase().includes(n)||e.value.toLowerCase().includes(n)):e).slice(0,Math.max(0,i))}function oi(e,t,i=.001){return Math.abs(e[0]-t[0])e[1]!=l>e[1]&&e[0]<(a-o)*(e[1]-s)/(l-s)+o&&(i=!i)}return i}function ai(e,t,i){const n=i[0]-t[0],r=i[1]-t[1],o=n*n+r*r;let s=o?((e[0]-t[0])*n+(e[1]-t[1])*r)/o:0;return s=Math.max(0,Math.min(1,s)),Math.hypot(e[0]-(t[0]+s*n),e[1]-(t[1]+s*r))}function li(e,t){if(!t||t.length<2)return null;let i=null,n=1/0;for(let r=0;rr&&s<-r||o<-r&&s>r)&&(a>r&&l<-r||a<-r&&l>r)}function pi(e,t=24){const i=e.map(e=>e[0]),n=e.map(e=>e[1]),r=Math.min(...i),o=Math.max(...i),s=Math.min(...n),a=Math.max(...n),l=Math.max(o-r,a-s)||1;let c=0,h=0,d=0;for(let t=0;t1e-9?[h/(3*c),d/(3*c)]:[(r+o)/2,(s+a)/2],p=(t,i)=>{const n=((t,i)=>{if(!si([t,i],e))return-1/0;let n=1/0;for(let r=0;rm&&(m=c,_=[n,l])}if(_){const[e,i]=_,n=(o-r)/t,l=(a-s)/t;for(let t=-4;t<=4;t++)for(let r=-4;r<=4;r++){const o=e+n*t/4,s=i+l*r/4,a=p(o,s);a>m&&(m=a,_=[o,s])}}return _||_i(e)||e[0]}function _i(e,t=1e-6){if(!e||e.length<3)return null;const i=e.length,n=[e.reduce((e,t)=>e+t[0],0)/i,e.reduce((e,t)=>e+t[1],0)/i];if(hi(n,e,t))return n;for(let n=0;ngi(e,t,i));return n.filter(e=>!n.some(t=>t!==e&&gi(t,e,i)))}function yi(e){if(!e||e.length<3)return 0;let t=0;for(let i=0;i[e[0],e[1]]),[e[0][0],e[0][1]]]]}function wi(e,t){if(!e||!t||e.length<3||t.length<3)return null;const i=Et(bi(e),bi(t));if(1!==i.length)return null;if(1!==i[0].length)return null;const n=i[0][0].slice(0,-1).map(e=>[e[0],e[1]]);return n.length>=3?n:null}function ki(e,t,i){for(let n=0;n1&&oi(i[0],i[i.length-1],t)&&i.pop(),i}function $i(e,t,i=1e-6){if(!e||e.length<3||!t||t.length<2)return null;const n=t[0],r=t[t.length-1];if(oi(n,r,i))return null;const o=ki(e,n,i),s=ki(e,r,i);if(o<0||s<0)return null;const a=t.slice(1,-1);for(const t of a)if(!hi(t,e,i))return null;for(let i=0;i{const s=[t];let a=(n+1)%e.length;for(let t=0;t<=e.length&&(s.push(e[a]),a!==o);t++)a=(a+1)%e.length;return s.push(r),xi(s,i)};let c,h;if(o===s){const s=xi([...t],i);if(s.length<3||yi(s)<=i)return null;const a=[];for(let i=0;i=0?t:[...t].reverse();for(const e of i)a.push(e)}c=xi(a,i),h=s}else c=xi([...l(n,o,r,s),...[...a].reverse()],i),h=xi([...l(r,s,n,o),...a],i);return c.length<3||h.length<3||yi(c)<=i||yi(h)<=i||Math.abs(yi(c)+yi(h)-yi(e))>Math.max(i,1e-6*yi(e))?null:[c,h]}function Si(e,t,i){const[n,r]=e.split(":");return"device"===n?r:"entity"===n?"lg_"+r:t&&t.startsWith("v_")?t:i()}function Mi(e){return e.length?Math.round(e.reduce((e,t)=>e+t,0)/e.length):null}function Ti(e,t){if(t>e[2]/e[3]){const i=e[3],n=e[3]*t;return{x:e[0]-(n-e[2])/2,y:e[1],w:n,h:i}}const i=e[2],n=e[2]/t;return{x:e[0],y:e[1]-(n-e[3])/2,w:i,h:n}}function Ri(e,t,i,n){if(e.length<2)return;const r=t.x+n,o=t.x+t.w-n,s=t.y+n,a=t.y+t.h-n;for(let t=0;t<60;t++){let t=!1;for(let n=0;n=0)i=t.slice(0,r).trim(),n=t.slice(r+1).trim();else{const e=t.split(".");e.length>2&&(i=e.slice(0,2).join("."),n=e.slice(2).join("."))}return/^[a-z0-9_]+\.[a-z0-9_]+$/.test(i)?r>=0&&!n||n&&!/^[a-zA-Z0-9_.-]+$/.test(n)?null:n?{entity:i,attr:n}:{entity:i}:null}function Li(e,t){const i=String(e??"").trim(),n=String(t??"").trim(),r=Hi(n?`${i}:${n}`:i);return r?`{${r.entity}${r.attr?`:${r.attr}`:""}}`:""}function ji(e,t,i){const n=String(t??"").trim();if(!n)return null;const r=e?.states?.[n];if(!r)return null;const o=String(i??"").trim(),s=e=>e.slice(0,60);if(o){const t=function(e){if(null==e)return null;if(Array.isArray(e)){const t=e.map(e=>null==e?"":String(e)).join(", ");return t?t.slice(0,60):null}if("object"==typeof e)return null;const t=String(e);return""===t?null:t.slice(0,60)}(r.attributes?.[o]);if(null===t)return null;const i=e?.formatEntityAttributeValue;if("function"==typeof i)try{const t=i.call(e,r,o);if("string"==typeof t&&""!==t)return{text:s(t),formatted:!0}}catch{}return{text:t,formatted:!1}}const a=r.state;if(null==a||""===a)return null;const l=e?.formatEntityState;if("function"==typeof l)try{const t=l.call(e,r);if("string"==typeof t&&""!==t)return{text:s(t),formatted:!0}}catch{}return{text:s(String(a)),formatted:!1}}function qi(e,t,i){const n=String(t??"").trim(),r=String(i??"").trim()||n;if(!r)return e.text;const o=e.formatted&&n?function(e,t){if(!t)return e;const i=e.replace(/\s+$/,"");return i.endsWith(t)?i.slice(0,i.length-t.length).replace(/\s+$/,""):e}(e.text,n):e.text;return`${o} ${r}`}function Bi(e,t){const i=(t?.entity||"").trim();if(!i)return"";const n=e?.states?.[i],r=n?.state;if(!n||null==r||""===r||"unavailable"===r||"unknown"===r)return Ei;const o=(t?.attr||"").trim(),s=ji(e,i,o||null);if(null===s)return Ei;return qi(s,o?"":String(n.attributes?.unit_of_measurement??"").trim(),t?.unit)}function Wi(e,t,i,n=()=>!0){const r=e??"";let o=!1;const s=r.replace(/\{([^{}\r\n]+)\}/g,(e,t)=>{const r=Hi(t);return r?(o=!0,n(r.entity||"")?Bi(i,r):Ei):e});if(o)return s;const a=(t?.entity||"").trim();if(!a)return r;if(!n(a)){const e=r.indexOf(Ni);return e>=0?r.slice(0,e)+Ei+r.slice(e+2):r?`${r} ${Ei}`:Ei}const l=Bi(i,t),c=r.indexOf(Ni);return c>=0?r.slice(0,c)+l+r.slice(c+2):r?`${r} ${l}`:l}const Ui=20;function Gi(e){const t=Number(e?.scale);if(Number.isFinite(t)&&t>0)return Math.min(20,Math.max(.15,t));return{s:.7,m:1,l:1.5}[String(e?.size??"")]??1}function Vi(e,t){if(!t)return e;let i=e;for(const[e,n]of Object.entries(t))i=i.split("{"+e+"}").join(String(n));return i}const Ki="#55606c",Yi=.55,Zi=20,Xi=25,Ji={c:"#607d8b",a:.18};function Qi(e,t=Ji){const i=e&&"object"==typeof e?e:null,n=i?.a;return{c:jt(i?.c,t.c),a:"number"==typeof n&&Number.isFinite(n)?Math.min(1,Math.max(0,n)):t.a}}function en(e,t){const i=Qi(e),n=t?.settings?.custom_fill;return n&&"object"==typeof n?Qi(n,i):i}function tn(e){const t=e?.settings||{},i=!e?.plan_url,n="glow"===t.fill_mode;return{showBorders:t.show_borders??i,showNames:t.show_names??i,color:jt(t.room_color,Ki),opacity:"number"==typeof t.room_opacity?Math.min(1,Math.max(0,t.room_opacity)):.55,fill:["lqi","light","temp","custom"].includes(t.fill_mode)?t.fill_mode:"none",customFill:Qi(t.custom_fill),glow:"boolean"==typeof t.glow_enabled?t.glow_enabled:n,tempMin:"number"==typeof t.temp_min?t.temp_min:20,tempMax:"number"==typeof t.temp_max?t.temp_max:25,showLqi:"boolean"==typeof t.show_lqi?t.show_lqi:null,cardFontScale:"number"==typeof t.card_font_scale&&t.card_font_scale>0?Math.min(3,Math.max(.5,t.card_font_scale)):1,labelTemp:!0===t.label_temp,labelHum:!0===t.label_hum,labelLqi:!0===t.label_lqi,labelLight:!0===t.label_light,bgColor:jt(t.bg_color,null),hideDecor:!0===t.hide_decor,hideOpenings:!0===t.hide_openings}}function nn(e,t){if(t.bgColor)return t.bgColor;const i=e?.bg_color;return jt(i,"")}const rn={light_on:{c:"#ffd45c",a:.18},light_off:{c:"#9aa0a6",a:.14},light_none:{c:"#6b7480",a:0},temp_cold:{c:"#4fc3f7",a:.18},temp_ok:{c:"#66d17a",a:.18},temp_hot:{c:"#ffd45c",a:.18},lqi_low:{c:"#f25a4a",a:.18},lqi_high:{c:"#4bd28f",a:.18},glow_base:{c:"#0d1b2a",a:.5},glow_light:{c:"#ffd9a0",a:.85},wall_fill:{c:"#ffffff",a:1}};function on(e){const t={},i=e?.fill_colors||{};for(const e of Object.keys(rn)){const n=rn[e],r=i[e];t[e]={c:jt(r?.c,n.c),a:r&&"number"==typeof r.a?Math.min(1,Math.max(0,r.a)):n.a}}return t}function sn(e,t,i){const n=Math.min(1,Math.max(0,i)),r=[1,3,5].map(t=>parseInt(e.slice(t,t+2),16)),o=[1,3,5].map(e=>parseInt(t.slice(e,e+2),16)),s=r.map((e,t)=>Math.round(e+(o[t]-e)*n));return"#"+s.map(e=>e.toString(16).padStart(2,"0")).join("")}function an(e,t,i,n,r,o,s,a=Ji){const l=function(e,t,i,n,r,o,s,a=Ji){if("custom"===e)return Qi(a);if("lqi"===e){if(null==t)return null;const e=(t-40)/140;return{c:sn(s.lqi_low.c,s.lqi_high.c,e),a:s.lqi_low.a+(s.lqi_high.a-s.lqi_low.a)*Math.min(1,Math.max(0,e))}}if("light"===e)return"none"===i?s.light_none.a>0?s.light_none:null:"on"===i?s.light_on:s.light_off;if("temp"===e){if(null==n)return null;const e=Math.min(r,o),t=Math.max(r,o);return nt?s.temp_hot:s.temp_ok}return null}(e,t,i,n,r,o,s,a);return l?{color:l.c,opacity:l.a,mode:e}:null}const ln={blind:["mdi:blinds","mdi:blinds-open"],shade:["mdi:blinds","mdi:blinds-open"],shutter:["mdi:window-shutter","mdi:window-shutter-open"],curtain:["mdi:curtains-closed","mdi:curtains"],window:["mdi:window-closed","mdi:window-open"],awning:["mdi:awning-outline","mdi:awning"],door:["mdi:door-closed","mdi:door-open"],garage:["mdi:garage","mdi:garage-open"],gate:["mdi:gate","mdi:gate-open"],damper:["mdi:circle-slice-8","mdi:circle-outline"]},cn=[["mdi:roller-shade-closed","mdi:roller-shade"],["mdi:blinds-horizontal-closed","mdi:blinds-horizontal"],["mdi:garage-variant","mdi:garage-open-variant"],["mdi:door","mdi:door-open"]];function hn(e){for(const t of[...Object.values(ln),...cn])if(e===t[0]||e===t[1])return t;return null}function dn(e,t,i,n,r){if(!n||"unavailable"===n||"unknown"===n)return e;if(r){const i="cover"===t?hn(e):null;return i?"closed"===n?i[0]:i[1]:e}if("binary_sensor"===t){if("door"===i)return"on"===n?"mdi:door-open":"mdi:door-closed";if("window"===i)return"on"===n?"mdi:window-open":"mdi:window-closed";if("garage_door"===i)return"on"===n?"mdi:garage-open-variant":"mdi:garage-variant"}if("cover"===t){const t=ln[String(i||"")];if(t)return"closed"===n?t[0]:t[1];const r=hn(e);return r?"closed"===n?r[0]:r[1]:e}return"lock"===t?"locked"===n?"mdi:lock":"mdi:lock-open-variant":"light"===t&&"mdi:lightbulb"===e&&"on"===n?"mdi:lightbulb-on":e}function un(e){const t=Math.min(4e4,Math.max(1e3,e))/100,i=t<=66?255:329.698727446*Math.pow(t-60,-.1332047592),n=t<=66?99.4708025861*Math.log(t)-161.1195681661:288.1221695283*Math.pow(t-60,-.0755148492),r=t>=66?255:t<=19?0:138.5177312231*Math.log(t-10)-305.0447927307,o=e=>Math.round(Math.min(255,Math.max(0,e)));return[o(i),o(n),o(r)]}const pn=1/2.2;function _n(e){const t=/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/.exec(e);return t?"#"+t.slice(1).map(e=>Math.min(255,Number(e)).toString(16).padStart(2,"0")).join(""):null}function mn(e){if(!e||"object"!=typeof e||Array.isArray(e))return null;const t=e;if(Object.keys(t).some(e=>"c"!==e&&"bri"!==e))return null;const i=jt(t.c,null);return i?void 0===t.bri||null===t.bri?{c:i}:"number"!=typeof t.bri||!Number.isFinite(t.bri)||t.bri<.01||t.bri>1?null:{c:i,bri:t.bri}:null}function gn(e,t,i){const n=mn(t),r=e?.attributes||{},o=n?.bri??function(e){const t=e?.attributes?.brightness,i="number"==typeof t?t:"string"==typeof t&&""!==t.trim()?Number(t):Number.NaN;return Number.isFinite(i)?Math.max(0,Math.min(1,i/255)):1}(e);if(n)return{c:n.c,bri:o};const s=Wt(r.rgb_color);if(s)return{c:_n(s)||jt(i,"#ffd9a0"),bri:o};const a=Number(r.color_temp_kelvin)||(Number(r.color_temp)>0?1e6/Number(r.color_temp):NaN);return Number.isFinite(a)&&a>0?{c:(l=un(a),"#"+l.slice(0,3).map(e=>Math.min(255,Math.max(0,Math.round(Number(e)||0))).toString(16).padStart(2,"0")).join("")),bri:o}:{c:jt(i,"#ffd9a0"),bri:o};var l}function fn(e,t,i){return"on"===e?.state?gn(e,t,i):null}function vn(e,t=1){const i=Math.max(0,Math.min(1,Number.isFinite(e)?e:1)),n=.7*Math.max(0,Math.min(1,Number.isFinite(t)?t:1))*(.4+.6*Math.pow(i,pn));return Math.max(0,Math.min(1,n))}function yn(e){return e.startsWith("light.")||e.startsWith("switch.")}function bn(e,t,i=1e-6){const n=[];if(!e||!t||e.length<3||t.length<3)return n;for(let r=0;ru||l>u)continue;const p=(r[0]-o[0])*h+(r[1]-o[1])*d,_=(s[0]-o[0])*h+(s[1]-o[1])*d,m=Math.max(0,Math.min(p,_)),g=Math.min(c,Math.max(p,_));g-m>i&&n.push([o[0]+h*m,o[1]+d*m,o[0]+h*g,o[1]+d*g])}}return n}function wn(e,t,i=1e-6){const n=[];for(const r of e){const e=[r[0],r[1]],o=[r[2],r[3]],s=o[0]-e[0],a=o[1]-e[1],l=Math.hypot(s,a);if(lo||r>o)continue;const s=(n[0]-e[0])*c+(n[1]-e[1])*h,a=(n[2]-e[0])*c+(n[3]-e[1])*h,u=Math.max(0,Math.min(s,a)),p=Math.min(l,Math.max(s,a));p-u>i&&d.push([u,p])}if(!d.length){n.push([e[0],e[1],o[0],o[1]]);continue}d.sort((e,t)=>e[0]-t[0]);let u=0;for(const[t,r]of d)t-u>i&&n.push([e[0]+c*u,e[1]+h*u,e[0]+c*t,e[1]+h*t]),u=Math.max(u,r);l-u>i&&n.push([e[0]+c*u,e[1]+h*u,o[0],o[1]])}return n}function kn(e,t,i=1e-6){const n=[];for(let t=0;t{if("string"!=typeof e||!e)return;const i=Mn(e);i.startsWith("/api/houseplan/content/")&&t.add(i)};for(const t of e?.spaces||[]){i(t?.plan_url);for(const e of t?.markers||[])for(const t of e?.pdfs||[])i(t?.url)}for(const t of e?.markers||[])for(const e of t?.pdfs||[])i(e?.url);return t}function Mn(e){return e?e.startsWith("/houseplan_files/plans/")?"/api/houseplan/content/plans/_/"+e.slice(23):e.startsWith("/houseplan_files/files/")?"/api/houseplan/content/files/"+e.slice(23):e:""}function Tn(e,t){const i=t?.settings?.fill_mode;return"none"===i||"lqi"===i||"light"===i||"temp"===i||"custom"===i?i:e}function Rn(e,t){const i=t?.settings;return"boolean"==typeof i?.glow?i.glow:"glow"===i?.fill_mode||e}function Cn(e,t,i,n){if(!t||!i||t===i)return e;const r="/files/"+t+"/",o="/files/"+i+"/";return e.map(e=>{if(!e.url.includes(r))return e;const t=e.url.split(r)[1]||"",[i,s]=[t.split("?")[0],t.includes("?")?"?"+t.split("?")[1]:""];if(n){const t=n[decodeURIComponent(i)]??n[i];return t?{...e,url:e.url.split(r+i)[0]+o+encodeURIComponent(t)+s}:e}return{...e,url:e.url.split(r).join(o)}})}function Dn(e,t=1){const i=Number(e);return Number.isFinite(i)&&i>0?Math.min(3,Math.max(.5,i)):t}function An(e,t,i){let n=null,r=null;for(const o of t){if(!(Math.abs(o[0]-e[0])<1e-6&&Math.abs(o[1]-e[1])<1e-6)){if(Math.abs(o[0]-e[0])<=i){const t=Math.abs(o[1]-e[1]);t>1e-6&&(!n||t1e-6&&(!r||t"number"==typeof e&&Number.isFinite(e),Wn=(e,t,i)=>Math.min(i,Math.max(t,e));function Un(e){return e.elevation<=-6?"night":e.elevation>=6?"day":e.rising?"dawn":"dusk"}function Gn(e){const t=(Math.floor(Number(e)||0)%1440+1440)%1440;return t>=300&&t<480?"dawn":t>=480&&t<1080?"day":t>=1080&&t<1260?"dusk":"night"}function Vn(e){const t=Un(e),i=e.azimuth*Math.PI/180;return{sunX:50-42*Math.sin(i),sunY:78-Wn(e.elevation,0,90)/90*64,sunOpacity:"night"===t?0:Wn((e.elevation+6)/12,0,1)}}function Kn(e){const t=(Math.floor(Number(e)||0)%1440+1440)%1440;if(t<300||t>=1260)return{sunX:50,sunY:78,sunOpacity:0};const i=(t-300)/960,n=(t-300)/120,r=(1260-t)/120;return{sunX:8+84*i,sunY:78-64*Math.sin(i*Math.PI),sunOpacity:Math.max(.18,Math.min(n,r,1))}}function Yn(e,t=new Date){const i=function(e){const t=e?.states?.["sun.sun"]?.attributes;return t&&Bn(t.azimuth)&&Bn(t.elevation)&&"boolean"==typeof t.rising?{azimuth:Ln(t.azimuth),elevation:t.elevation,rising:t.rising}:null}(e);if(i)return{phase:Un(i),source:"sun",...Vn(i)};const n="number"==typeof t?t:function(e=new Date){return 60*e.getHours()+e.getMinutes()}(t);return{phase:Gn(n),source:"clock",...Kn(n)}}function Zn(e){return`${e.source}|${e.phase}|${e.sunX.toFixed(2)}|${e.sunY.toFixed(2)}|`+e.sunOpacity.toFixed(3)}const Xn=[[-90,"#070c14"],[-12,"#070c14"],[-4,"#131a28"],[0,"#4a3527"],[10,"#e8ddcf"],[30,"#ffffff"],[90,"#ffffff"]],Jn=e=>Math.min(1,Math.max(0,e));function Qn(e,t,i=6){const n=e.angle*Math.PI/180,r=[Math.sin(n),-Math.cos(n)],o=n=>{const o=[e.x+r[0]*i*n,e.y+r[1]*i*n];return t.find(e=>e.poly.length>=3&&si(o,e.poly))||null},s=o(1),a=o(-1);return s&&a?null:s||a?s?{normal:[-r[0],-r[1]],roomId:s.id}:{normal:r,roomId:a.id}:null}function er(e,t,i){return i>0&&e[0]*t[0]+e[1]*t[1]>.05}function tr(e,t,i,n){return[[e[0],e[1]],[t[0],t[1]],[t[0]+i[0]*n,t[1]+i[1]*n],[e[0]+i[0]*n,e[1]+i[1]*n]]}function ir(e,t){try{const i=Nt([[...e.map(e=>[e[0],e[1]]),[e[0][0],e[0][1]]]],[[...t.map(e=>[e[0],e[1]]),[t[0][0],t[0][1]]]]),n=[];for(const e of i){const t=e?.[0];!Array.isArray(t)||t.length<4||n.push(t.slice(0,t.length-1).map(e=>[e[0],e[1]]))}return n}catch{return[]}}function nr(e,t,i,n,r,o,s){if(!(n>0))return[];const a=jn(i,r),l=[-a[0],-a[1]],c=function(e){const t=Math.min(90,Math.max(0,e));return.7*(.8+1.7*Math.pow(1-t/90,1.6))}(n),h=[];for(const i of t){if(!(i.length>0))continue;const t=Qn(i,e);if(!t||!er(t.normal,a,n))continue;const r=e.find(e=>e.id===t.roomId);if(!r)continue;const d=o&&o[t.roomId]||r.poly,u=i.angle*Math.PI/180,p=i.length/2,_=[-t.normal[0],-t.normal[1]],m=Math.max(0,s?.[i.id]||0),g=i.x+_[0]*m/2,f=i.y+_[1]*m/2,v=Math.cos(u)*p,y=Math.sin(u)*p,b=[g-v,f-y],w=[g+v,f+y],k=c*i.length,x=ir(tr(b,w,l,k),d);if(!x.length)continue;const $=l[0]*_[0]+l[1]*_[1];h.push({openingId:i.id,roomId:t.roomId,polys:x,a:b,b:w,dir:l,len:k,normal:_,depth:k*$})}return h}const rr=e=>"number"==typeof e&&Number.isInteger(e)&&e>=0&&e<=359?e:null;function or(e,t){const i=rr(t?.north_deg);return null!==i?i:rr(e?.north_deg)}function sr(e,t){const i=e=>"static"===e||"daynight"===e?e:null;return i(t?.bg_mode)??i(e?.bg_mode)??"static"}function ar(e,t){const i=t?.sun_rays;return"boolean"==typeof i?i:!0===e?.sun_rays}function lr(e){const t=e?.states?.["sun.sun"]?.attributes,i=Number(t?.azimuth),n=Number(t?.elevation);return Number.isFinite(i)&&Number.isFinite(n)?{azimuth:i,elevation:n}:null}const cr=["dawn","day","dusk","night"];function hr(e){if(!e)return"";const t=qn[e.phase];return[`--hp-day-cycle-outline-near:${t.outlineNear}`,`--hp-day-cycle-outline-mid:${t.outlineMid}`,`--hp-day-cycle-outline-far:${t.outlineFar}`].join(";")}function dr(e,t=1){if(!e)return G;const i=[`--hp-day-cycle-sun-x:${e.sunX.toFixed(2)}%`,`--hp-day-cycle-sun-y:${e.sunY.toFixed(2)}%`,`--hp-day-cycle-sun-opacity:${e.sunOpacity.toFixed(3)}`,`opacity:${Math.min(1,Math.max(0,t)).toFixed(4)}`].join(";");return B``}const ur={color:"#607d8b",opacity:1,widthCm:3.6,fill:!1,fillColor:"#607d8b",fillOpacity:.25},pr=(e,t=1)=>{const i=Number(e);return Number.isFinite(i)?Math.min(1,Math.max(0,i)):t},_r=e=>{let t=Number(e);return Number.isFinite(t)?(t=(t%360+360)%360,t>180?t-360:t):0};function mr(e,t,i){return _r(t)?[e.x,e.y]:i([e.x,e.y])}function gr(e,t,i,n){const r=Math.abs(i[0]-t[0]),o=Math.abs(i[1]-t[1]);return"line"===e?Math.hypot(r,o)>=n:r>=n&&o>=n}const fr=(e,t,i)=>{const n=Number(e),r=Number.isFinite(t)&&t>0?t:5;return Number.isFinite(n)&&n>0?n/r*i:0},vr=(e,t,i)=>{const n=Number(e),r=Number.isFinite(t)&&t>0?t:5;return Number.isFinite(n)&&i>0?n/i*r:0},yr=(e,t,i,n=ur.widthCm)=>{const r=Number(e?.width_cm);if(Number.isFinite(r)&&r>0)return r;const o=Number(e?.width);return Number.isFinite(o)&&o>0?vr(o,t,i):n};function br(e,t){return{color:e.color,opacity:pr(e.opacity),width_cm:Math.max(.1,Math.min(100,Number(e.widthCm)||.1)),...t?{fill:e.fill,fill_color:e.fillColor,fill_opacity:pr(e.fillOpacity,.25)}:{}}}function wr(e){const t=e.x+e.w/2,i=e.y+e.h/2,n=_r(e.angle)*Math.PI/180,r=Math.cos(n),o=Math.sin(n),s=(e,n)=>{const s=e-t,a=n-i;return[t+s*r-a*o,i+s*o+a*r]};return[s(e.x,e.y),s(e.x+e.w,e.y),s(e.x+e.w,e.y+e.h),s(e.x,e.y+e.h)]}function kr(e){const t=wr(e),i=(e,t)=>[(e[0]+t[0])/2,(e[1]+t[1])/2],n=[(t[0][0]+t[2][0])/2,(t[0][1]+t[2][1])/2];return{points:[...t,i(t[0],t[1]),i(t[1],t[2]),i(t[2],t[3]),i(t[3],t[0]),n],segments:t.map((e,i)=>({a:e,b:t[(i+1)%4]}))}}function xr(e,t,i,n,r,o,s,a){const l=_r(e.angle)*Math.PI/180,c=Math.cos(l),h=Math.sin(l),d=-Math.sin(l),u=Math.cos(l),p=e.x+e.w/2,_=e.y+e.h/2,m=t>0?-e.w/2:e.w/2,g=i>0?-e.h/2:e.h/2,f=p+m*c+g*d,v=_+m*h+g*u,y=n-f,b=r-v;let w=(y*c+b*h)*(t>0?1:-1),k=(y*d+b*u)*(i>0?1:-1),x=null;if(o){const t=w/Math.max(e.w,a),i=k/Math.max(e.h,a);x=Math.max(a/Math.max(e.w,a),a/Math.max(e.h,a),t,i),w=e.w*x,k=e.h*x}if(s>0)if(o){const t=Math.max(1,Math.round(e.w/s)),i=Math.max(1,Math.round(e.h/s));if(Math.abs(e.w-t*s)<1e-6&&Math.abs(e.h-i*s)<1e-6){const n=(e,t)=>{let i=Math.abs(e),n=Math.abs(t);for(;n;)[i,n]=[n,i%n];return Math.max(1,i)},r=1/n(t,i),o=Math.max(a/e.w,a/e.h),s=Math.max(o,Math.round((x??1)/r)*r);w=e.w*s,k=e.h*s}else{e.w>=e.h?(w=Math.round(w/s)*s,k=w*(e.h/Math.max(e.w,a))):(k=Math.round(k/s)*s,w=k*(e.w/Math.max(e.h,a)))}}else w=Math.round(w/s)*s,k=Math.round(k/s)*s;w=Math.max(a,w),k=Math.max(a,k);return{x:f+(t>0?w/2:-w/2)*c+(i>0?k/2:-k/2)*d-w/2,y:v+(t>0?w/2:-w/2)*h+(i>0?k/2:-k/2)*u-k/2,w:w,h:k,angle:_r(e.angle)||void 0}}const $r=(e,t,i)=>{const n=i[0]-t[0],r=i[1]-t[1],o=n*n+r*r;if(o<1e-12)return[...t];const s=Math.max(0,Math.min(1,((e[0]-t[0])*n+(e[1]-t[1])*r)/o));return[t[0]+n*s,t[1]+r*s]};function Sr(e,t,i,n){const r=n(e);let o=null;for(const r of t.points){const t=n(r),s=Math.hypot(t[0]-e[0],t[1]-e[1]);s<=i&&(!o||se.points),segments:e.flatMap(e=>e.segments)}}const Tr=Math.tan(.25*Math.PI/180);function Rr(e,t){const i=Math.abs(Number(t[0])-Number(e[0])),n=Math.abs(Number(t[1])-Number(e[1]));if(!([i,n].every(Number.isFinite)&&i>0&&n>0))return null;const r=i>=n?"horizontal":"vertical",o=Math.max(i,n),s=Math.min(i,n);return s/o>Tr?null:{axis:r,major:o,minor:s,angleDegrees:180*Math.atan2(s,o)/Math.PI}}function Cr(e,t){const i=Rr(e,t);return i?"horizontal"===i.axis?[Number(t[0]),Number(e[1])]:[Number(e[0]),Number(t[1])]:[Number(t[0]),Number(t[1])]}const Dr=e=>`${e[0]},${e[1]}`,Ar=(e,t)=>e[0]===t[0]&&e[1]===t[1],Or=(e,t)=>{const i=Dr(e),n=Dr(t);return i{let t=0;for(let i=0;i{if(e.length<3)return!1;for(let t=0;t1e-12},Fr=e=>{for(let t=0;t+1Ar(e,t.from)?[...t.to]:[...e],Er=(e,t)=>e.map(e=>Array.isArray(e?.poly)?{...e,poly:e.poly.map(e=>Ir(e,t))}:e),Nr=(e,t,i)=>{const n=e.flatMap((e,t)=>Zt(e)?.some(e=>Ar(e,i.from))?[t]:[]);if(!n.length)return!1;for(const i of n){const n=Zt(e[i]),r=Zt(t[i]);if(!n||!r)continue;if(!Pr(r))return!1;const o=Math.sign(zr(n)),s=Math.sign(zr(r));if(o&&s&&o!==s)return!1}const r=new Set;for(const e of n){const i=Zt(t[e]);if(i)for(let n=0;n{let i=0;for(const n of e){const e=Zt(n);if(e)for(let n=0;n{const{a:i,b:n,axis:r}=e,o="horizontal"===r?{from:n,to:[n[0],i[1]]}:{from:n,to:[i[0],n[1]]},s="horizontal"===r?{from:i,to:[i[0],n[1]]}:{from:i,to:[n[0],i[1]]};return[{move:o,preservedDegree:Hr(t,i)},{move:s,preservedDegree:Hr(t,n)}].sort((e,t)=>t.preservedDegree-e.preservedDegree||Dr(e.move.to).localeCompare(Dr(t.move.to))||Dr(e.move.from).localeCompare(Dr(t.move.from))).map(e=>e.move)};function jr(e){const t=JSON.parse(JSON.stringify(e||{}));let i=Array.isArray(t.rooms)?t.rooms:[];const n=new Map;for(const e of i){const t=Zt(e);if(t)for(let e=0;ee.key.localeCompare(t.key))){let t=null;for(const n of Lr(e,i)){const e=a.get(Dr(n.from));if(e&&e!==Dr(n.to))continue;const r=Er(i,n);if(Nr(i,r,n)){t=n,i=r;break}}t?(a.set(Dr(t.from),Dr(t.to)),r++,s=Math.max(s,Math.hypot(t.to[0]-t.from[0],t.to[1]-t.from[1]))):o++}t.rooms=i;for(const e of t.room_drafts||[]){if(!Array.isArray(e?.points)||e.points.length<2)continue;const t=e.points.map(e=>[...e]),i=t.slice(0,-1).flatMap((i,n)=>{const r=t[n+1],o=Rr(i,r);return o?[{key:`${String(e.id||"")}:${n}`,a:[...i],b:[...r],axis:o.axis}]:[]});let n=t;for(const e of i){let t=null;for(const i of Lr(e,[{poly:n}])){const e=n.map(e=>Ir(e,i)),r=e.length>=4&&Ar(e[0],e[e.length-1]),o=r?e.slice(0,-1):e;if(r?Pr(o):Fr(o)){t=i,n=e;break}}t?(r++,s=Math.max(s,Math.hypot(t.to[0]-t.from[0],t.to[1]-t.from[1]))):o++}e.points=n}for(const e of t.partitions||[]){if(!Array.isArray(e?.a)||!Array.isArray(e?.b))continue;const i=Rr(e.a,e.b);if(!i)continue;const n={key:String(e.id||Or(e.a,e.b)),a:[...e.a],b:[...e.b],axis:i.axis};let a=null;for(const i of Lr(n,[{poly:[e.a,e.b]}])){const n=Ir(e.a,i),r=Ir(e.b,i),o=Math.hypot(r[0]-n[0],r[1]-n[1]);if(!(o>0))continue;const s=(t.openings||[]).filter(t=>"partition"===t?.host?.kind&&t.host.id===e.id).every(e=>{const t=Number(e.host.t),i=Number(e.length);return Number.isFinite(t)&&t>=0&&t<=1&&Number.isFinite(i)&&i>0&&t*o-i/2>=-1e-12&&t*o+i/2<=o+1e-12});if(s){a=i,e.a=n,e.b=r;break}}a?(r++,s=Math.max(s,Math.hypot(a.to[0]-a.from[0],a.to[1]-a.from[1]))):o++}return{space:t,report:{wallsStraightened:r,wallsStraightenSkipped:o,maxStraightenShift:s},changed:r>0}}const qr=10**9,Br=240,Wr=1e-4;function Ur(e){if(Array.isArray(e))return e.map(e=>Ur(e));if(null!==e&&"object"==typeof e){const t={};for(const[i,n]of Object.entries(e))t[i]=Ur(n);return t}return e}function Gr(e){if("number"!=typeof e||!Number.isFinite(e))return e;const t=(e<0||Object.is(e,-0)?-1:1)*(Math.floor(Math.abs(e)*qr+.5)/qr);return 0===t?0:t}function Vr(e){if("number"!=typeof e||!Number.isFinite(e))return e;const t=e*Br,i=Math.round(t);if(Math.abs(t-i)null!==Yr(e)):[]}function Xr(e,t){for(const i of t)Object.prototype.hasOwnProperty.call(e,i)&&(e[i]=Gr(e[i]))}function Jr(e,t){for(const i of t)Object.prototype.hasOwnProperty.call(e,i)&&(e[i]=Vr(e[i]))}function Qr(e){if(Array.isArray(e))for(let t=0;t{for(const r of n){const n=t[r];"number"==typeof n&&Number.isFinite(n)&&i(n,e)}},n=t=>{if(Array.isArray(t))for(let n=0;n{if(Array.isArray(e))for(const t of e)n(t)};for(const i of Zr(e.rooms))t(i,["x","y","w","h"]),r(i.poly);for(const t of Zr(e.walls))n(t.a),n(t.b);for(const t of Zr(e.wall_segments))n(t.a),n(t.b);for(const i of Zr(e.openings))t(i,["x","y"]);for(const i of Zr(e.decor))"line"===i.kind?t(i,["x1","y1","x2","y2"]):"rect"===i.kind||"ellipse"===i.kind||"furniture"===i.kind?t(i,["x","y","w","h"]):"text"===i.kind&&t(i,["x","y"]);for(const t of Zr(e.room_drafts))r(t.points);for(const t of Zr(e.partitions))n(t.a),n(t.b);for(const t of Zr(e.wall_columns))n(t.center);for(const t of Zr(e.open_spans))n(t.a),n(t.b)}const r=Yr(t);if(!r)return;const o=new Map;if(n)for(const e of Zr(n.spaces))null!=e.id&&o.set(String(e.id),e);for(const e of Object.values(r)){const t=Yr(e);if(!t)continue;const n=null!=t.s&&o.get(String(t.s))||null;for(const e of["x","y"]){const r=t[e];"number"==typeof r&&Number.isFinite(r)&&i(r,n)}}}function io(e){const t=Number(e?.cell_cm);return t>0?t:5}function no(e,t={}){const i={canonicalized:0,far:0,maxShift:0,maxShiftCm:0,spaces:[],bySpace:new Map},n=Zr(Yr(e)?.spaces);let r=5;for(const e of n)r=Math.max(r,io(e));to(e,t,(e,t)=>{const n=e*Br,o=Math.abs(n-Math.round(n)),s=Vr(e),a=o>0&&o=Wr))return;const l=a&&"number"==typeof s?Math.abs(s-e):0;if(a?i.canonicalized++:i.far++,i.maxShift=Math.max(i.maxShift,l),i.maxShiftCm=Math.max(i.maxShiftCm,l*Br*(t?io(t):r)),!t?.id||!a)return;const c=String(t.id);let h=i.bySpace.get(c);h||(h={spaceId:c,space:String(t.title||c),canonicalized:0,far:0,maxShift:0,maxShiftCm:0},i.bySpace.set(c,h)),h.canonicalized++,h.maxShift=Math.max(h.maxShift,l),h.maxShiftCm=Math.max(h.maxShiftCm,l*Br*io(t))}),i.bySpace.size&&to(e,t,(e,t)=>{if(!t?.id)return;const n=i.bySpace.get(String(t.id));if(!n)return;const r=e*Br;Math.abs(r-Math.round(r))>=Wr&&n.far++}),i.spaces=[...i.bySpace.values()];const{bySpace:o,...s}=i;return s}function ro(e){return function(e){const t=e,i=Yr(t);i&&Jr(i,["x","y"]);return t}(Ur(e))}function oo(e){const t=e,i=Yr(t);if(!i)return t;for(const e of Object.values(i)){const t=Yr(e);t&&Jr(t,["x","y"])}return t}function so(e){return ao(Ur(e))}function ao(e){const t=e,i=Yr(t);if(!i)return t;for(const e of Zr(i.spaces)){Xr(e,["plan_x","plan_y","plan_scale","plan_scale_x","plan_scale_y","plan_angle"]);for(const t of Zr(e.rooms))Jr(t,["x","y","w","h"]),eo(t.poly);for(const t of Zr(e.walls))Qr(t.a),Qr(t.b);for(const t of Zr(e.wall_segments))Qr(t.a),Qr(t.b);for(const t of Zr(e.openings)){Jr(t,["x","y"]),Xr(t,["angle","length"]);const e=Yr(t.host);e&&Xr(e,["t"])}for(const t of Zr(e.decor))"line"===t.kind?Jr(t,["x1","y1","x2","y2"]):"rect"===t.kind||"ellipse"===t.kind||"furniture"===t.kind?(Jr(t,["x","y","w","h"]),Xr(t,["angle"])):"text"===t.kind&&(Jr(t,["x","y"]),Xr(t,["scale","angle"]));for(const t of Zr(e.room_drafts))eo(t.points);for(const t of Zr(e.partitions))Qr(t.a),Qr(t.b);for(const t of Zr(e.wall_columns))Qr(t.center),"square"===t.shape&&Xr(t,["angle"]);for(const t of Zr(e.open_spans))Qr(t.a),Qr(t.b)}for(const e of Zr(i.markers))Xr(e,["angle"]);return t}const lo=15,co=8;function ho(e,t,i,n,r){const o=i[0]-e[0],s=i[1]-e[1],a=Math.hypot(o,s);if(!(a>0)||a<=r+1e-9)return null;const l=o/a,c=s/a,h=t=>{const n=(t[0]-e[0])*l+(t[1]-e[1])*c,o=(r-n)/(a-n);return!Number.isFinite(o)||o<-1e-9||o>1+1e-9?null:[t[0]+(i[0]-t[0])*o,t[1]+(i[1]-t[1])*o]},d=h(t),u=h(n);return d&&u?[[e[0],e[1]],t,d,u,n]:null}const uo=Math.sin(.25*Math.PI/180);function po(e,t){return Number.isFinite(e)&&e>0&&Number.isFinite(t)&&t>0&&e*t<3}function _o(e){const t=Number(e)>0?Number(e):5;if(5===t)return 8;const i=5/t*8;return Math.min(80,Math.max(.5,i))}function mo(e,t){return Number.isFinite(e)&&e>0&&Number.isFinite(t)&&t>0&&e*t<2}function go(e){return Number.isFinite(e)?Math.max(1,Math.min(100,e)):1}function fo(e,t){return!Number.isFinite(e)||e<0?"":String(t?Math.round(e/2.54*100)/100:Math.round(100*e)/100)}function vo(e,t,i){if(!Number.isFinite(e)||e<=0)return 0;const n=Number(t)>0?Number(t):5;return go(e)/n*i}function yo(e,t){return t>0&&Number.isFinite(e)?Math.round(e/t)*t:e}function bo(e){return Math.max(1e-6*Math.abs(e),1e-9)}function wo(e,t){if(!(t>0&&Number.isFinite(e)))return e;const i=yo(e,t);return Math.abs(i-e)<=bo(t)?i:e}function ko(e,t){let i=t[0]-e[0],n=t[1]-e[1];const r=Math.hypot(i,n);return r<1e-12?[1,0]:(i/=r,n/=r,(i<-1e-12||Math.abs(i)<=1e-12&&n<0)&&(i=-i,n=-n),[i,n])}function xo(e,t,i){const n=[wo(e[0],i),wo(e[1],i)],r=[wo(t[0],i),wo(t[1],i)],o=yo((n[0]+r[0])/2,i),s=yo((n[1]+r[1])/2,i),[a,l]=ko(n,r);let c=Math.atan2(l,a);c<0&&(c+=Math.PI);const h=Math.round(1800*c)/1800,d=i>0&&i<.01?6:i<1?4:2;return`${o.toFixed(d)},${s.toFixed(d)}@${h.toFixed(4)}`}function $o(e,t,i,n){return 1===n?xo(e,t,i):xo([e[0]/n,e[1]/n],[t[0]/n,t[1]/n],i)}function So(e,t){if(!Array.isArray(e.a)||!Array.isArray(e.b)||e.a.length<2||e.b.length<2)return null;const i=[Number(e.a[0]),Number(e.a[1]),Number(e.b[0]),Number(e.b[1])];if(!i.every(Number.isFinite))return null;const n=t>0?t:1;return[[i[0]*n,i[1]*n],[i[2]*n,i[3]*n]]}function Mo(e,t,i,n,r){const o=r>0?r:1;return{key:$o(e,t,n,o),cm:go(i),a:[e[0]/o,e[1]/o],b:[t[0]/o,t[1]/o]}}function To(e,t){const i=t>0?t:1,n=[];for(const t of e){const e=t.key.lastIndexOf("@");if(e<0)continue;const[r,o]=t.key.slice(0,e).split(",").map(Number),s=Number(t.key.slice(e+1));[r,o,s].every(Number.isFinite)&&n.push({w:t,x:r*i,y:o*i,ang:s})}return n}function Ro(e,t){const[i,n]=ko(e,t);let r=Math.atan2(n,i);return r<0&&(r+=Math.PI),r}function Co(e,t){let i=Math.abs(e-t);return i>Math.PI/2&&(i=Math.PI-i),i<.02}function Do(e,t,i,n,r=1){if(!e?.length)return null;const o=$o(t,i,n,r),s=e.find(e=>e.key===o);if(s)return s;const a=r>0?r:1,l=bo(n)*a,c=(e,t)=>Math.abs(e[0]-t[0])<=l&&Math.abs(e[1]-t[1])<=l;for(const n of e){const e=So(n,a);if(e&&(c(e[0],t)&&c(e[1],i)||c(e[0],i)&&c(e[1],t)))return n}const h=(t[0]+i[0])/2,d=(t[1]+i[1])/2,u=Ro(t,i),p=Math.max(.5*n,1e-9)*a;for(const t of To(e,a))if(Co(t.ang,u)&&Math.hypot(t.x-h,t.y-d)<=p)return t.w;return null}function Ao(e,t,i,n,r=1){const o=Do(e,t,i,n,r);if(o&&o.cm>0)return go(o.cm);const s=function(e,t,i,n,r){if(!e?.length)return null;const o=r>0?r:1,s=Math.hypot(i[0]-t[0],i[1]-t[1]);if(s<1e-12)return null;const a=Ro(t,i),l=Math.max(.5*n,1e-9)*o;let c=null;for(const n of e){if(!(n.cm>0))continue;const e=So(n,o);if(!e)continue;const r=Math.hypot(e[1][0]-e[0][0],e[1][1]-e[0][1]);if(r<1e-12||!Co(Ro(e[0],e[1]),a))continue;if(bs(t[0],t[1],e[0][0],e[0][1],e[1][0],e[1][1])>l||bs(i[0],i[1],e[0][0],e[0][1],e[1][0],e[1][1])>l)continue;if(r+l(o.has(e.key)||(e=>{const t=So(e,n);if(!t)return!1;const[i,o]=t,a=o[0]-i[0],c=o[1]-i[1],h=Math.hypot(a,c);if(h<=l)return!1;if(!s.some(e=>{const t=[e[0],e[1]],n=[e[2],e[3]];return Co(Ro(i,o),Ro(t,n))&&bs(i[0],i[1],t[0],t[1],n[0],n[1])<=l&&bs(o[0],o[1],t[0],t[1],n[0],n[1])<=l}))return!1;const d=(r||[]).some(e=>{const t=[e[0],e[1]],n=[e[2],e[3]];if(!Co(Ro(i,o),Ro(t,n)))return!1;const r=e=>Math.abs((e[0]-i[0])*c-(e[1]-i[1])*a)/h;if(r(t)>l||r(n)>l)return!1;const s=h*h,d=((t[0]-i[0])*a+(t[1]-i[1])*c)/s,u=((n[0]-i[0])*a+(n[1]-i[1])*c)/s;return Math.min(1,Math.max(d,u))-Math.max(0,Math.min(d,u))>l/h});return!d})(e))&&e.cm>=1&&e.cm<=100)}function zo(e,t,i,n=8){const[r,o]=ko(e,t);let s=Math.atan2(o,r);s<0&&(s+=Math.PI);let a=i*Math.PI/180%Math.PI;a<0&&(a+=Math.PI);let l=Math.abs(s-a);return l>Math.PI/2&&(l=Math.PI-l),l<=n*Math.PI/180}function Po(e,t,i,n,r=1,o="affine",s){if(!e?.length)return[];if(t.length!==i.length)return"fixed-topology"===o&&s?.(),e.slice();const a=r>0?r:1,l=Math.max(.5*n,1e-9)*a,c=Math.max(n*a*1e-6,1e-9),h=[],d=new Map,u=new Map,p=(e,t,i)=>{const n=e.get(t)||new Set;n.add(i),e.set(t,n)};for(let e=0;e[e[0]+(t[0]-e[0])*i,e[1]+(t[1]-e[1])*i],m=(e,t)=>Math.hypot(e[0]-t[0],e[1]-t[1])<=c,g=(e,t)=>{if("fixed-topology"===o){const i=t.na[0]-t.oa[0],n=t.na[1]-t.oa[1],r=t.nb[0]-t.ob[0],o=t.nb[1]-t.ob[1];return Math.hypot(i-r,n-o)<=c?[e[0]+i,e[1]+n]:m(e,t.oa)?[...t.na]:m(e,t.ob)?[...t.nb]:[...e]}const i=Math.max(0,Math.min(1,((e[0]-t.oa[0])*t.dx+(e[1]-t.oa[1])*t.dy)/t.len2));return _(t.na,t.nb,i)},f=(e,t)=>{const[i,n]=ko(e,t);return(t[0]-e[0])*i+(t[1]-e[1])*n>=0?[[...e],[...t]]:[[...t],[...e]]},v=[],y=[],b=(e,t,i)=>{if(Math.hypot(t[0]-e[0],t[1]-e[1])<=c)return;const[r,o]=f(e,t),s=go(i);if(y.some(e=>e.entry.cm===s&&m(e.span[0],r)&&m(e.span[1],o)))return;const l=Mo(r,o,s,n,a);v.push(l),y.push({entry:l,span:[r,o]})};for(const t of e){const e=So(t,a);if(e){const[i,n]=f(e[0],e[1]),r=n[0]-i[0],s=n[1]-i[1],a=r*r+s*s,d=Math.sqrt(a);if(d<=c){v.push({...t,cm:go(t.cm)});continue}const u=[];for(const e of h){if(!Co(Ro(i,n),Ro(e.oa,e.ob)))continue;const t=e=>Math.abs((e[0]-i[0])*s-(e[1]-i[1])*r)/d;if(t(e.oa)>l||t(e.ob)>l)continue;const o=((e.oa[0]-i[0])*r+(e.oa[1]-i[1])*s)/a,h=((e.ob[0]-i[0])*r+(e.ob[1]-i[1])*s)/a,p=Math.max(0,Math.min(o,h)),_=Math.min(1,Math.max(o,h));(_-p)*d>c&&u.push({lo:p,hi:_,move:e})}if(!u.length){"fixed-topology"===o?v.push({...t}):b(i,n,t.cm);continue}const p=[0,1,...u.flatMap(({lo:e,hi:t})=>[e,t])].sort((e,t)=>e-t).filter((e,t,i)=>0===t||Math.abs(e-i[t-1])*d>c),y=[];for(let e=0;e+1a>=e.lo-1e-12&&a<=e.hi+1e-12);if(!l.length){y.push([o,s]);continue}const h=[g(o,l[0].move),g(s,l[0].move)],f=l.slice(1).some(e=>{const t=g(o,e.move),i=g(s,e.move);return!m(h[0],t)||!m(h[1],i)});y.push(f?[o,s]:h)}const w=(e,t)=>{const i=e[1][0]-e[0][0],n=e[1][1]-e[0][1],r=t[1][0]-t[0][0],o=t[1][1]-t[0][1],s=Math.hypot(i,n);return!(s<=c||i*r+n*o<=0)&&Math.abs(i*o-n*r)/s<=c},k=[];for(const e of y){const t=k[k.length-1];t&&m(t[1],e[0])&&w(t,e)?t[1]=e[1]:k.push([[...e[0]],[...e[1]]])}if("fixed-topology"===o&&1===k.length){const[i,n]=k[0];if(m(i,e[0])&&m(n,e[1])||m(i,e[1])&&m(n,e[0])){v.push({...t});continue}}for(const[e,i]of k)b(e,i,t.cm);continue}if("fixed-topology"===o){const e=u.get(t.key);if(1===e?.size){const i=[...e][0];v.push(i===t.key?{...t}:{...t,key:i});continue}const i=[To([t],a)[0]];1!==a&&i.push(To([t],1)[0]);const n=i.filter(Boolean).some(e=>h.some(t=>{if(!Co(e.ang,Ro(t.oa,t.ob)))return!1;const i=((e.x-t.oa[0])*t.dx+(e.y-t.oa[1])*t.dy)/t.len2;return i>=-1e-6&&i<=1.000001&&bs(e.x,e.y,t.oa[0],t.oa[1],t.ob[0],t.ob[1])<=l}));((e?.size||0)>1||n)&&s?.(),v.push({...t});continue}let i="";const r=d.get(t.key);if(1===r?.size&&(i=[...r][0]),!i){const e=To([t],a)[0];if(e){const t=new Set;for(const i of h){if(!Co(e.ang,Ro(i.oa,i.ob)))continue;const r=((e.x-i.oa[0])*i.dx+(e.y-i.oa[1])*i.dy)/i.len2;if(r<-1e-6||r>1.000001)continue;if(bs(e.x,e.y,i.oa[0],i.oa[1],i.ob[0],i.ob[1])>l)continue;const o=g([e.x,e.y],i),[s,c]=ko(i.na,i.nb),h=Math.max(n*a,1e-6);t.add($o([o[0]-s*h,o[1]-c*h],[o[0]+s*h,o[1]+c*h],n,a))}1===t.size&&(i=[...t][0])}}v.push({...t,key:i||t.key,cm:go(t.cm)})}return v}function Fo(e,t,i,n,r=1,o="affine"){return Po(e,t,i,n,r,o)}function Io(e,t,i,n,r=1,o="fixed-topology"){let s=!1;const a=Po(e,t,i,n,r,o,()=>{s=!0});return{walls:s?(e||[]).map(e=>({...e})):a,rejected:s}}function Eo(e,t,i,n=1,r=[]){const o=n>0?n:1,s=Math.abs(i),a=Math.max(s*o*Wr,1e-9),l=(r||[]).flatMap(e=>{const t=So(e,o);return t?t.map(e=>[...e]):[]}),c=e=>e.every((e,t)=>(e=>{if(!(s>0))return!0;const t=e/o/s;return Math.abs(t-Math.round(t))Math.abs(e-i[t])<=a)),h=[],d=e=>JSON.stringify([e.key,e.cm,e.a,e.b]);for(const i of e||[]){const e=So(i,o);if(!e)continue;const[n,r]=e;if(![n[0],n[1],r[0],r[1]].every(Number.isFinite)||!c(n)||!c(r)){h.push(d(i));continue}const s=r[0]-n[0],l=r[1]-n[1],u=Math.hypot(s,l);if(u<=a){h.push(d(i));continue}const p=s/u,_=l/u,m=[];for(const e of t){const[t,i]=e;if(![t?.[0],t?.[1],i?.[0],i?.[1]].every(Number.isFinite))continue;const r=e=>Math.abs((e[0]-n[0])*_-(e[1]-n[1])*p);if(r(t)>a||r(i)>a)continue;const o=(t[0]-n[0])*p+(t[1]-n[1])*_,s=(i[0]-n[0])*p+(i[1]-n[1])*_,l=Math.max(0,Math.min(o,s)),c=Math.min(u,Math.max(o,s));c-l>a&&m.push([l,c])}m.sort((e,t)=>e[0]-t[0]||e[1]-t[1]);let g=0;for(const[e,t]of m){if(e>g+a)break;if(g=Math.max(g,t),g>=u-a)break}ge.key!==s);return null==n||n<1?a:[...a,Mo(t,i,n,r,o)]}function Ho(e,t,i,n,r,o=[],s=1){let a=e?e.slice():[];for(const e of function(e,t,i,n,r=1,o=[]){const s=ss(e,t,i,n,r,o);if(!s)return[];const a=[];for(let e=0;e0||(c=No(c,t,i,n,r,s)))}return c}function jo(e){const{a:t,b:i,halfDepth:n}=e;if(!Array.isArray(t)||!Array.isArray(i)||t.length<2||i.length<2||![t[0],t[1],i[0],i[1]].every(Number.isFinite))return null;const r=i[0]-t[0],o=i[1]-t[1],s=Math.hypot(r,o);if(!(s>1e-9&&n>0&&Number.isFinite(n)))return null;const a=-o/s*n,l=r/s*n;return[[t[0]+a,t[1]+l],[i[0]+a,i[1]+l],[i[0]-a,i[1]-l],[t[0]-a,t[1]-l]]}function qo(e,t,i){return Math.hypot(e[0]-t[0],e[1]-t[1])<=i}function Bo(e,t,i){const n=t.b[0]-t.a[0],r=t.b[1]-t.a[1],o=n*n+r*r;if(!(o>i*i))return!1;const s=((e[0]-t.a[0])*n+(e[1]-t.a[1])*r)/o;if(!(s>0&&s<1))return!1;const a=[t.a[0]+n*s,t.a[1]+r*s];return Math.hypot(e[0]-a[0],e[1]-a[1])<=i}function Wo(e,t,i,n){const r=Math.hypot(t,i);if(!(r>1e-9&&n>0))return;const o=[t/r,i/r],s=e.find(e=>Math.abs(e.u[0]*o[1]-e.u[1]*o[0])<1e-9&&e.u[0]*o[0]+e.u[1]*o[1]>1-1e-9);s?s.halfDepth=Math.max(s.halfDepth,n):e.push({u:o,halfDepth:n})}function Uo(e,t){try{const i=Ht(xs(e),xs(t));let n=null,r=0;for(const e of i||[]){const t=e?.[0]||[],i=Math.abs(Xo(t));t.length>=4&&i>r&&(r=i,n=t.slice(0,-1).map(e=>[e[0],e[1]]))}return n}catch{return null}}function Go(e,t=1e-6){const i=(e||[]).map((e,t)=>({segment:e,index:t})).filter(({segment:e})=>e&&Array.isArray(e.a)&&Array.isArray(e.b)&&e.a.length>=2&&e.b.length>=2&&e.a.every(Number.isFinite)&&e.b.every(Number.isFinite)&&Number.isFinite(e.halfDepth)&&e.halfDepth>0&&Math.hypot(e.b[0]-e.a[0],e.b[1]-e.a[1])>1e-9);if(i.length<2)return[];const n=Math.max(Number.isFinite(t)?t:0,1e-9),r=i.flatMap(({segment:e})=>[e.a,e.b]).map(e=>[e[0],e[1]]).sort((e,t)=>e[0]-t[0]||e[1]-t[1]),o=[];for(const e of r)o.some(t=>qo(t,e,n))||o.push(e);const s=[];for(const e of o){const t=[];let r=!1;for(const{segment:o,index:s}of i){const i=Math.hypot(o.b[0]-o.a[0],o.b[1]-o.a[1]);qo(e,o.a,n)?t.push({u:[(o.b[0]-o.a[0])/i,(o.b[1]-o.a[1])/i],halfDepth:o.halfDepth,length:i,index:s}):qo(e,o.b,n)?t.push({u:[(o.a[0]-o.b[0])/i,(o.a[1]-o.b[1])/i],halfDepth:o.halfDepth,length:i,index:s}):Bo(e,o,n)&&(r=!0)}if(r||2!==t.length)continue;const[o,a]=t,l=o.u[0]*a.u[1]-o.u[1]*a.u[0];if(Math.abs(l)<1e-9)continue;const c=l<0?1:-1,h=[-o.u[1],o.u[0]],d=[-a.u[1],a.u[0]],u=[e[0]+h[0]*o.halfDepth*c,e[1]+h[1]*o.halfDepth*c],p=[e[0]-d[0]*a.halfDepth*c,e[1]-d[1]*a.halfDepth*c];if(!es(u,o.u,p,a.u))continue;const _=[[o,a,p,[d[0]*-c,d[1]*-c]],[a,o,u,[h[0]*c,h[1]*c]]];for(const[t,,i,r]of _){const o=Math.min(2*t.halfDepth,t.length),a=[-t.u[1]*t.halfDepth,t.u[0]*t.halfDepth],l=[[e[0]+a[0],e[1]+a[1]],[e[0]+t.u[0]*o+a[0],e[1]+t.u[1]*o+a[1]],[e[0]+t.u[0]*o-a[0],e[1]+t.u[1]*o-a[1]],[e[0]-a[0],e[1]-a[1]]],c=e=>(e[0]-i[0])*r[0]+(e[1]-i[1])*r[1],h=[];for(let e=0;e=-1e-12&&h.push(t),n>1e-12&&r<-1e-12||n<-1e-12&&r>1e-12){const e=n/(n-r);h.push([t[0]+(i[0]-t[0])*e,t[1]+(i[1]-t[1])*e])}}h.length>=3&&Math.abs(Xo(h))>n*n&&s.push({segmentIndex:t.index,wedge:h})}}return s}function Vo(e,t=1e-6){const i=(e||[]).filter(e=>e&&Array.isArray(e.a)&&Array.isArray(e.b)&&e.a.length>=2&&e.b.length>=2&&e.a.every(Number.isFinite)&&e.b.every(Number.isFinite)&&Number.isFinite(e.halfDepth)&&e.halfDepth>0&&Math.hypot(e.b[0]-e.a[0],e.b[1]-e.a[1])>1e-9);if(i.length<2)return[];const n=Math.max(Number.isFinite(t)?t:0,1e-9),r=i.flatMap(e=>[e.a,e.b]).map(e=>[e[0],e[1]]).sort((e,t)=>e[0]-t[0]||e[1]-t[1]),o=[];for(const e of r)o.some(t=>qo(t,e,n))||o.push(e);const s=[],a=i.map((e,t)=>({roomId:"",a:[e.a[0],e.a[1]],b:[e.b[0],e.b[1]],key:`join-${t}`,kind:"outer",cm:0,open:!1,half:e.halfDepth})),l=ds(a,n);for(const e of us(l).fans)s.push(e);const c=e=>!!ps(l,e);for(const e of o){if(c(e))continue;const t=[];for(const r of i)qo(e,r.a,n)?Wo(t,r.b[0]-r.a[0],r.b[1]-r.a[1],r.halfDepth):qo(e,r.b,n)?Wo(t,r.a[0]-r.b[0],r.a[1]-r.b[1],r.halfDepth):Bo(e,r,n)&&(Wo(t,r.a[0]-e[0],r.a[1]-e[1],r.halfDepth),Wo(t,r.b[0]-e[0],r.b[1]-e[1],r.halfDepth));if(!(t.length<2)){t.sort((e,t)=>Math.atan2(e.u[1],e.u[0])-Math.atan2(t.u[1],t.u[0])||e.halfDepth-t.halfDepth);for(let i=0;in*n&&s.push(m)}}}return s}function Ko(e,t,i,n){if(!(t>0)||!e||e.length<2)return"";if(i&&e.length>=3){let i=e;const r=e[e.length-1];if(e.length>=4&&Math.hypot(e[0][0]-r[0],e[0][1]-r[1])<1e-9&&(i=e.slice(0,-1)),i.length>=3){const e=i.map((e,i)=>n?.[i]||t),r=qs(i,e),o=ns(i,e);if(r&&o)return`${rs(r)} ${rs(os(o))}`}}const r=[];for(let i=0;i=1e-9&&a>0&&r.push({a:o,b:s,halfDepth:a})}const o=r.map(jo);for(const{segmentIndex:e,wedge:t}of Go(r)){const i=o[e];if(!i)continue;const n=Uo(i,t);n&&(o[e]=n)}const s=[...o.filter(e=>!!e),...Vo(r)],a=function(e){let t=null;try{for(const i of e){if(i.length<3)continue;const e=xs(i);t=t?Et(t,e):[e]}return t}catch{return null}}(s);return a?Fs(a):s.map(e=>rs(e)).join(" ")}function Yo(e,t,i,n,r=1){if(!i.length)return!1;const o=Zo(n,r),s=(e[0]+t[0])/2,a=(e[1]+t[1])/2,[l,c]=ko(e,t);for(const e of i){const[t,i]=ko([e[0],e[1]],[e[2],e[3]]);if(!(Math.abs(l*i-c*t)>.05)&&bs(s,a,e[0],e[1],e[2],e[3])<=o)return!0}return!1}function Zo(e,t){return Math.max(e*(t>0?t:1)*.04,1e-9)}function Xo(e){let t=0;for(let i=0;ie[1]!=l>e[1]&&e[0]<(a-o)*(e[1]-s)/(l-s+0)+o&&(i=!i)}return i}([c[0]+.001*a,c[1]+.001*l],e)||(a=-a,l=-l),[a,l]}function Qo(e,t){const i=e[0]*t[1]-e[1]*t[0],n=e[0]*t[0]+e[1]*t[1];return Math.abs(i)<1e-9&&n>0}function es(e,t,i,n){const r=t[0]*n[1]-t[1]*n[0];if(Math.abs(r)<1e-12)return null;const o=[i[0]-e[0],i[1]-e[1]],s=(o[0]*n[1]-o[1]*n[0])/r;return[e[0]+s*t[0],e[1]+s*t[1]]}function ts(e,t,i){const n=e?.length||0;if(n<3||!Array.isArray(i)||i.length!==n)return 0;const r=(Math.trunc(t)%n+n)%n,o=e[r],s=e[(r+1)%n],a=s[0]-o[0],l=s[1]-o[1],c=Math.hypot(a,l);if(!(c>0))return 0;const h=Math.max(0,Number(i[r])||0);if(!(h>0))return c;const d=[a/c,l/c],u=Jo(e,r),p=[o[0]+u[0]*h,o[1]+u[1]*h],_=t=>{const r=Math.max(0,Number(i[t])||0);if(!(r>0))return null;const s=e[t],a=e[(t+1)%n],l=a[0]-s[0],c=a[1]-s[1],h=Math.hypot(l,c);if(!(h>0))return null;const u=Jo(e,t),_=es(p,d,[s[0]+u[0]*r,s[1]+u[1]*r],[l/h,c/h]);return _?(_[0]-o[0])*d[0]+(_[1]-o[1])*d[1]:null},m=_((r-1+n)%n)??0,g=(_((r+1)%n)??c)-m;return g>0?g:0}function is(e,t,i,n,r,o,s,a=1){const l=(e||[]).find(e=>e?.id===t),c=Zt(l);if(!c||c.length<3)return null;const h=ms(e,t,i,n,r,o,s,a);if(!h)return c.map(()=>0);const d=4*Zo(r,a);return c.map((e,t)=>{const i=c[(t+1)%c.length],n=[(e[0]+i[0])/2,(e[1]+i[1])/2];for(let e=0;e!(e>0)))return e.map(e=>[e[0],e[1]]);const r=[];for(let o=0;o0||u>0)){r.push([e[o][0],e[o][1]]);continue}if(d>0!=u>0){const t=e[o],i=d>0?[t[0]+p*d,t[1]+_*d]:[t[0],t[1]],n=u>0?[t[0]+m*u,t[1]+g*u]:[t[0],t[1]];r.push(i),Math.hypot(n[0]-i[0],n[1]-i[1])>1e-9&&r.push(n);continue}if(Qo(w,k)){const t=e[o],i=[t[0]+p*d,t[1]+_*d],n=[t[0]+m*u,t[1]+g*u];r.push(i),Math.hypot(n[0]-i[0],n[1]-i[1])>1e-9&&r.push(n);continue}const S=es(x,w,$,k),M=Math.max(d,u,1e-9),T=ps(i,e[o])?.limit??4*M;if(S){const i=Math.hypot(S[0]-e[o][0],S[1]-e[o][1]);if(Number.isFinite(i)&&i<=T){r.push(S);continue}if(Es(e,t,o)){r.push(S);continue}}d>0&&r.push([e[o][0]+p*d,e[o][1]+_*d]),u>0&&r.push([e[o][0]+m*u,e[o][1]+g*u]),d>0||u>0||r.push([e[o][0],e[o][1]])}return r.length>=3?r:null}function rs(e,t=!0){if(!e.length)return"";let i=`M ${e[0][0]} ${e[0][1]}`;for(let t=1;te?.id===t),a=Zt(s);if(!a||a.length<3)return null;const l=Zo(n,r),c=[];for(const i of e||[]){if(!i||i.id===t)continue;const e=Zt(i);if(e)for(const t of bn(a,e,l))c.push([t[0],t[1]],[t[2],t[3]])}for(const e of i||[])c.push([e[0],e[1]],[e[2],e[3]]);for(const e of o||[]){const t=So(e,r);t&&c.push(t[0],t[1])}const h=[],d=[];for(let e=0;el)continue;const s=((e[0]-t[0])*(i[0]-t[0])+(e[1]-t[1])*(i[1]-t[1]))/(n*n);s<=r||s>=1-r||(o.some(e=>Math.abs(e-s)*n<=2*l)||o.push(s))}o.sort((e,t)=>e-t);for(const n of o)h.push([t[0]+(i[0]-t[0])*n,t[1]+(i[1]-t[1])*n]),d.push(e)}return{poly:h,parent:d,orig:a}}function as(e,t,i,n){const r=t.poly.length,o=new Array(r).fill(0);if(!e?.length)return o;const s=new Set,a=[];for(let l=0;l0?(o[l]=go(c.cm),s.add(c.key)):a.push(l)}if(!a.length)return o;const l=n>0?n:1,c=Math.max(.5*i,1e-9)*l,h=To(e,l).filter(e=>e.w.cm>0);for(let e=a.length-1;e>=0;e--){const i=a[e],n=t.poly[i],s=t.poly[(i+1)%r],d=Ro(n,s);let u=null;for(const e of h){const t=So(e.w,l);if(!t||!Co(Ro(t[0],t[1]),d))continue;if(bs(n[0],n[1],t[0][0],t[0][1],t[1][0],t[1][1])>c||bs(s[0],s[1],t[0][0],t[0][1],t[1][0],t[1][1])>c)continue;const i=Math.hypot(s[0]-n[0],s[1]-n[1]),r=Math.hypot(t[1][0]-t[0][0],t[1][1]-t[0][1]),o=Math.max(0,r-i);(!u||oc||bs(r[0],r[1],t[0][0],t[0][1],t[1][0],t[1][1])>c)continue;i=!0,o=Math.max(0,Math.hypot(t[1][0]-t[0][0],t[1][1]-t[0][1])-_)}else{if(bs(e.x,e.y,n[0],n[1],r[0],r[1])>c)continue;o=Math.hypot(e.x-d,e.y-u)}(!p||i&&!p.exact||i===p.exact&&o0?i:1,o=(e||[]).filter(e=>e&&!e.open&&null!==e.kind&&Number.isFinite(e.half)&&e.half>0&&Array.isArray(e.a)&&Array.isArray(e.b)&&e.a.length>=2&&e.b.length>=2&&[e.a[0],e.a[1],e.b[0],e.b[1]].every(Number.isFinite)&&Math.hypot(e.b[0]-e.a[0],e.b[1]-e.a[1])>n).sort((e,t)=>e.key.localeCompare(t.key)||e.a[0]-t.a[0]||e.a[1]-t.a[1]||e.b[0]-t.b[0]||e.b[1]-t.b[1]||e.half-t.half),s=new Map;for(const e of o){const t=s.get(e.key);t?e.half>t.half&&s.set(e.key,{...t,half:e.half}):s.set(e.key,e)}const a=[...s.values()].flatMap(e=>[{point:[e.a[0],e.a[1]],other:e.b,halfDepth:e.half,kind:e.kind,key:e.key},{point:[e.b[0],e.b[1]],other:e.a,halfDepth:e.half,kind:e.kind,key:e.key}]).sort((e,t)=>e.point[0]-t.point[0]||e.point[1]-t.point[1]||e.other[0]-t.other[0]||e.other[1]-t.other[1]||e.halfDepth-t.halfDepth),l=new Map;for(const e of a){const[t,i]=ls(e.point,n),r=cs(t,i),o=l.get(r)||[];o.push(e),l.set(r,o)}const c=[],h=new Map;for(const e of a){const t=hs(h,e.point,n).filter(t=>Math.hypot(t.point[0]-e.point[0],t.point[1]-e.point[1])<=n).sort((t,i)=>Math.hypot(t.point[0]-e.point[0],t.point[1]-e.point[1])-Math.hypot(i.point[0]-e.point[0],i.point[1]-e.point[1])||t.point[0]-i.point[0]||t.point[1]-i.point[1]);let i=t[0];if(!i){i={point:[...e.point],rays:[]},c.push(i);const[t,r]=ls(i.point,n),o=cs(t,r),s=h.get(o)||[];s.push(i),h.set(o,s)}const r=e.other[0]-e.point[0],o=e.other[1]-e.point[1],s=Math.hypot(r,o);if(!(s>n))continue;const a=[r/s,o/s];let l=Math.atan2(a[1],a[0]);l<0&&(l+=2*Math.PI),i.rays.push({u:a,halfDepth:e.halfDepth,length:s,angle:l})}const d=[],u=e=>{const t=1e-9*Math.max(1,r),i=e.filter(e=>Number.isFinite(e.halfDepth)&&e.halfDepth>0&&Number.isFinite(e.length)&&e.length>n);return i.filter((e,n)=>!i.some((i,r)=>r!==n&&i.halfDepth>=e.halfDepth-t&&i.length>=e.length-t&&(i.halfDepth>e.halfDepth+t||i.length>e.length+t||re.length-t.length||e.halfDepth-t.halfDepth).map(e=>({...e}))};for(const e of c){const t=e.rays.sort((e,t)=>e.angle-t.angle||e.length-t.length||e.halfDepth-t.halfDepth),i=[];for(const e of t){const t=i[i.length-1];t&&Math.abs(e.angle-t.angle)<=1e-9?t.supports.push({halfDepth:e.halfDepth,length:e.length}):i.push({u:[...e.u],angle:e.angle,supports:[{halfDepth:e.halfDepth,length:e.length}]})}if(i.length>1&&2*Math.PI-i[i.length-1].angle+i[0].angle<=1e-9){const e=i.pop();i[0].supports.push(...e.supports)}if(i.length<3)continue;const r=i.map(t=>{const i=u(t.supports),r=new Set,o=[];for(const s of i){const i=[e.point[0]+t.u[0]*s.length,e.point[1]+t.u[1]*s.length];for(const t of hs(l,i,n)){if("shared"!==t.kind||Math.hypot(t.point[0]-i[0],t.point[1]-i[1])>n)continue;const s=t.other[0]-t.point[0],a=t.other[1]-t.point[1],l=Math.hypot(s,a);if(!(l>n))continue;const c=s/l,h=a/l;if(Math.hypot(t.other[0]-e.point[0],t.other[1]-e.point[1])<=n)continue;const d=`${t.key}|${t.point[0]}|${t.point[1]}|${t.other[0]}|${t.other[1]}|${t.halfDepth}`;r.has(d)||(r.add(d),o.push({start:[t.point[0],t.point[1]],u:[c,h],length:l,halfDepth:t.halfDepth}))}}return o.sort((e,t)=>e.start[0]-t.start[0]||e.start[1]-t.start[1]||e.u[0]-t.u[0]||e.u[1]-t.u[1]||e.length-t.length||e.halfDepth-t.halfDepth),{u:[...t.u],halfDepth:Math.max(...i.map(e=>e.halfDepth)),length:Math.max(...i.map(e=>e.length)),supports:i,continuations:o}}).filter(e=>Number.isFinite(e.halfDepth)&&e.halfDepth>0&&Number.isFinite(e.length)&&e.length>n);if(r.length<3)continue;const o=Math.max(...r.map(e=>e.halfDepth));o>0&&Number.isFinite(o)&&d.push({point:[...e.point],rays:r,halfDepth:o,limit:1.25*o})}d.sort((e,t)=>e.point[0]-t.point[0]||e.point[1]-t.point[1]);const p=new Map;for(const e of d){const[t,i]=ls(e.point,n),r=cs(t,i),o=p.get(r)||[];o.push(e),p.set(r,o)}return{epsilon:n,coordinateScale:r,nodes:d,index:p}}function us(e){const t={fans:[],supports:[]};if(!e?.nodes?.length)return t;const i=Math.max(e.epsilon,1e-9)**2;for(const n of e.nodes){const e=n.rays.filter(e=>Number.isFinite(e.halfDepth)&&e.halfDepth>0).map(e=>({...e,thickLength:Math.max(...e.supports.filter(t=>t.halfDepth>=e.halfDepth-1e-12).map(e=>e.length),0),angle:(()=>{const t=Math.atan2(e.u[1],e.u[0]);return t<0?t+2*Math.PI:t})()})).sort((e,t)=>e.angle-t.angle);if(e.length<2)continue;const r=n.point;for(const i of e)for(const e of i.supports){if(!(e.halfDepth>0&&e.length>0))continue;const n=-i.u[1]*e.halfDepth,o=i.u[0]*e.halfDepth,s=[r[0]+i.u[0]*e.length,r[1]+i.u[1]*e.length];t.supports.push([[r[0]+n,r[1]+o],[s[0]+n,s[1]+o],[s[0]-n,s[1]-o],[r[0]-n,r[1]-o]])}for(let n=0;n{const e=s.angle-o.angle;return e>0?e:e+2*Math.PI})();if(a<1e-9)continue;const l=a>Math.PI+1e-9,c=4*Math.max(o.halfDepth,s.halfDepth),h=[r[0]-o.u[1]*o.halfDepth,r[1]+o.u[0]*o.halfDepth],d=[r[0]+s.u[1]*s.halfDepth,r[1]-s.u[0]*s.halfDepth],u=o.u[0]*s.u[1]-o.u[1]*s.u[0],p=e=>{let t=Math.atan2(e[1]-r[1],e[0]-r[0])-o.angle;for(;t<0;)t+=2*Math.PI;return t<=a+1e-9};let _=null;if(Math.abs(u)>1e-9){const e=((d[0]-h[0])*s.u[1]-(d[1]-h[1])*s.u[0])/u,t=((d[0]-h[0])*o.u[1]-(d[1]-h[1])*o.u[0])/u,i=[h[0]+o.u[0]*e,h[1]+o.u[1]*e];(l?e<=1e-9&&t<=1e-9:e>1e-9&&e<=o.thickLength&&t<=s.thickLength)&&Math.hypot(i[0]-r[0],i[1]-r[1])<=c&&p(i)&&(_=i)}const m=e=>{Math.abs(Xo(e))>i&&t.fans.push(e)};if(_){const e=1.5*Math.max(o.halfDepth,s.halfDepth);m(ho([r[0],r[1]],h,_,d,e)??[[r[0],r[1]],h,_,d]);continue}if(l){m([[r[0],r[1]],h,d]);continue}const g=(e,t)=>Math.min(t,Math.sqrt(Math.max(c**2-e**2,0)),2*Math.max(o.halfDepth,s.halfDepth)),f=[h[0]+o.u[0]*g(o.halfDepth,o.thickLength),h[1]+o.u[1]*g(o.halfDepth,o.thickLength)],v=[d[0]+s.u[0]*g(s.halfDepth,s.thickLength),d[1]+s.u[1]*g(s.halfDepth,s.thickLength)];m([[r[0],r[1]],h,f,v,d])}}return t}function ps(e,t){return!e||!Array.isArray(t)||t.length<2||!t.slice(0,2).every(Number.isFinite)?null:hs(e.index,t,e.epsilon).filter(i=>Math.hypot(i.point[0]-t[0],i.point[1]-t[1])<=e.epsilon).sort((e,i)=>Math.hypot(e.point[0]-t[0],e.point[1]-t[1])-Math.hypot(i.point[0]-t[0],i.point[1]-t[1])||e.point[0]-i.point[0]||e.point[1]-i.point[1])[0]||null}function _s(e,t,i,n,r,o,s){return ds(gs(e,t,i,n,r,o,s),4*Zo(n,s),s)}function ms(e,t,i,n,r,o,s,a=1){const l=ss(e,t,n,r,a,i);if(!l)return null;const c=function(e,t,i){const n=(e||[]).find(e=>e?.id===t),r=Zt(n);if(!r)return[];const o=[];for(const n of e||[]){if(!n||n.id===t)continue;const e=Zt(n);if(e)for(const t of bn(r,e,i))o.push(t)}return o}(e,t,Zo(r,a)),h=function(e,t,i,n,r){const o=Zo(n,r),s=[];for(let a=0;abs(h,d,e[0],e[1],e[2],e[3])<=o);s.push(u?"shared":"outer")}return s}(l.poly,c,n,r,a),d=as(i,l,r,a),u=d.map((e,t)=>h[t]&&e>0?vo(e,o,s)/2:0);return{...l,kinds:h,cms:d,offsets:u}}function gs(e,t,i,n,r,o,s=1){const a=[];for(const l of e||[]){if(!l?.id)continue;const c=ms(e,l.id,t,i,n,r,o,s);if(c)for(let e=0;e0&&(a=No(a,e.a,e.b,e.cm,n,s));return a}function vs(e,t,i,n,r,o,s=1){if(!t?.length)return[];const a=gs(e,t,i,n,r,o,s),l=new Map;for(const e of a){if(e.open||!e.kind||!e.roomId)continue;const t=l.get(e.key)||new Set;t.add(e.roomId),l.set(e.key,t)}const c=e=>{const t=[...l.get(e)||[]].sort();return 1!==t.length&&2!==t.length?`ambiguous:${e}`:`${1===t.length?"outer":"shared"}:${t.join("|")}`},h=[],d=new Set;for(const e of a)!e.open&&e.cm>0&&!d.has(e.key)&&(d.add(e.key),h.push({...e,ownerSignature:c(e.key)}));const u=[];for(const a of e||[]){if(!a?.id)continue;const l=ms(e,a.id,t,i,n,r,o,s);if(l)for(let e=0;e0)||null===l.kinds[i]){e++;continue}const o=c($o(l.poly[i],l.poly[(i+1)%l.poly.length],n,s));let a=e;for(;a+10&&u.push({a:[d[0],d[1]],b:[p[0],p[1]],key:$o(d,p,n,s),cm:r,len:_,ownerSignature:o}),e=a+1}}}u.sort((e,t)=>t.len-e.len||e.key.localeCompare(t.key));const p=[],_=new Set,m=new Set,g=4*Zo(n,s);for(const e of u){const t=h.filter(t=>!m.has(t.key)&&t.cm===e.cm&&t.ownerSignature===e.ownerSignature&&Co(Ro(t.a,t.b),Ro(e.a,e.b))&&bs(t.a[0],t.a[1],e.a[0],e.a[1],e.b[0],e.b[1])<=g&&bs(t.b[0],t.b[1],e.a[0],e.a[1],e.b[0],e.b[1])<=g);if(t.length){for(const e of t)m.add(e.key);_.has(e.key)||(_.add(e.key),p.push(Mo(e.a,e.b,e.cm,n,s)))}}for(const e of h)m.has(e.key)||_.has(e.key)||(_.add(e.key),p.push(Mo(e.a,e.b,e.cm,n,s)));return p}function ys(e,t,i,n,r,o,s,a=1){const l=Zo(r,a),c=(n[0]+n[2])/2,h=(n[1]+n[3])/2,d=Ro([n[0],n[1]],[n[2],n[3]]);let u=null;for(const n of gs(e,t,i,r,o,s,a)){if(!Co(Ro(n.a,n.b),d))continue;const e=bs(c,h,n.a[0],n.a[1],n.b[0],n.b[1]);e>4*l||(!u||e[e[0],e[1]]),o=Math.abs(Xo(r));r.length>=3&&o>i&&(t=r,i=o)}return t}function ks(e,t,i,n,r,o,s,a=1,l,c){const h=(e||[]).find(e=>e?.id===t),d=Zt(h);if(!d||d.length<3)return null;if(!i?.length)return d.map(e=>[e[0],e[1]]);const u=ms(e,t,i,n,r,o,s,a);if(!u||!u.offsets.some(e=>e>0))return d.map(e=>[e[0],e[1]]);const p=c||_s(e,i,n,r,o,s,a),_=ns(u.poly,u.offsets,p);if(!_)return d.map(e=>[e[0],e[1]]);if(!p.nodes.length)return _;const m=void 0===l?Ns(e,i,n,[],r,o,s,a):null,g=l??("ok"===m?.status||"degraded-extra"===m?.status?m.roomGeom:void 0);if(g)try{const e=ws(Ht(xs(u.poly),g));if(e)return e}catch{}return function(e,t){try{return ws(Nt(xs(e),xs(t)))}catch{return null}}(_,u.poly)||d.map(e=>[e[0],e[1]])}function xs(e){const t=e.map(e=>[e[0],e[1]]);return t.push([e[0][0],e[0][1]]),[t]}function $s(e){return!(!Array.isArray(e)||!e.length)&&e.every(e=>Array.isArray(e)&&e.length&&e.every(e=>Array.isArray(e)&&e.length>=4&&e.every(e=>Array.isArray(e)&&e.length>=2&&Number.isFinite(e[0])&&Number.isFinite(e[1])))&&Math.abs(yi(e[0]))>1e-9)}function Ss(e,t,i,n){const r=n?new Set(n):null;let o=null;for(let n=0;nt.epsilon))continue;const l=Ds([[e.point[0]+a[0]*n.halfDepth,e.point[1]+a[1]*n.halfDepth],[e.point[0]+s.u[0]*r+a[0]*n.halfDepth,e.point[1]+s.u[1]*r+a[1]*n.halfDepth],[e.point[0]+s.u[0]*r-a[0]*n.halfDepth,e.point[1]+s.u[1]*r-a[1]*n.halfDepth],[e.point[0]-a[0]*n.halfDepth,e.point[1]-a[1]*n.halfDepth]],t.coordinateScale);if(!l)continue;const c=xs(l);o=o?Et(o,c):c}}return o}function Ms(e,t,i){let n=null;for(const r of e.rays)for(const e of r.continuations){const r=[-e.u[1],e.u[0]],o=[e.start[0]+e.u[0]*e.length,e.start[1]+e.u[1]*e.length],s=Ds([[e.start[0]+r[0]*e.halfDepth,e.start[1]+r[1]*e.halfDepth],[o[0]+r[0]*e.halfDepth,o[1]+r[1]*e.halfDepth],[o[0]-r[0]*e.halfDepth,o[1]-r[1]*e.halfDepth],[e.start[0]-r[0]*e.halfDepth,e.start[1]-r[1]*e.halfDepth]],t.coordinateScale);if(!s)continue;const a=Nt(xs(s),i);Array.isArray(a)&&0!==a.length&&(n=n?Et(n,a):a)}return n}function Ts(e,t,i=2*(4*e.halfDepth+2*t.epsilon)){const n=function(e,t=uo){const i=new Set,n=Number.isFinite(t)&&t>=0?t:uo;for(let t=0;te-t)}(e);return n.length?Ss(e,t,i,n):null}function Rs(e,t,i,n,r){const o=function(e){let t=null;for(const i of e){const e=xs(i);try{t=t?Et(t,e):e}catch{}}return t}(function(e,t,i=!1){if(!e)return[];const n=[];for(const r of e.nodes)for(let o=0;o1e-9)||h>=Math.PI-1e-9)continue;const d=[-s.u[1],s.u[0]],u=[-a.u[1],a.u[0]],p=[r.point[0]+d[0]*s.halfDepth,r.point[1]+d[1]*s.halfDepth],_=[r.point[0]-u[0]*a.halfDepth,r.point[1]-u[1]*a.halfDepth],m=es(p,s.u,_,a.u);if(!m)continue;const g=Math.hypot(m[0]-r.point[0],m[1]-r.point[1]);if(!Number.isFinite(g)||g<=r.limit)continue;const f=t?Math.sqrt(Math.max(0,r.limit*r.limit-s.halfDepth*s.halfDepth)):0,v=t?Math.sqrt(Math.max(0,r.limit*r.limit-a.halfDepth*a.halfDepth)):0,y=Ds([[p[0]+s.u[0]*f,p[1]+s.u[1]*f],[_[0]+a.u[0]*v,_[1]+a.u[1]*v],m],e.coordinateScale);if(y&&n.push(y),i){const t=m[0]-r.point[0],i=m[1]-r.point[1],o=Math.hypot(t,i),s=g-r.limit;if(o>e.epsilon&&s>e.epsilon){const a=Math.min(Math.max(8*e.epsilon,.05*r.halfDepth),.25*s),l=t/o,c=i/o,h=-c,d=l,u=Ds([[m[0]-l*a+h*a,m[1]-c*a+d*a],[m[0]-l*a-h*a,m[1]-c*a-d*a],[m[0]+l*a,m[1]+c*a]],e.coordinateScale);u&&n.push(u)}}}return n}({...t,nodes:[e]},i,n));return o&&r?Ht(o,r):o}function Cs(e,t,i,n){if(!e||!t.nodes.length)return e;let r=null;try{r=function(e){let t=null;for(const i of e.nodes){const n=Ts(i,e);n&&(t=t?Et(t,n):n)}return t}(t)}catch{return e}let o=e;for(const e of t.nodes){const s=4*e.halfDepth+2*t.epsilon,a=2*s,l=[[e.point[0]-s,e.point[1]-s],[e.point[0]+s,e.point[1]-s],[e.point[0]+s,e.point[1]+s],[e.point[0]-s,e.point[1]+s]];try{let s=o;const c=Rs(e,t,!1,!0,r);c&&(s=Ht(s,c));let h=Ss(e,t,a);const d=Rs(e,t,!0,!0,r);d&&(h=Ht(h,d)),r&&(h=Et(h,r));const u=.02*Math.min(...e.rays.map(e=>e.halfDepth));if(h=Et(h,xs([[e.point[0]-u,e.point[1]-u],[e.point[0]+u,e.point[1]-u],[e.point[0]+u,e.point[1]+u],[e.point[0]-u,e.point[1]+u]])),!h)continue;let p=Nt(h,xs(l));n?p=Nt(p,n):i&&(p=Nt(p,i));const _=xs(l),m=Ht(s,_),g=i?Ht(Nt(s,_),i):null,f=Ms(e,t,_);o=Et(m,...g?[g]:[],...f?[f]:[],p)}catch{}}if(r)try{let e=r;n?e=Nt(e,n):i&&(e=Nt(e,i)),o=Et(o,e)}catch{}return o}function Ds(e,t=1){if(!Array.isArray(e)||e.length<3)return null;const i=Number.isFinite(t)&&t>0?t:1,n=1e-12*Math.max(1,i),r=[];for(const t of e){if(!Array.isArray(t)||t.length<2)return null;const e=Number(t[0]),i=Number(t[1]);if(!Number.isFinite(e)||!Number.isFinite(i))return null;const o=Math.round(e/n)*n,s=Math.round(i/n)*n;if(!Number.isFinite(o)||!Number.isFinite(s))return null;r.push([Object.is(o,-0)?0:o,Object.is(s,-0)?0:s])}return Math.abs(Xo(r))>n*n?r:null}function As(e,t,i,n){if(bs(e[0],e[1],t[0],t[1],i[0],i[1])>n)return!1;const r=i[0]-t[0],o=i[1]-t[1],s=(e[0]-t[0])*r+(e[1]-t[1])*o,a=r*r+o*o,l=n*Math.sqrt(a);return s>=-l&&s<=a+l}function Os(e,t,i){const n=[],r=[];for(let o=0;oi*i))continue;const d=i/Math.sqrt(h),u=[0,1];for(const e of t)for(const t of[e.a,e.b]){if(!As(t,s,a,i))continue;const e=((t[0]-s[0])*l+(t[1]-s[1])*c)/h;e>d&&e<1-d&&u.push(e)}u.sort((e,t)=>e-t);const p=u.filter((e,t)=>0===t||Math.abs(e-u[t-1])>d);for(let e=0;e=3&&r.length===n.length?{poly:n,offsets:r}:null}function zs(e,t,i,n,r,o,s,a){const l=(e||[]).map(Zt).filter(e=>!!e&&e.length>=3);if(!l.length)return null;let c=Et(xs(l[0]));for(let e=1;e"outer"===e.kind&&e.half>0),u=4*Zo(n,s),p=a||ds(h,u,s);let _=null;for(const e of function(e){const t=[];for(const i of Array.isArray(e)?e:[])if(Array.isArray(i))for(const e of i){if(!Array.isArray(e)||e.length<4)continue;const i=e.slice(0,-1).map(e=>[e[0],e[1]]);i.length>=3&&t.push(i)}return t}(c)){const t=Os(e,d,u);if(!t||!t.offsets.some(e=>e>0))continue;const i=qs(t.poly,t.offsets,p),n=ns(t.poly,t.offsets,p);if(!i||!n)continue;const r=Ht(xs(i),xs(n));_=_?Et(_,r):r}return{centre:c,shell:_||[]}}function Ps(e,t,i,n,r,o,s=1){try{const a=zs(e,t,i,n,r,o,s,_s(e,t,i,n,r,o,s));if(!a)return[];return a.shell?.length?Et(a.centre,a.shell):a.centre}catch{return null}}function Fs(e){if(!e)return"";let t="";for(const i of e)if(Array.isArray(i))for(const e of i){if(!Array.isArray(e)||e.length<4)continue;const i=e.slice(0,e.length-1);i.length<3||(t+=(t?" ":"")+rs(i.map(e=>[e[0],e[1]])))}return t}const Is=15;function Es(e,t,i){const n=e?.length||0;if(n<3||t?.length!==n)return!1;const r=e[(i-1+n)%n],o=e[i],s=e[(i+1)%n],a=[r[0]-o[0],r[1]-o[1]],l=[s[0]-o[0],s[1]-o[1]],c=Math.hypot(a[0],a[1]),h=Math.hypot(l[0],l[1]);if(!(c>1e-9&&h>1e-9))return!1;const d=Math.max(-1,Math.min(1,(a[0]*l[0]+a[1]*l[1])/(c*h))),u=Math.acos(d);if(!(u>1e-9))return!1;if(u>=Is*Math.PI/180)return!1;const p=Math.max(Math.max(0,t[(i-1+n)%n]),Math.max(0,t[i]));if(!(p>0))return!1;const _=p/Math.tan(u/2);return Number.isFinite(_)&&_>0&&_e>0))continue;for(const e of l.offsets)e>0&&(u=Math.max(u,2*e));const c=qs(l.poly,l.offsets,d),p=ns(l.poly,l.offsets,d);c&&h.push({outset:c,inset:p})}for(const e of l){const t=e.map(e=>e[0]),i=e.map(e=>e[1]);if(t.length){const n=Math.min(Math.max(...t)-Math.min(...t),Math.max(...i)-Math.min(...i)),r=Math.min(...e.map((t,i)=>{const n=e[(i+1)%e.length];return Math.hypot(n[0]-t[0],n[1]-t[1])}));u=Math.max(u,e.length>16?n:r)}}const p=function(e,t,i,n,r,o,s,a){if(!t?.length||!i?.length)return[];const l=4*Zo(n,s),c=new Map;for(const a of gs(e,t,i,n,r,o,s))!a.open&&a.half>0&&!c.has(a.key)&&c.set(a.key,a);const h=[...c.values()];if(h.length<2)return[];const d=a||ds(h,l),u=[];for(const e of i)for(const t of[[e[0],e[1]],[e[2],e[3]]])u.some(e=>Math.hypot(e[0]-t[0],e[1]-t[1])<=l)||u.push(t);const p=[],_=(e,t)=>{let i=0,n=0;if(Math.hypot(e.a[0]-t[0],e.a[1]-t[1])<=l)i=e.b[0]-e.a[0],n=e.b[1]-e.a[1];else{if(!(Math.hypot(e.b[0]-t[0],e.b[1]-t[1])<=l))return null;i=e.a[0]-e.b[0],n=e.a[1]-e.b[1]}const r=Math.hypot(i,n);return r>l?[i/r,n/r]:null};for(const e of u){const t=h.map(t=>({iv:t,u:_(t,e)})).filter(e=>!!e.u);for(let i=0;i0?[e.slice(),u,m,_]:[e.slice(),_,m,u];else{if(!f)continue;{const t=[-r.u[1],r.u[0]],i=[-o.u[1],o.u[0]],n=s<0?1:-1,a=[e[0]+t[0]*r.iv.half*n,e[1]+t[1]*r.iv.half*n],l=[e[0]-i[0]*o.iv.half*n,e[1]-i[1]*o.iv.half*n];y=s>0?[e.slice(),a,l]:[e.slice(),l,a]}}Math.abs(Xo(y))>l*l&&p.push(y)}}return p}(e,t,i,r,o,s,a,d),_=n.length?Bs(e,t,i,r,o,s,a):null;let m="exterior";try{const v=zs(e,t,i,r,o,s,a,d);m="paper";const y=v?v.shell?.length?Et(v.centre,v.shell):v.centre:[],b=e=>{const t=xs(e.outset);if(!e.inset)return t;const i=xs(e.inset);return Ht(t,i)};m="room-rings";let w=null;for(const e of h)try{const t=b(e);w=w?Et(w,t):t}catch{}if(m="edge-bodies",v)for(const n of js(e,t,i,r,o,s,a))try{const e=Nt(xs(n.quad),v.centre);w=w?Et(w,e):e}catch{}m="junctions",w=function(e,t,i=1,n=Et){let r=e;for(const e of t||[]){const t=Ds(e,i);if(t)try{const e=xs(t);r=r?n(r,e):e}catch{}}return r}(w,p,a),m="facade-clip",w&&v&&(w=Nt(w,v.centre)),m="exterior-shell";const k=[];let x=0;if(v?.shell?.length)if(w)try{const e=Et(w,v.shell);if(!$s(e))throw new Error("invalid shell union");w=e}catch{if(!$s(w)||!$s(v.shell))throw new Error("invalid shell");k.push({id:"exterior-shell",geom:v.shell}),x++}else w=v.shell;if(m="multi-wall-trim",w&&d.nodes.length){const e=e=>e.rays.some(e=>e.supports.some(e=>e.length<2*e.halfDepth)),t=d.nodes.filter(e);if(t.length){w=Cs(w,{...d,nodes:t},v?.centre,y)}}if(m="junction-corners",d.nodes.length){const n=us(d),l=function(e,t,i,n,r,o,s,a){try{const l=zs(e,t,i,n,r,o,s,{epsilon:a.epsilon,coordinateScale:a.coordinateScale,nodes:[],index:new Map});return l?l.shell?.length?Et(l.centre,l.shell):l.centre:null}catch{return null}}(e,t,i,r,o,s,a,d);for(const e of n.fans)try{let t=[xs(e)];if(l&&(t=Nt(t,l)),!t?.length)continue;w=w?Et(w,t):t}catch{}g=w,f=Math.max(d.epsilon,1e-9)**2,w=Array.isArray(g)?g.map(e=>{if(!Array.isArray(e)||!e.length)return e;const[t,...i]=e;return Math.abs(Xo(t||[]))<=f?null:[t,...i.filter(e=>Math.abs(Xo(e||[]))>f)]}).filter(e=>!!e):g}const $=w||[],S=[...$s($)?[{id:"room-primary",geom:$}]:[],...k.map((e,t)=>({id:`room-isolated-${t}`,geom:e.geom}))];m="openings";for(const e of n){if(!(e.length>0))continue;const t=Gs(_,e,!0);if(!t.negative&&!t.positive)continue;const i=e.angle*Math.PI/180,n=Math.cos(i),o=Math.sin(i),s=-o,l=n,c=e.length/2,h=1.25*Math.max(u,r*a),d=[[e.x-n*c-s*h,e.y-o*c-l*h],[e.x+n*c-s*h,e.y+o*c-l*h],[e.x+n*c+s*h,e.y+o*c+l*h],[e.x-n*c+s*h,e.y-o*c+l*h]];w&&(w=Ht(w,xs(d)));for(const e of k)e.geom=Ht(e.geom,xs(d))}m="extras";const M=[];let T=0;const R=c.mergeExtra||((e,t)=>e?Et(e,t):t);for(let e=0;ee.length>=2&&Number.isFinite(e[0])&&Number.isFinite(e[1]))||Math.abs(yi(t))<=1e-9){T++;continue}const i=[xs(t)];try{const t=R(w,i,e);if(!$s(t))throw new Error("invalid extra union");w=t}catch{T++,$s(i)&&M.push({id:`extra-${e}`,geom:i})}}M.push(...k),M.sort((e,t)=>Fs(e.geom).localeCompare(Fs(t.geom)));const C=w||[];return{status:T||x?"degraded-extra":"ok",geom:C,components:[...$s(C)?[{id:"primary",geom:C}]:[],...M.map((e,t)=>({...e,id:`isolated-${t}`}))],roomGeom:$,roomComponents:S,paperGeom:y,depthUnits:u,openingPadUnits:1.25*Math.max(u,r*a),openingIndex:_,multiWallNodes:d,degradedExtraCount:T+x}}catch{return c.onCoreFailure?.(m),{status:"failed-core",geom:[],components:[],roomGeom:[],paperGeom:[],roomComponents:[],depthUnits:u,openingIndex:null,multiWallNodes:d,degradedExtraCount:0}}var g,f}function Hs(e,t,i,n=[],r,o,s,a=1,l=[],c={}){if(!t?.length&&!l.length)return null;return Ls(Ns(e,t,i,n,r,o,s,a,l,c))}function Ls(e){if("failed-core"===e.status||"not-applicable"===e.status)return null;const t=e.components.map(e=>({id:e.id,d:Fs(e.geom),fillRule:"evenodd"})).filter(e=>!!e.d),i=t[0]?.d||"",n=Fs(e.paperGeom);if(t.length){const r={status:e.status,d:i,paths:t,components:e.components,roomGeom:e.roomGeom,multiWallNodes:e.multiWallNodes,paperD:n,depthUnits:e.depthUnits,fillRule:"evenodd"};return Object.defineProperties(r,{roomComponents:{value:e.roomComponents||[],enumerable:!1},openingIndex:{value:e.openingIndex,enumerable:!1},openingPadUnits:{value:e.openingPadUnits,enumerable:!1}}),r}return null}function js(e,t,i,n,r,o,s=1){if(!t?.length)return[];const a=new Set,l=[];for(const c of e||[]){if(!c?.id)continue;const h=ms(e,c.id,t,i,n,r,o,s);if(!h)continue;const d=h.poly;for(let e=0;e0))continue;const p=$o(t,i,n,s);if(a.has(p))continue;a.add(p);const _=vo(u,r,o),[m,g]=Jo(d,e),f=-m,v=-g,y=_/2,b=[[t[0]+f*y,t[1]+v*y],[i[0]+f*y,i[1]+v*y],[i[0]+m*y,i[1]+g*y],[t[0]+m*y,t[1]+g*y]];l.push({key:p,kind:c,cm:u,quad:b,a:[t[0],t[1]],b:[i[0],i[1]],depthUnits:_})}}return l}function qs(e,t,i){const n=e?.length||0;if(n<3||t.length!==n)return null;if(t.every(e=>!(e>0)))return e.map(e=>[e[0],e[1]]);os(e),t.slice().reverse();const r=[];for(let o=0;o0||l>0)){r.push([e[o][0],e[o][1]]);continue}if(a>0!=l>0){const t=e[o],i=a>0?[t[0]-c*a,t[1]-h*a]:[t[0],t[1]],n=l>0?[t[0]-d*l,t[1]-u*l]:[t[0],t[1]];r.push(i),Math.hypot(n[0]-i[0],n[1]-i[1])>1e-9&&r.push(n);continue}if(Qo(w,k)){const t=e[o],i=[t[0]-c*a,t[1]-h*a],n=[t[0]-d*l,t[1]-u*l];r.push(i),Math.hypot(n[0]-i[0],n[1]-i[1])>1e-9&&r.push(n);continue}const S=es(x,w,$,k),M=Math.max(a,l,1e-9),T=ps(i,e[o])?.limit??4*M;if(S){const i=Math.hypot(S[0]-e[o][0],S[1]-e[o][1]);if(Number.isFinite(i)&&i<=T){r.push(S);continue}if(Es(e,t,o)){r.push([e[o][0],e[o][1]]);continue}}a>0&&r.push([e[o][0]-c*a,e[o][1]-h*a]),l>0&&r.push([e[o][0]-d*l,e[o][1]-u*l])}return r.length>=3?r:null}function Bs(e,t,i,n,r,o,s=1){const a=[];for(const l of e||[]){if(!l?.id)continue;const c=ms(e,l.id,t,i,n,r,o,s);if(!c)continue;const h=Math.abs(yi(c.poly));for(let e=0;e[Math.max(t,e.x0),Math.min(i,e.x1)]).filter(e=>e[1]-e[0]>n).sort((e,t)=>e[0]-t[0]||e[1]-t[1]);if(!r.length)return{coverage:0,full:!1};let o=r[0][0],s=r[0][1],a=0,l=o<=t+n;for(let e=1;e=i-n,{coverage:a,full:l}}function Us(e,t){return Number(t.full)-Number(e.full)||e.faceDistance-t.faceDistance||e.area-t.area||e.roomId.localeCompare(t.roomId)}function Gs(e,t,i=!1){const n=Number(t?.x),r=Number(t?.y),o=Number(t?.angle),s=Number(t?.length);if(!([n,r,o,s].every(Number.isFinite)&&s>0))return{negative:null,positive:null};const a=o*Math.PI/180,l=Math.cos(a),c=Math.sin(a),h=-c,d=l,u=s/2,p=Math.max(1e-9,e.adjacencyEps),_=new Map;let m=0;for(const t of e.edges){if(i&&!(t.half>0))continue;if(!zo(t.a,t.b,o))continue;const[e,s]=ko(t.a,t.b);if(Math.abs((n-t.a[0])*s-(r-t.a[1])*e)>p)continue;const a=(t.a[0]-n)*l+(t.a[1]-r)*c,g=(t.b[0]-n)*l+(t.b[1]-r)*c,f=Math.max(-u,Math.min(a,g)),v=Math.min(u,Math.max(a,g));if(v-f<=p)continue;const y=t.inward[0]*h+t.inward[1]*d>=0?1:-1,b=((t.a[0]+t.b[0])/2-n)*h+((t.a[1]+t.b[1])/2-r)*d,w=Math.abs(b+y*t.half),k=`${y}|${t.roomId}`,x={x0:f,x1:v,half:t.half,cm:t.cm,key:t.key,axis:[e,s]},$=_.get(k);$?($.pieces.push(x),$.faceDistance=Math.min($.faceDistance,w)):_.set(k,{roomId:t.roomId,side:y,order:m++,pieces:[x],faceDistance:w,area:t.area,coverage:0,full:!1})}for(const e of _.values()){const t=Ws(e.pieces,-u,u,p);e.coverage=t.coverage,e.full=t.full}const g=e=>{const t=[..._.values()].filter(t=>t.side===e&&t.coverage>p);return t.sort(Us),t[0]||null};return{negative:g(-1),positive:g(1)}}function Vs(e,t){const i=Gs(e,t),n=[i.negative,i.positive].filter(e=>!!e);if(!n.length)return{ox:0,oy:0,cm:0,side:-1};const r=i.negative&&i.positive?-1:n[0].side,o=t.flip_v?-r:r,s=(-1===o?i.negative:i.positive)||n[0],a=[...s.pieces].sort((e,t)=>(e.x0<=0&&e.x1>=0?0:Math.min(Math.abs(e.x0),Math.abs(e.x1)))-(t.x0<=0&&t.x1>=0?0:Math.min(Math.abs(t.x0),Math.abs(t.x1)))||t.x1-t.x0-(e.x1-e.x0)||e.key.localeCompare(t.key))[0];if(!(a.half>0&&a.cm>0))return{ox:0,oy:0,cm:0,side:o};const l=t.angle*Math.PI/180,c=-Math.sin(l),h=Math.cos(l);return{ox:c*o*a.half,oy:h*o*a.half,cm:a.cm,side:o}}function Ks(e,t){const i=1e-9,n=t.filter(e=>Number.isFinite(e.x0)&&Number.isFinite(e.x1)&&Number.isFinite(e.half)&&e.x1>e.x0&&e.half>0);if(!n.length)return"";const r=n.flatMap(e=>[e.x0,e.x1]).sort((e,t)=>e-t),o=[];for(const e of r){const t=o[o.length-1];(void 0===t||e>t+i)&&o.push(e)}const s=[];for(let e=0;e+1t+i))continue;const a=(t+r)/2,l=n.reduce((e,t)=>a>=t.x0-i&&a<=t.x1+i?Math.max(e,t.half):e,0);if(!(l>0))continue;const c=s[s.length-1];c&&t<=c.x1+i&&Math.abs(l-c.half)<=i?c.x1=r:s.push({x0:t,x1:r,half:l})}const a=[];for(const e of s){const t=a[a.length-1],n=t?.[t.length-1];n&&e.x0<=n.x1+i?(e.x0=n.x1,t.push(e)):a.push([e])}return a.map(t=>{const i=t[0],n=t[t.length-1],r=Math.min(.25*Math.min(...t.map(e=>e.half)),.75),o=-e*r,s=[];if(1===e){s.push(`M ${i.x0} ${o} L ${n.x1} ${o}`);for(let e=t.length-1;e>=0;e--){const i=t[e];s.push(`L ${i.x1} ${i.half} L ${i.x0} ${i.half}`)}}else{s.push(`M ${n.x1} ${o} L ${i.x0} ${o}`);for(const e of t)s.push(`L ${e.x0} ${-e.half} L ${e.x1} ${-e.half}`)}return s.push("Z"),s.join(" ")}).join(" ")}function Ys(e,t,i,n){if(!n)return i;const r=e.angle*Math.PI/180,o=Math.cos(r),s=Math.sin(r),a=[],l=1e-9;for(const r of i){const[i,c]=r.axis,h=o*i+s*c;if(Math.abs(h)<=l)continue;const d=e.x*i+e.y*c,u=d+h*r.x0,p=d+h*r.x1,_=Math.min(u,p),m=Math.max(u,p),g=`${r.key}|${t}`,f=n.get(g)||[];let v=[[_,m]];for(const[e,t]of f){const i=[];for(const[n,r]of v)t<=n+l||e>=r-l?i.push([n,r]):(e>n+l&&i.push([n,Math.min(r,e)]),te[0]-t[0]||e[1]-t[1]),b=[];for(const e of y){const t=b[b.length-1];t&&e[0]<=t[1]+l?t[1]=Math.max(t[1],e[1]):b.push([e[0],e[1]])}n.set(g,b)}return a}function Zs(e,t){const i=new Map;return t.map(t=>function(e,t,i){const n=Gs(e,t,!0),r=n.negative,o=n.positive;if(!r&&!o)return null;let s;if(r&&o)s=[{candidate:r,side:-1},{candidate:o,side:1}];else{const e=r||o;s=[{candidate:e,side:-1},{candidate:e,side:1}]}const a=s.map(({candidate:e,side:n})=>({candidate:e,side:n,pieces:Ys(t,n,e.pieces,i)})),l=a.map(({candidate:e,side:t,pieces:i})=>({side:t,roomId:e.roomId,d:Ks(t,i)})),c=a.flatMap(({pieces:e})=>e);if(!c.length)return null;const h=Math.max(...c.map(e=>e.half));return{faces:l,minY:-h,maxY:h,wallKey:[...new Set(c.map(e=>e.key))].sort().join("|")}}(e,t,i))}const Xs=150,Js=1e-6;function Qs(e){return Number.isFinite(e)?Math.max(1,Math.min(Xs,e)):1}function ea(e){return((Number.isFinite(Number(e))?Number(e):0)%90+90)%90}const ta=e=>{const t=e.map(e=>[e[0],e[1]]);return!t.length||t[0][0]===t[t.length-1][0]&&t[0][1]===t[t.length-1][1]||t.push([...t[0]]),[t]},ia=(e,t)=>e[0]===t[0]&&e[1]===t[1];function na(e,t=1e-6){const i=Number.isFinite(t)&&t>0?t:Js,n=[];for(const t of e||[]){if(!Array.isArray(t)||t.length<2)return null;const e=Number(t[0]),r=Number(t[1]);if(!Number.isFinite(e)||!Number.isFinite(r))return null;const o=Math.round(e/i)*i,s=Math.round(r/i)*i;if(!Number.isFinite(o)||!Number.isFinite(s))return null;const a=[Object.is(o,-0)?0:o,Object.is(s,-0)?0:s];n.length&&ia(n[n.length-1],a)||n.push(a)}return n.length>1&&ia(n[0],n[n.length-1])&&n.pop(),n.length<3||new Set(n.map(e=>`${e[0]},${e[1]}`)).size<3?null:yi(n)>i*i?n:null}function ra(e){const t=[];for(const i of e||[])for(const e of i||[]){const i=(e||[]).filter(e=>Array.isArray(e)&&e.length>=2);i.length<4||t.push(`M ${i.slice(0,-1).map(e=>`${e[0]} ${e[1]}`).join(" L ")} Z`)}return t.join(" ")}function oa(e,t,i,n,r){return jo({a:e,b:t,halfDepth:vo(i,n,r)/2})}function sa(e,t,i){const n=Number(t)>0?Number(t):5,r=Qs(e.cm)/n*i,o=e.center[0],s=e.center[1];if("circle"===e.shape){const e=r/2;return Array.from({length:96},(t,i)=>{const n=i/96*Math.PI*2;return[o+Math.cos(n)*e,s+Math.sin(n)*e]})}const a=r/2,l=ea(e.angle)*Math.PI/180,c=Math.cos(l),h=Math.sin(l);return[[-a,-a],[a,-a],[a,a],[-a,a]].map(([e,t])=>[o+e*c-t*h,s+e*h+t*c])}function aa(e,t,i=1e-9){if(!t.length)return[e];let n=[ta(e)];try{for(const e of t){const t=e.b[0]-e.a[0],r=e.b[1]-e.a[1],o=Math.hypot(t,r);if(!(o>i))continue;const s=t/o,a=r/o,l=-a,c=s,h=1.25*Math.max(Number(e.depth)||0,4*i),d=Math.max(2*i,1e-9*o),u=[[e.a[0]-s*d-l*h,e.a[1]-a*d-c*h],[e.b[0]+s*d-l*h,e.b[1]+a*d-c*h],[e.b[0]+s*d+l*h,e.b[1]+a*d+c*h],[e.a[0]-s*d+l*h,e.a[1]-a*d+c*h]];n=Ht(n,ta(u))}return fa(n)}catch{return[e]}}function la(e,t){try{const i=Ht([[...e.map(e=>[e[0],e[1]]),[e[0][0],e[0][1]]]],[[...t.map(e=>[e[0],e[1]]),[t[0][0],t[0][1]]]]);let n=null,r=0;for(const e of i||[]){const t=e?.[0]||[],i=Math.abs(yi(t));t.length>=4&&i>r&&(r=i,n=t.slice(0,-1).map(e=>[e[0],e[1]]))}return n}catch{return null}}function ca(e,t,i,n=Math.max(2e-4*i,1e-9),r=[]){const o=[],s=[],a=new Map;for(const e of r){const t=a.get(e.hostId)||[];t.push(e),a.set(e.hostId,t)}const l=[],c=[],h=[];for(const n of e.room_drafts||[])for(let e=0;e+10))continue;const a={a:n.points[e],b:n.points[e+1],halfDepth:s},c=jo(a);c&&(o.push(a),l.push(c))}const d=[];for(const n of e.partitions||[]){const e={a:n.a,b:n.b,halfDepth:vo(n.cm,t,i)/2};if(!(e.halfDepth>0))continue;const r=jo(e);r&&(s.push(e),c.push(r),d.push({id:n.id,body:r}))}const u=[...o,...s];for(const{segmentIndex:e,wedge:t}of Go(u,n)){const i=e=o.length&&(d[i.at].body=n))}for(const e of d)h.push(...aa(e.body,a.get(e.id)||[],n));const p=(e.wall_columns||[]).map(e=>sa(e,t,i)),_=Vo([...o,...s],n).flatMap(e=>aa(e,r,n));return{drafts:l,partitions:c,columns:p,patches:_,all:[...l,...h,..._,...p]}}function ha(e){try{const t=e.map(e=>na(e)).filter(e=>!!e).map(e=>ta(e));return t.length?Et(t[0],...t.slice(1)):null}catch{return null}}const da=e=>`M ${e.map(e=>`${e[0]} ${e[1]}`).join(" L ")} Z`;function ua(e){const t=[];for(const i of e||[]){if(ma([i])<=1e-6)continue;const e=[];for(const t of i||[]){const i=(t||[]).filter(e=>Array.isArray(e)&&e.length>=2);i.length<4||e.push(da(i.slice(0,-1)))}e.length&&t.push(e.join(" "))}return t}function pa(e,t,i={}){const n=ha(e.filter(e=>e.length>=3)),r=t.some(e=>!na(e))?null:ha(t);if(!n)return[];if(r)try{return ua(Nt(n,r))}catch{}return function(e,t,i){let n=null;for(let r=0;r=4&&t.push(e.slice(0,-1).map(e=>[e[0],e[1]]));return t}function fa(e){const t=[];for(const i of e||[]){const e=i?.[0];e?.length>=4&&t.push(e.slice(0,-1).map(e=>[e[0],e[1]]))}return t}function va(e,t){let i=!1;for(let n=0,r=t.length-1;ne[1]!=l>e[1]&&e[0]<(a-o)*(e[1]-s)/(l-s||1e-12)+o&&(i=!i)}return i}function ya(e,t,i){return function(e,t){for(const i of t||[]){const t=i?.[0];if(!t?.length||!va(e,t))continue;let n=!1;for(let t=1;tva(e,t))}function ba(e,t,i){if(Math.hypot(e.center[0]-t.center[0],e.center[1]-t.center[1])>i)return!1;if(Math.abs(Qs(e.cm)-Qs(t.cm))>1e-6)return!1;if(e.shape!==t.shape)return!0;if("circle"===e.shape||"circle"===t.shape)return!0;const n=Math.abs(ea(e.angle)-ea(t.angle));return Math.min(n,90-n)<=1e-6}const wa=2.54;function ka(e){const t="number"==typeof e?e:NaN;return!Number.isFinite(t)||t<=0||5===t?1:5/t}function xa(e,t){return e*ka(t)}function $a(e){return e?wa:1}function Sa(e,t){if(!t)return String(e);const i=e/wa;return String(Math.round(1e6*i)/1e6)}function Ma(e,t){return t?e*wa:e}const Ta=1e3;function Ra(e,t){const i=Number(e),n=Number.isFinite(i)&&i>0?i:1,r=n>=1?t:t*n,o=n>=1?t/n:t;return{x:(t-r)/2,y:(t-o)/2,w:r,h:o}}const Ca=.01,Da=100;function Aa(e,t=1e3){const i=Ra(e?.plan_aspect,t),n=Number(e?.plan_scale),r=Number.isFinite(n)&&n>0?Math.min(Da,Math.max(Ca,n)):1,o=Number(e?.plan_scale_x),s=Number(e?.plan_scale_y),a=Number.isFinite(o)&&o>0?Math.min(Da,Math.max(Ca,o)):r,l=Number.isFinite(s)&&s>0?Math.min(Da,Math.max(Ca,s)):r,c=Number(e?.plan_x),h=Number(e?.plan_y),d=_r(e?.plan_angle);return{x:i.x+(Number.isFinite(c)?qa(c):0)*t,y:i.y+(Number.isFinite(h)?qa(h):0)*t,w:i.w*a,h:i.h*l,...d?{angle:d}:{}}}function Oa(e){if(null==e.x||null==e.y)return{x:e.x,y:e.y,w:e.w,h:e.h};const t=Number(e.w)||0,i=Number(e.h)||0;return{x:t<0?e.x+t:e.x,y:i<0?e.y+i:e.y,w:Math.abs(t),h:Math.abs(i)}}function za(e){return e&&Array.isArray(e.spaces)?e.spaces.map(e=>{const t=Ta,i=function(e){return Array.isArray(e)&&4===e.length&&e.every(e=>Number.isFinite(e))&&e[2]>1e-6&&e[3]>1e-6?e:[0,0,1,1]}(e.view_box);return{id:e.id,title:e.title,cellCm:Number.isFinite(Number(e.cell_cm))&&Number(e.cell_cm)>0?Number(e.cell_cm):5,vb:[i[0]*Ta,i[1]*t,i[2]*Ta,i[3]*t],bg:e.plan_url?{href:Mn(e.plan_url),...Aa(e,Ta)}:null,rooms:(e.rooms||[]).map(e=>{const i={...e,...Oa(e)};return{id:i.id,name:i.name,area:i.area??null,open_to:i.open_to||void 0,settings:i.settings||void 0,x:null!=i.x?i.x*Ta:void 0,y:null!=i.y?i.y*t:void 0,w:null!=i.w?i.w*Ta:void 0,h:null!=i.h?i.h*t:void 0,poly:i.poly?i.poly.map(e=>[e[0]*Ta,e[1]*t]):void 0,wall_ids:Array.isArray(i.wall_ids)?[...i.wall_ids]:void 0}}),wall_segments:(e.wall_segments||[]).map(e=>({...e,id:String(e.id),a:[Number(e.a?.[0])*Ta,Number(e.a?.[1])*t],b:[Number(e.b?.[0])*Ta,Number(e.b?.[1])*t],cm:Number(e.cm)})),room_drafts:(e.room_drafts||[]).map(e=>({id:e.id,points:(e.points||[]).map(e=>[e[0]*Ta,e[1]*t]),segments:(e.segments||[]).map(e=>({..."string"==typeof e.id&&e.id?{id:e.id}:{},cm:Number(e.cm)}))})),partitions:(e.partitions||[]).map(e=>({id:e.id,a:[e.a[0]*Ta,e.a[1]*t],b:[e.b[0]*Ta,e.b[1]*t],cm:Number(e.cm)})),wall_columns:(e.wall_columns||[]).map(e=>({id:e.id,shape:"circle"===e.shape?"circle":"square",center:[e.center[0]*Ta,e.center[1]*t],cm:Number(e.cm),..."circle"===e.shape?{}:{angle:ea(e.angle)}}))}}):[]}const Pa=5e3,Fa=Pa*Ta,Ia=240,Ea=Ta/Ia,Na=1/Ia;function Ha(e){if(!Number.isFinite(e))return e;const t=Math.round(e*Ia/Ta)*Ta/Ia;return Math.abs(t-e)<=1e-9*Ea?e:t}function La(e){return{x:Ha(e.x),y:Ha(e.y)}}function ja(e){return Number.isFinite(e)?Math.min(Fa,Math.max(-Fa,e)):0}function qa(e){return Number.isFinite(e)?Math.min(Pa,Math.max(-Pa,e)):0}const Ba=1/3,Wa=200;function Ua(e){let t=1/0,i=1/0,n=-1/0,r=-1/0;for(const o of e){const e=Number(o[0]),s=Number(o[1]);Number.isFinite(e)&&Number.isFinite(s)&&(en&&(n=e),s>r&&(r=s))}return t>n?null:{minX:t,minY:i,maxX:n,maxY:r}}function Ga(e){return e.poly&&e.poly.length?Ua(e.poly):null==e.x||null==e.y?null:Ua([[e.x,e.y],[e.x+(e.w||0),e.y+(e.h||0)]])}function Va(e,t){const i=[];for(const t of e.rooms||[]){const e=Ga(t);e&&i.push(e)}if(e.bg){const t=Ua(wr(e.bg));t&&i.push(t)}for(const e of t||[])if(Array.isArray(e)){const t=Ua([e]);t&&i.push(t)}else i.push(e);return i}const Ka=e=>{if(!e.length)return 0;const t=[...e].sort((e,t)=>e-t),i=t.length>>1;return t.length%2?t[i]:(t[i-1]+t[i])/2},Ya=e=>{let t=1/0,i=1/0,n=-1/0,r=-1/0;for(const o of e)o.minXn&&(n=o.maxX),o.maxY>r&&(r=o.maxY);return t>n||i>r?null:{x:t,y:i,w:n-t,h:r-i}};function Za(e,t){let{x:i,y:n,w:r,h:o}=e;r<30&&(i=i+r/2-100,r=Wa),o<30&&(n=n+o/2-100,o=Wa);const s=Math.max(r,o)*t;return{x:i-s,y:n-s,w:r+2*s,h:o+2*s}}function Xa(e,t={}){const i=t.pad??.05,n=t.k??10,r=t.minSpread??50,o=e.filter(e=>Number.isFinite(e.minX)&&Number.isFinite(e.minY)&&Number.isFinite(e.maxX)&&Number.isFinite(e.maxY)&&Math.abs(e.minX)<=Fa&&Math.abs(e.maxX)<=Fa&&Math.abs(e.minY)<=Fa&&Math.abs(e.maxY)<=Fa);if(!o.length)return{core:null,all:null,outliers:0};const s=Ya(o);if(o.length<4){const e=Za(s,i);return{core:e,all:e,outliers:0}}const a=o.map(e=>(e.minX+e.maxX)/2),l=o.map(e=>(e.minY+e.maxY)/2),c=Ka(a),h=Ka(l),d=o.map((e,t)=>Math.max(Math.abs(a[t]-c),Math.abs(l[t]-h))),u=Math.max(((e,t)=>e.length?e[Math.min(e.length-1,Math.max(0,Math.round(t*(e.length-1))))]:0)([...d].sort((e,t)=>e-t),.75),r),p=d.map(e=>e>n*u),_=p.filter(Boolean).length,m=_&&_<=o.length*Ba?o.filter((e,t)=>!p[t]):o;return{core:Za(Ya(m)||s,i),all:Za(s,i),outliers:m===o?0:_}}function Ja(e,t,i=.05){const n=Xa(Va(e,t),{pad:i});if(n.core)return n.core;const r=e.vb&&4===e.vb.length&&e.vb[2]>0&&e.vb[3]>0?e.vb:[0,0,Ta,Ta];return{x:r[0],y:r[1],w:r[2],h:r[3]}}function Qa(e){const t=Ja(e);return{x:t.x+t.w/2,y:t.y+t.h/2}}function el(e){const t=[];for(const i of e.rooms||[]){const e=Ga(i);e&&t.push(e)}const i=Xa(t,{pad:0}).core,n=Ta*ka(e.cellCm??5);return i?Math.max(n,Math.min(Fa,Math.max(i.w,i.h))):n}function tl(e,t,i,n=1){const r=Number(i),o=Number.isFinite(n)&&n>0?n:1;return!Number.isFinite(r)||r<=0?e*o:e*el(t)*o/r}const il=[1,2,5,10,20,50,100,200,500,1e3];function nl(e,t,i=7){if(!(e>0&&t>0&&Number.isFinite(t)))return null;const n=il.find(n=>e*n*t>=i);if(void 0===n)return null;const r=il.find(e=>e>=5*n)??5*n;return{fine:n,coarse:r}}function rl(e){if(e.poly&&e.poly.length){const t=e.poly.map(e=>e[0]),i=e.poly.map(e=>e[1]),n=Math.min(...t),r=Math.min(...i);return{x:n,y:r,w:Math.max(...t)-n,h:Math.max(...i)-r}}return{x:e.x??0,y:e.y??0,w:e.w??0,h:e.h??0}}function ol(e,t,i,n){const r=i["rl_"+(e.id||"")];if(r&&r.s===t)return{x:r.x*Ta,y:r.y*Ta};const o=function(e){if(e.poly){const t=e.poly.length;return[e.poly.reduce((e,t)=>e+t[0],0)/t,e.poly.reduce((e,t)=>e+t[1],0)/t]}return[e.x+e.w/2,e.y+.1*Math.min(e.w,e.h)]}(e);return La({x:o[0],y:o[1]})}const sl=["furniture","appliance","sanitary","other"],al=[{id:"sofa",group:"furniture",w:220,h:90,g:[["r",0,0,1,1],["l",.09,.26,.91,.26],["l",.09,.26,.09,1],["l",.91,.26,.91,1],["l",.5,.26,.5,1]]},{id:"armchair",group:"furniture",w:90,h:85,g:[["r",0,0,1,1],["l",.14,.28,.86,.28],["l",.14,.28,.14,1],["l",.86,.28,.86,1]]},{id:"coffee_table",group:"furniture",w:110,h:60,g:[["r",0,0,1,1],["r",.08,.14,.84,.72]]},{id:"table_dining",group:"furniture",w:140,h:80,g:[["r",0,0,1,1],["r",.06,.11,.88,.78]]},{id:"table_round",group:"furniture",w:120,h:120,g:[["e",.5,.5,.5,.5],["e",.5,.5,.41,.41]]},{id:"chair",group:"furniture",w:45,h:45,g:[["r",0,0,1,.18],["r",.06,.18,.88,.8]]},{id:"desk",group:"furniture",w:120,h:60,g:[["r",0,0,1,1],["r",.63,.07,.31,.86],["l",.63,.5,.94,.5]]},{id:"bed_double",group:"furniture",w:160,h:200,g:[["r",0,0,1,1],["r",0,0,1,.07],["r",.06,.1,.4,.15],["r",.54,.1,.4,.15],["l",0,.33,1,.33]]},{id:"bed_single",group:"furniture",w:90,h:200,g:[["r",0,0,1,1],["r",0,0,1,.07],["r",.15,.1,.7,.15],["l",0,.33,1,.33]]},{id:"nightstand",group:"furniture",w:45,h:40,g:[["r",0,0,1,1],["r",.12,.14,.76,.33],["r",.12,.53,.76,.33]]},{id:"wardrobe",group:"furniture",w:100,h:60,g:[["r",0,0,1,1],["l",0,.72,1,.72],["l",.5,.72,.5,1]]},{id:"bookshelf",group:"furniture",w:80,h:30,g:[["r",0,0,1,1],["l",.34,0,.34,1],["l",.67,0,.67,1]]},{id:"fridge",group:"appliance",w:60,h:65,g:[["r",0,0,1,1],["l",0,.36,1,.36],["l",.83,.44,.83,.64]]},{id:"stove",group:"appliance",w:60,h:60,g:[["r",0,0,1,1],["e",.29,.31,.15,.15],["e",.71,.31,.15,.15],["e",.29,.71,.15,.15],["e",.71,.71,.15,.15]]},{id:"dishwasher",group:"appliance",w:60,h:60,g:[["r",0,0,1,1],["r",.1,.12,.8,.76],["e",.5,.5,.27,.27],["e",.5,.5,.13,.13]]},{id:"washer",group:"appliance",w:60,h:60,g:[["r",0,0,1,1],["l",.08,.17,.92,.17],["e",.5,.57,.3,.3],["e",.5,.57,.14,.14]]},{id:"dryer",group:"appliance",w:60,h:60,g:[["r",0,0,1,1],["l",.08,.17,.92,.17],["e",.5,.57,.3,.3],["p",.36,.5,.5,.64,.64,.5]]},{id:"tv",group:"appliance",w:120,h:30,g:[["r",0,0,1,.42],["l",.5,.42,.5,.72],["l",.3,.72,.7,.72]]},{id:"ac",group:"appliance",w:90,h:25,g:[["r",0,0,1,1],["l",.05,.55,.95,.55],["l",.05,.79,.95,.79]]},{id:"water_heater",group:"appliance",w:45,h:45,g:[["e",.5,.5,.5,.5],["e",.5,.5,.31,.31]]},{id:"toilet",group:"sanitary",w:40,h:70,g:[["r",.06,0,.88,.2],["e",.5,.58,.37,.35],["e",.5,.58,.22,.2]]},{id:"bathtub",group:"sanitary",w:170,h:75,g:[["r",0,0,1,1],["r",.05,.11,.77,.78],["e",.89,.5,.045,.1]]},{id:"shower",group:"sanitary",w:90,h:90,g:[["r",0,0,1,1],["l",0,0,1,1],["l",1,0,0,1],["e",.5,.5,.08,.08]]},{id:"sink",group:"sanitary",w:60,h:45,g:[["r",0,0,1,1],["e",.5,.6,.34,.3],["e",.5,.15,.07,.07]]},{id:"kitchen_sink",group:"sanitary",w:80,h:60,g:[["r",0,0,1,1],["r",.06,.24,.44,.64],["r",.54,.24,.4,.64],["e",.5,.12,.06,.06]]},{id:"bidet",group:"sanitary",w:40,h:55,g:[["e",.5,.5,.44,.5],["e",.5,.5,.26,.3]]},{id:"stairs",group:"other",w:100,h:280,g:[["r",0,0,1,1],["l",0,.111,1,.111],["l",0,.222,1,.222],["l",0,.333,1,.333],["l",0,.444,1,.444],["l",0,.556,1,.556],["l",0,.667,1,.667],["l",0,.778,1,.778],["l",0,.889,1,.889],["l",.5,.93,.5,.06],["p",.38,.16,.5,.06,.62,.16]]},{id:"fireplace",group:"other",w:120,h:40,g:[["r",0,0,1,1],["p",.22,1,.22,.42,.78,.42,.78,1]]},{id:"plant",group:"other",w:40,h:40,g:[["e",.5,.5,.22,.22],["l",.5,.28,.5,.02],["l",.5,.72,.5,.98],["l",.28,.5,.02,.5],["l",.72,.5,.98,.5],["l",.34,.34,.13,.13],["l",.66,.66,.87,.87],["l",.66,.34,.87,.13],["l",.34,.66,.13,.87]]},{id:"rug",group:"other",w:200,h:140,g:[["r",0,0,1,1],["r",.06,.09,.88,.82]]}],ll=new Map(al.map(e=>[e.id,e]));function cl(e){return e&&ll.get(e)||null}function hl(e){return al.filter(t=>t.group===e)}function dl(e){const t=cl(e);return t?{w:t.w,h:t.h}:{w:60,h:60}}function ul(e,t,i=Ea,n=1e3){const r=Number(t)>0?Number(t):5;return(Number(e)||0)/r*i/n}const pl=5e-4,_l=Pa;function ml(e){return Number.isFinite(e)?Math.max(pl,Math.min(_l,e)):pl}function gl(e){return Number.isFinite(e)?Math.max(1,Math.min(1e4,e)):1}const fl=e=>{const t=Math.round(1e3*e)/1e3;return Object.is(t,-0)?"0":String(t)};function vl(e,t,i){const n=cl(e);if(!(n&&t>0&&i>0))return"";const r=e=>fl(e*t),o=e=>fl(e*i),s=[];for(const e of n.g)if("r"===e[0]){const[,t,i,n,a]=e;s.push(`M${r(t)} ${o(i)}H${r(t+n)}V${o(i+a)}H${r(t)}Z`)}else if("l"===e[0]){const[,t,i,n,a]=e;s.push(`M${r(t)} ${o(i)}L${r(n)} ${o(a)}`)}else if("e"===e[0]){const[,n,a,l,c]=e;s.push(`M${r(n-l)} ${o(a)}A${fl(l*t)} ${fl(c*i)} 0 0 1 ${r(n+l)} ${o(a)}A${fl(l*t)} ${fl(c*i)} 0 0 1 ${r(n-l)} ${o(a)}Z`)}else{const t=e.slice(1);if(t.length<4)continue;let i=`M${r(t[0])} ${o(t[1])}`;for(let e=2;e+1{let t=(e%360+360)%360;return t>180&&(t-=360),t};function bl(e,t,i,n,r,o=0){let s=null,a=r;for(const r of n){const[n,l,c,h]=r,d=c-n,u=h-l,p=d*d+u*u;if(!p)continue;const _=Math.sqrt(p);let m=((e-n)*d+(t-l)*u)/p;m=Math.max(0,Math.min(1,m));let g=n+m*d,f=l+m*u;const v=Math.hypot(e-g,t-f);if(!(v0){const e=Math.round(m*_/o)*o;g=n+e/_*d,f=l+e/_*u}s={cx:g+y*(i/2),cy:f+b*(i/2),angle:yl(180*Math.atan2(-y,b)/Math.PI),dist:v}}return s}const wl=15e3,kl=2e3,xl=new WeakMap;function $l(e,t){let i=xl.get(e);if(!i){const t=new Set;i={hiddenAt:"hidden"===e.visibilityState?Date.now():0,token:0,subscribers:t,onVisibility:()=>{const t=xl.get(e);if(!t)return;const i=Date.now();if("hidden"===e.visibilityState){t.hiddenAt||(t.hiddenAt=i);const e={kind:"hidden",token:t.token,at:i,hiddenFor:0,long:!1};for(const i of[...t.subscribers])i(e);return}const n=t.hiddenAt?Math.max(0,i-t.hiddenAt):0;t.hiddenAt=0,t.token++;const r={kind:"visible",token:t.token,at:i,hiddenFor:n,long:n>=wl};for(const e of[...t.subscribers])e(r)},onPageShow:t=>{const i=xl.get(e);if(!i)return;const n=Date.now();i.token++;const r={kind:"pageshow",token:i.token,at:n,hiddenFor:0,long:!!t.persisted,persisted:!!t.persisted};for(const e of[...i.subscribers])e(r)}},xl.set(e,i),e.addEventListener("visibilitychange",i.onVisibility),e.defaultView?.addEventListener("pageshow",i.onPageShow)}return i.subscribers.add(t),()=>{const i=xl.get(e);i&&(i.subscribers.delete(t),i.subscribers.size||(e.removeEventListener("visibilitychange",i.onVisibility),e.defaultView?.removeEventListener("pageshow",i.onPageShow),xl.delete(e)))}}const Sl={now:()=>Date.now(),setTimeout:(e,t)=>window.setTimeout(e,t),clearTimeout:e=>window.clearTimeout(e),requestAnimationFrame:e=>window.requestAnimationFrame(e),cancelAnimationFrame:e=>window.cancelAnimationFrame(e)};class Ml{constructor(e,t=Sl){this.onChange=e,this.clock=t,this._state="steady",this._token=0,this._frameFingerprint="",this._hasCompleteFrame=!1,this._overlayPhase="none",this._recoveryReason=null,this._overlayTimer=0,this._overlayRaf=0,this._overlayOpaqueAt=0,this._barrierRafs=new Set,this._trace=[],this._disposed=!1}get state(){return this._state}get token(){return this._token}get frameFingerprint(){return this._frameFingerprint}get hasCompleteFrame(){return this._hasCompleteFrame}get overlayPhase(){return this._overlayPhase}get overlayVisible(){return"none"!==this._overlayPhase}get overlayBlocksInteraction(){return this.overlayVisible}get recoveryReason(){return this._recoveryReason}get trace(){return this._trace.map(e=>({...e,...e.stage?{stage:[...e.stage]}:{}}))}note(e,t={}){this.record(e,void 0,t)}record(e,t,i={}){this._trace.push({at:this.clock.now(),token:this._token,event:e,state:this._state,...t?{reason:t}:{},...i}),this._trace.length>80&&this._trace.splice(0,this._trace.length-80)}changed(){this._disposed||this.onChange()}adoptCompleteFrame(e){e&&(this._hasCompleteFrame=!0,this._frameFingerprint=e,this._state="steady",this._recoveryReason=null,this.clearOverlay(),this.record("frame-adopted"),this.changed())}markCompleteFrame(e){e&&(this._hasCompleteFrame=!0,this._frameFingerprint=e,this._state="steady",this._recoveryReason=null,this.clearOverlay(),this.record("frame-complete"),this.changed())}refreshCompleteFrame(e){!this._disposed&&this._hasCompleteFrame&&"steady"===this._state&&e&&e!==this._frameFingerprint&&(this._frameFingerprint=e,this.record("frame-refreshed"))}visibility(e){return"hidden"===e.kind?(this.record("visibility-hidden"),this._token):e.long||"visible"!==e.kind&&"pageshow"!==e.kind?this.beginCandidate("pageshow"===e.kind?"pageshow":"long-resume"):(this.record("pageshow"===e.kind?"pageshow-noop":"visibility-visible-quick"),this._token)}beginCandidate(e,t="plan"){return this._token++,this._recoveryReason=t,this._hasCompleteFrame?(this._state="connection"===t?"offline-stale":"holding",this.clearOverlay()):this.overlayVisible?this._state="overlay-visible":(this._state="overlay-pending",this.scheduleOverlay(t)),this.record("candidate-start",e),this.changed(),this._token}connectionLost(){return this.beginCandidate("connection-lost","connection")}candidateReady(e){return!this._disposed&&e===this._token&&(this._state="candidate-ready",this.record("candidate-ready"),this.changed(),!0)}async commitAfterPaint(e,t){if(this._disposed||e!==this._token)return!1;let i=!0;const n=(async()=>{for(await t.updateComplete();i&&!this._disposed&&e===this._token;)if(t.stageValid()&&t.assetsReady()){if(await this.nextFrame(),e!==this._token||!t.stageValid()||!t.assetsReady())continue;if(await this.nextFrame(),e===this._token&&t.stageValid()&&t.assetsReady())return!0}else await this.nextFrame();return!1})();let r=0;const o=new Promise(e=>{r=this.clock.setTimeout(()=>e({ready:!1,timedOut:!0}),kl)}),s=await Promise.race([n.then(e=>({ready:e,timedOut:!1})),o]);return i=!1,this.clock.clearTimeout(r),!this._disposed&&e===this._token&&(!(!s.ready&&!s.timedOut)&&(s.ready?(this._hasCompleteFrame=!0,this._frameFingerprint=t.frameFingerprint(),this.record("paint-barrier"),this.finishOverlayAfterCommit(),!0):(this.record("paint-barrier-timeout"),this._hasCompleteFrame?(this._state="connection"===this._recoveryReason?"offline-stale":"steady",this.clearOverlay()):(this._state="recovery-error","opaque"!==this._overlayPhase&&this.showOverlayNow()),this.changed(),!1)))}retry(e=this._recoveryReason||"plan"){return this.beginCandidate("retry",e)}nextFrame(){return new Promise(e=>{const t=this.clock.requestAnimationFrame(()=>{this._barrierRafs.delete(t),e()});this._barrierRafs.add(t)})}scheduleOverlay(e){this.clearOverlayTimer(),this._recoveryReason=e,this._overlayTimer=this.clock.setTimeout(()=>{this._overlayTimer=0,this._hasCompleteFrame||"overlay-pending"!==this._state&&"candidate-ready"!==this._state||(this._state="overlay-visible",this._overlayPhase="entering",this.record("overlay-enter"),this.changed(),this._overlayRaf=this.clock.requestAnimationFrame(()=>{this._overlayRaf=0,"entering"===this._overlayPhase&&(this._overlayPhase="fading-in",this.changed(),this._overlayTimer=this.clock.setTimeout(()=>{this._overlayTimer=0,"fading-in"===this._overlayPhase&&(this._overlayPhase="opaque",this._overlayOpaqueAt=this.clock.now(),this.record("overlay-opaque"),this.changed())},150))}))},150)}showOverlayNow(){this.clearOverlayTimer(),this._overlayPhase="opaque",this._overlayOpaqueAt=this.clock.now(),this.record("overlay-error")}finishOverlayAfterCommit(){if(this.clearOverlayTimer(),"none"===this._overlayPhase||"entering"===this._overlayPhase||"fading-in"===this._overlayPhase)return this.clearOverlay(),this._state="steady",this._recoveryReason=null,this.record("candidate-committed"),void this.changed();const e=Math.max(0,this._overlayOpaqueAt+250-this.clock.now());this._overlayTimer=this.clock.setTimeout(()=>{this._overlayTimer=0,this._overlayPhase="leaving",this.record("overlay-leave"),this.changed(),this._overlayTimer=this.clock.setTimeout(()=>{this._overlayTimer=0,this._overlayPhase="none",this._state="steady",this._recoveryReason=null,this.record("candidate-committed"),this.changed()},180)},e)}clearOverlayTimer(){this._overlayTimer&&this.clock.clearTimeout(this._overlayTimer),this._overlayTimer=0}clearOverlay(){this.clearOverlayTimer(),this._overlayRaf&&this.clock.cancelAnimationFrame(this._overlayRaf),this._overlayRaf=0,this._overlayPhase="none",this._overlayOpaqueAt=0}dispose(){this._disposed=!0,this._token++,this.clearOverlay();for(const e of this._barrierRafs)this.clock.cancelAnimationFrame(e);this._barrierRafs.clear()}}function Tl(e){let t=2166136261,i=2654435769;const n=new WeakSet,r=e=>{for(let n=0;n>>0)+(i>>>2)}},o=e=>{if(null===e)return void r("n");const t=typeof e;if("string"===t)return void r(`s${e.length}:${e}`);if("number"===t)return void r(`d${Number.isNaN(e)?"NaN":String(e)}`);if("boolean"===t)return void r(e?"t":"f");if("undefined"===t)return void r("u");if("object"!==t)return void r(`${t}:${String(e)}`);const i=e;if(n.has(i))r("[cycle]");else{if(n.add(i),Array.isArray(e)){r("[");for(const t of e)o(t);return r("]"),void n.delete(i)}r("{");for(const t of Object.keys(e).sort())r(t),o(e[t]);r("}"),n.delete(i)}};return o(e),`${(t>>>0).toString(16).padStart(8,"0")}${(i>>>0).toString(16).padStart(8,"0")}`}function Rl(e){return Tl(e)}const Cl=new WeakMap;class Dl{constructor(e,t=()=>Date.now()){this.onUpdate=e,this.now=t,this.fallbackAuthority={},this.referenceOwner={},this.referenced=new Set,this.queued=new Set,this.disposed=!1,this.sharedUpdate=()=>{this.disposed||this.onUpdate()}}bind(e){const t=((e,t)=>{const i=e?.connection||e?.callWS||t;return!i||"object"!=typeof i&&"function"!=typeof i?t:i})(e,this.fallbackAuthority);return this.shared&&this.authority===t||(this.shared&&(this.shared.listeners.delete(this.sharedUpdate),this.shared.references.delete(this.referenceOwner)),this.authority=t,this.shared=(e=>{let t=Cl.get(e);return t||(t={signed:{},queued:new Set,inFlight:new Map,retry:new Map,listeners:new Set,settlers:new Set,references:new Map},Cl.set(e,t)),t})(t),this.disposed||(this.shared.listeners.add(this.sharedUpdate),this.shared.references.set(this.referenceOwner,new Set(this.referenced)))),this.shared}start(e,t){this.disposed=!1,this.referenced=new Set(t());const i=this.bind(e());i.listeners.add(this.sharedUpdate),i.references.set(this.referenceOwner,new Set(this.referenced)),this.stopTimer(),this.resignTimer=setInterval(()=>this.resign(e(),t()),288e5)}dispose(){if(this.disposed=!0,this.stopTimer(),clearTimeout(this.batchTimer),this.shared){this.shared.listeners.delete(this.sharedUpdate),this.shared.references.delete(this.referenceOwner);for(const e of this.queued)this.shared.queued.delete(e)}this.queued.clear()}invalidate(e){const t=this.bind(e);clearTimeout(this.batchTimer),this.queued.clear(),t.queued.clear(),t.inFlight.clear(),t.retry.clear(),t.signed={}}stopTimer(){void 0!==this.resignTimer&&clearInterval(this.resignTimer),this.resignTimer=void 0}display(e,t){const i=Mn(t);if(!i.startsWith("/api/houseplan/content/"))return i;const n=this.bind(e);this.referenced.add(i),n.references.set(this.referenceOwner,new Set(this.referenced));const r=n.signed[i],o=r?this.now()-r.at:1/0;return o<$n?r.url:o{const t=[...this.queued];this.queued.clear();for(const e of t)i.queued.delete(e);this.sign(e,t)},30))}sign(e,t){if(!t.length||!e?.callWS)return;const i=this.bind(e);for(const n of function(e,t){const i=Math.max(1,Math.floor(t)),n=[];for(let t=0;t{const n=i.inFlight.get(e);return!(void 0!==n&&t-n<15e3)&&(i.inFlight.set(e,t),!0)});r.length&&e.callWS({type:"houseplan/content/sign",paths:r}).then(e=>{const t=this.now();let n=0,o=!1;for(const s of r){const r=e?.urls?.[s];if("string"==typeof r&&r){const e=i.signed[s];e?.loaded&&e.url!==r?(e.pending={url:r,at:t},this.preloadReplacement(i,s,r,t)):(i.signed[s]={url:r,at:t,loaded:e?.url===r&&!!e.loaded},o=!0),i.retry.delete(s),n++}else this.backOff(i,s)}if(n&&(this.trimShared(i),o))for(const e of[...i.listeners])e()}).catch(()=>{for(const e of r)this.backOff(i,e)}).finally(()=>{for(const e of r)i.inFlight.get(e)===t&&i.inFlight.delete(e);for(const e of[...i.settlers])e()})}}preloadReplacement(e,t,i,n){const r=()=>{const r=e.signed[t];if(r?.pending?.url===i){e.signed[t]={url:i,at:n,loaded:!0},e.retry.delete(t),this.trimShared(e);for(const t of[...e.listeners])t()}},o=()=>{const n=e.signed[t];n?.pending?.url===i&&(delete n.pending,this.backOff(e,t))};if("undefined"==typeof Image)return void r();const s=new Image;s.onload=()=>{("function"==typeof s.decode?s.decode():Promise.resolve()).then(r).catch(o)},s.onerror=o,s.src=i}preloadCurrentImage(e,t,i){if(i.loaded)return Promise.resolve(!0);if(i.preload)return i.preload;if("undefined"==typeof Image)return i.loaded=!0,Promise.resolve(!0);const n=i.url;return i.preload=new Promise(r=>{const o=new Image;let s=!1;const a=o=>{if(s)return;s=!0,clearTimeout(l);const a=e.signed[t];if(a===i&&delete a.preload,o&&a===i&&a.url===n){a.loaded=!0;for(const t of[...e.listeners])t();r(!0)}else o||a!==i||this.backOff(e,t),r(!1)};o.onload=()=>{("function"==typeof o.decode?o.decode():Promise.resolve()).then(()=>a(!0)).catch(()=>a(!1))},o.onerror=()=>a(!1);const l=setTimeout(()=>a(!1),kl);o.src=n}),i.preload}prepareImage(e,t){const i=Mn(t);if(!i.startsWith("/api/houseplan/content/"))return Promise.resolve(!0);const n=this.bind(e),r=()=>{const t=n.signed[i];if(t&&this.now()-t.atthis.now()&&!n.inFlight.has(i)&&!n.queued.has(i)?Promise.resolve(!1):null},o=r();return o?(s=o,new Promise(e=>{let t=!1;const i=i=>{t||(t=!0,clearTimeout(n),e(i))},n=setTimeout(()=>i(!1),kl);s.then(i).catch(()=>i(!1))})):new Promise(e=>{let t=!1,i=null;const o=i=>{t||(t=!0,clearTimeout(a),n.settlers.delete(s),e(i))},s=()=>{t||i||(i=r(),i&&i.then(o).catch(()=>o(!1)))},a=setTimeout(()=>o(!1),kl);n.settlers.add(s),s()});var s}backOff(e,t){const i=e.retry.get(t)?.delay||0,n=Math.min(6e4,i?2*i:2e3);e.retry.set(t,{notBefore:this.now()+n,delay:n})}trimShared(e){const t=Object.entries(e.signed);if(t.length<=512)return;const i=new Set(e.inFlight.keys());for(const t of e.references.values())for(const e of t)i.add(e);for(const[e,n]of t)(n.pending||n.preload)&&i.add(e);t.sort((e,t)=>t[1].at-e[1].at);const n=t.filter(([e])=>i.has(e));for(const e of t){if(n.length>=512)break;i.has(e[0])||n.push(e)}e.signed=Object.fromEntries(n)}resign(e,t){const i=this.bind(e);this.referenced=new Set(t),i.references.set(this.referenceOwner,new Set(this.referenced));const n=this.now(),r=new Set;for(const e of i.references.values())for(const t of e)r.add(t);const o={};for(const[e,t]of Object.entries(i.signed))r.has(e)&&n-t.at!!o[e]&&!o[e].pending))}markLoaded(e,t,i){const n=Mn(t);if(!n.startsWith("/api/houseplan/content/"))return;const r=this.bind(e).signed[n];if(r&&(!i||r.url===i)&&!r.loaded){r.loaded=!0;for(const t of[...this.bind(e).listeners])t()}}isReady(e,t){const i=Mn(t);if(!i.startsWith("/api/houseplan/content/"))return!0;const n=this.bind(e).signed[i];return!!n&&this.now()-n.ate.id===t)}var Ol="M11,18H13V16H11V18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,6A4,4 0 0,0 8,10H10A2,2 0 0,1 12,8A2,2 0 0,1 14,10C14,12 11,11.75 11,15H13C13,12.75 16,12.5 16,10A4,4 0 0,0 12,6Z";function zl(e,t,i){return[e[0]*t+e[1]*i+e[2],e[3]*t+e[4]*i+e[5]]}function Pl(e,t){let i=0;for(const[n,r]of t){const t=zl(e,n[0],n[1]);i=Math.max(i,Math.hypot(t[0]-r[0],t[1]-r[1]))}return i}const Fl=e=>{if(null==e||""===e)return null;const t=Number(e);return Number.isFinite(t)?t:null},Il=e=>{const t=e,i=Fl(Array.isArray(t)?t[0]:t?.x),n=Fl(Array.isArray(t)?t[1]:t?.y);return null==i||null==n?null:[i,n]};function El(e){if(!Array.isArray(e))return null;const t=[];for(const i of e){const e=Il(i);if(!e)return null;t.push(e)}if(t.length>1){const e=t[0],i=t[t.length-1];e[0]===i[0]&&e[1]===i[1]&&t.pop()}if(t.length<3)return null;let i=0,n=0,r=0;for(let e=0;ee+t[0],0)/t.length,t.reduce((e,t)=>e+t[1],0)/t.length]:[n/(3*i),r/(3*i)]}function Nl(e){if(!Array.isArray(e))return null;const t=[];for(const i of e){const e=Il(i);if(!e)return null;t.push(e)}if(t.length>1){const e=t[0],i=t[t.length-1];e[0]===i[0]&&e[1]===i[1]&&t.pop()}const i=El(t);if(!i)return null;const n=t.map(e=>e[0]),r=t.map(e=>e[1]);return{centroid:i,bbox:[Math.min(...n),Math.min(...r),Math.max(...n),Math.max(...r)]}}function Hl(e){let t=e;if(t&&"object"==typeof t&&!Array.isArray(t)){const e=t;t=e.path??e.points??t}if(!Array.isArray(t)||!t.length)return[];let i=(t.some(e=>(e=>Array.isArray(e)?e.length>=2&&!Array.isArray(e[0])&&("object"!=typeof e[0]||null==e[0]):!!e&&"object"==typeof e&&("x"in e||"y"in e))(e))?[t]:t.filter(Array.isArray)).map(e=>e.map(Il).filter(e=>null!=e)).filter(e=>e.length>=2);i.length>64&&(i=i.slice(-64));const n=i.reduce((e,t)=>e+t.length,0);if(n<=4e3)return i.map(e=>e.slice());const r=4e3-2*i.length,o=i.map(e=>e.length-2),s=o.reduce((e,t)=>e+t,0),a=o.map(e=>r*e/s),l=a.map(Math.floor);let c=r-l.reduce((e,t)=>e+t,0);const h=a.map((e,t)=>({index:t,fraction:e-Math.floor(e)})).sort((e,t)=>t.fraction-e.fraction||e.index-t.index);for(let e=0;efunction(e,t){if(t>=e.length)return e.slice();if(t<=2)return[e[0],e[e.length-1]];const i=[];for(let n=0;ne.slice()),i=t[t.length-1];return i?(i.length>2?i.pop():t.pop(),t):[]}function ql(e,t){return"default"!==e?e:null!=t?String(t):"default"}function Bl(e){if(!e)return null;const t=e.vacuum_position||e.robot_position||null,i=t&&null!=Fl(t.x)&&null!=Fl(t.y)?{x:Fl(t.x),y:Fl(t.y),a:Fl(t.a??t.angle??t.theta)}:null,n=Hl(e.path?.path??e.path?.points??e.path),r=[],o=e.rooms,s=Array.isArray(o)?o.map((e,t)=>[String(e?.id??t),e]):o&&"object"==typeof o?Object.entries(o):[];for(const[e,t]of s){if(!t||"object"!=typeof t)continue;const i=String(t.name??t.label??"").trim(),n=[[Fl(t.cx),Fl(t.cy)],[Fl(t.center?.x),Fl(t.center?.y)],[Fl(t.x),Fl(t.y)]].find(([e,t])=>null!=e&&null!=t);let o=n?.[0]??null,s=n?.[1]??null;const a=Nl(t.outline),l=Fl(t.x0),c=Fl(t.y0),h=Fl(t.x1),d=Fl(t.y1),u=(null!=l&&null!=c&&null!=h&&null!=d?[Math.min(l,h),Math.min(c,d),Math.max(l,h),Math.max(c,d)]:null)||a?.bbox||null;if(null!=o&&null!=s||!a||([o,s]=a.centroid),null!=o&&null!=s||!u||(o=(u[0]+u[2])/2,s=(u[1]+u[3])/2),i&&null!=o&&null!=s){const t={id:e,name:i,cx:o,cy:s};u&&([t.x0,t.y0,t.x1,t.y1]=u),r.push(t)}}const a=function(e){return String(e.map_name??e.current_map??e.map_index??e.selected_map??"default")}(e);return i||r.length||n.length?{pos:i,path:n,rooms:r,mapId:a}:null}function Wl(e,t,i){return![e,t,i].every(Number.isFinite)||e<0||t<=0||i<=0?Number.POSITIVE_INFINITY:e/t*i}const Ul=40;function Gl(e,t,i){if(!e||!t)return null;const n=t.attributes||{},r=e.split(".")[0]||"",o=null!=i?.platform?String(i.platform):null,s=!!Il(n.vacuum_position??n.robot_position),a=!(!n.rooms||"object"!=typeof n.rooms),l=Hl(n.path?.path??n.path?.points??n.path).length>0,c=[n.map_name,n.current_map,n.map_index,n.selected_map].some(e=>null!=e),h="xiaomi_cloud_map_extractor"===o,d=a||l||c;let u=null;if(s?u="compatible":h?u="known_xcme_incomplete":d?u="partial":"camera"===r&&(u="camera"),!u)return null;const p=s?"camera"===r?300:200:h?100:d?50:0;return{entityId:e,name:String(n.friendly_name||e),platform:o,category:u,hasPosition:s,hasRooms:a,hasPath:l,hasMapId:c,score:p}}const Vl=e=>e.toLowerCase().replace(/[\s_\-.,]+/g,"");function Kl(e,t){const i=new Set(t.map(Vl).filter(Boolean)),n=new Set;for(const t of e){const e=Vl(t.name||"");e&&i.has(e)&&n.add(e)}return n.size}function Yl(e,t){const i=new Map(t.map(e=>[Vl(e.name),e])),n=[],r=[];for(const t of e){const e=i.get(Vl(t.name));e&&(n.push([[t.cx,t.cy],[e.cx,e.cy]]),r.push(t.name))}if(n.length<3)return null;const o=function(e){if(e.length<3)return null;let t=0,i=0,n=0,r=0,o=0,s=0,a=0,l=0,c=0,h=0,d=0,u=0;for(const[[p,_],[m,g]]of e){if(![p,_,m,g].every(Number.isFinite))return null;t+=p*p,i+=p*_,n+=p,r+=_*_,o+=_,s+=1,a+=p*m,l+=_*m,c+=m,h+=p*g,d+=_*g,u+=g}const p=[t,i,n,i,r,o,n,o,s],_=e=>{const[t,i,n,r,o,s,a,l,c]=p,h=t*(o*c-s*l)-i*(r*c-s*a)+n*(r*l-o*a);if(!Number.isFinite(h)||Math.abs(h)<1e-9)return null;const d=[(o*c-s*l)/h,(n*l-i*c)/h,(i*s-n*o)/h,(s*a-r*c)/h,(t*c-n*a)/h,(n*r-t*s)/h,(r*l-o*a)/h,(i*a-t*l)/h,(t*o-i*r)/h];return[d[0]*e[0]+d[1]*e[1]+d[2]*e[2],d[3]*e[0]+d[4]*e[1]+d[5]*e[2],d[6]*e[0]+d[7]*e[1]+d[8]*e[2]]},m=_([a,l,c]),g=_([h,d,u]);if(!m||!g)return null;const f=[m[0],m[1],m[2],g[0],g[1],g[2]];return f.every(Number.isFinite)?f:null}(n);return o?{matrix:o,matched:r,residual:Pl(o,n)}:null}function Zl(e,t,i){const n=e[e.length-1];if(n&&n[0]===t[0]&&n[1]===t[1])return e;if(e.push(t),e.length<=600)return e;let r=function(e,t){if(e.length<3)return e.slice();const i=new Uint8Array(e.length);i[0]=i[e.length-1]=1;const n=[[0,e.length-1]];for(;n.length;){const[r,o]=n.pop(),[s,a]=e[r],[l,c]=e[o],h=l-s,d=c-a,u=Math.hypot(h,d)||1e-9;let p=0,_=-1;for(let t=r+1;tp&&(p=i,_=t)}_>0&&p>t&&(i[_]=1,n.push([r,_],[_,o]))}const r=[];for(let t=0;t600&&(r=r.filter((e,t)=>t%2==0||t===r.length-1)),r}function Xl(e){return"cleaning"===e||"returning"===e||"on"===e}const Jl={0:[1,0],90:[0,1],180:[-1,0],270:[0,-1]};function Ql(e){const[t,i]=Jl[e.rot]||[1,0],n=e.mir?-1:1;return[e.s*t*n,-e.s*i,e.ox,e.s*i*n,e.s*t,e.oy]}function ec(e){const t=e[0]*e[4]-e[1]*e[3];if(!Number.isFinite(t)||Math.abs(t)<1e-12)return null;const i=t<0,n=Math.sqrt(Math.abs(t));let r=180*Math.atan2(-e[1],e[4])/Math.PI;return r=(90*Math.round(r/90)%360+360)%360,{ox:e[2],oy:e[5],s:n,rot:r,mir:i}}function tc(e,t){const i=[],n=[];for(const t of e)null!=t.x0?(i.push(t.x0,t.x1),n.push(t.y0,t.y1)):(i.push(t.cx),n.push(t.cy));if(!i.length)return{ox:t[0]+t[2]/2,oy:t[1]+t[3]/2,s:t[2]/1e4,rot:0,mir:!0};const r=Math.min(...i),o=Math.max(...i),s=Math.min(...n),a=Math.max(...n),l=Math.max(o-r,a-s)||1,c={ox:0,oy:0,s:.6*Math.min(t[2],t[3])/l,rot:0,mir:!0},h=Ql(c),[d,u]=zl(h,(r+o)/2,(s+a)/2);return c.ox=t[0]+t[2]/2-d,c.oy=t[1]+t[3]/2-u,c}function ic(e,t,i,n){const[r,o]=zl(Ql(t),i,n),s=Ql({...e,ox:0,oy:0}),[a,l]=zl(s,i,n);return{...e,ox:r-a,oy:o-l}}function nc(e){const t=e?.trail_mode;return"never"===t||"cleaning"===t||"always"===t?t:!1===e?.trail?"never":"cleaning"}const rc={availability:"available",status:"alarm",activity:"none"},oc=new Set(["motion","vibration","sound"]),sc=new Set(["occupancy","presence"]),ac=new Set(["door","window","garage_door","opening"]),lc=new Set(["running","power"]),cc=new Set(["smoke","gas","carbon_monoxide","moisture","safety","tamper","problem"]);const hc=new Set(["running","working","washing","rinsing","spinning","drying","heating","cooling","cleaning","cooking","playing","recording","pumping","irrigating","humidifying","dehumidifying","fan","preheating","defrosting"]),dc=new Set([...hc,"start","started","run","active","in_progress","wash","rinse","spin","dry"]),uc=new Set(["off","idle","paused","standby","docked","finished","complete","completed","stopped","ready","sleeping","stop","end","done","inactive"]),pc=new Set(["heat","cool","heat_cool","auto","dry","fan_only"]),_c=e=>""===e||"unknown"===e||"unavailable"===e||"__missing__"===e,mc=e=>String(e??"").trim().toLowerCase(),gc=e=>mc(e).replace(/[\s-]+/g,"_").replace(/_+/g,"_").replace(/^_|_$/g,""),fc=new Map([["run_state",0],["job_state",0],["operation_state",0],["activity_state",0],["machine_state",1],["running_state",1],["status",2],["device_status",2],["machine_status",2]]),vc=new Set(["wifi","connection","signal","battery"]);function yc(e,t){const i=e?.entities?.[t]||{};if("config"===i.entity_category)return null;const n=String(t||"").split(".").slice(1).join("."),r=[i.translation_key,i.original_name,i.name],o=[...r,n];r.some(e=>mc(e))||o.push(e?.states?.[t]?.attributes?.friendly_name);const s=o.map(gc).filter(Boolean);if(s.some(e=>e.split("_").some(e=>vc.has(e))))return null;let a=null;for(const e of s)for(const[t,i]of fc)(e===t||e.endsWith(`_${t}`))&&(a=null==a?i:Math.min(a,i));return a}const bc=(e,t)=>null!=yc(e,t);function wc(e,t){if(!t.startsWith("switch."))return!1;const i=e?.entities?.[t]||{},n=e?.states?.[t],r=t.slice(7).toLowerCase();if(/(?:^|_)(?:main_)?power$/.test(r))return!0;if([i.translation_key,i.original_name,i.name].map(mc).some(e=>["power","main power","power switch","питание"].includes(e)))return!0;const o=mc(n?.attributes?.friendly_name);return/(?:^|[\s._-])(?:main[\s._-]+)?power$/.test(o)||/(?:^|[\s._-])питание$/.test(o)}function kc(e){for(const t of["hvac_action","action","current_operation","run_state","job_state","operation","activity"]){const i=mc(e?.[t]);if(hc.has(i)||uc.has(i))return i}return""}function xc(e,t){const i=e?.states?.[t],n=i?mc(i.state):"__missing__",r=String(t||"").split(".")[0],o=mc(i?.attributes?.device_class),s={eid:t,state:n,availability:_c(n)?"unavailable":"available",status:"neutral",activity:"none",edge:"none"};if("unavailable"===s.availability)return s;if(function(e,t,i){return"alarm_control_panel"===e?"triggered"===i:"on"===i&&In(e,t)}(r,o,n))return{...s,status:"alarm"};if("binary_sensor"===r)return oc.has(o)?{...s,edge:"rising"}:sc.has(o)?{...s,activity:"on"===n?"presence":"none"}:ac.has(o)?{...s,status:"on"===n?"open":"neutral",edge:"rising"}:"moving"===o?{...s,activity:"on"===n?"transition":"none"}:lc.has(o)&&"on"===n?{...s,status:"working",activity:"running"}:s;if("cover"===r)return{...s,activity:"opening"===n||"closing"===n?"transition":"none",edge:"terminal_transition"};if("lock"===r)return{...s,status:"unlocked"===n||"open"===n?"open":"neutral",activity:"locking"===n||"unlocking"===n?"transition":"none",edge:"terminal_transition"};if("valve"===r)return{...s,status:["open","opening","closing"].includes(n)?"open":"neutral",activity:"opening"===n||"closing"===n?"transition":"none",edge:"terminal_transition"};if("climate"===r){const e=kc(i.attributes),t=Array.isArray(i.attributes?.hvac_modes)?i.attributes.hvac_modes.map(mc):[],r=!uc.has(n)&&(pc.has(n)||hc.has(n)||t.includes(n));return(e?hc.has(e):r)?{...s,status:"working",activity:"running"}:s}if(["light","switch","fan","humidifier"].includes(r))return"on"===n?{...s,status:"working",activity:"running"}:s;if("media_player"===r)return"off"===n?{...s,availability:"unavailable"}:s;if("vacuum"===r)return"cleaning"===n?{...s,status:"working",activity:"running"}:"returning"===n?{...s,status:"working",activity:"transition"}:s;if("script"===r)return"on"===n?{...s,status:"working",activity:"running"}:s;if("automation"===r)return s;if("button"===r||"event"===r)return{...s,edge:"change"};const a=kc(i.attributes);return hc.has(a)||hc.has(n)&&!uc.has(n)?{...s,status:"working",activity:"running"}:s}function $c(e,t,i){const n=i.filter(t=>t.startsWith("switch.")&&!e?.entities?.[t]?.entity_category),r=t.find(t=>wc(e,t));if(!(n.length>1&&!!r))return t.map(t=>xc(e,t));const o=t.find(t=>t!==r&&bc(e,t)),s=xc(e,r);return"unavailable"===s.availability||"off"===s.state?t.map(t=>({eid:t,state:e?.states?.[t]?mc(e.states[t].state):"__missing__",availability:"unavailable",status:"neutral",activity:"none",edge:"none"})):t.map(t=>{if(t===o)return function(e,t){const i=e?.states?.[t],n=i?mc(i.state):"__missing__",r={eid:t,state:n,availability:_c(n)?"unavailable":"available",status:"neutral",activity:"none",edge:"none"};if("unavailable"===r.availability)return r;const o=kc(i?.attributes);return hc.has(o)||dc.has(n)&&!uc.has(n)?{...r,status:"working",activity:"running"}:r}(e,t);return{...t===r?s:xc(e,t),status:"neutral",activity:"none",edge:"none"}})}function Sc(e){if(!e.length)return{availability:"available",status:"neutral",activity:"none"};const t=e.filter(e=>"available"===e.availability);if(!t.length)return{availability:"unavailable",status:"neutral",activity:"none"};if(t.some(e=>"alarm"===e.status))return rc;const i=t.some(e=>"working"===e.status)?"working":t.some(e=>"open"===e.status)?"open":"neutral",n=t.some(e=>"transition"===e.activity)?"transition":t.some(e=>"presence"===e.activity)?"presence":t.some(e=>"running"===e.activity)?"running":"none";return{availability:"available",status:i,activity:n}}function Mc(e,t){if(!(e=>!!e&&!_c(e))(e)||"unavailable"===t.availability||e===t.state)return null;if("rising"===t.edge)return"off"===e&&"on"===t.state?"event":null;if("change"===t.edge)return"event";if("terminal_transition"===t.edge){const i=new Set([e,t.state]);if(i.has("closed")&&i.has("open")||i.has("locked")&&i.has("unlocked"))return"transition"}return null}const Tc=(e,t=0)=>{const i=Number(e);return Number.isInteger(i)&&i>=0?i:t},Rc=(e,t)=>{const i={rev:0,configRev:Tc(t),off:new Set};if(null==e)return{snapshot:i,valid:!1};const n=e.off;return!Number.isInteger(e.rev)||Number(e.rev)<0||!Number.isInteger(e.config_rev)||Number(e.config_rev)<0||!Array.isArray(n)||n.some(e=>"string"!=typeof e||!e)?{snapshot:i,valid:!1}:{snapshot:{rev:Number(e.rev),configRev:Number(e.config_rev),off:new Set(n)},valid:!0}};function Cc(e,t=0){return Rc(e,t).snapshot}function Dc(e,t,i,n){const r=Rc(t,i);return n&&r.valid&&r.snapshot.configRev===i?e.configRev===i&&r.snapshot.rev{try{const[i,n]=await Promise.all([e.callWS({type:"config/device_registry/list"}),e.callWS({type:"config/entity_registry/list"})]),r=qc(i,"id"),o=qc(n,"entity_id");if(!r||!o)throw new Error("invalid_registry_response");t.devices=r,t.entities=o,t.authoritative=!0,t.access="full",t.lastSuccess=Date.now(),t.error=void 0}catch(e){t.authoritative=!1,t.access="limited",t.error=function(e){if(e&&"object"==typeof e){const t=e;return String(t.message||t.code||t.error||"registry_unavailable")}return String(e||"registry_unavailable")}(e)}finally{t.revision++,t.loading=void 0,function(e){for(const t of[...e.listeners])try{t()}catch{}}(t)}})()),t.loading}function Wc(e,t){void 0===t.reloadTimer&&(t.reloadTimer=globalThis.setTimeout(()=>{t.reloadTimer=void 0,Bc(e,t)},80))}async function Uc(e,t){if(t.subscribing||t.unsubDevice&&t.unsubEntity)return t.subscribing;const i=e?.connection?.subscribeEvents;return"function"==typeof i?(t.subscribing=(async()=>{try{t.unsubDevice||(t.unsubDevice=await i.call(e.connection,()=>Wc(e,t),"device_registry_updated")),t.unsubEntity||(t.unsubEntity=await i.call(e.connection,()=>Wc(e,t),"entity_registry_updated"))}catch{}finally{0===t.refs&&(t.unsubDevice?.(),t.unsubEntity?.(),t.unsubDevice=void 0,t.unsubEntity=void 0),t.subscribing=void 0}})(),t.subscribing):void 0}function Gc(e,t){const i=jc(e);if(!i)return()=>{};const n=0===i.refs;i.refs++,i.listeners.add(t),("pending"===i.access||n)&&Bc(e,i),Uc(e,i);let r=!1;return()=>{r||(r=!0,i.listeners.delete(t),i.refs=Math.max(0,i.refs-1),i.refs>0||(i.unsubDevice?.(),i.unsubEntity?.(),i.unsubDevice=void 0,i.unsubEntity=void 0,void 0!==i.reloadTimer&&globalThis.clearTimeout(i.reloadTimer),i.reloadTimer=void 0))}}function Vc(e){const t=jc(e);if(!t)return{revision:0,authoritative:!1,access:"limited",devices:e?.devices||{},entities:e?.entities||{},lastSuccess:0,error:"registry_unavailable"};let i=e?.devices||{},n=e?.entities||{};if(t.authoritative){const r=t.liveDevices!==i||t.liveEntities!==n,o=void 0!==t.liveDevices||void 0!==t.liveEntities;if(r&&(t.liveDevices=i,t.liveEntities=n,t.projectedRevision=void 0,o&&Wc(e,t)),t.projectedRevision!==t.revision||!t.projectedDevices||!t.projectedEntities){const e={...t.devices},r={...t.entities};for(const[t,n]of Object.entries(i))Object.prototype.hasOwnProperty.call(e,t)||(e[t]=n);for(const[e,t]of Object.entries(n))Object.prototype.hasOwnProperty.call(r,e)||(r[e]=t);t.projectedDevices=e,t.projectedEntities=r,t.projectedRevision=t.revision}i=t.projectedDevices,n=t.projectedEntities}return{revision:t.revision,authoritative:t.authoritative,access:t.access,devices:i,entities:n,lastSuccess:t.lastSuccess,error:t.error}}function Kc(e){if(!e||"object"!=typeof e&&"function"!=typeof e)return 0;let t=Ec.get(e);return t||(t=Nc++,Ec.set(e,t)),t}function Yc(e,t=Vc(e)){return[t.revision,t.access,Kc(t.devices),Kc(t.entities)].join(":")}function Zc(e){return!!e&&null==e.disabled_by}function Xc(e,t=Vc(e)){const i={},n={},r={};for(const[e,n]of Object.entries(t.devices||{}))Zc(n)&&(i[e]=n);for(const[e,i]of Object.entries(t.entities||{})){if(!Zc(i))continue;const r=i.device_id?t.devices?.[i.device_id]:null;t.authoritative&&i.device_id&&!r||(r&&!Zc(r)||(n[e]=i))}for(const[i,n]of Object.entries(e?.states||{})){const e=t.entities?.[i];if(e&&!Zc(e))continue;const o=e?.device_id?t.devices?.[e.device_id]:null;t.authoritative&&e?.device_id&&!o||(o&&!Zc(o)||(r[i]=n))}return{...e,devices:i,entities:n,states:r}}function Jc(e,t=Vc(e)){return{...e,devices:t.devices||{},entities:t.entities||{}}}function Qc(e,t){const i=[];for(const[n,r]of Object.entries(e||{}))r?.device_id===t&&i.push(n);return i}function eh(){if(Lc)return Lc;if("undefined"==typeof localStorage)return{};try{const e=JSON.parse(localStorage.getItem(Hc)||"{}");if(!e||"object"!=typeof e||Array.isArray(e))return{};const t=Date.now(),i={};for(const[n,r]of Object.entries(e))!r||"active"!==r.kind&&"ha_disabled"!==r.kind||!Number.isFinite(r.ts)||t-r.ts>7776e6||(i[n]=r);return Lc=i,Lc}catch{return{}}}function th(e,t,i=Vc(e)){if(!t||"virtual"===t)return{kind:"active",enabledEntityIds:[],allEntityIds:[]};const n=t.indexOf(":");if(n<1)return{kind:"unverified",reason:"registry_unavailable",enabledEntityIds:[],allEntityIds:[]};const r=t.slice(0,n),o=t.slice(n+1);if("device"!==r&&"entity"!==r||!o)return{kind:"unverified",reason:"registry_unavailable",enabledEntityIds:[],allEntityIds:[]};const s=i.devices||{},a=i.entities||{};if(i.authoritative){if("device"===r){const e=s[o];if(!e)return{kind:"orphaned",reason:"device_missing",enabledEntityIds:[],allEntityIds:[]};const t=Qc(a,o);if(!Zc(e))return{kind:"ha_disabled",reason:"device",enabledEntityIds:[],allEntityIds:t};const i=t.filter(e=>Zc(a[e]));return t.length&&!i.length?{kind:"ha_disabled",reason:"all_entities",enabledEntityIds:[],allEntityIds:t}:{kind:"active",enabledEntityIds:i,allEntityIds:t}}const t=a[o];return t&&!Zc(t)?{kind:"ha_disabled",reason:"entity",enabledEntityIds:[],allEntityIds:[o]}:t?.device_id&&!s[t.device_id]?{kind:"orphaned",reason:"device_missing",enabledEntityIds:[],allEntityIds:[o]}:t?.device_id&&!Zc(s[t.device_id])?{kind:"ha_disabled",reason:"device",enabledEntityIds:[],allEntityIds:[o]}:t||e?.states?.[o]?{kind:"active",enabledEntityIds:[o],allEntityIds:[o]}:{kind:"orphaned",reason:"entity_missing",enabledEntityIds:[],allEntityIds:[]}}const l=function(e){return eh()[e]||null}(t);if("device"===r){const t=s[o],i=Qc(a,o);if(null!=t?.disabled_by)return{kind:"ha_disabled",reason:"device",enabledEntityIds:[],allEntityIds:i};const n=i.filter(e=>{const t=a[e];return null==t?.disabled_by&&(!t.device_id||null==s[t.device_id]?.disabled_by)});if(i.length&&!n.length&&i.every(e=>null!=a[e]?.disabled_by))return{kind:"ha_disabled",reason:"all_entities",enabledEntityIds:[],allEntityIds:i};if("ha_disabled"===l?.kind)return{kind:"ha_disabled",reason:l.reason||"device",enabledEntityIds:[],allEntityIds:i};if(t||n.some(t=>!!e?.states?.[t]))return{kind:"active",enabledEntityIds:n,allEntityIds:i}}else{const t=a[o];if(null!=t?.disabled_by)return{kind:"ha_disabled",reason:"entity",enabledEntityIds:[],allEntityIds:[o]};if(t?.device_id&&null!=s[t.device_id]?.disabled_by)return{kind:"ha_disabled",reason:"device",enabledEntityIds:[],allEntityIds:[o]};if("ha_disabled"===l?.kind)return{kind:"ha_disabled",reason:l.reason||"entity",enabledEntityIds:[],allEntityIds:[o]};if(t||e?.states?.[o])return{kind:"active",enabledEntityIds:[o],allEntityIds:[o]}}return{kind:"unverified",reason:"registry_unavailable",enabledEntityIds:[],allEntityIds:[]}}function ih(e){const t={};for(const[i,n]of Object.entries(e.entities)){if(!n?.device_id||!Zc(n))continue;const r=e.devices?.[n.device_id];r&&!Zc(r)||(t[n.device_id]=t[n.device_id]||[]).push(i)}return t}function nh(e,t){const i=new Map;for(const n of e){if(!0===n?.removed)continue;const e=n?.binding||"";if(!e.startsWith("entity:"))continue;const r=e.slice(7);if(!r)continue;const o=t?.entities?.[r]?.device_id;if(!o)continue;const s=i.get(o)||new Set;s.add(r),i.set(o,s)}return{byDevice:i}}function rh(e,t,i,n){const r=n.byDevice.get(t);return r?.size?{partial:!0,entityIds:i.filter(t=>!r.has(t)&&!e?.entities?.[t]?.hidden)}:{partial:!1,entityIds:[...i]}}function oh(e,t,i){if(t.identifiers?.[0]?.[0])return t.identifiers[0][0];for(const t of i){const i=e.entities[t]?.platform;if(i)return i}return""}function sh(e,t){if(/_device_temperature$/.test(t))return!1;if(e.entities?.[t]?.entity_category)return!1;const i=e.states[t];if(!i)return/_temperature$/.test(t);const n=i.attributes||{};return"temperature"===n.device_class||/°C|°F/.test(n.unit_of_measurement||"")||/_temperature$/.test(t)}const ah=["vacuum","lawn_mower","climate","media_player","light","cover","lock","valve","alarm_control_panel","water_heater","fan","humidifier","siren","camera","remote"],lh=e=>[...e.filter(e=>!e.reg?.hidden),...e.filter(e=>!!e.reg?.hidden)];function ch(e,t){const i=t.map(t=>({eid:t,reg:e?.entities?.[t]})).filter(e=>!!e.reg);if(!i.length)return[];const n=i.filter(e=>!e.reg.entity_category),r=n.length?n:i;for(const e of ah){const t=r.filter(t=>t.eid.startsWith(e+"."));if(t.length)return lh(t).map(e=>e.eid)}const o=r.filter(t=>function(e,t){if(!t.startsWith("binary_sensor."))return!1;const i=mc(e?.states?.[t]?.attributes?.device_class||e?.entities?.[t]?.device_class||e?.entities?.[t]?.original_device_class);return oc.has(i)||sc.has(i)||ac.has(i)||lc.has(i)||"moving"===i||cc.has(i)}(e,t.eid));if(o.length)return lh(o).map(e=>e.eid);const s=r.filter(e=>e.eid.startsWith("switch."));if(s.length){const t=lh(s),n=t.find(t=>wc(e,t.eid));if(s.length>1&&n){const t=i.filter(t=>t.eid!==n.eid&&"config"!==t.reg?.entity_category&&null!=yc(e,t.eid)),r=t.filter(e=>!e.reg?.entity_category),o=r.length?r:t;for(const t of[0,1,2]){const i=lh(o.filter(i=>yc(e,i.eid)===t));if(i.length)return[i[0].eid,n.eid]}}return[(n||t[0]).eid]}const a=[];for(const e of Ie)a.push(...lh(r.filter(t=>t.eid.startsWith(e+"."))));return a.push(...lh(r.filter(e=>!Ie.includes(e.eid.split(".")[0])))),a.map(e=>e.eid)}function hh(e,t,i){const n=t.map(t=>({eid:t,reg:e.entities[t],st:e.states[t]})).filter(e=>e.reg),r=[n.filter(e=>!e.reg.hidden&&!e.reg.entity_category),n.filter(e=>!e.reg.entity_category),n.filter(e=>!e.reg.hidden),n];if("mdi:thermometer"===i||"mdi:air-filter"===i)for(const t of r){const i=t.find(t=>sh(e,t.eid));if(i)return i.eid}const o=ch(e,t);return o.find(yn)||o[0]}function dh(e,t,i=[]){return[...new Set(uh(e,t,i))].filter(e=>yn(e))}function uh(e,t,i=[]){const n=e?.startsWith("entity:")?new Set([e.slice(7)]):new Set(i);return(t||[]).filter(e=>"string"==typeof e&&!n.has(e))}function ph(e,t){if("string"==typeof t)return e.area===t;const i=e.marker?.room_id;return i?!!t.id&&i===t.id:!!t.area&&e.area===t.area}function _h(e){const t=e.marker?.binding?.startsWith("entity:")?e.marker.binding.slice(7):null,i=e.entities.filter(yn),n=e.primary&&yn(e.primary)?e.primary:null;return[...new Set([t,n,...i].filter(e=>!!e&&yn(e)))]}function mh(e){const t=_h(e),i=e.marker?.light_entity;return i&&t.includes(i)?i:t[0]||null}function gh(e,t){if(!1===t.marker?.is_light)return[];if(!0===t.marker?.is_light){return[{eid:mh(t),via:"forced"}]}const i=t.primary||ch(e,t.entities)[0];return i&&!i.startsWith("light.")?[]:t.entities.filter(t=>t.startsWith("light.")&&!!e.states?.[t]).map(e=>({eid:e,via:"light"}))}const fh=new WeakMap,vh=new WeakMap;function yh(e){return e.map(e=>[e.id||"",!0===e.hidden?1:0,e.primary||"",[...e.entities].join(","),void 0===e.controls?"":[...e.controls].join(","),e.marker?.id||"",e.marker?.binding||"",e.marker?.is_light,e.marker?.tap_action||"",!0===e.marker?.removed?1:0,e.marker?.light_entity||"",null==e.marker?.controls?"":[...e.marker.controls].join(",")].join("")).join("")}function bh(e){const t=yh(e),i=fh.get(e);if(i?.fingerprint===t)return i;const n=e.filter(e=>!e.hidden),r=new Map,o=new Map;for(const t of e){o.set(t,uh(t.marker?.binding,t.marker?.controls??t.controls,t.entities));const e=t.hidden||!0!==t.marker?.is_light?null:t.marker?.id||t.id;e&&r.set(String(e),t)}const s=new Map;for(const t of e){const e=o.get(t)||[],i=new Set(void 0===t.controls?e.filter(yn):t.controls.filter(yn)),n=mh(t),a=i.size?[...i]:n?[n]:[];for(const i of e){if(!i.startsWith("marker:"))continue;const e=i.slice(7);if(!e||e===String(t.marker?.id||t.id||""))continue;if(!r.has(e))continue;const n=s.get(e)||{markerId:e,controllers:[],driverEids:[]};n.controllers.push({device:t,driverEids:[...a]}),n.driverEids=[...new Set([...n.driverEids,...a])],s.set(e,n)}}const a={fingerprint:t,visible:n,markerById:r,persistedByDevice:o,incomingByMarker:s};return fh.set(e,a),a}function wh(e){return bh(e).incomingByMarker}function kh(e,t){const i=dh(t.marker?.binding,t.controls??t.marker?.controls,t.entities),n=uh(t.marker?.binding,t.marker?.controls??t.controls,t.entities);return(null!=t.marker?.is_light||!i.length&&!n.some(e=>e.startsWith("marker:")))&&gh(e,t).length>0}function xh(e,t){return gh(e,t).some(e=>!!e.eid)}function $h(e,t,i,n){const r="always"===e||"auto"===e&&t,o=r&&!i;return{sourceExists:r,fromSourceEnabled:r&&i,manualEnabled:r,radiusEnabled:r,passive:o,effectiveMode:o&&"auto"===n?"fixed":n}}function Sh(e,t){const i=dh(t.marker?.binding,t.controls??t.marker?.controls,t.entities).map(e=>({eid:e,via:"controls"})),n=kh(e,t)?gh(e,t):[];for(const e of n)e.eid&&i.some(t=>t.eid===e.eid)||i.push(e);return i}function Mh(e,t,i,n){if(null!=i)return Mh(e,t,null,n).filter(e=>ph(e.device,i));const r=yh(t),o=function(e,t){const i=new Set;for(const e of t){for(const t of e.entities)i.add(t);for(const t of e.controls||e.marker?.controls||[])"string"!=typeof t||t.startsWith("marker:")||i.add(t)}return[...i].sort().map(t=>`${t}:${e?.states?.[t]?.state??""}`).join("|")}(e,t),s=Oc(n),a=vh.get(t);if(a?.graphFingerprint===r&&a.stateFingerprint===o&&a.virtualLightFingerprint===s&&a.registry===e?.entities)return a.sources;const{visible:l,markerById:c,persistedByDevice:h,incomingByMarker:d}=bh(t),u=null==i?l:l.filter(e=>ph(e,i)),p=new Map,_=new Map;for(const t of u){const i=Sh(e,t).filter(e=>"controls"!==e.via);for(const n of i){const i=!0===t.marker?.is_light?String(t.marker?.id||t.id||""):"",r=i?`marker:${i}`:`entity:${n.eid}`,o=n.eid?[n.eid]:[],s={key:r,eid:n.eid||"",stateEids:o,serviceEids:o,device:t,via:n.via,castsGlow:!0,passive:!n.eid,on:!n.eid||"on"===e.states?.[n.eid]?.state};p.set(t,[...p.get(t)||[],s]),n.eid&&_.set(n.eid,s)}}const m=new Map;for(const e of t){const t=h.get(e)||[],i=new Set(void 0===e.controls?t.filter(yn):e.controls);m.set(e,i)}for(const t of p.values())for(const i of t){if(!i.passive)continue;const t=i.key.slice(7),r=d.get(t);i.on=!r||r.driverEids.some(t=>"on"===e.states?.[t]?.state),!r&&zc(i.device.marker)&&(i.on=Pc(i.device.marker,n))}const g=new Map,f=e=>{const t=g.get(e.key);(!t||e.castsGlow&&!t.castsGlow)&&g.set(e.key,e)};for(const t of l){const n=h.get(t)||[],r=null==i||ph(t,i);for(const i of n){if(i.startsWith("marker:")){const e=c.get(i.slice(7)),t=e?p.get(e):null;if(t)for(const e of t)f(e);continue}if(!yn(i)||!m.get(t)?.has(i))continue;const n=_.get(i);n?f(n):r&&f({key:`entity:${i}`,eid:i,stateEids:[i],serviceEids:[i],device:t,via:"controls",castsGlow:!1,passive:!1,on:"on"===e.states?.[i]?.state})}for(const e of p.get(t)||[])f(e)}const v=[...g.values()];return vh.set(t,{graphFingerprint:r,stateFingerprint:o,virtualLightFingerprint:s,registry:e?.entities,sources:v}),v}function Th(e){const t=e.filter(e=>e.castsGlow);return t.find(e=>e.on)||t[0]||null}function Rh(e){return e.length?e.some(e=>e.on)?"on":"off":"none"}function Ch(e,t){const i=[];for(const n of t){const t=e.states[n];if(!t)continue;const r=(t.attributes?.unit_of_measurement||"").toLowerCase();if(/_(linkquality|lqi)$/.test(n)||"lqi"===r){const e=parseFloat(t.state);isNaN(e)||i.push(e);continue}const o=t.attributes?.linkquality??t.attributes?.lqi;if(null!=o){const e=parseFloat(o);isNaN(e)||i.push(e)}}return Mi(i)}function Dh(e,t){for(const i of t){if(!sh(e,i))continue;const t=e.states[i];if(!t)continue;const n=parseFloat(t.state);if(!isNaN(n))return Math.round(10*n)/10}return null}function Ah(e,t){for(const i of t){if(!i.startsWith("climate."))continue;const t=e.states[i];if(!t||"unavailable"===t.state||"unknown"===t.state)continue;const n=parseFloat(t.attributes?.current_temperature);if(Number.isFinite(n))return Math.round(10*n)/10}return null}function Oh(e,t){if(e.entities?.[t]?.entity_category)return!1;const i=e.states[t];if(!i)return/_humidity$/.test(t);const n=i.attributes||{};return"humidity"===n.device_class||"%"===n.unit_of_measurement&&/_humidity$/.test(t)||/_humidity$/.test(t)}function zh(e,t){for(const i of t){if(!Oh(e,i))continue;const t=e.states[i];if(!t)continue;const n=parseFloat(t.state);if(!isNaN(n))return Math.round(n)}return null}function Ph(e,t){if(!t)return[];const i=[];for(const[t,n]of Object.entries(e.entities)){if(!t.startsWith("light.")||n.hidden||!Zc(n))continue;let r=null;if("group"===n.platform)r=n.area_id||null;else{if(!n.device_id)continue;{const t=e.devices[n.device_id];if(!Zc(t))continue;if("Group"!==t?.model)continue;r=t.area_id||n.area_id||null}}if(!r)continue;const o=e.states[t];i.push({eid:t,name:n.name||o?.attributes?.friendly_name||t,area:r})}return i}function Fh(e,t,i,n,r){const o=Fe(t,i,r);if(o!==Pe)return o;const s=[];for(const t of n){const i=e.states[t]?.attributes?.device_class;i&&s.push(i)}return function(e){for(const t of e){const e=ze[t];if(e)return e}return null}(s)??Pe}function Ih(e,t){return e.map(e=>{if(!Array.isArray(e.controls))return e;const i=e.controls.filter(e=>!("string"==typeof e&&e.startsWith("marker:")&&t.has(e.slice(7))));return i.length===e.controls.length?e:{...e,controls:i.length?i:null}})}function Eh(e,t,i){if(!t||!i||t===i)return[...e];const n=`marker:${t}`,r=`marker:${i}`;return e.map(e=>{const t=Array.isArray(e.controls)?e.controls.map(e=>e===n?r:e):e.controls,i="derived_marker_state"===e.value_badge?.source?.kind&&e.value_badge.source.ref===n?{...e.value_badge,source:{...e.value_badge.source,ref:r}}:e.value_badge;return t===e.controls&&i===e.value_badge?e:{...e,controls:t,value_badge:i}})}function Nh(e,t,i){if(!t||t===i)return!0;const n=new Map;for(const t of e)n.set(t.id,(t.controls||[]).filter(e=>"string"==typeof e&&e.startsWith("marker:")).map(e=>e.slice(7)));const r=new Set,o=[i];for(;o.length;){const e=o.pop();if(e===t)return!0;r.has(e)||(r.add(e),o.push(...n.get(e)||[]))}return!1}function Hh(e,t,i,n){const r=new Set([t]),o=e.filter(e=>{const o=e.id===t||!n&&e.binding===i;return o&&r.add(e.id),!o});return{markers:n?o:[...o,{id:t,binding:i,removed:!0,hidden:!0}],cleanupIds:r}}function Lh(e){const t=new Set,i=new Set,n=new Set;for(const r of e||[]){const e=String(r.binding||"").indexOf(":");if(e<1)continue;const o=r.binding.slice(0,e),s=r.binding.slice(e+1);s&&(!0===r?.removed?"device"===o?t.add(s):"entity"===o&&i.add(s):"entity"===o&&n.add(s))}return{devices:t,entities:i,liveEntities:n}}function jh(e,t,i){if(i.liveEntities.has(t))return!1;if(i.entities.has(t))return!0;const n=e?.entities?.[t]?.device_id;return!!n&&i.devices.has(n)}function qh(e,t,i,n,r){const o=dh(t.binding,t.controls,e.entities).filter(e=>!jh(i,e,n)).filter(e=>"active"===th(i,"entity:"+e,r).kind);e.marker=t,e.controls=o,e.userHidden=!0===t.hidden,e.hidden=e.userHidden||"ha_disabled"===e.bindingStatus?.kind,t.name&&(e.name=t.name),t.icon&&(e.icon=t.icon),null!=t.model&&(e.model=t.model),e.link=t.link??null,e.description=t.description??null,e.pdfs=t.pdfs||[],e.tapAction=t.tap_action??null}function Bh(e,t,i,n){if("string"==typeof e.room_id&&e.room_id.length>0&&null===e.area)return{area:"",space:e.space||n};const r=e.area||t||"";return{area:r,space:r&&i[r]||e.space||n}}function Wh(e){const t=e.hass,i=e.registry||Vc(t),n=Xc(t,i),r=Jc(t,i),{areaToSpace:o,markers:s,settings:a,excluded:l,showAll:c,firstSpaceId:h,loc:d,iconRules:u}=e,p=!1!==a.group_lights,_=Lh(s),m=Ph(n,p).filter(e=>!jh(n,e.eid,_)),g=new Set(m.map(e=>e.area)),f=ih(n),v=function(e){const t={};for(const[i,n]of Object.entries(e.entities||{}))n?.device_id&&(t[n.device_id]=t[n.device_id]||[]).push(i);return t}(r),y=nh(s,r),b=new Set;for(const e of s){const[t,i]=e.binding.split(":");"device"!==t&&"entity"!==t||!i||b.add(e.binding)}const w=(e,t)=>s.find(i=>i.binding===e+":"+t),k={},x=[];for(const e of Object.values(n.devices)){const r=e.area_id;if(!r||!o[r])continue;if("service"===e.entry_type)continue;if(b.has("device:"+e.id))continue;const s=th(t,"device:"+e.id,i);if("active"!==s.kind)continue;const h=w("device",e.id);if(h&&h.hidden&&!a.filter_seeded)continue;const _=rh(n,e.id,f[e.id]||[],y);if(_.partial&&!_.entityIds.length)continue;const m=_.entityIds,v=_.partial?{kind:"active",enabledEntityIds:m,allEntityIds:m}:s,$=oh(n,e,m),S=!a.filter_seeded;if(S&&!c){if(l.has($))continue;if("Group"===e.model)continue;if(/scene/i.test(e.model||""))continue;if(/bridge/i.test((e.model||"")+(e.name||"")))continue;if("myheat"===$&&e.via_device_id)continue}const M=(e.name_by_user||e.name||d("device.unnamed")).trim(),T=M+"|"+r;let R=Fh(n,M,e.model,m,u);if(m.some(e=>e.startsWith("lock."))&&(R="mdi:lock"),S&&!c&&p&&"mdi:lightbulb"===R&&g.has(r))continue;k[T]=(k[T]||0)+1;const C=k[T]>1?M+" "+k[T]:M,D={id:e.id,name:C,model:e.model||"",area:r,space:o[r],icon:R,entities:m,allEntities:v.allEntityIds,bindingStatus:v,bindingKind:"device",bindingRef:e.id,pdfs:[]};D.primary=hh(n,m,R),"mdi:thermometer"!==R&&"mdi:air-filter"!==R||(D.temp=Dh(n,m)),D.primary&&Oh(n,D.primary)&&(D.hum=zh(n,m)),x.push(D)}for(const e of m)o[e.area]&&(b.has("entity:"+e.eid)||x.push({id:"lg_"+e.eid,name:e.name,model:d("device.light_group"),area:e.area,space:o[e.area],icon:"mdi:lightbulb-group",entities:[e.eid],allEntities:[e.eid],bindingStatus:{kind:"active",enabledEntityIds:[e.eid],allEntityIds:[e.eid]},primary:e.eid,bindingKind:"entity",bindingRef:e.eid,pdfs:[]}));for(const e of s){if(e.removed)continue;const[s,l]=e.binding.split(":"),c="device"===s||"entity"===s?th(t,e.binding,i):null;if(!e.hidden||a.filter_seeded||"ha_disabled"===c?.kind)if("device"===s){const t=c;if("unverified"===t.kind)continue;const s=r.devices[l],{area:a,space:p}=Bh(e,s?.area_id,o,h),m="active"===t.kind?t.enabledEntityIds:[];let g=s?Fh(n,s.name_by_user||s.name||"",s.model,m,u):"mdi:help-circle";m.some(e=>e.startsWith("lock."))&&(g="mdi:lock");const f={id:e.id,name:s?.name_by_user||s?.name||d("device.fallback"),model:s?.model||"",area:a,space:p,icon:g,entities:m,allEntities:t.allEntityIds.length?t.allEntityIds:s&&v[s.id]||[],bindingStatus:t,bindingKind:"device",bindingRef:l};f.primary=hh(n,m,g),"mdi:thermometer"!==g&&"mdi:air-filter"!==g||(f.temp=Dh(n,m)),f.primary&&Oh(n,f.primary)&&(f.hum=zh(n,m)),qh(f,e,r,_,i),x.push(f)}else if("entity"===s){if(jh(r,l,_))continue;const t=c;if("unverified"===t.kind)continue;const s=r.entities[l],a=s?.area_id||s?.device_id&&r.devices[s.device_id]?.area_id||"",{area:d,space:p}=Bh(e,a,o,h),m=n.states[l],g=s?.name||m?.attributes?.friendly_name||l;let f=Fh(n,g,"",[l],u);l.startsWith("lock.")&&(f="mdi:lock");const v={id:e.id,name:g,model:"",area:d,space:p,icon:f,entities:"active"===t.kind?[l]:[],allEntities:[l],bindingStatus:t,primary:"active"===t.kind?l:void 0,bindingKind:"entity",bindingRef:l};"mdi:thermometer"!==f&&"mdi:air-filter"!==f||!v.entities.length||(v.temp=Dh(n,v.entities)),v.entities.length&&Oh(n,l)&&(v.hum=zh(n,v.entities)),qh(v,e,r,_,i),x.push(v)}else{const t=e.area||"",n=e.space||t&&o[t]||h,s={id:e.id,name:e.name||d("device.virtual"),model:e.model||"",area:t,space:n,icon:e.icon||"mdi:map-marker",entities:[],allEntities:[],bindingStatus:{kind:"active",enabledEntityIds:[],allEntityIds:[]},bindingKind:"virtual",virtual:!0};qh(s,e,r,_,i),x.push(s)}}return x}function Uh(e){const{marker:t,siblingMarkers:i=[],...n}=e;return Wh({...n,markers:[...i.filter(e=>e.id!==t.id),t]}).find(e=>e.id===t.id)||null}function Gh(e,t,i,n){if(!t)return null;if(function(e,t,i){if(!t)return!1;const n=t.indexOf(":");if(n<1)return!1;const r=t.slice(0,n),o=t.slice(n+1),s=Lh(i);return"device"===r?s.devices.has(o):"entity"===r&&jh(e,o,s)}(e,t,n))return null;const r=t.indexOf(":");if(r<0)return null;const o=t.slice(0,r),s=t.slice(r+1);if(!s)return null;if("entity"===o){const t=parseFloat(e.states[s]?.state);return Number.isFinite(t)?"temp"===i?Math.round(10*t)/10:Math.round(t):null}if("device"===o){const t=Object.entries(e.entities).filter(([,e])=>e.device_id===s).map(([e])=>e);return"temp"===i?Dh(e,t):zh(e,t)}return null}const Vh=new RegExp(["water","voda","coolant","flow_?temp","return_?temp","target","setpoint","chip","cpu","processor","board","core_temp","device_temp","batter","akkum","freezer","fridge","oven","kettle","boiler"].join("|"),"i");function Kh(e,t,i){const n=[];for(const r of t){if(r.area!==i||r.virtual)continue;const t=Ch(e,r.entities);null!=t&&n.push(t)}return Mi(n)}function Yh(e,t,i){const n=[];for(const r of t){if(r.area!==i)continue;if("mdi:thermometer"!==r.icon&&"mdi:air-filter"!==r.icon)continue;const t=Dh(e,r.entities);null!=t&&n.push(t)}return n.length?Math.round(n.reduce((e,t)=>e+t,0)/n.length*10)/10:null}const Zh={offStates:["off"],unknownUsesToggle:!0},Xh={light:Zh,switch:Zh,fan:Zh,humidifier:Zh,input_boolean:Zh,automation:Zh,remote:Zh,group:Zh,climate:{...Zh,featureMasks:{turn_on:256,turn_off:128,toggle:384}},media_player:{...Zh,featureMasks:{turn_on:128,turn_off:256,toggle:384}},siren:{...Zh,featureMasks:{turn_on:1,turn_off:2,toggle:3}},vacuum:{...Zh,featureMasks:{turn_on:1,turn_off:2,toggle:3}},water_heater:{...Zh,featureMasks:{turn_on:8,turn_off:8,toggle:8}},camera:{...Zh,featureMasks:{turn_on:1,turn_off:1,toggle:1}}};function Jh(e){return e.slice(0,e.indexOf("."))}function Qh(e,t,i){if(!e?.services||"object"!=typeof e.services)return!1;const n=e.services?.[t];return!!n&&Object.prototype.hasOwnProperty.call(n,i)}function ed(e,t,i){return Qh(e,t,i)?{domain:t,service:i}:Qh(e,"homeassistant",i)?{domain:"homeassistant",service:i}:null}function td(e,t){return e?.entities?.[t]||null}function id(e,t,i){const n=e?.states?.[i],r=td(t,i);return n?.attributes?.friendly_name||r?.name||r?.original_name||i}function nd(e,t){const i=td(e,t);if(null!=i?.disabled_by)return!0;const n=i?.device_id?e?.devices?.[i.device_id]:null;return null!=n?.disabled_by}function rd(e,t){const i=e?.attributes?.supported_features;if(null==i||""===i)return!1;const n=Number(i);return Number.isFinite(n)&&(n&t)===t}function od(e,t,i){const n=Jh(i);if("lock"===n||"alarm_control_panel"===n)return!0;if("cover"!==n)return!1;const r=e?.states?.[i],o=td(t,i),s=String(r?.attributes?.device_class||o?.device_class||o?.original_device_class||"");return Fi.has(s)}function sd(e,t,i,n,r){return{ref:i,entityId:n,name:n?id(e,t,n):null,reason:r}}function ad(e,t,i,n,r,o=null){return{target:null,skipped:sd(e,t,i,n,r),semantics:o,nextEffect:null,command:null}}function ld(e,t,i,n,r=i){const o=Jh(i);if("cover"!==o&&"valve"!==o)return function(e,t,i,n,r=i){const o=Jh(i),s=e?.states?.[i];if(nd(t,i))return ad(e,t,r,i,"ha-disabled","power");if(od(e,t,i))return ad(e,t,r,i,"secure");if(!s)return ad(e,t,r,i,"missing","power");if("unavailable"===s.state)return ad(e,t,r,i,"unavailable","power");const a=Xh[o];if(!a)return ad(e,t,r,i,"unsupported");let l,c;if("unknown"===s.state||""===s.state){if(!a.unknownUsesToggle)return ad(e,t,r,i,"unsupported","power");l="toggle",c="toggle"}else a.offStates.includes(String(s.state))?(l="turn-on",c="turn_on"):(l="turn-off",c="turn_off");const h=a.featureMasks?.[c];if(h&&!rd(s,h))return ad(e,t,r,i,"unsupported","power");const d=ed(e,o,c);return d?{target:{entityId:i,name:id(e,t,i),state:String(s.state||""),via:n},skipped:null,semantics:"power",nextEffect:l,command:{...d,data:{entity_id:i}}}:ad(e,t,r,i,"unsupported","power")}(e,t,i,n,r);if(nd(t,i))return ad(e,t,r,i,"ha-disabled",o);if(od(e,t,i))return ad(e,t,r,i,"secure",o);const s=e?.states?.[i];if(!s)return ad(e,t,r,i,"missing",o);if("unavailable"===s.state)return ad(e,t,r,i,"unavailable",o);const a=function(e,t,i){const n=String(i?.state||""),r="closed"===n?{service:`open_${t}`,effect:"open",feature:1}:"open"===n?{service:`close_${t}`,effect:"close",feature:2}:"opening"===n||"closing"===n?{service:`stop_${t}`,effect:"stop",feature:8}:null;return r&&rd(i,r.feature)&&Qh(e,t,r.service)?r:rd(i,3)&&Qh(e,t,"toggle")?{service:"toggle",effect:"toggle"}:null}(e,o,s);if(!a)return ad(e,t,r,i,"unsupported",o);return{target:{entityId:i,name:id(e,t,i),state:String(s.state||""),via:n},skipped:null,semantics:o,nextEffect:a.effect,command:{domain:o,service:a.service,data:{entity_id:i}}}}function cd(e){return"entity"===e.bindingKind?e.bindingRef&&yn(e.bindingRef)?[e.bindingRef]:[]:"virtual"===e.bindingKind?[]:_h(e)}function hd(e){const t=e.marker?.toggle_entity;return t&&cd(e).includes(t)?t:null}function dd(e,t,i){const n=function(e,t){if("entity"===e.bindingKind&&e.bindingRef){const t=e.marker?.light_entity?mh(e):null;return[...new Set([t,e.bindingRef].filter(e=>!!e))]}const i=e.entities.length?e.entities:e.allEntities||[],n=e.marker?.light_entity||e.primary&&yn(e.primary)?mh(e):null;return[...new Set([n,...ch(t,i)].filter(e=>!!e))]}(i,t);if(!n.length)return null;const r="entity"===i.bindingKind?"binding":"device-role";if("binding"===r)return ld(e,t,n[0],r);let o=null,s=null;for(const i of n){const n=ld(e,t,i,r);if(n.command)return n;const a=n.skipped?.reason;if("missing"===a||"unavailable"===a||"secure"===a)return n;"ha-disabled"!==a?o||=n:s||=n}return o||s}function ud(e){const t=e.skipped?.reason;return"missing"===t?"unavailable":t||"unsupported"}function pd(e,t){return{origin:e,kind:"none",semantics:null,targets:[],skippedTargets:[],noneReason:t,nextEffect:null,command:null}}function _d(e,t){return"cover"===e||"toggle"===e?"toggle":"more-info"===e||"run"===e||"info"===e?e:(null==e||""===e)&&"light"===t?"toggle":"info"}function md(e,t){return{origin:e,kind:"single",semantics:t.semantics,targets:t.target?[t.target]:[],skippedTargets:t.skipped?[t.skipped]:[],noneReason:t.command?null:ud(t),nextEffect:t.nextEffect,command:t.command}}function gd(e,t,i,n=[]){const r=new Map,o=[],s=new Set,a=e=>{const t=`${e.ref}\n${e.entityId||""}\n${e.reason}`;s.has(t)||(s.add(t),o.push(e))};for(const e of n)a(e);for(const n of i){const i=ld(e,t,n.entityId,n.via,n.ref);i.target?r.set(n.entityId,r.get(n.entityId)||i.target):i.skipped&&a(i.skipped)}const l=[...r.values()];if(!l.length)return{origin:"explicit-toggle",kind:"group",semantics:"group-power",targets:l,skippedTargets:o,noneReason:o.length&&o.every(e=>"secure"===e.reason)?"secure":"configured-targets-missing",nextEffect:null,command:null};const c=l.some(e=>"on"===e.state)?"turn_off":"turn_on",h=ed(e,"homeassistant",c);return h?{origin:"explicit-toggle",kind:"group",semantics:"group-power",targets:l,skippedTargets:o,noneReason:null,nextEffect:"turn_on"===c?"turn-on":"turn-off",command:{...h,data:{entity_id:l.map(e=>e.entityId)}}}:{origin:"explicit-toggle",kind:"group",semantics:"group-power",targets:[],skippedTargets:[...o,...l.map(i=>sd(e,t,i.entityId,i.entityId,"unsupported"))],noneReason:"unsupported",nextEffect:null,command:null}}function fd(e){return String(e.marker?.id||e.id||"")}function vd(e){const{hass:t,device:i}=e,n=e.registryHass||t,r=function(e){return"toggle"===e.tapAction?"explicit-toggle":"cover"===e.tapAction?"legacy-cover":!e.tapAction&&e.primary?.startsWith("light.")?"default-light":null}(i);if(!r)return null;const o=i.marker;if("explicit-toggle"===r&&zc(o)){const t=String(o?.id||i.id||""),n=wh(e.devices).get(t);if(n)return function(e,t){const i=e.registryHass||e.hass;return gd(e.hass,i,t.driverEids.map(e=>({entityId:e,via:"control-marker-driver",ref:`marker:${t.markerId}`})))}(e,n);const s=Pc(o,e.virtualLights);return{origin:r,kind:"single",semantics:"power",targets:[{entityId:"",name:i.name,state:s?"on":"off",via:"virtual-light"}],skippedTargets:[],noneReason:null,nextEffect:s?"turn-off":"turn-on",command:null,operation:{kind:"virtual-light",markerId:o.id}}}if("explicit-toggle"===r){if(uh(i.marker?.binding,i.marker?.controls??i.controls,i.entities).length)return function(e,t=null){const{hass:i,device:n,devices:r}=e,o=e.registryHass||i,s=uh(n.marker?.binding,n.marker?.controls??n.controls,n.entities),a=e.lightSources||Mh(i,r,null,e.virtualLights),l=new Map;for(const e of a){if(!e.key.startsWith("marker:"))continue;const t=l.get(e.key)||[];t.push(e),l.set(e.key,t)}const c=wh(r),h=new Map;for(const e of r){const t=String(e.marker?.id||e.id||"");t&&h.set(t,e)}const d=[],u=[];t&&d.push({entityId:t,via:"entity"===n.bindingKind?"binding":"device-role",ref:t});for(const e of s){if(!e.startsWith("marker:")){yn(e)?d.push({entityId:e,via:"control-entity",ref:e}):u.push(sd(i,o,e,e.includes(".")?e:null,"unsupported"));continue}const t=l.get(e)||[];if(!t.length){const t=h.get(e.slice(7));if("ha_disabled"===t?.bindingStatus?.kind){const n=t.bindingStatus.allEntityIds[0]||null;u.push({ref:e,entityId:n,name:t.name||(n?id(i,o,n):null),reason:"ha-disabled"})}else u.push(sd(i,o,e,null,"missing"));continue}const r=[...new Set(t.flatMap(e=>e.serviceEids))];if(r.length){for(const t of r)d.push({entityId:t,via:"control-entity",ref:e});continue}const s=t.some(e=>e.passive),a=e.slice(7),p=fd(n),_=c.get(a)?.controllers.find(e=>fd(e.device)===p);if(s&&_?.driverEids.length)for(const t of _.driverEids)d.push({entityId:t,via:"control-marker-driver",ref:e});else u.push(sd(i,o,e,null,s?"missing":"unsupported"))}return gd(i,o,d,u)}(e,hd(i))}if("legacy-cover"===r){const e=i.entities.find(e=>e.startsWith("cover."))||i.allEntities?.find(e=>e.startsWith("cover."));return e?md(r,ld(t,n,e,"device-role")):pd(r,"no-actionable-entity")}const s=hd(i),a=s?ld(t,n,s,"entity"===i.bindingKind?"binding":"device-role"):dd(t,n,i);return a?md(r,a):pd(r,i.virtual||"virtual"===i.bindingKind?"no-actionable-entity":"no-binding")}function yd(e){if(!e)return[];const t=Array.isArray(e.data.entity_id)?e.data.entity_id:[e.data.entity_id];return[...new Set(t)].sort()}function bd(e){return e?e.operation?e.operation:e.command?{kind:"ha-service",command:e.command}:null:null}const wd=new Set(["missing","ha-disabled","unavailable"]);function kd(e,t){if(!bd(e)||!e.nextEffect||!e.targets.length)return[];const i="group"===e.kind,n=i?e.targets.filter(e=>"on"===e.state).length:0,r=i?0===n?t.groupAllOff():t.groupCurrent(n,e.targets.length):t.state(e.targets[0]);let o;return o="toggle"===e.nextEffect?t.expectedByHa():i&&"turn-on"===e.nextEffect?t.groupAllOn():i&&"turn-off"===e.nextEffect?t.groupAllOff():t.effect(e.nextEffect),[t.current(r),t.expected(o),...e.skippedTargets.length?[t.unavailable(e.skippedTargets.length)]:[]]}function xd(e,t){const i=bd(e),n=bd(t);return!(!i||!n||i.kind!==n.kind)&&("virtual-light"===i.kind&&"virtual-light"===n.kind?i.markerId===n.markerId:"ha-service"===i.kind&&"ha-service"===n.kind&&function(e,t){const i=yd(e),n=yd(t);return i.length===n.length&&i.every((e,t)=>e===n[t])}(i.command,n.command))}function $d(e,t){const i=e.targets[0]||e.skippedTargets[0],n=[];return"group"===e.kind?e.targets.length&&n.push(t.group(e.targets)):i&&n.push(t.single(i)),e.nextEffect&&e.targets.length&&n.push("group"===e.kind?t.groupCurrentNext(e.targets,e.nextEffect):t.currentNext(e.targets[0],e.nextEffect)),e.skippedTargets.length&&n.push(t.skipped(e.skippedTargets)),!e.command&&e.noneReason&&n.push(t.none(e.noneReason)),n}const Sd=e=>!!e&&e.length>=2&&Number.isFinite(e[0])&&Number.isFinite(e[1]);function Md(e,t=5,i=5){return vo(e.cm,t,i)/2}function Td(e,t,i=1,n=5,r=5,o=0){const s=e.host;if(!s||"partition"!==s.kind||"string"!=typeof s.id||!s.id)return{resolved:null,reason:"invalid-host"};const a=t.find(e=>e.id===s.id);if(!a)return{resolved:null,reason:"missing-partition"};if(!Sd(a.a)||!Sd(a.b)||!Number.isFinite(s.t)||s.t<0||s.t>1)return{resolved:null,reason:"invalid-position"};const l=a.b[0]-a.a[0],c=a.b[1]-a.a[1],h=Math.hypot(l,c),d=Number(e.length)*i;if(!(h>1e-9&&d>0&&Number.isFinite(d)))return{resolved:null,reason:"invalid-length"};const u=s.t*h;if(u-d/2h-o+1e-9)return{resolved:null,reason:"does-not-fit"};const p=l/h,_=c/h;let m=180*Math.atan2(c,l)/Math.PI;return m>=90?m-=180:m<-90&&(m+=180),{reason:null,resolved:{opening:e,host:s,partition:a,center:[a.a[0]+l*s.t,a.a[1]+c*s.t],angle:m,length:d,depth:vo(a.cm,n,r),t:s.t,axis:{a:[a.a[0],a.a[1]],b:[a.b[0],a.b[1]],ux:p,uy:_,length:h}}}}function Rd(e,t,i=1,n=5,r=5){return Td(e,t,i,n,r,0)}function Cd(e,t,i=1,n=5,r=5){const o=e.host,s="partition"===o?.kind?t.find(e=>e.id===o.id):void 0;if(!s)return Rd(e,t,i,n,r);const a=Td(e,t,i,n,r,Md(s,n,r));return"does-not-fit"===a.reason?{resolved:null,reason:"does-not-fit-jamb"}:a}function Dd(e,t){return!e||(e.length!==t.length||e.host?.kind!==t.host?.kind||e.host?.id!==t.host?.id||e.host?.t!==t.host?.t)}function Ad(e){const{center:t,length:i,axis:n,host:r}=e,o=i/2;return{hostId:r.id,a:[t[0]-n.ux*o,t[1]-n.uy*o],b:[t[0]+n.ux*o,t[1]+n.uy*o],depth:e.depth}}function Od(e,t=!1){const i=t?1:-1,n=e.depth/2;return{ox:-e.axis.uy*i*n,oy:e.axis.ux*i*n,cm:e.partition.cm,side:i}}function zd(e,t,i){return e.flatMap(e=>!Sd(e.a)||!Sd(e.b)||Math.hypot(e.b[0]-e.a[0],e.b[1]-e.a[1])<=1e-9?[]:[{roomId:"",a:[e.a[0],e.a[1]],b:[e.b[0],e.b[1]],key:`partition:${e.id}`,kind:"outer",cm:e.cm,open:!1,half:vo(e.cm,t,i)/2,partitionHost:{kind:"partition",id:e.id}}])}function Pd(e,t,i){const{center:n,length:r,axis:o}=e,s=r/2,a=t.flatMap(e=>{if(!e.kind||e.open)return[];const t=e.b[0]-e.a[0],r=e.b[1]-e.a[1],a=Math.hypot(t,r);if(!(a>i))return[];const l=t/a,c=r/a;if(Math.abs(o.ux*c-o.uy*l)>1e-6)return[];if(Math.abs((n[0]-e.a[0])*c-(n[1]-e.a[1])*l)>i)return[];const h=e=>(e[0]-n[0])*o.ux+(e[1]-n[1])*o.uy,d=h(e.a),u=h(e.b),p=Math.max(-s,Math.min(d,u)),_=Math.min(s,Math.max(d,u));return _>=p-i?[[p,_]]:[]});if(!a.length)return!1;a.sort((e,t)=>e[0]-t[0]||e[1]-t[1]);let l=-s;for(const[e,t]of a){if(e>l+i)return!1;if(l=Math.max(l,t),l>=s-i)return!0}return!1}function Fd(e,t,i=1e-9){const n=e.length/(2*e.axis.length),r=e.t-n,o=e.t+n;return t.some(t=>{if(t.host.id!==e.host.id||t.opening.id===e.opening.id)return!1;const n=t.length/(2*t.axis.length);return Math.max(r,t.t-n)[]);for(let i=0;i=1-1e-9||u<=1e-9||u>=1-1e-9||(t[i].push(d),t[s].push(u))}}const i=[];for(let n=0;ne-t),1];for(let e=1;e{const i=t[2]-t[0],n=t[3]-t[1],r=i*i+n*n;if(!(r>0))return Math.hypot(e[0]-t[0],e[1]-t[1]);const o=Math.max(0,Math.min(1,((e[0]-t[0])*i+(e[1]-t[1])*n)/r));return Math.hypot(e[0]-(t[0]+o*i),e[1]-(t[1]+o*n))},jd=(e,t,i,n)=>{const r=n[2]-n[0],o=n[3]-n[1],s=t*o-i*r;if(Math.abs(s)<1e-12)return 1/0;const a=n[0]-e[0],l=n[1]-e[1],c=(a*o-l*r)/s;if(!(c>1e-9))return 1/0;const h=(a*i-l*t)/s;return h<-1e-9||h>1+1e-9?1/0:c};function qd(e,t,i,n=96){if(!(t>0&&Number.isFinite(e[0])&&Number.isFinite(e[1])))return[];const r=[];for(const n of i){if(!n||n.length<4)continue;if(![n[0],n[1],n[2],n[3]].every(Number.isFinite))continue;const i=Ld(e,n);if(i<1e-7)return[];i>t||r.push(n)}const o=[],s=Math.max(12,Math.round(n));for(let e=0;ee-t);const l=[];let c=Number.NEGATIVE_INFINITY;for(const i of o){if(i-c<1e-9)continue;c=i;const n=Math.cos(i),o=Math.sin(i);let s=t;for(const t of r){const i=jd(e,n,o,t);i=3?l:[]}const Bd=[o` + :host { + --hp-bg: var(--card-background-color, #16212e); + --hp-line: var(--divider-color, #2b3d4f); + --hp-txt: var(--primary-text-color, #e6edf3); + --hp-muted: var(--secondary-text-color, #8aa0b3); + --hp-accent: var(--primary-color, #3ea6ff); + --hp-on: #ffd45c; + --hp-open: #ff9f43; + /* design tokens (UI chrome only). The icon/plan scale math stays on + --icon-size/--dev-size cqw units and never uses these. */ + /* spacing scale, fact-based; stray 3/5/7/9/13/14px values are unified + onto the nearest step (max +-2px, the whole point of the pass) */ + --sp-1: 2px; + --sp-2: 4px; + --sp-3: 6px; + --sp-4: 8px; + --sp-5: 12px; + --sp-6: 16px; + /* px radii of dialogs/buttons/plates (the 22% badge radius is scale math) */ + --rad-s: 6px; + --rad-m: 8px; + --rad-l: 12px; + /* font tiers: fine print+labels / body+buttons / title */ + --fs-s: 12px; + --fs-m: 13px; + --fs-l: 15px; + /* elevation: badge / floating panel (menu, tip, toast) / dialog */ + --shadow-1: 0 1px 3px rgba(0, 0, 0, 0.45); + --shadow-2: 0 6px 22px rgba(0, 0, 0, 0.45); + --shadow-3: 0 8px 30px rgba(0, 0, 0, 0.5); + } + ha-card { + overflow: visible; /* overflow:hidden breaks position:sticky on the header */ + } + .empty { + padding: 40px 24px; + color: var(--hp-txt); + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + gap: var(--sp-4); + } + .empty .big { + --mdc-icon-size: 56px; + color: var(--hp-accent); + opacity: 0.7; + } + .empty .muted { + color: var(--hp-muted); + font-size: var(--fs-m); + margin: 0; + } + .empty .btn { + margin-top: var(--sp-4); + } + @keyframes fixedfloor-spin { + to { transform: rotate(360deg); } + } + .spacer { + flex: 1; + } + .bootveil { + position: absolute; + inset: 0; + z-index: 40; + display: flex; + align-items: center; + justify-content: center; + background: var(--hp-bg, #16212e); + opacity: 1; + transition: opacity 0.15s ease; + pointer-events: none; + } + .bootveil.off { + opacity: 0; + } + .bootveil .boothouse { + position: static; /* the main .zoomwrap SVG is pinned to inset:0 — not this icon */ + width: 56px; + height: 56px; + fill: var(--hp-accent); + opacity: 0.85; + animation: hp-boot-pulse 1.3s ease-in-out infinite; + } + @keyframes hp-boot-pulse { + 0%, 100% { opacity: 0.3; transform: scale(0.94); } + 50% { opacity: 0.9; transform: scale(1); } + } + @media (prefers-reduced-motion: reduce) { + .bootveil .boothouse { + animation: none; + opacity: 0.7; + } + } + @keyframes hp-pulse-short { + 0% { transform: scale(1); opacity: 0.55; } + 70% { opacity: 0.22; } + 100% { transform: scale(var(--ripple-scale, 1.5)); opacity: 0; } + } + /* Alternate identity: a rapid retrigger restarts the browser timeline. */ + @keyframes hp-pulse-short-b { + 0% { transform: scale(1); opacity: 0.55; } + 70% { opacity: 0.22; } + 100% { transform: scale(var(--ripple-scale, 1.5)); opacity: 0; } + } + @keyframes hp-pulse-continuous { + 0% { transform: scale(1); opacity: 0.55; } + 65% { opacity: 0.18; } + 100% { transform: scale(var(--ripple-scale, 1.5)); opacity: 0; } + } + @keyframes hp-pulse-alarm { + 0% { transform: scale(1); opacity: 0.72; } + 100% { transform: scale(1.5); opacity: 0; } + } + /* Space changes keep their direction, but travel only a few percent: the + motion explains what changed without making the whole plan fly around. */ + @keyframes hp-slide-left { + 0% { transform: translateX(4%); opacity: 0.72; } + 100% { transform: translateX(0); opacity: 1; } + } + @keyframes hp-slide-right { + 0% { transform: translateX(-4%); opacity: 0.72; } + 100% { transform: translateX(0); opacity: 1; } + } + @keyframes hp-sunfade-in { + from { opacity: 0; } + to { opacity: 1; } + } + @keyframes hp-sunfade-out { + from { opacity: 1; } + to { opacity: 0; } + } + .sr-only { + position: absolute !important; + width: 1px !important; + height: 1px !important; + padding: 0 !important; + margin: -1px !important; + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + white-space: nowrap !important; + border: 0 !important; + } + .toast { + position: fixed; + pointer-events: none; + left: 50%; + bottom: 22px; + transform: translateX(-50%); + background: var(--hp-bg); + border: 1px solid var(--hp-accent); + color: var(--hp-txt); + padding: var(--sp-4) var(--sp-6); + border-radius: var(--rad-l); + font-size: var(--fs-m); + box-shadow: var(--shadow-2); + z-index: 120; + max-width: 90vw; + } +`,o` + .fixedfloor-loading { + animation: fixedfloor-spin 1.1s linear infinite; + } + .fixedfloor-error p { + max-width: 42rem; + margin: 0; + overflow-wrap: anywhere; + } + @media (prefers-reduced-motion: reduce) { + .fixedfloor-loading { animation: none; } + } + .stage.noplan { + background: #ffffff; + } + /* Editors always keep the white drawing sheet — even when a backdrop + image is loaded. Without this, a picture plan painted the theme card + colour under the grid (owner 2026-08-05). View keeps the historical + theme / .noplan split. */ + .stage.mode-plan, + .stage.mode-devices, + .stage.mode-decor { + background: #ffffff; + } + .stage { + position: relative; + width: 100%; + container-type: inline-size; + overflow: hidden; + touch-action: none; /* custom pinch/pan gestures */ + background: var(--ha-card-background, var(--card-background-color, #111)); + } + .zoomwrap { + position: absolute; + inset: 0; + z-index: 1; + } + .stage.mode-transition, + .stage.mode-transition .zoomwrap { + transition: none !important; + } + .stage.mode-transition .hp-view-only-layer, + .stage.mode-transition .hp-editor-only-layer { + pointer-events: none; + will-change: opacity; + } + .stage.mode-transition .hp-paper { + fill: var(--hp-mode-paper) !important; + } + /* Four-phase environment (#146): constant layers cross-fade because CSS + gradients themselves are not reliably interpolated. Everything stays + behind the plan; the plan tree is never dimmed, tinted or faded. */ + .hp-day-cycle-env { + position: absolute; + inset: 0; + z-index: 0; + overflow: hidden; + pointer-events: none; + isolation: isolate; + } + .hp-day-cycle-bg { + position: absolute; + inset: 0; + opacity: 0; + overflow: hidden; + transition: opacity 1100ms cubic-bezier(.22, .61, .36, 1); + } + .hp-day-cycle-bg.active { opacity: 1; } + .hp-day-cycle-sun { + position: absolute; + left: var(--hp-day-cycle-sun-x); + top: var(--hp-day-cycle-sun-y); + width: clamp(140px, 25cqw, 250px); + height: clamp(140px, 25cqw, 250px); + transform: translate(-50%, -50%); + border-radius: 50%; + filter: blur(10px); + opacity: var(--hp-day-cycle-sun-opacity); + transition: + left 1100ms cubic-bezier(.22, .61, .36, 1), + top 1100ms cubic-bezier(.22, .61, .36, 1), + opacity 1100ms cubic-bezier(.22, .61, .36, 1); + } + .stage.mode-transition .hp-day-cycle-env { transition: none; } + .stage.daycycle .hp-paperg, + .hp-static-stage.daycycle .hp-paperg { + filter: + drop-shadow(0 0 1px var(--hp-day-cycle-outline-near)) + drop-shadow(0 0 5px var(--hp-day-cycle-outline-mid)) + drop-shadow(0 0 10px var(--hp-day-cycle-outline-far)); + transition: filter 1100ms cubic-bezier(.22, .61, .36, 1); + } + @media (prefers-reduced-motion: reduce) { + .hp-day-cycle-bg, + .hp-day-cycle-sun, + .stage.daycycle .hp-paperg, + .hp-static-stage.daycycle .hp-paperg { + transition: none; + } + } + .sunlayer { + pointer-events: none; + } + .compass { + width: 120px; + height: 120px; + flex: none; + touch-action: none; + cursor: grab; + user-select: none; + } + .compass:active { + cursor: grabbing; + } + .compass .cring { + fill: rgba(255, 255, 255, 0.04); + stroke: var(--divider-color, #444); + stroke-width: 2; + } + .compass .ctick { + stroke: var(--secondary-text-color, #9aa4ad); + stroke-width: 2; + } + .compass .ctick.minor { + stroke-width: 1; + opacity: 0.6; + } + .compass .cneedle line { + stroke: var(--primary-color, #3ea6ff); + stroke-width: 2.5; + stroke-linecap: round; + } + .compass .cneedle path { + fill: var(--primary-color, #3ea6ff); + } + .compass .cneedle text { + fill: var(--text-primary-color, #fff); + font-size: 11px; + font-weight: 700; + } + .compass .cdeg { + fill: var(--secondary-text-color, #9aa4ad); + font-size: 13px; + } + .compass.unset .cneedle { + opacity: 0.35; + } + /* HP-1552: first-open boot veil — the plan hides until the stage height settles */ + .stage.hpboot .zoomwrap, + .stage.hpboot .zoombadge { + visibility: hidden; + } + /* AUD-1552-02: post-veil grace — HA chrome landing after the cap moves + the stage height smoothly; the viewport ResizeObserver refits the plan + along the transition, so a late panel glides instead of jumping. */ + .stage.hpsettle { + transition: height 0.25s ease; + } + @media (prefers-reduced-motion: reduce) { + .stage.hpsettle { + transition: none; + } + } + .zoomctl { + display: inline-flex; + gap: var(--sp-1); + background: rgba(127, 127, 127, 0.12); + border-radius: var(--rad-m); + padding: var(--sp-1); + } + .zoomctl .zb { + border: none; + padding: var(--sp-3) var(--sp-4); + } + .zoomctl .zb[disabled] { + opacity: 0.4; + pointer-events: none; + } + .projection-toggle { + min-width: 44px; + min-height: 44px; + justify-content: center; + padding: var(--sp-3); + } + /* docs/CANVAS.md §5: the plane has no edges, so you can pan until nothing + is on screen. One pointer home, one click back. */ + .homearrow { + position: absolute; + z-index: 12; + transform: translate(-50%, -50%); + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + background: var(--card-background-color, var(--hp-bg)); + color: var(--hp-accent); + border: 1px solid var(--hp-accent); + opacity: 0.9; + padding: 0; + } + .homearrow ha-icon { + --mdc-icon-size: 22px; + line-height: 1; + } + .zoombadge { + position: absolute; + z-index: 12; + left: var(--sp-4); + bottom: var(--sp-4); + background: var(--card-background-color, var(--hp-bg)); + opacity: 0.92; + color: var(--hp-txt); + border: 1px solid var(--hp-accent); + border-radius: var(--rad-m); + padding: var(--sp-1) var(--sp-4); + font-size: var(--fs-s); + font-weight: 600; + pointer-events: none; + } + /* Only the plan canvas owns the whole zoom wrapper. Context trays now live + inside .stage as well, and may contain small SVG previews (furniture in + particular); the old descendant-wide stage-SVG selector stretched every + preview over the tray and stacked them into one blocking artefact. The + vacuum trail/fit overlays have their own explicit geometry below. */ + .zoomwrap > svg { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + display: block; + } + .plan-svg { z-index: 1; } + .iso-underlay-svg { z-index: 0; overflow: visible; } + .iso-shadows-svg { z-index: 3; overflow: visible; } + .iso-walls-svg { + z-index: 4; + overflow: visible; + } + .iso-underlay-svg, + .iso-shadows-svg, + .iso-walls-svg, + .iso-underlay, + .iso-shadows, + .iso-walls, + .iso-openings { + pointer-events: none; + } + .iso-side-hi { stop-color: #b9bdbe; } + .iso-side-lo { stop-color: #969c9f; } + .iso-top-hi { stop-color: #fafaf7; } + .iso-top-lo { stop-color: #e2e4e2; } + .iso-wall-side { + fill: url(#hp-iso-wall-side) #a8acae; + stroke: #92989b; + stroke-width: 0.7; + vector-effect: non-scaling-stroke; + } + .iso-wall-top { + fill: url(#hp-iso-wall-top) #f3f3f1; + stroke: #d7d9d8; + stroke-width: 0.8; + vector-effect: non-scaling-stroke; + } + .iso-floor-side { + fill: #858b8d; + stroke: #71787b; + stroke-width: 0.7; + vector-effect: non-scaling-stroke; + } + .iso-opening-panel { + fill: #d7d9d7; + fill-opacity: 0.96; + stroke: #7f878b; + stroke-width: 0.9; + vector-effect: non-scaling-stroke; + } + .iso-opening-panel.iso-window { + fill: #dfeff4; + fill-opacity: 0.72; + stroke: #8aa7b1; + } + .iso-ambient-shadow { + fill: rgba(15, 21, 25, 0.22); + filter: url(#hp-iso-ambient-shadow); + } + .iso-contact-shadow { + fill: none; + stroke: rgba(22, 28, 31, 0.25); + stroke-width: 3; + filter: url(#hp-iso-contact-shadow); + vector-effect: non-scaling-stroke; + } + .iso-leaf-shadow { + fill: none; + stroke: rgba(18, 23, 27, 0.24); + stroke-width: 4; + filter: url(#hp-iso-leaf-shadow); + vector-effect: non-scaling-stroke; + } + @media (prefers-color-scheme: dark) { + .iso-side-hi { stop-color: #4c555a; } + .iso-side-lo { stop-color: #343c40; } + .iso-top-hi { stop-color: #687176; } + .iso-top-lo { stop-color: #50585d; } + .iso-wall-side { stroke: #30373b; } + .iso-wall-top { stroke: #7b858a; } + .iso-floor-side { fill: #2d3438; stroke: #20272a; } + .iso-opening-panel { fill: #626b70; stroke: #899399; } + .iso-opening-panel.iso-window { fill: #75919b; stroke: #abc6ce; } + .iso-ambient-shadow { fill: rgba(0, 0, 0, 0.34); } + .iso-contact-shadow, .iso-leaf-shadow { stroke: rgba(0, 0, 0, 0.38); } + } + @media (forced-colors: active) { + .iso-wall-side, .iso-wall-top, .iso-floor-side, .iso-opening-panel { + fill: Canvas; + stroke: CanvasText; + forced-color-adjust: auto; + } + .iso-ambient-shadow, .iso-contact-shadow, .iso-leaf-shadow { display: none; } + } + @supports not (filter: blur(1px)) { + .iso-ambient-shadow, .iso-contact-shadow, .iso-leaf-shadow { display: none; } + } + /* Opaque plan paper (owner 2026-08-03): the scene bg_color / daynight sky + shows ONLY around the plan, never through it. The colour is the + pre-bg_color canvas — the theme card background under an image plan, + plain white for a hand-drawn one (.stage.noplan). On drawn plans the + paper is per-room shapes following the room contours — fill only, + never a stroke, so the paper cannot poke past a wall. Night dimming + comes from the .zoomwrap brightness filter, never from alpha + (docs/SUN.md). */ + .hp-paper { + fill: var(--ha-card-background, var(--card-background-color, #111)); + stroke: none; + } + .stage.noplan .hp-paper, + .stage.mode-plan .hp-paper, + .stage.mode-devices .hp-paper, + .stage.mode-decor .hp-paper { + fill: #ffffff; + } + /* Owner 2026-08-04: «углы границ комнат всё ещё с зубцами». A miter join + on a 30-45° corner shoots a spike far past the wall (and flips to an + ugly bevel once past the miter limit) — the same defect the decor lines + had before they got round caps. Every room border, in EVERY renderer + that reuses these styles (plan view, plan editor, static space-card), + joins its walls with a ROUND join instead: the corner reads as the + stroke's own radius, never as a tooth. The linecap matters only for the + open outlines below, but it costs nothing to state it here. */ + .room { + transition: 0.12s; + cursor: default; /* v1.40.1: rooms are not clickable — the label's link icon is */ + stroke-linejoin: round; + stroke-linecap: round; + } + .room.overlay { + fill: transparent; + stroke: transparent; + stroke-width: calc(2px * var(--hp-cell-visual-scale, 1)); + } + :host([data-pointer-hover]) .stage.mode-view .room.overlay:not(.styled):hover { + stroke: var(--hp-accent); + stroke-opacity: 1; + } + .room.yard { + fill: rgba(75, 140, 90, 0.14); + stroke: #4b8c5a; + stroke-width: calc(2px * var(--hp-cell-visual-scale, 1)); + } + :host([data-pointer-hover]) .stage.mode-view .room.yard:not(.styled):hover { + stroke: var(--hp-accent); + stroke-opacity: 1; + } + .room.styled { + stroke: var(--room-stroke, transparent); + stroke-opacity: var(--room-stroke-op, 0); + stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); + fill: var(--room-fill, transparent); + fill-opacity: var(--room-fill-op, 0); + } + .glow-base-layer, + .glow-base-tunnels, + .glow-pools-frame, + .glow-pools, + .glow-spot { + pointer-events: none; + } + /* The parent isolates all source spots from the room data fill, Glow base, + paper and backdrop. A spot is one circle clipped to the floor its lamp + can see, so the whole spot screen-blends as a single primitive — there is + no mask left to be dropped on a promoted layer. Per-stop alpha already + contains the shared 0.7 ceiling; the spot opacity below only animates + between 0 and 1 and is never another persistent alpha ceiling. */ + .glow-pools-frame, + .glow-pools, + .glow-spot { + isolation: isolate; + } + .glow-pools.blend-screen .glow-spot { + mix-blend-mode: screen; + } + .glow-spot { + opacity: 1; + transition: opacity 500ms ease; + } + .glow-spot.is-entering, + .glow-spot.is-leaving { + opacity: 0; + } + @media (prefers-reduced-motion: reduce) { + .glow-spot { + transition: none; + } + } + /* The explicit late room-hover layer owns the wash and halo. Keeping CSS + filters off room paths prevents Chromium from recompositing the sibling + screen-blended Glow layer for one bright frame on every hover. */ + :host([data-pointer-hover]) .stage.mode-view .room.styled:hover { + stroke: var(--hp-accent); + stroke-opacity: 1; + } + /* doors, windows & gates */ + .op-leaf { + transition: transform 0.6s ease; + } + .op-arc { + stroke-width: calc(1.5px * var(--hp-cell-visual-scale, 1)); + transition: stroke-dashoffset 0.6s ease; + } + /* hover affordance: a rounded outline hugging the wall strip + a grab cursor */ + .op-outline { + fill: none; + stroke: var(--hp-accent); + stroke-width: calc(1.5px * var(--hp-cell-visual-scale, 1)); + opacity: 0; + transition: opacity 0.15s; + pointer-events: none; + } + :host([data-pointer-hover]) .stage.markup g.opening:hover .op-outline { + opacity: 0.9; + } + /* openings are pure status graphics outside Plan mode: no cursor, no hover, + no hit target — View must not interact with them at all */ + .op-hit { + fill: transparent; + pointer-events: none; + cursor: default; + } + .stage.markup .op-hit { + pointer-events: auto; + cursor: grab; + touch-action: none; /* drags, not scrolls, on touch */ + } + .stage.markup .op-hit:active { + cursor: grabbing; + } + .stage.markup .opening.orphan { + pointer-events: auto; + cursor: pointer; + color: var(--error-color, #db4437); + } + .stage.markup .opening.orphan circle { + fill: var(--hp-bg); + stroke: currentColor; + stroke-width: calc(2px * var(--hp-cell-visual-scale, 1)); + } + .stage.markup .opening.orphan text { + fill: currentColor; + font-weight: 800; + font-size: calc(12px * var(--hp-cell-visual-scale, 1)); + pointer-events: none; + } + /* HP-1550-04: in the resize tool the wall handles own the hit test — the + transparent .op-hit of a door at the midpoint of a wall used to sit ON + TOP of the handle and made that wall ungrabbable for both rooms. + Openings are not editable in this tool (they ride along with the wall), + so their hit area goes fully inert; every other Plan tool is untouched. */ + .stage.markup.tool-resize .op-hit { + pointer-events: none; + cursor: default; + } + .stage.mode-view .oplock { + pointer-events: auto; + cursor: pointer; + } + @media (prefers-reduced-motion: reduce) { + .op-leaf, .op-arc { transition: none; } + } + .roomlabel { + pointer-events: none; /* draggable only in plan mode (rule below) */ + position: absolute; + transform: translate(-50%, -50%); + font-size: calc(var(--rl-icon-size, var(--icon-size, 2.5cqw)) * 0.5 * var(--rl-scale, 1) * var(--rl-font, 1) * var(--rl-space, 1)); + font-weight: 700; + letter-spacing: 0.04em; + white-space: nowrap; + cursor: grab; + user-select: none; + z-index: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.15em; + text-align: center; + } + .rlname { + display: inline-flex; + align-items: center; + gap: 0.25em; + font-size: calc(1em * var(--rl-name, 1)); + } + .zoomwrap.slide-left { animation: hp-slide-left 0.18s cubic-bezier(0.2, 0.7, 0.2, 1); } + .zoomwrap.slide-right { animation: hp-slide-right 0.18s cubic-bezier(0.2, 0.7, 0.2, 1); } + @media (prefers-reduced-motion: reduce) { + .zoomwrap.slide-left, + .zoomwrap.slide-right { animation: none; } + } + /* The name is the anchor: the label box is centred on the room point, so + anything that takes part in its layout SHIFTS THE NAME. The gear button + and the metrics hang below as absolutes — the name renders in exactly + the same place in view mode and in the plan editor (owner's request), + and the button sits at the very bottom of the card. */ + /* Standalone, centred on the room, sized from the device icon: icon-size + already rescales with the view, so the button zooms WITH the plan + instead of keeping a constant screen size (owner's spec). */ + .rlgearbtn { + --gear-h: calc(var(--icon-size, 2.5cqw) * 0.77); /* owner: half the previous size */ + position: absolute; + /* dead-centred on the room, both axes (owner's spec) */ + transform: translate(-50%, -50%); + display: inline-flex; + align-items: center; + gap: 0.35em; + height: var(--gear-h); + padding: 0 calc(var(--gear-h) * 0.38); + border: 0; + border-radius: 999px; + background: var(--hp-accent); + color: var(--text-primary-color, #fff); + font: inherit; + font-size: calc(var(--gear-h) * 0.42); + font-weight: 600; + line-height: 1; + white-space: nowrap; + cursor: pointer; + pointer-events: auto; + opacity: 0.92; + box-shadow: var(--shadow-1); + z-index: 2; + } + .rlgearbtn { transition: opacity 0.15s, filter 0.15s; } + :host([data-pointer-hover]) .rlgearbtn:hover { opacity: 1; filter: brightness(1.18); } + .rlgearbtn ha-icon { --mdc-icon-size: calc(var(--gear-h) * 0.55); display: inline-flex; } + .rlgear { + --mdc-icon-size: 0.9em; + display: inline-flex; + margin-right: 0.2em; + opacity: 0.6; + cursor: pointer; + pointer-events: auto; + } + :host([data-pointer-hover]) .rlgear:hover { opacity: 1; } + .rlgo { + --mdc-icon-size: 0.85em; + display: inline-flex; + opacity: 0.55; + } + .stage.mode-view .rlgo { + pointer-events: auto; + cursor: pointer; + } + :host([data-pointer-hover]) .stage.mode-view .rlgo:hover { opacity: 1; } + .roomlabel .rlmetrics { + position: absolute; /* below the name, outside the centring math */ + top: calc(100% + 0.15em); + left: 50%; + transform: translateX(-50%); + white-space: nowrap; + display: flex; + align-items: center; + gap: 0.55em; + font-size: calc(0.75em * var(--rl-meta, 1)); /* feedback: 0.62 was unreadable on a tablet */ + font-weight: 600; + letter-spacing: 0.02em; + opacity: 0.9; + } + .roomlabel .rlm { + display: inline-flex; + align-items: center; + gap: 0.12em; + } + .roomlabel .rlm ha-icon { + --mdc-icon-size: 1.05em; + display: inline-flex; + } + .roomlabel .rlm.lit { opacity: 1; } + .rlhandle { + display: none; + position: absolute; + width: 9px; + height: 9px; + border-radius: 2px; + background: var(--hp-accent); + border: 1px solid var(--card-background-color, #fff); + z-index: 2; + } + .rlhandle.tl { left: -6px; top: -6px; cursor: nwse-resize; } + .rlhandle.br { right: -6px; bottom: -6px; cursor: nwse-resize; } + .rlhandle.tr { right: -6px; top: -6px; cursor: nesw-resize; } + .rlhandle.bl { left: -6px; bottom: -6px; cursor: nesw-resize; } + :host([data-pointer-hover]) .stage.markup .roomlabel:hover .rlhandle { display: block; } + .stage.markup .roomlabel { pointer-events: auto; } + .roomlabel:active { cursor: grabbing; } + .measurelayer { + position: absolute; + inset: 0; + pointer-events: none; + } + .alignline { + stroke: var(--hp-accent); + stroke-width: calc(1.2px * var(--hp-cell-visual-scale, 1)); + stroke-dasharray: + calc(4px * var(--hp-cell-visual-scale, 1)) + calc(4px * var(--hp-cell-visual-scale, 1)); + pointer-events: none; + opacity: 0.9; + } + .aligndot { + fill: var(--hp-accent); + pointer-events: none; + } + .measurelabel.on45 { + color: #4bd28f; + border-color: #4bd28f; + } + .measurelabel { + position: absolute; + transform: translate(12px, -150%); + font-size: var(--fs-s); + font-weight: 600; + padding: 1px var(--sp-3); + border-radius: var(--rad-s); + background: rgba(0, 0, 0, 0.72); + color: #fff; + white-space: nowrap; + pointer-events: none; + user-select: none; + z-index: 3; + } + .measurelabel.opdimension { + transform: translate( + calc(-50% + var(--op-label-shift-x, 0px)), + calc(-50% + var(--op-label-shift-y, -12px)) + ); + border: 1px solid var(--hp-open, #ff9800); + } + /* decor (background) layer */ + .decorlayer .dshape { pointer-events: none; } + .stage.mode-decor .decorlayer .dshape { + pointer-events: visiblePainted; + cursor: pointer; + } + .stage.mode-decor.dtool-select .decorlayer .dshape { cursor: move; } + /* …but a DRAWING tool owns the canvas: existing shapes stop being targets + entirely, so a new line can start exactly on the end of an old one + instead of grabbing it (owner, 2026-08-04). Same for the picture tool — + a shape lying over the plan must not block the picture's own drag. + Only select (move) and erase (delete) keep the shapes clickable. */ + .stage.mode-decor.dtool-line .decorlayer .dshape, + .stage.mode-decor.dtool-rect .decorlayer .dshape, + .stage.mode-decor.dtool-ellipse .decorlayer .dshape, + .stage.mode-decor.dtool-text .decorlayer .dshape, + .stage.mode-decor.dtool-furniture .decorlayer .dshape, + .stage.mode-decor.dtool-backdrop .decorlayer .dshape { pointer-events: none; } + /* the furniture tool is a stamp: the press must reach the stage even when + it lands on a sofa that is already there (docs/FURNITURE.md §4) */ + .stage.mode-decor.dtool-furniture { cursor: copy; } + /* ONE exception (owner, 2026-08-04): under the TEXT tool an existing LABEL + is a target again — pressing it opens its editor instead of starting a + new label on top of the old one. Only labels: a line or a rectangle + under the text tool stays inert, so the press reaches the stage and a + new label is created there. */ + .stage.mode-decor.dtool-text .decorlayer .dshape.dtext, + .stage.mode-decor.dtool-select .decorlayer .dshape.dtext, + .stage.mode-decor.dtool-erase .decorlayer .dshape.dtext { + /* A label is one logical decor object. SVG's visiblePainted would hit + only the ink of individual glyphs, so spaces, counters and the area + inside its selection glow behaved like empty canvas. */ + pointer-events: visiblePainted; + pointer-events: bounding-box; + } + .stage.mode-decor.dtool-text .decorlayer .dshape.dtext { + cursor: text; + } + /* Erasing a hairline must not require pixel-perfect aim. The duplicate + geometry is invisible and exists only while Erase is active. A + non-scaling stroke keeps the target comfortably wide at every zoom. */ + .decorlayer .derasehit { + fill: none; + stroke: transparent; + stroke-width: 16px; + stroke-linecap: round; + stroke-linejoin: round; + vector-effect: non-scaling-stroke; + pointer-events: none; + } + .stage.mode-decor.dtool-erase .decorlayer .derasehit { + pointer-events: stroke; + } + /* A dashed line must remain selectable across its gaps. This proxy also + makes the only entry to line-style properties — double click in Select — + practical for hairlines at every zoom. */ + .decorlayer .dselecthit { + fill: none; + stroke: transparent; + stroke-width: 16px; + stroke-linecap: round; + vector-effect: non-scaling-stroke; + pointer-events: none; + } + .stage.mode-decor.dtool-select .decorlayer .dselecthit { + pointer-events: stroke; + cursor: move; + } + .decorlayer .dsel { + filter: drop-shadow(0 0 3px var(--hp-accent)); + } + .decorlayer .ddraft { + stroke-dasharray: + calc(6px * var(--hp-cell-visual-scale, 1)) + calc(5px * var(--hp-cell-visual-scale, 1)); + pointer-events: none; + } + .decorlayer text { + font-weight: 600; + user-select: none; + dominant-baseline: middle; + text-anchor: middle; + } + .stage.mode-decor { + outline: 2px solid #26a69a; + outline-offset: -2px; + } + /* backdrop transform frame (docs/BACKDROP.md §2). Editor chrome: the + outline never takes a pointer, the four corner handles do — and they are + finger-sized (r = 2 % of the visible view), because this is dragged on a + tablet as often as with a mouse. */ + .bdframe .bdbox { + fill: none; + stroke: var(--hp-accent); + stroke-width: 2; + stroke-dasharray: 10 7; + vector-effect: non-scaling-stroke; + pointer-events: none; + opacity: 0.9; + } + /* the HIT circle: invisible, finger-sized, the only thing that takes a + pointer. The visible bead is .bdknob, a quarter of its radius — the same + visual/hit split .dthandle + .dtknob and .rszhandle + .rszicon use. */ + .bdframe .bdhandle { + fill: transparent; + stroke: none; + pointer-events: all; + touch-action: none; + } + .bdframe .bdknob { + fill: var(--hp-accent); + stroke: #fff; + stroke-width: 1; + vector-effect: non-scaling-stroke; + pointer-events: none; + } + :host([data-pointer-hover]) .bdframe .bdhandle:hover + .bdknob { + fill: #fff; + stroke: var(--hp-accent); + } + .bdframe .bd-nwse { cursor: nwse-resize; } + .bdframe .bd-nesw { cursor: nesw-resize; } + /* the picture itself is the drag target for a move (grab, then grabbing) */ + .stage.mode-decor.bdgrab { cursor: grab; } + .stage.mode-decor.bdgrabbing, + .stage.mode-decor.bdgrabbing .bdframe .bdhandle { cursor: grabbing; } + .measurelabel.bdmeasure { + transform: translate(-50%, -50%); + border: 1px solid var(--hp-accent); + } + /* the selected decor object's frame — same chrome rules as the backdrop's: + the outline never takes a pointer, the handles always do, and they are + finger-sized because this is dragged on a tablet too */ + .dtframe .dtbox { + fill: none; + stroke: var(--hp-accent); + stroke-width: 1.5; + stroke-dasharray: 7 5; + vector-effect: non-scaling-stroke; + pointer-events: none; + opacity: 0.85; + } + .dtframe .dtstem, + .bdframe .dtstem { + stroke: var(--hp-accent); + stroke-width: 1.5; + vector-effect: non-scaling-stroke; + pointer-events: none; + opacity: 0.85; + } + /* the HIT circle: invisible, finger-sized, the only thing that takes a + pointer. The visible bead is .dtknob, a quarter of its radius — the + same visual/hit split .rszhandle + .rszicon use. */ + .dtframe .dthandle { + fill: transparent; + stroke: none; + pointer-events: all; + touch-action: none; + } + .dtframe .dtknob { + fill: var(--hp-accent); + stroke: #fff; + stroke-width: 1; + vector-effect: non-scaling-stroke; + pointer-events: none; + } + :host([data-pointer-hover]) .dtframe .dthandle:hover + .dtknob { + fill: #fff; + stroke: var(--hp-accent); + } + .dtframe .dt-nwse { cursor: nwse-resize; } + .dtframe .dt-nesw { cursor: nesw-resize; } + .dtframe .dtrot { cursor: grab; } + .dtframe .dtendpoint { cursor: crosshair; } + .bdframe .dtrot { cursor: grab; } + .dtarea { + resize: vertical; + min-height: 3.4em; + font: inherit; + line-height: 1.35; + } + .stage.mode-decor.dtool-line, .stage.mode-decor.dtool-rect, + .stage.mode-decor.dtool-ellipse, .stage.mode-decor.dtool-text { + cursor: crosshair; + } + .stage.mode-decor.dtool-erase, + .stage.mode-decor.dtool-erase .decorlayer .dshape { + cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cg transform='rotate(-45 12 12)'%3E%3Crect x='7' y='2' width='10' height='18' rx='2' fill='%23fff' stroke='%23111' stroke-width='1.5'/%3E%3Cpath d='M7 13h10v5a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2z' fill='%23ff9f43' stroke='%23111' stroke-width='1.5'/%3E%3C/g%3E%3C/svg%3E") 5 22, pointer; + } + .stage.mode-decor .room, .stage.mode-decor .devlayer { pointer-events: none; } + .stage.mode-decor .oplock { pointer-events: none; } + /* Backdrop-editor de-emphasis is a shared mode-transition coordinate. + It multiplies whole presentation groups and never changes Glow source + alpha, additive blending, or the underlying resolved state. */ + .stage .room, + .stage .devlayer, + .stage .opening, + .stage .room-outline, + .stage .wallbodies, + .stage .opening-tunnels, + .stage .glow-base-layer, + .stage .glow-pools-frame, + .stage .zero-walls { + opacity: var(--hp-mode-architecture-opacity, 1); + } + .opening-preview { + opacity: 0.5; + pointer-events: none; + } + .opening-preview[data-kind="passage"] { + opacity: 1; + } + .passage-preview-cut { + fill: var(--wall-fill, #ffffff); + fill-opacity: 0.35; + stroke: none; + pointer-events: none; + } + .passage-preview-boundary { + stroke: var(--hp-open, #ff9800); + stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); + pointer-events: none; + } + .opening-preview .op-leaf, + .opening-preview .op-arc { + transition: none; + } + .opening-preview-dot { + fill: var(--hp-open, #ff9800); + pointer-events: none; + } + .opening-dimensions, + .opening-dimension { + pointer-events: none; + } + .opening-dimension-line, + .opening-dimension-tick { + fill: none; + stroke: var(--hp-open, #ff9800); + stroke-width: 1.5; + vector-effect: non-scaling-stroke; + pointer-events: none; + } + .opening-dimension-tick { + stroke-width: 2; + } + .stage.edit .room { + pointer-events: none; + } + .stage.markup { + cursor: crosshair; + } + /* room-picking stages: merge (both clicks) and split before a room is chosen */ + .stage.markup.tool-merge, + .stage.markup.tool-split.pickstage, + .stage.markup.tool-delroom { + cursor: pointer; + } + .stage.markup.tool-wallthick { cursor: default; } + .stage.markup.tool-wallthick.wallhot { cursor: pointer; } + /* Solid wall colour sits under the hatch (owner: both, not either/or). */ + .wallbody-fill { + fill: var(--wall-fill, #ffffff); + fill-opacity: var(--wall-fill-op, 1); + fill-rule: evenodd; + stroke: none; + pointer-events: none; + } + .wallbody { + fill: url(#hp-wall-hatch); + fill-rule: evenodd; + stroke: var(--room-stroke, var(--hp-muted)); + stroke-width: calc(0.6px * var(--hp-cell-visual-scale, 1)); + pointer-events: none; + } + /* Thin-on-screen: hatch collapses into noise — fill alone, stroke from fill path's sibling. */ + .wallbody.solid { + fill: none; + } + .wallthick-hover { + fill: var(--accent-color, #03a9f4); + fill-opacity: 0.38; + stroke: var(--accent-color, #03a9f4); + stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); + stroke-linejoin: round; + stroke-linecap: round; + pointer-events: none; + } + .wallthick-hover.isopen { + fill: var(--error-color, #f44336); + stroke: var(--error-color, #f44336); + } + .zero-wall { + stroke: var(--zero-wall-stroke, var(--hp-muted)); + stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); + stroke-dasharray: + calc(7px * var(--hp-cell-visual-scale, 1)) + calc(7px * var(--hp-cell-visual-scale, 1)); + stroke-linecap: butt; + pointer-events: none; + opacity: 0.9; + } + .zero-walls.solid .zero-wall { + stroke-dasharray: none; + } + /* Rooms with zero/thick stretches: the polygon's own stroke is fully off. + The trimmed .room-outline draws normal walls; View hover gets its own + top overlay after the wall bodies. */ + .room.noedge { + stroke-opacity: 0 !important; + } + /* rooms with open boundaries draw their walls as separate M..L subpaths, + so a corner between two of them is two stroke ENDS meeting: round caps + fill it in the same way a round join fills a closed contour's corner + (owner 2026-08-04 — no teeth anywhere on a room border). */ + .room-outline { + fill: none; + stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); + stroke-linejoin: round; + stroke-linecap: round; + pointer-events: none; + } + /* Plan editor: trimmed outlines use the markup blue */ + .room-outline.outlined { + stroke: rgba(62, 166, 255, 0.55); + stroke-opacity: 1; + } + /* The wash is rendered directly above the resolved room fill but below + tunnels, Glow, sun and walls. Black at 22% reproduces the old + brightness(.78) contract without filtering/promoting an SVG ancestor or + changing the room's hue. The halo/outline are rendered again late, above + the wall bodies. */ + .room-hover-fill { + fill: #000; + fill-opacity: 0.22; + pointer-events: none; + } + .room-hover-halo { + fill: none; + stroke: var(--hp-accent); + stroke-opacity: 0.28; + stroke-width: calc(8px * var(--hp-cell-visual-scale, 1)); + stroke-linejoin: round; + stroke-linecap: round; + pointer-events: none; + } + .room-hover-outline { + fill: none; + stroke: var(--hp-accent); + stroke-width: calc(3px * var(--hp-cell-visual-scale, 1)); + stroke-linejoin: round; + stroke-linecap: round; + pointer-events: none; + } + .stage.markup .room { + pointer-events: none; + } + .stage.markup .devlayer .dev { + display: none; /* in plan mode the icons do not get in the way; labels stay */ + } + /* mode frames: the edit modes are visible at a glance */ + .stage.mode-plan { + outline: 2px solid #ffc14d; + outline-offset: -2px; + } + .stage.mode-devices { + outline: 2px solid var(--hp-accent); + outline-offset: -2px; + } + .room.outlined { + stroke: rgba(62, 166, 255, 0.55); + fill: rgba(62, 166, 255, 0.06); + } + /* AFTER .outlined: same specificity — source order decides (gotcha x4) */ + .room.picked { + stroke: #ffc14d; + stroke-width: calc(3px * var(--hp-cell-visual-scale, 1)); + fill: rgba(255, 193, 77, 0.25); + } + /* Owner 2026-08-04: the grid is a HINT, not content — at full strength the + dots argued with the plan on white paper. Both levels are muted, the + hierarchy is kept (major still denser than fine). */ + .griddot { + fill: var(--hp-accent); + opacity: 0.35; + stroke: rgba(0, 0, 0, 0.35); + stroke-width: 0.4; + } + /* docs/CANVAS.md §7: every coarse node (5x/10x the live step) keeps a + bigger, more opaque dot, so zoomed far out the grid still reads as a + grid instead of a grey wash. */ + .griddot.major { + opacity: 0.5; + stroke-width: 0; + } + /* the contour being drawn in the Plan editor: each wall is its own , + so the round cap IS the corner (matches the finished .room border) */ + .seg { + stroke: var(--hp-accent); + stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); + stroke-linecap: round; + stroke-linejoin: round; + } + .pathline { + stroke: #ffc14d; + stroke-width: calc(3px * var(--hp-cell-visual-scale, 1)); + fill: none; + stroke-linecap: round; + stroke-linejoin: round; + } + .preview { + stroke: #ffc14d; + stroke-width: calc(2px * var(--hp-cell-visual-scale, 1)); + stroke-dasharray: + calc(6px * var(--hp-cell-visual-scale, 1)) + calc(5px * var(--hp-cell-visual-scale, 1)); + opacity: 0.7; + } + .active-axis { + stroke: #ffc14d; + stroke-width: 2; + stroke-linecap: round; + vector-effect: non-scaling-stroke; + pointer-events: none; + } + .active-vertex { + fill: #ffc14d; + stroke: #171006; + stroke-width: 2; + vector-effect: non-scaling-stroke; + pointer-events: none; + } + .wall-repair-preview { + stroke: #d93025; + stroke-width: 4; + stroke-linecap: round; + stroke-dasharray: 5 4; + vector-effect: non-scaling-stroke; + pointer-events: none; + } + .drawwall-preview-fill { + fill: var(--wall-fill, #ffffff); + fill-opacity: calc(var(--wall-fill-op, 1) * 0.55); + fill-rule: evenodd; + stroke: none; + pointer-events: none; + } + .drawwall-preview { + fill: url(#hp-wall-hatch); + fill-opacity: 0.55; + fill-rule: evenodd; + stroke: var(--accent-color, #03a9f4); + stroke-width: calc(0.5px * var(--hp-cell-visual-scale, 1)); + stroke-opacity: 0.7; + pointer-events: none; + } + .drawwall-zero-preview { + fill: none; + stroke: var(--accent-color, #03a9f4); + stroke-width: calc(3px * var(--hp-cell-visual-scale, 1)); + stroke-linecap: butt; + stroke-linejoin: round; + opacity: 0.72; + pointer-events: none; + } + .drawwall-zero-preview.dashed { + stroke-dasharray: + calc(7px * var(--hp-cell-visual-scale, 1)) + calc(7px * var(--hp-cell-visual-scale, 1)); + } + .vertex { + fill: #ffc14d; + stroke: #4a2800; + stroke-width: calc(1px * var(--hp-cell-visual-scale, 1)); + } + .vertex.first { + fill: #4bd28f; + stroke: #04121f; + } + .plan-snap-overlay, + .plan-snap-overlay *, + .hidden-wall-diagnostic, + .hidden-wall-diagnostic * { + pointer-events: none; + } + .plan-snap-line, + .hidden-wall-line { + fill: none; + stroke: color-mix(in srgb, var(--hp-accent) 82%, white 18%); + /* Explicit non-scaling-stroke in the SVG keeps this one screen pixel. */ + stroke-width: 1; + stroke-linecap: round; + opacity: 0.92; + } + .plan-snap-node, + .hidden-wall-node { + fill: var(--ha-card-background, var(--card-background-color, #fff)); + stroke: color-mix(in srgb, var(--hp-accent) 88%, #07131c 12%); + stroke-width: 1; + vector-effect: non-scaling-stroke; + } + .plan-snap-node.active { + fill: #ffc14d; + stroke: #171006; + stroke-width: 2; + } + .plan-snap-node.active.dynamic { + fill: #4bd28f; + stroke: #04121f; + } + .plan-snap-node.conflict { + fill: #fff; + stroke: #d93025; + stroke-width: 3; + } + @media (prefers-color-scheme: dark) { + .plan-snap-line { + stroke: color-mix(in srgb, var(--hp-accent) 72%, white 28%); + } + .hidden-wall-line { + stroke: color-mix(in srgb, var(--hp-accent) 72%, white 28%); + } + .plan-snap-node, + .hidden-wall-node { + fill: #17242c; + stroke: #9bdcf5; + } + .plan-snap-node.active { + fill: #ffc14d; + stroke: #fff4d6; + } + .plan-snap-node.active.dynamic { + fill: #4bd28f; + stroke: #eafff4; + } + } + @media (forced-colors: active) { + .plan-snap-line, + .hidden-wall-line { + stroke: CanvasText; + opacity: 1; + forced-color-adjust: auto; + } + .plan-snap-node, + .hidden-wall-node { + fill: Canvas; + stroke: CanvasText; + forced-color-adjust: auto; + } + .plan-snap-node.active, + .plan-snap-node.active.dynamic { + fill: Highlight; + stroke: HighlightText; + forced-color-adjust: auto; + } + } + /* RGB lights: the bulb takes the light's actual color */ + /* v1.52.0: the RGB tint of the icon/border is gone — a lamp's colour + lives ONLY in its glow spot (owner's rule). The ripple-color fallback + keeps using the light colour; that is set inline via --ripple-color. */ + /* Sun wedges (docs/SUN.md). The layer is present ONLY above the 3° + threshold; crossing it fades the whole layer in or out over EXACTLY + 2 s (owner 2026-08-03 — RAY_FADE_MS in src/sun.ts must match). The + geometry is untouched: this is a plain opacity animation on the group, + so overlapping wedges keep their own blending while it plays. */ + .sunlayer { + animation: hp-sunfade-in 2s linear both; + } + .sunlayer.out { + animation: hp-sunfade-out 2s linear both; + } + @media (prefers-reduced-motion: reduce) { + /* no fade at all: the rays are simply there or simply gone */ + .sunlayer, .sunlayer.out { animation: none; } + .sunlayer.out { opacity: 0; } + } + .devlayer { + position: absolute; + inset: 0; + pointer-events: none; + z-index: 6; + } + .stage.mode-devices .dev { cursor: grab; } + .stage.mode-devices .dev:active { cursor: grabbing; } + .physical-hit { + fill: transparent; + stroke: transparent; + pointer-events: none; + cursor: grab; + } + .stage.tool-select .physical-hit { pointer-events: all; } + .physical-hit:active { cursor: grabbing; } + line.physical-hit { cursor: pointer; } + .physical-hit.selected { + fill: rgba(255, 193, 77, 0.24); + stroke: transparent; + } + line.physical-hit.selected { + stroke: transparent; + } + .physical-drag { + fill: rgba(255, 193, 77, 0.38); + stroke: #ffc14d; + stroke-width: 2.5; + vector-effect: non-scaling-stroke; + pointer-events: none; + } + .physical-chrome { pointer-events: none; } + .physical-chrome .frame, + .physical-chrome .stem { + fill: none; + stroke: #ffc14d; + stroke-width: 2; + vector-effect: non-scaling-stroke; + } + .physical-chrome .frame { fill: rgba(255, 193, 77, 0.22); } + .physical-chrome polyline.frame { fill: none; } + .physical-chrome .move-dot { + fill: #ffc14d; + stroke: #24262d; + stroke-width: 1.5; + vector-effect: non-scaling-stroke; + } + .physical-chrome .rotate-handle { + fill: #24262d; + stroke: #ffc14d; + stroke-width: 2.5; + vector-effect: non-scaling-stroke; + pointer-events: all; + cursor: crosshair; + } + .drawwall.invalid input { border-color: var(--error-color, #db4437); } + .drawwall .rangehint { margin-inline-start: 4px; font-size: 0.78em; opacity: 0.72; } + .drawwall.invalid .rangehint { color: var(--error-color, #db4437); opacity: 1; } + .stage.markup.tool-partition, + .stage.markup.tool-column { cursor: crosshair; } + /* room resize tool (docs/RESIZE.md) */ + /* wall handle: invisible finger-sized hit circle (HP-1550-04 hit priority + kept), the visible glyph lives in the sibling .rszicon */ + .rszhandle { + fill: transparent; + stroke: none; + pointer-events: all; + cursor: grab; + touch-action: none; + } + .rszhandle:active { cursor: grabbing; } + .rszhandle.disabled, + .rszhandle.disabled:active { + cursor: not-allowed; + } + /* wall-with-arrows glyph: accent ink over a bg halo, readable on any plan */ + .rszicon { pointer-events: none; } + .rszicon path { + fill: none; + stroke-linecap: round; + stroke-linejoin: round; + vector-effect: non-scaling-stroke; + } + .rszhalo { stroke: var(--hp-bg); stroke-width: 6; } + .rszink { stroke: var(--hp-accent); stroke-width: 2; } + :host([data-pointer-hover]) .rszhandle:hover + .rszicon .rszink { stroke-width: 3; } + .rszicon.disabled { opacity: 0.38; } + :host([data-pointer-hover]) .rszhandle.disabled:hover + .rszicon .rszink { stroke-width: 2; } + .rszmeasurelayer, + .rszmeasurelayer * { pointer-events: none; } + .rszmeasurehalo, + .rszmeasureink, + .rszleader { + fill: none; + stroke-linecap: round; + vector-effect: non-scaling-stroke; + } + .rszmeasurehalo { + stroke: var(--hp-bg); + stroke-width: 7px; + opacity: 0.9; + } + .rszmeasureink { + stroke: var(--hp-accent); + stroke-width: 3px; + } + .rszleader { + stroke: var(--hp-accent); + stroke-width: 2px; + opacity: 0.95; + } + /* the decor draft badge rides the MIDDLE of the shape, so it is centred + horizontally and lifted clear of the line instead of trailing the + cursor the way a wall badge does (owner 2026-08-04) */ + .measurelabel.dmeasure { + transform: translate(-50%, -160%); + } + .measurelabel.rszarea { + transform: translate( + calc(-50% + var(--rsz-label-x, 0px)), + calc(-50% + var(--rsz-label-y, 0px)) + ); + background: rgba(0, 0, 0, 0.6); + border: 1px solid var(--hp-accent); + } + /* width and depth of a piece of furniture while its corner is dragged — + centred on the edge they measure (docs/FURNITURE.md §6) */ + .measurelabel.furnmeasure { + transform: translate(-50%, -50%); + border: 1px solid var(--hp-accent); + } + .alignmsg { margin: 0 0 8px; font-size: 13px; line-height: 1.45; } +`,o` + /* Unified device activity: alarm, short event and continuous state all + share this one renderer. No static rings are allowed. */ + .dev ha-icon { + position: relative; + z-index: 1; + } + .device-pulse { + position: absolute; + left: 50%; + top: 50%; + width: var(--dev-size); + height: var(--dev-size); + transform: translate(-50%, -50%); + pointer-events: none; + z-index: 0; + } + .device-pulse i { + position: absolute; + inset: 0; + border-radius: 50%; + border: 2px solid var(--ripple-color, var(--hp-accent)); + opacity: 0; + } + /* A witnessed edge: exactly three waves over the 3.3 s runtime window. */ + .device-pulse.short i { + animation: hp-pulse-short 1.1s cubic-bezier(.22,.61,.36,1) 1 forwards; + } + .device-pulse.short i:nth-child(2) { animation-delay: 1.1s; } + .device-pulse.short i:nth-child(3) { animation-delay: 2.2s; } + .device-pulse.short.gen2 i { animation-name: hp-pulse-short-b; } + .device-pulse.continuous i:nth-child(n + 2), + .device-pulse.alarm i:nth-child(n + 3) { display: none; } + .device-pulse.continuous i:first-child { + animation: hp-pulse-continuous 3.6s cubic-bezier(.45,.05,.55,.95) infinite; + } + .device-pulse.alarm i { + border-width: 3px; + border-color: #F0410C; + animation: hp-pulse-alarm 2.4s cubic-bezier(.22,.61,.36,1) infinite; + } + .device-pulse.alarm i:nth-child(2) { animation-delay: 1.2s; } + .activity-dot { + position: absolute; + right: 8%; + bottom: 8%; + width: 18%; + height: 18%; + min-width: 4px; + min-height: 4px; + border-radius: 50%; + background: var(--ripple-color, var(--hp-accent)); + border: 1px solid color-mix(in srgb, var(--hp-bg) 82%, transparent); + pointer-events: none; + z-index: 3; + } + .kioskdots { + position: absolute; + left: 50%; + bottom: var(--sp-5); + transform: translateX(-50%); + display: flex; + gap: var(--sp-4); + z-index: 5; + pointer-events: none; + } + .kdot { + width: 9px; + height: 9px; + border-radius: 50%; + background: var(--hp-muted); + opacity: 0.55; + } + .kdot.on { background: var(--hp-accent); opacity: 1; } + .dev.valonly { + /* Saved coordinates remain the centre of a Text shell. The shell may + expand, while the invisible anchor stays one core diameter wide. */ + width: var(--dev-size, var(--icon-size, 2.5cqw)); + } + .dev.valonly .device-core { + width: max-content; + min-width: var(--dev-size, var(--icon-size, 2.5cqw)); + padding-inline: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.16); + border-radius: calc(var(--dev-size, var(--icon-size, 2.5cqw)) / 2); + } + .dev .valtext { + overflow: visible; + font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * var(--value-font-scale, .45)); + font-weight: 600; + white-space: nowrap; + } + @media (prefers-reduced-motion: reduce) { + .device-pulse { display: none; } + } + .dev .newdot { + position: absolute; + top: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * -0.12); + right: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * -0.12); + width: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.34); + height: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.34); + border-radius: 50%; + background: #f0301f; + border: 2px solid var(--card-background-color, var(--hp-bg)); + pointer-events: none; + z-index: 2; + } + .dev { + position: absolute; + /* The surface resolves compatibility icon_size units once. The shared + face receives an effective base and only applies the per-device size. */ + --dev-size: calc(var(--device-base-size, 2.25cqw) * var(--dev-scale, 1)); + /* 101.5/80 is the package shell/core ratio, including its stroke. */ + --device-shell-size: calc(var(--dev-size) * 1.26875); + --device-shell-inset: calc(var(--dev-size) * 0.134375); + --device-shell-stroke-ratio: 0.01875; + --device-shell-border-width: max(1px, calc(var(--dev-size) * var(--device-shell-stroke-ratio))); + --device-core-bg: var(--card-background-color, #fff); + --device-core-fg: var(--primary-text-color, #252525); + --device-core-bg: light-dark(#fff, #252525); + --device-core-fg: light-dark(#252525, #fff); + --device-face-bg: var(--device-core-bg); + --device-face-fg: var(--device-core-fg); + --device-shell-base-stroke: light-dark(#BCBCBC, rgb(37 37 37 / 75%)); + --device-shell-stroke: var(--device-shell-base-stroke); + --device-shell-shadow: + 0 calc(var(--dev-size) * .025) calc(var(--dev-size) * .05) rgb(37 40 45 / 12%), + 0 calc(var(--dev-size) * .1) calc(var(--dev-size) * .175) + calc(var(--dev-size) * -.025) rgb(37 40 45 / 18%); + --device-core-inset-shadow: 0 0 0 0 transparent; + --device-ring-color: transparent; + --device-ring-width: 0px; + /* The saved point is the exact centre of the icon core. */ + width: var(--dev-size); + height: var(--dev-size); + margin: calc(var(--dev-size) / -2) 0 0 calc(var(--dev-size) / -2); + border: 0; + background: transparent; + display: block; + color: var(--device-core-fg); + cursor: pointer; + pointer-events: auto; + transition: opacity 0.2s; + box-shadow: none; + outline: none; + z-index: 2; + } + .dev.theme-light { + --device-core-bg: #fff; + --device-core-fg: #000; + --device-shell-base-stroke: #BCBCBC; + --device-shell-stroke: var(--device-shell-base-stroke); + --device-shell-shadow: + 0 calc(var(--dev-size) * .025) calc(var(--dev-size) * .05) rgb(37 40 45 / 12%), + 0 calc(var(--dev-size) * .1) calc(var(--dev-size) * .175) + calc(var(--dev-size) * -.025) rgb(37 40 45 / 18%); + --device-core-inset-shadow: 0 0 0 0 transparent; + } + .dev.theme-dark { + --device-core-bg: #252525; + --device-core-fg: #fff; + --device-shell-base-stroke: rgb(37 37 37 / 75%); + --device-shell-stroke: var(--device-shell-base-stroke); + --device-shell-shadow: + 0 calc(var(--dev-size) * .025) calc(var(--dev-size) * .0375) rgb(37 40 45 / 12%), + 0 calc(var(--dev-size) * .1) calc(var(--dev-size) * .175) + calc(var(--dev-size) * -.025) rgb(37 40 45 / 18%); + --device-core-inset-shadow: + inset 0 calc(var(--dev-size) * 0.0125) calc(var(--dev-size) * 0.0125) rgb(255 255 255 / 70%); + } + .dev::before { + content: ''; + position: absolute; + left: 50%; + top: 50%; + width: max(44px, var(--device-shell-size)); + height: max(44px, var(--device-shell-size)); + transform: translate(-50%, -50%); + border-radius: 50%; + pointer-events: auto; + z-index: 3; + } + .device-shell { + position: absolute; + box-sizing: border-box; + display: flex; + align-items: center; + gap: calc(var(--dev-size) * 0.1); + padding: 0; + min-width: var(--dev-size); + min-height: var(--dev-size); + border: 0; + background: transparent; + box-shadow: none; + transition: opacity .2s; + pointer-events: none; + } + .device-shell-frame { + position: absolute; + z-index: 0; + box-sizing: border-box; + inset: calc(var(--device-shell-inset) / -1); + border: var(--device-shell-border-width) solid var(--device-shell-stroke); + /* A saturating radius is resolved from the final border box. Using a + second fractional length here lets border-box and radius quantise on + different sides of a device pixel at some zoom/DPR combinations. */ + border-radius: 9999px; + background: transparent; + box-shadow: var(--device-shell-shadow); + transition: border-color .15s, box-shadow .15s, opacity .2s; + pointer-events: auto; + /* Normative production fallback: never add a per-marker backdrop blur. */ + backdrop-filter: none; + } + .device-shell:not(.with-values) { + left: 0; + top: 0; + } + .device-shell:not(.with-values):not(.text-shell) .device-shell-frame { + border-radius: 50%; + } + .device-shell.with-values.pos-right { + left: 0; + top: 50%; + transform: translateY(-50%); + } + .device-shell.with-values.pos-left { + right: 0; + top: 50%; + transform: translateY(-50%); + flex-direction: row-reverse; + } + .device-shell.with-values.pos-bottom { + left: 50%; + top: 0; + transform: translateX(-50%); + flex-direction: column; + } + .device-shell.with-values.pos-top { + left: 50%; + bottom: 0; + transform: translateX(-50%); + flex-direction: column-reverse; + } + .device-core { + position: relative; + z-index: 1; + box-sizing: border-box; + flex: 0 0 auto; + width: var(--dev-size); + height: var(--dev-size); + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + background: var(--device-face-bg); + color: var(--device-face-fg); + box-shadow: + var(--device-core-inset-shadow), + 0 0 0 var(--device-ring-width) var(--device-ring-color); + line-height: 0; + transition: background .15s, color .15s, box-shadow .15s, opacity .2s; + pointer-events: none; + } + .device-sections { + display: flex; + flex-direction: column; + align-items: stretch; + justify-content: center; + gap: calc(var(--dev-size) * .08); + min-width: 0; + pointer-events: none; + } + .device-shell.pos-top .device-sections, + .device-shell.pos-bottom .device-sections { + flex-direction: row; + } + .dev ha-icon { + /* from --dev-size, NOT --icon-size: the per-device size multiplier must + scale the GLYPH with its badge. Pinned to the base size, "make this + icon bigger" grew an empty box around a default-size glyph (user + report via the owner, 2026-07-29). */ + --mdc-icon-size: calc(var(--dev-size, var(--device-base-size, 2.25cqw)) * 0.55); + display: flex; + align-items: center; + justify-content: center; + line-height: 0; + } + .dev.on { + --device-face-bg: #F0A00C; + --device-face-fg: light-dark(#fff, #252525); + --device-shell-stroke: #F0A00C; + } + .dev.theme-light.on { --device-face-fg: #fff; } + .dev.theme-dark.on { + --device-face-fg: #252525; + --device-shell-stroke-ratio: .0375; + } + .dev.open { + --device-face-bg: var(--hp-open); + --device-face-fg: light-dark(#fff, #252525); + } + .dev.theme-light.open { --device-face-fg: #fff; } + .dev.theme-dark.open { --device-face-fg: #252525; } + .dev.lock-locked { + --device-face-bg: #66D17A; + --device-face-fg: light-dark(#fff, #252525); + --device-shell-stroke: #66D17A; + } + .dev.theme-light.lock-locked { --device-face-fg: #fff; } + .dev.theme-dark.lock-locked { + --device-face-fg: #252525; + --device-shell-stroke-ratio: .025; + } + .dev.lock-unlocked { + --device-face-bg: #F0410C; + --device-face-fg: light-dark(#fff, #252525); + --device-shell-stroke: #F0410C; + } + .dev.theme-light.lock-unlocked { --device-face-fg: #fff; } + .dev.theme-dark.lock-unlocked { + --device-face-fg: #252525; + --device-shell-stroke-ratio: .025; + } + /* Interaction wins ordinary state colours. Alarm keeps priority through + the more-specific rule below. Unavailable has no visual hover. */ + :host([data-pointer-hover]) .dev:not(.unavail):hover { + --device-face-bg: #0C82F0; + --device-face-fg: light-dark(#fff, #252525); + --device-shell-stroke: var(--device-shell-base-stroke); + } + :host([data-pointer-hover]) .dev.theme-light:not(.unavail):hover { --device-face-fg: #fff; } + :host([data-pointer-hover]) .dev.theme-dark:not(.unavail):hover { --device-face-fg: #252525; } + :host([data-pointer-hover]) .dev:hover, + .dev:focus-visible { z-index: 5; } + .dev.unavail { + opacity: 0.35; + --device-face-bg: #B5BAC1; + --device-shell-stroke: var(--device-shell-base-stroke); + } + .dev.virtual .device-shell-frame { + border-style: dashed; + } + /* "hide from plan" flag, shown only in the device editor with the + "show hidden devices" toggle on (docs/FILTERING.md). BLUE, so a hidden + device cannot be mistaken for an unavailable one (translucent dark) — + and no live-state paint at all: a ghost is configuration, not status + (owner's request). */ + .dev.ghost { + opacity: 0.6; + color: var(--hp-accent); + } + .dev.ghost .device-shell-frame { + border-style: dashed; + border-color: var(--hp-accent); + box-shadow: none; + } + .dev.ghost .device-core { + background: rgba(62, 166, 255, 0.22); /* fallback for old WebViews */ + background: color-mix(in srgb, var(--hp-accent) 30%, var(--card-background-color, #1c2530)); + color: var(--hp-accent); + } + /* HA-disabled is not a user hide: neutral grey + a power-off badge keeps + the two ghosts distinguishable without relying on colour alone. */ + .dev.ghost.ha-disabled { + opacity: 0.62; + color: var(--secondary-text-color, #9aa0aa); + } + .dev.ghost.ha-disabled .device-shell-frame { + border-color: var(--secondary-text-color, #9aa0aa); + } + .dev.ghost.ha-disabled .device-core { + background: rgba(120, 124, 134, 0.2); + background: color-mix(in srgb, var(--secondary-text-color, #9aa0aa) 24%, var(--card-background-color, #1c2530)); + color: var(--secondary-text-color, #9aa0aa); + } + .dev .habadge { + position: absolute; + right: -18%; + bottom: -18%; + display: grid; + place-items: center; + width: 46%; + height: 46%; + min-width: 12px; + min-height: 12px; + border-radius: 50%; + background: var(--card-background-color, #242832); + border: 1px solid currentColor; + } + .dev .habadge ha-icon { width: 72%; height: 72%; } + .dev.sel { + --device-ring-color: #F0A00C; + --device-ring-width: calc(var(--dev-size) * 0.0375); + } + .dev:focus-visible { + --device-ring-color: #0C82F0; + --device-ring-width: calc(var(--dev-size) * 0.0375); + } + .dev:not(.on):not(.open):not(.alarm):not(.lock-locked):not(.lock-unlocked):not(.unavail):focus-visible { + --device-face-fg: #0C82F0; + } + /* Alert stays above focus, selection, hover and ordinary semantic paint. */ + .dev.alarm, + :host([data-pointer-hover]) .dev.alarm:hover, + .dev.alarm:focus-visible { + --device-face-bg: #F0410C; + --device-face-fg: light-dark(#fff, #252525); + --device-shell-stroke: #F0410C; + } + .dev.theme-light.alarm { --device-face-fg: #fff; } + .dev.theme-dark.alarm { + --device-face-fg: #252525; + --device-shell-stroke-ratio: .025; + } + .dev .value-badge { + position: relative; + z-index: 2; + box-sizing: border-box; + width: max-content; + min-width: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * .7875); + height: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * .7875); + display: flex; + align-items: center; + justify-content: center; + overflow: visible; + background: var(--device-core-bg); + border: 0; + border-radius: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.39375); + padding: 0 calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.14); + font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * var(--value-font-scale, .45)); + font-weight: 600; + line-height: 1; + color: var(--device-core-fg); + white-space: nowrap; + pointer-events: none; + } + .dev .value-badge.unavailable, + .dev .value-badge.missing { opacity: 0.66; } + .dev .lqi { + position: absolute; + top: calc(50% + var(--device-shell-size) / 2 + var(--dev-size) * .05); + left: 50%; + transform: translateX(-50%); + margin-top: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.05); + font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.38); + font-weight: 600; + line-height: 1; + text-shadow: 0 0 3px rgba(0, 0, 0, 0.9), 0 0 2px rgba(0, 0, 0, 0.9); + white-space: nowrap; + pointer-events: none; + } + .dev .lqi.below-value-badge { + margin-top: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.8875); + } + .temprange { + display: inline-flex; + align-items: center; + gap: var(--sp-3); + margin-left: auto; + color: var(--hp-muted); + font-size: var(--fs-s); + } + /* live vacuum: a round puck, no badge plate, soft pulse (docs/VACUUM.md) */ + .vacpuck { + position: absolute; + /* the base badge, but round and 20% smaller — the owner's wording: + «иконка похожа на иконку базы, только круглая и чуть меньше» */ + --puck-size: calc(var(--device-base-size, 2.25cqw) * 0.8); + width: var(--puck-size); + height: var(--puck-size); + border-radius: 50%; + transform: translate(-50%, -50%); + --vac-core-bg: light-dark(#fff, #252525); + --vac-core-fg: light-dark(#252525, #fff); + background: var(--vac-core-bg); + border: 1px solid #BCBCBC; + box-shadow: + 0 1px 2px rgb(37 40 45 / 12%), + 0 4px 8px -1.07px rgb(37 40 45 / 18%); + color: var(--vac-core-fg); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 6; + /* glide between sparse position updates; .jump disables it so the robot + never appears to drive through walls after a data gap */ + transition: left 1.2s linear, top 1.2s linear; + animation: vacpulse 2.2s ease-out infinite; + } + .vacpuck.theme-light { --vac-core-bg: #fff; --vac-core-fg: #252525; } + .vacpuck.theme-dark { --vac-core-bg: #252525; --vac-core-fg: #fff; } + .vacpuck.jump { transition: none; } + .vacpuck.stale { opacity: 0.45; animation: none; } + .vacpuck ha-icon { + --mdc-icon-size: calc(var(--puck-size) * 0.68); + color: var(--vac-core-fg); + /* same centering recipe as .dev ha-icon: without flex + line-height 0 + the glyph sits on its text baseline and appears to float around the + circle (owner report 2026-07-31) */ + display: flex; + align-items: center; + justify-content: center; + line-height: 0; + width: var(--mdc-icon-size); + height: var(--mdc-icon-size); + } + @keyframes vacpulse { + 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--hp-accent) 45%, transparent); } + 70% { box-shadow: 0 0 0 12px transparent; } + 100% { box-shadow: 0 0 0 0 transparent; } + } + @media (prefers-reduced-motion: reduce) { + .vacpuck { animation: none; } + } + .vactrail { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 5; + overflow: visible; + } + .vactrail polyline, + .vactrail path { + fill: none; + stroke-linejoin: round; + stroke-linecap: round; + vector-effect: non-scaling-stroke; + } + /* dark halo + light core: neutral, and one of the two always contrasts + with whatever fill is underneath */ + .vactrail g.prev { opacity: 0.4; } + .vactrail .case { + stroke: rgba(0, 0, 0, 0.4); + stroke-width: 2.25; + } + .vactrail .core { + stroke: rgba(255, 255, 255, 0.82); + stroke-width: 0.9; + } + .devicepreview-empty { + min-height: 82px; + margin: 4px 0 14px; + padding: var(--sp-4); + display: flex; + align-items: center; + justify-content: center; + gap: var(--sp-3); + border: 1px dashed var(--hp-line); + border-radius: var(--rad-m); + color: var(--hp-muted); + text-align: center; + } +`,o` + .hdr { + position: sticky; + top: var(--header-height, 56px); + z-index: 20; + background: var(--card-background-color, var(--hp-bg)); + border-radius: var(--ha-card-border-radius, 12px) var(--ha-card-border-radius, 12px) 0 0; + } + .tabs { + display: flex; + gap: var(--sp-2); + background: rgba(127, 127, 127, 0.12); + padding: var(--sp-2); + border-radius: var(--rad-l); + flex-wrap: wrap; + } + .tab { + border: 0; + background: transparent; + color: var(--hp-muted); + display: inline-flex; + align-items: center; + padding: var(--sp-3) var(--sp-5); + border-radius: var(--rad-m); + font-size: var(--fs-m); + font-weight: 600; + cursor: pointer; + transition: 0.15s; + font-family: inherit; + } + :host([data-pointer-hover]) .tab:hover { + color: var(--hp-txt); + } + .tab.active { + background: var(--hp-accent); + color: var(--text-primary-color, #fff); + } + /* docs/CANVAS.md §4.1: objects an order of magnitude away from the plan + do not decide the opening view — one quiet chip says so and offers to + take them in. Never a modal (owner). */ + .farhint { + position: absolute; + left: 50%; + transform: translateX(-50%); + bottom: var(--sp-4); + z-index: 12; + display: flex; + align-items: center; + gap: var(--sp-2); + max-width: calc(100% - var(--sp-8)); + background: var(--card-background-color, var(--hp-bg)); + opacity: 0.94; + color: var(--hp-txt); + border: 1px solid var(--divider-color, #33404d); + border-radius: var(--rad-m); + padding: var(--sp-1) var(--sp-3); + font-size: var(--fs-s); + } + .farhint ha-icon { + --mdc-icon-size: 18px; + color: var(--hp-warn, #e2a03f); + flex: none; + } + .farhint span { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .dropbtn { + display: flex; + align-items: center; + gap: var(--sp-4); + width: 100%; + text-align: left; + border: 1px solid var(--hp-muted); + border-radius: var(--rad-m); + background: transparent; + color: var(--hp-txt); + padding: var(--sp-4) 10px; + cursor: pointer; + font-family: inherit; + font-size: var(--fs-m); + margin-top: var(--sp-3); + } + .dropbtn .ref { color: var(--hp-muted); font-size: var(--fs-s); margin-left: auto; } + .dropbtn ha-icon { --mdc-icon-size: 18px; margin-left: var(--sp-2); } + .dropbtn.open { border-color: var(--hp-accent); } + .droppanel { + border: 1px solid var(--hp-accent); + border-top: none; + border-radius: 0 0 var(--rad-m) var(--rad-m); + padding: var(--sp-3); + margin-top: -4px; + } + .hdr.kioskhide { display: none; } + .decorbar .dcolor { + width: 30px; height: 26px; padding: 0; border: none; background: none; cursor: pointer; + } + .decorbar .dwidth { + font-family: inherit; font-size: var(--fs-s); border-radius: var(--rad-s); + background: var(--hp-bg2, transparent); color: var(--hp-txt); border: 1px solid var(--hp-muted); + padding: var(--sp-2) var(--sp-3); + } + .decorbar .bdhint { + font-size: var(--fs-s); + color: var(--hp-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .decorbar .dfill { + display: inline-flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-s); cursor: pointer; + } + .decorbar .dfill input[type="checkbox"] { + width: 16px; + height: 16px; + flex: none; + margin: 0; + padding: 0; + } + .decorbar hp-color-opacity { flex: 0 0 auto; } + .modes { + display: inline-flex; + gap: var(--sp-1); + background: rgba(127, 127, 127, 0.12); + border-radius: var(--rad-l); + padding: var(--sp-2); + } + .modetab { + display: inline-flex; + align-items: center; + gap: var(--sp-3); + border: 0; + background: transparent; + color: var(--hp-muted); + padding: var(--sp-3) 10px; /* 10px h-padding kept: +2px would wrap the header modes row */ + border-radius: var(--rad-m); + font-size: var(--fs-m); + font-weight: 600; + cursor: pointer; + font-family: inherit; + transition: background-color 0.14s ease, color 0.14s ease, transform 0.14s ease; + } + .modetab:active { transform: scale(0.97); } + .modetab ha-icon { --mdc-icon-size: 15px; } + /* issue #220: a tab can be dragged to a new position in the editors */ + .tab[data-reorderable] { cursor: grab; } + .tab.dragging { cursor: grabbing; opacity: 0.55; } + .tab.drop-before { box-shadow: inset 2px 0 0 0 var(--primary-color, #03a9f4); } + .tab.drop-after { box-shadow: inset -2px 0 0 0 var(--primary-color, #03a9f4); } + .modetab .closex { + --mdc-icon-size: 13px; + box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + min-width: 24px; + min-height: 24px; + /* Keep the old 13 × 13 flex footprint (+2 px left margin) while the + pointer target grows around it. Header width, height and wrapping do + not move; only near-misses that used to hit the active tab reach X. */ + margin: -5.5px -5.5px -5.5px -3.5px; + opacity: 0.75; + cursor: pointer; + border-radius: var(--rad-s); + } + :host([data-pointer-hover]) .modetab .closex:hover { opacity: 1; } + .editbar .barclose { + box-sizing: border-box; + width: 40px; + height: 40px; + padding: 0; + margin: 0; + min-width: 40px; + min-height: 40px; + justify-content: center; + gap: 0; + line-height: 0; + } + .editbar .barclose ha-icon { flex: none; margin: 0; } + .modetab.active { + background: var(--hp-accent); + color: var(--text-primary-color, #fff); + } + @media (max-width: 720px) { + .modetab .ml { display: none; } + } + .editbar { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: stretch; + border-bottom: 1px solid var(--hp-line); + font-size: var(--fs-m); + } + .editbar-tools { + display: flex; + align-items: center; + gap: 10px; + padding: var(--sp-4) var(--sp-5); + flex-wrap: wrap; + min-width: 0; + outline: none; + } + .editbar-end { + display: flex; + align-items: center; + padding: var(--sp-4) var(--sp-5); + border-inline-start: 1px solid var(--hp-line); + background: var(--card-background-color, var(--hp-bg)); + position: relative; + z-index: 2; + } + .tab .tabedit { + --mdc-icon-size: 13px; + display: inline-flex; + align-items: center; + margin-left: var(--sp-3); + opacity: 0.4; + } + :host([data-pointer-hover]) .tab:hover .tabedit { + opacity: 0.9; + } + .tab.tabadd { + padding: var(--sp-3) var(--sp-4); + } + .tab.tabadd ha-icon { + --mdc-icon-size: 15px; + } + .rhint { + font-size: var(--fs-s); + color: var(--hp-muted); + margin-bottom: var(--sp-3); + } + .togglehint { + overflow-wrap: anywhere; + } + .togglehint > div + div { + margin-top: var(--sp-1); + } + /* Keep the last editor bar mounted while the row collapses. This makes + both entering and leaving an editor change the card geometry gradually; + the header ResizeObserver keeps the stage fitted throughout. */ + .editorchrome { + display: block; + height: 0; + opacity: 0; + visibility: hidden; + overflow: hidden; + } + .editorchrome:not(.open) { + pointer-events: none; + } + .editorchrome.open { + height: auto; + opacity: 1; + visibility: visible; + overflow: visible; + } + .editorchrome.transitioning { + overflow: hidden; + will-change: height; + pointer-events: none; + } + /* The toolbar is already visible while its height is interpolating. Its + explicit navigation control must remain usable even though all editing + tools stay frozen until the transition settles. */ + .editorchrome.transitioning .barclose { + pointer-events: auto; + } + .editorchrome-inner { + min-height: 0; + transform-origin: top center; + } + .editorchrome.transitioning .editorchrome-inner { + will-change: opacity; + } + @media (prefers-reduced-motion: reduce) { + .modetab { transition: none; } + } + .editbar .warn { + color: #ffc14d; + } + .editbar .sname { + font-weight: 600; + } + .editbar input { + width: 74px; + background: transparent; + border: 1px solid var(--hp-line); + color: var(--hp-txt); + border-radius: var(--rad-s); + padding: var(--sp-3) var(--sp-4); + font-size: var(--fs-m); + } + .editbar label, + .editbar .hint { + color: var(--hp-muted); + font-size: var(--fs-s); + } + .editbar .drawwall { + display: inline-flex; + align-items: center; + gap: var(--sp-3); + white-space: nowrap; + } + .editbar .wallsgroup { + display: inline-flex; + align-items: center; + gap: 10px; + white-space: nowrap; + } + .editbar .drawwall input { + width: 4.2em; + } + .editbar .drawwall .opl { + color: var(--hp-muted); + font-size: var(--fs-s); + } + .menuwrap { + position: fixed; + inset: 0; + z-index: 80; + } + .menu { + position: fixed; + background: var(--hp-bg); + border: 1px solid var(--hp-accent); + border-radius: var(--rad-l); + box-shadow: var(--shadow-2); + min-width: 210px; + max-width: 300px; + overflow: hidden; + transform: translate(0, 8px); + } + .menu .hd { + padding: var(--sp-4) var(--sp-5); + font-weight: 600; + font-size: var(--fs-m); + border-bottom: 1px solid var(--hp-line); + display: flex; + align-items: center; + gap: var(--sp-3); + } + .menu .hd ha-icon, + .menu .it.all ha-icon { + color: var(--hp-accent); + --mdc-icon-size: 16px; + } + .menu .it { + padding: var(--sp-4) var(--sp-5); + font-size: var(--fs-m); + cursor: pointer; + display: flex; + align-items: center; + gap: var(--sp-4); + } + .menu .it ha-icon { + --mdc-icon-size: 16px; + color: var(--hp-muted); + } + :host([data-pointer-hover]) .menu .it:hover { + background: rgba(127, 127, 127, 0.15); + } + .menu .it.all { + color: var(--hp-accent); + font-weight: 600; + } + .tip { + position: fixed; + pointer-events: none; + background: var(--hp-bg); + border: 1px solid var(--hp-accent); + color: var(--hp-txt); + padding: var(--sp-3) 10px; + border-radius: var(--rad-m); + font-size: var(--fs-m); + box-shadow: var(--shadow-2); + z-index: 60; + max-width: 260px; + } + .tip .m { + color: var(--hp-muted); + font-size: var(--fs-s); + display: block; + } +`,o` + .head { + display: flex; + align-items: center; + gap: 10px; + padding: 10px var(--sp-5); + border-bottom: 1px solid var(--hp-line); + flex-wrap: wrap; + } + .title { + font-size: var(--fs-l); + font-weight: 600; + display: flex; + align-items: center; + gap: var(--sp-3); + white-space: nowrap; + } + .title ha-icon { + color: var(--hp-accent); + --mdc-icon-size: 18px; + } + @media (max-width: 620px) { + .head { gap: var(--sp-3); padding: var(--sp-4) 10px; } + .head .count { display: none; } + .head .title { font-size: var(--fs-m); } + } + .count { + font-size: var(--fs-s); + color: var(--hp-muted); + } + .btn { + display: inline-flex; + align-items: center; + gap: var(--sp-3); + border: 1px solid var(--hp-line); + background: transparent; + color: var(--hp-txt); + padding: var(--sp-3) 10px; + border-radius: var(--rad-m); + cursor: pointer; + transition: 0.15s; + font-family: inherit; + font-size: var(--fs-m); + } + .btn ha-icon { + --mdc-icon-size: 17px; + } + :host([data-pointer-hover]) .btn:hover { + border-color: var(--hp-accent); + } + .btn.on { + background: var(--hp-accent); + color: var(--text-primary-color, #fff); + border-color: var(--hp-accent); + } + .btn.ghost { + border: none; + } + .btn[disabled] { + opacity: 0.5; + pointer-events: none; + } + /* the compass dial in the general settings (docs/SUN.md) */ + .sunrow { + display: flex; + align-items: center; + gap: var(--sp-5); + margin: var(--sp-3) 0; + } + .suncol { + flex: 1; + display: flex; + flex-direction: column; + gap: var(--sp-3); + min-width: 0; + } + .recoveryoverlay { + position: absolute; + inset: 0; + /* Above the contextual editor tray (70): recovery is the one state in + which every stage-editing surface must be inert. Dialogs live outside + the stage and retain their own higher card-level stacking context. */ + z-index: 75; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--sp-4); + padding: var(--sp-6); + box-sizing: border-box; + /* The final solid layer guarantees an opaque recovery surface even when + a custom HA theme exposes its card colour as rgba(). */ + background: + linear-gradient(var(--ha-card-background, var(--card-background-color, #111)), + var(--ha-card-background, var(--card-background-color, #111))), + #111; + color: var(--primary-text-color, #fff); + text-align: center; + pointer-events: auto; + transition: opacity 0.15s ease; + } + .recoveryoverlay.phase-entering, + .recoveryoverlay.phase-leaving { + opacity: 0; + } + .recoveryoverlay.phase-fading-in, + .recoveryoverlay.phase-opaque { + opacity: 1; + } + .recoveryoverlay ha-icon { + --mdc-icon-size: 44px; + color: var(--hp-accent); + } + .editorloading { + position: absolute; + z-index: 74; + left: 50%; + top: 50%; + display: flex; + align-items: center; + gap: var(--sp-3); + padding: var(--sp-3) var(--sp-5); + border: 1px solid color-mix(in srgb, var(--hp-accent) 45%, transparent); + border-radius: 999px; + background: color-mix(in srgb, + var(--ha-card-background, var(--card-background-color, #111)) 92%, transparent); + color: var(--primary-text-color); + box-shadow: 0 6px 22px rgb(0 0 0 / 18%); + transform: translate(-50%, -50%); + pointer-events: none; + animation: editor-loading-in 0.15s ease both; + } + .editorloading ha-icon { + --mdc-icon-size: 22px; + color: var(--hp-accent); + animation: editor-loading-spin 0.9s linear infinite; + } + @keyframes editor-loading-in { + from { opacity: 0; transform: translate(-50%, calc(-50% + 4px)); } + to { opacity: 1; transform: translate(-50%, -50%); } + } + @keyframes editor-loading-spin { to { transform: rotate(360deg); } } + @media (prefers-reduced-motion: reduce) { + .recoveryoverlay, + .editorloading, + .editorloading ha-icon { + transition: none; + animation: none; + } + } + .oplock { + --oplock-size: calc(var(--icon-size, 2.5cqw) * 0.62); + --oplock-core-size: calc(var(--oplock-size) / 1.26875); + --oplock-stroke-ratio: 0.01875; + --oplock-stroke-width: max(1px, calc(var(--oplock-core-size) * var(--oplock-stroke-ratio))); + --oplock-core-bg: light-dark(#fff, #252525); + --oplock-core-fg: light-dark(#252525, #fff); + --oplock-shell-stroke: light-dark(#BCBCBC, rgb(37 37 37 / 75%)); + --oplock-shell-shadow: + 0 calc(var(--oplock-core-size) * .025) calc(var(--oplock-core-size) * .05) rgb(37 40 45 / 12%), + 0 calc(var(--oplock-core-size) * .1) calc(var(--oplock-core-size) * .175) + calc(var(--oplock-core-size) * -.025) rgb(37 40 45 / 18%); + --oplock-core-shadow: 0 0 0 0 transparent; + pointer-events: none; /* inert while editing; clickable in View (rule below) */ + position: absolute; + transform: translate(-50%, -50%); + width: var(--oplock-size); + height: var(--oplock-size); + display: grid; + place-items: center; + border: 0; + background: transparent; + z-index: 1; + } + .oplock.theme-light { + --oplock-core-bg: #fff; + --oplock-core-fg: #252525; + --oplock-shell-stroke: #BCBCBC; + } + .oplock.theme-dark { + --oplock-core-bg: #252525; + --oplock-core-fg: #fff; + --oplock-shell-stroke: rgb(37 37 37 / 75%); + --oplock-core-shadow: + inset 0 calc(var(--oplock-core-size) * .0125) + calc(var(--oplock-core-size) * .0125) rgb(255 255 255 / 70%); + } + .oplock-shell { + box-sizing: border-box; + width: 100%; + height: 100%; + border: var(--oplock-stroke-width) solid var(--oplock-shell-stroke); + border-radius: 50%; + background: transparent; + box-shadow: var(--oplock-shell-shadow); + display: grid; + place-items: center; + pointer-events: none; + backdrop-filter: none; + } + .oplock-core { + width: var(--oplock-core-size); + height: var(--oplock-core-size); + border-radius: 50%; + background: var(--oplock-core-bg); + color: var(--oplock-core-fg); + box-shadow: var(--oplock-core-shadow); + display: grid; + place-items: center; + pointer-events: none; + } + .oplock ha-icon { + --mdc-icon-size: calc(var(--oplock-core-size) * 0.55); + display: flex; + line-height: 0; + } + .oplock.locked { + --oplock-core-bg: #66D17A; + --oplock-core-fg: light-dark(#fff, #252525); + --oplock-shell-stroke: #66D17A; + } + .oplock.theme-light.locked { --oplock-core-fg: #fff; } + .oplock.theme-dark.locked { + --oplock-core-fg: #252525; + --oplock-stroke-ratio: .025; + } + .oplock.unlocked { + --oplock-core-bg: #F0410C; + --oplock-core-fg: light-dark(#fff, #252525); + --oplock-shell-stroke: #F0410C; + } + .oplock.theme-light.unlocked { --oplock-core-fg: #fff; } + .oplock.theme-dark.unlocked { + --oplock-core-fg: #252525; + --oplock-stroke-ratio: .025; + } + .oplock.unknown { --oplock-core-fg: var(--hp-muted); } + .btn.lockact { + width: 100%; + justify-content: center; + display: flex; + align-items: center; + gap: var(--sp-3); + margin-top: var(--sp-4); + } + .btn.lockact.warn { + color: var(--error-color, #d33); + border-color: var(--error-color, #d33); + } + .oprow { + display: flex; + align-items: center; + gap: var(--sp-4); + padding: var(--sp-3) 0; + } + .oprow b { margin-left: auto; } + .oprow.ok b { color: #66d17a; } + .oprow.warn b { color: var(--hp-open); } + .bindharow { + display: flex; + align-items: center; + gap: var(--sp-5); + flex-wrap: wrap; + } + .bindharow .entcheck { opacity: 0.9; } + .ctrlchips { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin: var(--sp-2) 0; } + .ctrlchip { + display: inline-flex; align-items: center; gap: var(--sp-2); + background: var(--hp-accent); color: var(--text-primary-color, #fff); + border-radius: var(--rad-l); padding: var(--sp-2) var(--sp-4); font-size: var(--fs-s); + } + .ctrlchip ha-icon { --mdc-icon-size: 14px; cursor: pointer; } + .ctrllist { display: flex; flex-direction: column; gap: var(--sp-1); margin-top: var(--sp-2); } + .ctrlopt { + display: flex; align-items: center; gap: var(--sp-4); text-align: left; + border: 0; background: transparent; color: var(--hp-txt); + padding: var(--sp-3) var(--sp-4); border-radius: var(--rad-s); cursor: pointer; font-family: inherit; font-size: var(--fs-m); + } + :host([data-pointer-hover]) .ctrlopt:hover { + background: var(--secondary-background-color, rgba(128,128,128,0.15)); + } + .ctrlopt .sub { color: var(--hp-muted); font-size: var(--fs-s); margin-left: auto; } + .ctrlopt ha-icon { --mdc-icon-size: 16px; } + .ctrlstates { display: flex; flex-direction: column; gap: var(--sp-2); } + .ctrlstate { display: inline-flex; align-items: center; gap: var(--sp-3); color: var(--hp-muted); } + .ctrlstate.on { color: var(--hp-txt); } + .ctrlstate ha-icon { --mdc-icon-size: 15px; } + .cardpreview { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--sp-2); + margin: var(--sp-4) 0 var(--sp-1); + padding: 10px; + border: 1px dashed var(--hp-muted); + border-radius: var(--rad-m); + } + .cardpreview .cpname { font-weight: 700; letter-spacing: 0.04em; } + .cardpreview .cpmeta { + display: inline-flex; + align-items: center; + gap: 0.3em; + font-weight: 600; + opacity: 0.85; + } + .cardpreview .cpmeta ha-icon { --mdc-icon-size: 1.05em; } + .iconauto { + display: flex; + align-items: center; + gap: var(--sp-3); + font-size: var(--fs-s); + margin: var(--sp-2) 0 0; + } + .iconauto ha-icon { --mdc-icon-size: 18px; } + .iconauto span { flex: 1; } + .iconauto .btn { min-height: 32px; padding: 0 var(--sp-3); } + hp-dialog .dfill { + display: inline-flex; + align-items: center; + gap: var(--sp-3); + cursor: pointer; + } + .wallthick-dlg { + position: absolute; + z-index: 40; + min-width: 200px; + transform: translate(-50%, 8px); + padding: 10px 12px; + border-radius: 10px; + background: var(--card-background-color, #fff); + box-shadow: 0 8px 28px rgba(0,0,0,.22); + display: flex; + flex-direction: column; + gap: 8px; + pointer-events: auto; + } + .wallthick-dlg .row { + display: flex; + align-items: center; + gap: 8px; + } + .wallthick-dlg input[type="number"] { + width: 5.5em; + padding: 4px 6px; + border: 1px solid var(--divider-color, #ccc); + border-radius: 6px; + background: var(--input-fill, transparent); + color: var(--primary-text-color); + } + .areasel, + .namein { + background: var(--hp-bg); + border: 1px solid var(--hp-line); + color: var(--hp-txt); + border-radius: var(--rad-s); + padding: var(--sp-3) var(--sp-4); + font-size: var(--fs-m); + font-family: inherit; + } + .namein { + width: 130px; + } + .habindingbanner { + display: flex; + align-items: center; + gap: var(--sp-3); + padding: var(--sp-3) var(--sp-4); + margin-bottom: var(--sp-4); + border: 1px solid var(--warning-color, #ff9800); + border-radius: var(--rad-m); + background: color-mix(in srgb, var(--warning-color, #ff9800) 12%, transparent); + color: var(--primary-text-color, #f1f3f6); + } + .habindingbanner > span { flex: 1; min-width: 0; } + .habindingbanner > ha-icon { color: var(--warning-color, #ff9800); flex: 0 0 auto; } + .habindingbanner.limited { + border-color: var(--secondary-text-color, #9aa0aa); + background: color-mix(in srgb, var(--secondary-text-color, #9aa0aa) 10%, transparent); + } + .habindingbanner.limited > ha-icon { color: var(--secondary-text-color, #9aa0aa); } + .srcrow { + display: flex; + align-items: center; + gap: var(--sp-4); + font-size: var(--fs-m); + cursor: pointer; + padding: var(--sp-1) 0; + } + .dispsection { + margin-top: var(--sp-5) !important; + padding-top: var(--sp-4); + border-top: 1px solid var(--hp-line); + font-weight: 600; + color: var(--hp-txt) !important; + } + .colorrow { + display: flex; + align-items: center; + gap: var(--sp-4); + } + .gsrow > hp-color-opacity { + min-width: min(100%, 210px); + justify-content: space-between; + } + .ripple-colorrow > hp-color-opacity { + width: 100%; + justify-content: space-between; + } + .ripple-sizerow > .opl { + min-width: 0; + } + .colorrow input[type='range'] { flex: 1; } + .colorrow .tempin { width: 70px; flex: none; } + /* beat the generic hp-dialog .body .namein { width:100% } rule */ + hp-dialog .body .temprange .tempin { width: 56px; flex: none; padding: var(--sp-2) var(--sp-3); } + hp-dialog .body .colorrow .tempin { width: 72px; flex: none; } + .srcrow { flex-wrap: nowrap; } + /* native HA controls (rendered only when the HA frontend defines them; + old HA and the smoke env keep the plain inputs). ha-switch is taller + than a checkbox - cap its footprint so .srcrow keeps its rhythm. */ + .srcrow ha-switch { flex: none; } + .colorrow ha-slider { flex: 1; min-width: 0; } + .srcrow > span:first-of-type { white-space: nowrap; } + .colorrow .opl { color: var(--hp-muted); font-size: var(--fs-s); } + .colorrow .opv { font-size: var(--fs-s); min-width: 34px; text-align: right; } + .markerlightgroup { + min-width: 0; + margin: var(--sp-5) 0 0; + padding: var(--sp-4); + border: 1px solid var(--hp-line); + border-radius: var(--rad-m); + } + .markerlightgroup legend { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: var(--sp-1); + padding: 0 var(--sp-2); + color: var(--hp-txt); + font-weight: 600; + } + .markerlightgroup legend > span { min-width: 0; overflow-wrap: anywhere; } + .markerlightgroup[disabled] > :not(legend) { opacity: .62; } + .markerhelpfield { margin-top: var(--sp-4); } + .markerhelplabel { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: var(--sp-1); + margin-bottom: var(--sp-1); + } + .markerhelplabel > label { min-width: 0; overflow-wrap: anywhere; } + .markerradios { display: grid; gap: var(--sp-1); min-width: 0; } + .markerlightgroup .srcrow > span:first-of-type { + min-width: 0; + white-space: normal; + overflow-wrap: anywhere; + } + .markerglowvalue { margin: var(--sp-3) 0; flex-wrap: wrap; } + .markerglowvalue hp-color-opacity { flex: none; } + .markerlightdisabled { + display: flex; + align-items: center; + gap: var(--sp-2); + margin-top: var(--sp-2) !important; + } + .markerlightdisabled ha-icon { --mdc-icon-size: 18px; flex: none; } + .markerbadgetechnical { + min-width: 0; + margin: var(--sp-1) 0 var(--sp-2) !important; + overflow-wrap: anywhere; + } + .markerbadgetechnical code { white-space: normal; } + .planrow { + display: flex; + align-items: center; + gap: 10px; + } + /* the "already uploaded" picker: a plan is never deleted for being + unreferenced, so it has to be findable again */ + .savedplans { + display: flex; + flex-direction: column; + gap: var(--sp-3); + max-height: 240px; + overflow: auto; + margin: var(--sp-3) 0 var(--sp-1); + padding: var(--sp-3); + border: 1px solid var(--hp-line); + border-radius: var(--rad-m); + background: var(--hp-bg2, rgba(255, 255, 255, 0.03)); + /* The same collapse that ate .candlist (v1.53.1): a scroll box is a + flex item whose automatic minimum size is ZERO (overflow != visible), + so inside hp-dialog .body — a flex column taller than its 66vh cap — + it shrank to a 14px sliver: the rows were in the DOM, the owner saw + a thin rounded stripe under the "Already uploaded" button. Don't + shrink, and keep a floor even when the box is empty or loading. */ + flex: 0 0 auto; + min-height: 2.6em; + } + .savedplan { + display: flex; + align-items: center; + gap: 10px; + } + .savedplan.cur { outline: 1px solid var(--hp-accent); border-radius: var(--rad-s); } + .savedplan img { + width: 56px; + height: 40px; + object-fit: contain; + border: 1px solid var(--hp-line); + border-radius: var(--rad-s); + background: #fff; + flex: none; + } + .savedmeta { display: flex; flex-direction: column; min-width: 0; flex: 1; } + .savedmeta b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .savedmeta .muted { font-size: var(--fs-s); } + .savedplan .btn.danger ha-icon { color: #f25a4a; } + .savedplan .btn[disabled] { opacity: 0.4; pointer-events: none; } + .planprev { + max-width: 120px; + max-height: 70px; + border: 1px solid var(--hp-line); + border-radius: var(--rad-s); + background: #fff; + } + .planname { + font-size: var(--fs-m); + max-width: 150px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .planname.muted { + color: var(--hp-muted); + } + .filebtn { + cursor: pointer; + } + .fileupload { display: inline-flex; min-width: 0; } + .fileupload > input { display: none; } + .btn.danger { + border-color: #b3402a; + color: #ff7a5c; + } + hp-dialog .row .spacer { + flex: 1; + } + hp-dialog .body { + max-height: 66vh; + overflow-y: auto; + } + .descin { + width: 100%; + box-sizing: border-box; + background: var(--hp-bg); + border: 1px solid var(--hp-line); + color: var(--hp-txt); + border-radius: var(--rad-s); + padding: var(--sp-3) var(--sp-4); + font-size: var(--fs-m); + font-family: inherit; + resize: vertical; + /* flex column of the dialog body squeezes textareas — keep a usable height */ + min-height: 92px; + flex-shrink: 0; + line-height: 1.35; + } + .bindsel { + display: flex; + flex-direction: column; + gap: var(--sp-3); + border: 1px solid var(--hp-line); + border-radius: var(--rad-m); + padding: var(--sp-4); + } + .bindsel .opt { + display: flex; + align-items: center; + gap: var(--sp-3); + border: 1px solid var(--hp-line); + background: transparent; + color: var(--hp-txt); + border-radius: var(--rad-s); + padding: var(--sp-3) var(--sp-4); + cursor: pointer; + font-size: var(--fs-m); + font-family: inherit; + } + .bindsel .opt.on { + background: var(--hp-accent); + color: var(--text-primary-color, #fff); + border-color: var(--hp-accent); + } + .curbind { + display: flex; + align-items: center; + gap: var(--sp-3); + font-size: var(--fs-m); + color: var(--hp-txt); + flex-wrap: wrap; + } + .curbind .ref { + color: var(--hp-muted); + font-size: var(--fs-s); + } + .vacbox .vacbtns { display: flex; gap: var(--sp-4); margin: var(--sp-3) 0; flex-wrap: wrap; } + .vacdiag { display: grid; gap: 4px; margin-bottom: var(--sp-3); } + .vacdiag > div { display: flex; justify-content: space-between; gap: var(--sp-5); } + .vacdiag > div > span { color: var(--secondary-text-color); } + .vacdiag > div > b { text-align: right; overflow-wrap: anywhere; } + .vacpicker { margin: var(--sp-3) 0; } + .vacsource-warning { display: grid; gap: 8px; } + .vacsource-warning .btn { justify-self: start; } + .vacpicker > summary { display: inline-flex; width: auto; list-style: none; cursor: pointer; } + .vacpicker > summary::-webkit-details-marker { display: none; } + .vacsource-list { display: grid; gap: 6px; margin-top: 8px; } + .vacsource-list details { padding: 6px 0 0; } + .vacsource-list details > summary { cursor: pointer; font-weight: 600; } + .vacsource { display: flex; align-items: center; justify-content: space-between; gap: 12px; + width: 100%; min-width: 0; padding: 9px 10px; border: 1px solid var(--divider-color); + border-radius: 10px; color: var(--primary-text-color); background: var(--secondary-background-color); + text-align: left; cursor: pointer; } + .vacsource.on { border-color: var(--accent-color); box-shadow: inset 3px 0 var(--accent-color); } + .vacsource > span:first-child { min-width: 0; display: grid; gap: 2px; } + .vacsource small { color: var(--secondary-text-color); overflow-wrap: anywhere; } + .vacsource-meta { color: var(--secondary-text-color); text-align: right; font-size: 0.82em; } + .vacxcme pre { margin: 8px 0 0; white-space: pre-wrap; user-select: text; } + .vacfit { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + z-index: 12; + overflow: visible; + touch-action: none; + cursor: grab; + /* the devlayer is pointer-events: none and every child opts back in — + without this line real clicks flew straight through the overlay + (owner: «уголки не кликабельны»; synthetic smoke events bypass + hit-testing, which is why they lied) */ + pointer-events: auto; + } + .vacfit:active { cursor: grabbing; } + .vacfit polygon { + fill: color-mix(in srgb, var(--hp-accent) 16%, transparent); + stroke: var(--hp-accent); + stroke-width: 2; + vector-effect: non-scaling-stroke; + stroke-dasharray: 6 4; + } + .vacfit text { + fill: var(--hp-accent); + font-size: 26px; + text-anchor: middle; + dominant-baseline: middle; + pointer-events: none; + user-select: none; + } + /* ---- the furniture palette (docs/FURNITURE.md §3) ------------------- */ + .furnpalette { + display: flex; + flex-direction: column; + max-height: 38vh; + border-top: 1px solid var(--hp-border, rgba(255, 255, 255, 0.12)); + background: var(--card-background-color, var(--hp-bg)); + font-size: 0.85em; + } + .furnhd { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + font-weight: 600; + opacity: 0.9; + } + .furnhd .spacer { flex: 1; } + .furnbody { + overflow: auto; + padding: 0 8px 6px; + } + .furngroup { + margin: 6px 0 3px; + font-size: 0.85em; + text-transform: uppercase; + letter-spacing: 0.04em; + opacity: 0.6; + } + .furnrow { + display: flex; + flex-wrap: wrap; + gap: 4px; + } + .furnitem { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + width: 74px; + padding: 4px 2px; + border: 1px solid transparent; + border-radius: 8px; + background: rgba(127, 127, 127, 0.08); + color: inherit; + font: inherit; + font-size: 0.8em; + line-height: 1.15; + text-align: center; + cursor: pointer; + } + :host([data-pointer-hover]) .furnitem:hover { background: rgba(127, 127, 127, 0.18); } + .furnitem.on { + border-color: var(--hp-accent); + background: rgba(38, 166, 154, 0.18); + } + .furnprev { + width: 40px; + height: 40px; + color: var(--primary-text-color, currentColor); + } + .furnsize { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; + padding: 6px 8px; + border-top: 1px solid var(--hp-border, rgba(255, 255, 255, 0.12)); + } + .furnsize label { + display: flex; + align-items: baseline; + gap: 3px; + opacity: 0.8; + } + .furnsize .furnunit { opacity: 0.6; font-size: 0.85em; } + .furnsize input { + width: 5.5em; + padding: 3px 5px; + } + .furnhint { opacity: 0.6; } + .vacfitdot { fill: var(--hp-accent); pointer-events: none; } + /* hit target: invisible and finger-sized; .vacfitknob is the visible bead */ + .vacfithandle { + fill: transparent; + stroke: none; + cursor: nwse-resize; + } + .vacfitknob { + fill: var(--hp-bg); + stroke: var(--hp-accent); + stroke-width: 2; + vector-effect: non-scaling-stroke; + pointer-events: none; + } + .vaccalbar { + position: fixed; + left: 50%; + bottom: 24px; + transform: translateX(-50%); + display: flex; + gap: var(--sp-5); + align-items: center; + background: var(--hp-bg); + color: var(--hp-txt); + border: 1px solid var(--hp-accent); + border-radius: var(--rad-l); + padding: 10px var(--sp-5); + z-index: 60; + box-shadow: var(--shadow-2); + } + .candlist { + max-height: 160px; + overflow-y: auto; + border-top: 1px solid var(--hp-line); + /* A scrollable box is a flex item that HAPPILY collapses: inside the + dialog body (a flex column) this list rendered its rows into a 1px + sliver — the DOM had 26 candidates and the user saw nothing. In the + binding dropdown it sits inside .droppanel (block context) and never + showed the bug. Field report, 2026-07-30. */ + flex: 0 0 auto; + min-height: 2.6em; + } + .cand { + display: flex; + justify-content: space-between; + gap: var(--sp-4); + padding: var(--sp-3) var(--sp-4); + cursor: pointer; + border-radius: var(--rad-s); + font-size: var(--fs-m); + } + :host([data-pointer-hover]) .cand:hover { + background: rgba(127, 127, 127, 0.15); + } + .cand.sel { + background: var(--hp-accent); + color: var(--text-primary-color, #fff); + } + .cand .cs { + color: var(--hp-muted); + font-size: var(--fs-s); + white-space: nowrap; + } + .opening-entity-candidate { + width: 100%; + border: 0; + background: transparent; + color: inherit; + font: inherit; + text-align: left; + } + .opening-entity-candidate.sel { + background: var(--hp-accent); + color: var(--text-primary-color, #fff); + } + .opening-entity-empty { cursor: default; } + .cand.sel .cs { + color: var(--text-primary-color, #fff); + opacity: 0.85; + } + .cand.muted { + color: var(--hp-muted); + cursor: default; + } + .pdfedit { + display: flex; + flex-wrap: wrap; + gap: var(--sp-3); + align-items: center; + } + .pdftag { + display: inline-flex; + align-items: center; + gap: var(--sp-2); + border: 1px solid var(--hp-line); + border-radius: var(--rad-s); + padding: var(--sp-2) var(--sp-3); + font-size: var(--fs-s); + } + .pdftag a { + color: var(--hp-txt); + text-decoration: none; + max-width: 150px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .pdftag .x { + --mdc-icon-size: 15px; + cursor: pointer; + color: var(--hp-muted); + } + :host([data-pointer-hover]) .pdftag .x:hover { + color: #ff7a5c; + } + .entlist { + display: flex; + flex-direction: column; + gap: var(--sp-2); + margin-bottom: 10px; + } + .entrow { + display: flex; + align-items: center; + gap: var(--sp-4); + padding: var(--sp-3) var(--sp-4); + border-radius: var(--rad-m); + background: var(--secondary-background-color, rgba(128, 128, 128, 0.12)); + } + .entrow ha-icon { --mdc-icon-size: 20px; color: var(--hp-muted); } + .entrow.on ha-icon { color: var(--hp-accent); } + .entrow .en { flex: 1; font-size: var(--fs-m); } + .entrow .ev { font-size: var(--fs-m); color: var(--hp-muted); } + .entbtn { + min-width: 74px; + min-height: 32px; + padding: var(--sp-2) var(--sp-5); + border: 1px solid var(--hp-muted); + border-radius: 999px; + background: transparent; + color: var(--hp-txt); + font: inherit; + font-size: var(--fs-m); + cursor: pointer; + } + .entbtn.on { + background: var(--hp-accent); + border-color: var(--hp-accent); + color: var(--text-primary-color, #fff); + font-weight: 600; + } + .inforow { + display: flex; + gap: 10px; + font-size: var(--fs-m); + margin: var(--sp-2) 0; + } + .inforow .k { + color: var(--hp-muted); + min-width: 84px; + } + .inforow a { + color: var(--hp-accent); + word-break: break-all; + } + .infodesc { + font-size: var(--fs-m); + white-space: pre-wrap; + margin-top: var(--sp-3); + } + .infodesc.muted { + color: var(--hp-muted); + } + .pdflist { + display: flex; + flex-direction: column; + gap: var(--sp-2); + } + .pdf { + display: inline-flex; + align-items: center; + gap: var(--sp-2); + color: var(--hp-accent); + text-decoration: none; + } + ha-icon-picker { + display: block; + } + .floorrow { + display: flex; + align-items: center; + gap: var(--sp-4); + padding: var(--sp-3) var(--sp-2); + font-size: var(--fs-m); + cursor: pointer; + } + .floorrow .floorlvl { + color: var(--hp-muted); + font-size: var(--fs-s); + border: 1px solid var(--hp-line); + border-radius: var(--rad-s); + padding: 0 var(--sp-3); + } + .rtest { + display: flex; + align-items: center; + gap: var(--sp-4); + margin-bottom: var(--sp-4); + } + .rtest .namein { flex: 1; } + .rtest ha-icon { color: var(--hp-accent); } + .rtesticon { font-size: var(--fs-s); color: var(--hp-muted); } + .rrow { + display: flex; + align-items: center; + gap: var(--sp-3); + margin: var(--sp-1) 0; + } + .rrow .rpat { flex: 2; } + .rrow .ricon { flex: 1.4; } + .rrow .rpat.bad { border-color: #ff7a5c; } + .rrow .rprev { --mdc-icon-size: 18px; color: var(--hp-txt); min-width: 18px; } + .rrow .ract { + --mdc-icon-size: 16px; + color: var(--hp-muted); + cursor: pointer; + } + :host([data-pointer-hover]) .rrow .ract:hover { color: var(--hp-txt); } + :host([data-pointer-hover]) .rrow .ract.del:hover { color: #ff7a5c; } + .gsrow .gsl { + min-width: 150px; + font-size: var(--fs-m); + color: var(--hp-muted); + } + .optimize-live { + display: grid; + justify-items: start; + gap: var(--sp-2); + margin-bottom: var(--sp-3); + } + .optimize-live .alignmsg, .optimize-live .rhint { margin-bottom: 0; } + .optimize-cleanup { min-height: 44px; } + .optimize-selected { color: var(--hp-txt); } + .optimize-details { + margin-top: var(--sp-3); + color: var(--hp-muted); + font-size: var(--fs-s); + overflow-wrap: anywhere; + } + .optimize-details > summary { + width: fit-content; + color: var(--hp-txt); + cursor: pointer; + font-weight: 600; + } + .optimize-details > summary:focus-visible { + outline: 2px solid var(--hp-accent); + outline-offset: 3px; + border-radius: var(--rad-s); + } + .optimize-details ul { margin: var(--sp-3) 0; padding-inline-start: 22px; } + .optimize-details li + li { margin-top: var(--sp-1); } + .btn.alignall { width: 100%; justify-content: center; } + .backupactions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--sp-3); + } + .backupactions .btn { justify-content: center; min-width: 0; } + .backupupload { display: inline-flex; min-width: 0; } + .backupupload > .btn { width: 100%; justify-content: center; } + .backupupload input { display: none; } + .backupbody { min-width: 0; } + .backupplanonly { margin-inline-start: var(--sp-4) !important; align-items: flex-start !important; } + .backupplanonly > span:first-of-type { display: grid; gap: 2px; white-space: normal; } + .backupplanonly small { color: var(--secondary-text-color); line-height: 1.35; } + .backupplanonlystatus { color: var(--hp-accent) !important; font-weight: 700; } + .backupfile, .backupsummary, .backupcontent { + display: flex; + flex-direction: column; + min-width: 0; + gap: var(--sp-1); + } + .backupfile b, .backupcontent span { + overflow-wrap: anywhere; + } + .backupfile span, .backupsummary span, .backupcontent span { + color: var(--hp-muted); + font-size: var(--fs-s); + } + .backupcounts { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--sp-2) var(--sp-4); + font-size: var(--fs-s); + } + .backupwarn, .backuperror { + border-radius: var(--rad-s); + padding: var(--sp-3); + font-size: var(--fs-s); + line-height: 1.45; + overflow-wrap: anywhere; + } + .backupwarn { background: color-mix(in srgb, var(--hp-accent) 12%, transparent); } + .backuperror { background: rgba(179, 64, 42, .16); color: #ff7a5c; } + .backupdetails { + border: 1px solid var(--hp-line); + border-radius: var(--rad-s); + padding: var(--sp-2) var(--sp-3); + font-size: var(--fs-s); + } + .backupdetails summary { cursor: pointer; font-weight: 700; } + .backupdetails > div { display: grid; gap: var(--sp-1); padding-block-start: var(--sp-2); } + .backupdetails code { overflow-wrap: anywhere; color: var(--hp-muted); } + .backupchoices { + display: flex; + flex-direction: column; + gap: var(--sp-2); + border: 1px solid var(--hp-line); + border-radius: var(--rad-s); + min-width: 0; + } + .backupchoices label { margin: 0 !important; display: flex; gap: var(--sp-2); } + .backupconfirm { align-items: flex-start !important; } + .backupconfirm > span:first-of-type { + min-width: 0; + white-space: normal; + overflow-wrap: anywhere; + } + @media (max-width: 520px) { + .backupactions, .backupcounts { grid-template-columns: 1fr; } + } + .aboutver { + font-size: var(--fs-s); + color: var(--hp-muted); + margin: var(--sp-2) 0 var(--sp-3); + } + .aboutlink { + display: flex; + align-items: center; + gap: var(--sp-3); + width: fit-content; + color: var(--hp-accent); + text-decoration: none; + font-size: var(--fs-m); + padding: var(--sp-1) 0; + } + :host([data-pointer-hover]) .aboutlink:hover { text-decoration: underline; } + .aboutlink ha-icon { --mdc-icon-size: 18px; line-height: 1; } + hp-dialog .body { + padding: var(--sp-5) var(--sp-6); + display: flex; + flex-direction: column; + gap: var(--sp-3); + } + hp-dialog .tapconfirm-body { + min-width: 0; + overflow-x: hidden; + } + hp-dialog .tapconfirm-body p { + max-width: 100%; + min-width: 0; + margin: 0; + overflow-wrap: anywhere; + white-space: normal; + } + hp-dialog .tapconfirm-line { + color: var(--hp-muted); + } + hp-dialog .body label { + font-size: var(--fs-s); + color: var(--hp-muted); + margin-top: var(--sp-3); + } + hp-dialog .body .namein, + hp-dialog .body .areasel { + width: 100%; + box-sizing: border-box; + } + /* Room settings contains long radio labels, two source pickers and live + previews, so it deliberately uses hp-dialog's medium width. Keep its + content shrinkable as well: the generic srcrow rule is nowrap because + compact switch rows need it, but here that created a horizontal scroll + box even on a wide desktop viewport. */ + hp-dialog.roomdialog .body { + min-width: 0; + overflow-x: hidden; + } + hp-dialog.roomdialog .body > * { + min-width: 0; + max-width: 100%; + box-sizing: border-box; + } + hp-dialog.roomdialog .srcrow { + min-width: 0; + align-items: flex-start; + } + hp-dialog.roomdialog .srcrow > span:first-of-type { + min-width: 0; + white-space: normal; + overflow-wrap: anywhere; + line-height: 1.35; + } + hp-dialog.roomdialog .dropbtn > b, + hp-dialog.roomdialog .dropbtn > .ref { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + hp-dialog .row { + display: flex; + justify-content: flex-end; + gap: var(--sp-4); + width: 100%; + min-width: 0; + box-sizing: border-box; + padding: var(--sp-5) var(--sp-6); + border-top: 1px solid var(--hp-line); + } + /* Stable destructive/commit footer contract. A flex spacer cannot react + when translated labels no longer fit: justify-content then overflows + the destructive button through the left inset. Two real groups wrap + as units instead — destructive actions stay left, while Cancel/Save + move together to a right-aligned second row when necessary. */ + hp-dialog .row.dialog-action-footer { + align-items: center; + justify-content: flex-start; + flex-wrap: wrap; + row-gap: var(--sp-4); + } + hp-dialog .dialog-action-group { + display: flex; + flex: 0 1 auto; + flex-wrap: wrap; + align-items: center; + gap: var(--sp-4); + max-width: 100%; + min-width: 0; + } + hp-dialog .dialog-action-group .btn { + flex: 0 0 auto; + min-height: 44px; + } + hp-dialog .dialog-action-danger { + margin-right: auto; + } + hp-dialog .dialog-action-commit { + margin-left: auto; + justify-content: flex-end; + } + hp-dialog .row.markerfooter { + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + } + hp-dialog .row.roomfooter { + align-items: center; + flex-wrap: wrap; + } + /* Device info can have Edit + Open in HA + Close. It uses a wide dialog; + wrapping remains as a phone fallback, but without a flex spacer (which + used to strand Edit alone on the first line). */ + hp-dialog .row.infofooter { + align-items: center; + justify-content: flex-start; + flex-wrap: wrap; + gap: var(--sp-3); + } + hp-dialog .row.infofooter .btn { + flex-shrink: 0; + } + hp-dialog .row.infofooter .infofooter-close { + margin-left: auto; + } + @media (max-width: 480px) { + hp-dialog .row.infofooter { + padding: var(--sp-4) var(--sp-5); + } + } + .markeractions, + .markersaveactions { + display: flex; + align-items: center; + gap: var(--sp-4); + } + .markeractions:empty { display: none; } + .markersaveactions { margin-left: auto; } + .device-inbox { + display: flex; + flex-direction: column; + gap: var(--sp-4); + padding: var(--sp-5) var(--sp-6); + min-width: 0; + } + .device-inbox-dialog { --hp-dialog-wide-width: 920px; } + .device-inbox-head { + display: grid; + grid-template-columns: minmax(180px, 1fr) auto; + align-items: center; + gap: var(--sp-4); + } + .device-inbox-search { + width: 100%; + min-width: 0; + box-sizing: border-box; + border: 1px solid var(--hp-line); + border-radius: var(--rad-m); + background: transparent; + color: var(--hp-txt); + font: inherit; + padding: 11px 14px; + } + .device-inbox-tabs { + display: flex; + gap: var(--sp-2); + overflow-x: auto; + scrollbar-width: thin; + padding-bottom: var(--sp-1); + } + .device-inbox-tabs button { + flex: 0 0 auto; + border: 1px solid var(--hp-line); + border-radius: 999px; + background: transparent; + color: var(--hp-txt); + font: inherit; + padding: 8px 12px; + cursor: pointer; + } + .device-inbox-tabs button.on { + border-color: var(--hp-accent); + background: color-mix(in srgb, var(--hp-accent) 18%, transparent); + } + .device-inbox-tabs button span { color: var(--hp-muted); margin-inline-start: 4px; } + .device-inbox-filters { + display: flex; + flex-wrap: wrap; + gap: var(--sp-4) var(--sp-6); + color: var(--hp-muted); + } + .device-inbox-filters label { + display: inline-flex; + align-items: center; + gap: var(--sp-2); + cursor: pointer; + } + .device-inbox-results { display: grid; gap: var(--sp-3); min-width: 0; } + .device-inbox-row { + display: grid; + grid-template-columns: 42px minmax(180px, 1fr) minmax(180px, auto); + align-items: center; + gap: var(--sp-4); + min-width: 0; + border: 1px solid var(--hp-line); + border-radius: var(--rad-m); + padding: var(--sp-4); + background: color-mix(in srgb, var(--hp-txt) 3%, transparent); + } + .device-inbox-icon { --mdc-icon-size: 28px; color: var(--hp-txt); justify-self: center; } + .device-inbox-copy { min-width: 0; } + .device-inbox-name { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2); } + .device-inbox-new { + border-radius: 999px; + background: var(--hp-accent); + color: var(--text-primary-color, #fff); + font-size: var(--fs-s); + padding: 2px 7px; + } + .device-inbox-meta, + .device-inbox-reason, + .device-inbox-copy code { + display: block; + color: var(--hp-muted); + font-size: var(--fs-s); + overflow-wrap: anywhere; + white-space: normal; + } + .device-inbox-status { color: var(--error-color, #db4437); margin-inline-start: var(--sp-2); } + .device-inbox-actions { + display: flex; + justify-content: flex-end; + align-items: center; + flex-wrap: wrap; + gap: var(--sp-2); + } + .device-inbox-actions .btn { min-height: 36px; padding: 7px 10px; } + .device-inbox-menu { position: relative; } + .device-inbox-menu summary { list-style: none; cursor: pointer; } + .device-inbox-menu summary::-webkit-details-marker { display: none; } + .device-inbox-menu-items { + position: absolute; + z-index: 2; + inset-inline-end: 0; + top: calc(100% + var(--sp-1)); + display: grid; + gap: var(--sp-1); + min-width: 180px; + padding: var(--sp-2); + border: 1px solid var(--hp-line); + border-radius: var(--rad-m); + background: var(--hp-panel, var(--card-background-color, #fff)); + box-shadow: 0 8px 24px rgba(0, 0, 0, .22); + } + .device-inbox-menu-items .btn { justify-content: flex-start; width: 100%; } + .device-inbox-empty { color: var(--hp-muted); text-align: center; padding: var(--sp-8); } + .device-inbox-more { align-self: center; } + @media (max-width: 680px) { + .device-inbox { padding: var(--sp-4); } + .device-inbox-head { grid-template-columns: minmax(0, 1fr); } + .device-inbox-head .btn { justify-self: stretch; } + .device-inbox-tabs { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + overflow-x: visible; + } + .device-inbox-tabs button { + min-width: 0; + overflow-wrap: anywhere; + } + .device-inbox-row { grid-template-columns: 36px minmax(0, 1fr); } + .device-inbox-actions { grid-column: 1 / -1; justify-content: flex-start; } + } +`];var Wd={"editor.context_actions":"Actions: {object}","editor.tool_options":"Tool options: {tool}","editor.palette":"Palette: {tool}","editor.open_group":"Tool group: {group}","editor.group_active":"{group} — active: {item}","editor.disabled_action":"{action} is unavailable: {reason}","editor.loading":"Loading editor…","editor.loading_aria":"The plan editor is loading","editor.load_failed":"Could not load the editor.","editor.refresh_advice":"Refresh the page and try again.","color_picker.title":"Color picker","color_picker.hue":"Hue","color_picker.saturation":"Saturation","color_picker.value":"Brightness","color_picker.hex":"Hex color","color_picker.invalid_hex":"Enter a 3- or 6-digit hex color","btn.properties":"Properties","btn.keep_as_walls":"Keep as walls","btn.delete_room_keep_walls":"Delete room, keep walls","btn.delete_room_with_walls":"Delete room and walls","title.markup_select":"Select and edit walls, columns and saved outlines","title.markup_column":"Column: click a grid point to place a square column","markup.partition":"Partition","markup.column":"Column","markup.hint_column":"click a grid point to place a square column","history.draft_segment":"Add room-draft segment","history.draft_merge":"Join unfinished room outlines","history.draft_segment_delete":"Remove room-draft segment","history.partition_add":"Add partition","history.column_add":"Add column","history.physical_edit":"Change physical object","history.physical_delete":"Delete physical object","history.physical_move":"Move physical object","history.contour_to_partitions":"Convert contour to closed walls","history.wall_chain_finish":"Finish wall chain","history.wall_face_batch":"Create rooms from walls","toast.column_duplicate":"A column with the same centre and outer size already exists","confirm.delete_draft":"Delete the whole unfinished room contour?","confirm.delete_draft_segment":"Delete this draft segment? The remaining contour may split in two.","physical.partition_properties":"Partition properties","physical.column_properties":"Column properties","physical.draft_properties":"Draft segment properties","physical.shape":"Shape","physical.square":"Square","physical.circle":"Circle","physical.diameter":"Diameter","physical.side":"Side","physical.rotation":"Rotation angle","physical.length":"Length","physical.allowed_range":"Allowed: {min}–{max} {unit}","physical.delete_segment":"Delete segment","physical.delete_draft":"Delete entire outline","physical.column_size_title":"Outer side of the square column placed by the click (1–150 cm).","card.title":"House plan","count.devices":"{n} dev.","empty.no_spaces":"No spaces yet.","empty.add_first":"Add the first space and upload a floor plan.","empty.install":'Install the House Plan integration and add it in "Devices & services".',"btn.add_space":"Add space","btn.cancel":"Cancel","btn.save":"Save","btn.close":"Close","btn.delete":"Delete","btn.edit":"Edit","btn.open_in_ha":"Open in HA","btn.reset":"Reset","btn.attach":"Attach…","btn.upload":"Upload…","btn.replace":"Replace…","title.zoom_in":"Zoom in","title.zoom_out":"Zoom out","title.zoom_fit":"Fit all","title.add_device":"Add a device to the plan","title.show_all":"Show hidden and disabled devices as service markers (this tab only)","title.markup":"Room markup: grid, lines, outlines","title.configure_space":"Configure space","title.add_space":"Add space","title.markup_add":"Walls: draw a continuous chain; Shift locks to 45°. Changing tool or leaving the editor finishes it as independent walls","title.markup_merge":"Merge: click one room, then the neighbour it shares a wall with","title.markup_split":"Split a room: click the room, then two points on its walls","title.markup_delroom":"Delete room: click inside a room and confirm","title.need_plan":"Upload a floor-plan image","markup.add":"Walls","markup.merge":"Merge","markup.split":"Split","markup.resize":"Resize","title.markup_resize":"Resize rooms: drag an available horizontal or vertical wall handle","markup.hint_resize":"drag an available wall handle · the wall stops at the first unsafe position · Esc cancels · Ctrl+Z — undo","resize.disabled.diagonal":"This wall is angled; Resize supports only horizontal and vertical walls","resize.disabled.side-angle":"An adjoining wall is angled; Resize requires both adjoining walls to meet this wall at right angles","resize.disabled.duplicate-physical-wall":"A separate partition, unfinished outline, or column overlaps this wall; remove or move it before resizing","resize.disabled.partial-shared":"A neighbouring room uses only part of this wall, so it cannot be moved safely as one shared wall","resize.disabled.unequal-shared":"The neighbouring room uses different endpoints or wall length, so the walls cannot move together safely","resize.disabled.multiple-rooms":"Moving this wall would affect more than two rooms","resize.disabled.thickness-conflict":"The wall thickness cannot be preserved safely","resize.disabled.opening-conflict":"An opening on this wall cannot be moved safely","resize.disabled.invalid-geometry":"This wall cannot be resized without changing the plan structure","resize.limit_stopped":"Wall stopped: the next step would break a junction limit","resize.commit_failed":"Resize was cancelled because the final plan did not pass the geometry check","resize.preview_failed":"Resize stopped at the last safe position because the plan geometry could not be preserved","markup.opening":"Opening","markup.delete_room":"Delete room","history.undo":"Undo","history.redo":"Redo","history.undo_named":"Undo: {name} (Ctrl+Z)","history.redo_named":"Redo: {name} (Ctrl+Shift+Z / Ctrl+Y)","history.undo_empty":"Nothing to undo","history.redo_empty":"Nothing to redo","history.undone":"Undone: {name}","history.redone":"Redone: {name}","history.add_room":"Create room","history.split_room":"Split room","history.merge_rooms":"Merge rooms","history.resize_room":"Resize room","history.wall_thickness":"Change wall thickness","history.add_opening":"Add door or window opening","history.edit_opening":"Edit opening","history.move_opening":"Move opening","history.delete_opening":"Delete opening","history.delete_room":"Delete room","history.delete_room_keep_walls":"Delete room, keep walls","history.delete_room_with_walls":"Delete room and walls","history.decor_add":"Add decor object","history.decor_edit":"Edit decor object","history.decor_move":"Move decor object","history.decor_transform":"Transform decor object","history.decor_delete":"Delete decor object","history.backdrop_transform":"Transform plan backdrop","title.markup_opening":"Openings: click a wall to place, click an opening to edit","opening.new":"New opening","opening.edit":"Opening","opening.door":"Door","opening.window":"Window","opening.gate":"Gate","opening.passage":"Open passage","opening.passage_binding_warning":"Saving will remove the open/close sensor and lock.","opening.invalid_passage_fields":"The open passage on plan “{room}” has unsupported settings: {fields}.","opening.type_label":"Type","opening.length_label":"Length, cm","opening.contact_label":"Open/close sensor","opening.lock_label":"Lock","opening.none":"— none —","opening.search_ph":"Search: name or entity id…","opening.invert":"Invert open/closed","opening.flip_h":"Hinge on the other jamb","opening.flip_v":"Opens to the other side","opening.open":"Open","opening.closed":"Closed","opening.locked":"Locked","opening.unlocked":"Unlocked","opening.state_unknown":"unavailable","opening.no_entities":"No sensors bound — a static symbol on the plan.","toast.opening_no_wall":"Click next to a room wall or independent wall","opening.host_partition":"Independent wall","opening.partition_orphan":"The independent wall for this opening no longer exists","opening.partition_jamb_margin":"Leave at least {distance} between the opening and the end of the wall","opening.rebind_partition":"Attach to another independent wall","confirm.delete_partition_openings_title":"Delete wall and openings?","confirm.delete_partition_openings_body":"This wall contains {count} opening(s). They will be deleted together.","confirm.delete_partition_openings_item":"• {type}, {length}","markup.delete":"Delete","markup.hint_points":"points: {n} · Shift — 45° steps · Esc — finish chain · Ctrl+Z — undo a point · closing an area offers a room","markup.hint_start":"click a grid dot to start a wall chain","tip.lqi":"average zigbee signal:","tip.area":"area: {value}","info.device_header":"Device on the plan","info.model":"Model","info.state":"State","info.link":"Link","info.manuals":"Manuals","info.none":"No additional information","marker.new_device":"New device","marker.name_label":"Name (shown on the plan)","marker.name_ph":"Name","marker.binding_label":"Bind to an HA device","marker.binding_disabled":"disabled in Home Assistant","marker.virtual_option":"Virtual device (no binding)","marker.search_ph":"Search device / group…","marker.nothing_found":"nothing found","marker.room_label":"Room","marker.room_override":" (override placement)","marker.room_choose":"— select a room —","marker.room_auto":"— by device area (auto) —","marker.icon_label":"Icon","marker.icon_ph":"mdi:… (empty = auto)","marker.display_label":"Display","display.badge":"Icon + state","display.icon_ripple":"Icon + state and activity","display.static_icon":"Always static icon","marker.display_hint":"Icon + state changes the plate for work, open, alarm and unavailable states. Icon + state and activity additionally shows a short pulse for events and a continuous pulse for ongoing work, movement or presence. Value + state replaces the icon with one unambiguous HA value. A static icon never reacts to device states.","marker.display_hint_badge":"The icon and dynamic plate show device state without the ordinary activity pulse. Red alarms remain visible.","marker.display_hint_icon":"The icon and dynamic plate show device state without the ordinary activity pulse. Red alarms remain visible.","marker.display_hint_icon_ripple":"The icon, dynamic plate and pulse show a short pulse for events and a continuous pulse for work, motion or presence. Red alarms have separate priority.","marker.display_hint_value":"One unambiguous Home Assistant value replaces the icon while the plate continues to show state. Red alarms remain visible.","marker.display_hint_static_icon":"The theme-aware shell and icon always stay the same. State, activity, unavailability and alarms do not change the face.","marker.static_alarm_warning":"This device can report alarms. Static display hides the marker's visual alarm indication.","marker.preview.title":"Display preview","marker.preview.actual":"Now","marker.preview.example":"Example","marker.preview.integration":"Provided by","marker.preview.source":"Display source","marker.preview.current_state":"Current state","marker.preview.result":"On the plan","marker.preview.details":"Source details","marker.preview.select_source":"Choose a Home Assistant device or entity to see its actual display.","marker.preview.unknown_provider":"Unknown integration","marker.preview.virtual_provider":"House Plan · virtual device","marker.preview.no_source":"No active source","marker.preview.no_state":"No current state","marker.preview.mixed_states":"Several different states","marker.preview.multiple_sources":"{n} sources","marker.preview.more_sources":"+{n} more","marker.preview.scaled":"The preview is scaled to {n}% to fit. The saved size is unchanged.","marker.preview.demo_activity":"Show activity example","marker.preview.demo_notice":"Temporary activity example; the actual device state is unchanged.","marker.preview.demo_short":"Show short activity","marker.preview.demo_continuous":"Show continuous activity","marker.preview.stop_continuous":"Stop continuous activity","marker.preview.demo_short_notice":"Short activity example; the actual device state is unchanged.","marker.preview.demo_continuous_notice":"Continuous activity example; the actual device state is unchanged.","marker.preview.demo_already_visible":"The current state already shows a real activity or alarm effect.","marker.preview.reduced_motion":"System reduced-motion is enabled, so ordinary activity is shown as a dot.","marker.preview.reason.neutral":"Neutral dark plate","marker.preview.reason.working":"Yellow plate: the device is working now","marker.preview.reason.working_activity":"Yellow plate and activity effect: the device is working now","marker.preview.reason.open":"Orange plate: physically open or unlocked","marker.preview.reason.cover_icon_state":"Neutral plate; the cover entity controls the displayed state and icon","marker.preview.reason.presence":"Activity effect while presence is detected","marker.preview.reason.event":"Short activity effect after a detected event","marker.preview.reason.transition":"Activity effect while the device is moving or changing state","marker.preview.reason.media_neutral":"Media devices stay on a neutral dark plate while available","marker.preview.reason.unavailable":"Subdued neutral plate: unavailable or off","marker.preview.reason.alarm":"Red alarm plate; alarm indication is always shown","marker.preview.reason.live_states_disabled":"Live-state styling is disabled for this card","marker.preview.reason.value_no_state":"No usable state value; the icon is shown instead","marker.preview.reason.value_ambiguous_sources":"Several possible value sources; the icon is shown instead","marker.preview.reason.value_non_scalar":"The source did not return a simple value; the icon is shown instead","marker.preview.reason.value_virtual":"A virtual device has no Home Assistant value; the icon is shown instead","marker.preview.reason.vacuum_live_plan_only":"Live vacuum position and trail are available only on the full plan","marker.preview.reason.hidden_design_preview":"The device is hidden on the plan; this design preview remains visible","marker.preview.reason.composite_power_source":"State comes from the device Power entity; auxiliary switches are ignored","marker.preview.reason.activity_display_disabled":"The selected display mode does not show ordinary activity effects","marker.preview.reason.ha_disabled":"The Home Assistant binding is disabled and will be hidden on the plan","marker.preview.reason.orphaned":"The saved Home Assistant binding can no longer be found","marker.preview.reason.static_icon":"Static mode: device state does not change the icon","marker.activity_color":"Activity pulse color","marker.ripple_size":"Activity pulse size","marker.activity_alarm_note":"Color and size affect ordinary activity only and do not change red alarms.","marker.pulse_a11y_alarm":"Alarm","marker.pulse_a11y_event":"Recent event","marker.pulse_a11y_presence":"Presence detected","marker.pulse_a11y_transition":"Changing state","marker.pulse_a11y_running":"Working","marker.state_a11y_neutral":"Default state","marker.state_a11y_working":"Working","marker.state_a11y_open":"Open","marker.state_a11y_locked":"Locked","marker.state_a11y_unlocked":"Unlocked","marker.state_a11y_alarm":"Alarm","marker.state_a11y_unavailable":"Unavailable","marker.lqi_a11y_low":"LQI {value}, low signal","marker.lqi_a11y_mid":"LQI {value}, medium signal","marker.lqi_a11y_high":"LQI {value}, high signal","marker.size_label":"Icon size / rotation","marker.angle_label":"Rotate","marker.model_label":"Model","marker.model_ph":"e.g. Aqara T&H","marker.link_label":"Link","marker.desc_label":"Description","marker.desc_ph":"Notes, specs…","marker.manuals_label":"Manuals (PDF etc.)","marker.sub_device":"device","marker.sub_z2m_group":" · Z2M group","marker.sub_group":"group","marker.sub_helper":"helper","space.new":"New space","space.header":"Space","space.title_label":"Title","space.title_ph":"e.g. Garage","space.plan_label":"Floor plan (background)","space.no_plan":"no plan image","space.plan_alt":"plan","room.new":"New room","room.name_label":"Display name","room.name_ph":"e.g. Terrace","room.area_label":"Home Assistant area (unassigned)","room.no_area_option":"— no area —","room.default_name":"Room","device.unnamed":"unnamed","device.light_group":"light group","device.fallback":"device","device.virtual":"virtual device","confirm.delete_room":'Delete room "{name}"?',"confirm.delete_room_title":'Delete room "{name}"?',"confirm.delete_room_body":"Choose whether the room's exclusive physical walls should remain. Walls shared with another room are always kept.","confirm.merge_rooms":'Merge rooms "{a}" and "{b}"?',"confirm.remove_marker":'Delete "{name}" from the plan? The device will disappear completely and stop contributing to plan data. You can add it again later.',"confirm.erase_decor":"Erase the {kind} object? You can undo this action from the editor history.","confirm.delete_space":'Delete space "{title}" with all its rooms and markup?',"space.delete_blocked":"This space is still used by {n} device(s). Move them to another space or delete them first.","toast.pos_save_failed":"Failed to save position: {err}","toast.no_entity":"The device has no suitable entity","toast.ha_disabled_action":"A Home Assistant object that is disabled cannot be used on the plan.","toast.ha_disabled_show_device":"A device disabled in Home Assistant cannot be shown on the plan. Enable it in Home Assistant first.","toast.ha_disabled_show_entity":"An entity disabled in Home Assistant cannot be shown on the plan. Enable it in Home Assistant first.","toast.ha_disabled_add":"A disabled Home Assistant object cannot be added to the plan. Enable it in Home Assistant first.","toast.ha_binding_unverified":"The object status could not be verified through the Home Assistant registry. Display and actions are temporarily unavailable.","toast.markup_needs_server":"Markup is available after the config is moved to the server","toast.space_order_changed":"Order changed. If any card pins its floor by number, check those panels.","toast.conflict":"Config was changed in another window — data refreshed, repeat your last action","toast.cfg_save_failed":"Failed to save config: {err}","toast.room_overlap":"The outline overlaps room “{name}” — rooms must not overlap","toast.contour_min_edges":"Draw at least two edges before closing the outline","toast.contour_cannot_close":"The outline cannot close because it is degenerate or intersects itself","toast.merge_not_adjacent":"Only rooms that share a wall can be merged","toast.rooms_merged":"Rooms merged into “{name}”","toast.split_pick_wall":"Start the cut on the room’s wall","toast.split_bad_cut":"The cut must run wall to wall inside the room, without crossing walls or itself","merge.header":"Merge rooms","merge.hint":"The merged room keeps one name and one area. The other area is released — its devices leave the plan until another room claims it.","merge.keep":"Keep","merge.no_area":"no area","room.split_header":"New room from the split","toast.room_saved":"Room saved ({n}). Devices added: {added}. Outline the next one or exit markup.","toast.room_saved_no_area":"Room saved ({n}, no area). Outline the next one or exit markup.","toast.marker_needs_server":"Device editing is available after the config is moved to the server","toast.virtual_name_required":"Enter a name for the virtual device","toast.marker_saved":"Device saved","toast.marker_removed":"Device deleted from the plan","toast.integration_missing":"The House Plan integration is not installed — management unavailable","toast.plan_formats":"Supported formats: SVG, PNG, JPG, WebP","toast.plan_required":"Upload a floor plan — it is required","toast.space_added_onboard":"Space added. Outline the rooms: click grid dots and close the contour.","toast.space_added":"Space added","toast.space_saved":"Space saved","toast.space_deleted":"Space deleted","toast.delete_failed":"Delete failed: {err}","toast.error":"Error: {err}","toast.toggle_target_unavailable":"Target “{name}” is unavailable — no action was performed","toast.toggle_targets_unavailable":"Targets are unavailable: {names}. No action was performed","toast.file_failed":'File "{name}" was not uploaded: {err}',"toast.files_attached":"Files attached: {n}","err.unknown":"unknown error","err.code":"code {code}","err.too_large":"file larger than {mb} MB","err.bad_ext":"unsupported type (PDF/image expected)","err.unauthorized":"administrator rights required","editor.title":"Title","editor.default_floor":"Initial space","editor.default_floor_missing":"Initial space “{id}” no longer exists. Choose another space.","editor.floor":"Fixed space","editor.floor_none":"— not fixed —","editor.floor_index":"YAML index {index} (preserved)","fixed_floor.loading":"Loading the fixed space…","fixed_floor.invalid_title":"The fixed space is unavailable","fixed_floor.invalid_body":"Check floor in the card configuration. The configured value is: {value}","editor.icon_size":"Icon size, % of plan width","editor.show_temperature":"Show temperature","editor.live_states":"Live states (on/off, open…)","editor.show_signal":"Show zigbee signal (LQI)","editor.language":"Interface language","editor.lang_auto":"Auto (HA profile)","editor.lang_en":"English","editor.lang_ru":"Русский","title.icon_rules":"Icon rules: which MDI icon devices get by name","rules.title":"Icon rules","rules.hint":"Rules are checked top-down against “device name + model” (case-insensitive regex); the first match wins. When nothing matches, the entity device class decides, then the generic chip icon.","rules.pattern_ph":"regex, e.g. plug|socket","rules.icon_ph":"mdi:power-socket-de","rules.add":"Add rule","rules.reset":"Reset to defaults","rules.test_ph":"Try a device name…","rules.invalid":"invalid regex","rules.saved":"Icon rules saved","btn.up":"Up","btn.down":"Down","tap.info":"Device card","tap.more_info":"HA more-info dialog","tap.toggle":"Toggle state","marker.tap_label":"Tap action for this device","tap.toggle_note":"The resolved state is shown below. Secure devices are never toggled from the plan.","import.title":"Create spaces from HA floors","import.hint":"Your Home Assistant already knows these floors. Pick the ones to turn into plan spaces — you will upload a floor-plan image for each one next. Rooms are then outlined by hand on the plan.","import.start":"Create {n} space(s)","import.manual":"Start from scratch","import.progress":"Floor {i} of {n}","import.done":"Spaces created. Outline the rooms: click grid dots and close the contour.","btn.skip":"Skip","space.scale_label":"Scale (grid cell size)","space.scale_unit":"cm per cell","space.scale_unit_imperial":"in per cell","space.display_section":"Display","space.show_borders":"Always show room borders","space.zero_wall_style":"Zero-thickness walls","space.zero_wall_dashed":"Dashed","space.zero_wall_solid":"Solid","space.zero_wall_help":"Dashed walls let light through. Solid walls block light even at zero thickness.","space.show_names":"Show room names (drag to move)","space.room_color":"Border & name color","space.opacity":"Opacity","space.fill_label":"Room fill","fill.none":"None","fill.lqi":"Zigbee signal","fill.light":"Lights","fill.custom":"Custom color","space.custom_fill":"Fill color","space.source_file":"I have a floor-plan image","space.source_draw":"No image — I'll outline rooms by hand","space.orientation":"Canvas","orient.landscape":"Landscape","orient.portrait":"Portrait","orient.square":"Square","fill.temp":"Temperature","space.temp_min":"Comfort from","space.temp_max":"to","tip.temp_avg":"average temperature:","tip.hum_avg":"average humidity:","space_card.button":"Open the space plan","space_card.not_found":"Space “{id}” not found","space_card.loading":"Loading…","continuity.restore_plan":"Restoring floor plan…","continuity.restore_connection":"Restoring device connection…","continuity.retry":"Try again","editor.space":"Space","editor.show_button":"Show button","editor.button_label":"Button label","editor.button_target":"Target dashboard path","marker.sub_entity":"entity","title.general_settings":"General settings","gs.title":"General settings","gs.hint":"Fill colors apply to every space; each color has its own opacity. Which fill mode a space uses is set in that space's dialog.","gs.light_group":"Fill: lights","gs.light_on":"Lights on","gs.light_off":"All lights off","gs.temp_group":"Fill: temperature","gs.temp_cold":"Cold","gs.temp_ok":"Comfortable","gs.temp_hot":"Hot","gs.lqi_group":"Fill: zigbee signal","gs.lqi_low":"Weak signal","gs.lqi_high":"Strong signal","gs.reset":"Reset to defaults","gs.saved":"General settings saved","space.show_lqi":"Show zigbee signal (LQI) next to devices","space.hide_decor":"Hide the decorative layer","space.hide_decor_tip":"Lines, shapes, labels and furniture stay where they are — visible in the backdrop editor, not on the plan.","space.hide_openings":"Hide openings","space.hide_openings_tip":"Door, window and gate symbols are not drawn, but the openings keep working: light passes through, the sun comes in at a window, contact sensors still open them. The plan editor always shows them.","gs.light_none":"No light sources","mode.plan":"Plan editor","mode.devices":"Device editor","display.value":"Value + state","marker.subarea":"no area, manual","device.new":"New device — open its editor to dismiss","opening.unlock_action":"Unlock","opening.lock_action":"Lock","opening.lock_pending":"Working…","title.close_editor":"Close editor (back to view)","devbar.add":"Add","devbar.show_all":"Hidden and disabled","devbar.rules":"Icon rules","device_inbox.button":"Devices","device_inbox.title":"Devices on the plan","device_inbox.search":"Search devices, entities and integrations…","device_inbox.add_virtual":"Add virtual device","device_inbox.tab_on_plan":"On plan","device_inbox.tab_available":"Available","device_inbox.tab_hidden":"Hidden","device_inbox.tab_readd":"Available again","device_inbox.only_new":"New only","device_inbox.show_entities":"Show entities","device_inbox.show_hidden":"Show hidden on plan","device_inbox.new":"New","device_inbox.find":"Find on plan","device_inbox.edit":"Edit","device_inbox.hide":"Hide","device_inbox.show":"Show","device_inbox.add":"Add","device_inbox.readd":"Add again","device_inbox.hide_available":"Hide from list","device_inbox.show_more":"Show more","device_inbox.more_actions":"More actions","device_inbox.find_hidden_hint":"Enable “Show hidden on plan” first","device_inbox.show_disabled":"Activate this binding in Home Assistant before showing it","device_inbox.empty_on_plan":"No matching devices are on the plan yet.","device_inbox.empty_available":"No devices are available to add.","device_inbox.empty_hidden":"There are no hidden devices.","device_inbox.empty_readd":"No removed devices are available to add again.","device_inbox.reason_visible_auto":"Discovered automatically","device_inbox.reason_visible_explicit":"Added explicitly","device_inbox.reason_manual_hidden":"Hidden by user","device_inbox.reason_automatic_hidden":"Hidden automatically","device_inbox.reason_service_entry":"Service-only registry entry","device_inbox.reason_excluded_integration":"Integration excluded by device filters","device_inbox.reason_excluded_domain":"Non-spatial entity type","device_inbox.reason_grouped_light":"Represented by a room light group","device_inbox.reason_represented_by_parent":"Represented by its parent device","device_inbox.reason_removed":"Previously removed from the plan","device_inbox.reason_available":"Available to add to the plan","device_inbox.reason_no_bound_room":"HA area is not linked to a plan room","device_inbox.status_ha_disabled":"Disabled in Home Assistant","device_inbox.status_orphaned":"Binding is no longer present in Home Assistant","device_inbox.status_unverified":"Home Assistant registry is temporarily unavailable","device_inbox.saved":"Device list updated","space.roomcard_section":"Room card shows:","space.label_temp":"Temperature","space.label_hum":"Humidity","space.label_lqi":"Average Zigbee signal","space.label_light":"Lights on/off","roomcard.light_on":"On","roomcard.light_off":"Off","roomcard.light_partial":"{on} of {total}","toast.split_pick_inside":"Intermediate cut points must be inside the room","mode.decor":"Background editor","decor.select":"Select","decor.line":"Line","decor.rect":"Rectangle","decor.ellipse":"Oval","decor.text":"Text","decor.erase":"Erase","decor.erase_confirm_title":"Erase object?","decor.color":"Color","decor.width":"Line width","decor.line_style":"Line style","decor.line_style_solid":"Solid","decor.line_style_dashed":"Dashed","decor.fill":"Fill","decor.fill_color":"Fill color","decor.length":"Length","decor.size":"Size","decor.angle":"Rotation angle, °","decor.text_size":"Text size","decor.backdrop_properties":"Plan backdrop properties","decor.text_title":"Text label","decor.object_title":"Edit {kind}","decor.text_label":"Text","decor.live_group":"Insert HA variable","decor.live_entity":"Entity","decor.live_entity_ph":"choose an entity","decor.live_attr":"Value","decor.live_attr_ph":"choose state or attribute","decor.live_state":"State","decor.backdrop":"Backdrop image","decor.backdrop_hint":"Drag to move; pull a corner to resize; use the top handle to rotate. Shift changes proportions or frees the angle.","decor.backdrop_reset":"Reset the picture","decor.backdrop_reset_done":"The picture is back at its original place and size","marker.icon_auto":"Auto: {icon} (by icon rules; pick one to override)","marker.icon_pin_auto":"Pin","mode.plan_tip":"Plan editor — the geometry of the home: draw and split/merge rooms, bind them to HA areas, place doors, windows and gates, move room cards, set the scale","mode.devices_tip":"Device editor — everything about icons: drag to position, click to edit binding/icon/display, add virtual devices, icon rules","mode.decor_tip":"Background editor — purely visual decor under the plan: lines, rectangles, ovals and text labels that never react to clicks","space.glow_enabled":"Light-source glow","gs.glow_group":"Light-source glow","gs.glow_base":"House darkness","gs.glow_light":"Default light color / intensity","gs.wall_group":"Walls","gs.wall_fill":"Wall fill","gs.glow_radius":"Glow radius","gs.unit_m":"m","gs.unit_ft":"ft","marker.controls_label":"Controls other light sources","marker.controls_hint":"With tap action “Toggle”, a click flips every source added here. The marker’s own bound entity is controlled directly and cannot be added; use the switch below to classify it as a light.","marker.controls_filter":"Search lights and switches…","info.controls":"Controls","marker.glow_radius_label":"Glow radius","marker.glow_radius.help":"Sets the glow radius in metres or feet; an empty value uses the radius from general settings.","marker.glow_radius.help.aria":"Help: glow radius","markup.wallthick":"Thickness","markup.select":"Select","title.markup_wallthick":"Thickness — click a wall to set its thickness from 0 to 100 cm.","markup.hint_wallthick":"click a wall · Esc closes without applying","wallthick.field":"Thickness","wallthick.unit_cm":"cm","wallthick.unit_in":"in","wallthick.apply_room":"Apply to all walls of this room","markup.draw_wall_title":"Thickness of each new wall segment (0–100 cm). Shared walls keep the existing value.","room.queue_progress":"Room {current} of {total}","toast.wall_rooms_saved":"Created rooms: {n}","toast.wall_chain_saved":"Wall chain saved","toast.wallthick_pick":"Click a wall","toast.wallthick_set":"Wall thickness set","toast.wallthick_cleared":"Wall thickness removed","toast.physical_range":"Enter a value from {min} to {max} {unit}","toast.zero_wall_opening_conflict":"Remove the opening on this wall segment first.","toast.zero_wall_ambiguous":"The wall segment is ambiguous. Simplify or adjust the junction.","toast.zero_wall_migration_blocked":"The space was not converted: {reason}. No data was changed.","toast.physical_angle":"Enter a rotation angle from 0° up to, but not including, 90°","toast.physical_limit":"The space has reached the limit for this type of geometry","toast.geometry_unsafe":"Change canceled: wall geometry could not be built safely.","junction.limit_angle":"The angle between walls is too sharp: {actual}°, minimum {limit}°.","junction.limit_valence":"Too many walls meet in one node: {actual}, maximum {limit}.","junction.limit_length":"The wall segment is too short: {actual} cm, minimum {limit} cm.","junction.limit_distance":"Walls and nodes are too close: {actual} cm, minimum {limit} cm.","junction.limit_clearance":"No room interior is left: {actual} cm², minimum {limit} cm².","junction.limit_check_failed":"The junction check could not run — the change was not saved","toast.wall_model_migration_blocked":"The wall model could not be updated: {reason}. The plan was not changed. Run “Optimize plans”; if the error repeats, fix the conflicting wall geometry.","toast.wall_model_client_outdated":"Update the card and reload the page before editing the plan.","wall_model.reason.invalid-room":"invalid room contour","wall_model.reason.zero-length":"zero-length wall segment","wall_model.reason.third-owner":"wall shared by more than two rooms","wall_model.reason.duplicate-id":"conflicting wall identifiers","wall_model.reason.thickness-conflict":"conflicting wall thickness values","wall_model.reason.opening-host":"opening cannot be assigned to one wall","toast.delete_room_pick":"Click inside the room you want to delete","toast.plan_snap_ambiguous":"Zoom in to choose the wall node","toast.wall_repair_ambiguous":"The room outline has more than one possible connection. Zoom in and connect it explicitly.","toast.wall_repair_too_large":"The gap is larger than 2 cm. Connect the walls before creating the room.","toast.wall_repair_changed":"The wall geometry changed. Try creating the room again.","toast.opening_on_zero_wall":"Openings cannot sit on a zero-thickness wall","marker.from_ha_option":"Pick from the HA list","marker.show_entities":"Show entities","marker.show_entities_tip":"Adds not only devices to the list, but all their entities too","marker.pick_ph":"Choose a device…","room.open_area":"Open the HA area","view.volumetric":"Volumetric view","view.flat":"Flat view","kiosk.title":"This screen's sizes","kiosk.hint":"Stored on this device only — every wall tablet or TV can have its own comfortable sizes.","kiosk.icon_scale":"Device icon size","kiosk.font_scale":"Room card text size","editor.kiosk":"Wall device (kiosk) mode","editor.cycle":"Auto-switch spaces every N seconds (kiosk, 0 = off)","room.settings_title":"Room settings","room.settings_section":"Room settings (override the space)","room.fill_label":"Fill in THIS room","fill.inherit":"As the space","room.custom_fill_space":"Space color","room.custom_fill_own":"Room color","room.temp_src_label":"Temperature source","room.hum_src_label":"Humidity source","room.src_average":"Average over the room's sensors (default)","room.src_pick":"A specific HA device or entity","room.src_ph":"Choose a source…","toast.room_updated":"Room updated","space.card_font":"Room-card font size (whole space)","room.sizes_section":"Font sizes","room.name_scale":"Room name size","room.label_scale":"Metrics size","preview.room_name":"Living room","toast.cfg_reload_failed":"Could not reload the plan from the server: {err}","room.settings_short":"Room settings","room.unnamed":"Unnamed room","marker.use_climate_temp":"Include the device temperature in the room","marker.use_climate_temp_tip":"Adds an air conditioner or thermostat current_temperature to the room average. Configure the external value badge separately below.","marker.value_badge_title":"Value badge","marker.value_badge.help":"Shows one selected value next to the icon. It does not affect room metrics, light, Glow or the tap action.","marker.value_badge.help.aria":"Help: device value badge","marker.value_badge_enabled":"Show a value badge","marker.value_badge_source":"Value","marker.value_badge_source.help":"Choose a specific state, supported attribute, or derived value of this device.","marker.value_badge_source.help.aria":"Help: value badge source","marker.value_badge_position":"Position","marker.value_badge_position.help":"The chosen side stays fixed and does not flip automatically at a plan edge.","marker.value_badge_position.help.aria":"Help: value badge position","marker.value_badge_right":"Right","marker.value_badge_bottom":"Below","marker.value_badge_left":"Left","marker.value_badge_top":"Above","marker.value_badge_empty":"This device has no available values","marker.value_badge_static":"A static icon does not show live values. The setting is preserved.","marker.value_badge_missing":"Source unavailable","marker.value_badge_missing_hint":"The saved source is currently unavailable. Keep it, disable the badge, or choose another value.","marker.value_badge_duplicate":"The same value is already shown inside the icon.","marker.value_badge_state":"State · {name}","marker.value_badge_marker_state":"Light-source state · {name}","marker.value_badge_lqi":"Average Zigbee signal quality","marker.value_badge_attr_current_temperature":"Current temperature · {name}","marker.value_badge_attr_temperature":"Target temperature · {name}","marker.value_badge_attr_current_humidity":"Current humidity · {name}","marker.value_badge_attr_humidity":"Humidity · {name}","marker.value_badge_attr_current_position":"Position · {name}","marker.value_badge_attr_percentage":"Speed · {name}","marker.value_badge_attr_brightness":"Brightness · {name}","marker.value_badge_attr_volume_level":"Volume · {name}","marker.value_badge_attr_battery_level":"Battery · {name}","marker.value_badge_attr_fan_speed":"Fan speed · {name}","marker.light_role_label":"Is this device a light source?","marker.light_role.help":"Controls whether this marker itself is a spatial light source. Linked lights above remain independent room-light controls.","marker.light_role.help.aria":"Help: whether this device is a light source","marker.light_role_auto_yes":"Auto (light source)","marker.light_role_auto_no":"Auto (not a light source)","marker.light_role_always":"Always a light source","marker.light_role_never":"Never a light source","marker.light_entity_label":"Leading light entity","marker.light_entity.help":"For a composite device, choose the entity whose state, colour and brightness represent this plan source. The automatic fallback remains compatible with older plans.","marker.light_entity.help.aria":"Help: leading light entity","marker.light_entity_auto":"Automatic ({entity})","marker.light_entity_none":"no controllable entity","marker.light_entity_missing":"The saved entity {entity} is unavailable. House Plan temporarily uses {fallback}; the saved choice will be restored if it returns.","marker.toggle_entity_label":"Entity to toggle","marker.toggle_entity.help":"For a composite device, choose the exact own light or switch operated by a tap. Automatic keeps the previous target rules and is independent from the leading light entity.","marker.toggle_entity.help.aria":"Help: entity to toggle","marker.toggle_entity_auto":"Automatic ({entity})","marker.toggle_entity_none":"no own controllable entity","marker.toggle_entity_missing":"The saved entity {entity} is no longer among this marker’s selectable channels. House Plan temporarily uses {fallback}; the saved choice will be restored if it returns.","marker.glow_color_label":"Glow colour and brightness","marker.glow_mode.help":"Use live source values, override only its colour, or fix both colour and brightness. The minimum is 1%; to disable the source, choose “Never”.","marker.glow_mode.help.aria":"Help: glow colour and brightness","marker.glow_mode_auto":"From source","marker.glow_mode_color":"Set colour","marker.glow_mode_fixed":"Set colour and brightness","marker.glow_color":"Glow colour","marker.glow_brightness":"Brightness","marker.glow_disabled_never":"Glow settings are unavailable because this marker is explicitly not a light source.","marker.glow_disabled_auto":"Auto mode found no spatial light source for this marker.","marker.glow_disabled_no_entity":"No active controllable Home Assistant entity is available for this spatial source.","marker.glow_passive_hint":"This source has no own Home Assistant data. Set its colour and brightness manually; its radius remains available.","marker.control_broken":"Saved source is missing or no longer marked as a light","marker.control_missing_label":"Missing plan light","marker.control_passive":"passive source","toast.marker_control_cycle":"This link would create a circular chain of light controls.","toast.marker_binding_required":"Choose a Home Assistant device before linking another light source.","confirm.unlock":"Unlock “{name}”?","toast.files_migrate_failed":"Attachments could not be moved to the new binding, links keep pointing at the old files: {err}","space.pick_saved":"Already uploaded","space.pick_saved_hint":"Plans stored on the server, including ones you detached earlier","space.no_saved":"No plans stored on the server yet.","space.loading":"Loading…","space.used_by":"in use: {list}","space.in_use":"A space still uses this plan — detach it first","btn.use":"Use","confirm.delete_plan":'Delete the plan file "{name}" from the server? This cannot be undone.',"toast.plans_list_failed":"Could not list the stored plans: {err}","toast.plan_delete_failed":"Could not delete the plan: {err}","marker.hide":"Hide","marker.hide_tip":'The device will disappear from the plan after saving but will still count toward the room signal. Restore it through the "Devices" catalog in the device editor.',"marker.show":"Show","marker.show_tip":"The device will appear on the plan again after saving.","marker.hidden_ghost":"Device hidden by the user","marker.ha_disabled_device":"The device is disabled in Home Assistant and hidden from the plan.","marker.ha_disabled_entity":"The entity is disabled in Home Assistant and hidden from the plan.","marker.ha_disabled_all_entities":"The device has no active Home Assistant entities, so it is hidden from the plan.","marker.ha_registry_limited":"The full Home Assistant registry is unavailable to this user. The unverified object cannot be shown or used for now.","marker.delete_tip":"Completely delete the device from the plan and every aggregate. You can add it again later.","tap.run":"Run automation/script/scene","tap.cover":"Open/close (curtains/blinds)","marker.run_target_label":"What to run","marker.run_search_ph":"Search: automation, script or scene…","marker.run_target_gone":"Target {id} not found — pick again","marker.tap_confirm":"Ask for confirmation","marker.tap_confirm_tip":"Show a confirmation dialog before acting — a guard against accidental taps.","marker.toggle_hint_single":"Target: {name} ({id}).","marker.virtual_light_target":"Virtual light: {name}.","marker.virtual_light_current":"Manual state: {state} → {effect}.","marker.virtual_light_state_on":"on","marker.virtual_light_state_off":"off","marker.toggle_hint_group":"Will toggle {count} source(s): {names}.","marker.toggle_hint_current":"Now: {state} → {effect}.","marker.toggle_hint_group_current":"Currently on: {on} of {count} → {effect}.","marker.toggle_hint_skipped":"Will be skipped ({count}): {targets}.","marker.toggle_effect_turn_on":"will turn on","marker.toggle_effect_turn_off":"will turn off","marker.toggle_effect_open":"will open","marker.toggle_effect_close":"will close","marker.toggle_effect_stop":"will stop","marker.toggle_effect_toggle":"state will toggle","marker.toggle_skip_missing":"not found","marker.toggle_skip_ha_disabled":"disabled in HA","marker.toggle_skip_unavailable":"unavailable","marker.toggle_skip_unsupported":"toggle is unsupported","marker.toggle_skip_secure":"blocked for security","marker.toggle_none_no_binding":"This device has no binding, so there is no state to toggle. Tapping will do nothing.","marker.toggle_none_no_actionable_entity":"This device has no state that can be toggled. Tapping will do nothing.","marker.toggle_none_configured_targets_missing":"The configured targets are unavailable. The device's own entity will not be substituted for them.","marker.toggle_none_ha_disabled":"The target is disabled in Home Assistant and cannot be used on the plan.","marker.toggle_none_unavailable":"The target is currently unavailable. Tapping will do nothing.","marker.toggle_none_unsupported":"Home Assistant exposes no safe toggle operation for this entity. Tapping will do nothing.","marker.toggle_none_secure":"Toggling locks, alarms and secure gates from the plan is blocked for security.","run.automation":"automation","run.script":"script","run.scene":"scene","confirm.tap_run":'Run "{name}"?',"confirm.tap_toggle":'Toggle "{name}"?',"confirm.tap_cover":'Open/close "{name}"?',"confirm.current_state":"Current state: {state}","confirm.expected_state":"After switching: {state}","confirm.group_current":"on {on} of {total}","confirm.group_all_on":"all are on","confirm.group_all_off":"all are off","confirm.unavailable_targets":"Unavailable: {count}","confirm.expected_by_ha":"Home Assistant will determine the state","confirm.state_on":"On","confirm.state_off":"Off","confirm.state_open":"Open","confirm.state_closed":"Closed","confirm.state_opening":"Opening","confirm.state_closing":"Closing","confirm.state_stopped":"Stopped","confirm.state_unknown":"Unknown","toast.run_started":"Started: {name}","toast.run_target_missing":"Run target not found — check the device settings","toast.run_target_required":"Pick an automation, script or scene","toast.tap_target_changed":"The action target changed. Try again.","toast.virtual_light_toggle_failed":"Could not toggle the virtual light: {err}","toast.value_badge_source_required":"Choose a value for the badge","btn.run":"Run","vac.section":"Robot vacuum: live position","vac.autocal":"Set up automatically","vac.live":"Live position on the plan","vac.trail":"Show the robot's path","vac.cal_maps":"Calibrated maps: {maps}","vac.autocal_no_rooms":"The integration reports no room list — open “Fit manually”","vac.autocal_no_match":"Room names did not match (need ≥3 in common) — open “Fit manually”","vac.autocal_done":"Done: bound via {rooms} rooms. Start a cleanup and check","vac.cal_need_pos":"The robot is not reporting coordinates — start a cleanup and pause it","vac.cal_done":"Calibration saved. Start a cleanup and check","vac.cal_cancelled":"Calibration cancelled","vac.fit":"Fit manually","vac.fit_hint":"Drag the robot map into place, stretch by the corners","vac.fit_rotate":"Rotate 90°","vac.fit_mirror":"Mirror","vac.trail_never":"Never","vac.trail_cleaning":"While cleaning","vac.trail_always":"Always","gs.bg_group":"Stage background","gs.bg_color":"Background around the plan","gs.bg_default":"Theme default","gs.bg_theme":"theme default","gs.bg_mode":"Plan background","gs.bg_static":"Static color","gs.bg_daynight":"Follows the Sun","gs.bg_daynight_hint":"The background moves through dawn, day, dusk and night using sun.sun, or your local clock when sun data is unavailable. No compass is needed for the background.","gs.sun_group":"Sun","gs.sun_missing":"The sun.sun entity was not found — the background follows your local clock; window rays are unavailable.","gs.north":"North on the plan","gs.north_ph":"not set","gs.north_hint":"Set north only for window rays (1° steps, 15° with Shift); the Sun background works without it.","gs.north_clear":"Clear","gs.north_letter":"N","gs.sun_rays":"Sunlight through windows","gs.about_group":"About","gs.about_version":"Houseplan Card v{v}","gs.about_github":"GitHub · docs & issues","gs.about_telegram":"Telegram chat","space.bg_color":"Background around the plan","space.bg_inherit":"Inherit general","space.bg_inherited":"inherits general settings","space.bg_mode":"Plan background","space.north":"North on the plan (override)","space.north_inherited":"inherited: {v}","space.sun_rays":"Sunlight through windows","space.sun_inherit":"Inherit general","space.sun_on":"On","space.sun_off":"Off","canvas.far_objects":"{n} object(s) far from the plan","canvas.show_far":"Show","canvas.home_tip":"The plan is over there — click to fit it","gs.grid_group":"Plan maintenance","gs.grid_hint":"Updates data models, aligns plan elements to the grid and merges redundant wall fragments. An exact report is shown before anything is stored.","gs.align_all":"Optimize plans","gs.align_title":"Optimize plans","gs.align_none":"All plans already use the current optimized data model.","gs.optimize_no_automatic_changes":"There are no automatic changes to apply. Review the items below.","gs.align_count":"{n} of {total} elements will move, by at most {cm} cm.","gs.align_where":"The largest shift is in “{s}”.","gs.align_turned":"Openings whose angle is corrected: {n}.","gs.align_removed_drafts":"Invalid outlines collapsed by the grid and removed: {n}.","gs.optimize_redundant_drafts":"Saved wall chains hidden by solid room walls and removed: {n}.","gs.align_preflight_failed":"Could not safely verify the geometry of the following spaces: {spaces}{more}.","gs.align_preflight_hint":"Plans were not changed. Copy the diagnostics with the button below and attach them to the bug report together with a space export.","gs.preflight_reason_prepare-exception":"Could not prepare the space geometry (exception while building the model)","gs.preflight_reason_wall-null":"The wall body did not build (the union came back empty)","gs.preflight_reason_wall-degraded-extra":"The wall body degraded with extra geometry","gs.preflight_reason_wall-failed-core":"The wall body core failed to assemble","gs.preflight_reason_wall-exception":"Wall construction threw an exception","gs.preflight_reason_floor-null":"The floor outline did not build","gs.preflight_reason_floor-exception":"Floor construction threw an exception","gs.preflight_copy":"Copy diagnostics","gs.preflight_copied":"Diagnostics copied","gs.preflight_update_hint":"The card and integration versions differ — update House Plan and retry.","gs.align_preflight_space":"Space {n}","gs.align_preflight_more":", and {n} more","gs.optimize_changes":"Model migrations: {m}; spaces updated: {c}; noisy coordinate values removed: {p}; merged real-wall fragments: {w}; merged zero-thickness wall fragments: {s}; independent walls: {i}.","gs.zero_walls_migrated":"Virtual wall spans converted: {n}.","gs.wall_segments_migrated":"Wall segments stabilised: {n}.","gs.optimize_lattice_summary":"Noisy coordinate values canonicalized: {n}; maximum movement: {cm} cm.","gs.optimize_lattice_space":"{space}: coordinate values canonicalized: {n}; off-grid values left unchanged: {far}.","gs.optimize_coincident_partitions":"Hidden independent wall sections absorbed into room walls: {n}.","gs.optimize_openings_rehosted":"Openings reattached to room walls: {n}.","gs.optimize_walls_straightened":"Walls straightened: {n}; maximum movement: {cm} cm.","gs.optimize_walls_straightened_where":"Largest wall correction: {s}.","gs.optimize_walls_straighten_skipped":"Near-axis walls left unchanged because they could not be repaired safely: {n}.","gs.optimize_glow_migration":"Legacy Glow: {spaces} spaces → no data fill + independent Glow; {rooms} rooms → inherited data fill + independent Glow.","gs.optimize_references":"References repaired: spaces — {spaces}; rooms — {rooms}; positions — {positions}; devices detached from missing spaces — {detached}.","gs.optimize_reference_more":", and {n} more","gs.optimize_orphans_removed":"Forgotten records removed: {total} — room labels: {rooms}; devices: {devices}; group markers: {groups}. They belonged to spaces deleted earlier.","gs.optimize_live_positions":"Old positions in deleted spaces belong to existing objects: {n}{names}. They will be kept.","gs.optimize_live_positions_remove":"Old positions in deleted spaces belong to existing objects: {n}{names}. They are selected for removal.","gs.optimize_live_names":": {names}{more}","gs.optimize_live_remove":"Remove old positions","gs.optimize_live_keep":"Keep old positions","gs.optimize_live_selected":"Old positions will be removed after Optimize is applied.","gs.optimize_unverified":"Could not safely verify positions: {n}. They were left unchanged.","gs.optimize_registry_limited":"Full administrator access to the Home Assistant registries is required for a safe check.","gs.optimize_vacuum_warning":"Vacuum room mappings that still need review: {n}.","gs.optimize_details":"Details","gs.optimize_details_more":"And {n} more records.","gs.optimize_detail_removed":"will be removed","gs.optimize_detail_live":"will be kept","gs.optimize_detail_unverified":"not verified","gs.optimize_detail_room_label":"room label","gs.optimize_detail_device":"device","gs.optimize_detail_group":"group marker","gs.optimize_detail_unknown":"unknown owner","gs.optimize_detail_item":"{status}: {kind} {id}; old space {space}","gs.align_warn":"Elements deliberately placed between grid nodes will move. One undo is available after the operation, only until the next plan edit.","gs.align_run":"Optimize","gs.align_done":"Plans optimized: {n} elements moved, {m} records maintained, {r} references repaired","gs.optimize_undo":"Undo last optimization","gs.optimize_undone":"The last optimization was undone","decor.furniture":"Furniture","furn.title":"Furniture library","furn.symbol":"Symbol","furn.group_furniture":"Furniture","furn.group_appliance":"Appliances","furn.group_sanitary":"Plumbing","furn.group_other":"Other","furn.width":"Width","furn.depth":"Depth","furn.pick_hint":"Pick a symbol, then click on the plan.","furn.place_hint":"Click on the plan — the piece lands against the nearest wall. Shift places it free.","furn.sym_sofa":"Sofa","furn.sym_armchair":"Armchair","furn.sym_coffee_table":"Coffee table","furn.sym_table_dining":"Dining table","furn.sym_table_round":"Round table","furn.sym_chair":"Chair","furn.sym_desk":"Desk","furn.sym_bed_double":"Double bed","furn.sym_bed_single":"Single bed","furn.sym_nightstand":"Nightstand","furn.sym_wardrobe":"Wardrobe","furn.sym_bookshelf":"Bookshelf","furn.sym_fridge":"Fridge","furn.sym_stove":"Cooker","furn.sym_dishwasher":"Dishwasher","furn.sym_washer":"Washing machine","furn.sym_dryer":"Tumble dryer","furn.sym_tv":"TV","furn.sym_ac":"Air conditioner","furn.sym_water_heater":"Water heater","furn.sym_toilet":"Toilet","furn.sym_bathtub":"Bathtub","furn.sym_shower":"Shower","furn.sym_sink":"Washbasin","furn.sym_kitchen_sink":"Kitchen sink","furn.sym_bidet":"Bidet","furn.sym_stairs":"Stairs","furn.sym_fireplace":"Fireplace","furn.sym_plant":"Plant","furn.sym_rug":"Rug","common.yes":"Yes","common.no":"No","vac.diag_source":"Source","vac.diag_platform":"Integration","vac.diag_status":"Status","vac.diag_position":"Position","vac.diag_rooms":"Rooms","vac.diag_rooms_value":"{total} · {matched} names match · {readiness}","vac.autocal_ready":"auto-calibration available","vac.autocal_not_ready":"need 3 matching names","vac.diag_path":"Integration path","vac.diag_map":"Map ID","vac.source_none":"not selected","vac.source_status_ok":"Ready","vac.source_status_missing":"Missing","vac.source_status_disabled":"Disabled in Home Assistant","vac.source_status_unavailable":"Unavailable","vac.source_status_unverified":"Cannot verify with current permissions","vac.source_status_unsupported":"No position data","vac.source_status_none":"No source","vac.source_banner_missing":"The saved source no longer exists. It was not replaced automatically; choose another source or restore it in Home Assistant.","vac.source_banner_disabled":"The saved source is disabled in Home Assistant. Enable it there or choose another source.","vac.source_banner_unverified":"Current Home Assistant permissions cannot verify this saved source. It remains pinned and will not be replaced automatically.","vac.choose_source":"Choose source","vac.source_auto":"Automatic","vac.source_auto_hint":"Use a compatible entity from this device","vac.all_cameras":"All cameras","vac.all_cameras_warn":"A camera may not provide robot data. Choose one only if it is your vacuum map.","vac.all_cameras_empty":"No other camera entities found.","vac.platform_unknown":"unknown integration","vac.cap_position":"position","vac.cap_rooms_short":"rooms","vac.cap_path":"path","vac.cap_map":"map ID","vac.cap_none":"no robot data detected","vac.xcme_hint":"Enable these Xiaomi Cloud Map Extractor attributes:","vac.documentation":"Documentation","vac.residual_title":"Check automatic calibration","vac.residual_message":"The matched rooms disagree by up to {error}. Apply this approximate calibration, refine it manually, or cancel without changing the saved setup.","vac.apply_proposal":"Apply","gs.backup_group":"Backup and transfer","gs.backup_hint":"Download a portable JSON backup or preview and import a backup made by House Plan.","backup.export_open":"Export","backup.import_open":"Import","backup.export_title":"Export House Plan","backup.import_title":"Import House Plan","backup.export_hint":"Choose whether the backup should contain the whole House Plan configuration or only the current space.","backup.full":"Full backup","backup.current_space":"Current space","backup.current_space_title":"Current space: {title}","backup.no_current_space":"No current space","backup.plan_only":"Plan only","backup.plan_only_hint":"Keep rooms, walls, openings, decor and room-label positions without devices or Home Assistant bindings.","backup.plan_only_preview":"This file contains the plan only","backup.privacy_warning":"The archive keeps names, Home Assistant identifiers and exact coordinates. Internal plans and attachments are referenced, not embedded; runtime states and vacuum trails are not included.","backup.download":"Download JSON","backup.export_done":"Backup downloaded","backup.reading":"Checking the backup…","backup.revalidated":"The plan changed after this preview. The summary was refreshed; review it and confirm again.","backup.error.unauthorized":"You do not have permission to export or import this plan.","backup.error.not_ready":"House Plan is not ready yet. Try again in a moment.","backup.error.too_large":"The backup exceeds the 8 MiB limit.","backup.error.invalid_json":"The selected file is not valid JSON.","backup.error.invalid_format":"The selected file is not a House Plan backup.","backup.error.unsupported_export_version":"This backup format is not supported by the installed version.","backup.error.future_model":"The backup was created by a newer House Plan data model.","backup.error.invalid_config":"The backup contains an invalid House Plan configuration.","backup.error.wall_model_migration_blocked":"The backup wall model could not be upgraded safely. Optimize the source plan first, then export it again.","backup.error.invalid_layout":"The backup contains invalid object positions.","backup.error.invalid_content":"The backup contains invalid or inconsistent content references.","backup.error.space_not_found":"The selected space no longer exists.","backup.error.capacity_exceeded":"Adding this backup would exceed the plan limits.","backup.error.preview_expired":"The preview expired. Select the backup file again.","backup.error.preview_owner_mismatch":"This preview belongs to another Home Assistant user.","backup.error.conflict":"The plan changed after the preview. Review the refreshed summary.","backup.error.content_confirmation_required":"Confirm that unavailable local content may be detached.","backup.error.commit_failed":"The backup could not be applied safely. The previous plan was restored or is pending recovery.","backup.error.missing_plan":"A referenced plan file disappeared after the preview. Review the backup again.","backup.error.missing_content":"A referenced local attachment disappeared after the preview. Review the backup again.","backup.error.marker_control_missing":"A linked plan light is missing from the backup.","backup.error.marker_control_not_light":"A linked plan target is no longer marked as a light source.","backup.error.marker_control_self":"A light source cannot control itself.","backup.error.marker_control_cycle":"The backup contains a circular chain of light controls.","backup.error.duplicate_marker_control":"The backup contains a duplicate plan-light link.","backup.error.no_backup":"There is no import or optimization snapshot to restore.","backup.same_source":"Created on this Home Assistant instance","backup.foreign_source":"Created on another Home Assistant instance","backup.created":"Created: {value}","backup.versions":"Card {card}; integration {integration}; data model {model}","backup.count_spaces":"Spaces: {n}","backup.count_rooms":"Rooms: {n}","backup.count_walls":"Walls: {n}","backup.count_openings":"Openings: {n}","backup.count_decor":"Decor objects: {n}","backup.count_markers":"Devices: {n}","backup.count_layout":"Positions: {n}","backup.bindings":"Bindings — devices: {device}, entities: {entity}, virtual: {virtual}; positions without a space: {legacy}","backup.binding_status":"Target status — active: {active}, disabled: {disabled}, missing: {missing}","backup.missing_areas":"Areas missing on the target: {areas}","backup.dropped_marker_links":"Plan-light links outside this transferred space were omitted: {n}.","backup.repaired_target_refs":"Existing references restored by this space import: {n}.","backup.preserved_unresolved_refs":"References that could not be restored unambiguously were preserved: {n}.","backup.preserved_unresolved_hint":"No data was guessed or deleted. After the import, run Optimize plans to inspect the remaining references.","backup.import_details":"Import reference details","backup.import_detail.incoming_remapped":"References updated inside the imported copy: {n}","backup.import_detail.target_repaired":"Existing references restored: {n}","backup.import_detail.preserved_unresolved":"Unresolved references preserved: {n}","backup.import_detail.collisions":"Destination conflicts preserved safely: {n}","backup.import_detail.dropped_links":"Incoming links omitted by transfer rules: {n}","backup.import_detail.bounded_lineages":"Overly nested identifiers left bounded: {n}","backup.replace_warning":"This replaces the current configuration and layout. Uploaded files are never deleted. One undo remains available until the next plan edit.","backup.foreign_bookkeeping":"Instance-specific known/new-device bookkeeping will not be imported.","backup.final_name":"New space name","backup.target_settings":"The target instance's global settings remain unchanged.","backup.duplicates":"Bindings already present: {n}","backup.skip":"Skip duplicate devices","backup.virtual_copy":"Add safe static virtual copies","backup.content":"Referenced content","backup.content_available":"available locally","backup.content_external":"external link","backup.content_detach_required":"will be detached","backup.confirm_detach":"I understand that unavailable internal plans and attachments will be detached from the imported configuration.","backup.replace":"Replace","backup.add":"Add space","backup.space_done":"Space imported: {rooms} rooms, {markers} devices, {refs} existing references restored","backup.full_done":"Backup restored: {spaces} spaces, {rooms} rooms, {markers} devices","backup.undo_import":"Undo last full import","backup.import_undone":"Full import undone"};const Ud=[{code:"en",nativeLabel:"English",dictionary:Wd},{code:"ru",nativeLabel:"Русский",dictionary:{"editor.context_actions":"Действия: {object}","editor.tool_options":"Параметры инструмента: {tool}","editor.palette":"Палитра: {tool}","editor.open_group":"Группа инструментов: {group}","editor.group_active":"{group} — активно: {item}","editor.disabled_action":"{action} недоступно: {reason}","editor.loading":"Загружаем редактор…","editor.loading_aria":"Редактор плана загружается","editor.load_failed":"Не удалось загрузить редактор.","editor.refresh_advice":"Обновите страницу и повторите попытку.","color_picker.title":"Выбор цвета","color_picker.hue":"Оттенок","color_picker.saturation":"Насыщенность","color_picker.value":"Яркость","color_picker.hex":"Цвет HEX","color_picker.invalid_hex":"Введите цвет HEX из 3 или 6 цифр","btn.properties":"Свойства","btn.keep_as_walls":"Оставить стенами","btn.delete_room_keep_walls":"Удалить комнату, оставить стены","btn.delete_room_with_walls":"Удалить комнату и стены","title.markup_select":"Выбор и редактирование стен, колонн и сохранённых контуров","title.markup_column":"Колонна: кликните точку сетки для квадратной колонны","markup.partition":"Перегородка","markup.column":"Колонна","markup.hint_column":"кликните точку сетки, чтобы поставить квадратную колонну","history.draft_segment":"Добавление сегмента черновика комнаты","history.draft_merge":"Соединение незавершённых контуров комнаты","history.draft_segment_delete":"Удаление сегмента черновика комнаты","history.partition_add":"Добавление перегородки","history.column_add":"Добавление колонны","history.physical_edit":"Изменение физического объекта","history.physical_delete":"Удаление физического объекта","history.physical_move":"Перемещение физического объекта","history.contour_to_partitions":"Преобразование контура в замкнутые стены","history.wall_chain_finish":"Завершение цепочки стен","history.wall_face_batch":"Создание комнат из стен","toast.column_duplicate":"Колонна с тем же центром и внешним размером уже существует","confirm.delete_draft":"Удалить весь незавершённый контур комнаты?","confirm.delete_draft_segment":"Удалить этот сегмент черновика? Оставшийся контур может разделиться на два.","physical.partition_properties":"Свойства перегородки","physical.column_properties":"Свойства колонны","physical.draft_properties":"Свойства сегмента черновика","physical.shape":"Форма","physical.square":"Квадрат","physical.circle":"Круг","physical.diameter":"Диаметр","physical.side":"Сторона","physical.rotation":"Угол поворота","physical.length":"Длина","physical.allowed_range":"Допустимо: {min}–{max} {unit}","physical.delete_segment":"Удалить сегмент","physical.delete_draft":"Удалить весь контур","physical.column_size_title":"Внешняя сторона квадратной колонны, создаваемой кликом (1–150 см).","card.title":"План дома","count.devices":"{n} устр.","empty.no_spaces":"Пространств пока нет.","empty.add_first":"Добавьте первое пространство и загрузите план этажа.","empty.install":"Установите интеграцию House Plan и добавьте запись в «Устройства и службы».","btn.add_space":"Добавить пространство","btn.cancel":"Отмена","btn.save":"Сохранить","btn.close":"Закрыть","btn.delete":"Удалить","btn.edit":"Редактировать","btn.open_in_ha":"Открыть в HA","btn.reset":"Сброс","btn.attach":"Прикрепить…","btn.upload":"Загрузить…","btn.replace":"Заменить…","title.zoom_in":"Приблизить","title.zoom_out":"Отдалить","title.zoom_fit":"Вписать всё","title.add_device":"Добавить устройство на план","title.show_all":"Показать скрытые и деактивированные устройства служебными маркерами (только в этой вкладке)","title.markup":"Разметка комнат: сетка, линии, контуры","title.configure_space":"Настроить пространство","title.add_space":"Добавить пространство","title.markup_add":"Стены: рисуйте непрерывную цепочку; Shift фиксирует 45°. Смена инструмента или выход завершает её как независимые стены","title.markup_merge":"Объединить: клик по одной комнате, затем по соседней с общей стеной","title.markup_split":"Разделить комнату: клик по комнате, затем две точки на её стенах","title.markup_delroom":"Удалить комнату: кликните внутри комнаты и подтвердите удаление","title.need_plan":"Загрузите подложку (план этажа)","markup.add":"Стены","markup.merge":"Объединить","markup.split":"Разделить","markup.resize":"Размер","title.markup_resize":"Изменение размера комнат: тяните доступную ручку горизонтальной или вертикальной стены","markup.hint_resize":"тяните доступную ручку стены · стена упирается в первую небезопасную позицию · Esc — отмена · Ctrl+Z — отмена шага","resize.disabled.diagonal":"Стена расположена под углом: Resize поддерживает только горизонтальные и вертикальные стены","resize.disabled.side-angle":"Примыкающая стена расположена под углом: для Resize обе примыкающие стены должны образовывать прямой угол","resize.disabled.duplicate-physical-wall":"Стену перекрывает отдельная перегородка, незавершённый контур или колонна; удалите или переместите её перед изменением размера","resize.disabled.partial-shared":"Соседняя комната использует только часть этой стены, поэтому её нельзя безопасно двигать как одну общую стену","resize.disabled.unequal-shared":"У соседней комнаты другие конечные точки или длина стены, поэтому стены нельзя безопасно двигать вместе","resize.disabled.multiple-rooms":"Перемещение затронуло бы больше двух комнат","resize.disabled.thickness-conflict":"Толщину стены нельзя безопасно сохранить","resize.disabled.opening-conflict":"Проём на этой стене нельзя безопасно переместить","resize.disabled.invalid-geometry":"Эту стену нельзя переместить без изменения структуры плана","resize.limit_stopped":"Стена остановлена: дальше нарушается ограничение стыков","resize.commit_failed":"Изменение размера отменено: итоговый план не прошёл проверку геометрии","resize.preview_failed":"Стена остановлена в последней безопасной позиции: геометрию плана нельзя сохранить без потерь","markup.opening":"Проём","markup.delete_room":"Удалить комнату","history.undo":"Отменить","history.redo":"Повторить","history.undo_named":"Отменить: {name} (Ctrl+Z)","history.redo_named":"Повторить: {name} (Ctrl+Shift+Z / Ctrl+Y)","history.undo_empty":"Нет операций для отмены","history.redo_empty":"Нет операций для повтора","history.undone":"Отменено: {name}","history.redone":"Повторено: {name}","history.add_room":"Создание комнаты","history.split_room":"Разделение комнаты","history.merge_rooms":"Объединение комнат","history.resize_room":"Изменение размера комнаты","history.wall_thickness":"Изменение толщины стены","history.add_opening":"Добавление дверного или оконного проёма","history.edit_opening":"Изменение проёма","history.move_opening":"Перемещение проёма","history.delete_opening":"Удаление проёма","history.delete_room":"Удаление комнаты","history.delete_room_keep_walls":"Удаление комнаты с сохранением стен","history.delete_room_with_walls":"Удаление комнаты и её стен","history.decor_add":"Добавление объекта декора","history.decor_edit":"Изменение объекта декора","history.decor_move":"Перемещение объекта декора","history.decor_transform":"Трансформация объекта декора","history.decor_delete":"Удаление объекта декора","history.backdrop_transform":"Трансформация подложки плана","title.markup_opening":"Проёмы: клик по стене — добавить, клик по проёму — редактировать","opening.new":"Новый проём","opening.edit":"Проём","opening.door":"Дверь","opening.window":"Окно","opening.gate":"Ворота","opening.passage":"Открытый проём","opening.passage_binding_warning":"При сохранении датчик открытия и замок будут удалены.","opening.invalid_passage_fields":"У открытого проёма на плане «{room}» есть недопустимые параметры: {fields}.","opening.type_label":"Тип","opening.length_label":"Длина, см","opening.contact_label":"Датчик открытия","opening.lock_label":"Замок","opening.none":"— нет —","opening.search_ph":"Поиск: имя или entity_id…","opening.invert":"Инвертировать открыто/закрыто","opening.flip_h":"Петли с другой стороны","opening.flip_v":"Открывается в другую сторону","opening.open":"Открыто","opening.closed":"Закрыто","opening.locked":"Заперто","opening.unlocked":"Не заперто","opening.state_unknown":"недоступно","opening.no_entities":"Датчики не привязаны — статичный символ на плане.","toast.opening_no_wall":"Кликните рядом со стеной комнаты или независимой стеной","opening.host_partition":"Независимая стена","opening.partition_orphan":"Независимая стена этого проёма больше не существует","opening.partition_jamb_margin":"Оставьте от края проёма до торца стены минимум {distance}","opening.rebind_partition":"Привязать к другой независимой стене","confirm.delete_partition_openings_title":"Удалить стену и проёмы?","confirm.delete_partition_openings_body":"В стене есть проёмы: {count}. Они будут удалены вместе со стеной.","confirm.delete_partition_openings_item":"• {type}, {length}","markup.delete":"Удалить","markup.hint_points":"точек: {n} · Shift — шаг 45° · Esc — завершить цепочку · Ctrl+Z — убрать точку · при замыкании будет предложена комната","markup.hint_start":"кликните точку сетки, чтобы начать цепочку стен","tip.lqi":"средний сигнал zigbee:","tip.area":"площадь: {value}","info.device_header":"Устройство на плане","info.model":"Модель","info.state":"Состояние","info.link":"Ссылка","info.manuals":"Инструкции","info.none":"Нет дополнительной информации","marker.new_device":"Новое устройство","marker.name_label":"Имя (отображается на плане)","marker.name_ph":"Название","marker.binding_label":"Привязка к устройству HA","marker.binding_disabled":"деактивировано в Home Assistant","marker.virtual_option":"Виртуальное устройство (без привязки)","marker.search_ph":"Поиск устройства / группы…","marker.nothing_found":"ничего не найдено","marker.room_label":"Комната","marker.room_override":" (переопределить размещение)","marker.room_choose":"— выберите комнату —","marker.room_auto":"— по зоне устройства (авто) —","marker.icon_label":"Иконка","marker.icon_ph":"mdi:… (пусто = авто)","marker.display_label":"Отображение","display.badge":"Значок + состояние","display.icon_ripple":"Значок + состояние и активность","display.static_icon":"Всегда статичный значок","marker.display_hint":"Значок + состояние меняет подложку при работе, открытии, тревоге и недоступности. Значок + состояние и активность дополнительно показывает короткую пульсацию для событий и постоянную — для работы, движения или присутствия. Значение + состояние заменяет значок одним однозначным значением HA. Статичный значок не реагирует на состояния устройства.","marker.display_hint_badge":"Значок и динамическая подложка показывают состояние устройства без обычной пульсации активности. Красная тревога сохраняется.","marker.display_hint_icon":"Значок и динамическая подложка показывают состояние устройства без обычной пульсации активности. Красная тревога сохраняется.","marker.display_hint_icon_ripple":"Значок, динамическая подложка и пульсация: короткая — для событий, постоянная — для работы, движения или присутствия. Красная тревога имеет отдельный приоритет.","marker.display_hint_value":"Значок заменяется одним однозначным значением Home Assistant; подложка продолжает показывать состояние. Красная тревога сохраняется.","marker.display_hint_static_icon":"Подложка в цветах темы и значок всегда остаются одинаковыми. Состояния, активность, недоступность и тревоги не меняют отображение.","marker.static_alarm_warning":"Это устройство может сообщать о тревогах. Статичный режим скрывает визуальную тревогу маркера.","marker.preview.title":"Предпросмотр отображения","marker.preview.actual":"Сейчас","marker.preview.example":"Пример","marker.preview.integration":"Интеграция","marker.preview.source":"Источник отображения","marker.preview.current_state":"Текущее состояние","marker.preview.result":"На плане","marker.preview.details":"Подробнее об источниках","marker.preview.select_source":"Выберите устройство или сущность Home Assistant, чтобы увидеть фактическое отображение.","marker.preview.unknown_provider":"Интеграция неизвестна","marker.preview.virtual_provider":"House Plan · виртуальное устройство","marker.preview.no_source":"Нет активного источника","marker.preview.no_state":"Нет текущего состояния","marker.preview.mixed_states":"Несколько разных состояний","marker.preview.multiple_sources":"Источников: {n}","marker.preview.more_sources":"ещё {n}","marker.preview.scaled":"Предпросмотр уменьшен до {n}%, чтобы поместиться. Сохранённый размер не изменится.","marker.preview.demo_activity":"Показать пример активности","marker.preview.demo_notice":"Временный пример активности; фактическое состояние устройства не меняется.","marker.preview.demo_short":"Показать краткую активность","marker.preview.demo_continuous":"Показать постоянную активность","marker.preview.stop_continuous":"Остановить постоянную активность","marker.preview.demo_short_notice":"Пример краткой активности; фактическое состояние устройства не меняется.","marker.preview.demo_continuous_notice":"Пример постоянной активности; фактическое состояние устройства не меняется.","marker.preview.demo_already_visible":"Текущее состояние уже показывает реальную активность или тревогу.","marker.preview.reduced_motion":"В системе включено уменьшение движения, поэтому обычная активность показана точкой.","marker.preview.reason.neutral":"Нейтральная тёмная подложка","marker.preview.reason.working":"Жёлтая подложка: устройство сейчас работает","marker.preview.reason.working_activity":"Жёлтая подложка и эффект активности: устройство сейчас работает","marker.preview.reason.open":"Оранжевая подложка: физически открыто или разблокировано","marker.preview.reason.cover_icon_state":"Нейтральная подложка; состояние и значок определяет сущность шторы или заслонки","marker.preview.reason.presence":"Эффект активности, пока обнаружено присутствие","marker.preview.reason.event":"Короткий эффект активности после события","marker.preview.reason.transition":"Эффект активности во время движения или смены состояния","marker.preview.reason.media_neutral":"Медиаустройство остаётся на нейтральной тёмной подложке, пока доступно","marker.preview.reason.unavailable":"Приглушённая нейтральная подложка: отключено или недоступно","marker.preview.reason.alarm":"Красная тревожная подложка; тревога отображается всегда","marker.preview.reason.live_states_disabled":"Оформление по живым состояниям отключено в настройках карточки","marker.preview.reason.value_no_state":"Нет подходящего значения состояния — вместо него показан значок","marker.preview.reason.value_ambiguous_sources":"Подходит несколько источников значения — вместо него показан значок","marker.preview.reason.value_non_scalar":"Источник вернул не простое значение — вместо него показан значок","marker.preview.reason.value_virtual":"У виртуального устройства нет значения HA — вместо него показан значок","marker.preview.reason.vacuum_live_plan_only":"Живая позиция и след пылесоса доступны только на полном плане","marker.preview.reason.hidden_design_preview":"Устройство скрыто на плане, но его оформление видно в предпросмотре","marker.preview.reason.composite_power_source":"Состояние берётся из сущности питания устройства; вспомогательные переключатели не учитываются","marker.preview.reason.activity_display_disabled":"Выбранный режим отображения не показывает обычные эффекты активности","marker.preview.reason.ha_disabled":"Привязка отключена в Home Assistant, поэтому устройство будет скрыто на плане","marker.preview.reason.orphaned":"Сохранённая привязка Home Assistant больше не найдена","marker.preview.reason.static_icon":"Статичный режим: состояние устройства не меняет значок","marker.activity_color":"Цвет пульсации активности","marker.ripple_size":"Размер пульсации активности","marker.activity_alarm_note":"Цвет и размер относятся только к обычной активности и не влияют на красную тревогу.","marker.pulse_a11y_alarm":"Тревога","marker.pulse_a11y_event":"Недавнее событие","marker.pulse_a11y_presence":"Обнаружено присутствие","marker.pulse_a11y_transition":"Изменение состояния","marker.pulse_a11y_running":"Работает","marker.state_a11y_neutral":"Обычное состояние","marker.state_a11y_working":"Работает","marker.state_a11y_open":"Открыто","marker.state_a11y_locked":"Заблокировано","marker.state_a11y_unlocked":"Разблокировано","marker.state_a11y_alarm":"Тревога","marker.state_a11y_unavailable":"Недоступно","marker.lqi_a11y_low":"LQI {value}, слабый сигнал","marker.lqi_a11y_mid":"LQI {value}, средний сигнал","marker.lqi_a11y_high":"LQI {value}, сильный сигнал","marker.size_label":"Размер / поворот значка","marker.angle_label":"Поворот","marker.model_label":"Модель","marker.model_ph":"напр. Aqara T&H","marker.link_label":"Ссылка","marker.desc_label":"Описание","marker.desc_ph":"Заметки, характеристики…","marker.manuals_label":"Инструкции (PDF и т.п.)","marker.sub_device":"устройство","marker.sub_z2m_group":" · Z2M-группа","marker.sub_group":"группа","marker.sub_helper":"хелпер","space.new":"Новое пространство","space.header":"Пространство","space.title_label":"Название","space.title_ph":"Например: Гараж","space.plan_label":"Подложка (план)","space.no_plan":"нет подложки","space.plan_alt":"план","room.new":"Новая комната","room.name_label":"Отображаемое имя","room.name_ph":"Например: Терраса","room.area_label":"Зона Home Assistant (свободные)","room.no_area_option":"— без зоны —","room.default_name":"Комната","device.unnamed":"без имени","device.light_group":"группа света","device.fallback":"устройство","device.virtual":"виртуальное устройство","confirm.delete_room":"Удалить комнату «{name}»?","confirm.delete_room_title":"Удалить комнату «{name}»?","confirm.delete_room_body":"Выберите, нужно ли оставить физические стены, принадлежащие только этой комнате. Общие с другой комнатой стены сохраняются всегда.","confirm.merge_rooms":"Слить комнаты «{a}» и «{b}»?","confirm.remove_marker":"Удалить «{name}» с плана? Устройство исчезнет полностью и перестанет участвовать в данных плана. Позже его можно будет добавить заново.","confirm.erase_decor":"Стереть объект «{kind}»? Действие можно отменить из истории редактора.","confirm.delete_space":"Удалить пространство «{title}» со всеми комнатами и разметкой?","space.delete_blocked":"Это пространство всё ещё используется устройствами: {n}. Сначала перенесите их в другое пространство или удалите.","toast.pos_save_failed":"Не удалось сохранить позицию: {err}","toast.no_entity":"У устройства нет подходящей сущности","toast.ha_disabled_action":"Деактивированный объект Home Assistant нельзя использовать на плане.","toast.ha_disabled_show_device":"Деактивированное в Home Assistant устройство нельзя показать на плане. Сначала активируйте его в Home Assistant.","toast.ha_disabled_show_entity":"Деактивированную в Home Assistant сущность нельзя показать на плане. Сначала активируйте её в Home Assistant.","toast.ha_disabled_add":"Деактивированный объект Home Assistant нельзя добавить на план. Сначала активируйте его в Home Assistant.","toast.ha_binding_unverified":"Статус объекта не удалось подтвердить по реестру Home Assistant. Отображение и действия временно недоступны.","toast.markup_needs_server":"Разметка доступна после переноса конфига на сервер","toast.space_order_changed":"Порядок изменён. Если где-то этаж карточки задан номером, проверьте такие панели.","toast.conflict":"Конфиг изменён в другом окне — данные обновлены, повторите последнее действие","toast.cfg_save_failed":"Не удалось сохранить конфиг: {err}","toast.room_overlap":"Контур накладывается на комнату «{name}» — комнаты не должны накладываться","toast.contour_min_edges":"Чтобы замкнуть контур, сначала нарисуйте минимум две грани","toast.contour_cannot_close":"Контур нельзя замкнуть: он вырожден или пересекает сам себя","toast.merge_not_adjacent":"Объединять можно только комнаты с общей стеной","toast.rooms_merged":"Комнаты объединены в «{name}»","toast.split_pick_wall":"Начните разрез на стене комнаты","toast.split_bad_cut":"Разрез — от стены до стены внутри комнаты, без пересечения стен и самого себя","merge.header":"Объединение комнат","merge.hint":"У объединённой комнаты одно имя и одна зона. Вторая зона освобождается — её устройства уйдут с плана, пока их не заберёт другая комната.","merge.keep":"Оставить","merge.no_area":"без зоны","room.split_header":"Новая комната после разделения","toast.room_saved":"Комната сохранена ({n}). Устройств добавлено: {added}. Обведите следующую или выйдите из разметки.","toast.room_saved_no_area":"Комната сохранена ({n}, без зоны). Обведите следующую или выйдите из разметки.","toast.marker_needs_server":"Редактирование устройств доступно после переноса конфига на сервер","toast.virtual_name_required":"Укажите имя виртуального устройства","toast.marker_saved":"Устройство сохранено","toast.marker_removed":"Устройство удалено с плана","toast.integration_missing":"Интеграция House Plan не установлена — управление недоступно","toast.plan_formats":"Поддерживаются SVG, PNG, JPG, WebP","toast.plan_required":"Загрузите подложку — план этажа обязателен","toast.space_added_onboard":"Пространство добавлено. Обведите комнаты: кликайте по точкам сетки и замкните контур.","toast.space_added":"Пространство добавлено","toast.space_saved":"Пространство сохранено","toast.space_deleted":"Пространство удалено","toast.delete_failed":"Ошибка удаления: {err}","toast.error":"Ошибка: {err}","toast.toggle_target_unavailable":"Цель «{name}» недоступна — действие не выполнено","toast.toggle_targets_unavailable":"Цели недоступны: {names}. Действие не выполнено","toast.file_failed":"Файл «{name}» не загружен: {err}","toast.files_attached":"Прикреплено файлов: {n}","err.unknown":"неизвестная ошибка","err.code":"код {code}","err.too_large":"файл больше {mb} МБ","err.bad_ext":"недопустимый тип (нужен PDF/изображение)","err.unauthorized":"нужны права администратора","editor.title":"Заголовок","editor.default_floor":"Стартовое пространство","editor.default_floor_missing":"Стартовое пространство «{id}» больше не существует. Выберите другое.","editor.floor":"Закреплённое пространство","editor.floor_none":"— не закреплять —","editor.floor_index":"YAML-индекс {index} (сохранён)","fixed_floor.loading":"Загружаем закреплённое пространство…","fixed_floor.invalid_title":"Закреплённое пространство недоступно","fixed_floor.invalid_body":"Проверьте floor в конфигурации карточки. Настроенное значение: {value}","editor.icon_size":"Размер иконок, % ширины плана","editor.show_temperature":"Показывать температуру","editor.live_states":"Живые состояния (вкл/выкл, открыто…)","editor.show_signal":"Показывать сигнал zigbee (LQI)","editor.language":"Язык интерфейса","editor.lang_auto":"Авто (профиль HA)","editor.lang_en":"English","editor.lang_ru":"Русский","title.icon_rules":"Правила иконок: какая MDI-иконка достаётся устройству по имени","rules.title":"Правила иконок","rules.hint":"Правила проверяются сверху вниз по строке «имя устройства + модель» (regex без учёта регистра); срабатывает первое совпадение. Если ничего не подошло — решает device class сущности, затем — иконка-заглушка.","rules.pattern_ph":"regex, напр. розетк|plug","rules.icon_ph":"mdi:power-socket-de","rules.add":"Добавить правило","rules.reset":"Сбросить к умолчаниям","rules.test_ph":"Проверьте имя устройства…","rules.invalid":"некорректный regex","rules.saved":"Правила иконок сохранены","btn.up":"Вверх","btn.down":"Вниз","tap.info":"Карточка устройства","tap.more_info":"Диалог HA (more-info)","tap.toggle":"Переключить состояние","marker.tap_label":"Действие по нажатию для этого устройства","tap.toggle_note":"Под селектором показано, какое состояние изменится. Защищённые устройства не переключаются с плана.","import.title":"Создать пространства из этажей HA","import.hint":"Home Assistant уже знает эти этажи. Отметьте, какие превратить в пространства плана — далее для каждого попросим картинку плана. Комнаты затем обводятся вручную по плану.","import.start":"Создать: {n}","import.manual":"Начать с нуля","import.progress":"Этаж {i} из {n}","import.done":"Пространства созданы. Обведите комнаты: кликайте по точкам сетки и замкните контур.","btn.skip":"Пропустить","space.scale_label":"Масштаб (размер клетки сетки)","space.scale_unit":"см на клетку","space.scale_unit_imperial":"дюйм на клетку","space.display_section":"Отображение","space.show_borders":"Всегда отображать границы комнат","space.zero_wall_style":"Стены нулевой толщины","space.zero_wall_dashed":"Пунктирные","space.zero_wall_solid":"Сплошные","space.zero_wall_help":"Пунктирные стены пропускают свет. Сплошные стены блокируют свет, даже при нулевой толщине.","space.show_names":"Отображать названия комнат (перетаскиваются)","space.room_color":"Цвет границ и названий","space.opacity":"Прозрачность","space.fill_label":"Заливка комнат","fill.none":"Нет","fill.lqi":"По силе зигби-сигнала","fill.light":"По освещению","fill.custom":"Свой цвет","space.custom_fill":"Цвет заливки","space.source_file":"У меня есть картинка плана","space.source_draw":"Нет подложки — нарисую комнаты вручную","space.orientation":"Холст","orient.landscape":"Альбомный","orient.portrait":"Портретный","orient.square":"Квадрат","fill.temp":"По температуре","space.temp_min":"Комфорт от","space.temp_max":"до","tip.temp_avg":"средняя температура:","tip.hum_avg":"средняя влажность:","space_card.button":"Перейти к пространству","space_card.not_found":"Пространство «{id}» не найдено","space_card.loading":"Загрузка…","continuity.restore_plan":"Восстанавливаем план…","continuity.restore_connection":"Восстанавливаем подключение к устройствам…","continuity.retry":"Повторить","editor.space":"Пространство","editor.show_button":"Показывать кнопку","editor.button_label":"Текст кнопки","editor.button_target":"Путь дашборда (куда вести)","marker.sub_entity":"сущность","title.general_settings":"Общие настройки","gs.title":"Общие настройки","gs.hint":"Цвета заливок действуют на все пространства; у каждого цвета своя прозрачность. Какой режим заливки использует пространство — задаётся в его диалоге.","gs.light_group":"Заливка: освещение","gs.light_on":"Свет включён","gs.light_off":"Весь свет выключен","gs.temp_group":"Заливка: температура","gs.temp_cold":"Холодно","gs.temp_ok":"Комфорт","gs.temp_hot":"Жарко","gs.lqi_group":"Заливка: зигби-сигнал","gs.lqi_low":"Слабый сигнал","gs.lqi_high":"Сильный сигнал","gs.reset":"Сбросить к умолчаниям","gs.saved":"Общие настройки сохранены","space.show_lqi":"Показывать зигби-сигнал (LQI) у устройств","space.hide_decor":"Скрыть декоративный слой","space.hide_decor_tip":"Линии, фигуры, надписи и мебель остаются на месте — их видно в редакторе подложки, но не на плане.","space.hide_openings":"Скрыть проёмы","space.hide_openings_tip":"Двери, окна и ворота не рисуются, но продолжают работать: свет проходит, солнце светит в окна, датчики открытия срабатывают. В редакторе плана проёмы видно всегда.","gs.light_none":"Нет источников света","mode.plan":"Редактор плана","mode.devices":"Редактор устройств","display.value":"Значение + состояние","marker.subarea":"без зоны, вручную","device.new":"Новое устройство — откройте его редактор, чтобы снять отметку","opening.unlock_action":"Открыть замок","opening.lock_action":"Закрыть замок","opening.lock_pending":"Выполняется…","title.close_editor":"Закрыть редактор (вернуться к просмотру)","devbar.add":"Добавить","devbar.show_all":"Скрытые и деактивированные","devbar.rules":"Правила иконок","device_inbox.button":"Устройства","device_inbox.title":"Устройства на плане","device_inbox.search":"Поиск по устройствам, сущностям и интеграциям…","device_inbox.add_virtual":"Добавить виртуальное устройство","device_inbox.tab_on_plan":"На плане","device_inbox.tab_available":"Доступны","device_inbox.tab_hidden":"Скрытые","device_inbox.tab_readd":"Доступны снова","device_inbox.only_new":"Только новые","device_inbox.show_entities":"Показывать сущности","device_inbox.show_hidden":"Показывать скрытые на плане","device_inbox.new":"Новое","device_inbox.find":"Найти на плане","device_inbox.edit":"Настроить","device_inbox.hide":"Скрыть","device_inbox.show":"Показать","device_inbox.add":"Добавить","device_inbox.readd":"Добавить заново","device_inbox.hide_available":"Скрыть из списка","device_inbox.show_more":"Показать ещё","device_inbox.more_actions":"Другие действия","device_inbox.find_hidden_hint":"Сначала включите «Показывать скрытые на плане»","device_inbox.show_disabled":"Перед показом активируйте эту привязку в Home Assistant","device_inbox.empty_on_plan":"На плане пока нет подходящих устройств.","device_inbox.empty_available":"Нет доступных устройств для добавления.","device_inbox.empty_hidden":"Скрытых устройств нет.","device_inbox.empty_readd":"Нет удалённых устройств, доступных для повторного добавления.","device_inbox.reason_visible_auto":"Найдено автоматически","device_inbox.reason_visible_explicit":"Добавлено явно","device_inbox.reason_manual_hidden":"Скрыто пользователем","device_inbox.reason_automatic_hidden":"Скрыто автоматически","device_inbox.reason_service_entry":"Служебная запись реестра","device_inbox.reason_excluded_integration":"Интеграция исключена фильтрами устройств","device_inbox.reason_excluded_domain":"Непространственный тип сущности","device_inbox.reason_grouped_light":"Представлено группой света комнаты","device_inbox.reason_represented_by_parent":"Представлено родительским устройством","device_inbox.reason_removed":"Ранее удалено с плана","device_inbox.reason_available":"Можно добавить на план","device_inbox.reason_no_bound_room":"Зона HA не связана с комнатой плана","device_inbox.status_ha_disabled":"Отключено в Home Assistant","device_inbox.status_orphaned":"Привязка больше не найдена в Home Assistant","device_inbox.status_unverified":"Реестр Home Assistant временно недоступен","device_inbox.saved":"Список устройств обновлён","space.roomcard_section":"В карточке комнаты:","space.label_temp":"Температура","space.label_hum":"Влажность","space.label_lqi":"Средний Zigbee-сигнал","space.label_light":"Свет вкл/выкл","roomcard.light_on":"Вкл","roomcard.light_off":"Выкл","roomcard.light_partial":"{on} из {total}","toast.split_pick_inside":"Промежуточные точки разреза — внутри комнаты","mode.decor":"Редактор подложки","decor.select":"Выбрать","decor.line":"Линия","decor.rect":"Прямоугольник","decor.ellipse":"Овал","decor.text":"Надпись","decor.erase":"Стереть","decor.erase_confirm_title":"Стереть объект?","decor.color":"Цвет","decor.width":"Толщина линии","decor.line_style":"Стиль линии","decor.line_style_solid":"Сплошная","decor.line_style_dashed":"Пунктирная","decor.fill":"Заливка","decor.fill_color":"Цвет заливки","decor.length":"Длина","decor.size":"Размер","decor.angle":"Угол поворота, °","decor.text_size":"Размер текста","decor.backdrop_properties":"Свойства картинки-подложки","decor.text_title":"Надпись","decor.object_title":"Редактирование: {kind}","decor.text_label":"Текст","decor.live_group":"Вставить переменную HA","decor.live_entity":"Сущность","decor.live_entity_ph":"выберите сущность","decor.live_attr":"Значение","decor.live_attr_ph":"выберите состояние или атрибут","decor.live_state":"Состояние","decor.backdrop":"Картинка-подложка","decor.backdrop_hint":"Тяните картинку — перемещение; угол — размер; верхнюю ручку — поворот. Shift меняет пропорции или снимает шаг угла.","decor.backdrop_reset":"Вернуть картинку","decor.backdrop_reset_done":"Картинка вернулась на своё место и в свой размер","marker.icon_auto":"Авто: {icon} (по правилам иконок; выберите свою, чтобы заменить)","marker.icon_pin_auto":"Закрепить","mode.plan_tip":"Редактор плана — геометрия дома: рисование и объединение/разделение комнат, привязка к зонам HA, двери, окна и ворота, карточки комнат, масштаб","mode.devices_tip":"Редактор устройств — всё про значки: перетаскивание, клик — настройка привязки/иконки/отображения, виртуальные устройства, правила иконок","mode.decor_tip":"Редактор подложки — чисто визуальный декор под планом: линии, прямоугольники, овалы и надписи, не реагирующие на клики","space.glow_enabled":"Свечение источников света","gs.glow_group":"Свечение источников света","gs.glow_base":"Темнота дома","gs.glow_light":"Цвет света по умолчанию / интенсивность","gs.wall_group":"Стены","gs.wall_fill":"Цвет заливки стен","gs.glow_radius":"Радиус свечения","gs.unit_m":"м","gs.unit_ft":"фут","marker.controls_label":"Управляет другими источниками света","marker.controls_hint":"При действии «Переключить» клик разом переключает все добавленные здесь источники. Собственная сущность маркера управляется напрямую и сюда не добавляется; чтобы считать её светом, включите флаг ниже.","marker.controls_filter":"Поиск ламп и выключателей…","info.controls":"Управляет","marker.glow_radius_label":"Радиус свечения","marker.glow_radius.help":"Задаёт радиус свечения в метрах или футах; пустое значение использует радиус из общих настроек.","marker.glow_radius.help.aria":"Подсказка: радиус свечения","markup.wallthick":"Толщина","markup.select":"Выбрать","title.markup_wallthick":"Толщина — клик по стене задаёт толщину от 0 до 100 см.","markup.hint_wallthick":"клик по стене · Esc закрывает без применения","wallthick.field":"Толщина","wallthick.unit_cm":"см","wallthick.unit_in":"дюйм","wallthick.apply_room":"Применить ко всем стенам комнаты","markup.draw_wall_title":"Толщина каждого нового сегмента стены (0–100 см). Общие стены сохраняют существующее значение.","room.queue_progress":"Комната {current} из {total}","toast.wall_rooms_saved":"Создано комнат: {n}","toast.wall_chain_saved":"Цепочка стен сохранена","toast.wallthick_pick":"Кликните по стене","toast.wallthick_set":"Толщина стены задана","toast.wallthick_cleared":"Толщина стены убрана","toast.physical_range":"Введите значение от {min} до {max} {unit}","toast.zero_wall_opening_conflict":"Сначала удалите проём на этом участке стены.","toast.zero_wall_ambiguous":"Не удалось однозначно выбрать участок стены. Уточните геометрию узла.","toast.zero_wall_migration_blocked":"Пространство не преобразовано: {reason}. Данные не изменены.","toast.physical_angle":"Введите угол поворота от 0° до 90°, не включая 90°","toast.physical_limit":"В пространстве достигнут лимит объектов этого типа","toast.geometry_unsafe":"Изменение отменено: геометрию стен нельзя безопасно построить.","junction.limit_angle":"Слишком острый угол между стенами: {actual}°, минимум {limit}°.","junction.limit_valence":"Слишком много стен в одном узле: {actual}, максимум {limit}.","junction.limit_length":"Слишком короткий участок стены: {actual} см, минимум {limit} см.","junction.limit_distance":"Стены и узлы слишком близко: {actual} см, минимум {limit} см.","junction.limit_clearance":"Внутри комнаты не остаётся места: {actual} см², минимум {limit} см².","junction.limit_check_failed":"Проверку стыков выполнить не удалось — изменение не сохранено","toast.wall_model_migration_blocked":"Не удалось обновить модель стен: {reason}. План не изменён. Запустите «Оптимизировать планы»; если ошибка повторится, исправьте конфликтующую геометрию стен.","toast.wall_model_client_outdated":"Обновите карточку и перезагрузите страницу перед редактированием плана.","wall_model.reason.invalid-room":"некорректный контур комнаты","wall_model.reason.zero-length":"отрезок стены нулевой длины","wall_model.reason.third-owner":"стена принадлежит более чем двум комнатам","wall_model.reason.duplicate-id":"конфликт идентификаторов стен","wall_model.reason.thickness-conflict":"конфликт значений толщины стены","wall_model.reason.opening-host":"проём нельзя однозначно привязать к стене","toast.delete_room_pick":"Кликните внутри комнаты, которую нужно удалить","toast.plan_snap_ambiguous":"Увеличьте масштаб, чтобы выбрать узел стены","toast.wall_repair_ambiguous":"У контура несколько возможных соединений. Увеличьте масштаб и соедините стены явно.","toast.wall_repair_too_large":"Разрыв больше 2 см. Соедините стены перед созданием комнаты.","toast.wall_repair_changed":"Геометрия стен изменилась. Попробуйте создать комнату ещё раз.","toast.opening_on_zero_wall":"Проёмы на стене нулевой толщины запрещены","marker.from_ha_option":"Выбрать из списка HA","marker.show_entities":"Показывать сущности","marker.show_entities_tip":"Добавляет в список не только устройства, но и все их сущности","marker.pick_ph":"Выберите устройство…","room.open_area":"Открыть зону в HA","view.volumetric":"Объёмный вид","view.flat":"Плоский вид","kiosk.title":"Размеры на этом экране","kiosk.hint":"Хранится только на этом устройстве — у каждого настенного планшета или ТВ свои удобные размеры.","kiosk.icon_scale":"Размер значков устройств","kiosk.font_scale":"Размер текста карточек комнат","editor.kiosk":"Режим настенного устройства (киоск)","editor.cycle":"Автосмена пространств каждые N секунд (киоск, 0 = выкл)","room.settings_title":"Настройки комнаты","room.settings_section":"Настройки комнаты (переопределяют пространство)","room.fill_label":"Заливка в ЭТОЙ комнате","fill.inherit":"Как у пространства","room.custom_fill_space":"Цвет пространства","room.custom_fill_own":"Цвет комнаты","room.temp_src_label":"Источник температуры","room.hum_src_label":"Источник влажности","room.src_average":"Средняя по датчикам комнаты (по умолчанию)","room.src_pick":"Конкретное устройство или сущность HA","room.src_ph":"Выберите источник…","toast.room_updated":"Комната обновлена","space.card_font":"Размер шрифта карточек комнат (всё пространство)","room.sizes_section":"Размеры шрифтов","room.name_scale":"Размер названия","room.label_scale":"Размер подписей","preview.room_name":"Гостиная","toast.cfg_reload_failed":"Не удалось перечитать план с сервера: {err}","room.settings_short":"Настройки комнаты","room.unnamed":"Комната без имени","marker.use_climate_temp":"Учитывать температуру устройства в комнате","marker.use_climate_temp_tip":"Добавляет current_temperature кондиционера или термостата в среднюю температуру комнаты. Внешний бейдж со значением настраивается отдельно ниже.","marker.value_badge_title":"Бейдж со значением","marker.value_badge.help":"Показывает одно выбранное значение рядом со значком. Не влияет на комнатные показатели, свет, Glow и действие по нажатию.","marker.value_badge.help.aria":"Подсказка: бейдж со значением устройства","marker.value_badge_enabled":"Отображать бейдж со значением","marker.value_badge_source":"Значение","marker.value_badge_source.help":"Выберите конкретное состояние, поддерживаемый атрибут или производное значение этого устройства.","marker.value_badge_source.help.aria":"Подсказка: источник значения бейджа","marker.value_badge_position":"Расположение","marker.value_badge_position.help":"Выбранная сторона сохраняется постоянно и не переворачивается автоматически у края плана.","marker.value_badge_position.help.aria":"Подсказка: расположение бейджа","marker.value_badge_right":"Справа","marker.value_badge_bottom":"Снизу","marker.value_badge_left":"Слева","marker.value_badge_top":"Сверху","marker.value_badge_empty":"У этого устройства нет доступных значений","marker.value_badge_static":"Статичный значок не показывает живые значения. Настройка сохранится.","marker.value_badge_missing":"Источник недоступен","marker.value_badge_missing_hint":"Сохранённый источник сейчас недоступен. Можно сохранить его, отключить бейдж или выбрать другой.","marker.value_badge_duplicate":"То же значение уже показано внутри значка.","marker.value_badge_state":"Состояние · {name}","marker.value_badge_marker_state":"Состояние источника света · {name}","marker.value_badge_lqi":"Качество Zigbee-сигнала (среднее)","marker.value_badge_attr_current_temperature":"Текущая температура · {name}","marker.value_badge_attr_temperature":"Целевая температура · {name}","marker.value_badge_attr_current_humidity":"Текущая влажность · {name}","marker.value_badge_attr_humidity":"Влажность · {name}","marker.value_badge_attr_current_position":"Положение · {name}","marker.value_badge_attr_percentage":"Скорость · {name}","marker.value_badge_attr_brightness":"Яркость · {name}","marker.value_badge_attr_volume_level":"Громкость · {name}","marker.value_badge_attr_battery_level":"Заряд батареи · {name}","marker.value_badge_attr_fan_speed":"Скорость вентилятора · {name}","marker.light_role_label":"Является источником света","marker.light_role.help":"Определяет, является ли сам маркер пространственным источником света. Привязанные выше лампы остаются независимыми источниками для комнаты.","marker.light_role.help.aria":"Подсказка: является ли устройство источником света","marker.light_role_auto_yes":"Автоматически (источник света)","marker.light_role_auto_no":"Автоматически (не источник света)","marker.light_role_always":"Всегда источник света","marker.light_role_never":"Никогда не источник света","marker.light_entity_label":"Ведущая сущность света","marker.light_entity.help":"Для составного устройства выберите сущность, состояние, цвет и яркость которой представляют этот источник на плане. Автоматический вариант сохраняет совместимость со старыми планами.","marker.light_entity.help.aria":"Подсказка: ведущая сущность света","marker.light_entity_auto":"Автоматически ({entity})","marker.light_entity_none":"нет управляемой сущности","marker.light_entity_missing":"Сохранённая сущность {entity} недоступна. Временно используется {fallback}; выбор восстановится, если сущность вернётся.","marker.toggle_entity_label":"Переключаемая сущность","marker.toggle_entity.help":"Для составного устройства выберите собственную лампу или переключатель, которой управляет нажатие. Автоматический вариант сохраняет прежние правила цели и не зависит от ведущей сущности света.","marker.toggle_entity.help.aria":"Подсказка: переключаемая сущность","marker.toggle_entity_auto":"Автоматически ({entity})","marker.toggle_entity_none":"нет собственной управляемой сущности","marker.toggle_entity_missing":"Сохранённой сущности {entity} больше нет среди выбираемых каналов этого маркера. Временно используется {fallback}; выбор восстановится, если сущность вернётся.","marker.glow_color_label":"Цвет и яркость свечения","marker.glow_mode.help":"Используйте данные источника, задайте только цвет или зафиксируйте цвет и яркость. Минимум — 1%; чтобы отключить источник, выберите «Никогда».","marker.glow_mode.help.aria":"Подсказка: цвет и яркость свечения","marker.glow_mode_auto":"Из источника","marker.glow_mode_color":"Задать цвет","marker.glow_mode_fixed":"Задать цвет и яркость","marker.glow_color":"Цвет свечения","marker.glow_brightness":"Яркость","marker.glow_disabled_never":"Настройки свечения недоступны: маркер явно исключён из источников света.","marker.glow_disabled_auto":"В автоматическом режиме у этого маркера не найден пространственный источник света.","marker.glow_disabled_no_entity":"Для пространственного источника нет активной управляемой сущности Home Assistant.","marker.glow_passive_hint":"У источника нет собственных данных Home Assistant. Задайте цвет и яркость вручную; радиус остаётся доступным.","marker.control_broken":"Сохранённый источник отсутствует или больше не отмечен как источник света","marker.control_missing_label":"Отсутствующий источник на плане","marker.control_passive":"пассивный источник","toast.marker_control_cycle":"Такая связь создаст циклическую цепочку управления светом.","toast.marker_binding_required":"Сначала выберите устройство Home Assistant, затем добавьте управляемый источник света.","confirm.unlock":"Открыть замок «{name}»?","toast.files_migrate_failed":"Не удалось перенести вложения к новой привязке, ссылки остались на старые файлы: {err}","space.pick_saved":"Уже загруженные","space.pick_saved_hint":"Планы, сохранённые на сервере, включая отцеплённые ранее","space.no_saved":"На сервере пока нет сохранённых планов.","space.loading":"Загрузка…","space.used_by":"используется: {list}","space.in_use":"План используется пространством — сначала отцепите его","btn.use":"Выбрать","confirm.delete_plan":"Удалить файл плана «{name}» с сервера? Действие необратимо.","toast.plans_list_failed":"Не удалось получить список планов: {err}","toast.plan_delete_failed":"Не удалось удалить план: {err}","marker.hide":"Скрыть","marker.hide_tip":"Устройство исчезнет с плана после сохранения, но продолжит участвовать в расчёте сигнала комнаты. Вернуть его можно через каталог «Устройства» в редакторе устройств.","marker.show":"Показать","marker.show_tip":"Устройство снова появится на плане после сохранения.","marker.hidden_ghost":"Устройство скрыто пользователем","marker.ha_disabled_device":"Устройство деактивировано в Home Assistant и скрыто с плана.","marker.ha_disabled_entity":"Сущность деактивирована в Home Assistant и скрыта с плана.","marker.ha_disabled_all_entities":"У устройства нет активных сущностей Home Assistant, поэтому оно скрыто с плана.","marker.ha_registry_limited":"Полный реестр Home Assistant недоступен этому пользователю. Неподтверждённый объект временно нельзя показывать или использовать.","marker.delete_tip":"Полностью удалить устройство с плана и из всех расчётов. Позже его можно добавить заново.","tap.run":"Запустить автоматизацию/скрипт/сцену","tap.cover":"Открыть/закрыть (шторы/жалюзи)","marker.run_target_label":"Что запускать","marker.run_search_ph":"Поиск: автоматизация, скрипт или сцена…","marker.run_target_gone":"Цель {id} не найдена — выберите заново","marker.tap_confirm":"Спрашивать подтверждение","marker.tap_confirm_tip":"Перед выполнением показать диалог подтверждения — защита от случайных нажатий.","marker.toggle_hint_single":"Цель: {name} ({id}).","marker.virtual_light_target":"Виртуальный свет: {name}.","marker.virtual_light_current":"Ручное состояние: {state} → {effect}.","marker.virtual_light_state_on":"включён","marker.virtual_light_state_off":"выключен","marker.toggle_hint_group":"Переключатся источники ({count}): {names}.","marker.toggle_hint_current":"Сейчас: {state} → {effect}.","marker.toggle_hint_group_current":"Сейчас включено {on} из {count} → {effect}.","marker.toggle_hint_skipped":"Не будут затронуты ({count}): {targets}.","marker.toggle_effect_turn_on":"включится","marker.toggle_effect_turn_off":"выключится","marker.toggle_effect_open":"откроется","marker.toggle_effect_close":"закроется","marker.toggle_effect_stop":"остановится","marker.toggle_effect_toggle":"состояние переключится","marker.toggle_skip_missing":"не найдено","marker.toggle_skip_ha_disabled":"деактивировано в HA","marker.toggle_skip_unavailable":"недоступно","marker.toggle_skip_unsupported":"переключение не поддерживается","marker.toggle_skip_secure":"заблокировано из соображений безопасности","marker.toggle_none_no_binding":"У устройства нет привязки, состояние переключать нечем. По нажатию ничего не произойдёт.","marker.toggle_none_no_actionable_entity":"У устройства нет состояния, которое можно переключить. По нажатию ничего не произойдёт.","marker.toggle_none_configured_targets_missing":"Настроенные цели сейчас недоступны. Собственная сущность устройства не будет подставлена вместо них.","marker.toggle_none_ha_disabled":"Цель деактивирована в Home Assistant и не может использоваться на плане.","marker.toggle_none_unavailable":"Цель сейчас недоступна. По нажатию ничего не произойдёт.","marker.toggle_none_unsupported":"Для этой сущности Home Assistant не предоставляет безопасного переключения. По нажатию ничего не произойдёт.","marker.toggle_none_secure":"Переключение замков, сигнализации и защитных ворот с плана заблокировано из соображений безопасности.","run.automation":"автоматизация","run.script":"скрипт","run.scene":"сцена","confirm.tap_run":"Запустить «{name}»?","confirm.tap_toggle":"Переключить «{name}»?","confirm.tap_cover":"Открыть/закрыть «{name}»?","confirm.current_state":"Текущее состояние: {state}","confirm.expected_state":"После переключения: {state}","confirm.group_current":"включено {on} из {total}","confirm.group_all_on":"все включены","confirm.group_all_off":"все выключены","confirm.unavailable_targets":"Недоступно: {count}","confirm.expected_by_ha":"Состояние определит Home Assistant","confirm.state_on":"Включено","confirm.state_off":"Выключено","confirm.state_open":"Открыто","confirm.state_closed":"Закрыто","confirm.state_opening":"Открывается","confirm.state_closing":"Закрывается","confirm.state_stopped":"Остановлено","confirm.state_unknown":"Неизвестно","toast.run_started":"Запущено: {name}","toast.run_target_missing":"Цель запуска не найдена — проверьте настройки устройства","toast.run_target_required":"Выберите автоматизацию, скрипт или сцену","toast.tap_target_changed":"Цель действия изменилась. Повторите попытку.","toast.virtual_light_toggle_failed":"Не удалось переключить виртуальный свет: {err}","toast.value_badge_source_required":"Выберите значение для бейджа","btn.run":"Выполнить","vac.section":"Робот-пылесос: живая позиция","vac.autocal":"Настроить автоматически","vac.live":"Живая позиция на плане","vac.trail":"Показывать путь робота","vac.cal_maps":"Откалиброваны карты: {maps}","vac.autocal_no_rooms":"Интеграция не отдаёт список комнат — откройте «Подогнать вручную»","vac.autocal_no_match":"Не совпали имена комнат (нужно ≥3 общих) — откройте «Подогнать вручную»","vac.autocal_done":"Готово: привязка по {rooms} комнатам. Запустите уборку и проверьте","vac.cal_need_pos":"Робот сейчас не отдаёт координаты — запустите уборку и поставьте на паузу","vac.cal_done":"Калибровка сохранена. Запустите уборку и проверьте","vac.cal_cancelled":"Калибровка отменена","vac.fit":"Подогнать вручную","vac.fit_hint":"Перетащите карту робота на место, растяните за уголки","vac.fit_rotate":"Повернуть 90°","vac.fit_mirror":"Отразить","vac.trail_never":"Не показывать никогда","vac.trail_cleaning":"Во время уборки","vac.trail_always":"Показывать всегда","gs.bg_group":"Фон сцены","gs.bg_color":"Цвет фона вокруг плана","gs.bg_default":"Как в теме","gs.bg_theme":"по умолчанию — из темы","gs.bg_mode":"Фон плана","gs.bg_static":"Статичный цвет","gs.bg_daynight":"Следует за Солнцем","gs.bg_daynight_hint":"Фон проходит рассвет, день, закат и ночь по данным sun.sun, а при их недоступности — по локальным часам. Компас для фона не нужен.","gs.sun_group":"Солнце","gs.sun_missing":"Сущность sun.sun не найдена — фон следует локальным часам, а оконные лучи недоступны.","gs.north":"Север на плане","gs.north_ph":"не задан","gs.north_hint":"Север нужен только для оконных лучей (шаг 1°, с Shift 15°); солнечный фон работает без него.","gs.north_clear":"Сбросить","gs.north_letter":"С","gs.sun_rays":"Солнце в окнах","gs.about_group":"О карточке","gs.about_version":"Houseplan Card v{v}","gs.about_github":"GitHub · документация и issues","gs.about_telegram":"Чат в Telegram","space.bg_color":"Цвет фона вокруг плана","space.bg_inherit":"Наследовать общий","space.bg_inherited":"наследуется из общих настроек","space.bg_mode":"Фон плана","space.north":"Север на плане (переопределение)","space.north_inherited":"наследуется: {v}","space.sun_rays":"Солнце в окнах","space.sun_inherit":"Наследовать общий","space.sun_on":"Вкл","space.sun_off":"Выкл","canvas.far_objects":"Объектов далеко от плана: {n}","canvas.show_far":"Показать","canvas.home_tip":"План там — нажмите, чтобы вписать","gs.grid_group":"Обслуживание планов","gs.grid_hint":"Обновляет модели данных, выравнивает элементы по сетке и объединяет лишние фрагменты стен. Перед записью будет показан точный отчёт.","gs.align_all":"Оптимизировать планы","gs.align_title":"Оптимизировать планы","gs.align_none":"Все планы уже используют актуальную и оптимальную модель данных.","gs.optimize_no_automatic_changes":"Автоматических изменений нет. Проверьте пункты ниже.","gs.align_count":"Сдвинется элементов: {n} из {total}, максимум на {cm} см.","gs.align_where":"Наибольший сдвиг — в пространстве «{s}».","gs.align_turned":"Проёмов с исправлением угла: {n}.","gs.align_removed_drafts":"Схлопнувшиеся на сетке некорректные контуры удалены: {n}.","gs.optimize_redundant_drafts":"Сохранённые цепочки стен, полностью скрытые стенами комнат, удалены: {n}.","gs.align_preflight_failed":"Не удалось безопасно проверить геометрию следующих пространств: {spaces}{more}.","gs.align_preflight_hint":"Планы не изменены. Скопируйте диагностику кнопкой ниже и приложите её к отчёту об ошибке вместе с экспортом пространства.","gs.preflight_reason_prepare-exception":"Не удалось подготовить геометрию пространства (исключение при сборке модели)","gs.preflight_reason_wall-null":"Тело стен не построилось (объединение вернуло пустоту)","gs.preflight_reason_wall-degraded-extra":"Тело стен деградировало с лишней геометрией","gs.preflight_reason_wall-failed-core":"Ядро тела стен не собралось","gs.preflight_reason_wall-exception":"Построение стен упало с исключением","gs.preflight_reason_floor-null":"Контур пола не построился","gs.preflight_reason_floor-exception":"Построение пола упало с исключением","gs.preflight_copy":"Скопировать диагностику","gs.preflight_copied":"Диагностика скопирована","gs.preflight_update_hint":"Версии карточки и интеграции различаются — обновите House Plan и повторите.","gs.align_preflight_space":"Пространство {n}","gs.align_preflight_more":" и ещё {n}","gs.optimize_changes":"Миграций модели: {m}; обновлено пространств: {c}; устранён шум координат: {p}; объединено отрезков реальных стен: {w}; объединено отрезков стен нулевой толщины: {s}; независимых: {i}.","gs.zero_walls_migrated":"Преобразовано виртуальных участков: {n}.","gs.wall_segments_migrated":"Стабилизировано сегментов стен: {n}.","gs.optimize_lattice_summary":"Канонизировано шумовых значений координат: {n}; максимальный сдвиг: {cm} см.","gs.optimize_lattice_space":"{space}: канонизировано значений координат: {n}; оставлено значений вне сетки: {far}.","gs.optimize_coincident_partitions":"Скрытые участки независимых стен поглощены стенами комнат: {n}.","gs.optimize_openings_rehosted":"Проёмы перепривязаны к стенам комнат: {n}.","gs.optimize_walls_straightened":"Выпрямлено стен: {n}; максимальное перемещение: {cm} см.","gs.optimize_walls_straightened_where":"Максимальная правка стены: {s}.","gs.optimize_walls_straighten_skipped":"Оставлено почти осевых стен, которые нельзя безопасно выпрямить: {n}.","gs.optimize_glow_migration":"Старый Glow: пространств — {spaces} → без заливки данных + независимый Glow; комнат — {rooms} → наследуемая заливка + независимый Glow.","gs.optimize_references":"Исправлено ссылок: пространства — {spaces}; комнаты — {rooms}; позиции — {positions}; устройств отвязано от отсутствующих пространств — {detached}.","gs.optimize_reference_more":", и ещё {n}","gs.optimize_orphans_removed":"Убрано забытых записей: {total} — подписи комнат: {rooms}; устройства: {devices}; групповые метки: {groups}. Все они принадлежали пространствам, удалённым ранее.","gs.optimize_live_positions":"Старые позиции в удалённых пространствах принадлежат существующим объектам: {n}{names}. Они будут сохранены.","gs.optimize_live_positions_remove":"Старые позиции в удалённых пространствах принадлежат существующим объектам: {n}{names}. Они выбраны для удаления.","gs.optimize_live_names":": {names}{more}","gs.optimize_live_remove":"Убрать старые позиции","gs.optimize_live_keep":"Сохранить старые позиции","gs.optimize_live_selected":"Старые позиции будут убраны после применения оптимизации.","gs.optimize_unverified":"Не удалось безопасно проверить позиций: {n}. Они оставлены без изменений.","gs.optimize_registry_limited":"Для безопасной проверки нужен полный доступ администратора к реестрам Home Assistant.","gs.optimize_vacuum_warning":"Сопоставления комнат пылесоса требуют проверки: {n}.","gs.optimize_details":"Подробности","gs.optimize_details_more":"И ещё записей: {n}.","gs.optimize_detail_removed":"будет удалено","gs.optimize_detail_live":"будет сохранено","gs.optimize_detail_unverified":"не проверено","gs.optimize_detail_room_label":"подпись комнаты","gs.optimize_detail_device":"устройство","gs.optimize_detail_group":"групповая метка","gs.optimize_detail_unknown":"неизвестный владелец","gs.optimize_detail_item":"{status}: {kind} {id}; прежнее пространство {space}","gs.align_warn":"Элементы, намеренно поставленные между узлами, будут сдвинуты. После операции доступна одна отмена — только до следующего изменения плана.","gs.align_run":"Оптимизировать","gs.align_done":"Планы оптимизированы: сдвинуто элементов — {n}, обслужено записей — {m}, исправлено ссылок — {r}","gs.optimize_undo":"Отменить последнюю оптимизацию","gs.optimize_undone":"Последняя оптимизация отменена","decor.furniture":"Мебель","furn.title":"Библиотека мебели","furn.symbol":"Символ","furn.group_furniture":"Мебель","furn.group_appliance":"Техника","furn.group_sanitary":"Сантехника","furn.group_other":"Прочее","furn.width":"Ширина","furn.depth":"Глубина","furn.pick_hint":"Выберите символ и кликните по плану.","furn.place_hint":"Кликните по плану — предмет встанет к ближайшей стене. Shift — свободно.","furn.sym_sofa":"Диван","furn.sym_armchair":"Кресло","furn.sym_coffee_table":"Журнальный столик","furn.sym_table_dining":"Обеденный стол","furn.sym_table_round":"Круглый стол","furn.sym_chair":"Стул","furn.sym_desk":"Письменный стол","furn.sym_bed_double":"Двуспальная кровать","furn.sym_bed_single":"Односпальная кровать","furn.sym_nightstand":"Тумбочка","furn.sym_wardrobe":"Шкаф","furn.sym_bookshelf":"Стеллаж","furn.sym_fridge":"Холодильник","furn.sym_stove":"Плита","furn.sym_dishwasher":"Посудомоечная машина","furn.sym_washer":"Стиральная машина","furn.sym_dryer":"Сушильная машина","furn.sym_tv":"Телевизор","furn.sym_ac":"Кондиционер","furn.sym_water_heater":"Бойлер","furn.sym_toilet":"Унитаз","furn.sym_bathtub":"Ванна","furn.sym_shower":"Душ","furn.sym_sink":"Раковина","furn.sym_kitchen_sink":"Кухонная мойка","furn.sym_bidet":"Биде","furn.sym_stairs":"Лестница","furn.sym_fireplace":"Камин","furn.sym_plant":"Растение","furn.sym_rug":"Ковёр","common.yes":"Да","common.no":"Нет","vac.diag_source":"Источник","vac.diag_platform":"Интеграция","vac.diag_status":"Статус","vac.diag_position":"Позиция","vac.diag_rooms":"Комнаты","vac.diag_rooms_value":"{total} · совпало имён: {matched} · {readiness}","vac.autocal_ready":"автокалибровка доступна","vac.autocal_not_ready":"нужно 3 совпавших имени","vac.diag_path":"Путь интеграции","vac.diag_map":"ID карты","vac.source_none":"не выбран","vac.source_status_ok":"Готов","vac.source_status_missing":"Не найден","vac.source_status_disabled":"Деактивирован в Home Assistant","vac.source_status_unavailable":"Недоступен","vac.source_status_unverified":"Нельзя проверить с текущими правами","vac.source_status_unsupported":"Нет данных позиции","vac.source_status_none":"Нет источника","vac.source_banner_missing":"Сохранённый источник больше не существует. Он не заменён автоматически: выберите другой или восстановите его в Home Assistant.","vac.source_banner_disabled":"Сохранённый источник деактивирован в Home Assistant. Активируйте его там или выберите другой.","vac.source_banner_unverified":"Текущих прав Home Assistant недостаточно для проверки источника. Привязка сохранена и не будет заменена автоматически.","vac.choose_source":"Выбрать источник","vac.source_auto":"Автоматически","vac.source_auto_hint":"Использовать совместимую сущность этого устройства","vac.all_cameras":"Все камеры","vac.all_cameras_warn":"Камера может не отдавать данные робота. Выбирайте её только если это карта вашего пылесоса.","vac.all_cameras_empty":"Других сущностей камеры не найдено.","vac.platform_unknown":"интеграция неизвестна","vac.cap_position":"позиция","vac.cap_rooms_short":"комнаты","vac.cap_path":"путь","vac.cap_map":"ID карты","vac.cap_none":"данные робота не обнаружены","vac.xcme_hint":"Включите атрибуты Xiaomi Cloud Map Extractor:","vac.documentation":"Документация","vac.residual_title":"Проверьте автокалибровку","vac.residual_message":"Совпавшие комнаты расходятся максимум на {error}. Примените приблизительную калибровку, подгоните её вручную либо отмените без изменения сохранённых настроек.","vac.apply_proposal":"Применить","gs.backup_group":"Резервная копия и перенос","gs.backup_hint":"Скачайте переносимую JSON-копию или проверьте и импортируйте копию, созданную House Plan.","backup.export_open":"Экспорт","backup.import_open":"Импорт","backup.export_title":"Экспорт House Plan","backup.import_title":"Импорт House Plan","backup.export_hint":"Выберите, должна ли копия содержать всю конфигурацию House Plan или только текущее пространство.","backup.full":"Полная копия","backup.current_space":"Текущее пространство","backup.current_space_title":"Текущее пространство: {title}","backup.no_current_space":"Нет текущего пространства","backup.plan_only":"Только планировка","backup.plan_only_hint":"Сохранить комнаты, стены, проёмы, декор и позиции подписей комнат без устройств и привязок Home Assistant.","backup.plan_only_preview":"Файл содержит только планировку","backup.privacy_warning":"Архив сохраняет названия, идентификаторы Home Assistant и точные координаты. Внутренние планы и вложения указываются ссылками, но не вкладываются; текущие состояния и маршруты пылесосов не включаются.","backup.download":"Скачать JSON","backup.export_done":"Резервная копия скачана","backup.reading":"Проверяем резервную копию…","backup.revalidated":"После предпросмотра план изменился. Сводка обновлена — проверьте её и подтвердите ещё раз.","backup.error.unauthorized":"У вас нет прав на экспорт или импорт этого плана.","backup.error.not_ready":"House Plan ещё не готов. Повторите попытку через несколько секунд.","backup.error.too_large":"Размер резервной копии превышает 8 МиБ.","backup.error.invalid_json":"Выбранный файл не является корректным JSON.","backup.error.invalid_format":"Выбранный файл не является резервной копией House Plan.","backup.error.unsupported_export_version":"Эта версия формата резервной копии не поддерживается установленной версией.","backup.error.future_model":"Резервная копия создана в более новой версии модели данных House Plan.","backup.error.invalid_config":"Резервная копия содержит некорректную конфигурацию House Plan.","backup.error.wall_model_migration_blocked":"Не удалось безопасно обновить модель стен из резервной копии. Сначала оптимизируйте исходный план и повторите экспорт.","backup.error.invalid_layout":"Резервная копия содержит некорректные позиции объектов.","backup.error.invalid_content":"Резервная копия содержит некорректные или несогласованные ссылки на файлы.","backup.error.space_not_found":"Выбранное пространство больше не существует.","backup.error.capacity_exceeded":"Добавление этой копии превысит ограничения размера плана.","backup.error.preview_expired":"Время предпросмотра истекло. Выберите файл резервной копии заново.","backup.error.preview_owner_mismatch":"Этот предпросмотр принадлежит другому пользователю Home Assistant.","backup.error.conflict":"После предпросмотра план изменился. Проверьте обновлённую сводку.","backup.error.content_confirmation_required":"Подтвердите отсоединение недоступных локальных файлов.","backup.error.commit_failed":"Не удалось безопасно применить копию. Предыдущий план восстановлен или ожидает восстановления.","backup.error.missing_plan":"После предпросмотра исчез связанный файл плана. Проверьте копию ещё раз.","backup.error.missing_content":"После предпросмотра исчез связанный локальный файл. Проверьте копию ещё раз.","backup.error.marker_control_missing":"В резервной копии отсутствует связанный источник света на плане.","backup.error.marker_control_not_light":"Связанная цель больше не отмечена как источник света.","backup.error.marker_control_self":"Источник света не может управлять самим собой.","backup.error.marker_control_cycle":"Резервная копия содержит циклическую цепочку управления светом.","backup.error.duplicate_marker_control":"Резервная копия содержит повторяющуюся связь с источником света.","backup.error.no_backup":"Нет снимка импорта или оптимизации, который можно восстановить.","backup.same_source":"Создано на этом экземпляре Home Assistant","backup.foreign_source":"Создано на другом экземпляре Home Assistant","backup.created":"Создано: {value}","backup.versions":"Карточка {card}; интеграция {integration}; модель данных {model}","backup.count_spaces":"Пространств: {n}","backup.count_rooms":"Комнат: {n}","backup.count_walls":"Стен: {n}","backup.count_openings":"Проёмов: {n}","backup.count_decor":"Объектов декора: {n}","backup.count_markers":"Устройств: {n}","backup.count_layout":"Позиций: {n}","backup.bindings":"Привязки — устройства: {device}, сущности: {entity}, виртуальные: {virtual}; позиций без пространства: {legacy}","backup.binding_status":"Статус на целевом экземпляре — активных: {active}, деактивированных: {disabled}, отсутствующих: {missing}","backup.missing_areas":"На целевом экземпляре отсутствуют зоны: {areas}","backup.dropped_marker_links":"Связи с источниками света вне переносимого пространства пропущены: {n}.","backup.repaired_target_refs":"Существующих ссылок восстановлено этим импортом: {n}.","backup.preserved_unresolved_refs":"Неоднозначные ссылки сохранены без изменений: {n}.","backup.preserved_unresolved_hint":"House Plan ничего не угадывал и не удалял. После импорта запустите «Оптимизировать планы», чтобы проверить оставшиеся ссылки.","backup.import_details":"Подробности восстановления ссылок","backup.import_detail.incoming_remapped":"Ссылок обновлено внутри импортируемой копии: {n}","backup.import_detail.target_repaired":"Существующих ссылок восстановлено: {n}","backup.import_detail.preserved_unresolved":"Неразрешимых ссылок сохранено: {n}","backup.import_detail.collisions":"Конфликтов назначения безопасно сохранено: {n}","backup.import_detail.dropped_links":"Входящих связей пропущено по правилам переноса: {n}","backup.import_detail.bounded_lineages":"Чрезмерно вложенных идентификаторов ограничено: {n}","backup.replace_warning":"Текущая конфигурация и расположение будут заменены. Загруженные файлы не удаляются. Одну отмену можно выполнить до следующего изменения плана.","backup.foreign_bookkeeping":"Служебные списки известных и новых устройств другого экземпляра импортированы не будут.","backup.final_name":"Название нового пространства","backup.target_settings":"Глобальные настройки целевого экземпляра не изменятся.","backup.duplicates":"Уже существующих привязок: {n}","backup.skip":"Пропустить повторяющиеся устройства","backup.virtual_copy":"Добавить безопасные статичные виртуальные копии","backup.content":"Связанное содержимое","backup.content_available":"доступно локально","backup.content_external":"внешняя ссылка","backup.content_detach_required":"будет отвязано","backup.confirm_detach":"Я понимаю, что недоступные внутренние планы и вложения будут отвязаны от импортируемой конфигурации.","backup.replace":"Заменить","backup.add":"Добавить пространство","backup.space_done":"Пространство импортировано: комнат — {rooms}, устройств — {markers}, существующих ссылок восстановлено — {refs}","backup.full_done":"Копия восстановлена: пространств — {spaces}, комнат — {rooms}, устройств — {markers}","backup.undo_import":"Отменить последний полный импорт","backup.import_undone":"Полный импорт отменён"}}],Gd=Wd;function Vd(e){return"string"==typeof e?e.trim().replaceAll("_","-").toLowerCase():""}const Kd=new Map(Ud.map(e=>[Vd(e.code),e]));function Yd(e){return Kd.get(Vd(e))}function Zd(e,t){const i=[{value:"",label:e},...Ud.map(({code:e,nativeLabel:t})=>({value:e,label:t}))],n="string"==typeof t?t:"";return n&&!i.some(e=>e.value===n)&&i.push({value:n,label:Yd(n)?.nativeLabel??n}),i}function Xd(e,t){return function(e,t,i,n){const r=new Map(i.map(e=>[Vd(e),e])),o=r.get(Vd(e));if(o)return o;const s=Vd(t),a=r.get(s);return a||(r.get(s.split("-")[0]||"")??n)}(t,e?.locale?.language||e?.language,Ud.map(({code:e})=>e),"en")}function Jd(e,t,i){const n=Yd(e)?.dictionary;return Vi(n?.[t]??Gd[t]??t,i)}function Qd(e,t){const i=Yd(e)?.dictionary[t]??Gd[t];return"string"==typeof i&&i.trim().length>0}function eu(e){const t=e.length/2,i=e.face.cm>0?e.face.cm/e.cellCm*e.gridPitch/2:xa(4,e.cellCm),n="gate"===e.type?Math.sin(10*Math.PI/180)*t:0;return{half:t,jambHalf:i,gateDepth:n,outlineHalf:Math.max(xa(16,e.cellCm),i+xa(8,e.cellCm),n+xa(8,e.cellCm)),hitHalf:Math.max(xa(20,e.cellCm),i+xa(10,e.cellCm),n+xa(12,e.cellCm))}}function tu(e){if("passage"===e.type)return W``;const t=Math.max(0,Math.min(1,e.amount)),{half:i,jambHalf:n}=eu(e),r=ka(e.cellCm),o=1.75*r,s=e.flipH?-1:1,a=e.flipV?-1:1,l="gate"===e.type?1:a;e.type,e.flipV,e.angle,e.face;let c;if("window"===e.type){const o=Math.PI/2*i,s=e.face.cm>0?W``:W``;c=W` + + + + + + + + + + + + + + ${s} + `}else if("gate"===e.type){const n=10*e.face.side*t;c=W` + + + + + + + + + + + + `}else{const n=Math.PI/2*e.length;c=W` + + + + + + + + `}return W` + + + ${c} + `}const iu={climate:["current_temperature","temperature","current_humidity","humidity"],water_heater:["current_temperature","temperature"],cover:["current_position"],valve:["current_position"],fan:["percentage"],humidifier:["current_humidity","humidity"],light:["brightness"],media_player:["volume_level"],vacuum:["battery_level","fan_speed"],lawn_mower:["battery_level","fan_speed"]};function nu(e){return e?e.sourceLabel?`${e.sourceLabel}: ${e.fullText}`:e.fullText:""}const ru=new WeakMap;function ou(e){return e?"entity_state"===e.kind?`state:${e.entity_id}`:"entity_attribute"===e.kind?`attr:${e.entity_id}:${e.attribute}`:"derived_marker_state"===e.kind?`marker:${e.ref}`:"derived:lqi":""}function su(e){if("derived:lqi"===e)return{kind:"derived_lqi"};if(e.startsWith("state:"))return{kind:"entity_state",entity_id:e.slice(6)};if(e.startsWith("attr:")){const t=e.slice(5),i=t.lastIndexOf(":");if(i>0)return{kind:"entity_attribute",entity_id:t.slice(0,i),attribute:t.slice(i+1)}}return e.startsWith("marker:marker:")?{kind:"derived_marker_state",ref:e.slice(7)}:null}function au(e,t){const i=e?.entities?.[t],n=e?.states?.[t];return String(i?.name||i?.original_name||n?.attributes?.friendly_name||t)}function lu(e){return e.replaceAll("_"," ")}function cu(e,t=1){const i=Number(e);return Number.isFinite(i)?`${Math.round(i*t)} %`:null}function hu(e){if("derived_lqi"===e.kind)return"lqi";if("entity_attribute"===e.kind){if(e.attribute.includes("temperature"))return"temperature";if(e.attribute.includes("humidity"))return"humidity"}return"default"}function du(e,t,i,n=[]){let r=null,o="",s="available";if("entity_state"===i.kind){o=au(e,i.entity_id);const t=e?.states?.[i.entity_id];if(t)if(pu(e,i.entity_id))if(["unknown","unavailable"].includes(String(t.state).toLowerCase()))s="unavailable";else{const n=ji(e,i.entity_id);r=n?qi(n,String(t.attributes?.unit_of_measurement||"")):null,r||(s="unavailable")}else s="unavailable";else s="missing"}else if("entity_attribute"===i.kind){o=`${lu(i.attribute)} · ${au(e,i.entity_id)}`;const t=e?.states?.[i.entity_id];t?pu(e,i.entity_id)?(r=function(e,t,i){const n=e?.states?.[t];if(!n||!(i in(n.attributes||{})))return null;const r=ji(e,t,i);if(r?.formatted)return r.text;const o=n.attributes?.[i];if("brightness"===i)return cu(o,100/255);if("volume_level"===i)return cu(o,100);if(["current_position","percentage","current_humidity","humidity","battery_level"].includes(i))return cu(o);if("current_temperature"===i||"temperature"===i){const t=Number(o);if(!Number.isFinite(t))return null;const i=String(e?.config?.unit_system?.temperature||"°C");return`${Math.round(10*t)/10} ${i}`}return r?.text||(["string","number","boolean"].includes(typeof o)?String(o):null)}(e,i.entity_id,i.attribute),null==r&&(s="unavailable")):s="unavailable":s="missing"}else if("derived_lqi"===i.kind){o="LQI";const i=Ch(e,t.entities);null==i?s="unavailable":r=String(i)}else{o="Light state";const t=n.find(e=>e.ref===i.ref);if(t){const i=t.on?"on":"off",n=e?.localize?.(`state.default.${i}`);r="string"==typeof n&&n?n:i,o=t.name||o}else s="missing"}const a=e?.localize?.("state.default.unavailable")||"Unavailable";return{source:i,sourceLabel:o,text:r??"—",fullText:r??a,availability:s,isLqi:"derived_lqi"===i.kind,tone:hu(i)}}function uu(e,t){if(!0===t.marker?.use_climate_temp){const i=t.entities.find(t=>t.startsWith("climate.")&&Number.isFinite(Number(e?.states?.[t]?.attributes?.current_temperature)));return i?{kind:"entity_attribute",entity_id:i,attribute:"current_temperature"}:null}if("mdi:thermometer"===t.icon||"mdi:air-filter"===t.icon){const i=t.entities.find(t=>sh(e,t)&&Number.isFinite(Number(e?.states?.[t]?.state)));if(i)return{kind:"entity_state",entity_id:i}}return t.primary&&Oh(e,t.primary)&&Number.isFinite(Number(e?.states?.[t.primary]?.state))?{kind:"entity_state",entity_id:t.primary}:null}function pu(e,t){const i=e?.entities?.[t];if(i&&!Zc(i))return!1;const n=i?.device_id?e?.devices?.[i.device_id]:null;return!n||Zc(n)}function _u(e,t){return pu(e,t)&&!!e?.states?.[t]}function mu(e,t,i=[t]){const n=function(e,t){const i=e=>[e.id,e.primary,e.hidden?1:0,e.userHidden?1:0,e.entities.join(","),(e.controls||[]).join(","),e.marker?.binding||"",!0===e.marker?.is_light?1:0,e.marker?.light_entity||"",(e.marker?.controls||[]).join(",")].join("|");return`${i(e)}\n${t.map(i).join("\n")}`}(t,i),r=ru.get(t);if(r&&r.states===(e?.states||null)&&r.entities===(e?.entities||null)&&r.devices===(e?.devices||null)&&r.signature===n)return r.result;const o=new Set(t.entities);for(const e of t.controls||[])e.startsWith("marker:")||o.add(e);const s=[],a=Mh(e,i),l=a.filter(e=>e.key.startsWith("marker:")).map(e=>({ref:e.key,on:e.on,name:e.device?.name||e.key})),c=i=>{const n=ou(i);if(s.some(e=>e.key===n))return;const r=du(e,t,i,l),o="entity_attribute"===i.kind?`${i.entity_id} · ${i.attribute}`:"entity_state"===i.kind?i.entity_id:"derived_marker_state"===i.kind?i.ref:"LQI";s.push({key:n,source:i,label:r.sourceLabel,technical:o,value:r.text,available:"available"===r.availability})};for(const t of[...o].sort()){if(!_u(e,t))continue;const i=t.split(".")[0],n=e?.entities?.[t];if("button"===i||"event"===i||"config"===n?.entity_category)continue;c({kind:"entity_state",entity_id:t});const r=e?.states?.[t];for(const e of iu[i]||[])e in(r?.attributes||{})&&c({kind:"entity_attribute",entity_id:t,attribute:e})}const h=new Set((t.marker?.controls||t.controls||[]).filter(e=>e.startsWith("marker:")));!0===t.marker?.is_light&&t.marker.id&&h.add(`marker:${t.marker.id}`);const d=new Set(a.map(e=>e.key));for(const e of[...h].sort())d.has(e)&&c({kind:"derived_marker_state",ref:e});return t.virtual||null==Ch(e,t.entities)||c({kind:"derived_lqi"}),ru.set(t,{states:e?.states||null,entities:e?.entities||null,devices:e?.devices||null,signature:n,result:s}),s}function gu(e,t,i){const n=uu(e,t);if(n&&i.some(e=>e.key===ou(n)))return n;const r=t.primary&&i.find(e=>e.key===`state:${t.primary}`);if(r)return r.source;const o=[e=>e.technical.includes("temperature"),e=>e.technical.includes("humidity"),e=>e.technical.includes("battery")];for(const e of o){const t=i.find(e);if(t)return t.source}return i.find(e=>"derived_lqi"!==e.source.kind)?.source||i[0]?.source||null}function fu(e){return e.touched?{value_badge:{enabled:e.enabled,source:e.source,position:e.position}}:e.originalHas?{value_badge:e.original}:{}}function vu(e,t,i){return{kind:"none",reason:"none",generation:e,expiresAt:null,color:t,diameterScale:i,animated:!1,reducedMotionIndicator:"none"}}function yu(e,t,i,n,r=null){return{kind:t,reason:"short"===t?"event":"running",generation:Math.max(1,Math.trunc(i)),expiresAt:"short"===t?r:null,color:e.color,diameterScale:n?1:e.diameterScale,animated:!n,reducedMotionIndicator:n?"dot":"none"}}const bu={availability:"available",status:"neutral",activity:"none"};function wu(e){return e<=40?"low":e<180?"mid":"high"}function ku(e){return Ut(e)}function xu(e){return"static_icon"===e.display?"neutral":"alarm"===e.visual.status?"alarm":"unavailable"===e.visual.availability?"unavailable":e.lockState?e.lockState:"working"===e.visual.status?"working":"open"===e.visual.status?"open":"neutral"}const $u={sourceKind:"none",decisionIds:["source.skipped_static_fast_path"],visualSources:[],criticalSources:[],samples:[]};function Su(e,t){const i=e?.entities?.[t],n=e?.states?.[t];return String(i?.name||i?.original_name||n?.attributes?.friendly_name||t)}function Mu(e,t){const i=e?.states?.[t];if(!i)return"";if("function"==typeof e?.formatEntityState)try{const t=e.formatEntityState(i);if("string"==typeof t&&t)return t}catch{}return String(i.state??"")}function Tu(e,t){if(t.virtual||"virtual"===t.bindingKind||"virtual"===t.marker?.binding)return"available";return(t.entities||[]).some(t=>{const i=String(e?.states?.[t]?.state??"").trim().toLowerCase();return""!==i&&"unknown"!==i&&"unavailable"!==i})?"available":"unavailable"}function Ru(e,t,i,n){return{eid:t,role:i,name:Su(e,t),state:String(e?.states?.[t]?.state??""),stateText:Mu(e,t),integrationDomain:e?.entities?.[t]?.platform||null,sample:n||xc(e,t)}}function Cu(e,t,i=[t],n,r=e){const o=t.hidden&&t.userHidden?{...t,hidden:!1}:t;let s="none",a=[];const l=[],c=[...o.entities,...o.allEntities||[]].some(e=>e.startsWith("cover."))?{...o,tapAction:"cover"}:o,h=(d=vd({hass:e,devices:i,device:c,lightSources:n,registryHass:r}),"cover"!==d?.semantics?null:d.targets[0]?.entityId||d.skippedTargets[0]?.entityId||null);var d;const u=Mh(e,[o]),p=uh(o.marker?.binding,o.marker?.controls??o.controls,o.entities),_=new Set(p.filter(e=>e.startsWith("marker:"))),m=!0===o.marker?.is_light||_.size>0?n||Mh(e,i):u,g=m.filter(e=>e.device.id===o.id&&"controls"!==e.via),f=u.filter(e=>"controls"!==e.via),v=g.length?g:f,y=zc(o.marker),b=y?[...v]:[...u.filter(e=>"controls"===e.via),...v];if(_.size)for(const e of m){if(!_.has(e.key))continue;e.stateEids.length>0&&b.some(t=>t.stateEids.some(t=>e.stateEids.includes(t)))||b.some(t=>t.key===e.key)||b.push({...e,via:"controls",castsGlow:!1})}const w=ch(r,o.entities),k=!!h&&("cover"===o.tapAction||o.primary?.startsWith("cover.")||w.some(e=>e.startsWith("cover.")));if(h&&!k&&l.push("source.cover_capability_bypassed"),k)s="cover",l.push("source.cover"),a=[Ru(e,h,"cover")];else if(b.length)s=!y&&b.some(e=>"controls"===e.via)?"controls":"light",y?l.push("source.manual_virtual_light"):l.push("controls"===s?"source.controls":"source.owned_light"),"controls"!==s||!o.virtual&&"virtual"!==o.bindingKind||l.push("source.virtual_controller"),a=b.map(i=>{const n="controls"===i.via?"control":"forced"===i.via?"forced_light":"light";if(!i.passive)return Ru(e,i.eid,n);const r=i.on?"on":"off";return{eid:i.key,role:n,name:t.name,state:r,stateText:r,integrationDomain:null,sample:{eid:i.key,state:r,availability:"available",status:i.on?"working":"neutral",activity:"none",edge:"none"}}});else{const t=ch(r,o.entities),i=t.length?t:o.entities.filter(t=>!!e?.states?.[t]);if(i.length){s="device_role",l.push("source.device_role");const t=$c(r===e?e:{...r,states:e?.states||{}},i,o.entities),n=i.some(e=>bc(r,e));a=t.map(t=>{const i=n?wc(r,t.eid)?"power_gate":bc(r,t.eid)?"lifecycle":"device_role":"device_role";return Ru(e,t.eid,i,t)})}else o.primary?(s="primary",l.push("source.primary_fallback"),a=[Ru(e,o.primary,"primary")]):l.push("source.none")}p.length>0&&"controls"!==s&&"light"!==s&&"cover"!==s&&l.push("source.filtered_saved_controls");const x=[];for(const t of o.entities||[]){const i=xc(e,t);"alarm"!==i.status||a.some(e=>e.eid===t)||x.push(Ru(e,t,"critical",i))}return x.length&&l.push("source.critical_sibling"),{sourceKind:s,decisionIds:l,visualSources:a,criticalSources:x,samples:[...a,...x].map(e=>e.sample)}}function Du(e,t,i,n){if(t.virtual)return{source:null,text:null,fallback:"value_virtual"};if(n){const i=function(e,t){if(!0!==t.marker?.use_climate_temp)return null;for(const i of t.entities){if(!i.startsWith("climate."))continue;const t=Number(e?.states?.[i]?.attributes?.current_temperature);if(Number.isFinite(t))return{eid:i,text:Math.round(10*t)/10+"°"}}return null}(e,t);if(i)return{source:{kind:"temperature",eid:i.eid,attribute:"current_temperature",text:i.text},text:i.text,fallback:null};const n=function(e,t){if("mdi:thermometer"!==t.icon&&"mdi:air-filter"!==t.icon)return null;for(const i of t.entities){if(!sh(e,i))continue;const t=Number(e?.states?.[i]?.state);if(Number.isFinite(t))return{eid:i,text:Math.round(10*t)/10+"°"}}return null}(e,t);if(n)return{source:{kind:"temperature",eid:n.eid,text:n.text},text:n.text,fallback:null};const r=function(e,t){if(!t.primary||!Oh(e,t.primary))return null;const i=Number(e?.states?.[t.primary]?.state);return Number.isFinite(i)?{eid:t.primary,text:`${Math.round(i)}%`}:null}(e,t);if(r)return{source:{kind:"humidity",eid:r.eid,text:r.text},text:r.text,fallback:null}}const r=i.visualSources.filter(e=>!e.eid.startsWith("marker:"));let o=r.map(e=>e.eid);const s=r.find(e=>"power_gate"===e.role);if(s&&(o=[s.eid]),!o.length&&i.visualSources.some(e=>e.eid.startsWith("marker:"))&&(o=ch(e,t.entities),!o.length&&t.primary&&e?.states?.[t.primary]&&(o=[t.primary])),o=[...new Set(o)],1!==o.length)return{source:null,text:null,fallback:o.length?"value_ambiguous_sources":"value_no_state"};const a=o[0],l=function(e,t){const i=e?.states?.[t];if(!i||null==i.state||""===String(i.state).trim())return{text:"",fallback:"value_no_state"};const n=i.state;if(!["string","number","boolean"].includes(typeof n))return{text:"",fallback:"value_non_scalar"};const r=String(n).trim().toLowerCase();if("unknown"===r||"unavailable"===r)return{text:"",fallback:"value_no_state"};const o=ji(e,t);return o?{text:qi(o,String(i.attributes?.unit_of_measurement||"")),fallback:null}:{text:"",fallback:"value_no_state"}}(e,a);return l.fallback?{source:null,text:null,fallback:l.fallback}:{source:{kind:"entity",eid:a,text:l.text},text:l.text,fallback:null}}function Au(e,t,i){const n=e.marker?.binding||(e.bindingKind&&e.bindingRef?`${e.bindingKind}:${e.bindingRef}`:e.virtual?"virtual":""),r=[...t.visualSources.map(e=>`${e.role}:${e.eid}`),...t.criticalSources.map(e=>`critical:${e.eid}`)].sort(),o=i?`${i.kind}:${i.eid}:${i.attribute||""}`:"none";return[n,t.sourceKind,...r,`value:${o}`].join("\n")}function Ou(e,t,i){return Au(t,i||Cu(e,t),null).replace(/\nvalue:none$/,"")}function zu(e){if(e.effectiveHidden)return[];if("static_icon"===e.display)return["static-icon"];const t=[],{visual:i}=e;return"alarm"===e.pulse.kind?t.push("alarm"):"unavailable"===i.availability?t.push("unavail"):"working"===i.status?t.push("on"):"open"===i.status&&t.push("open"),e.lockState&&t.push(`lock-${e.lockState}`),"none"!==e.pulse.reason&&"alarm"!==e.pulse.reason&&t.push("activity-"+e.pulse.reason),e.pulse.generation%2==0&&(t.push("pulse-gen2"),"short"===e.pulse.kind&&t.push("activity-gen2")),t}function Pu(e,t,i){const n=Ai(t.marker?.display),r="static_icon"===n,o=r&&!1===i.sourceDetails?$u:Cu(e,t,i.lightDevices||[t],i.lightSources,i.registryHass||e),s=t.bindingStatus,a="ha_disabled"===s?.kind,l="orphaned"===s?.kind,c=a?"ha_disabled":l?"orphaned":"active",h=!0===t.userHidden||!0===t.marker?.hidden,d=Sc(o.samples),u=o.visualSources.find(e=>e.eid.startsWith("lock.")),p=u?["unlocked","open"].includes(u.state.toLowerCase())?"unlocked":"locked"===u.state.toLowerCase()?"locked":null:null,_=!zc(t.marker)&&uh(t.marker?.binding,t.marker?.controls??t.controls,t.entities).length>0,m="controls"===o.sourceKind||_&&"light"!==o.sourceKind&&"cover"!==o.sourceKind,g=r&&!1===i.sourceDetails?{source:null,text:null,fallback:null}:Du(e,t,o,i.showTemperature),f=Au(t,o,g.source),v=Ou(e,t,o),y=i.activityRuntime,b=i.now??Date.now(),w=y?.expiresAt||(y?.flashTs?y.flashTs+3300:0),k=y?.sources===v&&y.flashTs&&y.flashKind&&w>b?y.flashKind:null,x=function(e){const t=[],i="ha_disabled"===e.bindingLifecycle||"orphaned"===e.bindingLifecycle;let n=!1;"ha_disabled"===e.bindingLifecycle?(n=!0,t.push("lifecycle.ha_disabled_hidden")):e.userHidden&&!e.designPreview?(n=!0,t.push("lifecycle.user_hidden")):e.userHidden?t.push("lifecycle.user_hidden_preview"):"orphaned"===e.bindingLifecycle?t.push("lifecycle.orphaned_diagnostic"):t.push("lifecycle.active");const r="static_icon"===e.display;let o=e.controllerFace?{...e.sourceVisual,availability:e.controllerAvailability}:e.sourceVisual;t.push(e.controllerFace?"available"===e.controllerAvailability?"availability.controller_available":"availability.controller_unavailable":"availability.source"),n?(o=bu,t.push("face.hidden")):r?(o=bu,t.push("face.static")):"alarm"===e.sourceVisual.status||e.liveStates?t.push("face.dynamic"):(o=bu,t.push("face.live_states_disabled")),!r&&!n&&e.liveStates&&"alarm"!==e.sourceVisual.status&&e.shortActivity&&(o={...o,activity:e.shortActivity},t.push(`activity.short_${e.shortActivity}`)),"alarm"===o.status?t.push("status.alarm"):"unavailable"===o.availability?t.push("status.unavailable"):"working"===o.status?t.push("status.working"):"open"===o.status?t.push("status.open"):t.push("status.neutral");const s="value"===e.display&&!n&&e.valueAvailable?"value":"icon";t.push("value"===s?"content.value":"value"===e.display?"content.value_fallback_icon":"content.icon"),"value"===e.display&&!e.valueAvailable&&e.valueFallback&&t.push(`content.${e.valueFallback}`);const a=e.liveStates&&!r&&!n,l=!r&&!n,c=!n&&!i&&!r&&"available"===o.availability&&("alarm"===o.status||e.liveStates&&"icon_ripple"===e.display),h=l&&e.vacuumLiveRequested;return t.push(a?"diagnostics.dynamic_icon":"diagnostics.base_icon"),t.push(l?"diagnostics.metrics_enabled":"diagnostics.metrics_suppressed"),t.push(c?"activity.pulse_eligible":"activity.pulse_suppressed"),t.push(h?"diagnostics.vacuum_live":"diagnostics.vacuum_static"),{effectiveHidden:n,bindingUnavailable:i,visual:o,face:s,dynamicIcon:a,metrics:l,liveColor:a,pulseEligible:c,vacuumLive:h,decisionIds:t}}({bindingLifecycle:c,userHidden:h,designPreview:!0===i.designPreview,display:n,liveStates:i.liveStates,sourceVisual:d,controllerFace:m,controllerAvailability:m?Tu(e,t):"available",shortActivity:k,valueAvailable:null!=g.text,valueFallback:g.fallback,vacuumLiveRequested:!0===t.marker?.vacuum?.live}),{effectiveHidden:$,visual:S}=x,M="icon_ripple"===n&&!$&&i.liveStates&&"alarm"!==S.status?S.activity:"none",T=x.metrics&&i.showTemperature?!0===t.marker?.use_climate_temp?Ah(e,t.entities):"mdi:thermometer"===t.icon||"mdi:air-filter"===t.icon?Dh(e,t.entities):null:null,R=x.metrics&&i.showTemperature&&t.primary&&Oh(e,t.primary)?zh(e,t.entities):null,C=x.metrics&&i.showSignal&&!t.virtual?Ch(e,t.entities):null,D=i.lightSources||("derived_marker_state"===t.marker?.value_badge?.source?.kind?Mh(e,i.lightDevices||[t]):[]),A=function(e,t,i){const n=null!=t.marker?.value_badge;if(i.effectiveHidden||"static_icon"===i.display)return null;const r=t.marker?.value_badge;if(n)return r?.enabled?r.source?{configured:!0,enabled:!0,position:r.position||"right",...du(e,t,r.source,i.markerStates)}:{configured:!0,enabled:!0,source:null,sourceLabel:"",text:"—",fullText:e?.localize?.("state.default.unavailable")||"Unavailable",position:r.position||"right",availability:"missing",isLqi:!1,tone:"default"}:null;if(!i.showTemperature||"value"===i.display)return null;const o=uu(e,t);if(!o)return null;let s=null,a="default";if("entity_attribute"===o.kind&&"current_temperature"===o.attribute){const i=Ah(e,t.entities);null!=i&&(s=`${i}°`),a="temperature"}else if("mdi:thermometer"===t.icon||"mdi:air-filter"===t.icon){const i=Dh(e,t.entities);null!=i&&(s=`${i}°`),a="temperature"}else{const i=zh(e,t.entities);null!=i&&(s=`${i}%`),a="humidity"}return null==s?null:{configured:!1,enabled:!0,source:o,position:"right",sourceLabel:"entity_attribute"===o.kind?`${lu(o.attribute)} · ${au(e,o.entity_id)}`:"entity_state"===o.kind?au(e,o.entity_id):"",text:s,fullText:s,availability:"available",isLqi:!1,tone:a}}(e,t,{showTemperature:i.showTemperature,showSignal:i.showSignal,display:n,effectiveHidden:$,markerStates:D.filter(e=>e.key.startsWith("marker:")).map(e=>({ref:e.key,on:e.on,name:e.device.name}))}),O=o.visualSources.find(e=>!e.eid.startsWith("marker:")),z="cover"===o.sourceKind?O?.eid:t.primary||O?.eid,P=z?e?.states?.[z]:null,F=x.dynamicIcon?dn(t.icon,z?.split(".")[0],P?.attributes?.device_class,P?.state,!!t.marker?.icon):t.icon,I=x.liveColor&&o.visualSources.filter(e=>!e.eid.startsWith("marker:")).map(t=>function(e){return e&&"on"===e.state?Wt(e.attributes?.rgb_color):null}(e?.states?.[t.eid])).find(e=>!!e)||null,E=Number(t.marker?.size)>0?Number(t.marker.size):1,N=Number(t.marker?.angle)||0,H=Number(t.marker?.ripple_size)>0?Number(t.marker.ripple_size):1.5,L=jt(t.marker?.ripple_color,null),j=r?null:L||I||null,q=function(e){const{display:t,visual:i,semanticActivity:n,liveStates:r,effectiveHidden:o,bindingUnavailable:s=!1,reducedMotion:a=!1}=e,l=Math.max(1,Math.trunc(e.shortGeneration||1)),c=Number.isFinite(e.diameterScale)?Math.max(1,Number(e.diameterScale)):1.5,h=e.color||function(e,t){return"presence"===t?"#1DC21D":"running"===t||"working"===e.status||"open"===e.status?"#F0A00C":"#0C82F0"}(i,n);if(o||s||"unavailable"===i.availability||"static_icon"===t)return vu(l,h,c);if("alarm"===i.status)return{kind:"alarm",reason:"alarm",generation:l,expiresAt:null,color:"#F0410C",diameterScale:1.5,animated:!a,reducedMotionIndicator:"none"};if(!r||"icon_ripple"!==t)return vu(l,h,c);const d=e.now??Date.now(),u=e.shortExpiresAt||null;return e.shortReason&&null!=u&&u>d?{kind:"short",reason:e.shortReason,generation:l,expiresAt:u,color:h,diameterScale:a?1:c,animated:!a,reducedMotionIndicator:a?"dot":"none"}:"presence"===n||"transition"===n||"running"===n?{kind:"continuous",reason:n,generation:l,expiresAt:null,color:h,diameterScale:a?1:c,animated:!a,reducedMotionIndicator:a?"dot":"none"}:vu(l,h,c)}({display:n,visual:S,semanticActivity:d.activity,shortReason:y?.sources===v?y?.flashKind:null,shortGeneration:y?.gen,shortExpiresAt:y?.sources===v?w:null,now:b,liveStates:i.liveStates,effectiveHidden:$,bindingUnavailable:x.bindingUnavailable,reducedMotion:i.reducedMotion,color:j,diameterScale:H}),B="value"===x.face?g.text:null,W="ha_disabled"===s?.kind?s.reason:null,U="ha_disabled"===(G={lifecycle:c,display:n,liveStates:i.liveStates,sourceKind:o.sourceKind,primaryDomain:(t.primary||"").split(".")[0],visual:S,activity:M}).lifecycle?"ha_disabled":"orphaned"===G.lifecycle?"orphaned":"static_icon"===G.display?"static_icon":"alarm"===G.visual.status?"alarm":G.liveStates?"unavailable"===G.visual.availability?"unavailable":"cover"===G.sourceKind?"cover_icon_state":"presence"===G.activity?"presence":"event"===G.activity?"event":"transition"===G.activity?"transition":"working"===G.visual.status?"icon_ripple"===G.display&&"none"!==G.activity?"working_activity":"working":"open"===G.visual.status?"open":"media_player"===G.primaryDomain?"media_neutral":"neutral":"live_states_disabled";var G;const V=[];i.designPreview&&h&&V.push("hidden_design_preview"),r||!0!==t.marker?.vacuum?.live||V.push("vacuum_live_plan_only");const K=o.visualSources.some(t=>"power_gate"===t.role||wc(e,t.eid)),Y=t.entities.filter(t=>t.startsWith("switch.")&&!e?.entities?.[t]?.entity_category).length;K&&Y>1&&V.push("composite_power_source"),"icon_ripple"!==n&&"static_icon"!==n&&"alarm"!==d.status&&"none"!==d.activity&&V.push("activity_display_disabled");const Z={binding:t.marker?.binding||(t.bindingKind&&t.bindingRef?`${t.bindingKind}:${t.bindingRef}`:t.virtual?"virtual":""),sourceKind:o.sourceKind,visualSources:o.visualSources,criticalSources:o.criticalSources,valueSource:g.source,sourceSignature:f,decisionIds:[...o.decisionIds,...x.decisionIds,...A?["diagnostics.value_badge"]:[],...null==C?[]:[`diagnostics.lqi_${wu(C)}`],`pulse.${q.kind}_${q.reason}`],visual:S,lockState:p,display:n,icon:F,valueText:B,valueFullText:B,fallbackReason:"value"!==n||B?null:g.fallback,activity:M,activityGeneration:y?.gen||1,pulse:q,classes:[],tempText:null==T?null:String(T),humText:null==R?null:String(R),valueBadge:A,lqiText:null==C||A?.isLqi?null:String(C),lqiColor:null==C?null:ku(C),lqiBand:null==C?null:wu(C),lightColor:I,scale:E,angle:N,rippleScale:H,rippleColor:j,userHidden:h,effectiveHidden:$,haDisabled:a,disabledReason:W,orphaned:l,vacuumLive:x.vacuumLive,explanation:{reason:U,notices:V}};return{...Z,classes:zu(Z)}}function Fu(e,t=new WeakMap){if(null===e||"object"!=typeof e)return e;const i=e,n=t.get(i);if(n)return n;if(Array.isArray(e)){const n=[];t.set(i,n);for(const i of e)n.push(Fu(i,t));return Object.freeze(n)}const r={};t.set(i,r);for(const[i,n]of Object.entries(e))"function"!=typeof n&&(r[i]=Fu(n,t));return Object.freeze(r)}function Iu(e){const t=new Map(e);let i;return i=Object.freeze({get size(){return t.size},get:e=>t.get(e),has:e=>t.has(e),entries:()=>t.entries(),keys:()=>t.keys(),values:()=>t.values(),forEach:(e,n)=>t.forEach((t,r)=>e.call(n,t,r,i)),[Symbol.iterator]:()=>t[Symbol.iterator]()}),i}function Eu(e){const t=new Set(e.entityIds||[]),i=new Set(e.deviceIds||[]),n=new Set(e.areaIds||[]);for(const n of e.devices){for(const e of n.entities||[])t.add(e);n.primary&&t.add(n.primary);for(const e of n.controls||[])t.add(e);n.marker?.vacuum?.source&&t.add(n.marker.vacuum.source),"device"===n.bindingKind&&n.bindingRef&&i.add(n.bindingRef),"entity"===n.bindingKind&&n.bindingRef&&t.add(n.bindingRef)}const r={},o={};for(const[o,s]of Object.entries(e.hass?.entities||{})){const a=s?.device_id?e.hass?.devices?.[s.device_id]:null;(t.has(o)||s?.device_id&&i.has(s.device_id)||n.has(s?.area_id)||n.has(a?.area_id))&&(t.add(o),r[o]=s,s?.device_id&&i.add(s.device_id))}for(const t of i){const i=e.hass?.devices?.[t];i&&(o[t]=i)}const s={};for(const i of t){const t=e.hass?.states?.[i];t&&(s[i]=t)}const a=Object.freeze({states:Fu(s),entities:Fu(r),devices:Fu(o),config:Fu(e.hass?.config),locale:Fu(e.hass?.locale),themes:Fu(e.hass?.themes)});return Object.freeze({sourceSequence:e.sourceSequence,capturedAt:e.capturedAt??Date.now(),hass:a,devices:Fu([...e.devices]),positions:Iu([...e.positions||[]].map(([e,t])=>[e,Object.freeze({x:t.x,y:t.y})])),presentations:Iu([...e.presentations].map(([e,t])=>[e,Fu(t)])),facts:Iu([...e.facts||[]].map(([e,t])=>[e,Fu(t)]))})}const Nu=(e,t)=>`${e}:${t?1:0}`;function Hu(e){return"boolean"==typeof e?.themes?.darkMode?e.themes.darkMode?"theme-dark":"theme-light":""}function Lu(e){const t=[];if(1!==e.scale&&t.push(`--dev-scale:${e.scale}`),"none"!==e.pulse.kind){t.push(`--ripple-scale:${e.pulse.diameterScale}`);const r=(i=e.pulse.color,n=null,"string"==typeof i&&(Lt.test(i)||Bt.test(i))?i:n);r&&t.push(`--ripple-color:${r}`)}var i,n;return t}function ju(e){const t=[...String(e)].reduce((e,t)=>/\s/.test(t)?e+.35:e+(t.codePointAt(0)>255?1:.62),0);return t<=8?.45:Math.max(.25,Math.round(3.6/t*1e3)/1e3)}function qu(e,t){const i=e.pulse,n=i.generation%2==0,r=function(e){const t=e.valueBadge;if(!t||!1!==t.configured)return[];const i=[];return null!=e.tempText&&"temperature"!==t.tone&&i.push({kind:"temperature",text:e.tempText,suffix:"°"}),null!=e.humText&&"humidity"!==t.tone&&i.push({kind:"humidity",text:e.humText,suffix:"%"}),i}(e),o=e.valueBadge,s=o?.position||"right",a=!!o||r.length>0,l=["device-shell",null!=e.valueText?"text-shell":"",a?`with-values pos-${s}`:"",r.length?"with-legacy":""].filter(Boolean).join(" ");return B` + ${"none"!==i.kind&&"dot"!==i.reducedMotionIndicator?B``:G} + ${"dot"===i.reducedMotionIndicator?B``:G} + ${t.newDevice?B``:G} + ${e.haDisabled?B``:G} + + ${null!=e.lqiText?B`${e.lqiText}`:G} + `}function Bu(e){return!Number.isFinite(e)||e<=0?2.25:e/2.5*2.25}function Wu({openings:e,geometries:t,fillsByRoomId:i,idPrefix:n="data",groupClass:r="opening-tunnels",dataLayer:o="data"}){const s=e.map((e,r)=>{const o=t[r];if(!o)return G;const s=o.faces.find(e=>-1===e.side),a=o.faces.find(e=>1===e.side);if(!s||!a)return G;const l=i.get(s.roomId)||null,c=i.get(a.roomId)||null;if(!l&&!c)return G;const h=`${s.d} ${a.d}`,d=!!l&&!!c&&l.color===c.color&&l.opacity===c.opacity,u=`translate(${e.rx} ${e.ry}) rotate(${e.angle})`;if(d)return W``;const p=o.maxY-o.minY;if(!(p>0))return G;const _=`${(100*Math.max(0,Math.min(1,-o.minY/p))).toFixed(6)}%`,m=l||{color:"#000000",opacity:0},g=c||{color:"#000000",opacity:0},f=`hp-opening-tunnel-${n.replace(/[^a-zA-Z0-9_-]/g,"-")}-${r}`;return W``});return W``}function Uu(e,t,i){const n=i>0?i:1;return{a:[e[0]/n,e[1]/n],b:[t[0]/n,t[1]/n]}}function Gu(e,t){const i=t>0?t:1;return[e.a[0]*i,e.a[1]*i,e.b[0]*i,e.b[1]*i]}function Vu(e){return Array.isArray(e)&&e.length>=2&&Number.isFinite(Number(e[0]))&&Number.isFinite(Number(e[1]))}function Ku(e){if(!Array.isArray(e))return[];const t=[];for(const i of e){if(!i||"object"!=typeof i)continue;const e=i;if(!Vu(e.a)||!Vu(e.b))continue;const n=[Number(e.a[0]),Number(e.a[1])],r=[Number(e.b[0]),Number(e.b[1])];Math.hypot(r[0]-n[0],r[1]-n[1])<.001||t.push({a:n,b:r})}return t}function Yu(e,t,i,n,r=!0){const o=(e||[]).filter(e=>e?.id),s=Ku(t);if(s.length)return function(e,t,i,n){const r=Ku(e);if(!r.length)return[];const o=function(e,t){const i=[],n=(e||[]).filter(e=>e?.id);for(let e=0;e4*n||o>4*n)continue;const s=(e[0]-r)*u+(e[1]-l)*p,c=(e[2]-r)*u+(e[3]-l)*p,h=Math.max(0,Math.min(s,c)),m=Math.min(d,Math.max(s,c));if(m-he.lo-t.lo||e.hi-t.hi);const i=[];for(const e of t){const t=i[i.length-1];t&&e.lo<=t.hi+a?t.hi=Math.max(t.hi,e.hi):i.push({...e})}for(const t of i){const i=[r+u*t.lo,l+p*t.lo],n=[r+u*t.hi,l+p*t.hi];Math.hypot(n[0]-i[0],n[1]-i[1])i.pair===e&&Math.abs(i.ux*s-i.uy*o)<=1e-6&&Math.abs((t[0]-i.origin[0])*i.uy-(t[1]-i.origin[1])*i.ux)<=c&&Math.abs((t[2]-i.origin[0])*i.uy-(t[3]-i.origin[1])*i.ux)<=c);h||(h={pair:e,origin:[t[0],t[1]],ux:o,uy:s,ranges:[]},l.push(h));const d=(t[0]-h.origin[0])*h.ux+(t[1]-h.origin[1])*h.uy,u=(t[2]-h.origin[0])*h.ux+(t[3]-h.origin[1])*h.uy;h.ranges.push({lo:Math.min(d,u),hi:Math.max(d,u)})}const h=[];for(const e of l){e.ranges.sort((e,t)=>e.lo-t.lo||e.hi-t.hi);const t=[];for(const i of e.ranges){const e=t[t.length-1];e&&i.lo<=e.hi+a?e.hi=Math.max(e.hi,i.hi):t.push({...i})}for(const n of t){const t=[e.origin[0]+e.ux*n.lo,e.origin[1]+e.uy*n.lo],r=[e.origin[0]+e.ux*n.hi,e.origin[1]+e.uy*n.hi];Math.hypot(r[0]-t[0],r[1]-t[1])>=a&&h.push(Uu(t,r,i))}}return h}(s,e,i,n).map(e=>Gu(e,i));if(!r)return[];const a=[],l=(e,t)=>(e.open_to||[]).includes(t.id)||(t.open_to||[]).includes(e.id);for(let e=0;er)&&zo([o[0],o[1]],[o[2],o[3]],i))return!0;return!1}const Xu=e=>Array.isArray(e)&&e.length>=2&&Number.isFinite(Number(e[0]))&&Number.isFinite(Number(e[1])),Ju=(e,t,i)=>[Number(e[0])*i,Number(e[1])*i,Number(t[0])*i,Number(t[1])*i],Qu=e=>{const t=`${Number(e[0]).toFixed(9)},${Number(e[1]).toFixed(9)}`,i=`${Number(e[2]).toFixed(9)},${Number(e[3]).toFixed(9)}`;return t<=i?`${t}|${i}`:`${i}|${t}`},ep=e=>{const t=new Map;for(const i of e.flat())!Array.isArray(i)||i.length<4||!i.every(Number.isFinite)||Math.hypot(i[2]-i[0],i[3]-i[1])<=1e-9||t.set(Qu(i),[...i]);return[...t.values()]};function tp(e){return"solid"===e?.zero_wall_style?"solid":"dashed"}function ip(e,t=1,i){const n=[];for(const r of Array.isArray(e?.wall_segments)?e.wall_segments:[])0===Number(r?.cm)&&Xu(r?.a)&&Xu(r?.b)&&(i&&!i.has(String(r.id||""))||n.push(Ju(r.a,r.b,t)));return ep([n])}function np(e,t=1){const i=[];for(const n of Array.isArray(e?.partitions)?e.partitions:[])0===Number(n?.cm)&&Xu(n?.a)&&Xu(n?.b)&&i.push(Ju(n.a,n.b,t));for(const n of Array.isArray(e?.room_drafts)?e.room_drafts:[]){const e=Array.isArray(n?.points)?n.points:[];for(let r=0;r+1e?.host?.kind===t.kind&&e.host.id===t.id)}const ap=new WeakMap,lp=new WeakMap;function cp(e){const t=za(e.cfg),i={};for(const t of e.cfg.spaces||[])for(const e of t.rooms||[])e.area&&(i[e.area]=t.id);const n=e.cfg.settings?.exclude_integrations?new Set(e.cfg.settings.exclude_integrations):Re,r=De(e.cfg.settings?.icon_rules?.length?e.cfg.settings.icon_rules:Ce);return Wh({hass:e.hass,registry:e.registry,areaToSpace:i,markers:e.cfg.markers||[],settings:e.cfg.settings||{},excluded:n,showAll:!!e.cfg.settings?.show_all,firstSpaceId:t[0]?.id||"",loc:t=>Jd(e.lang,t),iconRules:r})}function hp(e){const t=za(e.cfg).find(t=>t.id===e.spaceId);if(!t)return null;const i=tn(e.cfg.spaces.find(t=>t.id===e.spaceId)),n=on(e.cfg.settings),r=e.iconSize??2.5,o=r>8?2.5:r,s=Bu(o),a=e.registry?Xc(e.hass,e.registry):e.hass,l=e.registry?Jc(e.hass,e.registry):e.hass,c=(e.devices||cp(e)).filter(t=>t.space===e.spaceId),h=c.filter(e=>!e.hidden),d=function(e,t,i){const n={},r=i/100*el(t)*1.3;for(const i of t.rooms){if(!i.area)continue;const t=e.filter(e=>e.area===i.area);if(!t.length)continue;const o=rl(i),s=.1*Math.min(o.w,o.h),a=o.w-2*s,l=o.h-2*s,c=Math.max(1,Math.round(Math.sqrt(t.length*a/Math.max(l,1)))),h=a/c,d=l/Math.max(Math.ceil(t.length/c),1),u=t.map((e,t)=>({x:o.x+s+h*(t%c+.5),y:o.y+s+d*(Math.floor(t/c)+.5)}));Ri(u,o,r,.5*s),t.forEach((e,t)=>n[e.id]=La(u[t]))}return n}(c,t,o),u=[];for(const t of h){const i=e.layout[t.id];if(i&&i.s===e.spaceId){const e=i.x*Ta,t=i.y*Ta;u.push({minX:e,minY:t,maxX:e,maxY:t})}}const p=e.cfg.spaces.find(t=>t.id===e.spaceId)||{},_=Array.isArray(p.walls)?p.walls:[],m=Number(p.cell_cm)>0?Number(p.cell_cm):5,g=op(p,t,Ta,.02*Ea),f=(p.openings||[]).flatMap(e=>{if("partition"!==e.host?.kind)return[];const i=Rd(e,t.partitions,Ta,m,Ea).resolved;return i?[i]:[]}),v=Tl({partitions:t.partitions,roomDrafts:t.room_drafts,columns:t.wall_columns,cellCm:m,hostedOpenings:f.map(e=>({id:e.opening.id,host:e.host,length:e.length,type:e.opening.type}))}),y=function(e,t,i,n){let r=lp.get(e);r||(r=new Map,lp.set(e,r));const o=r.get(t);if(o?.fingerprint===i)return o.value;const s=n();return r.set(t,{fingerprint:i,value:s}),s}(e.cfg,t.id,v,()=>ca(t,m,Ea,2e-4*Ea,f.map(Ad)).all);for(const e of y){const t=e.map(e=>e[0]),i=e.map(e=>e[1]);t.length&&u.push({minX:Math.min(...t),minY:Math.min(...i),maxX:Math.max(...t),maxY:Math.max(...i)})}for(const e of g.lines)u.push({minX:Math.min(e[0],e[2]),minY:Math.min(e[1],e[3]),maxX:Math.max(e[0],e[2]),maxY:Math.max(e[1],e[3])});const b=Ja(t,u),w=[b.x,b.y,b.w,b.h],k=new Map(t.rooms.map(t=>{const r=Tn(i.fill,t);return[t,an(r,"lqi"===r&&t.area?Kh(a,c,t.area):null,"light"===r?Rh(Mh(a,c,t,e.virtualLights)):"none","temp"===r&&t.area?Yh(a,c,t.area):null,i.tempMin,i.tempMax,n,en(i.customFill,t))]})),x=new Map;for(const e of t.rooms)e.id&&x.set(e.id,k.get(e)||null);const $=function(e,t=1e3){const i=[];for(const[n,r]of(e||[]).entries()){if("passage"!==r?.type)continue;const e={...r,id:String(r.id||`passage-${n}`),type:"passage",rx:Number(r.x)*t,ry:Number(r.y)*t,rlen:Number(r.length)*t};[e.rx,e.ry,e.rlen,e.angle].every(Number.isFinite)&&e.rlen>0&&i.push(e)}return i}((p.openings||[]).flatMap(e=>{if(!e.host||"wall"===e.host.kind)return[e];const t=f.find(t=>t.opening.id===e.id);return t?[Id(e,t,Ta)]:[]}),Ta),S=gs(t.rooms,_,g.contour,Na,m,Ea,Ta),M=f.filter(e=>Pd(e,S,2e-4*Ea)).map(e=>({x:e.center[0],y:e.center[1],angle:e.angle,length:e.length})),T=t.rooms.filter(e=>e.area||i.showBorders||"none"!==Tn(i.fill,e)).map(e=>{let n="room "+(t.bg?"overlay":"yard"),r="";const o=Tn(i.fill,e);if(i.showBorders||"none"!==o){n+=" styled";const t=[`--room-stroke:${i.color}`,`--room-stroke-op:${i.showBorders&&!g.contour.length?i.opacity:0}`],o=k.get(e)||null;o?(n+=" filled",t.push(`--room-fill:${o.color}`,`--room-fill-op:${o.opacity.toFixed(3)}`)):t.push("--room-fill:transparent","--room-fill-op:0"),r=t.join(";")}const s=e.id||G,a=e.area||G,l=e.poly?W``:W``;return l}),R=t.rooms.filter(e=>{const t=k.get(e);return Rn(i.glow,e)&&(!t||t.opacity<=0)}).map(e=>e.poly?W``:W``),C=new Map;for(const e of t.rooms)if(e.id){const t=k.get(e)||null;C.set(e.id,Rn(i.glow,e)&&(!t||t.opacity<=0)?{color:n.glow_base.c,opacity:n.glow_base.a,mode:"glow"}:null)}const D=Mh(a,h,null,e.virtualLights),A=h.map(n=>{const r=function(e,t,i,n,r){const o=t[e.id];return o&&o.s===e.space?{x:o.x*Ta,y:o.y*Ta}:n[e.id]?n[e.id]:La(Qa(r))}(n,e.layout,e.cfg,d,t),o=(r.x-w[0])/w[2]*100,s=(r.y-w[1])/w[3]*100,c=i.showLqi??!1!==e.showSignal,u=e.presentations?.get(Nu(n.id,c))||Pu(a,n,{liveStates:!1!==e.liveStates,showTemperature:!1!==e.showTemperature,showSignal:c,activityRuntime:e.activityRuntime?.get(n.id),sourceDetails:!1,lightDevices:h,lightSources:D,registryHass:l,reducedMotion:e.reducedMotion}),p=[`left:${o}%`,`top:${s}%`,...Lu(u)],_=xu(u),m=[n.name,Jd(e.lang,`marker.state_a11y_${_}`),"none"!==u.pulse.kind?Jd(e.lang,`marker.pulse_a11y_${u.pulse.reason}`):"",u.valueFullText||u.valueText||"",nu(u.valueBadge),null!=u.lqiText&&u.lqiBand?Jd(e.lang,`marker.lqi_a11y_${u.lqiBand}`,{value:u.lqiText}):""].filter(Boolean).join(", ");return B``}),O=i.showNames?t.rooms.filter(e=>e.name).map(n=>{const r=ol(n,t.id,e.layout,e.cfg),o=(r.x-w[0])/w[2]*100,s=(r.y-w[1])/w[3]*100,a=Math.min(1,i.opacity+.25);return B`
${n.name}
`}):[],z=t.bg?e.displayUrl?e.displayUrl(t.bg.href):t.bg.href:"",P=e.cfg.spaces.find(t=>t.id===e.spaceId)?.settings||{},F="daynight"===sr(e.cfg?.settings,P)?Yn(a,e.dayCycleNow??new Date):null,I=nn(e.cfg?.settings,i),E=!!(_.length||y.length&&i.showBorders),N=E?Tl($.length?{rooms:t.rooms,walls:_,extras:y,cellCm:m,zero:g.contour,passages:$.map(e=>({x:e.rx,y:e.ry,angle:e.angle,length:e.rlen})),hostedCompositeOpenings:M}:{rooms:t.rooms,walls:_,extras:y,cellCm:m,zero:g.contour}):"",H=E?function(e,t,i,n){let r=ap.get(e);r||(r=new Map,ap.set(e,r));const o=r.get(t);if(o?.fingerprint===i)return o.value;const s=n();return r.set(t,{fingerprint:i,value:s}),s}(e.cfg,t.id,N,()=>Hs(t.rooms,_,g.contour,[...$.filter(e=>"partition"!==e.host?.kind).map(e=>({x:e.rx,y:e.ry,angle:e.angle,length:e.rlen})),...M],Na,m,Ea,Ta,y)):null,L=$.length&&_.length?function(e,t,i,n,r,o,s,a=1){return[r,o,s,a].every(Number.isFinite)&&o>0&&a>0&&i?.length?Zs(Bs(e,i,n,r,o,s,a),t):t.map(()=>null)}(t.rooms,$.map(e=>({x:e.rx,y:e.ry,angle:e.angle,length:e.rlen})),_,g.contour,Na,m,Ea,Ta):$.map(()=>null),j=$.length?Wu({openings:$,geometries:L,fillsByRoomId:x,idPrefix:`${t.id}-static-data`,groupClass:"opening-tunnels static-opening-tunnels",dataLayer:"data"}):G,q=$.length?Wu({openings:$,geometries:L,fillsByRoomId:C,idPrefix:`${t.id}-static-glow`,groupClass:"opening-tunnels glow-base-tunnels static-opening-tunnels",dataLayer:"glow-base"}):G,U=_.length&&H?.paperD?[{path:H.paperD}]:Xt(t.rooms),V=i.showBorders?H:null,K=e.stageWidth&&w[2]?e.stageWidth/w[2]:1,Y=_o(m),Z=!!V&&(po(V.depthUnits,K)||mo(Y,K)),X=i.color||"#607d8b",J=i.hideOpenings?[]:f.map(e=>{const t=e.opening,i="passage"!==t.type&&t.contact?a.states?.[t.contact]?.state:null,n=ii(t.type,i,!!t.invert),r=n>0&&!!t.contact,o="gate"===t.type?!t.flip_v:!!t.flip_v,s={type:t.type,length:e.length,angle:e.angle,amount:n,flipH:!!t.flip_h,flipV:!!t.flip_v,base:X,tone:r?"var(--hp-open)":X,cellCm:m,gridPitch:Ea,face:Od(e,o)};return W` + ${tu(s)} + `});return B` +
+ ${dr(F)} + + ${V?W` + + + + `:G} + ${U.map(e=>"path"in e?W``:"poly"in e?W``:W``)} + ${z?W`e.assetLoaded?.(t.bg.href,z)} + transform=${t.bg.angle?`rotate(${t.bg.angle} ${t.bg.x+t.bg.w/2} ${t.bg.y+t.bg.h/2})`:G} + preserveAspectRatio="none" />`:G} + ${T} + ${i.showBorders&&g.contour.length?W``:G} + ${j} + ${R.length?W``:G} + ${q} + ${V?W` + ${V.paths.map(e=>W` + + `)} + `:G} + ${i.showBorders&&g.lines.length?W``:G} + ${J} + + ${""} +
${A}${O}
+
+ `}let dp=null,up=null,pp=0,_p=-1,mp=null,gp=[];const fp=new Set,vp=(e,t)=>{"function"==typeof e&&t.push(e)};function yp(){if(dp)return dp;try{const e=JSON.parse(localStorage.getItem("houseplan_card_cfg_v1")||"null");if(e&&e.config&&Array.isArray(e.config.spaces)){const t=e.layout||{};return{config:e.config,rev:e.rev||0,configFingerprint:e.config_fingerprint||Tl(e.config),layout:t,layoutRev:e.layout_rev||0,layoutFingerprint:e.layout_fingerprint||Tl(t),virtualLights:Cc(e.virtual_lights,e.rev||0)}}}catch{}return null}function bp(e,t=!1){if(t&&(dp=null,pp++,up))return up.catch(()=>null).then(()=>bp(e,!1));if(dp)return Promise.resolve(dp);if(up)return _p!==pp?up.catch(()=>null).then(()=>bp(e,!1)):up;return _p=pp,up=async function(e,t){const[i,n]=await Promise.all([e.callWS({type:"houseplan/config/get"}),e.callWS({type:"houseplan/layout/get"})]),r=i?.rev??0,o=dp?Dc(dp.virtualLights,i?.virtual_lights,r,!!i&&"virtual_lights"in i):Cc(i?.virtual_lights,r),s={config:i?.config??null,rev:r,configFingerprint:Tl(i?.config??null),layout:n?.layout??{},layoutRev:n?.rev??0,layoutFingerprint:Tl(n?.layout??{}),virtualLights:o};t===pp&&(dp=s);const a=e.connection;if(a?.subscribeEvents&&mp!==a){for(const e of gp)e();gp=[],mp=a;const e=()=>{dp=null,pp++,fp.forEach(e=>e())},t=[];try{if(vp(await a.subscribeEvents(e,"houseplan_config_updated"),t),vp(await a.subscribeEvents(e,"houseplan_layout_updated"),t),vp(await a.subscribeEvents(e=>{if(dp){const t=Fc(dp.virtualLights,e?.data);if(t===dp.virtualLights)return;dp={...dp,virtualLights:t}}else pp++;fp.forEach(e=>e())},"houseplan_virtual_light_updated"),t),mp===a)gp=t;else for(const e of t)e()}catch{for(const e of t)e();mp===a&&(mp=null)}}return s}(e,pp).finally(()=>{up=null,_p=-1}),up}const wp=3300;function kp(e,t){return{sources:e,last:Object.fromEntries(t.map(e=>[e.eid,e.state])),flashTs:0,flashKind:null,timer:0,gen:0,expiresAt:0,alarmActive:"alarm"===Sc(t).status}}function xp(e,t,i,n){n(e.timer);const r=kp(t,i);Object.assign(e,r)}function $p(e,t,i,n,r){e.flashTs&&i-e.flashTs"transition"===e.activity)&&(i(e.timer),e.flashTs=0,e.flashKind=null,e.expiresAt=0);if("alarm"===Sc(t).status){e.alarmActive||(i(e.timer),e.flashTs=0,e.flashKind=null,e.expiresAt=0);for(const i of t)e.last[i.eid]=i.state;return e.alarmActive=!0,null}if(e.alarmActive){for(const i of t)e.last[i.eid]=i.state;return e.alarmActive=!1,null}let n=null;for(const i of t){const t=Mc(e.last[i.eid],i);("event"===t||!n&&t)&&(n=t),e.last[i.eid]=i.state}return n}class Mp{constructor(e,t=()=>{}){this._host=e,this._onGateChange=t,this._modality="unknown",this._gate=!1,this._media=null,this._connected=!1,this._onMediaChange=()=>this._syncGate()}get modality(){return this._modality}get hoverEnabled(){return this._gate}connect(e=globalThis.window){this._connected||(this._connected=!0,this._modality="unknown",this._media="function"==typeof e?.matchMedia?e.matchMedia("(any-hover: hover) and (any-pointer: fine)"):null,this._media?.addEventListener?.("change",this._onMediaChange),this._setGate(!1))}disconnect(){this._media?.removeEventListener?.("change",this._onMediaChange),this._media=null,this._connected=!1,this._modality="unknown",this._setGate(!1)}note(e){return this._modality=function(e,t){const i="mouse"===(n=t.pointerType)||"touch"===n||"pen"===n?n:null;var n;return i?"mouse"===i&&t.sourceCapabilities?.firesTouchEvents?e:i:e}(this._modality,e),this._syncGate(),this._modality}suspend(){this._setGate(!1)}_syncGate(){var e,t;this._setGate((e=this._modality,t=!!this._media?.matches,"mouse"===e&&t))}_setGate(e){this._gate!==e&&(this._gate=e,this._host.toggleAttribute("data-pointer-hover",e),this._onGateChange(e))}}class Tp extends he{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!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}))}catch{this._spaces=[]}finally{this._spacesLoading=!1}}}get _lang(){return Xd(this.hass,this._config?.language)}get _schema(){const e=this._spaces||[];return[e.length?{name:"space",selector:{select:{mode:"dropdown",options:e}}}:{name:"space",selector:{text:{}}},{name:"title",selector:{text:{}}},{name:"show_button",selector:{boolean:{}}},{name:"button_label",selector:{text:{}}},{name:"button_target",selector:{text:{}}},{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:{}}}]}render(){if(!this.hass||!this._config)return G;this._loadSpaces();const e=this._lang,t={space:Jd(e,"editor.space"),title:Jd(e,"editor.title"),show_button:Jd(e,"editor.show_button"),button_label:Jd(e,"editor.button_label"),button_target:Jd(e,"editor.button_target"),icon_size:Jd(e,"editor.icon_size"),show_temperature:Jd(e,"editor.show_temperature"),live_states:Jd(e,"editor.live_states"),show_signal:Jd(e,"editor.show_signal")};return B`t[e.name]||e.name} + @value-changed=${this._valueChanged} + >`}_valueChanged(e){const t={...this._config||{},...e.detail.value};delete t.aspect_ratio;const i=new Event("config-changed",{bubbles:!0,composed:!0});i.detail={config:t},this.dispatchEvent(i)}}Tp.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-space-card-editor")||customElements.define("houseplan-space-card-editor",Tp);const Rp=e=>{history.pushState(null,"",e),((e,t,i)=>{const n=new Event(t,{bubbles:!0,composed:!0});n.detail=i??{},e.dispatchEvent(n)})(window,"location-changed",{replace:!1})};class Cp extends he{constructor(){super(...arguments),this._snap=null,this._loading=!1,this._reloadQueued=!1,this._forceReloadQueued=!1,this._reloadRetryTimer=0,this._dayCycleTimer=0,this._dayCycleClockKey="",this._stageWidth=0,this._pendingStageWidth=0,this._stageWidthRaf=0,this._haRegistryConnection=null,this._haRegistryRevision=-1,this._devices=[],this._continuity=this._newContinuityController(),this._continuityHistory=[],this._continuityEpoch=0,this._continuityDataReady=!0,this._continuityPaintToken=-1,this._continuityDisposed=!1,this._renderSnapshotAt=Date.now(),this._hassSequence=0,this._connHooked=null,this._connectionWasLost=!1,this._visibleDeviceSnapshot=null,this._candidateDeviceSnapshot=null,this._stagedDeviceSnapshotToken=-1,this._capturedSnapshotSequence=-1,this._capturedSnapshotDevices=null,this._capturedSnapshotActivity="",this._capturedSnapshotVirtual="",this._activityRuntime=new Map,this._reducedMotion=!1,this._pointerModality=new Mp(this),this._onMotionChange=e=>{this._reducedMotion=e.matches,this._capturedSnapshotSequence=-1,this.requestUpdate()},this._onHaRegistryUpdate=()=>{const e=Vc(this.hass).revision;e!==this._haRegistryRevision&&(this._haRegistryRevision=e,this._refreshDevices(),this._capturedSnapshotSequence=-1,this.requestUpdate())},this._dayCycleTick=()=>{if(!this.isConnected||"hidden"===this.ownerDocument.visibilityState)return;const e=this._dayCycleState();if(!e)return this._dayCycleTimer&&(window.clearInterval(this._dayCycleTimer),this._dayCycleTimer=0),void(this._dayCycleClockKey="");const t=Zn(e);t!==this._dayCycleClockKey&&(this._dayCycleClockKey=t,this.requestUpdate())},this._pageVisibility=e=>{if(this._continuity.visibility(e),this._dayCycleVisibility(e),"hidden"!==e.kind){if(!e.long){const e=Date.now();let t=!1;for(const i of this._activityRuntime.values())!i.flashKind||(i.expiresAt||i.flashTs+3300)>e||(i.flashTs=0,i.flashKind=null,i.expiresAt=0,t=!0);return void(t&&(this._capturedSnapshotSequence=-1,this.requestUpdate()))}Date.now()-this._renderSnapshotAt>1e3&&this._continuity.note("device-snapshot-stale"),this._continuityDataReady=!1,this._continuityPaintToken=-1,this._load(!0)}else this._pointerModality.suspend()},this._onConnLost=()=>{this._connectionWasLost=!0,this._continuityDataReady=!1,this._continuityPaintToken=-1,this._continuity.connectionLost()},this._onConnReady=()=>{!this._connectionWasLost&&this._continuity.hasCompleteFrame?this._beginContinuityCandidate("connection-ready",!1,"plan"):(this._continuityDataReady=!1,this._continuityPaintToken=-1),this._load(!0)},this._onAssetLoaded=(e,t)=>{this._signer.markLoaded(this.hass,e,t),this._continuity.note("asset-ready"),this._continuityPaintToken=-1,"steady"!==this._continuity.state&&this.requestUpdate()},this._loadedOnce=!1,this._signer=new Dl(()=>this.requestUpdate()),this._retryContinuity=()=>{this._continuityDataReady=!1,this._continuityPaintToken=-1,this._continuity.retry(this._continuity.recoveryReason||"plan"),this._load(!0)},this._goToSpace=()=>{const e=(this._config?.button_target||"/plan-doma").replace(/#.*$/,"");Rp(`${e}#space=${encodeURIComponent(this._config.space)}`)}}_ensureHaRegistryAuthority(){const e=this.hass?.connection||null;e&&e!==this._haRegistryConnection&&(this._haRegistryRelease?.(),this._haRegistryConnection=e,this._haRegistryRevision=-1,this._haRegistryRelease=Gc(this.hass,this._onHaRegistryUpdate),this._onHaRegistryUpdate())}_newContinuityController(){return new Ml(()=>{this._continuityEpoch++,this.isConnected&&this.requestUpdate()})}_dayCycleState(e=new Date){const t=this._snap?.config;if(!t||!this._config)return null;const i=t.spaces?.find(e=>e.id===this._config?.space)?.settings||{};return"daynight"!==sr(t.settings,i)?null:Yn(this._renderDeviceSnapshot?.hass||this.hass,e)}_syncDayCycleClock(){const e=this._dayCycleState();this._dayCycleClockKey=e?Zn(e):"";const t="clock"===e?.source&&"hidden"!==this.ownerDocument.visibilityState&&this.isConnected;t&&!this._dayCycleTimer?this._dayCycleTimer=window.setInterval(this._dayCycleTick,3e4):!t&&this._dayCycleTimer&&(window.clearInterval(this._dayCycleTimer),this._dayCycleTimer=0)}_dayCycleVisibility(e){"hidden"!==e.kind?(this._dayCycleTick(),this._syncDayCycleClock()):this._dayCycleTimer&&(window.clearInterval(this._dayCycleTimer),this._dayCycleTimer=0)}_hookConnection(){const e=this.hass?.connection;e&&e!==this._connHooked&&(this._connHooked?.removeEventListener?.("ready",this._onConnReady),this._connHooked?.removeEventListener?.("disconnected",this._onConnLost),this._connHooked?.removeEventListener?.("reconnect-error",this._onConnLost),e.addEventListener?.("ready",this._onConnReady),e.addEventListener?.("disconnected",this._onConnLost),e.addEventListener?.("reconnect-error",this._onConnLost),this._connHooked=e)}static getConfigElement(){return document.createElement("houseplan-space-card-editor")}static getStubConfig(e){const t=yp();return{type:"custom:houseplan-space-card",space:za(t?.config||null)[0]?.id||"",show_button:!0,live_states:!0,show_temperature:!0,show_signal:!0}}setConfig(e){if(!e||!e.space)throw new Error('houseplan-space-card: "space" is required');this._config={show_button:!0,button_target:"/plan-doma",live_states:!0,show_temperature:!0,show_signal:!0,...e},this._snap=this._snap||yp()}connectedCallback(){var e;this._continuityDisposed&&(this._continuity=this._newContinuityController(),this._continuityDisposed=!1,this._continuityPaintToken=-1),super.connectedCallback(),this._pointerModality.connect(this.ownerDocument.defaultView),this._motionMedia=window.matchMedia?.("(prefers-reduced-motion: reduce)"),this._reducedMotion=!!this._motionMedia?.matches,this._motionMedia?.addEventListener?.("change",this._onMotionChange),this.hass&&this._ensureHaRegistryAuthority(),this._continuityUnsub?.(),this._continuityUnsub=$l(this.ownerDocument,this._pageVisibility),this._unsub=(e=()=>{this._beginContinuityCandidate("config-event",!1),this._reloadQueued=!0,this._load()},fp.add(e),()=>fp.delete(e)),this._signer.start(()=>this.hass,()=>this._referenced())}disconnectedCallback(){this._pointerModality.disconnect(),this._continuityUnsub?.(),this._continuityUnsub=void 0,this._motionMedia?.removeEventListener?.("change",this._onMotionChange),this._motionMedia=void 0,this._unsub?.(),this._unsub=void 0,window.clearTimeout(this._reloadRetryTimer),this._reloadRetryTimer=0,this._dayCycleTimer&&(window.clearInterval(this._dayCycleTimer),this._dayCycleTimer=0),this._dayCycleClockKey="",this._stageObserver?.disconnect(),this._stageObserver=void 0,this._observedStage=void 0,this._stageWidthRaf&&cancelAnimationFrame(this._stageWidthRaf),this._stageWidthRaf=0,this._pendingStageWidth=0,this._signer.dispose(),this._haRegistryRelease?.(),this._haRegistryRelease=void 0,this._haRegistryConnection=null,this._connHooked?.removeEventListener?.("ready",this._onConnReady),this._connHooked?.removeEventListener?.("disconnected",this._onConnLost),this._connHooked?.removeEventListener?.("reconnect-error",this._onConnLost),this._connHooked=null;for(const e of this._activityRuntime.values())window.clearTimeout(e.timer);this._activityRuntime.clear(),this._continuityHistory=[...this._continuityHistory,...this._continuity.trace].slice(-80),this._continuity.dispose(),this._continuityDisposed=!0,super.disconnectedCallback()}willUpdate(e){e.has("hass")&&this.hass&&(this._hassSequence++,this._renderSnapshotAt=Date.now(),this._continuity.note("hass-snapshot"),this._ensureHaRegistryAuthority(),this._hookConnection()),!this.hass||this._loading||this._snap&&!e.has("hass")||this._snap&&this._loadedOnce||this._load(),(e.has("hass")||e.has("_snap")||e.has("_config")||!this._devices.length)&&this._refreshDevices(),this._captureRenderDeviceSnapshot(),this._continuity.hasCompleteFrame&&"steady"===this._continuity.state&&this._continuity.refreshCompleteFrame(this._frameFingerprint())}_stampActivity(e,t){$p(e,t,Date.now(),window.clearTimeout.bind(window),e=>window.setTimeout(()=>this.requestUpdate(),e))}_syncActivity(e,t,i=this.hass){if(!1===this._config?.live_states){for(const e of this._activityRuntime.values())window.clearTimeout(e.timer);return void this._activityRuntime.clear()}const n=new Set,r=Mh(t,e,null,this._snap?.virtualLights);for(const o of e){if(o.hidden)continue;if("icon_ripple"!==Ai(o.marker?.display))continue;n.add(o.id);const s=Cu(t,o,e,r,i),a=s.samples,l=Ou(t,o,s);let c=this._activityRuntime.get(o.id);if(!c||c.sources!==l){c&&window.clearTimeout(c.timer),c=kp(l,a),this._activityRuntime.set(o.id,c);continue}const h=Sp(c,a,window.clearTimeout.bind(window));h&&this._stampActivity(c,h)}for(const[e,t]of this._activityRuntime)n.has(e)||(window.clearTimeout(t.timer),this._activityRuntime.delete(e))}_refreshDevices(){if(!this.hass||!this._snap?.config||!this._config)return;const e=Vc(this.hass),t=cp({hass:this.hass,registry:e,cfg:this._snap.config,lang:this._lang});this._syncActivity(t,Xc(this.hass,e),this.hass),this._devices=t}_captureRenderDeviceSnapshot(){if(!this.hass)return;const e=Date.now(),t=[...this._activityRuntime.entries()].map(([t,i])=>`${t}:${i.gen}:${i.flashTs}:`+(i.flashKind&&(i.expiresAt||i.flashTs+3300)>e?1:0)).join("|"),i=this._snap?.virtualLights?`${this._snap.virtualLights.configRev}:${this._snap.virtualLights.rev}`:"";if(this._capturedSnapshotSequence===this._hassSequence&&this._capturedSnapshotDevices===this._devices&&this._capturedSnapshotActivity===t&&this._capturedSnapshotVirtual===i)return;const n=Xc(this.hass,Vc(this.hass)),r=new Map,o=new Set(["sun.sun"]),s=new Set,a=new Set,l=e=>{if(!e)return;const t=e.indexOf(":");if(t<0)return void o.add(e);const i=e.slice(0,t),n=e.slice(t+1);"device"===i?s.add(n):"entity"===i&&o.add(n)},c=za(this._snap?.config||null).find(e=>e.id===this._config?.space);for(const e of c?.rooms||[])e.area&&a.add(e.area),l(e.settings?.temp_source),l(e.settings?.hum_source);const h=this._snap?.config?.spaces?.find(e=>e.id===this._config?.space);for(const e of h?.openings||[])for(const t of ni(e))o.add(t);const d=Mh(n,this._devices,null,this._snap?.virtualLights);for(const e of this._devices)for(const t of[!1,!0])r.set(Nu(e.id,t),Pu(n,e,{liveStates:!1!==this._config?.live_states,showTemperature:!1!==this._config?.show_temperature,showSignal:t,activityRuntime:this._activityRuntime.get(e.id),sourceDetails:!1,lightDevices:this._devices,lightSources:d,registryHass:this.hass,reducedMotion:this._reducedMotion}));const u=Eu({sourceSequence:this._hassSequence,hass:n,devices:this._devices,presentations:r,entityIds:o,deviceIds:s,areaIds:a});this._capturedSnapshotSequence=this._hassSequence,this._capturedSnapshotDevices=this._devices,this._capturedSnapshotActivity=t,this._capturedSnapshotVirtual=i,this._visibleDeviceSnapshot&&"steady"!==this._continuity.state?this._candidateDeviceSnapshot=u:(this._visibleDeviceSnapshot=u,this._candidateDeviceSnapshot=null)}get _renderDeviceSnapshot(){return this._stagedDeviceSnapshotToken===this._continuity.token?this._candidateDeviceSnapshot||this._visibleDeviceSnapshot:this._visibleDeviceSnapshot||this._candidateDeviceSnapshot}_beginContinuityCandidate(e,t,i="plan"){return this._continuityDataReady=t,this._continuityPaintToken=-1,this._stagedDeviceSnapshotToken=-1,this._continuity.beginCandidate(e,i)}_backdropRaw(){return this._snap?.config&&this._config&&za(this._snap.config).find(e=>e.id===this._config.space)?.bg?.href||""}_candidateBackdrop(e){return e&&this._config&&za(e).find(e=>e.id===this._config.space)?.bg?.href||""}_assetsReady(){const e=this._backdropRaw();return!e||this._signer.isReady(this.hass,e)}_frameFingerprint(){const e=this._snap;return Rl([e?.rev||0,e?.configFingerprint||Tl(e?.config),e?.layoutRev||0,e?.layoutFingerprint||Tl(e?.layout),e?.virtualLights?`${e.virtualLights.configRev}:${e.virtualLights.rev}`:"",this._config?.space||"",this._stageWidth,this.hass?.themes?.darkMode??this.hass?.themes?.default_theme??""])}_stageValid(){const e=this._observedStage;return!!e&&e.clientWidth>0&&e.clientHeight>0}_settleContinuityFrame(){if(!this._stageValid())return;if(!this._continuity.hasCompleteFrame&&"steady"===this._continuity.state){if(this._assetsReady())return this._renderSnapshotAt=Date.now(),void this._continuity.markCompleteFrame(this._frameFingerprint());this._beginContinuityCandidate("asset-wait",!0,"asset")}if(!this._continuityDataReady)return;if(!["holding","offline-stale","overlay-pending","overlay-visible","candidate-ready"].includes(this._continuity.state))return;const e=this._continuity.token;if(this._candidateDeviceSnapshot&&this._candidateDeviceSnapshot!==this._visibleDeviceSnapshot&&this._stagedDeviceSnapshotToken!==e)return this._stagedDeviceSnapshotToken=e,void this.requestUpdate();this._continuityPaintToken!==e&&(this._continuityPaintToken=e,this._continuity.candidateReady(e)&&this._continuity.commitAfterPaint(e,{updateComplete:()=>this.updateComplete,stageValid:()=>this.isConnected&&this._stageValid(),assetsReady:()=>this._assetsReady(),frameFingerprint:()=>this._frameFingerprint()}).then(t=>{t&&e===this._continuity.token?(this._renderSnapshotAt=Date.now(),this._candidateDeviceSnapshot&&(this._visibleDeviceSnapshot=this._candidateDeviceSnapshot),this._candidateDeviceSnapshot=null,this._stagedDeviceSnapshotToken=-1):e===this._continuity.token&&(this._continuityPaintToken=-1,this._stagedDeviceSnapshotToken=-1,this._candidateDeviceSnapshot=null,this.requestUpdate())}))}updated(){const e=this.renderRoot.querySelector(".hp-static-stage")||void 0;if(e!==this._observedStage)if(this._stageObserver?.disconnect(),this._observedStage=e,e){const t=()=>{const i=e.clientWidth;if(!(i<=0||Math.abs(i-this._stageWidth)<=.5)){if(this._stageWidth<=0)return this._stageWidth=i,void this.requestUpdate();this._pendingStageWidth=i,this._stageWidthRaf||(this._stageWidthRaf=requestAnimationFrame(()=>{this._stageWidthRaf=0;const e=this._pendingStageWidth;this._pendingStageWidth=0,!e||!this._observedStage||this._observedStage.clientWidth<=0||(Math.abs(this._observedStage.clientWidth-e)>.5?t():Math.abs(e-this._stageWidth)<=.5||(this._continuity.hasCompleteFrame&&this._beginContinuityCandidate("stage-resize",!0,"stage-size"),this._stageWidth=e,this.requestUpdate()))}))}};this._stageObserver=new ResizeObserver(t),this._stageObserver.observe(e),t()}else this._stageObserver=void 0;this._syncDayCycleClock(),this._settleContinuityFrame()}async _load(e=!1){if(this.hass){if(this._loading)return this._reloadQueued=!0,void(this._forceReloadQueued||=e);this._loading=!0,this._reloadQueued=!1;try{const t=await bp(this.hass,e),i=!this._snap||this._snap.configFingerprint!==t.configFingerprint,n=!this._snap||this._snap.layoutFingerprint!==t.layoutFingerprint,r=!this._snap||this._snap.virtualLights!==t.virtualLights;if(i&&!await this._signer.prepareImage(this.hass,this._candidateBackdrop(t.config)))return this._continuity.note("asset-failed"),window.clearTimeout(this._reloadRetryTimer),void(this._reloadRetryTimer=window.setTimeout(()=>{this._load(!0)},1e3));window.clearTimeout(this._reloadRetryTimer),this._reloadRetryTimer=0,(i||n)&&this._continuity.hasCompleteFrame&&"steady"===this._continuity.state&&this._beginContinuityCandidate("structural-response",!0),i||n||r?this._snap=t:this._snap&&(this._snap.rev=t.rev,this._snap.layoutRev=t.layoutRev),i&&this._continuity.note("config-candidate",{configRev:t.rev}),n&&this._continuity.note("layout-candidate",{layoutRev:t.layoutRev}),r&&(this._capturedSnapshotSequence=-1),this._loadedOnce=!0,this._connectionWasLost=!1,this._continuityDataReady=!0,this._refreshDevices()}catch{}finally{if(this._loading=!1,this._continuityDataReady=!0,this.requestUpdate(),this._reloadQueued){const e=this._forceReloadQueued;this._reloadQueued=!1,this._forceReloadQueued=!1,this._load(e)}}}}get _lang(){return Xd(this.hass,this._config?.language)}getCardSize(){const e=za(this._snap?.config||null).find(e=>e.id===this._config?.space);if(e){const t=e.vb[3]/e.vb[2];return Math.max(3,Math.round(8*t))+(!1===this._config?.show_button?0:1)}return 6}_errorCard(e){return B` +
${e}
+
`}_referenced(){return Sn(this._snap?.config)}_renderRecoveryOverlay(){if(!this._continuity.overlayVisible&&"recovery-error"!==this._continuity.state)return G;const e="connection"===this._continuity.recoveryReason;return B`
+ + ${Jd(this._lang,e?"continuity.restore_connection":"continuity.restore_plan")} + ${"recovery-error"===this._continuity.state?B``:G} +
`}houseplanContinuityTrace(){return[...this._continuityHistory,...this._continuity.trace].slice(-80).map(e=>({...e,...e.stage?{stage:[...e.stage]}:{}}))}render(){if(!this._config)return G;const e=this._snap?.config;if(!e)return this._errorCard(Jd(this._lang,"space_card.loading"));const t=this._config.space,i=this._renderDeviceSnapshot,n=hp({hass:i?.hass||this.hass,cfg:e,layout:this._snap?.layout||{},spaceId:t,iconSize:this._config.icon_size,stageWidth:this._stageWidth,lang:this._lang,displayUrl:e=>this._signer.display(this.hass,e),assetLoaded:this._onAssetLoaded,registry:i?{revision:i.sourceSequence,authoritative:!1,access:"limited",entities:{...i.hass.entities},devices:{...i.hass.devices},lastSuccess:i.capturedAt}:Vc(this.hass),devices:[...i?.devices||this._devices],presentations:i?.presentations,activityRuntime:this._activityRuntime,reducedMotion:this._reducedMotion,virtualLights:this._snap?.virtualLights,liveStates:!1!==this._config.live_states,showTemperature:!1!==this._config.show_temperature,showSignal:!1!==this._config.show_signal,inert:this._continuity.overlayBlocksInteraction});if(!n)return this._errorCard(Jd(this._lang,"space_card.not_found",{id:t}));const r=za(e).find(e=>e.id===t),o=void 0!==this._config.title?this._config.title:r?.title||"",s=!1!==this._config.show_button,a=this._config.button_label||Jd(this._lang,"space_card.button"),l=this._continuity.overlayVisible||"recovery-error"===this._continuity.state?this._continuity.recoveryReason:null;return B` + this._pointerModality.note(e)} + @pointerdown=${e=>this._pointerModality.note(e)} + @pointermove=${e=>this._pointerModality.note(e)}> + ${o?B`
${o}
`:G} +
+ ${n} + ${this._renderRecoveryOverlay()} +
+ ${s?B`
+ +
`:G} +
+ `}}Cp.properties={hass:{attribute:!1},_config:{state:!0},_snap:{state:!0},_continuityEpoch:{state:!0}},Cp.styles=[Bd,o` + .hp-static-title { + font-weight: 700; + padding: 10px 14px 6px; + font-size: 16px; + color: var(--primary-text-color); + } + .hp-static-body { + position: relative; + } + .hp-static-stage { + position: relative; + width: 100%; + container-type: inline-size; + overflow: hidden; + pointer-events: none; /* kill ALL interaction on the schematic (§4) */ + background: var(--ha-card-background, var(--card-background-color, #111)); + } + .hp-static-stage svg { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + display: block; + z-index: 1; + } + /* Opaque plan paper — the scene bg_color/day-cycle environment shows only AROUND + the plan (owner 2026-08-03). The static card keeps its historical + canvas colour: the theme card background. Drawn plans paper the room + contours (per-room shapes, fill only, no stroke) — same contract as + the full card. */ + .hp-paper { + fill: var(--ha-card-background, var(--card-background-color, #111)); + stroke: none; + } + .wallbody-fill { + fill: var(--wall-fill, #ffffff); + fill-opacity: var(--wall-fill-op, 1); + fill-rule: evenodd; + stroke: none; + pointer-events: none; + } + .wallbody { + fill: url(#hp-wall-hatch); + fill-rule: evenodd; + stroke: var(--room-stroke, var(--hp-muted, #607d8b)); + stroke-width: calc(0.6px * var(--hp-cell-visual-scale, 1)); + pointer-events: none; + } + .wallbody.solid { + fill: none; + } + .hp-static-stage .devlayer { + position: absolute; + inset: 0; + z-index: 2; + } + .hp-static-foot { + padding: 8px 12px 12px; + pointer-events: auto; /* the button stays clickable */ + } + .hp-static-btn { + width: 100%; + padding: 9px 14px; + border: none; + border-radius: 10px; + background: var(--primary-color, #3ea6ff); + color: var(--text-primary-color, #fff); + font-size: 14px; + font-weight: 600; + cursor: pointer; + } + :host([data-pointer-hover]) .hp-static-btn:hover { + filter: brightness(1.08); + } + .hp-static-error { + padding: 24px; + text-align: center; + color: var(--secondary-text-color, #9aa4ad); + } + `],customElements.get("houseplan-space-card")||customElements.define("houseplan-space-card",Cp),window.customCards=window.customCards||[],window.customCards.find(e=>"houseplan-space-card"===e.type)||window.customCards.push({type:"houseplan-space-card",name:"House Plan — Space (static)",description:"Read-only live schematic of a single houseplan space, with a deep-link button.",preview:!1,documentation:"https://github.com/Matysh/houseplan-card"});const Dp=o` + :host { + --hp-editor-tray-bg-fallback: color-mix(in srgb, var(--card-background-color, var(--hp-bg)) 94%, var(--hp-line)); + --hp-editor-tray-bg: color-mix(in srgb, var(--card-background-color, var(--hp-bg)) 86%, transparent); + --hp-editor-tray-border: color-mix(in srgb, var(--hp-line) 82%, var(--hp-accent)); + --hp-editor-tray-blur: 8px; + --hp-editor-tray-shadow: var(--shadow-2); + } + + /* Contextual editor controls float over the stage. Their appearance must + never alter stage height, viewport fitting or the pinned close action. */ + .editor-secondary-host { + position: absolute; + inset: var(--sp-2) var(--sp-4) auto; + z-index: 70; + display: flex; + justify-content: flex-end; + pointer-events: none; + } + .editor-secondary-host.kind-palette, + .editor-secondary-host.kind-group { + justify-content: flex-start; + } + .editor-secondary { + pointer-events: none; + max-width: 100%; + display: flex; + align-items: center; + gap: var(--sp-3); + padding: var(--sp-3) var(--sp-4); + color: var(--hp-txt); + background: var(--card-background-color, var(--hp-bg)); + background: var(--hp-editor-tray-bg-fallback); + border: 1px solid var(--hp-line); + border-color: var(--hp-editor-tray-border); + border-radius: var(--rad-l); + box-shadow: var(--hp-editor-tray-shadow); + opacity: 0; + visibility: hidden; + transform: translateY(-4px); + transition: + opacity 0.1s ease, + transform 0.1s cubic-bezier(0.2, 0.7, 0.2, 1), + visibility 0s linear 0.1s; + touch-action: auto; + } + .editor-secondary-host.open .editor-secondary { + pointer-events: auto; + opacity: 1; + visibility: visible; + transform: translateY(0); + transition-duration: 0.14s, 0.14s, 0s; + transition-delay: 0s; + } + .editor-secondary-host.blocked .editor-secondary { pointer-events: none; } + @supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { + .editor-secondary { + background: var(--hp-editor-tray-bg); + -webkit-backdrop-filter: blur(var(--hp-editor-tray-blur)); + backdrop-filter: blur(var(--hp-editor-tray-blur)); + } + } + .editor-secondary-content, + .editor-group-items { + display: flex; + align-items: center; + gap: var(--sp-3); + min-width: 0; + } + .editor-context-label { + max-width: 14rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding-inline-end: var(--sp-4); + border-inline-end: 1px solid var(--hp-line); + color: var(--hp-muted); + font-size: var(--fs-s); + } + .editor-secondary.kind-palette { + width: min(760px, 100%); + align-items: stretch; + padding: 0; + overflow: hidden; + } + .editor-secondary.kind-palette .editor-secondary-content, + .editor-secondary.kind-palette .furnpalette { + width: 100%; + } + .editor-secondary.kind-palette .furnpalette { + max-height: 38vh; + border: 0; + background: transparent; + } + .editor-group-launcher .group-chevron { + --mdc-icon-size: 16px; + transition: transform 0.14s ease; + } + .editor-group-launcher[aria-expanded="true"] .group-chevron { transform: rotate(180deg); } + + .editor-secondary .bdhint { + font-size: var(--fs-s); + color: var(--hp-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .editor-secondary .dfill { + display: inline-flex; + align-items: center; + gap: var(--sp-2); + font-size: var(--fs-s); + cursor: pointer; + } + .editor-secondary .dfill input[type="checkbox"] { + width: 16px; + height: 16px; + flex: none; + margin: 0; + padding: 0; + } + .editor-secondary hp-color-opacity { flex: 0 0 auto; } + .editor-secondary input { + width: 74px; + background: transparent; + border: 1px solid var(--hp-line); + color: var(--hp-txt); + border-radius: var(--rad-s); + padding: var(--sp-3) var(--sp-4); + font-size: var(--fs-m); + } + .editor-secondary label, + .editor-secondary .hint { + color: var(--hp-muted); + font-size: var(--fs-s); + } + .editor-secondary .drawwall { + display: inline-flex; + align-items: center; + gap: var(--sp-3); + white-space: nowrap; + } + .editor-secondary .drawwall input { width: 4.2em; } + .editor-secondary .drawwall .opl { + color: var(--hp-muted); + font-size: var(--fs-s); + } + + @container (min-width: 560px) and (max-width: 899px) { + .editor-secondary-content, + .editor-group-items { + flex-wrap: wrap; + justify-content: flex-end; + } + } + @container (max-width: 559px) { + .editor-secondary-host { inset-inline: var(--sp-4); } + .editor-secondary { max-width: 100%; } + .editor-secondary-content, + .editor-group-items { + overflow-x: auto; + scrollbar-width: thin; + } + .editor-secondary-content > *, + .editor-group-items > * { flex: none; } + .editor-context-label { display: none; } + } + @media (prefers-reduced-motion: reduce) { + .editor-secondary { transition: none; } + .editor-group-launcher .group-chevron { transition: none; } + } +`;function Ap(e){return e instanceof Error?e.name||"Error":typeof e}function Op(e){return Tl({id:e?.id??"",cell_cm:e?.cell_cm,rooms:e?.rooms||[],walls:e?.walls||[],wall_segments:e?.wall_segments||[],open_spans:e?.open_spans||[],openings:e?.openings||[],partitions:e?.partitions||[],room_drafts:e?.room_drafts||[],wall_columns:e?.wall_columns||[]})}function zp(e,t,i={}){const n=(Array.isArray(e?.spaces)?e.spaces:[]).find(e=>String(e?.id||"")===String(t||"")),r=Op(n);if(!n)return{spaceId:String(t||""),displayName:"",status:"failed",reason:"prepare-exception",fingerprint:r,ok:!1};const o=Hp({...e,spaces:[n]},{...i,fingerprint:()=>r}).spaces[0]||{spaceId:String(t||""),displayName:"",status:"failed",reason:"prepare-exception"};return{...o,fingerprint:r,ok:"failed"!==o.status}}function Pp(e,t,i,n=Ea,r=1e3){return(Array.isArray(e?.openings)?e.openings:[]).flatMap(e=>{const o={...e,rx:Number(e.x)*r,ry:Number(e.y)*r,rlen:Number(e.length)*r};if(!e.host||"wall"===e.host.kind)return[o];const s=Rd(e,t.partitions,r,i,n);return s.resolved?[{...e,rx:s.resolved.center[0],ry:s.resolved.center[1],rlen:s.resolved.length,angle:s.resolved.angle,partitionHost:s.resolved}]:[]})}function Fp(e,t=()=>!0){return e.flatMap(e=>e.host&&e.partitionHost&&t(e)?[Ad(e.partitionHost)]:[])}function Ip(e,t,i,n,r,o,s=Ea,a=1e3){const l=gs(t.rooms,[...i],n,r,o,s,a);return e.flatMap(e=>{const t={x:e.rx,y:e.ry,angle:Number(e.angle)||0,length:e.rlen};return e.host&&"wall"!==e.host.kind?e.partitionHost&&Pd(e.partitionHost,l,2e-4*s)?[t]:[]:[t]})}function Ep(e,t){const i=Na,n=Ea,r=Ta,o=Number.isFinite(Number(t.cellCm))&&Number(t.cellCm)>0?Number(t.cellCm):5,s=Array.isArray(e?.walls)?e.walls:[],a=function(e,t,i=Ea,n=1e3){return Yu(t.rooms,e?.open_spans,n,.02*i)}(e,t,n,r),l=function(e,t,i=Ea){if(!t.length)return[];const n=e.filter(e=>e?.id),r=.02*i,o=[];for(let e=0;e{const t=[(e[0]+e[2])/2,(e[1]+e[3])/2];return h.some(e=>Pn(t,e)<4*r)}).map(e=>[...e]);d.length&&o.push({a:s,b:a,segs:d})}return o}(t.rooms,a,n).flatMap(e=>e.segs),c=Pp(e,t,o,n,r),h=Fp(c),d=ca(t,o,n,2e-4*n,h).all;return{space:t,walls:s,openCuts:l,openings:c,roomOpenings:Ip(c,t,s,l,i,o,n,r),partitionCuts:h,physicalBodies:d,wallKeyPitch:i,cellCm:o,gridPitch:n,coordScale:r}}function Np(e,t,i){const n="string"==typeof e?.title?e.title.trim():"",r=null==e?.id?"":String(e.id).trim();return{spaceId:r,displayName:n||r||i(t+1)}}function Hp(e,t={}){const i=(t.fingerprint||Tl)(e),n=Array.isArray(e?.spaces)?e.spaces:[],r=t.fallbackSpaceName||(e=>`Space ${e}`),o=t.prepareSpace||Ep,s=t.wallPass||Ns,a=t.floorPass||Ps,l=[];for(let i=0;i0||d.physicalBodies.length>0;if(!d.space.rooms.length&&!u){l.push({...h,status:"not-applicable"});continue}let p=null;if(u){try{p=s(d.space.rooms,d.walls,d.openCuts,d.roomOpenings,d.wallKeyPitch,d.cellCm,d.gridPitch,d.coordScale,d.physicalBodies)}catch(e){l.push({...h,status:"failed",reason:"wall-exception",detail:Ap(e)});continue}if(null==p){l.push({...h,status:"failed",reason:"wall-null"});continue}if("degraded-extra"===p.status){l.push({...h,status:"failed",reason:"wall-degraded-extra"});continue}if("failed-core"===p.status){l.push({...h,status:"failed",reason:"wall-failed-core"});continue}t.captureWallGeometry?.(d,p)}if(d.space.rooms.length&&null==p?.paperGeom){let e;try{e=a(d.space.rooms,d.walls,d.openCuts,d.wallKeyPitch,d.cellCm,d.gridPitch,d.coordScale)}catch(e){l.push({...h,status:"failed",reason:"floor-exception",detail:Ap(e)});continue}if(null==e){l.push({...h,status:"failed",reason:"floor-null"});continue}}l.push({...h,status:"ok"})}const c=l.filter(e=>"failed"===e.status);return{fingerprint:i,spaces:l,failures:c,ok:0===c.length}}function Lp(e,t){return e.catch(()=>{}).then(t)}class jp{constructor(e=50){this._undo=[],this._redo=[],this._limit=Math.max(30,Math.floor(e))}get canUndo(){return this._undo.length>0}get canRedo(){return this._redo.length>0}get undoName(){return this._undo[this._undo.length-1]?.name??null}get redoName(){return this._redo[this._redo.length-1]?.name??null}get size(){return this._undo.length}push(e){this._undo.push(e),this._undo.length>this._limit&&this._undo.splice(0,this._undo.length-this._limit),this._redo=[]}undo(){const e=this._undo.pop()??null;return e&&this._redo.push(e),e}redo(){const e=this._redo.pop()??null;return e&&this._undo.push(e),e}clear(){this._undo=[],this._redo=[]}}const qp=new WeakMap;const Bp=(e,t,i)=>255===e[i+3]&&t.every((t,n)=>Math.abs(e[i+n]-t)<=2);async function Wp(e){const t=e.defaultView;if(!t||!t.CSS?.supports?.("mix-blend-mode","screen"))return!1;const i=[128,32,16],n=[16,64,128],r=[9,19,29],o=`\n \n \n \n \n \n `,s=t.URL.createObjectURL(new Blob([o],{type:"image/svg+xml"}));try{const o=await new Promise((e,i)=>{const n=new t.Image,r=t.setTimeout(()=>i(new Error("SVG blend probe timeout")),1e3);n.onload=()=>{t.clearTimeout(r),e(n)},n.onerror=()=>{t.clearTimeout(r),i(new Error("SVG blend probe failed"))},n.src=s}),a=e.createElement("canvas");a.width=4,a.height=1;const l=a.getContext("2d",{willReadFrequently:!0});if(!l)return!1;l.drawImage(o,0,0,4,1);const c=l.getImageData(0,0,4,1).data;return Bp(c,i,0)&&Bp(c,function(e,t){return[0,1,2].map(i=>Math.round(255-(255-e[i])*(255-t[i])/255))}(i,n),4)&&Bp(c,n,8)&&Bp(c,r,12)}catch{return!1}finally{t.URL.revokeObjectURL(s)}}const Up={now:()=>performance.now(),requestFrame:e=>requestAnimationFrame(e),cancelFrame:e=>cancelAnimationFrame(e)},Gp=e=>Math.max(0,Math.min(1,e)),Vp=(e,t,i)=>e+(t-e)*i,Kp=(e,t,i)=>{const n=1-e;return 3*n*n*e*t+3*n*e*e*i+e*e*e};function Yp(e,t){return{centerX:e.x+e.w/2,centerY:e.y+e.h/2,pixelsPerUnit:t>0&&e.w>0?t/e.w:1,viewBox:{...e}}}function Zp(e,t,i){const n=Math.max(1e-9,e.pixelsPerUnit),r=Math.max(1e-9,t/n),o=Math.max(1e-9,i/n);return{x:e.centerX-r/2,y:e.centerY-o/2,w:r,h:o}}function Xp(e){const t=e.trim(),i=/^rgba?\(\s*([\d.]+)[, ]+([\d.]+)[, ]+([\d.]+)(?:\s*[,/]\s*([\d.]+))?\s*\)$/i.exec(t);if(i)return[Number(i[1]),Number(i[2]),Number(i[3]),null==i[4]?1:Number(i[4])];const n=/^#([0-9a-f]{6}|[0-9a-f]{8})$/i.exec(t)?.[1];return n?[parseInt(n.slice(0,2),16),parseInt(n.slice(2,4),16),parseInt(n.slice(4,6),16),8===n.length?parseInt(n.slice(6,8),16)/255:1]:null}function Jp(e,t,i){const n=Xp(e),r=Xp(t);if(!n||!r)return i<.5?e:t;const o=Gp(i);return`rgba(${Math.round(Vp(n[0],r[0],o))}, ${Math.round(Vp(n[1],r[1],o))}, ${Math.round(Vp(n[2],r[2],o))}, ${Vp(n[3],r[3],o).toFixed(3)})`}function Qp(e,t,i){const n=(e=>{const t=Gp(e);if(0===t||1===t)return t;let i=0,n=1,r=t;for(let e=0;e<12;e++)r=(i+n)/2,Kp(r,.2,.2){const t=this._state;if(!t||t.token!==r)return;const i=Gp((e-t.startedAt)/t.duration);t.presented=i>=1?t.to:Qp(t.from,t.to,i),t.phase=i>=1?"settling":"running",this._hooks.frame(t),i<1?this._raf=this._clock.requestFrame(s):(this._raf=0,this._hooks.settled(t),this._state?.token===r&&(this._state=null))};return this._raf=this._clock.requestFrame(s),r}cancel(e=!0){this._raf&&this._clock.cancelFrame(this._raf),this._raf=0;const t=this._state;this._state=null,this._token++,t&&e&&(t.phase="settling",t.presented=t.to,this._hooks.frame(t),this._hooks.settled(t))}dispose(){this.cancel(!1)}}class t_{constructor(e){this.options=e,this._state="idle",this._inFlight=null}get state(){return this._state}ensure(){return"ready"===this._state?Promise.resolve(!0):"failed"===this._state?Promise.resolve(!1):(this._inFlight||(this._setState("loading"),this._inFlight=this._loadWithRetry().finally(()=>{this._inFlight=null})),this._inFlight)}async _loadWithRetry(){let e=new Error("Editor runtime did not load");for(const t of[0,1])try{const e=await this.options.load(t);if(e.fingerprint!==this.options.expectedFingerprint)throw new Error(`Editor runtime fingerprint mismatch: expected ${this.options.expectedFingerprint}, got ${e.fingerprint}`);const i=e.create();return this.options.install(i),this._setState("ready"),!0}catch(t){e=t}return this._setState("failed"),this.options.failed?.(e),!1}_setState(e){e!==this._state&&(this._state=e,this.options.stateChanged?.(e))}}const i_="houseplan_card_labs_v1",n_=Object.freeze([Object.freeze({id:"iso",issue:89,since:"1.62.0",expires:"1.65.0",summary:"Volumetric plan renderer"})]);function r_(e){const t=/^(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?$/.exec(String(e||"").trim());if(!t)return null;const i=t.slice(1).map(Number);return i.every(e=>Number.isSafeInteger(e))?i:null}function o_(e,t){for(let i=0;i<3;i++)if(e[i]!==t[i])return e[i]e.split(",")).map(e=>e.trim()).filter(Boolean)}function d_(e,t,i,n=n_){const r=function(e,t=n_){const i=r_(e),n=new Map;if(!i||!a_(t))return n;for(const e of t){const t=r_(e.since),r=r_(e.expires);o_(i,t)>=0&&o_(i,r)<0&&n.set(e.id,e)}return n}(i,n),o=new Set(a_(n)?n.map(e=>e.id):[]),s=new Set(function(e){if(!e)return[];try{const t=JSON.parse(e);return Array.isArray(t)?t.filter(e=>"string"==typeof e):[]}catch{return[]}}(t).filter(e=>r.has(e)));let a=!1;const l=e=>{if("off"===e)return s.clear(),void(a=!0);const t=e.startsWith("-"),i=t?e.slice(1):e;o.has(i)&&(a=!0,t||!r.has(i)?s.delete(i):s.add(i))};for(const t of h_(new URLSearchParams(String(e.search||"").replace(/^\?/,""))))l(t);for(const t of h_(l_(e.hash)))l(t);const c=Object.freeze([...s].sort());return{active:c,space:c_(e.hash),persist:a?JSON.stringify(c):void 0,knownUrlOperation:a}}let u_={active:Object.freeze([]),space:""},p_="",__=!1,m_="";const g_=new Set;function f_(){if("undefined"==typeof window)return u_;const e=d_(window.location,function(){try{return window.localStorage.getItem(i_)}catch{return null}}(),p_);void 0!==e.persist&&function(e){try{window.localStorage.setItem(i_,e)}catch{}}(e.persist),u_={active:e.active,space:e.space},window.__hpLabs=e.active;for(const e of g_)e(u_);return u_}function v_(){f_()}function y_(e){return p_=e,"undefined"==typeof window?u_:(__||(__=!0,window.addEventListener("hashchange",v_),window.addEventListener("popstate",v_)),f_())}const b_=Object.freeze({rotDeg:0,tiltDeg:20,xyScale:1,zScale:1,origin:Object.freeze([500,500])});function w_(e){return[e.rotDeg,e.tiltDeg,e.xyScale,e.zScale,e.origin[0],e.origin[1]].every(Number.isFinite)&&Math.abs(e.xyScale)>1e-12&&Math.abs(Math.cos(e.tiltDeg*Math.PI/180))>1e-12}function k_(e,t,i=b_){if(!(w_(i)&&Number.isFinite(e[0])&&Number.isFinite(e[1])&&Number.isFinite(t)))throw new Error("invalid isometric projection input");const n=i.rotDeg*Math.PI/180,r=i.tiltDeg*Math.PI/180,o=e[0]-i.origin[0],s=e[1]-i.origin[1],a=(o*Math.cos(n)-s*Math.sin(n))*i.xyScale,l=(o*Math.sin(n)+s*Math.cos(n))*i.xyScale;return[i.origin[0]+a,i.origin[1]+l*Math.cos(r)-t*i.zScale*Math.sin(r)]}function x_(e,t=b_){if(!w_(t)||!Number.isFinite(e[0])||!Number.isFinite(e[1]))throw new Error("invalid isometric projection input");const i=t.rotDeg*Math.PI/180,n=t.tiltDeg*Math.PI/180,r=(e[0]-t.origin[0])/t.xyScale,o=(e[1]-t.origin[1])/(t.xyScale*Math.cos(n));return[t.origin[0]+r*Math.cos(i)+o*Math.sin(i),t.origin[1]-r*Math.sin(i)+o*Math.cos(i)]}function $_(e=b_){return`matrix(${function(e=b_){if(!w_(e))throw new Error("invalid isometric camera");const t=e.rotDeg*Math.PI/180,i=e.tiltDeg*Math.PI/180,n=e.xyScale*Math.cos(t),r=-e.xyScale*Math.sin(t),o=e.xyScale*Math.sin(t)*Math.cos(i),s=e.xyScale*Math.cos(t)*Math.cos(i);return[n,o,r,s,e.origin[0]-n*e.origin[0]-r*e.origin[1],e.origin[1]-o*e.origin[0]-s*e.origin[1]]}(e).map(e=>Number(e.toFixed(12))).join(" ")})`}function S_(e,t=b_){const{rect:i,wallHeight:n}=e,r=e.openingHeight??n,o=e.floorDepth??0;if(!(i.w>=0)||!(i.h>=0)||!Number.isFinite(n)||!Number.isFinite(r)||!Number.isFinite(o)||n<0||r<0||o<0)throw new Error("invalid isometric frame");const s=[[i.x,i.y],[i.x+i.w,i.y],[i.x+i.w,i.y+i.h],[i.x,i.y+i.h]],a=Math.max(n,r),l=s.flatMap(e=>[k_(e,-o,t),k_(e,a,t)]),c=l.map(e=>e[0]),h=l.map(e=>e[1]),d=Math.min(...c),u=Math.min(...h);return{x:d,y:u,w:Math.max(...c)-d,h:Math.max(...h)-u}}const M_=e=>`${Number(e[0].toFixed(4))} ${Number(e[1].toFixed(4))}`;function T_(e){let t=0;for(let i=0;iArray.isArray(e)&&Number.isFinite(e[0])&&Number.isFinite(e[1])).map(e=>[Number(e[0]),Number(e[1])]),i=t[t.length-1];return t.length>1&&t[0][0]===i[0]&&t[0][1]===i[1]&&t.pop(),t}(e);if(i.length<3)return[];const n=!t;return T_(i)>0===n?i:[...i].reverse()}function C_(e,t,i){return e.length?`M ${e.map(e=>M_(k_(e,i,t))).join(" L ")} Z`:""}function D_(e,t,i){const n=t[1]-e[1],r=-(t[0]-e[0]),o=i.rotDeg*Math.PI/180;return n*Math.sin(o)+r*Math.cos(o)}function A_(e,t,i=b_){if(!Number.isFinite(t)||t<0)throw new Error("invalid floor edge height");const n=(e||[]).map(e=>(e||[]).map((e,t)=>function(e,t){const i=R_(e,t);if(!i.length)return i;let n=0;for(let e=1;e0)).filter(e=>e.length>=3&&Math.abs(T_(e))>=1e-9)).filter(e=>e.length&&T_(e[0])>0);n.sort((e,t)=>{const i=e[0][0],n=t[0][0];return i[0]-n[0]||i[1]-n[1]||Math.abs(T_(t[0]))-Math.abs(T_(e[0]))});const r=[],o=[];let s=0;for(let e=0;ee.depth-t.depth||e.component-t.component||e.edge-t.edge),{footprintPath:r.join(" "),sides:o,componentCount:n.length,edgeCount:s}}function O_(e,t){return e^=Math.round(64*(Number.isFinite(t)?t:0)),Math.imul(e,16777619)>>>0}const z_=e=>e.every(Number.isFinite);function P_(e,t,i,n){const r=e[0]*i,o=e[1]*n,s=t*Math.PI/180;return[r*Math.cos(s)-o*Math.sin(s),r*Math.sin(s)+o*Math.cos(s)]}function F_(e,t,i,n,r,o,s){const a=e.flipH?-1:1,l="gate"===e.type?1:e.flipV?-1:1,c=(e.type,e.flipV,e.angle,e.face,{ox:0,oy:0}),h=function(e,t){return[e[0]+t[0],e[1]+t[1]]}([e.x+c.ox,e.y+c.oy],P_(i,e.angle,a,l));return{leaf:t,hinge:h,closedVector:P_(n,e.angle,a,l),quarterVector:P_([-n[1],n[0]],e.angle,a,l),turnDeg:r,bottom:o,top:s}}const I_=e=>`${Number(e[0].toFixed(4))} ${Number(e[1].toFixed(4))}`;function E_(e,t,i=b_){const n=Math.max(0,Math.min(1,Number.isFinite(t)?t:0));return e.leaves.map(t=>{const r=function(e,t){const i=e.turnDeg*t*Math.PI/180;return[e.hinge[0]+e.closedVector[0]*Math.cos(i)+e.quarterVector[0]*Math.sin(i),e.hinge[1]+e.closedVector[1]*Math.cos(i)+e.quarterVector[1]*Math.sin(i)]}(t,n),o=k_(t.hinge,t.bottom,i),s=k_(r,t.bottom,i),a=k_(r,t.top,i),l=k_(t.hinge,t.top,i);return{id:e.id,sourceIndex:e.sourceIndex,type:e.type,leaf:t.leaf,d:`M ${I_(o)} L ${I_(s)} L ${I_(a)} L ${I_(l)} Z`,shadowD:`M ${I_(k_(t.hinge,0,i))} L ${I_(k_(r,0,i))}`,depth:Math.max(o[1],s[1])}})}const N_={window:120,door:90,passage:90,gate:300};function H_(e){return N_[e]}function L_(e,t){return{type:e,lengthCm:H_(e),flipH:!1,flipV:!1,revision:t}}function j_(e,t){const i=Math.max(0,e.renderedLength)/2,n=Math.max(0,e.target.physicalHalfWidth),r=n+.18*Math.max(0,t),o=e=>({x1:e,y1:-r,x2:e,y2:r});return{rect:{x:-i,y:n?-n:0,width:2*i,height:2*n},boundaries:[o(-i),o(i)]}}function q_(e,t){const i=[e[0],e[1]],n=[t[0],t[1]];return function(e,t){return e[0]-t[0]||e[1]-t[1]}(i,n)<=0?{a:i,b:n}:{a:n,b:i}}function B_(e){const t=new Map;return e.forEach((e,i)=>{if(!e.kind||e.open)return;const n=q_(e.a,e.b);if(Math.hypot(n.b[0]-n.a[0],n.b[1]-n.a[1])<=1e-9)return;const r=function(e,t){const i=e=>(Math.abs(e)<=5e-10?0:Math.round(1e6*e)/1e6).toFixed(6);return`${i(e[0])},${i(e[1])}>${i(t[0])},${i(t[1])}`}(n.a,n.b),o=t.get(r);if(o)return o.physicalHalfWidth=Math.max(o.physicalHalfWidth,e.half||0),o.sourceOrder=Math.min(o.sourceOrder,i),void(e.partitionHost&&(o.partitionHost&&o.partitionHost.id!==e.partitionHost.id?o.ambiguousPartitionHost=!0:o.partitionHost=e.partitionHost));t.set(r,{segmentKey:r,a:n.a,b:n.b,physicalHalfWidth:Math.max(0,e.half||0),sourceOrder:i,...e.partitionHost?{partitionHost:e.partitionHost}:{}})}),[...t.values()]}function W_(e,t){if(Math.abs(e.distance-t.distance)>1e-9)return e.distance-t.distance;if(Math.abs(e.perpendicular-t.perpendicular)>1e-9)return e.perpendicular-t.perpendicular;return(e.target.segmentKeyt.target.segmentKey?1:0)||e.target.sourceOrder-t.target.sourceOrder}function U_(e){const t=B_(e.intervals).map(t=>{const i=function(e,t){const i=t.b[0]-t.a[0],n=t.b[1]-t.a[1],r=Math.hypot(i,n),o=i/r,s=n/r,a=(e[0]-t.a[0])*o+(e[1]-t.a[1])*s,l=Math.max(0,Math.min(r,a)),c=t.a[0]+l*o,h=t.a[1]+l*s;return{along:l,length:r,x:c,y:h,distance:Math.hypot(e[0]-c,e[1]-h),perpendicular:Math.abs((e[0]-t.a[0])*s-(e[1]-t.a[1])*o)}}(e.pointer,t);return{target:t,...i,envelope:Math.max(e.baseTolerance,t.physicalHalfWidth+e.bodyPointerPadding)}}).filter(e=>e.distance<=e.envelope+1e-9).filter(t=>!function(e,t,i,n,r){const o=t.b[0]-t.a[0],s=t.b[1]-t.a[1],a=Math.hypot(o,s);if(!(a>1e-9))return!1;const l=o/a,c=s/a;for(const o of i){if(!o.open&&o.kind)continue;const i=q_(o.a,o.b),s=i.b[0]-i.a[0],a=i.b[1]-i.a[1],h=Math.hypot(s,a);if(!(h>1e-9))continue;const d=s/h,u=a/h;if(Math.abs(l*u-c*d)>1e-6)continue;if(Math.abs((i.a[0]-t.a[0])*c-(i.a[1]-t.a[1])*l)>r)continue;const p=(e[0]-i.a[0])*d+(e[1]-i.a[1])*u,_=1e-9;if(!(p<=_||p>=h-_)&&Math.abs((e[0]-i.a[0])*u-(e[1]-i.a[1])*d)<=n+1e-9)return!0}return!1}(e.pointer,t.target,e.intervals,t.envelope,Math.max(1e-9,Math.min(e.baseTolerance,.04*e.gridStep)))),i=t.filter(t=>!!t.target.partitionHost&&e.renderedLength+2*t.target.physicalHalfWidth>t.length+1e-9).sort(W_),n=t.filter(t=>!t.target.partitionHost||e.renderedLength+2*t.target.physicalHalfWidth<=t.length+1e-9).sort(W_);let r=n[0];if(!r)return{candidate:null,jambBlockedTarget:i[0]?.target||null};const o=n.filter(e=>Math.abs(e.distance-r.distance)<=1e-9&&Math.abs(e.perpendicular-r.perpendicular)<=1e-9),s=o.filter(e=>e.target.partitionHost);if(s.length){if(1!==new Set(s.map(e=>e.target.partitionHost.id)).size||s.some(e=>e.target.ambiguousPartitionHost))return{candidate:null,jambBlockedTarget:null};const e=s[0],t=e.target.b[0]-e.target.a[0],i=e.target.b[1]-e.target.a[1],n=Math.hypot(t,i),a=t/n,l=i/n;if(!o.every(t=>{const i=t.target.b[0]-t.target.a[0],n=t.target.b[1]-t.target.a[1],r=Math.hypot(i,n),o=i/r,s=n/r,c=Math.abs(a*s-l*o)<=1e-6,h=Math.abs((t.target.a[0]-e.target.a[0])*l-(t.target.a[1]-e.target.a[1])*a);return c&&h<=1e-9}))return{candidate:null,jambBlockedTarget:null};r=e}if(r.target.ambiguousPartitionHost)return{candidate:null,jambBlockedTarget:null};const{target:a,length:l}=r,c=a.b[0]-a.a[0],h=a.b[1]-a.a[1],d=c/l,u=h/l,p=a.partitionHost?a.physicalHalfWidth:0,_=Math.min(Math.max(0,e.renderedLength)/2+p,l/2);let m=r.along;const g=Math.max(e.gridStep,1e-9),f=l/2;m=Math.abs(m-f)<=g/2?f:Math.round(m/g)*g,m=Math.max(_,Math.min(l-_,m));const v=a.a[0]+d*m,y=a.a[1]+u*m,b=Math.max(0,e.renderedLength)/2,w=m-b,k=m+b,x=Math.max(0,w),$=Math.max(0,l-k),S=[a.a[0]+d*(w-x/2),a.a[1]+u*(w-x/2)],M=[a.a[0]+d*(k+$/2),a.a[1]+u*(k+$/2)];let T=180*Math.atan2(h,c)/Math.PI;T>=90?T-=180:T<-90&&(T+=180);const R=Math.abs(m-f)<=1e-9;return{candidate:{presetRevision:e.preset.revision,geometryRevision:e.geometryRevision,pointer:[e.pointer[0],e.pointer[1]],type:e.preset.type,lengthCm:e.preset.lengthCm,flipH:e.preset.flipH,flipV:e.preset.flipV,x:v,y:y,angle:T,renderedLength:e.renderedLength,target:a,...a.partitionHost?{host:{...a.partitionHost,t:l>0?m/l:0}}:{},measure:{labels:[{distance:x,midpoint:S},{distance:$,midpoint:M}],guide:R?{x:v,y:y,angle:T}:null}},jambBlockedTarget:null}}function G_(e,t,i,n,r=1e-6){return e.presetRevision===i&&e.geometryRevision===n&&Math.hypot(e.pointer[0]-t[0],e.pointer[1]-t[1])<=r}function V_(e){return!(!e.canEdit||e.kiosk||e.fixedFloor)&&("view"!==e.mode&&("mouse"===e.pointerType&&e.spaceCount>1))}const K_="1.68.1",Y_="f111431804c4c42c6e35286f29720d089bc8b77ffc364a6517f7a3510fcb11c8",Z_=1500,X_=(e,t)=>{if(!e.has(t))return{hit:!1};const i=e.get(t);return e.delete(t),e.set(t,i),{hit:!0,value:i}},J_=(e,t,i,n)=>{for(e.delete(t),e.set(t,i);e.size>n;){const t=e.keys().next().value;if(void 0===t)break;e.delete(t)}},Q_=new Map;let em=0;const tm=e=>`${window.innerWidth}x${window.innerHeight}|${location.pathname}|${JSON.stringify(e??{})}`;let im=1e4;const nm="houseplan_card_layout_v1",rm="houseplan_card_cfg_v1",om="houseplan_card_zoom_v1",sm="houseplan_card_nav_v1",am="houseplan_card_view_v1",lm="hp-dialog, hp-help, hp-color-opacity, hp-device-preview",cm=1e3,hm=[[0,1],[45,.88],[70,.62],[86,.32],[100,0]],dm=(e,t)=>{const i=Math.min(e.x,t.x),n=Math.min(e.y,t.y);return{x:i,y:n,w:Math.max(e.x+e.w,t.x+t.w)-i,h:Math.max(e.y+e.h,t.y+t.h)-n}},um=new Set(["select","draw","column","merge","split","resize","opening","wallthick","delroom"]),pm=(e,t,i)=>{const n=new Event(t,{bubbles:!0,composed:!0});n.detail=i??{},e.dispatchEvent(n)},_m=e=>{history.pushState(null,"",e),pm(window,"location-changed",{replace:!1})},mm=(e,t)=>{let i,n=null;const r=(...r)=>{clearTimeout(i),n=r,i=window.setTimeout(()=>{i=void 0;const t=n;n=null,t&&e(...t)},t)};return r.flush=()=>{if(void 0===i)return;clearTimeout(i),i=void 0;const t=n;n=null,t&&e(...t)},r.cancel=()=>{clearTimeout(i),i=void 0,n=null},r.pending=()=>void 0!==i,r},gm=e=>{try{e.target?.setPointerCapture?.(e.pointerId)}catch{}};class fm extends he{constructor(){super(...arguments),this._editorRuntime=null,this._onboardingRuntime=null,this._editorRuntimeLoadingVisible=!1,this._editorModeRequest=0,this._editorRuntimeLoader=new t_({expectedFingerprint:Y_,load:async e=>{const t=0===e?await import("./houseplan-editor-runtime-DUuA4F8c.js"):await import((()=>{const e=new URL("./houseplan-editor-runtime-DUuA4F8c.js",import.meta.url);return e.searchParams.set("hp_retry",K_),e.href})());return{fingerprint:t.EDITOR_RUNTIME_FINGERPRINT,create:()=>new t.HouseplanEditorRuntime(this)}},install:e=>{this._editorRuntime=e},stateChanged:e=>this._editorRuntimeStateChanged(e),failed:e=>{console.error("[houseplan] unable to load editor runtime",e),this._showToast(`${this._t("editor.load_failed")} ${this._t("editor.refresh_advice")}`)}}),this._onboardingRuntimeLoader=new t_({expectedFingerprint:Y_,load:async e=>{const t=0===e?await import("./houseplan-onboarding-runtime-Bj55plb1.js"):await import((()=>{const e=new URL("./houseplan-onboarding-runtime-Bj55plb1.js",import.meta.url);return e.searchParams.set("hp_retry",K_),e.href})());return{fingerprint:t.ONBOARDING_RUNTIME_FINGERPRINT,create:()=>new t.HouseplanOnboardingRuntime(this)}},install:e=>{this._onboardingRuntime=e},failed:e=>{console.error("[houseplan] unable to load onboarding runtime",e),this._showToast(`${this._t("editor.load_failed")} ${this._t("editor.refresh_advice")}`)}}),this._space="f1",this._layout={},this._serverStorage=!1,this._loadOk=!1,this._serverCanWrite=null,this._loading=!1,this._loadTries=0,this._serverCfg=null,this._cfgRev=0,this._cfgContentFingerprint="",this._unsubCfg=null,this._unsubLayout=null,this._unsubVirtual=null,this._liveSyncAttempt=null,this._liveSyncGeneration=0,this._liveSyncConnection=null,this._layoutRev=0,this._layoutContentFingerprint="",this._virtualLights=Cc(null),this._canOptimizeUndo=!1,this._undoKind=null,this._devices=[],this._regSignature="",this._defPos={},this._newSyncKey="",this._tip=null,this._hoverRoom=null,this._pointerModality=new Mp(this,()=>this._syncPointerHoverTargets()),this._devicePressAnimations=new Map,this._selId=null,this._toast="",this._mode="view",this._pendingNavMode=null,this._decorTool="select",this._decorStyle={...ur},this._decorDraft=null,this._decorMove=null,this._decorSel=null,this._decorEraseConfirm=null,this._decorTextDialog=null,this._decorShapeDialog=null,this._backdropDialog=null,this._decorTextSelection={start:0,end:0},this._furnPalette=null,this._dtBox=null,this._dtDrag=null,this._bdDrag=null,this._slide="",this._reducedMotion=!1,this._onMotionChange=e=>{this._reducedMotion=e.matches,this._cancelDevicePressFeedback(),e.matches&&this._modeTransitionPreparing?this._modeTransitionForceAtomic=!0:e.matches&&this._modeTransition.active&&this._cancelModeTransition(!0),this.requestUpdate()},this._editorChromeMode="plan",this._modeTransitionVisual=null,this._modeTransitionPreparing=!1,this._modeTransitionForceAtomic=!1,this._modeTransitionRequest=0,this._modeTransitionTargetZoom=1,this._modeTransitionEditorCamera=null,this._modeTransition=new e_({frame:e=>this._applyModeTransitionFrame(e),settled:e=>this._settleModeTransition(e)}),this._editorSecondaryCopy={groupActive:(e,t)=>this._t("editor.group_active",{group:e,item:t}),openGroup:e=>this._t("editor.open_group",{group:e}),disabledAction:(e,t)=>this._t("editor.disabled_action",{action:e,reason:t})},this._tool="draw",this._geometryHistory=new jp(50),this._wallDialog=null,this._drawWallField=null,this._activeDraftId=null,this._resumeDraftBySpace={},this._physicalSel=null,this._physicalDialog=null,this._partitionDeleteDialog=null,this._roomDeleteDialog=null,this._physicalDrag=null,this._physicalRotate=null,this._physicalLastTap=null,this._physicalPickCycle=null,this._wallUnionCacheValue=null,this._wallUnionPool=new Map,this._isoGeometryCache=new Map,this._isoFallback=new Set,this._openingTunnelCache=null,this._openingWallIndexCache=new Map,this._openingPlacementIntervalsCache=null,this._openingDimensionContextCache=null,this._planSnapGeometryCache=null,this._planStructuralGeometryCache=null,this._hiddenWallDiagnosticCache=null,this._physicalBodiesCache=null,this._lightPhysicalBodiesCache=null,this._cleanFloorCache=new Map,this._innerContourCache=new Map,this._glowClipCache=new Map,this._glowGeometryWarnings=new Set,this._lightBarrierCache=null,this._lightBarrierPool=new Map,this._glowFeatherUnits=null,this._glowRenderedSources=new Map,this._glowLastAppearance=new Map,this._glowEnteringSources=new Set,this._glowEnterRafs=new Map,this._glowFadeTimers=new Map,this._glowFeatherSuspendUntil=0,this._glowFeatherResumeTimer=0,this._glowSourceSeq=0,this._glowScreenBlend=!1,this._duplicateColumnId=null,this._duplicateColumnTimer=0,this._rszLimitViolation=null,this._path=[],this._cursorPt=null,this._planSnapHover=null,this._mergeSel=null,this._openingPreset=null,this._openingRebindId=null,this._openingPresetRevision=0,this._openingHoverCandidate=null,this._openingJambBlockCm=null,this._openingDialog=null,this._openingInfo=null,this._opDrag=null,this._opMeasure=null,this._mergeDialog=null,this._splitSel=null,this._pendingSplit=null,this._wallFaceBatch=null,this._wallRepairDiagnostic=null,this._wallFaceGraphCache=[],this._areaSel="",this._nameSel="",this._roomDialog=!1,this._roomEditId=null,this._roomFill="",this._roomCustomFill=null,this._roomTempSrc="",this._roomHumSrc="",this._roomSrcOpen=null,this._roomSrcFilter="",this._roomNameScale=1,this._roomLabelScale=1,this._zoom=1,this._view=null,this._zoomBySpace={},this._viewModeSnap=null,this._pointers=new Map,this._panStart=null,this._panLock=null,this._pinchStart=null,this._suppressClick=!1,this._touchContacts=new Map,this._touchSequenceMultitouch=!1,this._touchClickBlockUntil=0,this._connectedPath="",this._routeDepartureHandled=!1,this._onLocationChanged=()=>{this._connectedPath&&location.pathname!==this._connectedPath?this._leaveCardRoute():location.pathname===this._connectedPath&&(this._routeDepartureHandled=!1)},this._touchGestureGuard={capture:!0,handleEvent:e=>this._guardTouchGesture(e)},this._hdrH=118,this._booting=!0,this._bootFading=!1,this._bootSettling=!1,this._bootSettleRaf=0,this._bootLastH=-1,this._bootStart=0,this._bootLastChange=0,this._bootSoft=!1,this._tapConfirm=null,this._onboardingShown=!1,this._rulesDialog=null,this._preflightClipboardFallback=null,this._haIntegrationVersion=null,this._alignDialog=null,this._settingsDialog=null,this._backupExportDialog=null,this._backupImportDialog=null,this._sunRaysCache=null,this._dayCycleTimer=0,this._dayCycleClockKey="",this._compassDrag=!1,this._importDialog=null,this._importQueue=[],this._importTotal=0,this._rulesCompiledSrc="",this._infoCard=null,this._nativeMoreInfoEntity=null,this._deviceInbox=null,this._deviceInboxReturn=null,this._deviceInboxMemo=null,this._markerDialog=null,this._spaceDialog=null,this._keyHandler=e=>this._onKey(e),this._warmVp=null,this._warmVpArmed=!1,this._warmLongReturn=!1,this._warmRevivePending=!1,this._warmGen=++em,this._warmKey=null,this._warmSlot=null,this._hashApplied=!1,this._navApplied=!1,this._labs={active:Object.freeze([]),space:""},this._viewPreference={},this._renderProjection="flat",this._kioskScale={icon:1,font:1},this._kioskDialog=!1,this._activityRt=new Map,this._vacRt=new Map,this._vacViewKey="",this._vacLastView=null,this._vacRaf=0,this._vacSrvTrails={},this._vacJumpOnce=!1,this._continuity=this._newContinuityController(),this._continuityHistory=[],this._continuityEpoch=0,this._continuityDataReady=!0,this._continuityPaintToken=-1,this._continuityDisposed=!1,this._renderSnapshotAt=Date.now(),this._hassSequence=0,this._visibleDeviceSnapshot=null,this._candidateDeviceSnapshot=null,this._stagedDeviceSnapshotToken=-1,this._capturedSnapshotSequence=-1,this._capturedSnapshotDevices=null,this._capturedSnapshotLayout=null,this._capturedSnapshotActivity="",this._capturedSnapshotConfigEpoch=-1,this._capturedSnapshotVirtual="",this._lastValidStageSize=null,this._pendingRefitSize=null,this._refitRaf=0,this._pageVisibility=e=>{if(this._continuity.visibility(e),this._dayCycleVisibility(e),"hidden"===e.kind)return this._clearTransientHover(!0),this._cancelDevicePressFeedback(),void(this._modeTransitionPreparing?this._modeTransitionForceAtomic=!0:this._cancelModeTransition(!0));if(this._vacJumpOnce=!0,!e.long){const e=Date.now();let t=!1;for(const i of this._activityRt.values())!i.flashKind||(i.expiresAt||i.flashTs+wp)>e||(i.flashTs=0,i.flashKind=null,i.expiresAt=0,t=!0);return void(t&&this.requestUpdate())}Date.now()-this._renderSnapshotAt>1e3&&this._continuity.note("device-snapshot-stale"),this._continuityDataReady=!1,this._continuityPaintToken=-1,this._resumeSettling=!0,this._loading?this.requestUpdate():this._loadFromServer()},this._resumeSettling=!1,this._viewportInvalidAt=0,this._vacFit=null,this._vacAllCamerasFor=null,this._vacAllCameraCache=null,this._vacCalConfirm=null,this._kioskDots=!1,this._cyclePausedUntil=0,this._swipeStart=null,this._tabDrag=null,this._tabDragRelease=null,this._tabSuppressClick=!1,this._tabOrderWarned=!1,this._lastTap=0,this._onLabsSnapshot=e=>{const t=this._effectiveProjection();this._labs=e;const i=this._effectiveProjection();this._convertProjectionView(t,i),this.requestUpdate()},this._onHashChange=()=>{if(this._hasFixedFloor)return;const e=this._hashSpace();if(e&&this._model.find(t=>t.id===e)&&e!==this._space){if(this._wallFaceBatch&&this._roomDialogCancel(),"plan"===this._mode&&"draw"===this._tool&&!this._finishWallChain())return;this._activeDraftId&&(this._resumeDraftBySpace[this._space]=this._activeDraftId),this._commitSpace(e),this._selId=null,this._path=[],this._clearPlanSnapHover(),this._clearOpeningPlacement(!0),this._tool="draw",this._activeDraftId=null,this._draftSegmentCms=[],this._closingWallCm=null,"plan"===this._mode&&"draw"===this._tool&&this._resumeLastDraft(),this._restoreZoom(),this.requestUpdate()}},this._drag=null,this._rlResize=null,this._holdFired=!1,this._retryContinuity=()=>{this._continuityDataReady=!1,this._continuityPaintToken=-1,this._continuity.retry(this._continuity.recoveryReason||"plan"),this._loading||this._loadFromServer()},this._cfgEpoch=0,this._modelCache=null,this._emptySpaceStateActive=!1,this._decorSnapCache=null,this._markerPreviewMemo=null,this._markerPreviewDevicesMemo=null,this._showHidden=!1,this._connHooked=null,this._connectionWasLost=!1,this._haRegistryConnection=null,this._haRegistryRev=-1,this._haBindingCacheKey="",this._planHassMemo=null,this._onHaRegistryUpdate=()=>{const e=Vc(this.hass);e.revision===this._haRegistryRev&&this._devices.length||(this._haRegistryRev=e.revision,this._planHassMemo=null,this._regSignature="",this._maybeRebuildDevices(),this.requestUpdate())},this._onConnReady=()=>{if(this._loadTries=0,clearTimeout(this._loadRetryTimer),this._loadRetryTimer=void 0,function(e){const t=jc(e);t&&(Wc(e,t),Uc(e,t))}(this.hass),this._saveConfigDebounced.pending()&&this._saveConfigDebounced.flush(),this._cfgWriting)return clearTimeout(this._reloadRetry),void(this._reloadRetry=window.setTimeout(this._onConnReady,400));!this._connectionWasLost&&this._continuity.hasCompleteFrame?this._beginContinuityCandidate("connection-ready",!1,"plan"):(this._continuityDataReady=!1,this._continuityPaintToken=-1),this._loading||this._loadFromServer()},this._onConnLost=()=>{this._booting&&!this._continuity.hasCompleteFrame||(this._connectionWasLost=!0,this._continuityDataReady=!1,this._continuityPaintToken=-1,this._continuity.connectionLost())},this._signer=new Dl(()=>this.requestUpdate()),this._dirtyPos=new Set,this._sentPos=new Map,this._persistLayout=mm(()=>{if(this._serverStorage){const e=[...this._dirtyPos];this._dirtyPos.clear();for(const t of e){const e=ro(this._layout[t]);e&&(this._layout={...this._layout,[t]:e},this._sentPos.set(t,e),this.hass.callWS({type:"houseplan/layout/update",device_id:t,pos:e}).then(e=>this._noteLayoutRev(e)).catch(e=>this._showToast(this._t("toast.pos_save_failed",{err:this._errText(e)}))).finally(()=>{this._sentPos.get(t)===e&&this._sentPos.delete(t)}))}this._cacheSnapshot()}else this._layout=oo(Ur(this._layout)),localStorage.setItem(nm,JSON.stringify(this._layout))},600),this._frame=null,this._showFar=!1,this._writesPending=0,this._writeChain=Promise.resolve(),this._pendingPhysicalWrites=new Map,this._undoGeometry=()=>this._editorRuntimeOrThrow()._undoGeometry(),this._redoGeometry=()=>this._editorRuntimeOrThrow()._redoGeometry(),this._saveConfigDebounced=mm(()=>{this._serverCfg&&this._writeConfig().catch(e=>{const t=!0===e?.physicalGeometryRolledBack;"geometry-unsafe"!==e?.code&&("wall_model_client_outdated"===e?.code?this._showToast(this._t("toast.wall_model_client_outdated")):"conflict"===e?.code?(this._showToast(this._t("toast.conflict")),t||(this._cancelPath(),this._reloadConfigOnly(!0))):this._showToast(this._t("toast.cfg_save_failed",{err:this._errText(e)})),t&&this._reloadRejectedPhysicalWrite())})},500),this._draftSegmentCms=[],this._closingWallCm=null,this._savePhysicalDialog=()=>this._editorRuntimeOrThrow()._savePhysicalDialog(),this._deletePhysicalSelection=()=>this._editorRuntimeOrThrow()._deletePhysicalSelection(),this._confirmPartitionDelete=()=>this._editorRuntimeOrThrow()._confirmPartitionDelete(),this._deleteDraftWhole=()=>this._editorRuntimeOrThrow()._deleteDraftWhole(),this._deleteDraftSegment=()=>this._editorRuntimeOrThrow()._deleteDraftSegment(),this._confirmRoomDelete=e=>this._editorRuntimeOrThrow()._confirmRoomDelete(e),this._rebindPartitionOpening=()=>this._editorRuntimeOrThrow()._rebindPartitionOpening(),this._keepClosedAsPartitions=()=>this._editorRuntimeOrThrow()._keepClosedAsPartitions(),this._toggleServerPlans=async()=>this._onboardingRuntime&&this._spaceDialogUsesOnboardingRuntime("create")?this._onboardingRuntime._toggleServerPlans():this._editorRuntimeOrThrow()._toggleServerPlans(),this._aspectJob=null,this._dayCycleTick=()=>{if(!this.isConnected||"hidden"===this.ownerDocument.visibilityState)return;const e=this._dayCycleState();if(!e)return this._dayCycleTimer&&(clearInterval(this._dayCycleTimer),this._dayCycleTimer=0),void(this._dayCycleClockKey="");const t=Zn(e);t!==this._dayCycleClockKey&&(this._dayCycleClockKey=t,this.requestUpdate())},this._sunShown=!1,this._sunOut=!1,this._sunOutTimer=0,this._openSettingsDialog=()=>{if(this._editorRuntime)return this._editorRuntime._openSettingsDialog();this._ensureEditorRuntime().then(e=>{e&&this._openSettingsDialog()})},this._reportedPreflightFingerprint=null,this._openAlignDialog=()=>this._editorRuntimeOrThrow()._openAlignDialog(),this._toggleOptimizeLivePositions=()=>this._editorRuntimeOrThrow()._toggleOptimizeLivePositions(),this._optimizeUndoBusy=!1,this._openBackupExport=()=>this._editorRuntimeOrThrow()._openBackupExport(),this._openRulesDialog=()=>this._editorRuntimeOrThrow()._openRulesDialog(),this._climateCache=null,this._gearPtCache=new WeakMap}_editorRuntimeOrThrow(){if(!this._editorRuntime)throw new Error("Houseplan editor runtime is not loaded");return this._editorRuntime}async _ensureEditorRuntime(){return this._editorRuntimeLoader.ensure()}async _ensureOnboardingRuntime(){return this._onboardingRuntimeLoader.ensure()}_editorRuntimeStateChanged(e){clearTimeout(this._editorRuntimeLoadingTimer),this._editorRuntimeLoadingTimer=void 0,"loading"!==e?(this._editorRuntimeLoadingVisible=!1,this.requestUpdate()):this._editorRuntimeLoadingTimer=window.setTimeout(()=>{this._editorRuntimeLoadingVisible=!0,this.requestUpdate()},150)}async _requestMode(e,t=!0){const i=++this._editorModeRequest;("view"===e||await this._ensureEditorRuntime())&&i===this._editorModeRequest&&this._setMode(e,t)}get _canEdit(){return!!this._norm&&(!0===this._serverCanWrite||!1!==this._serverCanWrite&&!0===this.hass?.user?.is_admin)}get _kiosk(){return!!this._config?.kiosk}_showKioskDots(){this._kioskDots=!0,clearTimeout(this._kioskDotsTimer),this._kioskDotsTimer=window.setTimeout(()=>this._kioskDots=!1,2500)}get _modeTransitionBusy(){return this._modeTransitionPreparing||this._modeTransition.active}_cssColor(e,t){const i=String(e||"").trim();if(!i)return t;const n=document.createElement("span");n.style.cssText=`position:absolute;visibility:hidden;color:${i}`,this.renderRoot.append(n);const r=getComputedStyle(n).color||t;return n.remove(),r}_currentModeVisual(e=this._mode){const t=this._modeTransition.presented||this._modeTransitionVisual;if(t)return{...t,viewport:{...t.viewport,viewBox:{...t.viewport.viewBox}}};const i=this._stageEl;if(!i||i.clientWidth<=0||i.clientHeight<=0)return null;const n=this.renderRoot.querySelector(".editorchrome"),r=this._viewOr(this._baseVb()),o=this.renderRoot.querySelector(".hp-paper"),s=this.renderRoot.querySelector(".hp-backdrop"),a=this.renderRoot.querySelector(".zoomwrap"),l=a?getComputedStyle(a).filter:"",c=Number(/brightness\(([^)]+)\)/.exec(l)?.[1]);return{presentedMode:e,editorChromeHeight:"view"===e?0:n?.getBoundingClientRect().height||0,stageWidth:i.clientWidth,stageHeight:i.clientHeight,viewport:Yp(r,i.clientWidth),stageColor:getComputedStyle(i).backgroundColor||"rgb(255, 255, 255)",paperColor:o?getComputedStyle(o).fill:"rgb(255, 255, 255)",sceneBrightness:Number.isFinite(c)?c:1,architectureOpacity:"decor"===e?.35:1,backdropOpacity:s&&Number(getComputedStyle(s).opacity)||1,viewWeight:"view"===e?1:0,editorWeight:"view"===e?0:1,toolbarContentOpacity:"view"===e?0:1}}_viewForModeTarget(e,t,i,n,r){const o=Ti(this._baseVb(),n/Math.max(1,r)),s=Math.min(fm.ZOOM_MAX,Math.max(fm.ZOOM_MIN,e)),a=o.w/s,l=o.h/s,c=t??o.x+o.w/2,h=i??o.y+o.h/2;return this._clampView({x:c-a/2,y:h-l/2,w:a,h:l},o)}_targetStageColor(e){return"view"!==e?"rgb(255, 255, 255)":this._cssColor(this._stageBg(this._spaceDisplayForRender()),this._cssColor("var(--ha-card-background, var(--card-background-color, #111))","rgb(17, 17, 17)"))}_targetPaperColor(e){return"view"===e&&this._spaceModel()?.bg?this._cssColor("var(--ha-card-background, var(--card-background-color, #111))","rgb(17, 17, 17)"):"rgb(255, 255, 255)"}_targetBrightness(e){return 1}_applyModeTransitionFrame(e){e.targetMode===this._mode&&(this._modeTransitionVisual=e.presented,this._view={...e.presented.viewport.viewBox},this.requestUpdate())}_settleModeTransition(e){if(e.targetMode!==this._mode)return;const t=this._modeTransitionRequest;this._view={...e.to.viewport.viewBox},this._zoom=this._modeTransitionTargetZoom,this._modeTransitionVisual=null,this._modeTransitionPreparing=!1,this._lastValidStageSize=[e.to.stageWidth,e.to.stageHeight],"view"===this._mode&&(this._saveZoom(),this._viewModeSnap=null,this._modeTransitionEditorCamera=null),this.requestUpdate(),this.updateComplete.then(()=>{if(!this.isConnected||t!==this._modeTransitionRequest||this._modeTransitionBusy||e.targetMode!==this._mode)return;const i=this._stageEl;if(i&&i.clientWidth>0&&i.clientHeight>0&&(Math.abs(i.clientWidth-e.to.stageWidth)>.5||Math.abs(i.clientHeight-e.to.stageHeight)>.5)){const e=this._view;this._lastValidStageSize=[i.clientWidth,i.clientHeight],this._applyView(this._zoom,e?e.x+e.w/2:void 0,e?e.y+e.h/2:void 0),this.requestUpdate()}const n=this.renderRoot.activeElement;(!n||!n.isConnected||!!n.closest?.(".editorchrome, .stage"))&&this.renderRoot.querySelector(".modetab.active")?.focus?.({preventScroll:!0})})}_cancelModeTransition(e=!0){const t=!!this._modeTransition.state;this._modeTransitionRequest++,this._modeTransitionPreparing=!1,this._modeTransitionForceAtomic=!1,this._modeTransition.cancel(e),e&&t||(this._modeTransitionVisual=null)}_adoptMode(e){this._cancelModeTransition(!1),this._mode=e,"view"!==e&&(this._editorChromeMode=e)}_commitViewModeAtomic(e,t,i,n){if(this._modeTransitionPreparing=!1,this._modeTransitionVisual=null,this._modeTransitionForceAtomic=!1,this._zoom=t,e){const r=this._stageEl?.clientWidth||e.stageWidth,o=Math.max(1,e.stageHeight+e.editorChromeHeight);this._view=this._viewForModeTarget(t,i,n,r,o),this._lastValidStageSize=[r,o]}else this._view=null;this._viewModeSnap=null,this._modeTransitionEditorCamera=null,this._saveZoom(),this.requestUpdate()}_prepareModeTransition(e,t,i,n,r,o){this.updateComplete.then(()=>{if(!this.isConnected||e!==this._modeTransitionRequest||this._mode!==i)return void(e===this._modeTransitionRequest&&(this._modeTransitionPreparing=!1,this._modeTransitionVisual=null,this._modeTransitionForceAtomic=!1,this.requestUpdate()));const s=this.renderRoot.querySelector(".editorchrome"),a=s?.querySelector(".editorchrome-inner"),l="view"===i?0:a?.scrollHeight||a?.getBoundingClientRect().height||0,c=Math.max(1,t.stageHeight+t.editorChromeHeight),h=Math.max(1,c-l),d=this._stageEl?.clientWidth||t.stageWidth;if(d<=0||h<=0)return this._modeTransitionPreparing=!1,this._modeTransitionVisual=null,this._applyView(n,r,o),void this.requestUpdate();const u=this._viewForModeTarget(n,r,o,d,h),p={presentedMode:i,editorChromeHeight:l,stageWidth:d,stageHeight:h,viewport:Yp(u,d),stageColor:this._targetStageColor(i),paperColor:this._targetPaperColor(i),sceneBrightness:this._targetBrightness(i),architectureOpacity:"decor"===i?.35:1,backdropOpacity:"decor"===i&&"backdrop"!==this._decorTool?.5:1,viewWeight:"view"===i?1:0,editorWeight:"view"===i?0:1,toolbarContentOpacity:"view"===i?0:1};this._modeTransitionPreparing=!1;const _=this._modeTransitionForceAtomic;this._modeTransitionForceAtomic=!1,this._modeTransition.start(t,p,i,this._reducedMotion||_?0:220)})}get _hasFixedFloor(){return!!this._config&&Object.prototype.hasOwnProperty.call(this._config,"floor")}_fixedFloorState(e=this._model,t=this._loadOk){const i=this._config?.floor,n=function(e){if(!e.hasFloor)return{kind:"absent"};const t=e.floor;if("string"==typeof t)return t.length?e.spaceIds.includes(t)?{kind:"valid",id:t,source:"id"}:{kind:"invalid",reason:"unknown-id",value:t}:{kind:"invalid",reason:"empty-id",value:t};if("number"==typeof t){if(!Number.isFinite(t))return{kind:"invalid",reason:"non-finite-index",value:t};if(!Number.isInteger(t))return{kind:"invalid",reason:"fractional-index",value:t};if(t<0)return{kind:"invalid",reason:"negative-index",value:t};const i=e.spaceIds[t];return void 0===i?{kind:"invalid",reason:"out-of-range-index",value:t}:{kind:"valid",id:i,source:"index"}}return{kind:"invalid",reason:"invalid-type",value:t}}({spaceIds:e.map(e=>e.id),hasFloor:this._hasFixedFloor,floor:i});return!this._hasFixedFloor||t?n:"number"==typeof i?"valid"===n.kind||"invalid"===n.kind&&"out-of-range-index"===n.reason?{kind:"pending",value:i}:n:"valid"===n.kind||"invalid"===n.kind&&"unknown-id"!==n.reason?n:{kind:"pending",value:i}}_canCommitSpace(e,t=!1){if(t||!this._hasFixedFloor)return!0;const i=this._fixedFloorState();return"valid"===i.kind&&i.id===e}_commitSpace(e,t=!1){return!!this._canCommitSpace(e,t)&&(e!==this._space&&(this._clearTransientHover(!0),this._cancelDevicePressFeedback()),this._space=e,!0)}_slideTo(e,t){if(e===this._space)return!0;if(!this._canCommitSpace(e))return!1;if(this._wallFaceBatch&&this._roomDialogCancel(),"plan"===this._mode&&"draw"===this._tool&&!this._finishWallChain())return!1;this._cancelModeTransition(!0);const i=window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches;return this._activeDraftId&&(this._resumeDraftBySpace[this._space]=this._activeDraftId),this._commitSpace(e),this._path=[],this._clearPlanSnapHover(),this._clearOpeningPlacement(!0),this._tool="draw",this._activeDraftId=null,this._draftSegmentCms=[],this._closingWallCm=null,this._selId=null,this._physicalSel=null,this._editorSecondary?.closeForNavigation(),this._physicalDialog=null,this._physicalDrag=null,"plan"===this._mode&&"draw"===this._tool&&this._resumeLastDraft(),this._restoreZoom(),i||(this._slide=t,clearTimeout(this._slideTimer),this._slideTimer=window.setTimeout(()=>{this._slideTimer=void 0,this._slide="",this.requestUpdate()},190),this.requestUpdate()),!0}get _canReorderTabs(){return V_({canEdit:this._canEdit,kiosk:this._kiosk,mode:this._mode,pointerType:"mouse",spaceCount:this._model.length,fixedFloor:this._hasFixedFloor})}_tabPointerDown(e,t){V_({canEdit:this._canEdit,kiosk:this._kiosk,mode:this._mode,pointerType:e.pointerType,spaceCount:this._model.length,fixedFloor:this._hasFixedFloor})&&(gm(e),this._tabDragRelease=e=>this._tabPointerUp(e),window.addEventListener("pointerup",this._tabDragRelease),window.addEventListener("pointercancel",this._tabDragRelease),this._tabDrag={id:t,pointerId:e.pointerId,x:e.clientX,y:e.clientY,moved:!1,targetId:null,placement:null})}_tabDropTargetAt(e,t,i){const n=this._model.map(e=>e.id),r=n.indexOf(i);if(r<0)return null;const o=this.renderRoot.querySelectorAll('[data-hp="space-tab"]');for(const s of o){const o=s.dataset.id||"";if(!o||o===i)continue;const a=s.getBoundingClientRect();if(ea.right||ta.bottom)continue;const l=n.indexOf(o);return l<0?null:{targetId:o,placement:l=4)))return;var i,n;const r=this._tabDropTargetAt(e.clientX,e.clientY,t.id);t.moved&&t.targetId===r?.targetId&&t.placement===r?.placement||(this._tabDrag={...t,moved:!0,targetId:r?.targetId||null,placement:r?.placement||null})}_tabPointerUp(e){const t=this._tabDrag;if(t&&t.pointerId!==e.pointerId)return;const i="pointerup"===e.type&&t?.moved?this._tabDropTargetAt(e.clientX,e.clientY,t.id):null;"pointerup"===e.type&&t?.moved&&this._suppressNextTabClick(),this._endTabDrag(),t?.moved&&i&&this._commitTabOrder(t.id,i.targetId)}_endTabDrag(){this._tabDrag=null,this._tabDragRelease&&(window.removeEventListener("pointerup",this._tabDragRelease),window.removeEventListener("pointercancel",this._tabDragRelease),this._tabDragRelease=null)}_tabClick(e){if(this._tabSuppressClick)return this._tabSuppressClick=!1,clearTimeout(this._tabSuppressClickTimer),void(this._tabSuppressClickTimer=void 0);this._pickSpace(e)}_suppressNextTabClick(){this._tabSuppressClick=!0,clearTimeout(this._tabSuppressClickTimer),this._tabSuppressClickTimer=window.setTimeout(()=>{this._tabSuppressClick=!1,this._tabSuppressClickTimer=void 0},0)}_commitTabOrder(e,t){const i=this._serverCfg;if(!i||!this._canReorderTabs)return;const n=this._model.map(e=>e.id),r=function(e,t,i){const n=e.indexOf(t),r=e.indexOf(i);if(n<0||r<0||n===r)return e;const o=e.slice();return o.splice(n,1),o.splice(r,0,t),o}(n,e,t);if(r===n)return;const o=new Map(this._devices.map(e=>[String(e.id),String(e.area||"")])),s=function(e,t,i,n=()=>""){if(!i)return[];const r=[];for(const o of e){if(!o||!0===o.removed)continue;const e="string"==typeof o.id?o.id:"";if(!e)continue;if("string"==typeof o.space&&o.space)continue;const s=("string"==typeof o.area?o.area:"")||n(e)||"";s&&t[s]||r.push({id:e,space:i})}return r}(i.markers||[],Object.fromEntries(Object.entries(this._areaToSpace).map(([e,t])=>[e,t.space])),n[0]||"",e=>o.get(e)||"");if(s.length){const e=new Map(s.map(e=>[e.id,e.space]));for(const t of i.markers||[]){const i=e.get(String(t.id));i&&(t.space=i)}}i.spaces=function(e,t){const i=new Map(t.map((e,t)=>[e,t]));return e.map((e,t)=>({space:e,index:t})).sort((e,t)=>(i.get(String(e.space?.id))??Number.MAX_SAFE_INTEGER)-(i.get(String(t.space?.id))??Number.MAX_SAFE_INTEGER)||e.index-t.index).map(e=>e.space)}(i.spaces||[],r),this._saveConfig(),this._tabOrderWarned||(this._tabOrderWarned=!0,this._showToast(this._t("toast.space_order_changed")))}_pickSpace(e){if(this._endTabDrag(),e===this._space)return;const t=this._model.map(e=>e.id),i=t.indexOf(this._space),n=t.indexOf(e);this._navApplied=!0,this._showFar=!1,this._frame=null,this._slideTo(e,i>=0&&n0&&Date.now()>=this._cyclePausedUntil&&this._model.length>1&&this._zoom<=1.001){const e=this._model.map(e=>e.id),t=e.indexOf(this._space);this._slideTo(e[(t+1)%e.length],"left"),this._showKioskDots()}}_syncCycleTimer(){clearInterval(this._cycleTimer),this._cycleTimer=void 0,this.isConnected&&!this._hasFixedFloor&&this._config?.kiosk&&Number(this._config.cycle)>0&&(this._cycleTimer=window.setInterval(()=>this._cycleTick(),1e3*Number(this._config.cycle)))}get _editing(){return"plan"===this._mode||"devices"===this._mode||"decor"===this._mode}get _markup(){return"plan"===this._mode}get _wallUnionCache(){return this._wallUnionCacheValue}set _wallUnionCache(e){this._wallUnionCacheValue=e,null===e&&this._wallUnionPool.clear()}_newContinuityController(){return new Ml(()=>{this._resumeSettling="steady"!==this._continuity.state,this._continuityEpoch++,this.isConnected&&this.requestUpdate()})}get _labsIso(){return this._labs.active.includes("iso")}get _desiredProjection(){return"view"===this._mode&&this._labsIso&&"iso"===this._viewPreference[this._space]?"iso":"flat"}_saveViewPreference(){try{localStorage.setItem(am,JSON.stringify(this._viewPreference))}catch{}}_logicalViewCenter(e){const t=this._view;if(!t)return null;const i=[t.x+t.w/2,t.y+t.h/2],n="iso"===e?x_(i):i;return{x:n[0],y:n[1]}}_convertProjectionView(e,t){if(e===t)return;const i=this._logicalViewCenter(e);this._view=null;const n=i?"iso"===t?k_([i.x,i.y],0):[i.x,i.y]:null;this._applyView(this._zoom,n?.[0],n?.[1]),this._warmPatch({vp:this._warmViewportState()}),this.requestUpdate()}_setProjection(e){if(!this._labsIso||"view"!==this._mode)return;const t=this._effectiveProjection();if(this._viewPreference={...this._viewPreference,[this._space]:e},"iso"===e){const e=this._isoSceneKey();e&&this._isoFallback.delete(e)}this._saveViewPreference();const i=this._effectiveProjection();this._convertProjectionView(t,i)}_hashSpace(){return c_(window.location.hash||"")}connectedCallback(){this._connectedPath=location.pathname,this._routeDepartureHandled=!1,window.addEventListener("location-changed",this._onLocationChanged),window.addEventListener("popstate",this._onLocationChanged),this._continuityDisposed&&(this._continuity=this._newContinuityController(),this._continuityDisposed=!1,this._continuityPaintToken=-1);const e=(t=this.ownerDocument,qp.get(t)?.resolved);var t;void 0!==e&&(this._glowScreenBlend=e),this._continuityUnsub?.(),this._continuityUnsub=$l(this.ownerDocument,this._pageVisibility),super.connectedCallback(),this._pointerModality.connect(this.ownerDocument.defaultView);const i=this.ownerDocument.defaultView?.MutationObserver;i&&(this._pointerHoverObserver=new i(e=>{for(const t of e)for(const e of t.addedNodes)this._syncPointerHoverSubtree(e)}),this._pointerHoverObserver.observe(this.renderRoot,{childList:!0,subtree:!0})),this._motionMedia=window.matchMedia?.("(prefers-reduced-motion: reduce)"),this._reducedMotion=!!this._motionMedia?.matches,this._motionMedia?.addEventListener?.("change",this._onMotionChange),function(e){const t=qp.get(e);if(t)return t.promise;const i={promise:Promise.resolve(!1)};return i.promise=Wp(e).catch(()=>!1).then(e=>(i.resolved=e,e)),qp.set(e,i),i.promise}(this.ownerDocument).then(e=>{e!==this._glowScreenBlend&&(this._glowScreenBlend=e,this.isConnected&&this.requestUpdate())}),this.hass&&this._ensureHaRegistryAuthority(),window.addEventListener("keydown",this._keyHandler),this._signer.start(()=>this.hass,()=>Sn(this._serverCfg)),this._syncCycleTimer(),window.addEventListener("hashchange",this._onHashChange),this._labsUnsub?.(),this._labsUnsub=function(e,t){g_.add(t);const i=y_(e);return"undefined"==typeof window&&t(i),()=>{g_.delete(t)}}(K_,this._onLabsSnapshot),this._booting?this._bootWatch():this._bootFading&&(clearTimeout(this._bootTimer),this._bootTimer=window.setTimeout(()=>{this._bootFading=!1},220)),this._bootSoft&&(clearTimeout(this._bootSoftTimer),this._bootSoftTimer=window.setTimeout(()=>{this._bootSoft=!1},Z_)),!this._loadOk&&this._serverCfg&&this.hass&&this._scheduleLoadRetry(),!this._warmSlot&&this._config&&this._warmAdopt(),this._loadOk&&this._ensureLiveSyncSubscriptions(),this._warmVp&&!this._warmRevivePending&&void 0===this._warmReviveTimer&&(this._warmRevivePending=!0,this._warmReviveTimer=window.setTimeout(()=>this._warmReviveDialog(),0)),this._warmLongReturn&&this._beginResumeSettle(),this._warmLongReturn=!1,this.requestUpdate()}disconnectedCallback(){this._connectedPath&&location.pathname!==this._connectedPath&&this._leaveCardRoute(),window.removeEventListener("location-changed",this._onLocationChanged),window.removeEventListener("popstate",this._onLocationChanged),this._continuityUnsub?.(),this._continuityUnsub=void 0,this._motionMedia?.removeEventListener?.("change",this._onMotionChange),this._motionMedia=void 0,this._vacRaf&&(cancelAnimationFrame(this._vacRaf),this._vacRaf=0),this._refitRaf&&(cancelAnimationFrame(this._refitRaf),this._refitRaf=0),this._dayCycleTimer&&(clearInterval(this._dayCycleTimer),this._dayCycleTimer=0),this._dayCycleClockKey="",this._bootSettleRaf&&(cancelAnimationFrame(this._bootSettleRaf),this._bootSettleRaf=0),this._bootSettling=!1;for(const e of this._activityRt.values())clearTimeout(e.timer);window.removeEventListener("keydown",this._keyHandler),this._endTabDrag(),clearTimeout(this._tabSuppressClickTimer),this._tabSuppressClickTimer=void 0,this._tabSuppressClick=!1,clearInterval(this._cycleTimer),clearTimeout(this._kioskDotsTimer),clearTimeout(this._kioskHoldTimer),clearTimeout(this._reloadRetry),clearTimeout(this._loadRetryTimer),this._loadRetryTimer=void 0,this._connHooked?.removeEventListener?.("ready",this._onConnReady),this._connHooked?.removeEventListener?.("disconnected",this._onConnLost),this._connHooked?.removeEventListener?.("reconnect-error",this._onConnLost),this._connHooked=null,this._haRegistryRelease?.(),this._haRegistryRelease=void 0,this._haRegistryConnection=null,this._signer.dispose(),clearTimeout(this._toastTimer),clearTimeout(this._slideTimer),clearTimeout(this._editorRuntimeLoadingTimer),this._editorRuntimeLoadingTimer=void 0,this._editorRuntimeLoadingVisible=!1,this._modeTransition.dispose(),this._modeTransitionVisual=null,this._modeTransitionPreparing=!1,this._modeTransitionForceAtomic=!1,this._modeTransitionRequest++,this._slideTimer=void 0,this._slide="",clearTimeout(this._bootTimer),this._bootTimer=void 0,clearTimeout(this._bootSoftTimer),this._saveConfigDebounced.flush(),window.removeEventListener("hashchange",this._onHashChange),this._labsUnsub?.(),this._labsUnsub=void 0,clearTimeout(this._holdTimer),this._roViewport?.disconnect(),this._roViewport=void 0,this._roHdr?.disconnect(),this._roHdr=void 0,this._onWinResize&&(window.removeEventListener("resize",this._onWinResize),this._onWinResize=void 0),this._unsubCfg&&(this._unsubCfg(),this._unsubCfg=null),this._unsubLayout&&(this._unsubLayout(),this._unsubLayout=null),this._unsubVirtual&&(this._unsubVirtual(),this._unsubVirtual=null),this._unsubTrail&&(this._unsubTrail(),this._unsubTrail=void 0),this._liveSyncGeneration++,this._liveSyncAttempt=null,this._liveSyncConnection=null,clearTimeout(this._layoutSyncTimer),clearTimeout(this._duplicateColumnTimer);for(const e of this._glowFadeTimers.values())clearTimeout(e);for(const e of this._glowEnterRafs.values())cancelAnimationFrame(e);clearTimeout(this._glowFeatherResumeTimer),this._glowFeatherResumeTimer=0,this._glowFeatherSuspendUntil=0,this._glowFadeTimers.clear(),this._glowEnterRafs.clear(),this._glowEnteringSources.clear(),this._glowRenderedSources.clear(),this._glowLastAppearance.clear(),this._glowSourceSeq=0,this._warmSnapshot(),this._warmRevivePending=!1,clearTimeout(this._warmReviveTimer),this._warmReviveTimer=void 0,this._warmRelease(),this._clearPlanSnapHover(),this._clearOpeningPlacement(!0),this._touchContacts.clear(),this._touchSequenceMultitouch=!1,this._touchClickBlockUntil=0,this._clearTransientHover(!0),this._cancelDevicePressFeedback(),this._pointerHoverObserver?.disconnect(),this._pointerHoverObserver=void 0,this._pointerModality.disconnect(),this._editorSecondary?.reset(),this._resumeSettling=!1,this._continuityHistory=[...this._continuityHistory,...this._continuity.trace].slice(-80),this._continuity.dispose(),this._continuityDisposed=!0,super.disconnectedCallback()}_onKey(e){if("Escape"===e.key&&this._vacFit)return this._vacFit=null,this._showToast(this._t("vac.cal_cancelled")),void e.stopPropagation();if("Escape"===e.key){if(this._tapConfirm)return void(this._tapConfirm=null);if(this._vacCalConfirm)return void(this._vacCalConfirm=null);if(this._decorEraseConfirm)return void(this._decorEraseConfirm=null);if(this._openingInfo)return void(this._openingInfo=null);if(this._infoCard)return void this._closeInfoCard();if(this._rulesDialog)return void(this._rulesDialog=null);if(this._alignDialog)return this._alignDialog=null,void(this._preflightClipboardFallback=null);if(this._backupImportDialog)return void(this._backupImportDialog=null);if(this._backupExportDialog)return void(this._backupExportDialog=null);if(this._settingsDialog)return void(this._settingsDialog=null);if(this._markerDialog)return void this._closeMarkerDialog();if(this._deviceInbox)return void(this._deviceInbox=null);if(this._openingDialog)return void(this._openingDialog=null);if(this._physicalDialog)return void(this._physicalDialog=null);if(this._backdropDialog)return void(this._backdropDialog=null);if(this._decorShapeDialog)return void(this._decorShapeDialog=null);if(this._decorTextDialog)return void(this._decorTextDialog=null);if(this._spaceDialog&&!this._roomDialog)return this._spaceDialog=null,this._importQueue=[],void(this._importTotal=0);if(this._editorSecondary?.hasOpenGroup)return e.preventDefault(),void this._editorSecondary?.closeGroup(!0)}const t=e.composedPath?.()||[e.target],i=t.some(e=>e?.matches?.('input, textarea, select, [contenteditable="true"]')),n=t.some(e=>e?.classList?.contains?.("editor-secondary")),r=e.ctrlKey||e.metaKey,o=e.key.toLowerCase(),s=/^[a-z]$/.test(o),a="z"===o||!s&&"KeyZ"===e.code,l="y"===o||!s&&"KeyY"===e.code,c=r&&(a&&e.shiftKey||l),h=r&&a&&!e.shiftKey;if("decor"===this._mode){if((h||c)&&i)return;return c?(e.preventDefault(),void this._redoGeometry()):h?(e.preventDefault(),this._decorDraft?void(this._decorDraft=null):this._decorMove||this._dtDrag||this._bdDrag?void this._cancelDecorGesture():void this._undoGeometry()):"Delete"!==e.key&&"Backspace"!==e.key||!this._decorSel||i||n?void("Escape"===e.key&&(e.preventDefault(),this._decorDraft?this._decorDraft=null:this._decorMove||this._dtDrag||this._bdDrag?this._cancelDecorGesture():"furniture"===this._decorTool?(this._furnPalette=null,this._decorTool="select"):this._decorSel?this._decorSel=null:"select"!==this._decorTool?this._decorTool="select":this._setMode("view"))):(e.preventDefault(),void this._decorDeleteSel())}if(this._markup&&(!h&&!c||!i)){if(("Delete"===e.key||"Backspace"===e.key)&&this._physicalSel&&!i&&!n)return e.preventDefault(),void this._deletePhysicalSelection();if(c)return e.preventDefault(),void this._redoGeometry();if(h)return e.preventDefault(),this._resize?.dragging?void this._rszCancelDrag():this._wallFaceBatch?(this._roomDialogCancel(),void(this._activeDraftId&&this._path.length>1?this._undoActiveDraftPoint():this._undoPoint())):"draw"===this._tool&&this._path.length?void(this._activeDraftId&&this._path.length>1?this._undoActiveDraftPoint():this._undoPoint()):"split"===this._tool&&this._splitSel?.pts?.length?(this._splitSel={...this._splitSel,pts:this._splitSel.pts.slice(0,-1)},void(this._splitSel.pts.length||(this._cursorPt=null))):void this._undoGeometry();if("Escape"===e.key)return this._physicalDrag||this._physicalRotate?(e.preventDefault(),void this._cancelPhysicalGesture()):this._roomDialog?(e.preventDefault(),void this._roomDialogCancel()):"draw"===this._tool&&this._path.length?(e.preventDefault(),void this._finishWallChain()):this._physicalSel?(e.preventDefault(),void(this._physicalSel=null)):"resize"===this._tool?(e.preventDefault(),this._resize?.dragging?void this._rszCancelDrag():("exit-tool"===this._resize?.escapeIdle()&&(this._tool="draw"),void this.requestUpdate())):"split"===this._tool?(e.preventDefault(),void(this._splitSel?.pts?.length?(this._splitSel={...this._splitSel,pts:this._splitSel.pts.slice(0,-1)},this._splitSel.pts.length||(this._cursorPt=null)):this._splitSel?this._splitSel=null:this._tool="draw")):"merge"===this._tool?(e.preventDefault(),void(this._mergeSel?this._mergeSel=null:this._tool="draw")):this._wallDialog?(e.preventDefault(),void(this._wallDialog=null)):void("opening"!==this._tool&&"wallthick"!==this._tool&&"delroom"!==this._tool&&"column"!==this._tool||(e.preventDefault(),"opening"===this._tool&&this._clearOpeningPlacement(!0),this._tool="draw"))}}_undoPoint(){if(this._path.length){if(this._contourClosed)return this._path=this._path.slice(0,-1),void(this._closingWallCm=null);if(this._activeDraftId&&this._path.length>1&&this._curSpaceCfg){const e=this._geometrySnapshot();this._path=this._path.slice(0,-1),this._draftSegmentCms=this._draftSegmentCms.slice(0,-1);const t=this._curSpaceCfg,i=(t.room_drafts||[]).findIndex(e=>e.id===this._activeDraftId);if(i>=0){const n=Array.isArray(t.room_drafts[i]?.segments)?t.room_drafts[i].segments:[];this._path.length<2?(t.room_drafts.splice(i,1),t.room_drafts.length||delete t.room_drafts,this._activeDraftId=null):t.room_drafts[i]={id:this._activeDraftId,points:this._path.map(e=>[e[0]/cm,e[1]/cm]),segments:this._draftSegmentCms.map((e,t)=>({...n[t]||{},cm:e}))},this._commitPhysicalGeometry(this._t("history.draft_segment_delete"),e)}return}this._path=this._path.slice(0,-1)}}_undoActiveDraftPoint(){const e=this._activeDraftId,t=this._path[0]?[...this._path[0]]:null,i=this._geometryHistory.undo();if(!i)return void this._undoPoint();if(!this._applyGeometryState(i.before,!0))return void this._geometryHistory.clear();const n=e?this._spaceModel()?.room_drafts.find(t=>t.id===e):null;n?(this._activeDraftId=n.id,this._path=n.points.map(e=>[...e]),this._draftSegmentCms=this._adoptDraftCms(this._path,n.segments.map(e=>e.cm),n.id),this._resumeDraftBySpace[this._space]=n.id):(this._activeDraftId=null,this._path=t?[t]:[],this._draftSegmentCms=[],e&&this._resumeDraftBySpace[this._space]===e&&delete this._resumeDraftBySpace[this._space]),this._clearPlanSnapHover(),this._showToast(this._t("history.undone",{name:i.name}))}static async getConfigElement(){return await import("./editor-BC5jpMNv.js"),document.createElement("houseplan-card-editor")}static getStubConfig(){return{type:"custom:houseplan-card"}}static _warmBootReset(e){for(const e of Q_.values())for(const t of e)clearTimeout(t.evict);Q_.clear(),im=e&&e>0?e:1e4}static _warmBootStats(){let e=0,t=0;const i=[];for(const n of Q_.values())for(const r of n)e++,r.dlg&&(t++,i.push(r.dlg.kind));return{keys:Q_.size,slots:e,dlgs:t,drafts:i}}setConfig(e){const t=this._config,i=!!t&&Object.prototype.hasOwnProperty.call(t,"floor");this._config={icon_size:2.5,show_temperature:!0,live_states:!0,show_signal:!0,...e};(i!==this._hasFixedFloor||t?.floor!==this._config.floor)&&(this._hashApplied=!1,this._navApplied=!1,this._warmVpArmed=!1),this._config.kiosk&&(this._booting=!1,this._bootFading=!1),!this._hasFixedFloor&&e.default_floor&&this._commitSpace(e.default_floor,!0);try{this._zoomBySpace=JSON.parse(localStorage.getItem(om)||"{}")||{}}catch{this._zoomBySpace={}}try{const e=JSON.parse(localStorage.getItem(am)||"{}")||{};this._viewPreference=Object.fromEntries(Object.entries(e).filter(e=>"flat"===e[1]||"iso"===e[1]))}catch{this._viewPreference={}}this._labs=y_(K_);try{const e=JSON.parse(localStorage.getItem("houseplan_card_kiosk_v1")||"null");this._kioskScale={icon:Dn(e?.icon),font:Dn(e?.font)}}catch{}try{const e=JSON.parse(localStorage.getItem(rm)||"null");e&&e.config&&Array.isArray(e.config.spaces)&&(this._serverCfg=e.config,this._cfgEpoch++,this._cfgRev=e.rev||0,this._cfgContentFingerprint=e.config_fingerprint||Tl(e.config),this._layout=e.layout||{},this._layoutRev=e.layout_rev||0,this._layoutContentFingerprint=e.layout_fingerprint||Tl(this._layout),this._virtualLights=Cc(e.virtual_lights,this._cfgRev),this._serverStorage=!0)}catch{}this._adoptInitialSpace(this._model,this._loadOk),"view"!==this._mode||this._view||(this._zoom=this._zoomBySpace[this._space]||1),this.isConnected&&(this._syncCycleTimer(),this._warmAdopt(),this._warmLongReturn&&this._beginResumeSettle(),this._warmLongReturn=!1)}_warmAdopt(){if(this._config?.kiosk)return;const e=tm(this._config);if(this._warmKey===e&&this._warmSlot)return;this._warmSlot&&this._warmRelease();const t=this.parentNode,i=this._warmIdx(t),n=Q_.get(e);if(!n||!n.length)return;const r=n.find(e=>e.owner===this._warmGen);if(r)return this._warmLongReturn=!!r.freed&&Date.now()-r.freed>=wl,clearTimeout(r.evict),r.evict=0,r.freed=0,r.live=!0,this._warmSlot=r,this._warmKey=e,r.frameFingerprint&&this._continuity.adoptCompleteFrame(r.frameFingerprint),void(!this._devices.length&&r.devices?.length&&(this._devices=[...r.devices]));const{slot:o,sure:s}=((e,t,i,n)=>{const r=e=>{const t=!!i&&e.place?.deref()===i;return t&&e.idx===n?4:e.live?0:t?3:2};let o=null,s=0,a=0,l=null;for(const i of e){if(i.owner===t)continue;l=i;const e=r(i);e<=0||(e>s?(o=i,s=e,a=1):e===s&&a++)}return!o||a>1?{slot:o||l,sure:!1}:{slot:o,sure:!0}})(n,this._warmGen,t,i);o&&(this._warmLongReturn=!!o.freed&&Date.now()-o.freed>=wl,this._booting=!1,this._bootFading=!1,this._hdrH=o.hdrH,this._bootSoft=!0,this.isConnected&&(clearTimeout(this._bootSoftTimer),this._bootSoftTimer=window.setTimeout(()=>{this._bootSoft=!1},Z_)),this._warmKey=e,s?(clearTimeout(o.evict),o.evict=0,o.owner=this._warmGen,o.place=t?new WeakRef(t):null,o.idx=i,o.live=!0,this._warmSlot=o,this._warmVp=o.vp,o.frameFingerprint&&this._continuity.adoptCompleteFrame(o.frameFingerprint),!this._devices.length&&o.devices?.length&&(this._devices=[...o.devices]),this._warmAdoptViewport(this._config)):(this._warmSlot={owner:this._warmGen,path:location.pathname,place:t?new WeakRef(t):null,idx:i,live:!0,hdrH:o.hdrH,stageH:o.stageH,vp:null,frameFingerprint:"",devices:null,dlg:null,freed:0,evict:0},n.push(this._warmSlot),this._warmTrim(n)))}_warmIdx(e){const t=e?.children;if(!t)return-1;for(let e=0;e4;){const t=e.findIndex(e=>!e.live);if(t<0)break;clearTimeout(e[t].evict),e.splice(t,1)}}_warmScheduleEvict(e,t){if(clearTimeout(e.evict),!(e.dlg||e.vp&&"view"!==e.vp.mode))return;const i=e.freed,n=e.owner;e.evict=window.setTimeout(()=>{if(e.evict=0,e.freed!==i||e.owner!==n)return;var r;e.dlg=null,e.vp=(r=e.vp)&&"view"!==r.mode?{...r,mode:"view",zoom:r.snap?.space===r.space?r.snap.zoom:r.zoom,view:null,snap:null,tool:"draw",decorTool:"select",showHidden:!1,selId:null,rszSel:null,decorSel:null}:r,e.frameFingerprint="";const o=Q_.get(t);if(!e.live&&o&&o.length>1){const t=o.indexOf(e);t>=0&&o.splice(t,1)}},im+250)}_warmAdoptViewport(e){const t=this._warmVp;if(!t||this._warmSlot?.path!==location.pathname)return void(this._warmVp=null);const i=this._fixedFloorState();if(this._hashApplied||!this._model.find(e=>e.id===t.space)||"valid"===i.kind&&i.id!==t.space||this._hasFixedFloor&&"valid"!==i.kind)return void(this._warmVp=null);this._commitSpace(t.space,!0),this._navApplied=!0,this._adoptMode("view"!==t.mode&&this._canEdit&&!e.kiosk?t.mode:"view"),this._pendingNavMode="view"===t.mode||this._canEdit||e.kiosk?null:t.mode,this._zoom=t.zoom;const n=this._effectiveProjection(),r=n===t.projection&&this._labsIso===t.activeLabsIso;if(this._view=r&&t.view?{...t.view}:null,this._viewModeSnap=r&&t.snap?{...t.snap}:null,!r&&t.logicalCenter){const e="iso"===n?k_([t.logicalCenter.x,t.logicalCenter.y],0):[t.logicalCenter.x,t.logicalCenter.y];this._applyView(t.zoom,e[0],e[1])}var o;this._tool="opening"===(o=function(e){return"partition"===e?"draw":e}(o=t.tool))?"draw":"string"==typeof o&&um.has(o)?o:"draw",this._decorTool=t.decorTool,this._showHidden=t.showHidden,this._showFar!==t.showFar&&(this._showFar=t.showFar,this._frame=null),this._selId=t.selId,this._resize?.restoreSelection(t.rszSel),this._decorSel=t.decorSel,this._warmVpArmed=!0}_warmPatch(e,t=!1){if(this._config?.kiosk)return;const i=tm(this._config);if(!this._warmSlot||this._warmKey===i){if(!this._warmSlot){if(!t)return;const e=this.parentNode;this._warmKey=i,this._warmSlot={owner:this._warmGen,path:location.pathname,place:e?new WeakRef(e):null,idx:this._warmIdx(e),live:!0,hdrH:this._hdrH,stageH:0,vp:null,frameFingerprint:"",devices:null,dlg:null,freed:0,evict:0};const n=Q_.get(i)||[];for(n.push(this._warmSlot),Q_.set(i,n),this._warmTrim(n);Q_.size>8;){const e=Q_.keys().next().value;if(void 0===e||e===i)break;for(const t of Q_.get(e)||[])clearTimeout(t.evict);Q_.delete(e)}}Object.assign(this._warmSlot,e)}}_warmViewportState(){const e=this._effectiveProjection();return{space:this._space,mode:this._mode,projection:e,activeLabsIso:this._labsIso,logicalCenter:this._logicalViewCenter(e),zoom:this._zoom,view:this._view?{...this._view}:null,snap:this._viewModeSnap?{...this._viewModeSnap}:null,tool:this._tool,decorTool:this._decorTool,showHidden:this._showHidden,showFar:this._showFar,selId:this._selId,rszSel:this._resize?.selectedRoomId,decorSel:this._decorSel}}_warmDialogState(){const e=(e,t)=>({kind:e,space:this._space,mode:this._mode,data:t});return this._tapConfirm||this._alignDialog||this._mergeDialog||this._importDialog||this._backupExportDialog||this._backupImportDialog?null:this._openingInfo?e("openingInfo",this._openingInfo.id):this._infoCard?e("info",this._infoCard.id):this._rulesDialog?this._rulesDialog.busy?null:e("rules",this._rulesDialog):this._settingsDialog?this._settingsDialog.busy?null:e("settings",this._settingsDialog):this._markerDialog?this._markerDialog.busy?null:e("marker",this._markerDialog):this._openingDialog?e("opening",this._openingDialog):this._backdropDialog?e("backdrop",this._backdropDialog):this._decorShapeDialog?e("decorShape",this._decorShapeDialog):this._decorTextDialog?e("decorText",this._decorTextDialog):this._roomDialog?e("room",{editId:this._roomEditId,fill:this._roomFill,customFill:this._roomCustomFill,tempSrc:this._roomTempSrc,humSrc:this._roomHumSrc,srcOpen:this._roomSrcOpen,srcFilter:this._roomSrcFilter,nameScale:this._roomNameScale,labelScale:this._roomLabelScale,areaSel:this._areaSel,nameSel:this._nameSel,pendingSplit:this._pendingSplit,wallFaceBatch:this._wallFaceBatch,path:this._path}):this._spaceDialog?this._spaceDialog.busy?null:e("space",this._spaceDialog):null}_warmSnapshot(){if(this._booting||this._config?.kiosk||"steady"!==this._continuity.state)return;const e={vp:this._warmViewportState(),frameFingerprint:this._continuity.frameFingerprint,devices:this._devices};if(this._warmRevivePending||(e.dlg=this._warmDialogState()),this.isConnected&&this._warmSlot?.owner===this._warmGen){const t=this.parentNode;e.place=t?new WeakRef(t):null,e.idx=this._warmIdx(t)}this._warmPatch(e)}_warmReviveDialog(){this._warmRevivePending=!1;const e=this._warmSlot;if(this._warmReviveTimer=void 0,!e||!e.dlg)return;const t=e.dlg,i=e.freed;if(e.dlg=null,e.freed=0,clearTimeout(e.evict),e.evict=0,i&&!(Date.now()-i>im)&&t.space===this._space&&t.mode===this._mode){switch(t.kind){case"space":this._spaceDialog={...t.data,busy:!1,savedBusy:!1};break;case"marker":this._markerDialog={...t.data,busy:!1};break;case"settings":this._settingsDialog={...t.data,busy:!1};break;case"rules":this._rulesDialog={...t.data,busy:!1};break;case"opening":this._openingDialog={...t.data};break;case"backdrop":this._backdropDialog={...t.data};break;case"decorShape":this._decorShapeDialog={...t.data};break;case"decorText":{this._decorTextDialog={...t.data};const e=String(this._decorTextDialog?.text??"").length;this._decorTextSelection={start:e,end:e};break}case"room":{const e=t.data;this._roomEditId=e.editId,this._roomFill=e.fill,this._roomCustomFill=e.customFill||null,this._roomTempSrc=e.tempSrc,this._roomHumSrc=e.humSrc,this._roomSrcOpen=e.srcOpen,this._roomSrcFilter=e.srcFilter,this._roomNameScale=e.nameScale,this._roomLabelScale=e.labelScale,this._areaSel=e.areaSel,this._nameSel=e.nameSel,this._pendingSplit=e.pendingSplit,this._wallFaceBatch=e.wallFaceBatch||null,this._path=e.path,this._wallFaceBatch&&(this._activeDraftId=this._wallFaceBatch.activeDraftId,this._draftSegmentCms=[...this._wallFaceBatch.activeCms]),this._roomDialog=!0;break}case"info":{const e=this._devices.find(e=>e.id===t.data);e&&(this._infoCard=e);break}case"openingInfo":{const e=(this._curSpaceCfg?.openings||[]).find(e=>e.id===t.data);e&&(this._openingInfo=e);break}}this.requestUpdate()}}_cacheSnapshot(){if(this._serverCfg)try{this._cfgContentFingerprint=Tl(this._serverCfg),this._layoutContentFingerprint=Tl(this._layout),this._virtualLights=function(e,t,i){if(e.configRev===i)return e;const n=new Set((Array.isArray(t?.markers)?t.markers:[]).filter(zc).map(e=>e.id));return{...e,configRev:i,off:new Set([...e.off].filter(e=>n.has(e)))}}(this._virtualLights,this._serverCfg,this._cfgRev),localStorage.setItem(rm,JSON.stringify({config:this._serverCfg,rev:this._cfgRev,config_fingerprint:this._cfgContentFingerprint,layout:this._layout,layout_rev:this._layoutRev,layout_fingerprint:this._layoutContentFingerprint,virtual_lights:Ac(this._virtualLights)}))}catch{}}_beginContinuityCandidate(e,t,i="plan"){return this._booting&&!this._continuity.hasCompleteFrame?this._continuity.token:(this._continuityDataReady=t,this._continuityPaintToken=-1,this._stagedDeviceSnapshotToken=-1,this._resumeSettling=!0,this._continuity.beginCandidate(e,i))}_continuityStageValid(){const e=this._stageEl;return!!e&&e.clientWidth>0&&e.clientHeight>0}_continuityAssetsReady(){if(!this._model.length)return!1;const e=this._model.length?this._spaceModel():null;return!e?.bg?.href||this._signer.isReady(this.hass,e.bg.href)}_initialSpaceSelection(e,t=this._loadOk){const i=this._fixedFloorState(e,t);return"valid"===i.kind?{id:i.id,source:"fixed"}:this._hasFixedFloor?{id:null,source:"none"}:function(e){const t=new Set(e.spaceIds.filter(e=>!!e));if(!t.size)return{id:null,source:"none"};const i=[["hash",!1===e.acceptHash?null:e.hashSpace],["current",e.preserveCurrent?e.currentSpace:null],["saved",e.savedSpace],["default",e.defaultSpace],["first",e.spaceIds[0]]];for(const[e,n]of i)if(n&&t.has(n))return{id:n,source:e};return{id:null,source:"none"}}({spaceIds:e.map(e=>e.id),hashSpace:this._hashSpace(),acceptHash:!this._hashApplied,currentSpace:this._space,preserveCurrent:this._hashApplied||this._navApplied||this._warmVpArmed,savedSpace:this._savedNav()?.space,defaultSpace:this._config?.default_floor})}_adoptInitialSpace(e,t=this._loadOk){const i=this._initialSpaceSelection(e,t);return i.id?(this._commitSpace(i.id,!0),"hash"===i.source&&(this._hashApplied=!0),"saved"===i.source&&(this._navApplied=!0),i):i}_candidateBackdrop(e,t=this._space){const i=za(e),n=this._fixedFloorState(i,!0);if(this._hasFixedFloor&&"valid"!==n.kind)return"";const r=this._initialSpaceSelection(i,!0).id||(i.some(e=>e.id===t)?t:i[0]?.id);return i.find(e=>e.id===r)?.bg?.href||""}_visualFrameFingerprint(){const e=this._stageEl,t=e?[e.clientWidth,e.clientHeight]:[0,0];return Rl([this._cfgRev,this._cfgContentFingerprint||Tl(this._serverCfg),this._layoutRev,this._layoutContentFingerprint||Tl(this._layout),this._space,this._mode,this._view,t,this._glowScreenBlend?"screen":"normal",this.hass?.themes?.darkMode??this.hass?.themes?.default_theme??""])}_settleContinuityFrame(){if(this._booting||!this._continuityStageValid())return;if(!this._continuity.hasCompleteFrame&&"steady"===this._continuity.state)return void(this._continuityAssetsReady()&&(this._renderSnapshotAt=Date.now(),this._continuity.markCompleteFrame(this._visualFrameFingerprint())));if(!this._continuityDataReady)return;if(!["holding","offline-stale","overlay-pending","overlay-visible","candidate-ready"].includes(this._continuity.state))return;const e=this._continuity.token;if(this._candidateDeviceSnapshot&&this._candidateDeviceSnapshot!==this._visibleDeviceSnapshot&&this._stagedDeviceSnapshotToken!==e)return this._stagedDeviceSnapshotToken=e,void this.requestUpdate();this._continuityPaintToken!==e&&(this._continuityPaintToken=e,this._continuity.candidateReady(e)&&this._continuity.commitAfterPaint(e,{updateComplete:()=>this.updateComplete,stageValid:()=>this.isConnected&&this._continuityStageValid(),assetsReady:()=>this._continuityAssetsReady(),frameFingerprint:()=>this._visualFrameFingerprint()}).then(t=>{t&&e===this._continuity.token?(this._resumeSettling=!1,this._renderSnapshotAt=Date.now(),this._candidateDeviceSnapshot&&(this._visibleDeviceSnapshot=this._candidateDeviceSnapshot),this._candidateDeviceSnapshot=null,this._stagedDeviceSnapshotToken=-1,this._warmSnapshot()):e===this._continuity.token&&(this._continuityPaintToken=-1,this._stagedDeviceSnapshotToken=-1,this._candidateDeviceSnapshot=null,this.requestUpdate())}))}_onBackdropLoaded(e,t){this._signer.markLoaded(this.hass,e,t),this._continuity.note("asset-ready"),this._continuityPaintToken=-1,"steady"!==this._continuity.state&&this.requestUpdate()}_renderRecoveryOverlay(){if(!this._continuity.overlayVisible&&"recovery-error"!==this._continuity.state)return G;const e="connection"===this._continuity.recoveryReason;return B`
e.stopPropagation()} + @click=${e=>e.stopPropagation()} + @wheel=${e=>e.stopPropagation()}> + + ${this._t(e?"continuity.restore_connection":"continuity.restore_plan")} + ${"recovery-error"===this._continuity.state?B``:G} +
`}_renderEditorRuntimeLoading(){return this._editorRuntimeLoadingVisible?B`
+ + ${this._t("editor.loading")} +
`:G}houseplanContinuityTrace(){return[...this._continuityHistory,...this._continuity.trace].slice(-80).map(e=>({...e,...e.stage?{stage:[...e.stage]}:{}}))}getCardSize(){return 12}get _norm(){return!(!this._serverCfg||!this._serverCfg.spaces.length)}_cfgFingerprint(){const e=this._serverCfg?.spaces||[];let t=e.length+":";for(const i of e){t+=(i.id||"")+","+(i.plan_aspect||"")+","+(i.plan_url||"").length+","+(i.plan_x??"")+","+(i.plan_y??"")+","+(i.plan_scale??"")+","+(i.plan_scale_x??"")+","+(i.plan_scale_y??"")+","+(i.plan_angle??"")+","+(i.rooms?.length||0)+","+(i.openings?.length||0)+","+(i.decor?.length||0)+";";for(const e of i.rooms||[]){const i=e.poly?.[0],n=e.poly?.[e.poly.length-1];t+=(e.poly?.length||0)+"."+(e.id||"")+"."+(e.open_to||[]).join("+")+"."+(e.area||"")+"."+JSON.stringify(e.settings||0)+"."+(e.x??"")+","+(e.y??"")+","+(e.w??"")+","+(e.h??"")+","+(i?i[0]+"/"+i[1]:"")+","+(n?n[0]+"/"+n[1]:"")+";"}}return t}get _model(){if(!this._serverCfg)return[];const e=this._cfgEpoch+"|"+this._cfgFingerprint();if(this._modelCache&&this._modelCache.key===e)return this._modelCache.model;const t=this._buildModel();return this._modelCache={key:e,model:t},t}_buildModel(){if(!this._serverCfg)return[];const e=this._renderCfg;return za(e).map((t,i)=>{const n=e.spaces[i]?.plan_url;return t.bg&&n?{...t,bg:{...t.bg,href:n}}:t})}_spaceModel(){return this._hasFixedFloor?Al(this._model,this._space):(e=this._model,t=this._space,e.find(e=>e.id===t)??e[0]);var e,t}_spaceModelById(e){return Al(this._model,e)}_syncEmptySpaceState(){if(!!this._serverCfg&&0===this._serverCfg.spaces.length){if(!this._emptySpaceStateActive){this._emptySpaceStateActive=!0;for(const e of this._pointers.keys())for(const t of this.renderRoot.querySelectorAll("*"))try{t.hasPointerCapture?.(e)&&t.releasePointerCapture(e)}catch{}this._pointers.clear(),this._panStart=null,this._panLock=null,this._pinchStart=null,this._swipeStart=null,this._drag=null,this._rlResize=null,this._vacFit=null,this._compassDrag=!1,this._cancelModeTransition(!1),this._mode="view",this._clearGeometryGesture(),this._geometryHistory.clear(),this._resumeDraftBySpace={},this._tip=null,this._hoverRoom=null,this._openingInfo=null,this._closeInfoCard(),this._deviceInbox=null,this._deviceInboxReturn=null,this._markerDialog=null,this._physicalDialog=null,this._backdropDialog=null,this._decorShapeDialog=null,this._decorTextDialog=null,this._roomDialog=!1,"edit"===this._spaceDialog?.mode&&(this._spaceDialog=null),this._editorSecondary?.closeForNavigation(),this._saveConfigDebounced.cancel(),this._frame=null,this._planSnapGeometryCache=null,this._hiddenWallDiagnosticCache=null,this._decorSnapCache=null,this._commitSpace("",!0)}}else this._emptySpaceStateActive=!1}get _areaToSpace(){const e={};for(const t of this._model)for(const i of t.rooms)i.area&&(e[i.area]={space:t.id,room:i});return e}get _settings(){return this._serverCfg?.settings||{}}get _showAll(){return this._showHidden||!this._settings.filter_seeded&&!!this._settings.show_all}_seedHiddenDevices(){if(!this._serverCfg||!this._norm||!this._canEdit)return;const e=this._serverCfg,t=function(e){const t=e.hass,i=e.registry||Vc(t),n=Xc(t,i),r=Jc(t,i),{areaToSpace:o,markers:s,settings:a,excluded:l,iconRules:c}=e,h=!1!==a.group_lights,d=Lh(s),u=Ph(n,h).filter(e=>!jh(n,e.eid,d)),p=new Set(u.map(e=>e.area)),_=ih(n),m=nh(s,r),g=new Set(s.map(e=>e.binding)),f=[];for(const e of Object.values(n.devices)){const r=e.area_id;if(!r||!o[r])continue;if("service"===e.entry_type)continue;if(g.has("device:"+e.id))continue;if("active"!==th(t,"device:"+e.id,i).kind)continue;const s=rh(n,e.id,_[e.id]||[],m);if(s.partial&&!s.entityIds.length)continue;const a=s.entityIds,d=oh(n,e,a);let u=l.has(d)||"Group"===e.model||/scene/i.test(e.model||"")||/bridge/i.test((e.model||"")+(e.name||""))||"myheat"===d&&!!e.via_device_id;if(!u&&h&&p.has(r)){const t=(e.name_by_user||e.name||"").trim();"mdi:lightbulb"===Fh(n,t,e.model,a,c)&&(u=!0)}u&&f.push("device:"+e.id)}return f}({hass:this.hass,registry:this._haRegistry,areaToSpace:Object.fromEntries(Object.entries(this._areaToSpace).map(([e,t])=>[e,t.space])),markers:this._markers,settings:this._settings,excluded:this._excluded,firstSpaceId:this._model[0]?.id||"",iconRules:this._iconRules});if(!t.length&&e.settings?.filter_seeded)return;e.markers=e.markers||[];const i=[];for(const n of t){const t="h"+n.slice(n.indexOf(":")+1);e.markers.push({id:t,binding:n,hidden:!0}),i.push(n.slice(n.indexOf(":")+1))}const n={...e.settings||{},filter_seeded:!0};delete n.show_all,i.length&&Array.isArray(n.new_device_ids)&&(n.new_device_ids=n.new_device_ids.filter(e=>!i.includes(e))),e.settings=n,this._regSignature="",this._maybeRebuildDevices(),this._saveConfig(),this.requestUpdate()}get _iconRules(){const e=this._settings.icon_rules;if(!e||!Array.isArray(e)||!e.length)return;const t=JSON.stringify(e);return t!==this._rulesCompiledSrc&&(this._rulesCompiledSrc=t,this._rulesCompiled=De(e)),this._rulesCompiled}get _fillColors(){return on(this._settings)}get _excluded(){const e=this._settings.exclude_integrations;return e?new Set(e):Re}willUpdate(e){e.has("_serverCfg")&&this._cfgEpoch++,this._syncEmptySpaceState(),e.has("hass")&&this.hass&&(this._hassSequence++,this._renderSnapshotAt=Date.now(),this._continuity.note("hass-snapshot"),this._ensureHaRegistryAuthority(),this._planHassMemo=null,this._hookConnection(),!this._loadOk&&!this._loading&&this._loadTries<8&&this._loadFromServer(),this._maybeRebuildDevices(),this._vacTick(),this._activityTick()),this._continuity.hasCompleteFrame&&"steady"===this._continuity.state&&this._continuity.refreshCompleteFrame(this._visualFrameFingerprint()),this._captureRenderDeviceSnapshot()}updated(){this._pruneDevicePressFeedback(),this._syncDayCycleClock(),this._warmSnapshot(),this._editorRuntime&&this._dtMeasure();const e=this._stageEl;e&&!this._roViewport&&(this._roViewport=new ResizeObserver(()=>this._refitView()),this._roViewport.observe(e)),e&&this._booting&&!this._bootTimer&&this._bootWatch();const t=this.renderRoot.querySelector(".hdr");if(t&&e&&!this._roHdr){const i=()=>{const t=this.renderRoot.querySelector("ha-card");if(!t)return;const i=e.getBoundingClientRect().top-t.getBoundingClientRect().top,n=Math.min(Math.max(t.getBoundingClientRect().top,0),120),r=Math.round(i+n);r>=0&&r!==this._hdrH&&(this._hdrH=r),r>=0&&!this._booting&&!this._config?.kiosk&&e.clientHeight>0&&this._warmPatch({hdrH:r,stageH:e.clientHeight})};this._roHdr=new ResizeObserver(()=>requestAnimationFrame(i)),this._roHdr.observe(t),this._onWinResize=()=>requestAnimationFrame(i),window.addEventListener("resize",this._onWinResize),i()}if(e&&!this._view&&this._refitView(),this._editorSecondary?.afterRender(),this._settleContinuityFrame(),this._serverStorage&&this._loadOk&&0===this._model.length&&!this._spaceDialog&&!this._importDialog&&!this._onboardingShown){this._onboardingShown=!0;const e=function(e){const t=e?.floors;if(!t||"object"!=typeof t)return[];const i=[];for(const e of Object.values(t))e&&e.floor_id&&i.push({id:e.floor_id,name:e.name||e.floor_id,level:e.level??null});return i.sort((e,t)=>{const i=e.level??1e9,n=t.level??1e9;return i!==n?i-n:e.name.localeCompare(t.name)}),i}(this.hass);e.length?this._importDialog={floors:e.map(e=>({...e,checked:!0}))}:this._openSpaceDialog("create")}}_adoptStructuralResponses(e,t,i){const n=e?.config,r=n&&Array.isArray(n.spaces)?n:null,o=Tl(r),s=o!==(this._cfgContentFingerprint||Tl(this._serverCfg));if(s&&(this._geometryHistory.clear(),this._pendingPhysicalWrites.clear(),this._serverCfg&&this._clearGeometryGesture(),this._serverCfg=r,this._cfgContentFingerprint=o),this._cfgRev=e?.rev??this._cfgRev,e&&("virtual_lights"in e||"config"in e)){const t=Dc(this._virtualLights,e.virtual_lights,this._cfgRev,"virtual_lights"in e);Oc(t)!==Oc(this._virtualLights)&&(this._virtualLights=t,this._capturedSnapshotVirtual="")}let a=!1;if(void 0!==t||void 0!==i){const e=i??t?.layout??{},n=Tl(e);a=n!==(this._layoutContentFingerprint||Tl(this._layout)),a&&(this._layout=e,this._layoutContentFingerprint=n),this._layoutRev=t?.rev??this._layoutRev}return this._canOptimizeUndo=!(!e?.can_optimize_undo&&!t?.can_optimize_undo),this._haIntegrationVersion="string"==typeof e?.integration_version?e.integration_version:this._haIntegrationVersion,this._undoKind=e?.undo_kind||t?.undo_kind||null,"boolean"==typeof e?.can_write&&(this._serverCanWrite=e.can_write),s&&this._continuity.note("config-candidate",{configRev:this._cfgRev}),a&&this._continuity.note("layout-candidate",{layoutRev:this._layoutRev}),{configChanged:s,layoutChanged:a}}_resumePendingNavMode(){if(!this._pendingNavMode||!this._canEdit||this._config?.kiosk)return!1;const e=this._pendingNavMode;return this._pendingNavMode=null,this._setMode(e,!1),!0}async _loadFromServer(){this._loading=!0,this._loadTries++;const e=this._space,t=!!this._view;try{const[i,n]=await Promise.all([this.hass.callWS({type:"houseplan/config/get"}),this.hass.callWS({type:"houseplan/layout/get"})]),r=i?.config&&Array.isArray(i.config.spaces)?i.config:null,o=Tl(r)!==(this._cfgContentFingerprint||Tl(this._serverCfg))||Tl(n?.layout??{})!==(this._layoutContentFingerprint||Tl(this._layout));if(o){if(!await this._signer.prepareImage(this.hass,this._candidateBackdrop(r)))return this._continuity.note("asset-failed"),void this._scheduleLoadRetry(!0)}o&&this._continuity.hasCompleteFrame&&"steady"===this._continuity.state&&this._beginContinuityCandidate("structural-response",!0),this._connectionWasLost=!1,this._serverStorage=!0,"boolean"==typeof i?.can_write&&(this._serverCanWrite=i.can_write),this._canOptimizeUndo=!(!i?.can_optimize_undo&&!n?.can_optimize_undo),this._haIntegrationVersion="string"==typeof i?.integration_version?i.integration_version:this._haIntegrationVersion,this._adoptStructuralResponses(i,n),this._adoptInitialSpace(this._model,!0),this._resumePendingNavMode(),this._cacheSnapshot(),this._warmVpArmed&&this._space===this._warmVp?.space?this._warmVpArmed=!1:t&&this._space===e||this._restoreZoom(),this._loadOk=!0,this.hass.callWS({type:"houseplan/trail/get"}).then(e=>{this._vacSrvTrails=e?.trails||{},this.requestUpdate()}).catch(()=>{}),this._ensureLiveSyncSubscriptions()}catch(e){if(this._serverCfg)this._scheduleLoadRetry(!0);else if(this._loadTries>=8){this._serverStorage=!1;try{this._layout=JSON.parse(localStorage.getItem(nm)||"{}")||{}}catch{this._layout={}}}}finally{this._loading=!1,this._continuityDataReady=!0,this._regSignature="",this._maybeRebuildDevices(),this.requestUpdate()}}_ensureLiveSyncSubscriptions(){const e=this.hass?.connection;if(!e)return;if(e!==this._liveSyncConnection&&(this._unsubCfg?.(),this._unsubCfg=null,this._unsubLayout?.(),this._unsubLayout=null,this._unsubTrail?.(),this._unsubTrail=void 0,this._unsubVirtual?.(),this._unsubVirtual=null,this._liveSyncGeneration++,this._liveSyncAttempt=null,this._liveSyncConnection=e),this._liveSyncAttempt)return;const t=this._liveSyncGeneration,i=[],n=(n,r,o,s)=>{n()||i.push(async()=>{const i=await e.subscribeEvents(s,o);t===this._liveSyncGeneration&&this.isConnected&&this.hass?.connection===e&&!n()?r(i):i?.()})};if(n(()=>this._unsubCfg,e=>{this._unsubCfg=e},"houseplan_config_updated",e=>{const t=Number(e?.data?.rev??-1);t!==this._cfgRev&&this._reloadConfigOnly(!1,t)}),n(()=>this._unsubTrail,e=>{this._unsubTrail=e},"houseplan_trail_updated",async()=>{try{const e=await this.hass.callWS({type:"houseplan/trail/get"});this._vacSrvTrails=e?.trails||{},this.requestUpdate()}catch{}}),n(()=>this._unsubLayout,e=>{this._unsubLayout=e},"houseplan_layout_updated",e=>this._onLayoutEvent(Number(e?.data?.rev??-1))),n(()=>this._unsubVirtual,e=>{this._unsubVirtual=e},"houseplan_virtual_light_updated",e=>{const t=Fc(this._virtualLights,e?.data);t!==this._virtualLights&&(this._virtualLights=t,this._capturedSnapshotVirtual="",this._cacheSnapshot(),this.requestUpdate())}),!i.length)return;const r=function(e){return Promise.allSettled(e.map(e=>Promise.resolve().then(e)))}(i).then(()=>{});this._liveSyncAttempt=r,r.finally(()=>{this._liveSyncAttempt===r&&(this._liveSyncAttempt=null)})}async _reloadConfigOnly(e=!1,t){if(!e){if(void 0!==t&&t<=this._cfgRev)return;if(this._saveConfigDebounced.pending()&&this._saveConfigDebounced.flush(),this._cfgWriting)return clearTimeout(this._reloadRetry),void(this._reloadRetry=window.setTimeout(()=>this._reloadConfigOnly(!1,t),400))}this._beginContinuityCandidate("config-reload",!1);try{const e=await this.hass.callWS({type:"houseplan/config/get"}),t=e?.config&&Array.isArray(e.config.spaces)?e.config:null;if(Tl(t)!==(this._cfgContentFingerprint||Tl(this._serverCfg))&&!await this._signer.prepareImage(this.hass,this._candidateBackdrop(t)))return this._continuity.note("asset-failed"),void this._scheduleLoadRetry(!0);const i=this._space;this._adoptStructuralResponses(e),this._adoptInitialSpace(this._model,!0),this._resumePendingNavMode(),this._cacheSnapshot(),this._space!==i&&this._restoreZoom(),this._regSignature="",this._maybeRebuildDevices(),this.requestUpdate()}catch(e){this._showToast(this._t("toast.cfg_reload_failed",{err:this._errText(e)}))}finally{this._continuityDataReady=!0,this.requestUpdate()}}_scheduleLoadRetry(e=!1){if(void 0!==this._loadRetryTimer)return;const t=Math.min(8e3,500*2**Math.min(4,Math.max(1,this._loadTries-7)));this._loadRetryTimer=window.setTimeout(()=>{this._loadRetryTimer=void 0,!e&&this._loadOk||this._loading||!this.hass||this._loadFromServer()},t)}_ensureHaRegistryAuthority(){const e=this.hass?.connection||null;e&&e!==this._haRegistryConnection&&(this._haRegistryRelease?.(),this._haRegistryConnection=e,this._haRegistryRev=-1,this._haBindingCacheKey="",this._planHassMemo=null,this._haRegistryRelease=Gc(this.hass,this._onHaRegistryUpdate),this._onHaRegistryUpdate())}get _haRegistry(){return Vc(this.hass)}get _planHass(){const e=this._haRegistry,t=Yc(this.hass,e),i=this._planHassMemo;if(i&&i.hass===this.hass&&i.sig===t)return i.active;const n=Xc(this.hass,e),r=Jc(this.hass,e);return this._planHassMemo={hass:this.hass,sig:t,active:n,full:r},n}_captureRenderDeviceSnapshot(){if(!this.hass)return;const e=Date.now(),t=[...this._activityRt.entries()].map(([t,i])=>`${t}:${i.gen}:${i.flashTs}:`+(i.flashKind&&(i.expiresAt||i.flashTs+wp)>e?1:0)).join("|"),i=Oc(this._virtualLights);if(this._capturedSnapshotSequence===this._hassSequence&&this._capturedSnapshotDevices===this._devices&&this._capturedSnapshotLayout===this._layout&&this._capturedSnapshotConfigEpoch===this._cfgEpoch&&this._capturedSnapshotVirtual===i&&this._capturedSnapshotActivity===t)return;const n=this._planHass,r=new Map,o=new Map,s=new Set(["sun.sun"]);this._vacFit?.source&&s.add(this._vacFit.source);const a=new Set,l=new Set,c=e=>{if(!e)return;const t=e.indexOf(":");if(t<0)return void s.add(e);const i=e.slice(0,t),n=e.slice(t+1);"device"===i?a.add(n):"entity"===i&&s.add(n)};for(const e of this._model)for(const t of e.rooms)t.area&&l.add(t.area),c(t.settings?.temp_source),c(t.settings?.hum_source);for(const e of this._serverCfg?.spaces||[])for(const t of e.openings||[])for(const e of ni(t))s.add(e);for(const e of this._serverCfg?.spaces||[])for(const t of e.decor||[])if("text"===t.kind){for(const e of String(t.text||"").matchAll(/\{([^{}\r\n]+)\}/g)){const t=Hi(e[1]);t?.entity&&s.add(t.entity)}t.entity&&s.add(t.entity),o.set(`decor:${e.id}:${t.id}`,Wi(t.text,t,n,e=>!!n.entities?.[e]&&!!n.states?.[e]&&!jh(n,e,Lh(this._markers))))}const h=Mh(n,this._devices,null,this._virtualLights);for(const e of this._devices){for(const t of[!1,!0])r.set(Nu(e.id,t),Pu(n,e,{liveStates:!1!==this._config?.live_states,showTemperature:!1!==this._config?.show_temperature,showSignal:t,activityRuntime:this._activityRt.get(e.id),sourceDetails:!1,lightDevices:this._devices,lightSources:h,registryHass:this._fullRegistryHass,reducedMotion:this._reducedMotion}));if(this._isVacDev(e)){const t=this._vacSource(e,n),i=t?Bl(n?.states?.[t]?.attributes):null,r=this._vacRt.get(e.id);o.set(`vacuum:${e.id}`,{source:t,telemetry:i,mapId:i?this._vacMapId(e,i,n):null,runtime:r?{trail:r.trail,lastTs:r.lastTs,moving:r.moving,jump:r.jump}:null,server:this._vacSrvTrails[e.id]||null})}}const d=Eu({sourceSequence:this._hassSequence,hass:n,devices:this._devices,presentations:r,positions:(u=this._model.length>0,p=this._devices,_=e=>this._livePos(e),u?new Map(p.map(e=>[e.id,_(e)])):new Map),facts:o,entityIds:s,deviceIds:a,areaIds:l});var u,p,_;this._capturedSnapshotSequence=this._hassSequence,this._capturedSnapshotDevices=this._devices,this._capturedSnapshotLayout=this._layout,this._capturedSnapshotActivity=t,this._capturedSnapshotConfigEpoch=this._cfgEpoch,this._capturedSnapshotVirtual=i,this._visibleDeviceSnapshot&&"steady"!==this._continuity.state?this._candidateDeviceSnapshot=d:(this._visibleDeviceSnapshot=d,this._candidateDeviceSnapshot=null)}get _renderDeviceSnapshot(){return this._stagedDeviceSnapshotToken===this._continuity.token?this._candidateDeviceSnapshot||this._visibleDeviceSnapshot:this._visibleDeviceSnapshot||this._candidateDeviceSnapshot}get _renderPlanHass(){return this._renderDeviceSnapshot?.hass||this._planHass}get _renderDevices(){return this._renderDeviceSnapshot?.devices||this._devices}get _fullRegistryHass(){return this._planHass,this._planHassMemo?.full||this.hass}_bindingStatus(e){return th(this.hass,e,this._haRegistry)}houseplanDiagnostics(){const e=function(e){const t=Vc(e);return{access:t.access,authoritative:t.authoritative,revision:t.revision,lastSuccess:t.lastSuccess,error:t.error}}(this.hass),t={active:0,ha_disabled:0,orphaned:0,unverified:0};for(const e of this._markers)e.removed||"virtual"===e.binding||t[this._bindingStatus(e.binding).kind]++;return{registry:{...e,lastSuccessAgeMs:e.lastSuccess?Math.max(0,Date.now()-e.lastSuccess):null},bindings:t}}_openBindingInHa(e){const[t,i]=e.split(":");if(i)if("device"!==t){if("entity"===t){const e=this._fullRegistryHass.entities?.[i];e?.device_id&&_m("/config/devices/device/"+encodeURIComponent(e.device_id))}}else _m("/config/devices/device/"+encodeURIComponent(i))}_bindingHasHaPage(e){const[t,i]=e.split(":");return!!i&&("device"===t||"entity"===t&&!!this._fullRegistryHass.entities?.[i]?.device_id)}_toggleMarkerDialogVisibility(){const e=this._markerDialog;if(!e)return;const t="ha"===e.bindingMode?this._bindingStatus(e.binding):null,i=e.hideFromPlan||"ha_disabled"===t?.kind;i&&"ha_disabled"===t?.kind?this._showToast(this._t("entity"===t.reason?"toast.ha_disabled_show_entity":"toast.ha_disabled_show_device")):i&&"unverified"===t?.kind?this._showToast(this._t("toast.ha_binding_unverified")):this._markerDialog={...e,hideFromPlan:!i}}_hookConnection(){const e=this.hass?.connection;e&&e!==this._connHooked&&(this._connHooked?.removeEventListener?.("ready",this._onConnReady),this._connHooked?.removeEventListener?.("disconnected",this._onConnLost),this._connHooked?.removeEventListener?.("reconnect-error",this._onConnLost),e.addEventListener?.("ready",this._onConnReady),e.addEventListener?.("disconnected",this._onConnLost),e.addEventListener?.("reconnect-error",this._onConnLost),this._connHooked=e)}_display(e){return this._signer.display(this.hass,e)}_resign(){this._signer.resign(this.hass,Sn(this._serverCfg))}_onLayoutEvent(e){e<=this._layoutRev||(clearTimeout(this._layoutSyncTimer),this._layoutSyncTimer=window.setTimeout(()=>{e<=this._layoutRev||this._reloadLayoutOnly()},200))}_noteLayoutRev(e){const t=e?.rev;"number"==typeof t&&t>this._layoutRev&&(this._layoutRev=t)}async _reloadLayoutOnly(){if(!this._serverStorage||!this.hass?.callWS)return;this._beginContinuityCandidate("layout-reload",!1);const e=new Map;for(const t of this._dirtyPos)this._layout[t]&&e.set(t,this._layout[t]);this._persistLayout.pending()&&this._persistLayout.flush();for(const[t,i]of this._sentPos)e.set(t,i);try{const t=await this.hass.callWS({type:"houseplan/layout/get"}),i={...t?.layout||{}};for(const[t,n]of e)i[t]=n;const n=Tl(i);n!==(this._layoutContentFingerprint||Tl(this._layout))&&(this._layout=i,this._layoutContentFingerprint=n),this._layoutRev=t?.rev??this._layoutRev,this._canOptimizeUndo=!!t?.can_optimize_undo,this._haIntegrationVersion="string"==typeof t?.integration_version?t.integration_version:this._haIntegrationVersion,this._undoKind=t?.undo_kind||null,this._cacheSnapshot(),this.requestUpdate()}catch{}finally{this._continuityDataReady=!0,this.requestUpdate()}}_maybeRebuildDevices(){const e=this.hass;if(!e?.devices||!e?.entities||!e?.areas)return;const t=this._haRegistry,i=Yc(e,t)+":"+Object.keys(e.areas).length+":"+(this._norm?"n":"l")+":"+Xd(e,this._config?.language);if(i===this._regSignature&&this._devices.length)return;this._regSignature=i;const n=new Map(this._devices.map(e=>[e.id,e.bindingStatus?.kind||"active"]));this._devices=Wh({hass:e,registry:t,areaToSpace:Object.fromEntries(Object.entries(this._areaToSpace).map(([e,t])=>[e,t.space])),markers:this._markers,settings:this._settings,excluded:this._excluded,showAll:this._showAll,firstSpaceId:this._model[0]?.id||"",loc:e=>this._t(e),iconRules:this._iconRules});const r=this._markers.filter(e=>!e.removed&&"virtual"!==e.binding).map(e=>e.binding).sort(),o=t.revision+":"+r.join("|");if(t.authoritative&&o!==this._haBindingCacheKey){const i=new Map;for(const n of r)i.set(n,th(e,n,t));!function(e){if("undefined"==typeof localStorage||!e.size)return;const t=eh(),i=Date.now();for(const[n,r]of e)"active"===r.kind?t[n]={kind:"active",ts:i}:"ha_disabled"===r.kind&&(t[n]={kind:"ha_disabled",reason:r.reason,ts:i});const n=Object.fromEntries(Object.entries(t).sort((e,t)=>t[1].ts-e[1].ts).slice(0,1500));Lc=n;try{localStorage.setItem(Hc,JSON.stringify(n))}catch{}}(i),this._haBindingCacheKey=o}this._defPos=this._defaultPositions(),this._syncNewDevices(),this._seedHiddenDevices(),this._syncActivityRuntime();const s=new Set(this._devices.filter(e=>!e.hidden).map(e=>e.id));for(const e of this._vacRt.keys())s.has(e)||this._vacRt.delete(e);if(this._infoCard){const e=this._devices.find(e=>e.id===this._infoCard.id);this._infoCard=e&&"ha_disabled"!==e.bindingStatus?.kind?e:null}const a=this._devices.some(e=>"ha_disabled"===e.bindingStatus?.kind&&"ha_disabled"!==n.get(e.id));a&&("ha_disabled"!==this._infoCard?.bindingStatus?.kind&&"ha_disabled"!==this._devices.find(e=>e.id===this._infoCard?.id)?.bindingStatus?.kind||this._closeInfoCard(),this._drag&&"ha_disabled"===this._devices.find(e=>e.id===this._drag.id)?.bindingStatus?.kind&&(this._drag=null),clearTimeout(this._holdTimer),this._holdFired=!1,this._tip=null,this._tapConfirm=null),this._nativeMoreInfoEntity&&!this._planEntityAvailable(this._nativeMoreInfoEntity)&&(pm(this,"hass-more-info",{entityId:null}),this._nativeMoreInfoEntity=null)}_syncNewDevices(){if(!this._norm||!this._loadOk||!this._serverCfg)return;const e=this._devices.filter(e=>!e.marker&&!e.virtual).map(e=>e.id).sort(),t=e.join(",");if(t===this._newSyncKey)return;this._newSyncKey=t;const i=this._settings,{fresh:n,known:r}=function(e,t){if(!Array.isArray(t))return{fresh:[],known:[...e]};const i=new Set(t),n=e.filter(e=>!i.has(e));return{fresh:n,known:n.length?[...t,...n]:t}}(e,i.known_devices);if(!Array.isArray(i.known_devices)||n.length){const e=[...new Set([...i.new_device_ids||[],...n])];this._serverCfg={...this._serverCfg,settings:{...i,known_devices:r,new_device_ids:e}},this._saveConfig()}}get _newIds(){const e=this._settings.new_device_ids;return new Set(Array.isArray(e)?e:[])}_ackNewDevice(e){if(!this._newIds.has(e)||!this._serverCfg)return;const t=this._settings;this._serverCfg={...this._serverCfg,settings:{...t,new_device_ids:(t.new_device_ids||[]).filter(t=>t!==e)}},this._saveConfig(),this.requestUpdate()}get _markers(){return this._serverCfg?.markers||[]}_roomLqi(e){if(!e)return null;const t=[];for(const i of this._renderDevices){if(i.area!==e||i.virtual)continue;const n=Ch(this._renderPlanHass,i.entities);null!=n&&t.push(n)}return Mi(t)}_roomBounds(e){if(e.poly&&e.poly.length){const t=e.poly.map(e=>e[0]),i=e.poly.map(e=>e[1]),n=Math.min(...t),r=Math.min(...i);return{x:n,y:r,w:Math.max(...t)-n,h:Math.max(...i)-r}}return{x:e.x??0,y:e.y??0,w:e.w??0,h:e.h??0}}_defaultPositions(){const e={},t=this._config?.icon_size??2.5;for(const i of this._model){const n=t/100*el(i)*1.3;for(const t of i.rooms){if(!t.area)continue;const r=this._devices.filter(e=>e.area===t.area&&e.space===i.id);if(!r.length)continue;const o=this._roomBounds(t),s=.1*Math.min(o.w,o.h),a=o.w-2*s,l=o.h-2*s,c=Math.max(1,Math.round(Math.sqrt(r.length*a/Math.max(l,1)))),h=Math.ceil(r.length/c),d=a/c,u=l/Math.max(h,1),p=r.map((e,t)=>({x:o.x+s+d*(t%c+.5),y:o.y+s+u*(Math.floor(t/c)+.5)}));Ri(p,o,n,.5*s),r.forEach((t,i)=>e[t.id]=La(p[i]))}}return e}_pos(e){const t=this._renderDeviceSnapshot?.positions.get(e.id);return t?{x:t.x,y:t.y}:this._livePos(e)}_livePos(e){const t=this._spaceModelById(e.space),i=this._layout[e.id];if(i)if(this._norm){if(i.s===e.space)return{x:i.x*cm,y:i.y*cm}}else if(void 0===i.s)return{x:i.x,y:i.y};return this._defPos[e.id]?this._defPos[e.id]:t?La(Qa(t)):{x:500,y:500}}_savePos(e,t,i){if(this._spaceModelById(e.space)){if(this._norm){const n=this._gridPitch,r=Math.round(t/n)*n,o=Math.round(i/n)*n,s=this._layout[e.id]?.k;this._layout={...this._layout,[e.id]:{s:e.space,x:qa(r/cm),y:qa(o/cm),...s?{k:s}:{}}}}else this._layout={...this._layout,[e.id]:{x:Math.round(t),y:Math.round(i)}};this._dirtyPos.add(e.id),this._persistLayout()}}_visualSamples(e){return Cu(this._renderPlanHass,e,this._renderDevices,void 0,this._fullRegistryHass).samples}_devicePresentation(e,t=!1!==this._config?.show_signal,i=!1){const n=i?null:this._renderDeviceSnapshot?.presentations.get(Nu(e.id,t));return n||Pu(this._renderPlanHass,e,{liveStates:!1!==this._config?.live_states,showTemperature:!1!==this._config?.show_temperature,showSignal:t,designPreview:i,activityRuntime:this._activityRt.get(e.id),sourceDetails:!1,lightDevices:this._renderDevices,registryHass:this._fullRegistryHass,reducedMotion:this._reducedMotion})}_deviceVisual(e){return this._devicePresentation(e).visual}_stateClass(e,t=this._deviceVisual(e)){const i=this._devicePresentation(e);return i.effectiveHidden?"":("icon_ripple"!==i.display||!1===this._config?.live_states||"alarm"===t.status||t.activity,zu({...i,visual:t}).join(" "))}_liveTemp(e){if(!this._config?.show_temperature)return null;if(!0===e.marker?.use_climate_temp){const t=Ah(this._renderPlanHass,e.entities);if(null!=t)return t}return"mdi:thermometer"!==e.icon&&"mdi:air-filter"!==e.icon?null:Dh(this._renderPlanHass,e.entities)}_bindingEntities(e){if("virtual"===e||!e)return[];const t=this._bindingStatus(e);return"active"===t.kind?t.enabledEntityIds:t.allEntityIds}_bindingHasClimate(e){return this._bindingEntities(e).some(e=>e.startsWith("climate."))}_bindingHasAlarm(e){return this._bindingEntities(e).some(e=>{const t=this._planHass?.states?.[e]||this.hass?.states?.[e],i=this._fullRegistryHass.entities[e]||this.hass?.entities?.[e];return In(e.split(".")[0],t?.attributes?.device_class||i?.device_class||i?.original_device_class)})}_liveHum(e){return this._config?.show_temperature&&e.primary&&Oh(this.hass,e.primary)?zh(this.hass,e.entities):null}_deviceBindingActive(e,t=!0){if(e.virtual||"virtual"===e.bindingKind)return!0;if(!e.bindingKind||!e.bindingRef)return!1;const i=this._bindingStatus(`${e.bindingKind}:${e.bindingRef}`);return"active"===i.kind||(t&&this._showToast(this._t("ha_disabled"===i.kind?"toast.ha_disabled_action":"toast.ha_binding_unverified")),!1)}_openMoreInfo(e){e?this._planEntityAvailable(e)?(this._nativeMoreInfoEntity=e,pm(this,"hass-more-info",{entityId:e})):this._showToast(this._t("toast.ha_disabled_action")):this._showToast(this._t("toast.no_entity"))}_interruptViewGesture(e,t){if(clearTimeout(this._holdTimer),clearTimeout(this._kioskHoldTimer),void 0!==e)for(const i of[t,this._stageEl])try{i?.hasPointerCapture?.(e)&&i.releasePointerCapture(e)}catch{}this._pointers.clear(),this._panStart=null,this._panLock=null,this._pinchStart=null,this._swipeStart=null}_closeInfoCard(){this._interruptViewGesture(),this._holdFired=!1,this._infoCard=null}_ctxDevice(e,t){"view"===this._mode&&(e.preventDefault(),e.stopPropagation(),this._deviceBindingActive(t)&&(t.primary?this._openMoreInfo(t.primary):this._infoCard=t))}_clickDevice(e,t){if(e.stopPropagation(),this._drag?.moved||this._suppressClick||this._holdFired)return;if("plan"===this._mode)return;if("devices"===this._mode)return void this._openMarkerDialog(t);const i=this._devices.find(e=>e.id===t.id);if(!i)return;const n=_d(i.tapAction,i.primary?.split(".")[0]),r=(e,t)=>{i.marker?.tap_confirm?this._tapConfirm={kind:"run",text:e,exec:t}:t()};if("toggle"===n){const e=this._toggleIntent(i);if(!e)return;if(!bd(e))return void this._showUnavailableToggleTargets(e);const t=e=>{const t=bd(e);if(!t)return;if("virtual-light"===t.kind)return this._startDevicePressFeedback(i.id),void this.hass.callWS({type:"houseplan/virtual_light/toggle",marker_id:t.markerId}).then(e=>{const t=Fc(this._virtualLights,e);t!==this._virtualLights&&(this._virtualLights=t,this._capturedSnapshotVirtual="",this._cacheSnapshot(),this.requestUpdate())}).catch(e=>this._showToast(this._t("toast.virtual_light_toggle_failed",{err:this._errText(e)})));const{command:n}=t;this._startDevicePressFeedback(i.id),this.hass.callService(n.domain,n.service,n.data).catch(e=>this._showToast(this._t("toast.error",{err:this._errText(e)})))},n=((o=e).targets.length?o.targets:o.skippedTargets).map(e=>e.name||e.entityId||e.ref).join(", ")||i.name;if(i.marker?.tap_confirm){const r=this._toggleConfirmationLines(e);if(!r.length)return;this._tapConfirm={kind:"toggle",text:this._t("confirm.tap_toggle",{name:n}),lines:r,initialIntent:e,deviceId:i.id,exec:()=>{const n=this._devices.find(e=>e.id===i.id),r=n?this._toggleIntent(n):null;r&&!bd(r)&&this._showUnavailableToggleTargets(r)||(r&&xd(e,r)?t(r):this._showToast(this._t("toast.tap_target_changed")))}}}else t(e);return}var o;if("info"===n)return this._interruptViewGesture(),void(this._infoCard=i);if(this._deviceBindingActive(i)){if("run"===n){const e=i.marker?.tap_target||"",t=function(e){const t=String(e||"").split(".")[0];return"automation"===t?{domain:"automation",service:"trigger"}:"script"===t?{domain:"script",service:"turn_on"}:"scene"===t?{domain:"scene",service:"turn_on"}:null}(e),n=this.hass.states[e];if(!t||!n)return void this._showToast(this._t("toast.run_target_missing"));const o=n.attributes?.friendly_name||e;return void r(this._t("confirm.tap_run",{name:o}),()=>{this._deviceBindingActive(i)&&this._planEntityAvailable(e)&&(this._startDevicePressFeedback(i.id),this.hass.callService(t.domain,t.service,{entity_id:e}).then(()=>{this._stampActivity(i.id,"event",this._activitySourceKey(i)),this.requestUpdate(),this._showToast(this._t("toast.run_started",{name:o}))}).catch(e=>this._showToast(this._t("toast.error",{err:this._errText(e)}))))})}"more-info"===n&&i.primary?this._openMoreInfo(i.primary):this._infoCard=i}}_showUnavailableToggleTargets(e){const t=function(e){if(!e||"group"!==e.kind||bd(e)||"configured-targets-missing"!==e.noneReason)return[];if(!e.skippedTargets.length||e.skippedTargets.some(e=>!wd.has(e.reason)&&"secure"!==e.reason))return[];const t=[],i=new Set;for(const n of e.skippedTargets){if(!wd.has(n.reason))continue;const e=String(n.name||n.entityId||n.ref||"").trim();e&&!i.has(e)&&(i.add(e),t.push(e))}return t}(e);return!!t.length&&(this._showToast(1===t.length?this._t("toast.toggle_target_unavailable",{name:t[0]}):this._t("toast.toggle_targets_unavailable",{names:t.join(", ")})),!0)}_keyDevice(e,t){"Enter"!==e.key&&" "!==e.key||"view"!==this._mode&&"devices"!==this._mode||(e.preventDefault(),this._clickDevice(e,t))}_startDevicePressFeedback(e){const t=[...this.renderRoot.querySelectorAll(".dev[data-id]")].find(t=>t.dataset.id===e),i=t?.querySelector(".device-shell");if(!i||"function"!=typeof i.animate)return;const n=Number.parseFloat(this.ownerDocument.defaultView?.getComputedStyle(i).scale||""),r=Number.isFinite(n)?Math.max(.95,Math.min(1,n)):1;this._devicePressAnimations.get(e)?.cancel();const o=this._reducedMotion?[{outlineColor:"transparent",outlineStyle:"solid",outlineWidth:"0px"},{outlineColor:"var(--hp-accent, #3ea6ff)",outlineStyle:"solid",outlineWidth:"2px",outlineOffset:"2px",offset:.5},{outlineColor:"transparent",outlineStyle:"solid",outlineWidth:"0px"}]:[{scale:String(r)},{scale:"0.95",offset:.5},{scale:"1"}],s=i.animate(o,{duration:200,easing:"cubic-bezier(.22,.61,.36,1)"});this._devicePressAnimations.set(e,s);const a=()=>{this._devicePressAnimations.get(e)===s&&this._devicePressAnimations.delete(e)};s.addEventListener("finish",a,{once:!0}),s.addEventListener("cancel",a,{once:!0})}_cancelDevicePressFeedback(){for(const e of this._devicePressAnimations.values())e.cancel();this._devicePressAnimations.clear()}_pruneDevicePressFeedback(){if(!this._devicePressAnimations.size)return;const e=new Set([...this.renderRoot.querySelectorAll(".dev[data-id]")].map(e=>e.dataset.id||""));for(const[t,i]of this._devicePressAnimations)e.has(t)||(i.cancel(),this._devicePressAnimations.delete(t))}_t(e,t){return Jd(Xd(this.hass,this._config?.language),e,t)}get _colorPickerLabels(){return{title:this._t("color_picker.title"),hue:this._t("color_picker.hue"),saturation:this._t("color_picker.saturation"),value:this._t("color_picker.value"),hex:this._t("color_picker.hex"),invalidHex:this._t("color_picker.invalid_hex")}}_help(e){return this._editorRuntimeOrThrow()._help(e)}get _stageEl(){return this.renderRoot.querySelector(".stage")}_contentItems(e){const t=[];for(const i of this._devices){if(i.space!==e.id||i.hidden)continue;const n=this._pos(i);t.push({minX:n.x,minY:n.y,maxX:n.x,maxY:n.y})}if(e.id===this._space){for(const e of this._openingsR){const i=Number(e.angle)*Math.PI/180,n=Math.cos(i)*e.rlen/2,r=Math.sin(i)*e.rlen/2,o=Ua([[e.rx-n,e.ry-r],[e.rx+n,e.ry+r]]);o&&t.push(o)}const i=this._decorH;for(const e of this._decorList){const n=Ua("line"===e.kind?[[e.x1*cm,e.y1*i],[e.x2*cm,e.y2*i]]:"text"===e.kind?[[e.x*cm,e.y*i]]:wr({x:e.x*cm,y:e.y*i,w:e.w*cm,h:e.h*i,angle:e.angle}));n&&t.push(n)}for(const i of this._physicalBodiesR(e)){const e=Ua(i);e&&t.push(e)}}return Va(e,t)}_frameOf(){const e=this._spaceModel();if(!e){this._frame=null;const e={x:0,y:0,w:cm,h:cm};return{rect:e,all:e,outliers:0}}const t=this._frame,i="view"!==this._mode;if(t&&t.id===e.id&&this._bdDrag)return t;if(t&&t.id===e.id&&t.model===e&&t.layout===this._layout&&t.devs===this._devices&&t.far===this._showFar&&t.grow===i)return t;const n=Xa(this._contentItems(e));let r=n.all||Ja(e),o=this._showFar?r:n.core||Ja(e);return t&&t.id===e.id&&i&&t.grow&&(o=dm(t.rect,o),r=dm(t.all,r)),this._frame={id:e.id,model:e,layout:this._layout,devs:this._devices,far:this._showFar,grow:i,rect:o,all:r,outliers:n.outliers},this._frame}_isoSource(){const e=this._spaceModel();if(!e)return null;const t=this._spaceWalls,i=this._openCuts(),n=this._openingsR.map((e,t)=>({id:String(e.id||t),sourceIndex:t,type:e.type,x:e.rx,y:e.ry,angle:Number(e.angle)||0,length:e.rlen>0?e.rlen:900,flipH:!!e.flip_h,flipV:!!e.flip_v})),r=xa(64,this._cellCm),o=xa(10,this._cellCm),s=`${e.id}|${function(e){let t=2166136261;const i=e=>{if("number"!=typeof e)if("string"!=typeof e)if("boolean"!=typeof e)if(Array.isArray(e)){t=O_(t,e.length);for(const t of e)i(t)}else{if(e&&"object"==typeof e){const t=e;for(const e of Object.keys(t).sort())i(e),i(t[e]);return}t=O_(t,-1)}else t=O_(t,e?1:0);else for(const i of e)t=O_(t,i.codePointAt(0)||0);else t=O_(t,e)};return i(e),t.toString(36)}({rooms:e.rooms,walls:t,openCuts:i,openings:n,partitions:e.partitions,roomDrafts:e.room_drafts,columns:e.wall_columns,cellCm:this._cellCm,gridPitch:this._gridPitch,wallKeyPitch:this._wallKeyPitch,camera:b_,wallHeight:r,floorEdgeHeight:o,algorithm:3})}`;return{key:s,build:()=>{const o=ca(e,this._cellCm,this._gridPitch,2e-4*this._gridPitch,this._partitionOpeningCuts(e)).all,s=this._roomWallOpeningInputs(this._openingsR,e),a=t.length||o.length?Ns(e.rooms,t,i,s,this._wallKeyPitch,this._cellCm,this._gridPitch,cm,o):null;if(a&&("failed-core"===a.status||"not-applicable"===a.status))throw new Error("wall boolean geometry failed");const l=a?.paperGeom??Ps(e.rooms,t,i,this._wallKeyPitch,this._cellCm,this._gridPitch,cm);if(!l)throw new Error("floor boolean geometry failed");const c=a?.openingIndex||Bs(e.rooms,t,i,this._wallKeyPitch,this._cellCm,this._gridPitch,cm),h=n.map(e=>{const i=this._openingsR[e.sourceIndex],n="gate"===e.type?!e.flipV:e.flipV,o=i?.partitionHost?Od(i.partitionHost,n):t.length||"gate"===e.type?Vs(c,{x:e.x,y:e.y,angle:e.angle,length:e.length,flip_v:n}):{ox:0,oy:0,cm:0,side:-1};return function(e,t=64){if(!(z_([e.x,e.y,e.angle,e.length,e.face.ox,e.face.oy,t])&&e.length>0&&t>0))throw new Error("invalid isometric opening input");const i=e.length/2;let n;if("passage"===e.type)n=[];else if("gate"===e.type){const r=10*e.face.side;n=[F_(e,0,[-i,0],[i,0],r,0,.88*t),F_(e,1,[i,0],[-i,0],-r,0,.88*t)]}else n="window"===e.type?[F_(e,0,[-i,0],[i,0],-90,.27*t,.78*t),F_(e,1,[i,0],[-i,0],90,.27*t,.78*t)]:[F_(e,0,[-i,0],[e.length,0],-90,0,.92*t)];return{id:e.id,sourceIndex:e.sourceIndex,type:e.type,leaves:Object.freeze(n.map(e=>Object.freeze(e)))}}({...e,face:o},r)});return{walls:a?.components.flatMap(e=>e.geom)||[],floor:l,openings:Object.freeze(h)}}}}_isoSceneKey(){if(!this._labsIso||"view"!==this._mode)return null;try{return this._isoSource()?.key??null}catch{return`${this._space}|invalid`}}_isoScene(){const e=this._isoSource();if(!e)return null;const t=this._isoGeometryCache.get(e.key);if(t)return{key:e.key,...t};const i=this._frameOf().rect,n=e.build(),r=xa(64,this._cellCm),o=xa(10,this._cellCm),s=function(e){let t=1/0,i=1/0,n=-1/0,r=-1/0;for(const o of e)for(const e of o.leaves){const o=Math.hypot(e.closedVector[0],e.closedVector[1]);t=Math.min(t,e.hinge[0]-o),i=Math.min(i,e.hinge[1]-o),n=Math.max(n,e.hinge[0]+o),r=Math.max(r,e.hinge[1]+o)}return z_([t,i,n,r])?{x:t,y:i,w:n-t,h:r-i}:null}(n.openings),a=S_({rect:s?dm(i,s):i,wallHeight:r,openingHeight:r,floorDepth:o}),l=function(e,t=b_,i=64){if(!Number.isFinite(i)||i<0)throw new Error("invalid wall height");const n=[],r=[],o=[];let s=0;for(let a=0;a<(e||[]).length;a++){const l=e[a];for(let e=0;e<(l||[]).length;e++){const c=R_(l[e],e>0);if(!(c.length<3||Math.abs(T_(c))<1e-9)){n.push(C_(c,t,i)),s+=c.length;for(let n=0;ne.depth-t.depth||e.polygon-t.polygon||e.ring-t.ring||e.edge-t.edge),{topPath:n.join(" "),sides:r,contactPath:o.join(" "),edgeCount:s}}(n.walls,b_,r),c={geometry:l,floor:A_(n.floor,o),openings:n.openings,frame:a};return J_(this._isoGeometryCache,e.key,c,8),{key:e.key,...c}}_effectiveProjection(){if("iso"!==this._desiredProjection||!this._model.length)return"flat";const e=this._isoSceneKey()||`${this._space}|invalid`;if(this._isoFallback.has(e))return"flat";try{const e=this._isoScene();return e?(t="iso",i=e.key,n=this._isoFallback,"iso"===t&&i&&!n.has(i)?"iso":"flat"):"flat"}catch(t){if(!this._isoFallback.has(e)){this._isoFallback.add(e);const i=e.split("|");console.warn(`HOUSEPLAN ISO FALLBACK: #89, space ${this._space}, fingerprint ${i[i.length-1]}, ${t instanceof Error?t.message:"renderer error"}`)}return"flat"}var t,i,n}_scenePoint(e){return"iso"===this._renderProjection?k_(e,0):e}_floorView(e){if("iso"!==this._renderProjection)return e;const t=x_([e.x,e.y]),i=x_([e.x+e.w,e.y+e.h]);return{x:t[0],y:t[1],w:i[0]-t[0],h:i[1]-t[1]}}_baseVb(){if("iso"===this._effectiveProjection()){if(!this._spaceDisplayForRender().showBorders){const e=S_({rect:this._frameOf().rect,wallHeight:xa(64,this._cellCm)});return[e.x,e.y,e.w,e.h]}const e=this._isoScene()?.frame??this._frameOf().rect;return[e.x,e.y,e.w,e.h]}const e=this._frameOf().rect;return[e.x,e.y,e.w,e.h]}get _outliers(){return this._showFar?0:this._frameOf().outliers}_fitFar(){this._showFar=!0,this._frame=null,this._resetZoom()}_fitAll(){this._showFar=!0,this._frame=null,this._resetZoom()}_renderFarHint(){return this._kiosk||"view"!==this._mode||this._booting||!this._outliers?G:B`
+ + ${this._t("canvas.far_objects",{n:this._outliers})} + +
`}_renderHomeArrow(){if(this._booting)return G;const e=this._view;if(!e||!e.w||!e.h)return G;const t=this._baseVb(),i=t[0],n=t[1],r=t[2],o=t[3];if(!(i+r<=e.x||i>=e.x+e.w||n+o<=e.y||n>=e.y+e.h))return G;const s=Math.atan2(n+o/2-(e.y+e.h/2),i+r/2-(e.x+e.w/2)),a=50+38*Math.cos(s),l=50+38*Math.sin(s);return B``}_stageAspect(){const e=this._stageEl,t=this._baseVb();return e&&e.clientHeight?e.clientWidth/e.clientHeight:t[2]/t[3]}_viewOr(e){return this._view&&this._view.w?this._view:Ti(e,this._stageAspect())}_roomLabelReferenceViewWidth(e){if(!this._markup)return e.w;const t=this._stageEl,i=this.renderRoot.querySelector(".editorchrome");if(!t||t.clientWidth<=0||t.clientHeight<=0)return e.w;const n=t.clientHeight+(i?.getBoundingClientRect().height||0);return n<=0?e.w:this._viewForModeTarget(this._zoom,void 0,void 0,t.clientWidth,n).w}_screenToVb(e,t){const i=this._stageEl,n=this._viewOr(this._baseVb()),r=i?.clientWidth||1,o=i?.clientHeight||1;return[n.x+e/r*n.w,n.y+t/o*n.h]}_clampView(e,t){const i=(e,t,i,n)=>{const r=1*Math.max(t,n),o=i-r,s=i+n-t+r;return Math.max(Math.min(o,s),Math.min(Math.max(o,s),e))};return{w:e.w,h:e.h,x:i(e.x,e.w,t.x,t.w),y:i(e.y,e.h,t.y,t.h)}}_applyView(e,t,i){const n=this._baseVb(),r=Ti(n,this._stageAspect()),o=Math.min(fm.ZOOM_MAX,Math.max(fm.ZOOM_MIN,e)),s=r.w/o,a=r.h/o,l=this._viewOr(n),c=t??l.x+l.w/2,h=i??l.y+l.h/2,d=this._clampView({x:c-s/2,y:h-a/2,w:s,h:a},r);return!(this._view&&Math.abs(this._zoom-o)<1e-9&&Math.abs(this._view.x-d.x)<1e-6&&Math.abs(this._view.y-d.y)<1e-6&&Math.abs(this._view.w-d.w)<1e-6&&Math.abs(this._view.h-d.h)<1e-6)&&("opening"===this._tool&&(this._cursorPt=null,this._clearOpeningPlacement(!1)),this._zoom=o,this._view=d,!0)}_bootWatch(){clearTimeout(this._bootTimer),this._bootStart=Date.now(),this._bootLastH=-1,this._bootLastChange=this._bootStart;const e=()=>{if(!this._booting)return;const t=Date.now(),i=this._stageEl?this._stageEl.clientHeight:0;i!==this._bootLastH&&(this._bootLastH=i,this._bootLastChange=t);const n=t-this._bootStart;n>=1200||n>=700&&i>0&&t-this._bootLastChange>=250?this._bootSettled():this._bootTimer=window.setTimeout(e,100)};this._bootTimer=window.setTimeout(e,100)}_bootSettled(){this._booting&&!this._bootSettling&&(this._bootSettling=!0,this._refitView(),this._bootSettleRaf=requestAnimationFrame(()=>{this._bootSettleRaf=0,this._finishBootSettled()}))}_finishBootSettled(){if(!this._booting)return;this._refitView(),this._booting=!1,this._bootSettling=!1;const e=this._stageEl?.clientHeight??0;!this._config?.kiosk&&e>0&&this._warmPatch({hdrH:this._hdrH,stageH:e,vp:this._warmViewportState()},!0),this._bootFading=!0,this._bootTimer=window.setTimeout(()=>{this._bootFading=!1},220),this._bootSoft=!0,clearTimeout(this._bootSoftTimer),this._bootSoftTimer=window.setTimeout(()=>{this._bootSoft=!1},Z_)}_bootSoftCancel(){this._bootSoft&&(clearTimeout(this._bootSoftTimer),this._bootSoft=!1)}_beginResumeSettle(){this._booting||this._resumeSettling||(this._modeTransitionPreparing?this._modeTransitionForceAtomic=!0:this._cancelModeTransition(!0),this._viewportInvalidAt=0,this._beginContinuityCandidate("warm-resume",!1),this._loading?this.requestUpdate():this._loadFromServer())}_refitView(){if(this._modeTransitionBusy)return;const e=this._stageEl;if(!e||"visible"!==document.visibilityState||e.clientWidth<=0||e.clientHeight<=0)return void(this._viewportInvalidAt||(this._viewportInvalidAt=Date.now()));const t=[e.clientWidth,e.clientHeight],i=this._lastValidStageSize,n=!!i&&Math.abs(i[0]-t[0])<=.5&&Math.abs(i[1]-t[1])<=.5,r=this._viewportInvalidAt?Date.now()-this._viewportInvalidAt:0;if(this._viewportInvalidAt=0,!i)return this._lastValidStageSize=t,void(this._view||this._applyView(this._zoom));n?this._pendingRefitSize=null:(this._pendingRefitSize=t,this._refitRaf||(this._refitRaf=requestAnimationFrame(()=>{this._refitRaf=0;const e=this._pendingRefitSize;this._pendingRefitSize=null;const t=this._stageEl;if(!e||!t||t.clientWidth<=0||t.clientHeight<=0)return;if(Math.abs(t.clientWidth-e[0])>.5||Math.abs(t.clientHeight-e[1])>.5)return void this._refitView();const i=this._lastValidStageSize;if(i&&Math.abs(i[0]-e[0])<=.5&&Math.abs(i[1]-e[1])<=.5)return;this._lastValidStageSize=e;const n=this._view;r>=wl?this._beginContinuityCandidate("stage-size-restored",!0,"stage-size"):this._continuity.hasCompleteFrame&&this._beginContinuityCandidate("stage-resize",!0,"stage-size"),this._applyView(this._zoom,n?n.x+n.w/2:void 0,n?n.y+n.h/2:void 0)})))}_zoomAt(e,t,i){const n=this._stageEl;if(!n)return;const r=Ti(this._baseVb(),this._stageAspect()),o=Math.min(fm.ZOOM_MAX,Math.max(fm.ZOOM_MIN,i));"opening"===this._tool&&(this._cursorPt=null,this._clearOpeningPlacement(!1));const s=n.clientWidth,a=n.clientHeight,l=this._screenToVb(e,t),c=r.w/o,h=r.h/o;this._zoom=o,this._view=this._clampView({x:l[0]-e/s*c,y:l[1]-t/a*h,w:c,h:h},r)}_onWheel(e){const t=this._stageEl;if(!t)return;e.preventDefault();const i=t.getBoundingClientRect(),n=e.deltaY<0?1.15:1/1.15;this._zoomAt(e.clientX-i.left,e.clientY-i.top,this._zoom*n),this._saveZoom()}_stepZoom(e){const t=this._stageEl;t&&(this._zoomAt(t.clientWidth/2,t.clientHeight/2,this._zoom*(e>0?1.4:1/1.4)),this._saveZoom())}_resetZoom(){const e=this._baseVb();"opening"===this._tool&&(this._cursorPt=null,this._clearOpeningPlacement(!1)),this._zoom=1,this._view=Ti(e,this._stageAspect()),this._saveZoom()}_saveZoom(){if("view"===this._mode){this._zoomBySpace={...this._zoomBySpace,[this._space]:this._zoom};try{localStorage.setItem(om,JSON.stringify(this._zoomBySpace))}catch{}}}_restoreZoom(){const e=this._zoomBySpace[this._space]||1;this._zoom=e;const t=this._stageEl;if(t&&t.clientHeight){const t=this._baseVb();return this._applyView(e,t[0]+t[2]/2,t[1]+t[3]/2),void this.requestUpdate()}this._view=null,requestAnimationFrame(()=>{if(!this._stageEl)return;const t=this._baseVb();this._applyView(e,t[0]+t[2]/2,t[1]+t[3]/2),this.requestUpdate()})}_stagePointerDown(e){if(this._vacFit)return;if(this._kiosk&&(this._cyclePausedUntil=Date.now()+6e4,0===this._pointers.size?(this._swipeStart={x:e.clientX,y:e.clientY,id:e.pointerId},e.target.closest?.(".dev, .roomlabel, .oplock")||(clearTimeout(this._kioskHoldTimer),this._kioskHoldTimer=window.setTimeout(()=>{this._kioskDialog=!0,this._swipeStart=null},3e3))):(this._swipeStart=null,clearTimeout(this._kioskHoldTimer))),this._drag)return;if(this._markup&&e.target.closest?.(".roomlabel, .rlhandle, .rszhandle, .dev, .oplock, .op-hit, button"))return;if("devices"===this._mode&&e.target.closest(".dev"))return;if("decor"===this._mode&&this._decorPointerDown(e))return;this._pointers.set(e.pointerId,{x:e.clientX,y:e.clientY});const t=this._viewOr(this._baseVb());if(1===this._pointers.size)this._panStart={sx:e.clientX,sy:e.clientY,vx:t.x,vy:t.y},this._panLock=null,this._suppressClick=!1;else if(2===this._pointers.size){"opening"===this._tool?(this._cursorPt=null,this._clearOpeningPlacement(!1)):"draw"===this._tool&&this._clearPlanSnapHover();const e=[...this._pointers.values()],t=Math.hypot(e[0].x-e[1].x,e[0].y-e[1].y);this._pinchStart={dist:t,zoom:this._zoom},this._panStart=null,this._panLock=null}}get _swipeZone(){return!this._hasFixedFloor&&this._kiosk&&this._zoom<=1.001&&this._model.length>1}_stagePointerMove(e){if(this._physicalRotate?.pid!==e.pointerId)if(this._physicalDrag?.pid!==e.pointerId)if(this._dtDrag?.pid!==e.pointerId)if(this._bdDrag?.pid!==e.pointerId){if(this._decorDraft?.pid===e.pointerId){const t=this._decorDraft;let i=this._decorSnap(this._svgPoint(e),e.pointerType);if(e.shiftKey&&("rect"===t.kind||"ellipse"===t.kind)){const e=i[0]-t.a[0],n=i[1]-t.a[1],r=Math.max(Math.abs(e),Math.abs(n));i=this._snap([t.a[0]+(e<0?-r:r),t.a[1]+(n<0?-r:r)])}return void(this._decorDraft={...t,b:i})}if(this._decorMove?.pid!==e.pointerId)if(this._pointers.has(e.pointerId)){if(this._pointers.set(e.pointerId,{x:e.clientX,y:e.clientY}),this._markup&&1===this._pointers.size&&this._markupMove(e),this._pinchStart&&this._pointers.size>=2){"opening"===this._tool?(this._cursorPt=null,this._clearOpeningPlacement(!1)):"draw"===this._tool&&this._clearPlanSnapHover();const e=[...this._pointers.values()],t=Math.hypot(e[0].x-e[1].x,e[0].y-e[1].y)/(this._pinchStart.dist||1),i=this._stageEl.getBoundingClientRect(),n=(e[0].x+e[1].x)/2-i.left,r=(e[0].y+e[1].y)/2-i.top;this._zoomAt(n,r,this._pinchStart.zoom*t),this._suppressClick=!0,this._saveZoom()}else if(this._panStart){const t=e.clientX-this._panStart.sx,i=e.clientY-this._panStart.sy;Math.abs(t)+Math.abs(i)>4&&(this._suppressClick=!0,clearTimeout(this._holdTimer),"opening"===this._tool?(this._cursorPt=null,this._clearOpeningPlacement(!1)):"draw"===this._tool&&this._clearPlanSnapHover()),null===this._panLock&&Math.abs(t)+Math.abs(i)>8&&(this._panLock=this._swipeZone&&Math.abs(t)>1.5*Math.abs(i)?"swipe":"pan");const n=this._stageEl;if("pan"===this._panLock&&n){const e=this._baseVb(),r=this._viewOr(e),o=Ti(e,this._stageAspect());this._view=this._clampView({x:this._panStart.vx-t/(n.clientWidth||1)*r.w,y:this._panStart.vy-i/(n.clientHeight||1)*r.h,w:r.w,h:r.h},o)}}}else this._markupMove(e);else this._decorMoveUpdate(e)}else this._bdMove(e);else this._dtMove(e);else this._physicalMove(e);else this._physicalRotateMove(e)}_stagePointerLeave(e){this._markup&&("opening"===this._tool?(this._cursorPt=null,this._clearOpeningPlacement(!1)):"draw"===this._tool&&this._clearPlanSnapHover())}_stagePointerUp(e){if(this._kiosk){clearTimeout(this._kioskHoldTimer);const t=this._swipeStart;if(this._swipeStart=null,t&&t.id===e.pointerId){const i=e.clientX-t.x,n=e.clientY-t.y;if(Math.abs(i)+Math.abs(n)<8){const e=Date.now();e-this._lastTap<350&&this._resetZoom(),this._lastTap=e}const r="pan"===this._panLock?null:function(e,t,i,n,r,o=60){if(i>1.001||n.length<2)return null;if(Math.abs(e)e.id),this._space);r&&this._slideTo(r,i<0?"left":"right")&&(this._saveNav(),this._suppressClick=!0,setTimeout(()=>this._suppressClick=!1,0),this._showKioskDots())}}if(this._physicalDrag?.pid===e.pointerId)return void this._physicalUp(e);if(this._physicalRotate?.pid===e.pointerId)return void this._physicalRotateUp(e);if(this._dtDrag?.pid===e.pointerId)return void this._dtUp();if(this._bdDrag?.pid===e.pointerId)return void this._bdUp();if(this._decorDraft?.pid===e.pointerId)return void this._decorCommitDraft();if(this._decorMove?.pid===e.pointerId)return this._decorMove.moved&&(this._recordGeometry(this._t("history.decor_move"),this._decorMove.before),this._saveConfig()),void(this._decorMove=null);const t=!!this._pinchStart||!!this._panStart;this._pointers.delete(e.pointerId),this._pointers.size<2&&(this._pinchStart=null),0===this._pointers.size&&(this._panStart=null,this._panLock=null,setTimeout(()=>this._suppressClick=!1,0)),t&&0===this._pointers.size&&this.requestUpdate()}_clickRoom(e){!this._suppressClick&&e.area&&_m("/config/areas/area/"+e.area)}_pointerDown(e,t){if("plan"===this._mode)return;if("view"===this._mode){this._holdFired=!1,clearTimeout(this._holdTimer);const i=e.pointerId,n=e.currentTarget;return void(this._holdTimer=window.setTimeout(()=>{this._holdFired=!0,this._interruptViewGesture(i,n),this._infoCard=t},600))}if("ha_disabled"===t.bindingStatus?.kind)return;e.preventDefault();const i=this._pos(t);this._drag={id:t.id,sx:e.clientX,sy:e.clientY,ox:i.x,oy:i.y,moved:!1},gm(e),this._tip=null}_pointerMove(e,t){if(!this._drag||this._drag.id!==t.id)return;const i=this.renderRoot.querySelector(".stage");if(!i)return;const n=this._baseVb(),r=i.getBoundingClientRect(),o=this._viewOr(n),s=(e.clientX-this._drag.sx)/r.width*o.w,a=(e.clientY-this._drag.sy)/r.height*o.h;Math.abs(e.clientX-this._drag.sx)+Math.abs(e.clientY-this._drag.sy)>3&&(this._drag.moved=!0,clearTimeout(this._holdTimer));const l=ja(this._drag.ox+s),c=ja(this._drag.oy+a);this._savePos(t,l,c)}_pointerUp(e,t){if(clearTimeout(this._holdTimer),!this._drag||this._drag.id!==t.id)return;const i=this._drag.moved;this._drag=i?this._drag:null,i&&(this._selId=t.id,window.setTimeout(()=>this._drag=null,0))}_showToast(e){for(const e of this.renderRoot.querySelectorAll("hp-dialog"))e.closeTransientOverlays("toast");this._toast=e,clearTimeout(this._toastTimer),this._toastTimer=window.setTimeout(()=>{this._toast=""},3500)}_syncPointerHoverTargets(){const e=this._pointerModality.hoverEnabled;for(const t of this.renderRoot.querySelectorAll(lm))t.toggleAttribute("data-pointer-hover",e)}_syncPointerHoverSubtree(e){if(e.nodeType!==Node.ELEMENT_NODE)return;const t=e,i=this._pointerModality.hoverEnabled;t.matches(lm)&&t.toggleAttribute("data-pointer-hover",i);for(const e of t.querySelectorAll(lm))e.toggleAttribute("data-pointer-hover",i)}_clearTransientHover(e=!1){e&&this._pointerModality.suspend(),this._tip&&(this._tip=null),this._hoverRoom&&(this._hoverRoom=null)}_notePointer(e){const t=this._pointerModality.modality,i=this._pointerModality.note(e);"touch"!==i&&"pen"!==i||t===i&&!this._tip&&!this._hoverRoom||this._clearTransientHover()}_guardTouchGesture(e){if(this._editorSecondary?.handleOutsideDismiss(e))return;if("click"===e.type){if(!this._suppressClick&&!this._touchSequenceMultitouch&&Date.now()>this._touchClickBlockUntil)return;return e.preventDefault(),void e.stopImmediatePropagation()}const t=e;if(this._notePointer(t),"touch"===t.pointerType)if("pointerdown"!==e.type){if("pointermove"===e.type){const e=this._touchContacts.get(t.pointerId);if(!e)return;const i={...e,x:t.clientX,y:t.clientY};if(this._touchContacts.set(t.pointerId,i),this._touchSequenceMultitouch&&"view"===this._mode&&!this._vacFit&&this._pinchStart&&[...this._touchContacts.values()].every(e=>e.inStage)){this._pointers.set(t.pointerId,{x:t.clientX,y:t.clientY});const e=[...this._touchContacts.values()];if(e.length>=2&&this._stageEl){const[t,i]=e,n=Math.hypot(t.x-i.x,t.y-i.y)/(this._pinchStart.dist||1),r=this._stageEl.getBoundingClientRect();this._zoomAt((t.x+i.x)/2-r.left,(t.y+i.y)/2-r.top,this._pinchStart.zoom*n),this._saveZoom()}}return}if("pointerup"===e.type||"pointercancel"===e.type||"lostpointercapture"===e.type){if(this._clearTransientHover(),this._touchContacts.delete(t.pointerId),this._touchSequenceMultitouch){if(this._touchClickBlockUntil=Date.now()+500,this._pointers.delete(t.pointerId),!this._vacFit&&this._pointers.size>=2){const[e,t]=[...this._pointers.values()];this._pinchStart={dist:Math.hypot(e.x-t.x,e.y-t.y),zoom:this._zoom}}else this._pinchStart=null;0===this._pointers.size&&(this._panStart=null,this._panLock=null)}0===this._touchContacts.size&&(this._touchSequenceMultitouch=!1)}}else if(this._touchContacts.set(t.pointerId,{x:t.clientX,y:t.clientY,inStage:!!t.target?.closest?.(".stage")}),this._touchContacts.size>=2){this._clearTransientHover(),this._touchSequenceMultitouch=!0,this._touchClickBlockUntil=Number.POSITIVE_INFINITY,clearTimeout(this._holdTimer),clearTimeout(this._kioskHoldTimer),this._swipeStart=null;const e=[...this._touchContacts.values()];if("view"===this._mode&&!this._vacFit&&e.every(e=>e.inStage)){this._pointers=new Map([...this._touchContacts].map(([e,t])=>[e,{x:t.x,y:t.y}]));const[t,i]=e;this._pinchStart={dist:Math.hypot(t.x-i.x,t.y-i.y),zoom:this._zoom},this._panStart=null,this._panLock=null}else this._vacFit&&(this._pointers.clear(),this._pinchStart=null,this._panStart=null,this._panLock=null)}}_showTip(e,t,i,n,r,o){this._notePointer(e),this._pointerModality.hoverEnabled&&(this._drag||(this._tip={x:e.clientX,y:e.clientY,title:t,meta:i,lqi:n,temp:r,hum:o}))}get _gridPitch(){return Ea}get _cellCm(){const e=Number(this._curSpaceCfg?.cell_cm);return Number.isFinite(e)&&e>0?e:5}_fmtLen(e,t){const i=function(e,t,i,n){return Math.hypot(t[0]-e[0],t[1]-e[1])/i*n}(e,t,this._gridPitch,this._cellCm);return Kt(i,"mi"===this.hass?.config?.unit_system?.length)}get _curSpaceCfg(){const e=this._resize?.preview;return e&&e.space===this._space?e.sp:this._serverCfg?.spaces.find(e=>e.id===this._space)}get _renderCfg(){const e=this._resize?.preview;return e&&this._serverCfg?{...this._serverCfg,spaces:this._serverCfg.spaces.map(t=>t.id===e.space?e.sp:t)}:this._serverCfg}get _spaceH(){return this._curSpaceCfg,cm}get _segments(){const e=this._curSpaceCfg,t=this._spaceH;return Jt(e?.rooms||[]).map(e=>[e[0]*cm,e[1]*t,e[2]*cm,e[3]*t])}_savedNav(){if(this._hasFixedFloor)return null;try{return JSON.parse(localStorage.getItem(sm)||"null")}catch{return null}}_saveNav(){if(!this._hasFixedFloor)try{localStorage.setItem(sm,JSON.stringify({space:this._space}))}catch{}}_leaveCardRoute(){if(this._routeDepartureHandled)return;this._routeDepartureHandled=!0,"view"!==this._mode&&this._setMode("view",!1),this._pendingNavMode=null,this._geometryHistory.clear(),this._activeDraftId=null,this._resumeDraftBySpace={},this._draftSegmentCms=[],this._closingWallCm=null,this._drawWallField=null,this._showHidden=!1,this._tapConfirm=null,this._vacCalConfirm=null,this._decorEraseConfirm=null,this._openingInfo=null,this._closeInfoCard(),this._rulesDialog=null,this._alignDialog=null,this._preflightClipboardFallback=null,this._backupImportDialog=null,this._backupExportDialog=null,this._settingsDialog=null,this._deviceInbox=null,this._deviceInboxReturn=null,this._markerDialog=null,this._openingDialog=null,this._physicalDialog=null,this._wallDialog=null,this._backdropDialog=null,this._decorShapeDialog=null,this._decorTextDialog=null,this._mergeDialog=null,this._roomDialog=!1,this._spaceDialog=null,this._importDialog=null;const e=this._warmSlot;e?.owner===this._warmGen&&(e.vp=this._warmViewportState(),e.dlg=null,e.frameFingerprint="",clearTimeout(e.evict),e.evict=0),this._saveNav()}_setMode(e,t=!0){return this._editorRuntime?this._editorRuntime._setMode(e,t):"view"===e?void this._editorModeRequest++:void this._requestMode(e,t)}_primeDrawWallField(){return this._editorRuntimeOrThrow()._primeDrawWallField()}get _drawWallFieldValue(){return null===this._drawWallField?fo(15,this._imperial):this._drawWallField}get _drawWallCm(){const e=(e=>{const t=String(e??"").trim().replace(",",".");if(!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(t))return null;const i=Number(t);return Number.isFinite(i)?i:null})(this._drawWallFieldValue);if(null==e||e<0)return null;const t=this._imperial?2.54*e:e,i="column"===this._tool?Xs:100;return t>=("column"===this._tool?1:0)&&t<=i?t:null}get _drawWallMaxCm(){return"column"===this._tool?Xs:100}_showPhysicalRange(e=this._drawWallMaxCm,t=0){return this._editorRuntimeOrThrow()._showPhysicalRange(e,t)}_draftSegmentCount(e=this._curSpaceCfg){return this._editorRuntimeOrThrow()._draftSegmentCount(e)}_mergeSpacePartitions(e,t){return this._editorRuntimeOrThrow()._mergeSpacePartitions(e,t)}_finishWallChain(){return this._editorRuntimeOrThrow()._finishWallChain()}_activateMarkupTool(e){return this._editorRuntimeOrThrow()._activateMarkupTool(e)}_limitReached(e){return this._editorRuntimeOrThrow()._limitReached(e)}_svgPoint(e){return this._editorRuntimeOrThrow()._svgPoint(e)}_snap(e){return this._editorRuntimeOrThrow()._snap(e)}_snapDrawPoint(e,t=!1){return this._editorRuntimeOrThrow()._snapDrawPoint(e,t)}_planSnapOpeningCuts(e,t){return this._editorRuntimeOrThrow()._planSnapOpeningCuts(e,t)}_planSnapGeometrySnapshot(){return this._editorRuntimeOrThrow()._planSnapGeometrySnapshot()}_hiddenWallDiagnosticSnapshot(){return this._editorRuntimeOrThrow()._hiddenWallDiagnosticSnapshot()}_planStructuralGeometrySnapshot(){return this._editorRuntimeOrThrow()._planStructuralGeometrySnapshot()}_planSnapContextKey(e){return this._editorRuntimeOrThrow()._planSnapContextKey(e)}_resolvePlanDrawPoint(e,t){return this._editorRuntimeOrThrow()._resolvePlanDrawPoint(e,t)}get _activePlanSnapCandidate(){if(!this._markup||"draw"!==this._tool)return null;const e=this._planSnapHover;if(!e)return null;const t=this._planSnapGeometrySnapshot();return e.contextKey===this._planSnapContextKey(t.key)?e.candidate:null}get _activePlanSnapConflicts(){if(!this._markup||"draw"!==this._tool)return[];const e=this._planSnapHover;if(!e)return[];const t=this._planSnapGeometrySnapshot();return e.contextKey===this._planSnapContextKey(t.key)?e.conflicts:[]}_clearPlanSnapHover(e=!0){return this._editorRuntime?this._editorRuntimeOrThrow()._clearPlanSnapHover(e):(this._planSnapHover=null,void(e&&(this._cursorPt=null)))}_samePt(e,t){return oi(e,t)}_dropLegacySegments(){return this._editorRuntimeOrThrow()._dropLegacySegments()}_rollbackRejectedPhysicalWrites(e){return this._editorRuntimeOrThrow()._rollbackRejectedPhysicalWrites(e)}async _reloadRejectedPhysicalWrite(){return this._editorRuntimeOrThrow()._reloadRejectedPhysicalWrite()}get _cfgWriting(){return this._writesPending>0}async _sendConfigCandidate(e){const t=so(e),i=await this.hass.callWS({type:"houseplan/config/set",config:t,expected_rev:this._cfgRev});this._cfgRev=i?.rev??this._cfgRev+1}_writeConfig(){if(this._editorRuntime)return this._editorRuntime._writeConfig();this._writesPending++,this._writeChain=Lp(this._writeChain,async()=>{if(!this._serverCfg)return;const e=so(this._serverCfg),t=Tl(e);t!==Tl(this._serverCfg)&&(this._serverCfg=e),this._cfgContentFingerprint=t,await this._sendConfigCandidate(e)});return this._writeChain.finally(()=>{this._writesPending--})}_saveConfig(){this._cfgEpoch++,this._saveConfigDebounced()}_geometrySnapshotFromConfig(e,t){return this._editorRuntimeOrThrow()._geometrySnapshotFromConfig(e,t)}_geometrySnapshot(e=this._space){return this._editorRuntimeOrThrow()._geometrySnapshot(e)}_recordGeometry(e,t){return this._editorRuntimeOrThrow()._recordGeometry(e,t)}_restoreGeometryStateInConfig(e,t,i=!1){return this._editorRuntimeOrThrow()._restoreGeometryStateInConfig(e,t,i)}_restoreGeometryStateLocal(e){return this._editorRuntimeOrThrow()._restoreGeometryStateLocal(e)}_wallModelBlockerLabel(e){return this._editorRuntimeOrThrow()._wallModelBlockerLabel(e)}_hasLegacyZeroWallFields(e=this._serverCfg){return this._editorRuntimeOrThrow()._hasLegacyZeroWallFields(e)}_showWallModelMigrationBlocked(e){return this._editorRuntimeOrThrow()._showWallModelMigrationBlocked(e)}_limitSegmentsOf(e){return this._editorRuntimeOrThrow()._limitSegmentsOf(e)}_junctionLimitViolations(e,t){return this._editorRuntimeOrThrow()._junctionLimitViolations(e,t)}_junctionLimitLabel(e){return this._editorRuntimeOrThrow()._junctionLimitLabel(e)}_junctionLimitsIntroduced(e,t,i){return this._editorRuntimeOrThrow()._junctionLimitsIntroduced(e,t,i)}_commitPhysicalGeometry(e,t,i=[]){return this._editorRuntimeOrThrow()._commitPhysicalGeometry(e,t,i)}_clearGeometryGesture(){return this._editorRuntime?this._editorRuntimeOrThrow()._clearGeometryGesture():(this._path=[],this._planSnapHover=null,this._cursorPt=null,this._openingHoverCandidate=null,this._openingJambBlockCm=null,this._openingPreset=null,this._openingRebindId=null,this._mergeSel=null,this._mergeDialog=null,this._splitSel=null,this._pendingSplit=null,this._wallFaceBatch=null,this._wallRepairDiagnostic=null,this._roomDeleteDialog=null,this._wallDialog=null,this._physicalDialog=null,this._physicalSel=null,this._physicalDrag=null,this._physicalRotate=null,this._activeDraftId=null,this._draftSegmentCms=[],this._closingWallCm=null,this._openingDialog=null,this._resize?.reset(),this._decorDraft=null,this._decorMove=null,this._dtDrag=null,void(this._bdDrag=null))}_stagePointerCancel(e){if(this._editorRuntime)return this._editorRuntime._stagePointerCancel(e)}_applyGeometryState(e,t=!1){return this._editorRuntimeOrThrow()._applyGeometryState(e,t)}_roomAt(e){return this._editorRuntimeOrThrow()._roomAt(e)}_overlapRoom(e){return this._editorRuntimeOrThrow()._overlapRoom(e)}_pointInRoom(e,t){return t.poly?si(e,t.poly):null!=t.x&&e[0]>=t.x&&e[0]<=t.x+t.w&&e[1]>=t.y&&e[1]<=t.y+t.h}_contourSelfIntersects(e){return this._editorRuntimeOrThrow()._contourSelfIntersects(e)}_canAppendRoomDraftPoint(){return this._editorRuntimeOrThrow()._canAppendRoomDraftPoint()}_markupClick(e){if(this._editorRuntime)return this._editorRuntime._markupClick(e)}_draftEndAt(e,t){return this._editorRuntimeOrThrow()._draftEndAt(e,t)}_mergeDraftEndpoint(e){return this._editorRuntimeOrThrow()._mergeDraftEndpoint(e)}_adoptDraftCms(e,t,i){return this._editorRuntimeOrThrow()._adoptDraftCms(e,t,i)}_draftSegmentsForPath(e,t,i){return this._editorRuntimeOrThrow()._draftSegmentsForPath(e,t,i)}_persistActiveDraftSegment(){return this._editorRuntimeOrThrow()._persistActiveDraftSegment()}_activeWallSourceKey(e){return this._editorRuntimeOrThrow()._activeWallSourceKey(e)}_wallGraphSources(e){return this._editorRuntimeOrThrow()._wallGraphSources(e)}_wallFaceGraph(e,t){return this._editorRuntimeOrThrow()._wallFaceGraph(e,t)}_offerWallFaces(e,t=this._path.length-2,i){return this._editorRuntimeOrThrow()._offerWallFaces(e,t,i)}_beginWallFaceBatch(e){return this._editorRuntimeOrThrow()._beginWallFaceBatch(e)}_offerExistingWallFace(e){return this._editorRuntimeOrThrow()._offerExistingWallFace(e)}_columnClick(e){return this._editorRuntimeOrThrow()._columnClick(e)}_openPhysicalDialog(e,t,i){return this._editorRuntimeOrThrow()._openPhysicalDialog(e,t,i)}_physicalDown(e,t,i){return this._editorRuntimeOrThrow()._physicalDown(e,t,i)}_clampPhysicalDelta(e,t,i){return this._editorRuntimeOrThrow()._clampPhysicalDelta(e,t,i)}_physicalMove(e){return this._editorRuntimeOrThrow()._physicalMove(e)}_physicalUp(e){return this._editorRuntimeOrThrow()._physicalUp(e)}_registerPhysicalTap(e,t,i){return this._editorRuntimeOrThrow()._registerPhysicalTap(e,t,i)}_cancelPhysicalGesture(){return this._editorRuntimeOrThrow()._cancelPhysicalGesture()}_physicalRotateDown(e,t){return this._editorRuntimeOrThrow()._physicalRotateDown(e,t)}_physicalRotateMove(e){return this._editorRuntimeOrThrow()._physicalRotateMove(e)}_physicalRotateUp(e){return this._editorRuntimeOrThrow()._physicalRotateUp(e)}_rszRooms(){return this._editorRuntimeOrThrow()._rszRooms()}_rszOpenings(){return this._editorRuntimeOrThrow()._rszOpenings()}_rszObstacles(){return this._editorRuntimeOrThrow()._rszObstacles()}_rszOptsFor(e,t){return this._editorRuntimeOrThrow()._rszOptsFor(e,t)}_rszResolution(e,t,i){return this._editorRuntimeOrThrow()._rszResolution(e,t,i)}_rszSnapshot(){return this._editorRuntimeOrThrow()._rszSnapshot()}_rszResetController(){return this._editorRuntimeOrThrow()._rszResetController()}_rszProjectPreview(e,t,i,n,r){return this._editorRuntimeOrThrow()._rszProjectPreview(e,t,i,n,r)}_rszAcceptPreview(e,t){return this._editorRuntimeOrThrow()._rszAcceptPreview(e,t)}_rszSpaceCandidateGeometry(e,t){return this._editorRuntimeOrThrow()._rszSpaceCandidateGeometry(e,t)}_rszSpaceCandidateRenderable(e,t){return this._editorRuntimeOrThrow()._rszSpaceCandidateRenderable(e,t)}_rszCandidateRenderable(e){return this._editorRuntimeOrThrow()._rszCandidateRenderable(e)}_rszEdgeDown(e,t,i){return this._editorRuntimeOrThrow()._rszEdgeDown(e,t,i)}_rszReasonText(e){return this._editorRuntimeOrThrow()._rszReasonText(e)}_rszDisabledActivate(e,t){return this._editorRuntimeOrThrow()._rszDisabledActivate(e,t)}_rszDisabledKey(e,t){return this._editorRuntimeOrThrow()._rszDisabledKey(e,t)}_rszMove(e){return this._editorRuntimeOrThrow()._rszMove(e)}_rszUp(e){return this._editorRuntimeOrThrow()._rszUp(e)}_rszCancelDrag(e){return this._editorRuntimeOrThrow()._rszCancelDrag(e)}_rszPointerCancel(e){return this._editorRuntimeOrThrow()._rszPointerCancel(e)}_rszEdgeLabels(e,t,i=this._resize?.rooms){return this._editorRuntimeOrThrow()._rszEdgeLabels(e,t,i)}_rszInnerSpanCms(e,t,i){return this._editorRuntimeOrThrow()._rszInnerSpanCms(e,t,i)}_renderResizeMeasurements(){return this._editorRuntimeOrThrow()._renderResizeMeasurements()}_renderResizeLayer(e){return this._editorRuntimeOrThrow()._renderResizeLayer(e)}get _openingsR(){const e=this._curSpaceCfg,t=this._spaceH,i=this._spaceModel();return i?(e?.openings||[]).flatMap(e=>{const n={...e,rx:e.x*cm,ry:e.y*t,rlen:e.length*cm};if(!e.host||"wall"===e.host.kind)return[n];const r=Rd(e,i.partitions,cm,this._cellCm,this._gridPitch);if(!r.resolved)return"plan"===this._mode?[{...n,orphanReason:r.reason||"invalid-host"}]:[];let[o,s]=r.resolved.center;const a=this._physicalDrag;return a?.moved&&"partition"===a.kind&&a.id===e.host.id&&(o+=a.delta[0],s+=a.delta[1]),[{...e,rx:o,ry:s,rlen:r.resolved.length,angle:r.resolved.angle,partitionHost:r.resolved}]}):[]}_partitionOpeningCuts(e=this._spaceModel(),t=()=>!0){if(!e)return[];return Fp(Pp(this._curSpaceCfg?.id===e.id?this._curSpaceCfg:null,e,this._cellCm,this._gridPitch,cm),t)}_roomWallOpeningInputs(e=this._openingsR,t=this._spaceModel()){if(!t)return[];const i=this._openCuts();return Ip(e,t,this._spaceWalls,i,this._wallKeyPitch,this._cellCm,this._gridPitch,cm)}_openingFace(e,t,i){return e.partitionHost?Od(e.partitionHost,i):Vs(t,{x:e.rx,y:e.ry,angle:e.angle,length:e.rlen,flip_v:i})}_cmToUnits(e){return e/this._cellCm*this._gridPitch}get _decorList(){const e=this._curSpaceCfg;return Array.isArray(e?.decor)?e.decor:[]}get _decorH(){return cm}_decorResolvedStyle(e){return function(e,t,i,n=ur){const r="rect"===e?.kind||"ellipse"===e?.kind,o=e,s=jt(o?.color,n.color);return{color:s,opacity:pr(o?.opacity,n.opacity),widthCm:yr(e,t,i,n.widthCm),fill:!!r&&!0===o?.fill,fillColor:jt(o?.fill_color,o?.fill?s:n.fillColor),fillOpacity:r&&o?.fill?pr(o?.fill_opacity,.25):n.fillOpacity}}(e,this._cellCm,this._gridPitch,ur)}_decorWidthUnits(e){return((e,t,i,n=ur.widthCm)=>{const r=Number(e?.width_cm);if(Number.isFinite(r)&&r>0)return fr(r,t,i);const o=Number(e?.width);return Number.isFinite(o)&&o>0?o:fr(n,t,i)})(e,this._cellCm,this._gridPitch,ur.widthCm)}_decorTextSizeCm(e){if("text"===e?.kind){const t=Number(e.size_cm);return Number.isFinite(t)&&t>0?t:vr(20*Gi(e),this._cellCm,this._gridPitch)}return vr(20,this._cellCm,this._gridPitch)}_decorTextUnits(e){if("text"!==e.kind)return 20;const t=Number(e.size_cm);return Number.isFinite(t)&&t>0?fr(t,this._cellCm,this._gridPitch):20*Gi(e)}_decorSmallField(e){return Math.round(100*(this._imperial?e/2.54:e))/100}_decorSmallCm(e){const t=this._imperial?2.54*e:e;return Number.isFinite(t)?Math.max(.1,Math.min(100,t)):.1}_decorTextCm(e){const t=this._imperial?2.54*e:e;return Number.isFinite(t)?Math.max(.1,Math.min(2e3,t)):.1}_decorLargeField(e){return Math.round(100*(this._imperial?e/30.48:e/100))/100}_decorLargeCm(e){const t=this._imperial?30.48*e:100*e;return Number.isFinite(t)?Math.max(.1,Math.min(Pa*this._cellCm,t)):.1}_angleField(e){const t=Number(e);return Number.isFinite(t)?String(Number(t.toFixed(3))):"0"}_decorBoxOf(e){return"rect"!==e.kind&&"ellipse"!==e.kind&&"furniture"!==e.kind?null:{x:e.x*cm,y:e.y*this._decorH,w:e.w*cm,h:e.h*this._decorH,angle:_r(e.angle)||void 0}}_decorSnapGeometry(e){return this._editorRuntimeOrThrow()._decorSnapGeometry(e)}_decorSnap(e,t="mouse",i){return this._editorRuntimeOrThrow()._decorSnap(e,t,i)}_replaceDecor(e,t){return this._editorRuntimeOrThrow()._replaceDecor(e,t)}_cancelDecorGesture(){return this._editorRuntimeOrThrow()._cancelDecorGesture()}_decorPointerDown(e){return this._editorRuntimeOrThrow()._decorPointerDown(e)}_decorCommitDraft(){return this._editorRuntimeOrThrow()._decorCommitDraft()}_decorShapeDown(e,t){return this._editorRuntimeOrThrow()._decorShapeDown(e,t)}_decorMoveUpdate(e){return this._editorRuntimeOrThrow()._decorMoveUpdate(e)}_decorShapeDbl(e,t){if(this._editorRuntime)return this._editorRuntime._decorShapeDbl(e,t)}_openDecorProperties(e){return this._editorRuntimeOrThrow()._openDecorProperties(e)}_decorOpenText(e){return this._editorRuntimeOrThrow()._decorOpenText(e)}_decorRememberTextSelection(e){return this._editorRuntimeOrThrow()._decorRememberTextSelection(e)}_decorInsertLiveVariable(e){return this._editorRuntimeOrThrow()._decorInsertLiveVariable(e)}_decorSaveText(){return this._editorRuntimeOrThrow()._decorSaveText()}_decorSaveShape(){return this._editorRuntimeOrThrow()._decorSaveShape()}get _dtSel(){return"decor"===this._mode&&"select"===this._decorTool&&this._decorSel&&this._decorList.find(e=>e.id===this._decorSel)||null}_dtPivot(e){return this._editorRuntimeOrThrow()._dtPivot(e)}_dtApply(e,t){return this._editorRuntimeOrThrow()._dtApply(e,t)}_dtStart(e,t,i,n){return this._editorRuntimeOrThrow()._dtStart(e,t,i,n)}_dtMove(e){return this._editorRuntimeOrThrow()._dtMove(e)}_dtUp(){return this._editorRuntimeOrThrow()._dtUp()}_dtMeasure(){return this._editorRuntimeOrThrow()._dtMeasure()}_deleteDecor(e){return this._editorRuntimeOrThrow()._deleteDecor(e)}_decorDeleteSel(){return this._editorRuntimeOrThrow()._decorDeleteSel()}_confirmDecorErase(){return this._editorRuntimeOrThrow()._confirmDecorErase()}get _furnWalls(){const e=this._rawPhysicalBodiesR().flatMap(e=>e.map((t,i)=>{const n=e[(i+1)%e.length];return[t[0],t[1],n[0],n[1]]}));return[...this._segments,...e]}get _furnWallReach(){return 6*this._gridPitch}_furnFieldValue(e){return this._editorRuntimeOrThrow()._furnFieldValue(e)}_furnFieldToCm(e){return this._editorRuntimeOrThrow()._furnFieldToCm(e)}_furnPick(e){return this._editorRuntimeOrThrow()._furnPick(e)}_furnPlace(e,t=!1){return this._editorRuntimeOrThrow()._furnPlace(e,t)}_furnMoveUpdate(e){return this._editorRuntimeOrThrow()._furnMoveUpdate(e)}_decorApplyBox(e,t){return this._editorRuntimeOrThrow()._decorApplyBox(e,t)}get _furnLive(){const e=this._dtDrag;if(!e||"scale"!==e.kind||!e.orig)return null;const t=this._decorList.find(t=>t.id===e.id);if(!t||"furniture"!==t.kind)return null;const i=cm,n=this._decorH,r=t.w*i,o=t.h*n,s=function(e,t,i,n,r){const o=e+i/2,s=t+n/2,a=(Number(r)||0)*Math.PI/180,l=Math.cos(a),c=Math.sin(a),h=(e,t)=>{const i=e-o,n=t-s;return[o+i*l-n*c,s+i*c+n*l]};return[h(e,t),h(e+i,t),h(e+i,t+n),h(e,t+n)]}(t.x*i,t.y*n,r,o,Number(t.angle)||0),a=(e,t)=>[(e[0]+t[0])/2,(e[1]+t[1])/2],l=a(s[0],s[1]),c=a(s[0],s[3]);return[{x:l[0],y:l[1],text:this._fmtLen([0,0],[r,0])},{x:c[0],y:c[1],text:this._fmtLen([0,0],[0,o])}]}_renderFurnPalette(){return this._editorRuntimeOrThrow()._renderFurnPalette()}get _bdBase(){const e=this._curSpaceCfg;return e?.plan_url?{...Ra(e.plan_aspect,cm)}:null}get _bdRect(){const e=this._curSpaceCfg;return e?.plan_url?Aa(e,cm):null}get _bdParams(){const e=this._curSpaceCfg,t=Number(e?.plan_x),i=Number(e?.plan_y),n=Number(e?.plan_scale),r=Number(e?.plan_scale_x),o=Number(e?.plan_scale_y),s=Number.isFinite(n)&&n>0?n:1;return{dx:Number.isFinite(t)?t:0,dy:Number.isFinite(i)?i:0,sx:Number.isFinite(r)&&r>0?r:s,sy:Number.isFinite(o)&&o>0?o:s,angle:_r(e?.plan_angle)}}_openBackdropDialog(e){if(this._editorRuntime)return this._editorRuntime._openBackdropDialog(e)}_saveBackdropDialog(){return this._editorRuntimeOrThrow()._saveBackdropDialog()}get _bdActive(){return"decor"===this._mode&&!!this._bdRect&&"backdrop"===this._decorTool}get _bdMovable(){return"decor"===this._mode&&"backdrop"===this._decorTool&&!!this._bdRect}_bdApply(e,t,i,n,r){return this._editorRuntimeOrThrow()._bdApply(e,t,i,n,r)}_bdStart(e,t,i=!1){return this._editorRuntimeOrThrow()._bdStart(e,t,i)}_bdMove(e){return this._editorRuntimeOrThrow()._bdMove(e)}get _bdMoved(){if("decor"!==this._mode||!this._bdRect)return!1;const e=this._bdParams;return 0!==e.dx||0!==e.dy||1!==e.sx||1!==e.sy||0!==e.angle}_bdReset(){return this._editorRuntimeOrThrow()._bdReset()}_bdUp(){return this._editorRuntimeOrThrow()._bdUp()}get _bdLive(){if(!this._bdDrag)return null;const e=this._bdRect;return e?{x:e.x+e.w/2,y:e.y+e.h/2,text:`${this._fmtLen([0,0],[e.w,0])} × ${this._fmtLen([0,0],[0,e.h])}`}:null}_renderBackdropFrame(e){return this._editorRuntime?this._editorRuntimeOrThrow()._renderBackdropFrame(e):G}_renderTextFrame(e){const t=this._dtSel,i=this._dtBox;if(!t||!i||i.id!==t.id)return G;const n=.018*Math.max(e.w,e.h),r=n/4;if("line"===t.kind){const e=[t.x1*cm,t.y1*this._decorH],i=[t.x2*cm,t.y2*this._decorH];return W` + + ${[e,i].map((e,t)=>W`this._dtStart(e,"scale",void 0,t)}> + `)} + `}const[o,s]=this._dtPivot(t),a=Number(t.angle)||0,l=2.2*n;return W` + + + this._dtStart(e,"rotate")}> + + ${[[-1,-1,"nwse"],[1,-1,"nesw"],[1,1,"nwse"],[-1,1,"nesw"]].map(([e,t,o])=>W`this._dtStart(i,"scale",[e,t])}>`)} + `}_renderDecorLayer(){const e=cm,t=this._decorH,i="decor"===this._mode,n=i&&"erase"===this._decorTool,r=this._decorList.map(r=>{const o="dshape"+(i&&this._decorSel===r.id?" dsel":""),s=this._decorResolvedStyle(r),a=this._decorWidthUnits(r),l=e=>this._decorShapeDown(e,r),c=e=>this._decorShapeDbl(e,r);if("line"===r.kind)return W` + ${i&&"select"===this._decorTool?W``:G} + ${n?W``:G}`;if("rect"===r.kind){const i=(r.x+r.w/2)*e,h=(r.y+r.h/2)*t,d=_r(r.angle);return W` + ${n?W``:G}`}if("ellipse"===r.kind){const i=(r.x+r.w/2)*e,h=(r.y+r.h/2)*t,d=_r(r.angle);return W` + ${n?W``:G}`}if("furniture"===r.kind){const i=r.w*e,h=r.h*t,d=vl(r.symbol,i,h);if(!d)return G;const u=Number(r.angle)||0,p=r.x*e+i/2,_=r.y*t+h/2,m=`${u?`rotate(${u} ${p} ${_}) `:""}translate(${r.x*e} ${r.y*t})`;return W` + ${n?W``:G}`}if("text"===r.kind){const i=this._decorTextUnits(r),n=this._renderDeviceSnapshot?.facts.get(`decor:${this._space}:${r.id}`),a=function(e){return String(e??"").replace(/\r\n?/g,"\n").split("\n")}("string"==typeof n?n:Wi(r.text,r,this._renderPlanHass,e=>this._renderEntityAvailable(e))),h=r.x*e,d=r.y*t,u=Number(r.angle)||0,p=d-(a.length-1)*i*1.2/2;return W`${a.map((e,t)=>W`${e}`)}`}return G});let o=G;const s=this._decorDraft;if(s){const e=this._decorStyle,t=fr(e.widthCm,this._cellCm,this._gridPitch);if("line"===s.kind)o=W``;else{const i=Math.min(s.a[0],s.b[0]),n=Math.min(s.a[1],s.b[1]),r=Math.abs(s.b[0]-s.a[0]),a=Math.abs(s.b[1]-s.a[1]);o="rect"===s.kind?W``:W``}}return W`${r}${o}`}get _editorToolbarGroups(){return"plan"!==this._mode?[]:[{id:"opening",label:this._t("markup.opening"),icon:"mdi:door",activeItemId:"opening"===this._tool?this._openingPreset?.type:void 0,items:[{id:"window",label:this._t("opening.window"),icon:"mdi:window-closed-variant",role:"tool",invoke:()=>this._activateOpeningPlacement("window")},{id:"door",label:this._t("opening.door"),icon:"mdi:door-open",role:"tool",invoke:()=>this._activateOpeningPlacement("door")},{id:"passage",label:this._t("opening.passage"),icon:"mdi:arch",role:"tool",invoke:()=>this._activateOpeningPlacement("passage")},{id:"gate",label:this._t("opening.gate"),icon:"mdi:gate",role:"tool",invoke:()=>this._activateOpeningPlacement("gate")}]}]}_renderEditorGroupLauncher(e){return this._editorRuntimeOrThrow()._renderEditorGroupLauncher(e)}get _editorSecondaryContextId(){const e=`editor:${this._mode}:${this._space}:${this._cfgEpoch}`,t=this._editorSecondary?.activeGroup(this._editorToolbarGroups);if(t)return`${e}:group:${t.id}:${this._editorSecondary?.groupGeneration}`;if("plan"===this._mode){const t=this._physicalSel;return t?`${e}:selection:${t.kind}:${t.id}:${t.segment??""}`:`${e}:tool:${this._tool}:${this._path.length}`}return"decor"===this._mode?"furniture"===this._decorTool?`${e}:palette:furniture:${this._furnPalette?.symbol||"none"}`:"select"===this._decorTool&&this._decorSel?`${e}:selection:decor:${this._decorSel}`:`${e}:tool:${this._decorTool}:${this._bdMoved?1:0}:${this._bdDrag?1:0}`:`${e}:none`}_runEditorContext(e,t){return this._editorRuntimeOrThrow()._runEditorContext(e,t)}_renderEditorGroupModel(e){return this._editorRuntimeOrThrow()._renderEditorGroupModel(e)}_renderDrawWallControl(){return this._editorRuntimeOrThrow()._renderDrawWallControl()}_renderPlanSecondary(){return this._editorRuntimeOrThrow()._renderPlanSecondary()}_renderDecorSecondary(){return this._editorRuntimeOrThrow()._renderDecorSecondary()}_withBackdropReset(e){return this._editorRuntimeOrThrow()._withBackdropReset(e)}get _editorSecondaryDialogBlocked(){return!!(this._tapConfirm||this._vacCalConfirm||this._roomDialog||this._mergeDialog||this._openingDialog||this._physicalDialog||this._openingInfo||this._decorTextDialog||this._decorShapeDialog||this._backdropDialog||this._decorEraseConfirm||this._spaceDialog||this._markerDialog||this._deviceInbox||this._infoCard||this._rulesDialog||this._settingsDialog||this._alignDialog||this._importDialog||this._kioskDialog||this._backupExportDialog||this._backupImportDialog||this._wallDialog)}_renderEditorSecondary(){return this._editorRuntimeOrThrow()._renderEditorSecondary()}_renderDecorBar(){return this._editorRuntimeOrThrow()._renderDecorBar()}_renderDecorEraseConfirm(){return this._editorRuntimeOrThrow()._renderDecorEraseConfirm()}_renderDecorTextDialog(){return this._editorRuntimeOrThrow()._renderDecorTextDialog()}_renderDecorShapeDialog(){return this._editorRuntimeOrThrow()._renderDecorShapeDialog()}_renderBackdropDialog(){return this._editorRuntimeOrThrow()._renderBackdropDialog()}_cssPxToRender(e){return this._editorRuntimeOrThrow()._cssPxToRender(e)}_renderZeroWalls(e){if(e&&!e.showBorders&&!this._editing)return W``;const t=this._zeroWalls();if(!t.lines.length)return W``;const i=e?.color||"var(--hp-muted)";return W` + ${t.lines.map(e=>W``)} + `}_zeroWalls(){const e=this._spaceModel();return e?op(this._curSpaceCfg,e,cm,.02*this._gridPitch):{style:tp(this._curSpaceCfg),lines:[],contour:[],barriers:[],transmissive:[]}}_openCuts(){return this._zeroWalls().contour}_deleteRoomClick(e){return this._editorRuntimeOrThrow()._deleteRoomClick(e)}get _wallKeyPitch(){return Na}get _spaceWalls(){const e=this._curSpaceCfg?.walls;return Array.isArray(e)?e:[]}_cfgOpenCuts(){const e=this._curSpaceCfg,t=this._spaceModel();if(!e||!t)return[];const i=ip(e,1),n=rp(e,t.rooms,cm,.02*this._gridPitch).map(e=>e.map(e=>e/cm)),r=new Set;return[...i,...n].filter(e=>{const t=e.map(e=>Number(e).toFixed(9)).join(","),i=[e[2],e[3],e[0],e[1]].map(e=>Number(e).toFixed(9)).join(","),n=t[e[0]/cm,e[1]/cm,e[2]/cm,e[3]/cm]))}_paperShapes(e){if(!this._spaceWalls.length)return Xt(e);const t=this._wallUnionGeometry();return t?.paperD?[{path:t.paperD}]:Xt(e)}_wallUnionGeometry(){const e=this._spaceModel();if(!e)return null;const t=this._spaceWalls,i=this._physicalBodiesR();if(!t.length&&!i.length)return null;const n=`${this._space}|${this._cfgEpoch}|${e.rooms.length}`;if(!this._wallUnionCache||this._wallUnionCache.key!==n){const r=X_(this._wallUnionPool,n);if(r.hit)this._wallUnionCache=r.value;else{const r=this._openCuts(),o=this._roomWallOpeningInputs(),s=Hs(e.rooms,t,r,o,this._wallKeyPitch,this._cellCm,this._gridPitch,cm,i);s&&Object.defineProperty(s,"sourceFingerprint",{value:Tl([this._curSpaceCfg,this._cellCm,this._gridPitch]),enumerable:!1});const a={key:n,value:s};J_(this._wallUnionPool,n,a,8),this._wallUnionCache=a}}return this._wallUnionCache.value}_thickWallCuts(){const e=this._spaceModel();if(!e)return[];const t=this._spaceWalls;if(!t.length)return[];const i=this._openCuts();return js(e.rooms,t,i,this._wallKeyPitch,this._cellCm,this._gridPitch,cm).map(e=>[e.a[0],e.a[1],e.b[0],e.b[1]])}_innerRoomContour(e,t,i=this._openCuts(),n=this._wallUnionGeometry()?.roomGeom,r=this._wallUnionGeometry()?.multiWallNodes){const o=i.map(e=>e.join(",")).join(";"),s=`${e.id}|${this._cfgEpoch}|${t}|${o}`,a=X_(this._innerContourCache,s);if(a.hit)return a.value;const l=ks(e.rooms,t,this._spaceWalls,i,this._wallKeyPitch,this._cellCm,this._gridPitch,cm,n,r);return J_(this._innerContourCache,s,l,600),l}_wallThickHit(e){return this._editorRuntimeOrThrow()._wallThickHit(e)}get _wallThickHover(){if(!this._markup||"wallthick"!==this._tool||!this._cursorPt||this._wallDialog)return null;const e=this._wallThickHit(this._cursorPt);if(!e)return null;const t=function(e,t,i){const n=Number.isFinite(e)&&e>0?e:0,r=Number.isFinite(t)&&t>0?t:5,o=Number.isFinite(i)&&i>0?i:0;return n>0?vo(n,r,o)/2:xa(1.5*o,r)}(e.cm,this._cellCm,this._gridPitch);let i="";for(const n of e.segs)i+=(i?" ":"")+Ko([[n[0],n[1]],[n[2],n[3]]],t,!1);return{segs:e.segs,open:e.open,d:i}}_wallThickClick(e){return this._editorRuntimeOrThrow()._wallThickClick(e)}_wallThickApply(e){return this._editorRuntimeOrThrow()._wallThickApply(e)}_wallHatchDefs(e){const t=_o(this._cellCm);return W` + + + + `}_resolvedRoomFills(e,t){const i=new Map,n=new Map;for(const r of e.rooms){const e=this._roomDialog&&r.id===this._roomEditId?this._roomFill||t.fill:Tn(t.fill,r),o=this._roomDialog&&r.id===this._roomEditId?this._roomCustomFill||t.customFill:en(t.customFill,r),s=an(e,"lqi"===e&&r.area?this._roomLqi(r.area):null,"light"===e?Rh(Mh(this._renderPlanHass,this._renderDevices,r,this._virtualLights)):"none","temp"===e?this._roomTemp(r):null,t.tempMin,t.tempMax,this._fillColors,o);i.set(r,s),r.id&&n.set(r.id,s)}return{byRoom:i,byId:n}}_spaceDisplayForRender(){const e=tn(this._curSpaceCfg),t=this._spaceDialog;return t&&"edit"===t.mode&&t.spaceId===this._space?{...e,showBorders:t.showBorders,showNames:t.showNames,hideDecor:t.hideDecor,hideOpenings:t.hideOpenings,color:t.roomColor,opacity:t.roomOpacity,fill:t.fillMode,customFill:t.customFill?Qi(t.customFill):Ji,glow:t.glowEnabled,tempMin:t.tempMin,tempMax:t.tempMax,showLqi:t.showLqi,cardFontScale:t.cardFontScale,labelTemp:t.labelTemp,labelHum:t.labelHum,labelLqi:t.labelLqi,labelLight:t.labelLight}:e}_openingWallIndexFor(e,t){const i=e.rooms.map(e=>`${e.id}:${e.poly?.map(e=>e.join(",")).join("/")||`${e.x},${e.y},${e.w},${e.h}`}`).join(";"),n=this._spaceWalls.map(e=>`${e.key}:${e.a?.join(",")||""}:${e.b?.join(",")||""}:${e.cm}`).join(";"),r=t.map(e=>e.join(",")).join(";"),o=[e.id,this._cfgEpoch,this._wallKeyPitch,this._cellCm,this._gridPitch,i,n,r].join("|");let s=this._openingWallIndexCache.get(o);return s||(s=Bs(e.rooms,this._spaceWalls,t,this._wallKeyPitch,this._cellCm,this._gridPitch,cm)),J_(this._openingWallIndexCache,o,s,4),{key:o,value:s}}_renderOpeningTunnelFills(e,t,i="data"){if(this._markup||!this._spaceWalls.length||!this._openingsR.length)return W``;if("glow-base"===i&&![...t.byRoom.values()].some(Boolean))return W``;const n=this._openCuts(),r=this._openingsR.map(e=>({x:e.rx,y:e.ry,angle:e.angle,length:e.rlen})),o=r.map(e=>`${e.x},${e.y},${e.angle},${e.length}`).join(";"),s=this._openingWallIndexFor(e,n),a=`${s.key}|${o}`;return this._openingTunnelCache&&this._openingTunnelCache.key===a||(this._openingTunnelCache={key:a,value:Zs(s.value,r)}),Wu({openings:this._openingsR,geometries:this._openingTunnelCache.value,fillsByRoomId:t.byId,idPrefix:`${e.id}-${i}`,groupClass:"data"===i?"opening-tunnels":"opening-tunnels glow-base-tunnels",dataLayer:i})}_resolvedGlowBase(e,t,i){const n=new Map,r=new Map,o=this._fillColors.glow_base;for(const s of e.rooms){const e=i.byRoom.get(s),a=Rn(t.glow,s)&&(!e||e.opacity<=0)?{color:o.c,opacity:o.a,mode:"glow"}:null;n.set(s,a),s.id&&r.set(s.id,a)}return{byRoom:n,byId:r}}_renderGlowBaseRooms(e,t){if(this._markup||![...t.byRoom.values()].some(Boolean))return W``;const i=new Map(e.rooms.map(e=>[e,Zt(e)])),n=this._openCuts(),r=this._wallUnionGeometry()?.roomGeom,o=e=>"M "+e.map(e=>e[0]+" "+e[1]).join(" L ")+" Z",s=e.rooms.map(s=>{const a=t.byRoom.get(s)||null,l=i.get(s)||null;if(!a||!l)return G;const c=this._spaceWalls.length&&s.id&&this._innerRoomContour(e,s.id,n,r)||l,h=vi(c,e.rooms.filter(e=>e!==s).map(e=>i.get(e)).filter(e=>!!e)),d=this._cleanFloor(s,c,e).path;return d||h.length?W``:s.poly||c!==l?W``:W``});return W``}_renderWallBodies(e){if("iso"===this._renderProjection)return W``;if(e&&!e.showBorders&&("view"===this._mode||"devices"===this._mode))return W``;const t=this._wallUnionGeometry();if(!t)return W``;const i=this._stageEl,n=this._viewOr(this._baseVb()),r=i&&i.clientWidth&&n.w?i.clientWidth/n.w:1,o=e?.color||"#607d8b",s=po(t.depthUnits,r)||mo(_o(this._cellCm),r),a=this._fillColors.wall_fill;return W` + ${t.paths.map(e=>W` + + `)} + `}_isoDecorationLayers(e){const t="undefined"==typeof CSS||"function"!=typeof CSS.supports||CSS.supports("filter","blur(1px)"),i="function"==typeof matchMedia&&matchMedia("(forced-colors: active)").matches;return function(e){const t=!!e.showBorders;return{structural:t,panels:t&&!e.hideOpenings,shadows:t&&e.filtersSupported&&!e.forcedColors,materialNuance:t&&!e.forcedColors,floorSymbols:!e.hideOpenings&&!t}}({showBorders:e.showBorders,hideOpenings:e.hideOpenings,filtersSupported:t,forcedColors:i})}_isoOpeningPanels(e){if(!e.panels)return[];const t=this._isoScene();if(!t)return[];return t.openings.flatMap(e=>{const t=this._openingsR[e.sourceIndex];return t?E_(e,this._openingAmt(t)):[]}).sort((e,t)=>e.depth-t.depth||e.sourceIndex-t.sourceIndex||e.leaf-t.leaf)}_renderIsoDefs(e,t){const i=ka(this._cellCm);return W` + ${"walls"===t&&e.materialNuance?W` + + + + + + `:G} + ${"underlay"===t&&e.shadows?W` + + + `:G} + ${"shadows"===t&&e.shadows?W` + + + + + + `:G} + `}_renderIsoUnderlay(e){if(!e.structural)return W``;const t=this._isoScene()?.floor;return t?W``:W``}_renderIsoShadows(e,t){if(!e.shadows)return W``;const i=this._isoScene()?.geometry;return i?W``:W``}_renderIsoWalls(e,t){if("iso"!==this._renderProjection)return W``;if(!e.structural)return W``;const i=this._isoScene();return i?W` + ${this._renderIsoDefs(e,"walls")} + ${i.geometry.sides.map(e=>W``)} + + ${e.panels?W``:G} + `:W``}_roomHoverPaths(e){const t=this._hoverRoom;if("view"!==this._mode||!t||t.space!==e.id)return null;const i=e.rooms.find(e=>e===t.room||!!e.id&&e.id===t.room.id);if(!i)return null;const n=Zt(i);if(!n)return null;const r=e.rooms.filter(e=>e!==i).map(e=>({room:e,poly:Zt(e)})).filter(e=>!!e.poly),o=vi(n,r.map(e=>e.poly)),s=this._openCuts(),a=.02*this._gridPitch,l=s.filter(e=>{const t=[(e[0]+e[2])/2,(e[1]+e[3])/2];return n.some((e,i)=>Pn(t,[e[0],e[1],...n[(i+1)%n.length]])<=4*a)}),c=this._spaceWalls,h=this._wallUnionGeometry()?.roomGeom,d=c.length&&i.id&&this._innerRoomContour(e,i.id,s,h)||n,u=[{axis:n,face:d}];for(const t of o){const i=r.find(e=>e.poly===t)?.room;let n=t;if(c.length&&i?.id){const r=ms(e.rooms,i.id,c,s,this._wallKeyPitch,this._cellCm,this._gridPitch,cm);r&&(n=qs(r.poly,r.offsets)||t)}u.push({axis:t,face:n})}const p=this._roomWallOpeningInputs(this._openingsR,e).map(e=>{const t=e.angle*Math.PI/180,i=Math.cos(t)*e.length/2,n=Math.sin(t)*e.length/2;return[e.x-i,e.y-n,e.x+i,e.y+n]}),_=l.concat(p),m=u.map(({axis:e,face:t})=>{const i=_.map(i=>((e,t,i)=>{const n=e[2]-e[0],r=e[3]-e[1],o=Math.hypot(n,r);if(o.05)&&Pn([c,h],[i[0],i[1],n[0],n[1]])<=4*a){d=!0;break}}if(!d)return null;let u=null;for(let e=0;e.05)continue;const p=Pn([c,h],[t[0],t[1],n[0],n[1]]);(!u||p!!e);return i.length?kn(t,i,a).map(e=>`M ${e[0]} ${e[1]} L ${e[2]} ${e[3]}`).join(" "):`M ${t.map(e=>`${e[0]} ${e[1]}`).join(" L ")} Z`}).filter(Boolean).join(" ");if(!m)return null;const g=e=>`M ${e.map(e=>`${e[0]} ${e[1]}`).join(" L ")} Z`;return{fillD:[this._cleanFloor(i,d,e).path||g(d),...o.map(g)].join(" "),outlineD:m}}_renderRoomHoverFill(e){return e?W` + + `:W``}_renderRoomHoverOutline(e){return e?W` + + + `:W``}_renderWallThickUi(){return this._editorRuntime?this._editorRuntimeOrThrow()._renderWallThickUi():W``}_renderWallThickDialog(){return this._editorRuntimeOrThrow()._renderWallThickDialog()}_openingAt(e){return this._editorRuntimeOrThrow()._openingAt(e)}_resolveOpeningPlacement(e){return this._editorRuntimeOrThrow()._resolveOpeningPlacement(e)}_activateOpeningPlacement(e){return this._editorRuntimeOrThrow()._activateOpeningPlacement(e)}_clearOpeningPlacement(e){return this._editorRuntime?this._editorRuntimeOrThrow()._clearOpeningPlacement(e):(this._openingHoverCandidate=null,this._openingJambBlockCm=null,void(e&&(this._openingPreset=null,this._openingRebindId=null)))}_openingClick(e){return this._editorRuntimeOrThrow()._openingClick(e)}_editOpening(e){if(this._editorRuntime)return this._editorRuntimeOrThrow()._editOpening(e)}_opPointerDown(e,t){if(this._editorRuntime)return this._editorRuntimeOrThrow()._opPointerDown(e,t)}_opPointerMove(e,t){if(this._editorRuntime)return this._editorRuntimeOrThrow()._opPointerMove(e,t)}_opRuler(e,t){return this._editorRuntimeOrThrow()._opRuler(e,t)}_opPointerUp(e,t){if(this._editorRuntime)return this._editorRuntimeOrThrow()._opPointerUp(e,t)}_opClick(e,t){if(this._editorRuntime)return this._editorRuntimeOrThrow()._opClick(e,t)}_saveOpening(){return this._editorRuntimeOrThrow()._saveOpening()}_deleteOpening(){return this._editorRuntimeOrThrow()._deleteOpening()}_contactCandidates(){return this._editorRuntimeOrThrow()._contactCandidates()}_lockCandidates(){return this._editorRuntimeOrThrow()._lockCandidates()}_toggleOpeningEntityPicker(e){return this._editorRuntimeOrThrow()._toggleOpeningEntityPicker(e)}_filterOpeningEntities(e,t){return this._editorRuntimeOrThrow()._filterOpeningEntities(e,t)}_selectOpeningEntity(e,t){return this._editorRuntimeOrThrow()._selectOpeningEntity(e,t)}_mergeClick(e){return this._editorRuntimeOrThrow()._mergeClick(e)}_commitMerge(){return this._editorRuntimeOrThrow()._commitMerge()}_splitClick(e){return this._editorRuntimeOrThrow()._splitClick(e)}get _contourClosed(){return this._path.length>=4&&this._samePt(this._path[0],this._path[this._path.length-1])&&(null!=this._closingWallCm||!this._activeDraftId)}_markupMove(e){if(this._markup&&this._editorRuntime)return this._editorRuntime._markupMove(e)}get _openingPreview(){const e=this._openingPreset;if("opening"!==this._tool||!e||!this._cursorPt)return null;const t=this._cursorPt,i=this._openingHoverCandidate;if(i&&G_(i,[t[0],t[1]],e.revision,this._cfgEpoch))return i;if(this._openingAt(t))return this._openingHoverCandidate=null,null;const n=this._resolveOpeningPlacement(t);return this._openingHoverCandidate=n,n}get _opMeasureView(){return this._opMeasure||this._openingPreview?.measure||null}_saveRoom(){return this._editorRuntimeOrThrow()._saveRoom()}_decideWallFace(e){return this._editorRuntimeOrThrow()._decideWallFace(e)}_wallSourceCmAt(e,t,i){return this._editorRuntimeOrThrow()._wallSourceCmAt(e,t,i)}_activePathWithRepair(e,t){return this._editorRuntimeOrThrow()._activePathWithRepair(e,t)}_validateWallRepair(e,t){return this._editorRuntimeOrThrow()._validateWallRepair(e,t)}_applyWallRepair(e,t){return this._editorRuntimeOrThrow()._applyWallRepair(e,t)}_applyWallFaceBatch(){return this._editorRuntimeOrThrow()._applyWallFaceBatch()}_commitRoom(){return this._editorRuntimeOrThrow()._commitRoom()}_cancelPath(){return this._editorRuntimeOrThrow()._cancelPath()}_resumeLastDraft(){return this._editorRuntimeOrThrow()._resumeLastDraft()}_roomDialogCancel(){return this._editorRuntimeOrThrow()._roomDialogCancel()}get _freeAreas(){const e=new Set;for(const t of this._serverCfg?.spaces||[])for(const i of t.rooms||[])i.area&&e.add(i.area);for(const t of this._wallFaceBatch?.decisions||[])t.create&&t.area&&e.add(t.area);return Object.values(this.hass?.areas||{}).filter(t=>!e.has(t.area_id)).sort((e,t)=>(e.name||"").localeCompare(t.name||""))}_openDeviceInbox(){return this._editorRuntimeOrThrow()._openDeviceInbox()}_closeMarkerDialog(){return this._editorRuntimeOrThrow()._closeMarkerDialog()}_deviceInboxCandidates(e){return this._editorRuntimeOrThrow()._deviceInboxCandidates(e)}_deviceInboxRows(){return this._editorRuntimeOrThrow()._deviceInboxRows()}_deviceForInboxRow(e){return this._editorRuntimeOrThrow()._deviceForInboxRow(e)}_openInboxMarker(e,t=!1){return this._editorRuntimeOrThrow()._openInboxMarker(e,t)}async _setInboxHidden(e,t){return this._editorRuntimeOrThrow()._setInboxHidden(e,t)}_findInboxDevice(e){return this._editorRuntimeOrThrow()._findInboxDevice(e)}_deviceInboxTabKey(e){return this._editorRuntimeOrThrow()._deviceInboxTabKey(e)}_openMarkerDialog(e){if(this._editorRuntime)return this._editorRuntime._openMarkerDialog(e);this._ensureEditorRuntime().then(t=>{t&&this._openMarkerDialog(e)})}_runCandidates(){return this._editorRuntimeOrThrow()._runCandidates()}_bindingCandidates(){return this._editorRuntimeOrThrow()._bindingCandidates()}_physicalBodiesR(e=this._spaceModel()){if(!e)return[];const t=`${e.id}|${this._cfgEpoch}|${this._cellCm}|${this._gridPitch}`;if(this._physicalBodiesCache?.key===t)return this._physicalBodiesCache.all;const i=ca(e,this._cellCm,this._gridPitch,2e-4*this._gridPitch,this._partitionOpeningCuts(e));return this._physicalBodiesCache={key:t,...i},i.all}_rawPhysicalBodiesR(e=this._spaceModel()){if(!e)return[];this._physicalBodiesR(e);const t=this._physicalBodiesCache;return t?[...t.drafts,...t.partitions,...t.columns]:[]}_cleanFloor(e,t,i=this._spaceModel()){if(!i)return{floor:t,geom:null,path:"",area:ma([[[...t,t[0]]]])};const n=e.id||`#${i.rooms.indexOf(e)}`,r=`${i.id}|${this._cfgEpoch}|${n}`;if(!this._resize?.preview){const e=X_(this._cleanFloorCache,r);if(e.hit)return e.value}const o=t.map(e=>e[0]),s=t.map(e=>e[1]),a=[Math.min(...o),Math.min(...s),Math.max(...o),Math.max(...s)],l=this._physicalBodiesR(i).filter(e=>{const t=e.map(e=>e[0]),i=e.map(e=>e[1]);return Math.max(...t)>=a[0]&&Math.min(...t)<=a[2]&&Math.max(...i)>=a[1]&&Math.min(...i)<=a[3]}),c=l.length?_a(t,l):null,h={floor:t,geom:c,path:c?ra(c):"",area:ma(c||[[[...t,t[0]]]])};return this._resize?.preview||J_(this._cleanFloorCache,r,h,600),h}_autoIconForBinding(e){if("virtual"===e)return"mdi:map-marker";const[t,i]=e.split(":");if(!i)return"";const n=this._fullRegistryHass,r=this._bindingStatus(e),o="active"===r.kind?r.enabledEntityIds:r.allEntityIds;if("device"===t){const e=n.devices?.[i];if(!e)return"mdi:help-circle";const t=o;return t.some(e=>e.startsWith("lock."))?"mdi:lock":Fh(n,e.name_by_user||e.name||"",e.model,t,this._iconRules)}if("entity"===t){const e=n.entities?.[i],t=this.hass.states?.[i],r=e?.name||t?.attributes?.friendly_name||i;return i.startsWith("lock.")?"mdi:lock":Fh(n,r,"",[i],this._iconRules)}return""}_allRoomsFlat(){const e=[];for(const t of this._serverCfg?.spaces||[])for(const i of t.rooms||[])i.area?e.push({value:t.id+"#"+i.area,label:(t.title||t.id)+" · "+i.name}):i.id&&e.push({value:t.id+"#@"+i.id,label:(t.title||t.id)+" · "+i.name+" · "+this._t("marker.subarea")});return e}_errText(e){if(!e)return this._t("err.unknown");if("string"==typeof e)return e;if("invalid_passage_fields"===e.code){const t=String(e.message||e.error||"").match(/space=([^;]*);\s*opening=([^;]*);\s*fields=([^;]*)/);if(t){const e=this._serverCfg?.spaces?.find(e=>String(e.id)===t[1]),i={contact:"opening.contact_label",lock:"opening.lock_label",invert:"opening.invert",flip_h:"opening.flip_h",flip_v:"opening.flip_v"},n=t[3].split(",").filter(Boolean).map(e=>i[e]?this._t(i[e]):e).join(", ");return this._t("opening.invalid_passage_fields",{room:e?.title||t[1],fields:n})}}if("invalid_partition_opening_jamb_margin"===e.code){const t=String(e.message||e.error||"").match(/margin_cm=([^;]*)/),i=t?Number(t[1]):NaN;if(Number.isFinite(i))return this._t("opening.partition_jamb_margin",{distance:Kt(i,this._imperial)})}if(e.message)return e.message;if(e.error)return e.error;if(null!=e.code)return this._t("err.code",{code:e.code});try{return JSON.stringify(e)}catch{return String(e)}}_backupErrorText(e){return this._editorRuntimeOrThrow()._backupErrorText(e)}async _pickMarkerFiles(e){return this._editorRuntimeOrThrow()._pickMarkerFiles(e)}_removeMarkerPdf(e){return this._editorRuntimeOrThrow()._removeMarkerPdf(e)}_markerLightFields(e){return this._editorRuntimeOrThrow()._markerLightFields(e)}_markerTapActionFields(e){return this._editorRuntimeOrThrow()._markerTapActionFields(e)}_markerToggleEntityFields(e){return this._editorRuntimeOrThrow()._markerToggleEntityFields(e)}async _saveMarker(){return this._editorRuntimeOrThrow()._saveMarker()}async _deleteMarker(){return this._editorRuntimeOrThrow()._deleteMarker()}_normPos(e,t,i){return{s:e,x:t/cm,y:i/cm}}_spaceDialogUsesOnboardingRuntime(e){return"create"===e&&!this._editorRuntime&&(!!this._onboardingRuntime||0===(this._serverCfg?.spaces.length||0)||this._importTotal>0||this._importQueue.length>0)}_spaceRuntimeOrThrow(){if(!this._onboardingRuntime)throw new Error("Houseplan onboarding runtime is not loaded");return this._onboardingRuntime}_openSpaceDialog(e,t){return this._spaceDialogUsesOnboardingRuntime(e)?this._onboardingRuntime?void this._onboardingRuntime._openSpaceDialog(e,t):void this._ensureOnboardingRuntime().then(i=>{i&&this._spaceRuntimeOrThrow()._openSpaceDialog(e,t)}):this._editorRuntime?this._editorRuntime._openSpaceDialog(e,t):void this._ensureEditorRuntime().then(i=>{i&&this._openSpaceDialog(e,t)})}async _pickPlanFile(e){return this._onboardingRuntime&&this._spaceDialogUsesOnboardingRuntime("create")?this._onboardingRuntime._pickPlanFile(e):this._editorRuntimeOrThrow()._pickPlanFile(e)}_useServerPlan(e){return this._onboardingRuntime&&this._spaceDialogUsesOnboardingRuntime("create")?this._onboardingRuntime._useServerPlan(e):this._editorRuntimeOrThrow()._useServerPlan(e)}async _readPlanAspect(e){return this._onboardingRuntime&&this._spaceDialogUsesOnboardingRuntime("create")?this._onboardingRuntime._readPlanAspect(e):this._editorRuntimeOrThrow()._readPlanAspect(e)}async _deleteServerPlan(e){return this._onboardingRuntime&&this._spaceDialogUsesOnboardingRuntime("create")?this._onboardingRuntime._deleteServerPlan(e):this._editorRuntimeOrThrow()._deleteServerPlan(e)}_renderServerPlans(e){return this._onboardingRuntime&&this._spaceDialogUsesOnboardingRuntime(e.mode)?this._onboardingRuntime._renderServerPlans(e):this._editorRuntimeOrThrow()._renderServerPlans(e)}async _saveSpaceDialog(){return this._onboardingRuntime&&this._spaceDialogUsesOnboardingRuntime("create")?this._onboardingRuntime._saveSpaceDialog():this._editorRuntimeOrThrow()._saveSpaceDialog()}async _deleteSpace(){return this._editorRuntimeOrThrow()._deleteSpace()}async _saveConfigNow(){return this._editorRuntimeOrThrow()._saveConfigNow()}_startImport(){return this._onboardingRuntime?this._onboardingRuntime._startImport():this._editorRuntimeOrThrow()._startImport()}_openNextImport(){return this._onboardingRuntime?this._onboardingRuntime._openNextImport():this._editorRuntimeOrThrow()._openNextImport()}_skipImport(){return this._onboardingRuntime?this._onboardingRuntime._skipImport():this._editorRuntimeOrThrow()._skipImport()}_renderImportDialog(){return this._onboardingRuntime?this._onboardingRuntime._renderImportDialog():this._editorRuntimeOrThrow()._renderImportDialog()}_sunGlobal(){const e=this._settingsDialog;return e?{...this._settings,north_deg:e.northDeg??void 0,bg_mode:e.bgMode,sun_rays:e.sunRays}:this._settings}_sunSpace(){const e=this._spaceDialog,t=this._curSpaceCfg?.settings||{};return e&&"edit"===e.mode&&e.spaceId===this._space?{...t,north_deg:e.northDeg??void 0,bg_mode:e.bgMode??void 0,sun_rays:e.sunRays??void 0}:t}_effNorth(){return or(this._sunGlobal(),this._sunSpace())}_effBgMode(){return sr(this._sunGlobal(),this._sunSpace())}_dayCycleState(e=new Date){return(this._modeTransitionVisual?.viewWeight??("view"===this._mode?1:0))<=0||"daynight"!==this._effBgMode()?null:Yn(this._renderPlanHass,e)}_syncDayCycleClock(){const e=this._dayCycleState();this._dayCycleClockKey=e?Zn(e):"";const t="clock"===e?.source&&"hidden"!==this.ownerDocument.visibilityState&&this.isConnected;t&&!this._dayCycleTimer?this._dayCycleTimer=window.setInterval(this._dayCycleTick,3e4):!t&&this._dayCycleTimer&&(clearInterval(this._dayCycleTimer),this._dayCycleTimer=0)}_dayCycleVisibility(e){"hidden"!==e.kind?(this._dayCycleTick(),this._syncDayCycleClock()):this._dayCycleTimer&&(clearInterval(this._dayCycleTimer),this._dayCycleTimer=0)}_effSunRays(){return ar(this._sunGlobal(),this._sunSpace())}_sunNow(){return null!==this._effNorth()?lr(this._renderPlanHass):null}_renderSunRays(e){const t=W``,i=this._modeTransitionVisual?.viewWeight??0;if(this._editing&&i<=0||!this._effSunRays())return this._sunFadeReset(),t;const n=this._effNorth(),r=null!==n?lr(this._renderPlanHass):null;if(!r||r.elevation<=0)return this._sunFadeReset(),t;if(o=r.elevation,Number(o)>=3)this._sunOutTimer&&(clearTimeout(this._sunOutTimer),this._sunOutTimer=0),this._sunOut=!1,this._sunShown=!0;else{if(!this._sunShown)return t;this._sunOut||(this._sunOut=!0,this._sunOutTimer=window.setTimeout(()=>{this._sunOutTimer=0,this._sunShown=!1,this._sunOut=!1,this.requestUpdate()},2e3))}var o;const s=this._zeroWalls(),a=s.barriers.map(e=>e.join(",")).join(";"),l=`${e.id}|${r.azimuth}|${r.elevation}|${n}|${this._cfgEpoch}|${s.style}|${a}`;if(!this._sunRaysCache||this._sunRaysCache.key!==l){const t=e.rooms.map(e=>({id:e.id||"",poly:Zt(e)})).filter(e=>!!e.id&&!!e.poly),i=this._openingsR.filter(e=>"window"===e.type&&"partition"!==e.host?.kind).map(e=>({id:e.id,x:e.rx,y:e.ry,angle:e.angle,length:e.rlen})),o=this._spaceWalls,a=this._openCuts(),c=this._openingWallIndexFor(e,a).value,h={},d={},u=this._wallUnionGeometry()?.roomGeom;if(o.length){for(const i of t){const t=this._innerRoomContour(e,i.id,a,u);t&&(h[i.id]=t)}for(const e of i){const t=Vs(c,{x:e.x,y:e.y,angle:e.angle,length:e.length});t.cm>0&&(d[e.id]=vo(t.cm,this._cellCm,this._gridPitch))}}let p=nr(t,i,r.azimuth,r.elevation,n,o.length?h:void 0,o.length?d:void 0);const _=[...this._physicalBodiesR(e),...s.barriers.map(e=>[[e[0],e[1]],[e[2],e[3]]])];_.length&&(p=p.map(e=>{const t=function(e,t,i){return i>0?e.map(e=>function(e){const t=[...e].sort((e,t)=>e[0]-t[0]||e[1]-t[1]);if(t.length<=2)return t;const i=(e,t,i)=>(t[0]-e[0])*(i[1]-e[1])-(t[1]-e[1])*(i[0]-e[0]),n=[];for(const e of t){for(;n.length>=2&&i(n[n.length-2],n[n.length-1],e)<=0;)n.pop();n.push(e)}const r=[];for(let e=t.length-1;e>=0;e--){const n=t[e];for(;r.length>=2&&i(r[r.length-2],r[r.length-1],n)<=0;)r.pop();r.push(n)}return n.slice(0,-1).concat(r.slice(0,-1))}([...e,...e.map(e=>[e[0]+t[0]*i,e[1]+t[1]*i])])).filter(e=>e.length>=3):e}(_,e.dir,e.len),i=e.polys.map(e=>_a(e,t));return{...e,paths:i.map(ra).filter(Boolean),polys:i.flatMap(fa)}}).filter(e=>e.paths?.length||e.polys.length)),this._sunRaysCache={key:l,rays:p,rims:p.map(e=>function(e,t=1e-4){const[i,n]=e.dir,r=-n,o=i,s=[];for(const a of[e.a,e.b]){const l=[];for(const s of e.polys)for(let e=0;et)continue;if(Math.abs((h[0]-a[0])*r+(h[1]-a[1])*o)>t)continue;const d=(c[0]-a[0])*i+(c[1]-a[1])*n,u=(h[0]-a[0])*i+(h[1]-a[1])*n;Math.abs(u-d)<=t||l.push(de[0]-t[0]);const c=[];for(const e of l){const i=c[c.length-1];i&&e[0]<=i[1]+t?i[1]=Math.max(i[1],e[1]):c.push([e[0],e[1]])}for(const[e,t]of c)s.push([[a[0]+i*e,a[1]+n*e],[a[0]+i*t,a[1]+n*t]])}return s}(e))}}const c=this._sunRaysCache.rays,h=this._sunRaysCache.rims;if(!c.length)return t;const d=(u=function(e){const t=Math.min(90,Math.max(-90,Number(e)||0));let i=Xn[Xn.length-1][1];for(let e=1;e + ${c.map((e,t)=>{const i=(e.a[0]+e.b[0])/2,n=(e.a[1]+e.b[1])/2,r=i+e.normal[0]*e.depth,o=n+e.normal[1]*e.depth;return W` + ${p.map(([e,t])=>W``)} + + + ${_.map(([e,t])=>W``)} + `})} + + + ${c.map((e,t)=>e.paths?.length?e.paths.map(e=>W``):e.polys.map(e=>W``))} + ${c.map((e,t)=>(h[t]||[]).map(e=>W``))} + `}_sunFadeReset(){this._sunOutTimer&&(clearTimeout(this._sunOutTimer),this._sunOutTimer=0),this._sunShown=!1,this._sunOut=!1}_compassPoint(e){const t=e.currentTarget.getBoundingClientRect(),i=e.clientX-(t.left+t.width/2),n=e.clientY-(t.top+t.height/2);if(Math.hypot(i,n)<5)return;let r=Math.round(180*Math.atan2(i,-n)/Math.PI);e.shiftKey&&(r=15*Math.round(r/15)),r=(r%360+360)%360,this._settingsDialog={...this._settingsDialog,northDeg:r}}_renderCompass(){const e=this._settingsDialog.northDeg;return B`{e.currentTarget.setPointerCapture(e.pointerId),this._compassDrag=!0,this._compassPoint(e)}} + @pointermove=${e=>{this._compassDrag&&this._compassPoint(e)}} + @pointerup=${()=>this._compassDrag=!1} + @pointercancel=${()=>this._compassDrag=!1}> + + ${[0,45,90,135,180,225,270,315].map(e=>W``)} + + + + ${this._t("gs.north_letter")} + + ${null===e?"—":e+"°"} + `}_stageBg(e){const t=this._settingsDialog,i=this._spaceDialog,n=t?t.bgColor||"":nn(this._settings,{bgColor:null});return(i&&"edit"===i.mode&&i.spaceId===this._space?i.bgColor||"":e.bgColor||"")||n}_stageBgHex(){const e=this._stageEl;if(e){const t=getComputedStyle(e).backgroundColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);if(t)return"#"+t.slice(1,4).map(e=>(+e).toString(16).padStart(2,"0")).join("")}return"#111111"}_preflightDiagnostics(e,t){return this._editorRuntimeOrThrow()._preflightDiagnostics(e,t)}_reportPreflightFailure(e,t){return this._editorRuntimeOrThrow()._reportPreflightFailure(e,t)}_preflightVersionsDiffer(){return this._editorRuntimeOrThrow()._preflightVersionsDiffer()}async _copyPreflightDiagnostics(){return this._editorRuntimeOrThrow()._copyPreflightDiagnostics()}_checkOptimizeGeometry(e){return this._editorRuntimeOrThrow()._checkOptimizeGeometry(e)}_checkSpacePhysicalGeometry(e,t,i){return this._editorRuntimeOrThrow()._checkSpacePhysicalGeometry(e,t,i)}_optimizeReferenceContext(e){return this._editorRuntimeOrThrow()._optimizeReferenceContext(e)}_previewAlignDialog(e){return this._editorRuntimeOrThrow()._previewAlignDialog(e)}async _runAlignToGrid(){return this._editorRuntimeOrThrow()._runAlignToGrid()}async _undoPlanOptimization(){return this._editorRuntimeOrThrow()._undoPlanOptimization()}async _runBackupExport(){return this._editorRuntimeOrThrow()._runBackupExport()}async _pickBackupImport(e){return this._editorRuntimeOrThrow()._pickBackupImport(e)}async _setBackupDuplicatePolicy(e){return this._editorRuntimeOrThrow()._setBackupDuplicatePolicy(e)}async _applyBackupImport(){return this._editorRuntimeOrThrow()._applyBackupImport()}_renderBackupExportDialog(){return this._editorRuntimeOrThrow()._renderBackupExportDialog()}_renderBackupImportDialog(){return this._editorRuntimeOrThrow()._renderBackupImportDialog()}_setFillColor(e,t){return this._editorRuntimeOrThrow()._setFillColor(e,t)}async _saveSettingsDialog(){return this._editorRuntimeOrThrow()._saveSettingsDialog()}_boolInput(e,t,i=!1){return this._editorRuntimeOrThrow()._boolInput(e,t,i)}_rangeInput(e,t,i,n,r,o=!1,s){return this._editorRuntimeOrThrow()._rangeInput(e,t,i,n,r,o,s)}_renderColorRow(e,t){return this._editorRuntimeOrThrow()._renderColorRow(e,t)}get _glowRadiusCm(){const e=Number(this._settings.glow_radius_cm);return Number.isFinite(e)&&e>0?e:300}get _imperial(){return"mi"===this.hass?.config?.unit_system?.length}get _glowRadiusPlaceholder(){const e=this._glowRadiusCm;return this._imperial?String(Math.round(e/30.48*10)/10):String(e/100)}_glowTransition(e,t){let i=this._glowRenderedSources.get(e);if(t){const t=this._glowFadeTimers.get(e);if(null!=t&&(clearTimeout(t),this._glowFadeTimers.delete(e)),null==i){this._suspendGlowFeatherForTransition(),i=++this._glowSourceSeq,this._glowRenderedSources.set(e,i),this._glowEnteringSources.add(e);const t=requestAnimationFrame(()=>{this._glowEnterRafs.get(e)===t&&(this._glowEnterRafs.delete(e),this._glowEnteringSources.delete(e),this.isConnected&&this.requestUpdate())});this._glowEnterRafs.set(e,t)}return{domId:i,entering:this._glowEnteringSources.has(e),leaving:!1}}if(null==i)return null;const n=this._glowEnterRafs.get(e);if(null!=n&&cancelAnimationFrame(n),this._glowEnterRafs.delete(e),this._glowEnteringSources.delete(e),!this._glowFadeTimers.has(e)){this._suspendGlowFeatherForTransition();const t=window.setTimeout(()=>{this._glowFadeTimers.get(e)===t&&(this._glowFadeTimers.delete(e),this._glowRenderedSources.delete(e),this._glowLastAppearance.delete(e),this.isConnected&&this.requestUpdate())},534);this._glowFadeTimers.set(e,t)}return{domId:i,entering:!1,leaving:!0}}_suspendGlowFeatherForTransition(){if(window.matchMedia?.("(prefers-reduced-motion: reduce)").matches)return;this._glowFeatherSuspendUntil=Math.max(this._glowFeatherSuspendUntil,Date.now()+500),clearTimeout(this._glowFeatherResumeTimer);const e=Math.max(0,this._glowFeatherSuspendUntil-Date.now())+17,t=()=>{this._glowFeatherResumeTimer=0,Date.now()=128){const e=this._glowGeometryWarnings.values().next().value;e&&this._glowGeometryWarnings.delete(e)}this._glowGeometryWarnings.add(r),console.warn(`HOUSEPLAN GLOW GEOMETRY FALLBACK: #218, space ${e}, room ${i}, phase ${n}`)}}_lightBarriers(e,t){const i=Tl([this._curSpaceCfg,this._cellCm,this._gridPitch]),n=`${e.id}|${i}`,r=X_(this._lightBarrierPool,n);if(r.hit)return this._lightBarrierCache=r.value,r.value.value;const o=this._zeroWalls(),s=this._openCuts(),a=[...o.transmissive],l=Math.max(this._cmToUnits(10),.5*this._gridPitch),c=e=>t.some(t=>this._pointInRoom(e,t.r)),h=this._openingsR.filter(e=>{if("door"!==(t=String(e.type))&&"gate"!==t&&"passage"!==t)return!1;var t;const i=e.angle*Math.PI/180,n=-Math.sin(i),r=Math.cos(i);return c([e.rx+n*l,e.ry+r*l])&&c([e.rx-n*l,e.ry-r*l])}),d=this._roomWallOpeningInputs(h,e);for(const e of d){const t=e.angle*Math.PI/180,i=Math.cos(t)*e.length/2,n=Math.sin(t)*e.length/2;a.push([e.x-i,e.y-n,e.x+i,e.y+n])}const u=new Set(h.filter(e=>e.partitionHost).map(e=>e.id)),p=[e.id,this._cfgEpoch,this._cellCm,this._gridPitch,[...u].sort().join(",")].join("|");this._lightPhysicalBodiesCache?.key!==p&&(this._lightPhysicalBodiesCache={key:p,all:ca(e,this._cellCm,this._gridPitch,2e-4*this._gridPitch,this._partitionOpeningCuts(e,e=>u.has(e.id))).all});const _=this._lightPhysicalBodiesCache.all,m=this._spaceWalls,g=.02*this._gridPitch,f=[],v=this._wallUnionGeometry(),y=v?.sourceFingerprint,b=v&&y===i?function(e,t=[],i=[],n={}){if("failed-core"===e.status||"not-applicable"===e.status)return null;const r=e.roomComponents?.length?e.roomComponents:$s(e.roomGeom)?[{id:"room-primary",geom:e.roomGeom}]:[];let o=r[0]?.geom||null;const s=r.slice(1).map(e=>({...e}));try{for(const i of t){if(!(i.length>0&&e.openingIndex))continue;const t=Gs(e.openingIndex,i,!0);if(!t.negative&&!t.positive)continue;const n=i.angle*Math.PI/180,r=Math.cos(n),a=Math.sin(n),l=-a,c=r,h=i.length/2,d=e.openingPadUnits??1.25*Math.max(e.depthUnits,1),u=xs([[i.x-r*h-l*d,i.y-a*h-c*d],[i.x+r*h-l*d,i.y+a*h-c*d],[i.x+r*h+l*d,i.y+a*h+c*d],[i.x-r*h+l*d,i.y-a*h+c*d]]);o&&(o=Ht(o,u));for(const e of s)e.geom=Ht(e.geom,u)}const r=[...s];let a=s.length;const l=n.mergeExtra||((e,t)=>e?Et(e,t):t);for(let e=0;ee.length>=2&&Number.isFinite(e[0])&&Number.isFinite(e[1]))||Math.abs(yi(t))<=1e-9){a++;continue}const n=[xs(t)];try{const t=l(o,n,e);if(!$s(t))throw new Error("invalid extra union");o=t}catch{a++,$s(n)&&r.push({id:`policy-extra-${e}`,geom:n})}}r.sort((e,t)=>Fs(e.geom).localeCompare(Fs(t.geom)));const c=o||[];return{status:a?"degraded-extra":"ok",geom:c,components:[...$s(c)?[{id:"primary",geom:c}]:[],...r.map((e,t)=>({...e,id:`isolated-${t}`}))]}}catch{return null}}(v,d,_):null,w=b||(m.length||_.length?Ns(e.rooms,m,s,d,this._wallKeyPitch,this._cellCm,this._gridPitch,cm,_):null);if(!w||"ok"!==w.status&&"degraded-extra"!==w.status)for(const e of _)f.push(...Hd(e));else for(const e of w.components)for(const t of ga(e.geom))f.push(...Hd(t));for(const{poly:e}of t)for(const t of a.length?kn(e,a,g):Hd(e))f.push(t);for(const e of o.barriers)f.push(e);const k={occluders:Nd(f),floor:t.map(e=>e.poly),fingerprint:i,masonryGeometry:!w||"ok"!==w.status&&"degraded-extra"!==w.status?[]:w.components.flatMap(e=>e.geom),opaqueBodies:_},x={key:n,value:k};return J_(this._lightBarrierPool,n,x,8),this._lightBarrierCache=x,k}_renderGlowLayer(e,t){const i=this._fillColors,n=this._glowRadiusCm/this._cellCm*this._gridPitch,r=e.rooms.map(e=>({r:e,poly:Zt(e)})).filter(e=>!!e.poly),o=r.filter(({r:e})=>Rn(t.glow,e));if(!o.length)return this._forgetGlowSpace(e.id),W``;const{occluders:s,floor:a,fingerprint:l,masonryGeometry:c,opaqueBodies:h}=this._lightBarriers(e,r),d=Mh(this._renderPlanHass,this._renderDevices,null,this._virtualLights).filter(t=>t.device.space===e.id),u=new Map;for(const e of d){if(!e.device.id)continue;const t=u.get(e.device.id)||[];t.push(e),u.set(e.device.id,t)}const p=[],_=new Set;for(const t of this._renderDevices){if(t.space!==e.id)continue;const o=Th(u.get(t.id)||[]);if(!o)continue;const d=`${e.id}|${t.id}`;_.add(d);const m=fn(o.passive?{state:o.on?"on":"off",attributes:{}}:this._renderPlanHass.states[o.eid],t.marker?.glow_color,i.glow_light.c),g=Number(t.marker?.glow_radius_cm),f=Number.isFinite(g)&&g>0?g/this._cellCm*this._gridPitch:n,v=this._pos(t);if(ya([v.x,v.y],c,h)){this._forgetGlowSource(d);continue}const y=this._glowTransition(d,!!m);if(!y)continue;m&&this._glowLastAppearance.set(d,{c:m.c,alpha:vn(m.bri,i.glow_light.a)});const b=this._glowLastAppearance.get(d);if(!b)continue;let w=null;const k=`${e.id}|${l}|${v.x.toFixed(4)},${v.y.toFixed(4)}|${f.toFixed(4)}`,x=X_(this._glowClipCache,k);if(x.hit)w=x.value;else{const t=qd([v.x,v.y],f,s,96);w={lit:t.length>=3?pa([t],a,{onBoundsFailure:({boundIndex:t,phase:i})=>{const n=r[t]?.r;this._warnGlowGeometryFallback(e.id,l,n?.id||`#${t}`,i)}}):[]},J_(this._glowClipCache,k,w,256)}p.push({key:d,sourceEid:o.eid,domId:y.domId,entering:y.entering,leaving:y.leaving,pos:v,c:b.c,alpha:b.alpha,geometry:w,r:f})}const m=`${e.id}|`;for(const e of this._glowRenderedSources.keys())e.startsWith(m)&&!_.has(e)&&this._forgetGlowSource(e);if(!p.length)return W``;const g=this._spaceWalls,f=o.length===r.length?[]:this._openCuts(),v=this._wallUnionGeometry()?.roomGeom,y=o.length===r.length?null:o.map(({r:t,poly:i})=>{const n=g.length&&t.id&&this._innerRoomContour(e,t.id,f,v)||i,o=this._cleanFloor(t,n,e).path,s=vi(n,r.filter(e=>e.r!==t).map(e=>e.poly)),a=e=>"M "+e.map(e=>e[0]+" "+e[1]).join(" L ")+" Z";return[o||a(n),...s.map(a)].join(" ")}),b=this._viewOr(this._baseVb()),w=this._stageEl?.clientWidth&&b.w?this._stageEl.clientWidth/b.w:1,k=1/(w>0?w:1),x=!this._pinchStart&&!this._panStart&&Date.now()>=this._glowFeatherSuspendUntil;(null==this._glowFeatherUnits||x)&&(this._glowFeatherUnits=k);const $=this._glowFeatherUnits??k,S=4*$,M=p.reduce((e,t)=>({x:Math.min(e.x,t.pos.x-t.r-S),y:Math.min(e.y,t.pos.y-t.r-S),maxX:Math.max(e.maxX,t.pos.x+t.r+S),maxY:Math.max(e.maxY,t.pos.y+t.r+S),w:0,h:0}),{x:1/0,y:1/0,maxX:-1/0,maxY:-1/0,w:0,h:0});return M.w=M.maxX-M.x,M.h=M.maxY-M.y,W` + ${$e(p,e=>e.key,e=>{const t=e.domId,i=e.geometry;return W` + ${""} + + ${hm.map(([t,i])=>W` + `)} + + ${""} + ${i?W` + + + `:G}`})} + ${y?W`${y.map(e=>W``)}`:G} + ${""} + + + + + ${""} + + + ${$e(p,e=>e.key,e=>{const t=e.domId;return W` + + + `})} + + `}_renderAlignDialog(){return this._editorRuntimeOrThrow()._renderAlignDialog()}_renderSettingsDialog(){return this._editorRuntimeOrThrow()._renderSettingsDialog()}_rulesSet(e){return this._editorRuntimeOrThrow()._rulesSet(e)}async _saveRules(){return this._editorRuntimeOrThrow()._saveRules()}_renderRulesDialog(){return this._editorRuntimeOrThrow()._renderRulesDialog()}_saveKioskScale(e){return this._editorRuntimeOrThrow()._saveKioskScale(e)}_renderKioskDialog(){return this._editorRuntimeOrThrow()._renderKioskDialog()}_fixedFloorValue(e){if("string"==typeof e)return e||"''";try{const t=JSON.stringify(e);return(void 0===t?String(e):t).slice(0,160)}catch{return String(e).slice(0,160)}}render(){const e=!!this._importDialog||!(!this._spaceDialog||!this._spaceDialogUsesOnboardingRuntime(this._spaceDialog.mode)),t="view"!==this._mode||!!(this._roomDialog||this._mergeDialog||this._openingDialog||this._physicalDialog||this._partitionDeleteDialog||this._roomDeleteDialog||this._decorTextDialog||this._decorShapeDialog||this._backdropDialog||this._decorEraseConfirm||this._spaceDialog&&!this._spaceDialogUsesOnboardingRuntime(this._spaceDialog.mode)||this._deviceInbox||this._markerDialog||this._rulesDialog||this._settingsDialog||this._alignDialog||this._backupExportDialog||this._backupImportDialog||this._kioskDialog||this._vacFit||this._vacCalConfirm);if(e&&!this._onboardingRuntime&&this._ensureOnboardingRuntime(),t&&!this._editorRuntime&&this._ensureEditorRuntime(),!this._config||!this.hass)return G;const i=this._model,n=this.houseplanDiagnostics(),r=this._fixedFloorState(i);if("pending"===r.kind)return B` +
+
${this._config.title||this._t("card.title")}
+
+
+ +

${this._t("fixed_floor.loading")}

+
+
`;if("invalid"===r.kind)return B` +
+
${this._config.title||this._t("card.title")}
+
+ +
`;if(!i.length)return B` +
+
${this._config.title||this._t("card.title")}
+
+
+ +

${this._t("empty.no_spaces")}

+ ${this._serverStorage?B`

${this._t("empty.add_first")}

+ `:B`

${this._t("empty.install")}

`} +
+ ${this._spaceDialog&&(this._onboardingRuntime||this._editorRuntime)?this._renderSpaceDialog():G} + ${this._importDialog&&this._onboardingRuntime?this._renderImportDialog():G} + ${this._toast?B``:G} +
`;const o=this._spaceModel();if(!o)return G;const s="valid"===r.kind?[o]:i,a=o.vb,l=this._effectiveProjection();this._renderProjection=l,this._labs.active.length&&function(e){p_||y_(e);const t=u_,i=t.active.join(",");if(!i||i===m_)return;m_=i;const n=new Map(n_.map(e=>[e.id,e])),r=t.active.map(e=>{const t=n.get(e);return`${e} (#${t.issue}, expires ${t.expires})`}).join(", ");console.info(`HOUSEPLAN LABS: ${r}`)}(K_);const c="iso"===l,h="devices"===this._mode&&this._showAll,d=this._renderDevices.filter(e=>e.space===o.id&&(!e.hidden||h)),u=this._renderDeviceSnapshot,p=this._spaceDisplayForRender(),_=c?this._isoDecorationLayers(p):null,m=_?this._isoOpeningPanels(_):[],g=this._resolvedRoomFills(o,p),f=this._resolvedGlowBase(o,p,g),v=p.showLqi??this._config.show_signal??!0,y=this._config.icon_size??2.5,b=y>8?2.5:y,w=Bu(b),k=this._viewOr(this._baseVb()),x=this._floorView(k),$=this._editing?"":this._stageBg(p),S=this._opMeasureView,M=this._decorMeasure,T=this._bdLive,R=this._furnLive,C="view"===this._mode?this._editorChromeMode:this._mode,D=this._roomHoverPaths(o),A=o.bg?this._display(o.bg.href):"",O=this._continuity.overlayVisible||"recovery-error"===this._continuity.state?this._continuity.recoveryReason:null,z=this._modeTransitionVisual,P=this._dayCycleState(),F=z?.viewWeight??("view"===this._mode?1:0),I=this._modeTransition.state?.from.presentedMode,E=!this._markup||!!z&&("view"===z.presentedMode||"devices"===z.presentedMode||"view"===I||"devices"===I),N=z?.stageColor||$,H=z?.sceneBrightness??1;return B` + this._notePointer(e)} + @pointerdown=${this._touchGestureGuard} + @pointermove=${this._touchGestureGuard} + @pointerup=${this._touchGestureGuard} + @pointercancel=${this._touchGestureGuard} + @lostpointercapture=${this._touchGestureGuard} + @click=${this._touchGestureGuard}> +
+
+
+ + ${this._config.title||this._t("card.title")} +
+
this._tabPointerMove(e)}> + ${s.map(e=>B``)} + ${""} + ${!this._canEdit||this._kiosk||this._hasFixedFloor?G:B``} +
+ ${this._canEdit?B`
+ ${[["plan","mdi:floor-plan"],["devices","mdi:tune-variant"],["decor","mdi:draw"]].map(([e,t])=>B``)} +
`:G} + ${this._t("count.devices",{n:d.filter(e=>!e.hidden).length})} + + ${this._labsIso&&"view"===this._mode&&!this._kiosk?B``:G} +
+ + ${""} + + +
+ ${this._norm&&this._canEdit?B``:G} +
+ ${this._canEdit&&!this._kiosk?B`
+
+ ${this._editorRuntime?"plan"===C?this._renderMarkupBar():"devices"===C?this._renderDevicesBar():this._renderDecorBar():G} +
+
`:G} +
+ +
this._markupClick(e)} + @wheel=${e=>this._onWheel(e)} + @pointerdown=${e=>{this._notePointer(e),this._stagePointerDown(e)}} + @pointermove=${e=>this._stagePointerMove(e)} + @pointerleave=${e=>this._stagePointerLeave(e)} + @pointerup=${e=>this._stagePointerUp(e)} + @pointercancel=${e=>this._stagePointerCancel(e)}> + ${dr(P,F)} + ${this._editorRuntime?this._renderEditorSecondary():G} +
+ ${c&&_?.structural?W``:G} + + + ${""} + ${this._wallHatchDefs(p.color)}${W`${this._paperShapes(o.rooms).map(e=>"path"in e?W``:"poly"in e?W``:W``)}`} + ${this._editing?this._renderMarkupDefs(a):G} + ${""} + ${this._editing&&!this._markup&&this._gridLevels()?W``:G} + ${o.bg&&A?W`this._onBackdropLoaded(o.bg.href,A)} + transform=${o.bg.angle?`rotate(${o.bg.angle} ${o.bg.x+o.bg.w/2} ${o.bg.y+o.bg.h/2})`:G} + @dblclick=${e=>this._openBackdropDialog(e)} + preserveAspectRatio="none" />`:G} + ${(()=>{const e=this._openCuts(),t=this._thickWallCuts(),i=this._wallUnionGeometry()?.roomGeom,n=new Map,r=e=>(n.has(e)||n.set(e,Zt(e)),n.get(e));return o.rooms.filter(e=>e.area||"view"===this._mode||this._markup||p.showBorders).map(n=>{let s="room "+(o.bg?"overlay":"yard")+(this._markup?" outlined":"");!this._markup||n.id!==this._mergeSel&&n.id!==this._splitSel?.roomId||(s+=" picked");let a="";const l=Tn(p.fill,n);if(!this._markup&&(p.showBorders||"none"!==l)){s+=" styled";const e=[];e.push(`--room-stroke:${p.color}`,`--room-stroke-op:${p.showBorders?p.opacity:0}`);const t=g.byRoom.get(n)||null;t?(s+=" filled",e.push(`--room-fill:${t.color}`,`--room-fill-op:${t.opacity}`)):e.push("--room-fill:transparent","--room-fill-op:0"),a=e.join(";")}let c;const h=e=>{"view"===this._mode&&(void 0===c&&(c=this._roomArea(n)),this._showTip(e,n.name||this._t("room.unnamed"),c?this._t("tip.area",{value:c}):"",v?this._roomLqi(n.area):null,this._roomTemp(n),this._roomHum(n)))},d=r(n),u=this._markup&&(n.id===this._mergeSel||n.id===this._splitSel?.roomId),_=d&&!u?e.filter(e=>{const t=[(e[0]+e[2])/2,(e[1]+e[3])/2];return d.some((e,i)=>Pn(t,[e[0],e[1],...d[(i+1)%d.length]])<=.08*this._gridPitch)}):[],m=u?[]:t,f=_.concat(m);f.length&&(s+=" noedge");const y=this._spaceWalls.length&&n.id&&d&&this._innerRoomContour(o,n.id,e,i)||d,b=y?vi(y,(w=n,o.rooms.filter(e=>e!==w).map(r).filter(Boolean))):[];var w;const k=e=>"M "+e.map(e=>e[0]+" "+e[1]).join(" L ")+" Z",x=y?this._cleanFloor(n,y,o).path:"",$=n.id||G,S=n.area||G,M=x&&y?W`{this._notePointer(e),this._pointerModality.hoverEnabled&&(this._hoverRoom={space:o.id,room:n})}} + @pointermove=${h} + @pointerleave=${()=>this._clearTransientHover()}>`:b.length&&y?W`{this._notePointer(e),this._pointerModality.hoverEnabled&&(this._hoverRoom={space:o.id,room:n})}} + @pointermove=${h} + @pointerleave=${()=>this._clearTransientHover()}>`:y&&y!==d?W`{this._notePointer(e),this._pointerModality.hoverEnabled&&(this._hoverRoom={space:o.id,room:n})}} + @pointermove=${h} + @pointerleave=${()=>this._clearTransientHover()}>`:n.poly?W`{this._notePointer(e),this._pointerModality.hoverEnabled&&(this._hoverRoom={space:o.id,room:n})}} + @pointermove=${h} + @pointerleave=${()=>this._clearTransientHover()}>`:W`{this._notePointer(e),this._pointerModality.hoverEnabled&&(this._hoverRoom={space:o.id,room:n})}} + @pointermove=${h} + @pointerleave=${()=>this._clearTransientHover()}>`,T=f.length&&d?kn(d,f,.02*this._gridPitch):null,R=T?W``:G;return W`${M}${R}`})})()} + ${this._renderRoomHoverFill(D)} + ${this._renderOpeningTunnelFills(o,g)} + ${this._renderGlowBaseRooms(o,f)} + ${this._renderOpeningTunnelFills(o,f,"glow-base")} + ${""} + ${p.hideDecor&&"decor"!==this._mode?G:this._renderDecorLayer()} + ${E?this._renderGlowLayer(o,p):G} + ${this._renderSunRays(o)} + ${this._editing?W`${this._renderAlignGuides()}`:G} + ${this._markup?W`${this._renderMarkupLayer(a)}`:G} + ${""} + ${""} + ${this._editing?G:this._renderZeroWalls(p)} + ${this._renderWallBodies(p)} + ${this._markup&&"resize"===this._tool?this._renderResizeMeasurements():G} + ${this._renderRoomHoverOutline(D)} + ${""} + ${this._editing?this._renderZeroWalls(p):G} + ${this._markup?W`${this._renderHiddenWallDiagnosticOverlay()}`:G} + ${this._markup?W`${this._renderOpeningPlacementPreview()}`:G} + ${S?this._renderOpeningDimensionGuides(S):G} + ${S?.guide?this._renderOpeningCenterTick(S.guide):G} + ${""} + ${""} + ${this._markup?W`${this._renderActiveChainInk()}`:G} + ${this._markup?W`${this._renderPlanSnapOverlay()}`:G} + ${p.hideOpenings&&!this._markup||_&&!_.floorSymbols?G:this._renderOpenings(p)} + ${this._renderWallThickUi()} + ${this._markup&&"resize"===this._tool?this._renderResizeLayer(k):G} + ${""} + ${this._renderBackdropFrame(k)} + ${this._renderTextFrame(k)} + + + ${c&&_?.structural?W` + `:G} + ${""} +
+ ${d.map(e=>this._renderDevice(e,k,v))} + ${this._renderVacuums(d,k)} + ${this._renderVacFit(k)} + ${this._renderOpeningLocks(k)} + ${p.showNames||this._markup?o.rooms.map(e=>this._renderRoomLabel(e,o,k,p)):G} + ${this._markup?o.rooms.map(e=>this._renderRoomGear(e,o,k)):G} +
+ ${this._measureAnchor?B`
${this._renderMeasureLabel(k)}
`:G} + ${this._resize?.liveLabels?B`
${this._resize?.liveLabels?.map(e=>B`
${e.text}
`)}
`:G} + ${S?B`
${S.labels.map(e=>B`
${e.text}
`)}
`:G} + ${this._wallDialog?B`
${this._renderWallThickDialog()}
`:G} + ${M?B`
${M.text}
`:G} + ${R?B`
${R.map(e=>B`
${e.text}
`)}
`:G} + ${T?B`
${T.text}
`:G} +
+ ${this._zoom>1?B`
${Math.round(100*this._zoom)}%
`:G} + ${this._renderFarHint()} + ${this._renderHomeArrow()} + ${this._renderEditorRuntimeLoading()} + ${this._renderRecoveryOverlay()} + ${this._booting||this._bootFading?B``:G} +
+ + ${this._roomDialog&&this._editorRuntime?this._renderRoomDialog():G} + ${this._mergeDialog&&this._editorRuntime?this._renderMergeDialog():G} + ${this._openingDialog&&this._editorRuntime?this._renderOpeningDialog():G} + ${this._physicalDialog&&this._editorRuntime?this._renderPhysicalDialog():G} + ${this._partitionDeleteDialog&&this._editorRuntime?this._renderPartitionDeleteDialog():G} + ${this._roomDeleteDialog&&this._editorRuntime?this._renderRoomDeleteDialog():G} + ${this._openingInfo?this._renderOpeningInfoCard():G} + ${this._decorTextDialog&&this._editorRuntime?this._renderDecorTextDialog():G} + ${this._decorShapeDialog&&this._editorRuntime?this._renderDecorShapeDialog():G} + ${this._backdropDialog&&this._editorRuntime?this._renderBackdropDialog():G} + ${this._decorEraseConfirm&&this._editorRuntime?this._renderDecorEraseConfirm():G} + ${this._spaceDialog&&(this._onboardingRuntime||this._editorRuntime)?this._renderSpaceDialog():G} + ${this._deviceInbox&&this._editorRuntime?this._renderDeviceInbox():G} + ${this._markerDialog&&this._editorRuntime?this._renderMarkerDialog():G} + ${this._vacCalConfirm?B`this._vacCalConfirm=null}> +
+

${this._t("vac.residual_message",{error:this._vacCalConfirm.error})}

+
+
+ + + + +
+
`:G} + ${this._infoCard?this._renderInfoCard():G} + ${this._rulesDialog&&this._editorRuntime?this._renderRulesDialog():G} + ${this._settingsDialog&&this._editorRuntime?this._renderSettingsDialog():G} + ${this._alignDialog&&this._editorRuntime?this._renderAlignDialog():G} + ${this._backupExportDialog&&this._editorRuntime?this._renderBackupExportDialog():G} + ${this._backupImportDialog&&this._editorRuntime?this._renderBackupImportDialog():G} + ${this._importDialog&&this._onboardingRuntime?this._renderImportDialog():G} + ${this._tip?B`
+ ${this._tip.title}${this._tip.meta?B`${this._tip.meta}`:G} + ${null!=this._tip.temp?B`${this._t("tip.temp_avg")} ${this._tip.temp}°`:G} + ${null!=this._tip.hum?B`${this._t("tip.hum_avg")} ${this._tip.hum}%`:G} + ${null!=this._tip.lqi?B`${this._t("tip.lqi")} + ${this._tip.lqi}`:G} +
`:G} + ${this._kiosk&&!this._hasFixedFloor&&this._kioskDots&&this._model.length>1?B`
+ ${this._model.map(e=>B``)} +
`:G} + ${this._kioskDialog&&this._editorRuntime?this._renderKioskDialog():G} + ${this._vacFit?B`
+ ${this._t("vac.fit_hint")} + + + + +
`:G} + ${this._tapConfirm?B`this._tapConfirm=null}> +
+ ${"run"===this._tapConfirm.kind?B`

${this._tapConfirm.text}

`:this._tapConfirm.lines.map((e,t)=>B` +

${e}

`)} +
+
+ + + +
+
`:G} + ${this._toast?B``:G} +
+ `}_vacCandidateStatus(e,t,i=this._planHass){const n=this._bindingStatus("entity:"+e);if("ha_disabled"===n.kind)return"disabled";if("orphaned"===n.kind)return"missing";if("unverified"===n.kind)return"unverified";const r=i?.states?.[e];return"unavailable"===r?.state?"unavailable":t?.hasPosition?"ok":"unsupported"}_vacOpenAllCameras(e){const t=this._haRegistry.entities||{},i=[];for(const[e,n]of Object.entries(this.hass?.states||{})){if(!e.startsWith("camera."))continue;const r=Gl(e,n,t[e]);r&&i.push(r)}this._vacAllCameraCache={devId:e.id,candidates:i},this._vacAllCamerasFor=e.id}_vacSourceResolution(e,t=!1,i=this._planHass){const n=e.marker?.vacuum,r="string"==typeof n?.source&&!!n.source,o=new Set(e.entities||[]),s=new Set(o);r&&s.add(n.source);const a=i?.entities||{},l=[];for(const e of s){const t=i?.states?.[e],o=Gl(e,t,a[e]);o?l.push(o):r&&e===n.source&&l.push({entityId:e,name:String(t?.attributes?.friendly_name||e),platform:a[e]?.platform?String(a[e].platform):null,category:e.startsWith("camera.")?"camera":"partial",hasPosition:!1,hasRooms:!1,hasPath:!1,hasMapId:!1,score:0})}if(t&&this._vacAllCameraCache?.devId===e.id){const e=new Set(l.map(e=>e.entityId));for(const t of this._vacAllCameraCache.candidates)e.has(t.entityId)||l.push(t)}const c={};for(const e of l)c[e.entityId]=this._vacCandidateStatus(e.entityId,e,i);return function(e,t,i,n){const r="string"==typeof e&&e.length>0,o=new Set(t),s=Array.from(i).sort((t,i)=>r&&t.entityId===e?-1:r&&i.entityId===e?1:i.score-t.score||t.entityId.localeCompare(i.entityId));if(r){const t=s.find(t=>t.entityId===e);return{entityId:e,status:n[e]||(t?.hasPosition?"ok":"unverified"),pinned:!0,candidates:s}}const a=s.find(e=>o.has(e.entityId)&&e.hasPosition&&"ok"===n[e.entityId]);return a?{entityId:a.entityId,status:"ok",pinned:!1,candidates:s}:{entityId:null,status:"none",pinned:!1,candidates:s}}(n?.source,o,l,c)}_vacSource(e,t=this._planHass){if(!1===e.marker?.vacuum?.live)return null;const i=this._vacSourceResolution(e,!1,t);return"ok"===i.status||"unsupported"===i.status?i.entityId:null}_vacEntity(e){return e.primary?.startsWith("vacuum.")?e.primary:(e.entities||[]).find(e=>e.startsWith("vacuum."))||null}_isVacDev(e){return!!this._vacEntity(e)}_activitySourceKey(e){return this._activitySnapshot(e).sourceKey}_activitySnapshot(e,t=Mh(this._planHass,this._devices,null,this._virtualLights)){const i=Cu(this._planHass,e,this._devices,t,this._fullRegistryHass);return{samples:i.samples,sourceKey:Ou(this._planHass,e,i)}}_stampActivity(e,t,i){let n=this._activityRt.get(e);n||(n=kp(i||"",[]),this._activityRt.set(e,n)),null!=i&&(n.sources=i),$p(n,t,Date.now(),window.clearTimeout.bind(window),e=>window.setTimeout(()=>this.requestUpdate(),e))}_syncActivityRuntime(){const e=new Map;if(!this.hass)return e;if(!1===this._config?.live_states){for(const e of this._activityRt.values())clearTimeout(e.timer);return this._activityRt.clear(),e}const t=new Set,i=Mh(this._planHass,this._devices,null,this._virtualLights);for(const n of this._devices){if(n.hidden)continue;if("icon_ripple"!==Ai(n.marker?.display))continue;t.add(n.id);const r=this._activitySnapshot(n,i);e.set(n.id,r);const{samples:o,sourceKey:s}=r;let a=this._activityRt.get(n.id);a?a.sources!==s&&xp(a,s,o,window.clearTimeout.bind(window)):(a=kp(s,o),this._activityRt.set(n.id,a))}for(const[e,i]of this._activityRt)t.has(e)||(clearTimeout(i.timer),this._activityRt.delete(e));return e}_activityTick(){if(!this.hass)return;const e=this._syncActivityRuntime();for(const t of this._devices){if(t.hidden)continue;if("icon_ripple"!==Ai(t.marker?.display))continue;const i=e.get(t.id)||this._activitySnapshot(t),{samples:n,sourceKey:r}=i,o=this._activityRt.get(t.id);if(!o||o.sources!==r)continue;const s=Sp(o,n,window.clearTimeout.bind(window));s&&this._stampActivity(t.id,s,r)}}_vacTick(){if(this.hass)for(const e of this._devices){if(e.hidden||!this._isVacDev(e))continue;if("static_icon"===Ai(e.marker?.display)){this._vacRt.delete(e.id);continue}const t=this._vacSource(e);if(!t)continue;const i=this._vacEntity(e),n=Xl(this.hass.states[i||""]?.state),r=Bl(this.hass.states[t]?.attributes);let o=this._vacRt.get(e.id);o||(o={trail:[],lastKey:"",lastTs:0,moving:!1,jump:!1,endedTs:0,lastPos:null},this._vacRt.set(e.id,o)),n&&!o.moving&&(o.trail=[],o.lastPos=null);const s="never"!==nc(e.marker?.vacuum)&&!r?.path.length;!n&&o.moving&&(o.endedTs=Date.now(),s&&o.lastPos&&(o.trail=Zl(o.trail,o.lastPos,40)),o.lastPos=null),o.moving=n;const a=r?.pos;if(n&&a){const e=a.x+":"+a.y;if(e!==o.lastKey){const t=Date.now();o.jump=o.lastTs>0&&t-o.lastTs>1e4,o.lastKey=e,o.lastTs=t,s&&o.lastPos&&(o.trail=Zl(o.trail,o.lastPos,40)),o.lastPos=[a.x,a.y]}}}}_vacEnsureMarker(e){const t=this._serverCfg;if(!t)return null;t.markers=t.markers||[];const i=t.markers.find(t=>t.id===e.id);if(i)return i;if("device"!==e.bindingKind&&"entity"!==e.bindingKind||!e.bindingRef)return null;const n={id:e.id,binding:e.bindingKind+":"+e.bindingRef,space:e.space||null,area:e.area||null,hidden:!!e.hidden};return t.markers.push(n),n}_renderVacSection(e){return this._editorRuntimeOrThrow()._renderVacSection(e)}_vacMapId(e,t,i=this._planHass){return this._editorRuntimeOrThrow()._vacMapId(e,t,i)}_vacSaveMatrix(e,t,i,n){return this._editorRuntimeOrThrow()._vacSaveMatrix(e,t,i,n)}_vacPlanRoomAnchors(e){return this._editorRuntimeOrThrow()._vacPlanRoomAnchors(e)}_vacAutoCalibrate(e){return this._editorRuntimeOrThrow()._vacAutoCalibrate(e)}_vacApplyCalibrationProposal(e){return this._editorRuntimeOrThrow()._vacApplyCalibrationProposal(e)}_vacStartFit(e){return this._editorRuntimeOrThrow()._vacStartFit(e)}_vacFitSave(){return this._editorRuntimeOrThrow()._vacFitSave()}_vacFitTurn(e){return this._editorRuntimeOrThrow()._vacFitTurn(e)}_vacGhostCentre(e){return this._editorRuntimeOrThrow()._vacGhostCentre(e)}_vacDelta(e,t,i){return this._editorRuntimeOrThrow()._vacDelta(e,t,i)}_vacFitPointer(e,t){return this._editorRuntimeOrThrow()._vacFitPointer(e,t)}_renderVacFit(e){return this._editorRuntime?this._editorRuntimeOrThrow()._renderVacFit(e):G}_vacRafLoop(){this._vacRaf=requestAnimationFrame(()=>{const e=this.renderRoot,t=this._stageEl,i=this._vacLastView,n=e?.querySelectorAll?.(".vacpuck")||[];if(!t||!i||!n.length)return void(this._vacRaf=0);const r=t.getBoundingClientRect();for(const t of n){const n=t.getAttribute("data-mid"),o=t.getBoundingClientRect(),s=i.x+(o.left+o.width/2-r.left)/r.width*i.w,a=i.y+(o.top+o.height/2-r.top)/r.height*i.h;for(const t of e.querySelectorAll(`line.tip[data-mid="${n}"]`))t.setAttribute("x2",s.toFixed(1)),t.setAttribute("y2",a.toFixed(1))}this._vacRafLoop()})}_renderVacuums(e,t){if(this._markup||"decor"===this._mode)return G;const i=this._space+"|"+t.x+"|"+t.y+"|"+t.w+"|"+t.h,n=this._vacJumpOnce||i!==this._vacViewKey;this._vacViewKey=i,this._vacJumpOnce=!1;const r=[],o=[];for(const i of e){if(i.hidden||!this._isVacDev(i))continue;if("static_icon"===Ai(i.marker?.display))continue;const e=this._renderDeviceSnapshot?.facts.get(`vacuum:${i.id}`),s=e?.source??this._vacSource(i,this._renderPlanHass);if(!s)continue;const a=e?.telemetry??Bl(this._renderPlanHass?.states[s]?.attributes);if(!a)continue;const l=String(e?.mapId??this._vacMapId(i,a,this._renderPlanHass)),c=i.marker?.vacuum?.calibration?.[l];if(!c||6!==c.length)continue;const h=e?.runtime??this._vacRt.get(i.id),d=h?.moving??!1,u=nc(i.marker?.vacuum),p="always"===u||"cleaning"===u&&d,_=e?.server??this._vacSrvTrails[i.id],m=_?.current?.map_id===l&&Array.isArray(_.current.points)?_.current:null,g=_?.previous?.map_id===l&&Array.isArray(_.previous.points)?_.previous:null;if("always"===u&&g&&g.points.length>1){const e=g.points.map(([e,t])=>{const[i,n]=zl(c,e,t),r=this._scenePoint([i,n]);return r[0].toFixed(1)+","+r[1].toFixed(1)}).join(" ");o.push(W``)}if(p){const e=Ll(a,m,h?.trail||[]),t=!d||"integration"!==e.source&&"server"!==e.source?e.path:jl(e.path);if(t.length){const e=t.map(e=>e.map(([e,t],i)=>{const[n,r]=zl(c,e,t),o=this._scenePoint([n,r]);return`${i?"L":"M"} ${o[0].toFixed(1)} ${o[1].toFixed(1)}`}).join(" ")).join(" ");o.push(W``);const n=t[t.length-1];if(d&&n?.length>=2){const e=n[n.length-1],[t,r]=zl(c,e[0],e[1]),s=this._scenePoint([t,r]),a=s[0].toFixed(1),l=s[1].toFixed(1);o.push(W``)}}}if(!d||!a.pos)continue;const[f,v]=zl(c,a.pos.x,a.pos.y),y=this._scenePoint([f,v]),b=(y[0]-t.x)/t.w*100,w=(y[1]-t.y)/t.h*100,k=h&&h.lastTs>0&&Date.now()-h.lastTs>6e4,x=i.marker?.icon||i.icon||"mdi:robot-vacuum";r.push(B`
{e.stopPropagation();const t=this._vacEntity(i);t&&this._openMoreInfo(t)}}> + +
`)}return this._vacLastView=t,r.length&&!this._vacRaf&&this._vacRafLoop(),r.length||o.length?B` + ${o.length?W`${o}`:G} + ${r}`:G}_renderDevice(e,t,i=!0){const n=this._pos(e),r=this._scenePoint([n.x,n.y]),o=(r[0]-t.x)/t.w*100,s=(r[1]-t.y)/t.h*100,a=this._devicePresentation(e,i),l=[`left:${o}%`,`top:${s}%`,...Lu(a)],c=a.disabledReason,h=a.haDisabled?this._t(`marker.ha_disabled_${c}`):e.userHidden?this._t("marker.hidden_ghost"):e.name,d=xu(a),u="view"===this._mode||"devices"===this._mode,p=[h,a.haDisabled?"":this._t(`marker.state_a11y_${d}`),"none"!==a.pulse.kind?this._t(`marker.pulse_a11y_${a.pulse.reason}`):"",a.valueFullText||a.valueText||"",nu(a.valueBadge),null!=a.lqiText&&a.lqiBand?this._t(`marker.lqi_a11y_${a.lqiBand}`,{value:a.lqiText}):""].filter(Boolean).join(", "),_=[e.model,a.valueBadge?.fullText||"",null!=a.lqiText?"LQI "+a.lqiText:""].filter(Boolean).join(" · ");return B`
this._clickDevice(t,e)} + @keydown=${t=>this._keyDevice(t,e)} + @contextmenu=${t=>this._ctxDevice(t,e)} + @pointerover=${t=>this._showTip(t,e.name,a.haDisabled?h:_)} + @pointerleave=${()=>this._clearTransientHover()} + @pointerdown=${t=>this._pointerDown(t,e)} + @pointermove=${t=>{this._pointerMove(t,e),this._showTip(t,e.name,a.haDisabled?h:_)}} + @pointerup=${t=>this._pointerUp(t,e)} + @pointercancel=${t=>this._pointerUp(t,e)} + > + ${qu(a,{newDevice:this._newIds.has(e.id),newDeviceTitle:this._t("device.new"),disabledTitle:a.haDisabled?h:""})} +
`}_roomArea(e){const t=Zt(e);if(!t)return null;const i=this._spaceModel();if(!i)return null;const n=this._spaceWalls.length&&e.id&&this._innerRoomContour(i,e.id)||t,r=this._cleanFloor(e,n),o=this._cellCm/this._gridPitch;return Hn(r.area*o*o/1e4,"mi"===this._renderPlanHass?.config?.unit_system?.length)}_roomTemp(e){const t=e.settings?.temp_source;return t?Gh(this._renderPlanHass,t,"temp",this._markers):e.area?this._climate().get(e.area)?.temp??null:null}_roomHum(e){const t=e.settings?.hum_source;return t?Gh(this._renderPlanHass,t,"hum",this._markers):e.area?this._climate().get(e.area)?.hum??null:null}_climate(){const e=this._serverCfg?.markers,t=this._renderPlanHass,i=this._climateCache;if(i&&i.h===t&&i.r===this._iconRules&&i.mk===e)return i.m;const n=function(e,t,i){const n=new Map;if(!e?.entities)return n;const r=Lh(i),o=new Set;for(const e of i||[]){if(e?.removed||!0!==e?.use_climate_temp)continue;const t=(e.binding||"").indexOf(":");t>0&&o.add(e.binding.slice(t+1))}const s=new Map;for(const[t,i]of Object.entries(e.entities)){if(i.device_id&&r.devices.has(i.device_id)&&!r.liveEntities.has(t)||!i.device_id&&r.entities.has(t)&&!r.liveEntities.has(t))continue;const n=i.device_id?e.devices?.[i.device_id]:null,a=i.area_id||n?.area_id||null;if(!a)continue;if(i.entity_category)continue;if(!(o.size>0&&t.startsWith("climate.")&&(o.has(t)||i.device_id&&o.has(i.device_id)))){if(Re.has(i.platform))continue;if(Vh.test(t))continue}let l=s.get(a);l||(l=new Map,s.set(a,l));const c=i.device_id||t;let h=l.get(c);if(!h){const r=e.states?.[t];h={name:(n?n.name_by_user||n.name:i.name||r?.attributes?.friendly_name||t)||t,model:n?.model,ents:[]},l.set(c,h)}h.ents.push(t)}for(const[i,r]of s){const s=[],a=[];for(const[i,n]of r){const r=Fh(e,n.name,n.model,n.ents,t),l="mdi:thermometer"===r||"mdi:air-filter"===r;if(l){const t=Dh(e,n.ents);null!=t&&s.push(t)}if(o.size>0&&(o.has(i)||n.ents.some(e=>o.has(e)))){const t=Ah(e,n.ents);null!=t&&s.push(t)}if(l||"mdi:water-percent"===r){const t=zh(e,n.ents);null!=t&&a.push(t)}}(s.length||a.length)&&n.set(i,{temp:s.length?Math.round(s.reduce((e,t)=>e+t,0)/s.length*10)/10:null,hum:a.length?Math.round(a.reduce((e,t)=>e+t,0)/a.length):null})}return n}(t,this._iconRules,e);return this._climateCache={h:t,r:this._iconRules,mk:e,m:n},n}_resetRoomDialogFields(){return this._editorRuntimeOrThrow()._resetRoomDialogFields()}_openRoomEdit(e){return this._editorRuntimeOrThrow()._openRoomEdit(e)}_roomSettingsFromDialog(){return this._editorRuntimeOrThrow()._roomSettingsFromDialog()}_saveRoomEdit(){return this._editorRuntimeOrThrow()._saveRoomEdit()}_roomSrcCandidates(){return this._editorRuntimeOrThrow()._roomSrcCandidates()}_roomSrcLabel(e){return this._editorRuntimeOrThrow()._roomSrcLabel(e)}_labelPos(e,t){const i=this._layout["rl_"+(e.id||"")];if(i&&i.s===t)return{x:i.x*cm,y:i.y*cm};const n=this._roomCenter(e),r=this._gridPitch;return{x:ja(Gt(n[0],r)),y:ja(Gt(n[1],r))}}_labelDown(e,t,i){if(this._editorRuntime)return this._editorRuntimeOrThrow()._labelDown(e,t,i)}_labelMove(e,t,i){if(this._editorRuntime)return this._editorRuntimeOrThrow()._labelMove(e,t,i)}_labelUp(e){if(this._editorRuntime)return this._editorRuntimeOrThrow()._labelUp(e)}_labelScale(e){const t=this._layout["rl_"+(e.id||"")]?.k;return"number"==typeof t&&Number.isFinite(t)?Math.min(3,Math.max(.5,t)):1}_rlResizeDown(e,t,i){return this._editorRuntimeOrThrow()._rlResizeDown(e,t,i)}_rlResizeMove(e){return this._editorRuntimeOrThrow()._rlResizeMove(e)}_rlResizeUp(){return this._editorRuntimeOrThrow()._rlResizeUp()}_renderRoomGear(e,t,i){return this._editorRuntimeOrThrow()._renderRoomGear(e,t,i)}_renderRoomLabel(e,t,i,n){if(!e.name&&!this._markup)return G;const r=this._labelPos(e,t.id),o=this._scenePoint([r.x,r.y]),s=(o[0]-i.x)/i.w*100,a=(o[1]-i.y)/i.h*100,l=Math.min(1,n.opacity+.25),c=this._labelScale(e),h=[];if(e.area||e.settings?.temp_source||e.settings?.hum_source||n.labelLight){if(n.labelTemp){const t=this._roomTemp(e);null!=t&&h.push(B`${t}°`)}if(n.labelHum){const t=this._roomHum(e);null!=t&&h.push(B`${t}%`)}if(n.labelLqi&&e.area){const t=this._roomLqi(e.area);null!=t&&h.push(B`${t}`)}if(n.labelLight){const t=(d=Mh(this._renderPlanHass,this._renderDevices,e,this._virtualLights)).length?{on:d.filter(e=>e.on).length,total:d.length}:null;if(t){const e=0===t.on?this._t("roomcard.light_off"):t.on===t.total?this._t("roomcard.light_on"):this._t("roomcard.light_partial",{on:t.on,total:t.total});h.push(B`${e}`)}}}var d;const u=!!e.area,p=!this._markup;return B`
this._labelDown(i,e,t.id)} + @pointermove=${i=>this._labelMove(i,e,t.id)} + @pointerup=${()=>this._labelUp(e)} + @pointercancel=${()=>this._labelUp(e)} + >${e.name||(this._markup?this._t("room.unnamed"):"")}${u?B`{t.stopPropagation(),this._clickRoom(e)}:G} + @pointerdown=${p?e=>e.stopPropagation():G}>`:G} + ${h.length?B`${h}`:G} + ${"plan"===this._mode?["tl","tr","bl","br"].map(i=>B`this._rlResizeDown(i,e,t.id)} + @pointermove=${e=>this._rlResizeMove(e)} + @pointerup=${()=>this._rlResizeUp()} + @pointercancel=${()=>this._rlResizeUp()}>`):G} +
`}get _measureAnchor(){return this._markup&&this._cursorPt?"draw"===this._tool&&this._path.length&&!this._contourClosed?this._path[this._path.length-1]:"split"===this._tool&&this._splitSel?.pts?.length?this._splitSel.pts[this._splitSel.pts.length-1]:null:null}_renderMeasureLabel(e){const t=this._measureAnchor,i=this._cursorPt,n=(i[0]-e.x)/e.w*100,r=(i[1]-e.y)/e.h*100,o=On(t,i),s=Math.round(10*o)/10,a=function(e,t,i=.001){if(e.length<2||t.length<2)return!1;const n=Math.abs(t[0]-e[0]),r=Math.abs(t[1]-e[1]);if(!Number.isFinite(n)||!Number.isFinite(r)||Math.hypot(n,r)<=i)return!1;const o=Math.max(n,r,1);return n<=i*o||r<=i*o||Math.abs(n-r)<=i*o}(t,i,2e-4*this._gridPitch);return B`
+ ${this._fmtLen(t,i)} · ${s}°
`}get _decorMeasure(){const e=this._decorDraft;if(!e||"decor"!==this._mode)return null;const[t,i]=e.a,[n,r]=e.b;if(Math.abs(t-n)<1e-6&&Math.abs(i-r)<1e-6)return null;const o=(t+n)/2,s=(i+r)/2;if("line"===e.kind){const t=On(e.a,e.b);return{x:o,y:s,on45:zn(t),text:`${this._fmtLen(e.a,e.b)} · ${Math.round(10*t)/10}°`}}const a=this._fmtLen([t,i],[n,i]),l=this._fmtLen([n,i],[n,r]);if("ellipse"===e.kind){const e=this._fmtLen([0,0],[Math.abs(n-t)/2,0]),a=this._fmtLen([0,0],[0,Math.abs(r-i)/2]);return{x:o,y:s,on45:!1,text:Math.abs(Math.abs(n-t)-Math.abs(r-i))<1e-6?`R ${e}`:`Rx ${e} × Ry ${a}`}}return{x:o,y:s,on45:!1,text:`${a} × ${l} · ${Hn(vr(Math.abs(n-t),this._cellCm,this._gridPitch)*vr(Math.abs(r-i),this._cellCm,this._gridPitch)/1e4,this._imperial)}`}}get _alignPoint(){if(this._markup){if("draw"===this._tool&&this._path.length&&!this._contourClosed&&this._cursorPt)return this._cursorPt;if("split"===this._tool&&this._splitSel?.pts?.length&&this._cursorPt)return this._cursorPt;if(this._drag?.id.startsWith("rl_")&&this._drag.moved){const e=this._drag.id.slice(3),t=this._spaceModel()?.rooms.find(t=>t.id===e);return t?(()=>{const e=this._labelPos(t,this._space);return[e.x,e.y]})():null}return null}if("devices"===this._mode&&this._drag?.moved){const e=this._devices.find(e=>e.id===this._drag.id);return e?(()=>{const t=this._pos(e);return[t.x,t.y]})():null}if("decor"===this._mode){if(this._decorDraft)return this._decorDraft.b;if(this._decorMove){const e=this._decorList.find(e=>e.id===this._decorMove.id);if(!e)return null;const t=cm,i=this._decorH;return"line"===e.kind?[e.x1*t,e.y1*i]:[e.x*t,e.y*i]}return null}return null}_alignCandidates(){return this._editorRuntimeOrThrow()._alignCandidates()}_renderAlignGuides(){return this._editorRuntimeOrThrow()._renderAlignGuides()}_renderOpeningCenterTick(e){return this._editorRuntimeOrThrow()._renderOpeningCenterTick(e)}_renderOpeningDimensionGuides(e){return this._editorRuntimeOrThrow()._renderOpeningDimensionGuides(e)}_roomCenter(e){if(e.poly){const t=e.poly.length;return[e.poly.reduce((e,t)=>e+t[0],0)/t,e.poly.reduce((e,t)=>e+t[1],0)/t]}return[e.x+e.w/2,e.y+.1*Math.min(e.w,e.h)]}_openingAmt(e){const t=e.contact&&this._renderOpeningEntityAvailable(e.contact)?this._renderPlanHass.states[e.contact]?.state:null;return ii(e.type,t,!!e.invert)}_planEntityAvailable(e){return!!e&&(!jh(this._fullRegistryHass,e,Lh(this._markers))&&"active"===this._bindingStatus("entity:"+e).kind)}_renderEntityAvailable(e){return!!e&&(!jh(this._renderPlanHass,e,Lh(this._markers))&&(!!this._renderPlanHass.entities?.[e]&&!!this._renderPlanHass.states?.[e]))}_openingEntityAvailable(e){return function(e,t,i=Vc(e)){return!!t&&"active"===th(e,`entity:${t}`,i).kind}(this.hass,e,this._haRegistry)}_renderOpeningEntityAvailable(e){return t=this._renderPlanHass,!!(i=e)&&!!t?.states?.[i];var t,i}_renderOpeningPlacementPreview(){return this._editorRuntimeOrThrow()._renderOpeningPlacementPreview()}_renderOpenings(e){const t=this._openingsR;if(!t.length)return W``;const i=this._spaceModel();if(!i)return W``;const n=e.color,r=this._spaceWalls,o=this._openCuts(),s=this._openingWallIndexFor(i,o).value;return W`${t.map(e=>{if(e.orphanReason)return W`{t.stopPropagation(),this._editOpening(e)}}> + + ! + `;const t=this._openingAmt(e),i=t>0&&!!e.contact&&this._renderOpeningEntityAvailable(e.contact)?"var(--hp-open)":n,o="gate"===e.type?!e.flip_v:e.flip_v,a=e.partitionHost||r.length||"gate"===e.type?this._openingFace(e,s,!!o):{ox:0,oy:0,cm:0,side:-1},l={type:e.type,length:e.rlen,angle:e.angle,amount:t,flipH:!!e.flip_h,flipV:!!e.flip_v,base:n,tone:i,cellCm:this._cellCm,gridPitch:this._gridPitch,face:a},{half:c,outlineHalf:h,hitHalf:d}=eu(l),u=xa(10,this._cellCm),p=xa(12,this._cellCm);return W` + ${tu(l)} + + this._opClick(t,e)} + @pointerdown=${t=>this._opPointerDown(t,e)} + @pointermove=${t=>this._opPointerMove(t,e)} + @pointerup=${t=>this._opPointerUp(t,e)} + @pointercancel=${t=>this._opPointerUp(t,e)}> + `})}`}_renderOpeningLocks(e){const t=this._openingsR.filter(e=>!e.orphanReason&&("door"===e.type||"gate"===e.type)&&e.lock&&this._renderOpeningEntityAvailable(e.lock));if(!t.length)return B``;const i=this._spaceModel();if(!i)return B``;const n=this._openCuts(),r=this._openingWallIndexFor(i,n).value;return B`${t.map(t=>{const i=this._renderPlanHass.states[t.lock]?.state,n="locked"===i,o=n||["unlocked","open","opening","unlocking","locking"].includes(String(i)),s=(t.angle+90)*Math.PI/180,a="gate"===t.type?this._openingFace(t,r,!t.flip_v):null,l=xa(16,this._cellCm),c=a?-l*a.side:l*(t.flip_v?-1:1),h=t.rx+Math.cos(s)*c,d=t.ry+Math.sin(s)*c,u=this._scenePoint([h,d]),p=(u[0]-e.x)/e.w*100,_=(u[1]-e.y)/e.h*100;return B`
{e.stopPropagation(),"view"===this._mode&&(this._openingInfo=t)}}> + +
`})}`}_lockAction(e,t){if(this._openingEntityAvailable(e)){if("unlock"===t){const t=this.hass?.states?.[e]?.attributes?.friendly_name||e;if(!confirm(this._t("confirm.unlock",{name:t})))return}this.hass?.callService?.("lock",t,{entity_id:e})}}_renderOpeningInfoCard(){const e=this._openingInfo,t="passage"!==e.type&&e.contact&&this._openingEntityAvailable(e.contact)?e.contact:null,i=("door"===e.type||"gate"===e.type)&&e.lock&&this._openingEntityAvailable(e.lock)?e.lock:null,n=t?this.hass.states[t]?.state:null,r=this._openingAmt(e),o=i?this.hass.states[i]?.state:null,s="door"===e.type?"opening.door":"gate"===e.type?"opening.gate":"passage"===e.type?"opening.passage":"opening.window",a="door"===e.type?"mdi:door":"gate"===e.type?"mdi:gate":"passage"===e.type?"mdi:arch":"mdi:window-closed-variant",l="gate"===e.type?r>0?"mdi:gate-open":"mdi:gate":r>0?"mdi:door-open":"mdi:door-closed",c=(e,t,i,n="")=>B`
${t}${i}
`;return B`this._openingInfo=null}> +
+ ${t?c(l,this._t("opening.contact_label"),"unavailable"===n||null==n?this._t("opening.state_unknown"):this._t(r>0?"opening.open":"opening.closed"),r>0?"warn":"ok"):G} + ${i?c("locked"===o?"mdi:lock":"mdi:lock-open-variant",this._t("opening.lock_label"),"locked"===o?this._t("opening.locked"):["unlocked","open"].includes(String(o))?this._t("opening.unlocked"):this._t("opening.state_unknown"),"locked"===o?"ok":"warn"):G} + ${i&&("locked"===o||["unlocked","open"].includes(String(o)))?B``:i&&["locking","unlocking"].includes(String(o))?B``:G} + ${t||i?G:B`

${this._t("opening.no_entities")}

`} +
+
+ + +
+
`}_renderOpeningDialog(){return this._editorRuntimeOrThrow()._renderOpeningDialog()}_gridLevels(){return this._editorRuntimeOrThrow()._gridLevels()}_renderMarkupDefs(e){return this._editorRuntimeOrThrow()._renderMarkupDefs(e)}_renderPhysicalEditorLayer(){return this._editorRuntimeOrThrow()._renderPhysicalEditorLayer()}_renderHiddenWallDiagnosticOverlay(){return this._editorRuntimeOrThrow()._renderHiddenWallDiagnosticOverlay()}_renderPlanSnapOverlay(){return this._editorRuntimeOrThrow()._renderPlanSnapOverlay()}_syncPlanSnapActiveMarker(e){return this._editorRuntimeOrThrow()._syncPlanSnapActiveMarker(e)}_syncPlanSnapConflictMarkers(e){return this._editorRuntimeOrThrow()._syncPlanSnapConflictMarkers(e)}_planSnapPhysicalSegment(e){return this._editorRuntimeOrThrow()._planSnapPhysicalSegment(e)}_drawPreviewJoinPatchD(e,t){return this._editorRuntimeOrThrow()._drawPreviewJoinPatchD(e,t)}_renderMarkupLayer(e){return this._editorRuntimeOrThrow()._renderMarkupLayer(e)}_renderActiveChainInk(){return this._editorRuntimeOrThrow()._renderActiveChainInk()}_renderPartitionDeleteDialog(){return this._editorRuntimeOrThrow()._renderPartitionDeleteDialog()}_renderRoomDeleteDialog(){return this._editorRuntimeOrThrow()._renderRoomDeleteDialog()}_renderPhysicalDialog(){return this._editorRuntimeOrThrow()._renderPhysicalDialog()}_renderMarkupBar(){return this._editorRuntimeOrThrow()._renderMarkupBar()}_renderDevicesBar(){return this._editorRuntimeOrThrow()._renderDevicesBar()}_renderDeviceInbox(){return this._editorRuntimeOrThrow()._renderDeviceInbox()}_cardEntities(e){const t=this._planHass,i=[],n=new Set,r=e=>{if(!e||n.has(e)||!t.states[e])return;const r=t.entities[e];if("config"===r?.entity_category||"diagnostic"===r?.entity_category)return;n.add(e);const o=e.split(".")[0];["light","switch","fan","humidifier","siren","input_boolean"].includes(o)?i.push({eid:e,kind:"toggle"}):["cover","valve","lock","climate","media_player","vacuum","water_heater"].includes(o)?i.push({eid:e,kind:"open"}):["sensor","binary_sensor","number","select"].includes(o)&&i.push({eid:e,kind:"value"})};for(const i of Mh(t,this._devices,null,this._virtualLights))if(i.device.id===e.id)for(const e of[...i.serviceEids,...i.stateEids])r(e);e.primary&&r(e.primary);for(const t of e.entities)r(t);return i.slice(0,12)}_cardToggle(e){const t=e.split(".")[0];"lock"!==t&&"alarm_control_panel"!==t&&this._planEntityAvailable(e)&&this.hass.callService("homeassistant","toggle",{entity_id:e}).catch(e=>this._showToast(this._t("toast.error",{err:this._errText(e)})))}_renderInfoCard(){const e=this._infoCard,t=e.primary?this.hass.states[e.primary]:void 0,i=t?ji(this.hass,e.primary)?.text??t.state:null,n=(e.controls??e.marker?.controls??[]).filter(yn).filter(e=>this._planEntityAvailable(e));return B` +
+ ${(()=>{const t=this._cardEntities(e);return t.length?B`
+ ${t.map(({eid:e,kind:t})=>{const i=this.hass.states[e],n=this.hass.entities[e]?.name||i?.attributes?.friendly_name||e,r=i?ji(this.hass,e)?.text??i.state:"",o="on"===i?.state||["open","unlocked","playing","cleaning"].includes(i?.state);return B`
+ + ${n} + ${"toggle"===t?B``:"open"===t?B``:B`${r}`} +
`})} +
`:G})()} + ${e.model?B`
${this._t("info.model")}${e.model}
`:G} + ${i&&!this._cardEntities(e).length?B`
${this._t("info.state")}${i}
`:G} + ${Ci(e.link)?B`
${this._t("info.link")} + ${e.link}
`:G} + ${e.description?B`
${e.description}
`:G} + ${e.pdfs&&e.pdfs.length?B`
${this._t("info.manuals")} + ${e.pdfs.map(e=>B` + ${e.name}`)}
`:G} + ${n.length?B`
${this._t("info.controls")} + + ${n.map(e=>{const t=this.hass.states[e],i="on"===t?.state;return B` + + ${t?.attributes?.friendly_name||e}`})} +
`:G} + ${e.model||i||e.link||e.description||e.pdfs&&e.pdfs.length||n.length?G:B`
${this._t("info.none")}
`} +
+
+ + ${e.primary?B``:G} + +
+
`}_markerValueBadgeFields(e){return this._editorRuntimeOrThrow()._markerValueBadgeFields(e)}_markerDraft(e){return this._editorRuntimeOrThrow()._markerDraft(e)}_markerPreviewDevice(e){return this._editorRuntimeOrThrow()._markerPreviewDevice(e)}_markerPreviewDevices(e){return this._editorRuntimeOrThrow()._markerPreviewDevices(e)}_toggleIntent(e,t=this._devices){return this._editorRuntimeOrThrow()._toggleIntent(e,t)}_toggleIntentForDialog(e){return this._editorRuntimeOrThrow()._toggleIntentForDialog(e)}_toggleStateText(e,t){return this._editorRuntimeOrThrow()._toggleStateText(e,t)}_toggleConfirmationStateText(e){return this._editorRuntimeOrThrow()._toggleConfirmationStateText(e)}_toggleConfirmationLines(e){return this._editorRuntimeOrThrow()._toggleConfirmationLines(e)}_toggleHintLines(e){return this._editorRuntimeOrThrow()._toggleHintLines(e)}_effectiveStoredTapAction(e,t){return this._editorRuntimeOrThrow()._effectiveStoredTapAction(e,t)}_effectiveMarkerTapAction(e,t=this._markerPreviewDevice(e)){return this._editorRuntimeOrThrow()._effectiveMarkerTapAction(e,t)}_announceToggleDraft(e){return this._editorRuntimeOrThrow()._announceToggleDraft(e)}_valueBadgeForBinding(e,t){return this._editorRuntimeOrThrow()._valueBadgeForBinding(e,t)}_markerSpatialSource(e){return this._editorRuntimeOrThrow()._markerSpatialSource(e)}_markerAutoHasSpatialSource(e){return this._editorRuntimeOrThrow()._markerAutoHasSpatialSource(e)}_setMarkerLightRole(e){return this._editorRuntimeOrThrow()._setMarkerLightRole(e)}_controlRefInfo(e){return this._editorRuntimeOrThrow()._controlRefInfo(e)}_valueBadgeCandidateLabel(e){return this._editorRuntimeOrThrow()._valueBadgeCandidateLabel(e)}_controlCandidates(e){return this._editorRuntimeOrThrow()._controlCandidates(e)}_addControlRef(e,t){return this._editorRuntimeOrThrow()._addControlRef(e,t)}_setMarkerGlowMode(e){return this._editorRuntimeOrThrow()._setMarkerGlowMode(e)}_renderMarkerDialog(){return this._editorRuntimeOrThrow()._renderMarkerDialog()}_renderSpaceDialog(){return this._onboardingRuntime&&this._spaceDialog&&this._spaceDialogUsesOnboardingRuntime(this._spaceDialog.mode)?this._onboardingRuntime._renderSpaceDialog():this._editorRuntimeOrThrow()._renderSpaceDialog()}_renderMergeDialog(){return this._editorRuntimeOrThrow()._renderMergeDialog()}_renderCardPreview(e,t,i){const n=18*e;return B`
+ + ${this._t("preview.room_name")} + + 22.4° · + 45% · + ${this._t("roomcard.light_partial",{on:1,total:3})} + +
`}_renderRoomSource(e){return this._editorRuntimeOrThrow()._renderRoomSource(e)}_renderRoomDialog(){return this._editorRuntimeOrThrow()._renderRoomDialog()}}fm.properties={_tabDrag:{state:!0},_hdrH:{state:!0},_booting:{state:!0},_bootFading:{state:!0},_bootSoft:{state:!0},_continuityEpoch:{state:!0},_editorRuntimeLoadingVisible:{state:!0},_tapConfirm:{state:!0},hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_hoverRoom:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_mode:{state:!0},_tool:{state:!0},_wallDialog:{state:!0},_drawWallField:{state:!0},_activeDraftId:{state:!0},_physicalSel:{state:!0},_physicalDialog:{state:!0},_partitionDeleteDialog:{state:!0},_roomDeleteDialog:{state:!0},_physicalDrag:{state:!0},_physicalRotate:{state:!0},_duplicateColumnId:{state:!0},_opMeasure:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_mergeSel:{state:!0},_openingPreset:{state:!0},_openingDialog:{state:!0},_openingInfo:{state:!0},_mergeDialog:{state:!0},_splitSel:{state:!0},_decorTool:{state:!0},_decorStyle:{state:!0},_decorDraft:{state:!0},_decorSel:{state:!0},_decorEraseConfirm:{state:!0},_decorTextDialog:{state:!0},_decorShapeDialog:{state:!0},_backdropDialog:{state:!0},_furnPalette:{state:!0},_bdDrag:{state:!0},_dtBox:{state:!0},_dtDrag:{state:!0},_kioskDialog:{state:!0},_vacFit:{state:!0},_vacAllCamerasFor:{state:!0},_vacCalConfirm:{state:!0},_kioskDots:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_roomEditId:{state:!0},_roomFill:{state:!0},_roomCustomFill:{state:!0},_roomTempSrc:{state:!0},_roomHumSrc:{state:!0},_roomSrcOpen:{state:!0},_roomSrcFilter:{state:!0},_roomNameScale:{state:!0},_roomLabelScale:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_deviceInbox:{state:!0},_rulesDialog:{state:!0},_settingsDialog:{state:!0},_alignDialog:{state:!0},_preflightClipboardFallback:{state:!0},_backupExportDialog:{state:!0},_backupImportDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},fm.ZOOM_MAX=8,fm.ZOOM_MIN=1/3,fm.styles=[Bd,Dp],customElements.get("houseplan-card")||customElements.define("houseplan-card",fm),window.customCards=window.customCards||[],window.customCards.find(e=>"houseplan-card"===e.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info(`%c HOUSEPLAN-CARD %c v${K_} `,"background:#3ea6ff;color:#04121f;font-weight:700","");export{Ao as $,G as A,Qt as B,Ia as C,le as D,U as E,Zt as F,Na as G,Rd as H,Id as I,gs as J,lo as K,Pd as L,Md as M,No as N,vs as O,Cd as P,Bs as Q,Gs as R,vo as S,oi as T,ao as U,xo as V,Ku as W,Yu as X,ss as Y,Wr as Z,zo as _,o as a,ts as a$,no as a0,oo as a1,jr as a2,rp as a3,Ta as a4,Fo as a5,Ea as a6,Oo as a7,Ca as a8,Da as a9,ea as aA,Al as aB,op as aC,Tl as aD,Lp as aE,_s as aF,Ns as aG,ks as aH,Op as aI,hi as aJ,si as aK,$i as aL,va as aM,sa as aN,oa as aO,Pa as aP,Io as aQ,Eo as aR,Ls as aS,Kt as aT,tl as aU,ma as aV,_a as aW,Nn as aX,Hn as aY,pi as aZ,is as a_,Ui as aa,Gi as ab,Hi as ac,Li as ad,za as ae,ms as af,ns as ag,js as ah,jo as ai,Xs as aj,ba as ak,sp as al,Qs as am,fs as an,ar as ao,sr as ap,or as aq,nn as ar,Ce as as,Qd as at,k_ as au,fo as av,ja as aw,Gt as ax,Vt as ay,Cr as az,B as b,Yi as b$,W as b0,Pp as b1,Fp as b2,kr as b3,Mr as b4,Sr as b5,vr as b6,qa as b7,_r as b8,gr as b9,Fd as bA,wi as bB,li as bC,ei as bD,ci as bE,Lo as bF,Si as bG,mn as bH,mu as bI,gu as bJ,uh as bK,_d as bL,Ai as bM,Ii as bN,En as bO,Cn as bP,Eh as bQ,ro as bR,Hh as bS,Ih as bT,tn as bU,Qi as bV,Ji as bW,Sa as bX,tp as bY,Xi as bZ,Zi as b_,br as ba,On as bb,pr as bc,fr as bd,mr as be,xr as bf,gl as bg,dl as bh,ml as bi,ul as bj,bl as bk,sl as bl,hl as bm,Ho as bn,_o as bo,xa as bp,eu as bq,zd as br,U_ as bs,Od as bt,Vs as bu,L_ as bv,G_ as bw,Zu as bx,ti as by,Dd as bz,he as c,co as c$,Ki as c0,$a as c1,Hp as c2,zp as c3,rn as c4,Kr as c5,lr as c6,De as c7,Fe as c8,Ae as c9,vd as cA,kd as cB,$d as cC,Th as cD,Mh as cE,kh as cF,_h as cG,yn as cH,dh as cI,Nh as cJ,gn as cK,Pu as cL,ou as cM,xh as cN,$h as cO,mh as cP,cd as cQ,Oi as cR,Di as cS,su as cT,Ci as cU,Ma as cV,zi as cW,Pi as cX,so as cY,cl as cZ,vl as c_,Bl as ca,Kl as cb,Vi as cc,nc as cd,ql as ce,El as cf,Yl as cg,Wl as ch,Ul as ci,ec as cj,tc as ck,Ql as cl,ic as cm,zl as cn,Dn as co,An as cp,ii as cq,j_ as cr,tu as cs,H_ as ct,nl as cu,ys as cv,Vo as cw,Ko as cx,fu as cy,Uh as cz,Bd as d,ri as d0,Fa as d1,Zd as d2,fm as d3,pe as e,Hu as f,Lu as g,ui as h,_e as i,Rr as j,fi as k,Xd as l,Ol as m,Nt as n,yi as o,gi as p,Lh as q,qu as r,jt as s,Jd as t,jh as u,nu as v,yu as w,Jt as x,wn as y,Pn as z}; diff --git a/dist/houseplan-assets/houseplan-editor-runtime-DUuA4F8c.js b/dist/houseplan-assets/houseplan-editor-runtime-DUuA4F8c.js new file mode 100644 index 00000000..26fb1703 --- /dev/null +++ b/dist/houseplan-assets/houseplan-editor-runtime-DUuA4F8c.js @@ -0,0 +1,2572 @@ +globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="f111431804c4c42c6e35286f29720d089bc8b77ffc364a6517f7a3510fcb11c8";import{e as t,i as e,E as s,D as i,b as o,A as a,a as n,c as r,s as l,m as h,d as c,l as d,t as p,w as _,f as u,g as m,v as g,r as f,h as b,j as y,p as v,k as $,n as w,o as k,q as x,u as S,x as M,y as D,z as C,B as P,G as I,C as T,F as R,H as F,I as N,J as z,K as A,L as O,M as L,N as E,O as B,P as H,Q as G,R as W,S as j,T as q,U,V as K,W as V,X as J,Y,Z as X,_ as Z,$ as Q,a0 as tt,a1 as et,a2 as st,a3 as it,a4 as ot,a5 as at,a6 as nt,a7 as rt,a8 as lt,a9 as ht,aa as ct,ab as dt,ac as pt,ad as _t,ae as ut,af as mt,ag as gt,ah as ft,ai as bt,aj as yt,ak as vt,al as $t,am as wt,an as kt,ao as xt,ap as St,aq as Mt,ar as Dt,as as Ct,at as Pt,au as It,av as Tt,aw as Rt,ax as Ft,ay as Nt,az as zt,aA as At,aB as Ot,aC as Lt,aD as Et,aE as Bt,aF as Ht,aG as Gt,aH as Wt,aI as jt,aJ as qt,aK as Ut,aL as Kt,aM as Vt,aN as Jt,aO as Yt,aP as Xt,aQ as Zt,aR as Qt,aS as te,aT as ee,aU as se,aV as ie,aW as oe,aX as ae,aY as ne,aZ as re,a_ as le,a$ as he,b0 as ce,b1 as de,b2 as pe,b3 as _e,b4 as ue,b5 as me,b6 as ge,b7 as fe,b8 as be,b9 as ye,ba as ve,bb as $e,bc as we,bd as ke,be as xe,bf as Se,bg as Me,bh as De,bi as Ce,bj as Pe,bk as Ie,bl as Te,bm as Re,bn as Fe,bo as Ne,bp as ze,bq as Ae,br as Oe,bs as Le,bt as Ee,bu as Be,bv as He,bw as Ge,bx as We,by as je,bz as qe,bA as Ue,bB as Ke,bC as Ve,bD as Je,bE as Ye,bF as Xe,bG as Ze,bH as Qe,bI as ts,bJ as es,bK as ss,bL as is,bM as os,bN as as,bO as ns,bP as rs,bQ as ls,bR as hs,bS as cs,bT as ds,bU as ps,bV as _s,bW as us,bX as ms,bY as gs,bZ as fs,b_ as bs,b$ as ys,c0 as vs,c1 as $s,c2 as ws,c3 as ks,c4 as xs,c5 as Ss,c6 as Ms,c7 as Ds,c8 as Cs,c9 as Ps,ca as Is,cb as Ts,cc as Rs,cd as Fs,ce as Ns,cf as zs,cg as As,ch as Os,ci as Ls,cj as Es,ck as Bs,cl as Hs,cm as Gs,cn as Ws,co as js,cp as qs,cq as Us,cr as Ks,cs as Vs,ct as Js,cu as Ys,cv as Xs,cw as Zs,cx as Qs,cy as ti,cz as ei,cA as si,cB as ii,cC as oi,cD as ai,cE as ni,cF as ri,cG as li,cH as hi,cI as ci,cJ as di,cK as pi,cL as _i,cM as ui,cN as mi,cO as gi,cP as fi,cQ as bi,cR as yi,cS as vi,cT as $i,cU as wi,cV as ki,cW as xi,cX as Si,cY as Mi,cZ as Di,c_ as Ci,c$ as Pi,d0 as Ii,d1 as Ti}from"./houseplan-card-Cs2QHIcW.js";import{i as Ri,c as Fi,a as Ni,s as zi,t as Ai}from"./space-deletion-CJ8Co_o8.js";const Oi={},Li=t(class extends e{constructor(){super(...arguments),this.ot=Oi}render(t,e){return e()}update(t,[e,i]){if(Array.isArray(e)){if(Array.isArray(this.ot)&&this.ot.length===e.length&&e.every((t,e)=>t===this.ot[e]))return s}else if(this.ot===e)return s;return this.ot=Array.isArray(e)?Array.from(e):e,this.render(e,i)}}),Ei=(t,e,s)=>Math.min(s,Math.max(e,Number.isFinite(t)?t:e));function Bi(t){return Number.isFinite(t)?(t%360+360)%360:0}function Hi(t){if("string"!=typeof t)return null;const e=t.trim().replace(/^#/,""),s=/^[0-9a-fA-F]{3}$/.test(e)?e.split("").map(t=>`${t}${t}`).join(""):e;return/^[0-9a-fA-F]{6}$/.test(s)?`#${s.toLowerCase()}`:null}function Gi(t){const e=Hi(t);return e?{r:Number.parseInt(e.slice(1,3),16),g:Number.parseInt(e.slice(3,5),16),b:Number.parseInt(e.slice(5,7),16)}:null}function Wi(t){const e=Ei(t.r,0,255)/255,s=Ei(t.g,0,255)/255,i=Ei(t.b,0,255)/255,o=Math.max(e,s,i),a=o-Math.min(e,s,i);let n=0;return a>0&&(n=o===e?(s-i)/a%6*60:o===s?60*((i-e)/a+2):60*((e-s)/a+4)),{h:Bi(n),s:0===o?0:a/o*100,v:100*o}}function ji(t){return function(t){const e=t=>Math.round(Ei(t,0,255)).toString(16).padStart(2,"0");return`#${e(t.r)}${e(t.g)}${e(t.b)}`}(function(t){const e=Bi(t.h),s=Ei(t.s,0,100)/100,i=Ei(t.v,0,100)/100,o=i*s,a=o*(1-Math.abs(e/60%2-1)),n=i-o;let r;return r=e<60?[o,a,0]:e<120?[a,o,0]:e<180?[0,o,a]:e<240?[0,a,o]:e<300?[a,0,o]:[o,0,a],{r:Math.round(255*(r[0]+n)),g:Math.round(255*(r[1]+n)),b:Math.round(255*(r[2]+n))}}(t))}function qi(t){const e=t.visualViewport;return e&&Number.isFinite(e.width)&&Number.isFinite(e.height)?{left:Number(e.offsetLeft)||0,top:Number(e.offsetTop)||0,width:Math.max(0,Number(e.width)||0),height:Math.max(0,Number(e.height)||0)}:{left:0,top:0,width:t.innerWidth,height:t.innerHeight}}function Ui(t,e,s,i=7,o=8){const a=s.left+o,n=s.top+o,r=Math.max(a,s.left+s.width-o),l=Math.max(n,s.top+s.height-o),h=Math.max(0,r-a),c=Math.max(0,l-n),d=Math.min(Math.max(0,e.width),h),p=Math.min(Math.max(0,e.height),c);let _=t.left;_+d>r&&(_=t.right-d),_=Math.min(Math.max(a,_),Math.max(a,r-d));const u=t.bottom+i,m=t.top-i-p;let g="bottom",f=u;return u+p>l&&m>=n?(g="top",f=Math.min(Math.max(n,m),Math.max(n,l-p))):f=Math.min(Math.max(n,u),Math.max(n,l-p)),{left:Math.round(_),top:Math.round(f),side:g,maxWidth:Math.round(h),maxHeight:Math.round(c)}}class Ki{constructor(t,e,s){this.owner=t,this.name=e,this.keydown=s,this.host=null,this.root=null}window(){return this.owner.ownerDocument.defaultView}dialog(){return this.owner.closest("hp-dialog")}usesPopover(t=!1){const e=this.window();return!t&&!!e&&function(t){const e=t.HTMLElement?.prototype;return"function"==typeof e?.showPopover&&"function"==typeof e?.hidePopover}(e)}get hasFallback(){return!!this.host}containsPath(t){return t.includes(this.owner)||!!this.host&&t.includes(this.host)}ownsActiveElement(){let t=this.owner.ownerDocument.activeElement;for(;t?.shadowRoot?.activeElement;)t=t.shadowRoot.activeElement;return!(!t||t!==this.owner&&!this.owner.shadowRoot?.contains(t)&&!this.root?.contains(t))}renderFallback(t,e){const s=this.ensureFallback();return s?(i(o`${t}`,s),s):null}surface(t,e){return e?this.owner.shadowRoot?.querySelector(t)||null:this.root?.querySelector(t)||null}destroy(){this.root&&i(a,this.root),this.host?.remove(),this.host=null,this.root=null}ensureFallback(){if(this.root?.isConnected)return this.root;const t=this.dialog()?.overlayPortal()||this.owner.ownerDocument.body;if(!t)return null;const e=this.owner.ownerDocument.createElement("div");return e.dataset.hpOverlay=this.name,e.style.display="contents",this.keydown&&e.addEventListener("keydown",this.keydown,!0),t.append(e),this.host=e,this.root=e.attachShadow({mode:"open"}),this.root}}const Vi={title:"Color picker",hue:"Hue",saturation:"Saturation",value:"Brightness",hex:"Hex color",invalidHex:"Enter a 3- or 6-digit hex color"};class Ji extends r{constructor(){super(...arguments),this.label="",this.opacityLabel="Opacity",this.color="#607d8b",this.opacity=1,this.disabled=!1,this.showOpacity=!0,this.pickerLabels=Vi,this._open=!1,this._pickerRaf=0,this._emitRaf=0,this._forceFallback=!1,this._overlayDispose=null,this._hue=0,this._saturation=0,this._value=0,this._hexDraft="#607d8b",this._hexInvalid=!1,this._lastValidColor="#607d8b",this._activePointerId=null,this._outsidePointerDown=t=>{if(!this._open)return;const e=t.composedPath();this._floating.containsPath(e)||this._closePicker(!1,"outside")},this._keyDown=t=>{this._open&&"Escape"===t.key&&(t.preventDefault(),t.stopImmediatePropagation(),this._closePicker(this._floating.ownsActiveElement(),"escape"))},this._floating=new Ki(this,"color-opacity",this._keyDown),this._queuePickerPosition=()=>{if(!this._open)return;this._pickerRaf&&cancelAnimationFrame(this._pickerRaf);const t=this._window();t&&(this._pickerRaf=t.requestAnimationFrame(()=>{this._pickerRaf=0,this._positionPicker()||this._closePicker()}))}}connectedCallback(){super.connectedCallback(),this._syncFromColor(this.color,!1),this.ownerDocument.addEventListener("pointerdown",this._outsidePointerDown,!0),this.ownerDocument.addEventListener("scroll",this._queuePickerPosition,!0);const t=this.ownerDocument.defaultView;t?.addEventListener("resize",this._queuePickerPosition),t?.addEventListener("orientationchange",this._queuePickerPosition),t?.visualViewport?.addEventListener("resize",this._queuePickerPosition),t?.visualViewport?.addEventListener("scroll",this._queuePickerPosition),this.addEventListener("keydown",this._keyDown,!0)}disconnectedCallback(){this.ownerDocument.removeEventListener("pointerdown",this._outsidePointerDown,!0),this.ownerDocument.removeEventListener("scroll",this._queuePickerPosition,!0);const t=this.ownerDocument.defaultView;t?.removeEventListener("resize",this._queuePickerPosition),t?.removeEventListener("orientationchange",this._queuePickerPosition),t?.visualViewport?.removeEventListener("resize",this._queuePickerPosition),t?.visualViewport?.removeEventListener("scroll",this._queuePickerPosition),this.removeEventListener("keydown",this._keyDown,!0),this._pickerRaf&&cancelAnimationFrame(this._pickerRaf),this._emitRaf&&cancelAnimationFrame(this._emitRaf),this._pickerRaf=0,this._emitRaf=0,this._closePicker(!1,"disconnect"),super.disconnectedCallback()}updated(t){if(t.has("color")){const t=Hi(this.color);t&&t!==this._lastValidColor&&this._syncFromColor(t,this._open)}this.disabled&&this._open?this._closePicker():this._open&&(this._supportsPopover()||this._renderFallback(),(t.has("color")||t.has("opacity")||t.has("showOpacity")||t.has("label")||t.has("opacityLabel")||t.has("pickerLabels"))&&this._queuePickerPosition())}_window(){return this._floating.window()}_supportsPopover(){return this._floating.usesPopover(this._forceFallback)}_dialog(){return this._floating.dialog()}async _toggle(){this.disabled||(this._open?this._closePicker():(this._forceFallback=!1,this._syncFromColor(this.color,!1),this._hexInvalid=!1,this._activePointerId=null,this._open=!0,await this.updateComplete,this._open&&(this._supportsPopover()||this._renderFallback(),this._positionPicker()?this._overlayDispose=this._dialog()?.registerOverlay({owner:this,group:"transient",close:t=>this._closePicker("escape"===t&&this._floating.ownsActiveElement(),t)})||null:this._closePicker())))}_closePicker(t=!1,e="exclusive"){if(!this._open&&!this._floating.hasFallback)return;const s=this._overlayDispose;this._overlayDispose=null,s?.();const i=this.renderRoot.querySelector(".picker");if(i?.hidePopover)try{i.matches(":popover-open")&&i.hidePopover()}catch{}this._open=!1,this._cancelQueuedEmit(),this._activePointerId=null,this._floating.destroy(),t&&this.updateComplete.then(()=>this.renderRoot.querySelector(".trigger")?.focus())}_surface(){return this._floating.surface(".picker",this._supportsPopover())}_renderFallback(){const t=Ji.styles.cssText;this._floating.renderFallback(this._pickerTemplate(!1),t)}_positionPicker(){if(!this._open)return!1;const t=this._window(),e=this.renderRoot.querySelector(".trigger");let s=this._surface();if(!t||!e?.isConnected||!s?.isConnected)return!1;const i=qi(t);if(s.style.maxWidth=`${Math.max(0,i.width-16)}px`,s.style.maxHeight=`${Math.max(0,i.height-16)}px`,s.style.visibility="hidden",this._supportsPopover()&&s.showPopover)try{s.matches(":popover-open")||s.showPopover()}catch{if(this._forceFallback=!0,this._renderFallback(),s=this._surface(),!s?.isConnected)return!1;s.style.maxWidth=`${Math.max(0,i.width-16)}px`,s.style.maxHeight=`${Math.max(0,i.height-16)}px`,s.style.visibility="hidden"}const o=e.getBoundingClientRect(),a=s.getBoundingClientRect();if(!(o.width&&o.height&&a.width&&a.height))return!1;const n=Ui(o,a,i);return s.style.left=`${n.left}px`,s.style.top=`${n.top}px`,s.dataset.side=n.side,s.style.visibility="",!0}_labels(){const t=this.pickerLabels||Vi;return{title:t.title||Vi.title,hue:t.hue||Vi.hue,saturation:t.saturation||Vi.saturation,value:t.value||Vi.value,hex:t.hex||Vi.hex,invalidHex:t.invalidHex||Vi.invalidHex}}_syncFromColor(t,e){const s=Hi(l(t,"#607d8b"))||"#607d8b",i=Gi(s);if(!i)return;const o=Wi(i);(!e||o.s>1e-4)&&(this._hue=o.h),this._saturation=o.s,this._value=o.v,this._hexDraft=s,this._hexInvalid=!1,this._lastValidColor=s}_cancelQueuedEmit(t=!1){this._emitRaf&&cancelAnimationFrame(this._emitRaf),this._emitRaf=0,t&&this._syncFromColor(this._lastValidColor,!0)}_queueHsvEmit(){if(this._emitRaf)return;const t=this._window();t?this._emitRaf=t.requestAnimationFrame(()=>{this._emitRaf=0,this._emitHsv()}):this._emitHsv()}_emitHsv(){this._emit(ji({h:this._hue,s:this._saturation,v:this._value}),this.opacity)}_setHue(t){this._hue=Math.min(359,Math.max(0,Number.isFinite(t)?t:0)),this._emitHsv()}_setSaturation(t){this._saturation=Math.min(100,Math.max(0,Number.isFinite(t)?t:0)),this._emitHsv()}_setValue(t){this._value=Math.min(100,Math.max(0,Number.isFinite(t)?t:0)),this._emitHsv()}_setOpacity(t){this._emit(this._lastValidColor,t)}_shiftRangeKey(t,e,s,i,o){if(!t.shiftKey)return;const a="ArrowRight"===t.key||"ArrowUp"===t.key?10:"ArrowLeft"===t.key||"ArrowDown"===t.key?-10:0;a&&(t.preventDefault(),t.stopPropagation(),o(Math.min(i,Math.max(s,e+a))))}_setSvFromPointer(t,e){const s=t.currentTarget.getBoundingClientRect();s.width&&s.height&&(this._saturation=Math.min(100,Math.max(0,(t.clientX-s.left)/s.width*100)),this._value=Math.min(100,Math.max(0,100*(1-(t.clientY-s.top)/s.height))),e?(this._cancelQueuedEmit(),this._emitHsv()):this._queueHsvEmit())}_svPointerDown(t){if(t.preventDefault(),t.stopPropagation(),null===this._activePointerId){this._activePointerId=t.pointerId;try{t.currentTarget.setPointerCapture(t.pointerId)}catch{}this._setSvFromPointer(t,!0)}}_svPointerMove(t){t.pointerId===this._activePointerId&&(t.preventDefault(),t.stopPropagation(),this._setSvFromPointer(t,!1))}_svPointerUp(t){if(t.pointerId===this._activePointerId){t.preventDefault(),t.stopPropagation(),this._setSvFromPointer(t,!0),this._activePointerId=null;try{t.currentTarget.releasePointerCapture(t.pointerId)}catch{}}}_svPointerCancel(t){t.pointerId===this._activePointerId&&(t.preventDefault(),t.stopPropagation(),this._activePointerId=null,this._cancelQueuedEmit(!0))}_svLostPointerCapture(t){t.pointerId===this._activePointerId&&(this._activePointerId=null,this._cancelQueuedEmit(!0))}_hexInput(t){const e=t.target.value;this._hexDraft=e,this._hexInvalid=!1;const s=Hi(e),i=s?Gi(s):null;if(!s||!i)return;const o=Wi(i);o.s>1e-4&&(this._hue=o.h),this._saturation=o.s,this._value=o.v,this._emit(s,this.opacity,!0)}_commitHex(){const t=Hi(this._hexDraft);if(!t)return this._hexDraft=this._lastValidColor,void(this._hexInvalid=!0);this._hexDraft=t,this._hexInvalid=!1,t!==this._lastValidColor&&this._emit(t,this.opacity)}_hexKeyDown(t){"Enter"===t.key&&(t.preventDefault(),t.stopPropagation(),this._commitHex())}_emit(t,e,s=!1){const i=Hi(t)||this._lastValidColor,o=Number.isFinite(e)?e:Number(this.opacity)||0,a=Math.min(1,Math.max(0,o));this.color=i,this.opacity=a,this._lastValidColor=i,s||(this._hexDraft=i),this._hexInvalid=!1,this.dispatchEvent(new CustomEvent("hp-color-opacity-change",{detail:{color:i,opacity:a},bubbles:!0,composed:!0}))}_pickerTemplate(t){const e=Math.round(100*Math.min(1,Math.max(0,Number(this.opacity)||0))),s=ji({h:this._hue,s:this._saturation,v:this._value}),i=ji({h:this._hue,s:100,v:100}),n=this._labels(),r=Math.round(this._hue),l=Math.round(this._saturation),h=Math.round(this._value);return o` + `}render(){const t=Math.round(100*Math.min(1,Math.max(0,Number(this.opacity)||0))),e=l(this.color,"#607d8b"),s=this.showOpacity?`${this.label||"Color"}: ${e}, ${t}%`:`${this.label||"Color"}: ${e}`;return o` + ${this.label?o`${this.label}`:a} + + ${this._open&&!this.disabled&&this._supportsPopover()?this._pickerTemplate(!0):a} + `}}Ji.properties={label:{type:String},opacityLabel:{type:String,attribute:"opacity-label"},color:{type:String},opacity:{type:Number},disabled:{type:Boolean,reflect:!0},showOpacity:{type:Boolean,attribute:"show-opacity"},pickerLabels:{attribute:!1},_open:{state:!0},_hue:{state:!0},_saturation:{state:!0},_value:{state:!0},_hexDraft:{state:!0},_hexInvalid:{state:!0}},Ji.styles=n` + :host { + position: relative; + display: inline-flex; + align-items: center; + gap: 6px; + min-width: 0; + } + .label { + font-size: 12px; + color: var(--secondary-text-color, #9aa4ad); + white-space: nowrap; + } + .trigger { + width: 40px; + height: 40px; + flex: none; + box-sizing: border-box; + padding: 3px; + border: 1px solid var(--divider-color, #666); + border-radius: 7px; + background: + linear-gradient(45deg, #b8b8b8 25%, transparent 25%) 0 0 / 10px 10px, + linear-gradient(45deg, transparent 75%, #b8b8b8 75%) 0 0 / 10px 10px, + linear-gradient(45deg, transparent 75%, #b8b8b8 75%) 5px -5px / 10px 10px, + linear-gradient(45deg, #b8b8b8 25%, #eee 25%) 5px 5px / 10px 10px; + cursor: pointer; + } + :host([data-pointer-hover]) .trigger:hover { + border-color: var(--primary-color, #03a9f4); + } + .trigger[aria-expanded='true'] { + border-color: var(--primary-color, #03a9f4); + outline: none; + box-shadow: 0 0 0 1px var(--primary-color, #03a9f4); + } + .trigger:focus-visible { + border-color: var(--primary-color, #03a9f4); + outline: 2px solid var(--primary-color, #03a9f4); + outline-offset: 2px; + } + .swatch { + display: block; + width: 100%; + height: 100%; + border-radius: 3px; + box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.18); + pointer-events: none; + } + .picker { + /* The popover attribute promotes this surface into the browser top layer, outside + hp-dialog's scrolling/clipping body. JS supplies viewport coordinates. */ + position: fixed; + z-index: 2147483647; + inset: auto; + top: 0; + left: 0; + width: min(292px, calc(100vw - 16px)); + max-height: calc(100vh - 16px); + margin: 0; + box-sizing: border-box; + display: grid; + gap: 10px; + padding: 12px; + overflow: auto; + color: var(--primary-text-color, #fff); + background: var(--card-background-color, #202126); + border: 1px solid var(--primary-color, #03a9f4); + border-radius: 10px; + box-shadow: 0 10px 28px rgb(0 0 0 / 0.34); + } + .picker[popover]:not(:popover-open) { + /* Keep the author-level display:grid from exposing the surface for one + frame before showPopover() promotes it into the top layer. */ + display: none; + } + .picker-head, + .row, + .control-head { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + } + .picker-head { + justify-content: space-between; + } + .caption { + flex: 1; + min-width: 0; + overflow: hidden; + color: var(--secondary-text-color, #9aa4ad); + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; + } + .preview { + width: 52px; + height: 30px; + flex: none; + box-sizing: border-box; + padding: 3px; + border: 1px solid var(--divider-color, #666); + border-radius: 7px; + background: + linear-gradient(45deg, #b8b8b8 25%, transparent 25%) 0 0 / 10px 10px, + linear-gradient(45deg, transparent 75%, #b8b8b8 75%) 0 0 / 10px 10px, + linear-gradient(45deg, transparent 75%, #b8b8b8 75%) 5px -5px / 10px 10px, + linear-gradient(45deg, #b8b8b8 25%, #eee 25%) 5px 5px / 10px 10px; + } + .preview > span { + display: block; + width: 100%; + height: 100%; + border-radius: 3px; + } + .sv-field { + position: relative; + width: 100%; + height: 132px; + overflow: hidden; + box-sizing: border-box; + border: 1px solid var(--divider-color, #666); + border-radius: 8px; + background: + linear-gradient(to top, #000, transparent), + linear-gradient(to right, #fff, var(--hp-picker-hue, #f00)); + cursor: crosshair; + touch-action: none; + user-select: none; + } + .sv-thumb { + position: absolute; + left: var(--hp-picker-saturation, 0%); + top: var(--hp-picker-value, 100%); + width: 14px; + height: 14px; + box-sizing: border-box; + border: 2px solid #fff; + border-radius: 50%; + box-shadow: 0 0 0 1px rgb(0 0 0 / .65), 0 1px 3px rgb(0 0 0 / .5); + transform: translate(-50%, -50%); + pointer-events: none; + } + .control { + display: grid; + gap: 2px; + min-width: 0; + } + .control-value { + flex: none; + color: var(--secondary-text-color, #9aa4ad); + font-size: 12px; + } + input[type='range'] { + width: auto; + height: 40px; + min-width: 0; + flex: 1; + margin: -5px 0; + accent-color: var(--primary-color, #03a9f4); + touch-action: none; + } + .hue-range { + accent-color: var(--hp-picker-hue, #f00); + --hp-picker-hue-track: linear-gradient(to right, + #f00 0%, #ff0 16.667%, #0f0 33.333%, #0ff 50%, + #00f 66.667%, #f0f 83.333%, #f00 100%); + --hp-picker-hue-thumb-shadow: + 0 0 0 2px var(--card-background-color, #202126), + 0 0 0 3px var(--primary-text-color, #fff); + } + .hue-range::-webkit-slider-runnable-track { + height: 10px; + box-sizing: border-box; + border: 1px solid var(--divider-color, #666); + border-radius: 999px; + background: var(--hp-picker-hue-track); + } + .hue-range::-moz-range-track { + height: 10px; + box-sizing: border-box; + border: 1px solid var(--divider-color, #666); + border-radius: 999px; + background: var(--hp-picker-hue-track); + } + .hue-range::-moz-range-progress { + height: 10px; + border: 0; + border-radius: 999px; + background: transparent; + } + .hue-range::-webkit-slider-thumb { + border-radius: 50%; + box-shadow: var(--hp-picker-hue-thumb-shadow); + } + .hue-range::-moz-range-thumb { + border-radius: 50%; + box-shadow: var(--hp-picker-hue-thumb-shadow); + } + input[type='text'], + input[type='number'] { + min-height: 40px; + box-sizing: border-box; + border: 1px solid var(--divider-color, #666); + border-radius: 6px; + padding: 6px 8px; + color: var(--primary-text-color, #fff); + background: var(--input-fill-color, transparent); + font: inherit; + } + input[type='text'] { + width: 100%; + } + input[type='number'] { + width: 50px; + flex: none; + } + input:focus-visible, + .sv-field:focus-visible { + outline: 2px solid var(--primary-color, #03a9f4); + outline-offset: 1px; + } + input[aria-invalid='true'] { + border-color: var(--error-color, #db4437); + } + .error { + color: var(--error-color, #db4437); + font-size: 11px; + line-height: 1.25; + } + .pct { + color: var(--secondary-text-color, #9aa4ad); + font-size: 12px; + } + :host([disabled]) { + opacity: .5; + pointer-events: none; + } + @media (prefers-reduced-motion: reduce) { + .picker { transition: none !important; } + } + @media (forced-colors: active) { + .hue-range::-webkit-slider-runnable-track { + border-color: ButtonText; + background: Canvas; + } + .hue-range::-moz-range-track { + border-color: ButtonText; + background: Canvas; + } + .hue-range::-webkit-slider-thumb { + box-shadow: none; + } + .hue-range::-moz-range-thumb { + box-shadow: none; + } + } + `,customElements.get("hp-color-opacity")||customElements.define("hp-color-opacity",Ji);let Yi=0;class Xi extends r{constructor(){super(...arguments),this.text="",this.ariaLabel="",this.pointerHover=!1,this._open=!1,this._openTimer=0,this._closeTimer=0,this._positionRaf=0,this._forceFallback=!1,this._hoverOwned=!1,this._overlayDispose=null,this._scrollDialog=null,this._descriptionId="hp-help-description-"+ ++Yi,this._surfacePointerEnter=()=>{const t=this._window();this._closeTimer&&t?.clearTimeout(this._closeTimer),this._closeTimer=0},this._surfacePointerLeave=()=>{this._scheduleClose()},this._outsidePointerDown=t=>{if(!this._open)return;const e=t.composedPath();this._floating.containsPath(e)||this._closeHelp(!1,"outside")},this._dialogScroll=t=>{const e=this._floating.containsPath(t.composedPath()),s=t.target,i=this._dialog(),o=s instanceof Node&&(s===i||s instanceof Element&&s.contains(this));var a;this._open&&(a=o,!e&&a)&&this._closeHelp(!1,"scroll")},this._keyDown=t=>{if(!this._open||t.defaultPrevented)return;if("Escape"===t.key)return t.preventDefault(),t.stopImmediatePropagation(),void this._closeHelp(this._floating.ownsActiveElement(),"escape");const e=this.renderRoot.querySelector(".trigger");if(!e||!t.composedPath().includes(e))return;const s=this._surface();if(!s||s.scrollHeight<=s.clientHeight)return;const i=Math.max(40,.8*s.clientHeight),o={ArrowDown:40,ArrowUp:-40,PageDown:i,PageUp:-i};if("Home"===t.key)s.scrollTop=0;else if("End"===t.key)s.scrollTop=s.scrollHeight;else{if(!(t.key in o))return;s.scrollBy({top:o[t.key],behavior:"auto"})}t.preventDefault(),t.stopImmediatePropagation()},this._floating=new Ki(this,"help",this._keyDown),this._queuePosition=()=>{if(!this._open||this._positionRaf)return;const t=this._window();t&&(this._positionRaf=t.requestAnimationFrame(()=>{this._positionRaf=0,this._position()||this._closeHelp()}))}}connectedCallback(){super.connectedCallback(),this.addEventListener("keydown",this._keyDown,!0)}willUpdate(){this.toggleAttribute("data-has-content",this._hasContent())}disconnectedCallback(){const t=this.ownerDocument.defaultView;this._unsubscribeOpenListeners(),this.removeEventListener("keydown",this._keyDown,!0),this._clearTimers(),this._positionRaf&&t?.cancelAnimationFrame(this._positionRaf),this._positionRaf=0,this._closeHelp(!1,"disconnect"),super.disconnectedCallback()}updated(t){if(t.has("pointerHover")&&!this.pointerHover){const t=this._window();this._openTimer&&t?.clearTimeout(this._openTimer),this._openTimer=0,this._hoverOwned&&this._closeHelp(!1,"outside")}this._hasContent()||!this._open?this._open&&t.has("text")&&(this._usesPopover()||this._renderFallback(),this._queuePosition()):this._closeHelp()}_window(){return this._floating.window()}_dialog(){return this._floating.dialog()}_usesPopover(){return this._floating.usesPopover(this._forceFallback)}_hasContent(){return t=this.text,e=this.ariaLabel,"string"==typeof t&&t.trim().length>0&&"string"==typeof e&&e.trim().length>0;var t,e}_clearTimers(){const t=this._window();this._openTimer&&t?.clearTimeout(this._openTimer),this._closeTimer&&t?.clearTimeout(this._closeTimer),this._openTimer=0,this._closeTimer=0}_scheduleOpen(){const t=this._window();this.pointerHover&&t&&!this._open&&!this._openTimer&&(this._closeTimer&&t.clearTimeout(this._closeTimer),this._closeTimer=0,this._openTimer=t.setTimeout(()=>{this._openTimer=0,this.isConnected&&(this._hoverOwned=!0,this._openHelp())},300))}_scheduleClose(){const t=this._window(),e=this.renderRoot.querySelector(".trigger");t&&!e?.matches(":focus-visible")&&(this._openTimer&&t.clearTimeout(this._openTimer),this._closeTimer&&t.clearTimeout(this._closeTimer),this._openTimer=0,this._closeTimer=t.setTimeout(()=>{this._closeTimer=0,this.isConnected&&this._closeHelp()},150))}_triggerPointerEnter(t){this.pointerHover&&"mouse"===t.pointerType&&this._scheduleOpen()}_triggerPointerLeave(t){"mouse"===t.pointerType&&this._scheduleClose()}_triggerFocus(){queueMicrotask(()=>{const t=this.renderRoot.querySelector(".trigger");t?.matches(":focus-visible")&&(this._hoverOwned=!1,this._openHelp())})}_triggerBlur(){this._scheduleClose()}_triggerClick(){this._open?this._closeHelp():(this._hoverOwned=!1,this._openHelp())}_subscribeOpenListeners(){this.ownerDocument.addEventListener("pointerdown",this._outsidePointerDown,!0),this.ownerDocument.addEventListener("keydown",this._keyDown,!0);const t=this._window();t?.addEventListener("resize",this._queuePosition),t?.addEventListener("orientationchange",this._queuePosition),t?.visualViewport?.addEventListener("resize",this._queuePosition),t?.visualViewport?.addEventListener("scroll",this._queuePosition),this._scrollDialog=this._dialog(),this._scrollDialog?.addEventListener("scroll",this._dialogScroll,!0)}_unsubscribeOpenListeners(){this.ownerDocument.removeEventListener("pointerdown",this._outsidePointerDown,!0),this.ownerDocument.removeEventListener("keydown",this._keyDown,!0);const t=this._window();t?.removeEventListener("resize",this._queuePosition),t?.removeEventListener("orientationchange",this._queuePosition),t?.visualViewport?.removeEventListener("resize",this._queuePosition),t?.visualViewport?.removeEventListener("scroll",this._queuePosition),this._scrollDialog?.removeEventListener("scroll",this._dialogScroll,!0),this._scrollDialog=null}async _openHelp(){!this._open&&this._hasContent()&&(this._clearTimers(),this._forceFallback=!1,this._open=!0,this._subscribeOpenListeners(),await this.updateComplete,this._open&&(this._usesPopover()||this._renderFallback(),this._position()?this._overlayDispose=this._dialog()?.registerOverlay({owner:this,group:"transient",close:t=>this._closeHelp("escape"===t&&this._floating.ownsActiveElement(),t)})||null:this._closeHelp()))}_closeHelp(t=!1,e="exclusive"){if(!this._open&&!this._floating.hasFallback)return;this._clearTimers();const s=this._overlayDispose;this._overlayDispose=null,s?.();const i=this.renderRoot.querySelector(".tooltip");if(i?.hidePopover)try{i.matches(":popover-open")&&i.hidePopover()}catch{}this._open=!1,this._hoverOwned=!1,this._unsubscribeOpenListeners(),this._floating.destroy(),t&&this.updateComplete.then(()=>this.renderRoot.querySelector(".trigger")?.focus())}_tooltipTemplate(t){return o``}_renderFallback(){const t=Xi.styles.cssText;this._floating.renderFallback(this._tooltipTemplate(!1),t)}_surface(){return this._floating.surface(".tooltip",this._usesPopover())}_position(){if(!this._open)return!1;const t=this._window(),e=this.renderRoot.querySelector(".trigger");let s=this._surface();if(!t||!e?.isConnected||!s?.isConnected)return!1;const i=qi(t);if(s.style.maxWidth=`${Math.max(0,Math.min(320,i.width-16))}px`,s.style.maxHeight=`${Math.max(0,i.height-16)}px`,s.style.visibility="hidden",this._usesPopover()&&s.showPopover)try{s.matches(":popover-open")||s.showPopover()}catch{if(this._forceFallback=!0,this._renderFallback(),s=this._surface(),!s?.isConnected)return!1;s.style.visibility="hidden"}const o=e.getBoundingClientRect(),a=s.getBoundingClientRect();if(!(o.width&&o.height&&a.width&&a.height))return!1;const n=Ui(o,a,i);return s.style.left=`${n.left}px`,s.style.top=`${n.top}px`,s.dataset.side=n.side,s.style.visibility="",!0}render(){if(!this._hasContent())return a;const t=this.ariaLabel.trim();return o` + ${this.text} + + ${this._usesPopover()?this._tooltipTemplate(!0):a} + `}}Xi.properties={text:{type:String},ariaLabel:{type:String,attribute:"aria-label"},pointerHover:{type:Boolean,attribute:"data-pointer-hover",reflect:!0},_open:{state:!0},_forceFallback:{state:!0}},Xi.styles=n` + :host { + display: none; + flex: none; + vertical-align: middle; + } + + :host([data-has-content]) { + display: inline-flex; + } + + .trigger { + width: 32px; + height: 32px; + display: inline-flex; + align-items: center; + justify-content: center; + flex: none; + box-sizing: border-box; + padding: 0; + color: var(--secondary-text-color, #9aa4ad); + background: transparent; + border: 0; + border-radius: 50%; + cursor: help; + -webkit-tap-highlight-color: transparent; + } + + .trigger svg { + display: block; + width: 18px; + height: 18px; + fill: currentColor; + } + + :host([data-pointer-hover]) .trigger:hover, + .trigger:focus-visible, + .trigger[aria-expanded='true'] { + color: var(--primary-text-color, #fff); + background: rgb(127 127 127 / 0.18); + outline: none; + } + + .trigger:focus-visible { + box-shadow: 0 0 0 2px var(--primary-color, #03a9f4); + } + + .tooltip { + position: fixed; + z-index: 2147483647; + inset: auto; + top: 0; + left: 0; + width: max-content; + max-width: min(320px, calc(100vw - 16px)); + max-height: calc(100vh - 16px); + margin: 0; + box-sizing: border-box; + padding: 9px 11px; + overflow: auto; + color: var(--primary-text-color, #fff); + background: var(--card-background-color, #202126); + border: 1px solid var(--divider-color, rgb(255 255 255 / 0.2)); + border-radius: 9px; + box-shadow: 0 8px 24px rgb(0 0 0 / 0.32); + font: 400 13px/1.4 system-ui, sans-serif; + text-align: start; + white-space: normal; + overflow-wrap: anywhere; + opacity: 1; + transform: translateY(0); + transition: opacity 120ms ease, transform 120ms ease; + } + + .tooltip[data-side='top'] { + transform-origin: bottom center; + } + + .tooltip[data-side='bottom'] { + transform-origin: top center; + } + + .tooltip[popover]:not(:popover-open) { + display: none; + } + + @starting-style { + .tooltip { + opacity: 0; + transform: translateY(3px); + } + } + + .sr-only { + /* Keep the accessibility-only description outside every dialog scroll + container's overflow geometry. Toggling the hidden attribute on the former + absolutely-positioned node made some browsers add a vertical + scrollbar while help was open, even though the visible tooltip was + already in the Popover top layer / fixed fallback portal. */ + position: fixed; + inset: 0 auto auto 0; + width: 1px; + height: 1px; + padding: 0; + margin: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + clip-path: inset(50%); + white-space: nowrap; + border: 0; + } + + @media (pointer: coarse) { + .trigger { + width: 40px; + height: 40px; + } + } + + @media (prefers-reduced-motion: reduce) { + .tooltip { + transition: none; + } + } + `,customElements.get("hp-help")||customElements.define("hp-help",Xi);const Zi=new WeakMap;function Qi(t){const e=function(t){const e=t?.connection||t;return!e||"object"!=typeof e&&"function"!=typeof e?null:e}(t);if(!e)return null;let s=Zi.get(e);return s||(s={revision:0,loaded:!1,configEntries:{},manifests:{},listeners:new Set,refs:0},Zi.set(e,s)),s}function to(t){t.revision++;for(const e of[...t.listeners])try{e()}catch{}}function eo(t,e){const s=Qi(t);if(!s)return()=>{};s.refs++,s.listeners.add(e),async function(t,e){e.loading||"function"!=typeof t?.callWS?e.loading:e.loaded&&e.loadedAt&&Date.now()-e.loadedAt<3e5||(e.loading=(async()=>{const[s,i]=await Promise.allSettled([t.callWS({type:"config_entries/get"}),t.callWS({type:"manifest/list"})]);"fulfilled"===s.status&&Array.isArray(s.value)&&(e.configEntries=Object.fromEntries(s.value.filter(t=>"string"==typeof t?.entry_id).map(t=>[t.entry_id,t]))),"fulfilled"===i.status&&Array.isArray(i.value)&&(e.manifests=Object.fromEntries(i.value.filter(t=>"string"==typeof t?.domain).map(t=>[t.domain,t]))),e.loaded=!0,e.loadedAt=Date.now(),e.loading=void 0,to(e)})().catch(()=>{e.loaded=!0,e.loadedAt=Date.now(),e.loading=void 0,to(e)}),e.loading)}(t,s),async function(t,e){if(e.unsubscribe||e.subscribing)return e.subscribing;const s=t?.connection?.subscribeMessage;"function"==typeof s&&(e.subscribing=(async()=>{try{e.unsubscribe=await s.call(t.connection,t=>{const s=Array.isArray(t)?t:Array.isArray(t?.entries)?t.entries:t?[t]:[];let i=!1;const o={...e.configEntries};for(const t of s){const e=t?.entry||t?.data?.entry||("string"==typeof t?.entry_id?t:null),s=e?.entry_id;if("string"!=typeof s)continue;const a=t?.type||t?.action||t?.data?.action;"removed"===a||"remove"===a?delete o[s]:o[s]=e,i=!0}i&&(e.configEntries=o,to(e))},{type:"config_entries/subscribe"})}catch{}finally{0===e.refs&&(e.unsubscribe?.(),e.unsubscribe=void 0),e.subscribing=void 0}})(),e.subscribing)}(t,s);let i=!1;return()=>{i||(i=!0,s.listeners.delete(e),s.refs=Math.max(0,s.refs-1),s.refs||(s.unsubscribe?.(),s.unsubscribe=void 0))}}function so(t){const e=Qi(t);return e?{revision:e.revision,loaded:e.loaded,configEntries:e.configEntries,manifests:e.manifests}:{revision:0,loaded:!1,configEntries:{},manifests:{}}}function io(t,e,s=so(t),i){const o=s.manifests[e];return function(t,e){if("function"!=typeof t?.localize)return"";try{const s=`component.${e}.title`,i=t.localize(s);return"string"==typeof i&&i!==s?i:""}catch{return""}}(t,e)||String(o?.name||"")||String(i||"")||e.replace(/_/g," ")}function oo(t,e,s,i,o){const a=String(e||"").trim();if(!a)return null;const n=o?i.configEntries[o]:null;return{domain:a,label:io(t,a,i,n?.title),configEntryId:o,configEntryTitle:n?.title,confidence:s}}function ao(t){const e={"registry-owner":0,"entity-platform":1,"identifier-fallback":2},s=new Set;return t.filter(t=>!!t).sort((t,s)=>e[t.confidence]-e[s.confidence]||t.label.localeCompare(s.label)||t.domain.localeCompare(s.domain)).filter(t=>{const e=`${t.domain}\n${t.configEntryId||""}`;return!s.has(e)&&(s.add(e),!0)})}function no(t,e,s,i=so(t)){const o=s.entities?.[e]||t?.entities?.[e];if(!o)return null;const a=o.config_entry_id||void 0,n=a?i.configEntries[a]?.domain:"";return oo(t,o.platform||n,"entity-platform",i,a)}class ro extends r{constructor(){super(...arguments),this.deviceName="",this._metadataConnection=null,this._demoUntil=0,this._demoTimer=0,this._demoGeneration=1,this._demoKind=null,this._reducedMotion=!1,this._onMotionChange=t=>{this._reducedMotion=t.matches,this.requestUpdate()},this._startShortDemo=()=>{const t=this.presentation;t&&"icon_ripple"===t.display&&"none"===t.pulse.kind&&(this._demoGeneration++,this._demoKind="short",this._demoUntil=Date.now()+3300,window.clearTimeout(this._demoTimer),this._demoTimer=window.setTimeout(()=>{this._demoUntil=0,this._demoTimer=0,this._demoKind=null,this.requestUpdate()},3330),this.requestUpdate())},this._toggleContinuousDemo=()=>{const t=this.presentation;t&&"icon_ripple"===t.display&&"none"===t.pulse.kind&&("continuous"===this._demoKind?this._clearDemo():(this._clearDemo(),this._demoGeneration++,this._demoKind="continuous",this._demoUntil=Number.POSITIVE_INFINITY),this.requestUpdate())}}connectedCallback(){super.connectedCallback(),this._motionMedia=window.matchMedia?.("(prefers-reduced-motion: reduce)"),this._reducedMotion=!!this._motionMedia?.matches,this._motionMedia?.addEventListener?.("change",this._onMotionChange),this._ensureMetadata()}disconnectedCallback(){this._metadataRelease?.(),this._metadataRelease=void 0,this._metadataConnection=null,this._clearDemo(),this._motionMedia?.removeEventListener?.("change",this._onMotionChange),this._motionMedia=void 0,super.disconnectedCallback()}willUpdate(t){t.has("hass")&&this._ensureMetadata();const e=this.presentation,s=t.get("presentation");t.has("presentation")&&s?.binding!==e?.binding&&this._clearDemo(),e&&this._demoKind&&("icon_ripple"!==e.display||"none"!==e.pulse.kind)&&this._clearDemo()}_ensureMetadata(){const t=this.hass?.connection||null;t&&t!==this._metadataConnection&&(this._metadataRelease?.(),this._metadataConnection=t,this._metadataRelease=eo(this.hass,()=>this.requestUpdate()))}_clearDemo(){window.clearTimeout(this._demoTimer),this._demoTimer=0,this._demoUntil=0,this._demoKind=null}get _lang(){return d(this.hass)}_t(t,e){return p(this._lang,t,e)}_providerText(t){return"houseplan"===t.domain?this._t("marker.preview.virtual_provider"):t.label.toLowerCase()===t.domain.toLowerCase()?t.domain:`${t.label} (${t.domain})`}_providers(){return this.presentation&&this.registry?function(t,e,s,i=so(t)){if("virtual"===e)return[{domain:"houseplan",label:"House Plan",confidence:"registry-owner"}];const o=e.indexOf(":");if(o<1)return[];const a=e.slice(0,o),n=e.slice(o+1);if("entity"===a){const e=no(t,n,s,i);return e?[e]:[]}if("device"!==a)return[];const r=s.devices?.[n]||t?.devices?.[n];if(!r)return[];const l=[r.config_entry_id,...Array.isArray(r.config_entries)?r.config_entries:[]].filter(t=>"string"==typeof t&&!!t).map(e=>{const s=i.configEntries[e];return oo(t,s?.domain,"registry-owner",i,e)});if(l.some(Boolean))return ao(l);const h=[];for(const[e,o]of Object.entries(s.entities||{}))o?.device_id===n&&null==o?.disabled_by&&h.push(no(t,e,s,i));return h.some(Boolean)?ao(h):ao((Array.isArray(r.identifiers)?r.identifiers:[]).map(e=>{const s=Array.isArray(e)?e[0]:null;return oo(t,s,"identifier-fallback",i)}))}(this.hass,this.presentation.binding,this.registry,so(this.hass)):[]}_providerSummary(t){if(!t.length)return this._t("marker.preview.unknown_provider");const e=t.slice(0,2).map(t=>this._providerText(t)),s=t.length-e.length;return e.join(", ")+(s>0?` · ${this._t("marker.preview.more_sources",{n:s})}`:"")}_sourceSummary(t){const e=t.visualSources;return e.length?1===e.length?`${e[0].name} · ${e[0].eid} · ${this._sourceProvider(e[0].eid)}`:2===e.length?e.map(t=>t.name).join(" · "):this._t("marker.preview.multiple_sources",{n:e.length}):this._t("marker.preview.no_source")}_stateSummary(t){if(!t.visualSources.length)return this._t("marker.preview.no_state");if(1===t.visualSources.length)return t.visualSources[0].stateText||this._t("marker.preview.no_state");const e=[...new Set(t.visualSources.map(t=>t.stateText).filter(Boolean))];return 1===e.length?e[0]:this._t("marker.preview.mixed_states")}_reason(t){return this._t(`marker.preview.reason.${t}`)}_sourceProvider(t){if(!this.registry)return this._t("marker.preview.unknown_provider");const e=no(this.hass,t,this.registry,so(this.hass));return e?this._providerText(e):this._t("marker.preview.unknown_provider")}render(){const t=this.presentation;if(!t)return a;const e=!!this._demoKind&&this._demoUntil>Date.now()&&"icon_ripple"===t.display&&"none"===t.pulse.kind,s=e?(()=>{const e=_(t.pulse,this._demoKind,this._demoGeneration,this._reducedMotion,"short"===this._demoKind?this._demoUntil:null);return{...t,pulse:e,classes:[...t.classes.filter(t=>!t.startsWith("pulse-")),`pulse-${e.kind}`,...e.generation%2==0?["pulse-gen2"]:[]]}})():t,i=this._providers(),n="none"!==t.pulse.kind,r=s.scale*(s.pulse.animated?s.pulse.diameterScale:1);let l=-r/2,h=r/2,c=-r/2,d=r/2;if(s.valueBadge){const t=s.scale*Math.min(4,Math.max(.9,.29*s.valueBadge.text.length+.28)),e=.7*s.scale,i=.6*s.scale;"right"===s.valueBadge.position&&(h=Math.max(h,i+t)),"left"===s.valueBadge.position&&(l=Math.min(l,-i-t)),"top"===s.valueBadge.position&&(c=Math.min(c,-i-e)),"bottom"===s.valueBadge.position&&(d=Math.max(d,i+e))}if(null!=s.lqiText){const t=s.scale*("bottom"===s.valueBadge?.position?1.55:.95);d=Math.max(d,t)}const p=Math.min(1,2.35/Math.max(1,h-l),2.35/Math.max(1,d-c)),b=Math.round(100*p),y=[`left:calc(50% - ${54*((l+h)/2)*p}px)`,`top:calc(50% - ${54*((c+d)/2)*p}px)`,p<1?`transform:scale(${p})`:""].filter(Boolean).join(";"),v=["dev",u(this.hass),...s.classes,"virtual"===s.binding?"virtual":"",s.haDisabled?"ghost ha-disabled":"",null!=s.valueText?"valonly":""].filter(Boolean).join(" "),$=m(s).join(";"),w=e?this._t("continuous"===this._demoKind?"marker.preview.demo_continuous_notice":"marker.preview.demo_short_notice"):this._reason(t.explanation.reason),k=[this.deviceName,this._providerSummary(i),this._stateSummary(t),w,t.valueFullText||"",g(t.valueBadge),null!=t.lqiText&&t.lqiBand?this._t(`marker.lqi_a11y_${t.lqiBand}`,{value:t.lqiText}):""].filter(Boolean).join(". ");return o`
+
+ ${this._t("marker.preview.title")} + + ${this._t(e?"marker.preview.example":"marker.preview.actual")} + +
+
+ +
+
${this._t("marker.preview.integration")}${this._providerSummary(i)}
+
${this._t("marker.preview.source")}${this._sourceSummary(t)}
+
${this._t("marker.preview.current_state")}${this._stateSummary(t)}
+
${this._t("marker.preview.result")}${w}
+ ${t.fallbackReason?o`

${this._reason(t.fallbackReason)}

`:a} + ${t.explanation.notices.map(t=>o`

${this._reason(t)}

`)} + ${p<1?o`

${this._t("marker.preview.scaled",{n:b})}

`:a} + ${e&&this._reducedMotion?o`

${this._t("marker.preview.reduced_motion")}

`:a} +
+
+ ${t.visualSources.length||t.criticalSources.length?o`
+ ${this._t("marker.preview.details")} +
+ ${[...t.visualSources,...t.criticalSources].map(t=>o` +
+ ${t.name}${t.eid} + ${t.stateText||t.state} + ${this._sourceProvider(t.eid)} +
`)} +
+
`:a} + ${"icon_ripple"===t.display?o`
+ + +
`:a} +
`}}ro.properties={hass:{attribute:!1},presentation:{attribute:!1},registry:{attribute:!1},deviceName:{attribute:!1}},ro.styles=[c,n` + :host { display: block; min-width: 0; } + .devicepreview { + margin: 4px 0 14px; + padding: 14px; + border: 1px solid var(--divider-color, rgba(127, 127, 127, 0.28)); + border-radius: 14px; + background: color-mix(in srgb, var(--card-background-color, #20232b) 92%, var(--primary-color, #03a9f4)); + color: var(--primary-text-color, #fff); + overflow: hidden; + } + .previewhead { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; } + .previewbadge { + padding: 3px 9px; + border-radius: 999px; + color: var(--secondary-text-color, #9aa0aa); + background: color-mix(in srgb, var(--secondary-text-color, #9aa0aa) 12%, transparent); + font-size: 12px; + font-weight: 700; + } + .previewbadge.example { color: var(--warning-color, #ffb300); } + .previewgrid { display: grid; grid-template-columns: minmax(170px, 0.8fr) minmax(240px, 1.2fr); gap: 16px; align-items: stretch; } + .previewstage { + position: relative; + min-height: 168px; + border-radius: 12px; + background: var(--secondary-background-color, rgba(0, 0, 0, 0.16)); + overflow: hidden; + pointer-events: none; + container-type: inline-size; + --icon-size: 54px; + --device-base-size: 48.6px; + } + .previewfit { position: absolute; left: 50%; top: 50%; transform-origin: center; } + .previewstage .dev { left: 0; top: 0; cursor: default; } + :host([data-pointer-hover]) .previewstage .dev:hover { z-index: 2; } + .previewfacts { min-width: 0; display: grid; align-content: start; gap: 8px; } + .previewfacts > div { display: grid; grid-template-columns: minmax(115px, 0.8fr) minmax(0, 1.3fr); gap: 8px; } + .previewfacts span { color: var(--secondary-text-color, #9aa0aa); } + .previewfacts b { min-width: 0; overflow-wrap: anywhere; } + .previewnotice { margin: 2px 0 0; color: var(--secondary-text-color, #9aa0aa); font-size: 13px; } + .previewdetails { margin-top: 12px; } + .previewdetails summary { cursor: pointer; color: var(--secondary-text-color, #9aa0aa); } + .previewtable { display: grid; gap: 8px; margin-top: 9px; } + .previewsource { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 3px 10px; font-size: 13px; } + .previewsource > * { min-width: 0; overflow-wrap: anywhere; } + .previewsource code { color: var(--secondary-text-color, #9aa0aa); } + .previewdemo { + min-height: 40px; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border: 1px solid var(--divider-color, rgba(127, 127, 127, 0.35)); + border-radius: 10px; + background: transparent; + color: inherit; + cursor: pointer; + } + .previewdemos { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; } + .previewdemo:focus-visible { outline: 2px solid var(--primary-color, #03a9f4); outline-offset: 2px; } + .previewdemo:disabled { opacity: 0.5; cursor: not-allowed; } + @media (max-width: 640px) { + .previewgrid { grid-template-columns: minmax(0, 1fr); } + .previewstage { min-height: 142px; } + .previewfacts > div { grid-template-columns: minmax(0, 1fr); gap: 2px; } + } + `],customElements.get("hp-device-preview")||customElements.define("hp-device-preview",ro);const lo=(t,e)=>[t[0]-e[0],t[1]-e[1]],ho=(t,e)=>[t[0]+e[0],t[1]+e[1]],co=(t,e)=>t[0]*e[0]+t[1]*e[1],po=t=>Math.hypot(t[0],t[1]);function _o(t){let e=0;for(let s=0;st[1]!=l>t[1]&&t[0]<(r-a)*(t[1]-n)/(l-n)+a&&(s=!s)}return s}([r[0]+n[0]*l,r[1]+n[1]*l],t)&&(n=[-n[0],-n[1]]),n}function go(t){const e=t.length;if(e<3)return!1;for(let s=0;s(t[0]-o[0])*r[1]-(t[1]-o[1])*r[0],h=t=>(t[0]-o[0])*r[0]+(t[1]-o[1])*r[1],c=s,d=n-s;for(const e of t){const t=Math.abs(l(e));if(t<=s)continue;const o=h(e);o<=c||o>=d||tMath.abs(n+(t-p)/g*(r-n));i=Math.min(i,f(u),f(m))}}return i}function bo(t,e,s){if($(t,e,s))return!0;if(v(t,e,s)||v(e,t,s))return!1;let i=0;try{const s=w([[...t.map(t=>[t[0],t[1]]),[t[0][0],t[0][1]]]],[[...e.map(t=>[t[0],t[1]]),[e[0][0],e[0][1]]]]);for(const t of s)t?.[0]&&(i+=k(t[0]))}catch{return!1}return i>Math.max(1e-7,s*s)}function yo(t,e,s){for(const i of e)for(let e=0;es)continue;const h=(t.x-o[0])*l[0]+(t.y-o[1])*l[1];if(h-t.length/2>=-s&&h+t.length/2<=r+s)return!0}return!1}function vo(t,e,s){const i=Math.abs(e[0]-t[0]),o=Math.abs(e[1]-t[1]);return i<=s&&o>s?"v":o<=s&&i>s?"h":null}function $o(t,e,s){return Math.abs(t[0]-e[0])<=s&&Math.abs(t[1]-e[1])<=s}function wo(t,e,s,i,o){return $o(t,s,o)&&$o(e,i,o)||$o(t,i,o)&&$o(e,s,o)}function ko(t,e,s,i,o){const a=vo(t,e,o);return a&&vo(s,i,o)===a?"h"===a?Math.abs(t[1]-s[1])>o||Math.abs(t[1]-i[1])>o?0:Math.max(0,Math.min(Math.max(t[0],e[0]),Math.max(s[0],i[0]))-Math.max(Math.min(t[0],e[0]),Math.min(s[0],i[0]))):Math.abs(t[0]-s[0])>o||Math.abs(t[0]-i[0])>o?0:Math.max(0,Math.min(Math.max(t[1],e[1]),Math.max(s[1],i[1]))-Math.max(Math.min(t[1],e[1]),Math.min(s[1],i[1]))):0}function xo(t,e,s,i){const o=t.length,a=vo(t[(e-1+o)%o],t[e],i),n=vo(t[(e+1)%o],t[(e+2)%o],i),r="h"===s?"v":"h";return a===r&&n===r}function So(t){let e=1/0,s=1/0,i=-1/0,o=-1/0;for(const a of t)e=Math.min(e,a[0]),s=Math.min(s,a[1]),i=Math.max(i,a[0]),o=Math.max(o,a[1]);return[e,s,i,o]}function Mo(t,e,s){const i=So(t),o=So(e);return i[2]o}const Co=(t,e)=>"h"===e?t[0]:t[1],Po=(t,e)=>"h"===e?t[1]:t[0];function Io(t,e,s,i,o){const a=t.find(t=>t.id===e);if(!a)return null;const n=a.poly[s],r=a.poly[(s+1)%a.poly.length],l=vo(n,r,o);if(!l)return null;const h=0===i?r:n,c=0===i?n:r,d=Co(h,l),p=Co(c,l),_=Po(h,l),u=function(t,e,s,i){const o=[];for(const a of t)for(let t=0;ti||Math.abs(Po(r,e)-s)>i)continue;const l=Co(n,e),h=Co(r,e),c=Math.min(l,h),d=Math.max(l,h);d-c>i&&o.push({roomId:a.id,edge:t,lo:c,hi:d})}return o.sort((t,e)=>t.lo-e.lo||t.hi-e.hi||t.roomId.localeCompare(e.roomId)||t.edge-e.edge)}(t,l,_,o),m=function(t,e,s,i){const o=[e,s];for(const a of t){const t=Math.max(e,a.lo),n=Math.min(s,a.hi);n-t>i&&o.push(t,n)}const a=[...new Set(o)].sort((t,e)=>t-e),n=[];for(let e=0;e+1r>t.lo-i&&rt.roomId))].sort();n.push({lo:s,hi:o,owners:l})}return n}(u,Math.min(d,p),Math.max(d,p),o);return!m.length||m.some(t=>t.owners.length<1||t.owners.length>2)?null:{roomId:e,edge:s,movedEndpoint:i,axis:l,line:_,fixed:d,moving:p,intervals:u,baseline:m}}function To(t,e,s){const i=[];for(const o of t.intervals){const a=e.polys[o.roomId];if(!a){i.push(o);continue}if(o.edge<0||o.edge>=a.length)continue;const n=a[o.edge],r=a[(o.edge+1)%a.length];if(vo(n,r,s)!==t.axis||Math.abs(Po(n,t.axis)-t.line)>s||Math.abs(Po(r,t.axis)-t.line)>s)continue;const l=Co(n,t.axis),h=Co(r,t.axis),c=Math.min(l,h),d=Math.max(l,h);d-c>s&&i.push({...o,lo:c,hi:d})}return i}function Ro(t,e,s){return new Set(t.filter(t=>e>t.lo-s&&et.roomId)).size}function Fo(t,e,s){for(const i of e.sideOwnership){const e=t.polys[i.roomId];if(!e)return!1;const o=e[i.edge],a=e[(i.edge+1)%e.length];if(vo(o,a,s)!==i.axis)return!1;const n=Co(0===i.movedEndpoint?o:a,i.axis),r=To(i,t,s),l=(i.moving>=i.fixed?i.baseline[i.baseline.length-1]:i.baseline[0]).owners.length;if(l<1||l>2)return!1;const h=Math.min(i.fixed,i.moving,n),c=Math.max(i.fixed,i.moving,n),d=[h,c,i.fixed,i.moving,n];for(const t of i.intervals)d.push(t.lo,t.hi);for(const t of r)d.push(t.lo,t.hi);const p=[...new Set(d.filter(t=>t>=h&&t<=c))].sort((t,e)=>t-e),_=Math.min(i.fixed,i.moving),u=Math.max(i.fixed,i.moving),m=Math.min(i.fixed,n),g=Math.max(i.fixed,n);for(let t=0;t+1_-s&&am-s&&a2||d>2)return!1;if(n&&h){if(c!==d)return!1}else if(!n&&h){if(d!==l||c>0&&c!==d)return!1}else if(2===c?2!==d:d>1)return!1}}return!0}function No(t,e,s,i){const o=[s.n[0]*i,s.n[1]*i],a={polys:{},openings:{},movedSpans:{}};for(const e of s.roomIds){const i=t.find(t=>t.id===e),n=s.edgeByRoom[e];if(!i||null==n)continue;const r=(n+1)%i.poly.length;a.polys[e]=i.poly.map((t,e)=>e===n||e===r?ho(t,o):[...t]),a.movedSpans[e]=[[ho(i.poly[n],o),ho(i.poly[r],o)]]}for(const t of e)s.movingOpeningIds.includes(t.id)&&(a.openings[t.id]=[t.x+o[0],t.y+o[1]]);return a}function zo(t,e,s,i,o,a,n,r){if(!function(t,e,s,i){if(uo([t.x,t.y],e,s)>2*i)return!1;const o=po(lo(s,e));if(o<=i)return!1;const a=[(s[0]-e[0])/o,(s[1]-e[1])/o],n=(t.x-e[0])*a[0]+(t.y-e[1])*a[1];return n>=-i&&n<=o+i}(t,e,s,r))return!0;if(uo([t.x,t.y],i,o)>2*r)return!1;const l=0===a?i:o;return po(lo([t.x,t.y],l))>=t.length/2+n-r}function Ao(t,e,s,i){const o=i.movingHalf||0;return"circle"===t.kind?uo(t.center,e,s)2)return!1;const r=No(t,e,s,i);if(Object.keys(r.polys).length!==s.roomIds.length)return!1;if(!Fo(r,s,a))return!1;const l=new Set(s.roomIds),h=t=>r.polys[t.id]||t.poly;for(const i of s.roomIds){const l=t.find(t=>t.id===i),h=r.polys[i],c=s.edgeByRoom[i];if(!l||!h||h.length!==s.topology[i]||h.length!==l.poly.length||!go(h))return!1;const d=l.poly.length,p=(c-1+d)%d,_=[p,c,(c+1)%d];for(const t of _)if(y(h[t],h[(t+1)%d]))return!1;const u=_o(l.poly),m=_o(h);if(Math.abs(m)t.id===e),i=r.polys[e];for(const o of t){if(o.id===e||l.has(o.id))continue;const t=h(o);if(!Mo(s.poly,o.poly,a)||!Mo(i,t,a))if(v(s.poly,o.poly,a)){if(!v(i,t,a))return!1}else if(v(o.poly,s.poly,a)){if(!v(t,i,a))return!1}else if(v(i,t,a)||v(t,i,a)||bo(i,t,a))return!1}}t.find(t=>t.id===s.roomId);const c=r.polys[s.roomId],d=s.edgeByRoom[s.roomId],p=c[d],_=c[(d+1)%c.length];if(Math.abs(i)>a)for(const t of o.obstacles||[])if(Ao(t,p,_,o))return!1;for(const t of e){if(!s.movingOpeningIds.includes(t.id))continue;const e=r.openings[t.id];if(!e||t.hosted)return!1;if(!yo({...t,x:e[0],y:e[1]},[c],2*a))return!1}return!0}const Lo=new WeakMap,Eo=t=>"number"==typeof t&&Number.isFinite(t),Bo=(t,e)=>{const s=new Map;for(const i of t||[])!Eo(i?.cm)||!e&&i.cm<=0||s.set(i.cm,(s.get(i.cm)||0)+1);return s},Ho=t=>[String(t),"см"].join(" ");function Go(t,e,s={}){if(s.allowClear)return[];const i=!0===s.exactMultiplicity,o=Bo(t,i),a=Bo(e,i),n=[];for(const[t,e]of o){const s=a.get(t)||0;0===s?n.push({invariant:"wall_records",kind:"lost",owner:Ho(t),reference:`было ${e}`,detail:"записи этой толщины исчезли целиком"}):i&&s!==e&&n.push({invariant:"wall_records",kind:"count",owner:Ho(t),reference:`было ${e}, стало ${s}`,detail:"изменилось число записей этой толщины"})}if(i)for(const[t,e]of a)o.has(t)||n.push({invariant:"wall_records",kind:"count",owner:Ho(t),reference:`было 0, стало ${e}`,detail:"появились записи новой толщины"});return n}class Wo{constructor(){this._selectedRoomId=null,this._session=null,this._eligibility=null}get dragging(){return null!==this._session}ownsPointer(t){return this._session?.pointerId===t}get selectedRoomId(){return this._selectedRoomId}get snapshotIdentity(){return this._session?.snapshotIdentity||null}get activePointerId(){return this._session?.pointerId??null}get moved(){return!0===this._session?.moved}get delta(){return this._session?.delta||0}get rooms(){return this._session?.rooms||null}get openings(){return this._session?.openings||null}get plan(){return this._session?.plan||null}get preview(){return this._session?.accepted?.preview||null}get liveLabels(){return this._session?.accepted?.labels||null}selectRoom(t){this._selectedRoomId=t}restoreSelection(t){this._selectedRoomId=t}escapeIdle(){return this._selectedRoomId?(this._selectedRoomId=null,"selection-cleared"):"exit-tool"}resolve(t,e,s){this._eligibility&&this._eligibility.context===t||(this._eligibility={context:t,values:new Map});const i=this._eligibility.values.get(e);if(i)return i;const o=s();return this._eligibility.values.set(e,o),o}begin(t){return!this._session&&(this._session={pointerId:t.pointerId,start:[...t.start],roomId:t.roomId,plan:t.plan,options:t.options,rooms:t.rooms,openings:t.openings,snapshotIdentity:t.snapshotIdentity,before:t.before,moved:!1,delta:0,changedRoomIds:[...t.plan.roomIds],rejectionNotified:!1,wallUnionBefore:t.wallUnionBefore,accepted:null},!0)}move(t){const e=this._session;if(!e||e.pointerId!==t.pointerId)return{kind:"no-op"};const s=(i=e.start,o=t.point,a=e.plan.n,[i?.[0],i?.[1],o?.[0],o?.[1],a?.[0],a?.[1]].every(Number.isFinite)?(o[0]-i[0])*a[0]+(o[1]-i[1])*a[1]:0);var i,o,a;const n=t.snap([e.plan.a[0]+e.plan.n[0]*s,e.plan.a[1]+e.plan.n[1]*s]),r=(n[0]-e.plan.a[0])*e.plan.n[0]+(n[1]-e.plan.a[1])*e.plan.n[1],l=function(t,e,s,i,o,a){if(!Number.isFinite(i)||Math.abs(i)<1e-9)return 0;const n=Math.sign(i),r=Math.abs(i),l=Math.max(Math.abs(o),1e-6);let h=Lo.get(s);h&&h.opts===a||(h={opts:a,values:new Map},Lo.set(s,h));const c=h.values;let d=0;for(let i=Math.min(l,r),o=0;o<4096&&i<=r+1e-9;o++,i=Math.min(r,i+l)){const o=n*i,h=`${o.toFixed(9)}|${l.toFixed(9)}`;let p=c.get(h);if(void 0===p&&(p=Oo(t,e,s,o,a),c.size<4096&&c.set(h,p)),!p)break;if(d=o,Math.abs(i-r)<=1e-9)break}return d}(e.rooms,e.openings,e.plan,r,t.step,e.options);if(e.moved&&l===e.delta)return{kind:"no-op"};const h=No(e.rooms,e.openings,e.plan,l);let c;try{if(c=t.project(e.snapshotIdentity,h.polys,h.openings,e.changedRoomIds,e.rooms),!c.ok)return this._projectionRejected(e);if(Go(c.value.beforeWalls,c.value.afterWalls,{exactMultiplicity:!0}).length)return this._projectionRejected(e)}catch{return this._projectionRejected(e)}const d=e.accepted,p={preview:c.value.preview,labels:null,artifact:c.value.artifact,beforeWalls:c.value.beforeWalls,afterWalls:c.value.afterWalls};e.accepted=p;try{t.publish(p.preview,p.artifact),p.labels=t.measure(h,e.plan)}catch{e.accepted=d;try{t.publish(d?.preview||null,d?.artifact||null)}catch{}return this._projectionRejected(e)}return e.moved=!0,e.delta=l,{kind:"accepted",preview:c.value.preview,labels:p.labels,artifact:c.value.artifact}}finish(t){const e=this._session;if(!e||e.pointerId!==t.pointerId)return{kind:"no-op"};const s=e.accepted;let i;if(e.moved&&Math.abs(e.delta)>1e-9&&s)if(t.currentSnapshotIdentity!==e.snapshotIdentity)i={kind:"rejected",reason:"stale-snapshot"};else if(Oo(e.rooms,e.openings,e.plan,e.delta,e.options))if(Go(s.beforeWalls,s.afterWalls,{exactMultiplicity:!0}).length)i={kind:"rejected",reason:"wall-records"};else{let o=!1;try{o=t.validatePreview(s.preview)}catch{o=!1}i=o?{kind:"commit",preview:s.preview,before:e.before}:{kind:"rejected",reason:"invalid-candidate"}}else i={kind:"rejected",reason:"invalid-topology"};else i={kind:"no-op"};return this._session=null,"commit"===i.kind&&(this._eligibility=null),i}cancel(t,e){const s=this._session;return!s||void 0!==e&&s.pointerId!==e?{kind:"no-op"}:(this._session=null,this._eligibility=null,{kind:"cancelled",restoreWallUnion:t===s.snapshotIdentity?s.wallUnionBefore:null})}reset(){this._selectedRoomId=null,this._session=null,this._eligibility=null}_projectionRejected(t){const e=!t.rejectionNotified;return t.rejectionNotified=!0,{kind:"rejected",notify:e}}}const jo=(t,e)=>[(t[0]-e.x)/Math.max(1e-9,e.w)*e.stageWidth,(t[1]-e.y)/Math.max(1e-9,e.h)*e.stageHeight];function qo(t){const{poly:e,edge:s,text:i,view:o}=t,a=e[s],n=e[(s+1)%e.length],r=[(a[0]+n[0])/2,(a[1]+n[1])/2],l=function(t,e){const s=t[e],i=t[(e+1)%t.length],o=i[0]-s[0],a=i[1]-s[1],n=Math.hypot(o,a)||1;return(t=>{let e=0;for(let s=0;s=0?[-a/n,o/n]:[a/n,-o/n]}(e,s),h=Math.hypot(n[0]-a[0],n[1]-a[1])||1,c=[(n[0]-a[0])/h,(n[1]-a[1])/h],d=jo(r,o),p=jo(t.gearCenter,o),_=Math.max(34,7.2*i.length+12),u=Math.abs(l[0])>=Math.abs(l[1])?Math.max(28,_/2+4):Math.max(28,13),m=d[0]+l[0]*u,g=d[1]+l[1]*u,f=e=>{return s=m+c[0]*e,i=g+c[1]*e,o=_,a=18,n=p[0],r=p[1],l=t.gearWidthPx,h=t.gearHeightPx,2*Math.abs(s-n)=Math.abs(c[1])?c[0]:c[1])>0?-1:1,e=Math.max(o.stageWidth,o.stageHeight,64);for(let s=4;s<=e;s+=4){const e=s*t;if(!f(e)){b=e;break}const i=-e;if(!f(i)){b=i;break}}}const y=l[0]*u+c[0]*b,v=l[1]*u+c[1]*b,$=Math.hypot(y,v)||1,w=v/$*12,k=[r[0]+y/$*12*o.w/Math.max(1,o.stageWidth),r[1]+w*o.h/Math.max(1,o.stageHeight)];return{anchor:r,offsetXPx:y,offsetYPx:v,tangentOffsetPx:b,side:Math.abs(l[0])>=Math.abs(l[1])?l[0]<0?"left":"right":l[1]<0?"above":"below",leader:{a:r,b:k}}}const Uo=1e-9,Ko=1e7,Vo=t=>{const e=Math.sign(t)*Math.floor(Math.abs(t)*Ko+.5)/Ko;return Object.is(e,-0)?0:e},Jo=t=>`${Vo(t[0])},${Vo(t[1])}`,Yo=(t,e)=>Math.hypot(e[0]-t[0],e[1]-t[1]),Xo=(t,e)=>Math.atan2(e[1]-t[1],e[0]-t[0]),Zo=t=>Array.isArray(t)&&t.length>=2&&t.every(t=>Number.isFinite(t)),Qo=t=>(t||[]).filter(t=>Zo(t?.a)&&Zo(t?.b)&&Yo(t.a,t.b)>Uo);function ta(t,{minAngleDeg:e=15,maxValence:s=6}={}){const i=new Map;for(const e of Qo(t))for(const[t,s]of[[e.a,e.b],[e.b,e.a]]){const e=i.get(Jo(t))||[];e.push(Xo(t,s)),i.set(Jo(t),e)}const o=[];for(const[t,a]of i){if(a.length>s&&o.push({rule:"valence",subject:t,actual:a.length,limit:s}),a.length<2)continue;const i=[...a].sort((t,e)=>t-e);let n=1/0;for(let t=0;tUo&&s(180*Math.atan2(t.b[1]-t.a[1],t.b[0]-t.a[0])/Math.PI%180+180)%180,sa=(t,e,s=1)=>{const i=Math.abs(ea(t)-ea(e));return Math.min(i,180-i)<=s},ia=t=>{const e=new Map;for(const s of t)for(const t of[s.a,s.b]){const i=e.get(Jo(t));i?i.push(s):e.set(Jo(t),[s])}return e};function oa(t,e,s){const i=Qo(e),o=s??ia(i),a=new Set([t]);let n=Yo(t.a,t.b);const r=[t.a,t.b];for(;r.length;){const e=r.pop();for(const s of o.get(Jo(e))||[])a.has(s)||sa(s,t)&&Number(s.cm||0)===Number(t.cm||0)&&(a.add(s),n+=Yo(s.a,s.b),r.push(s.a,s.b))}return n}function aa(t,e,s,{minLengthCm:i=20}={}){const o=[],a=Qo(t),n=ia(a);for(const t of a){const r=oa(t,a,n)/s*(e||1),l=Math.max(i,Number(t.cm)>0?Number(t.cm):0);r{const i=s[0]-e[0],o=s[1]-e[1],a=i*i+o*o,n=a<=Uo?0:Math.max(0,Math.min(1,((t[0]-e[0])*i+(t[1]-e[1])*o)/a));return Math.hypot(t[0]-(e[0]+i*n),t[1]-(e[1]+o*n))};function ra(t,e,s,{minDistanceCm:i=5}={}){const o=Qo(t),a=new Map;for(const t of o)a.set(Jo(t.a),t.a),a.set(Jo(t.b),t.b);const n=((t,e,s)=>t/(e||1)*s)(i,e,s),r=n>Uo?n:1,l=(t,e)=>`${Math.floor(t/r)},${Math.floor(e/r)}`,h=new Map;for(const[t,e]of a){const s=l(e[0],e[1]),i=h.get(s);i?i.push([t,e]):h.set(s,[[t,e]])}const c=new Map;for(const t of o){const e=Math.min(t.a[0],t.b[0])-n,s=Math.max(t.a[0],t.b[0])+n,i=Math.min(t.a[1],t.b[1])-n,o=Math.max(t.a[1],t.b[1])+n;for(let a=Math.floor(e/r);a<=Math.floor(s/r);a++)for(let e=Math.floor(i/r);e<=Math.floor(o/r);e++){const s=`${a},${e}`,i=c.get(s);i?i.push(t):c.set(s,[t])}}const d=[];for(const[t,o]of a){const a=Math.floor(o[0]/r),l=Math.floor(o[1]/r);for(let r=-1;r<=1;r++)for(let c=-1;c<=1;c++)for(const[p,_]of h.get(`${a+r},${l+c}`)||[]){if(t>=p)continue;const a=Yo(o,_);a<=2e-7||a{const i=a[(s+1)%a.length];return t+(e[0]*i[1]-i[0]*e[1])},0))/2,r=(s||1)/i,l=n*r*r;return lt.removed).map(t=>t.binding)),c=new Set;for(const t of s)t.id!==n&&("device"===t.bindingKind&&t.bindingRef&&c.add(`device:${t.bindingRef}`),"entity"===t.bindingKind&&t.bindingRef&&c.add(`entity:${t.bindingRef}`));const d=new Set;for(const t of s)"device"===t.bindingKind&&t.name&&d.add(`${t.name.trim()}|${t.area||""}`);const p=[];for(const t of Object.values(e?.devices||{})){if(!t||"service"===t.entry_type)continue;const e=`device:${t.id}`;if(c.has(e))continue;const s=String(t.name_by_user||t.name||t.id).trim();e!==a&&!h.has(e)&&d.has(`${s}|${t.area_id||""}`)||p.push({value:e,label:s,sub:(t.model||r.device)+("Group"===t.model?r.z2mGroup:""),kind:"device",ref:t.id,areaId:t.area_id||"",model:t.model||""})}for(const[t,s]of Object.entries(e?.entities||{})){const i=`entity:${t}`;if(c.has(i))continue;if(S(e,t,l)&&!h.has(i))continue;const o=ha.has(s?.platform),a="group"===s?.platform;if(!o&&!a)continue;if(s?.hidden&&!h.has(i))continue;const n=e?.states?.[t];p.push({value:i,label:s?.name||n?.attributes?.friendly_name||t,sub:`${t.split(".")[0]} · ${a?r.group:r.helper}`,kind:"entity",ref:t,areaId:s?.area_id||s?.device_id&&e?.devices?.[s.device_id]?.area_id||"",model:"",parentDeviceId:s?.device_id||void 0})}if(o){const t=new Set(p.map(t=>t.value));for(const[s,i]of Object.entries(e?.entities||{})){const o=`entity:${s}`;if(c.has(o)||t.has(o)||i?.hidden&&!h.has(o))continue;const a=!!i?.device_id&&l.devices.has(i.device_id);if(S(e,s,l)&&!h.has(o)&&!a)continue;const n=e?.states?.[s],d=i?.device_id?e?.devices?.[i.device_id]:null,_=d?String(d.name_by_user||d.name||""):"";p.push({value:o,label:i?.name||n?.attributes?.friendly_name||s,sub:`${s.split(".")[0]} · ${r.entity}${_?` · ${_}`:""}`,kind:"entity",ref:s,areaId:i?.area_id||d?.area_id||"",model:"",parentDeviceId:i?.device_id||void 0})}}return p.sort((t,e)=>t.label.localeCompare(e.label)||t.value.localeCompare(e.value))}const da={kind:"active",enabledEntityIds:[],allEntityIds:[]};function pa(t){return t.startsWith("entity:")?"entity":"device"}function _a(t){const e=t.binding.indexOf(":"),s=e>=0?t.binding.slice(e+1):"",i=Object.keys(t).filter(t=>!["id","binding","hidden"].includes(t));return t.id===`h${s}`&&0===i.length}const ua=.001;function ma(t){return!!t&&t.length>=2&&Number.isFinite(t[0])&&Number.isFinite(t[1])}function ga(t){return`${t[0].toFixed(6)},${t[1].toFixed(6)}`}function fa(t,e,s){return Math.abs(t[0]-e[0])"room"!==t.kind&&s.some(s=>s!==t&&function(t,e,s){const i=t.b[0]-t.a[0],o=t.b[1]-t.a[1],a=Math.hypot(i,o);if(!(a>s))return!1;const n=i/a,r=o/a,l=e=>Math.abs((e[0]-t.a[0])*r-(e[1]-t.a[1])*n);if(l(e.a)>s||l(e.b)>s)return!1;const h=(e.a[0]-t.a[0])*n+(e.a[1]-t.a[1])*r,c=(e.b[0]-t.a[0])*n+(e.b[1]-t.a[1])*r;return Math.min(a,Math.max(h,c))-Math.max(0,Math.min(h,c))>s}(t,s,e))),o=i.map(t=>{const[e,s]=ba(t.a,t.b);return{a:e,b:s,key:`hidden|${va(t,e,s)}`,sourceKind:t.kind,sourceId:t.id}}).sort((t,e)=>t.key.localeCompare(e.key)),a=i.flatMap(t=>[t.a,t.b].map((e,s)=>({point:[e[0],e[1]],key:`hidden|${ya(t)}|endpoint-${s}`,sourceKind:t.kind,sourceId:t.id}))).sort((t,e)=>t.key.localeCompare(e.key));return{segments:o,endpoints:a}}function xa(t){const e=t.epsilon??ua,s=t.roomCuts||[],i=new Map;for(const e of t.partitionCuts||[]){if("string"!=typeof e.hostId||!e.hostId||!ma(e.a)||!ma(e.b))continue;const t=i.get(e.hostId)||[];t.push([e.a[0],e.a[1],e.b[0],e.b[1]]),i.set(e.hostId,t)}const o=[];for(const[e,i]of M(t.space.rooms).entries())i.length<4||o.push({a:[i[0],i[1]],b:[i[2],i[3]],kind:"room",id:`room-edge-${e}`,cuts:s});for(const e of t.space.room_drafts||[])if(e.id!==t.activeDraftId)for(let t=0;t+1wa(s,t,e)))continue;const t=ga(s);n.has(t)||n.set(t,{point:[s[0],s[1]],key:t})}}}return{segments:[...a.values()].sort((t,e)=>t.key.localeCompare(e.key)),endpoints:[...n.values()].sort((t,e)=>t.key.localeCompare(e.key))}}function Sa(t,e,s){return e.some(e=>ma(e)&&fa(t,e,s))}function Ma(t,e,s){if(!s)return!0;const i=t-s.distance;return i<-1e-9||Math.abs(i)<=1e-9&&e.localeCompare(s.key)<0}function Da(t,e,s){const i=s.epsilon??ua,o=s.excludePoints||[],a=[...t.endpoints,...(s.extraEndpoints||[]).filter(t=>ma(t.point)).map(t=>({point:[t.point[0],t.point[1]],key:t.key}))],n=new Map;for(const t of a){if(Sa(t.point,o,i))continue;const e=ga(t.point),s=n.get(e);(!s||t.key.localeCompare(s.key)<0)&&n.set(e,t)}return[...n.values()].map(t=>({kind:"endpoint",point:[...t.point],key:t.key,distance:Math.hypot(e[0]-t.point[0],e[1]-t.point[1])})).filter(t=>t.distance<=s.tolerance).sort((t,e)=>t.distance-e.distance||t.key.localeCompare(e.key))}function Ca(t,e){if(!t.length)return null;if(e>0){const s=new Set;for(let i=0;i1)return{kind:"ambiguous",candidate:null,conflicts:t.filter(t=>s.has(t.key)).map(t=>({point:[...t.point],key:t.key}))}}return{kind:"resolved",candidate:t[0],conflicts:[]}}function Pa(t,e,s){const i=t.b[0]-t.a[0],o=t.b[1]-t.a[1],a=Math.hypot(i,o);if(!(a>0))return[...t.a];const n=i/a,r=o/a,l=Math.max(0,Math.min(a,(e[0]-t.a[0])*n+(e[1]-t.a[1])*r)),h=s>0?Math.max(0,Math.min(a,Math.round(l/s)*s)):l;return[t.a[0]+n*h,t.a[1]+r*h]}function Ia(t,e,s){if(!(ma(e)&&ma(s.anchor)&&s.tolerance>=0))return{kind:"none",candidate:null,conflicts:[]};const i=s.epsilon??ua,o=function(t,e){const s=e[0]-t[0],i=e[1]-t[1];if(!(Math.hypot(s,i)>Number.EPSILON))return null;const o=Math.round(Math.atan2(i,s)/(Math.PI/4))*(Math.PI/4);return[Math.cos(o),Math.sin(o)]}(s.anchor,e);if(!o)return{kind:"none",candidate:null,conflicts:[]};const a=Da(t,e,s).filter(t=>function(t,e,s,i){const o=t[0]-e[0],a=t[1]-e[1];return o*s[0]+a*s[1]>i&&Math.abs(o*s[1]-a*s[0])<=i*Math.max(Math.hypot(o,a),1)}(t.point,s.anchor,o,i)),n=Ca(a,s.distinguishTolerance||0);if(n)return n;let r=null;for(const a of t.segments){const t=a.b[0]-a.a[0],n=a.b[1]-a.a[1],l=a.a[0]-s.anchor[0],h=a.a[1]-s.anchor[1],c=o[0]*n-o[1]*t;let d=null;if(Math.abs(c)<=i*Math.max(Math.hypot(t,n),1)){if(Math.abs(l*o[1]-h*o[0])>i*Math.max(Math.hypot(l,h),1))continue;const t=[a.a,a.b].map(t=>({point:t,along:(t[0]-s.anchor[0])*o[0]+(t[1]-s.anchor[1])*o[1]})).filter(t=>t.along>i).sort((t,e)=>t.along-e.along);t.length&&(d=[...t[0].point])}else{const e=(l*n-h*t)/c,a=(l*o[1]-h*o[0])/c;e>i&&a>=-i&&a<=1+i&&(d=[s.anchor[0]+o[0]*e,s.anchor[1]+o[1]*e])}if(!d)continue;const p=Math.hypot(e[0]-d[0],e[1]-d[1]);p>s.tolerance||Sa(d,s.excludePoints||[],i)||!Ma(p,a.key,r)||(r={kind:"line",point:d,key:a.key,distance:p,segment:a})}return r?{kind:"resolved",candidate:r,conflicts:[]}:{kind:"none",candidate:null,conflicts:[]}}const Ta=.001;function Ra(t,e,s,i){const o=Number.isFinite(t)&&t>0?Math.floor(t):0,a=t=>"number"==typeof t&&Number.isFinite(t)&&t>=0?t:null,n=a(s),r=a(i)??0,l=[];let h=null;for(let t=0;t=2&&Number.isFinite(t[0])&&Number.isFinite(t[1])}function za(t,e,s,i){return t*i-e*s}function Aa(t){let e=0;for(let s=0;st.localeCompare(e)),e[0]}function Ha(t,e,s){const i=[...t];for(let t=!0;t&&i.length>=3;){t=!1;for(let o=0;o=0){i.splice(o,1),t=!0;break}}}return i}function Ga(t,e){const s=t.map(t=>Math.max(0,Math.min(1,t))).sort((t,e)=>t-e),i=[];for(const t of s)(!i.length||Math.abs(t-i[i.length-1])>e)&&i.push(t);return i}function Wa(t,e){return[t.a[0]+(t.b[0]-t.a[0])*e,t.a[1]+(t.b[1]-t.a[1])*e]}function ja(t,e,s){const i=function(t,e){const s=e.b[0]-e.a[0],i=e.b[1]-e.a[1],o=s*s+i*i;return o>0?((t[0]-e.a[0])*s+(t[1]-e.a[1])*i)/o:0}(t,e);if(i<-s||i>1+s)return null;const o=Wa(e,i);return Math.hypot(o[0]-t[0],o[1]-t[1])<=s?Math.max(0,Math.min(1,i)):null}function qa(t,e,s,i,o){const a=t.b[0]-t.a[0],n=t.b[1]-t.a[1],r=e.b[0]-e.a[0],l=e.b[1]-e.a[1],h=e.a[0]-t.a[0],c=e.a[1]-t.a[1],d=za(a,n,r,l),p=Math.max(Math.hypot(a,n),Math.hypot(r,l),1);if(Math.abs(d)>o*p){const t=za(h,c,r,l)/d,e=za(h,c,a,n)/d;return void(t>=-o&&t<=1+o&&e>=-o&&e<=1+o&&(s.push(Math.max(0,Math.min(1,t))),i.push(Math.max(0,Math.min(1,e)))))}if(!(Math.abs(za(h,c,a,n))>o*p)){for(const i of[e.a,e.b]){const e=ja(i,t,o);null!=e&&s.push(e)}for(const s of[t.a,t.b]){const t=ja(s,e,o);null!=t&&i.push(t)}}}function Ua(t){let e=t+1|0;return e^=e<<13,e^=e>>>17,e^=e<<5,e>>>0}function Ka(t,e){return t.minY-e.minY||t.index-e.index}function Va(t){return t.subtreeMaxY=Math.max(t.item.maxY,t.left?.subtreeMaxY??-1/0,t.right?.subtreeMaxY??-1/0),t}function Ja(t){const e=t.right;return t.right=e.left,e.left=Va(t),Va(e)}function Ya(t){const e=t.left;return t.left=e.right,e.right=Va(t),Va(e)}function Xa(t,e){return t?(Ka(e,t.item)<0?(t.left=Xa(t.left,e),t.left.priority0)t.right=Za(t.right,e);else{if(!t.left)return t.right;if(!t.right)return t.left;t.left.priority=e&&Qa(t.left,e,s,i),t.item.minY<=s&&t.item.maxY>=e&&i.push(t.item),t.item.minY<=s&&Qa(t.right,e,s,i))}function tn(t,e=.001){const s=Number.isFinite(e)&&e>0?e:Ta,i=t.filter(t=>Na(t.a)&&Na(t.b)&&"string"==typeof t.key&&t.key.length>0&&Math.hypot(t.b[0]-t.a[0],t.b[1]-t.a[1])>s),o=i.map(()=>[0,1]);!function(t,e,s){const i=t.map((t,e)=>({index:e,minX:Math.min(t.a[0],t.b[0])-s,maxX:Math.max(t.a[0],t.b[0])+s,minY:Math.min(t.a[1],t.b[1])-s,maxY:Math.max(t.a[1],t.b[1])+s})),o=[...i].sort((t,e)=>t.minX-e.minX||t.minY-e.minY||t.index-e.index),a=[...i].sort((t,e)=>t.maxX-e.maxX||t.index-e.index),n=new Set;let r=null,l=0;for(const i of o){for(;lt.index-e.index);for(const a of o)qa(t[a.index],t[i.index],e[a.index],e[i.index],s);n.add(i.index),r=Xa(r,i)}}(i,o,s);const a=new Map;for(let t=0;t({key:t,a:e.a,b:e.b,sourceKeys:[...e.sourceKeys].sort((t,e)=>t.localeCompare(e))})).sort((t,e)=>t.key.localeCompare(e.key))}function en(t,e=.001){const s=Number.isFinite(e)&&e>0?e:Ta,i=tn(t,s),o=new Map,a=new Map,n=new Map(i.map(t=>[t.key,t]));for(const t of i){const e=La(t.a,s),i=La(t.b,s);o.set(e,t.a),o.set(i,t.b),a.has(e)||a.set(e,new Set),a.has(i)||a.set(i,new Set),a.get(e).add(i),a.get(i).add(e)}const r=new Map;for(const[t,e]of a){const s=o.get(t);r.set(t,[...e].sort((t,e)=>{const i=o.get(t),a=o.get(e);return Math.atan2(i[1]-s[1],i[0]-s[0])-Math.atan2(a[1]-s[1],a[0]-s[0])||t.localeCompare(e)}))}const l=new Set,h=new Map,c=(t,e)=>`${t}>${e}`;for(const t of i){const e=[La(t.a,s),La(t.b,s)];for(const[t,a]of[e,[e[1],e[0]]]){if(l.has(c(t,a)))continue;const e=[],d=[];let p=t,_=a,u=!1;for(let s=0;s<=2*i.length+2;s++){const s=c(p,_);if(l.has(s)){u=p===t&&_===a;break}l.add(s),e.push(p),d.push(Ea(p,_));const i=r.get(_)||[],o=i.indexOf(p);if(o<0||!i.length)break;if(p=_,_=i[(o-1+i.length)%i.length],p===t&&_===a){u=!0;break}}if(!u||new Set(e).size<3||new Set(e).size!==e.length)continue;const m=e.map(t=>o.get(t)),g=Aa(m);if(!(g>s*s))continue;const f=Ba(Ha(e,o,s)),b=new Set;for(const t of d)for(const e of n.get(t)?.sourceKeys||[])b.add(e);const y={ring:m,key:f,area:g,atomKeys:[...d],sourceKeys:[...b].sort((t,e)=>t.localeCompare(e))};h.has(f)||h.set(f,y)}}return{atoms:i,faces:[...h.values()].sort((t,e)=>t.area-e.area||t.key.localeCompare(e.key))}}function sn(t,e,s=.001){if(!Na(e))return null;const i=t.faces.filter(t=>{if(t.ring.some((i,o)=>function(t,e,s,i){const o=s[0]-e[0],a=s[1]-e[1],n=o*o+a*a;if(!(n>0))return Math.hypot(t[0]-e[0],t[1]-e[1])<=i;const r=((t[0]-e[0])*o+(t[1]-e[1])*a)/n;return!(r<0||r>1)&&Math.hypot(e[0]+o*r-t[0],e[1]+a*r-t[1])<=i}(e,i,t.ring[(o+1)%t.ring.length],s)))return!1;let i=!1;for(let s=0,o=t.ring.length-1;se[1]!=n[1]>e[1]&&e[0]<(n[0]-a[0])*(e[1]-a[1])/(n[1]-a[1])+a[0]&&(i=!i)}return i});return[...i].sort((t,e)=>t.area-e.area||t.key.localeCompare(e.key))[0]||null}function on(t,e,s){const i=s[0]-e[0],o=s[1]-e[1],a=i*i+o*o,n=a>0?Math.max(0,Math.min(1,((t[0]-e[0])*i+(t[1]-e[1])*o)/a)):0;return Math.hypot(e[0]+i*n-t[0],e[1]+o*n-t[1])}function an(t,e,s=.5){let i=Math.abs(t-e)%180;return i>90&&(i=180-i),i<=s}function nn(t,e,s,i){const o=t.filter(t=>"outer"===t.kind&&!t.open&&t.cm>0).map(t=>({interval:t,reusePartitionId:e.find(e=>function(t,e,s){return!(Math.abs(t.cm-e.cm)>1e-6)&&(an(180*Math.atan2(t.b[1]-t.a[1],t.b[0]-t.a[0])/Math.PI,180*Math.atan2(e.b[1]-e.a[1],e.b[0]-e.a[0])/Math.PI,.001)&&on(e.a,t.a,t.b)<=s&&on(e.b,t.a,t.b)<=s)}(e,t,i))?.id||null})),a=new Map;for(const e of s){if("partition"===e.host?.kind)continue;const s=t.map(t=>{const s=180*Math.atan2(t.b[1]-t.a[1],t.b[0]-t.a[0])/Math.PI;return{interval:t,distance:an(e.angle,s)?on([e.x,e.y],t.a,t.b):1/0}}).filter(t=>t.distance<=i).sort((t,e)=>t.distance-e.distance||t.interval.key.localeCompare(e.interval.key)),o=s[0]?.interval;"outer"===o?.kind&&!o.open&&o.cm>0&&a.set(e.id,o.key)}return{materialize:o,openingIntervals:a,removeOpeningIds:[...a.keys()].sort((t,e)=>t.localeCompare(e))}}function rn(t,e){const s=e.b[0]-e.a[0],i=e.b[1]-e.a[1],o=s*s+i*i;return o>0?Math.max(0,Math.min(1,((t[0]-e.a[0])*s+(t[1]-e.a[1])*i)/o)):0}const ln=t=>!t.key.startsWith("static:room|");function hn(t,e){const s=e.b[0]-e.a[0],i=e.b[1]-e.a[1],o=s*s+i*i;if(!(o>0))return null;const a=((t[0]-e.a[0])*s+(t[1]-e.a[1])*i)/o,n=[e.a[0]+s*a,e.a[1]+i*a];return{point:n,t:a,distance:Math.hypot(n[0]-t[0],n[1]-t[1])}}function cn(t,e){return e>0?Math.hypot(t[0]-Math.round(t[0]/e)*e,t[1]-Math.round(t[1]/e)*e):0}function dn(t,e,s){if(ln(t.source)!==ln(e.source))return ln(t.source)?t:e;const i=cn(t.point,s),o=cn(e.point,s);return Math.abs(i-o)>1e-9?i>o?t:e:function(t,e){return t[0]-e[0]||t[1]-e[1]}(t.point,e.point)>0?t:e}function pn(t){return[t.from[0],t.from[1],t.to[0],t.to[1],t.targetKind].map(t=>"number"==typeof t?t.toFixed(6):t).join("|")}function _n(t,e){return t.map(t=>t.key!==e.sourceKey?t:{...t,[e.endpoint]:[...e.to]})}function un(t,e){const s=e.epsilon??.001;if(!(e.maxDistance>=0)||!e.point&&!e.requiredSourceKey)return{kind:"none"};const i=new Map,o=t.flatMap(t=>[{source:t,endpoint:"a",point:t.a},{source:t,endpoint:"b",point:t.b}]);for(let t=0;ts)||l>e.maxDistance)continue;const h=dn(n,r,e.gridStep||0),c=h===n?r:n;if(!ln(h.source))continue;const d={sourceKey:h.source.key,endpoint:h.endpoint,from:[h.point[0],h.point[1]],to:[c.point[0],c.point[1]],targetSourceKey:c.source.key,targetKind:"endpoint",distance:l};i.set(pn(d),d)}for(const a of o)if(ln(a.source))for(const o of t){if(o===a.source)continue;const t=hn(a.point,o);if(!t||t.t<=s||t.t>=1-s||!(t.distance>s)||t.distance>e.maxDistance)continue;const n={sourceKey:a.source.key,endpoint:a.endpoint,from:[a.point[0],a.point[1]],to:t.point,targetSourceKey:o.key,targetKind:"line",distance:t.distance};i.set(pn(n),n)}const a=[];for(const o of i.values()){const i=en(_n(t,o),s),n=e.point?sn(i,e.point,s):i.faces.find(t=>!e.requiredSourceKey||t.sourceKeys.includes(e.requiredSourceKey))||null;n&&a.push({proposal:o,face:n})}if(a.sort((t,e)=>t.proposal.distance-e.proposal.distance||pn(t.proposal).localeCompare(pn(e.proposal))),!a.length)return{kind:"none"};return new Set(a.map(t=>`${pn(t.proposal)}|${t.face.key}`)).size>1?{kind:"ambiguous",proposals:a.map(t=>t.proposal)}:{kind:"repair",...a[0]}}const mn=(t,e)=>(t.composedPath?.()||[t.target]).some(t=>(t=>!!t&&"function"==typeof t.matches)(t)&&e(t));class gn{constructor(t){this.host=t,this._openGroupId=null,this._groupGeneration=0,this._renderedContext="",this._focusOwned=!1,this._currentModel=null,this._blocked=!1,this._dismissPointerTarget=null,this._globalDismissListening=!1,this._globalDismissGuard=t=>{this.handleOutsideDismiss(t)}}get hasOpenGroup(){return null!==this._openGroupId}get groupGeneration(){return this._groupGeneration}activeGroup(t){return this._openGroupId&&t.find(t=>t.id===this._openGroupId)||null}reset(){this._openGroupId=null,this._contentAnimation?.cancel(),this._contentAnimation=void 0,this._renderedContext="",this._focusOwned=!1,this._currentModel=null,this._blocked=!1,this._dismissPointerTarget=null,clearTimeout(this._dismissTimer),this._dismissTimer=void 0,this._syncGlobalDismissListener()}closeForNavigation(){const t="palette"===this._currentModel?.kind?this._currentModel.dismiss:void 0,e=!!this._openGroupId||!!t;this._openGroupId=null,t?.(),t&&(this._currentModel=null),this._clearDismissClick(),e&&this.host.requestUpdate(),this._syncGlobalDismissListener()}openPalette(){this._openGroupId&&this.closeGroup(!1)}toggleGroup(t,e){this._openGroupId!==e?t.some(t=>t.id===e)&&("palette"===this._currentModel?.kind&&this._currentModel.dismiss?.(),this._groupGeneration+=1,this._openGroupId=e,this.host.clearTip(),this.host.requestUpdate(),this._syncGlobalDismissListener()):this.closeGroup(!1)}closeGroup(t){const e=this._openGroupId;this._openGroupId=null,this.host.requestUpdate(),this._syncGlobalDismissListener(),t&&e&&this.host.updateComplete().then(()=>{const t=this.host.root(),s=[...t.querySelectorAll("[data-editor-group]")].find(t=>t.dataset.editorGroup===e&&null!==t.offsetParent);(s||t.querySelector(".editbar-tools"))?.focus?.()})}renderGroupLauncher(t,e,s){const i=this._openGroupId===t.id,a=t.items.find(e=>e.id===t.activeItemId&&!e.disabled);return o``}renderGroupModel(t,e,s){const i=t.items.find(t=>!t.disabled)?.id,n=t.items.some(e=>e.id===t.activeItemId&&!e.disabled)?t.activeItemId:void 0;return{contextId:e,kind:"group",ariaLabel:s.openGroup(t.label),visibleLabel:t.label,content:o`
this._groupKeydown(t)}> + ${t.items.map(e=>o``)} +
`}}runContext(t,e,s){t===e&&s()}render(t,e){this._currentModel=t,this._blocked=e;const s=t?.kind||"hidden";return o`
+ +
`}afterRender(){const t=this.host.root();this._blocked&&(this._openGroupId?this.closeGroup(!1):"palette"===this._currentModel?.kind&&this._currentModel.dismiss?.()),this._syncGlobalDismissListener();const e=t.querySelector(".editor-secondary"),s=this._renderedContext,i=e?.dataset.contextId||"";if(i&&s&&i!==s&&!window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches&&(this._contentAnimation?.cancel(),this._contentAnimation=e?.querySelector(".editor-secondary-content")?.animate([{opacity:.35,transform:"translateY(-4px)"},{opacity:1,transform:"translateY(0)"}],{duration:100,easing:"ease-out"})),this._renderedContext=i,s&&i!==s&&this._focusOwned){const s=i?e?.querySelector('[tabindex="0"], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled])'):t.querySelector(".editbar-tools");s?.focus?.(),this._focusOwned=!!i}if("palette"===this._currentModel?.kind&&this._currentModel.launcherId){const e=[...t.querySelectorAll("[data-editor-palette]")].find(t=>t.dataset.editorPalette===this._currentModel?.launcherId);e&&null!==e.offsetParent||this._currentModel.dismiss?.()}if(!this._openGroupId)return;const o=[...t.querySelectorAll("[data-editor-group]")].find(t=>t.dataset.editorGroup===this._openGroupId);o&&null!==o.offsetParent||this.closeGroup(!0)}handleOutsideDismiss(t){if(mn(t,t=>t.hasAttribute("data-editor-navigation")))return"pointerdown"===t.type?this.closeForNavigation():"click"===t.type&&this._dismissPointerTarget&&this._clearDismissClick(),!1;if("click"===t.type&&this._dismissPointerTarget){return!!(t.composedPath?.()||[t.target]).includes(this._dismissPointerTarget)&&(this._clearDismissClick(),t.preventDefault(),t.stopImmediatePropagation(),!0)}const e=this._openGroupId||this._blocked||"palette"!==this._currentModel?.kind?null:this._currentModel;if("pointerdown"===t.type&&(this._openGroupId||e)){const s=mn(t,t=>t.classList.contains("editor-secondary")),i=mn(t,t=>!!t.dataset.editorGroup),o=mn(t,t=>!!t.dataset.editorPalette),a=mn(t,t=>t.classList.contains("stage"));if(!(s||i||o||"stay-open-on-canvas"===e?.dismissPolicy&&a))return t.preventDefault(),t.stopImmediatePropagation(),this._dismissPointerTarget=t.target,clearTimeout(this._dismissTimer),this._dismissTimer=window.setTimeout(()=>{this._clearDismissClick()},800),this._openGroupId?this.closeGroup(!1):e?.dismiss?.(),this._syncGlobalDismissListener(),!0}return!1}_activateGroupItem(t,e,s){if(t.id!==this._openGroupId)return;const i=t.items.find(t=>t.id===e);if(!i||i.disabled)return;const o="toggle"===i.role&&"stay-open"===i.closePolicy,a=0===s.detail;o||(this._openGroupId=null,this.host.requestUpdate(),this._syncGlobalDismissListener()),i.invoke(),!o&&a&&this.host.updateComplete().then(()=>{const e=[...this.host.root().querySelectorAll("[data-editor-group]")].find(e=>e.dataset.editorGroup===t.id&&null!==e.offsetParent);e?.focus()})}_groupKeydown(t){if(!["ArrowLeft","ArrowRight","Home","End"].includes(t.key))return;const e=[...t.currentTarget.querySelectorAll(".editor-group-item:not([disabled])")];if(!e.length)return;t.preventDefault();const s=e.indexOf(this.host.root().activeElement),i="Home"===t.key?0:"End"===t.key?e.length-1:"ArrowLeft"===t.key?s<=0?e.length-1:s-1:(s+1)%e.length;e[i]?.focus()}_clearDismissClick(){this._dismissPointerTarget=null,clearTimeout(this._dismissTimer),this._dismissTimer=void 0,this._syncGlobalDismissListener()}_syncGlobalDismissListener(){const t=!this._blocked&&"palette"===this._currentModel?.kind,e=!!this._openGroupId||t||!!this._dismissPointerTarget;e!==this._globalDismissListening&&(this._globalDismissListening=e,e?(window.addEventListener("pointerdown",this._globalDismissGuard,!0),window.addEventListener("click",this._globalDismissGuard,!0)):(window.removeEventListener("pointerdown",this._globalDismissGuard,!0),window.removeEventListener("click",this._globalDismissGuard,!0)))}}const fn=t=>t.stopPropagation(),bn=1e-6*I,yn=6*I;function vn(t){return Number.isFinite(t)?Math.round(t/I)*I:t}const $n=(t,e,s,i)=>Math.hypot(s-t,i-e),wn=(t,e,s,i,o,a,n,r)=>Math.hypot(Math.max(Math.abs(o-t),Math.abs(o+n-(t+s))),Math.max(Math.abs(a-e),Math.abs(a+r-(e+i)))),kn=(t,e,s,i,o,a,n)=>{const r=Math.max(Number(i)||0,0)/2,l=Math.PI/180,h=Math.cos(s*l)*r,c=Math.sin(s*l)*r,d=Math.cos(n*l)*r,p=Math.sin(n*l)*r,_=Math.max($n(t+h,e+c,o+d,a+p),$n(t-h,e-c,o-d,a-p)),u=Math.max($n(t+h,e+c,o-d,a-p),$n(t-h,e-c,o+d,a+p));return Math.min(_,u)},xn=t=>{const e=Number(t?.cell_cm);return e>0?e:5};function Sn(t,e){const s=JSON.parse(JSON.stringify(t||[])),i=JSON.parse(JSON.stringify(e||{}));let o=0,a=0,n=0,r=0,l="",h=0,c=0,d=0;const p=(t,e)=>{Number.isFinite(t)&&Number.isFinite(e)&&t!==e&&Math.abs(e-t)<=bn&&c++},_=(t,e)=>{p(t[0],e[0]),p(t[1],e[1])},u={};let m=5;for(const t of s){const e=xn(t);null!=t?.id&&(u[String(t.id)]=e),e>m&&(m=e)}const g=(t,e,s,i=!1)=>{if(!(t>bn||i))return;o++,t>n&&(n=t);const a=t*T*e;a>r&&(r=a,l=s)};for(const t of s){const e=xn(t),s=null!=t?.id?String(t.id):"";for(const i of t.rooms||[]){a++;let t=0;if(i.poly?.length)i.poly=i.poly.map(e=>{const s=[vn(e[0]),vn(e[1])];return t=Math.max(t,$n(e[0],e[1],s[0],s[1])),_(e,s),s});else if(null!=i.x&&null!=i.y){const e=i.x,s=i.y,o=i.w||0,a=i.h||0,n=vn(e+o),r=vn(s+a),l=vn(e),h=vn(s),c=Math.max(I,n-l),d=Math.max(I,r-h);t=wn(e,s,o,a,l,h,c,d),i.x=l,i.y=h,i.w=c,i.h=d,p(e,l),p(s,h),p(e+o,l+c),p(s+a,h+d)}g(t,e,s)}for(const i of t.room_drafts||[]){a++;let t=0;const o=i.points||[],n=o.map(e=>{const s=[vn(e[0]),vn(e[1])];return t=Math.max(t,$n(e[0],e[1],s[0],s[1])),s}),r=n.length?[n[0]]:[],l=n.length?[[o[0],n[0]]]:[],h=[];for(let t=0;t+1=2)for(const[t,e]of l)_(t,e);g(t,e,s)}if(Array.isArray(t.room_drafts)){const e=t.room_drafts.length;t.room_drafts=t.room_drafts.filter(t=>t.points?.length>=2),d+=e-t.room_drafts.length,t.room_drafts.length||delete t.room_drafts}for(const i of t.partitions||[]){a++;const o=[i.a[0],i.a[1]],n=[i.b[0],i.b[1]],r=[vn(o[0]),vn(o[1])],l=[vn(n[0]),vn(n[1])];let h=Math.max($n(o[0],o[1],r[0],r[1]),$n(n[0],n[1],l[0],l[1]));const c=$n(r[0],r[1],l[0],l[1]),d=(t.openings||[]).filter(t=>"partition"===t.host?.kind&&t.host.id===i.id).every(t=>{const e=Number(t.length),s=Number(t.host.t),i=s*c;return Number.isFinite(e)&&e>0&&Number.isFinite(s)&&s>=0&&s<=1&&i-e/2>=-bn&&i+e/2<=c+bn});c>bn&&d?(i.a=r,i.b=l,_(o,r),_(n,l)):h=0,g(h,e,s)}for(const i of t.wall_columns||[]){a++;const t=[i.center[0],i.center[1]],o=[vn(t[0]),vn(t[1])],n=$n(t[0],t[1],o[0],o[1]);i.center=o,_(t,o),g(n,e,s)}for(const i of t.decor||[]){a++;let t=0;if("line"===i.kind){const e=[i.x1,i.y1],s=[i.x2,i.y2],o=[vn(e[0]),vn(e[1])],a=[vn(s[0]),vn(s[1])];t=Math.max($n(e[0],e[1],o[0],o[1]),$n(s[0],s[1],a[0],a[1])),i.x1=o[0],i.y1=o[1],i.x2=a[0],i.y2=a[1],_(e,o),_(s,a)}else{const e=i.x,s=i.y,o=i.w,a=i.h,n=vn(e),r=vn(s);if(null!=i.w&&null!=i.h){const l=vn(e+o),h=vn(s+a),c=Math.max(I,l-n),d=Math.max(I,h-r);t=wn(e,s,o,a,n,r,c,d),i.w=c,i.h=d,p(e+o,n+c),p(s+a,r+d)}else t=$n(e,s,n,r);i.x=n,i.y=r,p(e,n),p(s,r)}g(t,e,s)}for(const i of t.openings||[]){if(a++,"partition"===i.host?.kind){const o=(t.partitions||[]).find(t=>t.id===i.host.id),a=Number(i.host.t);if(!o||!Number.isFinite(a)||a<0||a>1)continue;const n=o.b[0]-o.a[0],r=o.b[1]-o.a[1];if(Math.hypot(n,r)<=bn)continue;const l=o.a[0]+n*a,c=o.a[1]+r*a;let d=180*Math.atan2(r,n)/Math.PI;d>=90?d-=180:d<-90&&(d+=180);const p=Number(i.angle),_=!(Number.isFinite(p)&&p===d),u=kn(i.x,i.y,Number.isFinite(p)?p:d,Number(i.length)||0,l,c,d);i.x=l,i.y=c,i.angle=d,_&&h++,g(u,e,s,_);continue}const o=P([i.x,i.y],t.rooms||[],yn,{step:I,length:Number(i.length)||0});if(!o)continue;const n=Number(i.angle),r=!(Number.isFinite(n)&&n===o.angle),l=kn(i.x,i.y,Number.isFinite(n)?n:o.angle,Number(i.length)||0,o.x,o.y,o.angle);i.x=o.x,i.y=o.y,i.angle=o.angle,r&&h++,g(l,e,s,r)}}for(const[t,e]of Object.entries(i)){if(!e||"object"!=typeof e)continue;const s=e;if("number"!=typeof s.x||"number"!=typeof s.y)continue;a++;const o=vn(s.x),n=vn(s.y),r=$n(s.x,s.y,o,n);i[t]={...s,x:o,y:n},p(s.x,o),p(s.y,n);const l="string"==typeof s.s?s.s:"";g(r,u[l]??m,l)}return{spaces:s,layout:i,report:{moved:o,coordsCanonicalized:c,total:a,maxShift:n,maxShiftCm:r,maxSpace:l,rotated:h,removedDrafts:d},changed:o>0||c>0}}function Mn(t,e){const s=e.base+t*e.span;return Math.min(1,Math.max(0,s))}const Dn=t=>Array.isArray(t)&&t.length>=2&&Number.isFinite(t[0])&&Number.isFinite(t[1]),Cn=(t,e)=>Math.hypot(t[0]-e[0],t[1]-e[1]);function Pn(t,e,s){const i=s[0]-e[0],o=s[1]-e[1],a=i*i+o*o;if(a<=0)return Cn(t,e);let n=((t[0]-e[0])*i+(t[1]-e[1])*o)/a;return n=Math.max(0,Math.min(1,n)),Math.hypot(t[0]-(e[0]+i*n),t[1]-(e[1]+o*n))}function In(t,e,s,i,o){for(const i of e)if(i&&!s.has(i.id)&&Dn(i.a)&&Dn(i.b)&&(Cn(t,i.a)<=o||Cn(t,i.b)<=o))return!0;for(const e of i?.roomPolygons||[])for(let s=0;s0&&r>0))return null;if(Math.abs(o[0]/n*(a[1]/r)-o[1]/n*(a[0]/r))>s)return null;for(const[s,o]of[[t.a,e.a],[t.a,e.b],[t.b,e.a],[t.b,e.b]])if(Cn(s,o)<=i)return[(s[0]+o[0])/2,(s[1]+o[1])/2];return null}function Rn(t,e){const s=.05*(Number(e.pitch)>0?Number(e.pitch):1),i=new Map;let o=(t||[]).filter(t=>!!t&&"string"==typeof t.id&&Dn(t.a)&&Dn(t.b));if(o.length<2)return{partitions:[...t||[]],merged:0,openingMoves:[]};const a=e.seedIds&&e.seedIds.length?new Set(e.seedIds):null;let n=0;for(let t=0;tCn(e,t.at)>s);let c=h[0]??r.a,d=h[h.length-1]??l.b;if(h.length<2&&(c=r.a,d=l.b),d[0]0?(d[0]-c[0])/_:0,m=_>0?(d[1]-c[1])/_:0,g=t=>_>0?((t[0]-c[0])*u+(t[1]-c[1])*m)/_:0;for(const t of[r,l]){const e=g(t.a),s=g(t.b)-e;for(const[o,a]of i)a.toId===t.id&&i.set(o,{fromId:a.fromId,toId:p.id,base:e+a.base*s,span:a.span*s});i.set(t.id,{fromId:t.id,toId:p.id,base:e,span:s})}a&&a.add(p.id),o=o.filter((e,s)=>s!==t.i&&s!==t.j),o.splice(t.i,0,p),n++}return{partitions:o,merged:n,openingMoves:[...i.values()]}}function Fn(t,e,s,i){if(!t?.length||!s.length)return 0;const o=new Map(s.map(t=>[t.fromId,t]));let a=0;for(const s of t){const t=s?.host;if(!t||"partition"!==t.kind)continue;const n=o.get(t.id);if(!n)continue;s.host={...t,id:n.toId,t:Mn(t.t,n)},a++;const r=F(s,e,i.coordScale,i.cellCm,i.gridPitch).resolved;r&&Object.assign(s,N(s,r,i.coordScale))}return a}function Nn(t,e){const s=e?.excludeDraftId||null;return{roomPolygons:(t?.rooms||[]).map(t=>R(t)).filter(t=>!!t),columns:t?.wall_columns||[],draftEnds:(t?.room_drafts||[]).flatMap(t=>{if(s&&t?.id===s)return[];const e=t?.points||[];return e.length?[e[0],e[e.length-1]]:[]})}}const zn=t=>!!t&&t.length>=2&&Number.isFinite(t[0])&&Number.isFinite(t[1]),An=(t,e)=>t[0]-e[0]||t[1]-e[1],On=(t,e,s,i)=>[t[0]+e*i,t[1]+s*i],Ln=(t,e,s,i)=>(t[0]-e[0])*s+(t[1]-e[1])*i,En=(t,e,s,i,o)=>{const a=e[0]-t[0],n=e[1]-t[1],r=Math.hypot(a,n);if(!(r>o))return null;const l=a/r,h=n/r,c=e=>Math.abs((e[0]-t[0])*h-(e[1]-t[1])*l);if(c(s)>o||c(i)>o)return null;const d=Ln(s,t,l,h),p=Ln(i,t,l,h),_=Math.max(0,Math.min(d,p)),u=Math.min(r,Math.max(d,p));return u-_>o?{lo:_,hi:u}:null},Bn=(t,e,s)=>Math.min(t.hi,e.hi)-Math.max(t.lo,e.lo)>s,Hn=t=>{const e=new Set(["id","a","b","cm"]);return Object.keys(t||{}).every(t=>e.has(t))},Gn=(t,e,s,i)=>{const o=t.b[0]-t.a[0],a=t.b[1]-t.a[1],n=Math.hypot(o,a);if(!(n>i))return!0;const r=o/n,l=a/n,h=j(t.cm,s.cellCm,s.gridPitch)/2;return e.some(e=>{const o=e?.center;if(!zn(o))return!0;const a=Ln(o,t.a,r,l),c=Math.abs((o[0]-t.a[0])*l-(o[1]-t.a[1])*r),d=j(Number(e.cm),s.cellCm,s.gridPitch)*("square"===e.shape?Math.SQRT1_2:.5);return!(Number.isFinite(d)&&d>0)||a>=-d-i&&a<=n+d+i&&c<=h+d+i})},Wn=(t,e,s)=>{if(t.host){const i=F(t,e,s.coordScale,s.cellCm,s.gridPitch).resolved;return i?{center:i.center,angle:i.angle,length:i.length}:null}const i=Number(t.x)*s.coordScale,o=Number(t.y)*s.coordScale,a=Number(t.angle),n=Number(t.length)*s.coordScale;return[i,o,a,n].every(Number.isFinite)&&n>0?{center:[i,o],angle:a,length:n}:null},jn=(t,e,s)=>{const i=t.angle*Math.PI/180,o=Math.cos(i),a=Math.sin(i),n=e.angle*Math.PI/180,r=Math.cos(n),l=Math.sin(n);if(Math.abs(o*l-a*r)>1e-6)return!1;const h=e.center[0]-t.center[0],c=e.center[1]-t.center[1];if(Math.abs(h*a-c*o)>s)return!1;const d=h*o+c*a;return Math.max(-t.length/2,d-e.length/2)({...t,a:[t.a[0]*e,t.a[1]*e],b:[t.b[0]*e,t.b[1]*e]}),Un=(t,e,s,i)=>{const o=`~r-${(t=>{let e=2166136261;for(let s=0;s>>0;return e.toString(36)})(`${t}|${e[0].toFixed(9)},${e[1].toFixed(9)}|${s[0].toFixed(9)},${s[1].toFixed(9)}`)}`,a=`${t.slice(0,Math.max(1,64-o.length))}${o}`;let n=a;for(let t=2;i.has(n);t++){const e=`-${t}`;n=`${a.slice(0,64-e.length)}${e}`}return i.add(n),n},Kn=(t,e)=>{const s=[];for(const i of t){const t=s[s.length-1];!(t&&Math.abs(t.hi-i.lo)<=e&&t.safe===i.safe)||i.safe&&t.signature!==i.signature?s.push({lo:i.lo,hi:i.hi,a:i.a,b:i.b,safe:i.safe,signature:i.signature,roomIds:i.roomIds,finalCm:i.finalCm}):(t.hi=i.hi,t.b=i.b)}return s};function Vn(t,e,s,i,o){const a=s||[],n=Array.isArray(t?.partitions)?t.partitions:[],r=Array.isArray(t?.openings)?t.openings:[],l=Array.isArray(t?.room_drafts)?t.room_drafts:[],h={walls:a,partitions:n,openings:r,roomDrafts:l,partitionsReconciled:0,openingsRehosted:0,removedDrafts:0};if(!n.length&&!l.length)return h;const c=Math.max(2e-4*o.gridPitch,1e-9);let d=a,p=n,_=r;const u=z(e.rooms,d,i,o.pitch,o.cellCm,o.gridPitch,o.coordScale),m=((t,e,s,i)=>{const o=new Map(t.map(t=>[t.id,t])),a=new Set;for(const t of e){if(!o.get(t.id)||!Array.isArray(t.points)||t.points.length<2||t.segments.length!==t.points.length-1)continue;let e=!0;for(let o=0;o+10)){e=!1;break}const l=Math.hypot(n[0]-a[0],n[1]-a[1]);if(!(l>i)){e=!1;break}const h=s.flatMap(t=>{const e=t.cm>0?t.cm:A;if(t.open||"outer"!==t.kind&&"shared"!==t.kind||e+it.lo-e.lo||t.hi-e.hi);let c=0;for(const t of h){if(t.lo>c+i)break;if(c=Math.max(c,t.hi),c>=l-i)break}c!m.has(t.id));let f=0,b=0;const y=new Map(e.partitions.map(t=>[t.id,t]));for(const t of[...n].sort((t,e)=>t.id.localeCompare(e.id))){if(!p.some(e=>e.id===t.id))continue;if(!Hn(t))continue;const s=y.get(t.id);if(!(s&&zn(s.a)&&zn(s.b)&&Number.isFinite(s.cm)&&s.cm>0))continue;const[a,n]=An(s.a,s.b)<=0?[[s.a[0],s.a[1]],[s.b[0],s.b[1]]]:[[s.b[0],s.b[1]],[s.a[0],s.a[1]]],r=n[0]-a[0],l=n[1]-a[1],h=Math.hypot(r,l);if(!(h>c))continue;const u=r/h,g=l/h,v=z(e.rooms,d,i,o.pitch,o.cellCm,o.gridPitch,o.coordScale),$=v.flatMap(t=>{if(t.open||"outer"!==t.kind&&"shared"!==t.kind)return[];const e=En(a,n,t.a,t.b,c);return e?[{interval:t,range:e}]:[]});if(!$.length)continue;const w=_.filter(t=>"partition"===t.host?.kind&&t.host.id===s.id).map(t=>({opening:t,resolved:F(t,e.partitions,o.coordScale,o.cellCm,o.gridPitch).resolved}));if(w.some(t=>!t.resolved))continue;const k=[0,h];for(const{range:t}of $)k.push(t.lo,t.hi);const x=k.slice(),S=new Map;for(const{opening:t,resolved:e}of w){const s=Ln(e.center,a,u,g),i={lo:s-e.length/2,hi:s+e.length/2};S.set(t.id,i),x.push(Math.max(0,i.lo),Math.min(h,i.hi))}const M=[...new Set(x.filter(t=>Number.isFinite(t)&&t>=-c&&t<=h+c).map(t=>Math.max(0,Math.min(h,t)).toFixed(9)))].map(Number).sort((t,e)=>t-e),D=p.map(t=>qn(t,o.coordScale)).filter(t=>t.id!==s.id),C=e.room_drafts.filter(t=>!m.has(t.id)),P=[];for(let t=0;t+1c))continue;const r=On(a,u,g,i),l=On(a,u,g,n),h=$.filter(({range:t})=>t.lo<=i+c&&t.hi>=n-c).map(({interval:t})=>t),d=new Map;for(const t of h)d.set(t.roomId,t);const p=[...d.values()],_=new Set(p.map(t=>t.kind)),m=new Set(p.map(t=>t.cm>0?t.cm:A)),f=p[0]?.kind,b=[...d.keys()].sort(),y=1===_.size&&1===m.size&&("outer"===f&&1===b.length||"shared"===f&&2===b.length),v={id:s.id,a:r,b:l,cm:s.cm},w=D.some(t=>!!En(r,l,t.a,t.b,c)),k=C.some(t=>t.points.some((e,s)=>s+10?p[0].cm:A:0,I=y?Math.max(S,s.cm):s.cm;P.push({lo:i,hi:n,a:r,b:l,safe:x,signature:x?`${f}|${b.join(",")}|${I}`:"",roomIds:b,roomCm:S,finalCm:I})}if(!P.some(t=>t.safe))continue;const I=new Map;for(const{opening:t,resolved:e}of w){const i=S.get(t.id),a=k.some(t=>t>i.lo+c&&tBn(t,i,c)),r=new Set(n.filter(t=>t.safe).map(t=>t.signature));if(!a&&n.length>0&&n.every(t=>t.safe)&&1===r.size&&O(e,v,c)){I.set(t.id,{resolved:e,signature:[...r][0]});continue}const l=L(s,o.cellCm,o.gridPitch),h={lo:i.lo-l,hi:i.hi+l};for(const t of P)Bn(t,h,c)&&(t.safe=!1,t.signature="")}let T=!0;for(;T;){T=!1;for(const[t,e]of[...I.entries()]){const i=S.get(t),a=P.filter(t=>Bn(t,i,c));if(a.length>0&&a.every(t=>t.safe)&&a.every(t=>t.signature===e.signature))continue;I.delete(t);const n=L(s,o.cellCm,o.gridPitch),r={lo:i.lo-n,hi:i.hi+n};for(const t of P)Bn(t,r,c)&&(t.safe=!1,t.signature="");T=!0}}const R=Kn(P,c),j=R.filter(t=>t.safe),q=R.filter(t=>!t.safe);if(!j.length)continue;if(p.length-1+q.length>2e3)continue;let U=d;for(const t of j){const[e,i]=An(s.a,s.b)<=0?[t.a,t.b]:[t.b,t.a];U=E(U,e,i,t.finalCm,o.pitch,o.coordScale)}if(U=B(e.rooms,U,i,o.pitch,o.cellCm,o.gridPitch,o.coordScale),U.length>500)continue;const K=new Set(p.map(t=>t.id));K.delete(s.id);const V=q.map((t,e)=>{const i=[t.a[0]/o.coordScale,t.a[1]/o.coordScale],a=[t.b[0]/o.coordScale,t.b[1]/o.coordScale],n=0===e?s.id:Un(s.id,i,a,K);return K.add(n),{id:n,a:i,b:a,cm:s.cm}}),J=p.findIndex(t=>t.id===s.id),Y=p.slice();Y.splice(J,1,...V);const X=Y.map(t=>qn(t,o.coordScale)),Z=new Map;let Q=!0;for(const{opening:t,resolved:e}of w){const s=I.get(t.id);if(s){const e=N(t,s.resolved,o.coordScale),{host:i,...a}=e;Z.set(t.id,a);continue}const i=S.get(t.id),a=V[q.findIndex(t=>t.lo<=i.lo+c&&t.hi>=i.hi-c)],n=a&&X.find(t=>t.id===a.id);if(!a||!n){Q=!1;break}const r=n.b[0]-n.a[0],l=n.b[1]-n.a[1],h=Math.hypot(r,l),d=Ln(e.center,n.a,r/h,l/h),p={...N(t,e,o.coordScale),host:{kind:"partition",id:a.id,t:d/h}};if(!H(p,X,o.coordScale,o.cellCm,o.gridPitch).resolved){Q=!1;break}Z.set(t.id,p)}if(!Q)continue;const tt=_.map(t=>Z.get(t.id)||t),et=[...I.keys()].map(t=>Z.get(t)),st=et.map(t=>Wn(t,X,o));if(st.some(t=>!t))continue;let it=!1;for(let t=0;tt.id));for(const t of tt){if(ot.has(t.id))continue;const e=Wn(t,X,o);e&&st.some(t=>jn(t,e,c))&&(it=!0)}if(it)continue;const at=G(e.rooms,U,i,o.pitch,o.cellCm,o.gridPitch,o.coordScale);[...I.entries()].every(([t,e])=>{const s=Z.get(t),i=W(at,{x:s.x*o.coordScale,y:s.y*o.coordScale,angle:s.angle,length:s.length*o.coordScale},!0),a=[i.negative,i.positive].filter(t=>!!t?.full),n=e.signature.split("|")[1].split(","),r=new Set(a.map(t=>t.roomId));return a.length===n.length&&r.size===n.length&&n.every(t=>r.has(t))})&&(d=U,p=Y,_=tt,f+=j.length,b+=I.size)}return{walls:d,partitions:p,openings:_,roomDrafts:g,partitionsReconciled:f,openingsRehosted:b,removedDrafts:m.size}}const Jn=t=>JSON.parse(JSON.stringify(t)),Yn=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),Xn=(t,e)=>e.length>0&&e.length<=35&&("space"===t?/^[a-z0-9_-]+$/.test(e):/^[A-Za-z0-9_-]+$/.test(e));function Zn(t,e){let s=String(e??"");const i=t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),o=new RegExp(`^${i}_(.+)_([0-9a-f]{8})$`);let a=0;for(;a<16;a++){const t=o.exec(s);if(!t)return{root:s,layers:a,bounded:!1};s=t[1]}return{root:s,layers:a,bounded:o.test(s)}}const Qn=(t,e,s)=>{const i=t.get(e)||[];i.push(s),t.set(e,i)};const tr=1e-9;class er extends Error{constructor(t,e=""){super(e?`${t}: ${e}`:t),this.reason=t,this.name="WallSegmentModelError"}}const sr=t=>JSON.parse(JSON.stringify(t)),ir=t=>Array.isArray(t)&&t.length>=2&&Number.isFinite(Number(t[0]))&&Number.isFinite(Number(t[1])),or=t=>[Number(t[0]),Number(t[1])],ar=t=>`${Number(t[0]).toFixed(12)},${Number(t[1]).toFixed(12)}`,nr=(t,e)=>{const s=ar(t),i=ar(e);return sar(t)<=ar(e)?[or(t),or(e)]:[or(e),or(t)],lr=(t,e)=>Math.hypot(e[0]-t[0],e[1]-t[1]),hr=t=>{const e=[[Number(t.points[0][0]),Number(t.points[0][1])]],s=[];for(let i=1;i{const s=new Set,i=t=>{const e=Number(t);if(!Number.isFinite(e))return;const i=e/I;Math.abs(i-Math.round(i))>=X&&s.add(e.toFixed(12))};for(const e of Array.isArray(t?.rooms)?t.rooms:[])for(const t of R(e)||[])i(t[0]),i(t[1]);for(const e of[t?.wall_segments,t?.walls])for(const t of Array.isArray(e)?e:[])i(t?.a?.[0]),i(t?.a?.[1]),i(t?.b?.[0]),i(t?.b?.[1]);for(const t of e)i(t?.[0]),i(t?.[1]);return s.size},dr=(t,e,s)=>{const i=s[0]-e[0],o=s[1]-e[1],a=i*i+o*o;return a>1e-18?((t[0]-e[0])*i+(t[1]-e[1])*o)/a:0},pr=(t,e,s)=>{const i=Math.max(0,Math.min(1,dr(t,e,s)));return Math.hypot(t[0]-(e[0]+(s[0]-e[0])*i),t[1]-(e[1]+(s[1]-e[1])*i))},_r=(t,e,s,i,o=tr)=>{const a=e[0]-t[0],n=e[1]-t[1],r=Math.hypot(a,n);if(r<=o)return 0;const l=e=>Math.abs((e[0]-t[0])*n-(e[1]-t[1])*a)/r;if(l(s)>o||l(i)>o)return 0;const h=dr(s,t,e),c=dr(i,t,e);return Math.max(0,Math.min(1,Math.max(h,c))-Math.max(0,Math.min(h,c)))*r},ur=t=>{const e=(new TextEncoder).encode(t),s=8*e.length,i=64*Math.ceil((e.length+9)/64),o=new Uint8Array(i);o.set(e),o[e.length]=128;const a=new DataView(o.buffer);a.setUint32(i-8,Math.floor(s/4294967296),!1),a.setUint32(i-4,s>>>0,!1);const n=new Uint32Array([1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298]),r=new Uint32Array([1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225]),l=new Uint32Array(64),h=(t,e)=>t>>>e|t<<32-e;for(let t=0;t>>3,o=h(s,17)^h(s,19)^s>>>10;l[t]=l[t-16]+i+l[t-7]+o>>>0}let[e,s,i,o,c,d,p,_]=r;for(let t=0;t<64;t++){const a=_+(h(c,6)^h(c,11)^h(c,25))+(c&d^~c&p)+n[t]+l[t]>>>0,r=e&s^e&i^s&i;_=p,p=d,d=c,c=o+a>>>0,o=i,i=s,s=e,e=a+((h(e,2)^h(e,13)^h(e,22))+r>>>0)>>>0}r[0]=r[0]+e>>>0,r[1]=r[1]+s>>>0,r[2]=r[2]+i>>>0,r[3]=r[3]+o>>>0,r[4]=r[4]+c>>>0,r[5]=r[5]+d>>>0,r[6]=r[6]+p>>>0,r[7]=r[7]+_>>>0}const c=new Uint8Array(32),d=new DataView(c.buffer);for(let t=0;t{const e="abcdefghijklmnopqrstuvwxyz234567";let s=0,i=0,o="";for(const a of t)for(i=i<<8|a,s+=8;s>=5;)o+=e[i>>>s-5&31],s-=5;return s&&(o+=e[i<<5-s&31]),o},gr=(t,e,s,i,o="")=>{const[a,n]=rr(e,s),r=`${t}|${ar(a)}|${ar(n)}|${[...i].sort().join(",")}${o}`;return`wall-${mr(ur(r)).slice(0,20)}`};let fr=0;const br=t=>{for(let e=0;e<1e3;e++){const e=globalThis.crypto?.randomUUID?.(),s=`wall-${e||mr(ur(`${Date.now()}|${fr++}|${Math.random()}`)).slice(0,26)}`;if(!t.has(s))return s}throw new er("duplicate-id","id factory exhausted")},yr=t=>{const e=new Map;for(const s of Array.isArray(t?.wall_segments)?t.wall_segments:[])if(s&&"string"==typeof s.id&&ir(s.a)&&ir(s.b)){if(e.has(s.id))throw new er("duplicate-id",s.id);e.set(s.id,s)}return e},vr=(t,e,s)=>{const i=(s,i)=>{const o=t[0]-s[0],a=t[1]-s[1];return Math.abs(e[0]-i[0]-o)<=tr&&Math.abs(e[1]-i[1]-a)<=tr?[o,a]:null};return i(s.a,s.b)||i(s.b,s.a)},$r=(t,e)=>{const s=Array.isArray(t.rooms)?t.rooms:[],i=V(t.open_spans).map(t=>[t.a[0],t.a[1],t.b[0],t.b[1]]),o=i.length?i:J(s,null,1,.04*I,!0),a=[];for(const t of s){const s=R(t),i=Array.isArray(t?.wall_ids)?t.wall_ids:[];if(s&&i.length===s.length)for(let t=0;t"partition"!==t.host?.kind&&[Number(t.x),Number(t.y)].every(Number.isFinite)&&Number.isFinite(Number(t.angle))&&Number(t.length)>0&&o.some(e=>pr([Number(t.x),Number(t.y)],[e[0],e[1]],[e[2],e[3]])<=.04*I)):[],l=r.map(t=>{const e=Number(t.angle)*Math.PI/180,s=[Math.cos(e),Math.sin(e)],i=Number(t.length)/2,o=[Number(t.x),Number(t.y)];return{a:[o[0]-s[0]*i,o[1]-s[1]*i],b:[o[0]+s[0]*i,o[1]+s[1]*i]}}),h=(t,e)=>r.some(s=>{const i=[Number(s.x),Number(s.y)];if(!Z(t,e,Number(s.angle)))return!1;if(pr(i,t,e)>.02*I)return!1;const o=lr(t,e),a=dr(i,t,e)*o,n=Number(s.length)/2;return a+n>=-tr&&a-n<=o+tr}),c=new Map,d=[];for(const i of s){const r=String(i?.id||""),p=R(i);if(!r||!p||p.length<3)throw new er("invalid-room",r);const _=Y(s,r,n,I,1,[...t.walls||[],...l]);if(!_||_.poly.length<3)throw new er("invalid-room",r);const u=Array.isArray(i.wall_ids)?i.wall_ids:[],m=u.length===p.length;let g=null,f=m;if(f)for(let t=0;ttr||Math.abs(i[1]-g[1])>tr)){f=!1;break}g=i}const b=[];for(let t=0;t<_.poly.length;t++){const s=_.poly[t],i=_.poly[(t+1)%_.poly.length];if(lr(s,i)<=tr)throw new er("zero-length",r);const n=nr(s,i);let l=c.get(n);if(!l){const[t,e]=rr(s,i);l={key:n,a:t,b:e,owners:new Set,preferredIds:new Set,positionalIds:new Set,preferredCarriers:new Map,parentKeys:new Set,zeroWall:!1},c.set(n,l)}if(l.owners.add(r),l.owners.size>2)throw new er("third-owner",n);const d=m?u[_.parent[t]]:void 0;if("string"==typeof d&&d){const o=e.get(d);f||!o||_r(s,i,o.a,o.b)>tr?l.preferredIds.add(d):l.positionalIds.add(d),l.preferredCarriers.set(d,{a:or(p[_.parent[t]]),b:or(p[(_.parent[t]+1)%p.length])})}const g=_.parent[t];l.parentKeys.add(K(p[g],p[(g+1)%p.length],I));const y=[(s[0]+i[0])/2,(s[1]+i[1])/2],v=t=>t.some(t=>pr(y,[t[0],t[1]],[t[2],t[3]])<=.04*I);l.zeroWall||=v(a)||v(o)&&!h(s,i),b.push(n)}const y={...i};delete y.wall_ids,d.push({...y,poly:_.poly.map(t=>[t[0],t[1]]),wall_ids:b})}return{atoms:[...c.values()].sort((t,e)=>t.key.localeCompare(e.key)),rooms:d}},wr=(t,e,s,i,o)=>{const a=new Map([...s.values()].map(t=>[nr(t.a,t.b),t])),n=(t=>{const e=new Map;for(const s of t)"wall"===s.host?.kind&&e.set(s.host.id,(e.get(s.host.id)||0)+1);return e})(t.openings||[]),r=new Map;for(const t of e){const e=o?.get(t.key);if(e){const i=s.get(e);if(!i)throw new er("duplicate-id",e);r.set(t,i);continue}if(t.preferredIds.size>1)throw new er("duplicate-id",[...t.preferredIds].sort().join(","));const i=[...t.preferredIds][0],l=i?s.get(i):void 0;if(l){r.set(t,l);continue}const h=i?t.preferredCarriers.get(i):void 0;if(i&&h){r.set(t,{id:i,a:h.a,b:h.b,cm:0});continue}const c=a.get(t.key);if(c){r.set(t,c);continue}const d=[...s.values()].filter(e=>_r(t.a,t.b,e.a,e.b)>tr),p=[...new Map(d.map(t=>[t.id,t])).values()];if(p.sort((t,e)=>(n.get(e.id)||0)-(n.get(t.id)||0)||lr(e.a,e.b)-lr(t.a,t.b)||t.id.localeCompare(e.id)),p[0])r.set(t,p[0]);else if(1===t.positionalIds.size){const e=s.get([...t.positionalIds][0]);e&&r.set(t,e)}}const l=new Map;for(const[t,e]of r){const s=l.get(e.id)||[];s.push(t),l.set(e.id,s)}for(const[t,e]of l){const i=s.get(t)||r.get(e[0]);if(!i)throw new er("duplicate-id",t);const o=[(i.a[0]+i.b[0])/2,(i.a[1]+i.b[1])/2];e.sort((t,e)=>{const s=pr(o,t.a,t.b)<=tr?0:1,a=pr(o,e.a,e.b)<=tr?0:1;if(s!==a)return s-a;return(pr(i.a,t.a,t.b)<=tr?0:1)-(pr(i.a,e.a,e.b)<=tr?0:1)||t.key.localeCompare(e.key)}),e[0].id=t}const h=(t=>{const e=new Set;for(const s of[t.rooms,t.openings,t.decor,t.room_drafts,t.partitions,t.wall_columns])for(const t of Array.isArray(s)?s:[])"string"==typeof t?.id&&t.id&&e.add(t.id);for(const s of Array.isArray(t.room_drafts)?t.room_drafts:[])for(const t of Array.isArray(s?.segments)?s.segments:[])"string"==typeof t?.id&&t.id&&e.add(t.id);return e})(t);for(const t of e)if(t.id){if(h.has(t.id))throw new er("duplicate-id",t.id);h.add(t.id)}const c=e.filter(t=>!t.id);if(i){const e=c.map(e=>{const[s,i]=rr(e.a,e.b),o=`${String(t.id||"")}|${ar(s)}|${ar(i)}|${[...e.owners].sort().join(",")}`,a=mr(ur(o));return{atom:e,seed:o,digest:a,base:`wall-${a.slice(0,20)}`}}).sort((t,e)=>t.digest.localeCompare(e.digest)||t.atom.key.localeCompare(e.atom.key)),s=new Map;for(const t of e){const e=s.get(t.digest);if(e&&e!==t.seed)throw new er("duplicate-id",t.digest);s.set(t.digest,t.seed);let i=1,o=t.base;for(;h.has(o);)o=`${t.base}-${++i}`;t.atom.id=o,h.add(o)}}else for(const t of c)t.id=br(h),h.add(t.id)},kr=(t,e)=>{if("partition"===t.host?.kind)return null;const s=[Number(t.x),Number(t.y)];if(!s.every(Number.isFinite))return null;const i=e=>{if(!(Number(e.cm)>0))return!1;const i=dr(s,e.a,e.b),o=lr(e.a,e.b),a=Number(t.length)/2;return i>=-tr&&i<=1+tr&&pr(s,e.a,e.b)<=.02*I&&Z(e.a,e.b,Number(t.angle))&&Number.isFinite(a)&&a>=0&&i*o-a>=-tr&&i*o+a<=o+tr},o="wall"===t.host?.kind?e.find(e=>e.id===t.host.id):null,a=o&&i(o)?[o]:e.filter(i);if(1!==a.length)return null;const n=a[0];return{kind:"wall",id:n.id,t:Math.max(0,Math.min(1,dr(s,n.a,n.b)))}},xr=(t,e)=>{const s=[Number(t.x),Number(t.y)];if(!s.every(Number.isFinite))return null;const i=Number(t.length)/2,o=e.filter(e=>{if(!(Number(e.cm)>0))return!1;const o=dr(s,e.a,e.b),a=lr(e.a,e.b);return o>=-tr&&o<=1+tr&&pr(s,e.a,e.b)<=.02*I&&Z(e.a,e.b,Number(t.angle))&&Number.isFinite(i)&&i>=0&&o*a-i>=-tr&&o*a+i<=a+tr}),a=(e=>{if(!e.length)return null;const i="wall"===t.host?.kind?e.find(e=>e.id===t.host.id):null;return i||[...e].sort((t,e)=>pr(s,t.a,t.b)-pr(s,e.a,e.b)||Number(e.cm)-Number(t.cm)||(t.ide.id?1:0))[0]})(o);return a?{kind:"wall",id:a.id,t:Math.max(0,Math.min(1,dr(s,a.a,a.b)))}:null},Sr=(t,e,s)=>{const i=yr(t),{atoms:o,rooms:a}=$r(t,i);wr(t,o,i,e,s);const n=o.map(e=>{const s=e.id?i.get(e.id):void 0,o=((t,e,s)=>{if(e.zeroWall)return 0;const i=Array.isArray(t.walls)?t.walls:[],o=K(e.a,e.b,I),a=i.filter(t=>Number(t.cm)>0&&(!(t.key!==o&&!e.parentKeys.has(t.key))||!(!ir(t.a)||!ir(t.b))&&_r(e.a,e.b,t.a,t.b)>=lr(e.a,e.b)-tr)).map(t=>Math.max(1,Math.min(100,Number(t.cm))));if(new Set(a.map(t=>t.toFixed(9))).size>1)throw new er("thickness-conflict",e.key);if(a.length)return a[0];const n=Q(i,e.a,e.b,I,1);return n>0?n:Number(s?.cm)>0?Number(s.cm):0})(t,e,s);return{...s||{},id:e.id,a:[...e.a],b:[...e.b],cm:o}}),r=new Map(o.map(t=>[t.key,t.id]));for(const t of a)t.wall_ids=t.wall_ids.map(t=>r.get(t));t.rooms=a,t.wall_segments=n,t.walls=n.filter(t=>t.cm>0).map(t=>({key:K(t.a,t.b,I),cm:t.cm,a:[...t.a],b:[...t.b]})),t.walls.length||delete t.walls,delete t.open_spans;for(const e of t.rooms)delete e.open_to;return((t,e)=>{const s=new Set;for(const e of[t.rooms,t.openings,t.decor,t.room_drafts,t.partitions,t.wall_columns,t.wall_segments])for(const t of Array.isArray(e)?e:[])"string"==typeof t?.id&&t.id&&s.add(t.id);for(const i of Array.isArray(t.room_drafts)?t.room_drafts:[]){const o=Array.isArray(i.segments)?i.segments:[];for(let a=0;a{const i=Array.isArray(t.openings)?t.openings:[];for(const t of i){if("partition"===t.host?.kind)continue;if(!t.host&&!s){const s=kr(t,e);s&&(t.host=s);continue}const i=kr(t,e)??(s?xr(t,e):null);if(i)t.host=i;else{if(!s)throw new er("opening-host",t.id);delete t.host}}})(t,n,e),n.reduce((t,e)=>t+(i.has(e.id)?0:1),0)};function Mr(t,e={}){const s=JSON.stringify(t),i=sr(t);U(i);let o=0;const a=Number(i?.model_version||0)<9;for(const t of Array.isArray(i?.spaces)?i.spaces:[]){const s=e.lineageSpaceId===String(t?.id||"")?e.lineageHints:void 0;o+=Sr(t,a,s)}return i.model_version=9,U(i),{config:i,changed:s!==JSON.stringify(i),migratedSegments:o}}const Dr=t=>!!t&&"object"==typeof t&&!Array.isArray(t);function Cr(t,e){const s=(t,e)=>{if(Array.isArray(t)&&Array.isArray(e)){const i=new Map;for(const e of t)Dr(e)&&"string"==typeof e.id&&e.id&&i.set(e.id,e);const o=e.map((e,o)=>{if(Dr(e)&&"string"==typeof e.id&&e.id){const t=i.get(e.id);return t?s(t,e):sr(e)}return o[e,e in t?s(t[e],i):sr(i)]);for(const e of Object.keys(t))delete t[e];for(const[e,s]of i)t[e]=s;return t}return sr(e)};return s(t,e)}const Pr=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),Ir=(t,e,s)=>Math.min(s,Math.max(e,t)),Tr=t=>ut({spaces:[t]})[0],Rr=(t,e,s)=>{const i=s>0?s:1,o=t=>`${(t[0]/i).toFixed(12)},${(t[1]/i).toFixed(12)}`,a=o(t),n=o(e);return a0?n:1,l=Math.max(i*r*.02,1e-9),h=[];for(const e of t||[])for(const t of R(e)||[])h.push([t[0],t[1]]);const c=[];for(const t of s||[])Array.isArray(t)&&t.length>=4&&t.slice(0,4).every(Number.isFinite)&&c.push([t[0],t[1]],[t[2],t[3]]);const d=(t,e)=>e.some(e=>Math.hypot(t[0]-e[0],t[1]-e[1])<=2*l),p=new Map;for(const n of t||[]){if(!n?.id)continue;const _=mt(t,n.id,e,s,i,o,a,r);if(_)for(let t=0;t<_.orig.length;t++){const e=[];for(let s=0;s<_.parent.length;s++)_.parent[s]===t&&e.push(s);for(let t=1;t+10&&u>0&&n===m&&u!==n))continue;const g=_.poly[i],f=_.poly[(i+1)%_.poly.length],b=Math.hypot(f[0]-g[0],f[1]-g[1]);if(!(b>l&&b<.5*a-1e-9*a))continue;if(d(g,c)||d(f,c))continue;if(d(g,h)&&d(f,h))continue;const y=Rr(g,f,r),v=p.get(y);v?v.targets.add(n):p.set(y,{a:[g[0],g[1]],b:[f[0],f[1]],targets:new Set([n])})}}}let _=e.slice();const u=(t,e)=>Math.hypot(t[0]-e[0],t[1]-e[1])<=2*l,m=(t,e)=>{if(!Array.isArray(t.a)||!Array.isArray(t.b)||t.a.length<2||t.b.length<2)return!1;const s=[Number(t.a[0])*r,Number(t.a[1])*r],i=[Number(t.b[0])*r,Number(t.b[1])*r];return!![...s,...i].every(Number.isFinite)&&(u(s,e.a)&&u(i,e.b)||u(s,e.b)&&u(i,e.a))};for(const t of[...p.values()].filter(t=>1===t.targets.size).sort((t,e)=>Rr(t.a,t.b,r).localeCompare(Rr(e.a,e.b,r)))){const e=[...t.targets][0],s=_.filter(e=>m(e,t));if(new Set(s.map(t=>t.cm)).size>1)continue;let o=!1;_=_.map(s=>m(s,t)?(o=!0,s.cm===e?s:{...s,cm:e}):s),o||(_=E(_,t.a,t.b,e,i,r))}return _}const Nr=t=>{const e=[];for(const s of t||[]){const t=R(s);if(t?.length)for(let s=0;s"string"==typeof t?.id?t.id:"").filter(Boolean),c=new Set(h),d=new Map;for(const t of h){const e=Zn("space",t);e.layers>0&&Xn("space",e.root)&&Qn(d,e.root,t)}const p=new Map,_=new Map,u=new Map,m=new Map;for(const t of r){const e="string"==typeof t?.id?t.id:"";if(!e)continue;const s=new Map;for(const i of Array.isArray(t.rooms)?t.rooms:[]){const t="string"==typeof i?.id?i.id:"";if(!t)continue;const o=Zn("room",t);o.layers>0&&Xn("room",o.root)&&Qn(s,o.root,t),_.has(t)||_.set(t,e),u.has(t)||u.set(t,String(i.name||""));const a="string"==typeof i.area?i.area:"";if(a){const s=m.get(a)||[];s.push({spaceId:e,roomId:t}),m.set(a,s)}}p.set(e,s)}const g=new Set(_.keys()),f=new Map,b=new Map;for(const t of l)"string"==typeof t?.id&&t.id&&(!0===t.removed?b.set(t.id,t):f.set(t.id,t));const y={spaceRefsRemapped:0,roomRefsRemapped:0,positionsRemapped:0,markersDetached:0,positionsUnresolved:0,nestedRefsUnresolved:0,deadSpaceIds:[],orphanRoomLabelsRemoved:0,orphanDevicePositionsRemoved:0,orphanGroupPositionsRemoved:0,liveMissingPositionsRemoved:0,removedPositions:[],liveMissingPositions:[],unverifiedPositions:[]},v=new Set,$=t=>c.has(t)?null:(()=>{const e=Zn("space",t).root;if(!Xn("space",e))return null;const s=d.get(e)||[];return 1===s.length?s[0]:null})(),w=(t,e)=>{const s=Zn("room",t).root;if(!Xn("room",s))return null;const i=p.get(e)?.get(s)||[];return 1===i.length?i[0]:null},k=t=>{const e=s.effectiveAreaByMarker?.[t]||"",i=e&&m.get(e)||[];return 1===i.length?i[0]:null},x=(t,e,s)=>{if("string"==typeof t.room_id&&t.room_id&&!g.has(t.room_id)){const i=e?w(t.room_id,e):null;i?(t.room_id=i,y.roomRefsRemapped++):s?(t.room_id=s.roomId,y.roomRefsRemapped++):!0!==t.removed&&delete t.room_id}const i=t.vacuum?.segment_map;if(i&&"object"==typeof i&&!Array.isArray(i))for(const t of Object.keys(i)){const s=i[t];if("string"!=typeof s||!s||g.has(s))continue;const o=e?w(s,e):null;o?(i[t]=o,y.roomRefsRemapped++):y.nestedRefsUnresolved++}};for(const t of l){const e="string"==typeof t?.id?t.id:"";if(!e)continue;const s="string"==typeof t.space?t.space:"",i=s&&!c.has(s),a=o[e],n="string"==typeof a?.s?a.s:"",r=n&&!c.has(n),l=!0===t.removed;let h=s&&c.has(s)?s:null,d=null,p=!1;if(i){const i=$(s);i?(t.space=i,h=i,p=!0,y.spaceRefsRemapped++):l||"virtual"===t.binding?l||(delete t.space,h=null,y.markersDetached++):(d=k(e),d?(t.space=d.spaceId,h=d.spaceId,y.spaceRefsRemapped++):(delete t.space,h=null,y.markersDetached++))}l&&!p||x(t,h,d),r&&p&&n===s&&h&&(o[e]={...a,s:h},y.positionsRemapped++,v.add(e))}for(const t of Object.keys(o)){if(v.has(t))continue;const e=o[t],s="string"==typeof e?.s?e.s:"";if(!s||c.has(s))continue;const i=$(s);if(i){if(t.startsWith("rl_")){const s=t.slice(3),a=w(s,i);if(a){const s=`rl_${a}`;Yn(o,s)||(o[s]={...e,s:i}),delete o[t],y.roomRefsRemapped++,y.positionsRemapped++;continue}}o[t]={...e,s:i},y.positionsRemapped++}}const S=s.ownerRoster,M=!0===S?.authoritative,D=new Set(S?.deviceIds||[]),C=new Set(S?.entityIds||[]),P=new Set(Array.isArray(i.settings?.known_devices)?i.settings.known_devices.filter(t=>"string"==typeof t):[]),I=(t,e="")=>{const s=String(S?.names?.[t]||e||"").trim();return s&&s!==t?s:""},T=(t,e,s,i="",o)=>({id:t,spaceId:e,kind:s,name:I(t,i),...o?{reason:o}:{}}),R=t=>{"room_label"===t?y.orphanRoomLabelsRemoved++:"group"===t?y.orphanGroupPositionsRemoved++:y.orphanDevicePositionsRemoved++},F=new Set;for(const[t,e]of Object.entries(o)){const i="string"==typeof e?.s?e.s:"";if(!i||c.has(i))continue;let a,n;const r=f.get(t),l=b.get(t);if(r)a=T(t,i,"device",String(r.name||"")),n="live";else if(l)a=T(t,i,"device",String(l.name||"")),n="absent";else if(t.startsWith("rl_")){const e=t.slice(3);a=T(t,i,"room_label",u.get(e)||""),n=g.has(e)?"live":"absent"}else if(t.startsWith("lg_")){const e=t.slice(3);a=T(t,i,"group"),n=C.has(e)?"live":M?"absent":"unverified","unverified"===n&&(a.reason="registry_unavailable")}else D.has(t)?(a=T(t,i,"device"),n="live"):P.has(t)?(a=T(t,i,"device"),n=M?"absent":"unverified","unverified"===n&&(a.reason="registry_unavailable")):(a=T(t,i,"unknown","","unknown_owner"),n="unverified");if("absent"!==n){if("live"===n){if(y.liveMissingPositions.push(a),!0===s.removeLiveMissingPositions){delete o[t],y.liveMissingPositionsRemoved++,R(a.kind),y.removedPositions.push(a);continue}}else y.unverifiedPositions.push(a);y.positionsUnresolved++,F.add(i)}else delete o[t],R(a.kind),y.removedPositions.push(a)}return y.deadSpaceIds=[...F].sort((t,e)=>t.localeCompare(e)),{config:i,layout:o,report:y,changed:JSON.stringify(i)!==a||JSON.stringify(o)!==n}}(t,e,s),n=a.config,r=tt(n,a.layout);U(n),et(a.layout);const l=JSON.stringify(t||{}),h=JSON.stringify(e||{}),c=Number.isInteger(Number(n.model_version))?Number(n.model_version):0,d=(t=>{let e=0,s=0,i=0;for(const s of t.markers||[]){if("ripple"===s.display&&(s.display="icon_ripple",e++),Array.isArray(s.controls)){const t="string"==typeof s.binding&&s.binding.startsWith("entity:")?s.binding.slice(7):"",i=t?s.controls.filter(e=>e!==t):s.controls;JSON.stringify(i)!==JSON.stringify(s.controls)&&(s.controls=i.length?i:null,e++)}const t=s.vacuum;t&&Pr(t,"trail")&&(["never","cleaning","always"].includes(t.trail_mode)||(t.trail_mode=!1===t.trail?"never":"cleaning"),delete t.trail,e++)}for(const o of t.spaces||[]){const t=o.settings;"glow"===t?.fill_mode&&("boolean"!=typeof t.glow_enabled&&(t.glow_enabled=!0),t.fill_mode="none",e++,s++);for(const t of o.rooms||[]){const s=t.settings;"glow"===s?.fill_mode&&("boolean"!=typeof s.glow&&(s.glow=!0),delete s.fill_mode,e++,i++)}if(Pr(o,"segments")&&(delete o.segments,e++),Pr(o,"plan_scale")){const t=Number(o.plan_scale);Number.isFinite(t)&&t>=lt&&t<=ht&&(Pr(o,"plan_scale_x")||(o.plan_scale_x=t),Pr(o,"plan_scale_y")||(o.plan_scale_y=t),delete o.plan_scale,e++)}const a=Number(o.cell_cm);Pr(o,"cell_cm")&&(!Number.isFinite(a)||a<.1||a>1e3)&&(o.cell_cm=Number.isFinite(a)&&a>0?Ir(a,.1,1e3):5,e++);const n=Number(o.cell_cm),r=Number.isFinite(n)&&n>0?n:5;for(const t of o.wall_columns||[])if("circle"===t.shape)Pr(t,"angle")&&(delete t.angle,e++);else if("square"===t.shape&&Pr(t,"angle")){const s=((Number(t.angle)||0)%90+90)%90;t.angle!==s&&(t.angle=s,e++)}for(const t of o.decor||[]){if(Pr(t,"width")){const s=Number(t.width);(Pr(t,"width_cm")||Number.isFinite(s))&&(Pr(t,"width_cm")||(t.width_cm=Number(Ir(s/nt*r,.1,100).toFixed(6))),delete t.width,e++)}if("rect"!==t?.kind&&"ellipse"!==t?.kind||!0!==t.fill||(Pr(t,"fill_color")||(t.fill_color=t.color||"#607d8b",e++),Pr(t,"fill_opacity")||(t.fill_opacity=.25,e++)),"text"!==t?.kind)continue;void 0===t.size_cm?(t.size_cm=Number(Ir(ct*dt(t)/nt*r,.1,2e3).toFixed(6)),delete t.scale,delete t.size,e++):(Pr(t,"scale")||Pr(t,"size"))&&(delete t.scale,delete t.size,e++);let s=String(t.text??"");const i=[...s.matchAll(/\{([^{}\r\n]+)\}/g)].some(t=>!!pt(t[1]));if(!(Pr(t,"entity")||Pr(t,"attr")||Pr(t,"unit")))continue;const o=String(t.unit??"").trim(),a="state"===String(t.attr??"").trim().toLowerCase()?null:t.attr,n=i||o?"":_t(t.entity,a);if(i||n){if(n){const e=s.indexOf("{}");s=e>=0?s.slice(0,e)+n+s.slice(e+2):`${s}${s?" ":""}${n}`,t.text=s}delete t.entity,delete t.attr,delete t.unit,e++}}}return{total:e,glowSpaces:s,glowRooms:i}})(n);let p=d.total;const _=(u=n.spaces||[],JSON.parse(JSON.stringify(u)));var u;const m=Sn(n.spaces||[],a.layout);let g=0,f=0,b=0,y="";const v=m.spaces.map(t=>{const e=st(t);g+=e.report.wallsStraightened,f+=e.report.wallsStraightenSkipped;const s=Number(t?.cell_cm)>0?Number(t.cell_cm):5,i=e.report.maxStraightenShift/I*s;return i>b&&(b=i,y=String(t?.id||"")),e.space}),$=g?Sn(v,m.layout):{...m,spaces:v};n.spaces=$.spaces;const w=g?{...m.report,moved:m.report.moved+$.report.moved,coordsCanonicalized:m.report.coordsCanonicalized+$.report.coordsCanonicalized,maxShift:Math.max(m.report.maxShift,$.report.maxShift),maxShiftCm:Math.max(m.report.maxShiftCm,$.report.maxShiftCm),maxSpace:$.report.maxShiftCm>m.report.maxShiftCm?$.report.maxSpace:m.report.maxSpace,rotated:m.report.rotated+$.report.rotated,removedDrafts:m.report.removedDrafts+$.report.removedDrafts}:{...m.report};let k=0,x=0,S=0,M=0,D=0,C=0,P=0,T=0;for(let t=0;t0===Number(t.cm)),walls:e.walls||[]}),a=Tr(e),r=Tr(s);if(!a||!r)continue;const l=Nr(a.rooms),h=Nr(r.rooms),c=.02*nt,d=(e.wall_segments||[]).filter(t=>0===Number(t.cm)).map(t=>({...t,key:K(t.a,t.b,I),cm:0})),p=new Set(d.map(t=>t.key)),u=it(e,a.rooms,ot,c);x+=u.length;for(const t of u){const e=[t[0]/ot,t[1]/ot],s=[t[2]/ot,t[3]/ot],i=K(e,s,I);p.has(i)||(p.add(i),d.push({key:i,a:e,b:s,cm:0}))}const m=d.length,g=at(d,l,h,I,ot),f=(t,e,s)=>{const i=s[0]-e[0],o=s[1]-e[1],a=i*i+o*o;if(!(a>0))return Math.hypot(t[0]-e[0],t[1]-e[1]);const n=Math.max(0,Math.min(1,((t[0]-e[0])*i+(t[1]-e[1])*o)/a));return Math.hypot(t[0]-(e[0]+i*n),t[1]-(e[1]+o*n))},b=g.filter(t=>{if(!Array.isArray(t.a)||!Array.isArray(t.b))return!1;const e=t.a.map(t=>t*ot),s=t.b.map(t=>t*ot);return h.some(([t,i])=>f(e,t,i)<=4*c&&f(s,t,i)<=4*c)});S+=Math.max(0,m-b.length);const y=(s.wall_segments||[]).filter(t=>Number(t.cm)>0);s.wall_segments=[...y,...b.map(({key:t,...e})=>e)];const v=b.map(t=>[t.a[0]*ot,t.a[1]*ot,t.b[0]*ot,t.b[1]*ot]),$=Array.isArray(e.walls)?e.walls.length:0;let w=at(e.walls,l,h,I,ot);w=Fr(r.rooms,w,v,I,Number(s.cell_cm)>0?Number(s.cell_cm):5,nt,ot),w=B(r.rooms,w,v,I,Number(s.cell_cm)>0?Number(s.cell_cm):5,nt,ot),w=rt(w,s.rooms||[],I,1,v.map(t=>[t[0]/ot,t[1]/ot,t[2]/ot,t[3]/ot])),k+=Math.max(0,$-w.length),w.length?s.walls=w:delete s.walls;const R=Rn(s.partitions||[],{pitch:I,geometry:Nn(s)});R.merged&&(M+=R.merged,s.partitions=R.partitions,Fn(s.openings,s.partitions,R.openingMoves,{coordScale:ot,cellCm:Number(s.cell_cm)>0?Number(s.cell_cm):5,gridPitch:nt}));const F=Tr(s);if(F){const t=o(s,F,s.walls||[],v,{pitch:I,cellCm:Number(s.cell_cm)>0?Number(s.cell_cm):5,gridPitch:nt,coordScale:ot});t.partitionsReconciled&&(D+=t.partitionsReconciled,C+=t.openingsRehosted,t.partitions.length?s.partitions=t.partitions:delete s.partitions,t.openings.length?s.openings=t.openings:delete s.openings,t.walls.length?s.walls=t.walls:delete s.walls),t.removedDrafts&&(P+=t.removedDrafts,t.roomDrafts.length?s.room_drafts=t.roomDrafts:delete s.room_drafts)}JSON.stringify({zero:(s.wall_segments||[]).filter(t=>0===Number(t.cm)),walls:s.walls||[]})!==i&&T++}let R=0;c<=9&&(R=function(t){const e=Mr(t);return Cr(t,e.config),{...e,config:t}}(n).migratedSegments);const F=U(n),N=et($.layout),z=JSON.stringify(F)!==l||JSON.stringify(N)!==h;c<9&&z&&(F.model_version=9);const A=JSON.stringify(F)!==l||JSON.stringify(N)!==h,O=Number.isInteger(Number(F.model_version))?Number(F.model_version):c,L=A?w:{...w,moved:0,coordsCanonicalized:0,maxShift:0,maxShiftCm:0,maxSpace:"",rotated:0,removedDrafts:0},E=A?a.report:{...a.report,spaceRefsRemapped:0,roomRefsRemapped:0,positionsRemapped:0,markersDetached:0,orphanRoomLabelsRemoved:0,orphanDevicePositionsRemoved:0,orphanGroupPositionsRemoved:0,liveMissingPositionsRemoved:0};return{config:F,layout:N,report:{...L,modelFrom:c,modelTo:O,migrated:A?p:0,glowSpacesMigrated:A?d.glowSpaces:0,glowRoomsMigrated:A?d.glowRooms:0,canonicalized:A?T:0,wallSegmentsMigrated:A?R:0,legacyZeroWallsMigrated:A?x:0,wallsMerged:A?k:0,spansMerged:A?S:0,partitionsMerged:A?M:0,partitionsReconciled:A?D:0,openingsRehosted:A?C:0,redundantDraftsRemoved:A?P:0,wallsStraightened:A?g:0,wallsStraightenSkipped:f,maxStraightenShiftCm:A?b:0,maxStraightenSpace:A?y:"",latticeCoordinatesCanonicalized:A?r.canonicalized:0,latticeCoordinatesFar:A?r.far:0,latticeMaxShift:A?r.maxShift:0,latticeMaxShiftCm:A?r.maxShiftCm:0,latticeSpaces:A?r.spaces:[],...E},changed:A}}const Ar=(t,e)=>t[0]*e[1]-t[1]*e[0],Or=(t,e)=>t[0]*e[0]+t[1]*e[1],Lr=(t,e)=>[t[0]-e[0],t[1]-e[1]],Er=(t,e,s)=>[t[0]+e[0]*s,t[1]+e[1]*s];function Br(t,e,s){return Math.abs(Ar(t,e))<=s}function Hr(t,e,s){return Math.abs(Ar(Lr(t,e),s))}function Gr(t,e,s,i,o){const a=e[0]-t[0],n=e[1]-t[1],r=Math.hypot(a,n);if(!(r>o&&s>0))return[];const l=[a/r,n/r];return function(t,e,s){const i=e.map(([e,s])=>[Math.max(0,Math.min(e,s)),Math.min(t,Math.max(e,s))]).filter(([t,e])=>e>t+s).sort((t,e)=>t[0]-e[0]||t[1]-e[1]),o=[];for(const t of i){const e=o[o.length-1];!e||t[0]>e[1]+s?o.push([...t]):e[1]=Math.max(e[1],t[1])}const a=[];let n=0;for(const[t,e]of o)t>n+s&&a.push([n,t]),n=Math.max(n,e);return t>n+s&&a.push([n,t]),a}(r,i.flatMap(e=>{const i=e.b[0]-e.a[0],a=e.b[1]-e.a[1],n=Math.hypot(i,a);if(!(n>o&&Br(l,[i/n,a/n],1e-6)))return[];if(Hr(e.a,t,l)>1.1*Math.max(s,o)||Hr(e.b,t,l)>1.1*Math.max(s,o))return[];return[[Or(Lr(e.a,t),l),Or(Lr(e.b,t),l)]]}),o).flatMap(([e,i])=>{const o=bt({a:Er(t,l,e),b:Er(t,l,i),halfDepth:s});return o?[o]:[]})}function Wr(t){const e=t.coordScale??1,s=Math.max(t.epsilon??2e-4*t.gridPitch,1e-9),i=[];for(const s of t.rooms||[]){if(!s?.id)continue;const o=mt(t.rooms,s.id,t.walls,t.openCuts,t.pitch,t.cellCm,t.gridPitch,e);if(!o||o.poly.length<3)continue;const a=gt(o.poly,o.offsets);!a||a.length<3||i.push({roomId:s.id,profile:o,inner:a})}i.sort((t,e)=>t.roomId.localeCompare(e.roomId));const o=(t.roomOpenings||[]).map(t=>{const e=t.angle*Math.PI/180,s=[Math.cos(e),Math.sin(e)],i=Math.max(0,t.length)/2;return{a:Er([t.x,t.y],s,-i),b:Er([t.x,t.y],s,i)}}),a=[];for(const i of ft(t.rooms,t.walls,t.openCuts,t.pitch,t.cellCm,t.gridPitch,e))for(const t of Gr(i.a,i.b,i.depthUnits/2,o,s))a.push({body:t});const n=new Map;for(const e of t.partitionCuts||[]){const t=n.get(e.hostId)||[];t.push(e),n.set(e.hostId,t)}for(const e of t.partitions||[]){const i=j(e.cm,t.cellCm,t.gridPitch)/2;for(const t of Gr(e.a,e.b,i,n.get(e.id)||[],s))a.push({body:t,partitionId:e.id})}return{rooms:i,boundaries:a,epsilon:s}}function jr(t,e,s,i){const o=t.profile.offsets[e];if(!(o>0))return null;const a=function(t,e){const s=t[e],i=t[(e+1)%t.length],o=i[0]-s[0],a=i[1]-s[1],n=Math.hypot(o,a)||1;return function(t){let e=0;for(let s=0;s=0?[-a/n,o/n]:[a/n,-o/n]}(t.profile.poly,e),n=Er(s.center,a,o),r=[];for(let e=0;ei&&Br(s.axis,[l[0]/h,l[1]/h],1e-6)))continue;if(Hr(o,n,s.axis)>i||Hr(a,n,s.axis)>i)continue;const c=Or(Lr(o,n),s.axis),d=Or(Lr(a,n),s.axis);r.push([Math.min(c,d),Math.max(c,d)])}const l=function(t,e){t.sort((t,e)=>t[0]-e[0]||t[1]-e[1]);const s=[];for(const i of t){const t=s[s.length-1];!t||i[0]>t[1]+e?s.push([...i]):t[1]=Math.max(t[1],i[1])}return s}(r,i).find(([t,e])=>t<=i&&e>=-i);if(!l)return null;const[h,c]=l,d=Er(n,s.axis,-s.half),p=Er(n,s.axis,s.half),_=Math.max(0,-s.half-h),u=Math.max(0,c-s.half),m=_>0?Er(n,s.axis,h):d,g=u>0?Er(n,s.axis,c):p,f=Or(a,s.normal)>=0?1:-1,b=(e,i,o)=>({from:e,to:i,label:[(e[0]+i[0])/2,(e[1]+i[1])/2],axis:s.axis,labelNormal:a,distance:o,roomId:t.roomId,roomSide:f,source:"room-face"});return{left:b(d,m,_),right:b(p,g,u)}}function qr(t){const e=Er(t.center,t.axis,-t.half),s=Er(t.center,t.axis,t.half),i=Er(t.center,t.axis,Math.min(-t.half,t.targetLo)),o=Er(t.center,t.axis,Math.max(t.half,t.targetHi)),a=(e,s)=>({from:e,to:s,label:[(e[0]+s[0])/2,(e[1]+s[1])/2],axis:t.axis,labelNormal:t.normal,distance:Math.hypot(s[0]-e[0],s[1]-e[1]),source:"host-end"});return[a(e,i),a(s,o)]}function Ur(t,e,s,i){const o=Lr(s,e),a=Or(o,o);if(!(a>i*i))return Math.hypot(...Lr(t,e))<=i;const n=Or(Lr(t,e),o)/a;if(n<-i||n>1+i)return!1;const r=Er(e,o,Math.max(0,Math.min(1,n)));return Math.hypot(...Lr(t,r))<=i}function Kr(t,e,s){let i=!1;for(let o=0,a=e.length-1;ot[1]!=h>t[1]&&t[0]<(l-n)*(t[1]-r)/(h-r)+n&&(i=!i)}return i}function Vr(t,e,s,i,o,a){const n=Lr(i,s),r=Ar(e,n);if(Math.abs(r)<=a)return null;const l=Lr(s,t),h=Ar(l,n)/r,c=Ar(l,e)/r;return h<-a||h>o+a||c<-a||c>1+a?null:Math.max(0,Math.min(o,h))}function Jr(t,e,s){const i=qr(s),o="partition"===t.host?.kind?t.host.id:void 0;return[{base:i[0],direction:[-s.axis[0],-s.axis[1]]},{base:i[1],direction:s.axis}].map(({base:t,direction:s})=>{const i=t.distance,a=function(t,e,s,i,o,a){let n=null;for(const r of i)if(!o||r.partitionId!==o){if(Kr(t,r.body,a))return 0;for(let i=0;iOr(Lr(t,o),s));return{center:o,axis:s,normal:i,half:Math.max(0,t.renderedLength)/2,targetLo:Math.min(...a),targetHi:Math.max(...a)}}(t),i=e.rooms.flatMap(t=>{const i=function(t,e,s){const{profile:i}=t;let o=null;for(let t=0;t0))continue;const a=i.poly[t],n=i.poly[(t+1)%i.poly.length],r=Lr(n,a),l=Math.hypot(r[0],r[1]);if(!(l>s&&Br(e.axis,[r[0]/l,r[1]/l],1e-6)))continue;if(Hr(a,e.center,e.axis)>s||Hr(n,e.center,e.axis)>s)continue;const h=Or(Lr(a,e.center),e.axis),c=Or(Lr(n,e.center),e.axis),d=Math.min(h,c),p=Math.max(h,c);if(d>-e.half+s||p0?d:p<0?-p:0);(!o||_2)return qr(s);if(i.length){const t=i.map(({room:t,edge:i})=>jr(t,i,s,e.epsilon));if(t.some(t=>!t))return qr(s);const o=t;return[...o.map(t=>t.left),...o.map(t=>t.right)]}return Jr(t,e,s)}const Xr="1.68.1",Zr={badge:"display.badge",icon_ripple:"display.icon_ripple",value:"display.value",static_icon:"display.static_icon"},Qr={badge:"marker.display_hint_badge",icon_ripple:"marker.display_hint_icon_ripple",value:"marker.display_hint_value",static_icon:"marker.display_hint_static_icon"},tl=2e3,el=1e3,sl=t=>{const e=String(t??"").trim().replace(",",".");if(!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(e))return null;const s=Number(e);return Number.isFinite(s)?s:null},il=(t,e,s,i)=>{for(t.delete(e),t.set(e,s);t.size>i;){const e=t.keys().next().value;if(void 0===e)break;t.delete(e)}},ol=1e3,al=2e3,nl=t=>{try{t.target?.setPointerCapture?.(t.pointerId)}catch{}},rl="f111431804c4c42c6e35286f29720d089bc8b77ffc364a6517f7a3510fcb11c8";class ll{constructor(t){this.host=t,this._junctionBaselineCache=new WeakMap,this._undoGeometry=()=>{if(this.host._physicalDrag||this.host._physicalRotate)return void this._cancelPhysicalGesture();if(this.host._decorDraft)return this.host._decorDraft=null,void this.host.requestUpdate();if(this.host._decorMove||this.host._dtDrag||this.host._bdDrag)return void this._cancelDecorGesture();if(this.host._resize.dragging)return void this._rszCancelDrag();const t=this.host._geometryHistory.undo();t&&(this._applyGeometryState(t.before,!0)?this.host._showToast(this.host._t("history.undone",{name:t.name})):this.host._geometryHistory.clear())},this._redoGeometry=()=>{if(this.host._physicalDrag||this.host._physicalRotate)return void this._cancelPhysicalGesture();if(this.host._decorDraft)return this.host._decorDraft=null,void this.host.requestUpdate();if(this.host._decorMove||this.host._dtDrag||this.host._bdDrag)return void this._cancelDecorGesture();if(this.host._resize.dragging)return void this._rszCancelDrag();const t=this.host._geometryHistory.redo();t&&(this._applyGeometryState(t.after,!0)?this.host._showToast(this.host._t("history.redone",{name:t.name})):this.host._geometryHistory.clear())},this._savePhysicalDialog=()=>{const t=this.host._physicalDialog,e=this.host._curSpaceCfg,s=this.host._spaceModel();if(!t||!e||!s)return;const i=sl(t.cm);if(null==i)return void this._showPhysicalRange("column"===t.kind?yt:100,"column"===t.kind?1:0);const o=this.host._imperial?2.54*i:i,a="column"===t.kind?yt:100,n="column"===t.kind?1:0;if(!Number.isFinite(o)||oa)return void this._showPhysicalRange(a,n);if("column"===t.kind){const e=s.wall_columns.find(e=>e.id===t.id);if(!e)return;const i=sl(t.angle||"0");if("circle"!==t.shape&&(null==i||i<0||i>=90))return void this.host._showToast(this.host._t("toast.physical_angle"));const a="circle"===t.shape?{id:t.id,shape:"circle",center:e.center,cm:o}:{id:t.id,shape:"square",center:e.center,cm:o,angle:i};if(s.wall_columns.some(e=>e.id!==t.id&&vt(e,a,.02*this.host._gridPitch)))return void this.host._showToast(this.host._t("toast.column_duplicate"))}const r=this._geometrySnapshot();if("partition"===t.kind){const s=(e.partitions||[]).find(e=>e.id===t.id);if(s&&0===o&&$t(e.openings,{kind:"partition",id:t.id}))return void this.host._showToast(this.host._t("toast.zero_wall_opening_conflict"));s&&(s.cm=o)}else if("column"===t.kind){const s=(e.wall_columns||[]).find(e=>e.id===t.id);s&&(s.cm=wt(o),s.shape="circle"===t.shape?"circle":"square","square"===s.shape?s.angle=sl(t.angle||"0"):delete s.angle)}else{const s=(e.room_drafts||[]).find(e=>e.id===t.id);s?.segments?.[t.segment||0]&&(s.segments[t.segment||0].cm=o)}const l=this._commitPhysicalGeometry(this.host._t("history.physical_edit"),r);this.host._physicalDialog=null,l||this.host.requestUpdate()},this._deletePhysicalSelection=()=>{const t=this.host._physicalSel,e=this.host._curSpaceCfg;if(!t||!e)return;if("draft"===t.kind)return void this._deleteDraftWhole();const s=this._geometrySnapshot();if("partition"===t.kind){const s=(e.openings||[]).filter(e=>"partition"===e.host?.kind&&e.host.id===t.id).sort((t,e)=>(t.host?.t||0)-(e.host?.t||0));if(s.length)return void(this.host._partitionDeleteDialog={id:t.id,openings:s.map(t=>JSON.parse(JSON.stringify(t)))})}const i="partition"===t.kind?"partitions":"column"===t.kind?"wall_columns":"room_drafts";e[i]=(e[i]||[]).filter(e=>e.id!==t.id),e[i].length||delete e[i],this.host._activeDraftId===t.id&&this._cancelPath(),this.host._physicalSel=null,this.host._physicalDialog=null,this._commitPhysicalGeometry(this.host._t("history.physical_delete"),s)},this._confirmPartitionDelete=()=>{const t=this.host._partitionDeleteDialog,e=this.host._curSpaceCfg;if(!t||!e)return;const s=this._geometrySnapshot();e.partitions=(e.partitions||[]).filter(e=>e.id!==t.id),e.partitions.length||delete e.partitions,e.openings=(e.openings||[]).filter(e=>"partition"!==e.host?.kind||e.host.id!==t.id),e.openings.length||delete e.openings,this.host._partitionDeleteDialog=null,this.host._physicalSel=null,this.host._physicalDialog=null,this._commitPhysicalGeometry(this.host._t("history.physical_delete"),s)},this._deleteDraftWhole=()=>{const t="draft"===this.host._physicalDialog?.kind?this.host._physicalDialog.id:"draft"===this.host._physicalSel?.kind?this.host._physicalSel.id:null,e=this.host._curSpaceCfg;if(!t||!e||!confirm(this.host._t("confirm.delete_draft")))return;const s=this._geometrySnapshot();e.room_drafts=(e.room_drafts||[]).filter(e=>e.id!==t),e.room_drafts.length||delete e.room_drafts,this.host._activeDraftId===t&&this._cancelPath(),this.host._physicalSel=null,this.host._physicalDialog=null,this._commitPhysicalGeometry(this.host._t("history.physical_delete"),s)},this._deleteDraftSegment=()=>{const t=this.host._physicalDialog,e=this.host._curSpaceCfg;if(!t||"draft"!==t.kind||!e)return;if(!confirm(this.host._t("confirm.delete_draft_segment")))return;const s=(e.room_drafts||[]).findIndex(e=>e.id===t.id);if(s<0)return;const i=e.room_drafts[s],o=Math.max(0,Math.min(i.segments.length-1,t.segment||0)),a=[],n=i.points.slice(0,o+1),r=i.points.slice(o+1);if(n.length>=2&&a.push({id:i.id,points:n,segments:i.segments.slice(0,o)}),r.length>=2&&a.push({id:a.length?`${i.id}-${Date.now().toString(36)}`:i.id,points:r,segments:i.segments.slice(o+1)}),2===a.length&&e.room_drafts.length>=200)return void this.host._showToast(this.host._t("toast.physical_limit"));const l=this._geometrySnapshot();e.room_drafts.splice(s,1,...a),e.room_drafts.length||delete e.room_drafts,this.host._activeDraftId===i.id&&this._cancelPath(),this.host._physicalDialog=null,this.host._physicalSel=a.length?{kind:"draft",id:a[0].id}:null,this._commitPhysicalGeometry(this.host._t("history.draft_segment_delete"),l)},this._confirmRoomDelete=t=>{const e=this.host._roomDeleteDialog,s=this.host._curSpaceCfg,i=this.host._spaceModel();if(!e||!s||!i)return;const o=i.rooms.find(t=>t.id===e.roomId);if(!o)return void(this.host._roomDeleteDialog=null);const a=this.host._openCuts(),n=nn(z(i.rooms,this.host._spaceWalls,a,this.host._wallKeyPitch,this.host._cellCm,this.host._gridPitch,ol).filter(t=>t.roomId===o.id),i.partitions,this.host._openingsR.map(t=>({...t,x:t.rx,y:t.ry})),.02*this.host._gridPitch),r=new Set(n.materialize.filter(t=>!t.reusePartitionId).map(t=>t.interval.key)).size;if(t&&(s.partitions||[]).length+r>al)return void this.host._showToast(this.host._t("toast.physical_limit"));const l=this._geometrySnapshot(),h=kt(i.rooms,this.host._spaceWalls,a,this.host._wallKeyPitch,this.host._cellCm,this.host._gridPitch,ol);if(t){s.partitions||=[];const t=new Map,e=new Map,i=t=>{const e=(s.wall_segments||[]).find(e=>{const s=[Number(e.a?.[0])*ol,Number(e.a?.[1])*ol],i=[Number(e.b?.[0])*ol,Number(e.b?.[1])*ol];return this._samePt(t.a,s)&&this._samePt(t.b,i)||this._samePt(t.a,i)&&this._samePt(t.b,s)});return"string"==typeof e?.id&&e.id?e.id:null},o=Date.now().toString(36);for(const a of n.materialize){let n=a.reusePartitionId?s.partitions.find(t=>t.id===a.reusePartitionId):e.get(a.interval.key);n||(n={id:i(a.interval)||`partition-room-${o}-${e.size}`,a:[a.interval.a[0]/ol,a.interval.a[1]/ol],b:[a.interval.b[0]/ol,a.interval.b[1]/ol],cm:a.interval.cm},s.partitions.push(n),e.set(a.interval.key,n)),t.set(a.interval.key,n)}for(const e of s.openings||[]){const s=n.openingIntervals.get(e.id),i=s?t.get(s):null,o=this.host._openingsR.find(t=>t.id===e.id);if(!i||!o)continue;const a={a:[i.a[0]*ol,i.a[1]*ol],b:[i.b[0]*ol,i.b[1]*ol]};e.host={kind:"partition",id:i.id,t:rn([o.rx,o.ry],a)}}}else if(n.removeOpeningIds.length){const t=new Set(n.removeOpeningIds);s.openings=(s.openings||[]).filter(e=>!t.has(e.id)),s.openings.length||delete s.openings}s.rooms=s.rooms.filter(t=>t.id!==o.id),this.host._cfgEpoch++;const c=this.host._normalizeWalls(h,this.host._openCuts());c.length?s.walls=c:delete s.walls,this.host._roomDeleteDialog=null,this._commitPhysicalGeometry(this.host._t(t?"history.delete_room_keep_walls":"history.delete_room_with_walls"),l),this.host._regSignature="",this.host._maybeRebuildDevices(),this.host.requestUpdate()},this._rebindPartitionOpening=()=>{const t=this.host._openingDialog;if(!t?.id)return;const e=t.id;this._activateMarkupTool("opening"),"opening"===this.host._tool&&(this.host._openingRebindId=e,this.host._openingPreset={type:t.type,lengthCm:t.lengthCm,flipH:t.flipH,flipV:t.flipV,revision:++this.host._openingPresetRevision},this.host._openingDialog=null,this.host._openingHoverCandidate=null)},this._keepClosedAsPartitions=()=>{if(this.host._wallFaceBatch)return void this._decideWallFace(!1);if(!this.host._contourClosed||this.host._pendingSplit||!this.host._curSpaceCfg)return;const t=this.host._curSpaceCfg,e=this.host._path.slice(0,-1);if((t.partitions||[]).length+e.length>al)return void this.host._showToast(this.host._t("toast.physical_limit"));const s=this._geometrySnapshot(),i=Ra(e.length,[...this.host._draftSegmentCms,this.host._closingWallCm??void 0],this.host._drawWallCm,A);t.partitions||=[];const o=Date.now().toString(36),a=this.host._activeDraftId?(t.room_drafts||[]).find(t=>t.id===this.host._activeDraftId):null,n=this._draftSegmentsForPath(this.host._path,a,i);for(let s=0;st.id!==this.host._activeDraftId),t.room_drafts.length||delete t.room_drafts),this._commitPhysicalGeometry(this.host._t("history.contour_to_partitions"),s),this.host._roomDialog=!1,this.host._path=[],delete this.host._resumeDraftBySpace[this.host._space],this.host._activeDraftId=null,this.host._draftSegmentCms=[],this.host._closingWallCm=null},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"}),e=this.host._spaceDialog;e&&(this.host._spaceDialog={...e,saved:t?.plans||[],savedBusy:!1})}catch(t){const e=this.host._spaceDialog;e&&(this.host._spaceDialog={...e,saved:[],savedBusy:!1}),this.host._showToast(this.host._t("toast.plans_list_failed",{err:this.host._errText(t)}))}}},this._openSettingsDialog=()=>{if(!this.host._norm)return;const t=this.host._glowRadiusCm,e=this.host._imperial?Math.round(t/30.48*10)/10:Math.round(t)/100;this.host._settingsDialog={colors:JSON.parse(JSON.stringify(this.host._fillColors)),glowRadius:e,bgColor:Dt(this.host._settings,{bgColor:null})||null,northDeg:Mt(this.host._settings,{}),bgMode:St(this.host._settings,{}),sunRays:xt(this.host._settings,{}),busy:!1}},this._openAlignDialog=()=>this._previewAlignDialog(!1),this._toggleOptimizeLivePositions=()=>{const t=this.host._alignDialog;t&&!t.busy&&t.report.liveMissingPositions.length&&this._previewAlignDialog(!t.removeLiveMissingPositions)},this._openBackupExport=()=>{this.host._settingsDialog=null,this.host._backupExportDialog={kind:"full",planOnly:!1,busy:!1,error:""}},this._openRulesDialog=()=>{if(!this.host._norm)return;const t=this.host._settings.icon_rules,e=(t&&t.length?t:Ct).map(t=>({...t}));this.host._rulesDialog={rules:e,test:"",busy:!1}},t._editorSecondary=new gn({root:()=>t.renderRoot,requestUpdate:()=>t.requestUpdate(),updateComplete:()=>t.updateComplete,clearTip:()=>{t._tip=null}}),t._resize=new Wo}_help(t){const e=`${t}.aria`,s=d(this.host.hass,this.host._config?.language);return Pt(s,t)&&Pt(s,e)?o``:a}_setMode(t,e=!0){this.host._endTabDrag(),this.host._pendingNavMode=null;const s=this.host._spaceModel();if(!s)return this.host._cancelModeTransition(!1),void(this.host._mode="view");if(this.host._kiosk&&"view"!==t)return;if(this.host._mode===t){if(!e&&"view"===t&&this.host._modeTransitionBusy){const e=this.host._currentModeVisual(t),s=this.host._modeTransitionTargetZoom,i=this.host._modeTransitionTargetCenterX,o=this.host._modeTransitionTargetCenterY;this.host._cancelModeTransition(!1),this.host._commitViewModeAtomic(e,s,i,o)}return}if(this.host._bootSoftCancel(),("plan"===t||"decor"===t)&&!this.host._norm)return void this.host._showToast(this.host._t("toast.markup_needs_server"));if(this.host._wallFaceBatch&&this._roomDialogCancel(),"plan"===this.host._mode&&"draw"===this.host._tool&&!this._finishWallChain())return;this.host._clearTransientHover(!0),this.host._cancelDevicePressFeedback();const i=this.host._mode,o=this.host._effectiveProjection(),a=this.host._currentModeVisual(i),n=this.host._modeTransitionBusy,r=a&&"view"!==i&&"view"!==t?{...a,toolbarContentOpacity:this.host._reducedMotion?1:.35}:a;this.host._cancelModeTransition(!1),this.host._editorSecondary.closeForNavigation(),(this.host._decorMove||this.host._dtDrag||this.host._bdDrag)&&this._cancelDecorGesture();const l=!s.bg&&"view"===t!=("view"===i),h=n&&"view"===i&&"view"!==t?this.host._modeTransitionEditorCamera:null;let c=h?.zoom??(n?this.host._modeTransitionTargetZoom:this.host._zoom),d=h?h.centerX:n?this.host._modeTransitionTargetCenterX:r?.viewport.centerX,p=h?h.centerY:n?this.host._modeTransitionTargetCenterY:r?.viewport.centerY;if("view"===i&&"view"!==t&&!n){const e=this.host._view;if(this.host._viewModeSnap={space:this.host._space,zoom:this.host._zoom,cx:e?this.host._logicalViewCenter(o)?.x:void 0,cy:e?this.host._logicalViewCenter(o)?.y:void 0},"iso"===o){const e=this.host._logicalViewCenter("iso");d=e?.x,p=e?.y,this.host._view=null,this.host._mode=t,this.host._applyView(this.host._zoom,e?.x,e?.y)}l&&(c=1,d=void 0,p=void 0)}if("plan"===i&&this.host._activeDraftId&&(this.host._resumeDraftBySpace[this.host._space]=this.host._activeDraftId),this.host._mode=t,"devices"===i&&"devices"!==t&&(this.host._showHidden=!1,this.host._deviceInbox=null,this.host._deviceInboxReturn=null,this.host._deviceInboxMemo=null),this.host._editorChromeMode="view"===t?i:t,"view"===t){"view"===i||n||(this.host._modeTransitionEditorCamera={zoom:this.host._zoom,centerX:r?.viewport.centerX,centerY:r?.viewport.centerY});const t=this.host._viewModeSnap;if(t&&t.space===this.host._space){c=t.zoom;const e=this.host._labsIso&&"iso"===this.host._viewPreference[this.host._space]?"iso":"flat",s=null!=t.cx&&null!=t.cy&&"iso"===e?It([t.cx,t.cy],0):null;d=s?.[0]??t.cx,p=s?.[1]??t.cy}else t&&(c=this.host._zoomBySpace[this.host._space]||1,d=void 0,p=void 0)}else this.host._modeTransitionEditorCamera={zoom:c,centerX:d,centerY:p};this.host._modeTransitionTargetZoom=c,this.host._modeTransitionTargetCenterX=d,this.host._modeTransitionTargetCenterY=p,("iso"===o||"view"===t&&this.host._labsIso&&"iso"===this.host._viewPreference[this.host._space])&&(this.host._modeTransitionForceAtomic=!0);const _=++this.host._modeTransitionRequest;e?r?(this.host._modeTransitionPreparing=!0,this.host._modeTransitionVisual=r,this.host._prepareModeTransition(_,r,t,c,d,p)):(this.host._modeTransitionPreparing=!1,this.host._modeTransitionVisual=null,this.host._zoom=c,this.host._view=null,requestAnimationFrame(()=>{this.host.isConnected&&_===this.host._modeTransitionRequest&&this.host._mode===t&&(this.host._applyView(c,d,p),"view"===t&&(this.host._viewModeSnap=null,this.host._modeTransitionEditorCamera=null),this.host.requestUpdate())})):"view"===t&&this.host._commitViewModeAtomic(r,c,d,p),this.host._path=[],this._clearPlanSnapHover(),this._clearOpeningPlacement(!0),this.host._tool="draw",this.host._mergeSel=null,this.host._mergeDialog=null,this.host._splitSel=null,this.host._pendingSplit=null,this.host._selId=null,this.host._physicalSel=null,this.host._physicalDialog=null,this.host._physicalDrag=null,this._rszResetController(),this.host._tip=null,this.host._hoverRoom=null,this.host._decorDraft=null,this.host._decorSel=null,this.host._decorMove=null,this.host._backdropDialog=null,this.host._decorTool="select",this.host._bdDrag=null,this.host._dtDrag=null,this.host._dtBox=null,"plan"===t&&(this._primeDrawWallField(),this._resumeLastDraft()),this.host._saveNav()}_primeDrawWallField(){null===this.host._drawWallField&&(this.host._drawWallField=Tt(A,this.host._imperial))}_showPhysicalRange(t=this.host._drawWallMaxCm,e=0){this.host._showToast(this.host._t("toast.physical_range",{min:Tt(e,this.host._imperial),max:Tt(t,this.host._imperial),unit:this.host._t(this.host._imperial?"wallthick.unit_in":"wallthick.unit_cm")}))}_draftSegmentCount(t=this.host._curSpaceCfg){return(t?.room_drafts||[]).reduce((t,e)=>t+(Array.isArray(e.segments)?e.segments.length:0),0)}_mergeSpacePartitions(t,e){const s=t?.partitions||[];if(s.length<2)return 0;const i=Rn(s,{pitch:I,seedIds:e,geometry:Nn(t,{excludeDraftId:this.host._activeDraftId})});return i.merged?(t.partitions=i.partitions,Fn(t.openings,t.partitions,i.openingMoves,{coordScale:ol,cellCm:this.host._cellCm,gridPitch:this.host._gridPitch}),i.merged):0}_finishWallChain(){if("draw"!==this.host._tool||this.host._wallFaceBatch||this.host._roomDialog)return!0;const t=this.host._curSpaceCfg;if(!t||this.host._path.length<2)return this.host._path=[],this.host._activeDraftId=null,this.host._draftSegmentCms=[],this.host._closingWallCm=null,this._clearPlanSnapHover(),!0;const e=Fa(this.host._path,Ra(this.host._path.length-1,this.host._draftSegmentCms,this.host._drawWallCm,A)),s=e.length;if((t.partitions||[]).length+s>al)return this.host._showToast(this.host._t("toast.physical_limit")),!1;const i=this._geometrySnapshot();t.partitions||=[];const o=Date.now().toString(36),a=[],n=this.host._activeDraftId?(t.room_drafts||[]).find(t=>t.id===this.host._activeDraftId):null,r=this._draftSegmentsForPath(this.host._path,n,e.map(t=>t.cm));for(let i=0;it.id!==this.host._activeDraftId),t.room_drafts.length||delete t.room_drafts),this._commitPhysicalGeometry(this.host._t("history.wall_chain_finish"),i),delete this.host._resumeDraftBySpace[this.host._space],this.host._path=[],this.host._activeDraftId=null,this.host._draftSegmentCms=[],this.host._closingWallCm=null,this._clearPlanSnapHover(),!0}_activateMarkupTool(t){t!==this.host._tool&&(this.host._wallFaceBatch&&this._roomDialogCancel(),("draw"!==this.host._tool||this._finishWallChain())&&(this._cancelPath(),"resize"===this.host._tool&&(this.host._resize.dragging?this._rszCancelDrag():this.host._resize.reset()),this.host._tool=t,"draw"===t&&this._resumeLastDraft(),"resize"===t&&this.host._resize.selectRoom(null),"wallthick"===t&&(this.host._wallDialog=null)))}_limitReached(t){const e=this.host._curSpaceCfg;if(!e)return!0;const s="draft"===t?(e.room_drafts||[]).length>=200:"partition"===t?(e.partitions||[]).length>=al:(e.wall_columns||[]).length>=500;return s&&this.host._showToast(this.host._t("toast.physical_limit")),s}_svgPoint(t){const e=this.host.renderRoot.querySelector(".stage").getBoundingClientRect();return this.host._screenToVb(t.clientX-e.left,t.clientY-e.top)}_snap(t){const e=this.host._gridPitch;return[Rt(Ft(t[0],e)),Rt(Ft(t[1],e))]}_snapDrawPoint(t,e=!1){const s=this.host._path[this.host._path.length-1],i=e&&s?Nt(s,t,this.host._gridPitch,Ti):t;return this._snap(i)}_planSnapOpeningCuts(t,e){if(!this.host._openingsR.length)return[];const s=this.host._openingWallIndexFor(t,e).value,i=[];for(const e of this.host._roomWallOpeningInputs(this.host._openingsR,t)){const t=W(s,e);if(!t.negative&&!t.positive)continue;const o=e.angle*Math.PI/180,a=Math.cos(o)*e.length/2,n=Math.sin(o)*e.length/2;i.push([e.x-a,e.y-n,e.x+a,e.y+n])}return i}_planSnapGeometrySnapshot(){const t=this.host._spaceModel();if(!t)return{key:`${this.host._space}|empty`,value:{segments:[],endpoints:[]}};const e=[this.host._space,this.host._cfgEpoch,this.host._activeDraftId||"",t.rooms.length,t.room_drafts.length,t.partitions.length].join("|");if(this.host._planSnapGeometryCache?.key===e)return this.host._planSnapGeometryCache;const s=this.host._openCuts(),i=xa({space:t,activeDraftId:this.host._activeDraftId,roomCuts:this._planSnapOpeningCuts(t,s),partitionCuts:this._partitionOpeningCuts(t),epsilon:2e-4*this.host._gridPitch});return this.host._planSnapGeometryCache={key:e,value:i},this.host._planSnapGeometryCache}_hiddenWallDiagnosticSnapshot(){const t=this.host._spaceModel();if(!t)return{key:`${this.host._space}|hidden-empty`,value:{segments:[],endpoints:[]}};const e=["hidden",this.host._space,this.host._cfgEpoch,this.host._activeDraftId||"",t.rooms.length,t.room_drafts.length,t.partitions.length].join("|");if(this.host._hiddenWallDiagnosticCache?.key===e)return this.host._hiddenWallDiagnosticCache;const s=ka({space:t,activeDraftId:this.host._activeDraftId,epsilon:2e-4*this.host._gridPitch});return this.host._hiddenWallDiagnosticCache={key:e,value:s},this.host._hiddenWallDiagnosticCache}_planStructuralGeometrySnapshot(){const t=this.host._spaceModel();if(!t)return{key:`${this.host._space}|structural-empty`,value:{segments:[],endpoints:[]}};const e=["structural",this.host._space,this.host._cfgEpoch,this.host._activeDraftId||"",t.rooms.length,t.room_drafts.length,t.partitions.length].join("|");if(this.host._planStructuralGeometryCache?.key===e)return this.host._planStructuralGeometryCache;const s=xa({space:t,activeDraftId:this.host._activeDraftId,epsilon:2e-4*this.host._gridPitch});return this.host._planStructuralGeometryCache={key:e,value:s},this.host._planStructuralGeometryCache}_planSnapContextKey(t){const e=this.host._path[0],s=this.host._path[this.host._path.length-1];return[t,this.host._tool,this.host._path.length,e?`${e[0]},${e[1]}`:"",s?`${s[0]},${s[1]}`:""].join("|")}_resolvePlanDrawPoint(t,e){const s=this._planSnapGeometrySnapshot(),i=this.host._path[this.host._path.length-1],o="draw"===this.host._tool&&this.host._path.length>=3?[{point:this.host._path[0],key:"closure:first-point"}]:[],a={tolerance:this._cssPxToRender(12),distinguishTolerance:this._cssPxToRender(8),gridStep:this.host._gridPitch,excludePoints:i?[i]:[],extraEndpoints:o,epsilon:2e-4*this.host._gridPitch},n=e&&i?Ia(s.value,t,{...a,anchor:i}):function(t,e,s){if(!(ma(e)&&s.tolerance>=0))return{kind:"none",candidate:null,conflicts:[]};const i=s.epsilon??ua,o=s.excludePoints||[],a=Ca(Da(t,e,s),s.distinguishTolerance||0);if(a)return a;let n=null;for(const a of t.segments){const t=[a.a[0],a.a[1],a.b[0],a.b[1]],r=C([e[0],e[1]],t);if(r>s.tolerance)continue;const l=Pa(a,e,s.gridStep);!Sa(l,o,i)&&Ma(r,a.key,n)&&(n={kind:"line",point:l,key:a.key,distance:r,segment:a})}return n?{kind:"resolved",candidate:n,conflicts:[]}:{kind:"none",candidate:null,conflicts:[]}}(s.value,t,a),r="resolved"===n.kind?n.candidate:null,l=r?[...r.point]:this._snapDrawPoint(t,e),h=i?zt(i,l):l;return{point:h,candidate:r&&q(h,r.point)?r:null,conflicts:"ambiguous"===n.kind?n.conflicts:[],ambiguous:"ambiguous"===n.kind,contextKey:this._planSnapContextKey(s.key)}}_clearPlanSnapHover(t=!0){this.host._planSnapHover=null,this._syncPlanSnapActiveMarker(null),this._syncPlanSnapConflictMarkers([]),t&&(this.host._cursorPt=null)}_samePt(t,e){return q(t,e)}_dropLegacySegments(){for(const t of this.host._serverCfg?.markers||[])"ripple"===t.display&&(t.display="icon_ripple");for(const t of this.host._serverCfg?.spaces||[]){delete t.segments;const e=new Set,s=t=>"string"==typeof t&&t.length>=1&&t.length<=64&&!e.has(t),i=t=>(e.add(t),!0),o=t=>Array.isArray(t)&&2===t.length&&t.every(t=>Number.isFinite(Number(t))&&Math.abs(Number(t))<=Xt);if(Array.isArray(t.partitions)&&(t.partitions=t.partitions.filter(t=>t&&s(t.id)&&o(t.a)&&o(t.b)&&Math.hypot(t.a[0]-t.b[0],t.a[1]-t.b[1])>1e-9&&i(t.id)).map(t=>({...t,cm:Number.isFinite(Number(t.cm))?Math.max(0,Math.min(100,Number(t.cm))):15})),t.partitions.length||delete t.partitions),Array.isArray(t.wall_columns)&&(t.wall_columns=t.wall_columns.filter(t=>t&&s(t.id)&&o(t.center)&&i(t.id)).map(t=>({id:t.id,shape:"circle"===t.shape?"circle":"square",center:[Number(t.center[0]),Number(t.center[1])],cm:wt(Number(t.cm)||15),..."circle"===t.shape?{}:{angle:At(t.angle)}})),t.wall_columns.length||delete t.wall_columns),Array.isArray(t.room_drafts)&&(t.room_drafts=t.room_drafts.filter(t=>t&&s(t.id)&&Array.isArray(t.points)&&t.points.length>=2&&t.points.every(o)&&i(t.id)).map(t=>{const e=hr(t);return{id:t.id,...e}}).filter(t=>t.points.length>=2),t.room_drafts.length||delete t.room_drafts),Array.isArray(t.walls)){const e=Ot(ut({spaces:[t]}),t.id),s=e?Lt(t,e,ol,.02*nt).contour.map(t=>t.map(t=>t/ol)):V(t.open_spans).map(t=>[t.a[0],t.a[1],t.b[0],t.b[1]]);t.walls=rt(t.walls,t.rooms||[],I,1,s),t.walls.length||delete t.walls}}}_rollbackRejectedPhysicalWrites(t){if(!this.host._serverCfg||!t.length)return!1;let e=!1;for(const[s]of t){const t=this.host._pendingPhysicalWrites.get(s);t&&(e=this._restoreGeometryStateInConfig(this.host._serverCfg,t.before)||e,this.host._pendingPhysicalWrites.delete(s))}return!!e&&(this._clearGeometryGesture(),this.host._geometryHistory.clear(),this.host._cfgEpoch++,this.host._modelCache=null,this.host._wallUnionCache=null,this.host._physicalBodiesCache=null,this.host._frame=null,this.host._regSignature="",this.host._cfgContentFingerprint=Et(this.host._serverCfg),this.host._maybeRebuildDevices(),this.host.requestUpdate(),!0)}async _reloadRejectedPhysicalWrite(){const t=this.host._writeChain;await t.catch(()=>{}),await this.host._reloadConfigOnly(!0)}_writeConfig(){this.host._writesPending++,this.host._writeChain=Bt(this.host._writeChain,async()=>{if(!this.host._serverCfg)return;this._dropLegacySegments();const t=Mi(this.host._serverCfg),e=Et(t),s=[...this.host._pendingPhysicalWrites.entries()];for(const[e,i]of s){const s=t.spaces.find(t=>t.id===e),o=jt(s);if(o===i.fingerprint)continue;let a=!1;try{a=this._checkSpacePhysicalGeometry(t,e).ok}catch{a=!1}if(!a)throw this._restoreGeometryStateLocal(i.before),this.host._pendingPhysicalWrites.delete(e),this.host._geometryHistory.clear(),this.host._showToast(this.host._t("toast.geometry_unsafe")),Object.assign(new Error("unsafe wall geometry"),{code:"geometry-unsafe"});i.fingerprint=o}e!==Et(this.host._serverCfg)&&(this.host._serverCfg=t),this.host._cfgContentFingerprint=e;try{await this.host._sendConfigCandidate(t)}catch(t){if(!this._rollbackRejectedPhysicalWrites(s))throw t;const e=t&&"object"==typeof t?t:Object.assign(new Error(String(t)),{cause:t});throw e.physicalGeometryRolledBack=!0,e}for(const[t,e]of s)this.host._pendingPhysicalWrites.get(t)?.fingerprint===e.fingerprint&&this.host._pendingPhysicalWrites.delete(t)});return this.host._writeChain.finally(()=>{this.host._writesPending--})}_saveConfig(){this.host._cfgEpoch++,this.host._saveConfigDebounced()}_geometrySnapshotFromConfig(t,e){const s=t?.spaces?.find(t=>t.id===e);if(!s)return null;const i=t=>JSON.parse(JSON.stringify(t)),o={};for(const t of["plan_x","plan_y","plan_scale","plan_scale_x","plan_scale_y","plan_angle"])void 0!==s[t]&&(o[t]=s[t]);return{spaceId:e,rooms:i(s.rooms||[]),...Array.isArray(s.openings)?{openings:i(s.openings)}:{},...Array.isArray(s.walls)?{walls:i(s.walls)}:{},...Array.isArray(s.wall_segments)?{wall_segments:i(s.wall_segments)}:{},...Array.isArray(s.open_spans)?{open_spans:i(s.open_spans)}:{},...Array.isArray(s.room_drafts)?{room_drafts:i(s.room_drafts)}:{},...Array.isArray(s.partitions)?{partitions:i(s.partitions)}:{},...Array.isArray(s.wall_columns)?{wall_columns:i(s.wall_columns)}:{},...Array.isArray(s.decor)?{decor:i(s.decor)}:{},plan_transform:o}}_geometrySnapshot(t=this.host._space){return this._geometrySnapshotFromConfig(this.host._serverCfg,t)}_recordGeometry(t,e){if(!e)return;const s=this._geometrySnapshot(e.spaceId);s&&JSON.stringify(e)!==JSON.stringify(s)&&(this.host._geometryHistory.push({name:t,before:e,after:s}),this.host.requestUpdate())}_restoreGeometryStateInConfig(t,e,s=!1){const i=t?.spaces?.find(t=>t.id===e.spaceId);if(!i)return!1;const o=t=>JSON.parse(JSON.stringify(t)),a=new Map((i.rooms||[]).map(t=>[t.id,t]));if(i.rooms=o(e.rooms),s)for(const t of i.rooms){const e=a.get(t.id);!Array.isArray(t.wall_ids)&&Array.isArray(e?.wall_ids)&&e.wall_ids.length===t.poly?.length&&(t.wall_ids=o(e.wall_ids))}const n=(t,e)=>{void 0!==e?i[t]=o(e):s&&"wall_segments"===t||delete i[t]},r=new Map((i.openings||[]).map(t=>[t.id,t])),l=new Map((i.room_drafts||[]).map(t=>[t.id,t]));if(n("openings",e.openings),n("walls",e.walls),n("wall_segments",e.wall_segments),n("open_spans",e.open_spans),n("room_drafts",e.room_drafts),n("partitions",e.partitions),n("wall_columns",e.wall_columns),n("decor",e.decor),s){for(const t of i.openings||[]){const e=r.get(t.id);t.host||"wall"!==e?.host?.kind||(t.host=o(e.host))}for(const t of i.room_drafts||[]){const e=l.get(t.id);for(let s=0;s<(t.segments||[]).length;s++)!t.segments[s]?.id&&e?.segments?.[s]?.id&&(t.segments[s].id=e.segments[s].id)}}for(const t of["plan_x","plan_y","plan_scale","plan_scale_x","plan_scale_y","plan_angle"])delete i[t];return Object.assign(i,o(e.plan_transform||{})),!0}_restoreGeometryStateLocal(t){return!!this._restoreGeometryStateInConfig(this.host._serverCfg,t)&&(this.host._cfgEpoch++,this.host._modelCache=null,this.host._wallUnionCache=null,this.host._physicalBodiesCache=null,this.host._frame=null,this.host.requestUpdate(),!0)}_wallModelBlockerLabel(t){const e=t instanceof er?t.reason:"invalid-room";return this.host._t(`wall_model.reason.${e}`)}_hasLegacyZeroWallFields(t=this.host._serverCfg){return(t?.spaces||[]).some(t=>Array.isArray(t?.open_spans)&&t.open_spans.length>0||(t?.rooms||[]).some(t=>Array.isArray(t?.open_to)&&t.open_to.length>0))}_showWallModelMigrationBlocked(t){const e=this._hasLegacyZeroWallFields()?"toast.zero_wall_migration_blocked":"toast.wall_model_migration_blocked";this.host._showToast(this.host._t(e,{reason:this._wallModelBlockerLabel(t)}))}_limitSegmentsOf(t){const e=[];for(const s of t?.wall_segments||[])s?.a&&s?.b&&e.push({id:String(s.id||""),a:s.a,b:s.b,cm:Number(s.cm)});for(const s of t?.partitions||[])s?.a&&s?.b&&e.push({id:String(s.id||""),a:s.a,b:s.b,cm:Number(s.cm)});for(const s of t?.room_drafts||[]){const t=Array.isArray(s?.points)?s.points:[],i=Array.isArray(s?.segments)?s.segments:[];for(let o=0;o+1t?.id===e);if(!i)return[];const o=Number(i.cell_cm)>0?Number(i.cell_cm):5,a=this._limitSegmentsOf(i),n=[...ta(a),...aa(a,o,I),...ra(a,o,I)];let r=null;try{r=Ht(i.rooms||[],i.walls||[],[],I,o,I,1)}catch{r=null}let l="ok"===s?.status||"degraded-extra"===s?.status?s.roomGeom:null;if(!l&&r&&r.nodes.length)try{const t=Gt(i.rooms||[],i.walls||[],[],[],I,o,I,1);l="ok"===t?.status||"degraded-extra"===t?.status?t.roomGeom:null}catch{l=null}for(const t of i.rooms||[]){const e=String(t?.id||"");if(!e)continue;let s=null;try{s=Wt(i.rooms||[],e,i.walls||[],[],I,o,I,1,l??void 0,r)}catch{s=null}n.push(...la(e,s,o,I))}return n}_junctionLimitLabel(t){const e=t=>String(Math.round(10*t)/10);return this.host._t(`junction.limit_${t.rule}`,{actual:e(t.actual),limit:e(t.limit)})}_junctionLimitsIntroduced(t,e,s,i){let o;const a=(e?.spaces||[]).find(t=>t?.id===s);let n="";try{n=jt(a)}catch{n=""}const r=e&&"object"==typeof e?this._junctionBaselineCache.get(e):void 0;if(r&&n&&r.fingerprint===n&&r.spaceId===s&&(o=r.violations),!o){try{const t=Number(e?.model_version||0)>=9?e:Mr(e).config;o=this._junctionLimitViolations(t,s)}catch{return[]}e&&"object"==typeof e&&n&&this._junctionBaselineCache.set(e,{spaceId:s,fingerprint:n,violations:o})}let l=[];try{l=this._junctionLimitViolations(t,s,i)}catch{return[{rule:"check_failed",subject:s,actual:0,limit:0}]}return function(t,e){const s=new Map;for(const t of e||[])s.set(t.rule,(s.get(t.rule)||0)+1);const i=new Map;for(const e of t||[])i.set(e.rule,[...i.get(e.rule)||[],e]);const o=[];for(const[t,e]of i){const i=e.length-(s.get(t)||0);i>0&&o.push(...e.slice(0,i))}return o}(l,o)}_commitPhysicalGeometry(t,e,s=[]){if(!e||!this.host._serverCfg)return!1;const i=this.host._serverCfg,o=this._geometrySnapshotFromConfig(i,e.spaceId),a=i.spaces.find(t=>t.id===e.spaceId);if(!a||jt(e)===jt(a))return!1;let n,r=!1;try{r=this._checkSpacePhysicalGeometry(i,e.spaceId).ok}catch{r=!1}if(!r)return this._clearGeometryGesture(),this._restoreGeometryStateLocal(e),this.host._showToast(this.host._t("toast.geometry_unsafe")),!1;let l=e;try{if(Number(i.model_version||0)<9){const t=JSON.parse(JSON.stringify(i));if(!this._restoreGeometryStateInConfig(t,e))throw new er("invalid-room",e.spaceId);const s=Mr(t).config;l=this._geometrySnapshotFromConfig(s,e.spaceId)||e;const a=JSON.parse(JSON.stringify(s));if(!o||!this._restoreGeometryStateInConfig(a,o,!0))throw new er("invalid-room",e.spaceId);const r=s.spaces.find(t=>t.id===e.spaceId),h=a.spaces.find(t=>t.id===e.spaceId);if(!Array.isArray(o.wall_segments)&&JSON.stringify(e.rooms)===JSON.stringify(o.rooms))for(const t of h?.wall_segments||[])t.cm=Q(o.walls,t.a,t.b,I,1);const c=((t,e,s)=>{const i=new Map,o=new Map((t?.rooms||[]).map(t=>[t.id,t])),a=new Map((e||[]).map(t=>[t.id,t])),{rooms:n}=$r(s,yr(t));for(const t of n){const e=a.get(t.id),n=o.get(t.id),r=R(e),l=(s.rooms||[]).find(e=>e.id===t.id),h=R(l),c=R(n);if(r&&h&&c&&r.length===h.length&&c.length===n?.wall_ids?.length&&t.poly.length===t.wall_ids.length)for(let e=0;e{const o=[];for(let a=0;atr||pr(r,s,i)>tr)continue;const l=dr(n,s,i),h=dr(r,s,i);l<-tr||h>1+tr||h<=l+tr||o.push({start:l,id:e[a]})}return o.sort((t,e)=>t.start-e.start)},o=s(c,n.wall_ids,r[e],r[(e+1)%r.length]),a=s(t.poly,t.wall_ids,h[e],h[(e+1)%h.length]);if(o.length&&o.length===a.length)for(let t=0;tt.id===e.spaceId),i=this.host._path.length>=2?this.host._path.map(t=>[t[0]/ol,t[1]/ol]):[];i.push(...s.map(t=>[t[0],t[1]])),h=cr(t)<=cr(l,i)&&this._checkSpacePhysicalGeometry(n,e.spaceId).ok}catch{h=!1}if(!h)return this._clearGeometryGesture(),this._restoreGeometryStateLocal(e),this.host._showToast(this.host._t("toast.geometry_unsafe")),!1;const c=JSON.parse(JSON.stringify(i));this._restoreGeometryStateInConfig(c,e);const d=this._junctionLimitsIntroduced(n,c,e.spaceId);if(d.length)return this._clearGeometryGesture(),this._restoreGeometryStateLocal(e),this.host._showToast(this._junctionLimitLabel(d[0])),!1;Cr(i,n),this._recordGeometry(t,l);const p=i.spaces.find(t=>t.id===e.spaceId),_=this.host._pendingPhysicalWrites.get(e.spaceId);return this.host._pendingPhysicalWrites.set(e.spaceId,{before:_?.before||l,fingerprint:jt(p)}),this._saveConfig(),!0}_clearGeometryGesture(){this.host._path=[],this._clearPlanSnapHover(),this._clearOpeningPlacement(!1),this.host._mergeSel=null,this.host._mergeDialog=null,this.host._splitSel=null,this.host._pendingSplit=null,this.host._wallFaceBatch=null,this.host._wallRepairDiagnostic=null,this.host._roomDeleteDialog=null,this.host._wallDialog=null,this.host._physicalDialog=null,this.host._physicalSel=null,this.host._physicalDrag=null,this.host._physicalRotate=null,this.host._activeDraftId=null,this.host._draftSegmentCms=[],this.host._closingWallCm=null,this.host._openingDialog=null,this._rszResetController(),this.host._decorDraft=null,this.host._decorMove=null,this.host._dtDrag=null,this.host._bdDrag=null}_stagePointerCancel(t){if(clearTimeout(this.host._kioskHoldTimer),this.host._swipeStart?.id===t.pointerId&&(this.host._swipeStart=null),this.host._physicalDrag?.pid===t.pointerId||this.host._physicalRotate?.pid===t.pointerId)return void this._cancelPhysicalGesture();if(this.host._decorDraft?.pid===t.pointerId)return this.host._decorDraft=null,void this.host.requestUpdate();if(this.host._decorMove?.pid===t.pointerId||this.host._dtDrag?.pid===t.pointerId||this.host._bdDrag?.pid===t.pointerId)return void this._cancelDecorGesture();"opening"===this.host._tool?(this.host._cursorPt=null,this._clearOpeningPlacement(!1)):"draw"===this.host._tool&&this._clearPlanSnapHover();const e=!!this.host._pinchStart||!!this.host._panStart;this.host._pointers.delete(t.pointerId),this.host._pointers.size<2&&(this.host._pinchStart=null),0===this.host._pointers.size&&(this.host._panStart=null,this.host._panLock=null),e&&0===this.host._pointers.size&&this.host.requestUpdate()}_applyGeometryState(t,e=!1){if(!this.host._canCommitSpace(t.spaceId))return!1;const s=this._geometrySnapshot(t.spaceId);if(!s||!this._restoreGeometryStateLocal(t))return!1;const i=this.host._serverCfg,o=jt(s)!==jt(t);if(o){let o=!1;try{const s=i?this._checkSpacePhysicalGeometry(i,t.spaceId):null;o=!!s?.ok||!(!e||"wall-degraded-extra"!==s?.reason)}catch{o=!1}if(!o)return this._restoreGeometryStateLocal(s),this.host._showToast(this.host._t("toast.geometry_unsafe")),!1}let a;try{a=Mr(i).config}catch(t){return this._restoreGeometryStateLocal(s),this._showWallModelMigrationBlocked(t),!1}if(o){let o=!1;try{const s=a?this._checkSpacePhysicalGeometry(a,t.spaceId):null;o=!!s?.ok||!(!e||"wall-degraded-extra"!==s?.reason)}catch{o=!1}if(!o)return this._restoreGeometryStateLocal(s),this.host._showToast(this.host._t("toast.geometry_unsafe")),!1;Cr(i,a);const n=i?.spaces.find(e=>e.id===t.spaceId),r=this.host._pendingPhysicalWrites.get(t.spaceId);this.host._pendingPhysicalWrites.set(t.spaceId,{before:r?.before||s,fingerprint:jt(n)})}else Cr(i,a);return this._clearGeometryGesture(),this.host._space!==t.spaceId&&(this.host._commitSpace(t.spaceId),this.host._saveNav(),this.host._restoreZoom()),this.host._modelCache=null,this.host._frame=null,this.host._regSignature="",this.host._maybeRebuildDevices(),this._saveConfig(),this.host.requestUpdate(),!0}_roomAt(t){return this.host._spaceModel()?.rooms.find(e=>{const s=R(e);return!!s&&qt(t,s)})}_overlapRoom(t){return this.host._spaceModel()?.rooms.find(e=>{const s=R(e);return!!s&&$(t,s)})}_pointInRoom(t,e){return e.poly?Ut(t,e.poly):null!=e.x&&t[0]>=e.x&&t[0]<=e.x+e.w&&t[1]>=e.y&&t[1]<=e.y+e.h}_contourSelfIntersects(t){if(!go(t))return!0;const e=t.length,s=.001;for(let i=0;i=500)return this.host._showToast(this.host._t("toast.physical_limit")),!1;const t=this.host._curSpaceCfg;return!(!this.host._activeDraftId&&(t?.room_drafts||[]).length>=200||this._draftSegmentCount(t)>=2e3||(t?.partitions||[]).length+this.host._path.length>al)||(this.host._showToast(this.host._t("toast.physical_limit")),!1)}_markupClick(t){if(this.host._vacFit)return;if(!this.host._markup)return;const e=this.host._spaceModel();if(!e)return void this._clearGeometryGesture();if(this.host._suppressClick)return;if(this.host._drag||this.host._rlResize)return;const s=t.composedPath?.()||[];if(s.some(t=>t?.classList?.contains?.("roomlabel")||t?.classList?.contains?.("rlhandle")))return;if(s.some(t=>t?.classList?.contains?.("physical-hit")))return;const i=this._svgPoint(t);if("select"===this.host._tool)return void(this.host._physicalSel=null);if("resize"===this.host._tool){if(this.host._resize.dragging||s.some(t=>t?.classList?.contains?.("rszhandle")))return;const t=[...e.rooms].reverse().find(t=>this._pointInRoom(i,t));return this.host._resize.selectRoom(t?.id||null),void this.host.requestUpdate()}if("delroom"===this.host._tool)return void this._deleteRoomClick(i);if("opening"===this.host._tool)return void this._openingClick(i);if("merge"===this.host._tool)return void this._mergeClick(i);if("wallthick"===this.host._tool)return void this._wallThickClick(i);if("split"===this.host._tool)return void this._splitClick(i);if("column"===this.host._tool)return void this._columnClick(i);this.host._wallRepairDiagnostic=null;const o=this._resolvePlanDrawPoint(i,t.shiftKey);if(o.ambiguous)return this.host._planSnapHover={contextKey:o.contextKey,candidate:null,conflicts:o.conflicts},this._syncPlanSnapActiveMarker(null),this._syncPlanSnapConflictMarkers(o.conflicts),void this.host._showToast(this.host._t("toast.plan_snap_ambiguous"));let a=o.point;if(t.ctrlKey||t.metaKey){if(t.preventDefault(),this.host._path.length<3)return;a=zt(this.host._path[this.host._path.length-1],this.host._path[0])}if(!this.host._path.length){if(!t.shiftKey&&!o.candidate&&this._offerExistingWallFace(i))return;const e=this._draftEndAt(a);return e?(this.host._activeDraftId=e.draft.id,this.host._resumeDraftBySpace[this.host._space]=e.draft.id,this.host._path=e.reverse?[...e.draft.points].reverse().map(t=>[...t]):e.draft.points.map(t=>[...t]),void(this.host._draftSegmentCms=this._adoptDraftCms(this.host._path,e.reverse?[...e.draft.segments].reverse().map(t=>t.cm):e.draft.segments.map(t=>t.cm),e.draft.id))):(this.host._activeDraftId=null,this.host._draftSegmentCms=[],void(this.host._path=[a]))}const n=this.host._path[this.host._path.length-1];if(this._samePt(a,n))return;const r=this._draftEndAt(a,this.host._activeDraftId||void 0);if(r)return void this._mergeDraftEndpoint(r);if(!this._canAppendRoomDraftPoint())return;const l=this.host._drawWallCm;if(null==l)return void this._showPhysicalRange(100);const h=this.host._path.map(t=>[...t]);this.host._path=[...this.host._path,a],this.host._draftSegmentCms=[...this.host._draftSegmentCms,l],this._persistActiveDraftSegment(),this._offerWallFaces(h)}_draftEndAt(t,e){const s=this.host._spaceModel();if(!s)return null;const i=this.host._viewOr(this.host._baseVb()),o=Math.max(.15*this.host._gridPitch,this.host._stageEl?.clientWidth?i.w/this.host._stageEl.clientWidth*12:0);for(const i of s.room_drafts||[]){if(i.id===e)continue;if(i.points.length<2)continue;const s=i.points[0],a=i.points[i.points.length-1];if(Math.hypot(t[0]-a[0],t[1]-a[1])<=o)return{draft:i,reverse:!1};if(Math.hypot(t[0]-s[0],t[1]-s[1])<=o)return{draft:i,reverse:!0}}return null}_mergeDraftEndpoint(t){const e=this.host._curSpaceCfg;if(!e||!this.host._path.length)return;const s=this.host._path.map(t=>[...t]),i=this._wallGraphSources(s),o=Array.isArray(e.room_drafts)?e.room_drafts:[],a=this.host._activeDraftId?o.find(t=>t.id===this.host._activeDraftId):null,n=o.find(e=>e.id===t.draft.id);if(!n)return;const r=t.reverse?t.draft.points.map(t=>[...t]):[...t.draft.points].reverse().map(t=>[...t]),l=t.reverse?(n.segments||[]).map(t=>({...t})):[...n.segments||[]].reverse().map(t=>({...t})),h=this.host._path[this.host._path.length-1],c=this._samePt(h,r[0]),d=c?null:this.host._drawWallCm;if(!c&&null==d)return void this._showPhysicalRange(100);const p=this._draftSegmentsForPath(this.host._path,a,this.host._draftSegmentCms),_=[...this.host._path.map(t=>[...t]),...c?r.slice(1):r],u=[...p,...null==d?[]:[{cm:d}],...l],m=_.length>=4&&this._samePt(_[0],_[_.length-1]),g=_,f=u;if(g.length>500)return void this.host._showToast(this.host._t("toast.physical_limit"));const b=(a?.segments?.length||0)+(n.segments?.length||0);if(this._draftSegmentCount(e)-b+f.length>2e3)return void this.host._showToast(this.host._t("toast.physical_limit"));if(m){const t=g.slice(0,-1);if(this._contourSelfIntersects(t)||k(t)<=1e-6)return void this.host._showToast(this.host._t("toast.contour_cannot_close"));const e=this._overlapRoom(t);if(e)return void this.host._showToast(this.host._t("toast.room_overlap",{name:e.name||""}))}const y=this._geometrySnapshot(),v=this.host._activeDraftId||t.draft.id,$={...a||n,id:v,points:g.map(t=>[t[0]/ol,t[1]/ol]),segments:f};e.room_drafts=o.filter(e=>e.id!==t.draft.id&&(!this.host._activeDraftId||e.id!==this.host._activeDraftId)),e.room_drafts.push($),this.host._activeDraftId=v,this.host._resumeDraftBySpace[this.host._space]=v,this.host._path=g,this.host._draftSegmentCms=this._adoptDraftCms(g,f.map(t=>Number(t.cm)),v),this.host._physicalSel=null,this._commitPhysicalGeometry(this.host._t("history.draft_merge"),y),null!=d&&this._offerWallFaces(s,s.length-1,i)}_adoptDraftCms(t,e,s){const i=Math.max(0,t.length-1),o=Ra(i,e,this.host._drawWallCm,A);return e.length!==i&&console.debug(`[houseplan] draft ${s??"?"}: восстановлено толщин ${i-e.length} (#234)`),o}_draftSegmentsForPath(t,e,s){const i=Array.isArray(e?.points)?e.points.map(t=>[Number(t?.[0])*ol,Number(t?.[1])*ol]):[],o=Array.isArray(e?.segments)?e.segments:[];return Ra(Math.max(0,t.length-1),s,this.host._drawWallCm,A).map((e,s)=>{const a=t[s],n=t[s+1],r=i.findIndex((t,e)=>{if(e+1>=i.length)return!1;const s=i[e+1];return this._samePt(a,t)&&this._samePt(n,s)||this._samePt(a,s)&&this._samePt(n,t)});return{...r>=0?o[r]:{},cm:e}})}_persistActiveDraftSegment(){if(this.host._path.length<2||!this.host._curSpaceCfg)return;const t=this._geometrySnapshot(),e=this.host._curSpaceCfg;e.room_drafts||=[],this.host._activeDraftId||(this.host._activeDraftId="draft-"+Date.now().toString(36)),this.host._resumeDraftBySpace[this.host._space]=this.host._activeDraftId;const s=e.room_drafts.findIndex(t=>t.id===this.host._activeDraftId),i={...s>=0?e.room_drafts[s]:{},id:this.host._activeDraftId,points:this.host._path.map(t=>[t[0]/ol,t[1]/ol]),segments:this._draftSegmentsForPath(this.host._path,s>=0?e.room_drafts[s]:null,this.host._draftSegmentCms)};s>=0?e.room_drafts[s]=i:e.room_drafts.push(i),this._commitPhysicalGeometry(this.host._t("history.draft_segment"),t)}_activeWallSourceKey(t){return`active:${this.host._activeDraftId||"session"}:${t}`}_wallGraphSources(t){const e=this._planStructuralGeometrySnapshot().value.segments.map(t=>({a:t.a,b:t.b,key:`static:${t.key}`}));for(let s=0;s+1t.key===s);if(i>=0){const[t]=this.host._wallFaceGraphCache.splice(i,1);return this.host._wallFaceGraphCache.push(t),t.value}const o=en(t,e);return this.host._wallFaceGraphCache.push({key:s,value:o}),this.host._wallFaceGraphCache.length>4&&this.host._wallFaceGraphCache.shift(),o}_offerWallFaces(t,e=this.host._path.length-2,s){if(this.host._path.length<2||this.host._wallFaceBatch||this.host._roomDialog)return;const i=this.host._spaceModel();if(!i)return;const o=this._activeWallSourceKey(e),a=2e-4*this.host._gridPitch;let n,r;try{const e=s||this._wallGraphSources(t);r=this._wallGraphSources(this.host._path),n=function(t,e,s){const i=new Set(t.faces.map(t=>t.key));return e.faces.filter(t=>!i.has(t.key)&&t.sourceKeys.includes(s))}(this._wallFaceGraph(e,a),this._wallFaceGraph(r,a),o)}catch{return}if(!n.length){const t=un(r,{requiredSourceKey:o,maxDistance:j(2,this.host._cellCm,this.host._gridPitch),gridStep:this.host._gridPitch,epsilon:a});return"ambiguous"===t.kind?(this.host._wallRepairDiagnostic=t.proposals[0]||null,void this.host._showToast(this.host._t(0===this.host._drawWallCm?"toast.zero_wall_ambiguous":"toast.wall_repair_ambiguous"))):void("repair"!==t.kind||this._overlapRoom(t.face.ring)||this._beginWallFaceBatch([{...t.face,repair:t.proposal}]))}for(const t of i.rooms){const e=R(t);if(!t.id||!e)continue;const s=Kt(e,this.host._path,.02*this.host._gridPitch);if(!s)continue;const[i,o]=s,a=k(i)>=k(o)?i:o,r=a===i?o:i,l=[...n].sort((t,e)=>Math.abs(t.area-k(r))-Math.abs(e.area-k(r))||t.key.localeCompare(e.key))[0];if(!l)return;return void this._beginWallFaceBatch([{...l,ring:r.map(t=>[t[0],t[1]]),split:{roomId:t.id,mainPoly:a,newPoly:r},consumeAllActive:!0}])}const l=n.filter(t=>!(t.ring.length<3||this._contourSelfIntersects(t.ring))&&!this._overlapRoom(t.ring));l.length&&this._beginWallFaceBatch(l)}_beginWallFaceBatch(t){this.host._wallRepairDiagnostic=null,this.host._wallFaceBatch={candidates:[...t].sort((t,e)=>t.area-e.area||t.key.localeCompare(e.key)),index:0,decisions:[],activePath:this.host._path.map(t=>[...t]),activeCms:[...this.host._draftSegmentCms],activeDraftId:this.host._activeDraftId},this._clearPlanSnapHover(),this.host._nameSel="",this.host._areaSel="",this._resetRoomDialogFields(),this.host._roomDialog=!0}_offerExistingWallFace(t){if(this.host._path.length||this.host._wallFaceBatch||this.host._roomDialog)return!1;const e=2e-4*this.host._gridPitch;try{let s,i=sn(this._wallFaceGraph(this._wallGraphSources([]),e),t,e);if(!i){const o=un(this._wallGraphSources([]),{point:t,maxDistance:j(2,this.host._cellCm,this.host._gridPitch),gridStep:this.host._gridPitch,epsilon:e});if("ambiguous"===o.kind)return this.host._wallRepairDiagnostic=o.proposals[0]||null,this.host._showToast(this.host._t(0===this.host._drawWallCm?"toast.zero_wall_ambiguous":"toast.wall_repair_ambiguous")),!0;if("repair"===o.kind)i=o.face,s=o.proposal;else if("none"===o.kind){const s=un(this._wallGraphSources([]),{point:t,maxDistance:this._cssPxToRender(12),gridStep:this.host._gridPitch,epsilon:e});if("repair"===s.kind)return this.host._wallRepairDiagnostic=s.proposal,this.host._showToast(this.host._t("toast.wall_repair_too_large")),!0}}return!(!i||i.ring.length<3||this._contourSelfIntersects(i.ring)||this._overlapRoom(i.ring))&&(this._beginWallFaceBatch([{...i,existing:!s,repair:s}]),!0)}catch{return!1}}_columnClick(t){const e=this._snap(t),s=this.host._drawWallCm;if(null==s)return void this._showPhysicalRange(yt,1);if(!this.host._curSpaceCfg||this._limitReached("column"))return;const i=this.host._spaceModel();if(!i)return;const o={id:"column-"+Date.now().toString(36),shape:"square",center:e,cm:wt(s),angle:0},a=(i.wall_columns||[]).find(t=>vt(t,o,.02*this.host._gridPitch));if(a)return clearTimeout(this.host._duplicateColumnTimer),this.host._duplicateColumnId=a.id,this.host._duplicateColumnTimer=window.setTimeout(()=>{this.host._duplicateColumnId=null},900),void this.host._showToast(this.host._t("toast.column_duplicate"));const n=this._geometrySnapshot(),r=this.host._curSpaceCfg;r.wall_columns||=[],r.wall_columns.push({...o,center:[e[0]/ol,e[1]/ol]}),this._commitPhysicalGeometry(this.host._t("history.column_add"),n)}_openPhysicalDialog(t,e,s){const i=this.host._spaceModel();if(i)if("partition"===t){const s=i.partitions.find(t=>t.id===e);s&&(this.host._physicalDialog={kind:t,id:e,cm:Tt(s.cm,this.host._imperial),length:this.host._fmtLen(s.a,s.b)})}else if("column"===t){const s=i.wall_columns.find(t=>t.id===e);s&&(this.host._physicalDialog={kind:t,id:e,cm:Tt(s.cm,this.host._imperial),shape:s.shape,angle:this.host._angleField("square"===s.shape?At(s.angle):0)})}else{const o=i.room_drafts.find(t=>t.id===e),a=Math.max(0,Math.min(o?.segments.length?o.segments.length-1:0,s||0));o?.segments[a]&&(this.host._physicalDialog={kind:t,id:e,segment:a,cm:Tt(o.segments[a].cm,this.host._imperial),length:this.host._fmtLen(o.points[a],o.points[a+1])})}}_physicalDown(t,e,s){const i=this.host._spaceModel();if(!i)return;t.stopPropagation(),nl(t);const o=this._svgPoint(t),a=[];for(const t of[...i.wall_columns].reverse())Vt(o,Jt(t,this.host._cellCm,this.host._gridPitch))&&a.push({kind:"column",id:t.id});for(const t of[...i.partitions].reverse()){const e=Yt(t.a,t.b,t.cm,this.host._cellCm,this.host._gridPitch);e&&Vt(o,e)&&a.push({kind:"partition",id:t.id})}a.some(t=>t.kind===e&&t.id===s)||a.unshift({kind:e,id:s});const n=a.map(t=>`${t.kind}:${t.id}`).sort().join("|"),r=performance.now(),l=this.host._physicalPickCycle,h=a.length>1&&l?.signature===n&&r-l.at>380&&r-l.at<=1200&&Math.hypot(t.clientX-l.x,t.clientY-l.y)<=10?(l.index+1)%a.length:Math.max(0,a.findIndex(t=>t.kind===e&&t.id===s));this.host._physicalPickCycle={signature:n,index:h,x:t.clientX,y:t.clientY,at:r},e=a[h].kind,s=a[h].id;const c="partition"===e?i.partitions.find(t=>t.id===s):i.wall_columns.find(t=>t.id===s);c&&(this.host._physicalSel={kind:e,id:s},this.host._physicalDrag={pid:t.pointerId,kind:e,id:s,start:this._svgPoint(t),startClient:[t.clientX,t.clientY],before:this._geometrySnapshot(),moved:!1,base:JSON.parse(JSON.stringify(c)),delta:[0,0]})}_clampPhysicalDelta(t,e,s){const i="partition"===t?[e.a,e.b]:[e.center],o=i.map(t=>t[0]),a=i.map(t=>t[1]);return[Math.max(-Xt*ol-Math.min(...o),Math.min(Xt*ol-Math.max(...o),s[0])),Math.max(-Xt*ol-Math.min(...a),Math.min(Xt*ol-Math.max(...a),s[1]))]}_physicalMove(t){const e=this.host._physicalDrag;if(!e||e.pid!==t.pointerId)return;t.stopPropagation();const s=this._svgPoint(t),i="partition"===e.kind?e.base.a:e.base.center,o=this._snap([i[0]+s[0]-e.start[0],i[1]+s[1]-e.start[1]]),a=this._clampPhysicalDelta(e.kind,e.base,[o[0]-i[0],o[1]-i[1]]);this.host._physicalDrag={...e,delta:a,moved:e.moved||Math.hypot(t.clientX-e.startClient[0],t.clientY-e.startClient[1])>=5}}_physicalUp(t){const e=this.host._physicalDrag;if(!e||e.pid!==t.pointerId)return;if(t.stopPropagation(),this.host._physicalDrag=null,!e.moved)return void this._registerPhysicalTap(e.kind,e.id);const s=this.host._spaceModel();if(!this.host._curSpaceCfg||!s)return;const i=this.host._curSpaceCfg;if("partition"===e.kind){const t=(i.partitions||[]).find(t=>t.id===e.id),s=e.base;if(t){const o={...s,a:[s.a[0]+e.delta[0],s.a[1]+e.delta[1]],b:[s.b[0]+e.delta[0],s.b[1]+e.delta[1]]};t.a=[o.a[0]/ol,o.a[1]/ol],t.b=[o.b[0]/ol,o.b[1]/ol];for(const t of i.openings||[]){if("partition"!==t.host?.kind||t.host.id!==e.id)continue;const s=F(t,[o],ol,this.host._cellCm,this.host._gridPitch).resolved;s&&Object.assign(t,N(t,s,ol))}}}else{const t=(i.wall_columns||[]).find(t=>t.id===e.id),o=e.base,a={...o,center:[o.center[0]+e.delta[0],o.center[1]+e.delta[1]]};if(s.wall_columns.some(t=>t.id!==e.id&&vt(t,a,.02*this.host._gridPitch)))return void this.host._showToast(this.host._t("toast.column_duplicate"));t&&(t.center=[(o.center[0]+e.delta[0])/ol,(o.center[1]+e.delta[1])/ol])}this._commitPhysicalGeometry(this.host._t("history.physical_move"),e.before)}_registerPhysicalTap(t,e,s){const i=performance.now(),o=this.host._physicalLastTap?.kind===t&&this.host._physicalLastTap.id===e&&this.host._physicalLastTap.segment===s&&i-this.host._physicalLastTap.at<=360;this.host._physicalLastTap={kind:t,id:e,segment:s,at:i},o&&(this.host._physicalLastTap=null,this._openPhysicalDialog(t,e,s))}_cancelPhysicalGesture(){this.host._physicalDrag=null,this.host._physicalRotate=null,this.host.requestUpdate()}_physicalRotateDown(t,e){if("square"!==e.shape||!this.host._spaceModel())return;t.preventDefault(),t.stopPropagation(),nl(t);const s=this._svgPoint(t);this.host._physicalSel={kind:"column",id:e.id},this.host._physicalRotate={pid:t.pointerId,id:e.id,center:[...e.center],startAngle:180*Math.atan2(s[1]-e.center[1],s[0]-e.center[0])/Math.PI,baseAngle:At(e.angle),angle:At(e.angle),before:this._geometrySnapshot(),moved:!1}}_physicalRotateMove(t){const e=this.host._physicalRotate;if(!e||e.pid!==t.pointerId)return;t.preventDefault(),t.stopPropagation();const s=this._svgPoint(t),i=180*Math.atan2(s[1]-e.center[1],s[0]-e.center[0])/Math.PI,o=e.baseAngle+i-e.startAngle,a=At(t.shiftKey?o:5*Math.round(o/5));this.host._physicalRotate={...e,angle:a,moved:e.moved||Math.abs(o-e.baseAngle)>=.5}}_physicalRotateUp(t){const e=this.host._physicalRotate;if(!e||e.pid!==t.pointerId)return;t.preventDefault(),t.stopPropagation(),this.host._physicalRotate=null;const s=this.host._spaceModel();if(!e.moved||!this.host._curSpaceCfg||!s)return;const i=s.wall_columns.find(t=>t.id===e.id);if(!i||"square"!==i.shape)return;const o={...i,angle:e.angle};if(s.wall_columns.some(t=>t.id!==e.id&&vt(t,o,.02*this.host._gridPitch)))return void this.host._showToast(this.host._t("toast.column_duplicate"));const a=this.host._curSpaceCfg.wall_columns?.find(t=>t.id===e.id);a&&(a.angle=e.angle,this._commitPhysicalGeometry(this.host._t("history.physical_edit"),e.before))}_rszRooms(){const t=[],e=this.host._spaceModel();if(!e)return t;for(const s of e.rooms){const e=s.id?R(s):null;e&&t.push({id:s.id,poly:e,wall_ids:Array.isArray(s.wall_ids)?[...s.wall_ids]:void 0})}return s=t,i=Math.max(1e-12,1e-12*this.host._gridPitch),s.map(t=>{if(!Array.isArray(t.wall_ids)||t.wall_ids.length!==t.poly.length)return{...t,poly:t.poly.map(t=>[...t])};const e=t.poly.map(t=>[...t]);let s=!0;for(;s&&e.length>3;){s=!1;for(let t=0;t0){e.splice(t,1),s=!0;break}}}return{...t,poly:e}});var s,i}_rszOpenings(){return this.host._openingsR.map(t=>({id:t.id,x:t.rx,y:t.ry,length:t.rlen,hosted:!!t.host,angle:t.angle,type:t.type}))}_rszObstacles(){const t=this.host._spaceModel();if(!t)return[];const e=[];for(const s of t.partitions||[])e.push({kind:"segment",a:[...s.a],b:[...s.b],half:j(s.cm,this.host._cellCm,this.host._gridPitch)/2});for(const s of t.room_drafts||[])for(let t=0;t+1Array.isArray(t.a)&&Array.isArray(t.b)&&t.a.length>=2&&t.b.length>=2).map(t=>({wall:t,a:[t.a[0]*ol,t.a[1]*this.host._spaceH],b:[t.b[0]*ol,t.b[1]*this.host._spaceH]})),o=Math.abs(t[0]-e[0])<=.05*this.host._gridPitch?"v":Math.abs(t[1]-e[1])<=.05*this.host._gridPitch?"h":null,a=i.filter(s=>!!o&&("h"===o?!(Math.abs(s.a[1]-t[1])>.05*this.host._gridPitch||Math.abs(s.b[1]-t[1])>.05*this.host._gridPitch)&&Math.min(Math.max(t[0],e[0]),Math.max(s.a[0],s.b[0]))-Math.max(Math.min(t[0],e[0]),Math.min(s.a[0],s.b[0]))>.05*this.host._gridPitch:!(Math.abs(s.a[0]-t[0])>.05*this.host._gridPitch||Math.abs(s.b[0]-t[0])>.05*this.host._gridPitch)&&Math.min(Math.max(t[1],e[1]),Math.max(s.a[1],s.b[1]))-Math.max(Math.min(t[1],e[1]),Math.min(s.a[1],s.b[1]))>.05*this.host._gridPitch)),n=new Set(a.map(t=>Number(t.wall.cm)).filter(t=>t>0));return{minDim:this.host._cmToUnits(30),eps:.05*this.host._gridPitch,step:this.host._gridPitch,movingHalf:s>0?j(s,this.host._cellCm,this.host._gridPitch)/2:0,obstacles:this._rszObstacles(),thicknessConflict:n.size>1}}_rszResolution(t,e,s){const i=this.host._resize.snapshotIdentity||s||this._rszSnapshot(),o=`${this.host._space}|${this.host._cellCm}|${this.host._gridPitch}|${i}`,a=`${t}:${e}`;return this.host._resize.resolve(o,a,()=>{const s=[...this.host._resize.rooms||this._rszRooms()],i=s.find(e=>e.id===t),o=i?.poly?.[e]||[0,0],a=i?.poly?.[(e+1)%(i?.poly?.length||1)]||[0,0];return function(t,e,s,i,o){const{eps:a}=o,n=t.find(t=>t.id===s);if(!n||i<0||i>=(n.poly?.length||0)||n.poly.length<4||!go(n.poly))return{enabled:!1,reason:"invalid-geometry"};const r=n.poly[i],l=n.poly[(i+1)%n.poly.length],h=vo(r,l,a);if(!h)return{enabled:!1,reason:"diagonal"};if(!xo(n.poly,i,h,a))return{enabled:!1,reason:"side-angle"};if(o.thicknessConflict)return{enabled:!1,reason:"thickness-conflict"};for(const t of o.obstacles||[])if(Do(t,r,l,o))return{enabled:!1,reason:"duplicate-physical-wall"};const c=po(lo(l,r)),d=[],p=new Set;let _=!1,u=!1;for(const e of t)if(e.id!==s)for(let t=0;t1||d.length>1)return{enabled:!1,reason:"multiple-rooms"};const m={[s]:i},g=[s];if(1===d.length){const t=d[0];if(!go(t.room.poly))return{enabled:!1,reason:"invalid-geometry"};if(!xo(t.room.poly,t.edge,h,a))return{enabled:!1,reason:"side-angle"};g.push(t.room.id),m[t.room.id]=t.edge}const f=[];for(const t of e)if(!(uo([t.x,t.y],r,l)>2*a)){if(t.hosted||t.length>c+2*a)return{enabled:!1,reason:"opening-conflict"};f.push(t.id)}const b=Object.fromEntries(g.map(e=>[e,t.find(t=>t.id===e).poly.length])),y=[];for(const e of g){const s=t.find(t=>t.id===e),i=m[e],o=(i-1+s.poly.length)%s.poly.length,n=(i+1)%s.poly.length,r=Io(t,e,o,1,a),l=Io(t,e,n,0,a);if(!r||!l)return{enabled:!1,reason:"partial-shared"};y.push(r,l)}const v={roomId:s,edge:i,a:[...r],b:[...l],n:mo(n.poly,i),roomIds:g,edgeByRoom:m,topology:b,movingOpeningIds:f,sideOwnership:y};if(!Oo(t,e,v,0,o))return{enabled:!1,reason:"invalid-geometry"};const $=Math.abs(Number(o.step));if(Number.isFinite($)&&$>a){const s=[-$,$];if(!s.some(s=>Oo(t,e,v,s,o))){if(!s.some(s=>Fo(No(t,e,v,s),v,a)))return{enabled:!1,reason:"partial-shared"};const i={...o,obstacles:[]};return s.some(s=>Oo(t,e,v,s,i))?{enabled:!1,reason:"duplicate-physical-wall"}:e.length&&s.some(e=>Oo(t,[],v,e,o))?{enabled:!1,reason:"opening-conflict"}:{enabled:!1,reason:"invalid-geometry"}}}return{enabled:!0,plan:v}}(s,[...this.host._resize.openings||this._rszOpenings()],t,e,this._rszOptsFor(o,a))})}_rszSnapshot(){return JSON.stringify(this._geometrySnapshot()||{spaceId:this.host._space,rooms:[],openings:[],walls:[],open_spans:[]})}_rszResetController(){const t=null!==this.host._resize.preview;this.host._resize.reset(),t&&this.host._cfgEpoch++}_rszProjectPreview(t,e,s,i,o){this.host._rszLimitViolation=null;const a=this.host._serverCfg?.spaces.find(t=>t.id===this.host._space);if(!a||!this.host._serverCfg)return{ok:!1,reason:"missing-context"};const n=JSON.parse(t),r={...a,rooms:n.rooms,openings:n.openings||[]};for(const t of["walls","wall_segments","open_spans","room_drafts","partitions","wall_columns","decor"])void 0!==n[t]?r[t]=n[t]:delete r[t];for(const t of["plan_x","plan_y","plan_scale","plan_scale_x","plan_scale_y","plan_angle"])void 0!==n.plan_transform?.[t]?r[t]=n.plan_transform[t]:delete r[t];const l=this.host._spaceH;for(const[t,s]of Object.entries(e)){const e=r.rooms.find(e=>e.id===t);e&&(e.poly=s.map(t=>[t[0]/ol,t[1]/l]),delete e.x,delete e.y,delete e.w,delete e.h)}for(const[t,e]of Object.entries(s)){const s=(r.openings||[]).find(e=>e.id===t);s&&(s.x=e[0]/ol,s.y=e[1]/l)}const h=[],c=[];for(const t of i){const e=o.find(e=>e.id===t),s=r.rooms.find(e=>e.id===t);if(!e||!s?.poly)continue;const i=s.poly.map(t=>[t[0]*ol,t[1]*l]);if(e.poly.length!==i.length)continue;const a=Array.isArray(e.wall_ids)?e.wall_ids:[];for(let t=0;tt.id===s):null;o&&(o.a=[i[t][0]/ol,i[t][1]/ol],o.b=[i[(t+1)%i.length][0]/ol,i[(t+1)%i.length][1]/ol])}}if(h.length&&Array.isArray(r.walls)&&r.walls.length){const t=Zt(r.walls,h,c,this.host._wallKeyPitch,ol,"fixed-topology");if(t.rejected)return{ok:!1,reason:"wall-metadata"};r.walls=t.walls}const d=[];for(const t of r.rooms||[]){const e=R(t);if(e&&!(e.length<2))for(let t=0;tJSON.stringify([t?.key,t?.cm,t?.a,t?.b]),_=new Map;for(const t of n.walls||[]){const e=p(t);_.set(e,(_.get(e)||0)+1)}const u=[];for(const t of r.walls||[]){const e=p(t),s=_.get(e)||0;s?_.set(e,s-1):u.push(t)}if(Qt(u,d,this.host._wallKeyPitch,ol,n.walls||[]).length)return{ok:!1,reason:"wall-metadata"};const m=this._rszSpaceCandidateGeometry(this.host._space,r);if(!m.ok)return{ok:!1,reason:"physical-geometry"};const g={...this.host._serverCfg,spaces:(this.host._serverCfg?.spaces||[]).map(t=>t?.id===this.host._space?r:t)},f=this._junctionLimitsIntroduced(g,this.host._serverCfg,this.host._space,m.wallGeometry);return f.length?(this.host._rszLimitViolation=f[0],{ok:!1,reason:"junction-limit"}):{ok:!0,value:{preview:{space:this.host._space,sp:r},beforeWalls:n.walls||[],afterWalls:r.walls||[],artifact:m.wallGeometry}}}_rszAcceptPreview(t,e){if(this.host._cfgEpoch++,!t||!e)return;const s=te(e);if(!s)return;const i=`${this.host._space}|${this.host._cfgEpoch}|${t.sp.rooms.length}`;Object.defineProperty(s,"sourceFingerprint",{value:Et([t.sp,this.host._cellCm,this.host._gridPitch]),enumerable:!1});const o={key:i,value:s};il(this.host._wallUnionPool,i,o,8),this.host._wallUnionCache=o}_rszSpaceCandidateGeometry(t,e){if(!this.host._serverCfg)return{ok:!1,wallGeometry:null};const s={...this.host._serverCfg,spaces:this.host._serverCfg.spaces.map(s=>s.id===t?e:s)};let i=null;try{return{ok:this._checkSpacePhysicalGeometry(s,t,t=>{i=t}).ok,wallGeometry:i}}catch{return{ok:!1,wallGeometry:null}}}_rszSpaceCandidateRenderable(t,e){if(!this.host._serverCfg)return!1;try{const s={...this.host._serverCfg,spaces:this.host._serverCfg.spaces.map(s=>s.id===t?e:s)};return this._checkSpacePhysicalGeometry(s,t).ok}catch{return!1}}_rszCandidateRenderable(t){return!!t&&t.space===this.host._space&&this._rszSpaceCandidateRenderable(t.space,t.sp)}_rszEdgeDown(t,e,s){if("resize"!==this.host._tool||this.host._resize.dragging)return;t.stopPropagation(),t.preventDefault();const i=this._rszRooms(),o=this._rszResolution(e,s);if(!o.enabled)return void this.host._showToast(this._rszReasonText(o.reason));nl(t);const a=o.plan,n=this._svgPoint(t),r=`${this.host._space}|${this.host._cfgEpoch}|${i.length}`,l=this.host._wallUnionCache?.key===r?this.host._wallUnionCache.value:null,h=this._rszSnapshot();this.host._resize.begin({pointerId:t.pointerId,start:[n[0],n[1]],roomId:e,plan:a,options:this._rszOptsFor(a.a,a.b),rooms:i,openings:this._rszOpenings(),snapshotIdentity:h,before:JSON.parse(h),wallUnionBefore:l})}_rszReasonText(t){return this.host._t(`resize.disabled.${t}`)}_rszDisabledActivate(t,e){t.stopPropagation(),t.preventDefault(),this.host._showToast(this._rszReasonText(e))}_rszDisabledKey(t,e){"Enter"!==t.key&&" "!==t.key||this._rszDisabledActivate(t,e)}_rszMove(t){if(!this.host._resize.ownsPointer(t.pointerId))return;t.stopPropagation();const e=this._svgPoint(t),s=this.host._resize.move({pointerId:t.pointerId,point:[e[0],e[1]],step:this.host._gridPitch,snap:t=>{const e=this._snap(t);return[e[0],e[1]]},project:(t,e,s,i,o)=>this._rszProjectPreview(t,e,s,i,o),publish:(t,e)=>this._rszAcceptPreview(t,e),measure:(t,e)=>this._rszEdgeLabels(t,e)});if("rejected"===s.kind)return s.notify&&this.host._showToast(this.host._rszLimitViolation?`${this.host._t("resize.limit_stopped")} — `+this._junctionLimitLabel(this.host._rszLimitViolation):this.host._t("resize.preview_failed")),void this.host.requestUpdate();"accepted"===s.kind&&this.host.requestUpdate()}_rszUp(t){if(!this.host._resize.ownsPointer(t.pointerId))return;t.stopPropagation();const e=this.host._resize.finish({pointerId:t.pointerId,currentSnapshotIdentity:this._rszSnapshot(),validatePreview:t=>this._rszCandidateRenderable(t)});if("no-op"===e.kind)return this.host._cfgEpoch++,void this.host.requestUpdate();if("rejected"===e.kind)return this.host._cfgEpoch++,this.host._showToast(this.host._t("resize.commit_failed")),void this.host.requestUpdate();const s=e.preview,i=this.host._serverCfg?.spaces.find(t=>t.id===s.space);i&&(i.rooms=s.sp.rooms,i.openings=s.sp.openings,Array.isArray(s.sp.walls)&&(s.sp.walls.length?i.walls=s.sp.walls:delete i.walls),Array.isArray(s.sp.wall_segments)&&(i.wall_segments=s.sp.wall_segments)),this.host._suppressClick=!0,setTimeout(()=>this.host._suppressClick=!1,0),this._commitPhysicalGeometry(this.host._t("history.resize_room"),e.before),this.host.requestUpdate()}_rszCancelDrag(t){const e=this.host._resize.cancel(this._rszSnapshot(),t);if("no-op"!==e.kind){if(this.host._cfgEpoch++,e.restoreWallUnion){const t=this.host._spaceModel();if(t){const s=`${this.host._space}|${this.host._cfgEpoch}|${t.rooms.length}`,i={key:s,value:e.restoreWallUnion};il(this.host._wallUnionPool,s,i,8),this.host._wallUnionCache=i}}this.host.requestUpdate()}}_rszPointerCancel(t){this.host._resize.ownsPointer(t.pointerId)&&(t.stopPropagation(),this._rszCancelDrag(t.pointerId))}_rszEdgeLabels(t,e,s=this.host._resize.rooms){const i=s,o=[],a=t.polys[e.roomId]||i?.find(t=>t.id===e.roomId).poly;if(!a||!i)return o;const n=a.length,r=this._rszInnerSpanCms(e.roomId,a,t.polys);for(const t of function(t,e){const s=t.length;return[(e-1+s)%s,(e+1)%s]}(a,e.edge)){const e=a[t],s=a[(t+1)%n],i=r?.[t];o.push({kind:"length",x:(e[0]+s[0])/2,y:(e[1]+s[1])/2,text:null==i?this.host._fmtLen(e,s):ee(i,"mi"===this.host.hass?.config?.unit_system?.length),edge:{a:[e[0],e[1]],b:[s[0],s[1]]}})}const l="mi"===this.host.hass?.config?.unit_system?.length,h=e.roomIds,c=this.host._spaceWalls,d=this.host._physicalBodiesR(),p=this.host._baseVb(),_=this.host._view&&this.host._view.w>0&&this.host._view.h>0?this.host._view:{x:p[0],y:p[1],w:p[2],h:p[3]},[u,m]=this.host._lastValidStageSize||[_.w,_.h],g={..._,stageWidth:Math.max(1,u),stageHeight:Math.max(1,m)},f=this.host._spaceModel(),b=this.host._config?.icon_size??2.5,y=f?se(b>8?2.5:b,f,_.w,this.host._kiosk?this.host._kioskScale.icon:1)*g.stageWidth/100:24,v=Math.max(10,.77*y),$=this.host._t("room.settings_short"),w=v*(1.457+.66*Math.max(1,$.length)*.42);for(const s of h){const a=t.polys[s]||i.find(t=>t.id===s).poly,n=c.length&&f&&this.host._innerRoomContour(f,s)||a,r=d.length?ie(oe(n,d))*Math.pow(this.host._cellCm/this.host._gridPitch,2)/1e4:ae(n,this.host._gridPitch,this.host._cellCm),h=ne(r,l),p=qo({poly:a,edge:e.edgeByRoom[s],text:h,view:g,gearCenter:re(a),gearWidthPx:w,gearHeightPx:v});o.push({kind:"area",roomId:s,x:p.anchor[0],y:p.anchor[1],text:h,placement:p})}return o}_rszInnerSpanCms(t,e,s){const i=Object.keys(s).length?Object.entries(s).map(([t,e])=>({id:t,poly:e})):this.host._spaceModel()?.rooms;if(!i?.length)return null;const o=this.host._spaceWalls;if(!o.length)return null;const a=this.host._openCuts(),n=le(i,t,o,a,this.host._wallKeyPitch,this.host._cellCm,this.host._gridPitch,ol);if(!n||n.length!==e.length)return null;const r=this.host._cellCm/this.host._gridPitch;return e.map((t,s)=>he(e,s,n)*r)}_renderResizeMeasurements(){const t=this.host._resize.liveLabels;if(!t?.length)return a;const e=t.filter(t=>"length"===t.kind),s=t.filter(t=>"area"===t.kind);return ce``}_renderResizeLayer(t){const e=Math.max(.013*t.w,5),s=e/2,i=t=>t.toFixed(1),o=`M ${i(-.7*s)} 0 H ${i(.7*s)} M 0 ${i(-.22*s)} V ${i(-s)} M ${i(-.32*s)} ${i(-.6*s)} L 0 ${i(-s)} L ${i(.32*s)} ${i(-.6*s)} M 0 ${i(.22*s)} V ${i(s)} M ${i(-.32*s)} ${i(.6*s)} L 0 ${i(s)} L ${i(.32*s)} ${i(.6*s)}`,a=[],n=this._rszRooms(),r=this.host._resize.snapshotIdentity||this._rszSnapshot();for(const t of n)for(let s=0;sthis._rszEdgeDown(e,t.id,s)} + @pointermove=${t=>this._rszMove(t)} + @pointerup=${t=>this._rszUp(t)} + @pointercancel=${t=>this._rszPointerCancel(t)} + @lostpointercapture=${t=>this._rszPointerCancel(t)} + @click=${_?t=>this._rszDisabledActivate(t,p.reason):null} + @keydown=${_?t=>this._rszDisabledKey(t,p.reason):null}> + ${u} + `),a.push(ce``)}return ce`${a}`}_partitionOpeningCuts(t=this.host._spaceModel(),e=()=>!0){if(!t)return[];const s=this.host._curSpaceCfg?.id===t.id?this.host._curSpaceCfg:null,i=de(s,t,this.host._cellCm,this.host._gridPitch,ol);return pe(i,e)}_decorSnapGeometry(t){const e=this.host._spaceModel();if(!e)return{points:[],segments:[]};const s=t||"",i=this.host._decorSnapCache;if(i&&i.epoch===this.host._cfgEpoch&&i.space===this.host._space&&i.height===this.host._decorH&&i.exclude===s)return i.geometry;const o=[];for(const e of this.host._decorList)if(e.id!==t)if("line"===e.kind){const t=[e.x1*ol,e.y1*this.host._decorH],s=[e.x2*ol,e.y2*this.host._decorH];o.push({points:[t,s,[(t[0]+s[0])/2,(t[1]+s[1])/2]],segments:[{a:t,b:s}]})}else if("text"===e.kind)o.push({points:[[e.x*ol,e.y*this.host._decorH]],segments:[]});else{const t=this.host._decorBoxOf(e);t&&o.push(_e(t))}for(const t of e.rooms){const e=R(t);e?.length&&o.push({points:e.flatMap((t,s)=>{const i=e[(s+1)%e.length];return[t,[(t[0]+i[0])/2,(t[1]+i[1])/2]]}),segments:e.map((t,s)=>({a:t,b:e[(s+1)%e.length]}))})}const a=ue(o);return this.host._decorSnapCache={epoch:this.host._cfgEpoch,space:this.host._space,height:this.host._decorH,exclude:s,geometry:a},a}_decorSnap(t,e="mouse",s){const i=this.host._stageEl,o=this.host._viewOr(this.host._baseVb()),a="touch"===e||"pen"===e?14:8,n=i?o.w/Math.max(1,i.clientWidth)*a:this.host._gridPitch;return me(t,this._decorSnapGeometry(s),n,t=>this._snap(t)).point}_replaceDecor(t,e){const s=this.host._curSpaceCfg;s&&(s.decor=this.host._decorList.map(s=>s.id===t?{...s,...e}:s),this.host.requestUpdate())}_cancelDecorGesture(){const t=this.host._decorMove?.before||this.host._dtDrag?.before||this.host._bdDrag?.before,e=t&&this.host._serverCfg?.spaces.find(e=>e.id===t.spaceId);if(t&&e){const s=t=>JSON.parse(JSON.stringify(t));void 0!==t.decor?e.decor=s(t.decor):delete e.decor;for(const t of["plan_x","plan_y","plan_scale","plan_scale_x","plan_scale_y","plan_angle"])delete e[t];Object.assign(e,s(t.plan_transform||{})),this.host._cfgEpoch++}this.host._decorMove=null,this.host._dtDrag=null,this.host._bdDrag=null,this.host.requestUpdate()}_decorPointerDown(t){const e=this.host._decorTool;if("select"===e||"erase"===e?t.target.closest?.(".dshape"):null)return!0;if("line"===e||"rect"===e||"ellipse"===e){t.preventDefault();const s=this._decorSnap(this._svgPoint(t),t.pointerType);return this.host._decorDraft={kind:e,a:s,b:s,pid:t.pointerId},nl(t),!0}if("text"===e){const e=this._decorSnap(this._svgPoint(t),t.pointerType);return this.host._decorTextDialog={x:fe(e[0]/ol),y:fe(e[1]/this.host._decorH),text:"",color:this.host._decorStyle.color,opacity:this.host._decorStyle.opacity,angle:"0",sizeCm:ge(ct,this.host._cellCm,this.host._gridPitch)},this.host._decorTextSelection={start:0,end:0},!0}if("furniture"===e)return!!this.host._furnPalette&&(t.preventDefault(),this._furnPlace(this._svgPoint(t),t.shiftKey),!0);if("select"===e&&(this.host._decorSel=null),this.host._bdMovable){const e=this.host._bdRect,s=this._svgPoint(t),i=e.x+e.w/2,o=e.y+e.h/2,a=-be(e.angle)*Math.PI/180,n=s[0]-i,r=s[1]-o,l=[i+n*Math.cos(a)-r*Math.sin(a),o+n*Math.sin(a)+r*Math.cos(a)];if(l[0]>=e.x&&l[0]<=e.x+e.w&&l[1]>=e.y&&l[1]<=e.y+e.h)return t.preventDefault(),this._bdStart(t)}return!1}_decorCommitDraft(){const t=this.host._decorDraft;if(this.host._decorDraft=null,!t)return;const e=.5*this.host._gridPitch;if(!ye(t.kind,t.a,t.b,e))return;const s=ol,i=this.host._decorH,o=this.host._decorStyle,a=this._geometrySnapshot(),n="dc"+Date.now().toString(36)+Math.random().toString(36).slice(2,5),r=fe;let l;if("line"===t.kind)l={id:n,kind:"line",x1:r(t.a[0]/s),y1:r(t.a[1]/i),x2:r(t.b[0]/s),y2:r(t.b[1]/i),...ve(o,!1)};else{const e=r(Math.min(t.a[0],t.b[0])/s),a=r(Math.min(t.a[1],t.b[1])/i),h=Math.abs(t.b[0]-t.a[0])/s,c=Math.abs(t.b[1]-t.a[1])/i;l={id:n,kind:t.kind,x:e,y:a,w:h,h:c,...ve(o,!0)}}this.host._curSpaceCfg.decor=[...this.host._decorList,l],this.host._decorSel=n,this._recordGeometry(this.host._t("history.decor_add"),a),this._saveConfig(),this.host.requestUpdate()}_decorShapeDown(t,e){if("decor"!==this.host._mode)return;const s=this.host._decorTool;if("text"===s){if("text"!==e.kind)return;return t.stopPropagation(),t.preventDefault(),void this._decorOpenText(e)}"select"!==s&&"erase"!==s||(t.stopPropagation(),t.preventDefault(),"erase"!==s?(this.host._decorSel=e.id,this.host._decorMove={id:e.id,start:this._svgPoint(t),orig:JSON.parse(JSON.stringify(e)),pid:t.pointerId,moved:!1,before:this._geometrySnapshot()},nl(t)):this.host._decorEraseConfirm={id:e.id,kind:e.kind})}_decorMoveUpdate(t){const e=this.host._decorMove;if("furniture"===e.orig?.kind)return void this._furnMoveUpdate(t);const s=this._svgPoint(t),i=e.orig,o=("line"===i.kind?i.x1:i.x)*ol,a=("line"===i.kind?i.y1:i.y)*this.host._decorH,n=this._decorSnap([o+(s[0]-e.start[0]),a+(s[1]-e.start[1])],t.pointerType,e.id);let r=(n[0]-o)/ol,l=(n[1]-a)/this.host._decorH;const h=e.orig,c="line"===h.kind?Math.min(h.x1,h.x2):h.x,d="line"===h.kind?Math.min(h.y1,h.y2):h.y,p="line"===h.kind?Math.abs(h.x2-h.x1):h.w||0,_="line"===h.kind?Math.abs(h.y2-h.y1):h.h||0,u=Xt;r=Math.max(-u-c,Math.min(u-c-p,r)),l=Math.max(-u-d,Math.min(u-d-_,l)),(r||l)&&(e.moved=!0);this.host._curSpaceCfg.decor=this.host._decorList.map(t=>{if(t.id!==e.id)return t;const s=e.orig;return"line"===t.kind?{...t,x1:s.x1+r,y1:s.y1+l,x2:s.x2+r,y2:s.y2+l}:{...t,x:s.x+r,y:s.y+l}}),this.host.requestUpdate()}_decorShapeDbl(t,e){"decor"===this.host._mode&&"select"===this.host._decorTool&&(t.preventDefault(),t.stopPropagation(),this.host._decorMove=null,this.host._decorSel=e.id,this._openDecorProperties(e))}_openDecorProperties(t){if("decor"!==this.host._mode||"select"!==this.host._decorTool)return;if("text"===t.kind)return void this._decorOpenText(t);if(!["line","rect","ellipse","furniture"].includes(t.kind))return;const e=this.host._decorResolvedStyle(t),s="line"===t.kind?t:null,i=this.host._decorBoxOf(t);this.host._decorShapeDialog={id:t.id,kind:t.kind,color:e.color,opacity:e.opacity,widthCm:e.widthCm,angle:this.host._angleField(be(s?$e([s.x1*ol,s.y1*this.host._decorH],[s.x2*ol,s.y2*this.host._decorH]):t.angle)),...s?{lengthCm:ge(Math.hypot((s.x2-s.x1)*ol,(s.y2-s.y1)*this.host._decorH),this.host._cellCm,this.host._gridPitch),lineStyle:"dashed"===s.line_style?"dashed":"solid"}:{},...i?{sizeWCm:ge(i.w,this.host._cellCm,this.host._gridPitch),sizeHCm:ge(i.h,this.host._cellCm,this.host._gridPitch)}:{},..."furniture"===t.kind?{symbol:t.symbol}:{},..."rect"===t.kind||"ellipse"===t.kind?{fill:e.fill,fillColor:e.fillColor,fillOpacity:e.fillOpacity}:{}}}_decorOpenText(t){if("text"!==t.kind)return;let e=String(t.text??"");const s=[...e.matchAll(/\{([^{}\r\n]+)\}/g)].some(t=>!!pt(t[1])),i=String(t.unit??"").trim(),o="state"===String(t.attr??"").trim().toLowerCase()?null:t.attr,a=s||i?"":_t(t.entity,o),n=!(!String(t.entity??"").trim()||s||a&&!i);if(a&&!n){const t=e.indexOf("{}");e=t>=0?e.slice(0,t)+a+e.slice(t+2):`${e}${e?" ":""}${a}`}this.host._decorTextDialog={id:t.id,x:t.x,y:t.y,text:e,color:l(t.color,this.host._decorStyle.color),opacity:we(t.opacity,this.host._decorStyle.opacity),angle:this.host._angleField(t.angle),sizeCm:this.host._decorTextSizeCm(t),pickerEntity:t.entity||"",preserveLegacy:n||void 0},this.host._decorTextSelection={start:e.length,end:e.length}}_decorRememberTextSelection(t){this.host._decorTextSelection={start:t.selectionStart??t.value.length,end:t.selectionEnd??t.value.length}}_decorInsertLiveVariable(t){const e=this.host._decorTextDialog;if(!e)return;const s=_t(e.pickerEntity,t);if(!s)return;const i=e.text,o=Math.max(0,Math.min(i.length,this.host._decorTextSelection.start)),a=Math.max(o,Math.min(i.length,this.host._decorTextSelection.end));if(i.length-(a-o)+s.length>200)return;const n=i.slice(0,o)+s+i.slice(a),r=o+s.length;this.host._decorTextSelection={start:r,end:r},this.host._decorTextDialog={...e,text:n,preserveLegacy:void 0},this.host.updateComplete.then(()=>{const t=this.host.renderRoot.querySelector("textarea.dtarea");t&&(t.focus(),t.setSelectionRange(r,r))})}_decorSaveText(){const t=this.host._decorTextDialog,e=String(t?.text??"").replace(/\r\n?/g,"\n").trim();if(!t||!e)return void(this.host._decorTextDialog=null);const s=this._geometrySnapshot(),i=this.host._curSpaceCfg,o={color:t.color,opacity:we(t.opacity),size_cm:Number(Math.max(.1,Math.min(tl,t.sizeCm)).toFixed(4)),...be(t.angle)?{angle:be(t.angle)}:{}};if(t.id)i.decor=this.host._decorList.map(s=>{if(s.id!==t.id)return s;if("text"!==s.kind)return s;if(t.preserveLegacy){const{angle:t,size:i,scale:a,...n}=s;return{...n,text:e,...o}}const{entity:i,attr:a,unit:n,...r}=s,{angle:l,size:h,scale:c,...d}=r;return{...d,text:e,...o}});else{const s="dc"+Date.now().toString(36)+Math.random().toString(36).slice(2,5);i.decor=[...this.host._decorList,{id:s,kind:"text",x:t.x,y:t.y,text:e,...o}],this.host._decorSel=s}this.host._decorTextDialog=null,this._recordGeometry(this.host._t(t.id?"history.decor_edit":"history.decor_add"),s),this._saveConfig(),this.host.requestUpdate()}_decorSaveShape(){const t=this.host._decorShapeDialog;if(!t)return;const e=this._geometrySnapshot(),s={color:t.color,opacity:we(t.opacity),widthCm:Math.max(.1,Math.min(100,Number(t.widthCm)||.1)),fill:!!t.fill,fillColor:t.fillColor||t.color,fillOpacity:we(t.fillOpacity,.25)};this.host._curSpaceCfg.decor=this.host._decorList.map(e=>{if(e.id!==t.id)return e;const i="rect"===t.kind||"ellipse"===t.kind,o=ve(s,i);if("line"===e.kind){const s=(e.x1+e.x2)/2*ol,i=(e.y1+e.y2)/2*this.host._decorH,a=ke(Math.max(.1,Number(t.lengthCm)||.1),this.host._cellCm,this.host._gridPitch),n=be(t.angle)*Math.PI/180,r=Math.cos(n)*a/2,l=Math.sin(n)*a/2,h=this._snap([s-r,i-l]),c=this._snap([s+r,i+l]),{width:d,line_style:p,..._}=e;return{..._,...o,x1:fe(h[0]/ol),y1:fe(h[1]/this.host._decorH),x2:fe(c[0]/ol),y2:fe(c[1]/this.host._decorH),..."dashed"===t.lineStyle?{line_style:"dashed"}:{}}}if("rect"===e.kind||"ellipse"===e.kind||"furniture"===e.kind){const s=e.w*ol,i=e.h*this.host._decorH,a=Math.max(this.host._gridPitch,Ft(ke(Number(t.sizeWCm),this.host._cellCm,this.host._gridPitch),this.host._gridPitch)),n=Math.max(this.host._gridPitch,Ft(ke(Number(t.sizeHCm),this.host._cellCm,this.host._gridPitch),this.host._gridPitch)),r=e.x*ol+s/2,l=e.y*this.host._decorH+i/2,h=be(t.angle),c=xe({x:r-a/2,y:l-n/2},h,t=>this._snap(t)),{width:d,angle:p,..._}=e;return{..._,...o,x:fe(c[0]/ol),y:fe(c[1]/this.host._decorH),w:a/ol,h:n/this.host._decorH,..."furniture"===e.kind&&t.symbol?{symbol:t.symbol}:{},...h?{angle:h}:{}}}return e}),this.host._decorStyle={...s,fill:"rect"===t.kind||"ellipse"===t.kind?s.fill:this.host._decorStyle.fill,fillColor:"rect"===t.kind||"ellipse"===t.kind?s.fillColor:this.host._decorStyle.fillColor,fillOpacity:"rect"===t.kind||"ellipse"===t.kind?s.fillOpacity:this.host._decorStyle.fillOpacity},this.host._decorShapeDialog=null,this._recordGeometry(this.host._t("history.decor_edit"),e),this._saveConfig(),this.host.requestUpdate()}_dtPivot(t){return"line"===t.kind?[(t.x1+t.x2)/2*ol,(t.y1+t.y2)/2*this.host._decorH]:"furniture"===t.kind||"rect"===t.kind||"ellipse"===t.kind?[(t.x+t.w/2)*ol,(t.y+t.h/2)*this.host._decorH]:[t.x*ol,t.y*this.host._decorH]}_dtApply(t,e){const s=this.host._curSpaceCfg;s&&(s.decor=this.host._decorList.map(s=>{if(s.id!==t)return s;const i={...s};"text"===s.kind&&void 0!==e.textSizeCm&&(delete i.size,delete i.scale);const o={...i};return void 0!==e.textSizeCm&&(o.size_cm=Number(Math.max(.1,Math.min(tl,e.textSizeCm)).toFixed(4))),void 0!==e.angle&&(e.angle?o.angle=Number(e.angle.toFixed(2)):delete o.angle),o}),this.host._cfgEpoch++,this.host.requestUpdate())}_dtStart(t,e,s,i){const o=this.host._dtSel;if(!o)return;t.stopPropagation(),t.preventDefault();const[a,n]=this._dtPivot(o),r=this._svgPoint(t),l=this.host._decorBoxOf(o);this.host._dtDrag={id:o.id,kind:e,pid:t.pointerId,ax:a,ay:n,r0:Math.hypot(r[0]-a,r[1]-n),a0:180*Math.atan2(r[1]-n,r[0]-a)/Math.PI,textSizeCm0:"text"===o.kind?this.host._decorTextSizeCm(o):1,angle0:"line"===o.kind?0:Number(o.angle)||0,sgx:s?.[0],sgy:s?.[1],orig:l||void 0,origShape:JSON.parse(JSON.stringify(o)),before:this._geometrySnapshot(),lineEnd:i,moved:!1},nl(t)}_dtMove(t){const e=this.host._dtDrag;if(!e)return;const s=this._svgPoint(t);if(void 0!==e.lineEnd&&"line"===e.origShape.kind){const i=this._decorSnap(s,t.pointerType,e.id),o=fe(i[0]/ol),a=fe(i[1]/this.host._decorH),n=e.origShape,r=0===e.lineEnd?n.x1:n.x2,l=0===e.lineEnd?n.y1:n.y2;return(Math.abs(o-r)>1e-9||Math.abs(a-l)>1e-9)&&(e.moved=!0),void this._replaceDecor(e.id,0===e.lineEnd?{x1:o,y1:a}:{x2:o,y2:a})}if("scale"===e.kind&&e.orig){const i=Se(e.orig,e.sgx??1,e.sgy??1,s[0],s[1],!t.shiftKey,this.host._gridPitch,this.host._gridPitch),o=Math.abs(i.x-e.orig.x)>1e-6||Math.abs(i.y-e.orig.y)>1e-6||Math.abs(i.w-e.orig.w)>1e-6||Math.abs(i.h-e.orig.h)>1e-6;if(!o&&!e.moved)return;return e.moved||=o,void this._decorApplyBox(e.id,i)}if("scale"===e.kind){const t=Math.hypot(s[0]-e.ax,s[1]-e.ay);if(e.r0<1e-6)return;const i=Math.max(.1,Math.min(tl,e.textSizeCm0*(t/e.r0))),o=Math.abs(i-e.textSizeCm0)>1e-6;if(!o&&!e.moved)return;return e.moved||=o,void this._dtApply(e.id,{textSizeCm:i})}const i=180*Math.atan2(s[1]-e.ay,s[0]-e.ax)/Math.PI;let o=e.angle0+(i-e.a0);t.shiftKey||(o=5*Math.round(o/5)),o=(o%360+360)%360,o>180&&(o-=360);const a=Math.abs(o-e.angle0)>1e-6;(a||e.moved)&&(e.moved||=a,this._dtApply(e.id,{angle:o}))}_dtUp(){const t=this.host._dtDrag;this.host._dtDrag=null,t?.moved&&(this._recordGeometry(this.host._t("history.decor_transform"),t.before),this._saveConfig()),this.host.requestUpdate()}_dtMeasure(){const t=this.host._dtSel;if(!t)return void(this.host._dtBox&&(this.host._dtBox=null,this.host.requestUpdate()));let e;if("line"===t.kind){const s=t.x1*ol,i=t.y1*this.host._decorH,o=t.x2*ol,a=t.y2*this.host._decorH;e={id:t.id,x:Math.min(s,o),y:Math.min(i,a),w:Math.abs(o-s),h:Math.abs(a-i)}}else if("furniture"===t.kind||"rect"===t.kind||"ellipse"===t.kind)e={id:t.id,x:t.x*ol,y:t.y*this.host._decorH,w:t.w*ol,h:t.h*this.host._decorH};else{const s=this.host.renderRoot.querySelector(`text.dtext[data-id="${t.id}"]`);if(!s||"function"!=typeof s.getBBox)return;let i;try{i=s.getBBox()}catch{return}if(!i||!i.width&&!i.height)return;e={id:t.id,x:i.x,y:i.y,w:i.width,h:i.height}}const s=this.host._dtBox;s&&s.id===e.id&&Math.abs(s.x-e.x)<.01&&Math.abs(s.y-e.y)<.01&&Math.abs(s.w-e.w)<.01&&Math.abs(s.h-e.h)<.01||(this.host._dtBox=e,this.host.requestUpdate())}_deleteDecor(t){if(!this.host._decorList.some(e=>e.id===t))return;const e=this._geometrySnapshot();this.host._curSpaceCfg.decor=this.host._decorList.filter(e=>e.id!==t),this.host._decorSel===t&&(this.host._decorSel=null),this._recordGeometry(this.host._t("history.decor_delete"),e),this._saveConfig(),this.host.requestUpdate()}_decorDeleteSel(){this.host._decorSel&&this._deleteDecor(this.host._decorSel)}_confirmDecorErase(){const t=this.host._decorEraseConfirm;this.host._decorEraseConfirm=null,t&&this._deleteDecor(t.id)}_furnFieldValue(t){return Math.round(100*(this.host._imperial?t/30.48:t/100))/100}_furnFieldToCm(t){return Me(this.host._imperial?30.48*t:100*t)}_furnPick(t){const e=De(t);this.host._furnPalette={symbol:t,w:e.w,h:e.h}}_furnPlace(t,e=!1){const s=this.host._furnPalette,i=this.host._curSpaceCfg;if(!s||!i)return;const o=ol,a=this.host._decorH,n=Ce(Pe(s.w,this.host._cellCm,this.host._gridPitch,o)),r=Ce(Pe(s.h,this.host._cellCm,this.host._gridPitch,o)),l=this._geometrySnapshot(),h=this._decorSnap(t);let c=h[0],d=h[1],p=0;const _=e?null:Ie(c,d,r*a,this.host._furnWalls,this.host._furnWallReach,this.host._gridPitch);_&&(c=_.cx,d=_.cy,p=_.angle);const u="df"+Date.now().toString(36)+Math.random().toString(36).slice(2,5),m={id:u,kind:"furniture",symbol:s.symbol,x:fe(c/o-n/2),y:fe(d/a-r/2),w:n,h:r,...ve(this.host._decorStyle,!1)};p&&(m.angle=Number(p.toFixed(2))),i.decor=[...this.host._decorList,m],this.host._decorSel=u,this.host._decorTool="select",this.host._furnPalette=null,this._recordGeometry(this.host._t("history.decor_add"),l),this._saveConfig(),this.host.requestUpdate()}_furnMoveUpdate(t){const e=this.host._decorMove;if("furniture"!==e.orig.kind)return;const s=e.orig,i=this.host._curSpaceCfg;if(!i)return;const o=ol,a=this.host._decorH,n=this._svgPoint(t),r=(s.x+s.w/2)*o+(n[0]-e.start[0]),l=(s.y+s.h/2)*a+(n[1]-e.start[1]);let h,c,d=Number(s.angle)||0;const p=t.shiftKey?null:Ie(r,l,s.h*a,this.host._furnWalls,this.host._furnWallReach,this.host._gridPitch);if(p)h=p.cx/o-s.w/2,c=p.cy/a-s.h/2,d=p.angle;else{const i=this._decorSnap([r-s.w/2*o,l-s.h/2*a],t.pointerType,e.id);h=i[0]/o,c=i[1]/a}h=fe(h),c=fe(c),(Math.abs(h-s.x)>1e-9||Math.abs(c-s.y)>1e-9||Math.abs(d-(Number(s.angle)||0))>1e-9)&&(e.moved=!0),i.decor=this.host._decorList.map(t=>{if(t.id!==e.id)return t;const s={...t,x:h,y:c};return d?s.angle=Number(d.toFixed(2)):delete s.angle,s}),this.host.requestUpdate()}_decorApplyBox(t,e){const s=this.host._curSpaceCfg;if(!s)return;const i=ol,o=this.host._decorH;s.decor=this.host._decorList.map(s=>{if(s.id!==t)return s;const a=xe(e,s.angle,t=>this._snap(t));return{...s,x:fe(a[0]/i),y:fe(a[1]/o),w:Math.max(this.host._gridPitch/i,Math.min(2*Xt,e.w/i)),h:Math.max(this.host._gridPitch/o,Math.min(2*Xt,e.h/o))}}),this.host._cfgEpoch++,this.host.requestUpdate()}_renderFurnPalette(){const t=this.host._furnPalette,e=this.host._t(this.host._imperial?"gs.unit_ft":"gs.unit_m");return o`
t.stopPropagation()}> +
+ ${this.host._t("furn.title")} + + +
+
+ ${Te.map(e=>o` +
${this.host._t(`furn.group_${e}`)}
+
+ ${Re(e).map(e=>o``)} +
`)} +
+ ${t?o`
+ + this.host._furnPalette={...t,w:this._furnFieldToCm(Number(e.target.value))}} /> + + this.host._furnPalette={...t,h:this._furnFieldToCm(Number(e.target.value))}} /> + ${this.host._t("furn.place_hint")} +
`:o`
${this.host._t("furn.pick_hint")}
`} +
`}_openBackdropDialog(t){if(!this.host._bdMovable||!this.host._bdRect)return;t?.preventDefault(),t?.stopPropagation(),this.host._bdDrag=null;const e=this.host._bdRect;this.host._backdropDialog={widthCm:ge(e.w,this.host._cellCm,this.host._gridPitch),heightCm:ge(e.h,this.host._cellCm,this.host._gridPitch),angle:this.host._angleField(e.angle)}}_saveBackdropDialog(){const t=this.host._backdropDialog,e=this.host._bdBase,s=this.host._bdRect;if(!t||!e||!s)return;const i=be(t.angle),o=this._geometrySnapshot(),a=Math.min(e.w*ht,Math.max(e.w*lt,Ft(ke(t.widthCm,this.host._cellCm,this.host._gridPitch),this.host._gridPitch))),n=Math.min(e.h*ht,Math.max(e.h*lt,Ft(ke(t.heightCm,this.host._cellCm,this.host._gridPitch),this.host._gridPitch))),r=s.x+s.w/2,l=s.y+s.h/2,h=xe({x:r-a/2,y:l-n/2},i,t=>this._snap(t));this._bdApply((h[0]-e.x)/ol,(h[1]-e.y)/ol,a/e.w,n/e.h,i),this.host._backdropDialog=null,this._recordGeometry(this.host._t("history.backdrop_transform"),o),this._saveConfig()}_bdApply(t,e,s,i,o){const a=this.host._curSpaceCfg;if(!a)return;a.plan_x=Number(fe(t).toFixed(6)),a.plan_y=Number(fe(e).toFixed(6)),delete a.plan_scale,a.plan_scale_x=Number(Math.min(ht,Math.max(lt,s)).toFixed(6)),a.plan_scale_y=Number(Math.min(ht,Math.max(lt,i)).toFixed(6));const n=be(o);n?a.plan_angle=Number(n.toFixed(2)):delete a.plan_angle,this.host._cfgEpoch++,this.host.requestUpdate()}_bdStart(t,e,s=!1){const i=this.host._bdBase,o=this.host._bdRect;if(!i||!o)return!1;const a=this._svgPoint(t),n=e?e[0]:0,r=e?e[1]:0,l=n>0?o.x:o.x+o.w,h=r>0?o.y:o.y+o.h;return this.host._bdDrag={kind:s?"rotate":e?"scale":"move",pid:t.pointerId,sx:a[0],sy:a[1],base:i,p0:this.host._bdParams,fx:l,fy:h,sgx:n,sgy:r,rect0:{x:o.x,y:o.y,w:o.w,h:o.h,angle:o.angle},before:this._geometrySnapshot(),moved:!1},nl(t),!0}_bdMove(t){const e=this.host._bdDrag;if(!e)return;const s=this._svgPoint(t),i=e.base;if("move"===e.kind){const t=e.rect0.x,o=e.rect0.y,a=this._snap([t+(s[0]-e.sx),o+(s[1]-e.sy)]),n=Math.abs(a[0]-t)>1e-9||Math.abs(a[1]-o)>1e-9;if(!n&&!e.moved)return;return e.moved||=n,void this._bdApply((a[0]-i.x)/ol,(a[1]-i.y)/ol,e.p0.sx,e.p0.sy,e.p0.angle)}if("rotate"===e.kind){const i=e.rect0.x+e.rect0.w/2,o=e.rect0.y+e.rect0.h/2,a=180*Math.atan2(e.sy-o,e.sx-i)/Math.PI;let n=e.p0.angle+(180*Math.atan2(s[1]-o,s[0]-i)/Math.PI-a);t.shiftKey||(n=5*Math.round(n/5)),n=be(n);const r=Math.abs(n-e.p0.angle)>1e-9;if(!r&&!e.moved)return;return e.moved||=r,void this._bdApply(e.p0.dx,e.p0.dy,e.p0.sx,e.p0.sy,n)}const o=Se(e.rect0,e.sgx,e.sgy,s[0],s[1],!t.shiftKey,this.host._gridPitch,Math.min(i.w,i.h)*lt),a=o.w/Math.max(1e-9,i.w),n=o.h/Math.max(1e-9,i.h),r=Math.abs(a-e.p0.sx)>1e-9||Math.abs(n-e.p0.sy)>1e-9||Math.abs(o.x-e.rect0.x)>1e-9||Math.abs(o.y-e.rect0.y)>1e-9;if(!r&&!e.moved)return;e.moved||=r;const l=xe(o,e.p0.angle,t=>this._snap(t));this._bdApply((l[0]-i.x)/ol,(l[1]-i.y)/ol,a,n,e.p0.angle)}_bdReset(){const t=this.host._curSpaceCfg;if(!t)return;const e=this._geometrySnapshot();delete t.plan_x,delete t.plan_y,delete t.plan_scale,delete t.plan_scale_x,delete t.plan_scale_y,delete t.plan_angle,this.host._bdDrag=null,this._recordGeometry(this.host._t("history.backdrop_transform"),e),this._saveConfig(),this.host._showToast(this.host._t("decor.backdrop_reset_done")),this.host.requestUpdate()}_bdUp(){const t=this.host._bdDrag;this.host._bdDrag=null,t?.moved&&(this._recordGeometry(this.host._t("history.backdrop_transform"),t.before),this._saveConfig()),this.host.requestUpdate()}_renderBackdropFrame(t){const e=this.host._bdRect;if(!this.host._bdActive||!e)return a;const s=.02*Math.max(t.w,t.h),i=s/4,o=e.x+e.w/2,n=e.y+e.h/2,r=be(e.angle),l=2.2*s;return ce` + + + {t.stopPropagation(),t.preventDefault(),this._bdStart(t,void 0,!0)}}> + + ${[[-1,-1,"nwse"],[1,-1,"nesw"],[1,1,"nwse"],[-1,1,"nesw"]].map(([t,o,a])=>{const n=t<0?e.x:e.x+e.w,r=o<0?e.y:e.y+e.h;return ce`{e.stopPropagation(),e.preventDefault(),this._bdStart(e,[t,o])}}>`})} + `}_renderEditorGroupLauncher(t){return this.host._editorSecondary.renderGroupLauncher(t,this.host._editorToolbarGroups,this.host._editorSecondaryCopy)}_runEditorContext(t,e){this.host._editorSecondary.runContext(t,this.host._editorSecondaryContextId,e)}_renderEditorGroupModel(t){return this.host._editorSecondary.renderGroupModel(t,this.host._editorSecondaryContextId,this.host._editorSecondaryCopy)}_renderDrawWallControl(){const t="column"===this.host._tool?1:0;return o``}_renderPlanSecondary(){const t=this.host._editorSecondaryContextId,e=this.host._physicalSel;if(e){if(!("partition"===e.kind?!!this.host._curSpaceCfg.partitions?.some(t=>t.id===e.id):"column"===e.kind?!!this.host._curSpaceCfg.wall_columns?.some(t=>t.id===e.id):!!this.host._curSpaceCfg.room_drafts?.some(t=>t.id===e.id)))return null;const s="partition"===e.kind?this.host._t("markup.partition"):"column"===e.kind?this.host._t("markup.column"):this.host._t("markup.add");return{contextId:t,kind:"selection",ariaLabel:this.host._t("editor.context_actions",{object:s}),visibleLabel:s,content:o` + + `}}const s="draw"===this.host._tool||"column"===this.host._tool,i="column"===this.host._tool?"markup.hint_column":"resize"===this.host._tool?"markup.hint_resize":"wallthick"===this.host._tool?"markup.hint_wallthick":null,n="draw"===this.host._tool?this.host._t(this.host._path.length?"markup.hint_points":"markup.hint_start",this.host._path.length?{n:this.host._path.length}:void 0):"";if(!s&&!i&&!n)return null;const r="draw"===this.host._tool&&this.host._path.length>0;return{contextId:t,kind:r?"operation":"tool",ariaLabel:this.host._t("editor.tool_options",{tool:this.host._t("draw"===this.host._tool?"markup.add":"column"===this.host._tool?"markup.column":"resize"===this.host._tool?"markup.resize":"markup.wallthick")}),content:o` + ${s?this._renderDrawWallControl():a} + ${n?o`${n}`:a} + ${i?o`${this.host._t(i)}`:a} + ${"draw"===this.host._tool&&this.host._path.length?o``:a}`}}_renderDecorSecondary(){const t=this.host._editorSecondaryContextId;if("furniture"===this.host._decorTool)return{contextId:t,kind:"palette",ariaLabel:this.host._t("editor.palette",{tool:this.host._t("decor.furniture")}),launcherId:"furniture",dismissPolicy:this.host._furnPalette?"stay-open-on-canvas":"outside",dismiss:()=>{"furniture"===this.host._decorTool&&(this.host._furnPalette=null,this.host._decorTool="select",this.host.requestUpdate())},content:this._renderFurnPalette()};const e=this.host._dtSel;if(e){const s=this.host._t(`decor.${e.kind}`);return{contextId:t,kind:"selection",ariaLabel:this.host._t("editor.context_actions",{object:s}),visibleLabel:s,content:o` + + `}}const s="line"===this.host._decorTool||"rect"===this.host._decorTool||"ellipse"===this.host._decorTool,i="rect"===this.host._decorTool||"ellipse"===this.host._decorTool,n="backdrop"===this.host._decorTool&&!!this.host._bdRect;return s||n?{contextId:t,kind:"tool",ariaLabel:this.host._t("editor.tool_options",{tool:this.host._t(s?`decor.${this.host._decorTool}`:"decor.backdrop")}),content:o` + ${s?o` + this.host._decorStyle={...this.host._decorStyle,...t.detail}}> + + ${i?o` + this.host._decorStyle={...this.host._decorStyle,fillColor:t.detail.color,fillOpacity:t.detail.opacity}}>`:a} + `:a} + ${n?o`${this.host._t("decor.backdrop_hint")}`:a}`}:null}_withBackdropReset(t){if(!this.host._bdMoved||this.host._bdDrag||"palette"===t?.kind)return t;const e=this.host._editorSecondaryContextId,s=o``;return t?{...t,kind:"selection"===t.kind?"mixed":t.kind,content:o`${t.content}${s}`}:{contextId:e,kind:"tool",ariaLabel:this.host._t("editor.tool_options",{tool:this.host._t("decor.backdrop")}),content:s}}_renderEditorSecondary(){if(!this.host._editing)return a;const t=this.host._editorSecondary.activeGroup(this.host._editorToolbarGroups),e=t?this._renderEditorGroupModel(t):"plan"===this.host._mode?this._renderPlanSecondary():"decor"===this.host._mode?this._withBackdropReset(this._renderDecorSecondary()):null;return this.host._editorSecondary.render(e,this.host._editorSecondaryDialogBlocked)}_renderDecorBar(){const t=[["select","mdi:cursor-default-outline","decor.select"],...this.host._bdRect?[["backdrop","mdi:image-move","decor.backdrop"]]:[],["line","mdi:vector-line","decor.line"],["rect","mdi:rectangle-outline","decor.rect"],["ellipse","mdi:ellipse-outline","decor.ellipse"],["text","mdi:format-text","decor.text"],["furniture","mdi:sofa-outline","decor.furniture"],["erase","mdi:eraser","decor.erase"]],e=this.host._geometryHistory.undoName,s=this.host._geometryHistory.redoName;return o`
+
+ ${t.map(([t,e,s])=>o``)} + ${this.host._editorToolbarGroups.map(t=>this._renderEditorGroupLauncher(t))} + + +
+
+ +
+
`}_renderDecorEraseConfirm(){const t=this.host._decorEraseConfirm,e=this.host._t(`decor.${t.kind}`);return o`this.host._decorEraseConfirm=null}> +

${this.host._t("confirm.erase_decor",{kind:e})}

+
+ + + +
+
`}_renderDecorTextDialog(){const t=this.host._decorTextDialog,e=(t.pickerEntity||"").trim(),s=e?this.host.hass?.states?.[e]:null;return o`this.host._decorTextDialog=null}> +
+ + ${""} + + this.host._decorTextDialog={...t,...e.detail}}> + +
this.host._decorTextDialog={...t,sizeCm:this.host._decorTextCm(Number(e.target.value))}} /> + ${this.host._t(this.host._imperial?"wallthick.unit_in":"wallthick.unit_cm")}
+ + this.host._decorTextDialog={...t,angle:e.target.value}} /> + + + this.host._decorTextDialog={...t,pickerEntity:e.target.value}} /> + + ${Object.keys(this.host.hass?.states||{}).map(t=>o``)} + + ${e?o` + + + `:a} +
+
+ + + +
+
`}_renderDecorShapeDialog(){const t=this.host._decorShapeDialog,e="rect"===t.kind||"ellipse"===t.kind,s=this.host._t("decor."+t.kind),i=this.host._t(this.host._imperial?"gs.unit_ft":"gs.unit_m");return o`this.host._decorShapeDialog=null}> +
+ ${"furniture"===t.kind?o` + + `:a} + this.host._decorShapeDialog={...t,...e.detail}}> + +
this.host._decorShapeDialog={...t,widthCm:this.host._decorSmallCm(Number(e.target.value))}} />${this.host._t(this.host._imperial?"wallthick.unit_in":"wallthick.unit_cm")}
+ ${"line"===t.kind?o` + +
+ + +
+ +
this.host._decorShapeDialog={...t,lengthCm:this.host._decorLargeCm(Number(e.target.value))}} /> + ${i}
`:o` + +
this.host._decorShapeDialog={...t,sizeWCm:this.host._decorLargeCm(Number(e.target.value))}} /> + ×this.host._decorShapeDialog={...t,sizeHCm:this.host._decorLargeCm(Number(e.target.value))}} /> + ${i}
`} + + this.host._decorShapeDialog={...t,angle:e.target.value}} /> + ${e?o` + this.host._decorShapeDialog={...t,fillColor:e.detail.color,fillOpacity:e.detail.opacity}}>`:a} +
+
+ + + +
+
`}_renderBackdropDialog(){const t=this.host._backdropDialog,e=this.host._t(this.host._imperial?"gs.unit_ft":"gs.unit_m");return o`this.host._backdropDialog=null}> +
+ +
this.host._backdropDialog={...t,widthCm:this.host._decorLargeCm(Number(e.target.value))}} /> + ×this.host._backdropDialog={...t,heightCm:this.host._decorLargeCm(Number(e.target.value))}} /> + ${e}
+ + this.host._backdropDialog={...t,angle:e.target.value}} /> +
+
+ + +
+
`}_cssPxToRender(t){const e=this.host._stageEl,s=this.host._viewOr(this.host._baseVb());return e?.clientWidth&&e.clientHeight?Math.max(s.w/e.clientWidth,s.h/e.clientHeight)*t:this.host._gridPitch/8*t}_deleteRoomClick(t){const e=this.host._spaceModel();if(!e)return;const s=[...e.rooms].reverse().find(e=>this._pointInRoom(t,e));s?s.id&&(this.host._roomDeleteDialog={roomId:s.id,name:s.name}):this.host._showToast(this.host._t("toast.delete_room_pick"))}_wallThickHit(t){const e=this.host._spaceModel();if(!e)return null;const s=6*this.host._gridPitch,i=this.host._openCuts();let o=null;const a=(t,e,i)=>{e>s||(!o||e100)return void this._showPhysicalRange(100);if("room"!==e.source.kind){const t=this._geometrySnapshot();if("partition"===e.source.kind){const t=(s.partitions||[]).find(t=>t.id===e.source.id);if(!t)return;if(0===n&&$t(s.openings,{kind:"partition",id:t.id}))return void this.host._showToast(this.host._t("toast.zero_wall_opening_conflict"));t.cm=n}else{const t=(s.room_drafts||[]).find(t=>t.id===e.source.id),i=t?.segments?.[e.source.segment];if(!i)return;i.cm=n}this.host._wallDialog=null;return this._commitPhysicalGeometry(this.host._t("history.wall_thickness"),t)&&this.host._showToast(this.host._t("toast.wallthick_set")),void this.host.requestUpdate()}const r=this._geometrySnapshot(),l=n>0?n:null;if(0===n){const i=.02*this.host._gridPitch,o=[e.a[0],e.a[1],e.b[0],e.b[1]],a=(s.rooms||[]).find(t=>t.id===e.roomId),n=new Set(t&&a&&Array.isArray(a.wall_ids)?a.wall_ids:(s.wall_segments||[]).filter(t=>{const e=[Number(t.a?.[0])*ol,Number(t.a?.[1])*ol],s=[Number(t.b?.[0])*ol,Number(t.b?.[1])*ol];return C(e,o)<=i&&C(s,o)<=i}).map(t=>String(t.id))),r=(s.openings||[]).some(t=>{if("partition"===t.host?.kind)return!1;if("wall"===t.host?.kind&&n.has(t.host.id))return!0;if(t.host)return!1;const e=[Number(t.x)*ol,Number(t.y)*ol];return e.every(Number.isFinite)&&C(e,o)<=i});if(r)return void this.host._showToast(this.host._t("toast.zero_wall_opening_conflict"))}let h,c=this.host._openCuts();if(n>0&&c.length){const o=[];if(t&&e.roomId){const t=(s.rooms||[]).find(t=>t.id===e.roomId),a=new Set(Array.isArray(t?.wall_ids)?t.wall_ids:[]);for(const t of s.wall_segments||[])a.has(t.id)&&o.push([Number(t.a?.[0])*ol,Number(t.a?.[1])*ol,Number(t.b?.[0])*ol,Number(t.b?.[1])*ol]);if(!o.length){const t=i.rooms.find(t=>t.id===e.roomId),s=t?R(t):null;for(let t=0;s&&t!o.some(e=>C([t[0],t[1]],e)<=a&&C([t[2],t[3]],e)<=a&&C([e[0],e[1]],t)<=a&&C([e[2],e[3]],t)<=a))}h=t&&e.roomId?Fe(s.walls,i.rooms,e.roomId,l,this.host._wallKeyPitch,c,ol):E(s.walls,e.a,e.b,l,this.host._wallKeyPitch,ol),h=this.host._normalizeWalls(h,c);for(const t of s.wall_segments||[])t.cm=Q(h,t.a,t.b,I,1);h.length?s.walls=h:delete s.walls,this.host._wallDialog=null,this._commitPhysicalGeometry(this.host._t("history.wall_thickness"),r)&&this.host._showToast(this.host._t(0===n?"toast.wallthick_cleared":"toast.wallthick_set")),this.host.requestUpdate()}_wallHatchDefs(t){if(!this.host._spaceWalls.length&&!this.host._physicalBodiesR().length&&!this.host._markup)return ce``;const e=Ne(this.host._cellCm);return ce` + + + + `}_renderWallThickUi(){const t=this.host._wallThickHover;return t&&t.d?ce``:ce``}_renderWallThickDialog(){const t=this.host._wallDialog;return t?o`
t.stopPropagation()}> +
+ + {this.host._wallDialog={...t,value:e.target.value}}} + @keydown=${t=>{"Enter"===t.key&&(t.preventDefault(),this._wallThickApply(!1))}} /> + ${this.host._t(this.host._imperial?"wallthick.unit_in":"wallthick.unit_cm")} +
+
+ ${"room"===t.source.kind?o``:a} + + +
+
`:o``}_openingAt(t){if(!this.host._openingsR.length)return null;const e=this.host._openingsR.flatMap(e=>{const s=e.angle*Math.PI/180,i=t[0]-e.rx,o=t[1]-e.ry,a=i*Math.cos(s)+o*Math.sin(s);return Math.abs(a)>e.rlen/2+ze(12,this.host._cellCm)?[]:[{o:e,localY:-i*Math.sin(s)+o*Math.cos(s)}]});if(!e.length)return null;const s=this.host._spaceModel();if(!s)return null;const i=this.host._openCuts(),o=this.host._openingWallIndexFor(s,i).value;return e.find(({o:t,localY:e})=>{const s="gate"===t.type?!t.flip_v:t.flip_v,i=t.partitionHost||this.host._spaceWalls.length||"gate"===t.type?this.host._openingFace(t,o,!!s):{ox:0,oy:0,cm:0,side:-1},a=Ae({type:t.type,length:t.rlen,angle:t.angle,amount:this.host._openingAmt(t),flipH:!!t.flip_h,flipV:!!t.flip_v,cellCm:this.host._cellCm,gridPitch:this.host._gridPitch,face:i});return Math.abs(e)<=a.hitHalf})?.o||null}_resolveOpeningPlacement(t){const e=this.host._openingPreset;if("opening"!==this.host._tool||!e)return null;const s=this.host._spaceModel();if(!s)return null;const i=this.host._openCuts(),o=this.host._openingWallIndexFor(s,i),a=`${o.key}|partitions:${this.host._cfgEpoch}`;this.host._openingPlacementIntervalsCache&&this.host._openingPlacementIntervalsCache.key===a||(this.host._openingPlacementIntervalsCache={key:a,value:[...z(s.rooms,this.host._spaceWalls,i,this.host._wallKeyPitch,this.host._cellCm,this.host._gridPitch,ol),...Oe(s.partitions,this.host._cellCm,this.host._gridPitch)]}),this.host._openingDimensionContextCache&&this.host._openingDimensionContextCache.key===a||(this.host._openingDimensionContextCache={key:a,value:Wr({rooms:s.rooms,walls:this.host._spaceWalls,openCuts:i,partitions:s.partitions,roomOpenings:this.host._roomWallOpeningInputs(),partitionCuts:this._partitionOpeningCuts(s),pitch:this.host._wallKeyPitch,cellCm:this.host._cellCm,gridPitch:this.host._gridPitch,coordScale:ol,epsilon:2e-4*this.host._gridPitch})});const n=Le({pointer:[t[0],t[1]],preset:e,geometryRevision:this.host._cfgEpoch,renderedLength:this.host._cmToUnits(e.lengthCm),intervals:this.host._openingPlacementIntervalsCache.value,baseTolerance:1.5*this.host._gridPitch,bodyPointerPadding:this._cssPxToRender("touch"===this.host._pointerModality.modality||"pen"===this.host._pointerModality.modality?10:6),gridStep:this.host._gridPitch});this.host._openingJambBlockCm=n.jambBlockedTarget?n.jambBlockedTarget.physicalHalfWidth/this.host._gridPitch*this.host._cellCm:null;const r=n.candidate;if(!r)return null;const l="gate"===r.type?!r.flipV:r.flipV;let h;if(r.host){const t=F({id:"preview",type:r.type,x:r.x/ol,y:r.y/ol,angle:r.angle,length:r.renderedLength/ol,host:r.host},s.partitions,ol,this.host._cellCm,this.host._gridPitch).resolved;h=t?Ee(t,l):{ox:0,oy:0,cm:0,side:-1}}else h=Be(o.value,{x:r.x,y:r.y,angle:r.angle,length:r.renderedLength,flip_v:l});const c="mi"===this.host.hass?.config?.unit_system?.length,d=Yr(r,this.host._openingDimensionContextCache.value).map(t=>({x:t.label[0],y:t.label[1],text:ee(t.distance/this.host._gridPitch*this.host._cellCm,c),dimension:t}));return{...r,face:h,measure:{labels:d,guide:r.measure.guide}}}_activateOpeningPlacement(t){this._activateMarkupTool("opening"),"opening"===this.host._tool&&(this.host._openingPreset=He(t,++this.host._openingPresetRevision),this.host._openingHoverCandidate=null,this.host._cursorPt=null)}_clearOpeningPlacement(t){this.host._openingHoverCandidate=null,this.host._openingJambBlockCm=null,t&&(this.host._openingPreset=null,this.host._openingRebindId=null)}_openingClick(t){const e=this.host._spaceModel();if(!e)return;const s=this.host._openingRebindId?null:this._openingAt(t);if(s)return void this._editOpening(s);const i=this.host._openingPreset;if(!i)return;const o=this.host._openingHoverCandidate,a=o&&Ge(o,[t[0],t[1]],i.revision,this.host._cfgEpoch)?o:this._resolveOpeningPlacement(t);if(!a){if(null!=this.host._openingJambBlockCm)return void this.host._showToast(this.host._t("opening.partition_jamb_margin",{distance:ee(this.host._openingJambBlockCm,this.host._imperial)}));const s=1.5*this.host._gridPitch,i=P(t,e.rooms,s);return i&&We(i.x,i.y,i.angle,this.host._openCuts(),s)?void this.host._showToast(this.host._t("toast.opening_on_zero_wall")):void this.host._showToast(this.host._t("toast.opening_no_wall"))}const n=this.host._openingRebindId?this.host._curSpaceCfg?.openings?.find(t=>t.id===this.host._openingRebindId):null;this.host._openingDialog={...n?{id:n.id,type:n.type,lengthCm:Math.round(n.length*ol/this.host._gridPitch*this.host._cellCm),lengthTouched:!1,contact:n.contact||"",lock:n.lock||"",invert:!!n.invert,flipH:!!n.flip_h,flipV:!!n.flip_v}:{type:a.type,lengthCm:a.lengthCm,contact:"",lock:"",invert:!1,flipH:a.flipH,flipV:a.flipV},...a.host?{host:a.host}:{},x:a.x,y:a.y,angle:a.angle},this.host._openingRebindId=null,this.host._openingHoverCandidate=null,this.host._cursorPt=null}_editOpening(t){this.host._openingDialog={id:t.id,type:t.type,lengthCm:Math.round(t.rlen/this.host._gridPitch*this.host._cellCm),lengthTouched:!1,contact:t.contact||"",lock:t.lock||"",invert:!!t.invert,flipH:!!t.flip_h,flipV:!!t.flip_v,..."partition"===t.host?.kind?{host:{...t.host}}:{},x:t.rx,y:t.ry,angle:t.angle}}_opPointerDown(t,e){if("plan"===this.host._mode&&this.host._spaceModel()&&"resize"!==this.host._tool){t.preventDefault(),t.stopPropagation();try{nl(t)}catch{}this.host._opDrag={id:e.id,moved:!1,sx:t.clientX,sy:t.clientY,dirty:!1,before:this._geometrySnapshot()}}}_opPointerMove(t,e){if(!this.host._opDrag||this.host._opDrag.id!==e.id)return;if(Math.abs(t.clientX-this.host._opDrag.sx)+Math.abs(t.clientY-this.host._opDrag.sy)<=3)return;const s=this.host._spaceModel();if(!s)return;const i=this._svgPoint(t),o=this.host._curSpaceCfg,a=o?.openings?.find(t=>t.id===e.id);if(!a)return;if("partition"===a.host?.kind){const t=s.partitions.find(t=>t.id===a.host.id);if(!t)return;const e=t.b[0]-t.a[0],o=t.b[1]-t.a[1],n=Math.hypot(e,o);if(!(n>1e-9))return;const r=e/n,l=o/n;if(Math.abs((i[0]-t.a[0])*l-(i[1]-t.a[1])*r)>4*this.host._gridPitch)return;const h=a.length*ol/2+L(t,this.host._cellCm,this.host._gridPitch);let c=(i[0]-t.a[0])*r+(i[1]-t.a[1])*l;if(c=Math.round(c/this.host._gridPitch)*this.host._gridPitch,c=Math.max(h,Math.min(n-h,c)),n<2*h-1e-9)return;const d=c/n,p=(t.a[0]+r*c)/ol,_=(t.a[1]+l*c)/this.host._spaceH;let u=180*Math.atan2(o,e)/Math.PI;u>=90?u-=180:u<-90&&(u+=180);const m=a.host.t!==d||a.x!==p||a.y!==_||a.angle!==u;return this.host._opDrag.moved=!0,m&&(this.host._opDrag.dirty=!0),a.host={...a.host,t:d},a.x=p,a.y=_,a.angle=u,this.host._opMeasure=null,m&&this.host._cfgEpoch++,void this.host.requestUpdate()}const n=P(i,s.rooms,4*this.host._gridPitch);if(!n)return;this.host._opDrag.moved=!0;const r=this._opRuler(n,a.length*ol);this.host._opMeasure=r.measure;const l=r.x/ol,h=r.y/this.host._spaceH,c=a.x!==l||a.y!==h||a.angle!==n.angle;if(c&&(this.host._opDrag.dirty=!0),a.x=l,a.y=h,a.angle=n.angle,Number(this.host._serverCfg?.model_version||0)>=9){const t=kr(a,o?.wall_segments||[]);t?a.host=t:delete a.host}c&&this.host._cfgEpoch++,this.host.requestUpdate()}_opRuler(t,e){const s=this.host._spaceModel()?.rooms||[],i=this.host._gridPitch/2;let o=t.x,a=t.y,n=je([o,a],t.angle,e,s,i);if(n&&n.centered&&(o!==n.wallCenter[0]||a!==n.wallCenter[1]))[o,a]=n.wallCenter,n=je([o,a],t.angle,e,s,i);else if(n){const[r,l]=n.wallA,[h,c]=n.wallB,d=h-r,p=c-l,_=Math.hypot(d,p);if(_>0){const h=this.host._gridPitch,c=Math.min(e/2,_/2);let u=Math.round(((o-r)*d+(a-l)*p)/_/h)*h;u=Math.max(c,Math.min(_-c,u)),o=r+u/_*d,a=l+u/_*p,n=je([o,a],t.angle,e,s,i)||n}}if(!n)return{x:o,y:a,angle:t.angle,measure:null};const r="mi"===this.host.hass?.config?.unit_system?.length,l=(t,e)=>({x:e[0],y:e[1],text:ee(t/this.host._gridPitch*this.host._cellCm,r)});return{x:o,y:a,angle:t.angle,measure:{labels:[l(n.sideA,n.midA),l(n.sideB,n.midB)],guide:n.centered?{x:n.wallCenter[0],y:n.wallCenter[1],angle:t.angle}:null}}}_opPointerUp(t,e){if(!this.host._opDrag||this.host._opDrag.id!==e.id)return;const s=this.host._opDrag,i=s.moved;this.host._opMeasure=null,i&&s.dirty&&this._commitPhysicalGeometry(this.host._t("history.move_opening"),s.before),i?window.setTimeout(()=>this.host._opDrag=null,0):this.host._opDrag=null}_opClick(t,e){"plan"===this.host._mode&&"resize"===this.host._tool||(t.stopPropagation(),this.host._opDrag?.moved||"plan"===this.host._mode&&this._editOpening(e))}_saveOpening(){const t=this.host._openingDialog,e=this.host._curSpaceCfg,s=this.host._spaceModel();if(!t||!e||!s)return;const i=this._geometrySnapshot(),o=this.host._spaceH,a=(e.openings||[]).find(e=>e.id===t.id),n={...a||{},id:t.id||"o"+Date.now().toString(36),type:t.type,x:t.x/ol,y:t.y/o,angle:t.angle,length:a&&!t.lengthTouched?a.length:this.host._cmToUnits(Math.max(20,t.lengthCm))/ol,...t.host?{host:{...t.host}}:{}};if("partition"===n.host?.kind){const t=qe(a,n)?H(n,s.partitions,ol,this.host._cellCm,this.host._gridPitch):F(n,s.partitions,ol,this.host._cellCm,this.host._gridPitch);if(!t.resolved){const e=s.partitions.find(t=>t.id===n.host.id);if("does-not-fit-jamb"===t.reason&&e){const t=L(e,this.host._cellCm,this.host._gridPitch);this.host._showToast(this.host._t("opening.partition_jamb_margin",{distance:ee(t/this.host._gridPitch*this.host._cellCm,this.host._imperial)}))}else this.host._showToast(this.host._t("opening.partition_orphan"));return}const i=(e.openings||[]).flatMap(t=>{if(!t.host||t.id===n.id)return[];const e=F(t,s.partitions,ol,this.host._cellCm,this.host._gridPitch).resolved;return e?[e]:[]});if(Ue(t.resolved,i))return void this.host._showToast(this.host._t("toast.opening_no_wall"));Object.assign(n,N(n,t.resolved,ol))}else if(Number(this.host._serverCfg?.model_version||0)>=9){const t=kr(n,e.wall_segments||[]);if(!t)return void this.host._showToast(this.host._t("toast.opening_no_wall"));n.host=t}else delete n.host;"passage"===t.type?(delete n.contact,delete n.lock,delete n.invert,delete n.flip_h,delete n.flip_v):(n.contact=t.contact||null,n.lock=("door"===t.type||"gate"===t.type)&&t.lock||null,n.invert=t.invert||void 0,n.flip_h="gate"!==t.type&&t.flipH||void 0,n.flip_v=t.flipV||void 0),e.openings=e.openings||[];const r=e.openings.findIndex(t=>t.id===n.id);r>=0?e.openings[r]=n:e.openings.push(n),this._commitPhysicalGeometry(this.host._t(t.id?"history.edit_opening":"history.add_opening"),i),this.host._openingDialog=null,this.host.requestUpdate()}_deleteOpening(){const t=this.host._openingDialog,e=this.host._curSpaceCfg;if(!t?.id||!e?.openings)return;const s=this._geometrySnapshot();e.openings=e.openings.filter(e=>e.id!==t.id),this._commitPhysicalGeometry(this.host._t("history.delete_opening"),s),this.host._openingDialog=null,this.host.requestUpdate()}_contactCandidates(){const t=[];for(const e of Object.keys(this.host.hass.states)){if(!this.host._openingEntityAvailable(e))continue;const s=e.split(".")[0];if("binary_sensor"!==s&&"cover"!==s)continue;const i=this.host.hass.states[e],o=["door","window","opening","garage_door","garage"].includes(i?.attributes?.device_class||"");("cover"!==s||o)&&t.push([e,i?.attributes?.friendly_name||e,o?0:1])}return t.sort((t,e)=>t[2]-e[2]||t[1].localeCompare(e[1])).map(([t,e])=>({value:t,label:e}))}_lockCandidates(){return Object.keys(this.host.hass.states).filter(t=>t.startsWith("lock.")&&this.host._openingEntityAvailable(t)).map(t=>({value:t,label:this.host.hass.states[t]?.attributes?.friendly_name||t})).sort((t,e)=>t.label.localeCompare(e.label))}_toggleOpeningEntityPicker(t){const e=this.host._openingDialog;if(!e)return;const s="contact"===t?!e.contactOpen:!e.lockOpen;this.host._openingDialog={...e,contactOpen:"contact"===t&&s,lockOpen:"lock"===t&&s}}_filterOpeningEntities(t,e){const s=this.host._openingDialog;s&&(this.host._openingDialog="contact"===t?{...s,contactFilter:e}:{...s,lockFilter:e})}_selectOpeningEntity(t,e){const s=this.host._openingDialog;s&&(this.host._openingDialog="contact"===t?{...s,contact:e,contactOpen:!1,contactFilter:""}:{...s,lock:e,lockOpen:!1,lockFilter:""})}_mergeClick(t){const e=this.host._spaceModel();if(!e)return;const s=e.rooms,i=[...s].reverse().find(e=>this._pointInRoom(t,e));if(!i?.id)return;const o=i.id;if(!this.host._mergeSel||this.host._mergeSel===o)return void(this.host._mergeSel=this.host._mergeSel===o?null:o);const a=s.find(t=>t.id===this.host._mergeSel),n=a?R(a):null,r=R(i),l=n&&r?Ke(n,r):null;if(!l)return this.host._showToast(this.host._t("toast.merge_not_adjacent")),void(this.host._mergeSel=null);this.host._mergeDialog={aId:this.host._mergeSel,bId:o,poly:l,pick:"a"},this.host._mergeSel=null}_commitMerge(){const t=this.host._mergeDialog,e=this.host._curSpaceCfg;if(!t||!e||!this.host._spaceModel())return;const s=this._geometrySnapshot(),i=this.host._spaceH,o="a"===t.pick?t.aId:t.bId,a="a"===t.pick?t.bId:t.aId,n=e.rooms.find(t=>t.id===o);if(!n)return void(this.host._mergeDialog=null);n.poly=t.poly.map(t=>[t[0]/ol,t[1]/i]),delete n.x,delete n.y,delete n.w,delete n.h,e.rooms=e.rooms.filter(t=>t.id!==a);const r=this._commitPhysicalGeometry(this.host._t("history.merge_rooms"),s);this.host._mergeDialog=null,this.host._regSignature="",this.host._maybeRebuildDevices(),r&&this.host._showToast(this.host._t("toast.rooms_merged",{name:n.name||""}))}_splitClick(t){const e=this.host._spaceModel();if(!e)return;const s=e.rooms;if(!this.host._splitSel){const e=[...s].reverse().find(e=>this._pointInRoom(t,e));if(!e?.id)return;return void(this.host._splitSel={roomId:e.id,pts:[]})}const i=s.find(t=>t.id===this.host._splitSel.roomId),o=i?R(i):null;if(!i||!o)return void(this.host._splitSel=null);const a=.02*this.host._gridPitch,n=6*this.host._gridPitch,r=Ve(t,o),l=r&&Je(r,o,this.host._gridPitch)||r,h=r&&l&&Math.hypot(r[0]-t[0],r[1]-t[1])<=n?l:null,c=!!h&&Ye(h,o,a),d=this.host._splitSel.pts;if(!d.length)return c?void(this.host._splitSel={...this.host._splitSel,pts:[h]}):void this.host._showToast(this.host._t("toast.split_pick_wall"));if(!c){const e=this._snap(t);return qt(e,o,a)?void(this.host._splitSel={...this.host._splitSel,pts:[...d,e]}):void this.host._showToast(this.host._t("toast.split_pick_inside"))}const p=Kt(o,[...d,h],a);if(!p)return void this.host._showToast(this.host._t("toast.split_bad_cut"));this._resetRoomDialogFields();const[_,u]=p,m=k(_)>=k(u)?_:u,g=m===_?u:_;this.host._pendingSplit={roomId:i.id,mainPoly:m,newPoly:g},this.host._cursorPt=null,this.host._nameSel="",this.host._areaSel="",this.host._roomDialog=!0}_markupMove(t){if(!this.host._markup)return;if("column"===this.host._tool)return void(this.host._cursorPt=this._snap(this._svgPoint(t)));if("opening"===this.host._tool||"wallthick"===this.host._tool)return void(this.host._cursorPt=this._svgPoint(t));const e="draw"===this.host._tool&&!this.host._contourClosed,s="split"===this.host._tool&&!!this.host._splitSel?.pts?.length;if(!e&&!s)return;const i=this._svgPoint(t);if(e){const e=this._resolvePlanDrawPoint(i,t.shiftKey);return this.host._planSnapHover={contextKey:e.contextKey,candidate:e.candidate,conflicts:e.conflicts},this._syncPlanSnapConflictMarkers(e.conflicts),this.host._path.length?void(this.host._cursorPt=e.point):void this._syncPlanSnapActiveMarker(e.candidate)}this.host._cursorPt=this._snap(i)}_saveRoom(){(this.host._areaSel||this.host._nameSel.trim())&&(this.host._wallFaceBatch?this._decideWallFace(!0):this._commitRoom())}_decideWallFace(t){const e=this.host._wallFaceBatch;if(!e)return;const s=e.candidates[e.index];if(!s)return;if(!t&&(s.existing||s.repair)&&1===e.candidates.length)return this.host._wallFaceBatch=null,this.host._roomDialog=!1,this.host._nameSel="",this.host._areaSel="",void this.host.requestUpdate();const i=t?{candidate:s,create:!0,name:this.host._nameSel.trim(),area:this.host._areaSel||null,settings:this._roomSettingsFromDialog()?JSON.parse(JSON.stringify(this._roomSettingsFromDialog())):null}:{candidate:s,create:!1},o=[...e.decisions,i],a=e.index+1;if(a"number"==typeof t&&Number.isFinite(t)&&t>=0?t:null,o=.02*this.host._gridPitch,a=this.host._spaceModel();if(!a)return A;for(const e of this._planSnapGeometrySnapshot().value.segments){if("room"===e.sourceKind||C(t,[e.a[0],e.a[1],e.b[0],e.b[1]])>o)continue;if("partition"===e.sourceKind)return i(a.partitions.find(t=>t.id===e.sourceId)?.cm)??A;const s=e.sourceId.lastIndexOf(":"),n=s>=0?e.sourceId.slice(0,s):e.sourceId,r=s>=0?Number(e.sourceId.slice(s+1)):-1;return i(a.room_drafts.find(t=>t.id===n)?.segments[r]?.cm)??A}const n=Ra(Math.max(0,e.length-1),s,this.host._drawWallCm,A);for(let s=0;s+1[...t]);if(!e?.sourceKey.startsWith("active:"))return s;const i=/:(\d+)$/.exec(e.sourceKey),o=i?Number(i[1])+("b"===e.endpoint?1:0):-1;return o>=0&&oe.key===t.sourceKey),a=i.find(e=>e.key===t.targetSourceKey);if(!o||!a)return!1;const n="a"===t.endpoint?o.a:o.b;return!(Math.hypot(n[0]-t.from[0],n[1]-t.from[1])>s)&&(!(C(t.to,[a.a[0],a.a[1],a.b[0],a.b[1]])>s)&&!function(t,e){if(!t.sourceKey.startsWith("static:partition|"))return!1;const s=t.sourceKey.slice(17).split("|")[0];return!!s&&e.some(t=>"partition"===t.host?.kind&&t.host.id===s)}(t,this.host._curSpaceCfg?.openings||[]))}_applyWallRepair(t,e){const s=this.host._curSpaceCfg;if(!s||!this._validateWallRepair(t,e.activePath))return!1;const i=e=>{e[0]=t.to[0]/ol,e[1]=t.to[1]/ol};if(t.sourceKey.startsWith("active:")){const o=/:(\d+)$/.exec(t.sourceKey),a=o?Number(o[1])+("b"===t.endpoint?1:0):-1;if(a<0||a>=e.activePath.length)return!1;e.activePath[a]=[...t.to];const n=(s.room_drafts||[]).find(t=>t.id===e.activeDraftId);return n?.points?.[a]&&i(n.points[a]),!0}if(!t.sourceKey.startsWith("static:"))return!1;const[o,a]=t.sourceKey.slice(7).split("|");if("partition"===o){const e=(s.partitions||[]).find(t=>t.id===a);if(!e)return!1;const o=[e.a[0]*ol,e.a[1]*ol],n=[e.b[0]*ol,e.b[1]*ol],r=Math.hypot(o[0]-t.from[0],o[1]-t.from[1])<=2e-4*this.host._gridPitch?e.a:Math.hypot(n[0]-t.from[0],n[1]-t.from[1])<=2e-4*this.host._gridPitch?e.b:null;return!!r&&(i(r),!0)}if("draft"===o){const e=a.lastIndexOf(":"),o=e>=0?a.slice(0,e):"",n=e>=0?Number(a.slice(e+1)):-1,r=(s.room_drafts||[]).find(t=>t.id===o);if(!r?.points?.[n]||!r?.points?.[n+1])return!1;const l=[n,n+1].find(e=>Math.hypot(r.points[e][0]*ol-t.from[0],r.points[e][1]*ol-t.from[1])<=2e-4*this.host._gridPitch);return null!=l&&(i(r.points[l]),!0)}return!1}_applyWallFaceBatch(){const t=this.host._wallFaceBatch,e=this.host._curSpaceCfg,s=this.host._spaceModel();if(!t||!e||!s)return;const i=(t,e)=>{this.host._showToast(this.host._t(t,e)),this._roomDialogCancel()},o=t.decisions.filter(t=>t.create),a=o.map(t=>t.candidate.ring),n=s.rooms;for(let t=0;ts.id!==e&&!!R(s)&&$(a[t],R(s)));if(s)return void i("toast.room_overlap",{name:s.name||""})}if((e.rooms||[]).length+o.length>400)return void i("toast.physical_limit");if(o.some(t=>t.candidate.split&&!e.rooms.some(e=>e.id===t.candidate.split.roomId)))return void i("toast.contour_cannot_close");const r=o.map(t=>t.candidate.repair).filter(t=>!!t);if(r.length>1||r[0]&&!this._validateWallRepair(r[0],t.activePath))return void i("toast.wall_repair_changed");const l=this._activePathWithRepair(t.activePath,r[0]),h=2e-4*this.host._gridPitch,c=new Map;for(let e=0;et.candidate.atomKeys)),e=o.some(t=>t.candidate.consumeAllActive);for(const s of tn(this._wallGraphSources(l),h)){const i=s.sourceKeys.find(t=>c.has(t));!i||e||t.has(s.key)||d.push({a:s.a,b:s.b,cm:c.get(i)})}}else d.push(...Fa(l,Ra(l.length-1,t.activeCms,this.host._drawWallCm,A)));if((e.partitions||[]).length+d.length>al)return void i("toast.physical_limit");const p=o.map(t=>t.candidate.ring.map(()=>"")),_=t.activeDraftId?(e.room_drafts||[]).find(e=>e.id===t.activeDraftId):null,u=this._draftSegmentsForPath(t.activePath,_,t.activeCms),m=(t,e)=>{const s=`${t[0].toFixed(9)},${t[1].toFixed(9)}`,i=`${e[0].toFixed(9)},${e[1].toFixed(9)}`;return s<=i?`${s}|${i}`:`${i}|${s}`};for(let e=0;et.candidate.ring.forEach((s,i)=>{const o=t.candidate.ring[(i+1)%t.candidate.ring.length];C(s,n)<=h&&C(o,n)<=h&&r.push({kind:"room",owner:e,edge:i,a:s,b:o})})),d.forEach((t,e)=>{C(t.a,n)<=h&&C(t.b,n)<=h&&r.push({kind:"partition",owner:e,edge:0,a:t.a,b:t.b})});const l=[(i[0]+a[0])/2,(i[1]+a[1])/2];r.sort((t,e)=>{const s=[t.a[0],t.a[1],t.b[0],t.b[1]],o=[e.a[0],e.a[1],e.b[0],e.b[1]];return(C(l,s)<=h?0:1)-(C(l,o)<=h?0:1)||(C(i,s)<=h?0:1)-(C(i,o)<=h?0:1)||m(t.a,t.b).localeCompare(m(e.a,e.b))});const c=r[0];if(!c)continue;const _=m(c.a,c.b);for(const t of r)m(t.a,t.b)===_&&("room"===t.kind?p[t.owner][t.edge]=s:d[t.owner].id=s)}const g=this._geometrySnapshot();if(r[0]&&!this._applyWallRepair(r[0],t))return void i("toast.wall_repair_changed");const f=o.some(t=>!!t.candidate.split),b=f?kt(s.rooms,e.walls,this.host._openCuts(),this.host._wallKeyPitch,this.host._cellCm,this.host._gridPitch,ol):null,y=f&&Array.isArray(e.walls)?e.walls:null;for(const t of o){const s=t.candidate.split;if(!s)continue;const o=e.rooms.find(t=>t.id===s.roomId);if(!o)return void i("toast.contour_cannot_close");o.poly=s.mainPoly.map(t=>[t[0]/ol,t[1]/this.host._spaceH]),delete o.x,delete o.y,delete o.w,delete o.h}const v=Date.now().toString(36),w=[];if(o.forEach((t,s)=>{const i=t.area?this.host.hass.areas[t.area]?.name:"",o={id:`r${v}-${s}`,name:t.name||i||this.host._t("room.default_name"),area:t.area||null,poly:t.candidate.ring.map(t=>[t[0]/ol,t[1]/this.host._spaceH]),...p[s].some(Boolean)?{wall_ids:p[s]}:{},...t.settings?{settings:JSON.parse(JSON.stringify(t.settings))}:{}};e.rooms.push(o),w.push({room:o,decision:t})}),t.activeDraftId&&Array.isArray(e.room_drafts)&&(e.room_drafts=e.room_drafts.filter(e=>e.id!==t.activeDraftId),e.room_drafts.length||delete e.room_drafts),d.length&&(e.partitions||=[],d.forEach((t,s)=>e.partitions.push({id:t.id||`partition-${v}-${s}`,a:[t.a[0]/ol,t.a[1]/ol],b:[t.b[0]/ol,t.b[1]/ol],cm:t.cm}))),f){const t=this.host._normalizeWalls(b,this.host._openCuts());t.length?e.walls=t:y?.length?e.walls=y:delete e.walls}if(w.length){this.host._cfgEpoch++;const s=this.host._openCuts(),i=this.host._spaceModel();if(!i)return;let o=e.walls;for(const{room:e,decision:a}of w){const n=a.candidate.ring,r=this._wallSourceCmAt(n[0],t.activePath,t.activeCms);o=Xe(o,i.rooms,e.id,r,this.host._wallKeyPitch,s,ol);for(const a of z(i.rooms,o,s,this.host._wallKeyPitch,this.host._cellCm,this.host._gridPitch,ol)){if(a.roomId!==e.id||"outer"!==a.kind)continue;const s=[(a.a[0]+a.b[0])/2,(a.a[1]+a.b[1])/2];o=E(o,a.a,a.b,this._wallSourceCmAt(s,t.activePath,t.activeCms),this.host._wallKeyPitch,ol)}}const a=this.host._normalizeWalls(o,s);a.length?e.walls=a:delete e.walls}if(!this._commitPhysicalGeometry(this.host._t("history.wall_face_batch"),g))return;delete this.host._resumeDraftBySpace[this.host._space],this.host._path=[],this.host._activeDraftId=null,this.host._draftSegmentCms=[],this.host._closingWallCm=null,this.host._wallFaceBatch=null,this.host._roomDialog=!1,this.host._nameSel="",this.host._areaSel="",this.host._regSignature="",this.host._maybeRebuildDevices();const x=new Set(o.map(t=>t.area).filter(t=>!!t));if(x.size){const t={...this.host._layout};for(const e of this.host._devices){if(!x.has(e.area||"")||e.space!==this.host._space)continue;if(this.host._layout[e.id])continue;const s=this.host._defPos[e.id];s&&(t[e.id]={s:this.host._space,x:s.x/ol,y:s.y/ol},this.host._dirtyPos.add(e.id))}this.host._layout=t,this.host._persistLayout()}this.host._showToast(this.host._t(o.length?"toast.wall_rooms_saved":"toast.wall_chain_saved",{n:o.length}))}_commitRoom(){const t=this.host._curSpaceCfg,e=this.host._spaceModel();if(!t||!e)return;const s=this._geometrySnapshot(),i=this.host._spaceH,o=!!this.host._pendingSplit,a=o?kt(e.rooms,t.walls,this.host._openCuts(),this.host._wallKeyPitch,this.host._cellCm,this.host._gridPitch,ol):null,n=o&&Array.isArray(t.walls)?t.walls:null;let r;if(this.host._pendingSplit){const e=t.rooms.find(t=>t.id===this.host._pendingSplit.roomId);if(!e)return this.host._pendingSplit=null,this.host._splitSel=null,void(this.host._roomDialog=!1);e.poly=this.host._pendingSplit.mainPoly.map(t=>[t[0]/ol,t[1]/i]),delete e.x,delete e.y,delete e.w,delete e.h,r=this.host._pendingSplit.newPoly}else{if(!this.host._contourClosed)return;r=this.host._path.slice(0,-1)}const l=this.host._areaSel?this.host.hass.areas[this.host._areaSel]?.name:"",h={id:"r"+Date.now().toString(36),name:this.host._nameSel||l||this.host._t("room.default_name"),area:this.host._areaSel||null,poly:r.map(t=>[t[0]/ol,t[1]/i]),...this._roomSettingsFromDialog()?{settings:this._roomSettingsFromDialog()}:{}};if(!o&&this.host._activeDraftId){const e=Array.isArray(t.room_drafts)?t.room_drafts.find(t=>t.id===this.host._activeDraftId):null,s=Ra(r.length,[...this.host._draftSegmentCms,this.host._closingWallCm??void 0],this.host._drawWallCm,A),i=this._draftSegmentsForPath(this.host._path,e,s);i.length===r.length&&i.every(t=>!!t.id)&&(h.wall_ids=i.map(t=>t.id))}if(t.rooms.push(h),!o&&this.host._activeDraftId&&Array.isArray(t.room_drafts)&&(t.room_drafts=t.room_drafts.filter(t=>t.id!==this.host._activeDraftId),t.room_drafts.length||delete t.room_drafts),o){const e=this.host._normalizeWalls(a,this.host._openCuts());e.length?t.walls=e:n?.length?t.walls=n:delete t.walls}if(!o){const e=Ra(r.length,[...this.host._draftSegmentCms,this.host._closingWallCm??void 0],this.host._drawWallCm,A),s=e[0];if(null!=s){this.host._cfgEpoch++;const i=this.host._openCuts(),o=this.host._spaceModel();if(!o)return;let a=Xe(t.walls,o.rooms,h.id,s,this.host._wallKeyPitch,i,ol);for(const t of z(o.rooms,a,i,this.host._wallKeyPitch,this.host._cellCm,this.host._gridPitch,ol)){if(t.roomId!==h.id||"outer"!==t.kind)continue;const s=[(t.a[0]+t.b[0])/2,(t.a[1]+t.b[1])/2],i=r.findIndex((t,e)=>{const i=r[(e+1)%r.length];return C(s,[t[0],t[1],i[0],i[1]])<=.02*this.host._gridPitch});i>=0&&(a=E(a,t.a,t.b,e[i],this.host._wallKeyPitch,ol))}a=this.host._normalizeWalls(a,i),a.length?t.walls=a:delete t.walls}}const c=this._commitPhysicalGeometry(this.host._t(o?"history.split_room":"history.add_room"),s);this.host._path=[],delete this.host._resumeDraftBySpace[this.host._space],this.host._activeDraftId=null,this.host._draftSegmentCms=[],this.host._closingWallCm=null,this.host._pendingSplit=null,this.host._splitSel=null;const d=this.host._areaSel;if(this.host._areaSel="",this.host._nameSel="",this.host._roomDialog=!1,this.host._regSignature="",this.host._maybeRebuildDevices(),!c)return;let p=0;if(d){const t=ol,e={...this.host._layout};for(const s of this.host._devices){if(s.area!==d||s.space!==this.host._space)continue;if(p++,this.host._layout[s.id])continue;const i=this.host._defPos[s.id];i&&(e[s.id]={s:this.host._space,x:i.x/ol,y:i.y/t},this.host._dirtyPos.add(s.id))}this.host._layout=e,this.host._persistLayout()}const _=this.host._model.find(t=>t.id===this.host._space)?.rooms.length||0;this.host._showToast(d?this.host._t("toast.room_saved",{n:_,added:p}):this.host._t("toast.room_saved_no_area",{n:_}))}_cancelPath(){this.host._wallRepairDiagnostic=null,this.host._activeDraftId&&(this.host._resumeDraftBySpace[this.host._space]=this.host._activeDraftId),this.host._path=[],this.host._activeDraftId=null,this.host._draftSegmentCms=[],this.host._closingWallCm=null,this._clearPlanSnapHover(),this.host._roomDialog=!1,this.host._pendingSplit=null,this.host._wallFaceBatch=null,this.host._splitSel=null,this.host._mergeSel=null,this.host._mergeDialog=null,this.host._physicalSel=null,this.host._physicalDrag=null,this.host._physicalRotate=null,this._clearOpeningPlacement(!0)}_resumeLastDraft(){const t=this.host._resumeDraftBySpace[this.host._space];if(!t)return;const e=this.host._spaceModel()?.room_drafts.find(e=>e.id===t);e?(this.host._activeDraftId=t,this.host._path=e.points.map(t=>[...t]),this.host._draftSegmentCms=this._adoptDraftCms(this.host._path,e.segments.map(t=>t.cm),e.id),this._clearPlanSnapHover()):delete this.host._resumeDraftBySpace[this.host._space]}_roomDialogCancel(){if(this.host._roomDialog=!1,this.host._roomEditId)return this.host._roomEditId=null,this.host._nameSel="",void(this.host._areaSel="");if(this.host._wallFaceBatch){const t=this.host._wallFaceBatch;return this.host._wallFaceBatch=null,this.host._path=t.activePath.map(t=>[...t]),this.host._draftSegmentCms=[...t.activeCms],this.host._activeDraftId=t.activeDraftId,this.host._nameSel="",this.host._areaSel="",void this.host.requestUpdate()}if(this.host._pendingSplit)return this.host._pendingSplit=null,void(this.host._splitSel=null);this.host._undoPoint()}_openDeviceInbox(){this.host._deviceInboxReturn=null,this.host._deviceInbox=this.host._deviceInbox||{tab:"on_plan",search:"",showEntities:!1,onlyNew:!1,limit:100}}_closeMarkerDialog(){if(this.host._markerDialog=null,this.host._deviceInboxReturn){const t={...this.host._deviceInboxReturn};this.host._deviceInbox=t,this.host._deviceInboxReturn=null,t.anchor&&this.host.updateComplete.then(()=>requestAnimationFrame(()=>{const e=`.device-inbox-row[data-binding="${CSS.escape(t.anchor)}"]`;this.host.renderRoot.querySelector(e)?.scrollIntoView({block:"nearest"})}))}}_deviceInboxCandidates(t){return ca({hass:this.host._planHass,devices:this.host._devices,markers:this.host._markers,showEntities:t,labels:{device:this.host._t("marker.sub_device"),z2mGroup:this.host._t("marker.sub_z2m_group"),group:this.host._t("marker.sub_group"),helper:this.host._t("marker.sub_helper"),entity:this.host._t("marker.sub_entity")}})}_deviceInboxRows(){const t=this.host._deviceInbox||this.host._deviceInboxReturn,e=!!t?.showEntities,s=[this.host._haRegistry.revision,this.host._cfgRev,this.host._cfgEpoch,this.host._regSignature,this.host._newSyncKey,e?1:0,this.host._showAll?1:0,d(this.host.hass,this.host._config?.language)].join("|");if(this.host._deviceInboxMemo?.key===s)return this.host._deviceInboxMemo.rows;const i=this._deviceInboxCandidates(e),o=new Set(i.map(t=>t.value));for(const t of this.host._markers)t.binding&&"virtual"!==t.binding&&o.add(t.binding);for(const t of this.host._devices)t.bindingKind&&"virtual"!==t.bindingKind&&t.bindingRef&&o.add(`${t.bindingKind}:${t.bindingRef}`);const a=new Map;for(const t of o)a.set(t,this.host._bindingStatus(t));const n=this.host._areaToSpace,r={};for(const[t,e]of Object.entries(this.host.hass?.areas||{}))r[t]=n[t]?.room?.name||e?.name||t;for(const[t,e]of Object.entries(n))r[t]=e.room.name||r[t]||t;const l=Object.fromEntries(this.host._model.map(t=>[t.id,t.title])),h=Object.fromEntries(Object.entries(n).map(([t,e])=>[t,e.space])),c={},p=new Map;for(const[t,e]of Object.entries(this.host._fullRegistryHass.entities||{})){const s=String(e?.platform||"").trim();if(s&&(c[`entity:${t}`]=s),s&&e?.device_id){const t=p.get(e.device_id)||new Set;t.add(s),p.set(e.device_id,t)}}for(const[t,e]of p)c[`device:${t}`]=[...e].sort().join(", ");const _={};for(const[t,e]of Object.entries(this.host._fullRegistryHass.devices||{})){const s=`device:${t}`,i=p.get(t)||new Set,o=Array.isArray(e?.identifiers?.[0])?String(e.identifiers[0][0]||""):"",a=[o,...i].some(t=>this.host._excluded.has(t));"service"===e?.entry_type?_[s]="service_entry":a?_[s]="excluded_integration":"Group"===e?.model?_[s]="grouped_light":/scene/i.test(e?.model||"")?_[s]="excluded_domain":(/bridge/i.test(`${e?.model||""}${e?.name||""}`)||"myheat"===o&&e?.via_device_id)&&(_[s]="represented_by_parent")}const u=function(t){const{devices:e,markers:s,candidates:i,statuses:o,newDeviceIds:a,showHiddenOnPlan:n,areaNames:r={},spaceNames:l={},spaceByArea:h={},integrationByBinding:c={},reasonByBinding:d={}}=t,p=new Map;for(const t of e)!t.virtual&&t.bindingKind&&"virtual"!==t.bindingKind&&t.bindingRef&&p.set(`${t.bindingKind}:${t.bindingRef}`,t);const _=new Map,u=new Map;for(const t of s)t?.binding&&"virtual"!==t.binding&&(t.removed?u.set(t.binding,t):_.set(t.binding,t));const m=new Map(i.map(t=>[t.value,t])),g=new Set([...p.keys(),..._.keys(),...m.keys(),...[...u.keys()].filter(t=>m.has(t))]),f=[];for(const t of g){const e=p.get(t),s=_.get(t),i=s?void 0:u.get(t),g=m.get(t);let b;if(i&&g)b="readd";else if(!0===s?.hidden)b="hidden";else if(e||s)b="on_plan";else{if(!g)continue;b="available"}const y=e?.bindingStatus||o.get(t)||da,v=s||i,$=pa(t),w=e?.area||g?.areaId||v?.area||"",k=e?.space||v?.space||h[w]||"",x=e?.name||v?.name||g?.label||t,S=e?.model||v?.model||g?.model||"",M=!!g?.parentDeviceId&&p.has(`device:${g.parentDeviceId}`),D="readd"===b?"removed":"hidden"===b?s&&_a(s)?d[t]||"automatic_hidden":"manual_hidden":"on_plan"===b?s?"visible_explicit":"visible_auto":d[t]||(M?"represented_by_parent":w&&k?"available":"no_bound_room"),C=!!e&&(!e.hidden||n),P="active"===y.kind,I=C&&(P||"ha_disabled"===y.kind&&n),T=[x,S,c[t],r[w],l[k],t,g?.sub].filter(Boolean).join(" ").toLocaleLowerCase();f.push({key:t,binding:t,category:b,status:y,reason:D,deviceId:e?.id,markerId:v?.id,name:x,icon:e?.icon||v?.icon||("entity"===$?"mdi:code-braces":"mdi:devices"),model:S,integration:c[t]||"",areaId:w,areaName:r[w]||"",spaceId:k,spaceName:l[k]||"",kind:$,isNew:!!e&&a.has(e.id),searchText:T,canFind:I,canEdit:!!e||!!s,canHide:"on_plan"===b&&P,canShow:"hidden"===b&&P,canAdd:("available"===b||"readd"===b)&&P})}const b=t=>"active"===t.status.kind?1:0;return f.sort((t,e)=>t.category.localeCompare(e.category)||Number(e.isNew)-Number(t.isNew)||b(t)-b(e)||t.name.localeCompare(e.name)||t.binding.localeCompare(e.binding))}({devices:this.host._devices,markers:this.host._markers,candidates:i,statuses:a,newDeviceIds:this.host._newIds,showHiddenOnPlan:this.host._showAll,areaNames:r,spaceNames:l,spaceByArea:h,integrationByBinding:c,reasonByBinding:_});return this.host._deviceInboxMemo={key:s,rows:u},u}_deviceForInboxRow(t){const e=t.deviceId?this.host._devices.find(e=>e.id===t.deviceId):null;if(e)return e;const s=t.markerId?this.host._markers.find(e=>e.id===t.markerId):null;return!s||s.removed?null:{id:s.id,name:t.name,model:t.model,area:t.areaId,space:t.spaceId||this.host._space,hidden:!0===s.hidden||"ha_disabled"===t.status.kind,userHidden:!0===s.hidden,bindingStatus:t.status,icon:t.icon,entities:"active"===t.status.kind?t.status.enabledEntityIds:[],allEntities:t.status.allEntityIds,primary:"active"===t.status.kind?t.status.enabledEntityIds[0]:void 0,marker:s,bindingKind:t.kind,bindingRef:t.binding.slice(t.binding.indexOf(":")+1),pdfs:s.pdfs||[]}}_openInboxMarker(t,e=!1){const s=this.host._deviceInbox;if(s){if(this.host._deviceInboxReturn={...s,anchor:t.key},this.host._deviceInbox=null,!e){const e=this._deviceForInboxRow(t);return void(e?this._openMarkerDialog(e):this._closeMarkerDialog())}this._openMarkerDialog(),this.host._markerDialog?this.host._markerDialog={...this.host._markerDialog,bindingMode:"ha",binding:t.binding,bindingOpen:!1,showEntities:"entity"===t.kind,name:""}:this._closeMarkerDialog()}}async _setInboxHidden(t,e){const s=this.host._deviceInbox,i=this.host._serverCfg;if(!s||!i||s.busy||"active"!==t.status.kind)return;const o=i.markers||[],a=o.find(e=>!e.removed&&e.binding===t.binding);if(!e&&!a)return;const n=a?.id||Ze(t.binding,t.markerId,()=>`m_${Date.now().toString(36)}`),r=a?{...a,hidden:e}:{id:n,binding:t.binding,hidden:!0};i.markers=[...o.filter(e=>e.id!==n&&(e.binding!==t.binding||!0===e.removed)),r],this.host._deviceInbox={...s,busy:t.key,anchor:t.key};try{await this._saveConfigNow(),this.host._regSignature="",this.host._deviceInboxMemo=null,this.host._maybeRebuildDevices(),this.host._deviceInbox&&(this.host._deviceInbox={...this.host._deviceInbox,busy:void 0}),this.host._showToast(this.host._t("device_inbox.saved"))}catch(t){this.host._serverCfg===i&&(i.markers=o),this.host._deviceInbox&&(this.host._deviceInbox={...this.host._deviceInbox,busy:void 0}),this.host._showToast(this.host._t("toast.error",{err:this.host._errText(t)}))}}_findInboxDevice(t){if(!t.canFind)return;const e=this._deviceForInboxRow(t);if(!e)return;if(this.host._deviceInbox=null,e.space&&e.space!==this.host._space){if(!this.host._commitSpace(e.space))return;this.host._restoreZoom()}const s=()=>{const t=this.host._devices.find(t=>t.id===e.id)||e,s=this.host._pos(t);this.host._applyView(this.host._zoom,s.x,s.y),this.host._selId=t.id,this.host.requestUpdate(),window.setTimeout(()=>{this.host._selId===t.id&&(this.host._selId=null,this.host.requestUpdate())},1500)};requestAnimationFrame(()=>requestAnimationFrame(s))}_deviceInboxTabKey(t){if("ArrowLeft"!==t.key&&"ArrowRight"!==t.key)return;const e=this.host._deviceInbox;if(!e)return;const s=["on_plan","available","hidden","readd"],i="ArrowRight"===t.key?1:-1,o=(s.indexOf(e.tab)+i+s.length)%s.length;this.host._deviceInbox={...e,tab:s[o],limit:100,onlyNew:!1},t.preventDefault()}_openMarkerDialog(t){if(this.host._vacAllCamerasFor=null,this.host._vacAllCameraCache=null,t&&this.host._ackNewDevice(t.id),this.host._norm)if(t){const e=t.marker,s=Object.prototype.hasOwnProperty.call(e||{},"is_light"),i=Object.prototype.hasOwnProperty.call(e||{},"light_entity"),o=Object.prototype.hasOwnProperty.call(e||{},"glow_color"),a=Object.prototype.hasOwnProperty.call(e||{},"value_badge"),n=Object.prototype.hasOwnProperty.call(e||{},"tap_action"),r=Object.prototype.hasOwnProperty.call(e||{},"toggle_entity"),h=Qe(e?.glow_color),c=this.host._devicePresentation(t,!0).valueBadge,d=ts(this.host._planHass,t,this.host._devices),p=es(this.host._planHass,t,d);this.host._markerDialog={devId:t.id,name:t.name,binding:"virtual"===t.bindingKind?"virtual":t.bindingKind+":"+t.bindingRef,bindingMode:"virtual"===t.bindingKind?"virtual":"ha",bindingOpen:!1,showEntities:"entity"===t.bindingKind&&!!this.host._fullRegistryHass.entities[t.bindingRef||""]?.device_id,bindingFilter:"",icon:t.marker?.icon||"",autoIcon:t.icon||"",display:os(t.marker?.display),rippleColor:l(t.marker?.ripple_color,""),rippleSize:Number(t.marker?.ripple_size)>0?Number(t.marker.ripple_size):1.5,size:Number(t.marker?.size)>0?Number(t.marker.size):1,angle:Number(t.marker?.angle)||0,tapAction:is(t.marker?.tap_action,t.primary?.split(".")[0]),tapActionTouched:!1,originalHasTapAction:n,originalTapAction:t.marker?.tap_action,tapHintAnnouncement:"",toggleEntity:e?.toggle_entity||"",toggleEntityTouched:!1,originalHasToggleEntity:r,originalToggleEntity:e?.toggle_entity,tapTarget:t.marker?.tap_target||"",tapConfirm:!0===t.marker?.tap_confirm,runFilter:"",controls:ss(t.marker?.binding,t.marker?.controls,t.entities),controlsFilter:"",lightRole:!0===e?.is_light?"always":!1===e?.is_light?"never":"auto",lightRoleTouched:!1,originalHasIsLight:s,originalIsLight:e?.is_light,lightEntity:e?.light_entity||"",lightEntityTouched:!1,originalHasLightEntity:i,originalLightEntity:e?.light_entity,glowMode:null!=h?.bri?"fixed":h?"color":"auto",glowColor:h?.c||this.host._fillColors.glow_light.c,glowBrightness:Math.max(1,Math.round(100*(h?.bri??1))),glowColorDrafted:!!h,glowBrightnessDrafted:null!=h?.bri,glowTouched:!1,originalHasGlowColor:o&&(!!h||null===e?.glow_color),originalGlowColor:h||(null===e?.glow_color?null:void 0),valueBadgeEnabled:a?!0===e?.value_badge?.enabled:!!c,valueBadgeSource:e?.value_badge?.source||c?.source||p,valueBadgePosition:e?.value_badge?.position||c?.position||"right",valueBadgeTouched:!1,originalHasValueBadge:a,originalValueBadge:e?.value_badge,useClimateTemp:!0===t.marker?.use_climate_temp,glowRadius:Number(t.marker?.glow_radius_cm)>0?String(this.host._imperial?Math.round(Number(t.marker.glow_radius_cm)/30.48*10)/10:Math.round(Number(t.marker.glow_radius_cm))/100):"",model:t.model||"",link:t.link||"",description:t.description||"",pdfs:[...t.pdfs||[]],room:t.marker?.room_id?t.space+"#@"+t.marker.room_id:t.space&&t.area?t.space+"#"+t.area:"",hideFromPlan:!0===t.marker?.hidden,busy:!1}}else this.host._markerDialog={name:"",binding:"virtual",bindingMode:"virtual",bindingOpen:!1,showEntities:!1,bindingFilter:"",icon:"",autoIcon:"",display:"badge",rippleColor:"",rippleSize:1.5,size:1,angle:0,tapAction:"info",tapActionTouched:!1,originalHasTapAction:!1,originalTapAction:void 0,tapHintAnnouncement:"",toggleEntity:"",toggleEntityTouched:!1,originalHasToggleEntity:!1,originalToggleEntity:void 0,tapTarget:"",tapConfirm:!1,runFilter:"",controls:[],controlsFilter:"",lightRole:"auto",lightRoleTouched:!1,originalHasIsLight:!1,originalIsLight:void 0,lightEntity:"",lightEntityTouched:!1,originalHasLightEntity:!1,originalLightEntity:void 0,glowMode:"auto",glowColor:this.host._fillColors.glow_light.c,glowBrightness:100,glowColorDrafted:!1,glowBrightnessDrafted:!1,glowTouched:!1,originalHasGlowColor:!1,originalGlowColor:void 0,valueBadgeEnabled:!1,valueBadgeSource:null,valueBadgePosition:"right",valueBadgeTouched:!1,originalHasValueBadge:!1,originalValueBadge:void 0,useClimateTemp:!1,glowRadius:"",model:"",link:"",description:"",pdfs:[],room:"",hideFromPlan:!1,busy:!1,uploadId:"up_"+Date.now().toString(36)+Math.random().toString(36).slice(2,6)};else this.host._showToast(this.host._t("toast.marker_needs_server"))}_runCandidates(){const t=[];for(const e of as)for(const[s,i]of Object.entries(this.host.hass.states))s.startsWith(e+".")&&this.host._planEntityAvailable(s)&&t.push({value:s,label:i?.attributes?.friendly_name||s,sub:this.host._t("run."+e)});return t.sort((t,e)=>t.sub.localeCompare(e.sub)||t.label.localeCompare(e.label))}_bindingCandidates(){const t=ca({hass:this.host._planHass,devices:this.host._devices,markers:this.host._markers,showEntities:!!this.host._markerDialog?.showEntities,currentBinding:this.host._markerDialog?.binding,currentDeviceId:this.host._markerDialog?.devId,labels:{device:this.host._t("marker.sub_device"),z2mGroup:this.host._t("marker.sub_z2m_group"),group:this.host._t("marker.sub_group"),helper:this.host._t("marker.sub_helper"),entity:this.host._t("marker.sub_entity")}}),e=(this.host._markerDialog?.bindingFilter||"").toLowerCase().trim(),s=e?t.filter(t=>(t.label+" "+t.sub+" "+t.value).toLowerCase().includes(e)):t;return s.sort((t,e)=>t.label.localeCompare(e.label)),s.slice(0,200)}_backupErrorText(t){const e=t?.code??t?.error;if("string"==typeof e){const t=`backup.error.${e}`,s=this.host._t(t);if(s!==t)return s}return this.host._errText(t)}async _pickMarkerFiles(t){const e=t.target,s=e.files?[...e.files]:[];if(e.value="",!s.length||!this.host._markerDialog)return;const i=this.host._markerDialog.uploadId||this.host._markerDialog.devId||"new",o=[];for(const t of s)try{const e=new FormData;e.append("marker_id",i),e.append("file",t,t.name);const s=this.host.hass?.fetchWithAuth?await this.host.hass.fetchWithAuth("/api/houseplan/upload",{method:"POST",body:e}):await fetch("/api/houseplan/upload",{method:"POST",body:e,headers:this.host.hass?.auth?.data?.access_token?{authorization:`Bearer ${this.host.hass.auth.data.access_token}`}:{}}),a=await s.json().catch(()=>({}));if(!s.ok||a.error){const t={too_large:this.host._t("err.too_large",{mb:a.max_mb||50}),bad_ext:this.host._t("err.bad_ext"),unauthorized:this.host._t("err.unauthorized")};throw new Error(t[a.error]||a.error||"HTTP "+s.status)}o.push({name:a.name||t.name,url:a.url})}catch(e){this.host._showToast(this.host._t("toast.file_failed",{name:t.name,err:this.host._errText(e)}))}o.length&&this.host._markerDialog&&(this.host._markerDialog={...this.host._markerDialog,pdfs:[...this.host._markerDialog.pdfs,...o]},this.host._showToast(this.host._t("toast.files_attached",{n:o.length})))}_removeMarkerPdf(t){this.host._markerDialog&&(this.host._markerDialog={...this.host._markerDialog,pdfs:this.host._markerDialog.pdfs.filter(e=>e.url!==t)})}_markerLightFields(t){const e={};if(t.lightRoleTouched?"always"===t.lightRole?e.is_light=!0:"never"===t.lightRole&&(e.is_light=!1):t.originalHasIsLight&&(e.is_light=t.originalIsLight??null),t.lightEntityTouched?t.lightEntity&&(e.light_entity=t.lightEntity):t.originalHasLightEntity&&(e.light_entity=t.originalLightEntity??null),t.glowTouched){if("auto"!==t.glowMode){const s={c:l(t.glowColor,this.host._fillColors.glow_light.c)};"fixed"===t.glowMode&&(s.bri=Math.max(.01,Math.min(1,Math.round(t.glowBrightness)/100))),e.glow_color=s}}else t.originalHasGlowColor&&(e.glow_color=t.originalGlowColor??null);return e}_markerTapActionFields(t){return t.tapActionTouched?{tap_action:t.tapAction||null}:t.originalHasTapAction?{tap_action:t.originalTapAction??null}:{}}_markerToggleEntityFields(t){return(e={touched:t.toggleEntityTouched,originalHas:t.originalHasToggleEntity,original:t.originalToggleEntity,value:t.toggleEntity}).touched?e.value?{toggle_entity:e.value}:{}:e.originalHas?{toggle_entity:e.original??null}:{};var e}async _saveMarker(){const t=this.host._markerDialog;if(!t||t.busy)return;const e=this._effectiveMarkerTapAction(t);if("ha"===t.bindingMode&&(!t.binding||"virtual"===t.binding))return;if("virtual"===t.binding&&!t.name.trim())return void this.host._showToast(this.host._t("toast.virtual_name_required"));if("run"===e&&!t.tapTarget)return void this.host._showToast(this.host._t("toast.run_target_required"));if(t.valueBadgeTouched&&t.valueBadgeEnabled&&!t.valueBadgeSource)return void this.host._showToast(this.host._t("toast.value_badge_source_required"));if("ha"===t.bindingMode){const e=this.host._bindingStatus(t.binding),s=t.devId?this.host._markers.find(e=>e.id===t.devId):null,i=!s||s.binding!==t.binding;if("active"!==e.kind&&i)return void this.host._showToast(this.host._t("ha_disabled"===e.kind?"toast.ha_disabled_add":"toast.ha_binding_unverified"));if("ha_disabled"===e.kind&&!0===s?.hidden&&!t.hideFromPlan)return this.host._markerDialog={...t,hideFromPlan:!0},void this.host._showToast(this.host._t("entity"===e.reason?"toast.ha_disabled_show_entity":"toast.ha_disabled_show_device"))}const s=this.host._serverCfg;if(!s)return;const i=s.markers||[],o=ns(t.room);let a=o?.space||null;const n=o?.area||null,r=o?.roomId||null,l=Ze(t.binding,t.devId,()=>"v_"+Date.now().toString(36)),h=t.devId,c=h?this.host._devices.find(t=>t.id===h):null,d=o?.space||c?.space||null,p=d?this.host._spaceModelById(d):this.host._spaceModel();if(!p)return;const _=p.id;"virtual"!==t.binding||a||(a=_),this.host._markerDialog={...t,busy:!0};try{const o="virtual"===t.binding?[]:i.filter(e=>e.removed&&e.binding===t.binding).map(t=>t.id),d=o.length>0,u=ss(t.binding,t.controls,this.host._bindingEntities(t.binding)),m=i.find(t=>t.id===l||t.id===h)?.vacuum||null,g={id:l,vacuum:m,binding:t.binding,name:t.name.trim()||null,icon:t.icon||null,display:"badge"!==t.display?t.display:null,ripple_color:"icon_ripple"===t.display&&t.rippleColor?t.rippleColor:null,ripple_size:"icon_ripple"===t.display&&1.5!==t.rippleSize?t.rippleSize:null,size:1!==t.size?t.size:null,angle:t.angle?t.angle:null,...this._markerTapActionFields(t),...this._markerToggleEntityFields(t),tap_target:"run"===e&&t.tapTarget||null,tap_confirm:!!t.tapConfirm||null,controls:u.length?u:null,...this._markerLightFields(t),...this._markerValueBadgeFields(t),use_climate_temp:!!t.useClimateTemp||null,glow_radius_cm:(()=>{const e=sl(t.glowRadius);return null==e||e<=0?null:Math.round(this.host._imperial?30.48*e:100*e)})(),model:t.model.trim()||null,link:t.link.trim()||null,description:t.description.trim()||null,pdfs:t.pdfs,hidden:!!t.hideFromPlan};("virtual"===t.binding||t.room)&&(g.space=a,g.area=n,g.room_id=r);const f=c?.marker?.room_id??null,b=!!t.room&&null!=c&&(c.space!==a||c.area!==n||f!==r);let y=!1;const v=t.uploadId||h;if(v&&v!==l&&g.pdfs?.length)try{const t=await this.host.hass.callWS({type:"houseplan/files/migrate",from_id:v,to_id:l}),e=t?.mapping||{};g.pdfs=rs(g.pdfs,v,l,e),y=Object.keys(e).length>0}catch(t){this.host._showToast(this.host._t("toast.files_migrate_failed",{err:this.host._errText(t)}))}s.markers=i,h&&h!==l&&(s.markers=ls(s.markers,h,l)),h&&h!==l&&"derived_marker_state"===g.value_badge?.source?.kind&&g.value_badge.source.ref===`marker:${h}`&&(g.value_badge.source={kind:"derived_marker_state",ref:`marker:${l}`}),s.markers=s.markers.filter(t=>t.id!==l&&t.id!==h&&("virtual"===g.binding||t.binding!==g.binding)),s.markers.push(g);let $=null;const w=h?this.host._layout[h]:null,k=w?{s:w.s||c?.space||this.host._space,x:w.x,y:w.y}:h&&c&&this.host._defPos[h]?this.host._normPos(c.space,this.host._defPos[h].x,this.host._defPos[h].y):null;if(!d&&k&&k.s===_)l===h&&this.host._layout[l]&&!b||($={s:k.s,x:k.x,y:k.y},this.host._layout={...this.host._layout,[l]:$});else if(d||!this.host._layout[l]||b){let t=p.vb[0]+p.vb[2]/2,e=p.vb[1]+p.vb[3]/2;const s=r?p.rooms.find(t=>t.id===r):n?p.rooms.find(t=>t.area===n):void 0;s&&([t,e]=this.host._roomCenter(s)),$=this.host._normPos(_,t,e),this.host._layout={...this.host._layout,[l]:$}}if(await this._saveConfigNow(),$){const t=hs($);this.host._layout={...this.host._layout,[l]:t},this.host._noteLayoutRev(await this.host.hass.callWS({type:"houseplan/layout/update",device_id:l,pos:t}))}const x=new Set(o);h&&h!==l&&x.add(h),x.delete(l);for(const t of x)delete this.host._layout[t],await this.host.hass.callWS({type:"houseplan/layout/delete",device_id:t}).then(t=>this.host._noteLayoutRev(t)).catch(()=>{});y&&v&&await this.host.hass.callWS({type:"houseplan/files/cleanup",marker_id:v}).catch(()=>{}),this._closeMarkerDialog(),this.host._regSignature="",this.host._maybeRebuildDevices(),this.host._showToast(this.host._t("toast.marker_saved"))}catch(t){this.host._markerDialog&&(this.host._markerDialog={...this.host._markerDialog,busy:!1}),this.host._showToast(this.host._t("toast.error",{err:this.host._errText(t)}))}}async _deleteMarker(){const t=this.host._markerDialog;if(!t||t.busy||!t.devId)return;const e=t.devId?this.host._devices.find(e=>e.id===t.devId):null,s=this.host._markers.find(e=>e.id===t.devId);if(!e&&!s)return;const i=t.name||this.host._t("device.fallback");if(!confirm(this.host._t("confirm.remove_marker",{name:i})))return;const o=this.host._serverCfg;o.markers=o.markers||[];const a=o.markers,n=e?.id||s.id,r=e?"virtual"===e.bindingKind?"virtual":e.bindingKind&&e.bindingRef?`${e.bindingKind}:${e.bindingRef}`:"":s.binding;if(!r)return;const l=cs(o.markers,n,r,"virtual"===r);o.markers=ds(l.markers,l.cleanupIds);const h=l.cleanupIds;this.host._markerDialog={...t,busy:!0};try{await this._saveConfigNow();for(const t of h){delete this.host._layout[t],delete this.host._defPos[t],this.host._dirtyPos.delete(t),this.host._sentPos.delete(t);const e=this.host._activityRt.get(t);e&&clearTimeout(e.timer),this.host._activityRt.delete(t),this.host._vacRt.delete(t),delete this.host._vacSrvTrails[t],await this.host.hass.callWS({type:"houseplan/layout/delete",device_id:t}).then(t=>this.host._noteLayoutRev(t)).catch(()=>{}),await this.host.hass.callWS({type:"houseplan/files/cleanup",marker_id:t}).catch(()=>{}),await this.host.hass.callWS({type:"houseplan/trail/delete",marker_id:t}).catch(()=>{})}this.host._deviceInboxReturn&&(this.host._deviceInboxReturn={...this.host._deviceInboxReturn,tab:"readd",anchor:r}),this._closeMarkerDialog(),this.host._infoCard?.id===n&&this.host._closeInfoCard(),this.host._selId===n&&(this.host._selId=null),this.host._drag?.id===n&&(this.host._drag=null),this.host._regSignature="",this.host._maybeRebuildDevices(),this.host._showToast(this.host._t("toast.marker_removed"))}catch(t){this.host._serverCfg===o&&(o.markers=a),this.host._markerDialog&&(this.host._markerDialog={...this.host._markerDialog,busy:!1}),this.host._showToast(this.host._t("toast.error",{err:this.host._errText(t)}))}}_openSpaceDialog(t,e){if(this.host._serverStorage&&this.host._serverCfg)if("edit"===t){const s=this.host._serverCfg.spaces.find(t=>t.id===e);if(!s)return;const i=ps(s),o=s.settings?.custom_fill&&"object"==typeof s.settings.custom_fill?_s(s.settings.custom_fill):null,a="none"===i.fill?{...o||us,a:0}:o;this.host._spaceDialog={mode:t,spaceId:e,title:s.title,planUrl:s.plan_url||null,planFile:null,source:s.plan_url?"file":"draw",showBorders:i.showBorders,showNames:i.showNames,zeroWallStyle:gs(s),displayTouched:!0,hideDecor:i.hideDecor,hideOpenings:i.hideOpenings,roomColor:i.color,roomOpacity:i.opacity,fillMode:"none"===i.fill?"custom":i.fill,customFill:a,glowEnabled:i.glow,bgColor:i.bgColor,bgMode:"static"===s.settings?.bg_mode||"daynight"===s.settings?.bg_mode?s.settings.bg_mode:null,northDeg:Mt({},s.settings),sunRays:"boolean"==typeof s.settings?.sun_rays?s.settings.sun_rays:null,tempMin:i.tempMin,tempMax:i.tempMax,showLqi:i.showLqi??this.host._config?.show_signal??!0,cardFontScale:i.cardFontScale,labelTemp:i.labelTemp,labelHum:i.labelHum,labelLqi:i.labelLqi,labelLight:i.labelLight,cellCm:Number(s.cell_cm)>0?Number(s.cell_cm):5,cellCmInput:ms(Number(s.cell_cm)>0?Number(s.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1}}else this.host._spaceDialog={mode:t,title:"",planUrl:null,planFile:null,...Ri(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:vs,roomOpacity:ys,fillMode:"custom",customFill:{...us,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:bs,tempMax:fs,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:$s(this.host._imperial),cellCmInput:ms($s(this.host._imperial),this.host._imperial),cellCmTouched:!1,busy:!1};else this.host._showToast(this.host._t("toast.integration_missing"))}async _pickPlanFile(t){const e=t.target,s=e.files?.[0];if(!s||!this.host._spaceDialog)return;const i={"image/svg+xml":"svg","image/png":"png","image/jpeg":"jpg","image/webp":"webp"}[s.type]||(s.name.toLowerCase().endsWith(".svg")?"svg":"");if(!i)return void this.host._showToast(this.host._t("toast.plan_formats"));const o=new Uint8Array(await s.arrayBuffer());let a="";for(let t=0;t{const e=new Image;e.onload=()=>t(e.naturalWidth&&e.naturalHeight?e.naturalWidth/e.naturalHeight:1.414),e.onerror=()=>t(1.414),e.src=r});URL.revokeObjectURL(r),this.host._spaceDialog={...this.host._spaceDialog,planFile:{ext:i,b64:n,aspect:l,name:s.name}}}_useServerPlan(t){const e=this.host._spaceDialog;e&&(this.host._spaceDialog={...e,planUrl:t,planFile:null,pickSaved:!1,savedAspect:void 0},this.host._aspectJob=this._readPlanAspect(t))}async _readPlanAspect(t){for(let e=0;e<40;e++){const e=this.host._display(t);if(e){const s=await new Promise(t=>{const s=new Image;s.onload=()=>t(s.naturalWidth&&s.naturalHeight?s.naturalWidth/s.naturalHeight:0),s.onerror=()=>t(0),s.src=e}),i=this.host._spaceDialog;return i&&i.planUrl===t&&Number.isFinite(s)&&s>0?(this.host._spaceDialog={...i,savedAspect:s},s):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 e=this.host._spaceDialog;e?.saved&&(this.host._spaceDialog={...e,saved:e.saved.filter(e=>e.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 o`
${this.host._t("space.loading")}
`;const e=t.saved||[];if(!e.length)return o`
${this.host._t("space.no_saved")}
`;return o`
+ ${e.map(e=>{return o` +
+ +
+ ${e.name} + ${s=e.size,s>=1048576?(s/1048576).toFixed(1)+" MB":Math.round(s/1024)+" KB"}${e.used_by.length?" · "+this.host._t("space.used_by",{list:e.used_by.join(", ")}):""} +
+ + +
`;var s})} +
`}async _saveSpaceDialog(){const t=this.host._spaceDialog;if(!t||t.busy||!t.title.trim())return;if("file"===t.source&&!t.planFile&&!t.planUrl)return void this.host._showToast(this.host._t("toast.plan_required"));const e="create"===t.mode&&0===(this.host._serverCfg?.spaces.length||0);this.host._spaceDialog={...t,busy:!0};try{const s="create"===t.mode?"s"+Date.now().toString(36):t.spaceId;let i=null;if("file"===t.source&&t.planFile){i={url:(await this.host.hass.callWS({type:"houseplan/plan/set",space_id:s,ext:t.planFile.ext,data:t.planFile.b64})).url,aspect:t.planFile.aspect}}let o=t.savedAspect||null;!i&&"file"===t.source&&t.planUrl&&!o&&this.host._aspectJob&&(o=await this.host._aspectJob||null);const a=this.host._serverCfg;let n;if("create"===t.mode)n=Fi(s,t.title.trim()),a.spaces.push(n);else{if(n=a.spaces.find(t=>t.id===s),!n)throw new Error("space "+s+" is gone from the config");n.title=t.title.trim()}i?(n.plan_url=i.url,n.plan_aspect=i.aspect):"file"===t.source&&t.planUrl&&t.planUrl!==n.plan_url&&(n.plan_url=t.planUrl,n.plan_aspect=o),"draw"===t.source&&(n.plan_url=null,n.plan_aspect=null,delete n.plan_x,delete n.plan_y,delete n.plan_scale,delete n.plan_scale_x,delete n.plan_scale_y,delete n.plan_angle),n.settings={...n.settings||{},show_borders:t.showBorders,show_names:t.showNames,hide_decor:t.hideDecor||void 0,hide_openings:t.hideOpenings||void 0,room_color:t.roomColor,room_opacity:t.roomOpacity,bg_color:t.bgColor||void 0,bg_mode:t.bgMode||void 0,north_deg:t.northDeg??void 0,sun_rays:t.sunRays??void 0,fill_mode:t.fillMode,custom_fill:t.customFill||void 0,glow_enabled:t.glowEnabled,temp_min:Number.isFinite(t.tempMin)?Math.min(t.tempMin,t.tempMax):bs,temp_max:Number.isFinite(t.tempMax)?Math.max(t.tempMin,t.tempMax):fs,show_lqi:t.showLqi,card_font_scale:1!==t.cardFontScale?t.cardFontScale:void 0,label_temp:t.labelTemp,label_hum:t.labelHum,label_lqi:t.labelLqi,label_light:t.labelLight},n.zero_wall_style=t.zeroWallStyle,n.cell_cm=Number.isFinite(t.cellCm)&&t.cellCm>0?Math.max(.1,Math.min(el,t.cellCm)):5,await this._saveConfigNow(),this.host._spaceDialog=null,"create"===t.mode&&this.host._commitSpace(n.id),this.host._regSignature="",this.host._maybeRebuildDevices(),this.host._importQueue.length?this._openNextImport():e||this.host._importTotal>0?(this.host._importTotal=0,this.host._commitSpace(this.host._serverCfg.spaces[0]?.id||this.host._space),this._setMode("plan"),this.host._tool="draw",this.host._path=[],this.host._cursorPt=null,this._primeDrawWallField(),this.host._showToast(this.host._t(e&&!this.host._importTotal?"toast.space_added_onboard":"import.done"))):(this.host._showToast("create"===t.mode?this.host._t("toast.space_added"):this.host._t("toast.space_saved")),"create"===t.mode&&("plan"!==this.host._mode?this._setMode("plan"):(this.host._tool="draw",this.host._path=[],this.host._cursorPt=null,this._primeDrawWallField(),this.host._saveNav())))}catch(t){"conflict"!==t?.code&&await this.host._reloadConfigOnly(!0),this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,busy:!1}),this.host._showToast(this.host._t("toast.error",{err:this.host._errText(t)}))}}async _deleteSpace(){const t=this.host._spaceDialog;if(!t||"edit"!==t.mode)return;const e=this.host._serverCfg;if(!e)return;const s=e.spaces.find(e=>e.id===t.spaceId),i=Ni(e,this.host._layout||{},t.spaceId||""),o=1===e.spaces.length&&e.spaces[0]?.id===t.spaceId;if(!i.count||o){if(confirm(this.host._t("confirm.delete_space",{title:s.title}))){this.host._spaceDialog={...t,deleteBlockers:0,busy:!0};try{this.host._saveConfigDebounced.pending()&&this.host._saveConfigDebounced.flush(),this.host._persistLayout.pending()&&this.host._persistLayout.flush(),await this.host._writeChain;const e=await this.host.hass.callWS({type:"houseplan/space/delete",space_id:t.spaceId,expected_config_rev:this.host._cfgRev,expected_layout_rev:this.host._layoutRev}),[s,i]=await Promise.all([this.host.hass.callWS({type:"houseplan/config/get"}),this.host.hass.callWS({type:"houseplan/layout/get"})]);this.host._adoptStructuralResponses(s,i),this.host._cfgRev=e?.config_rev??this.host._cfgRev,this.host._layoutRev=e?.layout_rev??this.host._layoutRev,this.host._spaceDialog=null,this.host._space===t.spaceId&&this.host._commitSpace(this.host._serverCfg.spaces[0]?.id||""),this.host._regSignature="",this.host._maybeRebuildDevices(),this.host._showToast(this.host._t("toast.space_deleted"))}catch(e){"conflict"!==e?.code&&"space_in_use"!==e?.code||await Promise.all([this.host._reloadConfigOnly(!0),this.host._reloadLayoutOnly()]);const s=this.host._serverCfg;if(this.host._spaceDialog&&s){const e=Ni(s,this.host._layout||{},t.spaceId||""),i=1===s.spaces.length&&s.spaces[0]?.id===t.spaceId;this.host._spaceDialog={...this.host._spaceDialog,busy:!1,deleteBlockers:i?0:e.count}}this.host._showToast(this.host._t("toast.delete_failed",{err:this.host._errText(e)}))}}}else this.host._spaceDialog={...t,deleteBlockers:i.count}}async _saveConfigNow(){this.host._cfgEpoch++;try{await this._writeConfig()}catch(t){throw t?.physicalGeometryRolledBack?await this._reloadRejectedPhysicalWrite():"conflict"===t?.code&&await this.host._reloadConfigOnly(),t}}_startImport(){const t=this.host._importDialog;if(!t)return;const e=t.floors.filter(t=>t.checked).map(t=>t.name);this.host._importDialog=null,e.length?(this.host._importQueue=e,this.host._importTotal=e.length,this._openNextImport()):this._openSpaceDialog("create")}_openNextImport(){const t=this.host._importQueue.shift();void 0!==t&&(this.host._spaceDialog={mode:"create",title:t,planUrl:null,planFile:null,...Ri(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:vs,roomOpacity:ys,fillMode:"custom",customFill:null,glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:bs,tempMax:fs,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:$s(this.host._imperial),cellCmInput:ms($s(this.host._imperial),this.host._imperial),cellCmTouched:!1,busy:!1})}_skipImport(){this.host._spaceDialog=null,this.host._importQueue.length?this._openNextImport():this.host._importTotal>0&&this.host._model.length&&(this.host._importTotal=0,this.host._commitSpace(this.host._serverCfg.spaces[0]?.id||this.host._space),this._setMode("plan"),this.host._showToast(this.host._t("import.done")))}_renderImportDialog(){const t=this.host._importDialog,e=t.floors.filter(t=>t.checked).length;return o`this.host._importDialog=null}> +
+
${this.host._t("import.hint")}
+ ${t.floors.map((e,s)=>o``)} +
+
+ + + +
+
`}_preflightDiagnostics(t,e){const s=new Map((e?.spaces||[]).map(t=>[String(t?.id||""),t]));return{kind:"houseplan-optimize-preflight",origin:"runtime",cardVersion:Xr,checkedAt:(new Date).toISOString(),preflightFingerprint:t.fingerprint,failures:t.failures.map(t=>({spaceId:t.spaceId,displayName:t.displayName,reason:t.reason,detail:t.detail??null,spaceGeometryFingerprint:s.has(t.spaceId)?jt(s.get(t.spaceId)):null}))}}_reportPreflightFailure(t,e){t.ok||t.fingerprint===this.host._reportedPreflightFingerprint||(this.host._reportedPreflightFingerprint=t.fingerprint,console.warn("[houseplan] optimize preflight failed",this._preflightDiagnostics(t,e)))}_preflightVersionsDiffer(){const t=this.host._haIntegrationVersion;return"string"==typeof t&&t.length>0&&t!==Xr}async _copyPreflightDiagnostics(){const t=this.host._alignDialog?.preflight;if(!t||t.ok)return;const e=JSON.stringify(this._preflightDiagnostics(t,this.host._alignDialog?.config??null),null,2);try{await navigator.clipboard.writeText(e),this.host._preflightClipboardFallback=null,this.host._showToast(this.host._t("gs.preflight_copied"))}catch{this.host._preflightClipboardFallback=e}}_checkOptimizeGeometry(t){return ws(t,{fallbackSpaceName:t=>this.host._t("gs.align_preflight_space",{n:String(t)})})}_checkSpacePhysicalGeometry(t,e,s){return ks(t,e,{fallbackSpaceName:t=>this.host._t("gs.align_preflight_space",{n:String(t)}),captureWallGeometry:s?(t,e)=>s(e):void 0})}_optimizeReferenceContext(t){const e=this.host._haRegistry,s=this.host._fullRegistryHass,i={},o=(...t)=>{for(const e of t){const t=String(e||"").trim();if(t)return t}return""};for(const[t,s]of Object.entries(e.devices||{}))i[t]=o(s?.name_by_user,s?.name,s?.model);for(const[t,s]of Object.entries(e.entities||{}))i[`lg_${t}`]=o(this.host.hass?.states?.[t]?.attributes?.friendly_name,s?.name,s?.original_name);for(const t of this.host._devices)i[t.id]=o(t.name,i[t.id]);for(const t of this.host._serverCfg?.markers||[]){const e=String(t.binding||"").indexOf(":"),a=e>0?t.binding.slice(0,e):"",n=e>0?t.binding.slice(e+1):"",r="device"===a?s?.devices?.[n]:"entity"===a?s?.entities?.[n]:null;i[t.id]=o(t.name,"device"===a?r?.name_by_user:null,r?.name,"entity"===a?this.host.hass?.states?.[n]?.attributes?.friendly_name:null,i[t.id])}return{effectiveAreaByMarker:Object.fromEntries(this.host._devices.filter(t=>!t.virtual&&!!t.area).map(t=>[t.id,t.area])),ownerRoster:{authoritative:e.authoritative,deviceIds:Object.keys(e.devices||{}),entityIds:[...new Set([...Object.keys(e.entities||{}),...Object.keys(this.host.hass?.states||{})])],names:i},removeLiveMissingPositions:t}}_previewAlignDialog(t){if(!this.host._norm||!this.host._serverCfg)return;const e=this.host._serverCfg.spaces||[];let s;try{s=zr(this.host._serverCfg,this.host._layout||{},this._optimizeReferenceContext(t))}catch(t){return void this._showWallModelMigrationBlocked(t)}const i=s.changed?this._checkOptimizeGeometry(s.config):null;i&&this._reportPreflightFailure(i,s.config);const o=Math.ceil(10*s.report.maxShiftCm)/10,a=e.find(t=>null!=t?.id&&String(t.id)===s.report.maxSpace),n=e.length>1&&a?String(a.title||a.id):"";this.host._preflightClipboardFallback=null,this.host._alignDialog={report:s.report,config:s.config,layout:s.layout,cm:o,where:n,preflight:i,changed:s.changed,busy:!1,removeLiveMissingPositions:t}}async _runAlignToGrid(){let t=this.host._alignDialog;if(!t||t.busy||!this.host._serverCfg||!t.changed||!t.preflight?.ok)return;const e=Et(t.config);if(t.preflight.fingerprint!==e){const e=this._checkOptimizeGeometry(t.config);if(this._reportPreflightFailure(e,t.config),t={...t,preflight:e},this.host._alignDialog=t,!e.ok)return}this._clearGeometryGesture(),this.host._alignDialog={...t,busy:!0};try{this.host._saveConfigDebounced.pending()&&this.host._saveConfigDebounced.flush(),await this.host._writeChain;const e=await this.host.hass.callWS({type:"houseplan/plan/optimize",config:t.config,layout:t.layout,expected_config_rev:this.host._cfgRev,expected_layout_rev:this.host._layoutRev});this.host._serverCfg=t.config,this.host._layout=t.layout,this.host._geometryHistory.clear(),this.host._cfgRev=e?.config_rev??this.host._cfgRev+1,this.host._layoutRev=e?.layout_rev??this.host._layoutRev+1,this.host._canOptimizeUndo=!!e?.can_undo,this.host._undoKind=e?.can_undo?"optimize":null,this.host._dirtyPos.clear(),this.host._sentPos.clear(),this.host._cfgEpoch++,this.host._modelCache=null,this.host._frame=null,this.host._regSignature="",this.host._maybeRebuildDevices(),this.host._cacheSnapshot(),this.host._alignDialog=null,this.host._preflightClipboardFallback=null,this.host.requestUpdate(),this.host._showToast(this.host._t("gs.align_done",{n:String(t.report.moved),m:String(t.report.migrated+t.report.canonicalized+t.report.coordsCanonicalized+t.report.latticeCoordinatesCanonicalized+t.report.wallsMerged+t.report.spansMerged+t.report.partitionsMerged+t.report.partitionsReconciled+t.report.openingsRehosted+t.report.wallsStraightened),r:String(t.report.spaceRefsRemapped+t.report.roomRefsRemapped+t.report.positionsRemapped+t.report.markersDetached+t.report.orphanRoomLabelsRemoved+t.report.orphanDevicePositionsRemoved+t.report.orphanGroupPositionsRemoved)}))}catch(t){if(this.host._alignDialog&&(this.host._alignDialog={...this.host._alignDialog,busy:!1}),"wall_model_client_outdated"===t?.code)return void this.host._showToast(this.host._t("toast.wall_model_client_outdated"));"conflict"===t?.code&&await Promise.all([this.host._reloadConfigOnly(!0),this.host._reloadLayoutOnly()]),this.host._showToast(this.host._t("toast.error",{err:this.host._errText(t)}))}}async _undoPlanOptimization(){if(!this.host._canOptimizeUndo||this.host._optimizeUndoBusy)return;const t=this.host._undoKind;this._clearGeometryGesture(),this.host._optimizeUndoBusy=!0,this.host.requestUpdate();try{await this.host.hass.callWS({type:"houseplan/plan/optimize_undo",expected_config_rev:this.host._cfgRev,expected_layout_rev:this.host._layoutRev});const[e,s]=await Promise.all([this.host.hass.callWS({type:"houseplan/config/get"}),this.host.hass.callWS({type:"houseplan/layout/get"})]);this.host._serverCfg=e?.config||this.host._serverCfg,this.host._cfgRev=e?.rev??this.host._cfgRev,this.host._layout=s?.layout||this.host._layout,this.host._geometryHistory.clear(),this.host._layoutRev=s?.rev??this.host._layoutRev,this.host._canOptimizeUndo=!1,this.host._undoKind=null,this.host._cfgEpoch++,this.host._modelCache=null,this.host._frame=null,this.host._regSignature="",this.host._maybeRebuildDevices(),this.host._cacheSnapshot(),this.host.requestUpdate(),this.host._showToast(this.host._t("import"===t?"backup.import_undone":"gs.optimize_undone"))}catch(t){this.host._canOptimizeUndo=!1,this.host._undoKind=null,this.host._showToast(this.host._t("toast.error",{err:this.host._errText(t)}))}finally{this.host._optimizeUndoBusy=!1,this.host.requestUpdate()}}async _runBackupExport(){const t=this.host._backupExportDialog;if(t&&!t.busy){this.host._backupExportDialog={...t,busy:!0,error:""};try{const e=await this.host.hass.callWS({type:"houseplan/export/create",kind:t.kind,space_id:"space"===t.kind?this.host._space:void 0,..."space"===t.kind&&t.planOnly?{plan_only:!0}:{},card_version:Xr}),s=new Blob([JSON.stringify(e.document,null,2)+"\n"],{type:"application/json;charset=utf-8"}),i=URL.createObjectURL(s),o=document.createElement("a");o.href=i,o.download=e.filename||`houseplan-${t.kind}.json`,o.style.display="none",document.body.append(o),o.click(),o.remove(),window.setTimeout(()=>URL.revokeObjectURL(i),0),this.host._backupExportDialog=null,this.host._showToast(this.host._t("backup.export_done"))}catch(t){this.host._backupExportDialog&&(this.host._backupExportDialog={...this.host._backupExportDialog,busy:!1,error:this._backupErrorText(t)})}}}async _pickBackupImport(t){const e=t.target,s=e.files?.[0];if(e.value="",s){this.host._settingsDialog=null,this.host._backupImportDialog={filename:s.name,size:s.size,token:"",preview:null,expectedConfigRev:this.host._cfgRev,expectedLayoutRev:this.host._layoutRev,duplicatePolicy:"skip",confirmMissing:!1,busy:!0,error:""};try{this.host._saveConfigDebounced.pending()&&this.host._saveConfigDebounced.flush(),await this.host._writeChain,this.host._persistLayout.pending()&&this.host._persistLayout.flush();const t="/api/houseplan/import/preview?duplicate_policy=skip",e={method:"POST",body:s,headers:{"content-type":"application/json"}},i=this.host.hass?.fetchWithAuth?await this.host.hass.fetchWithAuth(t,e):await fetch(t,{...e,headers:{...e.headers,...this.host.hass?.auth?.data?.access_token?{authorization:`Bearer ${this.host.hass.auth.data.access_token}`}:{}}}),o=await i.json().catch(()=>({}));if(!i.ok||o.error)throw Object.assign(new Error(o.message||o.error||`HTTP ${i.status}`),{code:o.error});this.host._backupImportDialog&&(this.host._backupImportDialog={...this.host._backupImportDialog,token:o.token,preview:o.preview,expectedConfigRev:o.expected_config_rev,expectedLayoutRev:o.expected_layout_rev,busy:!1})}catch(t){this.host._backupImportDialog&&(this.host._backupImportDialog={...this.host._backupImportDialog,busy:!1,error:this._backupErrorText(t)})}}}async _setBackupDuplicatePolicy(t){const e=this.host._backupImportDialog;if(e&&!e.busy&&e.token&&t!==e.duplicatePolicy){this.host._backupImportDialog={...e,duplicatePolicy:t,busy:!0,error:""};try{const s=await this.host.hass.callWS({type:"houseplan/import/revalidate",token:e.token,duplicate_policy:t});this.host._backupImportDialog&&(this.host._backupImportDialog={...this.host._backupImportDialog,preview:{...e.preview,...s.preview},expectedConfigRev:s.expected_config_rev,expectedLayoutRev:s.expected_layout_rev,confirmMissing:!1,busy:!1})}catch(t){this.host._backupImportDialog&&(this.host._backupImportDialog={...this.host._backupImportDialog,duplicatePolicy:e.duplicatePolicy,preview:e.preview,confirmMissing:e.confirmMissing,busy:!1,error:this._backupErrorText(t)})}}}async _applyBackupImport(){const t=this.host._backupImportDialog;if(!t||t.busy||!t.token||!t.preview)return;if(t.preview.confirmation_required&&!t.confirmMissing)return;this.host._backupImportDialog={...t,busy:!0,error:""};const e=this.host._space;try{const s=await this.host.hass.callWS({type:"houseplan/import/apply",token:t.token,expected_config_rev:t.expectedConfigRev,expected_layout_rev:t.expectedLayoutRev,duplicate_policy:t.duplicatePolicy,confirm_missing_content:t.confirmMissing}),[i,o]=await Promise.all([this.host.hass.callWS({type:"houseplan/config/get"}),this.host.hass.callWS({type:"houseplan/layout/get"})]);this.host._adoptStructuralResponses(i,o),this.host._geometryHistory.clear(),this.host._dirtyPos.clear(),this.host._sentPos.clear(),this.host._defPos={},this.host._cfgEpoch++,this.host._modelCache=null,this.host._frame=null,this.host._visibleDeviceSnapshot=null,this.host._candidateDeviceSnapshot=null,this.host._stagedDeviceSnapshotToken=-1,this.host._capturedSnapshotConfigEpoch=-1,this.host._regSignature="",this.host._signer.invalidate(this.host.hass),this.host._resign(),this.host._maybeRebuildDevices();const a=this.host._serverCfg?.spaces||[],n="space"===s.kind&&s.space_id?s.space_id:a.some(t=>t.id===e)?e:a[0]?.id||this.host._space;this.host._hasFixedFloor?this.host._adoptInitialSpace(this.host._model,!0):this.host._commitSpace(n),this.host._backupImportDialog=null,this.host._cacheSnapshot(),this.host.requestUpdate();const r="space"===s.kind?t.preview.counts:s.counts;this.host._showToast(this.host._t("space"===s.kind?"backup.space_done":"backup.full_done",{spaces:String(r?.spaces||0),rooms:String(r?.rooms||0),markers:String(r?.markers||0),refs:String(s.repaired_target_refs||0)}))}catch(t){if("conflict"===t?.code&&this.host._backupImportDialog?.token)try{const t=this.host._backupImportDialog,e=await this.host.hass.callWS({type:"houseplan/import/revalidate",token:t.token,duplicate_policy:t.duplicatePolicy});return void(this.host._backupImportDialog={...t,preview:{...t.preview,...e.preview},expectedConfigRev:e.expected_config_rev,expectedLayoutRev:e.expected_layout_rev,confirmMissing:!1,busy:!1,error:this.host._t("backup.revalidated")})}catch(e){t=e}this.host._backupImportDialog&&(this.host._backupImportDialog={...this.host._backupImportDialog,busy:!1,error:this._backupErrorText(t)})}}_renderBackupExportDialog(){const t=this.host._backupExportDialog,e=(this.host._serverCfg?.spaces||[]).find(t=>t.id===this.host._space);return o`this.host._backupExportDialog=null}> +
+
${this.host._t("backup.export_hint")}
+ + + ${"space"===t.kind&&e?o``:a} +
${this.host._t("backup.privacy_warning")}
+ ${t.error?o``:a} +
+
+ + + +
+
`}_renderBackupImportDialog(){const t=this.host._backupImportDialog,e=t.preview,s=e?.counts||{},i=e?.reference_report||{},n=t=>Object.values(t||{}).reduce((t,e)=>t+(Number(e)||0),0),r=[["incoming_remapped",n(i.remapped?.incoming)],["target_repaired",n(i.remapped?.target)],["preserved_unresolved",n(i.preservedUnresolved)],["collisions",n(i.collisions)],["dropped_links",n(i.droppedIncomingLinks)],["bounded_lineages",Number(i.boundedLineages)||0]].filter(([,t])=>t>0);return o`this.host._backupImportDialog=null}> +
+
${t.filename}${(t.size/1024).toFixed(1)} KB
+ ${t.busy&&!e?o`
${this.host._t("backup.reading")}
`:a} + ${t.error?o``:a} + ${e?o` +
+ ${this.host._t("full"===e.kind?"backup.full":"backup.current_space")} + ${e.plan_only?o`${this.host._t("backup.plan_only_preview")}`:a} + ${this.host._t("same"===e.source?"backup.same_source":"backup.foreign_source")} + ${this.host._t("backup.created",{value:e.created_at||"—"})} + ${this.host._t("backup.versions",{card:e.card_version||"—",integration:e.integration_version||"—",model:String(e.model_version??"—")})} +
+
+ ${this.host._t("backup.count_spaces",{n:"full"===e.kind?`${e.current_counts?.spaces||0} → ${s.spaces||0}`:String(s.spaces||0)})} + ${this.host._t("backup.count_rooms",{n:"full"===e.kind?`${e.current_counts?.rooms||0} → ${s.rooms||0}`:String(s.rooms||0)})} + ${this.host._t("backup.count_walls",{n:"full"===e.kind?`${e.current_counts?.walls||0} → ${s.walls||0}`:String(s.walls||0)})} + ${this.host._t("backup.count_openings",{n:"full"===e.kind?`${e.current_counts?.openings||0} → ${s.openings||0}`:String(s.openings||0)})} + ${this.host._t("backup.count_decor",{n:"full"===e.kind?`${e.current_counts?.decor||0} → ${s.decor||0}`:String(s.decor||0)})} + ${this.host._t("backup.count_markers",{n:"full"===e.kind?`${e.current_counts?.markers||0} → ${s.markers||0}`:String(s.markers||0)})} + ${this.host._t("backup.count_layout",{n:"full"===e.kind?`${e.current_counts?.layout||0} → ${s.layout||0}`:String(s.layout||0)})} +
+ ${e.bindings?o`
${this.host._t("backup.bindings",{device:String(e.bindings.device||0),entity:String(e.bindings.entity||0),virtual:String(e.bindings.virtual||0),legacy:String(e.legacy_positions||0)})}
${this.host._t("backup.binding_status",{active:String(e.bindings.active||0),disabled:String(e.bindings.disabled||0),missing:String(e.bindings.missing||0)})}
`:a} + ${e.missing_areas?.length?o`
${this.host._t("backup.missing_areas",{areas:e.missing_areas.join(", ")})}
`:a} + ${e.dropped_marker_links?o`
${this.host._t("backup.dropped_marker_links",{n:String(e.dropped_marker_links)})}
`:a} + ${e.repaired_target_refs?o`
${this.host._t("backup.repaired_target_refs",{n:String(e.repaired_target_refs)})}
`:a} + ${e.preserved_unresolved_refs?o` +
${this.host._t("backup.preserved_unresolved_refs",{n:String(e.preserved_unresolved_refs)})}
${this.host._t("backup.preserved_unresolved_hint")}
+ `:a} + ${r.length?o`
+ ${this.host._t("backup.import_details")} +
+ ${r.map(([t,e])=>o`${this.host._t(`backup.import_detail.${t}`,{n:String(e)})}`)} + ${(i.examples||[]).slice(0,8).map(t=>o` + ${t.owner} → ${t.reference} + `)} +
+
`:a} + ${"full"===e.kind?o` +
${this.host._t("backup.replace_warning")}
+ ${"foreign"===e.source?o`
${this.host._t("backup.foreign_bookkeeping")}
`:a}`:o` +
${this.host._t("backup.final_name")}${e.space_title}
+
${this.host._t("backup.target_settings")}
+ ${e.duplicates?o`
${this.host._t("backup.duplicates",{n:String(e.duplicates)})} + + +
`:a}`} + ${e.content?.length?o`
+ ${this.host._t("backup.content")} + ${e.content.map(t=>o`${t.url} · ${this.host._t("available"===t.state?"backup.content_available":"external"===t.state?"backup.content_external":"backup.content_detach_required")}`)} +
`:a} + ${e.confirmation_required?o``:a} + `:a} +
+
+ + + ${e?o``:a} +
+
`}_setFillColor(t,e){const s=this.host._settingsDialog;this.host._settingsDialog={...s,colors:{...s.colors,[t]:{...s.colors[t],...e}}}}async _saveSettingsDialog(){const t=this.host._settingsDialog;if(t&&!t.busy){this.host._settingsDialog={...t,busy:!0};try{const e=this.host._serverCfg,s=JSON.stringify(t.colors)===JSON.stringify(xs),i={...e.settings};s?delete i.fill_colors:i.fill_colors=t.colors;const o=this.host._imperial?30.48*t.glowRadius:100*t.glowRadius;Number.isFinite(o)&&o>0&&300!==Math.round(o)?i.glow_radius_cm=Math.round(o):delete i.glow_radius_cm,t.bgColor?i.bg_color=t.bgColor:delete i.bg_color,null!==t.northDeg&&Number.isInteger(t.northDeg)&&t.northDeg>=0&&t.northDeg<=359?i.north_deg=t.northDeg:delete i.north_deg,i.bg_mode=t.bgMode,t.sunRays?i.sun_rays=!0:delete i.sun_rays,delete i.weather_entity,this.host._serverCfg={...e,settings:i},await this._saveConfigNow(),this.host._settingsDialog=null,this.host.requestUpdate(),this.host._showToast(this.host._t("gs.saved"))}catch(t){this.host._settingsDialog&&(this.host._settingsDialog={...this.host._settingsDialog,busy:!1}),this.host._showToast(this.host._t("toast.error",{err:this.host._errText(t)}))}}}_boolInput(t,e,s=!1){const i=t=>e(!!t.target.checked);return customElements.get("ha-switch")?o``:o``}_rangeInput(t,e,s,i,n,r=!1,l){const h=t=>{const e=Number(t.target.value);Number.isFinite(e)&&n(e)};return customElements.get("ha-slider")?o``:o``}_renderColorRow(t,e){const s=this.host._settingsDialog.colors[t];return o`
+ {this._setFillColor(t,{c:e.detail.color,a:e.detail.opacity})}}> +
`}_renderAlignDialog(){const t=this.host._alignDialog,e=t.report,s=t.changed&&!t.preflight?.ok,i=t.preflight?.failures||[],n=i.slice(0,3).map(t=>t.displayName),r=n.length?n.join(", "):this.host._t("gs.align_preflight_space",{n:"1"}),l=Math.max(0,i.length-n.length),h=l?this.host._t("gs.align_preflight_more",{n:String(l)}):"",c=e.spaceRefsRemapped+e.roomRefsRemapped+e.positionsRemapped+e.markersDetached,d=e.migrated+e.canonicalized+e.coordsCanonicalized+e.wallSegmentsMigrated+e.wallsMerged+e.spansMerged+e.partitionsMerged+e.partitionsReconciled+e.openingsRehosted+e.redundantDraftsRemoved,p=e.moved+e.rotated+e.removedDrafts+e.coordsCanonicalized+e.wallsStraightened,_=Math.ceil(10*e.maxStraightenShiftCm)/10,u=(this.host._serverCfg?.spaces||[]).find(t=>String(t?.id||"")===e.maxStraightenSpace),m=(this.host._serverCfg?.spaces||[]).length>1&&u?String(u.title||u.id):"",g=e.orphanRoomLabelsRemoved+e.orphanDevicePositionsRemoved+e.orphanGroupPositionsRemoved,f=e.liveMissingPositions.map(t=>t.name).filter(Boolean),b=f.slice(0,3).join(", "),y=Math.max(0,f.length-3),v=b?this.host._t("gs.optimize_live_names",{names:b,more:y?this.host._t("gs.optimize_reference_more",{n:String(y)}):""}):"",$=e.unverifiedPositions.some(t=>"registry_unavailable"===t.reason),w=t=>e.removedPositions.some(e=>e.id===t.id)?this.host._t("gs.optimize_detail_removed"):e.liveMissingPositions.some(e=>e.id===t.id)?this.host._t("gs.optimize_detail_live"):this.host._t("gs.optimize_detail_unverified"),k=t=>this.host._t("room_label"===t?"gs.optimize_detail_room_label":"group"===t?"gs.optimize_detail_group":"device"===t?"gs.optimize_detail_device":"gs.optimize_detail_unknown"),x=[...e.removedPositions,...e.liveMissingPositions.filter(t=>!e.removedPositions.some(e=>e.id===t.id)),...e.unverifiedPositions],S=x.slice(0,10),M=Math.max(0,x.length-S.length);return o`{this.host._alignDialog=null,this.host._preflightClipboardFallback=null}}> +
+ ${s?o` +

${this.host._t("gs.align_preflight_failed",{spaces:r,more:h})}

+ ${i.slice(0,10).map(t=>o`

+ ${t.displayName}: ${this.host._t(`gs.preflight_reason_${t.reason}`)} +

`)} + ${i.length>10?o`

+ ${this.host._t("gs.align_preflight_more",{n:String(i.length-10)})} +

`:a} +
${this.host._t("gs.align_preflight_hint")}
+ ${this._preflightVersionsDiffer()?o` +
${this.host._t("gs.preflight_update_hint")}
`:a} +
+ +
+ ${this.host._preflightClipboardFallback?o`
+ ${this.host._t("gs.preflight_copy")} +
${this.host._preflightClipboardFallback}
+
`:a}`:t.changed?o` + ${e.moved?o`

${this.host._t("gs.align_count",{n:String(e.moved),total:String(e.total),cm:String(t.cm)})}

`:a} + ${e.latticeCoordinatesCanonicalized?o` +

${this.host._t("gs.optimize_lattice_summary",{n:String(e.latticeCoordinatesCanonicalized),cm:Ss(e.latticeMaxShiftCm)})}

+ ${e.latticeSpaces.map(t=>o`

${this.host._t("gs.optimize_lattice_space",{space:t.space,n:String(t.canonicalized),far:String(t.far)})}

`)} + `:a} + ${t.where?o`

${this.host._t("gs.align_where",{s:t.where})}

`:a} + ${e.rotated?o`

${this.host._t("gs.align_turned",{n:String(e.rotated)})}

`:a} + ${e.removedDrafts?o`

${this.host._t("gs.align_removed_drafts",{n:String(e.removedDrafts)})}

`:a} + ${e.redundantDraftsRemoved?o`

${this.host._t("gs.optimize_redundant_drafts",{n:String(e.redundantDraftsRemoved)})}

`:a} + ${e.wallSegmentsMigrated?o`

${this.host._t("gs.wall_segments_migrated",{n:String(e.wallSegmentsMigrated)})}

`:a} + ${e.legacyZeroWallsMigrated?o`

${this.host._t("gs.zero_walls_migrated",{n:String(e.legacyZeroWallsMigrated)})}

`:a} + ${d?o`

${this.host._t("gs.optimize_changes",{m:String(e.migrated),c:String(e.canonicalized),p:String(e.coordsCanonicalized),w:String(e.wallsMerged),s:String(e.spansMerged),i:String(e.partitionsMerged)})}

`:a} + ${e.partitionsReconciled?o`

${this.host._t("gs.optimize_coincident_partitions",{n:String(e.partitionsReconciled)})}

`:a} + ${e.openingsRehosted?o`

${this.host._t("gs.optimize_openings_rehosted",{n:String(e.openingsRehosted)})}

`:a} + ${e.wallsStraightened?o`

${this.host._t("gs.optimize_walls_straightened",{n:String(e.wallsStraightened),cm:String(_)})}

`:a} + ${m?o`

${this.host._t("gs.optimize_walls_straightened_where",{s:m})}

`:a} + ${e.glowSpacesMigrated||e.glowRoomsMigrated?o`

${this.host._t("gs.optimize_glow_migration",{spaces:String(e.glowSpacesMigrated),rooms:String(e.glowRoomsMigrated)})}

`:a} + ${p?o`
${this.host._t("gs.align_warn")}
`:a}`:o`

${this.host._t(e.liveMissingPositions.length||e.unverifiedPositions.length||e.nestedRefsUnresolved?"gs.optimize_no_automatic_changes":"gs.align_none")}

`} + ${!s&&e.wallsStraightenSkipped?o`

${this.host._t("gs.optimize_walls_straighten_skipped",{n:String(e.wallsStraightenSkipped)})}

`:a} + ${c?o`

${this.host._t("gs.optimize_references",{spaces:String(e.spaceRefsRemapped),rooms:String(e.roomRefsRemapped),positions:String(e.positionsRemapped),detached:String(e.markersDetached)})}

`:a} + ${g?o`

${this.host._t("gs.optimize_orphans_removed",{total:String(g),rooms:String(e.orphanRoomLabelsRemoved),devices:String(e.orphanDevicePositionsRemoved),groups:String(e.orphanGroupPositionsRemoved)})}

`:a} + ${e.liveMissingPositions.length?o`
+

${this.host._t(t.removeLiveMissingPositions?"gs.optimize_live_positions_remove":"gs.optimize_live_positions",{n:String(e.liveMissingPositions.length),names:v})}

+ + ${t.removeLiveMissingPositions?o`
+ ${this.host._t("gs.optimize_live_selected")} +
`:a} +
`:a} + ${e.unverifiedPositions.length?o``:a} + ${e.nestedRefsUnresolved?o``:a} + ${x.length?o`
+ ${this.host._t("gs.optimize_details")} +
    + ${S.map(t=>o`
  • ${this.host._t("gs.optimize_detail_item",{status:w(t),kind:k(t.kind),id:t.id,space:t.spaceId})}
  • `)} +
+ ${M?o`
${this.host._t("gs.optimize_details_more",{n:String(M)})}
`:a} +
`:a} +
+
+ + + ${t.changed&&t.preflight?.ok?o` + `:a} +
+
`}_renderSettingsDialog(){return o`this.host._settingsDialog=null}> +
+
${this.host._t("gs.hint")}
+ + ${this._renderColorRow("light_on","gs.light_on")} + ${this._renderColorRow("light_off","gs.light_off")} + ${this._renderColorRow("light_none","gs.light_none")} + + ${this._renderColorRow("temp_cold","gs.temp_cold")} + ${this._renderColorRow("temp_ok","gs.temp_ok")} + ${this._renderColorRow("temp_hot","gs.temp_hot")} + + ${this._renderColorRow("lqi_low","gs.lqi_low")} + ${this._renderColorRow("lqi_high","gs.lqi_high")} + + ${this._renderColorRow("glow_base","gs.glow_base")} + ${this._renderColorRow("glow_light","gs.glow_light")} +
+ ${this.host._t("gs.glow_radius")} + {const e=sl(t.target.value);null!=e&&e>0&&(this.host._settingsDialog={...this.host._settingsDialog,glowRadius:e})}} /> + ${this.host._imperial?this.host._t("gs.unit_ft"):this.host._t("gs.unit_m")} +
+ + ${this._renderColorRow("wall_fill","gs.wall_fill")} + +
+ ${this.host._t("gs.bg_mode")} + +
+ ${"static"===this.host._settingsDialog.bgMode?o`
+ {this.host._settingsDialog={...this.host._settingsDialog,bgColor:t.detail.color}}}> + ${this.host._settingsDialog.bgColor?o``:o`${this.host._t("gs.bg_theme")}`} +
`:o`
${this.host._t("gs.bg_daynight_hint")}
`} + + ${Ms(this.host.hass)?a:o`
${this.host._t("gs.sun_missing")}
`} +
+ ${this.host._renderCompass()} +
+ ${this.host._t("gs.north")} +
+ {const e=t.target.value.trim(),s=""===e?null:Math.round(Number(e));this.host._settingsDialog={...this.host._settingsDialog,northDeg:null!==s&&Number.isFinite(s)?Math.min(359,Math.max(0,s)):null}}} /> + ${null!==this.host._settingsDialog.northDeg?o``:a} +
+ ${null===this.host._settingsDialog.northDeg?o`
${this.host._t("gs.north_hint")}
`:a} +
+
+ + ${this.host._canEdit?o` + +
${this.host._t("gs.backup_hint")}
+
+ + + + + + ${this.host._canOptimizeUndo&&"import"===this.host._undoKind?o` + `:a} +
`:a} + +
${this.host._t("gs.grid_hint")}
+
+ +
+ ${this.host._canOptimizeUndo&&"import"!==this.host._undoKind?o`
+ +
`:a} + +
${this.host._t("gs.about_version",{v:Xr})}
+ + ${this.host._t("gs.about_github")} + + ${this.host._t("gs.about_telegram")} +
+
+ + + + +
+
`}_rulesSet(t){this.host._rulesDialog={...this.host._rulesDialog,rules:t}}async _saveRules(){const t=this.host._rulesDialog;if(!t||t.busy)return;const e=t.rules.filter(t=>t.pattern.trim()&&t.icon.trim());this.host._rulesDialog={...t,busy:!0};try{const t=this.host._serverCfg,s=JSON.stringify(e)===JSON.stringify(Ct),i={...t.settings};s?delete i.icon_rules:i.icon_rules=e,this.host._serverCfg={...t,settings:i},await this._saveConfigNow(),this.host._rulesDialog=null,this.host._regSignature="",this.host._maybeRebuildDevices(),this.host._showToast(this.host._t("rules.saved"))}catch(t){this.host._rulesDialog&&(this.host._rulesDialog={...this.host._rulesDialog,busy:!1}),this.host._showToast(this.host._t("toast.error",{err:this.host._errText(t)}))}}_renderRulesDialog(){const t=this.host._rulesDialog,e=Ds(t.rules),s=t.test.trim()?Cs(t.test,"",e):null,i=(e,s)=>{const i=[...t.rules],o=e+s;o<0||o>=i.length||([i[e],i[o]]=[i[o],i[e]],this._rulesSet(i))};return o`this.host._rulesDialog=null}> +
+
${this.host._t("rules.hint")}
+
+ this.host._rulesDialog={...t,test:e.target.value}} /> + ${s?o`${s}`:a} +
+ ${t.rules.map((e,s)=>{const a=""!==e.pattern.trim()&&!Ps(e.pattern);return o`
+ {const o=[...t.rules];o[s]={...e,pattern:i.target.value},this._rulesSet(o)}} /> + {const o=[...t.rules];o[s]={...e,icon:i.target.value},this._rulesSet(o)}} /> + + i(s,-1)}> + i(s,1)}> + this._rulesSet(t.rules.filter((t,e)=>e!==s))}> +
`})} + +
+
+ + + + +
+
`}_saveKioskScale(t){this.host._kioskScale={...this.host._kioskScale,...t};try{localStorage.setItem("houseplan_card_kiosk_v1",JSON.stringify(this.host._kioskScale))}catch{}this.host.requestUpdate()}_renderKioskDialog(){const t=this.host._kioskScale,e=(e,s)=>o` +
+ ${this._rangeInput(50,300,5,Math.round(100*t[e]),t=>this._saveKioskScale({[e]:t/100}))} + ${Math.round(100*t[e])}% +
`;return o`this.host._kioskDialog=!1}> +
+
${this.host._t("kiosk.hint")}
+ ${e("icon",this.host._t("kiosk.icon_scale"))} + ${e("font",this.host._t("kiosk.font_scale"))} +
+
+ + + +
+
`}_renderVacSection(t){const e=this.host._devices.find(e=>e.id===t.devId);if(!e||!this.host._isVacDev(e))return a;const s=e.marker?.vacuum||{},i=this.host._vacAllCamerasFor===e.id,n=this.host._vacSourceResolution(e,i),r=n.entityId,l=n.candidates.find(t=>t.entityId===r)||null,h=r?Is(this.host.hass?.states[r]?.attributes):null,c=!1!==s.live&&("ok"===n.status||"unsupported"===n.status),d=this._vacPlanRoomAnchors(e.space),p=h?Ts(h.rooms,d.map(t=>t.name)):0,_=!!(c&&h&&p>=3),u=Object.keys(s.calibration||{}),m=t=>{const s=this.host._vacEnsureMarker(e);s&&(s.vacuum={...s.vacuum||{},...t},this.host._regSignature="",this.host._maybeRebuildDevices(),this._saveConfig(),this.host.requestUpdate())},g=new Set(e.entities||[]),f=n.candidates.filter(t=>g.has(t.entityId)||t.entityId===s.source),b=i?n.candidates.filter(t=>t.entityId.startsWith("camera.")&&!f.some(e=>e.entityId===t.entityId)):[],y=t=>[t.hasPosition?this.host._t("vac.cap_position"):"",t.hasRooms?this.host._t("vac.cap_rooms_short"):"",t.hasPath?this.host._t("vac.cap_path"):"",t.hasMapId?this.host._t("vac.cap_map"):""].filter(Boolean).join(" · ")||this.host._t("vac.cap_none"),v=t=>o` + `,$=n.candidates.some(t=>g.has(t.entityId)&&"known_xcme_incomplete"===t.category)||!!n.pinned&&!!r?.startsWith("camera.")&&!l?.hasPosition;return o` + +
+
+
${this.host._t("vac.diag_source")}${r||this.host._t("vac.source_none")}
+ ${l?.platform?o`
${this.host._t("vac.diag_platform")}${l.platform}
`:a} +
${this.host._t("vac.diag_status")}${this.host._t(`vac.source_status_${n.status}`)}
+
${this.host._t("vac.diag_position")}${h?.pos?this.host._t("common.yes"):this.host._t("common.no")}
+
${this.host._t("vac.diag_rooms")}${Rs(this.host._t("vac.diag_rooms_value"),{total:String(h?.rooms.length||0),matched:String(p),readiness:this.host._t(p>=3?"vac.autocal_ready":"vac.autocal_not_ready")})}
+
${this.host._t("vac.diag_path")}${h?.path.length?this.host._t("common.yes"):this.host._t("common.no")}
+
${this.host._t("vac.diag_map")}${h?.mapId??"default"}
+
+ ${"missing"===n.status||"disabled"===n.status||"unverified"===n.status?o`
+ ${this.host._t(`vac.source_banner_${n.status}`)} + +
`:a} + ${$?o`
+ ${this.host._t("vac.xcme_hint")} +
attributes:
+  - vacuum_position
+  - rooms
+  - path
+  - map_name
+
`:a} +
+ ${this.host._t("vac.choose_source")} +
+ + ${f.map(v)} +
{t.currentTarget.open?this.host._vacOpenAllCameras(e):(this.host._vacAllCamerasFor=null,this.host._vacAllCameraCache=null)}}> + ${this.host._t("vac.all_cameras")} +
${this.host._t("vac.all_cameras_warn")}
+ ${i?b.length?b.map(v):o`
${this.host._t("vac.all_cameras_empty")}
`:a} +
+
+
+
+ ${_?o``:a} + ${c?o``:a} + ${this.host._t("vac.documentation")} +
+ ${h?o` + + + + ${u.length?o`
${Rs(this.host._t("vac.cal_maps"),{maps:u.join(", ")})}
`:a} + `:a} +
`}_vacMapId(t,e,s=this.host._planHass){const i=this.host._vacEntity(t),o=i?s?.states?.[i]?.attributes?.selected_map:null;return Ns(e.mapId,o)}_vacSaveMatrix(t,e,s,i){const o=this.host._devices.find(e=>e.id===t),a=o?this.host._vacEnsureMarker(o):this.host._serverCfg?.markers?.find(e=>e.id===t);if(!a)return!1;const n={...a.vacuum||{}};return n.source=e,n.calibration={...n.calibration||{},[s]:i.map(t=>Number(t.toFixed(6)))},a.vacuum=n,this.host._regSignature="",this.host._maybeRebuildDevices(),this._saveConfig(),this.host.requestUpdate(),!0}_vacPlanRoomAnchors(t){return(this.host._spaceModelById(t)?.rooms||[]).map(t=>({room:t,poly:R(t)})).filter(({room:t,poly:e})=>t.name&&e).map(({room:t,poly:e})=>{const s=zs(e);return s?{name:String(t.name),cx:s[0],cy:s[1]}:null}).filter(Boolean)}_vacAutoCalibrate(t){const e=this.host._vacSource(t),s=e?Is(this.host.hass?.states[e]?.attributes):null;if(!e||!s||s.rooms.length<3)return void this.host._showToast(this.host._t("vac.autocal_no_rooms"));const i=this._vacPlanRoomAnchors(t.space),o=As(s.rooms,i);if(!o)return void this.host._showToast(this.host._t("vac.autocal_no_match"));const a=this.host._serverCfg?.spaces?.find(e=>e.id===t.space),n=Number(a?.cell_cm),r=Number.isFinite(n)&&n>0?n:5,l=Os(o.residual,this.host._gridPitch,r),h=this._vacMapId(t,s);l>Ls?this.host._vacCalConfirm={markerId:t.id,source:e,mapId:h,matrix:o.matrix,rooms:o.matched.length,error:ee(l,"mi"===this.host.hass?.config?.unit_system?.length)}:this._vacSaveMatrix(t.id,e,h,o.matrix)&&this.host._showToast(Rs(this.host._t("vac.autocal_done"),{rooms:String(o.matched.length)}))}_vacApplyCalibrationProposal(t){const e=this.host._vacCalConfirm;if(e){if(this.host._vacCalConfirm=null,t){const t=this.host._devices.find(t=>t.id===e.markerId),s=Es(e.matrix);if(!t||!s)return;if(this.host._markerDialog=null,t.space!==this.host._space&&!this.host._commitSpace(t.space))return;return void(this.host._vacFit={markerId:e.markerId,source:e.source,mapId:e.mapId,p:s,drag:null})}this._vacSaveMatrix(e.markerId,e.source,e.mapId,e.matrix)&&this.host._showToast(Rs(this.host._t("vac.autocal_done"),{rooms:String(e.rooms)}))}}_vacStartFit(t){const e=this.host._vacSource(t),s=e?Is(this.host.hass?.states[e]?.attributes):null;if(!e||!s)return void this.host._showToast(this.host._t("vac.cal_need_pos"));const i=this._vacMapId(t,s),o=t.marker?.vacuum?.calibration?.[i],a=this.host._spaceModelById(t.space);if(!a)return;const n=a.vb,r=o&&6===o.length&&Es(o)||Bs(s.rooms,n);this.host._markerDialog=null,(t.space===this.host._space||this.host._commitSpace(t.space))&&(this.host._vacFit={markerId:t.id,source:e,mapId:i,p:r,drag:null})}_vacFitSave(){const t=this.host._vacFit;if(!t)return;const e=this._vacSaveMatrix(t.markerId,t.source,t.mapId,Hs(t.p));this.host._vacFit=null,e&&this.host._showToast(this.host._t("vac.cal_done"))}_vacFitTurn(t){const e=this.host._vacFit;if(!e)return;const s=Is(this.host.hass?.states[e.source]?.attributes),i=this._vacGhostCentre(s?.rooms||[]),o={...e.p,...t};this.host._vacFit={...e,p:Gs(o,e.p,i[0],i[1])}}_vacGhostCentre(t){const e=[],s=[];for(const i of t)e.push(i.x0??i.cx,i.x1??i.cx),s.push(i.y0??i.cy,i.y1??i.cy);return e.length?[(Math.min(...e)+Math.max(...e))/2,(Math.min(...s)+Math.max(...s))/2]:[0,0]}_vacDelta(t,e,s){const i=this.host._stageEl,o=i?.clientWidth||1,a=i?.clientHeight||1;return[e/o*t.w,s/a*t.h]}_vacFitPointer(t,e){const s=this.host._vacFit;if(!s)return;if(t.stopPropagation(),"pointerdown"===t.type){const e=t.target,i=e.getAttribute?.("data-corner");try{t.currentTarget.setPointerCapture?.(t.pointerId)}catch{}return void(this.host._vacFit={...s,drag:i?{kind:"scale",sx:t.clientX,sy:t.clientY,p0:{...s.p},fx:Number(i.split(",")[0]),fy:Number(i.split(",")[1])}:{kind:"move",sx:t.clientX,sy:t.clientY,p0:{...s.p},fx:0,fy:0}})}const i=s.drag;if(i){if("pointermove"===t.type){const[o,a]=this._vacDelta(e,t.clientX-i.sx,t.clientY-i.sy);if("move"===i.kind)this.host._vacFit={...s,p:{...i.p0,ox:i.p0.ox+o,oy:i.p0.oy+a}};else{const t=Is(this.host.hass?.states[s.source]?.attributes),e=this._vacGhostCentre(t?.rooms||[]),n=Hs(i.p0),[r,l]=Ws(n,e[0],e[1]),[h,c]=Ws(n,i.fx,i.fy),d=Math.hypot(r-h,l-c)||1,[p,_]=[2*r-h,2*l-c],u=Math.hypot(p+2*o-h,_+2*a-c)/2,m=Math.max(.05,u/d),g={...i.p0,s:i.p0.s*m};this.host._vacFit={...s,p:Gs(g,i.p0,i.fx,i.fy)}}return}"pointerup"!==t.type&&"pointercancel"!==t.type||(this.host._vacFit={...s,drag:null})}}_renderVacFit(t){const e=this.host._vacFit;if(!e)return a;const s=Is(this.host._renderPlanHass?.states?.[e.source]?.attributes);if(!s)return a;const i=Hs(e.p),n=[],r=[],l=[];for(const t of s.rooms){if(null==t.x0)continue;const e=[[t.x0,t.y0],[t.x1,t.y0],[t.x1,t.y1],[t.x0,t.y1]].map(([t,e])=>Ws(i,t,e));e.forEach(([t,e])=>{r.push(t),l.push(e)});const[s,o]=Ws(i,t.cx,t.cy);n.push(ce` + ${t.name}`)}let h=a;if(s.pos){const[e,o]=Ws(i,s.pos.x,s.pos.y);h=ce``}const c=[];if(r.length){const e=(()=>{const t=i[0]*i[4]-i[1]*i[3];return(e,s)=>[(i[4]*(e-i[2])-i[1]*(s-i[5]))/t,(-i[3]*(e-i[2])+i[0]*(s-i[5]))/t]})(),s=Math.min(...r),o=Math.max(...r),a=Math.min(...l),n=Math.max(...l),h=.022*t.w,d=h/4;for(const[t,i,r,l]of[[s,a,o,n],[o,a,s,n],[o,n,s,a],[s,n,o,a]]){const s=e(r,l);c.push(ce` + `)}}return o`this._vacFitPointer(e,t)} + @pointermove=${e=>this._vacFitPointer(e,t)} + @pointerup=${e=>this._vacFitPointer(e,t)} + @pointercancel=${e=>this._vacFitPointer(e,t)}>${n}${h}${c}`}_resetRoomDialogFields(){this.host._roomEditId=null,this.host._roomFill="",this.host._roomCustomFill=null,this.host._roomTempSrc="",this.host._roomHumSrc="",this.host._roomSrcOpen=null,this.host._roomSrcFilter="",this.host._roomNameScale=1,this.host._roomLabelScale=1}_openRoomEdit(t){if(!t.id)return;this.host._roomEditId=t.id,this.host._nameSel=t.name||"",this.host._areaSel=t.area||"",this.host._roomFill="glow"===t.settings?.fill_mode?"":t.settings?.fill_mode||"";const e=t.settings?.custom_fill;this.host._roomCustomFill=e&&"object"==typeof e?_s(e,ps(this.host._curSpaceCfg).customFill):null,this.host._roomTempSrc=t.settings?.temp_source||"",this.host._roomHumSrc=t.settings?.hum_source||"",this.host._roomNameScale=js(t.settings?.name_scale),this.host._roomLabelScale=js(t.settings?.label_scale),this.host._roomSrcOpen=null,this.host._roomSrcFilter="",this.host._roomDialog=!0}_roomSettingsFromDialog(){const t={};return this.host._roomFill&&(t.fill_mode=this.host._roomFill),this.host._roomCustomFill&&(t.custom_fill=this.host._roomCustomFill),this.host._roomTempSrc&&(t.temp_source=this.host._roomTempSrc),this.host._roomHumSrc&&(t.hum_source=this.host._roomHumSrc),1!==this.host._roomNameScale&&(t.name_scale=this.host._roomNameScale),1!==this.host._roomLabelScale&&(t.label_scale=this.host._roomLabelScale),Object.keys(t).length?t:null}_saveRoomEdit(){const t=this.host._curSpaceCfg,e=t?.rooms.find(t=>t.id===this.host._roomEditId);if(!e)return this.host._roomDialog=!1,void(this.host._roomEditId=null);e.name=this.host._nameSel.trim()||e.name,e.area=this.host._areaSel||null;const s=e.settings||{},i={...s};this.host._roomFill?i.fill_mode=this.host._roomFill:delete i.fill_mode,this.host._roomCustomFill?i.custom_fill=this.host._roomCustomFill:delete i.custom_fill,"glow"===s.fill_mode&&"boolean"!=typeof s.glow&&(i.glow=!0),this.host._roomTempSrc?i.temp_source=this.host._roomTempSrc:delete i.temp_source,this.host._roomHumSrc?i.hum_source=this.host._roomHumSrc:delete i.hum_source,1!==this.host._roomNameScale?i.name_scale=this.host._roomNameScale:delete i.name_scale,1!==this.host._roomLabelScale?i.label_scale=this.host._roomLabelScale:delete i.label_scale,Object.keys(i).length?e.settings=i:delete e.settings,this._saveConfig(),this.host._roomDialog=!1,this.host._roomEditId=null,this.host._nameSel="",this.host._areaSel="",this.host._regSignature="",this.host._maybeRebuildDevices(),this.host.requestUpdate(),this.host._showToast(this.host._t("toast.room_updated"))}_roomSrcCandidates(){const t=this.host._planHass,e=x(this.host._markers),s=this.host._roomSrcFilter.trim().toLowerCase(),i=[];for(const o of Object.values(t.devices)){if("service"===o.entry_type)continue;if(e.devices.has(o.id))continue;const t=(o.name_by_user||o.name||o.id).trim();s&&!t.toLowerCase().includes(s)||i.push({value:"device:"+o.id,label:t,sub:o.model||this.host._t("marker.sub_device")})}for(const[o,a]of Object.entries(t.entities)){if(!o.startsWith("sensor.")||a.hidden)continue;if(S(t,o,e))continue;const n=a.name||t.states[o]?.attributes?.friendly_name||o;s&&!(n+" "+o).toLowerCase().includes(s)||i.push({value:"entity:"+o,label:n,sub:o})}return i.sort((t,e)=>t.label.localeCompare(e.label)),i.slice(0,200)}_roomSrcLabel(t){const e=t.indexOf(":"),s=t.slice(0,e),i=t.slice(e+1);return"device"===s?this.host._fullRegistryHass.devices[i]?.name_by_user||this.host._fullRegistryHass.devices[i]?.name||i:this.host._fullRegistryHass.entities[i]?.name||this.host.hass.states[i]?.attributes?.friendly_name||i}_labelPos(t,e){const s=this.host._layout["rl_"+(t.id||"")];if(s&&s.s===e)return{x:s.x*ol,y:s.y*ol};const i=this._snap(this.host._roomCenter(t));return{x:i[0],y:i[1]}}_labelDown(t,e,s){if("plan"!==this.host._mode)return;t.preventDefault(),t.stopPropagation();const i=this._labelPos(e,s);this.host._drag={id:"rl_"+(e.id||""),sx:t.clientX,sy:t.clientY,ox:i.x,oy:i.y,moved:!1},nl(t),this.host._tip=null}_labelMove(t,e,s){const i="rl_"+(e.id||"");if(!this.host._drag||this.host._drag.id!==i)return;const o=this.host._stageEl;if(!o)return;const a=this.host._spaceModelById(s);if(!a)return;const n=a.vb,r=o.getBoundingClientRect(),l=this.host._viewOr(n),h=(t.clientX-this.host._drag.sx)/r.width*l.w,c=(t.clientY-this.host._drag.sy)/r.height*l.h;Math.abs(t.clientX-this.host._drag.sx)+Math.abs(t.clientY-this.host._drag.sy)>3&&(this.host._drag.moved=!0);const d=Rt(this.host._drag.ox+h),p=Rt(this.host._drag.oy+c);this.host._savePos({id:i,space:s},d,p)}_labelUp(t){const e="rl_"+(t.id||"");if(!this.host._drag||this.host._drag.id!==e)return;const s=this.host._drag.moved;this.host._drag=s?this.host._drag:null,s&&window.setTimeout(()=>this.host._drag=null,0)}_labelScale(t){const e=this.host._layout["rl_"+(t.id||"")]?.k;return"number"==typeof e&&Number.isFinite(e)?Math.min(3,Math.max(.5,e)):1}_rlResizeDown(t,e,s){if("plan"!==this.host._mode)return;t.preventDefault(),t.stopPropagation();const i=t.target.closest(".roomlabel");if(!i)return;const o=i.getBoundingClientRect(),a=o.left+o.width/2,n=o.top+o.height/2,r=Math.max(8,Math.hypot(t.clientX-a,t.clientY-n));this.host._rlResize={id:"rl_"+(e.id||""),space:s,k0:this._labelScale(e),cx:a,cy:n,d0:r},nl(t)}_rlResizeMove(t){const e=this.host._rlResize;if(!e)return;t.stopPropagation();const s=Math.max(8,Math.hypot(t.clientX-e.cx,t.clientY-e.cy)),i=Math.min(3,Math.max(.5,e.k0*(s/e.d0))),o=this.host._layout[e.id];if(o)this.host._layout={...this.host._layout,[e.id]:{...o,k:i}};else{const t=e.id.slice(3),s=this.host._spaceModelById(e.space);if(!s)return;const o=s.rooms.find(e=>e.id===t);if(!o)return;const a=this._labelPos(o,e.space);this.host._layout={...this.host._layout,[e.id]:{s:e.space,x:a.x/ol,y:a.y/ol,k:i}}}this.host._dirtyPos.add(e.id)}_rlResizeUp(){this.host._rlResize&&(this.host._rlResize=null,this.host._persistLayout())}_renderRoomGear(t,e,s){if(!t.id)return a;let i=null;if(t.poly?(i=this.host._gearPtCache.get(t.poly)||null,i||(i=re(t.poly),this.host._gearPtCache.set(t.poly,i))):null!=t.x&&null!=t.y&&(i=[t.x+(t.w||0)/2,t.y+(t.h||0)/2]),!i)return a;const n=(i[0]-s.x)/s.w*100,r=(i[1]-s.y)/s.h*100;return o``}_alignCandidates(){const t=[],e=this.host._spaceModel();if(this.host._markup){if(!e)return t;if(this.host._drag?.id.startsWith("rl_")){const s=this.host._drag.id.slice(3);for(const i of e.rooms){if(!i.name||i.id===s)continue;const e=this._labelPos(i,this.host._space);t.push([e.x,e.y])}return t}for(const s of e.rooms){const e=R(s);if(e)for(const s of e)t.push(s)}if("draw"===this.host._tool)for(const e of this.host._path)t.push(e);if("split"===this.host._tool&&this.host._splitSel?.pts)for(const e of this.host._splitSel.pts)t.push(e);return t}if("devices"===this.host._mode){for(const e of this.host._devices){if(e.space!==this.host._space||e.id===this.host._drag?.id||"ha_disabled"===e.bindingStatus?.kind)continue;const s=this.host._pos(e);t.push([s.x,s.y])}return t}if("decor"===this.host._mode){const e=this.host._decorMove?.id;return t.push(...this._decorSnapGeometry(e).points),this.host._decorDraft&&t.push(this.host._decorDraft.a),t}return t}_renderAlignGuides(){const t=this.host._alignPoint;if(!t)return ce``;const e=this.host._drag?.id.startsWith("rl_")?.5*this.host._gridPitch:.05*this.host._gridPitch,s=qs(t,this._alignCandidates(),e);if(!s.length)return ce``;const i=this.host._gridPitch,o=1.5*i;return ce` + ${s.map(e=>{const[s,a,n,r]="x"===e.axis?[e.at,e.from[1],e.at,t[1]+Math.sign(t[1]-e.from[1])*o]:[e.from[0],e.at,t[0]+Math.sign(t[0]-e.from[0])*o,e.at];return ce` + `})} + `}_renderOpeningCenterTick(t){const e=(t.angle+90)*Math.PI/180,s=ze(15,this.host._cellCm);return ce``}_renderOpeningDimensionGuides(t){const e=t.labels.flatMap(t=>t.dimension?[t.dimension]:[]);if(!e.length)return ce``;const s=this._cssPxToRender(4);return ce``}_renderOpeningPlacementPreview(){const t=this.host._openingPreview;if(!t)return ce``;const e={type:t.type,length:t.renderedLength,angle:t.angle,amount:Us(t.type,null),flipH:t.flipH,flipV:t.flipV,base:"var(--hp-open)",tone:"var(--hp-open)",cellCm:this.host._cellCm,gridPitch:this.host._gridPitch,face:t.face},s="passage"===t.type?Ks(t,ze(this.host._gridPitch,this.host._cellCm)):null;return ce` + `}_renderOpeningDialog(){const t=this.host._openingDialog,e=this.host._spaceModel()?.partitions||[],s=t.id?this.host._curSpaceCfg?.openings?.find(e=>e.id===t.id):null,i={id:t.id||"preview",type:t.type,x:t.x/ol,y:t.y/this.host._spaceH,angle:t.angle,length:s&&!t.lengthTouched?s.length:this.host._cmToUnits(Math.max(20,t.lengthCm))/ol,...t.host?{host:t.host}:{}},n=qe(s,i),r=t.host?n?H(i,e,ol,this.host._cellCm,this.host._gridPitch):F(i,e,ol,this.host._cellCm,this.host._gridPitch):null,l="does-not-fit-jamb"===r?.reason,h=t.host?e.find(e=>e.id===t.host.id):null,c=h?ee(h.cm/2,this.host._imperial):"",d=!!t.host&&!r?.resolved&&!l,p="gate"===t.type?"mdi:gate":"window"===t.type?"mdi:window-closed-variant":"passage"===t.type?"mdi:arch":"mdi:door",_=(e,s)=>{const i="contact"===e?t.contact:t.lock,n="contact"===e?!!t.contactOpen:!!t.lockOpen,r="contact"===e?t.contactFilter||"":t.lockFilter||"",l=s.find(t=>t.value===i),h=l?.label||this.host.hass.states[i]?.attributes?.friendly_name||this.host._fullRegistryHass.entities[i]?.name||i,c=Ii(s,r);return o` + + ${n?o`
+ this._filterOpeningEntities(e,t.target.value)} /> +
+ + ${c.map(t=>o` + `)} + ${c.length?a:o`
${this.host._t("marker.nothing_found")}
`} +
+
`:a}`};return o`this.host._openingDialog=null}> +
+ ${t.host?o` +
+ ${d||l?o``:a} + ${d?this.host._t("opening.partition_orphan"):l?this.host._t("opening.partition_jamb_margin",{distance:c}):this.host._t("opening.host_partition")} +
`:a} + + + + + + + + {const s=sl(e.target.value);null!=s&&(this.host._openingDialog={...t,lengthCm:s,lengthTouched:!0})}} /> + + ${"passage"===t.type&&(t.contact||t.lock)?o`
+ + ${this.host._t("opening.passage_binding_warning")} +
`:a} + + ${"passage"!==t.type?o` + ${_("contact",this._contactCandidates())} + ${t.contact?o``:a}`:a} + + ${"door"===t.type||"gate"===t.type?o` + ${_("lock",this._lockCandidates())}`:a} + + ${"gate"!==t.type&&"passage"!==t.type?o``:a} + ${"passage"!==t.type?o``:a} +
+ +
`}_gridLevels(){const t=this.host._stageEl,e=this.host._viewOr(this.host._baseVb()),s=t&&t.clientWidth&&e.w?t.clientWidth/e.w:1;return Ys(this.host._gridPitch,s)}_renderMarkupDefs(t){const e=this._gridLevels();if(!e)return ce``;const s=this.host._gridPitch*e.fine,i=this.host._gridPitch*e.coarse,o=this.host._gridPitch*e.fine*.14;return ce` + + + + + + + + + + + + + + `}_renderPhysicalEditorLayer(){const t=this.host._spaceModel();if(!t)return ce``;const e=this.host._gridPitch,s=this.host._viewOr(this.host._baseVb()),i=this.host._stageEl,o=i?.clientWidth?s.w/i.clientWidth:e/8,n=Math.max(.22*e,8*o),r=Math.max(n,12*o),l=t=>`M ${t.map(t=>`${t[0]} ${t[1]}`).join(" L ")} Z`,h=(t,e)=>this.host._physicalSel?.kind===t&&this.host._physicalSel.id===e||"column"===t&&this.host._duplicateColumnId===e,c=(t.room_drafts||[]).flatMap(t=>t.points.slice(0,-1).map((e,s)=>{const i=t.points[s+1],a=Number(t.segments[s]?.cm),n=j(Number.isFinite(a)?a:15,this.host._cellCm,this.host._gridPitch);return ce`{this.host._physicalSel={kind:"draft",id:t.id,segment:s},this._registerPhysicalTap("draft",t.id,s)}}>`})),d=(t.partitions||[]).map(t=>{const e=Yt(t.a,t.b,t.cm,this.host._cellCm,this.host._gridPitch);return e?ce`this._physicalDown(e,"partition",t.id)} + @pointermove=${t=>this._physicalMove(t)} + @pointerup=${t=>this._physicalUp(t)}>`:ce`this._physicalDown(e,"partition",t.id)} + @pointermove=${t=>this._physicalMove(t)} + @pointerup=${t=>this._physicalUp(t)}>`}),p=(t.wall_columns||[]).map(t=>{const e=this.host._physicalRotate?.id===t.id?{...t,angle:this.host._physicalRotate.angle}:t,s=Jt(e,this.host._cellCm,this.host._gridPitch);return ce`this._physicalDown(e,"column",t.id)} + @pointermove=${t=>this._physicalMove(t)} + @pointerup=${t=>this._physicalUp(t)}>`}),_=this.host._physicalDrag,u=(()=>{if(!_?.moved)return a;if("partition"===_.kind&&0===Number(_.base.cm)){const t=_.base;return ce``}const t="partition"===_.kind?Yt(_.base.a,_.base.b,_.base.cm,this.host._cellCm,this.host._gridPitch):Jt(_.base,this.host._cellCm,this.host._gridPitch);return t?ce``:a})(),m=(()=>{const s=this.host._physicalSel;if(!s)return a;if("draft"===s.kind){const e=t.room_drafts.find(t=>t.id===s.id);return e?ce` + t.join(",")).join(" ")}> + ${e.points.map(t=>ce``)} + `:a}if("partition"===s.kind){const e=t.partitions.find(t=>t.id===s.id);if(!e)return a;const i=Yt(e.a,e.b,e.cm,this.host._cellCm,this.host._gridPitch),o=(e.a[0]+e.b[0])/2,r=(e.a[1]+e.b[1])/2;return ce` + ${i?ce``:ce``} + + + + `}const i=t.wall_columns.find(t=>t.id===s.id);if(!i)return a;const h=this.host._physicalRotate?.id===i.id?{...i,angle:this.host._physicalRotate.angle}:i,c=Jt(h,this.host._cellCm,this.host._gridPitch);if("square"!==h.shape)return ce` + + + `;const d=j(h.cm,this.host._cellCm,this.host._gridPitch),p=At(h.angle)*Math.PI/180,_=Math.sin(p),u=-Math.cos(p),m=[h.center[0]+_*d/2,h.center[1]+u*d/2],g=[m[0]+_*Math.max(e,24*o),m[1]+u*Math.max(e,24*o)];return ce` + + + + this._physicalRotateDown(t,i)} + @pointermove=${t=>this._physicalRotateMove(t)} + @pointerup=${t=>this._physicalRotateUp(t)}> + `})();return ce`${c}${d}${p}${u}${m}`}_renderHiddenWallDiagnosticOverlay(){if(!this.host._markup)return ce``;const t=this._hiddenWallDiagnosticSnapshot().value;if(!t.segments.length)return ce``;const e=j(5,this.host._cellCm,this.host._gridPitch);return ce``}_renderPlanSnapOverlay(){if(!this.host._markup)return ce``;const t=this._planSnapGeometrySnapshot().value,e=this.host._activePlanSnapCandidate,s=new Set(this.host._activePlanSnapConflicts.map(t=>t.key)),i=j(5,this.host._cellCm,this.host._gridPitch),o=j(10,this.host._cellCm,this.host._gridPitch);return ce``}_syncPlanSnapActiveMarker(t){const e=this.host.renderRoot?.querySelector('[data-hp="plan-snap-active-marker"]');if(e){if(e.setAttribute("class",`plan-snap-node${t?" active":""}${"line"===t?.kind?" dynamic":""}`),e.setAttribute("data-active",t?"true":"false"),e.setAttribute("visibility",t?"visible":"hidden"),!t)return e.removeAttribute("data-kind"),void e.removeAttribute("data-key");e.setAttribute("data-kind",t.kind),e.setAttribute("data-key",t.key),e.setAttribute("cx",String(t.point[0])),e.setAttribute("cy",String(t.point[1])),e.setAttribute("r",String(j(10,this.host._cellCm,this.host._gridPitch)))}}_syncPlanSnapConflictMarkers(t){const e=new Set(t.map(t=>t.key));for(const t of this.host.renderRoot?.querySelectorAll('.plan-snap-node[data-kind="endpoint"]')||[])t.setAttribute("class","plan-snap-node"+(e.has(t.dataset.key||"")?" conflict":""))}_planSnapPhysicalSegment(t){let e=0;const s=this.host._spaceModel();if(!s)return null;if("partition"===t.sourceKind)e=Number(s.partitions.find(e=>e.id===t.sourceId)?.cm)||0;else if("draft"===t.sourceKind){const i=/^(.*):(\d+)$/.exec(t.sourceId),o=i?s.room_drafts.find(t=>t.id===i[1]):null;e=o&&i&&Number(o.segments[Number(i[2])]?.cm)||0}else e=Xs(s.rooms,this.host._spaceWalls,this.host._openCuts(),[t.a[0],t.a[1],t.b[0],t.b[1]],this.host._wallKeyPitch,this.host._cellCm,this.host._gridPitch,ol);return e>0?{a:[...t.a],b:[...t.b],halfDepth:j(e,this.host._cellCm,this.host._gridPitch)/2}:null}_drawPreviewJoinPatchD(t,e){if(t.length<2)return"";const s=[];for(let i=0;i+10&&s.push({a:t[i],b:t[i+1],halfDepth:e[i]});if(!s.length)return"";const i=2e-4*this.host._gridPitch,o=this._planSnapGeometrySnapshot().value.segments.filter(e=>t.some(t=>C(t,[e.a[0],e.a[1],e.b[0],e.b[1]])<=i)).map(t=>this._planSnapPhysicalSegment(t)).filter(t=>!!t);return Zs([...s,...o],i).map(t=>`M ${t.map(t=>`${t[0]} ${t[1]}`).join(" L ")} Z`).join(" ")}_renderMarkupLayer(t){const e=this.host._openCuts(),s=this.host._thickWallCuts(),i=e.concat(s),o=i.length?D(this.host._segments,i,.02*this.host._gridPitch):this.host._segments,n=this.host._path,r=this.host._gridPitch,l=this.host._viewOr(this.host._baseVb()),h="draw"===this.host._tool?this.host._drawWallCm:null,c=(()=>"draw"===this.host._tool&&n.length&&null!=h?this.host._contourClosed?n:this.host._cursorPt?[...n,this.host._cursorPt]:n.length>=2?n:null:null)(),d=c?Ra(c.length-1,this.host._contourClosed?[...this.host._draftSegmentCms,this.host._closingWallCm??void 0]:this.host._draftSegmentCms,h,A).map(t=>j(t,this.host._cellCm,this.host._gridPitch)/2):[],p=c?Qs(c,j(h,this.host._cellCm,this.host._gridPitch)/2,this.host._contourClosed,d):"",_=c?this._drawPreviewJoinPatchD(c,d):"",u=this.host._wallFaceBatch?.candidates[this.host._wallFaceBatch.index]?.repair||this.host._wallRepairDiagnostic;return ce` + ${this._gridLevels()?ce``:a} + ${o.map(t=>ce``)} + ${this._renderPhysicalEditorLayer()} + ${"column"===this.host._tool&&this.host._cursorPt&&this.host._drawWallCm?ce`{const t={shape:"square",center:this.host._cursorPt,cm:this.host._drawWallCm};return`M ${Jt(t,this.host._cellCm,this.host._gridPitch).map(t=>`${t[0]} ${t[1]}`).join(" L ")} Z`})()}>`:a} + ${p?ce` + `:a} + ${c&&0===h?ce`t.join(",")).join(" ")}>`:a} + ${_?ce` + `:a} + ${u?ce``:a} + ${"split"===this.host._tool&&this.host._splitSel?.pts?.length?ce`${this.host._splitSel.pts.length>1?ce``:a} + ${this.host._splitSel.pts.map((t,e)=>ce``)} + ${this.host._cursorPt?ce``:a}`:a} + `}_renderActiveChainInk(){const t=this.host._path,e=this.host._gridPitch;return ce` + ${t.length>1?ce``:a} + ${t.length&&this.host._cursorPt&&"draw"===this.host._tool&&!this.host._contourClosed?ce` + ${this.host._activePlanSnapCandidate||this.host._activePlanSnapConflicts.length?a:ce``}`:a} + ${t.map((t,s)=>ce``)} + `}_renderPartitionDeleteDialog(){const t=this.host._partitionDeleteDialog,e="mi"===this.host.hass?.config?.unit_system?.length;return o`this.host._partitionDeleteDialog=null}> +
+

${this.host._t("confirm.delete_partition_openings_body",{count:t.openings.length})}

+
    + ${t.openings.map(t=>o`
  • ${this.host._t("confirm.delete_partition_openings_item",{type:this.host._t(`opening.${t.type}`),length:ee(t.length*ol/this.host._gridPitch*this.host._cellCm,e)})}
  • `)} +
+
+
+ + + +
+
`}_renderRoomDeleteDialog(){const t=this.host._roomDeleteDialog;return o`this.host._roomDeleteDialog=null}> +
+

${this.host._t("confirm.delete_room_body")}

+
+
+ + + + +
+
`}_renderPhysicalDialog(){const t=this.host._physicalDialog,e="column"===t.kind;return o`this.host._physicalDialog=null}> +
+ ${e?o` + `:a} + +
this.host._physicalDialog={...t,cm:e.target.value}} /> + ${this.host._t(this.host._imperial?"wallthick.unit_in":"wallthick.unit_cm")}
+ ${e&&"square"===t.shape?o` + + this.host._physicalDialog={...t,angle:e.target.value}} />`:a} + ${t.length?o`
${this.host._t("physical.length")}: ${t.length}
`:a} +
+ +
`}_renderMarkupBar(){const t=this.host._geometryHistory.undoName,e=this.host._geometryHistory.redoName,s=t?this.host._t("history.undo_named",{name:t}):this.host._t("history.undo_empty"),i=e?this.host._t("history.redo_named",{name:e}):this.host._t("history.redo_empty");return o`
+
+ + + + + + + + + + ${this.host._editorToolbarGroups.map(t=>this._renderEditorGroupLauncher(t))} + + + + +
+
+ +
+
`}_renderDevicesBar(){return o`
+
+ + + + ${this.host._editorToolbarGroups.map(t=>this._renderEditorGroupLauncher(t))} +
+
+ +
+
`}_renderDeviceInbox(){const t=this.host._deviceInbox,e=this._deviceInboxRows(),s=Object.fromEntries(["on_plan","available","hidden","readd"].map(t=>[t,e.filter(e=>e.category===t).length])),i=function(t,e,s,i=!1){const o=s.trim().toLocaleLowerCase();return t.filter(t=>t.category===e&&(!i||t.isNew)&&(!o||t.searchText.includes(o)))}(e,t.tab,t.search,"on_plan"===t.tab&&t.onlyNew),n=i.slice(0,t.limit),r=t=>this.host._t(`device_inbox.tab_${t}`),l=`device_inbox.empty_${t.tab}`;return o`this.host._deviceInbox=null}> +
+
+ this.host._deviceInbox={...t,search:e.target.value,limit:100}} /> + +
+
+ ${["on_plan","available","hidden","readd"].map(e=>o` + `)} +
+
+ ${"on_plan"===t.tab?o``:a} + ${"available"===t.tab?o``:a} + +
+
+ ${n.length?n.map(t=>{const e="on_plan"===t.category?t.canFind?o``:o``:"hidden"===t.category?o``:o``,s="active"===t.status.kind?"":this.host._t(`device_inbox.status_${t.status.kind}`);return o`
+ +
+
${t.name} + ${t.isNew?o`${this.host._t("device_inbox.new")}`:a} +
+
+ ${[t.model,t.integration,t.spaceName,t.areaName].filter(Boolean).join(" · ")} +
+
+ ${this.host._t(`device_inbox.reason_${t.reason}`)} + ${s?o`${s}`:a} +
+ ${t.binding} +
+
+ ${e} + ${t.canEdit||t.canHide||"available"===t.category||"hidden"===t.category||this.host._bindingHasHaPage(t.binding)?o` +
+ + + +
+ ${t.canEdit&&("on_plan"!==t.category||t.canFind)?o``:a} + ${t.canHide?o``:a} + ${"available"===t.category?o``:a} + ${"hidden"===t.category?o``:a} + ${this.host._bindingHasHaPage(t.binding)?o``:a} +
+
`:a} +
+
`}):o`
${this.host._t(l)}
`} +
+ ${i.length>n.length?o``:a} +
+
+ +
+
`}_markerValueBadgeFields(t){return ti({touched:t.valueBadgeTouched,originalHas:t.originalHasValueBadge,original:t.originalValueBadge,enabled:t.valueBadgeEnabled,source:t.valueBadgeSource,position:t.valueBadgePosition})}_markerDraft(t){if("ha"===t.bindingMode&&(!t.binding||"virtual"===t.binding))return null;const e=ns(t.room),s=Ze(t.binding,t.devId,()=>"__hp_device_preview__"),i=this.host._markers.find(e=>e.id===s||e.id===t.devId),o=ss(t.binding,t.controls,this.host._bindingEntities(t.binding)),a=this._effectiveStoredTapAction(t),n={id:s,binding:t.binding,name:t.name.trim()||null,icon:t.icon||null,display:"badge"!==t.display?t.display:null,ripple_color:"icon_ripple"===t.display&&t.rippleColor?t.rippleColor:null,ripple_size:"icon_ripple"===t.display&&1.5!==t.rippleSize?t.rippleSize:null,size:1!==t.size?t.size:null,angle:t.angle||null,...this._markerTapActionFields(t),...this._markerToggleEntityFields(t),tap_target:"run"===a&&t.tapTarget||null,tap_confirm:!!t.tapConfirm||null,controls:o.length?o:null,...this._markerLightFields(t),...this._markerValueBadgeFields(t),use_climate_temp:!!t.useClimateTemp||null,glow_radius_cm:(()=>{const e=sl(t.glowRadius);return null==e||e<=0?null:Math.round(this.host._imperial?30.48*e:100*e)})(),model:t.model.trim()||null,link:t.link.trim()||null,description:t.description.trim()||null,pdfs:t.pdfs,hidden:t.hideFromPlan,vacuum:i?.vacuum||null};return("virtual"===t.binding||t.room)&&(n.space=e?.space||("virtual"===t.binding?this.host._space:null),n.area=e?.area||null,n.room_id=e?.roomId||null),n}_markerPreviewDevice(t){const e=this._markerDraft(t);if(!e)return null;const s=`${this.host._haRegistry.revision}\n${Et(this.host._markers)}\n${JSON.stringify(e)}`;if(this.host._markerPreviewMemo?.key===s)return this.host._markerPreviewMemo.device;const i=ei({hass:this.host.hass,registry:this.host._haRegistry,areaToSpace:Object.fromEntries(Object.entries(this.host._areaToSpace).map(([t,e])=>[t,e.space])),marker:e,siblingMarkers:this.host._markers,settings:this.host._settings,excluded:this.host._excluded,showAll:this.host._showAll,firstSpaceId:this.host._model[0]?.id||this.host._space,loc:t=>this.host._t(t),iconRules:this.host._iconRules});return this.host._markerPreviewMemo={key:s,device:i},i}_markerPreviewDevices(t){const e=this.host._markerPreviewDevicesMemo;if(e?.base===this.host._devices&&e.preview===t)return e.devices;const s=[...this.host._devices.filter(e=>e.id!==t.id),t];return this.host._markerPreviewDevicesMemo={base:this.host._devices,preview:t,devices:s},s}_toggleIntent(t,e=this.host._devices){return si({hass:this.host._planHass,registryHass:this.host._fullRegistryHass,devices:e,device:t,virtualLights:this.host._virtualLights})}_toggleIntentForDialog(t){const e=this._markerPreviewDevice(t);if(!e)return null;const s=this._markerPreviewDevices(e);return this._toggleIntent(e,s)}_toggleStateText(t,e){const s=this.host._planHass?.states?.[t]||this.host.hass?.states?.[t];try{return s&&"function"==typeof this.host.hass?.formatEntityState?this.host.hass.formatEntityState(s):e}catch{return e}}_toggleConfirmationStateText(t){const e=String(t.state||"unknown"),s=t.entityId?this._toggleStateText(t.entityId,e):e;if(s.trim().toLocaleLowerCase()!==e.trim().toLocaleLowerCase())return s;const i={on:"confirm.state_on",off:"confirm.state_off",open:"confirm.state_open",closed:"confirm.state_closed",opening:"confirm.state_opening",closing:"confirm.state_closing",unknown:"confirm.state_unknown"}[e];return i?this.host._t(i):e.replaceAll("_"," ").replaceAll("-"," ")}_toggleConfirmationLines(t){const e={"turn-on":"confirm.state_on","turn-off":"confirm.state_off",open:"confirm.state_open",close:"confirm.state_closed",stop:"confirm.state_stopped"};return ii(t,{state:t=>this._toggleConfirmationStateText(t),current:t=>this.host._t("confirm.current_state",{state:t}),expected:t=>this.host._t("confirm.expected_state",{state:t}),groupCurrent:(t,e)=>this.host._t("confirm.group_current",{on:t,total:e}),groupAllOn:()=>this.host._t("confirm.group_all_on"),groupAllOff:()=>this.host._t("confirm.group_all_off"),unavailable:t=>this.host._t("confirm.unavailable_targets",{count:t}),effect:t=>this.host._t(e[t]),expectedByHa:()=>this.host._t("confirm.expected_by_ha")})}_toggleHintLines(t){if(!t)return[];const e=t=>this.host._t(`marker.toggle_effect_${t.replace("-","_")}`),s=t=>this.host._t(`marker.toggle_skip_${t.replace("-","_")}`);return oi(t,{single:t=>{if("via"in t&&"virtual-light"===t.via)return this.host._t("marker.virtual_light_target",{name:t.name});const e=t.entityId||("ref"in t?t.ref:""),s=t.name||e;return this.host._t("marker.toggle_hint_single",{name:s,id:e})},group:t=>this.host._t("marker.toggle_hint_group",{count:t.length,names:t.map(t=>`${t.name} (${t.entityId})`).join(", ")}),currentNext:(t,s)=>"virtual-light"===t.via?this.host._t("marker.virtual_light_current",{state:this.host._t("on"===t.state?"marker.virtual_light_state_on":"marker.virtual_light_state_off"),effect:e(s)}):this.host._t("marker.toggle_hint_current",{state:this._toggleStateText(t.entityId,t.state),effect:e(s)}),groupCurrentNext:(t,s)=>this.host._t("marker.toggle_hint_group_current",{on:t.filter(t=>"on"===t.state).length,count:t.length,effect:e(s)}),skipped:t=>this.host._t("marker.toggle_hint_skipped",{count:t.length,targets:t.map(t=>{const e=t.entityId||t.ref;return`${t.name||e} (${e}: ${s(t.reason)})`}).join(", ")}),none:t=>this.host._t(`marker.toggle_none_${t.replaceAll("-","_")}`)})}_effectiveStoredTapAction(t,e){return t.tapActionTouched?t.tapAction:is(t.originalHasTapAction?t.originalTapAction:null,e)}_effectiveMarkerTapAction(t,e=this._markerPreviewDevice(t)){return this._effectiveStoredTapAction(t,e?.primary?.split(".")[0])}_announceToggleDraft(t){const e=this._markerPreviewDevice(t),s=t.tapActionTouched?t:{...t,tapAction:this._effectiveMarkerTapAction(t,e)},i="toggle"===s.tapAction?this._toggleHintLines(this._toggleIntentForDialog(s)).join(" "):"";return{...s,tapHintAnnouncement:i}}_valueBadgeForBinding(t,e){const s={...t,binding:e,valueBadgeEnabled:!1,valueBadgeSource:null,valueBadgeTouched:!0},i=this._markerPreviewDevice(s);if(!i)return{valueBadgeEnabled:!1,valueBadgeSource:null,valueBadgeTouched:!0};const o=[...this.host._devices.filter(t=>t.id!==i.id),i],a=ts(this.host._planHass,i,o),n=es(this.host._planHass,i,a);return{valueBadgeEnabled:t.valueBadgeEnabled&&!!n,valueBadgeSource:n,valueBadgeTouched:!0}}_markerSpatialSource(t){const e=this._markerPreviewDevice(t);if(!e)return null;const s={...e,hidden:!1};return ai(ni(this.host._planHass,[...this.host._devices.filter(t=>t.id!==s.id),s],null,this.host._virtualLights).filter(t=>t.device.id===s.id))}_markerAutoHasSpatialSource(t){const e={...t,lightRole:"auto",lightRoleTouched:!0},s=this._markerPreviewDevice(e);return!!s&&ri(this.host._planHass,{...s})}_setMarkerLightRole(t){const e=this.host._markerDialog;e&&(this.host._markerDialog={...e,lightRole:t,lightRoleTouched:!0})}_controlRefInfo(t){if(!t.startsWith("marker:"))return{label:this.host.hass.states[t]?.attributes?.friendly_name||t,sub:t,icon:t.startsWith("light.")?"mdi:lightbulb":"mdi:toggle-switch",warning:!this.host._planEntityAvailable(t)};const e=t.slice(7),s=this.host._markers.find(t=>t.id===e),i=this.host._devices.find(t=>t.id===e);if(!s||s.removed||!0!==s.is_light)return{label:this.host._t("marker.control_missing_label"),sub:`${this.host._t("marker.control_broken")} (${t})`,icon:"mdi:alert-outline",warning:!0};const o=this.host._serverCfg?.spaces.find(t=>t.id===(i?.space||s.space)),a=!!i&&li(i).length>0;return{label:s.name||i?.name||e,sub:[o?.title||i?.space||s.space,s.room_id||i?.area,a?"":this.host._t("marker.control_passive")].filter(Boolean).join(" · "),icon:s.icon||i?.icon||"mdi:lightbulb-outline",warning:!0===s.hidden||!0===i?.hidden}}_valueBadgeCandidateLabel(t){const e=t.source;if("derived_lqi"===e.kind)return this.host._t("marker.value_badge_lqi");if("derived_marker_state"===e.kind)return this.host._t("marker.value_badge_marker_state",{name:t.label});if("entity_state"===e.kind)return this.host._t("marker.value_badge_state",{name:t.label});const s=this.host.hass.states[e.entity_id]?.attributes?.friendly_name||this.host._fullRegistryHass.entities[e.entity_id]?.name||e.entity_id;return this.host._t(`marker.value_badge_attr_${e.attribute}`,{name:s})}_controlCandidates(t){const e=this._markerDraft(t)?.id||t.devId||"",s=[],i=new Set;for(const t of this.host._markers){if(t.id===e||t.removed||t.hidden||!0!==t.is_light)continue;if("virtual"!==t.binding&&"active"!==this.host._bindingStatus(t.binding).kind)continue;const o=this.host._devices.find(e=>e.id===t.id);if(!o||o.hidden)continue;const a=this._controlRefInfo(`marker:${t.id}`);for(const t of li(o))i.add(t);s.push({value:`marker:${t.id}`,label:a.label,sub:a.sub,icon:a.icon,search:`${a.label} ${a.sub} ${t.id} ${li(o).join(" ")}`.toLowerCase()})}const o=Object.keys(this.host.hass.states||{}).filter(e=>hi(e)&&!i.has(e)&&ci(t.binding,[e],this.host._bindingEntities(t.binding)).length>0&&this.host._planEntityAvailable(e)).map(t=>{const e=this._controlRefInfo(t);return{value:t,label:e.label,sub:t,icon:e.icon,search:`${e.label} ${t}`.toLowerCase()}}),a=t.controlsFilter.trim().toLowerCase();return[...s,...o].filter(e=>!t.controls.includes(e.value)&&(!a||e.search.includes(a))).slice(0,12).map(({search:t,...e})=>e)}_addControlRef(t,e){if(e.startsWith("marker:")){const s=this._markerDraft(t)?.id||t.devId||"";if(!s)return void this.host._showToast(this.host._t("toast.marker_binding_required"));const i=e.slice(7),o=this._markerDraft(t),a=o?[...this.host._markers.filter(t=>t.id!==s),o]:this.host._markers;if(di(a,s,i))return void this.host._showToast(this.host._t("toast.marker_control_cycle"))}this.host._markerDialog=this._announceToggleDraft({...t,controls:[...t.controls,e],controlsFilter:""})}_setMarkerGlowMode(t){const e=this.host._markerDialog;if(!e)return;if("auto"===t)return void(this.host._markerDialog={...e,glowMode:t,glowTouched:!0});const s=this._markerSpatialSource(e),i=pi(s?this.host._planHass.states[s.eid]:void 0,null,this.host._fillColors.glow_light.c),o=!e.glowColorDrafted,a="fixed"===t&&!e.glowBrightnessDrafted;this.host._markerDialog={...e,glowMode:t,glowColor:o?i.c:e.glowColor,glowBrightness:a?Math.max(1,Math.round(100*i.bri)):e.glowBrightness,glowColorDrafted:!0,glowBrightnessDrafted:e.glowBrightnessDrafted||"fixed"===t,glowTouched:!0}}_renderMarkerDialog(){const t=this.host._markerDialog,e="virtual"===t.bindingMode,s=this._bindingCandidates();this.host._bindingEntities(t.binding);const i=e?null:this.host._bindingStatus(t.binding),n=!e&&this.host._bindingHasHaPage(t.binding),r=this._markerPreviewDevice(t),l=this._effectiveMarkerTapAction(t,r),h=r?ps(this.host._serverCfg?.spaces.find(t=>t.id===r.space)):null,c=r?this._markerPreviewDevices(r):this.host._devices,d="toggle"===l&&r?this._toggleIntent(r,c):null,p=this._toggleHintLines(d),_=r?_i(this.host._planHass,r,{liveStates:!1!==this.host._config?.live_states,showTemperature:!1!==this.host._config?.show_temperature,showSignal:h?.showLqi??!1!==this.host._config?.show_signal,designPreview:!0,activityRuntime:this.host._activityRt.get(r.id),lightDevices:c,registryHass:this.host._fullRegistryHass,reducedMotion:this.host._reducedMotion}):null,u=r?ts(this.host._planHass,r,c):[],m=r?es(this.host._planHass,r,u):null,g=t.valueBadgeTouched?t.valueBadgeEnabled:!!_?.valueBadge,f=t.valueBadgeTouched?t.valueBadgeSource:_?.valueBadge?.source||t.valueBadgeSource,b=t.valueBadgeTouched?t.valueBadgePosition:_?.valueBadge?.position||t.valueBadgePosition,y=ui(f),v=!!f&&!u.some(t=>t.key===y),$=u.find(t=>t.key===y),w=_?.valueSource?_.valueSource.attribute?`attr:${_.valueSource.eid}:${_.valueSource.attribute}`:`state:${_.valueSource.eid}`:"",k=this._markerAutoHasSpatialSource(t),x=!!r&&mi(this.host._planHass,{...r}),S=gi(t.lightRole,k,x,t.glowMode),M=!S.sourceExists,D=!S.fromSourceEnabled,C=S.passive,P=S.effectiveMode,I="never"===t.lightRole?this.host._t("marker.glow_disabled_never"):"auto"!==t.lightRole||k?C?this.host._t("marker.glow_passive_hint"):this.host._t("marker.glow_disabled_no_entity"):this.host._t("marker.glow_disabled_auto"),T=r?li(r):[],R=r&&fi(r)||"",F=!!t.lightEntity&&!T.includes(t.lightEntity),N=r?bi(r):[],z=!!t.toggleEntity&&!N.includes(t.toggleEntity),A="toggle"===l?this._toggleIntentForDialog({...t,toggleEntity:"",toggleEntityTouched:!0}):null,O=A?[...A.targets,...A.skippedTargets].map(t=>t.entityId||("ref"in t?t.ref:"")).filter(Boolean).join(", "):"",L=(()=>{if(e)return null;const i=s.find(e=>e.value===t.binding);if(i)return i.label;const[o,a]=t.binding.split(":");return"device"===o?this.host._fullRegistryHass.devices[a]?.name_by_user||this.host._fullRegistryHass.devices[a]?.name||a:this.host._fullRegistryHass.entities[a]?.name||this.host.hass.states[a]?.attributes?.friendly_name||a})();return o` +
+ ${"ha_disabled"===i?.kind?o`
+ + ${this.host._t(`marker.ha_disabled_${i.reason}`)} + ${n?o``:a} +
`:"unverified"===i?.kind&&t.binding?o`
+ + ${this.host._t("marker.ha_registry_limited")} +
`:a} + + this.host._markerDialog={...t,name:e.target.value}} /> + + +
+ +
+ + +
+ ${"ha"===t.bindingMode?o` + ${t.bindingOpen?o`
+ this.host._markerDialog={...t,bindingFilter:e.target.value}} /> +
+ ${s.map(e=>o`
{const s={...t,binding:e.value,bindingOpen:!1,controls:ss(e.value,t.controls,this.host._bindingEntities(e.value)),autoIcon:this.host._autoIconForBinding(e.value)};this.host._markerDialog=this._announceToggleDraft({...s,...this._valueBadgeForBinding(s,e.value)})}}> + ${e.label}${e.sub} +
`)} + ${s.length?a:o`
${this.host._t("marker.nothing_found")}
`} +
+
`:a}`:a} +
+ + + + + ${this._renderVacSection(t)} + + + + ${"toggle"===l&&(N.length>1||z)?o`
+
+ + ${this._help("marker.toggle_entity.help")} +
+ + ${z?o`

+ + ${this.host._t("marker.toggle_entity_missing",{entity:t.toggleEntity,fallback:O||this.host._t("marker.toggle_entity_none")})} +

`:a} +
`:a} + ${"toggle"===l?o`
+ ${p.map(t=>o`
${t}
`)} +
+
${t.tapHintAnnouncement}
`:a} + ${"run"===l?(()=>{const e=t.runFilter.trim().toLowerCase(),s=this._runCandidates().filter(t=>!e||t.label.toLowerCase().includes(e)||t.value.includes(e)),i=t.tapTarget?this._runCandidates().find(e=>e.value===t.tapTarget):null;return o` + + ${t.tapTarget&&!i?o`
${this.host._t("marker.run_target_gone",{id:t.tapTarget})}
`:a} + {t.target.select()}} + @input=${e=>this.host._markerDialog={...t,runFilter:e.target.value,tapTarget:""}} /> + ${i?a:o`
+ ${s.slice(0,40).map(e=>o`
this.host._markerDialog={...t,tapTarget:e.value,runFilter:""}}> + ${e.label}${e.sub} +
`)} + ${s.length?a:o`
${this.host._t("marker.nothing_found")}
`} +
`}`})():a} + ${"run"===l||"toggle"===l?o``:a} + + +
${this.host._t("marker.controls_hint")}
+ ${t.controls.length?o`
+ ${t.controls.map(e=>{const s=this._controlRefInfo(e);return o` + ${s.label} + this.host._markerDialog=this._announceToggleDraft({...t,controls:t.controls.filter(t=>t!==e)})}> + `})} +
`:a} + this.host._markerDialog={...t,controlsFilter:e.target.value}} /> + ${t.controlsFilter.trim()?o`
+ ${this._controlCandidates(t).map(e=>o``)} +
`:a} + + ${this.host._bindingHasClimate(t.binding)?o``:a} +
+ ${this.host._t("marker.light_role_label")}${this._help("marker.light_role.help")} +
+ + + +
+
+ + ${"always"===t.lightRole&&(T.length>1||F)?o`
+
+ + ${this._help("marker.light_entity.help")} +
+ + ${F?o`

+ + ${this.host._t("marker.light_entity_missing",{entity:t.lightEntity,fallback:R||"—"})} +

`:a} +
`:a} + +
+ ${this.host._t("marker.glow_color_label")}${this._help("marker.glow_mode.help")} +
+ + + +
+ ${"auto"!==P?o`
+ {this.host._markerDialog={...t,glowMode:P,glowColor:e.detail.color,glowColorDrafted:!0,glowTouched:!0}}}> + ${"fixed"===P?o` + ${this.host._t("marker.glow_brightness")} + ${this._rangeInput(1,100,1,t.glowBrightness,e=>{this.host._markerDialog={...t,glowMode:P,glowBrightness:e,glowBrightnessDrafted:!0,glowTouched:!0}},M,this.host._t("marker.glow_brightness"))} + ${Math.round(t.glowBrightness)}%`:a} +
`:a} +
+
+
+ + ${this._help("marker.glow_radius.help")} +
+
+ this.host._markerDialog={...t,glowRadius:e.target.value}} /> + ${this.host._imperial?this.host._t("gs.unit_ft"):this.host._t("gs.unit_m")} +
+
+ ${M||C?o`

+ ${I} +

`:a} + + + ${customElements.get("ha-icon-picker")?o`{const s=e.detail.value||"";(t.icon||s!==t.autoIcon)&&(this.host._markerDialog={...t,icon:s})}}>`:o`this.host._markerDialog={...t,icon:e.target.value}} />`} + ${!t.icon&&t.autoIcon?o`

+ ${this.host._t("marker.icon_auto",{icon:t.autoIcon})} +

`:a} + + + +

${this.host._t(Qr[t.display])}

+ ${"static_icon"===t.display&&this.host._bindingHasAlarm(t.binding)?o`
+ + ${this.host._t("marker.static_alarm_warning")} +
`:a} +
+ ${this.host._t("marker.value_badge_title")}${this._help("marker.value_badge.help")} + + ${"static_icon"===t.display?o`

+ ${this.host._t("marker.value_badge_static")} +

`:u.length||t.valueBadgeSource?a:o`

+ ${this.host._t("marker.value_badge_empty")} +

`} + ${g?o` +
+ + ${this._help("marker.value_badge_source.help")} +
+ + ${$?o`

${$.technical}

`:a} + ${v?o`

+ ${this.host._t("marker.value_badge_missing_hint")} +

`:a} + ${"value"===t.display&&y===w?o`

+ ${this.host._t("marker.value_badge_duplicate")} +

`:a} +
+ + ${this._help("marker.value_badge_position.help")} +
+ + `:a} +
+ ${_?o` + `:o`
+ + ${this.host._t("marker.preview.select_source")} +
`} + ${"icon_ripple"===t.display?o`
+ {this.host._markerDialog={...t,rippleColor:e.detail.color}}}> +
+
+ ${this.host._t("marker.ripple_size")} + ${this._rangeInput(1,8,.5,t.rippleSize,e=>this.host._markerDialog={...t,rippleSize:e})} + ×${t.rippleSize} +
+

${this.host._t("marker.activity_alarm_note")}

`:a} + + +
+ ${this._rangeInput(.5,3,.1,t.size,e=>this.host._markerDialog={...t,size:e})} + ×${t.size.toFixed(1)} + ${this.host._t("marker.angle_label")} + ${""} + ${this._rangeInput(0,355,5,t.angle,e=>this.host._markerDialog={...t,angle:e})} + ${t.angle}° +
+ + + this.host._markerDialog={...t,model:e.target.value}} /> + + + this.host._markerDialog={...t,link:e.target.value}} /> + + + + + +
+ ${t.pdfs.map(t=>o` + ${t.name} + this._removeMarkerPdf(t.url)}>`)} + + + this._pickMarkerFiles(t)} /> + +
+
+
+
+ ${t.devId?o``:a} + ${t.devId?o``:a} +
+
+ + +
+
+
`}_renderSpaceDialog(){const t=this.host._spaceDialog,e=this.host._importTotal>0&&"create"===t.mode?this.host._t("import.progress",{i:this.host._importTotal-this.host._importQueue.length,n:this.host._importTotal}):"",s=()=>{this.host._spaceDialog=null,this.host._importQueue=[],this.host._importTotal=0};return o` +
+ + this.host._spaceDialog={...t,title:e.target.value}} /> + + + ${"file"===t.source?o`
+ ${t.planFile?o`${t.planFile.name}`:t.planUrl?o`${this.host._t("space.plan_alt")}`:o`${this.host._t("space.no_plan")}`} + + + this._pickPlanFile(t)} /> + + +
+ ${t.pickSaved?this._renderServerPlans(t):a}`:a} + + + +
+ {const s=e.target.value,i=sl(s),o=null==i?null:ki(i,this.host._imperial);this.host._spaceDialog={...t,cellCmInput:s,cellCmTouched:!0,cellCm:null!=o&&o>0?Math.max(.1,Math.min(el,o)):t.cellCm}}} /> + ${this.host._t(this.host._imperial?"space.scale_unit_imperial":"space.scale_unit")} +
+ + + + + +
${this.host._t("space.zero_wall_help")}
+ + + ${""} + +
${this.host._t("space.hide_decor_tip")}
+ +
${this.host._t("space.hide_openings_tip")}
+ + ${[["labelTemp","space.label_temp"],["labelHum","space.label_hum"],["labelLqi","space.label_lqi"],["labelLight","space.label_light"]].map(([e,s])=>o``)} + +
+ ${this._rangeInput(50,300,5,Math.round(100*t.cardFontScale),e=>this.host._spaceDialog={...t,cardFontScale:e/100})} + ${Math.round(100*t.cardFontScale)}% +
+ ${this.host._renderCardPreview(t.cardFontScale,1,1)} +
+ {this.host._spaceDialog={...t,roomColor:e.detail.color,roomOpacity:e.detail.opacity}}}> +
+ + + ${"static"===(t.bgMode??St(this.host._settings,{}))?o`
+ {this.host._spaceDialog={...t,bgColor:e.detail.color}}}> + ${t.bgColor?o``:o`${this.host._t("space.bg_inherited")}`} +
`:a} + +
+ {const s=e.target.value.trim(),i=""===s?null:Math.round(Number(s));this.host._spaceDialog={...t,northDeg:null!==i&&Number.isFinite(i)?Math.min(359,Math.max(0,i)):null}}} /> + ${null===t.northDeg?this.host._t("space.north_inherited",{v:null===Mt(this.host._settings,{})?"—":String(Mt(this.host._settings,{}))+"°"}):"°"} +
+ + + + ${xi.map(t=>[t,"fill."+t]).map(([e,s])=>o` + ${"custom"===e&&"custom"===t.fillMode?o`
+ ${this.host._t("space.custom_fill")} + {this.host._spaceDialog={...t,customFill:{c:e.detail.color,a:e.detail.opacity}}}}> + ${t.customFill?o``:a} +
`:a}`)} + + ${t.deleteBlockers?o``:a} +
+ +
`}_renderMergeDialog(){const t=this.host._mergeDialog,e=this.host._spaceModel()?.rooms||[],s=(s,i)=>{const a=e.find(t=>t.id===s),n=a?.area?this.host.hass.areas[a.area]?.name:null;return o``};return o`this.host._mergeDialog=null}> +
+

${this.host._t("merge.hint")}

+ + ${s(t.aId,"a")} + ${s(t.bId,"b")} +
+
+ + + +
+
`}_renderRoomSource(t){const e="temp"===t?this.host._roomTempSrc:this.host._roomHumSrc,s=e=>{"temp"===t?this.host._roomTempSrc=e:this.host._roomHumSrc=e,this.host.requestUpdate()},i=this.host._roomSrcOpen===t;return o` + + + + ${e||i?o` + ${i?o`
+ {this.host._roomSrcFilter=t.target.value,this.host.requestUpdate()}} /> +
+ ${this._roomSrcCandidates().map(t=>o`
{s(t.value),this.host._roomSrcOpen=null}}> + ${t.label}${t.sub} +
`)} +
+
`:a}`:a}`}_renderRoomDialog(){const t=!!this.host._roomEditId,e=t?null:this.host._wallFaceBatch,s=e&&e.candidates.length>1?this.host._t("room.queue_progress",{current:e.index+1,total:e.candidates.length}):"",i=!!this.host._areaSel||!!this.host._nameSel.trim(),n=ps(this.host._curSpaceCfg),r=this.host._roomFill||n.fill,l=this.host._roomCustomFill||n.customFill,h=[...this.host._freeAreas];if(t&&this.host._areaSel&&!h.some(t=>t.area_id===this.host._areaSel)){const t=this.host.hass.areas[this.host._areaSel];t&&h.unshift(t)}return o` +
+ ${s?o`

+ ${s} +

`:a} + + this.host._nameSel=t.target.value} /> + + + + + + ${[["","fill.inherit"],...Si.map(t=>[t,"fill."+t])].map(([t,e])=>o``)} + ${"custom"===r?o`
+ ${this.host._roomCustomFill?this.host._t("room.custom_fill_own"):this.host._t("room.custom_fill_space")} + {this.host._roomCustomFill={c:t.detail.color,a:t.detail.opacity}}}> + ${this.host._roomCustomFill?o``:a} +
`:a} + ${this._renderRoomSource("temp")} + ${this._renderRoomSource("hum")} + + + +
+ ${this._rangeInput(50,300,5,Math.round(100*this.host._roomNameScale),t=>{this.host._roomNameScale=t/100,this.host.requestUpdate()})} + ${Math.round(100*this.host._roomNameScale)}% +
+ +
+ ${this._rangeInput(50,300,5,Math.round(100*this.host._roomLabelScale),t=>{this.host._roomLabelScale=t/100,this.host.requestUpdate()})} + ${Math.round(100*this.host._roomLabelScale)}% +
+ ${this.host._renderCardPreview(ps(this.host._curSpaceCfg).cardFontScale,this.host._roomNameScale,this.host._roomLabelScale)} +
+
+ + + ${t?o``:o`${this.host._pendingSplit?a:o``} + `} +
+
`}}export{rl as EDITOR_RUNTIME_FINGERPRINT,ll as HouseplanEditorRuntime}; diff --git a/dist/houseplan-assets/houseplan-onboarding-runtime-Bj55plb1.js b/dist/houseplan-assets/houseplan-onboarding-runtime-Bj55plb1.js new file mode 100644 index 00000000..05f8b521 --- /dev/null +++ b/dist/houseplan-assets/houseplan-onboarding-runtime-Bj55plb1.js @@ -0,0 +1,217 @@ +globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="f111431804c4c42c6e35286f29720d089bc8b77ffc364a6517f7a3510fcb11c8";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 c,b$ as n,c0 as r,b as p,A as _,cV as d,ap as u,ar as g,cW as m}from"./houseplan-card-Cs2QHIcW.js";import{i as b,c as $,a as v,s as f,t as y}from"./space-deletion-CJ8Co_o8.js";const w=1e3,D=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},S="f111431804c4c42c6e35286f29720d089bc8b77ffc364a6517f7a3510fcb11c8";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),c=i.settings?.custom_fill&&"object"==typeof i.settings.custom_fill?s(i.settings.custom_fill):null,n="none"===h.fill?{...c||e,a:0}:c;return void(this.host._spaceDialog={mode:p,spaceId:_,title:i.title,planUrl:i.plan_url||null,planFile:null,source:i.plan_url?"file":"draw",showBorders:h.showBorders,showNames:h.showNames,zeroWallStyle:l(i),displayTouched:!0,hideDecor:h.hideDecor,hideOpenings:h.hideOpenings,roomColor:h.color,roomOpacity:h.opacity,fillMode:"none"===h.fill?"custom":h.fill,customFill:n,glowEnabled:h.glow,bgColor:h.bgColor,bgMode:"static"===i.settings?.bg_mode||"daynight"===i.settings?.bg_mode?i.settings.bg_mode:null,northDeg:a({},i.settings),sunRays:"boolean"==typeof i.settings?.sun_rays?i.settings.sun_rays:null,tempMin:h.tempMin,tempMax:h.tempMax,showLqi:h.showLqi??this.host._config?.show_signal??!0,cardFontScale:h.cardFontScale,labelTemp:h.labelTemp,labelHum:h.labelHum,labelLqi:h.labelLqi,labelLight:h.labelLight,cellCm:Number(i.cell_cm)>0?Number(i.cell_cm):5,cellCmInput:o(Number(i.cell_cm)>0?Number(i.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1})}const d=i(this.host._imperial);this.host._spaceDialog={mode:p,title:"",planUrl:null,planFile:null,...b(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:r,roomOpacity:n,fillMode:"custom",customFill:{...e,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:c,tempMax:h,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:d,cellCmInput:o(d,this.host._imperial),cellCmTouched:!1,busy:!1}}async _pickPlanFile(t){const s=t.target,e=s.files?.[0];if(!e||!this.host._spaceDialog)return;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{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:c,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`
${this.host._t("space.loading")}
`;const s=t.saved||[];if(!s.length)return p`
${this.host._t("space.no_saved")}
`;return p`
+ ${s.map(s=>{return p` +
+ +
+ ${s.name} + ${e=s.size,e>=1048576?`${(e/1048576).toFixed(1)} MB`:`${Math.round(e/1024)} KB`}${s.used_by.length?` · ${this.host._t("space.used_by",{list:s.used_by.join(", ")})}`:""} +
+ + +
`;var e})} +
`}async _saveConfigNow(){this.host._cfgEpoch++;try{await this.host._writeConfig()}catch(t){const s=t;throw s?.physicalGeometryRolledBack?await this.host._reloadRejectedPhysicalWrite():"conflict"===s?.code&&await this.host._reloadConfigOnly(),t}}async _saveSpaceDialog(){const t=this.host._spaceDialog;if(!t||t.busy||!t.title.trim())return;if("file"===t.source&&!t.planFile&&!t.planUrl)return void this.host._showToast(this.host._t("toast.plan_required"));const s="create"===t.mode&&0===(this.host._serverCfg?.spaces.length||0);this.host._spaceDialog={...t,busy:!0};try{const e="create"===t.mode?`s${Date.now().toString(36)}`:t.spaceId;let o=null;if("file"===t.source&&t.planFile){o={url:(await this.host.hass.callWS({type:"houseplan/plan/set",space_id:e,ext:t.planFile.ext,data:t.planFile.b64})).url,aspect:t.planFile.aspect}}let a=t.savedAspect||null;!o&&"file"===t.source&&t.planUrl&&!a&&this.host._aspectJob&&(a=await this.host._aspectJob||null);const l=this.host._serverCfg;let i=l.spaces.find(t=>t.id===e);if("create"===t.mode)i=$(e,t.title.trim()),l.spaces.push(i);else{if(!i)throw new Error(`space ${e} is gone from the config`);i.title=t.title.trim()}if(!i)throw new Error(`space ${e} is unavailable`);if(o?(i.plan_url=o.url,i.plan_aspect=o.aspect):"file"===t.source&&t.planUrl&&t.planUrl!==i.plan_url&&(i.plan_url=t.planUrl,i.plan_aspect=a),"draw"===t.source&&(i.plan_url=null,i.plan_aspect=null,delete i.plan_x,delete i.plan_y,delete i.plan_scale,delete i.plan_scale_x,delete i.plan_scale_y,delete i.plan_angle),i.settings={...i.settings||{},show_borders:t.showBorders,show_names:t.showNames,hide_decor:t.hideDecor||void 0,hide_openings:t.hideOpenings||void 0,room_color:t.roomColor,room_opacity:t.roomOpacity,bg_color:t.bgColor||void 0,bg_mode:t.bgMode||void 0,north_deg:t.northDeg??void 0,sun_rays:t.sunRays??void 0,fill_mode:t.fillMode,custom_fill:t.customFill||void 0,glow_enabled:t.glowEnabled,temp_min:Number.isFinite(t.tempMin)?Math.min(t.tempMin,t.tempMax):c,temp_max:Number.isFinite(t.tempMax)?Math.max(t.tempMin,t.tempMax):h,show_lqi:t.showLqi,card_font_scale:1!==t.cardFontScale?t.cardFontScale:void 0,label_temp:t.labelTemp,label_hum:t.labelHum,label_lqi:t.labelLqi,label_light:t.labelLight},i.zero_wall_style=t.zeroWallStyle,i.cell_cm=Number.isFinite(t.cellCm)&&t.cellCm>0?Math.max(.1,Math.min(w,t.cellCm)):5,await this._saveConfigNow(),this.host._spaceDialog=null,"create"===t.mode&&this.host._commitSpace(i.id),this.host._regSignature="",this.host._maybeRebuildDevices(),this.host._importQueue.length)this._openNextImport();else if(s||this.host._importTotal>0){const t=this.host._importTotal>0;this.host._importTotal=0,this.host._commitSpace(this.host._serverCfg.spaces[0]?.id||this.host._space),await this.host._requestMode("plan"),this.host._tool="draw",this.host._path=[],this.host._cursorPt=null,this.host._primeDrawWallField(),this.host._showToast(this.host._t(s&&!t?"toast.space_added_onboard":"import.done"))}else this.host._showToast(this.host._t("create"===t.mode?"toast.space_added":"toast.space_saved")),"create"===t.mode&&("plan"!==this.host._mode?await this.host._requestMode("plan"):(this.host._tool="draw",this.host._path=[],this.host._cursorPt=null,this.host._primeDrawWallField(),this.host._saveNav()))}catch(t){const s=t;"conflict"!==s?.code&&await this.host._reloadConfigOnly(!0),this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,busy:!1}),this.host._showToast(this.host._t("toast.error",{err:this.host._errText(t)}))}}async _deleteSpace(){const t=this.host._spaceDialog;if(!t||"edit"!==t.mode)return;const s=this.host._serverCfg;if(!s)return;const e=s.spaces.find(s=>s.id===t.spaceId);if(!e)return;const o=v(s,this.host._layout||{},t.spaceId||""),a=1===s.spaces.length&&s.spaces[0]?.id===t.spaceId;if(!o.count||a){if(confirm(this.host._t("confirm.delete_space",{title:e.title}))){this.host._spaceDialog={...t,deleteBlockers:0,busy:!0};try{this.host._saveConfigDebounced.pending()&&this.host._saveConfigDebounced.flush(),this.host._persistLayout.pending()&&this.host._persistLayout.flush(),await this.host._writeChain;const s=await this.host.hass.callWS({type:"houseplan/space/delete",space_id:t.spaceId,expected_config_rev:this.host._cfgRev,expected_layout_rev:this.host._layoutRev}),[e,o]=await Promise.all([this.host.hass.callWS({type:"houseplan/config/get"}),this.host.hass.callWS({type:"houseplan/layout/get"})]);this.host._adoptStructuralResponses(e,o),this.host._cfgRev=s?.config_rev??this.host._cfgRev,this.host._layoutRev=s?.layout_rev??this.host._layoutRev,this.host._spaceDialog=null,this.host._space===t.spaceId&&this.host._commitSpace(this.host._serverCfg.spaces[0]?.id||""),this.host._regSignature="",this.host._maybeRebuildDevices(),this.host._showToast(this.host._t("toast.space_deleted"))}catch(s){const e=s;"conflict"!==e?.code&&"space_in_use"!==e?.code||await Promise.all([this.host._reloadConfigOnly(!0),this.host._reloadLayoutOnly()]);const o=this.host._serverCfg;if(this.host._spaceDialog&&o){const s=v(o,this.host._layout||{},t.spaceId||""),e=1===o.spaces.length&&o.spaces[0]?.id===t.spaceId;this.host._spaceDialog={...this.host._spaceDialog,busy:!1,deleteBlockers:e?0:s.count}}this.host._showToast(this.host._t("toast.delete_failed",{err:this.host._errText(s)}))}}}else this.host._spaceDialog={...t,deleteBlockers:o.count}}_startImport(){const t=this.host._importDialog;if(!t)return;const s=t.floors.filter(t=>t.checked).map(t=>t.name);this.host._importDialog=null,s.length?(this.host._importQueue=s,this.host._importTotal=s.length,this._openNextImport()):this._openSpaceDialog("create")}_openNextImport(){const t=this.host._importQueue.shift();if(void 0===t)return;const s=i(this.host._imperial);this.host._spaceDialog={mode:"create",title:t,planUrl:null,planFile:null,...b(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:r,roomOpacity:n,fillMode:"custom",customFill:null,glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:c,tempMax:h,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:s,cellCmInput:o(s,this.host._imperial),cellCmTouched:!1,busy:!1}}_skipImport(){this.host._spaceDialog=null,this.host._importQueue.length?this._openNextImport():this.host._importTotal>0&&this.host._model.length&&(this.host._importTotal=0,this.host._commitSpace(this.host._serverCfg.spaces[0]?.id||this.host._space),this.host._requestMode("plan").then(()=>{this.host._showToast(this.host._t("import.done"))}))}_renderImportDialog(){const t=this.host._importDialog,s=t.floors.filter(t=>t.checked).length;return p`this.host._importDialog=null}> +
+
${this.host._t("import.hint")}
+ ${t.floors.map((s,e)=>p``)} +
+
+ + + +
+
`}_boolInput(t,s){return p`s(t.target.checked)} />`}_rangeInput(t,s,e,o,a){return p`a(Number(t.target.value))} />`}_renderSpaceDialog(){return this._renderSpaceDialogBody(this.host._spaceDialog)}_renderSpaceDialogBody(t){const s=this.host._importTotal>0&&"create"===t.mode?this.host._t("import.progress",{i:this.host._importTotal-this.host._importQueue.length,n:this.host._importTotal}):"",l=()=>{this.host._spaceDialog=null,this.host._importQueue=[],this.host._importTotal=0};return p` +
+ + this.host._spaceDialog={...t,title:s.target.value}} /> + + + ${"file"===t.source?p`
+ ${t.planFile?p`${t.planFile.name}`:t.planUrl?p`${this.host._t("space.plan_alt")}`:p`${this.host._t("space.no_plan")}`} + + + this._pickPlanFile(t)} /> + + +
+ ${t.pickSaved?this._renderServerPlans(t):_}`:_} + + + +
+ {const e=s.target.value,o=D(e),a=null==o?null:d(o,this.host._imperial);this.host._spaceDialog={...t,cellCmInput:e,cellCmTouched:!0,cellCm:null!=a&&a>0?Math.max(.1,Math.min(w,a)):t.cellCm}}} /> + ${this.host._t(this.host._imperial?"space.scale_unit_imperial":"space.scale_unit")} +
+ + + + + +
${this.host._t("space.zero_wall_help")}
+ + + +
${this.host._t("space.hide_decor_tip")}
+ +
${this.host._t("space.hide_openings_tip")}
+ + ${[["labelTemp","space.label_temp"],["labelHum","space.label_hum"],["labelLqi","space.label_lqi"],["labelLight","space.label_light"]].map(([s,e])=>p``)} + +
+ ${this._rangeInput(50,300,5,Math.round(100*t.cardFontScale),s=>{this.host._spaceDialog={...t,cardFontScale:s/100}})} + ${Math.round(100*t.cardFontScale)}% +
+ ${this.host._renderCardPreview(t.cardFontScale,1,1)} +
+ {this.host._spaceDialog={...t,roomColor:s.detail.color,roomOpacity:s.detail.opacity}}}> +
+ + + ${"static"===(t.bgMode??u(this.host._settings,{}))?p` +
+ {this.host._spaceDialog={...t,bgColor:s.detail.color}}}> + ${t.bgColor?p``:p`${this.host._t("space.bg_inherited")}`} +
`:_} + +
+ {const e=s.target.value.trim(),o=""===e?null:Math.round(Number(e));this.host._spaceDialog={...t,northDeg:null!==o&&Number.isFinite(o)?Math.min(359,Math.max(0,o)):null}}} /> + ${null===t.northDeg?this.host._t("space.north_inherited",{v:null===a(this.host._settings,{})?"—":`${a(this.host._settings,{})}°`}):"°"} +
+ + + + ${m.map(t=>[t,`fill.${t}`]).map(([s,o])=>p` + ${"custom"===s&&"custom"===t.fillMode?p` +
+ ${this.host._t("space.custom_fill")} + {this.host._spaceDialog={...t,customFill:{c:s.detail.color,a:s.detail.opacity}}}}> + ${t.customFill?p``:_} +
`:_}`)} + + ${t.deleteBlockers?p``:_} +
+ +
`}}export{C as HouseplanOnboardingRuntime,S as ONBOARDING_RUNTIME_FINGERPRINT}; diff --git a/dist/houseplan-assets/space-deletion-CJ8Co_o8.js b/dist/houseplan-assets/space-deletion-CJ8Co_o8.js new file mode 100644 index 00000000..d474a3b9 --- /dev/null +++ b/dist/houseplan-assets/space-deletion-CJ8Co_o8.js @@ -0,0 +1 @@ +function e(e,o){return{id:e,title:o,plan_url:null,view_box:[0,0,1,1],rooms:[],wall_segments:[]}}function o(e="file"){const o="draw"===e;return{source:e,showBorders:o,showNames:o,displayTouched:!1}}function r(e,o){if(e.displayTouched)return{...e,source:o};const r="draw"===o;return{...e,source:o,showBorders:r,showNames:r}}function s(e,o,r){return{...e,[o]:r,displayTouched:!0}}function t(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),t=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),n=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&t.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:n,count:n.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="f111431804c4c42c6e35286f29720d089bc8b77ffc364a6517f7a3510fcb11c8";export{t as a,e as c,o as i,r as s,s as t}; diff --git a/dist/houseplan-card.js b/dist/houseplan-card.js index c03e74c3..90b0b423 100755 --- a/dist/houseplan-card.js +++ b/dist/houseplan-card.js @@ -1,7763 +1 @@ -globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="130005653425b19cc41a05c9a1f2a786b6c7fb1f6ed052e2e552de8fd5dfc524";const e=globalThis,t=e.ShadowRoot&&(void 0===e.ShadyCSS||e.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,i=Symbol(),s=new WeakMap;let n=class{constructor(e,t,s){if(this._$cssResult$=!0,s!==i)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=t}get styleSheet(){let e=this.o;const i=this.t;if(t&&void 0===e){const t=void 0!==i&&1===i.length;t&&(e=s.get(i)),void 0===e&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),t&&s.set(i,e))}return e}toString(){return this.cssText}};const o=(e,...t)=>{const s=1===e.length?e[0]:t.reduce((t,i,s)=>t+(e=>{if(!0===e._$cssResult$)return e.cssText;if("number"==typeof e)return e;throw Error("Value passed to 'css' function must be a 'css' function result: "+e+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+e[s+1],e[0]);return new n(s,e,i)},a=t?e=>e:e=>e instanceof CSSStyleSheet?(e=>{let t="";for(const i of e.cssRules)t+=i.cssText;return(e=>new n("string"==typeof e?e:e+"",void 0,i))(t)})(e):e,{is:r,defineProperty:l,getOwnPropertyDescriptor:c,getOwnPropertyNames:h,getOwnPropertySymbols:d,getPrototypeOf:p}=Object,u=globalThis,_=u.trustedTypes,m=_?_.emptyScript:"",g=u.reactiveElementPolyfillSupport,f=(e,t)=>e,v={toAttribute(e,t){switch(t){case Boolean:e=e?m:null;break;case Object:case Array:e=null==e?e:JSON.stringify(e)}return e},fromAttribute(e,t){let i=e;switch(t){case Boolean:i=null!==e;break;case Number:i=null===e?null:Number(e);break;case Object:case Array:try{i=JSON.parse(e)}catch(e){i=null}}return i}},b=(e,t)=>!r(e,t),y={attribute:!0,type:String,converter:v,reflect:!1,useDefault:!1,hasChanged:b};Symbol.metadata??=Symbol("metadata"),u.litPropertyMetadata??=new WeakMap;let w=class extends HTMLElement{static addInitializer(e){this._$Ei(),(this.l??=[]).push(e)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(e,t=y){if(t.state&&(t.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(e)&&((t=Object.create(t)).wrapped=!0),this.elementProperties.set(e,t),!t.noAccessor){const i=Symbol(),s=this.getPropertyDescriptor(e,i,t);void 0!==s&&l(this.prototype,e,s)}}static getPropertyDescriptor(e,t,i){const{get:s,set:n}=c(this.prototype,e)??{get(){return this[t]},set(e){this[t]=e}};return{get:s,set(t){const o=s?.call(this);n?.call(this,t),this.requestUpdate(e,o,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this.elementProperties.get(e)??y}static _$Ei(){if(this.hasOwnProperty(f("elementProperties")))return;const e=p(this);e.finalize(),void 0!==e.l&&(this.l=[...e.l]),this.elementProperties=new Map(e.elementProperties)}static finalize(){if(this.hasOwnProperty(f("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(f("properties"))){const e=this.properties,t=[...h(e),...d(e)];for(const i of t)this.createProperty(i,e[i])}const e=this[Symbol.metadata];if(null!==e){const t=litPropertyMetadata.get(e);if(void 0!==t)for(const[e,i]of t)this.elementProperties.set(e,i)}this._$Eh=new Map;for(const[e,t]of this.elementProperties){const i=this._$Eu(e,t);void 0!==i&&this._$Eh.set(i,e)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(e){const t=[];if(Array.isArray(e)){const i=new Set(e.flat(1/0).reverse());for(const e of i)t.unshift(a(e))}else void 0!==e&&t.push(a(e));return t}static _$Eu(e,t){const i=t.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof e?e.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(e=>this.enableUpdating=e),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(e=>e(this))}addController(e){(this._$EO??=new Set).add(e),void 0!==this.renderRoot&&this.isConnected&&e.hostConnected?.()}removeController(e){this._$EO?.delete(e)}_$E_(){const e=new Map,t=this.constructor.elementProperties;for(const i of t.keys())this.hasOwnProperty(i)&&(e.set(i,this[i]),delete this[i]);e.size>0&&(this._$Ep=e)}createRenderRoot(){const i=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return((i,s)=>{if(t)i.adoptedStyleSheets=s.map(e=>e instanceof CSSStyleSheet?e:e.styleSheet);else for(const t of s){const s=document.createElement("style"),n=e.litNonce;void 0!==n&&s.setAttribute("nonce",n),s.textContent=t.cssText,i.appendChild(s)}})(i,this.constructor.elementStyles),i}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(e=>e.hostConnected?.())}enableUpdating(e){}disconnectedCallback(){this._$EO?.forEach(e=>e.hostDisconnected?.())}attributeChangedCallback(e,t,i){this._$AK(e,i)}_$ET(e,t){const i=this.constructor.elementProperties.get(e),s=this.constructor._$Eu(e,i);if(void 0!==s&&!0===i.reflect){const n=(void 0!==i.converter?.toAttribute?i.converter:v).toAttribute(t,i.type);this._$Em=e,null==n?this.removeAttribute(s):this.setAttribute(s,n),this._$Em=null}}_$AK(e,t){const i=this.constructor,s=i._$Eh.get(e);if(void 0!==s&&this._$Em!==s){const e=i.getPropertyOptions(s),n="function"==typeof e.converter?{fromAttribute:e.converter}:void 0!==e.converter?.fromAttribute?e.converter:v;this._$Em=s;const o=n.fromAttribute(t,e.type);this[s]=o??this._$Ej?.get(s)??o,this._$Em=null}}requestUpdate(e,t,i,s=!1,n){if(void 0!==e){const o=this.constructor;if(!1===s&&(n=this[e]),i??=o.getPropertyOptions(e),!((i.hasChanged??b)(n,t)||i.useDefault&&i.reflect&&n===this._$Ej?.get(e)&&!this.hasAttribute(o._$Eu(e,i))))return;this.C(e,t,i)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}C(e,t,{useDefault:i,reflect:s,wrapped:n},o){i&&!(this._$Ej??=new Map).has(e)&&(this._$Ej.set(e,o??t??this[e]),!0!==n||void 0!==o)||(this._$AL.has(e)||(this.hasUpdated||i||(t=void 0),this._$AL.set(e,t)),!0===s&&this._$Em!==e&&(this._$Eq??=new Set).add(e))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(e){Promise.reject(e)}const e=this.scheduleUpdate();return null!=e&&await e,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[e,t]of this._$Ep)this[e]=t;this._$Ep=void 0}const e=this.constructor.elementProperties;if(e.size>0)for(const[t,i]of e){const{wrapped:e}=i,s=this[t];!0!==e||this._$AL.has(t)||void 0===s||this.C(t,void 0,i,s)}}let e=!1;const t=this._$AL;try{e=this.shouldUpdate(t),e?(this.willUpdate(t),this._$EO?.forEach(e=>e.hostUpdate?.()),this.update(t)):this._$EM()}catch(t){throw e=!1,this._$EM(),t}e&&this._$AE(t)}willUpdate(e){}_$AE(e){this._$EO?.forEach(e=>e.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(e)),this.updated(e)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(e){return!0}update(e){this._$Eq&&=this._$Eq.forEach(e=>this._$ET(e,this[e])),this._$EM()}updated(e){}firstUpdated(e){}};w.elementStyles=[],w.shadowRootOptions={mode:"open"},w[f("elementProperties")]=new Map,w[f("finalized")]=new Map,g?.({ReactiveElement:w}),(u.reactiveElementVersions??=[]).push("2.1.2");const k=globalThis,x=e=>e,$=k.trustedTypes,S=$?$.createPolicy("lit-html",{createHTML:e=>e}):void 0,M="$lit$",C=`lit$${Math.random().toFixed(9).slice(2)}$`,D="?"+C,P=`<${D}>`,T=document,I=()=>T.createComment(""),R=e=>null===e||"object"!=typeof e&&"function"!=typeof e,A=Array.isArray,z="[ \t\n\f\r]",F=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,N=/-->/g,E=/>/g,O=RegExp(`>|${z}(?:([^\\s"'>=/]+)(${z}*=${z}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),H=/'/g,L=/"/g,B=/^(?:script|style|textarea|title)$/i,j=e=>(t,...i)=>({_$litType$:e,strings:t,values:i}),q=j(1),W=j(2),U=Symbol.for("lit-noChange"),G=Symbol.for("lit-nothing"),V=new WeakMap,K=T.createTreeWalker(T,129);function Y(e,t){if(!A(e)||!e.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==S?S.createHTML(t):t}const J=(e,t)=>{const i=e.length-1,s=[];let n,o=2===t?"":3===t?"":"",a=F;for(let t=0;t"===l[0]?(a=n??F,c=-1):void 0===l[1]?c=-2:(c=a.lastIndex-l[2].length,r=l[1],a=void 0===l[3]?O:'"'===l[3]?L:H):a===L||a===H?a=O:a===N||a===E?a=F:(a=O,n=void 0);const d=a===O&&e[t+1].startsWith("/>")?" ":"";o+=a===F?i+P:c>=0?(s.push(r),i.slice(0,c)+M+i.slice(c)+C+d):i+C+(-2===c?t:d)}return[Y(e,o+(e[i]||"")+(2===t?"":3===t?"":"")),s]};class X{constructor({strings:e,_$litType$:t},i){let s;this.parts=[];let n=0,o=0;const a=e.length-1,r=this.parts,[l,c]=J(e,t);if(this.el=X.createElement(l,i),K.currentNode=this.el.content,2===t||3===t){const e=this.el.content.firstChild;e.replaceWith(...e.childNodes)}for(;null!==(s=K.nextNode())&&r.length0){s.textContent=$?$.emptyScript:"";for(let i=0;iA(e)||"function"==typeof e?.[Symbol.iterator])(e)?this.k(e):this._(e)}O(e){return this._$AA.parentNode.insertBefore(e,this._$AB)}T(e){this._$AH!==e&&(this._$AR(),this._$AH=this.O(e))}_(e){this._$AH!==G&&R(this._$AH)?this._$AA.nextSibling.data=e:this.T(T.createTextNode(e)),this._$AH=e}$(e){const{values:t,_$litType$:i}=e,s="number"==typeof i?this._$AC(e):(void 0===i.el&&(i.el=X.createElement(Y(i.h,i.h[0]),this.options)),i);if(this._$AH?._$AD===s)this._$AH.p(t);else{const e=new Q(s,this),i=e.u(this.options);e.p(t),this.T(i),this._$AH=e}}_$AC(e){let t=V.get(e.strings);return void 0===t&&V.set(e.strings,t=new X(e)),t}k(e){A(this._$AH)||(this._$AH=[],this._$AR());const t=this._$AH;let i,s=0;for(const n of e)s===t.length?t.push(i=new ee(this.O(I()),this.O(I()),this,this.options)):i=t[s],i._$AI(n),s++;s2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=G}_$AI(e,t=this,i,s){const n=this.strings;let o=!1;if(void 0===n)e=Z(this,e,t,0),o=!R(e)||e!==this._$AH&&e!==U,o&&(this._$AH=e);else{const s=e;let a,r;for(e=n[0],a=0;a{const s=i?.renderBefore??t;let n=s._$litPart$;if(void 0===n){const e=i?.renderBefore??null;s._$litPart$=n=new ee(t.insertBefore(I(),e),e,void 0,i??{})}return n._$AI(e),n},ce=globalThis;let he=class extends w{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){const e=super.createRenderRoot();return this.renderOptions.renderBefore??=e.firstChild,e}update(e){const t=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(e),this._$Do=le(t,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return U}};he._$litElement$=!0,he.finalized=!0,ce.litElementHydrateSupport?.({LitElement:he});const de=ce.litElementPolyfillSupport;de?.({LitElement:he}),(ce.litElementVersions??=[]).push("4.2.2");const pe=2,ue=e=>(...t)=>({_$litDirective$:e,values:t});let _e=class{constructor(e){}get _$AU(){return this._$AM._$AU}_$AT(e,t,i){this._$Ct=e,this._$AM=t,this._$Ci=i}_$AS(e,t){return this.update(e,t)}update(e,t){return this.render(...t)}};const me={},ge=ue(class extends _e{constructor(){super(...arguments),this.ot=me}render(e,t){return t()}update(e,[t,i]){if(Array.isArray(t)){if(Array.isArray(this.ot)&&this.ot.length===t.length&&t.every((e,t)=>e===this.ot[t]))return U}else if(this.ot===t)return U;return this.ot=Array.isArray(t)?Array.from(t):t,this.render(t,i)}}),{I:fe}=ae,ve=e=>e,be=()=>document.createComment(""),ye=(e,t,i)=>{const s=e._$AA.parentNode,n=void 0===t?e._$AB:t._$AA;if(void 0===i){const t=s.insertBefore(be(),n),o=s.insertBefore(be(),n);i=new fe(t,o,e,e.options)}else{const t=i._$AB.nextSibling,o=i._$AM,a=o!==e;if(a){let t;i._$AQ?.(e),i._$AM=e,void 0!==i._$AP&&(t=e._$AU)!==o._$AU&&i._$AP(t)}if(t!==n||a){let e=i._$AA;for(;e!==t;){const t=ve(e).nextSibling;ve(s).insertBefore(e,n),e=t}}}return i},we=(e,t,i=e)=>(e._$AI(t,i),e),ke={},xe=(e,t=ke)=>e._$AH=t,$e=e=>{e._$AR(),e._$AA.remove()},Se=(e,t,i)=>{const s=new Map;for(let n=t;n<=i;n++)s.set(e[n],n);return s},Me=ue(class extends _e{constructor(e){if(super(e),e.type!==pe)throw Error("repeat() can only be used in text expressions")}dt(e,t,i){let s;void 0===i?i=t:void 0!==t&&(s=t);const n=[],o=[];let a=0;for(const t of e)n[a]=s?s(t,a):a,o[a]=i(t,a),a++;return{values:o,keys:n}}render(e,t,i){return this.dt(e,t,i).values}update(e,[t,i,s]){const n=(e=>e._$AH)(e),{values:o,keys:a}=this.dt(t,i,s);if(!Array.isArray(n))return this.ut=a,o;const r=this.ut??=[],l=[];let c,h,d=0,p=n.length-1,u=0,_=o.length-1;for(;d<=p&&u<=_;)if(null===n[d])d++;else if(null===n[p])p--;else if(r[d]===a[u])l[u]=we(n[d],o[u]),d++,u++;else if(r[p]===a[_])l[_]=we(n[p],o[_]),p--,_--;else if(r[d]===a[_])l[_]=we(n[d],o[_]),ye(e,l[_+1],n[d]),d++,_--;else if(r[p]===a[u])l[u]=we(n[p],o[u]),ye(e,n[d],n[p]),p--,u++;else if(void 0===c&&(c=Se(a,u,_),h=Se(r,d,p)),c.has(r[d]))if(c.has(r[p])){const t=h.get(a[u]),i=void 0!==t?n[t]:null;if(null===i){const t=ye(e,n[d]);we(t,o[u]),l[u]=t}else l[u]=we(i,o[u]),ye(e,n[d],i),n[t]=null;u++}else $e(n[p]),p--;else $e(n[d]),d++;for(;u<=_;){const t=ye(e,l[_+1]);we(t,o[u]),l[u++]=t}for(;d<=p;){const e=n[d++];null!==e&&$e(e)}return this.ut=a,xe(e,l),U}}),Ce=new WeakMap;let De=0;class Pe extends he{constructor(){super(...arguments),this.title="",this.icon="",this.wide=!1,this.dismissOnScrim=!1,this.hass=null,this._opener=null,this._focusRoot=null,this._useHaDialog=!1,this._closing=!1,this._overlays=[],this._titleId="hp-dialog-title-"+ ++De,this._focusInitial=()=>{const e=this._focusableElements(),t=e.find(e=>e.hasAttribute("autofocus"))||e[0]||(this._useHaDialog?null:this.renderRoot.querySelector(".close"))||this.renderRoot.querySelector(".surface")||this.renderRoot.querySelector("ha-dialog");t?.focus({preventScroll:!0})},this._requestClose=()=>{this._closing||(this._closing=!0,this.dispatchEvent(new CustomEvent("hp-close",{bubbles:!0,composed:!0})))},this._onKeyDown=e=>{if("Escape"===e.key){e.preventDefault(),e.stopImmediatePropagation(),this._pruneOverlays();const t=this._overlays[this._overlays.length-1];return t?void this._closeOverlay(t,"escape"):void this._requestClose()}if("Tab"!==e.key||this._useHaDialog)return;const t=this.renderRoot.querySelector(".close"),i=t?[t,...this._focusableElements()]:this._focusableElements();if(!i.length)return e.preventDefault(),void this.renderRoot.querySelector(".surface")?.focus({preventScroll:!0});const s=this._deepActiveElement(),n=i[0],o=i[i.length-1];!e.shiftKey||s!==n&&i.includes(s)?e.shiftKey||s!==o||(e.preventDefault(),n.focus()):(e.preventDefault(),o.focus())},this._onFallbackCancel=e=>{e.preventDefault(),this._requestClose()},this._onFallbackClick=e=>{this.dismissOnScrim&&e.target===e.currentTarget&&this._requestClose()}}connectedCallback(){super.connectedCallback(),this._opener=this._deepActiveElement();const e=this.getRootNode();this._focusRoot=e;const t=Ce.get(e)||{dialogs:new Set,opener:this._opener};t.dialogs.add(this),Ce.set(e,t),this._useHaDialog=!!customElements.get("ha-dialog"),this.addEventListener("keydown",this._onKeyDown,!0)}disconnectedCallback(){this.removeEventListener("keydown",this._onKeyDown,!0);const e=[...this._overlays];this._overlays=[];for(const t of e.reverse())t.close("disconnect");const t=this._focusRoot,i=this._opener;this._opener=null,this._focusRoot=null;const s=t?Ce.get(t):void 0;s?.dialogs.delete(this),super.disconnectedCallback(),t&&s&&requestAnimationFrame(()=>{const e=Ce.get(t);if(!e)return;if(!e.dialogs.size)return e.opener?.isConnected&&e.opener.focus({preventScroll:!0}),void Ce.delete(t);const s=i?.closest("hp-dialog");i?.isConnected&&s&&e.dialogs.has(s)&&i.focus({preventScroll:!0})})}firstUpdated(e){if(super.firstUpdated(e),!this._useHaDialog){const e=this.renderRoot.querySelector("dialog");e&&!e.open&&e.showModal()}queueMicrotask(()=>this._focusInitial())}_deepActiveElement(){let e=document.activeElement;for(;e?.shadowRoot?.activeElement;)e=e.shadowRoot.activeElement;return e&&e!==document.body?e:null}_focusableElements(){const e=["[autofocus]","a[href]","button:not([disabled])",'input:not([disabled]):not([type="hidden"])',"select:not([disabled])","textarea:not([disabled])",'[contenteditable="true"]','[tabindex]:not([tabindex="-1"])'].join(","),t=[],i=new Set,s=n=>{if(!i.has(n)){if(i.add(n),n instanceof HTMLElement){if(n.matches(e)&&t.push(n),n instanceof HTMLSlotElement){for(const e of n.assignedNodes({flatten:!0}))s(e);return}if(n.shadowRoot){for(const e of n.shadowRoot.childNodes)s(e);return}}for(const e of n.childNodes)s(e)}};for(const e of this.childNodes)s(e);const n=this.overlayPortal();return n&&s(n),t.filter(e=>{let t=e;for(;t;){const e=getComputedStyle(t);if(t.hidden||t.inert||"true"===t.getAttribute("aria-hidden")||"none"===e.display||"hidden"===e.visibility)return!1;if(t=t.assignedSlot||t.parentElement||(t.getRootNode()instanceof ShadowRoot?t.getRootNode().host:null),t===this)break}return!0})}_pruneOverlays(){this._overlays=this._overlays.filter(e=>e.owner.isConnected)}_closeOverlay(e,t){const i=this._overlays.findIndex(t=>t.token===e.token);i>=0&&this._overlays.splice(i,1),e.close(t)}registerOverlay(e){this._pruneOverlays();const t=this._overlays.find(t=>t.owner===e.owner);t&&this._overlays.splice(this._overlays.indexOf(t),1);const i=e.group||"transient";for(const e of[...this._overlays].reverse())e.group===i&&this._closeOverlay(e,"exclusive");const s={...e,group:i,token:Symbol("hp-overlay")};this._overlays.push(s);let n=!1;return()=>{if(n)return;n=!0;const e=this._overlays.findIndex(e=>e.token===s.token);e>=0&&this._overlays.splice(e,1)}}closeTransientOverlays(e="outside"){this._pruneOverlays();const t=[...this._overlays].filter(e=>"transient"===(e.group||"transient"));for(const i of t.reverse())this._closeOverlay(i,e);return t.length>0}overlayPortal(){return this.renderRoot.querySelector(".overlay-portal")}render(){const e=q` - ${this.icon?q``:G} - ${this.title} - `;return this._useHaDialog?q` - ${e} - - -
`:q` -
-
- ${e} - -
-
- -
-
-
`}}Pe.properties={title:{type:String},icon:{type:String},wide:{type:Boolean,reflect:!0},dismissOnScrim:{type:Boolean,attribute:"dismiss-on-scrim"},hass:{attribute:!1}},Pe.styles=o` - :host { - display: contents; - color: var(--primary-text-color, #e6e7eb); - font: inherit; - } - - ha-dialog { - --dialog-content-padding: 0; - --dialog-surface-background: var(--card-background-color, var(--hp-bg, #202126)); - --ha-dialog-border-radius: var(--rad-l, 18px); - /* HA's ha-dialog-header defaults this custom property to a one-line - fixed height. Our localized slot is intentionally allowed to wrap, - so leaving that default in place clips every line after the first at - the bottom of the header. auto is HA's public sizing hook and also - stays harmless on older ha-dialog implementations that do not consume - it. */ - --ha-dialog-header-title-height: auto; - color: inherit; - } - - ha-dialog::part(dialog) { - border: 1px solid var(--hp-accent, #d89300); - box-shadow: var(--shadow-3, 0 18px 48px rgb(0 0 0 / 0.34)); - overflow: hidden; - } - - /* The HA header slot is a flex item with a constrained inline size. The - old inline-flex title kept its min-content width, so HA clipped the last - word instead of giving it a second line. Keep every wrapper shrinkable - and let the text wrap; this applies to every hp-dialog, including long - device names and translated titles. */ - .header-title-slot { - display: block; - flex: 1 1 auto; - width: 100%; - max-width: 100%; - min-width: 0; - box-sizing: border-box; - white-space: normal; - } - - .title { - display: flex; - flex: 1 1 auto; - align-items: center; - gap: var(--sp-4, 12px); - width: 100%; - max-width: 100%; - min-width: 0; - font-weight: 600; - line-height: 1.25; - white-space: normal; - } - - .title ha-icon { - flex: none; - color: var(--hp-accent, #d89300); - } - - .title-text { - flex: 1 1 auto; - min-width: 0; - max-width: 100%; - white-space: normal; - overflow-wrap: anywhere; - word-break: normal; - } - - .footer { - /* ha-dialog lays out its footer slot as a flex row. display: contents - exposed the consumer's action row as a shrink-to-fit flex item, so a - wide device dialog got a half-width divider and its Hide action slid - toward the centre. Keep one full-width slot item in both HA and the - native fallback. */ - display: block; - width: 100%; - min-width: 0; - box-sizing: border-box; - } - - ::slotted([slot='footer']) { - width: 100%; - box-sizing: border-box; - } - - dialog { - width: auto; - max-width: none; - max-height: none; - margin: auto; - padding: 0; - border: 0; - overflow: visible; - color: inherit; - background: transparent; - } - - dialog::backdrop { - background: rgb(0 0 0 / 0.45); - } - - .surface { - width: min(360px, 92vw); - max-height: 92vh; - display: flex; - flex-direction: column; - overflow: hidden; - box-sizing: border-box; - color: inherit; - background: var(--card-background-color, var(--hp-bg, #202126)); - border: 1px solid var(--hp-accent, #d89300); - border-radius: var(--rad-l, 18px); - box-shadow: var(--shadow-3, 0 18px 48px rgb(0 0 0 / 0.34)); - } - - :host([wide]) .surface { - width: min(var(--hp-dialog-wide-width, 500px), 94vw); - } - - .header { - min-height: 56px; - display: flex; - align-items: center; - gap: var(--sp-4, 12px); - padding: var(--sp-4, 12px) var(--sp-5, 16px); - box-sizing: border-box; - border-bottom: 1px solid var(--hp-line, rgb(255 255 255 / 0.12)); - } - - .header .title { - flex: 1; - } - - .close { - width: 40px; - height: 40px; - flex: none; - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0; - color: inherit; - background: transparent; - border: 0; - border-radius: 50%; - cursor: pointer; - } - - :host([data-pointer-hover]) .close:hover, - .close:focus-visible { - background: rgb(127 127 127 / 0.16); - } - - .content { - min-height: 0; - display: flex; - flex-direction: column; - } - - .overlay-portal { - position: fixed; - z-index: 2147483647; - inset: 0; - overflow: visible; - pointer-events: none; - } - - .overlay-portal:empty { - display: none; - } - - .overlay-portal > * { - pointer-events: auto; - } - `,customElements.get("hp-dialog")||customElements.define("hp-dialog",Pe);const Te=/^#[0-9a-fA-F]{6}$/;function Ie(e,t){return"string"==typeof e&&Te.test(e)?e:t}const Re="(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])",Ae=new RegExp(`^rgb\\(${Re}, ${Re}, ${Re}\\)$`);function ze(e){if(!Array.isArray(e)||e.length<3)return null;const t=e.slice(0,3);if(!t.every(e=>"number"==typeof e&&Number.isFinite(e)))return null;const[i,s,n]=t.map(e=>Math.round(Math.min(255,Math.max(0,e))));return`rgb(${i}, ${s}, ${n})`}const Fe=(e,t,i)=>Math.min(i,Math.max(t,Number.isFinite(e)?e:t));function Ne(e){return Number.isFinite(e)?(e%360+360)%360:0}function Ee(e){if("string"!=typeof e)return null;const t=e.trim().replace(/^#/,""),i=/^[0-9a-fA-F]{3}$/.test(t)?t.split("").map(e=>`${e}${e}`).join(""):t;return/^[0-9a-fA-F]{6}$/.test(i)?`#${i.toLowerCase()}`:null}function Oe(e){const t=Ee(e);return t?{r:Number.parseInt(t.slice(1,3),16),g:Number.parseInt(t.slice(3,5),16),b:Number.parseInt(t.slice(5,7),16)}:null}function He(e){const t=Fe(e.r,0,255)/255,i=Fe(e.g,0,255)/255,s=Fe(e.b,0,255)/255,n=Math.max(t,i,s),o=n-Math.min(t,i,s);let a=0;return o>0&&(a=n===t?(i-s)/o%6*60:n===i?60*((s-t)/o+2):60*((t-i)/o+4)),{h:Ne(a),s:0===n?0:o/n*100,v:100*n}}function Le(e){return function(e){const t=e=>Math.round(Fe(e,0,255)).toString(16).padStart(2,"0");return`#${t(e.r)}${t(e.g)}${t(e.b)}`}(function(e){const t=Ne(e.h),i=Fe(e.s,0,100)/100,s=Fe(e.v,0,100)/100,n=s*i,o=n*(1-Math.abs(t/60%2-1)),a=s-n;let r;return r=t<60?[n,o,0]:t<120?[o,n,0]:t<180?[0,n,o]:t<240?[0,o,n]:t<300?[o,0,n]:[n,0,o],{r:Math.round(255*(r[0]+a)),g:Math.round(255*(r[1]+a)),b:Math.round(255*(r[2]+a))}}(e))}function Be(e){const t=e.visualViewport;return t&&Number.isFinite(t.width)&&Number.isFinite(t.height)?{left:Number(t.offsetLeft)||0,top:Number(t.offsetTop)||0,width:Math.max(0,Number(t.width)||0),height:Math.max(0,Number(t.height)||0)}:{left:0,top:0,width:e.innerWidth,height:e.innerHeight}}function je(e,t,i,s=7,n=8){const o=i.left+n,a=i.top+n,r=Math.max(o,i.left+i.width-n),l=Math.max(a,i.top+i.height-n),c=Math.max(0,r-o),h=Math.max(0,l-a),d=Math.min(Math.max(0,t.width),c),p=Math.min(Math.max(0,t.height),h);let u=e.left;u+d>r&&(u=e.right-d),u=Math.min(Math.max(o,u),Math.max(o,r-d));const _=e.bottom+s,m=e.top-s-p;let g="bottom",f=_;return _+p>l&&m>=a?(g="top",f=Math.min(Math.max(a,m),Math.max(a,l-p))):f=Math.min(Math.max(a,_),Math.max(a,l-p)),{left:Math.round(u),top:Math.round(f),side:g,maxWidth:Math.round(c),maxHeight:Math.round(h)}}class qe{constructor(e,t,i){this.owner=e,this.name=t,this.keydown=i,this.host=null,this.root=null}window(){return this.owner.ownerDocument.defaultView}dialog(){return this.owner.closest("hp-dialog")}usesPopover(e=!1){const t=this.window();return!e&&!!t&&function(e){const t=e.HTMLElement?.prototype;return"function"==typeof t?.showPopover&&"function"==typeof t?.hidePopover}(t)}get hasFallback(){return!!this.host}containsPath(e){return e.includes(this.owner)||!!this.host&&e.includes(this.host)}ownsActiveElement(){let e=this.owner.ownerDocument.activeElement;for(;e?.shadowRoot?.activeElement;)e=e.shadowRoot.activeElement;return!(!e||e!==this.owner&&!this.owner.shadowRoot?.contains(e)&&!this.root?.contains(e))}renderFallback(e,t){const i=this.ensureFallback();return i?(le(q`${e}`,i),i):null}surface(e,t){return t?this.owner.shadowRoot?.querySelector(e)||null:this.root?.querySelector(e)||null}destroy(){this.root&&le(G,this.root),this.host?.remove(),this.host=null,this.root=null}ensureFallback(){if(this.root?.isConnected)return this.root;const e=this.dialog()?.overlayPortal()||this.owner.ownerDocument.body;if(!e)return null;const t=this.owner.ownerDocument.createElement("div");return t.dataset.hpOverlay=this.name,t.style.display="contents",this.keydown&&t.addEventListener("keydown",this.keydown,!0),e.append(t),this.host=t,this.root=t.attachShadow({mode:"open"}),this.root}}const We={title:"Color picker",hue:"Hue",saturation:"Saturation",value:"Brightness",hex:"Hex color",invalidHex:"Enter a 3- or 6-digit hex color"};class Ue extends he{constructor(){super(...arguments),this.label="",this.opacityLabel="Opacity",this.color="#607d8b",this.opacity=1,this.disabled=!1,this.showOpacity=!0,this.pickerLabels=We,this._open=!1,this._pickerRaf=0,this._emitRaf=0,this._forceFallback=!1,this._overlayDispose=null,this._hue=0,this._saturation=0,this._value=0,this._hexDraft="#607d8b",this._hexInvalid=!1,this._lastValidColor="#607d8b",this._activePointerId=null,this._outsidePointerDown=e=>{if(!this._open)return;const t=e.composedPath();this._floating.containsPath(t)||this._closePicker(!1,"outside")},this._keyDown=e=>{this._open&&"Escape"===e.key&&(e.preventDefault(),e.stopImmediatePropagation(),this._closePicker(this._floating.ownsActiveElement(),"escape"))},this._floating=new qe(this,"color-opacity",this._keyDown),this._queuePickerPosition=()=>{if(!this._open)return;this._pickerRaf&&cancelAnimationFrame(this._pickerRaf);const e=this._window();e&&(this._pickerRaf=e.requestAnimationFrame(()=>{this._pickerRaf=0,this._positionPicker()||this._closePicker()}))}}connectedCallback(){super.connectedCallback(),this._syncFromColor(this.color,!1),this.ownerDocument.addEventListener("pointerdown",this._outsidePointerDown,!0),this.ownerDocument.addEventListener("scroll",this._queuePickerPosition,!0);const e=this.ownerDocument.defaultView;e?.addEventListener("resize",this._queuePickerPosition),e?.addEventListener("orientationchange",this._queuePickerPosition),e?.visualViewport?.addEventListener("resize",this._queuePickerPosition),e?.visualViewport?.addEventListener("scroll",this._queuePickerPosition),this.addEventListener("keydown",this._keyDown,!0)}disconnectedCallback(){this.ownerDocument.removeEventListener("pointerdown",this._outsidePointerDown,!0),this.ownerDocument.removeEventListener("scroll",this._queuePickerPosition,!0);const e=this.ownerDocument.defaultView;e?.removeEventListener("resize",this._queuePickerPosition),e?.removeEventListener("orientationchange",this._queuePickerPosition),e?.visualViewport?.removeEventListener("resize",this._queuePickerPosition),e?.visualViewport?.removeEventListener("scroll",this._queuePickerPosition),this.removeEventListener("keydown",this._keyDown,!0),this._pickerRaf&&cancelAnimationFrame(this._pickerRaf),this._emitRaf&&cancelAnimationFrame(this._emitRaf),this._pickerRaf=0,this._emitRaf=0,this._closePicker(!1,"disconnect"),super.disconnectedCallback()}updated(e){if(e.has("color")){const e=Ee(this.color);e&&e!==this._lastValidColor&&this._syncFromColor(e,this._open)}this.disabled&&this._open?this._closePicker():this._open&&(this._supportsPopover()||this._renderFallback(),(e.has("color")||e.has("opacity")||e.has("showOpacity")||e.has("label")||e.has("opacityLabel")||e.has("pickerLabels"))&&this._queuePickerPosition())}_window(){return this._floating.window()}_supportsPopover(){return this._floating.usesPopover(this._forceFallback)}_dialog(){return this._floating.dialog()}async _toggle(){this.disabled||(this._open?this._closePicker():(this._forceFallback=!1,this._syncFromColor(this.color,!1),this._hexInvalid=!1,this._activePointerId=null,this._open=!0,await this.updateComplete,this._open&&(this._supportsPopover()||this._renderFallback(),this._positionPicker()?this._overlayDispose=this._dialog()?.registerOverlay({owner:this,group:"transient",close:e=>this._closePicker("escape"===e&&this._floating.ownsActiveElement(),e)})||null:this._closePicker())))}_closePicker(e=!1,t="exclusive"){if(!this._open&&!this._floating.hasFallback)return;const i=this._overlayDispose;this._overlayDispose=null,i?.();const s=this.renderRoot.querySelector(".picker");if(s?.hidePopover)try{s.matches(":popover-open")&&s.hidePopover()}catch{}this._open=!1,this._cancelQueuedEmit(),this._activePointerId=null,this._floating.destroy(),e&&this.updateComplete.then(()=>this.renderRoot.querySelector(".trigger")?.focus())}_surface(){return this._floating.surface(".picker",this._supportsPopover())}_renderFallback(){const e=Ue.styles.cssText;this._floating.renderFallback(this._pickerTemplate(!1),e)}_positionPicker(){if(!this._open)return!1;const e=this._window(),t=this.renderRoot.querySelector(".trigger");let i=this._surface();if(!e||!t?.isConnected||!i?.isConnected)return!1;const s=Be(e);if(i.style.maxWidth=`${Math.max(0,s.width-16)}px`,i.style.maxHeight=`${Math.max(0,s.height-16)}px`,i.style.visibility="hidden",this._supportsPopover()&&i.showPopover)try{i.matches(":popover-open")||i.showPopover()}catch{if(this._forceFallback=!0,this._renderFallback(),i=this._surface(),!i?.isConnected)return!1;i.style.maxWidth=`${Math.max(0,s.width-16)}px`,i.style.maxHeight=`${Math.max(0,s.height-16)}px`,i.style.visibility="hidden"}const n=t.getBoundingClientRect(),o=i.getBoundingClientRect();if(!(n.width&&n.height&&o.width&&o.height))return!1;const a=je(n,o,s);return i.style.left=`${a.left}px`,i.style.top=`${a.top}px`,i.dataset.side=a.side,i.style.visibility="",!0}_labels(){const e=this.pickerLabels||We;return{title:e.title||We.title,hue:e.hue||We.hue,saturation:e.saturation||We.saturation,value:e.value||We.value,hex:e.hex||We.hex,invalidHex:e.invalidHex||We.invalidHex}}_syncFromColor(e,t){const i=Ee(Ie(e,"#607d8b"))||"#607d8b",s=Oe(i);if(!s)return;const n=He(s);(!t||n.s>1e-4)&&(this._hue=n.h),this._saturation=n.s,this._value=n.v,this._hexDraft=i,this._hexInvalid=!1,this._lastValidColor=i}_cancelQueuedEmit(e=!1){this._emitRaf&&cancelAnimationFrame(this._emitRaf),this._emitRaf=0,e&&this._syncFromColor(this._lastValidColor,!0)}_queueHsvEmit(){if(this._emitRaf)return;const e=this._window();e?this._emitRaf=e.requestAnimationFrame(()=>{this._emitRaf=0,this._emitHsv()}):this._emitHsv()}_emitHsv(){this._emit(Le({h:this._hue,s:this._saturation,v:this._value}),this.opacity)}_setHue(e){this._hue=Math.min(359,Math.max(0,Number.isFinite(e)?e:0)),this._emitHsv()}_setSaturation(e){this._saturation=Math.min(100,Math.max(0,Number.isFinite(e)?e:0)),this._emitHsv()}_setValue(e){this._value=Math.min(100,Math.max(0,Number.isFinite(e)?e:0)),this._emitHsv()}_setOpacity(e){this._emit(this._lastValidColor,e)}_shiftRangeKey(e,t,i,s,n){if(!e.shiftKey)return;const o="ArrowRight"===e.key||"ArrowUp"===e.key?10:"ArrowLeft"===e.key||"ArrowDown"===e.key?-10:0;o&&(e.preventDefault(),e.stopPropagation(),n(Math.min(s,Math.max(i,t+o))))}_setSvFromPointer(e,t){const i=e.currentTarget.getBoundingClientRect();i.width&&i.height&&(this._saturation=Math.min(100,Math.max(0,(e.clientX-i.left)/i.width*100)),this._value=Math.min(100,Math.max(0,100*(1-(e.clientY-i.top)/i.height))),t?(this._cancelQueuedEmit(),this._emitHsv()):this._queueHsvEmit())}_svPointerDown(e){if(e.preventDefault(),e.stopPropagation(),null===this._activePointerId){this._activePointerId=e.pointerId;try{e.currentTarget.setPointerCapture(e.pointerId)}catch{}this._setSvFromPointer(e,!0)}}_svPointerMove(e){e.pointerId===this._activePointerId&&(e.preventDefault(),e.stopPropagation(),this._setSvFromPointer(e,!1))}_svPointerUp(e){if(e.pointerId===this._activePointerId){e.preventDefault(),e.stopPropagation(),this._setSvFromPointer(e,!0),this._activePointerId=null;try{e.currentTarget.releasePointerCapture(e.pointerId)}catch{}}}_svPointerCancel(e){e.pointerId===this._activePointerId&&(e.preventDefault(),e.stopPropagation(),this._activePointerId=null,this._cancelQueuedEmit(!0))}_svLostPointerCapture(e){e.pointerId===this._activePointerId&&(this._activePointerId=null,this._cancelQueuedEmit(!0))}_hexInput(e){const t=e.target.value;this._hexDraft=t,this._hexInvalid=!1;const i=Ee(t),s=i?Oe(i):null;if(!i||!s)return;const n=He(s);n.s>1e-4&&(this._hue=n.h),this._saturation=n.s,this._value=n.v,this._emit(i,this.opacity,!0)}_commitHex(){const e=Ee(this._hexDraft);if(!e)return this._hexDraft=this._lastValidColor,void(this._hexInvalid=!0);this._hexDraft=e,this._hexInvalid=!1,e!==this._lastValidColor&&this._emit(e,this.opacity)}_hexKeyDown(e){"Enter"===e.key&&(e.preventDefault(),e.stopPropagation(),this._commitHex())}_emit(e,t,i=!1){const s=Ee(e)||this._lastValidColor,n=Number.isFinite(t)?t:Number(this.opacity)||0,o=Math.min(1,Math.max(0,n));this.color=s,this.opacity=o,this._lastValidColor=s,i||(this._hexDraft=s),this._hexInvalid=!1,this.dispatchEvent(new CustomEvent("hp-color-opacity-change",{detail:{color:s,opacity:o},bubbles:!0,composed:!0}))}_pickerTemplate(e){const t=Math.round(100*Math.min(1,Math.max(0,Number(this.opacity)||0))),i=Le({h:this._hue,s:this._saturation,v:this._value}),s=Le({h:this._hue,s:100,v:100}),n=this._labels(),o=Math.round(this._hue),a=Math.round(this._saturation),r=Math.round(this._value);return q` - `}render(){const e=Math.round(100*Math.min(1,Math.max(0,Number(this.opacity)||0))),t=Ie(this.color,"#607d8b"),i=this.showOpacity?`${this.label||"Color"}: ${t}, ${e}%`:`${this.label||"Color"}: ${t}`;return q` - ${this.label?q`${this.label}`:G} - - ${this._open&&!this.disabled&&this._supportsPopover()?this._pickerTemplate(!0):G} - `}}Ue.properties={label:{type:String},opacityLabel:{type:String,attribute:"opacity-label"},color:{type:String},opacity:{type:Number},disabled:{type:Boolean,reflect:!0},showOpacity:{type:Boolean,attribute:"show-opacity"},pickerLabels:{attribute:!1},_open:{state:!0},_hue:{state:!0},_saturation:{state:!0},_value:{state:!0},_hexDraft:{state:!0},_hexInvalid:{state:!0}},Ue.styles=o` - :host { - position: relative; - display: inline-flex; - align-items: center; - gap: 6px; - min-width: 0; - } - .label { - font-size: 12px; - color: var(--secondary-text-color, #9aa4ad); - white-space: nowrap; - } - .trigger { - width: 40px; - height: 40px; - flex: none; - box-sizing: border-box; - padding: 3px; - border: 1px solid var(--divider-color, #666); - border-radius: 7px; - background: - linear-gradient(45deg, #b8b8b8 25%, transparent 25%) 0 0 / 10px 10px, - linear-gradient(45deg, transparent 75%, #b8b8b8 75%) 0 0 / 10px 10px, - linear-gradient(45deg, transparent 75%, #b8b8b8 75%) 5px -5px / 10px 10px, - linear-gradient(45deg, #b8b8b8 25%, #eee 25%) 5px 5px / 10px 10px; - cursor: pointer; - } - :host([data-pointer-hover]) .trigger:hover { - border-color: var(--primary-color, #03a9f4); - } - .trigger[aria-expanded='true'] { - border-color: var(--primary-color, #03a9f4); - outline: none; - box-shadow: 0 0 0 1px var(--primary-color, #03a9f4); - } - .trigger:focus-visible { - border-color: var(--primary-color, #03a9f4); - outline: 2px solid var(--primary-color, #03a9f4); - outline-offset: 2px; - } - .swatch { - display: block; - width: 100%; - height: 100%; - border-radius: 3px; - box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.18); - pointer-events: none; - } - .picker { - /* The popover attribute promotes this surface into the browser top layer, outside - hp-dialog's scrolling/clipping body. JS supplies viewport coordinates. */ - position: fixed; - z-index: 2147483647; - inset: auto; - top: 0; - left: 0; - width: min(292px, calc(100vw - 16px)); - max-height: calc(100vh - 16px); - margin: 0; - box-sizing: border-box; - display: grid; - gap: 10px; - padding: 12px; - overflow: auto; - color: var(--primary-text-color, #fff); - background: var(--card-background-color, #202126); - border: 1px solid var(--primary-color, #03a9f4); - border-radius: 10px; - box-shadow: 0 10px 28px rgb(0 0 0 / 0.34); - } - .picker[popover]:not(:popover-open) { - /* Keep the author-level display:grid from exposing the surface for one - frame before showPopover() promotes it into the top layer. */ - display: none; - } - .picker-head, - .row, - .control-head { - display: flex; - align-items: center; - gap: 8px; - min-width: 0; - } - .picker-head { - justify-content: space-between; - } - .caption { - flex: 1; - min-width: 0; - overflow: hidden; - color: var(--secondary-text-color, #9aa4ad); - font-size: 12px; - text-overflow: ellipsis; - white-space: nowrap; - } - .preview { - width: 52px; - height: 30px; - flex: none; - box-sizing: border-box; - padding: 3px; - border: 1px solid var(--divider-color, #666); - border-radius: 7px; - background: - linear-gradient(45deg, #b8b8b8 25%, transparent 25%) 0 0 / 10px 10px, - linear-gradient(45deg, transparent 75%, #b8b8b8 75%) 0 0 / 10px 10px, - linear-gradient(45deg, transparent 75%, #b8b8b8 75%) 5px -5px / 10px 10px, - linear-gradient(45deg, #b8b8b8 25%, #eee 25%) 5px 5px / 10px 10px; - } - .preview > span { - display: block; - width: 100%; - height: 100%; - border-radius: 3px; - } - .sv-field { - position: relative; - width: 100%; - height: 132px; - overflow: hidden; - box-sizing: border-box; - border: 1px solid var(--divider-color, #666); - border-radius: 8px; - background: - linear-gradient(to top, #000, transparent), - linear-gradient(to right, #fff, var(--hp-picker-hue, #f00)); - cursor: crosshair; - touch-action: none; - user-select: none; - } - .sv-thumb { - position: absolute; - left: var(--hp-picker-saturation, 0%); - top: var(--hp-picker-value, 100%); - width: 14px; - height: 14px; - box-sizing: border-box; - border: 2px solid #fff; - border-radius: 50%; - box-shadow: 0 0 0 1px rgb(0 0 0 / .65), 0 1px 3px rgb(0 0 0 / .5); - transform: translate(-50%, -50%); - pointer-events: none; - } - .control { - display: grid; - gap: 2px; - min-width: 0; - } - .control-value { - flex: none; - color: var(--secondary-text-color, #9aa4ad); - font-size: 12px; - } - input[type='range'] { - width: auto; - height: 40px; - min-width: 0; - flex: 1; - margin: -5px 0; - accent-color: var(--primary-color, #03a9f4); - touch-action: none; - } - .hue-range { - accent-color: var(--hp-picker-hue, #f00); - --hp-picker-hue-track: linear-gradient(to right, - #f00 0%, #ff0 16.667%, #0f0 33.333%, #0ff 50%, - #00f 66.667%, #f0f 83.333%, #f00 100%); - --hp-picker-hue-thumb-shadow: - 0 0 0 2px var(--card-background-color, #202126), - 0 0 0 3px var(--primary-text-color, #fff); - } - .hue-range::-webkit-slider-runnable-track { - height: 10px; - box-sizing: border-box; - border: 1px solid var(--divider-color, #666); - border-radius: 999px; - background: var(--hp-picker-hue-track); - } - .hue-range::-moz-range-track { - height: 10px; - box-sizing: border-box; - border: 1px solid var(--divider-color, #666); - border-radius: 999px; - background: var(--hp-picker-hue-track); - } - .hue-range::-moz-range-progress { - height: 10px; - border: 0; - border-radius: 999px; - background: transparent; - } - .hue-range::-webkit-slider-thumb { - border-radius: 50%; - box-shadow: var(--hp-picker-hue-thumb-shadow); - } - .hue-range::-moz-range-thumb { - border-radius: 50%; - box-shadow: var(--hp-picker-hue-thumb-shadow); - } - input[type='text'], - input[type='number'] { - min-height: 40px; - box-sizing: border-box; - border: 1px solid var(--divider-color, #666); - border-radius: 6px; - padding: 6px 8px; - color: var(--primary-text-color, #fff); - background: var(--input-fill-color, transparent); - font: inherit; - } - input[type='text'] { - width: 100%; - } - input[type='number'] { - width: 50px; - flex: none; - } - input:focus-visible, - .sv-field:focus-visible { - outline: 2px solid var(--primary-color, #03a9f4); - outline-offset: 1px; - } - input[aria-invalid='true'] { - border-color: var(--error-color, #db4437); - } - .error { - color: var(--error-color, #db4437); - font-size: 11px; - line-height: 1.25; - } - .pct { - color: var(--secondary-text-color, #9aa4ad); - font-size: 12px; - } - :host([disabled]) { - opacity: .5; - pointer-events: none; - } - @media (prefers-reduced-motion: reduce) { - .picker { transition: none !important; } - } - @media (forced-colors: active) { - .hue-range::-webkit-slider-runnable-track { - border-color: ButtonText; - background: Canvas; - } - .hue-range::-moz-range-track { - border-color: ButtonText; - background: Canvas; - } - .hue-range::-webkit-slider-thumb { - box-shadow: none; - } - .hue-range::-moz-range-thumb { - box-shadow: none; - } - } - `,customElements.get("hp-color-opacity")||customElements.define("hp-color-opacity",Ue);let Ge=0;class Ve extends he{constructor(){super(...arguments),this.text="",this.ariaLabel="",this.pointerHover=!1,this._open=!1,this._openTimer=0,this._closeTimer=0,this._positionRaf=0,this._forceFallback=!1,this._hoverOwned=!1,this._overlayDispose=null,this._scrollDialog=null,this._descriptionId="hp-help-description-"+ ++Ge,this._surfacePointerEnter=()=>{const e=this._window();this._closeTimer&&e?.clearTimeout(this._closeTimer),this._closeTimer=0},this._surfacePointerLeave=()=>{this._scheduleClose()},this._outsidePointerDown=e=>{if(!this._open)return;const t=e.composedPath();this._floating.containsPath(t)||this._closeHelp(!1,"outside")},this._dialogScroll=e=>{const t=this._floating.containsPath(e.composedPath()),i=e.target,s=this._dialog(),n=i instanceof Node&&(i===s||i instanceof Element&&i.contains(this));var o;this._open&&(o=n,!t&&o)&&this._closeHelp(!1,"scroll")},this._keyDown=e=>{if(!this._open||e.defaultPrevented)return;if("Escape"===e.key)return e.preventDefault(),e.stopImmediatePropagation(),void this._closeHelp(this._floating.ownsActiveElement(),"escape");const t=this.renderRoot.querySelector(".trigger");if(!t||!e.composedPath().includes(t))return;const i=this._surface();if(!i||i.scrollHeight<=i.clientHeight)return;const s=Math.max(40,.8*i.clientHeight),n={ArrowDown:40,ArrowUp:-40,PageDown:s,PageUp:-s};if("Home"===e.key)i.scrollTop=0;else if("End"===e.key)i.scrollTop=i.scrollHeight;else{if(!(e.key in n))return;i.scrollBy({top:n[e.key],behavior:"auto"})}e.preventDefault(),e.stopImmediatePropagation()},this._floating=new qe(this,"help",this._keyDown),this._queuePosition=()=>{if(!this._open||this._positionRaf)return;const e=this._window();e&&(this._positionRaf=e.requestAnimationFrame(()=>{this._positionRaf=0,this._position()||this._closeHelp()}))}}connectedCallback(){super.connectedCallback(),this.addEventListener("keydown",this._keyDown,!0)}willUpdate(){this.toggleAttribute("data-has-content",this._hasContent())}disconnectedCallback(){const e=this.ownerDocument.defaultView;this._unsubscribeOpenListeners(),this.removeEventListener("keydown",this._keyDown,!0),this._clearTimers(),this._positionRaf&&e?.cancelAnimationFrame(this._positionRaf),this._positionRaf=0,this._closeHelp(!1,"disconnect"),super.disconnectedCallback()}updated(e){if(e.has("pointerHover")&&!this.pointerHover){const e=this._window();this._openTimer&&e?.clearTimeout(this._openTimer),this._openTimer=0,this._hoverOwned&&this._closeHelp(!1,"outside")}this._hasContent()||!this._open?this._open&&e.has("text")&&(this._usesPopover()||this._renderFallback(),this._queuePosition()):this._closeHelp()}_window(){return this._floating.window()}_dialog(){return this._floating.dialog()}_usesPopover(){return this._floating.usesPopover(this._forceFallback)}_hasContent(){return e=this.text,t=this.ariaLabel,"string"==typeof e&&e.trim().length>0&&"string"==typeof t&&t.trim().length>0;var e,t}_clearTimers(){const e=this._window();this._openTimer&&e?.clearTimeout(this._openTimer),this._closeTimer&&e?.clearTimeout(this._closeTimer),this._openTimer=0,this._closeTimer=0}_scheduleOpen(){const e=this._window();this.pointerHover&&e&&!this._open&&!this._openTimer&&(this._closeTimer&&e.clearTimeout(this._closeTimer),this._closeTimer=0,this._openTimer=e.setTimeout(()=>{this._openTimer=0,this.isConnected&&(this._hoverOwned=!0,this._openHelp())},300))}_scheduleClose(){const e=this._window(),t=this.renderRoot.querySelector(".trigger");e&&!t?.matches(":focus-visible")&&(this._openTimer&&e.clearTimeout(this._openTimer),this._closeTimer&&e.clearTimeout(this._closeTimer),this._openTimer=0,this._closeTimer=e.setTimeout(()=>{this._closeTimer=0,this.isConnected&&this._closeHelp()},150))}_triggerPointerEnter(e){this.pointerHover&&"mouse"===e.pointerType&&this._scheduleOpen()}_triggerPointerLeave(e){"mouse"===e.pointerType&&this._scheduleClose()}_triggerFocus(){queueMicrotask(()=>{const e=this.renderRoot.querySelector(".trigger");e?.matches(":focus-visible")&&(this._hoverOwned=!1,this._openHelp())})}_triggerBlur(){this._scheduleClose()}_triggerClick(){this._open?this._closeHelp():(this._hoverOwned=!1,this._openHelp())}_subscribeOpenListeners(){this.ownerDocument.addEventListener("pointerdown",this._outsidePointerDown,!0),this.ownerDocument.addEventListener("keydown",this._keyDown,!0);const e=this._window();e?.addEventListener("resize",this._queuePosition),e?.addEventListener("orientationchange",this._queuePosition),e?.visualViewport?.addEventListener("resize",this._queuePosition),e?.visualViewport?.addEventListener("scroll",this._queuePosition),this._scrollDialog=this._dialog(),this._scrollDialog?.addEventListener("scroll",this._dialogScroll,!0)}_unsubscribeOpenListeners(){this.ownerDocument.removeEventListener("pointerdown",this._outsidePointerDown,!0),this.ownerDocument.removeEventListener("keydown",this._keyDown,!0);const e=this._window();e?.removeEventListener("resize",this._queuePosition),e?.removeEventListener("orientationchange",this._queuePosition),e?.visualViewport?.removeEventListener("resize",this._queuePosition),e?.visualViewport?.removeEventListener("scroll",this._queuePosition),this._scrollDialog?.removeEventListener("scroll",this._dialogScroll,!0),this._scrollDialog=null}async _openHelp(){!this._open&&this._hasContent()&&(this._clearTimers(),this._forceFallback=!1,this._open=!0,this._subscribeOpenListeners(),await this.updateComplete,this._open&&(this._usesPopover()||this._renderFallback(),this._position()?this._overlayDispose=this._dialog()?.registerOverlay({owner:this,group:"transient",close:e=>this._closeHelp("escape"===e&&this._floating.ownsActiveElement(),e)})||null:this._closeHelp()))}_closeHelp(e=!1,t="exclusive"){if(!this._open&&!this._floating.hasFallback)return;this._clearTimers();const i=this._overlayDispose;this._overlayDispose=null,i?.();const s=this.renderRoot.querySelector(".tooltip");if(s?.hidePopover)try{s.matches(":popover-open")&&s.hidePopover()}catch{}this._open=!1,this._hoverOwned=!1,this._unsubscribeOpenListeners(),this._floating.destroy(),e&&this.updateComplete.then(()=>this.renderRoot.querySelector(".trigger")?.focus())}_tooltipTemplate(e){return q``}_renderFallback(){const e=Ve.styles.cssText;this._floating.renderFallback(this._tooltipTemplate(!1),e)}_surface(){return this._floating.surface(".tooltip",this._usesPopover())}_position(){if(!this._open)return!1;const e=this._window(),t=this.renderRoot.querySelector(".trigger");let i=this._surface();if(!e||!t?.isConnected||!i?.isConnected)return!1;const s=Be(e);if(i.style.maxWidth=`${Math.max(0,Math.min(320,s.width-16))}px`,i.style.maxHeight=`${Math.max(0,s.height-16)}px`,i.style.visibility="hidden",this._usesPopover()&&i.showPopover)try{i.matches(":popover-open")||i.showPopover()}catch{if(this._forceFallback=!0,this._renderFallback(),i=this._surface(),!i?.isConnected)return!1;i.style.visibility="hidden"}const n=t.getBoundingClientRect(),o=i.getBoundingClientRect();if(!(n.width&&n.height&&o.width&&o.height))return!1;const a=je(n,o,s);return i.style.left=`${a.left}px`,i.style.top=`${a.top}px`,i.dataset.side=a.side,i.style.visibility="",!0}render(){if(!this._hasContent())return G;const e=this.ariaLabel.trim();return q` - ${this.text} - - ${this._usesPopover()?this._tooltipTemplate(!0):G} - `}}Ve.properties={text:{type:String},ariaLabel:{type:String,attribute:"aria-label"},pointerHover:{type:Boolean,attribute:"data-pointer-hover",reflect:!0},_open:{state:!0},_forceFallback:{state:!0}},Ve.styles=o` - :host { - display: none; - flex: none; - vertical-align: middle; - } - - :host([data-has-content]) { - display: inline-flex; - } - - .trigger { - width: 32px; - height: 32px; - display: inline-flex; - align-items: center; - justify-content: center; - flex: none; - box-sizing: border-box; - padding: 0; - color: var(--secondary-text-color, #9aa4ad); - background: transparent; - border: 0; - border-radius: 50%; - cursor: help; - -webkit-tap-highlight-color: transparent; - } - - .trigger svg { - display: block; - width: 18px; - height: 18px; - fill: currentColor; - } - - :host([data-pointer-hover]) .trigger:hover, - .trigger:focus-visible, - .trigger[aria-expanded='true'] { - color: var(--primary-text-color, #fff); - background: rgb(127 127 127 / 0.18); - outline: none; - } - - .trigger:focus-visible { - box-shadow: 0 0 0 2px var(--primary-color, #03a9f4); - } - - .tooltip { - position: fixed; - z-index: 2147483647; - inset: auto; - top: 0; - left: 0; - width: max-content; - max-width: min(320px, calc(100vw - 16px)); - max-height: calc(100vh - 16px); - margin: 0; - box-sizing: border-box; - padding: 9px 11px; - overflow: auto; - color: var(--primary-text-color, #fff); - background: var(--card-background-color, #202126); - border: 1px solid var(--divider-color, rgb(255 255 255 / 0.2)); - border-radius: 9px; - box-shadow: 0 8px 24px rgb(0 0 0 / 0.32); - font: 400 13px/1.4 system-ui, sans-serif; - text-align: start; - white-space: normal; - overflow-wrap: anywhere; - opacity: 1; - transform: translateY(0); - transition: opacity 120ms ease, transform 120ms ease; - } - - .tooltip[data-side='top'] { - transform-origin: bottom center; - } - - .tooltip[data-side='bottom'] { - transform-origin: top center; - } - - .tooltip[popover]:not(:popover-open) { - display: none; - } - - @starting-style { - .tooltip { - opacity: 0; - transform: translateY(3px); - } - } - - .sr-only { - /* Keep the accessibility-only description outside every dialog scroll - container's overflow geometry. Toggling the hidden attribute on the former - absolutely-positioned node made some browsers add a vertical - scrollbar while help was open, even though the visible tooltip was - already in the Popover top layer / fixed fallback portal. */ - position: fixed; - inset: 0 auto auto 0; - width: 1px; - height: 1px; - padding: 0; - margin: 0; - overflow: hidden; - clip: rect(0, 0, 0, 0); - clip-path: inset(50%); - white-space: nowrap; - border: 0; - } - - @media (pointer: coarse) { - .trigger { - width: 40px; - height: 40px; - } - } - - @media (prefers-reduced-motion: reduce) { - .tooltip { - transition: none; - } - } - `,customElements.get("hp-help")||customElements.define("hp-help",Ve);const Ke=[o` - :host { - --hp-bg: var(--card-background-color, #16212e); - --hp-line: var(--divider-color, #2b3d4f); - --hp-txt: var(--primary-text-color, #e6edf3); - --hp-muted: var(--secondary-text-color, #8aa0b3); - --hp-accent: var(--primary-color, #3ea6ff); - --hp-on: #ffd45c; - --hp-open: #ff9f43; - /* design tokens (UI chrome only). The icon/plan scale math stays on - --icon-size/--dev-size cqw units and never uses these. */ - /* spacing scale, fact-based; stray 3/5/7/9/13/14px values are unified - onto the nearest step (max +-2px, the whole point of the pass) */ - --sp-1: 2px; - --sp-2: 4px; - --sp-3: 6px; - --sp-4: 8px; - --sp-5: 12px; - --sp-6: 16px; - /* px radii of dialogs/buttons/plates (the 22% badge radius is scale math) */ - --rad-s: 6px; - --rad-m: 8px; - --rad-l: 12px; - /* font tiers: fine print+labels / body+buttons / title */ - --fs-s: 12px; - --fs-m: 13px; - --fs-l: 15px; - /* elevation: badge / floating panel (menu, tip, toast) / dialog */ - --shadow-1: 0 1px 3px rgba(0, 0, 0, 0.45); - --shadow-2: 0 6px 22px rgba(0, 0, 0, 0.45); - --shadow-3: 0 8px 30px rgba(0, 0, 0, 0.5); - } - ha-card { - overflow: visible; /* overflow:hidden breaks position:sticky on the header */ - } - .empty { - padding: 40px 24px; - color: var(--hp-txt); - text-align: center; - display: flex; - flex-direction: column; - align-items: center; - gap: var(--sp-4); - } - .empty .big { - --mdc-icon-size: 56px; - color: var(--hp-accent); - opacity: 0.7; - } - .empty .muted { - color: var(--hp-muted); - font-size: var(--fs-m); - margin: 0; - } - .empty .btn { - margin-top: var(--sp-4); - } - @keyframes fixedfloor-spin { - to { transform: rotate(360deg); } - } - .spacer { - flex: 1; - } - .bootveil { - position: absolute; - inset: 0; - z-index: 40; - display: flex; - align-items: center; - justify-content: center; - background: var(--hp-bg, #16212e); - opacity: 1; - transition: opacity 0.15s ease; - pointer-events: none; - } - .bootveil.off { - opacity: 0; - } - .bootveil .boothouse { - position: static; /* the main .zoomwrap SVG is pinned to inset:0 — not this icon */ - width: 56px; - height: 56px; - fill: var(--hp-accent); - opacity: 0.85; - animation: hp-boot-pulse 1.3s ease-in-out infinite; - } - @keyframes hp-boot-pulse { - 0%, 100% { opacity: 0.3; transform: scale(0.94); } - 50% { opacity: 0.9; transform: scale(1); } - } - @media (prefers-reduced-motion: reduce) { - .bootveil .boothouse { - animation: none; - opacity: 0.7; - } - } - @keyframes hp-pulse-short { - 0% { transform: scale(1); opacity: 0.55; } - 70% { opacity: 0.22; } - 100% { transform: scale(var(--ripple-scale, 1.5)); opacity: 0; } - } - /* Alternate identity: a rapid retrigger restarts the browser timeline. */ - @keyframes hp-pulse-short-b { - 0% { transform: scale(1); opacity: 0.55; } - 70% { opacity: 0.22; } - 100% { transform: scale(var(--ripple-scale, 1.5)); opacity: 0; } - } - @keyframes hp-pulse-continuous { - 0% { transform: scale(1); opacity: 0.55; } - 65% { opacity: 0.18; } - 100% { transform: scale(var(--ripple-scale, 1.5)); opacity: 0; } - } - @keyframes hp-pulse-alarm { - 0% { transform: scale(1); opacity: 0.72; } - 100% { transform: scale(1.5); opacity: 0; } - } - /* Space changes keep their direction, but travel only a few percent: the - motion explains what changed without making the whole plan fly around. */ - @keyframes hp-slide-left { - 0% { transform: translateX(4%); opacity: 0.72; } - 100% { transform: translateX(0); opacity: 1; } - } - @keyframes hp-slide-right { - 0% { transform: translateX(-4%); opacity: 0.72; } - 100% { transform: translateX(0); opacity: 1; } - } - @keyframes hp-sunfade-in { - from { opacity: 0; } - to { opacity: 1; } - } - @keyframes hp-sunfade-out { - from { opacity: 1; } - to { opacity: 0; } - } - .sr-only { - position: absolute !important; - width: 1px !important; - height: 1px !important; - padding: 0 !important; - margin: -1px !important; - overflow: hidden !important; - clip: rect(0, 0, 0, 0) !important; - white-space: nowrap !important; - border: 0 !important; - } - .toast { - position: fixed; - pointer-events: none; - left: 50%; - bottom: 22px; - transform: translateX(-50%); - background: var(--hp-bg); - border: 1px solid var(--hp-accent); - color: var(--hp-txt); - padding: var(--sp-4) var(--sp-6); - border-radius: var(--rad-l); - font-size: var(--fs-m); - box-shadow: var(--shadow-2); - z-index: 120; - max-width: 90vw; - } -`,o` - .fixedfloor-loading { - animation: fixedfloor-spin 1.1s linear infinite; - } - .fixedfloor-error p { - max-width: 42rem; - margin: 0; - overflow-wrap: anywhere; - } - @media (prefers-reduced-motion: reduce) { - .fixedfloor-loading { animation: none; } - } - .stage.noplan { - background: #ffffff; - } - /* Editors always keep the white drawing sheet — even when a backdrop - image is loaded. Without this, a picture plan painted the theme card - colour under the grid (owner 2026-08-05). View keeps the historical - theme / .noplan split. */ - .stage.mode-plan, - .stage.mode-devices, - .stage.mode-decor { - background: #ffffff; - } - .stage { - position: relative; - width: 100%; - container-type: inline-size; - overflow: hidden; - touch-action: none; /* custom pinch/pan gestures */ - background: var(--ha-card-background, var(--card-background-color, #111)); - } - .zoomwrap { - position: absolute; - inset: 0; - z-index: 1; - } - .stage.mode-transition, - .stage.mode-transition .zoomwrap { - transition: none !important; - } - .stage.mode-transition .hp-view-only-layer, - .stage.mode-transition .hp-editor-only-layer { - pointer-events: none; - will-change: opacity; - } - .stage.mode-transition .hp-paper { - fill: var(--hp-mode-paper) !important; - } - /* Four-phase environment (#146): constant layers cross-fade because CSS - gradients themselves are not reliably interpolated. Everything stays - behind the plan; the plan tree is never dimmed, tinted or faded. */ - .hp-day-cycle-env { - position: absolute; - inset: 0; - z-index: 0; - overflow: hidden; - pointer-events: none; - isolation: isolate; - } - .hp-day-cycle-bg { - position: absolute; - inset: 0; - opacity: 0; - overflow: hidden; - transition: opacity 1100ms cubic-bezier(.22, .61, .36, 1); - } - .hp-day-cycle-bg.active { opacity: 1; } - .hp-day-cycle-sun { - position: absolute; - left: var(--hp-day-cycle-sun-x); - top: var(--hp-day-cycle-sun-y); - width: clamp(140px, 25cqw, 250px); - height: clamp(140px, 25cqw, 250px); - transform: translate(-50%, -50%); - border-radius: 50%; - filter: blur(10px); - opacity: var(--hp-day-cycle-sun-opacity); - transition: - left 1100ms cubic-bezier(.22, .61, .36, 1), - top 1100ms cubic-bezier(.22, .61, .36, 1), - opacity 1100ms cubic-bezier(.22, .61, .36, 1); - } - .stage.mode-transition .hp-day-cycle-env { transition: none; } - .stage.daycycle .hp-paperg, - .hp-static-stage.daycycle .hp-paperg { - filter: - drop-shadow(0 0 1px var(--hp-day-cycle-outline-near)) - drop-shadow(0 0 5px var(--hp-day-cycle-outline-mid)) - drop-shadow(0 0 10px var(--hp-day-cycle-outline-far)); - transition: filter 1100ms cubic-bezier(.22, .61, .36, 1); - } - @media (prefers-reduced-motion: reduce) { - .hp-day-cycle-bg, - .hp-day-cycle-sun, - .stage.daycycle .hp-paperg, - .hp-static-stage.daycycle .hp-paperg { - transition: none; - } - } - .sunlayer { - pointer-events: none; - } - .compass { - width: 120px; - height: 120px; - flex: none; - touch-action: none; - cursor: grab; - user-select: none; - } - .compass:active { - cursor: grabbing; - } - .compass .cring { - fill: rgba(255, 255, 255, 0.04); - stroke: var(--divider-color, #444); - stroke-width: 2; - } - .compass .ctick { - stroke: var(--secondary-text-color, #9aa4ad); - stroke-width: 2; - } - .compass .ctick.minor { - stroke-width: 1; - opacity: 0.6; - } - .compass .cneedle line { - stroke: var(--primary-color, #3ea6ff); - stroke-width: 2.5; - stroke-linecap: round; - } - .compass .cneedle path { - fill: var(--primary-color, #3ea6ff); - } - .compass .cneedle text { - fill: var(--text-primary-color, #fff); - font-size: 11px; - font-weight: 700; - } - .compass .cdeg { - fill: var(--secondary-text-color, #9aa4ad); - font-size: 13px; - } - .compass.unset .cneedle { - opacity: 0.35; - } - /* HP-1552: first-open boot veil — the plan hides until the stage height settles */ - .stage.hpboot .zoomwrap, - .stage.hpboot .zoombadge { - visibility: hidden; - } - /* AUD-1552-02: post-veil grace — HA chrome landing after the cap moves - the stage height smoothly; the viewport ResizeObserver refits the plan - along the transition, so a late panel glides instead of jumping. */ - .stage.hpsettle { - transition: height 0.25s ease; - } - @media (prefers-reduced-motion: reduce) { - .stage.hpsettle { - transition: none; - } - } - .zoomctl { - display: inline-flex; - gap: var(--sp-1); - background: rgba(127, 127, 127, 0.12); - border-radius: var(--rad-m); - padding: var(--sp-1); - } - .zoomctl .zb { - border: none; - padding: var(--sp-3) var(--sp-4); - } - .zoomctl .zb[disabled] { - opacity: 0.4; - pointer-events: none; - } - .projection-toggle { - min-width: 44px; - min-height: 44px; - justify-content: center; - padding: var(--sp-3); - } - /* docs/CANVAS.md §5: the plane has no edges, so you can pan until nothing - is on screen. One pointer home, one click back. */ - .homearrow { - position: absolute; - z-index: 12; - transform: translate(-50%, -50%); - width: 40px; - height: 40px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - background: var(--card-background-color, var(--hp-bg)); - color: var(--hp-accent); - border: 1px solid var(--hp-accent); - opacity: 0.9; - padding: 0; - } - .homearrow ha-icon { - --mdc-icon-size: 22px; - line-height: 1; - } - .zoombadge { - position: absolute; - z-index: 12; - left: var(--sp-4); - bottom: var(--sp-4); - background: var(--card-background-color, var(--hp-bg)); - opacity: 0.92; - color: var(--hp-txt); - border: 1px solid var(--hp-accent); - border-radius: var(--rad-m); - padding: var(--sp-1) var(--sp-4); - font-size: var(--fs-s); - font-weight: 600; - pointer-events: none; - } - /* Only the plan canvas owns the whole zoom wrapper. Context trays now live - inside .stage as well, and may contain small SVG previews (furniture in - particular); the old descendant-wide stage-SVG selector stretched every - preview over the tray and stacked them into one blocking artefact. The - vacuum trail/fit overlays have their own explicit geometry below. */ - .zoomwrap > svg { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - display: block; - } - .plan-svg { z-index: 1; } - .iso-underlay-svg { z-index: 0; overflow: visible; } - .iso-shadows-svg { z-index: 3; overflow: visible; } - .iso-walls-svg { - z-index: 4; - overflow: visible; - } - .iso-underlay-svg, - .iso-shadows-svg, - .iso-walls-svg, - .iso-underlay, - .iso-shadows, - .iso-walls, - .iso-openings { - pointer-events: none; - } - .iso-side-hi { stop-color: #b9bdbe; } - .iso-side-lo { stop-color: #969c9f; } - .iso-top-hi { stop-color: #fafaf7; } - .iso-top-lo { stop-color: #e2e4e2; } - .iso-wall-side { - fill: url(#hp-iso-wall-side) #a8acae; - stroke: #92989b; - stroke-width: 0.7; - vector-effect: non-scaling-stroke; - } - .iso-wall-top { - fill: url(#hp-iso-wall-top) #f3f3f1; - stroke: #d7d9d8; - stroke-width: 0.8; - vector-effect: non-scaling-stroke; - } - .iso-floor-side { - fill: #858b8d; - stroke: #71787b; - stroke-width: 0.7; - vector-effect: non-scaling-stroke; - } - .iso-opening-panel { - fill: #d7d9d7; - fill-opacity: 0.96; - stroke: #7f878b; - stroke-width: 0.9; - vector-effect: non-scaling-stroke; - } - .iso-opening-panel.iso-window { - fill: #dfeff4; - fill-opacity: 0.72; - stroke: #8aa7b1; - } - .iso-ambient-shadow { - fill: rgba(15, 21, 25, 0.22); - filter: url(#hp-iso-ambient-shadow); - } - .iso-contact-shadow { - fill: none; - stroke: rgba(22, 28, 31, 0.25); - stroke-width: 3; - filter: url(#hp-iso-contact-shadow); - vector-effect: non-scaling-stroke; - } - .iso-leaf-shadow { - fill: none; - stroke: rgba(18, 23, 27, 0.24); - stroke-width: 4; - filter: url(#hp-iso-leaf-shadow); - vector-effect: non-scaling-stroke; - } - @media (prefers-color-scheme: dark) { - .iso-side-hi { stop-color: #4c555a; } - .iso-side-lo { stop-color: #343c40; } - .iso-top-hi { stop-color: #687176; } - .iso-top-lo { stop-color: #50585d; } - .iso-wall-side { stroke: #30373b; } - .iso-wall-top { stroke: #7b858a; } - .iso-floor-side { fill: #2d3438; stroke: #20272a; } - .iso-opening-panel { fill: #626b70; stroke: #899399; } - .iso-opening-panel.iso-window { fill: #75919b; stroke: #abc6ce; } - .iso-ambient-shadow { fill: rgba(0, 0, 0, 0.34); } - .iso-contact-shadow, .iso-leaf-shadow { stroke: rgba(0, 0, 0, 0.38); } - } - @media (forced-colors: active) { - .iso-wall-side, .iso-wall-top, .iso-floor-side, .iso-opening-panel { - fill: Canvas; - stroke: CanvasText; - forced-color-adjust: auto; - } - .iso-ambient-shadow, .iso-contact-shadow, .iso-leaf-shadow { display: none; } - } - @supports not (filter: blur(1px)) { - .iso-ambient-shadow, .iso-contact-shadow, .iso-leaf-shadow { display: none; } - } - /* Opaque plan paper (owner 2026-08-03): the scene bg_color / daynight sky - shows ONLY around the plan, never through it. The colour is the - pre-bg_color canvas — the theme card background under an image plan, - plain white for a hand-drawn one (.stage.noplan). On drawn plans the - paper is per-room shapes following the room contours — fill only, - never a stroke, so the paper cannot poke past a wall. Night dimming - comes from the .zoomwrap brightness filter, never from alpha - (docs/SUN.md). */ - .hp-paper { - fill: var(--ha-card-background, var(--card-background-color, #111)); - stroke: none; - } - .stage.noplan .hp-paper, - .stage.mode-plan .hp-paper, - .stage.mode-devices .hp-paper, - .stage.mode-decor .hp-paper { - fill: #ffffff; - } - /* Owner 2026-08-04: «углы границ комнат всё ещё с зубцами». A miter join - on a 30-45° corner shoots a spike far past the wall (and flips to an - ugly bevel once past the miter limit) — the same defect the decor lines - had before they got round caps. Every room border, in EVERY renderer - that reuses these styles (plan view, plan editor, static space-card), - joins its walls with a ROUND join instead: the corner reads as the - stroke's own radius, never as a tooth. The linecap matters only for the - open outlines below, but it costs nothing to state it here. */ - .room { - transition: 0.12s; - cursor: default; /* v1.40.1: rooms are not clickable — the label's link icon is */ - stroke-linejoin: round; - stroke-linecap: round; - } - .room.overlay { - fill: transparent; - stroke: transparent; - stroke-width: calc(2px * var(--hp-cell-visual-scale, 1)); - } - :host([data-pointer-hover]) .stage.mode-view .room.overlay:not(.styled):hover { - stroke: var(--hp-accent); - stroke-opacity: 1; - } - .room.yard { - fill: rgba(75, 140, 90, 0.14); - stroke: #4b8c5a; - stroke-width: calc(2px * var(--hp-cell-visual-scale, 1)); - } - :host([data-pointer-hover]) .stage.mode-view .room.yard:not(.styled):hover { - stroke: var(--hp-accent); - stroke-opacity: 1; - } - .room.styled { - stroke: var(--room-stroke, transparent); - stroke-opacity: var(--room-stroke-op, 0); - stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); - fill: var(--room-fill, transparent); - fill-opacity: var(--room-fill-op, 0); - } - .glow-base-layer, - .glow-base-tunnels, - .glow-pools-frame, - .glow-pools, - .glow-spot { - pointer-events: none; - } - /* The parent isolates all source spots from the room data fill, Glow base, - paper and backdrop. A spot is one circle clipped to the floor its lamp - can see, so the whole spot screen-blends as a single primitive — there is - no mask left to be dropped on a promoted layer. Per-stop alpha already - contains the shared 0.7 ceiling; the spot opacity below only animates - between 0 and 1 and is never another persistent alpha ceiling. */ - .glow-pools-frame, - .glow-pools, - .glow-spot { - isolation: isolate; - } - .glow-pools.blend-screen .glow-spot { - mix-blend-mode: screen; - } - .glow-spot { - opacity: 1; - transition: opacity 500ms ease; - } - .glow-spot.is-entering, - .glow-spot.is-leaving { - opacity: 0; - } - @media (prefers-reduced-motion: reduce) { - .glow-spot { - transition: none; - } - } - /* The explicit late room-hover layer owns the wash and halo. Keeping CSS - filters off room paths prevents Chromium from recompositing the sibling - screen-blended Glow layer for one bright frame on every hover. */ - :host([data-pointer-hover]) .stage.mode-view .room.styled:hover { - stroke: var(--hp-accent); - stroke-opacity: 1; - } - /* doors, windows & gates */ - .op-leaf { - transition: transform 0.6s ease; - } - .op-arc { - stroke-width: calc(1.5px * var(--hp-cell-visual-scale, 1)); - transition: stroke-dashoffset 0.6s ease; - } - /* hover affordance: a rounded outline hugging the wall strip + a grab cursor */ - .op-outline { - fill: none; - stroke: var(--hp-accent); - stroke-width: calc(1.5px * var(--hp-cell-visual-scale, 1)); - opacity: 0; - transition: opacity 0.15s; - pointer-events: none; - } - :host([data-pointer-hover]) .stage.markup g.opening:hover .op-outline { - opacity: 0.9; - } - /* openings are pure status graphics outside Plan mode: no cursor, no hover, - no hit target — View must not interact with them at all */ - .op-hit { - fill: transparent; - pointer-events: none; - cursor: default; - } - .stage.markup .op-hit { - pointer-events: auto; - cursor: grab; - touch-action: none; /* drags, not scrolls, on touch */ - } - .stage.markup .op-hit:active { - cursor: grabbing; - } - .stage.markup .opening.orphan { - pointer-events: auto; - cursor: pointer; - color: var(--error-color, #db4437); - } - .stage.markup .opening.orphan circle { - fill: var(--hp-bg); - stroke: currentColor; - stroke-width: calc(2px * var(--hp-cell-visual-scale, 1)); - } - .stage.markup .opening.orphan text { - fill: currentColor; - font-weight: 800; - font-size: calc(12px * var(--hp-cell-visual-scale, 1)); - pointer-events: none; - } - /* HP-1550-04: in the resize tool the wall handles own the hit test — the - transparent .op-hit of a door at the midpoint of a wall used to sit ON - TOP of the handle and made that wall ungrabbable for both rooms. - Openings are not editable in this tool (they ride along with the wall), - so their hit area goes fully inert; every other Plan tool is untouched. */ - .stage.markup.tool-resize .op-hit { - pointer-events: none; - cursor: default; - } - .stage.mode-view .oplock { - pointer-events: auto; - cursor: pointer; - } - @media (prefers-reduced-motion: reduce) { - .op-leaf, .op-arc { transition: none; } - } - .roomlabel { - pointer-events: none; /* draggable only in plan mode (rule below) */ - position: absolute; - transform: translate(-50%, -50%); - font-size: calc(var(--rl-icon-size, var(--icon-size, 2.5cqw)) * 0.5 * var(--rl-scale, 1) * var(--rl-font, 1) * var(--rl-space, 1)); - font-weight: 700; - letter-spacing: 0.04em; - white-space: nowrap; - cursor: grab; - user-select: none; - z-index: 1; - display: flex; - flex-direction: column; - align-items: center; - gap: 0.15em; - text-align: center; - } - .rlname { - display: inline-flex; - align-items: center; - gap: 0.25em; - font-size: calc(1em * var(--rl-name, 1)); - } - .zoomwrap.slide-left { animation: hp-slide-left 0.18s cubic-bezier(0.2, 0.7, 0.2, 1); } - .zoomwrap.slide-right { animation: hp-slide-right 0.18s cubic-bezier(0.2, 0.7, 0.2, 1); } - @media (prefers-reduced-motion: reduce) { - .zoomwrap.slide-left, - .zoomwrap.slide-right { animation: none; } - } - /* The name is the anchor: the label box is centred on the room point, so - anything that takes part in its layout SHIFTS THE NAME. The gear button - and the metrics hang below as absolutes — the name renders in exactly - the same place in view mode and in the plan editor (owner's request), - and the button sits at the very bottom of the card. */ - /* Standalone, centred on the room, sized from the device icon: icon-size - already rescales with the view, so the button zooms WITH the plan - instead of keeping a constant screen size (owner's spec). */ - .rlgearbtn { - --gear-h: calc(var(--icon-size, 2.5cqw) * 0.77); /* owner: half the previous size */ - position: absolute; - /* dead-centred on the room, both axes (owner's spec) */ - transform: translate(-50%, -50%); - display: inline-flex; - align-items: center; - gap: 0.35em; - height: var(--gear-h); - padding: 0 calc(var(--gear-h) * 0.38); - border: 0; - border-radius: 999px; - background: var(--hp-accent); - color: var(--text-primary-color, #fff); - font: inherit; - font-size: calc(var(--gear-h) * 0.42); - font-weight: 600; - line-height: 1; - white-space: nowrap; - cursor: pointer; - pointer-events: auto; - opacity: 0.92; - box-shadow: var(--shadow-1); - z-index: 2; - } - .rlgearbtn { transition: opacity 0.15s, filter 0.15s; } - :host([data-pointer-hover]) .rlgearbtn:hover { opacity: 1; filter: brightness(1.18); } - .rlgearbtn ha-icon { --mdc-icon-size: calc(var(--gear-h) * 0.55); display: inline-flex; } - .rlgear { - --mdc-icon-size: 0.9em; - display: inline-flex; - margin-right: 0.2em; - opacity: 0.6; - cursor: pointer; - pointer-events: auto; - } - :host([data-pointer-hover]) .rlgear:hover { opacity: 1; } - .rlgo { - --mdc-icon-size: 0.85em; - display: inline-flex; - opacity: 0.55; - } - .stage.mode-view .rlgo { - pointer-events: auto; - cursor: pointer; - } - :host([data-pointer-hover]) .stage.mode-view .rlgo:hover { opacity: 1; } - .roomlabel .rlmetrics { - position: absolute; /* below the name, outside the centring math */ - top: calc(100% + 0.15em); - left: 50%; - transform: translateX(-50%); - white-space: nowrap; - display: flex; - align-items: center; - gap: 0.55em; - font-size: calc(0.75em * var(--rl-meta, 1)); /* feedback: 0.62 was unreadable on a tablet */ - font-weight: 600; - letter-spacing: 0.02em; - opacity: 0.9; - } - .roomlabel .rlm { - display: inline-flex; - align-items: center; - gap: 0.12em; - } - .roomlabel .rlm ha-icon { - --mdc-icon-size: 1.05em; - display: inline-flex; - } - .roomlabel .rlm.lit { opacity: 1; } - .rlhandle { - display: none; - position: absolute; - width: 9px; - height: 9px; - border-radius: 2px; - background: var(--hp-accent); - border: 1px solid var(--card-background-color, #fff); - z-index: 2; - } - .rlhandle.tl { left: -6px; top: -6px; cursor: nwse-resize; } - .rlhandle.br { right: -6px; bottom: -6px; cursor: nwse-resize; } - .rlhandle.tr { right: -6px; top: -6px; cursor: nesw-resize; } - .rlhandle.bl { left: -6px; bottom: -6px; cursor: nesw-resize; } - :host([data-pointer-hover]) .stage.markup .roomlabel:hover .rlhandle { display: block; } - .stage.markup .roomlabel { pointer-events: auto; } - .roomlabel:active { cursor: grabbing; } - .measurelayer { - position: absolute; - inset: 0; - pointer-events: none; - } - .alignline { - stroke: var(--hp-accent); - stroke-width: calc(1.2px * var(--hp-cell-visual-scale, 1)); - stroke-dasharray: - calc(4px * var(--hp-cell-visual-scale, 1)) - calc(4px * var(--hp-cell-visual-scale, 1)); - pointer-events: none; - opacity: 0.9; - } - .aligndot { - fill: var(--hp-accent); - pointer-events: none; - } - .measurelabel.on45 { - color: #4bd28f; - border-color: #4bd28f; - } - .measurelabel { - position: absolute; - transform: translate(12px, -150%); - font-size: var(--fs-s); - font-weight: 600; - padding: 1px var(--sp-3); - border-radius: var(--rad-s); - background: rgba(0, 0, 0, 0.72); - color: #fff; - white-space: nowrap; - pointer-events: none; - user-select: none; - z-index: 3; - } - .measurelabel.opdimension { - transform: translate( - calc(-50% + var(--op-label-shift-x, 0px)), - calc(-50% + var(--op-label-shift-y, -12px)) - ); - border: 1px solid var(--hp-open, #ff9800); - } - /* decor (background) layer */ - .decorlayer .dshape { pointer-events: none; } - .stage.mode-decor .decorlayer .dshape { - pointer-events: visiblePainted; - cursor: pointer; - } - .stage.mode-decor.dtool-select .decorlayer .dshape { cursor: move; } - /* …but a DRAWING tool owns the canvas: existing shapes stop being targets - entirely, so a new line can start exactly on the end of an old one - instead of grabbing it (owner, 2026-08-04). Same for the picture tool — - a shape lying over the plan must not block the picture's own drag. - Only select (move) and erase (delete) keep the shapes clickable. */ - .stage.mode-decor.dtool-line .decorlayer .dshape, - .stage.mode-decor.dtool-rect .decorlayer .dshape, - .stage.mode-decor.dtool-ellipse .decorlayer .dshape, - .stage.mode-decor.dtool-text .decorlayer .dshape, - .stage.mode-decor.dtool-furniture .decorlayer .dshape, - .stage.mode-decor.dtool-backdrop .decorlayer .dshape { pointer-events: none; } - /* the furniture tool is a stamp: the press must reach the stage even when - it lands on a sofa that is already there (docs/FURNITURE.md §4) */ - .stage.mode-decor.dtool-furniture { cursor: copy; } - /* ONE exception (owner, 2026-08-04): under the TEXT tool an existing LABEL - is a target again — pressing it opens its editor instead of starting a - new label on top of the old one. Only labels: a line or a rectangle - under the text tool stays inert, so the press reaches the stage and a - new label is created there. */ - .stage.mode-decor.dtool-text .decorlayer .dshape.dtext, - .stage.mode-decor.dtool-select .decorlayer .dshape.dtext, - .stage.mode-decor.dtool-erase .decorlayer .dshape.dtext { - /* A label is one logical decor object. SVG's visiblePainted would hit - only the ink of individual glyphs, so spaces, counters and the area - inside its selection glow behaved like empty canvas. */ - pointer-events: visiblePainted; - pointer-events: bounding-box; - } - .stage.mode-decor.dtool-text .decorlayer .dshape.dtext { - cursor: text; - } - /* Erasing a hairline must not require pixel-perfect aim. The duplicate - geometry is invisible and exists only while Erase is active. A - non-scaling stroke keeps the target comfortably wide at every zoom. */ - .decorlayer .derasehit { - fill: none; - stroke: transparent; - stroke-width: 16px; - stroke-linecap: round; - stroke-linejoin: round; - vector-effect: non-scaling-stroke; - pointer-events: none; - } - .stage.mode-decor.dtool-erase .decorlayer .derasehit { - pointer-events: stroke; - } - /* A dashed line must remain selectable across its gaps. This proxy also - makes the only entry to line-style properties — double click in Select — - practical for hairlines at every zoom. */ - .decorlayer .dselecthit { - fill: none; - stroke: transparent; - stroke-width: 16px; - stroke-linecap: round; - vector-effect: non-scaling-stroke; - pointer-events: none; - } - .stage.mode-decor.dtool-select .decorlayer .dselecthit { - pointer-events: stroke; - cursor: move; - } - .decorlayer .dsel { - filter: drop-shadow(0 0 3px var(--hp-accent)); - } - .decorlayer .ddraft { - stroke-dasharray: - calc(6px * var(--hp-cell-visual-scale, 1)) - calc(5px * var(--hp-cell-visual-scale, 1)); - pointer-events: none; - } - .decorlayer text { - font-weight: 600; - user-select: none; - dominant-baseline: middle; - text-anchor: middle; - } - .stage.mode-decor { - outline: 2px solid #26a69a; - outline-offset: -2px; - } - /* backdrop transform frame (docs/BACKDROP.md §2). Editor chrome: the - outline never takes a pointer, the four corner handles do — and they are - finger-sized (r = 2 % of the visible view), because this is dragged on a - tablet as often as with a mouse. */ - .bdframe .bdbox { - fill: none; - stroke: var(--hp-accent); - stroke-width: 2; - stroke-dasharray: 10 7; - vector-effect: non-scaling-stroke; - pointer-events: none; - opacity: 0.9; - } - /* the HIT circle: invisible, finger-sized, the only thing that takes a - pointer. The visible bead is .bdknob, a quarter of its radius — the same - visual/hit split .dthandle + .dtknob and .rszhandle + .rszicon use. */ - .bdframe .bdhandle { - fill: transparent; - stroke: none; - pointer-events: all; - touch-action: none; - } - .bdframe .bdknob { - fill: var(--hp-accent); - stroke: #fff; - stroke-width: 1; - vector-effect: non-scaling-stroke; - pointer-events: none; - } - :host([data-pointer-hover]) .bdframe .bdhandle:hover + .bdknob { - fill: #fff; - stroke: var(--hp-accent); - } - .bdframe .bd-nwse { cursor: nwse-resize; } - .bdframe .bd-nesw { cursor: nesw-resize; } - /* the picture itself is the drag target for a move (grab, then grabbing) */ - .stage.mode-decor.bdgrab { cursor: grab; } - .stage.mode-decor.bdgrabbing, - .stage.mode-decor.bdgrabbing .bdframe .bdhandle { cursor: grabbing; } - .measurelabel.bdmeasure { - transform: translate(-50%, -50%); - border: 1px solid var(--hp-accent); - } - /* the selected decor object's frame — same chrome rules as the backdrop's: - the outline never takes a pointer, the handles always do, and they are - finger-sized because this is dragged on a tablet too */ - .dtframe .dtbox { - fill: none; - stroke: var(--hp-accent); - stroke-width: 1.5; - stroke-dasharray: 7 5; - vector-effect: non-scaling-stroke; - pointer-events: none; - opacity: 0.85; - } - .dtframe .dtstem, - .bdframe .dtstem { - stroke: var(--hp-accent); - stroke-width: 1.5; - vector-effect: non-scaling-stroke; - pointer-events: none; - opacity: 0.85; - } - /* the HIT circle: invisible, finger-sized, the only thing that takes a - pointer. The visible bead is .dtknob, a quarter of its radius — the - same visual/hit split .rszhandle + .rszicon use. */ - .dtframe .dthandle { - fill: transparent; - stroke: none; - pointer-events: all; - touch-action: none; - } - .dtframe .dtknob { - fill: var(--hp-accent); - stroke: #fff; - stroke-width: 1; - vector-effect: non-scaling-stroke; - pointer-events: none; - } - :host([data-pointer-hover]) .dtframe .dthandle:hover + .dtknob { - fill: #fff; - stroke: var(--hp-accent); - } - .dtframe .dt-nwse { cursor: nwse-resize; } - .dtframe .dt-nesw { cursor: nesw-resize; } - .dtframe .dtrot { cursor: grab; } - .dtframe .dtendpoint { cursor: crosshair; } - .bdframe .dtrot { cursor: grab; } - .dtarea { - resize: vertical; - min-height: 3.4em; - font: inherit; - line-height: 1.35; - } - .stage.mode-decor.dtool-line, .stage.mode-decor.dtool-rect, - .stage.mode-decor.dtool-ellipse, .stage.mode-decor.dtool-text { - cursor: crosshair; - } - .stage.mode-decor.dtool-erase, - .stage.mode-decor.dtool-erase .decorlayer .dshape { - cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cg transform='rotate(-45 12 12)'%3E%3Crect x='7' y='2' width='10' height='18' rx='2' fill='%23fff' stroke='%23111' stroke-width='1.5'/%3E%3Cpath d='M7 13h10v5a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2z' fill='%23ff9f43' stroke='%23111' stroke-width='1.5'/%3E%3C/g%3E%3C/svg%3E") 5 22, pointer; - } - .stage.mode-decor .room, .stage.mode-decor .devlayer { pointer-events: none; } - .stage.mode-decor .oplock { pointer-events: none; } - /* Backdrop-editor de-emphasis is a shared mode-transition coordinate. - It multiplies whole presentation groups and never changes Glow source - alpha, additive blending, or the underlying resolved state. */ - .stage .room, - .stage .devlayer, - .stage .opening, - .stage .room-outline, - .stage .wallbodies, - .stage .opening-tunnels, - .stage .glow-base-layer, - .stage .glow-pools-frame, - .stage .zero-walls { - opacity: var(--hp-mode-architecture-opacity, 1); - } - .opening-preview { - opacity: 0.5; - pointer-events: none; - } - .opening-preview[data-kind="passage"] { - opacity: 1; - } - .passage-preview-cut { - fill: var(--wall-fill, #ffffff); - fill-opacity: 0.35; - stroke: none; - pointer-events: none; - } - .passage-preview-boundary { - stroke: var(--hp-open, #ff9800); - stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); - pointer-events: none; - } - .opening-preview .op-leaf, - .opening-preview .op-arc { - transition: none; - } - .opening-preview-dot { - fill: var(--hp-open, #ff9800); - pointer-events: none; - } - .opening-dimensions, - .opening-dimension { - pointer-events: none; - } - .opening-dimension-line, - .opening-dimension-tick { - fill: none; - stroke: var(--hp-open, #ff9800); - stroke-width: 1.5; - vector-effect: non-scaling-stroke; - pointer-events: none; - } - .opening-dimension-tick { - stroke-width: 2; - } - .stage.edit .room { - pointer-events: none; - } - .stage.markup { - cursor: crosshair; - } - /* room-picking stages: merge (both clicks) and split before a room is chosen */ - .stage.markup.tool-merge, - .stage.markup.tool-split.pickstage, - .stage.markup.tool-delroom { - cursor: pointer; - } - .stage.markup.tool-wallthick { cursor: default; } - .stage.markup.tool-wallthick.wallhot { cursor: pointer; } - /* Solid wall colour sits under the hatch (owner: both, not either/or). */ - .wallbody-fill { - fill: var(--wall-fill, #ffffff); - fill-opacity: var(--wall-fill-op, 1); - fill-rule: evenodd; - stroke: none; - pointer-events: none; - } - .wallbody { - fill: url(#hp-wall-hatch); - fill-rule: evenodd; - stroke: var(--room-stroke, var(--hp-muted)); - stroke-width: calc(0.6px * var(--hp-cell-visual-scale, 1)); - pointer-events: none; - } - /* Thin-on-screen: hatch collapses into noise — fill alone, stroke from fill path's sibling. */ - .wallbody.solid { - fill: none; - } - .wallthick-hover { - fill: var(--accent-color, #03a9f4); - fill-opacity: 0.38; - stroke: var(--accent-color, #03a9f4); - stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); - stroke-linejoin: round; - stroke-linecap: round; - pointer-events: none; - } - .wallthick-hover.isopen { - fill: var(--error-color, #f44336); - stroke: var(--error-color, #f44336); - } - .zero-wall { - stroke: var(--zero-wall-stroke, var(--hp-muted)); - stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); - stroke-dasharray: - calc(7px * var(--hp-cell-visual-scale, 1)) - calc(7px * var(--hp-cell-visual-scale, 1)); - stroke-linecap: butt; - pointer-events: none; - opacity: 0.9; - } - .zero-walls.solid .zero-wall { - stroke-dasharray: none; - } - /* Rooms with zero/thick stretches: the polygon's own stroke is fully off. - The trimmed .room-outline draws normal walls; View hover gets its own - top overlay after the wall bodies. */ - .room.noedge { - stroke-opacity: 0 !important; - } - /* rooms with open boundaries draw their walls as separate M..L subpaths, - so a corner between two of them is two stroke ENDS meeting: round caps - fill it in the same way a round join fills a closed contour's corner - (owner 2026-08-04 — no teeth anywhere on a room border). */ - .room-outline { - fill: none; - stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); - stroke-linejoin: round; - stroke-linecap: round; - pointer-events: none; - } - /* Plan editor: trimmed outlines use the markup blue */ - .room-outline.outlined { - stroke: rgba(62, 166, 255, 0.55); - stroke-opacity: 1; - } - /* The wash is rendered directly above the resolved room fill but below - tunnels, Glow, sun and walls. Black at 22% reproduces the old - brightness(.78) contract without filtering/promoting an SVG ancestor or - changing the room's hue. The halo/outline are rendered again late, above - the wall bodies. */ - .room-hover-fill { - fill: #000; - fill-opacity: 0.22; - pointer-events: none; - } - .room-hover-halo { - fill: none; - stroke: var(--hp-accent); - stroke-opacity: 0.28; - stroke-width: calc(8px * var(--hp-cell-visual-scale, 1)); - stroke-linejoin: round; - stroke-linecap: round; - pointer-events: none; - } - .room-hover-outline { - fill: none; - stroke: var(--hp-accent); - stroke-width: calc(3px * var(--hp-cell-visual-scale, 1)); - stroke-linejoin: round; - stroke-linecap: round; - pointer-events: none; - } - .stage.markup .room { - pointer-events: none; - } - .stage.markup .devlayer .dev { - display: none; /* in plan mode the icons do not get in the way; labels stay */ - } - /* mode frames: the edit modes are visible at a glance */ - .stage.mode-plan { - outline: 2px solid #ffc14d; - outline-offset: -2px; - } - .stage.mode-devices { - outline: 2px solid var(--hp-accent); - outline-offset: -2px; - } - .room.outlined { - stroke: rgba(62, 166, 255, 0.55); - fill: rgba(62, 166, 255, 0.06); - } - /* AFTER .outlined: same specificity — source order decides (gotcha x4) */ - .room.picked { - stroke: #ffc14d; - stroke-width: calc(3px * var(--hp-cell-visual-scale, 1)); - fill: rgba(255, 193, 77, 0.25); - } - /* Owner 2026-08-04: the grid is a HINT, not content — at full strength the - dots argued with the plan on white paper. Both levels are muted, the - hierarchy is kept (major still denser than fine). */ - .griddot { - fill: var(--hp-accent); - opacity: 0.35; - stroke: rgba(0, 0, 0, 0.35); - stroke-width: 0.4; - } - /* docs/CANVAS.md §7: every coarse node (5x/10x the live step) keeps a - bigger, more opaque dot, so zoomed far out the grid still reads as a - grid instead of a grey wash. */ - .griddot.major { - opacity: 0.5; - stroke-width: 0; - } - /* the contour being drawn in the Plan editor: each wall is its own , - so the round cap IS the corner (matches the finished .room border) */ - .seg { - stroke: var(--hp-accent); - stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); - stroke-linecap: round; - stroke-linejoin: round; - } - .pathline { - stroke: #ffc14d; - stroke-width: calc(3px * var(--hp-cell-visual-scale, 1)); - fill: none; - stroke-linecap: round; - stroke-linejoin: round; - } - .preview { - stroke: #ffc14d; - stroke-width: calc(2px * var(--hp-cell-visual-scale, 1)); - stroke-dasharray: - calc(6px * var(--hp-cell-visual-scale, 1)) - calc(5px * var(--hp-cell-visual-scale, 1)); - opacity: 0.7; - } - .active-axis { - stroke: #ffc14d; - stroke-width: 2; - stroke-linecap: round; - vector-effect: non-scaling-stroke; - pointer-events: none; - } - .active-vertex { - fill: #ffc14d; - stroke: #171006; - stroke-width: 2; - vector-effect: non-scaling-stroke; - pointer-events: none; - } - .wall-repair-preview { - stroke: #d93025; - stroke-width: 4; - stroke-linecap: round; - stroke-dasharray: 5 4; - vector-effect: non-scaling-stroke; - pointer-events: none; - } - .drawwall-preview-fill { - fill: var(--wall-fill, #ffffff); - fill-opacity: calc(var(--wall-fill-op, 1) * 0.55); - fill-rule: evenodd; - stroke: none; - pointer-events: none; - } - .drawwall-preview { - fill: url(#hp-wall-hatch); - fill-opacity: 0.55; - fill-rule: evenodd; - stroke: var(--accent-color, #03a9f4); - stroke-width: calc(0.5px * var(--hp-cell-visual-scale, 1)); - stroke-opacity: 0.7; - pointer-events: none; - } - .drawwall-zero-preview { - fill: none; - stroke: var(--accent-color, #03a9f4); - stroke-width: calc(3px * var(--hp-cell-visual-scale, 1)); - stroke-linecap: butt; - stroke-linejoin: round; - opacity: 0.72; - pointer-events: none; - } - .drawwall-zero-preview.dashed { - stroke-dasharray: - calc(7px * var(--hp-cell-visual-scale, 1)) - calc(7px * var(--hp-cell-visual-scale, 1)); - } - .vertex { - fill: #ffc14d; - stroke: #4a2800; - stroke-width: calc(1px * var(--hp-cell-visual-scale, 1)); - } - .vertex.first { - fill: #4bd28f; - stroke: #04121f; - } - .plan-snap-overlay, - .plan-snap-overlay *, - .hidden-wall-diagnostic, - .hidden-wall-diagnostic * { - pointer-events: none; - } - .plan-snap-line, - .hidden-wall-line { - fill: none; - stroke: color-mix(in srgb, var(--hp-accent) 82%, white 18%); - /* Explicit non-scaling-stroke in the SVG keeps this one screen pixel. */ - stroke-width: 1; - stroke-linecap: round; - opacity: 0.92; - } - .plan-snap-node, - .hidden-wall-node { - fill: var(--ha-card-background, var(--card-background-color, #fff)); - stroke: color-mix(in srgb, var(--hp-accent) 88%, #07131c 12%); - stroke-width: 1; - vector-effect: non-scaling-stroke; - } - .plan-snap-node.active { - fill: #ffc14d; - stroke: #171006; - stroke-width: 2; - } - .plan-snap-node.active.dynamic { - fill: #4bd28f; - stroke: #04121f; - } - .plan-snap-node.conflict { - fill: #fff; - stroke: #d93025; - stroke-width: 3; - } - @media (prefers-color-scheme: dark) { - .plan-snap-line { - stroke: color-mix(in srgb, var(--hp-accent) 72%, white 28%); - } - .hidden-wall-line { - stroke: color-mix(in srgb, var(--hp-accent) 72%, white 28%); - } - .plan-snap-node, - .hidden-wall-node { - fill: #17242c; - stroke: #9bdcf5; - } - .plan-snap-node.active { - fill: #ffc14d; - stroke: #fff4d6; - } - .plan-snap-node.active.dynamic { - fill: #4bd28f; - stroke: #eafff4; - } - } - @media (forced-colors: active) { - .plan-snap-line, - .hidden-wall-line { - stroke: CanvasText; - opacity: 1; - forced-color-adjust: auto; - } - .plan-snap-node, - .hidden-wall-node { - fill: Canvas; - stroke: CanvasText; - forced-color-adjust: auto; - } - .plan-snap-node.active, - .plan-snap-node.active.dynamic { - fill: Highlight; - stroke: HighlightText; - forced-color-adjust: auto; - } - } - /* RGB lights: the bulb takes the light's actual color */ - /* v1.52.0: the RGB tint of the icon/border is gone — a lamp's colour - lives ONLY in its glow spot (owner's rule). The ripple-color fallback - keeps using the light colour; that is set inline via --ripple-color. */ - /* Sun wedges (docs/SUN.md). The layer is present ONLY above the 3° - threshold; crossing it fades the whole layer in or out over EXACTLY - 2 s (owner 2026-08-03 — RAY_FADE_MS in src/sun.ts must match). The - geometry is untouched: this is a plain opacity animation on the group, - so overlapping wedges keep their own blending while it plays. */ - .sunlayer { - animation: hp-sunfade-in 2s linear both; - } - .sunlayer.out { - animation: hp-sunfade-out 2s linear both; - } - @media (prefers-reduced-motion: reduce) { - /* no fade at all: the rays are simply there or simply gone */ - .sunlayer, .sunlayer.out { animation: none; } - .sunlayer.out { opacity: 0; } - } - .devlayer { - position: absolute; - inset: 0; - pointer-events: none; - z-index: 6; - } - .stage.mode-devices .dev { cursor: grab; } - .stage.mode-devices .dev:active { cursor: grabbing; } - .physical-hit { - fill: transparent; - stroke: transparent; - pointer-events: none; - cursor: grab; - } - .stage.tool-select .physical-hit { pointer-events: all; } - .physical-hit:active { cursor: grabbing; } - line.physical-hit { cursor: pointer; } - .physical-hit.selected { - fill: rgba(255, 193, 77, 0.24); - stroke: transparent; - } - line.physical-hit.selected { - stroke: transparent; - } - .physical-drag { - fill: rgba(255, 193, 77, 0.38); - stroke: #ffc14d; - stroke-width: 2.5; - vector-effect: non-scaling-stroke; - pointer-events: none; - } - .physical-chrome { pointer-events: none; } - .physical-chrome .frame, - .physical-chrome .stem { - fill: none; - stroke: #ffc14d; - stroke-width: 2; - vector-effect: non-scaling-stroke; - } - .physical-chrome .frame { fill: rgba(255, 193, 77, 0.22); } - .physical-chrome polyline.frame { fill: none; } - .physical-chrome .move-dot { - fill: #ffc14d; - stroke: #24262d; - stroke-width: 1.5; - vector-effect: non-scaling-stroke; - } - .physical-chrome .rotate-handle { - fill: #24262d; - stroke: #ffc14d; - stroke-width: 2.5; - vector-effect: non-scaling-stroke; - pointer-events: all; - cursor: crosshair; - } - .drawwall.invalid input { border-color: var(--error-color, #db4437); } - .drawwall .rangehint { margin-inline-start: 4px; font-size: 0.78em; opacity: 0.72; } - .drawwall.invalid .rangehint { color: var(--error-color, #db4437); opacity: 1; } - .stage.markup.tool-partition, - .stage.markup.tool-column { cursor: crosshair; } - /* room resize tool (docs/RESIZE.md) */ - /* wall handle: invisible finger-sized hit circle (HP-1550-04 hit priority - kept), the visible glyph lives in the sibling .rszicon */ - .rszhandle { - fill: transparent; - stroke: none; - pointer-events: all; - cursor: grab; - touch-action: none; - } - .rszhandle:active { cursor: grabbing; } - .rszhandle.disabled, - .rszhandle.disabled:active { - cursor: not-allowed; - } - /* wall-with-arrows glyph: accent ink over a bg halo, readable on any plan */ - .rszicon { pointer-events: none; } - .rszicon path { - fill: none; - stroke-linecap: round; - stroke-linejoin: round; - vector-effect: non-scaling-stroke; - } - .rszhalo { stroke: var(--hp-bg); stroke-width: 6; } - .rszink { stroke: var(--hp-accent); stroke-width: 2; } - :host([data-pointer-hover]) .rszhandle:hover + .rszicon .rszink { stroke-width: 3; } - .rszicon.disabled { opacity: 0.38; } - :host([data-pointer-hover]) .rszhandle.disabled:hover + .rszicon .rszink { stroke-width: 2; } - .rszmeasurelayer, - .rszmeasurelayer * { pointer-events: none; } - .rszmeasurehalo, - .rszmeasureink, - .rszleader { - fill: none; - stroke-linecap: round; - vector-effect: non-scaling-stroke; - } - .rszmeasurehalo { - stroke: var(--hp-bg); - stroke-width: 7px; - opacity: 0.9; - } - .rszmeasureink { - stroke: var(--hp-accent); - stroke-width: 3px; - } - .rszleader { - stroke: var(--hp-accent); - stroke-width: 2px; - opacity: 0.95; - } - /* the decor draft badge rides the MIDDLE of the shape, so it is centred - horizontally and lifted clear of the line instead of trailing the - cursor the way a wall badge does (owner 2026-08-04) */ - .measurelabel.dmeasure { - transform: translate(-50%, -160%); - } - .measurelabel.rszarea { - transform: translate( - calc(-50% + var(--rsz-label-x, 0px)), - calc(-50% + var(--rsz-label-y, 0px)) - ); - background: rgba(0, 0, 0, 0.6); - border: 1px solid var(--hp-accent); - } - /* width and depth of a piece of furniture while its corner is dragged — - centred on the edge they measure (docs/FURNITURE.md §6) */ - .measurelabel.furnmeasure { - transform: translate(-50%, -50%); - border: 1px solid var(--hp-accent); - } - .alignmsg { margin: 0 0 8px; font-size: 13px; line-height: 1.45; } -`,o` - /* Unified device activity: alarm, short event and continuous state all - share this one renderer. No static rings are allowed. */ - .dev ha-icon { - position: relative; - z-index: 1; - } - .device-pulse { - position: absolute; - left: 50%; - top: 50%; - width: var(--dev-size); - height: var(--dev-size); - transform: translate(-50%, -50%); - pointer-events: none; - z-index: 0; - } - .device-pulse i { - position: absolute; - inset: 0; - border-radius: 50%; - border: 2px solid var(--ripple-color, var(--hp-accent)); - opacity: 0; - } - /* A witnessed edge: exactly three waves over the 3.3 s runtime window. */ - .device-pulse.short i { - animation: hp-pulse-short 1.1s cubic-bezier(.22,.61,.36,1) 1 forwards; - } - .device-pulse.short i:nth-child(2) { animation-delay: 1.1s; } - .device-pulse.short i:nth-child(3) { animation-delay: 2.2s; } - .device-pulse.short.gen2 i { animation-name: hp-pulse-short-b; } - .device-pulse.continuous i:nth-child(n + 2), - .device-pulse.alarm i:nth-child(n + 3) { display: none; } - .device-pulse.continuous i:first-child { - animation: hp-pulse-continuous 3.6s cubic-bezier(.45,.05,.55,.95) infinite; - } - .device-pulse.alarm i { - border-width: 3px; - border-color: #F0410C; - animation: hp-pulse-alarm 2.4s cubic-bezier(.22,.61,.36,1) infinite; - } - .device-pulse.alarm i:nth-child(2) { animation-delay: 1.2s; } - .activity-dot { - position: absolute; - right: 8%; - bottom: 8%; - width: 18%; - height: 18%; - min-width: 4px; - min-height: 4px; - border-radius: 50%; - background: var(--ripple-color, var(--hp-accent)); - border: 1px solid color-mix(in srgb, var(--hp-bg) 82%, transparent); - pointer-events: none; - z-index: 3; - } - .kioskdots { - position: absolute; - left: 50%; - bottom: var(--sp-5); - transform: translateX(-50%); - display: flex; - gap: var(--sp-4); - z-index: 5; - pointer-events: none; - } - .kdot { - width: 9px; - height: 9px; - border-radius: 50%; - background: var(--hp-muted); - opacity: 0.55; - } - .kdot.on { background: var(--hp-accent); opacity: 1; } - .dev.valonly { - /* Saved coordinates remain the centre of a Text shell. The shell may - expand, while the invisible anchor stays one core diameter wide. */ - width: var(--dev-size, var(--icon-size, 2.5cqw)); - } - .dev.valonly .device-core { - width: max-content; - min-width: var(--dev-size, var(--icon-size, 2.5cqw)); - padding-inline: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.16); - border-radius: calc(var(--dev-size, var(--icon-size, 2.5cqw)) / 2); - } - .dev .valtext { - overflow: visible; - font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * var(--value-font-scale, .45)); - font-weight: 600; - white-space: nowrap; - } - @media (prefers-reduced-motion: reduce) { - .device-pulse { display: none; } - } - .dev .newdot { - position: absolute; - top: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * -0.12); - right: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * -0.12); - width: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.34); - height: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.34); - border-radius: 50%; - background: #f0301f; - border: 2px solid var(--card-background-color, var(--hp-bg)); - pointer-events: none; - z-index: 2; - } - .dev { - position: absolute; - /* The surface resolves compatibility icon_size units once. The shared - face receives an effective base and only applies the per-device size. */ - --dev-size: calc(var(--device-base-size, 2.25cqw) * var(--dev-scale, 1)); - /* 101.5/80 is the package shell/core ratio, including its stroke. */ - --device-shell-size: calc(var(--dev-size) * 1.26875); - --device-shell-inset: calc(var(--dev-size) * 0.134375); - --device-shell-stroke-ratio: 0.01875; - --device-shell-border-width: max(1px, calc(var(--dev-size) * var(--device-shell-stroke-ratio))); - --device-core-bg: var(--card-background-color, #fff); - --device-core-fg: var(--primary-text-color, #252525); - --device-core-bg: light-dark(#fff, #252525); - --device-core-fg: light-dark(#252525, #fff); - --device-face-bg: var(--device-core-bg); - --device-face-fg: var(--device-core-fg); - --device-shell-base-stroke: light-dark(#BCBCBC, rgb(37 37 37 / 75%)); - --device-shell-stroke: var(--device-shell-base-stroke); - --device-shell-shadow: - 0 calc(var(--dev-size) * .025) calc(var(--dev-size) * .05) rgb(37 40 45 / 12%), - 0 calc(var(--dev-size) * .1) calc(var(--dev-size) * .175) - calc(var(--dev-size) * -.025) rgb(37 40 45 / 18%); - --device-core-inset-shadow: 0 0 0 0 transparent; - --device-ring-color: transparent; - --device-ring-width: 0px; - /* The saved point is the exact centre of the icon core. */ - width: var(--dev-size); - height: var(--dev-size); - margin: calc(var(--dev-size) / -2) 0 0 calc(var(--dev-size) / -2); - border: 0; - background: transparent; - display: block; - color: var(--device-core-fg); - cursor: pointer; - pointer-events: auto; - transition: opacity 0.2s; - box-shadow: none; - outline: none; - z-index: 2; - } - .dev.theme-light { - --device-core-bg: #fff; - --device-core-fg: #000; - --device-shell-base-stroke: #BCBCBC; - --device-shell-stroke: var(--device-shell-base-stroke); - --device-shell-shadow: - 0 calc(var(--dev-size) * .025) calc(var(--dev-size) * .05) rgb(37 40 45 / 12%), - 0 calc(var(--dev-size) * .1) calc(var(--dev-size) * .175) - calc(var(--dev-size) * -.025) rgb(37 40 45 / 18%); - --device-core-inset-shadow: 0 0 0 0 transparent; - } - .dev.theme-dark { - --device-core-bg: #252525; - --device-core-fg: #fff; - --device-shell-base-stroke: rgb(37 37 37 / 75%); - --device-shell-stroke: var(--device-shell-base-stroke); - --device-shell-shadow: - 0 calc(var(--dev-size) * .025) calc(var(--dev-size) * .0375) rgb(37 40 45 / 12%), - 0 calc(var(--dev-size) * .1) calc(var(--dev-size) * .175) - calc(var(--dev-size) * -.025) rgb(37 40 45 / 18%); - --device-core-inset-shadow: - inset 0 calc(var(--dev-size) * 0.0125) calc(var(--dev-size) * 0.0125) rgb(255 255 255 / 70%); - } - .dev::before { - content: ''; - position: absolute; - left: 50%; - top: 50%; - width: max(44px, var(--device-shell-size)); - height: max(44px, var(--device-shell-size)); - transform: translate(-50%, -50%); - border-radius: 50%; - pointer-events: auto; - z-index: 3; - } - .device-shell { - position: absolute; - box-sizing: border-box; - display: flex; - align-items: center; - gap: calc(var(--dev-size) * 0.1); - padding: 0; - min-width: var(--dev-size); - min-height: var(--dev-size); - border: 0; - background: transparent; - box-shadow: none; - transition: opacity .2s; - pointer-events: none; - } - .device-shell-frame { - position: absolute; - z-index: 0; - box-sizing: border-box; - inset: calc(var(--device-shell-inset) / -1); - border: var(--device-shell-border-width) solid var(--device-shell-stroke); - /* A saturating radius is resolved from the final border box. Using a - second fractional length here lets border-box and radius quantise on - different sides of a device pixel at some zoom/DPR combinations. */ - border-radius: 9999px; - background: transparent; - box-shadow: var(--device-shell-shadow); - transition: border-color .15s, box-shadow .15s, opacity .2s; - pointer-events: auto; - /* Normative production fallback: never add a per-marker backdrop blur. */ - backdrop-filter: none; - } - .device-shell:not(.with-values) { - left: 0; - top: 0; - } - .device-shell:not(.with-values):not(.text-shell) .device-shell-frame { - border-radius: 50%; - } - .device-shell.with-values.pos-right { - left: 0; - top: 50%; - transform: translateY(-50%); - } - .device-shell.with-values.pos-left { - right: 0; - top: 50%; - transform: translateY(-50%); - flex-direction: row-reverse; - } - .device-shell.with-values.pos-bottom { - left: 50%; - top: 0; - transform: translateX(-50%); - flex-direction: column; - } - .device-shell.with-values.pos-top { - left: 50%; - bottom: 0; - transform: translateX(-50%); - flex-direction: column-reverse; - } - .device-core { - position: relative; - z-index: 1; - box-sizing: border-box; - flex: 0 0 auto; - width: var(--dev-size); - height: var(--dev-size); - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - background: var(--device-face-bg); - color: var(--device-face-fg); - box-shadow: - var(--device-core-inset-shadow), - 0 0 0 var(--device-ring-width) var(--device-ring-color); - line-height: 0; - transition: background .15s, color .15s, box-shadow .15s, opacity .2s; - pointer-events: none; - } - .device-sections { - display: flex; - flex-direction: column; - align-items: stretch; - justify-content: center; - gap: calc(var(--dev-size) * .08); - min-width: 0; - pointer-events: none; - } - .device-shell.pos-top .device-sections, - .device-shell.pos-bottom .device-sections { - flex-direction: row; - } - .dev ha-icon { - /* from --dev-size, NOT --icon-size: the per-device size multiplier must - scale the GLYPH with its badge. Pinned to the base size, "make this - icon bigger" grew an empty box around a default-size glyph (user - report via the owner, 2026-07-29). */ - --mdc-icon-size: calc(var(--dev-size, var(--device-base-size, 2.25cqw)) * 0.55); - display: flex; - align-items: center; - justify-content: center; - line-height: 0; - } - .dev.on { - --device-face-bg: #F0A00C; - --device-face-fg: light-dark(#fff, #252525); - --device-shell-stroke: #F0A00C; - } - .dev.theme-light.on { --device-face-fg: #fff; } - .dev.theme-dark.on { - --device-face-fg: #252525; - --device-shell-stroke-ratio: .0375; - } - .dev.open { - --device-face-bg: var(--hp-open); - --device-face-fg: light-dark(#fff, #252525); - } - .dev.theme-light.open { --device-face-fg: #fff; } - .dev.theme-dark.open { --device-face-fg: #252525; } - .dev.lock-locked { - --device-face-bg: #66D17A; - --device-face-fg: light-dark(#fff, #252525); - --device-shell-stroke: #66D17A; - } - .dev.theme-light.lock-locked { --device-face-fg: #fff; } - .dev.theme-dark.lock-locked { - --device-face-fg: #252525; - --device-shell-stroke-ratio: .025; - } - .dev.lock-unlocked { - --device-face-bg: #F0410C; - --device-face-fg: light-dark(#fff, #252525); - --device-shell-stroke: #F0410C; - } - .dev.theme-light.lock-unlocked { --device-face-fg: #fff; } - .dev.theme-dark.lock-unlocked { - --device-face-fg: #252525; - --device-shell-stroke-ratio: .025; - } - /* Interaction wins ordinary state colours. Alarm keeps priority through - the more-specific rule below. Unavailable has no visual hover. */ - :host([data-pointer-hover]) .dev:not(.unavail):hover { - --device-face-bg: #0C82F0; - --device-face-fg: light-dark(#fff, #252525); - --device-shell-stroke: var(--device-shell-base-stroke); - } - :host([data-pointer-hover]) .dev.theme-light:not(.unavail):hover { --device-face-fg: #fff; } - :host([data-pointer-hover]) .dev.theme-dark:not(.unavail):hover { --device-face-fg: #252525; } - :host([data-pointer-hover]) .dev:hover, - .dev:focus-visible { z-index: 5; } - .dev.unavail { - opacity: 0.35; - --device-face-bg: #B5BAC1; - --device-shell-stroke: var(--device-shell-base-stroke); - } - .dev.virtual .device-shell-frame { - border-style: dashed; - } - /* "hide from plan" flag, shown only in the device editor with the - "show hidden devices" toggle on (docs/FILTERING.md). BLUE, so a hidden - device cannot be mistaken for an unavailable one (translucent dark) — - and no live-state paint at all: a ghost is configuration, not status - (owner's request). */ - .dev.ghost { - opacity: 0.6; - color: var(--hp-accent); - } - .dev.ghost .device-shell-frame { - border-style: dashed; - border-color: var(--hp-accent); - box-shadow: none; - } - .dev.ghost .device-core { - background: rgba(62, 166, 255, 0.22); /* fallback for old WebViews */ - background: color-mix(in srgb, var(--hp-accent) 30%, var(--card-background-color, #1c2530)); - color: var(--hp-accent); - } - /* HA-disabled is not a user hide: neutral grey + a power-off badge keeps - the two ghosts distinguishable without relying on colour alone. */ - .dev.ghost.ha-disabled { - opacity: 0.62; - color: var(--secondary-text-color, #9aa0aa); - } - .dev.ghost.ha-disabled .device-shell-frame { - border-color: var(--secondary-text-color, #9aa0aa); - } - .dev.ghost.ha-disabled .device-core { - background: rgba(120, 124, 134, 0.2); - background: color-mix(in srgb, var(--secondary-text-color, #9aa0aa) 24%, var(--card-background-color, #1c2530)); - color: var(--secondary-text-color, #9aa0aa); - } - .dev .habadge { - position: absolute; - right: -18%; - bottom: -18%; - display: grid; - place-items: center; - width: 46%; - height: 46%; - min-width: 12px; - min-height: 12px; - border-radius: 50%; - background: var(--card-background-color, #242832); - border: 1px solid currentColor; - } - .dev .habadge ha-icon { width: 72%; height: 72%; } - .dev.sel { - --device-ring-color: #F0A00C; - --device-ring-width: calc(var(--dev-size) * 0.0375); - } - .dev:focus-visible { - --device-ring-color: #0C82F0; - --device-ring-width: calc(var(--dev-size) * 0.0375); - } - .dev:not(.on):not(.open):not(.alarm):not(.lock-locked):not(.lock-unlocked):not(.unavail):focus-visible { - --device-face-fg: #0C82F0; - } - /* Alert stays above focus, selection, hover and ordinary semantic paint. */ - .dev.alarm, - :host([data-pointer-hover]) .dev.alarm:hover, - .dev.alarm:focus-visible { - --device-face-bg: #F0410C; - --device-face-fg: light-dark(#fff, #252525); - --device-shell-stroke: #F0410C; - } - .dev.theme-light.alarm { --device-face-fg: #fff; } - .dev.theme-dark.alarm { - --device-face-fg: #252525; - --device-shell-stroke-ratio: .025; - } - .dev .value-badge { - position: relative; - z-index: 2; - box-sizing: border-box; - width: max-content; - min-width: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * .7875); - height: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * .7875); - display: flex; - align-items: center; - justify-content: center; - overflow: visible; - background: var(--device-core-bg); - border: 0; - border-radius: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.39375); - padding: 0 calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.14); - font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * var(--value-font-scale, .45)); - font-weight: 600; - line-height: 1; - color: var(--device-core-fg); - white-space: nowrap; - pointer-events: none; - } - .dev .value-badge.unavailable, - .dev .value-badge.missing { opacity: 0.66; } - .dev .lqi { - position: absolute; - top: calc(50% + var(--device-shell-size) / 2 + var(--dev-size) * .05); - left: 50%; - transform: translateX(-50%); - margin-top: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.05); - font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.38); - font-weight: 600; - line-height: 1; - text-shadow: 0 0 3px rgba(0, 0, 0, 0.9), 0 0 2px rgba(0, 0, 0, 0.9); - white-space: nowrap; - pointer-events: none; - } - .dev .lqi.below-value-badge { - margin-top: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.8875); - } - .temprange { - display: inline-flex; - align-items: center; - gap: var(--sp-3); - margin-left: auto; - color: var(--hp-muted); - font-size: var(--fs-s); - } - /* live vacuum: a round puck, no badge plate, soft pulse (docs/VACUUM.md) */ - .vacpuck { - position: absolute; - /* the base badge, but round and 20% smaller — the owner's wording: - «иконка похожа на иконку базы, только круглая и чуть меньше» */ - --puck-size: calc(var(--device-base-size, 2.25cqw) * 0.8); - width: var(--puck-size); - height: var(--puck-size); - border-radius: 50%; - transform: translate(-50%, -50%); - --vac-core-bg: light-dark(#fff, #252525); - --vac-core-fg: light-dark(#252525, #fff); - background: var(--vac-core-bg); - border: 1px solid #BCBCBC; - box-shadow: - 0 1px 2px rgb(37 40 45 / 12%), - 0 4px 8px -1.07px rgb(37 40 45 / 18%); - color: var(--vac-core-fg); - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - z-index: 6; - /* glide between sparse position updates; .jump disables it so the robot - never appears to drive through walls after a data gap */ - transition: left 1.2s linear, top 1.2s linear; - animation: vacpulse 2.2s ease-out infinite; - } - .vacpuck.theme-light { --vac-core-bg: #fff; --vac-core-fg: #252525; } - .vacpuck.theme-dark { --vac-core-bg: #252525; --vac-core-fg: #fff; } - .vacpuck.jump { transition: none; } - .vacpuck.stale { opacity: 0.45; animation: none; } - .vacpuck ha-icon { - --mdc-icon-size: calc(var(--puck-size) * 0.68); - color: var(--vac-core-fg); - /* same centering recipe as .dev ha-icon: without flex + line-height 0 - the glyph sits on its text baseline and appears to float around the - circle (owner report 2026-07-31) */ - display: flex; - align-items: center; - justify-content: center; - line-height: 0; - width: var(--mdc-icon-size); - height: var(--mdc-icon-size); - } - @keyframes vacpulse { - 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--hp-accent) 45%, transparent); } - 70% { box-shadow: 0 0 0 12px transparent; } - 100% { box-shadow: 0 0 0 0 transparent; } - } - @media (prefers-reduced-motion: reduce) { - .vacpuck { animation: none; } - } - .vactrail { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - pointer-events: none; - z-index: 5; - overflow: visible; - } - .vactrail polyline, - .vactrail path { - fill: none; - stroke-linejoin: round; - stroke-linecap: round; - vector-effect: non-scaling-stroke; - } - /* dark halo + light core: neutral, and one of the two always contrasts - with whatever fill is underneath */ - .vactrail g.prev { opacity: 0.4; } - .vactrail .case { - stroke: rgba(0, 0, 0, 0.4); - stroke-width: 2.25; - } - .vactrail .core { - stroke: rgba(255, 255, 255, 0.82); - stroke-width: 0.9; - } - .devicepreview-empty { - min-height: 82px; - margin: 4px 0 14px; - padding: var(--sp-4); - display: flex; - align-items: center; - justify-content: center; - gap: var(--sp-3); - border: 1px dashed var(--hp-line); - border-radius: var(--rad-m); - color: var(--hp-muted); - text-align: center; - } -`,o` - .hdr { - position: sticky; - top: var(--header-height, 56px); - z-index: 20; - background: var(--card-background-color, var(--hp-bg)); - border-radius: var(--ha-card-border-radius, 12px) var(--ha-card-border-radius, 12px) 0 0; - } - .tabs { - display: flex; - gap: var(--sp-2); - background: rgba(127, 127, 127, 0.12); - padding: var(--sp-2); - border-radius: var(--rad-l); - flex-wrap: wrap; - } - .tab { - border: 0; - background: transparent; - color: var(--hp-muted); - display: inline-flex; - align-items: center; - padding: var(--sp-3) var(--sp-5); - border-radius: var(--rad-m); - font-size: var(--fs-m); - font-weight: 600; - cursor: pointer; - transition: 0.15s; - font-family: inherit; - } - :host([data-pointer-hover]) .tab:hover { - color: var(--hp-txt); - } - .tab.active { - background: var(--hp-accent); - color: var(--text-primary-color, #fff); - } - /* docs/CANVAS.md §4.1: objects an order of magnitude away from the plan - do not decide the opening view — one quiet chip says so and offers to - take them in. Never a modal (owner). */ - .farhint { - position: absolute; - left: 50%; - transform: translateX(-50%); - bottom: var(--sp-4); - z-index: 12; - display: flex; - align-items: center; - gap: var(--sp-2); - max-width: calc(100% - var(--sp-8)); - background: var(--card-background-color, var(--hp-bg)); - opacity: 0.94; - color: var(--hp-txt); - border: 1px solid var(--divider-color, #33404d); - border-radius: var(--rad-m); - padding: var(--sp-1) var(--sp-3); - font-size: var(--fs-s); - } - .farhint ha-icon { - --mdc-icon-size: 18px; - color: var(--hp-warn, #e2a03f); - flex: none; - } - .farhint span { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - .dropbtn { - display: flex; - align-items: center; - gap: var(--sp-4); - width: 100%; - text-align: left; - border: 1px solid var(--hp-muted); - border-radius: var(--rad-m); - background: transparent; - color: var(--hp-txt); - padding: var(--sp-4) 10px; - cursor: pointer; - font-family: inherit; - font-size: var(--fs-m); - margin-top: var(--sp-3); - } - .dropbtn .ref { color: var(--hp-muted); font-size: var(--fs-s); margin-left: auto; } - .dropbtn ha-icon { --mdc-icon-size: 18px; margin-left: var(--sp-2); } - .dropbtn.open { border-color: var(--hp-accent); } - .droppanel { - border: 1px solid var(--hp-accent); - border-top: none; - border-radius: 0 0 var(--rad-m) var(--rad-m); - padding: var(--sp-3); - margin-top: -4px; - } - .hdr.kioskhide { display: none; } - .decorbar .dcolor { - width: 30px; height: 26px; padding: 0; border: none; background: none; cursor: pointer; - } - .decorbar .dwidth { - font-family: inherit; font-size: var(--fs-s); border-radius: var(--rad-s); - background: var(--hp-bg2, transparent); color: var(--hp-txt); border: 1px solid var(--hp-muted); - padding: var(--sp-2) var(--sp-3); - } - .decorbar .bdhint { - font-size: var(--fs-s); - color: var(--hp-muted); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - .decorbar .dfill { - display: inline-flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-s); cursor: pointer; - } - .decorbar .dfill input[type="checkbox"] { - width: 16px; - height: 16px; - flex: none; - margin: 0; - padding: 0; - } - .decorbar hp-color-opacity { flex: 0 0 auto; } - .modes { - display: inline-flex; - gap: var(--sp-1); - background: rgba(127, 127, 127, 0.12); - border-radius: var(--rad-l); - padding: var(--sp-2); - } - .modetab { - display: inline-flex; - align-items: center; - gap: var(--sp-3); - border: 0; - background: transparent; - color: var(--hp-muted); - padding: var(--sp-3) 10px; /* 10px h-padding kept: +2px would wrap the header modes row */ - border-radius: var(--rad-m); - font-size: var(--fs-m); - font-weight: 600; - cursor: pointer; - font-family: inherit; - transition: background-color 0.14s ease, color 0.14s ease, transform 0.14s ease; - } - .modetab:active { transform: scale(0.97); } - .modetab ha-icon { --mdc-icon-size: 15px; } - /* issue #220: a tab can be dragged to a new position in the editors */ - .tab[data-reorderable] { cursor: grab; } - .tab.dragging { cursor: grabbing; opacity: 0.55; } - .tab.drop-before { box-shadow: inset 2px 0 0 0 var(--primary-color, #03a9f4); } - .tab.drop-after { box-shadow: inset -2px 0 0 0 var(--primary-color, #03a9f4); } - .modetab .closex { - --mdc-icon-size: 13px; - box-sizing: border-box; - display: inline-flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - min-width: 24px; - min-height: 24px; - /* Keep the old 13 × 13 flex footprint (+2 px left margin) while the - pointer target grows around it. Header width, height and wrapping do - not move; only near-misses that used to hit the active tab reach X. */ - margin: -5.5px -5.5px -5.5px -3.5px; - opacity: 0.75; - cursor: pointer; - border-radius: var(--rad-s); - } - :host([data-pointer-hover]) .modetab .closex:hover { opacity: 1; } - .editbar .barclose { - box-sizing: border-box; - width: 40px; - height: 40px; - padding: 0; - margin: 0; - min-width: 40px; - min-height: 40px; - justify-content: center; - gap: 0; - line-height: 0; - } - .editbar .barclose ha-icon { flex: none; margin: 0; } - .modetab.active { - background: var(--hp-accent); - color: var(--text-primary-color, #fff); - } - @media (max-width: 720px) { - .modetab .ml { display: none; } - } - .editbar { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - align-items: stretch; - border-bottom: 1px solid var(--hp-line); - font-size: var(--fs-m); - } - .editbar-tools { - display: flex; - align-items: center; - gap: 10px; - padding: var(--sp-4) var(--sp-5); - flex-wrap: wrap; - min-width: 0; - outline: none; - } - .editbar-end { - display: flex; - align-items: center; - padding: var(--sp-4) var(--sp-5); - border-inline-start: 1px solid var(--hp-line); - background: var(--card-background-color, var(--hp-bg)); - position: relative; - z-index: 2; - } - .tab .tabedit { - --mdc-icon-size: 13px; - display: inline-flex; - align-items: center; - margin-left: var(--sp-3); - opacity: 0.4; - } - :host([data-pointer-hover]) .tab:hover .tabedit { - opacity: 0.9; - } - .tab.tabadd { - padding: var(--sp-3) var(--sp-4); - } - .tab.tabadd ha-icon { - --mdc-icon-size: 15px; - } - .rhint { - font-size: var(--fs-s); - color: var(--hp-muted); - margin-bottom: var(--sp-3); - } - .togglehint { - overflow-wrap: anywhere; - } - .togglehint > div + div { - margin-top: var(--sp-1); - } - /* Keep the last editor bar mounted while the row collapses. This makes - both entering and leaving an editor change the card geometry gradually; - the header ResizeObserver keeps the stage fitted throughout. */ - .editorchrome { - display: block; - height: 0; - opacity: 0; - visibility: hidden; - overflow: hidden; - } - .editorchrome:not(.open) { - pointer-events: none; - } - .editorchrome.open { - height: auto; - opacity: 1; - visibility: visible; - overflow: visible; - } - .editorchrome.transitioning { - overflow: hidden; - will-change: height; - pointer-events: none; - } - /* The toolbar is already visible while its height is interpolating. Its - explicit navigation control must remain usable even though all editing - tools stay frozen until the transition settles. */ - .editorchrome.transitioning .barclose { - pointer-events: auto; - } - .editorchrome-inner { - min-height: 0; - transform-origin: top center; - } - .editorchrome.transitioning .editorchrome-inner { - will-change: opacity; - } - @media (prefers-reduced-motion: reduce) { - .modetab { transition: none; } - } - .editbar .warn { - color: #ffc14d; - } - .editbar .sname { - font-weight: 600; - } - .editbar input { - width: 74px; - background: transparent; - border: 1px solid var(--hp-line); - color: var(--hp-txt); - border-radius: var(--rad-s); - padding: var(--sp-3) var(--sp-4); - font-size: var(--fs-m); - } - .editbar label, - .editbar .hint { - color: var(--hp-muted); - font-size: var(--fs-s); - } - .editbar .drawwall { - display: inline-flex; - align-items: center; - gap: var(--sp-3); - white-space: nowrap; - } - .editbar .wallsgroup { - display: inline-flex; - align-items: center; - gap: 10px; - white-space: nowrap; - } - .editbar .drawwall input { - width: 4.2em; - } - .editbar .drawwall .opl { - color: var(--hp-muted); - font-size: var(--fs-s); - } - .menuwrap { - position: fixed; - inset: 0; - z-index: 80; - } - .menu { - position: fixed; - background: var(--hp-bg); - border: 1px solid var(--hp-accent); - border-radius: var(--rad-l); - box-shadow: var(--shadow-2); - min-width: 210px; - max-width: 300px; - overflow: hidden; - transform: translate(0, 8px); - } - .menu .hd { - padding: var(--sp-4) var(--sp-5); - font-weight: 600; - font-size: var(--fs-m); - border-bottom: 1px solid var(--hp-line); - display: flex; - align-items: center; - gap: var(--sp-3); - } - .menu .hd ha-icon, - .menu .it.all ha-icon { - color: var(--hp-accent); - --mdc-icon-size: 16px; - } - .menu .it { - padding: var(--sp-4) var(--sp-5); - font-size: var(--fs-m); - cursor: pointer; - display: flex; - align-items: center; - gap: var(--sp-4); - } - .menu .it ha-icon { - --mdc-icon-size: 16px; - color: var(--hp-muted); - } - :host([data-pointer-hover]) .menu .it:hover { - background: rgba(127, 127, 127, 0.15); - } - .menu .it.all { - color: var(--hp-accent); - font-weight: 600; - } - .tip { - position: fixed; - pointer-events: none; - background: var(--hp-bg); - border: 1px solid var(--hp-accent); - color: var(--hp-txt); - padding: var(--sp-3) 10px; - border-radius: var(--rad-m); - font-size: var(--fs-m); - box-shadow: var(--shadow-2); - z-index: 60; - max-width: 260px; - } - .tip .m { - color: var(--hp-muted); - font-size: var(--fs-s); - display: block; - } -`,o` - .head { - display: flex; - align-items: center; - gap: 10px; - padding: 10px var(--sp-5); - border-bottom: 1px solid var(--hp-line); - flex-wrap: wrap; - } - .title { - font-size: var(--fs-l); - font-weight: 600; - display: flex; - align-items: center; - gap: var(--sp-3); - white-space: nowrap; - } - .title ha-icon { - color: var(--hp-accent); - --mdc-icon-size: 18px; - } - @media (max-width: 620px) { - .head { gap: var(--sp-3); padding: var(--sp-4) 10px; } - .head .count { display: none; } - .head .title { font-size: var(--fs-m); } - } - .count { - font-size: var(--fs-s); - color: var(--hp-muted); - } - .btn { - display: inline-flex; - align-items: center; - gap: var(--sp-3); - border: 1px solid var(--hp-line); - background: transparent; - color: var(--hp-txt); - padding: var(--sp-3) 10px; - border-radius: var(--rad-m); - cursor: pointer; - transition: 0.15s; - font-family: inherit; - font-size: var(--fs-m); - } - .btn ha-icon { - --mdc-icon-size: 17px; - } - :host([data-pointer-hover]) .btn:hover { - border-color: var(--hp-accent); - } - .btn.on { - background: var(--hp-accent); - color: var(--text-primary-color, #fff); - border-color: var(--hp-accent); - } - .btn.ghost { - border: none; - } - .btn[disabled] { - opacity: 0.5; - pointer-events: none; - } - /* the compass dial in the general settings (docs/SUN.md) */ - .sunrow { - display: flex; - align-items: center; - gap: var(--sp-5); - margin: var(--sp-3) 0; - } - .suncol { - flex: 1; - display: flex; - flex-direction: column; - gap: var(--sp-3); - min-width: 0; - } - .recoveryoverlay { - position: absolute; - inset: 0; - /* Above the contextual editor tray (70): recovery is the one state in - which every stage-editing surface must be inert. Dialogs live outside - the stage and retain their own higher card-level stacking context. */ - z-index: 75; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: var(--sp-4); - padding: var(--sp-6); - box-sizing: border-box; - /* The final solid layer guarantees an opaque recovery surface even when - a custom HA theme exposes its card colour as rgba(). */ - background: - linear-gradient(var(--ha-card-background, var(--card-background-color, #111)), - var(--ha-card-background, var(--card-background-color, #111))), - #111; - color: var(--primary-text-color, #fff); - text-align: center; - pointer-events: auto; - transition: opacity 0.15s ease; - } - .recoveryoverlay.phase-entering, - .recoveryoverlay.phase-leaving { - opacity: 0; - } - .recoveryoverlay.phase-fading-in, - .recoveryoverlay.phase-opaque { - opacity: 1; - } - .recoveryoverlay ha-icon { - --mdc-icon-size: 44px; - color: var(--hp-accent); - } - @media (prefers-reduced-motion: reduce) { - .recoveryoverlay { - transition: none; - } - } - .oplock { - --oplock-size: calc(var(--icon-size, 2.5cqw) * 0.62); - --oplock-core-size: calc(var(--oplock-size) / 1.26875); - --oplock-stroke-ratio: 0.01875; - --oplock-stroke-width: max(1px, calc(var(--oplock-core-size) * var(--oplock-stroke-ratio))); - --oplock-core-bg: light-dark(#fff, #252525); - --oplock-core-fg: light-dark(#252525, #fff); - --oplock-shell-stroke: light-dark(#BCBCBC, rgb(37 37 37 / 75%)); - --oplock-shell-shadow: - 0 calc(var(--oplock-core-size) * .025) calc(var(--oplock-core-size) * .05) rgb(37 40 45 / 12%), - 0 calc(var(--oplock-core-size) * .1) calc(var(--oplock-core-size) * .175) - calc(var(--oplock-core-size) * -.025) rgb(37 40 45 / 18%); - --oplock-core-shadow: 0 0 0 0 transparent; - pointer-events: none; /* inert while editing; clickable in View (rule below) */ - position: absolute; - transform: translate(-50%, -50%); - width: var(--oplock-size); - height: var(--oplock-size); - display: grid; - place-items: center; - border: 0; - background: transparent; - z-index: 1; - } - .oplock.theme-light { - --oplock-core-bg: #fff; - --oplock-core-fg: #252525; - --oplock-shell-stroke: #BCBCBC; - } - .oplock.theme-dark { - --oplock-core-bg: #252525; - --oplock-core-fg: #fff; - --oplock-shell-stroke: rgb(37 37 37 / 75%); - --oplock-core-shadow: - inset 0 calc(var(--oplock-core-size) * .0125) - calc(var(--oplock-core-size) * .0125) rgb(255 255 255 / 70%); - } - .oplock-shell { - box-sizing: border-box; - width: 100%; - height: 100%; - border: var(--oplock-stroke-width) solid var(--oplock-shell-stroke); - border-radius: 50%; - background: transparent; - box-shadow: var(--oplock-shell-shadow); - display: grid; - place-items: center; - pointer-events: none; - backdrop-filter: none; - } - .oplock-core { - width: var(--oplock-core-size); - height: var(--oplock-core-size); - border-radius: 50%; - background: var(--oplock-core-bg); - color: var(--oplock-core-fg); - box-shadow: var(--oplock-core-shadow); - display: grid; - place-items: center; - pointer-events: none; - } - .oplock ha-icon { - --mdc-icon-size: calc(var(--oplock-core-size) * 0.55); - display: flex; - line-height: 0; - } - .oplock.locked { - --oplock-core-bg: #66D17A; - --oplock-core-fg: light-dark(#fff, #252525); - --oplock-shell-stroke: #66D17A; - } - .oplock.theme-light.locked { --oplock-core-fg: #fff; } - .oplock.theme-dark.locked { - --oplock-core-fg: #252525; - --oplock-stroke-ratio: .025; - } - .oplock.unlocked { - --oplock-core-bg: #F0410C; - --oplock-core-fg: light-dark(#fff, #252525); - --oplock-shell-stroke: #F0410C; - } - .oplock.theme-light.unlocked { --oplock-core-fg: #fff; } - .oplock.theme-dark.unlocked { - --oplock-core-fg: #252525; - --oplock-stroke-ratio: .025; - } - .oplock.unknown { --oplock-core-fg: var(--hp-muted); } - .btn.lockact { - width: 100%; - justify-content: center; - display: flex; - align-items: center; - gap: var(--sp-3); - margin-top: var(--sp-4); - } - .btn.lockact.warn { - color: var(--error-color, #d33); - border-color: var(--error-color, #d33); - } - .oprow { - display: flex; - align-items: center; - gap: var(--sp-4); - padding: var(--sp-3) 0; - } - .oprow b { margin-left: auto; } - .oprow.ok b { color: #66d17a; } - .oprow.warn b { color: var(--hp-open); } - .bindharow { - display: flex; - align-items: center; - gap: var(--sp-5); - flex-wrap: wrap; - } - .bindharow .entcheck { opacity: 0.9; } - .ctrlchips { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin: var(--sp-2) 0; } - .ctrlchip { - display: inline-flex; align-items: center; gap: var(--sp-2); - background: var(--hp-accent); color: var(--text-primary-color, #fff); - border-radius: var(--rad-l); padding: var(--sp-2) var(--sp-4); font-size: var(--fs-s); - } - .ctrlchip ha-icon { --mdc-icon-size: 14px; cursor: pointer; } - .ctrllist { display: flex; flex-direction: column; gap: var(--sp-1); margin-top: var(--sp-2); } - .ctrlopt { - display: flex; align-items: center; gap: var(--sp-4); text-align: left; - border: 0; background: transparent; color: var(--hp-txt); - padding: var(--sp-3) var(--sp-4); border-radius: var(--rad-s); cursor: pointer; font-family: inherit; font-size: var(--fs-m); - } - :host([data-pointer-hover]) .ctrlopt:hover { - background: var(--secondary-background-color, rgba(128,128,128,0.15)); - } - .ctrlopt .sub { color: var(--hp-muted); font-size: var(--fs-s); margin-left: auto; } - .ctrlopt ha-icon { --mdc-icon-size: 16px; } - .ctrlstates { display: flex; flex-direction: column; gap: var(--sp-2); } - .ctrlstate { display: inline-flex; align-items: center; gap: var(--sp-3); color: var(--hp-muted); } - .ctrlstate.on { color: var(--hp-txt); } - .ctrlstate ha-icon { --mdc-icon-size: 15px; } - .cardpreview { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--sp-2); - margin: var(--sp-4) 0 var(--sp-1); - padding: 10px; - border: 1px dashed var(--hp-muted); - border-radius: var(--rad-m); - } - .cardpreview .cpname { font-weight: 700; letter-spacing: 0.04em; } - .cardpreview .cpmeta { - display: inline-flex; - align-items: center; - gap: 0.3em; - font-weight: 600; - opacity: 0.85; - } - .cardpreview .cpmeta ha-icon { --mdc-icon-size: 1.05em; } - .iconauto { - display: flex; - align-items: center; - gap: var(--sp-3); - font-size: var(--fs-s); - margin: var(--sp-2) 0 0; - } - .iconauto ha-icon { --mdc-icon-size: 18px; } - .iconauto span { flex: 1; } - .iconauto .btn { min-height: 32px; padding: 0 var(--sp-3); } - hp-dialog .dfill { - display: inline-flex; - align-items: center; - gap: var(--sp-3); - cursor: pointer; - } - .wallthick-dlg { - position: absolute; - z-index: 40; - min-width: 200px; - transform: translate(-50%, 8px); - padding: 10px 12px; - border-radius: 10px; - background: var(--card-background-color, #fff); - box-shadow: 0 8px 28px rgba(0,0,0,.22); - display: flex; - flex-direction: column; - gap: 8px; - pointer-events: auto; - } - .wallthick-dlg .row { - display: flex; - align-items: center; - gap: 8px; - } - .wallthick-dlg input[type="number"] { - width: 5.5em; - padding: 4px 6px; - border: 1px solid var(--divider-color, #ccc); - border-radius: 6px; - background: var(--input-fill, transparent); - color: var(--primary-text-color); - } - .areasel, - .namein { - background: var(--hp-bg); - border: 1px solid var(--hp-line); - color: var(--hp-txt); - border-radius: var(--rad-s); - padding: var(--sp-3) var(--sp-4); - font-size: var(--fs-m); - font-family: inherit; - } - .namein { - width: 130px; - } - .habindingbanner { - display: flex; - align-items: center; - gap: var(--sp-3); - padding: var(--sp-3) var(--sp-4); - margin-bottom: var(--sp-4); - border: 1px solid var(--warning-color, #ff9800); - border-radius: var(--rad-m); - background: color-mix(in srgb, var(--warning-color, #ff9800) 12%, transparent); - color: var(--primary-text-color, #f1f3f6); - } - .habindingbanner > span { flex: 1; min-width: 0; } - .habindingbanner > ha-icon { color: var(--warning-color, #ff9800); flex: 0 0 auto; } - .habindingbanner.limited { - border-color: var(--secondary-text-color, #9aa0aa); - background: color-mix(in srgb, var(--secondary-text-color, #9aa0aa) 10%, transparent); - } - .habindingbanner.limited > ha-icon { color: var(--secondary-text-color, #9aa0aa); } - .srcrow { - display: flex; - align-items: center; - gap: var(--sp-4); - font-size: var(--fs-m); - cursor: pointer; - padding: var(--sp-1) 0; - } - .dispsection { - margin-top: var(--sp-5) !important; - padding-top: var(--sp-4); - border-top: 1px solid var(--hp-line); - font-weight: 600; - color: var(--hp-txt) !important; - } - .colorrow { - display: flex; - align-items: center; - gap: var(--sp-4); - } - .gsrow > hp-color-opacity { - min-width: min(100%, 210px); - justify-content: space-between; - } - .ripple-colorrow > hp-color-opacity { - width: 100%; - justify-content: space-between; - } - .ripple-sizerow > .opl { - min-width: 0; - } - .colorrow input[type='range'] { flex: 1; } - .colorrow .tempin { width: 70px; flex: none; } - /* beat the generic hp-dialog .body .namein { width:100% } rule */ - hp-dialog .body .temprange .tempin { width: 56px; flex: none; padding: var(--sp-2) var(--sp-3); } - hp-dialog .body .colorrow .tempin { width: 72px; flex: none; } - .srcrow { flex-wrap: nowrap; } - /* native HA controls (rendered only when the HA frontend defines them; - old HA and the smoke env keep the plain inputs). ha-switch is taller - than a checkbox - cap its footprint so .srcrow keeps its rhythm. */ - .srcrow ha-switch { flex: none; } - .colorrow ha-slider { flex: 1; min-width: 0; } - .srcrow > span:first-of-type { white-space: nowrap; } - .colorrow .opl { color: var(--hp-muted); font-size: var(--fs-s); } - .colorrow .opv { font-size: var(--fs-s); min-width: 34px; text-align: right; } - .markerlightgroup { - min-width: 0; - margin: var(--sp-5) 0 0; - padding: var(--sp-4); - border: 1px solid var(--hp-line); - border-radius: var(--rad-m); - } - .markerlightgroup legend { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: var(--sp-1); - padding: 0 var(--sp-2); - color: var(--hp-txt); - font-weight: 600; - } - .markerlightgroup legend > span { min-width: 0; overflow-wrap: anywhere; } - .markerlightgroup[disabled] > :not(legend) { opacity: .62; } - .markerhelpfield { margin-top: var(--sp-4); } - .markerhelplabel { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: var(--sp-1); - margin-bottom: var(--sp-1); - } - .markerhelplabel > label { min-width: 0; overflow-wrap: anywhere; } - .markerradios { display: grid; gap: var(--sp-1); min-width: 0; } - .markerlightgroup .srcrow > span:first-of-type { - min-width: 0; - white-space: normal; - overflow-wrap: anywhere; - } - .markerglowvalue { margin: var(--sp-3) 0; flex-wrap: wrap; } - .markerglowvalue hp-color-opacity { flex: none; } - .markerlightdisabled { - display: flex; - align-items: center; - gap: var(--sp-2); - margin-top: var(--sp-2) !important; - } - .markerlightdisabled ha-icon { --mdc-icon-size: 18px; flex: none; } - .markerbadgetechnical { - min-width: 0; - margin: var(--sp-1) 0 var(--sp-2) !important; - overflow-wrap: anywhere; - } - .markerbadgetechnical code { white-space: normal; } - .planrow { - display: flex; - align-items: center; - gap: 10px; - } - /* the "already uploaded" picker: a plan is never deleted for being - unreferenced, so it has to be findable again */ - .savedplans { - display: flex; - flex-direction: column; - gap: var(--sp-3); - max-height: 240px; - overflow: auto; - margin: var(--sp-3) 0 var(--sp-1); - padding: var(--sp-3); - border: 1px solid var(--hp-line); - border-radius: var(--rad-m); - background: var(--hp-bg2, rgba(255, 255, 255, 0.03)); - /* The same collapse that ate .candlist (v1.53.1): a scroll box is a - flex item whose automatic minimum size is ZERO (overflow != visible), - so inside hp-dialog .body — a flex column taller than its 66vh cap — - it shrank to a 14px sliver: the rows were in the DOM, the owner saw - a thin rounded stripe under the "Already uploaded" button. Don't - shrink, and keep a floor even when the box is empty or loading. */ - flex: 0 0 auto; - min-height: 2.6em; - } - .savedplan { - display: flex; - align-items: center; - gap: 10px; - } - .savedplan.cur { outline: 1px solid var(--hp-accent); border-radius: var(--rad-s); } - .savedplan img { - width: 56px; - height: 40px; - object-fit: contain; - border: 1px solid var(--hp-line); - border-radius: var(--rad-s); - background: #fff; - flex: none; - } - .savedmeta { display: flex; flex-direction: column; min-width: 0; flex: 1; } - .savedmeta b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } - .savedmeta .muted { font-size: var(--fs-s); } - .savedplan .btn.danger ha-icon { color: #f25a4a; } - .savedplan .btn[disabled] { opacity: 0.4; pointer-events: none; } - .planprev { - max-width: 120px; - max-height: 70px; - border: 1px solid var(--hp-line); - border-radius: var(--rad-s); - background: #fff; - } - .planname { - font-size: var(--fs-m); - max-width: 150px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - .planname.muted { - color: var(--hp-muted); - } - .filebtn { - cursor: pointer; - } - .fileupload { display: inline-flex; min-width: 0; } - .fileupload > input { display: none; } - .btn.danger { - border-color: #b3402a; - color: #ff7a5c; - } - hp-dialog .row .spacer { - flex: 1; - } - hp-dialog .body { - max-height: 66vh; - overflow-y: auto; - } - .descin { - width: 100%; - box-sizing: border-box; - background: var(--hp-bg); - border: 1px solid var(--hp-line); - color: var(--hp-txt); - border-radius: var(--rad-s); - padding: var(--sp-3) var(--sp-4); - font-size: var(--fs-m); - font-family: inherit; - resize: vertical; - /* flex column of the dialog body squeezes textareas — keep a usable height */ - min-height: 92px; - flex-shrink: 0; - line-height: 1.35; - } - .bindsel { - display: flex; - flex-direction: column; - gap: var(--sp-3); - border: 1px solid var(--hp-line); - border-radius: var(--rad-m); - padding: var(--sp-4); - } - .bindsel .opt { - display: flex; - align-items: center; - gap: var(--sp-3); - border: 1px solid var(--hp-line); - background: transparent; - color: var(--hp-txt); - border-radius: var(--rad-s); - padding: var(--sp-3) var(--sp-4); - cursor: pointer; - font-size: var(--fs-m); - font-family: inherit; - } - .bindsel .opt.on { - background: var(--hp-accent); - color: var(--text-primary-color, #fff); - border-color: var(--hp-accent); - } - .curbind { - display: flex; - align-items: center; - gap: var(--sp-3); - font-size: var(--fs-m); - color: var(--hp-txt); - flex-wrap: wrap; - } - .curbind .ref { - color: var(--hp-muted); - font-size: var(--fs-s); - } - .vacbox .vacbtns { display: flex; gap: var(--sp-4); margin: var(--sp-3) 0; flex-wrap: wrap; } - .vacdiag { display: grid; gap: 4px; margin-bottom: var(--sp-3); } - .vacdiag > div { display: flex; justify-content: space-between; gap: var(--sp-5); } - .vacdiag > div > span { color: var(--secondary-text-color); } - .vacdiag > div > b { text-align: right; overflow-wrap: anywhere; } - .vacpicker { margin: var(--sp-3) 0; } - .vacsource-warning { display: grid; gap: 8px; } - .vacsource-warning .btn { justify-self: start; } - .vacpicker > summary { display: inline-flex; width: auto; list-style: none; cursor: pointer; } - .vacpicker > summary::-webkit-details-marker { display: none; } - .vacsource-list { display: grid; gap: 6px; margin-top: 8px; } - .vacsource-list details { padding: 6px 0 0; } - .vacsource-list details > summary { cursor: pointer; font-weight: 600; } - .vacsource { display: flex; align-items: center; justify-content: space-between; gap: 12px; - width: 100%; min-width: 0; padding: 9px 10px; border: 1px solid var(--divider-color); - border-radius: 10px; color: var(--primary-text-color); background: var(--secondary-background-color); - text-align: left; cursor: pointer; } - .vacsource.on { border-color: var(--accent-color); box-shadow: inset 3px 0 var(--accent-color); } - .vacsource > span:first-child { min-width: 0; display: grid; gap: 2px; } - .vacsource small { color: var(--secondary-text-color); overflow-wrap: anywhere; } - .vacsource-meta { color: var(--secondary-text-color); text-align: right; font-size: 0.82em; } - .vacxcme pre { margin: 8px 0 0; white-space: pre-wrap; user-select: text; } - .vacfit { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - z-index: 12; - overflow: visible; - touch-action: none; - cursor: grab; - /* the devlayer is pointer-events: none and every child opts back in — - without this line real clicks flew straight through the overlay - (owner: «уголки не кликабельны»; synthetic smoke events bypass - hit-testing, which is why they lied) */ - pointer-events: auto; - } - .vacfit:active { cursor: grabbing; } - .vacfit polygon { - fill: color-mix(in srgb, var(--hp-accent) 16%, transparent); - stroke: var(--hp-accent); - stroke-width: 2; - vector-effect: non-scaling-stroke; - stroke-dasharray: 6 4; - } - .vacfit text { - fill: var(--hp-accent); - font-size: 26px; - text-anchor: middle; - dominant-baseline: middle; - pointer-events: none; - user-select: none; - } - /* ---- the furniture palette (docs/FURNITURE.md §3) ------------------- */ - .furnpalette { - display: flex; - flex-direction: column; - max-height: 38vh; - border-top: 1px solid var(--hp-border, rgba(255, 255, 255, 0.12)); - background: var(--card-background-color, var(--hp-bg)); - font-size: 0.85em; - } - .furnhd { - display: flex; - align-items: center; - gap: 6px; - padding: 4px 8px; - font-weight: 600; - opacity: 0.9; - } - .furnhd .spacer { flex: 1; } - .furnbody { - overflow: auto; - padding: 0 8px 6px; - } - .furngroup { - margin: 6px 0 3px; - font-size: 0.85em; - text-transform: uppercase; - letter-spacing: 0.04em; - opacity: 0.6; - } - .furnrow { - display: flex; - flex-wrap: wrap; - gap: 4px; - } - .furnitem { - display: flex; - flex-direction: column; - align-items: center; - gap: 2px; - width: 74px; - padding: 4px 2px; - border: 1px solid transparent; - border-radius: 8px; - background: rgba(127, 127, 127, 0.08); - color: inherit; - font: inherit; - font-size: 0.8em; - line-height: 1.15; - text-align: center; - cursor: pointer; - } - :host([data-pointer-hover]) .furnitem:hover { background: rgba(127, 127, 127, 0.18); } - .furnitem.on { - border-color: var(--hp-accent); - background: rgba(38, 166, 154, 0.18); - } - .furnprev { - width: 40px; - height: 40px; - color: var(--primary-text-color, currentColor); - } - .furnsize { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 6px; - padding: 6px 8px; - border-top: 1px solid var(--hp-border, rgba(255, 255, 255, 0.12)); - } - .furnsize label { - display: flex; - align-items: baseline; - gap: 3px; - opacity: 0.8; - } - .furnsize .furnunit { opacity: 0.6; font-size: 0.85em; } - .furnsize input { - width: 5.5em; - padding: 3px 5px; - } - .furnhint { opacity: 0.6; } - .vacfitdot { fill: var(--hp-accent); pointer-events: none; } - /* hit target: invisible and finger-sized; .vacfitknob is the visible bead */ - .vacfithandle { - fill: transparent; - stroke: none; - cursor: nwse-resize; - } - .vacfitknob { - fill: var(--hp-bg); - stroke: var(--hp-accent); - stroke-width: 2; - vector-effect: non-scaling-stroke; - pointer-events: none; - } - .vaccalbar { - position: fixed; - left: 50%; - bottom: 24px; - transform: translateX(-50%); - display: flex; - gap: var(--sp-5); - align-items: center; - background: var(--hp-bg); - color: var(--hp-txt); - border: 1px solid var(--hp-accent); - border-radius: var(--rad-l); - padding: 10px var(--sp-5); - z-index: 60; - box-shadow: var(--shadow-2); - } - .candlist { - max-height: 160px; - overflow-y: auto; - border-top: 1px solid var(--hp-line); - /* A scrollable box is a flex item that HAPPILY collapses: inside the - dialog body (a flex column) this list rendered its rows into a 1px - sliver — the DOM had 26 candidates and the user saw nothing. In the - binding dropdown it sits inside .droppanel (block context) and never - showed the bug. Field report, 2026-07-30. */ - flex: 0 0 auto; - min-height: 2.6em; - } - .cand { - display: flex; - justify-content: space-between; - gap: var(--sp-4); - padding: var(--sp-3) var(--sp-4); - cursor: pointer; - border-radius: var(--rad-s); - font-size: var(--fs-m); - } - :host([data-pointer-hover]) .cand:hover { - background: rgba(127, 127, 127, 0.15); - } - .cand.sel { - background: var(--hp-accent); - color: var(--text-primary-color, #fff); - } - .cand .cs { - color: var(--hp-muted); - font-size: var(--fs-s); - white-space: nowrap; - } - .opening-entity-candidate { - width: 100%; - border: 0; - background: transparent; - color: inherit; - font: inherit; - text-align: left; - } - .opening-entity-candidate.sel { - background: var(--hp-accent); - color: var(--text-primary-color, #fff); - } - .opening-entity-empty { cursor: default; } - .cand.sel .cs { - color: var(--text-primary-color, #fff); - opacity: 0.85; - } - .cand.muted { - color: var(--hp-muted); - cursor: default; - } - .pdfedit { - display: flex; - flex-wrap: wrap; - gap: var(--sp-3); - align-items: center; - } - .pdftag { - display: inline-flex; - align-items: center; - gap: var(--sp-2); - border: 1px solid var(--hp-line); - border-radius: var(--rad-s); - padding: var(--sp-2) var(--sp-3); - font-size: var(--fs-s); - } - .pdftag a { - color: var(--hp-txt); - text-decoration: none; - max-width: 150px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - .pdftag .x { - --mdc-icon-size: 15px; - cursor: pointer; - color: var(--hp-muted); - } - :host([data-pointer-hover]) .pdftag .x:hover { - color: #ff7a5c; - } - .entlist { - display: flex; - flex-direction: column; - gap: var(--sp-2); - margin-bottom: 10px; - } - .entrow { - display: flex; - align-items: center; - gap: var(--sp-4); - padding: var(--sp-3) var(--sp-4); - border-radius: var(--rad-m); - background: var(--secondary-background-color, rgba(128, 128, 128, 0.12)); - } - .entrow ha-icon { --mdc-icon-size: 20px; color: var(--hp-muted); } - .entrow.on ha-icon { color: var(--hp-accent); } - .entrow .en { flex: 1; font-size: var(--fs-m); } - .entrow .ev { font-size: var(--fs-m); color: var(--hp-muted); } - .entbtn { - min-width: 74px; - min-height: 32px; - padding: var(--sp-2) var(--sp-5); - border: 1px solid var(--hp-muted); - border-radius: 999px; - background: transparent; - color: var(--hp-txt); - font: inherit; - font-size: var(--fs-m); - cursor: pointer; - } - .entbtn.on { - background: var(--hp-accent); - border-color: var(--hp-accent); - color: var(--text-primary-color, #fff); - font-weight: 600; - } - .inforow { - display: flex; - gap: 10px; - font-size: var(--fs-m); - margin: var(--sp-2) 0; - } - .inforow .k { - color: var(--hp-muted); - min-width: 84px; - } - .inforow a { - color: var(--hp-accent); - word-break: break-all; - } - .infodesc { - font-size: var(--fs-m); - white-space: pre-wrap; - margin-top: var(--sp-3); - } - .infodesc.muted { - color: var(--hp-muted); - } - .pdflist { - display: flex; - flex-direction: column; - gap: var(--sp-2); - } - .pdf { - display: inline-flex; - align-items: center; - gap: var(--sp-2); - color: var(--hp-accent); - text-decoration: none; - } - ha-icon-picker { - display: block; - } - .floorrow { - display: flex; - align-items: center; - gap: var(--sp-4); - padding: var(--sp-3) var(--sp-2); - font-size: var(--fs-m); - cursor: pointer; - } - .floorrow .floorlvl { - color: var(--hp-muted); - font-size: var(--fs-s); - border: 1px solid var(--hp-line); - border-radius: var(--rad-s); - padding: 0 var(--sp-3); - } - .rtest { - display: flex; - align-items: center; - gap: var(--sp-4); - margin-bottom: var(--sp-4); - } - .rtest .namein { flex: 1; } - .rtest ha-icon { color: var(--hp-accent); } - .rtesticon { font-size: var(--fs-s); color: var(--hp-muted); } - .rrow { - display: flex; - align-items: center; - gap: var(--sp-3); - margin: var(--sp-1) 0; - } - .rrow .rpat { flex: 2; } - .rrow .ricon { flex: 1.4; } - .rrow .rpat.bad { border-color: #ff7a5c; } - .rrow .rprev { --mdc-icon-size: 18px; color: var(--hp-txt); min-width: 18px; } - .rrow .ract { - --mdc-icon-size: 16px; - color: var(--hp-muted); - cursor: pointer; - } - :host([data-pointer-hover]) .rrow .ract:hover { color: var(--hp-txt); } - :host([data-pointer-hover]) .rrow .ract.del:hover { color: #ff7a5c; } - .gsrow .gsl { - min-width: 150px; - font-size: var(--fs-m); - color: var(--hp-muted); - } - .optimize-live { - display: grid; - justify-items: start; - gap: var(--sp-2); - margin-bottom: var(--sp-3); - } - .optimize-live .alignmsg, .optimize-live .rhint { margin-bottom: 0; } - .optimize-cleanup { min-height: 44px; } - .optimize-selected { color: var(--hp-txt); } - .optimize-details { - margin-top: var(--sp-3); - color: var(--hp-muted); - font-size: var(--fs-s); - overflow-wrap: anywhere; - } - .optimize-details > summary { - width: fit-content; - color: var(--hp-txt); - cursor: pointer; - font-weight: 600; - } - .optimize-details > summary:focus-visible { - outline: 2px solid var(--hp-accent); - outline-offset: 3px; - border-radius: var(--rad-s); - } - .optimize-details ul { margin: var(--sp-3) 0; padding-inline-start: 22px; } - .optimize-details li + li { margin-top: var(--sp-1); } - .btn.alignall { width: 100%; justify-content: center; } - .backupactions { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: var(--sp-3); - } - .backupactions .btn { justify-content: center; min-width: 0; } - .backupupload { display: inline-flex; min-width: 0; } - .backupupload > .btn { width: 100%; justify-content: center; } - .backupupload input { display: none; } - .backupbody { min-width: 0; } - .backupplanonly { margin-inline-start: var(--sp-4) !important; align-items: flex-start !important; } - .backupplanonly > span:first-of-type { display: grid; gap: 2px; white-space: normal; } - .backupplanonly small { color: var(--secondary-text-color); line-height: 1.35; } - .backupplanonlystatus { color: var(--hp-accent) !important; font-weight: 700; } - .backupfile, .backupsummary, .backupcontent { - display: flex; - flex-direction: column; - min-width: 0; - gap: var(--sp-1); - } - .backupfile b, .backupcontent span { - overflow-wrap: anywhere; - } - .backupfile span, .backupsummary span, .backupcontent span { - color: var(--hp-muted); - font-size: var(--fs-s); - } - .backupcounts { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: var(--sp-2) var(--sp-4); - font-size: var(--fs-s); - } - .backupwarn, .backuperror { - border-radius: var(--rad-s); - padding: var(--sp-3); - font-size: var(--fs-s); - line-height: 1.45; - overflow-wrap: anywhere; - } - .backupwarn { background: color-mix(in srgb, var(--hp-accent) 12%, transparent); } - .backuperror { background: rgba(179, 64, 42, .16); color: #ff7a5c; } - .backupdetails { - border: 1px solid var(--hp-line); - border-radius: var(--rad-s); - padding: var(--sp-2) var(--sp-3); - font-size: var(--fs-s); - } - .backupdetails summary { cursor: pointer; font-weight: 700; } - .backupdetails > div { display: grid; gap: var(--sp-1); padding-block-start: var(--sp-2); } - .backupdetails code { overflow-wrap: anywhere; color: var(--hp-muted); } - .backupchoices { - display: flex; - flex-direction: column; - gap: var(--sp-2); - border: 1px solid var(--hp-line); - border-radius: var(--rad-s); - min-width: 0; - } - .backupchoices label { margin: 0 !important; display: flex; gap: var(--sp-2); } - .backupconfirm { align-items: flex-start !important; } - .backupconfirm > span:first-of-type { - min-width: 0; - white-space: normal; - overflow-wrap: anywhere; - } - @media (max-width: 520px) { - .backupactions, .backupcounts { grid-template-columns: 1fr; } - } - .aboutver { - font-size: var(--fs-s); - color: var(--hp-muted); - margin: var(--sp-2) 0 var(--sp-3); - } - .aboutlink { - display: flex; - align-items: center; - gap: var(--sp-3); - width: fit-content; - color: var(--hp-accent); - text-decoration: none; - font-size: var(--fs-m); - padding: var(--sp-1) 0; - } - :host([data-pointer-hover]) .aboutlink:hover { text-decoration: underline; } - .aboutlink ha-icon { --mdc-icon-size: 18px; line-height: 1; } - hp-dialog .body { - padding: var(--sp-5) var(--sp-6); - display: flex; - flex-direction: column; - gap: var(--sp-3); - } - hp-dialog .tapconfirm-body { - min-width: 0; - overflow-x: hidden; - } - hp-dialog .tapconfirm-body p { - max-width: 100%; - min-width: 0; - margin: 0; - overflow-wrap: anywhere; - white-space: normal; - } - hp-dialog .tapconfirm-line { - color: var(--hp-muted); - } - hp-dialog .body label { - font-size: var(--fs-s); - color: var(--hp-muted); - margin-top: var(--sp-3); - } - hp-dialog .body .namein, - hp-dialog .body .areasel { - width: 100%; - box-sizing: border-box; - } - /* Room settings contains long radio labels, two source pickers and live - previews, so it deliberately uses hp-dialog's medium width. Keep its - content shrinkable as well: the generic srcrow rule is nowrap because - compact switch rows need it, but here that created a horizontal scroll - box even on a wide desktop viewport. */ - hp-dialog.roomdialog .body { - min-width: 0; - overflow-x: hidden; - } - hp-dialog.roomdialog .body > * { - min-width: 0; - max-width: 100%; - box-sizing: border-box; - } - hp-dialog.roomdialog .srcrow { - min-width: 0; - align-items: flex-start; - } - hp-dialog.roomdialog .srcrow > span:first-of-type { - min-width: 0; - white-space: normal; - overflow-wrap: anywhere; - line-height: 1.35; - } - hp-dialog.roomdialog .dropbtn > b, - hp-dialog.roomdialog .dropbtn > .ref { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - hp-dialog .row { - display: flex; - justify-content: flex-end; - gap: var(--sp-4); - width: 100%; - min-width: 0; - box-sizing: border-box; - padding: var(--sp-5) var(--sp-6); - border-top: 1px solid var(--hp-line); - } - /* Stable destructive/commit footer contract. A flex spacer cannot react - when translated labels no longer fit: justify-content then overflows - the destructive button through the left inset. Two real groups wrap - as units instead — destructive actions stay left, while Cancel/Save - move together to a right-aligned second row when necessary. */ - hp-dialog .row.dialog-action-footer { - align-items: center; - justify-content: flex-start; - flex-wrap: wrap; - row-gap: var(--sp-4); - } - hp-dialog .dialog-action-group { - display: flex; - flex: 0 1 auto; - flex-wrap: wrap; - align-items: center; - gap: var(--sp-4); - max-width: 100%; - min-width: 0; - } - hp-dialog .dialog-action-group .btn { - flex: 0 0 auto; - min-height: 44px; - } - hp-dialog .dialog-action-danger { - margin-right: auto; - } - hp-dialog .dialog-action-commit { - margin-left: auto; - justify-content: flex-end; - } - hp-dialog .row.markerfooter { - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - } - hp-dialog .row.roomfooter { - align-items: center; - flex-wrap: wrap; - } - /* Device info can have Edit + Open in HA + Close. It uses a wide dialog; - wrapping remains as a phone fallback, but without a flex spacer (which - used to strand Edit alone on the first line). */ - hp-dialog .row.infofooter { - align-items: center; - justify-content: flex-start; - flex-wrap: wrap; - gap: var(--sp-3); - } - hp-dialog .row.infofooter .btn { - flex-shrink: 0; - } - hp-dialog .row.infofooter .infofooter-close { - margin-left: auto; - } - @media (max-width: 480px) { - hp-dialog .row.infofooter { - padding: var(--sp-4) var(--sp-5); - } - } - .markeractions, - .markersaveactions { - display: flex; - align-items: center; - gap: var(--sp-4); - } - .markeractions:empty { display: none; } - .markersaveactions { margin-left: auto; } - .device-inbox { - display: flex; - flex-direction: column; - gap: var(--sp-4); - padding: var(--sp-5) var(--sp-6); - min-width: 0; - } - .device-inbox-dialog { --hp-dialog-wide-width: 920px; } - .device-inbox-head { - display: grid; - grid-template-columns: minmax(180px, 1fr) auto; - align-items: center; - gap: var(--sp-4); - } - .device-inbox-search { - width: 100%; - min-width: 0; - box-sizing: border-box; - border: 1px solid var(--hp-line); - border-radius: var(--rad-m); - background: transparent; - color: var(--hp-txt); - font: inherit; - padding: 11px 14px; - } - .device-inbox-tabs { - display: flex; - gap: var(--sp-2); - overflow-x: auto; - scrollbar-width: thin; - padding-bottom: var(--sp-1); - } - .device-inbox-tabs button { - flex: 0 0 auto; - border: 1px solid var(--hp-line); - border-radius: 999px; - background: transparent; - color: var(--hp-txt); - font: inherit; - padding: 8px 12px; - cursor: pointer; - } - .device-inbox-tabs button.on { - border-color: var(--hp-accent); - background: color-mix(in srgb, var(--hp-accent) 18%, transparent); - } - .device-inbox-tabs button span { color: var(--hp-muted); margin-inline-start: 4px; } - .device-inbox-filters { - display: flex; - flex-wrap: wrap; - gap: var(--sp-4) var(--sp-6); - color: var(--hp-muted); - } - .device-inbox-filters label { - display: inline-flex; - align-items: center; - gap: var(--sp-2); - cursor: pointer; - } - .device-inbox-results { display: grid; gap: var(--sp-3); min-width: 0; } - .device-inbox-row { - display: grid; - grid-template-columns: 42px minmax(180px, 1fr) minmax(180px, auto); - align-items: center; - gap: var(--sp-4); - min-width: 0; - border: 1px solid var(--hp-line); - border-radius: var(--rad-m); - padding: var(--sp-4); - background: color-mix(in srgb, var(--hp-txt) 3%, transparent); - } - .device-inbox-icon { --mdc-icon-size: 28px; color: var(--hp-txt); justify-self: center; } - .device-inbox-copy { min-width: 0; } - .device-inbox-name { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2); } - .device-inbox-new { - border-radius: 999px; - background: var(--hp-accent); - color: var(--text-primary-color, #fff); - font-size: var(--fs-s); - padding: 2px 7px; - } - .device-inbox-meta, - .device-inbox-reason, - .device-inbox-copy code { - display: block; - color: var(--hp-muted); - font-size: var(--fs-s); - overflow-wrap: anywhere; - white-space: normal; - } - .device-inbox-status { color: var(--error-color, #db4437); margin-inline-start: var(--sp-2); } - .device-inbox-actions { - display: flex; - justify-content: flex-end; - align-items: center; - flex-wrap: wrap; - gap: var(--sp-2); - } - .device-inbox-actions .btn { min-height: 36px; padding: 7px 10px; } - .device-inbox-menu { position: relative; } - .device-inbox-menu summary { list-style: none; cursor: pointer; } - .device-inbox-menu summary::-webkit-details-marker { display: none; } - .device-inbox-menu-items { - position: absolute; - z-index: 2; - inset-inline-end: 0; - top: calc(100% + var(--sp-1)); - display: grid; - gap: var(--sp-1); - min-width: 180px; - padding: var(--sp-2); - border: 1px solid var(--hp-line); - border-radius: var(--rad-m); - background: var(--hp-panel, var(--card-background-color, #fff)); - box-shadow: 0 8px 24px rgba(0, 0, 0, .22); - } - .device-inbox-menu-items .btn { justify-content: flex-start; width: 100%; } - .device-inbox-empty { color: var(--hp-muted); text-align: center; padding: var(--sp-8); } - .device-inbox-more { align-self: center; } - @media (max-width: 680px) { - .device-inbox { padding: var(--sp-4); } - .device-inbox-head { grid-template-columns: minmax(0, 1fr); } - .device-inbox-head .btn { justify-self: stretch; } - .device-inbox-tabs { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - overflow-x: visible; - } - .device-inbox-tabs button { - min-width: 0; - overflow-wrap: anywhere; - } - .device-inbox-row { grid-template-columns: 36px minmax(0, 1fr); } - .device-inbox-actions { grid-column: 1 / -1; justify-content: flex-start; } - } -`],Ye=new Set(["hacs","sun","backup","hassio","met","telegram_bot","mobile_app","systemmonitor","better_thermostat","adaptive_lighting","yandex_pogoda","upnp_serial_number"]),Je=[{pattern:"протечк|leak|water sensor",icon:"mdi:water-alert"},{pattern:"клапан|valve",icon:"mdi:pipe-valve"},{pattern:"дым|smoke",icon:"mdi:smoke-detector"},{pattern:"термоголов|trv|radiator",icon:"mdi:radiator"},{pattern:"чайник|kettle|термопот",icon:"mdi:kettle"},{pattern:"сауна|sauna|harvia|парная|парилк",icon:"mdi:hot-tub"},{pattern:"температ|temperature|thermometer|climate sensor",icon:"mdi:thermometer"},{pattern:"qingping|air monitor|молекул|air quality",icon:"mdi:air-filter"},{pattern:"штор|curtain|blind|shade",icon:"mdi:roller-shade"},{pattern:"розетк|plug|socket|outlet",icon:"mdi:power-socket-de"},{pattern:"выключат|switch",icon:"mdi:light-switch"},{pattern:"лампа|лампочк|bulb|gx53|светильник|rgb|lamp|light strip",icon:"mdi:lightbulb"},{pattern:"камер|camera",icon:"mdi:cctv"},{pattern:"замок|ttlock|lock|sn609|sn9161",icon:"mdi:lock"},{pattern:"ворота|garage|gate",icon:"mdi:garage-variant"},{pattern:"калитк|door|открыт|contact",icon:"mdi:door"},{pattern:"счётчик|счетчик|kws|meter",icon:"mdi:meter-electric"},{pattern:"вводный автомат|breaker|wifimcbn",icon:"mdi:electric-switch"},{pattern:"myheat|котёл|котел|boiler|отоплен|heating",icon:"mdi:water-boiler"},{pattern:"холодильник|fridge",icon:"mdi:fridge"},{pattern:"стиральн|washer|washing",icon:"mdi:washing-machine"},{pattern:"сушилк|dryer",icon:"mdi:tumble-dryer"},{pattern:"пылесос|vacuum|dreame|roborock",icon:"mdi:robot-vacuum"},{pattern:"soundbar",icon:"mdi:soundbar"},{pattern:"колонк|станц|speaker|яндекс|yandex|алиса|alice",icon:"mdi:speaker"},{pattern:"tv|телевизор|hyundaitv|mitv|television",icon:"mdi:television"},{pattern:"keenetic|роутер|router|mesh|access point",icon:"mdi:router-wireless"},{pattern:"ибп|ups|kirpich",icon:"mdi:battery-charging-high"},{pattern:"slzb|координат|zigbee|coordinator",icon:"mdi:zigbee"},{pattern:"motion|движен|presence|присутств",icon:"mdi:motion-sensor"},{pattern:"humidity|влажн",icon:"mdi:water-percent"}];function Xe(e){const t=[];for(const i of e)if(i&&"string"==typeof i.pattern&&i.icon)try{t.push({re:new RegExp(i.pattern,"i"),icon:i.icon})}catch{}return t}const Ze=Xe(Je),Qe={temperature:"mdi:thermometer",humidity:"mdi:water-percent",motion:"mdi:motion-sensor",occupancy:"mdi:motion-sensor",presence:"mdi:motion-sensor",door:"mdi:door",window:"mdi:window-closed",garage_door:"mdi:garage-variant",smoke:"mdi:smoke-detector",moisture:"mdi:water-alert",gas:"mdi:gas-cylinder",power:"mdi:meter-electric",energy:"mdi:meter-electric",illuminance:"mdi:brightness-5",co2:"mdi:molecule-co2",pm25:"mdi:air-filter",battery:"mdi:battery"},et="mdi:chip";function tt(e,t,i){const s=((e||"")+" "+(t||"")).toLowerCase();for(const{re:e,icon:t}of i??Ze)if(e.test(s))return t;return et}const it=["light","switch","cover","valve","lock","climate","fan","media_player","camera","vacuum","humidifier","water_heater","alarm_control_panel","sensor","binary_sensor","event","button","number","select","update"];var st=/^-?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i,nt=Math.ceil,ot=Math.floor,at="[BigNumber Error] ",rt=at+"Number primitive has more than 15 significant digits: ",lt=1e14,ct=14,ht=9007199254740991,dt=[1,10,100,1e3,1e4,1e5,1e6,1e7,1e8,1e9,1e10,1e11,1e12,1e13],pt=1e7,ut=1e9;function _t(e){var t=0|e;return e>0||e===t?t:t-1}function mt(e){for(var t,i,s=1,n=e.length,o=e[0]+"";sc^i?1:-1;for(r=(l=n.length)<(c=o.length)?l:c,a=0;ao[a]^i?1:-1;return l==c?0:l>c^i?1:-1}function ft(e,t,i,s){if(ei||e!==ot(e))throw Error(at+(s||"Argument")+("number"==typeof e?ei?" out of range: ":" not an integer: ":" not a primitive number: ")+String(e))}function vt(e){var t=e.c.length-1;return _t(e.e/ct)==t&&e.c[t]%2!=0}function bt(e,t){return(e.length>1?e.charAt(0)+"."+e.slice(1):e)+(t<0?"e":"e+")+t}function yt(e,t,i){var s,n;if(t<0){for(n=i+".";++t;n+=i);e=n+e}else if(++t>(s=e.length)){for(n=i,t-=s;--t;n+=i);e+=n}else tb?p.c=p.e=null:e.e=10;l/=10,r++);return void(r>b?p.c=p.e=null:(p.e=r,p.c=[e]))}d=String(e)}else{if(!st.test(d=String(e)))return n(p,d,c);p.s=45==d.charCodeAt(0)?(d=d.slice(1),-1):1}(r=d.indexOf("."))>-1&&(d=d.replace(".","")),(l=d.search(/e/i))>0?(r<0&&(r=l),r+=+d.slice(l+1),d=d.substring(0,l)):r<0&&(r=d.length)}else{if(ft(t,2,$.length,"Base"),10==t&&S)return T(p=new M(e),_+p.e+1,m);if(d=String(e),c="number"==typeof e){if(0*e!=0)return n(p,d,c,t);if(p.s=1/e<0?(d=d.slice(1),-1):1,M.DEBUG&&d.replace(/^0\.0*|\./,"").length>15)throw Error(rt+e)}else p.s=45===d.charCodeAt(0)?(d=d.slice(1),-1):1;for(i=$.slice(0,t),r=l=0,h=d.length;lr){r=h;continue}}else if(!a&&(d==d.toUpperCase()&&(d=d.toLowerCase())||d==d.toLowerCase()&&(d=d.toUpperCase()))){a=!0,l=-1,r=0;continue}return n(p,String(e),c,t)}c=!1,(r=(d=s(d,t,10,p.s)).indexOf("."))>-1?d=d.replace(".",""):r=d.length}for(l=0;48===d.charCodeAt(l);l++);for(h=d.length;48===d.charCodeAt(--h););if(d=d.slice(l,++h)){if(h-=l,c&&M.DEBUG&&h>15&&(e>ht||e!==ot(e)))throw Error(rt+p.s*e);if((r=r-l-1)>b)p.c=p.e=null;else if(r=f)?bt(l,a):yt(l,a,"0");else if(o=(e=T(new M(e),t,i)).e,r=(l=mt(e.c)).length,1==s||2==s&&(t<=o||o<=g)){for(;ra),l=yt(l,o,"0"),o+1>r){if(--t>0)for(l+=".";t--;l+="0");}else if((t+=o-r)>0)for(o+1==r&&(l+=".");t--;l+="0");return e.s<0&&n?"-"+l:l}function D(e,t){for(var i,s,n=1,o=new M(e[0]);n=10;n/=10,s++);return(i=s+i*ct-1)>b?e.c=e.e=null:i=10;r/=10,n++);if((o=t-n)<0)o+=ct,a=t,l=d[c=0],h=ot(l/p[n-a-1]%10);else if((c=nt((o+1)/ct))>=d.length){if(!s)break e;for(;d.length<=c;d.push(0));l=h=0,n=1,a=(o%=ct)-ct+1}else{for(l=r=d[c],n=1;r>=10;r/=10,n++);h=(a=(o%=ct)-ct+n)<0?0:ot(l/p[n-a-1]%10)}if(s=s||t<0||null!=d[c+1]||(a<0?l:l%p[n-a-1]),s=i<4?(h||s)&&(0==i||i==(e.s<0?3:2)):h>5||5==h&&(4==i||s||6==i&&(o>0?a>0?l/p[n-a]:0:d[c-1])%10&1||i==(e.s<0?8:7)),t<1||!d[0])return d.length=0,s?(t-=e.e+1,d[0]=p[(ct-t%ct)%ct],e.e=-t||0):d[0]=e.e=0,e;if(0==o?(d.length=c,r=1,c--):(d.length=c+1,r=p[ct-o],d[c]=a>0?ot(l/p[n-a]%p[a])*r:0),s)for(;;){if(0==c){for(o=1,a=d[0];a>=10;a/=10,o++);for(a=d[0]+=r,r=1;a>=10;a/=10,r++);o!=r&&(e.e++,d[0]==lt&&(d[0]=1));break}if(d[c]+=r,d[c]!=lt)break;d[c--]=0,r=1}for(o=d.length;0===d[--o];d.pop());}e.e>b?e.c=e.e=null:e.e=f?bt(t,i):yt(t,i,"0"),e.s<0?"-"+t:t)}return M.clone=e,M.ROUND_UP=0,M.ROUND_DOWN=1,M.ROUND_CEIL=2,M.ROUND_FLOOR=3,M.ROUND_HALF_UP=4,M.ROUND_HALF_DOWN=5,M.ROUND_HALF_EVEN=6,M.ROUND_HALF_CEIL=7,M.ROUND_HALF_FLOOR=8,M.EUCLID=9,M.config=M.set=function(e){var t,i;if(null!=e){if("object"!=typeof e)throw Error(at+"Object expected: "+e);if(e.hasOwnProperty(t="DECIMAL_PLACES")&&(ft(i=e[t],0,ut,t),_=i),e.hasOwnProperty(t="ROUNDING_MODE")&&(ft(i=e[t],0,8,t),m=i),e.hasOwnProperty(t="EXPONENTIAL_AT")&&((i=e[t])&&i.pop?(ft(i[0],-ut,0,t),ft(i[1],0,ut,t),g=i[0],f=i[1]):(ft(i,-ut,ut,t),g=-(f=i<0?-i:i))),e.hasOwnProperty(t="RANGE"))if((i=e[t])&&i.pop)ft(i[0],-ut,-1,t),ft(i[1],1,ut,t),v=i[0],b=i[1];else{if(ft(i,-ut,ut,t),!i)throw Error(at+t+" cannot be zero: "+i);v=-(b=i<0?-i:i)}if(e.hasOwnProperty(t="CRYPTO")){if((i=e[t])!==!!i)throw Error(at+t+" not true or false: "+i);if(i){if("undefined"==typeof crypto||!crypto||!crypto.getRandomValues&&!crypto.randomBytes)throw y=!i,Error(at+"crypto unavailable");y=i}else y=i}if(e.hasOwnProperty(t="MODULO_MODE")&&(ft(i=e[t],0,9,t),w=i),e.hasOwnProperty(t="POW_PRECISION")&&(ft(i=e[t],0,ut,t),k=i),e.hasOwnProperty(t="FORMAT")){if("object"!=typeof(i=e[t]))throw Error(at+t+" not an object: "+i);x=i}if(e.hasOwnProperty(t="ALPHABET")){if("string"!=typeof(i=e[t])||/^.?$|[+\-.\s]|(.).*\1/.test(i))throw Error(at+t+" invalid: "+i);S="0123456789"==i.slice(0,10),$=i}}return{DECIMAL_PLACES:_,ROUNDING_MODE:m,EXPONENTIAL_AT:[g,f],RANGE:[v,b],CRYPTO:y,MODULO_MODE:w,POW_PRECISION:k,FORMAT:x,ALPHABET:$}},M.isBigNumber=function(e){if(!e||!0!==e._isBigNumber)return!1;if(!M.DEBUG)return!0;var t,i,s=e.c,n=e.e,o=e.s;e:if("[object Array]"=={}.toString.call(s)){if((1===o||-1===o)&&n>=-ut&&n<=ut&&n===ot(n)){if(0===s[0]){if(0===n&&1===s.length)return!0;break e}if((t=(n+1)%ct)<1&&(t+=ct),String(s[0]).length==t){for(t=0;t=lt||i!==ot(i))break e;if(0!==i)return!0}}}else if(null===s&&null===n&&(null===o||1===o||-1===o))return!0;throw Error(at+"Invalid BigNumber: "+e)},M.maximum=M.max=function(){return D(arguments,-1)},M.minimum=M.min=function(){return D(arguments,1)},M.random=(o=9007199254740992,a=Math.random()*o&2097151?function(){return ot(Math.random()*o)}:function(){return 8388608*(1073741824*Math.random()|0)+(8388608*Math.random()|0)},function(e){var t,i,s,n,o,r=0,l=[],c=new M(u);if(null==e?e=_:ft(e,0,ut),n=nt(e/ct),y)if(crypto.getRandomValues){for(t=crypto.getRandomValues(new Uint32Array(n*=2));r>>11))>=9e15?(i=crypto.getRandomValues(new Uint32Array(2)),t[r]=i[0],t[r+1]=i[1]):(l.push(o%1e14),r+=2);r=n/2}else{if(!crypto.randomBytes)throw y=!1,Error(at+"crypto unavailable");for(t=crypto.randomBytes(n*=7);r=9e15?crypto.randomBytes(7).copy(t,r):(l.push(o%1e14),r+=7);r=n/7}if(!y)for(;r=10;o/=10,r++);ri-1&&(null==a[n+1]&&(a[n+1]=0),a[n+1]+=a[n]/i|0,a[n]%=i)}return a.reverse()}return function(s,n,o,a,r){var l,c,h,d,p,u,g,f,v=s.indexOf("."),b=_,y=m;for(v>=0&&(d=k,k=0,s=s.replace(".",""),u=(f=new M(n)).pow(s.length-v),k=d,f.c=t(yt(mt(u.c),u.e,"0"),10,o,e),f.e=f.c.length),h=d=(g=t(s,n,o,r?(l=$,e):(l=e,$))).length;0==g[--d];g.pop());if(!g[0])return l.charAt(0);if(v<0?--h:(u.c=g,u.e=h,u.s=a,g=(u=i(u,f,b,y,o)).c,p=u.r,h=u.e),v=g[c=h+b+1],d=o/2,p=p||c<0||null!=g[c+1],p=y<4?(null!=v||p)&&(0==y||y==(u.s<0?3:2)):v>d||v==d&&(4==y||p||6==y&&1&g[c-1]||y==(u.s<0?8:7)),c<1||!g[0])s=p?yt(l.charAt(1),-b,l.charAt(0)):l.charAt(0);else{if(g.length=c,p)for(--o;++g[--c]>o;)g[c]=0,c||(++h,g=[1].concat(g));for(d=g.length;!g[--d];);for(v=0,s="";v<=d;s+=l.charAt(g[v++]));s=yt(s,h,l.charAt(0))}return s}}(),i=function(){function e(e,t,i){var s,n,o,a,r=0,l=e.length,c=t%pt,h=t/pt|0;for(e=e.slice();l--;)r=((n=c*(o=e[l]%pt)+(s=h*o+(a=e[l]/pt|0)*c)%pt*pt+r)/i|0)+(s/pt|0)+h*a,e[l]=n%i;return r&&(e=[r].concat(e)),e}function t(e,t,i,s){var n,o;if(i!=s)o=i>s?1:-1;else for(n=o=0;nt[n]?1:-1;break}return o}function i(e,t,i,s){for(var n=0;i--;)e[i]-=n,n=e[i]1;e.splice(0,1));}return function(s,n,o,a,r){var l,c,h,d,p,u,_,m,g,f,v,b,y,w,k,x,$,S=s.s==n.s?1:-1,C=s.c,D=n.c;if(!(C&&C[0]&&D&&D[0]))return new M(s.s&&n.s&&(C?!D||C[0]!=D[0]:D)?C&&0==C[0]||!D?0*S:S/0:NaN);for(g=(m=new M(S)).c=[],S=o+(c=s.e-n.e)+1,r||(r=lt,c=_t(s.e/ct)-_t(n.e/ct),S=S/ct|0),h=0;D[h]==(C[h]||0);h++);if(D[h]>(C[h]||0)&&c--,S<0)g.push(1),d=!0;else{for(w=C.length,x=D.length,h=0,S+=2,(p=ot(r/(D[0]+1)))>1&&(D=e(D,p,r),C=e(C,p,r),x=D.length,w=C.length),y=x,v=(f=C.slice(0,x)).length;v=r/2&&k++;do{if(p=0,(l=t(D,f,x,v))<0){if(b=f[0],x!=v&&(b=b*r+(f[1]||0)),(p=ot(b/k))>1)for(p>=r&&(p=r-1),_=(u=e(D,p,r)).length,v=f.length;1==t(u,f,_,v);)p--,i(u,x<_?$:D,_,r),_=u.length,l=1;else 0==p&&(l=p=1),_=(u=D.slice()).length;if(_=10;S/=10,h++);T(m,o+(m.e=h+c*ct-1)+1,a,d)}else m.e=c,m.r=+d;return m}}(),r=/^(-?)0([xbo])(?=\w[\w.]*$)/i,l=/^([^.]+)\.$/,c=/^\.([^.]+)$/,h=/^-?(Infinity|NaN)$/,d=/^\s*\+(?=[\w.])|^\s+|\s+$/g,n=function(e,t,i,s){var n,o=i?t:t.replace(d,"");if(h.test(o))e.s=isNaN(o)?null:o<0?-1:1;else{if(!i&&(o=o.replace(r,function(e,t,i){return n="x"==(i=i.toLowerCase())?16:"b"==i?2:8,s&&s!=n?e:t}),s&&(n=s,o=o.replace(l,"$1").replace(c,"0.$1")),t!=o))return new M(o,n);if(M.DEBUG)throw Error(at+"Not a"+(s?" base "+s:"")+" number: "+t);e.s=null}e.c=e.e=null},p.absoluteValue=p.abs=function(){var e=new M(this);return e.s<0&&(e.s=1),e},p.comparedTo=function(e,t){return gt(this,new M(e,t))},p.decimalPlaces=p.dp=function(e,t){var i,s,n,o=this;if(null!=e)return ft(e,0,ut),null==t?t=m:ft(t,0,8),T(new M(o),e+o.e+1,t);if(!(i=o.c))return null;if(s=((n=i.length-1)-_t(this.e/ct))*ct,n=i[n])for(;n%10==0;n/=10,s--);return s<0&&(s=0),s},p.dividedBy=p.div=function(e,t){return i(this,new M(e,t),_,m)},p.dividedToIntegerBy=p.idiv=function(e,t){return i(this,new M(e,t),0,1)},p.exponentiatedBy=p.pow=function(e,t){var i,s,n,o,a,r,l,c,h=this;if((e=new M(e)).c&&!e.isInteger())throw Error(at+"Exponent not an integer: "+I(e));if(null!=t&&(t=new M(t)),a=e.e>14,!h.c||!h.c[0]||1==h.c[0]&&!h.e&&1==h.c.length||!e.c||!e.c[0])return c=new M(Math.pow(+I(h),a?e.s*(2-vt(e)):+I(e))),t?c.mod(t):c;if(r=e.s<0,t){if(t.c?!t.c[0]:!t.s)return new M(NaN);(s=!r&&h.isInteger()&&t.isInteger())&&(h=h.mod(t))}else{if(e.e>9&&(h.e>0||h.e<-1||(0==h.e?h.c[0]>1||a&&h.c[1]>=24e7:h.c[0]<8e13||a&&h.c[0]<=9999975e7)))return o=h.s<0&&vt(e)?-0:0,h.e>-1&&(o=1/o),new M(r?1/o:o);k&&(o=nt(k/ct+2))}for(a?(i=new M(.5),r&&(e.s=1),l=vt(e)):l=(n=Math.abs(+I(e)))%2,c=new M(u);;){if(l){if(!(c=c.times(h)).c)break;o?c.c.length>o&&(c.c.length=o):s&&(c=c.mod(t))}if(n){if(0===(n=ot(n/2)))break;l=n%2}else if(T(e=e.times(i),e.e+1,1),e.e>14)l=vt(e);else{if(0===(n=+I(e)))break;l=n%2}h=h.times(h),o?h.c&&h.c.length>o&&(h.c.length=o):s&&(h=h.mod(t))}return s?c:(r&&(c=u.div(c)),t?c.mod(t):o?T(c,k,m,void 0):c)},p.integerValue=function(e){var t=new M(this);return null==e?e=m:ft(e,0,8),T(t,t.e+1,e)},p.isEqualTo=p.eq=function(e,t){return 0===gt(this,new M(e,t))},p.isFinite=function(){return!!this.c},p.isGreaterThan=p.gt=function(e,t){return gt(this,new M(e,t))>0},p.isGreaterThanOrEqualTo=p.gte=function(e,t){return 1===(t=gt(this,new M(e,t)))||0===t},p.isInteger=function(){return!!this.c&&_t(this.e/ct)>this.c.length-2},p.isLessThan=p.lt=function(e,t){return gt(this,new M(e,t))<0},p.isLessThanOrEqualTo=p.lte=function(e,t){return-1===(t=gt(this,new M(e,t)))||0===t},p.isNaN=function(){return!this.s},p.isNegative=function(){return this.s<0},p.isPositive=function(){return this.s>0},p.isZero=function(){return!!this.c&&0==this.c[0]},p.minus=function(e,t){var i,s,n,o,a=this,r=a.s;if(t=(e=new M(e,t)).s,!r||!t)return new M(NaN);if(r!=t)return e.s=-t,a.plus(e);var l=a.e/ct,c=e.e/ct,h=a.c,d=e.c;if(!l||!c){if(!h||!d)return h?(e.s=-t,e):new M(d?a:NaN);if(!h[0]||!d[0])return d[0]?(e.s=-t,e):new M(h[0]?a:3==m?-0:0)}if(l=_t(l),c=_t(c),h=h.slice(),r=l-c){for((o=r<0)?(r=-r,n=h):(c=l,n=d),n.reverse(),t=r;t--;n.push(0));n.reverse()}else for(s=(o=(r=h.length)<(t=d.length))?r:t,r=t=0;t0)for(;t--;h[i++]=0);for(t=lt-1;s>r;){if(h[--s]=0;){for(i=0,p=b[n]%g,u=b[n]/g|0,o=n+(a=l);o>n;)i=((c=p*(c=v[--a]%g)+(r=u*c+(h=v[a]/g|0)*p)%g*g+_[o]+i)/m|0)+(r/g|0)+u*h,_[o--]=c%m;_[o]=i}return i?++s:_.splice(0,1),P(e,_,s)},p.negated=function(){var e=new M(this);return e.s=-e.s||null,e},p.plus=function(e,t){var i,s=this,n=s.s;if(t=(e=new M(e,t)).s,!n||!t)return new M(NaN);if(n!=t)return e.s=-t,s.minus(e);var o=s.e/ct,a=e.e/ct,r=s.c,l=e.c;if(!o||!a){if(!r||!l)return new M(n/0);if(!r[0]||!l[0])return l[0]?e:new M(r[0]?s:0*n)}if(o=_t(o),a=_t(a),r=r.slice(),n=o-a){for(n>0?(a=o,i=l):(n=-n,i=r),i.reverse();n--;i.push(0));i.reverse()}for((n=r.length)-(t=l.length)<0&&(i=l,l=r,r=i,t=n),n=0;t;)n=(r[--t]=r[t]+l[t]+n)/lt|0,r[t]=lt===r[t]?0:r[t]%lt;return n&&(r=[n].concat(r),++a),P(e,r,a)},p.precision=p.sd=function(e,t){var i,s,n,o=this;if(null!=e&&e!==!!e)return ft(e,1,ut),null==t?t=m:ft(t,0,8),T(new M(o),e,t);if(!(i=o.c))return null;if(s=(n=i.length-1)*ct+1,n=i[n]){for(;n%10==0;n/=10,s--);for(n=i[0];n>=10;n/=10,s++);}return e&&o.e+1>s&&(s=o.e+1),s},p.shiftedBy=function(e){return ft(e,-9007199254740991,ht),this.times("1e"+e)},p.squareRoot=p.sqrt=function(){var e,t,s,n,o,a=this,r=a.c,l=a.s,c=a.e,h=_+4,d=new M("0.5");if(1!==l||!r||!r[0])return new M(!l||l<0&&(!r||r[0])?NaN:r?a:1/0);if(0==(l=Math.sqrt(+I(a)))||l==1/0?(((t=mt(r)).length+c)%2==0&&(t+="0"),l=Math.sqrt(+t),c=_t((c+1)/2)-(c<0||c%2),s=new M(t=l==1/0?"5e"+c:(t=l.toExponential()).slice(0,t.indexOf("e")+1)+c)):s=new M(l+""),s.c[0])for((l=(c=s.e)+h)<3&&(l=0);;)if(o=s,s=d.times(o.plus(i(a,o,h,1))),mt(o.c).slice(0,l)===(t=mt(s.c)).slice(0,l)){if(s.e0&&_>0){for(o=_%r||r,h=u.substr(0,o);o<_;o+=r)h+=c+u.substr(o,r);l>0&&(h+=c+u.slice(o)),p&&(h="-"+h)}s=d?h+(i.decimalSeparator||"")+((l=+i.fractionGroupSize)?d.replace(new RegExp("\\d{"+l+"}\\B","g"),"$&"+(i.fractionGroupSeparator||"")):d):h}return(i.prefix||"")+s+(i.suffix||"")},p.toFraction=function(e){var t,s,n,o,a,r,l,c,h,d,p,_,g=this,f=g.c;if(null!=e&&(!(l=new M(e)).isInteger()&&(l.c||1!==l.s)||l.lt(u)))throw Error(at+"Argument "+(l.isInteger()?"out of range: ":"not an integer: ")+I(l));if(!f)return new M(g);for(t=new M(u),h=s=new M(u),n=c=new M(u),_=mt(f),a=t.e=_.length-g.e-1,t.c[0]=dt[(r=a%ct)<0?ct+r:r],e=!e||l.comparedTo(t)>0?a>0?t:h:l,r=b,b=1/0,l=new M(_),c.c[0]=0;d=i(l,t,0,1),1!=(o=s.plus(d.times(n))).comparedTo(e);)s=n,n=o,h=c.plus(d.times(o=h)),c=o,t=l.minus(d.times(o=t)),l=o;return o=i(e.minus(s),n,0,1),c=c.plus(o.times(h)),s=s.plus(o.times(n)),c.s=h.s=g.s,p=i(h,n,a*=2,m).minus(g).abs().comparedTo(i(c,s,a,m).minus(g).abs())<1?[h,n]:[c,s],b=r,p},p.toNumber=function(){return+I(this)},p.toPrecision=function(e,t){return null!=e&&ft(e,1,ut),C(this,e,t,2)},p.toString=function(e){var t,i=this,n=i.s,o=i.e;return null===o?n?(t="Infinity",n<0&&(t="-"+t)):t="NaN":(null==e?t=o<=g||o>=f?bt(mt(i.c),o):yt(mt(i.c),o,"0"):10===e&&S?t=yt(mt((i=T(new M(i),_+o+1,m)).c),i.e,"0"):(ft(e,2,$.length,"Base"),t=s(yt(mt(i.c),o,"0"),10,e,n,!0)),n<0&&i.c[0]&&(t="-"+t)),t},p.valueOf=p.toJSON=function(){return I(this)},p._isBigNumber=!0,p[Symbol.toStringTag]="BigNumber",p[Symbol.for("nodejs.util.inspect.custom")]=p.valueOf,null!=t&&M.set(t),M}(),kt=class{key;left=null;right=null;constructor(e){this.key=e}},xt=class extends kt{constructor(e){super(e)}},$t=class{size=0;modificationCount=0;splayCount=0;splay(e){const t=this.root;if(null==t)return this.compare(e,e),-1;let i=null,s=null,n=null,o=null,a=t;const r=this.compare;let l;for(;;)if(l=r(a.key,e),l>0){let t=a.left;if(null==t)break;if(l=r(t.key,e),l>0&&(a.left=t.right,t.right=a,a=t,t=a.left,null==t))break;null==i?s=a:i.left=a,i=a,a=t}else{if(!(l<0))break;{let t=a.right;if(null==t)break;if(l=r(t.key,e),l<0&&(a.right=t.left,t.left=a,a=t,t=a.right,null==t))break;null==n?o=a:n.right=a,n=a,a=t}}return null!=n&&(n.right=a.left,a.left=o),null!=i&&(i.left=a.right,a.right=s),this.root!==a&&(this.root=a,this.splayCount++),l}splayMin(e){let t=e,i=t.left;for(;null!=i;){const e=i;t.left=e.right,e.right=t,t=e,i=t.left}return t}splayMax(e){let t=e,i=t.right;for(;null!=i;){const e=i;t.right=e.left,e.left=t,t=e,i=t.right}return t}_delete(e){if(null==this.root)return null;if(0!=this.splay(e))return null;let t=this.root;const i=t,s=t.left;if(this.size--,null==s)this.root=t.right;else{const e=t.right;t=this.splayMax(s),t.right=e,this.root=t}return this.modificationCount++,i}addNewRoot(e,t){this.size++,this.modificationCount++;const i=this.root;null!=i?(t<0?(e.left=i,e.right=i.right,i.right=null):(e.right=i,e.left=i.left,i.left=null),this.root=e):this.root=e}_first(){const e=this.root;return null==e?null:(this.root=this.splayMin(e),this.root)}_last(){const e=this.root;return null==e?null:(this.root=this.splayMax(e),this.root)}clear(){this.root=null,this.size=0,this.modificationCount++}has(e){return this.validKey(e)&&0==this.splay(e)}defaultCompare(){return(e,t)=>et?1:0}wrap(){return{getRoot:()=>this.root,setRoot:e=>{this.root=e},getSize:()=>this.size,getModificationCount:()=>this.modificationCount,getSplayCount:()=>this.splayCount,setSplayCount:e=>{this.splayCount=e},splay:e=>this.splay(e),has:e=>this.has(e)}}},St=class e extends $t{root=null;compare;validKey;constructor(e,t){super(),this.compare=e??this.defaultCompare(),this.validKey=t??(e=>null!=e&&null!=e)}delete(e){return!!this.validKey(e)&&null!=this._delete(e)}deleteAll(e){for(const t of e)this.delete(t)}forEach(e){const t=this[Symbol.iterator]();let i;for(;i=t.next(),!i.done;)e(i.value,i.value,this)}add(e){const t=this.splay(e);return 0!=t&&this.addNewRoot(new xt(e),t),this}addAndReturn(e){const t=this.splay(e);return 0!=t&&this.addNewRoot(new xt(e),t),this.root.key}addAll(e){for(const t of e)this.add(t)}isEmpty(){return null==this.root}isNotEmpty(){return null!=this.root}single(){if(0==this.size)throw"Bad state: No element";if(this.size>1)throw"Bad state: Too many element";return this.root.key}first(){if(0==this.size)throw"Bad state: No element";return this._first().key}last(){if(0==this.size)throw"Bad state: No element";return this._last().key}lastBefore(e){if(null==e)throw"Invalid arguments(s)";if(null==this.root)return null;if(this.splay(e)<0)return this.root.key;let t=this.root.left;if(null==t)return null;let i=t.right;for(;null!=i;)t=i,i=t.right;return t.key}firstAfter(e){if(null==e)throw"Invalid arguments(s)";if(null==this.root)return null;if(this.splay(e)>0)return this.root.key;let t=this.root.right;if(null==t)return null;let i=t.left;for(;null!=i;)t=i,i=t.left;return t.key}retainAll(t){const i=new e(this.compare,this.validKey),s=this.modificationCount;for(const e of t){if(s!=this.modificationCount)throw"Concurrent modification during iteration.";this.validKey(e)&&0==this.splay(e)&&i.add(this.root.key)}i.size!=this.size&&(this.root=i.root,this.size=i.size,this.modificationCount++)}lookup(e){if(!this.validKey(e))return null;return 0!=this.splay(e)?null:this.root.key}intersection(t){const i=new e(this.compare,this.validKey);for(const e of this)t.has(e)&&i.add(e);return i}difference(t){const i=new e(this.compare,this.validKey);for(const e of this)t.has(e)||i.add(e);return i}union(e){const t=this.clone();return t.addAll(e),t}clone(){const t=new e(this.compare,this.validKey);return t.size=this.size,t.root=this.copyNode(this.root),t}copyNode(e){if(null==e)return null;const t=new xt(e.key);return function e(t,i){let s,n;do{if(s=t.left,n=t.right,null!=s){const t=new xt(s.key);i.left=t,e(s,t)}if(null!=n){const e=new xt(n.key);i.right=e,t=n,i=e}}while(null!=n)}(e,t),t}toSet(){return this.clone()}entries(){return new Dt(this.wrap())}keys(){return this[Symbol.iterator]()}values(){return this[Symbol.iterator]()}[Symbol.iterator](){return new Ct(this.wrap())}[Symbol.toStringTag]="[object Set]"},Mt=class{tree;path=new Array;modificationCount=null;splayCount;constructor(e){this.tree=e,this.splayCount=e.getSplayCount()}[Symbol.iterator](){return this}next(){return this.moveNext()?{done:!1,value:this.current()}:{done:!0,value:null}}current(){if(!this.path.length)return null;const e=this.path[this.path.length-1];return this.getValue(e)}rebuildPath(e){this.path.splice(0,this.path.length),this.tree.splay(e),this.path.push(this.tree.getRoot()),this.splayCount=this.tree.getSplayCount()}findLeftMostDescendent(e){for(;null!=e;)this.path.push(e),e=e.left}moveNext(){if(this.modificationCount!=this.tree.getModificationCount()){if(null==this.modificationCount){this.modificationCount=this.tree.getModificationCount();let e=this.tree.getRoot();for(;null!=e;)this.path.push(e),e=e.left;return this.path.length>0}throw"Concurrent modification during iteration."}if(!this.path.length)return!1;this.splayCount!=this.tree.getSplayCount()&&this.rebuildPath(this.path[this.path.length-1].key);let e=this.path[this.path.length-1],t=e.right;if(null!=t){for(;null!=t;)this.path.push(t),t=t.left;return!0}for(this.path.pop();this.path.length&&this.path[this.path.length-1].right===e;)e=this.path.pop();return this.path.length>0}},Ct=class extends Mt{getValue(e){return e.key}},Dt=class extends Mt{getValue(e){return[e.key,e.key]}},Pt=e=>()=>e,Tt=e=>{const t=e?(t,i)=>i.minus(t).abs().isLessThanOrEqualTo(e):Pt(!1);return(e,i)=>t(e,i)?0:e.comparedTo(i)};function It(e){const t=e?(t,i,s,n,o)=>t.exponentiatedBy(2).isLessThanOrEqualTo(n.minus(i).exponentiatedBy(2).plus(o.minus(s).exponentiatedBy(2)).times(e)):Pt(!1);return(e,i,s)=>{const n=e.x,o=e.y,a=s.x,r=s.y,l=o.minus(r).times(i.x.minus(a)).minus(n.minus(a).times(i.y.minus(r)));return t(l,n,o,a,r)?0:l.comparedTo(0)}}var Rt=e=>e,At=e=>{if(e){const t=new St(Tt(e)),i=new St(Tt(e)),s=(e,t)=>t.addAndReturn(e),n=e=>({x:s(e.x,t),y:s(e.y,i)});return n({x:new wt(0),y:new wt(0)}),n}return Rt},zt=e=>({set:e=>{Ft=zt(e)},reset:()=>zt(e),compare:Tt(e),snap:At(e),orient:It(e)}),Ft=zt(),Nt=(e,t)=>e.ll.x.isLessThanOrEqualTo(t.x)&&t.x.isLessThanOrEqualTo(e.ur.x)&&e.ll.y.isLessThanOrEqualTo(t.y)&&t.y.isLessThanOrEqualTo(e.ur.y),Et=(e,t)=>{if(t.ur.x.isLessThan(e.ll.x)||e.ur.x.isLessThan(t.ll.x)||t.ur.y.isLessThan(e.ll.y)||e.ur.y.isLessThan(t.ll.y))return null;const i=e.ll.x.isLessThan(t.ll.x)?t.ll.x:e.ll.x,s=e.ur.x.isLessThan(t.ur.x)?e.ur.x:t.ur.x;return{ll:{x:i,y:e.ll.y.isLessThan(t.ll.y)?t.ll.y:e.ll.y},ur:{x:s,y:e.ur.y.isLessThan(t.ur.y)?e.ur.y:t.ur.y}}},Ot=(e,t)=>e.x.times(t.y).minus(e.y.times(t.x)),Ht=(e,t)=>e.x.times(t.x).plus(e.y.times(t.y)),Lt=e=>Ht(e,e).sqrt(),Bt=(e,t,i)=>{const s={x:t.x.minus(e.x),y:t.y.minus(e.y)},n={x:i.x.minus(e.x),y:i.y.minus(e.y)};return Ot(n,s).div(Lt(n)).div(Lt(s))},jt=(e,t,i)=>{const s={x:t.x.minus(e.x),y:t.y.minus(e.y)},n={x:i.x.minus(e.x),y:i.y.minus(e.y)};return Ht(n,s).div(Lt(n)).div(Lt(s))},qt=(e,t,i)=>t.y.isZero()?null:{x:e.x.plus(t.x.div(t.y).times(i.minus(e.y))),y:i},Wt=(e,t,i)=>t.x.isZero()?null:{x:i,y:e.y.plus(t.y.div(t.x).times(i.minus(e.x)))},Ut=class e{point;isLeft;segment;otherSE;consumedBy;static compare(t,i){const s=e.comparePoints(t.point,i.point);return 0!==s?s:(t.point!==i.point&&t.link(i),t.isLeft!==i.isLeft?t.isLeft?1:-1:Qt.compare(t.segment,i.segment))}static comparePoints(e,t){return e.x.isLessThan(t.x)?-1:e.x.isGreaterThan(t.x)?1:e.y.isLessThan(t.y)?-1:e.y.isGreaterThan(t.y)?1:0}constructor(e,t){void 0===e.events?e.events=[this]:e.events.push(this),this.point=e,this.isLeft=t}link(e){if(e.point===this.point)throw new Error("Tried to link already linked events");const t=e.point.events;for(let e=0,i=t.length;e{const s=i.otherSE;t.set(i,{sine:Bt(this.point,e.point,s.point),cosine:jt(this.point,e.point,s.point)})};return(e,s)=>{t.has(e)||i(e),t.has(s)||i(s);const{sine:n,cosine:o}=t.get(e),{sine:a,cosine:r}=t.get(s);return n.isGreaterThanOrEqualTo(0)&&a.isGreaterThanOrEqualTo(0)?o.isLessThan(r)?1:o.isGreaterThan(r)?-1:0:n.isLessThan(0)&&a.isLessThan(0)?o.isLessThan(r)?-1:o.isGreaterThan(r)?1:0:a.isLessThan(n)?-1:a.isGreaterThan(n)?1:0}}},Gt=class e{events;poly;_isExteriorRing;_enclosingRing;static factory(t){const i=[];for(let s=0,n=t.length;s0&&(e=i)}let t=e.segment.prevInResult(),i=t?t.prevInResult():null;for(;;){if(!t)return null;if(!i)return t.ringOut;if(i.ringOut!==t.ringOut)return i.ringOut?.enclosingRing()!==t.ringOut?t.ringOut:t.ringOut?.enclosingRing();t=i.prevInResult(),i=t?t.prevInResult():null}}},Vt=class{exteriorRing;interiorRings;constructor(e){this.exteriorRing=e,e.poly=this,this.interiorRings=[]}addInterior(e){this.interiorRings.push(e),e.poly=this}getGeom(){const e=this.exteriorRing.getGeom();if(null===e)return null;const t=[e];for(let e=0,i=this.interiorRings.length;e0?(this.tree.delete(t),i.push(e)):(this.segments.push(t),t.prev=s)}else{if(s&&n){const e=s.getIntersection(n);if(null!==e){if(!s.isAnEndpoint(e)){const t=this._splitSafely(s,e);for(let e=0,s=t.length;e0)return-1;const s=t.comparePoint(e.rightSE.point);return 0!==s?s:-1}if(i.isGreaterThan(s)){if(a.isLessThan(r)&&a.isLessThan(c))return-1;if(a.isGreaterThan(r)&&a.isGreaterThan(c))return 1;const i=t.comparePoint(e.leftSE.point);if(0!==i)return i;const s=e.comparePoint(t.rightSE.point);return s<0?1:s>0?-1:1}if(a.isLessThan(r))return-1;if(a.isGreaterThan(r))return 1;if(n.isLessThan(o)){const i=t.comparePoint(e.rightSE.point);if(0!==i)return i}if(n.isGreaterThan(o)){const i=e.comparePoint(t.rightSE.point);if(i<0)return 1;if(i>0)return-1}if(!n.eq(o)){const e=l.minus(a),t=n.minus(i),h=c.minus(r),d=o.minus(s);if(e.isGreaterThan(t)&&h.isLessThan(d))return 1;if(e.isLessThan(t)&&h.isGreaterThan(d))return-1}return n.isGreaterThan(o)?1:n.isLessThan(o)||l.isLessThan(c)?-1:l.isGreaterThan(c)?1:e.idt.id?1:0}constructor(e,t,i,s){this.id=++Zt,this.leftSE=e,e.segment=this,e.otherSE=t,this.rightSE=t,t.segment=this,t.otherSE=e,this.rings=i,this.windings=s}static fromRing(t,i,s){let n,o,a;const r=Ut.comparePoints(t,i);if(r<0)n=t,o=i,a=1;else{if(!(r>0))throw new Error(`Tried to create degenerate segment at [${t.x}, ${t.y}]`);n=i,o=t,a=-1}const l=new Ut(n,!0),c=new Ut(o,!1);return new e(l,c,[s],[a])}replaceRightSE(e){this.rightSE=e,this.rightSE.segment=this,this.rightSE.otherSE=this.leftSE,this.leftSE.otherSE=this.rightSE}bbox(){const e=this.leftSE.point.y,t=this.rightSE.point.y;return{ll:{x:this.leftSE.point.x,y:e.isLessThan(t)?e:t},ur:{x:this.rightSE.point.x,y:e.isGreaterThan(t)?e:t}}}vector(){return{x:this.rightSE.point.x.minus(this.leftSE.point.x),y:this.rightSE.point.y.minus(this.leftSE.point.y)}}isAnEndpoint(e){return e.x.eq(this.leftSE.point.x)&&e.y.eq(this.leftSE.point.y)||e.x.eq(this.rightSE.point.x)&&e.y.eq(this.rightSE.point.y)}comparePoint(e){return Ft.orient(this.leftSE.point,e,this.rightSE.point)}getIntersection(e){const t=this.bbox(),i=e.bbox(),s=Et(t,i);if(null===s)return null;const n=this.leftSE.point,o=this.rightSE.point,a=e.leftSE.point,r=e.rightSE.point,l=Nt(t,a)&&0===this.comparePoint(a),c=Nt(i,n)&&0===e.comparePoint(n),h=Nt(t,r)&&0===this.comparePoint(r),d=Nt(i,o)&&0===e.comparePoint(o);if(c&&l)return d&&!h?o:!d&&h?r:null;if(c)return h&&n.x.eq(r.x)&&n.y.eq(r.y)?null:n;if(l)return d&&o.x.eq(a.x)&&o.y.eq(a.y)?null:a;if(d&&h)return null;if(d)return o;if(h)return r;const p=((e,t,i,s)=>{if(t.x.isZero())return Wt(i,s,e.x);if(s.x.isZero())return Wt(e,t,i.x);if(t.y.isZero())return qt(i,s,e.y);if(s.y.isZero())return qt(e,t,i.y);const n=Ot(t,s);if(n.isZero())return null;const o={x:i.x.minus(e.x),y:i.y.minus(e.y)},a=Ot(o,t).div(n),r=Ot(o,s).div(n),l=e.x.plus(r.times(t.x)),c=i.x.plus(a.times(s.x)),h=e.y.plus(r.times(t.y)),d=i.y.plus(a.times(s.y));return{x:l.plus(c).div(2),y:h.plus(d).div(2)}})(n,this.vector(),a,e.vector());return null===p?null:Nt(s,p)?Ft.snap(p):null}split(t){const i=[],s=void 0!==t.events,n=new Ut(t,!0),o=new Ut(t,!1),a=this.rightSE;this.replaceRightSE(o),i.push(o),i.push(n);const r=new e(n,a,this.rings.slice(),this.windings.slice());return Ut.comparePoints(r.leftSE.point,r.rightSE.point)>0&&r.swapEvents(),Ut.comparePoints(this.leftSE.point,this.rightSE.point)>0&&this.swapEvents(),s&&(n.checkForConsuming(),o.checkForConsuming()),i}swapEvents(){const e=this.rightSE;this.rightSE=this.leftSE,this.leftSE=e,this.leftSE.isLeft=!0,this.rightSE.isLeft=!1;for(let e=0,t=this.windings.length;e0){const e=i;i=s,s=e}if(i.prev===s){const e=i;i=s,s=e}for(let e=0,t=s.rings.length;e1===e.length&&e[0].isSubject;this._isInResult=i(e)!==i(t);break}}return this._isInResult}},ei=class{poly;isExterior;segments;bbox;constructor(e,t,i){if(!Array.isArray(e)||0===e.length)throw new Error("Input geometry is not a valid Polygon or MultiPolygon");if(this.poly=t,this.isExterior=i,this.segments=[],"number"!=typeof e[0][0]||"number"!=typeof e[0][1])throw new Error("Input geometry is not a valid Polygon or MultiPolygon");const s=Ft.snap({x:new wt(e[0][0]),y:new wt(e[0][1])});this.bbox={ll:{x:s.x,y:s.y},ur:{x:s.x,y:s.y}};let n=s;for(let t=1,i=e.length;tXt.run("union",e,t),ni=(e,...t)=>Xt.run("intersection",e,t),oi=(e,...t)=>Xt.run("difference",e,t);function ai(e){const t=Math.max(0,Math.min(120,(e-40)/140*120));return`hsl(${Math.round(t)}, 85%, 55%)`}function ri(e,t){if(!(Number.isFinite(e)&&t>0))return e;const i=Math.round(e/t)*t;return Math.abs(i-e)<=1e-9*t?e:i}function li(e,t){if(t){const t=e/2.54;let i=Math.floor(t/12),s=Math.round(t-12*i);return 12===s&&(i+=1,s=0),`${i}′ ${s}″`}return`${(e/100).toFixed(2)} m`}function ci(e,t,i=1){const s=e[0].toFixed(i),n=e[1].toFixed(i),o=t[0].toFixed(i),a=t[1].toFixed(i),r=s=3?e.poly:e&&null!=e.x&&null!=e.y&&null!=e.w&&null!=e.h?[[e.x,e.y],[e.x+e.w,e.y],[e.x+e.w,e.y+e.h],[e.x,e.y+e.h]]:null}function di(e){const t=[];for(const i of e||[])i?.poly?.length>=3?t.push({poly:i.poly.map(e=>e.join(",")).join(" ")}):i&&null!=i.x&&null!=i.y&&null!=i.w&&null!=i.h&&t.push({rect:{x:i.x,y:i.y,w:i.w,h:i.h,rx:.03*Math.min(i.w,i.h)}});return t}function pi(e){const t=[],i=new Set;for(const s of e||[]){const e=hi(s);if(e)for(let s=0;s=90?e-=180:e<-90&&(e+=180),s.step&&s.step>0){const i=Math.sqrt(d),o=Math.min(Math.max(s.length||0,0)/2,i/2);let r=Math.round(p*i/s.step)*s.step;Math.abs(p*i-i/2)<=s.step/2&&(r=i/2),r=Math.max(o,Math.min(i-o,r));const l=r/i;n={x:t+l*c,y:a+l*h,angle:e}}else n={x:u[0],y:u[1],angle:e}}}return n}function _i(e,t,i,s,n,o=1){const a=t*Math.PI/180,r=[Math.cos(a),Math.sin(a)];let l=null,c=o;for(const t of pi(s)){const i=[[t[0],t[1]],[t[2],t[3]]],s=t=>Math.abs(r[0]*(t[1]-e[1])-r[1]*(t[0]-e[0]));if(s(i[0])>o||s(i[1])>o)continue;const n=(i[0][0]-e[0])*r[0]+(i[0][1]-e[1])*r[1],a=(i[1][0]-e[0])*r[0]+(i[1][1]-e[1])*r[1],h=Math.min(n,a),d=Math.max(n,a),p=h>0?h:d<0?-d:0;p[e[0]+r[0]*t,e[1]+r[1]*t],g=(h+d)/2;return{wallA:m(h),wallB:m(d),sideA:u,sideB:_,midA:m((h-p)/2),midB:m((p+d)/2),wallCenter:m(g),centered:Math.abs(g)<=n}}function mi(e,t,i=!1){if("passage"===e)return 1;if(null==t||"unavailable"===t||"unknown"===t)return"window"===e?0:1;const s=function(e){return["on","open","home","detected","playing","cleaning"].includes(String(e))}(t)!==!!i;return s?1:0}function gi(e){return"passage"===e.type?[]:[e.contact,e.lock].filter(e=>"string"==typeof e&&e.length>0)}function fi(e,t,i=.001){return Math.abs(e[0]-t[0])e[1]!=l>e[1]&&e[0]<(r-o)*(e[1]-a)/(l-a)+o&&(i=!i)}return i}function bi(e,t,i){const s=i[0]-t[0],n=i[1]-t[1],o=s*s+n*n;let a=o?((e[0]-t[0])*s+(e[1]-t[1])*n)/o:0;return a=Math.max(0,Math.min(1,a)),Math.hypot(e[0]-(t[0]+a*s),e[1]-(t[1]+a*n))}function yi(e,t,i=1e-6){if(!t||t.length<2)return!1;for(let s=0;sn&&a<-n||o<-n&&a>n)&&(r>n&&l<-n||r<-n&&l>n)}function $i(e,t=24){const i=e.map(e=>e[0]),s=e.map(e=>e[1]),n=Math.min(...i),o=Math.max(...i),a=Math.min(...s),r=Math.max(...s),l=Math.max(o-n,r-a)||1;let c=0,h=0,d=0;for(let t=0;t1e-9?[h/(3*c),d/(3*c)]:[(n+o)/2,(a+r)/2],u=(t,i)=>{const s=((t,i)=>{if(!vi([t,i],e))return-1/0;let s=1/0;for(let n=0;nm&&(m=c,_=[s,l])}if(_){const[e,i]=_,s=(o-n)/t,l=(r-a)/t;for(let t=-4;t<=4;t++)for(let n=-4;n<=4;n++){const o=e+s*t/4,a=i+l*n/4,r=u(o,a);r>m&&(m=r,_=[o,a])}}return _||Si(e)||e[0]}function Si(e,t=1e-6){if(!e||e.length<3)return null;const i=e.length,s=[e.reduce((e,t)=>e+t[0],0)/i,e.reduce((e,t)=>e+t[1],0)/i];if(wi(s,e,t))return s;for(let s=0;sCi(e,t,i));return s.filter(e=>!s.some(t=>t!==e&&Ci(t,e,i)))}function Ti(e){if(!e||e.length<3)return 0;let t=0;for(let i=0;i[e[0],e[1]]),[e[0][0],e[0][1]]]]}function Ri(e,t,i){for(let s=0;s1&&fi(i[0],i[i.length-1],t)&&i.pop(),i}function zi(e,t,i=1e-6){if(!e||e.length<3||!t||t.length<2)return null;const s=t[0],n=t[t.length-1];if(fi(s,n,i))return null;const o=Ri(e,s,i),a=Ri(e,n,i);if(o<0||a<0)return null;const r=t.slice(1,-1);for(const t of r)if(!wi(t,e,i))return null;for(let i=0;i{const a=[t];let r=(s+1)%e.length;for(let t=0;t<=e.length&&(a.push(e[r]),r!==o);t++)r=(r+1)%e.length;return a.push(n),Ai(a,i)};let c,h;if(o===a){const a=Ai([...t],i);if(a.length<3||Ti(a)<=i)return null;const r=[];for(let i=0;i=0?t:[...t].reverse();for(const e of i)r.push(e)}c=Ai(r,i),h=a}else c=Ai([...l(s,o,n,a),...[...r].reverse()],i),h=Ai([...l(n,a,s,o),...r],i);return c.length<3||h.length<3||Ti(c)<=i||Ti(h)<=i||Math.abs(Ti(c)+Ti(h)-Ti(e))>Math.max(i,1e-6*Ti(e))?null:[c,h]}function Fi(e,t,i){const[s,n]=e.split(":");return"device"===s?n:"entity"===s?"lg_"+n:t&&t.startsWith("v_")?t:i()}function Ni(e){return e.length?Math.round(e.reduce((e,t)=>e+t,0)/e.length):null}function Ei(e,t){if(t>e[2]/e[3]){const i=e[3],s=e[3]*t;return{x:e[0]-(s-e[2])/2,y:e[1],w:s,h:i}}const i=e[2],s=e[2]/t;return{x:e[0],y:e[1]-(s-e[3])/2,w:i,h:s}}function Oi(e,t,i,s){if(e.length<2)return;const n=t.x+s,o=t.x+t.w-s,a=t.y+s,r=t.y+t.h-s;for(let t=0;t<60;t++){let t=!1;for(let s=0;s=0)i=t.slice(0,n).trim(),s=t.slice(n+1).trim();else{const e=t.split(".");e.length>2&&(i=e.slice(0,2).join("."),s=e.slice(2).join("."))}return/^[a-z0-9_]+\.[a-z0-9_]+$/.test(i)?n>=0&&!s||s&&!/^[a-zA-Z0-9_.-]+$/.test(s)?null:s?{entity:i,attr:s}:{entity:i}:null}function Ji(e,t){const i=String(e??"").trim(),s=String(t??"").trim(),n=Yi(s?`${i}:${s}`:i);return n?`{${n.entity}${n.attr?`:${n.attr}`:""}}`:""}function Xi(e,t,i){const s=String(t??"").trim();if(!s)return null;const n=e?.states?.[s];if(!n)return null;const o=String(i??"").trim(),a=e=>e.slice(0,60);if(o){const t=function(e){if(null==e)return null;if(Array.isArray(e)){const t=e.map(e=>null==e?"":String(e)).join(", ");return t?t.slice(0,60):null}if("object"==typeof e)return null;const t=String(e);return""===t?null:t.slice(0,60)}(n.attributes?.[o]);if(null===t)return null;const i=e?.formatEntityAttributeValue;if("function"==typeof i)try{const t=i.call(e,n,o);if("string"==typeof t&&""!==t)return{text:a(t),formatted:!0}}catch{}return{text:t,formatted:!1}}const r=n.state;if(null==r||""===r)return null;const l=e?.formatEntityState;if("function"==typeof l)try{const t=l.call(e,n);if("string"==typeof t&&""!==t)return{text:a(t),formatted:!0}}catch{}return{text:a(String(r)),formatted:!1}}function Zi(e,t,i){const s=String(t??"").trim(),n=String(i??"").trim()||s;if(!n)return e.text;const o=e.formatted&&s?function(e,t){if(!t)return e;const i=e.replace(/\s+$/,"");return i.endsWith(t)?i.slice(0,i.length-t.length).replace(/\s+$/,""):e}(e.text,s):e.text;return`${o} ${n}`}function Qi(e,t){const i=(t?.entity||"").trim();if(!i)return"";const s=e?.states?.[i],n=s?.state;if(!s||null==n||""===n||"unavailable"===n||"unknown"===n)return Vi;const o=(t?.attr||"").trim(),a=Xi(e,i,o||null);if(null===a)return Vi;return Zi(a,o?"":String(s.attributes?.unit_of_measurement??"").trim(),t?.unit)}function es(e,t,i,s=()=>!0){const n=e??"";let o=!1;const a=n.replace(/\{([^{}\r\n]+)\}/g,(e,t)=>{const n=Yi(t);return n?(o=!0,s(n.entity||"")?Qi(i,n):Vi):e});if(o)return a;const r=(t?.entity||"").trim();if(!r)return n;if(!s(r)){const e=n.indexOf(Ki);return e>=0?n.slice(0,e)+Vi+n.slice(e+2):n?`${n} ${Vi}`:Vi}const l=Qi(i,t),c=n.indexOf(Ki);return c>=0?n.slice(0,c)+l+n.slice(c+2):n?`${n} ${l}`:l}const ts=20;function is(e){const t=Number(e?.scale);if(Number.isFinite(t)&&t>0)return Math.min(20,Math.max(.15,t));return{s:.7,m:1,l:1.5}[String(e?.size??"")]??1}function ss(e,t){if(!t)return e;let i=e;for(const[e,s]of Object.entries(t))i=i.split("{"+e+"}").join(String(s));return i}const ns="#55606c",os=.55,as={c:"#607d8b",a:.18};function rs(e,t=as){const i=e&&"object"==typeof e?e:null,s=i?.a;return{c:Ie(i?.c,t.c),a:"number"==typeof s&&Number.isFinite(s)?Math.min(1,Math.max(0,s)):t.a}}function ls(e,t){const i=rs(e),s=t?.settings?.custom_fill;return s&&"object"==typeof s?rs(s,i):i}function cs(e){const t=e?.settings||{},i=!e?.plan_url,s="glow"===t.fill_mode;return{showBorders:t.show_borders??i,showNames:t.show_names??i,color:Ie(t.room_color,ns),opacity:"number"==typeof t.room_opacity?Math.min(1,Math.max(0,t.room_opacity)):os,fill:["lqi","light","temp","custom"].includes(t.fill_mode)?t.fill_mode:"none",customFill:rs(t.custom_fill),glow:"boolean"==typeof t.glow_enabled?t.glow_enabled:s,tempMin:"number"==typeof t.temp_min?t.temp_min:20,tempMax:"number"==typeof t.temp_max?t.temp_max:25,showLqi:"boolean"==typeof t.show_lqi?t.show_lqi:null,cardFontScale:"number"==typeof t.card_font_scale&&t.card_font_scale>0?Math.min(3,Math.max(.5,t.card_font_scale)):1,labelTemp:!0===t.label_temp,labelHum:!0===t.label_hum,labelLqi:!0===t.label_lqi,labelLight:!0===t.label_light,bgColor:Ie(t.bg_color,null),hideDecor:!0===t.hide_decor,hideOpenings:!0===t.hide_openings}}function hs(e,t){if(t.bgColor)return t.bgColor;const i=e?.bg_color;return Ie(i,"")}const ds={light_on:{c:"#ffd45c",a:.18},light_off:{c:"#9aa0a6",a:.14},light_none:{c:"#6b7480",a:0},temp_cold:{c:"#4fc3f7",a:.18},temp_ok:{c:"#66d17a",a:.18},temp_hot:{c:"#ffd45c",a:.18},lqi_low:{c:"#f25a4a",a:.18},lqi_high:{c:"#4bd28f",a:.18},glow_base:{c:"#0d1b2a",a:.5},glow_light:{c:"#ffd9a0",a:.85},wall_fill:{c:"#ffffff",a:1}};function ps(e){const t={},i=e?.fill_colors||{};for(const e of Object.keys(ds)){const s=ds[e],n=i[e];t[e]={c:Ie(n?.c,s.c),a:n&&"number"==typeof n.a?Math.min(1,Math.max(0,n.a)):s.a}}return t}function us(e,t,i){const s=Math.min(1,Math.max(0,i)),n=[1,3,5].map(t=>parseInt(e.slice(t,t+2),16)),o=[1,3,5].map(e=>parseInt(t.slice(e,e+2),16)),a=n.map((e,t)=>Math.round(e+(o[t]-e)*s));return"#"+a.map(e=>e.toString(16).padStart(2,"0")).join("")}function _s(e,t,i,s,n,o,a,r=as){const l=function(e,t,i,s,n,o,a,r=as){if("custom"===e)return rs(r);if("lqi"===e){if(null==t)return null;const e=(t-40)/140;return{c:us(a.lqi_low.c,a.lqi_high.c,e),a:a.lqi_low.a+(a.lqi_high.a-a.lqi_low.a)*Math.min(1,Math.max(0,e))}}if("light"===e)return"none"===i?a.light_none.a>0?a.light_none:null:"on"===i?a.light_on:a.light_off;if("temp"===e){if(null==s)return null;const e=Math.min(n,o),t=Math.max(n,o);return st?a.temp_hot:a.temp_ok}return null}(e,t,i,s,n,o,a,r);return l?{color:l.c,opacity:l.a,mode:e}:null}const ms={blind:["mdi:blinds","mdi:blinds-open"],shade:["mdi:blinds","mdi:blinds-open"],shutter:["mdi:window-shutter","mdi:window-shutter-open"],curtain:["mdi:curtains-closed","mdi:curtains"],window:["mdi:window-closed","mdi:window-open"],awning:["mdi:awning-outline","mdi:awning"],door:["mdi:door-closed","mdi:door-open"],garage:["mdi:garage","mdi:garage-open"],gate:["mdi:gate","mdi:gate-open"],damper:["mdi:circle-slice-8","mdi:circle-outline"]},gs=[["mdi:roller-shade-closed","mdi:roller-shade"],["mdi:blinds-horizontal-closed","mdi:blinds-horizontal"],["mdi:garage-variant","mdi:garage-open-variant"],["mdi:door","mdi:door-open"]];function fs(e){for(const t of[...Object.values(ms),...gs])if(e===t[0]||e===t[1])return t;return null}function vs(e,t,i,s,n){if(!s||"unavailable"===s||"unknown"===s)return e;if(n){const i="cover"===t?fs(e):null;return i?"closed"===s?i[0]:i[1]:e}if("binary_sensor"===t){if("door"===i)return"on"===s?"mdi:door-open":"mdi:door-closed";if("window"===i)return"on"===s?"mdi:window-open":"mdi:window-closed";if("garage_door"===i)return"on"===s?"mdi:garage-open-variant":"mdi:garage-variant"}if("cover"===t){const t=ms[String(i||"")];if(t)return"closed"===s?t[0]:t[1];const n=fs(e);return n?"closed"===s?n[0]:n[1]:e}return"lock"===t?"locked"===s?"mdi:lock":"mdi:lock-open-variant":"light"===t&&"mdi:lightbulb"===e&&"on"===s?"mdi:lightbulb-on":e}function bs(e){const t=Math.min(4e4,Math.max(1e3,e))/100,i=t<=66?255:329.698727446*Math.pow(t-60,-.1332047592),s=t<=66?99.4708025861*Math.log(t)-161.1195681661:288.1221695283*Math.pow(t-60,-.0755148492),n=t>=66?255:t<=19?0:138.5177312231*Math.log(t-10)-305.0447927307,o=e=>Math.round(Math.min(255,Math.max(0,e)));return[o(i),o(s),o(n)]}const ys=1/2.2;function ws(e){const t=/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/.exec(e);return t?"#"+t.slice(1).map(e=>Math.min(255,Number(e)).toString(16).padStart(2,"0")).join(""):null}function ks(e){if(!e||"object"!=typeof e||Array.isArray(e))return null;const t=e;if(Object.keys(t).some(e=>"c"!==e&&"bri"!==e))return null;const i=Ie(t.c,null);return i?void 0===t.bri||null===t.bri?{c:i}:"number"!=typeof t.bri||!Number.isFinite(t.bri)||t.bri<.01||t.bri>1?null:{c:i,bri:t.bri}:null}function xs(e,t,i){const s=ks(t),n=e?.attributes||{},o=s?.bri??function(e){const t=e?.attributes?.brightness,i="number"==typeof t?t:"string"==typeof t&&""!==t.trim()?Number(t):Number.NaN;return Number.isFinite(i)?Math.max(0,Math.min(1,i/255)):1}(e);if(s)return{c:s.c,bri:o};const a=ze(n.rgb_color);if(a)return{c:ws(a)||Ie(i,"#ffd9a0"),bri:o};const r=Number(n.color_temp_kelvin)||(Number(n.color_temp)>0?1e6/Number(n.color_temp):NaN);return Number.isFinite(r)&&r>0?{c:(l=bs(r),"#"+l.slice(0,3).map(e=>Math.min(255,Math.max(0,Math.round(Number(e)||0))).toString(16).padStart(2,"0")).join("")),bri:o}:{c:Ie(i,"#ffd9a0"),bri:o};var l}function $s(e,t,i){return"on"===e?.state?xs(e,t,i):null}function Ss(e,t=1){const i=Math.max(0,Math.min(1,Number.isFinite(e)?e:1)),s=.7*Math.max(0,Math.min(1,Number.isFinite(t)?t:1))*(.4+.6*Math.pow(i,ys));return Math.max(0,Math.min(1,s))}function Ms(e){return e.startsWith("light.")||e.startsWith("switch.")}function Cs(e,t,i=1e-6){const s=[];if(!e||!t||e.length<3||t.length<3)return s;for(let n=0;np||l>p)continue;const u=(n[0]-o[0])*h+(n[1]-o[1])*d,_=(a[0]-o[0])*h+(a[1]-o[1])*d,m=Math.max(0,Math.min(u,_)),g=Math.min(c,Math.max(u,_));g-m>i&&s.push([o[0]+h*m,o[1]+d*m,o[0]+h*g,o[1]+d*g])}}return s}function Ds(e,t,i=1e-6){const s=[];for(const n of e){const e=[n[0],n[1]],o=[n[2],n[3]],a=o[0]-e[0],r=o[1]-e[1],l=Math.hypot(a,r);if(lo||n>o)continue;const a=(s[0]-e[0])*c+(s[1]-e[1])*h,r=(s[2]-e[0])*c+(s[3]-e[1])*h,p=Math.max(0,Math.min(a,r)),u=Math.min(l,Math.max(a,r));u-p>i&&d.push([p,u])}if(!d.length){s.push([e[0],e[1],o[0],o[1]]);continue}d.sort((e,t)=>e[0]-t[0]);let p=0;for(const[t,n]of d)t-p>i&&s.push([e[0]+c*p,e[1]+h*p,e[0]+c*t,e[1]+h*t]),p=Math.max(p,n);l-p>i&&s.push([e[0]+c*p,e[1]+h*p,o[0],o[1]])}return s}function Ps(e,t,i=1e-6){const s=[];for(let t=0;t{if("string"!=typeof e||!e)return;const i=As(e);i.startsWith("/api/houseplan/content/")&&t.add(i)};for(const t of e?.spaces||[]){i(t?.plan_url);for(const e of t?.markers||[])for(const t of e?.pdfs||[])i(t?.url)}for(const t of e?.markers||[])for(const e of t?.pdfs||[])i(e?.url);return t}function As(e){return e?e.startsWith("/houseplan_files/plans/")?"/api/houseplan/content/plans/_/"+e.slice(23):e.startsWith("/houseplan_files/files/")?"/api/houseplan/content/files/"+e.slice(23):e:""}function zs(e,t){const i=t?.settings?.fill_mode;return"none"===i||"lqi"===i||"light"===i||"temp"===i||"custom"===i?i:e}function Fs(e,t){const i=t?.settings;return"boolean"==typeof i?.glow?i.glow:"glow"===i?.fill_mode||e}function Ns(e,t=1){const i=Number(e);return Number.isFinite(i)&&i>0?Math.min(3,Math.max(.5,i)):t}function Es(e,t){let i=180*Math.atan2(t[1]-e[1],t[0]-e[0])/Math.PI;return i<0&&(i+=360),i}function Os(e,t=.5){const i=(e%45+45)%45;return i<=t||45-i<=t}function Hs(e,t){const i=t[2]-t[0],s=t[3]-t[1],n=i*i+s*s;if(!n)return Math.hypot(e[0]-t[0],e[1]-t[1]);let o=((e[0]-t[0])*i+(e[1]-t[1])*s)/n;return o=Math.max(0,Math.min(1,o)),Math.hypot(e[0]-(t[0]+o*i),e[1]-(t[1]+o*s))}const Ls=new Set(["smoke","gas","carbon_monoxide","moisture","safety","tamper","problem"]);function Bs(e,t){return"alarm_control_panel"===e||"siren"===e||"binary_sensor"===e&&!!t&&Ls.has(t)}function js(e){if(!e)return null;const t=e.indexOf("#");if(t<=0)return null;const i=e.slice(0,t),s=e.slice(t+1);if(!s)return null;if(s.startsWith("@")){const e=s.slice(1);return e?{space:i,area:null,roomId:e}:null}return{space:i,area:s,roomId:null}}const qs={availability:"available",status:"alarm",activity:"none"},Ws=new Set(["motion","vibration","sound"]),Us=new Set(["occupancy","presence"]),Gs=new Set(["door","window","garage_door","opening"]),Vs=new Set(["running","power"]),Ks=new Set(["smoke","gas","carbon_monoxide","moisture","safety","tamper","problem"]);const Ys=new Set(["running","working","washing","rinsing","spinning","drying","heating","cooling","cleaning","cooking","playing","recording","pumping","irrigating","humidifying","dehumidifying","fan","preheating","defrosting"]),Js=new Set([...Ys,"start","started","run","active","in_progress","wash","rinse","spin","dry"]),Xs=new Set(["off","idle","paused","standby","docked","finished","complete","completed","stopped","ready","sleeping","stop","end","done","inactive"]),Zs=new Set(["heat","cool","heat_cool","auto","dry","fan_only"]),Qs=e=>""===e||"unknown"===e||"unavailable"===e||"__missing__"===e,en=e=>String(e??"").trim().toLowerCase(),tn=e=>en(e).replace(/[\s-]+/g,"_").replace(/_+/g,"_").replace(/^_|_$/g,""),sn=new Map([["run_state",0],["job_state",0],["operation_state",0],["activity_state",0],["machine_state",1],["running_state",1],["status",2],["device_status",2],["machine_status",2]]),nn=new Set(["wifi","connection","signal","battery"]);function on(e,t){const i=e?.entities?.[t]||{};if("config"===i.entity_category)return null;const s=String(t||"").split(".").slice(1).join("."),n=[i.translation_key,i.original_name,i.name],o=[...n,s];n.some(e=>en(e))||o.push(e?.states?.[t]?.attributes?.friendly_name);const a=o.map(tn).filter(Boolean);if(a.some(e=>e.split("_").some(e=>nn.has(e))))return null;let r=null;for(const e of a)for(const[t,i]of sn)(e===t||e.endsWith(`_${t}`))&&(r=null==r?i:Math.min(r,i));return r}const an=(e,t)=>null!=on(e,t);function rn(e,t){if(!t.startsWith("switch."))return!1;const i=e?.entities?.[t]||{},s=e?.states?.[t],n=t.slice(7).toLowerCase();if(/(?:^|_)(?:main_)?power$/.test(n))return!0;if([i.translation_key,i.original_name,i.name].map(en).some(e=>["power","main power","power switch","питание"].includes(e)))return!0;const o=en(s?.attributes?.friendly_name);return/(?:^|[\s._-])(?:main[\s._-]+)?power$/.test(o)||/(?:^|[\s._-])питание$/.test(o)}function ln(e){for(const t of["hvac_action","action","current_operation","run_state","job_state","operation","activity"]){const i=en(e?.[t]);if(Ys.has(i)||Xs.has(i))return i}return""}function cn(e,t){const i=e?.states?.[t],s=i?en(i.state):"__missing__",n=String(t||"").split(".")[0],o=en(i?.attributes?.device_class),a={eid:t,state:s,availability:Qs(s)?"unavailable":"available",status:"neutral",activity:"none",edge:"none"};if("unavailable"===a.availability)return a;if(function(e,t,i){return"alarm_control_panel"===e?"triggered"===i:"on"===i&&Bs(e,t)}(n,o,s))return{...a,status:"alarm"};if("binary_sensor"===n)return Ws.has(o)?{...a,edge:"rising"}:Us.has(o)?{...a,activity:"on"===s?"presence":"none"}:Gs.has(o)?{...a,status:"on"===s?"open":"neutral",edge:"rising"}:"moving"===o?{...a,activity:"on"===s?"transition":"none"}:Vs.has(o)&&"on"===s?{...a,status:"working",activity:"running"}:a;if("cover"===n)return{...a,activity:"opening"===s||"closing"===s?"transition":"none",edge:"terminal_transition"};if("lock"===n)return{...a,status:"unlocked"===s||"open"===s?"open":"neutral",activity:"locking"===s||"unlocking"===s?"transition":"none",edge:"terminal_transition"};if("valve"===n)return{...a,status:["open","opening","closing"].includes(s)?"open":"neutral",activity:"opening"===s||"closing"===s?"transition":"none",edge:"terminal_transition"};if("climate"===n){const e=ln(i.attributes),t=Array.isArray(i.attributes?.hvac_modes)?i.attributes.hvac_modes.map(en):[],n=!Xs.has(s)&&(Zs.has(s)||Ys.has(s)||t.includes(s));return(e?Ys.has(e):n)?{...a,status:"working",activity:"running"}:a}if(["light","switch","fan","humidifier"].includes(n))return"on"===s?{...a,status:"working",activity:"running"}:a;if("media_player"===n)return"off"===s?{...a,availability:"unavailable"}:a;if("vacuum"===n)return"cleaning"===s?{...a,status:"working",activity:"running"}:"returning"===s?{...a,status:"working",activity:"transition"}:a;if("script"===n)return"on"===s?{...a,status:"working",activity:"running"}:a;if("automation"===n)return a;if("button"===n||"event"===n)return{...a,edge:"change"};const r=ln(i.attributes);return Ys.has(r)||Ys.has(s)&&!Xs.has(s)?{...a,status:"working",activity:"running"}:a}function hn(e,t,i){const s=i.filter(t=>t.startsWith("switch.")&&!e?.entities?.[t]?.entity_category),n=t.find(t=>rn(e,t));if(!(s.length>1&&!!n))return t.map(t=>cn(e,t));const o=t.find(t=>t!==n&&an(e,t)),a=cn(e,n);return"unavailable"===a.availability||"off"===a.state?t.map(t=>({eid:t,state:e?.states?.[t]?en(e.states[t].state):"__missing__",availability:"unavailable",status:"neutral",activity:"none",edge:"none"})):t.map(t=>{if(t===o)return function(e,t){const i=e?.states?.[t],s=i?en(i.state):"__missing__",n={eid:t,state:s,availability:Qs(s)?"unavailable":"available",status:"neutral",activity:"none",edge:"none"};if("unavailable"===n.availability)return n;const o=ln(i?.attributes);return Ys.has(o)||Js.has(s)&&!Xs.has(s)?{...n,status:"working",activity:"running"}:n}(e,t);return{...t===n?a:cn(e,t),status:"neutral",activity:"none",edge:"none"}})}function dn(e){if(!e.length)return{availability:"available",status:"neutral",activity:"none"};const t=e.filter(e=>"available"===e.availability);if(!t.length)return{availability:"unavailable",status:"neutral",activity:"none"};if(t.some(e=>"alarm"===e.status))return qs;const i=t.some(e=>"working"===e.status)?"working":t.some(e=>"open"===e.status)?"open":"neutral",s=t.some(e=>"transition"===e.activity)?"transition":t.some(e=>"presence"===e.activity)?"presence":t.some(e=>"running"===e.activity)?"running":"none";return{availability:"available",status:i,activity:s}}function pn(e,t){if(!(e=>!!e&&!Qs(e))(e)||"unavailable"===t.availability||e===t.state)return null;if("rising"===t.edge)return"off"===e&&"on"===t.state?"event":null;if("change"===t.edge)return"event";if("terminal_transition"===t.edge){const i=new Set([e,t.state]);if(i.has("closed")&&i.has("open")||i.has("locked")&&i.has("unlocked"))return"transition"}return null}const un=(e,t=0)=>{const i=Number(e);return Number.isInteger(i)&&i>=0?i:t},_n=(e,t)=>{const i={rev:0,configRev:un(t),off:new Set};if(null==e)return{snapshot:i,valid:!1};const s=e.off;return!Number.isInteger(e.rev)||Number(e.rev)<0||!Number.isInteger(e.config_rev)||Number(e.config_rev)<0||!Array.isArray(s)||s.some(e=>"string"!=typeof e||!e)?{snapshot:i,valid:!1}:{snapshot:{rev:Number(e.rev),configRev:Number(e.config_rev),off:new Set(s)},valid:!0}};function mn(e,t=0){return _n(e,t).snapshot}function gn(e,t,i,s){const n=_n(t,i);return s&&n.valid&&n.snapshot.configRev===i?e.configRev===i&&n.snapshot.rev{try{const[i,s]=await Promise.all([e.callWS({type:"config/device_registry/list"}),e.callWS({type:"config/entity_registry/list"})]),n=Dn(i,"id"),o=Dn(s,"entity_id");if(!n||!o)throw new Error("invalid_registry_response");t.devices=n,t.entities=o,t.authoritative=!0,t.access="full",t.lastSuccess=Date.now(),t.error=void 0}catch(e){t.authoritative=!1,t.access="limited",t.error=function(e){if(e&&"object"==typeof e){const t=e;return String(t.message||t.code||t.error||"registry_unavailable")}return String(e||"registry_unavailable")}(e)}finally{t.revision++,t.loading=void 0,function(e){for(const t of[...e.listeners])try{t()}catch{}}(t)}})()),t.loading}function Tn(e,t){void 0===t.reloadTimer&&(t.reloadTimer=globalThis.setTimeout(()=>{t.reloadTimer=void 0,Pn(e,t)},80))}async function In(e,t){if(t.subscribing||t.unsubDevice&&t.unsubEntity)return t.subscribing;const i=e?.connection?.subscribeEvents;return"function"==typeof i?(t.subscribing=(async()=>{try{t.unsubDevice||(t.unsubDevice=await i.call(e.connection,()=>Tn(e,t),"device_registry_updated")),t.unsubEntity||(t.unsubEntity=await i.call(e.connection,()=>Tn(e,t),"entity_registry_updated"))}catch{}finally{0===t.refs&&(t.unsubDevice?.(),t.unsubEntity?.(),t.unsubDevice=void 0,t.unsubEntity=void 0),t.subscribing=void 0}})(),t.subscribing):void 0}function Rn(e,t){const i=Cn(e);if(!i)return()=>{};const s=0===i.refs;i.refs++,i.listeners.add(t),("pending"===i.access||s)&&Pn(e,i),In(e,i);let n=!1;return()=>{n||(n=!0,i.listeners.delete(t),i.refs=Math.max(0,i.refs-1),i.refs>0||(i.unsubDevice?.(),i.unsubEntity?.(),i.unsubDevice=void 0,i.unsubEntity=void 0,void 0!==i.reloadTimer&&globalThis.clearTimeout(i.reloadTimer),i.reloadTimer=void 0))}}function An(e){const t=Cn(e);if(!t)return{revision:0,authoritative:!1,access:"limited",devices:e?.devices||{},entities:e?.entities||{},lastSuccess:0,error:"registry_unavailable"};let i=e?.devices||{},s=e?.entities||{};if(t.authoritative){const n=t.liveDevices!==i||t.liveEntities!==s,o=void 0!==t.liveDevices||void 0!==t.liveEntities;if(n&&(t.liveDevices=i,t.liveEntities=s,t.projectedRevision=void 0,o&&Tn(e,t)),t.projectedRevision!==t.revision||!t.projectedDevices||!t.projectedEntities){const e={...t.devices},n={...t.entities};for(const[t,s]of Object.entries(i))Object.prototype.hasOwnProperty.call(e,t)||(e[t]=s);for(const[e,t]of Object.entries(s))Object.prototype.hasOwnProperty.call(n,e)||(n[e]=t);t.projectedDevices=e,t.projectedEntities=n,t.projectedRevision=t.revision}i=t.projectedDevices,s=t.projectedEntities}return{revision:t.revision,authoritative:t.authoritative,access:t.access,devices:i,entities:s,lastSuccess:t.lastSuccess,error:t.error}}function zn(e){if(!e||"object"!=typeof e&&"function"!=typeof e)return 0;let t=xn.get(e);return t||(t=$n++,xn.set(e,t)),t}function Fn(e,t=An(e)){return[t.revision,t.access,zn(t.devices),zn(t.entities)].join(":")}function Nn(e){return!!e&&null==e.disabled_by}function En(e,t=An(e)){const i={},s={},n={};for(const[e,s]of Object.entries(t.devices||{}))Nn(s)&&(i[e]=s);for(const[e,i]of Object.entries(t.entities||{})){if(!Nn(i))continue;const n=i.device_id?t.devices?.[i.device_id]:null;t.authoritative&&i.device_id&&!n||(n&&!Nn(n)||(s[e]=i))}for(const[i,s]of Object.entries(e?.states||{})){const e=t.entities?.[i];if(e&&!Nn(e))continue;const o=e?.device_id?t.devices?.[e.device_id]:null;t.authoritative&&e?.device_id&&!o||(o&&!Nn(o)||(n[i]=s))}return{...e,devices:i,entities:s,states:n}}function On(e,t=An(e)){return{...e,devices:t.devices||{},entities:t.entities||{}}}function Hn(e,t){const i=[];for(const[s,n]of Object.entries(e||{}))n?.device_id===t&&i.push(s);return i}function Ln(){if(Mn)return Mn;if("undefined"==typeof localStorage)return{};try{const e=JSON.parse(localStorage.getItem(Sn)||"{}");if(!e||"object"!=typeof e||Array.isArray(e))return{};const t=Date.now(),i={};for(const[s,n]of Object.entries(e))!n||"active"!==n.kind&&"ha_disabled"!==n.kind||!Number.isFinite(n.ts)||t-n.ts>7776e6||(i[s]=n);return Mn=i,Mn}catch{return{}}}function Bn(e,t,i=An(e)){if(!t||"virtual"===t)return{kind:"active",enabledEntityIds:[],allEntityIds:[]};const s=t.indexOf(":");if(s<1)return{kind:"unverified",reason:"registry_unavailable",enabledEntityIds:[],allEntityIds:[]};const n=t.slice(0,s),o=t.slice(s+1);if("device"!==n&&"entity"!==n||!o)return{kind:"unverified",reason:"registry_unavailable",enabledEntityIds:[],allEntityIds:[]};const a=i.devices||{},r=i.entities||{};if(i.authoritative){if("device"===n){const e=a[o];if(!e)return{kind:"orphaned",reason:"device_missing",enabledEntityIds:[],allEntityIds:[]};const t=Hn(r,o);if(!Nn(e))return{kind:"ha_disabled",reason:"device",enabledEntityIds:[],allEntityIds:t};const i=t.filter(e=>Nn(r[e]));return t.length&&!i.length?{kind:"ha_disabled",reason:"all_entities",enabledEntityIds:[],allEntityIds:t}:{kind:"active",enabledEntityIds:i,allEntityIds:t}}const t=r[o];return t&&!Nn(t)?{kind:"ha_disabled",reason:"entity",enabledEntityIds:[],allEntityIds:[o]}:t?.device_id&&!a[t.device_id]?{kind:"orphaned",reason:"device_missing",enabledEntityIds:[],allEntityIds:[o]}:t?.device_id&&!Nn(a[t.device_id])?{kind:"ha_disabled",reason:"device",enabledEntityIds:[],allEntityIds:[o]}:t||e?.states?.[o]?{kind:"active",enabledEntityIds:[o],allEntityIds:[o]}:{kind:"orphaned",reason:"entity_missing",enabledEntityIds:[],allEntityIds:[]}}const l=function(e){return Ln()[e]||null}(t);if("device"===n){const t=a[o],i=Hn(r,o);if(null!=t?.disabled_by)return{kind:"ha_disabled",reason:"device",enabledEntityIds:[],allEntityIds:i};const s=i.filter(e=>{const t=r[e];return null==t?.disabled_by&&(!t.device_id||null==a[t.device_id]?.disabled_by)});if(i.length&&!s.length&&i.every(e=>null!=r[e]?.disabled_by))return{kind:"ha_disabled",reason:"all_entities",enabledEntityIds:[],allEntityIds:i};if("ha_disabled"===l?.kind)return{kind:"ha_disabled",reason:l.reason||"device",enabledEntityIds:[],allEntityIds:i};if(t||s.some(t=>!!e?.states?.[t]))return{kind:"active",enabledEntityIds:s,allEntityIds:i}}else{const t=r[o];if(null!=t?.disabled_by)return{kind:"ha_disabled",reason:"entity",enabledEntityIds:[],allEntityIds:[o]};if(t?.device_id&&null!=a[t.device_id]?.disabled_by)return{kind:"ha_disabled",reason:"device",enabledEntityIds:[],allEntityIds:[o]};if("ha_disabled"===l?.kind)return{kind:"ha_disabled",reason:l.reason||"entity",enabledEntityIds:[],allEntityIds:[o]};if(t||e?.states?.[o])return{kind:"active",enabledEntityIds:[o],allEntityIds:[o]}}return{kind:"unverified",reason:"registry_unavailable",enabledEntityIds:[],allEntityIds:[]}}function jn(e){const t={};for(const[i,s]of Object.entries(e.entities)){if(!s?.device_id||!Nn(s))continue;const n=e.devices?.[s.device_id];n&&!Nn(n)||(t[s.device_id]=t[s.device_id]||[]).push(i)}return t}function qn(e,t){const i=new Map;for(const s of e){if(!0===s?.removed)continue;const e=s?.binding||"";if(!e.startsWith("entity:"))continue;const n=e.slice(7);if(!n)continue;const o=t?.entities?.[n]?.device_id;if(!o)continue;const a=i.get(o)||new Set;a.add(n),i.set(o,a)}return{byDevice:i}}function Wn(e,t,i,s){const n=s.byDevice.get(t);return n?.size?{partial:!0,entityIds:i.filter(t=>!n.has(t)&&!e?.entities?.[t]?.hidden)}:{partial:!1,entityIds:[...i]}}function Un(e,t,i){if(t.identifiers?.[0]?.[0])return t.identifiers[0][0];for(const t of i){const i=e.entities[t]?.platform;if(i)return i}return""}function Gn(e,t){if(/_device_temperature$/.test(t))return!1;if(e.entities?.[t]?.entity_category)return!1;const i=e.states[t];if(!i)return/_temperature$/.test(t);const s=i.attributes||{};return"temperature"===s.device_class||/°C|°F/.test(s.unit_of_measurement||"")||/_temperature$/.test(t)}const Vn=["vacuum","lawn_mower","climate","media_player","light","cover","lock","valve","alarm_control_panel","water_heater","fan","humidifier","siren","camera","remote"],Kn=e=>[...e.filter(e=>!e.reg?.hidden),...e.filter(e=>!!e.reg?.hidden)];function Yn(e,t){const i=t.map(t=>({eid:t,reg:e?.entities?.[t]})).filter(e=>!!e.reg);if(!i.length)return[];const s=i.filter(e=>!e.reg.entity_category),n=s.length?s:i;for(const e of Vn){const t=n.filter(t=>t.eid.startsWith(e+"."));if(t.length)return Kn(t).map(e=>e.eid)}const o=n.filter(t=>function(e,t){if(!t.startsWith("binary_sensor."))return!1;const i=en(e?.states?.[t]?.attributes?.device_class||e?.entities?.[t]?.device_class||e?.entities?.[t]?.original_device_class);return Ws.has(i)||Us.has(i)||Gs.has(i)||Vs.has(i)||"moving"===i||Ks.has(i)}(e,t.eid));if(o.length)return Kn(o).map(e=>e.eid);const a=n.filter(e=>e.eid.startsWith("switch."));if(a.length){const t=Kn(a),s=t.find(t=>rn(e,t.eid));if(a.length>1&&s){const t=i.filter(t=>t.eid!==s.eid&&"config"!==t.reg?.entity_category&&null!=on(e,t.eid)),n=t.filter(e=>!e.reg?.entity_category),o=n.length?n:t;for(const t of[0,1,2]){const i=Kn(o.filter(i=>on(e,i.eid)===t));if(i.length)return[i[0].eid,s.eid]}}return[(s||t[0]).eid]}const r=[];for(const e of it)r.push(...Kn(n.filter(t=>t.eid.startsWith(e+"."))));return r.push(...Kn(n.filter(e=>!it.includes(e.eid.split(".")[0])))),r.map(e=>e.eid)}function Jn(e,t,i){const s=t.map(t=>({eid:t,reg:e.entities[t],st:e.states[t]})).filter(e=>e.reg),n=[s.filter(e=>!e.reg.hidden&&!e.reg.entity_category),s.filter(e=>!e.reg.entity_category),s.filter(e=>!e.reg.hidden),s];if("mdi:thermometer"===i||"mdi:air-filter"===i)for(const t of n){const i=t.find(t=>Gn(e,t.eid));if(i)return i.eid}const o=Yn(e,t);return o.find(Ms)||o[0]}function Xn(e,t,i=[]){return[...new Set(Zn(e,t,i))].filter(e=>Ms(e))}function Zn(e,t,i=[]){const s=e?.startsWith("entity:")?new Set([e.slice(7)]):new Set(i);return(t||[]).filter(e=>"string"==typeof e&&!s.has(e))}function Qn(e,t){if("string"==typeof t)return e.area===t;const i=e.marker?.room_id;return i?!!t.id&&i===t.id:!!t.area&&e.area===t.area}function eo(e){const t=e.marker?.binding?.startsWith("entity:")?e.marker.binding.slice(7):null,i=e.entities.filter(Ms),s=e.primary&&Ms(e.primary)?e.primary:null;return[...new Set([t,s,...i].filter(e=>!!e&&Ms(e)))]}function to(e){const t=eo(e),i=e.marker?.light_entity;return i&&t.includes(i)?i:t[0]||null}function io(e,t){if(!1===t.marker?.is_light)return[];if(!0===t.marker?.is_light){return[{eid:to(t),via:"forced"}]}const i=t.primary||Yn(e,t.entities)[0];return i&&!i.startsWith("light.")?[]:t.entities.filter(t=>t.startsWith("light.")&&!!e.states?.[t]).map(e=>({eid:e,via:"light"}))}const so=new WeakMap,no=new WeakMap;function oo(e){return e.map(e=>[e.id||"",!0===e.hidden?1:0,e.primary||"",[...e.entities].join(","),void 0===e.controls?"":[...e.controls].join(","),e.marker?.id||"",e.marker?.binding||"",e.marker?.is_light,e.marker?.tap_action||"",!0===e.marker?.removed?1:0,e.marker?.light_entity||"",null==e.marker?.controls?"":[...e.marker.controls].join(",")].join("")).join("")}function ao(e){const t=oo(e),i=so.get(e);if(i?.fingerprint===t)return i;const s=e.filter(e=>!e.hidden),n=new Map,o=new Map;for(const t of e){o.set(t,Zn(t.marker?.binding,t.marker?.controls??t.controls,t.entities));const e=t.hidden||!0!==t.marker?.is_light?null:t.marker?.id||t.id;e&&n.set(String(e),t)}const a=new Map;for(const t of e){const e=o.get(t)||[],i=new Set(void 0===t.controls?e.filter(Ms):t.controls.filter(Ms)),s=to(t),r=i.size?[...i]:s?[s]:[];for(const i of e){if(!i.startsWith("marker:"))continue;const e=i.slice(7);if(!e||e===String(t.marker?.id||t.id||""))continue;if(!n.has(e))continue;const s=a.get(e)||{markerId:e,controllers:[],driverEids:[]};s.controllers.push({device:t,driverEids:[...r]}),s.driverEids=[...new Set([...s.driverEids,...r])],a.set(e,s)}}const r={fingerprint:t,visible:s,markerById:n,persistedByDevice:o,incomingByMarker:a};return so.set(e,r),r}function ro(e){return ao(e).incomingByMarker}function lo(e,t){const i=Xn(t.marker?.binding,t.controls??t.marker?.controls,t.entities),s=Zn(t.marker?.binding,t.marker?.controls??t.controls,t.entities);return(null!=t.marker?.is_light||!i.length&&!s.some(e=>e.startsWith("marker:")))&&io(e,t).length>0}function co(e,t){const i=Xn(t.marker?.binding,t.controls??t.marker?.controls,t.entities).map(e=>({eid:e,via:"controls"})),s=lo(e,t)?io(e,t):[];for(const e of s)e.eid&&i.some(t=>t.eid===e.eid)||i.push(e);return i}function ho(e,t,i,s){if(null!=i)return ho(e,t,null,s).filter(e=>Qn(e.device,i));const n=oo(t),o=function(e,t){const i=new Set;for(const e of t){for(const t of e.entities)i.add(t);for(const t of e.controls||e.marker?.controls||[])"string"!=typeof t||t.startsWith("marker:")||i.add(t)}return[...i].sort().map(t=>`${t}:${e?.states?.[t]?.state??""}`).join("|")}(e,t),a=vn(s),r=no.get(t);if(r?.graphFingerprint===n&&r.stateFingerprint===o&&r.virtualLightFingerprint===a&&r.registry===e?.entities)return r.sources;const{visible:l,markerById:c,persistedByDevice:h,incomingByMarker:d}=ao(t),p=null==i?l:l.filter(e=>Qn(e,i)),u=new Map,_=new Map;for(const t of p){const i=co(e,t).filter(e=>"controls"!==e.via);for(const s of i){const i=!0===t.marker?.is_light?String(t.marker?.id||t.id||""):"",n=i?`marker:${i}`:`entity:${s.eid}`,o=s.eid?[s.eid]:[],a={key:n,eid:s.eid||"",stateEids:o,serviceEids:o,device:t,via:s.via,castsGlow:!0,passive:!s.eid,on:!s.eid||"on"===e.states?.[s.eid]?.state};u.set(t,[...u.get(t)||[],a]),s.eid&&_.set(s.eid,a)}}const m=new Map;for(const e of t){const t=h.get(e)||[],i=new Set(void 0===e.controls?t.filter(Ms):e.controls);m.set(e,i)}for(const t of u.values())for(const i of t){if(!i.passive)continue;const t=i.key.slice(7),n=d.get(t);i.on=!n||n.driverEids.some(t=>"on"===e.states?.[t]?.state),!n&&bn(i.device.marker)&&(i.on=yn(i.device.marker,s))}const g=new Map,f=e=>{const t=g.get(e.key);(!t||e.castsGlow&&!t.castsGlow)&&g.set(e.key,e)};for(const t of l){const s=h.get(t)||[],n=null==i||Qn(t,i);for(const i of s){if(i.startsWith("marker:")){const e=c.get(i.slice(7)),t=e?u.get(e):null;if(t)for(const e of t)f(e);continue}if(!Ms(i)||!m.get(t)?.has(i))continue;const s=_.get(i);s?f(s):n&&f({key:`entity:${i}`,eid:i,stateEids:[i],serviceEids:[i],device:t,via:"controls",castsGlow:!1,passive:!1,on:"on"===e.states?.[i]?.state})}for(const e of u.get(t)||[])f(e)}const v=[...g.values()];return no.set(t,{graphFingerprint:n,stateFingerprint:o,virtualLightFingerprint:a,registry:e?.entities,sources:v}),v}function po(e){const t=e.filter(e=>e.castsGlow);return t.find(e=>e.on)||t[0]||null}function uo(e){return e.length?e.some(e=>e.on)?"on":"off":"none"}function _o(e,t){const i=[];for(const s of t){const t=e.states[s];if(!t)continue;const n=(t.attributes?.unit_of_measurement||"").toLowerCase();if(/_(linkquality|lqi)$/.test(s)||"lqi"===n){const e=parseFloat(t.state);isNaN(e)||i.push(e);continue}const o=t.attributes?.linkquality??t.attributes?.lqi;if(null!=o){const e=parseFloat(o);isNaN(e)||i.push(e)}}return Ni(i)}function mo(e,t){for(const i of t){if(!Gn(e,i))continue;const t=e.states[i];if(!t)continue;const s=parseFloat(t.state);if(!isNaN(s))return Math.round(10*s)/10}return null}function go(e,t){for(const i of t){if(!i.startsWith("climate."))continue;const t=e.states[i];if(!t||"unavailable"===t.state||"unknown"===t.state)continue;const s=parseFloat(t.attributes?.current_temperature);if(Number.isFinite(s))return Math.round(10*s)/10}return null}function fo(e,t){if(e.entities?.[t]?.entity_category)return!1;const i=e.states[t];if(!i)return/_humidity$/.test(t);const s=i.attributes||{};return"humidity"===s.device_class||"%"===s.unit_of_measurement&&/_humidity$/.test(t)||/_humidity$/.test(t)}function vo(e,t){for(const i of t){if(!fo(e,i))continue;const t=e.states[i];if(!t)continue;const s=parseFloat(t.state);if(!isNaN(s))return Math.round(s)}return null}function bo(e,t){if(!t)return[];const i=[];for(const[t,s]of Object.entries(e.entities)){if(!t.startsWith("light.")||s.hidden||!Nn(s))continue;let n=null;if("group"===s.platform)n=s.area_id||null;else{if(!s.device_id)continue;{const t=e.devices[s.device_id];if(!Nn(t))continue;if("Group"!==t?.model)continue;n=t.area_id||s.area_id||null}}if(!n)continue;const o=e.states[t];i.push({eid:t,name:s.name||o?.attributes?.friendly_name||t,area:n})}return i}function yo(e,t,i,s,n){const o=tt(t,i,n);if(o!==et)return o;const a=[];for(const t of s){const i=e.states[t]?.attributes?.device_class;i&&a.push(i)}return function(e){for(const t of e){const e=Qe[t];if(e)return e}return null}(a)??et}function wo(e){const t=new Set,i=new Set,s=new Set;for(const n of e||[]){const e=String(n.binding||"").indexOf(":");if(e<1)continue;const o=n.binding.slice(0,e),a=n.binding.slice(e+1);a&&(!0===n?.removed?"device"===o?t.add(a):"entity"===o&&i.add(a):"entity"===o&&s.add(a))}return{devices:t,entities:i,liveEntities:s}}function ko(e,t,i){if(i.liveEntities.has(t))return!1;if(i.entities.has(t))return!0;const s=e?.entities?.[t]?.device_id;return!!s&&i.devices.has(s)}function xo(e,t,i,s,n){const o=Xn(t.binding,t.controls,e.entities).filter(e=>!ko(i,e,s)).filter(e=>"active"===Bn(i,"entity:"+e,n).kind);e.marker=t,e.controls=o,e.userHidden=!0===t.hidden,e.hidden=e.userHidden||"ha_disabled"===e.bindingStatus?.kind,t.name&&(e.name=t.name),t.icon&&(e.icon=t.icon),null!=t.model&&(e.model=t.model),e.link=t.link??null,e.description=t.description??null,e.pdfs=t.pdfs||[],e.tapAction=t.tap_action??null}function $o(e,t,i,s){if("string"==typeof e.room_id&&e.room_id.length>0&&null===e.area)return{area:"",space:e.space||s};const n=e.area||t||"";return{area:n,space:n&&i[n]||e.space||s}}function So(e){const t=e.hass,i=e.registry||An(t),s=En(t,i),n=On(t,i),{areaToSpace:o,markers:a,settings:r,excluded:l,showAll:c,firstSpaceId:h,loc:d,iconRules:p}=e,u=!1!==r.group_lights,_=wo(a),m=bo(s,u).filter(e=>!ko(s,e.eid,_)),g=new Set(m.map(e=>e.area)),f=jn(s),v=function(e){const t={};for(const[i,s]of Object.entries(e.entities||{}))s?.device_id&&(t[s.device_id]=t[s.device_id]||[]).push(i);return t}(n),b=qn(a,n),y=new Set;for(const e of a){const[t,i]=e.binding.split(":");"device"!==t&&"entity"!==t||!i||y.add(e.binding)}const w=(e,t)=>a.find(i=>i.binding===e+":"+t),k={},x=[];for(const e of Object.values(s.devices)){const n=e.area_id;if(!n||!o[n])continue;if("service"===e.entry_type)continue;if(y.has("device:"+e.id))continue;const a=Bn(t,"device:"+e.id,i);if("active"!==a.kind)continue;const h=w("device",e.id);if(h&&h.hidden&&!r.filter_seeded)continue;const _=Wn(s,e.id,f[e.id]||[],b);if(_.partial&&!_.entityIds.length)continue;const m=_.entityIds,v=_.partial?{kind:"active",enabledEntityIds:m,allEntityIds:m}:a,$=Un(s,e,m),S=!r.filter_seeded;if(S&&!c){if(l.has($))continue;if("Group"===e.model)continue;if(/scene/i.test(e.model||""))continue;if(/bridge/i.test((e.model||"")+(e.name||"")))continue;if("myheat"===$&&e.via_device_id)continue}const M=(e.name_by_user||e.name||d("device.unnamed")).trim(),C=M+"|"+n;let D=yo(s,M,e.model,m,p);if(m.some(e=>e.startsWith("lock."))&&(D="mdi:lock"),S&&!c&&u&&"mdi:lightbulb"===D&&g.has(n))continue;k[C]=(k[C]||0)+1;const P=k[C]>1?M+" "+k[C]:M,T={id:e.id,name:P,model:e.model||"",area:n,space:o[n],icon:D,entities:m,allEntities:v.allEntityIds,bindingStatus:v,bindingKind:"device",bindingRef:e.id,pdfs:[]};T.primary=Jn(s,m,D),"mdi:thermometer"!==D&&"mdi:air-filter"!==D||(T.temp=mo(s,m)),T.primary&&fo(s,T.primary)&&(T.hum=vo(s,m)),x.push(T)}for(const e of m)o[e.area]&&(y.has("entity:"+e.eid)||x.push({id:"lg_"+e.eid,name:e.name,model:d("device.light_group"),area:e.area,space:o[e.area],icon:"mdi:lightbulb-group",entities:[e.eid],allEntities:[e.eid],bindingStatus:{kind:"active",enabledEntityIds:[e.eid],allEntityIds:[e.eid]},primary:e.eid,bindingKind:"entity",bindingRef:e.eid,pdfs:[]}));for(const e of a){if(e.removed)continue;const[a,l]=e.binding.split(":"),c="device"===a||"entity"===a?Bn(t,e.binding,i):null;if(!e.hidden||r.filter_seeded||"ha_disabled"===c?.kind)if("device"===a){const t=c;if("unverified"===t.kind)continue;const a=n.devices[l],{area:r,space:u}=$o(e,a?.area_id,o,h),m="active"===t.kind?t.enabledEntityIds:[];let g=a?yo(s,a.name_by_user||a.name||"",a.model,m,p):"mdi:help-circle";m.some(e=>e.startsWith("lock."))&&(g="mdi:lock");const f={id:e.id,name:a?.name_by_user||a?.name||d("device.fallback"),model:a?.model||"",area:r,space:u,icon:g,entities:m,allEntities:t.allEntityIds.length?t.allEntityIds:a&&v[a.id]||[],bindingStatus:t,bindingKind:"device",bindingRef:l};f.primary=Jn(s,m,g),"mdi:thermometer"!==g&&"mdi:air-filter"!==g||(f.temp=mo(s,m)),f.primary&&fo(s,f.primary)&&(f.hum=vo(s,m)),xo(f,e,n,_,i),x.push(f)}else if("entity"===a){if(ko(n,l,_))continue;const t=c;if("unverified"===t.kind)continue;const a=n.entities[l],r=a?.area_id||a?.device_id&&n.devices[a.device_id]?.area_id||"",{area:d,space:u}=$o(e,r,o,h),m=s.states[l],g=a?.name||m?.attributes?.friendly_name||l;let f=yo(s,g,"",[l],p);l.startsWith("lock.")&&(f="mdi:lock");const v={id:e.id,name:g,model:"",area:d,space:u,icon:f,entities:"active"===t.kind?[l]:[],allEntities:[l],bindingStatus:t,primary:"active"===t.kind?l:void 0,bindingKind:"entity",bindingRef:l};"mdi:thermometer"!==f&&"mdi:air-filter"!==f||!v.entities.length||(v.temp=mo(s,v.entities)),v.entities.length&&fo(s,l)&&(v.hum=vo(s,v.entities)),xo(v,e,n,_,i),x.push(v)}else{const t=e.area||"",s=e.space||t&&o[t]||h,a={id:e.id,name:e.name||d("device.virtual"),model:e.model||"",area:t,space:s,icon:e.icon||"mdi:map-marker",entities:[],allEntities:[],bindingStatus:{kind:"active",enabledEntityIds:[],allEntityIds:[]},bindingKind:"virtual",virtual:!0};xo(a,e,n,_,i),x.push(a)}}return x}function Mo(e,t,i,s){if(!t)return null;if(function(e,t,i){if(!t)return!1;const s=t.indexOf(":");if(s<1)return!1;const n=t.slice(0,s),o=t.slice(s+1),a=wo(i);return"device"===n?a.devices.has(o):"entity"===n&&ko(e,o,a)}(e,t,s))return null;const n=t.indexOf(":");if(n<0)return null;const o=t.slice(0,n),a=t.slice(n+1);if(!a)return null;if("entity"===o){const t=parseFloat(e.states[a]?.state);return Number.isFinite(t)?"temp"===i?Math.round(10*t)/10:Math.round(t):null}if("device"===o){const t=Object.entries(e.entities).filter(([,e])=>e.device_id===a).map(([e])=>e);return"temp"===i?mo(e,t):vo(e,t)}return null}const Co=new RegExp(["water","voda","coolant","flow_?temp","return_?temp","target","setpoint","chip","cpu","processor","board","core_temp","device_temp","batter","akkum","freezer","fridge","oven","kettle","boiler"].join("|"),"i");function Do(e,t,i){const s=[];for(const n of t){if(n.area!==i||n.virtual)continue;const t=_o(e,n.entities);null!=t&&s.push(t)}return Ni(s)}function Po(e,t,i){const s=[];for(const n of t){if(n.area!==i)continue;if("mdi:thermometer"!==n.icon&&"mdi:air-filter"!==n.icon)continue;const t=mo(e,n.entities);null!=t&&s.push(t)}return s.length?Math.round(s.reduce((e,t)=>e+t,0)/s.length*10)/10:null}const To={climate:["current_temperature","temperature","current_humidity","humidity"],water_heater:["current_temperature","temperature"],cover:["current_position"],valve:["current_position"],fan:["percentage"],humidifier:["current_humidity","humidity"],light:["brightness"],media_player:["volume_level"],vacuum:["battery_level","fan_speed"],lawn_mower:["battery_level","fan_speed"]};function Io(e){return e?e.sourceLabel?`${e.sourceLabel}: ${e.fullText}`:e.fullText:""}const Ro=new WeakMap;function Ao(e){return e?"entity_state"===e.kind?`state:${e.entity_id}`:"entity_attribute"===e.kind?`attr:${e.entity_id}:${e.attribute}`:"derived_marker_state"===e.kind?`marker:${e.ref}`:"derived:lqi":""}function zo(e){if("derived:lqi"===e)return{kind:"derived_lqi"};if(e.startsWith("state:"))return{kind:"entity_state",entity_id:e.slice(6)};if(e.startsWith("attr:")){const t=e.slice(5),i=t.lastIndexOf(":");if(i>0)return{kind:"entity_attribute",entity_id:t.slice(0,i),attribute:t.slice(i+1)}}return e.startsWith("marker:marker:")?{kind:"derived_marker_state",ref:e.slice(7)}:null}function Fo(e,t){const i=e?.entities?.[t],s=e?.states?.[t];return String(i?.name||i?.original_name||s?.attributes?.friendly_name||t)}function No(e){return e.replaceAll("_"," ")}function Eo(e,t=1){const i=Number(e);return Number.isFinite(i)?`${Math.round(i*t)} %`:null}function Oo(e){if("derived_lqi"===e.kind)return"lqi";if("entity_attribute"===e.kind){if(e.attribute.includes("temperature"))return"temperature";if(e.attribute.includes("humidity"))return"humidity"}return"default"}function Ho(e,t,i,s=[]){let n=null,o="",a="available";if("entity_state"===i.kind){o=Fo(e,i.entity_id);const t=e?.states?.[i.entity_id];if(t)if(Bo(e,i.entity_id))if(["unknown","unavailable"].includes(String(t.state).toLowerCase()))a="unavailable";else{const s=Xi(e,i.entity_id);n=s?Zi(s,String(t.attributes?.unit_of_measurement||"")):null,n||(a="unavailable")}else a="unavailable";else a="missing"}else if("entity_attribute"===i.kind){o=`${No(i.attribute)} · ${Fo(e,i.entity_id)}`;const t=e?.states?.[i.entity_id];t?Bo(e,i.entity_id)?(n=function(e,t,i){const s=e?.states?.[t];if(!s||!(i in(s.attributes||{})))return null;const n=Xi(e,t,i);if(n?.formatted)return n.text;const o=s.attributes?.[i];if("brightness"===i)return Eo(o,100/255);if("volume_level"===i)return Eo(o,100);if(["current_position","percentage","current_humidity","humidity","battery_level"].includes(i))return Eo(o);if("current_temperature"===i||"temperature"===i){const t=Number(o);if(!Number.isFinite(t))return null;const i=String(e?.config?.unit_system?.temperature||"°C");return`${Math.round(10*t)/10} ${i}`}return n?.text||(["string","number","boolean"].includes(typeof o)?String(o):null)}(e,i.entity_id,i.attribute),null==n&&(a="unavailable")):a="unavailable":a="missing"}else if("derived_lqi"===i.kind){o="LQI";const i=_o(e,t.entities);null==i?a="unavailable":n=String(i)}else{o="Light state";const t=s.find(e=>e.ref===i.ref);if(t){const i=t.on?"on":"off",s=e?.localize?.(`state.default.${i}`);n="string"==typeof s&&s?s:i,o=t.name||o}else a="missing"}const r=e?.localize?.("state.default.unavailable")||"Unavailable";return{source:i,sourceLabel:o,text:n??"—",fullText:n??r,availability:a,isLqi:"derived_lqi"===i.kind,tone:Oo(i)}}function Lo(e,t){if(!0===t.marker?.use_climate_temp){const i=t.entities.find(t=>t.startsWith("climate.")&&Number.isFinite(Number(e?.states?.[t]?.attributes?.current_temperature)));return i?{kind:"entity_attribute",entity_id:i,attribute:"current_temperature"}:null}if("mdi:thermometer"===t.icon||"mdi:air-filter"===t.icon){const i=t.entities.find(t=>Gn(e,t)&&Number.isFinite(Number(e?.states?.[t]?.state)));if(i)return{kind:"entity_state",entity_id:i}}return t.primary&&fo(e,t.primary)&&Number.isFinite(Number(e?.states?.[t.primary]?.state))?{kind:"entity_state",entity_id:t.primary}:null}function Bo(e,t){const i=e?.entities?.[t];if(i&&!Nn(i))return!1;const s=i?.device_id?e?.devices?.[i.device_id]:null;return!s||Nn(s)}function jo(e,t){return Bo(e,t)&&!!e?.states?.[t]}function qo(e,t,i=[t]){const s=function(e,t){const i=e=>[e.id,e.primary,e.hidden?1:0,e.userHidden?1:0,e.entities.join(","),(e.controls||[]).join(","),e.marker?.binding||"",!0===e.marker?.is_light?1:0,e.marker?.light_entity||"",(e.marker?.controls||[]).join(",")].join("|");return`${i(e)}\n${t.map(i).join("\n")}`}(t,i),n=Ro.get(t);if(n&&n.states===(e?.states||null)&&n.entities===(e?.entities||null)&&n.devices===(e?.devices||null)&&n.signature===s)return n.result;const o=new Set(t.entities);for(const e of t.controls||[])e.startsWith("marker:")||o.add(e);const a=[],r=ho(e,i),l=r.filter(e=>e.key.startsWith("marker:")).map(e=>({ref:e.key,on:e.on,name:e.device?.name||e.key})),c=i=>{const s=Ao(i);if(a.some(e=>e.key===s))return;const n=Ho(e,t,i,l),o="entity_attribute"===i.kind?`${i.entity_id} · ${i.attribute}`:"entity_state"===i.kind?i.entity_id:"derived_marker_state"===i.kind?i.ref:"LQI";a.push({key:s,source:i,label:n.sourceLabel,technical:o,value:n.text,available:"available"===n.availability})};for(const t of[...o].sort()){if(!jo(e,t))continue;const i=t.split(".")[0],s=e?.entities?.[t];if("button"===i||"event"===i||"config"===s?.entity_category)continue;c({kind:"entity_state",entity_id:t});const n=e?.states?.[t];for(const e of To[i]||[])e in(n?.attributes||{})&&c({kind:"entity_attribute",entity_id:t,attribute:e})}const h=new Set((t.marker?.controls||t.controls||[]).filter(e=>e.startsWith("marker:")));!0===t.marker?.is_light&&t.marker.id&&h.add(`marker:${t.marker.id}`);const d=new Set(r.map(e=>e.key));for(const e of[...h].sort())d.has(e)&&c({kind:"derived_marker_state",ref:e});return t.virtual||null==_o(e,t.entities)||c({kind:"derived_lqi"}),Ro.set(t,{states:e?.states||null,entities:e?.entities||null,devices:e?.devices||null,signature:s,result:a}),a}function Wo(e,t,i){const s=Lo(e,t);if(s&&i.some(e=>e.key===Ao(s)))return s;const n=t.primary&&i.find(e=>e.key===`state:${t.primary}`);if(n)return n.source;const o=[e=>e.technical.includes("temperature"),e=>e.technical.includes("humidity"),e=>e.technical.includes("battery")];for(const e of o){const t=i.find(e);if(t)return t.source}return i.find(e=>"derived_lqi"!==e.source.kind)?.source||i[0]?.source||null}function Uo(e){return"boolean"==typeof e?.themes?.darkMode?e.themes.darkMode?"theme-dark":"theme-light":""}function Go(e){const t=[];if(1!==e.scale&&t.push(`--dev-scale:${e.scale}`),"none"!==e.pulse.kind){t.push(`--ripple-scale:${e.pulse.diameterScale}`);const n=(i=e.pulse.color,s=null,"string"==typeof i&&(Te.test(i)||Ae.test(i))?i:s);n&&t.push(`--ripple-color:${n}`)}var i,s;return t}function Vo(e){const t=[...String(e)].reduce((e,t)=>/\s/.test(t)?e+.35:e+(t.codePointAt(0)>255?1:.62),0);return t<=8?.45:Math.max(.25,Math.round(3.6/t*1e3)/1e3)}function Ko(e,t){const i=e.pulse,s=i.generation%2==0,n=function(e){const t=e.valueBadge;if(!t||!1!==t.configured)return[];const i=[];return null!=e.tempText&&"temperature"!==t.tone&&i.push({kind:"temperature",text:e.tempText,suffix:"°"}),null!=e.humText&&"humidity"!==t.tone&&i.push({kind:"humidity",text:e.humText,suffix:"%"}),i}(e),o=e.valueBadge,a=o?.position||"right",r=!!o||n.length>0,l=["device-shell",null!=e.valueText?"text-shell":"",r?`with-values pos-${a}`:"",n.length?"with-legacy":""].filter(Boolean).join(" ");return q` - ${"none"!==i.kind&&"dot"!==i.reducedMotionIndicator?q``:G} - ${"dot"===i.reducedMotionIndicator?q``:G} - ${t.newDevice?q``:G} - ${e.haDisabled?q``:G} - - ${null!=e.lqiText?q`${e.lqiText}`:G} - `}function Yo(e,t,i){return{kind:"none",reason:"none",generation:e,expiresAt:null,color:t,diameterScale:i,animated:!1,reducedMotionIndicator:"none"}}const Jo=new WeakMap;function Xo(e){const t=function(e){const t=e?.connection||e;return!t||"object"!=typeof t&&"function"!=typeof t?null:t}(e);if(!t)return null;let i=Jo.get(t);return i||(i={revision:0,loaded:!1,configEntries:{},manifests:{},listeners:new Set,refs:0},Jo.set(t,i)),i}function Zo(e){e.revision++;for(const t of[...e.listeners])try{t()}catch{}}function Qo(e,t){const i=Xo(e);if(!i)return()=>{};i.refs++,i.listeners.add(t),async function(e,t){t.loading||"function"!=typeof e?.callWS?t.loading:t.loaded&&t.loadedAt&&Date.now()-t.loadedAt<3e5||(t.loading=(async()=>{const[i,s]=await Promise.allSettled([e.callWS({type:"config_entries/get"}),e.callWS({type:"manifest/list"})]);"fulfilled"===i.status&&Array.isArray(i.value)&&(t.configEntries=Object.fromEntries(i.value.filter(e=>"string"==typeof e?.entry_id).map(e=>[e.entry_id,e]))),"fulfilled"===s.status&&Array.isArray(s.value)&&(t.manifests=Object.fromEntries(s.value.filter(e=>"string"==typeof e?.domain).map(e=>[e.domain,e]))),t.loaded=!0,t.loadedAt=Date.now(),t.loading=void 0,Zo(t)})().catch(()=>{t.loaded=!0,t.loadedAt=Date.now(),t.loading=void 0,Zo(t)}),t.loading)}(e,i),async function(e,t){if(t.unsubscribe||t.subscribing)return t.subscribing;const i=e?.connection?.subscribeMessage;"function"==typeof i&&(t.subscribing=(async()=>{try{t.unsubscribe=await i.call(e.connection,e=>{const i=Array.isArray(e)?e:Array.isArray(e?.entries)?e.entries:e?[e]:[];let s=!1;const n={...t.configEntries};for(const e of i){const t=e?.entry||e?.data?.entry||("string"==typeof e?.entry_id?e:null),i=t?.entry_id;if("string"!=typeof i)continue;const o=e?.type||e?.action||e?.data?.action;"removed"===o||"remove"===o?delete n[i]:n[i]=t,s=!0}s&&(t.configEntries=n,Zo(t))},{type:"config_entries/subscribe"})}catch{}finally{0===t.refs&&(t.unsubscribe?.(),t.unsubscribe=void 0),t.subscribing=void 0}})(),t.subscribing)}(e,i);let s=!1;return()=>{s||(s=!0,i.listeners.delete(t),i.refs=Math.max(0,i.refs-1),i.refs||(i.unsubscribe?.(),i.unsubscribe=void 0))}}function ea(e){const t=Xo(e);return t?{revision:t.revision,loaded:t.loaded,configEntries:t.configEntries,manifests:t.manifests}:{revision:0,loaded:!1,configEntries:{},manifests:{}}}function ta(e,t,i=ea(e),s){const n=i.manifests[t];return function(e,t){if("function"!=typeof e?.localize)return"";try{const i=`component.${t}.title`,s=e.localize(i);return"string"==typeof s&&s!==i?s:""}catch{return""}}(e,t)||String(n?.name||"")||String(s||"")||t.replace(/_/g," ")}function ia(e,t,i,s,n){const o=String(t||"").trim();if(!o)return null;const a=n?s.configEntries[n]:null;return{domain:o,label:ta(e,o,s,a?.title),configEntryId:n,configEntryTitle:a?.title,confidence:i}}function sa(e){const t={"registry-owner":0,"entity-platform":1,"identifier-fallback":2},i=new Set;return e.filter(e=>!!e).sort((e,i)=>t[e.confidence]-t[i.confidence]||e.label.localeCompare(i.label)||e.domain.localeCompare(i.domain)).filter(e=>{const t=`${e.domain}\n${e.configEntryId||""}`;return!i.has(t)&&(i.add(t),!0)})}function na(e,t,i,s=ea(e)){const n=i.entities?.[t]||e?.entities?.[t];if(!n)return null;const o=n.config_entry_id||void 0,a=o?s.configEntries[o]?.domain:"";return ia(e,n.platform||a,"entity-platform",s,o)}var oa={"editor.context_actions":"Actions: {object}","editor.tool_options":"Tool options: {tool}","editor.palette":"Palette: {tool}","editor.open_group":"Tool group: {group}","editor.group_active":"{group} — active: {item}","editor.disabled_action":"{action} is unavailable: {reason}","color_picker.title":"Color picker","color_picker.hue":"Hue","color_picker.saturation":"Saturation","color_picker.value":"Brightness","color_picker.hex":"Hex color","color_picker.invalid_hex":"Enter a 3- or 6-digit hex color","btn.properties":"Properties","btn.keep_as_walls":"Keep as walls","btn.delete_room_keep_walls":"Delete room, keep walls","btn.delete_room_with_walls":"Delete room and walls","title.markup_select":"Select and edit walls, columns and saved outlines","title.markup_column":"Column: click a grid point to place a square column","markup.partition":"Partition","markup.column":"Column","markup.hint_column":"click a grid point to place a square column","history.draft_segment":"Add room-draft segment","history.draft_merge":"Join unfinished room outlines","history.draft_segment_delete":"Remove room-draft segment","history.partition_add":"Add partition","history.column_add":"Add column","history.physical_edit":"Change physical object","history.physical_delete":"Delete physical object","history.physical_move":"Move physical object","history.contour_to_partitions":"Convert contour to closed walls","history.wall_chain_finish":"Finish wall chain","history.wall_face_batch":"Create rooms from walls","toast.column_duplicate":"A column with the same centre and outer size already exists","confirm.delete_draft":"Delete the whole unfinished room contour?","confirm.delete_draft_segment":"Delete this draft segment? The remaining contour may split in two.","physical.partition_properties":"Partition properties","physical.column_properties":"Column properties","physical.draft_properties":"Draft segment properties","physical.shape":"Shape","physical.square":"Square","physical.circle":"Circle","physical.diameter":"Diameter","physical.side":"Side","physical.rotation":"Rotation angle","physical.length":"Length","physical.allowed_range":"Allowed: {min}–{max} {unit}","physical.delete_segment":"Delete segment","physical.delete_draft":"Delete entire outline","physical.column_size_title":"Outer side of the square column placed by the click (1–150 cm).","card.title":"House plan","count.devices":"{n} dev.","empty.no_spaces":"No spaces yet.","empty.add_first":"Add the first space and upload a floor plan.","empty.install":'Install the House Plan integration and add it in "Devices & services".',"btn.add_space":"Add space","btn.cancel":"Cancel","btn.save":"Save","btn.close":"Close","btn.delete":"Delete","btn.edit":"Edit","btn.open_in_ha":"Open in HA","btn.reset":"Reset","btn.attach":"Attach…","btn.upload":"Upload…","btn.replace":"Replace…","title.zoom_in":"Zoom in","title.zoom_out":"Zoom out","title.zoom_fit":"Fit all","title.add_device":"Add a device to the plan","title.show_all":"Show hidden and disabled devices as service markers (this tab only)","title.markup":"Room markup: grid, lines, outlines","title.configure_space":"Configure space","title.add_space":"Add space","title.markup_add":"Walls: draw a continuous chain; Shift locks to 45°. Changing tool or leaving the editor finishes it as independent walls","title.markup_merge":"Merge: click one room, then the neighbour it shares a wall with","title.markup_split":"Split a room: click the room, then two points on its walls","title.markup_delroom":"Delete room: click inside a room and confirm","title.need_plan":"Upload a floor-plan image","markup.add":"Walls","markup.merge":"Merge","markup.split":"Split","markup.resize":"Resize","title.markup_resize":"Resize rooms: drag an available horizontal or vertical wall handle","markup.hint_resize":"drag an available wall handle · the wall stops at the first unsafe position · Esc cancels · Ctrl+Z — undo","resize.disabled.diagonal":"This wall is angled; Resize supports only horizontal and vertical walls","resize.disabled.side-angle":"An adjoining wall is angled; Resize requires both adjoining walls to meet this wall at right angles","resize.disabled.duplicate-physical-wall":"A separate partition, unfinished outline, or column overlaps this wall; remove or move it before resizing","resize.disabled.partial-shared":"A neighbouring room uses only part of this wall, so it cannot be moved safely as one shared wall","resize.disabled.unequal-shared":"The neighbouring room uses different endpoints or wall length, so the walls cannot move together safely","resize.disabled.multiple-rooms":"Moving this wall would affect more than two rooms","resize.disabled.thickness-conflict":"The wall thickness cannot be preserved safely","resize.disabled.opening-conflict":"An opening on this wall cannot be moved safely","resize.disabled.invalid-geometry":"This wall cannot be resized without changing the plan structure","resize.limit_stopped":"Wall stopped: the next step would break a junction limit","resize.commit_failed":"Resize was cancelled because the final plan did not pass the geometry check","resize.preview_failed":"Resize stopped at the last safe position because the plan geometry could not be preserved","markup.opening":"Opening","markup.delete_room":"Delete room","history.undo":"Undo","history.redo":"Redo","history.undo_named":"Undo: {name} (Ctrl+Z)","history.redo_named":"Redo: {name} (Ctrl+Shift+Z / Ctrl+Y)","history.undo_empty":"Nothing to undo","history.redo_empty":"Nothing to redo","history.undone":"Undone: {name}","history.redone":"Redone: {name}","history.add_room":"Create room","history.split_room":"Split room","history.merge_rooms":"Merge rooms","history.resize_room":"Resize room","history.wall_thickness":"Change wall thickness","history.add_opening":"Add door or window opening","history.edit_opening":"Edit opening","history.move_opening":"Move opening","history.delete_opening":"Delete opening","history.delete_room":"Delete room","history.delete_room_keep_walls":"Delete room, keep walls","history.delete_room_with_walls":"Delete room and walls","history.decor_add":"Add decor object","history.decor_edit":"Edit decor object","history.decor_move":"Move decor object","history.decor_transform":"Transform decor object","history.decor_delete":"Delete decor object","history.backdrop_transform":"Transform plan backdrop","title.markup_opening":"Openings: click a wall to place, click an opening to edit","opening.new":"New opening","opening.edit":"Opening","opening.door":"Door","opening.window":"Window","opening.gate":"Gate","opening.passage":"Open passage","opening.passage_binding_warning":"Saving will remove the open/close sensor and lock.","opening.invalid_passage_fields":"The open passage on plan “{room}” has unsupported settings: {fields}.","opening.type_label":"Type","opening.length_label":"Length, cm","opening.contact_label":"Open/close sensor","opening.lock_label":"Lock","opening.none":"— none —","opening.search_ph":"Search: name or entity id…","opening.invert":"Invert open/closed","opening.flip_h":"Hinge on the other jamb","opening.flip_v":"Opens to the other side","opening.open":"Open","opening.closed":"Closed","opening.locked":"Locked","opening.unlocked":"Unlocked","opening.state_unknown":"unavailable","opening.no_entities":"No sensors bound — a static symbol on the plan.","toast.opening_no_wall":"Click next to a room wall or independent wall","opening.host_partition":"Independent wall","opening.partition_orphan":"The independent wall for this opening no longer exists","opening.partition_jamb_margin":"Leave at least {distance} between the opening and the end of the wall","opening.rebind_partition":"Attach to another independent wall","confirm.delete_partition_openings_title":"Delete wall and openings?","confirm.delete_partition_openings_body":"This wall contains {count} opening(s). They will be deleted together.","confirm.delete_partition_openings_item":"• {type}, {length}","markup.delete":"Delete","markup.hint_points":"points: {n} · Shift — 45° steps · Esc — finish chain · Ctrl+Z — undo a point · closing an area offers a room","markup.hint_start":"click a grid dot to start a wall chain","tip.lqi":"average zigbee signal:","tip.area":"area: {value}","info.device_header":"Device on the plan","info.model":"Model","info.state":"State","info.link":"Link","info.manuals":"Manuals","info.none":"No additional information","marker.new_device":"New device","marker.name_label":"Name (shown on the plan)","marker.name_ph":"Name","marker.binding_label":"Bind to an HA device","marker.binding_disabled":"disabled in Home Assistant","marker.virtual_option":"Virtual device (no binding)","marker.search_ph":"Search device / group…","marker.nothing_found":"nothing found","marker.room_label":"Room","marker.room_override":" (override placement)","marker.room_choose":"— select a room —","marker.room_auto":"— by device area (auto) —","marker.icon_label":"Icon","marker.icon_ph":"mdi:… (empty = auto)","marker.display_label":"Display","display.badge":"Icon + state","display.icon_ripple":"Icon + state and activity","display.static_icon":"Always static icon","marker.display_hint":"Icon + state changes the plate for work, open, alarm and unavailable states. Icon + state and activity additionally shows a short pulse for events and a continuous pulse for ongoing work, movement or presence. Value + state replaces the icon with one unambiguous HA value. A static icon never reacts to device states.","marker.display_hint_badge":"The icon and dynamic plate show device state without the ordinary activity pulse. Red alarms remain visible.","marker.display_hint_icon":"The icon and dynamic plate show device state without the ordinary activity pulse. Red alarms remain visible.","marker.display_hint_icon_ripple":"The icon, dynamic plate and pulse show a short pulse for events and a continuous pulse for work, motion or presence. Red alarms have separate priority.","marker.display_hint_value":"One unambiguous Home Assistant value replaces the icon while the plate continues to show state. Red alarms remain visible.","marker.display_hint_static_icon":"The theme-aware shell and icon always stay the same. State, activity, unavailability and alarms do not change the face.","marker.static_alarm_warning":"This device can report alarms. Static display hides the marker's visual alarm indication.","marker.preview.title":"Display preview","marker.preview.actual":"Now","marker.preview.example":"Example","marker.preview.integration":"Provided by","marker.preview.source":"Display source","marker.preview.current_state":"Current state","marker.preview.result":"On the plan","marker.preview.details":"Source details","marker.preview.select_source":"Choose a Home Assistant device or entity to see its actual display.","marker.preview.unknown_provider":"Unknown integration","marker.preview.virtual_provider":"House Plan · virtual device","marker.preview.no_source":"No active source","marker.preview.no_state":"No current state","marker.preview.mixed_states":"Several different states","marker.preview.multiple_sources":"{n} sources","marker.preview.more_sources":"+{n} more","marker.preview.scaled":"The preview is scaled to {n}% to fit. The saved size is unchanged.","marker.preview.demo_activity":"Show activity example","marker.preview.demo_notice":"Temporary activity example; the actual device state is unchanged.","marker.preview.demo_short":"Show short activity","marker.preview.demo_continuous":"Show continuous activity","marker.preview.stop_continuous":"Stop continuous activity","marker.preview.demo_short_notice":"Short activity example; the actual device state is unchanged.","marker.preview.demo_continuous_notice":"Continuous activity example; the actual device state is unchanged.","marker.preview.demo_already_visible":"The current state already shows a real activity or alarm effect.","marker.preview.reduced_motion":"System reduced-motion is enabled, so ordinary activity is shown as a dot.","marker.preview.reason.neutral":"Neutral dark plate","marker.preview.reason.working":"Yellow plate: the device is working now","marker.preview.reason.working_activity":"Yellow plate and activity effect: the device is working now","marker.preview.reason.open":"Orange plate: physically open or unlocked","marker.preview.reason.cover_icon_state":"Neutral plate; the cover entity controls the displayed state and icon","marker.preview.reason.presence":"Activity effect while presence is detected","marker.preview.reason.event":"Short activity effect after a detected event","marker.preview.reason.transition":"Activity effect while the device is moving or changing state","marker.preview.reason.media_neutral":"Media devices stay on a neutral dark plate while available","marker.preview.reason.unavailable":"Subdued neutral plate: unavailable or off","marker.preview.reason.alarm":"Red alarm plate; alarm indication is always shown","marker.preview.reason.live_states_disabled":"Live-state styling is disabled for this card","marker.preview.reason.value_no_state":"No usable state value; the icon is shown instead","marker.preview.reason.value_ambiguous_sources":"Several possible value sources; the icon is shown instead","marker.preview.reason.value_non_scalar":"The source did not return a simple value; the icon is shown instead","marker.preview.reason.value_virtual":"A virtual device has no Home Assistant value; the icon is shown instead","marker.preview.reason.vacuum_live_plan_only":"Live vacuum position and trail are available only on the full plan","marker.preview.reason.hidden_design_preview":"The device is hidden on the plan; this design preview remains visible","marker.preview.reason.composite_power_source":"State comes from the device Power entity; auxiliary switches are ignored","marker.preview.reason.activity_display_disabled":"The selected display mode does not show ordinary activity effects","marker.preview.reason.ha_disabled":"The Home Assistant binding is disabled and will be hidden on the plan","marker.preview.reason.orphaned":"The saved Home Assistant binding can no longer be found","marker.preview.reason.static_icon":"Static mode: device state does not change the icon","marker.activity_color":"Activity pulse color","marker.ripple_size":"Activity pulse size","marker.activity_alarm_note":"Color and size affect ordinary activity only and do not change red alarms.","marker.pulse_a11y_alarm":"Alarm","marker.pulse_a11y_event":"Recent event","marker.pulse_a11y_presence":"Presence detected","marker.pulse_a11y_transition":"Changing state","marker.pulse_a11y_running":"Working","marker.state_a11y_neutral":"Default state","marker.state_a11y_working":"Working","marker.state_a11y_open":"Open","marker.state_a11y_locked":"Locked","marker.state_a11y_unlocked":"Unlocked","marker.state_a11y_alarm":"Alarm","marker.state_a11y_unavailable":"Unavailable","marker.lqi_a11y_low":"LQI {value}, low signal","marker.lqi_a11y_mid":"LQI {value}, medium signal","marker.lqi_a11y_high":"LQI {value}, high signal","marker.size_label":"Icon size / rotation","marker.angle_label":"Rotate","marker.model_label":"Model","marker.model_ph":"e.g. Aqara T&H","marker.link_label":"Link","marker.desc_label":"Description","marker.desc_ph":"Notes, specs…","marker.manuals_label":"Manuals (PDF etc.)","marker.sub_device":"device","marker.sub_z2m_group":" · Z2M group","marker.sub_group":"group","marker.sub_helper":"helper","space.new":"New space","space.header":"Space","space.title_label":"Title","space.title_ph":"e.g. Garage","space.plan_label":"Floor plan (background)","space.no_plan":"no plan image","space.plan_alt":"plan","room.new":"New room","room.name_label":"Display name","room.name_ph":"e.g. Terrace","room.area_label":"Home Assistant area (unassigned)","room.no_area_option":"— no area —","room.default_name":"Room","device.unnamed":"unnamed","device.light_group":"light group","device.fallback":"device","device.virtual":"virtual device","confirm.delete_room":'Delete room "{name}"?',"confirm.delete_room_title":'Delete room "{name}"?',"confirm.delete_room_body":"Choose whether the room's exclusive physical walls should remain. Walls shared with another room are always kept.","confirm.merge_rooms":'Merge rooms "{a}" and "{b}"?',"confirm.remove_marker":'Delete "{name}" from the plan? The device will disappear completely and stop contributing to plan data. You can add it again later.',"confirm.erase_decor":"Erase the {kind} object? You can undo this action from the editor history.","confirm.delete_space":'Delete space "{title}" with all its rooms and markup?',"space.delete_blocked":"This space is still used by {n} device(s). Move them to another space or delete them first.","toast.pos_save_failed":"Failed to save position: {err}","toast.no_entity":"The device has no suitable entity","toast.ha_disabled_action":"A Home Assistant object that is disabled cannot be used on the plan.","toast.ha_disabled_show_device":"A device disabled in Home Assistant cannot be shown on the plan. Enable it in Home Assistant first.","toast.ha_disabled_show_entity":"An entity disabled in Home Assistant cannot be shown on the plan. Enable it in Home Assistant first.","toast.ha_disabled_add":"A disabled Home Assistant object cannot be added to the plan. Enable it in Home Assistant first.","toast.ha_binding_unverified":"The object status could not be verified through the Home Assistant registry. Display and actions are temporarily unavailable.","toast.markup_needs_server":"Markup is available after the config is moved to the server","toast.space_order_changed":"Order changed. If any card pins its floor by number, check those panels.","toast.conflict":"Config was changed in another window — data refreshed, repeat your last action","toast.cfg_save_failed":"Failed to save config: {err}","toast.room_overlap":"The outline overlaps room “{name}” — rooms must not overlap","toast.contour_min_edges":"Draw at least two edges before closing the outline","toast.contour_cannot_close":"The outline cannot close because it is degenerate or intersects itself","toast.merge_not_adjacent":"Only rooms that share a wall can be merged","toast.rooms_merged":"Rooms merged into “{name}”","toast.split_pick_wall":"Start the cut on the room’s wall","toast.split_bad_cut":"The cut must run wall to wall inside the room, without crossing walls or itself","merge.header":"Merge rooms","merge.hint":"The merged room keeps one name and one area. The other area is released — its devices leave the plan until another room claims it.","merge.keep":"Keep","merge.no_area":"no area","room.split_header":"New room from the split","toast.room_saved":"Room saved ({n}). Devices added: {added}. Outline the next one or exit markup.","toast.room_saved_no_area":"Room saved ({n}, no area). Outline the next one or exit markup.","toast.marker_needs_server":"Device editing is available after the config is moved to the server","toast.virtual_name_required":"Enter a name for the virtual device","toast.marker_saved":"Device saved","toast.marker_removed":"Device deleted from the plan","toast.integration_missing":"The House Plan integration is not installed — management unavailable","toast.plan_formats":"Supported formats: SVG, PNG, JPG, WebP","toast.plan_required":"Upload a floor plan — it is required","toast.space_added_onboard":"Space added. Outline the rooms: click grid dots and close the contour.","toast.space_added":"Space added","toast.space_saved":"Space saved","toast.space_deleted":"Space deleted","toast.delete_failed":"Delete failed: {err}","toast.error":"Error: {err}","toast.toggle_target_unavailable":"Target “{name}” is unavailable — no action was performed","toast.toggle_targets_unavailable":"Targets are unavailable: {names}. No action was performed","toast.file_failed":'File "{name}" was not uploaded: {err}',"toast.files_attached":"Files attached: {n}","err.unknown":"unknown error","err.code":"code {code}","err.too_large":"file larger than {mb} MB","err.bad_ext":"unsupported type (PDF/image expected)","err.unauthorized":"administrator rights required","editor.title":"Title","editor.default_floor":"Initial space","editor.default_floor_missing":"Initial space “{id}” no longer exists. Choose another space.","editor.floor":"Fixed space","editor.floor_none":"— not fixed —","editor.floor_index":"YAML index {index} (preserved)","fixed_floor.loading":"Loading the fixed space…","fixed_floor.invalid_title":"The fixed space is unavailable","fixed_floor.invalid_body":"Check floor in the card configuration. The configured value is: {value}","editor.icon_size":"Icon size, % of plan width","editor.show_temperature":"Show temperature","editor.live_states":"Live states (on/off, open…)","editor.show_signal":"Show zigbee signal (LQI)","editor.language":"Interface language","editor.lang_auto":"Auto (HA profile)","editor.lang_en":"English","editor.lang_ru":"Русский","title.icon_rules":"Icon rules: which MDI icon devices get by name","rules.title":"Icon rules","rules.hint":"Rules are checked top-down against “device name + model” (case-insensitive regex); the first match wins. When nothing matches, the entity device class decides, then the generic chip icon.","rules.pattern_ph":"regex, e.g. plug|socket","rules.icon_ph":"mdi:power-socket-de","rules.add":"Add rule","rules.reset":"Reset to defaults","rules.test_ph":"Try a device name…","rules.invalid":"invalid regex","rules.saved":"Icon rules saved","btn.up":"Up","btn.down":"Down","tap.info":"Device card","tap.more_info":"HA more-info dialog","tap.toggle":"Toggle state","marker.tap_label":"Tap action for this device","tap.toggle_note":"The resolved state is shown below. Secure devices are never toggled from the plan.","import.title":"Create spaces from HA floors","import.hint":"Your Home Assistant already knows these floors. Pick the ones to turn into plan spaces — you will upload a floor-plan image for each one next. Rooms are then outlined by hand on the plan.","import.start":"Create {n} space(s)","import.manual":"Start from scratch","import.progress":"Floor {i} of {n}","import.done":"Spaces created. Outline the rooms: click grid dots and close the contour.","btn.skip":"Skip","space.scale_label":"Scale (grid cell size)","space.scale_unit":"cm per cell","space.scale_unit_imperial":"in per cell","space.display_section":"Display","space.show_borders":"Always show room borders","space.zero_wall_style":"Zero-thickness walls","space.zero_wall_dashed":"Dashed","space.zero_wall_solid":"Solid","space.zero_wall_help":"Dashed walls let light through. Solid walls block light even at zero thickness.","space.show_names":"Show room names (drag to move)","space.room_color":"Border & name color","space.opacity":"Opacity","space.fill_label":"Room fill","fill.none":"None","fill.lqi":"Zigbee signal","fill.light":"Lights","fill.custom":"Custom color","space.custom_fill":"Fill color","space.source_file":"I have a floor-plan image","space.source_draw":"No image — I'll outline rooms by hand","space.orientation":"Canvas","orient.landscape":"Landscape","orient.portrait":"Portrait","orient.square":"Square","fill.temp":"Temperature","space.temp_min":"Comfort from","space.temp_max":"to","tip.temp_avg":"average temperature:","tip.hum_avg":"average humidity:","space_card.button":"Open the space plan","space_card.not_found":"Space “{id}” not found","space_card.loading":"Loading…","continuity.restore_plan":"Restoring floor plan…","continuity.restore_connection":"Restoring device connection…","continuity.retry":"Try again","editor.space":"Space","editor.show_button":"Show button","editor.button_label":"Button label","editor.button_target":"Target dashboard path","marker.sub_entity":"entity","title.general_settings":"General settings","gs.title":"General settings","gs.hint":"Fill colors apply to every space; each color has its own opacity. Which fill mode a space uses is set in that space's dialog.","gs.light_group":"Fill: lights","gs.light_on":"Lights on","gs.light_off":"All lights off","gs.temp_group":"Fill: temperature","gs.temp_cold":"Cold","gs.temp_ok":"Comfortable","gs.temp_hot":"Hot","gs.lqi_group":"Fill: zigbee signal","gs.lqi_low":"Weak signal","gs.lqi_high":"Strong signal","gs.reset":"Reset to defaults","gs.saved":"General settings saved","space.show_lqi":"Show zigbee signal (LQI) next to devices","space.hide_decor":"Hide the decorative layer","space.hide_decor_tip":"Lines, shapes, labels and furniture stay where they are — visible in the backdrop editor, not on the plan.","space.hide_openings":"Hide openings","space.hide_openings_tip":"Door, window and gate symbols are not drawn, but the openings keep working: light passes through, the sun comes in at a window, contact sensors still open them. The plan editor always shows them.","gs.light_none":"No light sources","mode.plan":"Plan editor","mode.devices":"Device editor","display.value":"Value + state","marker.subarea":"no area, manual","device.new":"New device — open its editor to dismiss","opening.unlock_action":"Unlock","opening.lock_action":"Lock","opening.lock_pending":"Working…","title.close_editor":"Close editor (back to view)","devbar.add":"Add","devbar.show_all":"Hidden and disabled","devbar.rules":"Icon rules","device_inbox.button":"Devices","device_inbox.title":"Devices on the plan","device_inbox.search":"Search devices, entities and integrations…","device_inbox.add_virtual":"Add virtual device","device_inbox.tab_on_plan":"On plan","device_inbox.tab_available":"Available","device_inbox.tab_hidden":"Hidden","device_inbox.tab_readd":"Available again","device_inbox.only_new":"New only","device_inbox.show_entities":"Show entities","device_inbox.show_hidden":"Show hidden on plan","device_inbox.new":"New","device_inbox.find":"Find on plan","device_inbox.edit":"Edit","device_inbox.hide":"Hide","device_inbox.show":"Show","device_inbox.add":"Add","device_inbox.readd":"Add again","device_inbox.hide_available":"Hide from list","device_inbox.show_more":"Show more","device_inbox.more_actions":"More actions","device_inbox.find_hidden_hint":"Enable “Show hidden on plan” first","device_inbox.show_disabled":"Activate this binding in Home Assistant before showing it","device_inbox.empty_on_plan":"No matching devices are on the plan yet.","device_inbox.empty_available":"No devices are available to add.","device_inbox.empty_hidden":"There are no hidden devices.","device_inbox.empty_readd":"No removed devices are available to add again.","device_inbox.reason_visible_auto":"Discovered automatically","device_inbox.reason_visible_explicit":"Added explicitly","device_inbox.reason_manual_hidden":"Hidden by user","device_inbox.reason_automatic_hidden":"Hidden automatically","device_inbox.reason_service_entry":"Service-only registry entry","device_inbox.reason_excluded_integration":"Integration excluded by device filters","device_inbox.reason_excluded_domain":"Non-spatial entity type","device_inbox.reason_grouped_light":"Represented by a room light group","device_inbox.reason_represented_by_parent":"Represented by its parent device","device_inbox.reason_removed":"Previously removed from the plan","device_inbox.reason_available":"Available to add to the plan","device_inbox.reason_no_bound_room":"HA area is not linked to a plan room","device_inbox.status_ha_disabled":"Disabled in Home Assistant","device_inbox.status_orphaned":"Binding is no longer present in Home Assistant","device_inbox.status_unverified":"Home Assistant registry is temporarily unavailable","device_inbox.saved":"Device list updated","space.roomcard_section":"Room card shows:","space.label_temp":"Temperature","space.label_hum":"Humidity","space.label_lqi":"Average Zigbee signal","space.label_light":"Lights on/off","roomcard.light_on":"On","roomcard.light_off":"Off","roomcard.light_partial":"{on} of {total}","toast.split_pick_inside":"Intermediate cut points must be inside the room","mode.decor":"Background editor","decor.select":"Select","decor.line":"Line","decor.rect":"Rectangle","decor.ellipse":"Oval","decor.text":"Text","decor.erase":"Erase","decor.erase_confirm_title":"Erase object?","decor.color":"Color","decor.width":"Line width","decor.line_style":"Line style","decor.line_style_solid":"Solid","decor.line_style_dashed":"Dashed","decor.fill":"Fill","decor.fill_color":"Fill color","decor.length":"Length","decor.size":"Size","decor.angle":"Rotation angle, °","decor.text_size":"Text size","decor.backdrop_properties":"Plan backdrop properties","decor.text_title":"Text label","decor.object_title":"Edit {kind}","decor.text_label":"Text","decor.live_group":"Insert HA variable","decor.live_entity":"Entity","decor.live_entity_ph":"choose an entity","decor.live_attr":"Value","decor.live_attr_ph":"choose state or attribute","decor.live_state":"State","decor.backdrop":"Backdrop image","decor.backdrop_hint":"Drag to move; pull a corner to resize; use the top handle to rotate. Shift changes proportions or frees the angle.","decor.backdrop_reset":"Reset the picture","decor.backdrop_reset_done":"The picture is back at its original place and size","marker.icon_auto":"Auto: {icon} (by icon rules; pick one to override)","marker.icon_pin_auto":"Pin","mode.plan_tip":"Plan editor — the geometry of the home: draw and split/merge rooms, bind them to HA areas, place doors, windows and gates, move room cards, set the scale","mode.devices_tip":"Device editor — everything about icons: drag to position, click to edit binding/icon/display, add virtual devices, icon rules","mode.decor_tip":"Background editor — purely visual decor under the plan: lines, rectangles, ovals and text labels that never react to clicks","space.glow_enabled":"Light-source glow","gs.glow_group":"Light-source glow","gs.glow_base":"House darkness","gs.glow_light":"Default light color / intensity","gs.wall_group":"Walls","gs.wall_fill":"Wall fill","gs.glow_radius":"Glow radius","gs.unit_m":"m","gs.unit_ft":"ft","marker.controls_label":"Controls other light sources","marker.controls_hint":"With tap action “Toggle”, a click flips every source added here. The marker’s own bound entity is controlled directly and cannot be added; use the switch below to classify it as a light.","marker.controls_filter":"Search lights and switches…","info.controls":"Controls","marker.glow_radius_label":"Glow radius","marker.glow_radius.help":"Sets the glow radius in metres or feet; an empty value uses the radius from general settings.","marker.glow_radius.help.aria":"Help: glow radius","markup.wallthick":"Thickness","markup.select":"Select","title.markup_wallthick":"Thickness — click a wall to set its thickness from 0 to 100 cm.","markup.hint_wallthick":"click a wall · Esc closes without applying","wallthick.field":"Thickness","wallthick.unit_cm":"cm","wallthick.unit_in":"in","wallthick.apply_room":"Apply to all walls of this room","markup.draw_wall_title":"Thickness of each new wall segment (0–100 cm). Shared walls keep the existing value.","room.queue_progress":"Room {current} of {total}","toast.wall_rooms_saved":"Created rooms: {n}","toast.wall_chain_saved":"Wall chain saved","toast.wallthick_pick":"Click a wall","toast.wallthick_set":"Wall thickness set","toast.wallthick_cleared":"Wall thickness removed","toast.physical_range":"Enter a value from {min} to {max} {unit}","toast.zero_wall_opening_conflict":"Remove the opening on this wall segment first.","toast.zero_wall_ambiguous":"The wall segment is ambiguous. Simplify or adjust the junction.","toast.zero_wall_migration_blocked":"The space was not converted: {reason}. No data was changed.","toast.physical_angle":"Enter a rotation angle from 0° up to, but not including, 90°","toast.physical_limit":"The space has reached the limit for this type of geometry","toast.geometry_unsafe":"Change canceled: wall geometry could not be built safely.","junction.limit_angle":"The angle between walls is too sharp: {actual}°, minimum {limit}°.","junction.limit_valence":"Too many walls meet in one node: {actual}, maximum {limit}.","junction.limit_length":"The wall segment is too short: {actual} cm, minimum {limit} cm.","junction.limit_distance":"Walls and nodes are too close: {actual} cm, minimum {limit} cm.","junction.limit_clearance":"No room interior is left: {actual} cm², minimum {limit} cm².","junction.limit_check_failed":"The junction check could not run — the change was not saved","toast.wall_model_migration_blocked":"The wall model could not be updated: {reason}. The plan was not changed. Run “Optimize plans”; if the error repeats, fix the conflicting wall geometry.","toast.wall_model_client_outdated":"Update the card and reload the page before editing the plan.","wall_model.reason.invalid-room":"invalid room contour","wall_model.reason.zero-length":"zero-length wall segment","wall_model.reason.third-owner":"wall shared by more than two rooms","wall_model.reason.duplicate-id":"conflicting wall identifiers","wall_model.reason.thickness-conflict":"conflicting wall thickness values","wall_model.reason.opening-host":"opening cannot be assigned to one wall","toast.delete_room_pick":"Click inside the room you want to delete","toast.plan_snap_ambiguous":"Zoom in to choose the wall node","toast.wall_repair_ambiguous":"The room outline has more than one possible connection. Zoom in and connect it explicitly.","toast.wall_repair_too_large":"The gap is larger than 2 cm. Connect the walls before creating the room.","toast.wall_repair_changed":"The wall geometry changed. Try creating the room again.","toast.opening_on_zero_wall":"Openings cannot sit on a zero-thickness wall","marker.from_ha_option":"Pick from the HA list","marker.show_entities":"Show entities","marker.show_entities_tip":"Adds not only devices to the list, but all their entities too","marker.pick_ph":"Choose a device…","room.open_area":"Open the HA area","view.volumetric":"Volumetric view","view.flat":"Flat view","kiosk.title":"This screen's sizes","kiosk.hint":"Stored on this device only — every wall tablet or TV can have its own comfortable sizes.","kiosk.icon_scale":"Device icon size","kiosk.font_scale":"Room card text size","editor.kiosk":"Wall device (kiosk) mode","editor.cycle":"Auto-switch spaces every N seconds (kiosk, 0 = off)","room.settings_title":"Room settings","room.settings_section":"Room settings (override the space)","room.fill_label":"Fill in THIS room","fill.inherit":"As the space","room.custom_fill_space":"Space color","room.custom_fill_own":"Room color","room.temp_src_label":"Temperature source","room.hum_src_label":"Humidity source","room.src_average":"Average over the room's sensors (default)","room.src_pick":"A specific HA device or entity","room.src_ph":"Choose a source…","toast.room_updated":"Room updated","space.card_font":"Room-card font size (whole space)","room.sizes_section":"Font sizes","room.name_scale":"Room name size","room.label_scale":"Metrics size","preview.room_name":"Living room","toast.cfg_reload_failed":"Could not reload the plan from the server: {err}","room.settings_short":"Room settings","room.unnamed":"Unnamed room","marker.use_climate_temp":"Include the device temperature in the room","marker.use_climate_temp_tip":"Adds an air conditioner or thermostat current_temperature to the room average. Configure the external value badge separately below.","marker.value_badge_title":"Value badge","marker.value_badge.help":"Shows one selected value next to the icon. It does not affect room metrics, light, Glow or the tap action.","marker.value_badge.help.aria":"Help: device value badge","marker.value_badge_enabled":"Show a value badge","marker.value_badge_source":"Value","marker.value_badge_source.help":"Choose a specific state, supported attribute, or derived value of this device.","marker.value_badge_source.help.aria":"Help: value badge source","marker.value_badge_position":"Position","marker.value_badge_position.help":"The chosen side stays fixed and does not flip automatically at a plan edge.","marker.value_badge_position.help.aria":"Help: value badge position","marker.value_badge_right":"Right","marker.value_badge_bottom":"Below","marker.value_badge_left":"Left","marker.value_badge_top":"Above","marker.value_badge_empty":"This device has no available values","marker.value_badge_static":"A static icon does not show live values. The setting is preserved.","marker.value_badge_missing":"Source unavailable","marker.value_badge_missing_hint":"The saved source is currently unavailable. Keep it, disable the badge, or choose another value.","marker.value_badge_duplicate":"The same value is already shown inside the icon.","marker.value_badge_state":"State · {name}","marker.value_badge_marker_state":"Light-source state · {name}","marker.value_badge_lqi":"Average Zigbee signal quality","marker.value_badge_attr_current_temperature":"Current temperature · {name}","marker.value_badge_attr_temperature":"Target temperature · {name}","marker.value_badge_attr_current_humidity":"Current humidity · {name}","marker.value_badge_attr_humidity":"Humidity · {name}","marker.value_badge_attr_current_position":"Position · {name}","marker.value_badge_attr_percentage":"Speed · {name}","marker.value_badge_attr_brightness":"Brightness · {name}","marker.value_badge_attr_volume_level":"Volume · {name}","marker.value_badge_attr_battery_level":"Battery · {name}","marker.value_badge_attr_fan_speed":"Fan speed · {name}","marker.light_role_label":"Is this device a light source?","marker.light_role.help":"Controls whether this marker itself is a spatial light source. Linked lights above remain independent room-light controls.","marker.light_role.help.aria":"Help: whether this device is a light source","marker.light_role_auto_yes":"Auto (light source)","marker.light_role_auto_no":"Auto (not a light source)","marker.light_role_always":"Always a light source","marker.light_role_never":"Never a light source","marker.light_entity_label":"Leading light entity","marker.light_entity.help":"For a composite device, choose the entity whose state, colour and brightness represent this plan source. The automatic fallback remains compatible with older plans.","marker.light_entity.help.aria":"Help: leading light entity","marker.light_entity_auto":"Automatic ({entity})","marker.light_entity_none":"no controllable entity","marker.light_entity_missing":"The saved entity {entity} is unavailable. House Plan temporarily uses {fallback}; the saved choice will be restored if it returns.","marker.toggle_entity_label":"Entity to toggle","marker.toggle_entity.help":"For a composite device, choose the exact own light or switch operated by a tap. Automatic keeps the previous target rules and is independent from the leading light entity.","marker.toggle_entity.help.aria":"Help: entity to toggle","marker.toggle_entity_auto":"Automatic ({entity})","marker.toggle_entity_none":"no own controllable entity","marker.toggle_entity_missing":"The saved entity {entity} is no longer among this marker’s selectable channels. House Plan temporarily uses {fallback}; the saved choice will be restored if it returns.","marker.glow_color_label":"Glow colour and brightness","marker.glow_mode.help":"Use live source values, override only its colour, or fix both colour and brightness. The minimum is 1%; to disable the source, choose “Never”.","marker.glow_mode.help.aria":"Help: glow colour and brightness","marker.glow_mode_auto":"From source","marker.glow_mode_color":"Set colour","marker.glow_mode_fixed":"Set colour and brightness","marker.glow_color":"Glow colour","marker.glow_brightness":"Brightness","marker.glow_disabled_never":"Glow settings are unavailable because this marker is explicitly not a light source.","marker.glow_disabled_auto":"Auto mode found no spatial light source for this marker.","marker.glow_disabled_no_entity":"No active controllable Home Assistant entity is available for this spatial source.","marker.glow_passive_hint":"This source has no own Home Assistant data. Set its colour and brightness manually; its radius remains available.","marker.control_broken":"Saved source is missing or no longer marked as a light","marker.control_missing_label":"Missing plan light","marker.control_passive":"passive source","toast.marker_control_cycle":"This link would create a circular chain of light controls.","toast.marker_binding_required":"Choose a Home Assistant device before linking another light source.","confirm.unlock":"Unlock “{name}”?","toast.files_migrate_failed":"Attachments could not be moved to the new binding, links keep pointing at the old files: {err}","space.pick_saved":"Already uploaded","space.pick_saved_hint":"Plans stored on the server, including ones you detached earlier","space.no_saved":"No plans stored on the server yet.","space.loading":"Loading…","space.used_by":"in use: {list}","space.in_use":"A space still uses this plan — detach it first","btn.use":"Use","confirm.delete_plan":'Delete the plan file "{name}" from the server? This cannot be undone.',"toast.plans_list_failed":"Could not list the stored plans: {err}","toast.plan_delete_failed":"Could not delete the plan: {err}","marker.hide":"Hide","marker.hide_tip":'The device will disappear from the plan after saving but will still count toward the room signal. Restore it through the "Devices" catalog in the device editor.',"marker.show":"Show","marker.show_tip":"The device will appear on the plan again after saving.","marker.hidden_ghost":"Device hidden by the user","marker.ha_disabled_device":"The device is disabled in Home Assistant and hidden from the plan.","marker.ha_disabled_entity":"The entity is disabled in Home Assistant and hidden from the plan.","marker.ha_disabled_all_entities":"The device has no active Home Assistant entities, so it is hidden from the plan.","marker.ha_registry_limited":"The full Home Assistant registry is unavailable to this user. The unverified object cannot be shown or used for now.","marker.delete_tip":"Completely delete the device from the plan and every aggregate. You can add it again later.","tap.run":"Run automation/script/scene","tap.cover":"Open/close (curtains/blinds)","marker.run_target_label":"What to run","marker.run_search_ph":"Search: automation, script or scene…","marker.run_target_gone":"Target {id} not found — pick again","marker.tap_confirm":"Ask for confirmation","marker.tap_confirm_tip":"Show a confirmation dialog before acting — a guard against accidental taps.","marker.toggle_hint_single":"Target: {name} ({id}).","marker.virtual_light_target":"Virtual light: {name}.","marker.virtual_light_current":"Manual state: {state} → {effect}.","marker.virtual_light_state_on":"on","marker.virtual_light_state_off":"off","marker.toggle_hint_group":"Will toggle {count} source(s): {names}.","marker.toggle_hint_current":"Now: {state} → {effect}.","marker.toggle_hint_group_current":"Currently on: {on} of {count} → {effect}.","marker.toggle_hint_skipped":"Will be skipped ({count}): {targets}.","marker.toggle_effect_turn_on":"will turn on","marker.toggle_effect_turn_off":"will turn off","marker.toggle_effect_open":"will open","marker.toggle_effect_close":"will close","marker.toggle_effect_stop":"will stop","marker.toggle_effect_toggle":"state will toggle","marker.toggle_skip_missing":"not found","marker.toggle_skip_ha_disabled":"disabled in HA","marker.toggle_skip_unavailable":"unavailable","marker.toggle_skip_unsupported":"toggle is unsupported","marker.toggle_skip_secure":"blocked for security","marker.toggle_none_no_binding":"This device has no binding, so there is no state to toggle. Tapping will do nothing.","marker.toggle_none_no_actionable_entity":"This device has no state that can be toggled. Tapping will do nothing.","marker.toggle_none_configured_targets_missing":"The configured targets are unavailable. The device's own entity will not be substituted for them.","marker.toggle_none_ha_disabled":"The target is disabled in Home Assistant and cannot be used on the plan.","marker.toggle_none_unavailable":"The target is currently unavailable. Tapping will do nothing.","marker.toggle_none_unsupported":"Home Assistant exposes no safe toggle operation for this entity. Tapping will do nothing.","marker.toggle_none_secure":"Toggling locks, alarms and secure gates from the plan is blocked for security.","run.automation":"automation","run.script":"script","run.scene":"scene","confirm.tap_run":'Run "{name}"?',"confirm.tap_toggle":'Toggle "{name}"?',"confirm.tap_cover":'Open/close "{name}"?',"confirm.current_state":"Current state: {state}","confirm.expected_state":"After switching: {state}","confirm.group_current":"on {on} of {total}","confirm.group_all_on":"all are on","confirm.group_all_off":"all are off","confirm.unavailable_targets":"Unavailable: {count}","confirm.expected_by_ha":"Home Assistant will determine the state","confirm.state_on":"On","confirm.state_off":"Off","confirm.state_open":"Open","confirm.state_closed":"Closed","confirm.state_opening":"Opening","confirm.state_closing":"Closing","confirm.state_stopped":"Stopped","confirm.state_unknown":"Unknown","toast.run_started":"Started: {name}","toast.run_target_missing":"Run target not found — check the device settings","toast.run_target_required":"Pick an automation, script or scene","toast.tap_target_changed":"The action target changed. Try again.","toast.virtual_light_toggle_failed":"Could not toggle the virtual light: {err}","toast.value_badge_source_required":"Choose a value for the badge","btn.run":"Run","vac.section":"Robot vacuum: live position","vac.autocal":"Set up automatically","vac.live":"Live position on the plan","vac.trail":"Show the robot's path","vac.cal_maps":"Calibrated maps: {maps}","vac.autocal_no_rooms":"The integration reports no room list — open “Fit manually”","vac.autocal_no_match":"Room names did not match (need ≥3 in common) — open “Fit manually”","vac.autocal_done":"Done: bound via {rooms} rooms. Start a cleanup and check","vac.cal_need_pos":"The robot is not reporting coordinates — start a cleanup and pause it","vac.cal_done":"Calibration saved. Start a cleanup and check","vac.cal_cancelled":"Calibration cancelled","vac.fit":"Fit manually","vac.fit_hint":"Drag the robot map into place, stretch by the corners","vac.fit_rotate":"Rotate 90°","vac.fit_mirror":"Mirror","vac.trail_never":"Never","vac.trail_cleaning":"While cleaning","vac.trail_always":"Always","gs.bg_group":"Stage background","gs.bg_color":"Background around the plan","gs.bg_default":"Theme default","gs.bg_theme":"theme default","gs.bg_mode":"Plan background","gs.bg_static":"Static color","gs.bg_daynight":"Follows the Sun","gs.bg_daynight_hint":"The background moves through dawn, day, dusk and night using sun.sun, or your local clock when sun data is unavailable. No compass is needed for the background.","gs.sun_group":"Sun","gs.sun_missing":"The sun.sun entity was not found — the background follows your local clock; window rays are unavailable.","gs.north":"North on the plan","gs.north_ph":"not set","gs.north_hint":"Set north only for window rays (1° steps, 15° with Shift); the Sun background works without it.","gs.north_clear":"Clear","gs.north_letter":"N","gs.sun_rays":"Sunlight through windows","gs.about_group":"About","gs.about_version":"Houseplan Card v{v}","gs.about_github":"GitHub · docs & issues","gs.about_telegram":"Telegram chat","space.bg_color":"Background around the plan","space.bg_inherit":"Inherit general","space.bg_inherited":"inherits general settings","space.bg_mode":"Plan background","space.north":"North on the plan (override)","space.north_inherited":"inherited: {v}","space.sun_rays":"Sunlight through windows","space.sun_inherit":"Inherit general","space.sun_on":"On","space.sun_off":"Off","canvas.far_objects":"{n} object(s) far from the plan","canvas.show_far":"Show","canvas.home_tip":"The plan is over there — click to fit it","gs.grid_group":"Plan maintenance","gs.grid_hint":"Updates data models, aligns plan elements to the grid and merges redundant wall fragments. An exact report is shown before anything is stored.","gs.align_all":"Optimize plans","gs.align_title":"Optimize plans","gs.align_none":"All plans already use the current optimized data model.","gs.optimize_no_automatic_changes":"There are no automatic changes to apply. Review the items below.","gs.align_count":"{n} of {total} elements will move, by at most {cm} cm.","gs.align_where":"The largest shift is in “{s}”.","gs.align_turned":"Openings whose angle is corrected: {n}.","gs.align_removed_drafts":"Invalid outlines collapsed by the grid and removed: {n}.","gs.optimize_redundant_drafts":"Saved wall chains hidden by solid room walls and removed: {n}.","gs.align_preflight_failed":"Could not safely verify the geometry of the following spaces: {spaces}{more}.","gs.align_preflight_hint":"Plans were not changed. Copy the diagnostics with the button below and attach them to the bug report together with a space export.","gs.preflight_reason_prepare-exception":"Could not prepare the space geometry (exception while building the model)","gs.preflight_reason_wall-null":"The wall body did not build (the union came back empty)","gs.preflight_reason_wall-degraded-extra":"The wall body degraded with extra geometry","gs.preflight_reason_wall-failed-core":"The wall body core failed to assemble","gs.preflight_reason_wall-exception":"Wall construction threw an exception","gs.preflight_reason_floor-null":"The floor outline did not build","gs.preflight_reason_floor-exception":"Floor construction threw an exception","gs.preflight_copy":"Copy diagnostics","gs.preflight_copied":"Diagnostics copied","gs.preflight_update_hint":"The card and integration versions differ — update House Plan and retry.","gs.align_preflight_space":"Space {n}","gs.align_preflight_more":", and {n} more","gs.optimize_changes":"Model migrations: {m}; spaces updated: {c}; noisy coordinate values removed: {p}; merged real-wall fragments: {w}; merged zero-thickness wall fragments: {s}; independent walls: {i}.","gs.zero_walls_migrated":"Virtual wall spans converted: {n}.","gs.wall_segments_migrated":"Wall segments stabilised: {n}.","gs.optimize_lattice_summary":"Noisy coordinate values canonicalized: {n}; maximum movement: {cm} cm.","gs.optimize_lattice_space":"{space}: coordinate values canonicalized: {n}; off-grid values left unchanged: {far}.","gs.optimize_coincident_partitions":"Hidden independent wall sections absorbed into room walls: {n}.","gs.optimize_openings_rehosted":"Openings reattached to room walls: {n}.","gs.optimize_walls_straightened":"Walls straightened: {n}; maximum movement: {cm} cm.","gs.optimize_walls_straightened_where":"Largest wall correction: {s}.","gs.optimize_walls_straighten_skipped":"Near-axis walls left unchanged because they could not be repaired safely: {n}.","gs.optimize_glow_migration":"Legacy Glow: {spaces} spaces → no data fill + independent Glow; {rooms} rooms → inherited data fill + independent Glow.","gs.optimize_references":"References repaired: spaces — {spaces}; rooms — {rooms}; positions — {positions}; devices detached from missing spaces — {detached}.","gs.optimize_reference_more":", and {n} more","gs.optimize_orphans_removed":"Forgotten records removed: {total} — room labels: {rooms}; devices: {devices}; group markers: {groups}. They belonged to spaces deleted earlier.","gs.optimize_live_positions":"Old positions in deleted spaces belong to existing objects: {n}{names}. They will be kept.","gs.optimize_live_positions_remove":"Old positions in deleted spaces belong to existing objects: {n}{names}. They are selected for removal.","gs.optimize_live_names":": {names}{more}","gs.optimize_live_remove":"Remove old positions","gs.optimize_live_keep":"Keep old positions","gs.optimize_live_selected":"Old positions will be removed after Optimize is applied.","gs.optimize_unverified":"Could not safely verify positions: {n}. They were left unchanged.","gs.optimize_registry_limited":"Full administrator access to the Home Assistant registries is required for a safe check.","gs.optimize_vacuum_warning":"Vacuum room mappings that still need review: {n}.","gs.optimize_details":"Details","gs.optimize_details_more":"And {n} more records.","gs.optimize_detail_removed":"will be removed","gs.optimize_detail_live":"will be kept","gs.optimize_detail_unverified":"not verified","gs.optimize_detail_room_label":"room label","gs.optimize_detail_device":"device","gs.optimize_detail_group":"group marker","gs.optimize_detail_unknown":"unknown owner","gs.optimize_detail_item":"{status}: {kind} {id}; old space {space}","gs.align_warn":"Elements deliberately placed between grid nodes will move. One undo is available after the operation, only until the next plan edit.","gs.align_run":"Optimize","gs.align_done":"Plans optimized: {n} elements moved, {m} records maintained, {r} references repaired","gs.optimize_undo":"Undo last optimization","gs.optimize_undone":"The last optimization was undone","decor.furniture":"Furniture","furn.title":"Furniture library","furn.symbol":"Symbol","furn.group_furniture":"Furniture","furn.group_appliance":"Appliances","furn.group_sanitary":"Plumbing","furn.group_other":"Other","furn.width":"Width","furn.depth":"Depth","furn.pick_hint":"Pick a symbol, then click on the plan.","furn.place_hint":"Click on the plan — the piece lands against the nearest wall. Shift places it free.","furn.sym_sofa":"Sofa","furn.sym_armchair":"Armchair","furn.sym_coffee_table":"Coffee table","furn.sym_table_dining":"Dining table","furn.sym_table_round":"Round table","furn.sym_chair":"Chair","furn.sym_desk":"Desk","furn.sym_bed_double":"Double bed","furn.sym_bed_single":"Single bed","furn.sym_nightstand":"Nightstand","furn.sym_wardrobe":"Wardrobe","furn.sym_bookshelf":"Bookshelf","furn.sym_fridge":"Fridge","furn.sym_stove":"Cooker","furn.sym_dishwasher":"Dishwasher","furn.sym_washer":"Washing machine","furn.sym_dryer":"Tumble dryer","furn.sym_tv":"TV","furn.sym_ac":"Air conditioner","furn.sym_water_heater":"Water heater","furn.sym_toilet":"Toilet","furn.sym_bathtub":"Bathtub","furn.sym_shower":"Shower","furn.sym_sink":"Washbasin","furn.sym_kitchen_sink":"Kitchen sink","furn.sym_bidet":"Bidet","furn.sym_stairs":"Stairs","furn.sym_fireplace":"Fireplace","furn.sym_plant":"Plant","furn.sym_rug":"Rug","common.yes":"Yes","common.no":"No","vac.diag_source":"Source","vac.diag_platform":"Integration","vac.diag_status":"Status","vac.diag_position":"Position","vac.diag_rooms":"Rooms","vac.diag_rooms_value":"{total} · {matched} names match · {readiness}","vac.autocal_ready":"auto-calibration available","vac.autocal_not_ready":"need 3 matching names","vac.diag_path":"Integration path","vac.diag_map":"Map ID","vac.source_none":"not selected","vac.source_status_ok":"Ready","vac.source_status_missing":"Missing","vac.source_status_disabled":"Disabled in Home Assistant","vac.source_status_unavailable":"Unavailable","vac.source_status_unverified":"Cannot verify with current permissions","vac.source_status_unsupported":"No position data","vac.source_status_none":"No source","vac.source_banner_missing":"The saved source no longer exists. It was not replaced automatically; choose another source or restore it in Home Assistant.","vac.source_banner_disabled":"The saved source is disabled in Home Assistant. Enable it there or choose another source.","vac.source_banner_unverified":"Current Home Assistant permissions cannot verify this saved source. It remains pinned and will not be replaced automatically.","vac.choose_source":"Choose source","vac.source_auto":"Automatic","vac.source_auto_hint":"Use a compatible entity from this device","vac.all_cameras":"All cameras","vac.all_cameras_warn":"A camera may not provide robot data. Choose one only if it is your vacuum map.","vac.all_cameras_empty":"No other camera entities found.","vac.platform_unknown":"unknown integration","vac.cap_position":"position","vac.cap_rooms_short":"rooms","vac.cap_path":"path","vac.cap_map":"map ID","vac.cap_none":"no robot data detected","vac.xcme_hint":"Enable these Xiaomi Cloud Map Extractor attributes:","vac.documentation":"Documentation","vac.residual_title":"Check automatic calibration","vac.residual_message":"The matched rooms disagree by up to {error}. Apply this approximate calibration, refine it manually, or cancel without changing the saved setup.","vac.apply_proposal":"Apply","gs.backup_group":"Backup and transfer","gs.backup_hint":"Download a portable JSON backup or preview and import a backup made by House Plan.","backup.export_open":"Export","backup.import_open":"Import","backup.export_title":"Export House Plan","backup.import_title":"Import House Plan","backup.export_hint":"Choose whether the backup should contain the whole House Plan configuration or only the current space.","backup.full":"Full backup","backup.current_space":"Current space","backup.current_space_title":"Current space: {title}","backup.no_current_space":"No current space","backup.plan_only":"Plan only","backup.plan_only_hint":"Keep rooms, walls, openings, decor and room-label positions without devices or Home Assistant bindings.","backup.plan_only_preview":"This file contains the plan only","backup.privacy_warning":"The archive keeps names, Home Assistant identifiers and exact coordinates. Internal plans and attachments are referenced, not embedded; runtime states and vacuum trails are not included.","backup.download":"Download JSON","backup.export_done":"Backup downloaded","backup.reading":"Checking the backup…","backup.revalidated":"The plan changed after this preview. The summary was refreshed; review it and confirm again.","backup.error.unauthorized":"You do not have permission to export or import this plan.","backup.error.not_ready":"House Plan is not ready yet. Try again in a moment.","backup.error.too_large":"The backup exceeds the 8 MiB limit.","backup.error.invalid_json":"The selected file is not valid JSON.","backup.error.invalid_format":"The selected file is not a House Plan backup.","backup.error.unsupported_export_version":"This backup format is not supported by the installed version.","backup.error.future_model":"The backup was created by a newer House Plan data model.","backup.error.invalid_config":"The backup contains an invalid House Plan configuration.","backup.error.wall_model_migration_blocked":"The backup wall model could not be upgraded safely. Optimize the source plan first, then export it again.","backup.error.invalid_layout":"The backup contains invalid object positions.","backup.error.invalid_content":"The backup contains invalid or inconsistent content references.","backup.error.space_not_found":"The selected space no longer exists.","backup.error.capacity_exceeded":"Adding this backup would exceed the plan limits.","backup.error.preview_expired":"The preview expired. Select the backup file again.","backup.error.preview_owner_mismatch":"This preview belongs to another Home Assistant user.","backup.error.conflict":"The plan changed after the preview. Review the refreshed summary.","backup.error.content_confirmation_required":"Confirm that unavailable local content may be detached.","backup.error.commit_failed":"The backup could not be applied safely. The previous plan was restored or is pending recovery.","backup.error.missing_plan":"A referenced plan file disappeared after the preview. Review the backup again.","backup.error.missing_content":"A referenced local attachment disappeared after the preview. Review the backup again.","backup.error.marker_control_missing":"A linked plan light is missing from the backup.","backup.error.marker_control_not_light":"A linked plan target is no longer marked as a light source.","backup.error.marker_control_self":"A light source cannot control itself.","backup.error.marker_control_cycle":"The backup contains a circular chain of light controls.","backup.error.duplicate_marker_control":"The backup contains a duplicate plan-light link.","backup.error.no_backup":"There is no import or optimization snapshot to restore.","backup.same_source":"Created on this Home Assistant instance","backup.foreign_source":"Created on another Home Assistant instance","backup.created":"Created: {value}","backup.versions":"Card {card}; integration {integration}; data model {model}","backup.count_spaces":"Spaces: {n}","backup.count_rooms":"Rooms: {n}","backup.count_walls":"Walls: {n}","backup.count_openings":"Openings: {n}","backup.count_decor":"Decor objects: {n}","backup.count_markers":"Devices: {n}","backup.count_layout":"Positions: {n}","backup.bindings":"Bindings — devices: {device}, entities: {entity}, virtual: {virtual}; positions without a space: {legacy}","backup.binding_status":"Target status — active: {active}, disabled: {disabled}, missing: {missing}","backup.missing_areas":"Areas missing on the target: {areas}","backup.dropped_marker_links":"Plan-light links outside this transferred space were omitted: {n}.","backup.repaired_target_refs":"Existing references restored by this space import: {n}.","backup.preserved_unresolved_refs":"References that could not be restored unambiguously were preserved: {n}.","backup.preserved_unresolved_hint":"No data was guessed or deleted. After the import, run Optimize plans to inspect the remaining references.","backup.import_details":"Import reference details","backup.import_detail.incoming_remapped":"References updated inside the imported copy: {n}","backup.import_detail.target_repaired":"Existing references restored: {n}","backup.import_detail.preserved_unresolved":"Unresolved references preserved: {n}","backup.import_detail.collisions":"Destination conflicts preserved safely: {n}","backup.import_detail.dropped_links":"Incoming links omitted by transfer rules: {n}","backup.import_detail.bounded_lineages":"Overly nested identifiers left bounded: {n}","backup.replace_warning":"This replaces the current configuration and layout. Uploaded files are never deleted. One undo remains available until the next plan edit.","backup.foreign_bookkeeping":"Instance-specific known/new-device bookkeeping will not be imported.","backup.final_name":"New space name","backup.target_settings":"The target instance's global settings remain unchanged.","backup.duplicates":"Bindings already present: {n}","backup.skip":"Skip duplicate devices","backup.virtual_copy":"Add safe static virtual copies","backup.content":"Referenced content","backup.content_available":"available locally","backup.content_external":"external link","backup.content_detach_required":"will be detached","backup.confirm_detach":"I understand that unavailable internal plans and attachments will be detached from the imported configuration.","backup.replace":"Replace","backup.add":"Add space","backup.space_done":"Space imported: {rooms} rooms, {markers} devices, {refs} existing references restored","backup.full_done":"Backup restored: {spaces} spaces, {rooms} rooms, {markers} devices","backup.undo_import":"Undo last full import","backup.import_undone":"Full import undone"};const aa=[{code:"en",nativeLabel:"English",dictionary:oa},{code:"ru",nativeLabel:"Русский",dictionary:{"editor.context_actions":"Действия: {object}","editor.tool_options":"Параметры инструмента: {tool}","editor.palette":"Палитра: {tool}","editor.open_group":"Группа инструментов: {group}","editor.group_active":"{group} — активно: {item}","editor.disabled_action":"{action} недоступно: {reason}","color_picker.title":"Выбор цвета","color_picker.hue":"Оттенок","color_picker.saturation":"Насыщенность","color_picker.value":"Яркость","color_picker.hex":"Цвет HEX","color_picker.invalid_hex":"Введите цвет HEX из 3 или 6 цифр","btn.properties":"Свойства","btn.keep_as_walls":"Оставить стенами","btn.delete_room_keep_walls":"Удалить комнату, оставить стены","btn.delete_room_with_walls":"Удалить комнату и стены","title.markup_select":"Выбор и редактирование стен, колонн и сохранённых контуров","title.markup_column":"Колонна: кликните точку сетки для квадратной колонны","markup.partition":"Перегородка","markup.column":"Колонна","markup.hint_column":"кликните точку сетки, чтобы поставить квадратную колонну","history.draft_segment":"Добавление сегмента черновика комнаты","history.draft_merge":"Соединение незавершённых контуров комнаты","history.draft_segment_delete":"Удаление сегмента черновика комнаты","history.partition_add":"Добавление перегородки","history.column_add":"Добавление колонны","history.physical_edit":"Изменение физического объекта","history.physical_delete":"Удаление физического объекта","history.physical_move":"Перемещение физического объекта","history.contour_to_partitions":"Преобразование контура в замкнутые стены","history.wall_chain_finish":"Завершение цепочки стен","history.wall_face_batch":"Создание комнат из стен","toast.column_duplicate":"Колонна с тем же центром и внешним размером уже существует","confirm.delete_draft":"Удалить весь незавершённый контур комнаты?","confirm.delete_draft_segment":"Удалить этот сегмент черновика? Оставшийся контур может разделиться на два.","physical.partition_properties":"Свойства перегородки","physical.column_properties":"Свойства колонны","physical.draft_properties":"Свойства сегмента черновика","physical.shape":"Форма","physical.square":"Квадрат","physical.circle":"Круг","physical.diameter":"Диаметр","physical.side":"Сторона","physical.rotation":"Угол поворота","physical.length":"Длина","physical.allowed_range":"Допустимо: {min}–{max} {unit}","physical.delete_segment":"Удалить сегмент","physical.delete_draft":"Удалить весь контур","physical.column_size_title":"Внешняя сторона квадратной колонны, создаваемой кликом (1–150 см).","card.title":"План дома","count.devices":"{n} устр.","empty.no_spaces":"Пространств пока нет.","empty.add_first":"Добавьте первое пространство и загрузите план этажа.","empty.install":"Установите интеграцию House Plan и добавьте запись в «Устройства и службы».","btn.add_space":"Добавить пространство","btn.cancel":"Отмена","btn.save":"Сохранить","btn.close":"Закрыть","btn.delete":"Удалить","btn.edit":"Редактировать","btn.open_in_ha":"Открыть в HA","btn.reset":"Сброс","btn.attach":"Прикрепить…","btn.upload":"Загрузить…","btn.replace":"Заменить…","title.zoom_in":"Приблизить","title.zoom_out":"Отдалить","title.zoom_fit":"Вписать всё","title.add_device":"Добавить устройство на план","title.show_all":"Показать скрытые и деактивированные устройства служебными маркерами (только в этой вкладке)","title.markup":"Разметка комнат: сетка, линии, контуры","title.configure_space":"Настроить пространство","title.add_space":"Добавить пространство","title.markup_add":"Стены: рисуйте непрерывную цепочку; Shift фиксирует 45°. Смена инструмента или выход завершает её как независимые стены","title.markup_merge":"Объединить: клик по одной комнате, затем по соседней с общей стеной","title.markup_split":"Разделить комнату: клик по комнате, затем две точки на её стенах","title.markup_delroom":"Удалить комнату: кликните внутри комнаты и подтвердите удаление","title.need_plan":"Загрузите подложку (план этажа)","markup.add":"Стены","markup.merge":"Объединить","markup.split":"Разделить","markup.resize":"Размер","title.markup_resize":"Изменение размера комнат: тяните доступную ручку горизонтальной или вертикальной стены","markup.hint_resize":"тяните доступную ручку стены · стена упирается в первую небезопасную позицию · Esc — отмена · Ctrl+Z — отмена шага","resize.disabled.diagonal":"Стена расположена под углом: Resize поддерживает только горизонтальные и вертикальные стены","resize.disabled.side-angle":"Примыкающая стена расположена под углом: для Resize обе примыкающие стены должны образовывать прямой угол","resize.disabled.duplicate-physical-wall":"Стену перекрывает отдельная перегородка, незавершённый контур или колонна; удалите или переместите её перед изменением размера","resize.disabled.partial-shared":"Соседняя комната использует только часть этой стены, поэтому её нельзя безопасно двигать как одну общую стену","resize.disabled.unequal-shared":"У соседней комнаты другие конечные точки или длина стены, поэтому стены нельзя безопасно двигать вместе","resize.disabled.multiple-rooms":"Перемещение затронуло бы больше двух комнат","resize.disabled.thickness-conflict":"Толщину стены нельзя безопасно сохранить","resize.disabled.opening-conflict":"Проём на этой стене нельзя безопасно переместить","resize.disabled.invalid-geometry":"Эту стену нельзя переместить без изменения структуры плана","resize.limit_stopped":"Стена остановлена: дальше нарушается ограничение стыков","resize.commit_failed":"Изменение размера отменено: итоговый план не прошёл проверку геометрии","resize.preview_failed":"Стена остановлена в последней безопасной позиции: геометрию плана нельзя сохранить без потерь","markup.opening":"Проём","markup.delete_room":"Удалить комнату","history.undo":"Отменить","history.redo":"Повторить","history.undo_named":"Отменить: {name} (Ctrl+Z)","history.redo_named":"Повторить: {name} (Ctrl+Shift+Z / Ctrl+Y)","history.undo_empty":"Нет операций для отмены","history.redo_empty":"Нет операций для повтора","history.undone":"Отменено: {name}","history.redone":"Повторено: {name}","history.add_room":"Создание комнаты","history.split_room":"Разделение комнаты","history.merge_rooms":"Объединение комнат","history.resize_room":"Изменение размера комнаты","history.wall_thickness":"Изменение толщины стены","history.add_opening":"Добавление дверного или оконного проёма","history.edit_opening":"Изменение проёма","history.move_opening":"Перемещение проёма","history.delete_opening":"Удаление проёма","history.delete_room":"Удаление комнаты","history.delete_room_keep_walls":"Удаление комнаты с сохранением стен","history.delete_room_with_walls":"Удаление комнаты и её стен","history.decor_add":"Добавление объекта декора","history.decor_edit":"Изменение объекта декора","history.decor_move":"Перемещение объекта декора","history.decor_transform":"Трансформация объекта декора","history.decor_delete":"Удаление объекта декора","history.backdrop_transform":"Трансформация подложки плана","title.markup_opening":"Проёмы: клик по стене — добавить, клик по проёму — редактировать","opening.new":"Новый проём","opening.edit":"Проём","opening.door":"Дверь","opening.window":"Окно","opening.gate":"Ворота","opening.passage":"Открытый проём","opening.passage_binding_warning":"При сохранении датчик открытия и замок будут удалены.","opening.invalid_passage_fields":"У открытого проёма на плане «{room}» есть недопустимые параметры: {fields}.","opening.type_label":"Тип","opening.length_label":"Длина, см","opening.contact_label":"Датчик открытия","opening.lock_label":"Замок","opening.none":"— нет —","opening.search_ph":"Поиск: имя или entity_id…","opening.invert":"Инвертировать открыто/закрыто","opening.flip_h":"Петли с другой стороны","opening.flip_v":"Открывается в другую сторону","opening.open":"Открыто","opening.closed":"Закрыто","opening.locked":"Заперто","opening.unlocked":"Не заперто","opening.state_unknown":"недоступно","opening.no_entities":"Датчики не привязаны — статичный символ на плане.","toast.opening_no_wall":"Кликните рядом со стеной комнаты или независимой стеной","opening.host_partition":"Независимая стена","opening.partition_orphan":"Независимая стена этого проёма больше не существует","opening.partition_jamb_margin":"Оставьте от края проёма до торца стены минимум {distance}","opening.rebind_partition":"Привязать к другой независимой стене","confirm.delete_partition_openings_title":"Удалить стену и проёмы?","confirm.delete_partition_openings_body":"В стене есть проёмы: {count}. Они будут удалены вместе со стеной.","confirm.delete_partition_openings_item":"• {type}, {length}","markup.delete":"Удалить","markup.hint_points":"точек: {n} · Shift — шаг 45° · Esc — завершить цепочку · Ctrl+Z — убрать точку · при замыкании будет предложена комната","markup.hint_start":"кликните точку сетки, чтобы начать цепочку стен","tip.lqi":"средний сигнал zigbee:","tip.area":"площадь: {value}","info.device_header":"Устройство на плане","info.model":"Модель","info.state":"Состояние","info.link":"Ссылка","info.manuals":"Инструкции","info.none":"Нет дополнительной информации","marker.new_device":"Новое устройство","marker.name_label":"Имя (отображается на плане)","marker.name_ph":"Название","marker.binding_label":"Привязка к устройству HA","marker.binding_disabled":"деактивировано в Home Assistant","marker.virtual_option":"Виртуальное устройство (без привязки)","marker.search_ph":"Поиск устройства / группы…","marker.nothing_found":"ничего не найдено","marker.room_label":"Комната","marker.room_override":" (переопределить размещение)","marker.room_choose":"— выберите комнату —","marker.room_auto":"— по зоне устройства (авто) —","marker.icon_label":"Иконка","marker.icon_ph":"mdi:… (пусто = авто)","marker.display_label":"Отображение","display.badge":"Значок + состояние","display.icon_ripple":"Значок + состояние и активность","display.static_icon":"Всегда статичный значок","marker.display_hint":"Значок + состояние меняет подложку при работе, открытии, тревоге и недоступности. Значок + состояние и активность дополнительно показывает короткую пульсацию для событий и постоянную — для работы, движения или присутствия. Значение + состояние заменяет значок одним однозначным значением HA. Статичный значок не реагирует на состояния устройства.","marker.display_hint_badge":"Значок и динамическая подложка показывают состояние устройства без обычной пульсации активности. Красная тревога сохраняется.","marker.display_hint_icon":"Значок и динамическая подложка показывают состояние устройства без обычной пульсации активности. Красная тревога сохраняется.","marker.display_hint_icon_ripple":"Значок, динамическая подложка и пульсация: короткая — для событий, постоянная — для работы, движения или присутствия. Красная тревога имеет отдельный приоритет.","marker.display_hint_value":"Значок заменяется одним однозначным значением Home Assistant; подложка продолжает показывать состояние. Красная тревога сохраняется.","marker.display_hint_static_icon":"Подложка в цветах темы и значок всегда остаются одинаковыми. Состояния, активность, недоступность и тревоги не меняют отображение.","marker.static_alarm_warning":"Это устройство может сообщать о тревогах. Статичный режим скрывает визуальную тревогу маркера.","marker.preview.title":"Предпросмотр отображения","marker.preview.actual":"Сейчас","marker.preview.example":"Пример","marker.preview.integration":"Интеграция","marker.preview.source":"Источник отображения","marker.preview.current_state":"Текущее состояние","marker.preview.result":"На плане","marker.preview.details":"Подробнее об источниках","marker.preview.select_source":"Выберите устройство или сущность Home Assistant, чтобы увидеть фактическое отображение.","marker.preview.unknown_provider":"Интеграция неизвестна","marker.preview.virtual_provider":"House Plan · виртуальное устройство","marker.preview.no_source":"Нет активного источника","marker.preview.no_state":"Нет текущего состояния","marker.preview.mixed_states":"Несколько разных состояний","marker.preview.multiple_sources":"Источников: {n}","marker.preview.more_sources":"ещё {n}","marker.preview.scaled":"Предпросмотр уменьшен до {n}%, чтобы поместиться. Сохранённый размер не изменится.","marker.preview.demo_activity":"Показать пример активности","marker.preview.demo_notice":"Временный пример активности; фактическое состояние устройства не меняется.","marker.preview.demo_short":"Показать краткую активность","marker.preview.demo_continuous":"Показать постоянную активность","marker.preview.stop_continuous":"Остановить постоянную активность","marker.preview.demo_short_notice":"Пример краткой активности; фактическое состояние устройства не меняется.","marker.preview.demo_continuous_notice":"Пример постоянной активности; фактическое состояние устройства не меняется.","marker.preview.demo_already_visible":"Текущее состояние уже показывает реальную активность или тревогу.","marker.preview.reduced_motion":"В системе включено уменьшение движения, поэтому обычная активность показана точкой.","marker.preview.reason.neutral":"Нейтральная тёмная подложка","marker.preview.reason.working":"Жёлтая подложка: устройство сейчас работает","marker.preview.reason.working_activity":"Жёлтая подложка и эффект активности: устройство сейчас работает","marker.preview.reason.open":"Оранжевая подложка: физически открыто или разблокировано","marker.preview.reason.cover_icon_state":"Нейтральная подложка; состояние и значок определяет сущность шторы или заслонки","marker.preview.reason.presence":"Эффект активности, пока обнаружено присутствие","marker.preview.reason.event":"Короткий эффект активности после события","marker.preview.reason.transition":"Эффект активности во время движения или смены состояния","marker.preview.reason.media_neutral":"Медиаустройство остаётся на нейтральной тёмной подложке, пока доступно","marker.preview.reason.unavailable":"Приглушённая нейтральная подложка: отключено или недоступно","marker.preview.reason.alarm":"Красная тревожная подложка; тревога отображается всегда","marker.preview.reason.live_states_disabled":"Оформление по живым состояниям отключено в настройках карточки","marker.preview.reason.value_no_state":"Нет подходящего значения состояния — вместо него показан значок","marker.preview.reason.value_ambiguous_sources":"Подходит несколько источников значения — вместо него показан значок","marker.preview.reason.value_non_scalar":"Источник вернул не простое значение — вместо него показан значок","marker.preview.reason.value_virtual":"У виртуального устройства нет значения HA — вместо него показан значок","marker.preview.reason.vacuum_live_plan_only":"Живая позиция и след пылесоса доступны только на полном плане","marker.preview.reason.hidden_design_preview":"Устройство скрыто на плане, но его оформление видно в предпросмотре","marker.preview.reason.composite_power_source":"Состояние берётся из сущности питания устройства; вспомогательные переключатели не учитываются","marker.preview.reason.activity_display_disabled":"Выбранный режим отображения не показывает обычные эффекты активности","marker.preview.reason.ha_disabled":"Привязка отключена в Home Assistant, поэтому устройство будет скрыто на плане","marker.preview.reason.orphaned":"Сохранённая привязка Home Assistant больше не найдена","marker.preview.reason.static_icon":"Статичный режим: состояние устройства не меняет значок","marker.activity_color":"Цвет пульсации активности","marker.ripple_size":"Размер пульсации активности","marker.activity_alarm_note":"Цвет и размер относятся только к обычной активности и не влияют на красную тревогу.","marker.pulse_a11y_alarm":"Тревога","marker.pulse_a11y_event":"Недавнее событие","marker.pulse_a11y_presence":"Обнаружено присутствие","marker.pulse_a11y_transition":"Изменение состояния","marker.pulse_a11y_running":"Работает","marker.state_a11y_neutral":"Обычное состояние","marker.state_a11y_working":"Работает","marker.state_a11y_open":"Открыто","marker.state_a11y_locked":"Заблокировано","marker.state_a11y_unlocked":"Разблокировано","marker.state_a11y_alarm":"Тревога","marker.state_a11y_unavailable":"Недоступно","marker.lqi_a11y_low":"LQI {value}, слабый сигнал","marker.lqi_a11y_mid":"LQI {value}, средний сигнал","marker.lqi_a11y_high":"LQI {value}, сильный сигнал","marker.size_label":"Размер / поворот значка","marker.angle_label":"Поворот","marker.model_label":"Модель","marker.model_ph":"напр. Aqara T&H","marker.link_label":"Ссылка","marker.desc_label":"Описание","marker.desc_ph":"Заметки, характеристики…","marker.manuals_label":"Инструкции (PDF и т.п.)","marker.sub_device":"устройство","marker.sub_z2m_group":" · Z2M-группа","marker.sub_group":"группа","marker.sub_helper":"хелпер","space.new":"Новое пространство","space.header":"Пространство","space.title_label":"Название","space.title_ph":"Например: Гараж","space.plan_label":"Подложка (план)","space.no_plan":"нет подложки","space.plan_alt":"план","room.new":"Новая комната","room.name_label":"Отображаемое имя","room.name_ph":"Например: Терраса","room.area_label":"Зона Home Assistant (свободные)","room.no_area_option":"— без зоны —","room.default_name":"Комната","device.unnamed":"без имени","device.light_group":"группа света","device.fallback":"устройство","device.virtual":"виртуальное устройство","confirm.delete_room":"Удалить комнату «{name}»?","confirm.delete_room_title":"Удалить комнату «{name}»?","confirm.delete_room_body":"Выберите, нужно ли оставить физические стены, принадлежащие только этой комнате. Общие с другой комнатой стены сохраняются всегда.","confirm.merge_rooms":"Слить комнаты «{a}» и «{b}»?","confirm.remove_marker":"Удалить «{name}» с плана? Устройство исчезнет полностью и перестанет участвовать в данных плана. Позже его можно будет добавить заново.","confirm.erase_decor":"Стереть объект «{kind}»? Действие можно отменить из истории редактора.","confirm.delete_space":"Удалить пространство «{title}» со всеми комнатами и разметкой?","space.delete_blocked":"Это пространство всё ещё используется устройствами: {n}. Сначала перенесите их в другое пространство или удалите.","toast.pos_save_failed":"Не удалось сохранить позицию: {err}","toast.no_entity":"У устройства нет подходящей сущности","toast.ha_disabled_action":"Деактивированный объект Home Assistant нельзя использовать на плане.","toast.ha_disabled_show_device":"Деактивированное в Home Assistant устройство нельзя показать на плане. Сначала активируйте его в Home Assistant.","toast.ha_disabled_show_entity":"Деактивированную в Home Assistant сущность нельзя показать на плане. Сначала активируйте её в Home Assistant.","toast.ha_disabled_add":"Деактивированный объект Home Assistant нельзя добавить на план. Сначала активируйте его в Home Assistant.","toast.ha_binding_unverified":"Статус объекта не удалось подтвердить по реестру Home Assistant. Отображение и действия временно недоступны.","toast.markup_needs_server":"Разметка доступна после переноса конфига на сервер","toast.space_order_changed":"Порядок изменён. Если где-то этаж карточки задан номером, проверьте такие панели.","toast.conflict":"Конфиг изменён в другом окне — данные обновлены, повторите последнее действие","toast.cfg_save_failed":"Не удалось сохранить конфиг: {err}","toast.room_overlap":"Контур накладывается на комнату «{name}» — комнаты не должны накладываться","toast.contour_min_edges":"Чтобы замкнуть контур, сначала нарисуйте минимум две грани","toast.contour_cannot_close":"Контур нельзя замкнуть: он вырожден или пересекает сам себя","toast.merge_not_adjacent":"Объединять можно только комнаты с общей стеной","toast.rooms_merged":"Комнаты объединены в «{name}»","toast.split_pick_wall":"Начните разрез на стене комнаты","toast.split_bad_cut":"Разрез — от стены до стены внутри комнаты, без пересечения стен и самого себя","merge.header":"Объединение комнат","merge.hint":"У объединённой комнаты одно имя и одна зона. Вторая зона освобождается — её устройства уйдут с плана, пока их не заберёт другая комната.","merge.keep":"Оставить","merge.no_area":"без зоны","room.split_header":"Новая комната после разделения","toast.room_saved":"Комната сохранена ({n}). Устройств добавлено: {added}. Обведите следующую или выйдите из разметки.","toast.room_saved_no_area":"Комната сохранена ({n}, без зоны). Обведите следующую или выйдите из разметки.","toast.marker_needs_server":"Редактирование устройств доступно после переноса конфига на сервер","toast.virtual_name_required":"Укажите имя виртуального устройства","toast.marker_saved":"Устройство сохранено","toast.marker_removed":"Устройство удалено с плана","toast.integration_missing":"Интеграция House Plan не установлена — управление недоступно","toast.plan_formats":"Поддерживаются SVG, PNG, JPG, WebP","toast.plan_required":"Загрузите подложку — план этажа обязателен","toast.space_added_onboard":"Пространство добавлено. Обведите комнаты: кликайте по точкам сетки и замкните контур.","toast.space_added":"Пространство добавлено","toast.space_saved":"Пространство сохранено","toast.space_deleted":"Пространство удалено","toast.delete_failed":"Ошибка удаления: {err}","toast.error":"Ошибка: {err}","toast.toggle_target_unavailable":"Цель «{name}» недоступна — действие не выполнено","toast.toggle_targets_unavailable":"Цели недоступны: {names}. Действие не выполнено","toast.file_failed":"Файл «{name}» не загружен: {err}","toast.files_attached":"Прикреплено файлов: {n}","err.unknown":"неизвестная ошибка","err.code":"код {code}","err.too_large":"файл больше {mb} МБ","err.bad_ext":"недопустимый тип (нужен PDF/изображение)","err.unauthorized":"нужны права администратора","editor.title":"Заголовок","editor.default_floor":"Стартовое пространство","editor.default_floor_missing":"Стартовое пространство «{id}» больше не существует. Выберите другое.","editor.floor":"Закреплённое пространство","editor.floor_none":"— не закреплять —","editor.floor_index":"YAML-индекс {index} (сохранён)","fixed_floor.loading":"Загружаем закреплённое пространство…","fixed_floor.invalid_title":"Закреплённое пространство недоступно","fixed_floor.invalid_body":"Проверьте floor в конфигурации карточки. Настроенное значение: {value}","editor.icon_size":"Размер иконок, % ширины плана","editor.show_temperature":"Показывать температуру","editor.live_states":"Живые состояния (вкл/выкл, открыто…)","editor.show_signal":"Показывать сигнал zigbee (LQI)","editor.language":"Язык интерфейса","editor.lang_auto":"Авто (профиль HA)","editor.lang_en":"English","editor.lang_ru":"Русский","title.icon_rules":"Правила иконок: какая MDI-иконка достаётся устройству по имени","rules.title":"Правила иконок","rules.hint":"Правила проверяются сверху вниз по строке «имя устройства + модель» (regex без учёта регистра); срабатывает первое совпадение. Если ничего не подошло — решает device class сущности, затем — иконка-заглушка.","rules.pattern_ph":"regex, напр. розетк|plug","rules.icon_ph":"mdi:power-socket-de","rules.add":"Добавить правило","rules.reset":"Сбросить к умолчаниям","rules.test_ph":"Проверьте имя устройства…","rules.invalid":"некорректный regex","rules.saved":"Правила иконок сохранены","btn.up":"Вверх","btn.down":"Вниз","tap.info":"Карточка устройства","tap.more_info":"Диалог HA (more-info)","tap.toggle":"Переключить состояние","marker.tap_label":"Действие по нажатию для этого устройства","tap.toggle_note":"Под селектором показано, какое состояние изменится. Защищённые устройства не переключаются с плана.","import.title":"Создать пространства из этажей HA","import.hint":"Home Assistant уже знает эти этажи. Отметьте, какие превратить в пространства плана — далее для каждого попросим картинку плана. Комнаты затем обводятся вручную по плану.","import.start":"Создать: {n}","import.manual":"Начать с нуля","import.progress":"Этаж {i} из {n}","import.done":"Пространства созданы. Обведите комнаты: кликайте по точкам сетки и замкните контур.","btn.skip":"Пропустить","space.scale_label":"Масштаб (размер клетки сетки)","space.scale_unit":"см на клетку","space.scale_unit_imperial":"дюйм на клетку","space.display_section":"Отображение","space.show_borders":"Всегда отображать границы комнат","space.zero_wall_style":"Стены нулевой толщины","space.zero_wall_dashed":"Пунктирные","space.zero_wall_solid":"Сплошные","space.zero_wall_help":"Пунктирные стены пропускают свет. Сплошные стены блокируют свет, даже при нулевой толщине.","space.show_names":"Отображать названия комнат (перетаскиваются)","space.room_color":"Цвет границ и названий","space.opacity":"Прозрачность","space.fill_label":"Заливка комнат","fill.none":"Нет","fill.lqi":"По силе зигби-сигнала","fill.light":"По освещению","fill.custom":"Свой цвет","space.custom_fill":"Цвет заливки","space.source_file":"У меня есть картинка плана","space.source_draw":"Нет подложки — нарисую комнаты вручную","space.orientation":"Холст","orient.landscape":"Альбомный","orient.portrait":"Портретный","orient.square":"Квадрат","fill.temp":"По температуре","space.temp_min":"Комфорт от","space.temp_max":"до","tip.temp_avg":"средняя температура:","tip.hum_avg":"средняя влажность:","space_card.button":"Перейти к пространству","space_card.not_found":"Пространство «{id}» не найдено","space_card.loading":"Загрузка…","continuity.restore_plan":"Восстанавливаем план…","continuity.restore_connection":"Восстанавливаем подключение к устройствам…","continuity.retry":"Повторить","editor.space":"Пространство","editor.show_button":"Показывать кнопку","editor.button_label":"Текст кнопки","editor.button_target":"Путь дашборда (куда вести)","marker.sub_entity":"сущность","title.general_settings":"Общие настройки","gs.title":"Общие настройки","gs.hint":"Цвета заливок действуют на все пространства; у каждого цвета своя прозрачность. Какой режим заливки использует пространство — задаётся в его диалоге.","gs.light_group":"Заливка: освещение","gs.light_on":"Свет включён","gs.light_off":"Весь свет выключен","gs.temp_group":"Заливка: температура","gs.temp_cold":"Холодно","gs.temp_ok":"Комфорт","gs.temp_hot":"Жарко","gs.lqi_group":"Заливка: зигби-сигнал","gs.lqi_low":"Слабый сигнал","gs.lqi_high":"Сильный сигнал","gs.reset":"Сбросить к умолчаниям","gs.saved":"Общие настройки сохранены","space.show_lqi":"Показывать зигби-сигнал (LQI) у устройств","space.hide_decor":"Скрыть декоративный слой","space.hide_decor_tip":"Линии, фигуры, надписи и мебель остаются на месте — их видно в редакторе подложки, но не на плане.","space.hide_openings":"Скрыть проёмы","space.hide_openings_tip":"Двери, окна и ворота не рисуются, но продолжают работать: свет проходит, солнце светит в окна, датчики открытия срабатывают. В редакторе плана проёмы видно всегда.","gs.light_none":"Нет источников света","mode.plan":"Редактор плана","mode.devices":"Редактор устройств","display.value":"Значение + состояние","marker.subarea":"без зоны, вручную","device.new":"Новое устройство — откройте его редактор, чтобы снять отметку","opening.unlock_action":"Открыть замок","opening.lock_action":"Закрыть замок","opening.lock_pending":"Выполняется…","title.close_editor":"Закрыть редактор (вернуться к просмотру)","devbar.add":"Добавить","devbar.show_all":"Скрытые и деактивированные","devbar.rules":"Правила иконок","device_inbox.button":"Устройства","device_inbox.title":"Устройства на плане","device_inbox.search":"Поиск по устройствам, сущностям и интеграциям…","device_inbox.add_virtual":"Добавить виртуальное устройство","device_inbox.tab_on_plan":"На плане","device_inbox.tab_available":"Доступны","device_inbox.tab_hidden":"Скрытые","device_inbox.tab_readd":"Доступны снова","device_inbox.only_new":"Только новые","device_inbox.show_entities":"Показывать сущности","device_inbox.show_hidden":"Показывать скрытые на плане","device_inbox.new":"Новое","device_inbox.find":"Найти на плане","device_inbox.edit":"Настроить","device_inbox.hide":"Скрыть","device_inbox.show":"Показать","device_inbox.add":"Добавить","device_inbox.readd":"Добавить заново","device_inbox.hide_available":"Скрыть из списка","device_inbox.show_more":"Показать ещё","device_inbox.more_actions":"Другие действия","device_inbox.find_hidden_hint":"Сначала включите «Показывать скрытые на плане»","device_inbox.show_disabled":"Перед показом активируйте эту привязку в Home Assistant","device_inbox.empty_on_plan":"На плане пока нет подходящих устройств.","device_inbox.empty_available":"Нет доступных устройств для добавления.","device_inbox.empty_hidden":"Скрытых устройств нет.","device_inbox.empty_readd":"Нет удалённых устройств, доступных для повторного добавления.","device_inbox.reason_visible_auto":"Найдено автоматически","device_inbox.reason_visible_explicit":"Добавлено явно","device_inbox.reason_manual_hidden":"Скрыто пользователем","device_inbox.reason_automatic_hidden":"Скрыто автоматически","device_inbox.reason_service_entry":"Служебная запись реестра","device_inbox.reason_excluded_integration":"Интеграция исключена фильтрами устройств","device_inbox.reason_excluded_domain":"Непространственный тип сущности","device_inbox.reason_grouped_light":"Представлено группой света комнаты","device_inbox.reason_represented_by_parent":"Представлено родительским устройством","device_inbox.reason_removed":"Ранее удалено с плана","device_inbox.reason_available":"Можно добавить на план","device_inbox.reason_no_bound_room":"Зона HA не связана с комнатой плана","device_inbox.status_ha_disabled":"Отключено в Home Assistant","device_inbox.status_orphaned":"Привязка больше не найдена в Home Assistant","device_inbox.status_unverified":"Реестр Home Assistant временно недоступен","device_inbox.saved":"Список устройств обновлён","space.roomcard_section":"В карточке комнаты:","space.label_temp":"Температура","space.label_hum":"Влажность","space.label_lqi":"Средний Zigbee-сигнал","space.label_light":"Свет вкл/выкл","roomcard.light_on":"Вкл","roomcard.light_off":"Выкл","roomcard.light_partial":"{on} из {total}","toast.split_pick_inside":"Промежуточные точки разреза — внутри комнаты","mode.decor":"Редактор подложки","decor.select":"Выбрать","decor.line":"Линия","decor.rect":"Прямоугольник","decor.ellipse":"Овал","decor.text":"Надпись","decor.erase":"Стереть","decor.erase_confirm_title":"Стереть объект?","decor.color":"Цвет","decor.width":"Толщина линии","decor.line_style":"Стиль линии","decor.line_style_solid":"Сплошная","decor.line_style_dashed":"Пунктирная","decor.fill":"Заливка","decor.fill_color":"Цвет заливки","decor.length":"Длина","decor.size":"Размер","decor.angle":"Угол поворота, °","decor.text_size":"Размер текста","decor.backdrop_properties":"Свойства картинки-подложки","decor.text_title":"Надпись","decor.object_title":"Редактирование: {kind}","decor.text_label":"Текст","decor.live_group":"Вставить переменную HA","decor.live_entity":"Сущность","decor.live_entity_ph":"выберите сущность","decor.live_attr":"Значение","decor.live_attr_ph":"выберите состояние или атрибут","decor.live_state":"Состояние","decor.backdrop":"Картинка-подложка","decor.backdrop_hint":"Тяните картинку — перемещение; угол — размер; верхнюю ручку — поворот. Shift меняет пропорции или снимает шаг угла.","decor.backdrop_reset":"Вернуть картинку","decor.backdrop_reset_done":"Картинка вернулась на своё место и в свой размер","marker.icon_auto":"Авто: {icon} (по правилам иконок; выберите свою, чтобы заменить)","marker.icon_pin_auto":"Закрепить","mode.plan_tip":"Редактор плана — геометрия дома: рисование и объединение/разделение комнат, привязка к зонам HA, двери, окна и ворота, карточки комнат, масштаб","mode.devices_tip":"Редактор устройств — всё про значки: перетаскивание, клик — настройка привязки/иконки/отображения, виртуальные устройства, правила иконок","mode.decor_tip":"Редактор подложки — чисто визуальный декор под планом: линии, прямоугольники, овалы и надписи, не реагирующие на клики","space.glow_enabled":"Свечение источников света","gs.glow_group":"Свечение источников света","gs.glow_base":"Темнота дома","gs.glow_light":"Цвет света по умолчанию / интенсивность","gs.wall_group":"Стены","gs.wall_fill":"Цвет заливки стен","gs.glow_radius":"Радиус свечения","gs.unit_m":"м","gs.unit_ft":"фут","marker.controls_label":"Управляет другими источниками света","marker.controls_hint":"При действии «Переключить» клик разом переключает все добавленные здесь источники. Собственная сущность маркера управляется напрямую и сюда не добавляется; чтобы считать её светом, включите флаг ниже.","marker.controls_filter":"Поиск ламп и выключателей…","info.controls":"Управляет","marker.glow_radius_label":"Радиус свечения","marker.glow_radius.help":"Задаёт радиус свечения в метрах или футах; пустое значение использует радиус из общих настроек.","marker.glow_radius.help.aria":"Подсказка: радиус свечения","markup.wallthick":"Толщина","markup.select":"Выбрать","title.markup_wallthick":"Толщина — клик по стене задаёт толщину от 0 до 100 см.","markup.hint_wallthick":"клик по стене · Esc закрывает без применения","wallthick.field":"Толщина","wallthick.unit_cm":"см","wallthick.unit_in":"дюйм","wallthick.apply_room":"Применить ко всем стенам комнаты","markup.draw_wall_title":"Толщина каждого нового сегмента стены (0–100 см). Общие стены сохраняют существующее значение.","room.queue_progress":"Комната {current} из {total}","toast.wall_rooms_saved":"Создано комнат: {n}","toast.wall_chain_saved":"Цепочка стен сохранена","toast.wallthick_pick":"Кликните по стене","toast.wallthick_set":"Толщина стены задана","toast.wallthick_cleared":"Толщина стены убрана","toast.physical_range":"Введите значение от {min} до {max} {unit}","toast.zero_wall_opening_conflict":"Сначала удалите проём на этом участке стены.","toast.zero_wall_ambiguous":"Не удалось однозначно выбрать участок стены. Уточните геометрию узла.","toast.zero_wall_migration_blocked":"Пространство не преобразовано: {reason}. Данные не изменены.","toast.physical_angle":"Введите угол поворота от 0° до 90°, не включая 90°","toast.physical_limit":"В пространстве достигнут лимит объектов этого типа","toast.geometry_unsafe":"Изменение отменено: геометрию стен нельзя безопасно построить.","junction.limit_angle":"Слишком острый угол между стенами: {actual}°, минимум {limit}°.","junction.limit_valence":"Слишком много стен в одном узле: {actual}, максимум {limit}.","junction.limit_length":"Слишком короткий участок стены: {actual} см, минимум {limit} см.","junction.limit_distance":"Стены и узлы слишком близко: {actual} см, минимум {limit} см.","junction.limit_clearance":"Внутри комнаты не остаётся места: {actual} см², минимум {limit} см².","junction.limit_check_failed":"Проверку стыков выполнить не удалось — изменение не сохранено","toast.wall_model_migration_blocked":"Не удалось обновить модель стен: {reason}. План не изменён. Запустите «Оптимизировать планы»; если ошибка повторится, исправьте конфликтующую геометрию стен.","toast.wall_model_client_outdated":"Обновите карточку и перезагрузите страницу перед редактированием плана.","wall_model.reason.invalid-room":"некорректный контур комнаты","wall_model.reason.zero-length":"отрезок стены нулевой длины","wall_model.reason.third-owner":"стена принадлежит более чем двум комнатам","wall_model.reason.duplicate-id":"конфликт идентификаторов стен","wall_model.reason.thickness-conflict":"конфликт значений толщины стены","wall_model.reason.opening-host":"проём нельзя однозначно привязать к стене","toast.delete_room_pick":"Кликните внутри комнаты, которую нужно удалить","toast.plan_snap_ambiguous":"Увеличьте масштаб, чтобы выбрать узел стены","toast.wall_repair_ambiguous":"У контура несколько возможных соединений. Увеличьте масштаб и соедините стены явно.","toast.wall_repair_too_large":"Разрыв больше 2 см. Соедините стены перед созданием комнаты.","toast.wall_repair_changed":"Геометрия стен изменилась. Попробуйте создать комнату ещё раз.","toast.opening_on_zero_wall":"Проёмы на стене нулевой толщины запрещены","marker.from_ha_option":"Выбрать из списка HA","marker.show_entities":"Показывать сущности","marker.show_entities_tip":"Добавляет в список не только устройства, но и все их сущности","marker.pick_ph":"Выберите устройство…","room.open_area":"Открыть зону в HA","view.volumetric":"Объёмный вид","view.flat":"Плоский вид","kiosk.title":"Размеры на этом экране","kiosk.hint":"Хранится только на этом устройстве — у каждого настенного планшета или ТВ свои удобные размеры.","kiosk.icon_scale":"Размер значков устройств","kiosk.font_scale":"Размер текста карточек комнат","editor.kiosk":"Режим настенного устройства (киоск)","editor.cycle":"Автосмена пространств каждые N секунд (киоск, 0 = выкл)","room.settings_title":"Настройки комнаты","room.settings_section":"Настройки комнаты (переопределяют пространство)","room.fill_label":"Заливка в ЭТОЙ комнате","fill.inherit":"Как у пространства","room.custom_fill_space":"Цвет пространства","room.custom_fill_own":"Цвет комнаты","room.temp_src_label":"Источник температуры","room.hum_src_label":"Источник влажности","room.src_average":"Средняя по датчикам комнаты (по умолчанию)","room.src_pick":"Конкретное устройство или сущность HA","room.src_ph":"Выберите источник…","toast.room_updated":"Комната обновлена","space.card_font":"Размер шрифта карточек комнат (всё пространство)","room.sizes_section":"Размеры шрифтов","room.name_scale":"Размер названия","room.label_scale":"Размер подписей","preview.room_name":"Гостиная","toast.cfg_reload_failed":"Не удалось перечитать план с сервера: {err}","room.settings_short":"Настройки комнаты","room.unnamed":"Комната без имени","marker.use_climate_temp":"Учитывать температуру устройства в комнате","marker.use_climate_temp_tip":"Добавляет current_temperature кондиционера или термостата в среднюю температуру комнаты. Внешний бейдж со значением настраивается отдельно ниже.","marker.value_badge_title":"Бейдж со значением","marker.value_badge.help":"Показывает одно выбранное значение рядом со значком. Не влияет на комнатные показатели, свет, Glow и действие по нажатию.","marker.value_badge.help.aria":"Подсказка: бейдж со значением устройства","marker.value_badge_enabled":"Отображать бейдж со значением","marker.value_badge_source":"Значение","marker.value_badge_source.help":"Выберите конкретное состояние, поддерживаемый атрибут или производное значение этого устройства.","marker.value_badge_source.help.aria":"Подсказка: источник значения бейджа","marker.value_badge_position":"Расположение","marker.value_badge_position.help":"Выбранная сторона сохраняется постоянно и не переворачивается автоматически у края плана.","marker.value_badge_position.help.aria":"Подсказка: расположение бейджа","marker.value_badge_right":"Справа","marker.value_badge_bottom":"Снизу","marker.value_badge_left":"Слева","marker.value_badge_top":"Сверху","marker.value_badge_empty":"У этого устройства нет доступных значений","marker.value_badge_static":"Статичный значок не показывает живые значения. Настройка сохранится.","marker.value_badge_missing":"Источник недоступен","marker.value_badge_missing_hint":"Сохранённый источник сейчас недоступен. Можно сохранить его, отключить бейдж или выбрать другой.","marker.value_badge_duplicate":"То же значение уже показано внутри значка.","marker.value_badge_state":"Состояние · {name}","marker.value_badge_marker_state":"Состояние источника света · {name}","marker.value_badge_lqi":"Качество Zigbee-сигнала (среднее)","marker.value_badge_attr_current_temperature":"Текущая температура · {name}","marker.value_badge_attr_temperature":"Целевая температура · {name}","marker.value_badge_attr_current_humidity":"Текущая влажность · {name}","marker.value_badge_attr_humidity":"Влажность · {name}","marker.value_badge_attr_current_position":"Положение · {name}","marker.value_badge_attr_percentage":"Скорость · {name}","marker.value_badge_attr_brightness":"Яркость · {name}","marker.value_badge_attr_volume_level":"Громкость · {name}","marker.value_badge_attr_battery_level":"Заряд батареи · {name}","marker.value_badge_attr_fan_speed":"Скорость вентилятора · {name}","marker.light_role_label":"Является источником света","marker.light_role.help":"Определяет, является ли сам маркер пространственным источником света. Привязанные выше лампы остаются независимыми источниками для комнаты.","marker.light_role.help.aria":"Подсказка: является ли устройство источником света","marker.light_role_auto_yes":"Автоматически (источник света)","marker.light_role_auto_no":"Автоматически (не источник света)","marker.light_role_always":"Всегда источник света","marker.light_role_never":"Никогда не источник света","marker.light_entity_label":"Ведущая сущность света","marker.light_entity.help":"Для составного устройства выберите сущность, состояние, цвет и яркость которой представляют этот источник на плане. Автоматический вариант сохраняет совместимость со старыми планами.","marker.light_entity.help.aria":"Подсказка: ведущая сущность света","marker.light_entity_auto":"Автоматически ({entity})","marker.light_entity_none":"нет управляемой сущности","marker.light_entity_missing":"Сохранённая сущность {entity} недоступна. Временно используется {fallback}; выбор восстановится, если сущность вернётся.","marker.toggle_entity_label":"Переключаемая сущность","marker.toggle_entity.help":"Для составного устройства выберите собственную лампу или переключатель, которой управляет нажатие. Автоматический вариант сохраняет прежние правила цели и не зависит от ведущей сущности света.","marker.toggle_entity.help.aria":"Подсказка: переключаемая сущность","marker.toggle_entity_auto":"Автоматически ({entity})","marker.toggle_entity_none":"нет собственной управляемой сущности","marker.toggle_entity_missing":"Сохранённой сущности {entity} больше нет среди выбираемых каналов этого маркера. Временно используется {fallback}; выбор восстановится, если сущность вернётся.","marker.glow_color_label":"Цвет и яркость свечения","marker.glow_mode.help":"Используйте данные источника, задайте только цвет или зафиксируйте цвет и яркость. Минимум — 1%; чтобы отключить источник, выберите «Никогда».","marker.glow_mode.help.aria":"Подсказка: цвет и яркость свечения","marker.glow_mode_auto":"Из источника","marker.glow_mode_color":"Задать цвет","marker.glow_mode_fixed":"Задать цвет и яркость","marker.glow_color":"Цвет свечения","marker.glow_brightness":"Яркость","marker.glow_disabled_never":"Настройки свечения недоступны: маркер явно исключён из источников света.","marker.glow_disabled_auto":"В автоматическом режиме у этого маркера не найден пространственный источник света.","marker.glow_disabled_no_entity":"Для пространственного источника нет активной управляемой сущности Home Assistant.","marker.glow_passive_hint":"У источника нет собственных данных Home Assistant. Задайте цвет и яркость вручную; радиус остаётся доступным.","marker.control_broken":"Сохранённый источник отсутствует или больше не отмечен как источник света","marker.control_missing_label":"Отсутствующий источник на плане","marker.control_passive":"пассивный источник","toast.marker_control_cycle":"Такая связь создаст циклическую цепочку управления светом.","toast.marker_binding_required":"Сначала выберите устройство Home Assistant, затем добавьте управляемый источник света.","confirm.unlock":"Открыть замок «{name}»?","toast.files_migrate_failed":"Не удалось перенести вложения к новой привязке, ссылки остались на старые файлы: {err}","space.pick_saved":"Уже загруженные","space.pick_saved_hint":"Планы, сохранённые на сервере, включая отцеплённые ранее","space.no_saved":"На сервере пока нет сохранённых планов.","space.loading":"Загрузка…","space.used_by":"используется: {list}","space.in_use":"План используется пространством — сначала отцепите его","btn.use":"Выбрать","confirm.delete_plan":"Удалить файл плана «{name}» с сервера? Действие необратимо.","toast.plans_list_failed":"Не удалось получить список планов: {err}","toast.plan_delete_failed":"Не удалось удалить план: {err}","marker.hide":"Скрыть","marker.hide_tip":"Устройство исчезнет с плана после сохранения, но продолжит участвовать в расчёте сигнала комнаты. Вернуть его можно через каталог «Устройства» в редакторе устройств.","marker.show":"Показать","marker.show_tip":"Устройство снова появится на плане после сохранения.","marker.hidden_ghost":"Устройство скрыто пользователем","marker.ha_disabled_device":"Устройство деактивировано в Home Assistant и скрыто с плана.","marker.ha_disabled_entity":"Сущность деактивирована в Home Assistant и скрыта с плана.","marker.ha_disabled_all_entities":"У устройства нет активных сущностей Home Assistant, поэтому оно скрыто с плана.","marker.ha_registry_limited":"Полный реестр Home Assistant недоступен этому пользователю. Неподтверждённый объект временно нельзя показывать или использовать.","marker.delete_tip":"Полностью удалить устройство с плана и из всех расчётов. Позже его можно добавить заново.","tap.run":"Запустить автоматизацию/скрипт/сцену","tap.cover":"Открыть/закрыть (шторы/жалюзи)","marker.run_target_label":"Что запускать","marker.run_search_ph":"Поиск: автоматизация, скрипт или сцена…","marker.run_target_gone":"Цель {id} не найдена — выберите заново","marker.tap_confirm":"Спрашивать подтверждение","marker.tap_confirm_tip":"Перед выполнением показать диалог подтверждения — защита от случайных нажатий.","marker.toggle_hint_single":"Цель: {name} ({id}).","marker.virtual_light_target":"Виртуальный свет: {name}.","marker.virtual_light_current":"Ручное состояние: {state} → {effect}.","marker.virtual_light_state_on":"включён","marker.virtual_light_state_off":"выключен","marker.toggle_hint_group":"Переключатся источники ({count}): {names}.","marker.toggle_hint_current":"Сейчас: {state} → {effect}.","marker.toggle_hint_group_current":"Сейчас включено {on} из {count} → {effect}.","marker.toggle_hint_skipped":"Не будут затронуты ({count}): {targets}.","marker.toggle_effect_turn_on":"включится","marker.toggle_effect_turn_off":"выключится","marker.toggle_effect_open":"откроется","marker.toggle_effect_close":"закроется","marker.toggle_effect_stop":"остановится","marker.toggle_effect_toggle":"состояние переключится","marker.toggle_skip_missing":"не найдено","marker.toggle_skip_ha_disabled":"деактивировано в HA","marker.toggle_skip_unavailable":"недоступно","marker.toggle_skip_unsupported":"переключение не поддерживается","marker.toggle_skip_secure":"заблокировано из соображений безопасности","marker.toggle_none_no_binding":"У устройства нет привязки, состояние переключать нечем. По нажатию ничего не произойдёт.","marker.toggle_none_no_actionable_entity":"У устройства нет состояния, которое можно переключить. По нажатию ничего не произойдёт.","marker.toggle_none_configured_targets_missing":"Настроенные цели сейчас недоступны. Собственная сущность устройства не будет подставлена вместо них.","marker.toggle_none_ha_disabled":"Цель деактивирована в Home Assistant и не может использоваться на плане.","marker.toggle_none_unavailable":"Цель сейчас недоступна. По нажатию ничего не произойдёт.","marker.toggle_none_unsupported":"Для этой сущности Home Assistant не предоставляет безопасного переключения. По нажатию ничего не произойдёт.","marker.toggle_none_secure":"Переключение замков, сигнализации и защитных ворот с плана заблокировано из соображений безопасности.","run.automation":"автоматизация","run.script":"скрипт","run.scene":"сцена","confirm.tap_run":"Запустить «{name}»?","confirm.tap_toggle":"Переключить «{name}»?","confirm.tap_cover":"Открыть/закрыть «{name}»?","confirm.current_state":"Текущее состояние: {state}","confirm.expected_state":"После переключения: {state}","confirm.group_current":"включено {on} из {total}","confirm.group_all_on":"все включены","confirm.group_all_off":"все выключены","confirm.unavailable_targets":"Недоступно: {count}","confirm.expected_by_ha":"Состояние определит Home Assistant","confirm.state_on":"Включено","confirm.state_off":"Выключено","confirm.state_open":"Открыто","confirm.state_closed":"Закрыто","confirm.state_opening":"Открывается","confirm.state_closing":"Закрывается","confirm.state_stopped":"Остановлено","confirm.state_unknown":"Неизвестно","toast.run_started":"Запущено: {name}","toast.run_target_missing":"Цель запуска не найдена — проверьте настройки устройства","toast.run_target_required":"Выберите автоматизацию, скрипт или сцену","toast.tap_target_changed":"Цель действия изменилась. Повторите попытку.","toast.virtual_light_toggle_failed":"Не удалось переключить виртуальный свет: {err}","toast.value_badge_source_required":"Выберите значение для бейджа","btn.run":"Выполнить","vac.section":"Робот-пылесос: живая позиция","vac.autocal":"Настроить автоматически","vac.live":"Живая позиция на плане","vac.trail":"Показывать путь робота","vac.cal_maps":"Откалиброваны карты: {maps}","vac.autocal_no_rooms":"Интеграция не отдаёт список комнат — откройте «Подогнать вручную»","vac.autocal_no_match":"Не совпали имена комнат (нужно ≥3 общих) — откройте «Подогнать вручную»","vac.autocal_done":"Готово: привязка по {rooms} комнатам. Запустите уборку и проверьте","vac.cal_need_pos":"Робот сейчас не отдаёт координаты — запустите уборку и поставьте на паузу","vac.cal_done":"Калибровка сохранена. Запустите уборку и проверьте","vac.cal_cancelled":"Калибровка отменена","vac.fit":"Подогнать вручную","vac.fit_hint":"Перетащите карту робота на место, растяните за уголки","vac.fit_rotate":"Повернуть 90°","vac.fit_mirror":"Отразить","vac.trail_never":"Не показывать никогда","vac.trail_cleaning":"Во время уборки","vac.trail_always":"Показывать всегда","gs.bg_group":"Фон сцены","gs.bg_color":"Цвет фона вокруг плана","gs.bg_default":"Как в теме","gs.bg_theme":"по умолчанию — из темы","gs.bg_mode":"Фон плана","gs.bg_static":"Статичный цвет","gs.bg_daynight":"Следует за Солнцем","gs.bg_daynight_hint":"Фон проходит рассвет, день, закат и ночь по данным sun.sun, а при их недоступности — по локальным часам. Компас для фона не нужен.","gs.sun_group":"Солнце","gs.sun_missing":"Сущность sun.sun не найдена — фон следует локальным часам, а оконные лучи недоступны.","gs.north":"Север на плане","gs.north_ph":"не задан","gs.north_hint":"Север нужен только для оконных лучей (шаг 1°, с Shift 15°); солнечный фон работает без него.","gs.north_clear":"Сбросить","gs.north_letter":"С","gs.sun_rays":"Солнце в окнах","gs.about_group":"О карточке","gs.about_version":"Houseplan Card v{v}","gs.about_github":"GitHub · документация и issues","gs.about_telegram":"Чат в Telegram","space.bg_color":"Цвет фона вокруг плана","space.bg_inherit":"Наследовать общий","space.bg_inherited":"наследуется из общих настроек","space.bg_mode":"Фон плана","space.north":"Север на плане (переопределение)","space.north_inherited":"наследуется: {v}","space.sun_rays":"Солнце в окнах","space.sun_inherit":"Наследовать общий","space.sun_on":"Вкл","space.sun_off":"Выкл","canvas.far_objects":"Объектов далеко от плана: {n}","canvas.show_far":"Показать","canvas.home_tip":"План там — нажмите, чтобы вписать","gs.grid_group":"Обслуживание планов","gs.grid_hint":"Обновляет модели данных, выравнивает элементы по сетке и объединяет лишние фрагменты стен. Перед записью будет показан точный отчёт.","gs.align_all":"Оптимизировать планы","gs.align_title":"Оптимизировать планы","gs.align_none":"Все планы уже используют актуальную и оптимальную модель данных.","gs.optimize_no_automatic_changes":"Автоматических изменений нет. Проверьте пункты ниже.","gs.align_count":"Сдвинется элементов: {n} из {total}, максимум на {cm} см.","gs.align_where":"Наибольший сдвиг — в пространстве «{s}».","gs.align_turned":"Проёмов с исправлением угла: {n}.","gs.align_removed_drafts":"Схлопнувшиеся на сетке некорректные контуры удалены: {n}.","gs.optimize_redundant_drafts":"Сохранённые цепочки стен, полностью скрытые стенами комнат, удалены: {n}.","gs.align_preflight_failed":"Не удалось безопасно проверить геометрию следующих пространств: {spaces}{more}.","gs.align_preflight_hint":"Планы не изменены. Скопируйте диагностику кнопкой ниже и приложите её к отчёту об ошибке вместе с экспортом пространства.","gs.preflight_reason_prepare-exception":"Не удалось подготовить геометрию пространства (исключение при сборке модели)","gs.preflight_reason_wall-null":"Тело стен не построилось (объединение вернуло пустоту)","gs.preflight_reason_wall-degraded-extra":"Тело стен деградировало с лишней геометрией","gs.preflight_reason_wall-failed-core":"Ядро тела стен не собралось","gs.preflight_reason_wall-exception":"Построение стен упало с исключением","gs.preflight_reason_floor-null":"Контур пола не построился","gs.preflight_reason_floor-exception":"Построение пола упало с исключением","gs.preflight_copy":"Скопировать диагностику","gs.preflight_copied":"Диагностика скопирована","gs.preflight_update_hint":"Версии карточки и интеграции различаются — обновите House Plan и повторите.","gs.align_preflight_space":"Пространство {n}","gs.align_preflight_more":" и ещё {n}","gs.optimize_changes":"Миграций модели: {m}; обновлено пространств: {c}; устранён шум координат: {p}; объединено отрезков реальных стен: {w}; объединено отрезков стен нулевой толщины: {s}; независимых: {i}.","gs.zero_walls_migrated":"Преобразовано виртуальных участков: {n}.","gs.wall_segments_migrated":"Стабилизировано сегментов стен: {n}.","gs.optimize_lattice_summary":"Канонизировано шумовых значений координат: {n}; максимальный сдвиг: {cm} см.","gs.optimize_lattice_space":"{space}: канонизировано значений координат: {n}; оставлено значений вне сетки: {far}.","gs.optimize_coincident_partitions":"Скрытые участки независимых стен поглощены стенами комнат: {n}.","gs.optimize_openings_rehosted":"Проёмы перепривязаны к стенам комнат: {n}.","gs.optimize_walls_straightened":"Выпрямлено стен: {n}; максимальное перемещение: {cm} см.","gs.optimize_walls_straightened_where":"Максимальная правка стены: {s}.","gs.optimize_walls_straighten_skipped":"Оставлено почти осевых стен, которые нельзя безопасно выпрямить: {n}.","gs.optimize_glow_migration":"Старый Glow: пространств — {spaces} → без заливки данных + независимый Glow; комнат — {rooms} → наследуемая заливка + независимый Glow.","gs.optimize_references":"Исправлено ссылок: пространства — {spaces}; комнаты — {rooms}; позиции — {positions}; устройств отвязано от отсутствующих пространств — {detached}.","gs.optimize_reference_more":", и ещё {n}","gs.optimize_orphans_removed":"Убрано забытых записей: {total} — подписи комнат: {rooms}; устройства: {devices}; групповые метки: {groups}. Все они принадлежали пространствам, удалённым ранее.","gs.optimize_live_positions":"Старые позиции в удалённых пространствах принадлежат существующим объектам: {n}{names}. Они будут сохранены.","gs.optimize_live_positions_remove":"Старые позиции в удалённых пространствах принадлежат существующим объектам: {n}{names}. Они выбраны для удаления.","gs.optimize_live_names":": {names}{more}","gs.optimize_live_remove":"Убрать старые позиции","gs.optimize_live_keep":"Сохранить старые позиции","gs.optimize_live_selected":"Старые позиции будут убраны после применения оптимизации.","gs.optimize_unverified":"Не удалось безопасно проверить позиций: {n}. Они оставлены без изменений.","gs.optimize_registry_limited":"Для безопасной проверки нужен полный доступ администратора к реестрам Home Assistant.","gs.optimize_vacuum_warning":"Сопоставления комнат пылесоса требуют проверки: {n}.","gs.optimize_details":"Подробности","gs.optimize_details_more":"И ещё записей: {n}.","gs.optimize_detail_removed":"будет удалено","gs.optimize_detail_live":"будет сохранено","gs.optimize_detail_unverified":"не проверено","gs.optimize_detail_room_label":"подпись комнаты","gs.optimize_detail_device":"устройство","gs.optimize_detail_group":"групповая метка","gs.optimize_detail_unknown":"неизвестный владелец","gs.optimize_detail_item":"{status}: {kind} {id}; прежнее пространство {space}","gs.align_warn":"Элементы, намеренно поставленные между узлами, будут сдвинуты. После операции доступна одна отмена — только до следующего изменения плана.","gs.align_run":"Оптимизировать","gs.align_done":"Планы оптимизированы: сдвинуто элементов — {n}, обслужено записей — {m}, исправлено ссылок — {r}","gs.optimize_undo":"Отменить последнюю оптимизацию","gs.optimize_undone":"Последняя оптимизация отменена","decor.furniture":"Мебель","furn.title":"Библиотека мебели","furn.symbol":"Символ","furn.group_furniture":"Мебель","furn.group_appliance":"Техника","furn.group_sanitary":"Сантехника","furn.group_other":"Прочее","furn.width":"Ширина","furn.depth":"Глубина","furn.pick_hint":"Выберите символ и кликните по плану.","furn.place_hint":"Кликните по плану — предмет встанет к ближайшей стене. Shift — свободно.","furn.sym_sofa":"Диван","furn.sym_armchair":"Кресло","furn.sym_coffee_table":"Журнальный столик","furn.sym_table_dining":"Обеденный стол","furn.sym_table_round":"Круглый стол","furn.sym_chair":"Стул","furn.sym_desk":"Письменный стол","furn.sym_bed_double":"Двуспальная кровать","furn.sym_bed_single":"Односпальная кровать","furn.sym_nightstand":"Тумбочка","furn.sym_wardrobe":"Шкаф","furn.sym_bookshelf":"Стеллаж","furn.sym_fridge":"Холодильник","furn.sym_stove":"Плита","furn.sym_dishwasher":"Посудомоечная машина","furn.sym_washer":"Стиральная машина","furn.sym_dryer":"Сушильная машина","furn.sym_tv":"Телевизор","furn.sym_ac":"Кондиционер","furn.sym_water_heater":"Бойлер","furn.sym_toilet":"Унитаз","furn.sym_bathtub":"Ванна","furn.sym_shower":"Душ","furn.sym_sink":"Раковина","furn.sym_kitchen_sink":"Кухонная мойка","furn.sym_bidet":"Биде","furn.sym_stairs":"Лестница","furn.sym_fireplace":"Камин","furn.sym_plant":"Растение","furn.sym_rug":"Ковёр","common.yes":"Да","common.no":"Нет","vac.diag_source":"Источник","vac.diag_platform":"Интеграция","vac.diag_status":"Статус","vac.diag_position":"Позиция","vac.diag_rooms":"Комнаты","vac.diag_rooms_value":"{total} · совпало имён: {matched} · {readiness}","vac.autocal_ready":"автокалибровка доступна","vac.autocal_not_ready":"нужно 3 совпавших имени","vac.diag_path":"Путь интеграции","vac.diag_map":"ID карты","vac.source_none":"не выбран","vac.source_status_ok":"Готов","vac.source_status_missing":"Не найден","vac.source_status_disabled":"Деактивирован в Home Assistant","vac.source_status_unavailable":"Недоступен","vac.source_status_unverified":"Нельзя проверить с текущими правами","vac.source_status_unsupported":"Нет данных позиции","vac.source_status_none":"Нет источника","vac.source_banner_missing":"Сохранённый источник больше не существует. Он не заменён автоматически: выберите другой или восстановите его в Home Assistant.","vac.source_banner_disabled":"Сохранённый источник деактивирован в Home Assistant. Активируйте его там или выберите другой.","vac.source_banner_unverified":"Текущих прав Home Assistant недостаточно для проверки источника. Привязка сохранена и не будет заменена автоматически.","vac.choose_source":"Выбрать источник","vac.source_auto":"Автоматически","vac.source_auto_hint":"Использовать совместимую сущность этого устройства","vac.all_cameras":"Все камеры","vac.all_cameras_warn":"Камера может не отдавать данные робота. Выбирайте её только если это карта вашего пылесоса.","vac.all_cameras_empty":"Других сущностей камеры не найдено.","vac.platform_unknown":"интеграция неизвестна","vac.cap_position":"позиция","vac.cap_rooms_short":"комнаты","vac.cap_path":"путь","vac.cap_map":"ID карты","vac.cap_none":"данные робота не обнаружены","vac.xcme_hint":"Включите атрибуты Xiaomi Cloud Map Extractor:","vac.documentation":"Документация","vac.residual_title":"Проверьте автокалибровку","vac.residual_message":"Совпавшие комнаты расходятся максимум на {error}. Примените приблизительную калибровку, подгоните её вручную либо отмените без изменения сохранённых настроек.","vac.apply_proposal":"Применить","gs.backup_group":"Резервная копия и перенос","gs.backup_hint":"Скачайте переносимую JSON-копию или проверьте и импортируйте копию, созданную House Plan.","backup.export_open":"Экспорт","backup.import_open":"Импорт","backup.export_title":"Экспорт House Plan","backup.import_title":"Импорт House Plan","backup.export_hint":"Выберите, должна ли копия содержать всю конфигурацию House Plan или только текущее пространство.","backup.full":"Полная копия","backup.current_space":"Текущее пространство","backup.current_space_title":"Текущее пространство: {title}","backup.no_current_space":"Нет текущего пространства","backup.plan_only":"Только планировка","backup.plan_only_hint":"Сохранить комнаты, стены, проёмы, декор и позиции подписей комнат без устройств и привязок Home Assistant.","backup.plan_only_preview":"Файл содержит только планировку","backup.privacy_warning":"Архив сохраняет названия, идентификаторы Home Assistant и точные координаты. Внутренние планы и вложения указываются ссылками, но не вкладываются; текущие состояния и маршруты пылесосов не включаются.","backup.download":"Скачать JSON","backup.export_done":"Резервная копия скачана","backup.reading":"Проверяем резервную копию…","backup.revalidated":"После предпросмотра план изменился. Сводка обновлена — проверьте её и подтвердите ещё раз.","backup.error.unauthorized":"У вас нет прав на экспорт или импорт этого плана.","backup.error.not_ready":"House Plan ещё не готов. Повторите попытку через несколько секунд.","backup.error.too_large":"Размер резервной копии превышает 8 МиБ.","backup.error.invalid_json":"Выбранный файл не является корректным JSON.","backup.error.invalid_format":"Выбранный файл не является резервной копией House Plan.","backup.error.unsupported_export_version":"Эта версия формата резервной копии не поддерживается установленной версией.","backup.error.future_model":"Резервная копия создана в более новой версии модели данных House Plan.","backup.error.invalid_config":"Резервная копия содержит некорректную конфигурацию House Plan.","backup.error.wall_model_migration_blocked":"Не удалось безопасно обновить модель стен из резервной копии. Сначала оптимизируйте исходный план и повторите экспорт.","backup.error.invalid_layout":"Резервная копия содержит некорректные позиции объектов.","backup.error.invalid_content":"Резервная копия содержит некорректные или несогласованные ссылки на файлы.","backup.error.space_not_found":"Выбранное пространство больше не существует.","backup.error.capacity_exceeded":"Добавление этой копии превысит ограничения размера плана.","backup.error.preview_expired":"Время предпросмотра истекло. Выберите файл резервной копии заново.","backup.error.preview_owner_mismatch":"Этот предпросмотр принадлежит другому пользователю Home Assistant.","backup.error.conflict":"После предпросмотра план изменился. Проверьте обновлённую сводку.","backup.error.content_confirmation_required":"Подтвердите отсоединение недоступных локальных файлов.","backup.error.commit_failed":"Не удалось безопасно применить копию. Предыдущий план восстановлен или ожидает восстановления.","backup.error.missing_plan":"После предпросмотра исчез связанный файл плана. Проверьте копию ещё раз.","backup.error.missing_content":"После предпросмотра исчез связанный локальный файл. Проверьте копию ещё раз.","backup.error.marker_control_missing":"В резервной копии отсутствует связанный источник света на плане.","backup.error.marker_control_not_light":"Связанная цель больше не отмечена как источник света.","backup.error.marker_control_self":"Источник света не может управлять самим собой.","backup.error.marker_control_cycle":"Резервная копия содержит циклическую цепочку управления светом.","backup.error.duplicate_marker_control":"Резервная копия содержит повторяющуюся связь с источником света.","backup.error.no_backup":"Нет снимка импорта или оптимизации, который можно восстановить.","backup.same_source":"Создано на этом экземпляре Home Assistant","backup.foreign_source":"Создано на другом экземпляре Home Assistant","backup.created":"Создано: {value}","backup.versions":"Карточка {card}; интеграция {integration}; модель данных {model}","backup.count_spaces":"Пространств: {n}","backup.count_rooms":"Комнат: {n}","backup.count_walls":"Стен: {n}","backup.count_openings":"Проёмов: {n}","backup.count_decor":"Объектов декора: {n}","backup.count_markers":"Устройств: {n}","backup.count_layout":"Позиций: {n}","backup.bindings":"Привязки — устройства: {device}, сущности: {entity}, виртуальные: {virtual}; позиций без пространства: {legacy}","backup.binding_status":"Статус на целевом экземпляре — активных: {active}, деактивированных: {disabled}, отсутствующих: {missing}","backup.missing_areas":"На целевом экземпляре отсутствуют зоны: {areas}","backup.dropped_marker_links":"Связи с источниками света вне переносимого пространства пропущены: {n}.","backup.repaired_target_refs":"Существующих ссылок восстановлено этим импортом: {n}.","backup.preserved_unresolved_refs":"Неоднозначные ссылки сохранены без изменений: {n}.","backup.preserved_unresolved_hint":"House Plan ничего не угадывал и не удалял. После импорта запустите «Оптимизировать планы», чтобы проверить оставшиеся ссылки.","backup.import_details":"Подробности восстановления ссылок","backup.import_detail.incoming_remapped":"Ссылок обновлено внутри импортируемой копии: {n}","backup.import_detail.target_repaired":"Существующих ссылок восстановлено: {n}","backup.import_detail.preserved_unresolved":"Неразрешимых ссылок сохранено: {n}","backup.import_detail.collisions":"Конфликтов назначения безопасно сохранено: {n}","backup.import_detail.dropped_links":"Входящих связей пропущено по правилам переноса: {n}","backup.import_detail.bounded_lineages":"Чрезмерно вложенных идентификаторов ограничено: {n}","backup.replace_warning":"Текущая конфигурация и расположение будут заменены. Загруженные файлы не удаляются. Одну отмену можно выполнить до следующего изменения плана.","backup.foreign_bookkeeping":"Служебные списки известных и новых устройств другого экземпляра импортированы не будут.","backup.final_name":"Название нового пространства","backup.target_settings":"Глобальные настройки целевого экземпляра не изменятся.","backup.duplicates":"Уже существующих привязок: {n}","backup.skip":"Пропустить повторяющиеся устройства","backup.virtual_copy":"Добавить безопасные статичные виртуальные копии","backup.content":"Связанное содержимое","backup.content_available":"доступно локально","backup.content_external":"внешняя ссылка","backup.content_detach_required":"будет отвязано","backup.confirm_detach":"Я понимаю, что недоступные внутренние планы и вложения будут отвязаны от импортируемой конфигурации.","backup.replace":"Заменить","backup.add":"Добавить пространство","backup.space_done":"Пространство импортировано: комнат — {rooms}, устройств — {markers}, существующих ссылок восстановлено — {refs}","backup.full_done":"Копия восстановлена: пространств — {spaces}, комнат — {rooms}, устройств — {markers}","backup.undo_import":"Отменить последний полный импорт","backup.import_undone":"Полный импорт отменён"}}],ra=oa;function la(e){return"string"==typeof e?e.trim().replaceAll("_","-").toLowerCase():""}const ca=new Map(aa.map(e=>[la(e.code),e]));function ha(e){return ca.get(la(e))}function da(e,t){const i=[{value:"",label:e},...aa.map(({code:e,nativeLabel:t})=>({value:e,label:t}))],s="string"==typeof t?t:"";return s&&!i.some(e=>e.value===s)&&i.push({value:s,label:ha(s)?.nativeLabel??s}),i}function pa(e,t){return function(e,t,i,s){const n=new Map(i.map(e=>[la(e),e])),o=n.get(la(e));if(o)return o;const a=la(t),r=n.get(a);return r||(n.get(a.split("-")[0]||"")??s)}(t,e?.locale?.language||e?.language,aa.map(({code:e})=>e),"en")}function ua(e,t,i){const s=ha(e)?.dictionary;return ss(s?.[t]??ra[t]??t,i)}function _a(e,t){const i=ha(e)?.dictionary[t]??ra[t];return"string"==typeof i&&i.trim().length>0}class ma extends he{constructor(){super(...arguments),this.deviceName="",this._metadataConnection=null,this._demoUntil=0,this._demoTimer=0,this._demoGeneration=1,this._demoKind=null,this._reducedMotion=!1,this._onMotionChange=e=>{this._reducedMotion=e.matches,this.requestUpdate()},this._startShortDemo=()=>{const e=this.presentation;e&&"icon_ripple"===e.display&&"none"===e.pulse.kind&&(this._demoGeneration++,this._demoKind="short",this._demoUntil=Date.now()+3300,window.clearTimeout(this._demoTimer),this._demoTimer=window.setTimeout(()=>{this._demoUntil=0,this._demoTimer=0,this._demoKind=null,this.requestUpdate()},3330),this.requestUpdate())},this._toggleContinuousDemo=()=>{const e=this.presentation;e&&"icon_ripple"===e.display&&"none"===e.pulse.kind&&("continuous"===this._demoKind?this._clearDemo():(this._clearDemo(),this._demoGeneration++,this._demoKind="continuous",this._demoUntil=Number.POSITIVE_INFINITY),this.requestUpdate())}}connectedCallback(){super.connectedCallback(),this._motionMedia=window.matchMedia?.("(prefers-reduced-motion: reduce)"),this._reducedMotion=!!this._motionMedia?.matches,this._motionMedia?.addEventListener?.("change",this._onMotionChange),this._ensureMetadata()}disconnectedCallback(){this._metadataRelease?.(),this._metadataRelease=void 0,this._metadataConnection=null,this._clearDemo(),this._motionMedia?.removeEventListener?.("change",this._onMotionChange),this._motionMedia=void 0,super.disconnectedCallback()}willUpdate(e){e.has("hass")&&this._ensureMetadata();const t=this.presentation,i=e.get("presentation");e.has("presentation")&&i?.binding!==t?.binding&&this._clearDemo(),t&&this._demoKind&&("icon_ripple"!==t.display||"none"!==t.pulse.kind)&&this._clearDemo()}_ensureMetadata(){const e=this.hass?.connection||null;e&&e!==this._metadataConnection&&(this._metadataRelease?.(),this._metadataConnection=e,this._metadataRelease=Qo(this.hass,()=>this.requestUpdate()))}_clearDemo(){window.clearTimeout(this._demoTimer),this._demoTimer=0,this._demoUntil=0,this._demoKind=null}get _lang(){return pa(this.hass)}_t(e,t){return ua(this._lang,e,t)}_providerText(e){return"houseplan"===e.domain?this._t("marker.preview.virtual_provider"):e.label.toLowerCase()===e.domain.toLowerCase()?e.domain:`${e.label} (${e.domain})`}_providers(){return this.presentation&&this.registry?function(e,t,i,s=ea(e)){if("virtual"===t)return[{domain:"houseplan",label:"House Plan",confidence:"registry-owner"}];const n=t.indexOf(":");if(n<1)return[];const o=t.slice(0,n),a=t.slice(n+1);if("entity"===o){const t=na(e,a,i,s);return t?[t]:[]}if("device"!==o)return[];const r=i.devices?.[a]||e?.devices?.[a];if(!r)return[];const l=[r.config_entry_id,...Array.isArray(r.config_entries)?r.config_entries:[]].filter(e=>"string"==typeof e&&!!e).map(t=>{const i=s.configEntries[t];return ia(e,i?.domain,"registry-owner",s,t)});if(l.some(Boolean))return sa(l);const c=[];for(const[t,n]of Object.entries(i.entities||{}))n?.device_id===a&&null==n?.disabled_by&&c.push(na(e,t,i,s));return c.some(Boolean)?sa(c):sa((Array.isArray(r.identifiers)?r.identifiers:[]).map(t=>{const i=Array.isArray(t)?t[0]:null;return ia(e,i,"identifier-fallback",s)}))}(this.hass,this.presentation.binding,this.registry,ea(this.hass)):[]}_providerSummary(e){if(!e.length)return this._t("marker.preview.unknown_provider");const t=e.slice(0,2).map(e=>this._providerText(e)),i=e.length-t.length;return t.join(", ")+(i>0?` · ${this._t("marker.preview.more_sources",{n:i})}`:"")}_sourceSummary(e){const t=e.visualSources;return t.length?1===t.length?`${t[0].name} · ${t[0].eid} · ${this._sourceProvider(t[0].eid)}`:2===t.length?t.map(e=>e.name).join(" · "):this._t("marker.preview.multiple_sources",{n:t.length}):this._t("marker.preview.no_source")}_stateSummary(e){if(!e.visualSources.length)return this._t("marker.preview.no_state");if(1===e.visualSources.length)return e.visualSources[0].stateText||this._t("marker.preview.no_state");const t=[...new Set(e.visualSources.map(e=>e.stateText).filter(Boolean))];return 1===t.length?t[0]:this._t("marker.preview.mixed_states")}_reason(e){return this._t(`marker.preview.reason.${e}`)}_sourceProvider(e){if(!this.registry)return this._t("marker.preview.unknown_provider");const t=na(this.hass,e,this.registry,ea(this.hass));return t?this._providerText(t):this._t("marker.preview.unknown_provider")}render(){const e=this.presentation;if(!e)return G;const t=!!this._demoKind&&this._demoUntil>Date.now()&&"icon_ripple"===e.display&&"none"===e.pulse.kind,i=t?(()=>{const t=function(e,t,i,s,n=null){return{kind:t,reason:"short"===t?"event":"running",generation:Math.max(1,Math.trunc(i)),expiresAt:"short"===t?n:null,color:e.color,diameterScale:s?1:e.diameterScale,animated:!s,reducedMotionIndicator:s?"dot":"none"}}(e.pulse,this._demoKind,this._demoGeneration,this._reducedMotion,"short"===this._demoKind?this._demoUntil:null);return{...e,pulse:t,classes:[...e.classes.filter(e=>!e.startsWith("pulse-")),`pulse-${t.kind}`,...t.generation%2==0?["pulse-gen2"]:[]]}})():e,s=this._providers(),n="none"!==e.pulse.kind,o=i.scale*(i.pulse.animated?i.pulse.diameterScale:1);let a=-o/2,r=o/2,l=-o/2,c=o/2;if(i.valueBadge){const e=i.scale*Math.min(4,Math.max(.9,.29*i.valueBadge.text.length+.28)),t=.7*i.scale,s=.6*i.scale;"right"===i.valueBadge.position&&(r=Math.max(r,s+e)),"left"===i.valueBadge.position&&(a=Math.min(a,-s-e)),"top"===i.valueBadge.position&&(l=Math.min(l,-s-t)),"bottom"===i.valueBadge.position&&(c=Math.max(c,s+t))}if(null!=i.lqiText){const e=i.scale*("bottom"===i.valueBadge?.position?1.55:.95);c=Math.max(c,e)}const h=Math.min(1,2.35/Math.max(1,r-a),2.35/Math.max(1,c-l)),d=Math.round(100*h),p=[`left:calc(50% - ${54*((a+r)/2)*h}px)`,`top:calc(50% - ${54*((l+c)/2)*h}px)`,h<1?`transform:scale(${h})`:""].filter(Boolean).join(";"),u=["dev",Uo(this.hass),...i.classes,"virtual"===i.binding?"virtual":"",i.haDisabled?"ghost ha-disabled":"",null!=i.valueText?"valonly":""].filter(Boolean).join(" "),_=Go(i).join(";"),m=t?this._t("continuous"===this._demoKind?"marker.preview.demo_continuous_notice":"marker.preview.demo_short_notice"):this._reason(e.explanation.reason),g=[this.deviceName,this._providerSummary(s),this._stateSummary(e),m,e.valueFullText||"",Io(e.valueBadge),null!=e.lqiText&&e.lqiBand?this._t(`marker.lqi_a11y_${e.lqiBand}`,{value:e.lqiText}):""].filter(Boolean).join(". ");return q`
-
- ${this._t("marker.preview.title")} - - ${this._t(t?"marker.preview.example":"marker.preview.actual")} - -
-
- -
-
${this._t("marker.preview.integration")}${this._providerSummary(s)}
-
${this._t("marker.preview.source")}${this._sourceSummary(e)}
-
${this._t("marker.preview.current_state")}${this._stateSummary(e)}
-
${this._t("marker.preview.result")}${m}
- ${e.fallbackReason?q`

${this._reason(e.fallbackReason)}

`:G} - ${e.explanation.notices.map(e=>q`

${this._reason(e)}

`)} - ${h<1?q`

${this._t("marker.preview.scaled",{n:d})}

`:G} - ${t&&this._reducedMotion?q`

${this._t("marker.preview.reduced_motion")}

`:G} -
-
- ${e.visualSources.length||e.criticalSources.length?q`
- ${this._t("marker.preview.details")} -
- ${[...e.visualSources,...e.criticalSources].map(e=>q` -
- ${e.name}${e.eid} - ${e.stateText||e.state} - ${this._sourceProvider(e.eid)} -
`)} -
-
`:G} - ${"icon_ripple"===e.display?q`
- - -
`:G} -
`}}ma.properties={hass:{attribute:!1},presentation:{attribute:!1},registry:{attribute:!1},deviceName:{attribute:!1}},ma.styles=[Ke,o` - :host { display: block; min-width: 0; } - .devicepreview { - margin: 4px 0 14px; - padding: 14px; - border: 1px solid var(--divider-color, rgba(127, 127, 127, 0.28)); - border-radius: 14px; - background: color-mix(in srgb, var(--card-background-color, #20232b) 92%, var(--primary-color, #03a9f4)); - color: var(--primary-text-color, #fff); - overflow: hidden; - } - .previewhead { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; } - .previewbadge { - padding: 3px 9px; - border-radius: 999px; - color: var(--secondary-text-color, #9aa0aa); - background: color-mix(in srgb, var(--secondary-text-color, #9aa0aa) 12%, transparent); - font-size: 12px; - font-weight: 700; - } - .previewbadge.example { color: var(--warning-color, #ffb300); } - .previewgrid { display: grid; grid-template-columns: minmax(170px, 0.8fr) minmax(240px, 1.2fr); gap: 16px; align-items: stretch; } - .previewstage { - position: relative; - min-height: 168px; - border-radius: 12px; - background: var(--secondary-background-color, rgba(0, 0, 0, 0.16)); - overflow: hidden; - pointer-events: none; - container-type: inline-size; - --icon-size: 54px; - --device-base-size: 48.6px; - } - .previewfit { position: absolute; left: 50%; top: 50%; transform-origin: center; } - .previewstage .dev { left: 0; top: 0; cursor: default; } - :host([data-pointer-hover]) .previewstage .dev:hover { z-index: 2; } - .previewfacts { min-width: 0; display: grid; align-content: start; gap: 8px; } - .previewfacts > div { display: grid; grid-template-columns: minmax(115px, 0.8fr) minmax(0, 1.3fr); gap: 8px; } - .previewfacts span { color: var(--secondary-text-color, #9aa0aa); } - .previewfacts b { min-width: 0; overflow-wrap: anywhere; } - .previewnotice { margin: 2px 0 0; color: var(--secondary-text-color, #9aa0aa); font-size: 13px; } - .previewdetails { margin-top: 12px; } - .previewdetails summary { cursor: pointer; color: var(--secondary-text-color, #9aa0aa); } - .previewtable { display: grid; gap: 8px; margin-top: 9px; } - .previewsource { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 3px 10px; font-size: 13px; } - .previewsource > * { min-width: 0; overflow-wrap: anywhere; } - .previewsource code { color: var(--secondary-text-color, #9aa0aa); } - .previewdemo { - min-height: 40px; - display: inline-flex; - align-items: center; - gap: 8px; - padding: 8px 12px; - border: 1px solid var(--divider-color, rgba(127, 127, 127, 0.35)); - border-radius: 10px; - background: transparent; - color: inherit; - cursor: pointer; - } - .previewdemos { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; } - .previewdemo:focus-visible { outline: 2px solid var(--primary-color, #03a9f4); outline-offset: 2px; } - .previewdemo:disabled { opacity: 0.5; cursor: not-allowed; } - @media (max-width: 640px) { - .previewgrid { grid-template-columns: minmax(0, 1fr); } - .previewstage { min-height: 142px; } - .previewfacts > div { grid-template-columns: minmax(0, 1fr); gap: 2px; } - } - `],customElements.get("hp-device-preview")||customElements.define("hp-device-preview",ma);const ga=Math.tan(.25*Math.PI/180);function fa(e,t){const i=Math.abs(Number(t[0])-Number(e[0])),s=Math.abs(Number(t[1])-Number(e[1]));if(!([i,s].every(Number.isFinite)&&i>0&&s>0))return null;const n=i>=s?"horizontal":"vertical",o=Math.max(i,s),a=Math.min(i,s);return a/o>ga?null:{axis:n,major:o,minor:a,angleDegrees:180*Math.atan2(a,o)/Math.PI}}function va(e,t){const i=fa(e,t);return i?"horizontal"===i.axis?[Number(t[0]),Number(e[1])]:[Number(e[0]),Number(t[1])]:[Number(t[0]),Number(t[1])]}const ba=e=>`${e[0]},${e[1]}`,ya=(e,t)=>e[0]===t[0]&&e[1]===t[1],wa=(e,t)=>{const i=ba(e),s=ba(t);return i{let t=0;for(let i=0;i{if(e.length<3)return!1;for(let t=0;t1e-12},$a=e=>{for(let t=0;t+1ya(e,t.from)?[...t.to]:[...e],Ma=(e,t)=>e.map(e=>Array.isArray(e?.poly)?{...e,poly:e.poly.map(e=>Sa(e,t))}:e),Ca=(e,t,i)=>{const s=e.flatMap((e,t)=>hi(e)?.some(e=>ya(e,i.from))?[t]:[]);if(!s.length)return!1;for(const i of s){const s=hi(e[i]),n=hi(t[i]);if(!s||!n)continue;if(!xa(n))return!1;const o=Math.sign(ka(s)),a=Math.sign(ka(n));if(o&&a&&o!==a)return!1}const n=new Set;for(const e of s){const i=hi(t[e]);if(i)for(let s=0;s{let i=0;for(const s of e){const e=hi(s);if(e)for(let s=0;s{const{a:i,b:s,axis:n}=e,o="horizontal"===n?{from:s,to:[s[0],i[1]]}:{from:s,to:[i[0],s[1]]},a="horizontal"===n?{from:i,to:[i[0],s[1]]}:{from:i,to:[s[0],i[1]]};return[{move:o,preservedDegree:Da(t,i)},{move:a,preservedDegree:Da(t,s)}].sort((e,t)=>t.preservedDegree-e.preservedDegree||ba(e.move.to).localeCompare(ba(t.move.to))||ba(e.move.from).localeCompare(ba(t.move.from))).map(e=>e.move)};const Ta=(e,t)=>[e[0]-t[0],e[1]-t[1]],Ia=(e,t)=>[e[0]+t[0],e[1]+t[1]],Ra=(e,t)=>e[0]*t[0]+e[1]*t[1],Aa=e=>Math.hypot(e[0],e[1]);function za(e){let t=0;for(let i=0;ie[1]!=l>e[1]&&e[0]<(r-o)*(e[1]-a)/(l-a)+o&&(i=!i)}return i}([r[0]+a[0]*l,r[1]+a[1]*l],e)&&(a=[-a[0],-a[1]]),a}function Ea(e){const t=e.length;if(t<3)return!1;for(let i=0;i(e[0]-n[0])*r[1]-(e[1]-n[1])*r[0],c=e=>(e[0]-n[0])*r[0]+(e[1]-n[1])*r[1],h=i,d=a-i;for(const t of e){const e=Math.abs(l(t));if(e<=i)continue;const n=c(t);n<=h||n>=d||eMath.abs(a+(e-p)/g*(r-a));s=Math.min(s,f(_),f(m))}}return s}function Ha(e,t,i){if(Di(e,t,i))return!0;if(Ci(e,t,i)||Ci(t,e,i))return!1;let s=0;try{const i=ni([[...e.map(e=>[e[0],e[1]]),[e[0][0],e[0][1]]]],[[...t.map(e=>[e[0],e[1]]),[t[0][0],t[0][1]]]]);for(const e of i)e?.[0]&&(s+=Ti(e[0]))}catch{return!1}return s>Math.max(1e-7,i*i)}function La(e,t,i){for(const s of t)for(let t=0;ti)continue;const c=(e.x-n[0])*l[0]+(e.y-n[1])*l[1];if(c-e.length/2>=-i&&c+e.length/2<=r+i)return!0}return!1}function Ba(e,t,i){const s=Math.abs(t[0]-e[0]),n=Math.abs(t[1]-e[1]);return s<=i&&n>i?"v":n<=i&&s>i?"h":null}function ja(e,t,i){return Math.abs(e[0]-t[0])<=i&&Math.abs(e[1]-t[1])<=i}function qa(e,t,i,s,n){return ja(e,i,n)&&ja(t,s,n)||ja(e,s,n)&&ja(t,i,n)}function Wa(e,t,i,s,n){const o=Ba(e,t,n);return o&&Ba(i,s,n)===o?"h"===o?Math.abs(e[1]-i[1])>n||Math.abs(e[1]-s[1])>n?0:Math.max(0,Math.min(Math.max(e[0],t[0]),Math.max(i[0],s[0]))-Math.max(Math.min(e[0],t[0]),Math.min(i[0],s[0]))):Math.abs(e[0]-i[0])>n||Math.abs(e[0]-s[0])>n?0:Math.max(0,Math.min(Math.max(e[1],t[1]),Math.max(i[1],s[1]))-Math.max(Math.min(e[1],t[1]),Math.min(i[1],s[1]))):0}function Ua(e,t,i,s){const n=e.length,o=Ba(e[(t-1+n)%n],e[t],s),a=Ba(e[(t+1)%n],e[(t+2)%n],s),r="h"===i?"v":"h";return o===r&&a===r}function Ga(e){let t=1/0,i=1/0,s=-1/0,n=-1/0;for(const o of e)t=Math.min(t,o[0]),i=Math.min(i,o[1]),s=Math.max(s,o[0]),n=Math.max(n,o[1]);return[t,i,s,n]}function Va(e,t,i){const s=Ga(e),n=Ga(t);return s[2]n}const Ya=(e,t)=>"h"===t?e[0]:e[1],Ja=(e,t)=>"h"===t?e[1]:e[0];function Xa(e,t,i,s,n){const o=e.find(e=>e.id===t);if(!o)return null;const a=o.poly[i],r=o.poly[(i+1)%o.poly.length],l=Ba(a,r,n);if(!l)return null;const c=0===s?r:a,h=0===s?a:r,d=Ya(c,l),p=Ya(h,l),u=Ja(c,l),_=function(e,t,i,s){const n=[];for(const o of e)for(let e=0;es||Math.abs(Ja(r,t)-i)>s)continue;const l=Ya(a,t),c=Ya(r,t),h=Math.min(l,c),d=Math.max(l,c);d-h>s&&n.push({roomId:o.id,edge:e,lo:h,hi:d})}return n.sort((e,t)=>e.lo-t.lo||e.hi-t.hi||e.roomId.localeCompare(t.roomId)||e.edge-t.edge)}(e,l,u,n),m=function(e,t,i,s){const n=[t,i];for(const o of e){const e=Math.max(t,o.lo),a=Math.min(i,o.hi);a-e>s&&n.push(e,a)}const o=[...new Set(n)].sort((e,t)=>e-t),a=[];for(let t=0;t+1r>e.lo-s&&re.roomId))].sort();a.push({lo:i,hi:n,owners:l})}return a}(_,Math.min(d,p),Math.max(d,p),n);return!m.length||m.some(e=>e.owners.length<1||e.owners.length>2)?null:{roomId:t,edge:i,movedEndpoint:s,axis:l,line:u,fixed:d,moving:p,intervals:_,baseline:m}}function Za(e,t,i){const s=[];for(const n of e.intervals){const o=t.polys[n.roomId];if(!o){s.push(n);continue}if(n.edge<0||n.edge>=o.length)continue;const a=o[n.edge],r=o[(n.edge+1)%o.length];if(Ba(a,r,i)!==e.axis||Math.abs(Ja(a,e.axis)-e.line)>i||Math.abs(Ja(r,e.axis)-e.line)>i)continue;const l=Ya(a,e.axis),c=Ya(r,e.axis),h=Math.min(l,c),d=Math.max(l,c);d-h>i&&s.push({...n,lo:h,hi:d})}return s}function Qa(e,t,i){return new Set(e.filter(e=>t>e.lo-i&&te.roomId)).size}function er(e,t,i){for(const s of t.sideOwnership){const t=e.polys[s.roomId];if(!t)return!1;const n=t[s.edge],o=t[(s.edge+1)%t.length];if(Ba(n,o,i)!==s.axis)return!1;const a=Ya(0===s.movedEndpoint?n:o,s.axis),r=Za(s,e,i),l=(s.moving>=s.fixed?s.baseline[s.baseline.length-1]:s.baseline[0]).owners.length;if(l<1||l>2)return!1;const c=Math.min(s.fixed,s.moving,a),h=Math.max(s.fixed,s.moving,a),d=[c,h,s.fixed,s.moving,a];for(const e of s.intervals)d.push(e.lo,e.hi);for(const e of r)d.push(e.lo,e.hi);const p=[...new Set(d.filter(e=>e>=c&&e<=h))].sort((e,t)=>e-t),u=Math.min(s.fixed,s.moving),_=Math.max(s.fixed,s.moving),m=Math.min(s.fixed,a),g=Math.max(s.fixed,a);for(let e=0;e+1u-i&&o<_+i,c=o>m-i&&o2||d>2)return!1;if(a&&c){if(h!==d)return!1}else if(!a&&c){if(d!==l||h>0&&h!==d)return!1}else if(2===h?2!==d:d>1)return!1}}return!0}function tr(e,t,i,s){const n=[i.n[0]*s,i.n[1]*s],o={polys:{},openings:{},movedSpans:{}};for(const t of i.roomIds){const s=e.find(e=>e.id===t),a=i.edgeByRoom[t];if(!s||null==a)continue;const r=(a+1)%s.poly.length;o.polys[t]=s.poly.map((e,t)=>t===a||t===r?Ia(e,n):[...e]),o.movedSpans[t]=[[Ia(s.poly[a],n),Ia(s.poly[r],n)]]}for(const e of t)i.movingOpeningIds.includes(e.id)&&(o.openings[e.id]=[e.x+n[0],e.y+n[1]]);return o}function ir(e,t,i,s,n,o,a,r){if(!function(e,t,i,s){if(Fa([e.x,e.y],t,i)>2*s)return!1;const n=Aa(Ta(i,t));if(n<=s)return!1;const o=[(i[0]-t[0])/n,(i[1]-t[1])/n],a=(e.x-t[0])*o[0]+(e.y-t[1])*o[1];return a>=-s&&a<=n+s}(e,t,i,r))return!0;if(Fa([e.x,e.y],s,n)>2*r)return!1;const l=0===o?s:n;return Aa(Ta([e.x,e.y],l))>=e.length/2+a-r}function sr(e,t,i,s){const n=s.movingHalf||0;return"circle"===e.kind?Fa(e.center,t,i)2)return!1;const r=tr(e,t,i,s);if(Object.keys(r.polys).length!==i.roomIds.length)return!1;if(!er(r,i,o))return!1;const l=new Set(i.roomIds),c=e=>r.polys[e.id]||e.poly;for(const s of i.roomIds){const l=e.find(e=>e.id===s),c=r.polys[s],h=i.edgeByRoom[s];if(!l||!c||c.length!==i.topology[s]||c.length!==l.poly.length||!Ea(c))return!1;const d=l.poly.length,p=(h-1+d)%d,u=[p,h,(h+1)%d];for(const e of u)if(fa(c[e],c[(e+1)%d]))return!1;const _=za(l.poly),m=za(c);if(Math.abs(m)e.id===t),s=r.polys[t];for(const n of e){if(n.id===t||l.has(n.id))continue;const e=c(n);if(!Va(i.poly,n.poly,o)||!Va(s,e,o))if(Ci(i.poly,n.poly,o)){if(!Ci(s,e,o))return!1}else if(Ci(n.poly,i.poly,o)){if(!Ci(e,s,o))return!1}else if(Ci(s,e,o)||Ci(e,s,o)||Ha(s,e,o))return!1}}e.find(e=>e.id===i.roomId);const h=r.polys[i.roomId],d=i.edgeByRoom[i.roomId],p=h[d],u=h[(d+1)%h.length];if(Math.abs(s)>o)for(const e of n.obstacles||[])if(sr(e,p,u,n))return!1;for(const e of t){if(!i.movingOpeningIds.includes(e.id))continue;const t=r.openings[e.id];if(!t||e.hosted)return!1;if(!La({...e,x:t[0],y:t[1]},[h],2*o))return!1}return!0}const or=new WeakMap;function ar(e,t,i){const s=i/t;return Ti(e)*s*s/1e4}function rr(e,t){return t?`${Math.round(10.7639*e)} ft²`:`${(Math.round(10*e)/10).toFixed(1)} m²`}const lr=e=>"number"==typeof e&&Number.isFinite(e),cr=(e,t)=>{const i=new Map;for(const s of e||[])!lr(s?.cm)||!t&&s.cm<=0||i.set(s.cm,(i.get(s.cm)||0)+1);return i},hr=e=>[String(e),"см"].join(" ");function dr(e,t,i={}){if(i.allowClear)return[];const s=!0===i.exactMultiplicity,n=cr(e,s),o=cr(t,s),a=[];for(const[e,t]of n){const i=o.get(e)||0;0===i?a.push({invariant:"wall_records",kind:"lost",owner:hr(e),reference:`было ${t}`,detail:"записи этой толщины исчезли целиком"}):s&&i!==t&&a.push({invariant:"wall_records",kind:"count",owner:hr(e),reference:`было ${t}, стало ${i}`,detail:"изменилось число записей этой толщины"})}if(s)for(const[e,t]of o)n.has(e)||a.push({invariant:"wall_records",kind:"count",owner:hr(e),reference:`было 0, стало ${t}`,detail:"появились записи новой толщины"});return a}class pr{constructor(){this._selectedRoomId=null,this._session=null,this._eligibility=null}get dragging(){return null!==this._session}ownsPointer(e){return this._session?.pointerId===e}get selectedRoomId(){return this._selectedRoomId}get snapshotIdentity(){return this._session?.snapshotIdentity||null}get activePointerId(){return this._session?.pointerId??null}get moved(){return!0===this._session?.moved}get delta(){return this._session?.delta||0}get rooms(){return this._session?.rooms||null}get openings(){return this._session?.openings||null}get plan(){return this._session?.plan||null}get preview(){return this._session?.accepted?.preview||null}get liveLabels(){return this._session?.accepted?.labels||null}selectRoom(e){this._selectedRoomId=e}restoreSelection(e){this._selectedRoomId=e}escapeIdle(){return this._selectedRoomId?(this._selectedRoomId=null,"selection-cleared"):"exit-tool"}resolve(e,t,i){this._eligibility&&this._eligibility.context===e||(this._eligibility={context:e,values:new Map});const s=this._eligibility.values.get(t);if(s)return s;const n=i();return this._eligibility.values.set(t,n),n}begin(e){return!this._session&&(this._session={pointerId:e.pointerId,start:[...e.start],roomId:e.roomId,plan:e.plan,options:e.options,rooms:e.rooms,openings:e.openings,snapshotIdentity:e.snapshotIdentity,before:e.before,moved:!1,delta:0,changedRoomIds:[...e.plan.roomIds],rejectionNotified:!1,wallUnionBefore:e.wallUnionBefore,accepted:null},!0)}move(e){const t=this._session;if(!t||t.pointerId!==e.pointerId)return{kind:"no-op"};const i=(s=t.start,n=e.point,o=t.plan.n,[s?.[0],s?.[1],n?.[0],n?.[1],o?.[0],o?.[1]].every(Number.isFinite)?(n[0]-s[0])*o[0]+(n[1]-s[1])*o[1]:0);var s,n,o;const a=e.snap([t.plan.a[0]+t.plan.n[0]*i,t.plan.a[1]+t.plan.n[1]*i]),r=(a[0]-t.plan.a[0])*t.plan.n[0]+(a[1]-t.plan.a[1])*t.plan.n[1],l=function(e,t,i,s,n,o){if(!Number.isFinite(s)||Math.abs(s)<1e-9)return 0;const a=Math.sign(s),r=Math.abs(s),l=Math.max(Math.abs(n),1e-6);let c=or.get(i);c&&c.opts===o||(c={opts:o,values:new Map},or.set(i,c));const h=c.values;let d=0;for(let s=Math.min(l,r),n=0;n<4096&&s<=r+1e-9;n++,s=Math.min(r,s+l)){const n=a*s,c=`${n.toFixed(9)}|${l.toFixed(9)}`;let p=h.get(c);if(void 0===p&&(p=nr(e,t,i,n,o),h.size<4096&&h.set(c,p)),!p)break;if(d=n,Math.abs(s-r)<=1e-9)break}return d}(t.rooms,t.openings,t.plan,r,e.step,t.options);if(t.moved&&l===t.delta)return{kind:"no-op"};const c=tr(t.rooms,t.openings,t.plan,l);let h;try{if(h=e.project(t.snapshotIdentity,c.polys,c.openings,t.changedRoomIds,t.rooms),!h.ok)return this._projectionRejected(t);if(dr(h.value.beforeWalls,h.value.afterWalls,{exactMultiplicity:!0}).length)return this._projectionRejected(t)}catch{return this._projectionRejected(t)}const d=t.accepted,p={preview:h.value.preview,labels:null,artifact:h.value.artifact,beforeWalls:h.value.beforeWalls,afterWalls:h.value.afterWalls};t.accepted=p;try{e.publish(p.preview,p.artifact),p.labels=e.measure(c,t.plan)}catch{t.accepted=d;try{e.publish(d?.preview||null,d?.artifact||null)}catch{}return this._projectionRejected(t)}return t.moved=!0,t.delta=l,{kind:"accepted",preview:h.value.preview,labels:p.labels,artifact:h.value.artifact}}finish(e){const t=this._session;if(!t||t.pointerId!==e.pointerId)return{kind:"no-op"};const i=t.accepted;let s;if(t.moved&&Math.abs(t.delta)>1e-9&&i)if(e.currentSnapshotIdentity!==t.snapshotIdentity)s={kind:"rejected",reason:"stale-snapshot"};else if(nr(t.rooms,t.openings,t.plan,t.delta,t.options))if(dr(i.beforeWalls,i.afterWalls,{exactMultiplicity:!0}).length)s={kind:"rejected",reason:"wall-records"};else{let n=!1;try{n=e.validatePreview(i.preview)}catch{n=!1}s=n?{kind:"commit",preview:i.preview,before:t.before}:{kind:"rejected",reason:"invalid-candidate"}}else s={kind:"rejected",reason:"invalid-topology"};else s={kind:"no-op"};return this._session=null,"commit"===s.kind&&(this._eligibility=null),s}cancel(e,t){const i=this._session;return!i||void 0!==t&&i.pointerId!==t?{kind:"no-op"}:(this._session=null,this._eligibility=null,{kind:"cancelled",restoreWallUnion:e===i.snapshotIdentity?i.wallUnionBefore:null})}reset(){this._selectedRoomId=null,this._session=null,this._eligibility=null}_projectionRejected(e){const t=!e.rejectionNotified;return e.rejectionNotified=!0,{kind:"rejected",notify:t}}}const ur=(e,t)=>[(e[0]-t.x)/Math.max(1e-9,t.w)*t.stageWidth,(e[1]-t.y)/Math.max(1e-9,t.h)*t.stageHeight];function _r(e){const{poly:t,edge:i,text:s,view:n}=e,o=t[i],a=t[(i+1)%t.length],r=[(o[0]+a[0])/2,(o[1]+a[1])/2],l=function(e,t){const i=e[t],s=e[(t+1)%e.length],n=s[0]-i[0],o=s[1]-i[1],a=Math.hypot(n,o)||1;return(e=>{let t=0;for(let i=0;i=0?[-o/a,n/a]:[o/a,-n/a]}(t,i),c=Math.hypot(a[0]-o[0],a[1]-o[1])||1,h=[(a[0]-o[0])/c,(a[1]-o[1])/c],d=ur(r,n),p=ur(e.gearCenter,n),u=Math.max(34,7.2*s.length+12),_=Math.abs(l[0])>=Math.abs(l[1])?Math.max(28,u/2+4):Math.max(28,13),m=d[0]+l[0]*_,g=d[1]+l[1]*_,f=t=>{return i=m+h[0]*t,s=g+h[1]*t,n=u,o=18,a=p[0],r=p[1],l=e.gearWidthPx,c=e.gearHeightPx,2*Math.abs(i-a)=Math.abs(h[1])?h[0]:h[1])>0?-1:1,t=Math.max(n.stageWidth,n.stageHeight,64);for(let i=4;i<=t;i+=4){const t=i*e;if(!f(t)){v=t;break}const s=-t;if(!f(s)){v=s;break}}}const b=l[0]*_+h[0]*v,y=l[1]*_+h[1]*v,w=Math.hypot(b,y)||1,k=y/w*12,x=[r[0]+b/w*12*n.w/Math.max(1,n.stageWidth),r[1]+k*n.h/Math.max(1,n.stageHeight)];return{anchor:r,offsetXPx:b,offsetYPx:y,tangentOffsetPx:v,side:Math.abs(l[0])>=Math.abs(l[1])?l[0]<0?"left":"right":l[1]<0?"above":"below",leader:{a:r,b:x}}}function mr(e){const t=e%360;return t<0?t+360:t}function gr(e,t){const i=function(e,t){return mr(e+t)}(e,t)*Math.PI/180;return[Math.sin(i),-Math.cos(i)]}const fr=Object.freeze({dawn:Object.freeze({top:"#aabdd1",bottom:"#e8c8b7",horizon:"rgba(255,201,156,.56)",sun:"rgba(255,188,125,.78)",vignette:"rgba(65,72,99,.21)",outlineNear:"rgba(74,57,61,.25)",outlineMid:"rgba(255,238,224,.40)",outlineFar:"rgba(255,224,202,.18)"}),day:Object.freeze({top:"#dce9ef",bottom:"#cbdce3",horizon:"rgba(255,245,220,.45)",sun:"rgba(255,239,190,.72)",vignette:"rgba(65,91,105,.16)",outlineNear:"rgba(45,62,71,.28)",outlineMid:"rgba(255,255,255,.42)",outlineFar:"rgba(255,255,255,.20)"}),dusk:Object.freeze({top:"#48536c",bottom:"#9a7380",horizon:"rgba(242,156,114,.34)",sun:"rgba(255,167,113,.55)",vignette:"rgba(20,26,44,.39)",outlineNear:"rgba(238,219,225,.40)",outlineMid:"rgba(229,207,218,.26)",outlineFar:"rgba(215,190,205,.12)"}),night:Object.freeze({top:"#111a27",bottom:"#1f2f3e",horizon:"rgba(79,120,151,.16)",sun:"rgba(169,208,231,0)",vignette:"rgba(3,8,14,.58)",outlineNear:"rgba(218,238,249,.56)",outlineMid:"rgba(174,215,238,.30)",outlineFar:"rgba(136,194,226,.14)"})}),vr=e=>"number"==typeof e&&Number.isFinite(e),br=(e,t,i)=>Math.min(i,Math.max(t,e));function yr(e){return e.elevation<=-6?"night":e.elevation>=6?"day":e.rising?"dawn":"dusk"}function wr(e){const t=(Math.floor(Number(e)||0)%1440+1440)%1440;return t>=300&&t<480?"dawn":t>=480&&t<1080?"day":t>=1080&&t<1260?"dusk":"night"}function kr(e){const t=yr(e),i=e.azimuth*Math.PI/180;return{sunX:50-42*Math.sin(i),sunY:78-br(e.elevation,0,90)/90*64,sunOpacity:"night"===t?0:br((e.elevation+6)/12,0,1)}}function xr(e){const t=(Math.floor(Number(e)||0)%1440+1440)%1440;if(t<300||t>=1260)return{sunX:50,sunY:78,sunOpacity:0};const i=(t-300)/960,s=(t-300)/120,n=(1260-t)/120;return{sunX:8+84*i,sunY:78-64*Math.sin(i*Math.PI),sunOpacity:Math.max(.18,Math.min(s,n,1))}}function $r(e,t=new Date){const i=function(e){const t=e?.states?.["sun.sun"]?.attributes;return t&&vr(t.azimuth)&&vr(t.elevation)&&"boolean"==typeof t.rising?{azimuth:mr(t.azimuth),elevation:t.elevation,rising:t.rising}:null}(e);if(i)return{phase:yr(i),source:"sun",...kr(i)};const s="number"==typeof t?t:function(e=new Date){return 60*e.getHours()+e.getMinutes()}(t);return{phase:wr(s),source:"clock",...xr(s)}}function Sr(e){return`${e.source}|${e.phase}|${e.sunX.toFixed(2)}|${e.sunY.toFixed(2)}|`+e.sunOpacity.toFixed(3)}const Mr=[[-90,"#070c14"],[-12,"#070c14"],[-4,"#131a28"],[0,"#4a3527"],[10,"#e8ddcf"],[30,"#ffffff"],[90,"#ffffff"]],Cr=e=>Math.min(1,Math.max(0,e));function Dr(e,t,i=6){const s=e.angle*Math.PI/180,n=[Math.sin(s),-Math.cos(s)],o=s=>{const o=[e.x+n[0]*i*s,e.y+n[1]*i*s];return t.find(e=>e.poly.length>=3&&vi(o,e.poly))||null},a=o(1),r=o(-1);return a&&r?null:a||r?a?{normal:[-n[0],-n[1]],roomId:a.id}:{normal:n,roomId:r.id}:null}function Pr(e,t,i){return i>0&&e[0]*t[0]+e[1]*t[1]>.05}function Tr(e,t,i,s){return[[e[0],e[1]],[t[0],t[1]],[t[0]+i[0]*s,t[1]+i[1]*s],[e[0]+i[0]*s,e[1]+i[1]*s]]}function Ir(e,t){try{const i=ni([[...e.map(e=>[e[0],e[1]]),[e[0][0],e[0][1]]]],[[...t.map(e=>[e[0],e[1]]),[t[0][0],t[0][1]]]]),s=[];for(const e of i){const t=e?.[0];!Array.isArray(t)||t.length<4||s.push(t.slice(0,t.length-1).map(e=>[e[0],e[1]]))}return s}catch{return[]}}function Rr(e,t,i,s,n,o,a){if(!(s>0))return[];const r=gr(i,n),l=[-r[0],-r[1]],c=function(e){const t=Math.min(90,Math.max(0,e));return.7*(.8+1.7*Math.pow(1-t/90,1.6))}(s),h=[];for(const i of t){if(!(i.length>0))continue;const t=Dr(i,e);if(!t||!Pr(t.normal,r,s))continue;const n=e.find(e=>e.id===t.roomId);if(!n)continue;const d=o&&o[t.roomId]||n.poly,p=i.angle*Math.PI/180,u=i.length/2,_=[-t.normal[0],-t.normal[1]],m=Math.max(0,a?.[i.id]||0),g=i.x+_[0]*m/2,f=i.y+_[1]*m/2,v=Math.cos(p)*u,b=Math.sin(p)*u,y=[g-v,f-b],w=[g+v,f+b],k=c*i.length,x=Ir(Tr(y,w,l,k),d);if(!x.length)continue;const $=l[0]*_[0]+l[1]*_[1];h.push({openingId:i.id,roomId:t.roomId,polys:x,a:y,b:w,dir:l,len:k,normal:_,depth:k*$})}return h}const Ar=e=>"number"==typeof e&&Number.isInteger(e)&&e>=0&&e<=359?e:null;function zr(e,t){const i=Ar(t?.north_deg);return null!==i?i:Ar(e?.north_deg)}function Fr(e,t){const i=e=>"static"===e||"daynight"===e?e:null;return i(t?.bg_mode)??i(e?.bg_mode)??"static"}function Nr(e,t){const i=t?.sun_rays;return"boolean"==typeof i?i:!0===e?.sun_rays}function Er(e){const t=e?.states?.["sun.sun"]?.attributes,i=Number(t?.azimuth),s=Number(t?.elevation);return Number.isFinite(i)&&Number.isFinite(s)?{azimuth:i,elevation:s}:null}const Or=["dawn","day","dusk","night"];function Hr(e){if(!e)return"";const t=fr[e.phase];return[`--hp-day-cycle-outline-near:${t.outlineNear}`,`--hp-day-cycle-outline-mid:${t.outlineMid}`,`--hp-day-cycle-outline-far:${t.outlineFar}`].join(";")}function Lr(e,t=1){if(!e)return G;const i=[`--hp-day-cycle-sun-x:${e.sunX.toFixed(2)}%`,`--hp-day-cycle-sun-y:${e.sunY.toFixed(2)}%`,`--hp-day-cycle-sun-opacity:${e.sunOpacity.toFixed(3)}`,`opacity:${Math.min(1,Math.max(0,t)).toFixed(4)}`].join(";");return q``}const Br={color:"#607d8b",opacity:1,widthCm:3.6,fill:!1,fillColor:"#607d8b",fillOpacity:.25},jr=(e,t=1)=>{const i=Number(e);return Number.isFinite(i)?Math.min(1,Math.max(0,i)):t},qr=e=>{let t=Number(e);return Number.isFinite(t)?(t=(t%360+360)%360,t>180?t-360:t):0};function Wr(e,t,i){return qr(t)?[e.x,e.y]:i([e.x,e.y])}const Ur=(e,t,i)=>{const s=Number(e),n=Number.isFinite(t)&&t>0?t:5;return Number.isFinite(s)&&s>0?s/n*i:0},Gr=(e,t,i)=>{const s=Number(e),n=Number.isFinite(t)&&t>0?t:5;return Number.isFinite(s)&&i>0?s/i*n:0},Vr=(e,t,i,s=Br.widthCm)=>{const n=Number(e?.width_cm);if(Number.isFinite(n)&&n>0)return n;const o=Number(e?.width);return Number.isFinite(o)&&o>0?Gr(o,t,i):s};function Kr(e,t){return{color:e.color,opacity:jr(e.opacity),width_cm:Math.max(.1,Math.min(100,Number(e.widthCm)||.1)),...t?{fill:e.fill,fill_color:e.fillColor,fill_opacity:jr(e.fillOpacity,.25)}:{}}}function Yr(e){const t=e.x+e.w/2,i=e.y+e.h/2,s=qr(e.angle)*Math.PI/180,n=Math.cos(s),o=Math.sin(s),a=(e,s)=>{const a=e-t,r=s-i;return[t+a*n-r*o,i+a*o+r*n]};return[a(e.x,e.y),a(e.x+e.w,e.y),a(e.x+e.w,e.y+e.h),a(e.x,e.y+e.h)]}function Jr(e){const t=Yr(e),i=(e,t)=>[(e[0]+t[0])/2,(e[1]+t[1])/2],s=[(t[0][0]+t[2][0])/2,(t[0][1]+t[2][1])/2];return{points:[...t,i(t[0],t[1]),i(t[1],t[2]),i(t[2],t[3]),i(t[3],t[0]),s],segments:t.map((e,i)=>({a:e,b:t[(i+1)%4]}))}}function Xr(e,t,i,s,n,o,a,r){const l=qr(e.angle)*Math.PI/180,c=Math.cos(l),h=Math.sin(l),d=-Math.sin(l),p=Math.cos(l),u=e.x+e.w/2,_=e.y+e.h/2,m=t>0?-e.w/2:e.w/2,g=i>0?-e.h/2:e.h/2,f=u+m*c+g*d,v=_+m*h+g*p,b=s-f,y=n-v;let w=(b*c+y*h)*(t>0?1:-1),k=(b*d+y*p)*(i>0?1:-1),x=null;if(o){const t=w/Math.max(e.w,r),i=k/Math.max(e.h,r);x=Math.max(r/Math.max(e.w,r),r/Math.max(e.h,r),t,i),w=e.w*x,k=e.h*x}if(a>0)if(o){const t=Math.max(1,Math.round(e.w/a)),i=Math.max(1,Math.round(e.h/a));if(Math.abs(e.w-t*a)<1e-6&&Math.abs(e.h-i*a)<1e-6){const s=(e,t)=>{let i=Math.abs(e),s=Math.abs(t);for(;s;)[i,s]=[s,i%s];return Math.max(1,i)},n=1/s(t,i),o=Math.max(r/e.w,r/e.h),a=Math.max(o,Math.round((x??1)/n)*n);w=e.w*a,k=e.h*a}else{e.w>=e.h?(w=Math.round(w/a)*a,k=w*(e.h/Math.max(e.w,r))):(k=Math.round(k/a)*a,w=k*(e.w/Math.max(e.h,r)))}}else w=Math.round(w/a)*a,k=Math.round(k/a)*a;w=Math.max(r,w),k=Math.max(r,k);return{x:f+(t>0?w/2:-w/2)*c+(i>0?k/2:-k/2)*d-w/2,y:v+(t>0?w/2:-w/2)*h+(i>0?k/2:-k/2)*p-k/2,w:w,h:k,angle:qr(e.angle)||void 0}}const Zr=(e,t,i)=>{const s=i[0]-t[0],n=i[1]-t[1],o=s*s+n*n;if(o<1e-12)return[...t];const a=Math.max(0,Math.min(1,((e[0]-t[0])*s+(e[1]-t[1])*n)/o));return[t[0]+s*a,t[1]+n*a]};const Qr=10**9,el=240,tl=1e-4;function il(e){if(Array.isArray(e))return e.map(e=>il(e));if(null!==e&&"object"==typeof e){const t={};for(const[i,s]of Object.entries(e))t[i]=il(s);return t}return e}function sl(e){if("number"!=typeof e||!Number.isFinite(e))return e;const t=(e<0||Object.is(e,-0)?-1:1)*(Math.floor(Math.abs(e)*Qr+.5)/Qr);return 0===t?0:t}function nl(e){if("number"!=typeof e||!Number.isFinite(e))return e;const t=e*el,i=Math.round(t);if(Math.abs(t-i)null!==al(e)):[]}function ll(e,t){for(const i of t)Object.prototype.hasOwnProperty.call(e,i)&&(e[i]=sl(e[i]))}function cl(e,t){for(const i of t)Object.prototype.hasOwnProperty.call(e,i)&&(e[i]=nl(e[i]))}function hl(e){if(Array.isArray(e))for(let t=0;t{for(const n of s){const s=t[n];"number"==typeof s&&Number.isFinite(s)&&i(s,e)}},s=t=>{if(Array.isArray(t))for(let s=0;s{if(Array.isArray(e))for(const t of e)s(t)};for(const i of rl(e.rooms))t(i,["x","y","w","h"]),n(i.poly);for(const t of rl(e.walls))s(t.a),s(t.b);for(const t of rl(e.wall_segments))s(t.a),s(t.b);for(const i of rl(e.openings))t(i,["x","y"]);for(const i of rl(e.decor))"line"===i.kind?t(i,["x1","y1","x2","y2"]):"rect"===i.kind||"ellipse"===i.kind||"furniture"===i.kind?t(i,["x","y","w","h"]):"text"===i.kind&&t(i,["x","y"]);for(const t of rl(e.room_drafts))n(t.points);for(const t of rl(e.partitions))s(t.a),s(t.b);for(const t of rl(e.wall_columns))s(t.center);for(const t of rl(e.open_spans))s(t.a),s(t.b)}const n=al(t);if(!n)return;const o=new Map;if(s)for(const e of rl(s.spaces))null!=e.id&&o.set(String(e.id),e);for(const e of Object.values(n)){const t=al(e);if(!t)continue;const s=null!=t.s&&o.get(String(t.s))||null;for(const e of["x","y"]){const n=t[e];"number"==typeof n&&Number.isFinite(n)&&i(n,s)}}}function ul(e){const t=Number(e?.cell_cm);return t>0?t:5}function _l(e,t={}){const i={canonicalized:0,far:0,maxShift:0,maxShiftCm:0,spaces:[],bySpace:new Map},s=rl(al(e)?.spaces);let n=5;for(const e of s)n=Math.max(n,ul(e));pl(e,t,(e,t)=>{const s=e*el,o=Math.abs(s-Math.round(s)),a=nl(e),r=o>0&&o=tl))return;const l=r&&"number"==typeof a?Math.abs(a-e):0;if(r?i.canonicalized++:i.far++,i.maxShift=Math.max(i.maxShift,l),i.maxShiftCm=Math.max(i.maxShiftCm,l*el*(t?ul(t):n)),!t?.id||!r)return;const c=String(t.id);let h=i.bySpace.get(c);h||(h={spaceId:c,space:String(t.title||c),canonicalized:0,far:0,maxShift:0,maxShiftCm:0},i.bySpace.set(c,h)),h.canonicalized++,h.maxShift=Math.max(h.maxShift,l),h.maxShiftCm=Math.max(h.maxShiftCm,l*el*ul(t))}),i.bySpace.size&&pl(e,t,(e,t)=>{if(!t?.id)return;const s=i.bySpace.get(String(t.id));if(!s)return;const n=e*el;Math.abs(n-Math.round(n))>=tl&&s.far++}),i.spaces=[...i.bySpace.values()];const{bySpace:o,...a}=i;return a}function ml(e){return function(e){const t=e,i=al(t);i&&cl(i,["x","y"]);return t}(il(e))}function gl(e){const t=e,i=al(t);if(!i)return t;for(const e of Object.values(i)){const t=al(e);t&&cl(t,["x","y"])}return t}function fl(e){const t=e,i=al(t);if(!i)return t;for(const e of rl(i.spaces)){ll(e,["plan_x","plan_y","plan_scale","plan_scale_x","plan_scale_y","plan_angle"]);for(const t of rl(e.rooms))cl(t,["x","y","w","h"]),dl(t.poly);for(const t of rl(e.walls))hl(t.a),hl(t.b);for(const t of rl(e.wall_segments))hl(t.a),hl(t.b);for(const t of rl(e.openings)){cl(t,["x","y"]),ll(t,["angle","length"]);const e=al(t.host);e&&ll(e,["t"])}for(const t of rl(e.decor))"line"===t.kind?cl(t,["x1","y1","x2","y2"]):"rect"===t.kind||"ellipse"===t.kind||"furniture"===t.kind?(cl(t,["x","y","w","h"]),ll(t,["angle"])):"text"===t.kind&&(cl(t,["x","y"]),ll(t,["scale","angle"]));for(const t of rl(e.room_drafts))dl(t.points);for(const t of rl(e.partitions))hl(t.a),hl(t.b);for(const t of rl(e.wall_columns))hl(t.center),"square"===t.shape&&ll(t,["angle"]);for(const t of rl(e.open_spans))hl(t.a),hl(t.b)}for(const e of rl(i.markers))ll(e,["angle"]);return t}const vl=15;function bl(e,t,i,s,n){const o=i[0]-e[0],a=i[1]-e[1],r=Math.hypot(o,a);if(!(r>0)||r<=n+1e-9)return null;const l=o/r,c=a/r,h=t=>{const s=(t[0]-e[0])*l+(t[1]-e[1])*c,o=(n-s)/(r-s);return!Number.isFinite(o)||o<-1e-9||o>1+1e-9?null:[t[0]+(i[0]-t[0])*o,t[1]+(i[1]-t[1])*o]},d=h(t),p=h(s);return d&&p?[[e[0],e[1]],t,d,p,s]:null}const yl=Math.sin(.25*Math.PI/180);function wl(e,t){return Number.isFinite(e)&&e>0&&Number.isFinite(t)&&t>0&&e*t<3}function kl(e){const t=Number(e)>0?Number(e):5;if(5===t)return 8;const i=5/t*8;return Math.min(80,Math.max(.5,i))}function xl(e,t){return Number.isFinite(e)&&e>0&&Number.isFinite(t)&&t>0&&e*t<2}function $l(e){return Number.isFinite(e)?Math.max(1,Math.min(100,e)):1}function Sl(e,t){return!Number.isFinite(e)||e<0?"":String(t?Math.round(e/2.54*100)/100:Math.round(100*e)/100)}function Ml(e,t,i){if(!Number.isFinite(e)||e<=0)return 0;const s=Number(t)>0?Number(t):5;return $l(e)/s*i}function Cl(e,t){return t>0&&Number.isFinite(e)?Math.round(e/t)*t:e}function Dl(e){return Math.max(1e-6*Math.abs(e),1e-9)}function Pl(e,t){if(!(t>0&&Number.isFinite(e)))return e;const i=Cl(e,t);return Math.abs(i-e)<=Dl(t)?i:e}function Tl(e,t){let i=t[0]-e[0],s=t[1]-e[1];const n=Math.hypot(i,s);return n<1e-12?[1,0]:(i/=n,s/=n,(i<-1e-12||Math.abs(i)<=1e-12&&s<0)&&(i=-i,s=-s),[i,s])}function Il(e,t,i){const s=[Pl(e[0],i),Pl(e[1],i)],n=[Pl(t[0],i),Pl(t[1],i)],o=Cl((s[0]+n[0])/2,i),a=Cl((s[1]+n[1])/2,i),[r,l]=Tl(s,n);let c=Math.atan2(l,r);c<0&&(c+=Math.PI);const h=Math.round(1800*c)/1800,d=i>0&&i<.01?6:i<1?4:2;return`${o.toFixed(d)},${a.toFixed(d)}@${h.toFixed(4)}`}function Rl(e,t,i,s){return 1===s?Il(e,t,i):Il([e[0]/s,e[1]/s],[t[0]/s,t[1]/s],i)}function Al(e,t){if(!Array.isArray(e.a)||!Array.isArray(e.b)||e.a.length<2||e.b.length<2)return null;const i=[Number(e.a[0]),Number(e.a[1]),Number(e.b[0]),Number(e.b[1])];if(!i.every(Number.isFinite))return null;const s=t>0?t:1;return[[i[0]*s,i[1]*s],[i[2]*s,i[3]*s]]}function zl(e,t,i,s,n){const o=n>0?n:1;return{key:Rl(e,t,s,o),cm:$l(i),a:[e[0]/o,e[1]/o],b:[t[0]/o,t[1]/o]}}function Fl(e,t){const i=t>0?t:1,s=[];for(const t of e){const e=t.key.lastIndexOf("@");if(e<0)continue;const[n,o]=t.key.slice(0,e).split(",").map(Number),a=Number(t.key.slice(e+1));[n,o,a].every(Number.isFinite)&&s.push({w:t,x:n*i,y:o*i,ang:a})}return s}function Nl(e,t){const[i,s]=Tl(e,t);let n=Math.atan2(s,i);return n<0&&(n+=Math.PI),n}function El(e,t){let i=Math.abs(e-t);return i>Math.PI/2&&(i=Math.PI-i),i<.02}function Ol(e,t,i,s,n=1){if(!e?.length)return null;const o=Rl(t,i,s,n),a=e.find(e=>e.key===o);if(a)return a;const r=n>0?n:1,l=Dl(s)*r,c=(e,t)=>Math.abs(e[0]-t[0])<=l&&Math.abs(e[1]-t[1])<=l;for(const s of e){const e=Al(s,r);if(e&&(c(e[0],t)&&c(e[1],i)||c(e[0],i)&&c(e[1],t)))return s}const h=(t[0]+i[0])/2,d=(t[1]+i[1])/2,p=Nl(t,i),u=Math.max(.5*s,1e-9)*r;for(const t of Fl(e,r))if(El(t.ang,p)&&Math.hypot(t.x-h,t.y-d)<=u)return t.w;return null}function Hl(e,t,i,s,n=1){const o=Ol(e,t,i,s,n);if(o&&o.cm>0)return $l(o.cm);const a=function(e,t,i,s,n){if(!e?.length)return null;const o=n>0?n:1,a=Math.hypot(i[0]-t[0],i[1]-t[1]);if(a<1e-12)return null;const r=Nl(t,i),l=Math.max(.5*s,1e-9)*o;let c=null;for(const s of e){if(!(s.cm>0))continue;const e=Al(s,o);if(!e)continue;const n=Math.hypot(e[1][0]-e[0][0],e[1][1]-e[0][1]);if(n<1e-12||!El(Nl(e[0],e[1]),r))continue;if(Sc(t[0],t[1],e[0][0],e[0][1],e[1][0],e[1][1])>l||Sc(i[0],i[1],e[0][0],e[0][1],e[1][0],e[1][1])>l)continue;if(n+l(o.has(e.key)||(e=>{const t=Al(e,s);if(!t)return!1;const[i,o]=t,r=o[0]-i[0],c=o[1]-i[1],h=Math.hypot(r,c);if(h<=l)return!1;if(!a.some(e=>{const t=[e[0],e[1]],s=[e[2],e[3]];return El(Nl(i,o),Nl(t,s))&&Sc(i[0],i[1],t[0],t[1],s[0],s[1])<=l&&Sc(o[0],o[1],t[0],t[1],s[0],s[1])<=l}))return!1;const d=(n||[]).some(e=>{const t=[e[0],e[1]],s=[e[2],e[3]];if(!El(Nl(i,o),Nl(t,s)))return!1;const n=e=>Math.abs((e[0]-i[0])*c-(e[1]-i[1])*r)/h;if(n(t)>l||n(s)>l)return!1;const a=h*h,d=((t[0]-i[0])*r+(t[1]-i[1])*c)/a,p=((s[0]-i[0])*r+(s[1]-i[1])*c)/a;return Math.min(1,Math.max(d,p))-Math.max(0,Math.min(d,p))>l/h});return!d})(e))&&e.cm>=1&&e.cm<=100)}function Bl(e,t,i,s=8){const[n,o]=Tl(e,t);let a=Math.atan2(o,n);a<0&&(a+=Math.PI);let r=i*Math.PI/180%Math.PI;r<0&&(r+=Math.PI);let l=Math.abs(a-r);return l>Math.PI/2&&(l=Math.PI-l),l<=s*Math.PI/180}function jl(e,t,i,s,n=1,o="affine",a){if(!e?.length)return[];if(t.length!==i.length)return"fixed-topology"===o&&a?.(),e.slice();const r=n>0?n:1,l=Math.max(.5*s,1e-9)*r,c=Math.max(s*r*1e-6,1e-9),h=[],d=new Map,p=new Map,u=(e,t,i)=>{const s=e.get(t)||new Set;s.add(i),e.set(t,s)};for(let e=0;e[e[0]+(t[0]-e[0])*i,e[1]+(t[1]-e[1])*i],m=(e,t)=>Math.hypot(e[0]-t[0],e[1]-t[1])<=c,g=(e,t)=>{if("fixed-topology"===o){const i=t.na[0]-t.oa[0],s=t.na[1]-t.oa[1],n=t.nb[0]-t.ob[0],o=t.nb[1]-t.ob[1];return Math.hypot(i-n,s-o)<=c?[e[0]+i,e[1]+s]:m(e,t.oa)?[...t.na]:m(e,t.ob)?[...t.nb]:[...e]}const i=Math.max(0,Math.min(1,((e[0]-t.oa[0])*t.dx+(e[1]-t.oa[1])*t.dy)/t.len2));return _(t.na,t.nb,i)},f=(e,t)=>{const[i,s]=Tl(e,t);return(t[0]-e[0])*i+(t[1]-e[1])*s>=0?[[...e],[...t]]:[[...t],[...e]]},v=[],b=[],y=(e,t,i)=>{if(Math.hypot(t[0]-e[0],t[1]-e[1])<=c)return;const[n,o]=f(e,t),a=$l(i);if(b.some(e=>e.entry.cm===a&&m(e.span[0],n)&&m(e.span[1],o)))return;const l=zl(n,o,a,s,r);v.push(l),b.push({entry:l,span:[n,o]})};for(const t of e){const e=Al(t,r);if(e){const[i,s]=f(e[0],e[1]),n=s[0]-i[0],a=s[1]-i[1],r=n*n+a*a,d=Math.sqrt(r);if(d<=c){v.push({...t,cm:$l(t.cm)});continue}const p=[];for(const e of h){if(!El(Nl(i,s),Nl(e.oa,e.ob)))continue;const t=e=>Math.abs((e[0]-i[0])*a-(e[1]-i[1])*n)/d;if(t(e.oa)>l||t(e.ob)>l)continue;const o=((e.oa[0]-i[0])*n+(e.oa[1]-i[1])*a)/r,h=((e.ob[0]-i[0])*n+(e.ob[1]-i[1])*a)/r,u=Math.max(0,Math.min(o,h)),_=Math.min(1,Math.max(o,h));(_-u)*d>c&&p.push({lo:u,hi:_,move:e})}if(!p.length){"fixed-topology"===o?v.push({...t}):y(i,s,t.cm);continue}const u=[0,1,...p.flatMap(({lo:e,hi:t})=>[e,t])].sort((e,t)=>e-t).filter((e,t,i)=>0===t||Math.abs(e-i[t-1])*d>c),b=[];for(let e=0;e+1r>=e.lo-1e-12&&r<=e.hi+1e-12);if(!l.length){b.push([o,a]);continue}const h=[g(o,l[0].move),g(a,l[0].move)],f=l.slice(1).some(e=>{const t=g(o,e.move),i=g(a,e.move);return!m(h[0],t)||!m(h[1],i)});b.push(f?[o,a]:h)}const w=(e,t)=>{const i=e[1][0]-e[0][0],s=e[1][1]-e[0][1],n=t[1][0]-t[0][0],o=t[1][1]-t[0][1],a=Math.hypot(i,s);return!(a<=c||i*n+s*o<=0)&&Math.abs(i*o-s*n)/a<=c},k=[];for(const e of b){const t=k[k.length-1];t&&m(t[1],e[0])&&w(t,e)?t[1]=e[1]:k.push([[...e[0]],[...e[1]]])}if("fixed-topology"===o&&1===k.length){const[i,s]=k[0];if(m(i,e[0])&&m(s,e[1])||m(i,e[1])&&m(s,e[0])){v.push({...t});continue}}for(const[e,i]of k)y(e,i,t.cm);continue}if("fixed-topology"===o){const e=p.get(t.key);if(1===e?.size){const i=[...e][0];v.push(i===t.key?{...t}:{...t,key:i});continue}const i=[Fl([t],r)[0]];1!==r&&i.push(Fl([t],1)[0]);const s=i.filter(Boolean).some(e=>h.some(t=>{if(!El(e.ang,Nl(t.oa,t.ob)))return!1;const i=((e.x-t.oa[0])*t.dx+(e.y-t.oa[1])*t.dy)/t.len2;return i>=-1e-6&&i<=1.000001&&Sc(e.x,e.y,t.oa[0],t.oa[1],t.ob[0],t.ob[1])<=l}));((e?.size||0)>1||s)&&a?.(),v.push({...t});continue}let i="";const n=d.get(t.key);if(1===n?.size&&(i=[...n][0]),!i){const e=Fl([t],r)[0];if(e){const t=new Set;for(const i of h){if(!El(e.ang,Nl(i.oa,i.ob)))continue;const n=((e.x-i.oa[0])*i.dx+(e.y-i.oa[1])*i.dy)/i.len2;if(n<-1e-6||n>1.000001)continue;if(Sc(e.x,e.y,i.oa[0],i.oa[1],i.ob[0],i.ob[1])>l)continue;const o=g([e.x,e.y],i),[a,c]=Tl(i.na,i.nb),h=Math.max(s*r,1e-6);t.add(Rl([o[0]-a*h,o[1]-c*h],[o[0]+a*h,o[1]+c*h],s,r))}1===t.size&&(i=[...t][0])}}v.push({...t,key:i||t.key,cm:$l(t.cm)})}return v}function ql(e,t,i,s,n=1,o="affine"){return jl(e,t,i,s,n,o)}function Wl(e,t,i,s=1,n=[]){const o=s>0?s:1,a=Math.abs(i),r=Math.max(a*o*tl,1e-9),l=(n||[]).flatMap(e=>{const t=Al(e,o);return t?t.map(e=>[...e]):[]}),c=e=>e.every((e,t)=>(e=>{if(!(a>0))return!0;const t=e/o/a;return Math.abs(t-Math.round(t))Math.abs(e-i[t])<=r)),h=[],d=e=>JSON.stringify([e.key,e.cm,e.a,e.b]);for(const i of e||[]){const e=Al(i,o);if(!e)continue;const[s,n]=e;if(![s[0],s[1],n[0],n[1]].every(Number.isFinite)||!c(s)||!c(n)){h.push(d(i));continue}const a=n[0]-s[0],l=n[1]-s[1],p=Math.hypot(a,l);if(p<=r){h.push(d(i));continue}const u=a/p,_=l/p,m=[];for(const e of t){const[t,i]=e;if(![t?.[0],t?.[1],i?.[0],i?.[1]].every(Number.isFinite))continue;const n=e=>Math.abs((e[0]-s[0])*_-(e[1]-s[1])*u);if(n(t)>r||n(i)>r)continue;const o=(t[0]-s[0])*u+(t[1]-s[1])*_,a=(i[0]-s[0])*u+(i[1]-s[1])*_,l=Math.max(0,Math.min(o,a)),c=Math.min(p,Math.max(o,a));c-l>r&&m.push([l,c])}m.sort((e,t)=>e[0]-t[0]||e[1]-t[1]);let g=0;for(const[e,t]of m){if(e>g+r)break;if(g=Math.max(g,t),g>=p-r)break}ge.key!==a);return null==s||s<1?r:[...r,zl(t,i,s,n,o)]}function Gl(e,t,i,s,n,o=[],a=1){let r=e?e.slice():[];for(const e of function(e,t,i,s,n=1,o=[]){const a=dc(e,t,i,s,n,o);if(!a)return[];const r=[];for(let e=0;e0||(c=Ul(c,t,i,s,n,a)))}return c}function Kl(e){const{a:t,b:i,halfDepth:s}=e;if(!Array.isArray(t)||!Array.isArray(i)||t.length<2||i.length<2||![t[0],t[1],i[0],i[1]].every(Number.isFinite))return null;const n=i[0]-t[0],o=i[1]-t[1],a=Math.hypot(n,o);if(!(a>1e-9&&s>0&&Number.isFinite(s)))return null;const r=-o/a*s,l=n/a*s;return[[t[0]+r,t[1]+l],[i[0]+r,i[1]+l],[i[0]-r,i[1]-l],[t[0]-r,t[1]-l]]}function Yl(e,t,i){return Math.hypot(e[0]-t[0],e[1]-t[1])<=i}function Jl(e,t,i){const s=t.b[0]-t.a[0],n=t.b[1]-t.a[1],o=s*s+n*n;if(!(o>i*i))return!1;const a=((e[0]-t.a[0])*s+(e[1]-t.a[1])*n)/o;if(!(a>0&&a<1))return!1;const r=[t.a[0]+s*a,t.a[1]+n*a];return Math.hypot(e[0]-r[0],e[1]-r[1])<=i}function Xl(e,t,i,s){const n=Math.hypot(t,i);if(!(n>1e-9&&s>0))return;const o=[t/n,i/n],a=e.find(e=>Math.abs(e.u[0]*o[1]-e.u[1]*o[0])<1e-9&&e.u[0]*o[0]+e.u[1]*o[1]>1-1e-9);a?a.halfDepth=Math.max(a.halfDepth,s):e.push({u:o,halfDepth:s})}function Zl(e,t){try{const i=oi(Dc(e),Dc(t));let s=null,n=0;for(const e of i||[]){const t=e?.[0]||[],i=Math.abs(nc(t));t.length>=4&&i>n&&(n=i,s=t.slice(0,-1).map(e=>[e[0],e[1]]))}return s}catch{return null}}function Ql(e,t=1e-6){const i=(e||[]).map((e,t)=>({segment:e,index:t})).filter(({segment:e})=>e&&Array.isArray(e.a)&&Array.isArray(e.b)&&e.a.length>=2&&e.b.length>=2&&e.a.every(Number.isFinite)&&e.b.every(Number.isFinite)&&Number.isFinite(e.halfDepth)&&e.halfDepth>0&&Math.hypot(e.b[0]-e.a[0],e.b[1]-e.a[1])>1e-9);if(i.length<2)return[];const s=Math.max(Number.isFinite(t)?t:0,1e-9),n=i.flatMap(({segment:e})=>[e.a,e.b]).map(e=>[e[0],e[1]]).sort((e,t)=>e[0]-t[0]||e[1]-t[1]),o=[];for(const e of n)o.some(t=>Yl(t,e,s))||o.push(e);const a=[];for(const e of o){const t=[];let n=!1;for(const{segment:o,index:a}of i){const i=Math.hypot(o.b[0]-o.a[0],o.b[1]-o.a[1]);Yl(e,o.a,s)?t.push({u:[(o.b[0]-o.a[0])/i,(o.b[1]-o.a[1])/i],halfDepth:o.halfDepth,length:i,index:a}):Yl(e,o.b,s)?t.push({u:[(o.a[0]-o.b[0])/i,(o.a[1]-o.b[1])/i],halfDepth:o.halfDepth,length:i,index:a}):Jl(e,o,s)&&(n=!0)}if(n||2!==t.length)continue;const[o,r]=t,l=o.u[0]*r.u[1]-o.u[1]*r.u[0];if(Math.abs(l)<1e-9)continue;const c=l<0?1:-1,h=[-o.u[1],o.u[0]],d=[-r.u[1],r.u[0]],p=[e[0]+h[0]*o.halfDepth*c,e[1]+h[1]*o.halfDepth*c],u=[e[0]-d[0]*r.halfDepth*c,e[1]-d[1]*r.halfDepth*c];if(!rc(p,o.u,u,r.u))continue;const _=[[o,r,u,[d[0]*-c,d[1]*-c]],[r,o,p,[h[0]*c,h[1]*c]]];for(const[t,,i,n]of _){const o=Math.min(2*t.halfDepth,t.length),r=[-t.u[1]*t.halfDepth,t.u[0]*t.halfDepth],l=[[e[0]+r[0],e[1]+r[1]],[e[0]+t.u[0]*o+r[0],e[1]+t.u[1]*o+r[1]],[e[0]+t.u[0]*o-r[0],e[1]+t.u[1]*o-r[1]],[e[0]-r[0],e[1]-r[1]]],c=e=>(e[0]-i[0])*n[0]+(e[1]-i[1])*n[1],h=[];for(let e=0;e=-1e-12&&h.push(t),s>1e-12&&n<-1e-12||s<-1e-12&&n>1e-12){const e=s/(s-n);h.push([t[0]+(i[0]-t[0])*e,t[1]+(i[1]-t[1])*e])}}h.length>=3&&Math.abs(nc(h))>s*s&&a.push({segmentIndex:t.index,wedge:h})}}return a}function ec(e,t=1e-6){const i=(e||[]).filter(e=>e&&Array.isArray(e.a)&&Array.isArray(e.b)&&e.a.length>=2&&e.b.length>=2&&e.a.every(Number.isFinite)&&e.b.every(Number.isFinite)&&Number.isFinite(e.halfDepth)&&e.halfDepth>0&&Math.hypot(e.b[0]-e.a[0],e.b[1]-e.a[1])>1e-9);if(i.length<2)return[];const s=Math.max(Number.isFinite(t)?t:0,1e-9),n=i.flatMap(e=>[e.a,e.b]).map(e=>[e[0],e[1]]).sort((e,t)=>e[0]-t[0]||e[1]-t[1]),o=[];for(const e of n)o.some(t=>Yl(t,e,s))||o.push(e);const a=[],r=i.map((e,t)=>({roomId:"",a:[e.a[0],e.a[1]],b:[e.b[0],e.b[1]],key:`join-${t}`,kind:"outer",cm:0,open:!1,half:e.halfDepth})),l=gc(r,s);for(const e of fc(l).fans)a.push(e);const c=e=>!!vc(l,e);for(const e of o){if(c(e))continue;const t=[];for(const n of i)Yl(e,n.a,s)?Xl(t,n.b[0]-n.a[0],n.b[1]-n.a[1],n.halfDepth):Yl(e,n.b,s)?Xl(t,n.a[0]-n.b[0],n.a[1]-n.b[1],n.halfDepth):Jl(e,n,s)&&(Xl(t,n.a[0]-e[0],n.a[1]-e[1],n.halfDepth),Xl(t,n.b[0]-e[0],n.b[1]-e[1],n.halfDepth));if(!(t.length<2)){t.sort((e,t)=>Math.atan2(e.u[1],e.u[0])-Math.atan2(t.u[1],t.u[0])||e.halfDepth-t.halfDepth);for(let i=0;is*s&&a.push(m)}}}return a}function tc(e,t,i,s){if(!(t>0)||!e||e.length<2)return"";if(i&&e.length>=3){let i=e;const n=e[e.length-1];if(e.length>=4&&Math.hypot(e[0][0]-n[0],e[0][1]-n[1])<1e-9&&(i=e.slice(0,-1)),i.length>=3){const e=i.map((e,i)=>s?.[i]||t),n=Vc(i,e),o=lc(i,e);if(n&&o)return`${cc(n)} ${cc(hc(o))}`}}const n=[];for(let i=0;i=1e-9&&r>0&&n.push({a:o,b:a,halfDepth:r})}const o=n.map(Kl);for(const{segmentIndex:e,wedge:t}of Ql(n)){const i=o[e];if(!i)continue;const s=Zl(i,t);s&&(o[e]=s)}const a=[...o.filter(e=>!!e),...ec(n)],r=function(e){let t=null;try{for(const i of e){if(i.length<3)continue;const e=Dc(i);t=t?si(t,e):[e]}return t}catch{return null}}(a);return r?Lc(r):a.map(e=>cc(e)).join(" ")}function ic(e,t,i,s,n=1){if(!i.length)return!1;const o=sc(s,n),a=(e[0]+t[0])/2,r=(e[1]+t[1])/2,[l,c]=Tl(e,t);for(const e of i){const[t,i]=Tl([e[0],e[1]],[e[2],e[3]]);if(!(Math.abs(l*i-c*t)>.05)&&Sc(a,r,e[0],e[1],e[2],e[3])<=o)return!0}return!1}function sc(e,t){return Math.max(e*(t>0?t:1)*.04,1e-9)}function nc(e){let t=0;for(let i=0;ie[1]!=l>e[1]&&e[0]<(r-o)*(e[1]-a)/(l-a+0)+o&&(i=!i)}return i}([c[0]+.001*r,c[1]+.001*l],e)||(r=-r,l=-l),[r,l]}function ac(e,t){const i=e[0]*t[1]-e[1]*t[0],s=e[0]*t[0]+e[1]*t[1];return Math.abs(i)<1e-9&&s>0}function rc(e,t,i,s){const n=t[0]*s[1]-t[1]*s[0];if(Math.abs(n)<1e-12)return null;const o=[i[0]-e[0],i[1]-e[1]],a=(o[0]*s[1]-o[1]*s[0])/n;return[e[0]+a*t[0],e[1]+a*t[1]]}function lc(e,t,i){const s=e?.length||0;if(s<3||t.length!==s)return null;if(t.every(e=>!(e>0)))return e.map(e=>[e[0],e[1]]);const n=[];for(let o=0;o0||p>0)){n.push([e[o][0],e[o][1]]);continue}if(d>0!=p>0){const t=e[o],i=d>0?[t[0]+u*d,t[1]+_*d]:[t[0],t[1]],s=p>0?[t[0]+m*p,t[1]+g*p]:[t[0],t[1]];n.push(i),Math.hypot(s[0]-i[0],s[1]-i[1])>1e-9&&n.push(s);continue}if(ac(w,k)){const t=e[o],i=[t[0]+u*d,t[1]+_*d],s=[t[0]+m*p,t[1]+g*p];n.push(i),Math.hypot(s[0]-i[0],s[1]-i[1])>1e-9&&n.push(s);continue}const S=rc(x,w,$,k),M=Math.max(d,p,1e-9),C=vc(i,e[o])?.limit??4*M;if(S){const i=Math.hypot(S[0]-e[o][0],S[1]-e[o][1]);if(Number.isFinite(i)&&i<=C){n.push(S);continue}if(jc(e,t,o)){n.push(S);continue}}d>0&&n.push([e[o][0]+u*d,e[o][1]+_*d]),p>0&&n.push([e[o][0]+m*p,e[o][1]+g*p]),d>0||p>0||n.push([e[o][0],e[o][1]])}return n.length>=3?n:null}function cc(e,t=!0){if(!e.length)return"";let i=`M ${e[0][0]} ${e[0][1]}`;for(let t=1;te?.id===t),r=hi(a);if(!r||r.length<3)return null;const l=sc(s,n),c=[];for(const i of e||[]){if(!i||i.id===t)continue;const e=hi(i);if(e)for(const t of Cs(r,e,l))c.push([t[0],t[1]],[t[2],t[3]])}for(const e of i||[])c.push([e[0],e[1]],[e[2],e[3]]);for(const e of o||[]){const t=Al(e,n);t&&c.push(t[0],t[1])}const h=[],d=[];for(let e=0;el)continue;const a=((e[0]-t[0])*(i[0]-t[0])+(e[1]-t[1])*(i[1]-t[1]))/(s*s);a<=n||a>=1-n||(o.some(e=>Math.abs(e-a)*s<=2*l)||o.push(a))}o.sort((e,t)=>e-t);for(const s of o)h.push([t[0]+(i[0]-t[0])*s,t[1]+(i[1]-t[1])*s]),d.push(e)}return{poly:h,parent:d,orig:r}}function pc(e,t,i,s){const n=t.poly.length,o=new Array(n).fill(0);if(!e?.length)return o;const a=new Set,r=[];for(let l=0;l0?(o[l]=$l(c.cm),a.add(c.key)):r.push(l)}if(!r.length)return o;const l=s>0?s:1,c=Math.max(.5*i,1e-9)*l,h=Fl(e,l).filter(e=>e.w.cm>0);for(let e=r.length-1;e>=0;e--){const i=r[e],s=t.poly[i],a=t.poly[(i+1)%n],d=Nl(s,a);let p=null;for(const e of h){const t=Al(e.w,l);if(!t||!El(Nl(t[0],t[1]),d))continue;if(Sc(s[0],s[1],t[0][0],t[0][1],t[1][0],t[1][1])>c||Sc(a[0],a[1],t[0][0],t[0][1],t[1][0],t[1][1])>c)continue;const i=Math.hypot(a[0]-s[0],a[1]-s[1]),n=Math.hypot(t[1][0]-t[0][0],t[1][1]-t[0][1]),o=Math.max(0,n-i);(!p||oc||Sc(n[0],n[1],t[0][0],t[0][1],t[1][0],t[1][1])>c)continue;i=!0,o=Math.max(0,Math.hypot(t[1][0]-t[0][0],t[1][1]-t[0][1])-_)}else{if(Sc(e.x,e.y,s[0],s[1],n[0],n[1])>c)continue;o=Math.hypot(e.x-d,e.y-p)}(!u||i&&!u.exact||i===u.exact&&o0?i:1,o=(e||[]).filter(e=>e&&!e.open&&null!==e.kind&&Number.isFinite(e.half)&&e.half>0&&Array.isArray(e.a)&&Array.isArray(e.b)&&e.a.length>=2&&e.b.length>=2&&[e.a[0],e.a[1],e.b[0],e.b[1]].every(Number.isFinite)&&Math.hypot(e.b[0]-e.a[0],e.b[1]-e.a[1])>s).sort((e,t)=>e.key.localeCompare(t.key)||e.a[0]-t.a[0]||e.a[1]-t.a[1]||e.b[0]-t.b[0]||e.b[1]-t.b[1]||e.half-t.half),a=new Map;for(const e of o){const t=a.get(e.key);t?e.half>t.half&&a.set(e.key,{...t,half:e.half}):a.set(e.key,e)}const r=[...a.values()].flatMap(e=>[{point:[e.a[0],e.a[1]],other:e.b,halfDepth:e.half,kind:e.kind,key:e.key},{point:[e.b[0],e.b[1]],other:e.a,halfDepth:e.half,kind:e.kind,key:e.key}]).sort((e,t)=>e.point[0]-t.point[0]||e.point[1]-t.point[1]||e.other[0]-t.other[0]||e.other[1]-t.other[1]||e.halfDepth-t.halfDepth),l=new Map;for(const e of r){const[t,i]=uc(e.point,s),n=_c(t,i),o=l.get(n)||[];o.push(e),l.set(n,o)}const c=[],h=new Map;for(const e of r){const t=mc(h,e.point,s).filter(t=>Math.hypot(t.point[0]-e.point[0],t.point[1]-e.point[1])<=s).sort((t,i)=>Math.hypot(t.point[0]-e.point[0],t.point[1]-e.point[1])-Math.hypot(i.point[0]-e.point[0],i.point[1]-e.point[1])||t.point[0]-i.point[0]||t.point[1]-i.point[1]);let i=t[0];if(!i){i={point:[...e.point],rays:[]},c.push(i);const[t,n]=uc(i.point,s),o=_c(t,n),a=h.get(o)||[];a.push(i),h.set(o,a)}const n=e.other[0]-e.point[0],o=e.other[1]-e.point[1],a=Math.hypot(n,o);if(!(a>s))continue;const r=[n/a,o/a];let l=Math.atan2(r[1],r[0]);l<0&&(l+=2*Math.PI),i.rays.push({u:r,halfDepth:e.halfDepth,length:a,angle:l})}const d=[],p=e=>{const t=1e-9*Math.max(1,n),i=e.filter(e=>Number.isFinite(e.halfDepth)&&e.halfDepth>0&&Number.isFinite(e.length)&&e.length>s);return i.filter((e,s)=>!i.some((i,n)=>n!==s&&i.halfDepth>=e.halfDepth-t&&i.length>=e.length-t&&(i.halfDepth>e.halfDepth+t||i.length>e.length+t||ne.length-t.length||e.halfDepth-t.halfDepth).map(e=>({...e}))};for(const e of c){const t=e.rays.sort((e,t)=>e.angle-t.angle||e.length-t.length||e.halfDepth-t.halfDepth),i=[];for(const e of t){const t=i[i.length-1];t&&Math.abs(e.angle-t.angle)<=1e-9?t.supports.push({halfDepth:e.halfDepth,length:e.length}):i.push({u:[...e.u],angle:e.angle,supports:[{halfDepth:e.halfDepth,length:e.length}]})}if(i.length>1&&2*Math.PI-i[i.length-1].angle+i[0].angle<=1e-9){const e=i.pop();i[0].supports.push(...e.supports)}if(i.length<3)continue;const n=i.map(t=>{const i=p(t.supports),n=new Set,o=[];for(const a of i){const i=[e.point[0]+t.u[0]*a.length,e.point[1]+t.u[1]*a.length];for(const t of mc(l,i,s)){if("shared"!==t.kind||Math.hypot(t.point[0]-i[0],t.point[1]-i[1])>s)continue;const a=t.other[0]-t.point[0],r=t.other[1]-t.point[1],l=Math.hypot(a,r);if(!(l>s))continue;const c=a/l,h=r/l;if(Math.hypot(t.other[0]-e.point[0],t.other[1]-e.point[1])<=s)continue;const d=`${t.key}|${t.point[0]}|${t.point[1]}|${t.other[0]}|${t.other[1]}|${t.halfDepth}`;n.has(d)||(n.add(d),o.push({start:[t.point[0],t.point[1]],u:[c,h],length:l,halfDepth:t.halfDepth}))}}return o.sort((e,t)=>e.start[0]-t.start[0]||e.start[1]-t.start[1]||e.u[0]-t.u[0]||e.u[1]-t.u[1]||e.length-t.length||e.halfDepth-t.halfDepth),{u:[...t.u],halfDepth:Math.max(...i.map(e=>e.halfDepth)),length:Math.max(...i.map(e=>e.length)),supports:i,continuations:o}}).filter(e=>Number.isFinite(e.halfDepth)&&e.halfDepth>0&&Number.isFinite(e.length)&&e.length>s);if(n.length<3)continue;const o=Math.max(...n.map(e=>e.halfDepth));o>0&&Number.isFinite(o)&&d.push({point:[...e.point],rays:n,halfDepth:o,limit:1.25*o})}d.sort((e,t)=>e.point[0]-t.point[0]||e.point[1]-t.point[1]);const u=new Map;for(const e of d){const[t,i]=uc(e.point,s),n=_c(t,i),o=u.get(n)||[];o.push(e),u.set(n,o)}return{epsilon:s,coordinateScale:n,nodes:d,index:u}}function fc(e){const t={fans:[],supports:[]};if(!e?.nodes?.length)return t;const i=Math.max(e.epsilon,1e-9)**2;for(const s of e.nodes){const e=s.rays.filter(e=>Number.isFinite(e.halfDepth)&&e.halfDepth>0).map(e=>({...e,thickLength:Math.max(...e.supports.filter(t=>t.halfDepth>=e.halfDepth-1e-12).map(e=>e.length),0),angle:(()=>{const t=Math.atan2(e.u[1],e.u[0]);return t<0?t+2*Math.PI:t})()})).sort((e,t)=>e.angle-t.angle);if(e.length<2)continue;const n=s.point;for(const i of e)for(const e of i.supports){if(!(e.halfDepth>0&&e.length>0))continue;const s=-i.u[1]*e.halfDepth,o=i.u[0]*e.halfDepth,a=[n[0]+i.u[0]*e.length,n[1]+i.u[1]*e.length];t.supports.push([[n[0]+s,n[1]+o],[a[0]+s,a[1]+o],[a[0]-s,a[1]-o],[n[0]-s,n[1]-o]])}for(let s=0;s{const e=a.angle-o.angle;return e>0?e:e+2*Math.PI})();if(r<1e-9)continue;const l=r>Math.PI+1e-9,c=4*Math.max(o.halfDepth,a.halfDepth),h=[n[0]-o.u[1]*o.halfDepth,n[1]+o.u[0]*o.halfDepth],d=[n[0]+a.u[1]*a.halfDepth,n[1]-a.u[0]*a.halfDepth],p=o.u[0]*a.u[1]-o.u[1]*a.u[0],u=e=>{let t=Math.atan2(e[1]-n[1],e[0]-n[0])-o.angle;for(;t<0;)t+=2*Math.PI;return t<=r+1e-9};let _=null;if(Math.abs(p)>1e-9){const e=((d[0]-h[0])*a.u[1]-(d[1]-h[1])*a.u[0])/p,t=((d[0]-h[0])*o.u[1]-(d[1]-h[1])*o.u[0])/p,i=[h[0]+o.u[0]*e,h[1]+o.u[1]*e];(l?e<=1e-9&&t<=1e-9:e>1e-9&&e<=o.thickLength&&t<=a.thickLength)&&Math.hypot(i[0]-n[0],i[1]-n[1])<=c&&u(i)&&(_=i)}const m=e=>{Math.abs(nc(e))>i&&t.fans.push(e)};if(_){const e=1.5*Math.max(o.halfDepth,a.halfDepth);m(bl([n[0],n[1]],h,_,d,e)??[[n[0],n[1]],h,_,d]);continue}if(l){m([[n[0],n[1]],h,d]);continue}const g=(e,t)=>Math.min(t,Math.sqrt(Math.max(c**2-e**2,0)),2*Math.max(o.halfDepth,a.halfDepth)),f=[h[0]+o.u[0]*g(o.halfDepth,o.thickLength),h[1]+o.u[1]*g(o.halfDepth,o.thickLength)],v=[d[0]+a.u[0]*g(a.halfDepth,a.thickLength),d[1]+a.u[1]*g(a.halfDepth,a.thickLength)];m([[n[0],n[1]],h,f,v,d])}}return t}function vc(e,t){return!e||!Array.isArray(t)||t.length<2||!t.slice(0,2).every(Number.isFinite)?null:mc(e.index,t,e.epsilon).filter(i=>Math.hypot(i.point[0]-t[0],i.point[1]-t[1])<=e.epsilon).sort((e,i)=>Math.hypot(e.point[0]-t[0],e.point[1]-t[1])-Math.hypot(i.point[0]-t[0],i.point[1]-t[1])||e.point[0]-i.point[0]||e.point[1]-i.point[1])[0]||null}function bc(e,t,i,s,n,o,a){return gc(wc(e,t,i,s,n,o,a),4*sc(s,a),a)}function yc(e,t,i,s,n,o,a,r=1){const l=dc(e,t,s,n,r,i);if(!l)return null;const c=function(e,t,i){const s=(e||[]).find(e=>e?.id===t),n=hi(s);if(!n)return[];const o=[];for(const s of e||[]){if(!s||s.id===t)continue;const e=hi(s);if(e)for(const t of Cs(n,e,i))o.push(t)}return o}(e,t,sc(n,r)),h=function(e,t,i,s,n){const o=sc(s,n),a=[];for(let r=0;rSc(h,d,e[0],e[1],e[2],e[3])<=o);a.push(p?"shared":"outer")}return a}(l.poly,c,s,n,r),d=pc(i,l,n,r),p=d.map((e,t)=>h[t]&&e>0?Ml(e,o,a)/2:0);return{...l,kinds:h,cms:d,offsets:p}}function wc(e,t,i,s,n,o,a=1){const r=[];for(const l of e||[]){if(!l?.id)continue;const c=yc(e,l.id,t,i,s,n,o,a);if(c)for(let e=0;e0&&(r=Ul(r,e.a,e.b,e.cm,s,a));return r}function xc(e,t,i,s,n,o,a=1){if(!t?.length)return[];const r=wc(e,t,i,s,n,o,a),l=new Map;for(const e of r){if(e.open||!e.kind||!e.roomId)continue;const t=l.get(e.key)||new Set;t.add(e.roomId),l.set(e.key,t)}const c=e=>{const t=[...l.get(e)||[]].sort();return 1!==t.length&&2!==t.length?`ambiguous:${e}`:`${1===t.length?"outer":"shared"}:${t.join("|")}`},h=[],d=new Set;for(const e of r)!e.open&&e.cm>0&&!d.has(e.key)&&(d.add(e.key),h.push({...e,ownerSignature:c(e.key)}));const p=[];for(const r of e||[]){if(!r?.id)continue;const l=yc(e,r.id,t,i,s,n,o,a);if(l)for(let e=0;e0)||null===l.kinds[i]){e++;continue}const o=c(Rl(l.poly[i],l.poly[(i+1)%l.poly.length],s,a));let r=e;for(;r+10&&p.push({a:[d[0],d[1]],b:[u[0],u[1]],key:Rl(d,u,s,a),cm:n,len:_,ownerSignature:o}),e=r+1}}}p.sort((e,t)=>t.len-e.len||e.key.localeCompare(t.key));const u=[],_=new Set,m=new Set,g=4*sc(s,a);for(const e of p){const t=h.filter(t=>!m.has(t.key)&&t.cm===e.cm&&t.ownerSignature===e.ownerSignature&&El(Nl(t.a,t.b),Nl(e.a,e.b))&&Sc(t.a[0],t.a[1],e.a[0],e.a[1],e.b[0],e.b[1])<=g&&Sc(t.b[0],t.b[1],e.a[0],e.a[1],e.b[0],e.b[1])<=g);if(t.length){for(const e of t)m.add(e.key);_.has(e.key)||(_.add(e.key),u.push(zl(e.a,e.b,e.cm,s,a)))}}for(const e of h)m.has(e.key)||_.has(e.key)||(_.add(e.key),u.push(zl(e.a,e.b,e.cm,s,a)));return u}function $c(e,t,i,s,n,o,a,r=1){const l=sc(n,r),c=(s[0]+s[2])/2,h=(s[1]+s[3])/2,d=Nl([s[0],s[1]],[s[2],s[3]]);let p=null;for(const s of wc(e,t,i,n,o,a,r)){if(!El(Nl(s.a,s.b),d))continue;const e=Sc(c,h,s.a[0],s.a[1],s.b[0],s.b[1]);e>4*l||(!p||e[e[0],e[1]]),o=Math.abs(nc(n));n.length>=3&&o>i&&(t=n,i=o)}return t}function Cc(e,t,i,s,n,o,a,r=1,l,c){const h=(e||[]).find(e=>e?.id===t),d=hi(h);if(!d||d.length<3)return null;if(!i?.length)return d.map(e=>[e[0],e[1]]);const p=yc(e,t,i,s,n,o,a,r);if(!p||!p.offsets.some(e=>e>0))return d.map(e=>[e[0],e[1]]);const u=c||bc(e,i,s,n,o,a,r),_=lc(p.poly,p.offsets,u);if(!_)return d.map(e=>[e[0],e[1]]);if(!u.nodes.length)return _;const m=void 0===l?qc(e,i,s,[],n,o,a,r):null,g=l??("ok"===m?.status||"degraded-extra"===m?.status?m.roomGeom:void 0);if(g)try{const e=Mc(oi(Dc(p.poly),g));if(e)return e}catch{}return function(e,t){try{return Mc(ni(Dc(e),Dc(t)))}catch{return null}}(_,p.poly)||d.map(e=>[e[0],e[1]])}function Dc(e){const t=e.map(e=>[e[0],e[1]]);return t.push([e[0][0],e[0][1]]),[t]}function Pc(e){return!(!Array.isArray(e)||!e.length)&&e.every(e=>Array.isArray(e)&&e.length&&e.every(e=>Array.isArray(e)&&e.length>=4&&e.every(e=>Array.isArray(e)&&e.length>=2&&Number.isFinite(e[0])&&Number.isFinite(e[1])))&&Math.abs(Ti(e[0]))>1e-9)}function Tc(e,t,i,s){const n=s?new Set(s):null;let o=null;for(let s=0;st.epsilon))continue;const l=Fc([[e.point[0]+r[0]*s.halfDepth,e.point[1]+r[1]*s.halfDepth],[e.point[0]+a.u[0]*n+r[0]*s.halfDepth,e.point[1]+a.u[1]*n+r[1]*s.halfDepth],[e.point[0]+a.u[0]*n-r[0]*s.halfDepth,e.point[1]+a.u[1]*n-r[1]*s.halfDepth],[e.point[0]-r[0]*s.halfDepth,e.point[1]-r[1]*s.halfDepth]],t.coordinateScale);if(!l)continue;const c=Dc(l);o=o?si(o,c):c}}return o}function Ic(e,t,i){let s=null;for(const n of e.rays)for(const e of n.continuations){const n=[-e.u[1],e.u[0]],o=[e.start[0]+e.u[0]*e.length,e.start[1]+e.u[1]*e.length],a=Fc([[e.start[0]+n[0]*e.halfDepth,e.start[1]+n[1]*e.halfDepth],[o[0]+n[0]*e.halfDepth,o[1]+n[1]*e.halfDepth],[o[0]-n[0]*e.halfDepth,o[1]-n[1]*e.halfDepth],[e.start[0]-n[0]*e.halfDepth,e.start[1]-n[1]*e.halfDepth]],t.coordinateScale);if(!a)continue;const r=ni(Dc(a),i);Array.isArray(r)&&0!==r.length&&(s=s?si(s,r):r)}return s}function Rc(e,t,i=2*(4*e.halfDepth+2*t.epsilon)){const s=function(e,t=yl){const i=new Set,s=Number.isFinite(t)&&t>=0?t:yl;for(let t=0;te-t)}(e);return s.length?Tc(e,t,i,s):null}function Ac(e,t,i,s,n){const o=function(e){let t=null;for(const i of e){const e=Dc(i);try{t=t?si(t,e):e}catch{}}return t}(function(e,t,i=!1){if(!e)return[];const s=[];for(const n of e.nodes)for(let o=0;o1e-9)||h>=Math.PI-1e-9)continue;const d=[-a.u[1],a.u[0]],p=[-r.u[1],r.u[0]],u=[n.point[0]+d[0]*a.halfDepth,n.point[1]+d[1]*a.halfDepth],_=[n.point[0]-p[0]*r.halfDepth,n.point[1]-p[1]*r.halfDepth],m=rc(u,a.u,_,r.u);if(!m)continue;const g=Math.hypot(m[0]-n.point[0],m[1]-n.point[1]);if(!Number.isFinite(g)||g<=n.limit)continue;const f=t?Math.sqrt(Math.max(0,n.limit*n.limit-a.halfDepth*a.halfDepth)):0,v=t?Math.sqrt(Math.max(0,n.limit*n.limit-r.halfDepth*r.halfDepth)):0,b=Fc([[u[0]+a.u[0]*f,u[1]+a.u[1]*f],[_[0]+r.u[0]*v,_[1]+r.u[1]*v],m],e.coordinateScale);if(b&&s.push(b),i){const t=m[0]-n.point[0],i=m[1]-n.point[1],o=Math.hypot(t,i),a=g-n.limit;if(o>e.epsilon&&a>e.epsilon){const r=Math.min(Math.max(8*e.epsilon,.05*n.halfDepth),.25*a),l=t/o,c=i/o,h=-c,d=l,p=Fc([[m[0]-l*r+h*r,m[1]-c*r+d*r],[m[0]-l*r-h*r,m[1]-c*r-d*r],[m[0]+l*r,m[1]+c*r]],e.coordinateScale);p&&s.push(p)}}}return s}({...t,nodes:[e]},i,s));return o&&n?oi(o,n):o}function zc(e,t,i,s){if(!e||!t.nodes.length)return e;let n=null;try{n=function(e){let t=null;for(const i of e.nodes){const s=Rc(i,e);s&&(t=t?si(t,s):s)}return t}(t)}catch{return e}let o=e;for(const e of t.nodes){const a=4*e.halfDepth+2*t.epsilon,r=2*a,l=[[e.point[0]-a,e.point[1]-a],[e.point[0]+a,e.point[1]-a],[e.point[0]+a,e.point[1]+a],[e.point[0]-a,e.point[1]+a]];try{let a=o;const c=Ac(e,t,!1,!0,n);c&&(a=oi(a,c));let h=Tc(e,t,r);const d=Ac(e,t,!0,!0,n);d&&(h=oi(h,d)),n&&(h=si(h,n));const p=.02*Math.min(...e.rays.map(e=>e.halfDepth));if(h=si(h,Dc([[e.point[0]-p,e.point[1]-p],[e.point[0]+p,e.point[1]-p],[e.point[0]+p,e.point[1]+p],[e.point[0]-p,e.point[1]+p]])),!h)continue;let u=ni(h,Dc(l));s?u=ni(u,s):i&&(u=ni(u,i));const _=Dc(l),m=oi(a,_),g=i?oi(ni(a,_),i):null,f=Ic(e,t,_);o=si(m,...g?[g]:[],...f?[f]:[],u)}catch{}}if(n)try{let e=n;s?e=ni(e,s):i&&(e=ni(e,i)),o=si(o,e)}catch{}return o}function Fc(e,t=1){if(!Array.isArray(e)||e.length<3)return null;const i=Number.isFinite(t)&&t>0?t:1,s=1e-12*Math.max(1,i),n=[];for(const t of e){if(!Array.isArray(t)||t.length<2)return null;const e=Number(t[0]),i=Number(t[1]);if(!Number.isFinite(e)||!Number.isFinite(i))return null;const o=Math.round(e/s)*s,a=Math.round(i/s)*s;if(!Number.isFinite(o)||!Number.isFinite(a))return null;n.push([Object.is(o,-0)?0:o,Object.is(a,-0)?0:a])}return Math.abs(nc(n))>s*s?n:null}function Nc(e,t,i,s){if(Sc(e[0],e[1],t[0],t[1],i[0],i[1])>s)return!1;const n=i[0]-t[0],o=i[1]-t[1],a=(e[0]-t[0])*n+(e[1]-t[1])*o,r=n*n+o*o,l=s*Math.sqrt(r);return a>=-l&&a<=r+l}function Ec(e,t,i){const s=[],n=[];for(let o=0;oi*i))continue;const d=i/Math.sqrt(h),p=[0,1];for(const e of t)for(const t of[e.a,e.b]){if(!Nc(t,a,r,i))continue;const e=((t[0]-a[0])*l+(t[1]-a[1])*c)/h;e>d&&e<1-d&&p.push(e)}p.sort((e,t)=>e-t);const u=p.filter((e,t)=>0===t||Math.abs(e-p[t-1])>d);for(let e=0;e=3&&n.length===s.length?{poly:s,offsets:n}:null}function Oc(e,t,i,s,n,o,a,r){const l=(e||[]).map(hi).filter(e=>!!e&&e.length>=3);if(!l.length)return null;let c=si(Dc(l[0]));for(let e=1;e"outer"===e.kind&&e.half>0),p=4*sc(s,a),u=r||gc(h,p,a);let _=null;for(const e of function(e){const t=[];for(const i of Array.isArray(e)?e:[])if(Array.isArray(i))for(const e of i){if(!Array.isArray(e)||e.length<4)continue;const i=e.slice(0,-1).map(e=>[e[0],e[1]]);i.length>=3&&t.push(i)}return t}(c)){const t=Ec(e,d,p);if(!t||!t.offsets.some(e=>e>0))continue;const i=Vc(t.poly,t.offsets,u),s=lc(t.poly,t.offsets,u);if(!i||!s)continue;const n=oi(Dc(i),Dc(s));_=_?si(_,n):n}return{centre:c,shell:_||[]}}function Hc(e,t,i,s,n,o,a=1){try{const r=Oc(e,t,i,s,n,o,a,bc(e,t,i,s,n,o,a));if(!r)return[];return r.shell?.length?si(r.centre,r.shell):r.centre}catch{return null}}function Lc(e){if(!e)return"";let t="";for(const i of e)if(Array.isArray(i))for(const e of i){if(!Array.isArray(e)||e.length<4)continue;const i=e.slice(0,e.length-1);i.length<3||(t+=(t?" ":"")+cc(i.map(e=>[e[0],e[1]])))}return t}const Bc=15;function jc(e,t,i){const s=e?.length||0;if(s<3||t?.length!==s)return!1;const n=e[(i-1+s)%s],o=e[i],a=e[(i+1)%s],r=[n[0]-o[0],n[1]-o[1]],l=[a[0]-o[0],a[1]-o[1]],c=Math.hypot(r[0],r[1]),h=Math.hypot(l[0],l[1]);if(!(c>1e-9&&h>1e-9))return!1;const d=Math.max(-1,Math.min(1,(r[0]*l[0]+r[1]*l[1])/(c*h))),p=Math.acos(d);if(!(p>1e-9))return!1;if(p>=Bc*Math.PI/180)return!1;const u=Math.max(Math.max(0,t[(i-1+s)%s]),Math.max(0,t[i]));if(!(u>0))return!1;const _=u/Math.tan(p/2);return Number.isFinite(_)&&_>0&&_e>0))continue;for(const e of l.offsets)e>0&&(p=Math.max(p,2*e));const c=Vc(l.poly,l.offsets,d),u=lc(l.poly,l.offsets,d);c&&h.push({outset:c,inset:u})}for(const e of l){const t=e.map(e=>e[0]),i=e.map(e=>e[1]);if(t.length){const s=Math.min(Math.max(...t)-Math.min(...t),Math.max(...i)-Math.min(...i)),n=Math.min(...e.map((t,i)=>{const s=e[(i+1)%e.length];return Math.hypot(s[0]-t[0],s[1]-t[1])}));p=Math.max(p,e.length>16?s:n)}}const u=function(e,t,i,s,n,o,a,r){if(!t?.length||!i?.length)return[];const l=4*sc(s,a),c=new Map;for(const r of wc(e,t,i,s,n,o,a))!r.open&&r.half>0&&!c.has(r.key)&&c.set(r.key,r);const h=[...c.values()];if(h.length<2)return[];const d=r||gc(h,l),p=[];for(const e of i)for(const t of[[e[0],e[1]],[e[2],e[3]]])p.some(e=>Math.hypot(e[0]-t[0],e[1]-t[1])<=l)||p.push(t);const u=[],_=(e,t)=>{let i=0,s=0;if(Math.hypot(e.a[0]-t[0],e.a[1]-t[1])<=l)i=e.b[0]-e.a[0],s=e.b[1]-e.a[1];else{if(!(Math.hypot(e.b[0]-t[0],e.b[1]-t[1])<=l))return null;i=e.a[0]-e.b[0],s=e.a[1]-e.b[1]}const n=Math.hypot(i,s);return n>l?[i/n,s/n]:null};for(const e of p){const t=h.map(t=>({iv:t,u:_(t,e)})).filter(e=>!!e.u);for(let i=0;i0?[e.slice(),p,m,_]:[e.slice(),_,m,p];else{if(!f)continue;{const t=[-n.u[1],n.u[0]],i=[-o.u[1],o.u[0]],s=a<0?1:-1,r=[e[0]+t[0]*n.iv.half*s,e[1]+t[1]*n.iv.half*s],l=[e[0]-i[0]*o.iv.half*s,e[1]-i[1]*o.iv.half*s];b=a>0?[e.slice(),r,l]:[e.slice(),l,r]}}Math.abs(nc(b))>l*l&&u.push(b)}}return u}(e,t,i,n,o,a,r,d),_=s.length?Kc(e,t,i,n,o,a,r):null;let m="exterior";try{const v=Oc(e,t,i,n,o,a,r,d);m="paper";const b=v?v.shell?.length?si(v.centre,v.shell):v.centre:[],y=e=>{const t=Dc(e.outset);if(!e.inset)return t;const i=Dc(e.inset);return oi(t,i)};m="room-rings";let w=null;for(const e of h)try{const t=y(e);w=w?si(w,t):t}catch{}if(m="edge-bodies",v)for(const s of Gc(e,t,i,n,o,a,r))try{const e=ni(Dc(s.quad),v.centre);w=w?si(w,e):e}catch{}m="junctions",w=function(e,t,i=1,s=si){let n=e;for(const e of t||[]){const t=Fc(e,i);if(t)try{const e=Dc(t);n=n?s(n,e):e}catch{}}return n}(w,u,r),m="facade-clip",w&&v&&(w=ni(w,v.centre)),m="exterior-shell";const k=[];let x=0;if(v?.shell?.length)if(w)try{const e=si(w,v.shell);if(!Pc(e))throw new Error("invalid shell union");w=e}catch{if(!Pc(w)||!Pc(v.shell))throw new Error("invalid shell");k.push({id:"exterior-shell",geom:v.shell}),x++}else w=v.shell;if(m="multi-wall-trim",w&&d.nodes.length){const e=e=>e.rays.some(e=>e.supports.some(e=>e.length<2*e.halfDepth)),t=d.nodes.filter(e);if(t.length){w=zc(w,{...d,nodes:t},v?.centre,b)}}if(m="junction-corners",d.nodes.length){const s=fc(d),l=function(e,t,i,s,n,o,a,r){try{const l=Oc(e,t,i,s,n,o,a,{epsilon:r.epsilon,coordinateScale:r.coordinateScale,nodes:[],index:new Map});return l?l.shell?.length?si(l.centre,l.shell):l.centre:null}catch{return null}}(e,t,i,n,o,a,r,d);for(const e of s.fans)try{let t=[Dc(e)];if(l&&(t=ni(t,l)),!t?.length)continue;w=w?si(w,t):t}catch{}g=w,f=Math.max(d.epsilon,1e-9)**2,w=Array.isArray(g)?g.map(e=>{if(!Array.isArray(e)||!e.length)return e;const[t,...i]=e;return Math.abs(nc(t||[]))<=f?null:[t,...i.filter(e=>Math.abs(nc(e||[]))>f)]}).filter(e=>!!e):g}const $=w||[],S=[...Pc($)?[{id:"room-primary",geom:$}]:[],...k.map((e,t)=>({id:`room-isolated-${t}`,geom:e.geom}))];m="openings";for(const e of s){if(!(e.length>0))continue;const t=Xc(_,e,!0);if(!t.negative&&!t.positive)continue;const i=e.angle*Math.PI/180,s=Math.cos(i),o=Math.sin(i),a=-o,l=s,c=e.length/2,h=1.25*Math.max(p,n*r),d=[[e.x-s*c-a*h,e.y-o*c-l*h],[e.x+s*c-a*h,e.y+o*c-l*h],[e.x+s*c+a*h,e.y+o*c+l*h],[e.x-s*c+a*h,e.y-o*c+l*h]];w&&(w=oi(w,Dc(d)));for(const e of k)e.geom=oi(e.geom,Dc(d))}m="extras";const M=[];let C=0;const D=c.mergeExtra||((e,t)=>e?si(e,t):t);for(let e=0;ee.length>=2&&Number.isFinite(e[0])&&Number.isFinite(e[1]))||Math.abs(Ti(t))<=1e-9){C++;continue}const i=[Dc(t)];try{const t=D(w,i,e);if(!Pc(t))throw new Error("invalid extra union");w=t}catch{C++,Pc(i)&&M.push({id:`extra-${e}`,geom:i})}}M.push(...k),M.sort((e,t)=>Lc(e.geom).localeCompare(Lc(t.geom)));const P=w||[];return{status:C||x?"degraded-extra":"ok",geom:P,components:[...Pc(P)?[{id:"primary",geom:P}]:[],...M.map((e,t)=>({...e,id:`isolated-${t}`}))],roomGeom:$,roomComponents:S,paperGeom:b,depthUnits:p,openingPadUnits:1.25*Math.max(p,n*r),openingIndex:_,multiWallNodes:d,degradedExtraCount:C+x}}catch{return c.onCoreFailure?.(m),{status:"failed-core",geom:[],components:[],roomGeom:[],paperGeom:[],roomComponents:[],depthUnits:p,openingIndex:null,multiWallNodes:d,degradedExtraCount:0}}var g,f}function Wc(e,t,i,s=[],n,o,a,r=1,l=[],c={}){if(!t?.length&&!l.length)return null;return Uc(qc(e,t,i,s,n,o,a,r,l,c))}function Uc(e){if("failed-core"===e.status||"not-applicable"===e.status)return null;const t=e.components.map(e=>({id:e.id,d:Lc(e.geom),fillRule:"evenodd"})).filter(e=>!!e.d),i=t[0]?.d||"",s=Lc(e.paperGeom);if(t.length){const n={status:e.status,d:i,paths:t,components:e.components,roomGeom:e.roomGeom,multiWallNodes:e.multiWallNodes,paperD:s,depthUnits:e.depthUnits,fillRule:"evenodd"};return Object.defineProperties(n,{roomComponents:{value:e.roomComponents||[],enumerable:!1},openingIndex:{value:e.openingIndex,enumerable:!1},openingPadUnits:{value:e.openingPadUnits,enumerable:!1}}),n}return null}function Gc(e,t,i,s,n,o,a=1){if(!t?.length)return[];const r=new Set,l=[];for(const c of e||[]){if(!c?.id)continue;const h=yc(e,c.id,t,i,s,n,o,a);if(!h)continue;const d=h.poly;for(let e=0;e0))continue;const u=Rl(t,i,s,a);if(r.has(u))continue;r.add(u);const _=Ml(p,n,o),[m,g]=oc(d,e),f=-m,v=-g,b=_/2,y=[[t[0]+f*b,t[1]+v*b],[i[0]+f*b,i[1]+v*b],[i[0]+m*b,i[1]+g*b],[t[0]+m*b,t[1]+g*b]];l.push({key:u,kind:c,cm:p,quad:y,a:[t[0],t[1]],b:[i[0],i[1]],depthUnits:_})}}return l}function Vc(e,t,i){const s=e?.length||0;if(s<3||t.length!==s)return null;if(t.every(e=>!(e>0)))return e.map(e=>[e[0],e[1]]);hc(e),t.slice().reverse();const n=[];for(let o=0;o0||l>0)){n.push([e[o][0],e[o][1]]);continue}if(r>0!=l>0){const t=e[o],i=r>0?[t[0]-c*r,t[1]-h*r]:[t[0],t[1]],s=l>0?[t[0]-d*l,t[1]-p*l]:[t[0],t[1]];n.push(i),Math.hypot(s[0]-i[0],s[1]-i[1])>1e-9&&n.push(s);continue}if(ac(w,k)){const t=e[o],i=[t[0]-c*r,t[1]-h*r],s=[t[0]-d*l,t[1]-p*l];n.push(i),Math.hypot(s[0]-i[0],s[1]-i[1])>1e-9&&n.push(s);continue}const S=rc(x,w,$,k),M=Math.max(r,l,1e-9),C=vc(i,e[o])?.limit??4*M;if(S){const i=Math.hypot(S[0]-e[o][0],S[1]-e[o][1]);if(Number.isFinite(i)&&i<=C){n.push(S);continue}if(jc(e,t,o)){n.push([e[o][0],e[o][1]]);continue}}r>0&&n.push([e[o][0]-c*r,e[o][1]-h*r]),l>0&&n.push([e[o][0]-d*l,e[o][1]-p*l])}return n.length>=3?n:null}function Kc(e,t,i,s,n,o,a=1){const r=[];for(const l of e||[]){if(!l?.id)continue;const c=yc(e,l.id,t,i,s,n,o,a);if(!c)continue;const h=Math.abs(Ti(c.poly));for(let e=0;e[Math.max(t,e.x0),Math.min(i,e.x1)]).filter(e=>e[1]-e[0]>s).sort((e,t)=>e[0]-t[0]||e[1]-t[1]);if(!n.length)return{coverage:0,full:!1};let o=n[0][0],a=n[0][1],r=0,l=o<=t+s;for(let e=1;e=i-s,{coverage:r,full:l}}function Jc(e,t){return Number(t.full)-Number(e.full)||e.faceDistance-t.faceDistance||e.area-t.area||e.roomId.localeCompare(t.roomId)}function Xc(e,t,i=!1){const s=Number(t?.x),n=Number(t?.y),o=Number(t?.angle),a=Number(t?.length);if(!([s,n,o,a].every(Number.isFinite)&&a>0))return{negative:null,positive:null};const r=o*Math.PI/180,l=Math.cos(r),c=Math.sin(r),h=-c,d=l,p=a/2,u=Math.max(1e-9,e.adjacencyEps),_=new Map;let m=0;for(const t of e.edges){if(i&&!(t.half>0))continue;if(!Bl(t.a,t.b,o))continue;const[e,a]=Tl(t.a,t.b);if(Math.abs((s-t.a[0])*a-(n-t.a[1])*e)>u)continue;const r=(t.a[0]-s)*l+(t.a[1]-n)*c,g=(t.b[0]-s)*l+(t.b[1]-n)*c,f=Math.max(-p,Math.min(r,g)),v=Math.min(p,Math.max(r,g));if(v-f<=u)continue;const b=t.inward[0]*h+t.inward[1]*d>=0?1:-1,y=((t.a[0]+t.b[0])/2-s)*h+((t.a[1]+t.b[1])/2-n)*d,w=Math.abs(y+b*t.half),k=`${b}|${t.roomId}`,x={x0:f,x1:v,half:t.half,cm:t.cm,key:t.key,axis:[e,a]},$=_.get(k);$?($.pieces.push(x),$.faceDistance=Math.min($.faceDistance,w)):_.set(k,{roomId:t.roomId,side:b,order:m++,pieces:[x],faceDistance:w,area:t.area,coverage:0,full:!1})}for(const e of _.values()){const t=Yc(e.pieces,-p,p,u);e.coverage=t.coverage,e.full=t.full}const g=e=>{const t=[..._.values()].filter(t=>t.side===e&&t.coverage>u);return t.sort(Jc),t[0]||null};return{negative:g(-1),positive:g(1)}}function Zc(e,t){const i=Xc(e,t),s=[i.negative,i.positive].filter(e=>!!e);if(!s.length)return{ox:0,oy:0,cm:0,side:-1};const n=i.negative&&i.positive?-1:s[0].side,o=t.flip_v?-n:n,a=(-1===o?i.negative:i.positive)||s[0],r=[...a.pieces].sort((e,t)=>(e.x0<=0&&e.x1>=0?0:Math.min(Math.abs(e.x0),Math.abs(e.x1)))-(t.x0<=0&&t.x1>=0?0:Math.min(Math.abs(t.x0),Math.abs(t.x1)))||t.x1-t.x0-(e.x1-e.x0)||e.key.localeCompare(t.key))[0];if(!(r.half>0&&r.cm>0))return{ox:0,oy:0,cm:0,side:o};const l=t.angle*Math.PI/180,c=-Math.sin(l),h=Math.cos(l);return{ox:c*o*r.half,oy:h*o*r.half,cm:r.cm,side:o}}function Qc(e,t){const i=1e-9,s=t.filter(e=>Number.isFinite(e.x0)&&Number.isFinite(e.x1)&&Number.isFinite(e.half)&&e.x1>e.x0&&e.half>0);if(!s.length)return"";const n=s.flatMap(e=>[e.x0,e.x1]).sort((e,t)=>e-t),o=[];for(const e of n){const t=o[o.length-1];(void 0===t||e>t+i)&&o.push(e)}const a=[];for(let e=0;e+1t+i))continue;const r=(t+n)/2,l=s.reduce((e,t)=>r>=t.x0-i&&r<=t.x1+i?Math.max(e,t.half):e,0);if(!(l>0))continue;const c=a[a.length-1];c&&t<=c.x1+i&&Math.abs(l-c.half)<=i?c.x1=n:a.push({x0:t,x1:n,half:l})}const r=[];for(const e of a){const t=r[r.length-1],s=t?.[t.length-1];s&&e.x0<=s.x1+i?(e.x0=s.x1,t.push(e)):r.push([e])}return r.map(t=>{const i=t[0],s=t[t.length-1],n=Math.min(.25*Math.min(...t.map(e=>e.half)),.75),o=-e*n,a=[];if(1===e){a.push(`M ${i.x0} ${o} L ${s.x1} ${o}`);for(let e=t.length-1;e>=0;e--){const i=t[e];a.push(`L ${i.x1} ${i.half} L ${i.x0} ${i.half}`)}}else{a.push(`M ${s.x1} ${o} L ${i.x0} ${o}`);for(const e of t)a.push(`L ${e.x0} ${-e.half} L ${e.x1} ${-e.half}`)}return a.push("Z"),a.join(" ")}).join(" ")}function eh(e,t,i,s){if(!s)return i;const n=e.angle*Math.PI/180,o=Math.cos(n),a=Math.sin(n),r=[],l=1e-9;for(const n of i){const[i,c]=n.axis,h=o*i+a*c;if(Math.abs(h)<=l)continue;const d=e.x*i+e.y*c,p=d+h*n.x0,u=d+h*n.x1,_=Math.min(p,u),m=Math.max(p,u),g=`${n.key}|${t}`,f=s.get(g)||[];let v=[[_,m]];for(const[e,t]of f){const i=[];for(const[s,n]of v)t<=s+l||e>=n-l?i.push([s,n]):(e>s+l&&i.push([s,Math.min(n,e)]),te[0]-t[0]||e[1]-t[1]),y=[];for(const e of b){const t=y[y.length-1];t&&e[0]<=t[1]+l?t[1]=Math.max(t[1],e[1]):y.push([e[0],e[1]])}s.set(g,y)}return r}function th(e,t){const i=new Map;return t.map(t=>function(e,t,i){const s=Xc(e,t,!0),n=s.negative,o=s.positive;if(!n&&!o)return null;let a;if(n&&o)a=[{candidate:n,side:-1},{candidate:o,side:1}];else{const e=n||o;a=[{candidate:e,side:-1},{candidate:e,side:1}]}const r=a.map(({candidate:e,side:s})=>({candidate:e,side:s,pieces:eh(t,s,e.pieces,i)})),l=r.map(({candidate:e,side:t,pieces:i})=>({side:t,roomId:e.roomId,d:Qc(t,i)})),c=r.flatMap(({pieces:e})=>e);if(!c.length)return null;const h=Math.max(...c.map(e=>e.half));return{faces:l,minY:-h,maxY:h,wallKey:[...new Set(c.map(e=>e.key))].sort().join("|")}}(e,t,i))}const ih=150,sh=1e-6;function nh(e){return Number.isFinite(e)?Math.max(1,Math.min(ih,e)):1}function oh(e){return((Number.isFinite(Number(e))?Number(e):0)%90+90)%90}const ah=e=>{const t=e.map(e=>[e[0],e[1]]);return!t.length||t[0][0]===t[t.length-1][0]&&t[0][1]===t[t.length-1][1]||t.push([...t[0]]),[t]},rh=(e,t)=>e[0]===t[0]&&e[1]===t[1];function lh(e,t=1e-6){const i=Number.isFinite(t)&&t>0?t:sh,s=[];for(const t of e||[]){if(!Array.isArray(t)||t.length<2)return null;const e=Number(t[0]),n=Number(t[1]);if(!Number.isFinite(e)||!Number.isFinite(n))return null;const o=Math.round(e/i)*i,a=Math.round(n/i)*i;if(!Number.isFinite(o)||!Number.isFinite(a))return null;const r=[Object.is(o,-0)?0:o,Object.is(a,-0)?0:a];s.length&&rh(s[s.length-1],r)||s.push(r)}return s.length>1&&rh(s[0],s[s.length-1])&&s.pop(),s.length<3||new Set(s.map(e=>`${e[0]},${e[1]}`)).size<3?null:Ti(s)>i*i?s:null}function ch(e){const t=[];for(const i of e||[])for(const e of i||[]){const i=(e||[]).filter(e=>Array.isArray(e)&&e.length>=2);i.length<4||t.push(`M ${i.slice(0,-1).map(e=>`${e[0]} ${e[1]}`).join(" L ")} Z`)}return t.join(" ")}function hh(e,t,i,s,n){return Kl({a:e,b:t,halfDepth:Ml(i,s,n)/2})}function dh(e,t,i){const s=Number(t)>0?Number(t):5,n=nh(e.cm)/s*i,o=e.center[0],a=e.center[1];if("circle"===e.shape){const e=n/2;return Array.from({length:96},(t,i)=>{const s=i/96*Math.PI*2;return[o+Math.cos(s)*e,a+Math.sin(s)*e]})}const r=n/2,l=oh(e.angle)*Math.PI/180,c=Math.cos(l),h=Math.sin(l);return[[-r,-r],[r,-r],[r,r],[-r,r]].map(([e,t])=>[o+e*c-t*h,a+e*h+t*c])}function ph(e,t,i=1e-9){if(!t.length)return[e];let s=[ah(e)];try{for(const e of t){const t=e.b[0]-e.a[0],n=e.b[1]-e.a[1],o=Math.hypot(t,n);if(!(o>i))continue;const a=t/o,r=n/o,l=-r,c=a,h=1.25*Math.max(Number(e.depth)||0,4*i),d=Math.max(2*i,1e-9*o),p=[[e.a[0]-a*d-l*h,e.a[1]-r*d-c*h],[e.b[0]+a*d-l*h,e.b[1]+r*d-c*h],[e.b[0]+a*d+l*h,e.b[1]+r*d+c*h],[e.a[0]-a*d+l*h,e.a[1]-r*d+c*h]];s=oi(s,ah(p))}return kh(s)}catch{return[e]}}function uh(e,t){try{const i=oi([[...e.map(e=>[e[0],e[1]]),[e[0][0],e[0][1]]]],[[...t.map(e=>[e[0],e[1]]),[t[0][0],t[0][1]]]]);let s=null,n=0;for(const e of i||[]){const t=e?.[0]||[],i=Math.abs(Ti(t));t.length>=4&&i>n&&(n=i,s=t.slice(0,-1).map(e=>[e[0],e[1]]))}return s}catch{return null}}function _h(e,t,i,s=Math.max(2e-4*i,1e-9),n=[]){const o=[],a=[],r=new Map;for(const e of n){const t=r.get(e.hostId)||[];t.push(e),r.set(e.hostId,t)}const l=[],c=[],h=[];for(const s of e.room_drafts||[])for(let e=0;e+10))continue;const r={a:s.points[e],b:s.points[e+1],halfDepth:a},c=Kl(r);c&&(o.push(r),l.push(c))}const d=[];for(const s of e.partitions||[]){const e={a:s.a,b:s.b,halfDepth:Ml(s.cm,t,i)/2};if(!(e.halfDepth>0))continue;const n=Kl(e);n&&(a.push(e),c.push(n),d.push({id:s.id,body:n}))}const p=[...o,...a];for(const{segmentIndex:e,wedge:t}of Ql(p,s)){const i=e=o.length&&(d[i.at].body=s))}for(const e of d)h.push(...ph(e.body,r.get(e.id)||[],s));const u=(e.wall_columns||[]).map(e=>dh(e,t,i)),_=ec([...o,...a],s).flatMap(e=>ph(e,n,s));return{drafts:l,partitions:c,columns:u,patches:_,all:[...l,...h,..._,...u]}}function mh(e){try{const t=e.map(e=>lh(e)).filter(e=>!!e).map(e=>ah(e));return t.length?si(t[0],...t.slice(1)):null}catch{return null}}const gh=e=>`M ${e.map(e=>`${e[0]} ${e[1]}`).join(" L ")} Z`;function fh(e){const t=[];for(const i of e||[]){if(yh([i])<=1e-6)continue;const e=[];for(const t of i||[]){const i=(t||[]).filter(e=>Array.isArray(e)&&e.length>=2);i.length<4||e.push(gh(i.slice(0,-1)))}e.length&&t.push(e.join(" "))}return t}function vh(e,t,i={}){const s=mh(e.filter(e=>e.length>=3)),n=t.some(e=>!lh(e))?null:mh(t);if(!s)return[];if(n)try{return fh(ni(s,n))}catch{}return function(e,t,i){let s=null;for(let n=0;n=4&&t.push(e.slice(0,-1).map(e=>[e[0],e[1]]));return t}function kh(e){const t=[];for(const i of e||[]){const e=i?.[0];e?.length>=4&&t.push(e.slice(0,-1).map(e=>[e[0],e[1]]))}return t}function xh(e,t){let i=!1;for(let s=0,n=t.length-1;se[1]!=l>e[1]&&e[0]<(r-o)*(e[1]-a)/(l-a||1e-12)+o&&(i=!i)}return i}function $h(e,t,i){return function(e,t){for(const i of t||[]){const t=i?.[0];if(!t?.length||!xh(e,t))continue;let s=!1;for(let t=1;txh(e,t))}function Sh(e,t,i){if(Math.hypot(e.center[0]-t.center[0],e.center[1]-t.center[1])>i)return!1;if(Math.abs(nh(e.cm)-nh(t.cm))>1e-6)return!1;if(e.shape!==t.shape)return!0;if("circle"===e.shape||"circle"===t.shape)return!0;const s=Math.abs(oh(e.angle)-oh(t.angle));return Math.min(s,90-s)<=1e-6}const Mh=2.54;function Ch(e){const t="number"==typeof e?e:NaN;return!Number.isFinite(t)||t<=0||5===t?1:5/t}function Dh(e,t){return e*Ch(t)}function Ph(e){return e?Mh:1}function Th(e,t){if(!t)return String(e);const i=e/Mh;return String(Math.round(1e6*i)/1e6)}const Ih=1e3;function Rh(e,t){const i=Number(e),s=Number.isFinite(i)&&i>0?i:1,n=s>=1?t:t*s,o=s>=1?t/s:t;return{x:(t-n)/2,y:(t-o)/2,w:n,h:o}}const Ah=.01,zh=100;function Fh(e,t=1e3){const i=Rh(e?.plan_aspect,t),s=Number(e?.plan_scale),n=Number.isFinite(s)&&s>0?Math.min(zh,Math.max(Ah,s)):1,o=Number(e?.plan_scale_x),a=Number(e?.plan_scale_y),r=Number.isFinite(o)&&o>0?Math.min(zh,Math.max(Ah,o)):n,l=Number.isFinite(a)&&a>0?Math.min(zh,Math.max(Ah,a)):n,c=Number(e?.plan_x),h=Number(e?.plan_y),d=qr(e?.plan_angle);return{x:i.x+(Number.isFinite(c)?Gh(c):0)*t,y:i.y+(Number.isFinite(h)?Gh(h):0)*t,w:i.w*r,h:i.h*l,...d?{angle:d}:{}}}function Nh(e){if(null==e.x||null==e.y)return{x:e.x,y:e.y,w:e.w,h:e.h};const t=Number(e.w)||0,i=Number(e.h)||0;return{x:t<0?e.x+t:e.x,y:i<0?e.y+i:e.y,w:Math.abs(t),h:Math.abs(i)}}function Eh(e){return e&&Array.isArray(e.spaces)?e.spaces.map(e=>{const t=Ih,i=function(e){return Array.isArray(e)&&4===e.length&&e.every(e=>Number.isFinite(e))&&e[2]>1e-6&&e[3]>1e-6?e:[0,0,1,1]}(e.view_box);return{id:e.id,title:e.title,cellCm:Number.isFinite(Number(e.cell_cm))&&Number(e.cell_cm)>0?Number(e.cell_cm):5,vb:[i[0]*Ih,i[1]*t,i[2]*Ih,i[3]*t],bg:e.plan_url?{href:As(e.plan_url),...Fh(e,Ih)}:null,rooms:(e.rooms||[]).map(e=>{const i={...e,...Nh(e)};return{id:i.id,name:i.name,area:i.area??null,open_to:i.open_to||void 0,settings:i.settings||void 0,x:null!=i.x?i.x*Ih:void 0,y:null!=i.y?i.y*t:void 0,w:null!=i.w?i.w*Ih:void 0,h:null!=i.h?i.h*t:void 0,poly:i.poly?i.poly.map(e=>[e[0]*Ih,e[1]*t]):void 0,wall_ids:Array.isArray(i.wall_ids)?[...i.wall_ids]:void 0}}),wall_segments:(e.wall_segments||[]).map(e=>({...e,id:String(e.id),a:[Number(e.a?.[0])*Ih,Number(e.a?.[1])*t],b:[Number(e.b?.[0])*Ih,Number(e.b?.[1])*t],cm:Number(e.cm)})),room_drafts:(e.room_drafts||[]).map(e=>({id:e.id,points:(e.points||[]).map(e=>[e[0]*Ih,e[1]*t]),segments:(e.segments||[]).map(e=>({..."string"==typeof e.id&&e.id?{id:e.id}:{},cm:Number(e.cm)}))})),partitions:(e.partitions||[]).map(e=>({id:e.id,a:[e.a[0]*Ih,e.a[1]*t],b:[e.b[0]*Ih,e.b[1]*t],cm:Number(e.cm)})),wall_columns:(e.wall_columns||[]).map(e=>({id:e.id,shape:"circle"===e.shape?"circle":"square",center:[e.center[0]*Ih,e.center[1]*t],cm:Number(e.cm),..."circle"===e.shape?{}:{angle:oh(e.angle)}}))}}):[]}const Oh=5e3,Hh=Oh*Ih,Lh=240,Bh=Ih/Lh,jh=1/Lh;function qh(e){if(!Number.isFinite(e))return e;const t=Math.round(e*Lh/Ih)*Ih/Lh;return Math.abs(t-e)<=1e-9*Bh?e:t}function Wh(e){return{x:qh(e.x),y:qh(e.y)}}function Uh(e){return Number.isFinite(e)?Math.min(Hh,Math.max(-Hh,e)):0}function Gh(e){return Number.isFinite(e)?Math.min(Oh,Math.max(-Oh,e)):0}const Vh=1/3,Kh=200;function Yh(e){let t=1/0,i=1/0,s=-1/0,n=-1/0;for(const o of e){const e=Number(o[0]),a=Number(o[1]);Number.isFinite(e)&&Number.isFinite(a)&&(es&&(s=e),a>n&&(n=a))}return t>s?null:{minX:t,minY:i,maxX:s,maxY:n}}function Jh(e){return e.poly&&e.poly.length?Yh(e.poly):null==e.x||null==e.y?null:Yh([[e.x,e.y],[e.x+(e.w||0),e.y+(e.h||0)]])}function Xh(e,t){const i=[];for(const t of e.rooms||[]){const e=Jh(t);e&&i.push(e)}if(e.bg){const t=Yh(Yr(e.bg));t&&i.push(t)}for(const e of t||[])if(Array.isArray(e)){const t=Yh([e]);t&&i.push(t)}else i.push(e);return i}const Zh=e=>{if(!e.length)return 0;const t=[...e].sort((e,t)=>e-t),i=t.length>>1;return t.length%2?t[i]:(t[i-1]+t[i])/2},Qh=e=>{let t=1/0,i=1/0,s=-1/0,n=-1/0;for(const o of e)o.minXs&&(s=o.maxX),o.maxY>n&&(n=o.maxY);return t>s||i>n?null:{x:t,y:i,w:s-t,h:n-i}};function ed(e,t){let{x:i,y:s,w:n,h:o}=e;n<30&&(i=i+n/2-100,n=Kh),o<30&&(s=s+o/2-100,o=Kh);const a=Math.max(n,o)*t;return{x:i-a,y:s-a,w:n+2*a,h:o+2*a}}function td(e,t={}){const i=t.pad??.05,s=t.k??10,n=t.minSpread??50,o=e.filter(e=>Number.isFinite(e.minX)&&Number.isFinite(e.minY)&&Number.isFinite(e.maxX)&&Number.isFinite(e.maxY)&&Math.abs(e.minX)<=Hh&&Math.abs(e.maxX)<=Hh&&Math.abs(e.minY)<=Hh&&Math.abs(e.maxY)<=Hh);if(!o.length)return{core:null,all:null,outliers:0};const a=Qh(o);if(o.length<4){const e=ed(a,i);return{core:e,all:e,outliers:0}}const r=o.map(e=>(e.minX+e.maxX)/2),l=o.map(e=>(e.minY+e.maxY)/2),c=Zh(r),h=Zh(l),d=o.map((e,t)=>Math.max(Math.abs(r[t]-c),Math.abs(l[t]-h))),p=Math.max(((e,t)=>e.length?e[Math.min(e.length-1,Math.max(0,Math.round(t*(e.length-1))))]:0)([...d].sort((e,t)=>e-t),.75),n),u=d.map(e=>e>s*p),_=u.filter(Boolean).length,m=_&&_<=o.length*Vh?o.filter((e,t)=>!u[t]):o;return{core:ed(Qh(m)||a,i),all:ed(a,i),outliers:m===o?0:_}}function id(e,t,i=.05){const s=td(Xh(e,t),{pad:i});if(s.core)return s.core;const n=e.vb&&4===e.vb.length&&e.vb[2]>0&&e.vb[3]>0?e.vb:[0,0,Ih,Ih];return{x:n[0],y:n[1],w:n[2],h:n[3]}}function sd(e){const t=id(e);return{x:t.x+t.w/2,y:t.y+t.h/2}}function nd(e){const t=[];for(const i of e.rooms||[]){const e=Jh(i);e&&t.push(e)}const i=td(t,{pad:0}).core,s=Ih*Ch(e.cellCm??5);return i?Math.max(s,Math.min(Hh,Math.max(i.w,i.h))):s}function od(e,t,i,s=1){const n=Number(i),o=Number.isFinite(s)&&s>0?s:1;return!Number.isFinite(n)||n<=0?e*o:e*nd(t)*o/n}const ad=[1,2,5,10,20,50,100,200,500,1e3];function rd(e){if(e.poly&&e.poly.length){const t=e.poly.map(e=>e[0]),i=e.poly.map(e=>e[1]),s=Math.min(...t),n=Math.min(...i);return{x:s,y:n,w:Math.max(...t)-s,h:Math.max(...i)-n}}return{x:e.x??0,y:e.y??0,w:e.w??0,h:e.h??0}}function ld(e,t,i,s){const n=i["rl_"+(e.id||"")];if(n&&n.s===t)return{x:n.x*Ih,y:n.y*Ih};const o=function(e){if(e.poly){const t=e.poly.length;return[e.poly.reduce((e,t)=>e+t[0],0)/t,e.poly.reduce((e,t)=>e+t[1],0)/t]}return[e.x+e.w/2,e.y+.1*Math.min(e.w,e.h)]}(e);return Wh({x:o[0],y:o[1]})}const cd=["furniture","appliance","sanitary","other"],hd=[{id:"sofa",group:"furniture",w:220,h:90,g:[["r",0,0,1,1],["l",.09,.26,.91,.26],["l",.09,.26,.09,1],["l",.91,.26,.91,1],["l",.5,.26,.5,1]]},{id:"armchair",group:"furniture",w:90,h:85,g:[["r",0,0,1,1],["l",.14,.28,.86,.28],["l",.14,.28,.14,1],["l",.86,.28,.86,1]]},{id:"coffee_table",group:"furniture",w:110,h:60,g:[["r",0,0,1,1],["r",.08,.14,.84,.72]]},{id:"table_dining",group:"furniture",w:140,h:80,g:[["r",0,0,1,1],["r",.06,.11,.88,.78]]},{id:"table_round",group:"furniture",w:120,h:120,g:[["e",.5,.5,.5,.5],["e",.5,.5,.41,.41]]},{id:"chair",group:"furniture",w:45,h:45,g:[["r",0,0,1,.18],["r",.06,.18,.88,.8]]},{id:"desk",group:"furniture",w:120,h:60,g:[["r",0,0,1,1],["r",.63,.07,.31,.86],["l",.63,.5,.94,.5]]},{id:"bed_double",group:"furniture",w:160,h:200,g:[["r",0,0,1,1],["r",0,0,1,.07],["r",.06,.1,.4,.15],["r",.54,.1,.4,.15],["l",0,.33,1,.33]]},{id:"bed_single",group:"furniture",w:90,h:200,g:[["r",0,0,1,1],["r",0,0,1,.07],["r",.15,.1,.7,.15],["l",0,.33,1,.33]]},{id:"nightstand",group:"furniture",w:45,h:40,g:[["r",0,0,1,1],["r",.12,.14,.76,.33],["r",.12,.53,.76,.33]]},{id:"wardrobe",group:"furniture",w:100,h:60,g:[["r",0,0,1,1],["l",0,.72,1,.72],["l",.5,.72,.5,1]]},{id:"bookshelf",group:"furniture",w:80,h:30,g:[["r",0,0,1,1],["l",.34,0,.34,1],["l",.67,0,.67,1]]},{id:"fridge",group:"appliance",w:60,h:65,g:[["r",0,0,1,1],["l",0,.36,1,.36],["l",.83,.44,.83,.64]]},{id:"stove",group:"appliance",w:60,h:60,g:[["r",0,0,1,1],["e",.29,.31,.15,.15],["e",.71,.31,.15,.15],["e",.29,.71,.15,.15],["e",.71,.71,.15,.15]]},{id:"dishwasher",group:"appliance",w:60,h:60,g:[["r",0,0,1,1],["r",.1,.12,.8,.76],["e",.5,.5,.27,.27],["e",.5,.5,.13,.13]]},{id:"washer",group:"appliance",w:60,h:60,g:[["r",0,0,1,1],["l",.08,.17,.92,.17],["e",.5,.57,.3,.3],["e",.5,.57,.14,.14]]},{id:"dryer",group:"appliance",w:60,h:60,g:[["r",0,0,1,1],["l",.08,.17,.92,.17],["e",.5,.57,.3,.3],["p",.36,.5,.5,.64,.64,.5]]},{id:"tv",group:"appliance",w:120,h:30,g:[["r",0,0,1,.42],["l",.5,.42,.5,.72],["l",.3,.72,.7,.72]]},{id:"ac",group:"appliance",w:90,h:25,g:[["r",0,0,1,1],["l",.05,.55,.95,.55],["l",.05,.79,.95,.79]]},{id:"water_heater",group:"appliance",w:45,h:45,g:[["e",.5,.5,.5,.5],["e",.5,.5,.31,.31]]},{id:"toilet",group:"sanitary",w:40,h:70,g:[["r",.06,0,.88,.2],["e",.5,.58,.37,.35],["e",.5,.58,.22,.2]]},{id:"bathtub",group:"sanitary",w:170,h:75,g:[["r",0,0,1,1],["r",.05,.11,.77,.78],["e",.89,.5,.045,.1]]},{id:"shower",group:"sanitary",w:90,h:90,g:[["r",0,0,1,1],["l",0,0,1,1],["l",1,0,0,1],["e",.5,.5,.08,.08]]},{id:"sink",group:"sanitary",w:60,h:45,g:[["r",0,0,1,1],["e",.5,.6,.34,.3],["e",.5,.15,.07,.07]]},{id:"kitchen_sink",group:"sanitary",w:80,h:60,g:[["r",0,0,1,1],["r",.06,.24,.44,.64],["r",.54,.24,.4,.64],["e",.5,.12,.06,.06]]},{id:"bidet",group:"sanitary",w:40,h:55,g:[["e",.5,.5,.44,.5],["e",.5,.5,.26,.3]]},{id:"stairs",group:"other",w:100,h:280,g:[["r",0,0,1,1],["l",0,.111,1,.111],["l",0,.222,1,.222],["l",0,.333,1,.333],["l",0,.444,1,.444],["l",0,.556,1,.556],["l",0,.667,1,.667],["l",0,.778,1,.778],["l",0,.889,1,.889],["l",.5,.93,.5,.06],["p",.38,.16,.5,.06,.62,.16]]},{id:"fireplace",group:"other",w:120,h:40,g:[["r",0,0,1,1],["p",.22,1,.22,.42,.78,.42,.78,1]]},{id:"plant",group:"other",w:40,h:40,g:[["e",.5,.5,.22,.22],["l",.5,.28,.5,.02],["l",.5,.72,.5,.98],["l",.28,.5,.02,.5],["l",.72,.5,.98,.5],["l",.34,.34,.13,.13],["l",.66,.66,.87,.87],["l",.66,.34,.87,.13],["l",.34,.66,.13,.87]]},{id:"rug",group:"other",w:200,h:140,g:[["r",0,0,1,1],["r",.06,.09,.88,.82]]}],dd=new Map(hd.map(e=>[e.id,e]));function pd(e){return e&&dd.get(e)||null}function ud(e){return hd.filter(t=>t.group===e)}function _d(e,t,i=Bh,s=1e3){const n=Number(t)>0?Number(t):5;return(Number(e)||0)/n*i/s}const md=5e-4,gd=Oh;function fd(e){return Number.isFinite(e)?Math.max(md,Math.min(gd,e)):md}const vd=e=>{const t=Math.round(1e3*e)/1e3;return Object.is(t,-0)?"0":String(t)};function bd(e,t,i){const s=pd(e);if(!(s&&t>0&&i>0))return"";const n=e=>vd(e*t),o=e=>vd(e*i),a=[];for(const e of s.g)if("r"===e[0]){const[,t,i,s,r]=e;a.push(`M${n(t)} ${o(i)}H${n(t+s)}V${o(i+r)}H${n(t)}Z`)}else if("l"===e[0]){const[,t,i,s,r]=e;a.push(`M${n(t)} ${o(i)}L${n(s)} ${o(r)}`)}else if("e"===e[0]){const[,s,r,l,c]=e;a.push(`M${n(s-l)} ${o(r)}A${vd(l*t)} ${vd(c*i)} 0 0 1 ${n(s+l)} ${o(r)}A${vd(l*t)} ${vd(c*i)} 0 0 1 ${n(s-l)} ${o(r)}Z`)}else{const t=e.slice(1);if(t.length<4)continue;let i=`M${n(t[0])} ${o(t[1])}`;for(let e=2;e+1{let t=(e%360+360)%360;return t>180&&(t-=360),t};function wd(e,t,i,s,n,o=0){let a=null,r=n;for(const n of s){const[s,l,c,h]=n,d=c-s,p=h-l,u=d*d+p*p;if(!u)continue;const _=Math.sqrt(u);let m=((e-s)*d+(t-l)*p)/u;m=Math.max(0,Math.min(1,m));let g=s+m*d,f=l+m*p;const v=Math.hypot(e-g,t-f);if(!(v0){const e=Math.round(m*_/o)*o;g=s+e/_*d,f=l+e/_*p}a={cx:g+b*(i/2),cy:f+y*(i/2),angle:yd(180*Math.atan2(-b,y)/Math.PI),dist:v}}return a}const kd=1e-9,xd=1e7,$d=e=>{const t=Math.sign(e)*Math.floor(Math.abs(e)*xd+.5)/xd;return Object.is(t,-0)?0:t},Sd=e=>`${$d(e[0])},${$d(e[1])}`,Md=(e,t)=>Math.hypot(t[0]-e[0],t[1]-e[1]),Cd=(e,t)=>Math.atan2(t[1]-e[1],t[0]-e[0]),Dd=e=>Array.isArray(e)&&e.length>=2&&e.every(e=>Number.isFinite(e)),Pd=e=>(e||[]).filter(e=>Dd(e?.a)&&Dd(e?.b)&&Md(e.a,e.b)>kd);function Td(e,{minAngleDeg:t=15,maxValence:i=6}={}){const s=new Map;for(const t of Pd(e))for(const[e,i]of[[t.a,t.b],[t.b,t.a]]){const t=s.get(Sd(e))||[];t.push(Cd(e,i)),s.set(Sd(e),t)}const n=[];for(const[e,o]of s){if(o.length>i&&n.push({rule:"valence",subject:e,actual:o.length,limit:i}),o.length<2)continue;const s=[...o].sort((e,t)=>e-t);let a=1/0;for(let e=0;ekd&&i(180*Math.atan2(e.b[1]-e.a[1],e.b[0]-e.a[0])/Math.PI%180+180)%180,Rd=(e,t,i=1)=>{const s=Math.abs(Id(e)-Id(t));return Math.min(s,180-s)<=i},Ad=e=>{const t=new Map;for(const i of e)for(const e of[i.a,i.b]){const s=t.get(Sd(e));s?s.push(i):t.set(Sd(e),[i])}return t};function zd(e,t,i){const s=Pd(t),n=i??Ad(s),o=new Set([e]);let a=Md(e.a,e.b);const r=[e.a,e.b];for(;r.length;){const t=r.pop();for(const i of n.get(Sd(t))||[])o.has(i)||Rd(i,e)&&Number(i.cm||0)===Number(e.cm||0)&&(o.add(i),a+=Md(i.a,i.b),r.push(i.a,i.b))}return a}function Fd(e,t,i,{minLengthCm:s=20}={}){const n=[],o=Pd(e),a=Ad(o);for(const e of o){const r=zd(e,o,a)/i*(t||1),l=Math.max(s,Number(e.cm)>0?Number(e.cm):0);r{const s=i[0]-t[0],n=i[1]-t[1],o=s*s+n*n,a=o<=kd?0:Math.max(0,Math.min(1,((e[0]-t[0])*s+(e[1]-t[1])*n)/o));return Math.hypot(e[0]-(t[0]+s*a),e[1]-(t[1]+n*a))};function Ed(e,t,i,{minDistanceCm:s=5}={}){const n=Pd(e),o=new Map;for(const e of n)o.set(Sd(e.a),e.a),o.set(Sd(e.b),e.b);const a=((e,t,i)=>e/(t||1)*i)(s,t,i),r=a>kd?a:1,l=(e,t)=>`${Math.floor(e/r)},${Math.floor(t/r)}`,c=new Map;for(const[e,t]of o){const i=l(t[0],t[1]),s=c.get(i);s?s.push([e,t]):c.set(i,[[e,t]])}const h=new Map;for(const e of n){const t=Math.min(e.a[0],e.b[0])-a,i=Math.max(e.a[0],e.b[0])+a,s=Math.min(e.a[1],e.b[1])-a,n=Math.max(e.a[1],e.b[1])+a;for(let o=Math.floor(t/r);o<=Math.floor(i/r);o++)for(let t=Math.floor(s/r);t<=Math.floor(n/r);t++){const i=`${o},${t}`,s=h.get(i);s?s.push(e):h.set(i,[e])}}const d=[];for(const[e,n]of o){const o=Math.floor(n[0]/r),l=Math.floor(n[1]/r);for(let r=-1;r<=1;r++)for(let h=-1;h<=1;h++)for(const[p,u]of c.get(`${o+r},${l+h}`)||[]){if(e>=p)continue;const o=Md(n,u);o<=2e-7||o{const s=o[(i+1)%o.length];return e+(t[0]*s[1]-s[0]*t[1])},0))/2,r=(i||1)/s,l=a*r*r;return l0?i:1;return{a:[e[0]/s,e[1]/s],b:[t[0]/s,t[1]/s]}}function Ld(e,t){const i=t>0?t:1;return[e.a[0]*i,e.a[1]*i,e.b[0]*i,e.b[1]*i]}function Bd(e){return Array.isArray(e)&&e.length>=2&&Number.isFinite(Number(e[0]))&&Number.isFinite(Number(e[1]))}function jd(e){if(!Array.isArray(e))return[];const t=[];for(const i of e){if(!i||"object"!=typeof i)continue;const e=i;if(!Bd(e.a)||!Bd(e.b))continue;const s=[Number(e.a[0]),Number(e.a[1])],n=[Number(e.b[0]),Number(e.b[1])];Math.hypot(n[0]-s[0],n[1]-s[1])<.001||t.push({a:s,b:n})}return t}function qd(e,t,i,s,n=!0){const o=(e||[]).filter(e=>e?.id),a=jd(t);if(a.length)return function(e,t,i,s){const n=jd(e);if(!n.length)return[];const o=function(e,t){const i=[],s=(e||[]).filter(e=>e?.id);for(let e=0;e4*s||o>4*s)continue;const a=(e[0]-n)*p+(e[1]-l)*u,c=(e[2]-n)*p+(e[3]-l)*u,h=Math.max(0,Math.min(a,c)),m=Math.min(d,Math.max(a,c));if(m-he.lo-t.lo||e.hi-t.hi);const i=[];for(const e of t){const t=i[i.length-1];t&&e.lo<=t.hi+r?t.hi=Math.max(t.hi,e.hi):i.push({...e})}for(const t of i){const i=[n+p*t.lo,l+u*t.lo],s=[n+p*t.hi,l+u*t.hi];Math.hypot(s[0]-i[0],s[1]-i[1])i.pair===e&&Math.abs(i.ux*a-i.uy*o)<=1e-6&&Math.abs((t[0]-i.origin[0])*i.uy-(t[1]-i.origin[1])*i.ux)<=c&&Math.abs((t[2]-i.origin[0])*i.uy-(t[3]-i.origin[1])*i.ux)<=c);h||(h={pair:e,origin:[t[0],t[1]],ux:o,uy:a,ranges:[]},l.push(h));const d=(t[0]-h.origin[0])*h.ux+(t[1]-h.origin[1])*h.uy,p=(t[2]-h.origin[0])*h.ux+(t[3]-h.origin[1])*h.uy;h.ranges.push({lo:Math.min(d,p),hi:Math.max(d,p)})}const h=[];for(const e of l){e.ranges.sort((e,t)=>e.lo-t.lo||e.hi-t.hi);const t=[];for(const i of e.ranges){const e=t[t.length-1];e&&i.lo<=e.hi+r?e.hi=Math.max(e.hi,i.hi):t.push({...i})}for(const s of t){const t=[e.origin[0]+e.ux*s.lo,e.origin[1]+e.uy*s.lo],n=[e.origin[0]+e.ux*s.hi,e.origin[1]+e.uy*s.hi];Math.hypot(n[0]-t[0],n[1]-t[1])>=r&&h.push(Hd(t,n,i))}}return h}(a,e,i,s).map(e=>Ld(e,i));if(!n)return[];const r=[],l=(e,t)=>(e.open_to||[]).includes(t.id)||(t.open_to||[]).includes(e.id);for(let e=0;e{const t=Gd.get(e);if(!t)return;const i=Date.now();if("hidden"===e.visibilityState){t.hiddenAt||(t.hiddenAt=i);const e={kind:"hidden",token:t.token,at:i,hiddenFor:0,long:!1};for(const i of[...t.subscribers])i(e);return}const s=t.hiddenAt?Math.max(0,i-t.hiddenAt):0;t.hiddenAt=0,t.token++;const n={kind:"visible",token:t.token,at:i,hiddenFor:s,long:s>=Wd};for(const e of[...t.subscribers])e(n)},onPageShow:t=>{const i=Gd.get(e);if(!i)return;const s=Date.now();i.token++;const n={kind:"pageshow",token:i.token,at:s,hiddenFor:0,long:!!t.persisted,persisted:!!t.persisted};for(const e of[...i.subscribers])e(n)}},Gd.set(e,i),e.addEventListener("visibilitychange",i.onVisibility),e.defaultView?.addEventListener("pageshow",i.onPageShow)}return i.subscribers.add(t),()=>{const i=Gd.get(e);i&&(i.subscribers.delete(t),i.subscribers.size||(e.removeEventListener("visibilitychange",i.onVisibility),e.defaultView?.removeEventListener("pageshow",i.onPageShow),Gd.delete(e)))}}const Kd={now:()=>Date.now(),setTimeout:(e,t)=>window.setTimeout(e,t),clearTimeout:e=>window.clearTimeout(e),requestAnimationFrame:e=>window.requestAnimationFrame(e),cancelAnimationFrame:e=>window.cancelAnimationFrame(e)};class Yd{constructor(e,t=Kd){this.onChange=e,this.clock=t,this._state="steady",this._token=0,this._frameFingerprint="",this._hasCompleteFrame=!1,this._overlayPhase="none",this._recoveryReason=null,this._overlayTimer=0,this._overlayRaf=0,this._overlayOpaqueAt=0,this._barrierRafs=new Set,this._trace=[],this._disposed=!1}get state(){return this._state}get token(){return this._token}get frameFingerprint(){return this._frameFingerprint}get hasCompleteFrame(){return this._hasCompleteFrame}get overlayPhase(){return this._overlayPhase}get overlayVisible(){return"none"!==this._overlayPhase}get overlayBlocksInteraction(){return this.overlayVisible}get recoveryReason(){return this._recoveryReason}get trace(){return this._trace.map(e=>({...e,...e.stage?{stage:[...e.stage]}:{}}))}note(e,t={}){this.record(e,void 0,t)}record(e,t,i={}){this._trace.push({at:this.clock.now(),token:this._token,event:e,state:this._state,...t?{reason:t}:{},...i}),this._trace.length>80&&this._trace.splice(0,this._trace.length-80)}changed(){this._disposed||this.onChange()}adoptCompleteFrame(e){e&&(this._hasCompleteFrame=!0,this._frameFingerprint=e,this._state="steady",this._recoveryReason=null,this.clearOverlay(),this.record("frame-adopted"),this.changed())}markCompleteFrame(e){e&&(this._hasCompleteFrame=!0,this._frameFingerprint=e,this._state="steady",this._recoveryReason=null,this.clearOverlay(),this.record("frame-complete"),this.changed())}refreshCompleteFrame(e){!this._disposed&&this._hasCompleteFrame&&"steady"===this._state&&e&&e!==this._frameFingerprint&&(this._frameFingerprint=e,this.record("frame-refreshed"))}visibility(e){return"hidden"===e.kind?(this.record("visibility-hidden"),this._token):e.long||"visible"!==e.kind&&"pageshow"!==e.kind?this.beginCandidate("pageshow"===e.kind?"pageshow":"long-resume"):(this.record("pageshow"===e.kind?"pageshow-noop":"visibility-visible-quick"),this._token)}beginCandidate(e,t="plan"){return this._token++,this._recoveryReason=t,this._hasCompleteFrame?(this._state="connection"===t?"offline-stale":"holding",this.clearOverlay()):this.overlayVisible?this._state="overlay-visible":(this._state="overlay-pending",this.scheduleOverlay(t)),this.record("candidate-start",e),this.changed(),this._token}connectionLost(){return this.beginCandidate("connection-lost","connection")}candidateReady(e){return!this._disposed&&e===this._token&&(this._state="candidate-ready",this.record("candidate-ready"),this.changed(),!0)}async commitAfterPaint(e,t){if(this._disposed||e!==this._token)return!1;let i=!0;const s=(async()=>{for(await t.updateComplete();i&&!this._disposed&&e===this._token;)if(t.stageValid()&&t.assetsReady()){if(await this.nextFrame(),e!==this._token||!t.stageValid()||!t.assetsReady())continue;if(await this.nextFrame(),e===this._token&&t.stageValid()&&t.assetsReady())return!0}else await this.nextFrame();return!1})();let n=0;const o=new Promise(e=>{n=this.clock.setTimeout(()=>e({ready:!1,timedOut:!0}),Ud)}),a=await Promise.race([s.then(e=>({ready:e,timedOut:!1})),o]);return i=!1,this.clock.clearTimeout(n),!this._disposed&&e===this._token&&(!(!a.ready&&!a.timedOut)&&(a.ready?(this._hasCompleteFrame=!0,this._frameFingerprint=t.frameFingerprint(),this.record("paint-barrier"),this.finishOverlayAfterCommit(),!0):(this.record("paint-barrier-timeout"),this._hasCompleteFrame?(this._state="connection"===this._recoveryReason?"offline-stale":"steady",this.clearOverlay()):(this._state="recovery-error","opaque"!==this._overlayPhase&&this.showOverlayNow()),this.changed(),!1)))}retry(e=this._recoveryReason||"plan"){return this.beginCandidate("retry",e)}nextFrame(){return new Promise(e=>{const t=this.clock.requestAnimationFrame(()=>{this._barrierRafs.delete(t),e()});this._barrierRafs.add(t)})}scheduleOverlay(e){this.clearOverlayTimer(),this._recoveryReason=e,this._overlayTimer=this.clock.setTimeout(()=>{this._overlayTimer=0,this._hasCompleteFrame||"overlay-pending"!==this._state&&"candidate-ready"!==this._state||(this._state="overlay-visible",this._overlayPhase="entering",this.record("overlay-enter"),this.changed(),this._overlayRaf=this.clock.requestAnimationFrame(()=>{this._overlayRaf=0,"entering"===this._overlayPhase&&(this._overlayPhase="fading-in",this.changed(),this._overlayTimer=this.clock.setTimeout(()=>{this._overlayTimer=0,"fading-in"===this._overlayPhase&&(this._overlayPhase="opaque",this._overlayOpaqueAt=this.clock.now(),this.record("overlay-opaque"),this.changed())},150))}))},150)}showOverlayNow(){this.clearOverlayTimer(),this._overlayPhase="opaque",this._overlayOpaqueAt=this.clock.now(),this.record("overlay-error")}finishOverlayAfterCommit(){if(this.clearOverlayTimer(),"none"===this._overlayPhase||"entering"===this._overlayPhase||"fading-in"===this._overlayPhase)return this.clearOverlay(),this._state="steady",this._recoveryReason=null,this.record("candidate-committed"),void this.changed();const e=Math.max(0,this._overlayOpaqueAt+250-this.clock.now());this._overlayTimer=this.clock.setTimeout(()=>{this._overlayTimer=0,this._overlayPhase="leaving",this.record("overlay-leave"),this.changed(),this._overlayTimer=this.clock.setTimeout(()=>{this._overlayTimer=0,this._overlayPhase="none",this._state="steady",this._recoveryReason=null,this.record("candidate-committed"),this.changed()},180)},e)}clearOverlayTimer(){this._overlayTimer&&this.clock.clearTimeout(this._overlayTimer),this._overlayTimer=0}clearOverlay(){this.clearOverlayTimer(),this._overlayRaf&&this.clock.cancelAnimationFrame(this._overlayRaf),this._overlayRaf=0,this._overlayPhase="none",this._overlayOpaqueAt=0}dispose(){this._disposed=!0,this._token++,this.clearOverlay();for(const e of this._barrierRafs)this.clock.cancelAnimationFrame(e);this._barrierRafs.clear()}}function Jd(e){let t=2166136261,i=2654435769;const s=new WeakSet,n=e=>{for(let s=0;s>>0)+(i>>>2)}},o=e=>{if(null===e)return void n("n");const t=typeof e;if("string"===t)return void n(`s${e.length}:${e}`);if("number"===t)return void n(`d${Number.isNaN(e)?"NaN":String(e)}`);if("boolean"===t)return void n(e?"t":"f");if("undefined"===t)return void n("u");if("object"!==t)return void n(`${t}:${String(e)}`);const i=e;if(s.has(i))n("[cycle]");else{if(s.add(i),Array.isArray(e)){n("[");for(const t of e)o(t);return n("]"),void s.delete(i)}n("{");for(const t of Object.keys(e).sort())n(t),o(e[t]);n("}"),s.delete(i)}};return o(e),`${(t>>>0).toString(16).padStart(8,"0")}${(i>>>0).toString(16).padStart(8,"0")}`}function Xd(e){return Jd(e)}const Zd=new WeakMap;class Qd{constructor(e,t=()=>Date.now()){this.onUpdate=e,this.now=t,this.fallbackAuthority={},this.referenceOwner={},this.referenced=new Set,this.queued=new Set,this.disposed=!1,this.sharedUpdate=()=>{this.disposed||this.onUpdate()}}bind(e){const t=((e,t)=>{const i=e?.connection||e?.callWS||t;return!i||"object"!=typeof i&&"function"!=typeof i?t:i})(e,this.fallbackAuthority);return this.shared&&this.authority===t||(this.shared&&(this.shared.listeners.delete(this.sharedUpdate),this.shared.references.delete(this.referenceOwner)),this.authority=t,this.shared=(e=>{let t=Zd.get(e);return t||(t={signed:{},queued:new Set,inFlight:new Map,retry:new Map,listeners:new Set,settlers:new Set,references:new Map},Zd.set(e,t)),t})(t),this.disposed||(this.shared.listeners.add(this.sharedUpdate),this.shared.references.set(this.referenceOwner,new Set(this.referenced)))),this.shared}start(e,t){this.disposed=!1,this.referenced=new Set(t());const i=this.bind(e());i.listeners.add(this.sharedUpdate),i.references.set(this.referenceOwner,new Set(this.referenced)),this.stopTimer(),this.resignTimer=setInterval(()=>this.resign(e(),t()),288e5)}dispose(){if(this.disposed=!0,this.stopTimer(),clearTimeout(this.batchTimer),this.shared){this.shared.listeners.delete(this.sharedUpdate),this.shared.references.delete(this.referenceOwner);for(const e of this.queued)this.shared.queued.delete(e)}this.queued.clear()}invalidate(e){const t=this.bind(e);clearTimeout(this.batchTimer),this.queued.clear(),t.queued.clear(),t.inFlight.clear(),t.retry.clear(),t.signed={}}stopTimer(){void 0!==this.resignTimer&&clearInterval(this.resignTimer),this.resignTimer=void 0}display(e,t){const i=As(t);if(!i.startsWith("/api/houseplan/content/"))return i;const s=this.bind(e);this.referenced.add(i),s.references.set(this.referenceOwner,new Set(this.referenced));const n=s.signed[i],o=n?this.now()-n.at:1/0;return o{const t=[...this.queued];this.queued.clear();for(const e of t)i.queued.delete(e);this.sign(e,t)},30))}sign(e,t){if(!t.length||!e?.callWS)return;const i=this.bind(e);for(const s of function(e,t){const i=Math.max(1,Math.floor(t)),s=[];for(let t=0;t{const s=i.inFlight.get(e);return!(void 0!==s&&t-s<15e3)&&(i.inFlight.set(e,t),!0)});n.length&&e.callWS({type:"houseplan/content/sign",paths:n}).then(e=>{const t=this.now();let s=0,o=!1;for(const a of n){const n=e?.urls?.[a];if("string"==typeof n&&n){const e=i.signed[a];e?.loaded&&e.url!==n?(e.pending={url:n,at:t},this.preloadReplacement(i,a,n,t)):(i.signed[a]={url:n,at:t,loaded:e?.url===n&&!!e.loaded},o=!0),i.retry.delete(a),s++}else this.backOff(i,a)}if(s&&(this.trimShared(i),o))for(const e of[...i.listeners])e()}).catch(()=>{for(const e of n)this.backOff(i,e)}).finally(()=>{for(const e of n)i.inFlight.get(e)===t&&i.inFlight.delete(e);for(const e of[...i.settlers])e()})}}preloadReplacement(e,t,i,s){const n=()=>{const n=e.signed[t];if(n?.pending?.url===i){e.signed[t]={url:i,at:s,loaded:!0},e.retry.delete(t),this.trimShared(e);for(const t of[...e.listeners])t()}},o=()=>{const s=e.signed[t];s?.pending?.url===i&&(delete s.pending,this.backOff(e,t))};if("undefined"==typeof Image)return void n();const a=new Image;a.onload=()=>{("function"==typeof a.decode?a.decode():Promise.resolve()).then(n).catch(o)},a.onerror=o,a.src=i}preloadCurrentImage(e,t,i){if(i.loaded)return Promise.resolve(!0);if(i.preload)return i.preload;if("undefined"==typeof Image)return i.loaded=!0,Promise.resolve(!0);const s=i.url;return i.preload=new Promise(n=>{const o=new Image;let a=!1;const r=o=>{if(a)return;a=!0,clearTimeout(l);const r=e.signed[t];if(r===i&&delete r.preload,o&&r===i&&r.url===s){r.loaded=!0;for(const t of[...e.listeners])t();n(!0)}else o||r!==i||this.backOff(e,t),n(!1)};o.onload=()=>{("function"==typeof o.decode?o.decode():Promise.resolve()).then(()=>r(!0)).catch(()=>r(!1))},o.onerror=()=>r(!1);const l=setTimeout(()=>r(!1),Ud);o.src=s}),i.preload}prepareImage(e,t){const i=As(t);if(!i.startsWith("/api/houseplan/content/"))return Promise.resolve(!0);const s=this.bind(e),n=()=>{const t=s.signed[i];if(t&&this.now()-t.atthis.now()&&!s.inFlight.has(i)&&!s.queued.has(i)?Promise.resolve(!1):null},o=n();return o?(a=o,new Promise(e=>{let t=!1;const i=i=>{t||(t=!0,clearTimeout(s),e(i))},s=setTimeout(()=>i(!1),Ud);a.then(i).catch(()=>i(!1))})):new Promise(e=>{let t=!1,i=null;const o=i=>{t||(t=!0,clearTimeout(r),s.settlers.delete(a),e(i))},a=()=>{t||i||(i=n(),i&&i.then(o).catch(()=>o(!1)))},r=setTimeout(()=>o(!1),Ud);s.settlers.add(a),a()});var a}backOff(e,t){const i=e.retry.get(t)?.delay||0,s=Math.min(6e4,i?2*i:2e3);e.retry.set(t,{notBefore:this.now()+s,delay:s})}trimShared(e){const t=Object.entries(e.signed);if(t.length<=512)return;const i=new Set(e.inFlight.keys());for(const t of e.references.values())for(const e of t)i.add(e);for(const[e,s]of t)(s.pending||s.preload)&&i.add(e);t.sort((e,t)=>t[1].at-e[1].at);const s=t.filter(([e])=>i.has(e));for(const e of t){if(s.length>=512)break;i.has(e[0])||s.push(e)}e.signed=Object.fromEntries(s)}resign(e,t){const i=this.bind(e);this.referenced=new Set(t),i.references.set(this.referenceOwner,new Set(this.referenced));const s=this.now(),n=new Set;for(const e of i.references.values())for(const t of e)n.add(t);const o={};for(const[e,t]of Object.entries(i.signed))n.has(e)&&s-t.at!!o[e]&&!o[e].pending))}markLoaded(e,t,i){const s=As(t);if(!s.startsWith("/api/houseplan/content/"))return;const n=this.bind(e).signed[s];if(n&&(!i||n.url===i)&&!n.loaded){n.loaded=!0;for(const t of[...this.bind(e).listeners])t()}}isReady(e,t){const i=As(t);if(!i.startsWith("/api/houseplan/content/"))return!0;const s=this.bind(e).signed[i];return!!s&&this.now()-s.ate.id===t)}function tp(e="file"){const t="draw"===e;return{source:e,showBorders:t,showNames:t,displayTouched:!1}}function ip(e,t){if(e.displayTouched)return{...e,source:t};const i="draw"===t;return{...e,source:t,showBorders:i,showNames:i}}function sp(e,t,i){return{...e,[t]:i,displayTouched:!0}}function np(e,t,i){return[e[0]*t+e[1]*i+e[2],e[3]*t+e[4]*i+e[5]]}function op(e,t){let i=0;for(const[s,n]of t){const t=np(e,s[0],s[1]);i=Math.max(i,Math.hypot(t[0]-n[0],t[1]-n[1]))}return i}const ap=e=>{if(null==e||""===e)return null;const t=Number(e);return Number.isFinite(t)?t:null},rp=e=>{const t=e,i=ap(Array.isArray(t)?t[0]:t?.x),s=ap(Array.isArray(t)?t[1]:t?.y);return null==i||null==s?null:[i,s]};function lp(e){if(!Array.isArray(e))return null;const t=[];for(const i of e){const e=rp(i);if(!e)return null;t.push(e)}if(t.length>1){const e=t[0],i=t[t.length-1];e[0]===i[0]&&e[1]===i[1]&&t.pop()}if(t.length<3)return null;let i=0,s=0,n=0;for(let e=0;ee+t[0],0)/t.length,t.reduce((e,t)=>e+t[1],0)/t.length]:[s/(3*i),n/(3*i)]}function cp(e){if(!Array.isArray(e))return null;const t=[];for(const i of e){const e=rp(i);if(!e)return null;t.push(e)}if(t.length>1){const e=t[0],i=t[t.length-1];e[0]===i[0]&&e[1]===i[1]&&t.pop()}const i=lp(t);if(!i)return null;const s=t.map(e=>e[0]),n=t.map(e=>e[1]);return{centroid:i,bbox:[Math.min(...s),Math.min(...n),Math.max(...s),Math.max(...n)]}}function hp(e){let t=e;if(t&&"object"==typeof t&&!Array.isArray(t)){const e=t;t=e.path??e.points??t}if(!Array.isArray(t)||!t.length)return[];let i=(t.some(e=>(e=>Array.isArray(e)?e.length>=2&&!Array.isArray(e[0])&&("object"!=typeof e[0]||null==e[0]):!!e&&"object"==typeof e&&("x"in e||"y"in e))(e))?[t]:t.filter(Array.isArray)).map(e=>e.map(rp).filter(e=>null!=e)).filter(e=>e.length>=2);i.length>64&&(i=i.slice(-64));const s=i.reduce((e,t)=>e+t.length,0);if(s<=4e3)return i.map(e=>e.slice());const n=4e3-2*i.length,o=i.map(e=>e.length-2),a=o.reduce((e,t)=>e+t,0),r=o.map(e=>n*e/a),l=r.map(Math.floor);let c=n-l.reduce((e,t)=>e+t,0);const h=r.map((e,t)=>({index:t,fraction:e-Math.floor(e)})).sort((e,t)=>t.fraction-e.fraction||e.index-t.index);for(let e=0;efunction(e,t){if(t>=e.length)return e.slice();if(t<=2)return[e[0],e[e.length-1]];const i=[];for(let s=0;se.slice()),i=t[t.length-1];return i?(i.length>2?i.pop():t.pop(),t):[]}function up(e){if(!e)return null;const t=e.vacuum_position||e.robot_position||null,i=t&&null!=ap(t.x)&&null!=ap(t.y)?{x:ap(t.x),y:ap(t.y),a:ap(t.a??t.angle??t.theta)}:null,s=hp(e.path?.path??e.path?.points??e.path),n=[],o=e.rooms,a=Array.isArray(o)?o.map((e,t)=>[String(e?.id??t),e]):o&&"object"==typeof o?Object.entries(o):[];for(const[e,t]of a){if(!t||"object"!=typeof t)continue;const i=String(t.name??t.label??"").trim(),s=[[ap(t.cx),ap(t.cy)],[ap(t.center?.x),ap(t.center?.y)],[ap(t.x),ap(t.y)]].find(([e,t])=>null!=e&&null!=t);let o=s?.[0]??null,a=s?.[1]??null;const r=cp(t.outline),l=ap(t.x0),c=ap(t.y0),h=ap(t.x1),d=ap(t.y1),p=(null!=l&&null!=c&&null!=h&&null!=d?[Math.min(l,h),Math.min(c,d),Math.max(l,h),Math.max(c,d)]:null)||r?.bbox||null;if(null!=o&&null!=a||!r||([o,a]=r.centroid),null!=o&&null!=a||!p||(o=(p[0]+p[2])/2,a=(p[1]+p[3])/2),i&&null!=o&&null!=a){const t={id:e,name:i,cx:o,cy:a};p&&([t.x0,t.y0,t.x1,t.y1]=p),n.push(t)}}const r=function(e){return String(e.map_name??e.current_map??e.map_index??e.selected_map??"default")}(e);return i||n.length||s.length?{pos:i,path:s,rooms:n,mapId:r}:null}function _p(e,t,i){if(!e||!t)return null;const s=t.attributes||{},n=e.split(".")[0]||"",o=null!=i?.platform?String(i.platform):null,a=!!rp(s.vacuum_position??s.robot_position),r=!(!s.rooms||"object"!=typeof s.rooms),l=hp(s.path?.path??s.path?.points??s.path).length>0,c=[s.map_name,s.current_map,s.map_index,s.selected_map].some(e=>null!=e),h="xiaomi_cloud_map_extractor"===o,d=r||l||c;let p=null;if(a?p="compatible":h?p="known_xcme_incomplete":d?p="partial":"camera"===n&&(p="camera"),!p)return null;const u=a?"camera"===n?300:200:h?100:d?50:0;return{entityId:e,name:String(s.friendly_name||e),platform:o,category:p,hasPosition:a,hasRooms:r,hasPath:l,hasMapId:c,score:u}}const mp=e=>e.toLowerCase().replace(/[\s_\-.,]+/g,"");function gp(e,t){const i=new Map(t.map(e=>[mp(e.name),e])),s=[],n=[];for(const t of e){const e=i.get(mp(t.name));e&&(s.push([[t.cx,t.cy],[e.cx,e.cy]]),n.push(t.name))}if(s.length<3)return null;const o=function(e){if(e.length<3)return null;let t=0,i=0,s=0,n=0,o=0,a=0,r=0,l=0,c=0,h=0,d=0,p=0;for(const[[u,_],[m,g]]of e){if(![u,_,m,g].every(Number.isFinite))return null;t+=u*u,i+=u*_,s+=u,n+=_*_,o+=_,a+=1,r+=u*m,l+=_*m,c+=m,h+=u*g,d+=_*g,p+=g}const u=[t,i,s,i,n,o,s,o,a],_=e=>{const[t,i,s,n,o,a,r,l,c]=u,h=t*(o*c-a*l)-i*(n*c-a*r)+s*(n*l-o*r);if(!Number.isFinite(h)||Math.abs(h)<1e-9)return null;const d=[(o*c-a*l)/h,(s*l-i*c)/h,(i*a-s*o)/h,(a*r-n*c)/h,(t*c-s*r)/h,(s*n-t*a)/h,(n*l-o*r)/h,(i*r-t*l)/h,(t*o-i*n)/h];return[d[0]*e[0]+d[1]*e[1]+d[2]*e[2],d[3]*e[0]+d[4]*e[1]+d[5]*e[2],d[6]*e[0]+d[7]*e[1]+d[8]*e[2]]},m=_([r,l,c]),g=_([h,d,p]);if(!m||!g)return null;const f=[m[0],m[1],m[2],g[0],g[1],g[2]];return f.every(Number.isFinite)?f:null}(s);return o?{matrix:o,matched:n,residual:op(o,s)}:null}function fp(e,t,i){const s=e[e.length-1];if(s&&s[0]===t[0]&&s[1]===t[1])return e;if(e.push(t),e.length<=600)return e;let n=function(e,t){if(e.length<3)return e.slice();const i=new Uint8Array(e.length);i[0]=i[e.length-1]=1;const s=[[0,e.length-1]];for(;s.length;){const[n,o]=s.pop(),[a,r]=e[n],[l,c]=e[o],h=l-a,d=c-r,p=Math.hypot(h,d)||1e-9;let u=0,_=-1;for(let t=n+1;tu&&(u=i,_=t)}_>0&&u>t&&(i[_]=1,s.push([n,_],[_,o]))}const n=[];for(let t=0;t600&&(n=n.filter((e,t)=>t%2==0||t===n.length-1)),n}function vp(e){return"cleaning"===e||"returning"===e||"on"===e}const bp={0:[1,0],90:[0,1],180:[-1,0],270:[0,-1]};function yp(e){const[t,i]=bp[e.rot]||[1,0],s=e.mir?-1:1;return[e.s*t*s,-e.s*i,e.ox,e.s*i*s,e.s*t,e.oy]}function wp(e){const t=e[0]*e[4]-e[1]*e[3];if(!Number.isFinite(t)||Math.abs(t)<1e-12)return null;const i=t<0,s=Math.sqrt(Math.abs(t));let n=180*Math.atan2(-e[1],e[4])/Math.PI;return n=(90*Math.round(n/90)%360+360)%360,{ox:e[2],oy:e[5],s:s,rot:n,mir:i}}function kp(e,t,i,s){const[n,o]=np(yp(t),i,s),a=yp({...e,ox:0,oy:0}),[r,l]=np(a,i,s);return{...e,ox:n-r,oy:o-l}}function xp(e){const t=e?.trail_mode;return"never"===t||"cleaning"===t||"always"===t?t:!1===e?.trail?"never":"cleaning"}const $p=new Set(["group","template","derivative","min_max","threshold","integration","statistics","trend","utility_meter","tod","switch_as_x","schedule"]);function Sp(e){const{hass:t,devices:i,markers:s,showEntities:n,currentBinding:o,currentDeviceId:a,labels:r}=e,l=wo(s),c=new Set(s.filter(e=>e.removed).map(e=>e.binding)),h=new Set;for(const e of i)e.id!==a&&("device"===e.bindingKind&&e.bindingRef&&h.add(`device:${e.bindingRef}`),"entity"===e.bindingKind&&e.bindingRef&&h.add(`entity:${e.bindingRef}`));const d=new Set;for(const e of i)"device"===e.bindingKind&&e.name&&d.add(`${e.name.trim()}|${e.area||""}`);const p=[];for(const e of Object.values(t?.devices||{})){if(!e||"service"===e.entry_type)continue;const t=`device:${e.id}`;if(h.has(t))continue;const i=String(e.name_by_user||e.name||e.id).trim();t!==o&&!c.has(t)&&d.has(`${i}|${e.area_id||""}`)||p.push({value:t,label:i,sub:(e.model||r.device)+("Group"===e.model?r.z2mGroup:""),kind:"device",ref:e.id,areaId:e.area_id||"",model:e.model||""})}for(const[e,i]of Object.entries(t?.entities||{})){const s=`entity:${e}`;if(h.has(s))continue;if(ko(t,e,l)&&!c.has(s))continue;const n=$p.has(i?.platform),o="group"===i?.platform;if(!n&&!o)continue;if(i?.hidden&&!c.has(s))continue;const a=t?.states?.[e];p.push({value:s,label:i?.name||a?.attributes?.friendly_name||e,sub:`${e.split(".")[0]} · ${o?r.group:r.helper}`,kind:"entity",ref:e,areaId:i?.area_id||i?.device_id&&t?.devices?.[i.device_id]?.area_id||"",model:"",parentDeviceId:i?.device_id||void 0})}if(n){const e=new Set(p.map(e=>e.value));for(const[i,s]of Object.entries(t?.entities||{})){const n=`entity:${i}`;if(h.has(n)||e.has(n)||s?.hidden&&!c.has(n))continue;const o=!!s?.device_id&&l.devices.has(s.device_id);if(ko(t,i,l)&&!c.has(n)&&!o)continue;const a=t?.states?.[i],d=s?.device_id?t?.devices?.[s.device_id]:null,u=d?String(d.name_by_user||d.name||""):"";p.push({value:n,label:s?.name||a?.attributes?.friendly_name||i,sub:`${i.split(".")[0]} · ${r.entity}${u?` · ${u}`:""}`,kind:"entity",ref:i,areaId:s?.area_id||d?.area_id||"",model:"",parentDeviceId:s?.device_id||void 0})}}return p.sort((e,t)=>e.label.localeCompare(t.label)||e.value.localeCompare(t.value))}const Mp={kind:"active",enabledEntityIds:[],allEntityIds:[]};function Cp(e){return e.startsWith("entity:")?"entity":"device"}function Dp(e){const t=e.binding.indexOf(":"),i=t>=0?e.binding.slice(t+1):"",s=Object.keys(e).filter(e=>!["id","binding","hidden"].includes(e));return e.id===`h${i}`&&0===s.length}const Pp={offStates:["off"],unknownUsesToggle:!0},Tp={light:Pp,switch:Pp,fan:Pp,humidifier:Pp,input_boolean:Pp,automation:Pp,remote:Pp,group:Pp,climate:{...Pp,featureMasks:{turn_on:256,turn_off:128,toggle:384}},media_player:{...Pp,featureMasks:{turn_on:128,turn_off:256,toggle:384}},siren:{...Pp,featureMasks:{turn_on:1,turn_off:2,toggle:3}},vacuum:{...Pp,featureMasks:{turn_on:1,turn_off:2,toggle:3}},water_heater:{...Pp,featureMasks:{turn_on:8,turn_off:8,toggle:8}},camera:{...Pp,featureMasks:{turn_on:1,turn_off:1,toggle:1}}};function Ip(e){return e.slice(0,e.indexOf("."))}function Rp(e,t,i){if(!e?.services||"object"!=typeof e.services)return!1;const s=e.services?.[t];return!!s&&Object.prototype.hasOwnProperty.call(s,i)}function Ap(e,t,i){return Rp(e,t,i)?{domain:t,service:i}:Rp(e,"homeassistant",i)?{domain:"homeassistant",service:i}:null}function zp(e,t){return e?.entities?.[t]||null}function Fp(e,t,i){const s=e?.states?.[i],n=zp(t,i);return s?.attributes?.friendly_name||n?.name||n?.original_name||i}function Np(e,t){const i=zp(e,t);if(null!=i?.disabled_by)return!0;const s=i?.device_id?e?.devices?.[i.device_id]:null;return null!=s?.disabled_by}function Ep(e,t){const i=e?.attributes?.supported_features;if(null==i||""===i)return!1;const s=Number(i);return Number.isFinite(s)&&(s&t)===t}function Op(e,t,i){const s=Ip(i);if("lock"===s||"alarm_control_panel"===s)return!0;if("cover"!==s)return!1;const n=e?.states?.[i],o=zp(t,i),a=String(n?.attributes?.device_class||o?.device_class||o?.original_device_class||"");return Ui.has(a)}function Hp(e,t,i,s,n){return{ref:i,entityId:s,name:s?Fp(e,t,s):null,reason:n}}function Lp(e,t,i,s,n,o=null){return{target:null,skipped:Hp(e,t,i,s,n),semantics:o,nextEffect:null,command:null}}function Bp(e,t,i,s,n=i){const o=Ip(i);if("cover"!==o&&"valve"!==o)return function(e,t,i,s,n=i){const o=Ip(i),a=e?.states?.[i];if(Np(t,i))return Lp(e,t,n,i,"ha-disabled","power");if(Op(e,t,i))return Lp(e,t,n,i,"secure");if(!a)return Lp(e,t,n,i,"missing","power");if("unavailable"===a.state)return Lp(e,t,n,i,"unavailable","power");const r=Tp[o];if(!r)return Lp(e,t,n,i,"unsupported");let l,c;if("unknown"===a.state||""===a.state){if(!r.unknownUsesToggle)return Lp(e,t,n,i,"unsupported","power");l="toggle",c="toggle"}else r.offStates.includes(String(a.state))?(l="turn-on",c="turn_on"):(l="turn-off",c="turn_off");const h=r.featureMasks?.[c];if(h&&!Ep(a,h))return Lp(e,t,n,i,"unsupported","power");const d=Ap(e,o,c);return d?{target:{entityId:i,name:Fp(e,t,i),state:String(a.state||""),via:s},skipped:null,semantics:"power",nextEffect:l,command:{...d,data:{entity_id:i}}}:Lp(e,t,n,i,"unsupported","power")}(e,t,i,s,n);if(Np(t,i))return Lp(e,t,n,i,"ha-disabled",o);if(Op(e,t,i))return Lp(e,t,n,i,"secure",o);const a=e?.states?.[i];if(!a)return Lp(e,t,n,i,"missing",o);if("unavailable"===a.state)return Lp(e,t,n,i,"unavailable",o);const r=function(e,t,i){const s=String(i?.state||""),n="closed"===s?{service:`open_${t}`,effect:"open",feature:1}:"open"===s?{service:`close_${t}`,effect:"close",feature:2}:"opening"===s||"closing"===s?{service:`stop_${t}`,effect:"stop",feature:8}:null;return n&&Ep(i,n.feature)&&Rp(e,t,n.service)?n:Ep(i,3)&&Rp(e,t,"toggle")?{service:"toggle",effect:"toggle"}:null}(e,o,a);if(!r)return Lp(e,t,n,i,"unsupported",o);return{target:{entityId:i,name:Fp(e,t,i),state:String(a.state||""),via:s},skipped:null,semantics:o,nextEffect:r.effect,command:{domain:o,service:r.service,data:{entity_id:i}}}}function jp(e){return"entity"===e.bindingKind?e.bindingRef&&Ms(e.bindingRef)?[e.bindingRef]:[]:"virtual"===e.bindingKind?[]:eo(e)}function qp(e){const t=e.marker?.toggle_entity;return t&&jp(e).includes(t)?t:null}function Wp(e,t,i){const s=function(e,t){if("entity"===e.bindingKind&&e.bindingRef){const t=e.marker?.light_entity?to(e):null;return[...new Set([t,e.bindingRef].filter(e=>!!e))]}const i=e.entities.length?e.entities:e.allEntities||[],s=e.marker?.light_entity||e.primary&&Ms(e.primary)?to(e):null;return[...new Set([s,...Yn(t,i)].filter(e=>!!e))]}(i,t);if(!s.length)return null;const n="entity"===i.bindingKind?"binding":"device-role";if("binding"===n)return Bp(e,t,s[0],n);let o=null,a=null;for(const i of s){const s=Bp(e,t,i,n);if(s.command)return s;const r=s.skipped?.reason;if("missing"===r||"unavailable"===r||"secure"===r)return s;"ha-disabled"!==r?o||=s:a||=s}return o||a}function Up(e){const t=e.skipped?.reason;return"missing"===t?"unavailable":t||"unsupported"}function Gp(e,t){return{origin:e,kind:"none",semantics:null,targets:[],skippedTargets:[],noneReason:t,nextEffect:null,command:null}}function Vp(e,t){return"cover"===e||"toggle"===e?"toggle":"more-info"===e||"run"===e||"info"===e?e:(null==e||""===e)&&"light"===t?"toggle":"info"}function Kp(e,t){return{origin:e,kind:"single",semantics:t.semantics,targets:t.target?[t.target]:[],skippedTargets:t.skipped?[t.skipped]:[],noneReason:t.command?null:Up(t),nextEffect:t.nextEffect,command:t.command}}function Yp(e,t,i,s=[]){const n=new Map,o=[],a=new Set,r=e=>{const t=`${e.ref}\n${e.entityId||""}\n${e.reason}`;a.has(t)||(a.add(t),o.push(e))};for(const e of s)r(e);for(const s of i){const i=Bp(e,t,s.entityId,s.via,s.ref);i.target?n.set(s.entityId,n.get(s.entityId)||i.target):i.skipped&&r(i.skipped)}const l=[...n.values()];if(!l.length)return{origin:"explicit-toggle",kind:"group",semantics:"group-power",targets:l,skippedTargets:o,noneReason:o.length&&o.every(e=>"secure"===e.reason)?"secure":"configured-targets-missing",nextEffect:null,command:null};const c=l.some(e=>"on"===e.state)?"turn_off":"turn_on",h=Ap(e,"homeassistant",c);return h?{origin:"explicit-toggle",kind:"group",semantics:"group-power",targets:l,skippedTargets:o,noneReason:null,nextEffect:"turn_on"===c?"turn-on":"turn-off",command:{...h,data:{entity_id:l.map(e=>e.entityId)}}}:{origin:"explicit-toggle",kind:"group",semantics:"group-power",targets:[],skippedTargets:[...o,...l.map(i=>Hp(e,t,i.entityId,i.entityId,"unsupported"))],noneReason:"unsupported",nextEffect:null,command:null}}function Jp(e){return String(e.marker?.id||e.id||"")}function Xp(e){const{hass:t,device:i}=e,s=e.registryHass||t,n=function(e){return"toggle"===e.tapAction?"explicit-toggle":"cover"===e.tapAction?"legacy-cover":!e.tapAction&&e.primary?.startsWith("light.")?"default-light":null}(i);if(!n)return null;const o=i.marker;if("explicit-toggle"===n&&bn(o)){const t=String(o?.id||i.id||""),s=ro(e.devices).get(t);if(s)return function(e,t){const i=e.registryHass||e.hass;return Yp(e.hass,i,t.driverEids.map(e=>({entityId:e,via:"control-marker-driver",ref:`marker:${t.markerId}`})))}(e,s);const a=yn(o,e.virtualLights);return{origin:n,kind:"single",semantics:"power",targets:[{entityId:"",name:i.name,state:a?"on":"off",via:"virtual-light"}],skippedTargets:[],noneReason:null,nextEffect:a?"turn-off":"turn-on",command:null,operation:{kind:"virtual-light",markerId:o.id}}}if("explicit-toggle"===n){if(Zn(i.marker?.binding,i.marker?.controls??i.controls,i.entities).length)return function(e,t=null){const{hass:i,device:s,devices:n}=e,o=e.registryHass||i,a=Zn(s.marker?.binding,s.marker?.controls??s.controls,s.entities),r=e.lightSources||ho(i,n,null,e.virtualLights),l=new Map;for(const e of r){if(!e.key.startsWith("marker:"))continue;const t=l.get(e.key)||[];t.push(e),l.set(e.key,t)}const c=ro(n),h=new Map;for(const e of n){const t=String(e.marker?.id||e.id||"");t&&h.set(t,e)}const d=[],p=[];t&&d.push({entityId:t,via:"entity"===s.bindingKind?"binding":"device-role",ref:t});for(const e of a){if(!e.startsWith("marker:")){Ms(e)?d.push({entityId:e,via:"control-entity",ref:e}):p.push(Hp(i,o,e,e.includes(".")?e:null,"unsupported"));continue}const t=l.get(e)||[];if(!t.length){const t=h.get(e.slice(7));if("ha_disabled"===t?.bindingStatus?.kind){const s=t.bindingStatus.allEntityIds[0]||null;p.push({ref:e,entityId:s,name:t.name||(s?Fp(i,o,s):null),reason:"ha-disabled"})}else p.push(Hp(i,o,e,null,"missing"));continue}const n=[...new Set(t.flatMap(e=>e.serviceEids))];if(n.length){for(const t of n)d.push({entityId:t,via:"control-entity",ref:e});continue}const a=t.some(e=>e.passive),r=e.slice(7),u=Jp(s),_=c.get(r)?.controllers.find(e=>Jp(e.device)===u);if(a&&_?.driverEids.length)for(const t of _.driverEids)d.push({entityId:t,via:"control-marker-driver",ref:e});else p.push(Hp(i,o,e,null,a?"missing":"unsupported"))}return Yp(i,o,d,p)}(e,qp(i))}if("legacy-cover"===n){const e=i.entities.find(e=>e.startsWith("cover."))||i.allEntities?.find(e=>e.startsWith("cover."));return e?Kp(n,Bp(t,s,e,"device-role")):Gp(n,"no-actionable-entity")}const a=qp(i),r=a?Bp(t,s,a,"entity"===i.bindingKind?"binding":"device-role"):Wp(t,s,i);return r?Kp(n,r):Gp(n,i.virtual||"virtual"===i.bindingKind?"no-actionable-entity":"no-binding")}function Zp(e){if(!e)return[];const t=Array.isArray(e.data.entity_id)?e.data.entity_id:[e.data.entity_id];return[...new Set(t)].sort()}function Qp(e){return e?e.operation?e.operation:e.command?{kind:"ha-service",command:e.command}:null:null}const eu=new Set(["missing","ha-disabled","unavailable"]);function tu(e,t){const i=Qp(e),s=Qp(t);return!(!i||!s||i.kind!==s.kind)&&("virtual-light"===i.kind&&"virtual-light"===s.kind?i.markerId===s.markerId:"ha-service"===i.kind&&"ha-service"===s.kind&&function(e,t){const i=Zp(e),s=Zp(t);return i.length===s.length&&i.every((e,t)=>e===s[t])}(i.command,s.command))}const iu=e=>!!e&&e.length>=2&&Number.isFinite(e[0])&&Number.isFinite(e[1]);function su(e,t=5,i=5){return Ml(e.cm,t,i)/2}function nu(e,t,i=1,s=5,n=5,o=0){const a=e.host;if(!a||"partition"!==a.kind||"string"!=typeof a.id||!a.id)return{resolved:null,reason:"invalid-host"};const r=t.find(e=>e.id===a.id);if(!r)return{resolved:null,reason:"missing-partition"};if(!iu(r.a)||!iu(r.b)||!Number.isFinite(a.t)||a.t<0||a.t>1)return{resolved:null,reason:"invalid-position"};const l=r.b[0]-r.a[0],c=r.b[1]-r.a[1],h=Math.hypot(l,c),d=Number(e.length)*i;if(!(h>1e-9&&d>0&&Number.isFinite(d)))return{resolved:null,reason:"invalid-length"};const p=a.t*h;if(p-d/2h-o+1e-9)return{resolved:null,reason:"does-not-fit"};const u=l/h,_=c/h;let m=180*Math.atan2(c,l)/Math.PI;return m>=90?m-=180:m<-90&&(m+=180),{reason:null,resolved:{opening:e,host:a,partition:r,center:[r.a[0]+l*a.t,r.a[1]+c*a.t],angle:m,length:d,depth:Ml(r.cm,s,n),t:a.t,axis:{a:[r.a[0],r.a[1]],b:[r.b[0],r.b[1]],ux:u,uy:_,length:h}}}}function ou(e,t,i=1,s=5,n=5){return nu(e,t,i,s,n,0)}function au(e,t,i=1,s=5,n=5){const o=e.host,a="partition"===o?.kind?t.find(e=>e.id===o.id):void 0;if(!a)return ou(e,t,i,s,n);const r=nu(e,t,i,s,n,su(a,s,n));return"does-not-fit"===r.reason?{resolved:null,reason:"does-not-fit-jamb"}:r}function ru(e,t){return!e||(e.length!==t.length||e.host?.kind!==t.host?.kind||e.host?.id!==t.host?.id||e.host?.t!==t.host?.t)}function lu(e){const{center:t,length:i,axis:s,host:n}=e,o=i/2;return{hostId:n.id,a:[t[0]-s.ux*o,t[1]-s.uy*o],b:[t[0]+s.ux*o,t[1]+s.uy*o],depth:e.depth}}function cu(e,t=!1){const i=t?1:-1,s=e.depth/2;return{ox:-e.axis.uy*i*s,oy:e.axis.ux*i*s,cm:e.partition.cm,side:i}}function hu(e,t,i){return e.flatMap(e=>!iu(e.a)||!iu(e.b)||Math.hypot(e.b[0]-e.a[0],e.b[1]-e.a[1])<=1e-9?[]:[{roomId:"",a:[e.a[0],e.a[1]],b:[e.b[0],e.b[1]],key:`partition:${e.id}`,kind:"outer",cm:e.cm,open:!1,half:Ml(e.cm,t,i)/2,partitionHost:{kind:"partition",id:e.id}}])}function du(e,t,i){const{center:s,length:n,axis:o}=e,a=n/2,r=t.flatMap(e=>{if(!e.kind||e.open)return[];const t=e.b[0]-e.a[0],n=e.b[1]-e.a[1],r=Math.hypot(t,n);if(!(r>i))return[];const l=t/r,c=n/r;if(Math.abs(o.ux*c-o.uy*l)>1e-6)return[];if(Math.abs((s[0]-e.a[0])*c-(s[1]-e.a[1])*l)>i)return[];const h=e=>(e[0]-s[0])*o.ux+(e[1]-s[1])*o.uy,d=h(e.a),p=h(e.b),u=Math.max(-a,Math.min(d,p)),_=Math.min(a,Math.max(d,p));return _>=u-i?[[u,_]]:[]});if(!r.length)return!1;r.sort((e,t)=>e[0]-t[0]||e[1]-t[1]);let l=-a;for(const[e,t]of r){if(e>l+i)return!1;if(l=Math.max(l,t),l>=a-i)return!0}return!1}function pu(e,t,i){return{...e,x:t.center[0]/i,y:t.center[1]/i,angle:t.angle}}const uu=.001;function _u(e){return!!e&&e.length>=2&&Number.isFinite(e[0])&&Number.isFinite(e[1])}function mu(e){return`${e[0].toFixed(6)},${e[1].toFixed(6)}`}function gu(e,t,i){return Math.abs(e[0]-t[0])"room"!==e.kind&&i.some(i=>i!==e&&function(e,t,i){const s=e.b[0]-e.a[0],n=e.b[1]-e.a[1],o=Math.hypot(s,n);if(!(o>i))return!1;const a=s/o,r=n/o,l=t=>Math.abs((t[0]-e.a[0])*r-(t[1]-e.a[1])*a);if(l(t.a)>i||l(t.b)>i)return!1;const c=(t.a[0]-e.a[0])*a+(t.a[1]-e.a[1])*r,h=(t.b[0]-e.a[0])*a+(t.b[1]-e.a[1])*r;return Math.min(o,Math.max(c,h))-Math.max(0,Math.min(c,h))>i}(e,i,t))),n=s.map(e=>{const[t,i]=fu(e.a,e.b);return{a:t,b:i,key:`hidden|${bu(e,t,i)}`,sourceKind:e.kind,sourceId:e.id}}).sort((e,t)=>e.key.localeCompare(t.key)),o=s.flatMap(e=>[e.a,e.b].map((t,i)=>({point:[t[0],t[1]],key:`hidden|${vu(e)}|endpoint-${i}`,sourceKind:e.kind,sourceId:e.id}))).sort((e,t)=>e.key.localeCompare(t.key));return{segments:n,endpoints:o}}function xu(e){const t=e.epsilon??uu,i=e.roomCuts||[],s=new Map;for(const t of e.partitionCuts||[]){if("string"!=typeof t.hostId||!t.hostId||!_u(t.a)||!_u(t.b))continue;const e=s.get(t.hostId)||[];e.push([t.a[0],t.a[1],t.b[0],t.b[1]]),s.set(t.hostId,e)}const n=[];for(const[t,s]of pi(e.space.rooms).entries())s.length<4||n.push({a:[s[0],s[1]],b:[s[2],s[3]],kind:"room",id:`room-edge-${t}`,cuts:i});for(const t of e.space.room_drafts||[])if(t.id!==e.activeDraftId)for(let e=0;e+1wu(s,e,t)))continue;const e=mu(s);a.has(e)||a.set(e,{point:[s[0],s[1]],key:e})}}}return{segments:[...o.values()].sort((e,t)=>e.key.localeCompare(t.key)),endpoints:[...a.values()].sort((e,t)=>e.key.localeCompare(t.key))}}function $u(e,t,i){return t.some(t=>_u(t)&&gu(e,t,i))}function Su(e,t,i){if(!i)return!0;const s=e-i.distance;return s<-1e-9||Math.abs(s)<=1e-9&&t.localeCompare(i.key)<0}function Mu(e,t,i){const s=i.epsilon??uu,n=i.excludePoints||[],o=[...e.endpoints,...(i.extraEndpoints||[]).filter(e=>_u(e.point)).map(e=>({point:[e.point[0],e.point[1]],key:e.key}))],a=new Map;for(const e of o){if($u(e.point,n,s))continue;const t=mu(e.point),i=a.get(t);(!i||e.key.localeCompare(i.key)<0)&&a.set(t,e)}return[...a.values()].map(e=>({kind:"endpoint",point:[...e.point],key:e.key,distance:Math.hypot(t[0]-e.point[0],t[1]-e.point[1])})).filter(e=>e.distance<=i.tolerance).sort((e,t)=>e.distance-t.distance||e.key.localeCompare(t.key))}function Cu(e,t){if(!e.length)return null;if(t>0){const i=new Set;for(let s=0;s1)return{kind:"ambiguous",candidate:null,conflicts:e.filter(e=>i.has(e.key)).map(e=>({point:[...e.point],key:e.key}))}}return{kind:"resolved",candidate:e[0],conflicts:[]}}function Du(e,t,i){const s=e.b[0]-e.a[0],n=e.b[1]-e.a[1],o=Math.hypot(s,n);if(!(o>0))return[...e.a];const a=s/o,r=n/o,l=Math.max(0,Math.min(o,(t[0]-e.a[0])*a+(t[1]-e.a[1])*r)),c=i>0?Math.max(0,Math.min(o,Math.round(l/i)*i)):l;return[e.a[0]+a*c,e.a[1]+r*c]}function Pu(e,t,i){if(!(_u(t)&&_u(i.anchor)&&i.tolerance>=0))return{kind:"none",candidate:null,conflicts:[]};const s=i.epsilon??uu,n=function(e,t){const i=t[0]-e[0],s=t[1]-e[1];if(!(Math.hypot(i,s)>Number.EPSILON))return null;const n=Math.round(Math.atan2(s,i)/(Math.PI/4))*(Math.PI/4);return[Math.cos(n),Math.sin(n)]}(i.anchor,t);if(!n)return{kind:"none",candidate:null,conflicts:[]};const o=Mu(e,t,i).filter(e=>function(e,t,i,s){const n=e[0]-t[0],o=e[1]-t[1];return n*i[0]+o*i[1]>s&&Math.abs(n*i[1]-o*i[0])<=s*Math.max(Math.hypot(n,o),1)}(e.point,i.anchor,n,s)),a=Cu(o,i.distinguishTolerance||0);if(a)return a;let r=null;for(const o of e.segments){const e=o.b[0]-o.a[0],a=o.b[1]-o.a[1],l=o.a[0]-i.anchor[0],c=o.a[1]-i.anchor[1],h=n[0]*a-n[1]*e;let d=null;if(Math.abs(h)<=s*Math.max(Math.hypot(e,a),1)){if(Math.abs(l*n[1]-c*n[0])>s*Math.max(Math.hypot(l,c),1))continue;const e=[o.a,o.b].map(e=>({point:e,along:(e[0]-i.anchor[0])*n[0]+(e[1]-i.anchor[1])*n[1]})).filter(e=>e.along>s).sort((e,t)=>e.along-t.along);e.length&&(d=[...e[0].point])}else{const t=(l*a-c*e)/h,o=(l*n[1]-c*n[0])/h;t>s&&o>=-s&&o<=1+s&&(d=[i.anchor[0]+n[0]*t,i.anchor[1]+n[1]*t])}if(!d)continue;const p=Math.hypot(t[0]-d[0],t[1]-d[1]);p>i.tolerance||$u(d,i.excludePoints||[],s)||!Su(p,o.key,r)||(r={kind:"line",point:d,key:o.key,distance:p,segment:o})}return r?{kind:"resolved",candidate:r,conflicts:[]}:{kind:"none",candidate:null,conflicts:[]}}const Tu=.001;function Iu(e,t,i,s){const n=Number.isFinite(e)&&e>0?Math.floor(e):0,o=e=>"number"==typeof e&&Number.isFinite(e)&&e>=0?e:null,a=o(i),r=o(s)??0,l=[];let c=null;for(let e=0;e=2&&Number.isFinite(e[0])&&Number.isFinite(e[1])}function zu(e,t,i,s){return e*s-t*i}function Fu(e){let t=0;for(let i=0;ie.localeCompare(t)),t[0]}function Lu(e,t,i){const s=[...e];for(let e=!0;e&&s.length>=3;){e=!1;for(let n=0;n=0){s.splice(n,1),e=!0;break}}}return s}function Bu(e,t){const i=e.map(e=>Math.max(0,Math.min(1,e))).sort((e,t)=>e-t),s=[];for(const e of i)(!s.length||Math.abs(e-s[s.length-1])>t)&&s.push(e);return s}function ju(e,t){return[e.a[0]+(e.b[0]-e.a[0])*t,e.a[1]+(e.b[1]-e.a[1])*t]}function qu(e,t,i){const s=function(e,t){const i=t.b[0]-t.a[0],s=t.b[1]-t.a[1],n=i*i+s*s;return n>0?((e[0]-t.a[0])*i+(e[1]-t.a[1])*s)/n:0}(e,t);if(s<-i||s>1+i)return null;const n=ju(t,s);return Math.hypot(n[0]-e[0],n[1]-e[1])<=i?Math.max(0,Math.min(1,s)):null}function Wu(e,t,i,s,n){const o=e.b[0]-e.a[0],a=e.b[1]-e.a[1],r=t.b[0]-t.a[0],l=t.b[1]-t.a[1],c=t.a[0]-e.a[0],h=t.a[1]-e.a[1],d=zu(o,a,r,l),p=Math.max(Math.hypot(o,a),Math.hypot(r,l),1);if(Math.abs(d)>n*p){const e=zu(c,h,r,l)/d,t=zu(c,h,o,a)/d;return void(e>=-n&&e<=1+n&&t>=-n&&t<=1+n&&(i.push(Math.max(0,Math.min(1,e))),s.push(Math.max(0,Math.min(1,t)))))}if(!(Math.abs(zu(c,h,o,a))>n*p)){for(const s of[t.a,t.b]){const t=qu(s,e,n);null!=t&&i.push(t)}for(const i of[e.a,e.b]){const e=qu(i,t,n);null!=e&&s.push(e)}}}function Uu(e){let t=e+1|0;return t^=t<<13,t^=t>>>17,t^=t<<5,t>>>0}function Gu(e,t){return e.minY-t.minY||e.index-t.index}function Vu(e){return e.subtreeMaxY=Math.max(e.item.maxY,e.left?.subtreeMaxY??-1/0,e.right?.subtreeMaxY??-1/0),e}function Ku(e){const t=e.right;return e.right=t.left,t.left=Vu(e),Vu(t)}function Yu(e){const t=e.left;return e.left=t.right,t.right=Vu(e),Vu(t)}function Ju(e,t){return e?(Gu(t,e.item)<0?(e.left=Ju(e.left,t),e.left.priority0)e.right=Xu(e.right,t);else{if(!e.left)return e.right;if(!e.right)return e.left;e.left.priority=t&&Zu(e.left,t,i,s),e.item.minY<=i&&e.item.maxY>=t&&s.push(e.item),e.item.minY<=i&&Zu(e.right,t,i,s))}function Qu(e,t=.001){const i=Number.isFinite(t)&&t>0?t:Tu,s=e.filter(e=>Au(e.a)&&Au(e.b)&&"string"==typeof e.key&&e.key.length>0&&Math.hypot(e.b[0]-e.a[0],e.b[1]-e.a[1])>i),n=s.map(()=>[0,1]);!function(e,t,i){const s=e.map((e,t)=>({index:t,minX:Math.min(e.a[0],e.b[0])-i,maxX:Math.max(e.a[0],e.b[0])+i,minY:Math.min(e.a[1],e.b[1])-i,maxY:Math.max(e.a[1],e.b[1])+i})),n=[...s].sort((e,t)=>e.minX-t.minX||e.minY-t.minY||e.index-t.index),o=[...s].sort((e,t)=>e.maxX-t.maxX||e.index-t.index),a=new Set;let r=null,l=0;for(const s of n){for(;le.index-t.index);for(const o of n)Wu(e[o.index],e[s.index],t[o.index],t[s.index],i);a.add(s.index),r=Ju(r,s)}}(s,n,i);const o=new Map;for(let e=0;e({key:e,a:t.a,b:t.b,sourceKeys:[...t.sourceKeys].sort((e,t)=>e.localeCompare(t))})).sort((e,t)=>e.key.localeCompare(t.key))}function e_(e,t=.001){const i=Number.isFinite(t)&&t>0?t:Tu,s=Qu(e,i),n=new Map,o=new Map,a=new Map(s.map(e=>[e.key,e]));for(const e of s){const t=Eu(e.a,i),s=Eu(e.b,i);n.set(t,e.a),n.set(s,e.b),o.has(t)||o.set(t,new Set),o.has(s)||o.set(s,new Set),o.get(t).add(s),o.get(s).add(t)}const r=new Map;for(const[e,t]of o){const i=n.get(e);r.set(e,[...t].sort((e,t)=>{const s=n.get(e),o=n.get(t);return Math.atan2(s[1]-i[1],s[0]-i[0])-Math.atan2(o[1]-i[1],o[0]-i[0])||e.localeCompare(t)}))}const l=new Set,c=new Map,h=(e,t)=>`${e}>${t}`;for(const e of s){const t=[Eu(e.a,i),Eu(e.b,i)];for(const[e,o]of[t,[t[1],t[0]]]){if(l.has(h(e,o)))continue;const t=[],d=[];let p=e,u=o,_=!1;for(let i=0;i<=2*s.length+2;i++){const i=h(p,u);if(l.has(i)){_=p===e&&u===o;break}l.add(i),t.push(p),d.push(Ou(p,u));const s=r.get(u)||[],n=s.indexOf(p);if(n<0||!s.length)break;if(p=u,u=s[(n-1+s.length)%s.length],p===e&&u===o){_=!0;break}}if(!_||new Set(t).size<3||new Set(t).size!==t.length)continue;const m=t.map(e=>n.get(e)),g=Fu(m);if(!(g>i*i))continue;const f=Hu(Lu(t,n,i)),v=new Set;for(const e of d)for(const t of a.get(e)?.sourceKeys||[])v.add(t);const b={ring:m,key:f,area:g,atomKeys:[...d],sourceKeys:[...v].sort((e,t)=>e.localeCompare(t))};c.has(f)||c.set(f,b)}}return{atoms:s,faces:[...c.values()].sort((e,t)=>e.area-t.area||e.key.localeCompare(t.key))}}function t_(e,t,i=.001){if(!Au(t))return null;const s=e.faces.filter(e=>{if(e.ring.some((s,n)=>function(e,t,i,s){const n=i[0]-t[0],o=i[1]-t[1],a=n*n+o*o;if(!(a>0))return Math.hypot(e[0]-t[0],e[1]-t[1])<=s;const r=((e[0]-t[0])*n+(e[1]-t[1])*o)/a;return!(r<0||r>1)&&Math.hypot(t[0]+n*r-e[0],t[1]+o*r-e[1])<=s}(t,s,e.ring[(n+1)%e.ring.length],i)))return!1;let s=!1;for(let i=0,n=e.ring.length-1;it[1]!=a[1]>t[1]&&t[0]<(a[0]-o[0])*(t[1]-o[1])/(a[1]-o[1])+o[0]&&(s=!s)}return s});return[...s].sort((e,t)=>e.area-t.area||e.key.localeCompare(t.key))[0]||null}function i_(e,t,i){const s=i[0]-t[0],n=i[1]-t[1],o=s*s+n*n,a=o>0?Math.max(0,Math.min(1,((e[0]-t[0])*s+(e[1]-t[1])*n)/o)):0;return Math.hypot(t[0]+s*a-e[0],t[1]+n*a-e[1])}function s_(e,t,i=.5){let s=Math.abs(e-t)%180;return s>90&&(s=180-s),s<=i}function n_(e,t,i,s){const n=e.filter(e=>"outer"===e.kind&&!e.open&&e.cm>0).map(e=>({interval:e,reusePartitionId:t.find(t=>function(e,t,i){return!(Math.abs(e.cm-t.cm)>1e-6)&&(s_(180*Math.atan2(e.b[1]-e.a[1],e.b[0]-e.a[0])/Math.PI,180*Math.atan2(t.b[1]-t.a[1],t.b[0]-t.a[0])/Math.PI,.001)&&i_(t.a,e.a,e.b)<=i&&i_(t.b,e.a,e.b)<=i)}(t,e,s))?.id||null})),o=new Map;for(const t of i){if("partition"===t.host?.kind)continue;const i=e.map(e=>{const i=180*Math.atan2(e.b[1]-e.a[1],e.b[0]-e.a[0])/Math.PI;return{interval:e,distance:s_(t.angle,i)?i_([t.x,t.y],e.a,e.b):1/0}}).filter(e=>e.distance<=s).sort((e,t)=>e.distance-t.distance||e.interval.key.localeCompare(t.interval.key)),n=i[0]?.interval;"outer"===n?.kind&&!n.open&&n.cm>0&&o.set(t.id,n.key)}return{materialize:n,openingIntervals:o,removeOpeningIds:[...o.keys()].sort((e,t)=>e.localeCompare(t))}}function o_(e,t){const i=t.b[0]-t.a[0],s=t.b[1]-t.a[1],n=i*i+s*s;return n>0?Math.max(0,Math.min(1,((e[0]-t.a[0])*i+(e[1]-t.a[1])*s)/n)):0}const a_=e=>!e.key.startsWith("static:room|");function r_(e,t){const i=t.b[0]-t.a[0],s=t.b[1]-t.a[1],n=i*i+s*s;if(!(n>0))return null;const o=((e[0]-t.a[0])*i+(e[1]-t.a[1])*s)/n,a=[t.a[0]+i*o,t.a[1]+s*o];return{point:a,t:o,distance:Math.hypot(a[0]-e[0],a[1]-e[1])}}function l_(e,t){return t>0?Math.hypot(e[0]-Math.round(e[0]/t)*t,e[1]-Math.round(e[1]/t)*t):0}function c_(e,t,i){if(a_(e.source)!==a_(t.source))return a_(e.source)?e:t;const s=l_(e.point,i),n=l_(t.point,i);return Math.abs(s-n)>1e-9?s>n?e:t:function(e,t){return e[0]-t[0]||e[1]-t[1]}(e.point,t.point)>0?e:t}function h_(e){return[e.from[0],e.from[1],e.to[0],e.to[1],e.targetKind].map(e=>"number"==typeof e?e.toFixed(6):e).join("|")}function d_(e,t){return e.map(e=>e.key!==t.sourceKey?e:{...e,[t.endpoint]:[...t.to]})}function p_(e,t){const i=t.epsilon??.001;if(!(t.maxDistance>=0)||!t.point&&!t.requiredSourceKey)return{kind:"none"};const s=new Map,n=e.flatMap(e=>[{source:e,endpoint:"a",point:e.a},{source:e,endpoint:"b",point:e.b}]);for(let e=0;ei)||l>t.maxDistance)continue;const c=c_(a,r,t.gridStep||0),h=c===a?r:a;if(!a_(c.source))continue;const d={sourceKey:c.source.key,endpoint:c.endpoint,from:[c.point[0],c.point[1]],to:[h.point[0],h.point[1]],targetSourceKey:h.source.key,targetKind:"endpoint",distance:l};s.set(h_(d),d)}for(const o of n)if(a_(o.source))for(const n of e){if(n===o.source)continue;const e=r_(o.point,n);if(!e||e.t<=i||e.t>=1-i||!(e.distance>i)||e.distance>t.maxDistance)continue;const a={sourceKey:o.source.key,endpoint:o.endpoint,from:[o.point[0],o.point[1]],to:e.point,targetSourceKey:n.key,targetKind:"line",distance:e.distance};s.set(h_(a),a)}const o=[];for(const n of s.values()){const s=e_(d_(e,n),i),a=t.point?t_(s,t.point,i):s.faces.find(e=>!t.requiredSourceKey||e.sourceKeys.includes(t.requiredSourceKey))||null;a&&o.push({proposal:n,face:a})}if(o.sort((e,t)=>e.proposal.distance-t.proposal.distance||h_(e.proposal).localeCompare(h_(t.proposal))),!o.length)return{kind:"none"};return new Set(o.map(e=>`${h_(e.proposal)}|${e.face.key}`)).size>1?{kind:"ambiguous",proposals:o.map(e=>e.proposal)}:{kind:"repair",...o[0]}}const u_=1e-5;function __(e){const t=e.map(()=>[]);for(let i=0;i=1-1e-9||p<=1e-9||p>=1-1e-9||(t[i].push(d),t[a].push(p))}}const i=[];for(let s=0;se-t),1];for(let e=1;e{const i=t[2]-t[0],s=t[3]-t[1],n=i*i+s*s;if(!(n>0))return Math.hypot(e[0]-t[0],e[1]-t[1]);const o=Math.max(0,Math.min(1,((e[0]-t[0])*i+(e[1]-t[1])*s)/n));return Math.hypot(e[0]-(t[0]+o*i),e[1]-(t[1]+o*s))},f_=(e,t,i,s)=>{const n=s[2]-s[0],o=s[3]-s[1],a=t*o-i*n;if(Math.abs(a)<1e-12)return 1/0;const r=s[0]-e[0],l=s[1]-e[1],c=(r*o-l*n)/a;if(!(c>1e-9))return 1/0;const h=(r*i-l*t)/a;return h<-1e-9||h>1+1e-9?1/0:c};function v_(e,t,i,s=96){if(!(t>0&&Number.isFinite(e[0])&&Number.isFinite(e[1])))return[];const n=[];for(const s of i){if(!s||s.length<4)continue;if(![s[0],s[1],s[2],s[3]].every(Number.isFinite))continue;const i=g_(e,s);if(i<1e-7)return[];i>t||n.push(s)}const o=[],a=Math.max(12,Math.round(s));for(let e=0;ee-t);const l=[];let c=Number.NEGATIVE_INFINITY;for(const i of o){if(i-c<1e-9)continue;c=i;const s=Math.cos(i),o=Math.sin(i);let a=t;for(const t of n){const i=f_(e,s,o,t);i=3?l:[]}class b_ extends he{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 pa(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},t=this._floorToken;return t?e.floor=t:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],t=this._lang,i=[{value:"",label:ua(t,"editor.floor_none")}],s=this._floorToken;s&&i.push({value:s,label:ua(t,"editor.floor_index",{index:String(this._config?.floor)})});const n="string"==typeof this._config?.floor?this._config.floor:"";n&&!e.some(e=>e.value===n)&&i.push({value:n,label:n}),i.push(...e);const o="string"==typeof this._config?.default_floor?this._config.default_floor:"",a=[...e];return o&&!e.some(e=>e.value===o)&&a.unshift({value:o,label:o}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:i}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:a}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:da(ua(t,"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 G;this._loadSpaces();const e=this._lang,t={title:ua(e,"editor.title"),floor:ua(e,"editor.floor"),default_floor:ua(e,"editor.default_floor"),language:ua(e,"editor.language"),icon_size:ua(e,"editor.icon_size"),show_temperature:ua(e,"editor.show_temperature"),live_states:ua(e,"editor.live_states"),show_signal:ua(e,"editor.show_signal"),kiosk:ua(e,"editor.kiosk"),cycle:ua(e,"editor.cycle")},i=this._schema,s=function(e,t,i){if(!i||null===t)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||t.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),n=e=>q`t[e.name]||e.name} - @value-changed=${this._valueChanged} - >`;return q` - ${n(i.slice(0,3))} - ${s?q``:G} - ${n(i.slice(3))} - `}_valueChanged(e){const t={...this._config,...e.detail.value};""===t.floor?delete t.floor:t.floor===this._floorToken&&(t.floor=this._config?.floor);const i=new Event("config-changed",{bubbles:!0,composed:!0});i.detail={config:t},this.dispatchEvent(i)}}function y_(e){const t=e.length/2,i=e.face.cm>0?e.face.cm/e.cellCm*e.gridPitch/2:Dh(4,e.cellCm),s="gate"===e.type?Math.sin(10*Math.PI/180)*t:0;return{half:t,jambHalf:i,gateDepth:s,outlineHalf:Math.max(Dh(16,e.cellCm),i+Dh(8,e.cellCm),s+Dh(8,e.cellCm)),hitHalf:Math.max(Dh(20,e.cellCm),i+Dh(10,e.cellCm),s+Dh(12,e.cellCm))}}function w_(e){if("passage"===e.type)return W``;const t=Math.max(0,Math.min(1,e.amount)),{half:i,jambHalf:s}=y_(e),n=Ch(e.cellCm),o=1.75*n,a=e.flipH?-1:1,r=e.flipV?-1:1,l="gate"===e.type?1:r;e.type,e.flipV,e.angle,e.face;let c;if("window"===e.type){const o=Math.PI/2*i,a=e.face.cm>0?W``:W``;c=W` - - - - - - - - - - - - - - ${a} - `}else if("gate"===e.type){const s=10*e.face.side*t;c=W` - - - - - - - - - - - - `}else{const s=Math.PI/2*e.length;c=W` - - - - - - - - `}return W` - - - ${c} - `}b_.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",b_);const k_={availability:"available",status:"neutral",activity:"none"};function x_(e){return e<=40?"low":e<180?"mid":"high"}function $_(e){return ai(e)}function S_(e){return"static_icon"===e.display?"neutral":"alarm"===e.visual.status?"alarm":"unavailable"===e.visual.availability?"unavailable":e.lockState?e.lockState:"working"===e.visual.status?"working":"open"===e.visual.status?"open":"neutral"}const M_={sourceKind:"none",decisionIds:["source.skipped_static_fast_path"],visualSources:[],criticalSources:[],samples:[]};function C_(e,t){const i=e?.entities?.[t],s=e?.states?.[t];return String(i?.name||i?.original_name||s?.attributes?.friendly_name||t)}function D_(e,t){const i=e?.states?.[t];if(!i)return"";if("function"==typeof e?.formatEntityState)try{const t=e.formatEntityState(i);if("string"==typeof t&&t)return t}catch{}return String(i.state??"")}function P_(e,t){if(t.virtual||"virtual"===t.bindingKind||"virtual"===t.marker?.binding)return"available";return(t.entities||[]).some(t=>{const i=String(e?.states?.[t]?.state??"").trim().toLowerCase();return""!==i&&"unknown"!==i&&"unavailable"!==i})?"available":"unavailable"}function T_(e,t,i,s){return{eid:t,role:i,name:C_(e,t),state:String(e?.states?.[t]?.state??""),stateText:D_(e,t),integrationDomain:e?.entities?.[t]?.platform||null,sample:s||cn(e,t)}}function I_(e,t,i=[t],s,n=e){const o=t.hidden&&t.userHidden?{...t,hidden:!1}:t;let a="none",r=[];const l=[],c=[...o.entities,...o.allEntities||[]].some(e=>e.startsWith("cover."))?{...o,tapAction:"cover"}:o,h=(d=Xp({hass:e,devices:i,device:c,lightSources:s,registryHass:n}),"cover"!==d?.semantics?null:d.targets[0]?.entityId||d.skippedTargets[0]?.entityId||null);var d;const p=ho(e,[o]),u=Zn(o.marker?.binding,o.marker?.controls??o.controls,o.entities),_=new Set(u.filter(e=>e.startsWith("marker:"))),m=!0===o.marker?.is_light||_.size>0?s||ho(e,i):p,g=m.filter(e=>e.device.id===o.id&&"controls"!==e.via),f=p.filter(e=>"controls"!==e.via),v=g.length?g:f,b=bn(o.marker),y=b?[...v]:[...p.filter(e=>"controls"===e.via),...v];if(_.size)for(const e of m){if(!_.has(e.key))continue;e.stateEids.length>0&&y.some(t=>t.stateEids.some(t=>e.stateEids.includes(t)))||y.some(t=>t.key===e.key)||y.push({...e,via:"controls",castsGlow:!1})}const w=Yn(n,o.entities),k=!!h&&("cover"===o.tapAction||o.primary?.startsWith("cover.")||w.some(e=>e.startsWith("cover.")));if(h&&!k&&l.push("source.cover_capability_bypassed"),k)a="cover",l.push("source.cover"),r=[T_(e,h,"cover")];else if(y.length)a=!b&&y.some(e=>"controls"===e.via)?"controls":"light",b?l.push("source.manual_virtual_light"):l.push("controls"===a?"source.controls":"source.owned_light"),"controls"!==a||!o.virtual&&"virtual"!==o.bindingKind||l.push("source.virtual_controller"),r=y.map(i=>{const s="controls"===i.via?"control":"forced"===i.via?"forced_light":"light";if(!i.passive)return T_(e,i.eid,s);const n=i.on?"on":"off";return{eid:i.key,role:s,name:t.name,state:n,stateText:n,integrationDomain:null,sample:{eid:i.key,state:n,availability:"available",status:i.on?"working":"neutral",activity:"none",edge:"none"}}});else{const t=Yn(n,o.entities),i=t.length?t:o.entities.filter(t=>!!e?.states?.[t]);if(i.length){a="device_role",l.push("source.device_role");const t=hn(n===e?e:{...n,states:e?.states||{}},i,o.entities),s=i.some(e=>an(n,e));r=t.map(t=>{const i=s?rn(n,t.eid)?"power_gate":an(n,t.eid)?"lifecycle":"device_role":"device_role";return T_(e,t.eid,i,t)})}else o.primary?(a="primary",l.push("source.primary_fallback"),r=[T_(e,o.primary,"primary")]):l.push("source.none")}u.length>0&&"controls"!==a&&"light"!==a&&"cover"!==a&&l.push("source.filtered_saved_controls");const x=[];for(const t of o.entities||[]){const i=cn(e,t);"alarm"!==i.status||r.some(e=>e.eid===t)||x.push(T_(e,t,"critical",i))}return x.length&&l.push("source.critical_sibling"),{sourceKind:a,decisionIds:l,visualSources:r,criticalSources:x,samples:[...r,...x].map(e=>e.sample)}}function R_(e,t,i,s){if(t.virtual)return{source:null,text:null,fallback:"value_virtual"};if(s){const i=function(e,t){if(!0!==t.marker?.use_climate_temp)return null;for(const i of t.entities){if(!i.startsWith("climate."))continue;const t=Number(e?.states?.[i]?.attributes?.current_temperature);if(Number.isFinite(t))return{eid:i,text:Math.round(10*t)/10+"°"}}return null}(e,t);if(i)return{source:{kind:"temperature",eid:i.eid,attribute:"current_temperature",text:i.text},text:i.text,fallback:null};const s=function(e,t){if("mdi:thermometer"!==t.icon&&"mdi:air-filter"!==t.icon)return null;for(const i of t.entities){if(!Gn(e,i))continue;const t=Number(e?.states?.[i]?.state);if(Number.isFinite(t))return{eid:i,text:Math.round(10*t)/10+"°"}}return null}(e,t);if(s)return{source:{kind:"temperature",eid:s.eid,text:s.text},text:s.text,fallback:null};const n=function(e,t){if(!t.primary||!fo(e,t.primary))return null;const i=Number(e?.states?.[t.primary]?.state);return Number.isFinite(i)?{eid:t.primary,text:`${Math.round(i)}%`}:null}(e,t);if(n)return{source:{kind:"humidity",eid:n.eid,text:n.text},text:n.text,fallback:null}}const n=i.visualSources.filter(e=>!e.eid.startsWith("marker:"));let o=n.map(e=>e.eid);const a=n.find(e=>"power_gate"===e.role);if(a&&(o=[a.eid]),!o.length&&i.visualSources.some(e=>e.eid.startsWith("marker:"))&&(o=Yn(e,t.entities),!o.length&&t.primary&&e?.states?.[t.primary]&&(o=[t.primary])),o=[...new Set(o)],1!==o.length)return{source:null,text:null,fallback:o.length?"value_ambiguous_sources":"value_no_state"};const r=o[0],l=function(e,t){const i=e?.states?.[t];if(!i||null==i.state||""===String(i.state).trim())return{text:"",fallback:"value_no_state"};const s=i.state;if(!["string","number","boolean"].includes(typeof s))return{text:"",fallback:"value_non_scalar"};const n=String(s).trim().toLowerCase();if("unknown"===n||"unavailable"===n)return{text:"",fallback:"value_no_state"};const o=Xi(e,t);return o?{text:Zi(o,String(i.attributes?.unit_of_measurement||"")),fallback:null}:{text:"",fallback:"value_no_state"}}(e,r);return l.fallback?{source:null,text:null,fallback:l.fallback}:{source:{kind:"entity",eid:r,text:l.text},text:l.text,fallback:null}}function A_(e,t,i){const s=e.marker?.binding||(e.bindingKind&&e.bindingRef?`${e.bindingKind}:${e.bindingRef}`:e.virtual?"virtual":""),n=[...t.visualSources.map(e=>`${e.role}:${e.eid}`),...t.criticalSources.map(e=>`critical:${e.eid}`)].sort(),o=i?`${i.kind}:${i.eid}:${i.attribute||""}`:"none";return[s,t.sourceKind,...n,`value:${o}`].join("\n")}function z_(e,t,i){return A_(t,i||I_(e,t),null).replace(/\nvalue:none$/,"")}function F_(e){if(e.effectiveHidden)return[];if("static_icon"===e.display)return["static-icon"];const t=[],{visual:i}=e;return"alarm"===e.pulse.kind?t.push("alarm"):"unavailable"===i.availability?t.push("unavail"):"working"===i.status?t.push("on"):"open"===i.status&&t.push("open"),e.lockState&&t.push(`lock-${e.lockState}`),"none"!==e.pulse.reason&&"alarm"!==e.pulse.reason&&t.push("activity-"+e.pulse.reason),e.pulse.generation%2==0&&(t.push("pulse-gen2"),"short"===e.pulse.kind&&t.push("activity-gen2")),t}function N_(e,t,i){const s=Bi(t.marker?.display),n="static_icon"===s,o=n&&!1===i.sourceDetails?M_:I_(e,t,i.lightDevices||[t],i.lightSources,i.registryHass||e),a=t.bindingStatus,r="ha_disabled"===a?.kind,l="orphaned"===a?.kind,c=r?"ha_disabled":l?"orphaned":"active",h=!0===t.userHidden||!0===t.marker?.hidden,d=dn(o.samples),p=o.visualSources.find(e=>e.eid.startsWith("lock.")),u=p?["unlocked","open"].includes(p.state.toLowerCase())?"unlocked":"locked"===p.state.toLowerCase()?"locked":null:null,_=!bn(t.marker)&&Zn(t.marker?.binding,t.marker?.controls??t.controls,t.entities).length>0,m="controls"===o.sourceKind||_&&"light"!==o.sourceKind&&"cover"!==o.sourceKind,g=n&&!1===i.sourceDetails?{source:null,text:null,fallback:null}:R_(e,t,o,i.showTemperature),f=A_(t,o,g.source),v=z_(e,t,o),b=i.activityRuntime,y=i.now??Date.now(),w=b?.expiresAt||(b?.flashTs?b.flashTs+3300:0),k=b?.sources===v&&b.flashTs&&b.flashKind&&w>y?b.flashKind:null,x=function(e){const t=[],i="ha_disabled"===e.bindingLifecycle||"orphaned"===e.bindingLifecycle;let s=!1;"ha_disabled"===e.bindingLifecycle?(s=!0,t.push("lifecycle.ha_disabled_hidden")):e.userHidden&&!e.designPreview?(s=!0,t.push("lifecycle.user_hidden")):e.userHidden?t.push("lifecycle.user_hidden_preview"):"orphaned"===e.bindingLifecycle?t.push("lifecycle.orphaned_diagnostic"):t.push("lifecycle.active");const n="static_icon"===e.display;let o=e.controllerFace?{...e.sourceVisual,availability:e.controllerAvailability}:e.sourceVisual;t.push(e.controllerFace?"available"===e.controllerAvailability?"availability.controller_available":"availability.controller_unavailable":"availability.source"),s?(o=k_,t.push("face.hidden")):n?(o=k_,t.push("face.static")):"alarm"===e.sourceVisual.status||e.liveStates?t.push("face.dynamic"):(o=k_,t.push("face.live_states_disabled")),!n&&!s&&e.liveStates&&"alarm"!==e.sourceVisual.status&&e.shortActivity&&(o={...o,activity:e.shortActivity},t.push(`activity.short_${e.shortActivity}`)),"alarm"===o.status?t.push("status.alarm"):"unavailable"===o.availability?t.push("status.unavailable"):"working"===o.status?t.push("status.working"):"open"===o.status?t.push("status.open"):t.push("status.neutral");const a="value"===e.display&&!s&&e.valueAvailable?"value":"icon";t.push("value"===a?"content.value":"value"===e.display?"content.value_fallback_icon":"content.icon"),"value"===e.display&&!e.valueAvailable&&e.valueFallback&&t.push(`content.${e.valueFallback}`);const r=e.liveStates&&!n&&!s,l=!n&&!s,c=!s&&!i&&!n&&"available"===o.availability&&("alarm"===o.status||e.liveStates&&"icon_ripple"===e.display),h=l&&e.vacuumLiveRequested;return t.push(r?"diagnostics.dynamic_icon":"diagnostics.base_icon"),t.push(l?"diagnostics.metrics_enabled":"diagnostics.metrics_suppressed"),t.push(c?"activity.pulse_eligible":"activity.pulse_suppressed"),t.push(h?"diagnostics.vacuum_live":"diagnostics.vacuum_static"),{effectiveHidden:s,bindingUnavailable:i,visual:o,face:a,dynamicIcon:r,metrics:l,liveColor:r,pulseEligible:c,vacuumLive:h,decisionIds:t}}({bindingLifecycle:c,userHidden:h,designPreview:!0===i.designPreview,display:s,liveStates:i.liveStates,sourceVisual:d,controllerFace:m,controllerAvailability:m?P_(e,t):"available",shortActivity:k,valueAvailable:null!=g.text,valueFallback:g.fallback,vacuumLiveRequested:!0===t.marker?.vacuum?.live}),{effectiveHidden:$,visual:S}=x,M="icon_ripple"===s&&!$&&i.liveStates&&"alarm"!==S.status?S.activity:"none",C=x.metrics&&i.showTemperature?!0===t.marker?.use_climate_temp?go(e,t.entities):"mdi:thermometer"===t.icon||"mdi:air-filter"===t.icon?mo(e,t.entities):null:null,D=x.metrics&&i.showTemperature&&t.primary&&fo(e,t.primary)?vo(e,t.entities):null,P=x.metrics&&i.showSignal&&!t.virtual?_o(e,t.entities):null,T=i.lightSources||("derived_marker_state"===t.marker?.value_badge?.source?.kind?ho(e,i.lightDevices||[t]):[]),I=function(e,t,i){const s=null!=t.marker?.value_badge;if(i.effectiveHidden||"static_icon"===i.display)return null;const n=t.marker?.value_badge;if(s)return n?.enabled?n.source?{configured:!0,enabled:!0,position:n.position||"right",...Ho(e,t,n.source,i.markerStates)}:{configured:!0,enabled:!0,source:null,sourceLabel:"",text:"—",fullText:e?.localize?.("state.default.unavailable")||"Unavailable",position:n.position||"right",availability:"missing",isLqi:!1,tone:"default"}:null;if(!i.showTemperature||"value"===i.display)return null;const o=Lo(e,t);if(!o)return null;let a=null,r="default";if("entity_attribute"===o.kind&&"current_temperature"===o.attribute){const i=go(e,t.entities);null!=i&&(a=`${i}°`),r="temperature"}else if("mdi:thermometer"===t.icon||"mdi:air-filter"===t.icon){const i=mo(e,t.entities);null!=i&&(a=`${i}°`),r="temperature"}else{const i=vo(e,t.entities);null!=i&&(a=`${i}%`),r="humidity"}return null==a?null:{configured:!1,enabled:!0,source:o,position:"right",sourceLabel:"entity_attribute"===o.kind?`${No(o.attribute)} · ${Fo(e,o.entity_id)}`:"entity_state"===o.kind?Fo(e,o.entity_id):"",text:a,fullText:a,availability:"available",isLqi:!1,tone:r}}(e,t,{showTemperature:i.showTemperature,showSignal:i.showSignal,display:s,effectiveHidden:$,markerStates:T.filter(e=>e.key.startsWith("marker:")).map(e=>({ref:e.key,on:e.on,name:e.device.name}))}),R=o.visualSources.find(e=>!e.eid.startsWith("marker:")),A="cover"===o.sourceKind?R?.eid:t.primary||R?.eid,z=A?e?.states?.[A]:null,F=x.dynamicIcon?vs(t.icon,A?.split(".")[0],z?.attributes?.device_class,z?.state,!!t.marker?.icon):t.icon,N=x.liveColor&&o.visualSources.filter(e=>!e.eid.startsWith("marker:")).map(t=>function(e){return e&&"on"===e.state?ze(e.attributes?.rgb_color):null}(e?.states?.[t.eid])).find(e=>!!e)||null,E=Number(t.marker?.size)>0?Number(t.marker.size):1,O=Number(t.marker?.angle)||0,H=Number(t.marker?.ripple_size)>0?Number(t.marker.ripple_size):1.5,L=Ie(t.marker?.ripple_color,null),B=n?null:L||N||null,j=function(e){const{display:t,visual:i,semanticActivity:s,liveStates:n,effectiveHidden:o,bindingUnavailable:a=!1,reducedMotion:r=!1}=e,l=Math.max(1,Math.trunc(e.shortGeneration||1)),c=Number.isFinite(e.diameterScale)?Math.max(1,Number(e.diameterScale)):1.5,h=e.color||function(e,t){return"presence"===t?"#1DC21D":"running"===t||"working"===e.status||"open"===e.status?"#F0A00C":"#0C82F0"}(i,s);if(o||a||"unavailable"===i.availability||"static_icon"===t)return Yo(l,h,c);if("alarm"===i.status)return{kind:"alarm",reason:"alarm",generation:l,expiresAt:null,color:"#F0410C",diameterScale:1.5,animated:!r,reducedMotionIndicator:"none"};if(!n||"icon_ripple"!==t)return Yo(l,h,c);const d=e.now??Date.now(),p=e.shortExpiresAt||null;return e.shortReason&&null!=p&&p>d?{kind:"short",reason:e.shortReason,generation:l,expiresAt:p,color:h,diameterScale:r?1:c,animated:!r,reducedMotionIndicator:r?"dot":"none"}:"presence"===s||"transition"===s||"running"===s?{kind:"continuous",reason:s,generation:l,expiresAt:null,color:h,diameterScale:r?1:c,animated:!r,reducedMotionIndicator:r?"dot":"none"}:Yo(l,h,c)}({display:s,visual:S,semanticActivity:d.activity,shortReason:b?.sources===v?b?.flashKind:null,shortGeneration:b?.gen,shortExpiresAt:b?.sources===v?w:null,now:y,liveStates:i.liveStates,effectiveHidden:$,bindingUnavailable:x.bindingUnavailable,reducedMotion:i.reducedMotion,color:B,diameterScale:H}),q="value"===x.face?g.text:null,W="ha_disabled"===a?.kind?a.reason:null,U="ha_disabled"===(G={lifecycle:c,display:s,liveStates:i.liveStates,sourceKind:o.sourceKind,primaryDomain:(t.primary||"").split(".")[0],visual:S,activity:M}).lifecycle?"ha_disabled":"orphaned"===G.lifecycle?"orphaned":"static_icon"===G.display?"static_icon":"alarm"===G.visual.status?"alarm":G.liveStates?"unavailable"===G.visual.availability?"unavailable":"cover"===G.sourceKind?"cover_icon_state":"presence"===G.activity?"presence":"event"===G.activity?"event":"transition"===G.activity?"transition":"working"===G.visual.status?"icon_ripple"===G.display&&"none"!==G.activity?"working_activity":"working":"open"===G.visual.status?"open":"media_player"===G.primaryDomain?"media_neutral":"neutral":"live_states_disabled";var G;const V=[];i.designPreview&&h&&V.push("hidden_design_preview"),n||!0!==t.marker?.vacuum?.live||V.push("vacuum_live_plan_only");const K=o.visualSources.some(t=>"power_gate"===t.role||rn(e,t.eid)),Y=t.entities.filter(t=>t.startsWith("switch.")&&!e?.entities?.[t]?.entity_category).length;K&&Y>1&&V.push("composite_power_source"),"icon_ripple"!==s&&"static_icon"!==s&&"alarm"!==d.status&&"none"!==d.activity&&V.push("activity_display_disabled");const J={binding:t.marker?.binding||(t.bindingKind&&t.bindingRef?`${t.bindingKind}:${t.bindingRef}`:t.virtual?"virtual":""),sourceKind:o.sourceKind,visualSources:o.visualSources,criticalSources:o.criticalSources,valueSource:g.source,sourceSignature:f,decisionIds:[...o.decisionIds,...x.decisionIds,...I?["diagnostics.value_badge"]:[],...null==P?[]:[`diagnostics.lqi_${x_(P)}`],`pulse.${j.kind}_${j.reason}`],visual:S,lockState:u,display:s,icon:F,valueText:q,valueFullText:q,fallbackReason:"value"!==s||q?null:g.fallback,activity:M,activityGeneration:b?.gen||1,pulse:j,classes:[],tempText:null==C?null:String(C),humText:null==D?null:String(D),valueBadge:I,lqiText:null==P||I?.isLqi?null:String(P),lqiColor:null==P?null:$_(P),lqiBand:null==P?null:x_(P),lightColor:N,scale:E,angle:O,rippleScale:H,rippleColor:B,userHidden:h,effectiveHidden:$,haDisabled:r,disabledReason:W,orphaned:l,vacuumLive:x.vacuumLive,explanation:{reason:U,notices:V}};return{...J,classes:F_(J)}}function E_(e,t=new WeakMap){if(null===e||"object"!=typeof e)return e;const i=e,s=t.get(i);if(s)return s;if(Array.isArray(e)){const s=[];t.set(i,s);for(const i of e)s.push(E_(i,t));return Object.freeze(s)}const n={};t.set(i,n);for(const[i,s]of Object.entries(e))"function"!=typeof s&&(n[i]=E_(s,t));return Object.freeze(n)}function O_(e){const t=new Map(e);let i;return i=Object.freeze({get size(){return t.size},get:e=>t.get(e),has:e=>t.has(e),entries:()=>t.entries(),keys:()=>t.keys(),values:()=>t.values(),forEach:(e,s)=>t.forEach((t,n)=>e.call(s,t,n,i)),[Symbol.iterator]:()=>t[Symbol.iterator]()}),i}function H_(e){const t=new Set(e.entityIds||[]),i=new Set(e.deviceIds||[]),s=new Set(e.areaIds||[]);for(const s of e.devices){for(const e of s.entities||[])t.add(e);s.primary&&t.add(s.primary);for(const e of s.controls||[])t.add(e);s.marker?.vacuum?.source&&t.add(s.marker.vacuum.source),"device"===s.bindingKind&&s.bindingRef&&i.add(s.bindingRef),"entity"===s.bindingKind&&s.bindingRef&&t.add(s.bindingRef)}const n={},o={};for(const[o,a]of Object.entries(e.hass?.entities||{})){const r=a?.device_id?e.hass?.devices?.[a.device_id]:null;(t.has(o)||a?.device_id&&i.has(a.device_id)||s.has(a?.area_id)||s.has(r?.area_id))&&(t.add(o),n[o]=a,a?.device_id&&i.add(a.device_id))}for(const t of i){const i=e.hass?.devices?.[t];i&&(o[t]=i)}const a={};for(const i of t){const t=e.hass?.states?.[i];t&&(a[i]=t)}const r=Object.freeze({states:E_(a),entities:E_(n),devices:E_(o),config:E_(e.hass?.config),locale:E_(e.hass?.locale),themes:E_(e.hass?.themes)});return Object.freeze({sourceSequence:e.sourceSequence,capturedAt:e.capturedAt??Date.now(),hass:r,devices:E_([...e.devices]),positions:O_([...e.positions||[]].map(([e,t])=>[e,Object.freeze({x:t.x,y:t.y})])),presentations:O_([...e.presentations].map(([e,t])=>[e,E_(t)])),facts:O_([...e.facts||[]].map(([e,t])=>[e,E_(t)]))})}const L_=(e,t)=>`${e}:${t?1:0}`;function B_(e){return!Number.isFinite(e)||e<=0?2.25:e/2.5*2.25}function j_({openings:e,geometries:t,fillsByRoomId:i,idPrefix:s="data",groupClass:n="opening-tunnels",dataLayer:o="data"}){const a=e.map((e,n)=>{const o=t[n];if(!o)return G;const a=o.faces.find(e=>-1===e.side),r=o.faces.find(e=>1===e.side);if(!a||!r)return G;const l=i.get(a.roomId)||null,c=i.get(r.roomId)||null;if(!l&&!c)return G;const h=`${a.d} ${r.d}`,d=!!l&&!!c&&l.color===c.color&&l.opacity===c.opacity,p=`translate(${e.rx} ${e.ry}) rotate(${e.angle})`;if(d)return W``;const u=o.maxY-o.minY;if(!(u>0))return G;const _=`${(100*Math.max(0,Math.min(1,-o.minY/u))).toFixed(6)}%`,m=l||{color:"#000000",opacity:0},g=c||{color:"#000000",opacity:0},f=`hp-opening-tunnel-${s.replace(/[^a-zA-Z0-9_-]/g,"-")}-${n}`;return W``});return W``}const q_=e=>Array.isArray(e)&&e.length>=2&&Number.isFinite(Number(e[0]))&&Number.isFinite(Number(e[1])),W_=(e,t,i)=>[Number(e[0])*i,Number(e[1])*i,Number(t[0])*i,Number(t[1])*i],U_=e=>{const t=`${Number(e[0]).toFixed(9)},${Number(e[1]).toFixed(9)}`,i=`${Number(e[2]).toFixed(9)},${Number(e[3]).toFixed(9)}`;return t<=i?`${t}|${i}`:`${i}|${t}`},G_=e=>{const t=new Map;for(const i of e.flat())!Array.isArray(i)||i.length<4||!i.every(Number.isFinite)||Math.hypot(i[2]-i[0],i[3]-i[1])<=1e-9||t.set(U_(i),[...i]);return[...t.values()]};function V_(e){return"solid"===e?.zero_wall_style?"solid":"dashed"}function K_(e,t=1,i){const s=[];for(const n of Array.isArray(e?.wall_segments)?e.wall_segments:[])0===Number(n?.cm)&&q_(n?.a)&&q_(n?.b)&&(i&&!i.has(String(n.id||""))||s.push(W_(n.a,n.b,t)));return G_([s])}function Y_(e,t=1){const i=[];for(const s of Array.isArray(e?.partitions)?e.partitions:[])0===Number(s?.cm)&&q_(s?.a)&&q_(s?.b)&&i.push(W_(s.a,s.b,t));for(const s of Array.isArray(e?.room_drafts)?e.room_drafts:[]){const e=Array.isArray(s?.points)?s.points:[];for(let n=0;n+1e?.host?.kind===t.kind&&e.host.id===t.id)}const Q_=new WeakMap,em=new WeakMap;function tm(e){const t=Eh(e.cfg),i={};for(const t of e.cfg.spaces||[])for(const e of t.rooms||[])e.area&&(i[e.area]=t.id);const s=e.cfg.settings?.exclude_integrations?new Set(e.cfg.settings.exclude_integrations):Ye,n=Xe(e.cfg.settings?.icon_rules?.length?e.cfg.settings.icon_rules:Je);return So({hass:e.hass,registry:e.registry,areaToSpace:i,markers:e.cfg.markers||[],settings:e.cfg.settings||{},excluded:s,showAll:!!e.cfg.settings?.show_all,firstSpaceId:t[0]?.id||"",loc:t=>ua(e.lang,t),iconRules:n})}function im(e){const t=Eh(e.cfg).find(t=>t.id===e.spaceId);if(!t)return null;const i=cs(e.cfg.spaces.find(t=>t.id===e.spaceId)),s=ps(e.cfg.settings),n=e.iconSize??2.5,o=n>8?2.5:n,a=B_(o),r=e.registry?En(e.hass,e.registry):e.hass,l=e.registry?On(e.hass,e.registry):e.hass,c=(e.devices||tm(e)).filter(t=>t.space===e.spaceId),h=c.filter(e=>!e.hidden),d=function(e,t,i){const s={},n=i/100*nd(t)*1.3;for(const i of t.rooms){if(!i.area)continue;const t=e.filter(e=>e.area===i.area);if(!t.length)continue;const o=rd(i),a=.1*Math.min(o.w,o.h),r=o.w-2*a,l=o.h-2*a,c=Math.max(1,Math.round(Math.sqrt(t.length*r/Math.max(l,1)))),h=r/c,d=l/Math.max(Math.ceil(t.length/c),1),p=t.map((e,t)=>({x:o.x+a+h*(t%c+.5),y:o.y+a+d*(Math.floor(t/c)+.5)}));Oi(p,o,n,.5*a),t.forEach((e,t)=>s[e.id]=Wh(p[t]))}return s}(c,t,o),p=[];for(const t of h){const i=e.layout[t.id];if(i&&i.s===e.spaceId){const e=i.x*Ih,t=i.y*Ih;p.push({minX:e,minY:t,maxX:e,maxY:t})}}const u=e.cfg.spaces.find(t=>t.id===e.spaceId)||{},_=Array.isArray(u.walls)?u.walls:[],m=Number(u.cell_cm)>0?Number(u.cell_cm):5,g=X_(u,t,Ih,.02*Bh),f=(u.openings||[]).flatMap(e=>{if("partition"!==e.host?.kind)return[];const i=ou(e,t.partitions,Ih,m,Bh).resolved;return i?[i]:[]}),v=Jd({partitions:t.partitions,roomDrafts:t.room_drafts,columns:t.wall_columns,cellCm:m,hostedOpenings:f.map(e=>({id:e.opening.id,host:e.host,length:e.length,type:e.opening.type}))}),b=function(e,t,i,s){let n=em.get(e);n||(n=new Map,em.set(e,n));const o=n.get(t);if(o?.fingerprint===i)return o.value;const a=s();return n.set(t,{fingerprint:i,value:a}),a}(e.cfg,t.id,v,()=>_h(t,m,Bh,2e-4*Bh,f.map(lu)).all);for(const e of b){const t=e.map(e=>e[0]),i=e.map(e=>e[1]);t.length&&p.push({minX:Math.min(...t),minY:Math.min(...i),maxX:Math.max(...t),maxY:Math.max(...i)})}for(const e of g.lines)p.push({minX:Math.min(e[0],e[2]),minY:Math.min(e[1],e[3]),maxX:Math.max(e[0],e[2]),maxY:Math.max(e[1],e[3])});const y=id(t,p),w=[y.x,y.y,y.w,y.h],k=new Map(t.rooms.map(t=>{const n=zs(i.fill,t);return[t,_s(n,"lqi"===n&&t.area?Do(r,c,t.area):null,"light"===n?uo(ho(r,c,t,e.virtualLights)):"none","temp"===n&&t.area?Po(r,c,t.area):null,i.tempMin,i.tempMax,s,ls(i.customFill,t))]})),x=new Map;for(const e of t.rooms)e.id&&x.set(e.id,k.get(e)||null);const $=function(e,t=1e3){const i=[];for(const[s,n]of(e||[]).entries()){if("passage"!==n?.type)continue;const e={...n,id:String(n.id||`passage-${s}`),type:"passage",rx:Number(n.x)*t,ry:Number(n.y)*t,rlen:Number(n.length)*t};[e.rx,e.ry,e.rlen,e.angle].every(Number.isFinite)&&e.rlen>0&&i.push(e)}return i}((u.openings||[]).flatMap(e=>{if(!e.host||"wall"===e.host.kind)return[e];const t=f.find(t=>t.opening.id===e.id);return t?[pu(e,t,Ih)]:[]}),Ih),S=wc(t.rooms,_,g.contour,jh,m,Bh,Ih),M=f.filter(e=>du(e,S,2e-4*Bh)).map(e=>({x:e.center[0],y:e.center[1],angle:e.angle,length:e.length})),C=t.rooms.filter(e=>e.area||i.showBorders||"none"!==zs(i.fill,e)).map(e=>{let s="room "+(t.bg?"overlay":"yard"),n="";const o=zs(i.fill,e);if(i.showBorders||"none"!==o){s+=" styled";const t=[`--room-stroke:${i.color}`,`--room-stroke-op:${i.showBorders&&!g.contour.length?i.opacity:0}`],o=k.get(e)||null;o?(s+=" filled",t.push(`--room-fill:${o.color}`,`--room-fill-op:${o.opacity.toFixed(3)}`)):t.push("--room-fill:transparent","--room-fill-op:0"),n=t.join(";")}const a=e.id||G,r=e.area||G,l=e.poly?W``:W``;return l}),D=t.rooms.filter(e=>{const t=k.get(e);return Fs(i.glow,e)&&(!t||t.opacity<=0)}).map(e=>e.poly?W``:W``),P=new Map;for(const e of t.rooms)if(e.id){const t=k.get(e)||null;P.set(e.id,Fs(i.glow,e)&&(!t||t.opacity<=0)?{color:s.glow_base.c,opacity:s.glow_base.a,mode:"glow"}:null)}const T=ho(r,h,null,e.virtualLights),I=h.map(s=>{const n=function(e,t,i,s,n){const o=t[e.id];return o&&o.s===e.space?{x:o.x*Ih,y:o.y*Ih}:s[e.id]?s[e.id]:Wh(sd(n))}(s,e.layout,e.cfg,d,t),o=(n.x-w[0])/w[2]*100,a=(n.y-w[1])/w[3]*100,c=i.showLqi??!1!==e.showSignal,p=e.presentations?.get(L_(s.id,c))||N_(r,s,{liveStates:!1!==e.liveStates,showTemperature:!1!==e.showTemperature,showSignal:c,activityRuntime:e.activityRuntime?.get(s.id),sourceDetails:!1,lightDevices:h,lightSources:T,registryHass:l,reducedMotion:e.reducedMotion}),u=[`left:${o}%`,`top:${a}%`,...Go(p)],_=S_(p),m=[s.name,ua(e.lang,`marker.state_a11y_${_}`),"none"!==p.pulse.kind?ua(e.lang,`marker.pulse_a11y_${p.pulse.reason}`):"",p.valueFullText||p.valueText||"",Io(p.valueBadge),null!=p.lqiText&&p.lqiBand?ua(e.lang,`marker.lqi_a11y_${p.lqiBand}`,{value:p.lqiText}):""].filter(Boolean).join(", ");return q``}),R=i.showNames?t.rooms.filter(e=>e.name).map(s=>{const n=ld(s,t.id,e.layout,e.cfg),o=(n.x-w[0])/w[2]*100,a=(n.y-w[1])/w[3]*100,r=Math.min(1,i.opacity+.25);return q`
${s.name}
`}):[],A=t.bg?e.displayUrl?e.displayUrl(t.bg.href):t.bg.href:"",z=e.cfg.spaces.find(t=>t.id===e.spaceId)?.settings||{},F="daynight"===Fr(e.cfg?.settings,z)?$r(r,e.dayCycleNow??new Date):null,N=hs(e.cfg?.settings,i),E=!!(_.length||b.length&&i.showBorders),O=E?Jd($.length?{rooms:t.rooms,walls:_,extras:b,cellCm:m,zero:g.contour,passages:$.map(e=>({x:e.rx,y:e.ry,angle:e.angle,length:e.rlen})),hostedCompositeOpenings:M}:{rooms:t.rooms,walls:_,extras:b,cellCm:m,zero:g.contour}):"",H=E?function(e,t,i,s){let n=Q_.get(e);n||(n=new Map,Q_.set(e,n));const o=n.get(t);if(o?.fingerprint===i)return o.value;const a=s();return n.set(t,{fingerprint:i,value:a}),a}(e.cfg,t.id,O,()=>Wc(t.rooms,_,g.contour,[...$.filter(e=>"partition"!==e.host?.kind).map(e=>({x:e.rx,y:e.ry,angle:e.angle,length:e.rlen})),...M],jh,m,Bh,Ih,b)):null,L=$.length&&_.length?function(e,t,i,s,n,o,a,r=1){return[n,o,a,r].every(Number.isFinite)&&o>0&&r>0&&i?.length?th(Kc(e,i,s,n,o,a,r),t):t.map(()=>null)}(t.rooms,$.map(e=>({x:e.rx,y:e.ry,angle:e.angle,length:e.rlen})),_,g.contour,jh,m,Bh,Ih):$.map(()=>null),B=$.length?j_({openings:$,geometries:L,fillsByRoomId:x,idPrefix:`${t.id}-static-data`,groupClass:"opening-tunnels static-opening-tunnels",dataLayer:"data"}):G,j=$.length?j_({openings:$,geometries:L,fillsByRoomId:P,idPrefix:`${t.id}-static-glow`,groupClass:"opening-tunnels glow-base-tunnels static-opening-tunnels",dataLayer:"glow-base"}):G,U=_.length&&H?.paperD?[{path:H.paperD}]:di(t.rooms),V=i.showBorders?H:null,K=e.stageWidth&&w[2]?e.stageWidth/w[2]:1,Y=kl(m),J=!!V&&(wl(V.depthUnits,K)||xl(Y,K)),X=i.color||"#607d8b",Z=i.hideOpenings?[]:f.map(e=>{const t=e.opening,i="passage"!==t.type&&t.contact?r.states?.[t.contact]?.state:null,s=mi(t.type,i,!!t.invert),n=s>0&&!!t.contact,o="gate"===t.type?!t.flip_v:!!t.flip_v,a={type:t.type,length:e.length,angle:e.angle,amount:s,flipH:!!t.flip_h,flipV:!!t.flip_v,base:X,tone:n?"var(--hp-open)":X,cellCm:m,gridPitch:Bh,face:cu(e,o)};return W` - ${w_(a)} - `});return q` -
- ${Lr(F)} - - ${V?W` - - - - `:G} - ${U.map(e=>"path"in e?W``:"poly"in e?W``:W``)} - ${A?W`e.assetLoaded?.(t.bg.href,A)} - transform=${t.bg.angle?`rotate(${t.bg.angle} ${t.bg.x+t.bg.w/2} ${t.bg.y+t.bg.h/2})`:G} - preserveAspectRatio="none" />`:G} - ${C} - ${i.showBorders&&g.contour.length?W``:G} - ${B} - ${D.length?W``:G} - ${j} - ${V?W` - ${V.paths.map(e=>W` - - `)} - `:G} - ${i.showBorders&&g.lines.length?W``:G} - ${Z} - - ${""} -
${I}${R}
-
- `}let sm=null,nm=null,om=0,am=-1,rm=null,lm=[];const cm=new Set,hm=(e,t)=>{"function"==typeof e&&t.push(e)};function dm(){if(sm)return sm;try{const e=JSON.parse(localStorage.getItem("houseplan_card_cfg_v1")||"null");if(e&&e.config&&Array.isArray(e.config.spaces)){const t=e.layout||{};return{config:e.config,rev:e.rev||0,configFingerprint:e.config_fingerprint||Jd(e.config),layout:t,layoutRev:e.layout_rev||0,layoutFingerprint:e.layout_fingerprint||Jd(t),virtualLights:mn(e.virtual_lights,e.rev||0)}}}catch{}return null}function pm(e,t=!1){if(t&&(sm=null,om++,nm))return nm.catch(()=>null).then(()=>pm(e,!1));if(sm)return Promise.resolve(sm);if(nm)return am!==om?nm.catch(()=>null).then(()=>pm(e,!1)):nm;return am=om,nm=async function(e,t){const[i,s]=await Promise.all([e.callWS({type:"houseplan/config/get"}),e.callWS({type:"houseplan/layout/get"})]),n=i?.rev??0,o=sm?gn(sm.virtualLights,i?.virtual_lights,n,!!i&&"virtual_lights"in i):mn(i?.virtual_lights,n),a={config:i?.config??null,rev:n,configFingerprint:Jd(i?.config??null),layout:s?.layout??{},layoutRev:s?.rev??0,layoutFingerprint:Jd(s?.layout??{}),virtualLights:o};t===om&&(sm=a);const r=e.connection;if(r?.subscribeEvents&&rm!==r){for(const e of lm)e();lm=[],rm=r;const e=()=>{sm=null,om++,cm.forEach(e=>e())},t=[];try{if(hm(await r.subscribeEvents(e,"houseplan_config_updated"),t),hm(await r.subscribeEvents(e,"houseplan_layout_updated"),t),hm(await r.subscribeEvents(e=>{if(sm){const t=wn(sm.virtualLights,e?.data);if(t===sm.virtualLights)return;sm={...sm,virtualLights:t}}else om++;cm.forEach(e=>e())},"houseplan_virtual_light_updated"),t),rm===r)lm=t;else for(const e of t)e()}catch{for(const e of t)e();rm===r&&(rm=null)}}return a}(e,om).finally(()=>{nm=null,am=-1}),nm}const um=3300;function _m(e,t){return{sources:e,last:Object.fromEntries(t.map(e=>[e.eid,e.state])),flashTs:0,flashKind:null,timer:0,gen:0,expiresAt:0,alarmActive:"alarm"===dn(t).status}}function mm(e,t,i,s){s(e.timer);const n=_m(t,i);Object.assign(e,n)}function gm(e,t,i,s,n){e.flashTs&&i-e.flashTs"transition"===e.activity)&&(i(e.timer),e.flashTs=0,e.flashKind=null,e.expiresAt=0);if("alarm"===dn(t).status){e.alarmActive||(i(e.timer),e.flashTs=0,e.flashKind=null,e.expiresAt=0);for(const i of t)e.last[i.eid]=i.state;return e.alarmActive=!0,null}if(e.alarmActive){for(const i of t)e.last[i.eid]=i.state;return e.alarmActive=!1,null}let s=null;for(const i of t){const t=pn(e.last[i.eid],i);("event"===t||!s&&t)&&(s=t),e.last[i.eid]=i.state}return s}class vm{constructor(e,t=()=>{}){this._host=e,this._onGateChange=t,this._modality="unknown",this._gate=!1,this._media=null,this._connected=!1,this._onMediaChange=()=>this._syncGate()}get modality(){return this._modality}get hoverEnabled(){return this._gate}connect(e=globalThis.window){this._connected||(this._connected=!0,this._modality="unknown",this._media="function"==typeof e?.matchMedia?e.matchMedia("(any-hover: hover) and (any-pointer: fine)"):null,this._media?.addEventListener?.("change",this._onMediaChange),this._setGate(!1))}disconnect(){this._media?.removeEventListener?.("change",this._onMediaChange),this._media=null,this._connected=!1,this._modality="unknown",this._setGate(!1)}note(e){return this._modality=function(e,t){const i="mouse"===(s=t.pointerType)||"touch"===s||"pen"===s?s:null;var s;return i?"mouse"===i&&t.sourceCapabilities?.firesTouchEvents?e:i:e}(this._modality,e),this._syncGate(),this._modality}suspend(){this._setGate(!1)}_syncGate(){var e,t;this._setGate((e=this._modality,t=!!this._media?.matches,"mouse"===e&&t))}_setGate(e){this._gate!==e&&(this._gate=e,this._host.toggleAttribute("data-pointer-hover",e),this._onGateChange(e))}}class bm extends he{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!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}))}catch{this._spaces=[]}finally{this._spacesLoading=!1}}}get _lang(){return pa(this.hass,this._config?.language)}get _schema(){const e=this._spaces||[];return[e.length?{name:"space",selector:{select:{mode:"dropdown",options:e}}}:{name:"space",selector:{text:{}}},{name:"title",selector:{text:{}}},{name:"show_button",selector:{boolean:{}}},{name:"button_label",selector:{text:{}}},{name:"button_target",selector:{text:{}}},{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:{}}}]}render(){if(!this.hass||!this._config)return G;this._loadSpaces();const e=this._lang,t={space:ua(e,"editor.space"),title:ua(e,"editor.title"),show_button:ua(e,"editor.show_button"),button_label:ua(e,"editor.button_label"),button_target:ua(e,"editor.button_target"),icon_size:ua(e,"editor.icon_size"),show_temperature:ua(e,"editor.show_temperature"),live_states:ua(e,"editor.live_states"),show_signal:ua(e,"editor.show_signal")};return q`t[e.name]||e.name} - @value-changed=${this._valueChanged} - >`}_valueChanged(e){const t={...this._config||{},...e.detail.value};delete t.aspect_ratio;const i=new Event("config-changed",{bubbles:!0,composed:!0});i.detail={config:t},this.dispatchEvent(i)}}bm.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-space-card-editor")||customElements.define("houseplan-space-card-editor",bm);const ym=e=>{history.pushState(null,"",e),((e,t,i)=>{const s=new Event(t,{bubbles:!0,composed:!0});s.detail=i??{},e.dispatchEvent(s)})(window,"location-changed",{replace:!1})};class wm extends he{constructor(){super(...arguments),this._snap=null,this._loading=!1,this._reloadQueued=!1,this._forceReloadQueued=!1,this._reloadRetryTimer=0,this._dayCycleTimer=0,this._dayCycleClockKey="",this._stageWidth=0,this._pendingStageWidth=0,this._stageWidthRaf=0,this._haRegistryConnection=null,this._haRegistryRevision=-1,this._devices=[],this._continuity=this._newContinuityController(),this._continuityHistory=[],this._continuityEpoch=0,this._continuityDataReady=!0,this._continuityPaintToken=-1,this._continuityDisposed=!1,this._renderSnapshotAt=Date.now(),this._hassSequence=0,this._connHooked=null,this._connectionWasLost=!1,this._visibleDeviceSnapshot=null,this._candidateDeviceSnapshot=null,this._stagedDeviceSnapshotToken=-1,this._capturedSnapshotSequence=-1,this._capturedSnapshotDevices=null,this._capturedSnapshotActivity="",this._capturedSnapshotVirtual="",this._activityRuntime=new Map,this._reducedMotion=!1,this._pointerModality=new vm(this),this._onMotionChange=e=>{this._reducedMotion=e.matches,this._capturedSnapshotSequence=-1,this.requestUpdate()},this._onHaRegistryUpdate=()=>{const e=An(this.hass).revision;e!==this._haRegistryRevision&&(this._haRegistryRevision=e,this._refreshDevices(),this._capturedSnapshotSequence=-1,this.requestUpdate())},this._dayCycleTick=()=>{if(!this.isConnected||"hidden"===this.ownerDocument.visibilityState)return;const e=this._dayCycleState();if(!e)return this._dayCycleTimer&&(window.clearInterval(this._dayCycleTimer),this._dayCycleTimer=0),void(this._dayCycleClockKey="");const t=Sr(e);t!==this._dayCycleClockKey&&(this._dayCycleClockKey=t,this.requestUpdate())},this._pageVisibility=e=>{if(this._continuity.visibility(e),this._dayCycleVisibility(e),"hidden"!==e.kind){if(!e.long){const e=Date.now();let t=!1;for(const i of this._activityRuntime.values())!i.flashKind||(i.expiresAt||i.flashTs+3300)>e||(i.flashTs=0,i.flashKind=null,i.expiresAt=0,t=!0);return void(t&&(this._capturedSnapshotSequence=-1,this.requestUpdate()))}Date.now()-this._renderSnapshotAt>1e3&&this._continuity.note("device-snapshot-stale"),this._continuityDataReady=!1,this._continuityPaintToken=-1,this._load(!0)}else this._pointerModality.suspend()},this._onConnLost=()=>{this._connectionWasLost=!0,this._continuityDataReady=!1,this._continuityPaintToken=-1,this._continuity.connectionLost()},this._onConnReady=()=>{!this._connectionWasLost&&this._continuity.hasCompleteFrame?this._beginContinuityCandidate("connection-ready",!1,"plan"):(this._continuityDataReady=!1,this._continuityPaintToken=-1),this._load(!0)},this._onAssetLoaded=(e,t)=>{this._signer.markLoaded(this.hass,e,t),this._continuity.note("asset-ready"),this._continuityPaintToken=-1,"steady"!==this._continuity.state&&this.requestUpdate()},this._loadedOnce=!1,this._signer=new Qd(()=>this.requestUpdate()),this._retryContinuity=()=>{this._continuityDataReady=!1,this._continuityPaintToken=-1,this._continuity.retry(this._continuity.recoveryReason||"plan"),this._load(!0)},this._goToSpace=()=>{const e=(this._config?.button_target||"/plan-doma").replace(/#.*$/,"");ym(`${e}#space=${encodeURIComponent(this._config.space)}`)}}_ensureHaRegistryAuthority(){const e=this.hass?.connection||null;e&&e!==this._haRegistryConnection&&(this._haRegistryRelease?.(),this._haRegistryConnection=e,this._haRegistryRevision=-1,this._haRegistryRelease=Rn(this.hass,this._onHaRegistryUpdate),this._onHaRegistryUpdate())}_newContinuityController(){return new Yd(()=>{this._continuityEpoch++,this.isConnected&&this.requestUpdate()})}_dayCycleState(e=new Date){const t=this._snap?.config;if(!t||!this._config)return null;const i=t.spaces?.find(e=>e.id===this._config?.space)?.settings||{};return"daynight"!==Fr(t.settings,i)?null:$r(this._renderDeviceSnapshot?.hass||this.hass,e)}_syncDayCycleClock(){const e=this._dayCycleState();this._dayCycleClockKey=e?Sr(e):"";const t="clock"===e?.source&&"hidden"!==this.ownerDocument.visibilityState&&this.isConnected;t&&!this._dayCycleTimer?this._dayCycleTimer=window.setInterval(this._dayCycleTick,3e4):!t&&this._dayCycleTimer&&(window.clearInterval(this._dayCycleTimer),this._dayCycleTimer=0)}_dayCycleVisibility(e){"hidden"!==e.kind?(this._dayCycleTick(),this._syncDayCycleClock()):this._dayCycleTimer&&(window.clearInterval(this._dayCycleTimer),this._dayCycleTimer=0)}_hookConnection(){const e=this.hass?.connection;e&&e!==this._connHooked&&(this._connHooked?.removeEventListener?.("ready",this._onConnReady),this._connHooked?.removeEventListener?.("disconnected",this._onConnLost),this._connHooked?.removeEventListener?.("reconnect-error",this._onConnLost),e.addEventListener?.("ready",this._onConnReady),e.addEventListener?.("disconnected",this._onConnLost),e.addEventListener?.("reconnect-error",this._onConnLost),this._connHooked=e)}static getConfigElement(){return document.createElement("houseplan-space-card-editor")}static getStubConfig(e){const t=dm();return{type:"custom:houseplan-space-card",space:Eh(t?.config||null)[0]?.id||"",show_button:!0,live_states:!0,show_temperature:!0,show_signal:!0}}setConfig(e){if(!e||!e.space)throw new Error('houseplan-space-card: "space" is required');this._config={show_button:!0,button_target:"/plan-doma",live_states:!0,show_temperature:!0,show_signal:!0,...e},this._snap=this._snap||dm()}connectedCallback(){var e;this._continuityDisposed&&(this._continuity=this._newContinuityController(),this._continuityDisposed=!1,this._continuityPaintToken=-1),super.connectedCallback(),this._pointerModality.connect(this.ownerDocument.defaultView),this._motionMedia=window.matchMedia?.("(prefers-reduced-motion: reduce)"),this._reducedMotion=!!this._motionMedia?.matches,this._motionMedia?.addEventListener?.("change",this._onMotionChange),this.hass&&this._ensureHaRegistryAuthority(),this._continuityUnsub?.(),this._continuityUnsub=Vd(this.ownerDocument,this._pageVisibility),this._unsub=(e=()=>{this._beginContinuityCandidate("config-event",!1),this._reloadQueued=!0,this._load()},cm.add(e),()=>cm.delete(e)),this._signer.start(()=>this.hass,()=>this._referenced())}disconnectedCallback(){this._pointerModality.disconnect(),this._continuityUnsub?.(),this._continuityUnsub=void 0,this._motionMedia?.removeEventListener?.("change",this._onMotionChange),this._motionMedia=void 0,this._unsub?.(),this._unsub=void 0,window.clearTimeout(this._reloadRetryTimer),this._reloadRetryTimer=0,this._dayCycleTimer&&(window.clearInterval(this._dayCycleTimer),this._dayCycleTimer=0),this._dayCycleClockKey="",this._stageObserver?.disconnect(),this._stageObserver=void 0,this._observedStage=void 0,this._stageWidthRaf&&cancelAnimationFrame(this._stageWidthRaf),this._stageWidthRaf=0,this._pendingStageWidth=0,this._signer.dispose(),this._haRegistryRelease?.(),this._haRegistryRelease=void 0,this._haRegistryConnection=null,this._connHooked?.removeEventListener?.("ready",this._onConnReady),this._connHooked?.removeEventListener?.("disconnected",this._onConnLost),this._connHooked?.removeEventListener?.("reconnect-error",this._onConnLost),this._connHooked=null;for(const e of this._activityRuntime.values())window.clearTimeout(e.timer);this._activityRuntime.clear(),this._continuityHistory=[...this._continuityHistory,...this._continuity.trace].slice(-80),this._continuity.dispose(),this._continuityDisposed=!0,super.disconnectedCallback()}willUpdate(e){e.has("hass")&&this.hass&&(this._hassSequence++,this._renderSnapshotAt=Date.now(),this._continuity.note("hass-snapshot"),this._ensureHaRegistryAuthority(),this._hookConnection()),!this.hass||this._loading||this._snap&&!e.has("hass")||this._snap&&this._loadedOnce||this._load(),(e.has("hass")||e.has("_snap")||e.has("_config")||!this._devices.length)&&this._refreshDevices(),this._captureRenderDeviceSnapshot(),this._continuity.hasCompleteFrame&&"steady"===this._continuity.state&&this._continuity.refreshCompleteFrame(this._frameFingerprint())}_stampActivity(e,t){gm(e,t,Date.now(),window.clearTimeout.bind(window),e=>window.setTimeout(()=>this.requestUpdate(),e))}_syncActivity(e,t,i=this.hass){if(!1===this._config?.live_states){for(const e of this._activityRuntime.values())window.clearTimeout(e.timer);return void this._activityRuntime.clear()}const s=new Set,n=ho(t,e,null,this._snap?.virtualLights);for(const o of e){if(o.hidden)continue;if("icon_ripple"!==Bi(o.marker?.display))continue;s.add(o.id);const a=I_(t,o,e,n,i),r=a.samples,l=z_(t,o,a);let c=this._activityRuntime.get(o.id);if(!c||c.sources!==l){c&&window.clearTimeout(c.timer),c=_m(l,r),this._activityRuntime.set(o.id,c);continue}const h=fm(c,r,window.clearTimeout.bind(window));h&&this._stampActivity(c,h)}for(const[e,t]of this._activityRuntime)s.has(e)||(window.clearTimeout(t.timer),this._activityRuntime.delete(e))}_refreshDevices(){if(!this.hass||!this._snap?.config||!this._config)return;const e=An(this.hass),t=tm({hass:this.hass,registry:e,cfg:this._snap.config,lang:this._lang});this._syncActivity(t,En(this.hass,e),this.hass),this._devices=t}_captureRenderDeviceSnapshot(){if(!this.hass)return;const e=Date.now(),t=[...this._activityRuntime.entries()].map(([t,i])=>`${t}:${i.gen}:${i.flashTs}:`+(i.flashKind&&(i.expiresAt||i.flashTs+3300)>e?1:0)).join("|"),i=this._snap?.virtualLights?`${this._snap.virtualLights.configRev}:${this._snap.virtualLights.rev}`:"";if(this._capturedSnapshotSequence===this._hassSequence&&this._capturedSnapshotDevices===this._devices&&this._capturedSnapshotActivity===t&&this._capturedSnapshotVirtual===i)return;const s=En(this.hass,An(this.hass)),n=new Map,o=new Set(["sun.sun"]),a=new Set,r=new Set,l=e=>{if(!e)return;const t=e.indexOf(":");if(t<0)return void o.add(e);const i=e.slice(0,t),s=e.slice(t+1);"device"===i?a.add(s):"entity"===i&&o.add(s)},c=Eh(this._snap?.config||null).find(e=>e.id===this._config?.space);for(const e of c?.rooms||[])e.area&&r.add(e.area),l(e.settings?.temp_source),l(e.settings?.hum_source);const h=this._snap?.config?.spaces?.find(e=>e.id===this._config?.space);for(const e of h?.openings||[])for(const t of gi(e))o.add(t);const d=ho(s,this._devices,null,this._snap?.virtualLights);for(const e of this._devices)for(const t of[!1,!0])n.set(L_(e.id,t),N_(s,e,{liveStates:!1!==this._config?.live_states,showTemperature:!1!==this._config?.show_temperature,showSignal:t,activityRuntime:this._activityRuntime.get(e.id),sourceDetails:!1,lightDevices:this._devices,lightSources:d,registryHass:this.hass,reducedMotion:this._reducedMotion}));const p=H_({sourceSequence:this._hassSequence,hass:s,devices:this._devices,presentations:n,entityIds:o,deviceIds:a,areaIds:r});this._capturedSnapshotSequence=this._hassSequence,this._capturedSnapshotDevices=this._devices,this._capturedSnapshotActivity=t,this._capturedSnapshotVirtual=i,this._visibleDeviceSnapshot&&"steady"!==this._continuity.state?this._candidateDeviceSnapshot=p:(this._visibleDeviceSnapshot=p,this._candidateDeviceSnapshot=null)}get _renderDeviceSnapshot(){return this._stagedDeviceSnapshotToken===this._continuity.token?this._candidateDeviceSnapshot||this._visibleDeviceSnapshot:this._visibleDeviceSnapshot||this._candidateDeviceSnapshot}_beginContinuityCandidate(e,t,i="plan"){return this._continuityDataReady=t,this._continuityPaintToken=-1,this._stagedDeviceSnapshotToken=-1,this._continuity.beginCandidate(e,i)}_backdropRaw(){return this._snap?.config&&this._config&&Eh(this._snap.config).find(e=>e.id===this._config.space)?.bg?.href||""}_candidateBackdrop(e){return e&&this._config&&Eh(e).find(e=>e.id===this._config.space)?.bg?.href||""}_assetsReady(){const e=this._backdropRaw();return!e||this._signer.isReady(this.hass,e)}_frameFingerprint(){const e=this._snap;return Xd([e?.rev||0,e?.configFingerprint||Jd(e?.config),e?.layoutRev||0,e?.layoutFingerprint||Jd(e?.layout),e?.virtualLights?`${e.virtualLights.configRev}:${e.virtualLights.rev}`:"",this._config?.space||"",this._stageWidth,this.hass?.themes?.darkMode??this.hass?.themes?.default_theme??""])}_stageValid(){const e=this._observedStage;return!!e&&e.clientWidth>0&&e.clientHeight>0}_settleContinuityFrame(){if(!this._stageValid())return;if(!this._continuity.hasCompleteFrame&&"steady"===this._continuity.state){if(this._assetsReady())return this._renderSnapshotAt=Date.now(),void this._continuity.markCompleteFrame(this._frameFingerprint());this._beginContinuityCandidate("asset-wait",!0,"asset")}if(!this._continuityDataReady)return;if(!["holding","offline-stale","overlay-pending","overlay-visible","candidate-ready"].includes(this._continuity.state))return;const e=this._continuity.token;if(this._candidateDeviceSnapshot&&this._candidateDeviceSnapshot!==this._visibleDeviceSnapshot&&this._stagedDeviceSnapshotToken!==e)return this._stagedDeviceSnapshotToken=e,void this.requestUpdate();this._continuityPaintToken!==e&&(this._continuityPaintToken=e,this._continuity.candidateReady(e)&&this._continuity.commitAfterPaint(e,{updateComplete:()=>this.updateComplete,stageValid:()=>this.isConnected&&this._stageValid(),assetsReady:()=>this._assetsReady(),frameFingerprint:()=>this._frameFingerprint()}).then(t=>{t&&e===this._continuity.token?(this._renderSnapshotAt=Date.now(),this._candidateDeviceSnapshot&&(this._visibleDeviceSnapshot=this._candidateDeviceSnapshot),this._candidateDeviceSnapshot=null,this._stagedDeviceSnapshotToken=-1):e===this._continuity.token&&(this._continuityPaintToken=-1,this._stagedDeviceSnapshotToken=-1,this._candidateDeviceSnapshot=null,this.requestUpdate())}))}updated(){const e=this.renderRoot.querySelector(".hp-static-stage")||void 0;if(e!==this._observedStage)if(this._stageObserver?.disconnect(),this._observedStage=e,e){const t=()=>{const i=e.clientWidth;if(!(i<=0||Math.abs(i-this._stageWidth)<=.5)){if(this._stageWidth<=0)return this._stageWidth=i,void this.requestUpdate();this._pendingStageWidth=i,this._stageWidthRaf||(this._stageWidthRaf=requestAnimationFrame(()=>{this._stageWidthRaf=0;const e=this._pendingStageWidth;this._pendingStageWidth=0,!e||!this._observedStage||this._observedStage.clientWidth<=0||(Math.abs(this._observedStage.clientWidth-e)>.5?t():Math.abs(e-this._stageWidth)<=.5||(this._continuity.hasCompleteFrame&&this._beginContinuityCandidate("stage-resize",!0,"stage-size"),this._stageWidth=e,this.requestUpdate()))}))}};this._stageObserver=new ResizeObserver(t),this._stageObserver.observe(e),t()}else this._stageObserver=void 0;this._syncDayCycleClock(),this._settleContinuityFrame()}async _load(e=!1){if(this.hass){if(this._loading)return this._reloadQueued=!0,void(this._forceReloadQueued||=e);this._loading=!0,this._reloadQueued=!1;try{const t=await pm(this.hass,e),i=!this._snap||this._snap.configFingerprint!==t.configFingerprint,s=!this._snap||this._snap.layoutFingerprint!==t.layoutFingerprint,n=!this._snap||this._snap.virtualLights!==t.virtualLights;if(i&&!await this._signer.prepareImage(this.hass,this._candidateBackdrop(t.config)))return this._continuity.note("asset-failed"),window.clearTimeout(this._reloadRetryTimer),void(this._reloadRetryTimer=window.setTimeout(()=>{this._load(!0)},1e3));window.clearTimeout(this._reloadRetryTimer),this._reloadRetryTimer=0,(i||s)&&this._continuity.hasCompleteFrame&&"steady"===this._continuity.state&&this._beginContinuityCandidate("structural-response",!0),i||s||n?this._snap=t:this._snap&&(this._snap.rev=t.rev,this._snap.layoutRev=t.layoutRev),i&&this._continuity.note("config-candidate",{configRev:t.rev}),s&&this._continuity.note("layout-candidate",{layoutRev:t.layoutRev}),n&&(this._capturedSnapshotSequence=-1),this._loadedOnce=!0,this._connectionWasLost=!1,this._continuityDataReady=!0,this._refreshDevices()}catch{}finally{if(this._loading=!1,this._continuityDataReady=!0,this.requestUpdate(),this._reloadQueued){const e=this._forceReloadQueued;this._reloadQueued=!1,this._forceReloadQueued=!1,this._load(e)}}}}get _lang(){return pa(this.hass,this._config?.language)}getCardSize(){const e=Eh(this._snap?.config||null).find(e=>e.id===this._config?.space);if(e){const t=e.vb[3]/e.vb[2];return Math.max(3,Math.round(8*t))+(!1===this._config?.show_button?0:1)}return 6}_errorCard(e){return q` -
${e}
-
`}_referenced(){return Rs(this._snap?.config)}_renderRecoveryOverlay(){if(!this._continuity.overlayVisible&&"recovery-error"!==this._continuity.state)return G;const e="connection"===this._continuity.recoveryReason;return q`
- - ${ua(this._lang,e?"continuity.restore_connection":"continuity.restore_plan")} - ${"recovery-error"===this._continuity.state?q``:G} -
`}houseplanContinuityTrace(){return[...this._continuityHistory,...this._continuity.trace].slice(-80).map(e=>({...e,...e.stage?{stage:[...e.stage]}:{}}))}render(){if(!this._config)return G;const e=this._snap?.config;if(!e)return this._errorCard(ua(this._lang,"space_card.loading"));const t=this._config.space,i=this._renderDeviceSnapshot,s=im({hass:i?.hass||this.hass,cfg:e,layout:this._snap?.layout||{},spaceId:t,iconSize:this._config.icon_size,stageWidth:this._stageWidth,lang:this._lang,displayUrl:e=>this._signer.display(this.hass,e),assetLoaded:this._onAssetLoaded,registry:i?{revision:i.sourceSequence,authoritative:!1,access:"limited",entities:{...i.hass.entities},devices:{...i.hass.devices},lastSuccess:i.capturedAt}:An(this.hass),devices:[...i?.devices||this._devices],presentations:i?.presentations,activityRuntime:this._activityRuntime,reducedMotion:this._reducedMotion,virtualLights:this._snap?.virtualLights,liveStates:!1!==this._config.live_states,showTemperature:!1!==this._config.show_temperature,showSignal:!1!==this._config.show_signal,inert:this._continuity.overlayBlocksInteraction});if(!s)return this._errorCard(ua(this._lang,"space_card.not_found",{id:t}));const n=Eh(e).find(e=>e.id===t),o=void 0!==this._config.title?this._config.title:n?.title||"",a=!1!==this._config.show_button,r=this._config.button_label||ua(this._lang,"space_card.button"),l=this._continuity.overlayVisible||"recovery-error"===this._continuity.state?this._continuity.recoveryReason:null;return q` - this._pointerModality.note(e)} - @pointerdown=${e=>this._pointerModality.note(e)} - @pointermove=${e=>this._pointerModality.note(e)}> - ${o?q`
${o}
`:G} -
- ${s} - ${this._renderRecoveryOverlay()} -
- ${a?q`
- -
`:G} -
- `}}wm.properties={hass:{attribute:!1},_config:{state:!0},_snap:{state:!0},_continuityEpoch:{state:!0}},wm.styles=[Ke,o` - .hp-static-title { - font-weight: 700; - padding: 10px 14px 6px; - font-size: 16px; - color: var(--primary-text-color); - } - .hp-static-body { - position: relative; - } - .hp-static-stage { - position: relative; - width: 100%; - container-type: inline-size; - overflow: hidden; - pointer-events: none; /* kill ALL interaction on the schematic (§4) */ - background: var(--ha-card-background, var(--card-background-color, #111)); - } - .hp-static-stage svg { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - display: block; - z-index: 1; - } - /* Opaque plan paper — the scene bg_color/day-cycle environment shows only AROUND - the plan (owner 2026-08-03). The static card keeps its historical - canvas colour: the theme card background. Drawn plans paper the room - contours (per-room shapes, fill only, no stroke) — same contract as - the full card. */ - .hp-paper { - fill: var(--ha-card-background, var(--card-background-color, #111)); - stroke: none; - } - .wallbody-fill { - fill: var(--wall-fill, #ffffff); - fill-opacity: var(--wall-fill-op, 1); - fill-rule: evenodd; - stroke: none; - pointer-events: none; - } - .wallbody { - fill: url(#hp-wall-hatch); - fill-rule: evenodd; - stroke: var(--room-stroke, var(--hp-muted, #607d8b)); - stroke-width: calc(0.6px * var(--hp-cell-visual-scale, 1)); - pointer-events: none; - } - .wallbody.solid { - fill: none; - } - .hp-static-stage .devlayer { - position: absolute; - inset: 0; - z-index: 2; - } - .hp-static-foot { - padding: 8px 12px 12px; - pointer-events: auto; /* the button stays clickable */ - } - .hp-static-btn { - width: 100%; - padding: 9px 14px; - border: none; - border-radius: 10px; - background: var(--primary-color, #3ea6ff); - color: var(--text-primary-color, #fff); - font-size: 14px; - font-weight: 600; - cursor: pointer; - } - :host([data-pointer-hover]) .hp-static-btn:hover { - filter: brightness(1.08); - } - .hp-static-error { - padding: 24px; - text-align: center; - color: var(--secondary-text-color, #9aa4ad); - } - `],customElements.get("houseplan-space-card")||customElements.define("houseplan-space-card",wm),window.customCards=window.customCards||[],window.customCards.find(e=>"houseplan-space-card"===e.type)||window.customCards.push({type:"houseplan-space-card",name:"House Plan — Space (static)",description:"Read-only live schematic of a single houseplan space, with a deep-link button.",preview:!1,documentation:"https://github.com/Matysh/houseplan-card"});const km=o` - :host { - --hp-editor-tray-bg-fallback: color-mix(in srgb, var(--card-background-color, var(--hp-bg)) 94%, var(--hp-line)); - --hp-editor-tray-bg: color-mix(in srgb, var(--card-background-color, var(--hp-bg)) 86%, transparent); - --hp-editor-tray-border: color-mix(in srgb, var(--hp-line) 82%, var(--hp-accent)); - --hp-editor-tray-blur: 8px; - --hp-editor-tray-shadow: var(--shadow-2); - } - - /* Contextual editor controls float over the stage. Their appearance must - never alter stage height, viewport fitting or the pinned close action. */ - .editor-secondary-host { - position: absolute; - inset: var(--sp-2) var(--sp-4) auto; - z-index: 70; - display: flex; - justify-content: flex-end; - pointer-events: none; - } - .editor-secondary-host.kind-palette, - .editor-secondary-host.kind-group { - justify-content: flex-start; - } - .editor-secondary { - pointer-events: none; - max-width: 100%; - display: flex; - align-items: center; - gap: var(--sp-3); - padding: var(--sp-3) var(--sp-4); - color: var(--hp-txt); - background: var(--card-background-color, var(--hp-bg)); - background: var(--hp-editor-tray-bg-fallback); - border: 1px solid var(--hp-line); - border-color: var(--hp-editor-tray-border); - border-radius: var(--rad-l); - box-shadow: var(--hp-editor-tray-shadow); - opacity: 0; - visibility: hidden; - transform: translateY(-4px); - transition: - opacity 0.1s ease, - transform 0.1s cubic-bezier(0.2, 0.7, 0.2, 1), - visibility 0s linear 0.1s; - touch-action: auto; - } - .editor-secondary-host.open .editor-secondary { - pointer-events: auto; - opacity: 1; - visibility: visible; - transform: translateY(0); - transition-duration: 0.14s, 0.14s, 0s; - transition-delay: 0s; - } - .editor-secondary-host.blocked .editor-secondary { pointer-events: none; } - @supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { - .editor-secondary { - background: var(--hp-editor-tray-bg); - -webkit-backdrop-filter: blur(var(--hp-editor-tray-blur)); - backdrop-filter: blur(var(--hp-editor-tray-blur)); - } - } - .editor-secondary-content, - .editor-group-items { - display: flex; - align-items: center; - gap: var(--sp-3); - min-width: 0; - } - .editor-context-label { - max-width: 14rem; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - padding-inline-end: var(--sp-4); - border-inline-end: 1px solid var(--hp-line); - color: var(--hp-muted); - font-size: var(--fs-s); - } - .editor-secondary.kind-palette { - width: min(760px, 100%); - align-items: stretch; - padding: 0; - overflow: hidden; - } - .editor-secondary.kind-palette .editor-secondary-content, - .editor-secondary.kind-palette .furnpalette { - width: 100%; - } - .editor-secondary.kind-palette .furnpalette { - max-height: 38vh; - border: 0; - background: transparent; - } - .editor-group-launcher .group-chevron { - --mdc-icon-size: 16px; - transition: transform 0.14s ease; - } - .editor-group-launcher[aria-expanded="true"] .group-chevron { transform: rotate(180deg); } - - .editor-secondary .bdhint { - font-size: var(--fs-s); - color: var(--hp-muted); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - .editor-secondary .dfill { - display: inline-flex; - align-items: center; - gap: var(--sp-2); - font-size: var(--fs-s); - cursor: pointer; - } - .editor-secondary .dfill input[type="checkbox"] { - width: 16px; - height: 16px; - flex: none; - margin: 0; - padding: 0; - } - .editor-secondary hp-color-opacity { flex: 0 0 auto; } - .editor-secondary input { - width: 74px; - background: transparent; - border: 1px solid var(--hp-line); - color: var(--hp-txt); - border-radius: var(--rad-s); - padding: var(--sp-3) var(--sp-4); - font-size: var(--fs-m); - } - .editor-secondary label, - .editor-secondary .hint { - color: var(--hp-muted); - font-size: var(--fs-s); - } - .editor-secondary .drawwall { - display: inline-flex; - align-items: center; - gap: var(--sp-3); - white-space: nowrap; - } - .editor-secondary .drawwall input { width: 4.2em; } - .editor-secondary .drawwall .opl { - color: var(--hp-muted); - font-size: var(--fs-s); - } - - @container (min-width: 560px) and (max-width: 899px) { - .editor-secondary-content, - .editor-group-items { - flex-wrap: wrap; - justify-content: flex-end; - } - } - @container (max-width: 559px) { - .editor-secondary-host { inset-inline: var(--sp-4); } - .editor-secondary { max-width: 100%; } - .editor-secondary-content, - .editor-group-items { - overflow-x: auto; - scrollbar-width: thin; - } - .editor-secondary-content > *, - .editor-group-items > * { flex: none; } - .editor-context-label { display: none; } - } - @media (prefers-reduced-motion: reduce) { - .editor-secondary { transition: none; } - .editor-group-launcher .group-chevron { transition: none; } - } -`,xm=(e,t)=>(e.composedPath?.()||[e.target]).some(e=>(e=>!!e&&"function"==typeof e.matches)(e)&&t(e));class $m{constructor(e){this.host=e,this._openGroupId=null,this._groupGeneration=0,this._renderedContext="",this._focusOwned=!1,this._currentModel=null,this._blocked=!1,this._dismissPointerTarget=null,this._globalDismissListening=!1,this._globalDismissGuard=e=>{this.handleOutsideDismiss(e)}}get hasOpenGroup(){return null!==this._openGroupId}get groupGeneration(){return this._groupGeneration}activeGroup(e){return this._openGroupId&&e.find(e=>e.id===this._openGroupId)||null}reset(){this._openGroupId=null,this._contentAnimation?.cancel(),this._contentAnimation=void 0,this._renderedContext="",this._focusOwned=!1,this._currentModel=null,this._blocked=!1,this._dismissPointerTarget=null,clearTimeout(this._dismissTimer),this._dismissTimer=void 0,this._syncGlobalDismissListener()}closeForNavigation(){const e="palette"===this._currentModel?.kind?this._currentModel.dismiss:void 0,t=!!this._openGroupId||!!e;this._openGroupId=null,e?.(),e&&(this._currentModel=null),this._clearDismissClick(),t&&this.host.requestUpdate(),this._syncGlobalDismissListener()}openPalette(){this._openGroupId&&this.closeGroup(!1)}toggleGroup(e,t){this._openGroupId!==t?e.some(e=>e.id===t)&&("palette"===this._currentModel?.kind&&this._currentModel.dismiss?.(),this._groupGeneration+=1,this._openGroupId=t,this.host.clearTip(),this.host.requestUpdate(),this._syncGlobalDismissListener()):this.closeGroup(!1)}closeGroup(e){const t=this._openGroupId;this._openGroupId=null,this.host.requestUpdate(),this._syncGlobalDismissListener(),e&&t&&this.host.updateComplete().then(()=>{const e=this.host.root(),i=[...e.querySelectorAll("[data-editor-group]")].find(e=>e.dataset.editorGroup===t&&null!==e.offsetParent);(i||e.querySelector(".editbar-tools"))?.focus?.()})}renderGroupLauncher(e,t,i){const s=this._openGroupId===e.id,n=e.items.find(t=>t.id===e.activeItemId&&!t.disabled);return q``}renderGroupModel(e,t,i){const s=e.items.find(e=>!e.disabled)?.id,n=e.items.some(t=>t.id===e.activeItemId&&!t.disabled)?e.activeItemId:void 0;return{contextId:t,kind:"group",ariaLabel:i.openGroup(e.label),visibleLabel:e.label,content:q`
this._groupKeydown(e)}> - ${e.items.map(t=>q``)} -
`}}runContext(e,t,i){e===t&&i()}render(e,t){this._currentModel=e,this._blocked=t;const i=e?.kind||"hidden";return q`
- -
`}afterRender(){const e=this.host.root();this._blocked&&(this._openGroupId?this.closeGroup(!1):"palette"===this._currentModel?.kind&&this._currentModel.dismiss?.()),this._syncGlobalDismissListener();const t=e.querySelector(".editor-secondary"),i=this._renderedContext,s=t?.dataset.contextId||"";if(s&&i&&s!==i&&!window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches&&(this._contentAnimation?.cancel(),this._contentAnimation=t?.querySelector(".editor-secondary-content")?.animate([{opacity:.35,transform:"translateY(-4px)"},{opacity:1,transform:"translateY(0)"}],{duration:100,easing:"ease-out"})),this._renderedContext=s,i&&s!==i&&this._focusOwned){const i=s?t?.querySelector('[tabindex="0"], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled])'):e.querySelector(".editbar-tools");i?.focus?.(),this._focusOwned=!!s}if("palette"===this._currentModel?.kind&&this._currentModel.launcherId){const t=[...e.querySelectorAll("[data-editor-palette]")].find(e=>e.dataset.editorPalette===this._currentModel?.launcherId);t&&null!==t.offsetParent||this._currentModel.dismiss?.()}if(!this._openGroupId)return;const n=[...e.querySelectorAll("[data-editor-group]")].find(e=>e.dataset.editorGroup===this._openGroupId);n&&null!==n.offsetParent||this.closeGroup(!0)}handleOutsideDismiss(e){if(xm(e,e=>e.hasAttribute("data-editor-navigation")))return"pointerdown"===e.type?this.closeForNavigation():"click"===e.type&&this._dismissPointerTarget&&this._clearDismissClick(),!1;if("click"===e.type&&this._dismissPointerTarget){return!!(e.composedPath?.()||[e.target]).includes(this._dismissPointerTarget)&&(this._clearDismissClick(),e.preventDefault(),e.stopImmediatePropagation(),!0)}const t=this._openGroupId||this._blocked||"palette"!==this._currentModel?.kind?null:this._currentModel;if("pointerdown"===e.type&&(this._openGroupId||t)){const i=xm(e,e=>e.classList.contains("editor-secondary")),s=xm(e,e=>!!e.dataset.editorGroup),n=xm(e,e=>!!e.dataset.editorPalette),o=xm(e,e=>e.classList.contains("stage"));if(!(i||s||n||"stay-open-on-canvas"===t?.dismissPolicy&&o))return e.preventDefault(),e.stopImmediatePropagation(),this._dismissPointerTarget=e.target,clearTimeout(this._dismissTimer),this._dismissTimer=window.setTimeout(()=>{this._clearDismissClick()},800),this._openGroupId?this.closeGroup(!1):t?.dismiss?.(),this._syncGlobalDismissListener(),!0}return!1}_activateGroupItem(e,t,i){if(e.id!==this._openGroupId)return;const s=e.items.find(e=>e.id===t);if(!s||s.disabled)return;const n="toggle"===s.role&&"stay-open"===s.closePolicy,o=0===i.detail;n||(this._openGroupId=null,this.host.requestUpdate(),this._syncGlobalDismissListener()),s.invoke(),!n&&o&&this.host.updateComplete().then(()=>{const t=[...this.host.root().querySelectorAll("[data-editor-group]")].find(t=>t.dataset.editorGroup===e.id&&null!==t.offsetParent);t?.focus()})}_groupKeydown(e){if(!["ArrowLeft","ArrowRight","Home","End"].includes(e.key))return;const t=[...e.currentTarget.querySelectorAll(".editor-group-item:not([disabled])")];if(!t.length)return;e.preventDefault();const i=t.indexOf(this.host.root().activeElement),s="Home"===e.key?0:"End"===e.key?t.length-1:"ArrowLeft"===e.key?i<=0?t.length-1:i-1:(i+1)%t.length;t[s]?.focus()}_clearDismissClick(){this._dismissPointerTarget=null,clearTimeout(this._dismissTimer),this._dismissTimer=void 0,this._syncGlobalDismissListener()}_syncGlobalDismissListener(){const e=!this._blocked&&"palette"===this._currentModel?.kind,t=!!this._openGroupId||e||!!this._dismissPointerTarget;t!==this._globalDismissListening&&(this._globalDismissListening=t,t?(window.addEventListener("pointerdown",this._globalDismissGuard,!0),window.addEventListener("click",this._globalDismissGuard,!0)):(window.removeEventListener("pointerdown",this._globalDismissGuard,!0),window.removeEventListener("click",this._globalDismissGuard,!0)))}}const Sm=e=>e.stopPropagation(),Mm=4.166666666666667e-9;function Cm(e){return Number.isFinite(e)?Math.round(e/jh)*jh:e}const Dm=(e,t,i,s)=>Math.hypot(i-e,s-t),Pm=(e,t,i,s,n,o,a,r)=>Math.hypot(Math.max(Math.abs(n-e),Math.abs(n+a-(e+i))),Math.max(Math.abs(o-t),Math.abs(o+r-(t+s)))),Tm=(e,t,i,s,n,o,a)=>{const r=Math.max(Number(s)||0,0)/2,l=Math.PI/180,c=Math.cos(i*l)*r,h=Math.sin(i*l)*r,d=Math.cos(a*l)*r,p=Math.sin(a*l)*r,u=Math.max(Dm(e+c,t+h,n+d,o+p),Dm(e-c,t-h,n-d,o-p)),_=Math.max(Dm(e+c,t+h,n-d,o-p),Dm(e-c,t-h,n+d,o+p));return Math.min(u,_)},Im=e=>{const t=Number(e?.cell_cm);return t>0?t:5};function Rm(e,t){const i=JSON.parse(JSON.stringify(e||[])),s=JSON.parse(JSON.stringify(t||{}));let n=0,o=0,a=0,r=0,l="",c=0,h=0,d=0;const p=(e,t)=>{Number.isFinite(e)&&Number.isFinite(t)&&e!==t&&Math.abs(t-e)<=Mm&&h++},u=(e,t)=>{p(e[0],t[0]),p(e[1],t[1])},_={};let m=5;for(const e of i){const t=Im(e);null!=e?.id&&(_[String(e.id)]=t),t>m&&(m=t)}const g=(e,t,i,s=!1)=>{if(!(e>Mm||s))return;n++,e>a&&(a=e);const o=e*Lh*t;o>r&&(r=o,l=i)};for(const e of i){const t=Im(e),i=null!=e?.id?String(e.id):"";for(const s of e.rooms||[]){o++;let e=0;if(s.poly?.length)s.poly=s.poly.map(t=>{const i=[Cm(t[0]),Cm(t[1])];return e=Math.max(e,Dm(t[0],t[1],i[0],i[1])),u(t,i),i});else if(null!=s.x&&null!=s.y){const t=s.x,i=s.y,n=s.w||0,o=s.h||0,a=Cm(t+n),r=Cm(i+o),l=Cm(t),c=Cm(i),h=Math.max(jh,a-l),d=Math.max(jh,r-c);e=Pm(t,i,n,o,l,c,h,d),s.x=l,s.y=c,s.w=h,s.h=d,p(t,l),p(i,c),p(t+n,l+h),p(i+o,c+d)}g(e,t,i)}for(const s of e.room_drafts||[]){o++;let e=0;const n=s.points||[],a=n.map(t=>{const i=[Cm(t[0]),Cm(t[1])];return e=Math.max(e,Dm(t[0],t[1],i[0],i[1])),i}),r=a.length?[a[0]]:[],l=a.length?[[n[0],a[0]]]:[],c=[];for(let e=0;e+1=2)for(const[e,t]of l)u(e,t);g(e,t,i)}if(Array.isArray(e.room_drafts)){const t=e.room_drafts.length;e.room_drafts=e.room_drafts.filter(e=>e.points?.length>=2),d+=t-e.room_drafts.length,e.room_drafts.length||delete e.room_drafts}for(const s of e.partitions||[]){o++;const n=[s.a[0],s.a[1]],a=[s.b[0],s.b[1]],r=[Cm(n[0]),Cm(n[1])],l=[Cm(a[0]),Cm(a[1])];let c=Math.max(Dm(n[0],n[1],r[0],r[1]),Dm(a[0],a[1],l[0],l[1]));const h=Dm(r[0],r[1],l[0],l[1]),d=(e.openings||[]).filter(e=>"partition"===e.host?.kind&&e.host.id===s.id).every(e=>{const t=Number(e.length),i=Number(e.host.t),s=i*h;return Number.isFinite(t)&&t>0&&Number.isFinite(i)&&i>=0&&i<=1&&s-t/2>=-Mm&&s+t/2<=h+Mm});h>Mm&&d?(s.a=r,s.b=l,u(n,r),u(a,l)):c=0,g(c,t,i)}for(const s of e.wall_columns||[]){o++;const e=[s.center[0],s.center[1]],n=[Cm(e[0]),Cm(e[1])],a=Dm(e[0],e[1],n[0],n[1]);s.center=n,u(e,n),g(a,t,i)}for(const s of e.decor||[]){o++;let e=0;if("line"===s.kind){const t=[s.x1,s.y1],i=[s.x2,s.y2],n=[Cm(t[0]),Cm(t[1])],o=[Cm(i[0]),Cm(i[1])];e=Math.max(Dm(t[0],t[1],n[0],n[1]),Dm(i[0],i[1],o[0],o[1])),s.x1=n[0],s.y1=n[1],s.x2=o[0],s.y2=o[1],u(t,n),u(i,o)}else{const t=s.x,i=s.y,n=s.w,o=s.h,a=Cm(t),r=Cm(i);if(null!=s.w&&null!=s.h){const l=Cm(t+n),c=Cm(i+o),h=Math.max(jh,l-a),d=Math.max(jh,c-r);e=Pm(t,i,n,o,a,r,h,d),s.w=h,s.h=d,p(t+n,a+h),p(i+o,r+d)}else e=Dm(t,i,a,r);s.x=a,s.y=r,p(t,a),p(i,r)}g(e,t,i)}for(const s of e.openings||[]){if(o++,"partition"===s.host?.kind){const n=(e.partitions||[]).find(e=>e.id===s.host.id),o=Number(s.host.t);if(!n||!Number.isFinite(o)||o<0||o>1)continue;const a=n.b[0]-n.a[0],r=n.b[1]-n.a[1];if(Math.hypot(a,r)<=Mm)continue;const l=n.a[0]+a*o,h=n.a[1]+r*o;let d=180*Math.atan2(r,a)/Math.PI;d>=90?d-=180:d<-90&&(d+=180);const p=Number(s.angle),u=!(Number.isFinite(p)&&p===d),_=Tm(s.x,s.y,Number.isFinite(p)?p:d,Number(s.length)||0,l,h,d);s.x=l,s.y=h,s.angle=d,u&&c++,g(_,t,i,u);continue}const n=ui([s.x,s.y],e.rooms||[],.025,{step:jh,length:Number(s.length)||0});if(!n)continue;const a=Number(s.angle),r=!(Number.isFinite(a)&&a===n.angle),l=Tm(s.x,s.y,Number.isFinite(a)?a:n.angle,Number(s.length)||0,n.x,n.y,n.angle);s.x=n.x,s.y=n.y,s.angle=n.angle,r&&c++,g(l,t,i,r)}}for(const[e,t]of Object.entries(s)){if(!t||"object"!=typeof t)continue;const i=t;if("number"!=typeof i.x||"number"!=typeof i.y)continue;o++;const n=Cm(i.x),a=Cm(i.y),r=Dm(i.x,i.y,n,a);s[e]={...i,x:n,y:a},p(i.x,n),p(i.y,a);const l="string"==typeof i.s?i.s:"";g(r,_[l]??m,l)}return{spaces:i,layout:s,report:{moved:n,coordsCanonicalized:h,total:o,maxShift:a,maxShiftCm:r,maxSpace:l,rotated:c,removedDrafts:d},changed:n>0||h>0}}function Am(e,t){const i=t.base+e*t.span;return Math.min(1,Math.max(0,i))}const zm=e=>Array.isArray(e)&&e.length>=2&&Number.isFinite(e[0])&&Number.isFinite(e[1]),Fm=(e,t)=>Math.hypot(e[0]-t[0],e[1]-t[1]);function Nm(e,t,i){const s=i[0]-t[0],n=i[1]-t[1],o=s*s+n*n;if(o<=0)return Fm(e,t);let a=((e[0]-t[0])*s+(e[1]-t[1])*n)/o;return a=Math.max(0,Math.min(1,a)),Math.hypot(e[0]-(t[0]+s*a),e[1]-(t[1]+n*a))}function Em(e,t,i,s,n){for(const s of t)if(s&&!i.has(s.id)&&zm(s.a)&&zm(s.b)&&(Fm(e,s.a)<=n||Fm(e,s.b)<=n))return!0;for(const t of s?.roomPolygons||[])for(let i=0;i0&&r>0))return null;if(Math.abs(n[0]/a*(o[1]/r)-n[1]/a*(o[0]/r))>i)return null;for(const[i,n]of[[e.a,t.a],[e.a,t.b],[e.b,t.a],[e.b,t.b]])if(Fm(i,n)<=s)return[(i[0]+n[0])/2,(i[1]+n[1])/2];return null}function Hm(e,t){const i=.05*(Number(t.pitch)>0?Number(t.pitch):1),s=new Map;let n=(e||[]).filter(e=>!!e&&"string"==typeof e.id&&zm(e.a)&&zm(e.b));if(n.length<2)return{partitions:[...e||[]],merged:0,openingMoves:[]};const o=t.seedIds&&t.seedIds.length?new Set(t.seedIds):null;let a=0;for(let e=0;eFm(t,e.at)>i);let h=c[0]??r.a,d=c[c.length-1]??l.b;if(c.length<2&&(h=r.a,d=l.b),d[0]0?(d[0]-h[0])/u:0,m=u>0?(d[1]-h[1])/u:0,g=e=>u>0?((e[0]-h[0])*_+(e[1]-h[1])*m)/u:0;for(const e of[r,l]){const t=g(e.a),i=g(e.b)-t;for(const[n,o]of s)o.toId===e.id&&s.set(n,{fromId:o.fromId,toId:p.id,base:t+o.base*i,span:o.span*i});s.set(e.id,{fromId:e.id,toId:p.id,base:t,span:i})}o&&o.add(p.id),n=n.filter((t,i)=>i!==e.i&&i!==e.j),n.splice(e.i,0,p),a++}return{partitions:n,merged:a,openingMoves:[...s.values()]}}function Lm(e,t,i,s){if(!e?.length||!i.length)return 0;const n=new Map(i.map(e=>[e.fromId,e]));let o=0;for(const i of e){const e=i?.host;if(!e||"partition"!==e.kind)continue;const a=n.get(e.id);if(!a)continue;i.host={...e,id:a.toId,t:Am(e.t,a)},o++;const r=ou(i,t,s.coordScale,s.cellCm,s.gridPitch).resolved;r&&Object.assign(i,pu(i,r,s.coordScale))}return o}function Bm(e,t){const i=t?.excludeDraftId||null;return{roomPolygons:(e?.rooms||[]).map(e=>hi(e)).filter(e=>!!e),columns:e?.wall_columns||[],draftEnds:(e?.room_drafts||[]).flatMap(e=>{if(i&&e?.id===i)return[];const t=e?.points||[];return t.length?[t[0],t[t.length-1]]:[]})}}const jm=e=>!!e&&e.length>=2&&Number.isFinite(e[0])&&Number.isFinite(e[1]),qm=(e,t)=>e[0]-t[0]||e[1]-t[1],Wm=(e,t,i,s)=>[e[0]+t*s,e[1]+i*s],Um=(e,t,i,s)=>(e[0]-t[0])*i+(e[1]-t[1])*s,Gm=(e,t,i,s,n)=>{const o=t[0]-e[0],a=t[1]-e[1],r=Math.hypot(o,a);if(!(r>n))return null;const l=o/r,c=a/r,h=t=>Math.abs((t[0]-e[0])*c-(t[1]-e[1])*l);if(h(i)>n||h(s)>n)return null;const d=Um(i,e,l,c),p=Um(s,e,l,c),u=Math.max(0,Math.min(d,p)),_=Math.min(r,Math.max(d,p));return _-u>n?{lo:u,hi:_}:null},Vm=(e,t,i)=>Math.min(e.hi,t.hi)-Math.max(e.lo,t.lo)>i,Km=e=>{const t=new Set(["id","a","b","cm"]);return Object.keys(e||{}).every(e=>t.has(e))},Ym=(e,t,i,s)=>{const n=e.b[0]-e.a[0],o=e.b[1]-e.a[1],a=Math.hypot(n,o);if(!(a>s))return!0;const r=n/a,l=o/a,c=Ml(e.cm,i.cellCm,i.gridPitch)/2;return t.some(t=>{const n=t?.center;if(!jm(n))return!0;const o=Um(n,e.a,r,l),h=Math.abs((n[0]-e.a[0])*l-(n[1]-e.a[1])*r),d=Ml(Number(t.cm),i.cellCm,i.gridPitch)*("square"===t.shape?Math.SQRT1_2:.5);return!(Number.isFinite(d)&&d>0)||o>=-d-s&&o<=a+d+s&&h<=c+d+s})},Jm=(e,t,i)=>{if(e.host){const s=ou(e,t,i.coordScale,i.cellCm,i.gridPitch).resolved;return s?{center:s.center,angle:s.angle,length:s.length}:null}const s=Number(e.x)*i.coordScale,n=Number(e.y)*i.coordScale,o=Number(e.angle),a=Number(e.length)*i.coordScale;return[s,n,o,a].every(Number.isFinite)&&a>0?{center:[s,n],angle:o,length:a}:null},Xm=(e,t,i)=>{const s=e.angle*Math.PI/180,n=Math.cos(s),o=Math.sin(s),a=t.angle*Math.PI/180,r=Math.cos(a),l=Math.sin(a);if(Math.abs(n*l-o*r)>1e-6)return!1;const c=t.center[0]-e.center[0],h=t.center[1]-e.center[1];if(Math.abs(c*o-h*n)>i)return!1;const d=c*n+h*o;return Math.max(-e.length/2,d-t.length/2)({...e,a:[e.a[0]*t,e.a[1]*t],b:[e.b[0]*t,e.b[1]*t]}),Qm=(e,t,i,s)=>{const n=`~r-${(e=>{let t=2166136261;for(let i=0;i>>0;return t.toString(36)})(`${e}|${t[0].toFixed(9)},${t[1].toFixed(9)}|${i[0].toFixed(9)},${i[1].toFixed(9)}`)}`,o=`${e.slice(0,Math.max(1,64-n.length))}${n}`;let a=o;for(let e=2;s.has(a);e++){const t=`-${e}`;a=`${o.slice(0,64-t.length)}${t}`}return s.add(a),a},eg=(e,t)=>{const i=[];for(const s of e){const e=i[i.length-1];!(e&&Math.abs(e.hi-s.lo)<=t&&e.safe===s.safe)||s.safe&&e.signature!==s.signature?i.push({lo:s.lo,hi:s.hi,a:s.a,b:s.b,safe:s.safe,signature:s.signature,roomIds:s.roomIds,finalCm:s.finalCm}):(e.hi=s.hi,e.b=s.b)}return i};function tg(e,t,i,s,n){const o=i||[],a=Array.isArray(e?.partitions)?e.partitions:[],r=Array.isArray(e?.openings)?e.openings:[],l=Array.isArray(e?.room_drafts)?e.room_drafts:[],c={walls:o,partitions:a,openings:r,roomDrafts:l,partitionsReconciled:0,openingsRehosted:0,removedDrafts:0};if(!a.length&&!l.length)return c;const h=Math.max(2e-4*n.gridPitch,1e-9);let d=o,p=a,u=r;const _=wc(t.rooms,d,s,n.pitch,n.cellCm,n.gridPitch,n.coordScale),m=((e,t,i,s)=>{const n=new Map(e.map(e=>[e.id,e])),o=new Set;for(const e of t){if(!n.get(e.id)||!Array.isArray(e.points)||e.points.length<2||e.segments.length!==e.points.length-1)continue;let t=!0;for(let n=0;n+10)){t=!1;break}const l=Math.hypot(a[0]-o[0],a[1]-o[1]);if(!(l>s)){t=!1;break}const c=i.flatMap(e=>{const t=e.cm>0?e.cm:vl;if(e.open||"outer"!==e.kind&&"shared"!==e.kind||t+se.lo-t.lo||e.hi-t.hi);let h=0;for(const e of c){if(e.lo>h+s)break;if(h=Math.max(h,e.hi),h>=l-s)break}h!m.has(e.id));let f=0,v=0;const b=new Map(t.partitions.map(e=>[e.id,e]));for(const e of[...a].sort((e,t)=>e.id.localeCompare(t.id))){if(!p.some(t=>t.id===e.id))continue;if(!Km(e))continue;const i=b.get(e.id);if(!(i&&jm(i.a)&&jm(i.b)&&Number.isFinite(i.cm)&&i.cm>0))continue;const[o,a]=qm(i.a,i.b)<=0?[[i.a[0],i.a[1]],[i.b[0],i.b[1]]]:[[i.b[0],i.b[1]],[i.a[0],i.a[1]]],r=a[0]-o[0],l=a[1]-o[1],c=Math.hypot(r,l);if(!(c>h))continue;const _=r/c,g=l/c,y=wc(t.rooms,d,s,n.pitch,n.cellCm,n.gridPitch,n.coordScale),w=y.flatMap(e=>{if(e.open||"outer"!==e.kind&&"shared"!==e.kind)return[];const t=Gm(o,a,e.a,e.b,h);return t?[{interval:e,range:t}]:[]});if(!w.length)continue;const k=u.filter(e=>"partition"===e.host?.kind&&e.host.id===i.id).map(e=>({opening:e,resolved:ou(e,t.partitions,n.coordScale,n.cellCm,n.gridPitch).resolved}));if(k.some(e=>!e.resolved))continue;const x=[0,c];for(const{range:e}of w)x.push(e.lo,e.hi);const $=x.slice(),S=new Map;for(const{opening:e,resolved:t}of k){const i=Um(t.center,o,_,g),s={lo:i-t.length/2,hi:i+t.length/2};S.set(e.id,s),$.push(Math.max(0,s.lo),Math.min(c,s.hi))}const M=[...new Set($.filter(e=>Number.isFinite(e)&&e>=-h&&e<=c+h).map(e=>Math.max(0,Math.min(c,e)).toFixed(9)))].map(Number).sort((e,t)=>e-t),C=p.map(e=>Zm(e,n.coordScale)).filter(e=>e.id!==i.id),D=t.room_drafts.filter(e=>!m.has(e.id)),P=[];for(let e=0;e+1h))continue;const r=Wm(o,_,g,s),l=Wm(o,_,g,a),c=w.filter(({range:e})=>e.lo<=s+h&&e.hi>=a-h).map(({interval:e})=>e),d=new Map;for(const e of c)d.set(e.roomId,e);const p=[...d.values()],u=new Set(p.map(e=>e.kind)),m=new Set(p.map(e=>e.cm>0?e.cm:vl)),f=p[0]?.kind,v=[...d.keys()].sort(),b=1===u.size&&1===m.size&&("outer"===f&&1===v.length||"shared"===f&&2===v.length),y={id:i.id,a:r,b:l,cm:i.cm},k=C.some(e=>!!Gm(r,l,e.a,e.b,h)),x=D.some(e=>e.points.some((t,i)=>i+10?p[0].cm:vl:0,T=b?Math.max(S,i.cm):i.cm;P.push({lo:s,hi:a,a:r,b:l,safe:$,signature:$?`${f}|${v.join(",")}|${T}`:"",roomIds:v,roomCm:S,finalCm:T})}if(!P.some(e=>e.safe))continue;const T=new Map;for(const{opening:e,resolved:t}of k){const s=S.get(e.id),o=x.some(e=>e>s.lo+h&&eVm(e,s,h)),r=new Set(a.filter(e=>e.safe).map(e=>e.signature));if(!o&&a.length>0&&a.every(e=>e.safe)&&1===r.size&&du(t,y,h)){T.set(e.id,{resolved:t,signature:[...r][0]});continue}const l=su(i,n.cellCm,n.gridPitch),c={lo:s.lo-l,hi:s.hi+l};for(const e of P)Vm(e,c,h)&&(e.safe=!1,e.signature="")}let I=!0;for(;I;){I=!1;for(const[e,t]of[...T.entries()]){const s=S.get(e),o=P.filter(e=>Vm(e,s,h));if(o.length>0&&o.every(e=>e.safe)&&o.every(e=>e.signature===t.signature))continue;T.delete(e);const a=su(i,n.cellCm,n.gridPitch),r={lo:s.lo-a,hi:s.hi+a};for(const e of P)Vm(e,r,h)&&(e.safe=!1,e.signature="");I=!0}}const R=eg(P,h),A=R.filter(e=>e.safe),z=R.filter(e=>!e.safe);if(!A.length)continue;if(p.length-1+z.length>2e3)continue;let F=d;for(const e of A){const[t,s]=qm(i.a,i.b)<=0?[e.a,e.b]:[e.b,e.a];F=Ul(F,t,s,e.finalCm,n.pitch,n.coordScale)}if(F=xc(t.rooms,F,s,n.pitch,n.cellCm,n.gridPitch,n.coordScale),F.length>500)continue;const N=new Set(p.map(e=>e.id));N.delete(i.id);const E=z.map((e,t)=>{const s=[e.a[0]/n.coordScale,e.a[1]/n.coordScale],o=[e.b[0]/n.coordScale,e.b[1]/n.coordScale],a=0===t?i.id:Qm(i.id,s,o,N);return N.add(a),{id:a,a:s,b:o,cm:i.cm}}),O=p.findIndex(e=>e.id===i.id),H=p.slice();H.splice(O,1,...E);const L=H.map(e=>Zm(e,n.coordScale)),B=new Map;let j=!0;for(const{opening:e,resolved:t}of k){const i=T.get(e.id);if(i){const t=pu(e,i.resolved,n.coordScale),{host:s,...o}=t;B.set(e.id,o);continue}const s=S.get(e.id),o=E[z.findIndex(e=>e.lo<=s.lo+h&&e.hi>=s.hi-h)],a=o&&L.find(e=>e.id===o.id);if(!o||!a){j=!1;break}const r=a.b[0]-a.a[0],l=a.b[1]-a.a[1],c=Math.hypot(r,l),d=Um(t.center,a.a,r/c,l/c),p={...pu(e,t,n.coordScale),host:{kind:"partition",id:o.id,t:d/c}};if(!au(p,L,n.coordScale,n.cellCm,n.gridPitch).resolved){j=!1;break}B.set(e.id,p)}if(!j)continue;const q=u.map(e=>B.get(e.id)||e),W=[...T.keys()].map(e=>B.get(e)),U=W.map(e=>Jm(e,L,n));if(U.some(e=>!e))continue;let G=!1;for(let e=0;ee.id));for(const e of q){if(V.has(e.id))continue;const t=Jm(e,L,n);t&&U.some(e=>Xm(e,t,h))&&(G=!0)}if(G)continue;const K=Kc(t.rooms,F,s,n.pitch,n.cellCm,n.gridPitch,n.coordScale);[...T.entries()].every(([e,t])=>{const i=B.get(e),s=Xc(K,{x:i.x*n.coordScale,y:i.y*n.coordScale,angle:i.angle,length:i.length*n.coordScale},!0),o=[s.negative,s.positive].filter(e=>!!e?.full),a=t.signature.split("|")[1].split(","),r=new Set(o.map(e=>e.roomId));return o.length===a.length&&r.size===a.length&&a.every(e=>r.has(e))})&&(d=F,p=H,u=q,f+=A.length,v+=T.size)}return{walls:d,partitions:p,openings:u,roomDrafts:g,partitionsReconciled:f,openingsRehosted:v,removedDrafts:m.size}}const ig=e=>JSON.parse(JSON.stringify(e)),sg=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),ng=(e,t)=>t.length>0&&t.length<=35&&("space"===e?/^[a-z0-9_-]+$/.test(t):/^[A-Za-z0-9_-]+$/.test(t));function og(e,t){let i=String(t??"");const s=e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),n=new RegExp(`^${s}_(.+)_([0-9a-f]{8})$`);let o=0;for(;o<16;o++){const e=n.exec(i);if(!e)return{root:i,layers:o,bounded:!1};i=e[1]}return{root:i,layers:o,bounded:n.test(i)}}const ag=(e,t,i)=>{const s=e.get(t)||[];s.push(i),e.set(t,s)};const rg=1e-9;class lg extends Error{constructor(e,t=""){super(t?`${e}: ${t}`:e),this.reason=e,this.name="WallSegmentModelError"}}const cg=e=>JSON.parse(JSON.stringify(e)),hg=e=>Array.isArray(e)&&e.length>=2&&Number.isFinite(Number(e[0]))&&Number.isFinite(Number(e[1])),dg=e=>[Number(e[0]),Number(e[1])],pg=e=>`${Number(e[0]).toFixed(12)},${Number(e[1]).toFixed(12)}`,ug=(e,t)=>{const i=pg(e),s=pg(t);return ipg(e)<=pg(t)?[dg(e),dg(t)]:[dg(t),dg(e)],mg=(e,t)=>Math.hypot(t[0]-e[0],t[1]-e[1]),gg=e=>{const t=[[Number(e.points[0][0]),Number(e.points[0][1])]],i=[];for(let s=1;s{const i=new Set,s=e=>{const t=Number(e);if(!Number.isFinite(t))return;const s=t/jh;Math.abs(s-Math.round(s))>=tl&&i.add(t.toFixed(12))};for(const t of Array.isArray(e?.rooms)?e.rooms:[])for(const e of hi(t)||[])s(e[0]),s(e[1]);for(const t of[e?.wall_segments,e?.walls])for(const e of Array.isArray(t)?t:[])s(e?.a?.[0]),s(e?.a?.[1]),s(e?.b?.[0]),s(e?.b?.[1]);for(const e of t)s(e?.[0]),s(e?.[1]);return i.size},vg=(e,t,i)=>{const s=i[0]-t[0],n=i[1]-t[1],o=s*s+n*n;return o>1e-18?((e[0]-t[0])*s+(e[1]-t[1])*n)/o:0},bg=(e,t,i)=>{const s=Math.max(0,Math.min(1,vg(e,t,i)));return Math.hypot(e[0]-(t[0]+(i[0]-t[0])*s),e[1]-(t[1]+(i[1]-t[1])*s))},yg=(e,t,i,s,n=rg)=>{const o=t[0]-e[0],a=t[1]-e[1],r=Math.hypot(o,a);if(r<=n)return 0;const l=t=>Math.abs((t[0]-e[0])*a-(t[1]-e[1])*o)/r;if(l(i)>n||l(s)>n)return 0;const c=vg(i,e,t),h=vg(s,e,t);return Math.max(0,Math.min(1,Math.max(c,h))-Math.max(0,Math.min(c,h)))*r},wg=e=>{const t=(new TextEncoder).encode(e),i=8*t.length,s=64*Math.ceil((t.length+9)/64),n=new Uint8Array(s);n.set(t),n[t.length]=128;const o=new DataView(n.buffer);o.setUint32(s-8,Math.floor(i/4294967296),!1),o.setUint32(s-4,i>>>0,!1);const a=new Uint32Array([1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298]),r=new Uint32Array([1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225]),l=new Uint32Array(64),c=(e,t)=>e>>>t|e<<32-t;for(let e=0;e>>3,n=c(i,17)^c(i,19)^i>>>10;l[e]=l[e-16]+s+l[e-7]+n>>>0}let[t,i,s,n,h,d,p,u]=r;for(let e=0;e<64;e++){const o=u+(c(h,6)^c(h,11)^c(h,25))+(h&d^~h&p)+a[e]+l[e]>>>0,r=t&i^t&s^i&s;u=p,p=d,d=h,h=n+o>>>0,n=s,s=i,i=t,t=o+((c(t,2)^c(t,13)^c(t,22))+r>>>0)>>>0}r[0]=r[0]+t>>>0,r[1]=r[1]+i>>>0,r[2]=r[2]+s>>>0,r[3]=r[3]+n>>>0,r[4]=r[4]+h>>>0,r[5]=r[5]+d>>>0,r[6]=r[6]+p>>>0,r[7]=r[7]+u>>>0}const h=new Uint8Array(32),d=new DataView(h.buffer);for(let e=0;e{const t="abcdefghijklmnopqrstuvwxyz234567";let i=0,s=0,n="";for(const o of e)for(s=s<<8|o,i+=8;i>=5;)n+=t[s>>>i-5&31],i-=5;return i&&(n+=t[s<<5-i&31]),n},xg=(e,t,i,s,n="")=>{const[o,a]=_g(t,i),r=`${e}|${pg(o)}|${pg(a)}|${[...s].sort().join(",")}${n}`;return`wall-${kg(wg(r)).slice(0,20)}`};let $g=0;const Sg=e=>{for(let t=0;t<1e3;t++){const t=globalThis.crypto?.randomUUID?.(),i=`wall-${t||kg(wg(`${Date.now()}|${$g++}|${Math.random()}`)).slice(0,26)}`;if(!e.has(i))return i}throw new lg("duplicate-id","id factory exhausted")},Mg=e=>{const t=new Map;for(const i of Array.isArray(e?.wall_segments)?e.wall_segments:[])if(i&&"string"==typeof i.id&&hg(i.a)&&hg(i.b)){if(t.has(i.id))throw new lg("duplicate-id",i.id);t.set(i.id,i)}return t},Cg=(e,t,i)=>{const s=(i,s)=>{const n=e[0]-i[0],o=e[1]-i[1];return Math.abs(t[0]-s[0]-n)<=rg&&Math.abs(t[1]-s[1]-o)<=rg?[n,o]:null};return s(i.a,i.b)||s(i.b,i.a)},Dg=(e,t)=>{const i=Array.isArray(e.rooms)?e.rooms:[],s=jd(e.open_spans).map(e=>[e.a[0],e.a[1],e.b[0],e.b[1]]),n=s.length?s:qd(i,null,1,.04*jh,!0),o=[];for(const e of i){const i=hi(e),s=Array.isArray(e?.wall_ids)?e.wall_ids:[];if(i&&s.length===i.length)for(let e=0;e"partition"!==e.host?.kind&&[Number(e.x),Number(e.y)].every(Number.isFinite)&&Number.isFinite(Number(e.angle))&&Number(e.length)>0&&n.some(t=>bg([Number(e.x),Number(e.y)],[t[0],t[1]],[t[2],t[3]])<=.04*jh)):[],l=r.map(e=>{const t=Number(e.angle)*Math.PI/180,i=[Math.cos(t),Math.sin(t)],s=Number(e.length)/2,n=[Number(e.x),Number(e.y)];return{a:[n[0]-i[0]*s,n[1]-i[1]*s],b:[n[0]+i[0]*s,n[1]+i[1]*s]}}),c=(e,t)=>r.some(i=>{const s=[Number(i.x),Number(i.y)];if(!Bl(e,t,Number(i.angle)))return!1;if(bg(s,e,t)>.02*jh)return!1;const n=mg(e,t),o=vg(s,e,t)*n,a=Number(i.length)/2;return o+a>=-rg&&o-a<=n+rg}),h=new Map,d=[];for(const s of i){const r=String(s?.id||""),p=hi(s);if(!r||!p||p.length<3)throw new lg("invalid-room",r);const u=dc(i,r,a,jh,1,[...e.walls||[],...l]);if(!u||u.poly.length<3)throw new lg("invalid-room",r);const _=Array.isArray(s.wall_ids)?s.wall_ids:[],m=_.length===p.length;let g=null,f=m;if(f)for(let e=0;erg||Math.abs(s[1]-g[1])>rg)){f=!1;break}g=s}const v=[];for(let e=0;e2)throw new lg("third-owner",a);const d=m?_[u.parent[e]]:void 0;if("string"==typeof d&&d){const n=t.get(d);f||!n||yg(i,s,n.a,n.b)>rg?l.preferredIds.add(d):l.positionalIds.add(d),l.preferredCarriers.set(d,{a:dg(p[u.parent[e]]),b:dg(p[(u.parent[e]+1)%p.length])})}const g=u.parent[e];l.parentKeys.add(Il(p[g],p[(g+1)%p.length],jh));const b=[(i[0]+s[0])/2,(i[1]+s[1])/2],y=e=>e.some(e=>bg(b,[e[0],e[1]],[e[2],e[3]])<=.04*jh);l.zeroWall||=y(o)||y(n)&&!c(i,s),v.push(a)}const b={...s};delete b.wall_ids,d.push({...b,poly:u.poly.map(e=>[e[0],e[1]]),wall_ids:v})}return{atoms:[...h.values()].sort((e,t)=>e.key.localeCompare(t.key)),rooms:d}},Pg=(e,t,i,s,n)=>{const o=new Map([...i.values()].map(e=>[ug(e.a,e.b),e])),a=(e=>{const t=new Map;for(const i of e)"wall"===i.host?.kind&&t.set(i.host.id,(t.get(i.host.id)||0)+1);return t})(e.openings||[]),r=new Map;for(const e of t){const t=n?.get(e.key);if(t){const s=i.get(t);if(!s)throw new lg("duplicate-id",t);r.set(e,s);continue}if(e.preferredIds.size>1)throw new lg("duplicate-id",[...e.preferredIds].sort().join(","));const s=[...e.preferredIds][0],l=s?i.get(s):void 0;if(l){r.set(e,l);continue}const c=s?e.preferredCarriers.get(s):void 0;if(s&&c){r.set(e,{id:s,a:c.a,b:c.b,cm:0});continue}const h=o.get(e.key);if(h){r.set(e,h);continue}const d=[...i.values()].filter(t=>yg(e.a,e.b,t.a,t.b)>rg),p=[...new Map(d.map(e=>[e.id,e])).values()];if(p.sort((e,t)=>(a.get(t.id)||0)-(a.get(e.id)||0)||mg(t.a,t.b)-mg(e.a,e.b)||e.id.localeCompare(t.id)),p[0])r.set(e,p[0]);else if(1===e.positionalIds.size){const t=i.get([...e.positionalIds][0]);t&&r.set(e,t)}}const l=new Map;for(const[e,t]of r){const i=l.get(t.id)||[];i.push(e),l.set(t.id,i)}for(const[e,t]of l){const s=i.get(e)||r.get(t[0]);if(!s)throw new lg("duplicate-id",e);const n=[(s.a[0]+s.b[0])/2,(s.a[1]+s.b[1])/2];t.sort((e,t)=>{const i=bg(n,e.a,e.b)<=rg?0:1,o=bg(n,t.a,t.b)<=rg?0:1;if(i!==o)return i-o;return(bg(s.a,e.a,e.b)<=rg?0:1)-(bg(s.a,t.a,t.b)<=rg?0:1)||e.key.localeCompare(t.key)}),t[0].id=e}const c=(e=>{const t=new Set;for(const i of[e.rooms,e.openings,e.decor,e.room_drafts,e.partitions,e.wall_columns])for(const e of Array.isArray(i)?i:[])"string"==typeof e?.id&&e.id&&t.add(e.id);for(const i of Array.isArray(e.room_drafts)?e.room_drafts:[])for(const e of Array.isArray(i?.segments)?i.segments:[])"string"==typeof e?.id&&e.id&&t.add(e.id);return t})(e);for(const e of t)if(e.id){if(c.has(e.id))throw new lg("duplicate-id",e.id);c.add(e.id)}const h=t.filter(e=>!e.id);if(s){const t=h.map(t=>{const[i,s]=_g(t.a,t.b),n=`${String(e.id||"")}|${pg(i)}|${pg(s)}|${[...t.owners].sort().join(",")}`,o=kg(wg(n));return{atom:t,seed:n,digest:o,base:`wall-${o.slice(0,20)}`}}).sort((e,t)=>e.digest.localeCompare(t.digest)||e.atom.key.localeCompare(t.atom.key)),i=new Map;for(const e of t){const t=i.get(e.digest);if(t&&t!==e.seed)throw new lg("duplicate-id",e.digest);i.set(e.digest,e.seed);let s=1,n=e.base;for(;c.has(n);)n=`${e.base}-${++s}`;e.atom.id=n,c.add(n)}}else for(const e of h)e.id=Sg(c),c.add(e.id)},Tg=(e,t)=>{if("partition"===e.host?.kind)return null;const i=[Number(e.x),Number(e.y)];if(!i.every(Number.isFinite))return null;const s=t=>{if(!(Number(t.cm)>0))return!1;const s=vg(i,t.a,t.b),n=mg(t.a,t.b),o=Number(e.length)/2;return s>=-rg&&s<=1+rg&&bg(i,t.a,t.b)<=.02*jh&&Bl(t.a,t.b,Number(e.angle))&&Number.isFinite(o)&&o>=0&&s*n-o>=-rg&&s*n+o<=n+rg},n="wall"===e.host?.kind?t.find(t=>t.id===e.host.id):null,o=n&&s(n)?[n]:t.filter(s);if(1!==o.length)return null;const a=o[0];return{kind:"wall",id:a.id,t:Math.max(0,Math.min(1,vg(i,a.a,a.b)))}},Ig=(e,t)=>{const i=[Number(e.x),Number(e.y)];if(!i.every(Number.isFinite))return null;const s=Number(e.length)/2,n=t.filter(t=>{if(!(Number(t.cm)>0))return!1;const n=vg(i,t.a,t.b),o=mg(t.a,t.b);return n>=-rg&&n<=1+rg&&bg(i,t.a,t.b)<=.02*jh&&Bl(t.a,t.b,Number(e.angle))&&Number.isFinite(s)&&s>=0&&n*o-s>=-rg&&n*o+s<=o+rg}),o=(t=>{if(!t.length)return null;const s="wall"===e.host?.kind?t.find(t=>t.id===e.host.id):null;return s||[...t].sort((e,t)=>bg(i,e.a,e.b)-bg(i,t.a,t.b)||Number(t.cm)-Number(e.cm)||(e.idt.id?1:0))[0]})(n);return o?{kind:"wall",id:o.id,t:Math.max(0,Math.min(1,vg(i,o.a,o.b)))}:null},Rg=(e,t,i)=>{const s=Mg(e),{atoms:n,rooms:o}=Dg(e,s);Pg(e,n,s,t,i);const a=n.map(t=>{const i=t.id?s.get(t.id):void 0,n=((e,t,i)=>{if(t.zeroWall)return 0;const s=Array.isArray(e.walls)?e.walls:[],n=Il(t.a,t.b,jh),o=s.filter(e=>Number(e.cm)>0&&(!(e.key!==n&&!t.parentKeys.has(e.key))||!(!hg(e.a)||!hg(e.b))&&yg(t.a,t.b,e.a,e.b)>=mg(t.a,t.b)-rg)).map(e=>Math.max(1,Math.min(100,Number(e.cm))));if(new Set(o.map(e=>e.toFixed(9))).size>1)throw new lg("thickness-conflict",t.key);if(o.length)return o[0];const a=Hl(s,t.a,t.b,jh,1);return a>0?a:Number(i?.cm)>0?Number(i.cm):0})(e,t,i);return{...i||{},id:t.id,a:[...t.a],b:[...t.b],cm:n}}),r=new Map(n.map(e=>[e.key,e.id]));for(const e of o)e.wall_ids=e.wall_ids.map(e=>r.get(e));e.rooms=o,e.wall_segments=a,e.walls=a.filter(e=>e.cm>0).map(e=>({key:Il(e.a,e.b,jh),cm:e.cm,a:[...e.a],b:[...e.b]})),e.walls.length||delete e.walls,delete e.open_spans;for(const t of e.rooms)delete t.open_to;return((e,t)=>{const i=new Set;for(const t of[e.rooms,e.openings,e.decor,e.room_drafts,e.partitions,e.wall_columns,e.wall_segments])for(const e of Array.isArray(t)?t:[])"string"==typeof e?.id&&e.id&&i.add(e.id);for(const s of Array.isArray(e.room_drafts)?e.room_drafts:[]){const n=Array.isArray(s.segments)?s.segments:[];for(let o=0;o{const s=Array.isArray(e.openings)?e.openings:[];for(const e of s){if("partition"===e.host?.kind)continue;if(!e.host&&!i){const i=Tg(e,t);i&&(e.host=i);continue}const s=Tg(e,t)??(i?Ig(e,t):null);if(s)e.host=s;else{if(!i)throw new lg("opening-host",e.id);delete e.host}}})(e,a,t),a.reduce((e,t)=>e+(s.has(t.id)?0:1),0)};function Ag(e,t={}){const i=JSON.stringify(e),s=cg(e);fl(s);let n=0;const o=Number(s?.model_version||0)<9;for(const e of Array.isArray(s?.spaces)?s.spaces:[]){const i=t.lineageSpaceId===String(e?.id||"")?t.lineageHints:void 0;n+=Rg(e,o,i)}return s.model_version=9,fl(s),{config:s,changed:i!==JSON.stringify(s),migratedSegments:n}}const zg=e=>!!e&&"object"==typeof e&&!Array.isArray(e);function Fg(e,t){const i=(e,t)=>{if(Array.isArray(e)&&Array.isArray(t)){const s=new Map;for(const t of e)zg(t)&&"string"==typeof t.id&&t.id&&s.set(t.id,t);const n=t.map((t,n)=>{if(zg(t)&&"string"==typeof t.id&&t.id){const e=s.get(t.id);return e?i(e,t):cg(t)}return n[t,t in e?i(e[t],s):cg(s)]);for(const t of Object.keys(e))delete e[t];for(const[t,i]of s)e[t]=i;return e}return cg(t)};return i(e,t)}const Ng=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),Eg=(e,t,i)=>Math.min(i,Math.max(t,e)),Og=e=>Eh({spaces:[e]})[0],Hg=(e,t,i)=>{const s=i>0?i:1,n=e=>`${(e[0]/s).toFixed(12)},${(e[1]/s).toFixed(12)}`,o=n(e),a=n(t);return o0?a:1,l=Math.max(s*r*.02,1e-9),c=[];for(const t of e||[])for(const e of hi(t)||[])c.push([e[0],e[1]]);const h=[];for(const e of i||[])Array.isArray(e)&&e.length>=4&&e.slice(0,4).every(Number.isFinite)&&h.push([e[0],e[1]],[e[2],e[3]]);const d=(e,t)=>t.some(t=>Math.hypot(e[0]-t[0],e[1]-t[1])<=2*l),p=new Map;for(const a of e||[]){if(!a?.id)continue;const u=yc(e,a.id,t,i,s,n,o,r);if(u)for(let e=0;e0&&_>0&&a===m&&_!==a))continue;const g=u.poly[s],f=u.poly[(s+1)%u.poly.length],v=Math.hypot(f[0]-g[0],f[1]-g[1]);if(!(v>l&&v<.5*o-1e-9*o))continue;if(d(g,h)||d(f,h))continue;if(d(g,c)&&d(f,c))continue;const b=Hg(g,f,r),y=p.get(b);y?y.targets.add(a):p.set(b,{a:[g[0],g[1]],b:[f[0],f[1]],targets:new Set([a])})}}}let u=t.slice();const _=(e,t)=>Math.hypot(e[0]-t[0],e[1]-t[1])<=2*l,m=(e,t)=>{if(!Array.isArray(e.a)||!Array.isArray(e.b)||e.a.length<2||e.b.length<2)return!1;const i=[Number(e.a[0])*r,Number(e.a[1])*r],s=[Number(e.b[0])*r,Number(e.b[1])*r];return!![...i,...s].every(Number.isFinite)&&(_(i,t.a)&&_(s,t.b)||_(i,t.b)&&_(s,t.a))};for(const e of[...p.values()].filter(e=>1===e.targets.size).sort((e,t)=>Hg(e.a,e.b,r).localeCompare(Hg(t.a,t.b,r)))){const t=[...e.targets][0],i=u.filter(t=>m(t,e));if(new Set(i.map(e=>e.cm)).size>1)continue;let n=!1;u=u.map(i=>m(i,e)?(n=!0,i.cm===t?i:{...i,cm:t}):i),n||(u=Ul(u,e.a,e.b,t,s,r))}return u}const Bg=e=>{const t=[];for(const i of e||[]){const e=hi(i);if(e?.length)for(let i=0;i"string"==typeof e?.id?e.id:"").filter(Boolean),h=new Set(c),d=new Map;for(const e of c){const t=og("space",e);t.layers>0&&ng("space",t.root)&&ag(d,t.root,e)}const p=new Map,u=new Map,_=new Map,m=new Map;for(const e of r){const t="string"==typeof e?.id?e.id:"";if(!t)continue;const i=new Map;for(const s of Array.isArray(e.rooms)?e.rooms:[]){const e="string"==typeof s?.id?s.id:"";if(!e)continue;const n=og("room",e);n.layers>0&&ng("room",n.root)&&ag(i,n.root,e),u.has(e)||u.set(e,t),_.has(e)||_.set(e,String(s.name||""));const o="string"==typeof s.area?s.area:"";if(o){const i=m.get(o)||[];i.push({spaceId:t,roomId:e}),m.set(o,i)}}p.set(t,i)}const g=new Set(u.keys()),f=new Map,v=new Map;for(const e of l)"string"==typeof e?.id&&e.id&&(!0===e.removed?v.set(e.id,e):f.set(e.id,e));const b={spaceRefsRemapped:0,roomRefsRemapped:0,positionsRemapped:0,markersDetached:0,positionsUnresolved:0,nestedRefsUnresolved:0,deadSpaceIds:[],orphanRoomLabelsRemoved:0,orphanDevicePositionsRemoved:0,orphanGroupPositionsRemoved:0,liveMissingPositionsRemoved:0,removedPositions:[],liveMissingPositions:[],unverifiedPositions:[]},y=new Set,w=e=>h.has(e)?null:(()=>{const t=og("space",e).root;if(!ng("space",t))return null;const i=d.get(t)||[];return 1===i.length?i[0]:null})(),k=(e,t)=>{const i=og("room",e).root;if(!ng("room",i))return null;const s=p.get(t)?.get(i)||[];return 1===s.length?s[0]:null},x=e=>{const t=i.effectiveAreaByMarker?.[e]||"",s=t&&m.get(t)||[];return 1===s.length?s[0]:null},$=(e,t,i)=>{if("string"==typeof e.room_id&&e.room_id&&!g.has(e.room_id)){const s=t?k(e.room_id,t):null;s?(e.room_id=s,b.roomRefsRemapped++):i?(e.room_id=i.roomId,b.roomRefsRemapped++):!0!==e.removed&&delete e.room_id}const s=e.vacuum?.segment_map;if(s&&"object"==typeof s&&!Array.isArray(s))for(const e of Object.keys(s)){const i=s[e];if("string"!=typeof i||!i||g.has(i))continue;const n=t?k(i,t):null;n?(s[e]=n,b.roomRefsRemapped++):b.nestedRefsUnresolved++}};for(const e of l){const t="string"==typeof e?.id?e.id:"";if(!t)continue;const i="string"==typeof e.space?e.space:"",s=i&&!h.has(i),o=n[t],a="string"==typeof o?.s?o.s:"",r=a&&!h.has(a),l=!0===e.removed;let c=i&&h.has(i)?i:null,d=null,p=!1;if(s){const s=w(i);s?(e.space=s,c=s,p=!0,b.spaceRefsRemapped++):l||"virtual"===e.binding?l||(delete e.space,c=null,b.markersDetached++):(d=x(t),d?(e.space=d.spaceId,c=d.spaceId,b.spaceRefsRemapped++):(delete e.space,c=null,b.markersDetached++))}l&&!p||$(e,c,d),r&&p&&a===i&&c&&(n[t]={...o,s:c},b.positionsRemapped++,y.add(t))}for(const e of Object.keys(n)){if(y.has(e))continue;const t=n[e],i="string"==typeof t?.s?t.s:"";if(!i||h.has(i))continue;const s=w(i);if(s){if(e.startsWith("rl_")){const i=e.slice(3),o=k(i,s);if(o){const i=`rl_${o}`;sg(n,i)||(n[i]={...t,s:s}),delete n[e],b.roomRefsRemapped++,b.positionsRemapped++;continue}}n[e]={...t,s:s},b.positionsRemapped++}}const S=i.ownerRoster,M=!0===S?.authoritative,C=new Set(S?.deviceIds||[]),D=new Set(S?.entityIds||[]),P=new Set(Array.isArray(s.settings?.known_devices)?s.settings.known_devices.filter(e=>"string"==typeof e):[]),T=(e,t="")=>{const i=String(S?.names?.[e]||t||"").trim();return i&&i!==e?i:""},I=(e,t,i,s="",n)=>({id:e,spaceId:t,kind:i,name:T(e,s),...n?{reason:n}:{}}),R=e=>{"room_label"===e?b.orphanRoomLabelsRemoved++:"group"===e?b.orphanGroupPositionsRemoved++:b.orphanDevicePositionsRemoved++},A=new Set;for(const[e,t]of Object.entries(n)){const s="string"==typeof t?.s?t.s:"";if(!s||h.has(s))continue;let o,a;const r=f.get(e),l=v.get(e);if(r)o=I(e,s,"device",String(r.name||"")),a="live";else if(l)o=I(e,s,"device",String(l.name||"")),a="absent";else if(e.startsWith("rl_")){const t=e.slice(3);o=I(e,s,"room_label",_.get(t)||""),a=g.has(t)?"live":"absent"}else if(e.startsWith("lg_")){const t=e.slice(3);o=I(e,s,"group"),a=D.has(t)?"live":M?"absent":"unverified","unverified"===a&&(o.reason="registry_unavailable")}else C.has(e)?(o=I(e,s,"device"),a="live"):P.has(e)?(o=I(e,s,"device"),a=M?"absent":"unverified","unverified"===a&&(o.reason="registry_unavailable")):(o=I(e,s,"unknown","","unknown_owner"),a="unverified");if("absent"!==a){if("live"===a){if(b.liveMissingPositions.push(o),!0===i.removeLiveMissingPositions){delete n[e],b.liveMissingPositionsRemoved++,R(o.kind),b.removedPositions.push(o);continue}}else b.unverifiedPositions.push(o);b.positionsUnresolved++,A.add(s)}else delete n[e],R(o.kind),b.removedPositions.push(o)}return b.deadSpaceIds=[...A].sort((e,t)=>e.localeCompare(t)),{config:s,layout:n,report:b,changed:JSON.stringify(s)!==o||JSON.stringify(n)!==a}}(e,t,i),a=o.config,r=_l(a,o.layout);fl(a),gl(o.layout);const l=JSON.stringify(e||{}),c=JSON.stringify(t||{}),h=Number.isInteger(Number(a.model_version))?Number(a.model_version):0,d=(e=>{let t=0,i=0,s=0;for(const i of e.markers||[]){if("ripple"===i.display&&(i.display="icon_ripple",t++),Array.isArray(i.controls)){const e="string"==typeof i.binding&&i.binding.startsWith("entity:")?i.binding.slice(7):"",s=e?i.controls.filter(t=>t!==e):i.controls;JSON.stringify(s)!==JSON.stringify(i.controls)&&(i.controls=s.length?s:null,t++)}const e=i.vacuum;e&&Ng(e,"trail")&&(["never","cleaning","always"].includes(e.trail_mode)||(e.trail_mode=!1===e.trail?"never":"cleaning"),delete e.trail,t++)}for(const n of e.spaces||[]){const e=n.settings;"glow"===e?.fill_mode&&("boolean"!=typeof e.glow_enabled&&(e.glow_enabled=!0),e.fill_mode="none",t++,i++);for(const e of n.rooms||[]){const i=e.settings;"glow"===i?.fill_mode&&("boolean"!=typeof i.glow&&(i.glow=!0),delete i.fill_mode,t++,s++)}if(Ng(n,"segments")&&(delete n.segments,t++),Ng(n,"plan_scale")){const e=Number(n.plan_scale);Number.isFinite(e)&&e>=Ah&&e<=zh&&(Ng(n,"plan_scale_x")||(n.plan_scale_x=e),Ng(n,"plan_scale_y")||(n.plan_scale_y=e),delete n.plan_scale,t++)}const o=Number(n.cell_cm);Ng(n,"cell_cm")&&(!Number.isFinite(o)||o<.1||o>1e3)&&(n.cell_cm=Number.isFinite(o)&&o>0?Eg(o,.1,1e3):5,t++);const a=Number(n.cell_cm),r=Number.isFinite(a)&&a>0?a:5;for(const e of n.wall_columns||[])if("circle"===e.shape)Ng(e,"angle")&&(delete e.angle,t++);else if("square"===e.shape&&Ng(e,"angle")){const i=((Number(e.angle)||0)%90+90)%90;e.angle!==i&&(e.angle=i,t++)}for(const e of n.decor||[]){if(Ng(e,"width")){const i=Number(e.width);(Ng(e,"width_cm")||Number.isFinite(i))&&(Ng(e,"width_cm")||(e.width_cm=Number(Eg(i/Bh*r,.1,100).toFixed(6))),delete e.width,t++)}if("rect"!==e?.kind&&"ellipse"!==e?.kind||!0!==e.fill||(Ng(e,"fill_color")||(e.fill_color=e.color||"#607d8b",t++),Ng(e,"fill_opacity")||(e.fill_opacity=.25,t++)),"text"!==e?.kind)continue;void 0===e.size_cm?(e.size_cm=Number(Eg(ts*is(e)/Bh*r,.1,2e3).toFixed(6)),delete e.scale,delete e.size,t++):(Ng(e,"scale")||Ng(e,"size"))&&(delete e.scale,delete e.size,t++);let i=String(e.text??"");const s=[...i.matchAll(/\{([^{}\r\n]+)\}/g)].some(e=>!!Yi(e[1]));if(!(Ng(e,"entity")||Ng(e,"attr")||Ng(e,"unit")))continue;const n=String(e.unit??"").trim(),o="state"===String(e.attr??"").trim().toLowerCase()?null:e.attr,a=s||n?"":Ji(e.entity,o);if(s||a){if(a){const t=i.indexOf("{}");i=t>=0?i.slice(0,t)+a+i.slice(t+2):`${i}${i?" ":""}${a}`,e.text=i}delete e.entity,delete e.attr,delete e.unit,t++}}}return{total:t,glowSpaces:i,glowRooms:s}})(a);let p=d.total;const u=(_=a.spaces||[],JSON.parse(JSON.stringify(_)));var _;const m=Rm(a.spaces||[],o.layout);let g=0,f=0,v=0,b="";const y=m.spaces.map(e=>{const t=function(e){const t=JSON.parse(JSON.stringify(e||{}));let i=Array.isArray(t.rooms)?t.rooms:[];const s=new Map;for(const e of i){const t=hi(e);if(t)for(let e=0;ee.key.localeCompare(t.key))){let t=null;for(const s of Pa(e,i)){const e=r.get(ba(s.from));if(e&&e!==ba(s.to))continue;const n=Ma(i,s);if(Ca(i,n,s)){t=s,i=n;break}}t?(r.set(ba(t.from),ba(t.to)),n++,a=Math.max(a,Math.hypot(t.to[0]-t.from[0],t.to[1]-t.from[1]))):o++}t.rooms=i;for(const e of t.room_drafts||[]){if(!Array.isArray(e?.points)||e.points.length<2)continue;const t=e.points.map(e=>[...e]),i=t.slice(0,-1).flatMap((i,s)=>{const n=t[s+1],o=fa(i,n);return o?[{key:`${String(e.id||"")}:${s}`,a:[...i],b:[...n],axis:o.axis}]:[]});let s=t;for(const e of i){let t=null;for(const i of Pa(e,[{poly:s}])){const e=s.map(e=>Sa(e,i)),n=e.length>=4&&ya(e[0],e[e.length-1]),o=n?e.slice(0,-1):e;if(n?xa(o):$a(o)){t=i,s=e;break}}t?(n++,a=Math.max(a,Math.hypot(t.to[0]-t.from[0],t.to[1]-t.from[1]))):o++}e.points=s}for(const e of t.partitions||[]){if(!Array.isArray(e?.a)||!Array.isArray(e?.b))continue;const i=fa(e.a,e.b);if(!i)continue;const s={key:String(e.id||wa(e.a,e.b)),a:[...e.a],b:[...e.b],axis:i.axis};let r=null;for(const i of Pa(s,[{poly:[e.a,e.b]}])){const s=Sa(e.a,i),n=Sa(e.b,i),o=Math.hypot(n[0]-s[0],n[1]-s[1]);if(!(o>0))continue;const a=(t.openings||[]).filter(t=>"partition"===t?.host?.kind&&t.host.id===e.id).every(e=>{const t=Number(e.host.t),i=Number(e.length);return Number.isFinite(t)&&t>=0&&t<=1&&Number.isFinite(i)&&i>0&&t*o-i/2>=-1e-12&&t*o+i/2<=o+1e-12});if(a){r=i,e.a=s,e.b=n;break}}r?(n++,a=Math.max(a,Math.hypot(r.to[0]-r.from[0],r.to[1]-r.from[1]))):o++}return{space:t,report:{wallsStraightened:n,wallsStraightenSkipped:o,maxStraightenShift:a},changed:n>0}}(e);g+=t.report.wallsStraightened,f+=t.report.wallsStraightenSkipped;const i=Number(e?.cell_cm)>0?Number(e.cell_cm):5,s=t.report.maxStraightenShift/jh*i;return s>v&&(v=s,b=String(e?.id||"")),t.space}),w=g?Rm(y,m.layout):{...m,spaces:y};a.spaces=w.spaces;const k=g?{...m.report,moved:m.report.moved+w.report.moved,coordsCanonicalized:m.report.coordsCanonicalized+w.report.coordsCanonicalized,maxShift:Math.max(m.report.maxShift,w.report.maxShift),maxShiftCm:Math.max(m.report.maxShiftCm,w.report.maxShiftCm),maxSpace:w.report.maxShiftCm>m.report.maxShiftCm?w.report.maxSpace:m.report.maxSpace,rotated:m.report.rotated+w.report.rotated,removedDrafts:m.report.removedDrafts+w.report.removedDrafts}:{...m.report};let x=0,$=0,S=0,M=0,C=0,D=0,P=0,T=0;for(let e=0;e0===Number(e.cm)),walls:t.walls||[]}),o=Og(t),r=Og(i);if(!o||!r)continue;const l=Bg(o.rooms),c=Bg(r.rooms),h=.02*Bh,d=(t.wall_segments||[]).filter(e=>0===Number(e.cm)).map(e=>({...e,key:Il(e.a,e.b,jh),cm:0})),p=new Set(d.map(e=>e.key)),_=J_(t,o.rooms,Ih,h);$+=_.length;for(const e of _){const t=[e[0]/Ih,e[1]/Ih],i=[e[2]/Ih,e[3]/Ih],s=Il(t,i,jh);p.has(s)||(p.add(s),d.push({key:s,a:t,b:i,cm:0}))}const m=d.length,g=ql(d,l,c,jh,Ih),f=(e,t,i)=>{const s=i[0]-t[0],n=i[1]-t[1],o=s*s+n*n;if(!(o>0))return Math.hypot(e[0]-t[0],e[1]-t[1]);const a=Math.max(0,Math.min(1,((e[0]-t[0])*s+(e[1]-t[1])*n)/o));return Math.hypot(e[0]-(t[0]+s*a),e[1]-(t[1]+n*a))},v=g.filter(e=>{if(!Array.isArray(e.a)||!Array.isArray(e.b))return!1;const t=e.a.map(e=>e*Ih),i=e.b.map(e=>e*Ih);return c.some(([e,s])=>f(t,e,s)<=4*h&&f(i,e,s)<=4*h)});S+=Math.max(0,m-v.length);const b=(i.wall_segments||[]).filter(e=>Number(e.cm)>0);i.wall_segments=[...b,...v.map(({key:e,...t})=>t)];const y=v.map(e=>[e.a[0]*Ih,e.a[1]*Ih,e.b[0]*Ih,e.b[1]*Ih]),w=Array.isArray(t.walls)?t.walls.length:0;let k=ql(t.walls,l,c,jh,Ih);k=Lg(r.rooms,k,y,jh,Number(i.cell_cm)>0?Number(i.cell_cm):5,Bh,Ih),k=xc(r.rooms,k,y,jh,Number(i.cell_cm)>0?Number(i.cell_cm):5,Bh,Ih),k=Ll(k,i.rooms||[],jh,1,y.map(e=>[e[0]/Ih,e[1]/Ih,e[2]/Ih,e[3]/Ih])),x+=Math.max(0,w-k.length),k.length?i.walls=k:delete i.walls;const I=Hm(i.partitions||[],{pitch:jh,geometry:Bm(i)});I.merged&&(M+=I.merged,i.partitions=I.partitions,Lm(i.openings,i.partitions,I.openingMoves,{coordScale:Ih,cellCm:Number(i.cell_cm)>0?Number(i.cell_cm):5,gridPitch:Bh}));const R=Og(i);if(R){const e=n(i,R,i.walls||[],y,{pitch:jh,cellCm:Number(i.cell_cm)>0?Number(i.cell_cm):5,gridPitch:Bh,coordScale:Ih});e.partitionsReconciled&&(C+=e.partitionsReconciled,D+=e.openingsRehosted,e.partitions.length?i.partitions=e.partitions:delete i.partitions,e.openings.length?i.openings=e.openings:delete i.openings,e.walls.length?i.walls=e.walls:delete i.walls),e.removedDrafts&&(P+=e.removedDrafts,e.roomDrafts.length?i.room_drafts=e.roomDrafts:delete i.room_drafts)}JSON.stringify({zero:(i.wall_segments||[]).filter(e=>0===Number(e.cm)),walls:i.walls||[]})!==s&&T++}let I=0;h<=9&&(I=function(e){const t=Ag(e);return Fg(e,t.config),{...t,config:e}}(a).migratedSegments);const R=fl(a),A=gl(w.layout),z=JSON.stringify(R)!==l||JSON.stringify(A)!==c;h<9&&z&&(R.model_version=9);const F=JSON.stringify(R)!==l||JSON.stringify(A)!==c,N=Number.isInteger(Number(R.model_version))?Number(R.model_version):h,E=F?k:{...k,moved:0,coordsCanonicalized:0,maxShift:0,maxShiftCm:0,maxSpace:"",rotated:0,removedDrafts:0},O=F?o.report:{...o.report,spaceRefsRemapped:0,roomRefsRemapped:0,positionsRemapped:0,markersDetached:0,orphanRoomLabelsRemoved:0,orphanDevicePositionsRemoved:0,orphanGroupPositionsRemoved:0,liveMissingPositionsRemoved:0};return{config:R,layout:A,report:{...E,modelFrom:h,modelTo:N,migrated:F?p:0,glowSpacesMigrated:F?d.glowSpaces:0,glowRoomsMigrated:F?d.glowRooms:0,canonicalized:F?T:0,wallSegmentsMigrated:F?I:0,legacyZeroWallsMigrated:F?$:0,wallsMerged:F?x:0,spansMerged:F?S:0,partitionsMerged:F?M:0,partitionsReconciled:F?C:0,openingsRehosted:F?D:0,redundantDraftsRemoved:F?P:0,wallsStraightened:F?g:0,wallsStraightenSkipped:f,maxStraightenShiftCm:F?v:0,maxStraightenSpace:F?b:"",latticeCoordinatesCanonicalized:F?r.canonicalized:0,latticeCoordinatesFar:F?r.far:0,latticeMaxShift:F?r.maxShift:0,latticeMaxShiftCm:F?r.maxShiftCm:0,latticeSpaces:F?r.spaces:[],...O},changed:F}}function qg(e,t,i){const s=(e?.spaces||[]).find(e=>e?.id===i),n=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),o=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===i||"string"==typeof e.room_id&&n.has(e.room_id)||t?.[e.id]?.s===i).map(e=>e.id))].sort((e,t)=>e.localeCompare(t));return{markerIds:o,count:o.length}}function Wg(e){return e instanceof Error?e.name||"Error":typeof e}function Ug(e){return Jd({id:e?.id??"",cell_cm:e?.cell_cm,rooms:e?.rooms||[],walls:e?.walls||[],wall_segments:e?.wall_segments||[],open_spans:e?.open_spans||[],openings:e?.openings||[],partitions:e?.partitions||[],room_drafts:e?.room_drafts||[],wall_columns:e?.wall_columns||[]})}function Gg(e,t,i,s=Bh,n=1e3){return(Array.isArray(e?.openings)?e.openings:[]).flatMap(e=>{const o={...e,rx:Number(e.x)*n,ry:Number(e.y)*n,rlen:Number(e.length)*n};if(!e.host||"wall"===e.host.kind)return[o];const a=ou(e,t.partitions,n,i,s);return a.resolved?[{...e,rx:a.resolved.center[0],ry:a.resolved.center[1],rlen:a.resolved.length,angle:a.resolved.angle,partitionHost:a.resolved}]:[]})}function Vg(e,t=()=>!0){return e.flatMap(e=>e.host&&e.partitionHost&&t(e)?[lu(e.partitionHost)]:[])}function Kg(e,t,i,s,n,o,a=Bh,r=1e3){const l=wc(t.rooms,[...i],s,n,o,a,r);return e.flatMap(e=>{const t={x:e.rx,y:e.ry,angle:Number(e.angle)||0,length:e.rlen};return e.host&&"wall"!==e.host.kind?e.partitionHost&&du(e.partitionHost,l,2e-4*a)?[t]:[]:[t]})}function Yg(e,t){const i=jh,s=Bh,n=Ih,o=Number.isFinite(Number(t.cellCm))&&Number(t.cellCm)>0?Number(t.cellCm):5,a=Array.isArray(e?.walls)?e.walls:[],r=function(e,t,i=Bh,s=1e3){return qd(t.rooms,e?.open_spans,s,.02*i)}(e,t,s,n),l=function(e,t,i=Bh){if(!t.length)return[];const s=e.filter(e=>e?.id),n=.02*i,o=[];for(let e=0;e{const t=[(e[0]+e[2])/2,(e[1]+e[3])/2];return h.some(e=>Hs(t,e)<4*n)}).map(e=>[...e]);d.length&&o.push({a:a,b:r,segs:d})}return o}(t.rooms,r,s).flatMap(e=>e.segs),c=Gg(e,t,o,s,n),h=Vg(c),d=_h(t,o,s,2e-4*s,h).all;return{space:t,walls:a,openCuts:l,openings:c,roomOpenings:Kg(c,t,a,l,i,o,s,n),partitionCuts:h,physicalBodies:d,wallKeyPitch:i,cellCm:o,gridPitch:s,coordScale:n}}function Jg(e,t,i){const s="string"==typeof e?.title?e.title.trim():"",n=null==e?.id?"":String(e.id).trim();return{spaceId:n,displayName:s||n||i(t+1)}}function Xg(e,t={}){const i=(t.fingerprint||Jd)(e),s=Array.isArray(e?.spaces)?e.spaces:[],n=t.fallbackSpaceName||(e=>`Space ${e}`),o=t.prepareSpace||Yg,a=t.wallPass||qc,r=t.floorPass||Hc,l=[];for(let i=0;i0||d.physicalBodies.length>0;if(!d.space.rooms.length&&!p){l.push({...h,status:"not-applicable"});continue}let u=null;if(p){try{u=a(d.space.rooms,d.walls,d.openCuts,d.roomOpenings,d.wallKeyPitch,d.cellCm,d.gridPitch,d.coordScale,d.physicalBodies)}catch(e){l.push({...h,status:"failed",reason:"wall-exception",detail:Wg(e)});continue}if(null==u){l.push({...h,status:"failed",reason:"wall-null"});continue}if("degraded-extra"===u.status){l.push({...h,status:"failed",reason:"wall-degraded-extra"});continue}if("failed-core"===u.status){l.push({...h,status:"failed",reason:"wall-failed-core"});continue}t.captureWallGeometry?.(d,u)}if(d.space.rooms.length&&null==u?.paperGeom){let e;try{e=r(d.space.rooms,d.walls,d.openCuts,d.wallKeyPitch,d.cellCm,d.gridPitch,d.coordScale)}catch(e){l.push({...h,status:"failed",reason:"floor-exception",detail:Wg(e)});continue}if(null==e){l.push({...h,status:"failed",reason:"floor-null"});continue}}l.push({...h,status:"ok"})}const c=l.filter(e=>"failed"===e.status);return{fingerprint:i,spaces:l,failures:c,ok:0===c.length}}class Zg{constructor(e=50){this._undo=[],this._redo=[],this._limit=Math.max(30,Math.floor(e))}get canUndo(){return this._undo.length>0}get canRedo(){return this._redo.length>0}get undoName(){return this._undo[this._undo.length-1]?.name??null}get redoName(){return this._redo[this._redo.length-1]?.name??null}get size(){return this._undo.length}push(e){this._undo.push(e),this._undo.length>this._limit&&this._undo.splice(0,this._undo.length-this._limit),this._redo=[]}undo(){const e=this._undo.pop()??null;return e&&this._redo.push(e),e}redo(){const e=this._redo.pop()??null;return e&&this._undo.push(e),e}clear(){this._undo=[],this._redo=[]}}const Qg=new WeakMap;const ef=(e,t,i)=>255===e[i+3]&&t.every((t,s)=>Math.abs(e[i+s]-t)<=2);async function tf(e){const t=e.defaultView;if(!t||!t.CSS?.supports?.("mix-blend-mode","screen"))return!1;const i=[128,32,16],s=[16,64,128],n=[9,19,29],o=`\n \n \n \n \n \n `,a=t.URL.createObjectURL(new Blob([o],{type:"image/svg+xml"}));try{const o=await new Promise((e,i)=>{const s=new t.Image,n=t.setTimeout(()=>i(new Error("SVG blend probe timeout")),1e3);s.onload=()=>{t.clearTimeout(n),e(s)},s.onerror=()=>{t.clearTimeout(n),i(new Error("SVG blend probe failed"))},s.src=a}),r=e.createElement("canvas");r.width=4,r.height=1;const l=r.getContext("2d",{willReadFrequently:!0});if(!l)return!1;l.drawImage(o,0,0,4,1);const c=l.getImageData(0,0,4,1).data;return ef(c,i,0)&&ef(c,function(e,t){return[0,1,2].map(i=>Math.round(255-(255-e[i])*(255-t[i])/255))}(i,s),4)&&ef(c,s,8)&&ef(c,n,12)}catch{return!1}finally{t.URL.revokeObjectURL(a)}}const sf={now:()=>performance.now(),requestFrame:e=>requestAnimationFrame(e),cancelFrame:e=>cancelAnimationFrame(e)},nf=e=>Math.max(0,Math.min(1,e)),of=(e,t,i)=>e+(t-e)*i,af=(e,t,i)=>{const s=1-e;return 3*s*s*e*t+3*s*e*e*i+e*e*e};function rf(e,t){return{centerX:e.x+e.w/2,centerY:e.y+e.h/2,pixelsPerUnit:t>0&&e.w>0?t/e.w:1,viewBox:{...e}}}function lf(e,t,i){const s=Math.max(1e-9,e.pixelsPerUnit),n=Math.max(1e-9,t/s),o=Math.max(1e-9,i/s);return{x:e.centerX-n/2,y:e.centerY-o/2,w:n,h:o}}function cf(e){const t=e.trim(),i=/^rgba?\(\s*([\d.]+)[, ]+([\d.]+)[, ]+([\d.]+)(?:\s*[,/]\s*([\d.]+))?\s*\)$/i.exec(t);if(i)return[Number(i[1]),Number(i[2]),Number(i[3]),null==i[4]?1:Number(i[4])];const s=/^#([0-9a-f]{6}|[0-9a-f]{8})$/i.exec(t)?.[1];return s?[parseInt(s.slice(0,2),16),parseInt(s.slice(2,4),16),parseInt(s.slice(4,6),16),8===s.length?parseInt(s.slice(6,8),16)/255:1]:null}function hf(e,t,i){const s=cf(e),n=cf(t);if(!s||!n)return i<.5?e:t;const o=nf(i);return`rgba(${Math.round(of(s[0],n[0],o))}, ${Math.round(of(s[1],n[1],o))}, ${Math.round(of(s[2],n[2],o))}, ${of(s[3],n[3],o).toFixed(3)})`}function df(e,t,i){const s=(e=>{const t=nf(e);if(0===t||1===t)return t;let i=0,s=1,n=t;for(let e=0;e<12;e++)n=(i+s)/2,af(n,.2,.2){const t=this._state;if(!t||t.token!==n)return;const i=nf((e-t.startedAt)/t.duration);t.presented=i>=1?t.to:df(t.from,t.to,i),t.phase=i>=1?"settling":"running",this._hooks.frame(t),i<1?this._raf=this._clock.requestFrame(a):(this._raf=0,this._hooks.settled(t),this._state?.token===n&&(this._state=null))};return this._raf=this._clock.requestFrame(a),n}cancel(e=!0){this._raf&&this._clock.cancelFrame(this._raf),this._raf=0;const t=this._state;this._state=null,this._token++,t&&e&&(t.phase="settling",t.presented=t.to,this._hooks.frame(t),this._hooks.settled(t))}dispose(){this.cancel(!1)}}const uf="houseplan_card_labs_v1",_f=Object.freeze([Object.freeze({id:"iso",issue:89,since:"1.62.0",expires:"1.65.0",summary:"Volumetric plan renderer"})]);function mf(e){const t=/^(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?$/.exec(String(e||"").trim());if(!t)return null;const i=t.slice(1).map(Number);return i.every(e=>Number.isSafeInteger(e))?i:null}function gf(e,t){for(let i=0;i<3;i++)if(e[i]!==t[i])return e[i]e.split(",")).map(e=>e.trim()).filter(Boolean)}function kf(e,t,i,s=_f){const n=function(e,t=_f){const i=mf(e),s=new Map;if(!i||!vf(t))return s;for(const e of t){const t=mf(e.since),n=mf(e.expires);gf(i,t)>=0&&gf(i,n)<0&&s.set(e.id,e)}return s}(i,s),o=new Set(vf(s)?s.map(e=>e.id):[]),a=new Set(function(e){if(!e)return[];try{const t=JSON.parse(e);return Array.isArray(t)?t.filter(e=>"string"==typeof e):[]}catch{return[]}}(t).filter(e=>n.has(e)));let r=!1;const l=e=>{if("off"===e)return a.clear(),void(r=!0);const t=e.startsWith("-"),i=t?e.slice(1):e;o.has(i)&&(r=!0,t||!n.has(i)?a.delete(i):a.add(i))};for(const t of wf(new URLSearchParams(String(e.search||"").replace(/^\?/,""))))l(t);for(const t of wf(bf(e.hash)))l(t);const c=Object.freeze([...a].sort());return{active:c,space:yf(e.hash),persist:r?JSON.stringify(c):void 0,knownUrlOperation:r}}let xf={active:Object.freeze([]),space:""},$f="",Sf=!1,Mf="";const Cf=new Set;function Df(){if("undefined"==typeof window)return xf;const e=kf(window.location,function(){try{return window.localStorage.getItem(uf)}catch{return null}}(),$f);void 0!==e.persist&&function(e){try{window.localStorage.setItem(uf,e)}catch{}}(e.persist),xf={active:e.active,space:e.space},window.__hpLabs=e.active;for(const e of Cf)e(xf);return xf}function Pf(){Df()}function Tf(e){return $f=e,"undefined"==typeof window?xf:(Sf||(Sf=!0,window.addEventListener("hashchange",Pf),window.addEventListener("popstate",Pf)),Df())}const If=Object.freeze({rotDeg:0,tiltDeg:20,xyScale:1,zScale:1,origin:Object.freeze([500,500])});function Rf(e){return[e.rotDeg,e.tiltDeg,e.xyScale,e.zScale,e.origin[0],e.origin[1]].every(Number.isFinite)&&Math.abs(e.xyScale)>1e-12&&Math.abs(Math.cos(e.tiltDeg*Math.PI/180))>1e-12}function Af(e,t,i=If){if(!(Rf(i)&&Number.isFinite(e[0])&&Number.isFinite(e[1])&&Number.isFinite(t)))throw new Error("invalid isometric projection input");const s=i.rotDeg*Math.PI/180,n=i.tiltDeg*Math.PI/180,o=e[0]-i.origin[0],a=e[1]-i.origin[1],r=(o*Math.cos(s)-a*Math.sin(s))*i.xyScale,l=(o*Math.sin(s)+a*Math.cos(s))*i.xyScale;return[i.origin[0]+r,i.origin[1]+l*Math.cos(n)-t*i.zScale*Math.sin(n)]}function zf(e,t=If){if(!Rf(t)||!Number.isFinite(e[0])||!Number.isFinite(e[1]))throw new Error("invalid isometric projection input");const i=t.rotDeg*Math.PI/180,s=t.tiltDeg*Math.PI/180,n=(e[0]-t.origin[0])/t.xyScale,o=(e[1]-t.origin[1])/(t.xyScale*Math.cos(s));return[t.origin[0]+n*Math.cos(i)+o*Math.sin(i),t.origin[1]-n*Math.sin(i)+o*Math.cos(i)]}function Ff(e=If){return`matrix(${function(e=If){if(!Rf(e))throw new Error("invalid isometric camera");const t=e.rotDeg*Math.PI/180,i=e.tiltDeg*Math.PI/180,s=e.xyScale*Math.cos(t),n=-e.xyScale*Math.sin(t),o=e.xyScale*Math.sin(t)*Math.cos(i),a=e.xyScale*Math.cos(t)*Math.cos(i);return[s,o,n,a,e.origin[0]-s*e.origin[0]-n*e.origin[1],e.origin[1]-o*e.origin[0]-a*e.origin[1]]}(e).map(e=>Number(e.toFixed(12))).join(" ")})`}function Nf(e,t=If){const{rect:i,wallHeight:s}=e,n=e.openingHeight??s,o=e.floorDepth??0;if(!(i.w>=0)||!(i.h>=0)||!Number.isFinite(s)||!Number.isFinite(n)||!Number.isFinite(o)||s<0||n<0||o<0)throw new Error("invalid isometric frame");const a=[[i.x,i.y],[i.x+i.w,i.y],[i.x+i.w,i.y+i.h],[i.x,i.y+i.h]],r=Math.max(s,n),l=a.flatMap(e=>[Af(e,-o,t),Af(e,r,t)]),c=l.map(e=>e[0]),h=l.map(e=>e[1]),d=Math.min(...c),p=Math.min(...h);return{x:d,y:p,w:Math.max(...c)-d,h:Math.max(...h)-p}}const Ef=e=>`${Number(e[0].toFixed(4))} ${Number(e[1].toFixed(4))}`;function Of(e){let t=0;for(let i=0;iArray.isArray(e)&&Number.isFinite(e[0])&&Number.isFinite(e[1])).map(e=>[Number(e[0]),Number(e[1])]),i=t[t.length-1];return t.length>1&&t[0][0]===i[0]&&t[0][1]===i[1]&&t.pop(),t}(e);if(i.length<3)return[];const s=!t;return Of(i)>0===s?i:[...i].reverse()}function Lf(e,t,i){return e.length?`M ${e.map(e=>Ef(Af(e,i,t))).join(" L ")} Z`:""}function Bf(e,t,i){const s=t[1]-e[1],n=-(t[0]-e[0]),o=i.rotDeg*Math.PI/180;return s*Math.sin(o)+n*Math.cos(o)}function jf(e,t,i=If){if(!Number.isFinite(t)||t<0)throw new Error("invalid floor edge height");const s=(e||[]).map(e=>(e||[]).map((e,t)=>function(e,t){const i=Hf(e,t);if(!i.length)return i;let s=0;for(let e=1;e0)).filter(e=>e.length>=3&&Math.abs(Of(e))>=1e-9)).filter(e=>e.length&&Of(e[0])>0);s.sort((e,t)=>{const i=e[0][0],s=t[0][0];return i[0]-s[0]||i[1]-s[1]||Math.abs(Of(t[0]))-Math.abs(Of(e[0]))});const n=[],o=[];let a=0;for(let e=0;ee.depth-t.depth||e.component-t.component||e.edge-t.edge),{footprintPath:n.join(" "),sides:o,componentCount:s.length,edgeCount:a}}function qf(e,t){return e^=Math.round(64*(Number.isFinite(t)?t:0)),Math.imul(e,16777619)>>>0}const Wf=e=>e.every(Number.isFinite);function Uf(e,t,i,s){const n=e[0]*i,o=e[1]*s,a=t*Math.PI/180;return[n*Math.cos(a)-o*Math.sin(a),n*Math.sin(a)+o*Math.cos(a)]}function Gf(e,t,i,s,n,o,a){const r=e.flipH?-1:1,l="gate"===e.type?1:e.flipV?-1:1,c=(e.type,e.flipV,e.angle,e.face,{ox:0,oy:0}),h=function(e,t){return[e[0]+t[0],e[1]+t[1]]}([e.x+c.ox,e.y+c.oy],Uf(i,e.angle,r,l));return{leaf:t,hinge:h,closedVector:Uf(s,e.angle,r,l),quarterVector:Uf([-s[1],s[0]],e.angle,r,l),turnDeg:n,bottom:o,top:a}}const Vf=e=>`${Number(e[0].toFixed(4))} ${Number(e[1].toFixed(4))}`;function Kf(e,t,i=If){const s=Math.max(0,Math.min(1,Number.isFinite(t)?t:0));return e.leaves.map(t=>{const n=function(e,t){const i=e.turnDeg*t*Math.PI/180;return[e.hinge[0]+e.closedVector[0]*Math.cos(i)+e.quarterVector[0]*Math.sin(i),e.hinge[1]+e.closedVector[1]*Math.cos(i)+e.quarterVector[1]*Math.sin(i)]}(t,s),o=Af(t.hinge,t.bottom,i),a=Af(n,t.bottom,i),r=Af(n,t.top,i),l=Af(t.hinge,t.top,i);return{id:e.id,sourceIndex:e.sourceIndex,type:e.type,leaf:t.leaf,d:`M ${Vf(o)} L ${Vf(a)} L ${Vf(r)} L ${Vf(l)} Z`,shadowD:`M ${Vf(Af(t.hinge,0,i))} L ${Vf(Af(n,0,i))}`,depth:Math.max(o[1],a[1])}})}const Yf={window:120,door:90,passage:90,gate:300};function Jf(e){return Yf[e]}function Xf(e,t){const i=[e[0],e[1]],s=[t[0],t[1]];return function(e,t){return e[0]-t[0]||e[1]-t[1]}(i,s)<=0?{a:i,b:s}:{a:s,b:i}}function Zf(e){const t=new Map;return e.forEach((e,i)=>{if(!e.kind||e.open)return;const s=Xf(e.a,e.b);if(Math.hypot(s.b[0]-s.a[0],s.b[1]-s.a[1])<=1e-9)return;const n=function(e,t){const i=e=>(Math.abs(e)<=5e-10?0:Math.round(1e6*e)/1e6).toFixed(6);return`${i(e[0])},${i(e[1])}>${i(t[0])},${i(t[1])}`}(s.a,s.b),o=t.get(n);if(o)return o.physicalHalfWidth=Math.max(o.physicalHalfWidth,e.half||0),o.sourceOrder=Math.min(o.sourceOrder,i),void(e.partitionHost&&(o.partitionHost&&o.partitionHost.id!==e.partitionHost.id?o.ambiguousPartitionHost=!0:o.partitionHost=e.partitionHost));t.set(n,{segmentKey:n,a:s.a,b:s.b,physicalHalfWidth:Math.max(0,e.half||0),sourceOrder:i,...e.partitionHost?{partitionHost:e.partitionHost}:{}})}),[...t.values()]}function Qf(e,t){if(Math.abs(e.distance-t.distance)>1e-9)return e.distance-t.distance;if(Math.abs(e.perpendicular-t.perpendicular)>1e-9)return e.perpendicular-t.perpendicular;return(e.target.segmentKeyt.target.segmentKey?1:0)||e.target.sourceOrder-t.target.sourceOrder}function ev(e){const t=Zf(e.intervals).map(t=>{const i=function(e,t){const i=t.b[0]-t.a[0],s=t.b[1]-t.a[1],n=Math.hypot(i,s),o=i/n,a=s/n,r=(e[0]-t.a[0])*o+(e[1]-t.a[1])*a,l=Math.max(0,Math.min(n,r)),c=t.a[0]+l*o,h=t.a[1]+l*a;return{along:l,length:n,x:c,y:h,distance:Math.hypot(e[0]-c,e[1]-h),perpendicular:Math.abs((e[0]-t.a[0])*a-(e[1]-t.a[1])*o)}}(e.pointer,t);return{target:t,...i,envelope:Math.max(e.baseTolerance,t.physicalHalfWidth+e.bodyPointerPadding)}}).filter(e=>e.distance<=e.envelope+1e-9).filter(t=>!function(e,t,i,s,n){const o=t.b[0]-t.a[0],a=t.b[1]-t.a[1],r=Math.hypot(o,a);if(!(r>1e-9))return!1;const l=o/r,c=a/r;for(const o of i){if(!o.open&&o.kind)continue;const i=Xf(o.a,o.b),a=i.b[0]-i.a[0],r=i.b[1]-i.a[1],h=Math.hypot(a,r);if(!(h>1e-9))continue;const d=a/h,p=r/h;if(Math.abs(l*p-c*d)>1e-6)continue;if(Math.abs((i.a[0]-t.a[0])*c-(i.a[1]-t.a[1])*l)>n)continue;const u=(e[0]-i.a[0])*d+(e[1]-i.a[1])*p,_=1e-9;if(!(u<=_||u>=h-_)&&Math.abs((e[0]-i.a[0])*p-(e[1]-i.a[1])*d)<=s+1e-9)return!0}return!1}(e.pointer,t.target,e.intervals,t.envelope,Math.max(1e-9,Math.min(e.baseTolerance,.04*e.gridStep)))),i=t.filter(t=>!!t.target.partitionHost&&e.renderedLength+2*t.target.physicalHalfWidth>t.length+1e-9).sort(Qf),s=t.filter(t=>!t.target.partitionHost||e.renderedLength+2*t.target.physicalHalfWidth<=t.length+1e-9).sort(Qf);let n=s[0];if(!n)return{candidate:null,jambBlockedTarget:i[0]?.target||null};const o=s.filter(e=>Math.abs(e.distance-n.distance)<=1e-9&&Math.abs(e.perpendicular-n.perpendicular)<=1e-9),a=o.filter(e=>e.target.partitionHost);if(a.length){if(1!==new Set(a.map(e=>e.target.partitionHost.id)).size||a.some(e=>e.target.ambiguousPartitionHost))return{candidate:null,jambBlockedTarget:null};const e=a[0],t=e.target.b[0]-e.target.a[0],i=e.target.b[1]-e.target.a[1],s=Math.hypot(t,i),r=t/s,l=i/s,c=o.every(t=>{const i=t.target.b[0]-t.target.a[0],s=t.target.b[1]-t.target.a[1],n=Math.hypot(i,s),o=i/n,a=s/n,c=Math.abs(r*a-l*o)<=1e-6,h=Math.abs((t.target.a[0]-e.target.a[0])*l-(t.target.a[1]-e.target.a[1])*r);return c&&h<=1e-9});if(!c)return{candidate:null,jambBlockedTarget:null};n=e}if(n.target.ambiguousPartitionHost)return{candidate:null,jambBlockedTarget:null};const{target:r,length:l}=n,c=r.b[0]-r.a[0],h=r.b[1]-r.a[1],d=c/l,p=h/l,u=r.partitionHost?r.physicalHalfWidth:0,_=Math.min(Math.max(0,e.renderedLength)/2+u,l/2);let m=n.along;const g=Math.max(e.gridStep,1e-9),f=l/2;m=Math.abs(m-f)<=g/2?f:Math.round(m/g)*g,m=Math.max(_,Math.min(l-_,m));const v=r.a[0]+d*m,b=r.a[1]+p*m,y=Math.max(0,e.renderedLength)/2,w=m-y,k=m+y,x=Math.max(0,w),$=Math.max(0,l-k),S=[r.a[0]+d*(w-x/2),r.a[1]+p*(w-x/2)],M=[r.a[0]+d*(k+$/2),r.a[1]+p*(k+$/2)];let C=180*Math.atan2(h,c)/Math.PI;C>=90?C-=180:C<-90&&(C+=180);const D=Math.abs(m-f)<=1e-9;return{candidate:{presetRevision:e.preset.revision,geometryRevision:e.geometryRevision,pointer:[e.pointer[0],e.pointer[1]],type:e.preset.type,lengthCm:e.preset.lengthCm,flipH:e.preset.flipH,flipV:e.preset.flipV,x:v,y:b,angle:C,renderedLength:e.renderedLength,target:r,...r.partitionHost?{host:{...r.partitionHost,t:l>0?m/l:0}}:{},measure:{labels:[{distance:x,midpoint:S},{distance:$,midpoint:M}],guide:D?{x:v,y:b,angle:C}:null}},jambBlockedTarget:null}}function tv(e,t,i,s,n=1e-6){return e.presetRevision===i&&e.geometryRevision===s&&Math.hypot(e.pointer[0]-t[0],e.pointer[1]-t[1])<=n}const iv=(e,t)=>e[0]*t[1]-e[1]*t[0],sv=(e,t)=>e[0]*t[0]+e[1]*t[1],nv=(e,t)=>[e[0]-t[0],e[1]-t[1]],ov=(e,t,i)=>[e[0]+t[0]*i,e[1]+t[1]*i];function av(e,t,i){return Math.abs(iv(e,t))<=i}function rv(e,t,i){return Math.abs(iv(nv(e,t),i))}function lv(e,t,i,s,n){const o=t[0]-e[0],a=t[1]-e[1],r=Math.hypot(o,a);if(!(r>n&&i>0))return[];const l=[o/r,a/r];return function(e,t,i){const s=t.map(([t,i])=>[Math.max(0,Math.min(t,i)),Math.min(e,Math.max(t,i))]).filter(([e,t])=>t>e+i).sort((e,t)=>e[0]-t[0]||e[1]-t[1]),n=[];for(const e of s){const t=n[n.length-1];!t||e[0]>t[1]+i?n.push([...e]):t[1]=Math.max(t[1],e[1])}const o=[];let a=0;for(const[e,t]of n)e>a+i&&o.push([a,e]),a=Math.max(a,t);return e>a+i&&o.push([a,e]),o}(r,s.flatMap(t=>{const s=t.b[0]-t.a[0],o=t.b[1]-t.a[1],a=Math.hypot(s,o);if(!(a>n&&av(l,[s/a,o/a],1e-6)))return[];if(rv(t.a,e,l)>1.1*Math.max(i,n)||rv(t.b,e,l)>1.1*Math.max(i,n))return[];return[[sv(nv(t.a,e),l),sv(nv(t.b,e),l)]]}),n).flatMap(([t,s])=>{const n=Kl({a:ov(e,l,t),b:ov(e,l,s),halfDepth:i});return n?[n]:[]})}function cv(e){const t=e.coordScale??1,i=Math.max(e.epsilon??2e-4*e.gridPitch,1e-9),s=[];for(const i of e.rooms||[]){if(!i?.id)continue;const n=yc(e.rooms,i.id,e.walls,e.openCuts,e.pitch,e.cellCm,e.gridPitch,t);if(!n||n.poly.length<3)continue;const o=lc(n.poly,n.offsets);!o||o.length<3||s.push({roomId:i.id,profile:n,inner:o})}s.sort((e,t)=>e.roomId.localeCompare(t.roomId));const n=(e.roomOpenings||[]).map(e=>{const t=e.angle*Math.PI/180,i=[Math.cos(t),Math.sin(t)],s=Math.max(0,e.length)/2;return{a:ov([e.x,e.y],i,-s),b:ov([e.x,e.y],i,s)}}),o=[];for(const s of Gc(e.rooms,e.walls,e.openCuts,e.pitch,e.cellCm,e.gridPitch,t))for(const e of lv(s.a,s.b,s.depthUnits/2,n,i))o.push({body:e});const a=new Map;for(const t of e.partitionCuts||[]){const e=a.get(t.hostId)||[];e.push(t),a.set(t.hostId,e)}for(const t of e.partitions||[]){const s=Ml(t.cm,e.cellCm,e.gridPitch)/2;for(const e of lv(t.a,t.b,s,a.get(t.id)||[],i))o.push({body:e,partitionId:t.id})}return{rooms:s,boundaries:o,epsilon:i}}function hv(e,t,i,s){const n=e.profile.offsets[t];if(!(n>0))return null;const o=function(e,t){const i=e[t],s=e[(t+1)%e.length],n=s[0]-i[0],o=s[1]-i[1],a=Math.hypot(n,o)||1;return function(e){let t=0;for(let i=0;i=0?[-o/a,n/a]:[o/a,-n/a]}(e.profile.poly,t),a=ov(i.center,o,n),r=[];for(let t=0;ts&&av(i.axis,[l[0]/c,l[1]/c],1e-6)))continue;if(rv(n,a,i.axis)>s||rv(o,a,i.axis)>s)continue;const h=sv(nv(n,a),i.axis),d=sv(nv(o,a),i.axis);r.push([Math.min(h,d),Math.max(h,d)])}const l=function(e,t){e.sort((e,t)=>e[0]-t[0]||e[1]-t[1]);const i=[];for(const s of e){const e=i[i.length-1];!e||s[0]>e[1]+t?i.push([...s]):e[1]=Math.max(e[1],s[1])}return i}(r,s).find(([e,t])=>e<=s&&t>=-s);if(!l)return null;const[c,h]=l,d=ov(a,i.axis,-i.half),p=ov(a,i.axis,i.half),u=Math.max(0,-i.half-c),_=Math.max(0,h-i.half),m=u>0?ov(a,i.axis,c):d,g=_>0?ov(a,i.axis,h):p,f=sv(o,i.normal)>=0?1:-1,v=(t,s,n)=>({from:t,to:s,label:[(t[0]+s[0])/2,(t[1]+s[1])/2],axis:i.axis,labelNormal:o,distance:n,roomId:e.roomId,roomSide:f,source:"room-face"});return{left:v(d,m,u),right:v(p,g,_)}}function dv(e){const t=ov(e.center,e.axis,-e.half),i=ov(e.center,e.axis,e.half),s=ov(e.center,e.axis,Math.min(-e.half,e.targetLo)),n=ov(e.center,e.axis,Math.max(e.half,e.targetHi)),o=(t,i)=>({from:t,to:i,label:[(t[0]+i[0])/2,(t[1]+i[1])/2],axis:e.axis,labelNormal:e.normal,distance:Math.hypot(i[0]-t[0],i[1]-t[1]),source:"host-end"});return[o(t,s),o(i,n)]}function pv(e,t,i,s){const n=nv(i,t),o=sv(n,n);if(!(o>s*s))return Math.hypot(...nv(e,t))<=s;const a=sv(nv(e,t),n)/o;if(a<-s||a>1+s)return!1;const r=ov(t,n,Math.max(0,Math.min(1,a)));return Math.hypot(...nv(e,r))<=s}function uv(e,t,i){let s=!1;for(let n=0,o=t.length-1;ne[1]!=c>e[1]&&e[0]<(l-a)*(e[1]-r)/(c-r)+a&&(s=!s)}return s}function _v(e,t,i,s,n,o){const a=nv(s,i),r=iv(t,a);if(Math.abs(r)<=o)return null;const l=nv(i,e),c=iv(l,a)/r,h=iv(l,t)/r;return c<-o||c>n+o||h<-o||h>1+o?null:Math.max(0,Math.min(n,c))}function mv(e,t,i){const s=dv(i),n="partition"===e.host?.kind?e.host.id:void 0;return[{base:s[0],direction:[-i.axis[0],-i.axis[1]]},{base:s[1],direction:i.axis}].map(({base:e,direction:i})=>{const s=e.distance,o=function(e,t,i,s,n,o){let a=null;for(const r of s)if(!n||r.partitionId!==n){if(uv(e,r.body,o))return 0;for(let s=0;ssv(nv(e,n),i));return{center:n,axis:i,normal:s,half:Math.max(0,e.renderedLength)/2,targetLo:Math.min(...o),targetHi:Math.max(...o)}}(e),s=t.rooms.flatMap(e=>{const s=function(e,t,i){const{profile:s}=e;let n=null;for(let e=0;e0))continue;const o=s.poly[e],a=s.poly[(e+1)%s.poly.length],r=nv(a,o),l=Math.hypot(r[0],r[1]);if(!(l>i&&av(t.axis,[r[0]/l,r[1]/l],1e-6)))continue;if(rv(o,t.center,t.axis)>i||rv(a,t.center,t.axis)>i)continue;const c=sv(nv(o,t.center),t.axis),h=sv(nv(a,t.center),t.axis),d=Math.min(c,h),p=Math.max(c,h);if(d>-t.half+i||p0?d:p<0?-p:0);(!n||u2)return dv(i);if(s.length){const e=s.map(({room:e,edge:s})=>hv(e,s,i,t.epsilon));if(e.some(e=>!e))return dv(i);const n=e;return[...n.map(e=>e.left),...n.map(e=>e.right)]}return mv(e,t,i)}function fv(e){return!(!e.canEdit||e.kiosk||e.fixedFloor)&&("view"!==e.mode&&("mouse"===e.pointerType&&e.spaceCount>1))}const vv="1.68.1",bv={badge:"display.badge",icon_ripple:"display.icon_ripple",value:"display.value",static_icon:"display.static_icon"},yv={badge:"marker.display_hint_badge",icon_ripple:"marker.display_hint_icon_ripple",value:"marker.display_hint_value",static_icon:"marker.display_hint_static_icon"},wv=2e3,kv=1e3,xv=1500,$v=e=>{const t=String(e??"").trim().replace(",",".");if(!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(t))return null;const i=Number(t);return Number.isFinite(i)?i:null},Sv=(e,t)=>{if(!e.has(t))return{hit:!1};const i=e.get(t);return e.delete(t),e.set(t,i),{hit:!0,value:i}},Mv=(e,t,i,s)=>{for(e.delete(t),e.set(t,i);e.size>s;){const t=e.keys().next().value;if(void 0===t)break;e.delete(t)}},Cv=new Map;let Dv=0;const Pv=e=>`${window.innerWidth}x${window.innerHeight}|${location.pathname}|${JSON.stringify(e??{})}`;let Tv=1e4;const Iv="houseplan_card_layout_v1",Rv="houseplan_card_cfg_v1",Av="houseplan_card_zoom_v1",zv="houseplan_card_nav_v1",Fv="houseplan_card_kiosk_v1",Nv="houseplan_card_view_v1",Ev="hp-dialog, hp-help, hp-color-opacity, hp-device-preview",Ov=1e3,Hv=[[0,1],[45,.88],[70,.62],[86,.32],[100,0]],Lv=(e,t)=>{const i=Math.min(e.x,t.x),s=Math.min(e.y,t.y);return{x:i,y:s,w:Math.max(e.x+e.w,t.x+t.w)-i,h:Math.max(e.y+e.h,t.y+t.h)-s}},Bv=new Set(["select","draw","column","merge","split","resize","opening","wallthick","delroom"]),jv=2e3,qv=(e,t,i)=>{const s=new Event(t,{bubbles:!0,composed:!0});s.detail=i??{},e.dispatchEvent(s)},Wv=e=>{history.pushState(null,"",e),qv(window,"location-changed",{replace:!1})},Uv=(e,t)=>{let i,s=null;const n=(...n)=>{clearTimeout(i),s=n,i=window.setTimeout(()=>{i=void 0;const t=s;s=null,t&&e(...t)},t)};return n.flush=()=>{if(void 0===i)return;clearTimeout(i),i=void 0;const t=s;s=null,t&&e(...t)},n.cancel=()=>{clearTimeout(i),i=void 0,s=null},n.pending=()=>void 0!==i,n},Gv=e=>{try{e.target?.setPointerCapture?.(e.pointerId)}catch{}};class Vv extends he{constructor(){super(...arguments),this._space="f1",this._layout={},this._serverStorage=!1,this._loadOk=!1,this._serverCanWrite=null,this._loading=!1,this._loadTries=0,this._serverCfg=null,this._cfgRev=0,this._cfgContentFingerprint="",this._unsubCfg=null,this._unsubLayout=null,this._unsubVirtual=null,this._liveSyncAttempt=null,this._liveSyncGeneration=0,this._liveSyncConnection=null,this._layoutRev=0,this._layoutContentFingerprint="",this._virtualLights=mn(null),this._canOptimizeUndo=!1,this._undoKind=null,this._devices=[],this._regSignature="",this._defPos={},this._newSyncKey="",this._tip=null,this._hoverRoom=null,this._pointerModality=new vm(this,()=>this._syncPointerHoverTargets()),this._devicePressAnimations=new Map,this._selId=null,this._toast="",this._mode="view",this._pendingNavMode=null,this._decorTool="select",this._decorStyle={...Br},this._decorDraft=null,this._decorMove=null,this._decorSel=null,this._decorEraseConfirm=null,this._decorTextDialog=null,this._decorShapeDialog=null,this._backdropDialog=null,this._decorTextSelection={start:0,end:0},this._furnPalette=null,this._dtBox=null,this._dtDrag=null,this._bdDrag=null,this._slide="",this._reducedMotion=!1,this._onMotionChange=e=>{this._reducedMotion=e.matches,this._cancelDevicePressFeedback(),e.matches&&this._modeTransitionPreparing?this._modeTransitionForceAtomic=!0:e.matches&&this._modeTransition.active&&this._cancelModeTransition(!0),this.requestUpdate()},this._editorChromeMode="plan",this._modeTransitionVisual=null,this._modeTransitionPreparing=!1,this._modeTransitionForceAtomic=!1,this._modeTransitionRequest=0,this._modeTransitionTargetZoom=1,this._modeTransitionEditorCamera=null,this._modeTransition=new pf({frame:e=>this._applyModeTransitionFrame(e),settled:e=>this._settleModeTransition(e)}),this._editorSecondary=new $m({root:()=>this.renderRoot,requestUpdate:()=>this.requestUpdate(),updateComplete:()=>this.updateComplete,clearTip:()=>{this._tip=null}}),this._editorSecondaryCopy={groupActive:(e,t)=>this._t("editor.group_active",{group:e,item:t}),openGroup:e=>this._t("editor.open_group",{group:e}),disabledAction:(e,t)=>this._t("editor.disabled_action",{action:e,reason:t})},this._tool="draw",this._geometryHistory=new Zg(50),this._wallDialog=null,this._drawWallField=null,this._activeDraftId=null,this._resumeDraftBySpace={},this._physicalSel=null,this._physicalDialog=null,this._partitionDeleteDialog=null,this._roomDeleteDialog=null,this._physicalDrag=null,this._physicalRotate=null,this._physicalLastTap=null,this._physicalPickCycle=null,this._wallUnionCacheValue=null,this._wallUnionPool=new Map,this._isoGeometryCache=new Map,this._isoFallback=new Set,this._openingTunnelCache=null,this._openingWallIndexCache=new Map,this._openingPlacementIntervalsCache=null,this._openingDimensionContextCache=null,this._planSnapGeometryCache=null,this._planStructuralGeometryCache=null,this._hiddenWallDiagnosticCache=null,this._physicalBodiesCache=null,this._lightPhysicalBodiesCache=null,this._cleanFloorCache=new Map,this._innerContourCache=new Map,this._glowClipCache=new Map,this._glowGeometryWarnings=new Set,this._lightBarrierCache=null,this._lightBarrierPool=new Map,this._glowFeatherUnits=null,this._glowRenderedSources=new Map,this._glowLastAppearance=new Map,this._glowEnteringSources=new Set,this._glowEnterRafs=new Map,this._glowFadeTimers=new Map,this._glowFeatherSuspendUntil=0,this._glowFeatherResumeTimer=0,this._glowSourceSeq=0,this._glowScreenBlend=!1,this._duplicateColumnId=null,this._duplicateColumnTimer=0,this._resize=new pr,this._rszLimitViolation=null,this._path=[],this._cursorPt=null,this._planSnapHover=null,this._mergeSel=null,this._openingPreset=null,this._openingRebindId=null,this._openingPresetRevision=0,this._openingHoverCandidate=null,this._openingJambBlockCm=null,this._openingDialog=null,this._openingInfo=null,this._opDrag=null,this._opMeasure=null,this._mergeDialog=null,this._splitSel=null,this._pendingSplit=null,this._wallFaceBatch=null,this._wallRepairDiagnostic=null,this._wallFaceGraphCache=[],this._areaSel="",this._nameSel="",this._roomDialog=!1,this._roomEditId=null,this._roomFill="",this._roomCustomFill=null,this._roomTempSrc="",this._roomHumSrc="",this._roomSrcOpen=null,this._roomSrcFilter="",this._roomNameScale=1,this._roomLabelScale=1,this._zoom=1,this._view=null,this._zoomBySpace={},this._viewModeSnap=null,this._pointers=new Map,this._panStart=null,this._panLock=null,this._pinchStart=null,this._suppressClick=!1,this._touchContacts=new Map,this._touchSequenceMultitouch=!1,this._touchClickBlockUntil=0,this._connectedPath="",this._routeDepartureHandled=!1,this._onLocationChanged=()=>{this._connectedPath&&location.pathname!==this._connectedPath?this._leaveCardRoute():location.pathname===this._connectedPath&&(this._routeDepartureHandled=!1)},this._touchGestureGuard={capture:!0,handleEvent:e=>this._guardTouchGesture(e)},this._hdrH=118,this._booting=!0,this._bootFading=!1,this._bootSettling=!1,this._bootSettleRaf=0,this._bootLastH=-1,this._bootStart=0,this._bootLastChange=0,this._bootSoft=!1,this._tapConfirm=null,this._onboardingShown=!1,this._rulesDialog=null,this._preflightClipboardFallback=null,this._haIntegrationVersion=null,this._alignDialog=null,this._settingsDialog=null,this._backupExportDialog=null,this._backupImportDialog=null,this._sunRaysCache=null,this._dayCycleTimer=0,this._dayCycleClockKey="",this._compassDrag=!1,this._importDialog=null,this._importQueue=[],this._importTotal=0,this._rulesCompiledSrc="",this._infoCard=null,this._nativeMoreInfoEntity=null,this._deviceInbox=null,this._deviceInboxReturn=null,this._deviceInboxMemo=null,this._markerDialog=null,this._spaceDialog=null,this._keyHandler=e=>this._onKey(e),this._warmVp=null,this._warmVpArmed=!1,this._warmLongReturn=!1,this._warmRevivePending=!1,this._warmGen=++Dv,this._warmKey=null,this._warmSlot=null,this._hashApplied=!1,this._navApplied=!1,this._labs={active:Object.freeze([]),space:""},this._viewPreference={},this._renderProjection="flat",this._kioskScale={icon:1,font:1},this._kioskDialog=!1,this._activityRt=new Map,this._vacRt=new Map,this._vacViewKey="",this._vacLastView=null,this._vacRaf=0,this._vacSrvTrails={},this._vacJumpOnce=!1,this._continuity=this._newContinuityController(),this._continuityHistory=[],this._continuityEpoch=0,this._continuityDataReady=!0,this._continuityPaintToken=-1,this._continuityDisposed=!1,this._renderSnapshotAt=Date.now(),this._hassSequence=0,this._visibleDeviceSnapshot=null,this._candidateDeviceSnapshot=null,this._stagedDeviceSnapshotToken=-1,this._capturedSnapshotSequence=-1,this._capturedSnapshotDevices=null,this._capturedSnapshotLayout=null,this._capturedSnapshotActivity="",this._capturedSnapshotConfigEpoch=-1,this._capturedSnapshotVirtual="",this._lastValidStageSize=null,this._pendingRefitSize=null,this._refitRaf=0,this._pageVisibility=e=>{if(this._continuity.visibility(e),this._dayCycleVisibility(e),"hidden"===e.kind)return this._clearTransientHover(!0),this._cancelDevicePressFeedback(),void(this._modeTransitionPreparing?this._modeTransitionForceAtomic=!0:this._cancelModeTransition(!0));if(this._vacJumpOnce=!0,!e.long){const e=Date.now();let t=!1;for(const i of this._activityRt.values())!i.flashKind||(i.expiresAt||i.flashTs+um)>e||(i.flashTs=0,i.flashKind=null,i.expiresAt=0,t=!0);return void(t&&this.requestUpdate())}Date.now()-this._renderSnapshotAt>1e3&&this._continuity.note("device-snapshot-stale"),this._continuityDataReady=!1,this._continuityPaintToken=-1,this._resumeSettling=!0,this._loading?this.requestUpdate():this._loadFromServer()},this._resumeSettling=!1,this._viewportInvalidAt=0,this._vacFit=null,this._vacAllCamerasFor=null,this._vacAllCameraCache=null,this._vacCalConfirm=null,this._kioskDots=!1,this._cyclePausedUntil=0,this._swipeStart=null,this._tabDrag=null,this._tabDragRelease=null,this._tabSuppressClick=!1,this._tabOrderWarned=!1,this._lastTap=0,this._onLabsSnapshot=e=>{const t=this._effectiveProjection();this._labs=e;const i=this._effectiveProjection();this._convertProjectionView(t,i),this.requestUpdate()},this._onHashChange=()=>{if(this._hasFixedFloor)return;const e=this._hashSpace();if(e&&this._model.find(t=>t.id===e)&&e!==this._space){if(this._wallFaceBatch&&this._roomDialogCancel(),"plan"===this._mode&&"draw"===this._tool&&!this._finishWallChain())return;this._activeDraftId&&(this._resumeDraftBySpace[this._space]=this._activeDraftId),this._commitSpace(e),this._selId=null,this._path=[],this._clearPlanSnapHover(),this._clearOpeningPlacement(!0),this._tool="draw",this._activeDraftId=null,this._draftSegmentCms=[],this._closingWallCm=null,"plan"===this._mode&&"draw"===this._tool&&this._resumeLastDraft(),this._restoreZoom(),this.requestUpdate()}},this._drag=null,this._rlResize=null,this._holdFired=!1,this._retryContinuity=()=>{this._continuityDataReady=!1,this._continuityPaintToken=-1,this._continuity.retry(this._continuity.recoveryReason||"plan"),this._loading||this._loadFromServer()},this._cfgEpoch=0,this._modelCache=null,this._emptySpaceStateActive=!1,this._decorSnapCache=null,this._markerPreviewMemo=null,this._markerPreviewDevicesMemo=null,this._showHidden=!1,this._connHooked=null,this._connectionWasLost=!1,this._haRegistryConnection=null,this._haRegistryRev=-1,this._haBindingCacheKey="",this._planHassMemo=null,this._onHaRegistryUpdate=()=>{const e=An(this.hass);e.revision===this._haRegistryRev&&this._devices.length||(this._haRegistryRev=e.revision,this._planHassMemo=null,this._regSignature="",this._maybeRebuildDevices(),this.requestUpdate())},this._onConnReady=()=>{if(this._loadTries=0,clearTimeout(this._loadRetryTimer),this._loadRetryTimer=void 0,function(e){const t=Cn(e);t&&(Tn(e,t),In(e,t))}(this.hass),this._saveConfigDebounced.pending()&&this._saveConfigDebounced.flush(),this._cfgWriting)return clearTimeout(this._reloadRetry),void(this._reloadRetry=window.setTimeout(this._onConnReady,400));!this._connectionWasLost&&this._continuity.hasCompleteFrame?this._beginContinuityCandidate("connection-ready",!1,"plan"):(this._continuityDataReady=!1,this._continuityPaintToken=-1),this._loading||this._loadFromServer()},this._onConnLost=()=>{this._booting&&!this._continuity.hasCompleteFrame||(this._connectionWasLost=!0,this._continuityDataReady=!1,this._continuityPaintToken=-1,this._continuity.connectionLost())},this._signer=new Qd(()=>this.requestUpdate()),this._dirtyPos=new Set,this._sentPos=new Map,this._persistLayout=Uv(()=>{if(this._serverStorage){const e=[...this._dirtyPos];this._dirtyPos.clear();for(const t of e){const e=ml(this._layout[t]);e&&(this._layout={...this._layout,[t]:e},this._sentPos.set(t,e),this.hass.callWS({type:"houseplan/layout/update",device_id:t,pos:e}).then(e=>this._noteLayoutRev(e)).catch(e=>this._showToast(this._t("toast.pos_save_failed",{err:this._errText(e)}))).finally(()=>{this._sentPos.get(t)===e&&this._sentPos.delete(t)}))}this._cacheSnapshot()}else this._layout=gl(il(this._layout)),localStorage.setItem(Iv,JSON.stringify(this._layout))},600),this._frame=null,this._showFar=!1,this._writesPending=0,this._writeChain=Promise.resolve(),this._pendingPhysicalWrites=new Map,this._junctionBaselineCache=new WeakMap,this._undoGeometry=()=>{if(this._physicalDrag||this._physicalRotate)return void this._cancelPhysicalGesture();if(this._decorDraft)return this._decorDraft=null,void this.requestUpdate();if(this._decorMove||this._dtDrag||this._bdDrag)return void this._cancelDecorGesture();if(this._resize.dragging)return void this._rszCancelDrag();const e=this._geometryHistory.undo();e&&(this._applyGeometryState(e.before,!0)?this._showToast(this._t("history.undone",{name:e.name})):this._geometryHistory.clear())},this._redoGeometry=()=>{if(this._physicalDrag||this._physicalRotate)return void this._cancelPhysicalGesture();if(this._decorDraft)return this._decorDraft=null,void this.requestUpdate();if(this._decorMove||this._dtDrag||this._bdDrag)return void this._cancelDecorGesture();if(this._resize.dragging)return void this._rszCancelDrag();const e=this._geometryHistory.redo();e&&(this._applyGeometryState(e.after,!0)?this._showToast(this._t("history.redone",{name:e.name})):this._geometryHistory.clear())},this._saveConfigDebounced=Uv(()=>{this._serverCfg&&this._writeConfig().catch(e=>{const t=!0===e?.physicalGeometryRolledBack;"geometry-unsafe"!==e?.code&&("wall_model_client_outdated"===e?.code?this._showToast(this._t("toast.wall_model_client_outdated")):"conflict"===e?.code?(this._showToast(this._t("toast.conflict")),t||(this._cancelPath(),this._reloadConfigOnly(!0))):this._showToast(this._t("toast.cfg_save_failed",{err:this._errText(e)})),t&&this._reloadRejectedPhysicalWrite())})},500),this._draftSegmentCms=[],this._closingWallCm=null,this._savePhysicalDialog=()=>{const e=this._physicalDialog,t=this._curSpaceCfg,i=this._spaceModel();if(!e||!t||!i)return;const s=$v(e.cm);if(null==s)return void this._showPhysicalRange("column"===e.kind?ih:100,"column"===e.kind?1:0);const n=this._imperial?2.54*s:s,o="column"===e.kind?ih:100,a="column"===e.kind?1:0;if(!Number.isFinite(n)||no)return void this._showPhysicalRange(o,a);if("column"===e.kind){const t=i.wall_columns.find(t=>t.id===e.id);if(!t)return;const s=$v(e.angle||"0");if("circle"!==e.shape&&(null==s||s<0||s>=90))return void this._showToast(this._t("toast.physical_angle"));const o="circle"===e.shape?{id:e.id,shape:"circle",center:t.center,cm:n}:{id:e.id,shape:"square",center:t.center,cm:n,angle:s};if(i.wall_columns.some(t=>t.id!==e.id&&Sh(t,o,.02*this._gridPitch)))return void this._showToast(this._t("toast.column_duplicate"))}const r=this._geometrySnapshot();if("partition"===e.kind){const i=(t.partitions||[]).find(t=>t.id===e.id);if(i&&0===n&&Z_(t.openings,{kind:"partition",id:e.id}))return void this._showToast(this._t("toast.zero_wall_opening_conflict"));i&&(i.cm=n)}else if("column"===e.kind){const i=(t.wall_columns||[]).find(t=>t.id===e.id);i&&(i.cm=nh(n),i.shape="circle"===e.shape?"circle":"square","square"===i.shape?i.angle=$v(e.angle||"0"):delete i.angle)}else{const i=(t.room_drafts||[]).find(t=>t.id===e.id);i?.segments?.[e.segment||0]&&(i.segments[e.segment||0].cm=n)}const l=this._commitPhysicalGeometry(this._t("history.physical_edit"),r);this._physicalDialog=null,l||this.requestUpdate()},this._deletePhysicalSelection=()=>{const e=this._physicalSel,t=this._curSpaceCfg;if(!e||!t)return;if("draft"===e.kind)return void this._deleteDraftWhole();const i=this._geometrySnapshot();if("partition"===e.kind){const i=(t.openings||[]).filter(t=>"partition"===t.host?.kind&&t.host.id===e.id).sort((e,t)=>(e.host?.t||0)-(t.host?.t||0));if(i.length)return void(this._partitionDeleteDialog={id:e.id,openings:i.map(e=>JSON.parse(JSON.stringify(e)))})}const s="partition"===e.kind?"partitions":"column"===e.kind?"wall_columns":"room_drafts";t[s]=(t[s]||[]).filter(t=>t.id!==e.id),t[s].length||delete t[s],this._activeDraftId===e.id&&this._cancelPath(),this._physicalSel=null,this._physicalDialog=null,this._commitPhysicalGeometry(this._t("history.physical_delete"),i)},this._confirmPartitionDelete=()=>{const e=this._partitionDeleteDialog,t=this._curSpaceCfg;if(!e||!t)return;const i=this._geometrySnapshot();t.partitions=(t.partitions||[]).filter(t=>t.id!==e.id),t.partitions.length||delete t.partitions,t.openings=(t.openings||[]).filter(t=>"partition"!==t.host?.kind||t.host.id!==e.id),t.openings.length||delete t.openings,this._partitionDeleteDialog=null,this._physicalSel=null,this._physicalDialog=null,this._commitPhysicalGeometry(this._t("history.physical_delete"),i)},this._deleteDraftWhole=()=>{const e="draft"===this._physicalDialog?.kind?this._physicalDialog.id:"draft"===this._physicalSel?.kind?this._physicalSel.id:null,t=this._curSpaceCfg;if(!e||!t||!confirm(this._t("confirm.delete_draft")))return;const i=this._geometrySnapshot();t.room_drafts=(t.room_drafts||[]).filter(t=>t.id!==e),t.room_drafts.length||delete t.room_drafts,this._activeDraftId===e&&this._cancelPath(),this._physicalSel=null,this._physicalDialog=null,this._commitPhysicalGeometry(this._t("history.physical_delete"),i)},this._deleteDraftSegment=()=>{const e=this._physicalDialog,t=this._curSpaceCfg;if(!e||"draft"!==e.kind||!t)return;if(!confirm(this._t("confirm.delete_draft_segment")))return;const i=(t.room_drafts||[]).findIndex(t=>t.id===e.id);if(i<0)return;const s=t.room_drafts[i],n=Math.max(0,Math.min(s.segments.length-1,e.segment||0)),o=[],a=s.points.slice(0,n+1),r=s.points.slice(n+1);if(a.length>=2&&o.push({id:s.id,points:a,segments:s.segments.slice(0,n)}),r.length>=2&&o.push({id:o.length?`${s.id}-${Date.now().toString(36)}`:s.id,points:r,segments:s.segments.slice(n+1)}),2===o.length&&t.room_drafts.length>=200)return void this._showToast(this._t("toast.physical_limit"));const l=this._geometrySnapshot();t.room_drafts.splice(i,1,...o),t.room_drafts.length||delete t.room_drafts,this._activeDraftId===s.id&&this._cancelPath(),this._physicalDialog=null,this._physicalSel=o.length?{kind:"draft",id:o[0].id}:null,this._commitPhysicalGeometry(this._t("history.draft_segment_delete"),l)},this._confirmRoomDelete=e=>{const t=this._roomDeleteDialog,i=this._curSpaceCfg,s=this._spaceModel();if(!t||!i||!s)return;const n=s.rooms.find(e=>e.id===t.roomId);if(!n)return void(this._roomDeleteDialog=null);const o=this._openCuts(),a=n_(wc(s.rooms,this._spaceWalls,o,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov).filter(e=>e.roomId===n.id),s.partitions,this._openingsR.map(e=>({...e,x:e.rx,y:e.ry})),.02*this._gridPitch),r=new Set(a.materialize.filter(e=>!e.reusePartitionId).map(e=>e.interval.key)).size;if(e&&(i.partitions||[]).length+r>jv)return void this._showToast(this._t("toast.physical_limit"));const l=this._geometrySnapshot(),c=kc(s.rooms,this._spaceWalls,o,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov);if(e){i.partitions||=[];const e=new Map,t=new Map,s=e=>{const t=(i.wall_segments||[]).find(t=>{const i=[Number(t.a?.[0])*Ov,Number(t.a?.[1])*Ov],s=[Number(t.b?.[0])*Ov,Number(t.b?.[1])*Ov];return this._samePt(e.a,i)&&this._samePt(e.b,s)||this._samePt(e.a,s)&&this._samePt(e.b,i)});return"string"==typeof t?.id&&t.id?t.id:null},n=Date.now().toString(36);for(const o of a.materialize){let a=o.reusePartitionId?i.partitions.find(e=>e.id===o.reusePartitionId):t.get(o.interval.key);a||(a={id:s(o.interval)||`partition-room-${n}-${t.size}`,a:[o.interval.a[0]/Ov,o.interval.a[1]/Ov],b:[o.interval.b[0]/Ov,o.interval.b[1]/Ov],cm:o.interval.cm},i.partitions.push(a),t.set(o.interval.key,a)),e.set(o.interval.key,a)}for(const t of i.openings||[]){const i=a.openingIntervals.get(t.id),s=i?e.get(i):null,n=this._openingsR.find(e=>e.id===t.id);if(!s||!n)continue;const o={a:[s.a[0]*Ov,s.a[1]*Ov],b:[s.b[0]*Ov,s.b[1]*Ov]};t.host={kind:"partition",id:s.id,t:o_([n.rx,n.ry],o)}}}else if(a.removeOpeningIds.length){const e=new Set(a.removeOpeningIds);i.openings=(i.openings||[]).filter(t=>!e.has(t.id)),i.openings.length||delete i.openings}i.rooms=i.rooms.filter(e=>e.id!==n.id),this._cfgEpoch++;const h=this._normalizeWalls(c,this._openCuts());h.length?i.walls=h:delete i.walls,this._roomDeleteDialog=null,this._commitPhysicalGeometry(this._t(e?"history.delete_room_keep_walls":"history.delete_room_with_walls"),l),this._regSignature="",this._maybeRebuildDevices(),this.requestUpdate()},this._rebindPartitionOpening=()=>{const e=this._openingDialog;if(!e?.id)return;const t=e.id;this._activateMarkupTool("opening"),"opening"===this._tool&&(this._openingRebindId=t,this._openingPreset={type:e.type,lengthCm:e.lengthCm,flipH:e.flipH,flipV:e.flipV,revision:++this._openingPresetRevision},this._openingDialog=null,this._openingHoverCandidate=null)},this._keepClosedAsPartitions=()=>{if(this._wallFaceBatch)return void this._decideWallFace(!1);if(!this._contourClosed||this._pendingSplit||!this._curSpaceCfg)return;const e=this._curSpaceCfg,t=this._path.slice(0,-1);if((e.partitions||[]).length+t.length>jv)return void this._showToast(this._t("toast.physical_limit"));const i=this._geometrySnapshot(),s=Iu(t.length,[...this._draftSegmentCms,this._closingWallCm??void 0],this._drawWallCm,vl);e.partitions||=[];const n=Date.now().toString(36),o=this._activeDraftId?(e.room_drafts||[]).find(e=>e.id===this._activeDraftId):null,a=this._draftSegmentsForPath(this._path,o,s);for(let i=0;ie.id!==this._activeDraftId),e.room_drafts.length||delete e.room_drafts),this._commitPhysicalGeometry(this._t("history.contour_to_partitions"),i),this._roomDialog=!1,this._path=[],delete this._resumeDraftBySpace[this._space],this._activeDraftId=null,this._draftSegmentCms=[],this._closingWallCm=null},this._toggleServerPlans=async()=>{const e=this._spaceDialog;if(e)if(e.pickSaved)this._spaceDialog={...e,pickSaved:!1};else{this._spaceDialog={...e,pickSaved:!0,savedBusy:!0};try{const e=await this.hass.callWS({type:"houseplan/plans/list"}),t=this._spaceDialog;t&&(this._spaceDialog={...t,saved:e?.plans||[],savedBusy:!1})}catch(e){const t=this._spaceDialog;t&&(this._spaceDialog={...t,saved:[],savedBusy:!1}),this._showToast(this._t("toast.plans_list_failed",{err:this._errText(e)}))}}},this._aspectJob=null,this._dayCycleTick=()=>{if(!this.isConnected||"hidden"===this.ownerDocument.visibilityState)return;const e=this._dayCycleState();if(!e)return this._dayCycleTimer&&(clearInterval(this._dayCycleTimer),this._dayCycleTimer=0),void(this._dayCycleClockKey="");const t=Sr(e);t!==this._dayCycleClockKey&&(this._dayCycleClockKey=t,this.requestUpdate())},this._sunShown=!1,this._sunOut=!1,this._sunOutTimer=0,this._openSettingsDialog=()=>{if(!this._norm)return;const e=this._glowRadiusCm,t=this._imperial?Math.round(e/30.48*10)/10:Math.round(e)/100;this._settingsDialog={colors:JSON.parse(JSON.stringify(this._fillColors)),glowRadius:t,bgColor:hs(this._settings,{bgColor:null})||null,northDeg:zr(this._settings,{}),bgMode:Fr(this._settings,{}),sunRays:Nr(this._settings,{}),busy:!1}},this._reportedPreflightFingerprint=null,this._openAlignDialog=()=>this._previewAlignDialog(!1),this._toggleOptimizeLivePositions=()=>{const e=this._alignDialog;e&&!e.busy&&e.report.liveMissingPositions.length&&this._previewAlignDialog(!e.removeLiveMissingPositions)},this._optimizeUndoBusy=!1,this._openBackupExport=()=>{this._settingsDialog=null,this._backupExportDialog={kind:"full",planOnly:!1,busy:!1,error:""}},this._openRulesDialog=()=>{if(!this._norm)return;const e=this._settings.icon_rules,t=(e&&e.length?e:Je).map(e=>({...e}));this._rulesDialog={rules:t,test:"",busy:!1}},this._climateCache=null,this._gearPtCache=new WeakMap}get _canEdit(){return!!this._norm&&(!0===this._serverCanWrite||!1!==this._serverCanWrite&&!0===this.hass?.user?.is_admin)}get _kiosk(){return!!this._config?.kiosk}_showKioskDots(){this._kioskDots=!0,clearTimeout(this._kioskDotsTimer),this._kioskDotsTimer=window.setTimeout(()=>this._kioskDots=!1,2500)}get _modeTransitionBusy(){return this._modeTransitionPreparing||this._modeTransition.active}_cssColor(e,t){const i=String(e||"").trim();if(!i)return t;const s=document.createElement("span");s.style.cssText=`position:absolute;visibility:hidden;color:${i}`,this.renderRoot.append(s);const n=getComputedStyle(s).color||t;return s.remove(),n}_currentModeVisual(e=this._mode){const t=this._modeTransition.presented||this._modeTransitionVisual;if(t)return{...t,viewport:{...t.viewport,viewBox:{...t.viewport.viewBox}}};const i=this._stageEl;if(!i||i.clientWidth<=0||i.clientHeight<=0)return null;const s=this.renderRoot.querySelector(".editorchrome"),n=this._viewOr(this._baseVb()),o=this.renderRoot.querySelector(".hp-paper"),a=this.renderRoot.querySelector(".hp-backdrop"),r=this.renderRoot.querySelector(".zoomwrap"),l=r?getComputedStyle(r).filter:"",c=Number(/brightness\(([^)]+)\)/.exec(l)?.[1]);return{presentedMode:e,editorChromeHeight:"view"===e?0:s?.getBoundingClientRect().height||0,stageWidth:i.clientWidth,stageHeight:i.clientHeight,viewport:rf(n,i.clientWidth),stageColor:getComputedStyle(i).backgroundColor||"rgb(255, 255, 255)",paperColor:o?getComputedStyle(o).fill:"rgb(255, 255, 255)",sceneBrightness:Number.isFinite(c)?c:1,architectureOpacity:"decor"===e?.35:1,backdropOpacity:a&&Number(getComputedStyle(a).opacity)||1,viewWeight:"view"===e?1:0,editorWeight:"view"===e?0:1,toolbarContentOpacity:"view"===e?0:1}}_viewForModeTarget(e,t,i,s,n){const o=Ei(this._baseVb(),s/Math.max(1,n)),a=Math.min(Vv.ZOOM_MAX,Math.max(Vv.ZOOM_MIN,e)),r=o.w/a,l=o.h/a,c=t??o.x+o.w/2,h=i??o.y+o.h/2;return this._clampView({x:c-r/2,y:h-l/2,w:r,h:l},o)}_targetStageColor(e){return"view"!==e?"rgb(255, 255, 255)":this._cssColor(this._stageBg(this._spaceDisplayForRender()),this._cssColor("var(--ha-card-background, var(--card-background-color, #111))","rgb(17, 17, 17)"))}_targetPaperColor(e){return"view"===e&&this._spaceModel()?.bg?this._cssColor("var(--ha-card-background, var(--card-background-color, #111))","rgb(17, 17, 17)"):"rgb(255, 255, 255)"}_targetBrightness(e){return 1}_applyModeTransitionFrame(e){e.targetMode===this._mode&&(this._modeTransitionVisual=e.presented,this._view={...e.presented.viewport.viewBox},this.requestUpdate())}_settleModeTransition(e){if(e.targetMode!==this._mode)return;const t=this._modeTransitionRequest;this._view={...e.to.viewport.viewBox},this._zoom=this._modeTransitionTargetZoom,this._modeTransitionVisual=null,this._modeTransitionPreparing=!1,this._lastValidStageSize=[e.to.stageWidth,e.to.stageHeight],"view"===this._mode&&(this._saveZoom(),this._viewModeSnap=null,this._modeTransitionEditorCamera=null),this.requestUpdate(),this.updateComplete.then(()=>{if(!this.isConnected||t!==this._modeTransitionRequest||this._modeTransitionBusy||e.targetMode!==this._mode)return;const i=this._stageEl;if(i&&i.clientWidth>0&&i.clientHeight>0&&(Math.abs(i.clientWidth-e.to.stageWidth)>.5||Math.abs(i.clientHeight-e.to.stageHeight)>.5)){const e=this._view;this._lastValidStageSize=[i.clientWidth,i.clientHeight],this._applyView(this._zoom,e?e.x+e.w/2:void 0,e?e.y+e.h/2:void 0),this.requestUpdate()}const s=this.renderRoot.activeElement;(!s||!s.isConnected||!!s.closest?.(".editorchrome, .stage"))&&this.renderRoot.querySelector(".modetab.active")?.focus?.({preventScroll:!0})})}_cancelModeTransition(e=!0){const t=!!this._modeTransition.state;this._modeTransitionRequest++,this._modeTransitionPreparing=!1,this._modeTransitionForceAtomic=!1,this._modeTransition.cancel(e),e&&t||(this._modeTransitionVisual=null)}_adoptMode(e){this._cancelModeTransition(!1),this._mode=e,"view"!==e&&(this._editorChromeMode=e)}_commitViewModeAtomic(e,t,i,s){if(this._modeTransitionPreparing=!1,this._modeTransitionVisual=null,this._modeTransitionForceAtomic=!1,this._zoom=t,e){const n=this._stageEl?.clientWidth||e.stageWidth,o=Math.max(1,e.stageHeight+e.editorChromeHeight);this._view=this._viewForModeTarget(t,i,s,n,o),this._lastValidStageSize=[n,o]}else this._view=null;this._viewModeSnap=null,this._modeTransitionEditorCamera=null,this._saveZoom(),this.requestUpdate()}_prepareModeTransition(e,t,i,s,n,o){this.updateComplete.then(()=>{if(!this.isConnected||e!==this._modeTransitionRequest||this._mode!==i)return void(e===this._modeTransitionRequest&&(this._modeTransitionPreparing=!1,this._modeTransitionVisual=null,this._modeTransitionForceAtomic=!1,this.requestUpdate()));const a=this.renderRoot.querySelector(".editorchrome"),r=a?.querySelector(".editorchrome-inner"),l="view"===i?0:r?.scrollHeight||r?.getBoundingClientRect().height||0,c=Math.max(1,t.stageHeight+t.editorChromeHeight),h=Math.max(1,c-l),d=this._stageEl?.clientWidth||t.stageWidth;if(d<=0||h<=0)return this._modeTransitionPreparing=!1,this._modeTransitionVisual=null,this._applyView(s,n,o),void this.requestUpdate();const p=this._viewForModeTarget(s,n,o,d,h),u={presentedMode:i,editorChromeHeight:l,stageWidth:d,stageHeight:h,viewport:rf(p,d),stageColor:this._targetStageColor(i),paperColor:this._targetPaperColor(i),sceneBrightness:this._targetBrightness(i),architectureOpacity:"decor"===i?.35:1,backdropOpacity:"decor"===i&&"backdrop"!==this._decorTool?.5:1,viewWeight:"view"===i?1:0,editorWeight:"view"===i?0:1,toolbarContentOpacity:"view"===i?0:1};this._modeTransitionPreparing=!1;const _=this._modeTransitionForceAtomic;this._modeTransitionForceAtomic=!1,this._modeTransition.start(t,u,i,this._reducedMotion||_?0:220)})}get _hasFixedFloor(){return!!this._config&&Object.prototype.hasOwnProperty.call(this._config,"floor")}_fixedFloorState(e=this._model,t=this._loadOk){const i=this._config?.floor,s=function(e){if(!e.hasFloor)return{kind:"absent"};const t=e.floor;if("string"==typeof t)return t.length?e.spaceIds.includes(t)?{kind:"valid",id:t,source:"id"}:{kind:"invalid",reason:"unknown-id",value:t}:{kind:"invalid",reason:"empty-id",value:t};if("number"==typeof t){if(!Number.isFinite(t))return{kind:"invalid",reason:"non-finite-index",value:t};if(!Number.isInteger(t))return{kind:"invalid",reason:"fractional-index",value:t};if(t<0)return{kind:"invalid",reason:"negative-index",value:t};const i=e.spaceIds[t];return void 0===i?{kind:"invalid",reason:"out-of-range-index",value:t}:{kind:"valid",id:i,source:"index"}}return{kind:"invalid",reason:"invalid-type",value:t}}({spaceIds:e.map(e=>e.id),hasFloor:this._hasFixedFloor,floor:i});return!this._hasFixedFloor||t?s:"number"==typeof i?"valid"===s.kind||"invalid"===s.kind&&"out-of-range-index"===s.reason?{kind:"pending",value:i}:s:"valid"===s.kind||"invalid"===s.kind&&"unknown-id"!==s.reason?s:{kind:"pending",value:i}}_canCommitSpace(e,t=!1){if(t||!this._hasFixedFloor)return!0;const i=this._fixedFloorState();return"valid"===i.kind&&i.id===e}_commitSpace(e,t=!1){return!!this._canCommitSpace(e,t)&&(e!==this._space&&(this._clearTransientHover(!0),this._cancelDevicePressFeedback()),this._space=e,!0)}_slideTo(e,t){if(e===this._space)return!0;if(!this._canCommitSpace(e))return!1;if(this._wallFaceBatch&&this._roomDialogCancel(),"plan"===this._mode&&"draw"===this._tool&&!this._finishWallChain())return!1;this._cancelModeTransition(!0);const i=window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches;return this._activeDraftId&&(this._resumeDraftBySpace[this._space]=this._activeDraftId),this._commitSpace(e),this._path=[],this._clearPlanSnapHover(),this._clearOpeningPlacement(!0),this._tool="draw",this._activeDraftId=null,this._draftSegmentCms=[],this._closingWallCm=null,this._selId=null,this._physicalSel=null,this._editorSecondary.closeForNavigation(),this._physicalDialog=null,this._physicalDrag=null,"plan"===this._mode&&"draw"===this._tool&&this._resumeLastDraft(),this._restoreZoom(),i||(this._slide=t,clearTimeout(this._slideTimer),this._slideTimer=window.setTimeout(()=>{this._slideTimer=void 0,this._slide="",this.requestUpdate()},190),this.requestUpdate()),!0}get _canReorderTabs(){return fv({canEdit:this._canEdit,kiosk:this._kiosk,mode:this._mode,pointerType:"mouse",spaceCount:this._model.length,fixedFloor:this._hasFixedFloor})}_tabPointerDown(e,t){fv({canEdit:this._canEdit,kiosk:this._kiosk,mode:this._mode,pointerType:e.pointerType,spaceCount:this._model.length,fixedFloor:this._hasFixedFloor})&&(Gv(e),this._tabDragRelease=e=>this._tabPointerUp(e),window.addEventListener("pointerup",this._tabDragRelease),window.addEventListener("pointercancel",this._tabDragRelease),this._tabDrag={id:t,pointerId:e.pointerId,x:e.clientX,y:e.clientY,moved:!1,targetId:null,placement:null})}_tabDropTargetAt(e,t,i){const s=this._model.map(e=>e.id),n=s.indexOf(i);if(n<0)return null;const o=this.renderRoot.querySelectorAll('[data-hp="space-tab"]');for(const a of o){const o=a.dataset.id||"";if(!o||o===i)continue;const r=a.getBoundingClientRect();if(er.right||tr.bottom)continue;const l=s.indexOf(o);return l<0?null:{targetId:o,placement:l=4)))return;var i,s;const n=this._tabDropTargetAt(e.clientX,e.clientY,t.id);t.moved&&t.targetId===n?.targetId&&t.placement===n?.placement||(this._tabDrag={...t,moved:!0,targetId:n?.targetId||null,placement:n?.placement||null})}_tabPointerUp(e){const t=this._tabDrag;if(t&&t.pointerId!==e.pointerId)return;const i="pointerup"===e.type&&t?.moved?this._tabDropTargetAt(e.clientX,e.clientY,t.id):null;"pointerup"===e.type&&t?.moved&&this._suppressNextTabClick(),this._endTabDrag(),t?.moved&&i&&this._commitTabOrder(t.id,i.targetId)}_endTabDrag(){this._tabDrag=null,this._tabDragRelease&&(window.removeEventListener("pointerup",this._tabDragRelease),window.removeEventListener("pointercancel",this._tabDragRelease),this._tabDragRelease=null)}_tabClick(e){if(this._tabSuppressClick)return this._tabSuppressClick=!1,clearTimeout(this._tabSuppressClickTimer),void(this._tabSuppressClickTimer=void 0);this._pickSpace(e)}_suppressNextTabClick(){this._tabSuppressClick=!0,clearTimeout(this._tabSuppressClickTimer),this._tabSuppressClickTimer=window.setTimeout(()=>{this._tabSuppressClick=!1,this._tabSuppressClickTimer=void 0},0)}_commitTabOrder(e,t){const i=this._serverCfg;if(!i||!this._canReorderTabs)return;const s=this._model.map(e=>e.id),n=function(e,t,i){const s=e.indexOf(t),n=e.indexOf(i);if(s<0||n<0||s===n)return e;const o=e.slice();return o.splice(s,1),o.splice(n,0,t),o}(s,e,t);if(n===s)return;const o=new Map(this._devices.map(e=>[String(e.id),String(e.area||"")])),a=function(e,t,i,s=()=>""){if(!i)return[];const n=[];for(const o of e){if(!o||!0===o.removed)continue;const e="string"==typeof o.id?o.id:"";if(!e)continue;if("string"==typeof o.space&&o.space)continue;const a=("string"==typeof o.area?o.area:"")||s(e)||"";a&&t[a]||n.push({id:e,space:i})}return n}(i.markers||[],Object.fromEntries(Object.entries(this._areaToSpace).map(([e,t])=>[e,t.space])),s[0]||"",e=>o.get(e)||"");if(a.length){const e=new Map(a.map(e=>[e.id,e.space]));for(const t of i.markers||[]){const i=e.get(String(t.id));i&&(t.space=i)}}i.spaces=function(e,t){const i=new Map(t.map((e,t)=>[e,t]));return e.map((e,t)=>({space:e,index:t})).sort((e,t)=>(i.get(String(e.space?.id))??Number.MAX_SAFE_INTEGER)-(i.get(String(t.space?.id))??Number.MAX_SAFE_INTEGER)||e.index-t.index).map(e=>e.space)}(i.spaces||[],n),this._saveConfig(),this._tabOrderWarned||(this._tabOrderWarned=!0,this._showToast(this._t("toast.space_order_changed")))}_pickSpace(e){if(this._endTabDrag(),e===this._space)return;const t=this._model.map(e=>e.id),i=t.indexOf(this._space),s=t.indexOf(e);this._navApplied=!0,this._showFar=!1,this._frame=null,this._slideTo(e,i>=0&&s0&&Date.now()>=this._cyclePausedUntil&&this._model.length>1&&this._zoom<=1.001){const e=this._model.map(e=>e.id),t=e.indexOf(this._space);this._slideTo(e[(t+1)%e.length],"left"),this._showKioskDots()}}_syncCycleTimer(){clearInterval(this._cycleTimer),this._cycleTimer=void 0,this.isConnected&&!this._hasFixedFloor&&this._config?.kiosk&&Number(this._config.cycle)>0&&(this._cycleTimer=window.setInterval(()=>this._cycleTick(),1e3*Number(this._config.cycle)))}get _editing(){return"plan"===this._mode||"devices"===this._mode||"decor"===this._mode}get _markup(){return"plan"===this._mode}get _wallUnionCache(){return this._wallUnionCacheValue}set _wallUnionCache(e){this._wallUnionCacheValue=e,null===e&&this._wallUnionPool.clear()}_newContinuityController(){return new Yd(()=>{this._resumeSettling="steady"!==this._continuity.state,this._continuityEpoch++,this.isConnected&&this.requestUpdate()})}get _labsIso(){return this._labs.active.includes("iso")}get _desiredProjection(){return"view"===this._mode&&this._labsIso&&"iso"===this._viewPreference[this._space]?"iso":"flat"}_saveViewPreference(){try{localStorage.setItem(Nv,JSON.stringify(this._viewPreference))}catch{}}_logicalViewCenter(e){const t=this._view;if(!t)return null;const i=[t.x+t.w/2,t.y+t.h/2],s="iso"===e?zf(i):i;return{x:s[0],y:s[1]}}_convertProjectionView(e,t){if(e===t)return;const i=this._logicalViewCenter(e);this._view=null;const s=i?"iso"===t?Af([i.x,i.y],0):[i.x,i.y]:null;this._applyView(this._zoom,s?.[0],s?.[1]),this._warmPatch({vp:this._warmViewportState()}),this.requestUpdate()}_setProjection(e){if(!this._labsIso||"view"!==this._mode)return;const t=this._effectiveProjection();if(this._viewPreference={...this._viewPreference,[this._space]:e},"iso"===e){const e=this._isoSceneKey();e&&this._isoFallback.delete(e)}this._saveViewPreference();const i=this._effectiveProjection();this._convertProjectionView(t,i)}_hashSpace(){return yf(window.location.hash||"")}connectedCallback(){this._connectedPath=location.pathname,this._routeDepartureHandled=!1,window.addEventListener("location-changed",this._onLocationChanged),window.addEventListener("popstate",this._onLocationChanged),this._continuityDisposed&&(this._continuity=this._newContinuityController(),this._continuityDisposed=!1,this._continuityPaintToken=-1);const e=(t=this.ownerDocument,Qg.get(t)?.resolved);var t;void 0!==e&&(this._glowScreenBlend=e),this._continuityUnsub?.(),this._continuityUnsub=Vd(this.ownerDocument,this._pageVisibility),super.connectedCallback(),this._pointerModality.connect(this.ownerDocument.defaultView);const i=this.ownerDocument.defaultView?.MutationObserver;i&&(this._pointerHoverObserver=new i(e=>{for(const t of e)for(const e of t.addedNodes)this._syncPointerHoverSubtree(e)}),this._pointerHoverObserver.observe(this.renderRoot,{childList:!0,subtree:!0})),this._motionMedia=window.matchMedia?.("(prefers-reduced-motion: reduce)"),this._reducedMotion=!!this._motionMedia?.matches,this._motionMedia?.addEventListener?.("change",this._onMotionChange),function(e){const t=Qg.get(e);if(t)return t.promise;const i={promise:Promise.resolve(!1)};return i.promise=tf(e).catch(()=>!1).then(e=>(i.resolved=e,e)),Qg.set(e,i),i.promise}(this.ownerDocument).then(e=>{e!==this._glowScreenBlend&&(this._glowScreenBlend=e,this.isConnected&&this.requestUpdate())}),this.hass&&this._ensureHaRegistryAuthority(),window.addEventListener("keydown",this._keyHandler),this._signer.start(()=>this.hass,()=>Rs(this._serverCfg)),this._syncCycleTimer(),window.addEventListener("hashchange",this._onHashChange),this._labsUnsub?.(),this._labsUnsub=function(e,t){Cf.add(t);const i=Tf(e);return"undefined"==typeof window&&t(i),()=>{Cf.delete(t)}}(vv,this._onLabsSnapshot),this._booting?this._bootWatch():this._bootFading&&(clearTimeout(this._bootTimer),this._bootTimer=window.setTimeout(()=>{this._bootFading=!1},220)),this._bootSoft&&(clearTimeout(this._bootSoftTimer),this._bootSoftTimer=window.setTimeout(()=>{this._bootSoft=!1},xv)),!this._loadOk&&this._serverCfg&&this.hass&&this._scheduleLoadRetry(),!this._warmSlot&&this._config&&this._warmAdopt(),this._loadOk&&this._ensureLiveSyncSubscriptions(),this._warmVp&&!this._warmRevivePending&&void 0===this._warmReviveTimer&&(this._warmRevivePending=!0,this._warmReviveTimer=window.setTimeout(()=>this._warmReviveDialog(),0)),this._warmLongReturn&&this._beginResumeSettle(),this._warmLongReturn=!1,this.requestUpdate()}disconnectedCallback(){this._connectedPath&&location.pathname!==this._connectedPath&&this._leaveCardRoute(),window.removeEventListener("location-changed",this._onLocationChanged),window.removeEventListener("popstate",this._onLocationChanged),this._continuityUnsub?.(),this._continuityUnsub=void 0,this._motionMedia?.removeEventListener?.("change",this._onMotionChange),this._motionMedia=void 0,this._vacRaf&&(cancelAnimationFrame(this._vacRaf),this._vacRaf=0),this._refitRaf&&(cancelAnimationFrame(this._refitRaf),this._refitRaf=0),this._dayCycleTimer&&(clearInterval(this._dayCycleTimer),this._dayCycleTimer=0),this._dayCycleClockKey="",this._bootSettleRaf&&(cancelAnimationFrame(this._bootSettleRaf),this._bootSettleRaf=0),this._bootSettling=!1;for(const e of this._activityRt.values())clearTimeout(e.timer);window.removeEventListener("keydown",this._keyHandler),this._endTabDrag(),clearTimeout(this._tabSuppressClickTimer),this._tabSuppressClickTimer=void 0,this._tabSuppressClick=!1,clearInterval(this._cycleTimer),clearTimeout(this._kioskDotsTimer),clearTimeout(this._kioskHoldTimer),clearTimeout(this._reloadRetry),clearTimeout(this._loadRetryTimer),this._loadRetryTimer=void 0,this._connHooked?.removeEventListener?.("ready",this._onConnReady),this._connHooked?.removeEventListener?.("disconnected",this._onConnLost),this._connHooked?.removeEventListener?.("reconnect-error",this._onConnLost),this._connHooked=null,this._haRegistryRelease?.(),this._haRegistryRelease=void 0,this._haRegistryConnection=null,this._signer.dispose(),clearTimeout(this._toastTimer),clearTimeout(this._slideTimer),this._modeTransition.dispose(),this._modeTransitionVisual=null,this._modeTransitionPreparing=!1,this._modeTransitionForceAtomic=!1,this._modeTransitionRequest++,this._slideTimer=void 0,this._slide="",clearTimeout(this._bootTimer),this._bootTimer=void 0,clearTimeout(this._bootSoftTimer),this._saveConfigDebounced.flush(),window.removeEventListener("hashchange",this._onHashChange),this._labsUnsub?.(),this._labsUnsub=void 0,clearTimeout(this._holdTimer),this._roViewport?.disconnect(),this._roViewport=void 0,this._roHdr?.disconnect(),this._roHdr=void 0,this._onWinResize&&(window.removeEventListener("resize",this._onWinResize),this._onWinResize=void 0),this._unsubCfg&&(this._unsubCfg(),this._unsubCfg=null),this._unsubLayout&&(this._unsubLayout(),this._unsubLayout=null),this._unsubVirtual&&(this._unsubVirtual(),this._unsubVirtual=null),this._unsubTrail&&(this._unsubTrail(),this._unsubTrail=void 0),this._liveSyncGeneration++,this._liveSyncAttempt=null,this._liveSyncConnection=null,clearTimeout(this._layoutSyncTimer),clearTimeout(this._duplicateColumnTimer);for(const e of this._glowFadeTimers.values())clearTimeout(e);for(const e of this._glowEnterRafs.values())cancelAnimationFrame(e);clearTimeout(this._glowFeatherResumeTimer),this._glowFeatherResumeTimer=0,this._glowFeatherSuspendUntil=0,this._glowFadeTimers.clear(),this._glowEnterRafs.clear(),this._glowEnteringSources.clear(),this._glowRenderedSources.clear(),this._glowLastAppearance.clear(),this._glowSourceSeq=0,this._warmSnapshot(),this._warmRevivePending=!1,clearTimeout(this._warmReviveTimer),this._warmReviveTimer=void 0,this._warmRelease(),this._clearPlanSnapHover(),this._clearOpeningPlacement(!0),this._touchContacts.clear(),this._touchSequenceMultitouch=!1,this._touchClickBlockUntil=0,this._clearTransientHover(!0),this._cancelDevicePressFeedback(),this._pointerHoverObserver?.disconnect(),this._pointerHoverObserver=void 0,this._pointerModality.disconnect(),this._editorSecondary.reset(),this._resumeSettling=!1,this._continuityHistory=[...this._continuityHistory,...this._continuity.trace].slice(-80),this._continuity.dispose(),this._continuityDisposed=!0,super.disconnectedCallback()}_onKey(e){if("Escape"===e.key&&this._vacFit)return this._vacFit=null,this._showToast(this._t("vac.cal_cancelled")),void e.stopPropagation();if("Escape"===e.key){if(this._tapConfirm)return void(this._tapConfirm=null);if(this._vacCalConfirm)return void(this._vacCalConfirm=null);if(this._decorEraseConfirm)return void(this._decorEraseConfirm=null);if(this._openingInfo)return void(this._openingInfo=null);if(this._infoCard)return void this._closeInfoCard();if(this._rulesDialog)return void(this._rulesDialog=null);if(this._alignDialog)return this._alignDialog=null,void(this._preflightClipboardFallback=null);if(this._backupImportDialog)return void(this._backupImportDialog=null);if(this._backupExportDialog)return void(this._backupExportDialog=null);if(this._settingsDialog)return void(this._settingsDialog=null);if(this._markerDialog)return void this._closeMarkerDialog();if(this._deviceInbox)return void(this._deviceInbox=null);if(this._openingDialog)return void(this._openingDialog=null);if(this._physicalDialog)return void(this._physicalDialog=null);if(this._backdropDialog)return void(this._backdropDialog=null);if(this._decorShapeDialog)return void(this._decorShapeDialog=null);if(this._decorTextDialog)return void(this._decorTextDialog=null);if(this._spaceDialog&&!this._roomDialog)return this._spaceDialog=null,this._importQueue=[],void(this._importTotal=0);if(this._editorSecondary.hasOpenGroup)return e.preventDefault(),void this._editorSecondary.closeGroup(!0)}const t=e.composedPath?.()||[e.target],i=t.some(e=>e?.matches?.('input, textarea, select, [contenteditable="true"]')),s=t.some(e=>e?.classList?.contains?.("editor-secondary")),n=e.ctrlKey||e.metaKey,o=e.key.toLowerCase(),a=/^[a-z]$/.test(o),r="z"===o||!a&&"KeyZ"===e.code,l="y"===o||!a&&"KeyY"===e.code,c=n&&(r&&e.shiftKey||l),h=n&&r&&!e.shiftKey;if("decor"===this._mode){if((h||c)&&i)return;return c?(e.preventDefault(),void this._redoGeometry()):h?(e.preventDefault(),this._decorDraft?void(this._decorDraft=null):this._decorMove||this._dtDrag||this._bdDrag?void this._cancelDecorGesture():void this._undoGeometry()):"Delete"!==e.key&&"Backspace"!==e.key||!this._decorSel||i||s?void("Escape"===e.key&&(e.preventDefault(),this._decorDraft?this._decorDraft=null:this._decorMove||this._dtDrag||this._bdDrag?this._cancelDecorGesture():"furniture"===this._decorTool?(this._furnPalette=null,this._decorTool="select"):this._decorSel?this._decorSel=null:"select"!==this._decorTool?this._decorTool="select":this._setMode("view"))):(e.preventDefault(),void this._decorDeleteSel())}if(this._markup&&(!h&&!c||!i)){if(("Delete"===e.key||"Backspace"===e.key)&&this._physicalSel&&!i&&!s)return e.preventDefault(),void this._deletePhysicalSelection();if(c)return e.preventDefault(),void this._redoGeometry();if(h)return e.preventDefault(),this._resize.dragging?void this._rszCancelDrag():this._wallFaceBatch?(this._roomDialogCancel(),void(this._activeDraftId&&this._path.length>1?this._undoActiveDraftPoint():this._undoPoint())):"draw"===this._tool&&this._path.length?void(this._activeDraftId&&this._path.length>1?this._undoActiveDraftPoint():this._undoPoint()):"split"===this._tool&&this._splitSel?.pts?.length?(this._splitSel={...this._splitSel,pts:this._splitSel.pts.slice(0,-1)},void(this._splitSel.pts.length||(this._cursorPt=null))):void this._undoGeometry();if("Escape"===e.key)return this._physicalDrag||this._physicalRotate?(e.preventDefault(),void this._cancelPhysicalGesture()):this._roomDialog?(e.preventDefault(),void this._roomDialogCancel()):"draw"===this._tool&&this._path.length?(e.preventDefault(),void this._finishWallChain()):this._physicalSel?(e.preventDefault(),void(this._physicalSel=null)):"resize"===this._tool?(e.preventDefault(),this._resize.dragging?void this._rszCancelDrag():("exit-tool"===this._resize.escapeIdle()&&(this._tool="draw"),void this.requestUpdate())):"split"===this._tool?(e.preventDefault(),void(this._splitSel?.pts?.length?(this._splitSel={...this._splitSel,pts:this._splitSel.pts.slice(0,-1)},this._splitSel.pts.length||(this._cursorPt=null)):this._splitSel?this._splitSel=null:this._tool="draw")):"merge"===this._tool?(e.preventDefault(),void(this._mergeSel?this._mergeSel=null:this._tool="draw")):this._wallDialog?(e.preventDefault(),void(this._wallDialog=null)):void("opening"!==this._tool&&"wallthick"!==this._tool&&"delroom"!==this._tool&&"column"!==this._tool||(e.preventDefault(),"opening"===this._tool&&this._clearOpeningPlacement(!0),this._tool="draw"))}}_undoPoint(){if(this._path.length){if(this._contourClosed)return this._path=this._path.slice(0,-1),void(this._closingWallCm=null);if(this._activeDraftId&&this._path.length>1&&this._curSpaceCfg){const e=this._geometrySnapshot();this._path=this._path.slice(0,-1),this._draftSegmentCms=this._draftSegmentCms.slice(0,-1);const t=this._curSpaceCfg,i=(t.room_drafts||[]).findIndex(e=>e.id===this._activeDraftId);if(i>=0){const s=Array.isArray(t.room_drafts[i]?.segments)?t.room_drafts[i].segments:[];this._path.length<2?(t.room_drafts.splice(i,1),t.room_drafts.length||delete t.room_drafts,this._activeDraftId=null):t.room_drafts[i]={id:this._activeDraftId,points:this._path.map(e=>[e[0]/Ov,e[1]/Ov]),segments:this._draftSegmentCms.map((e,t)=>({...s[t]||{},cm:e}))},this._commitPhysicalGeometry(this._t("history.draft_segment_delete"),e)}return}this._path=this._path.slice(0,-1)}}_undoActiveDraftPoint(){const e=this._activeDraftId,t=this._path[0]?[...this._path[0]]:null,i=this._geometryHistory.undo();if(!i)return void this._undoPoint();if(!this._applyGeometryState(i.before,!0))return void this._geometryHistory.clear();const s=e?this._spaceModel()?.room_drafts.find(t=>t.id===e):null;s?(this._activeDraftId=s.id,this._path=s.points.map(e=>[...e]),this._draftSegmentCms=this._adoptDraftCms(this._path,s.segments.map(e=>e.cm),s.id),this._resumeDraftBySpace[this._space]=s.id):(this._activeDraftId=null,this._path=t?[t]:[],this._draftSegmentCms=[],e&&this._resumeDraftBySpace[this._space]===e&&delete this._resumeDraftBySpace[this._space]),this._clearPlanSnapHover(),this._showToast(this._t("history.undone",{name:i.name}))}static getConfigElement(){return document.createElement("houseplan-card-editor")}static getStubConfig(){return{type:"custom:houseplan-card"}}static _warmBootReset(e){for(const e of Cv.values())for(const t of e)clearTimeout(t.evict);Cv.clear(),Tv=e&&e>0?e:1e4}static _warmBootStats(){let e=0,t=0;const i=[];for(const s of Cv.values())for(const n of s)e++,n.dlg&&(t++,i.push(n.dlg.kind));return{keys:Cv.size,slots:e,dlgs:t,drafts:i}}setConfig(e){const t=this._config,i=!!t&&Object.prototype.hasOwnProperty.call(t,"floor");this._config={icon_size:2.5,show_temperature:!0,live_states:!0,show_signal:!0,...e};(i!==this._hasFixedFloor||t?.floor!==this._config.floor)&&(this._hashApplied=!1,this._navApplied=!1,this._warmVpArmed=!1),this._config.kiosk&&(this._booting=!1,this._bootFading=!1),!this._hasFixedFloor&&e.default_floor&&this._commitSpace(e.default_floor,!0);try{this._zoomBySpace=JSON.parse(localStorage.getItem(Av)||"{}")||{}}catch{this._zoomBySpace={}}try{const e=JSON.parse(localStorage.getItem(Nv)||"{}")||{};this._viewPreference=Object.fromEntries(Object.entries(e).filter(e=>"flat"===e[1]||"iso"===e[1]))}catch{this._viewPreference={}}this._labs=Tf(vv);try{const e=JSON.parse(localStorage.getItem(Fv)||"null");this._kioskScale={icon:Ns(e?.icon),font:Ns(e?.font)}}catch{}try{const e=JSON.parse(localStorage.getItem(Rv)||"null");e&&e.config&&Array.isArray(e.config.spaces)&&(this._serverCfg=e.config,this._cfgEpoch++,this._cfgRev=e.rev||0,this._cfgContentFingerprint=e.config_fingerprint||Jd(e.config),this._layout=e.layout||{},this._layoutRev=e.layout_rev||0,this._layoutContentFingerprint=e.layout_fingerprint||Jd(this._layout),this._virtualLights=mn(e.virtual_lights,this._cfgRev),this._serverStorage=!0)}catch{}this._adoptInitialSpace(this._model,this._loadOk),"view"!==this._mode||this._view||(this._zoom=this._zoomBySpace[this._space]||1),this.isConnected&&(this._syncCycleTimer(),this._warmAdopt(),this._warmLongReturn&&this._beginResumeSettle(),this._warmLongReturn=!1)}_warmAdopt(){if(this._config?.kiosk)return;const e=Pv(this._config);if(this._warmKey===e&&this._warmSlot)return;this._warmSlot&&this._warmRelease();const t=this.parentNode,i=this._warmIdx(t),s=Cv.get(e);if(!s||!s.length)return;const n=s.find(e=>e.owner===this._warmGen);if(n)return this._warmLongReturn=!!n.freed&&Date.now()-n.freed>=Wd,clearTimeout(n.evict),n.evict=0,n.freed=0,n.live=!0,this._warmSlot=n,this._warmKey=e,n.frameFingerprint&&this._continuity.adoptCompleteFrame(n.frameFingerprint),void(!this._devices.length&&n.devices?.length&&(this._devices=[...n.devices]));const{slot:o,sure:a}=((e,t,i,s)=>{const n=e=>{const t=!!i&&e.place?.deref()===i;return t&&e.idx===s?4:e.live?0:t?3:2};let o=null,a=0,r=0,l=null;for(const i of e){if(i.owner===t)continue;l=i;const e=n(i);e<=0||(e>a?(o=i,a=e,r=1):e===a&&r++)}return!o||r>1?{slot:o||l,sure:!1}:{slot:o,sure:!0}})(s,this._warmGen,t,i);o&&(this._warmLongReturn=!!o.freed&&Date.now()-o.freed>=Wd,this._booting=!1,this._bootFading=!1,this._hdrH=o.hdrH,this._bootSoft=!0,this.isConnected&&(clearTimeout(this._bootSoftTimer),this._bootSoftTimer=window.setTimeout(()=>{this._bootSoft=!1},xv)),this._warmKey=e,a?(clearTimeout(o.evict),o.evict=0,o.owner=this._warmGen,o.place=t?new WeakRef(t):null,o.idx=i,o.live=!0,this._warmSlot=o,this._warmVp=o.vp,o.frameFingerprint&&this._continuity.adoptCompleteFrame(o.frameFingerprint),!this._devices.length&&o.devices?.length&&(this._devices=[...o.devices]),this._warmAdoptViewport(this._config)):(this._warmSlot={owner:this._warmGen,path:location.pathname,place:t?new WeakRef(t):null,idx:i,live:!0,hdrH:o.hdrH,stageH:o.stageH,vp:null,frameFingerprint:"",devices:null,dlg:null,freed:0,evict:0},s.push(this._warmSlot),this._warmTrim(s)))}_warmIdx(e){const t=e?.children;if(!t)return-1;for(let e=0;e4;){const t=e.findIndex(e=>!e.live);if(t<0)break;clearTimeout(e[t].evict),e.splice(t,1)}}_warmScheduleEvict(e,t){if(clearTimeout(e.evict),!(e.dlg||e.vp&&"view"!==e.vp.mode))return;const i=e.freed,s=e.owner;e.evict=window.setTimeout(()=>{if(e.evict=0,e.freed!==i||e.owner!==s)return;var n;e.dlg=null,e.vp=(n=e.vp)&&"view"!==n.mode?{...n,mode:"view",zoom:n.snap?.space===n.space?n.snap.zoom:n.zoom,view:null,snap:null,tool:"draw",decorTool:"select",showHidden:!1,selId:null,rszSel:null,decorSel:null}:n,e.frameFingerprint="";const o=Cv.get(t);if(!e.live&&o&&o.length>1){const t=o.indexOf(e);t>=0&&o.splice(t,1)}},Tv+250)}_warmAdoptViewport(e){const t=this._warmVp;if(!t||this._warmSlot?.path!==location.pathname)return void(this._warmVp=null);const i=this._fixedFloorState();if(this._hashApplied||!this._model.find(e=>e.id===t.space)||"valid"===i.kind&&i.id!==t.space||this._hasFixedFloor&&"valid"!==i.kind)return void(this._warmVp=null);this._commitSpace(t.space,!0),this._navApplied=!0,this._adoptMode("view"!==t.mode&&this._canEdit&&!e.kiosk?t.mode:"view"),this._pendingNavMode="view"===t.mode||this._canEdit||e.kiosk?null:t.mode,this._zoom=t.zoom;const s=this._effectiveProjection(),n=s===t.projection&&this._labsIso===t.activeLabsIso;if(this._view=n&&t.view?{...t.view}:null,this._viewModeSnap=n&&t.snap?{...t.snap}:null,!n&&t.logicalCenter){const e="iso"===s?Af([t.logicalCenter.x,t.logicalCenter.y],0):[t.logicalCenter.x,t.logicalCenter.y];this._applyView(t.zoom,e[0],e[1])}var o;this._tool="opening"===(o=function(e){return"partition"===e?"draw":e}(o=t.tool))?"draw":"string"==typeof o&&Bv.has(o)?o:"draw",this._decorTool=t.decorTool,this._showHidden=t.showHidden,this._showFar!==t.showFar&&(this._showFar=t.showFar,this._frame=null),this._selId=t.selId,this._resize.restoreSelection(t.rszSel),this._decorSel=t.decorSel,this._warmVpArmed=!0}_warmPatch(e,t=!1){if(this._config?.kiosk)return;const i=Pv(this._config);if(!this._warmSlot||this._warmKey===i){if(!this._warmSlot){if(!t)return;const e=this.parentNode;this._warmKey=i,this._warmSlot={owner:this._warmGen,path:location.pathname,place:e?new WeakRef(e):null,idx:this._warmIdx(e),live:!0,hdrH:this._hdrH,stageH:0,vp:null,frameFingerprint:"",devices:null,dlg:null,freed:0,evict:0};const s=Cv.get(i)||[];for(s.push(this._warmSlot),Cv.set(i,s),this._warmTrim(s);Cv.size>8;){const e=Cv.keys().next().value;if(void 0===e||e===i)break;for(const t of Cv.get(e)||[])clearTimeout(t.evict);Cv.delete(e)}}Object.assign(this._warmSlot,e)}}_warmViewportState(){const e=this._effectiveProjection();return{space:this._space,mode:this._mode,projection:e,activeLabsIso:this._labsIso,logicalCenter:this._logicalViewCenter(e),zoom:this._zoom,view:this._view?{...this._view}:null,snap:this._viewModeSnap?{...this._viewModeSnap}:null,tool:this._tool,decorTool:this._decorTool,showHidden:this._showHidden,showFar:this._showFar,selId:this._selId,rszSel:this._resize.selectedRoomId,decorSel:this._decorSel}}_warmDialogState(){const e=(e,t)=>({kind:e,space:this._space,mode:this._mode,data:t});return this._tapConfirm||this._alignDialog||this._mergeDialog||this._importDialog||this._backupExportDialog||this._backupImportDialog?null:this._openingInfo?e("openingInfo",this._openingInfo.id):this._infoCard?e("info",this._infoCard.id):this._rulesDialog?this._rulesDialog.busy?null:e("rules",this._rulesDialog):this._settingsDialog?this._settingsDialog.busy?null:e("settings",this._settingsDialog):this._markerDialog?this._markerDialog.busy?null:e("marker",this._markerDialog):this._openingDialog?e("opening",this._openingDialog):this._backdropDialog?e("backdrop",this._backdropDialog):this._decorShapeDialog?e("decorShape",this._decorShapeDialog):this._decorTextDialog?e("decorText",this._decorTextDialog):this._roomDialog?e("room",{editId:this._roomEditId,fill:this._roomFill,customFill:this._roomCustomFill,tempSrc:this._roomTempSrc,humSrc:this._roomHumSrc,srcOpen:this._roomSrcOpen,srcFilter:this._roomSrcFilter,nameScale:this._roomNameScale,labelScale:this._roomLabelScale,areaSel:this._areaSel,nameSel:this._nameSel,pendingSplit:this._pendingSplit,wallFaceBatch:this._wallFaceBatch,path:this._path}):this._spaceDialog?this._spaceDialog.busy?null:e("space",this._spaceDialog):null}_warmSnapshot(){if(this._booting||this._config?.kiosk||"steady"!==this._continuity.state)return;const e={vp:this._warmViewportState(),frameFingerprint:this._continuity.frameFingerprint,devices:this._devices};if(this._warmRevivePending||(e.dlg=this._warmDialogState()),this.isConnected&&this._warmSlot?.owner===this._warmGen){const t=this.parentNode;e.place=t?new WeakRef(t):null,e.idx=this._warmIdx(t)}this._warmPatch(e)}_warmReviveDialog(){this._warmRevivePending=!1;const e=this._warmSlot;if(this._warmReviveTimer=void 0,!e||!e.dlg)return;const t=e.dlg,i=e.freed;if(e.dlg=null,e.freed=0,clearTimeout(e.evict),e.evict=0,i&&!(Date.now()-i>Tv)&&t.space===this._space&&t.mode===this._mode){switch(t.kind){case"space":this._spaceDialog={...t.data,busy:!1,savedBusy:!1};break;case"marker":this._markerDialog={...t.data,busy:!1};break;case"settings":this._settingsDialog={...t.data,busy:!1};break;case"rules":this._rulesDialog={...t.data,busy:!1};break;case"opening":this._openingDialog={...t.data};break;case"backdrop":this._backdropDialog={...t.data};break;case"decorShape":this._decorShapeDialog={...t.data};break;case"decorText":{this._decorTextDialog={...t.data};const e=String(this._decorTextDialog?.text??"").length;this._decorTextSelection={start:e,end:e};break}case"room":{const e=t.data;this._roomEditId=e.editId,this._roomFill=e.fill,this._roomCustomFill=e.customFill||null,this._roomTempSrc=e.tempSrc,this._roomHumSrc=e.humSrc,this._roomSrcOpen=e.srcOpen,this._roomSrcFilter=e.srcFilter,this._roomNameScale=e.nameScale,this._roomLabelScale=e.labelScale,this._areaSel=e.areaSel,this._nameSel=e.nameSel,this._pendingSplit=e.pendingSplit,this._wallFaceBatch=e.wallFaceBatch||null,this._path=e.path,this._wallFaceBatch&&(this._activeDraftId=this._wallFaceBatch.activeDraftId,this._draftSegmentCms=[...this._wallFaceBatch.activeCms]),this._roomDialog=!0;break}case"info":{const e=this._devices.find(e=>e.id===t.data);e&&(this._infoCard=e);break}case"openingInfo":{const e=(this._curSpaceCfg?.openings||[]).find(e=>e.id===t.data);e&&(this._openingInfo=e);break}}this.requestUpdate()}}_cacheSnapshot(){if(this._serverCfg)try{this._cfgContentFingerprint=Jd(this._serverCfg),this._layoutContentFingerprint=Jd(this._layout),this._virtualLights=function(e,t,i){if(e.configRev===i)return e;const s=new Set((Array.isArray(t?.markers)?t.markers:[]).filter(bn).map(e=>e.id));return{...e,configRev:i,off:new Set([...e.off].filter(e=>s.has(e)))}}(this._virtualLights,this._serverCfg,this._cfgRev),localStorage.setItem(Rv,JSON.stringify({config:this._serverCfg,rev:this._cfgRev,config_fingerprint:this._cfgContentFingerprint,layout:this._layout,layout_rev:this._layoutRev,layout_fingerprint:this._layoutContentFingerprint,virtual_lights:fn(this._virtualLights)}))}catch{}}_beginContinuityCandidate(e,t,i="plan"){return this._booting&&!this._continuity.hasCompleteFrame?this._continuity.token:(this._continuityDataReady=t,this._continuityPaintToken=-1,this._stagedDeviceSnapshotToken=-1,this._resumeSettling=!0,this._continuity.beginCandidate(e,i))}_continuityStageValid(){const e=this._stageEl;return!!e&&e.clientWidth>0&&e.clientHeight>0}_continuityAssetsReady(){if(!this._model.length)return!1;const e=this._model.length?this._spaceModel():null;return!e?.bg?.href||this._signer.isReady(this.hass,e.bg.href)}_initialSpaceSelection(e,t=this._loadOk){const i=this._fixedFloorState(e,t);return"valid"===i.kind?{id:i.id,source:"fixed"}:this._hasFixedFloor?{id:null,source:"none"}:function(e){const t=new Set(e.spaceIds.filter(e=>!!e));if(!t.size)return{id:null,source:"none"};const i=[["hash",!1===e.acceptHash?null:e.hashSpace],["current",e.preserveCurrent?e.currentSpace:null],["saved",e.savedSpace],["default",e.defaultSpace],["first",e.spaceIds[0]]];for(const[e,s]of i)if(s&&t.has(s))return{id:s,source:e};return{id:null,source:"none"}}({spaceIds:e.map(e=>e.id),hashSpace:this._hashSpace(),acceptHash:!this._hashApplied,currentSpace:this._space,preserveCurrent:this._hashApplied||this._navApplied||this._warmVpArmed,savedSpace:this._savedNav()?.space,defaultSpace:this._config?.default_floor})}_adoptInitialSpace(e,t=this._loadOk){const i=this._initialSpaceSelection(e,t);return i.id?(this._commitSpace(i.id,!0),"hash"===i.source&&(this._hashApplied=!0),"saved"===i.source&&(this._navApplied=!0),i):i}_candidateBackdrop(e,t=this._space){const i=Eh(e),s=this._fixedFloorState(i,!0);if(this._hasFixedFloor&&"valid"!==s.kind)return"";const n=this._initialSpaceSelection(i,!0).id||(i.some(e=>e.id===t)?t:i[0]?.id);return i.find(e=>e.id===n)?.bg?.href||""}_visualFrameFingerprint(){const e=this._stageEl,t=e?[e.clientWidth,e.clientHeight]:[0,0];return Xd([this._cfgRev,this._cfgContentFingerprint||Jd(this._serverCfg),this._layoutRev,this._layoutContentFingerprint||Jd(this._layout),this._space,this._mode,this._view,t,this._glowScreenBlend?"screen":"normal",this.hass?.themes?.darkMode??this.hass?.themes?.default_theme??""])}_settleContinuityFrame(){if(this._booting||!this._continuityStageValid())return;if(!this._continuity.hasCompleteFrame&&"steady"===this._continuity.state)return void(this._continuityAssetsReady()&&(this._renderSnapshotAt=Date.now(),this._continuity.markCompleteFrame(this._visualFrameFingerprint())));if(!this._continuityDataReady)return;if(!["holding","offline-stale","overlay-pending","overlay-visible","candidate-ready"].includes(this._continuity.state))return;const e=this._continuity.token;if(this._candidateDeviceSnapshot&&this._candidateDeviceSnapshot!==this._visibleDeviceSnapshot&&this._stagedDeviceSnapshotToken!==e)return this._stagedDeviceSnapshotToken=e,void this.requestUpdate();this._continuityPaintToken!==e&&(this._continuityPaintToken=e,this._continuity.candidateReady(e)&&this._continuity.commitAfterPaint(e,{updateComplete:()=>this.updateComplete,stageValid:()=>this.isConnected&&this._continuityStageValid(),assetsReady:()=>this._continuityAssetsReady(),frameFingerprint:()=>this._visualFrameFingerprint()}).then(t=>{t&&e===this._continuity.token?(this._resumeSettling=!1,this._renderSnapshotAt=Date.now(),this._candidateDeviceSnapshot&&(this._visibleDeviceSnapshot=this._candidateDeviceSnapshot),this._candidateDeviceSnapshot=null,this._stagedDeviceSnapshotToken=-1,this._warmSnapshot()):e===this._continuity.token&&(this._continuityPaintToken=-1,this._stagedDeviceSnapshotToken=-1,this._candidateDeviceSnapshot=null,this.requestUpdate())}))}_onBackdropLoaded(e,t){this._signer.markLoaded(this.hass,e,t),this._continuity.note("asset-ready"),this._continuityPaintToken=-1,"steady"!==this._continuity.state&&this.requestUpdate()}_renderRecoveryOverlay(){if(!this._continuity.overlayVisible&&"recovery-error"!==this._continuity.state)return G;const e="connection"===this._continuity.recoveryReason;return q`
e.stopPropagation()} - @click=${e=>e.stopPropagation()} - @wheel=${e=>e.stopPropagation()}> - - ${this._t(e?"continuity.restore_connection":"continuity.restore_plan")} - ${"recovery-error"===this._continuity.state?q``:G} -
`}houseplanContinuityTrace(){return[...this._continuityHistory,...this._continuity.trace].slice(-80).map(e=>({...e,...e.stage?{stage:[...e.stage]}:{}}))}getCardSize(){return 12}get _norm(){return!(!this._serverCfg||!this._serverCfg.spaces.length)}_cfgFingerprint(){const e=this._serverCfg?.spaces||[];let t=e.length+":";for(const i of e){t+=(i.id||"")+","+(i.plan_aspect||"")+","+(i.plan_url||"").length+","+(i.plan_x??"")+","+(i.plan_y??"")+","+(i.plan_scale??"")+","+(i.plan_scale_x??"")+","+(i.plan_scale_y??"")+","+(i.plan_angle??"")+","+(i.rooms?.length||0)+","+(i.openings?.length||0)+","+(i.decor?.length||0)+";";for(const e of i.rooms||[]){const i=e.poly?.[0],s=e.poly?.[e.poly.length-1];t+=(e.poly?.length||0)+"."+(e.id||"")+"."+(e.open_to||[]).join("+")+"."+(e.area||"")+"."+JSON.stringify(e.settings||0)+"."+(e.x??"")+","+(e.y??"")+","+(e.w??"")+","+(e.h??"")+","+(i?i[0]+"/"+i[1]:"")+","+(s?s[0]+"/"+s[1]:"")+";"}}return t}get _model(){if(!this._serverCfg)return[];const e=this._cfgEpoch+"|"+this._cfgFingerprint();if(this._modelCache&&this._modelCache.key===e)return this._modelCache.model;const t=this._buildModel();return this._modelCache={key:e,model:t},t}_buildModel(){if(!this._serverCfg)return[];const e=this._renderCfg;return Eh(e).map((t,i)=>{const s=e.spaces[i]?.plan_url;return t.bg&&s?{...t,bg:{...t.bg,href:s}}:t})}_spaceModel(){return this._hasFixedFloor?ep(this._model,this._space):(e=this._model,t=this._space,e.find(e=>e.id===t)??e[0]);var e,t}_spaceModelById(e){return ep(this._model,e)}_syncEmptySpaceState(){if(!!this._serverCfg&&0===this._serverCfg.spaces.length){if(!this._emptySpaceStateActive){this._emptySpaceStateActive=!0;for(const e of this._pointers.keys())for(const t of this.renderRoot.querySelectorAll("*"))try{t.hasPointerCapture?.(e)&&t.releasePointerCapture(e)}catch{}this._pointers.clear(),this._panStart=null,this._panLock=null,this._pinchStart=null,this._swipeStart=null,this._drag=null,this._rlResize=null,this._vacFit=null,this._compassDrag=!1,this._cancelModeTransition(!1),this._mode="view",this._clearGeometryGesture(),this._geometryHistory.clear(),this._resumeDraftBySpace={},this._tip=null,this._hoverRoom=null,this._openingInfo=null,this._closeInfoCard(),this._deviceInbox=null,this._deviceInboxReturn=null,this._markerDialog=null,this._physicalDialog=null,this._backdropDialog=null,this._decorShapeDialog=null,this._decorTextDialog=null,this._roomDialog=!1,"edit"===this._spaceDialog?.mode&&(this._spaceDialog=null),this._editorSecondary.closeForNavigation(),this._saveConfigDebounced.cancel(),this._frame=null,this._planSnapGeometryCache=null,this._hiddenWallDiagnosticCache=null,this._decorSnapCache=null,this._commitSpace("",!0)}}else this._emptySpaceStateActive=!1}get _areaToSpace(){const e={};for(const t of this._model)for(const i of t.rooms)i.area&&(e[i.area]={space:t.id,room:i});return e}get _settings(){return this._serverCfg?.settings||{}}get _showAll(){return this._showHidden||!this._settings.filter_seeded&&!!this._settings.show_all}_seedHiddenDevices(){if(!this._serverCfg||!this._norm||!this._canEdit)return;const e=this._serverCfg,t=function(e){const t=e.hass,i=e.registry||An(t),s=En(t,i),n=On(t,i),{areaToSpace:o,markers:a,settings:r,excluded:l,iconRules:c}=e,h=!1!==r.group_lights,d=wo(a),p=bo(s,h).filter(e=>!ko(s,e.eid,d)),u=new Set(p.map(e=>e.area)),_=jn(s),m=qn(a,n),g=new Set(a.map(e=>e.binding)),f=[];for(const e of Object.values(s.devices)){const n=e.area_id;if(!n||!o[n])continue;if("service"===e.entry_type)continue;if(g.has("device:"+e.id))continue;if("active"!==Bn(t,"device:"+e.id,i).kind)continue;const a=Wn(s,e.id,_[e.id]||[],m);if(a.partial&&!a.entityIds.length)continue;const r=a.entityIds,d=Un(s,e,r);let p=l.has(d)||"Group"===e.model||/scene/i.test(e.model||"")||/bridge/i.test((e.model||"")+(e.name||""))||"myheat"===d&&!!e.via_device_id;if(!p&&h&&u.has(n)){const t=(e.name_by_user||e.name||"").trim();"mdi:lightbulb"===yo(s,t,e.model,r,c)&&(p=!0)}p&&f.push("device:"+e.id)}return f}({hass:this.hass,registry:this._haRegistry,areaToSpace:Object.fromEntries(Object.entries(this._areaToSpace).map(([e,t])=>[e,t.space])),markers:this._markers,settings:this._settings,excluded:this._excluded,firstSpaceId:this._model[0]?.id||"",iconRules:this._iconRules});if(!t.length&&e.settings?.filter_seeded)return;e.markers=e.markers||[];const i=[];for(const s of t){const t="h"+s.slice(s.indexOf(":")+1);e.markers.push({id:t,binding:s,hidden:!0}),i.push(s.slice(s.indexOf(":")+1))}const s={...e.settings||{},filter_seeded:!0};delete s.show_all,i.length&&Array.isArray(s.new_device_ids)&&(s.new_device_ids=s.new_device_ids.filter(e=>!i.includes(e))),e.settings=s,this._regSignature="",this._maybeRebuildDevices(),this._saveConfig(),this.requestUpdate()}get _iconRules(){const e=this._settings.icon_rules;if(!e||!Array.isArray(e)||!e.length)return;const t=JSON.stringify(e);return t!==this._rulesCompiledSrc&&(this._rulesCompiledSrc=t,this._rulesCompiled=Xe(e)),this._rulesCompiled}get _fillColors(){return ps(this._settings)}get _excluded(){const e=this._settings.exclude_integrations;return e?new Set(e):Ye}willUpdate(e){e.has("_serverCfg")&&this._cfgEpoch++,this._syncEmptySpaceState(),e.has("hass")&&this.hass&&(this._hassSequence++,this._renderSnapshotAt=Date.now(),this._continuity.note("hass-snapshot"),this._ensureHaRegistryAuthority(),this._planHassMemo=null,this._hookConnection(),!this._loadOk&&!this._loading&&this._loadTries<8&&this._loadFromServer(),this._maybeRebuildDevices(),this._vacTick(),this._activityTick()),this._continuity.hasCompleteFrame&&"steady"===this._continuity.state&&this._continuity.refreshCompleteFrame(this._visualFrameFingerprint()),this._captureRenderDeviceSnapshot()}updated(){this._pruneDevicePressFeedback(),this._syncDayCycleClock(),this._warmSnapshot(),this._dtMeasure();const e=this._stageEl;e&&!this._roViewport&&(this._roViewport=new ResizeObserver(()=>this._refitView()),this._roViewport.observe(e)),e&&this._booting&&!this._bootTimer&&this._bootWatch();const t=this.renderRoot.querySelector(".hdr");if(t&&e&&!this._roHdr){const i=()=>{const t=this.renderRoot.querySelector("ha-card");if(!t)return;const i=e.getBoundingClientRect().top-t.getBoundingClientRect().top,s=Math.min(Math.max(t.getBoundingClientRect().top,0),120),n=Math.round(i+s);n>=0&&n!==this._hdrH&&(this._hdrH=n),n>=0&&!this._booting&&!this._config?.kiosk&&e.clientHeight>0&&this._warmPatch({hdrH:n,stageH:e.clientHeight})};this._roHdr=new ResizeObserver(()=>requestAnimationFrame(i)),this._roHdr.observe(t),this._onWinResize=()=>requestAnimationFrame(i),window.addEventListener("resize",this._onWinResize),i()}if(e&&!this._view&&this._refitView(),this._editorSecondary.afterRender(),this._settleContinuityFrame(),this._serverStorage&&this._loadOk&&0===this._model.length&&!this._spaceDialog&&!this._importDialog&&!this._onboardingShown){this._onboardingShown=!0;const e=function(e){const t=e?.floors;if(!t||"object"!=typeof t)return[];const i=[];for(const e of Object.values(t))e&&e.floor_id&&i.push({id:e.floor_id,name:e.name||e.floor_id,level:e.level??null});return i.sort((e,t)=>{const i=e.level??1e9,s=t.level??1e9;return i!==s?i-s:e.name.localeCompare(t.name)}),i}(this.hass);e.length?this._importDialog={floors:e.map(e=>({...e,checked:!0}))}:this._openSpaceDialog("create")}}_adoptStructuralResponses(e,t,i){const s=e?.config,n=s&&Array.isArray(s.spaces)?s:null,o=Jd(n),a=o!==(this._cfgContentFingerprint||Jd(this._serverCfg));if(a&&(this._geometryHistory.clear(),this._pendingPhysicalWrites.clear(),this._serverCfg&&this._clearGeometryGesture(),this._serverCfg=n,this._cfgContentFingerprint=o),this._cfgRev=e?.rev??this._cfgRev,e&&("virtual_lights"in e||"config"in e)){const t=gn(this._virtualLights,e.virtual_lights,this._cfgRev,"virtual_lights"in e);vn(t)!==vn(this._virtualLights)&&(this._virtualLights=t,this._capturedSnapshotVirtual="")}let r=!1;if(void 0!==t||void 0!==i){const e=i??t?.layout??{},s=Jd(e);r=s!==(this._layoutContentFingerprint||Jd(this._layout)),r&&(this._layout=e,this._layoutContentFingerprint=s),this._layoutRev=t?.rev??this._layoutRev}return this._canOptimizeUndo=!(!e?.can_optimize_undo&&!t?.can_optimize_undo),this._haIntegrationVersion="string"==typeof e?.integration_version?e.integration_version:this._haIntegrationVersion,this._undoKind=e?.undo_kind||t?.undo_kind||null,"boolean"==typeof e?.can_write&&(this._serverCanWrite=e.can_write),a&&this._continuity.note("config-candidate",{configRev:this._cfgRev}),r&&this._continuity.note("layout-candidate",{layoutRev:this._layoutRev}),{configChanged:a,layoutChanged:r}}_resumePendingNavMode(){if(!this._pendingNavMode||!this._canEdit||this._config?.kiosk)return!1;const e=this._pendingNavMode;return this._pendingNavMode=null,this._setMode(e,!1),!0}async _loadFromServer(){this._loading=!0,this._loadTries++;const e=this._space,t=!!this._view;try{const[i,s]=await Promise.all([this.hass.callWS({type:"houseplan/config/get"}),this.hass.callWS({type:"houseplan/layout/get"})]),n=i?.config&&Array.isArray(i.config.spaces)?i.config:null,o=Jd(n)!==(this._cfgContentFingerprint||Jd(this._serverCfg))||Jd(s?.layout??{})!==(this._layoutContentFingerprint||Jd(this._layout));if(o){if(!await this._signer.prepareImage(this.hass,this._candidateBackdrop(n)))return this._continuity.note("asset-failed"),void this._scheduleLoadRetry(!0)}o&&this._continuity.hasCompleteFrame&&"steady"===this._continuity.state&&this._beginContinuityCandidate("structural-response",!0),this._connectionWasLost=!1,this._serverStorage=!0,"boolean"==typeof i?.can_write&&(this._serverCanWrite=i.can_write),this._canOptimizeUndo=!(!i?.can_optimize_undo&&!s?.can_optimize_undo),this._haIntegrationVersion="string"==typeof i?.integration_version?i.integration_version:this._haIntegrationVersion,this._adoptStructuralResponses(i,s),this._adoptInitialSpace(this._model,!0),this._resumePendingNavMode(),this._cacheSnapshot(),this._warmVpArmed&&this._space===this._warmVp?.space?this._warmVpArmed=!1:t&&this._space===e||this._restoreZoom(),this._loadOk=!0,this.hass.callWS({type:"houseplan/trail/get"}).then(e=>{this._vacSrvTrails=e?.trails||{},this.requestUpdate()}).catch(()=>{}),this._ensureLiveSyncSubscriptions()}catch(e){if(this._serverCfg)this._scheduleLoadRetry(!0);else if(this._loadTries>=8){this._serverStorage=!1;try{this._layout=JSON.parse(localStorage.getItem(Iv)||"{}")||{}}catch{this._layout={}}}}finally{this._loading=!1,this._continuityDataReady=!0,this._regSignature="",this._maybeRebuildDevices(),this.requestUpdate()}}_ensureLiveSyncSubscriptions(){const e=this.hass?.connection;if(!e)return;if(e!==this._liveSyncConnection&&(this._unsubCfg?.(),this._unsubCfg=null,this._unsubLayout?.(),this._unsubLayout=null,this._unsubTrail?.(),this._unsubTrail=void 0,this._unsubVirtual?.(),this._unsubVirtual=null,this._liveSyncGeneration++,this._liveSyncAttempt=null,this._liveSyncConnection=e),this._liveSyncAttempt)return;const t=this._liveSyncGeneration,i=[],s=(s,n,o,a)=>{s()||i.push(async()=>{const i=await e.subscribeEvents(a,o);t===this._liveSyncGeneration&&this.isConnected&&this.hass?.connection===e&&!s()?n(i):i?.()})};if(s(()=>this._unsubCfg,e=>{this._unsubCfg=e},"houseplan_config_updated",e=>{const t=Number(e?.data?.rev??-1);t!==this._cfgRev&&this._reloadConfigOnly(!1,t)}),s(()=>this._unsubTrail,e=>{this._unsubTrail=e},"houseplan_trail_updated",async()=>{try{const e=await this.hass.callWS({type:"houseplan/trail/get"});this._vacSrvTrails=e?.trails||{},this.requestUpdate()}catch{}}),s(()=>this._unsubLayout,e=>{this._unsubLayout=e},"houseplan_layout_updated",e=>this._onLayoutEvent(Number(e?.data?.rev??-1))),s(()=>this._unsubVirtual,e=>{this._unsubVirtual=e},"houseplan_virtual_light_updated",e=>{const t=wn(this._virtualLights,e?.data);t!==this._virtualLights&&(this._virtualLights=t,this._capturedSnapshotVirtual="",this._cacheSnapshot(),this.requestUpdate())}),!i.length)return;const n=function(e){return Promise.allSettled(e.map(e=>Promise.resolve().then(e)))}(i).then(()=>{});this._liveSyncAttempt=n,n.finally(()=>{this._liveSyncAttempt===n&&(this._liveSyncAttempt=null)})}async _reloadConfigOnly(e=!1,t){if(!e){if(void 0!==t&&t<=this._cfgRev)return;if(this._saveConfigDebounced.pending()&&this._saveConfigDebounced.flush(),this._cfgWriting)return clearTimeout(this._reloadRetry),void(this._reloadRetry=window.setTimeout(()=>this._reloadConfigOnly(!1,t),400))}this._beginContinuityCandidate("config-reload",!1);try{const e=await this.hass.callWS({type:"houseplan/config/get"}),t=e?.config&&Array.isArray(e.config.spaces)?e.config:null;if(Jd(t)!==(this._cfgContentFingerprint||Jd(this._serverCfg))&&!await this._signer.prepareImage(this.hass,this._candidateBackdrop(t)))return this._continuity.note("asset-failed"),void this._scheduleLoadRetry(!0);const i=this._space;this._adoptStructuralResponses(e),this._adoptInitialSpace(this._model,!0),this._resumePendingNavMode(),this._cacheSnapshot(),this._space!==i&&this._restoreZoom(),this._regSignature="",this._maybeRebuildDevices(),this.requestUpdate()}catch(e){this._showToast(this._t("toast.cfg_reload_failed",{err:this._errText(e)}))}finally{this._continuityDataReady=!0,this.requestUpdate()}}_scheduleLoadRetry(e=!1){if(void 0!==this._loadRetryTimer)return;const t=Math.min(8e3,500*2**Math.min(4,Math.max(1,this._loadTries-7)));this._loadRetryTimer=window.setTimeout(()=>{this._loadRetryTimer=void 0,!e&&this._loadOk||this._loading||!this.hass||this._loadFromServer()},t)}_ensureHaRegistryAuthority(){const e=this.hass?.connection||null;e&&e!==this._haRegistryConnection&&(this._haRegistryRelease?.(),this._haRegistryConnection=e,this._haRegistryRev=-1,this._haBindingCacheKey="",this._planHassMemo=null,this._haRegistryRelease=Rn(this.hass,this._onHaRegistryUpdate),this._onHaRegistryUpdate())}get _haRegistry(){return An(this.hass)}get _planHass(){const e=this._haRegistry,t=Fn(this.hass,e),i=this._planHassMemo;if(i&&i.hass===this.hass&&i.sig===t)return i.active;const s=En(this.hass,e),n=On(this.hass,e);return this._planHassMemo={hass:this.hass,sig:t,active:s,full:n},s}_captureRenderDeviceSnapshot(){if(!this.hass)return;const e=Date.now(),t=[...this._activityRt.entries()].map(([t,i])=>`${t}:${i.gen}:${i.flashTs}:`+(i.flashKind&&(i.expiresAt||i.flashTs+um)>e?1:0)).join("|"),i=vn(this._virtualLights);if(this._capturedSnapshotSequence===this._hassSequence&&this._capturedSnapshotDevices===this._devices&&this._capturedSnapshotLayout===this._layout&&this._capturedSnapshotConfigEpoch===this._cfgEpoch&&this._capturedSnapshotVirtual===i&&this._capturedSnapshotActivity===t)return;const s=this._planHass,n=new Map,o=new Map,a=new Set(["sun.sun"]);this._vacFit?.source&&a.add(this._vacFit.source);const r=new Set,l=new Set,c=e=>{if(!e)return;const t=e.indexOf(":");if(t<0)return void a.add(e);const i=e.slice(0,t),s=e.slice(t+1);"device"===i?r.add(s):"entity"===i&&a.add(s)};for(const e of this._model)for(const t of e.rooms)t.area&&l.add(t.area),c(t.settings?.temp_source),c(t.settings?.hum_source);for(const e of this._serverCfg?.spaces||[])for(const t of e.openings||[])for(const e of gi(t))a.add(e);for(const e of this._serverCfg?.spaces||[])for(const t of e.decor||[])if("text"===t.kind){for(const e of String(t.text||"").matchAll(/\{([^{}\r\n]+)\}/g)){const t=Yi(e[1]);t?.entity&&a.add(t.entity)}t.entity&&a.add(t.entity),o.set(`decor:${e.id}:${t.id}`,es(t.text,t,s,e=>!!s.entities?.[e]&&!!s.states?.[e]&&!ko(s,e,wo(this._markers))))}const h=ho(s,this._devices,null,this._virtualLights);for(const e of this._devices){for(const t of[!1,!0])n.set(L_(e.id,t),N_(s,e,{liveStates:!1!==this._config?.live_states,showTemperature:!1!==this._config?.show_temperature,showSignal:t,activityRuntime:this._activityRt.get(e.id),sourceDetails:!1,lightDevices:this._devices,lightSources:h,registryHass:this._fullRegistryHass,reducedMotion:this._reducedMotion}));if(this._isVacDev(e)){const t=this._vacSource(e,s),i=t?up(s?.states?.[t]?.attributes):null,n=this._vacRt.get(e.id);o.set(`vacuum:${e.id}`,{source:t,telemetry:i,mapId:i?this._vacMapId(e,i,s):null,runtime:n?{trail:n.trail,lastTs:n.lastTs,moving:n.moving,jump:n.jump}:null,server:this._vacSrvTrails[e.id]||null})}}const d=H_({sourceSequence:this._hassSequence,hass:s,devices:this._devices,presentations:n,positions:(p=this._model.length>0,u=this._devices,_=e=>this._livePos(e),p?new Map(u.map(e=>[e.id,_(e)])):new Map),facts:o,entityIds:a,deviceIds:r,areaIds:l});var p,u,_;this._capturedSnapshotSequence=this._hassSequence,this._capturedSnapshotDevices=this._devices,this._capturedSnapshotLayout=this._layout,this._capturedSnapshotActivity=t,this._capturedSnapshotConfigEpoch=this._cfgEpoch,this._capturedSnapshotVirtual=i,this._visibleDeviceSnapshot&&"steady"!==this._continuity.state?this._candidateDeviceSnapshot=d:(this._visibleDeviceSnapshot=d,this._candidateDeviceSnapshot=null)}get _renderDeviceSnapshot(){return this._stagedDeviceSnapshotToken===this._continuity.token?this._candidateDeviceSnapshot||this._visibleDeviceSnapshot:this._visibleDeviceSnapshot||this._candidateDeviceSnapshot}get _renderPlanHass(){return this._renderDeviceSnapshot?.hass||this._planHass}get _renderDevices(){return this._renderDeviceSnapshot?.devices||this._devices}get _fullRegistryHass(){return this._planHass,this._planHassMemo?.full||this.hass}_bindingStatus(e){return Bn(this.hass,e,this._haRegistry)}houseplanDiagnostics(){const e=function(e){const t=An(e);return{access:t.access,authoritative:t.authoritative,revision:t.revision,lastSuccess:t.lastSuccess,error:t.error}}(this.hass),t={active:0,ha_disabled:0,orphaned:0,unverified:0};for(const e of this._markers)e.removed||"virtual"===e.binding||t[this._bindingStatus(e.binding).kind]++;return{registry:{...e,lastSuccessAgeMs:e.lastSuccess?Math.max(0,Date.now()-e.lastSuccess):null},bindings:t}}_openBindingInHa(e){const[t,i]=e.split(":");if(i)if("device"!==t){if("entity"===t){const e=this._fullRegistryHass.entities?.[i];e?.device_id&&Wv("/config/devices/device/"+encodeURIComponent(e.device_id))}}else Wv("/config/devices/device/"+encodeURIComponent(i))}_bindingHasHaPage(e){const[t,i]=e.split(":");return!!i&&("device"===t||"entity"===t&&!!this._fullRegistryHass.entities?.[i]?.device_id)}_toggleMarkerDialogVisibility(){const e=this._markerDialog;if(!e)return;const t="ha"===e.bindingMode?this._bindingStatus(e.binding):null,i=e.hideFromPlan||"ha_disabled"===t?.kind;i&&"ha_disabled"===t?.kind?this._showToast(this._t("entity"===t.reason?"toast.ha_disabled_show_entity":"toast.ha_disabled_show_device")):i&&"unverified"===t?.kind?this._showToast(this._t("toast.ha_binding_unverified")):this._markerDialog={...e,hideFromPlan:!i}}_hookConnection(){const e=this.hass?.connection;e&&e!==this._connHooked&&(this._connHooked?.removeEventListener?.("ready",this._onConnReady),this._connHooked?.removeEventListener?.("disconnected",this._onConnLost),this._connHooked?.removeEventListener?.("reconnect-error",this._onConnLost),e.addEventListener?.("ready",this._onConnReady),e.addEventListener?.("disconnected",this._onConnLost),e.addEventListener?.("reconnect-error",this._onConnLost),this._connHooked=e)}_display(e){return this._signer.display(this.hass,e)}_resign(){this._signer.resign(this.hass,Rs(this._serverCfg))}_onLayoutEvent(e){e<=this._layoutRev||(clearTimeout(this._layoutSyncTimer),this._layoutSyncTimer=window.setTimeout(()=>{e<=this._layoutRev||this._reloadLayoutOnly()},200))}_noteLayoutRev(e){const t=e?.rev;"number"==typeof t&&t>this._layoutRev&&(this._layoutRev=t)}async _reloadLayoutOnly(){if(!this._serverStorage||!this.hass?.callWS)return;this._beginContinuityCandidate("layout-reload",!1);const e=new Map;for(const t of this._dirtyPos)this._layout[t]&&e.set(t,this._layout[t]);this._persistLayout.pending()&&this._persistLayout.flush();for(const[t,i]of this._sentPos)e.set(t,i);try{const t=await this.hass.callWS({type:"houseplan/layout/get"}),i={...t?.layout||{}};for(const[t,s]of e)i[t]=s;const s=Jd(i);s!==(this._layoutContentFingerprint||Jd(this._layout))&&(this._layout=i,this._layoutContentFingerprint=s),this._layoutRev=t?.rev??this._layoutRev,this._canOptimizeUndo=!!t?.can_optimize_undo,this._haIntegrationVersion="string"==typeof t?.integration_version?t.integration_version:this._haIntegrationVersion,this._undoKind=t?.undo_kind||null,this._cacheSnapshot(),this.requestUpdate()}catch{}finally{this._continuityDataReady=!0,this.requestUpdate()}}_maybeRebuildDevices(){const e=this.hass;if(!e?.devices||!e?.entities||!e?.areas)return;const t=this._haRegistry,i=Fn(e,t)+":"+Object.keys(e.areas).length+":"+(this._norm?"n":"l")+":"+pa(e,this._config?.language);if(i===this._regSignature&&this._devices.length)return;this._regSignature=i;const s=new Map(this._devices.map(e=>[e.id,e.bindingStatus?.kind||"active"]));this._devices=So({hass:e,registry:t,areaToSpace:Object.fromEntries(Object.entries(this._areaToSpace).map(([e,t])=>[e,t.space])),markers:this._markers,settings:this._settings,excluded:this._excluded,showAll:this._showAll,firstSpaceId:this._model[0]?.id||"",loc:e=>this._t(e),iconRules:this._iconRules});const n=this._markers.filter(e=>!e.removed&&"virtual"!==e.binding).map(e=>e.binding).sort(),o=t.revision+":"+n.join("|");if(t.authoritative&&o!==this._haBindingCacheKey){const i=new Map;for(const s of n)i.set(s,Bn(e,s,t));!function(e){if("undefined"==typeof localStorage||!e.size)return;const t=Ln(),i=Date.now();for(const[s,n]of e)"active"===n.kind?t[s]={kind:"active",ts:i}:"ha_disabled"===n.kind&&(t[s]={kind:"ha_disabled",reason:n.reason,ts:i});const s=Object.fromEntries(Object.entries(t).sort((e,t)=>t[1].ts-e[1].ts).slice(0,1500));Mn=s;try{localStorage.setItem(Sn,JSON.stringify(s))}catch{}}(i),this._haBindingCacheKey=o}this._defPos=this._defaultPositions(),this._syncNewDevices(),this._seedHiddenDevices(),this._syncActivityRuntime();const a=new Set(this._devices.filter(e=>!e.hidden).map(e=>e.id));for(const e of this._vacRt.keys())a.has(e)||this._vacRt.delete(e);if(this._infoCard){const e=this._devices.find(e=>e.id===this._infoCard.id);this._infoCard=e&&"ha_disabled"!==e.bindingStatus?.kind?e:null}const r=this._devices.some(e=>"ha_disabled"===e.bindingStatus?.kind&&"ha_disabled"!==s.get(e.id));r&&("ha_disabled"!==this._infoCard?.bindingStatus?.kind&&"ha_disabled"!==this._devices.find(e=>e.id===this._infoCard?.id)?.bindingStatus?.kind||this._closeInfoCard(),this._drag&&"ha_disabled"===this._devices.find(e=>e.id===this._drag.id)?.bindingStatus?.kind&&(this._drag=null),clearTimeout(this._holdTimer),this._holdFired=!1,this._tip=null,this._tapConfirm=null),this._nativeMoreInfoEntity&&!this._planEntityAvailable(this._nativeMoreInfoEntity)&&(qv(this,"hass-more-info",{entityId:null}),this._nativeMoreInfoEntity=null)}_syncNewDevices(){if(!this._norm||!this._loadOk||!this._serverCfg)return;const e=this._devices.filter(e=>!e.marker&&!e.virtual).map(e=>e.id).sort(),t=e.join(",");if(t===this._newSyncKey)return;this._newSyncKey=t;const i=this._settings,{fresh:s,known:n}=function(e,t){if(!Array.isArray(t))return{fresh:[],known:[...e]};const i=new Set(t),s=e.filter(e=>!i.has(e));return{fresh:s,known:s.length?[...t,...s]:t}}(e,i.known_devices);if(!Array.isArray(i.known_devices)||s.length){const e=[...new Set([...i.new_device_ids||[],...s])];this._serverCfg={...this._serverCfg,settings:{...i,known_devices:n,new_device_ids:e}},this._saveConfig()}}get _newIds(){const e=this._settings.new_device_ids;return new Set(Array.isArray(e)?e:[])}_ackNewDevice(e){if(!this._newIds.has(e)||!this._serverCfg)return;const t=this._settings;this._serverCfg={...this._serverCfg,settings:{...t,new_device_ids:(t.new_device_ids||[]).filter(t=>t!==e)}},this._saveConfig(),this.requestUpdate()}get _markers(){return this._serverCfg?.markers||[]}_roomLqi(e){if(!e)return null;const t=[];for(const i of this._renderDevices){if(i.area!==e||i.virtual)continue;const s=_o(this._renderPlanHass,i.entities);null!=s&&t.push(s)}return Ni(t)}_roomBounds(e){if(e.poly&&e.poly.length){const t=e.poly.map(e=>e[0]),i=e.poly.map(e=>e[1]),s=Math.min(...t),n=Math.min(...i);return{x:s,y:n,w:Math.max(...t)-s,h:Math.max(...i)-n}}return{x:e.x??0,y:e.y??0,w:e.w??0,h:e.h??0}}_defaultPositions(){const e={},t=this._config?.icon_size??2.5;for(const i of this._model){const s=t/100*nd(i)*1.3;for(const t of i.rooms){if(!t.area)continue;const n=this._devices.filter(e=>e.area===t.area&&e.space===i.id);if(!n.length)continue;const o=this._roomBounds(t),a=.1*Math.min(o.w,o.h),r=o.w-2*a,l=o.h-2*a,c=Math.max(1,Math.round(Math.sqrt(n.length*r/Math.max(l,1)))),h=Math.ceil(n.length/c),d=r/c,p=l/Math.max(h,1),u=n.map((e,t)=>({x:o.x+a+d*(t%c+.5),y:o.y+a+p*(Math.floor(t/c)+.5)}));Oi(u,o,s,.5*a),n.forEach((t,i)=>e[t.id]=Wh(u[i]))}}return e}_pos(e){const t=this._renderDeviceSnapshot?.positions.get(e.id);return t?{x:t.x,y:t.y}:this._livePos(e)}_livePos(e){const t=this._spaceModelById(e.space),i=this._layout[e.id];if(i)if(this._norm){if(i.s===e.space)return{x:i.x*Ov,y:i.y*Ov}}else if(void 0===i.s)return{x:i.x,y:i.y};return this._defPos[e.id]?this._defPos[e.id]:t?Wh(sd(t)):{x:500,y:500}}_savePos(e,t,i){if(this._spaceModelById(e.space)){if(this._norm){const s=this._gridPitch,n=Math.round(t/s)*s,o=Math.round(i/s)*s,a=this._layout[e.id]?.k;this._layout={...this._layout,[e.id]:{s:e.space,x:Gh(n/Ov),y:Gh(o/Ov),...a?{k:a}:{}}}}else this._layout={...this._layout,[e.id]:{x:Math.round(t),y:Math.round(i)}};this._dirtyPos.add(e.id),this._persistLayout()}}_visualSamples(e){return I_(this._renderPlanHass,e,this._renderDevices,void 0,this._fullRegistryHass).samples}_devicePresentation(e,t=!1!==this._config?.show_signal,i=!1){const s=i?null:this._renderDeviceSnapshot?.presentations.get(L_(e.id,t));return s||N_(this._renderPlanHass,e,{liveStates:!1!==this._config?.live_states,showTemperature:!1!==this._config?.show_temperature,showSignal:t,designPreview:i,activityRuntime:this._activityRt.get(e.id),sourceDetails:!1,lightDevices:this._renderDevices,registryHass:this._fullRegistryHass,reducedMotion:this._reducedMotion})}_deviceVisual(e){return this._devicePresentation(e).visual}_stateClass(e,t=this._deviceVisual(e)){const i=this._devicePresentation(e);return i.effectiveHidden?"":("icon_ripple"!==i.display||!1===this._config?.live_states||"alarm"===t.status||t.activity,F_({...i,visual:t}).join(" "))}_liveTemp(e){if(!this._config?.show_temperature)return null;if(!0===e.marker?.use_climate_temp){const t=go(this._renderPlanHass,e.entities);if(null!=t)return t}return"mdi:thermometer"!==e.icon&&"mdi:air-filter"!==e.icon?null:mo(this._renderPlanHass,e.entities)}_bindingEntities(e){if("virtual"===e||!e)return[];const t=this._bindingStatus(e);return"active"===t.kind?t.enabledEntityIds:t.allEntityIds}_bindingHasClimate(e){return this._bindingEntities(e).some(e=>e.startsWith("climate."))}_bindingHasAlarm(e){return this._bindingEntities(e).some(e=>{const t=this._planHass?.states?.[e]||this.hass?.states?.[e],i=this._fullRegistryHass.entities[e]||this.hass?.entities?.[e];return Bs(e.split(".")[0],t?.attributes?.device_class||i?.device_class||i?.original_device_class)})}_liveHum(e){return this._config?.show_temperature&&e.primary&&fo(this.hass,e.primary)?vo(this.hass,e.entities):null}_deviceBindingActive(e,t=!0){if(e.virtual||"virtual"===e.bindingKind)return!0;if(!e.bindingKind||!e.bindingRef)return!1;const i=this._bindingStatus(`${e.bindingKind}:${e.bindingRef}`);return"active"===i.kind||(t&&this._showToast(this._t("ha_disabled"===i.kind?"toast.ha_disabled_action":"toast.ha_binding_unverified")),!1)}_openMoreInfo(e){e?this._planEntityAvailable(e)?(this._nativeMoreInfoEntity=e,qv(this,"hass-more-info",{entityId:e})):this._showToast(this._t("toast.ha_disabled_action")):this._showToast(this._t("toast.no_entity"))}_interruptViewGesture(e,t){if(clearTimeout(this._holdTimer),clearTimeout(this._kioskHoldTimer),void 0!==e)for(const i of[t,this._stageEl])try{i?.hasPointerCapture?.(e)&&i.releasePointerCapture(e)}catch{}this._pointers.clear(),this._panStart=null,this._panLock=null,this._pinchStart=null,this._swipeStart=null}_closeInfoCard(){this._interruptViewGesture(),this._holdFired=!1,this._infoCard=null}_ctxDevice(e,t){"view"===this._mode&&(e.preventDefault(),e.stopPropagation(),this._deviceBindingActive(t)&&(t.primary?this._openMoreInfo(t.primary):this._infoCard=t))}_clickDevice(e,t){if(e.stopPropagation(),this._drag?.moved||this._suppressClick||this._holdFired)return;if("plan"===this._mode)return;if("devices"===this._mode)return void this._openMarkerDialog(t);const i=this._devices.find(e=>e.id===t.id);if(!i)return;const s=Vp(i.tapAction,i.primary?.split(".")[0]),n=(e,t)=>{i.marker?.tap_confirm?this._tapConfirm={kind:"run",text:e,exec:t}:t()};if("toggle"===s){const e=this._toggleIntent(i);if(!e)return;if(!Qp(e))return void this._showUnavailableToggleTargets(e);const t=e=>{const t=Qp(e);if(!t)return;if("virtual-light"===t.kind)return this._startDevicePressFeedback(i.id),void this.hass.callWS({type:"houseplan/virtual_light/toggle",marker_id:t.markerId}).then(e=>{const t=wn(this._virtualLights,e);t!==this._virtualLights&&(this._virtualLights=t,this._capturedSnapshotVirtual="",this._cacheSnapshot(),this.requestUpdate())}).catch(e=>this._showToast(this._t("toast.virtual_light_toggle_failed",{err:this._errText(e)})));const{command:s}=t;this._startDevicePressFeedback(i.id),this.hass.callService(s.domain,s.service,s.data).catch(e=>this._showToast(this._t("toast.error",{err:this._errText(e)})))},s=((o=e).targets.length?o.targets:o.skippedTargets).map(e=>e.name||e.entityId||e.ref).join(", ")||i.name;if(i.marker?.tap_confirm){const n=this._toggleConfirmationLines(e);if(!n.length)return;this._tapConfirm={kind:"toggle",text:this._t("confirm.tap_toggle",{name:s}),lines:n,initialIntent:e,deviceId:i.id,exec:()=>{const s=this._devices.find(e=>e.id===i.id),n=s?this._toggleIntent(s):null;n&&!Qp(n)&&this._showUnavailableToggleTargets(n)||(n&&tu(e,n)?t(n):this._showToast(this._t("toast.tap_target_changed")))}}}else t(e);return}var o;if("info"===s)return this._interruptViewGesture(),void(this._infoCard=i);if(this._deviceBindingActive(i)){if("run"===s){const e=i.marker?.tap_target||"",t=function(e){const t=String(e||"").split(".")[0];return"automation"===t?{domain:"automation",service:"trigger"}:"script"===t?{domain:"script",service:"turn_on"}:"scene"===t?{domain:"scene",service:"turn_on"}:null}(e),s=this.hass.states[e];if(!t||!s)return void this._showToast(this._t("toast.run_target_missing"));const o=s.attributes?.friendly_name||e;return void n(this._t("confirm.tap_run",{name:o}),()=>{this._deviceBindingActive(i)&&this._planEntityAvailable(e)&&(this._startDevicePressFeedback(i.id),this.hass.callService(t.domain,t.service,{entity_id:e}).then(()=>{this._stampActivity(i.id,"event",this._activitySourceKey(i)),this.requestUpdate(),this._showToast(this._t("toast.run_started",{name:o}))}).catch(e=>this._showToast(this._t("toast.error",{err:this._errText(e)}))))})}"more-info"===s&&i.primary?this._openMoreInfo(i.primary):this._infoCard=i}}_showUnavailableToggleTargets(e){const t=function(e){if(!e||"group"!==e.kind||Qp(e)||"configured-targets-missing"!==e.noneReason)return[];if(!e.skippedTargets.length||e.skippedTargets.some(e=>!eu.has(e.reason)&&"secure"!==e.reason))return[];const t=[],i=new Set;for(const s of e.skippedTargets){if(!eu.has(s.reason))continue;const e=String(s.name||s.entityId||s.ref||"").trim();e&&!i.has(e)&&(i.add(e),t.push(e))}return t}(e);return!!t.length&&(this._showToast(1===t.length?this._t("toast.toggle_target_unavailable",{name:t[0]}):this._t("toast.toggle_targets_unavailable",{names:t.join(", ")})),!0)}_keyDevice(e,t){"Enter"!==e.key&&" "!==e.key||"view"!==this._mode&&"devices"!==this._mode||(e.preventDefault(),this._clickDevice(e,t))}_startDevicePressFeedback(e){const t=[...this.renderRoot.querySelectorAll(".dev[data-id]")].find(t=>t.dataset.id===e),i=t?.querySelector(".device-shell");if(!i||"function"!=typeof i.animate)return;const s=Number.parseFloat(this.ownerDocument.defaultView?.getComputedStyle(i).scale||""),n=Number.isFinite(s)?Math.max(.95,Math.min(1,s)):1;this._devicePressAnimations.get(e)?.cancel();const o=this._reducedMotion?[{outlineColor:"transparent",outlineStyle:"solid",outlineWidth:"0px"},{outlineColor:"var(--hp-accent, #3ea6ff)",outlineStyle:"solid",outlineWidth:"2px",outlineOffset:"2px",offset:.5},{outlineColor:"transparent",outlineStyle:"solid",outlineWidth:"0px"}]:[{scale:String(n)},{scale:"0.95",offset:.5},{scale:"1"}],a=i.animate(o,{duration:200,easing:"cubic-bezier(.22,.61,.36,1)"});this._devicePressAnimations.set(e,a);const r=()=>{this._devicePressAnimations.get(e)===a&&this._devicePressAnimations.delete(e)};a.addEventListener("finish",r,{once:!0}),a.addEventListener("cancel",r,{once:!0})}_cancelDevicePressFeedback(){for(const e of this._devicePressAnimations.values())e.cancel();this._devicePressAnimations.clear()}_pruneDevicePressFeedback(){if(!this._devicePressAnimations.size)return;const e=new Set([...this.renderRoot.querySelectorAll(".dev[data-id]")].map(e=>e.dataset.id||""));for(const[t,i]of this._devicePressAnimations)e.has(t)||(i.cancel(),this._devicePressAnimations.delete(t))}_t(e,t){return ua(pa(this.hass,this._config?.language),e,t)}get _colorPickerLabels(){return{title:this._t("color_picker.title"),hue:this._t("color_picker.hue"),saturation:this._t("color_picker.saturation"),value:this._t("color_picker.value"),hex:this._t("color_picker.hex"),invalidHex:this._t("color_picker.invalid_hex")}}_help(e){const t=`${e}.aria`,i=pa(this.hass,this._config?.language);return _a(i,e)&&_a(i,t)?q``:G}get _stageEl(){return this.renderRoot.querySelector(".stage")}_contentItems(e){const t=[];for(const i of this._devices){if(i.space!==e.id||i.hidden)continue;const s=this._pos(i);t.push({minX:s.x,minY:s.y,maxX:s.x,maxY:s.y})}if(e.id===this._space){for(const e of this._openingsR){const i=Number(e.angle)*Math.PI/180,s=Math.cos(i)*e.rlen/2,n=Math.sin(i)*e.rlen/2,o=Yh([[e.rx-s,e.ry-n],[e.rx+s,e.ry+n]]);o&&t.push(o)}const i=this._decorH;for(const e of this._decorList){const s=Yh("line"===e.kind?[[e.x1*Ov,e.y1*i],[e.x2*Ov,e.y2*i]]:"text"===e.kind?[[e.x*Ov,e.y*i]]:Yr({x:e.x*Ov,y:e.y*i,w:e.w*Ov,h:e.h*i,angle:e.angle}));s&&t.push(s)}for(const i of this._physicalBodiesR(e)){const e=Yh(i);e&&t.push(e)}}return Xh(e,t)}_frameOf(){const e=this._spaceModel();if(!e){this._frame=null;const e={x:0,y:0,w:Ov,h:Ov};return{rect:e,all:e,outliers:0}}const t=this._frame,i="view"!==this._mode;if(t&&t.id===e.id&&this._bdDrag)return t;if(t&&t.id===e.id&&t.model===e&&t.layout===this._layout&&t.devs===this._devices&&t.far===this._showFar&&t.grow===i)return t;const s=td(this._contentItems(e));let n=s.all||id(e),o=this._showFar?n:s.core||id(e);return t&&t.id===e.id&&i&&t.grow&&(o=Lv(t.rect,o),n=Lv(t.all,n)),this._frame={id:e.id,model:e,layout:this._layout,devs:this._devices,far:this._showFar,grow:i,rect:o,all:n,outliers:s.outliers},this._frame}_isoSource(){const e=this._spaceModel();if(!e)return null;const t=this._spaceWalls,i=this._openCuts(),s=this._openingsR.map((e,t)=>({id:String(e.id||t),sourceIndex:t,type:e.type,x:e.rx,y:e.ry,angle:Number(e.angle)||0,length:e.rlen>0?e.rlen:900,flipH:!!e.flip_h,flipV:!!e.flip_v})),n=Dh(64,this._cellCm),o=Dh(10,this._cellCm),a=`${e.id}|${function(e){let t=2166136261;const i=e=>{if("number"!=typeof e)if("string"!=typeof e)if("boolean"!=typeof e)if(Array.isArray(e)){t=qf(t,e.length);for(const t of e)i(t)}else{if(e&&"object"==typeof e){const t=e;for(const e of Object.keys(t).sort())i(e),i(t[e]);return}t=qf(t,-1)}else t=qf(t,e?1:0);else for(const i of e)t=qf(t,i.codePointAt(0)||0);else t=qf(t,e)};return i(e),t.toString(36)}({rooms:e.rooms,walls:t,openCuts:i,openings:s,partitions:e.partitions,roomDrafts:e.room_drafts,columns:e.wall_columns,cellCm:this._cellCm,gridPitch:this._gridPitch,wallKeyPitch:this._wallKeyPitch,camera:If,wallHeight:n,floorEdgeHeight:o,algorithm:3})}`;return{key:a,build:()=>{const o=_h(e,this._cellCm,this._gridPitch,2e-4*this._gridPitch,this._partitionOpeningCuts(e)).all,a=this._roomWallOpeningInputs(this._openingsR,e),r=t.length||o.length?qc(e.rooms,t,i,a,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov,o):null;if(r&&("failed-core"===r.status||"not-applicable"===r.status))throw new Error("wall boolean geometry failed");const l=r?.paperGeom??Hc(e.rooms,t,i,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov);if(!l)throw new Error("floor boolean geometry failed");const c=r?.openingIndex||Kc(e.rooms,t,i,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov),h=s.map(e=>{const i=this._openingsR[e.sourceIndex],s="gate"===e.type?!e.flipV:e.flipV,o=i?.partitionHost?cu(i.partitionHost,s):t.length||"gate"===e.type?Zc(c,{x:e.x,y:e.y,angle:e.angle,length:e.length,flip_v:s}):{ox:0,oy:0,cm:0,side:-1};return function(e,t=64){if(!(Wf([e.x,e.y,e.angle,e.length,e.face.ox,e.face.oy,t])&&e.length>0&&t>0))throw new Error("invalid isometric opening input");const i=e.length/2;let s;if("passage"===e.type)s=[];else if("gate"===e.type){const n=10*e.face.side;s=[Gf(e,0,[-i,0],[i,0],n,0,.88*t),Gf(e,1,[i,0],[-i,0],-n,0,.88*t)]}else s="window"===e.type?[Gf(e,0,[-i,0],[i,0],-90,.27*t,.78*t),Gf(e,1,[i,0],[-i,0],90,.27*t,.78*t)]:[Gf(e,0,[-i,0],[e.length,0],-90,0,.92*t)];return{id:e.id,sourceIndex:e.sourceIndex,type:e.type,leaves:Object.freeze(s.map(e=>Object.freeze(e)))}}({...e,face:o},n)});return{walls:r?.components.flatMap(e=>e.geom)||[],floor:l,openings:Object.freeze(h)}}}}_isoSceneKey(){if(!this._labsIso||"view"!==this._mode)return null;try{return this._isoSource()?.key??null}catch{return`${this._space}|invalid`}}_isoScene(){const e=this._isoSource();if(!e)return null;const t=this._isoGeometryCache.get(e.key);if(t)return{key:e.key,...t};const i=this._frameOf().rect,s=e.build(),n=Dh(64,this._cellCm),o=Dh(10,this._cellCm),a=function(e){let t=1/0,i=1/0,s=-1/0,n=-1/0;for(const o of e)for(const e of o.leaves){const o=Math.hypot(e.closedVector[0],e.closedVector[1]);t=Math.min(t,e.hinge[0]-o),i=Math.min(i,e.hinge[1]-o),s=Math.max(s,e.hinge[0]+o),n=Math.max(n,e.hinge[1]+o)}return Wf([t,i,s,n])?{x:t,y:i,w:s-t,h:n-i}:null}(s.openings),r=Nf({rect:a?Lv(i,a):i,wallHeight:n,openingHeight:n,floorDepth:o}),l=function(e,t=If,i=64){if(!Number.isFinite(i)||i<0)throw new Error("invalid wall height");const s=[],n=[],o=[];let a=0;for(let r=0;r<(e||[]).length;r++){const l=e[r];for(let e=0;e<(l||[]).length;e++){const c=Hf(l[e],e>0);if(!(c.length<3||Math.abs(Of(c))<1e-9)){s.push(Lf(c,t,i)),a+=c.length;for(let s=0;se.depth-t.depth||e.polygon-t.polygon||e.ring-t.ring||e.edge-t.edge),{topPath:s.join(" "),sides:n,contactPath:o.join(" "),edgeCount:a}}(s.walls,If,n),c={geometry:l,floor:jf(s.floor,o),openings:s.openings,frame:r};return Mv(this._isoGeometryCache,e.key,c,8),{key:e.key,...c}}_effectiveProjection(){if("iso"!==this._desiredProjection||!this._model.length)return"flat";const e=this._isoSceneKey()||`${this._space}|invalid`;if(this._isoFallback.has(e))return"flat";try{const e=this._isoScene();return e?(t="iso",i=e.key,s=this._isoFallback,"iso"===t&&i&&!s.has(i)?"iso":"flat"):"flat"}catch(t){if(!this._isoFallback.has(e)){this._isoFallback.add(e);const i=e.split("|");console.warn(`HOUSEPLAN ISO FALLBACK: #89, space ${this._space}, fingerprint ${i[i.length-1]}, ${t instanceof Error?t.message:"renderer error"}`)}return"flat"}var t,i,s}_scenePoint(e){return"iso"===this._renderProjection?Af(e,0):e}_floorView(e){if("iso"!==this._renderProjection)return e;const t=zf([e.x,e.y]),i=zf([e.x+e.w,e.y+e.h]);return{x:t[0],y:t[1],w:i[0]-t[0],h:i[1]-t[1]}}_baseVb(){if("iso"===this._effectiveProjection()){if(!this._spaceDisplayForRender().showBorders){const e=Nf({rect:this._frameOf().rect,wallHeight:Dh(64,this._cellCm)});return[e.x,e.y,e.w,e.h]}const e=this._isoScene()?.frame??this._frameOf().rect;return[e.x,e.y,e.w,e.h]}const e=this._frameOf().rect;return[e.x,e.y,e.w,e.h]}get _outliers(){return this._showFar?0:this._frameOf().outliers}_fitFar(){this._showFar=!0,this._frame=null,this._resetZoom()}_fitAll(){this._showFar=!0,this._frame=null,this._resetZoom()}_renderFarHint(){return this._kiosk||"view"!==this._mode||this._booting||!this._outliers?G:q`
- - ${this._t("canvas.far_objects",{n:this._outliers})} - -
`}_renderHomeArrow(){if(this._booting)return G;const e=this._view;if(!e||!e.w||!e.h)return G;const t=this._baseVb(),i=t[0],s=t[1],n=t[2],o=t[3];if(!(i+n<=e.x||i>=e.x+e.w||s+o<=e.y||s>=e.y+e.h))return G;const a=Math.atan2(s+o/2-(e.y+e.h/2),i+n/2-(e.x+e.w/2)),r=50+38*Math.cos(a),l=50+38*Math.sin(a);return q``}_stageAspect(){const e=this._stageEl,t=this._baseVb();return e&&e.clientHeight?e.clientWidth/e.clientHeight:t[2]/t[3]}_viewOr(e){return this._view&&this._view.w?this._view:Ei(e,this._stageAspect())}_roomLabelReferenceViewWidth(e){if(!this._markup)return e.w;const t=this._stageEl,i=this.renderRoot.querySelector(".editorchrome");if(!t||t.clientWidth<=0||t.clientHeight<=0)return e.w;const s=t.clientHeight+(i?.getBoundingClientRect().height||0);return s<=0?e.w:this._viewForModeTarget(this._zoom,void 0,void 0,t.clientWidth,s).w}_screenToVb(e,t){const i=this._stageEl,s=this._viewOr(this._baseVb()),n=i?.clientWidth||1,o=i?.clientHeight||1;return[s.x+e/n*s.w,s.y+t/o*s.h]}_clampView(e,t){const i=(e,t,i,s)=>{const n=1*Math.max(t,s),o=i-n,a=i+s-t+n;return Math.max(Math.min(o,a),Math.min(Math.max(o,a),e))};return{w:e.w,h:e.h,x:i(e.x,e.w,t.x,t.w),y:i(e.y,e.h,t.y,t.h)}}_applyView(e,t,i){const s=this._baseVb(),n=Ei(s,this._stageAspect()),o=Math.min(Vv.ZOOM_MAX,Math.max(Vv.ZOOM_MIN,e)),a=n.w/o,r=n.h/o,l=this._viewOr(s),c=t??l.x+l.w/2,h=i??l.y+l.h/2,d=this._clampView({x:c-a/2,y:h-r/2,w:a,h:r},n);return!(this._view&&Math.abs(this._zoom-o)<1e-9&&Math.abs(this._view.x-d.x)<1e-6&&Math.abs(this._view.y-d.y)<1e-6&&Math.abs(this._view.w-d.w)<1e-6&&Math.abs(this._view.h-d.h)<1e-6)&&("opening"===this._tool&&(this._cursorPt=null,this._clearOpeningPlacement(!1)),this._zoom=o,this._view=d,!0)}_bootWatch(){clearTimeout(this._bootTimer),this._bootStart=Date.now(),this._bootLastH=-1,this._bootLastChange=this._bootStart;const e=()=>{if(!this._booting)return;const t=Date.now(),i=this._stageEl?this._stageEl.clientHeight:0;i!==this._bootLastH&&(this._bootLastH=i,this._bootLastChange=t);const s=t-this._bootStart;s>=1200||s>=700&&i>0&&t-this._bootLastChange>=250?this._bootSettled():this._bootTimer=window.setTimeout(e,100)};this._bootTimer=window.setTimeout(e,100)}_bootSettled(){this._booting&&!this._bootSettling&&(this._bootSettling=!0,this._refitView(),this._bootSettleRaf=requestAnimationFrame(()=>{this._bootSettleRaf=0,this._finishBootSettled()}))}_finishBootSettled(){if(!this._booting)return;this._refitView(),this._booting=!1,this._bootSettling=!1;const e=this._stageEl?.clientHeight??0;!this._config?.kiosk&&e>0&&this._warmPatch({hdrH:this._hdrH,stageH:e,vp:this._warmViewportState()},!0),this._bootFading=!0,this._bootTimer=window.setTimeout(()=>{this._bootFading=!1},220),this._bootSoft=!0,clearTimeout(this._bootSoftTimer),this._bootSoftTimer=window.setTimeout(()=>{this._bootSoft=!1},xv)}_bootSoftCancel(){this._bootSoft&&(clearTimeout(this._bootSoftTimer),this._bootSoft=!1)}_beginResumeSettle(){this._booting||this._resumeSettling||(this._modeTransitionPreparing?this._modeTransitionForceAtomic=!0:this._cancelModeTransition(!0),this._viewportInvalidAt=0,this._beginContinuityCandidate("warm-resume",!1),this._loading?this.requestUpdate():this._loadFromServer())}_refitView(){if(this._modeTransitionBusy)return;const e=this._stageEl;if(!e||"visible"!==document.visibilityState||e.clientWidth<=0||e.clientHeight<=0)return void(this._viewportInvalidAt||(this._viewportInvalidAt=Date.now()));const t=[e.clientWidth,e.clientHeight],i=this._lastValidStageSize,s=!!i&&Math.abs(i[0]-t[0])<=.5&&Math.abs(i[1]-t[1])<=.5,n=this._viewportInvalidAt?Date.now()-this._viewportInvalidAt:0;if(this._viewportInvalidAt=0,!i)return this._lastValidStageSize=t,void(this._view||this._applyView(this._zoom));s?this._pendingRefitSize=null:(this._pendingRefitSize=t,this._refitRaf||(this._refitRaf=requestAnimationFrame(()=>{this._refitRaf=0;const e=this._pendingRefitSize;this._pendingRefitSize=null;const t=this._stageEl;if(!e||!t||t.clientWidth<=0||t.clientHeight<=0)return;if(Math.abs(t.clientWidth-e[0])>.5||Math.abs(t.clientHeight-e[1])>.5)return void this._refitView();const i=this._lastValidStageSize;if(i&&Math.abs(i[0]-e[0])<=.5&&Math.abs(i[1]-e[1])<=.5)return;this._lastValidStageSize=e;const s=this._view;n>=Wd?this._beginContinuityCandidate("stage-size-restored",!0,"stage-size"):this._continuity.hasCompleteFrame&&this._beginContinuityCandidate("stage-resize",!0,"stage-size"),this._applyView(this._zoom,s?s.x+s.w/2:void 0,s?s.y+s.h/2:void 0)})))}_zoomAt(e,t,i){const s=this._stageEl;if(!s)return;const n=Ei(this._baseVb(),this._stageAspect()),o=Math.min(Vv.ZOOM_MAX,Math.max(Vv.ZOOM_MIN,i));"opening"===this._tool&&(this._cursorPt=null,this._clearOpeningPlacement(!1));const a=s.clientWidth,r=s.clientHeight,l=this._screenToVb(e,t),c=n.w/o,h=n.h/o;this._zoom=o,this._view=this._clampView({x:l[0]-e/a*c,y:l[1]-t/r*h,w:c,h:h},n)}_onWheel(e){const t=this._stageEl;if(!t)return;e.preventDefault();const i=t.getBoundingClientRect(),s=e.deltaY<0?1.15:1/1.15;this._zoomAt(e.clientX-i.left,e.clientY-i.top,this._zoom*s),this._saveZoom()}_stepZoom(e){const t=this._stageEl;t&&(this._zoomAt(t.clientWidth/2,t.clientHeight/2,this._zoom*(e>0?1.4:1/1.4)),this._saveZoom())}_resetZoom(){const e=this._baseVb();"opening"===this._tool&&(this._cursorPt=null,this._clearOpeningPlacement(!1)),this._zoom=1,this._view=Ei(e,this._stageAspect()),this._saveZoom()}_saveZoom(){if("view"===this._mode){this._zoomBySpace={...this._zoomBySpace,[this._space]:this._zoom};try{localStorage.setItem(Av,JSON.stringify(this._zoomBySpace))}catch{}}}_restoreZoom(){const e=this._zoomBySpace[this._space]||1;this._zoom=e;const t=this._stageEl;if(t&&t.clientHeight){const t=this._baseVb();return this._applyView(e,t[0]+t[2]/2,t[1]+t[3]/2),void this.requestUpdate()}this._view=null,requestAnimationFrame(()=>{if(!this._stageEl)return;const t=this._baseVb();this._applyView(e,t[0]+t[2]/2,t[1]+t[3]/2),this.requestUpdate()})}_stagePointerDown(e){if(this._vacFit)return;if(this._kiosk&&(this._cyclePausedUntil=Date.now()+6e4,0===this._pointers.size?(this._swipeStart={x:e.clientX,y:e.clientY,id:e.pointerId},e.target.closest?.(".dev, .roomlabel, .oplock")||(clearTimeout(this._kioskHoldTimer),this._kioskHoldTimer=window.setTimeout(()=>{this._kioskDialog=!0,this._swipeStart=null},3e3))):(this._swipeStart=null,clearTimeout(this._kioskHoldTimer))),this._drag)return;if(this._markup&&e.target.closest?.(".roomlabel, .rlhandle, .rszhandle, .dev, .oplock, .op-hit, button"))return;if("devices"===this._mode&&e.target.closest(".dev"))return;if("decor"===this._mode&&this._decorPointerDown(e))return;this._pointers.set(e.pointerId,{x:e.clientX,y:e.clientY});const t=this._viewOr(this._baseVb());if(1===this._pointers.size)this._panStart={sx:e.clientX,sy:e.clientY,vx:t.x,vy:t.y},this._panLock=null,this._suppressClick=!1;else if(2===this._pointers.size){"opening"===this._tool?(this._cursorPt=null,this._clearOpeningPlacement(!1)):"draw"===this._tool&&this._clearPlanSnapHover();const e=[...this._pointers.values()],t=Math.hypot(e[0].x-e[1].x,e[0].y-e[1].y);this._pinchStart={dist:t,zoom:this._zoom},this._panStart=null,this._panLock=null}}get _swipeZone(){return!this._hasFixedFloor&&this._kiosk&&this._zoom<=1.001&&this._model.length>1}_stagePointerMove(e){if(this._physicalRotate?.pid!==e.pointerId)if(this._physicalDrag?.pid!==e.pointerId)if(this._dtDrag?.pid!==e.pointerId)if(this._bdDrag?.pid!==e.pointerId){if(this._decorDraft?.pid===e.pointerId){const t=this._decorDraft;let i=this._decorSnap(this._svgPoint(e),e.pointerType);if(e.shiftKey&&("rect"===t.kind||"ellipse"===t.kind)){const e=i[0]-t.a[0],s=i[1]-t.a[1],n=Math.max(Math.abs(e),Math.abs(s));i=this._snap([t.a[0]+(e<0?-n:n),t.a[1]+(s<0?-n:n)])}return void(this._decorDraft={...t,b:i})}if(this._decorMove?.pid!==e.pointerId)if(this._pointers.has(e.pointerId)){if(this._pointers.set(e.pointerId,{x:e.clientX,y:e.clientY}),this._markup&&1===this._pointers.size&&this._markupMove(e),this._pinchStart&&this._pointers.size>=2){"opening"===this._tool?(this._cursorPt=null,this._clearOpeningPlacement(!1)):"draw"===this._tool&&this._clearPlanSnapHover();const e=[...this._pointers.values()],t=Math.hypot(e[0].x-e[1].x,e[0].y-e[1].y)/(this._pinchStart.dist||1),i=this._stageEl.getBoundingClientRect(),s=(e[0].x+e[1].x)/2-i.left,n=(e[0].y+e[1].y)/2-i.top;this._zoomAt(s,n,this._pinchStart.zoom*t),this._suppressClick=!0,this._saveZoom()}else if(this._panStart){const t=e.clientX-this._panStart.sx,i=e.clientY-this._panStart.sy;Math.abs(t)+Math.abs(i)>4&&(this._suppressClick=!0,clearTimeout(this._holdTimer),"opening"===this._tool?(this._cursorPt=null,this._clearOpeningPlacement(!1)):"draw"===this._tool&&this._clearPlanSnapHover()),null===this._panLock&&Math.abs(t)+Math.abs(i)>8&&(this._panLock=this._swipeZone&&Math.abs(t)>1.5*Math.abs(i)?"swipe":"pan");const s=this._stageEl;if("pan"===this._panLock&&s){const e=this._baseVb(),n=this._viewOr(e),o=Ei(e,this._stageAspect());this._view=this._clampView({x:this._panStart.vx-t/(s.clientWidth||1)*n.w,y:this._panStart.vy-i/(s.clientHeight||1)*n.h,w:n.w,h:n.h},o)}}}else this._markupMove(e);else this._decorMoveUpdate(e)}else this._bdMove(e);else this._dtMove(e);else this._physicalMove(e);else this._physicalRotateMove(e)}_stagePointerLeave(e){this._markup&&("opening"===this._tool?(this._cursorPt=null,this._clearOpeningPlacement(!1)):"draw"===this._tool&&this._clearPlanSnapHover())}_stagePointerUp(e){if(this._kiosk){clearTimeout(this._kioskHoldTimer);const t=this._swipeStart;if(this._swipeStart=null,t&&t.id===e.pointerId){const i=e.clientX-t.x,s=e.clientY-t.y;if(Math.abs(i)+Math.abs(s)<8){const e=Date.now();e-this._lastTap<350&&this._resetZoom(),this._lastTap=e}const n="pan"===this._panLock?null:function(e,t,i,s,n,o=60){if(i>1.001||s.length<2)return null;if(Math.abs(e)e.id),this._space);n&&this._slideTo(n,i<0?"left":"right")&&(this._saveNav(),this._suppressClick=!0,setTimeout(()=>this._suppressClick=!1,0),this._showKioskDots())}}if(this._physicalDrag?.pid===e.pointerId)return void this._physicalUp(e);if(this._physicalRotate?.pid===e.pointerId)return void this._physicalRotateUp(e);if(this._dtDrag?.pid===e.pointerId)return void this._dtUp();if(this._bdDrag?.pid===e.pointerId)return void this._bdUp();if(this._decorDraft?.pid===e.pointerId)return void this._decorCommitDraft();if(this._decorMove?.pid===e.pointerId)return this._decorMove.moved&&(this._recordGeometry(this._t("history.decor_move"),this._decorMove.before),this._saveConfig()),void(this._decorMove=null);const t=!!this._pinchStart||!!this._panStart;this._pointers.delete(e.pointerId),this._pointers.size<2&&(this._pinchStart=null),0===this._pointers.size&&(this._panStart=null,this._panLock=null,setTimeout(()=>this._suppressClick=!1,0)),t&&0===this._pointers.size&&this.requestUpdate()}_clickRoom(e){!this._suppressClick&&e.area&&Wv("/config/areas/area/"+e.area)}_pointerDown(e,t){if("plan"===this._mode)return;if("view"===this._mode){this._holdFired=!1,clearTimeout(this._holdTimer);const i=e.pointerId,s=e.currentTarget;return void(this._holdTimer=window.setTimeout(()=>{this._holdFired=!0,this._interruptViewGesture(i,s),this._infoCard=t},600))}if("ha_disabled"===t.bindingStatus?.kind)return;e.preventDefault();const i=this._pos(t);this._drag={id:t.id,sx:e.clientX,sy:e.clientY,ox:i.x,oy:i.y,moved:!1},Gv(e),this._tip=null}_pointerMove(e,t){if(!this._drag||this._drag.id!==t.id)return;const i=this.renderRoot.querySelector(".stage");if(!i)return;const s=this._baseVb(),n=i.getBoundingClientRect(),o=this._viewOr(s),a=(e.clientX-this._drag.sx)/n.width*o.w,r=(e.clientY-this._drag.sy)/n.height*o.h;Math.abs(e.clientX-this._drag.sx)+Math.abs(e.clientY-this._drag.sy)>3&&(this._drag.moved=!0,clearTimeout(this._holdTimer));const l=Uh(this._drag.ox+a),c=Uh(this._drag.oy+r);this._savePos(t,l,c)}_pointerUp(e,t){if(clearTimeout(this._holdTimer),!this._drag||this._drag.id!==t.id)return;const i=this._drag.moved;this._drag=i?this._drag:null,i&&(this._selId=t.id,window.setTimeout(()=>this._drag=null,0))}_showToast(e){for(const e of this.renderRoot.querySelectorAll("hp-dialog"))e.closeTransientOverlays("toast");this._toast=e,clearTimeout(this._toastTimer),this._toastTimer=window.setTimeout(()=>{this._toast=""},3500)}_syncPointerHoverTargets(){const e=this._pointerModality.hoverEnabled;for(const t of this.renderRoot.querySelectorAll(Ev))t.toggleAttribute("data-pointer-hover",e)}_syncPointerHoverSubtree(e){if(e.nodeType!==Node.ELEMENT_NODE)return;const t=e,i=this._pointerModality.hoverEnabled;t.matches(Ev)&&t.toggleAttribute("data-pointer-hover",i);for(const e of t.querySelectorAll(Ev))e.toggleAttribute("data-pointer-hover",i)}_clearTransientHover(e=!1){e&&this._pointerModality.suspend(),this._tip&&(this._tip=null),this._hoverRoom&&(this._hoverRoom=null)}_notePointer(e){const t=this._pointerModality.modality,i=this._pointerModality.note(e);"touch"!==i&&"pen"!==i||t===i&&!this._tip&&!this._hoverRoom||this._clearTransientHover()}_guardTouchGesture(e){if(this._editorSecondary.handleOutsideDismiss(e))return;if("click"===e.type){if(!this._suppressClick&&!this._touchSequenceMultitouch&&Date.now()>this._touchClickBlockUntil)return;return e.preventDefault(),void e.stopImmediatePropagation()}const t=e;if(this._notePointer(t),"touch"===t.pointerType)if("pointerdown"!==e.type){if("pointermove"===e.type){const e=this._touchContacts.get(t.pointerId);if(!e)return;const i={...e,x:t.clientX,y:t.clientY};if(this._touchContacts.set(t.pointerId,i),this._touchSequenceMultitouch&&"view"===this._mode&&!this._vacFit&&this._pinchStart&&[...this._touchContacts.values()].every(e=>e.inStage)){this._pointers.set(t.pointerId,{x:t.clientX,y:t.clientY});const e=[...this._touchContacts.values()];if(e.length>=2&&this._stageEl){const[t,i]=e,s=Math.hypot(t.x-i.x,t.y-i.y)/(this._pinchStart.dist||1),n=this._stageEl.getBoundingClientRect();this._zoomAt((t.x+i.x)/2-n.left,(t.y+i.y)/2-n.top,this._pinchStart.zoom*s),this._saveZoom()}}return}if("pointerup"===e.type||"pointercancel"===e.type||"lostpointercapture"===e.type){if(this._clearTransientHover(),this._touchContacts.delete(t.pointerId),this._touchSequenceMultitouch){if(this._touchClickBlockUntil=Date.now()+500,this._pointers.delete(t.pointerId),!this._vacFit&&this._pointers.size>=2){const[e,t]=[...this._pointers.values()];this._pinchStart={dist:Math.hypot(e.x-t.x,e.y-t.y),zoom:this._zoom}}else this._pinchStart=null;0===this._pointers.size&&(this._panStart=null,this._panLock=null)}0===this._touchContacts.size&&(this._touchSequenceMultitouch=!1)}}else if(this._touchContacts.set(t.pointerId,{x:t.clientX,y:t.clientY,inStage:!!t.target?.closest?.(".stage")}),this._touchContacts.size>=2){this._clearTransientHover(),this._touchSequenceMultitouch=!0,this._touchClickBlockUntil=Number.POSITIVE_INFINITY,clearTimeout(this._holdTimer),clearTimeout(this._kioskHoldTimer),this._swipeStart=null;const e=[...this._touchContacts.values()];if("view"===this._mode&&!this._vacFit&&e.every(e=>e.inStage)){this._pointers=new Map([...this._touchContacts].map(([e,t])=>[e,{x:t.x,y:t.y}]));const[t,i]=e;this._pinchStart={dist:Math.hypot(t.x-i.x,t.y-i.y),zoom:this._zoom},this._panStart=null,this._panLock=null}else this._vacFit&&(this._pointers.clear(),this._pinchStart=null,this._panStart=null,this._panLock=null)}}_showTip(e,t,i,s,n,o){this._notePointer(e),this._pointerModality.hoverEnabled&&(this._drag||(this._tip={x:e.clientX,y:e.clientY,title:t,meta:i,lqi:s,temp:n,hum:o}))}get _gridPitch(){return Bh}get _cellCm(){const e=Number(this._curSpaceCfg?.cell_cm);return Number.isFinite(e)&&e>0?e:5}_fmtLen(e,t){const i=function(e,t,i,s){return Math.hypot(t[0]-e[0],t[1]-e[1])/i*s}(e,t,this._gridPitch,this._cellCm);return li(i,"mi"===this.hass?.config?.unit_system?.length)}get _curSpaceCfg(){const e=this._resize.preview;return e&&e.space===this._space?e.sp:this._serverCfg?.spaces.find(e=>e.id===this._space)}get _renderCfg(){const e=this._resize.preview;return e&&this._serverCfg?{...this._serverCfg,spaces:this._serverCfg.spaces.map(t=>t.id===e.space?e.sp:t)}:this._serverCfg}get _spaceH(){return this._curSpaceCfg,Ov}get _segments(){const e=this._curSpaceCfg,t=this._spaceH;return pi(e?.rooms||[]).map(e=>[e[0]*Ov,e[1]*t,e[2]*Ov,e[3]*t])}_savedNav(){if(this._hasFixedFloor)return null;try{return JSON.parse(localStorage.getItem(zv)||"null")}catch{return null}}_saveNav(){if(!this._hasFixedFloor)try{localStorage.setItem(zv,JSON.stringify({space:this._space}))}catch{}}_leaveCardRoute(){if(this._routeDepartureHandled)return;this._routeDepartureHandled=!0,"view"!==this._mode&&this._setMode("view",!1),this._pendingNavMode=null,this._geometryHistory.clear(),this._activeDraftId=null,this._resumeDraftBySpace={},this._draftSegmentCms=[],this._closingWallCm=null,this._drawWallField=null,this._showHidden=!1,this._tapConfirm=null,this._vacCalConfirm=null,this._decorEraseConfirm=null,this._openingInfo=null,this._closeInfoCard(),this._rulesDialog=null,this._alignDialog=null,this._preflightClipboardFallback=null,this._backupImportDialog=null,this._backupExportDialog=null,this._settingsDialog=null,this._deviceInbox=null,this._deviceInboxReturn=null,this._markerDialog=null,this._openingDialog=null,this._physicalDialog=null,this._wallDialog=null,this._backdropDialog=null,this._decorShapeDialog=null,this._decorTextDialog=null,this._mergeDialog=null,this._roomDialog=!1,this._spaceDialog=null,this._importDialog=null;const e=this._warmSlot;e?.owner===this._warmGen&&(e.vp=this._warmViewportState(),e.dlg=null,e.frameFingerprint="",clearTimeout(e.evict),e.evict=0),this._saveNav()}_setMode(e,t=!0){this._endTabDrag(),this._pendingNavMode=null;const i=this._spaceModel();if(!i)return this._cancelModeTransition(!1),void(this._mode="view");if(this._kiosk&&"view"!==e)return;if(this._mode===e){if(!t&&"view"===e&&this._modeTransitionBusy){const t=this._currentModeVisual(e),i=this._modeTransitionTargetZoom,s=this._modeTransitionTargetCenterX,n=this._modeTransitionTargetCenterY;this._cancelModeTransition(!1),this._commitViewModeAtomic(t,i,s,n)}return}if(this._bootSoftCancel(),("plan"===e||"decor"===e)&&!this._norm)return void this._showToast(this._t("toast.markup_needs_server"));if(this._wallFaceBatch&&this._roomDialogCancel(),"plan"===this._mode&&"draw"===this._tool&&!this._finishWallChain())return;this._clearTransientHover(!0),this._cancelDevicePressFeedback();const s=this._mode,n=this._effectiveProjection(),o=this._currentModeVisual(s),a=this._modeTransitionBusy,r=o&&"view"!==s&&"view"!==e?{...o,toolbarContentOpacity:this._reducedMotion?1:.35}:o;this._cancelModeTransition(!1),this._editorSecondary.closeForNavigation(),(this._decorMove||this._dtDrag||this._bdDrag)&&this._cancelDecorGesture();const l=!i.bg&&"view"===e!=("view"===s),c=a&&"view"===s&&"view"!==e?this._modeTransitionEditorCamera:null;let h=c?.zoom??(a?this._modeTransitionTargetZoom:this._zoom),d=c?c.centerX:a?this._modeTransitionTargetCenterX:r?.viewport.centerX,p=c?c.centerY:a?this._modeTransitionTargetCenterY:r?.viewport.centerY;if("view"===s&&"view"!==e&&!a){const t=this._view;if(this._viewModeSnap={space:this._space,zoom:this._zoom,cx:t?this._logicalViewCenter(n)?.x:void 0,cy:t?this._logicalViewCenter(n)?.y:void 0},"iso"===n){const t=this._logicalViewCenter("iso");d=t?.x,p=t?.y,this._view=null,this._mode=e,this._applyView(this._zoom,t?.x,t?.y)}l&&(h=1,d=void 0,p=void 0)}if("plan"===s&&this._activeDraftId&&(this._resumeDraftBySpace[this._space]=this._activeDraftId),this._mode=e,"devices"===s&&"devices"!==e&&(this._showHidden=!1,this._deviceInbox=null,this._deviceInboxReturn=null,this._deviceInboxMemo=null),this._editorChromeMode="view"===e?s:e,"view"===e){"view"===s||a||(this._modeTransitionEditorCamera={zoom:this._zoom,centerX:r?.viewport.centerX,centerY:r?.viewport.centerY});const e=this._viewModeSnap;if(e&&e.space===this._space){h=e.zoom;const t=this._labsIso&&"iso"===this._viewPreference[this._space]?"iso":"flat",i=null!=e.cx&&null!=e.cy&&"iso"===t?Af([e.cx,e.cy],0):null;d=i?.[0]??e.cx,p=i?.[1]??e.cy}else e&&(h=this._zoomBySpace[this._space]||1,d=void 0,p=void 0)}else this._modeTransitionEditorCamera={zoom:h,centerX:d,centerY:p};this._modeTransitionTargetZoom=h,this._modeTransitionTargetCenterX=d,this._modeTransitionTargetCenterY=p,("iso"===n||"view"===e&&this._labsIso&&"iso"===this._viewPreference[this._space])&&(this._modeTransitionForceAtomic=!0);const u=++this._modeTransitionRequest;t?r?(this._modeTransitionPreparing=!0,this._modeTransitionVisual=r,this._prepareModeTransition(u,r,e,h,d,p)):(this._modeTransitionPreparing=!1,this._modeTransitionVisual=null,this._zoom=h,this._view=null,requestAnimationFrame(()=>{this.isConnected&&u===this._modeTransitionRequest&&this._mode===e&&(this._applyView(h,d,p),"view"===e&&(this._viewModeSnap=null,this._modeTransitionEditorCamera=null),this.requestUpdate())})):"view"===e&&this._commitViewModeAtomic(r,h,d,p),this._path=[],this._clearPlanSnapHover(),this._clearOpeningPlacement(!0),this._tool="draw",this._mergeSel=null,this._mergeDialog=null,this._splitSel=null,this._pendingSplit=null,this._selId=null,this._physicalSel=null,this._physicalDialog=null,this._physicalDrag=null,this._rszResetController(),this._tip=null,this._hoverRoom=null,this._decorDraft=null,this._decorSel=null,this._decorMove=null,this._backdropDialog=null,this._decorTool="select",this._bdDrag=null,this._dtDrag=null,this._dtBox=null,"plan"===e&&(this._primeDrawWallField(),this._resumeLastDraft()),this._saveNav()}_primeDrawWallField(){null===this._drawWallField&&(this._drawWallField=Sl(vl,this._imperial))}get _drawWallFieldValue(){return null===this._drawWallField?Sl(vl,this._imperial):this._drawWallField}get _drawWallCm(){const e=$v(this._drawWallFieldValue);if(null==e||e<0)return null;const t=this._imperial?2.54*e:e,i="column"===this._tool?ih:100;return t>=("column"===this._tool?1:0)&&t<=i?t:null}get _drawWallMaxCm(){return"column"===this._tool?ih:100}_showPhysicalRange(e=this._drawWallMaxCm,t=0){this._showToast(this._t("toast.physical_range",{min:Sl(t,this._imperial),max:Sl(e,this._imperial),unit:this._t(this._imperial?"wallthick.unit_in":"wallthick.unit_cm")}))}_draftSegmentCount(e=this._curSpaceCfg){return(e?.room_drafts||[]).reduce((e,t)=>e+(Array.isArray(t.segments)?t.segments.length:0),0)}_mergeSpacePartitions(e,t){const i=e?.partitions||[];if(i.length<2)return 0;const s=Hm(i,{pitch:jh,seedIds:t,geometry:Bm(e,{excludeDraftId:this._activeDraftId})});return s.merged?(e.partitions=s.partitions,Lm(e.openings,e.partitions,s.openingMoves,{coordScale:Ov,cellCm:this._cellCm,gridPitch:this._gridPitch}),s.merged):0}_finishWallChain(){if("draw"!==this._tool||this._wallFaceBatch||this._roomDialog)return!0;const e=this._curSpaceCfg;if(!e||this._path.length<2)return this._path=[],this._activeDraftId=null,this._draftSegmentCms=[],this._closingWallCm=null,this._clearPlanSnapHover(),!0;const t=Ru(this._path,Iu(this._path.length-1,this._draftSegmentCms,this._drawWallCm,vl)),i=t.length;if((e.partitions||[]).length+i>jv)return this._showToast(this._t("toast.physical_limit")),!1;const s=this._geometrySnapshot();e.partitions||=[];const n=Date.now().toString(36),o=[],a=this._activeDraftId?(e.room_drafts||[]).find(e=>e.id===this._activeDraftId):null,r=this._draftSegmentsForPath(this._path,a,t.map(e=>e.cm));for(let s=0;se.id!==this._activeDraftId),e.room_drafts.length||delete e.room_drafts),this._commitPhysicalGeometry(this._t("history.wall_chain_finish"),s),delete this._resumeDraftBySpace[this._space],this._path=[],this._activeDraftId=null,this._draftSegmentCms=[],this._closingWallCm=null,this._clearPlanSnapHover(),!0}_activateMarkupTool(e){e!==this._tool&&(this._wallFaceBatch&&this._roomDialogCancel(),("draw"!==this._tool||this._finishWallChain())&&(this._cancelPath(),"resize"===this._tool&&(this._resize.dragging?this._rszCancelDrag():this._resize.reset()),this._tool=e,"draw"===e&&this._resumeLastDraft(),"resize"===e&&this._resize.selectRoom(null),"wallthick"===e&&(this._wallDialog=null)))}_limitReached(e){const t=this._curSpaceCfg;if(!t)return!0;const i="draft"===e?(t.room_drafts||[]).length>=200:"partition"===e?(t.partitions||[]).length>=jv:(t.wall_columns||[]).length>=500;return i&&this._showToast(this._t("toast.physical_limit")),i}_svgPoint(e){const t=this.renderRoot.querySelector(".stage").getBoundingClientRect();return this._screenToVb(e.clientX-t.left,e.clientY-t.top)}_snap(e){const t=this._gridPitch;return[Uh(ri(e[0],t)),Uh(ri(e[1],t))]}_snapDrawPoint(e,t=!1){const i=this._path[this._path.length-1],s=t&&i?function(e,t,i,s=1/0){if(!(i>0&&e?.every(Number.isFinite)&&t?.every(Number.isFinite)))return[t[0],t[1]];const n=t[0]-e[0],o=t[1]-e[1];if(Math.abs(n)+Math.abs(o)<=1e-12)return[e[0],e[1]];const a=Math.PI/4;let r=Math.atan2(o,n);r<0&&(r+=2*Math.PI);const l=Math.floor(r/a+.5)%8,[c,h]=[[1,0],[1,1],[0,1],[-1,1],[-1,0],[-1,-1],[0,-1],[1,-1]][l],d=c*c+h*h;let p=Math.max(0,Math.round((n*c+o*h)/(i*d)));if(Number.isFinite(s)&&s>=0){const t=(e,t)=>{if(!t)return 1/0;const n=t>0?s-e:e+s;return Math.max(0,Math.floor((n+1e-9*i)/i))};p=Math.min(p,t(e[0],c),t(e[1],h))}return[e[0]+c*p*i,e[1]+h*p*i]}(i,e,this._gridPitch,Hh):e;return this._snap(s)}_planSnapOpeningCuts(e,t){if(!this._openingsR.length)return[];const i=this._openingWallIndexFor(e,t).value,s=[];for(const t of this._roomWallOpeningInputs(this._openingsR,e)){const e=Xc(i,t);if(!e.negative&&!e.positive)continue;const n=t.angle*Math.PI/180,o=Math.cos(n)*t.length/2,a=Math.sin(n)*t.length/2;s.push([t.x-o,t.y-a,t.x+o,t.y+a])}return s}_planSnapGeometrySnapshot(){const e=this._spaceModel();if(!e)return{key:`${this._space}|empty`,value:{segments:[],endpoints:[]}};const t=[this._space,this._cfgEpoch,this._activeDraftId||"",e.rooms.length,e.room_drafts.length,e.partitions.length].join("|");if(this._planSnapGeometryCache?.key===t)return this._planSnapGeometryCache;const i=this._openCuts(),s=xu({space:e,activeDraftId:this._activeDraftId,roomCuts:this._planSnapOpeningCuts(e,i),partitionCuts:this._partitionOpeningCuts(e),epsilon:2e-4*this._gridPitch});return this._planSnapGeometryCache={key:t,value:s},this._planSnapGeometryCache}_hiddenWallDiagnosticSnapshot(){const e=this._spaceModel();if(!e)return{key:`${this._space}|hidden-empty`,value:{segments:[],endpoints:[]}};const t=["hidden",this._space,this._cfgEpoch,this._activeDraftId||"",e.rooms.length,e.room_drafts.length,e.partitions.length].join("|");if(this._hiddenWallDiagnosticCache?.key===t)return this._hiddenWallDiagnosticCache;const i=ku({space:e,activeDraftId:this._activeDraftId,epsilon:2e-4*this._gridPitch});return this._hiddenWallDiagnosticCache={key:t,value:i},this._hiddenWallDiagnosticCache}_planStructuralGeometrySnapshot(){const e=this._spaceModel();if(!e)return{key:`${this._space}|structural-empty`,value:{segments:[],endpoints:[]}};const t=["structural",this._space,this._cfgEpoch,this._activeDraftId||"",e.rooms.length,e.room_drafts.length,e.partitions.length].join("|");if(this._planStructuralGeometryCache?.key===t)return this._planStructuralGeometryCache;const i=xu({space:e,activeDraftId:this._activeDraftId,epsilon:2e-4*this._gridPitch});return this._planStructuralGeometryCache={key:t,value:i},this._planStructuralGeometryCache}_planSnapContextKey(e){const t=this._path[0],i=this._path[this._path.length-1];return[e,this._tool,this._path.length,t?`${t[0]},${t[1]}`:"",i?`${i[0]},${i[1]}`:""].join("|")}_resolvePlanDrawPoint(e,t){const i=this._planSnapGeometrySnapshot(),s=this._path[this._path.length-1],n="draw"===this._tool&&this._path.length>=3?[{point:this._path[0],key:"closure:first-point"}]:[],o={tolerance:this._cssPxToRender(12),distinguishTolerance:this._cssPxToRender(8),gridStep:this._gridPitch,excludePoints:s?[s]:[],extraEndpoints:n,epsilon:2e-4*this._gridPitch},a=t&&s?Pu(i.value,e,{...o,anchor:s}):function(e,t,i){if(!(_u(t)&&i.tolerance>=0))return{kind:"none",candidate:null,conflicts:[]};const s=i.epsilon??uu,n=i.excludePoints||[],o=Cu(Mu(e,t,i),i.distinguishTolerance||0);if(o)return o;let a=null;for(const o of e.segments){const e=[o.a[0],o.a[1],o.b[0],o.b[1]],r=Hs([t[0],t[1]],e);if(r>i.tolerance)continue;const l=Du(o,t,i.gridStep);!$u(l,n,s)&&Su(r,o.key,a)&&(a={kind:"line",point:l,key:o.key,distance:r,segment:o})}return a?{kind:"resolved",candidate:a,conflicts:[]}:{kind:"none",candidate:null,conflicts:[]}}(i.value,e,o),r="resolved"===a.kind?a.candidate:null,l=r?[...r.point]:this._snapDrawPoint(e,t),c=s?va(s,l):l;return{point:c,candidate:r&&fi(c,r.point)?r:null,conflicts:"ambiguous"===a.kind?a.conflicts:[],ambiguous:"ambiguous"===a.kind,contextKey:this._planSnapContextKey(i.key)}}get _activePlanSnapCandidate(){if(!this._markup||"draw"!==this._tool)return null;const e=this._planSnapHover;if(!e)return null;const t=this._planSnapGeometrySnapshot();return e.contextKey===this._planSnapContextKey(t.key)?e.candidate:null}get _activePlanSnapConflicts(){if(!this._markup||"draw"!==this._tool)return[];const e=this._planSnapHover;if(!e)return[];const t=this._planSnapGeometrySnapshot();return e.contextKey===this._planSnapContextKey(t.key)?e.conflicts:[]}_clearPlanSnapHover(e=!0){this._planSnapHover=null,this._syncPlanSnapActiveMarker(null),this._syncPlanSnapConflictMarkers([]),e&&(this._cursorPt=null)}_samePt(e,t){return fi(e,t)}_dropLegacySegments(){for(const e of this._serverCfg?.markers||[])"ripple"===e.display&&(e.display="icon_ripple");for(const e of this._serverCfg?.spaces||[]){delete e.segments;const t=new Set,i=e=>"string"==typeof e&&e.length>=1&&e.length<=64&&!t.has(e),s=e=>(t.add(e),!0),n=e=>Array.isArray(e)&&2===e.length&&e.every(e=>Number.isFinite(Number(e))&&Math.abs(Number(e))<=Oh);if(Array.isArray(e.partitions)&&(e.partitions=e.partitions.filter(e=>e&&i(e.id)&&n(e.a)&&n(e.b)&&Math.hypot(e.a[0]-e.b[0],e.a[1]-e.b[1])>1e-9&&s(e.id)).map(e=>({...e,cm:Number.isFinite(Number(e.cm))?Math.max(0,Math.min(100,Number(e.cm))):15})),e.partitions.length||delete e.partitions),Array.isArray(e.wall_columns)&&(e.wall_columns=e.wall_columns.filter(e=>e&&i(e.id)&&n(e.center)&&s(e.id)).map(e=>({id:e.id,shape:"circle"===e.shape?"circle":"square",center:[Number(e.center[0]),Number(e.center[1])],cm:nh(Number(e.cm)||15),..."circle"===e.shape?{}:{angle:oh(e.angle)}})),e.wall_columns.length||delete e.wall_columns),Array.isArray(e.room_drafts)&&(e.room_drafts=e.room_drafts.filter(e=>e&&i(e.id)&&Array.isArray(e.points)&&e.points.length>=2&&e.points.every(n)&&s(e.id)).map(e=>{const t=gg(e);return{id:e.id,...t}}).filter(e=>e.points.length>=2),e.room_drafts.length||delete e.room_drafts),Array.isArray(e.walls)){const t=ep(Eh({spaces:[e]}),e.id),i=t?X_(e,t,Ov,.02*Bh).contour.map(e=>e.map(e=>e/Ov)):jd(e.open_spans).map(e=>[e.a[0],e.a[1],e.b[0],e.b[1]]);e.walls=Ll(e.walls,e.rooms||[],jh,1,i),e.walls.length||delete e.walls}}}_rollbackRejectedPhysicalWrites(e){if(!this._serverCfg||!e.length)return!1;let t=!1;for(const[i]of e){const e=this._pendingPhysicalWrites.get(i);e&&(t=this._restoreGeometryStateInConfig(this._serverCfg,e.before)||t,this._pendingPhysicalWrites.delete(i))}return!!t&&(this._clearGeometryGesture(),this._geometryHistory.clear(),this._cfgEpoch++,this._modelCache=null,this._wallUnionCache=null,this._physicalBodiesCache=null,this._frame=null,this._regSignature="",this._cfgContentFingerprint=Jd(this._serverCfg),this._maybeRebuildDevices(),this.requestUpdate(),!0)}async _reloadRejectedPhysicalWrite(){const e=this._writeChain;await e.catch(()=>{}),await this._reloadConfigOnly(!0)}get _cfgWriting(){return this._writesPending>0}_writeConfig(){var e,t;this._writesPending++,this._writeChain=(e=this._writeChain,t=async()=>{if(!this._serverCfg)return;this._dropLegacySegments();const e=fl(il(this._serverCfg)),t=Jd(e),i=[...this._pendingPhysicalWrites.entries()];for(const[t,s]of i){const i=Ug(e.spaces.find(e=>e.id===t));if(i===s.fingerprint)continue;let n=!1;try{n=this._checkSpacePhysicalGeometry(e,t).ok}catch{n=!1}if(!n)throw this._restoreGeometryStateLocal(s.before),this._pendingPhysicalWrites.delete(t),this._geometryHistory.clear(),this._showToast(this._t("toast.geometry_unsafe")),Object.assign(new Error("unsafe wall geometry"),{code:"geometry-unsafe"});s.fingerprint=i}let s;t!==Jd(this._serverCfg)&&(this._serverCfg=e),this._cfgContentFingerprint=t;try{s=await this.hass.callWS({type:"houseplan/config/set",config:e,expected_rev:this._cfgRev})}catch(e){if(!this._rollbackRejectedPhysicalWrites(i))throw e;const t=e&&"object"==typeof e?e:Object.assign(new Error(String(e)),{cause:e});throw t.physicalGeometryRolledBack=!0,t}this._cfgRev=s?.rev??this._cfgRev+1;for(const[e,t]of i)this._pendingPhysicalWrites.get(e)?.fingerprint===t.fingerprint&&this._pendingPhysicalWrites.delete(e)},e.catch(()=>{}).then(t));return this._writeChain.finally(()=>{this._writesPending--})}_saveConfig(){this._cfgEpoch++,this._saveConfigDebounced()}_geometrySnapshotFromConfig(e,t){const i=e?.spaces?.find(e=>e.id===t);if(!i)return null;const s=e=>JSON.parse(JSON.stringify(e)),n={};for(const e of["plan_x","plan_y","plan_scale","plan_scale_x","plan_scale_y","plan_angle"])void 0!==i[e]&&(n[e]=i[e]);return{spaceId:t,rooms:s(i.rooms||[]),...Array.isArray(i.openings)?{openings:s(i.openings)}:{},...Array.isArray(i.walls)?{walls:s(i.walls)}:{},...Array.isArray(i.wall_segments)?{wall_segments:s(i.wall_segments)}:{},...Array.isArray(i.open_spans)?{open_spans:s(i.open_spans)}:{},...Array.isArray(i.room_drafts)?{room_drafts:s(i.room_drafts)}:{},...Array.isArray(i.partitions)?{partitions:s(i.partitions)}:{},...Array.isArray(i.wall_columns)?{wall_columns:s(i.wall_columns)}:{},...Array.isArray(i.decor)?{decor:s(i.decor)}:{},plan_transform:n}}_geometrySnapshot(e=this._space){return this._geometrySnapshotFromConfig(this._serverCfg,e)}_recordGeometry(e,t){if(!t)return;const i=this._geometrySnapshot(t.spaceId);i&&JSON.stringify(t)!==JSON.stringify(i)&&(this._geometryHistory.push({name:e,before:t,after:i}),this.requestUpdate())}_restoreGeometryStateInConfig(e,t,i=!1){const s=e?.spaces?.find(e=>e.id===t.spaceId);if(!s)return!1;const n=e=>JSON.parse(JSON.stringify(e)),o=new Map((s.rooms||[]).map(e=>[e.id,e]));if(s.rooms=n(t.rooms),i)for(const e of s.rooms){const t=o.get(e.id);!Array.isArray(e.wall_ids)&&Array.isArray(t?.wall_ids)&&t.wall_ids.length===e.poly?.length&&(e.wall_ids=n(t.wall_ids))}const a=(e,t)=>{void 0!==t?s[e]=n(t):i&&"wall_segments"===e||delete s[e]},r=new Map((s.openings||[]).map(e=>[e.id,e])),l=new Map((s.room_drafts||[]).map(e=>[e.id,e]));if(a("openings",t.openings),a("walls",t.walls),a("wall_segments",t.wall_segments),a("open_spans",t.open_spans),a("room_drafts",t.room_drafts),a("partitions",t.partitions),a("wall_columns",t.wall_columns),a("decor",t.decor),i){for(const e of s.openings||[]){const t=r.get(e.id);e.host||"wall"!==t?.host?.kind||(e.host=n(t.host))}for(const e of s.room_drafts||[]){const t=l.get(e.id);for(let i=0;i<(e.segments||[]).length;i++)!e.segments[i]?.id&&t?.segments?.[i]?.id&&(e.segments[i].id=t.segments[i].id)}}for(const e of["plan_x","plan_y","plan_scale","plan_scale_x","plan_scale_y","plan_angle"])delete s[e];return Object.assign(s,n(t.plan_transform||{})),!0}_restoreGeometryStateLocal(e){return!!this._restoreGeometryStateInConfig(this._serverCfg,e)&&(this._cfgEpoch++,this._modelCache=null,this._wallUnionCache=null,this._physicalBodiesCache=null,this._frame=null,this.requestUpdate(),!0)}_wallModelBlockerLabel(e){const t=e instanceof lg?e.reason:"invalid-room";return this._t(`wall_model.reason.${t}`)}_hasLegacyZeroWallFields(e=this._serverCfg){return(e?.spaces||[]).some(e=>Array.isArray(e?.open_spans)&&e.open_spans.length>0||(e?.rooms||[]).some(e=>Array.isArray(e?.open_to)&&e.open_to.length>0))}_showWallModelMigrationBlocked(e){const t=this._hasLegacyZeroWallFields()?"toast.zero_wall_migration_blocked":"toast.wall_model_migration_blocked";this._showToast(this._t(t,{reason:this._wallModelBlockerLabel(e)}))}_limitSegmentsOf(e){const t=[];for(const i of e?.wall_segments||[])i?.a&&i?.b&&t.push({id:String(i.id||""),a:i.a,b:i.b,cm:Number(i.cm)});for(const i of e?.partitions||[])i?.a&&i?.b&&t.push({id:String(i.id||""),a:i.a,b:i.b,cm:Number(i.cm)});for(const i of e?.room_drafts||[]){const e=Array.isArray(i?.points)?i.points:[],s=Array.isArray(i?.segments)?i.segments:[];for(let n=0;n+1e?.id===t);if(!s)return[];const n=Number(s.cell_cm)>0?Number(s.cell_cm):5,o=this._limitSegmentsOf(s),a=[...Td(o),...Fd(o,n,jh),...Ed(o,n,jh)];let r=null;try{r=bc(s.rooms||[],s.walls||[],[],jh,n,jh,1)}catch{r=null}let l="ok"===i?.status||"degraded-extra"===i?.status?i.roomGeom:null;if(!l&&r&&r.nodes.length)try{const e=qc(s.rooms||[],s.walls||[],[],[],jh,n,jh,1);l="ok"===e?.status||"degraded-extra"===e?.status?e.roomGeom:null}catch{l=null}for(const e of s.rooms||[]){const t=String(e?.id||"");if(!t)continue;let i=null;try{i=Cc(s.rooms||[],t,s.walls||[],[],jh,n,jh,1,l??void 0,r)}catch{i=null}a.push(...Od(t,i,n,jh))}return a}_junctionLimitLabel(e){const t=e=>String(Math.round(10*e)/10);return this._t(`junction.limit_${e.rule}`,{actual:t(e.actual),limit:t(e.limit)})}_junctionLimitsIntroduced(e,t,i,s){let n;const o=(t?.spaces||[]).find(e=>e?.id===i);let a="";try{a=Ug(o)}catch{a=""}const r=t&&"object"==typeof t?this._junctionBaselineCache.get(t):void 0;if(r&&a&&r.fingerprint===a&&r.spaceId===i&&(n=r.violations),!n){try{const e=Number(t?.model_version||0)>=9?t:Ag(t).config;n=this._junctionLimitViolations(e,i)}catch{return[]}t&&"object"==typeof t&&a&&this._junctionBaselineCache.set(t,{spaceId:i,fingerprint:a,violations:n})}let l=[];try{l=this._junctionLimitViolations(e,i,s)}catch{return[{rule:"check_failed",subject:i,actual:0,limit:0}]}return function(e,t){const i=new Map;for(const e of t||[])i.set(e.rule,(i.get(e.rule)||0)+1);const s=new Map;for(const t of e||[])s.set(t.rule,[...s.get(t.rule)||[],t]);const n=[];for(const[e,t]of s){const s=t.length-(i.get(e)||0);s>0&&n.push(...t.slice(0,s))}return n}(l,n)}_commitPhysicalGeometry(e,t,i=[]){if(!t||!this._serverCfg)return!1;const s=this._serverCfg,n=this._geometrySnapshotFromConfig(s,t.spaceId),o=s.spaces.find(e=>e.id===t.spaceId);if(!o||Ug(t)===Ug(o))return!1;let a,r=!1;try{r=this._checkSpacePhysicalGeometry(s,t.spaceId).ok}catch{r=!1}if(!r)return this._clearGeometryGesture(),this._restoreGeometryStateLocal(t),this._showToast(this._t("toast.geometry_unsafe")),!1;let l=t;try{if(Number(s.model_version||0)<9){const e=JSON.parse(JSON.stringify(s));if(!this._restoreGeometryStateInConfig(e,t))throw new lg("invalid-room",t.spaceId);const i=Ag(e).config;l=this._geometrySnapshotFromConfig(i,t.spaceId)||t;const o=JSON.parse(JSON.stringify(i));if(!n||!this._restoreGeometryStateInConfig(o,n,!0))throw new lg("invalid-room",t.spaceId);const r=i.spaces.find(e=>e.id===t.spaceId),c=o.spaces.find(e=>e.id===t.spaceId);if(!Array.isArray(n.wall_segments)&&JSON.stringify(t.rooms)===JSON.stringify(n.rooms))for(const e of c?.wall_segments||[])e.cm=Hl(n.walls,e.a,e.b,jh,1);const h=((e,t,i)=>{const s=new Map,n=new Map((e?.rooms||[]).map(e=>[e.id,e])),o=new Map((t||[]).map(e=>[e.id,e])),{rooms:a}=Dg(i,Mg(e));for(const e of a){const t=o.get(e.id),a=n.get(e.id),r=hi(t),l=hi((i.rooms||[]).find(t=>t.id===e.id)),c=hi(a);if(r&&l&&c&&r.length===l.length&&c.length===a?.wall_ids?.length&&e.poly.length===e.wall_ids.length)for(let t=0;t{const n=[];for(let o=0;org||bg(r,i,s)>rg)continue;const l=vg(a,i,s),c=vg(r,i,s);l<-rg||c>1+rg||c<=l+rg||n.push({start:l,id:t[o]})}return n.sort((e,t)=>e.start-t.start)},n=i(c,a.wall_ids,r[t],r[(t+1)%r.length]),o=i(e.poly,e.wall_ids,l[t],l[(t+1)%l.length]);if(n.length&&n.length===o.length)for(let e=0;ee.id===t.spaceId),s=this._path.length>=2?this._path.map(e=>[e[0]/Ov,e[1]/Ov]):[];s.push(...i.map(e=>[e[0],e[1]])),c=fg(e)<=fg(l,s)&&this._checkSpacePhysicalGeometry(a,t.spaceId).ok}catch{c=!1}if(!c)return this._clearGeometryGesture(),this._restoreGeometryStateLocal(t),this._showToast(this._t("toast.geometry_unsafe")),!1;const h=JSON.parse(JSON.stringify(s));this._restoreGeometryStateInConfig(h,t);const d=this._junctionLimitsIntroduced(a,h,t.spaceId);if(d.length)return this._clearGeometryGesture(),this._restoreGeometryStateLocal(t),this._showToast(this._junctionLimitLabel(d[0])),!1;Fg(s,a),this._recordGeometry(e,l);const p=s.spaces.find(e=>e.id===t.spaceId),u=this._pendingPhysicalWrites.get(t.spaceId);return this._pendingPhysicalWrites.set(t.spaceId,{before:u?.before||l,fingerprint:Ug(p)}),this._saveConfig(),!0}_clearGeometryGesture(){this._path=[],this._clearPlanSnapHover(),this._clearOpeningPlacement(!1),this._mergeSel=null,this._mergeDialog=null,this._splitSel=null,this._pendingSplit=null,this._wallFaceBatch=null,this._wallRepairDiagnostic=null,this._roomDeleteDialog=null,this._wallDialog=null,this._physicalDialog=null,this._physicalSel=null,this._physicalDrag=null,this._physicalRotate=null,this._activeDraftId=null,this._draftSegmentCms=[],this._closingWallCm=null,this._openingDialog=null,this._rszResetController(),this._decorDraft=null,this._decorMove=null,this._dtDrag=null,this._bdDrag=null}_stagePointerCancel(e){if(clearTimeout(this._kioskHoldTimer),this._swipeStart?.id===e.pointerId&&(this._swipeStart=null),this._physicalDrag?.pid===e.pointerId||this._physicalRotate?.pid===e.pointerId)return void this._cancelPhysicalGesture();if(this._decorDraft?.pid===e.pointerId)return this._decorDraft=null,void this.requestUpdate();if(this._decorMove?.pid===e.pointerId||this._dtDrag?.pid===e.pointerId||this._bdDrag?.pid===e.pointerId)return void this._cancelDecorGesture();"opening"===this._tool?(this._cursorPt=null,this._clearOpeningPlacement(!1)):"draw"===this._tool&&this._clearPlanSnapHover();const t=!!this._pinchStart||!!this._panStart;this._pointers.delete(e.pointerId),this._pointers.size<2&&(this._pinchStart=null),0===this._pointers.size&&(this._panStart=null,this._panLock=null),t&&0===this._pointers.size&&this.requestUpdate()}_applyGeometryState(e,t=!1){if(!this._canCommitSpace(e.spaceId))return!1;const i=this._geometrySnapshot(e.spaceId);if(!i||!this._restoreGeometryStateLocal(e))return!1;const s=this._serverCfg,n=Ug(i)!==Ug(e);if(n){let n=!1;try{const i=s?this._checkSpacePhysicalGeometry(s,e.spaceId):null;n=!!i?.ok||!(!t||"wall-degraded-extra"!==i?.reason)}catch{n=!1}if(!n)return this._restoreGeometryStateLocal(i),this._showToast(this._t("toast.geometry_unsafe")),!1}let o;try{o=Ag(s).config}catch(e){return this._restoreGeometryStateLocal(i),this._showWallModelMigrationBlocked(e),!1}if(n){let n=!1;try{const i=o?this._checkSpacePhysicalGeometry(o,e.spaceId):null;n=!!i?.ok||!(!t||"wall-degraded-extra"!==i?.reason)}catch{n=!1}if(!n)return this._restoreGeometryStateLocal(i),this._showToast(this._t("toast.geometry_unsafe")),!1;Fg(s,o);const a=s?.spaces.find(t=>t.id===e.spaceId),r=this._pendingPhysicalWrites.get(e.spaceId);this._pendingPhysicalWrites.set(e.spaceId,{before:r?.before||i,fingerprint:Ug(a)})}else Fg(s,o);return this._clearGeometryGesture(),this._space!==e.spaceId&&(this._commitSpace(e.spaceId),this._saveNav(),this._restoreZoom()),this._modelCache=null,this._frame=null,this._regSignature="",this._maybeRebuildDevices(),this._saveConfig(),this.requestUpdate(),!0}_roomAt(e){return this._spaceModel()?.rooms.find(t=>{const i=hi(t);return!!i&&wi(e,i)})}_overlapRoom(e){return this._spaceModel()?.rooms.find(t=>{const i=hi(t);return!!i&&Di(e,i)})}_pointInRoom(e,t){return t.poly?vi(e,t.poly):null!=t.x&&e[0]>=t.x&&e[0]<=t.x+t.w&&e[1]>=t.y&&e[1]<=t.y+t.h}_contourSelfIntersects(e){if(!Ea(e))return!0;const t=e.length,i=.001;for(let s=0;s=500)return this._showToast(this._t("toast.physical_limit")),!1;const e=this._curSpaceCfg;return!(!this._activeDraftId&&(e?.room_drafts||[]).length>=200||this._draftSegmentCount(e)>=2e3||(e?.partitions||[]).length+this._path.length>jv)||(this._showToast(this._t("toast.physical_limit")),!1)}_markupClick(e){if(this._vacFit)return;if(!this._markup)return;const t=this._spaceModel();if(!t)return void this._clearGeometryGesture();if(this._suppressClick)return;if(this._drag||this._rlResize)return;const i=e.composedPath?.()||[];if(i.some(e=>e?.classList?.contains?.("roomlabel")||e?.classList?.contains?.("rlhandle")))return;if(i.some(e=>e?.classList?.contains?.("physical-hit")))return;const s=this._svgPoint(e);if("select"===this._tool)return void(this._physicalSel=null);if("resize"===this._tool){if(this._resize.dragging||i.some(e=>e?.classList?.contains?.("rszhandle")))return;const e=[...t.rooms].reverse().find(e=>this._pointInRoom(s,e));return this._resize.selectRoom(e?.id||null),void this.requestUpdate()}if("delroom"===this._tool)return void this._deleteRoomClick(s);if("opening"===this._tool)return void this._openingClick(s);if("merge"===this._tool)return void this._mergeClick(s);if("wallthick"===this._tool)return void this._wallThickClick(s);if("split"===this._tool)return void this._splitClick(s);if("column"===this._tool)return void this._columnClick(s);this._wallRepairDiagnostic=null;const n=this._resolvePlanDrawPoint(s,e.shiftKey);if(n.ambiguous)return this._planSnapHover={contextKey:n.contextKey,candidate:null,conflicts:n.conflicts},this._syncPlanSnapActiveMarker(null),this._syncPlanSnapConflictMarkers(n.conflicts),void this._showToast(this._t("toast.plan_snap_ambiguous"));let o=n.point;if(e.ctrlKey||e.metaKey){if(e.preventDefault(),this._path.length<3)return;o=va(this._path[this._path.length-1],this._path[0])}if(!this._path.length){if(!e.shiftKey&&!n.candidate&&this._offerExistingWallFace(s))return;const t=this._draftEndAt(o);return t?(this._activeDraftId=t.draft.id,this._resumeDraftBySpace[this._space]=t.draft.id,this._path=t.reverse?[...t.draft.points].reverse().map(e=>[...e]):t.draft.points.map(e=>[...e]),void(this._draftSegmentCms=this._adoptDraftCms(this._path,t.reverse?[...t.draft.segments].reverse().map(e=>e.cm):t.draft.segments.map(e=>e.cm),t.draft.id))):(this._activeDraftId=null,this._draftSegmentCms=[],void(this._path=[o]))}const a=this._path[this._path.length-1];if(this._samePt(o,a))return;const r=this._draftEndAt(o,this._activeDraftId||void 0);if(r)return void this._mergeDraftEndpoint(r);if(!this._canAppendRoomDraftPoint())return;const l=this._drawWallCm;if(null==l)return void this._showPhysicalRange(100);const c=this._path.map(e=>[...e]);this._path=[...this._path,o],this._draftSegmentCms=[...this._draftSegmentCms,l],this._persistActiveDraftSegment(),this._offerWallFaces(c)}_draftEndAt(e,t){const i=this._spaceModel();if(!i)return null;const s=this._viewOr(this._baseVb()),n=Math.max(.15*this._gridPitch,this._stageEl?.clientWidth?s.w/this._stageEl.clientWidth*12:0);for(const s of i.room_drafts||[]){if(s.id===t)continue;if(s.points.length<2)continue;const i=s.points[0],o=s.points[s.points.length-1];if(Math.hypot(e[0]-o[0],e[1]-o[1])<=n)return{draft:s,reverse:!1};if(Math.hypot(e[0]-i[0],e[1]-i[1])<=n)return{draft:s,reverse:!0}}return null}_mergeDraftEndpoint(e){const t=this._curSpaceCfg;if(!t||!this._path.length)return;const i=this._path.map(e=>[...e]),s=this._wallGraphSources(i),n=Array.isArray(t.room_drafts)?t.room_drafts:[],o=this._activeDraftId?n.find(e=>e.id===this._activeDraftId):null,a=n.find(t=>t.id===e.draft.id);if(!a)return;const r=e.reverse?e.draft.points.map(e=>[...e]):[...e.draft.points].reverse().map(e=>[...e]),l=e.reverse?(a.segments||[]).map(e=>({...e})):[...a.segments||[]].reverse().map(e=>({...e})),c=this._path[this._path.length-1],h=this._samePt(c,r[0]),d=h?null:this._drawWallCm;if(!h&&null==d)return void this._showPhysicalRange(100);const p=this._draftSegmentsForPath(this._path,o,this._draftSegmentCms),u=[...this._path.map(e=>[...e]),...h?r.slice(1):r],_=[...p,...null==d?[]:[{cm:d}],...l],m=u.length>=4&&this._samePt(u[0],u[u.length-1]),g=u,f=_;if(g.length>500)return void this._showToast(this._t("toast.physical_limit"));const v=(o?.segments?.length||0)+(a.segments?.length||0);if(this._draftSegmentCount(t)-v+f.length>2e3)return void this._showToast(this._t("toast.physical_limit"));if(m){const e=g.slice(0,-1);if(this._contourSelfIntersects(e)||Ti(e)<=1e-6)return void this._showToast(this._t("toast.contour_cannot_close"));const t=this._overlapRoom(e);if(t)return void this._showToast(this._t("toast.room_overlap",{name:t.name||""}))}const b=this._geometrySnapshot(),y=this._activeDraftId||e.draft.id,w={...o||a,id:y,points:g.map(e=>[e[0]/Ov,e[1]/Ov]),segments:f};t.room_drafts=n.filter(t=>t.id!==e.draft.id&&(!this._activeDraftId||t.id!==this._activeDraftId)),t.room_drafts.push(w),this._activeDraftId=y,this._resumeDraftBySpace[this._space]=y,this._path=g,this._draftSegmentCms=this._adoptDraftCms(g,f.map(e=>Number(e.cm)),y),this._physicalSel=null,this._commitPhysicalGeometry(this._t("history.draft_merge"),b),null!=d&&this._offerWallFaces(i,i.length-1,s)}_adoptDraftCms(e,t,i){const s=Math.max(0,e.length-1),n=Iu(s,t,this._drawWallCm,vl);return t.length!==s&&console.debug(`[houseplan] draft ${i??"?"}: восстановлено толщин ${s-t.length} (#234)`),n}_draftSegmentsForPath(e,t,i){const s=Array.isArray(t?.points)?t.points.map(e=>[Number(e?.[0])*Ov,Number(e?.[1])*Ov]):[],n=Array.isArray(t?.segments)?t.segments:[];return Iu(Math.max(0,e.length-1),i,this._drawWallCm,vl).map((t,i)=>{const o=e[i],a=e[i+1],r=s.findIndex((e,t)=>{if(t+1>=s.length)return!1;const i=s[t+1];return this._samePt(o,e)&&this._samePt(a,i)||this._samePt(o,i)&&this._samePt(a,e)});return{...r>=0?n[r]:{},cm:t}})}_persistActiveDraftSegment(){if(this._path.length<2||!this._curSpaceCfg)return;const e=this._geometrySnapshot(),t=this._curSpaceCfg;t.room_drafts||=[],this._activeDraftId||(this._activeDraftId="draft-"+Date.now().toString(36)),this._resumeDraftBySpace[this._space]=this._activeDraftId;const i=t.room_drafts.findIndex(e=>e.id===this._activeDraftId),s={...i>=0?t.room_drafts[i]:{},id:this._activeDraftId,points:this._path.map(e=>[e[0]/Ov,e[1]/Ov]),segments:this._draftSegmentsForPath(this._path,i>=0?t.room_drafts[i]:null,this._draftSegmentCms)};i>=0?t.room_drafts[i]=s:t.room_drafts.push(s),this._commitPhysicalGeometry(this._t("history.draft_segment"),e)}_activeWallSourceKey(e){return`active:${this._activeDraftId||"session"}:${e}`}_wallGraphSources(e){const t=this._planStructuralGeometrySnapshot().value.segments.map(e=>({a:e.a,b:e.b,key:`static:${e.key}`}));for(let i=0;i+1e.key===i);if(s>=0){const[e]=this._wallFaceGraphCache.splice(s,1);return this._wallFaceGraphCache.push(e),e.value}const n=e_(e,t);return this._wallFaceGraphCache.push({key:i,value:n}),this._wallFaceGraphCache.length>4&&this._wallFaceGraphCache.shift(),n}_offerWallFaces(e,t=this._path.length-2,i){if(this._path.length<2||this._wallFaceBatch||this._roomDialog)return;const s=this._spaceModel();if(!s)return;const n=this._activeWallSourceKey(t),o=2e-4*this._gridPitch;let a,r;try{const t=i||this._wallGraphSources(e);r=this._wallGraphSources(this._path),a=function(e,t,i){const s=new Set(e.faces.map(e=>e.key));return t.faces.filter(e=>!s.has(e.key)&&e.sourceKeys.includes(i))}(this._wallFaceGraph(t,o),this._wallFaceGraph(r,o),n)}catch{return}if(!a.length){const e=p_(r,{requiredSourceKey:n,maxDistance:Ml(2,this._cellCm,this._gridPitch),gridStep:this._gridPitch,epsilon:o});return"ambiguous"===e.kind?(this._wallRepairDiagnostic=e.proposals[0]||null,void this._showToast(this._t(0===this._drawWallCm?"toast.zero_wall_ambiguous":"toast.wall_repair_ambiguous"))):void("repair"!==e.kind||this._overlapRoom(e.face.ring)||this._beginWallFaceBatch([{...e.face,repair:e.proposal}]))}for(const e of s.rooms){const t=hi(e);if(!e.id||!t)continue;const i=zi(t,this._path,.02*this._gridPitch);if(!i)continue;const[s,n]=i,o=Ti(s)>=Ti(n)?s:n,r=o===s?n:s,l=[...a].sort((e,t)=>Math.abs(e.area-Ti(r))-Math.abs(t.area-Ti(r))||e.key.localeCompare(t.key))[0];if(!l)return;return void this._beginWallFaceBatch([{...l,ring:r.map(e=>[e[0],e[1]]),split:{roomId:e.id,mainPoly:o,newPoly:r},consumeAllActive:!0}])}const l=a.filter(e=>!(e.ring.length<3||this._contourSelfIntersects(e.ring))&&!this._overlapRoom(e.ring));l.length&&this._beginWallFaceBatch(l)}_beginWallFaceBatch(e){this._wallRepairDiagnostic=null,this._wallFaceBatch={candidates:[...e].sort((e,t)=>e.area-t.area||e.key.localeCompare(t.key)),index:0,decisions:[],activePath:this._path.map(e=>[...e]),activeCms:[...this._draftSegmentCms],activeDraftId:this._activeDraftId},this._clearPlanSnapHover(),this._nameSel="",this._areaSel="",this._resetRoomDialogFields(),this._roomDialog=!0}_offerExistingWallFace(e){if(this._path.length||this._wallFaceBatch||this._roomDialog)return!1;const t=2e-4*this._gridPitch;try{let i,s=t_(this._wallFaceGraph(this._wallGraphSources([]),t),e,t);if(!s){const n=p_(this._wallGraphSources([]),{point:e,maxDistance:Ml(2,this._cellCm,this._gridPitch),gridStep:this._gridPitch,epsilon:t});if("ambiguous"===n.kind)return this._wallRepairDiagnostic=n.proposals[0]||null,this._showToast(this._t(0===this._drawWallCm?"toast.zero_wall_ambiguous":"toast.wall_repair_ambiguous")),!0;if("repair"===n.kind)s=n.face,i=n.proposal;else if("none"===n.kind){const i=p_(this._wallGraphSources([]),{point:e,maxDistance:this._cssPxToRender(12),gridStep:this._gridPitch,epsilon:t});if("repair"===i.kind)return this._wallRepairDiagnostic=i.proposal,this._showToast(this._t("toast.wall_repair_too_large")),!0}}return!(!s||s.ring.length<3||this._contourSelfIntersects(s.ring)||this._overlapRoom(s.ring))&&(this._beginWallFaceBatch([{...s,existing:!i,repair:i}]),!0)}catch{return!1}}_columnClick(e){const t=this._snap(e),i=this._drawWallCm;if(null==i)return void this._showPhysicalRange(ih,1);if(!this._curSpaceCfg||this._limitReached("column"))return;const s=this._spaceModel();if(!s)return;const n={id:"column-"+Date.now().toString(36),shape:"square",center:t,cm:nh(i),angle:0},o=(s.wall_columns||[]).find(e=>Sh(e,n,.02*this._gridPitch));if(o)return clearTimeout(this._duplicateColumnTimer),this._duplicateColumnId=o.id,this._duplicateColumnTimer=window.setTimeout(()=>{this._duplicateColumnId=null},900),void this._showToast(this._t("toast.column_duplicate"));const a=this._geometrySnapshot(),r=this._curSpaceCfg;r.wall_columns||=[],r.wall_columns.push({...n,center:[t[0]/Ov,t[1]/Ov]}),this._commitPhysicalGeometry(this._t("history.column_add"),a)}_openPhysicalDialog(e,t,i){const s=this._spaceModel();if(s)if("partition"===e){const i=s.partitions.find(e=>e.id===t);i&&(this._physicalDialog={kind:e,id:t,cm:Sl(i.cm,this._imperial),length:this._fmtLen(i.a,i.b)})}else if("column"===e){const i=s.wall_columns.find(e=>e.id===t);i&&(this._physicalDialog={kind:e,id:t,cm:Sl(i.cm,this._imperial),shape:i.shape,angle:this._angleField("square"===i.shape?oh(i.angle):0)})}else{const n=s.room_drafts.find(e=>e.id===t),o=Math.max(0,Math.min(n?.segments.length?n.segments.length-1:0,i||0));n?.segments[o]&&(this._physicalDialog={kind:e,id:t,segment:o,cm:Sl(n.segments[o].cm,this._imperial),length:this._fmtLen(n.points[o],n.points[o+1])})}}_physicalDown(e,t,i){const s=this._spaceModel();if(!s)return;e.stopPropagation(),Gv(e);const n=this._svgPoint(e),o=[];for(const e of[...s.wall_columns].reverse())xh(n,dh(e,this._cellCm,this._gridPitch))&&o.push({kind:"column",id:e.id});for(const e of[...s.partitions].reverse()){const t=hh(e.a,e.b,e.cm,this._cellCm,this._gridPitch);t&&xh(n,t)&&o.push({kind:"partition",id:e.id})}o.some(e=>e.kind===t&&e.id===i)||o.unshift({kind:t,id:i});const a=o.map(e=>`${e.kind}:${e.id}`).sort().join("|"),r=performance.now(),l=this._physicalPickCycle,c=o.length>1&&l?.signature===a&&r-l.at>380&&r-l.at<=1200&&Math.hypot(e.clientX-l.x,e.clientY-l.y)<=10?(l.index+1)%o.length:Math.max(0,o.findIndex(e=>e.kind===t&&e.id===i));this._physicalPickCycle={signature:a,index:c,x:e.clientX,y:e.clientY,at:r},t=o[c].kind,i=o[c].id;const h="partition"===t?s.partitions.find(e=>e.id===i):s.wall_columns.find(e=>e.id===i);h&&(this._physicalSel={kind:t,id:i},this._physicalDrag={pid:e.pointerId,kind:t,id:i,start:this._svgPoint(e),startClient:[e.clientX,e.clientY],before:this._geometrySnapshot(),moved:!1,base:JSON.parse(JSON.stringify(h)),delta:[0,0]})}_clampPhysicalDelta(e,t,i){const s="partition"===e?[t.a,t.b]:[t.center],n=s.map(e=>e[0]),o=s.map(e=>e[1]);return[Math.max(-Oh*Ov-Math.min(...n),Math.min(Oh*Ov-Math.max(...n),i[0])),Math.max(-Oh*Ov-Math.min(...o),Math.min(Oh*Ov-Math.max(...o),i[1]))]}_physicalMove(e){const t=this._physicalDrag;if(!t||t.pid!==e.pointerId)return;e.stopPropagation();const i=this._svgPoint(e),s="partition"===t.kind?t.base.a:t.base.center,n=this._snap([s[0]+i[0]-t.start[0],s[1]+i[1]-t.start[1]]),o=this._clampPhysicalDelta(t.kind,t.base,[n[0]-s[0],n[1]-s[1]]);this._physicalDrag={...t,delta:o,moved:t.moved||Math.hypot(e.clientX-t.startClient[0],e.clientY-t.startClient[1])>=5}}_physicalUp(e){const t=this._physicalDrag;if(!t||t.pid!==e.pointerId)return;if(e.stopPropagation(),this._physicalDrag=null,!t.moved)return void this._registerPhysicalTap(t.kind,t.id);const i=this._spaceModel();if(!this._curSpaceCfg||!i)return;const s=this._curSpaceCfg;if("partition"===t.kind){const e=(s.partitions||[]).find(e=>e.id===t.id),i=t.base;if(e){const n={...i,a:[i.a[0]+t.delta[0],i.a[1]+t.delta[1]],b:[i.b[0]+t.delta[0],i.b[1]+t.delta[1]]};e.a=[n.a[0]/Ov,n.a[1]/Ov],e.b=[n.b[0]/Ov,n.b[1]/Ov];for(const e of s.openings||[]){if("partition"!==e.host?.kind||e.host.id!==t.id)continue;const i=ou(e,[n],Ov,this._cellCm,this._gridPitch).resolved;i&&Object.assign(e,pu(e,i,Ov))}}}else{const e=(s.wall_columns||[]).find(e=>e.id===t.id),n=t.base,o={...n,center:[n.center[0]+t.delta[0],n.center[1]+t.delta[1]]};if(i.wall_columns.some(e=>e.id!==t.id&&Sh(e,o,.02*this._gridPitch)))return void this._showToast(this._t("toast.column_duplicate"));e&&(e.center=[(n.center[0]+t.delta[0])/Ov,(n.center[1]+t.delta[1])/Ov])}this._commitPhysicalGeometry(this._t("history.physical_move"),t.before)}_registerPhysicalTap(e,t,i){const s=performance.now(),n=this._physicalLastTap?.kind===e&&this._physicalLastTap.id===t&&this._physicalLastTap.segment===i&&s-this._physicalLastTap.at<=360;this._physicalLastTap={kind:e,id:t,segment:i,at:s},n&&(this._physicalLastTap=null,this._openPhysicalDialog(e,t,i))}_cancelPhysicalGesture(){this._physicalDrag=null,this._physicalRotate=null,this.requestUpdate()}_physicalRotateDown(e,t){if("square"!==t.shape||!this._spaceModel())return;e.preventDefault(),e.stopPropagation(),Gv(e);const i=this._svgPoint(e);this._physicalSel={kind:"column",id:t.id},this._physicalRotate={pid:e.pointerId,id:t.id,center:[...t.center],startAngle:180*Math.atan2(i[1]-t.center[1],i[0]-t.center[0])/Math.PI,baseAngle:oh(t.angle),angle:oh(t.angle),before:this._geometrySnapshot(),moved:!1}}_physicalRotateMove(e){const t=this._physicalRotate;if(!t||t.pid!==e.pointerId)return;e.preventDefault(),e.stopPropagation();const i=this._svgPoint(e),s=180*Math.atan2(i[1]-t.center[1],i[0]-t.center[0])/Math.PI,n=t.baseAngle+s-t.startAngle,o=oh(e.shiftKey?n:5*Math.round(n/5));this._physicalRotate={...t,angle:o,moved:t.moved||Math.abs(n-t.baseAngle)>=.5}}_physicalRotateUp(e){const t=this._physicalRotate;if(!t||t.pid!==e.pointerId)return;e.preventDefault(),e.stopPropagation(),this._physicalRotate=null;const i=this._spaceModel();if(!t.moved||!this._curSpaceCfg||!i)return;const s=i.wall_columns.find(e=>e.id===t.id);if(!s||"square"!==s.shape)return;const n={...s,angle:t.angle};if(i.wall_columns.some(e=>e.id!==t.id&&Sh(e,n,.02*this._gridPitch)))return void this._showToast(this._t("toast.column_duplicate"));const o=this._curSpaceCfg.wall_columns?.find(e=>e.id===t.id);o&&(o.angle=t.angle,this._commitPhysicalGeometry(this._t("history.physical_edit"),t.before))}_rszRooms(){const e=[],t=this._spaceModel();if(!t)return e;for(const i of t.rooms){const t=i.id?hi(i):null;t&&e.push({id:i.id,poly:t,wall_ids:Array.isArray(i.wall_ids)?[...i.wall_ids]:void 0})}return i=e,s=Math.max(1e-12,1e-12*this._gridPitch),i.map(e=>{if(!Array.isArray(e.wall_ids)||e.wall_ids.length!==e.poly.length)return{...e,poly:e.poly.map(e=>[...e])};const t=e.poly.map(e=>[...e]);let i=!0;for(;i&&t.length>3;){i=!1;for(let e=0;e0){t.splice(e,1),i=!0;break}}}return{...e,poly:t}});var i,s}_rszOpenings(){return this._openingsR.map(e=>({id:e.id,x:e.rx,y:e.ry,length:e.rlen,hosted:!!e.host,angle:e.angle,type:e.type}))}_rszObstacles(){const e=this._spaceModel();if(!e)return[];const t=[];for(const i of e.partitions||[])t.push({kind:"segment",a:[...i.a],b:[...i.b],half:Ml(i.cm,this._cellCm,this._gridPitch)/2});for(const i of e.room_drafts||[])for(let e=0;e+1Array.isArray(e.a)&&Array.isArray(e.b)&&e.a.length>=2&&e.b.length>=2).map(e=>({wall:e,a:[e.a[0]*Ov,e.a[1]*this._spaceH],b:[e.b[0]*Ov,e.b[1]*this._spaceH]})),n=Math.abs(e[0]-t[0])<=.05*this._gridPitch?"v":Math.abs(e[1]-t[1])<=.05*this._gridPitch?"h":null,o=s.filter(i=>!!n&&("h"===n?!(Math.abs(i.a[1]-e[1])>.05*this._gridPitch||Math.abs(i.b[1]-e[1])>.05*this._gridPitch)&&Math.min(Math.max(e[0],t[0]),Math.max(i.a[0],i.b[0]))-Math.max(Math.min(e[0],t[0]),Math.min(i.a[0],i.b[0]))>.05*this._gridPitch:!(Math.abs(i.a[0]-e[0])>.05*this._gridPitch||Math.abs(i.b[0]-e[0])>.05*this._gridPitch)&&Math.min(Math.max(e[1],t[1]),Math.max(i.a[1],i.b[1]))-Math.max(Math.min(e[1],t[1]),Math.min(i.a[1],i.b[1]))>.05*this._gridPitch)),a=new Set(o.map(e=>Number(e.wall.cm)).filter(e=>e>0));return{minDim:this._cmToUnits(30),eps:.05*this._gridPitch,step:this._gridPitch,movingHalf:i>0?Ml(i,this._cellCm,this._gridPitch)/2:0,obstacles:this._rszObstacles(),thicknessConflict:a.size>1}}_rszResolution(e,t,i){const s=this._resize.snapshotIdentity||i||this._rszSnapshot(),n=`${this._space}|${this._cellCm}|${this._gridPitch}|${s}`,o=`${e}:${t}`;return this._resize.resolve(n,o,()=>{const i=[...this._resize.rooms||this._rszRooms()],s=i.find(t=>t.id===e),n=s?.poly?.[t]||[0,0],o=s?.poly?.[(t+1)%(s?.poly?.length||1)]||[0,0];return function(e,t,i,s,n){const{eps:o}=n,a=e.find(e=>e.id===i);if(!a||s<0||s>=(a.poly?.length||0)||a.poly.length<4||!Ea(a.poly))return{enabled:!1,reason:"invalid-geometry"};const r=a.poly[s],l=a.poly[(s+1)%a.poly.length],c=Ba(r,l,o);if(!c)return{enabled:!1,reason:"diagonal"};if(!Ua(a.poly,s,c,o))return{enabled:!1,reason:"side-angle"};if(n.thicknessConflict)return{enabled:!1,reason:"thickness-conflict"};for(const e of n.obstacles||[])if(Ka(e,r,l,n))return{enabled:!1,reason:"duplicate-physical-wall"};const h=Aa(Ta(l,r)),d=[],p=new Set;let u=!1,_=!1;for(const t of e)if(t.id!==i)for(let e=0;e1||d.length>1)return{enabled:!1,reason:"multiple-rooms"};const m={[i]:s},g=[i];if(1===d.length){const e=d[0];if(!Ea(e.room.poly))return{enabled:!1,reason:"invalid-geometry"};if(!Ua(e.room.poly,e.edge,c,o))return{enabled:!1,reason:"side-angle"};g.push(e.room.id),m[e.room.id]=e.edge}const f=[];for(const e of t)if(!(Fa([e.x,e.y],r,l)>2*o)){if(e.hosted||e.length>h+2*o)return{enabled:!1,reason:"opening-conflict"};f.push(e.id)}const v=Object.fromEntries(g.map(t=>[t,e.find(e=>e.id===t).poly.length])),b=[];for(const t of g){const i=e.find(e=>e.id===t),s=m[t],n=(s-1+i.poly.length)%i.poly.length,a=(s+1)%i.poly.length,r=Xa(e,t,n,1,o),l=Xa(e,t,a,0,o);if(!r||!l)return{enabled:!1,reason:"partial-shared"};b.push(r,l)}const y={roomId:i,edge:s,a:[...r],b:[...l],n:Na(a.poly,s),roomIds:g,edgeByRoom:m,topology:v,movingOpeningIds:f,sideOwnership:b};if(!nr(e,t,y,0,n))return{enabled:!1,reason:"invalid-geometry"};const w=Math.abs(Number(n.step));if(Number.isFinite(w)&&w>o){const i=[-w,w];if(!i.some(i=>nr(e,t,y,i,n))){if(!i.some(i=>er(tr(e,t,y,i),y,o)))return{enabled:!1,reason:"partial-shared"};const s={...n,obstacles:[]};return i.some(i=>nr(e,t,y,i,s))?{enabled:!1,reason:"duplicate-physical-wall"}:t.length&&i.some(t=>nr(e,[],y,t,n))?{enabled:!1,reason:"opening-conflict"}:{enabled:!1,reason:"invalid-geometry"}}}return{enabled:!0,plan:y}}(i,[...this._resize.openings||this._rszOpenings()],e,t,this._rszOptsFor(n,o))})}_rszSnapshot(){return JSON.stringify(this._geometrySnapshot()||{spaceId:this._space,rooms:[],openings:[],walls:[],open_spans:[]})}_rszResetController(){const e=null!==this._resize.preview;this._resize.reset(),e&&this._cfgEpoch++}_rszProjectPreview(e,t,i,s,n){this._rszLimitViolation=null;const o=this._serverCfg?.spaces.find(e=>e.id===this._space);if(!o||!this._serverCfg)return{ok:!1,reason:"missing-context"};const a=JSON.parse(e),r={...o,rooms:a.rooms,openings:a.openings||[]};for(const e of["walls","wall_segments","open_spans","room_drafts","partitions","wall_columns","decor"])void 0!==a[e]?r[e]=a[e]:delete r[e];for(const e of["plan_x","plan_y","plan_scale","plan_scale_x","plan_scale_y","plan_angle"])void 0!==a.plan_transform?.[e]?r[e]=a.plan_transform[e]:delete r[e];const l=this._spaceH;for(const[e,i]of Object.entries(t)){const t=r.rooms.find(t=>t.id===e);t&&(t.poly=i.map(e=>[e[0]/Ov,e[1]/l]),delete t.x,delete t.y,delete t.w,delete t.h)}for(const[e,t]of Object.entries(i)){const i=(r.openings||[]).find(t=>t.id===e);i&&(i.x=t[0]/Ov,i.y=t[1]/l)}const c=[],h=[];for(const e of s){const t=n.find(t=>t.id===e),i=r.rooms.find(t=>t.id===e);if(!t||!i?.poly)continue;const s=i.poly.map(e=>[e[0]*Ov,e[1]*l]);if(t.poly.length!==s.length)continue;const o=Array.isArray(t.wall_ids)?t.wall_ids:[];for(let e=0;ee.id===i):null;n&&(n.a=[s[e][0]/Ov,s[e][1]/Ov],n.b=[s[(e+1)%s.length][0]/Ov,s[(e+1)%s.length][1]/Ov])}}if(c.length&&Array.isArray(r.walls)&&r.walls.length){const e=function(e,t,i,s,n=1,o="fixed-topology"){let a=!1;const r=jl(e,t,i,s,n,o,()=>{a=!0});return{walls:a?(e||[]).map(e=>({...e})):r,rejected:a}}(r.walls,c,h,this._wallKeyPitch,Ov,"fixed-topology");if(e.rejected)return{ok:!1,reason:"wall-metadata"};r.walls=e.walls}const d=[];for(const e of r.rooms||[]){const t=hi(e);if(t&&!(t.length<2))for(let e=0;eJSON.stringify([e?.key,e?.cm,e?.a,e?.b]),u=new Map;for(const e of a.walls||[]){const t=p(e);u.set(t,(u.get(t)||0)+1)}const _=[];for(const e of r.walls||[]){const t=p(e),i=u.get(t)||0;i?u.set(t,i-1):_.push(e)}if(Wl(_,d,this._wallKeyPitch,Ov,a.walls||[]).length)return{ok:!1,reason:"wall-metadata"};const m=this._rszSpaceCandidateGeometry(this._space,r);if(!m.ok)return{ok:!1,reason:"physical-geometry"};const g={...this._serverCfg,spaces:(this._serverCfg?.spaces||[]).map(e=>e?.id===this._space?r:e)},f=this._junctionLimitsIntroduced(g,this._serverCfg,this._space,m.wallGeometry);return f.length?(this._rszLimitViolation=f[0],{ok:!1,reason:"junction-limit"}):{ok:!0,value:{preview:{space:this._space,sp:r},beforeWalls:a.walls||[],afterWalls:r.walls||[],artifact:m.wallGeometry}}}_rszAcceptPreview(e,t){if(this._cfgEpoch++,!e||!t)return;const i=Uc(t);if(!i)return;const s=`${this._space}|${this._cfgEpoch}|${e.sp.rooms.length}`;Object.defineProperty(i,"sourceFingerprint",{value:Jd([e.sp,this._cellCm,this._gridPitch]),enumerable:!1});const n={key:s,value:i};Mv(this._wallUnionPool,s,n,8),this._wallUnionCache=n}_rszSpaceCandidateGeometry(e,t){if(!this._serverCfg)return{ok:!1,wallGeometry:null};const i={...this._serverCfg,spaces:this._serverCfg.spaces.map(i=>i.id===e?t:i)};let s=null;try{return{ok:this._checkSpacePhysicalGeometry(i,e,e=>{s=e}).ok,wallGeometry:s}}catch{return{ok:!1,wallGeometry:null}}}_rszSpaceCandidateRenderable(e,t){if(!this._serverCfg)return!1;try{const i={...this._serverCfg,spaces:this._serverCfg.spaces.map(i=>i.id===e?t:i)};return this._checkSpacePhysicalGeometry(i,e).ok}catch{return!1}}_rszCandidateRenderable(e){return!!e&&e.space===this._space&&this._rszSpaceCandidateRenderable(e.space,e.sp)}_rszEdgeDown(e,t,i){if("resize"!==this._tool||this._resize.dragging)return;e.stopPropagation(),e.preventDefault();const s=this._rszRooms(),n=this._rszResolution(t,i);if(!n.enabled)return void this._showToast(this._rszReasonText(n.reason));Gv(e);const o=n.plan,a=this._svgPoint(e),r=`${this._space}|${this._cfgEpoch}|${s.length}`,l=this._wallUnionCache?.key===r?this._wallUnionCache.value:null,c=this._rszSnapshot();this._resize.begin({pointerId:e.pointerId,start:[a[0],a[1]],roomId:t,plan:o,options:this._rszOptsFor(o.a,o.b),rooms:s,openings:this._rszOpenings(),snapshotIdentity:c,before:JSON.parse(c),wallUnionBefore:l})}_rszReasonText(e){return this._t(`resize.disabled.${e}`)}_rszDisabledActivate(e,t){e.stopPropagation(),e.preventDefault(),this._showToast(this._rszReasonText(t))}_rszDisabledKey(e,t){"Enter"!==e.key&&" "!==e.key||this._rszDisabledActivate(e,t)}_rszMove(e){if(!this._resize.ownsPointer(e.pointerId))return;e.stopPropagation();const t=this._svgPoint(e),i=this._resize.move({pointerId:e.pointerId,point:[t[0],t[1]],step:this._gridPitch,snap:e=>{const t=this._snap(e);return[t[0],t[1]]},project:(e,t,i,s,n)=>this._rszProjectPreview(e,t,i,s,n),publish:(e,t)=>this._rszAcceptPreview(e,t),measure:(e,t)=>this._rszEdgeLabels(e,t)});if("rejected"===i.kind)return i.notify&&this._showToast(this._rszLimitViolation?`${this._t("resize.limit_stopped")} — `+this._junctionLimitLabel(this._rszLimitViolation):this._t("resize.preview_failed")),void this.requestUpdate();"accepted"===i.kind&&this.requestUpdate()}_rszUp(e){if(!this._resize.ownsPointer(e.pointerId))return;e.stopPropagation();const t=this._resize.finish({pointerId:e.pointerId,currentSnapshotIdentity:this._rszSnapshot(),validatePreview:e=>this._rszCandidateRenderable(e)});if("no-op"===t.kind)return this._cfgEpoch++,void this.requestUpdate();if("rejected"===t.kind)return this._cfgEpoch++,this._showToast(this._t("resize.commit_failed")),void this.requestUpdate();const i=t.preview,s=this._serverCfg?.spaces.find(e=>e.id===i.space);s&&(s.rooms=i.sp.rooms,s.openings=i.sp.openings,Array.isArray(i.sp.walls)&&(i.sp.walls.length?s.walls=i.sp.walls:delete s.walls),Array.isArray(i.sp.wall_segments)&&(s.wall_segments=i.sp.wall_segments)),this._suppressClick=!0,setTimeout(()=>this._suppressClick=!1,0),this._commitPhysicalGeometry(this._t("history.resize_room"),t.before),this.requestUpdate()}_rszCancelDrag(e){const t=this._resize.cancel(this._rszSnapshot(),e);if("no-op"!==t.kind){if(this._cfgEpoch++,t.restoreWallUnion){const e=this._spaceModel();if(e){const i=`${this._space}|${this._cfgEpoch}|${e.rooms.length}`,s={key:i,value:t.restoreWallUnion};Mv(this._wallUnionPool,i,s,8),this._wallUnionCache=s}}this.requestUpdate()}}_rszPointerCancel(e){this._resize.ownsPointer(e.pointerId)&&(e.stopPropagation(),this._rszCancelDrag(e.pointerId))}_rszEdgeLabels(e,t,i=this._resize.rooms){const s=i,n=[],o=e.polys[t.roomId]||s?.find(e=>e.id===t.roomId).poly;if(!o||!s)return n;const a=o.length,r=this._rszInnerSpanCms(t.roomId,o,e.polys);for(const e of function(e,t){const i=e.length;return[(t-1+i)%i,(t+1)%i]}(o,t.edge)){const t=o[e],i=o[(e+1)%a],s=r?.[e];n.push({kind:"length",x:(t[0]+i[0])/2,y:(t[1]+i[1])/2,text:null==s?this._fmtLen(t,i):li(s,"mi"===this.hass?.config?.unit_system?.length),edge:{a:[t[0],t[1]],b:[i[0],i[1]]}})}const l="mi"===this.hass?.config?.unit_system?.length,c=t.roomIds,h=this._spaceWalls,d=this._physicalBodiesR(),p=this._baseVb(),u=this._view&&this._view.w>0&&this._view.h>0?this._view:{x:p[0],y:p[1],w:p[2],h:p[3]},[_,m]=this._lastValidStageSize||[u.w,u.h],g={...u,stageWidth:Math.max(1,_),stageHeight:Math.max(1,m)},f=this._spaceModel(),v=this._config?.icon_size??2.5,b=f?od(v>8?2.5:v,f,u.w,this._kiosk?this._kioskScale.icon:1)*g.stageWidth/100:24,y=Math.max(10,.77*b),w=this._t("room.settings_short"),k=y*(1.457+.66*Math.max(1,w.length)*.42);for(const i of c){const o=e.polys[i]||s.find(e=>e.id===i).poly,a=h.length&&f&&this._innerRoomContour(f,i)||o,r=rr(d.length?yh(bh(a,d))*Math.pow(this._cellCm/this._gridPitch,2)/1e4:ar(a,this._gridPitch,this._cellCm),l),c=_r({poly:o,edge:t.edgeByRoom[i],text:r,view:g,gearCenter:$i(o),gearWidthPx:k,gearHeightPx:y});n.push({kind:"area",roomId:i,x:c.anchor[0],y:c.anchor[1],text:r,placement:c})}return n}_rszInnerSpanCms(e,t,i){const s=Object.keys(i).length?Object.entries(i).map(([e,t])=>({id:e,poly:t})):this._spaceModel()?.rooms;if(!s?.length)return null;const n=this._spaceWalls;if(!n.length)return null;const o=function(e,t,i,s,n,o,a,r=1){const l=(e||[]).find(e=>e?.id===t),c=hi(l);if(!c||c.length<3)return null;const h=yc(e,t,i,s,n,o,a,r);if(!h)return c.map(()=>0);const d=4*sc(n,r);return c.map((e,t)=>{const i=c[(t+1)%c.length],s=[(e[0]+i[0])/2,(e[1]+i[1])/2];for(let e=0;efunction(e,t,i){const s=e?.length||0;if(s<3||!Array.isArray(i)||i.length!==s)return 0;const n=(Math.trunc(t)%s+s)%s,o=e[n],a=e[(n+1)%s],r=a[0]-o[0],l=a[1]-o[1],c=Math.hypot(r,l);if(!(c>0))return 0;const h=Math.max(0,Number(i[n])||0);if(!(h>0))return c;const d=[r/c,l/c],p=oc(e,n),u=[o[0]+p[0]*h,o[1]+p[1]*h],_=t=>{const n=Math.max(0,Number(i[t])||0);if(!(n>0))return null;const a=e[t],r=e[(t+1)%s],l=r[0]-a[0],c=r[1]-a[1],h=Math.hypot(l,c);if(!(h>0))return null;const p=oc(e,t),_=rc(u,d,[a[0]+p[0]*n,a[1]+p[1]*n],[l/h,c/h]);return _?(_[0]-o[0])*d[0]+(_[1]-o[1])*d[1]:null},m=_((n-1+s)%s)??0,g=(_((n+1)%s)??c)-m;return g>0?g:0}(t,i,o)*a)}_renderResizeMeasurements(){const e=this._resize.liveLabels;if(!e?.length)return G;const t=e.filter(e=>"length"===e.kind),i=e.filter(e=>"area"===e.kind);return W``}_renderResizeLayer(e){const t=Math.max(.013*e.w,5),i=t/2,s=e=>e.toFixed(1),n=`M ${s(-.7*i)} 0 H ${s(.7*i)} M 0 ${s(-.22*i)} V ${s(-i)} M ${s(-.32*i)} ${s(-.6*i)} L 0 ${s(-i)} L ${s(.32*i)} ${s(-.6*i)} M 0 ${s(.22*i)} V ${s(i)} M ${s(-.32*i)} ${s(.6*i)} L 0 ${s(i)} L ${s(.32*i)} ${s(.6*i)}`,o=[],a=this._rszRooms(),r=this._resize.snapshotIdentity||this._rszSnapshot();for(const e of a)for(let i=0;ithis._rszEdgeDown(t,e.id,i)} - @pointermove=${e=>this._rszMove(e)} - @pointerup=${e=>this._rszUp(e)} - @pointercancel=${e=>this._rszPointerCancel(e)} - @lostpointercapture=${e=>this._rszPointerCancel(e)} - @click=${u?e=>this._rszDisabledActivate(e,p.reason):null} - @keydown=${u?e=>this._rszDisabledKey(e,p.reason):null}> - ${_} - `),o.push(W``)}return W`${o}`}get _openingsR(){const e=this._curSpaceCfg,t=this._spaceH,i=this._spaceModel();return i?(e?.openings||[]).flatMap(e=>{const s={...e,rx:e.x*Ov,ry:e.y*t,rlen:e.length*Ov};if(!e.host||"wall"===e.host.kind)return[s];const n=ou(e,i.partitions,Ov,this._cellCm,this._gridPitch);if(!n.resolved)return"plan"===this._mode?[{...s,orphanReason:n.reason||"invalid-host"}]:[];let[o,a]=n.resolved.center;const r=this._physicalDrag;return r?.moved&&"partition"===r.kind&&r.id===e.host.id&&(o+=r.delta[0],a+=r.delta[1]),[{...e,rx:o,ry:a,rlen:n.resolved.length,angle:n.resolved.angle,partitionHost:n.resolved}]}):[]}_partitionOpeningCuts(e=this._spaceModel(),t=()=>!0){if(!e)return[];return Vg(Gg(this._curSpaceCfg?.id===e.id?this._curSpaceCfg:null,e,this._cellCm,this._gridPitch,Ov),t)}_roomWallOpeningInputs(e=this._openingsR,t=this._spaceModel()){if(!t)return[];const i=this._openCuts();return Kg(e,t,this._spaceWalls,i,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov)}_openingFace(e,t,i){return e.partitionHost?cu(e.partitionHost,i):Zc(t,{x:e.rx,y:e.ry,angle:e.angle,length:e.rlen,flip_v:i})}_cmToUnits(e){return e/this._cellCm*this._gridPitch}get _decorList(){const e=this._curSpaceCfg;return Array.isArray(e?.decor)?e.decor:[]}get _decorH(){return Ov}_decorResolvedStyle(e){return function(e,t,i,s=Br){const n="rect"===e?.kind||"ellipse"===e?.kind,o=e,a=Ie(o?.color,s.color);return{color:a,opacity:jr(o?.opacity,s.opacity),widthCm:Vr(e,t,i,s.widthCm),fill:!!n&&!0===o?.fill,fillColor:Ie(o?.fill_color,o?.fill?a:s.fillColor),fillOpacity:n&&o?.fill?jr(o?.fill_opacity,.25):s.fillOpacity}}(e,this._cellCm,this._gridPitch,Br)}_decorWidthUnits(e){return((e,t,i,s=Br.widthCm)=>{const n=Number(e?.width_cm);if(Number.isFinite(n)&&n>0)return Ur(n,t,i);const o=Number(e?.width);return Number.isFinite(o)&&o>0?o:Ur(s,t,i)})(e,this._cellCm,this._gridPitch,Br.widthCm)}_decorTextSizeCm(e){if("text"===e?.kind){const t=Number(e.size_cm);return Number.isFinite(t)&&t>0?t:Gr(ts*is(e),this._cellCm,this._gridPitch)}return Gr(ts,this._cellCm,this._gridPitch)}_decorTextUnits(e){if("text"!==e.kind)return ts;const t=Number(e.size_cm);return Number.isFinite(t)&&t>0?Ur(t,this._cellCm,this._gridPitch):ts*is(e)}_decorSmallField(e){return Math.round(100*(this._imperial?e/2.54:e))/100}_decorSmallCm(e){const t=this._imperial?2.54*e:e;return Number.isFinite(t)?Math.max(.1,Math.min(100,t)):.1}_decorTextCm(e){const t=this._imperial?2.54*e:e;return Number.isFinite(t)?Math.max(.1,Math.min(wv,t)):.1}_decorLargeField(e){return Math.round(100*(this._imperial?e/30.48:e/100))/100}_decorLargeCm(e){const t=this._imperial?30.48*e:100*e;return Number.isFinite(t)?Math.max(.1,Math.min(Oh*this._cellCm,t)):.1}_angleField(e){const t=Number(e);return Number.isFinite(t)?String(Number(t.toFixed(3))):"0"}_decorBoxOf(e){return"rect"!==e.kind&&"ellipse"!==e.kind&&"furniture"!==e.kind?null:{x:e.x*Ov,y:e.y*this._decorH,w:e.w*Ov,h:e.h*this._decorH,angle:qr(e.angle)||void 0}}_decorSnapGeometry(e){const t=this._spaceModel();if(!t)return{points:[],segments:[]};const i=e||"",s=this._decorSnapCache;if(s&&s.epoch===this._cfgEpoch&&s.space===this._space&&s.height===this._decorH&&s.exclude===i)return s.geometry;const n=[];for(const t of this._decorList)if(t.id!==e)if("line"===t.kind){const e=[t.x1*Ov,t.y1*this._decorH],i=[t.x2*Ov,t.y2*this._decorH];n.push({points:[e,i,[(e[0]+i[0])/2,(e[1]+i[1])/2]],segments:[{a:e,b:i}]})}else if("text"===t.kind)n.push({points:[[t.x*Ov,t.y*this._decorH]],segments:[]});else{const e=this._decorBoxOf(t);e&&n.push(Jr(e))}for(const e of t.rooms){const t=hi(e);t?.length&&n.push({points:t.flatMap((e,i)=>{const s=t[(i+1)%t.length];return[e,[(e[0]+s[0])/2,(e[1]+s[1])/2]]}),segments:t.map((e,i)=>({a:e,b:t[(i+1)%t.length]}))})}const o=function(e){return{points:e.flatMap(e=>e.points),segments:e.flatMap(e=>e.segments)}}(n);return this._decorSnapCache={epoch:this._cfgEpoch,space:this._space,height:this._decorH,exclude:i,geometry:o},o}_decorSnap(e,t="mouse",i){const s=this._stageEl,n=this._viewOr(this._baseVb()),o="touch"===t||"pen"===t?14:8,a=s?n.w/Math.max(1,s.clientWidth)*o:this._gridPitch;return function(e,t,i,s){const n=s(e);let o=null;for(const n of t.points){const t=s(n),a=Math.hypot(t[0]-e[0],t[1]-e[1]);a<=i&&(!o||athis._snap(e)).point}_replaceDecor(e,t){const i=this._curSpaceCfg;i&&(i.decor=this._decorList.map(i=>i.id===e?{...i,...t}:i),this.requestUpdate())}_cancelDecorGesture(){const e=this._decorMove?.before||this._dtDrag?.before||this._bdDrag?.before,t=e&&this._serverCfg?.spaces.find(t=>t.id===e.spaceId);if(e&&t){const i=e=>JSON.parse(JSON.stringify(e));void 0!==e.decor?t.decor=i(e.decor):delete t.decor;for(const e of["plan_x","plan_y","plan_scale","plan_scale_x","plan_scale_y","plan_angle"])delete t[e];Object.assign(t,i(e.plan_transform||{})),this._cfgEpoch++}this._decorMove=null,this._dtDrag=null,this._bdDrag=null,this.requestUpdate()}_decorPointerDown(e){const t=this._decorTool;if("select"===t||"erase"===t?e.target.closest?.(".dshape"):null)return!0;if("line"===t||"rect"===t||"ellipse"===t){e.preventDefault();const i=this._decorSnap(this._svgPoint(e),e.pointerType);return this._decorDraft={kind:t,a:i,b:i,pid:e.pointerId},Gv(e),!0}if("text"===t){const t=this._decorSnap(this._svgPoint(e),e.pointerType);return this._decorTextDialog={x:Gh(t[0]/Ov),y:Gh(t[1]/this._decorH),text:"",color:this._decorStyle.color,opacity:this._decorStyle.opacity,angle:"0",sizeCm:Gr(ts,this._cellCm,this._gridPitch)},this._decorTextSelection={start:0,end:0},!0}if("furniture"===t)return!!this._furnPalette&&(e.preventDefault(),this._furnPlace(this._svgPoint(e),e.shiftKey),!0);if("select"===t&&(this._decorSel=null),this._bdMovable){const t=this._bdRect,i=this._svgPoint(e),s=t.x+t.w/2,n=t.y+t.h/2,o=-qr(t.angle)*Math.PI/180,a=i[0]-s,r=i[1]-n,l=[s+a*Math.cos(o)-r*Math.sin(o),n+a*Math.sin(o)+r*Math.cos(o)];if(l[0]>=t.x&&l[0]<=t.x+t.w&&l[1]>=t.y&&l[1]<=t.y+t.h)return e.preventDefault(),this._bdStart(e)}return!1}_decorCommitDraft(){const e=this._decorDraft;if(this._decorDraft=null,!e)return;const t=.5*this._gridPitch;if(!function(e,t,i,s){const n=Math.abs(i[0]-t[0]),o=Math.abs(i[1]-t[1]);return"line"===e?Math.hypot(n,o)>=s:n>=s&&o>=s}(e.kind,e.a,e.b,t))return;const i=Ov,s=this._decorH,n=this._decorStyle,o=this._geometrySnapshot(),a="dc"+Date.now().toString(36)+Math.random().toString(36).slice(2,5),r=Gh;let l;if("line"===e.kind)l={id:a,kind:"line",x1:r(e.a[0]/i),y1:r(e.a[1]/s),x2:r(e.b[0]/i),y2:r(e.b[1]/s),...Kr(n,!1)};else{const t=r(Math.min(e.a[0],e.b[0])/i),o=r(Math.min(e.a[1],e.b[1])/s),c=Math.abs(e.b[0]-e.a[0])/i,h=Math.abs(e.b[1]-e.a[1])/s;l={id:a,kind:e.kind,x:t,y:o,w:c,h:h,...Kr(n,!0)}}this._curSpaceCfg.decor=[...this._decorList,l],this._decorSel=a,this._recordGeometry(this._t("history.decor_add"),o),this._saveConfig(),this.requestUpdate()}_decorShapeDown(e,t){if("decor"!==this._mode)return;const i=this._decorTool;if("text"===i){if("text"!==t.kind)return;return e.stopPropagation(),e.preventDefault(),void this._decorOpenText(t)}"select"!==i&&"erase"!==i||(e.stopPropagation(),e.preventDefault(),"erase"!==i?(this._decorSel=t.id,this._decorMove={id:t.id,start:this._svgPoint(e),orig:JSON.parse(JSON.stringify(t)),pid:e.pointerId,moved:!1,before:this._geometrySnapshot()},Gv(e)):this._decorEraseConfirm={id:t.id,kind:t.kind})}_decorMoveUpdate(e){const t=this._decorMove;if("furniture"===t.orig?.kind)return void this._furnMoveUpdate(e);const i=this._svgPoint(e),s=t.orig,n=("line"===s.kind?s.x1:s.x)*Ov,o=("line"===s.kind?s.y1:s.y)*this._decorH,a=this._decorSnap([n+(i[0]-t.start[0]),o+(i[1]-t.start[1])],e.pointerType,t.id);let r=(a[0]-n)/Ov,l=(a[1]-o)/this._decorH;const c=t.orig,h="line"===c.kind?Math.min(c.x1,c.x2):c.x,d="line"===c.kind?Math.min(c.y1,c.y2):c.y,p="line"===c.kind?Math.abs(c.x2-c.x1):c.w||0,u="line"===c.kind?Math.abs(c.y2-c.y1):c.h||0,_=Oh;r=Math.max(-_-h,Math.min(_-h-p,r)),l=Math.max(-_-d,Math.min(_-d-u,l)),(r||l)&&(t.moved=!0);this._curSpaceCfg.decor=this._decorList.map(e=>{if(e.id!==t.id)return e;const i=t.orig;return"line"===e.kind?{...e,x1:i.x1+r,y1:i.y1+l,x2:i.x2+r,y2:i.y2+l}:{...e,x:i.x+r,y:i.y+l}}),this.requestUpdate()}_decorShapeDbl(e,t){"decor"===this._mode&&"select"===this._decorTool&&(e.preventDefault(),e.stopPropagation(),this._decorMove=null,this._decorSel=t.id,this._openDecorProperties(t))}_openDecorProperties(e){if("decor"!==this._mode||"select"!==this._decorTool)return;if("text"===e.kind)return void this._decorOpenText(e);if(!["line","rect","ellipse","furniture"].includes(e.kind))return;const t=this._decorResolvedStyle(e),i="line"===e.kind?e:null,s=this._decorBoxOf(e);this._decorShapeDialog={id:e.id,kind:e.kind,color:t.color,opacity:t.opacity,widthCm:t.widthCm,angle:this._angleField(qr(i?Es([i.x1*Ov,i.y1*this._decorH],[i.x2*Ov,i.y2*this._decorH]):e.angle)),...i?{lengthCm:Gr(Math.hypot((i.x2-i.x1)*Ov,(i.y2-i.y1)*this._decorH),this._cellCm,this._gridPitch),lineStyle:"dashed"===i.line_style?"dashed":"solid"}:{},...s?{sizeWCm:Gr(s.w,this._cellCm,this._gridPitch),sizeHCm:Gr(s.h,this._cellCm,this._gridPitch)}:{},..."furniture"===e.kind?{symbol:e.symbol}:{},..."rect"===e.kind||"ellipse"===e.kind?{fill:t.fill,fillColor:t.fillColor,fillOpacity:t.fillOpacity}:{}}}_decorOpenText(e){if("text"!==e.kind)return;let t=String(e.text??"");const i=[...t.matchAll(/\{([^{}\r\n]+)\}/g)].some(e=>!!Yi(e[1])),s=String(e.unit??"").trim(),n="state"===String(e.attr??"").trim().toLowerCase()?null:e.attr,o=i||s?"":Ji(e.entity,n),a=!(!String(e.entity??"").trim()||i||o&&!s);if(o&&!a){const e=t.indexOf("{}");t=e>=0?t.slice(0,e)+o+t.slice(e+2):`${t}${t?" ":""}${o}`}this._decorTextDialog={id:e.id,x:e.x,y:e.y,text:t,color:Ie(e.color,this._decorStyle.color),opacity:jr(e.opacity,this._decorStyle.opacity),angle:this._angleField(e.angle),sizeCm:this._decorTextSizeCm(e),pickerEntity:e.entity||"",preserveLegacy:a||void 0},this._decorTextSelection={start:t.length,end:t.length}}_decorRememberTextSelection(e){this._decorTextSelection={start:e.selectionStart??e.value.length,end:e.selectionEnd??e.value.length}}_decorInsertLiveVariable(e){const t=this._decorTextDialog;if(!t)return;const i=Ji(t.pickerEntity,e);if(!i)return;const s=t.text,n=Math.max(0,Math.min(s.length,this._decorTextSelection.start)),o=Math.max(n,Math.min(s.length,this._decorTextSelection.end));if(s.length-(o-n)+i.length>200)return;const a=s.slice(0,n)+i+s.slice(o),r=n+i.length;this._decorTextSelection={start:r,end:r},this._decorTextDialog={...t,text:a,preserveLegacy:void 0},this.updateComplete.then(()=>{const e=this.renderRoot.querySelector("textarea.dtarea");e&&(e.focus(),e.setSelectionRange(r,r))})}_decorSaveText(){const e=this._decorTextDialog,t=String(e?.text??"").replace(/\r\n?/g,"\n").trim();if(!e||!t)return void(this._decorTextDialog=null);const i=this._geometrySnapshot(),s=this._curSpaceCfg,n={color:e.color,opacity:jr(e.opacity),size_cm:Number(Math.max(.1,Math.min(wv,e.sizeCm)).toFixed(4)),...qr(e.angle)?{angle:qr(e.angle)}:{}};if(e.id)s.decor=this._decorList.map(i=>{if(i.id!==e.id)return i;if("text"!==i.kind)return i;if(e.preserveLegacy){const{angle:e,size:s,scale:o,...a}=i;return{...a,text:t,...n}}const{entity:s,attr:o,unit:a,...r}=i,{angle:l,size:c,scale:h,...d}=r;return{...d,text:t,...n}});else{const i="dc"+Date.now().toString(36)+Math.random().toString(36).slice(2,5);s.decor=[...this._decorList,{id:i,kind:"text",x:e.x,y:e.y,text:t,...n}],this._decorSel=i}this._decorTextDialog=null,this._recordGeometry(this._t(e.id?"history.decor_edit":"history.decor_add"),i),this._saveConfig(),this.requestUpdate()}_decorSaveShape(){const e=this._decorShapeDialog;if(!e)return;const t=this._geometrySnapshot(),i={color:e.color,opacity:jr(e.opacity),widthCm:Math.max(.1,Math.min(100,Number(e.widthCm)||.1)),fill:!!e.fill,fillColor:e.fillColor||e.color,fillOpacity:jr(e.fillOpacity,.25)};this._curSpaceCfg.decor=this._decorList.map(t=>{if(t.id!==e.id)return t;const s="rect"===e.kind||"ellipse"===e.kind,n=Kr(i,s);if("line"===t.kind){const i=(t.x1+t.x2)/2*Ov,s=(t.y1+t.y2)/2*this._decorH,o=Ur(Math.max(.1,Number(e.lengthCm)||.1),this._cellCm,this._gridPitch),a=qr(e.angle)*Math.PI/180,r=Math.cos(a)*o/2,l=Math.sin(a)*o/2,c=this._snap([i-r,s-l]),h=this._snap([i+r,s+l]),{width:d,line_style:p,...u}=t;return{...u,...n,x1:Gh(c[0]/Ov),y1:Gh(c[1]/this._decorH),x2:Gh(h[0]/Ov),y2:Gh(h[1]/this._decorH),..."dashed"===e.lineStyle?{line_style:"dashed"}:{}}}if("rect"===t.kind||"ellipse"===t.kind||"furniture"===t.kind){const i=t.w*Ov,s=t.h*this._decorH,o=Math.max(this._gridPitch,ri(Ur(Number(e.sizeWCm),this._cellCm,this._gridPitch),this._gridPitch)),a=Math.max(this._gridPitch,ri(Ur(Number(e.sizeHCm),this._cellCm,this._gridPitch),this._gridPitch)),r=t.x*Ov+i/2,l=t.y*this._decorH+s/2,c=qr(e.angle),h=Wr({x:r-o/2,y:l-a/2},c,e=>this._snap(e)),{width:d,angle:p,...u}=t;return{...u,...n,x:Gh(h[0]/Ov),y:Gh(h[1]/this._decorH),w:o/Ov,h:a/this._decorH,..."furniture"===t.kind&&e.symbol?{symbol:e.symbol}:{},...c?{angle:c}:{}}}return t}),this._decorStyle={...i,fill:"rect"===e.kind||"ellipse"===e.kind?i.fill:this._decorStyle.fill,fillColor:"rect"===e.kind||"ellipse"===e.kind?i.fillColor:this._decorStyle.fillColor,fillOpacity:"rect"===e.kind||"ellipse"===e.kind?i.fillOpacity:this._decorStyle.fillOpacity},this._decorShapeDialog=null,this._recordGeometry(this._t("history.decor_edit"),t),this._saveConfig(),this.requestUpdate()}get _dtSel(){return"decor"===this._mode&&"select"===this._decorTool&&this._decorSel&&this._decorList.find(e=>e.id===this._decorSel)||null}_dtPivot(e){return"line"===e.kind?[(e.x1+e.x2)/2*Ov,(e.y1+e.y2)/2*this._decorH]:"furniture"===e.kind||"rect"===e.kind||"ellipse"===e.kind?[(e.x+e.w/2)*Ov,(e.y+e.h/2)*this._decorH]:[e.x*Ov,e.y*this._decorH]}_dtApply(e,t){const i=this._curSpaceCfg;i&&(i.decor=this._decorList.map(i=>{if(i.id!==e)return i;const s={...i};"text"===i.kind&&void 0!==t.textSizeCm&&(delete s.size,delete s.scale);const n={...s};return void 0!==t.textSizeCm&&(n.size_cm=Number(Math.max(.1,Math.min(wv,t.textSizeCm)).toFixed(4))),void 0!==t.angle&&(t.angle?n.angle=Number(t.angle.toFixed(2)):delete n.angle),n}),this._cfgEpoch++,this.requestUpdate())}_dtStart(e,t,i,s){const n=this._dtSel;if(!n)return;e.stopPropagation(),e.preventDefault();const[o,a]=this._dtPivot(n),r=this._svgPoint(e),l=this._decorBoxOf(n);this._dtDrag={id:n.id,kind:t,pid:e.pointerId,ax:o,ay:a,r0:Math.hypot(r[0]-o,r[1]-a),a0:180*Math.atan2(r[1]-a,r[0]-o)/Math.PI,textSizeCm0:"text"===n.kind?this._decorTextSizeCm(n):1,angle0:"line"===n.kind?0:Number(n.angle)||0,sgx:i?.[0],sgy:i?.[1],orig:l||void 0,origShape:JSON.parse(JSON.stringify(n)),before:this._geometrySnapshot(),lineEnd:s,moved:!1},Gv(e)}_dtMove(e){const t=this._dtDrag;if(!t)return;const i=this._svgPoint(e);if(void 0!==t.lineEnd&&"line"===t.origShape.kind){const s=this._decorSnap(i,e.pointerType,t.id),n=Gh(s[0]/Ov),o=Gh(s[1]/this._decorH),a=t.origShape,r=0===t.lineEnd?a.x1:a.x2,l=0===t.lineEnd?a.y1:a.y2;return(Math.abs(n-r)>1e-9||Math.abs(o-l)>1e-9)&&(t.moved=!0),void this._replaceDecor(t.id,0===t.lineEnd?{x1:n,y1:o}:{x2:n,y2:o})}if("scale"===t.kind&&t.orig){const s=Xr(t.orig,t.sgx??1,t.sgy??1,i[0],i[1],!e.shiftKey,this._gridPitch,this._gridPitch),n=Math.abs(s.x-t.orig.x)>1e-6||Math.abs(s.y-t.orig.y)>1e-6||Math.abs(s.w-t.orig.w)>1e-6||Math.abs(s.h-t.orig.h)>1e-6;if(!n&&!t.moved)return;return t.moved||=n,void this._decorApplyBox(t.id,s)}if("scale"===t.kind){const e=Math.hypot(i[0]-t.ax,i[1]-t.ay);if(t.r0<1e-6)return;const s=Math.max(.1,Math.min(wv,t.textSizeCm0*(e/t.r0))),n=Math.abs(s-t.textSizeCm0)>1e-6;if(!n&&!t.moved)return;return t.moved||=n,void this._dtApply(t.id,{textSizeCm:s})}const s=180*Math.atan2(i[1]-t.ay,i[0]-t.ax)/Math.PI;let n=t.angle0+(s-t.a0);e.shiftKey||(n=5*Math.round(n/5)),n=(n%360+360)%360,n>180&&(n-=360);const o=Math.abs(n-t.angle0)>1e-6;(o||t.moved)&&(t.moved||=o,this._dtApply(t.id,{angle:n}))}_dtUp(){const e=this._dtDrag;this._dtDrag=null,e?.moved&&(this._recordGeometry(this._t("history.decor_transform"),e.before),this._saveConfig()),this.requestUpdate()}_dtMeasure(){const e=this._dtSel;if(!e)return void(this._dtBox&&(this._dtBox=null,this.requestUpdate()));let t;if("line"===e.kind){const i=e.x1*Ov,s=e.y1*this._decorH,n=e.x2*Ov,o=e.y2*this._decorH;t={id:e.id,x:Math.min(i,n),y:Math.min(s,o),w:Math.abs(n-i),h:Math.abs(o-s)}}else if("furniture"===e.kind||"rect"===e.kind||"ellipse"===e.kind)t={id:e.id,x:e.x*Ov,y:e.y*this._decorH,w:e.w*Ov,h:e.h*this._decorH};else{const i=this.renderRoot.querySelector(`text.dtext[data-id="${e.id}"]`);if(!i||"function"!=typeof i.getBBox)return;let s;try{s=i.getBBox()}catch{return}if(!s||!s.width&&!s.height)return;t={id:e.id,x:s.x,y:s.y,w:s.width,h:s.height}}const i=this._dtBox;i&&i.id===t.id&&Math.abs(i.x-t.x)<.01&&Math.abs(i.y-t.y)<.01&&Math.abs(i.w-t.w)<.01&&Math.abs(i.h-t.h)<.01||(this._dtBox=t,this.requestUpdate())}_deleteDecor(e){if(!this._decorList.some(t=>t.id===e))return;const t=this._geometrySnapshot();this._curSpaceCfg.decor=this._decorList.filter(t=>t.id!==e),this._decorSel===e&&(this._decorSel=null),this._recordGeometry(this._t("history.decor_delete"),t),this._saveConfig(),this.requestUpdate()}_decorDeleteSel(){this._decorSel&&this._deleteDecor(this._decorSel)}_confirmDecorErase(){const e=this._decorEraseConfirm;this._decorEraseConfirm=null,e&&this._deleteDecor(e.id)}get _furnWalls(){const e=this._rawPhysicalBodiesR().flatMap(e=>e.map((t,i)=>{const s=e[(i+1)%e.length];return[t[0],t[1],s[0],s[1]]}));return[...this._segments,...e]}get _furnWallReach(){return 6*this._gridPitch}_furnFieldValue(e){return Math.round(100*(this._imperial?e/30.48:e/100))/100}_furnFieldToCm(e){return t=this._imperial?30.48*e:100*e,Number.isFinite(t)?Math.max(1,Math.min(1e4,t)):1;var t}_furnPick(e){const t=function(e){const t=pd(e);return t?{w:t.w,h:t.h}:{w:60,h:60}}(e);this._furnPalette={symbol:e,w:t.w,h:t.h}}_furnPlace(e,t=!1){const i=this._furnPalette,s=this._curSpaceCfg;if(!i||!s)return;const n=Ov,o=this._decorH,a=fd(_d(i.w,this._cellCm,this._gridPitch,n)),r=fd(_d(i.h,this._cellCm,this._gridPitch,n)),l=this._geometrySnapshot(),c=this._decorSnap(e);let h=c[0],d=c[1],p=0;const u=t?null:wd(h,d,r*o,this._furnWalls,this._furnWallReach,this._gridPitch);u&&(h=u.cx,d=u.cy,p=u.angle);const _="df"+Date.now().toString(36)+Math.random().toString(36).slice(2,5),m={id:_,kind:"furniture",symbol:i.symbol,x:Gh(h/n-a/2),y:Gh(d/o-r/2),w:a,h:r,...Kr(this._decorStyle,!1)};p&&(m.angle=Number(p.toFixed(2))),s.decor=[...this._decorList,m],this._decorSel=_,this._decorTool="select",this._furnPalette=null,this._recordGeometry(this._t("history.decor_add"),l),this._saveConfig(),this.requestUpdate()}_furnMoveUpdate(e){const t=this._decorMove;if("furniture"!==t.orig.kind)return;const i=t.orig,s=this._curSpaceCfg;if(!s)return;const n=Ov,o=this._decorH,a=this._svgPoint(e),r=(i.x+i.w/2)*n+(a[0]-t.start[0]),l=(i.y+i.h/2)*o+(a[1]-t.start[1]);let c,h,d=Number(i.angle)||0;const p=e.shiftKey?null:wd(r,l,i.h*o,this._furnWalls,this._furnWallReach,this._gridPitch);if(p)c=p.cx/n-i.w/2,h=p.cy/o-i.h/2,d=p.angle;else{const s=this._decorSnap([r-i.w/2*n,l-i.h/2*o],e.pointerType,t.id);c=s[0]/n,h=s[1]/o}c=Gh(c),h=Gh(h),(Math.abs(c-i.x)>1e-9||Math.abs(h-i.y)>1e-9||Math.abs(d-(Number(i.angle)||0))>1e-9)&&(t.moved=!0),s.decor=this._decorList.map(e=>{if(e.id!==t.id)return e;const i={...e,x:c,y:h};return d?i.angle=Number(d.toFixed(2)):delete i.angle,i}),this.requestUpdate()}_decorApplyBox(e,t){const i=this._curSpaceCfg;if(!i)return;const s=Ov,n=this._decorH;i.decor=this._decorList.map(i=>{if(i.id!==e)return i;const o=Wr(t,i.angle,e=>this._snap(e));return{...i,x:Gh(o[0]/s),y:Gh(o[1]/n),w:Math.max(this._gridPitch/s,Math.min(2*Oh,t.w/s)),h:Math.max(this._gridPitch/n,Math.min(2*Oh,t.h/n))}}),this._cfgEpoch++,this.requestUpdate()}get _furnLive(){const e=this._dtDrag;if(!e||"scale"!==e.kind||!e.orig)return null;const t=this._decorList.find(t=>t.id===e.id);if(!t||"furniture"!==t.kind)return null;const i=Ov,s=this._decorH,n=t.w*i,o=t.h*s,a=function(e,t,i,s,n){const o=e+i/2,a=t+s/2,r=(Number(n)||0)*Math.PI/180,l=Math.cos(r),c=Math.sin(r),h=(e,t)=>{const i=e-o,s=t-a;return[o+i*l-s*c,a+i*c+s*l]};return[h(e,t),h(e+i,t),h(e+i,t+s),h(e,t+s)]}(t.x*i,t.y*s,n,o,Number(t.angle)||0),r=(e,t)=>[(e[0]+t[0])/2,(e[1]+t[1])/2],l=r(a[0],a[1]),c=r(a[0],a[3]);return[{x:l[0],y:l[1],text:this._fmtLen([0,0],[n,0])},{x:c[0],y:c[1],text:this._fmtLen([0,0],[0,o])}]}_renderFurnPalette(){const e=this._furnPalette,t=this._t(this._imperial?"gs.unit_ft":"gs.unit_m");return q`
e.stopPropagation()}> -
- ${this._t("furn.title")} - - -
-
- ${cd.map(t=>q` -
${this._t(`furn.group_${t}`)}
-
- ${ud(t).map(t=>q``)} -
`)} -
- ${e?q`
- - this._furnPalette={...e,w:this._furnFieldToCm(Number(t.target.value))}} /> - - this._furnPalette={...e,h:this._furnFieldToCm(Number(t.target.value))}} /> - ${this._t("furn.place_hint")} -
`:q`
${this._t("furn.pick_hint")}
`} -
`}get _bdBase(){const e=this._curSpaceCfg;return e?.plan_url?{...Rh(e.plan_aspect,Ov)}:null}get _bdRect(){const e=this._curSpaceCfg;return e?.plan_url?Fh(e,Ov):null}get _bdParams(){const e=this._curSpaceCfg,t=Number(e?.plan_x),i=Number(e?.plan_y),s=Number(e?.plan_scale),n=Number(e?.plan_scale_x),o=Number(e?.plan_scale_y),a=Number.isFinite(s)&&s>0?s:1;return{dx:Number.isFinite(t)?t:0,dy:Number.isFinite(i)?i:0,sx:Number.isFinite(n)&&n>0?n:a,sy:Number.isFinite(o)&&o>0?o:a,angle:qr(e?.plan_angle)}}_openBackdropDialog(e){if(!this._bdMovable||!this._bdRect)return;e?.preventDefault(),e?.stopPropagation(),this._bdDrag=null;const t=this._bdRect;this._backdropDialog={widthCm:Gr(t.w,this._cellCm,this._gridPitch),heightCm:Gr(t.h,this._cellCm,this._gridPitch),angle:this._angleField(t.angle)}}_saveBackdropDialog(){const e=this._backdropDialog,t=this._bdBase,i=this._bdRect;if(!e||!t||!i)return;const s=qr(e.angle),n=this._geometrySnapshot(),o=Math.min(t.w*zh,Math.max(t.w*Ah,ri(Ur(e.widthCm,this._cellCm,this._gridPitch),this._gridPitch))),a=Math.min(t.h*zh,Math.max(t.h*Ah,ri(Ur(e.heightCm,this._cellCm,this._gridPitch),this._gridPitch))),r=Wr({x:i.x+i.w/2-o/2,y:i.y+i.h/2-a/2},s,e=>this._snap(e));this._bdApply((r[0]-t.x)/Ov,(r[1]-t.y)/Ov,o/t.w,a/t.h,s),this._backdropDialog=null,this._recordGeometry(this._t("history.backdrop_transform"),n),this._saveConfig()}get _bdActive(){return"decor"===this._mode&&!!this._bdRect&&"backdrop"===this._decorTool}get _bdMovable(){return"decor"===this._mode&&"backdrop"===this._decorTool&&!!this._bdRect}_bdApply(e,t,i,s,n){const o=this._curSpaceCfg;if(!o)return;o.plan_x=Number(Gh(e).toFixed(6)),o.plan_y=Number(Gh(t).toFixed(6)),delete o.plan_scale,o.plan_scale_x=Number(Math.min(zh,Math.max(Ah,i)).toFixed(6)),o.plan_scale_y=Number(Math.min(zh,Math.max(Ah,s)).toFixed(6));const a=qr(n);a?o.plan_angle=Number(a.toFixed(2)):delete o.plan_angle,this._cfgEpoch++,this.requestUpdate()}_bdStart(e,t,i=!1){const s=this._bdBase,n=this._bdRect;if(!s||!n)return!1;const o=this._svgPoint(e),a=t?t[0]:0,r=t?t[1]:0,l=a>0?n.x:n.x+n.w,c=r>0?n.y:n.y+n.h;return this._bdDrag={kind:i?"rotate":t?"scale":"move",pid:e.pointerId,sx:o[0],sy:o[1],base:s,p0:this._bdParams,fx:l,fy:c,sgx:a,sgy:r,rect0:{x:n.x,y:n.y,w:n.w,h:n.h,angle:n.angle},before:this._geometrySnapshot(),moved:!1},Gv(e),!0}_bdMove(e){const t=this._bdDrag;if(!t)return;const i=this._svgPoint(e),s=t.base;if("move"===t.kind){const e=t.rect0.x,n=t.rect0.y,o=this._snap([e+(i[0]-t.sx),n+(i[1]-t.sy)]),a=Math.abs(o[0]-e)>1e-9||Math.abs(o[1]-n)>1e-9;if(!a&&!t.moved)return;return t.moved||=a,void this._bdApply((o[0]-s.x)/Ov,(o[1]-s.y)/Ov,t.p0.sx,t.p0.sy,t.p0.angle)}if("rotate"===t.kind){const s=t.rect0.x+t.rect0.w/2,n=t.rect0.y+t.rect0.h/2,o=180*Math.atan2(t.sy-n,t.sx-s)/Math.PI;let a=t.p0.angle+(180*Math.atan2(i[1]-n,i[0]-s)/Math.PI-o);e.shiftKey||(a=5*Math.round(a/5)),a=qr(a);const r=Math.abs(a-t.p0.angle)>1e-9;if(!r&&!t.moved)return;return t.moved||=r,void this._bdApply(t.p0.dx,t.p0.dy,t.p0.sx,t.p0.sy,a)}const n=Xr(t.rect0,t.sgx,t.sgy,i[0],i[1],!e.shiftKey,this._gridPitch,Math.min(s.w,s.h)*Ah),o=n.w/Math.max(1e-9,s.w),a=n.h/Math.max(1e-9,s.h),r=Math.abs(o-t.p0.sx)>1e-9||Math.abs(a-t.p0.sy)>1e-9||Math.abs(n.x-t.rect0.x)>1e-9||Math.abs(n.y-t.rect0.y)>1e-9;if(!r&&!t.moved)return;t.moved||=r;const l=Wr(n,t.p0.angle,e=>this._snap(e));this._bdApply((l[0]-s.x)/Ov,(l[1]-s.y)/Ov,o,a,t.p0.angle)}get _bdMoved(){if("decor"!==this._mode||!this._bdRect)return!1;const e=this._bdParams;return 0!==e.dx||0!==e.dy||1!==e.sx||1!==e.sy||0!==e.angle}_bdReset(){const e=this._curSpaceCfg;if(!e)return;const t=this._geometrySnapshot();delete e.plan_x,delete e.plan_y,delete e.plan_scale,delete e.plan_scale_x,delete e.plan_scale_y,delete e.plan_angle,this._bdDrag=null,this._recordGeometry(this._t("history.backdrop_transform"),t),this._saveConfig(),this._showToast(this._t("decor.backdrop_reset_done")),this.requestUpdate()}_bdUp(){const e=this._bdDrag;this._bdDrag=null,e?.moved&&(this._recordGeometry(this._t("history.backdrop_transform"),e.before),this._saveConfig()),this.requestUpdate()}get _bdLive(){if(!this._bdDrag)return null;const e=this._bdRect;return e?{x:e.x+e.w/2,y:e.y+e.h/2,text:`${this._fmtLen([0,0],[e.w,0])} × ${this._fmtLen([0,0],[0,e.h])}`}:null}_renderBackdropFrame(e){const t=this._bdRect;if(!this._bdActive||!t)return G;const i=.02*Math.max(e.w,e.h),s=i/4,n=t.x+t.w/2,o=t.y+t.h/2,a=qr(t.angle),r=2.2*i;return W` - - - {e.stopPropagation(),e.preventDefault(),this._bdStart(e,void 0,!0)}}> - - ${[[-1,-1,"nwse"],[1,-1,"nesw"],[1,1,"nwse"],[-1,1,"nesw"]].map(([e,n,o])=>{const a=e<0?t.x:t.x+t.w,r=n<0?t.y:t.y+t.h;return W`{t.stopPropagation(),t.preventDefault(),this._bdStart(t,[e,n])}}>`})} - `}_renderTextFrame(e){const t=this._dtSel,i=this._dtBox;if(!t||!i||i.id!==t.id)return G;const s=.018*Math.max(e.w,e.h),n=s/4;if("line"===t.kind){const e=[t.x1*Ov,t.y1*this._decorH],i=[t.x2*Ov,t.y2*this._decorH];return W` - - ${[e,i].map((e,t)=>W`this._dtStart(e,"scale",void 0,t)}> - `)} - `}const[o,a]=this._dtPivot(t),r=Number(t.angle)||0,l=2.2*s;return W` - - - this._dtStart(e,"rotate")}> - - ${[[-1,-1,"nwse"],[1,-1,"nesw"],[1,1,"nwse"],[-1,1,"nesw"]].map(([e,t,o])=>W`this._dtStart(i,"scale",[e,t])}>`)} - `}_renderDecorLayer(){const e=Ov,t=this._decorH,i="decor"===this._mode,s=i&&"erase"===this._decorTool,n=this._decorList.map(n=>{const o="dshape"+(i&&this._decorSel===n.id?" dsel":""),a=this._decorResolvedStyle(n),r=this._decorWidthUnits(n),l=e=>this._decorShapeDown(e,n),c=e=>this._decorShapeDbl(e,n);if("line"===n.kind)return W` - ${i&&"select"===this._decorTool?W``:G} - ${s?W``:G}`;if("rect"===n.kind){const i=(n.x+n.w/2)*e,h=(n.y+n.h/2)*t,d=qr(n.angle);return W` - ${s?W``:G}`}if("ellipse"===n.kind){const i=(n.x+n.w/2)*e,h=(n.y+n.h/2)*t,d=qr(n.angle);return W` - ${s?W``:G}`}if("furniture"===n.kind){const i=n.w*e,h=n.h*t,d=bd(n.symbol,i,h);if(!d)return G;const p=Number(n.angle)||0,u=n.x*e+i/2,_=n.y*t+h/2,m=`${p?`rotate(${p} ${u} ${_}) `:""}translate(${n.x*e} ${n.y*t})`;return W` - ${s?W``:G}`}if("text"===n.kind){const i=this._decorTextUnits(n),s=this._renderDeviceSnapshot?.facts.get(`decor:${this._space}:${n.id}`),r=function(e){return String(e??"").replace(/\r\n?/g,"\n").split("\n")}("string"==typeof s?s:es(n.text,n,this._renderPlanHass,e=>this._renderEntityAvailable(e))),h=n.x*e,d=n.y*t,p=Number(n.angle)||0,u=d-(r.length-1)*i*1.2/2;return W`${r.map((e,t)=>W`${e}`)}`}return G});let o=G;const a=this._decorDraft;if(a){const e=this._decorStyle,t=Ur(e.widthCm,this._cellCm,this._gridPitch);if("line"===a.kind)o=W``;else{const i=Math.min(a.a[0],a.b[0]),s=Math.min(a.a[1],a.b[1]),n=Math.abs(a.b[0]-a.a[0]),r=Math.abs(a.b[1]-a.a[1]);o="rect"===a.kind?W``:W``}}return W`${n}${o}`}get _editorToolbarGroups(){return"plan"!==this._mode?[]:[{id:"opening",label:this._t("markup.opening"),icon:"mdi:door",activeItemId:"opening"===this._tool?this._openingPreset?.type:void 0,items:[{id:"window",label:this._t("opening.window"),icon:"mdi:window-closed-variant",role:"tool",invoke:()=>this._activateOpeningPlacement("window")},{id:"door",label:this._t("opening.door"),icon:"mdi:door-open",role:"tool",invoke:()=>this._activateOpeningPlacement("door")},{id:"passage",label:this._t("opening.passage"),icon:"mdi:arch",role:"tool",invoke:()=>this._activateOpeningPlacement("passage")},{id:"gate",label:this._t("opening.gate"),icon:"mdi:gate",role:"tool",invoke:()=>this._activateOpeningPlacement("gate")}]}]}_renderEditorGroupLauncher(e){return this._editorSecondary.renderGroupLauncher(e,this._editorToolbarGroups,this._editorSecondaryCopy)}get _editorSecondaryContextId(){const e=`editor:${this._mode}:${this._space}:${this._cfgEpoch}`,t=this._editorSecondary.activeGroup(this._editorToolbarGroups);if(t)return`${e}:group:${t.id}:${this._editorSecondary.groupGeneration}`;if("plan"===this._mode){const t=this._physicalSel;return t?`${e}:selection:${t.kind}:${t.id}:${t.segment??""}`:`${e}:tool:${this._tool}:${this._path.length}`}return"decor"===this._mode?"furniture"===this._decorTool?`${e}:palette:furniture:${this._furnPalette?.symbol||"none"}`:"select"===this._decorTool&&this._decorSel?`${e}:selection:decor:${this._decorSel}`:`${e}:tool:${this._decorTool}:${this._bdMoved?1:0}:${this._bdDrag?1:0}`:`${e}:none`}_runEditorContext(e,t){this._editorSecondary.runContext(e,this._editorSecondaryContextId,t)}_renderEditorGroupModel(e){return this._editorSecondary.renderGroupModel(e,this._editorSecondaryContextId,this._editorSecondaryCopy)}_renderDrawWallControl(){const e="column"===this._tool?1:0;return q``}_renderPlanSecondary(){const e=this._editorSecondaryContextId,t=this._physicalSel;if(t){if(!("partition"===t.kind?!!this._curSpaceCfg.partitions?.some(e=>e.id===t.id):"column"===t.kind?!!this._curSpaceCfg.wall_columns?.some(e=>e.id===t.id):!!this._curSpaceCfg.room_drafts?.some(e=>e.id===t.id)))return null;const i="partition"===t.kind?this._t("markup.partition"):"column"===t.kind?this._t("markup.column"):this._t("markup.add");return{contextId:e,kind:"selection",ariaLabel:this._t("editor.context_actions",{object:i}),visibleLabel:i,content:q` - - `}}const i="draw"===this._tool||"column"===this._tool,s="column"===this._tool?"markup.hint_column":"resize"===this._tool?"markup.hint_resize":"wallthick"===this._tool?"markup.hint_wallthick":null,n="draw"===this._tool?this._t(this._path.length?"markup.hint_points":"markup.hint_start",this._path.length?{n:this._path.length}:void 0):"";if(!i&&!s&&!n)return null;const o="draw"===this._tool&&this._path.length>0;return{contextId:e,kind:o?"operation":"tool",ariaLabel:this._t("editor.tool_options",{tool:this._t("draw"===this._tool?"markup.add":"column"===this._tool?"markup.column":"resize"===this._tool?"markup.resize":"markup.wallthick")}),content:q` - ${i?this._renderDrawWallControl():G} - ${n?q`${n}`:G} - ${s?q`${this._t(s)}`:G} - ${"draw"===this._tool&&this._path.length?q``:G}`}}_renderDecorSecondary(){const e=this._editorSecondaryContextId;if("furniture"===this._decorTool)return{contextId:e,kind:"palette",ariaLabel:this._t("editor.palette",{tool:this._t("decor.furniture")}),launcherId:"furniture",dismissPolicy:this._furnPalette?"stay-open-on-canvas":"outside",dismiss:()=>{"furniture"===this._decorTool&&(this._furnPalette=null,this._decorTool="select",this.requestUpdate())},content:this._renderFurnPalette()};const t=this._dtSel;if(t){const i=this._t(`decor.${t.kind}`);return{contextId:e,kind:"selection",ariaLabel:this._t("editor.context_actions",{object:i}),visibleLabel:i,content:q` - - `}}const i="line"===this._decorTool||"rect"===this._decorTool||"ellipse"===this._decorTool,s="rect"===this._decorTool||"ellipse"===this._decorTool,n="backdrop"===this._decorTool&&!!this._bdRect;return i||n?{contextId:e,kind:"tool",ariaLabel:this._t("editor.tool_options",{tool:this._t(i?`decor.${this._decorTool}`:"decor.backdrop")}),content:q` - ${i?q` - this._decorStyle={...this._decorStyle,...e.detail}}> - - ${s?q` - this._decorStyle={...this._decorStyle,fillColor:e.detail.color,fillOpacity:e.detail.opacity}}>`:G} - `:G} - ${n?q`${this._t("decor.backdrop_hint")}`:G}`}:null}_withBackdropReset(e){if(!this._bdMoved||this._bdDrag||"palette"===e?.kind)return e;const t=this._editorSecondaryContextId,i=q``;return e?{...e,kind:"selection"===e.kind?"mixed":e.kind,content:q`${e.content}${i}`}:{contextId:t,kind:"tool",ariaLabel:this._t("editor.tool_options",{tool:this._t("decor.backdrop")}),content:i}}get _editorSecondaryDialogBlocked(){return!!(this._tapConfirm||this._vacCalConfirm||this._roomDialog||this._mergeDialog||this._openingDialog||this._physicalDialog||this._openingInfo||this._decorTextDialog||this._decorShapeDialog||this._backdropDialog||this._decorEraseConfirm||this._spaceDialog||this._markerDialog||this._deviceInbox||this._infoCard||this._rulesDialog||this._settingsDialog||this._alignDialog||this._importDialog||this._kioskDialog||this._backupExportDialog||this._backupImportDialog||this._wallDialog)}_renderEditorSecondary(){if(!this._editing)return G;const e=this._editorSecondary.activeGroup(this._editorToolbarGroups),t=e?this._renderEditorGroupModel(e):"plan"===this._mode?this._renderPlanSecondary():"decor"===this._mode?this._withBackdropReset(this._renderDecorSecondary()):null;return this._editorSecondary.render(t,this._editorSecondaryDialogBlocked)}_renderDecorBar(){const e=[["select","mdi:cursor-default-outline","decor.select"],...this._bdRect?[["backdrop","mdi:image-move","decor.backdrop"]]:[],["line","mdi:vector-line","decor.line"],["rect","mdi:rectangle-outline","decor.rect"],["ellipse","mdi:ellipse-outline","decor.ellipse"],["text","mdi:format-text","decor.text"],["furniture","mdi:sofa-outline","decor.furniture"],["erase","mdi:eraser","decor.erase"]],t=this._geometryHistory.undoName,i=this._geometryHistory.redoName;return q`
-
- ${e.map(([e,t,i])=>q``)} - ${this._editorToolbarGroups.map(e=>this._renderEditorGroupLauncher(e))} - - -
-
- -
-
`}_renderDecorEraseConfirm(){const e=this._decorEraseConfirm,t=this._t(`decor.${e.kind}`);return q`this._decorEraseConfirm=null}> -

${this._t("confirm.erase_decor",{kind:t})}

-
- - - -
-
`}_renderDecorTextDialog(){const e=this._decorTextDialog,t=(e.pickerEntity||"").trim(),i=t?this.hass?.states?.[t]:null;return q`this._decorTextDialog=null}> -
- - ${""} - - this._decorTextDialog={...e,...t.detail}}> - -
this._decorTextDialog={...e,sizeCm:this._decorTextCm(Number(t.target.value))}} /> - ${this._t(this._imperial?"wallthick.unit_in":"wallthick.unit_cm")}
- - this._decorTextDialog={...e,angle:t.target.value}} /> - - - this._decorTextDialog={...e,pickerEntity:t.target.value}} /> - - ${Object.keys(this.hass?.states||{}).map(e=>q``)} - - ${t?q` - - - `:G} -
-
- - - -
-
`}_renderDecorShapeDialog(){const e=this._decorShapeDialog,t="rect"===e.kind||"ellipse"===e.kind,i=this._t("decor."+e.kind),s=this._t(this._imperial?"gs.unit_ft":"gs.unit_m");return q`this._decorShapeDialog=null}> -
- ${"furniture"===e.kind?q` - - `:G} - this._decorShapeDialog={...e,...t.detail}}> - -
this._decorShapeDialog={...e,widthCm:this._decorSmallCm(Number(t.target.value))}} />${this._t(this._imperial?"wallthick.unit_in":"wallthick.unit_cm")}
- ${"line"===e.kind?q` - -
- - -
- -
this._decorShapeDialog={...e,lengthCm:this._decorLargeCm(Number(t.target.value))}} /> - ${s}
`:q` - -
this._decorShapeDialog={...e,sizeWCm:this._decorLargeCm(Number(t.target.value))}} /> - ×this._decorShapeDialog={...e,sizeHCm:this._decorLargeCm(Number(t.target.value))}} /> - ${s}
`} - - this._decorShapeDialog={...e,angle:t.target.value}} /> - ${t?q` - this._decorShapeDialog={...e,fillColor:t.detail.color,fillOpacity:t.detail.opacity}}>`:G} -
-
- - - -
-
`}_renderBackdropDialog(){const e=this._backdropDialog,t=this._t(this._imperial?"gs.unit_ft":"gs.unit_m");return q`this._backdropDialog=null}> -
- -
this._backdropDialog={...e,widthCm:this._decorLargeCm(Number(t.target.value))}} /> - ×this._backdropDialog={...e,heightCm:this._decorLargeCm(Number(t.target.value))}} /> - ${t}
- - this._backdropDialog={...e,angle:t.target.value}} /> -
-
- - -
-
`}_cssPxToRender(e){const t=this._stageEl,i=this._viewOr(this._baseVb());return t?.clientWidth&&t.clientHeight?Math.max(i.w/t.clientWidth,i.h/t.clientHeight)*e:this._gridPitch/8*e}_renderZeroWalls(e){if(e&&!e.showBorders&&!this._editing)return W``;const t=this._zeroWalls();if(!t.lines.length)return W``;const i=e?.color||"var(--hp-muted)";return W` - ${t.lines.map(e=>W``)} - `}_zeroWalls(){const e=this._spaceModel();return e?X_(this._curSpaceCfg,e,Ov,.02*this._gridPitch):{style:V_(this._curSpaceCfg),lines:[],contour:[],barriers:[],transmissive:[]}}_openCuts(){return this._zeroWalls().contour}_deleteRoomClick(e){const t=this._spaceModel();if(!t)return;const i=[...t.rooms].reverse().find(t=>this._pointInRoom(e,t));i?i.id&&(this._roomDeleteDialog={roomId:i.id,name:i.name}):this._showToast(this._t("toast.delete_room_pick"))}get _wallKeyPitch(){return jh}get _spaceWalls(){const e=this._curSpaceCfg?.walls;return Array.isArray(e)?e:[]}_cfgOpenCuts(){const e=this._curSpaceCfg,t=this._spaceModel();if(!e||!t)return[];const i=K_(e,1),s=J_(e,t.rooms,Ov,.02*this._gridPitch).map(e=>e.map(e=>e/Ov)),n=new Set;return[...i,...s].filter(e=>{const t=e.map(e=>Number(e).toFixed(9)).join(","),i=[e[2],e[3],e[0],e[1]].map(e=>Number(e).toFixed(9)).join(","),s=t[e[0]/Ov,e[1]/Ov,e[2]/Ov,e[3]/Ov]))}_paperShapes(e){if(!this._spaceWalls.length)return di(e);const t=this._wallUnionGeometry();return t?.paperD?[{path:t.paperD}]:di(e)}_wallUnionGeometry(){const e=this._spaceModel();if(!e)return null;const t=this._spaceWalls,i=this._physicalBodiesR();if(!t.length&&!i.length)return null;const s=`${this._space}|${this._cfgEpoch}|${e.rooms.length}`;if(!this._wallUnionCache||this._wallUnionCache.key!==s){const n=Sv(this._wallUnionPool,s);if(n.hit)this._wallUnionCache=n.value;else{const n=this._openCuts(),o=this._roomWallOpeningInputs(),a=Wc(e.rooms,t,n,o,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov,i);a&&Object.defineProperty(a,"sourceFingerprint",{value:Jd([this._curSpaceCfg,this._cellCm,this._gridPitch]),enumerable:!1});const r={key:s,value:a};Mv(this._wallUnionPool,s,r,8),this._wallUnionCache=r}}return this._wallUnionCache.value}_thickWallCuts(){const e=this._spaceModel();if(!e)return[];const t=this._spaceWalls;if(!t.length)return[];const i=this._openCuts();return Gc(e.rooms,t,i,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov).map(e=>[e.a[0],e.a[1],e.b[0],e.b[1]])}_innerRoomContour(e,t,i=this._openCuts(),s=this._wallUnionGeometry()?.roomGeom,n=this._wallUnionGeometry()?.multiWallNodes){const o=i.map(e=>e.join(",")).join(";"),a=`${e.id}|${this._cfgEpoch}|${t}|${o}`,r=Sv(this._innerContourCache,a);if(r.hit)return r.value;const l=Cc(e.rooms,t,this._spaceWalls,i,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov,s,n);return Mv(this._innerContourCache,a,l,600),l}_wallThickHit(e){const t=this._spaceModel();if(!t)return null;const i=6*this._gridPitch,s=this._openCuts();let n=null;const o=(e,t,s)=>{t>i||(!n||t0?e:0,n=Number.isFinite(t)&&t>0?t:5,o=Number.isFinite(i)&&i>0?i:0;return s>0?Ml(s,n,o)/2:Dh(1.5*o,n)}(e.cm,this._cellCm,this._gridPitch);let i="";for(const s of e.segs)i+=(i?" ":"")+tc([[s[0],s[1]],[s[2],s[3]]],t,!1);return{segs:e.segs,open:e.open,d:i}}_wallThickClick(e){const t=this._wallThickHit(e);if(!t)return void this._showToast(this._t("toast.wallthick_pick"));const i=t.cm,s=this._viewOr(this._baseVb()),n=(t.a[0]+t.b[0])/2,o=(t.a[1]+t.b[1])/2;this._wallDialog={a:t.a,b:t.b,value:Sl(i,this._imperial),roomId:t.roomId,source:t.source,sx:(n-s.x)/s.w*100,sy:(o-s.y)/s.h*100}}_wallThickApply(e){const t=this._wallDialog;if(!t)return;const i=this._curSpaceCfg,s=this._spaceModel();if(!i||!s)return;const n=t.value.trim(),o=n?$v(n):null;if(null==o)return void this._showPhysicalRange(100);const a=this._imperial?2.54*o:o;if(!Number.isFinite(a)||a<0||a>100)return void this._showPhysicalRange(100);if("room"!==t.source.kind){const e=this._geometrySnapshot();if("partition"===t.source.kind){const e=(i.partitions||[]).find(e=>e.id===t.source.id);if(!e)return;if(0===a&&Z_(i.openings,{kind:"partition",id:e.id}))return void this._showToast(this._t("toast.zero_wall_opening_conflict"));e.cm=a}else{const e=(i.room_drafts||[]).find(e=>e.id===t.source.id),s=e?.segments?.[t.source.segment];if(!s)return;s.cm=a}this._wallDialog=null;return this._commitPhysicalGeometry(this._t("history.wall_thickness"),e)&&this._showToast(this._t("toast.wallthick_set")),void this.requestUpdate()}const r=this._geometrySnapshot(),l=a>0?a:null;if(0===a){const s=.02*this._gridPitch,n=[t.a[0],t.a[1],t.b[0],t.b[1]],o=(i.rooms||[]).find(e=>e.id===t.roomId),a=new Set(e&&o&&Array.isArray(o.wall_ids)?o.wall_ids:(i.wall_segments||[]).filter(e=>{const t=[Number(e.a?.[0])*Ov,Number(e.a?.[1])*Ov],i=[Number(e.b?.[0])*Ov,Number(e.b?.[1])*Ov];return Hs(t,n)<=s&&Hs(i,n)<=s}).map(e=>String(e.id))),r=(i.openings||[]).some(e=>{if("partition"===e.host?.kind)return!1;if("wall"===e.host?.kind&&a.has(e.host.id))return!0;if(e.host)return!1;const t=[Number(e.x)*Ov,Number(e.y)*Ov];return t.every(Number.isFinite)&&Hs(t,n)<=s});if(r)return void this._showToast(this._t("toast.zero_wall_opening_conflict"))}let c,h=this._openCuts();if(a>0&&h.length){const n=[];if(e&&t.roomId){const e=(i.rooms||[]).find(e=>e.id===t.roomId),o=new Set(Array.isArray(e?.wall_ids)?e.wall_ids:[]);for(const e of i.wall_segments||[])o.has(e.id)&&n.push([Number(e.a?.[0])*Ov,Number(e.a?.[1])*Ov,Number(e.b?.[0])*Ov,Number(e.b?.[1])*Ov]);if(!n.length){const e=s.rooms.find(e=>e.id===t.roomId),i=e?hi(e):null;for(let e=0;i&&e!n.some(t=>Hs([e[0],e[1]],t)<=o&&Hs([e[2],e[3]],t)<=o&&Hs([t[0],t[1]],e)<=o&&Hs([t[2],t[3]],e)<=o))}c=e&&t.roomId?Gl(i.walls,s.rooms,t.roomId,l,this._wallKeyPitch,h,Ov):Ul(i.walls,t.a,t.b,l,this._wallKeyPitch,Ov),c=this._normalizeWalls(c,h);for(const e of i.wall_segments||[])e.cm=Hl(c,e.a,e.b,jh,1);c.length?i.walls=c:delete i.walls,this._wallDialog=null,this._commitPhysicalGeometry(this._t("history.wall_thickness"),r)&&this._showToast(this._t(0===a?"toast.wallthick_cleared":"toast.wallthick_set")),this.requestUpdate()}_wallHatchDefs(e){if(!this._spaceWalls.length&&!this._physicalBodiesR().length&&!this._markup)return W``;const t=kl(this._cellCm);return W` - - - - `}_resolvedRoomFills(e,t){const i=new Map,s=new Map;for(const n of e.rooms){const e=this._roomDialog&&n.id===this._roomEditId?this._roomFill||t.fill:zs(t.fill,n),o=this._roomDialog&&n.id===this._roomEditId?this._roomCustomFill||t.customFill:ls(t.customFill,n),a=_s(e,"lqi"===e&&n.area?this._roomLqi(n.area):null,"light"===e?uo(ho(this._renderPlanHass,this._renderDevices,n,this._virtualLights)):"none","temp"===e?this._roomTemp(n):null,t.tempMin,t.tempMax,this._fillColors,o);i.set(n,a),n.id&&s.set(n.id,a)}return{byRoom:i,byId:s}}_spaceDisplayForRender(){const e=cs(this._curSpaceCfg),t=this._spaceDialog;return t&&"edit"===t.mode&&t.spaceId===this._space?{...e,showBorders:t.showBorders,showNames:t.showNames,hideDecor:t.hideDecor,hideOpenings:t.hideOpenings,color:t.roomColor,opacity:t.roomOpacity,fill:t.fillMode,customFill:t.customFill?rs(t.customFill):as,glow:t.glowEnabled,tempMin:t.tempMin,tempMax:t.tempMax,showLqi:t.showLqi,cardFontScale:t.cardFontScale,labelTemp:t.labelTemp,labelHum:t.labelHum,labelLqi:t.labelLqi,labelLight:t.labelLight}:e}_openingWallIndexFor(e,t){const i=e.rooms.map(e=>`${e.id}:${e.poly?.map(e=>e.join(",")).join("/")||`${e.x},${e.y},${e.w},${e.h}`}`).join(";"),s=this._spaceWalls.map(e=>`${e.key}:${e.a?.join(",")||""}:${e.b?.join(",")||""}:${e.cm}`).join(";"),n=t.map(e=>e.join(",")).join(";"),o=[e.id,this._cfgEpoch,this._wallKeyPitch,this._cellCm,this._gridPitch,i,s,n].join("|");let a=this._openingWallIndexCache.get(o);return a||(a=Kc(e.rooms,this._spaceWalls,t,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov)),Mv(this._openingWallIndexCache,o,a,4),{key:o,value:a}}_renderOpeningTunnelFills(e,t,i="data"){if(this._markup||!this._spaceWalls.length||!this._openingsR.length)return W``;if("glow-base"===i&&![...t.byRoom.values()].some(Boolean))return W``;const s=this._openCuts(),n=this._openingsR.map(e=>({x:e.rx,y:e.ry,angle:e.angle,length:e.rlen})),o=n.map(e=>`${e.x},${e.y},${e.angle},${e.length}`).join(";"),a=this._openingWallIndexFor(e,s),r=`${a.key}|${o}`;return this._openingTunnelCache&&this._openingTunnelCache.key===r||(this._openingTunnelCache={key:r,value:th(a.value,n)}),j_({openings:this._openingsR,geometries:this._openingTunnelCache.value,fillsByRoomId:t.byId,idPrefix:`${e.id}-${i}`,groupClass:"data"===i?"opening-tunnels":"opening-tunnels glow-base-tunnels",dataLayer:i})}_resolvedGlowBase(e,t,i){const s=new Map,n=new Map,o=this._fillColors.glow_base;for(const a of e.rooms){const e=i.byRoom.get(a),r=Fs(t.glow,a)&&(!e||e.opacity<=0)?{color:o.c,opacity:o.a,mode:"glow"}:null;s.set(a,r),a.id&&n.set(a.id,r)}return{byRoom:s,byId:n}}_renderGlowBaseRooms(e,t){if(this._markup||![...t.byRoom.values()].some(Boolean))return W``;const i=new Map(e.rooms.map(e=>[e,hi(e)])),s=this._openCuts(),n=this._wallUnionGeometry()?.roomGeom,o=e=>"M "+e.map(e=>e[0]+" "+e[1]).join(" L ")+" Z",a=e.rooms.map(a=>{const r=t.byRoom.get(a)||null,l=i.get(a)||null;if(!r||!l)return G;const c=this._spaceWalls.length&&a.id&&this._innerRoomContour(e,a.id,s,n)||l,h=Pi(c,e.rooms.filter(e=>e!==a).map(e=>i.get(e)).filter(e=>!!e)),d=this._cleanFloor(a,c,e).path;return d||h.length?W``:a.poly||c!==l?W``:W``});return W``}_renderWallBodies(e){if("iso"===this._renderProjection)return W``;if(e&&!e.showBorders&&("view"===this._mode||"devices"===this._mode))return W``;const t=this._wallUnionGeometry();if(!t)return W``;const i=this._stageEl,s=this._viewOr(this._baseVb()),n=i&&i.clientWidth&&s.w?i.clientWidth/s.w:1,o=e?.color||"#607d8b",a=wl(t.depthUnits,n)||xl(kl(this._cellCm),n),r=this._fillColors.wall_fill;return W` - ${t.paths.map(e=>W` - - `)} - `}_isoDecorationLayers(e){const t="undefined"==typeof CSS||"function"!=typeof CSS.supports||CSS.supports("filter","blur(1px)"),i="function"==typeof matchMedia&&matchMedia("(forced-colors: active)").matches;return function(e){const t=!!e.showBorders;return{structural:t,panels:t&&!e.hideOpenings,shadows:t&&e.filtersSupported&&!e.forcedColors,materialNuance:t&&!e.forcedColors,floorSymbols:!e.hideOpenings&&!t}}({showBorders:e.showBorders,hideOpenings:e.hideOpenings,filtersSupported:t,forcedColors:i})}_isoOpeningPanels(e){if(!e.panels)return[];const t=this._isoScene();if(!t)return[];return t.openings.flatMap(e=>{const t=this._openingsR[e.sourceIndex];return t?Kf(e,this._openingAmt(t)):[]}).sort((e,t)=>e.depth-t.depth||e.sourceIndex-t.sourceIndex||e.leaf-t.leaf)}_renderIsoDefs(e,t){const i=Ch(this._cellCm);return W` - ${"walls"===t&&e.materialNuance?W` - - - - - - `:G} - ${"underlay"===t&&e.shadows?W` - - - `:G} - ${"shadows"===t&&e.shadows?W` - - - - - - `:G} - `}_renderIsoUnderlay(e){if(!e.structural)return W``;const t=this._isoScene()?.floor;return t?W``:W``}_renderIsoShadows(e,t){if(!e.shadows)return W``;const i=this._isoScene()?.geometry;return i?W``:W``}_renderIsoWalls(e,t){if("iso"!==this._renderProjection)return W``;if(!e.structural)return W``;const i=this._isoScene();return i?W` - ${this._renderIsoDefs(e,"walls")} - ${i.geometry.sides.map(e=>W``)} - - ${e.panels?W``:G} - `:W``}_roomHoverPaths(e){const t=this._hoverRoom;if("view"!==this._mode||!t||t.space!==e.id)return null;const i=e.rooms.find(e=>e===t.room||!!e.id&&e.id===t.room.id);if(!i)return null;const s=hi(i);if(!s)return null;const n=e.rooms.filter(e=>e!==i).map(e=>({room:e,poly:hi(e)})).filter(e=>!!e.poly),o=Pi(s,n.map(e=>e.poly)),a=this._openCuts(),r=.02*this._gridPitch,l=a.filter(e=>{const t=[(e[0]+e[2])/2,(e[1]+e[3])/2];return s.some((e,i)=>Hs(t,[e[0],e[1],...s[(i+1)%s.length]])<=4*r)}),c=this._spaceWalls,h=this._wallUnionGeometry()?.roomGeom,d=c.length&&i.id&&this._innerRoomContour(e,i.id,a,h)||s,p=[{axis:s,face:d}];for(const t of o){const i=n.find(e=>e.poly===t)?.room;let s=t;if(c.length&&i?.id){const n=yc(e.rooms,i.id,c,a,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov);n&&(s=Vc(n.poly,n.offsets)||t)}p.push({axis:t,face:s})}const u=this._roomWallOpeningInputs(this._openingsR,e).map(e=>{const t=e.angle*Math.PI/180,i=Math.cos(t)*e.length/2,s=Math.sin(t)*e.length/2;return[e.x-i,e.y-s,e.x+i,e.y+s]}),_=l.concat(u),m=p.map(({axis:e,face:t})=>{const i=_.map(i=>((e,t,i)=>{const s=e[2]-e[0],n=e[3]-e[1],o=Math.hypot(s,n);if(o.05)&&Hs([c,h],[i[0],i[1],s[0],s[1]])<=4*r){d=!0;break}}if(!d)return null;let p=null;for(let e=0;e.05)continue;const u=Hs([c,h],[t[0],t[1],s[0],s[1]]);(!p||u!!e);return i.length?Ps(t,i,r).map(e=>`M ${e[0]} ${e[1]} L ${e[2]} ${e[3]}`).join(" "):`M ${t.map(e=>`${e[0]} ${e[1]}`).join(" L ")} Z`}).filter(Boolean).join(" ");if(!m)return null;const g=e=>`M ${e.map(e=>`${e[0]} ${e[1]}`).join(" L ")} Z`;return{fillD:[this._cleanFloor(i,d,e).path||g(d),...o.map(g)].join(" "),outlineD:m}}_renderRoomHoverFill(e){return e?W` - - `:W``}_renderRoomHoverOutline(e){return e?W` - - - `:W``}_renderWallThickUi(){const e=this._wallThickHover;return e&&e.d?W``:W``}_renderWallThickDialog(){const e=this._wallDialog;return e?q`
e.stopPropagation()}> -
- - {this._wallDialog={...e,value:t.target.value}}} - @keydown=${e=>{"Enter"===e.key&&(e.preventDefault(),this._wallThickApply(!1))}} /> - ${this._t(this._imperial?"wallthick.unit_in":"wallthick.unit_cm")} -
-
- ${"room"===e.source.kind?q``:G} - - -
-
`:q``}_openingAt(e){if(!this._openingsR.length)return null;const t=this._openingsR.flatMap(t=>{const i=t.angle*Math.PI/180,s=e[0]-t.rx,n=e[1]-t.ry,o=s*Math.cos(i)+n*Math.sin(i);return Math.abs(o)>t.rlen/2+Dh(12,this._cellCm)?[]:[{o:t,localY:-s*Math.sin(i)+n*Math.cos(i)}]});if(!t.length)return null;const i=this._spaceModel();if(!i)return null;const s=this._openCuts(),n=this._openingWallIndexFor(i,s).value;return t.find(({o:e,localY:t})=>{const i="gate"===e.type?!e.flip_v:e.flip_v,s=e.partitionHost||this._spaceWalls.length||"gate"===e.type?this._openingFace(e,n,!!i):{ox:0,oy:0,cm:0,side:-1},o=y_({type:e.type,length:e.rlen,angle:e.angle,amount:this._openingAmt(e),flipH:!!e.flip_h,flipV:!!e.flip_v,cellCm:this._cellCm,gridPitch:this._gridPitch,face:s});return Math.abs(t)<=o.hitHalf})?.o||null}_resolveOpeningPlacement(e){const t=this._openingPreset;if("opening"!==this._tool||!t)return null;const i=this._spaceModel();if(!i)return null;const s=this._openCuts(),n=this._openingWallIndexFor(i,s),o=`${n.key}|partitions:${this._cfgEpoch}`;this._openingPlacementIntervalsCache&&this._openingPlacementIntervalsCache.key===o||(this._openingPlacementIntervalsCache={key:o,value:[...wc(i.rooms,this._spaceWalls,s,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov),...hu(i.partitions,this._cellCm,this._gridPitch)]}),this._openingDimensionContextCache&&this._openingDimensionContextCache.key===o||(this._openingDimensionContextCache={key:o,value:cv({rooms:i.rooms,walls:this._spaceWalls,openCuts:s,partitions:i.partitions,roomOpenings:this._roomWallOpeningInputs(),partitionCuts:this._partitionOpeningCuts(i),pitch:this._wallKeyPitch,cellCm:this._cellCm,gridPitch:this._gridPitch,coordScale:Ov,epsilon:2e-4*this._gridPitch})});const a=ev({pointer:[e[0],e[1]],preset:t,geometryRevision:this._cfgEpoch,renderedLength:this._cmToUnits(t.lengthCm),intervals:this._openingPlacementIntervalsCache.value,baseTolerance:1.5*this._gridPitch,bodyPointerPadding:this._cssPxToRender("touch"===this._pointerModality.modality||"pen"===this._pointerModality.modality?10:6),gridStep:this._gridPitch});this._openingJambBlockCm=a.jambBlockedTarget?a.jambBlockedTarget.physicalHalfWidth/this._gridPitch*this._cellCm:null;const r=a.candidate;if(!r)return null;const l="gate"===r.type?!r.flipV:r.flipV;let c;if(r.host){const e=ou({id:"preview",type:r.type,x:r.x/Ov,y:r.y/Ov,angle:r.angle,length:r.renderedLength/Ov,host:r.host},i.partitions,Ov,this._cellCm,this._gridPitch).resolved;c=e?cu(e,l):{ox:0,oy:0,cm:0,side:-1}}else c=Zc(n.value,{x:r.x,y:r.y,angle:r.angle,length:r.renderedLength,flip_v:l});const h="mi"===this.hass?.config?.unit_system?.length,d=gv(r,this._openingDimensionContextCache.value).map(e=>({x:e.label[0],y:e.label[1],text:li(e.distance/this._gridPitch*this._cellCm,h),dimension:e}));return{...r,face:c,measure:{labels:d,guide:r.measure.guide}}}_activateOpeningPlacement(e){this._activateMarkupTool("opening"),"opening"===this._tool&&(this._openingPreset=function(e,t){return{type:e,lengthCm:Jf(e),flipH:!1,flipV:!1,revision:t}}(e,++this._openingPresetRevision),this._openingHoverCandidate=null,this._cursorPt=null)}_clearOpeningPlacement(e){this._openingHoverCandidate=null,this._openingJambBlockCm=null,e&&(this._openingPreset=null,this._openingRebindId=null)}_openingClick(e){const t=this._spaceModel();if(!t)return;const i=this._openingRebindId?null:this._openingAt(e);if(i)return void this._editOpening(i);const s=this._openingPreset;if(!s)return;const n=this._openingHoverCandidate,o=n&&tv(n,[e[0],e[1]],s.revision,this._cfgEpoch)?n:this._resolveOpeningPlacement(e);if(!o){if(null!=this._openingJambBlockCm)return void this._showToast(this._t("opening.partition_jamb_margin",{distance:li(this._openingJambBlockCm,this._imperial)}));const i=1.5*this._gridPitch,s=ui(e,t.rooms,i);return s&&function(e,t,i,s,n){for(const o of s)if(!(Hs([e,t],o)>n)&&Bl([o[0],o[1]],[o[2],o[3]],i))return!0;return!1}(s.x,s.y,s.angle,this._openCuts(),i)?void this._showToast(this._t("toast.opening_on_zero_wall")):void this._showToast(this._t("toast.opening_no_wall"))}const a=this._openingRebindId?this._curSpaceCfg?.openings?.find(e=>e.id===this._openingRebindId):null;this._openingDialog={...a?{id:a.id,type:a.type,lengthCm:Math.round(a.length*Ov/this._gridPitch*this._cellCm),lengthTouched:!1,contact:a.contact||"",lock:a.lock||"",invert:!!a.invert,flipH:!!a.flip_h,flipV:!!a.flip_v}:{type:o.type,lengthCm:o.lengthCm,contact:"",lock:"",invert:!1,flipH:o.flipH,flipV:o.flipV},...o.host?{host:o.host}:{},x:o.x,y:o.y,angle:o.angle},this._openingRebindId=null,this._openingHoverCandidate=null,this._cursorPt=null}_editOpening(e){this._openingDialog={id:e.id,type:e.type,lengthCm:Math.round(e.rlen/this._gridPitch*this._cellCm),lengthTouched:!1,contact:e.contact||"",lock:e.lock||"",invert:!!e.invert,flipH:!!e.flip_h,flipV:!!e.flip_v,..."partition"===e.host?.kind?{host:{...e.host}}:{},x:e.rx,y:e.ry,angle:e.angle}}_opPointerDown(e,t){if("plan"===this._mode&&this._spaceModel()&&"resize"!==this._tool){e.preventDefault(),e.stopPropagation();try{Gv(e)}catch{}this._opDrag={id:t.id,moved:!1,sx:e.clientX,sy:e.clientY,dirty:!1,before:this._geometrySnapshot()}}}_opPointerMove(e,t){if(!this._opDrag||this._opDrag.id!==t.id)return;if(Math.abs(e.clientX-this._opDrag.sx)+Math.abs(e.clientY-this._opDrag.sy)<=3)return;const i=this._spaceModel();if(!i)return;const s=this._svgPoint(e),n=this._curSpaceCfg,o=n?.openings?.find(e=>e.id===t.id);if(!o)return;if("partition"===o.host?.kind){const e=i.partitions.find(e=>e.id===o.host.id);if(!e)return;const t=e.b[0]-e.a[0],n=e.b[1]-e.a[1],a=Math.hypot(t,n);if(!(a>1e-9))return;const r=t/a,l=n/a;if(Math.abs((s[0]-e.a[0])*l-(s[1]-e.a[1])*r)>4*this._gridPitch)return;const c=o.length*Ov/2+su(e,this._cellCm,this._gridPitch);let h=(s[0]-e.a[0])*r+(s[1]-e.a[1])*l;if(h=Math.round(h/this._gridPitch)*this._gridPitch,h=Math.max(c,Math.min(a-c,h)),a<2*c-1e-9)return;const d=h/a,p=(e.a[0]+r*h)/Ov,u=(e.a[1]+l*h)/this._spaceH;let _=180*Math.atan2(n,t)/Math.PI;_>=90?_-=180:_<-90&&(_+=180);const m=o.host.t!==d||o.x!==p||o.y!==u||o.angle!==_;return this._opDrag.moved=!0,m&&(this._opDrag.dirty=!0),o.host={...o.host,t:d},o.x=p,o.y=u,o.angle=_,this._opMeasure=null,m&&this._cfgEpoch++,void this.requestUpdate()}const a=ui(s,i.rooms,4*this._gridPitch);if(!a)return;this._opDrag.moved=!0;const r=this._opRuler(a,o.length*Ov);this._opMeasure=r.measure;const l=r.x/Ov,c=r.y/this._spaceH,h=o.x!==l||o.y!==c||o.angle!==a.angle;if(h&&(this._opDrag.dirty=!0),o.x=l,o.y=c,o.angle=a.angle,Number(this._serverCfg?.model_version||0)>=9){const e=Tg(o,n?.wall_segments||[]);e?o.host=e:delete o.host}h&&this._cfgEpoch++,this.requestUpdate()}_opRuler(e,t){const i=this._spaceModel()?.rooms||[],s=this._gridPitch/2;let n=e.x,o=e.y,a=_i([n,o],e.angle,t,i,s);if(a&&a.centered&&(n!==a.wallCenter[0]||o!==a.wallCenter[1]))[n,o]=a.wallCenter,a=_i([n,o],e.angle,t,i,s);else if(a){const[r,l]=a.wallA,[c,h]=a.wallB,d=c-r,p=h-l,u=Math.hypot(d,p);if(u>0){const c=this._gridPitch,h=Math.min(t/2,u/2);let _=Math.round(((n-r)*d+(o-l)*p)/u/c)*c;_=Math.max(h,Math.min(u-h,_)),n=r+_/u*d,o=l+_/u*p,a=_i([n,o],e.angle,t,i,s)||a}}if(!a)return{x:n,y:o,angle:e.angle,measure:null};const r="mi"===this.hass?.config?.unit_system?.length,l=(e,t)=>({x:t[0],y:t[1],text:li(e/this._gridPitch*this._cellCm,r)});return{x:n,y:o,angle:e.angle,measure:{labels:[l(a.sideA,a.midA),l(a.sideB,a.midB)],guide:a.centered?{x:a.wallCenter[0],y:a.wallCenter[1],angle:e.angle}:null}}}_opPointerUp(e,t){if(!this._opDrag||this._opDrag.id!==t.id)return;const i=this._opDrag,s=i.moved;this._opMeasure=null,s&&i.dirty&&this._commitPhysicalGeometry(this._t("history.move_opening"),i.before),s?window.setTimeout(()=>this._opDrag=null,0):this._opDrag=null}_opClick(e,t){"plan"===this._mode&&"resize"===this._tool||(e.stopPropagation(),this._opDrag?.moved||"plan"===this._mode&&this._editOpening(t))}_saveOpening(){const e=this._openingDialog,t=this._curSpaceCfg,i=this._spaceModel();if(!e||!t||!i)return;const s=this._geometrySnapshot(),n=this._spaceH,o=(t.openings||[]).find(t=>t.id===e.id),a={...o||{},id:e.id||"o"+Date.now().toString(36),type:e.type,x:e.x/Ov,y:e.y/n,angle:e.angle,length:o&&!e.lengthTouched?o.length:this._cmToUnits(Math.max(20,e.lengthCm))/Ov,...e.host?{host:{...e.host}}:{}};if("partition"===a.host?.kind){const e=ru(o,a)?au(a,i.partitions,Ov,this._cellCm,this._gridPitch):ou(a,i.partitions,Ov,this._cellCm,this._gridPitch);if(!e.resolved){const t=i.partitions.find(e=>e.id===a.host.id);if("does-not-fit-jamb"===e.reason&&t){const e=su(t,this._cellCm,this._gridPitch);this._showToast(this._t("opening.partition_jamb_margin",{distance:li(e/this._gridPitch*this._cellCm,this._imperial)}))}else this._showToast(this._t("opening.partition_orphan"));return}const s=(t.openings||[]).flatMap(e=>{if(!e.host||e.id===a.id)return[];const t=ou(e,i.partitions,Ov,this._cellCm,this._gridPitch).resolved;return t?[t]:[]});if(function(e,t,i=1e-9){const s=e.length/(2*e.axis.length),n=e.t-s,o=e.t+s;return t.some(t=>{if(t.host.id!==e.host.id||t.opening.id===e.opening.id)return!1;const s=t.length/(2*t.axis.length);return Math.max(n,t.t-s)=9){const e=Tg(a,t.wall_segments||[]);if(!e)return void this._showToast(this._t("toast.opening_no_wall"));a.host=e}else delete a.host;"passage"===e.type?(delete a.contact,delete a.lock,delete a.invert,delete a.flip_h,delete a.flip_v):(a.contact=e.contact||null,a.lock=("door"===e.type||"gate"===e.type)&&e.lock||null,a.invert=e.invert||void 0,a.flip_h="gate"!==e.type&&e.flipH||void 0,a.flip_v=e.flipV||void 0),t.openings=t.openings||[];const r=t.openings.findIndex(e=>e.id===a.id);r>=0?t.openings[r]=a:t.openings.push(a),this._commitPhysicalGeometry(this._t(e.id?"history.edit_opening":"history.add_opening"),s),this._openingDialog=null,this.requestUpdate()}_deleteOpening(){const e=this._openingDialog,t=this._curSpaceCfg;if(!e?.id||!t?.openings)return;const i=this._geometrySnapshot();t.openings=t.openings.filter(t=>t.id!==e.id),this._commitPhysicalGeometry(this._t("history.delete_opening"),i),this._openingDialog=null,this.requestUpdate()}_contactCandidates(){const e=[];for(const t of Object.keys(this.hass.states)){if(!this._openingEntityAvailable(t))continue;const i=t.split(".")[0];if("binary_sensor"!==i&&"cover"!==i)continue;const s=this.hass.states[t],n=["door","window","opening","garage_door","garage"].includes(s?.attributes?.device_class||"");("cover"!==i||n)&&e.push([t,s?.attributes?.friendly_name||t,n?0:1])}return e.sort((e,t)=>e[2]-t[2]||e[1].localeCompare(t[1])).map(([e,t])=>({value:e,label:t}))}_lockCandidates(){return Object.keys(this.hass.states).filter(e=>e.startsWith("lock.")&&this._openingEntityAvailable(e)).map(e=>({value:e,label:this.hass.states[e]?.attributes?.friendly_name||e})).sort((e,t)=>e.label.localeCompare(t.label))}_toggleOpeningEntityPicker(e){const t=this._openingDialog;if(!t)return;const i="contact"===e?!t.contactOpen:!t.lockOpen;this._openingDialog={...t,contactOpen:"contact"===e&&i,lockOpen:"lock"===e&&i}}_filterOpeningEntities(e,t){const i=this._openingDialog;i&&(this._openingDialog="contact"===e?{...i,contactFilter:t}:{...i,lockFilter:t})}_selectOpeningEntity(e,t){const i=this._openingDialog;i&&(this._openingDialog="contact"===e?{...i,contact:t,contactOpen:!1,contactFilter:""}:{...i,lock:t,lockOpen:!1,lockFilter:""})}_mergeClick(e){const t=this._spaceModel();if(!t)return;const i=t.rooms,s=[...i].reverse().find(t=>this._pointInRoom(e,t));if(!s?.id)return;const n=s.id;if(!this._mergeSel||this._mergeSel===n)return void(this._mergeSel=this._mergeSel===n?null:n);const o=i.find(e=>e.id===this._mergeSel),a=o?hi(o):null,r=hi(s),l=a&&r?function(e,t){if(!e||!t||e.length<3||t.length<3)return null;const i=si(Ii(e),Ii(t));if(1!==i.length)return null;if(1!==i[0].length)return null;const s=i[0][0].slice(0,-1).map(e=>[e[0],e[1]]);return s.length>=3?s:null}(a,r):null;if(!l)return this._showToast(this._t("toast.merge_not_adjacent")),void(this._mergeSel=null);this._mergeDialog={aId:this._mergeSel,bId:n,poly:l,pick:"a"},this._mergeSel=null}_commitMerge(){const e=this._mergeDialog,t=this._curSpaceCfg;if(!e||!t||!this._spaceModel())return;const i=this._geometrySnapshot(),s=this._spaceH,n="a"===e.pick?e.aId:e.bId,o="a"===e.pick?e.bId:e.aId,a=t.rooms.find(e=>e.id===n);if(!a)return void(this._mergeDialog=null);a.poly=e.poly.map(e=>[e[0]/Ov,e[1]/s]),delete a.x,delete a.y,delete a.w,delete a.h,t.rooms=t.rooms.filter(e=>e.id!==o);const r=this._commitPhysicalGeometry(this._t("history.merge_rooms"),i);this._mergeDialog=null,this._regSignature="",this._maybeRebuildDevices(),r&&this._showToast(this._t("toast.rooms_merged",{name:a.name||""}))}_splitClick(e){const t=this._spaceModel();if(!t)return;const i=t.rooms;if(!this._splitSel){const t=[...i].reverse().find(t=>this._pointInRoom(e,t));if(!t?.id)return;return void(this._splitSel={roomId:t.id,pts:[]})}const s=i.find(e=>e.id===this._splitSel.roomId),n=s?hi(s):null;if(!s||!n)return void(this._splitSel=null);const o=.02*this._gridPitch,a=6*this._gridPitch,r=function(e,t){if(!t||t.length<2)return null;let i=null,s=1/0;for(let n=0;n=n)continue;n=_;const m=Math.sqrt(p),g=(i>0?Math.max(0,Math.min(m,Math.round(u*m/i)*i)):u*m)/m;s=[a+g*h,r+g*d]}return s}(r,n,this._gridPitch)||r,c=r&&l&&Math.hypot(r[0]-e[0],r[1]-e[1])<=a?l:null,h=!!c&&yi(c,n,o),d=this._splitSel.pts;if(!d.length)return h?void(this._splitSel={...this._splitSel,pts:[c]}):void this._showToast(this._t("toast.split_pick_wall"));if(!h){const t=this._snap(e);return wi(t,n,o)?void(this._splitSel={...this._splitSel,pts:[...d,t]}):void this._showToast(this._t("toast.split_pick_inside"))}const p=zi(n,[...d,c],o);if(!p)return void this._showToast(this._t("toast.split_bad_cut"));this._resetRoomDialogFields();const[u,_]=p,m=Ti(u)>=Ti(_)?u:_,g=m===u?_:u;this._pendingSplit={roomId:s.id,mainPoly:m,newPoly:g},this._cursorPt=null,this._nameSel="",this._areaSel="",this._roomDialog=!0}get _contourClosed(){return this._path.length>=4&&this._samePt(this._path[0],this._path[this._path.length-1])&&(null!=this._closingWallCm||!this._activeDraftId)}_markupMove(e){if(!this._markup)return;if("column"===this._tool)return void(this._cursorPt=this._snap(this._svgPoint(e)));if("opening"===this._tool||"wallthick"===this._tool)return void(this._cursorPt=this._svgPoint(e));const t="draw"===this._tool&&!this._contourClosed,i="split"===this._tool&&!!this._splitSel?.pts?.length;if(!t&&!i)return;const s=this._svgPoint(e);if(t){const t=this._resolvePlanDrawPoint(s,e.shiftKey);return this._planSnapHover={contextKey:t.contextKey,candidate:t.candidate,conflicts:t.conflicts},this._syncPlanSnapConflictMarkers(t.conflicts),this._path.length?void(this._cursorPt=t.point):void this._syncPlanSnapActiveMarker(t.candidate)}this._cursorPt=this._snap(s)}get _openingPreview(){const e=this._openingPreset;if("opening"!==this._tool||!e||!this._cursorPt)return null;const t=this._cursorPt,i=this._openingHoverCandidate;if(i&&tv(i,[t[0],t[1]],e.revision,this._cfgEpoch))return i;if(this._openingAt(t))return this._openingHoverCandidate=null,null;const s=this._resolveOpeningPlacement(t);return this._openingHoverCandidate=s,s}get _opMeasureView(){return this._opMeasure||this._openingPreview?.measure||null}_saveRoom(){(this._areaSel||this._nameSel.trim())&&(this._wallFaceBatch?this._decideWallFace(!0):this._commitRoom())}_decideWallFace(e){const t=this._wallFaceBatch;if(!t)return;const i=t.candidates[t.index];if(!i)return;if(!e&&(i.existing||i.repair)&&1===t.candidates.length)return this._wallFaceBatch=null,this._roomDialog=!1,this._nameSel="",this._areaSel="",void this.requestUpdate();const s=e?{candidate:i,create:!0,name:this._nameSel.trim(),area:this._areaSel||null,settings:this._roomSettingsFromDialog()?JSON.parse(JSON.stringify(this._roomSettingsFromDialog())):null}:{candidate:i,create:!1},n=[...t.decisions,s],o=t.index+1;if(o"number"==typeof e&&Number.isFinite(e)&&e>=0?e:null,n=.02*this._gridPitch,o=this._spaceModel();if(!o)return vl;for(const t of this._planSnapGeometrySnapshot().value.segments){if("room"===t.sourceKind||Hs(e,[t.a[0],t.a[1],t.b[0],t.b[1]])>n)continue;if("partition"===t.sourceKind)return s(o.partitions.find(e=>e.id===t.sourceId)?.cm)??vl;const i=t.sourceId.lastIndexOf(":"),a=i>=0?t.sourceId.slice(0,i):t.sourceId,r=i>=0?Number(t.sourceId.slice(i+1)):-1;return s(o.room_drafts.find(e=>e.id===a)?.segments[r]?.cm)??vl}const a=Iu(Math.max(0,t.length-1),i,this._drawWallCm,vl);for(let i=0;i+1[...e]);if(!t?.sourceKey.startsWith("active:"))return i;const s=/:(\d+)$/.exec(t.sourceKey),n=s?Number(s[1])+("b"===t.endpoint?1:0):-1;return n>=0&&nt.key===e.sourceKey),o=s.find(t=>t.key===e.targetSourceKey);if(!n||!o)return!1;const a="a"===e.endpoint?n.a:n.b;return!(Math.hypot(a[0]-e.from[0],a[1]-e.from[1])>i)&&(!(Hs(e.to,[o.a[0],o.a[1],o.b[0],o.b[1]])>i)&&!function(e,t){if(!e.sourceKey.startsWith("static:partition|"))return!1;const i=e.sourceKey.slice(17).split("|")[0];return!!i&&t.some(e=>"partition"===e.host?.kind&&e.host.id===i)}(e,this._curSpaceCfg?.openings||[]))}_applyWallRepair(e,t){const i=this._curSpaceCfg;if(!i||!this._validateWallRepair(e,t.activePath))return!1;const s=t=>{t[0]=e.to[0]/Ov,t[1]=e.to[1]/Ov};if(e.sourceKey.startsWith("active:")){const n=/:(\d+)$/.exec(e.sourceKey),o=n?Number(n[1])+("b"===e.endpoint?1:0):-1;if(o<0||o>=t.activePath.length)return!1;t.activePath[o]=[...e.to];const a=(i.room_drafts||[]).find(e=>e.id===t.activeDraftId);return a?.points?.[o]&&s(a.points[o]),!0}if(!e.sourceKey.startsWith("static:"))return!1;const[n,o]=e.sourceKey.slice(7).split("|");if("partition"===n){const t=(i.partitions||[]).find(e=>e.id===o);if(!t)return!1;const n=[t.a[0]*Ov,t.a[1]*Ov],a=[t.b[0]*Ov,t.b[1]*Ov],r=Math.hypot(n[0]-e.from[0],n[1]-e.from[1])<=2e-4*this._gridPitch?t.a:Math.hypot(a[0]-e.from[0],a[1]-e.from[1])<=2e-4*this._gridPitch?t.b:null;return!!r&&(s(r),!0)}if("draft"===n){const t=o.lastIndexOf(":"),n=t>=0?o.slice(0,t):"",a=t>=0?Number(o.slice(t+1)):-1,r=(i.room_drafts||[]).find(e=>e.id===n);if(!r?.points?.[a]||!r?.points?.[a+1])return!1;const l=[a,a+1].find(t=>Math.hypot(r.points[t][0]*Ov-e.from[0],r.points[t][1]*Ov-e.from[1])<=2e-4*this._gridPitch);return null!=l&&(s(r.points[l]),!0)}return!1}_applyWallFaceBatch(){const e=this._wallFaceBatch,t=this._curSpaceCfg,i=this._spaceModel();if(!e||!t||!i)return;const s=(e,t)=>{this._showToast(this._t(e,t)),this._roomDialogCancel()},n=e.decisions.filter(e=>e.create),o=n.map(e=>e.candidate.ring),a=i.rooms;for(let e=0;ei.id!==t&&!!hi(i)&&Di(o[e],hi(i)));if(i)return void s("toast.room_overlap",{name:i.name||""})}if((t.rooms||[]).length+n.length>400)return void s("toast.physical_limit");if(n.some(e=>e.candidate.split&&!t.rooms.some(t=>t.id===e.candidate.split.roomId)))return void s("toast.contour_cannot_close");const r=n.map(e=>e.candidate.repair).filter(e=>!!e);if(r.length>1||r[0]&&!this._validateWallRepair(r[0],e.activePath))return void s("toast.wall_repair_changed");const l=this._activePathWithRepair(e.activePath,r[0]),c=2e-4*this._gridPitch,h=new Map;for(let t=0;te.candidate.atomKeys)),t=n.some(e=>e.candidate.consumeAllActive);for(const i of Qu(this._wallGraphSources(l),c)){const s=i.sourceKeys.find(e=>h.has(e));!s||t||e.has(i.key)||d.push({a:i.a,b:i.b,cm:h.get(s)})}}else d.push(...Ru(l,Iu(l.length-1,e.activeCms,this._drawWallCm,vl)));if((t.partitions||[]).length+d.length>jv)return void s("toast.physical_limit");const p=n.map(e=>e.candidate.ring.map(()=>"")),u=e.activeDraftId?(t.room_drafts||[]).find(t=>t.id===e.activeDraftId):null,_=this._draftSegmentsForPath(e.activePath,u,e.activeCms),m=(e,t)=>{const i=`${e[0].toFixed(9)},${e[1].toFixed(9)}`,s=`${t[0].toFixed(9)},${t[1].toFixed(9)}`;return i<=s?`${i}|${s}`:`${s}|${i}`};for(let t=0;t<_.length;t++){const i=_[t]?.id;if(!i)continue;const s=e.activePath[t],o=e.activePath[t+1],a=[s[0],s[1],o[0],o[1]],r=[];n.forEach((e,t)=>e.candidate.ring.forEach((i,s)=>{const n=e.candidate.ring[(s+1)%e.candidate.ring.length];Hs(i,a)<=c&&Hs(n,a)<=c&&r.push({kind:"room",owner:t,edge:s,a:i,b:n})})),d.forEach((e,t)=>{Hs(e.a,a)<=c&&Hs(e.b,a)<=c&&r.push({kind:"partition",owner:t,edge:0,a:e.a,b:e.b})});const l=[(s[0]+o[0])/2,(s[1]+o[1])/2];r.sort((e,t)=>{const i=[e.a[0],e.a[1],e.b[0],e.b[1]],n=[t.a[0],t.a[1],t.b[0],t.b[1]];return(Hs(l,i)<=c?0:1)-(Hs(l,n)<=c?0:1)||(Hs(s,i)<=c?0:1)-(Hs(s,n)<=c?0:1)||m(e.a,e.b).localeCompare(m(t.a,t.b))});const h=r[0];if(!h)continue;const u=m(h.a,h.b);for(const e of r)m(e.a,e.b)===u&&("room"===e.kind?p[e.owner][e.edge]=i:d[e.owner].id=i)}const g=this._geometrySnapshot();if(r[0]&&!this._applyWallRepair(r[0],e))return void s("toast.wall_repair_changed");const f=n.some(e=>!!e.candidate.split),v=f?kc(i.rooms,t.walls,this._openCuts(),this._wallKeyPitch,this._cellCm,this._gridPitch,Ov):null,b=f&&Array.isArray(t.walls)?t.walls:null;for(const e of n){const i=e.candidate.split;if(!i)continue;const n=t.rooms.find(e=>e.id===i.roomId);if(!n)return void s("toast.contour_cannot_close");n.poly=i.mainPoly.map(e=>[e[0]/Ov,e[1]/this._spaceH]),delete n.x,delete n.y,delete n.w,delete n.h}const y=Date.now().toString(36),w=[];if(n.forEach((e,i)=>{const s=e.area?this.hass.areas[e.area]?.name:"",n={id:`r${y}-${i}`,name:e.name||s||this._t("room.default_name"),area:e.area||null,poly:e.candidate.ring.map(e=>[e[0]/Ov,e[1]/this._spaceH]),...p[i].some(Boolean)?{wall_ids:p[i]}:{},...e.settings?{settings:JSON.parse(JSON.stringify(e.settings))}:{}};t.rooms.push(n),w.push({room:n,decision:e})}),e.activeDraftId&&Array.isArray(t.room_drafts)&&(t.room_drafts=t.room_drafts.filter(t=>t.id!==e.activeDraftId),t.room_drafts.length||delete t.room_drafts),d.length&&(t.partitions||=[],d.forEach((e,i)=>t.partitions.push({id:e.id||`partition-${y}-${i}`,a:[e.a[0]/Ov,e.a[1]/Ov],b:[e.b[0]/Ov,e.b[1]/Ov],cm:e.cm}))),f){const e=this._normalizeWalls(v,this._openCuts());e.length?t.walls=e:b?.length?t.walls=b:delete t.walls}if(w.length){this._cfgEpoch++;const i=this._openCuts(),s=this._spaceModel();if(!s)return;let n=t.walls;for(const{room:t,decision:o}of w){const a=o.candidate.ring,r=this._wallSourceCmAt(a[0],e.activePath,e.activeCms);n=Vl(n,s.rooms,t.id,r,this._wallKeyPitch,i,Ov);for(const o of wc(s.rooms,n,i,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov)){if(o.roomId!==t.id||"outer"!==o.kind)continue;const i=[(o.a[0]+o.b[0])/2,(o.a[1]+o.b[1])/2];n=Ul(n,o.a,o.b,this._wallSourceCmAt(i,e.activePath,e.activeCms),this._wallKeyPitch,Ov)}}const o=this._normalizeWalls(n,i);o.length?t.walls=o:delete t.walls}if(!this._commitPhysicalGeometry(this._t("history.wall_face_batch"),g))return;delete this._resumeDraftBySpace[this._space],this._path=[],this._activeDraftId=null,this._draftSegmentCms=[],this._closingWallCm=null,this._wallFaceBatch=null,this._roomDialog=!1,this._nameSel="",this._areaSel="",this._regSignature="",this._maybeRebuildDevices();const k=new Set(n.map(e=>e.area).filter(e=>!!e));if(k.size){const e={...this._layout};for(const t of this._devices){if(!k.has(t.area||"")||t.space!==this._space)continue;if(this._layout[t.id])continue;const i=this._defPos[t.id];i&&(e[t.id]={s:this._space,x:i.x/Ov,y:i.y/Ov},this._dirtyPos.add(t.id))}this._layout=e,this._persistLayout()}this._showToast(this._t(n.length?"toast.wall_rooms_saved":"toast.wall_chain_saved",{n:n.length}))}_commitRoom(){const e=this._curSpaceCfg,t=this._spaceModel();if(!e||!t)return;const i=this._geometrySnapshot(),s=this._spaceH,n=!!this._pendingSplit,o=n?kc(t.rooms,e.walls,this._openCuts(),this._wallKeyPitch,this._cellCm,this._gridPitch,Ov):null,a=n&&Array.isArray(e.walls)?e.walls:null;let r;if(this._pendingSplit){const t=e.rooms.find(e=>e.id===this._pendingSplit.roomId);if(!t)return this._pendingSplit=null,this._splitSel=null,void(this._roomDialog=!1);t.poly=this._pendingSplit.mainPoly.map(e=>[e[0]/Ov,e[1]/s]),delete t.x,delete t.y,delete t.w,delete t.h,r=this._pendingSplit.newPoly}else{if(!this._contourClosed)return;r=this._path.slice(0,-1)}const l=this._areaSel?this.hass.areas[this._areaSel]?.name:"",c={id:"r"+Date.now().toString(36),name:this._nameSel||l||this._t("room.default_name"),area:this._areaSel||null,poly:r.map(e=>[e[0]/Ov,e[1]/s]),...this._roomSettingsFromDialog()?{settings:this._roomSettingsFromDialog()}:{}};if(!n&&this._activeDraftId){const t=Array.isArray(e.room_drafts)?e.room_drafts.find(e=>e.id===this._activeDraftId):null,i=Iu(r.length,[...this._draftSegmentCms,this._closingWallCm??void 0],this._drawWallCm,vl),s=this._draftSegmentsForPath(this._path,t,i);s.length===r.length&&s.every(e=>!!e.id)&&(c.wall_ids=s.map(e=>e.id))}if(e.rooms.push(c),!n&&this._activeDraftId&&Array.isArray(e.room_drafts)&&(e.room_drafts=e.room_drafts.filter(e=>e.id!==this._activeDraftId),e.room_drafts.length||delete e.room_drafts),n){const t=this._normalizeWalls(o,this._openCuts());t.length?e.walls=t:a?.length?e.walls=a:delete e.walls}if(!n){const t=Iu(r.length,[...this._draftSegmentCms,this._closingWallCm??void 0],this._drawWallCm,vl),i=t[0];if(null!=i){this._cfgEpoch++;const s=this._openCuts(),n=this._spaceModel();if(!n)return;let o=Vl(e.walls,n.rooms,c.id,i,this._wallKeyPitch,s,Ov);for(const e of wc(n.rooms,o,s,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov)){if(e.roomId!==c.id||"outer"!==e.kind)continue;const i=[(e.a[0]+e.b[0])/2,(e.a[1]+e.b[1])/2],s=r.findIndex((e,t)=>{const s=r[(t+1)%r.length];return Hs(i,[e[0],e[1],s[0],s[1]])<=.02*this._gridPitch});s>=0&&(o=Ul(o,e.a,e.b,t[s],this._wallKeyPitch,Ov))}o=this._normalizeWalls(o,s),o.length?e.walls=o:delete e.walls}}const h=this._commitPhysicalGeometry(this._t(n?"history.split_room":"history.add_room"),i);this._path=[],delete this._resumeDraftBySpace[this._space],this._activeDraftId=null,this._draftSegmentCms=[],this._closingWallCm=null,this._pendingSplit=null,this._splitSel=null;const d=this._areaSel;if(this._areaSel="",this._nameSel="",this._roomDialog=!1,this._regSignature="",this._maybeRebuildDevices(),!h)return;let p=0;if(d){const e=Ov,t={...this._layout};for(const i of this._devices){if(i.area!==d||i.space!==this._space)continue;if(p++,this._layout[i.id])continue;const s=this._defPos[i.id];s&&(t[i.id]={s:this._space,x:s.x/Ov,y:s.y/e},this._dirtyPos.add(i.id))}this._layout=t,this._persistLayout()}const u=this._model.find(e=>e.id===this._space)?.rooms.length||0;this._showToast(d?this._t("toast.room_saved",{n:u,added:p}):this._t("toast.room_saved_no_area",{n:u}))}_cancelPath(){this._wallRepairDiagnostic=null,this._activeDraftId&&(this._resumeDraftBySpace[this._space]=this._activeDraftId),this._path=[],this._activeDraftId=null,this._draftSegmentCms=[],this._closingWallCm=null,this._clearPlanSnapHover(),this._roomDialog=!1,this._pendingSplit=null,this._wallFaceBatch=null,this._splitSel=null,this._mergeSel=null,this._mergeDialog=null,this._physicalSel=null,this._physicalDrag=null,this._physicalRotate=null,this._clearOpeningPlacement(!0)}_resumeLastDraft(){const e=this._resumeDraftBySpace[this._space];if(!e)return;const t=this._spaceModel()?.room_drafts.find(t=>t.id===e);t?(this._activeDraftId=e,this._path=t.points.map(e=>[...e]),this._draftSegmentCms=this._adoptDraftCms(this._path,t.segments.map(e=>e.cm),t.id),this._clearPlanSnapHover()):delete this._resumeDraftBySpace[this._space]}_roomDialogCancel(){if(this._roomDialog=!1,this._roomEditId)return this._roomEditId=null,this._nameSel="",void(this._areaSel="");if(this._wallFaceBatch){const e=this._wallFaceBatch;return this._wallFaceBatch=null,this._path=e.activePath.map(e=>[...e]),this._draftSegmentCms=[...e.activeCms],this._activeDraftId=e.activeDraftId,this._nameSel="",this._areaSel="",void this.requestUpdate()}if(this._pendingSplit)return this._pendingSplit=null,void(this._splitSel=null);this._undoPoint()}get _freeAreas(){const e=new Set;for(const t of this._serverCfg?.spaces||[])for(const i of t.rooms||[])i.area&&e.add(i.area);for(const t of this._wallFaceBatch?.decisions||[])t.create&&t.area&&e.add(t.area);return Object.values(this.hass?.areas||{}).filter(t=>!e.has(t.area_id)).sort((e,t)=>(e.name||"").localeCompare(t.name||""))}_openDeviceInbox(){this._deviceInboxReturn=null,this._deviceInbox=this._deviceInbox||{tab:"on_plan",search:"",showEntities:!1,onlyNew:!1,limit:100}}_closeMarkerDialog(){if(this._markerDialog=null,this._deviceInboxReturn){const e={...this._deviceInboxReturn};this._deviceInbox=e,this._deviceInboxReturn=null,e.anchor&&this.updateComplete.then(()=>requestAnimationFrame(()=>{const t=`.device-inbox-row[data-binding="${CSS.escape(e.anchor)}"]`;this.renderRoot.querySelector(t)?.scrollIntoView({block:"nearest"})}))}}_deviceInboxCandidates(e){return Sp({hass:this._planHass,devices:this._devices,markers:this._markers,showEntities:e,labels:{device:this._t("marker.sub_device"),z2mGroup:this._t("marker.sub_z2m_group"),group:this._t("marker.sub_group"),helper:this._t("marker.sub_helper"),entity:this._t("marker.sub_entity")}})}_deviceInboxRows(){const e=this._deviceInbox||this._deviceInboxReturn,t=!!e?.showEntities,i=[this._haRegistry.revision,this._cfgRev,this._cfgEpoch,this._regSignature,this._newSyncKey,t?1:0,this._showAll?1:0,pa(this.hass,this._config?.language)].join("|");if(this._deviceInboxMemo?.key===i)return this._deviceInboxMemo.rows;const s=this._deviceInboxCandidates(t),n=new Set(s.map(e=>e.value));for(const e of this._markers)e.binding&&"virtual"!==e.binding&&n.add(e.binding);for(const e of this._devices)e.bindingKind&&"virtual"!==e.bindingKind&&e.bindingRef&&n.add(`${e.bindingKind}:${e.bindingRef}`);const o=new Map;for(const e of n)o.set(e,this._bindingStatus(e));const a=this._areaToSpace,r={};for(const[e,t]of Object.entries(this.hass?.areas||{}))r[e]=a[e]?.room?.name||t?.name||e;for(const[e,t]of Object.entries(a))r[e]=t.room.name||r[e]||e;const l=Object.fromEntries(this._model.map(e=>[e.id,e.title])),c=Object.fromEntries(Object.entries(a).map(([e,t])=>[e,t.space])),h={},d=new Map;for(const[e,t]of Object.entries(this._fullRegistryHass.entities||{})){const i=String(t?.platform||"").trim();if(i&&(h[`entity:${e}`]=i),i&&t?.device_id){const e=d.get(t.device_id)||new Set;e.add(i),d.set(t.device_id,e)}}for(const[e,t]of d)h[`device:${e}`]=[...t].sort().join(", ");const p={};for(const[e,t]of Object.entries(this._fullRegistryHass.devices||{})){const i=`device:${e}`,s=d.get(e)||new Set,n=Array.isArray(t?.identifiers?.[0])?String(t.identifiers[0][0]||""):"",o=[n,...s].some(e=>this._excluded.has(e));"service"===t?.entry_type?p[i]="service_entry":o?p[i]="excluded_integration":"Group"===t?.model?p[i]="grouped_light":/scene/i.test(t?.model||"")?p[i]="excluded_domain":(/bridge/i.test(`${t?.model||""}${t?.name||""}`)||"myheat"===n&&t?.via_device_id)&&(p[i]="represented_by_parent")}const u=function(e){const{devices:t,markers:i,candidates:s,statuses:n,newDeviceIds:o,showHiddenOnPlan:a,areaNames:r={},spaceNames:l={},spaceByArea:c={},integrationByBinding:h={},reasonByBinding:d={}}=e,p=new Map;for(const e of t)!e.virtual&&e.bindingKind&&"virtual"!==e.bindingKind&&e.bindingRef&&p.set(`${e.bindingKind}:${e.bindingRef}`,e);const u=new Map,_=new Map;for(const e of i)e?.binding&&"virtual"!==e.binding&&(e.removed?_.set(e.binding,e):u.set(e.binding,e));const m=new Map(s.map(e=>[e.value,e])),g=new Set([...p.keys(),...u.keys(),...m.keys(),...[..._.keys()].filter(e=>m.has(e))]),f=[];for(const e of g){const t=p.get(e),i=u.get(e),s=i?void 0:_.get(e),g=m.get(e);let v;if(s&&g)v="readd";else if(!0===i?.hidden)v="hidden";else if(t||i)v="on_plan";else{if(!g)continue;v="available"}const b=t?.bindingStatus||n.get(e)||Mp,y=i||s,w=Cp(e),k=t?.area||g?.areaId||y?.area||"",x=t?.space||y?.space||c[k]||"",$=t?.name||y?.name||g?.label||e,S=t?.model||y?.model||g?.model||"",M=!!g?.parentDeviceId&&p.has(`device:${g.parentDeviceId}`),C="readd"===v?"removed":"hidden"===v?i&&Dp(i)?d[e]||"automatic_hidden":"manual_hidden":"on_plan"===v?i?"visible_explicit":"visible_auto":d[e]||(M?"represented_by_parent":k&&x?"available":"no_bound_room"),D=!!t&&(!t.hidden||a),P="active"===b.kind,T=D&&(P||"ha_disabled"===b.kind&&a),I=[$,S,h[e],r[k],l[x],e,g?.sub].filter(Boolean).join(" ").toLocaleLowerCase();f.push({key:e,binding:e,category:v,status:b,reason:C,deviceId:t?.id,markerId:y?.id,name:$,icon:t?.icon||y?.icon||("entity"===w?"mdi:code-braces":"mdi:devices"),model:S,integration:h[e]||"",areaId:k,areaName:r[k]||"",spaceId:x,spaceName:l[x]||"",kind:w,isNew:!!t&&o.has(t.id),searchText:I,canFind:T,canEdit:!!t||!!i,canHide:"on_plan"===v&&P,canShow:"hidden"===v&&P,canAdd:("available"===v||"readd"===v)&&P})}const v=e=>"active"===e.status.kind?1:0;return f.sort((e,t)=>e.category.localeCompare(t.category)||Number(t.isNew)-Number(e.isNew)||v(e)-v(t)||e.name.localeCompare(t.name)||e.binding.localeCompare(t.binding))}({devices:this._devices,markers:this._markers,candidates:s,statuses:o,newDeviceIds:this._newIds,showHiddenOnPlan:this._showAll,areaNames:r,spaceNames:l,spaceByArea:c,integrationByBinding:h,reasonByBinding:p});return this._deviceInboxMemo={key:i,rows:u},u}_deviceForInboxRow(e){const t=e.deviceId?this._devices.find(t=>t.id===e.deviceId):null;if(t)return t;const i=e.markerId?this._markers.find(t=>t.id===e.markerId):null;return!i||i.removed?null:{id:i.id,name:e.name,model:e.model,area:e.areaId,space:e.spaceId||this._space,hidden:!0===i.hidden||"ha_disabled"===e.status.kind,userHidden:!0===i.hidden,bindingStatus:e.status,icon:e.icon,entities:"active"===e.status.kind?e.status.enabledEntityIds:[],allEntities:e.status.allEntityIds,primary:"active"===e.status.kind?e.status.enabledEntityIds[0]:void 0,marker:i,bindingKind:e.kind,bindingRef:e.binding.slice(e.binding.indexOf(":")+1),pdfs:i.pdfs||[]}}_openInboxMarker(e,t=!1){const i=this._deviceInbox;if(i){if(this._deviceInboxReturn={...i,anchor:e.key},this._deviceInbox=null,!t){const t=this._deviceForInboxRow(e);return void(t?this._openMarkerDialog(t):this._closeMarkerDialog())}this._openMarkerDialog(),this._markerDialog?this._markerDialog={...this._markerDialog,bindingMode:"ha",binding:e.binding,bindingOpen:!1,showEntities:"entity"===e.kind,name:""}:this._closeMarkerDialog()}}async _setInboxHidden(e,t){const i=this._deviceInbox,s=this._serverCfg;if(!i||!s||i.busy||"active"!==e.status.kind)return;const n=s.markers||[],o=n.find(t=>!t.removed&&t.binding===e.binding);if(!t&&!o)return;const a=o?.id||Fi(e.binding,e.markerId,()=>`m_${Date.now().toString(36)}`),r=o?{...o,hidden:t}:{id:a,binding:e.binding,hidden:!0};s.markers=[...n.filter(t=>t.id!==a&&(t.binding!==e.binding||!0===t.removed)),r],this._deviceInbox={...i,busy:e.key,anchor:e.key};try{await this._saveConfigNow(),this._regSignature="",this._deviceInboxMemo=null,this._maybeRebuildDevices(),this._deviceInbox&&(this._deviceInbox={...this._deviceInbox,busy:void 0}),this._showToast(this._t("device_inbox.saved"))}catch(e){this._serverCfg===s&&(s.markers=n),this._deviceInbox&&(this._deviceInbox={...this._deviceInbox,busy:void 0}),this._showToast(this._t("toast.error",{err:this._errText(e)}))}}_findInboxDevice(e){if(!e.canFind)return;const t=this._deviceForInboxRow(e);if(!t)return;if(this._deviceInbox=null,t.space&&t.space!==this._space){if(!this._commitSpace(t.space))return;this._restoreZoom()}const i=()=>{const e=this._devices.find(e=>e.id===t.id)||t,i=this._pos(e);this._applyView(this._zoom,i.x,i.y),this._selId=e.id,this.requestUpdate(),window.setTimeout(()=>{this._selId===e.id&&(this._selId=null,this.requestUpdate())},1500)};requestAnimationFrame(()=>requestAnimationFrame(i))}_deviceInboxTabKey(e){if("ArrowLeft"!==e.key&&"ArrowRight"!==e.key)return;const t=this._deviceInbox;if(!t)return;const i=["on_plan","available","hidden","readd"],s="ArrowRight"===e.key?1:-1,n=(i.indexOf(t.tab)+s+i.length)%i.length;this._deviceInbox={...t,tab:i[n],limit:100,onlyNew:!1},e.preventDefault()}_openMarkerDialog(e){if(this._vacAllCamerasFor=null,this._vacAllCameraCache=null,e&&this._ackNewDevice(e.id),this._norm)if(e){const t=e.marker,i=Object.prototype.hasOwnProperty.call(t||{},"is_light"),s=Object.prototype.hasOwnProperty.call(t||{},"light_entity"),n=Object.prototype.hasOwnProperty.call(t||{},"glow_color"),o=Object.prototype.hasOwnProperty.call(t||{},"value_badge"),a=Object.prototype.hasOwnProperty.call(t||{},"tap_action"),r=Object.prototype.hasOwnProperty.call(t||{},"toggle_entity"),l=ks(t?.glow_color),c=this._devicePresentation(e,!0).valueBadge,h=qo(this._planHass,e,this._devices),d=Wo(this._planHass,e,h);this._markerDialog={devId:e.id,name:e.name,binding:"virtual"===e.bindingKind?"virtual":e.bindingKind+":"+e.bindingRef,bindingMode:"virtual"===e.bindingKind?"virtual":"ha",bindingOpen:!1,showEntities:"entity"===e.bindingKind&&!!this._fullRegistryHass.entities[e.bindingRef||""]?.device_id,bindingFilter:"",icon:e.marker?.icon||"",autoIcon:e.icon||"",display:Bi(e.marker?.display),rippleColor:Ie(e.marker?.ripple_color,""),rippleSize:Number(e.marker?.ripple_size)>0?Number(e.marker.ripple_size):1.5,size:Number(e.marker?.size)>0?Number(e.marker.size):1,angle:Number(e.marker?.angle)||0,tapAction:Vp(e.marker?.tap_action,e.primary?.split(".")[0]),tapActionTouched:!1,originalHasTapAction:a,originalTapAction:e.marker?.tap_action,tapHintAnnouncement:"",toggleEntity:t?.toggle_entity||"",toggleEntityTouched:!1,originalHasToggleEntity:r,originalToggleEntity:t?.toggle_entity,tapTarget:e.marker?.tap_target||"",tapConfirm:!0===e.marker?.tap_confirm,runFilter:"",controls:Zn(e.marker?.binding,e.marker?.controls,e.entities),controlsFilter:"",lightRole:!0===t?.is_light?"always":!1===t?.is_light?"never":"auto",lightRoleTouched:!1,originalHasIsLight:i,originalIsLight:t?.is_light,lightEntity:t?.light_entity||"",lightEntityTouched:!1,originalHasLightEntity:s,originalLightEntity:t?.light_entity,glowMode:null!=l?.bri?"fixed":l?"color":"auto",glowColor:l?.c||this._fillColors.glow_light.c,glowBrightness:Math.max(1,Math.round(100*(l?.bri??1))),glowColorDrafted:!!l,glowBrightnessDrafted:null!=l?.bri,glowTouched:!1,originalHasGlowColor:n&&(!!l||null===t?.glow_color),originalGlowColor:l||(null===t?.glow_color?null:void 0),valueBadgeEnabled:o?!0===t?.value_badge?.enabled:!!c,valueBadgeSource:t?.value_badge?.source||c?.source||d,valueBadgePosition:t?.value_badge?.position||c?.position||"right",valueBadgeTouched:!1,originalHasValueBadge:o,originalValueBadge:t?.value_badge,useClimateTemp:!0===e.marker?.use_climate_temp,glowRadius:Number(e.marker?.glow_radius_cm)>0?String(this._imperial?Math.round(Number(e.marker.glow_radius_cm)/30.48*10)/10:Math.round(Number(e.marker.glow_radius_cm))/100):"",model:e.model||"",link:e.link||"",description:e.description||"",pdfs:[...e.pdfs||[]],room:e.marker?.room_id?e.space+"#@"+e.marker.room_id:e.space&&e.area?e.space+"#"+e.area:"",hideFromPlan:!0===e.marker?.hidden,busy:!1}}else this._markerDialog={name:"",binding:"virtual",bindingMode:"virtual",bindingOpen:!1,showEntities:!1,bindingFilter:"",icon:"",autoIcon:"",display:"badge",rippleColor:"",rippleSize:1.5,size:1,angle:0,tapAction:"info",tapActionTouched:!1,originalHasTapAction:!1,originalTapAction:void 0,tapHintAnnouncement:"",toggleEntity:"",toggleEntityTouched:!1,originalHasToggleEntity:!1,originalToggleEntity:void 0,tapTarget:"",tapConfirm:!1,runFilter:"",controls:[],controlsFilter:"",lightRole:"auto",lightRoleTouched:!1,originalHasIsLight:!1,originalIsLight:void 0,lightEntity:"",lightEntityTouched:!1,originalHasLightEntity:!1,originalLightEntity:void 0,glowMode:"auto",glowColor:this._fillColors.glow_light.c,glowBrightness:100,glowColorDrafted:!1,glowBrightnessDrafted:!1,glowTouched:!1,originalHasGlowColor:!1,originalGlowColor:void 0,valueBadgeEnabled:!1,valueBadgeSource:null,valueBadgePosition:"right",valueBadgeTouched:!1,originalHasValueBadge:!1,originalValueBadge:void 0,useClimateTemp:!1,glowRadius:"",model:"",link:"",description:"",pdfs:[],room:"",hideFromPlan:!1,busy:!1,uploadId:"up_"+Date.now().toString(36)+Math.random().toString(36).slice(2,6)};else this._showToast(this._t("toast.marker_needs_server"))}_runCandidates(){const e=[];for(const t of Gi)for(const[i,s]of Object.entries(this.hass.states))i.startsWith(t+".")&&this._planEntityAvailable(i)&&e.push({value:i,label:s?.attributes?.friendly_name||i,sub:this._t("run."+t)});return e.sort((e,t)=>e.sub.localeCompare(t.sub)||e.label.localeCompare(t.label))}_bindingCandidates(){const e=Sp({hass:this._planHass,devices:this._devices,markers:this._markers,showEntities:!!this._markerDialog?.showEntities,currentBinding:this._markerDialog?.binding,currentDeviceId:this._markerDialog?.devId,labels:{device:this._t("marker.sub_device"),z2mGroup:this._t("marker.sub_z2m_group"),group:this._t("marker.sub_group"),helper:this._t("marker.sub_helper"),entity:this._t("marker.sub_entity")}}),t=(this._markerDialog?.bindingFilter||"").toLowerCase().trim(),i=t?e.filter(e=>(e.label+" "+e.sub+" "+e.value).toLowerCase().includes(t)):e;return i.sort((e,t)=>e.label.localeCompare(t.label)),i.slice(0,200)}_physicalBodiesR(e=this._spaceModel()){if(!e)return[];const t=`${e.id}|${this._cfgEpoch}|${this._cellCm}|${this._gridPitch}`;if(this._physicalBodiesCache?.key===t)return this._physicalBodiesCache.all;const i=_h(e,this._cellCm,this._gridPitch,2e-4*this._gridPitch,this._partitionOpeningCuts(e));return this._physicalBodiesCache={key:t,...i},i.all}_rawPhysicalBodiesR(e=this._spaceModel()){if(!e)return[];this._physicalBodiesR(e);const t=this._physicalBodiesCache;return t?[...t.drafts,...t.partitions,...t.columns]:[]}_cleanFloor(e,t,i=this._spaceModel()){if(!i)return{floor:t,geom:null,path:"",area:yh([[[...t,t[0]]]])};const s=e.id||`#${i.rooms.indexOf(e)}`,n=`${i.id}|${this._cfgEpoch}|${s}`;if(!this._resize.preview){const e=Sv(this._cleanFloorCache,n);if(e.hit)return e.value}const o=t.map(e=>e[0]),a=t.map(e=>e[1]),r=[Math.min(...o),Math.min(...a),Math.max(...o),Math.max(...a)],l=this._physicalBodiesR(i).filter(e=>{const t=e.map(e=>e[0]),i=e.map(e=>e[1]);return Math.max(...t)>=r[0]&&Math.min(...t)<=r[2]&&Math.max(...i)>=r[1]&&Math.min(...i)<=r[3]}),c=l.length?bh(t,l):null,h={floor:t,geom:c,path:c?ch(c):"",area:yh(c||[[[...t,t[0]]]])};return this._resize.preview||Mv(this._cleanFloorCache,n,h,600),h}_autoIconForBinding(e){if("virtual"===e)return"mdi:map-marker";const[t,i]=e.split(":");if(!i)return"";const s=this._fullRegistryHass,n=this._bindingStatus(e),o="active"===n.kind?n.enabledEntityIds:n.allEntityIds;if("device"===t){const e=s.devices?.[i];if(!e)return"mdi:help-circle";const t=o;return t.some(e=>e.startsWith("lock."))?"mdi:lock":yo(s,e.name_by_user||e.name||"",e.model,t,this._iconRules)}if("entity"===t){const e=s.entities?.[i],t=this.hass.states?.[i],n=e?.name||t?.attributes?.friendly_name||i;return i.startsWith("lock.")?"mdi:lock":yo(s,n,"",[i],this._iconRules)}return""}_allRoomsFlat(){const e=[];for(const t of this._serverCfg?.spaces||[])for(const i of t.rooms||[])i.area?e.push({value:t.id+"#"+i.area,label:(t.title||t.id)+" · "+i.name}):i.id&&e.push({value:t.id+"#@"+i.id,label:(t.title||t.id)+" · "+i.name+" · "+this._t("marker.subarea")});return e}_errText(e){if(!e)return this._t("err.unknown");if("string"==typeof e)return e;if("invalid_passage_fields"===e.code){const t=String(e.message||e.error||"").match(/space=([^;]*);\s*opening=([^;]*);\s*fields=([^;]*)/);if(t){const e=this._serverCfg?.spaces?.find(e=>String(e.id)===t[1]),i={contact:"opening.contact_label",lock:"opening.lock_label",invert:"opening.invert",flip_h:"opening.flip_h",flip_v:"opening.flip_v"},s=t[3].split(",").filter(Boolean).map(e=>i[e]?this._t(i[e]):e).join(", ");return this._t("opening.invalid_passage_fields",{room:e?.title||t[1],fields:s})}}if("invalid_partition_opening_jamb_margin"===e.code){const t=String(e.message||e.error||"").match(/margin_cm=([^;]*)/),i=t?Number(t[1]):NaN;if(Number.isFinite(i))return this._t("opening.partition_jamb_margin",{distance:li(i,this._imperial)})}if(e.message)return e.message;if(e.error)return e.error;if(null!=e.code)return this._t("err.code",{code:e.code});try{return JSON.stringify(e)}catch{return String(e)}}_backupErrorText(e){const t=e?.code??e?.error;if("string"==typeof t){const e=`backup.error.${t}`,i=this._t(e);if(i!==e)return i}return this._errText(e)}async _pickMarkerFiles(e){const t=e.target,i=t.files?[...t.files]:[];if(t.value="",!i.length||!this._markerDialog)return;const s=this._markerDialog.uploadId||this._markerDialog.devId||"new",n=[];for(const e of i)try{const t=new FormData;t.append("marker_id",s),t.append("file",e,e.name);const i=this.hass?.fetchWithAuth?await this.hass.fetchWithAuth("/api/houseplan/upload",{method:"POST",body:t}):await fetch("/api/houseplan/upload",{method:"POST",body:t,headers:this.hass?.auth?.data?.access_token?{authorization:`Bearer ${this.hass.auth.data.access_token}`}:{}}),o=await i.json().catch(()=>({}));if(!i.ok||o.error){const e={too_large:this._t("err.too_large",{mb:o.max_mb||50}),bad_ext:this._t("err.bad_ext"),unauthorized:this._t("err.unauthorized")};throw new Error(e[o.error]||o.error||"HTTP "+i.status)}n.push({name:o.name||e.name,url:o.url})}catch(t){this._showToast(this._t("toast.file_failed",{name:e.name,err:this._errText(t)}))}n.length&&this._markerDialog&&(this._markerDialog={...this._markerDialog,pdfs:[...this._markerDialog.pdfs,...n]},this._showToast(this._t("toast.files_attached",{n:n.length})))}_removeMarkerPdf(e){this._markerDialog&&(this._markerDialog={...this._markerDialog,pdfs:this._markerDialog.pdfs.filter(t=>t.url!==e)})}_markerLightFields(e){const t={};if(e.lightRoleTouched?"always"===e.lightRole?t.is_light=!0:"never"===e.lightRole&&(t.is_light=!1):e.originalHasIsLight&&(t.is_light=e.originalIsLight??null),e.lightEntityTouched?e.lightEntity&&(t.light_entity=e.lightEntity):e.originalHasLightEntity&&(t.light_entity=e.originalLightEntity??null),e.glowTouched){if("auto"!==e.glowMode){const i={c:Ie(e.glowColor,this._fillColors.glow_light.c)};"fixed"===e.glowMode&&(i.bri=Math.max(.01,Math.min(1,Math.round(e.glowBrightness)/100))),t.glow_color=i}}else e.originalHasGlowColor&&(t.glow_color=e.originalGlowColor??null);return t}_markerTapActionFields(e){return e.tapActionTouched?{tap_action:e.tapAction||null}:e.originalHasTapAction?{tap_action:e.originalTapAction??null}:{}}_markerToggleEntityFields(e){return(t={touched:e.toggleEntityTouched,originalHas:e.originalHasToggleEntity,original:e.originalToggleEntity,value:e.toggleEntity}).touched?t.value?{toggle_entity:t.value}:{}:t.originalHas?{toggle_entity:t.original??null}:{};var t}async _saveMarker(){const e=this._markerDialog;if(!e||e.busy)return;const t=this._effectiveMarkerTapAction(e);if("ha"===e.bindingMode&&(!e.binding||"virtual"===e.binding))return;if("virtual"===e.binding&&!e.name.trim())return void this._showToast(this._t("toast.virtual_name_required"));if("run"===t&&!e.tapTarget)return void this._showToast(this._t("toast.run_target_required"));if(e.valueBadgeTouched&&e.valueBadgeEnabled&&!e.valueBadgeSource)return void this._showToast(this._t("toast.value_badge_source_required"));if("ha"===e.bindingMode){const t=this._bindingStatus(e.binding),i=e.devId?this._markers.find(t=>t.id===e.devId):null,s=!i||i.binding!==e.binding;if("active"!==t.kind&&s)return void this._showToast(this._t("ha_disabled"===t.kind?"toast.ha_disabled_add":"toast.ha_binding_unverified"));if("ha_disabled"===t.kind&&!0===i?.hidden&&!e.hideFromPlan)return this._markerDialog={...e,hideFromPlan:!0},void this._showToast(this._t("entity"===t.reason?"toast.ha_disabled_show_entity":"toast.ha_disabled_show_device"))}const i=this._serverCfg;if(!i)return;const s=i.markers||[],n=js(e.room);let o=n?.space||null;const a=n?.area||null,r=n?.roomId||null,l=Fi(e.binding,e.devId,()=>"v_"+Date.now().toString(36)),c=e.devId,h=c?this._devices.find(e=>e.id===c):null,d=n?.space||h?.space||null,p=d?this._spaceModelById(d):this._spaceModel();if(!p)return;const u=p.id;"virtual"!==e.binding||o||(o=u),this._markerDialog={...e,busy:!0};try{const n="virtual"===e.binding?[]:s.filter(t=>t.removed&&t.binding===e.binding).map(e=>e.id),d=n.length>0,_=Zn(e.binding,e.controls,this._bindingEntities(e.binding)),m=s.find(e=>e.id===l||e.id===c)?.vacuum||null,g={id:l,vacuum:m,binding:e.binding,name:e.name.trim()||null,icon:e.icon||null,display:"badge"!==e.display?e.display:null,ripple_color:"icon_ripple"===e.display&&e.rippleColor?e.rippleColor:null,ripple_size:"icon_ripple"===e.display&&1.5!==e.rippleSize?e.rippleSize:null,size:1!==e.size?e.size:null,angle:e.angle?e.angle:null,...this._markerTapActionFields(e),...this._markerToggleEntityFields(e),tap_target:"run"===t&&e.tapTarget||null,tap_confirm:!!e.tapConfirm||null,controls:_.length?_:null,...this._markerLightFields(e),...this._markerValueBadgeFields(e),use_climate_temp:!!e.useClimateTemp||null,glow_radius_cm:(()=>{const t=$v(e.glowRadius);return null==t||t<=0?null:Math.round(this._imperial?30.48*t:100*t)})(),model:e.model.trim()||null,link:e.link.trim()||null,description:e.description.trim()||null,pdfs:e.pdfs,hidden:!!e.hideFromPlan};("virtual"===e.binding||e.room)&&(g.space=o,g.area=a,g.room_id=r);const f=h?.marker?.room_id??null,v=!!e.room&&null!=h&&(h.space!==o||h.area!==a||f!==r);let b=!1;const y=e.uploadId||c;if(y&&y!==l&&g.pdfs?.length)try{const e=await this.hass.callWS({type:"houseplan/files/migrate",from_id:y,to_id:l}),t=e?.mapping||{};g.pdfs=function(e,t,i,s){if(!t||!i||t===i)return e;const n="/files/"+t+"/",o="/files/"+i+"/";return e.map(e=>{if(!e.url.includes(n))return e;const t=e.url.split(n)[1]||"",[i,a]=[t.split("?")[0],t.includes("?")?"?"+t.split("?")[1]:""];if(s){const t=s[decodeURIComponent(i)]??s[i];return t?{...e,url:e.url.split(n+i)[0]+o+encodeURIComponent(t)+a}:e}return{...e,url:e.url.split(n).join(o)}})}(g.pdfs,y,l,t),b=Object.keys(t).length>0}catch(e){this._showToast(this._t("toast.files_migrate_failed",{err:this._errText(e)}))}i.markers=s,c&&c!==l&&(i.markers=function(e,t,i){if(!t||!i||t===i)return[...e];const s=`marker:${t}`,n=`marker:${i}`;return e.map(e=>{const t=Array.isArray(e.controls)?e.controls.map(e=>e===s?n:e):e.controls,i="derived_marker_state"===e.value_badge?.source?.kind&&e.value_badge.source.ref===s?{...e.value_badge,source:{...e.value_badge.source,ref:n}}:e.value_badge;return t===e.controls&&i===e.value_badge?e:{...e,controls:t,value_badge:i}})}(i.markers,c,l)),c&&c!==l&&"derived_marker_state"===g.value_badge?.source?.kind&&g.value_badge.source.ref===`marker:${c}`&&(g.value_badge.source={kind:"derived_marker_state",ref:`marker:${l}`}),i.markers=i.markers.filter(e=>e.id!==l&&e.id!==c&&("virtual"===g.binding||e.binding!==g.binding)),i.markers.push(g);let w=null;const k=c?this._layout[c]:null,x=k?{s:k.s||h?.space||this._space,x:k.x,y:k.y}:c&&h&&this._defPos[c]?this._normPos(h.space,this._defPos[c].x,this._defPos[c].y):null;if(!d&&x&&x.s===u)l===c&&this._layout[l]&&!v||(w={s:x.s,x:x.x,y:x.y},this._layout={...this._layout,[l]:w});else if(d||!this._layout[l]||v){let e=p.vb[0]+p.vb[2]/2,t=p.vb[1]+p.vb[3]/2;const i=r?p.rooms.find(e=>e.id===r):a?p.rooms.find(e=>e.area===a):void 0;i&&([e,t]=this._roomCenter(i)),w=this._normPos(u,e,t),this._layout={...this._layout,[l]:w}}if(await this._saveConfigNow(),w){const e=ml(w);this._layout={...this._layout,[l]:e},this._noteLayoutRev(await this.hass.callWS({type:"houseplan/layout/update",device_id:l,pos:e}))}const $=new Set(n);c&&c!==l&&$.add(c),$.delete(l);for(const e of $)delete this._layout[e],await this.hass.callWS({type:"houseplan/layout/delete",device_id:e}).then(e=>this._noteLayoutRev(e)).catch(()=>{});b&&y&&await this.hass.callWS({type:"houseplan/files/cleanup",marker_id:y}).catch(()=>{}),this._closeMarkerDialog(),this._regSignature="",this._maybeRebuildDevices(),this._showToast(this._t("toast.marker_saved"))}catch(e){this._markerDialog&&(this._markerDialog={...this._markerDialog,busy:!1}),this._showToast(this._t("toast.error",{err:this._errText(e)}))}}async _deleteMarker(){const e=this._markerDialog;if(!e||e.busy||!e.devId)return;const t=e.devId?this._devices.find(t=>t.id===e.devId):null,i=this._markers.find(t=>t.id===e.devId);if(!t&&!i)return;const s=e.name||this._t("device.fallback");if(!confirm(this._t("confirm.remove_marker",{name:s})))return;const n=this._serverCfg;n.markers=n.markers||[];const o=n.markers,a=t?.id||i.id,r=t?"virtual"===t.bindingKind?"virtual":t.bindingKind&&t.bindingRef?`${t.bindingKind}:${t.bindingRef}`:"":i.binding;if(!r)return;const l=function(e,t,i,s){const n=new Set([t]),o=e.filter(e=>{const o=e.id===t||!s&&e.binding===i;return o&&n.add(e.id),!o});return{markers:s?o:[...o,{id:t,binding:i,removed:!0,hidden:!0}],cleanupIds:n}}(n.markers,a,r,"virtual"===r);var c,h;n.markers=(c=l.markers,h=l.cleanupIds,c.map(e=>{if(!Array.isArray(e.controls))return e;const t=e.controls.filter(e=>!("string"==typeof e&&e.startsWith("marker:")&&h.has(e.slice(7))));return t.length===e.controls.length?e:{...e,controls:t.length?t:null}}));const d=l.cleanupIds;this._markerDialog={...e,busy:!0};try{await this._saveConfigNow();for(const e of d){delete this._layout[e],delete this._defPos[e],this._dirtyPos.delete(e),this._sentPos.delete(e);const t=this._activityRt.get(e);t&&clearTimeout(t.timer),this._activityRt.delete(e),this._vacRt.delete(e),delete this._vacSrvTrails[e],await this.hass.callWS({type:"houseplan/layout/delete",device_id:e}).then(e=>this._noteLayoutRev(e)).catch(()=>{}),await this.hass.callWS({type:"houseplan/files/cleanup",marker_id:e}).catch(()=>{}),await this.hass.callWS({type:"houseplan/trail/delete",marker_id:e}).catch(()=>{})}this._deviceInboxReturn&&(this._deviceInboxReturn={...this._deviceInboxReturn,tab:"readd",anchor:r}),this._closeMarkerDialog(),this._infoCard?.id===a&&this._closeInfoCard(),this._selId===a&&(this._selId=null),this._drag?.id===a&&(this._drag=null),this._regSignature="",this._maybeRebuildDevices(),this._showToast(this._t("toast.marker_removed"))}catch(e){this._serverCfg===n&&(n.markers=o),this._markerDialog&&(this._markerDialog={...this._markerDialog,busy:!1}),this._showToast(this._t("toast.error",{err:this._errText(e)}))}}_normPos(e,t,i){return{s:e,x:t/Ov,y:i/Ov}}_openSpaceDialog(e,t){if(this._serverStorage&&this._serverCfg)if("edit"===e){const i=this._serverCfg.spaces.find(e=>e.id===t);if(!i)return;const s=cs(i),n=i.settings?.custom_fill&&"object"==typeof i.settings.custom_fill?rs(i.settings.custom_fill):null,o="none"===s.fill?{...n||as,a:0}:n;this._spaceDialog={mode:e,spaceId:t,title:i.title,planUrl:i.plan_url||null,planFile:null,source:i.plan_url?"file":"draw",showBorders:s.showBorders,showNames:s.showNames,zeroWallStyle:V_(i),displayTouched:!0,hideDecor:s.hideDecor,hideOpenings:s.hideOpenings,roomColor:s.color,roomOpacity:s.opacity,fillMode:"none"===s.fill?"custom":s.fill,customFill:o,glowEnabled:s.glow,bgColor:s.bgColor,bgMode:"static"===i.settings?.bg_mode||"daynight"===i.settings?.bg_mode?i.settings.bg_mode:null,northDeg:zr({},i.settings),sunRays:"boolean"==typeof i.settings?.sun_rays?i.settings.sun_rays:null,tempMin:s.tempMin,tempMax:s.tempMax,showLqi:s.showLqi??this._config?.show_signal??!0,cardFontScale:s.cardFontScale,labelTemp:s.labelTemp,labelHum:s.labelHum,labelLqi:s.labelLqi,labelLight:s.labelLight,cellCm:Number(i.cell_cm)>0?Number(i.cell_cm):5,cellCmInput:Th(Number(i.cell_cm)>0?Number(i.cell_cm):5,this._imperial),cellCmTouched:!1,busy:!1}}else this._spaceDialog={mode:e,title:"",planUrl:null,planFile:null,...tp(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:ns,roomOpacity:os,fillMode:"custom",customFill:{...as,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:20,tempMax:25,showLqi:this._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:Ph(this._imperial),cellCmInput:Th(Ph(this._imperial),this._imperial),cellCmTouched:!1,busy:!1};else this._showToast(this._t("toast.integration_missing"))}async _pickPlanFile(e){const t=e.target,i=t.files?.[0];if(!i||!this._spaceDialog)return;const s={"image/svg+xml":"svg","image/png":"png","image/jpeg":"jpg","image/webp":"webp"}[i.type]||(i.name.toLowerCase().endsWith(".svg")?"svg":"");if(!s)return void this._showToast(this._t("toast.plan_formats"));const n=new Uint8Array(await i.arrayBuffer());let o="";for(let e=0;e{const t=new Image;t.onload=()=>e(t.naturalWidth&&t.naturalHeight?t.naturalWidth/t.naturalHeight:1.414),t.onerror=()=>e(1.414),t.src=r});URL.revokeObjectURL(r),this._spaceDialog={...this._spaceDialog,planFile:{ext:s,b64:a,aspect:l,name:i.name}}}_useServerPlan(e){const t=this._spaceDialog;t&&(this._spaceDialog={...t,planUrl:e,planFile:null,pickSaved:!1,savedAspect:void 0},this._aspectJob=this._readPlanAspect(e))}async _readPlanAspect(e){for(let t=0;t<40;t++){const t=this._display(e);if(t){const i=await new Promise(e=>{const i=new Image;i.onload=()=>e(i.naturalWidth&&i.naturalHeight?i.naturalWidth/i.naturalHeight:0),i.onerror=()=>e(0),i.src=t}),s=this._spaceDialog;return s&&s.planUrl===e&&Number.isFinite(i)&&i>0?(this._spaceDialog={...s,savedAspect:i},i):0}if(await new Promise(e=>setTimeout(e,150)),this._spaceDialog?.planUrl!==e)return 0}return 0}async _deleteServerPlan(e){if(confirm(this._t("confirm.delete_plan",{name:e})))try{await this.hass.callWS({type:"houseplan/plans/delete",name:e});const t=this._spaceDialog;t?.saved&&(this._spaceDialog={...t,saved:t.saved.filter(t=>t.name!==e)})}catch(e){this._showToast(this._t("toast.plan_delete_failed",{err:this._errText(e)}))}}_renderServerPlans(e){if(e.savedBusy)return q`
${this._t("space.loading")}
`;const t=e.saved||[];if(!t.length)return q`
${this._t("space.no_saved")}
`;return q`
- ${t.map(t=>q` -
- -
- ${t.name} - ${(e=>e>=1048576?(e/1048576).toFixed(1)+" MB":Math.round(e/1024)+" KB")(t.size)}${t.used_by.length?" · "+this._t("space.used_by",{list:t.used_by.join(", ")}):""} -
- - -
`)} -
`}async _saveSpaceDialog(){const e=this._spaceDialog;if(!e||e.busy||!e.title.trim())return;if("file"===e.source&&!e.planFile&&!e.planUrl)return void this._showToast(this._t("toast.plan_required"));const t="create"===e.mode&&0===(this._serverCfg?.spaces.length||0);this._spaceDialog={...e,busy:!0};try{const n="create"===e.mode?"s"+Date.now().toString(36):e.spaceId;let o=null;if("file"===e.source&&e.planFile){o={url:(await this.hass.callWS({type:"houseplan/plan/set",space_id:n,ext:e.planFile.ext,data:e.planFile.b64})).url,aspect:e.planFile.aspect}}let a=e.savedAspect||null;!o&&"file"===e.source&&e.planUrl&&!a&&this._aspectJob&&(a=await this._aspectJob||null);const r=this._serverCfg;let l;if("create"===e.mode)i=n,s=e.title.trim(),l={id:i,title:s,plan_url:null,view_box:[0,0,1,1],rooms:[],wall_segments:[]},r.spaces.push(l);else{if(l=r.spaces.find(e=>e.id===n),!l)throw new Error("space "+n+" is gone from the config");l.title=e.title.trim()}o?(l.plan_url=o.url,l.plan_aspect=o.aspect):"file"===e.source&&e.planUrl&&e.planUrl!==l.plan_url&&(l.plan_url=e.planUrl,l.plan_aspect=a),"draw"===e.source&&(l.plan_url=null,l.plan_aspect=null,delete l.plan_x,delete l.plan_y,delete l.plan_scale,delete l.plan_scale_x,delete l.plan_scale_y,delete l.plan_angle),l.settings={...l.settings||{},show_borders:e.showBorders,show_names:e.showNames,hide_decor:e.hideDecor||void 0,hide_openings:e.hideOpenings||void 0,room_color:e.roomColor,room_opacity:e.roomOpacity,bg_color:e.bgColor||void 0,bg_mode:e.bgMode||void 0,north_deg:e.northDeg??void 0,sun_rays:e.sunRays??void 0,fill_mode:e.fillMode,custom_fill:e.customFill||void 0,glow_enabled:e.glowEnabled,temp_min:Number.isFinite(e.tempMin)?Math.min(e.tempMin,e.tempMax):20,temp_max:Number.isFinite(e.tempMax)?Math.max(e.tempMin,e.tempMax):25,show_lqi:e.showLqi,card_font_scale:1!==e.cardFontScale?e.cardFontScale:void 0,label_temp:e.labelTemp,label_hum:e.labelHum,label_lqi:e.labelLqi,label_light:e.labelLight},l.zero_wall_style=e.zeroWallStyle,l.cell_cm=Number.isFinite(e.cellCm)&&e.cellCm>0?Math.max(.1,Math.min(kv,e.cellCm)):5,await this._saveConfigNow(),this._spaceDialog=null,"create"===e.mode&&this._commitSpace(l.id),this._regSignature="",this._maybeRebuildDevices(),this._importQueue.length?this._openNextImport():t||this._importTotal>0?(this._importTotal=0,this._commitSpace(this._serverCfg.spaces[0]?.id||this._space),this._setMode("plan"),this._tool="draw",this._path=[],this._cursorPt=null,this._primeDrawWallField(),this._showToast(this._t(t&&!this._importTotal?"toast.space_added_onboard":"import.done"))):(this._showToast("create"===e.mode?this._t("toast.space_added"):this._t("toast.space_saved")),"create"===e.mode&&("plan"!==this._mode?this._setMode("plan"):(this._tool="draw",this._path=[],this._cursorPt=null,this._primeDrawWallField(),this._saveNav())))}catch(e){"conflict"!==e?.code&&await this._reloadConfigOnly(!0),this._spaceDialog&&(this._spaceDialog={...this._spaceDialog,busy:!1}),this._showToast(this._t("toast.error",{err:this._errText(e)}))}var i,s}async _deleteSpace(){const e=this._spaceDialog;if(!e||"edit"!==e.mode)return;const t=this._serverCfg;if(!t)return;const i=t.spaces.find(t=>t.id===e.spaceId),s=qg(t,this._layout||{},e.spaceId||""),n=1===t.spaces.length&&t.spaces[0]?.id===e.spaceId;if(!s.count||n){if(confirm(this._t("confirm.delete_space",{title:i.title}))){this._spaceDialog={...e,deleteBlockers:0,busy:!0};try{this._saveConfigDebounced.pending()&&this._saveConfigDebounced.flush(),this._persistLayout.pending()&&this._persistLayout.flush(),await this._writeChain;const t=await this.hass.callWS({type:"houseplan/space/delete",space_id:e.spaceId,expected_config_rev:this._cfgRev,expected_layout_rev:this._layoutRev}),[i,s]=await Promise.all([this.hass.callWS({type:"houseplan/config/get"}),this.hass.callWS({type:"houseplan/layout/get"})]);this._adoptStructuralResponses(i,s),this._cfgRev=t?.config_rev??this._cfgRev,this._layoutRev=t?.layout_rev??this._layoutRev,this._spaceDialog=null,this._space===e.spaceId&&this._commitSpace(this._serverCfg.spaces[0]?.id||""),this._regSignature="",this._maybeRebuildDevices(),this._showToast(this._t("toast.space_deleted"))}catch(t){"conflict"!==t?.code&&"space_in_use"!==t?.code||await Promise.all([this._reloadConfigOnly(!0),this._reloadLayoutOnly()]);const i=this._serverCfg;if(this._spaceDialog&&i){const t=qg(i,this._layout||{},e.spaceId||""),s=1===i.spaces.length&&i.spaces[0]?.id===e.spaceId;this._spaceDialog={...this._spaceDialog,busy:!1,deleteBlockers:s?0:t.count}}this._showToast(this._t("toast.delete_failed",{err:this._errText(t)}))}}}else this._spaceDialog={...e,deleteBlockers:s.count}}async _saveConfigNow(){this._cfgEpoch++;try{await this._writeConfig()}catch(e){throw e?.physicalGeometryRolledBack?await this._reloadRejectedPhysicalWrite():"conflict"===e?.code&&await this._reloadConfigOnly(),e}}_startImport(){const e=this._importDialog;if(!e)return;const t=e.floors.filter(e=>e.checked).map(e=>e.name);this._importDialog=null,t.length?(this._importQueue=t,this._importTotal=t.length,this._openNextImport()):this._openSpaceDialog("create")}_openNextImport(){const e=this._importQueue.shift();void 0!==e&&(this._spaceDialog={mode:"create",title:e,planUrl:null,planFile:null,...tp(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:ns,roomOpacity:os,fillMode:"custom",customFill:null,glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:20,tempMax:25,showLqi:this._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:Ph(this._imperial),cellCmInput:Th(Ph(this._imperial),this._imperial),cellCmTouched:!1,busy:!1})}_skipImport(){this._spaceDialog=null,this._importQueue.length?this._openNextImport():this._importTotal>0&&this._model.length&&(this._importTotal=0,this._commitSpace(this._serverCfg.spaces[0]?.id||this._space),this._setMode("plan"),this._showToast(this._t("import.done")))}_renderImportDialog(){const e=this._importDialog,t=e.floors.filter(e=>e.checked).length;return q`this._importDialog=null}> -
-
${this._t("import.hint")}
- ${e.floors.map((t,i)=>q``)} -
-
- - - -
-
`}_sunGlobal(){const e=this._settingsDialog;return e?{...this._settings,north_deg:e.northDeg??void 0,bg_mode:e.bgMode,sun_rays:e.sunRays}:this._settings}_sunSpace(){const e=this._spaceDialog,t=this._curSpaceCfg?.settings||{};return e&&"edit"===e.mode&&e.spaceId===this._space?{...t,north_deg:e.northDeg??void 0,bg_mode:e.bgMode??void 0,sun_rays:e.sunRays??void 0}:t}_effNorth(){return zr(this._sunGlobal(),this._sunSpace())}_effBgMode(){return Fr(this._sunGlobal(),this._sunSpace())}_dayCycleState(e=new Date){return(this._modeTransitionVisual?.viewWeight??("view"===this._mode?1:0))<=0||"daynight"!==this._effBgMode()?null:$r(this._renderPlanHass,e)}_syncDayCycleClock(){const e=this._dayCycleState();this._dayCycleClockKey=e?Sr(e):"";const t="clock"===e?.source&&"hidden"!==this.ownerDocument.visibilityState&&this.isConnected;t&&!this._dayCycleTimer?this._dayCycleTimer=window.setInterval(this._dayCycleTick,3e4):!t&&this._dayCycleTimer&&(clearInterval(this._dayCycleTimer),this._dayCycleTimer=0)}_dayCycleVisibility(e){"hidden"!==e.kind?(this._dayCycleTick(),this._syncDayCycleClock()):this._dayCycleTimer&&(clearInterval(this._dayCycleTimer),this._dayCycleTimer=0)}_effSunRays(){return Nr(this._sunGlobal(),this._sunSpace())}_sunNow(){return null!==this._effNorth()?Er(this._renderPlanHass):null}_renderSunRays(e){const t=W``,i=this._modeTransitionVisual?.viewWeight??0;if(this._editing&&i<=0||!this._effSunRays())return this._sunFadeReset(),t;const s=this._effNorth(),n=null!==s?Er(this._renderPlanHass):null;if(!n||n.elevation<=0)return this._sunFadeReset(),t;if(o=n.elevation,Number(o)>=3)this._sunOutTimer&&(clearTimeout(this._sunOutTimer),this._sunOutTimer=0),this._sunOut=!1,this._sunShown=!0;else{if(!this._sunShown)return t;this._sunOut||(this._sunOut=!0,this._sunOutTimer=window.setTimeout(()=>{this._sunOutTimer=0,this._sunShown=!1,this._sunOut=!1,this.requestUpdate()},2e3))}var o;const a=this._zeroWalls(),r=a.barriers.map(e=>e.join(",")).join(";"),l=`${e.id}|${n.azimuth}|${n.elevation}|${s}|${this._cfgEpoch}|${a.style}|${r}`;if(!this._sunRaysCache||this._sunRaysCache.key!==l){const t=e.rooms.map(e=>({id:e.id||"",poly:hi(e)})).filter(e=>!!e.id&&!!e.poly),i=this._openingsR.filter(e=>"window"===e.type&&"partition"!==e.host?.kind).map(e=>({id:e.id,x:e.rx,y:e.ry,angle:e.angle,length:e.rlen})),o=this._spaceWalls,r=this._openCuts(),c=this._openingWallIndexFor(e,r).value,h={},d={},p=this._wallUnionGeometry()?.roomGeom;if(o.length){for(const i of t){const t=this._innerRoomContour(e,i.id,r,p);t&&(h[i.id]=t)}for(const e of i){const t=Zc(c,{x:e.x,y:e.y,angle:e.angle,length:e.length});t.cm>0&&(d[e.id]=Ml(t.cm,this._cellCm,this._gridPitch))}}let u=Rr(t,i,n.azimuth,n.elevation,s,o.length?h:void 0,o.length?d:void 0);const _=[...this._physicalBodiesR(e),...a.barriers.map(e=>[[e[0],e[1]],[e[2],e[3]]])];_.length&&(u=u.map(e=>{const t=function(e,t,i){return i>0?e.map(e=>function(e){const t=[...e].sort((e,t)=>e[0]-t[0]||e[1]-t[1]);if(t.length<=2)return t;const i=(e,t,i)=>(t[0]-e[0])*(i[1]-e[1])-(t[1]-e[1])*(i[0]-e[0]),s=[];for(const e of t){for(;s.length>=2&&i(s[s.length-2],s[s.length-1],e)<=0;)s.pop();s.push(e)}const n=[];for(let e=t.length-1;e>=0;e--){const s=t[e];for(;n.length>=2&&i(n[n.length-2],n[n.length-1],s)<=0;)n.pop();n.push(s)}return s.slice(0,-1).concat(n.slice(0,-1))}([...e,...e.map(e=>[e[0]+t[0]*i,e[1]+t[1]*i])])).filter(e=>e.length>=3):e}(_,e.dir,e.len),i=e.polys.map(e=>bh(e,t));return{...e,paths:i.map(ch).filter(Boolean),polys:i.flatMap(kh)}}).filter(e=>e.paths?.length||e.polys.length)),this._sunRaysCache={key:l,rays:u,rims:u.map(e=>function(e,t=1e-4){const[i,s]=e.dir,n=-s,o=i,a=[];for(const r of[e.a,e.b]){const l=[];for(const a of e.polys)for(let e=0;et)continue;if(Math.abs((h[0]-r[0])*n+(h[1]-r[1])*o)>t)continue;const d=(c[0]-r[0])*i+(c[1]-r[1])*s,p=(h[0]-r[0])*i+(h[1]-r[1])*s;Math.abs(p-d)<=t||l.push(de[0]-t[0]);const c=[];for(const e of l){const i=c[c.length-1];i&&e[0]<=i[1]+t?i[1]=Math.max(i[1],e[1]):c.push([e[0],e[1]])}for(const[e,t]of c)a.push([[r[0]+i*e,r[1]+s*e],[r[0]+i*t,r[1]+s*t]])}return a}(e))}}const c=this._sunRaysCache.rays,h=this._sunRaysCache.rims;if(!c.length)return t;const d=(p=function(e){const t=Math.min(90,Math.max(-90,Number(e)||0));let i=Mr[Mr.length-1][1];for(let e=1;e - ${c.map((e,t)=>{const i=(e.a[0]+e.b[0])/2,s=(e.a[1]+e.b[1])/2,n=i+e.normal[0]*e.depth,o=s+e.normal[1]*e.depth;return W` - ${u.map(([e,t])=>W``)} - - - ${_.map(([e,t])=>W``)} - `})} - - - ${c.map((e,t)=>e.paths?.length?e.paths.map(e=>W``):e.polys.map(e=>W``))} - ${c.map((e,t)=>(h[t]||[]).map(e=>W``))} - `}_sunFadeReset(){this._sunOutTimer&&(clearTimeout(this._sunOutTimer),this._sunOutTimer=0),this._sunShown=!1,this._sunOut=!1}_compassPoint(e){const t=e.currentTarget.getBoundingClientRect(),i=e.clientX-(t.left+t.width/2),s=e.clientY-(t.top+t.height/2);if(Math.hypot(i,s)<5)return;let n=Math.round(180*Math.atan2(i,-s)/Math.PI);e.shiftKey&&(n=15*Math.round(n/15)),n=(n%360+360)%360,this._settingsDialog={...this._settingsDialog,northDeg:n}}_renderCompass(){const e=this._settingsDialog.northDeg;return q`{e.currentTarget.setPointerCapture(e.pointerId),this._compassDrag=!0,this._compassPoint(e)}} - @pointermove=${e=>{this._compassDrag&&this._compassPoint(e)}} - @pointerup=${()=>this._compassDrag=!1} - @pointercancel=${()=>this._compassDrag=!1}> - - ${[0,45,90,135,180,225,270,315].map(e=>W``)} - - - - ${this._t("gs.north_letter")} - - ${null===e?"—":e+"°"} - `}_stageBg(e){const t=this._settingsDialog,i=this._spaceDialog,s=t?t.bgColor||"":hs(this._settings,{bgColor:null});return(i&&"edit"===i.mode&&i.spaceId===this._space?i.bgColor||"":e.bgColor||"")||s}_stageBgHex(){const e=this._stageEl;if(e){const t=getComputedStyle(e).backgroundColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);if(t)return"#"+t.slice(1,4).map(e=>(+e).toString(16).padStart(2,"0")).join("")}return"#111111"}_preflightDiagnostics(e,t){const i=new Map((t?.spaces||[]).map(e=>[String(e?.id||""),e]));return{kind:"houseplan-optimize-preflight",origin:"runtime",cardVersion:vv,checkedAt:(new Date).toISOString(),preflightFingerprint:e.fingerprint,failures:e.failures.map(e=>({spaceId:e.spaceId,displayName:e.displayName,reason:e.reason,detail:e.detail??null,spaceGeometryFingerprint:i.has(e.spaceId)?Ug(i.get(e.spaceId)):null}))}}_reportPreflightFailure(e,t){e.ok||e.fingerprint===this._reportedPreflightFingerprint||(this._reportedPreflightFingerprint=e.fingerprint,console.warn("[houseplan] optimize preflight failed",this._preflightDiagnostics(e,t)))}_preflightVersionsDiffer(){const e=this._haIntegrationVersion;return"string"==typeof e&&e.length>0&&e!==vv}async _copyPreflightDiagnostics(){const e=this._alignDialog?.preflight;if(!e||e.ok)return;const t=JSON.stringify(this._preflightDiagnostics(e,this._alignDialog?.config??null),null,2);try{await navigator.clipboard.writeText(t),this._preflightClipboardFallback=null,this._showToast(this._t("gs.preflight_copied"))}catch{this._preflightClipboardFallback=t}}_checkOptimizeGeometry(e){return Xg(e,{fallbackSpaceName:e=>this._t("gs.align_preflight_space",{n:String(e)})})}_checkSpacePhysicalGeometry(e,t,i){return function(e,t,i={}){const s=(Array.isArray(e?.spaces)?e.spaces:[]).find(e=>String(e?.id||"")===String(t||"")),n=Ug(s);if(!s)return{spaceId:String(t||""),displayName:"",status:"failed",reason:"prepare-exception",fingerprint:n,ok:!1};const o=Xg({...e,spaces:[s]},{...i,fingerprint:()=>n}).spaces[0]||{spaceId:String(t||""),displayName:"",status:"failed",reason:"prepare-exception"};return{...o,fingerprint:n,ok:"failed"!==o.status}}(e,t,{fallbackSpaceName:e=>this._t("gs.align_preflight_space",{n:String(e)}),captureWallGeometry:i?(e,t)=>i(t):void 0})}_optimizeReferenceContext(e){const t=this._haRegistry,i=this._fullRegistryHass,s={},n=(...e)=>{for(const t of e){const e=String(t||"").trim();if(e)return e}return""};for(const[e,i]of Object.entries(t.devices||{}))s[e]=n(i?.name_by_user,i?.name,i?.model);for(const[e,i]of Object.entries(t.entities||{}))s[`lg_${e}`]=n(this.hass?.states?.[e]?.attributes?.friendly_name,i?.name,i?.original_name);for(const e of this._devices)s[e.id]=n(e.name,s[e.id]);for(const e of this._serverCfg?.markers||[]){const t=String(e.binding||"").indexOf(":"),o=t>0?e.binding.slice(0,t):"",a=t>0?e.binding.slice(t+1):"",r="device"===o?i?.devices?.[a]:"entity"===o?i?.entities?.[a]:null;s[e.id]=n(e.name,"device"===o?r?.name_by_user:null,r?.name,"entity"===o?this.hass?.states?.[a]?.attributes?.friendly_name:null,s[e.id])}return{effectiveAreaByMarker:Object.fromEntries(this._devices.filter(e=>!e.virtual&&!!e.area).map(e=>[e.id,e.area])),ownerRoster:{authoritative:t.authoritative,deviceIds:Object.keys(t.devices||{}),entityIds:[...new Set([...Object.keys(t.entities||{}),...Object.keys(this.hass?.states||{})])],names:s},removeLiveMissingPositions:e}}_previewAlignDialog(e){if(!this._norm||!this._serverCfg)return;const t=this._serverCfg.spaces||[];let i;try{i=jg(this._serverCfg,this._layout||{},this._optimizeReferenceContext(e))}catch(e){return void this._showWallModelMigrationBlocked(e)}const s=i.changed?this._checkOptimizeGeometry(i.config):null;s&&this._reportPreflightFailure(s,i.config);const n=Math.ceil(10*i.report.maxShiftCm)/10,o=t.find(e=>null!=e?.id&&String(e.id)===i.report.maxSpace),a=t.length>1&&o?String(o.title||o.id):"";this._preflightClipboardFallback=null,this._alignDialog={report:i.report,config:i.config,layout:i.layout,cm:n,where:a,preflight:s,changed:i.changed,busy:!1,removeLiveMissingPositions:e}}async _runAlignToGrid(){let e=this._alignDialog;if(!e||e.busy||!this._serverCfg||!e.changed||!e.preflight?.ok)return;const t=Jd(e.config);if(e.preflight.fingerprint!==t){const t=this._checkOptimizeGeometry(e.config);if(this._reportPreflightFailure(t,e.config),e={...e,preflight:t},this._alignDialog=e,!t.ok)return}this._clearGeometryGesture(),this._alignDialog={...e,busy:!0};try{this._saveConfigDebounced.pending()&&this._saveConfigDebounced.flush(),await this._writeChain;const t=await this.hass.callWS({type:"houseplan/plan/optimize",config:e.config,layout:e.layout,expected_config_rev:this._cfgRev,expected_layout_rev:this._layoutRev});this._serverCfg=e.config,this._layout=e.layout,this._geometryHistory.clear(),this._cfgRev=t?.config_rev??this._cfgRev+1,this._layoutRev=t?.layout_rev??this._layoutRev+1,this._canOptimizeUndo=!!t?.can_undo,this._undoKind=t?.can_undo?"optimize":null,this._dirtyPos.clear(),this._sentPos.clear(),this._cfgEpoch++,this._modelCache=null,this._frame=null,this._regSignature="",this._maybeRebuildDevices(),this._cacheSnapshot(),this._alignDialog=null,this._preflightClipboardFallback=null,this.requestUpdate(),this._showToast(this._t("gs.align_done",{n:String(e.report.moved),m:String(e.report.migrated+e.report.canonicalized+e.report.coordsCanonicalized+e.report.latticeCoordinatesCanonicalized+e.report.wallsMerged+e.report.spansMerged+e.report.partitionsMerged+e.report.partitionsReconciled+e.report.openingsRehosted+e.report.wallsStraightened),r:String(e.report.spaceRefsRemapped+e.report.roomRefsRemapped+e.report.positionsRemapped+e.report.markersDetached+e.report.orphanRoomLabelsRemoved+e.report.orphanDevicePositionsRemoved+e.report.orphanGroupPositionsRemoved)}))}catch(e){if(this._alignDialog&&(this._alignDialog={...this._alignDialog,busy:!1}),"wall_model_client_outdated"===e?.code)return void this._showToast(this._t("toast.wall_model_client_outdated"));"conflict"===e?.code&&await Promise.all([this._reloadConfigOnly(!0),this._reloadLayoutOnly()]),this._showToast(this._t("toast.error",{err:this._errText(e)}))}}async _undoPlanOptimization(){if(!this._canOptimizeUndo||this._optimizeUndoBusy)return;const e=this._undoKind;this._clearGeometryGesture(),this._optimizeUndoBusy=!0,this.requestUpdate();try{await this.hass.callWS({type:"houseplan/plan/optimize_undo",expected_config_rev:this._cfgRev,expected_layout_rev:this._layoutRev});const[t,i]=await Promise.all([this.hass.callWS({type:"houseplan/config/get"}),this.hass.callWS({type:"houseplan/layout/get"})]);this._serverCfg=t?.config||this._serverCfg,this._cfgRev=t?.rev??this._cfgRev,this._layout=i?.layout||this._layout,this._geometryHistory.clear(),this._layoutRev=i?.rev??this._layoutRev,this._canOptimizeUndo=!1,this._undoKind=null,this._cfgEpoch++,this._modelCache=null,this._frame=null,this._regSignature="",this._maybeRebuildDevices(),this._cacheSnapshot(),this.requestUpdate(),this._showToast(this._t("import"===e?"backup.import_undone":"gs.optimize_undone"))}catch(e){this._canOptimizeUndo=!1,this._undoKind=null,this._showToast(this._t("toast.error",{err:this._errText(e)}))}finally{this._optimizeUndoBusy=!1,this.requestUpdate()}}async _runBackupExport(){const e=this._backupExportDialog;if(e&&!e.busy){this._backupExportDialog={...e,busy:!0,error:""};try{const t=await this.hass.callWS({type:"houseplan/export/create",kind:e.kind,space_id:"space"===e.kind?this._space:void 0,..."space"===e.kind&&e.planOnly?{plan_only:!0}:{},card_version:vv}),i=new Blob([JSON.stringify(t.document,null,2)+"\n"],{type:"application/json;charset=utf-8"}),s=URL.createObjectURL(i),n=document.createElement("a");n.href=s,n.download=t.filename||`houseplan-${e.kind}.json`,n.style.display="none",document.body.append(n),n.click(),n.remove(),window.setTimeout(()=>URL.revokeObjectURL(s),0),this._backupExportDialog=null,this._showToast(this._t("backup.export_done"))}catch(e){this._backupExportDialog&&(this._backupExportDialog={...this._backupExportDialog,busy:!1,error:this._backupErrorText(e)})}}}async _pickBackupImport(e){const t=e.target,i=t.files?.[0];if(t.value="",i){this._settingsDialog=null,this._backupImportDialog={filename:i.name,size:i.size,token:"",preview:null,expectedConfigRev:this._cfgRev,expectedLayoutRev:this._layoutRev,duplicatePolicy:"skip",confirmMissing:!1,busy:!0,error:""};try{this._saveConfigDebounced.pending()&&this._saveConfigDebounced.flush(),await this._writeChain,this._persistLayout.pending()&&this._persistLayout.flush();const e="/api/houseplan/import/preview?duplicate_policy=skip",t={method:"POST",body:i,headers:{"content-type":"application/json"}},s=this.hass?.fetchWithAuth?await this.hass.fetchWithAuth(e,t):await fetch(e,{...t,headers:{...t.headers,...this.hass?.auth?.data?.access_token?{authorization:`Bearer ${this.hass.auth.data.access_token}`}:{}}}),n=await s.json().catch(()=>({}));if(!s.ok||n.error)throw Object.assign(new Error(n.message||n.error||`HTTP ${s.status}`),{code:n.error});this._backupImportDialog&&(this._backupImportDialog={...this._backupImportDialog,token:n.token,preview:n.preview,expectedConfigRev:n.expected_config_rev,expectedLayoutRev:n.expected_layout_rev,busy:!1})}catch(e){this._backupImportDialog&&(this._backupImportDialog={...this._backupImportDialog,busy:!1,error:this._backupErrorText(e)})}}}async _setBackupDuplicatePolicy(e){const t=this._backupImportDialog;if(t&&!t.busy&&t.token&&e!==t.duplicatePolicy){this._backupImportDialog={...t,duplicatePolicy:e,busy:!0,error:""};try{const i=await this.hass.callWS({type:"houseplan/import/revalidate",token:t.token,duplicate_policy:e});this._backupImportDialog&&(this._backupImportDialog={...this._backupImportDialog,preview:{...t.preview,...i.preview},expectedConfigRev:i.expected_config_rev,expectedLayoutRev:i.expected_layout_rev,confirmMissing:!1,busy:!1})}catch(e){this._backupImportDialog&&(this._backupImportDialog={...this._backupImportDialog,duplicatePolicy:t.duplicatePolicy,preview:t.preview,confirmMissing:t.confirmMissing,busy:!1,error:this._backupErrorText(e)})}}}async _applyBackupImport(){const e=this._backupImportDialog;if(!e||e.busy||!e.token||!e.preview)return;if(e.preview.confirmation_required&&!e.confirmMissing)return;this._backupImportDialog={...e,busy:!0,error:""};const t=this._space;try{const i=await this.hass.callWS({type:"houseplan/import/apply",token:e.token,expected_config_rev:e.expectedConfigRev,expected_layout_rev:e.expectedLayoutRev,duplicate_policy:e.duplicatePolicy,confirm_missing_content:e.confirmMissing}),[s,n]=await Promise.all([this.hass.callWS({type:"houseplan/config/get"}),this.hass.callWS({type:"houseplan/layout/get"})]);this._adoptStructuralResponses(s,n),this._geometryHistory.clear(),this._dirtyPos.clear(),this._sentPos.clear(),this._defPos={},this._cfgEpoch++,this._modelCache=null,this._frame=null,this._visibleDeviceSnapshot=null,this._candidateDeviceSnapshot=null,this._stagedDeviceSnapshotToken=-1,this._capturedSnapshotConfigEpoch=-1,this._regSignature="",this._signer.invalidate(this.hass),this._resign(),this._maybeRebuildDevices();const o=this._serverCfg?.spaces||[],a="space"===i.kind&&i.space_id?i.space_id:o.some(e=>e.id===t)?t:o[0]?.id||this._space;this._hasFixedFloor?this._adoptInitialSpace(this._model,!0):this._commitSpace(a),this._backupImportDialog=null,this._cacheSnapshot(),this.requestUpdate();const r="space"===i.kind?e.preview.counts:i.counts;this._showToast(this._t("space"===i.kind?"backup.space_done":"backup.full_done",{spaces:String(r?.spaces||0),rooms:String(r?.rooms||0),markers:String(r?.markers||0),refs:String(i.repaired_target_refs||0)}))}catch(e){if("conflict"===e?.code&&this._backupImportDialog?.token)try{const e=this._backupImportDialog,t=await this.hass.callWS({type:"houseplan/import/revalidate",token:e.token,duplicate_policy:e.duplicatePolicy});return void(this._backupImportDialog={...e,preview:{...e.preview,...t.preview},expectedConfigRev:t.expected_config_rev,expectedLayoutRev:t.expected_layout_rev,confirmMissing:!1,busy:!1,error:this._t("backup.revalidated")})}catch(t){e=t}this._backupImportDialog&&(this._backupImportDialog={...this._backupImportDialog,busy:!1,error:this._backupErrorText(e)})}}_renderBackupExportDialog(){const e=this._backupExportDialog,t=(this._serverCfg?.spaces||[]).find(e=>e.id===this._space);return q`this._backupExportDialog=null}> -
-
${this._t("backup.export_hint")}
- - - ${"space"===e.kind&&t?q``:G} -
${this._t("backup.privacy_warning")}
- ${e.error?q``:G} -
-
- - - -
-
`}_renderBackupImportDialog(){const e=this._backupImportDialog,t=e.preview,i=t?.counts||{},s=t?.reference_report||{},n=e=>Object.values(e||{}).reduce((e,t)=>e+(Number(t)||0),0),o=[["incoming_remapped",n(s.remapped?.incoming)],["target_repaired",n(s.remapped?.target)],["preserved_unresolved",n(s.preservedUnresolved)],["collisions",n(s.collisions)],["dropped_links",n(s.droppedIncomingLinks)],["bounded_lineages",Number(s.boundedLineages)||0]].filter(([,e])=>e>0);return q`this._backupImportDialog=null}> -
-
${e.filename}${(e.size/1024).toFixed(1)} KB
- ${e.busy&&!t?q`
${this._t("backup.reading")}
`:G} - ${e.error?q``:G} - ${t?q` -
- ${this._t("full"===t.kind?"backup.full":"backup.current_space")} - ${t.plan_only?q`${this._t("backup.plan_only_preview")}`:G} - ${this._t("same"===t.source?"backup.same_source":"backup.foreign_source")} - ${this._t("backup.created",{value:t.created_at||"—"})} - ${this._t("backup.versions",{card:t.card_version||"—",integration:t.integration_version||"—",model:String(t.model_version??"—")})} -
-
- ${this._t("backup.count_spaces",{n:"full"===t.kind?`${t.current_counts?.spaces||0} → ${i.spaces||0}`:String(i.spaces||0)})} - ${this._t("backup.count_rooms",{n:"full"===t.kind?`${t.current_counts?.rooms||0} → ${i.rooms||0}`:String(i.rooms||0)})} - ${this._t("backup.count_walls",{n:"full"===t.kind?`${t.current_counts?.walls||0} → ${i.walls||0}`:String(i.walls||0)})} - ${this._t("backup.count_openings",{n:"full"===t.kind?`${t.current_counts?.openings||0} → ${i.openings||0}`:String(i.openings||0)})} - ${this._t("backup.count_decor",{n:"full"===t.kind?`${t.current_counts?.decor||0} → ${i.decor||0}`:String(i.decor||0)})} - ${this._t("backup.count_markers",{n:"full"===t.kind?`${t.current_counts?.markers||0} → ${i.markers||0}`:String(i.markers||0)})} - ${this._t("backup.count_layout",{n:"full"===t.kind?`${t.current_counts?.layout||0} → ${i.layout||0}`:String(i.layout||0)})} -
- ${t.bindings?q`
${this._t("backup.bindings",{device:String(t.bindings.device||0),entity:String(t.bindings.entity||0),virtual:String(t.bindings.virtual||0),legacy:String(t.legacy_positions||0)})}
${this._t("backup.binding_status",{active:String(t.bindings.active||0),disabled:String(t.bindings.disabled||0),missing:String(t.bindings.missing||0)})}
`:G} - ${t.missing_areas?.length?q`
${this._t("backup.missing_areas",{areas:t.missing_areas.join(", ")})}
`:G} - ${t.dropped_marker_links?q`
${this._t("backup.dropped_marker_links",{n:String(t.dropped_marker_links)})}
`:G} - ${t.repaired_target_refs?q`
${this._t("backup.repaired_target_refs",{n:String(t.repaired_target_refs)})}
`:G} - ${t.preserved_unresolved_refs?q` -
${this._t("backup.preserved_unresolved_refs",{n:String(t.preserved_unresolved_refs)})}
${this._t("backup.preserved_unresolved_hint")}
- `:G} - ${o.length?q`
- ${this._t("backup.import_details")} -
- ${o.map(([e,t])=>q`${this._t(`backup.import_detail.${e}`,{n:String(t)})}`)} - ${(s.examples||[]).slice(0,8).map(e=>q` - ${e.owner} → ${e.reference} - `)} -
-
`:G} - ${"full"===t.kind?q` -
${this._t("backup.replace_warning")}
- ${"foreign"===t.source?q`
${this._t("backup.foreign_bookkeeping")}
`:G}`:q` -
${this._t("backup.final_name")}${t.space_title}
-
${this._t("backup.target_settings")}
- ${t.duplicates?q`
${this._t("backup.duplicates",{n:String(t.duplicates)})} - - -
`:G}`} - ${t.content?.length?q`
- ${this._t("backup.content")} - ${t.content.map(e=>q`${e.url} · ${this._t("available"===e.state?"backup.content_available":"external"===e.state?"backup.content_external":"backup.content_detach_required")}`)} -
`:G} - ${t.confirmation_required?q``:G} - `:G} -
-
- - - ${t?q``:G} -
-
`}_setFillColor(e,t){const i=this._settingsDialog;this._settingsDialog={...i,colors:{...i.colors,[e]:{...i.colors[e],...t}}}}async _saveSettingsDialog(){const e=this._settingsDialog;if(e&&!e.busy){this._settingsDialog={...e,busy:!0};try{const t=this._serverCfg,i=JSON.stringify(e.colors)===JSON.stringify(ds),s={...t.settings};i?delete s.fill_colors:s.fill_colors=e.colors;const n=this._imperial?30.48*e.glowRadius:100*e.glowRadius;Number.isFinite(n)&&n>0&&300!==Math.round(n)?s.glow_radius_cm=Math.round(n):delete s.glow_radius_cm,e.bgColor?s.bg_color=e.bgColor:delete s.bg_color,null!==e.northDeg&&Number.isInteger(e.northDeg)&&e.northDeg>=0&&e.northDeg<=359?s.north_deg=e.northDeg:delete s.north_deg,s.bg_mode=e.bgMode,e.sunRays?s.sun_rays=!0:delete s.sun_rays,delete s.weather_entity,this._serverCfg={...t,settings:s},await this._saveConfigNow(),this._settingsDialog=null,this.requestUpdate(),this._showToast(this._t("gs.saved"))}catch(e){this._settingsDialog&&(this._settingsDialog={...this._settingsDialog,busy:!1}),this._showToast(this._t("toast.error",{err:this._errText(e)}))}}}_boolInput(e,t,i=!1){const s=e=>t(!!e.target.checked);return customElements.get("ha-switch")?q``:q``}_rangeInput(e,t,i,s,n,o=!1,a){const r=e=>{const t=Number(e.target.value);Number.isFinite(t)&&n(t)};return customElements.get("ha-slider")?q``:q``}_renderColorRow(e,t){const i=this._settingsDialog.colors[e];return q`
- {this._setFillColor(e,{c:t.detail.color,a:t.detail.opacity})}}> -
`}get _glowRadiusCm(){const e=Number(this._settings.glow_radius_cm);return Number.isFinite(e)&&e>0?e:300}get _imperial(){return"mi"===this.hass?.config?.unit_system?.length}get _glowRadiusPlaceholder(){const e=this._glowRadiusCm;return this._imperial?String(Math.round(e/30.48*10)/10):String(e/100)}_glowTransition(e,t){let i=this._glowRenderedSources.get(e);if(t){const t=this._glowFadeTimers.get(e);if(null!=t&&(clearTimeout(t),this._glowFadeTimers.delete(e)),null==i){this._suspendGlowFeatherForTransition(),i=++this._glowSourceSeq,this._glowRenderedSources.set(e,i),this._glowEnteringSources.add(e);const t=requestAnimationFrame(()=>{this._glowEnterRafs.get(e)===t&&(this._glowEnterRafs.delete(e),this._glowEnteringSources.delete(e),this.isConnected&&this.requestUpdate())});this._glowEnterRafs.set(e,t)}return{domId:i,entering:this._glowEnteringSources.has(e),leaving:!1}}if(null==i)return null;const s=this._glowEnterRafs.get(e);if(null!=s&&cancelAnimationFrame(s),this._glowEnterRafs.delete(e),this._glowEnteringSources.delete(e),!this._glowFadeTimers.has(e)){this._suspendGlowFeatherForTransition();const t=window.setTimeout(()=>{this._glowFadeTimers.get(e)===t&&(this._glowFadeTimers.delete(e),this._glowRenderedSources.delete(e),this._glowLastAppearance.delete(e),this.isConnected&&this.requestUpdate())},534);this._glowFadeTimers.set(e,t)}return{domId:i,entering:!1,leaving:!0}}_suspendGlowFeatherForTransition(){if(window.matchMedia?.("(prefers-reduced-motion: reduce)").matches)return;this._glowFeatherSuspendUntil=Math.max(this._glowFeatherSuspendUntil,Date.now()+500),clearTimeout(this._glowFeatherResumeTimer);const e=Math.max(0,this._glowFeatherSuspendUntil-Date.now())+17,t=()=>{this._glowFeatherResumeTimer=0,Date.now()=128){const e=this._glowGeometryWarnings.values().next().value;e&&this._glowGeometryWarnings.delete(e)}this._glowGeometryWarnings.add(n),console.warn(`HOUSEPLAN GLOW GEOMETRY FALLBACK: #218, space ${e}, room ${i}, phase ${s}`)}}_lightBarriers(e,t){const i=Jd([this._curSpaceCfg,this._cellCm,this._gridPitch]),s=`${e.id}|${i}`,n=Sv(this._lightBarrierPool,s);if(n.hit)return this._lightBarrierCache=n.value,n.value.value;const o=this._zeroWalls(),a=this._openCuts(),r=[...o.transmissive],l=Math.max(this._cmToUnits(10),.5*this._gridPitch),c=e=>t.some(t=>this._pointInRoom(e,t.r)),h=this._openingsR.filter(e=>{if("door"!==(t=String(e.type))&&"gate"!==t&&"passage"!==t)return!1;var t;const i=e.angle*Math.PI/180,s=-Math.sin(i),n=Math.cos(i);return c([e.rx+s*l,e.ry+n*l])&&c([e.rx-s*l,e.ry-n*l])}),d=this._roomWallOpeningInputs(h,e);for(const e of d){const t=e.angle*Math.PI/180,i=Math.cos(t)*e.length/2,s=Math.sin(t)*e.length/2;r.push([e.x-i,e.y-s,e.x+i,e.y+s])}const p=new Set(h.filter(e=>e.partitionHost).map(e=>e.id)),u=[e.id,this._cfgEpoch,this._cellCm,this._gridPitch,[...p].sort().join(",")].join("|");this._lightPhysicalBodiesCache?.key!==u&&(this._lightPhysicalBodiesCache={key:u,all:_h(e,this._cellCm,this._gridPitch,2e-4*this._gridPitch,this._partitionOpeningCuts(e,e=>p.has(e.id))).all});const _=this._lightPhysicalBodiesCache.all,m=this._spaceWalls,g=.02*this._gridPitch,f=[],v=this._wallUnionGeometry(),b=v?.sourceFingerprint,y=v&&b===i?function(e,t=[],i=[],s={}){if("failed-core"===e.status||"not-applicable"===e.status)return null;const n=e.roomComponents?.length?e.roomComponents:Pc(e.roomGeom)?[{id:"room-primary",geom:e.roomGeom}]:[];let o=n[0]?.geom||null;const a=n.slice(1).map(e=>({...e}));try{for(const i of t){if(!(i.length>0&&e.openingIndex))continue;const t=Xc(e.openingIndex,i,!0);if(!t.negative&&!t.positive)continue;const s=i.angle*Math.PI/180,n=Math.cos(s),r=Math.sin(s),l=-r,c=n,h=i.length/2,d=e.openingPadUnits??1.25*Math.max(e.depthUnits,1),p=Dc([[i.x-n*h-l*d,i.y-r*h-c*d],[i.x+n*h-l*d,i.y+r*h-c*d],[i.x+n*h+l*d,i.y+r*h+c*d],[i.x-n*h+l*d,i.y-r*h+c*d]]);o&&(o=oi(o,p));for(const e of a)e.geom=oi(e.geom,p)}const n=[...a];let r=a.length;const l=s.mergeExtra||((e,t)=>e?si(e,t):t);for(let e=0;ee.length>=2&&Number.isFinite(e[0])&&Number.isFinite(e[1]))||Math.abs(Ti(t))<=1e-9){r++;continue}const s=[Dc(t)];try{const t=l(o,s,e);if(!Pc(t))throw new Error("invalid extra union");o=t}catch{r++,Pc(s)&&n.push({id:`policy-extra-${e}`,geom:s})}}n.sort((e,t)=>Lc(e.geom).localeCompare(Lc(t.geom)));const c=o||[];return{status:r?"degraded-extra":"ok",geom:c,components:[...Pc(c)?[{id:"primary",geom:c}]:[],...n.map((e,t)=>({...e,id:`isolated-${t}`}))]}}catch{return null}}(v,d,_):null,w=y||(m.length||_.length?qc(e.rooms,m,a,d,this._wallKeyPitch,this._cellCm,this._gridPitch,Ov,_):null);if(!w||"ok"!==w.status&&"degraded-extra"!==w.status)for(const e of _)f.push(...m_(e));else for(const e of w.components)for(const t of wh(e.geom))f.push(...m_(t));for(const{poly:e}of t)for(const t of r.length?Ps(e,r,g):m_(e))f.push(t);for(const e of o.barriers)f.push(e);const k={occluders:__(f),floor:t.map(e=>e.poly),fingerprint:i,masonryGeometry:!w||"ok"!==w.status&&"degraded-extra"!==w.status?[]:w.components.flatMap(e=>e.geom),opaqueBodies:_},x={key:s,value:k};return Mv(this._lightBarrierPool,s,x,8),this._lightBarrierCache=x,k}_renderGlowLayer(e,t){const i=this._fillColors,s=this._glowRadiusCm/this._cellCm*this._gridPitch,n=e.rooms.map(e=>({r:e,poly:hi(e)})).filter(e=>!!e.poly),o=n.filter(({r:e})=>Fs(t.glow,e));if(!o.length)return this._forgetGlowSpace(e.id),W``;const{occluders:a,floor:r,fingerprint:l,masonryGeometry:c,opaqueBodies:h}=this._lightBarriers(e,n),d=ho(this._renderPlanHass,this._renderDevices,null,this._virtualLights).filter(t=>t.device.space===e.id),p=new Map;for(const e of d){if(!e.device.id)continue;const t=p.get(e.device.id)||[];t.push(e),p.set(e.device.id,t)}const u=[],_=new Set;for(const t of this._renderDevices){if(t.space!==e.id)continue;const o=po(p.get(t.id)||[]);if(!o)continue;const d=`${e.id}|${t.id}`;_.add(d);const m=$s(o.passive?{state:o.on?"on":"off",attributes:{}}:this._renderPlanHass.states[o.eid],t.marker?.glow_color,i.glow_light.c),g=Number(t.marker?.glow_radius_cm),f=Number.isFinite(g)&&g>0?g/this._cellCm*this._gridPitch:s,v=this._pos(t);if($h([v.x,v.y],c,h)){this._forgetGlowSource(d);continue}const b=this._glowTransition(d,!!m);if(!b)continue;m&&this._glowLastAppearance.set(d,{c:m.c,alpha:Ss(m.bri,i.glow_light.a)});const y=this._glowLastAppearance.get(d);if(!y)continue;let w=null;const k=`${e.id}|${l}|${v.x.toFixed(4)},${v.y.toFixed(4)}|${f.toFixed(4)}`,x=Sv(this._glowClipCache,k);if(x.hit)w=x.value;else{const t=v_([v.x,v.y],f,a,96);w={lit:t.length>=3?vh([t],r,{onBoundsFailure:({boundIndex:t,phase:i})=>{const s=n[t]?.r;this._warnGlowGeometryFallback(e.id,l,s?.id||`#${t}`,i)}}):[]},Mv(this._glowClipCache,k,w,256)}u.push({key:d,sourceEid:o.eid,domId:b.domId,entering:b.entering,leaving:b.leaving,pos:v,c:y.c,alpha:y.alpha,geometry:w,r:f})}const m=`${e.id}|`;for(const e of this._glowRenderedSources.keys())e.startsWith(m)&&!_.has(e)&&this._forgetGlowSource(e);if(!u.length)return W``;const g=this._spaceWalls,f=o.length===n.length?[]:this._openCuts(),v=this._wallUnionGeometry()?.roomGeom,b=o.length===n.length?null:o.map(({r:t,poly:i})=>{const s=g.length&&t.id&&this._innerRoomContour(e,t.id,f,v)||i,o=this._cleanFloor(t,s,e).path,a=Pi(s,n.filter(e=>e.r!==t).map(e=>e.poly)),r=e=>"M "+e.map(e=>e[0]+" "+e[1]).join(" L ")+" Z";return[o||r(s),...a.map(r)].join(" ")}),y=this._viewOr(this._baseVb()),w=this._stageEl?.clientWidth&&y.w?this._stageEl.clientWidth/y.w:1,k=1/(w>0?w:1),x=!this._pinchStart&&!this._panStart&&Date.now()>=this._glowFeatherSuspendUntil;(null==this._glowFeatherUnits||x)&&(this._glowFeatherUnits=k);const $=this._glowFeatherUnits??k,S=4*$,M=u.reduce((e,t)=>({x:Math.min(e.x,t.pos.x-t.r-S),y:Math.min(e.y,t.pos.y-t.r-S),maxX:Math.max(e.maxX,t.pos.x+t.r+S),maxY:Math.max(e.maxY,t.pos.y+t.r+S),w:0,h:0}),{x:1/0,y:1/0,maxX:-1/0,maxY:-1/0,w:0,h:0});return M.w=M.maxX-M.x,M.h=M.maxY-M.y,W` - ${Me(u,e=>e.key,e=>{const t=e.domId,i=e.geometry;return W` - ${""} - - ${Hv.map(([t,i])=>W` - `)} - - ${""} - ${i?W` - - - `:G}`})} - ${b?W`${b.map(e=>W``)}`:G} - ${""} - - - - - ${""} - - - ${Me(u,e=>e.key,e=>{const t=e.domId;return W` - - - `})} - - `}_renderAlignDialog(){const e=this._alignDialog,t=e.report,i=e.changed&&!e.preflight?.ok,s=e.preflight?.failures||[],n=s.slice(0,3).map(e=>e.displayName),o=n.length?n.join(", "):this._t("gs.align_preflight_space",{n:"1"}),a=Math.max(0,s.length-n.length),r=a?this._t("gs.align_preflight_more",{n:String(a)}):"",l=t.spaceRefsRemapped+t.roomRefsRemapped+t.positionsRemapped+t.markersDetached,c=t.migrated+t.canonicalized+t.coordsCanonicalized+t.wallSegmentsMigrated+t.wallsMerged+t.spansMerged+t.partitionsMerged+t.partitionsReconciled+t.openingsRehosted+t.redundantDraftsRemoved,h=t.moved+t.rotated+t.removedDrafts+t.coordsCanonicalized+t.wallsStraightened,d=Math.ceil(10*t.maxStraightenShiftCm)/10,p=(this._serverCfg?.spaces||[]).find(e=>String(e?.id||"")===t.maxStraightenSpace),u=(this._serverCfg?.spaces||[]).length>1&&p?String(p.title||p.id):"",_=t.orphanRoomLabelsRemoved+t.orphanDevicePositionsRemoved+t.orphanGroupPositionsRemoved,m=t.liveMissingPositions.map(e=>e.name).filter(Boolean),g=m.slice(0,3).join(", "),f=Math.max(0,m.length-3),v=g?this._t("gs.optimize_live_names",{names:g,more:f?this._t("gs.optimize_reference_more",{n:String(f)}):""}):"",b=t.unverifiedPositions.some(e=>"registry_unavailable"===e.reason),y=e=>t.removedPositions.some(t=>t.id===e.id)?this._t("gs.optimize_detail_removed"):t.liveMissingPositions.some(t=>t.id===e.id)?this._t("gs.optimize_detail_live"):this._t("gs.optimize_detail_unverified"),w=e=>this._t("room_label"===e?"gs.optimize_detail_room_label":"group"===e?"gs.optimize_detail_group":"device"===e?"gs.optimize_detail_device":"gs.optimize_detail_unknown"),k=[...t.removedPositions,...t.liveMissingPositions.filter(e=>!t.removedPositions.some(t=>t.id===e.id)),...t.unverifiedPositions],x=k.slice(0,10),$=Math.max(0,k.length-x.length);return q`{this._alignDialog=null,this._preflightClipboardFallback=null}}> -
- ${i?q` -

${this._t("gs.align_preflight_failed",{spaces:o,more:r})}

- ${s.slice(0,10).map(e=>q`

- ${e.displayName}: ${this._t(`gs.preflight_reason_${e.reason}`)} -

`)} - ${s.length>10?q`

- ${this._t("gs.align_preflight_more",{n:String(s.length-10)})} -

`:G} -
${this._t("gs.align_preflight_hint")}
- ${this._preflightVersionsDiffer()?q` -
${this._t("gs.preflight_update_hint")}
`:G} -
- -
- ${this._preflightClipboardFallback?q`
- ${this._t("gs.preflight_copy")} -
${this._preflightClipboardFallback}
-
`:G}`:e.changed?q` - ${t.moved?q`

${this._t("gs.align_count",{n:String(t.moved),total:String(t.total),cm:String(e.cm)})}

`:G} - ${t.latticeCoordinatesCanonicalized?q` -

${this._t("gs.optimize_lattice_summary",{n:String(t.latticeCoordinatesCanonicalized),cm:ol(t.latticeMaxShiftCm)})}

- ${t.latticeSpaces.map(e=>q`

${this._t("gs.optimize_lattice_space",{space:e.space,n:String(e.canonicalized),far:String(e.far)})}

`)} - `:G} - ${e.where?q`

${this._t("gs.align_where",{s:e.where})}

`:G} - ${t.rotated?q`

${this._t("gs.align_turned",{n:String(t.rotated)})}

`:G} - ${t.removedDrafts?q`

${this._t("gs.align_removed_drafts",{n:String(t.removedDrafts)})}

`:G} - ${t.redundantDraftsRemoved?q`

${this._t("gs.optimize_redundant_drafts",{n:String(t.redundantDraftsRemoved)})}

`:G} - ${t.wallSegmentsMigrated?q`

${this._t("gs.wall_segments_migrated",{n:String(t.wallSegmentsMigrated)})}

`:G} - ${t.legacyZeroWallsMigrated?q`

${this._t("gs.zero_walls_migrated",{n:String(t.legacyZeroWallsMigrated)})}

`:G} - ${c?q`

${this._t("gs.optimize_changes",{m:String(t.migrated),c:String(t.canonicalized),p:String(t.coordsCanonicalized),w:String(t.wallsMerged),s:String(t.spansMerged),i:String(t.partitionsMerged)})}

`:G} - ${t.partitionsReconciled?q`

${this._t("gs.optimize_coincident_partitions",{n:String(t.partitionsReconciled)})}

`:G} - ${t.openingsRehosted?q`

${this._t("gs.optimize_openings_rehosted",{n:String(t.openingsRehosted)})}

`:G} - ${t.wallsStraightened?q`

${this._t("gs.optimize_walls_straightened",{n:String(t.wallsStraightened),cm:String(d)})}

`:G} - ${u?q`

${this._t("gs.optimize_walls_straightened_where",{s:u})}

`:G} - ${t.glowSpacesMigrated||t.glowRoomsMigrated?q`

${this._t("gs.optimize_glow_migration",{spaces:String(t.glowSpacesMigrated),rooms:String(t.glowRoomsMigrated)})}

`:G} - ${h?q`
${this._t("gs.align_warn")}
`:G}`:q`

${this._t(t.liveMissingPositions.length||t.unverifiedPositions.length||t.nestedRefsUnresolved?"gs.optimize_no_automatic_changes":"gs.align_none")}

`} - ${!i&&t.wallsStraightenSkipped?q`

${this._t("gs.optimize_walls_straighten_skipped",{n:String(t.wallsStraightenSkipped)})}

`:G} - ${l?q`

${this._t("gs.optimize_references",{spaces:String(t.spaceRefsRemapped),rooms:String(t.roomRefsRemapped),positions:String(t.positionsRemapped),detached:String(t.markersDetached)})}

`:G} - ${_?q`

${this._t("gs.optimize_orphans_removed",{total:String(_),rooms:String(t.orphanRoomLabelsRemoved),devices:String(t.orphanDevicePositionsRemoved),groups:String(t.orphanGroupPositionsRemoved)})}

`:G} - ${t.liveMissingPositions.length?q`
-

${this._t(e.removeLiveMissingPositions?"gs.optimize_live_positions_remove":"gs.optimize_live_positions",{n:String(t.liveMissingPositions.length),names:v})}

- - ${e.removeLiveMissingPositions?q`
- ${this._t("gs.optimize_live_selected")} -
`:G} -
`:G} - ${t.unverifiedPositions.length?q``:G} - ${t.nestedRefsUnresolved?q``:G} - ${k.length?q`
- ${this._t("gs.optimize_details")} -
    - ${x.map(e=>q`
  • ${this._t("gs.optimize_detail_item",{status:y(e),kind:w(e.kind),id:e.id,space:e.spaceId})}
  • `)} -
- ${$?q`
${this._t("gs.optimize_details_more",{n:String($)})}
`:G} -
`:G} -
-
- - - ${e.changed&&e.preflight?.ok?q` - `:G} -
-
`}_renderSettingsDialog(){return q`this._settingsDialog=null}> -
-
${this._t("gs.hint")}
- - ${this._renderColorRow("light_on","gs.light_on")} - ${this._renderColorRow("light_off","gs.light_off")} - ${this._renderColorRow("light_none","gs.light_none")} - - ${this._renderColorRow("temp_cold","gs.temp_cold")} - ${this._renderColorRow("temp_ok","gs.temp_ok")} - ${this._renderColorRow("temp_hot","gs.temp_hot")} - - ${this._renderColorRow("lqi_low","gs.lqi_low")} - ${this._renderColorRow("lqi_high","gs.lqi_high")} - - ${this._renderColorRow("glow_base","gs.glow_base")} - ${this._renderColorRow("glow_light","gs.glow_light")} -
- ${this._t("gs.glow_radius")} - {const t=$v(e.target.value);null!=t&&t>0&&(this._settingsDialog={...this._settingsDialog,glowRadius:t})}} /> - ${this._imperial?this._t("gs.unit_ft"):this._t("gs.unit_m")} -
- - ${this._renderColorRow("wall_fill","gs.wall_fill")} - -
- ${this._t("gs.bg_mode")} - -
- ${"static"===this._settingsDialog.bgMode?q`
- {this._settingsDialog={...this._settingsDialog,bgColor:e.detail.color}}}> - ${this._settingsDialog.bgColor?q``:q`${this._t("gs.bg_theme")}`} -
`:q`
${this._t("gs.bg_daynight_hint")}
`} - - ${Er(this.hass)?G:q`
${this._t("gs.sun_missing")}
`} -
- ${this._renderCompass()} -
- ${this._t("gs.north")} -
- {const t=e.target.value.trim(),i=""===t?null:Math.round(Number(t));this._settingsDialog={...this._settingsDialog,northDeg:null!==i&&Number.isFinite(i)?Math.min(359,Math.max(0,i)):null}}} /> - ${null!==this._settingsDialog.northDeg?q``:G} -
- ${null===this._settingsDialog.northDeg?q`
${this._t("gs.north_hint")}
`:G} -
-
- - ${this._canEdit?q` - -
${this._t("gs.backup_hint")}
-
- - - - - - ${this._canOptimizeUndo&&"import"===this._undoKind?q` - `:G} -
`:G} - -
${this._t("gs.grid_hint")}
-
- -
- ${this._canOptimizeUndo&&"import"!==this._undoKind?q`
- -
`:G} - -
${this._t("gs.about_version",{v:vv})}
- - ${this._t("gs.about_github")} - - ${this._t("gs.about_telegram")} -
-
- - - - -
-
`}_rulesSet(e){this._rulesDialog={...this._rulesDialog,rules:e}}async _saveRules(){const e=this._rulesDialog;if(!e||e.busy)return;const t=e.rules.filter(e=>e.pattern.trim()&&e.icon.trim());this._rulesDialog={...e,busy:!0};try{const e=this._serverCfg,i=JSON.stringify(t)===JSON.stringify(Je),s={...e.settings};i?delete s.icon_rules:s.icon_rules=t,this._serverCfg={...e,settings:s},await this._saveConfigNow(),this._rulesDialog=null,this._regSignature="",this._maybeRebuildDevices(),this._showToast(this._t("rules.saved"))}catch(e){this._rulesDialog&&(this._rulesDialog={...this._rulesDialog,busy:!1}),this._showToast(this._t("toast.error",{err:this._errText(e)}))}}_renderRulesDialog(){const e=this._rulesDialog,t=Xe(e.rules),i=e.test.trim()?tt(e.test,"",t):null,s=(t,i)=>{const s=[...e.rules],n=t+i;n<0||n>=s.length||([s[t],s[n]]=[s[n],s[t]],this._rulesSet(s))};return q`this._rulesDialog=null}> -
-
${this._t("rules.hint")}
-
- this._rulesDialog={...e,test:t.target.value}} /> - ${i?q`${i}`:G} -
- ${e.rules.map((t,i)=>{const n=""!==t.pattern.trim()&&!function(e){try{return new RegExp(e,"i"),!0}catch{return!1}}(t.pattern);return q`
- {const n=[...e.rules];n[i]={...t,pattern:s.target.value},this._rulesSet(n)}} /> - {const n=[...e.rules];n[i]={...t,icon:s.target.value},this._rulesSet(n)}} /> - - s(i,-1)}> - s(i,1)}> - this._rulesSet(e.rules.filter((e,t)=>t!==i))}> -
`})} - -
-
- - - - -
-
`}_saveKioskScale(e){this._kioskScale={...this._kioskScale,...e};try{localStorage.setItem(Fv,JSON.stringify(this._kioskScale))}catch{}this.requestUpdate()}_renderKioskDialog(){const e=this._kioskScale,t=(t,i)=>q` -
- ${this._rangeInput(50,300,5,Math.round(100*e[t]),e=>this._saveKioskScale({[t]:e/100}))} - ${Math.round(100*e[t])}% -
`;return q`this._kioskDialog=!1}> -
-
${this._t("kiosk.hint")}
- ${t("icon",this._t("kiosk.icon_scale"))} - ${t("font",this._t("kiosk.font_scale"))} -
-
- - - -
-
`}_fixedFloorValue(e){if("string"==typeof e)return e||"''";try{const t=JSON.stringify(e);return(void 0===t?String(e):t).slice(0,160)}catch{return String(e).slice(0,160)}}render(){if(!this._config||!this.hass)return G;const e=this._model,t=this.houseplanDiagnostics(),i=this._fixedFloorState(e);if("pending"===i.kind)return q` -
-
${this._config.title||this._t("card.title")}
-
-
- -

${this._t("fixed_floor.loading")}

-
-
`;if("invalid"===i.kind)return q` -
-
${this._config.title||this._t("card.title")}
-
- -
`;if(!e.length)return q` -
-
${this._config.title||this._t("card.title")}
-
-
- -

${this._t("empty.no_spaces")}

- ${this._serverStorage?q`

${this._t("empty.add_first")}

- `:q`

${this._t("empty.install")}

`} -
- ${this._spaceDialog?this._renderSpaceDialog():G} - ${this._importDialog?this._renderImportDialog():G} - ${this._toast?q``:G} -
`;const s=this._spaceModel();if(!s)return G;const n="valid"===i.kind?[s]:e,o=s.vb,a=this._effectiveProjection();this._renderProjection=a,this._labs.active.length&&function(e){$f||Tf(e);const t=xf,i=t.active.join(",");if(!i||i===Mf)return;Mf=i;const s=new Map(_f.map(e=>[e.id,e])),n=t.active.map(e=>{const t=s.get(e);return`${e} (#${t.issue}, expires ${t.expires})`}).join(", ");console.info(`HOUSEPLAN LABS: ${n}`)}(vv);const r="iso"===a,l="devices"===this._mode&&this._showAll,c=this._renderDevices.filter(e=>e.space===s.id&&(!e.hidden||l)),h=this._renderDeviceSnapshot,d=this._spaceDisplayForRender(),p=r?this._isoDecorationLayers(d):null,u=p?this._isoOpeningPanels(p):[],_=this._resolvedRoomFills(s,d),m=this._resolvedGlowBase(s,d,_),g=d.showLqi??this._config.show_signal??!0,f=this._config.icon_size??2.5,v=f>8?2.5:f,b=B_(v),y=this._viewOr(this._baseVb()),w=this._floorView(y),k=this._editing?"":this._stageBg(d),x=this._opMeasureView,$=this._decorMeasure,S=this._bdLive,M=this._furnLive,C="view"===this._mode?this._editorChromeMode:this._mode,D=this._roomHoverPaths(s),P=s.bg?this._display(s.bg.href):"",T=this._continuity.overlayVisible||"recovery-error"===this._continuity.state?this._continuity.recoveryReason:null,I=this._modeTransitionVisual,R=this._dayCycleState(),A=I?.viewWeight??("view"===this._mode?1:0),z=this._modeTransition.state?.from.presentedMode,F=!this._markup||!!I&&("view"===I.presentedMode||"devices"===I.presentedMode||"view"===z||"devices"===z),N=I?.stageColor||k,E=I?.sceneBrightness??1;return q` - this._notePointer(e)} - @pointerdown=${this._touchGestureGuard} - @pointermove=${this._touchGestureGuard} - @pointerup=${this._touchGestureGuard} - @pointercancel=${this._touchGestureGuard} - @lostpointercapture=${this._touchGestureGuard} - @click=${this._touchGestureGuard}> -
-
-
- - ${this._config.title||this._t("card.title")} -
-
this._tabPointerMove(e)}> - ${n.map(e=>q``)} - ${""} - ${!this._canEdit||this._kiosk||this._hasFixedFloor?G:q``} -
- ${this._canEdit?q`
- ${[["plan","mdi:floor-plan"],["devices","mdi:tune-variant"],["decor","mdi:draw"]].map(([e,t])=>q``)} -
`:G} - ${this._t("count.devices",{n:c.filter(e=>!e.hidden).length})} - - ${this._labsIso&&"view"===this._mode&&!this._kiosk?q``:G} -
- - ${""} - - -
- ${this._norm&&this._canEdit?q``:G} -
- ${this._canEdit&&!this._kiosk?q`
-
- ${"plan"===C?this._renderMarkupBar():"devices"===C?this._renderDevicesBar():this._renderDecorBar()} -
-
`:G} -
- -
this._markupClick(e)} - @wheel=${e=>this._onWheel(e)} - @pointerdown=${e=>{this._notePointer(e),this._stagePointerDown(e)}} - @pointermove=${e=>this._stagePointerMove(e)} - @pointerleave=${e=>this._stagePointerLeave(e)} - @pointerup=${e=>this._stagePointerUp(e)} - @pointercancel=${e=>this._stagePointerCancel(e)}> - ${Lr(R,A)} - ${this._renderEditorSecondary()} -
- ${r&&p?.structural?W``:G} - - - ${""} - ${this._wallHatchDefs(d.color)}${W`${this._paperShapes(s.rooms).map(e=>"path"in e?W``:"poly"in e?W``:W``)}`} - ${this._editing?this._renderMarkupDefs(o):G} - ${""} - ${this._editing&&!this._markup&&this._gridLevels()?W``:G} - ${s.bg&&P?W`this._onBackdropLoaded(s.bg.href,P)} - transform=${s.bg.angle?`rotate(${s.bg.angle} ${s.bg.x+s.bg.w/2} ${s.bg.y+s.bg.h/2})`:G} - @dblclick=${e=>this._openBackdropDialog(e)} - preserveAspectRatio="none" />`:G} - ${(()=>{const e=this._openCuts(),t=this._thickWallCuts(),i=this._wallUnionGeometry()?.roomGeom,n=new Map,o=e=>(n.has(e)||n.set(e,hi(e)),n.get(e));return s.rooms.filter(e=>e.area||"view"===this._mode||this._markup||d.showBorders).map(n=>{let a="room "+(s.bg?"overlay":"yard")+(this._markup?" outlined":"");!this._markup||n.id!==this._mergeSel&&n.id!==this._splitSel?.roomId||(a+=" picked");let r="";const l=zs(d.fill,n);if(!this._markup&&(d.showBorders||"none"!==l)){a+=" styled";const e=[];e.push(`--room-stroke:${d.color}`,`--room-stroke-op:${d.showBorders?d.opacity:0}`);const t=_.byRoom.get(n)||null;t?(a+=" filled",e.push(`--room-fill:${t.color}`,`--room-fill-op:${t.opacity}`)):e.push("--room-fill:transparent","--room-fill-op:0"),r=e.join(";")}let c;const h=e=>{"view"===this._mode&&(void 0===c&&(c=this._roomArea(n)),this._showTip(e,n.name||this._t("room.unnamed"),c?this._t("tip.area",{value:c}):"",g?this._roomLqi(n.area):null,this._roomTemp(n),this._roomHum(n)))},p=o(n),u=this._markup&&(n.id===this._mergeSel||n.id===this._splitSel?.roomId),m=p&&!u?e.filter(e=>{const t=[(e[0]+e[2])/2,(e[1]+e[3])/2];return p.some((e,i)=>Hs(t,[e[0],e[1],...p[(i+1)%p.length]])<=.08*this._gridPitch)}):[],f=u?[]:t,v=m.concat(f);v.length&&(a+=" noedge");const b=this._spaceWalls.length&&n.id&&p&&this._innerRoomContour(s,n.id,e,i)||p,y=b?Pi(b,(w=n,s.rooms.filter(e=>e!==w).map(o).filter(Boolean))):[];var w;const k=e=>"M "+e.map(e=>e[0]+" "+e[1]).join(" L ")+" Z",x=b?this._cleanFloor(n,b,s).path:"",$=n.id||G,S=n.area||G,M=x&&b?W`{this._notePointer(e),this._pointerModality.hoverEnabled&&(this._hoverRoom={space:s.id,room:n})}} - @pointermove=${h} - @pointerleave=${()=>this._clearTransientHover()}>`:y.length&&b?W`{this._notePointer(e),this._pointerModality.hoverEnabled&&(this._hoverRoom={space:s.id,room:n})}} - @pointermove=${h} - @pointerleave=${()=>this._clearTransientHover()}>`:b&&b!==p?W`{this._notePointer(e),this._pointerModality.hoverEnabled&&(this._hoverRoom={space:s.id,room:n})}} - @pointermove=${h} - @pointerleave=${()=>this._clearTransientHover()}>`:n.poly?W`{this._notePointer(e),this._pointerModality.hoverEnabled&&(this._hoverRoom={space:s.id,room:n})}} - @pointermove=${h} - @pointerleave=${()=>this._clearTransientHover()}>`:W`{this._notePointer(e),this._pointerModality.hoverEnabled&&(this._hoverRoom={space:s.id,room:n})}} - @pointermove=${h} - @pointerleave=${()=>this._clearTransientHover()}>`,C=v.length&&p?Ps(p,v,.02*this._gridPitch):null,D=C?W``:G;return W`${M}${D}`})})()} - ${this._renderRoomHoverFill(D)} - ${this._renderOpeningTunnelFills(s,_)} - ${this._renderGlowBaseRooms(s,m)} - ${this._renderOpeningTunnelFills(s,m,"glow-base")} - ${""} - ${d.hideDecor&&"decor"!==this._mode?G:this._renderDecorLayer()} - ${F?this._renderGlowLayer(s,d):G} - ${this._renderSunRays(s)} - ${this._editing?W`${this._renderAlignGuides()}`:G} - ${this._markup?W`${this._renderMarkupLayer(o)}`:G} - ${""} - ${""} - ${this._editing?G:this._renderZeroWalls(d)} - ${this._renderWallBodies(d)} - ${this._markup&&"resize"===this._tool?this._renderResizeMeasurements():G} - ${this._renderRoomHoverOutline(D)} - ${""} - ${this._editing?this._renderZeroWalls(d):G} - ${this._markup?W`${this._renderHiddenWallDiagnosticOverlay()}`:G} - ${this._markup?W`${this._renderOpeningPlacementPreview()}`:G} - ${x?this._renderOpeningDimensionGuides(x):G} - ${x?.guide?this._renderOpeningCenterTick(x.guide):G} - ${""} - ${""} - ${this._markup?W`${this._renderActiveChainInk()}`:G} - ${this._markup?W`${this._renderPlanSnapOverlay()}`:G} - ${d.hideOpenings&&!this._markup||p&&!p.floorSymbols?G:this._renderOpenings(d)} - ${this._renderWallThickUi()} - ${this._markup&&"resize"===this._tool?this._renderResizeLayer(y):G} - ${""} - ${this._renderBackdropFrame(y)} - ${this._renderTextFrame(y)} - - - ${r&&p?.structural?W` - `:G} - ${""} -
- ${c.map(e=>this._renderDevice(e,y,g))} - ${this._renderVacuums(c,y)} - ${this._renderVacFit(y)} - ${this._renderOpeningLocks(y)} - ${d.showNames||this._markup?s.rooms.map(e=>this._renderRoomLabel(e,s,y,d)):G} - ${this._markup?s.rooms.map(e=>this._renderRoomGear(e,s,y)):G} -
- ${this._measureAnchor?q`
${this._renderMeasureLabel(y)}
`:G} - ${this._resize.liveLabels?q`
${this._resize.liveLabels?.map(e=>q`
${e.text}
`)}
`:G} - ${x?q`
${x.labels.map(e=>q`
${e.text}
`)}
`:G} - ${this._wallDialog?q`
${this._renderWallThickDialog()}
`:G} - ${$?q`
${$.text}
`:G} - ${M?q`
${M.map(e=>q`
${e.text}
`)}
`:G} - ${S?q`
${S.text}
`:G} -
- ${this._zoom>1?q`
${Math.round(100*this._zoom)}%
`:G} - ${this._renderFarHint()} - ${this._renderHomeArrow()} - ${this._renderRecoveryOverlay()} - ${this._booting||this._bootFading?q``:G} -
- - ${this._roomDialog?this._renderRoomDialog():G} - ${this._mergeDialog?this._renderMergeDialog():G} - ${this._openingDialog?this._renderOpeningDialog():G} - ${this._physicalDialog?this._renderPhysicalDialog():G} - ${this._partitionDeleteDialog?this._renderPartitionDeleteDialog():G} - ${this._roomDeleteDialog?this._renderRoomDeleteDialog():G} - ${this._openingInfo?this._renderOpeningInfoCard():G} - ${this._decorTextDialog?this._renderDecorTextDialog():G} - ${this._decorShapeDialog?this._renderDecorShapeDialog():G} - ${this._backdropDialog?this._renderBackdropDialog():G} - ${this._decorEraseConfirm?this._renderDecorEraseConfirm():G} - ${this._spaceDialog?this._renderSpaceDialog():G} - ${this._deviceInbox?this._renderDeviceInbox():G} - ${this._markerDialog?this._renderMarkerDialog():G} - ${this._vacCalConfirm?q`this._vacCalConfirm=null}> -
-

${this._t("vac.residual_message",{error:this._vacCalConfirm.error})}

-
-
- - - - -
-
`:G} - ${this._infoCard?this._renderInfoCard():G} - ${this._rulesDialog?this._renderRulesDialog():G} - ${this._settingsDialog?this._renderSettingsDialog():G} - ${this._alignDialog?this._renderAlignDialog():G} - ${this._backupExportDialog?this._renderBackupExportDialog():G} - ${this._backupImportDialog?this._renderBackupImportDialog():G} - ${this._importDialog?this._renderImportDialog():G} - ${this._tip?q`
- ${this._tip.title}${this._tip.meta?q`${this._tip.meta}`:G} - ${null!=this._tip.temp?q`${this._t("tip.temp_avg")} ${this._tip.temp}°`:G} - ${null!=this._tip.hum?q`${this._t("tip.hum_avg")} ${this._tip.hum}%`:G} - ${null!=this._tip.lqi?q`${this._t("tip.lqi")} - ${this._tip.lqi}`:G} -
`:G} - ${this._kiosk&&!this._hasFixedFloor&&this._kioskDots&&this._model.length>1?q`
- ${this._model.map(e=>q``)} -
`:G} - ${this._kioskDialog?this._renderKioskDialog():G} - ${this._vacFit?q`
- ${this._t("vac.fit_hint")} - - - - -
`:G} - ${this._tapConfirm?q`this._tapConfirm=null}> -
- ${"run"===this._tapConfirm.kind?q`

${this._tapConfirm.text}

`:this._tapConfirm.lines.map((e,t)=>q` -

${e}

`)} -
-
- - - -
-
`:G} - ${this._toast?q``:G} -
- `}_vacCandidateStatus(e,t,i=this._planHass){const s=this._bindingStatus("entity:"+e);if("ha_disabled"===s.kind)return"disabled";if("orphaned"===s.kind)return"missing";if("unverified"===s.kind)return"unverified";const n=i?.states?.[e];return"unavailable"===n?.state?"unavailable":t?.hasPosition?"ok":"unsupported"}_vacOpenAllCameras(e){const t=this._haRegistry.entities||{},i=[];for(const[e,s]of Object.entries(this.hass?.states||{})){if(!e.startsWith("camera."))continue;const n=_p(e,s,t[e]);n&&i.push(n)}this._vacAllCameraCache={devId:e.id,candidates:i},this._vacAllCamerasFor=e.id}_vacSourceResolution(e,t=!1,i=this._planHass){const s=e.marker?.vacuum,n="string"==typeof s?.source&&!!s.source,o=new Set(e.entities||[]),a=new Set(o);n&&a.add(s.source);const r=i?.entities||{},l=[];for(const e of a){const t=i?.states?.[e],o=_p(e,t,r[e]);o?l.push(o):n&&e===s.source&&l.push({entityId:e,name:String(t?.attributes?.friendly_name||e),platform:r[e]?.platform?String(r[e].platform):null,category:e.startsWith("camera.")?"camera":"partial",hasPosition:!1,hasRooms:!1,hasPath:!1,hasMapId:!1,score:0})}if(t&&this._vacAllCameraCache?.devId===e.id){const e=new Set(l.map(e=>e.entityId));for(const t of this._vacAllCameraCache.candidates)e.has(t.entityId)||l.push(t)}const c={};for(const e of l)c[e.entityId]=this._vacCandidateStatus(e.entityId,e,i);return function(e,t,i,s){const n="string"==typeof e&&e.length>0,o=new Set(t),a=Array.from(i).sort((t,i)=>n&&t.entityId===e?-1:n&&i.entityId===e?1:i.score-t.score||t.entityId.localeCompare(i.entityId));if(n){const t=a.find(t=>t.entityId===e);return{entityId:e,status:s[e]||(t?.hasPosition?"ok":"unverified"),pinned:!0,candidates:a}}const r=a.find(e=>o.has(e.entityId)&&e.hasPosition&&"ok"===s[e.entityId]);return r?{entityId:r.entityId,status:"ok",pinned:!1,candidates:a}:{entityId:null,status:"none",pinned:!1,candidates:a}}(s?.source,o,l,c)}_vacSource(e,t=this._planHass){if(!1===e.marker?.vacuum?.live)return null;const i=this._vacSourceResolution(e,!1,t);return"ok"===i.status||"unsupported"===i.status?i.entityId:null}_vacEntity(e){return e.primary?.startsWith("vacuum.")?e.primary:(e.entities||[]).find(e=>e.startsWith("vacuum."))||null}_isVacDev(e){return!!this._vacEntity(e)}_activitySourceKey(e){return this._activitySnapshot(e).sourceKey}_activitySnapshot(e,t=ho(this._planHass,this._devices,null,this._virtualLights)){const i=I_(this._planHass,e,this._devices,t,this._fullRegistryHass);return{samples:i.samples,sourceKey:z_(this._planHass,e,i)}}_stampActivity(e,t,i){let s=this._activityRt.get(e);s||(s=_m(i||"",[]),this._activityRt.set(e,s)),null!=i&&(s.sources=i),gm(s,t,Date.now(),window.clearTimeout.bind(window),e=>window.setTimeout(()=>this.requestUpdate(),e))}_syncActivityRuntime(){const e=new Map;if(!this.hass)return e;if(!1===this._config?.live_states){for(const e of this._activityRt.values())clearTimeout(e.timer);return this._activityRt.clear(),e}const t=new Set,i=ho(this._planHass,this._devices,null,this._virtualLights);for(const s of this._devices){if(s.hidden)continue;if("icon_ripple"!==Bi(s.marker?.display))continue;t.add(s.id);const n=this._activitySnapshot(s,i);e.set(s.id,n);const{samples:o,sourceKey:a}=n;let r=this._activityRt.get(s.id);r?r.sources!==a&&mm(r,a,o,window.clearTimeout.bind(window)):(r=_m(a,o),this._activityRt.set(s.id,r))}for(const[e,i]of this._activityRt)t.has(e)||(clearTimeout(i.timer),this._activityRt.delete(e));return e}_activityTick(){if(!this.hass)return;const e=this._syncActivityRuntime();for(const t of this._devices){if(t.hidden)continue;if("icon_ripple"!==Bi(t.marker?.display))continue;const i=e.get(t.id)||this._activitySnapshot(t),{samples:s,sourceKey:n}=i,o=this._activityRt.get(t.id);if(!o||o.sources!==n)continue;const a=fm(o,s,window.clearTimeout.bind(window));a&&this._stampActivity(t.id,a,n)}}_vacTick(){if(this.hass)for(const e of this._devices){if(e.hidden||!this._isVacDev(e))continue;if("static_icon"===Bi(e.marker?.display)){this._vacRt.delete(e.id);continue}const t=this._vacSource(e);if(!t)continue;const i=this._vacEntity(e),s=vp(this.hass.states[i||""]?.state),n=up(this.hass.states[t]?.attributes);let o=this._vacRt.get(e.id);o||(o={trail:[],lastKey:"",lastTs:0,moving:!1,jump:!1,endedTs:0,lastPos:null},this._vacRt.set(e.id,o)),s&&!o.moving&&(o.trail=[],o.lastPos=null);const a="never"!==xp(e.marker?.vacuum)&&!n?.path.length;!s&&o.moving&&(o.endedTs=Date.now(),a&&o.lastPos&&(o.trail=fp(o.trail,o.lastPos,40)),o.lastPos=null),o.moving=s;const r=n?.pos;if(s&&r){const e=r.x+":"+r.y;if(e!==o.lastKey){const t=Date.now();o.jump=o.lastTs>0&&t-o.lastTs>1e4,o.lastKey=e,o.lastTs=t,a&&o.lastPos&&(o.trail=fp(o.trail,o.lastPos,40)),o.lastPos=[r.x,r.y]}}}}_vacEnsureMarker(e){const t=this._serverCfg;if(!t)return null;t.markers=t.markers||[];const i=t.markers.find(t=>t.id===e.id);if(i)return i;if("device"!==e.bindingKind&&"entity"!==e.bindingKind||!e.bindingRef)return null;const s={id:e.id,binding:e.bindingKind+":"+e.bindingRef,space:e.space||null,area:e.area||null,hidden:!!e.hidden};return t.markers.push(s),s}_renderVacSection(e){const t=this._devices.find(t=>t.id===e.devId);if(!t||!this._isVacDev(t))return G;const i=t.marker?.vacuum||{},s=this._vacAllCamerasFor===t.id,n=this._vacSourceResolution(t,s),o=n.entityId,a=n.candidates.find(e=>e.entityId===o)||null,r=o?up(this.hass?.states[o]?.attributes):null,l=!1!==i.live&&("ok"===n.status||"unsupported"===n.status),c=this._vacPlanRoomAnchors(t.space),h=r?function(e,t){const i=new Set(t.map(mp).filter(Boolean)),s=new Set;for(const t of e){const e=mp(t.name||"");e&&i.has(e)&&s.add(e)}return s.size}(r.rooms,c.map(e=>e.name)):0,d=!!(l&&r&&h>=3),p=Object.keys(i.calibration||{}),u=e=>{const i=this._vacEnsureMarker(t);i&&(i.vacuum={...i.vacuum||{},...e},this._regSignature="",this._maybeRebuildDevices(),this._saveConfig(),this.requestUpdate())},_=new Set(t.entities||[]),m=n.candidates.filter(e=>_.has(e.entityId)||e.entityId===i.source),g=s?n.candidates.filter(e=>e.entityId.startsWith("camera.")&&!m.some(t=>t.entityId===e.entityId)):[],f=e=>[e.hasPosition?this._t("vac.cap_position"):"",e.hasRooms?this._t("vac.cap_rooms_short"):"",e.hasPath?this._t("vac.cap_path"):"",e.hasMapId?this._t("vac.cap_map"):""].filter(Boolean).join(" · ")||this._t("vac.cap_none"),v=e=>q` - `,b=n.candidates.some(e=>_.has(e.entityId)&&"known_xcme_incomplete"===e.category)||!!n.pinned&&!!o?.startsWith("camera.")&&!a?.hasPosition;return q` - -
-
-
${this._t("vac.diag_source")}${o||this._t("vac.source_none")}
- ${a?.platform?q`
${this._t("vac.diag_platform")}${a.platform}
`:G} -
${this._t("vac.diag_status")}${this._t(`vac.source_status_${n.status}`)}
-
${this._t("vac.diag_position")}${r?.pos?this._t("common.yes"):this._t("common.no")}
-
${this._t("vac.diag_rooms")}${ss(this._t("vac.diag_rooms_value"),{total:String(r?.rooms.length||0),matched:String(h),readiness:this._t(h>=3?"vac.autocal_ready":"vac.autocal_not_ready")})}
-
${this._t("vac.diag_path")}${r?.path.length?this._t("common.yes"):this._t("common.no")}
-
${this._t("vac.diag_map")}${r?.mapId??"default"}
-
- ${"missing"===n.status||"disabled"===n.status||"unverified"===n.status?q`
- ${this._t(`vac.source_banner_${n.status}`)} - -
`:G} - ${b?q`
- ${this._t("vac.xcme_hint")} -
attributes:
-  - vacuum_position
-  - rooms
-  - path
-  - map_name
-
`:G} -
- ${this._t("vac.choose_source")} -
- - ${m.map(v)} -
{e.currentTarget.open?this._vacOpenAllCameras(t):(this._vacAllCamerasFor=null,this._vacAllCameraCache=null)}}> - ${this._t("vac.all_cameras")} -
${this._t("vac.all_cameras_warn")}
- ${s?g.length?g.map(v):q`
${this._t("vac.all_cameras_empty")}
`:G} -
-
-
-
- ${d?q``:G} - ${l?q``:G} - ${this._t("vac.documentation")} -
- ${r?q` - - - - ${p.length?q`
${ss(this._t("vac.cal_maps"),{maps:p.join(", ")})}
`:G} - `:G} -
`}_vacMapId(e,t,i=this._planHass){const s=this._vacEntity(e),n=s?i?.states?.[s]?.attributes?.selected_map:null;return o=t.mapId,a=n,"default"!==o?o:null!=a?String(a):"default";var o,a}_vacSaveMatrix(e,t,i,s){const n=this._devices.find(t=>t.id===e),o=n?this._vacEnsureMarker(n):this._serverCfg?.markers?.find(t=>t.id===e);if(!o)return!1;const a={...o.vacuum||{}};return a.source=t,a.calibration={...a.calibration||{},[i]:s.map(e=>Number(e.toFixed(6)))},o.vacuum=a,this._regSignature="",this._maybeRebuildDevices(),this._saveConfig(),this.requestUpdate(),!0}_vacPlanRoomAnchors(e){return(this._spaceModelById(e)?.rooms||[]).map(e=>({room:e,poly:hi(e)})).filter(({room:e,poly:t})=>e.name&&t).map(({room:e,poly:t})=>{const i=lp(t);return i?{name:String(e.name),cx:i[0],cy:i[1]}:null}).filter(Boolean)}_vacAutoCalibrate(e){const t=this._vacSource(e),i=t?up(this.hass?.states[t]?.attributes):null;if(!t||!i||i.rooms.length<3)return void this._showToast(this._t("vac.autocal_no_rooms"));const s=this._vacPlanRoomAnchors(e.space),n=gp(i.rooms,s);if(!n)return void this._showToast(this._t("vac.autocal_no_match"));const o=this._serverCfg?.spaces?.find(t=>t.id===e.space),a=Number(o?.cell_cm),r=Number.isFinite(a)&&a>0?a:5,l=function(e,t,i){return![e,t,i].every(Number.isFinite)||e<0||t<=0||i<=0?Number.POSITIVE_INFINITY:e/t*i}(n.residual,this._gridPitch,r),c=this._vacMapId(e,i);l>40?this._vacCalConfirm={markerId:e.id,source:t,mapId:c,matrix:n.matrix,rooms:n.matched.length,error:li(l,"mi"===this.hass?.config?.unit_system?.length)}:this._vacSaveMatrix(e.id,t,c,n.matrix)&&this._showToast(ss(this._t("vac.autocal_done"),{rooms:String(n.matched.length)}))}_vacApplyCalibrationProposal(e){const t=this._vacCalConfirm;if(t){if(this._vacCalConfirm=null,e){const e=this._devices.find(e=>e.id===t.markerId),i=wp(t.matrix);if(!e||!i)return;if(this._markerDialog=null,e.space!==this._space&&!this._commitSpace(e.space))return;return void(this._vacFit={markerId:t.markerId,source:t.source,mapId:t.mapId,p:i,drag:null})}this._vacSaveMatrix(t.markerId,t.source,t.mapId,t.matrix)&&this._showToast(ss(this._t("vac.autocal_done"),{rooms:String(t.rooms)}))}}_vacStartFit(e){const t=this._vacSource(e),i=t?up(this.hass?.states[t]?.attributes):null;if(!t||!i)return void this._showToast(this._t("vac.cal_need_pos"));const s=this._vacMapId(e,i),n=e.marker?.vacuum?.calibration?.[s],o=this._spaceModelById(e.space);if(!o)return;const a=o.vb,r=n&&6===n.length&&wp(n)||function(e,t){const i=[],s=[];for(const t of e)null!=t.x0?(i.push(t.x0,t.x1),s.push(t.y0,t.y1)):(i.push(t.cx),s.push(t.cy));if(!i.length)return{ox:t[0]+t[2]/2,oy:t[1]+t[3]/2,s:t[2]/1e4,rot:0,mir:!0};const n=Math.min(...i),o=Math.max(...i),a=Math.min(...s),r=Math.max(...s),l=Math.max(o-n,r-a)||1,c={ox:0,oy:0,s:.6*Math.min(t[2],t[3])/l,rot:0,mir:!0},h=yp(c),[d,p]=np(h,(n+o)/2,(a+r)/2);return c.ox=t[0]+t[2]/2-d,c.oy=t[1]+t[3]/2-p,c}(i.rooms,a);this._markerDialog=null,(e.space===this._space||this._commitSpace(e.space))&&(this._vacFit={markerId:e.id,source:t,mapId:s,p:r,drag:null})}_vacFitSave(){const e=this._vacFit;if(!e)return;const t=this._vacSaveMatrix(e.markerId,e.source,e.mapId,yp(e.p));this._vacFit=null,t&&this._showToast(this._t("vac.cal_done"))}_vacFitTurn(e){const t=this._vacFit;if(!t)return;const i=up(this.hass?.states[t.source]?.attributes),s=this._vacGhostCentre(i?.rooms||[]),n={...t.p,...e};this._vacFit={...t,p:kp(n,t.p,s[0],s[1])}}_vacGhostCentre(e){const t=[],i=[];for(const s of e)t.push(s.x0??s.cx,s.x1??s.cx),i.push(s.y0??s.cy,s.y1??s.cy);return t.length?[(Math.min(...t)+Math.max(...t))/2,(Math.min(...i)+Math.max(...i))/2]:[0,0]}_vacDelta(e,t,i){const s=this._stageEl,n=s?.clientWidth||1,o=s?.clientHeight||1;return[t/n*e.w,i/o*e.h]}_vacFitPointer(e,t){const i=this._vacFit;if(!i)return;if(e.stopPropagation(),"pointerdown"===e.type){const t=e.target,s=t.getAttribute?.("data-corner");try{e.currentTarget.setPointerCapture?.(e.pointerId)}catch{}return void(this._vacFit={...i,drag:s?{kind:"scale",sx:e.clientX,sy:e.clientY,p0:{...i.p},fx:Number(s.split(",")[0]),fy:Number(s.split(",")[1])}:{kind:"move",sx:e.clientX,sy:e.clientY,p0:{...i.p},fx:0,fy:0}})}const s=i.drag;if(s){if("pointermove"===e.type){const[n,o]=this._vacDelta(t,e.clientX-s.sx,e.clientY-s.sy);if("move"===s.kind)this._vacFit={...i,p:{...s.p0,ox:s.p0.ox+n,oy:s.p0.oy+o}};else{const e=up(this.hass?.states[i.source]?.attributes),t=this._vacGhostCentre(e?.rooms||[]),a=yp(s.p0),[r,l]=np(a,t[0],t[1]),[c,h]=np(a,s.fx,s.fy),d=Math.hypot(r-c,l-h)||1,[p,u]=[2*r-c,2*l-h],_=Math.hypot(p+2*n-c,u+2*o-h)/2,m=Math.max(.05,_/d),g={...s.p0,s:s.p0.s*m};this._vacFit={...i,p:kp(g,s.p0,s.fx,s.fy)}}return}"pointerup"!==e.type&&"pointercancel"!==e.type||(this._vacFit={...i,drag:null})}}_renderVacFit(e){const t=this._vacFit;if(!t)return G;const i=up(this._renderPlanHass?.states?.[t.source]?.attributes);if(!i)return G;const s=yp(t.p),n=[],o=[],a=[];for(const e of i.rooms){if(null==e.x0)continue;const t=[[e.x0,e.y0],[e.x1,e.y0],[e.x1,e.y1],[e.x0,e.y1]].map(([e,t])=>np(s,e,t));t.forEach(([e,t])=>{o.push(e),a.push(t)});const[i,r]=np(s,e.cx,e.cy);n.push(W` - ${e.name}`)}let r=G;if(i.pos){const[t,n]=np(s,i.pos.x,i.pos.y);r=W``}const l=[];if(o.length){const t=(()=>{const e=s[0]*s[4]-s[1]*s[3];return(t,i)=>[(s[4]*(t-s[2])-s[1]*(i-s[5]))/e,(-s[3]*(t-s[2])+s[0]*(i-s[5]))/e]})(),i=Math.min(...o),n=Math.max(...o),r=Math.min(...a),c=Math.max(...a),h=.022*e.w,d=h/4;for(const[e,s,o,a]of[[i,r,n,c],[n,r,i,c],[n,c,i,r],[i,c,n,r]]){const i=t(o,a);l.push(W` - `)}}return q`this._vacFitPointer(t,e)} - @pointermove=${t=>this._vacFitPointer(t,e)} - @pointerup=${t=>this._vacFitPointer(t,e)} - @pointercancel=${t=>this._vacFitPointer(t,e)}>${n}${r}${l}`}_vacRafLoop(){this._vacRaf=requestAnimationFrame(()=>{const e=this.renderRoot,t=this._stageEl,i=this._vacLastView,s=e?.querySelectorAll?.(".vacpuck")||[];if(!t||!i||!s.length)return void(this._vacRaf=0);const n=t.getBoundingClientRect();for(const t of s){const s=t.getAttribute("data-mid"),o=t.getBoundingClientRect(),a=i.x+(o.left+o.width/2-n.left)/n.width*i.w,r=i.y+(o.top+o.height/2-n.top)/n.height*i.h;for(const t of e.querySelectorAll(`line.tip[data-mid="${s}"]`))t.setAttribute("x2",a.toFixed(1)),t.setAttribute("y2",r.toFixed(1))}this._vacRafLoop()})}_renderVacuums(e,t){if(this._markup||"decor"===this._mode)return G;const i=this._space+"|"+t.x+"|"+t.y+"|"+t.w+"|"+t.h,s=this._vacJumpOnce||i!==this._vacViewKey;this._vacViewKey=i,this._vacJumpOnce=!1;const n=[],o=[];for(const i of e){if(i.hidden||!this._isVacDev(i))continue;if("static_icon"===Bi(i.marker?.display))continue;const e=this._renderDeviceSnapshot?.facts.get(`vacuum:${i.id}`),a=e?.source??this._vacSource(i,this._renderPlanHass);if(!a)continue;const r=e?.telemetry??up(this._renderPlanHass?.states[a]?.attributes);if(!r)continue;const l=String(e?.mapId??this._vacMapId(i,r,this._renderPlanHass)),c=i.marker?.vacuum?.calibration?.[l];if(!c||6!==c.length)continue;const h=e?.runtime??this._vacRt.get(i.id),d=h?.moving??!1,p=xp(i.marker?.vacuum),u="always"===p||"cleaning"===p&&d,_=e?.server??this._vacSrvTrails[i.id],m=_?.current?.map_id===l&&Array.isArray(_.current.points)?_.current:null,g=_?.previous?.map_id===l&&Array.isArray(_.previous.points)?_.previous:null;if("always"===p&&g&&g.points.length>1){const e=g.points.map(([e,t])=>{const[i,s]=np(c,e,t),n=this._scenePoint([i,s]);return n[0].toFixed(1)+","+n[1].toFixed(1)}).join(" ");o.push(W``)}if(u){const e=dp(r,m,h?.trail||[]),t=!d||"integration"!==e.source&&"server"!==e.source?e.path:pp(e.path);if(t.length){const e=t.map(e=>e.map(([e,t],i)=>{const[s,n]=np(c,e,t),o=this._scenePoint([s,n]);return`${i?"L":"M"} ${o[0].toFixed(1)} ${o[1].toFixed(1)}`}).join(" ")).join(" ");o.push(W``);const s=t[t.length-1];if(d&&s?.length>=2){const e=s[s.length-1],[t,n]=np(c,e[0],e[1]),a=this._scenePoint([t,n]),r=a[0].toFixed(1),l=a[1].toFixed(1);o.push(W``)}}}if(!d||!r.pos)continue;const[f,v]=np(c,r.pos.x,r.pos.y),b=this._scenePoint([f,v]),y=(b[0]-t.x)/t.w*100,w=(b[1]-t.y)/t.h*100,k=h&&h.lastTs>0&&Date.now()-h.lastTs>6e4,x=i.marker?.icon||i.icon||"mdi:robot-vacuum";n.push(q`
{e.stopPropagation();const t=this._vacEntity(i);t&&this._openMoreInfo(t)}}> - -
`)}return this._vacLastView=t,n.length&&!this._vacRaf&&this._vacRafLoop(),n.length||o.length?q` - ${o.length?W`${o}`:G} - ${n}`:G}_renderDevice(e,t,i=!0){const s=this._pos(e),n=this._scenePoint([s.x,s.y]),o=(n[0]-t.x)/t.w*100,a=(n[1]-t.y)/t.h*100,r=this._devicePresentation(e,i),l=[`left:${o}%`,`top:${a}%`,...Go(r)],c=r.disabledReason,h=r.haDisabled?this._t(`marker.ha_disabled_${c}`):e.userHidden?this._t("marker.hidden_ghost"):e.name,d=S_(r),p="view"===this._mode||"devices"===this._mode,u=[h,r.haDisabled?"":this._t(`marker.state_a11y_${d}`),"none"!==r.pulse.kind?this._t(`marker.pulse_a11y_${r.pulse.reason}`):"",r.valueFullText||r.valueText||"",Io(r.valueBadge),null!=r.lqiText&&r.lqiBand?this._t(`marker.lqi_a11y_${r.lqiBand}`,{value:r.lqiText}):""].filter(Boolean).join(", "),_=[e.model,r.valueBadge?.fullText||"",null!=r.lqiText?"LQI "+r.lqiText:""].filter(Boolean).join(" · ");return q`
this._clickDevice(t,e)} - @keydown=${t=>this._keyDevice(t,e)} - @contextmenu=${t=>this._ctxDevice(t,e)} - @pointerover=${t=>this._showTip(t,e.name,r.haDisabled?h:_)} - @pointerleave=${()=>this._clearTransientHover()} - @pointerdown=${t=>this._pointerDown(t,e)} - @pointermove=${t=>{this._pointerMove(t,e),this._showTip(t,e.name,r.haDisabled?h:_)}} - @pointerup=${t=>this._pointerUp(t,e)} - @pointercancel=${t=>this._pointerUp(t,e)} - > - ${Ko(r,{newDevice:this._newIds.has(e.id),newDeviceTitle:this._t("device.new"),disabledTitle:r.haDisabled?h:""})} -
`}_roomArea(e){const t=hi(e);if(!t)return null;const i=this._spaceModel();if(!i)return null;const s=this._spaceWalls.length&&e.id&&this._innerRoomContour(i,e.id)||t,n=this._cleanFloor(e,s),o=this._cellCm/this._gridPitch;return rr(n.area*o*o/1e4,"mi"===this._renderPlanHass?.config?.unit_system?.length)}_roomTemp(e){const t=e.settings?.temp_source;return t?Mo(this._renderPlanHass,t,"temp",this._markers):e.area?this._climate().get(e.area)?.temp??null:null}_roomHum(e){const t=e.settings?.hum_source;return t?Mo(this._renderPlanHass,t,"hum",this._markers):e.area?this._climate().get(e.area)?.hum??null:null}_climate(){const e=this._serverCfg?.markers,t=this._renderPlanHass,i=this._climateCache;if(i&&i.h===t&&i.r===this._iconRules&&i.mk===e)return i.m;const s=function(e,t,i){const s=new Map;if(!e?.entities)return s;const n=wo(i),o=new Set;for(const e of i||[]){if(e?.removed||!0!==e?.use_climate_temp)continue;const t=(e.binding||"").indexOf(":");t>0&&o.add(e.binding.slice(t+1))}const a=new Map;for(const[t,i]of Object.entries(e.entities)){if(i.device_id&&n.devices.has(i.device_id)&&!n.liveEntities.has(t)||!i.device_id&&n.entities.has(t)&&!n.liveEntities.has(t))continue;const s=i.device_id?e.devices?.[i.device_id]:null,r=i.area_id||s?.area_id||null;if(!r)continue;if(i.entity_category)continue;if(!(o.size>0&&t.startsWith("climate.")&&(o.has(t)||i.device_id&&o.has(i.device_id)))){if(Ye.has(i.platform))continue;if(Co.test(t))continue}let l=a.get(r);l||(l=new Map,a.set(r,l));const c=i.device_id||t;let h=l.get(c);if(!h){const n=e.states?.[t];h={name:(s?s.name_by_user||s.name:i.name||n?.attributes?.friendly_name||t)||t,model:s?.model,ents:[]},l.set(c,h)}h.ents.push(t)}for(const[i,n]of a){const a=[],r=[];for(const[i,s]of n){const n=yo(e,s.name,s.model,s.ents,t),l="mdi:thermometer"===n||"mdi:air-filter"===n;if(l){const t=mo(e,s.ents);null!=t&&a.push(t)}if(o.size>0&&(o.has(i)||s.ents.some(e=>o.has(e)))){const t=go(e,s.ents);null!=t&&a.push(t)}if(l||"mdi:water-percent"===n){const t=vo(e,s.ents);null!=t&&r.push(t)}}(a.length||r.length)&&s.set(i,{temp:a.length?Math.round(a.reduce((e,t)=>e+t,0)/a.length*10)/10:null,hum:r.length?Math.round(r.reduce((e,t)=>e+t,0)/r.length):null})}return s}(t,this._iconRules,e);return this._climateCache={h:t,r:this._iconRules,mk:e,m:s},s}_resetRoomDialogFields(){this._roomEditId=null,this._roomFill="",this._roomCustomFill=null,this._roomTempSrc="",this._roomHumSrc="",this._roomSrcOpen=null,this._roomSrcFilter="",this._roomNameScale=1,this._roomLabelScale=1}_openRoomEdit(e){if(!e.id)return;this._roomEditId=e.id,this._nameSel=e.name||"",this._areaSel=e.area||"",this._roomFill="glow"===e.settings?.fill_mode?"":e.settings?.fill_mode||"";const t=e.settings?.custom_fill;this._roomCustomFill=t&&"object"==typeof t?rs(t,cs(this._curSpaceCfg).customFill):null,this._roomTempSrc=e.settings?.temp_source||"",this._roomHumSrc=e.settings?.hum_source||"",this._roomNameScale=Ns(e.settings?.name_scale),this._roomLabelScale=Ns(e.settings?.label_scale),this._roomSrcOpen=null,this._roomSrcFilter="",this._roomDialog=!0}_roomSettingsFromDialog(){const e={};return this._roomFill&&(e.fill_mode=this._roomFill),this._roomCustomFill&&(e.custom_fill=this._roomCustomFill),this._roomTempSrc&&(e.temp_source=this._roomTempSrc),this._roomHumSrc&&(e.hum_source=this._roomHumSrc),1!==this._roomNameScale&&(e.name_scale=this._roomNameScale),1!==this._roomLabelScale&&(e.label_scale=this._roomLabelScale),Object.keys(e).length?e:null}_saveRoomEdit(){const e=this._curSpaceCfg,t=e?.rooms.find(e=>e.id===this._roomEditId);if(!t)return this._roomDialog=!1,void(this._roomEditId=null);t.name=this._nameSel.trim()||t.name,t.area=this._areaSel||null;const i=t.settings||{},s={...i};this._roomFill?s.fill_mode=this._roomFill:delete s.fill_mode,this._roomCustomFill?s.custom_fill=this._roomCustomFill:delete s.custom_fill,"glow"===i.fill_mode&&"boolean"!=typeof i.glow&&(s.glow=!0),this._roomTempSrc?s.temp_source=this._roomTempSrc:delete s.temp_source,this._roomHumSrc?s.hum_source=this._roomHumSrc:delete s.hum_source,1!==this._roomNameScale?s.name_scale=this._roomNameScale:delete s.name_scale,1!==this._roomLabelScale?s.label_scale=this._roomLabelScale:delete s.label_scale,Object.keys(s).length?t.settings=s:delete t.settings,this._saveConfig(),this._roomDialog=!1,this._roomEditId=null,this._nameSel="",this._areaSel="",this._regSignature="",this._maybeRebuildDevices(),this.requestUpdate(),this._showToast(this._t("toast.room_updated"))}_roomSrcCandidates(){const e=this._planHass,t=wo(this._markers),i=this._roomSrcFilter.trim().toLowerCase(),s=[];for(const n of Object.values(e.devices)){if("service"===n.entry_type)continue;if(t.devices.has(n.id))continue;const e=(n.name_by_user||n.name||n.id).trim();i&&!e.toLowerCase().includes(i)||s.push({value:"device:"+n.id,label:e,sub:n.model||this._t("marker.sub_device")})}for(const[n,o]of Object.entries(e.entities)){if(!n.startsWith("sensor.")||o.hidden)continue;if(ko(e,n,t))continue;const a=o.name||e.states[n]?.attributes?.friendly_name||n;i&&!(a+" "+n).toLowerCase().includes(i)||s.push({value:"entity:"+n,label:a,sub:n})}return s.sort((e,t)=>e.label.localeCompare(t.label)),s.slice(0,200)}_roomSrcLabel(e){const t=e.indexOf(":"),i=e.slice(0,t),s=e.slice(t+1);return"device"===i?this._fullRegistryHass.devices[s]?.name_by_user||this._fullRegistryHass.devices[s]?.name||s:this._fullRegistryHass.entities[s]?.name||this.hass.states[s]?.attributes?.friendly_name||s}_labelPos(e,t){const i=this._layout["rl_"+(e.id||"")];if(i&&i.s===t)return{x:i.x*Ov,y:i.y*Ov};const s=this._snap(this._roomCenter(e));return{x:s[0],y:s[1]}}_labelDown(e,t,i){if("plan"!==this._mode)return;e.preventDefault(),e.stopPropagation();const s=this._labelPos(t,i);this._drag={id:"rl_"+(t.id||""),sx:e.clientX,sy:e.clientY,ox:s.x,oy:s.y,moved:!1},Gv(e),this._tip=null}_labelMove(e,t,i){const s="rl_"+(t.id||"");if(!this._drag||this._drag.id!==s)return;const n=this._stageEl;if(!n)return;const o=this._spaceModelById(i);if(!o)return;const a=o.vb,r=n.getBoundingClientRect(),l=this._viewOr(a),c=(e.clientX-this._drag.sx)/r.width*l.w,h=(e.clientY-this._drag.sy)/r.height*l.h;Math.abs(e.clientX-this._drag.sx)+Math.abs(e.clientY-this._drag.sy)>3&&(this._drag.moved=!0);const d=Uh(this._drag.ox+c),p=Uh(this._drag.oy+h);this._savePos({id:s,space:i},d,p)}_labelUp(e){const t="rl_"+(e.id||"");if(!this._drag||this._drag.id!==t)return;const i=this._drag.moved;this._drag=i?this._drag:null,i&&window.setTimeout(()=>this._drag=null,0)}_labelScale(e){const t=this._layout["rl_"+(e.id||"")]?.k;return"number"==typeof t&&Number.isFinite(t)?Math.min(3,Math.max(.5,t)):1}_rlResizeDown(e,t,i){if("plan"!==this._mode)return;e.preventDefault(),e.stopPropagation();const s=e.target.closest(".roomlabel");if(!s)return;const n=s.getBoundingClientRect(),o=n.left+n.width/2,a=n.top+n.height/2,r=Math.max(8,Math.hypot(e.clientX-o,e.clientY-a));this._rlResize={id:"rl_"+(t.id||""),space:i,k0:this._labelScale(t),cx:o,cy:a,d0:r},Gv(e)}_rlResizeMove(e){const t=this._rlResize;if(!t)return;e.stopPropagation();const i=Math.max(8,Math.hypot(e.clientX-t.cx,e.clientY-t.cy)),s=Math.min(3,Math.max(.5,t.k0*(i/t.d0))),n=this._layout[t.id];if(n)this._layout={...this._layout,[t.id]:{...n,k:s}};else{const e=t.id.slice(3),i=this._spaceModelById(t.space);if(!i)return;const n=i.rooms.find(t=>t.id===e);if(!n)return;const o=this._labelPos(n,t.space);this._layout={...this._layout,[t.id]:{s:t.space,x:o.x/Ov,y:o.y/Ov,k:s}}}this._dirtyPos.add(t.id)}_rlResizeUp(){this._rlResize&&(this._rlResize=null,this._persistLayout())}_renderRoomGear(e,t,i){if(!e.id)return G;let s=null;if(e.poly?(s=this._gearPtCache.get(e.poly)||null,s||(s=$i(e.poly),this._gearPtCache.set(e.poly,s))):null!=e.x&&null!=e.y&&(s=[e.x+(e.w||0)/2,e.y+(e.h||0)/2]),!s)return G;const n=(s[0]-i.x)/i.w*100,o=(s[1]-i.y)/i.h*100;return q``}_renderRoomLabel(e,t,i,s){if(!e.name&&!this._markup)return G;const n=this._labelPos(e,t.id),o=this._scenePoint([n.x,n.y]),a=(o[0]-i.x)/i.w*100,r=(o[1]-i.y)/i.h*100,l=Math.min(1,s.opacity+.25),c=this._labelScale(e),h=[];if(e.area||e.settings?.temp_source||e.settings?.hum_source||s.labelLight){if(s.labelTemp){const t=this._roomTemp(e);null!=t&&h.push(q`${t}°`)}if(s.labelHum){const t=this._roomHum(e);null!=t&&h.push(q`${t}%`)}if(s.labelLqi&&e.area){const t=this._roomLqi(e.area);null!=t&&h.push(q`${t}`)}if(s.labelLight){const t=(d=ho(this._renderPlanHass,this._renderDevices,e,this._virtualLights)).length?{on:d.filter(e=>e.on).length,total:d.length}:null;if(t){const e=0===t.on?this._t("roomcard.light_off"):t.on===t.total?this._t("roomcard.light_on"):this._t("roomcard.light_partial",{on:t.on,total:t.total});h.push(q`${e}`)}}}var d;const p=!!e.area,u=!this._markup;return q`
this._labelDown(i,e,t.id)} - @pointermove=${i=>this._labelMove(i,e,t.id)} - @pointerup=${()=>this._labelUp(e)} - @pointercancel=${()=>this._labelUp(e)} - >${e.name||(this._markup?this._t("room.unnamed"):"")}${p?q`{t.stopPropagation(),this._clickRoom(e)}:G} - @pointerdown=${u?e=>e.stopPropagation():G}>`:G} - ${h.length?q`${h}`:G} - ${"plan"===this._mode?["tl","tr","bl","br"].map(i=>q`this._rlResizeDown(i,e,t.id)} - @pointermove=${e=>this._rlResizeMove(e)} - @pointerup=${()=>this._rlResizeUp()} - @pointercancel=${()=>this._rlResizeUp()}>`):G} -
`}get _measureAnchor(){return this._markup&&this._cursorPt?"draw"===this._tool&&this._path.length&&!this._contourClosed?this._path[this._path.length-1]:"split"===this._tool&&this._splitSel?.pts?.length?this._splitSel.pts[this._splitSel.pts.length-1]:null:null}_renderMeasureLabel(e){const t=this._measureAnchor,i=this._cursorPt,s=(i[0]-e.x)/e.w*100,n=(i[1]-e.y)/e.h*100,o=Es(t,i),a=Math.round(10*o)/10,r=function(e,t,i=.001){if(e.length<2||t.length<2)return!1;const s=Math.abs(t[0]-e[0]),n=Math.abs(t[1]-e[1]);if(!Number.isFinite(s)||!Number.isFinite(n)||Math.hypot(s,n)<=i)return!1;const o=Math.max(s,n,1);return s<=i*o||n<=i*o||Math.abs(s-n)<=i*o}(t,i,2e-4*this._gridPitch);return q`
- ${this._fmtLen(t,i)} · ${a}°
`}get _decorMeasure(){const e=this._decorDraft;if(!e||"decor"!==this._mode)return null;const[t,i]=e.a,[s,n]=e.b;if(Math.abs(t-s)<1e-6&&Math.abs(i-n)<1e-6)return null;const o=(t+s)/2,a=(i+n)/2;if("line"===e.kind){const t=Es(e.a,e.b);return{x:o,y:a,on45:Os(t),text:`${this._fmtLen(e.a,e.b)} · ${Math.round(10*t)/10}°`}}const r=this._fmtLen([t,i],[s,i]),l=this._fmtLen([s,i],[s,n]);if("ellipse"===e.kind){const e=this._fmtLen([0,0],[Math.abs(s-t)/2,0]),r=this._fmtLen([0,0],[0,Math.abs(n-i)/2]);return{x:o,y:a,on45:!1,text:Math.abs(Math.abs(s-t)-Math.abs(n-i))<1e-6?`R ${e}`:`Rx ${e} × Ry ${r}`}}return{x:o,y:a,on45:!1,text:`${r} × ${l} · ${rr(Gr(Math.abs(s-t),this._cellCm,this._gridPitch)*Gr(Math.abs(n-i),this._cellCm,this._gridPitch)/1e4,this._imperial)}`}}get _alignPoint(){if(this._markup){if("draw"===this._tool&&this._path.length&&!this._contourClosed&&this._cursorPt)return this._cursorPt;if("split"===this._tool&&this._splitSel?.pts?.length&&this._cursorPt)return this._cursorPt;if(this._drag?.id.startsWith("rl_")&&this._drag.moved){const e=this._drag.id.slice(3),t=this._spaceModel()?.rooms.find(t=>t.id===e);return t?(()=>{const e=this._labelPos(t,this._space);return[e.x,e.y]})():null}return null}if("devices"===this._mode&&this._drag?.moved){const e=this._devices.find(e=>e.id===this._drag.id);return e?(()=>{const t=this._pos(e);return[t.x,t.y]})():null}if("decor"===this._mode){if(this._decorDraft)return this._decorDraft.b;if(this._decorMove){const e=this._decorList.find(e=>e.id===this._decorMove.id);if(!e)return null;const t=Ov,i=this._decorH;return"line"===e.kind?[e.x1*t,e.y1*i]:[e.x*t,e.y*i]}return null}return null}_alignCandidates(){const e=[],t=this._spaceModel();if(this._markup){if(!t)return e;if(this._drag?.id.startsWith("rl_")){const i=this._drag.id.slice(3);for(const s of t.rooms){if(!s.name||s.id===i)continue;const t=this._labelPos(s,this._space);e.push([t.x,t.y])}return e}for(const i of t.rooms){const t=hi(i);if(t)for(const i of t)e.push(i)}if("draw"===this._tool)for(const t of this._path)e.push(t);if("split"===this._tool&&this._splitSel?.pts)for(const t of this._splitSel.pts)e.push(t);return e}if("devices"===this._mode){for(const t of this._devices){if(t.space!==this._space||t.id===this._drag?.id||"ha_disabled"===t.bindingStatus?.kind)continue;const i=this._pos(t);e.push([i.x,i.y])}return e}if("decor"===this._mode){const t=this._decorMove?.id;return e.push(...this._decorSnapGeometry(t).points),this._decorDraft&&e.push(this._decorDraft.a),e}return e}_renderAlignGuides(){const e=this._alignPoint;if(!e)return W``;const t=this._drag?.id.startsWith("rl_")?.5*this._gridPitch:.05*this._gridPitch,i=function(e,t,i){let s=null,n=null;for(const o of t)if(!(Math.abs(o[0]-e[0])<1e-6&&Math.abs(o[1]-e[1])<1e-6)){if(Math.abs(o[0]-e[0])<=i){const t=Math.abs(o[1]-e[1]);t>1e-6&&(!s||t1e-6&&(!n||t - ${i.map(t=>{const[i,o,a,r]="x"===t.axis?[t.at,t.from[1],t.at,e[1]+Math.sign(e[1]-t.from[1])*n]:[t.from[0],t.at,e[0]+Math.sign(e[0]-t.from[0])*n,t.at];return W` - `})} - `}_renderOpeningCenterTick(e){const t=(e.angle+90)*Math.PI/180,i=Dh(15,this._cellCm);return W``}_renderOpeningDimensionGuides(e){const t=e.labels.flatMap(e=>e.dimension?[e.dimension]:[]);if(!t.length)return W``;const i=this._cssPxToRender(4);return W``}_roomCenter(e){if(e.poly){const t=e.poly.length;return[e.poly.reduce((e,t)=>e+t[0],0)/t,e.poly.reduce((e,t)=>e+t[1],0)/t]}return[e.x+e.w/2,e.y+.1*Math.min(e.w,e.h)]}_openingAmt(e){const t=e.contact&&this._renderOpeningEntityAvailable(e.contact)?this._renderPlanHass.states[e.contact]?.state:null;return mi(e.type,t,!!e.invert)}_planEntityAvailable(e){return!!e&&(!ko(this._fullRegistryHass,e,wo(this._markers))&&"active"===this._bindingStatus("entity:"+e).kind)}_renderEntityAvailable(e){return!!e&&(!ko(this._renderPlanHass,e,wo(this._markers))&&(!!this._renderPlanHass.entities?.[e]&&!!this._renderPlanHass.states?.[e]))}_openingEntityAvailable(e){return function(e,t,i=An(e)){return!!t&&"active"===Bn(e,`entity:${t}`,i).kind}(this.hass,e,this._haRegistry)}_renderOpeningEntityAvailable(e){return t=this._renderPlanHass,!!(i=e)&&!!t?.states?.[i];var t,i}_renderOpeningPlacementPreview(){const e=this._openingPreview;if(!e)return W``;const t={type:e.type,length:e.renderedLength,angle:e.angle,amount:mi(e.type,null),flipH:e.flipH,flipV:e.flipV,base:"var(--hp-open)",tone:"var(--hp-open)",cellCm:this._cellCm,gridPitch:this._gridPitch,face:e.face},i="passage"===e.type?function(e,t){const i=Math.max(0,e.renderedLength)/2,s=Math.max(0,e.target.physicalHalfWidth),n=s+.18*Math.max(0,t),o=e=>({x1:e,y1:-n,x2:e,y2:n});return{rect:{x:-i,y:s?-s:0,width:2*i,height:2*s},boundaries:[o(-i),o(i)]}}(e,Dh(this._gridPitch,this._cellCm)):null;return W` - `}_renderOpenings(e){const t=this._openingsR;if(!t.length)return W``;const i=this._spaceModel();if(!i)return W``;const s=e.color,n=this._spaceWalls,o=this._openCuts(),a=this._openingWallIndexFor(i,o).value;return W`${t.map(e=>{if(e.orphanReason)return W`{t.stopPropagation(),this._editOpening(e)}}> - - ! - `;const t=this._openingAmt(e),i=t>0&&!!e.contact&&this._renderOpeningEntityAvailable(e.contact)?"var(--hp-open)":s,o="gate"===e.type?!e.flip_v:e.flip_v,r=e.partitionHost||n.length||"gate"===e.type?this._openingFace(e,a,!!o):{ox:0,oy:0,cm:0,side:-1},l={type:e.type,length:e.rlen,angle:e.angle,amount:t,flipH:!!e.flip_h,flipV:!!e.flip_v,base:s,tone:i,cellCm:this._cellCm,gridPitch:this._gridPitch,face:r},{half:c,outlineHalf:h,hitHalf:d}=y_(l),p=Dh(10,this._cellCm),u=Dh(12,this._cellCm);return W` - ${w_(l)} - - this._opClick(t,e)} - @pointerdown=${t=>this._opPointerDown(t,e)} - @pointermove=${t=>this._opPointerMove(t,e)} - @pointerup=${t=>this._opPointerUp(t,e)} - @pointercancel=${t=>this._opPointerUp(t,e)}> - `})}`}_renderOpeningLocks(e){const t=this._openingsR.filter(e=>!e.orphanReason&&("door"===e.type||"gate"===e.type)&&e.lock&&this._renderOpeningEntityAvailable(e.lock));if(!t.length)return q``;const i=this._spaceModel();if(!i)return q``;const s=this._openCuts(),n=this._openingWallIndexFor(i,s).value;return q`${t.map(t=>{const i=this._renderPlanHass.states[t.lock]?.state,s="locked"===i,o=s||["unlocked","open","opening","unlocking","locking"].includes(String(i)),a=(t.angle+90)*Math.PI/180,r="gate"===t.type?this._openingFace(t,n,!t.flip_v):null,l=Dh(16,this._cellCm),c=r?-l*r.side:l*(t.flip_v?-1:1),h=t.rx+Math.cos(a)*c,d=t.ry+Math.sin(a)*c,p=this._scenePoint([h,d]),u=(p[0]-e.x)/e.w*100,_=(p[1]-e.y)/e.h*100;return q`
{e.stopPropagation(),"view"===this._mode&&(this._openingInfo=t)}}> - -
`})}`}_lockAction(e,t){if(this._openingEntityAvailable(e)){if("unlock"===t){const t=this.hass?.states?.[e]?.attributes?.friendly_name||e;if(!confirm(this._t("confirm.unlock",{name:t})))return}this.hass?.callService?.("lock",t,{entity_id:e})}}_renderOpeningInfoCard(){const e=this._openingInfo,t="passage"!==e.type&&e.contact&&this._openingEntityAvailable(e.contact)?e.contact:null,i=("door"===e.type||"gate"===e.type)&&e.lock&&this._openingEntityAvailable(e.lock)?e.lock:null,s=t?this.hass.states[t]?.state:null,n=this._openingAmt(e),o=i?this.hass.states[i]?.state:null,a="door"===e.type?"opening.door":"gate"===e.type?"opening.gate":"passage"===e.type?"opening.passage":"opening.window",r="door"===e.type?"mdi:door":"gate"===e.type?"mdi:gate":"passage"===e.type?"mdi:arch":"mdi:window-closed-variant",l="gate"===e.type?n>0?"mdi:gate-open":"mdi:gate":n>0?"mdi:door-open":"mdi:door-closed",c=(e,t,i,s="")=>q`
${t}${i}
`;return q`this._openingInfo=null}> -
- ${t?c(l,this._t("opening.contact_label"),"unavailable"===s||null==s?this._t("opening.state_unknown"):this._t(n>0?"opening.open":"opening.closed"),n>0?"warn":"ok"):G} - ${i?c("locked"===o?"mdi:lock":"mdi:lock-open-variant",this._t("opening.lock_label"),"locked"===o?this._t("opening.locked"):["unlocked","open"].includes(String(o))?this._t("opening.unlocked"):this._t("opening.state_unknown"),"locked"===o?"ok":"warn"):G} - ${i&&("locked"===o||["unlocked","open"].includes(String(o)))?q``:i&&["locking","unlocking"].includes(String(o))?q``:G} - ${t||i?G:q`

${this._t("opening.no_entities")}

`} -
-
- - -
-
`}_renderOpeningDialog(){const e=this._openingDialog,t=this._spaceModel()?.partitions||[],i=e.id?this._curSpaceCfg?.openings?.find(t=>t.id===e.id):null,s={id:e.id||"preview",type:e.type,x:e.x/Ov,y:e.y/this._spaceH,angle:e.angle,length:i&&!e.lengthTouched?i.length:this._cmToUnits(Math.max(20,e.lengthCm))/Ov,...e.host?{host:e.host}:{}},n=ru(i,s),o=e.host?n?au(s,t,Ov,this._cellCm,this._gridPitch):ou(s,t,Ov,this._cellCm,this._gridPitch):null,a="does-not-fit-jamb"===o?.reason,r=e.host?t.find(t=>t.id===e.host.id):null,l=r?li(r.cm/2,this._imperial):"",c=!!e.host&&!o?.resolved&&!a,h="gate"===e.type?"mdi:gate":"window"===e.type?"mdi:window-closed-variant":"passage"===e.type?"mdi:arch":"mdi:door",d=(t,i)=>{const s="contact"===t?e.contact:e.lock,n="contact"===t?!!e.contactOpen:!!e.lockOpen,o="contact"===t?e.contactFilter||"":e.lockFilter||"",a=i.find(e=>e.value===s),r=a?.label||this.hass.states[s]?.attributes?.friendly_name||this._fullRegistryHass.entities[s]?.name||s,l=function(e,t,i=200){const s=t.trim().toLowerCase();return(s?e.filter(e=>e.label.toLowerCase().includes(s)||e.value.toLowerCase().includes(s)):e).slice(0,Math.max(0,i))}(i,o);return q` - - ${n?q`
- this._filterOpeningEntities(t,e.target.value)} /> -
- - ${l.map(e=>q` - `)} - ${l.length?G:q`
${this._t("marker.nothing_found")}
`} -
-
`:G}`};return q`this._openingDialog=null}> -
- ${e.host?q` -
- ${c||a?q``:G} - ${c?this._t("opening.partition_orphan"):a?this._t("opening.partition_jamb_margin",{distance:l}):this._t("opening.host_partition")} -
`:G} - - - - - - - - {const i=$v(t.target.value);null!=i&&(this._openingDialog={...e,lengthCm:i,lengthTouched:!0})}} /> - - ${"passage"===e.type&&(e.contact||e.lock)?q`
- - ${this._t("opening.passage_binding_warning")} -
`:G} - - ${"passage"!==e.type?q` - ${d("contact",this._contactCandidates())} - ${e.contact?q``:G}`:G} - - ${"door"===e.type||"gate"===e.type?q` - ${d("lock",this._lockCandidates())}`:G} - - ${"gate"!==e.type&&"passage"!==e.type?q``:G} - ${"passage"!==e.type?q``:G} -
- -
`}_gridLevels(){const e=this._stageEl,t=this._viewOr(this._baseVb()),i=e&&e.clientWidth&&t.w?e.clientWidth/t.w:1;return function(e,t,i=7){if(!(e>0&&t>0&&Number.isFinite(t)))return null;const s=ad.find(s=>e*s*t>=i);if(void 0===s)return null;const n=ad.find(e=>e>=5*s)??5*s;return{fine:s,coarse:n}}(this._gridPitch,i)}_renderMarkupDefs(e){const t=this._gridLevels();if(!t)return W``;const i=this._gridPitch*t.fine,s=this._gridPitch*t.coarse,n=this._gridPitch*t.fine*.14;return W` - - - - - - - - - - - - - - `}_renderPhysicalEditorLayer(){const e=this._spaceModel();if(!e)return W``;const t=this._gridPitch,i=this._viewOr(this._baseVb()),s=this._stageEl,n=s?.clientWidth?i.w/s.clientWidth:t/8,o=Math.max(.22*t,8*n),a=Math.max(o,12*n),r=e=>`M ${e.map(e=>`${e[0]} ${e[1]}`).join(" L ")} Z`,l=(e,t)=>this._physicalSel?.kind===e&&this._physicalSel.id===t||"column"===e&&this._duplicateColumnId===t,c=(e.room_drafts||[]).flatMap(e=>e.points.slice(0,-1).map((t,i)=>{const s=e.points[i+1],o=Number(e.segments[i]?.cm),a=Ml(Number.isFinite(o)?o:15,this._cellCm,this._gridPitch);return W`{this._physicalSel={kind:"draft",id:e.id,segment:i},this._registerPhysicalTap("draft",e.id,i)}}>`})),h=(e.partitions||[]).map(e=>{const t=hh(e.a,e.b,e.cm,this._cellCm,this._gridPitch);return t?W`this._physicalDown(t,"partition",e.id)} - @pointermove=${e=>this._physicalMove(e)} - @pointerup=${e=>this._physicalUp(e)}>`:W`this._physicalDown(t,"partition",e.id)} - @pointermove=${e=>this._physicalMove(e)} - @pointerup=${e=>this._physicalUp(e)}>`}),d=(e.wall_columns||[]).map(e=>{const t=dh(this._physicalRotate?.id===e.id?{...e,angle:this._physicalRotate.angle}:e,this._cellCm,this._gridPitch);return W`this._physicalDown(t,"column",e.id)} - @pointermove=${e=>this._physicalMove(e)} - @pointerup=${e=>this._physicalUp(e)}>`}),p=this._physicalDrag,u=(()=>{if(!p?.moved)return G;if("partition"===p.kind&&0===Number(p.base.cm)){const e=p.base;return W``}const e="partition"===p.kind?hh(p.base.a,p.base.b,p.base.cm,this._cellCm,this._gridPitch):dh(p.base,this._cellCm,this._gridPitch);return e?W``:G})(),_=(()=>{const i=this._physicalSel;if(!i)return G;if("draft"===i.kind){const t=e.room_drafts.find(e=>e.id===i.id);return t?W` - e.join(",")).join(" ")}> - ${t.points.map(e=>W``)} - `:G}if("partition"===i.kind){const t=e.partitions.find(e=>e.id===i.id);if(!t)return G;const s=hh(t.a,t.b,t.cm,this._cellCm,this._gridPitch),n=(t.a[0]+t.b[0])/2,a=(t.a[1]+t.b[1])/2;return W` - ${s?W``:W``} - - - - `}const s=e.wall_columns.find(e=>e.id===i.id);if(!s)return G;const l=this._physicalRotate?.id===s.id?{...s,angle:this._physicalRotate.angle}:s,c=dh(l,this._cellCm,this._gridPitch);if("square"!==l.shape)return W` - - - `;const h=Ml(l.cm,this._cellCm,this._gridPitch),d=oh(l.angle)*Math.PI/180,p=Math.sin(d),u=-Math.cos(d),_=[l.center[0]+p*h/2,l.center[1]+u*h/2],m=[_[0]+p*Math.max(t,24*n),_[1]+u*Math.max(t,24*n)];return W` - - - - this._physicalRotateDown(e,s)} - @pointermove=${e=>this._physicalRotateMove(e)} - @pointerup=${e=>this._physicalRotateUp(e)}> - `})();return W`${c}${h}${d}${u}${_}`}_renderHiddenWallDiagnosticOverlay(){if(!this._markup)return W``;const e=this._hiddenWallDiagnosticSnapshot().value;if(!e.segments.length)return W``;const t=Ml(5,this._cellCm,this._gridPitch);return W``}_renderPlanSnapOverlay(){if(!this._markup)return W``;const e=this._planSnapGeometrySnapshot().value,t=this._activePlanSnapCandidate,i=new Set(this._activePlanSnapConflicts.map(e=>e.key)),s=Ml(5,this._cellCm,this._gridPitch),n=Ml(10,this._cellCm,this._gridPitch);return W``}_syncPlanSnapActiveMarker(e){const t=this.renderRoot?.querySelector('[data-hp="plan-snap-active-marker"]');if(t){if(t.setAttribute("class",`plan-snap-node${e?" active":""}${"line"===e?.kind?" dynamic":""}`),t.setAttribute("data-active",e?"true":"false"),t.setAttribute("visibility",e?"visible":"hidden"),!e)return t.removeAttribute("data-kind"),void t.removeAttribute("data-key");t.setAttribute("data-kind",e.kind),t.setAttribute("data-key",e.key),t.setAttribute("cx",String(e.point[0])),t.setAttribute("cy",String(e.point[1])),t.setAttribute("r",String(Ml(10,this._cellCm,this._gridPitch)))}}_syncPlanSnapConflictMarkers(e){const t=new Set(e.map(e=>e.key));for(const e of this.renderRoot?.querySelectorAll('.plan-snap-node[data-kind="endpoint"]')||[])e.setAttribute("class","plan-snap-node"+(t.has(e.dataset.key||"")?" conflict":""))}_planSnapPhysicalSegment(e){let t=0;const i=this._spaceModel();if(!i)return null;if("partition"===e.sourceKind)t=Number(i.partitions.find(t=>t.id===e.sourceId)?.cm)||0;else if("draft"===e.sourceKind){const s=/^(.*):(\d+)$/.exec(e.sourceId),n=s?i.room_drafts.find(e=>e.id===s[1]):null;t=n&&s&&Number(n.segments[Number(s[2])]?.cm)||0}else t=$c(i.rooms,this._spaceWalls,this._openCuts(),[e.a[0],e.a[1],e.b[0],e.b[1]],this._wallKeyPitch,this._cellCm,this._gridPitch,Ov);return t>0?{a:[...e.a],b:[...e.b],halfDepth:Ml(t,this._cellCm,this._gridPitch)/2}:null}_drawPreviewJoinPatchD(e,t){if(e.length<2)return"";const i=[];for(let s=0;s+10&&i.push({a:e[s],b:e[s+1],halfDepth:t[s]});if(!i.length)return"";const s=2e-4*this._gridPitch,n=this._planSnapGeometrySnapshot().value.segments.filter(t=>e.some(e=>Hs(e,[t.a[0],t.a[1],t.b[0],t.b[1]])<=s)).map(e=>this._planSnapPhysicalSegment(e)).filter(e=>!!e);return ec([...i,...n],s).map(e=>`M ${e.map(e=>`${e[0]} ${e[1]}`).join(" L ")} Z`).join(" ")}_renderMarkupLayer(e){const t=this._openCuts(),i=this._thickWallCuts(),s=t.concat(i),n=s.length?Ds(this._segments,s,.02*this._gridPitch):this._segments,o=this._path,a=this._gridPitch,r=this._viewOr(this._baseVb()),l="draw"===this._tool?this._drawWallCm:null,c=(()=>"draw"===this._tool&&o.length&&null!=l?this._contourClosed?o:this._cursorPt?[...o,this._cursorPt]:o.length>=2?o:null:null)(),h=c?Iu(c.length-1,this._contourClosed?[...this._draftSegmentCms,this._closingWallCm??void 0]:this._draftSegmentCms,l,vl).map(e=>Ml(e,this._cellCm,this._gridPitch)/2):[],d=c?tc(c,Ml(l,this._cellCm,this._gridPitch)/2,this._contourClosed,h):"",p=c?this._drawPreviewJoinPatchD(c,h):"",u=this._wallFaceBatch?.candidates[this._wallFaceBatch.index]?.repair||this._wallRepairDiagnostic;return W` - ${this._gridLevels()?W``:G} - ${n.map(e=>W``)} - ${this._renderPhysicalEditorLayer()} - ${"column"===this._tool&&this._cursorPt&&this._drawWallCm?W``M ${dh({shape:"square",center:this._cursorPt,cm:this._drawWallCm},this._cellCm,this._gridPitch).map(e=>`${e[0]} ${e[1]}`).join(" L ")} Z`)()}>`:G} - ${d?W` - `:G} - ${c&&0===l?W`e.join(",")).join(" ")}>`:G} - ${p?W` - `:G} - ${u?W``:G} - ${"split"===this._tool&&this._splitSel?.pts?.length?W`${this._splitSel.pts.length>1?W``:G} - ${this._splitSel.pts.map((e,t)=>W``)} - ${this._cursorPt?W``:G}`:G} - `}_renderActiveChainInk(){const e=this._path,t=this._gridPitch;return W` - ${e.length>1?W``:G} - ${e.length&&this._cursorPt&&"draw"===this._tool&&!this._contourClosed?W` - ${this._activePlanSnapCandidate||this._activePlanSnapConflicts.length?G:W``}`:G} - ${e.map((e,i)=>W``)} - `}_renderPartitionDeleteDialog(){const e=this._partitionDeleteDialog,t="mi"===this.hass?.config?.unit_system?.length;return q`this._partitionDeleteDialog=null}> -
-

${this._t("confirm.delete_partition_openings_body",{count:e.openings.length})}

-
    - ${e.openings.map(e=>q`
  • ${this._t("confirm.delete_partition_openings_item",{type:this._t(`opening.${e.type}`),length:li(e.length*Ov/this._gridPitch*this._cellCm,t)})}
  • `)} -
-
-
- - - -
-
`}_renderRoomDeleteDialog(){const e=this._roomDeleteDialog;return q`this._roomDeleteDialog=null}> -
-

${this._t("confirm.delete_room_body")}

-
-
- - - - -
-
`}_renderPhysicalDialog(){const e=this._physicalDialog,t="column"===e.kind;return q`this._physicalDialog=null}> -
- ${t?q` - `:G} - -
this._physicalDialog={...e,cm:t.target.value}} /> - ${this._t(this._imperial?"wallthick.unit_in":"wallthick.unit_cm")}
- ${t&&"square"===e.shape?q` - - this._physicalDialog={...e,angle:t.target.value}} />`:G} - ${e.length?q`
${this._t("physical.length")}: ${e.length}
`:G} -
- -
`}_renderMarkupBar(){const e=this._geometryHistory.undoName,t=this._geometryHistory.redoName,i=e?this._t("history.undo_named",{name:e}):this._t("history.undo_empty"),s=t?this._t("history.redo_named",{name:t}):this._t("history.redo_empty");return q`
-
- - - - - - - - - - ${this._editorToolbarGroups.map(e=>this._renderEditorGroupLauncher(e))} - - - - -
-
- -
-
`}_renderDevicesBar(){return q`
-
- - - - ${this._editorToolbarGroups.map(e=>this._renderEditorGroupLauncher(e))} -
-
- -
-
`}_renderDeviceInbox(){const e=this._deviceInbox,t=this._deviceInboxRows(),i=Object.fromEntries(["on_plan","available","hidden","readd"].map(e=>[e,t.filter(t=>t.category===e).length])),s=function(e,t,i,s=!1){const n=i.trim().toLocaleLowerCase();return e.filter(e=>e.category===t&&(!s||e.isNew)&&(!n||e.searchText.includes(n)))}(t,e.tab,e.search,"on_plan"===e.tab&&e.onlyNew),n=s.slice(0,e.limit),o=e=>this._t(`device_inbox.tab_${e}`),a=`device_inbox.empty_${e.tab}`;return q`this._deviceInbox=null}> -
-
- this._deviceInbox={...e,search:t.target.value,limit:100}} /> - -
-
- ${["on_plan","available","hidden","readd"].map(t=>q` - `)} -
-
- ${"on_plan"===e.tab?q``:G} - ${"available"===e.tab?q``:G} - -
-
- ${n.length?n.map(e=>{const t="on_plan"===e.category?e.canFind?q``:q``:"hidden"===e.category?q``:q``,i="active"===e.status.kind?"":this._t(`device_inbox.status_${e.status.kind}`);return q`
- -
-
${e.name} - ${e.isNew?q`${this._t("device_inbox.new")}`:G} -
-
- ${[e.model,e.integration,e.spaceName,e.areaName].filter(Boolean).join(" · ")} -
-
- ${this._t(`device_inbox.reason_${e.reason}`)} - ${i?q`${i}`:G} -
- ${e.binding} -
-
- ${t} - ${e.canEdit||e.canHide||"available"===e.category||"hidden"===e.category||this._bindingHasHaPage(e.binding)?q` -
- - - -
- ${e.canEdit&&("on_plan"!==e.category||e.canFind)?q``:G} - ${e.canHide?q``:G} - ${"available"===e.category?q``:G} - ${"hidden"===e.category?q``:G} - ${this._bindingHasHaPage(e.binding)?q``:G} -
-
`:G} -
-
`}):q`
${this._t(a)}
`} -
- ${s.length>n.length?q``:G} -
-
- -
-
`}_cardEntities(e){const t=this._planHass,i=[],s=new Set,n=e=>{if(!e||s.has(e)||!t.states[e])return;const n=t.entities[e];if("config"===n?.entity_category||"diagnostic"===n?.entity_category)return;s.add(e);const o=e.split(".")[0];["light","switch","fan","humidifier","siren","input_boolean"].includes(o)?i.push({eid:e,kind:"toggle"}):["cover","valve","lock","climate","media_player","vacuum","water_heater"].includes(o)?i.push({eid:e,kind:"open"}):["sensor","binary_sensor","number","select"].includes(o)&&i.push({eid:e,kind:"value"})};for(const i of ho(t,this._devices,null,this._virtualLights))if(i.device.id===e.id)for(const e of[...i.serviceEids,...i.stateEids])n(e);e.primary&&n(e.primary);for(const t of e.entities)n(t);return i.slice(0,12)}_cardToggle(e){const t=e.split(".")[0];"lock"!==t&&"alarm_control_panel"!==t&&this._planEntityAvailable(e)&&this.hass.callService("homeassistant","toggle",{entity_id:e}).catch(e=>this._showToast(this._t("toast.error",{err:this._errText(e)})))}_renderInfoCard(){const e=this._infoCard,t=e.primary?this.hass.states[e.primary]:void 0,i=t?Xi(this.hass,e.primary)?.text??t.state:null,s=(e.controls??e.marker?.controls??[]).filter(Ms).filter(e=>this._planEntityAvailable(e));return q` -
- ${(()=>{const t=this._cardEntities(e);return t.length?q`
- ${t.map(({eid:e,kind:t})=>{const i=this.hass.states[e],s=this.hass.entities[e]?.name||i?.attributes?.friendly_name||e,n=i?Xi(this.hass,e)?.text??i.state:"",o="on"===i?.state||["open","unlocked","playing","cleaning"].includes(i?.state);return q`
- - ${s} - ${"toggle"===t?q``:"open"===t?q``:q`${n}`} -
`})} -
`:G})()} - ${e.model?q`
${this._t("info.model")}${e.model}
`:G} - ${i&&!this._cardEntities(e).length?q`
${this._t("info.state")}${i}
`:G} - ${Hi(e.link)?q`
${this._t("info.link")} - ${e.link}
`:G} - ${e.description?q`
${e.description}
`:G} - ${e.pdfs&&e.pdfs.length?q`
${this._t("info.manuals")} - ${e.pdfs.map(e=>q` - ${e.name}`)}
`:G} - ${s.length?q`
${this._t("info.controls")} - - ${s.map(e=>{const t=this.hass.states[e],i="on"===t?.state;return q` - - ${t?.attributes?.friendly_name||e}`})} -
`:G} - ${e.model||i||e.link||e.description||e.pdfs&&e.pdfs.length||s.length?G:q`
${this._t("info.none")}
`} -
-
- - ${e.primary?q``:G} - -
-
`}_markerValueBadgeFields(e){return(t={touched:e.valueBadgeTouched,originalHas:e.originalHasValueBadge,original:e.originalValueBadge,enabled:e.valueBadgeEnabled,source:e.valueBadgeSource,position:e.valueBadgePosition}).touched?{value_badge:{enabled:t.enabled,source:t.source,position:t.position}}:t.originalHas?{value_badge:t.original}:{};var t}_markerDraft(e){if("ha"===e.bindingMode&&(!e.binding||"virtual"===e.binding))return null;const t=js(e.room),i=Fi(e.binding,e.devId,()=>"__hp_device_preview__"),s=this._markers.find(t=>t.id===i||t.id===e.devId),n=Zn(e.binding,e.controls,this._bindingEntities(e.binding)),o=this._effectiveStoredTapAction(e),a={id:i,binding:e.binding,name:e.name.trim()||null,icon:e.icon||null,display:"badge"!==e.display?e.display:null,ripple_color:"icon_ripple"===e.display&&e.rippleColor?e.rippleColor:null,ripple_size:"icon_ripple"===e.display&&1.5!==e.rippleSize?e.rippleSize:null,size:1!==e.size?e.size:null,angle:e.angle||null,...this._markerTapActionFields(e),...this._markerToggleEntityFields(e),tap_target:"run"===o&&e.tapTarget||null,tap_confirm:!!e.tapConfirm||null,controls:n.length?n:null,...this._markerLightFields(e),...this._markerValueBadgeFields(e),use_climate_temp:!!e.useClimateTemp||null,glow_radius_cm:(()=>{const t=$v(e.glowRadius);return null==t||t<=0?null:Math.round(this._imperial?30.48*t:100*t)})(),model:e.model.trim()||null,link:e.link.trim()||null,description:e.description.trim()||null,pdfs:e.pdfs,hidden:e.hideFromPlan,vacuum:s?.vacuum||null};return("virtual"===e.binding||e.room)&&(a.space=t?.space||("virtual"===e.binding?this._space:null),a.area=t?.area||null,a.room_id=t?.roomId||null),a}_markerPreviewDevice(e){const t=this._markerDraft(e);if(!t)return null;const i=`${this._haRegistry.revision}\n${Jd(this._markers)}\n${JSON.stringify(t)}`;if(this._markerPreviewMemo?.key===i)return this._markerPreviewMemo.device;const s=function(e){const{marker:t,siblingMarkers:i=[],...s}=e;return So({...s,markers:[...i.filter(e=>e.id!==t.id),t]}).find(e=>e.id===t.id)||null}({hass:this.hass,registry:this._haRegistry,areaToSpace:Object.fromEntries(Object.entries(this._areaToSpace).map(([e,t])=>[e,t.space])),marker:t,siblingMarkers:this._markers,settings:this._settings,excluded:this._excluded,showAll:this._showAll,firstSpaceId:this._model[0]?.id||this._space,loc:e=>this._t(e),iconRules:this._iconRules});return this._markerPreviewMemo={key:i,device:s},s}_markerPreviewDevices(e){const t=this._markerPreviewDevicesMemo;if(t?.base===this._devices&&t.preview===e)return t.devices;const i=[...this._devices.filter(t=>t.id!==e.id),e];return this._markerPreviewDevicesMemo={base:this._devices,preview:e,devices:i},i}_toggleIntent(e,t=this._devices){return Xp({hass:this._planHass,registryHass:this._fullRegistryHass,devices:t,device:e,virtualLights:this._virtualLights})}_toggleIntentForDialog(e){const t=this._markerPreviewDevice(e);if(!t)return null;const i=this._markerPreviewDevices(t);return this._toggleIntent(t,i)}_toggleStateText(e,t){const i=this._planHass?.states?.[e]||this.hass?.states?.[e];try{return i&&"function"==typeof this.hass?.formatEntityState?this.hass.formatEntityState(i):t}catch{return t}}_toggleConfirmationStateText(e){const t=String(e.state||"unknown"),i=e.entityId?this._toggleStateText(e.entityId,t):t;if(i.trim().toLocaleLowerCase()!==t.trim().toLocaleLowerCase())return i;const s={on:"confirm.state_on",off:"confirm.state_off",open:"confirm.state_open",closed:"confirm.state_closed",opening:"confirm.state_opening",closing:"confirm.state_closing",unknown:"confirm.state_unknown"}[t];return s?this._t(s):t.replaceAll("_"," ").replaceAll("-"," ")}_toggleConfirmationLines(e){const t={"turn-on":"confirm.state_on","turn-off":"confirm.state_off",open:"confirm.state_open",close:"confirm.state_closed",stop:"confirm.state_stopped"};return function(e,t){if(!Qp(e)||!e.nextEffect||!e.targets.length)return[];const i="group"===e.kind,s=i?e.targets.filter(e=>"on"===e.state).length:0,n=i?0===s?t.groupAllOff():t.groupCurrent(s,e.targets.length):t.state(e.targets[0]);let o;return o="toggle"===e.nextEffect?t.expectedByHa():i&&"turn-on"===e.nextEffect?t.groupAllOn():i&&"turn-off"===e.nextEffect?t.groupAllOff():t.effect(e.nextEffect),[t.current(n),t.expected(o),...e.skippedTargets.length?[t.unavailable(e.skippedTargets.length)]:[]]}(e,{state:e=>this._toggleConfirmationStateText(e),current:e=>this._t("confirm.current_state",{state:e}),expected:e=>this._t("confirm.expected_state",{state:e}),groupCurrent:(e,t)=>this._t("confirm.group_current",{on:e,total:t}),groupAllOn:()=>this._t("confirm.group_all_on"),groupAllOff:()=>this._t("confirm.group_all_off"),unavailable:e=>this._t("confirm.unavailable_targets",{count:e}),effect:e=>this._t(t[e]),expectedByHa:()=>this._t("confirm.expected_by_ha")})}_toggleHintLines(e){if(!e)return[];const t=e=>this._t(`marker.toggle_effect_${e.replace("-","_")}`),i=e=>this._t(`marker.toggle_skip_${e.replace("-","_")}`);return function(e,t){const i=e.targets[0]||e.skippedTargets[0],s=[];return"group"===e.kind?e.targets.length&&s.push(t.group(e.targets)):i&&s.push(t.single(i)),e.nextEffect&&e.targets.length&&s.push("group"===e.kind?t.groupCurrentNext(e.targets,e.nextEffect):t.currentNext(e.targets[0],e.nextEffect)),e.skippedTargets.length&&s.push(t.skipped(e.skippedTargets)),!e.command&&e.noneReason&&s.push(t.none(e.noneReason)),s}(e,{single:e=>{if("via"in e&&"virtual-light"===e.via)return this._t("marker.virtual_light_target",{name:e.name});const t=e.entityId||("ref"in e?e.ref:""),i=e.name||t;return this._t("marker.toggle_hint_single",{name:i,id:t})},group:e=>this._t("marker.toggle_hint_group",{count:e.length,names:e.map(e=>`${e.name} (${e.entityId})`).join(", ")}),currentNext:(e,i)=>"virtual-light"===e.via?this._t("marker.virtual_light_current",{state:this._t("on"===e.state?"marker.virtual_light_state_on":"marker.virtual_light_state_off"),effect:t(i)}):this._t("marker.toggle_hint_current",{state:this._toggleStateText(e.entityId,e.state),effect:t(i)}),groupCurrentNext:(e,i)=>this._t("marker.toggle_hint_group_current",{on:e.filter(e=>"on"===e.state).length,count:e.length,effect:t(i)}),skipped:e=>this._t("marker.toggle_hint_skipped",{count:e.length,targets:e.map(e=>{const t=e.entityId||e.ref;return`${e.name||t} (${t}: ${i(e.reason)})`}).join(", ")}),none:e=>this._t(`marker.toggle_none_${e.replaceAll("-","_")}`)})}_effectiveStoredTapAction(e,t){return e.tapActionTouched?e.tapAction:Vp(e.originalHasTapAction?e.originalTapAction:null,t)}_effectiveMarkerTapAction(e,t=this._markerPreviewDevice(e)){return this._effectiveStoredTapAction(e,t?.primary?.split(".")[0])}_announceToggleDraft(e){const t=this._markerPreviewDevice(e),i=e.tapActionTouched?e:{...e,tapAction:this._effectiveMarkerTapAction(e,t)},s="toggle"===i.tapAction?this._toggleHintLines(this._toggleIntentForDialog(i)).join(" "):"";return{...i,tapHintAnnouncement:s}}_valueBadgeForBinding(e,t){const i={...e,binding:t,valueBadgeEnabled:!1,valueBadgeSource:null,valueBadgeTouched:!0},s=this._markerPreviewDevice(i);if(!s)return{valueBadgeEnabled:!1,valueBadgeSource:null,valueBadgeTouched:!0};const n=[...this._devices.filter(e=>e.id!==s.id),s],o=qo(this._planHass,s,n),a=Wo(this._planHass,s,o);return{valueBadgeEnabled:e.valueBadgeEnabled&&!!a,valueBadgeSource:a,valueBadgeTouched:!0}}_markerSpatialSource(e){const t=this._markerPreviewDevice(e);if(!t)return null;const i={...t,hidden:!1};return po(ho(this._planHass,[...this._devices.filter(e=>e.id!==i.id),i],null,this._virtualLights).filter(e=>e.device.id===i.id))}_markerAutoHasSpatialSource(e){const t={...e,lightRole:"auto",lightRoleTouched:!0},i=this._markerPreviewDevice(t);return!!i&&lo(this._planHass,{...i})}_setMarkerLightRole(e){const t=this._markerDialog;t&&(this._markerDialog={...t,lightRole:e,lightRoleTouched:!0})}_controlRefInfo(e){if(!e.startsWith("marker:"))return{label:this.hass.states[e]?.attributes?.friendly_name||e,sub:e,icon:e.startsWith("light.")?"mdi:lightbulb":"mdi:toggle-switch",warning:!this._planEntityAvailable(e)};const t=e.slice(7),i=this._markers.find(e=>e.id===t),s=this._devices.find(e=>e.id===t);if(!i||i.removed||!0!==i.is_light)return{label:this._t("marker.control_missing_label"),sub:`${this._t("marker.control_broken")} (${e})`,icon:"mdi:alert-outline",warning:!0};const n=this._serverCfg?.spaces.find(e=>e.id===(s?.space||i.space)),o=!!s&&eo(s).length>0;return{label:i.name||s?.name||t,sub:[n?.title||s?.space||i.space,i.room_id||s?.area,o?"":this._t("marker.control_passive")].filter(Boolean).join(" · "),icon:i.icon||s?.icon||"mdi:lightbulb-outline",warning:!0===i.hidden||!0===s?.hidden}}_valueBadgeCandidateLabel(e){const t=e.source;if("derived_lqi"===t.kind)return this._t("marker.value_badge_lqi");if("derived_marker_state"===t.kind)return this._t("marker.value_badge_marker_state",{name:e.label});if("entity_state"===t.kind)return this._t("marker.value_badge_state",{name:e.label});const i=this.hass.states[t.entity_id]?.attributes?.friendly_name||this._fullRegistryHass.entities[t.entity_id]?.name||t.entity_id;return this._t(`marker.value_badge_attr_${t.attribute}`,{name:i})}_controlCandidates(e){const t=this._markerDraft(e)?.id||e.devId||"",i=[],s=new Set;for(const e of this._markers){if(e.id===t||e.removed||e.hidden||!0!==e.is_light)continue;if("virtual"!==e.binding&&"active"!==this._bindingStatus(e.binding).kind)continue;const n=this._devices.find(t=>t.id===e.id);if(!n||n.hidden)continue;const o=this._controlRefInfo(`marker:${e.id}`);for(const e of eo(n))s.add(e);i.push({value:`marker:${e.id}`,label:o.label,sub:o.sub,icon:o.icon,search:`${o.label} ${o.sub} ${e.id} ${eo(n).join(" ")}`.toLowerCase()})}const n=Object.keys(this.hass.states||{}).filter(t=>Ms(t)&&!s.has(t)&&Xn(e.binding,[t],this._bindingEntities(e.binding)).length>0&&this._planEntityAvailable(t)).map(e=>{const t=this._controlRefInfo(e);return{value:e,label:t.label,sub:e,icon:t.icon,search:`${t.label} ${e}`.toLowerCase()}}),o=e.controlsFilter.trim().toLowerCase();return[...i,...n].filter(t=>!e.controls.includes(t.value)&&(!o||t.search.includes(o))).slice(0,12).map(({search:e,...t})=>t)}_addControlRef(e,t){if(t.startsWith("marker:")){const i=this._markerDraft(e)?.id||e.devId||"";if(!i)return void this._showToast(this._t("toast.marker_binding_required"));const s=t.slice(7),n=this._markerDraft(e);if(function(e,t,i){if(!t||t===i)return!0;const s=new Map;for(const t of e)s.set(t.id,(t.controls||[]).filter(e=>"string"==typeof e&&e.startsWith("marker:")).map(e=>e.slice(7)));const n=new Set,o=[i];for(;o.length;){const e=o.pop();if(e===t)return!0;n.has(e)||(n.add(e),o.push(...s.get(e)||[]))}return!1}(n?[...this._markers.filter(e=>e.id!==i),n]:this._markers,i,s))return void this._showToast(this._t("toast.marker_control_cycle"))}this._markerDialog=this._announceToggleDraft({...e,controls:[...e.controls,t],controlsFilter:""})}_setMarkerGlowMode(e){const t=this._markerDialog;if(!t)return;if("auto"===e)return void(this._markerDialog={...t,glowMode:e,glowTouched:!0});const i=this._markerSpatialSource(t),s=xs(i?this._planHass.states[i.eid]:void 0,null,this._fillColors.glow_light.c),n=!t.glowColorDrafted,o="fixed"===e&&!t.glowBrightnessDrafted;this._markerDialog={...t,glowMode:e,glowColor:n?s.c:t.glowColor,glowBrightness:o?Math.max(1,Math.round(100*s.bri)):t.glowBrightness,glowColorDrafted:!0,glowBrightnessDrafted:t.glowBrightnessDrafted||"fixed"===e,glowTouched:!0}}_renderMarkerDialog(){const e=this._markerDialog,t="virtual"===e.bindingMode,i=this._bindingCandidates();this._bindingEntities(e.binding);const s=t?null:this._bindingStatus(e.binding),n=!t&&this._bindingHasHaPage(e.binding),o=this._markerPreviewDevice(e),a=this._effectiveMarkerTapAction(e,o),r=o?cs(this._serverCfg?.spaces.find(e=>e.id===o.space)):null,l=o?this._markerPreviewDevices(o):this._devices,c="toggle"===a&&o?this._toggleIntent(o,l):null,h=this._toggleHintLines(c),d=o?N_(this._planHass,o,{liveStates:!1!==this._config?.live_states,showTemperature:!1!==this._config?.show_temperature,showSignal:r?.showLqi??!1!==this._config?.show_signal,designPreview:!0,activityRuntime:this._activityRt.get(o.id),lightDevices:l,registryHass:this._fullRegistryHass,reducedMotion:this._reducedMotion}):null,p=o?qo(this._planHass,o,l):[],u=o?Wo(this._planHass,o,p):null,_=e.valueBadgeTouched?e.valueBadgeEnabled:!!d?.valueBadge,m=e.valueBadgeTouched?e.valueBadgeSource:d?.valueBadge?.source||e.valueBadgeSource,g=e.valueBadgeTouched?e.valueBadgePosition:d?.valueBadge?.position||e.valueBadgePosition,f=Ao(m),v=!!m&&!p.some(e=>e.key===f),b=p.find(e=>e.key===f),y=d?.valueSource?d.valueSource.attribute?`attr:${d.valueSource.eid}:${d.valueSource.attribute}`:`state:${d.valueSource.eid}`:"",w=this._markerAutoHasSpatialSource(e),k=!!o&&function(e,t){return io(e,t).some(e=>!!e.eid)}(this._planHass,{...o}),x=function(e,t,i,s){const n="always"===e||"auto"===e&&t,o=n&&!i;return{sourceExists:n,fromSourceEnabled:n&&i,manualEnabled:n,radiusEnabled:n,passive:o,effectiveMode:o&&"auto"===s?"fixed":s}}(e.lightRole,w,k,e.glowMode),$=!x.sourceExists,S=!x.fromSourceEnabled,M=x.passive,C=x.effectiveMode,D="never"===e.lightRole?this._t("marker.glow_disabled_never"):"auto"!==e.lightRole||w?M?this._t("marker.glow_passive_hint"):this._t("marker.glow_disabled_no_entity"):this._t("marker.glow_disabled_auto"),P=o?eo(o):[],T=o&&to(o)||"",I=!!e.lightEntity&&!P.includes(e.lightEntity),R=o?jp(o):[],A=!!e.toggleEntity&&!R.includes(e.toggleEntity),z="toggle"===a?this._toggleIntentForDialog({...e,toggleEntity:"",toggleEntityTouched:!0}):null,F=z?[...z.targets,...z.skippedTargets].map(e=>e.entityId||("ref"in e?e.ref:"")).filter(Boolean).join(", "):"",N=(()=>{if(t)return null;const s=i.find(t=>t.value===e.binding);if(s)return s.label;const[n,o]=e.binding.split(":");return"device"===n?this._fullRegistryHass.devices[o]?.name_by_user||this._fullRegistryHass.devices[o]?.name||o:this._fullRegistryHass.entities[o]?.name||this.hass.states[o]?.attributes?.friendly_name||o})();return q` -
- ${"ha_disabled"===s?.kind?q`
- - ${this._t(`marker.ha_disabled_${s.reason}`)} - ${n?q``:G} -
`:"unverified"===s?.kind&&e.binding?q`
- - ${this._t("marker.ha_registry_limited")} -
`:G} - - this._markerDialog={...e,name:t.target.value}} /> - - -
- -
- - -
- ${"ha"===e.bindingMode?q` - ${e.bindingOpen?q`
- this._markerDialog={...e,bindingFilter:t.target.value}} /> -
- ${i.map(t=>q`
{const i={...e,binding:t.value,bindingOpen:!1,controls:Zn(t.value,e.controls,this._bindingEntities(t.value)),autoIcon:this._autoIconForBinding(t.value)};this._markerDialog=this._announceToggleDraft({...i,...this._valueBadgeForBinding(i,t.value)})}}> - ${t.label}${t.sub} -
`)} - ${i.length?G:q`
${this._t("marker.nothing_found")}
`} -
-
`:G}`:G} -
- - - - - ${this._renderVacSection(e)} - - - - ${"toggle"===a&&(R.length>1||A)?q`
-
- - ${this._help("marker.toggle_entity.help")} -
- - ${A?q`

- - ${this._t("marker.toggle_entity_missing",{entity:e.toggleEntity,fallback:F||this._t("marker.toggle_entity_none")})} -

`:G} -
`:G} - ${"toggle"===a?q`
- ${h.map(e=>q`
${e}
`)} -
-
${e.tapHintAnnouncement}
`:G} - ${"run"===a?(()=>{const t=e.runFilter.trim().toLowerCase(),i=this._runCandidates().filter(e=>!t||e.label.toLowerCase().includes(t)||e.value.includes(t)),s=e.tapTarget?this._runCandidates().find(t=>t.value===e.tapTarget):null;return q` - - ${e.tapTarget&&!s?q`
${this._t("marker.run_target_gone",{id:e.tapTarget})}
`:G} - {e.target.select()}} - @input=${t=>this._markerDialog={...e,runFilter:t.target.value,tapTarget:""}} /> - ${s?G:q`
- ${i.slice(0,40).map(t=>q`
this._markerDialog={...e,tapTarget:t.value,runFilter:""}}> - ${t.label}${t.sub} -
`)} - ${i.length?G:q`
${this._t("marker.nothing_found")}
`} -
`}`})():G} - ${"run"===a||"toggle"===a?q``:G} - - -
${this._t("marker.controls_hint")}
- ${e.controls.length?q`
- ${e.controls.map(t=>{const i=this._controlRefInfo(t);return q` - ${i.label} - this._markerDialog=this._announceToggleDraft({...e,controls:e.controls.filter(e=>e!==t)})}> - `})} -
`:G} - this._markerDialog={...e,controlsFilter:t.target.value}} /> - ${e.controlsFilter.trim()?q`
- ${this._controlCandidates(e).map(t=>q``)} -
`:G} - - ${this._bindingHasClimate(e.binding)?q``:G} -
- ${this._t("marker.light_role_label")}${this._help("marker.light_role.help")} -
- - - -
-
- - ${"always"===e.lightRole&&(P.length>1||I)?q`
-
- - ${this._help("marker.light_entity.help")} -
- - ${I?q`

- - ${this._t("marker.light_entity_missing",{entity:e.lightEntity,fallback:T||"—"})} -

`:G} -
`:G} - -
- ${this._t("marker.glow_color_label")}${this._help("marker.glow_mode.help")} -
- - - -
- ${"auto"!==C?q`
- {this._markerDialog={...e,glowMode:C,glowColor:t.detail.color,glowColorDrafted:!0,glowTouched:!0}}}> - ${"fixed"===C?q` - ${this._t("marker.glow_brightness")} - ${this._rangeInput(1,100,1,e.glowBrightness,t=>{this._markerDialog={...e,glowMode:C,glowBrightness:t,glowBrightnessDrafted:!0,glowTouched:!0}},$,this._t("marker.glow_brightness"))} - ${Math.round(e.glowBrightness)}%`:G} -
`:G} -
-
-
- - ${this._help("marker.glow_radius.help")} -
-
- this._markerDialog={...e,glowRadius:t.target.value}} /> - ${this._imperial?this._t("gs.unit_ft"):this._t("gs.unit_m")} -
-
- ${$||M?q`

- ${D} -

`:G} - - - ${customElements.get("ha-icon-picker")?q`{const i=t.detail.value||"";(e.icon||i!==e.autoIcon)&&(this._markerDialog={...e,icon:i})}}>`:q`this._markerDialog={...e,icon:t.target.value}} />`} - ${!e.icon&&e.autoIcon?q`

- ${this._t("marker.icon_auto",{icon:e.autoIcon})} -

`:G} - - - -

${this._t(yv[e.display])}

- ${"static_icon"===e.display&&this._bindingHasAlarm(e.binding)?q`
- - ${this._t("marker.static_alarm_warning")} -
`:G} -
- ${this._t("marker.value_badge_title")}${this._help("marker.value_badge.help")} - - ${"static_icon"===e.display?q`

- ${this._t("marker.value_badge_static")} -

`:p.length||e.valueBadgeSource?G:q`

- ${this._t("marker.value_badge_empty")} -

`} - ${_?q` -
- - ${this._help("marker.value_badge_source.help")} -
- - ${b?q`

${b.technical}

`:G} - ${v?q`

- ${this._t("marker.value_badge_missing_hint")} -

`:G} - ${"value"===e.display&&f===y?q`

- ${this._t("marker.value_badge_duplicate")} -

`:G} -
- - ${this._help("marker.value_badge_position.help")} -
- - `:G} -
- ${d?q` - `:q`
- - ${this._t("marker.preview.select_source")} -
`} - ${"icon_ripple"===e.display?q`
- {this._markerDialog={...e,rippleColor:t.detail.color}}}> -
-
- ${this._t("marker.ripple_size")} - ${this._rangeInput(1,8,.5,e.rippleSize,t=>this._markerDialog={...e,rippleSize:t})} - ×${e.rippleSize} -
-

${this._t("marker.activity_alarm_note")}

`:G} - - -
- ${this._rangeInput(.5,3,.1,e.size,t=>this._markerDialog={...e,size:t})} - ×${e.size.toFixed(1)} - ${this._t("marker.angle_label")} - ${""} - ${this._rangeInput(0,355,5,e.angle,t=>this._markerDialog={...e,angle:t})} - ${e.angle}° -
- - - this._markerDialog={...e,model:t.target.value}} /> - - - this._markerDialog={...e,link:t.target.value}} /> - - - - - -
- ${e.pdfs.map(e=>q` - ${e.name} - this._removeMarkerPdf(e.url)}>`)} - - - this._pickMarkerFiles(e)} /> - -
-
-
-
- ${e.devId?q``:G} - ${e.devId?q``:G} -
-
- - -
-
-
`}_renderSpaceDialog(){const e=this._spaceDialog,t=this._importTotal>0&&"create"===e.mode?this._t("import.progress",{i:this._importTotal-this._importQueue.length,n:this._importTotal}):"",i=()=>{this._spaceDialog=null,this._importQueue=[],this._importTotal=0};return q` -
- - this._spaceDialog={...e,title:t.target.value}} /> - - - ${"file"===e.source?q`
- ${e.planFile?q`${e.planFile.name}`:e.planUrl?q`${this._t("space.plan_alt")}`:q`${this._t("space.no_plan")}`} - - - this._pickPlanFile(e)} /> - - -
- ${e.pickSaved?this._renderServerPlans(e):G}`:G} - - - -
- {const i=t.target.value,s=$v(i),n=null==s?null:(o=s,this._imperial?o*Mh:o);var o;this._spaceDialog={...e,cellCmInput:i,cellCmTouched:!0,cellCm:null!=n&&n>0?Math.max(.1,Math.min(kv,n)):e.cellCm}}} /> - ${this._t(this._imperial?"space.scale_unit_imperial":"space.scale_unit")} -
- - - - - -
${this._t("space.zero_wall_help")}
- - - ${""} - -
${this._t("space.hide_decor_tip")}
- -
${this._t("space.hide_openings_tip")}
- - ${[["labelTemp","space.label_temp"],["labelHum","space.label_hum"],["labelLqi","space.label_lqi"],["labelLight","space.label_light"]].map(([t,i])=>q``)} - -
- ${this._rangeInput(50,300,5,Math.round(100*e.cardFontScale),t=>this._spaceDialog={...e,cardFontScale:t/100})} - ${Math.round(100*e.cardFontScale)}% -
- ${this._renderCardPreview(e.cardFontScale,1,1)} -
- {this._spaceDialog={...e,roomColor:t.detail.color,roomOpacity:t.detail.opacity}}}> -
- - - ${"static"===(e.bgMode??Fr(this._settings,{}))?q`
- {this._spaceDialog={...e,bgColor:t.detail.color}}}> - ${e.bgColor?q``:q`${this._t("space.bg_inherited")}`} -
`:G} - -
- {const i=t.target.value.trim(),s=""===i?null:Math.round(Number(i));this._spaceDialog={...e,northDeg:null!==s&&Number.isFinite(s)?Math.min(359,Math.max(0,s)):null}}} /> - ${null===e.northDeg?this._t("space.north_inherited",{v:null===zr(this._settings,{})?"—":String(zr(this._settings,{}))+"°"}):"°"} -
- - - - ${qi.map(e=>[e,"fill."+e]).map(([t,i])=>q` - ${"custom"===t&&"custom"===e.fillMode?q`
- ${this._t("space.custom_fill")} - {this._spaceDialog={...e,customFill:{c:t.detail.color,a:t.detail.opacity}}}}> - ${e.customFill?q``:G} -
`:G}`)} - - ${e.deleteBlockers?q``:G} -
- -
`}_renderMergeDialog(){const e=this._mergeDialog,t=this._spaceModel()?.rooms||[],i=(i,s)=>{const n=t.find(e=>e.id===i),o=n?.area?this.hass.areas[n.area]?.name:null;return q``};return q`this._mergeDialog=null}> -
-

${this._t("merge.hint")}

- - ${i(e.aId,"a")} - ${i(e.bId,"b")} -
-
- - - -
-
`}_renderCardPreview(e,t,i){const s=18*e;return q`
- - ${this._t("preview.room_name")} - - 22.4° · - 45% · - ${this._t("roomcard.light_partial",{on:1,total:3})} - -
`}_renderRoomSource(e){const t="temp"===e?this._roomTempSrc:this._roomHumSrc,i=t=>{"temp"===e?this._roomTempSrc=t:this._roomHumSrc=t,this.requestUpdate()},s=this._roomSrcOpen===e;return q` - - - - ${t||s?q` - ${s?q`
- {this._roomSrcFilter=e.target.value,this.requestUpdate()}} /> -
- ${this._roomSrcCandidates().map(e=>q`
{i(e.value),this._roomSrcOpen=null}}> - ${e.label}${e.sub} -
`)} -
-
`:G}`:G}`}_renderRoomDialog(){const e=!!this._roomEditId,t=e?null:this._wallFaceBatch,i=t&&t.candidates.length>1?this._t("room.queue_progress",{current:t.index+1,total:t.candidates.length}):"",s=!!this._areaSel||!!this._nameSel.trim(),n=cs(this._curSpaceCfg),o=this._roomFill||n.fill,a=this._roomCustomFill||n.customFill,r=[...this._freeAreas];if(e&&this._areaSel&&!r.some(e=>e.area_id===this._areaSel)){const e=this.hass.areas[this._areaSel];e&&r.unshift(e)}return q` -
- ${i?q`

- ${i} -

`:G} - - this._nameSel=e.target.value} /> - - - - - - ${[["","fill.inherit"],...Wi.map(e=>[e,"fill."+e])].map(([e,t])=>q``)} - ${"custom"===o?q`
- ${this._roomCustomFill?this._t("room.custom_fill_own"):this._t("room.custom_fill_space")} - {this._roomCustomFill={c:e.detail.color,a:e.detail.opacity}}}> - ${this._roomCustomFill?q``:G} -
`:G} - ${this._renderRoomSource("temp")} - ${this._renderRoomSource("hum")} - - - -
- ${this._rangeInput(50,300,5,Math.round(100*this._roomNameScale),e=>{this._roomNameScale=e/100,this.requestUpdate()})} - ${Math.round(100*this._roomNameScale)}% -
- -
- ${this._rangeInput(50,300,5,Math.round(100*this._roomLabelScale),e=>{this._roomLabelScale=e/100,this.requestUpdate()})} - ${Math.round(100*this._roomLabelScale)}% -
- ${this._renderCardPreview(cs(this._curSpaceCfg).cardFontScale,this._roomNameScale,this._roomLabelScale)} -
-
- - - ${e?q``:q`${this._pendingSplit?G:q``} - `} -
-
`}}Vv.properties={_tabDrag:{state:!0},_hdrH:{state:!0},_booting:{state:!0},_bootFading:{state:!0},_bootSoft:{state:!0},_continuityEpoch:{state:!0},_tapConfirm:{state:!0},hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_hoverRoom:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_mode:{state:!0},_tool:{state:!0},_wallDialog:{state:!0},_drawWallField:{state:!0},_activeDraftId:{state:!0},_physicalSel:{state:!0},_physicalDialog:{state:!0},_partitionDeleteDialog:{state:!0},_roomDeleteDialog:{state:!0},_physicalDrag:{state:!0},_physicalRotate:{state:!0},_duplicateColumnId:{state:!0},_opMeasure:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_mergeSel:{state:!0},_openingPreset:{state:!0},_openingDialog:{state:!0},_openingInfo:{state:!0},_mergeDialog:{state:!0},_splitSel:{state:!0},_decorTool:{state:!0},_decorStyle:{state:!0},_decorDraft:{state:!0},_decorSel:{state:!0},_decorEraseConfirm:{state:!0},_decorTextDialog:{state:!0},_decorShapeDialog:{state:!0},_backdropDialog:{state:!0},_furnPalette:{state:!0},_bdDrag:{state:!0},_dtBox:{state:!0},_dtDrag:{state:!0},_kioskDialog:{state:!0},_vacFit:{state:!0},_vacAllCamerasFor:{state:!0},_vacCalConfirm:{state:!0},_kioskDots:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_roomEditId:{state:!0},_roomFill:{state:!0},_roomCustomFill:{state:!0},_roomTempSrc:{state:!0},_roomHumSrc:{state:!0},_roomSrcOpen:{state:!0},_roomSrcFilter:{state:!0},_roomNameScale:{state:!0},_roomLabelScale:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_deviceInbox:{state:!0},_rulesDialog:{state:!0},_settingsDialog:{state:!0},_alignDialog:{state:!0},_preflightClipboardFallback:{state:!0},_backupExportDialog:{state:!0},_backupImportDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},Vv.ZOOM_MAX=8,Vv.ZOOM_MIN=1/3,Vv.styles=[Ke,km],customElements.get("houseplan-card")||customElements.define("houseplan-card",Vv),window.customCards=window.customCards||[],window.customCards.find(e=>"houseplan-card"===e.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info(`%c HOUSEPLAN-CARD %c v${vv} `,"background:#3ea6ff;color:#04121f;font-weight:700",""); +globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="f111431804c4c42c6e35286f29720d089bc8b77ffc364a6517f7a3510fcb11c8";export{d3 as HouseplanCard}from"./houseplan-assets/houseplan-card-Cs2QHIcW.js"; diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 4526a853..cb7d03b7 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -22,7 +22,10 @@ media-wrapper survival) are pinned by `test/styles-split.test.mjs`, and ``` houseplan-card/ ├─ src/ # card sources (TypeScript + Lit 3) -│ ├─ houseplan-card.ts # the card: rendering, states, drag, tooltip, sticky header +│ ├─ houseplan-card.ts # eager View shell, HA lifecycle and projection +│ ├─ editor-runtime-loader.ts # lazy loader: dedupe, retry and build handshake +│ ├─ houseplan-editor-runtime.ts # Plan/Devices/Background composition root +│ ├─ houseplan-onboarding-runtime.ts # first-space/import dialogs, independent of editor │ ├─ hp-dialog.ts # shared HA/native modal shell, focus and transient-overlay lifecycle │ ├─ hp-help.ts # presentation-only, localized contextual-help surface │ ├─ floating-surface.ts # pure visual-viewport flip/shift geometry for dialog surfaces @@ -37,7 +40,7 @@ houseplan-card/ │ └─ data/ │ ├─ house.ts # geometry: ROOMS (rooms→area), FLOOR_VB (viewBox), names │ └─ backgrounds.ts # VECTOR plans (SVG base64) + FLOOR_BG_RECT (positioning) -├─ dist/houseplan-card.js # build (rollup+terser), ~290 KB, plans embedded +├─ dist/ # entry + manifest + content-hashed JS chunks ├─ demo/golden/ # deterministic HP-QA-01 matrix, capture/verify/accept ├─ demo/performance/ # large-house budgets and same-runner comparison ├─ scripts/release-*.mjs # exact-SHA publication contract and local orchestrator @@ -54,10 +57,16 @@ houseplan-card/ └─ docs/ # this documentation ``` -Rollup embeds a source fingerprint in the bundle. Performance and golden-image -tooling compares it with the current `src/` tree before recording results, so a -committed demo snapshot from an older source revision cannot produce a false -baseline. +Rollup embeds one source fingerprint in the eager entry and both lazy runtimes. +`dist/houseplan-assets.json` records the import graph, sizes and SHA-256 of every +generated asset. View loads only the initial graph; Plan, Devices and Background +share one editor runtime loaded on first intent. An empty installation loads a +separate onboarding dialog chunk, so creating the first space does not require +the editor asset; saving it then continues into Plan as before. The backend keeps the public +entry URL stable and serves only manifest-listed JS basenames below +`/houseplan_files/houseplan-assets/`. Performance, golden and smoke tooling +verify the manifest and every asset before recording results, so a stale or +partially copied tree cannot produce a false baseline. Prerelease publication has one fail-closed contract shared by the local command and the manual GitHub workflow. The tag version must match all six shipped diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 07a45050..5ec58bc2 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +- View now starts without downloading the three editors; their shared runtime + is loaded on first use and protected by one retry and a build-version check. + Release and test tooling now verify the complete multi-file frontend asset + tree rather than one JavaScript file + ([#337](https://github.com/Matysh/houseplan-card/issues/337)). + - The wall-junction checks are honest at the boundaries: false refusals on microscopic coordinate noise are gone, very long walls no longer crash the check, and an internal check error refuses the write instead of waving it diff --git a/docs/CHANGELOG.ru.md b/docs/CHANGELOG.ru.md index 8bbf9da8..7dca11f7 100755 --- a/docs/CHANGELOG.ru.md +++ b/docs/CHANGELOG.ru.md @@ -8,6 +8,12 @@ ## Не выпущено +- Режим просмотра теперь запускается без загрузки трёх редакторов: их общий + runtime подгружается при первом обращении и защищён повторной попыткой и + проверкой версии сборки. Релизные и тестовые инструменты проверяют всё дерево + frontend-ассетов, а не один JavaScript-файл + ([#337](https://github.com/Matysh/houseplan-card/issues/337)). + - Проверки стыков стен стали честными на границах: ложные отказы на микроскопических погрешностях координат устранены, очень длинные стены не роняют проверку, а внутренняя ошибка проверки больше не пропускает запись diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 9b8163bb..bdc3d765 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -102,10 +102,16 @@ git config core.untrackedCache true - IMPORTANT (audit lesson): the rollup typescript plugin reports a syntax error as a WARNING and still builds the bundle — a truncated file can "pass". That is why the build starts with `tsc --noEmit`, which fails on such errors. Always build with `npm run build`, never bare `rollup -c`. -- Before committing a frontend source change, run `npm run bundle:sync`: `npm run build` - writes `dist/houseplan-card.js` only, while the release contract also requires the - committed `custom_components/houseplan/frontend/houseplan-card.js` snapshot to be - byte-identical. The demo copy is refreshed by the same command but remains untracked. +- Before committing a frontend source change, run `npm run bundle:sync`. Rollup writes + `dist/houseplan-card.js`, `dist/houseplan-assets.json` and content-hashed chunks under + `dist/houseplan-assets/`; the command synchronizes that complete tree to the committed + integration snapshot and the untracked demo copy, then verifies every manifest hash. + `node scripts/bundle-tree.mjs dist custom_components/houseplan/frontend` is the + read-only parity check used by CI and release automation. +- The first-space/import dialog is a separate `houseplan-onboarding-runtime-*` + chunk. Do not fold it into `houseplan-editor-runtime-*`: empty-install + onboarding is a View prerequisite, while a configured View must request + neither lazy runtime until the corresponding user intent. ## Maintenance diagnostics @@ -172,9 +178,9 @@ commands and the explicit review workflow are documented in ```bash cd /tmp/hpc && npm ci # once -npx rollup -c # → dist/houseplan-card.js -node --check dist/houseplan-card.js -cp dist/houseplan-card.js custom_components/houseplan/frontend/ +npm run bundle:sync # build + entry/manifest/chunks → integration + demo +npm run bundle:budget # initial View graph must stay <= 256000 B gzip +node scripts/bundle-tree.mjs dist custom_components/houseplan/frontend ``` ## Deployment to the dacha (ha.jbstudio.pro) @@ -184,7 +190,9 @@ cp dist/houseplan-card.js custom_components/houseplan/frontend/ - **The HA config root is `/mnt/data/supervisor/homeassistant`** — in this SSH environment `/config` does not exist; a deploy aimed at `/config/...` fails with "No such file or directory". -- JS: `scp -P 22222 -i dist/houseplan-card.js root@ha.jbstudio.pro:/mnt/data/supervisor/homeassistant/custom_components/houseplan/frontend/` +- Frontend: copy the complete `custom_components/houseplan/frontend/` tree. + Copying only `houseplan-card.js` is unsupported: the entry imports hashed chunks and + validates its editor runtime against the build fingerprint. - Cache busting: `sed` the `?v=` version in `.storage/lovelace_resources`, then restart HA. - **The `frontend/` subfolder is not optional.** `__init__.py` registers `Path(__file__).parent / "frontend" / "houseplan-card.js"` as the static path. diff --git a/docs/TESTING.md b/docs/TESTING.md index 75f49261..6d1d504c 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -3255,6 +3255,26 @@ require hands on real hardware — they remain for the human pass. Popover and forced portal fallback have the same no-layout-shift contract [auto: `smoke_help_affordance`]. +## Lazy editor runtime and frontend asset tree (#337) + +- [ ] A cold configured View reaches a complete interactive frame without any + request for `houseplan-editor-runtime-*.js`. The first Plan/Devices/ + Background intent requests it once; later editor switches do not repeat + the request [auto: `smoke_lazy_editor_chunk`]. +- [ ] Two failed requests, or a runtime with a different build fingerprint, + leave mode, camera and plan in View and show the localized refresh advice + [auto: `editor-runtime-loader.test`, `smoke_lazy_editor_chunk`]. +- [ ] An empty installation requests the dedicated onboarding chunk, displays + the first-space dialog and still has no editor request. Saving a drawn + first space requests the editor once and continues into Plan; async + `getConfigElement()` still returns `houseplan-card-editor` + [auto: `smoke_lazy_editor_chunk`]. +- [ ] `bundle:budget` follows transitive static imports and keeps initial View + at or below 256000 B gzip. Bundle sync, demo freshness, CI artifacts and + release zip validation fail when any manifest-listed asset is missing or + its SHA-256 differs [auto: `bundle-assets.test`, `bundle-freshness.test`, + release-contract tests]. + ## Hiding layers: decor, openings, zero-thickness walls (docs/UX-MODES.md) - [ ] **Room names have one literal off state (#203)**: disable «Показывать diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index bc7afca5..2833b8c5 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -101,7 +101,14 @@ on-disk path, not the JavaScript URL served by Home Assistant. Copy the release folder to `config/custom_components/houseplan`, restart Home Assistant, add the integration, then add the resource above only if Lovelace -resources are YAML-managed. +resources are YAML-managed. Always copy the complete integration folder: the +stable resource URL remains one file, but that entry loads internal +content-hashed modules. A lone `houseplan-card.js` is not a supported install. + +The ordinary View does not download editor code. The first opening of Plan, +Device or Background may therefore take a brief moment. If that internal module +cannot be loaded after one retry, the plan stays in View and asks you to refresh +the page; no half-open editor is kept. ### Permissions diff --git a/docs/USER-GUIDE.ru.md b/docs/USER-GUIDE.ru.md index 85813c30..9a64c1d9 100644 --- a/docs/USER-GUIDE.ru.md +++ b/docs/USER-GUIDE.ru.md @@ -101,6 +101,15 @@ resources: 3. Добавьте интеграцию **House Plan**. 4. Если ресурсы Lovelace управляются YAML, добавьте ресурс из примера выше. +Копируйте папку интеграции целиком: стабильный URL ресурса по-прежнему один, +но entry-файл загружает внутренние модули с хешированными именами. Установка +одного `houseplan-card.js` не поддерживается. + +В обычном просмотре код редакторов не загружается. Поэтому первое открытие +«Плана», «Устройств» или «Подложки» может занять короткое время. Если внутренний +модуль не загрузился и после одной повторной попытки, план остаётся в просмотре +и предлагает обновить страницу; наполовину открытого редактора не остаётся. + ### Права пользователей | Настройка интеграции | Просмотр | Управление устройствами | Редактирование планов и загрузка файлов | diff --git a/package.json b/package.json index 8b465e18..388ed7e4 100755 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "test": "tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs && node --test test/*.test.mjs", "benchmark:wall-model": "tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs && node scripts/benchmark-wall-segment-model.mjs", "bundle:sync": "npm run build && node scripts/bundle-sync.mjs", + "bundle:budget": "node scripts/bundle-budget.mjs", "invariants": "tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs && node scripts/model-invariants.mjs", "docs:accept": "node scripts/docs-accept.mjs", "smokes:select": "node scripts/smoke-select.mjs", diff --git a/rollup.config.mjs b/rollup.config.mjs index 41d78f39..1d820beb 100755 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -3,13 +3,22 @@ import json from '@rollup/plugin-json'; import resolve from '@rollup/plugin-node-resolve'; import terser from '@rollup/plugin-terser'; import { sourceFingerprint } from './scripts/source-fingerprint.mjs'; +import { cssTemplateMinifier } from './scripts/css-template-minifier.mjs'; +import { + buildFingerprintPlugin, + bundleManifestPlugin, + cleanBundleOutputPlugin, + editorRuntimeRetryUrlPlugin, +} from './scripts/bundle-manifest.mjs'; const SOURCE_FINGERPRINT = sourceFingerprint(); export default { input: 'src/houseplan-card.ts', output: { - file: 'dist/houseplan-card.js', + dir: 'dist', + entryFileNames: 'houseplan-card.js', + chunkFileNames: 'houseplan-assets/[name]-[hash].js', format: 'es', sourcemap: false, // Tooling reads this before recording screenshots/performance. A committed @@ -17,5 +26,15 @@ export default { // plausible-looking but invalid baseline. intro: `globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__=${JSON.stringify(SOURCE_FINGERPRINT)};`, }, - plugins: [resolve(), json(), typescript(), terser({ format: { comments: false } })], + plugins: [ + cleanBundleOutputPlugin(), + buildFingerprintPlugin(SOURCE_FINGERPRINT), + cssTemplateMinifier(), + resolve(), + json(), + typescript({ compilerOptions: { outDir: 'dist/.ts' } }), + terser({ format: { comments: false } }), + editorRuntimeRetryUrlPlugin(), + bundleManifestPlugin(SOURCE_FINGERPRINT), + ], }; diff --git a/scripts/bundle-budget.mjs b/scripts/bundle-budget.mjs new file mode 100644 index 00000000..26aaa86a --- /dev/null +++ b/scripts/bundle-budget.mjs @@ -0,0 +1,39 @@ +#!/usr/bin/env node +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +export const INITIAL_VIEW_GZIP_BUDGET = 256_000; + +export function assertBundleBudget(manifest, budget = INITIAL_VIEW_GZIP_BUDGET) { + if (manifest?.schema !== 1 || !Array.isArray(manifest.files)) { + throw new Error('invalid houseplan-assets.json'); + } + if (!manifest.lazyEditorFiles?.length) { + throw new Error('bundle has no lazy editor graph'); + } + if (manifest.initialViewFiles.some((path) => manifest.lazyEditorFiles.includes(path))) { + throw new Error('initial View graph overlaps lazy editor graph'); + } + if (manifest.initialViewGzipBytes > budget) { + throw new Error( + `initial View graph ${manifest.initialViewGzipBytes} B gzip exceeds ${budget} B budget`, + ); + } + return { + initialViewGzipBytes: manifest.initialViewGzipBytes, + lazyEditorGzipBytes: manifest.lazyEditorGzipBytes, + }; +} + +if (import.meta.url === pathToFileURL(process.argv[1] || '').href) { + try { + const manifest = JSON.parse(readFileSync(resolve('dist/houseplan-assets.json'), 'utf8')); + const result = assertBundleBudget(manifest); + console.log(`initial View: ${result.initialViewGzipBytes} B gzip`); + console.log(`lazy editor: ${result.lazyEditorGzipBytes} B gzip`); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + } +} diff --git a/scripts/bundle-manifest.mjs b/scripts/bundle-manifest.mjs new file mode 100644 index 00000000..21bc57da --- /dev/null +++ b/scripts/bundle-manifest.mjs @@ -0,0 +1,169 @@ +import { createHash } from 'node:crypto'; +import { existsSync, readFileSync, rmSync } from 'node:fs'; +import { dirname, posix, relative, resolve } from 'node:path'; +import { gzipSync } from 'node:zlib'; + +const BUILD_FINGERPRINT_TOKEN = '__HOUSEPLAN_SOURCE_FINGERPRINT__'; +const EDITOR_RETRY_ASSET_TOKEN = '__HOUSEPLAN_EDITOR_RETRY_ASSET__'; +const ONBOARDING_RETRY_ASSET_TOKEN = '__HOUSEPLAN_ONBOARDING_RETRY_ASSET__'; + +const sha256 = (value) => createHash('sha256').update(value).digest('hex'); + +const reachable = (entry, byPath, edge) => { + const seen = new Set(); + const visit = (path) => { + if (seen.has(path)) return; + const file = byPath.get(path); + if (!file) throw new Error(`bundle manifest references missing asset: ${path}`); + seen.add(path); + for (const child of file[edge] || []) visit(child); + }; + visit(entry); + return seen; +}; + +export function buildBundleManifest(bundle, fingerprint) { + const files = Object.values(bundle) + .filter((item) => item.type === 'chunk') + .map((chunk) => { + const contents = Buffer.from(chunk.code, 'utf8'); + return { + path: chunk.fileName.replaceAll('\\', '/'), + sha256: sha256(contents), + rawBytes: contents.length, + gzipBytes: gzipSync(contents, { level: 9 }).length, + isEntry: chunk.isEntry, + imports: [...chunk.imports].sort(), + dynamicImports: [...chunk.dynamicImports].sort(), + }; + }) + .sort((left, right) => left.path.localeCompare(right.path)); + const entry = files.find((file) => file.isEntry)?.path; + if (!entry) throw new Error('bundle manifest has no entry chunk'); + const byPath = new Map(files.map((file) => [file.path, file])); + const initial = reachable(entry, byPath, 'imports'); + const dynamicRoots = [...initial] + .flatMap((path) => byPath.get(path)?.dynamicImports || []); + const lazy = new Set(); + for (const root of dynamicRoots) { + for (const path of reachable(root, byPath, 'imports')) if (!initial.has(path)) lazy.add(path); + } + const onboardingRoots = dynamicRoots.filter((path) => path.includes('houseplan-onboarding-runtime-')); + const editorRoots = dynamicRoots.filter((path) => !onboardingRoots.includes(path)); + const graphFrom = (roots) => { + const graph = new Set(); + for (const root of roots) { + for (const path of reachable(root, byPath, 'imports')) if (!initial.has(path)) graph.add(path); + } + return graph; + }; + const lazyEditor = graphFrom(editorRoots); + const lazyOnboarding = graphFrom(onboardingRoots); + const sum = (paths) => [...paths] + .reduce((total, path) => total + (byPath.get(path)?.gzipBytes || 0), 0); + return { + schema: 1, + fingerprint, + entry, + initialViewFiles: [...initial].sort(), + initialViewGzipBytes: sum(initial), + lazyFiles: [...lazy].sort(), + lazyGzipBytes: sum(lazy), + lazyEditorFiles: [...lazyEditor].sort(), + lazyEditorGzipBytes: sum(lazyEditor), + lazyOnboardingFiles: [...lazyOnboarding].sort(), + lazyOnboardingGzipBytes: sum(lazyOnboarding), + files, + }; +} + +export function bundleManifestPlugin(fingerprint) { + return { + name: 'houseplan-bundle-manifest', + generateBundle(_options, bundle) { + const manifest = buildBundleManifest(bundle, fingerprint); + this.emitFile({ + type: 'asset', + fileName: 'houseplan-assets.json', + source: `${JSON.stringify(manifest, null, 2)}\n`, + }); + }, + }; +} + +/** Embed one immutable build id in both sides of the lazy handshake. */ +export function buildFingerprintPlugin(fingerprint) { + return { + name: 'houseplan-build-fingerprint', + transform(code, id) { + if (!id.replaceAll('\\', '/').includes('/src/') + || !code.includes(BUILD_FINGERPRINT_TOKEN)) return null; + return { + code: code.replaceAll(BUILD_FINGERPRINT_TOKEN, fingerprint), + map: null, + }; + }, + }; +} + +/** + * Point the second import attempt at the exact content-hashed editor chunk. + * Rollup owns that filename, so the URL is injected only after chunk naming; + * the first import remains a normal Rollup edge for graph accounting. + */ +export function editorRuntimeRetryUrlPlugin() { + return { + name: 'houseplan-editor-retry-url', + generateBundle(_options, bundle) { + const chunks = Object.values(bundle).filter((item) => item.type === 'chunk'); + const editor = chunks.find((chunk) => Object.keys(chunk.modules) + .some((id) => id.replaceAll('\\', '/').endsWith('/src/houseplan-editor-runtime.ts'))); + const onboarding = chunks.find((chunk) => Object.keys(chunk.modules) + .some((id) => id.replaceAll('\\', '/').endsWith('/src/houseplan-onboarding-runtime.ts'))); + if (!editor) throw new Error('editor runtime chunk was not emitted'); + if (!onboarding) throw new Error('onboarding runtime chunk was not emitted'); + let editorReplacements = 0; + let onboardingReplacements = 0; + for (const chunk of chunks) { + if (chunk.code.includes(EDITOR_RETRY_ASSET_TOKEN)) { + let asset = posix.relative(posix.dirname(chunk.fileName), editor.fileName); + if (!asset.startsWith('.')) asset = `./${asset}`; + editorReplacements += chunk.code.split(EDITOR_RETRY_ASSET_TOKEN).length - 1; + chunk.code = chunk.code.replaceAll(EDITOR_RETRY_ASSET_TOKEN, asset); + } + if (chunk.code.includes(ONBOARDING_RETRY_ASSET_TOKEN)) { + let asset = posix.relative(posix.dirname(chunk.fileName), onboarding.fileName); + if (!asset.startsWith('.')) asset = `./${asset}`; + onboardingReplacements += chunk.code.split(ONBOARDING_RETRY_ASSET_TOKEN).length - 1; + chunk.code = chunk.code.replaceAll(ONBOARDING_RETRY_ASSET_TOKEN, asset); + } + } + if (editorReplacements !== 1 || onboardingReplacements !== 1) { + throw new Error('lazy retry URL placeholder counts are ' + + `${editorReplacements}/${onboardingReplacements}, expected 1/1`); + } + }, + }; +} + +/** Remove only files named by the previous generated manifest. */ +export function cleanBundleOutputPlugin(outputRoot = 'dist') { + return { + name: 'houseplan-clean-bundle-output', + buildStart() { + const root = resolve(outputRoot); + const manifestPath = resolve(root, 'houseplan-assets.json'); + if (!existsSync(manifestPath)) return; + const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')); + for (const file of manifest?.files || []) { + const path = resolve(root, String(file.path || '')); + const rel = relative(root, path); + if (!rel || rel.startsWith('..') || rel.includes(':')) { + throw new Error(`previous bundle path escapes output root: ${file.path}`); + } + rmSync(path, { force: true }); + } + rmSync(manifestPath, { force: true }); + }, + }; +} diff --git a/scripts/bundle-sync.mjs b/scripts/bundle-sync.mjs index fb4b6fad..0af2fc73 100644 --- a/scripts/bundle-sync.mjs +++ b/scripts/bundle-sync.mjs @@ -9,24 +9,81 @@ * туда» жило шестью разными `cp` в воркфлоу и трижды в документации; когда * копию забывали, смок врал согласованно (#236). */ -import { copyFileSync, existsSync, mkdirSync } from 'node:fs'; -import { dirname, resolve } from 'node:path'; +import { createHash } from 'node:crypto'; +import { + copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, +} from 'node:fs'; +import { dirname, relative, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); -const SOURCE = resolve(ROOT, 'dist/houseplan-card.js'); +const SOURCE_ROOT = resolve(ROOT, 'dist'); +const MANIFEST_NAME = 'houseplan-assets.json'; const TARGETS = [ - 'custom_components/houseplan/frontend/houseplan-card.js', - 'demo/srv/assets/houseplan-card.js', + 'custom_components/houseplan/frontend', + 'demo/srv/assets', ]; -if (!existsSync(SOURCE)) { - console.error('dist/houseplan-card.js не найден: сначала `npm run build`'); +const manifestPath = resolve(SOURCE_ROOT, MANIFEST_NAME); +if (!existsSync(manifestPath)) { + console.error(`dist/${MANIFEST_NAME} не найден: сначала \`npm run build\``); process.exit(1); } -for (const target of TARGETS) { - const path = resolve(ROOT, target); - mkdirSync(dirname(path), { recursive: true }); - copyFileSync(SOURCE, path); - console.log(`бандл → ${target}`); + +const readManifest = (path) => { + const parsed = JSON.parse(readFileSync(path, 'utf8')); + if (parsed?.schema !== 1 || !Array.isArray(parsed.files) || !parsed.entry) { + throw new Error(`${path}: invalid House Plan bundle manifest`); + } + return parsed; +}; +const sourceManifest = readManifest(manifestPath); +const managedFiles = [MANIFEST_NAME, ...sourceManifest.files.map((file) => file.path)]; +const sha256 = (path) => createHash('sha256').update(readFileSync(path)).digest('hex'); +const contained = (root, name) => { + const path = resolve(root, name); + const rel = relative(root, path); + if (!rel || rel.startsWith('..') || rel.includes(':')) { + throw new Error(`bundle path escapes target root: ${name}`); + } + return path; +}; + +for (const file of sourceManifest.files) { + const path = contained(SOURCE_ROOT, file.path); + if (!existsSync(path)) throw new Error(`manifest asset is missing: ${file.path}`); + if (sha256(path) !== file.sha256) throw new Error(`manifest hash mismatch: ${file.path}`); +} + +for (const target of TARGETS) { + const targetRoot = resolve(ROOT, target); + const oldManifestPath = resolve(targetRoot, MANIFEST_NAME); + let old = null; + if (existsSync(oldManifestPath)) { + old = readManifest(oldManifestPath); + } + // Content-hashed dependencies first, stable entry second, manifest last. + // At every observable point the current manifest therefore names a complete + // tree; an interrupted copy cannot publish an allowlist for absent chunks. + const payload = sourceManifest.files + .map((file) => file.path) + .sort((left, right) => (left === sourceManifest.entry ? 1 : 0) + - (right === sourceManifest.entry ? 1 : 0) || left.localeCompare(right)); + for (const name of payload) { + const source = contained(SOURCE_ROOT, name); + const destination = contained(targetRoot, name); + mkdirSync(dirname(destination), { recursive: true }); + copyFileSync(source, destination); + } + copyFileSync(manifestPath, oldManifestPath); + for (const file of old?.files || []) { + if (!managedFiles.includes(file.path)) rmSync(contained(targetRoot, file.path), { force: true }); + } + const copied = readManifest(resolve(targetRoot, MANIFEST_NAME)); + for (const file of copied.files) { + if (sha256(contained(targetRoot, file.path)) !== file.sha256) { + throw new Error(`${target}/${file.path}: copied hash mismatch`); + } + } + console.log(`бандл-дерево → ${target}`); } diff --git a/scripts/bundle-tree.mjs b/scripts/bundle-tree.mjs new file mode 100644 index 00000000..8363ba03 --- /dev/null +++ b/scripts/bundle-tree.mjs @@ -0,0 +1,83 @@ +#!/usr/bin/env node +import { createHash } from 'node:crypto'; +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, relative, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +export const BUNDLE_MANIFEST = 'houseplan-assets.json'; + +export const sha256Bytes = (contents) => createHash('sha256').update(contents).digest('hex'); + +export function containedBundlePath(root, name) { + if (typeof name !== 'string' || !name || name.includes('\\')) { + throw new Error(`invalid bundle path: ${String(name)}`); + } + const path = resolve(root, name); + const rel = relative(resolve(root), path); + if (!rel || rel.startsWith('..') || rel.includes(':')) { + throw new Error(`bundle path escapes root: ${name}`); + } + return path; +} + +export function readBundleManifest(root) { + const path = resolve(root, BUNDLE_MANIFEST); + if (!existsSync(path)) throw new Error(`${path}: bundle manifest is missing`); + const manifest = JSON.parse(readFileSync(path, 'utf8')); + if (manifest?.schema !== 1 || typeof manifest.entry !== 'string' + || typeof manifest.fingerprint !== 'string' || !Array.isArray(manifest.files)) { + throw new Error(`${path}: invalid House Plan bundle manifest`); + } + const names = manifest.files.map((file) => file?.path); + if (new Set(names).size !== names.length || !names.includes(manifest.entry)) { + throw new Error(`${path}: duplicate assets or missing entry`); + } + return manifest; +} + +export function verifyBundleTree(root) { + const manifest = readBundleManifest(root); + for (const file of manifest.files) { + if (typeof file?.path !== 'string' || !file.path.endsWith('.js') + || !/^[a-zA-Z0-9._/-]+$/.test(file.path)) { + throw new Error(`invalid manifest asset path: ${String(file?.path)}`); + } + const path = containedBundlePath(root, file.path); + if (!existsSync(path)) throw new Error(`manifest asset is missing: ${file.path}`); + const actual = sha256Bytes(readFileSync(path)); + if (actual !== file.sha256) { + throw new Error(`manifest hash mismatch: ${file.path} (${actual} != ${file.sha256})`); + } + } + return manifest; +} + +export function compareBundleTrees(sourceRoot, targetRoot) { + const source = verifyBundleTree(sourceRoot); + const target = verifyBundleTree(targetRoot); + if (JSON.stringify(target) !== JSON.stringify(source)) { + throw new Error('bundle manifests differ'); + } + for (const file of source.files) { + const left = readFileSync(containedBundlePath(sourceRoot, file.path)); + const right = readFileSync(containedBundlePath(targetRoot, file.path)); + if (!left.equals(right)) throw new Error(`bundle asset differs: ${file.path}`); + } + return source; +} + +const invokedDirectly = process.argv[1] + && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url)); +if (invokedDirectly) { + try { + const [source, target] = process.argv.slice(2); + if (!source) throw new Error('usage: node scripts/bundle-tree.mjs [matching-root]'); + const manifest = target + ? compareBundleTrees(resolve(source), resolve(target)) + : verifyBundleTree(resolve(source)); + console.log(`verified ${manifest.files.length} bundle assets`); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; + } +} diff --git a/scripts/coordinate-write-barrier-guard.mjs b/scripts/coordinate-write-barrier-guard.mjs index 093846b5..76142a37 100644 --- a/scripts/coordinate-write-barrier-guard.mjs +++ b/scripts/coordinate-write-barrier-guard.mjs @@ -24,15 +24,24 @@ const requireWindow = (errors, source, index, pattern, label, before = 650) => { export function checkCoordinateWriteBarriers(root = defaultRoot) { const errors = []; - const frontend = readFileSync(resolve(root, 'src/houseplan-card.ts'), 'utf8'); + // The editor implementation is a lazy chunk, but the persisted-coordinate + // inventory remains one logical frontend. Normalize the typed host port so + // the existing boundary windows keep describing the production call sites. + const card = readFileSync(resolve(root, 'src/houseplan-card.ts'), 'utf8'); + const editor = readFileSync(resolve(root, 'src/houseplan-editor-runtime.ts'), 'utf8') + .replaceAll('this.host.', 'this.'); + const frontend = `${card}\n${editor}`; const configWrites = occurrences(frontend, "type: 'houseplan/config/set'"); if (configWrites.length !== 1) errors.push(`frontend config writer inventory: ${configWrites.length}`); for (const index of configWrites) requireWindow( errors, frontend, index, - /const candidate = canonicalizeConfigGeometry\(this\._serverCfg\);[\s\S]*config: candidate/, - 'frontend config/set', 3600, + /const canonicalCandidate = canonicalizeConfigGeometry\(candidate\);[\s\S]*config: canonicalCandidate/, + 'frontend config/set', 600, ); + if (occurrences(frontend, '._sendConfigCandidate(candidate)').length !== 2) { + errors.push('frontend canonical config transport callers changed'); + } const positionWrites = occurrences(frontend, "type: 'houseplan/layout/update'"); if (positionWrites.length !== 2) errors.push(`frontend position writer inventory: ${positionWrites.length}`); diff --git a/scripts/css-template-minifier.mjs b/scripts/css-template-minifier.mjs new file mode 100644 index 00000000..b6630c7c --- /dev/null +++ b/scripts/css-template-minifier.mjs @@ -0,0 +1,107 @@ +/** + * Conservative minifier for static Lit css`` templates. + * + * This deliberately is not a general CSS optimiser: it removes comments and + * redundant whitespace while preserving strings, escapes and descendant + * combinators. Interpolated templates fail closed because rewriting across a + * JavaScript expression would make the build depend on runtime values. + */ +const isWhitespace = (char) => /[\t\n\f\r ]/.test(char); +const TIGHT_BEFORE = new Set(['{', '}', ':', ';', ',', '>', ')']); +const TIGHT_AFTER = new Set(['{', '}', ':', ';', ',', '>', '(']); + +export function minifyCssText(source, label = '') { + let out = ''; + let quote = ''; + let pendingSpace = false; + + for (let i = 0; i < source.length; i += 1) { + const char = source[i]; + const next = source[i + 1]; + + if (quote) { + out += char; + if (char === '\\') { + if (i + 1 >= source.length) throw new Error(`${label}: dangling CSS escape`); + out += source[++i]; + } else if (char === quote) quote = ''; + continue; + } + + if (char === '"' || char === "'") { + if (pendingSpace && out && !TIGHT_AFTER.has(out.at(-1))) out += ' '; + pendingSpace = false; + quote = char; + out += char; + continue; + } + + if (char === '/' && next === '*') { + const end = source.indexOf('*/', i + 2); + if (end < 0) throw new Error(`${label}: unclosed CSS comment`); + i = end + 1; + continue; + } + + if (isWhitespace(char)) { + pendingSpace = true; + continue; + } + + if (pendingSpace && out && !TIGHT_AFTER.has(out.at(-1)) && !TIGHT_BEFORE.has(char)) { + out += ' '; + } + pendingSpace = false; + if (TIGHT_BEFORE.has(char) && out.endsWith(' ')) out = out.slice(0, -1); + out += char; + } + + if (quote) throw new Error(`${label}: unclosed CSS string`); + return out.trim(); +} + +export function minifyStaticCssTemplates(code, id = '') { + let cursor = 0; + let output = ''; + let changed = false; + while (true) { + const start = code.indexOf('css`', cursor); + if (start < 0) break; + let end = start + 4; + let escaped = false; + for (; end < code.length; end += 1) { + const char = code[end]; + if (escaped) { + escaped = false; + continue; + } + if (char === '\\') { + escaped = true; + continue; + } + if (char === '$' && code[end + 1] === '{') { + throw new Error(`${id}:${end}: interpolated css template is not supported`); + } + if (char === '`') break; + } + if (end >= code.length) throw new Error(`${id}:${start}: unclosed css template`); + const css = code.slice(start + 4, end); + output += code.slice(cursor, start); + output += `css\`${minifyCssText(css, `${id}:${start}`)}\``; + cursor = end + 1; + changed = true; + } + if (!changed) return null; + return output + code.slice(cursor); +} + +export function cssTemplateMinifier() { + return { + name: 'houseplan-css-template-minifier', + transform(code, id) { + if (!id.endsWith('.ts') || !code.includes('css`')) return null; + const transformed = minifyStaticCssTemplates(code, id); + return transformed == null ? null : { code: transformed, map: null }; + }, + }; +} diff --git a/scripts/mutation-gate.mjs b/scripts/mutation-gate.mjs index e6e3712f..178bd973 100644 --- a/scripts/mutation-gate.mjs +++ b/scripts/mutation-gate.mjs @@ -37,10 +37,96 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; +const repoRoot = fileURLToPath(new URL('..', import.meta.url)); + +const occurrenceCount = (source, needle) => source.split(needle).length - 1; + +/** + * Map a logical pre-split card anchor onto the lazy editor source. + * + * Keeping the mutation definitions in their original logical form makes the + * registry readable and preserves the history of each regression. At runtime + * we relocate only anchors that actually moved into the typed editor host. + * The mapping is deliberately strict: an ambiguous or missing anchor still + * fails the cheap mutation applicability gate. + */ +function relocateEditorPatch(patch, cardSource, editorSource) { + if (patch.file !== 'src/houseplan-card.ts' + || occurrenceCount(cardSource, patch.find) === 1) return patch; + if (occurrenceCount(editorSource, patch.find) === 1) { + return { ...patch, file: 'src/houseplan-editor-runtime.ts' }; + } + + const normalized = editorSource.replaceAll('this.host.', 'this.'); + if (occurrenceCount(normalized, patch.find) !== 1) return patch; + + const normalizedStart = normalized.indexOf(patch.find); + let rawIndex = 0; + let normalizedIndex = 0; + while (normalizedIndex < normalizedStart) { + if (editorSource.startsWith('this.host.', rawIndex)) { + rawIndex += 'this.host.'.length; + normalizedIndex += 'this.'.length; + } else { + rawIndex += 1; + normalizedIndex += 1; + } + } + const rawStart = rawIndex; + const hostFlags = []; + let findIndex = 0; + while (findIndex < patch.find.length) { + if (patch.find.startsWith('this.', findIndex)) { + hostFlags.push(editorSource.startsWith('this.host.', rawIndex)); + } + if (editorSource.startsWith('this.host.', rawIndex)) { + rawIndex += 'this.host.'.length; + findIndex += 'this.'.length; + } else { + rawIndex += 1; + findIndex += 1; + } + } + const rawFind = editorSource.slice(rawStart, rawIndex); + let replacementThis = 0; + const rawReplace = patch.replace.replaceAll('this.', () => { + const host = hostFlags[replacementThis++] === true; + return host ? 'this.host.' : 'this.'; + }); + return { + ...patch, + file: 'src/houseplan-editor-runtime.ts', + find: rawFind, + replace: rawReplace, + }; +} + // --- реестр --------------------------------------------------------------- // `find` обязан встречаться в файле ровно один раз: патч, который ложится «куда // попало», проверяет не то, что объявлен проверять. Это контролирует --check. -export const MUTANTS = [ +const MUTANT_DEFINITIONS = [ + { + id: 'editor-runtime-fingerprint-handshake', + guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs ' + + '&& node --test test/editor-runtime-loader.test.mjs', + because: 'a mixed entry/editor build must fail closed instead of installing incompatible code (#337)', + patches: [{ + file: 'src/editor-runtime-loader.ts', + find: ' if (module.fingerprint !== this.options.expectedFingerprint) {', + replace: ' if (false && module.fingerprint !== this.options.expectedFingerprint) {', + }], + }, + { + id: 'editor-runtime-one-retry', + guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs ' + + '&& node --test test/editor-runtime-loader.test.mjs', + because: 'a transient chunk failure gets exactly one bounded retry (#337)', + patches: [{ + file: 'src/editor-runtime-loader.ts', + find: ' for (const attempt of [0, 1] as const) {', + replace: ' for (const attempt of [0] as const) {', + }], + }, { id: 'wallthick-hover-floor-back', guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs ' @@ -1527,9 +1613,9 @@ export const MUTANTS = [ guard: 'npm run bundle:sync && node demo/smoke_near_axis_optimize.mjs', because: 'opening the Optimize preview or cancelling it must never persist a lossy repair (#290)', patches: [{ - file: 'src/houseplan-card.ts', - find: ' private _openAlignDialog = (): void => this._previewAlignDialog(false);', - replace: ' private _openAlignDialog = (): void => {\n' + file: 'src/houseplan-editor-runtime.ts', + find: 'public _openAlignDialog = (): void => this._previewAlignDialog(false);', + replace: 'public _openAlignDialog = (): void => {\n' + ' this._previewAlignDialog(false);\n' + ' void this._runAlignToGrid();\n' + ' };', @@ -3432,8 +3518,16 @@ export const MUTANTS = [ }, ]; +const mutationCardSource = readFileSync(join(repoRoot, 'src/houseplan-card.ts'), 'utf8'); +const mutationEditorSource = readFileSync(join(repoRoot, 'src/houseplan-editor-runtime.ts'), 'utf8'); +export const MUTANTS = MUTANT_DEFINITIONS.map((mutant) => ({ + ...mutant, + patches: mutant.patches.map((patch) => relocateEditorPatch( + patch, mutationCardSource, mutationEditorSource, + )), +})); + // --- механика --------------------------------------------------------------- -const repoRoot = fileURLToPath(new URL('..', import.meta.url)); export function applyPatches(root, patches) { for (const patch of patches) { @@ -3493,7 +3587,8 @@ export function guardNeedsTestBuild(guard) { * Нужен ли гварду СОБРАННЫЙ бандл (#332). * * Бандл читают только браузерные проверки: смоки и golden-сцены грузят - * `demo/srv/assets/houseplan-card.js`, и мутант обязан попасть в него, иначе + * дерево `demo/srv/assets/`, и мутант обязан попасть в entry и lazy chunks, + * иначе * guard проверяет чистый код. Юнит- и бэкенд-гварды бандл не открывают ни в * каком виде (проверено по реестру и по исходникам тестов: dist/** читается * только как git-чекаут, который в worktree и так есть). Rollup-сборка — @@ -3543,7 +3638,10 @@ function buildBundle(dir) { if (built.status !== 0) { throw new Error(`сборка мутанта упала:\n${(built.stderr || built.stdout).slice(-2000)}`); } - cpSync(join(dir, 'dist', 'houseplan-card.js'), join(dir, 'demo', 'srv', 'assets', 'houseplan-card.js')); + const synced = sh('node scripts/bundle-sync.mjs', dir); + if (synced.status !== 0) { + throw new Error(`дерево бандла мутанта не синхронизировалось:\n${(synced.stderr || synced.stdout).slice(-2000)}`); + } } function runMutant(mutant) { diff --git a/scripts/release-prerelease.mjs b/scripts/release-prerelease.mjs index c66ad5e4..03b0643b 100644 --- a/scripts/release-prerelease.mjs +++ b/scripts/release-prerelease.mjs @@ -252,17 +252,33 @@ if (invokedDirectly) { // Копия стенда (`demo/srv/assets`) больше не коммитится (#255): её собирает // `npm run bundle:sync` перед браузерными прогонами. В релиз входят ровно // две копии — артефакт сборки и та, что ставит HACS. - const names = [ - 'dist/houseplan-card.js', - 'custom_components/houseplan/frontend/houseplan-card.js', - ]; - // Hash Git blobs, not checkout bytes. On Windows, Git can expose CRLF in - // the worktree while the exact tagged blobs and Linux release checkout use - // LF. The release must be bound to the immutable commit representation. - const hashes = names.map((name) => [name, sha256Bytes(committedFile(sha, name))]); - if (new Set(hashes.map(([, hash]) => hash)).size !== 1) - throw new Error(`Committed bundle snapshots differ: ${hashes.map(([name, hash]) => `${name}=${hash}`).join(', ')}`); - return hashes[0][1]; + const distManifestBytes = committedFile(sha, 'dist/houseplan-assets.json'); + const frontendManifestBytes = committedFile( + sha, 'custom_components/houseplan/frontend/houseplan-assets.json', + ); + if (!distManifestBytes.equals(frontendManifestBytes)) { + throw new Error('Committed bundle manifests differ'); + } + const manifest = JSON.parse(distManifestBytes.toString('utf8')); + if (manifest?.schema !== 1 || !Array.isArray(manifest.files) + || manifest.entry !== 'houseplan-card.js') { + throw new Error('Committed bundle manifest is invalid'); + } + for (const file of manifest.files) { + if (typeof file?.path !== 'string' || !file.path.endsWith('.js') + || file.path.includes('..') || file.path.includes('\\')) { + throw new Error(`Committed bundle asset path is invalid: ${String(file?.path)}`); + } + const dist = committedFile(sha, `dist/${file.path}`); + const frontend = committedFile(sha, `custom_components/houseplan/frontend/${file.path}`); + if (!dist.equals(frontend)) throw new Error(`Committed bundle asset differs: ${file.path}`); + const actual = sha256Bytes(dist); + if (actual !== file.sha256) { + throw new Error(`Committed bundle asset hash mismatch: ${file.path}`); + } + } + const entry = manifest.files.find((file) => file.path === manifest.entry); + return { manifest, entrySha256: entry.sha256 }; }; const materializeCommittedBundle = (sha, expectedSha256, artifactsDir) => { @@ -275,18 +291,31 @@ if (invokedDirectly) { return bundlePath; }; - const verifyZipContents = (zipPath, version, bundleSha256) => { - const entries = readZipEntries(zipPath, ['manifest.json', 'frontend/houseplan-card.js']); + const verifyZipContents = (zipPath, version, bundleSnapshot) => { + const header = readZipEntries(zipPath, [ + 'manifest.json', 'frontend/houseplan-assets.json', + ]); + const frontendManifest = JSON.parse(header.get('frontend/houseplan-assets.json').toString('utf8')); + if (JSON.stringify(frontendManifest) !== JSON.stringify(bundleSnapshot.manifest)) { + throw new Error('houseplan.zip frontend manifest differs from committed bundle'); + } + const required = [ + 'manifest.json', 'frontend/houseplan-assets.json', + ...bundleSnapshot.manifest.files.map((file) => `frontend/${file.path}`), + ]; + const entries = readZipEntries(zipPath, required); const manifest = JSON.parse(entries.get('manifest.json').toString('utf8')); if (manifest.version !== version) throw new Error(`houseplan.zip manifest version ${manifest.version} != ${version}`); - const bundledHash = createHash('sha256') - .update(entries.get('frontend/houseplan-card.js')).digest('hex'); - if (bundledHash !== bundleSha256) - throw new Error(`houseplan.zip frontend hash ${bundledHash} != committed bundle ${bundleSha256}`); + for (const file of bundleSnapshot.manifest.files) { + const bundledHash = sha256Bytes(entries.get(`frontend/${file.path}`)); + if (bundledHash !== file.sha256) { + throw new Error(`houseplan.zip frontend hash mismatch: ${file.path}`); + } + } }; - const buildZip = (sha, version, bundleSha256, artifactsDir) => { + const buildZip = (sha, version, bundleSnapshot, artifactsDir) => { const zipPath = resolve(artifactsDir, 'houseplan.zip'); run('git', [ // `git archive` on Windows otherwise applies local core.autocrlf and @@ -294,11 +323,11 @@ if (invokedDirectly) { '-c', 'core.autocrlf=false', 'archive', '--format=zip', `--output=${zipPath}`, `${sha}:custom_components/houseplan`, ]); - verifyZipContents(zipPath, version, bundleSha256); + verifyZipContents(zipPath, version, bundleSnapshot); return zipPath; }; - const verifyRemoteAssetContents = (version, bundleSha256) => { + const verifyRemoteAssetContents = (version, bundleSnapshot) => { const download = mkdtempSync(resolve(tmpdir(), 'houseplan-release-check-')); try { run('gh', [ @@ -308,9 +337,9 @@ if (invokedDirectly) { try { const cardPath = resolve(download, 'houseplan-card.js'); const cardHash = sha256Path(cardPath); - if (cardHash !== bundleSha256) - throw new Error(`Published houseplan-card.js hash ${cardHash} != candidate ${bundleSha256}`); - verifyZipContents(resolve(download, 'houseplan.zip'), version, bundleSha256); + if (cardHash !== bundleSnapshot.entrySha256) + throw new Error(`Published houseplan-card.js hash ${cardHash} != candidate ${bundleSnapshot.entrySha256}`); + verifyZipContents(resolve(download, 'houseplan.zip'), version, bundleSnapshot); } catch (error) { throw new ReleaseAssetContentError( error instanceof Error ? error.message : String(error), @@ -466,7 +495,8 @@ if (invokedDirectly) { const sha = run('git', ['rev-parse', 'HEAD']).stdout; const remoteBranch = run('git', ['rev-parse', `origin/${branch}`]).stdout; if (sha !== remoteBranch) throw new Error(`HEAD ${sha} is not synchronized with origin/${branch} ${remoteBranch}`); - const bundleSha256 = assertBundleSnapshots(sha); + const bundleSnapshot = assertBundleSnapshots(sha); + const bundleSha256 = bundleSnapshot.entrySha256; const validateRuns = assertGreenValidate(sha); validateIssues(); const existingTag = remoteTag(); @@ -519,7 +549,7 @@ if (invokedDirectly) { // A matching name and non-zero size are insufficient: bind both // downloadable assets to this exact candidate before closing issues. try { - verifyRemoteAssetContents(contract.version, bundleSha256); + verifyRemoteAssetContents(contract.version, bundleSnapshot); } catch (error) { if (!(error instanceof ReleaseAssetContentError)) throw error; console.log(`Published release needs stale-asset recovery: ${error.message}`); @@ -534,7 +564,7 @@ if (invokedDirectly) { } } - const zipPath = buildZip(sha, contract.version, bundleSha256, artifactsDir); + const zipPath = buildZip(sha, contract.version, bundleSnapshot, artifactsDir); if (!existingTag.exists) { const local = localTag(); if (local.exists && (!local.annotated || local.commit !== sha)) { @@ -577,7 +607,7 @@ if (invokedDirectly) { const finalTag = remoteTag(); if (!finalTag.exists || finalTag.commit !== sha) throw new Error(`Published tag ${tag} no longer resolves to exact SHA ${sha}`); - verifyRemoteAssetContents(contract.version, bundleSha256); + verifyRemoteAssetContents(contract.version, bundleSnapshot); verifyHacsDiscovery(); finishIssues(published.url); console.log(`Published and content-verified: ${published.url}`); diff --git a/scripts/smoke-links.mjs b/scripts/smoke-links.mjs index fb2c7acd..76ecda68 100644 --- a/scripts/smoke-links.mjs +++ b/scripts/smoke-links.mjs @@ -34,6 +34,16 @@ export const SMOKE_LINKS = [ + 'fail-closed candidate) through the rendered card, never naming the key-quantisation ' + 'internals that decide which nodes are one node (#331 §2.1)', }, + { + symbols: [ + 'EditorRuntimeLoader', 'EditorRuntimeLoaderState', 'EditorRuntimeModule', + 'EDITOR_RUNTIME_FINGERPRINT', 'createHouseplanEditorRuntime', + ], + smokes: ['smoke_lazy_editor_chunk.mjs'], + because: 'the production-bundle scenario proves that the content-hashed editor chunk stays ' + + 'off the View network path, is shared by all three editors, retries once, and refuses an ' + + 'incompatible build before editor state is installed (#337)', + }, { symbols: [ 'placeResizeAreaLabel', 'resizeInwardNormal', 'resizeMeasuredEdges', diff --git a/scripts/source-fingerprint.mjs b/scripts/source-fingerprint.mjs index 8b55be00..d05489bd 100644 --- a/scripts/source-fingerprint.mjs +++ b/scripts/source-fingerprint.mjs @@ -18,6 +18,8 @@ const BUILD_INPUTS = [ 'rollup.config.mjs', 'tsconfig.json', 'scripts/source-fingerprint.mjs', + 'scripts/css-template-minifier.mjs', + 'scripts/bundle-manifest.mjs', ]; const fingerprintFiles = (root) => { diff --git a/scripts/verify-houseplan-zip.mjs b/scripts/verify-houseplan-zip.mjs new file mode 100644 index 00000000..da47e479 --- /dev/null +++ b/scripts/verify-houseplan-zip.mjs @@ -0,0 +1,43 @@ +#!/usr/bin/env node +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { readZipEntries } from './release-prerelease.mjs'; +import { readBundleManifest, sha256Bytes } from './bundle-tree.mjs'; + +export function verifyHouseplanZip(zipPath, bundleRoot, expectedVersion = '') { + const expected = readBundleManifest(bundleRoot); + const header = readZipEntries(zipPath, [ + 'manifest.json', 'frontend/houseplan-assets.json', + ]); + const integration = JSON.parse(header.get('manifest.json').toString('utf8')); + if (expectedVersion && integration.version !== expectedVersion) { + throw new Error(`houseplan.zip manifest version ${integration.version} != ${expectedVersion}`); + } + const bundled = JSON.parse(header.get('frontend/houseplan-assets.json').toString('utf8')); + if (JSON.stringify(bundled) !== JSON.stringify(expected)) { + throw new Error('houseplan.zip frontend manifest differs from source tree'); + } + const entries = readZipEntries(zipPath, expected.files.map((file) => `frontend/${file.path}`)); + for (const file of expected.files) { + if (sha256Bytes(entries.get(`frontend/${file.path}`)) !== file.sha256) { + throw new Error(`houseplan.zip frontend hash mismatch: ${file.path}`); + } + } + return { version: integration.version, files: expected.files.length }; +} + +const invokedDirectly = process.argv[1] + && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url)); +if (invokedDirectly) { + try { + const [zip, root, version = ''] = process.argv.slice(2); + if (!zip || !root) { + throw new Error('usage: node scripts/verify-houseplan-zip.mjs [version]'); + } + const result = verifyHouseplanZip(resolve(zip), resolve(root), version); + console.log(`verified houseplan.zip ${result.version}: ${result.files} frontend assets`); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; + } +} diff --git a/src/area-format.ts b/src/area-format.ts new file mode 100644 index 00000000..a469f751 --- /dev/null +++ b/src/area-format.ts @@ -0,0 +1,12 @@ +import { polygonArea } from './logic'; + +/** View-safe area helpers kept separate from the lazy resize implementation. */ +export function areaM2(poly: number[][], gridPitch: number, cellCm: number): number { + const cmPerUnit = cellCm / gridPitch; + return (polygonArea(poly) * cmPerUnit * cmPerUnit) / 1e4; +} + +export function formatArea(m2: number, imperial: boolean): string { + if (imperial) return `${Math.round(m2 * 10.7639)} ft²`; + return `${(Math.round(m2 * 10) / 10).toFixed(1)} m²`; +} diff --git a/src/editor-runtime-loader.ts b/src/editor-runtime-loader.ts new file mode 100644 index 00000000..96569444 --- /dev/null +++ b/src/editor-runtime-loader.ts @@ -0,0 +1,72 @@ +export type EditorRuntimeLoaderState = 'idle' | 'loading' | 'ready' | 'failed'; + +export interface EditorRuntimeModule { + readonly fingerprint: string; + create(): Runtime; +} + +export interface EditorRuntimeLoaderOptions { + readonly expectedFingerprint: string; + readonly load: (attempt: 0 | 1) => Promise>; + readonly install: (runtime: Runtime) => void; + readonly stateChanged?: (state: EditorRuntimeLoaderState) => void; + readonly failed?: (error: unknown) => void; +} + +/** + * One atomic lazy-runtime boundary shared by all editor entry points. + * + * A failed module is retried exactly once. Construction happens before + * `install`, so a parse, fingerprint or constructor failure cannot leave a + * half-installed editor attached to the View card. + */ +export class EditorRuntimeLoader { + private _state: EditorRuntimeLoaderState = 'idle'; + private _inFlight: Promise | null = null; + + public constructor(private readonly options: EditorRuntimeLoaderOptions) {} + + public get state(): EditorRuntimeLoaderState { + return this._state; + } + + public ensure(): Promise { + if (this._state === 'ready') return Promise.resolve(true); + if (this._state === 'failed') return Promise.resolve(false); + if (this._inFlight) return this._inFlight; + this._setState('loading'); + this._inFlight = this._loadWithRetry().finally(() => { + this._inFlight = null; + }); + return this._inFlight; + } + + private async _loadWithRetry(): Promise { + let lastError: unknown = new Error('Editor runtime did not load'); + for (const attempt of [0, 1] as const) { + try { + const module = await this.options.load(attempt); + if (module.fingerprint !== this.options.expectedFingerprint) { + throw new Error( + `Editor runtime fingerprint mismatch: expected ${this.options.expectedFingerprint}, got ${module.fingerprint}`, + ); + } + const runtime = module.create(); + this.options.install(runtime); + this._setState('ready'); + return true; + } catch (error: unknown) { + lastError = error; + } + } + this._setState('failed'); + this.options.failed?.(lastError); + return false; + } + + private _setState(state: EditorRuntimeLoaderState): void { + if (state === this._state) return; + this._state = state; + this.options.stateChanged?.(state); + } +} diff --git a/src/houseplan-card.ts b/src/houseplan-card.ts index 933061b6..6997ef71 100755 --- a/src/houseplan-card.ts +++ b/src/houseplan-card.ts @@ -11,10 +11,7 @@ import { guard } from 'lit/directives/guard.js'; import { repeat } from 'lit/directives/repeat.js'; import './hp-dialog'; import type { HpDialog } from './hp-dialog'; -import './hp-color-opacity'; import type { ColorPickerLabels } from './hp-color-opacity'; -import './hp-help'; -import './hp-device-preview'; import { EXCLUDED_DOMAINS, DEFAULT_ICON_RULES, compileIconRules, isValidPattern, iconFor, type IconRule, type CompiledIconRule, @@ -43,12 +40,12 @@ import { import { resolveSafeResize, coalesceResizeRooms, - polyIsSimple, areaM2, formatArea, MIN_ROOM_CM, + polyIsSimple, MIN_ROOM_CM, type SafeOpeningIn, type SafeResizeObstacle, type SafeResizeOptions, type SafeResizePlan, type SafeResizeReason, type SafeResizeResolution, } from './resize'; +import { formatArea } from './area-format'; import { - ResizeController, type ResizeProjectionResult, } from './resize-controller'; import { @@ -70,7 +67,6 @@ import { import { degradeWalls, rekeyWallsAfterMoveChecked, wallRecordCarrierViolations, setWallThickness, setWallThicknessForRoom, cmToField, wallCmToUnits, - multiWallNodesForGeometry, wallEdgeBodies, wallBodiesGeometry, wallBodiesGeometryPath, wallBodiesUnionPath, recutWallBodiesGeometry, floorFootprintGeometry, @@ -178,9 +174,10 @@ import { } from './plan-snap-overlay'; import { atomizeWallSegments, buildWallFaceGraph, findNewWallFacesInGraphs, findWallFaceAtPoint, - normalizeUnifiedWallTool, wallChainSegments, chainSegmentCms, + wallChainSegments, chainSegmentCms, type WallFaceGraph, type WallGraphFace, type WallGraphSourceSegment, } from './wall-face-graph'; +import { normalizeUnifiedWallTool } from './wall-tool-compat'; import { parameterOnPartition, planRoomDeletion } from './room-deletion'; import { planWallFaceRepair, repairMovesHostedPartition, type WallFaceRepairProposal, @@ -188,12 +185,10 @@ import { import { LightSegment, polygonSegments, splitAtIntersections, visibilityPolygon, } from './light-visibility'; -import './editor'; import './space-card'; import { cardStyles } from './styles'; import { editorSecondaryStyles } from './editor-secondary.styles'; import { - EditorSecondaryController, type EditorSecondaryCopy, type EditorSecondaryModel, type EditorToolbarGroup, @@ -207,7 +202,6 @@ import { } from './space-geometry'; import { optimizePlans, type OptimizeReport } from './plan-optimizer'; import { - WALL_SEGMENT_MODEL_VERSION, adoptWallSegmentModelCandidateInPlace, commitWallSegmentModel, fixedTopologyWallLineageHints, sanitizeRoomDraftPath, resolveRoomOpeningHost, wallModelOffGridValueCount, WallSegmentModelError, @@ -275,6 +269,7 @@ import { ModeTransitionController, viewportFromViewBox, type HouseplanMode, type ModeTransitionState, type ModeVisualState, type ModeViewBox, } from './mode-transition'; +import { EditorRuntimeLoader, type EditorRuntimeLoaderState } from './editor-runtime-loader'; import { currentLabs, hashSpace, noteLabsRender, subscribeLabs, type LabsSnapshot, } from './labs'; @@ -332,6 +327,8 @@ import { import { applyOpeningMoves, mergeCollinearPartitions, spaceMergeGeometry } from './wall-merge'; const CARD_VERSION = '1.68.1'; +const ENTRY_BUILD_FINGERPRINT = '__HOUSEPLAN_SOURCE_FINGERPRINT__'; +const EDITOR_RETRY_ASSET = '__HOUSEPLAN_EDITOR_RETRY_ASSET__'; type ResizeLiveLabel = { kind: 'length'; @@ -800,7 +797,105 @@ interface DeviceInboxDialogState { type FixedFloorState = FixedFloorSelection | { kind: 'pending'; value: unknown }; -class HouseplanCard extends LitElement { +export class HouseplanCard extends LitElement { + public _editorRuntime: import('./houseplan-editor-runtime').HouseplanEditorRuntime | null = null; + public _onboardingRuntime: import('./houseplan-onboarding-runtime').HouseplanOnboardingRuntime | null = null; + private _editorRuntimeLoadingVisible = false; + private _editorRuntimeLoadingTimer?: number; + private _editorModeRequest = 0; + private readonly _editorRuntimeLoader = new EditorRuntimeLoader< + import('./houseplan-editor-runtime').HouseplanEditorRuntime + >({ + expectedFingerprint: ENTRY_BUILD_FINGERPRINT, + load: async (attempt) => { + const module = attempt === 0 + ? await import('./houseplan-editor-runtime') + : await import(/* @vite-ignore */ (() => { + const url = new URL(EDITOR_RETRY_ASSET, import.meta.url); + url.searchParams.set('hp_retry', CARD_VERSION); + return url.href; + })()) as typeof import('./houseplan-editor-runtime'); + return { + fingerprint: module.EDITOR_RUNTIME_FINGERPRINT, + create: () => new module.HouseplanEditorRuntime( + this as unknown as import('./houseplan-editor-runtime').HouseplanEditorHostPort, + ), + }; + }, + install: (runtime) => { + this._editorRuntime = runtime; + }, + stateChanged: (state) => this._editorRuntimeStateChanged(state), + failed: (error) => { + console.error('[houseplan] unable to load editor runtime', error); + this._showToast(`${this._t('editor.load_failed')} ${this._t('editor.refresh_advice')}`); + }, + }); + + private readonly _onboardingRuntimeLoader = new EditorRuntimeLoader< + import('./houseplan-onboarding-runtime').HouseplanOnboardingRuntime + >({ + expectedFingerprint: ENTRY_BUILD_FINGERPRINT, + load: async (attempt) => { + const module = attempt === 0 + ? await import('./houseplan-onboarding-runtime') + : await import(/* @vite-ignore */ (() => { + const url = new URL('__HOUSEPLAN_ONBOARDING_RETRY_ASSET__', import.meta.url); + url.searchParams.set('hp_retry', CARD_VERSION); + return url.href; + })()) as typeof import('./houseplan-onboarding-runtime'); + return { + fingerprint: module.ONBOARDING_RUNTIME_FINGERPRINT, + create: () => new module.HouseplanOnboardingRuntime( + this as unknown as import('./houseplan-editor-runtime').HouseplanEditorHostPort, + ), + }; + }, + install: (runtime) => { + this._onboardingRuntime = runtime; + }, + failed: (error) => { + console.error('[houseplan] unable to load onboarding runtime', error); + this._showToast(`${this._t('editor.load_failed')} ${this._t('editor.refresh_advice')}`); + }, + }); + + public _editorRuntimeOrThrow(): import('./houseplan-editor-runtime').HouseplanEditorRuntime { + if (!this._editorRuntime) throw new Error('Houseplan editor runtime is not loaded'); + return this._editorRuntime; + } + + public async _ensureEditorRuntime(): Promise { + return this._editorRuntimeLoader.ensure(); + } + + public async _ensureOnboardingRuntime(): Promise { + return this._onboardingRuntimeLoader.ensure(); + } + + private _editorRuntimeStateChanged(state: EditorRuntimeLoaderState): void { + clearTimeout(this._editorRuntimeLoadingTimer); + this._editorRuntimeLoadingTimer = undefined; + if (state === 'loading') { + this._editorRuntimeLoadingTimer = window.setTimeout(() => { + this._editorRuntimeLoadingVisible = true; + this.requestUpdate(); + }, 150); + return; + } + this._editorRuntimeLoadingVisible = false; + this.requestUpdate(); + } + + public async _requestMode( + mode: 'view' | 'plan' | 'devices' | 'decor', + animate = true, + ): Promise { + const request = ++this._editorModeRequest; + if (mode !== 'view' && !(await this._ensureEditorRuntime())) return; + if (request !== this._editorModeRequest) return; + this._setMode(mode, animate); + } public hass?: any; private _config?: CardConfig; @@ -1013,12 +1108,7 @@ class HouseplanCard extends LitElement { }); /** Explicit second-level toolbar group. No current tools are grouped yet; * the shared host/API prevents future editors from inventing dropdowns. */ - private readonly _editorSecondary = new EditorSecondaryController({ - root: () => this.renderRoot as ShadowRoot, - requestUpdate: () => this.requestUpdate(), - updateComplete: () => this.updateComplete, - clearTip: () => { this._tip = null; }, - }); + private declare _editorSecondary: import('./editor-secondary').EditorSecondaryController; private readonly _editorSecondaryCopy: EditorSecondaryCopy = { groupActive: (group, item) => this._t('editor.group_active', { group, item }), openGroup: (group) => this._t('editor.open_group', { group }), @@ -1329,7 +1419,7 @@ class HouseplanCard extends LitElement { this._closingWallCm = null; this._selId = null; this._physicalSel = null; - this._editorSecondary.closeForNavigation(); + this._editorSecondary?.closeForNavigation(); this._physicalDialog = null; this._physicalDrag = null; if (this._mode === 'plan' && this._tool === 'draw') this._resumeLastDraft(); @@ -1697,9 +1787,9 @@ class HouseplanCard extends LitElement { private _duplicateColumnId: string | null = null; private _duplicateColumnTimer = 0; // Room Resize orchestration is isolated from this Lit/config composition shell (#264). - private _resize = new ResizeController< + private declare _resize: import('./resize-controller').ResizeController< ResizePreview, ResizeLiveLabel[], SpaceGeometryState, ResizeWallUnion, ResizeWallArtifact - >(); + >; /** #329 AC7a: the limit the LAST projection broke, or null if it broke none. */ private _rszLimitViolation: JunctionLimitViolation | null = null; private _path: number[][] = []; // current outline (render units, vertices snapped to the grid) @@ -2255,6 +2345,7 @@ class HouseplanCard extends LitElement { _bootFading: { state: true }, _bootSoft: { state: true }, _continuityEpoch: { state: true }, + _editorRuntimeLoadingVisible: { state: true }, _tapConfirm: { state: true }, hass: { attribute: false }, _config: { state: true }, @@ -2456,6 +2547,9 @@ class HouseplanCard extends LitElement { this._signer.dispose(); clearTimeout(this._toastTimer); clearTimeout(this._slideTimer); + clearTimeout(this._editorRuntimeLoadingTimer); + this._editorRuntimeLoadingTimer = undefined; + this._editorRuntimeLoadingVisible = false; this._modeTransition.dispose(); this._modeTransitionVisual = null; this._modeTransitionPreparing = false; @@ -2536,7 +2630,7 @@ class HouseplanCard extends LitElement { this._pointerHoverObserver?.disconnect(); this._pointerHoverObserver = undefined; this._pointerModality.disconnect(); - this._editorSecondary.reset(); + this._editorSecondary?.reset(); // R1: the rAF is the only normal owner that clears this flag. Reset only // after the disconnect snapshot (which must still skip unstable geometry), // so a same-element reattach can start fresh instead of keeping the veil. @@ -2580,9 +2674,9 @@ class HouseplanCard extends LitElement { this._importTotal = 0; return; } - if (this._editorSecondary.hasOpenGroup) { + if (this._editorSecondary?.hasOpenGroup) { e.preventDefault(); - this._editorSecondary.closeGroup(true); + this._editorSecondary?.closeGroup(true); return; } } @@ -2657,7 +2751,7 @@ class HouseplanCard extends LitElement { } if (undo) { e.preventDefault(); - if (this._resize.dragging) { + if (this._resize?.dragging) { this._rszCancelDrag(); return; } @@ -2710,12 +2804,12 @@ class HouseplanCard extends LitElement { } if (this._tool === 'resize') { e.preventDefault(); - if (this._resize.dragging) { + if (this._resize?.dragging) { // Esc mid-drag: the immutable preview is simply discarded this._rszCancelDrag(); return; } - if (this._resize.escapeIdle() === 'exit-tool') this._tool = 'draw'; + if (this._resize?.escapeIdle() === 'exit-tool') this._tool = 'draw'; this.requestUpdate(); return; } @@ -2826,7 +2920,8 @@ class HouseplanCard extends LitElement { this._showToast(this._t('history.undone', { name: command.name })); } - public static getConfigElement() { + public static async getConfigElement() { + await import('./editor'); return document.createElement('houseplan-card-editor'); } @@ -3105,7 +3200,7 @@ class HouseplanCard extends LitElement { this._showHidden = vp.showHidden; if (this._showFar !== vp.showFar) { this._showFar = vp.showFar; this._frame = null; } this._selId = vp.selId; - this._resize.restoreSelection(vp.rszSel); + this._resize?.restoreSelection(vp.rszSel); this._decorSel = vp.decorSel; this._warmVpArmed = true; // _loadFromServer must not re-centre this } @@ -3160,7 +3255,7 @@ class HouseplanCard extends LitElement { showHidden: this._showHidden, showFar: this._showFar, selId: this._selId, - rszSel: this._resize.selectedRoomId, + rszSel: this._resize?.selectedRoomId, decorSel: this._decorSel, }; } @@ -3488,6 +3583,15 @@ class HouseplanCard extends LitElement { `; } + private _renderEditorRuntimeLoading(): TemplateResult | typeof nothing { + if (!this._editorRuntimeLoadingVisible) return nothing; + return html`
+ + ${this._t('editor.loading')} +
`; + } + /** Redacted lifecycle diagnostics used by the deterministic sampler. */ public houseplanContinuityTrace(): readonly import('./visual-continuity').ContinuityTraceEvent[] { return [...this._continuityHistory, ...this._continuity.trace].slice(-80) @@ -3631,7 +3735,7 @@ class HouseplanCard extends LitElement { this._decorTextDialog = null; this._roomDialog = false; if (this._spaceDialog?.mode === 'edit') this._spaceDialog = null; - this._editorSecondary.closeForNavigation(); + this._editorSecondary?.closeForNavigation(); this._saveConfigDebounced.cancel(); this._frame = null; this._planSnapGeometryCache = null; @@ -3757,7 +3861,8 @@ class HouseplanCard extends LitElement { this._pruneDevicePressFeedback(); this._syncDayCycleClock(); this._warmSnapshot(); // DEV-B703-03: the memo follows what is on screen - this._dtMeasure(); // the selected label's frame follows the glyphs + // Decor selection cannot exist before the lazy editor runtime is ready. + if (this._editorRuntime) this._dtMeasure(); const stage = this._stageEl; if (stage && !this._roViewport) { this._roViewport = new ResizeObserver(() => this._refitView()); @@ -3802,7 +3907,7 @@ class HouseplanCard extends LitElement { measure(); } if (stage && !this._view) this._refitView(); - this._editorSecondary.afterRender(); + this._editorSecondary?.afterRender(); this._settleContinuityFrame(); // onboarding: on an empty server config, open the space dialog right away if ( @@ -5204,11 +5309,7 @@ class HouseplanCard extends LitElement { /** Localize both parts of a help affordance while hp-help stays presentation-only. */ private _help(key: Extract): TemplateResult | typeof nothing { - const ariaKey = `${key}.aria` as I18nKey; - const lang = langOf(this.hass, this._config?.language); - if (!hasTranslation(lang, key) || !hasTranslation(lang, ariaKey)) return nothing; - return html``; + return this._editorRuntimeOrThrow()._help(key); } private get _stageEl(): HTMLElement | null { @@ -6266,7 +6367,7 @@ class HouseplanCard extends LitElement { * `click` after the final pointerup rather than in the same task. */ private _guardTouchGesture(ev: Event): void { - if (this._editorSecondary.handleOutsideDismiss(ev)) return; + if (this._editorSecondary?.handleOutsideDismiss(ev)) return; if (ev.type === 'click') { if (!this._suppressClick && !this._touchSequenceMultitouch && Date.now() > this._touchClickBlockUntil) return; @@ -6392,14 +6493,14 @@ class HouseplanCard extends LitElement { // HP-1550-01: while a resize drag is live, every render-path reader sees // the preview overlay; _writeConfig deliberately reads _serverCfg instead, // so a queued write can never pick the preview up. - const pv = this._resize.preview; + const pv = this._resize?.preview; if (pv && pv.space === this._space) return pv.sp; return this._serverCfg?.spaces.find((s: any) => s.id === this._space); } /** The config AS RENDERED: _serverCfg with the live resize preview substituted in. */ private get _renderCfg(): ServerConfig | null { - const pv = this._resize.preview; + const pv = this._resize?.preview; if (!pv || !this._serverCfg) return this._serverCfg; return { ...this._serverCfg, @@ -6508,217 +6609,21 @@ class HouseplanCard extends LitElement { } private _setMode(mode: 'view' | 'plan' | 'devices' | 'decor', animate = true): void { - this._endTabDrag(); - // 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 - // the new instance already fails closed to `view`. Its close button must - // cancel the deferred editor in one press, not let the server response - // reopen it and force a second press (#95). - this._pendingNavMode = null; - const space = this._spaceModel(); - if (!space) { - this._cancelModeTransition(false); - this._mode = 'view'; - return; - } - if (this._kiosk && mode !== 'view') return; // wall devices never edit - if (this._mode === mode) { - if (!animate && mode === 'view' && this._modeTransitionBusy) { - const from = this._currentModeVisual(mode); - const targetZoom = this._modeTransitionTargetZoom; - const targetCenterX = this._modeTransitionTargetCenterX; - const targetCenterY = this._modeTransitionTargetCenterY; - this._cancelModeTransition(false); - this._commitViewModeAtomic(from, targetZoom, targetCenterX, targetCenterY); - } - return; - } - this._bootSoftCancel(); // navigation owns its own short, bounded transition - if ((mode === 'plan' || mode === 'decor') && !this._norm) { - this._showToast(this._t('toast.markup_needs_server')); - return; - } - if (this._wallFaceBatch) this._roomDialogCancel(); - if (this._mode === 'plan' && this._tool === 'draw' && !this._finishWallChain()) return; - this._clearTransientHover(true); - this._cancelDevicePressFeedback(); - const previousMode = this._mode; - const previousProjection = this._effectiveProjection(); - const capturedVisual = this._currentModeVisual(previousMode); - const retargeting = this._modeTransitionBusy; - // The target toolbar replaces the old editor markup during the hidden - // measurement frame. Give that incoming content its own coordinate on the - // shared controller so editor-to-editor switches do not pop abruptly. - const fromVisual = capturedVisual && previousMode !== 'view' && mode !== 'view' - ? { ...capturedVisual, toolbarContentOpacity: this._reducedMotion ? 1 : 0.35 } - : capturedVisual; - this._cancelModeTransition(false); - this._editorSecondary.closeForNavigation(); - // A live decor transform is a transaction. Switching tabs must cancel and - // restore it, not merely forget its pointer record after the config has - // already been mutated by move/resize. - if (this._decorMove || this._dtDrag || this._bdDrag) this._cancelDecorGesture(); - const baseChanges = !space.bg && (mode === 'view') !== (previousMode === 'view'); - // A running/preparing transition has not committed its target to `_zoom` - // yet. Retarget from the painted frame, but preserve the latest camera - // intent; otherwise rapid View -> editor A -> editor B resurrects the old - // View zoom/center and settles somewhere a direct transition never would. - const returningToEditor = retargeting && previousMode === 'view' && mode !== 'view' - ? this._modeTransitionEditorCamera : null; - let targetZoom = returningToEditor?.zoom - ?? (retargeting ? this._modeTransitionTargetZoom : this._zoom); - let targetCenterX = returningToEditor - ? returningToEditor.centerX - : retargeting ? this._modeTransitionTargetCenterX : fromVisual?.viewport.centerX; - let targetCenterY = returningToEditor - ? returningToEditor.centerY - : retargeting ? this._modeTransitionTargetCenterY : fromVisual?.viewport.centerY; - if (previousMode === 'view' && mode !== 'view' && !retargeting) { - // remember the view-mode viewport: whatever zooming happens inside the - // editors is a working tool, not what the user wants to see afterwards - const v = this._view; - this._viewModeSnap = { - space: this._space, - zoom: this._zoom, - cx: v ? this._logicalViewCenter(previousProjection)?.x : undefined, - cy: v ? this._logicalViewCenter(previousProjection)?.y : undefined, - }; - if (previousProjection === 'iso') { - const logical = this._logicalViewCenter('iso'); - targetCenterX = logical?.x; - targetCenterY = logical?.y; - this._view = null; - this._mode = mode; - this._applyView(this._zoom, logical?.x, logical?.y); - } - if (baseChanges) { - targetZoom = 1; - targetCenterX = undefined; - targetCenterY = undefined; - } - } - if (previousMode === 'plan' && this._activeDraftId) - this._resumeDraftBySpace[this._space] = this._activeDraftId; - this._mode = mode; - if (previousMode === 'devices' && mode !== 'devices') { - this._showHidden = false; - this._deviceInbox = null; - this._deviceInboxReturn = null; - this._deviceInboxMemo = null; - } - this._editorChromeMode = mode === 'view' ? previousMode as 'plan' | 'devices' | 'decor' : mode; - if (mode === 'view') { - if (previousMode !== 'view' && !retargeting) { - this._modeTransitionEditorCamera = { - zoom: this._zoom, - centerX: fromVisual?.viewport.centerX, - centerY: fromVisual?.viewport.centerY, - }; - } - const snap = this._viewModeSnap; - // restore the snapshot only for the space it was taken in - if (snap && snap.space === this._space) { - targetZoom = snap.zoom; - const targetProjection = this._labsIso && this._viewPreference[this._space] === 'iso' - ? 'iso' : 'flat'; - const center = snap.cx != null && snap.cy != null && targetProjection === 'iso' - ? projectPlanPoint([snap.cx, snap.cy], 0) : null; - targetCenterX = center?.[0] ?? snap.cx; - targetCenterY = center?.[1] ?? snap.cy; - } else if (snap) { - // HP-1543-01: the floor CHANGED inside the editor — the snapshot - // belongs to the floor the editor was entered from, while _zoom still - // carries the editor's working zoom for the floor we are exiting on. - // Dropping the snapshot alone left that editor zoom (say 500%) on - // screen in view mode. The per-space store was never polluted - // (_saveZoom is view-only), so the standard centred - // _restoreZoom() puts back this floor's saved VIEW viewport. Its rAF - // reads _zoomBySpace, not the snapshot, so the same-tick floor-tab - // race the view-only guard protects against stays closed. - targetZoom = this._zoomBySpace[this._space] || 1; - targetCenterX = undefined; - targetCenterY = undefined; - } - } else { - this._modeTransitionEditorCamera = { - zoom: targetZoom, centerX: targetCenterX, centerY: targetCenterY, - }; - } - this._modeTransitionTargetZoom = targetZoom; - this._modeTransitionTargetCenterX = targetCenterX; - this._modeTransitionTargetCenterY = targetCenterY; - if (previousProjection === 'iso' - || (mode === 'view' && this._labsIso && this._viewPreference[this._space] === 'iso')) { - this._modeTransitionForceAtomic = true; - } - const request = ++this._modeTransitionRequest; - if (!animate) { - // The only non-animated caller is route departure; keep the guard local - // so a future editor caller cannot accidentally use View geometry. + if (!this._editorRuntime) { if (mode === 'view') { - this._commitViewModeAtomic(fromVisual, targetZoom, targetCenterX, targetCenterY); + // Cancel a pending editor intent without disturbing the unchanged View. + this._editorModeRequest++; + return; } - } else if (fromVisual) { - this._modeTransitionPreparing = true; - this._modeTransitionVisual = fromVisual; - this._prepareModeTransition( - request, fromVisual, mode, targetZoom, targetCenterX, targetCenterY, - ); - } else { - // A zero-sized stage cannot animate. Keep the functional fallback - // atomic and publish the exact target as soon as it is measurable. - this._modeTransitionPreparing = false; - this._modeTransitionVisual = null; - this._zoom = targetZoom; - this._view = null; - requestAnimationFrame(() => { - if (!this.isConnected || request !== this._modeTransitionRequest || this._mode !== mode) return; - this._applyView(targetZoom, targetCenterX, targetCenterY); - if (mode === 'view') { - this._viewModeSnap = null; - this._modeTransitionEditorCamera = null; - } - this.requestUpdate(); - }); + void this._requestMode(mode, animate); + return; } - this._path = []; - this._clearPlanSnapHover(); - this._clearOpeningPlacement(true); - this._tool = 'draw'; - this._mergeSel = null; - this._mergeDialog = null; - this._splitSel = null; - this._pendingSplit = null; - this._selId = null; - this._physicalSel = null; - this._physicalDialog = null; - this._physicalDrag = null; - this._rszResetController(); - this._tip = null; - this._hoverRoom = null; - this._decorDraft = null; - this._decorSel = null; - this._decorMove = null; - this._backdropDialog = null; - // Every Background session starts predictably on Select. The image has an - // explicit tool of its own; only that tool may claim its body or frame. - this._decorTool = 'select'; - this._bdDrag = null; - this._dtDrag = null; - this._dtBox = null; - if (mode === 'plan') { - this._primeDrawWallField(); - this._resumeLastDraft(); - } - this._saveNav(); + return this._editorRuntime._setMode(mode, animate); } /** Prime the Draw thickness field to 15 cm once per Plan session. */ private _primeDrawWallField(): void { - if (this._drawWallField === null) { - this._drawWallField = cmToField(DRAW_WALL_DEFAULT_CM, this._imperial); - } + return this._editorRuntimeOrThrow()._primeDrawWallField(); } private get _drawWallFieldValue(): string { @@ -6742,17 +6647,11 @@ class HouseplanCard extends LitElement { } private _showPhysicalRange(max = this._drawWallMaxCm, min = 0): void { - this._showToast(this._t('toast.physical_range', { - min: cmToField(min, this._imperial), - max: cmToField(max, this._imperial), - unit: this._t(this._imperial ? 'wallthick.unit_in' : 'wallthick.unit_cm'), - })); + return this._editorRuntimeOrThrow()._showPhysicalRange(max, min); } private _draftSegmentCount(sp = this._curSpaceCfg as any): number { - return (sp?.room_drafts || []).reduce( - (sum: number, d: any) => sum + (Array.isArray(d.segments) ? d.segments.length : 0), 0, - ); + return this._editorRuntimeOrThrow()._draftSegmentCount(sp); } /** @@ -6770,116 +6669,25 @@ class HouseplanCard extends LitElement { * `x/y/angle` projection that older readers still consume (#132). */ private _mergeSpacePartitions(sp: any, seedIds?: string[]): number { - const partitions = (sp?.partitions || []) as PartitionCfg[]; - if (partitions.length < 2) return 0; - const result = mergeCollinearPartitions(partitions, { - pitch: GRID_STEP_N, - seedIds, - geometry: spaceMergeGeometry(sp, { excludeDraftId: this._activeDraftId }), - }); - if (!result.merged) return 0; - sp.partitions = result.partitions; - applyOpeningMoves(sp.openings, sp.partitions, result.openingMoves, { - coordScale: NORM_W, cellCm: this._cellCm, gridPitch: this._gridPitch, - }); - return result.merged; + return this._editorRuntimeOrThrow()._mergeSpacePartitions(sp, seedIds); } private _finishWallChain(): boolean { - if (this._tool !== 'draw' || this._wallFaceBatch || this._roomDialog) return true; - const sp = this._curSpaceCfg as any; - if (!sp || this._path.length < 2) { - this._path = []; - this._activeDraftId = null; - this._draftSegmentCms = []; - this._closingWallCm = null; - this._clearPlanSnapHover(); - return true; - } - const segments = wallChainSegments( - this._path, - chainSegmentCms( - this._path.length - 1, this._draftSegmentCms, - this._drawWallCm, DRAW_WALL_DEFAULT_CM, - ), - ); - const segmentCount = segments.length; - if ((sp.partitions || []).length + segmentCount > MAX_PARTITIONS) { - this._showToast(this._t('toast.physical_limit')); - return false; - } - const before = this._geometrySnapshot(); - sp.partitions ||= []; - const seed = Date.now().toString(36); - const drawnIds: string[] = []; - const activeDraft = this._activeDraftId - ? (sp.room_drafts || []).find((draft: any) => draft.id === this._activeDraftId) - : null; - const lineage = this._draftSegmentsForPath( - this._path, activeDraft, segments.map((segment) => segment.cm), - ); - for (let i = 0; i < segmentCount; i++) { - const segment = segments[i]; - const id = lineage[i]?.id || `partition-${seed}-${i}`; - drawnIds.push(id); - sp.partitions.push({ - id, - a: [segment.a[0] / NORM_W, segment.a[1] / NORM_W], - b: [segment.b[0] / NORM_W, segment.b[1] / NORM_W], - cm: segment.cm, - }); - } - // A straight run drawn in several clicks is one wall, not five (#229). - // Only what this chain touches is merged: what has piled up earlier waits - // for «Optimise plans», where the sweep is explicit and undoable. - this._mergeSpacePartitions(sp, drawnIds); - if (this._activeDraftId && Array.isArray(sp.room_drafts)) { - sp.room_drafts = sp.room_drafts.filter((draft: any) => draft.id !== this._activeDraftId); - if (!sp.room_drafts.length) delete sp.room_drafts; - } - this._commitPhysicalGeometry(this._t('history.wall_chain_finish'), before); - delete this._resumeDraftBySpace[this._space]; - this._path = []; - this._activeDraftId = null; - this._draftSegmentCms = []; - this._closingWallCm = null; - this._clearPlanSnapHover(); - return true; + return this._editorRuntimeOrThrow()._finishWallChain(); } /** One transition gate for Plan toolbar tools. */ private _activateMarkupTool(tool: MarkupTool): void { - if (tool === this._tool) return; - if (this._wallFaceBatch) this._roomDialogCancel(); - if (this._tool === 'draw' && !this._finishWallChain()) return; - else this._cancelPath(); - if (this._tool === 'resize') { - if (this._resize.dragging) this._rszCancelDrag(); - else this._resize.reset(); - } - this._tool = tool; - if (tool === 'draw') this._resumeLastDraft(); - if (tool === 'resize') this._resize.selectRoom(null); - if (tool === 'wallthick') this._wallDialog = null; + return this._editorRuntimeOrThrow()._activateMarkupTool(tool); } private _limitReached(kind: 'draft' | 'partition' | 'column'): boolean { - const sp = this._curSpaceCfg as any; - if (!sp) return true; - const reached = kind === 'draft' - ? (sp.room_drafts || []).length >= MAX_ROOM_DRAFTS - : kind === 'partition' - ? (sp.partitions || []).length >= MAX_PARTITIONS - : (sp.wall_columns || []).length >= MAX_WALL_COLUMNS; - if (reached) this._showToast(this._t('toast.physical_limit')); - return reached; + return this._editorRuntimeOrThrow()._limitReached(kind); } private _svgPoint(ev: MouseEvent): number[] { - const stage = this.renderRoot.querySelector('.stage') as HTMLElement; - const r = stage.getBoundingClientRect(); - return this._screenToVb(ev.clientX - r.left, ev.clientY - r.top); + return this._editorRuntimeOrThrow()._svgPoint(ev); } /** @@ -6889,82 +6697,29 @@ class HouseplanCard extends LitElement { * to bump into any more, only the ±5000 the backend refuses to store. */ private _snap(p: number[]): number[] { - const g = this._gridPitch; - return [clampCanvasR(snapToGrid(p[0], g)), clampCanvasR(snapToGrid(p[1], g))]; + return this._editorRuntimeOrThrow()._snap(p); } /** Grid snap for the free end of the wall currently being drawn. */ private _snapDrawPoint(p: number[], lock45 = false): number[] { - const anchor = this._path[this._path.length - 1]; - const candidate = lock45 && anchor - ? snapSegment45(anchor, p, this._gridPitch, SANE_LIMIT) - : p; - return this._snap(candidate); + return this._editorRuntimeOrThrow()._snapDrawPoint(p, lock45); } /** Canonical physical opening slots on room-wall centrelines. */ private _planSnapOpeningCuts(space: SpaceModel, openCuts: number[][]): number[][] { - if (!this._openingsR.length) return []; - const index = this._openingWallIndexFor(space, openCuts).value; - const cuts: number[][] = []; - for (const input of this._roomWallOpeningInputs(this._openingsR, space)) { - const association = resolveOpeningWallAssociation(index, input); - if (!association.negative && !association.positive) continue; - const rad = input.angle * Math.PI / 180; - const dx = Math.cos(rad) * input.length / 2; - const dy = Math.sin(rad) * input.length / 2; - cuts.push([input.x - dx, input.y - dy, input.x + dx, input.y + dy]); - } - return cuts; + return this._editorRuntimeOrThrow()._planSnapOpeningCuts(space, openCuts); } /** Static presentation axes shared by the overlay and its hit resolver. */ private _planSnapGeometrySnapshot(): { key: string; value: PlanSnapGeometry } { - const space = this._spaceModel(); - if (!space) { - return { key: `${this._space}|empty`, value: { segments: [], endpoints: [] } }; - } - const key = [ - this._space, this._cfgEpoch, this._activeDraftId || '', - space.rooms.length, space.room_drafts.length, space.partitions.length, - ].join('|'); - if (this._planSnapGeometryCache?.key === key) return this._planSnapGeometryCache; - const zeroCuts = this._openCuts(); - const value = buildPlanSnapGeometry({ - space, - activeDraftId: this._activeDraftId, - // A zero-thickness wall is still a canonical wall axis and snap target. - // Only an actual opening removes the presentation interval (#306). - roomCuts: this._planSnapOpeningCuts(space, zeroCuts), - partitionCuts: this._partitionOpeningCuts(space), - epsilon: this._gridPitch * 0.0002, - }); - this._planSnapGeometryCache = { key, value }; - return this._planSnapGeometryCache; + return this._editorRuntimeOrThrow()._planSnapGeometrySnapshot(); } /** Independent sources hidden under another wall, without snap deduplication. */ private _hiddenWallDiagnosticSnapshot(): { key: string; value: HiddenWallDiagnosticGeometry; } { - const space = this._spaceModel(); - if (!space) { - return { key: `${this._space}|hidden-empty`, value: { segments: [], endpoints: [] } }; - } - const key = [ - 'hidden', this._space, this._cfgEpoch, this._activeDraftId || '', - space.rooms.length, space.room_drafts.length, space.partitions.length, - ].join('|'); - if (this._hiddenWallDiagnosticCache?.key === key) { - return this._hiddenWallDiagnosticCache; - } - const value = buildHiddenWallDiagnosticGeometry({ - space, - activeDraftId: this._activeDraftId, - epsilon: this._gridPitch * 0.0002, - }); - this._hiddenWallDiagnosticCache = { key, value }; - return this._hiddenWallDiagnosticCache; + return this._editorRuntimeOrThrow()._hiddenWallDiagnosticSnapshot(); } /** @@ -6974,34 +6729,11 @@ class HouseplanCard extends LitElement { * Keeping this snapshot separate preserves the established face contract. */ private _planStructuralGeometrySnapshot(): { key: string; value: PlanSnapGeometry } { - const space = this._spaceModel(); - if (!space) { - return { key: `${this._space}|structural-empty`, value: { segments: [], endpoints: [] } }; - } - const key = [ - 'structural', this._space, this._cfgEpoch, this._activeDraftId || '', - space.rooms.length, space.room_drafts.length, space.partitions.length, - ].join('|'); - if (this._planStructuralGeometryCache?.key === key) { - return this._planStructuralGeometryCache; - } - const value = buildPlanSnapGeometry({ - space, - activeDraftId: this._activeDraftId, - epsilon: this._gridPitch * 0.0002, - }); - this._planStructuralGeometryCache = { key, value }; - return this._planStructuralGeometryCache; + return this._editorRuntimeOrThrow()._planStructuralGeometrySnapshot(); } private _planSnapContextKey(geometryKey: string): string { - const first = this._path[0]; - const anchor = this._path[this._path.length - 1]; - return [ - geometryKey, this._tool, this._path.length, - first ? `${first[0]},${first[1]}` : '', - anchor ? `${anchor[0]},${anchor[1]}` : '', - ].join('|'); + return this._editorRuntimeOrThrow()._planSnapContextKey(geometryKey); } private _resolvePlanDrawPoint( @@ -7010,36 +6742,7 @@ class HouseplanCard extends LitElement { point: number[]; candidate: PlanSnapCandidate | null; conflicts: PlanSnapEndpoint[]; ambiguous: boolean; contextKey: string; } { - const snapshot = this._planSnapGeometrySnapshot(); - const anchor = this._path[this._path.length - 1]; - const closure = this._tool === 'draw' && this._path.length >= 3 - ? [{ point: this._path[0], key: 'closure:first-point' }] - : []; - const options = { - tolerance: this._cssPxToRender(12), - distinguishTolerance: this._cssPxToRender(8), - gridStep: this._gridPitch, - excludePoints: anchor ? [anchor] : [], - extraEndpoints: closure, - epsilon: this._gridPitch * 0.0002, - }; - const resolution = lock45 && anchor - ? resolveStrictPlanSnap(snapshot.value, raw, { ...options, anchor }) - : resolvePlanSnapResult(snapshot.value, raw, options); - const candidate = resolution.kind === 'resolved' ? resolution.candidate : null; - const snapped = candidate ? [...candidate.point] : this._snapDrawPoint(raw, lock45); - const point = anchor ? snapNearAxisEndpoint(anchor, snapped) : snapped; - // A nearby topology endpoint loses snap ownership when the drafting rule - // moves the actually persisted point away from it. Preview and click must - // describe the exact same geometry (#290). - const effectiveCandidate = candidate && samePoint(point, candidate.point) ? candidate : null; - return { - point, - candidate: effectiveCandidate, - conflicts: resolution.kind === 'ambiguous' ? resolution.conflicts : [], - ambiguous: resolution.kind === 'ambiguous', - contextKey: this._planSnapContextKey(snapshot.key), - }; + return this._editorRuntimeOrThrow()._resolvePlanDrawPoint(raw, lock45); } private get _activePlanSnapCandidate(): PlanSnapCandidate | null { @@ -7059,10 +6762,12 @@ class HouseplanCard extends LitElement { } private _clearPlanSnapHover(clearCursor = true): void { - this._planSnapHover = null; - this._syncPlanSnapActiveMarker(null); - this._syncPlanSnapConflictMarkers([]); - if (clearCursor) this._cursorPt = null; + if (!this._editorRuntime) { + this._planSnapHover = null; + if (clearCursor) this._cursorPt = null; + return; + } + return this._editorRuntimeOrThrow()._clearPlanSnapHover(clearCursor); } private _samePt(a: readonly number[], b: readonly number[]): boolean { @@ -7075,63 +6780,7 @@ class HouseplanCard extends LitElement { * `partitions` collection and must never be confused with that legacy field. */ private _dropLegacySegments(): void { - // «Ripple only» was removed from the UI: keep old configs readable, then - // materialise the recognisable icon+activity presentation on any write. - for (const marker of this._serverCfg?.markers || []) { - if (marker.display === 'ripple') marker.display = 'icon_ripple'; - } - for (const sp of this._serverCfg?.spaces || []) { - delete (sp as any).segments; - const physicalIds = new Set(); - const validId = (id: any): id is string => typeof id === 'string' - && id.length >= 1 && id.length <= 64 && !physicalIds.has(id); - const keepId = (id: string): boolean => { physicalIds.add(id); return true; }; - const point = (p: any): p is number[] => Array.isArray(p) && p.length === 2 - && p.every((v: any) => Number.isFinite(Number(v)) && Math.abs(Number(v)) <= CANVAS_LIMIT); - if (Array.isArray((sp as any).partitions)) { - (sp as any).partitions = (sp as any).partitions.filter((p: any) => - p && validId(p.id) && point(p.a) && point(p.b) - && Math.hypot(p.a[0] - p.b[0], p.a[1] - p.b[1]) > 1e-9 - && keepId(p.id)) - .map((p: any) => ({ - ...p, - cm: Number.isFinite(Number(p.cm)) ? Math.max(0, Math.min(100, Number(p.cm))) : 15, - })); - if (!(sp as any).partitions.length) delete (sp as any).partitions; - } - if (Array.isArray((sp as any).wall_columns)) { - (sp as any).wall_columns = (sp as any).wall_columns.filter((c: any) => - c && validId(c.id) && point(c.center) && keepId(c.id)) - .map((c: any) => ({ - id: c.id, shape: c.shape === 'circle' ? 'circle' : 'square', - center: [Number(c.center[0]), Number(c.center[1])], - cm: clampColumnCm(Number(c.cm) || 15), - ...(c.shape === 'circle' ? {} : { angle: canonicalColumnAngle(c.angle) }), - })); - if (!(sp as any).wall_columns.length) delete (sp as any).wall_columns; - } - if (Array.isArray((sp as any).room_drafts)) { - (sp as any).room_drafts = (sp as any).room_drafts.filter((d: any) => - d && validId(d.id) && Array.isArray(d.points) - && d.points.length >= 2 && d.points.every(point) && keepId(d.id)) - .map((d: any) => { - const sanitized = sanitizeRoomDraftPath(d); - return { id: d.id, ...sanitized }; - }) - .filter((d: any) => d.points.length >= 2); - if (!(sp as any).room_drafts.length) delete (sp as any).room_drafts; - } - if (Array.isArray(sp.walls)) { - const model = selectSpaceModelById(spaceModels({ spaces: [sp] } as any), sp.id); - const cuts = model - ? resolveZeroWalls(sp, model, NORM_W, GRID_PITCH * 0.02).contour - .map((cut) => cut.map((value) => value / NORM_W)) - : sanitizeOpenSpans((sp as any).open_spans) - .map((e) => [e.a[0], e.a[1], e.b[0], e.b[1]]); - sp.walls = degradeWalls(sp.walls, sp.rooms || [], GRID_STEP_N, 1, cuts); - if (!sp.walls.length) delete sp.walls; - } - } + return this._editorRuntimeOrThrow()._dropLegacySegments(); } /** @@ -7164,34 +6813,12 @@ class HouseplanCard extends LitElement { private _rollbackRejectedPhysicalWrites( entries: Array<[string, { fingerprint: string; before: SpaceGeometryState }]>, ): boolean { - if (!this._serverCfg || !entries.length) return false; - let restored = false; - for (const [spaceId] of entries) { - const pending = this._pendingPhysicalWrites.get(spaceId); - if (!pending) continue; - restored = this._restoreGeometryStateInConfig(this._serverCfg, pending.before) || restored; - this._pendingPhysicalWrites.delete(spaceId); - } - if (!restored) return false; - this._clearGeometryGesture(); - this._geometryHistory.clear(); - this._cfgEpoch++; - this._modelCache = null; - this._wallUnionCache = null; - this._physicalBodiesCache = null; - this._frame = null; - this._regSignature = ''; - this._cfgContentFingerprint = contentFingerprint(this._serverCfg); - this._maybeRebuildDevices(); - this.requestUpdate(); - return true; + return this._editorRuntimeOrThrow()._rollbackRejectedPhysicalWrites(entries); } /** Revalidate the synchronous rollback after every write already queued. */ private async _reloadRejectedPhysicalWrite(): Promise { - const queued = this._writeChain; - await queued.catch(() => undefined); - await this._reloadConfigOnly(true); + return this._editorRuntimeOrThrow()._reloadRejectedPhysicalWrite(); } /** A config write is in flight — the card must not adopt a server revision. */ @@ -7199,63 +6826,37 @@ class HouseplanCard extends LitElement { return this._writesPending > 0; } + /** + * The sole frontend config transport. Keeping the final canonicalization at + * this boundary makes View-owned metadata writes and strict editor writes + * converge on the same payload contract without making View load the editor. + */ + private async _sendConfigCandidate(candidate: ServerConfig): Promise { + const canonicalCandidate = canonicalizeConfigGeometry(candidate); + const response = await this.hass.callWS({ + type: 'houseplan/config/set', config: canonicalCandidate, expected_rev: this._cfgRev, + }); + this._cfgRev = response?.rev ?? this._cfgRev + 1; + } + private _writeConfig(): Promise { + if (this._editorRuntime) return this._editorRuntime._writeConfig(); + // Registry discovery may update shared metadata while the card is still + // display-only. Persist that small View-owned mutation without pulling in + // the editor runtime; strict physical-write validation is impossible here + // because such writes can only be created by an installed editor. this._writesPending++; this._writeChain = enqueueSerializedWrite(this._writeChain, async () => { if (!this._serverCfg) return; - this._dropLegacySegments(); const candidate = canonicalizeConfigGeometry(this._serverCfg); const candidateFingerprint = contentFingerprint(candidate); - const strictEntries = [...this._pendingPhysicalWrites.entries()]; - for (const [spaceId, accepted] of strictEntries) { - const candidateSpace = candidate.spaces.find((space: any) => space.id === spaceId); - const exactFingerprint = spacePhysicalGeometryFingerprint(candidateSpace); - if (exactFingerprint === accepted.fingerprint) continue; - let safe = false; - try { safe = this._checkSpacePhysicalGeometry(candidate, spaceId).ok; } catch { safe = false; } - if (!safe) { - this._restoreGeometryStateLocal(accepted.before); - this._pendingPhysicalWrites.delete(spaceId); - this._geometryHistory.clear(); - this._showToast(this._t('toast.geometry_unsafe')); - throw Object.assign(new Error('unsafe wall geometry'), { code: 'geometry-unsafe' }); - } - accepted.fingerprint = exactFingerprint; - } - // Do not replace the reactive root merely because the pure helper - // returned a clone. Besides an unnecessary render, that used to expose - // unrelated write-time cleanup as a visual change (#224 review H1). - // A real coordinate change still adopts the exact object sent below; - // willUpdate owns the corresponding geometry-epoch bump. if (candidateFingerprint !== contentFingerprint(this._serverCfg)) { this._serverCfg = candidate; } this._cfgContentFingerprint = candidateFingerprint; - let r: any; - try { - r = await this.hass.callWS({ - type: 'houseplan/config/set', config: candidate, expected_rev: this._cfgRev, - }); - } catch (error) { - const rolledBack = this._rollbackRejectedPhysicalWrites(strictEntries); - if (!rolledBack) throw error; - // HA normally rejects with an Error-like object, but integrations and - // test doubles may reject with a string. Never lose the rollback tag: - // the caller still has to schedule an authoritative re-read after the - // synchronous, fail-closed restoration (#314). - const failure: any = error && typeof error === 'object' - ? error : Object.assign(new Error(String(error)), { cause: error }); - failure.physicalGeometryRolledBack = true; - throw failure; - } - this._cfgRev = r?.rev ?? this._cfgRev + 1; - for (const [spaceId, accepted] of strictEntries) { - if (this._pendingPhysicalWrites.get(spaceId)?.fingerprint === accepted.fingerprint) - this._pendingPhysicalWrites.delete(spaceId); - } + await this._sendConfigCandidate(candidate); }); const mine = this._writeChain.finally(() => { this._writesPending--; }); - // keep the chain itself unadorned so the next link waits for the write only return mine; } @@ -7272,295 +6873,65 @@ class HouseplanCard extends LitElement { /** Deep, immutable geometry state from the real config (never Resize preview). */ private _geometrySnapshotFromConfig(config: any, spaceId: string): SpaceGeometryState | null { - const sp = config?.spaces?.find((s: any) => s.id === spaceId); - if (!sp) return null; - const copy = (value: T): T => JSON.parse(JSON.stringify(value)); - const plan_transform: SpaceGeometryState['plan_transform'] = {}; - for (const key of ['plan_x', 'plan_y', 'plan_scale', 'plan_scale_x', 'plan_scale_y', 'plan_angle'] as const) { - if (sp[key] !== undefined) plan_transform[key] = sp[key]; - } - return { - spaceId, - rooms: copy(sp.rooms || []), - ...(Array.isArray(sp.openings) ? { openings: copy(sp.openings) } : {}), - ...(Array.isArray(sp.walls) ? { walls: copy(sp.walls) } : {}), - ...(Array.isArray(sp.wall_segments) ? { wall_segments: copy(sp.wall_segments) } : {}), - ...(Array.isArray((sp as any).open_spans) - ? { open_spans: copy((sp as any).open_spans) } - : {}), - ...(Array.isArray((sp as any).room_drafts) - ? { room_drafts: copy((sp as any).room_drafts) } - : {}), - ...(Array.isArray((sp as any).partitions) - ? { partitions: copy((sp as any).partitions) } - : {}), - ...(Array.isArray((sp as any).wall_columns) - ? { wall_columns: copy((sp as any).wall_columns) } - : {}), - ...(Array.isArray(sp.decor) ? { decor: copy(sp.decor) } : {}), - plan_transform, - }; + return this._editorRuntimeOrThrow()._geometrySnapshotFromConfig(config, spaceId); } private _geometrySnapshot(spaceId = this._space): SpaceGeometryState | null { - return this._geometrySnapshotFromConfig(this._serverCfg, spaceId); + return this._editorRuntimeOrThrow()._geometrySnapshot(spaceId); } /** Finish one geometry transaction and invalidate the redo branch. */ private _recordGeometry(name: string, before: SpaceGeometryState | null): void { - if (!before) return; - const after = this._geometrySnapshot(before.spaceId); - if (!after || JSON.stringify(before) === JSON.stringify(after)) return; - this._geometryHistory.push({ name, before, after }); - this.requestUpdate(); + return this._editorRuntimeOrThrow()._recordGeometry(name, before); } /** Replace only persisted geometry in memory; no history entry and no WS. */ private _restoreGeometryStateInConfig( config: any, state: SpaceGeometryState, preserveIdentityHints = false, ): boolean { - const sp = config?.spaces?.find((space: any) => space.id === state.spaceId); - if (!sp) return false; - const copy = (value: T): T => JSON.parse(JSON.stringify(value)); - const oldRooms = new Map((sp.rooms || []).map((room: any) => [room.id, room])); - sp.rooms = copy(state.rooms); - if (preserveIdentityHints) { - for (const room of sp.rooms) { - const old: any = oldRooms.get(room.id); - if (!Array.isArray(room.wall_ids) && Array.isArray(old?.wall_ids) - && old.wall_ids.length === room.poly?.length) room.wall_ids = copy(old.wall_ids); - } - } - const assign = (key: 'openings' | 'walls' | 'wall_segments' | 'open_spans' | 'room_drafts' - | 'partitions' | 'wall_columns' | 'decor', value: unknown): void => { - if (value !== undefined) (sp as any)[key] = copy(value); - else if (!(preserveIdentityHints && key === 'wall_segments')) delete (sp as any)[key]; - }; - const oldOpenings = new Map((sp.openings || []).map((opening: any) => [opening.id, opening])); - const oldDrafts = new Map((sp.room_drafts || []).map((draft: any) => [draft.id, draft])); - assign('openings', state.openings); - assign('walls', state.walls); - assign('wall_segments', state.wall_segments); - assign('open_spans', state.open_spans); - assign('room_drafts', state.room_drafts); - assign('partitions', state.partitions); - assign('wall_columns', state.wall_columns); - assign('decor', state.decor); - if (preserveIdentityHints) { - for (const opening of sp.openings || []) { - const old: any = oldOpenings.get(opening.id); - if (!opening.host && old?.host?.kind === 'wall') opening.host = copy(old.host); - } - for (const draft of sp.room_drafts || []) { - const old: any = oldDrafts.get(draft.id); - for (let index = 0; index < (draft.segments || []).length; index++) { - if (!draft.segments[index]?.id && old?.segments?.[index]?.id) - draft.segments[index].id = old.segments[index].id; - } - } - } - for (const key of ['plan_x', 'plan_y', 'plan_scale', 'plan_scale_x', 'plan_scale_y', 'plan_angle'] as const) - delete (sp as any)[key]; - Object.assign(sp, copy(state.plan_transform || {})); - return true; + return this._editorRuntimeOrThrow()._restoreGeometryStateInConfig(config, state, preserveIdentityHints); } private _restoreGeometryStateLocal(state: SpaceGeometryState): boolean { - if (!this._restoreGeometryStateInConfig(this._serverCfg, state)) return false; - this._cfgEpoch++; - this._modelCache = null; - this._wallUnionCache = null; - this._physicalBodiesCache = null; - this._frame = null; - this.requestUpdate(); - return true; + return this._editorRuntimeOrThrow()._restoreGeometryStateLocal(state); } /** Shared fail-closed transaction boundary for every physical writer. */ private _wallModelBlockerLabel(error: unknown): string { - const reason = error instanceof WallSegmentModelError ? error.reason : 'invalid-room'; - return this._t(`wall_model.reason.${reason}`); + return this._editorRuntimeOrThrow()._wallModelBlockerLabel(error); } /** A legacy virtual-wall projection is still present only until its first * structural v9 write. Keep its atomic-failure copy distinct from generic * wall-model validation without persisting provenance on the new atoms. */ private _hasLegacyZeroWallFields(config: any = this._serverCfg): boolean { - return (config?.spaces || []).some((space: any) => ( - (Array.isArray(space?.open_spans) && space.open_spans.length > 0) - || (space?.rooms || []).some((room: any) => ( - Array.isArray(room?.open_to) && room.open_to.length > 0 - )) - )); + return this._editorRuntimeOrThrow()._hasLegacyZeroWallFields(config); } private _showWallModelMigrationBlocked(error: unknown): void { - const key = this._hasLegacyZeroWallFields() - ? 'toast.zero_wall_migration_blocked' - : 'toast.wall_model_migration_blocked'; - this._showToast(this._t(key, { reason: this._wallModelBlockerLabel(error) })); + return this._editorRuntimeOrThrow()._showWallModelMigrationBlocked(error); } /** #329: every wall of a space as the limit checks see it. */ private _limitSegmentsOf(space: any): LimitSegment[] { - const segments: LimitSegment[] = []; - for (const segment of (space?.wall_segments || [])) { - if (segment?.a && segment?.b) { - segments.push({ - id: String(segment.id || ''), a: segment.a, b: segment.b, cm: Number(segment.cm), - }); - } - } - for (const partition of (space?.partitions || [])) { - if (partition?.a && partition?.b) { - segments.push({ - id: String(partition.id || ''), a: partition.a, b: partition.b, cm: Number(partition.cm), - }); - } - } - for (const draft of (space?.room_drafts || [])) { - const points = Array.isArray(draft?.points) ? draft.points : []; - const drafted = Array.isArray(draft?.segments) ? draft.segments : []; - for (let index = 0; index + 1 < points.length; index++) { - segments.push({ - id: String(drafted[index]?.id || `${draft?.id || 'draft'}-${index}`), - a: points[index], b: points[index + 1], cm: Number(drafted[index]?.cm), - }); - } - } - return segments; + return this._editorRuntimeOrThrow()._limitSegmentsOf(space); } /** #329 П1-П5 over one space. Pure input, no side effects. */ - private _junctionLimitViolations( - config: any, spaceId: string, - /** #330 §4.7: an already computed masonry pass of THIS config (e.g. the - * resize preflight's artifact) — the union is never paid twice. */ - sharedGeometry?: any, - ): JunctionLimitViolation[] { - const space = (config?.spaces || []).find((item: any) => item?.id === spaceId); - if (!space) return []; - const cellCm = Number(space.cell_cm) > 0 ? Number(space.cell_cm) : 5; - const segments = this._limitSegmentsOf(space); - const violations = [ - ...checkNodes(segments), - ...checkSegmentLengths(segments, cellCm, GRID_STEP_N), - ...checkNodeDistances(segments, cellCm, GRID_STEP_N), - ]; - // #330 §4.7: without the shared passes every room recomputed the FULL - // junction topology and masonry union for itself — 144 rooms × a whole- - // plan polyclip pass cost 4.2 s per candidate. innerContourForRoom - // already accepts both caches (the render path uses them); compute each - // once per check and hand them to every room. - let sharedNodes: ReturnType | null = null; - try { - sharedNodes = multiWallNodesForGeometry( - space.rooms || [], space.walls || [], [], GRID_STEP_N, cellCm, GRID_STEP_N, 1, - ); - } catch { sharedNodes = null; } - let sharedRoomGeometry: any = sharedGeometry?.status === 'ok' - || sharedGeometry?.status === 'degraded-extra' - ? sharedGeometry.roomGeom : null; - // The masonry union is the expensive pass (seconds on a large plan), and - // innerContourForRoom only reaches for it when the junction topology is - // non-trivial — with no multi-wall nodes it returns the room's own inset - // early. Pay for the union once, and only when someone will read it. - if (!sharedRoomGeometry && sharedNodes && sharedNodes.nodes.length) { - try { - const geometry = wallBodiesGeometry( - space.rooms || [], space.walls || [], [], [], GRID_STEP_N, cellCm, GRID_STEP_N, 1, - ); - sharedRoomGeometry = geometry?.status === 'ok' || geometry?.status === 'degraded-extra' - ? geometry.roomGeom : null; - } catch { sharedRoomGeometry = null; } - } - for (const room of (space.rooms || [])) { - const roomId = String(room?.id || ''); - if (!roomId) continue; - let inner: number[][] | null = null; - try { - inner = innerContourForRoom( - space.rooms || [], roomId, space.walls || [], [], - GRID_STEP_N, cellCm, GRID_STEP_N, 1, - sharedRoomGeometry ?? undefined, sharedNodes, - ); - } catch { inner = null; } - violations.push(...checkRoomClearance(roomId, inner, cellCm, GRID_STEP_N)); - } - return violations; + private _junctionLimitViolations(config: any, spaceId: string): JunctionLimitViolation[] { + return this._editorRuntimeOrThrow()._junctionLimitViolations(config, spaceId); } /** Localised refusal text for the first violation a write introduces. */ private _junctionLimitLabel(violation: JunctionLimitViolation): string { - const round = (value: number) => String(Math.round(value * 10) / 10); - return this._t(`junction.limit_${violation.rule}` as any, { - actual: round(violation.actual), limit: round(violation.limit), - }); + return this._editorRuntimeOrThrow()._junctionLimitLabel(violation); } - /** #330 §4.4: baseline violations per (document identity, space, geometry). */ - private _junctionBaselineCache = new WeakMap(); - /** #329: violations this write would ADD; inherited ones stay untouched. */ private _junctionLimitsIntroduced( candidate: any, previousConfig: any, spaceId: string, - candidateGeometry?: any, ): JunctionLimitViolation[] { - // The baseline must be the previous document AS THE CANDIDATE SEES IT: a - // legacy space carries no wall catalogue at all, so comparing it raw with - // a migrated candidate reported every inherited short segment as new and - // refused legitimate resizes of real plans. A LEGACY document therefore - // crosses the identity barrier first; a current-version document already - // carries the catalogue and is judged as-is (#330 §4.6) — a no-op - // re-migration cost 69 ms per pointermove on a 576-atom plan. - let inherited: JunctionLimitViolation[] | undefined; - // #330 §4.4: a resize gesture judges the SAME previous document on every - // pointermove. The cache key is the document's identity plus the - // geometry fingerprint of ITS space — content, not a counter: the - // preview epoch (_cfgEpoch) ticks on every accepted preview overlay - // without the stored document changing (that miss cost one full baseline - // pass per pointermove, review 330-r2), while an in-place structural - // commit changes the fingerprint and honestly invalidates. - const previousSpace = (previousConfig?.spaces || []) - .find((space: any) => space?.id === spaceId); - let fingerprint = ''; - try { fingerprint = spacePhysicalGeometryFingerprint(previousSpace); } - catch { fingerprint = ''; } - const cached = previousConfig && typeof previousConfig === 'object' - ? this._junctionBaselineCache.get(previousConfig) : undefined; - if (cached && fingerprint && cached.fingerprint === fingerprint - && cached.spaceId === spaceId) { - inherited = cached.violations; - } - if (!inherited) { - try { - const baseline = Number(previousConfig?.model_version || 0) >= WALL_SEGMENT_MODEL_VERSION - ? previousConfig - : commitWallSegmentModel(previousConfig).config; - inherited = this._junctionLimitViolations(baseline, spaceId); - } catch { - // An unmigratable baseline proves nothing about inheritance; never - // refuse the write on that basis. - return []; - } - if (previousConfig && typeof previousConfig === 'object' && fingerprint) { - this._junctionBaselineCache.set(previousConfig, { - spaceId, fingerprint, violations: inherited, - }); - } - } - let next: JunctionLimitViolation[] = []; - try { next = this._junctionLimitViolations(candidate, spaceId, candidateGeometry); } - catch { - // #331 §2.5: an exception while judging the CANDIDATE is a refusal, - // not a pass — the same fail-closed stance as the #278 geometry guard. - // The baseline branch above stays fail-open on purpose: an unprovable - // inheritance is no reason to block an unrelated write. - return [{ rule: 'check_failed', subject: spaceId, actual: 0, limit: 0 }]; - } - return increasedViolations(next, inherited); + return this._editorRuntimeOrThrow()._junctionLimitsIntroduced(candidate, previousConfig, spaceId); } private _commitPhysicalGeometry( @@ -7568,300 +6939,68 @@ class HouseplanCard extends LitElement { before: SpaceGeometryState | null, additionalAuthoredPoints: readonly (readonly number[])[] = [], ): boolean { - if (!before || !this._serverCfg) return false; - const liveCandidate = this._serverCfg; - const editedState = this._geometrySnapshotFromConfig(liveCandidate, before.spaceId); - const liveSpace = liveCandidate.spaces.find((space: any) => space.id === before.spaceId); - if (!liveSpace || spacePhysicalGeometryFingerprint(before) - === spacePhysicalGeometryFingerprint(liveSpace)) return false; - // Preserve #278's existing fail-closed guard. Identity materialisation may - // repair/atomise a degraded legacy projection, but it must never be used to - // make an otherwise rejected user edit look safe. - let legacySafe = false; - try { legacySafe = this._checkSpacePhysicalGeometry(liveCandidate, before.spaceId).ok; } - catch { legacySafe = false; } - if (!legacySafe) { - this._clearGeometryGesture(); - this._restoreGeometryStateLocal(before); - this._showToast(this._t('toast.geometry_unsafe')); - return false; - } - let committedCandidate: any; - let historyBefore = before; - try { - // ADR 282 Stage 1: every structural writer crosses the same atomic - // canonicalisation/identity barrier. The pure candidate is adopted only - // after every space migrated successfully, so a blocker leaves the live - // config byte-equivalent. - if (Number(liveCandidate.model_version || 0) < 9) { - // The first edit of a pre-v9 document must derive identity and legacy - // zero-wall projection from the pre-edit carrier, not from its already - // moved/split coordinates. - // Materialise that baseline locally, then replay the edited legacy - // projection over it so lineage and Undo share the same stable IDs. - const baselineSource = JSON.parse(JSON.stringify(liveCandidate)); - if (!this._restoreGeometryStateInConfig(baselineSource, before)) - throw new WallSegmentModelError('invalid-room', before.spaceId); - const baseline = commitWallSegmentModel(baselineSource).config; - historyBefore = this._geometrySnapshotFromConfig(baseline, before.spaceId) || before; - const editedWithIdentity = JSON.parse(JSON.stringify(baseline)); - if (!editedState || !this._restoreGeometryStateInConfig( - editedWithIdentity, editedState, true, - )) throw new WallSegmentModelError('invalid-room', before.spaceId); - const baselineSpace = baseline.spaces.find((space: any) => space.id === before.spaceId); - const editedSpace = editedWithIdentity.spaces.find((space: any) => space.id === before.spaceId); - // A fixed-topology pre-catalogue edit expresses thickness through the - // legacy `walls[]` projection. Replay it onto the baseline catalogue - // before v9 treats `cm` as authoritative. Geometry-changing writers - // instead rely on lineage: projecting moved walls onto old baseline - // coordinates would incorrectly turn the moved atom into zero. - if (!Array.isArray(editedState.wall_segments) - && JSON.stringify(before.rooms) === JSON.stringify(editedState.rooms)) { - for (const segment of editedSpace?.wall_segments || []) { - segment.cm = thicknessCmAt( - editedState.walls, segment.a, segment.b, GRID_STEP_N, 1, - ); - } - } - const lineageHints = fixedTopologyWallLineageHints( - baselineSpace, before.rooms, editedSpace, - ); - committedCandidate = commitWallSegmentModel(editedWithIdentity, { - lineageHints, lineageSpaceId: before.spaceId, - }).config; - } else committedCandidate = commitWallSegmentModel(liveCandidate).config; - } catch (error) { - this._clearGeometryGesture(); - this._restoreGeometryStateLocal(before); - this._showWallModelMigrationBlocked(error); - return false; - } - let safe = false; - try { - const afterSpace = committedCandidate.spaces.find((space: any) => space.id === before.spaceId); - // A completed point of the active, grid-snapped chain is authored input, - // even before its first draft record exists in `before`. Include that - // transient carrier in the growth baseline; later conversion from draft - // to room/partition must not count the same coordinate as newly derived. - const authoredPoints = this._path.length >= 2 - ? this._path.map((point) => [point[0] / NORM_W, point[1] / NORM_W]) - : []; - authoredPoints.push(...additionalAuthoredPoints.map((point) => [point[0], point[1]])); - safe = wallModelOffGridValueCount(afterSpace) - <= wallModelOffGridValueCount(historyBefore, authoredPoints) - && this._checkSpacePhysicalGeometry(committedCandidate, before.spaceId).ok; - } catch { - safe = false; - } - if (!safe) { - this._clearGeometryGesture(); - this._restoreGeometryStateLocal(before); - this._showToast(this._t('toast.geometry_unsafe')); - return false; - } - // #329: the owner's junction limits refuse the WRITE. An existing plan's - // inherited violations are never re-judged (spec §3), so the candidate is - // compared against the pre-edit document. - const beforeConfig = JSON.parse(JSON.stringify(liveCandidate)); - this._restoreGeometryStateInConfig(beforeConfig, before); - const introduced = this._junctionLimitsIntroduced( - committedCandidate, beforeConfig, before.spaceId, - ); - if (introduced.length) { - this._clearGeometryGesture(); - this._restoreGeometryStateLocal(before); - this._showToast(this._junctionLimitLabel(introduced[0])); - return false; - } - adoptWallSegmentModelCandidateInPlace(liveCandidate, committedCandidate); - this._recordGeometry(name, historyBefore); - const afterSpace = liveCandidate.spaces.find((space: any) => space.id === before.spaceId); - const pending = this._pendingPhysicalWrites.get(before.spaceId); - this._pendingPhysicalWrites.set(before.spaceId, { - before: pending?.before || historyBefore, - fingerprint: spacePhysicalGeometryFingerprint(afterSpace), - }); - this._saveConfig(); - return true; + return this._editorRuntimeOrThrow()._commitPhysicalGeometry(name, before, additionalAuthoredPoints); } /** Drop every transient gesture before replacing committed geometry. */ private _clearGeometryGesture(): void { - this._path = []; - this._clearPlanSnapHover(); - this._clearOpeningPlacement(false); - this._mergeSel = null; - this._mergeDialog = null; - this._splitSel = null; - this._pendingSplit = null; - this._wallFaceBatch = null; - this._wallRepairDiagnostic = null; - this._roomDeleteDialog = null; - this._wallDialog = null; - this._physicalDialog = null; - this._physicalSel = null; - this._physicalDrag = null; - this._physicalRotate = null; - this._activeDraftId = null; - this._draftSegmentCms = []; - this._closingWallCm = null; - this._openingDialog = null; - this._rszResetController(); - this._decorDraft = null; - this._decorMove = null; - this._dtDrag = null; - this._bdDrag = null; + if (!this._editorRuntime) { + // Empty-install onboarding is intentionally independent from the editor + // chunk. Before that chunk exists there can be no live editor gesture, + // but warm state may still carry inert draft fields: clear them eagerly + // so the empty-state reset never pulls editor code into View. + this._path = []; + this._planSnapHover = null; + this._cursorPt = null; + this._openingHoverCandidate = null; + this._openingJambBlockCm = null; + this._openingPreset = null; + this._openingRebindId = null; + this._mergeSel = null; + this._mergeDialog = null; + this._splitSel = null; + this._pendingSplit = null; + this._wallFaceBatch = null; + this._wallRepairDiagnostic = null; + this._roomDeleteDialog = null; + this._wallDialog = null; + this._physicalDialog = null; + this._physicalSel = null; + this._physicalDrag = null; + this._physicalRotate = null; + this._activeDraftId = null; + this._draftSegmentCms = []; + this._closingWallCm = null; + this._openingDialog = null; + this._resize?.reset(); + this._decorDraft = null; + this._decorMove = null; + this._dtDrag = null; + this._bdDrag = null; + return; + } + return this._editorRuntimeOrThrow()._clearGeometryGesture(); } /** Browser/OS cancellation is an aborted transaction, never a commit. */ private _stagePointerCancel(ev: PointerEvent): void { - clearTimeout(this._kioskHoldTimer); - if (this._swipeStart?.id === ev.pointerId) this._swipeStart = null; - if (this._physicalDrag?.pid === ev.pointerId || this._physicalRotate?.pid === ev.pointerId) { - this._cancelPhysicalGesture(); - return; - } - if (this._decorDraft?.pid === ev.pointerId) { - this._decorDraft = null; - this.requestUpdate(); - return; - } - if (this._decorMove?.pid === ev.pointerId || this._dtDrag?.pid === ev.pointerId - || this._bdDrag?.pid === ev.pointerId) { - this._cancelDecorGesture(); - return; - } - if (this._tool === 'opening') { - this._cursorPt = null; - this._clearOpeningPlacement(false); - } else if (this._tool === 'draw') { - this._clearPlanSnapHover(); - } - const viewportGestureEnded = !!this._pinchStart || !!this._panStart; - this._pointers.delete(ev.pointerId); - if (this._pointers.size < 2) this._pinchStart = null; - if (this._pointers.size === 0) { - this._panStart = null; - this._panLock = null; - } - if (viewportGestureEnded && this._pointers.size === 0) this.requestUpdate(); + if (!this._editorRuntime) return; + return this._editorRuntime._stagePointerCancel(ev); } private _applyGeometryState( state: SpaceGeometryState, allowHistoryBoundaryRepair = false, ): boolean { - if (!this._canCommitSpace(state.spaceId)) return false; - const before = this._geometrySnapshot(state.spaceId); - if (!before || !this._restoreGeometryStateLocal(state)) return false; - const restoredCandidate = this._serverCfg; - const physicalChanged = spacePhysicalGeometryFingerprint(before) - !== spacePhysicalGeometryFingerprint(state); - if (physicalChanged) { - let safe = false; - try { - const check = restoredCandidate - ? this._checkSpacePhysicalGeometry(restoredCandidate, state.spaceId) - : null; - safe = !!check?.ok || !!(allowHistoryBoundaryRepair - && check?.reason === 'wall-degraded-extra'); - } catch { safe = false; } - if (!safe) { - this._restoreGeometryStateLocal(before); - this._showToast(this._t('toast.geometry_unsafe')); - return false; - } - } - let committedCandidate: any; - try { - committedCandidate = commitWallSegmentModel(restoredCandidate).config; - } catch (error) { - this._restoreGeometryStateLocal(before); - this._showWallModelMigrationBlocked(error); - return false; - } - if (physicalChanged) { - let safe = false; - try { - const check = committedCandidate - ? this._checkSpacePhysicalGeometry(committedCandidate, state.spaceId) - : null; - // A history snapshot can predate the write-time wall degradation that - // canonicalized its command. Restore that one repairable baseline so - // Undo remains byte-exact immediately; _writeConfig still degrades and - // strictly validates the outbound candidate before it can leave the - // card. Every other preflight failure stays fail-closed. - safe = !!check?.ok || !!(allowHistoryBoundaryRepair - && check?.reason === 'wall-degraded-extra'); - } catch { safe = false; } - if (!safe) { - this._restoreGeometryStateLocal(before); - this._showToast(this._t('toast.geometry_unsafe')); - return false; - } - adoptWallSegmentModelCandidateInPlace(restoredCandidate, committedCandidate); - const afterSpace = restoredCandidate?.spaces.find((space: any) => space.id === state.spaceId); - const pending = this._pendingPhysicalWrites.get(state.spaceId); - this._pendingPhysicalWrites.set(state.spaceId, { - before: pending?.before || before, - fingerprint: spacePhysicalGeometryFingerprint(afterSpace), - }); - } else adoptWallSegmentModelCandidateInPlace(restoredCandidate, committedCandidate); - this._clearGeometryGesture(); - if (this._space !== state.spaceId) { - this._commitSpace(state.spaceId); - this._saveNav(); - this._restoreZoom(); - } - this._modelCache = null; - this._frame = null; - this._regSignature = ''; - this._maybeRebuildDevices(); - this._saveConfig(); - this.requestUpdate(); - return true; + return this._editorRuntimeOrThrow()._applyGeometryState(state, allowHistoryBoundaryRepair); } private _undoGeometry = (): void => { - if (this._physicalDrag || this._physicalRotate) { - this._cancelPhysicalGesture(); - return; - } - if (this._decorDraft) { this._decorDraft = null; this.requestUpdate(); return; } - if (this._decorMove || this._dtDrag || this._bdDrag) { - this._cancelDecorGesture(); - return; - } - if (this._resize.dragging) { this._rszCancelDrag(); return; } - const command = this._geometryHistory.undo(); - if (!command) return; - if (!this._applyGeometryState(command.before, true)) { - this._geometryHistory.clear(); - return; - } - this._showToast(this._t('history.undone', { name: command.name })); - }; + return this._editorRuntimeOrThrow()._undoGeometry(); + } private _redoGeometry = (): void => { - // Redo and Undo share the same transaction boundary. The first invocation - // cancels an in-progress transform; only the next one navigates history. - if (this._physicalDrag || this._physicalRotate) { - this._cancelPhysicalGesture(); - return; - } - if (this._decorDraft) { this._decorDraft = null; this.requestUpdate(); return; } - if (this._decorMove || this._dtDrag || this._bdDrag) { - this._cancelDecorGesture(); - return; - } - if (this._resize.dragging) { this._rszCancelDrag(); return; } - const command = this._geometryHistory.redo(); - if (!command) return; - if (!this._applyGeometryState(command.after, true)) { - this._geometryHistory.clear(); - return; - } - this._showToast(this._t('history.redone', { name: command.name })); - }; + return this._editorRuntimeOrThrow()._redoGeometry(); + } private _saveConfigDebounced = debounce(() => { if (!this._serverCfg) return; @@ -7890,205 +7029,33 @@ class HouseplanCard extends LitElement { * rooms share walls, so new vertices legitimately land on existing outlines. */ private _roomAt(p: number[]): RoomCfg | undefined { - return this._spaceModel()?.rooms.find((r) => { - const poly = roomPoly(r); - return !!poly && pointStrictlyInside(p, poly); - }); + return this._editorRuntimeOrThrow()._roomAt(p); } /** The first existing room the outline would overlap (rooms must not overlap). */ private _overlapRoom(verts: number[][]): RoomCfg | undefined { - return this._spaceModel()?.rooms.find((r) => { - const poly = roomPoly(r); - return !!poly && roomsOverlap(verts, poly); - }); + return this._editorRuntimeOrThrow()._overlapRoom(verts); } private _pointInRoom(p: number[], r: RoomCfg): boolean { if (r.poly) return pointInPolygon(p, r.poly); - return ( - r.x != null && p[0] >= r.x! && p[0] <= r.x! + r.w! && p[1] >= r.y! && p[1] <= r.y! + r.h! - ); + return r.x != null && p[0] >= r.x && p[0] <= r.x + r.w! + && p[1] >= r.y! && p[1] <= r.y! + r.h!; } /** A room ring may meet only at the endpoints of neighbouring edges. */ private _contourSelfIntersects(poly: number[][]): boolean { - if (!polyIsSimple(poly)) return true; // transverse crossings - const n = poly.length; - const eps = 0.001; - for (let i = 0; i < n; i++) { - const a = poly[i], b = poly[(i + 1) % n]; - for (let j = i + 1; j < n; j++) { - // Neighbours deliberately share one endpoint, including first ↔ last. - if (j === i + 1 || (i === 0 && j === n - 1)) continue; - const c = poly[j], d = poly[(j + 1) % n]; - const cd = [c[0], c[1], d[0], d[1]]; - const ab = [a[0], a[1], b[0], b[1]]; - // This also catches a closing edge passing through an older vertex and - // collinear overlap, which a proper-crossing test intentionally omits. - if (distToSegment(a, cd) <= eps || distToSegment(b, cd) <= eps || - distToSegment(c, ab) <= eps || distToSegment(d, ab) <= eps) return true; - } - } - return false; + return this._editorRuntimeOrThrow()._contourSelfIntersects(poly); } /** The same append limits guard both an ordinary point and an auto-close terminal point. */ private _canAppendRoomDraftPoint(): boolean { - if (this._drawWallCm == null) { this._showPhysicalRange(100); return false; } - if (this._path.length >= MAX_DRAFT_POINTS) { - this._showToast(this._t('toast.physical_limit')); - return false; - } - const spCfg = this._curSpaceCfg as any; - const newDraft = !this._activeDraftId; - if ((newDraft && (spCfg?.room_drafts || []).length >= MAX_ROOM_DRAFTS) - || this._draftSegmentCount(spCfg) >= MAX_DRAFT_SEGMENTS - // Every accepted segment must be finishable later without trapping - // the user at the partition limit. - || (spCfg?.partitions || []).length + this._path.length > MAX_PARTITIONS) { - this._showToast(this._t('toast.physical_limit')); - return false; - } - return true; + return this._editorRuntimeOrThrow()._canAppendRoomDraftPoint(); } private _markupClick(ev: MouseEvent): void { - if (this._vacFit) return; // the fit overlay owns all pointer input - if (!this._markup) return; - const space = this._spaceModel(); - if (!space) { - this._clearGeometryGesture(); - return; - } - // a pan or pinch just happened — the synthesized click is not a draw - if (this._suppressClick) return; - // Room cards swallow markup clicks: dragging, resizing or just clicking a - // card must not feed the active tool (draw point, delete room, merge/split - // pick, opening placement). The drag itself already stops pointer events, - // but the synthesized `click` afterwards still bubbles to the stage. - if (this._drag || this._rlResize) return; - const path = (ev.composedPath?.() || []) as any[]; - if (path.some((n) => n?.classList?.contains?.('roomlabel') || n?.classList?.contains?.('rlhandle'))) return; - if (path.some((n) => n?.classList?.contains?.('physical-hit'))) return; - const raw = this._svgPoint(ev); - if (this._tool === 'select') { - this._physicalSel = null; - return; - } - if (this._tool === 'resize') { - // a click picks the room for the scale frame; handle drags never get here - if (this._resize.dragging || path.some((n) => n?.classList?.contains?.('rszhandle'))) return; - const room = [...space.rooms].reverse().find((r) => this._pointInRoom(raw, r)); - this._resize.selectRoom(room?.id || null); - this.requestUpdate(); - return; - } - if (this._tool === 'delroom') { - this._deleteRoomClick(raw); - return; - } - if (this._tool === 'opening') { - this._openingClick(raw); - return; - } - if (this._tool === 'merge') { - this._mergeClick(raw); - return; - } - if (this._tool === 'wallthick') { - this._wallThickClick(raw); - return; - } - if (this._tool === 'split') { - this._splitClick(raw); - return; - } - if (this._tool === 'column') { - this._columnClick(raw); - return; - } - // Walls: every completed segment is crash-safe in room_drafts until an - // explicit finish converts the chain or a confirmed face batch consumes it. - this._wallRepairDiagnostic = null; - const resolved = this._resolvePlanDrawPoint(raw, ev.shiftKey); - if (resolved.ambiguous) { - this._planSnapHover = { - contextKey: resolved.contextKey, candidate: null, conflicts: resolved.conflicts, - }; - this._syncPlanSnapActiveMarker(null); - this._syncPlanSnapConflictMarkers(resolved.conflicts); - this._showToast(this._t('toast.plan_snap_ambiguous')); - return; - } - let pt = resolved.point; - if (ev.ctrlKey || ev.metaKey) { - // The closure shortcut owns the gesture even before the chain has the - // two existing edges required to form a room. Falling through here - // would turn a refused Ctrl/Cmd+click into an ordinary drawing click. - ev.preventDefault(); - if (this._path.length < 3) return; - // Preserve the established shortcut, but commit its closing wall through - // the same graph/draft path as an ordinary click on the first node. - pt = snapNearAxisEndpoint( - this._path[this._path.length - 1], this._path[0], - ); - } - // Island rooms (v1.34.0): drawing INSIDE an existing room is legal — the - // contour may become a nested room (a column, an inner room). Partial - // overlaps are still rejected, but only at closing time, when the whole - // outline is known (roomsOverlap treats full nesting as legal). - if (!this._path.length) { - // A free idle click can name a room inside masonry that was completed in - // an earlier session. Shift and architectural snap hits retain the Walls - // gesture, so this offer never steals an intentional new chain. - if (!ev.shiftKey && !resolved.candidate && this._offerExistingWallFace(raw)) return; - // After reload a saved open contour is resumed explicitly by clicking - // either free end. Mid-segment branching is deliberately unsupported. - const endHit = this._draftEndAt(pt); - if (endHit) { - this._activeDraftId = endHit.draft.id; - this._resumeDraftBySpace[this._space] = endHit.draft.id; - this._path = endHit.reverse - ? [...endHit.draft.points].reverse().map((p) => [...p]) - : endHit.draft.points.map((p) => [...p]); - this._draftSegmentCms = this._adoptDraftCms( - this._path, - endHit.reverse - ? [...endHit.draft.segments].reverse().map((s) => s.cm) - : endHit.draft.segments.map((s) => s.cm), - endHit.draft.id, - ); - return; - } - this._activeDraftId = null; - this._draftSegmentCms = []; - this._path = [pt]; - return; - } - const last = this._path[this._path.length - 1]; - if (this._samePt(pt, last)) return; // repeated click on the same point - // A saved outline may be continued into another saved outline, but only - // endpoint-to-endpoint. The two records become one atomic history step; - // mid-segment branching remains deliberately unsupported. - const join = this._draftEndAt(pt, this._activeDraftId || undefined); - if (join) { - this._mergeDraftEndpoint(join); - return; - } - if (!this._canAppendRoomDraftPoint()) return; - // Точка и толщина её отрезка пишутся вместе (#234). Раньше запись жила в - // отдельном методе, который молча выходил при невалидном поле толщины, и - // тогда `_draftSegmentCms` становился короче числа отрезков: превью - // показывало текущее поле, а запись — 15 см. Инвариант читается прямо - // здесь, а не выводится из двух проверок в разных местах. - const cm = this._drawWallCm; - if (cm == null) { this._showPhysicalRange(100); return; } - const beforePath = this._path.map((point) => [...point]); - this._path = [...this._path, pt]; - this._draftSegmentCms = [...this._draftSegmentCms, cm]; - this._persistActiveDraftSegment(); - this._offerWallFaces(beforePath); + if (!this._editorRuntime) return; + return this._editorRuntime._markupClick(ev); } private _draftSegmentCms: number[] = []; @@ -8097,110 +7064,13 @@ class HouseplanCard extends LitElement { private _draftEndAt( pt: number[], excludeId?: string, ): { draft: RoomDraftCfg; reverse: boolean } | null { - const space = this._spaceModel(); - if (!space) return null; - const view = this._viewOr(this._baseVb()); - const eps = Math.max(this._gridPitch * 0.15, - this._stageEl?.clientWidth ? (view.w / this._stageEl.clientWidth) * 12 : 0); - for (const draft of space.room_drafts || []) { - if (draft.id === excludeId) continue; - if (draft.points.length < 2) continue; - const first = draft.points[0], last = draft.points[draft.points.length - 1]; - if (Math.hypot(pt[0] - last[0], pt[1] - last[1]) <= eps) return { draft, reverse: false }; - if (Math.hypot(pt[0] - first[0], pt[1] - first[1]) <= eps) return { draft, reverse: true }; - } - return null; + return this._editorRuntimeOrThrow()._draftEndAt(pt, excludeId); } private _mergeDraftEndpoint( hit: { draft: RoomDraftCfg; reverse: boolean }, ): void { - const sp = this._curSpaceCfg as any; - if (!sp || !this._path.length) return; - const beforePath = this._path.map((point) => [...point]); - // Capture topology before the records are merged: the other draft is a - // static pre-existing source, not geometry introduced by this click. - const beforeGraphSources = this._wallGraphSources(beforePath); - const drafts = Array.isArray(sp.room_drafts) ? sp.room_drafts : []; - const activeRaw = this._activeDraftId - ? drafts.find((d: any) => d.id === this._activeDraftId) : null; - const otherRaw = drafts.find((d: any) => d.id === hit.draft.id); - if (!otherRaw) return; - - // `_draftEndAt.reverse` describes how to resume WITH the clicked end at - // the tail. A merge needs that end at the head, hence the opposite order. - const otherPoints = hit.reverse - ? hit.draft.points.map((p) => [...p]) - : [...hit.draft.points].reverse().map((p) => [...p]); - const otherSegments = hit.reverse - ? (otherRaw.segments || []).map((s: any) => ({ ...s })) - : [...(otherRaw.segments || [])].reverse().map((s: any) => ({ ...s })); - const last = this._path[this._path.length - 1]; - const touching = this._samePt(last, otherPoints[0]); - const connectorCm = touching ? null : this._drawWallCm; - if (!touching && connectorCm == null) { this._showPhysicalRange(100); return; } - - const activeSegments = this._draftSegmentsForPath( - this._path, activeRaw, this._draftSegmentCms, - ); - const mergedPoints = [ - ...this._path.map((p) => [...p]), - ...(touching ? otherPoints.slice(1) : otherPoints), - ]; - const mergedSegments = [ - ...activeSegments, - ...(connectorCm == null ? [] : [{ cm: connectorCm }]), - ...otherSegments, - ]; - const closed = mergedPoints.length >= 4 - && this._samePt(mergedPoints[0], mergedPoints[mergedPoints.length - 1]); - const persistedPoints = mergedPoints; - const persistedSegments = mergedSegments; - if (persistedPoints.length > MAX_DRAFT_POINTS) { - this._showToast(this._t('toast.physical_limit')); - return; - } - const oldSegments = (activeRaw?.segments?.length || 0) + (otherRaw.segments?.length || 0); - if (this._draftSegmentCount(sp) - oldSegments + persistedSegments.length > MAX_DRAFT_SEGMENTS) { - this._showToast(this._t('toast.physical_limit')); - return; - } - if (closed) { - const ring = persistedPoints.slice(0, -1); - if (this._contourSelfIntersects(ring) || polygonArea(ring) <= 1e-6) { - this._showToast(this._t('toast.contour_cannot_close')); - return; - } - const clash = this._overlapRoom(ring); - if (clash) { - this._showToast(this._t('toast.room_overlap', { name: clash.name || '' })); - return; - } - } - - const before = this._geometrySnapshot(); - const id = this._activeDraftId || hit.draft.id; - const base = activeRaw || otherRaw; - const saved = { - ...base, id, - points: persistedPoints.map((p) => [p[0] / NORM_W, p[1] / NORM_W]), - segments: persistedSegments, - }; - sp.room_drafts = drafts.filter((d: any) => d.id !== hit.draft.id - && (!this._activeDraftId || d.id !== this._activeDraftId)); - sp.room_drafts.push(saved); - this._activeDraftId = id; - this._resumeDraftBySpace[this._space] = id; - this._path = persistedPoints; - this._draftSegmentCms = this._adoptDraftCms( - persistedPoints, persistedSegments.map((s: any) => Number(s.cm)), id, - ); - this._physicalSel = null; - this._commitPhysicalGeometry(this._t('history.draft_merge'), before); - - if (connectorCm != null) { - this._offerWallFaces(beforePath, beforePath.length - 1, beforeGraphSources); - } + return this._editorRuntimeOrThrow()._mergeDraftEndpoint(hit); } /** @@ -8213,14 +7083,7 @@ class HouseplanCard extends LitElement { * fix it. */ private _adoptDraftCms(path: readonly (readonly number[])[], recorded: readonly (number | null | undefined)[], id?: string): number[] { - const count = Math.max(0, path.length - 1); - const resolved = chainSegmentCms(count, recorded, this._drawWallCm, DRAW_WALL_DEFAULT_CM); - if (recorded.length !== count) { - console.debug( - `[houseplan] draft ${id ?? '?'}: восстановлено толщин ${count - recorded.length} (#234)`, - ); - } - return resolved; + return this._editorRuntimeOrThrow()._adoptDraftCms(path, recorded, id); } /** @@ -8232,23 +7095,7 @@ class HouseplanCard extends LitElement { path: readonly (readonly number[])[], draft: any, cms: readonly (number | null | undefined)[], ): Array<{ id?: string; cm: number; [key: string]: any }> { - const points = Array.isArray(draft?.points) - ? draft.points.map((p: any) => [Number(p?.[0]) * NORM_W, Number(p?.[1]) * NORM_W]) - : []; - const stored = Array.isArray(draft?.segments) ? draft.segments : []; - const resolved = chainSegmentCms( - Math.max(0, path.length - 1), cms, this._drawWallCm, DRAW_WALL_DEFAULT_CM, - ); - return resolved.map((cm, index) => { - const a = path[index], b = path[index + 1]; - const oldIndex = points.findIndex((oldA: number[], candidate: number) => { - if (candidate + 1 >= points.length) return false; - const oldB = points[candidate + 1]; - return (this._samePt(a, oldA) && this._samePt(b, oldB)) - || (this._samePt(a, oldB) && this._samePt(b, oldA)); - }); - return { ...(oldIndex >= 0 ? stored[oldIndex] : {}), cm }; - }); + return this._editorRuntimeOrThrow()._draftSegmentsForPath(path, draft, cms); } /** @@ -8259,59 +7106,22 @@ class HouseplanCard extends LitElement { * drift apart the moment the toolbar field is mid-edit. */ private _persistActiveDraftSegment(): void { - if (this._path.length < 2 || !this._curSpaceCfg) return; - const before = this._geometrySnapshot(); - const sp = this._curSpaceCfg as any; - sp.room_drafts ||= []; - if (!this._activeDraftId) this._activeDraftId = 'draft-' + Date.now().toString(36); - this._resumeDraftBySpace[this._space] = this._activeDraftId; - const i = sp.room_drafts.findIndex((d: any) => d.id === this._activeDraftId); - const saved = { - ...(i >= 0 ? sp.room_drafts[i] : {}), - id: this._activeDraftId, - points: this._path.map((p) => [p[0] / NORM_W, p[1] / NORM_W]), - segments: this._draftSegmentsForPath( - this._path, i >= 0 ? sp.room_drafts[i] : null, this._draftSegmentCms, - ), - }; - if (i >= 0) sp.room_drafts[i] = saved; - else sp.room_drafts.push(saved); - this._commitPhysicalGeometry(this._t('history.draft_segment'), before); + return this._editorRuntimeOrThrow()._persistActiveDraftSegment(); } private _activeWallSourceKey(index: number): string { - return `active:${this._activeDraftId || 'session'}:${index}`; + return this._editorRuntimeOrThrow()._activeWallSourceKey(index); } /** Canonical solid axes plus one immutable projection of the active chain. */ private _wallGraphSources(path: readonly (readonly number[])[]): WallGraphSourceSegment[] { - const staticGeometry = this._planStructuralGeometrySnapshot().value; - const sources: WallGraphSourceSegment[] = staticGeometry.segments.map((segment) => ({ - a: segment.a, b: segment.b, key: `static:${segment.key}`, - })); - for (let i = 0; i + 1 < path.length; i++) { - sources.push({ a: path[i], b: path[i + 1], key: this._activeWallSourceKey(i) }); - } - return sources; + return this._editorRuntimeOrThrow()._wallGraphSources(path); } private _wallFaceGraph( sources: readonly WallGraphSourceSegment[], epsilon: number, ): WallFaceGraph { - // Exact JSON identity avoids stale topology. Source keys include real - // open-span cuts/provenance; presentation opening cuts are intentionally - // absent from the structural graph (#185). - const key = `${this._space}|${epsilon}|${JSON.stringify(sources)}`; - const cachedIndex = this._wallFaceGraphCache.findIndex((entry) => entry.key === key); - if (cachedIndex >= 0) { - const [cached] = this._wallFaceGraphCache.splice(cachedIndex, 1); - this._wallFaceGraphCache.push(cached); - return cached.value; - } - const value = buildWallFaceGraph(sources, epsilon); - this._wallFaceGraphCache.push({ key, value }); - if (this._wallFaceGraphCache.length > 4) this._wallFaceGraphCache.shift(); - return value; + return this._editorRuntimeOrThrow()._wallFaceGraph(sources, epsilon); } /** Detect click-induced faces and open a mutation-free decision queue. */ @@ -8320,552 +7130,86 @@ class HouseplanCard extends LitElement { addedSegmentIndex = this._path.length - 2, beforeGraphSources?: WallGraphSourceSegment[], ): void { - if (this._path.length < 2 || this._wallFaceBatch || this._roomDialog) return; - const space = this._spaceModel(); - if (!space) return; - const addedSourceKey = this._activeWallSourceKey(addedSegmentIndex); - const epsilon = this._gridPitch * 0.0002; - let faces: WallGraphFace[]; - let after: WallGraphSourceSegment[]; - try { - const before = beforeGraphSources || this._wallGraphSources(beforePath); - after = this._wallGraphSources(this._path); - faces = findNewWallFacesInGraphs( - this._wallFaceGraph(before, epsilon), this._wallFaceGraph(after, epsilon), - addedSourceKey, - ); - } catch { - // Topology is advisory. The just-persisted draft remains the source of - // truth and drawing can continue when malformed legacy geometry exists. - return; - } - if (!faces.length) { - const repair = planWallFaceRepair(after, { - requiredSourceKey: addedSourceKey, - maxDistance: wallCmToUnits(2, this._cellCm, this._gridPitch), - gridStep: this._gridPitch, - epsilon, - }); - if (repair.kind === 'ambiguous') { - this._wallRepairDiagnostic = repair.proposals[0] || null; - this._showToast(this._t( - this._drawWallCm === 0 ? 'toast.zero_wall_ambiguous' : 'toast.wall_repair_ambiguous', - )); - return; - } - if (repair.kind === 'repair' && !this._overlapRoom(repair.face.ring)) { - this._beginWallFaceBatch([{ ...repair.face, repair: repair.proposal }]); - } - return; - } - - // A clean wall-to-wall cut is one product decision about the smaller part, - // even though the planar delta contains both resulting faces. - for (const room of space.rooms) { - const poly = roomPoly(room); - if (!room.id || !poly) continue; - const parts = splitRoomPath(poly, this._path, this._gridPitch * 0.02); - if (!parts) continue; - const [p1, p2] = parts; - const main = polygonArea(p1) >= polygonArea(p2) ? p1 : p2; - const fresh = main === p1 ? p2 : p1; - const matched = [...faces].sort((left, right) => - Math.abs(left.area - polygonArea(fresh)) - Math.abs(right.area - polygonArea(fresh)) - || left.key.localeCompare(right.key))[0]; - if (!matched) return; - this._beginWallFaceBatch([{ - ...matched, - ring: fresh.map((point) => [point[0], point[1]] as [number, number]), - split: { roomId: room.id, mainPoly: main, newPoly: fresh }, - consumeAllActive: true, - }]); - return; - } - - const eligible = faces.filter((face) => { - if (face.ring.length < 3 || this._contourSelfIntersects(face.ring)) return false; - // roomsOverlap rejects exact duplicates and partial overlap, while full - // nesting in either direction deliberately remains eligible. - return !this._overlapRoom(face.ring); - }); - if (eligible.length) this._beginWallFaceBatch(eligible); + return this._editorRuntimeOrThrow()._offerWallFaces(beforePath, addedSegmentIndex, beforeGraphSources); } private _beginWallFaceBatch(candidates: WallFaceCandidate[]): void { - this._wallRepairDiagnostic = null; - this._wallFaceBatch = { - candidates: [...candidates].sort((left, right) => - left.area - right.area || left.key.localeCompare(right.key)), - index: 0, - decisions: [], - activePath: this._path.map((point) => [...point]), - activeCms: [...this._draftSegmentCms], - activeDraftId: this._activeDraftId, - }; - this._clearPlanSnapHover(); - this._nameSel = ''; - this._areaSel = ''; - this._resetRoomDialogFields(); - this._roomDialog = true; + return this._editorRuntimeOrThrow()._beginWallFaceBatch(candidates); } /** Offer the smallest unoccupied exact face under an idle Walls click. */ private _offerExistingWallFace(raw: number[]): boolean { - if (this._path.length || this._wallFaceBatch || this._roomDialog) return false; - const epsilon = this._gridPitch * 0.0002; - try { - const graph = this._wallFaceGraph(this._wallGraphSources([]), epsilon); - let face = findWallFaceAtPoint(graph, raw, epsilon); - let repair: WallFaceRepairProposal | undefined; - if (!face) { - const result = planWallFaceRepair(this._wallGraphSources([]), { - point: raw, - maxDistance: wallCmToUnits(2, this._cellCm, this._gridPitch), - gridStep: this._gridPitch, - epsilon, - }); - if (result.kind === 'ambiguous') { - this._wallRepairDiagnostic = result.proposals[0] || null; - this._showToast(this._t( - this._drawWallCm === 0 ? 'toast.zero_wall_ambiguous' : 'toast.wall_repair_ambiguous', - )); - return true; - } - if (result.kind === 'repair') { - face = result.face; - repair = result.proposal; - } else if (result.kind === 'none') { - const diagnostic = planWallFaceRepair(this._wallGraphSources([]), { - point: raw, - maxDistance: this._cssPxToRender(12), - gridStep: this._gridPitch, - epsilon, - }); - if (diagnostic.kind === 'repair') { - this._wallRepairDiagnostic = diagnostic.proposal; - this._showToast(this._t('toast.wall_repair_too_large')); - return true; - } - } - } - if (!face || face.ring.length < 3 || this._contourSelfIntersects(face.ring) - || this._overlapRoom(face.ring)) return false; - this._beginWallFaceBatch([{ ...face, existing: !repair, repair }]); - return true; - } catch { - return false; - } + return this._editorRuntimeOrThrow()._offerExistingWallFace(raw); } private _columnClick(raw: number[]): void { - const center = this._snap(raw); - const cm = this._drawWallCm; - if (cm == null) { this._showPhysicalRange(COLUMN_MAX_CM, 1); return; } - if (!this._curSpaceCfg || this._limitReached('column')) return; - const model = this._spaceModel(); - if (!model) return; - const candidate: WallColumnCfg = { - id: 'column-' + Date.now().toString(36), shape: 'square', center, cm: clampColumnCm(cm), angle: 0, - }; - const duplicate = (model.wall_columns || []).find((c) => - sameColumnPlacement(c, candidate, this._gridPitch * 0.02)); - if (duplicate) { - clearTimeout(this._duplicateColumnTimer); - this._duplicateColumnId = duplicate.id; - this._duplicateColumnTimer = window.setTimeout(() => { - this._duplicateColumnId = null; - }, 900); - this._showToast(this._t('toast.column_duplicate')); - return; - } - const before = this._geometrySnapshot(); - const sp = this._curSpaceCfg as any; - sp.wall_columns ||= []; - sp.wall_columns.push({ ...candidate, center: [center[0] / NORM_W, center[1] / NORM_W] }); - this._commitPhysicalGeometry(this._t('history.column_add'), before); + return this._editorRuntimeOrThrow()._columnClick(raw); } private _openPhysicalDialog( kind: 'partition' | 'column' | 'draft', id: string, segment?: number, ): void { - const model = this._spaceModel(); - if (!model) return; - if (kind === 'partition') { - const p = model.partitions.find((x) => x.id === id); - if (p) this._physicalDialog = { - kind, id, cm: cmToField(p.cm, this._imperial), length: this._fmtLen(p.a, p.b), - }; - } else if (kind === 'column') { - const c = model.wall_columns.find((x) => x.id === id); - if (c) this._physicalDialog = { - kind, id, cm: cmToField(c.cm, this._imperial), shape: c.shape, - angle: this._angleField(c.shape === 'square' ? canonicalColumnAngle(c.angle) : 0), - }; - } else { - const d = model.room_drafts.find((x) => x.id === id); - const i = Math.max(0, Math.min(d?.segments.length ? d.segments.length - 1 : 0, segment || 0)); - if (d?.segments[i]) this._physicalDialog = { - kind, id, segment: i, cm: cmToField(d.segments[i].cm, this._imperial), - length: this._fmtLen(d.points[i], d.points[i + 1]), - }; - } + return this._editorRuntimeOrThrow()._openPhysicalDialog(kind, id, segment); } private _savePhysicalDialog = (): void => { - const d = this._physicalDialog; - const sp = this._curSpaceCfg as any; - const model = this._spaceModel(); - if (!d || !sp || !model) return; - const raw = strictNumber(d.cm); - if (raw == null) { - this._showPhysicalRange( - d.kind === 'column' ? COLUMN_MAX_CM : 100, d.kind === 'column' ? 1 : 0, - ); - return; - } - const cmRaw = this._imperial ? raw * 2.54 : raw; - const max = d.kind === 'column' ? COLUMN_MAX_CM : 100; - const min = d.kind === 'column' ? 1 : 0; - if (!Number.isFinite(cmRaw) || cmRaw < min || cmRaw > max) { - this._showPhysicalRange(max, min); - return; - } - if (d.kind === 'column') { - const current = model.wall_columns.find((x) => x.id === d.id); - if (!current) return; - const rawAngle = strictNumber(d.angle || '0'); - if (d.shape !== 'circle' - && (rawAngle == null || rawAngle < 0 || rawAngle >= 90)) { - this._showToast(this._t('toast.physical_angle')); - return; - } - const candidate: WallColumnCfg = d.shape === 'circle' - ? { id: d.id, shape: 'circle', center: current.center, cm: cmRaw } - : { id: d.id, shape: 'square', center: current.center, cm: cmRaw, - angle: rawAngle! }; - if (model.wall_columns.some((c) => c.id !== d.id - && sameColumnPlacement(c, candidate, this._gridPitch * 0.02))) { - this._showToast(this._t('toast.column_duplicate')); - return; - } - } - const before = this._geometrySnapshot(); - if (d.kind === 'partition') { - const p = (sp.partitions || []).find((x: any) => x.id === d.id); - if (p && cmRaw === 0 && zeroWallHasOpening(sp.openings, { - kind: 'partition', id: d.id, - })) { - this._showToast(this._t('toast.zero_wall_opening_conflict')); - return; - } - if (p) p.cm = cmRaw; - } else if (d.kind === 'column') { - const c = (sp.wall_columns || []).find((x: any) => x.id === d.id); - if (c) { - c.cm = clampColumnCm(cmRaw); - c.shape = d.shape === 'circle' ? 'circle' : 'square'; - if (c.shape === 'square') c.angle = strictNumber(d.angle || '0')!; - else delete c.angle; - } - } else { - const draft = (sp.room_drafts || []).find((x: any) => x.id === d.id); - if (draft?.segments?.[d.segment || 0]) draft.segments[d.segment || 0].cm = cmRaw; - } - const committed = this._commitPhysicalGeometry(this._t('history.physical_edit'), before); - this._physicalDialog = null; - if (!committed) this.requestUpdate(); - }; + return this._editorRuntimeOrThrow()._savePhysicalDialog(); + } private _deletePhysicalSelection = (): void => { - const sel = this._physicalSel; - const sp = this._curSpaceCfg as any; - if (!sel || !sp) return; - if (sel.kind === 'draft') { this._deleteDraftWhole(); return; } - const before = this._geometrySnapshot(); - if (sel.kind === 'partition') { - const hosted = (sp.openings || []) - .filter((opening: OpeningCfg) => opening.host?.kind === 'partition' - && opening.host.id === sel.id) - .sort((a: OpeningCfg, b: OpeningCfg) => (a.host?.t || 0) - (b.host?.t || 0)); - if (hosted.length) { - this._partitionDeleteDialog = { - id: sel.id, - openings: hosted.map((opening: OpeningCfg) => JSON.parse(JSON.stringify(opening))), - }; - return; - } - } - const key = sel.kind === 'partition' ? 'partitions' - : sel.kind === 'column' ? 'wall_columns' : 'room_drafts'; - sp[key] = (sp[key] || []).filter((x: any) => x.id !== sel.id); - if (!sp[key].length) delete sp[key]; - if (this._activeDraftId === sel.id) this._cancelPath(); - this._physicalSel = null; - this._physicalDialog = null; - this._commitPhysicalGeometry(this._t('history.physical_delete'), before); - }; + return this._editorRuntimeOrThrow()._deletePhysicalSelection(); + } private _confirmPartitionDelete = (): void => { - const dialog = this._partitionDeleteDialog; - const sp = this._curSpaceCfg as any; - if (!dialog || !sp) return; - const before = this._geometrySnapshot(); - sp.partitions = (sp.partitions || []).filter((partition: PartitionCfg) => - partition.id !== dialog.id); - if (!sp.partitions.length) delete sp.partitions; - sp.openings = (sp.openings || []).filter((opening: OpeningCfg) => - opening.host?.kind !== 'partition' || opening.host.id !== dialog.id); - if (!sp.openings.length) delete sp.openings; - this._partitionDeleteDialog = null; - this._physicalSel = null; - this._physicalDialog = null; - this._commitPhysicalGeometry(this._t('history.physical_delete'), before); - }; + return this._editorRuntimeOrThrow()._confirmPartitionDelete(); + } private _deleteDraftWhole = (): void => { - const id = this._physicalDialog?.kind === 'draft' - ? this._physicalDialog.id - : this._physicalSel?.kind === 'draft' ? this._physicalSel.id : null; - const sp = this._curSpaceCfg as any; - if (!id || !sp || !confirm(this._t('confirm.delete_draft'))) return; - const before = this._geometrySnapshot(); - sp.room_drafts = (sp.room_drafts || []).filter((x: any) => x.id !== id); - if (!sp.room_drafts.length) delete sp.room_drafts; - if (this._activeDraftId === id) this._cancelPath(); - this._physicalSel = null; - this._physicalDialog = null; - this._commitPhysicalGeometry(this._t('history.physical_delete'), before); - }; + return this._editorRuntimeOrThrow()._deleteDraftWhole(); + } private _deleteDraftSegment = (): void => { - const dlg = this._physicalDialog; - const sp = this._curSpaceCfg as any; - if (!dlg || dlg.kind !== 'draft' || !sp) return; - if (!confirm(this._t('confirm.delete_draft_segment'))) return; - const index = (sp.room_drafts || []).findIndex((x: any) => x.id === dlg.id); - if (index < 0) return; - const draft = sp.room_drafts[index]; - const cut = Math.max(0, Math.min(draft.segments.length - 1, dlg.segment || 0)); - const pieces: any[] = []; - const leftPoints = draft.points.slice(0, cut + 1); - const rightPoints = draft.points.slice(cut + 1); - if (leftPoints.length >= 2) pieces.push({ - id: draft.id, points: leftPoints, segments: draft.segments.slice(0, cut), - }); - if (rightPoints.length >= 2) pieces.push({ - id: pieces.length ? `${draft.id}-${Date.now().toString(36)}` : draft.id, - points: rightPoints, segments: draft.segments.slice(cut + 1), - }); - if (pieces.length === 2 && sp.room_drafts.length >= MAX_ROOM_DRAFTS) { - this._showToast(this._t('toast.physical_limit')); - return; - } - const before = this._geometrySnapshot(); - sp.room_drafts.splice(index, 1, ...pieces); - if (!sp.room_drafts.length) delete sp.room_drafts; - if (this._activeDraftId === draft.id) this._cancelPath(); - this._physicalDialog = null; - this._physicalSel = pieces.length ? { kind: 'draft', id: pieces[0].id } : null; - this._commitPhysicalGeometry(this._t('history.draft_segment_delete'), before); - }; + return this._editorRuntimeOrThrow()._deleteDraftSegment(); + } private _physicalDown(ev: PointerEvent, kind: 'partition' | 'column', id: string): void { - const model = this._spaceModel(); - if (!model) return; - ev.stopPropagation(); - capturePointer(ev); - const point = this._svgPoint(ev); - const candidates: Array<{ kind: 'partition' | 'column'; id: string }> = []; - for (const c of [...model.wall_columns].reverse()) { - if (pointInPhysicalBody(point, columnBody(c, this._cellCm, this._gridPitch))) - candidates.push({ kind: 'column', id: c.id }); - } - for (const p of [...model.partitions].reverse()) { - const body = partitionBody(p.a, p.b, p.cm, this._cellCm, this._gridPitch); - if (body && pointInPhysicalBody(point, body)) candidates.push({ kind: 'partition', id: p.id }); - } - if (!candidates.some((x) => x.kind === kind && x.id === id)) candidates.unshift({ kind, id }); - const signature = candidates.map((x) => `${x.kind}:${x.id}`).sort().join('|'); - const now = performance.now(); - const cycle = this._physicalPickCycle; - const repeat = candidates.length > 1 && cycle?.signature === signature - && now - cycle.at > 380 && now - cycle.at <= 1200 - && Math.hypot(ev.clientX - cycle.x, ev.clientY - cycle.y) <= 10; - const index = repeat ? (cycle.index + 1) % candidates.length - : Math.max(0, candidates.findIndex((x) => x.kind === kind && x.id === id)); - this._physicalPickCycle = { signature, index, x: ev.clientX, y: ev.clientY, at: now }; - kind = candidates[index].kind; - id = candidates[index].id; - const base = kind === 'partition' - ? model.partitions.find((x) => x.id === id) - : model.wall_columns.find((x) => x.id === id); - if (!base) return; - this._physicalSel = { kind, id }; - this._physicalDrag = { - pid: ev.pointerId, kind, id, start: this._svgPoint(ev), - startClient: [ev.clientX, ev.clientY], - before: this._geometrySnapshot(), moved: false, - base: JSON.parse(JSON.stringify(base)), delta: [0, 0], - }; + return this._editorRuntimeOrThrow()._physicalDown(ev, kind, id); } private _clampPhysicalDelta( kind: 'partition' | 'column', base: PartitionCfg | WallColumnCfg, delta: number[], ): number[] { - const points = kind === 'partition' - ? [(base as PartitionCfg).a, (base as PartitionCfg).b] - : [(base as WallColumnCfg).center]; - const xs = points.map((p) => p[0]), ys = points.map((p) => p[1]); - return [ - Math.max(-CANVAS_LIMIT * NORM_W - Math.min(...xs), - Math.min(CANVAS_LIMIT * NORM_W - Math.max(...xs), delta[0])), - Math.max(-CANVAS_LIMIT * NORM_W - Math.min(...ys), - Math.min(CANVAS_LIMIT * NORM_W - Math.max(...ys), delta[1])), - ]; + return this._editorRuntimeOrThrow()._clampPhysicalDelta(kind, base, delta); } private _physicalMove(ev: PointerEvent): void { - const drag = this._physicalDrag; - if (!drag || drag.pid !== ev.pointerId) return; - ev.stopPropagation(); - const raw = this._svgPoint(ev); - const anchor = drag.kind === 'partition' - ? (drag.base as PartitionCfg).a : (drag.base as WallColumnCfg).center; - const target = this._snap([ - anchor[0] + raw[0] - drag.start[0], anchor[1] + raw[1] - drag.start[1], - ]); - const delta = this._clampPhysicalDelta( - drag.kind, drag.base, [target[0] - anchor[0], target[1] - anchor[1]], - ); - this._physicalDrag = { - ...drag, delta, - moved: drag.moved || Math.hypot( - ev.clientX - drag.startClient[0], ev.clientY - drag.startClient[1], - ) >= 5, - }; + return this._editorRuntimeOrThrow()._physicalMove(ev); } private _physicalUp(ev: PointerEvent): void { - const drag = this._physicalDrag; - if (!drag || drag.pid !== ev.pointerId) return; - ev.stopPropagation(); - this._physicalDrag = null; - if (!drag.moved) { - this._registerPhysicalTap(drag.kind, drag.id); - return; - } - const model = this._spaceModel(); - if (!this._curSpaceCfg || !model) return; - const sp = this._curSpaceCfg as any; - if (drag.kind === 'partition') { - const p = (sp.partitions || []).find((x: any) => x.id === drag.id); - const base = drag.base as PartitionCfg; - if (p) { - const moved: PartitionCfg = { - ...base, - a: [base.a[0] + drag.delta[0], base.a[1] + drag.delta[1]], - b: [base.b[0] + drag.delta[0], base.b[1] + drag.delta[1]], - }; - p.a = [moved.a[0] / NORM_W, moved.a[1] / NORM_W]; - p.b = [moved.b[0] / NORM_W, moved.b[1] / NORM_W]; - for (const opening of sp.openings || []) { - if (opening.host?.kind !== 'partition' || opening.host.id !== drag.id) continue; - const resolved = resolvePartitionOpeningCompat( - opening, [moved], NORM_W, this._cellCm, this._gridPitch, - ).resolved; - if (resolved) Object.assign( - opening, materializePartitionOpening(opening, resolved, NORM_W), - ); - } - } - } else { - const c = (sp.wall_columns || []).find((x: any) => x.id === drag.id); - const base = drag.base as WallColumnCfg; - const candidate: WallColumnCfg = { - ...base, - center: [base.center[0] + drag.delta[0], base.center[1] + drag.delta[1]], - } as WallColumnCfg; - if (model.wall_columns.some((x) => x.id !== drag.id - && sameColumnPlacement(x, candidate, this._gridPitch * 0.02))) { - this._showToast(this._t('toast.column_duplicate')); - return; - } - if (c) c.center = [ - (base.center[0] + drag.delta[0]) / NORM_W, - (base.center[1] + drag.delta[1]) / NORM_W, - ]; - } - this._commitPhysicalGeometry(this._t('history.physical_move'), drag.before); + return this._editorRuntimeOrThrow()._physicalUp(ev); } private _registerPhysicalTap( kind: 'partition' | 'column' | 'draft', id: string, segment?: number, ): void { - const now = performance.now(); - const twice = this._physicalLastTap?.kind === kind - && this._physicalLastTap.id === id - && this._physicalLastTap.segment === segment - && now - this._physicalLastTap.at <= 360; - this._physicalLastTap = { kind, id, segment, at: now }; - if (twice) { - this._physicalLastTap = null; - this._openPhysicalDialog(kind, id, segment); - } + return this._editorRuntimeOrThrow()._registerPhysicalTap(kind, id, segment); } private _cancelPhysicalGesture(): void { - this._physicalDrag = null; - this._physicalRotate = null; - this.requestUpdate(); + return this._editorRuntimeOrThrow()._cancelPhysicalGesture(); } private _physicalRotateDown(ev: PointerEvent, c: WallColumnCfg): void { - if (c.shape !== 'square' || !this._spaceModel()) return; - ev.preventDefault(); - ev.stopPropagation(); - capturePointer(ev); - const p = this._svgPoint(ev); - this._physicalSel = { kind: 'column', id: c.id }; - this._physicalRotate = { - pid: ev.pointerId, id: c.id, center: [...c.center], - startAngle: Math.atan2(p[1] - c.center[1], p[0] - c.center[0]) * 180 / Math.PI, - baseAngle: canonicalColumnAngle(c.angle), angle: canonicalColumnAngle(c.angle), - before: this._geometrySnapshot(), moved: false, - }; + return this._editorRuntimeOrThrow()._physicalRotateDown(ev, c); } private _physicalRotateMove(ev: PointerEvent): void { - const drag = this._physicalRotate; - if (!drag || drag.pid !== ev.pointerId) return; - ev.preventDefault(); - ev.stopPropagation(); - const p = this._svgPoint(ev); - const pointerAngle = Math.atan2(p[1] - drag.center[1], p[0] - drag.center[0]) * 180 / Math.PI; - const raw = drag.baseAngle + pointerAngle - drag.startAngle; - const angle = canonicalColumnAngle(ev.shiftKey ? raw : Math.round(raw / 5) * 5); - this._physicalRotate = { ...drag, angle, moved: drag.moved || Math.abs(raw - drag.baseAngle) >= 0.5 }; + return this._editorRuntimeOrThrow()._physicalRotateMove(ev); } private _physicalRotateUp(ev: PointerEvent): void { - const drag = this._physicalRotate; - if (!drag || drag.pid !== ev.pointerId) return; - ev.preventDefault(); - ev.stopPropagation(); - this._physicalRotate = null; - const model = this._spaceModel(); - if (!drag.moved || !this._curSpaceCfg || !model) return; - const current = model.wall_columns.find((c) => c.id === drag.id); - if (!current || current.shape !== 'square') return; - const candidate: WallColumnCfg = { ...current, angle: drag.angle }; - if (model.wall_columns.some((c) => c.id !== drag.id - && sameColumnPlacement(c, candidate, this._gridPitch * 0.02))) { - this._showToast(this._t('toast.column_duplicate')); - return; - } - const stored = (this._curSpaceCfg as any).wall_columns?.find((c: any) => c.id === drag.id); - if (!stored) return; - stored.angle = drag.angle; - this._commitPhysicalGeometry(this._t('history.physical_edit'), drag.before); + return this._editorRuntimeOrThrow()._physicalRotateUp(ev); } @@ -8873,121 +7217,34 @@ class HouseplanCard extends LitElement { /** Rooms of the current space as render-unit polygons (legacy rects converted). */ private _rszRooms(): { id: string; poly: number[][]; wall_ids?: string[] }[] { - const out: { id: string; poly: number[][]; wall_ids?: string[] }[] = []; - const space = this._spaceModel(); - if (!space) return out; - for (const r of space.rooms) { - const poly = r.id ? roomPoly(r) : null; - if (poly) out.push({ - id: r.id!, poly, - wall_ids: Array.isArray(r.wall_ids) ? [...r.wall_ids] : undefined, - }); - } - return coalesceResizeRooms(out, Math.max(1e-12, this._gridPitch * 1e-12)); + return this._editorRuntimeOrThrow()._rszRooms(); } private _rszOpenings(): SafeOpeningIn[] { - return this._openingsR.map((o) => ({ - id: o.id, x: o.rx, y: o.ry, length: o.rlen, - hosted: !!o.host, angle: o.angle, type: o.type, - })); + return this._editorRuntimeOrThrow()._rszOpenings(); } private _rszObstacles(): SafeResizeObstacle[] { - const space = this._spaceModel(); - if (!space) return []; - const obstacles: SafeResizeObstacle[] = []; - for (const partition of space.partitions || []) { - obstacles.push({ - kind: 'segment', a: [...partition.a], b: [...partition.b], - half: wallCmToUnits(partition.cm, this._cellCm, this._gridPitch) / 2, - }); - } - for (const draft of space.room_drafts || []) { - for (let i = 0; i + 1 < draft.points.length; i++) { - obstacles.push({ - kind: 'segment', a: [...draft.points[i]], b: [...draft.points[i + 1]], - half: wallCmToUnits( - Number.isFinite(Number(draft.segments?.[i]?.cm)) - ? Number(draft.segments[i].cm) : DRAW_WALL_DEFAULT_CM, - this._cellCm, this._gridPitch, - ) / 2, - }); - } - } - for (const column of space.wall_columns || []) { - const half = wallCmToUnits(column.cm, this._cellCm, this._gridPitch) / 2; - obstacles.push({ - kind: 'circle', center: [...column.center], - radius: column.shape === 'square' ? half * Math.SQRT2 : half, - }); - } - return obstacles; + return this._editorRuntimeOrThrow()._rszObstacles(); } private _rszOptsFor(a: number[], b: number[]): SafeResizeOptions { - const cm = thicknessCmAt(this._spaceWalls, a, b, this._wallKeyPitch, NORM_W); - const exact = this._spaceWalls.filter((wall) => Array.isArray(wall.a) && Array.isArray(wall.b) - && wall.a.length >= 2 && wall.b.length >= 2).map((wall) => ({ - wall, - a: [wall.a![0] * NORM_W, wall.a![1] * this._spaceH], - b: [wall.b![0] * NORM_W, wall.b![1] * this._spaceH], - })); - const axis = Math.abs(a[0] - b[0]) <= this._gridPitch * 0.05 ? 'v' - : Math.abs(a[1] - b[1]) <= this._gridPitch * 0.05 ? 'h' : null; - const overlaps = exact.filter((entry) => { - if (!axis) return false; - if (axis === 'h') { - if (Math.abs(entry.a[1] - a[1]) > this._gridPitch * 0.05 - || Math.abs(entry.b[1] - a[1]) > this._gridPitch * 0.05) return false; - return Math.min(Math.max(a[0], b[0]), Math.max(entry.a[0], entry.b[0])) - - Math.max(Math.min(a[0], b[0]), Math.min(entry.a[0], entry.b[0])) > this._gridPitch * 0.05; - } - if (Math.abs(entry.a[0] - a[0]) > this._gridPitch * 0.05 - || Math.abs(entry.b[0] - a[0]) > this._gridPitch * 0.05) return false; - return Math.min(Math.max(a[1], b[1]), Math.max(entry.a[1], entry.b[1])) - - Math.max(Math.min(a[1], b[1]), Math.min(entry.a[1], entry.b[1])) > this._gridPitch * 0.05; - }); - const exactCms = new Set(overlaps.map((entry) => Number(entry.wall.cm)).filter((value) => value > 0)); - return { - minDim: this._cmToUnits(MIN_ROOM_CM), - eps: this._gridPitch * 0.05, - step: this._gridPitch, - movingHalf: cm > 0 ? wallCmToUnits(cm, this._cellCm, this._gridPitch) / 2 : 0, - obstacles: this._rszObstacles(), - thicknessConflict: exactCms.size > 1, - }; + return this._editorRuntimeOrThrow()._rszOptsFor(a, b); } private _rszResolution( roomId: string, edge: number, renderSnapshot?: string, ): SafeResizeResolution { - const snap = this._resize.snapshotIdentity || renderSnapshot || this._rszSnapshot(); - const key = `${this._space}|${this._cellCm}|${this._gridPitch}|${snap}`; - const cacheKey = `${roomId}:${edge}`; - return this._resize.resolve(key, cacheKey, () => { - const rooms = [...(this._resize.rooms || this._rszRooms())]; - const room = rooms.find((candidate) => candidate.id === roomId); - const a = room?.poly?.[edge] || [0, 0]; - const b = room?.poly?.[(edge + 1) % (room?.poly?.length || 1)] || [0, 0]; - return resolveSafeResize( - rooms, [...(this._resize.openings || this._rszOpenings())], roomId, edge, - this._rszOptsFor(a, b), - ); - }); + return this._editorRuntimeOrThrow()._rszResolution(roomId, edge, renderSnapshot); } private _rszSnapshot(): string { - return JSON.stringify(this._geometrySnapshot() || { - spaceId: this._space, rooms: [], openings: [], walls: [], open_spans: [], - }); + return this._editorRuntimeOrThrow()._rszSnapshot(); } /** Lifecycle reset which cannot leave the memoized model on a discarded preview. */ private _rszResetController(): void { - const hadPreview = this._resize.preview !== null; - this._resize.reset(); - if (hadPreview) this._cfgEpoch++; + return this._editorRuntimeOrThrow()._rszResetController(); } /** Live preview of the candidate geometry, based on the immutable pre-drag snapshot — @@ -9010,161 +7267,14 @@ class HouseplanCard extends LitElement { changedRoomIds: readonly string[], sourceRooms: readonly { id: string; poly: number[][]; wall_ids?: string[] }[], ): ResizeProjectionResult { - // #329 AC7a: only the LAST projection may explain the refusal, so the - // previous verdict never leaks into a rejection of another kind. - this._rszLimitViolation = null; - const real = this._serverCfg?.spaces.find((s: any) => s.id === this._space); - if (!real || !this._serverCfg) return { ok: false, reason: 'missing-context' }; - const s = JSON.parse(snapshot); // fresh deep copies every move — free to mutate - const sp: any = { - ...real, - rooms: s.rooms, - openings: s.openings || [], - }; - for (const key of ['walls', 'wall_segments', 'open_spans', 'room_drafts', 'partitions', 'wall_columns', 'decor'] as const) { - if (s[key] !== undefined) (sp as any)[key] = s[key]; - else delete (sp as any)[key]; - } - for (const key of ['plan_x', 'plan_y', 'plan_scale', 'plan_scale_x', 'plan_scale_y', 'plan_angle'] as const) { - if (s.plan_transform?.[key] !== undefined) (sp as any)[key] = s.plan_transform[key]; - else delete (sp as any)[key]; - } - const H = this._spaceH; - for (const [id, poly] of Object.entries(polys)) { - const r = sp.rooms.find((x: any) => x.id === id); - if (!r) continue; - r.poly = poly.map((p) => [p[0] / NORM_W, p[1] / H]); - delete r.x; delete r.y; delete r.w; delete r.h; // a resized room is saved as a polygon - } - for (const [id, c] of Object.entries(ops)) { - const o = (sp.openings || []).find((x: any) => x.id === id); - if (!o) continue; - o.x = c[0] / NORM_W; - o.y = c[1] / H; - } - // Geometry that belongs to a wall must ride in the SAME live overlay as - // its room polygons. Map from the immutable snapshot on every move (never - // from the previous preview), so atomic identity and thickness cannot lag - // behind or accumulate rounding error during a long drag. - const oldSpans: [number[], number[]][] = []; - const newSpans: [number[], number[]][] = []; - for (const id of changedRoomIds) { - const oldR = sourceRooms.find((r) => r.id === id); - const nr = sp.rooms.find((x: any) => x.id === id); - if (!oldR || !nr?.poly) continue; - const newPoly = nr.poly.map((p: number[]) => [p[0] * NORM_W, p[1] * H] as number[]); - if (oldR.poly.length !== newPoly.length) continue; - const ids = Array.isArray((oldR as any).wall_ids) ? (oldR as any).wall_ids : []; - for (let i = 0; i < oldR.poly.length; i++) { - oldSpans.push([oldR.poly[i], oldR.poly[(i + 1) % oldR.poly.length]]); - newSpans.push([newPoly[i], newPoly[(i + 1) % newPoly.length]]); - const id = ids[i]; - const segment = typeof id === 'string' - ? (sp.wall_segments || []).find((item: any) => item.id === id) - : null; - if (segment) { - segment.a = [newPoly[i][0] / NORM_W, newPoly[i][1] / NORM_W]; - segment.b = [ - newPoly[(i + 1) % newPoly.length][0] / NORM_W, - newPoly[(i + 1) % newPoly.length][1] / NORM_W, - ]; - } - } - } - if (oldSpans.length) { - if (Array.isArray(sp.walls) && sp.walls.length) { - const rekeyed = rekeyWallsAfterMoveChecked( - sp.walls, oldSpans, newSpans, this._wallKeyPitch, NORM_W, 'fixed-topology', - ); - if (rekeyed.rejected) return { ok: false, reason: 'wall-metadata' }; - sp.walls = rekeyed.walls; - } - } - const wallCarriers: [number[], number[]][] = []; - for (const room of sp.rooms || []) { - const poly = roomPoly(room); - if (!poly || poly.length < 2) continue; - for (let index = 0; index < poly.length; index++) { - wallCarriers.push([ - [poly[index][0] * NORM_W, poly[index][1] * NORM_W], - [poly[(index + 1) % poly.length][0] * NORM_W, - poly[(index + 1) % poly.length][1] * NORM_W], - ]); - } - } - // Old plans may contain explicit historical debt (for example an authored - // off-grid thickness breakpoint). Safe Resize must not silently repair it, - // but it may not create a new invalid record either. Remove byte-identical - // old records as a multiset and prove only newly written/split records. - // That keeps pointermove bounded by the touched thickness profile instead - // of comparing every historical record with every carrier twice per frame. - const wallSignature = (wall: any): string => JSON.stringify([ - wall?.key, wall?.cm, wall?.a, wall?.b, - ]); - const oldWallCounts = new Map(); - for (const wall of s.walls || []) { - const key = wallSignature(wall); - oldWallCounts.set(key, (oldWallCounts.get(key) || 0) + 1); - } - const changedWalls: WallEntry[] = []; - for (const wall of sp.walls || []) { - const key = wallSignature(wall); - const remaining = oldWallCounts.get(key) || 0; - if (remaining) oldWallCounts.set(key, remaining - 1); - else changedWalls.push(wall); - } - if (wallRecordCarrierViolations( - changedWalls, wallCarriers, this._wallKeyPitch, NORM_W, s.walls || [], - ).length) return { ok: false, reason: 'wall-metadata' }; - const preflight = this._rszSpaceCandidateGeometry(this._space, sp); - if (!preflight.ok) { - return { ok: false, reason: 'physical-geometry' }; - } - // #329 AC7a: a step that would ADD a junction-limit violation is never - // projected, so the drag stops at the last allowed position instead of - // committing an impossible plan. - const limitCandidate = { - ...this._serverCfg, - spaces: (this._serverCfg?.spaces || []).map( - (space: any) => (space?.id === this._space ? sp : space), - ), - }; - // #330 §4.7: the preflight just built the candidate's masonry — hand it - // to П5 so the pointermove never pays the union twice. - const limited = this._junctionLimitsIntroduced( - limitCandidate, this._serverCfg, this._space, preflight.wallGeometry, - ); - if (limited.length) { - this._rszLimitViolation = limited[0]; - return { ok: false, reason: 'junction-limit' }; - } - return { - ok: true, - value: { - preview: { space: this._space, sp }, - beforeWalls: s.walls || [], - afterWalls: sp.walls || [], - artifact: preflight.wallGeometry, - }, - }; + return this._editorRuntimeOrThrow()._rszProjectPreview(snapshot, polys, ops, changedRoomIds, sourceRooms); } /** Publish one controller-accepted preview and retain its validated masonry pass. */ private _rszAcceptPreview( preview: ResizePreview | null, wallGeometry: ResizeWallArtifact | null, ): void { - this._cfgEpoch++; - if (!preview || !wallGeometry) return; - const projected = wallBodiesGeometryPath(wallGeometry); - if (!projected) return; - const key = `${this._space}|${this._cfgEpoch}|${preview.sp.rooms.length}`; - Object.defineProperty(projected, 'sourceFingerprint', { - value: contentFingerprint([preview.sp, this._cellCm, this._gridPitch]), - enumerable: false, - }); - const entry = { key, value: projected }; - lruWrite(this._wallUnionPool, key, entry, 8); - this._wallUnionCache = entry; + return this._editorRuntimeOrThrow()._rszAcceptPreview(preview, wallGeometry); } /** Validate once and retain the exact geometry pass for the preview render. */ @@ -9172,183 +7282,45 @@ class HouseplanCard extends LitElement { ok: boolean; wallGeometry: ReturnType | null; } { - if (!this._serverCfg) return { ok: false, wallGeometry: null }; - const candidate = { - ...this._serverCfg, - spaces: this._serverCfg.spaces.map((space: any) => - space.id === spaceId ? sp : space), - } as ServerConfig; - let wallGeometry: ReturnType | null = null; - try { - const check = this._checkSpacePhysicalGeometry( - candidate, spaceId, (geometry) => { wallGeometry = geometry; }, - ); - return { ok: check.ok, wallGeometry }; - } catch { - return { ok: false, wallGeometry: null }; - } + return this._editorRuntimeOrThrow()._rszSpaceCandidateGeometry(spaceId, sp); } /** Fail-closed check for one exact candidate through the common barrier. */ private _rszSpaceCandidateRenderable(spaceId: string, sp: any): boolean { - if (!this._serverCfg) return false; - try { - const candidate = { - ...this._serverCfg, - spaces: this._serverCfg.spaces.map((space: any) => - space.id === spaceId ? sp : space), - } as ServerConfig; - return this._checkSpacePhysicalGeometry(candidate, spaceId).ok; - } catch { - return false; - } + return this._editorRuntimeOrThrow()._rszSpaceCandidateRenderable(spaceId, sp); } /** Final identity guard for the exact overlay already shown to the user. */ private _rszCandidateRenderable(preview: { space: string; sp: any } | null): boolean { - return !!preview && preview.space === this._space - && this._rszSpaceCandidateRenderable(preview.space, preview.sp); + return this._editorRuntimeOrThrow()._rszCandidateRenderable(preview); } private _rszEdgeDown(ev: PointerEvent, roomId: string, edge: number): void { - if (this._tool !== 'resize' || this._resize.dragging) return; - ev.stopPropagation(); - ev.preventDefault(); - const rooms = this._rszRooms(); - const resolution = this._rszResolution(roomId, edge); - if (!resolution.enabled) { - this._showToast(this._rszReasonText(resolution.reason)); - return; - } - capturePointer(ev); - const plan = resolution.plan; - const start = this._svgPoint(ev); - const wallUnionKey = `${this._space}|${this._cfgEpoch}|${rooms.length}`; - const wallUnionBefore = this._wallUnionCache?.key === wallUnionKey - ? this._wallUnionCache.value : null; - const snapshotIdentity = this._rszSnapshot(); - this._resize.begin({ - pointerId: ev.pointerId, start: [start[0], start[1]], roomId, plan, - options: this._rszOptsFor(plan.a, plan.b), rooms, - openings: this._rszOpenings(), snapshotIdentity, - before: JSON.parse(snapshotIdentity) as SpaceGeometryState, - wallUnionBefore, - }); + return this._editorRuntimeOrThrow()._rszEdgeDown(ev, roomId, edge); } private _rszReasonText(reason: SafeResizeReason): string { - return this._t(`resize.disabled.${reason}` as I18nKey); + return this._editorRuntimeOrThrow()._rszReasonText(reason); } private _rszDisabledActivate(ev: Event, reason: SafeResizeReason): void { - ev.stopPropagation(); - ev.preventDefault(); - this._showToast(this._rszReasonText(reason)); + return this._editorRuntimeOrThrow()._rszDisabledActivate(ev, reason); } private _rszDisabledKey(ev: KeyboardEvent, reason: SafeResizeReason): void { - if (ev.key !== 'Enter' && ev.key !== ' ') return; - this._rszDisabledActivate(ev, reason); + return this._editorRuntimeOrThrow()._rszDisabledKey(ev, reason); } private _rszMove(ev: PointerEvent): void { - if (!this._resize.ownsPointer(ev.pointerId)) return; - ev.stopPropagation(); - const p = this._svgPoint(ev); - const result = this._resize.move({ - pointerId: ev.pointerId, - point: [p[0], p[1]], - step: this._gridPitch, - snap: (point) => { - const snapped = this._snap(point); - return [snapped[0], snapped[1]]; - }, - project: (snapshot, polys, openings, changedRoomIds, rooms) => - this._rszProjectPreview(snapshot, polys, openings, changedRoomIds, rooms), - publish: (preview, artifact) => this._rszAcceptPreview(preview, artifact), - measure: (candidate, plan) => this._rszEdgeLabels(candidate, plan), - }); - if (result.kind === 'rejected') { - // Persistence metadata is part of the geometry transaction. If wall or - // virtual-span rekeying would be lossy, keep the last complete preview: - // the pointer visibly stops there and pointerup can commit only that - // already-rendered safe candidate. - if (result.notify) { - // #329 AC7a: a step stopped by a junction limit names THAT rule — the - // generic "geometry cannot be saved" wording would hide which limit - // the wall ran into. - this._showToast(this._rszLimitViolation - ? `${this._t('resize.limit_stopped')} — ` - + this._junctionLimitLabel(this._rszLimitViolation) - : this._t('resize.preview_failed')); - } - this.requestUpdate(); - return; - } - if (result.kind === 'accepted') this.requestUpdate(); + return this._editorRuntimeOrThrow()._rszMove(ev); } private _rszUp(ev: PointerEvent): void { - if (!this._resize.ownsPointer(ev.pointerId)) return; - ev.stopPropagation(); - const result = this._resize.finish({ - pointerId: ev.pointerId, - currentSnapshotIdentity: this._rszSnapshot(), - validatePreview: (preview) => this._rszCandidateRenderable(preview), - }); - if (result.kind === 'no-op') { - // HP-1550-01: nothing to restore — the preview never touched the config - this._cfgEpoch++; - this.requestUpdate(); - return; - } - if (result.kind === 'rejected') { - this._cfgEpoch++; - this._showToast(this._t('resize.commit_failed')); - this.requestUpdate(); - return; - } - // Commit the exact preview in one step. No simplify/rebuild pass is allowed: - // preview and persistence share the same fixed-topology candidate. - const preview = result.preview; - const sp = this._serverCfg?.spaces.find((space: any) => space.id === preview.space); - if (sp) { - sp.rooms = preview.sp.rooms; - sp.openings = preview.sp.openings; - if (Array.isArray(preview.sp.walls)) { - if (preview.sp.walls.length) sp.walls = preview.sp.walls; - else delete sp.walls; - } - if (Array.isArray(preview.sp.wall_segments)) sp.wall_segments = preview.sp.wall_segments; - } - // the click synthesized after the drag must not re-pick the selection - this._suppressClick = true; - setTimeout(() => (this._suppressClick = false), 0); - this._commitPhysicalGeometry(this._t('history.resize_room'), result.before); - this.requestUpdate(); + return this._editorRuntimeOrThrow()._rszUp(ev); } private _rszCancelDrag(pointerId?: number): void { - const result = this._resize.cancel(this._rszSnapshot(), pointerId); - if (result.kind === 'no-op') return; - // HP-1550-01/-03: a cancel just drops the overlay — the real config was - // never touched, so there is nothing to restore, no undo step and no write - this._cfgEpoch++; - if (result.restoreWallUnion) { - // The server-backed geometry did not change during the gesture. The - // preview advanced the structural epoch and temporarily selected its - // candidate union, but Cancel returns to the byte-identical pre-drag - // config. Alias that already-proved result under the fresh epoch instead - // of paying another O(n log n) polygon union on the next paint. - const space = this._spaceModel(); - if (space) { - const key = `${this._space}|${this._cfgEpoch}|${space.rooms.length}`; - const entry = { key, value: result.restoreWallUnion }; - lruWrite(this._wallUnionPool, key, entry, 8); - this._wallUnionCache = entry; - } - } - this.requestUpdate(); + return this._editorRuntimeOrThrow()._rszCancelDrag(pointerId); } /** HP-1550-03: pointercancel / lostpointercapture is an ABORT, not a release — @@ -9357,97 +7329,14 @@ class HouseplanCard extends LitElement { * guard also absorbs the lostpointercapture that follows a normal pointerup * or a pointercancel (the drag is already gone — no double cancel/commit). */ private _rszPointerCancel(ev: PointerEvent): void { - if (!this._resize.ownsPointer(ev.pointerId)) return; - ev.stopPropagation(); - this._rszCancelDrag(ev.pointerId); + return this._editorRuntimeOrThrow()._rszPointerCancel(ev); } private _rszEdgeLabels( res: { polys: Record }, plan: SafeResizePlan, - sourceRooms: readonly { id: string; poly: number[][] }[] | null = this._resize.rooms, + sourceRooms: readonly { id: string; poly: number[][] }[] | null = this._resize?.rooms, ): ResizeLiveLabel[] { - const rooms = sourceRooms; - const labels: ResizeLiveLabel[] = []; - const own = res.polys[plan.roomId] || rooms?.find((r) => r.id === plan.roomId)!.poly; - if (!own || !rooms) return labels; - const n = own.length; - // Длины — между внутренними гранями, как и площадь ниже (#233). Раньше - // здесь считалась осевая длина, и одно облачко подписей несло две разные - // конвенции: «3.00 × 4.00» по центрам стен рядом с площадью по полу. - const spanCms = this._rszInnerSpanCms(plan.roomId, own, res.polys); - // The moving wall is obvious under the pointer. Only its two side walls - // remain useful measurements, and each one owns the matching highlight. - for (const edge of resizeMeasuredEdges(own, plan.edge)) { - const a = own[edge], b = own[(edge + 1) % n]; - const cm = spanCms?.[edge]; - labels.push({ - kind: 'length', - x: (a[0] + b[0]) / 2, - y: (a[1] + b[1]) / 2, - text: cm == null ? this._fmtLen(a, b) - : formatLength(cm, this.hass?.config?.unit_system?.length === 'mi'), - edge: { a: [a[0], a[1]], b: [b[0], b[1]] }, - }); - } - // Live areas sit beside the moving wall, one on each owner side. Placement - // is pure screen-space math fed by the stage size cached by ResizeObserver; - // no pointermove DOM measurement is allowed. - const imperial = this.hass?.config?.unit_system?.length === 'mi'; - const ids = plan.roomIds; - const walls = this._spaceWalls; - const physical = this._physicalBodiesR(); - const base = this._baseVb(); - const currentView = this._view && this._view.w > 0 && this._view.h > 0 - ? this._view - : { x: base[0], y: base[1], w: base[2], h: base[3] }; - const [cachedW, cachedH] = this._lastValidStageSize || [currentView.w, currentView.h]; - const view = { - ...currentView, - stageWidth: Math.max(1, cachedW), - stageHeight: Math.max(1, cachedH), - }; - const space = this._spaceModel(); - const cfgSize = this._config?.icon_size ?? 2.5; - const iconPct = cfgSize > 8 ? 2.5 : cfgSize; - const iconPx = space - ? iconCqw(iconPct, space, currentView.w, this._kiosk ? this._kioskScale.icon : 1) - * view.stageWidth / 100 - : 24; - const gearHeightPx = Math.max(10, iconPx * 0.77); - const gearText = this._t('room.settings_short'); - // Mirrors the CSS proportions conservatively: icon + gap + two paddings + - // localized text at the button's 0.42*height font size. - const gearWidthPx = gearHeightPx * ( - 0.55 + 0.35 * 0.42 + 0.76 + Math.max(1, gearText.length) * 0.66 * 0.42 - ); - for (const id of ids) { - const poly = res.polys[id] || rooms.find((r) => r.id === id)!.poly; - // The preview is already the active render model. Reuse the same shared - // masonry union + contour cache that the following render consumes; - // rebuilding both independently here doubled one Resize frame. - const floor = walls.length && space - ? (this._innerRoomContour(space, id) || poly) - : poly; - const m2 = physical.length - ? geometryArea(floorMinusBodies(floor, physical)) - * Math.pow(this._cellCm / this._gridPitch, 2) / 1e4 - : areaM2(floor, this._gridPitch, this._cellCm); - const text = formatArea(m2, imperial); - const placement = placeResizeAreaLabel({ - poly, - edge: plan.edgeByRoom[id], - text, - view, - gearCenter: poleOfInaccessibility(poly), - gearWidthPx, - gearHeightPx, - }); - labels.push({ - kind: 'area', roomId: id, - x: placement.anchor[0], y: placement.anchor[1], text, placement, - }); - } - return labels; + return this._editorRuntimeOrThrow()._rszEdgeLabels(res, plan, sourceRooms); } /** @@ -9461,89 +7350,18 @@ class HouseplanCard extends LitElement { private _rszInnerSpanCms( roomId: string, own: number[][], polys: Record, ): number[] | null { - const rooms = Object.keys(polys).length - ? Object.entries(polys).map(([id, poly]) => ({ id, poly })) - : this._spaceModel()?.rooms; - if (!rooms?.length) return null; - const walls = this._spaceWalls; - if (!walls.length) return null; - const openCuts = this._openCuts(); - const offsets = ownEdgeOffsets( - rooms, roomId, walls, openCuts, - this._wallKeyPitch, this._cellCm, this._gridPitch, NORM_W, - ); - if (!offsets || offsets.length !== own.length) return null; - const perUnitCm = this._cellCm / this._gridPitch; - return own.map((_, edge) => innerEdgeSpan(own, edge, offsets) * perUnitCm); + return this._editorRuntimeOrThrow()._rszInnerSpanCms(roomId, own, polys); } /** Pointer-inert measurement ink above masonry and below openings/handles. */ private _renderResizeMeasurements(): TemplateResult | typeof nothing { - const live = this._resize.liveLabels; - if (!live?.length) return nothing; - const lengths = live.filter((label) => label.kind === 'length'); - const areas = live.filter((label) => label.kind === 'area'); - return svg``; + return this._editorRuntimeOrThrow()._renderResizeMeasurements(); } /** Fixed-topology wall handles. Disabled geometry remains visible and * explains why it cannot start a gesture; the old corner scale is gone. */ private _renderResizeLayer(view: { x: number; y: number; w: number; h: number }): TemplateResult { - const hr = Math.max(view.w * 0.013, 5); // finger-sized HIT radius on touch, like .vacfithandle - // Wall-handle glyph: half the old circle — a wall segment with two arrows - // pointing perpendicular to it (the directions the wall can be dragged). - // Drawn in local coords (wall along X), rotated per edge at render time. - // The invisible circle above keeps the full finger-sized hit area and the - // HP-1550-04 hit-test priority over openings. - const s = hr / 2, f = (v: number) => v.toFixed(1); - const iconD = - `M ${f(-0.7 * s)} 0 H ${f(0.7 * s)}` + - ` M 0 ${f(-0.22 * s)} V ${f(-s)} M ${f(-0.32 * s)} ${f(-0.6 * s)} L 0 ${f(-s)} L ${f(0.32 * s)} ${f(-0.6 * s)}` + - ` M 0 ${f(0.22 * s)} V ${f(s)} M ${f(-0.32 * s)} ${f(0.6 * s)} L 0 ${f(s)} L ${f(0.32 * s)} ${f(0.6 * s)}`; - const parts: TemplateResult[] = []; - const rooms = this._rszRooms(); - // A geometry snapshot is a deep clone/serialization. It is the cache key - // for every handle in this frame, so compute it once per layer render — - // never once per room edge. During a drag the immutable gesture snapshot - // remains authoritative inside _rszResolution. - const renderSnapshot = this._resize.snapshotIdentity || this._rszSnapshot(); - for (const r of rooms) { - for (let i = 0; i < r.poly.length; i++) { - const a = r.poly[i], b = r.poly[(i + 1) % r.poly.length]; - if (Math.hypot(b[0] - a[0], b[1] - a[1]) < this._gridPitch) continue; - const mx = f((a[0] + b[0]) / 2), my = f((a[1] + b[1]) / 2); - const ang = f(Math.atan2(b[1] - a[1], b[0] - a[0]) * 180 / Math.PI); - const resolution = this._rszResolution(r.id, i, renderSnapshot); - const disabled = !resolution.enabled; - const reason = disabled ? this._rszReasonText(resolution.reason) : this._t('title.markup_resize'); - parts.push(svg` this._rszEdgeDown(e, r.id, i)} - @pointermove=${(e: PointerEvent) => this._rszMove(e)} - @pointerup=${(e: PointerEvent) => this._rszUp(e)} - @pointercancel=${(e: PointerEvent) => this._rszPointerCancel(e)} - @lostpointercapture=${(e: PointerEvent) => this._rszPointerCancel(e)} - @click=${disabled ? (e: Event) => this._rszDisabledActivate(e, resolution.reason) : null} - @keydown=${disabled ? (e: KeyboardEvent) => this._rszDisabledKey(e, resolution.reason) : null}> - ${reason} - `); - parts.push(svg``); - } - } - return svg`${parts}`; + return this._editorRuntimeOrThrow()._renderResizeLayer(view); } /** Openings of the current space in render units. */ @@ -9589,10 +7407,10 @@ class HouseplanCard extends LitElement { ): PartitionOpeningCut[] { if (!space) return []; const config = this._curSpaceCfg?.id === space.id ? this._curSpaceCfg : null; - const openings = geometryOpenings( - config, space, this._cellCm, this._gridPitch, NORM_W, + return geometryPartitionOpeningCuts( + geometryOpenings(config, space, this._cellCm, this._gridPitch, NORM_W), + accept, ); - return geometryPartitionOpeningCuts(openings, accept); } /** @@ -9708,484 +7526,72 @@ class HouseplanCard extends LitElement { /** Magnet candidates are intentionally limited to decor and room contours. */ private _decorSnapGeometry(excludeId?: string): SnapGeometry { - const space = this._spaceModel(); - if (!space) return { points: [], segments: [] }; - const cacheKey = excludeId || ''; - const cached = this._decorSnapCache; - if (cached && cached.epoch === this._cfgEpoch && cached.space === this._space - && cached.height === this._decorH && cached.exclude === cacheKey) { - return cached.geometry; - } - const parts: SnapGeometry[] = []; - for (const shape of this._decorList) { - if (shape.id === excludeId) continue; - if (shape.kind === 'line') { - const a = [shape.x1 * NORM_W, shape.y1 * this._decorH]; - const b = [shape.x2 * NORM_W, shape.y2 * this._decorH]; - parts.push({ - points: [a, b, [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]], - segments: [{ a, b }], - }); - } else if (shape.kind === 'text') { - parts.push({ points: [[shape.x * NORM_W, shape.y * this._decorH]], segments: [] }); - } else { - const box = this._decorBoxOf(shape); - if (box) parts.push(boxAnchors(box)); - } - } - for (const room of space.rooms) { - const poly = roomPoly(room); - if (!poly?.length) continue; - parts.push({ - points: poly.flatMap((p, i) => { - const q = poly[(i + 1) % poly.length]; - return [p, [(p[0] + q[0]) / 2, (p[1] + q[1]) / 2]]; - }), - segments: poly.map((p, i) => ({ a: p, b: poly[(i + 1) % poly.length] })), - }); - } - const geometry = mergeSnapGeometry(parts); - this._decorSnapCache = { - epoch: this._cfgEpoch, space: this._space, height: this._decorH, - exclude: cacheKey, geometry, - }; - return geometry; + return this._editorRuntimeOrThrow()._decorSnapGeometry(excludeId); } private _decorSnap(raw: number[], pointerType = 'mouse', excludeId?: string): number[] { - const stage = this._stageEl; - const view = this._viewOr(this._baseVb()); - const px = pointerType === 'touch' || pointerType === 'pen' ? 14 : 8; - const tolerance = stage ? (view.w / Math.max(1, stage.clientWidth)) * px : this._gridPitch; - return snapDecorPoint( - raw, this._decorSnapGeometry(excludeId), tolerance, - (point) => this._snap(point), - ).point; + return this._editorRuntimeOrThrow()._decorSnap(raw, pointerType, excludeId); } private _replaceDecor(id: string, patch: Partial): void { - const sp = this._curSpaceCfg; - if (!sp) return; - sp.decor = this._decorList.map((shape) => shape.id === id ? { ...shape, ...patch } : shape); - // The edited shape is excluded from the gesture's magnet candidates, so - // keep the cached geometry of every *other* object across pointermoves. - // A different exclude id/cache key rebuilds it when the next gesture starts. - this.requestUpdate(); + return this._editorRuntimeOrThrow()._replaceDecor(id, patch); } /** Esc/Ctrl+Z during a live gesture restores its transaction start without creating history. */ private _cancelDecorGesture(): void { - const before = this._decorMove?.before || this._dtDrag?.before || this._bdDrag?.before; - const sp = before && this._serverCfg?.spaces.find((space: any) => space.id === before.spaceId); - if (before && sp) { - const copy = (value: T): T => JSON.parse(JSON.stringify(value)); - if (before.decor !== undefined) sp.decor = copy(before.decor); - else delete sp.decor; - for (const key of ['plan_x', 'plan_y', 'plan_scale', 'plan_scale_x', 'plan_scale_y', 'plan_angle'] as const) - delete sp[key]; - Object.assign(sp, copy(before.plan_transform || {})); - this._cfgEpoch++; - } - this._decorMove = null; - this._dtDrag = null; - this._bdDrag = null; - this.requestUpdate(); + return this._editorRuntimeOrThrow()._cancelDecorGesture(); } /** Begin a decor gesture. Returns true when the event is consumed (no pan). */ private _decorPointerDown(ev: PointerEvent): boolean { - const t = this._decorTool; - // A DRAWING tool owns the whole canvas. Pressing on top of an existing - // shape must start a NEW figure at that very point — otherwise a line can - // never begin at the end of another line, because the old line grabs the - // press first (owner, 2026-08-04). Only select/erase talk to shapes, and - // only for them does the shape's own handler get to deal with the event. - const onShape = (t === 'select' || t === 'erase') - ? ((ev.target as HTMLElement).closest?.('.dshape') as SVGElement | null) - : null; - if (onShape) return true; // the shape's own handler deals with it - if (t === 'line' || t === 'rect' || t === 'ellipse') { - ev.preventDefault(); - const p = this._decorSnap(this._svgPoint(ev), ev.pointerType); - this._decorDraft = { kind: t, a: p, b: p, pid: ev.pointerId }; - capturePointer(ev); - return true; - } - if (t === 'text') { - // …and the press did NOT land on an existing label: those are the one - // exception to the inertness above (see _decorShapeDown). - const p = this._decorSnap(this._svgPoint(ev), ev.pointerType); - this._decorTextDialog = { - x: clampCanvasN(p[0] / NORM_W), y: clampCanvasN(p[1] / this._decorH), - text: '', color: this._decorStyle.color, opacity: this._decorStyle.opacity, - angle: '0', sizeCm: decorUnitsToCm(DECOR_TEXT_BASE, this._cellCm, this._gridPitch), - }; - this._decorTextSelection = { start: 0, end: 0 }; - return true; - } - if (t === 'furniture') { - // The furniture tool is a STAMP: the palette arms a symbol, the press - // puts it down at its real size and the editor goes back to `select` - // with the new piece selected (owner: «сразу выделен»). Without an armed - // symbol the press does nothing but keep the pan — pressing the canvas - // must not silently place whatever was chosen last week. - if (!this._furnPalette) return false; - ev.preventDefault(); - this._furnPlace(this._svgPoint(ev), ev.shiftKey); - return true; - } - // Empty-space selection belongs only to Select. Erase is an object - // command: a miss must be a true no-op. With SVG text its painted glyphs - // do not cover the whole logical label box, so clearing here made a click - // between letters appear to erase only the selection outline. - if (t === 'select') this._decorSel = null; - // …and under its own tool the picture is grabbable by its body - // (docs/BACKDROP.md §2). Only INSIDE the image rect: press beside the - // picture and the plane still pans with one finger. - if (this._bdMovable) { - const r = this._bdRect!; - const p = this._svgPoint(ev); - const cx = r.x + r.w / 2, cy = r.y + r.h / 2; - const a = (-normalizeAngle(r.angle) * Math.PI) / 180; - const dx = p[0] - cx, dy = p[1] - cy; - const local = [cx + dx * Math.cos(a) - dy * Math.sin(a), cy + dx * Math.sin(a) + dy * Math.cos(a)]; - if (local[0] >= r.x && local[0] <= r.x + r.w && local[1] >= r.y && local[1] <= r.y + r.h) { - ev.preventDefault(); - return this._bdStart(ev); - } - } - return false; // pan is allowed + return this._editorRuntimeOrThrow()._decorPointerDown(ev); } /** Commit the dragged shape (ignore degenerate ones) and persist. */ private _decorCommitDraft(): void { - const d = this._decorDraft; - this._decorDraft = null; - if (!d) return; - const min = this._gridPitch * 0.5; - if (!validDecorDraft(d.kind, d.a, d.b, min)) return; - const W = NORM_W, H = this._decorH; - const st = this._decorStyle; - const before = this._geometrySnapshot(); - const id = 'dc' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5); - // creation had no canvas guard at all while the MOVE did — a draft could be - // born outside the range the mover then refused to leave. One limit, both - // ends of the gesture (docs/CANVAS.md §9). - const cn = clampCanvasN; - let shape: DecorShape; - if (d.kind === 'line') { - shape = { id, kind: 'line', x1: cn(d.a[0] / W), y1: cn(d.a[1] / H), - x2: cn(d.b[0] / W), y2: cn(d.b[1] / H), ...decorStylePatch(st, false) } as DecorShape; - } else { - const x = cn(Math.min(d.a[0], d.b[0]) / W), y = cn(Math.min(d.a[1], d.b[1]) / H); - const w = Math.abs(d.b[0] - d.a[0]) / W, h = Math.abs(d.b[1] - d.a[1]) / H; - shape = { id, kind: d.kind, x, y, w, h, ...decorStylePatch(st, true) } as DecorShape; - } - const sp = this._curSpaceCfg; - sp.decor = [...this._decorList, shape]; - this._decorSel = id; - this._recordGeometry(this._t('history.decor_add'), before); - this._saveConfig(); - this.requestUpdate(); + return this._editorRuntimeOrThrow()._decorCommitDraft(); } /** Select tool: pointerdown on a shape starts moving it. */ private _decorShapeDown(ev: PointerEvent, shape: DecorShape): void { - if (this._mode !== 'decor') return; - // Under any other tool the shape is not a target at all: the press has to - // reach the stage, where the drawing tool starts a new figure (or the - // backdrop tool grabs the picture). Swallowing it here was the bug — the - // click on a line end did nothing but keep the old selection alive. - const t = this._decorTool; - // ONE exception (owner, 2026-08-04): under the TEXT tool an existing LABEL - // is a target again, and pressing it opens its editor instead of starting - // a new label on top of it. Everything else stays inert — a press on a - // line or a rectangle under the text tool still reaches the stage and - // creates a new label there (the CSS keeps them pointer-inert, this is - // the belt to that pair of braces). - if (t === 'text') { - if (shape.kind !== 'text') return; - ev.stopPropagation(); - ev.preventDefault(); - this._decorOpenText(shape); - return; - } - if (t !== 'select' && t !== 'erase') return; - ev.stopPropagation(); - ev.preventDefault(); - if (t === 'erase') { - this._decorEraseConfirm = { id: shape.id, kind: shape.kind }; - return; - } - this._decorSel = shape.id; - this._decorMove = { - id: shape.id, start: this._svgPoint(ev), orig: JSON.parse(JSON.stringify(shape)), - pid: ev.pointerId, moved: false, before: this._geometrySnapshot(), - }; - capturePointer(ev); + return this._editorRuntimeOrThrow()._decorShapeDown(ev, shape); } private _decorMoveUpdate(ev: PointerEvent): void { - const m = this._decorMove!; - // Furniture is dragged by its CENTRE and has a magnet of its own - // (docs/FURNITURE.md §5) — a different rule, not a different gesture. - if (m.orig?.kind === 'furniture') { this._furnMoveUpdate(ev); return; } - const p = this._svgPoint(ev); - const o0: any = m.orig; - // The delta used to be what got snapped, which preserves whatever off-grid - // offset the shape already had: a legacy shape at 0.3013 stayed at 0.3013 - // for ever, one step at a time. Snap the RESULTING ANCHOR instead, so one - // drag is enough to put any shape on the grid (docs/CANVAS.md §9). - const ax0 = (o0.kind === 'line' ? o0.x1 : o0.x) * NORM_W; - const ay0 = (o0.kind === 'line' ? o0.y1 : o0.y) * this._decorH; - const anchor = this._decorSnap( - [ax0 + (p[0] - m.start[0]), ay0 + (p[1] - m.start[1])], - ev.pointerType, m.id, - ); - let dx = (anchor[0] - ax0) / NORM_W; - let dy = (anchor[1] - ay0) / this._decorH; - // audit follow-up L4 gave decor a bounds clamp of -0.25..1.25 — the plan - // was a sheet with edges then. It is not any more (docs/CANVAS.md): the - // clamp is now the same garbage limit the backend enforces, so decor can - // follow a plan that lives at 2.7 and still cannot be flung to 1e100. - const o: any = m.orig; - const curX = o.kind === 'line' ? Math.min(o.x1, o.x2) : o.x; - const curY = o.kind === 'line' ? Math.min(o.y1, o.y2) : o.y; - const w = o.kind === 'line' ? Math.abs(o.x2 - o.x1) : (o.w || 0); - const h = o.kind === 'line' ? Math.abs(o.y2 - o.y1) : (o.h || 0); - const lim = CANVAS_LIMIT; - dx = Math.max(-lim - curX, Math.min(lim - curX - w, dx)); - dy = Math.max(-lim - curY, Math.min(lim - curY - h, dy)); - if (dx || dy) m.moved = true; - const sp = this._curSpaceCfg; - sp.decor = this._decorList.map((x) => { - if (x.id !== m.id) return x; - const o: any = m.orig; - if (x.kind === 'line') return { ...x, x1: o.x1 + dx, y1: o.y1 + dy, x2: o.x2 + dx, y2: o.y2 + dy }; - return { ...x, x: o.x + dx, y: o.y + dy }; - }); - this.requestUpdate(); + return this._editorRuntimeOrThrow()._decorMoveUpdate(ev); } /** Select tool: every decor object has a double-click properties dialog. */ private _decorShapeDbl(ev: MouseEvent, shape: DecorShape): void { - if (this._mode !== 'decor' || this._decorTool !== 'select') return; - ev.preventDefault(); - ev.stopPropagation(); - this._decorMove = null; - this._decorSel = shape.id; - this._openDecorProperties(shape); + if (!this._editorRuntime) return; + return this._editorRuntime._decorShapeDbl(ev, shape); } /** One properties entry point shared by double click and the context tray. */ private _openDecorProperties(shape: DecorShape): void { - if (this._mode !== 'decor' || this._decorTool !== 'select') return; - if (shape.kind === 'text') { - this._decorOpenText(shape); - return; - } - if (!['line', 'rect', 'ellipse', 'furniture'].includes(shape.kind)) return; - const style = this._decorResolvedStyle(shape); - const line = shape.kind === 'line' ? shape : null; - const box = this._decorBoxOf(shape); - this._decorShapeDialog = { - id: shape.id, - kind: shape.kind as 'line' | 'rect' | 'ellipse' | 'furniture', - color: style.color, opacity: style.opacity, widthCm: style.widthCm, - angle: this._angleField(line - ? normalizeAngle(segmentAngle( - [line.x1 * NORM_W, line.y1 * this._decorH], - [line.x2 * NORM_W, line.y2 * this._decorH], - )) - : normalizeAngle((shape as any).angle)), - ...(line ? { - lengthCm: decorUnitsToCm( - Math.hypot((line.x2 - line.x1) * NORM_W, (line.y2 - line.y1) * this._decorH), - this._cellCm, this._gridPitch, - ), - lineStyle: line.line_style === 'dashed' ? 'dashed' : 'solid', - } : {}), - ...(box ? { - sizeWCm: decorUnitsToCm(box.w, this._cellCm, this._gridPitch), - sizeHCm: decorUnitsToCm(box.h, this._cellCm, this._gridPitch), - } : {}), - ...(shape.kind === 'furniture' ? { symbol: shape.symbol } : {}), - ...(shape.kind === 'rect' || shape.kind === 'ellipse' ? { - fill: style.fill, fillColor: style.fillColor, fillOpacity: style.fillOpacity, - } : {}), - }; + return this._editorRuntimeOrThrow()._openDecorProperties(shape); } /** Open the editor of an existing label (double click, or the text tool). */ private _decorOpenText(shape: DecorShape): void { - if (shape.kind !== 'text') return; - let text = String(shape.text ?? ''); - const hasInline = [...text.matchAll(/\{([^{}\r\n]+)\}/g)] - .some((m) => !!liveTextReference(m[1])); - const explicitUnit = String(shape.unit ?? '').trim(); - // Old dialogs represented the entity state as attr="state"; inline text - // represents that same value with a bare `{entity}` token. - const legacyAttr = String(shape.attr ?? '').trim().toLowerCase() === 'state' - ? null - : shape.attr; - const legacyToken = !hasInline && !explicitUnit - ? liveTextToken(shape.entity, legacyAttr) - : ''; - const preserveLegacy = !!String(shape.entity ?? '').trim() && !hasInline - && (!legacyToken || !!explicitUnit); - if (legacyToken && !preserveLegacy) { - const slot = text.indexOf('{}'); - text = slot >= 0 - ? text.slice(0, slot) + legacyToken + text.slice(slot + 2) - : `${text}${text ? ' ' : ''}${legacyToken}`; - } - this._decorTextDialog = { - id: shape.id, x: shape.x, y: shape.y, text, - color: safeStoredColor(shape.color, this._decorStyle.color), - opacity: clamp01(shape.opacity, this._decorStyle.opacity), - angle: this._angleField(shape.angle), - sizeCm: this._decorTextSizeCm(shape), - pickerEntity: shape.entity || '', - preserveLegacy: preserveLegacy || undefined, - }; - this._decorTextSelection = { start: text.length, end: text.length }; + return this._editorRuntimeOrThrow()._decorOpenText(shape); } private _decorRememberTextSelection(el: HTMLTextAreaElement): void { - this._decorTextSelection = { - start: el.selectionStart ?? el.value.length, - end: el.selectionEnd ?? el.value.length, - }; + return this._editorRuntimeOrThrow()._decorRememberTextSelection(el); } /** Insert a complete reference without ever truncating it into broken text. */ private _decorInsertLiveVariable(attr: string | null): void { - const d = this._decorTextDialog; - if (!d) return; - const token = liveTextToken(d.pickerEntity, attr); - if (!token) return; - const old = d.text; - const start = Math.max(0, Math.min(old.length, this._decorTextSelection.start)); - const end = Math.max(start, Math.min(old.length, this._decorTextSelection.end)); - if (old.length - (end - start) + token.length > 200) return; - const text = old.slice(0, start) + token + old.slice(end); - const caret = start + token.length; - this._decorTextSelection = { start: caret, end: caret }; - // Inserting a new inline reference is the explicit replacement of any - // unrepresentable legacy link; the stale side fields can now be dropped. - this._decorTextDialog = { ...d, text, preserveLegacy: undefined }; - this.updateComplete.then(() => { - const el = this.renderRoot.querySelector('textarea.dtarea'); - if (!el) return; - el.focus(); - el.setSelectionRange(caret, caret); - }); + return this._editorRuntimeOrThrow()._decorInsertLiveVariable(attr); } private _decorSaveText(): void { - const d = this._decorTextDialog; - // The user's own line breaks are kept; the surrounding whitespace is not. - const text = String(d?.text ?? '').replace(/\r\n?/g, '\n').trim(); - if (!d || !text) { this._decorTextDialog = null; return; } - const before = this._geometrySnapshot(); - const sp = this._curSpaceCfg; - const textStyle = { - color: d.color, opacity: clamp01(d.opacity), - size_cm: Number(Math.max(0.1, Math.min(DECOR_TEXT_CM_MAX, d.sizeCm)).toFixed(4)), - ...(normalizeAngle(d.angle) ? { angle: normalizeAngle(d.angle) } : {}), - }; - if (d.id) { - sp.decor = this._decorList.map((x) => { - if (x.id !== d.id) return x; - if (x.kind !== 'text') return x; - // A legacy unit or non-canonical attribute cannot be represented by - // the inline grammar. Keep those fields until the user explicitly - // replaces the binding; ordinary representable links still migrate. - if (d.preserveLegacy) { - const { angle: _angle, size: _size, scale: _scale, ...straight } = x; - return { ...straight, text, ...textStyle }; - } - // beta.9 and earlier stored one live link beside the text. Saving in - // the new editor migrates it to inline tokens and drops the old fields. - const { entity, attr, unit, ...rest } = x; - const { angle: _angle, size: _size, scale: _scale, ...straight } = rest; - return { ...straight, text, ...textStyle }; - }); - } else { - const id = 'dc' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5); - sp.decor = [...this._decorList, { id, kind: 'text', x: d.x, y: d.y, - text, ...textStyle }]; - this._decorSel = id; - } - this._decorTextDialog = null; - this._recordGeometry(this._t(d.id ? 'history.decor_edit' : 'history.decor_add'), before); - this._saveConfig(); - this.requestUpdate(); + return this._editorRuntimeOrThrow()._decorSaveText(); } private _decorSaveShape(): void { - const d = this._decorShapeDialog; - if (!d) return; - const before = this._geometrySnapshot(); - const style: DecorStyle = { - color: d.color, opacity: clamp01(d.opacity), - widthCm: Math.max(0.1, Math.min(100, Number(d.widthCm) || 0.1)), - fill: !!d.fill, fillColor: d.fillColor || d.color, - fillOpacity: clamp01(d.fillOpacity, 0.25), - }; - const sp = this._curSpaceCfg; - sp.decor = this._decorList.map((shape) => { - if (shape.id !== d.id) return shape; - const fillable = d.kind === 'rect' || d.kind === 'ellipse'; - const visual = decorStylePatch(style, fillable); - if (shape.kind === 'line') { - const cx = ((shape.x1 + shape.x2) / 2) * NORM_W; - const cy = ((shape.y1 + shape.y2) / 2) * this._decorH; - const length = decorCmToUnits(Math.max(0.1, Number(d.lengthCm) || 0.1), this._cellCm, this._gridPitch); - const rad = (normalizeAngle(d.angle) * Math.PI) / 180; - const dx = Math.cos(rad) * length / 2, dy = Math.sin(rad) * length / 2; - const a = this._snap([cx - dx, cy - dy]); - const b = this._snap([cx + dx, cy + dy]); - const { width: _legacyWidth, line_style: _oldLineStyle, ...rest } = shape; - return { ...rest, ...visual, - x1: clampCanvasN(a[0] / NORM_W), y1: clampCanvasN(a[1] / this._decorH), - x2: clampCanvasN(b[0] / NORM_W), y2: clampCanvasN(b[1] / this._decorH), - ...(d.lineStyle === 'dashed' ? { line_style: 'dashed' as const } : {}), - } as DecorShape; - } - if (shape.kind === 'rect' || shape.kind === 'ellipse' || shape.kind === 'furniture') { - const oldW = shape.w * NORM_W, oldH = shape.h * this._decorH; - const w = Math.max(this._gridPitch, snapToGrid( - decorCmToUnits(Number(d.sizeWCm), this._cellCm, this._gridPitch), this._gridPitch, - )); - const h = Math.max(this._gridPitch, snapToGrid( - decorCmToUnits(Number(d.sizeHCm), this._cellCm, this._gridPitch), this._gridPitch, - )); - const cx = shape.x * NORM_W + oldW / 2, cy = shape.y * this._decorH + oldH / 2; - const angle = normalizeAngle(d.angle); - const topLeft = resizedBoxTopLeft( - { x: cx - w / 2, y: cy - h / 2 }, angle, - (point) => this._snap(point), - ); - const { width: _legacyWidth, angle: _oldAngle, ...rest } = shape; - return { ...rest, ...visual, - x: clampCanvasN(topLeft[0] / NORM_W), y: clampCanvasN(topLeft[1] / this._decorH), - w: w / NORM_W, h: h / this._decorH, - ...(shape.kind === 'furniture' && d.symbol ? { symbol: d.symbol } : {}), - ...(angle ? { angle } : {}), - } as DecorShape; - } - return shape; - }); - this._decorStyle = { ...style, - fill: d.kind === 'rect' || d.kind === 'ellipse' ? style.fill : this._decorStyle.fill, - fillColor: d.kind === 'rect' || d.kind === 'ellipse' ? style.fillColor : this._decorStyle.fillColor, - fillOpacity: d.kind === 'rect' || d.kind === 'ellipse' ? style.fillOpacity : this._decorStyle.fillOpacity, - }; - this._decorShapeDialog = null; - this._recordGeometry(this._t('history.decor_edit'), before); - this._saveConfig(); - this.requestUpdate(); + return this._editorRuntimeOrThrow()._decorSaveShape(); } // ---- common decor transform controller ---- @@ -10208,128 +7614,26 @@ class HouseplanCard extends LitElement { * their geometric centres. */ private _dtPivot(sh: DecorShape): number[] { - if (sh.kind === 'line') return [ - ((sh.x1 + sh.x2) / 2) * NORM_W, - ((sh.y1 + sh.y2) / 2) * this._decorH, - ]; - if (sh.kind === 'furniture' || sh.kind === 'rect' || sh.kind === 'ellipse') - return [(sh.x + sh.w / 2) * NORM_W, (sh.y + sh.h / 2) * this._decorH]; - return [sh.x * NORM_W, sh.y * this._decorH]; + return this._editorRuntimeOrThrow()._dtPivot(sh); } /** Write physical text size/angle into the shape — live, without saving. */ private _dtApply(id: string, patch: { textSizeCm?: number; angle?: number }): void { - const sp = this._curSpaceCfg; - if (!sp) return; - sp.decor = this._decorList.map((x) => { - if (x.id !== id) return x; - const rest: any = { ...x }; - if (x.kind === 'text' && patch.textSizeCm !== undefined) { - delete rest.size; - delete rest.scale; - } - const out: any = { ...rest }; - if (patch.textSizeCm !== undefined) - out.size_cm = Number(Math.max(0.1, Math.min(DECOR_TEXT_CM_MAX, patch.textSizeCm)).toFixed(4)); - if (patch.angle !== undefined) { - if (patch.angle) out.angle = Number(patch.angle.toFixed(2)); - else delete out.angle; // straight again = the field goes away - } - return out; - }); - this._cfgEpoch++; - this.requestUpdate(); + return this._editorRuntimeOrThrow()._dtApply(id, patch); } private _dtStart( ev: PointerEvent, kind: 'scale' | 'rotate', corner?: number[], lineEnd?: 0 | 1, ): void { - const sh = this._dtSel; - if (!sh) return; - ev.stopPropagation(); - ev.preventDefault(); - const [ax, ay] = this._dtPivot(sh); - const p = this._svgPoint(ev); - const box = this._decorBoxOf(sh); - this._dtDrag = { - id: sh.id, kind, pid: ev.pointerId, ax, ay, - r0: Math.hypot(p[0] - ax, p[1] - ay), - a0: (Math.atan2(p[1] - ay, p[0] - ax) * 180) / Math.PI, - textSizeCm0: sh.kind === 'text' ? this._decorTextSizeCm(sh) : 1, - angle0: sh.kind === 'line' ? 0 : Number(sh.angle) || 0, - // which corner is being pulled, and the box it started from: a piece of - // furniture is resized about the OPPOSITE corner, so both have to be - // remembered — a label, scaled about its anchor, needs neither - sgx: corner?.[0], sgy: corner?.[1], - orig: box || undefined, - origShape: JSON.parse(JSON.stringify(sh)), - before: this._geometrySnapshot(), - lineEnd, - moved: false, - }; - capturePointer(ev); + return this._editorRuntimeOrThrow()._dtStart(ev, kind, corner, lineEnd); } private _dtMove(ev: PointerEvent): void { - const d = this._dtDrag; - if (!d) return; - const p = this._svgPoint(ev); - if (d.lineEnd !== undefined && d.origShape.kind === 'line') { - const at = this._decorSnap(p, ev.pointerType, d.id); - const nx = clampCanvasN(at[0] / NORM_W), ny = clampCanvasN(at[1] / this._decorH); - const old = d.origShape; - const ox = d.lineEnd === 0 ? old.x1 : old.x2; - const oy = d.lineEnd === 0 ? old.y1 : old.y2; - if (Math.abs(nx - ox) > 1e-9 || Math.abs(ny - oy) > 1e-9) d.moved = true; - this._replaceDecor(d.id, d.lineEnd === 0 ? { x1: nx, y1: ny } : { x2: nx, y2: ny }); - return; - } - if (d.kind === 'scale' && d.orig) { - // Box geometry preserves ratio by default and separates axes with Shift. - // Each dimension lands on a whole cell. - const box = resizeDecorBox( - d.orig, d.sgx ?? 1, d.sgy ?? 1, p[0], p[1], - !ev.shiftKey, this._gridPitch, this._gridPitch, - ); - const changed = Math.abs(box.x - d.orig.x) > 1e-6 || Math.abs(box.y - d.orig.y) > 1e-6 - || Math.abs(box.w - d.orig.w) > 1e-6 || Math.abs(box.h - d.orig.h) > 1e-6; - if (!changed && !d.moved) return; - d.moved ||= changed; - this._decorApplyBox(d.id, box); - return; - } - if (d.kind === 'scale') { - // uniform, about the anchor: the distance from the anchor is invariant - // under the block's own rotation, so a rotated block scales the same way - const r = Math.hypot(p[0] - d.ax, p[1] - d.ay); - if (d.r0 < 1e-6) return; - const sizeCm = Math.max(0.1, Math.min(DECOR_TEXT_CM_MAX, d.textSizeCm0 * (r / d.r0))); - const changed = Math.abs(sizeCm - d.textSizeCm0) > 1e-6; - if (!changed && !d.moved) return; - d.moved ||= changed; - this._dtApply(d.id, { textSizeCm: sizeCm }); - return; - } - const a = (Math.atan2(p[1] - d.ay, p[0] - d.ax) * 180) / Math.PI; - let ang = d.angle0 + (a - d.a0); - // 5° rotation steps; Shift remains an angle-only precision modifier. - if (!ev.shiftKey) ang = Math.round(ang / DT_ANGLE_STEP) * DT_ANGLE_STEP; - ang = ((ang % 360) + 360) % 360; - if (ang > 180) ang -= 360; - const changed = Math.abs(ang - d.angle0) > 1e-6; - if (!changed && !d.moved) return; - d.moved ||= changed; - this._dtApply(d.id, { angle: ang }); + return this._editorRuntimeOrThrow()._dtMove(ev); } private _dtUp(): void { - const d = this._dtDrag; - this._dtDrag = null; - if (d?.moved) { - this._recordGeometry(this._t('history.decor_transform'), d.before); - this._saveConfig(); - } - this.requestUpdate(); + return this._editorRuntimeOrThrow()._dtUp(); } /** @@ -10340,59 +7644,19 @@ class HouseplanCard extends LitElement { * comparing the numbers before asking for another update. */ private _dtMeasure(): void { - const sh = this._dtSel; - if (!sh) { - if (this._dtBox) { this._dtBox = null; this.requestUpdate(); } - return; - } - let box: { id: string; x: number; y: number; w: number; h: number }; - if (sh.kind === 'line') { - const x1 = sh.x1 * NORM_W, y1 = sh.y1 * this._decorH; - const x2 = sh.x2 * NORM_W, y2 = sh.y2 * this._decorH; - box = { id: sh.id, x: Math.min(x1, x2), y: Math.min(y1, y2), - w: Math.abs(x2 - x1), h: Math.abs(y2 - y1) }; - } else if (sh.kind === 'furniture' || sh.kind === 'rect' || sh.kind === 'ellipse') { - // …and furniture needs no measuring at all: its box IS the config. The - // frame therefore appears in the SAME frame as the selection, not one - // after it, and a resize can never be a render behind the shape. - box = { id: sh.id, x: sh.x * NORM_W, y: sh.y * this._decorH, - w: sh.w * NORM_W, h: sh.h * this._decorH }; - } else { - const el = this.renderRoot.querySelector(`text.dtext[data-id="${sh.id}"]`) as SVGGraphicsElement | null; - if (!el || typeof (el as any).getBBox !== 'function') return; - let b: DOMRect; - try { b = el.getBBox(); } catch { return; } // not rendered yet (hidden card) - if (!b || (!b.width && !b.height)) return; - box = { id: sh.id, x: b.x, y: b.y, w: b.width, h: b.height }; - } - const cur = this._dtBox; - const same = cur && cur.id === box.id && Math.abs(cur.x - box.x) < 0.01 - && Math.abs(cur.y - box.y) < 0.01 && Math.abs(cur.w - box.w) < 0.01 - && Math.abs(cur.h - box.h) < 0.01; - if (same) return; - this._dtBox = box; - this.requestUpdate(); + return this._editorRuntimeOrThrow()._dtMeasure(); } private _deleteDecor(id: string): void { - if (!this._decorList.some((shape) => shape.id === id)) return; - const before = this._geometrySnapshot(); - const sp = this._curSpaceCfg; - sp.decor = this._decorList.filter((shape) => shape.id !== id); - if (this._decorSel === id) this._decorSel = null; - this._recordGeometry(this._t('history.decor_delete'), before); - this._saveConfig(); - this.requestUpdate(); + return this._editorRuntimeOrThrow()._deleteDecor(id); } private _decorDeleteSel(): void { - if (this._decorSel) this._deleteDecor(this._decorSel); + return this._editorRuntimeOrThrow()._decorDeleteSel(); } private _confirmDecorErase(): void { - const pending = this._decorEraseConfirm; - this._decorEraseConfirm = null; - if (pending) this._deleteDecor(pending.id); + return this._editorRuntimeOrThrow()._confirmDecorErase(); } // ================= the furniture library (docs/FURNITURE.md) ================= @@ -10418,20 +7682,19 @@ class HouseplanCard extends LitElement { * `_imperial` it asks is the card's one answer to "feet or metres?", the * same the glow radius reads. */ private _furnFieldValue(cm: number): number { - return Math.round((this._imperial ? cm / 30.48 : cm / 100) * 100) / 100; + return this._editorRuntimeOrThrow()._furnFieldValue(cm); } /** …and back. The config never stores feet: a unit system is how a user * reads a plan, not what the plan is (docs/STYLING-HOOKS.md §6). */ private _furnFieldToCm(v: number): number { - return clampFurnCm(this._imperial ? v * 30.48 : v * 100); + return this._editorRuntimeOrThrow()._furnFieldToCm(v); } /** Arm a symbol: the palette remembers it with ITS default real size, which * the two fields then let the user overrule before the click. */ private _furnPick(symbol: string): void { - const d = furnitureDefaultCm(symbol); - this._furnPalette = { symbol, w: d.w, h: d.h }; + return this._editorRuntimeOrThrow()._furnPick(symbol); } /** @@ -10447,37 +7710,7 @@ class HouseplanCard extends LitElement { * case (put the bed against that wall) is one click and no dragging. */ private _furnPlace(raw: number[], free = false): void { - const pal = this._furnPalette; - const sp = this._curSpaceCfg; - if (!pal || !sp) return; - const W = NORM_W, H = this._decorH; - const wN = clampFurnSize(cmToNorm(pal.w, this._cellCm, this._gridPitch, W)); - const hN = clampFurnSize(cmToNorm(pal.h, this._cellCm, this._gridPitch, W)); - 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._furnWalls, - this._furnWallReach, this._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, - ...decorStylePatch(this._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)); - sp.decor = [...this._decorList, shape]; - this._decorSel = id; - // …and the editor goes back to the tool that can move what was just placed - this._decorTool = 'select'; - this._furnPalette = null; - this._recordGeometry(this._t('history.decor_add'), before); - this._saveConfig(); - this.requestUpdate(); + return this._editorRuntimeOrThrow()._furnPlace(raw, free); } /** @@ -10488,62 +7721,12 @@ class HouseplanCard extends LitElement { * the angle it already had is kept. */ private _furnMoveUpdate(ev: PointerEvent): void { - const m = this._decorMove!; - if (m.orig.kind !== 'furniture') return; - const o: any = m.orig; - const sp = this._curSpaceCfg; - if (!sp) return; - const W = NORM_W, H = this._decorH; - const p = this._svgPoint(ev); - const rawCx = (o.x + o.w / 2) * W + (p[0] - m.start[0]); - const rawCy = (o.y + o.h / 2) * H + (p[1] - m.start[1]); - let x: number, y: number; - let angle = Number(o.angle) || 0; - const snap = ev.shiftKey ? null : snapFurnitureToWall( - rawCx, rawCy, o.h * H, this._furnWalls, this._furnWallReach, this._gridPitch); - if (snap) { - x = snap.cx / W - o.w / 2; - y = snap.cy / H - o.h / 2; - angle = snap.angle; - } else { - const a = this._decorSnap( - [rawCx - (o.w / 2) * W, rawCy - (o.h / 2) * H], ev.pointerType, m.id, - ); - x = a[0] / W; - y = a[1] / H; - } - x = clampCanvasN(x); y = clampCanvasN(y); - if (Math.abs(x - o.x) > 1e-9 || Math.abs(y - o.y) > 1e-9 - || Math.abs(angle - (Number(o.angle) || 0)) > 1e-9) m.moved = true; - sp.decor = this._decorList.map((s) => { - if (s.id !== m.id) return s; - const out: any = { ...s, x, y }; - if (angle) out.angle = Number(angle.toFixed(2)); - else delete out.angle; - return out; - }); - this.requestUpdate(); + return this._editorRuntimeOrThrow()._furnMoveUpdate(ev); } /** Write a resized box into the shape — live, without saving. */ private _decorApplyBox(id: string, box: { x: number; y: number; w: number; h: number }): void { - const sp = this._curSpaceCfg; - if (!sp) return; - const W = NORM_W, H = this._decorH; - sp.decor = this._decorList.map((s) => { - if (s.id !== id) return s; - const topLeft = resizedBoxTopLeft( - box, (s as any).angle, (point) => this._snap(point), - ); - return { - ...s, - x: clampCanvasN(topLeft[0] / W), y: clampCanvasN(topLeft[1] / H), - w: Math.max(this._gridPitch / W, Math.min(CANVAS_LIMIT * 2, box.w / W)), - h: Math.max(this._gridPitch / H, Math.min(CANVAS_LIMIT * 2, box.h / H)), - } as DecorShape; - }); - this._cfgEpoch++; - this.requestUpdate(); + return this._editorRuntimeOrThrow()._decorApplyBox(id, box); } /** @@ -10578,56 +7761,7 @@ class HouseplanCard extends LitElement { * symbol's real size and shown in metres or feet by the HA unit system. */ private _renderFurnPalette(): TemplateResult { - const pal = this._furnPalette; - // the unit the fields are read in — the same two words the glow radius - // already uses, because a plan has one unit system, not one per control - const unit = this._t(this._imperial ? 'gs.unit_ft' : 'gs.unit_m'); - const preview = (id: string): TemplateResult => { - const s = furnitureSymbol(id)!; - // fit the symbol into a 40×40 box keeping its real proportions, so a - // sofa reads as a sofa and a toilet does not become a square - const k = 36 / Math.max(s.w, s.h); - const w = s.w * k, h = s.h * k; - return svg`` as unknown as TemplateResult; - }; - return html`
e.stopPropagation()}> -
- ${this._t('furn.title')} - - -
-
- ${FURNITURE_GROUPS.map((g) => html` -
${this._t(`furn.group_${g}` as any)}
-
- ${furnitureOfGroup(g as FurnitureGroup).map((s) => html``)} -
`)} -
- ${pal ? html`
- - (this._furnPalette = { - ...pal, w: this._furnFieldToCm(Number((e.target as HTMLInputElement).value)) })} /> - - (this._furnPalette = { - ...pal, h: this._furnFieldToCm(Number((e.target as HTMLInputElement).value)) })} /> - ${this._t('furn.place_hint')} -
` : html`
${this._t('furn.pick_hint')}
`} -
`; + return this._editorRuntimeOrThrow()._renderFurnPalette(); } // ============ backdrop transform frame (docs/BACKDROP.md) ============ @@ -10661,44 +7795,12 @@ class HouseplanCard extends LitElement { } private _openBackdropDialog(ev?: Event): void { - if (!this._bdMovable || !this._bdRect) return; - ev?.preventDefault(); - ev?.stopPropagation(); - this._bdDrag = null; - const r = this._bdRect; - this._backdropDialog = { - widthCm: decorUnitsToCm(r.w, this._cellCm, this._gridPitch), - heightCm: decorUnitsToCm(r.h, this._cellCm, this._gridPitch), - angle: this._angleField(r.angle), - }; + if (!this._editorRuntime) return; + return this._editorRuntime._openBackdropDialog(ev); } private _saveBackdropDialog(): void { - const d = this._backdropDialog; - const base = this._bdBase, current = this._bdRect; - if (!d || !base || !current) return; - const angle = normalizeAngle(d.angle); - const before = this._geometrySnapshot(); - const w = Math.min(base.w * PLAN_SCALE_MAX, Math.max( - base.w * PLAN_SCALE_MIN, snapToGrid( - decorCmToUnits(d.widthCm, this._cellCm, this._gridPitch), this._gridPitch, - ), - )); - const h = Math.min(base.h * PLAN_SCALE_MAX, Math.max( - base.h * PLAN_SCALE_MIN, snapToGrid( - decorCmToUnits(d.heightCm, this._cellCm, this._gridPitch), this._gridPitch, - ), - )); - const cx = current.x + current.w / 2, cy = current.y + current.h / 2; - const topLeft = resizedBoxTopLeft( - { x: cx - w / 2, y: cy - h / 2 }, angle, - (point) => this._snap(point), - ); - this._bdApply((topLeft[0] - base.x) / NORM_W, (topLeft[1] - base.y) / NORM_W, - w / base.w, h / base.h, angle); - this._backdropDialog = null; - this._recordGeometry(this._t('history.backdrop_transform'), before); - this._saveConfig(); + return this._editorRuntimeOrThrow()._saveBackdropDialog(); } /** @@ -10725,20 +7827,7 @@ class HouseplanCard extends LitElement { /** Write a transform into the space config — live, without saving. */ private _bdApply(dx: number, dy: number, sx: number, sy: number, angle: number): void { - const sp = this._curSpaceCfg; - if (!sp) return; - // 6 decimals: the same precision the rest of the normalised config keeps, - // and enough for a 1000-unit canvas to be exact to a thousandth of a pixel - sp.plan_x = Number(clampCanvasN(dx).toFixed(6)); - sp.plan_y = Number(clampCanvasN(dy).toFixed(6)); - delete sp.plan_scale; - sp.plan_scale_x = Number(Math.min(PLAN_SCALE_MAX, Math.max(PLAN_SCALE_MIN, sx)).toFixed(6)); - sp.plan_scale_y = Number(Math.min(PLAN_SCALE_MAX, Math.max(PLAN_SCALE_MIN, sy)).toFixed(6)); - const a = normalizeAngle(angle); - if (a) sp.plan_angle = Number(a.toFixed(2)); - else delete sp.plan_angle; - this._cfgEpoch++; - this.requestUpdate(); + return this._editorRuntimeOrThrow()._bdApply(dx, dy, sx, sy, angle); } /** @@ -10747,26 +7836,7 @@ class HouseplanCard extends LitElement { * body, i.e. a move. Returns false when there is nothing to grab. */ private _bdStart(ev: PointerEvent, corner?: number[], rotate = false): boolean { - const base = this._bdBase, r = this._bdRect; - if (!base || !r) return false; - const p = this._svgPoint(ev); - const sgx = corner ? corner[0] : 0; - const sgy = corner ? corner[1] : 0; - // the corner that stays put is the OPPOSITE one - const fx = sgx > 0 ? r.x : r.x + r.w; - const fy = sgy > 0 ? r.y : r.y + r.h; - this._bdDrag = { - kind: rotate ? 'rotate' : corner ? 'scale' : 'move', - pid: ev.pointerId, - sx: p[0], sy: p[1], - base, p0: this._bdParams, - fx, fy, sgx, sgy, - rect0: { x: r.x, y: r.y, w: r.w, h: r.h, angle: r.angle }, - before: this._geometrySnapshot(), - moved: false, - }; - capturePointer(ev); - return true; + return this._editorRuntimeOrThrow()._bdStart(ev, corner, rotate); } /** @@ -10782,46 +7852,7 @@ class HouseplanCard extends LitElement { * Positional snapping is mandatory (UX-05). */ private _bdMove(ev: PointerEvent): void { - const d = this._bdDrag; - if (!d) return; - const p = this._svgPoint(ev); - const b = d.base; - if (d.kind === 'move') { - const x0 = d.rect0.x, y0 = d.rect0.y; - const at = this._snap([x0 + (p[0] - d.sx), y0 + (p[1] - d.sy)]); - const changed = Math.abs(at[0] - x0) > 1e-9 || Math.abs(at[1] - y0) > 1e-9; - if (!changed && !d.moved) return; - d.moved ||= changed; - this._bdApply((at[0] - b.x) / NORM_W, (at[1] - b.y) / NORM_W, - d.p0.sx, d.p0.sy, d.p0.angle); - return; - } - if (d.kind === 'rotate') { - const cx = d.rect0.x + d.rect0.w / 2, cy = d.rect0.y + d.rect0.h / 2; - const a0 = Math.atan2(d.sy - cy, d.sx - cx) * 180 / Math.PI; - let angle = d.p0.angle + (Math.atan2(p[1] - cy, p[0] - cx) * 180 / Math.PI - a0); - if (!ev.shiftKey) angle = Math.round(angle / DT_ANGLE_STEP) * DT_ANGLE_STEP; - angle = normalizeAngle(angle); - const changed = Math.abs(angle - d.p0.angle) > 1e-9; - if (!changed && !d.moved) return; - d.moved ||= changed; - this._bdApply(d.p0.dx, d.p0.dy, d.p0.sx, d.p0.sy, angle); - return; - } - const box = resizeDecorBox( - d.rect0, d.sgx, d.sgy, p[0], p[1], !ev.shiftKey, - this._gridPitch, Math.min(b.w, b.h) * PLAN_SCALE_MIN, - ); - const sx = box.w / Math.max(1e-9, b.w), sy = box.h / Math.max(1e-9, b.h); - const changed = Math.abs(sx - d.p0.sx) > 1e-9 || Math.abs(sy - d.p0.sy) > 1e-9 - || Math.abs(box.x - d.rect0.x) > 1e-9 || Math.abs(box.y - d.rect0.y) > 1e-9; - if (!changed && !d.moved) return; - d.moved ||= changed; - const topLeft = resizedBoxTopLeft( - box, d.p0.angle, (point) => this._snap(point), - ); - this._bdApply((topLeft[0] - b.x) / NORM_W, (topLeft[1] - b.y) / NORM_W, - sx, sy, d.p0.angle); + return this._editorRuntimeOrThrow()._bdMove(ev); } /** Has this space's picture been moved or scaled at all? */ @@ -10833,27 +7864,12 @@ class HouseplanCard extends LitElement { /** Put the picture back where an untouched plan has it: centred, own size. */ private _bdReset(): void { - const sp = this._curSpaceCfg; - if (!sp) return; - const before = this._geometrySnapshot(); - delete sp.plan_x; delete sp.plan_y; delete sp.plan_scale; - delete sp.plan_scale_x; delete sp.plan_scale_y; delete sp.plan_angle; - this._bdDrag = null; - this._recordGeometry(this._t('history.backdrop_transform'), before); - this._saveConfig(); - this._showToast(this._t('decor.backdrop_reset_done')); - this.requestUpdate(); + return this._editorRuntimeOrThrow()._bdReset(); } /** Release: persist only when something actually moved. */ private _bdUp(): void { - const d = this._bdDrag; - this._bdDrag = null; - if (d?.moved) { - this._recordGeometry(this._t('history.backdrop_transform'), d.before); - this._saveConfig(); - } - this.requestUpdate(); + return this._editorRuntimeOrThrow()._bdUp(); } /** @@ -10876,42 +7892,8 @@ class HouseplanCard extends LitElement { /** The transform frame itself: a dashed outline and four corner handles. */ private _renderBackdropFrame(view: { x: number; y: number; w: number; h: number }): TemplateResult | typeof nothing { - const r = this._bdRect; - if (!this._bdActive || !r) return nothing; - // Two radii, one gesture — the split the text frame uses (docs/LIVE-TEXT.md - // §3) and, since 2026-08-05, every corner handle in the card. `hr` is the - // HIT radius: a fraction of the visible view, so the target stays - // finger-sized at any zoom. `kr` is what you SEE — a quarter of it, because - // a blob the size of a room is not a handle, it is an occlusion (owner: - // «уменьшить в 4 раза… они постоянно гигантские»). The clickable area is - // unchanged; only the ink shrank. - const hr = Math.max(view.w, view.h) * 0.02; - const kr = hr / 4; - const cx = r.x + r.w / 2, cy = r.y + r.h / 2; - const angle = normalizeAngle(r.angle); - const arm = hr * 2.2; - const corners: [number, number, string][] = [ - [-1, -1, 'nwse'], [1, -1, 'nesw'], [1, 1, 'nwse'], [-1, 1, 'nesw'], - ]; - return svg` - - - { - e.stopPropagation(); e.preventDefault(); this._bdStart(e, undefined, true); - }}> - - ${corners.map(([sx, sy, cur]) => { - const cx = sx < 0 ? r.x : r.x + r.w; - const cy = sy < 0 ? r.y : r.y + r.h; - return svg` { - e.stopPropagation(); e.preventDefault(); this._bdStart(e, [sx, sy]); - }}>`; - })} - ` as unknown as TemplateResult; + if (!this._editorRuntime) return nothing; + return this._editorRuntimeOrThrow()._renderBackdropFrame(view); } /** @@ -11114,18 +8096,14 @@ class HouseplanCard extends LitElement { } private _renderEditorGroupLauncher(group: EditorToolbarGroup): TemplateResult { - return this._editorSecondary.renderGroupLauncher( - group, - this._editorToolbarGroups, - this._editorSecondaryCopy, - ); + return this._editorRuntimeOrThrow()._renderEditorGroupLauncher(group); } /** Stable target identity + current config epoch + operation revision. */ private get _editorSecondaryContextId(): string { const base = `editor:${this._mode}:${this._space}:${this._cfgEpoch}`; - const group = this._editorSecondary.activeGroup(this._editorToolbarGroups); - if (group) return `${base}:group:${group.id}:${this._editorSecondary.groupGeneration}`; + const group = this._editorSecondary?.activeGroup(this._editorToolbarGroups); + if (group) return `${base}:group:${group.id}:${this._editorSecondary?.groupGeneration}`; if (this._mode === 'plan') { const sel = this._physicalSel; if (sel) return `${base}:selection:${sel.kind}:${sel.id}:${sel.segment ?? ''}`; @@ -11142,196 +8120,30 @@ class HouseplanCard extends LitElement { } private _runEditorContext(contextId: string, action: () => void): void { - this._editorSecondary.runContext(contextId, this._editorSecondaryContextId, action); + return this._editorRuntimeOrThrow()._runEditorContext(contextId, action); } private _renderEditorGroupModel(group: EditorToolbarGroup): EditorSecondaryModel { - return this._editorSecondary.renderGroupModel( - group, - this._editorSecondaryContextId, - this._editorSecondaryCopy, - ); + return this._editorRuntimeOrThrow()._renderEditorGroupModel(group); } private _renderDrawWallControl(): TemplateResult { - const min = this._tool === 'column' ? 1 : 0; - return html``; + return this._editorRuntimeOrThrow()._renderDrawWallControl(); } private _renderPlanSecondary(): EditorSecondaryModel | null { - const contextId = this._editorSecondaryContextId; - const sel = this._physicalSel; - if (sel) { - const exists = sel.kind === 'partition' - ? !!this._curSpaceCfg.partitions?.some((item: PartitionCfg) => item.id === sel.id) - : sel.kind === 'column' - ? !!this._curSpaceCfg.wall_columns?.some((item: WallColumnCfg) => item.id === sel.id) - : !!this._curSpaceCfg.room_drafts?.some((item: RoomDraftCfg) => item.id === sel.id); - if (!exists) return null; - const label = sel.kind === 'partition' ? this._t('markup.partition') - : sel.kind === 'column' ? this._t('markup.column') : this._t('markup.add'); - return { - contextId, - kind: 'selection', - ariaLabel: this._t('editor.context_actions', { object: label }), - visibleLabel: label, - content: html` - - `, - }; - } - - const hasThickness = this._tool === 'draw' || this._tool === 'column'; - const hintKey = this._tool === 'column' ? 'markup.hint_column' - : this._tool === 'resize' ? 'markup.hint_resize' - : this._tool === 'wallthick' ? 'markup.hint_wallthick' - : null; - const drawHint = this._tool === 'draw' - ? this._t(this._path.length ? 'markup.hint_points' : 'markup.hint_start', - this._path.length ? { n: this._path.length } : undefined) - : ''; - if (!hasThickness && !hintKey && !drawHint) return null; - const operation = this._tool === 'draw' && this._path.length > 0; - return { - contextId, - kind: operation ? 'operation' : 'tool', - ariaLabel: this._t('editor.tool_options', { - tool: this._t((this._tool === 'draw' ? 'markup.add' - : this._tool === 'column' ? 'markup.column' - : this._tool === 'resize' ? 'markup.resize' - : 'markup.wallthick') as any), - }), - content: html` - ${hasThickness ? this._renderDrawWallControl() : nothing} - ${drawHint ? html`${drawHint}` : nothing} - ${hintKey ? html`${this._t(hintKey as any)}` : nothing} - ${this._tool === 'draw' && this._path.length - ? html`` : nothing}`, - }; + return this._editorRuntimeOrThrow()._renderPlanSecondary(); } private _renderDecorSecondary(): EditorSecondaryModel | null { - const contextId = this._editorSecondaryContextId; - if (this._decorTool === 'furniture') { - return { - contextId, - kind: 'palette', - ariaLabel: this._t('editor.palette', { tool: this._t('decor.furniture') }), - launcherId: 'furniture', - dismissPolicy: this._furnPalette ? 'stay-open-on-canvas' : 'outside', - dismiss: () => { - if (this._decorTool !== 'furniture') return; - this._furnPalette = null; - this._decorTool = 'select'; - this.requestUpdate(); - }, - content: this._renderFurnPalette(), - }; - } - const selected = this._dtSel; - if (selected) { - const label = this._t((`decor.${selected.kind}`) as any); - return { - contextId, - kind: 'selection', - ariaLabel: this._t('editor.context_actions', { object: label }), - visibleLabel: label, - content: html` - - `, - }; - } - - const draws = this._decorTool === 'line' || this._decorTool === 'rect' || this._decorTool === 'ellipse'; - const canFill = this._decorTool === 'rect' || this._decorTool === 'ellipse'; - const backdrop = this._decorTool === 'backdrop' && !!this._bdRect; - if (!draws && !backdrop) return null; - return { - contextId, - kind: 'tool', - ariaLabel: this._t('editor.tool_options', { - tool: this._t((draws ? `decor.${this._decorTool}` : 'decor.backdrop') as any), - }), - content: html` - ${draws ? html` - ) => - (this._decorStyle = { ...this._decorStyle, ...e.detail })}> - - ${canFill ? html` - ) => - (this._decorStyle = { ...this._decorStyle, - fillColor: e.detail.color, fillOpacity: e.detail.opacity })}>` : nothing} - ` : nothing} - ${backdrop ? html`${this._t('decor.backdrop_hint')}` : nothing}`, - }; + return this._editorRuntimeOrThrow()._renderDecorSecondary(); } /** Backdrop reset is a space-level maintenance action, not a tool option. * Keep it reachable from every ordinary decor context as it was before the * contextual tray extraction; an explicit palette keeps priority. */ private _withBackdropReset(model: EditorSecondaryModel | null): EditorSecondaryModel | null { - if (!this._bdMoved || this._bdDrag || model?.kind === 'palette') return model; - const contextId = this._editorSecondaryContextId; - const reset = html``; - if (!model) { - return { - contextId, - kind: 'tool', - ariaLabel: this._t('editor.tool_options', { tool: this._t('decor.backdrop') }), - content: reset, - }; - } - return { - ...model, - kind: model.kind === 'selection' ? 'mixed' : model.kind, - content: html`${model.content}${reset}`, - }; + return this._editorRuntimeOrThrow()._withBackdropReset(model); } private get _editorSecondaryDialogBlocked(): boolean { @@ -11346,283 +8158,32 @@ class HouseplanCard extends LitElement { } private _renderEditorSecondary(): TemplateResult | typeof nothing { - if (!this._editing) return nothing; - const group = this._editorSecondary.activeGroup(this._editorToolbarGroups); - const model = group ? this._renderEditorGroupModel(group) - : this._mode === 'plan' ? this._renderPlanSecondary() - : this._mode === 'decor' ? this._withBackdropReset(this._renderDecorSecondary()) - : null; - return this._editorSecondary.render(model, this._editorSecondaryDialogBlocked); + return this._editorRuntimeOrThrow()._renderEditorSecondary(); } private _renderDecorBar(): TemplateResult { - const tools = [ - ['select', 'mdi:cursor-default-outline', 'decor.select'], - // moving the picture is a TOOL (docs/BACKDROP.md §2) — offered only when - // there IS a picture, so a hand-drawn space's bar is unchanged - ...(this._bdRect ? [['backdrop', 'mdi:image-move', 'decor.backdrop'] as const] : []), - ['line', 'mdi:vector-line', 'decor.line'], - ['rect', 'mdi:rectangle-outline', 'decor.rect'], - ['ellipse', 'mdi:ellipse-outline', 'decor.ellipse'], - ['text', 'mdi:format-text', 'decor.text'], - // the library sits next to the shapes it belongs with (docs/FURNITURE.md) - ['furniture', 'mdi:sofa-outline', 'decor.furniture'], - ['erase', 'mdi:eraser', 'decor.erase'], - ] as const; - const undoName = this._geometryHistory.undoName; - const redoName = this._geometryHistory.redoName; - return html`
-
- ${tools.map( - ([t, ic, k]) => html``, - )} - ${this._editorToolbarGroups.map((group) => this._renderEditorGroupLauncher(group))} - - -
-
- -
-
`; + return this._editorRuntimeOrThrow()._renderDecorBar(); } private _renderDecorEraseConfirm(): TemplateResult { - const pending = this._decorEraseConfirm!; - const kind = this._t(`decor.${pending.kind}` as any); - return html` (this._decorEraseConfirm = null)}> -

${this._t('confirm.erase_decor', { kind })}

-
- - - -
-
`; + return this._editorRuntimeOrThrow()._renderDecorEraseConfirm(); } private _renderDecorTextDialog(): TemplateResult { - const d = this._decorTextDialog!; - const ent = (d.pickerEntity || '').trim(); - const st = ent ? this.hass?.states?.[ent] : null; - return html` (this._decorTextDialog = null)}> -
- - ${''/* a textarea, not an input: the user's own line breaks are kept - and rendered (centred). Enter is a NEW LINE here, so saving - moved to Ctrl/Cmd+Enter and the button. */} - - ) => - (this._decorTextDialog = { ...d, ...e.detail })}> - -
(this._decorTextDialog = { ...d, - sizeCm: this._decorTextCm(Number((e.target as HTMLInputElement).value)) })} /> - ${this._t(this._imperial ? 'wallthick.unit_in' : 'wallthick.unit_cm')}
- - (this._decorTextDialog = { ...d, - angle: (e.target as HTMLInputElement).value })} /> - - - (this._decorTextDialog = { - ...d, pickerEntity: (e.target as HTMLInputElement).value, - })} /> - - ${Object.keys(this.hass?.states || {}).map((id) => html``)} - - ${ent ? html` - - - ` : nothing} -
-
- - - -
-
`; + return this._editorRuntimeOrThrow()._renderDecorTextDialog(); } private _renderDecorShapeDialog(): TemplateResult { - const d = this._decorShapeDialog!; - const canFill = d.kind === 'rect' || d.kind === 'ellipse'; - const kindLabel = this._t(('decor.' + d.kind) as any); - const unit = this._t(this._imperial ? 'gs.unit_ft' : 'gs.unit_m'); - return html` (this._decorShapeDialog = null)}> -
- ${d.kind === 'furniture' ? html` - - ` : nothing} - ) => - (this._decorShapeDialog = { ...d, ...e.detail })}> - -
(this._decorShapeDialog = { - ...d, widthCm: this._decorSmallCm(Number((e.target as HTMLInputElement).value)), - })} />${this._t(this._imperial ? 'wallthick.unit_in' : 'wallthick.unit_cm')}
- ${d.kind === 'line' ? html` - -
- - -
- -
(this._decorShapeDialog = { ...d, - lengthCm: this._decorLargeCm(Number((e.target as HTMLInputElement).value)) })} /> - ${unit}
` : html` - -
(this._decorShapeDialog = { ...d, - sizeWCm: this._decorLargeCm(Number((e.target as HTMLInputElement).value)) })} /> - × (this._decorShapeDialog = { ...d, - sizeHCm: this._decorLargeCm(Number((e.target as HTMLInputElement).value)) })} /> - ${unit}
`} - - (this._decorShapeDialog = { ...d, - angle: (e.target as HTMLInputElement).value })} /> - ${canFill ? html` - ) => - (this._decorShapeDialog = { ...d, - fillColor: e.detail.color, fillOpacity: e.detail.opacity })}>` : nothing} -
-
- - - -
-
`; + return this._editorRuntimeOrThrow()._renderDecorShapeDialog(); } private _renderBackdropDialog(): TemplateResult { - const d = this._backdropDialog!; - const unit = this._t(this._imperial ? 'gs.unit_ft' : 'gs.unit_m'); - return html` (this._backdropDialog = null)}> -
- -
(this._backdropDialog = { ...d, - widthCm: this._decorLargeCm(Number((e.target as HTMLInputElement).value)) })} /> - × (this._backdropDialog = { ...d, - heightCm: this._decorLargeCm(Number((e.target as HTMLInputElement).value)) })} /> - ${unit}
- - (this._backdropDialog = { ...d, - angle: (e.target as HTMLInputElement).value })} /> -
-
- - -
-
`; + return this._editorRuntimeOrThrow()._renderBackdropDialog(); } /** CSS-pixel interaction constants stay stable at every plan zoom. */ private _cssPxToRender(px: number): number { - const stage = this._stageEl; - const view = this._viewOr(this._baseVb()); - if (!stage?.clientWidth || !stage.clientHeight) return (this._gridPitch / 8) * px; - return Math.max(view.w / stage.clientWidth, view.h / stage.clientHeight) * px; + return this._editorRuntimeOrThrow()._cssPxToRender(px); } /** Zero-thickness walls. Their paint and light policy share one resolver. */ @@ -11654,112 +8215,12 @@ class HouseplanCard extends LitElement { /** Delete tool: defer all wall consequences to one explicit accessible choice. */ private _deleteRoomClick(raw: number[]): void { - const space = this._spaceModel(); - if (!space) return; - const room = [...space.rooms].reverse().find((r) => this._pointInRoom(raw, r)); - if (!room) { - this._showToast(this._t('toast.delete_room_pick')); - return; - } - if (!room.id) return; - this._roomDeleteDialog = { roomId: room.id, name: room.name }; + return this._editorRuntimeOrThrow()._deleteRoomClick(raw); } private _confirmRoomDelete = (keepWalls: boolean): void => { - const dialog = this._roomDeleteDialog; - const sp = this._curSpaceCfg; - const space = this._spaceModel(); - if (!dialog || !sp || !space) return; - const room = space.rooms.find((candidate) => candidate.id === dialog.roomId); - if (!room) { this._roomDeleteDialog = null; return; } - const openCuts = this._openCuts(); - const allIntervals = wallIntervals( - space.rooms, this._spaceWalls, openCuts, - this._wallKeyPitch, this._cellCm, this._gridPitch, NORM_W, - ); - const roomIntervals = allIntervals.filter((interval) => interval.roomId === room.id); - const plan = planRoomDeletion( - roomIntervals, - space.partitions, - this._openingsR.map((opening) => ({ - ...opening, x: opening.rx, y: opening.ry, - })), - this._gridPitch * 0.02, - ); - const newCount = new Set(plan.materialize - .filter((item) => !item.reusePartitionId) - .map((item) => item.interval.key)).size; - if (keepWalls && (sp.partitions || []).length + newCount > MAX_PARTITIONS) { - this._showToast(this._t('toast.physical_limit')); - return; - } - const before = this._geometrySnapshot(); - const materializedWalls = materializeWallIntervals( - space.rooms, this._spaceWalls, openCuts, - this._wallKeyPitch, this._cellCm, this._gridPitch, NORM_W, - ); - if (keepWalls) { - sp.partitions ||= []; - const targetByInterval = new Map(); - const createdByKey = new Map(); - const sourceWallId = (interval: { a: readonly number[]; b: readonly number[] }): string | null => { - const match = (sp.wall_segments || []).find((segment: any) => { - const a = [Number(segment.a?.[0]) * NORM_W, Number(segment.a?.[1]) * NORM_W]; - const b = [Number(segment.b?.[0]) * NORM_W, Number(segment.b?.[1]) * NORM_W]; - return (this._samePt(interval.a, a) && this._samePt(interval.b, b)) - || (this._samePt(interval.a, b) && this._samePt(interval.b, a)); - }); - return typeof match?.id === 'string' && match.id ? match.id : null; - }; - const seed = Date.now().toString(36); - for (const item of plan.materialize) { - let target = item.reusePartitionId - ? sp.partitions.find((partition: PartitionCfg) => partition.id === item.reusePartitionId) - : createdByKey.get(item.interval.key); - if (!target) { - target = { - id: sourceWallId(item.interval) || `partition-room-${seed}-${createdByKey.size}`, - a: [item.interval.a[0] / NORM_W, item.interval.a[1] / NORM_W], - b: [item.interval.b[0] / NORM_W, item.interval.b[1] / NORM_W], - cm: item.interval.cm, - }; - sp.partitions.push(target); - createdByKey.set(item.interval.key, target); - } - targetByInterval.set(item.interval.key, target); - } - for (const opening of sp.openings || []) { - const intervalKey = plan.openingIntervals.get(opening.id); - const target = intervalKey ? targetByInterval.get(intervalKey) : null; - const rendered = this._openingsR.find((candidate) => candidate.id === opening.id); - if (!target || !rendered) continue; - const renderTarget = { - a: [target.a[0] * NORM_W, target.a[1] * NORM_W], - b: [target.b[0] * NORM_W, target.b[1] * NORM_W], - }; - opening.host = { - kind: 'partition', id: target.id, - t: parameterOnPartition([rendered.rx, rendered.ry], renderTarget), - }; - } - } else if (plan.removeOpeningIds.length) { - const remove = new Set(plan.removeOpeningIds); - sp.openings = (sp.openings || []).filter((opening: OpeningCfg) => !remove.has(opening.id)); - if (!sp.openings.length) delete sp.openings; - } - sp.rooms = sp.rooms.filter((r: any) => r.id !== room.id); - this._cfgEpoch++; - const normalized = this._normalizeWalls(materializedWalls, this._openCuts()); - if (normalized.length) sp.walls = normalized; - else delete sp.walls; - this._roomDeleteDialog = null; - this._commitPhysicalGeometry(this._t( - keepWalls ? 'history.delete_room_keep_walls' : 'history.delete_room_with_walls', - ), before); - this._regSignature = ''; - this._maybeRebuildDevices(); - this.requestUpdate(); - }; + return this._editorRuntimeOrThrow()._confirmRoomDelete(keepWalls); + } // ================= WALL THICKNESS (docs/WALL-THICKNESS.md) ================= @@ -11916,62 +8377,7 @@ class HouseplanCard extends LitElement { * well. The unit of the tool is now the same interval the renderer draws. */ private _wallThickHit(raw: number[]): WallThickHit | null { - const space = this._spaceModel(); - if (!space) return null; - const pull = this._gridPitch * 6; - const cuts = this._openCuts(); - type Hit = WallThickHit; - // #313: the tool serves every wall — room intervals AND independent - // masonry. On an exact overlap (the #308 duplicate) the independent wall - // wins: it owns the hit zone, exactly like the select tool, and it is - // the body the eye actually sees. - let best: { hit: Hit; d: number; independent: boolean } | null = null; - const offer = (hit: Hit, d: number, independent: boolean): void => { - if (d > pull) return; - if (!best || d < best.d - 1e-9 - || (independent && !best.independent && d <= best.d + 1e-9)) { - best = { hit, d, independent }; - } - }; - for (const iv of wallIntervals( - space.rooms, this._spaceWalls, cuts, - this._wallKeyPitch, this._cellCm, this._gridPitch, NORM_W, - )) { - offer({ - a: iv.a, b: iv.b, roomId: iv.roomId, - segs: [[iv.a[0], iv.a[1], iv.b[0], iv.b[1]]], - open: iv.open, cm: iv.cm, source: { kind: 'room' }, - }, distToSegment(raw, [iv.a[0], iv.a[1], iv.b[0], iv.b[1]]), false); - } - for (const partition of space.partitions || []) { - const a = [partition.a[0], partition.a[1]]; - const b = [partition.b[0], partition.b[1]]; - offer({ - a, b, roomId: '', segs: [[a[0], a[1], b[0], b[1]]], - open: false, cm: Number(partition.cm) || 0, - source: { kind: 'partition', id: partition.id }, - }, distToSegment(raw, [a[0], a[1], b[0], b[1]]), true); - } - for (const draft of space.room_drafts || []) { - // The active chain belongs to the draw tool; like the snap geometry, - // Thickness only sees saved drafts. - if (draft.id === this._activeDraftId) continue; - for (let i = 0; i + 1 < draft.points.length; i++) { - const a = [draft.points[i][0], draft.points[i][1]]; - const b = [draft.points[i + 1][0], draft.points[i + 1][1]]; - // Zero is a legitimate stored value («Empty / 0 leaves the new wall - // thin», docs/WALL-THICKNESS.md §6) — only a MISSING record falls - // back to the drawing default. `|| 15` would silently turn a stored - // 0 into 15 on Apply (CODE-REVIEW-313-r1 High). - const rawCm = Number(draft.segments[i]?.cm); - offer({ - a, b, roomId: '', segs: [[a[0], a[1], b[0], b[1]]], - open: false, cm: Number.isFinite(rawCm) ? rawCm : 15, - source: { kind: 'draft', id: draft.id, segment: i }, - }, distToSegment(raw, [a[0], a[1], b[0], b[1]]), true); - } - } - return best ? (best as { hit: Hit }).hit : null; + return this._editorRuntimeOrThrow()._wallThickHit(raw); } private get _wallThickHover(): { segs: number[][]; open: boolean; d: string } | null { @@ -11992,168 +8398,18 @@ class HouseplanCard extends LitElement { } private _wallThickClick(raw: number[]): void { - const hit = this._wallThickHit(raw); - if (!hit) { - this._showToast(this._t('toast.wallthick_pick')); - return; - } - const cm = hit.cm; - const view = this._viewOr(this._baseVb()); - const mx = (hit.a[0] + hit.b[0]) / 2, my = (hit.a[1] + hit.b[1]) / 2; - this._wallDialog = { - a: hit.a, b: hit.b, - value: cmToField(cm, this._imperial), - roomId: hit.roomId, - source: hit.source, - sx: ((mx - view.x) / view.w) * 100, - sy: ((my - view.y) / view.h) * 100, - }; + return this._editorRuntimeOrThrow()._wallThickClick(raw); } private _wallThickApply(allRoom: boolean): void { - const d = this._wallDialog; - if (!d) return; - const sp = this._curSpaceCfg; - const space = this._spaceModel(); - if (!sp || !space) return; - const text = d.value.trim(); - const raw = text ? strictNumber(text) : null; - if (raw == null) { this._showPhysicalRange(100); return; } - const cmRaw = this._imperial ? raw * 2.54 : raw; - if (!Number.isFinite(cmRaw) || cmRaw < 0 || cmRaw > 100) { - this._showPhysicalRange(100); - return; - } - // Independent walls and saved drafts own their records. Zero is the same - // canonical bodyless state here as it is for a contour atom (#306). - if (d.source.kind !== 'room') { - const before = this._geometrySnapshot(); - if (d.source.kind === 'partition') { - const partition = (sp.partitions || []) - .find((item: PartitionCfg) => item.id === (d.source as { id: string }).id); - if (!partition) return; - if (cmRaw === 0 && zeroWallHasOpening(sp.openings, { - kind: 'partition', id: partition.id, - })) { - this._showToast(this._t('toast.zero_wall_opening_conflict')); - return; - } - partition.cm = cmRaw; - } else { - const draft = (sp.room_drafts || []) - .find((item: any) => item.id === (d.source as { id: string }).id); - const segment = draft?.segments?.[(d.source as { segment: number }).segment]; - if (!segment) return; - segment.cm = cmRaw; - } - this._wallDialog = null; - const committed = this._commitPhysicalGeometry( - this._t('history.wall_thickness'), before, - ); - if (committed) this._showToast(this._t('toast.wallthick_set')); - this.requestUpdate(); - return; - } - const before = this._geometrySnapshot(); - const cm = cmRaw > 0 ? cmRaw : null; - if (cmRaw === 0) { - const eps = this._gridPitch * 0.02; - const target = [d.a[0], d.a[1], d.b[0], d.b[1]]; - const room = (sp.rooms || []).find((item: any) => item.id === d.roomId); - const ids = new Set(allRoom && room && Array.isArray(room.wall_ids) - ? room.wall_ids - : (sp.wall_segments || []).filter((segment: any) => { - const a = [Number(segment.a?.[0]) * NORM_W, Number(segment.a?.[1]) * NORM_W]; - const b = [Number(segment.b?.[0]) * NORM_W, Number(segment.b?.[1]) * NORM_W]; - return distToSegment(a, target) <= eps && distToSegment(b, target) <= eps; - }).map((segment: any) => String(segment.id))); - const blocked = (sp.openings || []).some((opening: OpeningCfg) => { - if (opening.host?.kind === 'partition') return false; - if (opening.host?.kind === 'wall' && ids.has(opening.host.id)) return true; - if (opening.host) return false; - const point = [Number(opening.x) * NORM_W, Number(opening.y) * NORM_W]; - return point.every(Number.isFinite) && distToSegment(point, target) <= eps; - }); - if (blocked) { - this._showToast(this._t('toast.zero_wall_opening_conflict')); - return; - } - } - let openCuts = this._openCuts(); - if (cmRaw > 0 && openCuts.length) { - // The selected zero axis is a cut only in the PRE-edit body profile. - // Remove exactly the carriers being restored before the compatibility - // wall normalizer runs; otherwise it discards the new positive record as - // if the segment were still a zero interval (#306). - const targets: number[][] = []; - if (allRoom && d.roomId) { - const room = (sp.rooms || []).find((item: any) => item.id === d.roomId); - const ids = new Set(Array.isArray(room?.wall_ids) ? room.wall_ids : []); - for (const segment of sp.wall_segments || []) { - if (!ids.has(segment.id)) continue; - targets.push([ - Number(segment.a?.[0]) * NORM_W, Number(segment.a?.[1]) * NORM_W, - Number(segment.b?.[0]) * NORM_W, Number(segment.b?.[1]) * NORM_W, - ]); - } - // A pre-v9 room has no catalogue IDs yet. Its current zero axes still - // come from the rendered room outline, so use that outline as the - // carrier set for this first explicit positive-thickness edit. The - // common v9 barrier below will materialise stable IDs atomically. - if (!targets.length) { - const renderedRoom = space.rooms.find((item: any) => item.id === d.roomId); - const poly = renderedRoom ? roomPoly(renderedRoom) : null; - for (let index = 0; poly && index < poly.length; index++) { - const a = poly[index], b = poly[(index + 1) % poly.length]; - targets.push([a[0], a[1], b[0], b[1]]); - } - } - } else targets.push([d.a[0], d.a[1], d.b[0], d.b[1]]); - const eps = this._gridPitch * 0.02; - openCuts = openCuts.filter((cut) => !targets.some((target) => ( - distToSegment([cut[0], cut[1]], target) <= eps - && distToSegment([cut[2], cut[3]], target) <= eps - && distToSegment([target[0], target[1]], cut) <= eps - && distToSegment([target[2], target[3]], cut) <= eps - ))); - } - let next: WallEntry[]; - if (allRoom && d.roomId) { - next = setWallThicknessForRoom( - sp.walls, space.rooms, d.roomId, cm, this._wallKeyPitch, openCuts, NORM_W, - ); - } else { - next = setWallThickness(sp.walls, d.a, d.b, cm, this._wallKeyPitch, NORM_W); - } - next = this._normalizeWalls(next, openCuts); - // ADR 282 Stage 1: this writer knows that absence from `next` is an - // explicit zero chosen by the user, not a missing compatibility record. - // Mirror that intent into the canonical catalogue before the common - // barrier; otherwise its lineage fallback would resurrect the old cm. - for (const segment of sp.wall_segments || []) { - segment.cm = thicknessCmAt( - next, segment.a, segment.b, GRID_STEP_N, 1, - ); - } - if (next.length) sp.walls = next; - else delete sp.walls; - this._wallDialog = null; - if (this._commitPhysicalGeometry(this._t('history.wall_thickness'), before)) - this._showToast(this._t(cmRaw === 0 ? 'toast.wallthick_cleared' : 'toast.wallthick_set')); - this.requestUpdate(); + return this._editorRuntimeOrThrow()._wallThickApply(allRoom); } private _wallHatchDefs(color: string): TemplateResult { - if (!this._spaceWalls.length && !this._physicalBodiesR().length && !this._markup) - return svg`` as unknown as TemplateResult; - // The step is a physical distance now — 9.6 cm of plan, whatever the grid - // scale — so it must NOT be compensated for zoom: a wall that changes its - // hatching as you zoom is exactly what #230 is about (spec §4.2). + // Hatching is visible in View as well as in Plan, so its tiny SVG + // definition belongs to the eager projection rather than editor runtime. const step = wallHatchStepUnits(this._cellCm); const stripe = 2 * (step / HATCH_BASE_STEP_UNITS); - // Bake the wall colour into the pattern — CSS vars on content - // do not inherit from the filled path, so var(--room-stroke) fell back - // to grey while the wall outline used the real border colour. const stroke = color || '#607d8b'; return svg` ` as unknown as TemplateResult; + if (!this._editorRuntime) return svg`` as unknown as TemplateResult; + return this._editorRuntimeOrThrow()._renderWallThickUi(); } /** Thickness input popover, anchored in stage % like measure labels. */ private _renderWallThickDialog(): TemplateResult { - const d = this._wallDialog; - if (!d) return html``; - return html`
e.stopPropagation()}> -
- - { - this._wallDialog = { ...d, value: (e.target as HTMLInputElement).value }; - }} - @keydown=${(e: KeyboardEvent) => { - if (e.key === 'Enter') { e.preventDefault(); this._wallThickApply(false); } - }} /> - ${this._t(this._imperial ? 'wallthick.unit_in' : 'wallthick.unit_cm')} -
-
- ${d.source.kind === 'room' ? html`` : nothing} - - -
-
`; + return this._editorRuntimeOrThrow()._renderWallThickDialog(); } private _openingAt(raw: readonly number[]): RenderOpening | null { - if (!this._openingsR.length) return null; - const roughHits = this._openingsR.flatMap((o) => { - const rad = o.angle * Math.PI / 180; - const dx = raw[0] - o.rx, dy = raw[1] - o.ry; - const localX = dx * Math.cos(rad) + dy * Math.sin(rad); - if (Math.abs(localX) > o.rlen / 2 + gridVisualUnits(12, this._cellCm)) return []; - return [{ o, localY: -dx * Math.sin(rad) + dy * Math.cos(rad) }]; - }); - if (!roughHits.length) return null; - const space = this._spaceModel(); - if (!space) return null; - const openCuts = this._openCuts(); - const wallIndex = this._openingWallIndexFor(space, openCuts).value; - return roughHits.find(({ o, localY }) => { - const faceFlipV = o.type === 'gate' ? !o.flip_v : o.flip_v; - const face = o.partitionHost || this._spaceWalls.length || o.type === 'gate' - ? this._openingFace(o, wallIndex, !!faceFlipV) - : { ox: 0, oy: 0, cm: 0, side: -1 as -1 | 1 }; - const metrics = openingVisibleMetrics({ - type: o.type, - length: o.rlen, - angle: o.angle, - amount: this._openingAmt(o), - flipH: !!o.flip_h, - flipV: !!o.flip_v, - base: '', tone: '', - cellCm: this._cellCm, - gridPitch: this._gridPitch, - face, - }); - // Match the committed `.op-hit` rectangle exactly. The old radial test - // made a 3 m gate suppress placement in a huge circular area far away - // from its visible/hit geometry. - return Math.abs(localY) <= metrics.hitHalf; - })?.o || null; + return this._editorRuntimeOrThrow()._openingAt(raw); } private _resolveOpeningPlacement(raw: readonly number[]): OpeningPlacementCandidate | null { - const preset = this._openingPreset; - if (this._tool !== 'opening' || !preset) return null; - const space = this._spaceModel(); - if (!space) return null; - const openCuts = this._openCuts(); - const wallIndex = this._openingWallIndexFor(space, openCuts); - const placementKey = `${wallIndex.key}|partitions:${this._cfgEpoch}`; - if (!this._openingPlacementIntervalsCache - || this._openingPlacementIntervalsCache.key !== placementKey) { - this._openingPlacementIntervalsCache = { - key: placementKey, - value: [ - ...wallIntervals( - space.rooms, this._spaceWalls, openCuts, - this._wallKeyPitch, this._cellCm, this._gridPitch, NORM_W, - ), - ...partitionPlacementIntervals(space.partitions, this._cellCm, this._gridPitch), - ], - }; - } - if (!this._openingDimensionContextCache - || this._openingDimensionContextCache.key !== placementKey) { - this._openingDimensionContextCache = { - key: placementKey, - value: buildOpeningDimensionContext({ - rooms: space.rooms, - walls: this._spaceWalls, - openCuts, - partitions: space.partitions, - roomOpenings: this._roomWallOpeningInputs(), - partitionCuts: this._partitionOpeningCuts(space), - pitch: this._wallKeyPitch, - cellCm: this._cellCm, - gridPitch: this._gridPitch, - coordScale: NORM_W, - epsilon: this._gridPitch * 0.0002, - }), - }; - } - const resolution = resolveOpeningPlacementResult({ - pointer: [raw[0], raw[1]], - preset, - geometryRevision: this._cfgEpoch, - renderedLength: this._cmToUnits(preset.lengthCm), - intervals: this._openingPlacementIntervalsCache.value, - baseTolerance: this._gridPitch * 1.5, - bodyPointerPadding: this._cssPxToRender( - this._pointerModality.modality === 'touch' || this._pointerModality.modality === 'pen' ? 10 : 6, - ), - gridStep: this._gridPitch, - }); - this._openingJambBlockCm = resolution.jambBlockedTarget - ? (resolution.jambBlockedTarget.physicalHalfWidth / this._gridPitch) * this._cellCm - : null; - const core = resolution.candidate; - if (!core) return null; - const faceFlipV = core.type === 'gate' ? !core.flipV : core.flipV; - let face: OpeningFaceOffset; - if (core.host) { - const hosted = resolvePartitionOpeningCompat({ - id: 'preview', type: core.type, - x: core.x / NORM_W, y: core.y / NORM_W, - angle: core.angle, length: core.renderedLength / NORM_W, - host: core.host, - }, space.partitions, NORM_W, this._cellCm, this._gridPitch).resolved; - face = hosted - ? partitionOpeningFace(hosted, faceFlipV) - : { ox: 0, oy: 0, cm: 0, side: -1 }; - } else { - face = openingInnerFaceOffsetFromIndex( - wallIndex.value, - { x: core.x, y: core.y, angle: core.angle, length: core.renderedLength, flip_v: faceFlipV }, - ); - } - const imperial = this.hass?.config?.unit_system?.length === 'mi'; - // #238: placement labels are physical dimensions. They consume the ONE - // resolved candidate above and never re-run pointer snap; existing-opening - // drag deliberately keeps `_opRuler`/`openingShoulders` below. - const dimensions = resolveOpeningDimensions( - core, this._openingDimensionContextCache.value, - ); - const labels = dimensions.map((dimension) => ({ - x: dimension.label[0], - y: dimension.label[1], - text: formatLength((dimension.distance / this._gridPitch) * this._cellCm, imperial), - dimension, - })); - return { ...core, face, measure: { labels, guide: core.measure.guide } }; + return this._editorRuntimeOrThrow()._resolveOpeningPlacement(raw); } private _activateOpeningPlacement(type: OpeningPlacementType): void { - this._activateMarkupTool('opening'); - if (this._tool !== 'opening') return; - this._openingPreset = openingPlacementPreset(type, ++this._openingPresetRevision); - this._openingHoverCandidate = null; - this._cursorPt = null; + return this._editorRuntimeOrThrow()._activateOpeningPlacement(type); } private _clearOpeningPlacement(clearPreset: boolean): void { - this._openingHoverCandidate = null; - this._openingJambBlockCm = null; - if (clearPreset) { - this._openingPreset = null; - this._openingRebindId = null; + if (!this._editorRuntime) { + this._openingHoverCandidate = null; + this._openingJambBlockCm = null; + if (clearPreset) { + this._openingPreset = null; + this._openingRebindId = null; + } + return; } + return this._editorRuntimeOrThrow()._clearOpeningPlacement(clearPreset); } /** Opening tool: click an existing opening to edit it, or a wall to place one. */ private _openingClick(raw: number[]): void { - const space = this._spaceModel(); - if (!space) return; - // Rebind is an explicit one-shot placement: the orphan's stale fallback - // position must not win hit testing when the replacement wall crosses it. - const hit = this._openingRebindId ? null : this._openingAt(raw); - if (hit) { - this._editOpening(hit); - return; - } - const preset = this._openingPreset; - if (!preset) return; - const cached = this._openingHoverCandidate; - const place = cached && sameOpeningPlacementInput( - cached, [raw[0], raw[1]], preset.revision, this._cfgEpoch, - ) ? cached : this._resolveOpeningPlacement(raw); - if (!place) { - if (this._openingJambBlockCm != null) { - this._showToast(this._t('opening.partition_jamb_margin', { - distance: formatLength(this._openingJambBlockCm, this._imperial), - })); - return; - } - const eps = this._gridPitch * 1.5; - const snap = snapToWall(raw, space.rooms, eps); - if (snap && pointOnOpenCut(snap.x, snap.y, snap.angle, this._openCuts(), eps)) { - this._showToast(this._t('toast.opening_on_zero_wall')); - return; - } - this._showToast(this._t('toast.opening_no_wall')); - return; - } - const rebound = this._openingRebindId - ? this._curSpaceCfg?.openings?.find((item: OpeningCfg) => item.id === this._openingRebindId) - : null; - this._openingDialog = { - ...(rebound ? { - id: rebound.id, - type: rebound.type, - lengthCm: Math.round((rebound.length * NORM_W / this._gridPitch) * this._cellCm), - lengthTouched: false, - contact: rebound.contact || '', lock: rebound.lock || '', - invert: !!rebound.invert, flipH: !!rebound.flip_h, flipV: !!rebound.flip_v, - } : { - type: place.type, lengthCm: place.lengthCm, contact: '', lock: '', - invert: false, flipH: place.flipH, flipV: place.flipV, - }), - ...(place.host ? { host: place.host } : {}), - x: place.x, y: place.y, angle: place.angle, - }; - this._openingRebindId = null; - // rulers, tick and ghost live only through the placement gesture - this._openingHoverCandidate = null; - this._cursorPt = null; + return this._editorRuntimeOrThrow()._openingClick(raw); } /** Open the properties dialog for an existing opening. */ private _editOpening(o: RenderOpening): void { - this._openingDialog = { - id: o.id, - type: o.type, - lengthCm: Math.round((o.rlen / this._gridPitch) * this._cellCm), - lengthTouched: false, - contact: o.contact || '', - lock: o.lock || '', - invert: !!o.invert, - flipH: !!o.flip_h, - flipV: !!o.flip_v, - ...(o.host?.kind === 'partition' ? { host: { ...o.host } } : {}), - x: o.rx, y: o.ry, angle: o.angle, - }; + if (!this._editorRuntime) return; + return this._editorRuntimeOrThrow()._editOpening(o); } /** Drag an opening along the walls (view mode): it re-snaps continuously. */ private _opPointerDown(ev: PointerEvent, o: OpeningCfg): void { - if (this._mode !== 'plan' || !this._spaceModel()) return; - // HP-1550-04: in the resize tool the wall handles own the geometry — a door - // in the middle of a wall must neither swallow the handle nor start its own - // drag (it travels with the wall through the resize pipeline instead) - if (this._tool === 'resize') return; - ev.preventDefault(); - ev.stopPropagation(); - try { - capturePointer(ev); - } catch { - /* an inactive pointerId (synthetic events, some browsers) must not kill the drag */ - } - this._opDrag = { - id: o.id, moved: false, sx: ev.clientX, sy: ev.clientY, dirty: false, - before: this._geometrySnapshot(), - }; + if (!this._editorRuntime) return; + return this._editorRuntimeOrThrow()._opPointerDown(ev, o); } private _opPointerMove(ev: PointerEvent, o: OpeningCfg): void { - if (!this._opDrag || this._opDrag.id !== o.id) return; - // audit L4: the other drag pipelines require 3 px before calling it a drag. - // Without it every tap counted as a drag: the properties dialog never - // opened and an unchanged config was written (which then broadcast the - // event behind the L2 data loss). - if (Math.abs(ev.clientX - this._opDrag.sx) + Math.abs(ev.clientY - this._opDrag.sy) <= 3) return; - const space = this._spaceModel(); - if (!space) return; - const raw = this._svgPoint(ev); - const sp = this._curSpaceCfg; - const cfg = sp?.openings?.find((x: OpeningCfg) => x.id === o.id); - if (!cfg) return; - if (cfg.host?.kind === 'partition') { - const partition = space.partitions.find((item) => item.id === cfg.host!.id); - if (!partition) return; - const dx = partition.b[0] - partition.a[0], dy = partition.b[1] - partition.a[1]; - const length = Math.hypot(dx, dy); - if (!(length > 1e-9)) return; - const ux = dx / length, uy = dy / length; - const perpendicular = Math.abs( - (raw[0] - partition.a[0]) * uy - (raw[1] - partition.a[1]) * ux, - ); - if (perpendicular > this._gridPitch * 4) return; - const half = cfg.length * NORM_W / 2; - const jamb = partitionOpeningJambMargin(partition, this._cellCm, this._gridPitch); - const shoulder = half + jamb; - let along = (raw[0] - partition.a[0]) * ux + (raw[1] - partition.a[1]) * uy; - along = Math.round(along / this._gridPitch) * this._gridPitch; - along = Math.max(shoulder, Math.min(length - shoulder, along)); - if (length < shoulder * 2 - 1e-9) return; - const t = along / length; - const nx = (partition.a[0] + ux * along) / NORM_W; - const ny = (partition.a[1] + uy * along) / this._spaceH; - let angle = Math.atan2(dy, dx) * 180 / Math.PI; - if (angle >= 90) angle -= 180; - else if (angle < -90) angle += 180; - const changed = cfg.host.t !== t || cfg.x !== nx || cfg.y !== ny || cfg.angle !== angle; - this._opDrag.moved = true; - if (changed) this._opDrag.dirty = true; - cfg.host = { ...cfg.host, t }; - cfg.x = nx; cfg.y = ny; cfg.angle = angle; - this._opMeasure = null; - if (changed) this._cfgEpoch++; - this.requestUpdate(); - return; - } - const snap = snapToWall(raw, space.rooms, this._gridPitch * 4); - if (!snap) return; // too far from any wall: the opening stays where it was - this._opDrag.moved = true; - // ruler badges on both shoulders + soft magnet to the wall's center - // (owner 2026-08-03) — the very same helper the PLACEMENT preview uses - const r = this._opRuler(snap, cfg.length * NORM_W); - this._opMeasure = r.measure; - const nx = r.x / NORM_W; - const ny = r.y / this._spaceH; - const geometryChanged = cfg.x !== nx || cfg.y !== ny || cfg.angle !== snap.angle; - if (geometryChanged) this._opDrag.dirty = true; - cfg.x = nx; - cfg.y = ny; - cfg.angle = snap.angle; - if (Number(this._serverCfg?.model_version || 0) >= 9) { - const host = resolveRoomOpeningHost(cfg, sp?.wall_segments || []); - if (host) cfg.host = host; - else delete cfg.host; - } - // The wall cut and room-coloured tunnel are geometry caches too. Advance - // the preview epoch so both follow the live opening instead of remaining at - // its pre-drag location until pointerup/save. - if (geometryChanged) this._cfgEpoch++; - this.requestUpdate(); + if (!this._editorRuntime) return; + return this._editorRuntimeOrThrow()._opPointerMove(ev, o); } /** @@ -13010,369 +8973,65 @@ class HouseplanCard extends LitElement { snap: { x: number; y: number; angle: number }, rlen: number, ): { x: number; y: number; angle: number; measure: OpMeasure | null } { - const rooms = this._spaceModel()?.rooms || []; - const tol = this._gridPitch / 2; - let cx = snap.x, cy = snap.y; - let sh = openingShoulders([cx, cy], snap.angle, rlen, rooms, tol); - if (sh && sh.centered && (cx !== sh.wallCenter[0] || cy !== sh.wallCenter[1])) { - [cx, cy] = sh.wallCenter; - sh = openingShoulders([cx, cy], snap.angle, rlen, rooms, tol); - } else if (sh) { - // Not centred: quantise the offset ALONG the wall to the grid step - // (docs/CANVAS.md §9.3). Grid-BOUND would lift the opening off a diagonal - // wall, so the wall stays the master and the grid only says WHERE on it. - // The magnet is consulted first on purpose — a wall whose middle is not a - // node must still be able to hold a centred window. - const [ax, ay] = sh.wallA, [bx, by] = sh.wallB; - const wx = bx - ax, wy = by - ay; - const len = Math.hypot(wx, wy); - if (len > 0) { - const g = this._gridPitch; - const half = Math.min(rlen / 2, len / 2); - let along = Math.round((((cx - ax) * wx + (cy - ay) * wy) / len) / g) * g; - along = Math.max(half, Math.min(len - half, along)); - cx = ax + (along / len) * wx; - cy = ay + (along / len) * wy; - sh = openingShoulders([cx, cy], snap.angle, rlen, rooms, tol) || sh; - } - } - if (!sh) return { x: cx, y: cy, angle: snap.angle, measure: null }; - const imperial = this.hass?.config?.unit_system?.length === 'mi'; - const lbl = (d: number, m: number[]) => - ({ x: m[0], y: m[1], text: formatLength((d / this._gridPitch) * this._cellCm, imperial) }); - return { - x: cx, y: cy, angle: snap.angle, - measure: { - labels: [lbl(sh.sideA, sh.midA), lbl(sh.sideB, sh.midB)], - guide: sh.centered - ? { x: sh.wallCenter[0], y: sh.wallCenter[1], angle: snap.angle } - : null, - }, - }; + return this._editorRuntimeOrThrow()._opRuler(snap, rlen); } private _opPointerUp(ev: PointerEvent, o: OpeningCfg): void { - if (!this._opDrag || this._opDrag.id !== o.id) return; - const drag = this._opDrag; - const moved = drag.moved; - this._opMeasure = null; // badges and the center tick live only through the drag - // only write when the geometry actually changed (audit L4) - if (moved && drag.dirty) { - this._commitPhysicalGeometry(this._t('history.move_opening'), drag.before); - } - // keep the flag until the click event that follows pointerup, then let it go - if (moved) window.setTimeout(() => (this._opDrag = null), 0); - else this._opDrag = null; + if (!this._editorRuntime) return; + return this._editorRuntimeOrThrow()._opPointerUp(ev, o); } /** Click: the status card (delayed so a double click can cancel it). */ private _opClick(ev: MouseEvent, o: RenderOpening): void { - // HP-1550-04: in the resize tool a click over an opening falls through to - // the stage (room picking) instead of opening the editor dialog - if (this._mode === 'plan' && this._tool === 'resize') return; - ev.stopPropagation(); - if (this._opDrag?.moved) return; // that click was the tail of a drag - // openings are inert outside Plan mode (owner's decision: View must not - // interact with them at all); in Plan any click on an opening edits it - if (this._mode === 'plan') this._editOpening(o); + if (!this._editorRuntime) return; + return this._editorRuntimeOrThrow()._opClick(ev, o); } private _saveOpening(): void { - const d = this._openingDialog; - const sp = this._curSpaceCfg; - const model = this._spaceModel(); - if (!d || !sp || !model) return; - const before = this._geometrySnapshot(); - const H = this._spaceH; - const previous = (sp.openings || []).find((item: OpeningCfg) => item.id === d.id); - const o: OpeningCfg = { - ...(previous || {}), - id: d.id || 'o' + Date.now().toString(36), - type: d.type, - x: d.x / NORM_W, - y: d.y / H, - angle: d.angle, - length: previous && !d.lengthTouched - ? previous.length - : this._cmToUnits(Math.max(20, d.lengthCm)) / NORM_W, - ...(d.host ? { host: { ...d.host } } : {}), - }; - if (o.host?.kind === 'partition') { - const strict = partitionOpeningNeedsStrictValidation(previous, o); - const resolution = strict - ? resolvePartitionOpeningStrict( - o, model.partitions, NORM_W, this._cellCm, this._gridPitch, - ) - : resolvePartitionOpeningCompat( - o, model.partitions, NORM_W, this._cellCm, this._gridPitch, - ); - if (!resolution.resolved) { - const partition = model.partitions.find((item) => item.id === o.host!.id); - if (resolution.reason === 'does-not-fit-jamb' && partition) { - const margin = partitionOpeningJambMargin( - partition, this._cellCm, this._gridPitch, - ); - this._showToast(this._t('opening.partition_jamb_margin', { - distance: formatLength( - (margin / this._gridPitch) * this._cellCm, this._imperial, - ), - })); - } else { - this._showToast(this._t('opening.partition_orphan')); - } - return; - } - const siblings = (sp.openings || []).flatMap((item: OpeningCfg) => { - if (!item.host || item.id === o.id) return []; - const sibling = resolvePartitionOpeningCompat( - item, model.partitions, NORM_W, this._cellCm, this._gridPitch, - ).resolved; - return sibling ? [sibling] : []; - }); - if (hostedOpeningIntervalsOverlap(resolution.resolved, siblings)) { - this._showToast(this._t('toast.opening_no_wall')); - return; - } - Object.assign(o, materializePartitionOpening(o, resolution.resolved, NORM_W)); - } else if (Number(this._serverCfg?.model_version || 0) >= 9) { - const host = resolveRoomOpeningHost(o, sp.wall_segments || []); - if (!host) { - this._showToast(this._t('toast.opening_no_wall')); - return; - } - o.host = host; - } else { - delete o.host; - } - if (d.type === 'passage') { - // The canonical passage record contains geometry only. Delete keys - // instead of writing null/false so imports and old broken records have - // one unambiguous representation after an explicit edit. - delete o.contact; - delete o.lock; - delete o.invert; - delete o.flip_h; - delete o.flip_v; - } else { - o.contact = d.contact || null; - o.lock = d.type === 'door' || d.type === 'gate' ? d.lock || null : null; - o.invert = d.invert || undefined; - o.flip_h = d.type !== 'gate' && d.flipH || undefined; - o.flip_v = d.flipV || undefined; - } - sp.openings = sp.openings || []; - const i = sp.openings.findIndex((x: OpeningCfg) => x.id === o.id); - if (i >= 0) sp.openings[i] = o; - else sp.openings.push(o); - this._commitPhysicalGeometry( - this._t(d.id ? 'history.edit_opening' : 'history.add_opening'), before, - ); - this._openingDialog = null; - this.requestUpdate(); + return this._editorRuntimeOrThrow()._saveOpening(); } private _deleteOpening(): void { - const d = this._openingDialog; - const sp = this._curSpaceCfg; - if (!d?.id || !sp?.openings) return; - const before = this._geometrySnapshot(); - sp.openings = sp.openings.filter((x: OpeningCfg) => x.id !== d.id); - this._commitPhysicalGeometry(this._t('history.delete_opening'), before); - this._openingDialog = null; - this.requestUpdate(); + return this._editorRuntimeOrThrow()._deleteOpening(); } private _rebindPartitionOpening = (): void => { - const d = this._openingDialog; - if (!d?.id) return; - const openingId = d.id; - this._activateMarkupTool('opening'); - if (this._tool !== 'opening') return; - // Tool activation clears every stale placement session. Publish the - // rebind identity only after that gate so the next click cannot degrade - // into creating a second opening. - this._openingRebindId = openingId; - this._openingPreset = { - type: d.type, - lengthCm: d.lengthCm, - flipH: d.flipH, - flipV: d.flipV, - revision: ++this._openingPresetRevision, - }; - this._openingDialog = null; - this._openingHoverCandidate = null; - }; + return this._editorRuntimeOrThrow()._rebindPartitionOpening(); + } /** Contact-sensor candidates: door/window/gate-like classes first, then the rest. */ private _contactCandidates(): { value: string; label: string }[] { - const out: [string, string, number][] = []; - for (const eid of Object.keys(this.hass.states)) { - if (!this._openingEntityAvailable(eid)) continue; - const dom = eid.split('.')[0]; - if (dom !== 'binary_sensor' && dom !== 'cover') continue; - const st = this.hass.states[eid]; - const dc = st?.attributes?.device_class || ''; - const doorish = ['door', 'window', 'opening', 'garage_door', 'garage'].includes(dc); - if (dom === 'cover' && !doorish) continue; - out.push([eid, st?.attributes?.friendly_name || eid, doorish ? 0 : 1]); - } - return out - .sort((a, b) => a[2] - b[2] || a[1].localeCompare(b[1])) - .map(([value, label]) => ({ value, label })); + return this._editorRuntimeOrThrow()._contactCandidates(); } private _lockCandidates(): { value: string; label: string }[] { - return Object.keys(this.hass.states) - .filter((eid) => eid.startsWith('lock.') && this._openingEntityAvailable(eid)) - .map((eid) => ({ value: eid, label: this.hass.states[eid]?.attributes?.friendly_name || eid })) - .sort((a, b) => a.label.localeCompare(b.label)); + return this._editorRuntimeOrThrow()._lockCandidates(); } private _toggleOpeningEntityPicker(kind: 'contact' | 'lock'): void { - const d = this._openingDialog; - if (!d) return; - const nextOpen = kind === 'contact' ? !d.contactOpen : !d.lockOpen; - this._openingDialog = { - ...d, - contactOpen: kind === 'contact' ? nextOpen : false, - lockOpen: kind === 'lock' ? nextOpen : false, - }; + return this._editorRuntimeOrThrow()._toggleOpeningEntityPicker(kind); } private _filterOpeningEntities(kind: 'contact' | 'lock', value: string): void { - const d = this._openingDialog; - if (!d) return; - this._openingDialog = kind === 'contact' - ? { ...d, contactFilter: value } - : { ...d, lockFilter: value }; + return this._editorRuntimeOrThrow()._filterOpeningEntities(kind, value); } private _selectOpeningEntity(kind: 'contact' | 'lock', value: string): void { - const d = this._openingDialog; - if (!d) return; - this._openingDialog = kind === 'contact' - ? { ...d, contact: value, contactOpen: false, contactFilter: '' } - : { ...d, lock: value, lockOpen: false, lockFilter: '' }; + return this._editorRuntimeOrThrow()._selectOpeningEntity(kind, value); } /** Merge: first click picks a room, second picks the room to merge it with. */ private _mergeClick(raw: number[]): void { - const space = this._spaceModel(); - if (!space) return; - const rooms = space.rooms; - const hit = [...rooms].reverse().find((r) => this._pointInRoom(raw, r)); - if (!hit?.id) return; - const hitId = hit.id; - if (!this._mergeSel || this._mergeSel === hitId) { - this._mergeSel = this._mergeSel === hitId ? null : hitId; // click again = deselect - return; - } - const a = rooms.find((r) => r.id === this._mergeSel); - const pa = a ? roomPoly(a) : null; - const pb = roomPoly(hit); - const merged = pa && pb ? mergeRooms(pa, pb) : null; - if (!merged) { - // only rooms sharing a wall collapse into one outline (see mergeRooms) - this._showToast(this._t('toast.merge_not_adjacent')); - this._mergeSel = null; - return; - } - this._mergeDialog = { aId: this._mergeSel, bId: hitId, poly: merged, pick: 'a' }; - this._mergeSel = null; + return this._editorRuntimeOrThrow()._mergeClick(raw); } private _commitMerge(): void { - const d = this._mergeDialog; - const sp = this._curSpaceCfg; - if (!d || !sp || !this._spaceModel()) return; - const before = this._geometrySnapshot(); - const H = this._spaceH; - const keepId = d.pick === 'a' ? d.aId : d.bId; - const dropId = d.pick === 'a' ? d.bId : d.aId; - const keep = sp.rooms.find((r: any) => r.id === keepId); - if (!keep) { - this._mergeDialog = null; - return; - } - // the kept room keeps its id, so its label position and devices stay put - keep.poly = d.poly.map((p) => [p[0] / NORM_W, p[1] / H]); - delete keep.x; delete keep.y; delete keep.w; delete keep.h; // a merged room is never a rect - sp.rooms = sp.rooms.filter((r: any) => r.id !== dropId); - const committed = this._commitPhysicalGeometry(this._t('history.merge_rooms'), before); - this._mergeDialog = null; - this._regSignature = ''; - this._maybeRebuildDevices(); - if (committed) this._showToast(this._t('toast.rooms_merged', { name: keep.name || '' })); + return this._editorRuntimeOrThrow()._commitMerge(); } /** Split: click the room, then two points on its walls. */ private _splitClick(raw: number[]): void { - const space = this._spaceModel(); - if (!space) return; - const rooms = space.rooms; - if (!this._splitSel) { - const hit = [...rooms].reverse().find((r) => this._pointInRoom(raw, r)); - if (!hit?.id) return; - this._splitSel = { roomId: hit.id, pts: [] }; - return; - } - const room = rooms.find((r) => r.id === this._splitSel!.roomId); - const poly = room ? roomPoly(room) : null; - if (!room || !poly) { - this._splitSel = null; - return; - } - // A split point lands on the room's nearest wall — the user aims at a wall, - // and rooms need not be grid-aligned (imported/legacy polygons), so snapping - // to the grid would miss the outline. The pull is capped: a click far from - // any wall (e.g. an accidental one in the middle of the room) is a miss and - // gets the toast, not a wall the user never meant. splitRoom() still rejects - // any cut that is not a clean wall-to-wall chord. - // …and it is still QUANTISED: the offset ALONG the wall moves in whole grid - // steps (docs/CANVAS.md §9), which on the axis-aligned, grid-drawn walls the - // editor itself makes IS a grid node. There is no free-position bypass. - const eps = this._gridPitch * 0.02; - const pull = this._gridPitch * 6; // ≈2.5% of the plan width — generous but intentional - const raw0 = closestPointOnBoundary(raw, poly); - const near = raw0 ? (snapPointAlongPoly(raw0, poly, this._gridPitch) || raw0) : raw0; - const wallPt = raw0 && near && Math.hypot(raw0[0] - raw[0], raw0[1] - raw[1]) <= pull ? near : null; - const onWall = !!wallPt && pointOnBoundary(wallPt, poly, eps); - const cur = this._splitSel.pts; - if (!cur.length) { - // the cut starts on a wall - if (!onWall) { - this._showToast(this._t('toast.split_pick_wall')); - return; - } - this._splitSel = { ...this._splitSel, pts: [wallPt!] }; - return; - } - if (!onWall) { - // an interior click adds an intermediate vertex of the cut path - const mid = this._snap(raw); - if (!ptInside(mid, poly, eps)) { - this._showToast(this._t('toast.split_pick_inside')); - return; - } - this._splitSel = { ...this._splitSel, pts: [...cur, mid] }; - return; - } - // a wall point finishes the cut - const parts = splitRoomPath(poly, [...cur, wallPt!], eps); - if (!parts) { - this._showToast(this._t('toast.split_bad_cut')); - return; - } - this._resetRoomDialogFields(); - // the bigger part stays the room it was — name, area and devices go with it - const [p1, p2] = parts; - const main = polygonArea(p1) >= polygonArea(p2) ? p1 : p2; - const fresh = main === p1 ? p2 : p1; - this._pendingSplit = { roomId: room.id!, mainPoly: main, newPoly: fresh }; - this._cursorPt = null; - this._nameSel = ''; - this._areaSel = ''; - this._roomDialog = true; + return this._editorRuntimeOrThrow()._splitClick(raw); } private get _contourClosed(): boolean { @@ -13385,39 +9044,8 @@ class HouseplanCard extends LitElement { } private _markupMove(ev: MouseEvent): void { - if (!this._markup) return; - if (this._tool === 'column') { - this._cursorPt = this._snap(this._svgPoint(ev)); - return; - } - if (this._tool === 'opening' || this._tool === 'wallthick') { - // hover preview: raw cursor point; snapping happens in the preview getters - this._cursorPt = this._svgPoint(ev); - return; - } - const architectural = this._tool === 'draw' && !this._contourClosed; - const cutting = this._tool === 'split' && !!this._splitSel?.pts?.length; - if (!architectural && !cutting) return; - const raw = this._svgPoint(ev); - if (architectural) { - const resolved = this._resolvePlanDrawPoint(raw, ev.shiftKey); - this._planSnapHover = { - contextKey: resolved.contextKey, - candidate: resolved.candidate, - conflicts: resolved.conflicts, - }; - this._syncPlanSnapConflictMarkers(resolved.conflicts); - // Before the first click there is no rubber-band to repaint. Updating the - // dedicated marker avoids walking the complete large-house Lit tree for - // every mouse move while click still resolves from the event coordinate. - if (!this._path.length) { - this._syncPlanSnapActiveMarker(resolved.candidate); - return; - } - this._cursorPt = resolved.point; - return; - } - this._cursorPt = this._snap(raw); + if (!this._markup || !this._editorRuntime) return; + return this._editorRuntime._markupMove(ev); } /** One resolved architectural candidate shared by hover and click. */ @@ -13448,659 +9076,58 @@ class HouseplanCard extends LitElement { /** Save a room with an optional HA-area binding. * An area supplies the fallback name; a room without one needs a name. */ private _saveRoom(): void { - if (!this._areaSel && !this._nameSel.trim()) return; - if (this._wallFaceBatch) { - this._decideWallFace(true); - return; - } - this._commitRoom(); + return this._editorRuntimeOrThrow()._saveRoom(); } private _decideWallFace(create: boolean): void { - const batch = this._wallFaceBatch; - if (!batch) return; - const candidate = batch.candidates[batch.index]; - if (!candidate) return; - if (!create && (candidate.existing || candidate.repair) && batch.candidates.length === 1) { - this._wallFaceBatch = null; - this._roomDialog = false; - this._nameSel = ''; - this._areaSel = ''; - this.requestUpdate(); - return; - } - const decision: WallFaceDecision = create ? { - candidate, - create: true, - name: this._nameSel.trim(), - area: this._areaSel || null, - settings: this._roomSettingsFromDialog() - ? JSON.parse(JSON.stringify(this._roomSettingsFromDialog())) : null, - } : { candidate, create: false }; - const decisions = [...batch.decisions, decision]; - const nextIndex = batch.index + 1; - if (nextIndex < batch.candidates.length) { - this._wallFaceBatch = { ...batch, decisions, index: nextIndex }; - this._nameSel = ''; - this._areaSel = ''; - this._resetRoomDialogFields(); - this.requestUpdate(); - return; - } - this._wallFaceBatch = { ...batch, decisions }; - this._applyWallFaceBatch(); + return this._editorRuntimeOrThrow()._decideWallFace(create); } private _wallSourceCmAt( point: number[], activePath: number[][], activeCms: number[], ): number { - const validCm = (value: unknown): number | null => - typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null; - const epsilon = this._gridPitch * 0.02; - const model = this._spaceModel(); - if (!model) return DRAW_WALL_DEFAULT_CM; - // Already-saved independent masonry is authoritative when the new chain - // traces it. Room-owned walls are preserved by the canonical wall helpers. - for (const segment of this._planSnapGeometrySnapshot().value.segments) { - if (segment.sourceKind === 'room' - || distToSegment(point, [segment.a[0], segment.a[1], segment.b[0], segment.b[1]]) > epsilon) continue; - if (segment.sourceKind === 'partition') { - return validCm(model.partitions.find((item) => item.id === segment.sourceId)?.cm) - ?? DRAW_WALL_DEFAULT_CM; - } - const separator = segment.sourceId.lastIndexOf(':'); - const draftId = separator >= 0 ? segment.sourceId.slice(0, separator) : segment.sourceId; - const index = separator >= 0 ? Number(segment.sourceId.slice(separator + 1)) : -1; - return validCm(model.room_drafts.find((item) => item.id === draftId)?.segments[index]?.cm) - ?? DRAW_WALL_DEFAULT_CM; - } - // Толщина отрезка активной цепочки решается тем же резолвером (#234): - // именно это значение подсвечивает инструмент «Толщина», и расхождение с - // записью здесь было тем способом, которым дефект и обнаружился. - const resolved = chainSegmentCms( - Math.max(0, activePath.length - 1), activeCms, - this._drawWallCm, DRAW_WALL_DEFAULT_CM, - ); - for (let i = 0; i + 1 < activePath.length; i++) { - const a = activePath[i], b = activePath[i + 1]; - if (distToSegment(point, [a[0], a[1], b[0], b[1]]) <= epsilon) { - return resolved[i] ?? DRAW_WALL_DEFAULT_CM; - } - } - return DRAW_WALL_DEFAULT_CM; + return this._editorRuntimeOrThrow()._wallSourceCmAt(point, activePath, activeCms); } private _activePathWithRepair( path: number[][], proposal: WallFaceRepairProposal | undefined, ): number[][] { - const result = path.map((point) => [...point]); - if (!proposal?.sourceKey.startsWith('active:')) return result; - const match = /:(\d+)$/.exec(proposal.sourceKey); - const index = match ? Number(match[1]) + (proposal.endpoint === 'b' ? 1 : 0) : -1; - if (index >= 0 && index < result.length) result[index] = [...proposal.to]; - return result; + return this._editorRuntimeOrThrow()._activePathWithRepair(path, proposal); } private _validateWallRepair( proposal: WallFaceRepairProposal, activePath: number[][], ): boolean { - const epsilon = this._gridPitch * 0.0002; - const sources = this._wallGraphSources(activePath); - const source = sources.find((item) => item.key === proposal.sourceKey); - const target = sources.find((item) => item.key === proposal.targetSourceKey); - if (!source || !target) return false; - const current = proposal.endpoint === 'a' ? source.a : source.b; - if (Math.hypot(current[0] - proposal.from[0], current[1] - proposal.from[1]) > epsilon) return false; - if (distToSegment(proposal.to, [target.a[0], target.a[1], target.b[0], target.b[1]]) > epsilon) { - return false; - } - if (repairMovesHostedPartition( - proposal, (this._curSpaceCfg as any)?.openings || [], - )) return false; - return true; + return this._editorRuntimeOrThrow()._validateWallRepair(proposal, activePath); } /** Apply one revalidated endpoint move inside the surrounding room transaction. */ private _applyWallRepair( proposal: WallFaceRepairProposal, batch: WallFaceBatch, ): boolean { - const sp = this._curSpaceCfg as any; - if (!sp || !this._validateWallRepair(proposal, batch.activePath)) return false; - const write = (point: number[]): void => { - point[0] = proposal.to[0] / NORM_W; - point[1] = proposal.to[1] / NORM_W; - }; - if (proposal.sourceKey.startsWith('active:')) { - const match = /:(\d+)$/.exec(proposal.sourceKey); - const index = match ? Number(match[1]) + (proposal.endpoint === 'b' ? 1 : 0) : -1; - if (index < 0 || index >= batch.activePath.length) return false; - batch.activePath[index] = [...proposal.to]; - const draft = (sp.room_drafts || []).find((item: any) => item.id === batch.activeDraftId); - if (draft?.points?.[index]) write(draft.points[index]); - return true; - } - if (!proposal.sourceKey.startsWith('static:')) return false; - const [kind, sourceId] = proposal.sourceKey.slice('static:'.length).split('|'); - if (kind === 'partition') { - const partition = (sp.partitions || []).find((item: PartitionCfg) => item.id === sourceId); - if (!partition) return false; - const renderA = [partition.a[0] * NORM_W, partition.a[1] * NORM_W]; - const renderB = [partition.b[0] * NORM_W, partition.b[1] * NORM_W]; - const point = Math.hypot(renderA[0] - proposal.from[0], renderA[1] - proposal.from[1]) - <= this._gridPitch * 0.0002 ? partition.a - : Math.hypot(renderB[0] - proposal.from[0], renderB[1] - proposal.from[1]) - <= this._gridPitch * 0.0002 ? partition.b : null; - if (!point) return false; - write(point); - return true; - } - if (kind === 'draft') { - const separator = sourceId.lastIndexOf(':'); - const draftId = separator >= 0 ? sourceId.slice(0, separator) : ''; - const segment = separator >= 0 ? Number(sourceId.slice(separator + 1)) : -1; - const draft = (sp.room_drafts || []).find((item: any) => item.id === draftId); - if (!draft?.points?.[segment] || !draft?.points?.[segment + 1]) return false; - const candidates = [segment, segment + 1]; - const index = candidates.find((candidate) => Math.hypot( - draft.points[candidate][0] * NORM_W - proposal.from[0], - draft.points[candidate][1] * NORM_W - proposal.from[1], - ) <= this._gridPitch * 0.0002); - if (index == null) return false; - write(draft.points[index]); - return true; - } - return false; + return this._editorRuntimeOrThrow()._applyWallRepair(proposal, batch); } /** Revalidate and apply every queued answer as one geometry transaction. */ private _applyWallFaceBatch(): void { - const batch = this._wallFaceBatch; - const sp = this._curSpaceCfg as any; - const model = this._spaceModel(); - if (!batch || !sp || !model) return; - const abort = (message: string, vars?: Record): void => { - this._showToast(this._t(message as any, vars)); - this._roomDialogCancel(); - }; - const accepted = batch.decisions.filter((decision) => decision.create); - const acceptedRings = accepted.map((decision) => decision.candidate.ring); - const existingRooms = model.rooms; - for (let i = 0; i < acceptedRings.length; i++) { - if (this._contourSelfIntersects(acceptedRings[i]) || polygonArea(acceptedRings[i]) <= 1e-6) { - abort('toast.contour_cannot_close'); - return; - } - for (let j = i + 1; j < acceptedRings.length; j++) { - if (roomsOverlap(acceptedRings[i], acceptedRings[j])) { - abort('toast.contour_cannot_close'); - return; - } - } - const ownSplit = accepted[i].candidate.split?.roomId; - const clash = existingRooms.find((room) => room.id !== ownSplit - && !!roomPoly(room) && roomsOverlap(acceptedRings[i], roomPoly(room)!)); - if (clash) { - abort('toast.room_overlap', { name: clash.name || '' }); - return; - } - } - if ((sp.rooms || []).length + accepted.length > MAX_ROOMS) { - abort('toast.physical_limit'); - return; - } - if (accepted.some((decision) => decision.candidate.split - && !sp.rooms.some((room: any) => room.id === decision.candidate.split!.roomId))) { - abort('toast.contour_cannot_close'); - return; - } - - const repairs = accepted - .map((decision) => decision.candidate.repair) - .filter((repair): repair is WallFaceRepairProposal => !!repair); - if (repairs.length > 1 || (repairs[0] && !this._validateWallRepair(repairs[0], batch.activePath))) { - abort('toast.wall_repair_changed'); - return; - } - const effectiveActivePath = this._activePathWithRepair(batch.activePath, repairs[0]); - - const epsilon = this._gridPitch * 0.0002; - const activeSourceCms = new Map(); - for (let i = 0; i < batch.activeCms.length; i++) - activeSourceCms.set(this._activeWallSourceKey(i), batch.activeCms[i]); - const partitions: Array<{ id?: string; a: number[]; b: number[]; cm: number }> = []; - if (!accepted.length) { - partitions.push(...wallChainSegments( - effectiveActivePath, - chainSegmentCms( - effectiveActivePath.length - 1, batch.activeCms, - this._drawWallCm, DRAW_WALL_DEFAULT_CM, - ), - )); - } else { - const consumed = new Set(accepted.flatMap((decision) => decision.candidate.atomKeys)); - const consumeAll = accepted.some((decision) => decision.candidate.consumeAllActive); - for (const atom of atomizeWallSegments(this._wallGraphSources(effectiveActivePath), epsilon)) { - const activeKey = atom.sourceKeys.find((key) => activeSourceCms.has(key)); - if (!activeKey || consumeAll || consumed.has(atom.key)) continue; - partitions.push({ a: atom.a, b: atom.b, cm: activeSourceCms.get(activeKey)! }); - } - } - if ((sp.partitions || []).length + partitions.length > MAX_PARTITIONS) { - abort('toast.physical_limit'); - return; - } - - // A face decision may split one persisted draft segment between a room - // contour and leftover partitions. Pick the single physical child that - // inherits the old ID by the same midpoint/old-a rule as the catalogue. - const roomLineage = accepted.map((decision) => - decision.candidate.ring.map(() => '')); - const activeDraft = batch.activeDraftId - ? (sp.room_drafts || []).find((draft: any) => draft.id === batch.activeDraftId) - : null; - const draftLineage = this._draftSegmentsForPath( - batch.activePath, activeDraft, batch.activeCms, - ); - const geometryKey = (a: readonly number[], b: readonly number[]): string => { - const ka = `${a[0].toFixed(9)},${a[1].toFixed(9)}`; - const kb = `${b[0].toFixed(9)},${b[1].toFixed(9)}`; - return ka <= kb ? `${ka}|${kb}` : `${kb}|${ka}`; - }; - for (let sourceIndex = 0; sourceIndex < draftLineage.length; sourceIndex++) { - const id = draftLineage[sourceIndex]?.id; - if (!id) continue; - const sourceA = batch.activePath[sourceIndex]; - const sourceB = batch.activePath[sourceIndex + 1]; - const sourceSpan = [sourceA[0], sourceA[1], sourceB[0], sourceB[1]]; - const candidates: Array<{ - kind: 'room' | 'partition'; owner: number; edge: number; a: number[]; b: number[]; - }> = []; - accepted.forEach((decision, owner) => decision.candidate.ring.forEach((a, edge) => { - const b = decision.candidate.ring[(edge + 1) % decision.candidate.ring.length]; - if (distToSegment(a, sourceSpan) <= epsilon && distToSegment(b, sourceSpan) <= epsilon) - candidates.push({ kind: 'room', owner, edge, a, b }); - })); - partitions.forEach((partition, owner) => { - if (distToSegment(partition.a, sourceSpan) <= epsilon - && distToSegment(partition.b, sourceSpan) <= epsilon) - candidates.push({ kind: 'partition', owner, edge: 0, a: partition.a, b: partition.b }); - }); - const midpoint = [(sourceA[0] + sourceB[0]) / 2, (sourceA[1] + sourceB[1]) / 2]; - candidates.sort((left, right) => { - const leftSpan = [left.a[0], left.a[1], left.b[0], left.b[1]]; - const rightSpan = [right.a[0], right.a[1], right.b[0], right.b[1]]; - return (distToSegment(midpoint, leftSpan) <= epsilon ? 0 : 1) - - (distToSegment(midpoint, rightSpan) <= epsilon ? 0 : 1) - || (distToSegment(sourceA, leftSpan) <= epsilon ? 0 : 1) - - (distToSegment(sourceA, rightSpan) <= epsilon ? 0 : 1) - || geometryKey(left.a, left.b).localeCompare(geometryKey(right.a, right.b)); - }); - const winner = candidates[0]; - if (!winner) continue; - const winnerKey = geometryKey(winner.a, winner.b); - for (const candidate of candidates) { - if (geometryKey(candidate.a, candidate.b) !== winnerKey) continue; - if (candidate.kind === 'room') roomLineage[candidate.owner][candidate.edge] = id; - else partitions[candidate.owner].id = id; - } - } - - const before = this._geometrySnapshot(); - if (repairs[0] && !this._applyWallRepair(repairs[0], batch)) { - abort('toast.wall_repair_changed'); - return; - } - const hasSplit = accepted.some((decision) => !!decision.candidate.split); - const splitWalls = hasSplit - ? materializeWallIntervals( - model.rooms, sp.walls, this._openCuts(), - this._wallKeyPitch, this._cellCm, this._gridPitch, NORM_W, - ) - : null; - const wallsBeforeSplit = hasSplit && Array.isArray(sp.walls) ? sp.walls : null; - for (const decision of accepted) { - const split = decision.candidate.split; - if (!split) continue; - const main = sp.rooms.find((room: any) => room.id === split.roomId); - if (!main) { - abort('toast.contour_cannot_close'); - return; - } - main.poly = split.mainPoly.map((point) => [point[0] / NORM_W, point[1] / this._spaceH]); - delete main.x; delete main.y; delete main.w; delete main.h; - } - - const seed = Date.now().toString(36); - const newRooms: Array<{ room: any; decision: WallFaceDecision }> = []; - accepted.forEach((decision, index) => { - const areaName = decision.area ? this.hass.areas[decision.area]?.name : ''; - const room = { - id: `r${seed}-${index}`, - name: decision.name || areaName || this._t('room.default_name'), - area: decision.area || null, - poly: decision.candidate.ring.map((point) => - [point[0] / NORM_W, point[1] / this._spaceH]), - ...(roomLineage[index].some(Boolean) ? { wall_ids: roomLineage[index] } : {}), - ...(decision.settings ? { settings: JSON.parse(JSON.stringify(decision.settings)) } : {}), - }; - sp.rooms.push(room); - newRooms.push({ room, decision }); - }); - - if (batch.activeDraftId && Array.isArray(sp.room_drafts)) { - sp.room_drafts = sp.room_drafts.filter((draft: any) => draft.id !== batch.activeDraftId); - if (!sp.room_drafts.length) delete sp.room_drafts; - } - if (partitions.length) { - sp.partitions ||= []; - partitions.forEach((partition, index) => sp.partitions.push({ - id: partition.id || `partition-${seed}-${index}`, - a: [partition.a[0] / NORM_W, partition.a[1] / NORM_W], - b: [partition.b[0] / NORM_W, partition.b[1] / NORM_W], - cm: partition.cm, - })); - } - - if (hasSplit) { - const next = this._normalizeWalls(splitWalls, this._openCuts()); - if (next.length) sp.walls = next; - else if (!wallsBeforeSplit?.length) delete sp.walls; - else sp.walls = wallsBeforeSplit; - } - if (newRooms.length) { - // Make the new model visible to the canonical wall helpers before they - // key intervals. This is still inside the one history/save transaction. - this._cfgEpoch++; - const openCuts = this._openCuts(); - const updatedModel = this._spaceModel(); - if (!updatedModel) return; - let walls = sp.walls; - for (const { room, decision } of newRooms) { - const ring = decision.candidate.ring; - const fallback = this._wallSourceCmAt( - ring[0], batch.activePath, batch.activeCms, - ); - walls = applyWallThicknessToNewRoom( - walls, updatedModel.rooms, room.id, fallback, - this._wallKeyPitch, openCuts, NORM_W, - ); - for (const interval of wallIntervals( - updatedModel.rooms, walls, openCuts, - this._wallKeyPitch, this._cellCm, this._gridPitch, NORM_W, - )) { - if (interval.roomId !== room.id || interval.kind !== 'outer') continue; - const midpoint = [ - (interval.a[0] + interval.b[0]) / 2, - (interval.a[1] + interval.b[1]) / 2, - ]; - walls = setWallThickness( - walls, interval.a, interval.b, - this._wallSourceCmAt(midpoint, batch.activePath, batch.activeCms), - this._wallKeyPitch, NORM_W, - ); - } - } - const normalized = this._normalizeWalls(walls, openCuts); - if (normalized.length) sp.walls = normalized; - else delete sp.walls; - } - - if (!this._commitPhysicalGeometry(this._t('history.wall_face_batch'), before)) return; - delete this._resumeDraftBySpace[this._space]; - this._path = []; - this._activeDraftId = null; - this._draftSegmentCms = []; - this._closingWallCm = null; - this._wallFaceBatch = null; - this._roomDialog = false; - this._nameSel = ''; - this._areaSel = ''; - this._regSignature = ''; - this._maybeRebuildDevices(); - // Match the ordinary room-save contract: newly area-bound devices are - // auto-added and their initial positions are pinned immediately, while a - // clean split leaves the surviving parent's existing layout untouched. - const boundAreas = new Set(accepted - .map((decision) => decision.area) - .filter((area): area is string => !!area)); - if (boundAreas.size) { - const next = { ...this._layout }; - for (const device of this._devices) { - if (!boundAreas.has(device.area || '') || device.space !== this._space) continue; - if (this._layout[device.id]) continue; - const position = this._defPos[device.id]; - if (!position) continue; - next[device.id] = { - s: this._space, x: position.x / NORM_W, y: position.y / NORM_W, - }; - this._dirtyPos.add(device.id); - } - this._layout = next; - this._persistLayout(); - } - this._showToast(this._t(accepted.length ? 'toast.wall_rooms_saved' : 'toast.wall_chain_saved', { - n: accepted.length, - })); + return this._editorRuntimeOrThrow()._applyWallFaceBatch(); } private _commitRoom(): void { - const sp = this._curSpaceCfg; - const space = this._spaceModel(); - if (!sp || !space) return; - const before = this._geometrySnapshot(); - const H = this._spaceH; - const wasSplit = !!this._pendingSplit; - // Split changes the two source-wall edges into shorter children. Preserve - // the effective old profile while the original outline still proves the - // full extent of legacy midpoint-only wall keys; after the mutation that - // information is irrecoverable and one child would normalise to 0 cm. - const splitWalls = wasSplit - ? materializeWallIntervals( - space.rooms, sp.walls, this._openCuts(), - this._wallKeyPitch, this._cellCm, this._gridPitch, NORM_W, - ) - : null; - const wallsBeforeSplit = wasSplit && Array.isArray(sp.walls) ? sp.walls : null; - let verts: number[][]; - if (this._pendingSplit) { - // apply the cut now: the bigger part keeps the original room, this dialog names the rest - const main = sp.rooms.find((r: any) => r.id === this._pendingSplit!.roomId); - if (!main) { - this._pendingSplit = null; - this._splitSel = null; - this._roomDialog = false; - return; - } - main.poly = this._pendingSplit.mainPoly.map((p) => [p[0] / NORM_W, p[1] / H]); - delete main.x; delete main.y; delete main.w; delete main.h; - verts = this._pendingSplit.newPoly; - } else { - if (!this._contourClosed) return; - verts = this._path.slice(0, -1); // without the duplicated closing vertex - } - const areaName = this._areaSel ? this.hass.areas[this._areaSel]?.name : ''; - const newRoom: any = { - id: 'r' + Date.now().toString(36), - name: this._nameSel || areaName || this._t('room.default_name'), - area: this._areaSel || null, - poly: verts.map((p) => [p[0] / NORM_W, p[1] / H]), - ...(this._roomSettingsFromDialog() ? { settings: this._roomSettingsFromDialog() } : {}), - }; - if (!wasSplit && this._activeDraftId) { - const activeDraft = Array.isArray((sp as any).room_drafts) - ? (sp as any).room_drafts.find((draft: any) => draft.id === this._activeDraftId) - : null; - const cms = chainSegmentCms( - verts.length, - [...this._draftSegmentCms, this._closingWallCm ?? undefined], - this._drawWallCm, DRAW_WALL_DEFAULT_CM, - ); - const lineage = this._draftSegmentsForPath(this._path, activeDraft, cms); - if (lineage.length === verts.length && lineage.every((segment) => !!segment.id)) { - newRoom.wall_ids = lineage.map((segment) => segment.id!); - } - } - sp.rooms.push(newRoom); - // Closing a saved draft promotes it into a room in the same transaction. - if (!wasSplit && this._activeDraftId && Array.isArray((sp as any).room_drafts)) { - (sp as any).room_drafts = (sp as any).room_drafts.filter((d: any) => d.id !== this._activeDraftId); - if (!(sp as any).room_drafts.length) delete (sp as any).room_drafts; - } - // A Split rewrites the parent outline and adds a child, so a span that used - // to sit between the parent and a neighbour may now belong to the child. - // Geometry first, then spans and the derived open_to — otherwise border - // trimming reads the new geometry while glow reads the old connectivity - // (AUD-159B6-02). - if (wasSplit) { - const next = this._normalizeWalls(splitWalls, this._openCuts()); - if (next.length) sp.walls = next; - else if (!wallsBeforeSplit?.length) delete sp.walls; - else sp.walls = wallsBeforeSplit; - } - // Draw-session wall thickness: apply to new edges only; keep neighbour cm - // on shared stretches. Split naming does not use the Draw field. - if (!wasSplit) { - const edgeCms = chainSegmentCms( - verts.length, - [...this._draftSegmentCms, this._closingWallCm ?? undefined], - this._drawWallCm, DRAW_WALL_DEFAULT_CM, - ); - const cm = edgeCms[0]; - if (cm != null) { - this._cfgEpoch++; // the new room must be in the model before keying - const openCuts = this._openCuts(); - const updatedSpace = this._spaceModel(); - if (!updatedSpace) return; - let next = applyWallThicknessToNewRoom( - sp.walls, updatedSpace.rooms, newRoom.id, cm, - this._wallKeyPitch, openCuts, NORM_W, - ); - // The room may have been drawn while the toolbar thickness changed. - // Shared stretches keep the already-existing physical wall; every - // outer atomic stretch receives the value of its source draft edge. - for (const iv of wallIntervals( - updatedSpace.rooms, next, openCuts, - this._wallKeyPitch, this._cellCm, this._gridPitch, NORM_W, - )) { - if (iv.roomId !== newRoom.id || iv.kind !== 'outer') continue; - const mid = [(iv.a[0] + iv.b[0]) / 2, (iv.a[1] + iv.b[1]) / 2]; - const source = verts.findIndex((a, i) => { - const b = verts[(i + 1) % verts.length]; - return distToSegment(mid, [a[0], a[1], b[0], b[1]]) <= this._gridPitch * 0.02; - }); - if (source >= 0) next = setWallThickness( - next, iv.a, iv.b, edgeCms[source], - this._wallKeyPitch, NORM_W, - ); - } - next = this._normalizeWalls(next, openCuts); - if (next.length) sp.walls = next; - else delete sp.walls; - } - } - const committed = this._commitPhysicalGeometry( - this._t(wasSplit ? 'history.split_room' : 'history.add_room'), before, - ); - this._path = []; - delete this._resumeDraftBySpace[this._space]; - this._activeDraftId = null; - this._draftSegmentCms = []; - this._closingWallCm = null; - this._pendingSplit = null; - this._splitSel = null; - const boundArea = this._areaSel; - this._areaSel = ''; - this._nameSel = ''; - this._roomDialog = false; - this._regSignature = ''; - this._maybeRebuildDevices(); - if (!committed) return; - // auto-add the area's device icons + PIN their positions in the layout, - // so that icons do not get reshuffled when the order in the HA registry changes. - let added = 0; - if (boundArea) { - const H2 = NORM_W; - const next = { ...this._layout }; - for (const d of this._devices) { - if (d.area !== boundArea || d.space !== this._space) continue; - added++; - if (this._layout[d.id]) continue; // placed manually — leave it alone - const dp = this._defPos[d.id]; - if (!dp) continue; - next[d.id] = { s: this._space, x: dp.x / NORM_W, y: dp.y / H2 }; - this._dirtyPos.add(d.id); - } - this._layout = next; - this._persistLayout(); - } - const roomsN = this._model.find((s) => s.id === this._space)?.rooms.length || 0; - this._showToast( - boundArea - ? this._t('toast.room_saved', { n: roomsN, added }) - : this._t('toast.room_saved_no_area', { n: roomsN }), - ); + return this._editorRuntimeOrThrow()._commitRoom(); } private _cancelPath(): void { - this._wallRepairDiagnostic = null; - if (this._activeDraftId) this._resumeDraftBySpace[this._space] = this._activeDraftId; - this._path = []; - this._activeDraftId = null; - this._draftSegmentCms = []; - this._closingWallCm = null; - this._clearPlanSnapHover(); - this._roomDialog = false; - this._pendingSplit = null; - this._wallFaceBatch = null; - this._splitSel = null; - this._mergeSel = null; - this._mergeDialog = null; - this._physicalSel = null; - this._physicalDrag = null; - this._physicalRotate = null; - this._clearOpeningPlacement(true); + return this._editorRuntimeOrThrow()._cancelPath(); } private _resumeLastDraft(): void { - const id = this._resumeDraftBySpace[this._space]; - if (!id) return; - const draft = this._spaceModel()?.room_drafts.find((d) => d.id === id); - if (!draft) { delete this._resumeDraftBySpace[this._space]; return; } - this._activeDraftId = id; - this._path = draft.points.map((p) => [...p]); - this._draftSegmentCms = this._adoptDraftCms( - this._path, draft.segments.map((s: any) => s.cm), draft.id, - ); - this._clearPlanSnapHover(); + return this._editorRuntimeOrThrow()._resumeLastDraft(); } /** Cancel a room flow without applying geometry; graph batches keep their terminal draft. */ private _roomDialogCancel(): void { - this._roomDialog = false; - if (this._roomEditId) { - this._roomEditId = null; - this._nameSel = ''; - this._areaSel = ''; - return; - } - if (this._wallFaceBatch) { - const batch = this._wallFaceBatch; - this._wallFaceBatch = null; - this._path = batch.activePath.map((point) => [...point]); - this._draftSegmentCms = [...batch.activeCms]; - this._activeDraftId = batch.activeDraftId; - this._nameSel = ''; - this._areaSel = ''; - this.requestUpdate(); - return; - } - if (this._pendingSplit) { - // nothing was applied yet — drop the cut entirely, the room stays whole - this._pendingSplit = null; - this._splitSel = null; - return; - } - this._undoPoint(); + return this._editorRuntimeOrThrow()._roomDialogCancel(); } /** HA areas not yet assigned to any room in the config. */ @@ -14118,449 +9145,65 @@ class HouseplanCard extends LitElement { // ================= DEVICE EDITOR (markers) ================= private _openDeviceInbox(): void { - this._deviceInboxReturn = null; - this._deviceInbox = this._deviceInbox || { - tab: 'on_plan', search: '', showEntities: false, onlyNew: false, limit: 100, - }; + return this._editorRuntimeOrThrow()._openDeviceInbox(); } private _closeMarkerDialog(): void { - this._markerDialog = null; - if (this._deviceInboxReturn) { - const restored = { ...this._deviceInboxReturn }; - this._deviceInbox = restored; - this._deviceInboxReturn = null; - if (restored.anchor) { - void this.updateComplete.then(() => requestAnimationFrame(() => { - const selector = `.device-inbox-row[data-binding="${CSS.escape(restored.anchor!)}"]`; - this.renderRoot.querySelector(selector)?.scrollIntoView({ block: 'nearest' }); - })); - } - } + return this._editorRuntimeOrThrow()._closeMarkerDialog(); } private _deviceInboxCandidates(showEntities: boolean) { - return bindingCandidates({ - hass: this._planHass, - devices: this._devices, - markers: this._markers, - showEntities, - labels: { - device: this._t('marker.sub_device'), - z2mGroup: this._t('marker.sub_z2m_group'), - group: this._t('marker.sub_group'), - helper: this._t('marker.sub_helper'), - entity: this._t('marker.sub_entity'), - }, - }); + return this._editorRuntimeOrThrow()._deviceInboxCandidates(showEntities); } private _deviceInboxRows(): DeviceInboxRow[] { - const dialog = this._deviceInbox || this._deviceInboxReturn; - const showEntities = !!dialog?.showEntities; - const key = [ - this._haRegistry.revision, this._cfgRev, this._cfgEpoch, this._regSignature, - this._newSyncKey, showEntities ? 1 : 0, this._showAll ? 1 : 0, - langOf(this.hass, this._config?.language), - ].join('|'); - if (this._deviceInboxMemo?.key === key) return this._deviceInboxMemo.rows; - const candidates = this._deviceInboxCandidates(showEntities); - const bindings = new Set(candidates.map((item) => item.value)); - for (const marker of this._markers) if (marker.binding && marker.binding !== 'virtual') bindings.add(marker.binding); - for (const device of this._devices) { - if (device.bindingKind && device.bindingKind !== 'virtual' && device.bindingRef) { - bindings.add(`${device.bindingKind}:${device.bindingRef}`); - } - } - const statuses = new Map(); - for (const binding of bindings) statuses.set(binding, this._bindingStatus(binding)); - - const areaMap = this._areaToSpace; - const areaNames: Record = {}; - for (const [id, area] of Object.entries(this.hass?.areas || {})) { - areaNames[id] = areaMap[id]?.room?.name || area?.name || id; - } - // A room may keep a valid HA area reference even when the current user has - // only a limited area-registry snapshot. Its plan-visible name is still - // authoritative for the catalog and search. - for (const [id, target] of Object.entries(areaMap)) { - areaNames[id] = target.room.name || areaNames[id] || id; - } - const spaceNames = Object.fromEntries(this._model.map((space) => [space.id, space.title])); - const spaceByArea = Object.fromEntries( - Object.entries(areaMap).map(([area, value]) => [area, value.space]), - ); - const integrationByBinding: Record = {}; - const devicePlatforms = new Map>(); - for (const [entityId, entity] of Object.entries(this._fullRegistryHass.entities || {})) { - const platform = String(entity?.platform || '').trim(); - if (platform) integrationByBinding[`entity:${entityId}`] = platform; - if (platform && entity?.device_id) { - const set = devicePlatforms.get(entity.device_id) || new Set(); - set.add(platform); - devicePlatforms.set(entity.device_id, set); - } - } - for (const [deviceId, platforms] of devicePlatforms) { - integrationByBinding[`device:${deviceId}`] = [...platforms].sort().join(', '); - } - const reasonByBinding: Record = {}; - for (const [deviceId, device] of Object.entries(this._fullRegistryHass.devices || {})) { - const binding = `device:${deviceId}`; - const platforms = devicePlatforms.get(deviceId) || new Set(); - const identifierDomain = Array.isArray(device?.identifiers?.[0]) - ? String(device.identifiers[0][0] || '') : ''; - const excluded = [identifierDomain, ...platforms].some((domain) => this._excluded.has(domain)); - if (device?.entry_type === 'service') reasonByBinding[binding] = 'service_entry'; - else if (excluded) reasonByBinding[binding] = 'excluded_integration'; - else if (device?.model === 'Group') reasonByBinding[binding] = 'grouped_light'; - else if (/scene/i.test(device?.model || '')) reasonByBinding[binding] = 'excluded_domain'; - else if (/bridge/i.test(`${device?.model || ''}${device?.name || ''}`) - || (identifierDomain === 'myheat' && device?.via_device_id)) { - reasonByBinding[binding] = 'represented_by_parent'; - } - } - const rows = buildDeviceInbox({ - devices: this._devices, - markers: this._markers, - candidates, - statuses, - newDeviceIds: this._newIds, - showHiddenOnPlan: this._showAll, - areaNames, spaceNames, spaceByArea, integrationByBinding, reasonByBinding, - }); - this._deviceInboxMemo = { key, rows }; - return rows; + return this._editorRuntimeOrThrow()._deviceInboxRows(); } private _deviceForInboxRow(row: DeviceInboxRow): DevItem | null { - const runtime = row.deviceId ? this._devices.find((item) => item.id === row.deviceId) : null; - if (runtime) return runtime; - const marker = row.markerId ? this._markers.find((item) => item.id === row.markerId) : null; - if (!marker || marker.removed) return null; - return { - id: marker.id, - name: row.name, - model: row.model, - area: row.areaId, - space: row.spaceId || this._space, - hidden: marker.hidden === true || row.status.kind === 'ha_disabled', - userHidden: marker.hidden === true, - bindingStatus: row.status, - icon: row.icon, - entities: row.status.kind === 'active' ? row.status.enabledEntityIds : [], - allEntities: row.status.allEntityIds, - primary: row.status.kind === 'active' ? row.status.enabledEntityIds[0] : undefined, - marker, - bindingKind: row.kind, - bindingRef: row.binding.slice(row.binding.indexOf(':') + 1), - pdfs: marker.pdfs || [], - }; + return this._editorRuntimeOrThrow()._deviceForInboxRow(row); } private _openInboxMarker(row: DeviceInboxRow, add = false): void { - const snapshot = this._deviceInbox; - if (!snapshot) return; - this._deviceInboxReturn = { ...snapshot, anchor: row.key }; - this._deviceInbox = null; - if (!add) { - const device = this._deviceForInboxRow(row); - if (device) this._openMarkerDialog(device); - else this._closeMarkerDialog(); - return; - } - this._openMarkerDialog(); - if (!this._markerDialog) { - this._closeMarkerDialog(); - return; - } - this._markerDialog = { - ...this._markerDialog, - bindingMode: 'ha', binding: row.binding, bindingOpen: false, - showEntities: row.kind === 'entity', name: '', - }; + return this._editorRuntimeOrThrow()._openInboxMarker(row, add); } private async _setInboxHidden(row: DeviceInboxRow, hidden: boolean): Promise { - const dialog = this._deviceInbox; - const cfg = this._serverCfg; - if (!dialog || !cfg || dialog.busy || row.status.kind !== 'active') return; - const previous = cfg.markers || []; - const live = previous.find((marker) => !marker.removed && marker.binding === row.binding); - if (!hidden && !live) return; - const id = live?.id || markerIdForBinding( - row.binding, row.markerId, () => `m_${Date.now().toString(36)}`, - ); - const next: Marker = live - ? { ...live, hidden } - : { id, binding: row.binding, hidden: true }; - cfg.markers = [ - ...previous.filter((marker) => marker.id !== id - && (marker.binding !== row.binding || marker.removed === true)), - next, - ]; - this._deviceInbox = { ...dialog, busy: row.key, anchor: row.key }; - try { - await this._saveConfigNow(); - this._regSignature = ''; - this._deviceInboxMemo = null; - this._maybeRebuildDevices(); - if (this._deviceInbox) this._deviceInbox = { ...this._deviceInbox, busy: undefined }; - this._showToast(this._t('device_inbox.saved' as any)); - } catch (error: any) { - if (this._serverCfg === cfg) cfg.markers = previous; - if (this._deviceInbox) this._deviceInbox = { ...this._deviceInbox, busy: undefined }; - this._showToast(this._t('toast.error', { err: this._errText(error) })); - } + return this._editorRuntimeOrThrow()._setInboxHidden(row, hidden); } private _findInboxDevice(row: DeviceInboxRow): void { - if (!row.canFind) return; - const device = this._deviceForInboxRow(row); - if (!device) return; - this._deviceInbox = null; - if (device.space && device.space !== this._space) { - if (!this._commitSpace(device.space)) return; - this._restoreZoom(); - } - const focus = () => { - const current = this._devices.find((item) => item.id === device.id) || device; - const point = this._pos(current); - this._applyView(this._zoom, point.x, point.y); - this._selId = current.id; - this.requestUpdate(); - window.setTimeout(() => { - if (this._selId === current.id) { - this._selId = null; - this.requestUpdate(); - } - }, 1500); - }; - requestAnimationFrame(() => requestAnimationFrame(focus)); + return this._editorRuntimeOrThrow()._findInboxDevice(row); } private _deviceInboxTabKey(event: KeyboardEvent): void { - if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return; - const dialog = this._deviceInbox; - if (!dialog) return; - const tabs: DeviceInboxCategory[] = ['on_plan', 'available', 'hidden', 'readd']; - const offset = event.key === 'ArrowRight' ? 1 : -1; - const index = (tabs.indexOf(dialog.tab) + offset + tabs.length) % tabs.length; - this._deviceInbox = { ...dialog, tab: tabs[index], limit: 100, onlyNew: false }; - event.preventDefault(); + return this._editorRuntimeOrThrow()._deviceInboxTabKey(event); } private _openMarkerDialog(d?: DevItem): void { - // A global-camera snapshot belongs to one explicit expansion only; never - // carry it across device-dialog sessions. - this._vacAllCamerasFor = null; - this._vacAllCameraCache = null; - if (d) this._ackNewDevice(d.id); - if (!this._norm) { - this._showToast(this._t('toast.marker_needs_server')); + if (!this._editorRuntime) { + void this._ensureEditorRuntime().then((ready) => { + if (ready) this._openMarkerDialog(d); + }); return; } - if (d) { - const marker = d.marker; - const hasIsLight = Object.prototype.hasOwnProperty.call(marker || {}, 'is_light'); - const hasLightEntity = Object.prototype.hasOwnProperty.call(marker || {}, 'light_entity'); - const hasGlowColor = Object.prototype.hasOwnProperty.call(marker || {}, 'glow_color'); - const hasValueBadge = Object.prototype.hasOwnProperty.call(marker || {}, 'value_badge'); - const hasTapAction = Object.prototype.hasOwnProperty.call(marker || {}, 'tap_action'); - const hasToggleEntity = Object.prototype.hasOwnProperty.call(marker || {}, 'toggle_entity'); - const glowOverride = normalizeGlowColorOverride(marker?.glow_color); - const currentBadge = this._devicePresentation(d, true).valueBadge; - const badgeCandidates = valueBadgeCandidates(this._planHass, d, this._devices); - const recommendedBadge = recommendedValueBadgeSource(this._planHass, d, badgeCandidates); - this._markerDialog = { - devId: d.id, - name: d.name, - binding: d.bindingKind === 'virtual' ? 'virtual' : d.bindingKind + ':' + d.bindingRef, - bindingMode: d.bindingKind === 'virtual' ? 'virtual' : 'ha', - bindingOpen: false, - // a marker bound to an ENTITY of a device only shows up with the box on - showEntities: d.bindingKind === 'entity' && !!this._fullRegistryHass.entities[d.bindingRef || '']?.device_id, - bindingFilter: '', - icon: d.marker?.icon || '', - autoIcon: d.icon || '', - display: normalizeDeviceDisplay(d.marker?.display), - rippleColor: safeStoredColor(d.marker?.ripple_color, ''), - rippleSize: Number(d.marker?.ripple_size) > 0 ? Number(d.marker!.ripple_size) : 1.5, - size: Number(d.marker?.size) > 0 ? Number(d.marker!.size) : 1, - angle: Number(d.marker?.angle) || 0, - tapAction: projectedTapAction(d.marker?.tap_action, d.primary?.split('.')[0]), - tapActionTouched: false, - originalHasTapAction: hasTapAction, - originalTapAction: d.marker?.tap_action, - tapHintAnnouncement: '', - toggleEntity: marker?.toggle_entity || '', - toggleEntityTouched: false, - originalHasToggleEntity: hasToggleEntity, - originalToggleEntity: marker?.toggle_entity, - tapTarget: d.marker?.tap_target || '', - tapConfirm: d.marker?.tap_confirm === true, - runFilter: '', - // Keep unknown, temporarily inactive and duplicate external targets - // byte-for-byte across Open → Save. Runtime uses the filtered - // effective projection; a legacy self-reference is not a light source. - controls: persistedExternalControls(d.marker?.binding, d.marker?.controls, d.entities), - controlsFilter: '', - lightRole: marker?.is_light === true ? 'always' : marker?.is_light === false ? 'never' : 'auto', - lightRoleTouched: false, - originalHasIsLight: hasIsLight, - originalIsLight: marker?.is_light, - lightEntity: marker?.light_entity || '', - lightEntityTouched: false, - originalHasLightEntity: hasLightEntity, - originalLightEntity: marker?.light_entity, - glowMode: glowOverride?.bri != null ? 'fixed' : glowOverride ? 'color' : 'auto', - glowColor: glowOverride?.c || this._fillColors.glow_light.c, - glowBrightness: Math.max(1, Math.round((glowOverride?.bri ?? 1) * 100)), - glowColorDrafted: !!glowOverride, - glowBrightnessDrafted: glowOverride?.bri != null, - glowTouched: false, - originalHasGlowColor: hasGlowColor && (!!glowOverride || marker?.glow_color === null), - originalGlowColor: glowOverride || (marker?.glow_color === null ? null : undefined), - valueBadgeEnabled: hasValueBadge ? marker?.value_badge?.enabled === true : !!currentBadge, - valueBadgeSource: marker?.value_badge?.source || currentBadge?.source || recommendedBadge, - valueBadgePosition: marker?.value_badge?.position || currentBadge?.position || 'right', - valueBadgeTouched: false, - originalHasValueBadge: hasValueBadge, - originalValueBadge: marker?.value_badge, - useClimateTemp: d.marker?.use_climate_temp === true, - glowRadius: Number(d.marker?.glow_radius_cm) > 0 - ? String(this._imperial - ? Math.round((Number(d.marker!.glow_radius_cm) / 30.48) * 10) / 10 - : Math.round(Number(d.marker!.glow_radius_cm)) / 100) - : '', - model: d.model || '', - link: d.link || '', - description: d.description || '', - pdfs: [...(d.pdfs || [])], - room: d.marker?.room_id - ? d.space + '#@' + d.marker.room_id - : d.space && d.area ? d.space + '#' + d.area : '', - hideFromPlan: d.marker?.hidden === true, - busy: false, - }; - } else { - this._markerDialog = { - name: '', binding: 'virtual', bindingMode: 'virtual', bindingOpen: false, - showEntities: false, bindingFilter: '', icon: '', autoIcon: '', - display: 'badge', rippleColor: '', rippleSize: 1.5, size: 1, angle: 0, - tapAction: 'info', tapActionTouched: false, - originalHasTapAction: false, originalTapAction: undefined, tapHintAnnouncement: '', - toggleEntity: '', toggleEntityTouched: false, - originalHasToggleEntity: false, originalToggleEntity: undefined, - tapTarget: '', tapConfirm: false, runFilter: '', - controls: [], controlsFilter: '', - lightRole: 'auto', lightRoleTouched: false, - originalHasIsLight: false, originalIsLight: undefined, - lightEntity: '', lightEntityTouched: false, - originalHasLightEntity: false, originalLightEntity: undefined, - glowMode: 'auto', glowColor: this._fillColors.glow_light.c, glowBrightness: 100, - glowColorDrafted: false, glowBrightnessDrafted: false, glowTouched: false, - originalHasGlowColor: false, originalGlowColor: undefined, - valueBadgeEnabled: false, valueBadgeSource: null, valueBadgePosition: 'right', - valueBadgeTouched: false, originalHasValueBadge: false, originalValueBadge: undefined, - useClimateTemp: false, glowRadius: '', model: '', - link: '', description: '', pdfs: [], room: '', hideFromPlan: false, busy: false, - uploadId: 'up_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6), - }; - } + return this._editorRuntime._openMarkerDialog(d); } /** Runnable targets for the 'run' tap action: automations, scripts, scenes. */ private _runCandidates(): { value: string; label: string; sub: string }[] { - const out: { value: string; label: string; sub: string }[] = []; - for (const dom of RUN_TARGET_DOMAINS) { - for (const [eid, st] of Object.entries(this.hass.states)) { - if (!eid.startsWith(dom + '.')) continue; - if (!this._planEntityAvailable(eid)) continue; - out.push({ - value: eid, - label: st?.attributes?.friendly_name || eid, - sub: this._t(('run.' + dom) as any), - }); - } - } - return out.sort((a, b) => a.sub.localeCompare(b.sub) || a.label.localeCompare(b.label)); + return this._editorRuntimeOrThrow()._runCandidates(); } /** Binding candidates: HA devices + group/helper entities, minus the ones already placed. */ private _bindingCandidates(): { value: string; label: string; sub: string }[] { - const list = bindingCandidates({ - hass: this._planHass, - devices: this._devices, - markers: this._markers, - showEntities: !!this._markerDialog?.showEntities, - currentBinding: this._markerDialog?.binding, - currentDeviceId: this._markerDialog?.devId, - labels: { - device: this._t('marker.sub_device'), - z2mGroup: this._t('marker.sub_z2m_group'), - group: this._t('marker.sub_group'), - helper: this._t('marker.sub_helper'), - entity: this._t('marker.sub_entity'), - }, - }); - const f = (this._markerDialog?.bindingFilter || '').toLowerCase().trim(); - const filtered = f - ? list.filter((o) => (o.label + ' ' + o.sub + ' ' + o.value).toLowerCase().includes(f)) - : list; - filtered.sort((a, b) => a.label.localeCompare(b.label)); - return filtered.slice(0, 200); + return this._editorRuntimeOrThrow()._bindingCandidates(); } /** A closed outline may intentionally remain a set of independent walls. */ private _keepClosedAsPartitions = (): void => { - if (this._wallFaceBatch) { - this._decideWallFace(false); - return; - } - if (!this._contourClosed || this._pendingSplit || !this._curSpaceCfg) return; - const sp = this._curSpaceCfg as any; - const verts = this._path.slice(0, -1); - if ((sp.partitions || []).length + verts.length > MAX_PARTITIONS) { - this._showToast(this._t('toast.physical_limit')); - return; - } - const before = this._geometrySnapshot(); - // Замкнутый контур: отрезков столько же, сколько вершин, и последний — - // закрывающий. Его известное значение подаётся резолверу как запись, всё - // остальное решает единое правило (#234). - const cms = chainSegmentCms( - verts.length, - [...this._draftSegmentCms, this._closingWallCm ?? undefined], - this._drawWallCm, DRAW_WALL_DEFAULT_CM, - ); - sp.partitions ||= []; - const seed = Date.now().toString(36); - const activeDraft = this._activeDraftId - ? (sp.room_drafts || []).find((draft: any) => draft.id === this._activeDraftId) - : null; - const lineage = this._draftSegmentsForPath(this._path, activeDraft, cms); - for (let i = 0; i < verts.length; i++) { - const a = verts[i], b = verts[(i + 1) % verts.length]; - sp.partitions.push({ - id: lineage[i]?.id || `partition-${seed}-${i}`, - a: [a[0] / NORM_W, a[1] / NORM_W], - b: [b[0] / NORM_W, b[1] / NORM_W], - cm: cms[i], - }); - } - if (this._activeDraftId && Array.isArray(sp.room_drafts)) { - sp.room_drafts = sp.room_drafts.filter((d: any) => d.id !== this._activeDraftId); - if (!sp.room_drafts.length) delete sp.room_drafts; - } - this._commitPhysicalGeometry(this._t('history.contour_to_partitions'), before); - this._roomDialog = false; - this._path = []; - delete this._resumeDraftBySpace[this._space]; - this._activeDraftId = null; - this._draftSegmentCms = []; - this._closingWallCm = null; - }; + return this._editorRuntimeOrThrow()._keepClosedAsPartitions(); + } /** All independent physical bodies in render units. Physics intentionally * does not depend on show_borders. */ @@ -14597,7 +9240,7 @@ class HouseplanCard extends LitElement { } const roomKey = room.id || `#${space.rooms.indexOf(room)}`; const key = `${space.id}|${this._cfgEpoch}|${roomKey}`; - if (!this._resize.preview) { + if (!this._resize?.preview) { const cached = lruRead(this._cleanFloorCache, key); if (cached.hit) return cached.value; } @@ -14615,7 +9258,7 @@ class HouseplanCard extends LitElement { path: geom ? polyclipPathD(geom) : '', area: geom ? geometryArea(geom) : geometryArea([[[...floor, floor[0]]]]), }; - if (!this._resize.preview) { + if (!this._resize?.preview) { lruWrite(this._cleanFloorCache, key, result, 600); } return result; @@ -14709,13 +9352,7 @@ class HouseplanCard extends LitElement { } private _backupErrorText(e: any): string { - const code = e?.code ?? e?.error; - if (typeof code === 'string') { - const key = `backup.error.${code}`; - const translated = this._t(key as I18nKey); - if (translated !== key) return translated; - } - return this._errText(e); + return this._editorRuntimeOrThrow()._backupErrorText(e); } /** @@ -14723,382 +9360,38 @@ class HouseplanCard extends LitElement { * limit breaks the connection on large PDFs. */ private async _pickMarkerFiles(ev: Event): Promise { - const input = ev.target as HTMLInputElement; - const files = input.files ? [...input.files] : []; - input.value = ''; - if (!files.length || !this._markerDialog) return; - const mid = this._markerDialog.uploadId || this._markerDialog.devId || 'new'; - const uploaded: PdfRef[] = []; - for (const file of files) { - try { - const fd = new FormData(); - fd.append('marker_id', mid); - fd.append('file', file, file.name); - // fetchWithAuth refreshes a stale access_token itself; the fallback is the raw token - const resp: Response = this.hass?.fetchWithAuth - ? await this.hass.fetchWithAuth('/api/houseplan/upload', { method: 'POST', body: fd }) - : await fetch('/api/houseplan/upload', { - method: 'POST', - body: fd, - headers: this.hass?.auth?.data?.access_token - ? { authorization: `Bearer ${this.hass.auth.data.access_token}` } - : {}, - }); - const json = await resp.json().catch(() => ({})); - if (!resp.ok || json.error) { - const map: Record = { - too_large: this._t('err.too_large', { mb: json.max_mb || 50 }), - bad_ext: this._t('err.bad_ext'), - unauthorized: this._t('err.unauthorized'), - }; - throw new Error(map[json.error] || json.error || 'HTTP ' + resp.status); - } - uploaded.push({ name: json.name || file.name, url: json.url }); - } catch (e: any) { - this._showToast(this._t('toast.file_failed', { name: file.name, err: this._errText(e) })); - } - } - // the dialog might have closed during the upload — add only if it is still open - if (uploaded.length && this._markerDialog) { - this._markerDialog = { ...this._markerDialog, pdfs: [...this._markerDialog.pdfs, ...uploaded] }; - this._showToast(this._t('toast.files_attached', { n: uploaded.length })); - } + return this._editorRuntimeOrThrow()._pickMarkerFiles(ev); } private _removeMarkerPdf(url: string): void { - if (!this._markerDialog) return; - this._markerDialog = { - ...this._markerDialog, - pdfs: this._markerDialog.pdfs.filter((p) => p.url !== url), - }; + return this._editorRuntimeOrThrow()._removeMarkerPdf(url); } /** Persist only deliberate role/appearance edits; untouched legacy absence stays absent. */ private _markerLightFields(d: NonNullable): Partial { - const fields: Partial = {}; - if (!d.lightRoleTouched) { - if (d.originalHasIsLight) fields.is_light = d.originalIsLight ?? null; - } else if (d.lightRole === 'always') fields.is_light = true; - else if (d.lightRole === 'never') fields.is_light = false; - - if (!d.lightEntityTouched) { - if (d.originalHasLightEntity) fields.light_entity = d.originalLightEntity ?? null; - } else if (d.lightEntity) { - fields.light_entity = d.lightEntity; - } - - if (!d.glowTouched) { - if (d.originalHasGlowColor) fields.glow_color = d.originalGlowColor ?? null; - } else if (d.glowMode !== 'auto') { - const glow: NonNullable = { - c: safeStoredColor(d.glowColor, this._fillColors.glow_light.c), - }; - if (d.glowMode === 'fixed') { - glow.bri = Math.max(0.01, Math.min(1, Math.round(d.glowBrightness) / 100)); - } - fields.glow_color = glow; - } - return fields; + return this._editorRuntimeOrThrow()._markerLightFields(d); } /** Preserve absent/default and legacy cover until the user edits the select. */ private _markerTapActionFields( d: NonNullable, ): Pick | Record { - if (!d.tapActionTouched) { - return d.originalHasTapAction ? { tap_action: d.originalTapAction ?? null } : {}; - } - return { tap_action: d.tapAction || null }; + return this._editorRuntimeOrThrow()._markerTapActionFields(d); } /** Preserve absence and stale/future literals until this exact selector is edited. */ private _markerToggleEntityFields( d: NonNullable, ): Pick | Record { - return toggleEntityWriteFields({ - touched: d.toggleEntityTouched, - originalHas: d.originalHasToggleEntity, - original: d.originalToggleEntity, - value: d.toggleEntity, - }); + return this._editorRuntimeOrThrow()._markerToggleEntityFields(d); } private async _saveMarker(): Promise { - const dlg = this._markerDialog; - if (!dlg || dlg.busy) return; - const effectiveTapAction = this._effectiveMarkerTapAction(dlg); - if (dlg.bindingMode === 'ha' && (!dlg.binding || dlg.binding === 'virtual')) return; - if (dlg.binding === 'virtual' && !dlg.name.trim()) { - this._showToast(this._t('toast.virtual_name_required')); - return; - } - if (effectiveTapAction === 'run' && !dlg.tapTarget) { - this._showToast(this._t('toast.run_target_required')); - return; - } - if (dlg.valueBadgeTouched && dlg.valueBadgeEnabled && !dlg.valueBadgeSource) { - this._showToast(this._t('toast.value_badge_source_required')); - return; - } - if (dlg.bindingMode === 'ha') { - const status = this._bindingStatus(dlg.binding); - const previous = dlg.devId - ? this._markers.find((marker) => marker.id === dlg.devId) : null; - const replacingWithInactive = !previous || previous.binding !== dlg.binding; - if (status.kind !== 'active' && replacingWithInactive) { - this._showToast(this._t(status.kind === 'ha_disabled' - ? 'toast.ha_disabled_add' : 'toast.ha_binding_unverified')); - return; - } - // Saving metadata for an already-disabled binding is allowed, but a - // blocked Show attempt may never erase an older explicit user hidden. - if (status.kind === 'ha_disabled' && previous?.hidden === true && !dlg.hideFromPlan) { - this._markerDialog = { ...dlg, hideFromPlan: true }; - this._showToast(this._t(status.reason === 'entity' - ? 'toast.ha_disabled_show_entity' : 'toast.ha_disabled_show_device')); - return; - } - } - const cfg = this._serverCfg; - if (!cfg) return; - const markers = cfg.markers || []; - const roomRef = parseRoomRef(dlg.room); - let space: string | null = roomRef?.space || null; - const area: string | null = roomRef?.area || null; - const roomId: string | null = roomRef?.roomId || null; - const id = markerIdForBinding(dlg.binding, dlg.devId, () => 'v_' + Date.now().toString(36)); - const oldId = dlg.devId; - const prevDev = oldId ? this._devices.find((x) => x.id === oldId) : null; - const explicitSpaceId = roomRef?.space || prevDev?.space || null; - const targetSpaceModel = explicitSpaceId - ? this._spaceModelById(explicitSpaceId) - : this._spaceModel(); - if (!targetSpaceModel) return; - const targetSpaceId = targetSpaceModel.id; - if (dlg.binding === 'virtual' && !space) space = targetSpaceId; - this._markerDialog = { ...dlg, busy: true }; - try { - const replacedRemovedIds = dlg.binding === 'virtual' - ? [] - : markers - .filter((m) => m.removed && m.binding === dlg.binding) - .map((m) => m.id); - const replacingRemoved = replacedRemovedIds.length > 0; - const controls = persistedExternalControls( - dlg.binding, dlg.controls, this._bindingEntities(dlg.binding), - ); - // the vacuum block is edited live outside the dialog transaction — - // the rebuild below must carry it over, not erase it - const prevVac = markers.find((m0: Marker) => m0.id === id || m0.id === oldId)?.vacuum || null; - const marker: Marker = { - id, - vacuum: prevVac, - binding: dlg.binding, - name: dlg.name.trim() || null, - icon: dlg.icon || null, - display: dlg.display !== 'badge' ? dlg.display : null, - ripple_color: dlg.display === 'icon_ripple' && dlg.rippleColor ? dlg.rippleColor : null, - ripple_size: dlg.display === 'icon_ripple' && dlg.rippleSize !== 1.5 ? dlg.rippleSize : null, - size: dlg.size !== 1 ? dlg.size : null, - angle: dlg.angle ? dlg.angle : null, - ...this._markerTapActionFields(dlg), - ...this._markerToggleEntityFields(dlg), - tap_target: effectiveTapAction === 'run' ? dlg.tapTarget || null : null, - tap_confirm: dlg.tapConfirm ? true : null, - controls: controls.length ? controls : null, - // pdfs may be rewritten below when rebinding changes the marker id - ...this._markerLightFields(dlg), - ...this._markerValueBadgeFields(dlg), - use_climate_temp: dlg.useClimateTemp ? true : null, - glow_radius_cm: (() => { - const v = strictNumber(dlg.glowRadius); - if (v == null || v <= 0) return null; - return Math.round(this._imperial ? v * 30.48 : v * 100); - })(), - model: dlg.model.trim() || null, - link: dlg.link.trim() || null, - description: dlg.description.trim() || null, - pdfs: dlg.pdfs, - // Explicit false, not absence: a marker of any kind is what tells the - // seeder "the user decided" — unticking must not invite a re-hide - // (docs/FILTERING.md). - hidden: dlg.hideFromPlan ? true : false, - }; - // save the room choice (always for virtual ones; for bound ones — if chosen) - if (dlg.binding === 'virtual' || dlg.room) { - marker.space = space; - marker.area = area; - marker.room_id = roomId; - } - // the room changed → move the icon to its center - const prevRoomId = prevDev?.marker?.room_id ?? null; - const roomChanged = !!dlg.room && prevDev != null - && (prevDev.space !== space || prevDev.area !== area || prevRoomId !== roomId); - // Rebinding changes the marker id, so the uploaded files must follow. - // Order matters (review CR-2): COPY first, save the config, and only then - // delete the old folder. If the save is rejected, the old urls in the - // stored config still resolve — the files never left. A failed copy - // leaves the urls untouched and tells the user (review CR-3). - let cleanupOldFiles = false; - // a new icon uploaded into its own staging folder; an edited one into its - // own id. Either way the files move to the final id here. - const fileSrc = dlg.uploadId || oldId; - if (fileSrc && fileSrc !== id && marker.pdfs?.length) { - try { - const res: any = await this.hass.callWS({ - type: 'houseplan/files/migrate', from_id: fileSrc, to_id: id, - }); - const mapping = res?.mapping || {}; - marker.pdfs = migratePdfUrls(marker.pdfs, fileSrc, id, mapping); - cleanupOldFiles = Object.keys(mapping).length > 0; - } catch (e: any) { - this._showToast(this._t('toast.files_migrate_failed', { err: this._errText(e) })); - } - } - // Rebinding changes source identity. Rewrite every marker:* edge in the - // same config transaction before replacing the marker itself. - cfg.markers = markers; - if (oldId && oldId !== id) cfg.markers = rewriteMarkerControlReferences(cfg.markers, oldId, id); - if (oldId && oldId !== id && marker.value_badge?.source?.kind === 'derived_marker_state' - && marker.value_badge.source.ref === `marker:${oldId}`) { - marker.value_badge.source = { kind: 'derived_marker_state', ref: `marker:${id}` }; - } - // remove the previous marker (by the old id and by the new id) - cfg.markers = cfg.markers.filter( - (m) => m.id !== id && m.id !== oldId - && (marker.binding === 'virtual' || m.binding !== marker.binding), - ); - cfg.markers.push(marker); - // Position rule (owner's decision, v1.33.4): editing an existing icon — - // rebinding it to another HA device/entity or to another room — must NOT - // move it. Its current position (saved or the ephemeral auto one) is - // migrated to the new marker id. Only two cases still center the icon: - // a truly NEW icon, and a move to a room in a DIFFERENT space (keeping - // the old coordinates there would be meaningless). - // Write POINT-WISE (layout/update), not the whole layout — a full layout/set - // overwrites positions changed in other windows (the v1.4.4 incident). - let newPos: { s: string; x: number; y: number } | null = null; - const prevRec = oldId ? this._layout[oldId] : null; - const prevPos = prevRec - ? { s: prevRec.s || prevDev?.space || this._space, x: prevRec.x, y: prevRec.y } - : oldId && prevDev && this._defPos[oldId] - ? this._normPos(prevDev.space, this._defPos[oldId].x, this._defPos[oldId].y) - : null; - if (!replacingRemoved && prevPos && prevPos.s === targetSpaceId) { - // stays in place; pin it under the (possibly new) id - if (id !== oldId || !this._layout[id] || roomChanged) { - newPos = { s: prevPos.s, x: prevPos.x, y: prevPos.y }; - this._layout = { ...this._layout, [id]: newPos }; - } - } else if (replacingRemoved || !this._layout[id] || roomChanged) { - let cx = targetSpaceModel.vb[0] + targetSpaceModel.vb[2] / 2; - let cy = targetSpaceModel.vb[1] + targetSpaceModel.vb[3] / 2; - const room = roomId - ? targetSpaceModel.rooms.find((r) => r.id === roomId) - : area - ? targetSpaceModel.rooms.find((r) => r.area === area) - : undefined; - if (room) [cx, cy] = this._roomCenter(room); - newPos = this._normPos(targetSpaceId, cx, cy); - this._layout = { ...this._layout, [id]: newPos }; - } - await this._saveConfigNow(); - if (newPos) { - const pos = canonicalizePosition(newPos); - this._layout = { ...this._layout, [id]: pos }; - this._noteLayoutRev(await this.hass.callWS({ - type: 'houseplan/layout/update', device_id: id, pos, - })); - } - const obsoleteIds = new Set(replacedRemovedIds); - if (oldId && oldId !== id) obsoleteIds.add(oldId); - obsoleteIds.delete(id); - for (const obsoleteId of obsoleteIds) { - // Rebinding or replacing a legacy tombstone changed the icon id. - delete this._layout[obsoleteId]; - await this.hass.callWS({ type: 'houseplan/layout/delete', device_id: obsoleteId }) - .then((r: any) => this._noteLayoutRev(r)).catch(() => undefined); - } - // the config is committed — now it is safe to drop the old folder - if (cleanupOldFiles && fileSrc) { - await this.hass - .callWS({ type: 'houseplan/files/cleanup', marker_id: fileSrc }) - .catch(() => undefined); // leftovers are harmless; broken links are not - } - this._closeMarkerDialog(); - this._regSignature = ''; - this._maybeRebuildDevices(); - this._showToast(this._t('toast.marker_saved')); - } catch (e: any) { - // audit L3: the dialog may have been closed (Esc) while the save was - // in flight — spreading null yields a truthy husk and the renderer - // then crashes, blanking the whole card. The toast below is the - // only remaining signal, so it must still fire. - if (this._markerDialog) this._markerDialog = { ...this._markerDialog, busy: false }; - this._showToast(this._t('toast.error', { err: this._errText(e) })); - } + return this._editorRuntimeOrThrow()._saveMarker(); } private async _deleteMarker(): Promise { - const dlg = this._markerDialog; - if (!dlg || dlg.busy || !dlg.devId) return; - const d = dlg.devId ? this._devices.find((x) => x.id === dlg.devId) : null; - const persisted = this._markers.find((marker) => marker.id === dlg.devId); - if (!d && !persisted) return; - const label = dlg.name || this._t('device.fallback'); - if (!confirm(this._t('confirm.remove_marker', { name: label }))) return; - const cfg = this._serverCfg!; - cfg.markers = cfg.markers || []; - const previousMarkers = cfg.markers; - const targetId = d?.id || persisted!.id; - const binding = d - ? d.bindingKind === 'virtual' ? 'virtual' - : d.bindingKind && d.bindingRef ? `${d.bindingKind}:${d.bindingRef}` : '' - : persisted!.binding; - if (!binding) return; - const deletion = deletePlanMarkerRecords( - cfg.markers, targetId, binding, binding === 'virtual', - ); - cfg.markers = removeMarkerControlReferences(deletion.markers, deletion.cleanupIds); - const cleanupIds = deletion.cleanupIds; - this._markerDialog = { ...dlg, busy: true }; - try { - await this._saveConfigNow(); - // Housekeeping follows the durable config write. Every call is - // idempotent and best-effort: the device is already deleted even if an - // old integration cannot yet serve one of the cleanup commands. - for (const id of cleanupIds) { - delete this._layout[id]; - delete this._defPos[id]; - this._dirtyPos.delete(id); - this._sentPos.delete(id); - const activity = this._activityRt.get(id); - if (activity) clearTimeout(activity.timer); - this._activityRt.delete(id); - this._vacRt.delete(id); - delete this._vacSrvTrails[id]; - await this.hass.callWS({ type: 'houseplan/layout/delete', device_id: id }) - .then((r: any) => this._noteLayoutRev(r)).catch(() => undefined); - await this.hass.callWS({ type: 'houseplan/files/cleanup', marker_id: id }) - .catch(() => undefined); - await this.hass.callWS({ type: 'houseplan/trail/delete', marker_id: id }) - .catch(() => undefined); - } - if (this._deviceInboxReturn) { - this._deviceInboxReturn = { ...this._deviceInboxReturn, tab: 'readd', anchor: binding }; - } - this._closeMarkerDialog(); - if (this._infoCard?.id === targetId) this._closeInfoCard(); - if (this._selId === targetId) this._selId = null; - if (this._drag?.id === targetId) this._drag = null; - this._regSignature = ''; - this._maybeRebuildDevices(); - this._showToast(this._t('toast.marker_removed')); - } catch (e: any) { - if (this._serverCfg === cfg) cfg.markers = previousMarkers; - if (this._markerDialog) this._markerDialog = { ...this._markerDialog, busy: false }; - this._showToast(this._t('toast.error', { err: this._errText(e) })); - } + return this._editorRuntimeOrThrow()._deleteMarker(); } private _normPos(space: string, x: number, y: number): { s: string; x: number; y: number } { @@ -15107,104 +9400,47 @@ class HouseplanCard extends LitElement { // ================= SPACE MANAGEMENT ================= + private _spaceDialogUsesOnboardingRuntime(mode: 'edit' | 'create'): boolean { + return mode === 'create' && !this._editorRuntime && ( + !!this._onboardingRuntime + || + (this._serverCfg?.spaces.length || 0) === 0 + || this._importTotal > 0 + || this._importQueue.length > 0 + ); + } + + private _spaceRuntimeOrThrow(): import('./houseplan-onboarding-runtime').HouseplanOnboardingRuntime { + if (!this._onboardingRuntime) throw new Error('Houseplan onboarding runtime is not loaded'); + return this._onboardingRuntime; + } + private _openSpaceDialog(mode: 'edit' | 'create', spaceId?: string): void { - if (!this._serverStorage || !this._serverCfg) { - this._showToast(this._t('toast.integration_missing')); + if (this._spaceDialogUsesOnboardingRuntime(mode)) { + if (this._onboardingRuntime) { + this._onboardingRuntime._openSpaceDialog(mode, spaceId); + return; + } + void this._ensureOnboardingRuntime().then((ready) => { + if (ready) this._spaceRuntimeOrThrow()._openSpaceDialog(mode, spaceId); + }); return; } - if (mode === 'edit') { - const sp = this._serverCfg!.spaces.find((x: any) => x.id === spaceId); - if (!sp) return; - const disp = spaceDisplayOf(sp); - const storedCustom = sp.settings?.custom_fill && typeof sp.settings.custom_fill === 'object' - ? customFillOf(sp.settings.custom_fill) : null; - // Space-level None is no longer a separate UI choice. Project it to the - // same custom colour at zero opacity, so merely opening/saving settings - // cannot change either the visible floor or the Glow darkness. - const dialogCustom = disp.fill === 'none' - ? { ...(storedCustom || DEFAULT_CUSTOM_FILL), a: 0 } - : storedCustom; - this._spaceDialog = { - mode, spaceId, title: sp.title, planUrl: sp.plan_url || null, planFile: null, - source: sp.plan_url ? 'file' : 'draw', - showBorders: disp.showBorders, showNames: disp.showNames, - zeroWallStyle: zeroWallStyleOf(sp), - displayTouched: true, - hideDecor: disp.hideDecor, hideOpenings: disp.hideOpenings, - roomColor: disp.color, roomOpacity: disp.opacity, - // `none` is a legacy space token. The current editor represents its - // exact appearance as a transparent user colour. - fillMode: disp.fill === 'none' ? 'custom' : disp.fill, - customFill: dialogCustom, - glowEnabled: disp.glow, - bgColor: disp.bgColor, - bgMode: sp.settings?.bg_mode === 'static' || sp.settings?.bg_mode === 'daynight' ? sp.settings.bg_mode : null, - northDeg: northDegOf({}, sp.settings), - sunRays: typeof sp.settings?.sun_rays === 'boolean' ? sp.settings.sun_rays : null, - tempMin: disp.tempMin, tempMax: disp.tempMax, - showLqi: disp.showLqi ?? this._config?.show_signal ?? true, - cardFontScale: disp.cardFontScale, - labelTemp: disp.labelTemp, labelHum: disp.labelHum, - labelLqi: disp.labelLqi, labelLight: disp.labelLight, - cellCm: Number(sp.cell_cm) > 0 ? Number(sp.cell_cm) : 5, - cellCmInput: gridCellFieldValue( - Number(sp.cell_cm) > 0 ? Number(sp.cell_cm) : 5, this._imperial, - ), - cellCmTouched: false, - busy: false, - }; - } else { - this._spaceDialog = { - mode, title: '', planUrl: null, planFile: null, - ...initialSpaceDisplayDraft(), - hideDecor: false, hideOpenings: false, zeroWallStyle: 'dashed', - roomColor: DEFAULT_ROOM_COLOR, roomOpacity: DEFAULT_ROOM_OPACITY, fillMode: 'custom', - // `custom` replaces the old `none` choice in the editor, but creating - // a space must preserve the old no-floor visual by default. The user - // can make this colour visible explicitly with the opacity control. - customFill: { ...DEFAULT_CUSTOM_FILL, a: 0 }, - glowEnabled: true, - bgColor: null, - bgMode: 'daynight', northDeg: null, sunRays: null, - tempMin: DEFAULT_TEMP_MIN, tempMax: DEFAULT_TEMP_MAX, - showLqi: this._config?.show_signal ?? true, - cardFontScale: 1, - labelTemp: false, labelHum: false, labelLqi: false, labelLight: false, - cellCm: newSpaceCellCm(this._imperial), - cellCmInput: gridCellFieldValue(newSpaceCellCm(this._imperial), this._imperial), - cellCmTouched: false, - busy: false, - }; + if (!this._editorRuntime) { + void this._ensureEditorRuntime().then((ready) => { + if (ready) this._openSpaceDialog(mode, spaceId); + }); + return; } + return this._editorRuntime._openSpaceDialog(mode, spaceId); } /** Background file selection: read base64 and determine the aspect ratio. */ private async _pickPlanFile(ev: Event): Promise { - const input = ev.target as HTMLInputElement; - const file = input.files?.[0]; - if (!file || !this._spaceDialog) return; - const extMap: Record = { - 'image/svg+xml': 'svg', 'image/png': 'png', 'image/jpeg': 'jpg', 'image/webp': 'webp', - }; - const ext = extMap[file.type] || (file.name.toLowerCase().endsWith('.svg') ? 'svg' : ''); - if (!ext) { - this._showToast(this._t('toast.plan_formats')); - return; + if (this._onboardingRuntime && this._spaceDialogUsesOnboardingRuntime('create')) { + return this._onboardingRuntime._pickPlanFile(ev); } - const buf = new Uint8Array(await file.arrayBuffer()); - let bin = ''; - for (let i = 0; i < buf.length; i += 32768) bin += String.fromCharCode(...buf.subarray(i, i + 32768)); - const b64 = btoa(bin); - // aspect ratio: render into an Image - const url = URL.createObjectURL(file); - const aspect = await new Promise((res) => { - const img = new Image(); - img.onload = () => res(img.naturalWidth && img.naturalHeight ? img.naturalWidth / img.naturalHeight : 1.414); - img.onerror = () => res(1.414); - img.src = url; - }); - URL.revokeObjectURL(url); - this._spaceDialog = { ...this._spaceDialog, planFile: { ext, b64, aspect, name: file.name } }; + return this._editorRuntimeOrThrow()._pickPlanFile(ev); } /** @@ -15214,36 +9450,21 @@ class HouseplanCard extends LitElement { * it out of the list. It is also the only way one is ever deleted. */ private _toggleServerPlans = async (): Promise => { - const d = this._spaceDialog; - if (!d) return; - if (d.pickSaved) { - this._spaceDialog = { ...d, pickSaved: false }; - return; + if (this._onboardingRuntime && this._spaceDialogUsesOnboardingRuntime('create')) { + return this._onboardingRuntime._toggleServerPlans(); } - this._spaceDialog = { ...d, pickSaved: true, savedBusy: true }; - try { - const r: any = await this.hass.callWS({ type: 'houseplan/plans/list' }); - const cur = this._spaceDialog; - if (cur) this._spaceDialog = { ...cur, saved: r?.plans || [], savedBusy: false }; - } catch (e: any) { - const cur = this._spaceDialog; - if (cur) this._spaceDialog = { ...cur, saved: [], savedBusy: false }; - this._showToast(this._t('toast.plans_list_failed', { err: this._errText(e) })); - } - }; + return this._editorRuntimeOrThrow()._toggleServerPlans(); + } /** The in-flight proportions read for the last picked saved plan. Save * awaits it rather than shipping whatever was there before (HP-1490-04). */ private _aspectJob: Promise | null = null; private _useServerPlan(url: string): void { - const d = this._spaceDialog; - if (!d) return; - // Attach immediately — the click should not wait for anything. The OLD - // file's proportions go right away: they describe the previous image, and - // a Save racing the read must get "unknown", never "the wrong shape". - this._spaceDialog = { ...d, planUrl: url, planFile: null, pickSaved: false, savedAspect: undefined }; - this._aspectJob = this._readPlanAspect(url); + if (this._onboardingRuntime && this._spaceDialogUsesOnboardingRuntime('create')) { + return this._onboardingRuntime._useServerPlan(url); + } + return this._editorRuntimeOrThrow()._useServerPlan(url); } /** @@ -15257,284 +9478,35 @@ class HouseplanCard extends LitElement { * answer would reshape whatever the user opened next. */ private async _readPlanAspect(url: string): Promise { - for (let i = 0; i < 40; i++) { // ~6 s, then give up quietly - const src = this._display(url); - if (src) { - const ratio = await new Promise((res) => { - const img = new Image(); - img.onload = () => res(img.naturalWidth && img.naturalHeight - ? img.naturalWidth / img.naturalHeight : 0); - img.onerror = () => res(0); - img.src = src; - }); - const cur = this._spaceDialog; - if (cur && cur.planUrl === url && Number.isFinite(ratio) && ratio > 0) { - this._spaceDialog = { ...cur, savedAspect: ratio }; - return ratio; - } - return 0; - } - await new Promise((r) => setTimeout(r, 150)); - if (this._spaceDialog?.planUrl !== url) return 0; // the user moved on + if (this._onboardingRuntime && this._spaceDialogUsesOnboardingRuntime('create')) { + return this._onboardingRuntime._readPlanAspect(url); } - return 0; + return this._editorRuntimeOrThrow()._readPlanAspect(url); } private async _deleteServerPlan(name: string): Promise { - if (!confirm(this._t('confirm.delete_plan', { name }))) return; - try { - await this.hass.callWS({ type: 'houseplan/plans/delete', name }); - const d = this._spaceDialog; - if (d?.saved) this._spaceDialog = { ...d, saved: d.saved.filter((p) => p.name !== name) }; - } catch (e: any) { - this._showToast(this._t('toast.plan_delete_failed', { err: this._errText(e) })); + if (this._onboardingRuntime && this._spaceDialogUsesOnboardingRuntime('create')) { + return this._onboardingRuntime._deleteServerPlan(name); } + return this._editorRuntimeOrThrow()._deleteServerPlan(name); } private _renderServerPlans(d: NonNullable): TemplateResult { - if (d.savedBusy) return html`
${this._t('space.loading')}
`; - const list = d.saved || []; - if (!list.length) return html`
${this._t('space.no_saved')}
`; - const kb = (n: number) => (n >= 1048576 ? (n / 1048576).toFixed(1) + ' MB' : Math.round(n / 1024) + ' KB'); - return html`
- ${list.map((p) => html` -
- -
- ${p.name} - ${kb(p.size)}${p.used_by.length - ? ' · ' + this._t('space.used_by', { list: p.used_by.join(', ') }) - : ''} -
- - -
`)} -
`; + if (this._onboardingRuntime && this._spaceDialogUsesOnboardingRuntime(d.mode)) { + return this._onboardingRuntime._renderServerPlans(d); + } + return this._editorRuntimeOrThrow()._renderServerPlans(d); } private async _saveSpaceDialog(): Promise { - const d = this._spaceDialog; - if (!d || d.busy || !d.title.trim()) return; - if (d.source === 'file' && !d.planFile && !d.planUrl) { - this._showToast(this._t('toast.plan_required')); - return; - } - const wasFirst = d.mode === 'create' && (this._serverCfg?.spaces.length || 0) === 0; - this._spaceDialog = { ...d, busy: true }; - try { - const spaceId = d.mode === 'create' ? 's' + Date.now().toString(36) : d.spaceId!; - - /* Upload BEFORE touching the config, and never hold a reference to a - config object across an await. `houseplan/config/get` runs on every - `houseplan_config_updated` event and REPLACES `_serverCfg`; a space - object captured before the upload is then detached, so plan_url, - aspect and settings were written into an orphan and the save shipped - the untouched config. Symptom: the file lands on disk, the plan never - appears, and re-saving does not help (owner's install, 2026-07-27). */ - let uploaded: { url: string; aspect: number } | null = null; - if (d.source === 'file' && d.planFile) { - const resp = await this.hass.callWS({ - type: 'houseplan/plan/set', space_id: spaceId, ext: d.planFile.ext, data: d.planFile.b64, - }); - uploaded = { url: resp.url, aspect: d.planFile.aspect }; - } - - // A plan picked from the server list reads its proportions from the - // image, and Save used to outrun that read: the snapshot still carried - // the PREVIOUS file's ratio and a wide plan came out at the old shape - // for good (HP-1490-04). Wait for the read — it is bounded (~6 s) and - // the dialog is already busy. Unknown stays unknown, never the old - // value: a square fallback is honest, an inherited ratio is not. - let pickedAspect: number | null = d.savedAspect || null; - if (!uploaded && d.source === 'file' && d.planUrl && !pickedAspect && this._aspectJob) { - pickedAspect = (await this._aspectJob) || null; - } - - // from here on: no awaits until the save, so `sp` cannot be orphaned - const cfg = this._serverCfg!; - let sp: any; - if (d.mode === 'create') { - sp = createEmptySpaceConfig(spaceId, d.title.trim()); - cfg.spaces.push(sp); - } else { - sp = cfg.spaces.find((x: any) => x.id === spaceId); - if (!sp) throw new Error('space ' + spaceId + ' is gone from the config'); - sp.title = d.title.trim(); - } - if (uploaded) { - sp.plan_url = uploaded.url; - // the image's own proportions, so it can be centred before it loads - sp.plan_aspect = uploaded.aspect; - } else if (d.source === 'file' && d.planUrl && d.planUrl !== sp.plan_url) { - // picked from the server list: no upload, just a reference — and the - // previous image's proportions never survive the switch - sp.plan_url = d.planUrl; - sp.plan_aspect = pickedAspect; - } - // switching an existing space to "draw" detaches its background image - // (the uploaded file stays on disk; only the reference is cleared). - // Its transform goes with it — there is nothing left for plan_x/plan_y/ - // plan_scale to describe, and a stale one would silently apply to the - // NEXT picture uploaded here (docs/BACKDROP.md §1). - if (d.source === 'draw') { - sp.plan_url = null; sp.plan_aspect = null; - delete sp.plan_x; delete sp.plan_y; delete sp.plan_scale; - delete sp.plan_scale_x; delete sp.plan_scale_y; delete sp.plan_angle; - } - // Persist exactly what the dialog showed. Source defaults are projected - // visibly while editing the create state, never hidden here at Save. - sp.settings = { - ...(sp.settings || {}), - show_borders: d.showBorders, - show_names: d.showNames, - // written only when ON: a plan that never hid anything stores nothing - hide_decor: d.hideDecor || undefined, - hide_openings: d.hideOpenings || undefined, - room_color: d.roomColor, - room_opacity: d.roomOpacity, - bg_color: d.bgColor || undefined, // empty = inherit the general setting - bg_mode: d.bgMode || undefined, // sun on the plan (docs/SUN.md); empty = inherit - north_deg: d.northDeg ?? undefined, - sun_rays: d.sunRays ?? undefined, - fill_mode: d.fillMode, - custom_fill: d.customFill || undefined, - // Always materialize the independent value. In particular, editing a - // legacy fill_mode:'glow' space preserves its appearance atomically - // while replacing the old token with the selected data fill. - glow_enabled: d.glowEnabled, - temp_min: Number.isFinite(d.tempMin) ? Math.min(d.tempMin, d.tempMax) : DEFAULT_TEMP_MIN, - temp_max: Number.isFinite(d.tempMax) ? Math.max(d.tempMin, d.tempMax) : DEFAULT_TEMP_MAX, - show_lqi: d.showLqi, - card_font_scale: d.cardFontScale !== 1 ? d.cardFontScale : undefined, - label_temp: d.labelTemp, - label_hum: d.labelHum, - label_lqi: d.labelLqi, - label_light: d.labelLight, - }; - // This is a geometry policy of the space, not a visual room setting. - sp.zero_wall_style = d.zeroWallStyle; - sp.cell_cm = Number.isFinite(d.cellCm) && d.cellCm > 0 - ? Math.max(CELL_CM_MIN, Math.min(CELL_CM_MAX, d.cellCm)) : 5; - // Nothing to clean up from here: the backend collects the superseded - // file inside the same locked transaction that accepted this config - // (review R3-1). A cleanup driven from the client could not be ordered - // against another client's commit and deleted its freshly saved plan. - await this._saveConfigNow(); - this._spaceDialog = null; - if (d.mode === 'create') this._commitSpace(sp.id); - this._regSignature = ''; - this._maybeRebuildDevices(); - if (this._importQueue.length) { - // floors-import wizard: proceed to the next floor - this._openNextImport(); - } else if (wasFirst || this._importTotal > 0) { - // guide the user onward: straight into room markup mode - this._importTotal = 0; - this._commitSpace(this._serverCfg!.spaces[0]?.id || this._space); - this._setMode('plan'); - this._tool = 'draw'; - this._path = []; - this._cursorPt = null; - this._primeDrawWallField(); - this._showToast(this._t(wasFirst && !this._importTotal ? 'toast.space_added_onboard' : 'import.done')); - } else { - this._showToast(d.mode === 'create' ? this._t('toast.space_added') : this._t('toast.space_saved')); - // A freshly created space is empty — View has nothing to show. Open - // the plan editor (draw tool) so the user can mark rooms immediately, - // same as the first-space onboard path above. - if (d.mode === 'create') { - if (this._mode !== 'plan') this._setMode('plan'); - else { - this._tool = 'draw'; - this._path = []; - this._cursorPt = null; - this._primeDrawWallField(); - this._saveNav(); - } - } - } - } catch (e: any) { - // The dialog edits the reactive config optimistically before config/set. - // A rejected create used to leave that unsaved space on screen; the next - // delete then flushed the same malformed write and repeated its error. - // Re-adopt server truth for every failed space transaction. Conflict is - // already reloaded by _saveConfigNow; do not issue the same read twice. - // A lost response is safe too: if the server did commit, config/get - // returns it. - if (e?.code !== 'conflict') await this._reloadConfigOnly(true); - // audit L3: the dialog may have been closed (Esc) while the save was - // in flight — spreading null yields a truthy husk and the renderer - // then crashes, blanking the whole card. The toast below is the - // only remaining signal, so it must still fire. - if (this._spaceDialog) this._spaceDialog = { ...this._spaceDialog, busy: false }; - this._showToast(this._t('toast.error', { err: this._errText(e) })); + if (this._onboardingRuntime && this._spaceDialogUsesOnboardingRuntime('create')) { + return this._onboardingRuntime._saveSpaceDialog(); } + return this._editorRuntimeOrThrow()._saveSpaceDialog(); } private async _deleteSpace(): Promise { - const d = this._spaceDialog; - if (!d || d.mode !== 'edit') return; - const serverCfg = this._serverCfg; - if (!serverCfg) return; - const sp = serverCfg.spaces.find((x: any) => x.id === d.spaceId); - const dependencies = collectSpaceMarkerDependencies( - serverCfg, this._layout || {}, d.spaceId || '', - ); - const deletingLastSpace = serverCfg.spaces.length === 1 - && serverCfg.spaces[0]?.id === d.spaceId; - if (dependencies.count && !deletingLastSpace) { - this._spaceDialog = { ...d, deleteBlockers: dependencies.count }; - return; - } - if (!confirm(this._t('confirm.delete_space', { title: sp.title }))) return; - this._spaceDialog = { ...d, deleteBlockers: 0, busy: true }; - try { - if (this._saveConfigDebounced.pending()) this._saveConfigDebounced.flush(); - if (this._persistLayout.pending()) this._persistLayout.flush(); - await this._writeChain; - const response: any = await this.hass.callWS({ - type: 'houseplan/space/delete', - space_id: d.spaceId, - expected_config_rev: this._cfgRev, - expected_layout_rev: this._layoutRev, - }); - const [configResponse, layoutResponse] = await Promise.all([ - this.hass.callWS({ type: 'houseplan/config/get' }), - this.hass.callWS({ type: 'houseplan/layout/get' }), - ]); - this._adoptStructuralResponses(configResponse, layoutResponse); - this._cfgRev = response?.config_rev ?? this._cfgRev; - this._layoutRev = response?.layout_rev ?? this._layoutRev; - this._spaceDialog = null; - if (this._space === d.spaceId) this._commitSpace(this._serverCfg!.spaces[0]?.id || ''); - this._regSignature = ''; - this._maybeRebuildDevices(); - this._showToast(this._t('toast.space_deleted')); - } catch (e: any) { - if (e?.code === 'conflict' || e?.code === 'space_in_use') { - await Promise.all([this._reloadConfigOnly(true), this._reloadLayoutOnly()]); - } - const refreshedConfig = this._serverCfg; - if (this._spaceDialog && refreshedConfig) { - const refreshed = collectSpaceMarkerDependencies( - refreshedConfig, this._layout || {}, d.spaceId || '', - ); - const stillLastSpace = refreshedConfig.spaces.length === 1 - && refreshedConfig.spaces[0]?.id === d.spaceId; - this._spaceDialog = { - ...this._spaceDialog, - busy: false, - deleteBlockers: stillLastSpace ? 0 : refreshed.count, - }; - } - this._showToast(this._t('toast.delete_failed', { err: this._errText(e) })); - } + return this._editorRuntimeOrThrow()._deleteSpace(); } /** Immediate config save with a revision bump (no debounce). @@ -15543,101 +9515,32 @@ class HouseplanCard extends LitElement { user's retry starts from the fresh config instead of hitting the same conflict again. */ private async _saveConfigNow(): Promise { - this._cfgEpoch++; - try { - // same queue as the debounced writer: a dialog saving while a background - // write is still out must not race it into a self-inflicted conflict - await this._writeConfig(); - } catch (e: any) { - if (e?.physicalGeometryRolledBack) await this._reloadRejectedPhysicalWrite(); - else if (e?.code === 'conflict') await this._reloadConfigOnly(); - throw e; - } + return this._editorRuntimeOrThrow()._saveConfigNow(); } // ================= FLOORS IMPORT WIZARD ================= private _startImport(): void { - const dlg = this._importDialog; - if (!dlg) return; - const titles = dlg.floors.filter((f) => f.checked).map((f) => f.name); - this._importDialog = null; - if (!titles.length) { - this._openSpaceDialog('create'); - return; - } - this._importQueue = titles; - this._importTotal = titles.length; - this._openNextImport(); + if (this._onboardingRuntime) return this._onboardingRuntime._startImport(); + return this._editorRuntimeOrThrow()._startImport(); } /** Open the space dialog for the next queued floor (title prefilled, plan required). */ private _openNextImport(): void { - const title = this._importQueue.shift(); - if (title === undefined) return; - this._spaceDialog = { - mode: 'create', title, planUrl: null, planFile: null, - ...initialSpaceDisplayDraft(), - hideDecor: false, hideOpenings: false, zeroWallStyle: 'dashed', - roomColor: DEFAULT_ROOM_COLOR, roomOpacity: DEFAULT_ROOM_OPACITY, fillMode: 'custom', - customFill: null, - glowEnabled: true, - bgColor: null, - bgMode: 'daynight', northDeg: null, sunRays: null, - tempMin: DEFAULT_TEMP_MIN, tempMax: DEFAULT_TEMP_MAX, - showLqi: this._config?.show_signal ?? true, - cardFontScale: 1, - labelTemp: false, labelHum: false, labelLqi: false, labelLight: false, - cellCm: newSpaceCellCm(this._imperial), - cellCmInput: gridCellFieldValue(newSpaceCellCm(this._imperial), this._imperial), - cellCmTouched: false, - busy: false, - }; + if (this._onboardingRuntime) return this._onboardingRuntime._openNextImport(); + return this._editorRuntimeOrThrow()._openNextImport(); } /** Skip the current floor of the wizard without creating a space. */ private _skipImport(): void { - this._spaceDialog = null; - if (this._importQueue.length) this._openNextImport(); - else if (this._importTotal > 0 && this._model.length) { - this._importTotal = 0; - this._commitSpace(this._serverCfg!.spaces[0]?.id || this._space); - this._setMode('plan'); - this._showToast(this._t('import.done')); - } + if (this._onboardingRuntime) return this._onboardingRuntime._skipImport(); + return this._editorRuntimeOrThrow()._skipImport(); } private _renderImportDialog(): TemplateResult { - const d = this._importDialog!; - const n = d.floors.filter((f) => f.checked).length; - return html` (this._importDialog = null)}> -
-
${this._t('import.hint')}
- ${d.floors.map( - (f, i) => html``, - )} -
-
- - - -
-
`; + if (this._onboardingRuntime) return this._onboardingRuntime._renderImportDialog(); + return this._editorRuntimeOrThrow()._renderImportDialog(); } // ================= SUN ON THE PLAN (docs/SUN.md) ================= @@ -15985,22 +9888,14 @@ class HouseplanCard extends LitElement { // ================= GENERAL SETTINGS ================= private _openSettingsDialog = (): void => { - if (!this._norm) return; - // deep copy so the dialog edits do not leak into the live palette - const cm = this._glowRadiusCm; - const glowRadius = this._imperial - ? Math.round((cm / 30.48) * 10) / 10 - : Math.round(cm) / 100; - this._settingsDialog = { - colors: JSON.parse(JSON.stringify(this._fillColors)), - glowRadius, - bgColor: stageBgOf(this._settings, { bgColor: null }) || null, - northDeg: northDegOf(this._settings, {}), - bgMode: bgModeOf(this._settings, {}), - sunRays: sunRaysOn(this._settings, {}), - busy: false, - }; - }; + if (!this._editorRuntime) { + void this._ensureEditorRuntime().then((ready) => { + if (ready) this._openSettingsDialog(); + }); + return; + } + return this._editorRuntime._openSettingsDialog(); + } /** * Preview whole-plan maintenance. Nothing is written here: the pure run @@ -16016,28 +9911,7 @@ class HouseplanCard extends LitElement { preflight: OptimizeGeometryPreflightResult, candidate: ServerConfig | null, ): object { - // CODE-REVIEW-295-r1 M1: hash the CANDIDATE spaces the preflight judged, - // not the already-saved config — a saved-config hash is exactly what a - // space export would reproduce, and the block promises what the export - // does not carry. - const spacesById = new Map(((candidate as any)?.spaces || []) - .map((space: any) => [String(space?.id || ''), space])); - return { - kind: 'houseplan-optimize-preflight', - origin: 'runtime', - cardVersion: CARD_VERSION, - checkedAt: new Date().toISOString(), - preflightFingerprint: preflight.fingerprint, - failures: preflight.failures.map((failure) => ({ - spaceId: failure.spaceId, - displayName: failure.displayName, - reason: failure.reason, - detail: failure.detail ?? null, - spaceGeometryFingerprint: spacesById.has(failure.spaceId) - ? spacePhysicalGeometryFingerprint(spacesById.get(failure.spaceId)) - : null, - })), - }; + return this._editorRuntimeOrThrow()._preflightDiagnostics(preflight, candidate); } /** #295: dev-log once per distinct failing preflight, not once per render. */ @@ -16046,10 +9920,7 @@ class HouseplanCard extends LitElement { preflight: OptimizeGeometryPreflightResult, candidate: ServerConfig | null, ): void { - if (preflight.ok || preflight.fingerprint === this._reportedPreflightFingerprint) return; - this._reportedPreflightFingerprint = preflight.fingerprint; - // eslint-disable-next-line no-console - console.warn('[houseplan] optimize preflight failed', this._preflightDiagnostics(preflight, candidate)); + return this._editorRuntimeOrThrow()._reportPreflightFailure(preflight, candidate); } /** @@ -16060,34 +9931,15 @@ class HouseplanCard extends LitElement { * report where the owner had nothing newer to update to). */ private _preflightVersionsDiffer(): boolean { - const integration = this._haIntegrationVersion; - return typeof integration === 'string' && integration.length > 0 - && integration !== CARD_VERSION; + return this._editorRuntimeOrThrow()._preflightVersionsDiffer(); } private async _copyPreflightDiagnostics(): Promise { - const preflight = this._alignDialog?.preflight; - if (!preflight || preflight.ok) return; - const text = JSON.stringify( - this._preflightDiagnostics(preflight, this._alignDialog?.config ?? null), null, 2, - ); - try { - await navigator.clipboard.writeText(text); - this._preflightClipboardFallback = null; - this._showToast(this._t('gs.preflight_copied')); - } catch { - // Insecure context / embedded webview: surface the block inline so the - // owner can select and copy it by hand. - this._preflightClipboardFallback = text; - } + return this._editorRuntimeOrThrow()._copyPreflightDiagnostics(); } private _checkOptimizeGeometry(config: ServerConfig): OptimizeGeometryPreflightResult { - return checkOptimizeGeometry(config, { - fallbackSpaceName: (index) => this._t('gs.align_preflight_space', { - n: String(index), - }), - }); + return this._editorRuntimeOrThrow()._checkOptimizeGeometry(config); } /** One strict production source shared by editor commits and stale rechecks. */ @@ -16098,184 +9950,33 @@ class HouseplanCard extends LitElement { geometry: ReturnType, ) => void, ) { - return checkSpacePhysicalGeometry(config, spaceId, { - fallbackSpaceName: (index) => this._t('gs.align_preflight_space', { n: String(index) }), - captureWallGeometry: captureWallGeometry - ? (_input, geometry) => captureWallGeometry(geometry) - : undefined, - }); + return this._editorRuntimeOrThrow()._checkSpacePhysicalGeometry(config, spaceId, captureWallGeometry); } private _optimizeReferenceContext( removeLiveMissingPositions: boolean, ): SpaceReferenceRepairContext { - const registry = this._haRegistry; - const full = this._fullRegistryHass; - const names: Record = {}; - const humanName = (...values: unknown[]): string => { - for (const value of values) { - const name = String(value || '').trim(); - if (name) return name; - } - return ''; - }; - for (const [deviceId, device] of Object.entries(registry.devices || {})) { - names[deviceId] = humanName(device?.name_by_user, device?.name, device?.model); - } - for (const [entityId, entity] of Object.entries(registry.entities || {})) { - names[`lg_${entityId}`] = humanName( - this.hass?.states?.[entityId]?.attributes?.friendly_name, - entity?.name, - entity?.original_name, - ); - } - for (const device of this._devices) names[device.id] = humanName(device.name, names[device.id]); - for (const marker of this._serverCfg?.markers || []) { - const separator = String(marker.binding || '').indexOf(':'); - const kind = separator > 0 ? marker.binding.slice(0, separator) : ''; - const ref = separator > 0 ? marker.binding.slice(separator + 1) : ''; - const bound = kind === 'device' - ? full?.devices?.[ref] - : kind === 'entity' ? full?.entities?.[ref] : null; - names[marker.id] = humanName( - marker.name, - kind === 'device' ? bound?.name_by_user : null, - bound?.name, - kind === 'entity' ? this.hass?.states?.[ref]?.attributes?.friendly_name : null, - names[marker.id], - ); - } - return { - effectiveAreaByMarker: Object.fromEntries( - this._devices - .filter((device) => !device.virtual && !!device.area) - .map((device) => [device.id, device.area]), - ), - ownerRoster: { - authoritative: registry.authoritative, - deviceIds: Object.keys(registry.devices || {}), - // State-only YAML entities are positive existence evidence even though - // an authoritative Entity Registry cannot list them. - entityIds: [...new Set([ - ...Object.keys(registry.entities || {}), - ...Object.keys(this.hass?.states || {}), - ])], - names, - }, - removeLiveMissingPositions, - }; + return this._editorRuntimeOrThrow()._optimizeReferenceContext(removeLiveMissingPositions); } private _previewAlignDialog(removeLiveMissingPositions: boolean): void { - if (!this._norm || !this._serverCfg) return; - const spaces = this._serverCfg.spaces || []; - let r; - try { - r = optimizePlans( - this._serverCfg, - this._layout || {}, - this._optimizeReferenceContext(removeLiveMissingPositions), - ); - } catch (error) { - this._showWallModelMigrationBlocked(error); - return; - } - const preflight = r.changed ? this._checkOptimizeGeometry(r.config) : null; - if (preflight) this._reportPreflightFailure(preflight, r.config); - // The maximum geometry shift is an UPPER BOUND, not a sample. The run - // measured every element in the centimetres of ITS OWN space — converting - // one normalised maximum through the first space's `cell_cm` understated - // a two-scale plan twentyfold (AUD-158B1-01) — and the last tenth is - // rounded UP, so the promise can never be smaller than the deed. - const cm = Math.ceil(r.report.maxShiftCm * 10) / 10; - const sp = spaces.find((x: any) => x?.id != null && String(x.id) === r.report.maxSpace); - const where = spaces.length > 1 && sp ? String(sp.title || sp.id) : ''; - // CODE-REVIEW-295-r1 M2: the inline clipboard fallback belongs to one - // dialog showing — a reopened dialog must not display the previous - // refusal's JSON while the visible reasons already describe a new one. - this._preflightClipboardFallback = null; - this._alignDialog = { - report: r.report, config: r.config, layout: r.layout, cm, where, - preflight, changed: r.changed, busy: false, removeLiveMissingPositions, - }; + return this._editorRuntimeOrThrow()._previewAlignDialog(removeLiveMissingPositions); } - private _openAlignDialog = (): void => this._previewAlignDialog(false); + private _openAlignDialog = (): void => { + return this._editorRuntimeOrThrow()._openAlignDialog(); + } private _toggleOptimizeLivePositions = (): void => { - const dialog = this._alignDialog; - if (!dialog || dialog.busy || !dialog.report.liveMissingPositions.length) return; - this._previewAlignDialog(!dialog.removeLiveMissingPositions); - }; + return this._editorRuntimeOrThrow()._toggleOptimizeLivePositions(); + } /** * The backend persists an intent before either store changes, then keeps a * one-deep snapshot that remains undoable until the next plan edit. */ private async _runAlignToGrid(): Promise { - let d = this._alignDialog; - if (!d || d.busy || !this._serverCfg || !d.changed || !d.preflight?.ok) return; - const fingerprint = contentFingerprint(d.config); - if (d.preflight.fingerprint !== fingerprint) { - const preflight = this._checkOptimizeGeometry(d.config); - this._reportPreflightFailure(preflight, d.config); - d = { ...d, preflight }; - this._alignDialog = d; - if (!preflight.ok) return; - } - this._clearGeometryGesture(); - this._alignDialog = { ...d, busy: true }; - try { - if (this._saveConfigDebounced.pending()) this._saveConfigDebounced.flush(); - await this._writeChain; - const resp = await this.hass.callWS({ - type: 'houseplan/plan/optimize', - config: d.config, - layout: d.layout, - expected_config_rev: this._cfgRev, - expected_layout_rev: this._layoutRev, - }); - this._serverCfg = d.config; - this._layout = d.layout; - this._geometryHistory.clear(); - this._cfgRev = resp?.config_rev ?? this._cfgRev + 1; - this._layoutRev = resp?.layout_rev ?? this._layoutRev + 1; - this._canOptimizeUndo = !!resp?.can_undo; - this._undoKind = resp?.can_undo ? 'optimize' : null; - this._dirtyPos.clear(); - this._sentPos.clear(); - this._cfgEpoch++; - this._modelCache = null; - this._frame = null; - this._regSignature = ''; - this._maybeRebuildDevices(); - this._cacheSnapshot(); - this._alignDialog = null; - this._preflightClipboardFallback = null; - this.requestUpdate(); - this._showToast(this._t('gs.align_done', { - n: String(d.report.moved), - m: String(d.report.migrated + d.report.canonicalized - + d.report.coordsCanonicalized + d.report.latticeCoordinatesCanonicalized - + d.report.wallsMerged + d.report.spansMerged - + d.report.partitionsMerged + d.report.partitionsReconciled - + d.report.openingsRehosted + d.report.wallsStraightened), - r: String(d.report.spaceRefsRemapped + d.report.roomRefsRemapped - + d.report.positionsRemapped + d.report.markersDetached - + d.report.orphanRoomLabelsRemoved + d.report.orphanDevicePositionsRemoved - + d.report.orphanGroupPositionsRemoved), - })); - } catch (e: any) { - if (this._alignDialog) this._alignDialog = { ...this._alignDialog, busy: false }; - if (e?.code === 'wall_model_client_outdated') { - this._showToast(this._t('toast.wall_model_client_outdated')); - return; - } - if (e?.code === 'conflict') { - await Promise.all([this._reloadConfigOnly(true), this._reloadLayoutOnly()]); - } - this._showToast(this._t('toast.error', { err: this._errText(e) })); - } + return this._editorRuntimeOrThrow()._runAlignToGrid(); } /** Prevent a double click from sending two restores of the same snapshot. */ @@ -16283,448 +9984,43 @@ class HouseplanCard extends LitElement { /** Restore the one-deep snapshot, provided no later plan edit exists. */ private async _undoPlanOptimization(): Promise { - if (!this._canOptimizeUndo || this._optimizeUndoBusy) return; - const undoKind = this._undoKind; - this._clearGeometryGesture(); - this._optimizeUndoBusy = true; - this.requestUpdate(); - try { - await this.hass.callWS({ - type: 'houseplan/plan/optimize_undo', - expected_config_rev: this._cfgRev, - expected_layout_rev: this._layoutRev, - }); - const [cfgResp, layResp] = await Promise.all([ - this.hass.callWS({ type: 'houseplan/config/get' }), - this.hass.callWS({ type: 'houseplan/layout/get' }), - ]); - this._serverCfg = cfgResp?.config || this._serverCfg; - this._cfgRev = cfgResp?.rev ?? this._cfgRev; - this._layout = layResp?.layout || this._layout; - this._geometryHistory.clear(); - this._layoutRev = layResp?.rev ?? this._layoutRev; - this._canOptimizeUndo = false; - this._undoKind = null; - this._cfgEpoch++; - this._modelCache = null; - this._frame = null; - this._regSignature = ''; - this._maybeRebuildDevices(); - this._cacheSnapshot(); - this.requestUpdate(); - this._showToast(this._t(undoKind === 'import' ? 'backup.import_undone' : 'gs.optimize_undone')); - } catch (e: any) { - this._canOptimizeUndo = false; - this._undoKind = null; - this._showToast(this._t('toast.error', { err: this._errText(e) })); - } finally { - this._optimizeUndoBusy = false; - this.requestUpdate(); - } + return this._editorRuntimeOrThrow()._undoPlanOptimization(); } private _openBackupExport = (): void => { - this._settingsDialog = null; - this._backupExportDialog = { kind: 'full', planOnly: false, busy: false, error: '' }; - }; + return this._editorRuntimeOrThrow()._openBackupExport(); + } private async _runBackupExport(): Promise { - const d = this._backupExportDialog; - if (!d || d.busy) return; - this._backupExportDialog = { ...d, busy: true, error: '' }; - try { - const response: any = await this.hass.callWS({ - type: 'houseplan/export/create', - kind: d.kind, - space_id: d.kind === 'space' ? this._space : undefined, - ...(d.kind === 'space' && d.planOnly ? { plan_only: true } : {}), - card_version: CARD_VERSION, - }); - const blob = new Blob([JSON.stringify(response.document, null, 2) + '\n'], { - type: 'application/json;charset=utf-8', - }); - const url = URL.createObjectURL(blob); - const anchor = document.createElement('a'); - anchor.href = url; - anchor.download = response.filename || `houseplan-${d.kind}.json`; - anchor.style.display = 'none'; - document.body.append(anchor); - anchor.click(); - anchor.remove(); - window.setTimeout(() => URL.revokeObjectURL(url), 0); - this._backupExportDialog = null; - this._showToast(this._t('backup.export_done')); - } catch (error: any) { - if (this._backupExportDialog) { - this._backupExportDialog = { ...this._backupExportDialog, busy: false, error: this._backupErrorText(error) }; - } - } + return this._editorRuntimeOrThrow()._runBackupExport(); } private async _pickBackupImport(ev: Event): Promise { - const input = ev.target as HTMLInputElement; - const file = input.files?.[0]; - input.value = ''; - if (!file) return; - this._settingsDialog = null; - this._backupImportDialog = { - filename: file.name, size: file.size, token: '', preview: null, - expectedConfigRev: this._cfgRev, expectedLayoutRev: this._layoutRev, - duplicatePolicy: 'skip', confirmMissing: false, busy: true, error: '', - }; - try { - if (this._saveConfigDebounced.pending()) this._saveConfigDebounced.flush(); - await this._writeChain; - if (this._persistLayout.pending()) this._persistLayout.flush(); - const path = '/api/houseplan/import/preview?duplicate_policy=skip'; - const init = { method: 'POST', body: file, headers: { 'content-type': 'application/json' } }; - const response: Response = this.hass?.fetchWithAuth - ? await this.hass.fetchWithAuth(path, init) - : await fetch(path, { - ...init, - headers: { - ...init.headers, - ...(this.hass?.auth?.data?.access_token - ? { authorization: `Bearer ${this.hass.auth.data.access_token}` } : {}), - }, - }); - const data = await response.json().catch(() => ({})); - if (!response.ok || data.error) throw Object.assign(new Error(data.message || data.error || `HTTP ${response.status}`), { code: data.error }); - if (this._backupImportDialog) { - this._backupImportDialog = { - ...this._backupImportDialog, - token: data.token, - preview: data.preview, - expectedConfigRev: data.expected_config_rev, - expectedLayoutRev: data.expected_layout_rev, - busy: false, - }; - } - } catch (error: any) { - if (this._backupImportDialog) { - this._backupImportDialog = { ...this._backupImportDialog, busy: false, error: this._backupErrorText(error) }; - } - } + return this._editorRuntimeOrThrow()._pickBackupImport(ev); } private async _setBackupDuplicatePolicy(policy: 'skip' | 'virtual'): Promise { - const d = this._backupImportDialog; - if (!d || d.busy || !d.token || policy === d.duplicatePolicy) return; - this._backupImportDialog = { ...d, duplicatePolicy: policy, busy: true, error: '' }; - try { - const response: any = await this.hass.callWS({ - type: 'houseplan/import/revalidate', token: d.token, duplicate_policy: policy, - }); - if (this._backupImportDialog) { - this._backupImportDialog = { - ...this._backupImportDialog, - preview: { ...d.preview, ...response.preview }, - expectedConfigRev: response.expected_config_rev, - expectedLayoutRev: response.expected_layout_rev, - confirmMissing: false, - busy: false, - }; - } - } catch (error: any) { - if (this._backupImportDialog) { - this._backupImportDialog = { - ...this._backupImportDialog, - duplicatePolicy: d.duplicatePolicy, - preview: d.preview, - confirmMissing: d.confirmMissing, - busy: false, - error: this._backupErrorText(error), - }; - } - } + return this._editorRuntimeOrThrow()._setBackupDuplicatePolicy(policy); } private async _applyBackupImport(): Promise { - const d = this._backupImportDialog; - if (!d || d.busy || !d.token || !d.preview) return; - if (d.preview.confirmation_required && !d.confirmMissing) return; - this._backupImportDialog = { ...d, busy: true, error: '' }; - const previousSpace = this._space; - try { - const result: any = await this.hass.callWS({ - type: 'houseplan/import/apply', - token: d.token, - expected_config_rev: d.expectedConfigRev, - expected_layout_rev: d.expectedLayoutRev, - duplicate_policy: d.duplicatePolicy, - confirm_missing_content: d.confirmMissing, - }); - const [configResponse, layoutResponse] = await Promise.all([ - this.hass.callWS({ type: 'houseplan/config/get' }), - this.hass.callWS({ type: 'houseplan/layout/get' }), - ]); - this._adoptStructuralResponses(configResponse, layoutResponse); - this._geometryHistory.clear(); - this._dirtyPos.clear(); - this._sentPos.clear(); - this._defPos = {}; - this._cfgEpoch++; - this._modelCache = null; - this._frame = null; - this._visibleDeviceSnapshot = null; - this._candidateDeviceSnapshot = null; - this._stagedDeviceSnapshotToken = -1; - this._capturedSnapshotConfigEpoch = -1; - this._regSignature = ''; - this._signer.invalidate(this.hass); - this._resign(); - this._maybeRebuildDevices(); - const spaces = this._serverCfg?.spaces || []; - const nextSpace = result.kind === 'space' && result.space_id - ? result.space_id - : spaces.some((space: any) => space.id === previousSpace) - ? previousSpace : spaces[0]?.id || this._space; - if (this._hasFixedFloor) this._adoptInitialSpace(this._model, true); - else this._commitSpace(nextSpace); - this._backupImportDialog = null; - this._cacheSnapshot(); - this.requestUpdate(); - const outcomeCounts = result.kind === 'space' ? d.preview.counts : result.counts; - this._showToast(this._t(result.kind === 'space' ? 'backup.space_done' : 'backup.full_done', { - spaces: String(outcomeCounts?.spaces || 0), rooms: String(outcomeCounts?.rooms || 0), - markers: String(outcomeCounts?.markers || 0), - refs: String(result.repaired_target_refs || 0), - })); - } catch (error: any) { - if (error?.code === 'conflict' && this._backupImportDialog?.token) { - try { - const current = this._backupImportDialog; - const refreshed: any = await this.hass.callWS({ - type: 'houseplan/import/revalidate', token: current.token, - duplicate_policy: current.duplicatePolicy, - }); - this._backupImportDialog = { - ...current, - preview: { ...current.preview, ...refreshed.preview }, - expectedConfigRev: refreshed.expected_config_rev, - expectedLayoutRev: refreshed.expected_layout_rev, - confirmMissing: false, - busy: false, - error: this._t('backup.revalidated'), - }; - return; - } catch (refreshError: any) { - error = refreshError; - } - } - if (this._backupImportDialog) { - this._backupImportDialog = { ...this._backupImportDialog, busy: false, error: this._backupErrorText(error) }; - } - } + return this._editorRuntimeOrThrow()._applyBackupImport(); } private _renderBackupExportDialog(): TemplateResult { - const d = this._backupExportDialog!; - const currentSpace = (this._serverCfg?.spaces || []).find((space: any) => space.id === this._space); - return html` (this._backupExportDialog = null)}> -
-
${this._t('backup.export_hint')}
- - - ${d.kind === 'space' && currentSpace ? html`` : nothing} -
${this._t('backup.privacy_warning')}
- ${d.error ? html`` : nothing} -
-
- - - -
-
`; + return this._editorRuntimeOrThrow()._renderBackupExportDialog(); } private _renderBackupImportDialog(): TemplateResult { - const d = this._backupImportDialog!; - const p = d.preview; - const counts = p?.counts || {}; - const report = p?.reference_report || {}; - const sum = (values: any): number => Object.values(values || {}).reduce( - (total: number, value: any) => total + (Number(value) || 0), 0, - ); - const allReportRows: Array<[ - 'incoming_remapped' | 'target_repaired' | 'preserved_unresolved' - | 'collisions' | 'dropped_links' | 'bounded_lineages', - number, - ]> = [ - ['incoming_remapped', sum(report.remapped?.incoming)], - ['target_repaired', sum(report.remapped?.target)], - ['preserved_unresolved', sum(report.preservedUnresolved)], - ['collisions', sum(report.collisions)], - ['dropped_links', sum(report.droppedIncomingLinks)], - ['bounded_lineages', Number(report.boundedLineages) || 0], - ]; - const reportRows = allReportRows.filter(([, value]) => value > 0); - return html` (this._backupImportDialog = null)}> -
-
${d.filename}${(d.size / 1024).toFixed(1)} KB
- ${d.busy && !p ? html`
${this._t('backup.reading')}
` : nothing} - ${d.error ? html`` : nothing} - ${p ? html` -
- ${this._t(p.kind === 'full' ? 'backup.full' : 'backup.current_space')} - ${p.plan_only ? html`${this._t('backup.plan_only_preview')}` : nothing} - ${this._t(p.source === 'same' ? 'backup.same_source' : 'backup.foreign_source')} - ${this._t('backup.created', { value: p.created_at || '—' })} - ${this._t('backup.versions', { - card: p.card_version || '—', integration: p.integration_version || '—', - model: String(p.model_version ?? '—'), - })} -
-
- ${this._t('backup.count_spaces', { n: p.kind === 'full' - ? `${p.current_counts?.spaces || 0} → ${counts.spaces || 0}` : String(counts.spaces || 0) })} - ${this._t('backup.count_rooms', { n: p.kind === 'full' - ? `${p.current_counts?.rooms || 0} → ${counts.rooms || 0}` : String(counts.rooms || 0) })} - ${this._t('backup.count_walls', { n: p.kind === 'full' - ? `${p.current_counts?.walls || 0} → ${counts.walls || 0}` : String(counts.walls || 0) })} - ${this._t('backup.count_openings', { n: p.kind === 'full' - ? `${p.current_counts?.openings || 0} → ${counts.openings || 0}` : String(counts.openings || 0) })} - ${this._t('backup.count_decor', { n: p.kind === 'full' - ? `${p.current_counts?.decor || 0} → ${counts.decor || 0}` : String(counts.decor || 0) })} - ${this._t('backup.count_markers', { n: p.kind === 'full' - ? `${p.current_counts?.markers || 0} → ${counts.markers || 0}` : String(counts.markers || 0) })} - ${this._t('backup.count_layout', { n: p.kind === 'full' - ? `${p.current_counts?.layout || 0} → ${counts.layout || 0}` : String(counts.layout || 0) })} -
- ${p.bindings ? html`
${this._t('backup.bindings', { - device: String(p.bindings.device || 0), entity: String(p.bindings.entity || 0), - virtual: String(p.bindings.virtual || 0), legacy: String(p.legacy_positions || 0), - })}
${this._t('backup.binding_status', { - active: String(p.bindings.active || 0), disabled: String(p.bindings.disabled || 0), - missing: String(p.bindings.missing || 0), - })}
` : nothing} - ${p.missing_areas?.length ? html`
${this._t('backup.missing_areas', { - areas: p.missing_areas.join(', '), - })}
` : nothing} - ${p.dropped_marker_links ? html`
${this._t('backup.dropped_marker_links', { - n: String(p.dropped_marker_links), - })}
` : nothing} - ${p.repaired_target_refs ? html`
${this._t('backup.repaired_target_refs', { - n: String(p.repaired_target_refs), - })}
` : nothing} - ${p.preserved_unresolved_refs ? html` -
${this._t('backup.preserved_unresolved_refs', { - n: String(p.preserved_unresolved_refs), - })}
${this._t('backup.preserved_unresolved_hint')}
- ` : nothing} - ${reportRows.length ? html`
- ${this._t('backup.import_details')} -
- ${reportRows.map(([key, value]) => html`${this._t( - `backup.import_detail.${key}`, { n: String(value) }, - )}`)} - ${(report.examples || []).slice(0, 8).map((item: any) => html` - ${item.owner} → ${item.reference} - `)} -
-
` : nothing} - ${p.kind === 'full' ? html` -
${this._t('backup.replace_warning')}
- ${p.source === 'foreign' ? html`
${this._t('backup.foreign_bookkeeping')}
` : nothing}` : html` -
${this._t('backup.final_name')}${p.space_title}
-
${this._t('backup.target_settings')}
- ${p.duplicates ? html`
${this._t('backup.duplicates', { n: String(p.duplicates) })} - - -
` : nothing}`} - ${p.content?.length ? html`
- ${this._t('backup.content')} - ${p.content.map((item: any) => html`${item.url} · ${this._t( - item.state === 'available' ? 'backup.content_available' - : item.state === 'external' ? 'backup.content_external' - : 'backup.content_detach_required', - )}`)} -
` : nothing} - ${p.confirmation_required ? html`` : nothing} - ` : nothing} -
-
- - - ${p ? html`` : nothing} -
-
`; + return this._editorRuntimeOrThrow()._renderBackupImportDialog(); } private _setFillColor(key: keyof FillColors, patch: Partial<{ c: string; a: number }>): void { - const d = this._settingsDialog!; - this._settingsDialog = { ...d, colors: { ...d.colors, [key]: { ...d.colors[key], ...patch } } }; + return this._editorRuntimeOrThrow()._setFillColor(key, patch); } private async _saveSettingsDialog(): Promise { - const d = this._settingsDialog; - if (!d || d.busy) return; - this._settingsDialog = { ...d, busy: true }; - try { - const cfg = this._serverCfg!; - const isDefault = JSON.stringify(d.colors) === JSON.stringify(DEFAULT_FILL_COLORS); - const settings: any = { ...cfg.settings }; - if (isDefault) delete settings.fill_colors; - else settings.fill_colors = d.colors; - const cm = this._imperial ? d.glowRadius * 30.48 : d.glowRadius * 100; - if (Number.isFinite(cm) && cm > 0 && Math.round(cm) !== 300) settings.glow_radius_cm = Math.round(cm); - else delete settings.glow_radius_cm; - if (d.bgColor) settings.bg_color = d.bgColor; - else delete settings.bg_color; - // Sun background compatibility is explicit: legacy absence means static, - // while new installations and spaces materialise daynight. - if (d.northDeg !== null && Number.isInteger(d.northDeg) && d.northDeg >= 0 && d.northDeg <= 359) - settings.north_deg = d.northDeg; - else delete settings.north_deg; - settings.bg_mode = d.bgMode; - if (d.sunRays) settings.sun_rays = true; - else delete settings.sun_rays; - // Legacy compatibility: old configs may still contain this accepted - // field, but weather no longer affects sunlight and the UI no longer - // exposes it. Saving general settings cleans the obsolete value up. - delete settings.weather_entity; - this._serverCfg = { ...cfg, settings }; - await this._saveConfigNow(); - this._settingsDialog = null; - this.requestUpdate(); - this._showToast(this._t('gs.saved')); - } catch (e: any) { - // audit L3: the dialog may have been closed (Esc) while the save was - // in flight — spreading null yields a truthy husk and the renderer - // then crashes, blanking the whole card. The toast below is the - // only remaining signal, so it must still fire. - if (this._settingsDialog) this._settingsDialog = { ...this._settingsDialog, busy: false }; - this._showToast(this._t('toast.error', { err: this._errText(e) })); - } + return this._editorRuntimeOrThrow()._saveSettingsDialog(); } /** Boolean toggle for dialog rows: the native ha-switch when the HA @@ -16734,10 +10030,7 @@ class HouseplanCard extends LitElement { * fire `change` and both are read back via `.checked` off the event * target - one handler, two renderers. */ private _boolInput(checked: boolean, onChange: (v: boolean) => void, disabled = false): TemplateResult { - const h = (e: Event) => onChange(!!(e.target as HTMLInputElement).checked); - return customElements.get('ha-switch') - ? html`` - : html``; + return this._editorRuntimeOrThrow()._boolInput(checked, onChange, disabled); } /** Range slider for dialog rows: ha-slider when available, plain @@ -16749,29 +10042,11 @@ class HouseplanCard extends LitElement { min: number, max: number, step: number, value: number, onInput: (v: number) => void, disabled = false, ariaLabel?: string, ): TemplateResult { - const h = (e: Event) => { - const n = Number((e.target as HTMLInputElement).value); - if (Number.isFinite(n)) onInput(n); - }; - return customElements.get('ha-slider') - ? html`` - : html``; + return this._editorRuntimeOrThrow()._rangeInput(min, max, step, value, onInput, disabled, ariaLabel); } private _renderColorRow(key: keyof FillColors, labelKey: string): TemplateResult { - const d = this._settingsDialog!; - const v = d.colors[key]; - return html`
- ) => { - this._setFillColor(key, { c: e.detail.color, a: e.detail.opacity }); - }}> -
`; + return this._editorRuntimeOrThrow()._renderColorRow(key, labelKey); } /** Glow radius: stored in cm (config.settings.glow_radius_cm), default 3 m. */ @@ -17294,530 +10569,38 @@ class HouseplanCard extends LitElement { * and promises the one-deep undo before either store is changed. */ private _renderAlignDialog(): TemplateResult { - const d = this._alignDialog!; - const r = d.report; - const failed = d.changed && !d.preflight?.ok; - const failures = d.preflight?.failures || []; - const visibleNames = failures.slice(0, 3).map((failure) => failure.displayName); - const spaces = visibleNames.length - ? visibleNames.join(', ') - : this._t('gs.align_preflight_space', { n: '1' }); - const remaining = Math.max(0, failures.length - visibleNames.length); - const more = remaining - ? this._t('gs.align_preflight_more', { n: String(remaining) }) - : ''; - const repaired = r.spaceRefsRemapped + r.roomRefsRemapped - + r.positionsRemapped + r.markersDetached; - const modelMaintenance = r.migrated + r.canonicalized + r.coordsCanonicalized - + r.wallSegmentsMigrated - + r.wallsMerged + r.spansMerged + r.partitionsMerged - + r.partitionsReconciled + r.openingsRehosted + r.redundantDraftsRemoved; - const gridWarning = r.moved + r.rotated + r.removedDrafts - + r.coordsCanonicalized + r.wallsStraightened; - const straightenCm = Math.ceil(r.maxStraightenShiftCm * 10) / 10; - const straightenSpace = (this._serverCfg?.spaces || []).find( - (space: any) => String(space?.id || '') === r.maxStraightenSpace, - ); - const straightenWhere = (this._serverCfg?.spaces || []).length > 1 && straightenSpace - ? String(straightenSpace.title || straightenSpace.id) : ''; - const removed = r.orphanRoomLabelsRemoved + r.orphanDevicePositionsRemoved - + r.orphanGroupPositionsRemoved; - const liveNames = r.liveMissingPositions.map((item) => item.name).filter(Boolean); - const visibleLiveNames = liveNames.slice(0, 3).join(', '); - const remainingLiveNames = Math.max(0, liveNames.length - 3); - const liveNamesText = visibleLiveNames - ? this._t('gs.optimize_live_names', { - names: visibleLiveNames, - more: remainingLiveNames - ? this._t('gs.optimize_reference_more', { n: String(remainingLiveNames) }) : '', - }) - : ''; - const registryLimited = r.unverifiedPositions.some( - (item) => item.reason === 'registry_unavailable', - ); - const detailStatus = (item: typeof r.removedPositions[number]): string => { - if (r.removedPositions.some((removedItem) => removedItem.id === item.id)) { - return this._t('gs.optimize_detail_removed'); - } - if (r.liveMissingPositions.some((liveItem) => liveItem.id === item.id)) { - return this._t('gs.optimize_detail_live'); - } - return this._t('gs.optimize_detail_unverified'); - }; - const detailKind = (kind: typeof r.removedPositions[number]['kind']): string => this._t( - kind === 'room_label' ? 'gs.optimize_detail_room_label' - : kind === 'group' ? 'gs.optimize_detail_group' - : kind === 'device' ? 'gs.optimize_detail_device' - : 'gs.optimize_detail_unknown', - ); - const referenceDetails = [ - ...r.removedPositions, - ...r.liveMissingPositions.filter((item) => ( - !r.removedPositions.some((removedItem) => removedItem.id === item.id) - )), - ...r.unverifiedPositions, - ]; - const visibleDetails = referenceDetails.slice(0, 10); - const remainingDetails = Math.max(0, referenceDetails.length - visibleDetails.length); - return html` { this._alignDialog = null; this._preflightClipboardFallback = null; }}> -
- ${failed - ? html` -

${this._t('gs.align_preflight_failed', { spaces, more })}

- ${failures.slice(0, 10).map((failure) => html`

- ${failure.displayName}: ${this._t(`gs.preflight_reason_${failure.reason}` as any)} -

`)} - ${failures.length > 10 ? html`

- ${this._t('gs.align_preflight_more', { n: String(failures.length - 10) })} -

` : nothing} -
${this._t('gs.align_preflight_hint')}
- ${this._preflightVersionsDiffer() ? html` -
${this._t('gs.preflight_update_hint')}
` : nothing} -
- -
- ${this._preflightClipboardFallback ? html`
- ${this._t('gs.preflight_copy')} -
${this._preflightClipboardFallback}
-
` : nothing}` - : !d.changed - ? html`

${this._t( - r.liveMissingPositions.length || r.unverifiedPositions.length - || r.nestedRefsUnresolved - ? 'gs.optimize_no_automatic_changes' : 'gs.align_none', - )}

` - : html` - ${r.moved ? html`

${this._t('gs.align_count', { - n: String(r.moved), total: String(r.total), cm: String(d.cm), - })}

` : nothing} - ${r.latticeCoordinatesCanonicalized ? html` -

${this._t('gs.optimize_lattice_summary', { - n: String(r.latticeCoordinatesCanonicalized), - cm: formatLatticeShiftCm(r.latticeMaxShiftCm), - })}

- ${r.latticeSpaces.map((space) => html`

${this._t( - 'gs.optimize_lattice_space', { - space: space.space, - n: String(space.canonicalized), - far: String(space.far), - }, - )}

`)} - ` : nothing} - ${d.where - ? html`

${this._t('gs.align_where', { s: d.where })}

` - : nothing} - ${r.rotated - ? html`

${this._t('gs.align_turned', { n: String(r.rotated) })}

` - : nothing} - ${r.removedDrafts - ? html`

${this._t('gs.align_removed_drafts', { - n: String(r.removedDrafts), - })}

` - : nothing} - ${r.redundantDraftsRemoved - ? html`

${this._t('gs.optimize_redundant_drafts', { - n: String(r.redundantDraftsRemoved), - })}

` - : nothing} - ${r.wallSegmentsMigrated ? html`

${this._t( - 'gs.wall_segments_migrated', { n: String(r.wallSegmentsMigrated) }, - )}

` : nothing} - ${r.legacyZeroWallsMigrated ? html`

${this._t( - 'gs.zero_walls_migrated', { n: String(r.legacyZeroWallsMigrated) }, - )}

` : nothing} - ${modelMaintenance ? html`

${this._t('gs.optimize_changes', { - m: String(r.migrated), c: String(r.canonicalized), - p: String(r.coordsCanonicalized), w: String(r.wallsMerged), - s: String(r.spansMerged), i: String(r.partitionsMerged), - })}

` : nothing} - ${r.partitionsReconciled ? html`

${this._t( - 'gs.optimize_coincident_partitions', { n: String(r.partitionsReconciled) }, - )}

` : nothing} - ${r.openingsRehosted ? html`

${this._t( - 'gs.optimize_openings_rehosted', { n: String(r.openingsRehosted) }, - )}

` : nothing} - ${r.wallsStraightened ? html`

${this._t( - 'gs.optimize_walls_straightened', { - n: String(r.wallsStraightened), cm: String(straightenCm), - }, - )}

` : nothing} - ${straightenWhere ? html`

${this._t( - 'gs.optimize_walls_straightened_where', { s: straightenWhere }, - )}

` : nothing} - ${r.glowSpacesMigrated || r.glowRoomsMigrated - ? html`

${this._t('gs.optimize_glow_migration', { - spaces: String(r.glowSpacesMigrated), - rooms: String(r.glowRoomsMigrated), - })}

` - : nothing} - ${gridWarning ? html`
${this._t('gs.align_warn')}
` : nothing}`} - ${!failed && r.wallsStraightenSkipped ? html`

${this._t( - 'gs.optimize_walls_straighten_skipped', { - n: String(r.wallsStraightenSkipped), - }, - )}

` : nothing} - ${repaired - ? html`

${this._t('gs.optimize_references', { - spaces: String(r.spaceRefsRemapped), rooms: String(r.roomRefsRemapped), - positions: String(r.positionsRemapped), detached: String(r.markersDetached), - })}

` - : nothing} - ${removed - ? html`

${this._t('gs.optimize_orphans_removed', { - total: String(removed), - rooms: String(r.orphanRoomLabelsRemoved), - devices: String(r.orphanDevicePositionsRemoved), - groups: String(r.orphanGroupPositionsRemoved), - })}

` - : nothing} - ${r.liveMissingPositions.length - ? html`
-

${this._t(d.removeLiveMissingPositions - ? 'gs.optimize_live_positions_remove' : 'gs.optimize_live_positions', { - n: String(r.liveMissingPositions.length), names: liveNamesText, - })}

- - ${d.removeLiveMissingPositions - ? html`
- ${this._t('gs.optimize_live_selected')} -
` - : nothing} -
` - : nothing} - ${r.unverifiedPositions.length - ? html`` - : nothing} - ${r.nestedRefsUnresolved - ? html`` - : nothing} - ${referenceDetails.length - ? html`
- ${this._t('gs.optimize_details')} -
    - ${visibleDetails.map((item) => html`
  • ${this._t('gs.optimize_detail_item', { - status: detailStatus(item), kind: detailKind(item.kind), - id: item.id, space: item.spaceId, - })}
  • `)} -
- ${remainingDetails - ? html`
${this._t('gs.optimize_details_more', { - n: String(remainingDetails), - })}
` - : nothing} -
` - : nothing} -
-
- - - ${!d.changed || !d.preflight?.ok ? nothing : html` - `} -
-
`; + return this._editorRuntimeOrThrow()._renderAlignDialog(); } private _renderSettingsDialog(): TemplateResult { - return html` (this._settingsDialog = null)}> -
-
${this._t('gs.hint')}
- - ${this._renderColorRow('light_on', 'gs.light_on')} - ${this._renderColorRow('light_off', 'gs.light_off')} - ${this._renderColorRow('light_none', 'gs.light_none')} - - ${this._renderColorRow('temp_cold', 'gs.temp_cold')} - ${this._renderColorRow('temp_ok', 'gs.temp_ok')} - ${this._renderColorRow('temp_hot', 'gs.temp_hot')} - - ${this._renderColorRow('lqi_low', 'gs.lqi_low')} - ${this._renderColorRow('lqi_high', 'gs.lqi_high')} - - ${this._renderColorRow('glow_base', 'gs.glow_base')} - ${this._renderColorRow('glow_light', 'gs.glow_light')} -
- ${this._t('gs.glow_radius')} - { - const v = strictNumber((e.target as HTMLInputElement).value); - if (v != null && v > 0) - this._settingsDialog = { ...this._settingsDialog!, glowRadius: v }; - }} /> - ${this._imperial ? this._t('gs.unit_ft') : this._t('gs.unit_m')} -
- - ${this._renderColorRow('wall_fill', 'gs.wall_fill')} - -
- ${this._t('gs.bg_mode')} - -
- ${this._settingsDialog!.bgMode === 'static' - ? html`
- ) => { - this._settingsDialog = { ...this._settingsDialog!, bgColor: e.detail.color }; - }}> - ${this._settingsDialog!.bgColor - ? html`` - : html`${this._t('gs.bg_theme')}`} -
` - : html`
${this._t('gs.bg_daynight_hint')}
`} - - ${!sunStateOf(this.hass) - ? html`
${this._t('gs.sun_missing')}
` - : nothing} -
- ${this._renderCompass()} -
- ${this._t('gs.north')} -
- { - const raw = (e.target as HTMLInputElement).value.trim(); - const n = raw === '' ? null : Math.round(Number(raw)); - this._settingsDialog = { - ...this._settingsDialog!, - northDeg: n !== null && Number.isFinite(n) ? Math.min(359, Math.max(0, n)) : null, - }; - }} /> - ${this._settingsDialog!.northDeg !== null - ? html`` - : nothing} -
- ${this._settingsDialog!.northDeg === null - ? html`
${this._t('gs.north_hint')}
` - : nothing} -
-
- - ${this._canEdit ? html` - -
${this._t('gs.backup_hint')}
-
- - - - - - ${this._canOptimizeUndo && this._undoKind === 'import' ? html` - ` : nothing} -
` : nothing} - -
${this._t('gs.grid_hint')}
-
- -
- ${this._canOptimizeUndo && this._undoKind !== 'import' ? html`
- -
` : nothing} - -
${this._t('gs.about_version', { v: CARD_VERSION })}
- - ${this._t('gs.about_github')} - - ${this._t('gs.about_telegram')} -
-
- - - - -
-
`; + return this._editorRuntimeOrThrow()._renderSettingsDialog(); } // ================= ICON RULES EDITOR ================= private _openRulesDialog = (): void => { - if (!this._norm) return; - const custom = this._settings.icon_rules; - const rules = (custom && custom.length ? custom : DEFAULT_ICON_RULES).map((r) => ({ ...r })); - this._rulesDialog = { rules, test: '', busy: false }; - }; + return this._editorRuntimeOrThrow()._openRulesDialog(); + } private _rulesSet(rules: IconRule[]): void { - this._rulesDialog = { ...this._rulesDialog!, rules }; + return this._editorRuntimeOrThrow()._rulesSet(rules); } private async _saveRules(): Promise { - const dlg = this._rulesDialog; - if (!dlg || dlg.busy) return; - const cleaned = dlg.rules.filter((r) => r.pattern.trim() && r.icon.trim()); - this._rulesDialog = { ...dlg, busy: true }; - try { - const cfg = this._serverCfg!; - const isDefault = JSON.stringify(cleaned) === JSON.stringify(DEFAULT_ICON_RULES); - const settings: any = { ...cfg.settings }; - if (isDefault) delete settings.icon_rules; - else settings.icon_rules = cleaned; - this._serverCfg = { ...cfg, settings }; - await this._saveConfigNow(); - this._rulesDialog = null; - this._regSignature = ''; - this._maybeRebuildDevices(); - this._showToast(this._t('rules.saved')); - } catch (e: any) { - // audit L3: the dialog may have been closed (Esc) while the save was - // in flight — spreading null yields a truthy husk and the renderer - // then crashes, blanking the whole card. The toast below is the - // only remaining signal, so it must still fire. - if (this._rulesDialog) this._rulesDialog = { ...this._rulesDialog, busy: false }; - this._showToast(this._t('toast.error', { err: this._errText(e) })); - } + return this._editorRuntimeOrThrow()._saveRules(); } private _renderRulesDialog(): TemplateResult { - const d = this._rulesDialog!; - const compiled = compileIconRules(d.rules); - const testIcon = d.test.trim() ? iconFor(d.test, '', compiled) : null; - const move = (i: number, delta: number) => { - const r = [...d.rules]; - const j = i + delta; - if (j < 0 || j >= r.length) return; - [r[i], r[j]] = [r[j], r[i]]; - this._rulesSet(r); - }; - return html` (this._rulesDialog = null)}> -
-
${this._t('rules.hint')}
-
- (this._rulesDialog = { ...d, test: (e.target as HTMLInputElement).value })} /> - ${testIcon ? html`${testIcon}` : nothing} -
- ${d.rules.map((r, i) => { - const bad = r.pattern.trim() !== '' && !isValidPattern(r.pattern); - return html`
- { - const rules = [...d.rules]; - rules[i] = { ...r, pattern: (e.target as HTMLInputElement).value }; - this._rulesSet(rules); - }} /> - { - const rules = [...d.rules]; - rules[i] = { ...r, icon: (e.target as HTMLInputElement).value }; - this._rulesSet(rules); - }} /> - - move(i, -1)}> - move(i, 1)}> - this._rulesSet(d.rules.filter((_, j) => j !== i))}> -
`; - })} - -
-
- - - - -
-
`; + return this._editorRuntimeOrThrow()._renderRulesDialog(); } private _saveKioskScale(patch: Partial<{ icon: number; font: number }>): void { - this._kioskScale = { ...this._kioskScale, ...patch }; - try { - localStorage.setItem(LS_KIOSK, JSON.stringify(this._kioskScale)); - } catch { - /* ignore */ - } - this.requestUpdate(); + return this._editorRuntimeOrThrow()._saveKioskScale(patch); } /** Per-SCREEN size settings (wall tablets differ) — stored locally. */ private _renderKioskDialog(): TemplateResult { - const k = this._kioskScale; - const row = (key: 'icon' | 'font', label: string) => html` -
- ${this._rangeInput(50, 300, 5, Math.round(k[key] * 100), (n) => this._saveKioskScale({ [key]: n / 100 }))} - ${Math.round(k[key] * 100)}% -
`; - return html` (this._kioskDialog = false)}> -
-
${this._t('kiosk.hint')}
- ${row('icon', this._t('kiosk.icon_scale'))} - ${row('font', this._t('kiosk.font_scale'))} -
-
- - - -
-
`; + return this._editorRuntimeOrThrow()._renderKioskDialog(); } // ================= render ================= @@ -17833,6 +10616,22 @@ class HouseplanCard extends LitElement { } protected render(): TemplateResult | typeof nothing { + const onboardingRuntimeRequested = !!this._importDialog + || !!(this._spaceDialog && this._spaceDialogUsesOnboardingRuntime(this._spaceDialog.mode)); + const editorRuntimeRequested = this._mode !== 'view' + || !!(this._roomDialog || this._mergeDialog || this._openingDialog + || this._physicalDialog || this._partitionDeleteDialog || this._roomDeleteDialog + || this._decorTextDialog || this._decorShapeDialog || this._backdropDialog + || this._decorEraseConfirm + || (this._spaceDialog && !this._spaceDialogUsesOnboardingRuntime(this._spaceDialog.mode)) + || this._deviceInbox + || this._markerDialog || this._rulesDialog || this._settingsDialog + || this._alignDialog || this._backupExportDialog || this._backupImportDialog + || this._kioskDialog || this._vacFit || this._vacCalConfirm); + if (onboardingRuntimeRequested && !this._onboardingRuntime) { + void this._ensureOnboardingRuntime(); + } + if (editorRuntimeRequested && !this._editorRuntime) void this._ensureEditorRuntime(); if (!this._config || !this.hass) return nothing; const model = this._model; const diagnostics = this.houseplanDiagnostics(); @@ -17885,8 +10684,10 @@ class HouseplanCard extends LitElement { ` : html`

${this._t('empty.install')}

`} - ${this._spaceDialog ? this._renderSpaceDialog() : nothing} - ${this._importDialog ? this._renderImportDialog() : nothing} + ${this._spaceDialog + ? (this._onboardingRuntime || this._editorRuntime) ? this._renderSpaceDialog() : nothing + : nothing} + ${this._importDialog ? this._onboardingRuntime ? this._renderImportDialog() : nothing : nothing} ${this._toast ? html`` : nothing} `; } @@ -18050,11 +10851,13 @@ class HouseplanCard extends LitElement { ?inert=${!this._editing}>
- ${editorChromeMode === 'plan' - ? this._renderMarkupBar() - : editorChromeMode === 'devices' - ? this._renderDevicesBar() - : this._renderDecorBar()} + ${this._editorRuntime + ? editorChromeMode === 'plan' + ? this._renderMarkupBar() + : editorChromeMode === 'devices' + ? this._renderDevicesBar() + : this._renderDecorBar() + : nothing}
` : nothing} @@ -18071,7 +10874,7 @@ class HouseplanCard extends LitElement { @pointerup=${(e: PointerEvent) => this._stagePointerUp(e)} @pointercancel=${(e: PointerEvent) => this._stagePointerCancel(e)}> ${renderDayCycleEnvironment(dayCycle, dayCycleWeight)} - ${this._renderEditorSecondary()} + ${this._editorRuntime ? this._renderEditorSecondary() : nothing}
@@ -18371,8 +11174,8 @@ class HouseplanCard extends LitElement { ${this._measureAnchor ? html`
${this._renderMeasureLabel(view)}
` : nothing} - ${this._resize.liveLabels - ? html`
${this._resize.liveLabels?.map((l) => html`
${this._resize?.liveLabels?.map((l) => html` - ${this._roomDialog ? this._renderRoomDialog() : nothing} - ${this._mergeDialog ? this._renderMergeDialog() : nothing} - ${this._openingDialog ? this._renderOpeningDialog() : nothing} - ${this._physicalDialog ? this._renderPhysicalDialog() : nothing} - ${this._partitionDeleteDialog ? this._renderPartitionDeleteDialog() : nothing} - ${this._roomDeleteDialog ? this._renderRoomDeleteDialog() : nothing} + ${this._roomDialog ? this._editorRuntime ? this._renderRoomDialog() : nothing : nothing} + ${this._mergeDialog ? this._editorRuntime ? this._renderMergeDialog() : nothing : nothing} + ${this._openingDialog ? this._editorRuntime ? this._renderOpeningDialog() : nothing : nothing} + ${this._physicalDialog ? this._editorRuntime ? this._renderPhysicalDialog() : nothing : nothing} + ${this._partitionDeleteDialog ? this._editorRuntime ? this._renderPartitionDeleteDialog() : nothing : nothing} + ${this._roomDeleteDialog ? this._editorRuntime ? this._renderRoomDeleteDialog() : nothing : nothing} ${this._openingInfo ? this._renderOpeningInfoCard() : nothing} - ${this._decorTextDialog ? this._renderDecorTextDialog() : nothing} - ${this._decorShapeDialog ? this._renderDecorShapeDialog() : nothing} - ${this._backdropDialog ? this._renderBackdropDialog() : nothing} - ${this._decorEraseConfirm ? this._renderDecorEraseConfirm() : nothing} - ${this._spaceDialog ? this._renderSpaceDialog() : nothing} - ${this._deviceInbox ? this._renderDeviceInbox() : nothing} - ${this._markerDialog ? this._renderMarkerDialog() : nothing} + ${this._decorTextDialog ? this._editorRuntime ? this._renderDecorTextDialog() : nothing : nothing} + ${this._decorShapeDialog ? this._editorRuntime ? this._renderDecorShapeDialog() : nothing : nothing} + ${this._backdropDialog ? this._editorRuntime ? this._renderBackdropDialog() : nothing : nothing} + ${this._decorEraseConfirm ? this._editorRuntime ? this._renderDecorEraseConfirm() : nothing : nothing} + ${this._spaceDialog + ? (this._onboardingRuntime || this._editorRuntime) ? this._renderSpaceDialog() : nothing + : nothing} + ${this._deviceInbox ? this._editorRuntime ? this._renderDeviceInbox() : nothing : nothing} + ${this._markerDialog ? this._editorRuntime ? this._renderMarkerDialog() : nothing : nothing} ${this._vacCalConfirm ? html` (this._vacCalConfirm = null)}> @@ -18452,12 +11258,12 @@ class HouseplanCard extends LitElement {
` : nothing} ${this._infoCard ? this._renderInfoCard() : nothing} - ${this._rulesDialog ? this._renderRulesDialog() : nothing} - ${this._settingsDialog ? this._renderSettingsDialog() : nothing} - ${this._alignDialog ? this._renderAlignDialog() : nothing} - ${this._backupExportDialog ? this._renderBackupExportDialog() : nothing} - ${this._backupImportDialog ? this._renderBackupImportDialog() : nothing} - ${this._importDialog ? this._renderImportDialog() : nothing} + ${this._rulesDialog ? this._editorRuntime ? this._renderRulesDialog() : nothing : nothing} + ${this._settingsDialog ? this._editorRuntime ? this._renderSettingsDialog() : nothing : nothing} + ${this._alignDialog ? this._editorRuntime ? this._renderAlignDialog() : nothing : nothing} + ${this._backupExportDialog ? this._editorRuntime ? this._renderBackupExportDialog() : nothing : nothing} + ${this._backupImportDialog ? this._editorRuntime ? this._renderBackupImportDialog() : nothing : nothing} + ${this._importDialog ? this._onboardingRuntime ? this._renderImportDialog() : nothing : nothing} ${this._tip ? html`
${this._tip.title}${this._tip.meta ? html`${this._tip.meta}` : nothing} @@ -18478,7 +11284,7 @@ class HouseplanCard extends LitElement { ${this._model.map((m) => html``)}
` : nothing} - ${this._kioskDialog ? this._renderKioskDialog() : nothing} + ${this._kioskDialog ? this._editorRuntime ? this._renderKioskDialog() : nothing : nothing} ${this._vacFit ? html`
${this._t('vac.fit_hint')} @@ -18784,140 +11590,7 @@ class HouseplanCard extends LitElement { /** «Живая позиция» in the device dialog — vacuum markers only. */ private _renderVacSection(dlg: any): TemplateResult | typeof nothing { - const dev = this._devices.find((x) => x.id === dlg.devId); - if (!dev || !this._isVacDev(dev)) return nothing; - const v = dev.marker?.vacuum || {}; - const includeAllCameras = this._vacAllCamerasFor === dev.id; - const resolution = this._vacSourceResolution(dev, includeAllCameras); - const src = resolution.entityId; - const selected = resolution.candidates.find((candidate) => candidate.entityId === src) || null; - const tele = src ? readVacTelemetry(this.hass?.states[src]?.attributes) : null; - const canUseSource = v.live !== false - && (resolution.status === 'ok' || resolution.status === 'unsupported'); - const planRooms = this._vacPlanRoomAnchors(dev.space); - const roomMatches = tele - ? vacRoomNameMatchCount(tele.rooms, planRooms.map((room) => room.name)) : 0; - const tierA = !!(canUseSource && tele && roomMatches >= 3); - const cals = Object.keys(v.calibration || {}); - const setVac = (patch: Record) => { - // HP-1540-01: materialise the marker first — find() alone silently - // dropped every edit for a vacuum that had never been saved - const m = this._vacEnsureMarker(dev); - if (!m) return; - m.vacuum = { ...(m.vacuum || {}), ...patch }; - this._regSignature = ''; - // First-use markers are materialised above, so the dialog must rebuild - // immediately instead of waiting for an unrelated HA state tick. - this._maybeRebuildDevices(); - this._saveConfig(); - this.requestUpdate(); - }; - const sameDevice = new Set(dev.entities || []); - const primaryCandidates = resolution.candidates.filter((candidate) => - sameDevice.has(candidate.entityId) || candidate.entityId === v.source); - const globalCandidates = includeAllCameras - ? resolution.candidates.filter((candidate) => candidate.entityId.startsWith('camera.') - && !primaryCandidates.some((entry) => entry.entityId === candidate.entityId)) - : []; - const capability = (candidate: VacSourceCandidate) => [ - candidate.hasPosition ? this._t('vac.cap_position') : '', - candidate.hasRooms ? this._t('vac.cap_rooms_short') : '', - candidate.hasPath ? this._t('vac.cap_path') : '', - candidate.hasMapId ? this._t('vac.cap_map') : '', - ].filter(Boolean).join(' · ') || this._t('vac.cap_none'); - const candidateButton = (candidate: VacSourceCandidate) => html` - `; - const knownSameDeviceXcme = resolution.candidates.some((candidate) => - sameDevice.has(candidate.entityId) && candidate.category === 'known_xcme_incomplete'); - const showXcmeHint = knownSameDeviceXcme - || (!!resolution.pinned && !!src?.startsWith('camera.') && !selected?.hasPosition); - const openPicker = () => { - const picker = [...this.renderRoot.querySelectorAll('details.vacpicker')] - .find((details) => details.dataset.devId === dev.id); - if (!picker) return; - picker.open = true; - picker.querySelector('summary')?.focus(); - }; - return html` - -
-
-
${this._t('vac.diag_source')}${src || this._t('vac.source_none')}
- ${selected?.platform ? html`
${this._t('vac.diag_platform')}${selected.platform}
` : nothing} -
${this._t('vac.diag_status')}${this._t((`vac.source_status_${resolution.status}`) as any)}
-
${this._t('vac.diag_position')}${tele?.pos ? this._t('common.yes') : this._t('common.no')}
-
${this._t('vac.diag_rooms')}${subst(this._t('vac.diag_rooms_value'), { - total: String(tele?.rooms.length || 0), matched: String(roomMatches), - readiness: this._t(roomMatches >= 3 ? 'vac.autocal_ready' : 'vac.autocal_not_ready'), - })}
-
${this._t('vac.diag_path')}${tele?.path.length ? this._t('common.yes') : this._t('common.no')}
-
${this._t('vac.diag_map')}${tele?.mapId ?? 'default'}
-
- ${(resolution.status === 'missing' || resolution.status === 'disabled' || resolution.status === 'unverified') - ? html`
- ${this._t((`vac.source_banner_${resolution.status}`) as any)} - -
` : nothing} - ${showXcmeHint ? html`
- ${this._t('vac.xcme_hint')} -
attributes:
-  - vacuum_position
-  - rooms
-  - path
-  - map_name
-
` : nothing} -
- ${this._t('vac.choose_source')} -
- - ${primaryCandidates.map(candidateButton)} -
{ - const open = (event.currentTarget as HTMLDetailsElement).open; - if (open) this._vacOpenAllCameras(dev); - else { - this._vacAllCamerasFor = null; - this._vacAllCameraCache = null; - } - }}> - ${this._t('vac.all_cameras')} -
${this._t('vac.all_cameras_warn')}
- ${includeAllCameras - ? (globalCandidates.length ? globalCandidates.map(candidateButton) - : html`
${this._t('vac.all_cameras_empty')}
`) - : nothing} -
-
-
-
- ${tierA ? html`` : nothing} - ${canUseSource - ? html`` - : nothing} - ${this._t('vac.documentation')} -
- ${tele ? html` - - - - ${cals.length ? html`
${subst(this._t('vac.cal_maps'), { maps: cals.join(', ') })}
` : nothing} - ` : nothing} -
`; + return this._editorRuntimeOrThrow()._renderVacSection(dlg); } /** @@ -18926,255 +11599,63 @@ class HouseplanCard extends LitElement { * live X50 Master) — without this both floors would share one matrix. */ private _vacMapId(d: DevItem, tele: { mapId: string }, planHass = this._planHass): string { - // HP-1541-01: nullish, not truthy — selected_map: 0 is a real map id and - // must equal what trails.py resolve_map_id stores server-side. - const ve = this._vacEntity(d); - const sel = ve ? planHass?.states?.[ve]?.attributes?.selected_map : null; - return vacMapIdWithFallback(tele.mapId, sel); + return this._editorRuntimeOrThrow()._vacMapId(d, tele, planHass); } /** Persist a solved matrix into marker.vacuum.calibration[mapId]. * Returns whether the write actually landed — callers must not toast * success otherwise (HP-1540-01). */ private _vacSaveMatrix(markerId: string, source: string, mapId: string, matrix: Affine): boolean { - // HP-1540-01: a first-use vacuum has no marker yet — materialise it - const dev = this._devices.find((x) => x.id === markerId); - const m = dev ? this._vacEnsureMarker(dev) - : this._serverCfg?.markers?.find((x: Marker) => x.id === markerId); - if (!m) return false; - const v = { ...(m.vacuum || {}) }; - v.source = source; - v.calibration = { ...(v.calibration || {}), [mapId]: matrix.map((n) => Number(n.toFixed(6))) }; - m.vacuum = v; - this._regSignature = ''; - this._maybeRebuildDevices(); - this._saveConfig(); - this.requestUpdate(); - return true; + return this._editorRuntimeOrThrow()._vacSaveMatrix(markerId, source, mapId, matrix); } /** The exact plan-room set accepted by auto-calibration and diagnostics. */ private _vacPlanRoomAnchors(spaceId: string | null | undefined): Array<{ name: string; cx: number; cy: number; }> { - return (this._spaceModelById(spaceId)?.rooms || []) - // HP-1540-04: legacy rectangle rooms (x/y/w/h) are still first-class; - // roomPoly() gives the same outline the renderer uses for them. - .map((room: any) => ({ room, poly: roomPoly(room) })) - .filter(({ room, poly }: any) => room.name && poly) - .map(({ room, poly }: any) => { - const centroid = areaCentroid(poly); - return centroid ? { name: String(room.name), cx: centroid[0], cy: centroid[1] } : null; - }) - .filter(Boolean) as Array<{ name: string; cx: number; cy: number }>; + return this._editorRuntimeOrThrow()._vacPlanRoomAnchors(spaceId); } /** «Настроить автоматически»: robot rooms ↔ plan rooms by name. */ private _vacAutoCalibrate(d: DevItem): void { - const src = this._vacSource(d); - const tele = src ? readVacTelemetry(this.hass?.states[src]?.attributes) : null; - if (!src || !tele || tele.rooms.length < 3) { - this._showToast(this._t('vac.autocal_no_rooms')); - return; - } - const planRooms = this._vacPlanRoomAnchors(d.space); - const res = autoCalibrate(tele.rooms, planRooms); - if (!res) { - this._showToast(this._t('vac.autocal_no_match')); - return; - } - const rawSpace = this._serverCfg?.spaces?.find((space: any) => space.id === d.space); - const rawCellCm = Number(rawSpace?.cell_cm); - const cellCm = Number.isFinite(rawCellCm) && rawCellCm > 0 ? rawCellCm : 5; - const residualCm = vacCalibrationResidualCm(res.residual, this._gridPitch, cellCm); - const mapId = this._vacMapId(d, tele); - if (residualCm > VAC_CALIBRATION_WARN_CM) { - this._vacCalConfirm = { - markerId: d.id, source: src, mapId, matrix: res.matrix, - rooms: res.matched.length, - error: formatLength(residualCm, this.hass?.config?.unit_system?.length === 'mi'), - }; - return; - } - if (!this._vacSaveMatrix(d.id, src, mapId, res.matrix)) return; - this._showToast(subst(this._t('vac.autocal_done'), { rooms: String(res.matched.length) })); + return this._editorRuntimeOrThrow()._vacAutoCalibrate(d); } private _vacApplyCalibrationProposal(manual: boolean): void { - const proposal = this._vacCalConfirm; - if (!proposal) return; - this._vacCalConfirm = null; - if (manual) { - const dev = this._devices.find((candidate) => candidate.id === proposal.markerId); - const fit = fitFromMatrix(proposal.matrix); - if (!dev || !fit) return; - this._markerDialog = null; - if (dev.space !== this._space && !this._commitSpace(dev.space)) return; - this._vacFit = { - markerId: proposal.markerId, source: proposal.source, - mapId: proposal.mapId, p: fit, drag: null, - }; - return; - } - if (this._vacSaveMatrix( - proposal.markerId, proposal.source, proposal.mapId, proposal.matrix, - )) this._showToast(subst(this._t('vac.autocal_done'), { rooms: String(proposal.rooms) })); + return this._editorRuntimeOrThrow()._vacApplyCalibrationProposal(manual); } /** «Подогнать вручную»: open the fit overlay and leave the dialog. */ private _vacStartFit(d: DevItem): void { - const src = this._vacSource(d); - const tele = src ? readVacTelemetry(this.hass?.states[src]?.attributes) : null; - if (!src || !tele) { - this._showToast(this._t('vac.cal_need_pos')); - return; - } - const mapId = this._vacMapId(d, tele); - const existing = d.marker?.vacuum?.calibration?.[mapId] as Affine | undefined; - const sp = this._spaceModelById(d.space); - if (!sp) return; - const vb = sp.vb as [number, number, number, number]; - const p = (existing && existing.length === 6 && fitFromMatrix(existing)) - || initialFit(tele.rooms, vb); - this._markerDialog = null; - if (d.space !== this._space && !this._commitSpace(d.space)) return; - this._vacFit = { markerId: d.id, source: src, mapId, p, drag: null }; + return this._editorRuntimeOrThrow()._vacStartFit(d); } private _vacFitSave(): void { - const f = this._vacFit; - if (!f) return; - // HP-1540-01: no success toast for a save that did not happen - const ok = this._vacSaveMatrix(f.markerId, f.source, f.mapId, fitMatrix(f.p)); - this._vacFit = null; - if (ok) this._showToast(this._t('vac.cal_done')); + return this._editorRuntimeOrThrow()._vacFitSave(); } /** Rotate/mirror around the ghost centre so the map does not fly away. */ private _vacFitTurn(patch: Partial): void { - const f = this._vacFit; - if (!f) return; - const tele = readVacTelemetry(this.hass?.states[f.source]?.attributes); - const c = this._vacGhostCentre(tele?.rooms || []); - const next = { ...f.p, ...patch } as FitParams; - this._vacFit = { ...f, p: reanchorFit(next, f.p, c[0], c[1]) }; + return this._editorRuntimeOrThrow()._vacFitTurn(patch); } private _vacGhostCentre(rooms: VacRoom[]): VacPt { - const xs: number[] = [], ys: number[] = []; - for (const r of rooms) { - xs.push(r.x0 ?? r.cx, r.x1 ?? r.cx); - ys.push(r.y0 ?? r.cy, r.y1 ?? r.cy); - } - if (!xs.length) return [0, 0]; - return [(Math.min(...xs) + Math.max(...xs)) / 2, (Math.min(...ys) + Math.max(...ys)) / 2]; + return this._editorRuntimeOrThrow()._vacGhostCentre(rooms); } /** px→canvas-units for a pointer delta, via the current stage size. */ private _vacDelta(view: { w: number; h: number }, dxPx: number, dyPx: number): VacPt { - const st = this._stageEl; - const w = st?.clientWidth || 1, h = st?.clientHeight || 1; - return [(dxPx / w) * view.w, (dyPx / h) * view.h]; + return this._editorRuntimeOrThrow()._vacDelta(view, dxPx, dyPx); } private _vacFitPointer(ev: PointerEvent, view: { x: number; y: number; w: number; h: number }): void { - const f = this._vacFit; - if (!f) return; - ev.stopPropagation(); - if (ev.type === 'pointerdown') { - const t = ev.target as HTMLElement; - const corner = t.getAttribute?.('data-corner'); - try { - (ev.currentTarget as HTMLElement).setPointerCapture?.(ev.pointerId); - } catch { /* synthetic pointers (tests) have no active id — capture is a nicety */ } - this._vacFit = { ...f, drag: corner - ? { kind: 'scale', sx: ev.clientX, sy: ev.clientY, p0: { ...f.p }, - fx: Number(corner.split(',')[0]), fy: Number(corner.split(',')[1]) } - : { kind: 'move', sx: ev.clientX, sy: ev.clientY, p0: { ...f.p }, fx: 0, fy: 0 } }; - return; - } - const d = f.drag; - if (!d) return; - if (ev.type === 'pointermove') { - const [dx, dy] = this._vacDelta(view, ev.clientX - d.sx, ev.clientY - d.sy); - if (d.kind === 'move') { - this._vacFit = { ...f, p: { ...d.p0, ox: d.p0.ox + dx, oy: d.p0.oy + dy } }; - } else { - // corner-stretch: uniform scale about the OPPOSITE corner (fx, fy — - // the fixed corner in robot coords), like a graphics-editor frame - const tele = readVacTelemetry(this.hass?.states[f.source]?.attributes); - const c = this._vacGhostCentre(tele?.rooms || []); - const m0 = fitMatrix(d.p0); - const [gx, gy] = applyAffine(m0, c[0], c[1]); - const [px0, py0] = applyAffine(m0, d.fx, d.fy); - const span0 = Math.hypot(gx - px0, gy - py0) || 1; - // distance change of the dragged corner (opposite of fixed) from centre - const [cx0, cy0] = [2 * gx - px0, 2 * gy - py0]; - const span1 = Math.hypot(cx0 + dx * 2 - px0, cy0 + dy * 2 - py0) / 2; - const k = Math.max(0.05, span1 / span0); - const next = { ...d.p0, s: d.p0.s * k } as FitParams; - this._vacFit = { ...f, p: reanchorFit(next, d.p0, d.fx, d.fy) }; - } - return; - } - if (ev.type === 'pointerup' || ev.type === 'pointercancel') { - this._vacFit = { ...f, drag: null }; - } + return this._editorRuntimeOrThrow()._vacFitPointer(ev, view); } /** The translucent robot map over the plan while fitting. */ private _renderVacFit(view: { x: number; y: number; w: number; h: number }): TemplateResult | typeof nothing { - const f = this._vacFit; - if (!f) return nothing; - const tele = readVacTelemetry(this._renderPlanHass?.states?.[f.source]?.attributes); - if (!tele) return nothing; - const m = fitMatrix(f.p); - const rects: TemplateResult[] = []; - const xs: number[] = [], ys: number[] = []; - for (const r of tele.rooms) { - if (r.x0 == null) continue; - const cs = [[r.x0!, r.y0!], [r.x1!, r.y0!], [r.x1!, r.y1!], [r.x0!, r.y1!]] - .map(([x, y]) => applyAffine(m, x, y)); - cs.forEach(([x, y]) => { xs.push(x); ys.push(y); }); - const [lx, ly] = applyAffine(m, r.cx, r.cy); - rects.push(svg` - ${r.name}`); - } - let dot: TemplateResult | typeof nothing = nothing; - if (tele.pos) { - const [dx2, dy2] = applyAffine(m, tele.pos.x, tele.pos.y); - dot = svg``; - } - // corner handles on the ghost bbox; data-corner carries the FIXED corner - // (the opposite one) in robot coordinates - const handles: TemplateResult[] = []; - if (xs.length) { - const inv = ((): ((x: number, y: number) => VacPt) => { - const det = m[0] * m[4] - m[1] * m[3]; - return (x, y) => [ - (m[4] * (x - m[2]) - m[1] * (y - m[5])) / det, - (-m[3] * (x - m[2]) + m[0] * (y - m[5])) / det, - ]; - })(); - const x0 = Math.min(...xs), x1 = Math.max(...xs); - const y0 = Math.min(...ys), y1 = Math.max(...ys); - // hit radius (finger-sized: these are grabbed on tablets) and the bead - // you actually see, a quarter of it — same split as .bdhandle/.bdknob - const r = view.w * 0.022; - const kr = r / 4; - for (const [hx, hy, ox2, oy2] of [[x0, y0, x1, y1], [x1, y0, x0, y1], [x1, y1, x0, y0], [x0, y1, x1, y0]] as number[][]) { - const fixed = inv(ox2, oy2); - handles.push(svg` - `); - } - } - return html` this._vacFitPointer(e, view)} - @pointermove=${(e: PointerEvent) => this._vacFitPointer(e, view)} - @pointerup=${(e: PointerEvent) => this._vacFitPointer(e, view)} - @pointercancel=${(e: PointerEvent) => this._vacFitPointer(e, view)}>${rects}${dot}${handles}`; + if (!this._editorRuntime) return nothing; + return this._editorRuntimeOrThrow()._renderVacFit(view); } /** Every frame: glue the growing tip segment to the animated puck centre. */ @@ -19416,232 +11897,79 @@ class HouseplanCard extends LitElement { } private _resetRoomDialogFields(): void { - this._roomEditId = null; - this._roomFill = ''; - this._roomCustomFill = null; - this._roomTempSrc = ''; - this._roomHumSrc = ''; - this._roomSrcOpen = null; - this._roomSrcFilter = ''; - this._roomNameScale = 1; - this._roomLabelScale = 1; + return this._editorRuntimeOrThrow()._resetRoomDialogFields(); } /** Open the room dialog for an EXISTING room (the gear on its card). */ private _openRoomEdit(r: RoomCfg): void { - if (!r.id) return; - this._roomEditId = r.id; - this._nameSel = r.name || ''; - this._areaSel = r.area || ''; - this._roomFill = r.settings?.fill_mode === 'glow' - ? '' - : ((r.settings?.fill_mode as any) || ''); - const rawCustom = r.settings?.custom_fill; - this._roomCustomFill = rawCustom && typeof rawCustom === 'object' - ? customFillOf(rawCustom, spaceDisplayOf(this._curSpaceCfg).customFill) - : null; - this._roomTempSrc = r.settings?.temp_source || ''; - this._roomHumSrc = r.settings?.hum_source || ''; - this._roomNameScale = clampScale(r.settings?.name_scale); - this._roomLabelScale = clampScale(r.settings?.label_scale); - this._roomSrcOpen = null; - this._roomSrcFilter = ''; - this._roomDialog = true; + return this._editorRuntimeOrThrow()._openRoomEdit(r); } /** Collect the room settings object from the dialog state (null = all inherited). */ private _roomSettingsFromDialog(): RoomCfg['settings'] { - const st: any = {}; - if (this._roomFill) st.fill_mode = this._roomFill; - if (this._roomCustomFill) st.custom_fill = this._roomCustomFill; - if (this._roomTempSrc) st.temp_source = this._roomTempSrc; - if (this._roomHumSrc) st.hum_source = this._roomHumSrc; - if (this._roomNameScale !== 1) st.name_scale = this._roomNameScale; - if (this._roomLabelScale !== 1) st.label_scale = this._roomLabelScale; - return Object.keys(st).length ? st : null; + return this._editorRuntimeOrThrow()._roomSettingsFromDialog(); } /** Save the room edited via the gear (name, area, tier-3 settings). */ private _saveRoomEdit(): void { - const sp = this._curSpaceCfg; - const room = sp?.rooms.find((x: any) => x.id === this._roomEditId); - if (!room) { - this._roomDialog = false; - this._roomEditId = null; - return; - } - room.name = this._nameSel.trim() || room.name; - room.area = this._areaSel || null; - // Preserve unknown/future room settings. If this dialog replaces a legacy - // fill_mode:'glow', materialize its effective Glow in the same write so a - // seemingly unrelated room edit cannot switch the light overlay off. - const previous = room.settings || {}; - const next: any = { ...previous }; - if (this._roomFill) next.fill_mode = this._roomFill; - else delete next.fill_mode; - if (this._roomCustomFill) next.custom_fill = this._roomCustomFill; - else delete next.custom_fill; - if (previous.fill_mode === 'glow' && typeof previous.glow !== 'boolean') next.glow = true; - if (this._roomTempSrc) next.temp_source = this._roomTempSrc; - else delete next.temp_source; - if (this._roomHumSrc) next.hum_source = this._roomHumSrc; - else delete next.hum_source; - if (this._roomNameScale !== 1) next.name_scale = this._roomNameScale; - else delete next.name_scale; - if (this._roomLabelScale !== 1) next.label_scale = this._roomLabelScale; - else delete next.label_scale; - if (Object.keys(next).length) room.settings = next; - else delete room.settings; - this._saveConfig(); - this._roomDialog = false; - this._roomEditId = null; - this._nameSel = ''; - this._areaSel = ''; - this._regSignature = ''; - this._maybeRebuildDevices(); - this.requestUpdate(); - this._showToast(this._t('toast.room_updated')); + return this._editorRuntimeOrThrow()._saveRoomEdit(); } /** Devices + sensor entities for the measurement-source picker. */ private _roomSrcCandidates(): { value: string; label: string; sub: string }[] { - const h = this._planHass; - const removed = removedPlanBindings(this._markers); - const q = this._roomSrcFilter.trim().toLowerCase(); - const list: { value: string; label: string; sub: string }[] = []; - for (const dev of Object.values(h.devices)) { - if (dev.entry_type === 'service') continue; - if (removed.devices.has(dev.id)) continue; - const name = (dev.name_by_user || dev.name || dev.id).trim(); - if (q && !name.toLowerCase().includes(q)) continue; - list.push({ value: 'device:' + dev.id, label: name, sub: dev.model || this._t('marker.sub_device') }); - } - for (const [eid, reg] of Object.entries(h.entities)) { - if (!eid.startsWith('sensor.') || reg.hidden) continue; - if (isRemovedPlanEntity(h, eid, removed)) continue; - const label = reg.name || h.states[eid]?.attributes?.friendly_name || eid; - if (q && !(label + ' ' + eid).toLowerCase().includes(q)) continue; - list.push({ value: 'entity:' + eid, label, sub: eid }); - } - list.sort((a, b) => a.label.localeCompare(b.label)); - return list.slice(0, 200); + return this._editorRuntimeOrThrow()._roomSrcCandidates(); } /** Human label of a picked measurement source. */ private _roomSrcLabel(src: string): string { - const i = src.indexOf(':'); - const k = src.slice(0, i); - const ref = src.slice(i + 1); - if (k === 'device') { - return this._fullRegistryHass.devices[ref]?.name_by_user - || this._fullRegistryHass.devices[ref]?.name || ref; - } - return this._fullRegistryHass.entities[ref]?.name - || this.hass.states[ref]?.attributes?.friendly_name || ref; + return this._editorRuntimeOrThrow()._roomSrcLabel(src); } /** Saved label position (layout key rl_) or the room center. */ private _labelPos(r: RoomCfg, spaceId: string): { x: number; y: number } { const saved = this._layout['rl_' + (r.id || '')]; - if (saved && saved.s === spaceId) { - return { x: saved.x * NORM_W, y: saved.y * NORM_W }; - } - // a label nobody has dragged sits at the room's CENTROID, which is not a - // node for an odd-sized or polygonal room — put it on the nearest one - const c = this._snap(this._roomCenter(r)); - return { x: c[0], y: c[1] }; + if (saved && saved.s === spaceId) return { x: saved.x * NORM_W, y: saved.y * NORM_W }; + const center = this._roomCenter(r); + const grid = this._gridPitch; + return { + x: clampCanvasR(snapToGrid(center[0], grid)), + y: clampCanvasR(snapToGrid(center[1], grid)), + }; } /** Room-name labels are dragged exactly like device icons (same layout store). */ private _labelDown(ev: PointerEvent, r: RoomCfg, spaceId: string): void { - if (this._mode !== 'plan') return; - ev.preventDefault(); - ev.stopPropagation(); - const p = this._labelPos(r, spaceId); - this._drag = { id: 'rl_' + (r.id || ''), sx: ev.clientX, sy: ev.clientY, ox: p.x, oy: p.y, moved: false }; - capturePointer(ev); - this._tip = null; + if (!this._editorRuntime) return; + return this._editorRuntimeOrThrow()._labelDown(ev, r, spaceId); } private _labelMove(ev: PointerEvent, r: RoomCfg, spaceId: string): void { - const id = 'rl_' + (r.id || ''); - if (!this._drag || this._drag.id !== id) return; - const stage = this._stageEl; - if (!stage) return; - const space = this._spaceModelById(spaceId); - if (!space) return; - const vb = space.vb; - const rect = stage.getBoundingClientRect(); - const v = this._viewOr(vb); - const dx = ((ev.clientX - this._drag.sx) / rect.width) * v.w; - const dy = ((ev.clientY - this._drag.sy) / rect.height) * v.h; - if (Math.abs(ev.clientX - this._drag.sx) + Math.abs(ev.clientY - this._drag.sy) > 3) this._drag.moved = true; - // DEV-B58-01, and worse than the marker's: this clamped to the space's - // STORED view_box, which for every existing plan is the old unit square. - // A room drawn at 2.5 had a name that could not be dragged to its own room. - const nx = clampCanvasR(this._drag.ox + dx); - const ny = clampCanvasR(this._drag.oy + dy); - this._savePos({ id, space: spaceId } as DevItem, nx, ny); + if (!this._editorRuntime) return; + return this._editorRuntimeOrThrow()._labelMove(ev, r, spaceId); } private _labelUp(r: RoomCfg): void { - const id = 'rl_' + (r.id || ''); - if (!this._drag || this._drag.id !== id) return; - const moved = this._drag.moved; - this._drag = moved ? this._drag : null; - if (moved) window.setTimeout(() => (this._drag = null), 0); + if (!this._editorRuntime) return; + return this._editorRuntimeOrThrow()._labelUp(r); } /** Saved room-card scale (layout key rl_, field k), clamped 0.5..3. */ private _labelScale(r: RoomCfg): number { - const k = (this._layout['rl_' + (r.id || '')] as any)?.k; - return typeof k === 'number' && Number.isFinite(k) ? Math.min(3, Math.max(0.5, k)) : 1; + const scale = (this._layout['rl_' + (r.id || '')] as any)?.k; + return typeof scale === 'number' && Number.isFinite(scale) + ? Math.min(3, Math.max(0.5, scale)) : 1; } private _rlResizeDown(ev: PointerEvent, r: RoomCfg, spaceId: string): void { - if (this._mode !== 'plan') return; - ev.preventDefault(); - ev.stopPropagation(); - const card = (ev.target as HTMLElement).closest('.roomlabel') as HTMLElement | null; - if (!card) return; - const b = card.getBoundingClientRect(); - const cx = b.left + b.width / 2; - const cy = b.top + b.height / 2; - const d0 = Math.max(8, Math.hypot(ev.clientX - cx, ev.clientY - cy)); - this._rlResize = { id: 'rl_' + (r.id || ''), space: spaceId, k0: this._labelScale(r), cx, cy, d0 }; - capturePointer(ev); + return this._editorRuntimeOrThrow()._rlResizeDown(ev, r, spaceId); } private _rlResizeMove(ev: PointerEvent): void { - const rs = this._rlResize; - if (!rs) return; - ev.stopPropagation(); - const dist = Math.max(8, Math.hypot(ev.clientX - rs.cx, ev.clientY - rs.cy)); - const k = Math.min(3, Math.max(0.5, rs.k0 * (dist / rs.d0))); - const rec: any = this._layout[rs.id]; - if (!rec) { - // the card was never dragged: pin its current default position first - const roomId = rs.id.slice(3); - const sp = this._spaceModelById(rs.space); - if (!sp) return; - const room = sp.rooms.find((x) => x.id === roomId); - if (!room) return; - const p = this._labelPos(room, rs.space); - - this._layout = { - ...this._layout, - [rs.id]: { s: rs.space, x: p.x / NORM_W, y: p.y / NORM_W, k }, - }; - } else { - this._layout = { ...this._layout, [rs.id]: { ...rec, k } }; - } - this._dirtyPos.add(rs.id); + return this._editorRuntimeOrThrow()._rlResizeMove(ev); } private _rlResizeUp(): void { - if (!this._rlResize) return; - this._rlResize = null; - this._persistLayout(); + return this._editorRuntimeOrThrow()._rlResizeUp(); } /** The room-settings button: detached from the (movable) label, always at @@ -19652,28 +11980,7 @@ class HouseplanCard extends LitElement { private _renderRoomGear( r: RoomCfg, space: SpaceModel, view: { x: number; y: number; w: number; h: number }, ): TemplateResult | typeof nothing { - if (!r.id) return nothing; - let c: number[] | null = null; - if (r.poly) { - // the VISUAL centre (largest inscribed circle) — interiorPoint only - // promises "inside", which sat visibly off-centre on an L-shaped room. - // The model is memoized, so the poly array is a stable cache key. - c = this._gearPtCache.get(r.poly) || null; - if (!c) { c = poleOfInaccessibility(r.poly); this._gearPtCache.set(r.poly, c); } - } else if (r.x != null && r.y != null) { - c = [r.x + (r.w || 0) / 2, r.y + (r.h || 0) / 2]; - } - if (!c) return nothing; - const left = ((c[0] - view.x) / view.w) * 100; - const top = ((c[1] - view.y) / view.h) * 100; - return html``; + return this._editorRuntimeOrThrow()._renderRoomGear(r, space, view); } private _renderRoomLabel( @@ -19857,102 +12164,24 @@ class HouseplanCard extends LitElement { /** Alignment candidates for the current context (owner's matrix). */ private _alignCandidates(): number[][] { - const out: number[][] = []; - const spm = this._spaceModel(); - if (this._markup) { - if (!spm) return out; - if (this._drag?.id.startsWith('rl_')) { - // room-card drag: centers of the OTHER room cards - const dragged = this._drag.id.slice(3); - for (const r of spm.rooms) { - if (!r.name || r.id === dragged) continue; - const p = this._labelPos(r, this._space); - out.push([p.x, p.y]); - } - return out; - } - // drawing: room vertices + current path/split points - for (const r of spm.rooms) { - const poly = roomPoly(r); - if (poly) for (const p of poly) out.push(p); - } - if (this._tool === 'draw') for (const p of this._path) out.push(p); - if (this._tool === 'split' && this._splitSel?.pts) for (const p of this._splitSel.pts) out.push(p); - return out; - } - if (this._mode === 'devices') { - // other icons of this space only (owner's decision) - for (const d of this._devices) { - if (d.space !== this._space || d.id === this._drag?.id - || d.bindingStatus?.kind === 'ha_disabled') continue; - const p = this._pos(d); - out.push([p.x, p.y]); - } - return out; - } - if (this._mode === 'decor') { - const movingId = this._decorMove?.id; - out.push(...this._decorSnapGeometry(movingId).points); - if (this._decorDraft) out.push(this._decorDraft.a); - return out; - } - return out; + return this._editorRuntimeOrThrow()._alignCandidates(); } private _renderAlignGuides(): TemplateResult { - const pt = this._alignPoint; - if (!pt) return svg`` as unknown as TemplateResult; - // exact node match for grid-snapped things; half a cell for free-moving cards - const tol = this._drag?.id.startsWith('rl_') ? this._gridPitch * 0.5 : this._gridPitch * 0.05; - const guides = alignGuides(pt, this._alignCandidates(), tol); - if (!guides.length) return svg`` as unknown as TemplateResult; - const g = this._gridPitch; - const over = g * 1.5; // extend a little past the point - return svg` - ${guides.map((gd: AlignGuide) => { - const [x1, y1, x2, y2] = gd.axis === 'x' - ? [gd.at, gd.from[1], gd.at, pt[1] + Math.sign(pt[1] - gd.from[1]) * over] - : [gd.from[0], gd.at, pt[0] + Math.sign(pt[0] - gd.from[0]) * over, gd.at]; - return svg` - `; - })} - ` as unknown as TemplateResult; + return this._editorRuntimeOrThrow()._renderAlignGuides(); } /** Perpendicular dashed tick through the wall's center while a dragged opening * sits exactly in the middle — same look as the alignment guides. Length is * about the wall stroke (2.5) × 6 to each side. */ private _renderOpeningCenterTick(gd: { x: number; y: number; angle: number }): TemplateResult { - const rad = ((gd.angle + 90) * Math.PI) / 180; - const half = gridVisualUnits(2.5 * 6, this._cellCm); - return svg`` as unknown as TemplateResult; + return this._editorRuntimeOrThrow()._renderOpeningCenterTick(gd); } /** Physical dimension segments for a NEW-opening placement preview (#238). * Existing-opening drag labels carry no `dimension` and render nothing here. */ private _renderOpeningDimensionGuides(measure: OpMeasure): TemplateResult { - const dimensions = measure.labels.flatMap((label) => label.dimension ? [label.dimension] : []); - if (!dimensions.length) return svg`` as unknown as TemplateResult; - const tickHalf = this._cssPxToRender(4); - return svg`` as unknown as TemplateResult; + return this._editorRuntimeOrThrow()._renderOpeningDimensionGuides(measure); } private _roomCenter(r: RoomCfg): number[] { @@ -20001,42 +12230,7 @@ class HouseplanCard extends LitElement { * its editor hitbox is still supplied by the shared metrics below. */ private _renderOpeningPlacementPreview(): TemplateResult { - const candidate = this._openingPreview; - if (!candidate) return svg``; - const visibleSpec: OpeningVisibleSpec = { - type: candidate.type, - length: candidate.renderedLength, - angle: candidate.angle, - amount: openingAmount(candidate.type, null), - flipH: candidate.flipH, - flipV: candidate.flipV, - base: 'var(--hp-open)', - tone: 'var(--hp-open)', - cellCm: this._cellCm, - gridPitch: this._gridPitch, - face: candidate.face, - }; - const passageGeometry = candidate.type === 'passage' - ? passagePlacementPreviewGeometry( - candidate, gridVisualUnits(this._gridPitch, this._cellCm), - ) - : null; - return svg` - ` as unknown as TemplateResult; + return this._editorRuntimeOrThrow()._renderOpeningPlacementPreview(); } private _renderOpenings(disp: SpaceDisplay): TemplateResult { @@ -20229,463 +12423,45 @@ class HouseplanCard extends LitElement { } private _renderOpeningDialog(): TemplateResult { - const d = this._openingDialog!; - const partitions = this._spaceModel()?.partitions || []; - const previous = d.id - ? this._curSpaceCfg?.openings?.find((item: OpeningCfg) => item.id === d.id) - : null; - const dialogOpening: OpeningCfg = { - id: d.id || 'preview', type: d.type, - x: d.x / NORM_W, y: d.y / this._spaceH, - angle: d.angle, - length: previous && !d.lengthTouched - ? previous.length - : this._cmToUnits(Math.max(20, d.lengthCm)) / NORM_W, - ...(d.host ? { host: d.host } : {}), - }; - const strict = partitionOpeningNeedsStrictValidation(previous, dialogOpening); - const hostResolution = d.host - ? strict - ? resolvePartitionOpeningStrict( - dialogOpening, partitions, NORM_W, this._cellCm, this._gridPitch, - ) - : resolvePartitionOpeningCompat( - dialogOpening, partitions, NORM_W, this._cellCm, this._gridPitch, - ) - : null; - const jambInvalid = hostResolution?.reason === 'does-not-fit-jamb'; - const hostPartition = d.host - ? partitions.find((partition) => partition.id === d.host!.id) - : null; - const jambDistance = hostPartition - ? formatLength(hostPartition.cm / 2, this._imperial) - : ''; - const orphan = !!d.host && !hostResolution?.resolved && !jambInvalid; - const icon = d.type === 'gate' ? 'mdi:gate' - : d.type === 'window' ? 'mdi:window-closed-variant' - : d.type === 'passage' ? 'mdi:arch' : 'mdi:door'; - const picker = (kind: 'contact' | 'lock', list: { value: string; label: string }[]) => { - const cur = kind === 'contact' ? d.contact : d.lock; - const open = kind === 'contact' ? !!d.contactOpen : !!d.lockOpen; - const filter = kind === 'contact' ? d.contactFilter || '' : d.lockFilter || ''; - const selected = list.find((candidate) => candidate.value === cur); - const selectedLabel = selected?.label - || this.hass.states[cur]?.attributes?.friendly_name - || this._fullRegistryHass.entities[cur]?.name - || cur; - const filtered = filterOpeningEntityCandidates(list, filter); - return html` - - ${open - ? html`
- this._filterOpeningEntities( - kind, (e.target as HTMLInputElement).value, - )} /> -
- - ${filtered.map((candidate) => html` - `)} - ${!filtered.length - ? html`
${this._t('marker.nothing_found')}
` - : nothing} -
-
` - : nothing}`; - }; - return html` (this._openingDialog = null)}> -
- ${d.host ? html` -
- ${orphan || jambInvalid ? html`` : nothing} - ${orphan - ? this._t('opening.partition_orphan') - : jambInvalid - ? this._t('opening.partition_jamb_margin', { distance: jambDistance }) - : this._t('opening.host_partition')} -
` : nothing} - - - - - - - - { - const n = strictNumber((e.target as HTMLInputElement).value); - if (n != null) this._openingDialog = { ...d, lengthCm: n, lengthTouched: true }; - }} /> - - ${d.type === 'passage' && (d.contact || d.lock) - ? html`
- - ${this._t('opening.passage_binding_warning')} -
` - : nothing} - - ${d.type !== 'passage' - ? html` - ${picker('contact', this._contactCandidates())} - ${d.contact - ? html`` - : nothing}` - : nothing} - - ${d.type === 'door' || d.type === 'gate' - ? html` - ${picker('lock', this._lockCandidates())}` - : nothing} - - ${d.type !== 'gate' && d.type !== 'passage' - ? html`` - : nothing} - ${d.type !== 'passage' - ? html`` - : nothing} -
- -
`; + return this._editorRuntimeOrThrow()._renderOpeningDialog(); } /** Adaptive grid density for the current view (docs/CANVAS.md §7): * which multiple of the drawing pitch is still legible on screen, and * which coarser one carries the accent dots. */ private _gridLevels(): { fine: number; coarse: number } | null { - const stage = this._stageEl; - const v = this._viewOr(this._baseVb()); - const px = stage && stage.clientWidth && v.w ? stage.clientWidth / v.w : 1; - return gridLevels(this._gridPitch, px); + return this._editorRuntimeOrThrow()._gridLevels(); } private _renderMarkupDefs(_vb: number[]): TemplateResult { - const lv = this._gridLevels(); - if (!lv) return svg`` as unknown as TemplateResult; - const g = this._gridPitch * lv.fine; - const G = this._gridPitch * lv.coarse; - const dotR = this._gridPitch * lv.fine * 0.14; - // Two patterns, CAD-style: the fine dots thin out as you zoom away - // (gridLevels drops whole decades of them) while every coarse node keeps - // a bigger, darker dot, so the eye never loses the scale reference. - return svg` - - - - - - - - - - - - - - `; + return this._editorRuntimeOrThrow()._renderMarkupDefs(_vb); } private _renderPhysicalEditorLayer(): TemplateResult { - const space = this._spaceModel(); - if (!space) return svg`` as unknown as TemplateResult; - const g = this._gridPitch; - const view = this._viewOr(this._baseVb()); - const stage = this._stageEl; - const unitsPerPx = stage?.clientWidth ? view.w / stage.clientWidth : g / 8; - const touchStroke = 24; - const handleR = Math.max(g * 0.22, unitsPerPx * 8); - const rotateHandleR = Math.max(handleR, unitsPerPx * 12); - const path = (poly: number[][]) => `M ${poly.map((p) => `${p[0]} ${p[1]}`).join(' L ')} Z`; - const selected = (kind: string, id: string) => - (this._physicalSel?.kind === kind && this._physicalSel.id === id) - || (kind === 'column' && this._duplicateColumnId === id); - const draftSegs = (space.room_drafts || []).flatMap((d) => d.points.slice(0, -1).map((a, i) => { - const b = d.points[i + 1]; - const storedCm = Number(d.segments[i]?.cm); - const depth = wallCmToUnits( - Number.isFinite(storedCm) ? storedCm : 15, this._cellCm, this._gridPitch, - ); - return svg` { - this._physicalSel = { kind: 'draft', id: d.id, segment: i }; - this._registerPhysicalTap('draft', d.id, i); - }}>`; - })); - const partitions = (space.partitions || []).map((p) => { - const body = partitionBody(p.a, p.b, p.cm, this._cellCm, this._gridPitch); - return body - ? svg` this._physicalDown(e, 'partition', p.id)} - @pointermove=${(e: PointerEvent) => this._physicalMove(e)} - @pointerup=${(e: PointerEvent) => this._physicalUp(e)}>` - : svg` this._physicalDown(e, 'partition', p.id)} - @pointermove=${(e: PointerEvent) => this._physicalMove(e)} - @pointerup=${(e: PointerEvent) => this._physicalUp(e)}>`; - }); - const columns = (space.wall_columns || []).map((c) => { - const shown = this._physicalRotate?.id === c.id - ? { ...c, angle: this._physicalRotate.angle } as WallColumnCfg : c; - const body = columnBody(shown, this._cellCm, this._gridPitch); - return svg` this._physicalDown(e, 'column', c.id)} - @pointermove=${(e: PointerEvent) => this._physicalMove(e)} - @pointerup=${(e: PointerEvent) => this._physicalUp(e)}>`; - }); - const drag = this._physicalDrag; - const ghost = (() => { - if (!drag?.moved) return nothing; - if (drag.kind === 'partition' && Number((drag.base as PartitionCfg).cm) === 0) { - const partition = drag.base as PartitionCfg; - return svg``; - } - const poly = drag.kind === 'partition' - ? partitionBody((drag.base as PartitionCfg).a, (drag.base as PartitionCfg).b, - (drag.base as PartitionCfg).cm, this._cellCm, this._gridPitch) - : columnBody(drag.base as WallColumnCfg, this._cellCm, this._gridPitch); - return poly ? svg`` : nothing; - })(); - const chrome = (() => { - const sel = this._physicalSel; - if (!sel) return nothing; - if (sel.kind === 'draft') { - const d = space.room_drafts.find((x) => x.id === sel.id); - if (!d) return nothing; - return svg` - p.join(',')).join(' ')}> - ${d.points.map((p) => svg``)} - `; - } - if (sel.kind === 'partition') { - const p = space.partitions.find((x) => x.id === sel.id); - if (!p) return nothing; - const body = partitionBody(p.a, p.b, p.cm, this._cellCm, this._gridPitch); - const mx = (p.a[0] + p.b[0]) / 2, my = (p.a[1] + p.b[1]) / 2; - return svg` - ${body - ? svg`` - : svg``} - - - - `; - } - const base = space.wall_columns.find((x) => x.id === sel.id); - if (!base) return nothing; - const c = this._physicalRotate?.id === base.id - ? { ...base, angle: this._physicalRotate.angle } as WallColumnCfg : base; - const body = columnBody(c, this._cellCm, this._gridPitch); - if (c.shape !== 'square') return svg` - - - `; - const size = wallCmToUnits(c.cm, this._cellCm, this._gridPitch); - const angle = canonicalColumnAngle(c.angle) * Math.PI / 180; - const ux = Math.sin(angle), uy = -Math.cos(angle); - const edge = [c.center[0] + ux * size / 2, c.center[1] + uy * size / 2]; - const handle = [edge[0] + ux * Math.max(g, unitsPerPx * 24), - edge[1] + uy * Math.max(g, unitsPerPx * 24)]; - return svg` - - - - this._physicalRotateDown(e, base)} - @pointermove=${(e: PointerEvent) => this._physicalRotateMove(e)} - @pointerup=${(e: PointerEvent) => this._physicalRotateUp(e)}> - `; - })(); - return svg`${draftSegs}${partitions}${columns}${ghost}${chrome}`; + return this._editorRuntimeOrThrow()._renderPhysicalEditorLayer(); } private _renderHiddenWallDiagnosticOverlay(): TemplateResult { - if (!this._markup) return svg`` as unknown as TemplateResult; - const geometry = this._hiddenWallDiagnosticSnapshot().value; - if (!geometry.segments.length) return svg`` as unknown as TemplateResult; - const radius = wallCmToUnits(5, this._cellCm, this._gridPitch); - return svg`` as unknown as TemplateResult; + return this._editorRuntimeOrThrow()._renderHiddenWallDiagnosticOverlay(); } private _renderPlanSnapOverlay(): TemplateResult { - if (!this._markup) { - return svg`` as unknown as TemplateResult; - } - const geometry = this._planSnapGeometrySnapshot().value; - const active = this._activePlanSnapCandidate; - const conflictKeys = new Set(this._activePlanSnapConflicts.map((item) => item.key)); - const staticRadius = wallCmToUnits(5, this._cellCm, this._gridPitch); - const activeRadius = wallCmToUnits(10, this._cellCm, this._gridPitch); - return svg`` as unknown as TemplateResult; + return this._editorRuntimeOrThrow()._renderPlanSnapOverlay(); } /** Update the only pointer-dependent node without scheduling a full card render. */ private _syncPlanSnapActiveMarker(candidate: PlanSnapCandidate | null): void { - const marker = this.renderRoot?.querySelector( - '[data-hp="plan-snap-active-marker"]', - ); - if (!marker) return; - marker.setAttribute('class', `plan-snap-node${candidate ? ' active' : ''}${ - candidate?.kind === 'line' ? ' dynamic' : '' - }`); - marker.setAttribute('data-active', candidate ? 'true' : 'false'); - marker.setAttribute('visibility', candidate ? 'visible' : 'hidden'); - if (!candidate) { - marker.removeAttribute('data-kind'); - marker.removeAttribute('data-key'); - return; - } - marker.setAttribute('data-kind', candidate.kind); - marker.setAttribute('data-key', candidate.key); - marker.setAttribute('cx', String(candidate.point[0])); - marker.setAttribute('cy', String(candidate.point[1])); - marker.setAttribute('r', String(wallCmToUnits(10, this._cellCm, this._gridPitch))); + return this._editorRuntimeOrThrow()._syncPlanSnapActiveMarker(candidate); } /** Pointer-only ambiguity styling without a full Lit render on large plans. */ private _syncPlanSnapConflictMarkers(conflicts: readonly PlanSnapEndpoint[]): void { - const keys = new Set(conflicts.map((item) => item.key)); - for (const marker of this.renderRoot?.querySelectorAll( - '.plan-snap-node[data-kind="endpoint"]', - ) || []) { - marker.setAttribute('class', `plan-snap-node${keys.has(marker.dataset.key || '') ? ' conflict' : ''}`); - } + return this._editorRuntimeOrThrow()._syncPlanSnapConflictMarkers(conflicts); } /** Physical depth for one immutable architectural snap segment. */ private _planSnapPhysicalSegment(segment: PlanSnapSegment): LinearWallSegment | null { - let cm = 0; - const space = this._spaceModel(); - if (!space) return null; - if (segment.sourceKind === 'partition') { - cm = Number(space.partitions.find((item) => item.id === segment.sourceId)?.cm) || 0; - } else if (segment.sourceKind === 'draft') { - const match = /^(.*):(\d+)$/.exec(segment.sourceId); - const draft = match ? space.room_drafts.find((item) => item.id === match[1]) : null; - cm = draft && match ? Number(draft.segments[Number(match[2])]?.cm) || 0 : 0; - } else { - cm = intervalCmAt( - space.rooms, this._spaceWalls, this._openCuts(), - [segment.a[0], segment.a[1], segment.b[0], segment.b[1]], - this._wallKeyPitch, this._cellCm, this._gridPitch, NORM_W, - ); - } - if (!(cm > 0)) return null; - return { - a: [...segment.a], b: [...segment.b], - halfDepth: wallCmToUnits(cm, this._cellCm, this._gridPitch) / 2, - }; + return this._editorRuntimeOrThrow()._planSnapPhysicalSegment(segment); } /** @@ -20696,110 +12472,11 @@ class HouseplanCard extends LitElement { private _drawPreviewJoinPatchD( points: number[][], halfDepths: number[], ): string { - if (points.length < 2) return ''; - const preview: LinearWallSegment[] = []; - for (let i = 0; i + 1 < points.length; i++) { - if (!(halfDepths[i] > 0)) continue; - preview.push({ a: points[i], b: points[i + 1], halfDepth: halfDepths[i] }); - } - if (!preview.length) return ''; - const eps = this._gridPitch * 0.0002; - const touching = this._planSnapGeometrySnapshot().value.segments - .filter((segment) => points.some((point) => distToSegment(point, [ - segment.a[0], segment.a[1], segment.b[0], segment.b[1], - ]) <= eps)) - .map((segment) => this._planSnapPhysicalSegment(segment)) - .filter((segment): segment is LinearWallSegment => !!segment); - const patches = linearWallJoinPatches([...preview, ...touching], eps); - return patches.map((patch) => - `M ${patch.map((point) => `${point[0]} ${point[1]}`).join(' L ')} Z`).join(' '); + return this._editorRuntimeOrThrow()._drawPreviewJoinPatchD(points, halfDepths); } private _renderMarkupLayer(vb: number[]): TemplateResult { - // Derived axes omit zero and positive-body stretches; their dedicated - // layers paint those segments with the correct style and z-order. - const openCuts = this._openCuts(); - const thickCuts = this._thickWallCuts(); - const allCuts = openCuts.concat(thickCuts); - const segs = allCuts.length - ? cutSegments(this._segments, allCuts, this._gridPitch * 0.02) - : this._segments; - const path = this._path; - const g = this._gridPitch; - const view = this._viewOr(this._baseVb()); - const drawCm = this._tool === 'draw' ? this._drawWallCm : null; - const previewPts = (() => { - if (this._tool !== 'draw' || !path.length || drawCm == null) return null; - if (this._contourClosed) return path; - if (this._cursorPt) return [...path, this._cursorPt]; - return path.length >= 2 ? path : null; - })(); - // Превью берёт толщины из того же резолвера, что и запись (#234). Раньше - // здесь была вторая формула, и расходились они ровно на пропуске: на экране - // текущее поле, в конфиге — 15 см. - const previewHalfDepths = previewPts - ? chainSegmentCms( - previewPts.length - 1, - this._contourClosed - ? [...this._draftSegmentCms, this._closingWallCm ?? undefined] - : this._draftSegmentCms, - drawCm, DRAW_WALL_DEFAULT_CM, - ).map((cm) => wallCmToUnits(cm, this._cellCm, this._gridPitch) / 2) - : []; - const previewD = previewPts - ? drawWallPreviewD( - previewPts, - wallCmToUnits(drawCm!, this._cellCm, this._gridPitch) / 2, - this._contourClosed, - previewHalfDepths, - ) - : ''; - const previewJoinPatchD = previewPts - ? this._drawPreviewJoinPatchD(previewPts, previewHalfDepths) - : ''; - const repairPreview = this._wallFaceBatch?.candidates[this._wallFaceBatch.index]?.repair - || this._wallRepairDiagnostic; - return svg` - ${this._gridLevels() - ? svg`` - : nothing} - ${segs.map((s) => svg``)} - ${this._renderPhysicalEditorLayer()} - ${this._tool === 'column' && this._cursorPt && this._drawWallCm - ? svg` { - const c: WallColumnCfg = { id: 'preview', shape: 'square', center: this._cursorPt!, cm: this._drawWallCm! }; - const body = columnBody(c, this._cellCm, this._gridPitch); - return `M ${body.map((p) => `${p[0]} ${p[1]}`).join(' L ')} Z`; - })()}>` - : nothing} - ${previewD - ? svg` - ` - : nothing} - ${previewPts && drawCm === 0 - ? svg` point.join(',')).join(' ')}>` - : nothing} - ${previewJoinPatchD - ? svg` - ` - : nothing} - ${repairPreview ? svg`` : nothing} - ${this._tool === 'split' && this._splitSel?.pts?.length - ? svg`${this._splitSel.pts.length > 1 - ? svg`` - : nothing} - ${this._splitSel.pts.map((p, i) => svg``)} - ${this._cursorPt - ? svg`` - : nothing}` - : nothing} - `; + return this._editorRuntimeOrThrow()._renderMarkupLayer(vb); } /** @@ -20812,378 +12489,31 @@ class HouseplanCard extends LitElement { * snap geometry keeps excluding the active draft (no self-snapping). */ private _renderActiveChainInk(): TemplateResult { - const path = this._path; - const g = this._gridPitch; - return svg` - ${path.length > 1 - ? svg`` - : nothing} - ${path.length && this._cursorPt && this._tool === 'draw' && !this._contourClosed - ? svg` - ${!this._activePlanSnapCandidate && !this._activePlanSnapConflicts.length - ? svg`` : nothing}` - : nothing} - ${path.map((p, i) => svg``)} - ` as unknown as TemplateResult; + return this._editorRuntimeOrThrow()._renderActiveChainInk(); } private _renderPartitionDeleteDialog(): TemplateResult { - const dialog = this._partitionDeleteDialog!; - const imperial = this.hass?.config?.unit_system?.length === 'mi'; - return html` (this._partitionDeleteDialog = null)}> -
-

${this._t('confirm.delete_partition_openings_body', { - count: dialog.openings.length, - })}

-
    - ${dialog.openings.map((opening) => html`
  • ${this._t( - 'confirm.delete_partition_openings_item', { - type: this._t(`opening.${opening.type}` as I18nKey), - length: formatLength( - (opening.length * NORM_W / this._gridPitch) * this._cellCm, - imperial, - ), - }, - )}
  • `)} -
-
-
- - - -
-
`; + return this._editorRuntimeOrThrow()._renderPartitionDeleteDialog(); } private _renderRoomDeleteDialog(): TemplateResult { - const dialog = this._roomDeleteDialog!; - return html` (this._roomDeleteDialog = null)}> -
-

${this._t('confirm.delete_room_body')}

-
-
- - - - -
-
`; + return this._editorRuntimeOrThrow()._renderRoomDeleteDialog(); } private _renderPhysicalDialog(): TemplateResult { - const d = this._physicalDialog!; - const column = d.kind === 'column'; - return html` (this._physicalDialog = null)}> -
- ${column ? html` - ` : nothing} - -
(this._physicalDialog = { - ...d, cm: (e.target as HTMLInputElement).value, - })} /> - ${this._t(this._imperial ? 'wallthick.unit_in' : 'wallthick.unit_cm')}
- ${column && d.shape === 'square' ? html` - - (this._physicalDialog = { - ...d, angle: (e.target as HTMLInputElement).value, - })} />` : nothing} - ${d.length ? html`
${this._t('physical.length')}: ${d.length}
` : nothing} -
- -
`; + return this._editorRuntimeOrThrow()._renderPhysicalDialog(); } private _renderMarkupBar(): TemplateResult { - const undoName = this._geometryHistory.undoName; - const redoName = this._geometryHistory.redoName; - const undoTitle = undoName - ? this._t('history.undo_named', { name: undoName }) - : this._t('history.undo_empty'); - const redoTitle = redoName - ? this._t('history.redo_named', { name: redoName }) - : this._t('history.redo_empty'); - return html`
-
- - - - - - - - - - ${this._editorToolbarGroups.map((group) => this._renderEditorGroupLauncher(group))} - - - - -
-
- -
-
`; + return this._editorRuntimeOrThrow()._renderMarkupBar(); } private _renderDevicesBar(): TemplateResult { - return html`
-
- - - - ${this._editorToolbarGroups.map((group) => this._renderEditorGroupLauncher(group))} -
-
- -
-
`; + return this._editorRuntimeOrThrow()._renderDevicesBar(); } private _renderDeviceInbox(): TemplateResult { - const dialog = this._deviceInbox!; - const rows = this._deviceInboxRows(); - const counts = Object.fromEntries((['on_plan', 'available', 'hidden', 'readd'] as const) - .map((category) => [category, rows.filter((row) => row.category === category).length])); - const filtered = filterDeviceInbox( - rows, dialog.tab, dialog.search, dialog.tab === 'on_plan' && dialog.onlyNew, - ); - const visible = filtered.slice(0, dialog.limit); - const tabLabel = (tab: DeviceInboxCategory) => this._t(`device_inbox.tab_${tab}` as any); - const emptyKey = `device_inbox.empty_${dialog.tab}` as any; - const openVirtual = () => { - this._deviceInboxReturn = { ...dialog }; - this._deviceInbox = null; - this._openMarkerDialog(); - if (!this._markerDialog) this._closeMarkerDialog(); - }; - return html` (this._deviceInbox = null)}> -
-
- (this._deviceInbox = { - ...dialog, search: (event.target as HTMLInputElement).value, limit: 100, - })} /> - -
-
- ${(['on_plan', 'available', 'hidden', 'readd'] as DeviceInboxCategory[]).map((tab) => html` - `)} -
-
- ${dialog.tab === 'on_plan' ? html`` : nothing} - ${dialog.tab === 'available' ? html`` : nothing} - -
-
- ${visible.length ? visible.map((row) => { - const primary = row.category === 'on_plan' - ? row.canFind ? html`` - : html`` - : row.category === 'hidden' - ? html`` - : html``; - const status = row.status.kind === 'active' ? '' - : this._t(`device_inbox.status_${row.status.kind}` as any); - return html`
- -
-
${row.name} - ${row.isNew ? html`${this._t('device_inbox.new' as any)}` : nothing} -
-
- ${[row.model, row.integration, row.spaceName, row.areaName].filter(Boolean).join(' · ')} -
-
- ${this._t(`device_inbox.reason_${row.reason}` as any)} - ${status ? html`${status}` : nothing} -
- ${row.binding} -
-
- ${primary} - ${row.canEdit || row.canHide || row.category === 'available' - || row.category === 'hidden' || this._bindingHasHaPage(row.binding) ? html` -
- - - -
- ${row.canEdit && !(row.category === 'on_plan' && !row.canFind) - ? html`` : nothing} - ${row.canHide ? html`` : nothing} - ${row.category === 'available' ? html`` : nothing} - ${row.category === 'hidden' ? html`` : nothing} - ${this._bindingHasHaPage(row.binding) ? html`` : nothing} -
-
` : nothing} -
-
`; - }) : html`
${this._t(emptyKey)}
`} -
- ${filtered.length > visible.length ? html`` : nothing} -
-
- -
-
`; + return this._editorRuntimeOrThrow()._renderDeviceInbox(); } /** Entities of a device worth CONTROLLING or reading, in a sensible order. */ @@ -21309,1390 +12639,133 @@ class HouseplanCard extends LitElement { private _markerValueBadgeFields( d: NonNullable, ): Pick | Record { - return valueBadgeWriteFields({ - touched: d.valueBadgeTouched, - originalHas: d.originalHasValueBadge, - original: d.originalValueBadge, - enabled: d.valueBadgeEnabled, - source: d.valueBadgeSource, - position: d.valueBadgePosition, - }); + return this._editorRuntimeOrThrow()._markerValueBadgeFields(d); } /** Convert the transactional dialog state into the marker that Save would write. */ private _markerDraft(d: NonNullable): Marker | null { - if (d.bindingMode === 'ha' && (!d.binding || d.binding === 'virtual')) return null; - const roomRef = parseRoomRef(d.room); - const id = markerIdForBinding(d.binding, d.devId, () => '__hp_device_preview__'); - const previous = this._markers.find((marker) => marker.id === id || marker.id === d.devId); - const controls = persistedExternalControls( - d.binding, d.controls, this._bindingEntities(d.binding), - ); - const effectiveTapAction = this._effectiveStoredTapAction(d); - const marker: Marker = { - id, - binding: d.binding, - name: d.name.trim() || null, - icon: d.icon || null, - display: d.display !== 'badge' ? d.display : null, - ripple_color: d.display === 'icon_ripple' && d.rippleColor ? d.rippleColor : null, - ripple_size: d.display === 'icon_ripple' && d.rippleSize !== 1.5 ? d.rippleSize : null, - size: d.size !== 1 ? d.size : null, - angle: d.angle || null, - ...this._markerTapActionFields(d), - ...this._markerToggleEntityFields(d), - tap_target: effectiveTapAction === 'run' ? d.tapTarget || null : null, - tap_confirm: d.tapConfirm ? true : null, - controls: controls.length ? controls : null, - ...this._markerLightFields(d), - ...this._markerValueBadgeFields(d), - use_climate_temp: d.useClimateTemp ? true : null, - glow_radius_cm: (() => { - const value = strictNumber(d.glowRadius); - if (value == null || value <= 0) return null; - return Math.round(this._imperial ? value * 30.48 : value * 100); - })(), - model: d.model.trim() || null, - link: d.link.trim() || null, - description: d.description.trim() || null, - pdfs: d.pdfs, - hidden: d.hideFromPlan, - vacuum: previous?.vacuum || null, - }; - if (d.binding === 'virtual' || d.room) { - marker.space = roomRef?.space || (d.binding === 'virtual' ? this._space : null); - marker.area = roomRef?.area || null; - marker.room_id = roomRef?.roomId || null; - } - return marker; + return this._editorRuntimeOrThrow()._markerDraft(d); } /** Runtime device for the unsaved marker, built by the production pipeline. */ private _markerPreviewDevice(d: NonNullable): DevItem | null { - const marker = this._markerDraft(d); - if (!marker) return null; - const key = `${this._haRegistry.revision}\n${contentFingerprint(this._markers)}\n${JSON.stringify(marker)}`; - if (this._markerPreviewMemo?.key === key) return this._markerPreviewMemo.device; - const device = deviceFromMarkerDraft({ - hass: this.hass, - registry: this._haRegistry, - areaToSpace: Object.fromEntries( - Object.entries(this._areaToSpace).map(([area, value]) => [area, value.space]), - ), - marker, - siblingMarkers: this._markers, - settings: this._settings, - excluded: this._excluded, - showAll: this._showAll, - firstSpaceId: this._model[0]?.id || this._space, - loc: (key) => this._t(key), - iconRules: this._iconRules, - }); - this._markerPreviewMemo = { key, device }; - return device; + return this._editorRuntimeOrThrow()._markerPreviewDevice(d); } /** Preserve array identity while the dialog draft and runtime roster stay * unchanged, allowing light/value resolvers to reuse their WeakMap caches. */ private _markerPreviewDevices(preview: DevItem): readonly DevItem[] { - const memo = this._markerPreviewDevicesMemo; - if (memo?.base === this._devices && memo.preview === preview) return memo.devices; - const devices = [...this._devices.filter((item) => item.id !== preview.id), preview]; - this._markerPreviewDevicesMemo = { base: this._devices, preview, devices }; - return devices; + return this._editorRuntimeOrThrow()._markerPreviewDevices(preview); } private _toggleIntent( device: DevItem, devices: readonly DevItem[] = this._devices, ): ResolvedToggleIntent | null { - return resolveToggleIntent({ - hass: this._planHass, - registryHass: this._fullRegistryHass, - devices, - device, - virtualLights: this._virtualLights, - }); + return this._editorRuntimeOrThrow()._toggleIntent(device, devices); } private _toggleIntentForDialog( d: NonNullable, ): ResolvedToggleIntent | null { - const preview = this._markerPreviewDevice(d); - if (!preview) return null; - const devices = this._markerPreviewDevices(preview); - return this._toggleIntent(preview, devices); + return this._editorRuntimeOrThrow()._toggleIntentForDialog(d); } private _toggleStateText(entityId: string, fallback: string): string { - const state = this._planHass?.states?.[entityId] || this.hass?.states?.[entityId]; - try { - return state && typeof this.hass?.formatEntityState === 'function' - ? this.hass.formatEntityState(state) : fallback; - } catch { - return fallback; - } + return this._editorRuntimeOrThrow()._toggleStateText(entityId, fallback); } /** Current-state copy is localized without teaching the dialog command semantics. */ private _toggleConfirmationStateText(target: ResolvedToggleTarget): string { - const raw = String(target.state || 'unknown'); - const formatted = target.entityId ? this._toggleStateText(target.entityId, raw) : raw; - if (formatted.trim().toLocaleLowerCase() !== raw.trim().toLocaleLowerCase()) return formatted; - const known: Partial> = { - on: 'confirm.state_on', - off: 'confirm.state_off', - open: 'confirm.state_open', - closed: 'confirm.state_closed', - opening: 'confirm.state_opening', - closing: 'confirm.state_closing', - unknown: 'confirm.state_unknown', - }; - const key = known[raw]; - if (key) return this._t(key); - // A future integration-specific token stays honest and readable even in a - // harness without HA's formatter; underscores are never exposed as UI. - return raw.replaceAll('_', ' ').replaceAll('-', ' '); + return this._editorRuntimeOrThrow()._toggleConfirmationStateText(target); } /** Snapshot lines shown by Toggle confirmation; execution still re-resolves later. */ private _toggleConfirmationLines(intent: ResolvedToggleIntent): string[] { - const effectKeys: Record, I18nKey> = { - 'turn-on': 'confirm.state_on', - 'turn-off': 'confirm.state_off', - open: 'confirm.state_open', - close: 'confirm.state_closed', - stop: 'confirm.state_stopped', - }; - return formatToggleConfirmation(intent, { - state: (target) => this._toggleConfirmationStateText(target), - current: (state) => this._t('confirm.current_state', { state }), - expected: (state) => this._t('confirm.expected_state', { state }), - groupCurrent: (on, total) => this._t('confirm.group_current', { on, total }), - groupAllOn: () => this._t('confirm.group_all_on'), - groupAllOff: () => this._t('confirm.group_all_off'), - unavailable: (count) => this._t('confirm.unavailable_targets', { count }), - effect: (effect) => this._t(effectKeys[effect]), - expectedByHa: () => this._t('confirm.expected_by_ha'), - }); + return this._editorRuntimeOrThrow()._toggleConfirmationLines(intent); } private _toggleHintLines(intent: ResolvedToggleIntent | null): string[] { - if (!intent) return []; - const effect = (value: ToggleNextEffect): string => - this._t((`marker.toggle_effect_${value.replace('-', '_')}`) as any); - const skipReason = (value: ToggleSkipReason): string => - this._t((`marker.toggle_skip_${value.replace('-', '_')}`) as any); - return formatToggleIntent(intent, { - single: (target) => { - if ('via' in target && target.via === 'virtual-light') { - return this._t('marker.virtual_light_target', { name: target.name }); - } - const entityId = target.entityId || ('ref' in target ? target.ref : ''); - const name = target.name || entityId; - return this._t('marker.toggle_hint_single', { name, id: entityId }); - }, - group: (targets) => this._t('marker.toggle_hint_group', { - count: targets.length, - names: targets.map((target) => `${target.name} (${target.entityId})`).join(', '), - }), - currentNext: (target, next) => target.via === 'virtual-light' - ? this._t('marker.virtual_light_current', { - state: this._t(target.state === 'on' - ? 'marker.virtual_light_state_on' : 'marker.virtual_light_state_off'), - effect: effect(next), - }) - : this._t('marker.toggle_hint_current', { - state: this._toggleStateText(target.entityId, target.state), - effect: effect(next), - }), - groupCurrentNext: (targets, next) => this._t('marker.toggle_hint_group_current', { - on: targets.filter((target) => target.state === 'on').length, - count: targets.length, - effect: effect(next), - }), - skipped: (targets) => this._t('marker.toggle_hint_skipped', { - count: targets.length, - targets: targets.map((target) => { - const id = target.entityId || target.ref; - return `${target.name || id} (${id}: ${skipReason(target.reason)})`; - }).join(', '), - }), - none: (reason: ToggleNoneReason) => - this._t((`marker.toggle_none_${reason.replaceAll('-', '_')}`) as any), - }); + return this._editorRuntimeOrThrow()._toggleHintLines(intent); } /** One projection for the untouched select, hints and draft transitions. */ private _effectiveStoredTapAction( d: NonNullable, primaryDomain?: string, ): string { - return d.tapActionTouched - ? d.tapAction - : projectedTapAction( - d.originalHasTapAction ? d.originalTapAction : null, - primaryDomain, - ); + return this._editorRuntimeOrThrow()._effectiveStoredTapAction(d, primaryDomain); } private _effectiveMarkerTapAction( d: NonNullable, preview = this._markerPreviewDevice(d), ): string { - return this._effectiveStoredTapAction(d, preview?.primary?.split('.')[0]); + return this._editorRuntimeOrThrow()._effectiveMarkerTapAction(d, preview); } /** Store one user-triggered announcement; live HA state ticks do not mutate it. */ private _announceToggleDraft( d: NonNullable, ): NonNullable { - const preview = this._markerPreviewDevice(d); - const next = !d.tapActionTouched - ? { ...d, tapAction: this._effectiveMarkerTapAction(d, preview) } - : d; - const text = next.tapAction === 'toggle' - ? this._toggleHintLines(this._toggleIntentForDialog(next)).join(' ') - : ''; - return { ...next, tapHintAnnouncement: text }; + return this._editorRuntimeOrThrow()._announceToggleDraft(d); } private _valueBadgeForBinding( d: NonNullable, binding: string, ): Pick, 'valueBadgeEnabled' | 'valueBadgeSource' | 'valueBadgeTouched'> { - const draft = { - ...d, binding, - valueBadgeEnabled: false, valueBadgeSource: null, valueBadgeTouched: true, - }; - const device = this._markerPreviewDevice(draft); - if (!device) return { valueBadgeEnabled: false, valueBadgeSource: null, valueBadgeTouched: true }; - const lightDevices = [...this._devices.filter((item) => item.id !== device.id), device]; - const candidates = valueBadgeCandidates(this._planHass, device, lightDevices); - const source = recommendedValueBadgeSource(this._planHass, device, candidates); - return { - valueBadgeEnabled: d.valueBadgeEnabled && !!source, - valueBadgeSource: source, - valueBadgeTouched: true, - }; + return this._editorRuntimeOrThrow()._valueBadgeForBinding(d, binding); } private _markerSpatialSource(d: NonNullable) { - const device = this._markerPreviewDevice(d); - if (!device) return null; - const preview = { ...device, hidden: false }; - return selectSpatialGlowSource(resolvedLightSources(this._planHass, [ - ...this._devices.filter((item) => item.id !== preview.id), preview, - ], null, this._virtualLights).filter((source) => source.device.id === preview.id)); + return this._editorRuntimeOrThrow()._markerSpatialSource(d); } private _markerAutoHasSpatialSource(d: NonNullable): boolean { - const autoDraft = { ...d, lightRole: 'auto' as const, lightRoleTouched: true }; - const device = this._markerPreviewDevice(autoDraft); - return !!device && hasOwnSpatialSource(this._planHass, { ...device, hidden: false }); + return this._editorRuntimeOrThrow()._markerAutoHasSpatialSource(d); } private _setMarkerLightRole(role: 'auto' | 'always' | 'never'): void { - const d = this._markerDialog; - if (!d) return; - this._markerDialog = { ...d, lightRole: role, lightRoleTouched: true }; + return this._editorRuntimeOrThrow()._setMarkerLightRole(role); } private _controlRefInfo(ref: string): { label: string; sub: string; icon: string; warning: boolean } { - if (!ref.startsWith('marker:')) { - return { - label: this.hass.states[ref]?.attributes?.friendly_name || ref, - sub: ref, - icon: ref.startsWith('light.') ? 'mdi:lightbulb' : 'mdi:toggle-switch', - warning: !this._planEntityAvailable(ref), - }; - } - const id = ref.slice('marker:'.length); - const marker = this._markers.find((item) => item.id === id); - const device = this._devices.find((item) => item.id === id); - if (!marker || marker.removed || marker.is_light !== true) { - return { - label: this._t('marker.control_missing_label'), - sub: `${this._t('marker.control_broken')} (${ref})`, - icon: 'mdi:alert-outline', warning: true, - }; - } - const space = this._serverCfg?.spaces.find((item: any) => item.id === (device?.space || marker.space)); - const stateful = !!device && ownControllableEntities(device).length > 0; - return { - label: marker.name || device?.name || id, - sub: [space?.title || device?.space || marker.space, marker.room_id || device?.area, - stateful ? '' : this._t('marker.control_passive')].filter(Boolean).join(' · '), - icon: marker.icon || device?.icon || 'mdi:lightbulb-outline', - warning: marker.hidden === true || device?.hidden === true, - }; + return this._editorRuntimeOrThrow()._controlRefInfo(ref); } private _valueBadgeCandidateLabel(candidate: ValueBadgeCandidate): string { - const source = candidate.source; - if (source.kind === 'derived_lqi') return this._t('marker.value_badge_lqi'); - if (source.kind === 'derived_marker_state') { - return this._t('marker.value_badge_marker_state', { name: candidate.label }); - } - if (source.kind === 'entity_state') { - return this._t('marker.value_badge_state', { name: candidate.label }); - } - const entityName = this.hass.states[source.entity_id]?.attributes?.friendly_name - || this._fullRegistryHass.entities[source.entity_id]?.name || source.entity_id; - return this._t((`marker.value_badge_attr_${source.attribute}`) as any, { name: entityName }); + return this._editorRuntimeOrThrow()._valueBadgeCandidateLabel(candidate); } private _controlCandidates(d: NonNullable): { value: string; label: string; sub: string; icon: string; }[] { - const currentId = this._markerDraft(d)?.id || d.devId || ''; - const plan: { value: string; label: string; sub: string; icon: string; search: string }[] = []; - const coveredEntities = new Set(); - for (const marker of this._markers) { - if (marker.id === currentId || marker.removed || marker.hidden || marker.is_light !== true) continue; - if (marker.binding !== 'virtual' && this._bindingStatus(marker.binding).kind !== 'active') continue; - const device = this._devices.find((item) => item.id === marker.id); - if (!device || device.hidden) continue; - const info = this._controlRefInfo(`marker:${marker.id}`); - for (const eid of ownControllableEntities(device)) coveredEntities.add(eid); - plan.push({ - value: `marker:${marker.id}`, label: info.label, sub: info.sub, icon: info.icon, - search: `${info.label} ${info.sub} ${marker.id} ${ownControllableEntities(device).join(' ')}`.toLowerCase(), - }); - } - const ha = Object.keys(this.hass.states || {}) - .filter((eid) => isControllable(eid) && !coveredEntities.has(eid) - && effectiveMarkerControls(d.binding, [eid], this._bindingEntities(d.binding)).length > 0 - && this._planEntityAvailable(eid)) - .map((eid) => { - const info = this._controlRefInfo(eid); - return { value: eid, label: info.label, sub: eid, icon: info.icon, - search: `${info.label} ${eid}`.toLowerCase() }; - }); - const q = d.controlsFilter.trim().toLowerCase(); - return [...plan, ...ha] - .filter((item) => !d.controls.includes(item.value) && (!q || item.search.includes(q))) - .slice(0, 12) - .map(({ search: _search, ...item }) => item); + return this._editorRuntimeOrThrow()._controlCandidates(d); } private _addControlRef(d: NonNullable, ref: string): void { - if (ref.startsWith('marker:')) { - const controllerId = this._markerDraft(d)?.id || d.devId || ''; - if (!controllerId) { - this._showToast(this._t('toast.marker_binding_required')); - return; - } - const targetId = ref.slice('marker:'.length); - const draft = this._markerDraft(d); - const graph = draft - ? [...this._markers.filter((marker) => marker.id !== controllerId), draft] - : this._markers; - if (markerControlWouldCycle(graph, controllerId, targetId)) { - this._showToast(this._t('toast.marker_control_cycle')); - return; - } - } - this._markerDialog = this._announceToggleDraft({ - ...d, controls: [...d.controls, ref], controlsFilter: '', - }); + return this._editorRuntimeOrThrow()._addControlRef(d, ref); } /** Switch modes without losing manual drafts; entering a manual mode snapshots live values once. */ private _setMarkerGlowMode(mode: 'auto' | 'color' | 'fixed'): void { - const d = this._markerDialog; - if (!d) return; - if (mode === 'auto') { - this._markerDialog = { ...d, glowMode: mode, glowTouched: true }; - return; - } - const source = this._markerSpatialSource(d); - const values = resolveGlowValues( - source ? this._planHass.states[source.eid] : undefined, - null, - this._fillColors.glow_light.c, - ); - const needColor = !d.glowColorDrafted; - const needBrightness = mode === 'fixed' && !d.glowBrightnessDrafted; - this._markerDialog = { - ...d, - glowMode: mode, - glowColor: needColor ? values.c : d.glowColor, - glowBrightness: needBrightness ? Math.max(1, Math.round(values.bri * 100)) : d.glowBrightness, - glowColorDrafted: true, - glowBrightnessDrafted: d.glowBrightnessDrafted || mode === 'fixed', - glowTouched: true, - }; + return this._editorRuntimeOrThrow()._setMarkerGlowMode(mode); } private _renderMarkerDialog(): TemplateResult { - const d = this._markerDialog!; - const isVirtual = d.bindingMode === 'virtual'; - const cands = this._bindingCandidates(); - const ownEntities = this._bindingEntities(d.binding); - const bindingStatus = isVirtual ? null : this._bindingStatus(d.binding); - const canOpenBindingInHa = !isVirtual && this._bindingHasHaPage(d.binding); - const previewDevice = this._markerPreviewDevice(d); - // Untouched defaults are projections, not stored values. Re-resolve the - // select from the current preview on every render: HA may reveal a more - // meaningful leading entity (most visibly `light.*`) after the dialog was - // opened. Runtime already uses that current primary, so keeping the stale - // draft value here made the select say "Device card" while a tap toggled - // the lamp. An explicit user choice remains authoritative and stable. - const effectiveTapAction = this._effectiveMarkerTapAction(d, previewDevice); - const previewSpaceDisplay = previewDevice - ? spaceDisplayOf(this._serverCfg?.spaces.find((space: any) => space.id === previewDevice.space)) - : null; - const previewLightDevices = previewDevice - ? this._markerPreviewDevices(previewDevice) - : this._devices; - const toggleIntent = effectiveTapAction === 'toggle' && previewDevice - ? this._toggleIntent(previewDevice, previewLightDevices) : null; - const toggleHintLines = this._toggleHintLines(toggleIntent); - const previewPresentation = previewDevice - ? resolveDevicePresentation(this._planHass, previewDevice, { - liveStates: this._config?.live_states !== false, - showTemperature: this._config?.show_temperature !== false, - showSignal: previewSpaceDisplay?.showLqi ?? (this._config?.show_signal !== false), - designPreview: true, - activityRuntime: this._activityRt.get(previewDevice.id), - lightDevices: previewLightDevices, - registryHass: this._fullRegistryHass, - reducedMotion: this._reducedMotion, - }) - : null; - const badgeCandidates = previewDevice - ? valueBadgeCandidates(this._planHass, previewDevice, previewLightDevices) : []; - const badgeRecommendation = previewDevice - ? recommendedValueBadgeSource(this._planHass, previewDevice, badgeCandidates) : null; - const effectiveBadgeEnabled = d.valueBadgeTouched - ? d.valueBadgeEnabled : !!previewPresentation?.valueBadge; - const effectiveBadgeSource = d.valueBadgeTouched - ? d.valueBadgeSource : previewPresentation?.valueBadge?.source || d.valueBadgeSource; - const effectiveBadgePosition = d.valueBadgeTouched - ? d.valueBadgePosition : previewPresentation?.valueBadge?.position || d.valueBadgePosition; - const badgeSourceKey = valueBadgeSourceKey(effectiveBadgeSource); - const badgeSourceMissing = !!effectiveBadgeSource - && !badgeCandidates.some((item) => item.key === badgeSourceKey); - const selectedBadgeCandidate = badgeCandidates.find((item) => item.key === badgeSourceKey); - const innerValueSourceKey = previewPresentation?.valueSource - ? previewPresentation.valueSource.attribute - ? `attr:${previewPresentation.valueSource.eid}:${previewPresentation.valueSource.attribute}` - : `state:${previewPresentation.valueSource.eid}` - : ''; - const autoHasSpatialSource = this._markerAutoHasSpatialSource(d); - const statefulSource = !!previewDevice - && hasOwnStatefulLightSource(this._planHass, { ...previewDevice, hidden: false }); - const lightSettings = resolveDeviceLightSettings( - d.lightRole, autoHasSpatialSource, statefulSource, d.glowMode, - ); - const glowSourceDisabled = !lightSettings.sourceExists; - const liveGlowDisabled = !lightSettings.fromSourceEnabled; - const passiveSource = lightSettings.passive; - const displayedGlowMode = lightSettings.effectiveMode; - const glowDisabledHint = d.lightRole === 'never' - ? this._t('marker.glow_disabled_never') - : d.lightRole === 'auto' && !autoHasSpatialSource - ? this._t('marker.glow_disabled_auto') - : passiveSource ? this._t('marker.glow_passive_hint') - : this._t('marker.glow_disabled_no_entity'); - const leadingEntities = previewDevice ? ownControllableEntities(previewDevice) : []; - // Keep the fallback text aligned with the production resolver. In - // particular, an entity binding or the resolved primary may precede the - // registry order used for the remaining candidates. - const effectiveLeading = previewDevice ? forcedLightEntityOf(previewDevice) || '' : ''; - const staleLeading = !!d.lightEntity && !leadingEntities.includes(d.lightEntity); - const toggleEntities = previewDevice ? toggleEntityCandidates(previewDevice) : []; - const staleToggleEntity = !!d.toggleEntity && !toggleEntities.includes(d.toggleEntity); - const automaticToggleIntent = effectiveTapAction === 'toggle' - ? this._toggleIntentForDialog({ ...d, toggleEntity: '', toggleEntityTouched: true }) - : null; - const automaticToggleTarget = automaticToggleIntent - ? [...automaticToggleIntent.targets, ...automaticToggleIntent.skippedTargets] - .map((target) => target.entityId || ('ref' in target ? target.ref : '')) - .filter(Boolean) - .join(', ') - : ''; - const curLabel = (() => { - if (isVirtual) return null; - const found = cands.find((c) => c.value === d.binding); - if (found) return found.label; - const [k, ref] = d.binding.split(':'); - if (k === 'device') return this._fullRegistryHass.devices[ref]?.name_by_user || this._fullRegistryHass.devices[ref]?.name || ref; - return this._fullRegistryHass.entities[ref]?.name || this.hass.states[ref]?.attributes?.friendly_name || ref; - })(); - return html` -
- ${bindingStatus?.kind === 'ha_disabled' - ? html`
- - ${this._t((`marker.ha_disabled_${bindingStatus.reason}`) as any)} - ${canOpenBindingInHa - ? html`` - : nothing} -
` - : bindingStatus?.kind === 'unverified' && !!d.binding - ? html`
- - ${this._t('marker.ha_registry_limited')} -
` - : nothing} - - (this._markerDialog = { ...d, name: (e.target as HTMLInputElement).value })} /> - - -
- -
- - -
- ${d.bindingMode === 'ha' - ? html` - ${d.bindingOpen - ? html`
- (this._markerDialog = { ...d, bindingFilter: (e.target as HTMLInputElement).value })} /> -
- ${cands.map( - (c) => html`
{ - const next = { - ...d, binding: c.value, bindingOpen: false, - controls: persistedExternalControls( - c.value, d.controls, this._bindingEntities(c.value), - ), - autoIcon: this._autoIconForBinding(c.value), - }; - this._markerDialog = this._announceToggleDraft({ - ...next, ...this._valueBadgeForBinding(next, c.value), - }); - }}> - ${c.label}${c.sub} -
`, - )} - ${!cands.length ? html`
${this._t('marker.nothing_found')}
` : nothing} -
-
` - : nothing}` - : nothing} -
- - - - - ${this._renderVacSection(d)} - - - - ${effectiveTapAction === 'toggle' - && (toggleEntities.length > 1 || staleToggleEntity) - ? html`
-
- - ${this._help('marker.toggle_entity.help')} -
- - ${staleToggleEntity ? html`

- - ${this._t('marker.toggle_entity_missing', { - entity: d.toggleEntity, - fallback: automaticToggleTarget || this._t('marker.toggle_entity_none'), - })} -

` : nothing} -
` - : nothing} - ${effectiveTapAction === 'toggle' - ? html`
- ${toggleHintLines.map((line) => html`
${line}
`)} -
-
${d.tapHintAnnouncement}
` - : nothing} - ${effectiveTapAction === 'run' - ? (() => { - const q = d.runFilter.trim().toLowerCase(); - const cands = this._runCandidates().filter( - (c) => !q || c.label.toLowerCase().includes(q) || c.value.includes(q), - ); - const cur = d.tapTarget ? this._runCandidates().find((c) => c.value === d.tapTarget) : null; - return html` - - ${d.tapTarget && !cur - ? html`
${this._t('marker.run_target_gone', { id: d.tapTarget })}
` - : nothing} - { (e.target as HTMLInputElement).select(); }} - @input=${(e: Event) => (this._markerDialog = { ...d, runFilter: (e.target as HTMLInputElement).value, tapTarget: '' })} /> - ${!cur - ? html`
- ${cands.slice(0, 40).map( - (c) => html`
(this._markerDialog = { ...d, tapTarget: c.value, runFilter: '' })}> - ${c.label}${c.sub} -
`, - )} - ${!cands.length ? html`
${this._t('marker.nothing_found')}
` : nothing} -
` - : nothing}`; - })() - : nothing} - ${effectiveTapAction === 'run' || effectiveTapAction === 'toggle' - ? html`` - : nothing} - - -
${this._t('marker.controls_hint')}
- ${d.controls.length - ? html`
- ${d.controls.map((eid) => { - const info = this._controlRefInfo(eid); - return html` - ${info.label} - - (this._markerDialog = this._announceToggleDraft({ - ...d, controls: d.controls.filter((x) => x !== eid), - }))}> - `; - })} -
` - : nothing} - (this._markerDialog = { ...d, controlsFilter: (e.target as HTMLInputElement).value })} /> - ${d.controlsFilter.trim() - ? html`
- ${this._controlCandidates(d).map((candidate) => html``)} -
` - : nothing} - - ${this._bindingHasClimate(d.binding) - ? html`` - : nothing} -
- ${this._t('marker.light_role_label')}${this._help('marker.light_role.help')} -
- - - -
-
- - ${d.lightRole === 'always' && (leadingEntities.length > 1 || staleLeading) - ? html`
-
- - ${this._help('marker.light_entity.help')} -
- - ${staleLeading ? html`

- - ${this._t('marker.light_entity_missing', { - entity: d.lightEntity, fallback: effectiveLeading || '—', - })} -

` : nothing} -
` - : nothing} - -
- ${this._t('marker.glow_color_label')}${this._help('marker.glow_mode.help')} -
- - - -
- ${displayedGlowMode !== 'auto' ? html`
- ) => { - this._markerDialog = { - ...d, glowMode: displayedGlowMode, - glowColor: e.detail.color, glowColorDrafted: true, glowTouched: true, - }; - }}> - ${displayedGlowMode === 'fixed' ? html` - ${this._t('marker.glow_brightness')} - ${this._rangeInput(1, 100, 1, d.glowBrightness, (n) => { - this._markerDialog = { - ...d, glowMode: displayedGlowMode, - glowBrightness: n, glowBrightnessDrafted: true, glowTouched: true, - }; - }, glowSourceDisabled, this._t('marker.glow_brightness'))} - ${Math.round(d.glowBrightness)}%` : nothing} -
` : nothing} -
-
-
- - ${this._help('marker.glow_radius.help')} -
-
- (this._markerDialog = { ...d, glowRadius: (e.target as HTMLInputElement).value })} /> - ${this._imperial ? this._t('gs.unit_ft') : this._t('gs.unit_m')} -
-
- ${glowSourceDisabled || passiveSource - ? html`

- ${glowDisabledHint} -

` - : nothing} - - - ${customElements.get('ha-icon-picker') - // Feed the effective icon to HA's picker so its field renders both - // the glyph and the mdi:* label. autoIcon is presentation-only: - // untouched dialogs still save d.icon as an empty auto override. - ? html` { - const icon = e.detail.value || ''; - // Some picker versions announce an assigned value. Do not - // let that turn the display-only auto icon into an override. - if (!d.icon && icon === d.autoIcon) return; - this._markerDialog = { ...d, icon }; - }}>` - : html` (this._markerDialog = { ...d, icon: (e.target as HTMLInputElement).value })} />`} - ${!d.icon && d.autoIcon - ? html`

- ${this._t('marker.icon_auto', { icon: d.autoIcon })} -

` - : nothing} - - - -

${this._t(DISPLAY_HINT_KEYS[d.display])}

- ${d.display === 'static_icon' && this._bindingHasAlarm(d.binding) - ? html`
- - ${this._t('marker.static_alarm_warning')} -
` - : nothing} -
- ${this._t('marker.value_badge_title')}${this._help('marker.value_badge.help')} - - ${d.display === 'static_icon' - ? html`

- ${this._t('marker.value_badge_static')} -

` - : !badgeCandidates.length && !d.valueBadgeSource - ? html`

- ${this._t('marker.value_badge_empty')} -

` - : nothing} - ${effectiveBadgeEnabled ? html` -
- - ${this._help('marker.value_badge_source.help')} -
- - ${selectedBadgeCandidate - ? html`

${selectedBadgeCandidate.technical}

` - : nothing} - ${badgeSourceMissing ? html`

- ${this._t('marker.value_badge_missing_hint')} -

` : nothing} - ${d.display === 'value' && badgeSourceKey === innerValueSourceKey - ? html`

- ${this._t('marker.value_badge_duplicate')} -

` : nothing} -
- - ${this._help('marker.value_badge_position.help')} -
- - ` : nothing} -
- ${previewPresentation - ? html` - ` - : html`
- - ${this._t('marker.preview.select_source')} -
`} - ${d.display === 'icon_ripple' - ? html`
- ) => { - this._markerDialog = { ...d, rippleColor: e.detail.color }; - }}> -
-
- ${this._t('marker.ripple_size')} - ${this._rangeInput(1, 8, 0.5, d.rippleSize, (n) => (this._markerDialog = { ...d, rippleSize: n }))} - ×${d.rippleSize} -
-

${this._t('marker.activity_alarm_note')}

` - : nothing} - - -
- ${this._rangeInput(0.5, 3, 0.1, d.size, (n) => (this._markerDialog = { ...d, size: n }))} - ×${d.size.toFixed(1)} - ${this._t('marker.angle_label')} - ${''/* 5 degrees, not 10 (owner 2026-08-03): a marker often has to - line up with a wall that is not on a 10-degree grid. */} - ${this._rangeInput(0, 355, 5, d.angle, (n) => (this._markerDialog = { ...d, angle: n }))} - ${d.angle}° -
- - - (this._markerDialog = { ...d, model: (e.target as HTMLInputElement).value })} /> - - - (this._markerDialog = { ...d, link: (e.target as HTMLInputElement).value })} /> - - - - - -
- ${d.pdfs.map( - (p) => html` - ${p.name} - this._removeMarkerPdf(p.url)}>`, - )} - - - this._pickMarkerFiles(e)} /> - -
-
-
-
- ${d.devId - ? html`` - : nothing} - ${d.devId - ? html`` - : nothing} -
-
- - -
-
-
`; + return this._editorRuntimeOrThrow()._renderMarkerDialog(); } private _renderSpaceDialog(): TemplateResult { - const d = this._spaceDialog!; - const progress = this._importTotal > 0 && d.mode === 'create' - ? this._t('import.progress', { - i: this._importTotal - this._importQueue.length, - n: this._importTotal, - }) - : ''; - const close = () => { - this._spaceDialog = null; - this._importQueue = []; - this._importTotal = 0; - }; - return html` -
- - (this._spaceDialog = { ...d, title: (e.target as HTMLInputElement).value })} /> - - - ${d.source === 'file' - ? html`
- ${d.planFile - ? html`${d.planFile.name}` - : d.planUrl - ? html`${this._t('space.plan_alt')}` - : html`${this._t('space.no_plan')}`} - - - this._pickPlanFile(e)} /> - - -
- ${d.pickSaved ? this._renderServerPlans(d) : nothing}` - : nothing} - - - -
- { - const raw = (e.target as HTMLInputElement).value; - const n = strictNumber(raw); - const canonical = n == null ? null : gridCellFieldToCm(n, this._imperial); - this._spaceDialog = { - ...d, - cellCmInput: raw, - cellCmTouched: true, - cellCm: canonical != null && canonical > 0 - ? Math.max(CELL_CM_MIN, Math.min(CELL_CM_MAX, canonical)) : d.cellCm, - }; - }} /> - ${this._t( - this._imperial ? 'space.scale_unit_imperial' : 'space.scale_unit', - )} -
- - - - - -
${this._t('space.zero_wall_help')}
- - - ${''/* the two "draw less" switches (owner 2026-08-05). They only - hide: the shapes and the openings stay in the config and - stay visible in the editor that owns them, so nothing is - lost and nothing becomes uneditable. */} - -
${this._t('space.hide_decor_tip')}
- -
${this._t('space.hide_openings_tip')}
- - ${([['labelTemp', 'space.label_temp'], ['labelHum', 'space.label_hum'], - ['labelLqi', 'space.label_lqi'], ['labelLight', 'space.label_light']] as const).map( - ([f, k]) => html``, - )} - -
- ${this._rangeInput(50, 300, 5, Math.round(d.cardFontScale * 100), (n) => (this._spaceDialog = { ...d, cardFontScale: n / 100 }))} - ${Math.round(d.cardFontScale * 100)}% -
- ${this._renderCardPreview(d.cardFontScale, 1, 1)} -
- ) => { - this._spaceDialog = { - ...d, roomColor: e.detail.color, roomOpacity: e.detail.opacity, - }; - }}> -
- - - ${(d.bgMode ?? bgModeOf(this._settings, {})) === 'static' - ? html`
- ) => { - this._spaceDialog = { ...d, bgColor: e.detail.color }; - }}> - ${d.bgColor - ? html`` - : html`${this._t('space.bg_inherited')}`} -
` - : nothing} - -
- { - const raw = (e.target as HTMLInputElement).value.trim(); - const n = raw === '' ? null : Math.round(Number(raw)); - this._spaceDialog = { ...d, northDeg: n !== null && Number.isFinite(n) ? Math.min(359, Math.max(0, n)) : null }; - }} /> - ${d.northDeg === null - ? this._t('space.north_inherited', { - v: northDegOf(this._settings, {}) === null ? '—' : String(northDegOf(this._settings, {})) + '°', - }) - : '°'} -
- - - - ${SPACE_FILL_UI_MODES.map((v) => [v, 'fill.' + v] as const).map( - ([v, k]) => html` - ${v === 'custom' && d.fillMode === 'custom' - ? html`
- ${this._t('space.custom_fill')} - ) => { - this._spaceDialog = { ...d, customFill: { c: e.detail.color, a: e.detail.opacity } }; - }}> - ${d.customFill - ? html`` - : nothing} -
` - : nothing}`, - )} - - ${d.deleteBlockers - ? html`` - : nothing} -
- -
`; + if (this._onboardingRuntime && this._spaceDialog + && this._spaceDialogUsesOnboardingRuntime(this._spaceDialog.mode)) { + return this._onboardingRuntime._renderSpaceDialog(); + } + return this._editorRuntimeOrThrow()._renderSpaceDialog(); } private _renderMergeDialog(): TemplateResult { - const d = this._mergeDialog!; - const rooms = this._spaceModel()?.rooms || []; - const opt = (id: string, key: 'a' | 'b') => { - const r = rooms.find((x) => x.id === id); - const area = r?.area ? this.hass.areas[r.area]?.name : null; - return html``; - }; - return html` (this._mergeDialog = null)}> -
-

${this._t('merge.hint')}

- - ${opt(d.aId, 'a')} - ${opt(d.bId, 'b')} -
-
- - - -
-
`; + return this._editorRuntimeOrThrow()._renderMergeDialog(); } /** Live sample of a room card at the given multipliers (dialog preview). */ @@ -22711,160 +12784,11 @@ class HouseplanCard extends LitElement { /** One measurement-source control (average vs an explicit device/entity). */ private _renderRoomSource(kind: 'temp' | 'hum'): TemplateResult { - const val = kind === 'temp' ? this._roomTempSrc : this._roomHumSrc; - const setVal = (v: string) => { - if (kind === 'temp') this._roomTempSrc = v; - else this._roomHumSrc = v; - this.requestUpdate(); - }; - const open = this._roomSrcOpen === kind; - return html` - - - - ${val || open - ? html` - ${open - ? html`
- { this._roomSrcFilter = (e.target as HTMLInputElement).value; this.requestUpdate(); }} /> -
- ${this._roomSrcCandidates().map( - (c) => html`
{ setVal(c.value); this._roomSrcOpen = null; }}> - ${c.label}${c.sub} -
`, - )} -
-
` - : nothing}` - : nothing}`; + return this._editorRuntimeOrThrow()._renderRoomSource(kind); } private _renderRoomDialog(): TemplateResult { - const edit = !!this._roomEditId; - const faceBatch = !edit ? this._wallFaceBatch : null; - const batchProgress = faceBatch && faceBatch.candidates.length > 1 - ? this._t('room.queue_progress', { - current: faceBatch.index + 1, total: faceBatch.candidates.length, - }) - : ''; - const canSaveNew = !!this._areaSel || !!this._nameSel.trim(); - const spaceDisplay = spaceDisplayOf(this._curSpaceCfg); - const effectiveFill = this._roomFill || spaceDisplay.fill; - const customFill = this._roomCustomFill || spaceDisplay.customFill; - // the free-areas list must include the edited room's CURRENT area - const areas = [...this._freeAreas]; - if (edit && this._areaSel && !areas.some((a) => a.area_id === this._areaSel)) { - const cur = this.hass.areas[this._areaSel]; - if (cur) areas.unshift(cur); - } - return html` -
- ${batchProgress ? html`

- ${batchProgress} -

` : nothing} - - (this._nameSel = (e.target as HTMLInputElement).value)} /> - - - - - - ${([['', 'fill.inherit'], ...ROOM_FILL_MODES.map((v) => [v, 'fill.' + v])] as const).map( - ([v, k]) => html``, - )} - ${effectiveFill === 'custom' - ? html`
- ${this._roomCustomFill - ? this._t('room.custom_fill_own') : this._t('room.custom_fill_space')} - ) => { - this._roomCustomFill = { c: e.detail.color, a: e.detail.opacity }; - }}> - ${this._roomCustomFill - ? html`` - : nothing} -
` - : nothing} - ${this._renderRoomSource('temp')} - ${this._renderRoomSource('hum')} - - - -
- ${this._rangeInput(50, 300, 5, Math.round(this._roomNameScale * 100), (n) => { this._roomNameScale = n / 100; this.requestUpdate(); })} - ${Math.round(this._roomNameScale * 100)}% -
- -
- ${this._rangeInput(50, 300, 5, Math.round(this._roomLabelScale * 100), (n) => { this._roomLabelScale = n / 100; this.requestUpdate(); })} - ${Math.round(this._roomLabelScale * 100)}% -
- ${this._renderCardPreview( - spaceDisplayOf(this._curSpaceCfg).cardFontScale, - this._roomNameScale, - this._roomLabelScale, - )} -
-
- - - ${edit - ? html`` - : html`${!this._pendingSplit ? html`` : nothing} - `} -
-
`; + return this._editorRuntimeOrThrow()._renderRoomDialog(); } static styles = [cardStyles, editorSecondaryStyles]; diff --git a/src/houseplan-editor-runtime.ts b/src/houseplan-editor-runtime.ts new file mode 100644 index 00000000..f85f2b5e --- /dev/null +++ b/src/houseplan-editor-runtime.ts @@ -0,0 +1,13010 @@ +/** + * House Plan Card — an interactive house plan as a native Lovelace card. + * Configuration sources: + * 1) SERVER (the houseplan integration, WS houseplan/config/get) — spaces, plans, + * rooms, device overrides, virtual devices. Coordinates are NORMALIZED (0..1). + * 2) LEGACY fallback — baked-in country-house data (src/data/*), coordinates in a 1489×1053 canvas. + * The icon layout is stored on the server (houseplan/layout/*), fallback — localStorage. + */ +import { LitElement, html, svg, nothing, TemplateResult, PropertyValues } from 'lit'; +import { guard } from 'lit/directives/guard.js'; +import { repeat } from 'lit/directives/repeat.js'; +import './hp-dialog'; +import type { HpDialog } from './hp-dialog'; +import './hp-color-opacity'; +import type { ColorPickerLabels } from './hp-color-opacity'; +import './hp-help'; +import './hp-device-preview'; +import { + EXCLUDED_DOMAINS, DEFAULT_ICON_RULES, compileIconRules, isValidPattern, iconFor, + type IconRule, type CompiledIconRule, +} from './rules'; +import { + lqiColor, snapToGrid, snapSegment45, samePoint, pointInPolygon, markerIdForBinding, + segmentCm, formatLength, roomEdges, roomPoly, paperRoomShapes, pointStrictlyInside, roomsOverlap, + pointOnBoundary, mergeRooms, splitRoomPath, polygonArea, closestPointOnBoundary, pointStrictlyInside as ptInside, islandsOf, sharedBoundary, distToSegment, outlineWithout, cutSegments, alignGuides, segmentAngle, is45, isExact45Vector, type AlignGuide, swipeTarget, clampScale, migratePdfUrls, roomFillModeOf, roomGlowOf, contentUrl, + snapToWall, snapPointAlongPoly, openingAmount, openingShoulders, interiorPoint, + isInteriorLightOpeningType, openingEntityReferences, filterOpeningEntityCandidates, + poleOfInaccessibility, subst, + averageLqi, fitView, declump, safeUrl, floorsOf, type FloorInfo, + stateIcon, lightColorOf, parseRoomRef, diffNewDevices, resolveGlowValues, resolveGlowAppearance, + glowAlpha, normalizeGlowColorOverride, isControllable, + spaceDisplayOf, resolveEffectiveRoomFill, fillColorsOf, DEFAULT_FILL_COLORS, + customFillOf, roomCustomFillOf, DEFAULT_CUSTOM_FILL, + type FillColors, type FillColorEntry, type ResolvedRoomFill, runServiceFor, RUN_TARGET_DOMAINS, + DEFAULT_ROOM_COLOR, DEFAULT_ROOM_OPACITY, stageBgOf, + DEFAULT_TEMP_MIN, DEFAULT_TEMP_MAX, type SpaceDisplay, + referencedContentUrls, + DISPLAY_MODES, TAP_ACTIONS, SPACE_FILL_UI_MODES, ROOM_FILL_MODES, + normalizeDeviceDisplay, isAlarmCapable, type DeviceDisplayMode, + liveText, liveTextReference, liveTextToken, hassValue, valueWithUnit, decorTextScale, decorTextLines, + DECOR_TEXT_BASE, +} from './logic'; +import { + resolveSafeResize, + coalesceResizeRooms, + polyIsSimple, areaM2, formatArea, MIN_ROOM_CM, + type SafeOpeningIn, type SafeResizeObstacle, type SafeResizeOptions, + type SafeResizePlan, type SafeResizeReason, type SafeResizeResolution, +} from './resize'; +import { + ResizeController, + type ResizeProjectionResult, +} from './resize-controller'; +import { + placeResizeAreaLabel, resizeMeasuredEdges, + type ResizeAreaPlacement, +} from './resize-labels'; +import { + computeSunRays, dayPhase, northDegOf, bgModeOf, sunRaysOn, + sunStateOf, rayPeakAlpha, raysVisible, rayColor, RAY_FADE_MS, type SunRay, + rayStops, resolveDayCycle, dayCycleFingerprint, type DayCycleState, + rayRimEdges, rimStops, rimPeakAlpha, RIM_COLOR, +} from './sun'; +import { dayCycleStageVars, renderDayCycleEnvironment } from './day-cycle-render'; +import { + FURNITURE_GROUPS, furnitureOfGroup, furnitureSymbol, furnitureDefaultCm, + furniturePathD, furnitureCorners, snapFurnitureToWall, + cmToNorm, clampFurnSize, clampFurnCm, FURN_WALL_CELLS, type FurnitureGroup, +} from './furniture'; +import { + degradeWalls, rekeyWallsAfterMoveChecked, wallRecordCarrierViolations, + setWallThickness, setWallThicknessForRoom, cmToField, wallCmToUnits, + multiWallNodesForGeometry, + wallEdgeBodies, wallBodiesGeometry, wallBodiesGeometryPath, wallBodiesUnionPath, + recutWallBodiesGeometry, + floorFootprintGeometry, + innerContourForRoom, roomWallProfile, outsetContour, + openingInnerFaceOffsetFromIndex, openingTunnelGeometriesFromIndex, + openingWallIndex as buildOpeningWallIndex, resolveOpeningWallAssociation, + applyWallThicknessToNewRoom, + drawWallPreviewD, linearWallJoinPatches, DRAW_WALL_DEFAULT_CM, + wallIntervals, materializeWallIntervals, + normalizeWallIntervals, + intervalCmAt, wallBodyNeedsSolid, wallHatchNeedsSolid, wallHatchStepUnits, + HATCH_BASE_STEP_UNITS, type OpeningTunnelGeometry, type OpeningWallIndex, + type LinearWallSegment, type WallEntry, type WallInterval, + innerEdgeSpan, ownEdgeOffsets, thicknessCmAt, +} from './wall-thickness'; +import { + checkNodeDistances, checkNodes, checkRoomClearance, checkSegmentLengths, + increasedViolations, type JunctionLimitViolation, type LimitSegment, +} from './junction-limits'; +import { + pointOnOpenCut, sanitizeOpenSpans, + type OpenSpanEntry, +} from './open-spans'; +import { ContentSigner } from './signing'; +import { + resolveFixedFloor, resolveInitialSpace, settleBestEffort, + type FixedFloorSelection, type InitialSpaceSelection, +} from './initial-load'; +import { selectActiveSpaceModel, selectSpaceModelById } from './space-model-selection'; +import { + createEmptySpaceConfig, initialSpaceDisplayDraft, switchSpacePlanSource, touchSpaceDisplay, +} from './space-dialog'; +import { mdiHomeCityOutline } from '@mdi/js'; +import { + Affine, applyAffine, readVacTelemetry, + autoCalibrate, pushTrailPoint, isVacMoving, vacTrailMode, vacMapIdWithFallback, + parseVacSourceCandidate, resolveVacSource, resolveCurrentVacPath, trimVacPathTarget, areaCentroid, + vacCalibrationResidualCm, vacRoomNameMatchCount, VAC_CALIBRATION_WARN_CM, + VAC_TELEPORT_GAP_MS, VAC_STALE_MS, + FitParams, fitMatrix, fitFromMatrix, initialFit, reanchorFit, VacRoom, + Pt as VacPt, type VacPath, type VacSourceCandidate, + type VacSourceResolution, type VacSourceStatus, +} from './vacuum'; +import { + buildDevices, deviceFromMarkerDraft, seedHiddenBindings, lqiFor, tempFor, humFor, climateTempFor, isHumEntity, + areaTemp, areaHum, sourceValue, areaClimateMap, + resolvedLightSources, resolvedLightState, resolvedLightStats, + hasOwnSpatialSource, hasOwnStatefulLightSource, ownControllableEntities, + forcedLightEntityOf, + resolveDeviceLightSettings, selectSpatialGlowSource, + resolvedDeviceStateEntities, removedPlanBindings, isRemovedPlanEntity, + deletePlanMarkerRecords, effectiveMarkerControls, persistedExternalControls, + removeMarkerControlReferences, rewriteMarkerControlReferences, markerControlWouldCycle, + resolveIcon, + type AreaClimate, +} from './devices'; +import { + bindingCandidates, buildDeviceInbox, filterDeviceInbox, + type DeviceInboxCategory, type DeviceInboxReason, type DeviceInboxRow, +} from './device-inbox'; +import { + formatToggleConfirmation, formatToggleIntent, projectedTapAction, resolveToggleIntent, + sameToggleOperationTargets, toggleCoverEntity, toggleIntentName, toggleOperation, + toggleEntityCandidates, unavailableToggleTargetNames, + type ResolvedToggleIntent, type ResolvedToggleTarget, + type ToggleNextEffect, type ToggleNoneReason, + type ToggleSkipReason, +} from './device-toggle'; +import { toggleEntityWriteFields } from './marker-toggle-entity'; +import { + adoptVirtualLightServerSnapshot, + applyVirtualLightEvent, + reconcileVirtualLightSnapshot, + virtualLightFingerprint, + virtualLightSnapshot, + virtualLightWire, + type VirtualLightSnapshot, +} from './virtual-light-state'; +import type { + OpeningCfg, PartitionOpeningHost, + RoomCfg, RoomDraftCfg, PartitionCfg, WallColumnCfg, + SpaceModel, PdfRef, Marker, ServerConfig, DevItem, CardConfig, + MarkerValueBadge, ValueBadgePosition, ValueBadgeSource, ZeroWallStyle, +} from './types'; +import { + COLUMN_MAX_CM, canonicalColumnAngle, clampColumnCm, columnBody, + directionalOccluders, floorMinusBodies, geometryArea, geometryOuterRings, + geometryAllRings, intersectionPaths, partitionBody, polyclipPathD, + pointInOpaquePlanBody, pointInPhysicalBody, sameColumnPlacement, + physicalBodyParts, + type PartitionOpeningCut, +} from './physical-geometry'; +import { + hostedOpeningIntervalsOverlap, materializePartitionOpening, + partitionOpeningJambMargin, partitionOpeningNeedsStrictValidation, + partitionOpeningFace, + partitionPlacementIntervals, resolvePartitionOpeningCompat, resolvePartitionOpeningStrict, + type PartitionOpeningOrphanReason, type ResolvedPartitionOpening, +} from './partition-openings'; +import { + buildHiddenWallDiagnosticGeometry, buildPlanSnapGeometry, + resolvePlanSnapResult, resolveStrictPlanSnap, + type HiddenWallDiagnosticGeometry, + type PlanSnapCandidate, type PlanSnapEndpoint, type PlanSnapGeometry, type PlanSnapSegment, +} from './plan-snap-overlay'; +import { + atomizeWallSegments, buildWallFaceGraph, findNewWallFacesInGraphs, findWallFaceAtPoint, + normalizeUnifiedWallTool, wallChainSegments, chainSegmentCms, + type WallFaceGraph, type WallGraphFace, type WallGraphSourceSegment, +} from './wall-face-graph'; +import { parameterOnPartition, planRoomDeletion } from './room-deletion'; +import { + planWallFaceRepair, repairMovesHostedPartition, type WallFaceRepairProposal, +} from './wall-face-repair'; +import { + LightSegment, polygonSegments, splitAtIntersections, visibilityPolygon, +} from './light-visibility'; +import './space-card'; +import { cardStyles } from './styles'; +import { editorSecondaryStyles } from './editor-secondary.styles'; +import { + EditorSecondaryController, + type EditorSecondaryCopy, + type EditorSecondaryModel, + type EditorToolbarGroup, +} from './editor-secondary'; +import { + fitInSquare, planRect, contentBounds, spaceModels, contentFrame, contentItems, spaceFrame, + spaceCenter, iconUnit, iconCqw, gridLevels, itemOf, snapPt, + MIN_ZOOM, PAN_SLACK, CANVAS_LIMIT, SANE_LIMIT, GRID_PITCH, GRID_STEP_N, + PLAN_SCALE_MIN, PLAN_SCALE_MAX, + clampCanvasR, clampCanvasN, type ContentItem, type Rect, +} from './space-geometry'; +import { optimizePlans, type OptimizeReport } from './plan-optimizer'; +import { + WALL_SEGMENT_MODEL_VERSION, + adoptWallSegmentModelCandidateInPlace, commitWallSegmentModel, + fixedTopologyWallLineageHints, sanitizeRoomDraftPath, + resolveRoomOpeningHost, wallModelOffGridValueCount, WallSegmentModelError, +} from './wall-segment-model'; +import { + legacyZeroContourLines, resolveZeroWalls, zeroContourLines, + zeroWallHasOpening, zeroWallStyleOf, +} from './zero-walls'; +import { snapNearAxisEndpoint } from './near-axis'; +import type { SpaceReferenceRepairContext } from './space-reference-repair'; +import { collectSpaceMarkerDependencies } from './space-deletion'; +import { + checkSpacePhysicalGeometry, + checkOptimizeGeometry, + geometryOpenCuts, + geometryOpenings, + geometryPartitionOpeningCuts, + geometryRoomOpeningInputs, + spacePhysicalGeometryFingerprint, + type GeometryOpeningProjection, + type OptimizeGeometryPreflightResult, +} from './plan-geometry-preflight'; +import { + canonicalizeConfigGeometry, + canonicalizeLayoutGeometry, + canonicalizePosition, + formatLatticeShiftCm, +} from './coordinate-canonicalization'; +import { enqueueSerializedWrite } from './serialized-write-queue'; +import { hasTranslation, langOf, t, type I18nKey } from './i18n'; +import { CommandStack } from './command-stack'; +import { resolvedSvgScreenBlend, svgScreenBlendSupported } from './glow-blend'; +import { + CONTINUITY_LONG_HIDDEN_MS, + VisualContinuityController, + contentFingerprint, + subscribePageVisibility, + visualFrameFingerprint, + type PageVisibilitySignal, +} from './visual-continuity'; +import { PointerModalityController } from './pointer-modality'; +import { + entityVisualSample, entityVisualSamplesForDevice, + type DeviceActivity, type DeviceVisualState, type EntityVisualSample, +} from './device-visual'; +import { + activitySourceSignature, deviceA11yState, presentationClasses, resolveDevicePresentation, + resolvePresentationSources, type ResolvedDevicePresentation, +} from './device-presentation'; +import { + ACTIVITY_WINDOW_MS, advanceFiniteActivity, createFiniteActivityRuntime, + resetFiniteActivityRuntime, stampFiniteActivity, type FiniteActivityRuntime, +} from './activity-runtime'; +import { + recommendedValueBadgeSource, valueBadgeCandidates, valueBadgeSourceFromKey, + valueBadgeSourceKey, valueBadgeTitle, valueBadgeWriteFields, type ValueBadgeCandidate, +} from './device-value-badge'; +import { + createRenderDeviceSnapshot, presentationSnapshotKey, renderDeviceSnapshotPositions, + type RenderDeviceSnapshot, +} from './render-device-snapshot'; +import { deviceFaceStyle, deviceThemeClass, renderDeviceFace } from './device-face'; +import { effectiveDeviceBaseSize } from './device-marker-geometry'; +import { + ModeTransitionController, viewportFromViewBox, + type HouseplanMode, type ModeTransitionState, type ModeVisualState, type ModeViewBox, +} from './mode-transition'; +import { + currentLabs, hashSpace, noteLabsRender, subscribeLabs, type LabsSnapshot, +} from './labs'; +import { + ISO_CAMERA, ISO_FLOOR_EDGE_HEIGHT, ISO_WALL_HEIGHT, isoFloorMatrixCss, + projectPlanPoint, projectedFrame, + unprojectFloorPoint, type ScenePoint, type ViewRect, +} from './iso-projection'; +import { + buildIsoFloorGeometry, buildIsoWallGeometry, isoEffectiveView, isoGeometryFingerprint, + type IsoFloorGeometry, type IsoWallGeometry, +} from './iso-walls'; +import { + buildIsoOpeningBasis, isoOpeningBounds, projectIsoOpening, resolveIsoDecoration, + type IsoDecorationLayers, type IsoOpeningBasis, type IsoOpeningPanel, +} from './iso-openings'; +import { + acquireHaRegistries, activeRegistryHass, cacheHaBindingStatuses, + fullRegistryHass, haRegistryBuildSignature, haRegistryDiagnostics, haRegistrySnapshot, + openingEntityAvailable, refreshHaRegistries, renderOpeningEntityAvailable, + resolveHaBindingStatus, + type HaBindingStatus, type HaRegistrySnapshot, +} from './ha-binding-status'; +import type { DecorShape, DecorStyle } from './editors/decor/types'; +import { + DEFAULT_DECOR_STYLE, boxAnchors, boxCorners, clamp01, decorCmToUnits, + decorStrokeUnits, decorStyleOf, decorStylePatch, + decorUnitsToCm, mergeSnapGeometry, normalizeAngle, resizeDecorBox, + resizedBoxTopLeft, snapDecorPoint, validDecorDraft, + type DecorBox, type SnapGeometry, +} from './editors/decor/geometry'; +import { renderOpeningTunnelFills } from './render/opening-tunnels'; +import { + openingVisibleMetrics, renderOpeningVisibleGeometry, + type OpeningFaceOffset, type OpeningVisibleSpec, +} from './render/opening-symbol'; +import { + openingDefaultLengthCm, openingPlacementPreset, passagePlacementPreviewGeometry, + resolveOpeningPlacementResult, sameOpeningPlacementInput, + type OpeningPlacementCore, type OpeningPlacementPreset, type OpeningPlacementType, +} from './opening-placement'; +import { + buildOpeningDimensionContext, resolveOpeningDimensions, + type OpeningDimension, type OpeningDimensionContext, +} from './opening-dimensions'; +import { safeStoredColor } from './color'; +import { + gridCellFieldToCm, gridCellFieldValue, gridVisualScale, gridVisualUnits, + newSpaceCellCm, wallThickHoverHalfUnits, +} from './grid-scale'; +import { + applySpaceOrder, canStartTabDrag, markersNeedingPlacement, passedDragThreshold, + reorderSpaceIds, +} from './space-order'; +import { applyOpeningMoves, mergeCollinearPartitions, spaceMergeGeometry } from './wall-merge'; + +const CARD_VERSION = '1.68.1'; + +type ResizeLiveLabel = { + kind: 'length'; + x: number; + y: number; + text: string; + edge: { a: [number, number]; b: [number, number] }; +} | { + kind: 'area'; + roomId: string; + x: number; + y: number; + text: string; + placement: ResizeAreaPlacement; +}; +type ResizePreview = { space: string; sp: any }; +type ResizeWallUnion = ReturnType; +type ResizeWallArtifact = ReturnType; +const DISPLAY_LABEL_KEYS: Record = { + badge: 'display.badge', + icon_ripple: 'display.icon_ripple', + value: 'display.value', + static_icon: 'display.static_icon', +}; +const DISPLAY_HINT_KEYS: Record = { + badge: 'marker.display_hint_badge', + icon_ripple: 'marker.display_hint_icon_ripple', + value: 'marker.display_hint_value', + static_icon: 'marker.display_hint_static_icon', +}; +/** Keeps every previously valid scale at the maximum 20 cm grid scale lossless. */ +const DECOR_TEXT_CM_MAX = 2000; +const CELL_CM_MIN = 0.1; +const CELL_CM_MAX = 1000; +/** HP-1552 boot-veil timing (AUD-1552-02). The veil holds for at least + * BOOT_MIN_MS; every stage-height change restarts a BOOT_QUIET_MS + * trailing-quiescence requirement (chrome still settling near the cap + * extends the wait); BOOT_MAX_MS lifts the veil unconditionally. + * BOOT_MIN_MS exceeds the old 600 ms window by a frame-latency margin: a + * panel applied right at the window's edge (~590 ms) only materializes in + * the stage height a couple of rAF/render frames later. */ +const BOOT_MIN_MS = 700; +const BOOT_QUIET_MS = 250; +const BOOT_MAX_MS = 1200; +/** AUD-1552-02: post-reveal grace during which late chrome shifts glide + * (CSS height transition on the stage) instead of snapping. */ +const BOOT_SOFT_MS = 1500; +/** Numeric editor fields must consume the whole value: `50abc` is invalid, + * not a surprisingly accepted 50. Decimal comma remains supported. */ +const strictNumber = (value: string): number | null => { + const text = String(value ?? '').trim().replace(',', '.'); + if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(text)) return null; + const parsed = Number(text); + return Number.isFinite(parsed) ? parsed : null; +}; + +type LruRead = { hit: true; value: V } | { hit: false }; +const lruRead = (cache: Map, key: K): LruRead => { + if (!cache.has(key)) return { hit: false }; + const value = cache.get(key)!; + cache.delete(key); + cache.set(key, value); + return { hit: true, value }; +}; +const lruWrite = (cache: Map, key: K, value: V, limit: number): void => { + cache.delete(key); + cache.set(key, value); + while (cache.size > limit) { + const oldest = cache.keys().next().value as K | undefined; + if (oldest === undefined) break; + cache.delete(oldest); + } +}; +/** DEV-B703-01: warm re-mount memo — MODULE scope, so it lives with the loaded + * PAGE, not with any card instance. Lovelace re-creates card elements when + * the websocket reconnects after a long-backgrounded tab; the fresh instance + * used to run the whole first-open boot (veil + BOOT_MIN_MS + quiescence), + * which reads as «план перезагрузился», even though the page (and HA's + * chrome) never went anywhere. Within one loaded page the chrome IS already + * settled, so the geometry the previous instance settled at is still valid: + * remember it per (viewport size × card config) and let the next instance + * open instantly in the final geometry, no veil at all. A window resize + * between instances changes the key → miss → the full protective boot (the + * only case where the chrome may genuinely re-settle). The config part of + * the key keeps two DIFFERENT cards on one page from adopting each other's + * header height (same config twice on one view is indistinguishable — and + * then the heights match anyway). */ +/** DEV-B703-03 — what the dead instance was LOOKING at. The header height + * alone was not enough: `_view` (the pan) never left the instance, and the + * EDITOR zoom is deliberately not persisted (`_saveZoom` is view-only) while + * the editor MODE is (LS_NAV). So a re-mount inside an editor came back at + * the view-mode zoom, and a panned view came back re-centred on the plan — + * the owner's «чуть-чуть дёргается масштаб». The memo now carries the whole + * viewport, so the restore is the same rect, not the same zoom number. */ +type WarmViewport = { + space: string; + mode: 'view' | 'plan' | 'devices' | 'decor'; + projection: 'flat' | 'iso'; + activeLabsIso: boolean; + logicalCenter: { x: number; y: number } | null; + zoom: number; + view: { x: number; y: number; w: number; h: number } | null; + /** the view-mode viewport an editor was entered from (_viewModeSnap) */ + snap: { space: string; zoom: number; cx?: number; cy?: number } | null; + tool: MarkupTool; + decorTool: DecorTool; + showHidden: boolean; + /** «показать дальние» changes _baseVb, so the restored view rect is only + * the same rect if the frame it was clamped against is the same one */ + showFar: boolean; + selId: string | null; + rszSel: string | null; + decorSel: string | null; +}; +const expiredWarmViewport = (vp: WarmViewport | null): WarmViewport | null => { + if (!vp || vp.mode === 'view') return vp; + return { + ...vp, + mode: 'view', + zoom: vp.snap?.space === vp.space ? vp.snap.zoom : vp.zoom, + view: null, + snap: null, + tool: 'draw', + decorTool: 'select', + showHidden: false, + selId: null, + rszSel: null, + decorSel: null, + }; +}; +/** Which dialog was open and its draft (docs/WARM-REMOUNT.md §3). `data` is + * the live draft OBJECT — the memo is module state, never serialised, so a + * half-filled device dialog with its uploaded pdfs survives for free. */ +type WarmDialogKind = 'space' | 'marker' | 'settings' | 'opening' | 'decorText' | 'decorShape' | 'backdrop' | 'rules' | 'room' | 'info' | 'openingInfo'; +type WarmDialog = { kind: WarmDialogKind; space: string; mode: string; data: any }; +/** AUD-159B1-01: one entry per CARD PLACEMENT, not per key. Two cards with an + * identical config on one view share the key, so the key alone cannot say + * whose viewport this is; `place`/`idx` (the parent element the card was + * mounted in, and its position among that parent's children) identify the + * DOM slot, and `owner` the live instance sitting in it. A re-mount into the + * same slot inherits the entry; a different card never does. */ +type WarmEntry = { + /** generation id of the instance that currently owns the slot */ + owner: number; + /** HA route where this placement was captured. */ + path: string; + /** the parent element the owner was mounted in (weak — never keep DOM alive) */ + place: WeakRef | null; + /** the owner's index among that parent's children */ + idx: number; + /** the owner is attached; a dead slot is a tombstone waiting for a successor */ + live: boolean; + hdrH: number; + stageH: number; + vp: WarmViewport | null; + /** Last complete visual frame for #73; safe only inside the same placement. */ + frameFingerprint: string; + /** Metadata projection is immutable-by-replacement and avoids an empty + * device layer on a same-document warm remount before registry refresh. */ + devices: readonly DevItem[] | null; + dlg: WarmDialog | null; + /** when the instance that wrote `dlg` detached; 0 = it is still alive */ + freed: number; + /** the TTL timer that frees `dlg` once it can no longer be revived */ + evict: number; +}; +const warmBoot = new Map(); +let warmGen = 0; +/** `location.pathname` is the dashboard AND the view path: two Lovelace views + * never share a key, so a card that comes back on another view boots cold + * instead of inheriting a stranger's viewport (AUD-159B1-01). The hash is + * deliberately out — `#space=` is OUR deep link, not another placement. */ +const warmBootKey = (config: unknown): string => + `${window.innerWidth}x${window.innerHeight}|${location.pathname}|${JSON.stringify(config ?? {})}`; +/** A dialog is revived only if the instance that owned it died THIS long ago. + * A Lovelace rebuild detaches and re-attaches within one task; a user who + * walked off to another dashboard view and came back later must not be met + * by a dialog they have long forgotten opening. */ +let WARM_REVIVE_MS = 10000; +/** The memo gains a key on every window RESIZE and never loses one; the + * values used to be two numbers, and now they can hold a dialog draft (a + * plan pdf among it). Keep the last few viewports — a stale entry only + * costs the next card at that size a cold boot. */ +const WARM_MAX_KEYS = 8; +/** How many placements of ONE key are remembered. More identical cards than + * this on one view and the oldest dead slot is dropped — it only costs that + * placement a cold boot. */ +const WARM_MAX_SLOTS = 4; + +/** Which slot of `list` belongs to the instance now claiming it, and may we + * trust it with the viewport/dialog (`sure`) or only with the settled height? + * Ranked, best first (AUD-159B1-01): + * 4 — same parent, same index: literally the DOM slot we are standing in, + * which is how Lovelace replaces a card (the predecessor may still be + * attached for another task — that is the case the audit reproduced); + * 0 — some OTHER placement whose owner is still attached: a neighbouring + * card with an identical config, never ours to inherit; + * 3 — same parent, shifted index, owner gone: still our placement; + * 2 — a tombstone whose placement is gone with its subtree — the ordinary + * Lovelace rebuild, where the container is rebuilt too. + * A tie means two candidates are equally plausible: then only the settled + * height is adopted, and it is the same for all of them anyway. */ +const warmMatch = ( + list: WarmEntry[], + gen: number, + place: Node | null, + idx: number, +): { slot: WarmEntry | null; sure: boolean } => { + const score = (s: WarmEntry): number => { + const same = !!place && s.place?.deref() === place; + if (same && s.idx === idx) return 4; + if (s.live) return 0; + return same ? 3 : 2; + }; + let best: WarmEntry | null = null; + let bestScore = 0; + let ties = 0; + let newest: WarmEntry | null = null; + for (const s of list) { + if (s.owner === gen) continue; + newest = s; + const sc = score(s); + if (sc <= 0) continue; + if (sc > bestScore) { best = s; bestScore = sc; ties = 1; } + else if (sc === bestScore) ties++; + } + if (!best || ties > 1) return { slot: best || newest, sure: false }; + return { slot: best, sure: true }; +}; +/** Rotation step of a decor text block — the same 5° a device icon turns in + * (marker dialog). Shift affects angle precision only, never position. */ +const DT_ANGLE_STEP = 5; +/** Line spacing of a multi-line label, in font sizes. */ +const DT_LINE = 1.2; +const LS_KEY = 'houseplan_card_layout_v1'; +const LS_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering +const LS_ZOOM = 'houseplan_card_zoom_v1'; +const LS_NAV = 'houseplan_card_nav_v1'; // last space only; editor sessions never survive page navigation +const LS_KIOSK = 'houseplan_card_kiosk_v1'; // per-SCREEN size multipliers (each wall tablet differs) +const LS_VIEW = 'houseplan_card_view_v1'; // presentation preference per space, Labs-only +const POINTER_HOVER_TARGET_SELECTOR = 'hp-dialog, hp-help, hp-color-opacity, hp-device-preview'; +const NORM_W = 1000; // side of the render space — the canvas is square (v1.48.0) +/** Short semantic-event / direct-terminal-transition window. Event uses + three sequential 1.1 s waves; motion cool-down itself never animates. */ +/** + * How finely the lit region is traced where nothing blocks the light. 96 steps + * put the chord error at 0.05% of the radius — under a tenth of a pixel on a + * wall tablet, and cheap because only unobstructed directions use them. + */ +const GLOW_ARC_STEPS = 96; +/** + * Width of the lit→unlit ramp along a shadow edge, in SCREEN pixels: the eye + * reads a perfectly geometric edge as a cut-out, and a real penumbra is never + * wider than a hair at this scale. Measured on screen on purpose, so zooming in + * does not turn a hairline into a smear. + */ +const GLOW_EDGE_FEATHER_PX = 2; +/** + * Radial profile of a pool, as [offset %, share of the calibrated alpha]. + * Monotonic all the way out: a lamp is brightest under itself and dies at its + * radius. The centre keeps the full calibrated alpha, so nothing about the + * brightness maths (docs/specs/067) changes — only where that alpha is spent. + */ +const GLOW_FALLOFF: readonly (readonly [number, number])[] = [ + [0, 1], [45, 0.88], [70, 0.62], [86, 0.32], [100, 0], +]; +/** A source pool fades in/out without changing its final calibrated alpha. */ +const GLOW_FADE_MS = 500; + +/** Smallest rectangle holding both (docs/CANVAS.md §4). */ +const unionRect = (a: Rect, b: Rect): Rect => { + const x = Math.min(a.x, b.x), y = Math.min(a.y, b.y); + return { x, y, w: Math.max(a.x + a.w, b.x + b.w) - x, h: Math.max(a.y + a.h, b.y + b.h) - y }; +}; + +/** #313: one Thickness-tool hit — a room interval or independent masonry. */ +type WallThickSource = { kind: 'room' } + | { kind: 'partition'; id: string } + | { kind: 'draft'; id: string; segment: number }; +type WallThickHit = { + a: number[]; b: number[]; roomId: string; segs: number[][]; + open: boolean; cm: number; source: WallThickSource; +}; + +type MarkupTool = 'select' | 'draw' | 'column' | 'merge' | 'split' | 'resize' | 'opening' | 'wallthick' | 'delroom'; +type RoomFillFrame = { + byRoom: Map; + byId: Map; +}; +type WallFaceCandidate = WallGraphFace & { + split?: { roomId: string; mainPoly: number[][]; newPoly: number[][] }; + consumeAllActive?: boolean; + /** Face existed before this click; rejecting it must be a true no-op. */ + existing?: boolean; + repair?: WallFaceRepairProposal; +}; +type WallFaceDecision = { + candidate: WallFaceCandidate; + create: boolean; + name?: string; + area?: string | null; + settings?: RoomCfg['settings']; +}; +type WallFaceBatch = { + candidates: WallFaceCandidate[]; + index: number; + decisions: WallFaceDecision[]; + activePath: number[][]; + activeCms: number[]; + activeDraftId: string | null; +}; +/** + * The floor a source can see, and nothing else. One region means one clip: + * a beam through a doorway, the room it lands in and the shadow of a column + * are all the same computation, so they can never disagree with each other. + */ +type GlowClipGeometry = { lit: string[] }; +const MARKUP_TOOLS = new Set([ + 'select', 'draw', 'column', 'merge', 'split', 'resize', + 'opening', 'wallthick', 'delroom', +]); +/** Warm viewport is page-memory, so it may contain a tool name from the old bundle. */ +const normalizeMarkupTool = (value: unknown): MarkupTool => { + // #173 replaces the public one-shot Partition tool with one Walls chain. + // A warm page may still carry the old session token; reading it is inert. + value = normalizeUnifiedWallTool(value); + // Opening placement is valid only together with its explicit session-only + // type preset. Warm viewport state does not persist that preset. + if (value === 'opening') return 'draw'; + return typeof value === 'string' && MARKUP_TOOLS.has(value as MarkupTool) + ? value as MarkupTool + : 'draw'; +}; +const MAX_ROOM_DRAFTS = 200; +const MAX_DRAFT_POINTS = 500; +const MAX_DRAFT_SEGMENTS = 2000; +const MAX_PARTITIONS = 2000; +const MAX_ROOMS = 400; +const MAX_WALL_COLUMNS = 500; +/** Everything whose topology or wall association changes in the plan editor. */ +interface SpaceGeometryState { + spaceId: string; + rooms: any[]; + openings?: OpeningCfg[]; + walls?: WallEntry[]; + wall_segments?: any[]; + open_spans?: OpenSpanEntry[]; + room_drafts?: RoomDraftCfg[]; + partitions?: PartitionCfg[]; + wall_columns?: WallColumnCfg[]; + decor?: DecorShape[]; + plan_transform: { + plan_x?: number; plan_y?: number; plan_scale?: number; + plan_scale_x?: number; plan_scale_y?: number; plan_angle?: number; + }; +} +/** Tools of the decor (background) editor. `furniture` is the library + * (docs/FURNITURE.md): it opens a palette and places a symbol at real size. */ +type DecorTool = 'select' | 'backdrop' | 'line' | 'rect' | 'ellipse' | 'text' | 'furniture' | 'erase'; + +const fireEvent = (node: EventTarget, type: string, detail?: unknown) => { + const ev = new Event(type, { bubbles: true, composed: true }) as any; + ev.detail = detail ?? {}; + node.dispatchEvent(ev); +}; + +const navigate = (path: string) => { + history.pushState(null, '', path); + fireEvent(window, 'location-changed', { replace: false }); +}; + +/** + * Debounce with `flush()` and `pending`. Both are load-bearing: a pending + * config write MUST be flushed before the card adopts a server revision, + * otherwise the edit is silently dropped (audit L2, 2026-07-27). + */ +interface Debounced void> { + (...a: Parameters): void; + flush(): void; + cancel(): void; + pending(): boolean; +} + +const debounce = void>(fn: T, ms: number): Debounced => { + let t: number | undefined; + let last: Parameters | null = null; + const wrapped = ((...a: Parameters) => { + clearTimeout(t); + last = a; + t = window.setTimeout(() => { + t = undefined; + const args = last; + last = null; + if (args) fn(...args); + }, ms); + }) as Debounced; + wrapped.flush = () => { + if (t === undefined) return; + clearTimeout(t); + t = undefined; + const args = last; + last = null; + if (args) fn(...args); + }; + wrapped.cancel = () => { + clearTimeout(t); + t = undefined; + last = null; + }; + wrapped.pending = () => t !== undefined; + return wrapped; +}; + +/** + * Capture the pointer for a drag, tolerating an inactive pointerId. + * + * `setPointerCapture` throws for synthetic events and for pointers some + * browsers consider gone; that killed a drag outright. The opening pipeline + * was hardened for this, the device/label/resize ones were not (audit + * follow-up L4 sub-item) — now they all go through here. + */ +const capturePointer = (ev: PointerEvent): void => { + try { + (ev.target as Element | null)?.setPointerCapture?.(ev.pointerId); + } catch { + /* an inactive pointerId must never kill the drag */ + } +}; + +/** Ruler labels + the centre-magnet tick. Existing-opening DRAG carries its + * legacy two labels; a new-opening PLACEMENT may additionally carry physical + * line/tick geometry (#238). */ +interface OpMeasureLabel { + x: number; + y: number; + text: string; + dimension?: OpeningDimension; +} + +interface OpMeasure { + labels: OpMeasureLabel[]; + guide: { x: number; y: number; angle: number } | null; +} + +type OpeningPlacementCandidate = Omit & { + face: OpeningFaceOffset; + measure: OpMeasure; +}; + +type RenderOpening = OpeningCfg & { + rx: number; ry: number; rlen: number; + partitionHost?: ResolvedPartitionOpening; + orphanReason?: PartitionOpeningOrphanReason; +}; + +interface DeviceInboxDialogState { + tab: DeviceInboxCategory; + search: string; + showEntities: boolean; + onlyNew: boolean; + limit: number; + /** Logical row restored after a nested marker dialog closes. */ + anchor?: string; + busy?: string; +} + +type FixedFloorState = FixedFloorSelection | { kind: 'pending'; value: unknown }; + + +export interface HouseplanEditorHostPort { + _ackNewDevice: (id: string) => void; + _activeDraftId: string | null; + _activePlanSnapCandidate: PlanSnapCandidate | null; + _activePlanSnapConflicts: PlanSnapEndpoint[]; + _activityRt: Map; + _adoptInitialSpace: (models: SpaceModel[], authoritative?: boolean) => InitialSpaceSelection; + _adoptStructuralResponses: (cfgResp: any, layResp?: any, layoutOverride?: Record) => { configChanged: boolean; layoutChanged: boolean; }; + _alignDialog: { report: OptimizeReport; config: any; layout: Record; preflight: OptimizeGeometryPreflightResult | null; cm: number; where: string; changed: boolean; busy: boolean; removeLiveMissingPositions: boolean; } | null; + _alignPoint: number[] | null; + _allRoomsFlat: () => { value: string; label: string; }[]; + _angleField: (value: unknown) => string; + _applyView: (zoom: number, cx?: number, cy?: number) => boolean; + _areaSel: string; + _areaToSpace: Record; + _aspectJob: Promise | null; + _autoIconForBinding: (binding: string) => string; + _backdropDialog: { widthCm: number; heightCm: number; angle: string; } | null; + _backupExportDialog: { kind: "full" | "space"; planOnly: boolean; busy: boolean; error: string; } | null; + _backupImportDialog: { filename: string; size: number; token: string; preview: any; expectedConfigRev: number; expectedLayoutRev: number; duplicatePolicy: "skip" | "virtual"; confirmMissing: boolean; busy: boolean; error: string; } | null; + _baseVb: () => number[]; + _bdActive: boolean; + _bdBase: Rect | null; + _bdDrag: { kind: "move" | "scale" | "rotate"; pid: number; sx: number; sy: number; base: Rect; p0: { dx: number; dy: number; sx: number; sy: number; angle: number; }; fx: number; fy: number; sgx: number; sgy: number; rect0: DecorBox; before: SpaceGeometryState | null; moved: boolean; } | null; + _bdMovable: boolean; + _bdMoved: boolean; + _bdParams: { dx: number; dy: number; sx: number; sy: number; angle: number; }; + _bdRect: (Rect & { angle?: number; }) | null; + _bindingEntities: (binding: string) => string[]; + _bindingHasAlarm: (binding: string) => boolean; + _bindingHasClimate: (binding: string) => boolean; + _bindingHasHaPage: (binding: string) => boolean; + _bindingStatus: (binding: string) => HaBindingStatus; + _bootSoftCancel: () => void; + _cacheSnapshot: () => void; + _canCommitSpace: (id: string, authority?: boolean) => boolean; + _canEdit: boolean; + _canOptimizeUndo: boolean; + _cancelDevicePressFeedback: () => void; + _cancelModeTransition: (commitTarget?: boolean) => void; + _candidateDeviceSnapshot: RenderDeviceSnapshot | null; + _capturedSnapshotConfigEpoch: number; + _cellCm: number; + _cfgContentFingerprint: string; + _cfgEpoch: number; + _cfgRev: number; + _clearTransientHover: (suspend?: boolean) => void; + _closeInfoCard: () => void; + _closingWallCm: number | null; + _cmToUnits: (cm: number) => number; + _colorPickerLabels: ColorPickerLabels; + _commitSpace: (id: string, authority?: boolean) => boolean; + _commitViewModeAtomic: (from: ModeVisualState | null, targetZoom: number, targetCenterX?: number, targetCenterY?: number) => void; + _config: CardConfig | undefined; + _contourClosed: boolean; + _curSpaceCfg: any; + _currentModeVisual: (mode?: HouseplanMode) => ModeVisualState | null; + _cursorPt: number[] | null; + _decorBoxOf: (shape: DecorShape) => DecorBox | null; + _decorDraft: { kind: "line" | "rect" | "ellipse"; a: number[]; b: number[]; pid: number; } | null; + _decorEraseConfirm: { id: string; kind: DecorShape["kind"]; } | null; + _decorH: number; + _decorLargeCm: (value: number) => number; + _decorLargeField: (cm: number) => number; + _decorList: DecorShape[]; + _decorMove: { id: string; start: number[]; orig: DecorShape; pid: number; moved: boolean; before: SpaceGeometryState | null; } | null; + _decorResolvedStyle: (shape?: DecorShape | null) => DecorStyle; + _decorSel: string | null; + _decorShapeDialog: { id: string; kind: "line" | "rect" | "ellipse" | "furniture"; color: string; opacity: number; widthCm: number; lineStyle?: "solid" | "dashed"; fill?: boolean; fillColor?: string; fillOpacity?: number; lengthCm?: number; sizeWCm?: number; sizeHCm?: number; angle: string; symbol?: string; } | null; + _decorSmallCm: (value: number) => number; + _decorSmallField: (cm: number) => number; + _decorSnapCache: { epoch: number; space: string; height: number; exclude: string; geometry: SnapGeometry; } | null; + _decorStyle: DecorStyle; + _decorTextCm: (value: number) => number; + _decorTextDialog: { id?: string; x: number; y: number; text: string; color: string; opacity: number; angle: string; sizeCm: number; pickerEntity?: string; preserveLegacy?: boolean; } | null; + _decorTextSelection: { start: number; end: number; }; + _decorTextSizeCm: (shape?: DecorShape | null) => number; + _decorTool: DecorTool; + _defPos: Record; + _deviceInbox: DeviceInboxDialogState | null; + _deviceInboxMemo: { key: string; rows: DeviceInboxRow[]; } | null; + _deviceInboxReturn: DeviceInboxDialogState | null; + _devicePresentation: (d: DevItem, showLqi?: boolean, designPreview?: boolean) => ResolvedDevicePresentation; + _devices: DevItem[]; + _dirtyPos: Set; + _display: (url: string | null | undefined) => string; + _draftSegmentCms: number[]; + _drag: { id: string; sx: number; sy: number; ox: number; oy: number; moved: boolean; } | null; + _drawWallCm: number | null; + _drawWallField: string | null; + _drawWallFieldValue: string; + _drawWallMaxCm: number; + _dtBox: { id: string; x: number; y: number; w: number; h: number; } | null; + _dtDrag: { id: string; kind: "scale" | "rotate"; pid: number; ax: number; ay: number; r0: number; a0: number; textSizeCm0: number; angle0: number; sgx?: number; sgy?: number; orig?: DecorBox; origShape: DecorShape; before: SpaceGeometryState | null; lineEnd?: 0 | 1; moved: boolean; } | null; + _dtSel: DecorShape | null; + _duplicateColumnId: string | null; + _duplicateColumnTimer: number; + _editing: boolean; + _editorChromeMode: "plan" | "devices" | "decor"; + _editorSecondary: EditorSecondaryController; + _editorSecondaryContextId: string; + _editorSecondaryCopy: EditorSecondaryCopy; + _editorSecondaryDialogBlocked: boolean; + _editorToolbarGroups: readonly EditorToolbarGroup[]; + _effectiveProjection: () => "flat" | "iso"; + _endTabDrag: () => void; + _errText: (e: any) => string; + _excluded: Set; + _fillColors: FillColors; + _fmtLen: (a: number[], b: number[]) => string; + _frame: { id: string; model: SpaceModel; layout: unknown; devs: unknown; far: boolean; grow: boolean; rect: Rect; all: Rect; outliers: number; } | null; + _freeAreas: any[]; + _fullRegistryHass: any; + _furnPalette: { symbol: string; w: number; h: number; } | null; + _furnWallReach: number; + _furnWalls: number[][]; + _gearPtCache: WeakMap; + _geometryHistory: CommandStack; + _glowRadiusCm: number; + _glowRadiusPlaceholder: string; + _gridPitch: number; + _haIntegrationVersion: string | null; + _haRegistry: HaRegistrySnapshot; + _hasFixedFloor: boolean; + _hiddenWallDiagnosticCache: { key: string; value: HiddenWallDiagnosticGeometry; } | null; + _hoverRoom: { space: string; room: RoomCfg; } | null; + _iconRules: CompiledIconRule[] | undefined; + _imperial: boolean; + _importDialog: { floors: (FloorInfo & { checked: boolean; })[]; } | null; + _importQueue: string[]; + _importTotal: number; + _infoCard: DevItem | null; + _innerRoomContour: (space: SpaceModel, roomId: string, openCuts?: number[][], roomWalls?: any, multiWallNodes?: import("C:/Users/Sergey/Downloads/dev/houseplan-dev/houseplan-card-src/src/wall-thickness").MultiWallNodeMap | null | undefined) => number[][] | null; + _isVacDev: (d: DevItem) => boolean; + _kiosk: boolean; + _kioskDialog: boolean; + _kioskHoldTimer: number | undefined; + _kioskScale: { icon: number; font: number; }; + _labsIso: boolean; + _lastValidStageSize: [number, number] | null; + _layout: Record; + _layoutRev: number; + _logicalViewCenter: (projection: "flat" | "iso") => { x: number; y: number; } | null; + _markerDialog: { devId?: string; uploadId?: string; name: string; binding: string; bindingMode: "virtual" | "ha"; bindingOpen: boolean; showEntities: boolean; bindingFilter: string; icon: string; autoIcon: string; display: DeviceDisplayMode; rippleColor: string; rippleSize: number; size: number; angle: number; tapAction: string; tapActionTouched: boolean; originalHasTapAction: boolean; originalTapAction: string | null | undefined; tapHintAnnouncement: string; toggleEntity: string; toggleEntityTouched: boolean; originalHasToggleEntity: boolean; originalToggleEntity: string | null | undefined; tapTarget: string; tapConfirm: boolean; runFilter: string; controls: string[]; controlsFilter: string; glowRadius: string; lightRole: "auto" | "always" | "never"; lightRoleTouched: boolean; originalHasIsLight: boolean; originalIsLight: boolean | null | undefined; lightEntity: string; lightEntityTouched: boolean; originalHasLightEntity: boolean; originalLightEntity: string | null | undefined; glowMode: "auto" | "color" | "fixed"; glowColor: string; glowBrightness: number; glowColorDrafted: boolean; glowBrightnessDrafted: boolean; glowTouched: boolean; originalHasGlowColor: boolean; originalGlowColor: { c: string; bri?: number | null; } | null | undefined; valueBadgeEnabled: boolean; valueBadgeSource: ValueBadgeSource | null; valueBadgePosition: ValueBadgePosition; valueBadgeTouched: boolean; originalHasValueBadge: boolean; originalValueBadge: MarkerValueBadge | null | undefined; useClimateTemp: boolean; model: string; link: string; description: string; pdfs: PdfRef[]; room: string; hideFromPlan: boolean; busy: boolean; } | null; + _markerPreviewDevicesMemo: { base: readonly DevItem[]; preview: DevItem; devices: readonly DevItem[]; } | null; + _markerPreviewMemo: { key: string; device: DevItem | null; } | null; + _markers: Marker[]; + _markup: boolean; + _maybeRebuildDevices: () => void; + _mergeDialog: { aId: string; bId: string; poly: number[][]; pick: "a" | "b"; } | null; + _mergeSel: string | null; + _mode: "view" | "plan" | "devices" | "decor"; + _modeTransitionBusy: boolean; + _modeTransitionEditorCamera: { zoom: number; centerX?: number; centerY?: number; } | null; + _modeTransitionForceAtomic: boolean; + _modeTransitionPreparing: boolean; + _modeTransitionRequest: number; + _modeTransitionTargetCenterX: number | undefined; + _modeTransitionTargetCenterY: number | undefined; + _modeTransitionTargetZoom: number; + _modeTransitionVisual: ModeVisualState | null; + _model: SpaceModel[]; + _modelCache: { key: string; model: SpaceModel[]; } | null; + _nameSel: string; + _newIds: Set; + _newSyncKey: string; + _norm: boolean; + _normPos: (space: string, x: number, y: number) => { s: string; x: number; y: number; }; + _normalizeWalls: (walls: WallEntry[] | null | undefined, cuts: number[][]) => WallEntry[]; + _noteLayoutRev: (r: any) => void; + _opDrag: { id: string; moved: boolean; sx: number; sy: number; dirty: boolean; before: SpaceGeometryState | null; } | null; + _opMeasure: OpMeasure | null; + _openBindingInHa: (binding: string) => void; + _openCuts: () => number[][]; + _openingAmt: (o: OpeningCfg) => number; + _openingDialog: { id?: string; type: "door" | "window" | "gate" | "passage"; lengthCm: number; lengthTouched?: boolean; contact: string; lock: string; contactOpen?: boolean; contactFilter?: string; lockOpen?: boolean; lockFilter?: string; invert: boolean; flipH: boolean; flipV: boolean; host?: PartitionOpeningHost; x: number; y: number; angle: number; } | null; + _openingDimensionContextCache: { key: string; value: OpeningDimensionContext; } | null; + _openingEntityAvailable: (eid: string | null | undefined) => boolean; + _openingFace: (opening: RenderOpening, index: OpeningWallIndex, flipV: boolean) => OpeningFaceOffset; + _openingHoverCandidate: OpeningPlacementCandidate | null; + _openingJambBlockCm: number | null; + _openingPlacementIntervalsCache: { key: string; value: WallInterval[]; } | null; + _openingPreset: OpeningPlacementPreset | null; + _openingPresetRevision: number; + _openingPreview: OpeningPlacementCandidate | null; + _openingRebindId: string | null; + _openingWallIndexFor: (space: SpaceModel, openCuts: number[][]) => { key: string; value: OpeningWallIndex; }; + _openingsR: RenderOpening[]; + _optimizeUndoBusy: boolean; + _panLock: "pan" | "swipe" | null; + _panStart: { sx: number; sy: number; vx: number; vy: number; } | null; + _partitionDeleteDialog: { id: string; openings: OpeningCfg[]; } | null; + _path: number[][]; + _pendingNavMode: "plan" | "devices" | "decor" | null; + _pendingPhysicalWrites: Map; + _pendingSplit: { roomId: string; mainPoly: number[][]; newPoly: number[][]; } | null; + _persistLayout: Debounced<() => void>; + _physicalBodiesCache: { key: string; drafts: number[][][]; partitions: number[][][]; columns: number[][][]; patches: number[][][]; all: number[][][]; } | null; + _physicalBodiesR: (space?: SpaceModel | undefined) => number[][][]; + _physicalDialog: { kind: "partition" | "column" | "draft"; id: string; cm: string; segment?: number; shape?: "square" | "circle"; angle?: string; length?: string; } | null; + _physicalDrag: { pid: number; kind: "partition" | "column"; id: string; start: number[]; startClient: number[]; before: SpaceGeometryState | null; moved: boolean; base: PartitionCfg | WallColumnCfg; delta: number[]; } | null; + _physicalLastTap: { kind: "partition" | "column" | "draft"; id: string; segment?: number; at: number; } | null; + _physicalPickCycle: { signature: string; index: number; x: number; y: number; at: number; } | null; + _physicalRotate: { pid: number; id: string; center: number[]; startAngle: number; baseAngle: number; angle: number; before: SpaceGeometryState | null; moved: boolean; } | null; + _physicalSel: { kind: "partition" | "column" | "draft"; id: string; segment?: number; } | null; + _pinchStart: { dist: number; zoom: number; } | null; + _planEntityAvailable: (eid: string | null | undefined) => boolean; + _planHass: any; + _planSnapGeometryCache: { key: string; value: PlanSnapGeometry; } | null; + _planSnapHover: { contextKey: string; candidate: PlanSnapCandidate | null; conflicts: PlanSnapEndpoint[]; } | null; + _planStructuralGeometryCache: { key: string; value: PlanSnapGeometry; } | null; + _pointerModality: PointerModalityController; + _pointers: Map; + _pos: (d: DevItem) => { x: number; y: number; }; + _preflightClipboardFallback: string | null; + _prepareModeTransition: (request: number, from: ModeVisualState, targetMode: HouseplanMode, targetZoom: number, targetCenterX?: number, targetCenterY?: number) => void; + _reducedMotion: boolean; + _regSignature: string; + _reloadConfigOnly: (force?: boolean, observedRev?: number) => Promise; + _reloadLayoutOnly: () => Promise; + _renderCardPreview: (spaceScale: number, nameScale: number, labelScale: number) => TemplateResult; + _renderCompass: () => TemplateResult; + _renderPlanHass: any; + _reportedPreflightFingerprint: string | null; + _resign: () => void; + _resize: ResizeController; + _restoreZoom: () => void; + _resumeDraftBySpace: Record; + _rlResize: { id: string; space: string; k0: number; cx: number; cy: number; d0: number; } | null; + _roomCenter: (r: RoomCfg) => number[]; + _roomCustomFill: FillColorEntry | null; + _roomDeleteDialog: { roomId: string; name: string; } | null; + _roomDialog: boolean; + _roomEditId: string | null; + _roomFill: "" | "none" | "lqi" | "light" | "temp" | "custom"; + _roomHumSrc: string; + _roomLabelScale: number; + _roomNameScale: number; + _roomSrcFilter: string; + _roomSrcOpen: "temp" | "hum" | null; + _roomTempSrc: string; + _roomWallOpeningInputs: (openings?: readonly RenderOpening[], space?: SpaceModel | undefined) => Array<{ x: number; y: number; angle: number; length: number; }>; + _rszLimitViolation: JunctionLimitViolation | null; + _rulesDialog: { rules: IconRule[]; test: string; busy: boolean; } | null; + _saveConfigDebounced: Debounced<() => void>; + _saveNav: () => void; + _savePos: (d: DevItem, x: number, y: number) => void; + _screenToVb: (sx: number, sy: number) => number[]; + _segments: number[][]; + _selId: string | null; + _sentPos: Map; + _serverCfg: ServerConfig | null; + _serverStorage: boolean; + _settings: { exclude_integrations?: string[]; group_lights?: boolean; show_all?: boolean; filter_seeded?: boolean; icon_rules?: { pattern: string; icon: string; }[]; }; + _settingsDialog: { colors: FillColors; glowRadius: number; bgColor: string | null; northDeg: number | null; bgMode: "static" | "daynight"; sunRays: boolean; busy: boolean; } | null; + _showAll: boolean; + _showHidden: boolean; + _showToast: (msg: string) => void; + _signer: ContentSigner; + _space: string; + _spaceDialog: { mode: "edit" | "create"; spaceId?: string; title: string; planUrl: string | null; planFile: { ext: string; b64: string; aspect: number; name: string; } | null; pickSaved?: boolean; saved?: { name: string; url: string; size: number; modified: number; used_by: string[]; }[] | null; savedBusy?: boolean; savedAspect?: number; source: "file" | "draw"; showBorders: boolean; showNames: boolean; zeroWallStyle: ZeroWallStyle; displayTouched: boolean; hideDecor: boolean; hideOpenings: boolean; roomColor: string; roomOpacity: number; bgColor: string | null; bgMode: "static" | "daynight" | null; northDeg: number | null; sunRays: boolean | null; fillMode: "none" | "lqi" | "light" | "temp" | "custom"; customFill: FillColorEntry | null; glowEnabled: boolean; tempMin: number; tempMax: number; showLqi: boolean; cardFontScale: number; labelTemp: boolean; labelHum: boolean; labelLqi: boolean; labelLight: boolean; cellCm: number; cellCmInput?: string; cellCmTouched?: boolean; deleteBlockers?: number; busy: boolean; } | null; + _spaceH: number; + _spaceModel: () => SpaceModel | undefined; + _spaceModelById: (id: string | null | undefined) => SpaceModel | undefined; + _spaceWalls: WallEntry[]; + _splitSel: { roomId: string; pts: number[][]; } | null; + _stageBgHex: () => string; + _stageEl: HTMLElement | null; + _stagedDeviceSnapshotToken: number; + _suppressClick: boolean; + _swipeStart: { x: number; y: number; id: number; } | null; + _t: (key: I18nKey, vars?: Record) => string; + _thickWallCuts: () => number[][]; + _tip: { x: number; y: number; title: string; meta: string; lqi?: number | null; temp?: number | null; hum?: number | null; } | null; + _toggleMarkerDialogVisibility: () => void; + _tool: MarkupTool; + _undoKind: "optimize" | "import" | null; + _undoPoint: () => void; + _vacAllCameraCache: { devId: string; candidates: VacSourceCandidate[]; } | null; + _vacAllCamerasFor: string | null; + _vacCalConfirm: { markerId: string; source: string; mapId: string; matrix: Affine; rooms: number; error: string; } | null; + _vacEnsureMarker: (d: DevItem) => Marker | null; + _vacEntity: (d: DevItem) => string | null; + _vacFit: { markerId: string; source: string; mapId: string; p: FitParams; drag: null | { kind: "move" | "scale"; sx: number; sy: number; p0: FitParams; fx: number; fy: number; }; } | null; + _vacOpenAllCameras: (d: DevItem) => void; + _vacRt: Map; + _vacSource: (d: DevItem, planHass?: any) => string | null; + _vacSourceResolution: (d: DevItem, includeAllCameras?: boolean, planHass?: any) => VacSourceResolution; + _vacSrvTrails: Record; + _view: { x: number; y: number; w: number; h: number; } | null; + _viewModeSnap: { space: string; zoom: number; cx?: number; cy?: number; } | null; + _viewOr: (vb: number[]) => { x: number; y: number; w: number; h: number; }; + _viewPreference: Record; + _virtualLights: VirtualLightSnapshot; + _visibleDeviceSnapshot: RenderDeviceSnapshot | null; + _wallDialog: { a: number[]; b: number[]; value: string; roomId: string | null; source: WallThickSource; sx: number; sy: number; } | null; + _wallFaceBatch: WallFaceBatch | null; + _wallFaceGraphCache: { key: string; value: WallFaceGraph; }[]; + _wallKeyPitch: number; + _wallRepairDiagnostic: WallFaceRepairProposal | null; + _wallThickHover: { segs: number[][]; open: boolean; d: string; } | null; + _wallUnionCache: { key: string; value: ReturnType; } | null; + _wallUnionPool: Map; }>; + _writeChain: Promise; + _writesPending: number; + _sendConfigCandidate: (candidate: ServerConfig) => Promise; + _writeConfig: () => Promise; + _requestMode: (mode: 'view' | 'plan' | 'devices' | 'decor', animate?: boolean) => Promise; + _primeDrawWallField: () => void; + _reloadRejectedPhysicalWrite: () => Promise; + _zoom: number; + _zoomBySpace: Record; + hass: any; + isConnected: boolean; + renderRoot: HTMLElement | DocumentFragment; + requestUpdate: (name?: PropertyKey, oldValue?: unknown, options?: import("C:/Users/Sergey/Downloads/dev/houseplan-dev/houseplan-card-src/node_modules/lit-element/development/lit-element").PropertyDeclaration, useNewValue?: boolean, newValue?: unknown) => void; + updateComplete: Promise; +} + +/** Lazily loaded implementation of editor-only interaction and rendering. */ +export const EDITOR_RUNTIME_FINGERPRINT = '__HOUSEPLAN_SOURCE_FINGERPRINT__'; + +export class HouseplanEditorRuntime { + /** #330: baseline limits are stable across one resize gesture. */ + private _junctionBaselineCache = new WeakMap(); + + public constructor(public readonly host: HouseplanEditorHostPort) { + host._editorSecondary = new EditorSecondaryController({ + root: () => host.renderRoot as ShadowRoot, + requestUpdate: () => host.requestUpdate(), + updateComplete: () => host.updateComplete, + clearTip: () => { host._tip = null; }, + }); + host._resize = new ResizeController< + ResizePreview, ResizeLiveLabel[], SpaceGeometryState, ResizeWallUnion, ResizeWallArtifact + >(); + } + +public _help(key: Extract): TemplateResult | typeof nothing { + const ariaKey = `${key}.aria` as I18nKey; + const lang = langOf(this.host.hass, this.host._config?.language); + if (!hasTranslation(lang, key) || !hasTranslation(lang, ariaKey)) return nothing; + return html``; + } + +public _setMode(mode: 'view' | 'plan' | 'devices' | 'decor', animate = true): void { + this.host._endTabDrag(); + // 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 + // the new instance already fails closed to `view`. Its close button must + // cancel the deferred editor in one press, not let the server response + // reopen it and force a second press (#95). + this.host._pendingNavMode = null; + const space = this.host._spaceModel(); + if (!space) { + this.host._cancelModeTransition(false); + this.host._mode = 'view'; + return; + } + if (this.host._kiosk && mode !== 'view') return; // wall devices never edit + if (this.host._mode === mode) { + if (!animate && mode === 'view' && this.host._modeTransitionBusy) { + const from = this.host._currentModeVisual(mode); + const targetZoom = this.host._modeTransitionTargetZoom; + const targetCenterX = this.host._modeTransitionTargetCenterX; + const targetCenterY = this.host._modeTransitionTargetCenterY; + this.host._cancelModeTransition(false); + this.host._commitViewModeAtomic(from, targetZoom, targetCenterX, targetCenterY); + } + return; + } + this.host._bootSoftCancel(); // navigation owns its own short, bounded transition + if ((mode === 'plan' || mode === 'decor') && !this.host._norm) { + this.host._showToast(this.host._t('toast.markup_needs_server')); + return; + } + if (this.host._wallFaceBatch) this._roomDialogCancel(); + if (this.host._mode === 'plan' && this.host._tool === 'draw' && !this._finishWallChain()) return; + this.host._clearTransientHover(true); + this.host._cancelDevicePressFeedback(); + const previousMode = this.host._mode; + const previousProjection = this.host._effectiveProjection(); + const capturedVisual = this.host._currentModeVisual(previousMode); + const retargeting = this.host._modeTransitionBusy; + // The target toolbar replaces the old editor markup during the hidden + // measurement frame. Give that incoming content its own coordinate on the + // shared controller so editor-to-editor switches do not pop abruptly. + const fromVisual = capturedVisual && previousMode !== 'view' && mode !== 'view' + ? { ...capturedVisual, toolbarContentOpacity: this.host._reducedMotion ? 1 : 0.35 } + : capturedVisual; + this.host._cancelModeTransition(false); + this.host._editorSecondary.closeForNavigation(); + // A live decor transform is a transaction. Switching tabs must cancel and + // restore it, not merely forget its pointer record after the config has + // already been mutated by move/resize. + if (this.host._decorMove || this.host._dtDrag || this.host._bdDrag) this._cancelDecorGesture(); + const baseChanges = !space.bg && (mode === 'view') !== (previousMode === 'view'); + // A running/preparing transition has not committed its target to `_zoom` + // yet. Retarget from the painted frame, but preserve the latest camera + // intent; otherwise rapid View -> editor A -> editor B resurrects the old + // View zoom/center and settles somewhere a direct transition never would. + const returningToEditor = retargeting && previousMode === 'view' && mode !== 'view' + ? this.host._modeTransitionEditorCamera : null; + let targetZoom = returningToEditor?.zoom + ?? (retargeting ? this.host._modeTransitionTargetZoom : this.host._zoom); + let targetCenterX = returningToEditor + ? returningToEditor.centerX + : retargeting ? this.host._modeTransitionTargetCenterX : fromVisual?.viewport.centerX; + let targetCenterY = returningToEditor + ? returningToEditor.centerY + : retargeting ? this.host._modeTransitionTargetCenterY : fromVisual?.viewport.centerY; + if (previousMode === 'view' && mode !== 'view' && !retargeting) { + // remember the view-mode viewport: whatever zooming happens inside the + // editors is a working tool, not what the user wants to see afterwards + const v = this.host._view; + this.host._viewModeSnap = { + space: this.host._space, + zoom: this.host._zoom, + cx: v ? this.host._logicalViewCenter(previousProjection)?.x : undefined, + cy: v ? this.host._logicalViewCenter(previousProjection)?.y : undefined, + }; + if (previousProjection === 'iso') { + const logical = this.host._logicalViewCenter('iso'); + targetCenterX = logical?.x; + targetCenterY = logical?.y; + this.host._view = null; + this.host._mode = mode; + this.host._applyView(this.host._zoom, logical?.x, logical?.y); + } + if (baseChanges) { + targetZoom = 1; + targetCenterX = undefined; + targetCenterY = undefined; + } + } + if (previousMode === 'plan' && this.host._activeDraftId) + this.host._resumeDraftBySpace[this.host._space] = this.host._activeDraftId; + this.host._mode = mode; + if (previousMode === 'devices' && mode !== 'devices') { + this.host._showHidden = false; + this.host._deviceInbox = null; + this.host._deviceInboxReturn = null; + this.host._deviceInboxMemo = null; + } + this.host._editorChromeMode = mode === 'view' ? previousMode as 'plan' | 'devices' | 'decor' : mode; + if (mode === 'view') { + if (previousMode !== 'view' && !retargeting) { + this.host._modeTransitionEditorCamera = { + zoom: this.host._zoom, + centerX: fromVisual?.viewport.centerX, + centerY: fromVisual?.viewport.centerY, + }; + } + const snap = this.host._viewModeSnap; + // restore the snapshot only for the space it was taken in + if (snap && snap.space === this.host._space) { + targetZoom = snap.zoom; + const targetProjection = this.host._labsIso && this.host._viewPreference[this.host._space] === 'iso' + ? 'iso' : 'flat'; + const center = snap.cx != null && snap.cy != null && targetProjection === 'iso' + ? projectPlanPoint([snap.cx, snap.cy], 0) : null; + targetCenterX = center?.[0] ?? snap.cx; + targetCenterY = center?.[1] ?? snap.cy; + } else if (snap) { + // HP-1543-01: the floor CHANGED inside the editor — the snapshot + // belongs to the floor the editor was entered from, while _zoom still + // carries the editor's working zoom for the floor we are exiting on. + // Dropping the snapshot alone left that editor zoom (say 500%) on + // screen in view mode. The per-space store was never polluted + // (_saveZoom is view-only), so the standard centred + // _restoreZoom() puts back this floor's saved VIEW viewport. Its rAF + // reads _zoomBySpace, not the snapshot, so the same-tick floor-tab + // race the view-only guard protects against stays closed. + targetZoom = this.host._zoomBySpace[this.host._space] || 1; + targetCenterX = undefined; + targetCenterY = undefined; + } + } else { + this.host._modeTransitionEditorCamera = { + zoom: targetZoom, centerX: targetCenterX, centerY: targetCenterY, + }; + } + this.host._modeTransitionTargetZoom = targetZoom; + this.host._modeTransitionTargetCenterX = targetCenterX; + this.host._modeTransitionTargetCenterY = targetCenterY; + if (previousProjection === 'iso' + || (mode === 'view' && this.host._labsIso && this.host._viewPreference[this.host._space] === 'iso')) { + this.host._modeTransitionForceAtomic = true; + } + const request = ++this.host._modeTransitionRequest; + if (!animate) { + // The only non-animated caller is route departure; keep the guard local + // so a future editor caller cannot accidentally use View geometry. + if (mode === 'view') { + this.host._commitViewModeAtomic(fromVisual, targetZoom, targetCenterX, targetCenterY); + } + } else if (fromVisual) { + this.host._modeTransitionPreparing = true; + this.host._modeTransitionVisual = fromVisual; + this.host._prepareModeTransition( + request, fromVisual, mode, targetZoom, targetCenterX, targetCenterY, + ); + } else { + // A zero-sized stage cannot animate. Keep the functional fallback + // atomic and publish the exact target as soon as it is measurable. + this.host._modeTransitionPreparing = false; + this.host._modeTransitionVisual = null; + this.host._zoom = targetZoom; + this.host._view = null; + requestAnimationFrame(() => { + if (!this.host.isConnected || request !== this.host._modeTransitionRequest || this.host._mode !== mode) return; + this.host._applyView(targetZoom, targetCenterX, targetCenterY); + if (mode === 'view') { + this.host._viewModeSnap = null; + this.host._modeTransitionEditorCamera = null; + } + this.host.requestUpdate(); + }); + } + this.host._path = []; + this._clearPlanSnapHover(); + this._clearOpeningPlacement(true); + this.host._tool = 'draw'; + this.host._mergeSel = null; + this.host._mergeDialog = null; + this.host._splitSel = null; + this.host._pendingSplit = null; + this.host._selId = null; + this.host._physicalSel = null; + this.host._physicalDialog = null; + this.host._physicalDrag = null; + this._rszResetController(); + this.host._tip = null; + this.host._hoverRoom = null; + this.host._decorDraft = null; + this.host._decorSel = null; + this.host._decorMove = null; + this.host._backdropDialog = null; + // Every Background session starts predictably on Select. The image has an + // explicit tool of its own; only that tool may claim its body or frame. + this.host._decorTool = 'select'; + this.host._bdDrag = null; + this.host._dtDrag = null; + this.host._dtBox = null; + if (mode === 'plan') { + this._primeDrawWallField(); + this._resumeLastDraft(); + } + this.host._saveNav(); + } + +public _primeDrawWallField(): void { + if (this.host._drawWallField === null) { + this.host._drawWallField = cmToField(DRAW_WALL_DEFAULT_CM, this.host._imperial); + } + } + +public _showPhysicalRange(max = this.host._drawWallMaxCm, min = 0): void { + this.host._showToast(this.host._t('toast.physical_range', { + min: cmToField(min, this.host._imperial), + max: cmToField(max, this.host._imperial), + unit: this.host._t(this.host._imperial ? 'wallthick.unit_in' : 'wallthick.unit_cm'), + })); + } + +public _draftSegmentCount(sp = this.host._curSpaceCfg as any): number { + return (sp?.room_drafts || []).reduce( + (sum: number, d: any) => sum + (Array.isArray(d.segments) ? d.segments.length : 0), 0, + ); + } + +public _mergeSpacePartitions(sp: any, seedIds?: string[]): number { + const partitions = (sp?.partitions || []) as PartitionCfg[]; + if (partitions.length < 2) return 0; + const result = mergeCollinearPartitions(partitions, { + pitch: GRID_STEP_N, + seedIds, + geometry: spaceMergeGeometry(sp, { excludeDraftId: this.host._activeDraftId }), + }); + if (!result.merged) return 0; + sp.partitions = result.partitions; + applyOpeningMoves(sp.openings, sp.partitions, result.openingMoves, { + coordScale: NORM_W, cellCm: this.host._cellCm, gridPitch: this.host._gridPitch, + }); + return result.merged; + } + +public _finishWallChain(): boolean { + if (this.host._tool !== 'draw' || this.host._wallFaceBatch || this.host._roomDialog) return true; + const sp = this.host._curSpaceCfg as any; + if (!sp || this.host._path.length < 2) { + this.host._path = []; + this.host._activeDraftId = null; + this.host._draftSegmentCms = []; + this.host._closingWallCm = null; + this._clearPlanSnapHover(); + return true; + } + const segments = wallChainSegments( + this.host._path, + chainSegmentCms( + this.host._path.length - 1, this.host._draftSegmentCms, + this.host._drawWallCm, DRAW_WALL_DEFAULT_CM, + ), + ); + const segmentCount = segments.length; + if ((sp.partitions || []).length + segmentCount > MAX_PARTITIONS) { + this.host._showToast(this.host._t('toast.physical_limit')); + return false; + } + const before = this._geometrySnapshot(); + sp.partitions ||= []; + const seed = Date.now().toString(36); + const drawnIds: string[] = []; + const activeDraft = this.host._activeDraftId + ? (sp.room_drafts || []).find((draft) => draft.id === this.host._activeDraftId) + : null; + const lineage = this._draftSegmentsForPath( + this.host._path, activeDraft, segments.map((segment) => segment.cm), + ); + for (let i = 0; i < segmentCount; i++) { + const segment = segments[i]; + const id = lineage[i]?.id || `partition-${seed}-${i}`; + drawnIds.push(id); + sp.partitions.push({ + id, + a: [segment.a[0] / NORM_W, segment.a[1] / NORM_W], + b: [segment.b[0] / NORM_W, segment.b[1] / NORM_W], + cm: segment.cm, + }); + } + // A straight run drawn in several clicks is one wall, not five (#229). + // Only what this chain touches is merged: what has piled up earlier waits + // for «Optimise plans», where the sweep is explicit and undoable. + this._mergeSpacePartitions(sp, drawnIds); + if (this.host._activeDraftId && Array.isArray(sp.room_drafts)) { + sp.room_drafts = sp.room_drafts.filter((draft) => draft.id !== this.host._activeDraftId); + if (!sp.room_drafts.length) delete sp.room_drafts; + } + this._commitPhysicalGeometry(this.host._t('history.wall_chain_finish'), before); + delete this.host._resumeDraftBySpace[this.host._space]; + this.host._path = []; + this.host._activeDraftId = null; + this.host._draftSegmentCms = []; + this.host._closingWallCm = null; + this._clearPlanSnapHover(); + return true; + } + +public _activateMarkupTool(tool: MarkupTool): void { + if (tool === this.host._tool) return; + if (this.host._wallFaceBatch) this._roomDialogCancel(); + if (this.host._tool === 'draw' && !this._finishWallChain()) return; + else this._cancelPath(); + if (this.host._tool === 'resize') { + if (this.host._resize.dragging) this._rszCancelDrag(); + else this.host._resize.reset(); + } + this.host._tool = tool; + if (tool === 'draw') this._resumeLastDraft(); + if (tool === 'resize') this.host._resize.selectRoom(null); + if (tool === 'wallthick') this.host._wallDialog = null; + } + +public _limitReached(kind: 'draft' | 'partition' | 'column'): boolean { + const sp = this.host._curSpaceCfg as any; + if (!sp) return true; + const reached = kind === 'draft' + ? (sp.room_drafts || []).length >= MAX_ROOM_DRAFTS + : kind === 'partition' + ? (sp.partitions || []).length >= MAX_PARTITIONS + : (sp.wall_columns || []).length >= MAX_WALL_COLUMNS; + if (reached) this.host._showToast(this.host._t('toast.physical_limit')); + return reached; + } + +public _svgPoint(ev: MouseEvent): number[] { + const stage = this.host.renderRoot.querySelector('.stage') as HTMLElement; + const r = stage.getBoundingClientRect(); + return this.host._screenToVb(ev.clientX - r.left, ev.clientY - r.top); + } + +public _snap(p: number[]): number[] { + const g = this.host._gridPitch; + return [clampCanvasR(snapToGrid(p[0], g)), clampCanvasR(snapToGrid(p[1], g))]; + } + +public _snapDrawPoint(p: number[], lock45 = false): number[] { + const anchor = this.host._path[this.host._path.length - 1]; + const candidate = lock45 && anchor + ? snapSegment45(anchor, p, this.host._gridPitch, SANE_LIMIT) + : p; + return this._snap(candidate); + } + +public _planSnapOpeningCuts(space: SpaceModel, openCuts: number[][]): number[][] { + if (!this.host._openingsR.length) return []; + const index = this.host._openingWallIndexFor(space, openCuts).value; + const cuts: number[][] = []; + for (const input of this.host._roomWallOpeningInputs(this.host._openingsR, space)) { + const association = resolveOpeningWallAssociation(index, input); + if (!association.negative && !association.positive) continue; + const rad = input.angle * Math.PI / 180; + const dx = Math.cos(rad) * input.length / 2; + const dy = Math.sin(rad) * input.length / 2; + cuts.push([input.x - dx, input.y - dy, input.x + dx, input.y + dy]); + } + return cuts; + } + +public _planSnapGeometrySnapshot(): { key: string; value: PlanSnapGeometry } { + const space = this.host._spaceModel(); + if (!space) { + return { key: `${this.host._space}|empty`, value: { segments: [], endpoints: [] } }; + } + const key = [ + this.host._space, this.host._cfgEpoch, this.host._activeDraftId || '', + space.rooms.length, space.room_drafts.length, space.partitions.length, + ].join('|'); + if (this.host._planSnapGeometryCache?.key === key) return this.host._planSnapGeometryCache; + const zeroCuts = this.host._openCuts(); + const value = buildPlanSnapGeometry({ + space, + activeDraftId: this.host._activeDraftId, + // A zero-thickness wall is still a canonical wall axis and snap target. + // Only an actual opening removes the presentation interval (#306). + roomCuts: this._planSnapOpeningCuts(space, zeroCuts), + partitionCuts: this._partitionOpeningCuts(space), + epsilon: this.host._gridPitch * 0.0002, + }); + this.host._planSnapGeometryCache = { key, value }; + return this.host._planSnapGeometryCache; + } + +public _hiddenWallDiagnosticSnapshot(): { + key: string; value: HiddenWallDiagnosticGeometry; + } { + const space = this.host._spaceModel(); + if (!space) { + return { key: `${this.host._space}|hidden-empty`, value: { segments: [], endpoints: [] } }; + } + const key = [ + 'hidden', this.host._space, this.host._cfgEpoch, this.host._activeDraftId || '', + space.rooms.length, space.room_drafts.length, space.partitions.length, + ].join('|'); + if (this.host._hiddenWallDiagnosticCache?.key === key) { + return this.host._hiddenWallDiagnosticCache; + } + const value = buildHiddenWallDiagnosticGeometry({ + space, + activeDraftId: this.host._activeDraftId, + epsilon: this.host._gridPitch * 0.0002, + }); + this.host._hiddenWallDiagnosticCache = { key, value }; + return this.host._hiddenWallDiagnosticCache; + } + +public _planStructuralGeometrySnapshot(): { key: string; value: PlanSnapGeometry } { + const space = this.host._spaceModel(); + if (!space) { + return { key: `${this.host._space}|structural-empty`, value: { segments: [], endpoints: [] } }; + } + const key = [ + 'structural', this.host._space, this.host._cfgEpoch, this.host._activeDraftId || '', + space.rooms.length, space.room_drafts.length, space.partitions.length, + ].join('|'); + if (this.host._planStructuralGeometryCache?.key === key) { + return this.host._planStructuralGeometryCache; + } + const value = buildPlanSnapGeometry({ + space, + activeDraftId: this.host._activeDraftId, + epsilon: this.host._gridPitch * 0.0002, + }); + this.host._planStructuralGeometryCache = { key, value }; + return this.host._planStructuralGeometryCache; + } + +public _planSnapContextKey(geometryKey: string): string { + const first = this.host._path[0]; + const anchor = this.host._path[this.host._path.length - 1]; + return [ + geometryKey, this.host._tool, this.host._path.length, + first ? `${first[0]},${first[1]}` : '', + anchor ? `${anchor[0]},${anchor[1]}` : '', + ].join('|'); + } + +public _resolvePlanDrawPoint( + raw: number[], lock45: boolean, + ): { + point: number[]; candidate: PlanSnapCandidate | null; + conflicts: PlanSnapEndpoint[]; ambiguous: boolean; contextKey: string; + } { + const snapshot = this._planSnapGeometrySnapshot(); + const anchor = this.host._path[this.host._path.length - 1]; + const closure = this.host._tool === 'draw' && this.host._path.length >= 3 + ? [{ point: this.host._path[0], key: 'closure:first-point' }] + : []; + const options = { + tolerance: this._cssPxToRender(12), + distinguishTolerance: this._cssPxToRender(8), + gridStep: this.host._gridPitch, + excludePoints: anchor ? [anchor] : [], + extraEndpoints: closure, + epsilon: this.host._gridPitch * 0.0002, + }; + const resolution = lock45 && anchor + ? resolveStrictPlanSnap(snapshot.value, raw, { ...options, anchor }) + : resolvePlanSnapResult(snapshot.value, raw, options); + const candidate = resolution.kind === 'resolved' ? resolution.candidate : null; + const snapped = candidate ? [...candidate.point] : this._snapDrawPoint(raw, lock45); + const point = anchor ? snapNearAxisEndpoint(anchor, snapped) : snapped; + // A nearby topology endpoint loses snap ownership when the drafting rule + // moves the actually persisted point away from it. Preview and click must + // describe the exact same geometry (#290). + const effectiveCandidate = candidate && samePoint(point, candidate.point) ? candidate : null; + return { + point, + candidate: effectiveCandidate, + conflicts: resolution.kind === 'ambiguous' ? resolution.conflicts : [], + ambiguous: resolution.kind === 'ambiguous', + contextKey: this._planSnapContextKey(snapshot.key), + }; + } + +public _clearPlanSnapHover(clearCursor = true): void { + this.host._planSnapHover = null; + this._syncPlanSnapActiveMarker(null); + this._syncPlanSnapConflictMarkers([]); + if (clearCursor) this.host._cursorPt = null; + } + +public _samePt(a: readonly number[], b: readonly number[]): boolean { + return samePoint(a, b); + } + +public _dropLegacySegments(): void { + // «Ripple only» was removed from the UI: keep old configs readable, then + // materialise the recognisable icon+activity presentation on any write. + for (const marker of this.host._serverCfg?.markers || []) { + if (marker.display === 'ripple') marker.display = 'icon_ripple'; + } + for (const sp of this.host._serverCfg?.spaces || []) { + delete (sp as any).segments; + const physicalIds = new Set(); + const validId = (id: any): id is string => typeof id === 'string' + && id.length >= 1 && id.length <= 64 && !physicalIds.has(id); + const keepId = (id: string): boolean => { physicalIds.add(id); return true; }; + const point = (p: any): p is number[] => Array.isArray(p) && p.length === 2 + && p.every((v: any) => Number.isFinite(Number(v)) && Math.abs(Number(v)) <= CANVAS_LIMIT); + if (Array.isArray((sp as any).partitions)) { + (sp as any).partitions = (sp as any).partitions.filter((p: any) => + p && validId(p.id) && point(p.a) && point(p.b) + && Math.hypot(p.a[0] - p.b[0], p.a[1] - p.b[1]) > 1e-9 + && keepId(p.id)) + .map((p: any) => ({ + ...p, + cm: Number.isFinite(Number(p.cm)) ? Math.max(0, Math.min(100, Number(p.cm))) : 15, + })); + if (!(sp as any).partitions.length) delete (sp as any).partitions; + } + if (Array.isArray((sp as any).wall_columns)) { + (sp as any).wall_columns = (sp as any).wall_columns.filter((c: any) => + c && validId(c.id) && point(c.center) && keepId(c.id)) + .map((c: any) => ({ + id: c.id, shape: c.shape === 'circle' ? 'circle' : 'square', + center: [Number(c.center[0]), Number(c.center[1])], + cm: clampColumnCm(Number(c.cm) || 15), + ...(c.shape === 'circle' ? {} : { angle: canonicalColumnAngle(c.angle) }), + })); + if (!(sp as any).wall_columns.length) delete (sp as any).wall_columns; + } + if (Array.isArray((sp as any).room_drafts)) { + (sp as any).room_drafts = (sp as any).room_drafts.filter((d: any) => + d && validId(d.id) && Array.isArray(d.points) + && d.points.length >= 2 && d.points.every(point) && keepId(d.id)) + .map((d: any) => { + const sanitized = sanitizeRoomDraftPath(d); + return { id: d.id, ...sanitized }; + }) + .filter((d: any) => d.points.length >= 2); + if (!(sp as any).room_drafts.length) delete (sp as any).room_drafts; + } + if (Array.isArray(sp.walls)) { + const model = selectSpaceModelById(spaceModels({ spaces: [sp] } as any), sp.id); + const cuts = model + ? resolveZeroWalls(sp, model, NORM_W, GRID_PITCH * 0.02).contour + .map((cut) => cut.map((value) => value / NORM_W)) + : sanitizeOpenSpans((sp as any).open_spans) + .map((e) => [e.a[0], e.a[1], e.b[0], e.b[1]]); + sp.walls = degradeWalls(sp.walls, sp.rooms || [], GRID_STEP_N, 1, cuts); + if (!sp.walls.length) delete sp.walls; + } + } + } + +public _rollbackRejectedPhysicalWrites( + entries: Array<[string, { fingerprint: string; before: SpaceGeometryState }]>, + ): boolean { + if (!this.host._serverCfg || !entries.length) return false; + let restored = false; + for (const [spaceId] of entries) { + const pending = this.host._pendingPhysicalWrites.get(spaceId); + if (!pending) continue; + restored = this._restoreGeometryStateInConfig(this.host._serverCfg, pending.before) || restored; + this.host._pendingPhysicalWrites.delete(spaceId); + } + if (!restored) return false; + this._clearGeometryGesture(); + this.host._geometryHistory.clear(); + this.host._cfgEpoch++; + this.host._modelCache = null; + this.host._wallUnionCache = null; + this.host._physicalBodiesCache = null; + this.host._frame = null; + this.host._regSignature = ''; + this.host._cfgContentFingerprint = contentFingerprint(this.host._serverCfg); + this.host._maybeRebuildDevices(); + this.host.requestUpdate(); + return true; + } + +public async _reloadRejectedPhysicalWrite(): Promise { + const queued = this.host._writeChain; + await queued.catch(() => undefined); + await this.host._reloadConfigOnly(true); + } + +public _writeConfig(): Promise { + this.host._writesPending++; + this.host._writeChain = enqueueSerializedWrite(this.host._writeChain, async () => { + if (!this.host._serverCfg) return; + this._dropLegacySegments(); + const candidate = canonicalizeConfigGeometry(this.host._serverCfg); + const candidateFingerprint = contentFingerprint(candidate); + const strictEntries = [...this.host._pendingPhysicalWrites.entries()]; + for (const [spaceId, accepted] of strictEntries) { + const candidateSpace = candidate.spaces.find((space) => space.id === spaceId); + const exactFingerprint = spacePhysicalGeometryFingerprint(candidateSpace); + if (exactFingerprint === accepted.fingerprint) continue; + let safe = false; + try { safe = this._checkSpacePhysicalGeometry(candidate, spaceId).ok; } catch { safe = false; } + if (!safe) { + this._restoreGeometryStateLocal(accepted.before); + this.host._pendingPhysicalWrites.delete(spaceId); + this.host._geometryHistory.clear(); + this.host._showToast(this.host._t('toast.geometry_unsafe')); + throw Object.assign(new Error('unsafe wall geometry'), { code: 'geometry-unsafe' }); + } + accepted.fingerprint = exactFingerprint; + } + // Do not replace the reactive root merely because the pure helper + // returned a clone. Besides an unnecessary render, that used to expose + // unrelated write-time cleanup as a visual change (#224 review H1). + // A real coordinate change still adopts the exact object sent below; + // willUpdate owns the corresponding geometry-epoch bump. + if (candidateFingerprint !== contentFingerprint(this.host._serverCfg)) { + this.host._serverCfg = candidate; + } + this.host._cfgContentFingerprint = candidateFingerprint; + try { + await this.host._sendConfigCandidate(candidate); + } catch (error) { + const rolledBack = this._rollbackRejectedPhysicalWrites(strictEntries); + if (!rolledBack) throw error; + // HA normally rejects with an Error-like object, but integrations and + // test doubles may reject with a string. Never lose the rollback tag: + // the caller still has to schedule an authoritative re-read after the + // synchronous, fail-closed restoration (#314). + const failure: any = error && typeof error === 'object' + ? error : Object.assign(new Error(String(error)), { cause: error }); + failure.physicalGeometryRolledBack = true; + throw failure; + } + for (const [spaceId, accepted] of strictEntries) { + if (this.host._pendingPhysicalWrites.get(spaceId)?.fingerprint === accepted.fingerprint) + this.host._pendingPhysicalWrites.delete(spaceId); + } + }); + const mine = this.host._writeChain.finally(() => { this.host._writesPending--; }); + // keep the chain itself unadorned so the next link waits for the write only + return mine; + } + +public _saveConfig(): void { + this.host._cfgEpoch++; + this.host._saveConfigDebounced(); + } + +public _geometrySnapshotFromConfig(config: any, spaceId: string): SpaceGeometryState | null { + const sp = config?.spaces?.find((s: any) => s.id === spaceId); + if (!sp) return null; + const copy = (value: T): T => JSON.parse(JSON.stringify(value)); + const plan_transform: SpaceGeometryState['plan_transform'] = {}; + for (const key of ['plan_x', 'plan_y', 'plan_scale', 'plan_scale_x', 'plan_scale_y', 'plan_angle'] as const) { + if (sp[key] !== undefined) plan_transform[key] = sp[key]; + } + return { + spaceId, + rooms: copy(sp.rooms || []), + ...(Array.isArray(sp.openings) ? { openings: copy(sp.openings) } : {}), + ...(Array.isArray(sp.walls) ? { walls: copy(sp.walls) } : {}), + ...(Array.isArray(sp.wall_segments) ? { wall_segments: copy(sp.wall_segments) } : {}), + ...(Array.isArray((sp as any).open_spans) + ? { open_spans: copy((sp as any).open_spans) } + : {}), + ...(Array.isArray((sp as any).room_drafts) + ? { room_drafts: copy((sp as any).room_drafts) } + : {}), + ...(Array.isArray((sp as any).partitions) + ? { partitions: copy((sp as any).partitions) } + : {}), + ...(Array.isArray((sp as any).wall_columns) + ? { wall_columns: copy((sp as any).wall_columns) } + : {}), + ...(Array.isArray(sp.decor) ? { decor: copy(sp.decor) } : {}), + plan_transform, + }; + } + +public _geometrySnapshot(spaceId = this.host._space): SpaceGeometryState | null { + return this._geometrySnapshotFromConfig(this.host._serverCfg, spaceId); + } + +public _recordGeometry(name: string, before: SpaceGeometryState | null): void { + if (!before) return; + const after = this._geometrySnapshot(before.spaceId); + if (!after || JSON.stringify(before) === JSON.stringify(after)) return; + this.host._geometryHistory.push({ name, before, after }); + this.host.requestUpdate(); + } + +public _restoreGeometryStateInConfig( + config: any, state: SpaceGeometryState, preserveIdentityHints = false, + ): boolean { + const sp = config?.spaces?.find((space) => space.id === state.spaceId); + if (!sp) return false; + const copy = (value: T): T => JSON.parse(JSON.stringify(value)); + const oldRooms = new Map((sp.rooms || []).map((room) => [room.id, room])); + sp.rooms = copy(state.rooms); + if (preserveIdentityHints) { + for (const room of sp.rooms) { + const old: any = oldRooms.get(room.id); + if (!Array.isArray(room.wall_ids) && Array.isArray(old?.wall_ids) + && old.wall_ids.length === room.poly?.length) room.wall_ids = copy(old.wall_ids); + } + } + const assign = (key: 'openings' | 'walls' | 'wall_segments' | 'open_spans' | 'room_drafts' + | 'partitions' | 'wall_columns' | 'decor', value: unknown): void => { + if (value !== undefined) (sp as any)[key] = copy(value); + else if (!(preserveIdentityHints && key === 'wall_segments')) delete (sp as any)[key]; + }; + const oldOpenings = new Map((sp.openings || []).map((opening: any) => [opening.id, opening])); + const oldDrafts = new Map((sp.room_drafts || []).map((draft) => [draft.id, draft])); + assign('openings', state.openings); + assign('walls', state.walls); + assign('wall_segments', state.wall_segments); + assign('open_spans', state.open_spans); + assign('room_drafts', state.room_drafts); + assign('partitions', state.partitions); + assign('wall_columns', state.wall_columns); + assign('decor', state.decor); + if (preserveIdentityHints) { + for (const opening of sp.openings || []) { + const old: any = oldOpenings.get(opening.id); + if (!opening.host && old?.host?.kind === 'wall') opening.host = copy(old.host); + } + for (const draft of sp.room_drafts || []) { + const old: any = oldDrafts.get(draft.id); + for (let index = 0; index < (draft.segments || []).length; index++) { + if (!draft.segments[index]?.id && old?.segments?.[index]?.id) + draft.segments[index].id = old.segments[index].id; + } + } + } + for (const key of ['plan_x', 'plan_y', 'plan_scale', 'plan_scale_x', 'plan_scale_y', 'plan_angle'] as const) + delete (sp as any)[key]; + Object.assign(sp, copy(state.plan_transform || {})); + return true; + } + +public _restoreGeometryStateLocal(state: SpaceGeometryState): boolean { + if (!this._restoreGeometryStateInConfig(this.host._serverCfg, state)) return false; + this.host._cfgEpoch++; + this.host._modelCache = null; + this.host._wallUnionCache = null; + this.host._physicalBodiesCache = null; + this.host._frame = null; + this.host.requestUpdate(); + return true; + } + +public _wallModelBlockerLabel(error: unknown): string { + const reason = error instanceof WallSegmentModelError ? error.reason : 'invalid-room'; + return this.host._t(`wall_model.reason.${reason}`); + } + +public _hasLegacyZeroWallFields(config: any = this.host._serverCfg): boolean { + return (config?.spaces || []).some((space) => ( + (Array.isArray(space?.open_spans) && space.open_spans.length > 0) + || (space?.rooms || []).some((room) => ( + Array.isArray(room?.open_to) && room.open_to.length > 0 + )) + )); + } + +public _showWallModelMigrationBlocked(error: unknown): void { + const key = this._hasLegacyZeroWallFields() + ? 'toast.zero_wall_migration_blocked' + : 'toast.wall_model_migration_blocked'; + this.host._showToast(this.host._t(key, { reason: this._wallModelBlockerLabel(error) })); + } + +public _limitSegmentsOf(space: any): LimitSegment[] { + const segments: LimitSegment[] = []; + for (const segment of (space?.wall_segments || [])) { + if (segment?.a && segment?.b) { + segments.push({ + id: String(segment.id || ''), a: segment.a, b: segment.b, cm: Number(segment.cm), + }); + } + } + for (const partition of (space?.partitions || [])) { + if (partition?.a && partition?.b) { + segments.push({ + id: String(partition.id || ''), a: partition.a, b: partition.b, cm: Number(partition.cm), + }); + } + } + for (const draft of (space?.room_drafts || [])) { + const points = Array.isArray(draft?.points) ? draft.points : []; + const drafted = Array.isArray(draft?.segments) ? draft.segments : []; + for (let index = 0; index + 1 < points.length; index++) { + segments.push({ + id: String(drafted[index]?.id || `${draft?.id || 'draft'}-${index}`), + a: points[index], b: points[index + 1], cm: Number(drafted[index]?.cm), + }); + } + } + return segments; + } + +public _junctionLimitViolations( + config: any, spaceId: string, + /** #330 §4.7: an already computed masonry pass of THIS config (e.g. the + * resize preflight's artifact) — the union is never paid twice. */ + sharedGeometry?: any, + ): JunctionLimitViolation[] { + const space = (config?.spaces || []).find((item) => item?.id === spaceId); + if (!space) return []; + const cellCm = Number(space.cell_cm) > 0 ? Number(space.cell_cm) : 5; + const segments = this._limitSegmentsOf(space); + const violations = [ + ...checkNodes(segments), + ...checkSegmentLengths(segments, cellCm, GRID_STEP_N), + ...checkNodeDistances(segments, cellCm, GRID_STEP_N), + ]; + // #330 §4.7: without the shared passes every room recomputed the FULL + // junction topology and masonry union for itself. Compute both at most + // once and hand them to every room clearance check. + let sharedNodes: ReturnType | null = null; + try { + sharedNodes = multiWallNodesForGeometry( + space.rooms || [], space.walls || [], [], GRID_STEP_N, cellCm, GRID_STEP_N, 1, + ); + } catch { sharedNodes = null; } + let sharedRoomGeometry: any = sharedGeometry?.status === 'ok' + || sharedGeometry?.status === 'degraded-extra' + ? sharedGeometry.roomGeom : null; + if (!sharedRoomGeometry && sharedNodes && sharedNodes.nodes.length) { + try { + const geometry = wallBodiesGeometry( + space.rooms || [], space.walls || [], [], [], GRID_STEP_N, cellCm, GRID_STEP_N, 1, + ); + sharedRoomGeometry = geometry?.status === 'ok' || geometry?.status === 'degraded-extra' + ? geometry.roomGeom : null; + } catch { sharedRoomGeometry = null; } + } + for (const room of (space.rooms || [])) { + const roomId = String(room?.id || ''); + if (!roomId) continue; + let inner: number[][] | null = null; + try { + inner = innerContourForRoom( + space.rooms || [], roomId, space.walls || [], [], + GRID_STEP_N, cellCm, GRID_STEP_N, 1, + sharedRoomGeometry ?? undefined, sharedNodes, + ); + } catch { inner = null; } + violations.push(...checkRoomClearance(roomId, inner, cellCm, GRID_STEP_N)); + } + return violations; + } + +public _junctionLimitLabel(violation: JunctionLimitViolation): string { + const round = (value: number) => String(Math.round(value * 10) / 10); + return this.host._t(`junction.limit_${violation.rule}` as any, { + actual: round(violation.actual), limit: round(violation.limit), + }); + } + +public _junctionLimitsIntroduced( + candidate: any, previousConfig: any, spaceId: string, + candidateGeometry?: any, + ): JunctionLimitViolation[] { + // The baseline must be the previous document AS THE CANDIDATE SEES IT: a + // legacy space carries no wall catalogue at all, so comparing it raw with + // a migrated candidate reported every inherited short segment as new and + // refused legitimate resizes of real plans. Both sides therefore cross + // the same identity barrier first. Current-version documents are already + // materialized, while legacy documents cross that barrier once. + let inherited: JunctionLimitViolation[] | undefined; + const previousSpace = (previousConfig?.spaces || []) + .find((space: any) => space?.id === spaceId); + let fingerprint = ''; + try { fingerprint = spacePhysicalGeometryFingerprint(previousSpace); } + catch { fingerprint = ''; } + const cached = previousConfig && typeof previousConfig === 'object' + ? this._junctionBaselineCache.get(previousConfig) : undefined; + if (cached && fingerprint && cached.fingerprint === fingerprint + && cached.spaceId === spaceId) { + inherited = cached.violations; + } + if (!inherited) { + try { + const baseline = Number(previousConfig?.model_version || 0) >= WALL_SEGMENT_MODEL_VERSION + ? previousConfig + : commitWallSegmentModel(previousConfig).config; + inherited = this._junctionLimitViolations(baseline, spaceId); + } catch { + // An unmigratable baseline proves nothing about inheritance; never + // refuse the write on that basis. + return []; + } + if (previousConfig && typeof previousConfig === 'object' && fingerprint) { + this._junctionBaselineCache.set(previousConfig, { + spaceId, fingerprint, violations: inherited, + }); + } + } + let next: JunctionLimitViolation[] = []; + try { next = this._junctionLimitViolations(candidate, spaceId, candidateGeometry); } + catch { + // #331 §2.5: candidate judgment fails closed; only an unprovable + // baseline above is allowed to fail open. + return [{ rule: 'check_failed', subject: spaceId, actual: 0, limit: 0 }]; + } + return increasedViolations(next, inherited); + } + +public _commitPhysicalGeometry( + name: string, + before: SpaceGeometryState | null, + additionalAuthoredPoints: readonly (readonly number[])[] = [], + ): boolean { + if (!before || !this.host._serverCfg) return false; + const liveCandidate = this.host._serverCfg; + const editedState = this._geometrySnapshotFromConfig(liveCandidate, before.spaceId); + const liveSpace = liveCandidate.spaces.find((space) => space.id === before.spaceId); + if (!liveSpace || spacePhysicalGeometryFingerprint(before) + === spacePhysicalGeometryFingerprint(liveSpace)) return false; + // Preserve #278's existing fail-closed guard. Identity materialisation may + // repair/atomise a degraded legacy projection, but it must never be used to + // make an otherwise rejected user edit look safe. + let legacySafe = false; + try { legacySafe = this._checkSpacePhysicalGeometry(liveCandidate, before.spaceId).ok; } + catch { legacySafe = false; } + if (!legacySafe) { + this._clearGeometryGesture(); + this._restoreGeometryStateLocal(before); + this.host._showToast(this.host._t('toast.geometry_unsafe')); + return false; + } + let committedCandidate: any; + let historyBefore = before; + try { + // ADR 282 Stage 1: every structural writer crosses the same atomic + // canonicalisation/identity barrier. The pure candidate is adopted only + // after every space migrated successfully, so a blocker leaves the live + // config byte-equivalent. + if (Number(liveCandidate.model_version || 0) < 9) { + // The first edit of a pre-v9 document must derive identity and legacy + // zero-wall projection from the pre-edit carrier, not from its already + // moved/split coordinates. + // Materialise that baseline locally, then replay the edited legacy + // projection over it so lineage and Undo share the same stable IDs. + const baselineSource = JSON.parse(JSON.stringify(liveCandidate)); + if (!this._restoreGeometryStateInConfig(baselineSource, before)) + throw new WallSegmentModelError('invalid-room', before.spaceId); + const baseline = commitWallSegmentModel(baselineSource).config; + historyBefore = this._geometrySnapshotFromConfig(baseline, before.spaceId) || before; + const editedWithIdentity = JSON.parse(JSON.stringify(baseline)); + if (!editedState || !this._restoreGeometryStateInConfig( + editedWithIdentity, editedState, true, + )) throw new WallSegmentModelError('invalid-room', before.spaceId); + const baselineSpace = baseline.spaces.find((space) => space.id === before.spaceId); + const editedSpace = editedWithIdentity.spaces.find((space) => space.id === before.spaceId); + // A fixed-topology pre-catalogue edit expresses thickness through the + // legacy `walls[]` projection. Replay it onto the baseline catalogue + // before v9 treats `cm` as authoritative. Geometry-changing writers + // instead rely on lineage: projecting moved walls onto old baseline + // coordinates would incorrectly turn the moved atom into zero. + if (!Array.isArray(editedState.wall_segments) + && JSON.stringify(before.rooms) === JSON.stringify(editedState.rooms)) { + for (const segment of editedSpace?.wall_segments || []) { + segment.cm = thicknessCmAt( + editedState.walls, segment.a, segment.b, GRID_STEP_N, 1, + ); + } + } + const lineageHints = fixedTopologyWallLineageHints( + baselineSpace, before.rooms, editedSpace, + ); + committedCandidate = commitWallSegmentModel(editedWithIdentity, { + lineageHints, lineageSpaceId: before.spaceId, + }).config; + } else committedCandidate = commitWallSegmentModel(liveCandidate).config; + } catch (error) { + this._clearGeometryGesture(); + this._restoreGeometryStateLocal(before); + this._showWallModelMigrationBlocked(error); + return false; + } + let safe = false; + try { + const afterSpace = committedCandidate.spaces.find((space) => space.id === before.spaceId); + // A completed point of the active, grid-snapped chain is authored input, + // even before its first draft record exists in `before`. Include that + // transient carrier in the growth baseline; later conversion from draft + // to room/partition must not count the same coordinate as newly derived. + const authoredPoints = this.host._path.length >= 2 + ? this.host._path.map((point) => [point[0] / NORM_W, point[1] / NORM_W]) + : []; + authoredPoints.push(...additionalAuthoredPoints.map((point) => [point[0], point[1]])); + safe = wallModelOffGridValueCount(afterSpace) + <= wallModelOffGridValueCount(historyBefore, authoredPoints) + && this._checkSpacePhysicalGeometry(committedCandidate, before.spaceId).ok; + } catch { + safe = false; + } + if (!safe) { + this._clearGeometryGesture(); + this._restoreGeometryStateLocal(before); + this.host._showToast(this.host._t('toast.geometry_unsafe')); + return false; + } + // #329: the owner's junction limits refuse the WRITE. An existing plan's + // inherited violations are never re-judged (spec §3), so the candidate is + // compared against the pre-edit document. + const beforeConfig = JSON.parse(JSON.stringify(liveCandidate)); + this._restoreGeometryStateInConfig(beforeConfig, before); + const introduced = this._junctionLimitsIntroduced( + committedCandidate, beforeConfig, before.spaceId, + ); + if (introduced.length) { + this._clearGeometryGesture(); + this._restoreGeometryStateLocal(before); + this.host._showToast(this._junctionLimitLabel(introduced[0])); + return false; + } + adoptWallSegmentModelCandidateInPlace(liveCandidate, committedCandidate); + this._recordGeometry(name, historyBefore); + const afterSpace = liveCandidate.spaces.find((space) => space.id === before.spaceId); + const pending = this.host._pendingPhysicalWrites.get(before.spaceId); + this.host._pendingPhysicalWrites.set(before.spaceId, { + before: pending?.before || historyBefore, + fingerprint: spacePhysicalGeometryFingerprint(afterSpace), + }); + this._saveConfig(); + return true; + } + +public _clearGeometryGesture(): void { + this.host._path = []; + this._clearPlanSnapHover(); + this._clearOpeningPlacement(false); + this.host._mergeSel = null; + this.host._mergeDialog = null; + this.host._splitSel = null; + this.host._pendingSplit = null; + this.host._wallFaceBatch = null; + this.host._wallRepairDiagnostic = null; + this.host._roomDeleteDialog = null; + this.host._wallDialog = null; + this.host._physicalDialog = null; + this.host._physicalSel = null; + this.host._physicalDrag = null; + this.host._physicalRotate = null; + this.host._activeDraftId = null; + this.host._draftSegmentCms = []; + this.host._closingWallCm = null; + this.host._openingDialog = null; + this._rszResetController(); + this.host._decorDraft = null; + this.host._decorMove = null; + this.host._dtDrag = null; + this.host._bdDrag = null; + } + +public _stagePointerCancel(ev: PointerEvent): void { + clearTimeout(this.host._kioskHoldTimer); + if (this.host._swipeStart?.id === ev.pointerId) this.host._swipeStart = null; + if (this.host._physicalDrag?.pid === ev.pointerId || this.host._physicalRotate?.pid === ev.pointerId) { + this._cancelPhysicalGesture(); + return; + } + if (this.host._decorDraft?.pid === ev.pointerId) { + this.host._decorDraft = null; + this.host.requestUpdate(); + return; + } + if (this.host._decorMove?.pid === ev.pointerId || this.host._dtDrag?.pid === ev.pointerId + || this.host._bdDrag?.pid === ev.pointerId) { + this._cancelDecorGesture(); + return; + } + if (this.host._tool === 'opening') { + this.host._cursorPt = null; + this._clearOpeningPlacement(false); + } else if (this.host._tool === 'draw') { + this._clearPlanSnapHover(); + } + const viewportGestureEnded = !!this.host._pinchStart || !!this.host._panStart; + this.host._pointers.delete(ev.pointerId); + if (this.host._pointers.size < 2) this.host._pinchStart = null; + if (this.host._pointers.size === 0) { + this.host._panStart = null; + this.host._panLock = null; + } + if (viewportGestureEnded && this.host._pointers.size === 0) this.host.requestUpdate(); + } + +public _applyGeometryState( + state: SpaceGeometryState, allowHistoryBoundaryRepair = false, + ): boolean { + if (!this.host._canCommitSpace(state.spaceId)) return false; + const before = this._geometrySnapshot(state.spaceId); + if (!before || !this._restoreGeometryStateLocal(state)) return false; + const restoredCandidate = this.host._serverCfg; + const physicalChanged = spacePhysicalGeometryFingerprint(before) + !== spacePhysicalGeometryFingerprint(state); + if (physicalChanged) { + let safe = false; + try { + const check = restoredCandidate + ? this._checkSpacePhysicalGeometry(restoredCandidate, state.spaceId) + : null; + safe = !!check?.ok || !!(allowHistoryBoundaryRepair + && check?.reason === 'wall-degraded-extra'); + } catch { safe = false; } + if (!safe) { + this._restoreGeometryStateLocal(before); + this.host._showToast(this.host._t('toast.geometry_unsafe')); + return false; + } + } + let committedCandidate: any; + try { + committedCandidate = commitWallSegmentModel(restoredCandidate).config; + } catch (error) { + this._restoreGeometryStateLocal(before); + this._showWallModelMigrationBlocked(error); + return false; + } + if (physicalChanged) { + let safe = false; + try { + const check = committedCandidate + ? this._checkSpacePhysicalGeometry(committedCandidate, state.spaceId) + : null; + // A history snapshot can predate the write-time wall degradation that + // canonicalized its command. Restore that one repairable baseline so + // Undo remains byte-exact immediately; _writeConfig still degrades and + // strictly validates the outbound candidate before it can leave the + // card. Every other preflight failure stays fail-closed. + safe = !!check?.ok || !!(allowHistoryBoundaryRepair + && check?.reason === 'wall-degraded-extra'); + } catch { safe = false; } + if (!safe) { + this._restoreGeometryStateLocal(before); + this.host._showToast(this.host._t('toast.geometry_unsafe')); + return false; + } + adoptWallSegmentModelCandidateInPlace(restoredCandidate, committedCandidate); + const afterSpace = restoredCandidate?.spaces.find((space) => space.id === state.spaceId); + const pending = this.host._pendingPhysicalWrites.get(state.spaceId); + this.host._pendingPhysicalWrites.set(state.spaceId, { + before: pending?.before || before, + fingerprint: spacePhysicalGeometryFingerprint(afterSpace), + }); + } else adoptWallSegmentModelCandidateInPlace(restoredCandidate, committedCandidate); + this._clearGeometryGesture(); + if (this.host._space !== state.spaceId) { + this.host._commitSpace(state.spaceId); + this.host._saveNav(); + this.host._restoreZoom(); + } + this.host._modelCache = null; + this.host._frame = null; + this.host._regSignature = ''; + this.host._maybeRebuildDevices(); + this._saveConfig(); + this.host.requestUpdate(); + return true; + } + +public _undoGeometry = (): void => { + if (this.host._physicalDrag || this.host._physicalRotate) { + this._cancelPhysicalGesture(); + return; + } + if (this.host._decorDraft) { this.host._decorDraft = null; this.host.requestUpdate(); return; } + if (this.host._decorMove || this.host._dtDrag || this.host._bdDrag) { + this._cancelDecorGesture(); + return; + } + if (this.host._resize.dragging) { this._rszCancelDrag(); return; } + const command = this.host._geometryHistory.undo(); + if (!command) return; + if (!this._applyGeometryState(command.before, true)) { + this.host._geometryHistory.clear(); + return; + } + this.host._showToast(this.host._t('history.undone', { name: command.name })); + }; + +public _redoGeometry = (): void => { + // Redo and Undo share the same transaction boundary. The first invocation + // cancels an in-progress transform; only the next one navigates history. + if (this.host._physicalDrag || this.host._physicalRotate) { + this._cancelPhysicalGesture(); + return; + } + if (this.host._decorDraft) { this.host._decorDraft = null; this.host.requestUpdate(); return; } + if (this.host._decorMove || this.host._dtDrag || this.host._bdDrag) { + this._cancelDecorGesture(); + return; + } + if (this.host._resize.dragging) { this._rszCancelDrag(); return; } + const command = this.host._geometryHistory.redo(); + if (!command) return; + if (!this._applyGeometryState(command.after, true)) { + this.host._geometryHistory.clear(); + return; + } + this.host._showToast(this.host._t('history.redone', { name: command.name })); + }; + +public _roomAt(p: number[]): RoomCfg | undefined { + return this.host._spaceModel()?.rooms.find((r) => { + const poly = roomPoly(r); + return !!poly && pointStrictlyInside(p, poly); + }); + } + +public _overlapRoom(verts: number[][]): RoomCfg | undefined { + return this.host._spaceModel()?.rooms.find((r) => { + const poly = roomPoly(r); + return !!poly && roomsOverlap(verts, poly); + }); + } + +public _pointInRoom(p: number[], r: RoomCfg): boolean { + if (r.poly) return pointInPolygon(p, r.poly); + return ( + r.x != null && p[0] >= r.x! && p[0] <= r.x! + r.w! && p[1] >= r.y! && p[1] <= r.y! + r.h! + ); + } + +public _contourSelfIntersects(poly: number[][]): boolean { + if (!polyIsSimple(poly)) return true; // transverse crossings + const n = poly.length; + const eps = 0.001; + for (let i = 0; i < n; i++) { + const a = poly[i], b = poly[(i + 1) % n]; + for (let j = i + 1; j < n; j++) { + // Neighbours deliberately share one endpoint, including first ↔ last. + if (j === i + 1 || (i === 0 && j === n - 1)) continue; + const c = poly[j], d = poly[(j + 1) % n]; + const cd = [c[0], c[1], d[0], d[1]]; + const ab = [a[0], a[1], b[0], b[1]]; + // This also catches a closing edge passing through an older vertex and + // collinear overlap, which a proper-crossing test intentionally omits. + if (distToSegment(a, cd) <= eps || distToSegment(b, cd) <= eps || + distToSegment(c, ab) <= eps || distToSegment(d, ab) <= eps) return true; + } + } + return false; + } + +public _canAppendRoomDraftPoint(): boolean { + if (this.host._drawWallCm == null) { this._showPhysicalRange(100); return false; } + if (this.host._path.length >= MAX_DRAFT_POINTS) { + this.host._showToast(this.host._t('toast.physical_limit')); + return false; + } + const spCfg = this.host._curSpaceCfg as any; + const newDraft = !this.host._activeDraftId; + if ((newDraft && (spCfg?.room_drafts || []).length >= MAX_ROOM_DRAFTS) + || this._draftSegmentCount(spCfg) >= MAX_DRAFT_SEGMENTS + // Every accepted segment must be finishable later without trapping + // the user at the partition limit. + || (spCfg?.partitions || []).length + this.host._path.length > MAX_PARTITIONS) { + this.host._showToast(this.host._t('toast.physical_limit')); + return false; + } + return true; + } + +public _markupClick(ev: MouseEvent): void { + if (this.host._vacFit) return; // the fit overlay owns all pointer input + if (!this.host._markup) return; + const space = this.host._spaceModel(); + if (!space) { + this._clearGeometryGesture(); + return; + } + // a pan or pinch just happened — the synthesized click is not a draw + if (this.host._suppressClick) return; + // Room cards swallow markup clicks: dragging, resizing or just clicking a + // card must not feed the active tool (draw point, delete room, merge/split + // pick, opening placement). The drag itself already stops pointer events, + // but the synthesized `click` afterwards still bubbles to the stage. + if (this.host._drag || this.host._rlResize) return; + const path = (ev.composedPath?.() || []) as any[]; + if (path.some((n) => n?.classList?.contains?.('roomlabel') || n?.classList?.contains?.('rlhandle'))) return; + if (path.some((n) => n?.classList?.contains?.('physical-hit'))) return; + const raw = this._svgPoint(ev); + if (this.host._tool === 'select') { + this.host._physicalSel = null; + return; + } + if (this.host._tool === 'resize') { + // a click picks the room for the scale frame; handle drags never get here + if (this.host._resize.dragging || path.some((n) => n?.classList?.contains?.('rszhandle'))) return; + const room = [...space.rooms].reverse().find((r) => this._pointInRoom(raw, r)); + this.host._resize.selectRoom(room?.id || null); + this.host.requestUpdate(); + return; + } + if (this.host._tool === 'delroom') { + this._deleteRoomClick(raw); + return; + } + if (this.host._tool === 'opening') { + this._openingClick(raw); + return; + } + if (this.host._tool === 'merge') { + this._mergeClick(raw); + return; + } + if (this.host._tool === 'wallthick') { + this._wallThickClick(raw); + return; + } + if (this.host._tool === 'split') { + this._splitClick(raw); + return; + } + if (this.host._tool === 'column') { + this._columnClick(raw); + return; + } + // Walls: every completed segment is crash-safe in room_drafts until an + // explicit finish converts the chain or a confirmed face batch consumes it. + this.host._wallRepairDiagnostic = null; + const resolved = this._resolvePlanDrawPoint(raw, ev.shiftKey); + if (resolved.ambiguous) { + this.host._planSnapHover = { + contextKey: resolved.contextKey, candidate: null, conflicts: resolved.conflicts, + }; + this._syncPlanSnapActiveMarker(null); + this._syncPlanSnapConflictMarkers(resolved.conflicts); + this.host._showToast(this.host._t('toast.plan_snap_ambiguous')); + return; + } + let pt = resolved.point; + if (ev.ctrlKey || ev.metaKey) { + // The closure shortcut owns the gesture even before the chain has the + // two existing edges required to form a room. Falling through here + // would turn a refused Ctrl/Cmd+click into an ordinary drawing click. + ev.preventDefault(); + if (this.host._path.length < 3) return; + // Preserve the established shortcut, but commit its closing wall through + // the same graph/draft path as an ordinary click on the first node. + pt = snapNearAxisEndpoint( + this.host._path[this.host._path.length - 1], this.host._path[0], + ); + } + // Island rooms (v1.34.0): drawing INSIDE an existing room is legal — the + // contour may become a nested room (a column, an inner room). Partial + // overlaps are still rejected, but only at closing time, when the whole + // outline is known (roomsOverlap treats full nesting as legal). + if (!this.host._path.length) { + // A free idle click can name a room inside masonry that was completed in + // an earlier session. Shift and architectural snap hits retain the Walls + // gesture, so this offer never steals an intentional new chain. + if (!ev.shiftKey && !resolved.candidate && this._offerExistingWallFace(raw)) return; + // After reload a saved open contour is resumed explicitly by clicking + // either free end. Mid-segment branching is deliberately unsupported. + const endHit = this._draftEndAt(pt); + if (endHit) { + this.host._activeDraftId = endHit.draft.id; + this.host._resumeDraftBySpace[this.host._space] = endHit.draft.id; + this.host._path = endHit.reverse + ? [...endHit.draft.points].reverse().map((p) => [...p]) + : endHit.draft.points.map((p) => [...p]); + this.host._draftSegmentCms = this._adoptDraftCms( + this.host._path, + endHit.reverse + ? [...endHit.draft.segments].reverse().map((s) => s.cm) + : endHit.draft.segments.map((s) => s.cm), + endHit.draft.id, + ); + return; + } + this.host._activeDraftId = null; + this.host._draftSegmentCms = []; + this.host._path = [pt]; + return; + } + const last = this.host._path[this.host._path.length - 1]; + if (this._samePt(pt, last)) return; // repeated click on the same point + // A saved outline may be continued into another saved outline, but only + // endpoint-to-endpoint. The two records become one atomic history step; + // mid-segment branching remains deliberately unsupported. + const join = this._draftEndAt(pt, this.host._activeDraftId || undefined); + if (join) { + this._mergeDraftEndpoint(join); + return; + } + if (!this._canAppendRoomDraftPoint()) return; + // Точка и толщина её отрезка пишутся вместе (#234). Раньше запись жила в + // отдельном методе, который молча выходил при невалидном поле толщины, и + // тогда `_draftSegmentCms` становился короче числа отрезков: превью + // показывало текущее поле, а запись — 15 см. Инвариант читается прямо + // здесь, а не выводится из двух проверок в разных местах. + const cm = this.host._drawWallCm; + if (cm == null) { this._showPhysicalRange(100); return; } + const beforePath = this.host._path.map((point) => [...point]); + this.host._path = [...this.host._path, pt]; + this.host._draftSegmentCms = [...this.host._draftSegmentCms, cm]; + this._persistActiveDraftSegment(); + this._offerWallFaces(beforePath); + } + +public _draftEndAt( + pt: number[], excludeId?: string, + ): { draft: RoomDraftCfg; reverse: boolean } | null { + const space = this.host._spaceModel(); + if (!space) return null; + const view = this.host._viewOr(this.host._baseVb()); + const eps = Math.max(this.host._gridPitch * 0.15, + this.host._stageEl?.clientWidth ? (view.w / this.host._stageEl.clientWidth) * 12 : 0); + for (const draft of space.room_drafts || []) { + if (draft.id === excludeId) continue; + if (draft.points.length < 2) continue; + const first = draft.points[0], last = draft.points[draft.points.length - 1]; + if (Math.hypot(pt[0] - last[0], pt[1] - last[1]) <= eps) return { draft, reverse: false }; + if (Math.hypot(pt[0] - first[0], pt[1] - first[1]) <= eps) return { draft, reverse: true }; + } + return null; + } + +public _mergeDraftEndpoint( + hit: { draft: RoomDraftCfg; reverse: boolean }, + ): void { + const sp = this.host._curSpaceCfg as any; + if (!sp || !this.host._path.length) return; + const beforePath = this.host._path.map((point) => [...point]); + // Capture topology before the records are merged: the other draft is a + // static pre-existing source, not geometry introduced by this click. + const beforeGraphSources = this._wallGraphSources(beforePath); + const drafts = Array.isArray(sp.room_drafts) ? sp.room_drafts : []; + const activeRaw = this.host._activeDraftId + ? drafts.find((d: any) => d.id === this.host._activeDraftId) : null; + const otherRaw = drafts.find((d: any) => d.id === hit.draft.id); + if (!otherRaw) return; + + // `_draftEndAt.reverse` describes how to resume WITH the clicked end at + // the tail. A merge needs that end at the head, hence the opposite order. + const otherPoints = hit.reverse + ? hit.draft.points.map((p) => [...p]) + : [...hit.draft.points].reverse().map((p) => [...p]); + const otherSegments = hit.reverse + ? (otherRaw.segments || []).map((s: any) => ({ ...s })) + : [...(otherRaw.segments || [])].reverse().map((s: any) => ({ ...s })); + const last = this.host._path[this.host._path.length - 1]; + const touching = this._samePt(last, otherPoints[0]); + const connectorCm = touching ? null : this.host._drawWallCm; + if (!touching && connectorCm == null) { this._showPhysicalRange(100); return; } + + const activeSegments = this._draftSegmentsForPath( + this.host._path, activeRaw, this.host._draftSegmentCms, + ); + const mergedPoints = [ + ...this.host._path.map((p) => [...p]), + ...(touching ? otherPoints.slice(1) : otherPoints), + ]; + const mergedSegments = [ + ...activeSegments, + ...(connectorCm == null ? [] : [{ cm: connectorCm }]), + ...otherSegments, + ]; + const closed = mergedPoints.length >= 4 + && this._samePt(mergedPoints[0], mergedPoints[mergedPoints.length - 1]); + const persistedPoints = mergedPoints; + const persistedSegments = mergedSegments; + if (persistedPoints.length > MAX_DRAFT_POINTS) { + this.host._showToast(this.host._t('toast.physical_limit')); + return; + } + const oldSegments = (activeRaw?.segments?.length || 0) + (otherRaw.segments?.length || 0); + if (this._draftSegmentCount(sp) - oldSegments + persistedSegments.length > MAX_DRAFT_SEGMENTS) { + this.host._showToast(this.host._t('toast.physical_limit')); + return; + } + if (closed) { + const ring = persistedPoints.slice(0, -1); + if (this._contourSelfIntersects(ring) || polygonArea(ring) <= 1e-6) { + this.host._showToast(this.host._t('toast.contour_cannot_close')); + return; + } + const clash = this._overlapRoom(ring); + if (clash) { + this.host._showToast(this.host._t('toast.room_overlap', { name: clash.name || '' })); + return; + } + } + + const before = this._geometrySnapshot(); + const id = this.host._activeDraftId || hit.draft.id; + const base = activeRaw || otherRaw; + const saved = { + ...base, id, + points: persistedPoints.map((p) => [p[0] / NORM_W, p[1] / NORM_W]), + segments: persistedSegments, + }; + sp.room_drafts = drafts.filter((d: any) => d.id !== hit.draft.id + && (!this.host._activeDraftId || d.id !== this.host._activeDraftId)); + sp.room_drafts.push(saved); + this.host._activeDraftId = id; + this.host._resumeDraftBySpace[this.host._space] = id; + this.host._path = persistedPoints; + this.host._draftSegmentCms = this._adoptDraftCms( + persistedPoints, persistedSegments.map((s: any) => Number(s.cm)), id, + ); + this.host._physicalSel = null; + this._commitPhysicalGeometry(this.host._t('history.draft_merge'), before); + + if (connectorCm != null) { + this._offerWallFaces(beforePath, beforePath.length - 1, beforeGraphSources); + } + } + +public _adoptDraftCms(path: readonly (readonly number[])[], recorded: readonly (number | null | undefined)[], id?: string): number[] { + const count = Math.max(0, path.length - 1); + const resolved = chainSegmentCms(count, recorded, this.host._drawWallCm, DRAW_WALL_DEFAULT_CM); + if (recorded.length !== count) { + console.debug( + `[houseplan] draft ${id ?? '?'}: восстановлено толщин ${count - recorded.length} (#234)`, + ); + } + return resolved; + } + +public _draftSegmentsForPath( + path: readonly (readonly number[])[], draft: any, + cms: readonly (number | null | undefined)[], + ): Array<{ id?: string; cm: number; [key: string]: any }> { + const points = Array.isArray(draft?.points) + ? draft.points.map((p: any) => [Number(p?.[0]) * NORM_W, Number(p?.[1]) * NORM_W]) + : []; + const stored = Array.isArray(draft?.segments) ? draft.segments : []; + const resolved = chainSegmentCms( + Math.max(0, path.length - 1), cms, this.host._drawWallCm, DRAW_WALL_DEFAULT_CM, + ); + return resolved.map((cm, index) => { + const a = path[index], b = path[index + 1]; + const oldIndex = points.findIndex((oldA: number[], candidate: number) => { + if (candidate + 1 >= points.length) return false; + const oldB = points[candidate + 1]; + return (this._samePt(a, oldA) && this._samePt(b, oldB)) + || (this._samePt(a, oldB) && this._samePt(b, oldA)); + }); + return { ...(oldIndex >= 0 ? stored[oldIndex] : {}), cm }; + }); + } + +public _persistActiveDraftSegment(): void { + if (this.host._path.length < 2 || !this.host._curSpaceCfg) return; + const before = this._geometrySnapshot(); + const sp = this.host._curSpaceCfg as any; + sp.room_drafts ||= []; + if (!this.host._activeDraftId) this.host._activeDraftId = 'draft-' + Date.now().toString(36); + this.host._resumeDraftBySpace[this.host._space] = this.host._activeDraftId; + const i = sp.room_drafts.findIndex((d: any) => d.id === this.host._activeDraftId); + const saved = { + ...(i >= 0 ? sp.room_drafts[i] : {}), + id: this.host._activeDraftId, + points: this.host._path.map((p) => [p[0] / NORM_W, p[1] / NORM_W]), + segments: this._draftSegmentsForPath( + this.host._path, i >= 0 ? sp.room_drafts[i] : null, this.host._draftSegmentCms, + ), + }; + if (i >= 0) sp.room_drafts[i] = saved; + else sp.room_drafts.push(saved); + this._commitPhysicalGeometry(this.host._t('history.draft_segment'), before); + } + +public _activeWallSourceKey(index: number): string { + return `active:${this.host._activeDraftId || 'session'}:${index}`; + } + +public _wallGraphSources(path: readonly (readonly number[])[]): WallGraphSourceSegment[] { + const staticGeometry = this._planStructuralGeometrySnapshot().value; + const sources: WallGraphSourceSegment[] = staticGeometry.segments.map((segment) => ({ + a: segment.a, b: segment.b, key: `static:${segment.key}`, + })); + for (let i = 0; i + 1 < path.length; i++) { + sources.push({ a: path[i], b: path[i + 1], key: this._activeWallSourceKey(i) }); + } + return sources; + } + +public _wallFaceGraph( + sources: readonly WallGraphSourceSegment[], epsilon: number, + ): WallFaceGraph { + // Exact JSON identity avoids stale topology. Source keys include real + // open-span cuts/provenance; presentation opening cuts are intentionally + // absent from the structural graph (#185). + const key = `${this.host._space}|${epsilon}|${JSON.stringify(sources)}`; + const cachedIndex = this.host._wallFaceGraphCache.findIndex((entry) => entry.key === key); + if (cachedIndex >= 0) { + const [cached] = this.host._wallFaceGraphCache.splice(cachedIndex, 1); + this.host._wallFaceGraphCache.push(cached); + return cached.value; + } + const value = buildWallFaceGraph(sources, epsilon); + this.host._wallFaceGraphCache.push({ key, value }); + if (this.host._wallFaceGraphCache.length > 4) this.host._wallFaceGraphCache.shift(); + return value; + } + +public _offerWallFaces( + beforePath: number[][], + addedSegmentIndex = this.host._path.length - 2, + beforeGraphSources?: WallGraphSourceSegment[], + ): void { + if (this.host._path.length < 2 || this.host._wallFaceBatch || this.host._roomDialog) return; + const space = this.host._spaceModel(); + if (!space) return; + const addedSourceKey = this._activeWallSourceKey(addedSegmentIndex); + const epsilon = this.host._gridPitch * 0.0002; + let faces: WallGraphFace[]; + let after: WallGraphSourceSegment[]; + try { + const before = beforeGraphSources || this._wallGraphSources(beforePath); + after = this._wallGraphSources(this.host._path); + faces = findNewWallFacesInGraphs( + this._wallFaceGraph(before, epsilon), this._wallFaceGraph(after, epsilon), + addedSourceKey, + ); + } catch { + // Topology is advisory. The just-persisted draft remains the source of + // truth and drawing can continue when malformed legacy geometry exists. + return; + } + if (!faces.length) { + const repair = planWallFaceRepair(after, { + requiredSourceKey: addedSourceKey, + maxDistance: wallCmToUnits(2, this.host._cellCm, this.host._gridPitch), + gridStep: this.host._gridPitch, + epsilon, + }); + if (repair.kind === 'ambiguous') { + this.host._wallRepairDiagnostic = repair.proposals[0] || null; + this.host._showToast(this.host._t( + this.host._drawWallCm === 0 ? 'toast.zero_wall_ambiguous' : 'toast.wall_repair_ambiguous', + )); + return; + } + if (repair.kind === 'repair' && !this._overlapRoom(repair.face.ring)) { + this._beginWallFaceBatch([{ ...repair.face, repair: repair.proposal }]); + } + return; + } + + // A clean wall-to-wall cut is one product decision about the smaller part, + // even though the planar delta contains both resulting faces. + for (const room of space.rooms) { + const poly = roomPoly(room); + if (!room.id || !poly) continue; + const parts = splitRoomPath(poly, this.host._path, this.host._gridPitch * 0.02); + if (!parts) continue; + const [p1, p2] = parts; + const main = polygonArea(p1) >= polygonArea(p2) ? p1 : p2; + const fresh = main === p1 ? p2 : p1; + const matched = [...faces].sort((left, right) => + Math.abs(left.area - polygonArea(fresh)) - Math.abs(right.area - polygonArea(fresh)) + || left.key.localeCompare(right.key))[0]; + if (!matched) return; + this._beginWallFaceBatch([{ + ...matched, + ring: fresh.map((point) => [point[0], point[1]] as [number, number]), + split: { roomId: room.id, mainPoly: main, newPoly: fresh }, + consumeAllActive: true, + }]); + return; + } + + const eligible = faces.filter((face) => { + if (face.ring.length < 3 || this._contourSelfIntersects(face.ring)) return false; + // roomsOverlap rejects exact duplicates and partial overlap, while full + // nesting in either direction deliberately remains eligible. + return !this._overlapRoom(face.ring); + }); + if (eligible.length) this._beginWallFaceBatch(eligible); + } + +public _beginWallFaceBatch(candidates: WallFaceCandidate[]): void { + this.host._wallRepairDiagnostic = null; + this.host._wallFaceBatch = { + candidates: [...candidates].sort((left, right) => + left.area - right.area || left.key.localeCompare(right.key)), + index: 0, + decisions: [], + activePath: this.host._path.map((point) => [...point]), + activeCms: [...this.host._draftSegmentCms], + activeDraftId: this.host._activeDraftId, + }; + this._clearPlanSnapHover(); + this.host._nameSel = ''; + this.host._areaSel = ''; + this._resetRoomDialogFields(); + this.host._roomDialog = true; + } + +public _offerExistingWallFace(raw: number[]): boolean { + if (this.host._path.length || this.host._wallFaceBatch || this.host._roomDialog) return false; + const epsilon = this.host._gridPitch * 0.0002; + try { + const graph = this._wallFaceGraph(this._wallGraphSources([]), epsilon); + let face = findWallFaceAtPoint(graph, raw, epsilon); + let repair: WallFaceRepairProposal | undefined; + if (!face) { + const result = planWallFaceRepair(this._wallGraphSources([]), { + point: raw, + maxDistance: wallCmToUnits(2, this.host._cellCm, this.host._gridPitch), + gridStep: this.host._gridPitch, + epsilon, + }); + if (result.kind === 'ambiguous') { + this.host._wallRepairDiagnostic = result.proposals[0] || null; + this.host._showToast(this.host._t( + this.host._drawWallCm === 0 ? 'toast.zero_wall_ambiguous' : 'toast.wall_repair_ambiguous', + )); + return true; + } + if (result.kind === 'repair') { + face = result.face; + repair = result.proposal; + } else if (result.kind === 'none') { + const diagnostic = planWallFaceRepair(this._wallGraphSources([]), { + point: raw, + maxDistance: this._cssPxToRender(12), + gridStep: this.host._gridPitch, + epsilon, + }); + if (diagnostic.kind === 'repair') { + this.host._wallRepairDiagnostic = diagnostic.proposal; + this.host._showToast(this.host._t('toast.wall_repair_too_large')); + return true; + } + } + } + if (!face || face.ring.length < 3 || this._contourSelfIntersects(face.ring) + || this._overlapRoom(face.ring)) return false; + this._beginWallFaceBatch([{ ...face, existing: !repair, repair }]); + return true; + } catch { + return false; + } + } + +public _columnClick(raw: number[]): void { + const center = this._snap(raw); + const cm = this.host._drawWallCm; + if (cm == null) { this._showPhysicalRange(COLUMN_MAX_CM, 1); return; } + if (!this.host._curSpaceCfg || this._limitReached('column')) return; + const model = this.host._spaceModel(); + if (!model) return; + const candidate: WallColumnCfg = { + id: 'column-' + Date.now().toString(36), shape: 'square', center, cm: clampColumnCm(cm), angle: 0, + }; + const duplicate = (model.wall_columns || []).find((c) => + sameColumnPlacement(c, candidate, this.host._gridPitch * 0.02)); + if (duplicate) { + clearTimeout(this.host._duplicateColumnTimer); + this.host._duplicateColumnId = duplicate.id; + this.host._duplicateColumnTimer = window.setTimeout(() => { + this.host._duplicateColumnId = null; + }, 900); + this.host._showToast(this.host._t('toast.column_duplicate')); + return; + } + const before = this._geometrySnapshot(); + const sp = this.host._curSpaceCfg as any; + sp.wall_columns ||= []; + sp.wall_columns.push({ ...candidate, center: [center[0] / NORM_W, center[1] / NORM_W] }); + this._commitPhysicalGeometry(this.host._t('history.column_add'), before); + } + +public _openPhysicalDialog( + kind: 'partition' | 'column' | 'draft', id: string, segment?: number, + ): void { + const model = this.host._spaceModel(); + if (!model) return; + if (kind === 'partition') { + const p = model.partitions.find((x) => x.id === id); + if (p) this.host._physicalDialog = { + kind, id, cm: cmToField(p.cm, this.host._imperial), length: this.host._fmtLen(p.a, p.b), + }; + } else if (kind === 'column') { + const c = model.wall_columns.find((x) => x.id === id); + if (c) this.host._physicalDialog = { + kind, id, cm: cmToField(c.cm, this.host._imperial), shape: c.shape, + angle: this.host._angleField(c.shape === 'square' ? canonicalColumnAngle(c.angle) : 0), + }; + } else { + const d = model.room_drafts.find((x) => x.id === id); + const i = Math.max(0, Math.min(d?.segments.length ? d.segments.length - 1 : 0, segment || 0)); + if (d?.segments[i]) this.host._physicalDialog = { + kind, id, segment: i, cm: cmToField(d.segments[i].cm, this.host._imperial), + length: this.host._fmtLen(d.points[i], d.points[i + 1]), + }; + } + } + +public _savePhysicalDialog = (): void => { + const d = this.host._physicalDialog; + const sp = this.host._curSpaceCfg as any; + const model = this.host._spaceModel(); + if (!d || !sp || !model) return; + const raw = strictNumber(d.cm); + if (raw == null) { + this._showPhysicalRange( + d.kind === 'column' ? COLUMN_MAX_CM : 100, d.kind === 'column' ? 1 : 0, + ); + return; + } + const cmRaw = this.host._imperial ? raw * 2.54 : raw; + const max = d.kind === 'column' ? COLUMN_MAX_CM : 100; + const min = d.kind === 'column' ? 1 : 0; + if (!Number.isFinite(cmRaw) || cmRaw < min || cmRaw > max) { + this._showPhysicalRange(max, min); + return; + } + if (d.kind === 'column') { + const current = model.wall_columns.find((x) => x.id === d.id); + if (!current) return; + const rawAngle = strictNumber(d.angle || '0'); + if (d.shape !== 'circle' + && (rawAngle == null || rawAngle < 0 || rawAngle >= 90)) { + this.host._showToast(this.host._t('toast.physical_angle')); + return; + } + const candidate: WallColumnCfg = d.shape === 'circle' + ? { id: d.id, shape: 'circle', center: current.center, cm: cmRaw } + : { id: d.id, shape: 'square', center: current.center, cm: cmRaw, + angle: rawAngle! }; + if (model.wall_columns.some((c) => c.id !== d.id + && sameColumnPlacement(c, candidate, this.host._gridPitch * 0.02))) { + this.host._showToast(this.host._t('toast.column_duplicate')); + return; + } + } + const before = this._geometrySnapshot(); + if (d.kind === 'partition') { + const p = (sp.partitions || []).find((x: any) => x.id === d.id); + if (p && cmRaw === 0 && zeroWallHasOpening(sp.openings, { + kind: 'partition', id: d.id, + })) { + this.host._showToast(this.host._t('toast.zero_wall_opening_conflict')); + return; + } + if (p) p.cm = cmRaw; + } else if (d.kind === 'column') { + const c = (sp.wall_columns || []).find((x: any) => x.id === d.id); + if (c) { + c.cm = clampColumnCm(cmRaw); + c.shape = d.shape === 'circle' ? 'circle' : 'square'; + if (c.shape === 'square') c.angle = strictNumber(d.angle || '0')!; + else delete c.angle; + } + } else { + const draft = (sp.room_drafts || []).find((x: any) => x.id === d.id); + if (draft?.segments?.[d.segment || 0]) draft.segments[d.segment || 0].cm = cmRaw; + } + const committed = this._commitPhysicalGeometry(this.host._t('history.physical_edit'), before); + this.host._physicalDialog = null; + if (!committed) this.host.requestUpdate(); + }; + +public _deletePhysicalSelection = (): void => { + const sel = this.host._physicalSel; + const sp = this.host._curSpaceCfg as any; + if (!sel || !sp) return; + if (sel.kind === 'draft') { this._deleteDraftWhole(); return; } + const before = this._geometrySnapshot(); + if (sel.kind === 'partition') { + const hosted = (sp.openings || []) + .filter((opening: OpeningCfg) => opening.host?.kind === 'partition' + && opening.host.id === sel.id) + .sort((a: OpeningCfg, b: OpeningCfg) => (a.host?.t || 0) - (b.host?.t || 0)); + if (hosted.length) { + this.host._partitionDeleteDialog = { + id: sel.id, + openings: hosted.map((opening: OpeningCfg) => JSON.parse(JSON.stringify(opening))), + }; + return; + } + } + const key = sel.kind === 'partition' ? 'partitions' + : sel.kind === 'column' ? 'wall_columns' : 'room_drafts'; + sp[key] = (sp[key] || []).filter((x: any) => x.id !== sel.id); + if (!sp[key].length) delete sp[key]; + if (this.host._activeDraftId === sel.id) this._cancelPath(); + this.host._physicalSel = null; + this.host._physicalDialog = null; + this._commitPhysicalGeometry(this.host._t('history.physical_delete'), before); + }; + +public _confirmPartitionDelete = (): void => { + const dialog = this.host._partitionDeleteDialog; + const sp = this.host._curSpaceCfg as any; + if (!dialog || !sp) return; + const before = this._geometrySnapshot(); + sp.partitions = (sp.partitions || []).filter((partition: PartitionCfg) => + partition.id !== dialog.id); + if (!sp.partitions.length) delete sp.partitions; + sp.openings = (sp.openings || []).filter((opening: OpeningCfg) => + opening.host?.kind !== 'partition' || opening.host.id !== dialog.id); + if (!sp.openings.length) delete sp.openings; + this.host._partitionDeleteDialog = null; + this.host._physicalSel = null; + this.host._physicalDialog = null; + this._commitPhysicalGeometry(this.host._t('history.physical_delete'), before); + }; + +public _deleteDraftWhole = (): void => { + const id = this.host._physicalDialog?.kind === 'draft' + ? this.host._physicalDialog.id + : this.host._physicalSel?.kind === 'draft' ? this.host._physicalSel.id : null; + const sp = this.host._curSpaceCfg as any; + if (!id || !sp || !confirm(this.host._t('confirm.delete_draft'))) return; + const before = this._geometrySnapshot(); + sp.room_drafts = (sp.room_drafts || []).filter((x: any) => x.id !== id); + if (!sp.room_drafts.length) delete sp.room_drafts; + if (this.host._activeDraftId === id) this._cancelPath(); + this.host._physicalSel = null; + this.host._physicalDialog = null; + this._commitPhysicalGeometry(this.host._t('history.physical_delete'), before); + }; + +public _deleteDraftSegment = (): void => { + const dlg = this.host._physicalDialog; + const sp = this.host._curSpaceCfg as any; + if (!dlg || dlg.kind !== 'draft' || !sp) return; + if (!confirm(this.host._t('confirm.delete_draft_segment'))) return; + const index = (sp.room_drafts || []).findIndex((x: any) => x.id === dlg.id); + if (index < 0) return; + const draft = sp.room_drafts[index]; + const cut = Math.max(0, Math.min(draft.segments.length - 1, dlg.segment || 0)); + const pieces: any[] = []; + const leftPoints = draft.points.slice(0, cut + 1); + const rightPoints = draft.points.slice(cut + 1); + if (leftPoints.length >= 2) pieces.push({ + id: draft.id, points: leftPoints, segments: draft.segments.slice(0, cut), + }); + if (rightPoints.length >= 2) pieces.push({ + id: pieces.length ? `${draft.id}-${Date.now().toString(36)}` : draft.id, + points: rightPoints, segments: draft.segments.slice(cut + 1), + }); + if (pieces.length === 2 && sp.room_drafts.length >= MAX_ROOM_DRAFTS) { + this.host._showToast(this.host._t('toast.physical_limit')); + return; + } + const before = this._geometrySnapshot(); + sp.room_drafts.splice(index, 1, ...pieces); + if (!sp.room_drafts.length) delete sp.room_drafts; + if (this.host._activeDraftId === draft.id) this._cancelPath(); + this.host._physicalDialog = null; + this.host._physicalSel = pieces.length ? { kind: 'draft', id: pieces[0].id } : null; + this._commitPhysicalGeometry(this.host._t('history.draft_segment_delete'), before); + }; + +public _physicalDown(ev: PointerEvent, kind: 'partition' | 'column', id: string): void { + const model = this.host._spaceModel(); + if (!model) return; + ev.stopPropagation(); + capturePointer(ev); + const point = this._svgPoint(ev); + const candidates: Array<{ kind: 'partition' | 'column'; id: string }> = []; + for (const c of [...model.wall_columns].reverse()) { + if (pointInPhysicalBody(point, columnBody(c, this.host._cellCm, this.host._gridPitch))) + candidates.push({ kind: 'column', id: c.id }); + } + for (const p of [...model.partitions].reverse()) { + const body = partitionBody(p.a, p.b, p.cm, this.host._cellCm, this.host._gridPitch); + if (body && pointInPhysicalBody(point, body)) candidates.push({ kind: 'partition', id: p.id }); + } + if (!candidates.some((x) => x.kind === kind && x.id === id)) candidates.unshift({ kind, id }); + const signature = candidates.map((x) => `${x.kind}:${x.id}`).sort().join('|'); + const now = performance.now(); + const cycle = this.host._physicalPickCycle; + const repeat = candidates.length > 1 && cycle?.signature === signature + && now - cycle.at > 380 && now - cycle.at <= 1200 + && Math.hypot(ev.clientX - cycle.x, ev.clientY - cycle.y) <= 10; + const index = repeat ? (cycle.index + 1) % candidates.length + : Math.max(0, candidates.findIndex((x) => x.kind === kind && x.id === id)); + this.host._physicalPickCycle = { signature, index, x: ev.clientX, y: ev.clientY, at: now }; + kind = candidates[index].kind; + id = candidates[index].id; + const base = kind === 'partition' + ? model.partitions.find((x) => x.id === id) + : model.wall_columns.find((x) => x.id === id); + if (!base) return; + this.host._physicalSel = { kind, id }; + this.host._physicalDrag = { + pid: ev.pointerId, kind, id, start: this._svgPoint(ev), + startClient: [ev.clientX, ev.clientY], + before: this._geometrySnapshot(), moved: false, + base: JSON.parse(JSON.stringify(base)), delta: [0, 0], + }; + } + +public _clampPhysicalDelta( + kind: 'partition' | 'column', base: PartitionCfg | WallColumnCfg, delta: number[], + ): number[] { + const points = kind === 'partition' + ? [(base as PartitionCfg).a, (base as PartitionCfg).b] + : [(base as WallColumnCfg).center]; + const xs = points.map((p) => p[0]), ys = points.map((p) => p[1]); + return [ + Math.max(-CANVAS_LIMIT * NORM_W - Math.min(...xs), + Math.min(CANVAS_LIMIT * NORM_W - Math.max(...xs), delta[0])), + Math.max(-CANVAS_LIMIT * NORM_W - Math.min(...ys), + Math.min(CANVAS_LIMIT * NORM_W - Math.max(...ys), delta[1])), + ]; + } + +public _physicalMove(ev: PointerEvent): void { + const drag = this.host._physicalDrag; + if (!drag || drag.pid !== ev.pointerId) return; + ev.stopPropagation(); + const raw = this._svgPoint(ev); + const anchor = drag.kind === 'partition' + ? (drag.base as PartitionCfg).a : (drag.base as WallColumnCfg).center; + const target = this._snap([ + anchor[0] + raw[0] - drag.start[0], anchor[1] + raw[1] - drag.start[1], + ]); + const delta = this._clampPhysicalDelta( + drag.kind, drag.base, [target[0] - anchor[0], target[1] - anchor[1]], + ); + this.host._physicalDrag = { + ...drag, delta, + moved: drag.moved || Math.hypot( + ev.clientX - drag.startClient[0], ev.clientY - drag.startClient[1], + ) >= 5, + }; + } + +public _physicalUp(ev: PointerEvent): void { + const drag = this.host._physicalDrag; + if (!drag || drag.pid !== ev.pointerId) return; + ev.stopPropagation(); + this.host._physicalDrag = null; + if (!drag.moved) { + this._registerPhysicalTap(drag.kind, drag.id); + return; + } + const model = this.host._spaceModel(); + if (!this.host._curSpaceCfg || !model) return; + const sp = this.host._curSpaceCfg as any; + if (drag.kind === 'partition') { + const p = (sp.partitions || []).find((x: any) => x.id === drag.id); + const base = drag.base as PartitionCfg; + if (p) { + const moved: PartitionCfg = { + ...base, + a: [base.a[0] + drag.delta[0], base.a[1] + drag.delta[1]], + b: [base.b[0] + drag.delta[0], base.b[1] + drag.delta[1]], + }; + p.a = [moved.a[0] / NORM_W, moved.a[1] / NORM_W]; + p.b = [moved.b[0] / NORM_W, moved.b[1] / NORM_W]; + for (const opening of sp.openings || []) { + if (opening.host?.kind !== 'partition' || opening.host.id !== drag.id) continue; + const resolved = resolvePartitionOpeningCompat( + opening, [moved], NORM_W, this.host._cellCm, this.host._gridPitch, + ).resolved; + if (resolved) Object.assign( + opening, materializePartitionOpening(opening, resolved, NORM_W), + ); + } + } + } else { + const c = (sp.wall_columns || []).find((x: any) => x.id === drag.id); + const base = drag.base as WallColumnCfg; + const candidate: WallColumnCfg = { + ...base, + center: [base.center[0] + drag.delta[0], base.center[1] + drag.delta[1]], + } as WallColumnCfg; + if (model.wall_columns.some((x) => x.id !== drag.id + && sameColumnPlacement(x, candidate, this.host._gridPitch * 0.02))) { + this.host._showToast(this.host._t('toast.column_duplicate')); + return; + } + if (c) c.center = [ + (base.center[0] + drag.delta[0]) / NORM_W, + (base.center[1] + drag.delta[1]) / NORM_W, + ]; + } + this._commitPhysicalGeometry(this.host._t('history.physical_move'), drag.before); + } + +public _registerPhysicalTap( + kind: 'partition' | 'column' | 'draft', id: string, segment?: number, + ): void { + const now = performance.now(); + const twice = this.host._physicalLastTap?.kind === kind + && this.host._physicalLastTap.id === id + && this.host._physicalLastTap.segment === segment + && now - this.host._physicalLastTap.at <= 360; + this.host._physicalLastTap = { kind, id, segment, at: now }; + if (twice) { + this.host._physicalLastTap = null; + this._openPhysicalDialog(kind, id, segment); + } + } + +public _cancelPhysicalGesture(): void { + this.host._physicalDrag = null; + this.host._physicalRotate = null; + this.host.requestUpdate(); + } + +public _physicalRotateDown(ev: PointerEvent, c: WallColumnCfg): void { + if (c.shape !== 'square' || !this.host._spaceModel()) return; + ev.preventDefault(); + ev.stopPropagation(); + capturePointer(ev); + const p = this._svgPoint(ev); + this.host._physicalSel = { kind: 'column', id: c.id }; + this.host._physicalRotate = { + pid: ev.pointerId, id: c.id, center: [...c.center], + startAngle: Math.atan2(p[1] - c.center[1], p[0] - c.center[0]) * 180 / Math.PI, + baseAngle: canonicalColumnAngle(c.angle), angle: canonicalColumnAngle(c.angle), + before: this._geometrySnapshot(), moved: false, + }; + } + +public _physicalRotateMove(ev: PointerEvent): void { + const drag = this.host._physicalRotate; + if (!drag || drag.pid !== ev.pointerId) return; + ev.preventDefault(); + ev.stopPropagation(); + const p = this._svgPoint(ev); + const pointerAngle = Math.atan2(p[1] - drag.center[1], p[0] - drag.center[0]) * 180 / Math.PI; + const raw = drag.baseAngle + pointerAngle - drag.startAngle; + const angle = canonicalColumnAngle(ev.shiftKey ? raw : Math.round(raw / 5) * 5); + this.host._physicalRotate = { ...drag, angle, moved: drag.moved || Math.abs(raw - drag.baseAngle) >= 0.5 }; + } + +public _physicalRotateUp(ev: PointerEvent): void { + const drag = this.host._physicalRotate; + if (!drag || drag.pid !== ev.pointerId) return; + ev.preventDefault(); + ev.stopPropagation(); + this.host._physicalRotate = null; + const model = this.host._spaceModel(); + if (!drag.moved || !this.host._curSpaceCfg || !model) return; + const current = model.wall_columns.find((c) => c.id === drag.id); + if (!current || current.shape !== 'square') return; + const candidate: WallColumnCfg = { ...current, angle: drag.angle }; + if (model.wall_columns.some((c) => c.id !== drag.id + && sameColumnPlacement(c, candidate, this.host._gridPitch * 0.02))) { + this.host._showToast(this.host._t('toast.column_duplicate')); + return; + } + const stored = (this.host._curSpaceCfg as any).wall_columns?.find((c: any) => c.id === drag.id); + if (!stored) return; + stored.angle = drag.angle; + this._commitPhysicalGeometry(this.host._t('history.physical_edit'), drag.before); + } + +public _rszRooms(): { id: string; poly: number[][]; wall_ids?: string[] }[] { + const out: { id: string; poly: number[][]; wall_ids?: string[] }[] = []; + const space = this.host._spaceModel(); + if (!space) return out; + for (const r of space.rooms) { + const poly = r.id ? roomPoly(r) : null; + if (poly) out.push({ + id: r.id!, poly, + wall_ids: Array.isArray(r.wall_ids) ? [...r.wall_ids] : undefined, + }); + } + return coalesceResizeRooms(out, Math.max(1e-12, this.host._gridPitch * 1e-12)); + } + +public _rszOpenings(): SafeOpeningIn[] { + return this.host._openingsR.map((o) => ({ + id: o.id, x: o.rx, y: o.ry, length: o.rlen, + hosted: !!o.host, angle: o.angle, type: o.type, + })); + } + +public _rszObstacles(): SafeResizeObstacle[] { + const space = this.host._spaceModel(); + if (!space) return []; + const obstacles: SafeResizeObstacle[] = []; + for (const partition of space.partitions || []) { + obstacles.push({ + kind: 'segment', a: [...partition.a], b: [...partition.b], + half: wallCmToUnits(partition.cm, this.host._cellCm, this.host._gridPitch) / 2, + }); + } + for (const draft of space.room_drafts || []) { + for (let i = 0; i + 1 < draft.points.length; i++) { + obstacles.push({ + kind: 'segment', a: [...draft.points[i]], b: [...draft.points[i + 1]], + half: wallCmToUnits( + Number.isFinite(Number(draft.segments?.[i]?.cm)) + ? Number(draft.segments[i].cm) : DRAW_WALL_DEFAULT_CM, + this.host._cellCm, this.host._gridPitch, + ) / 2, + }); + } + } + for (const column of space.wall_columns || []) { + const half = wallCmToUnits(column.cm, this.host._cellCm, this.host._gridPitch) / 2; + obstacles.push({ + kind: 'circle', center: [...column.center], + radius: column.shape === 'square' ? half * Math.SQRT2 : half, + }); + } + return obstacles; + } + +public _rszOptsFor(a: number[], b: number[]): SafeResizeOptions { + const cm = thicknessCmAt(this.host._spaceWalls, a, b, this.host._wallKeyPitch, NORM_W); + const exact = this.host._spaceWalls.filter((wall) => Array.isArray(wall.a) && Array.isArray(wall.b) + && wall.a.length >= 2 && wall.b.length >= 2).map((wall) => ({ + wall, + a: [wall.a![0] * NORM_W, wall.a![1] * this.host._spaceH], + b: [wall.b![0] * NORM_W, wall.b![1] * this.host._spaceH], + })); + const axis = Math.abs(a[0] - b[0]) <= this.host._gridPitch * 0.05 ? 'v' + : Math.abs(a[1] - b[1]) <= this.host._gridPitch * 0.05 ? 'h' : null; + const overlaps = exact.filter((entry) => { + if (!axis) return false; + if (axis === 'h') { + if (Math.abs(entry.a[1] - a[1]) > this.host._gridPitch * 0.05 + || Math.abs(entry.b[1] - a[1]) > this.host._gridPitch * 0.05) return false; + return Math.min(Math.max(a[0], b[0]), Math.max(entry.a[0], entry.b[0])) + - Math.max(Math.min(a[0], b[0]), Math.min(entry.a[0], entry.b[0])) > this.host._gridPitch * 0.05; + } + if (Math.abs(entry.a[0] - a[0]) > this.host._gridPitch * 0.05 + || Math.abs(entry.b[0] - a[0]) > this.host._gridPitch * 0.05) return false; + return Math.min(Math.max(a[1], b[1]), Math.max(entry.a[1], entry.b[1])) + - Math.max(Math.min(a[1], b[1]), Math.min(entry.a[1], entry.b[1])) > this.host._gridPitch * 0.05; + }); + const exactCms = new Set(overlaps.map((entry) => Number(entry.wall.cm)).filter((value) => value > 0)); + return { + minDim: this.host._cmToUnits(MIN_ROOM_CM), + eps: this.host._gridPitch * 0.05, + step: this.host._gridPitch, + movingHalf: cm > 0 ? wallCmToUnits(cm, this.host._cellCm, this.host._gridPitch) / 2 : 0, + obstacles: this._rszObstacles(), + thicknessConflict: exactCms.size > 1, + }; + } + +public _rszResolution( + roomId: string, edge: number, renderSnapshot?: string, + ): SafeResizeResolution { + const snap = this.host._resize.snapshotIdentity || renderSnapshot || this._rszSnapshot(); + const key = `${this.host._space}|${this.host._cellCm}|${this.host._gridPitch}|${snap}`; + const cacheKey = `${roomId}:${edge}`; + return this.host._resize.resolve(key, cacheKey, () => { + const rooms = [...(this.host._resize.rooms || this._rszRooms())]; + const room = rooms.find((candidate) => candidate.id === roomId); + const a = room?.poly?.[edge] || [0, 0]; + const b = room?.poly?.[(edge + 1) % (room?.poly?.length || 1)] || [0, 0]; + return resolveSafeResize( + rooms, [...(this.host._resize.openings || this._rszOpenings())], roomId, edge, + this._rszOptsFor(a, b), + ); + }); + } + +public _rszSnapshot(): string { + return JSON.stringify(this._geometrySnapshot() || { + spaceId: this.host._space, rooms: [], openings: [], walls: [], open_spans: [], + }); + } + +public _rszResetController(): void { + const hadPreview = this.host._resize.preview !== null; + this.host._resize.reset(); + if (hadPreview) this.host._cfgEpoch++; + } + +public _rszProjectPreview( + snapshot: string, + polys: Record, + ops: Record, + changedRoomIds: readonly string[], + sourceRooms: readonly { id: string; poly: number[][]; wall_ids?: string[] }[], + ): ResizeProjectionResult { + // #329 AC7a: only the LAST projection may explain the refusal, so the + // previous verdict never leaks into a rejection of another kind. + this.host._rszLimitViolation = null; + const real = this.host._serverCfg?.spaces.find((s: any) => s.id === this.host._space); + if (!real || !this.host._serverCfg) return { ok: false, reason: 'missing-context' }; + const s = JSON.parse(snapshot); // fresh deep copies every move — free to mutate + const sp: any = { + ...real, + rooms: s.rooms, + openings: s.openings || [], + }; + for (const key of ['walls', 'wall_segments', 'open_spans', 'room_drafts', 'partitions', 'wall_columns', 'decor'] as const) { + if (s[key] !== undefined) (sp as any)[key] = s[key]; + else delete (sp as any)[key]; + } + for (const key of ['plan_x', 'plan_y', 'plan_scale', 'plan_scale_x', 'plan_scale_y', 'plan_angle'] as const) { + if (s.plan_transform?.[key] !== undefined) (sp as any)[key] = s.plan_transform[key]; + else delete (sp as any)[key]; + } + const H = this.host._spaceH; + for (const [id, poly] of Object.entries(polys)) { + const r = sp.rooms.find((x: any) => x.id === id); + if (!r) continue; + r.poly = poly.map((p) => [p[0] / NORM_W, p[1] / H]); + delete r.x; delete r.y; delete r.w; delete r.h; // a resized room is saved as a polygon + } + for (const [id, c] of Object.entries(ops)) { + const o = (sp.openings || []).find((x: any) => x.id === id); + if (!o) continue; + o.x = c[0] / NORM_W; + o.y = c[1] / H; + } + // Geometry that belongs to a wall must ride in the SAME live overlay as + // its room polygons. Map from the immutable snapshot on every move (never + // from the previous preview), so atomic identity and thickness cannot lag + // behind or accumulate rounding error during a long drag. + const oldSpans: [number[], number[]][] = []; + const newSpans: [number[], number[]][] = []; + for (const id of changedRoomIds) { + const oldR = sourceRooms.find((r) => r.id === id); + const nr = sp.rooms.find((x: any) => x.id === id); + if (!oldR || !nr?.poly) continue; + const newPoly = nr.poly.map((p: number[]) => [p[0] * NORM_W, p[1] * H] as number[]); + if (oldR.poly.length !== newPoly.length) continue; + const ids = Array.isArray((oldR as any).wall_ids) ? (oldR as any).wall_ids : []; + for (let i = 0; i < oldR.poly.length; i++) { + oldSpans.push([oldR.poly[i], oldR.poly[(i + 1) % oldR.poly.length]]); + newSpans.push([newPoly[i], newPoly[(i + 1) % newPoly.length]]); + const id = ids[i]; + const segment = typeof id === 'string' + ? (sp.wall_segments || []).find((item) => item.id === id) + : null; + if (segment) { + segment.a = [newPoly[i][0] / NORM_W, newPoly[i][1] / NORM_W]; + segment.b = [ + newPoly[(i + 1) % newPoly.length][0] / NORM_W, + newPoly[(i + 1) % newPoly.length][1] / NORM_W, + ]; + } + } + } + if (oldSpans.length) { + if (Array.isArray(sp.walls) && sp.walls.length) { + const rekeyed = rekeyWallsAfterMoveChecked( + sp.walls, oldSpans, newSpans, this.host._wallKeyPitch, NORM_W, 'fixed-topology', + ); + if (rekeyed.rejected) return { ok: false, reason: 'wall-metadata' }; + sp.walls = rekeyed.walls; + } + } + const wallCarriers: [number[], number[]][] = []; + for (const room of sp.rooms || []) { + const poly = roomPoly(room); + if (!poly || poly.length < 2) continue; + for (let index = 0; index < poly.length; index++) { + wallCarriers.push([ + [poly[index][0] * NORM_W, poly[index][1] * NORM_W], + [poly[(index + 1) % poly.length][0] * NORM_W, + poly[(index + 1) % poly.length][1] * NORM_W], + ]); + } + } + // Old plans may contain explicit historical debt (for example an authored + // off-grid thickness breakpoint). Safe Resize must not silently repair it, + // but it may not create a new invalid record either. Remove byte-identical + // old records as a multiset and prove only newly written/split records. + // That keeps pointermove bounded by the touched thickness profile instead + // of comparing every historical record with every carrier twice per frame. + const wallSignature = (wall: any): string => JSON.stringify([ + wall?.key, wall?.cm, wall?.a, wall?.b, + ]); + const oldWallCounts = new Map(); + for (const wall of s.walls || []) { + const key = wallSignature(wall); + oldWallCounts.set(key, (oldWallCounts.get(key) || 0) + 1); + } + const changedWalls: WallEntry[] = []; + for (const wall of sp.walls || []) { + const key = wallSignature(wall); + const remaining = oldWallCounts.get(key) || 0; + if (remaining) oldWallCounts.set(key, remaining - 1); + else changedWalls.push(wall); + } + if (wallRecordCarrierViolations( + changedWalls, wallCarriers, this.host._wallKeyPitch, NORM_W, s.walls || [], + ).length) return { ok: false, reason: 'wall-metadata' }; + const preflight = this._rszSpaceCandidateGeometry(this.host._space, sp); + if (!preflight.ok) { + return { ok: false, reason: 'physical-geometry' }; + } + // #329 AC7a: a step that would ADD a junction-limit violation is never + // projected, so the drag stops at the last allowed position instead of + // committing an impossible plan. + const limitCandidate = { + ...this.host._serverCfg, + spaces: (this.host._serverCfg?.spaces || []).map( + (space) => (space?.id === this.host._space ? sp : space), + ), + }; + // #330 §4.7: reuse the masonry pass already computed by the resize + // preflight instead of rebuilding it while checking room clearance. + const limited = this._junctionLimitsIntroduced( + limitCandidate, this.host._serverCfg, this.host._space, preflight.wallGeometry, + ); + if (limited.length) { + this.host._rszLimitViolation = limited[0]; + return { ok: false, reason: 'junction-limit' }; + } + return { + ok: true, + value: { + preview: { space: this.host._space, sp }, + beforeWalls: s.walls || [], + afterWalls: sp.walls || [], + artifact: preflight.wallGeometry, + }, + }; + } + +public _rszAcceptPreview( + preview: ResizePreview | null, wallGeometry: ResizeWallArtifact | null, + ): void { + this.host._cfgEpoch++; + if (!preview || !wallGeometry) return; + const projected = wallBodiesGeometryPath(wallGeometry); + if (!projected) return; + const key = `${this.host._space}|${this.host._cfgEpoch}|${preview.sp.rooms.length}`; + Object.defineProperty(projected, 'sourceFingerprint', { + value: contentFingerprint([preview.sp, this.host._cellCm, this.host._gridPitch]), + enumerable: false, + }); + const entry = { key, value: projected }; + lruWrite(this.host._wallUnionPool, key, entry, 8); + this.host._wallUnionCache = entry; + } + +public _rszSpaceCandidateGeometry(spaceId: string, sp: any): { + ok: boolean; + wallGeometry: ReturnType | null; + } { + if (!this.host._serverCfg) return { ok: false, wallGeometry: null }; + const candidate = { + ...this.host._serverCfg, + spaces: this.host._serverCfg.spaces.map((space) => + space.id === spaceId ? sp : space), + } as ServerConfig; + let wallGeometry: ReturnType | null = null; + try { + const check = this._checkSpacePhysicalGeometry( + candidate, spaceId, (geometry) => { wallGeometry = geometry; }, + ); + return { ok: check.ok, wallGeometry }; + } catch { + return { ok: false, wallGeometry: null }; + } + } + +public _rszSpaceCandidateRenderable(spaceId: string, sp: any): boolean { + if (!this.host._serverCfg) return false; + try { + const candidate = { + ...this.host._serverCfg, + spaces: this.host._serverCfg.spaces.map((space) => + space.id === spaceId ? sp : space), + } as ServerConfig; + return this._checkSpacePhysicalGeometry(candidate, spaceId).ok; + } catch { + return false; + } + } + +public _rszCandidateRenderable(preview: { space: string; sp: any } | null): boolean { + return !!preview && preview.space === this.host._space + && this._rszSpaceCandidateRenderable(preview.space, preview.sp); + } + +public _rszEdgeDown(ev: PointerEvent, roomId: string, edge: number): void { + if (this.host._tool !== 'resize' || this.host._resize.dragging) return; + ev.stopPropagation(); + ev.preventDefault(); + const rooms = this._rszRooms(); + const resolution = this._rszResolution(roomId, edge); + if (!resolution.enabled) { + this.host._showToast(this._rszReasonText(resolution.reason)); + return; + } + capturePointer(ev); + const plan = resolution.plan; + const start = this._svgPoint(ev); + const wallUnionKey = `${this.host._space}|${this.host._cfgEpoch}|${rooms.length}`; + const wallUnionBefore = this.host._wallUnionCache?.key === wallUnionKey + ? this.host._wallUnionCache.value : null; + const snapshotIdentity = this._rszSnapshot(); + this.host._resize.begin({ + pointerId: ev.pointerId, start: [start[0], start[1]], roomId, plan, + options: this._rszOptsFor(plan.a, plan.b), rooms, + openings: this._rszOpenings(), snapshotIdentity, + before: JSON.parse(snapshotIdentity) as SpaceGeometryState, + wallUnionBefore, + }); + } + +public _rszReasonText(reason: SafeResizeReason): string { + return this.host._t(`resize.disabled.${reason}` as I18nKey); + } + +public _rszDisabledActivate(ev: Event, reason: SafeResizeReason): void { + ev.stopPropagation(); + ev.preventDefault(); + this.host._showToast(this._rszReasonText(reason)); + } + +public _rszDisabledKey(ev: KeyboardEvent, reason: SafeResizeReason): void { + if (ev.key !== 'Enter' && ev.key !== ' ') return; + this._rszDisabledActivate(ev, reason); + } + +public _rszMove(ev: PointerEvent): void { + if (!this.host._resize.ownsPointer(ev.pointerId)) return; + ev.stopPropagation(); + const p = this._svgPoint(ev); + const result = this.host._resize.move({ + pointerId: ev.pointerId, + point: [p[0], p[1]], + step: this.host._gridPitch, + snap: (point) => { + const snapped = this._snap(point); + return [snapped[0], snapped[1]]; + }, + project: (snapshot, polys, openings, changedRoomIds, rooms) => + this._rszProjectPreview(snapshot, polys, openings, changedRoomIds, rooms), + publish: (preview, artifact) => this._rszAcceptPreview(preview, artifact), + measure: (candidate, plan) => this._rszEdgeLabels(candidate, plan), + }); + if (result.kind === 'rejected') { + // Persistence metadata is part of the geometry transaction. If wall or + // virtual-span rekeying would be lossy, keep the last complete preview: + // the pointer visibly stops there and pointerup can commit only that + // already-rendered safe candidate. + if (result.notify) { + // #329 AC7a: a step stopped by a junction limit names THAT rule — the + // generic "geometry cannot be saved" wording would hide which limit + // the wall ran into. + this.host._showToast(this.host._rszLimitViolation + ? `${this.host._t('resize.limit_stopped')} — ` + + this._junctionLimitLabel(this.host._rszLimitViolation) + : this.host._t('resize.preview_failed')); + } + this.host.requestUpdate(); + return; + } + if (result.kind === 'accepted') this.host.requestUpdate(); + } + +public _rszUp(ev: PointerEvent): void { + if (!this.host._resize.ownsPointer(ev.pointerId)) return; + ev.stopPropagation(); + const result = this.host._resize.finish({ + pointerId: ev.pointerId, + currentSnapshotIdentity: this._rszSnapshot(), + validatePreview: (preview) => this._rszCandidateRenderable(preview), + }); + if (result.kind === 'no-op') { + // HP-1550-01: nothing to restore — the preview never touched the config + this.host._cfgEpoch++; + this.host.requestUpdate(); + return; + } + if (result.kind === 'rejected') { + this.host._cfgEpoch++; + this.host._showToast(this.host._t('resize.commit_failed')); + this.host.requestUpdate(); + return; + } + // Commit the exact preview in one step. No simplify/rebuild pass is allowed: + // preview and persistence share the same fixed-topology candidate. + const preview = result.preview; + const sp = this.host._serverCfg?.spaces.find((space) => space.id === preview.space); + if (sp) { + sp.rooms = preview.sp.rooms; + sp.openings = preview.sp.openings; + if (Array.isArray(preview.sp.walls)) { + if (preview.sp.walls.length) sp.walls = preview.sp.walls; + else delete sp.walls; + } + if (Array.isArray(preview.sp.wall_segments)) sp.wall_segments = preview.sp.wall_segments; + } + // the click synthesized after the drag must not re-pick the selection + this.host._suppressClick = true; + setTimeout(() => (this.host._suppressClick = false), 0); + this._commitPhysicalGeometry(this.host._t('history.resize_room'), result.before); + this.host.requestUpdate(); + } + +public _rszCancelDrag(pointerId?: number): void { + const result = this.host._resize.cancel(this._rszSnapshot(), pointerId); + if (result.kind === 'no-op') return; + // HP-1550-01/-03: a cancel just drops the overlay — the real config was + // never touched, so there is nothing to restore, no undo step and no write + this.host._cfgEpoch++; + if (result.restoreWallUnion) { + // The server-backed geometry did not change during the gesture. The + // preview advanced the structural epoch and temporarily selected its + // candidate union, but Cancel returns to the byte-identical pre-drag + // config. Alias that already-proved result under the fresh epoch instead + // of paying another O(n log n) polygon union on the next paint. + const space = this.host._spaceModel(); + if (space) { + const key = `${this.host._space}|${this.host._cfgEpoch}|${space.rooms.length}`; + const entry = { key, value: result.restoreWallUnion }; + lruWrite(this.host._wallUnionPool, key, entry, 8); + this.host._wallUnionCache = entry; + } + } + this.host.requestUpdate(); + } + +public _rszPointerCancel(ev: PointerEvent): void { + if (!this.host._resize.ownsPointer(ev.pointerId)) return; + ev.stopPropagation(); + this._rszCancelDrag(ev.pointerId); + } + +public _rszEdgeLabels( + res: { polys: Record }, plan: SafeResizePlan, + sourceRooms: readonly { id: string; poly: number[][] }[] | null = this.host._resize.rooms, + ): ResizeLiveLabel[] { + const rooms = sourceRooms; + const labels: ResizeLiveLabel[] = []; + const own = res.polys[plan.roomId] || rooms?.find((r) => r.id === plan.roomId)!.poly; + if (!own || !rooms) return labels; + const n = own.length; + // Длины — между внутренними гранями, как и площадь ниже (#233). Раньше + // здесь считалась осевая длина, и одно облачко подписей несло две разные + // конвенции: «3.00 × 4.00» по центрам стен рядом с площадью по полу. + const spanCms = this._rszInnerSpanCms(plan.roomId, own, res.polys); + // The moving wall is obvious under the pointer. Only its two side walls + // remain useful measurements, and each one owns the matching highlight. + for (const edge of resizeMeasuredEdges(own, plan.edge)) { + const a = own[edge], b = own[(edge + 1) % n]; + const cm = spanCms?.[edge]; + labels.push({ + kind: 'length', + x: (a[0] + b[0]) / 2, + y: (a[1] + b[1]) / 2, + text: cm == null ? this.host._fmtLen(a, b) + : formatLength(cm, this.host.hass?.config?.unit_system?.length === 'mi'), + edge: { a: [a[0], a[1]], b: [b[0], b[1]] }, + }); + } + // Live areas sit beside the moving wall, one on each owner side. Placement + // is pure screen-space math fed by the stage size cached by ResizeObserver; + // no pointermove DOM measurement is allowed. + const imperial = this.host.hass?.config?.unit_system?.length === 'mi'; + const ids = plan.roomIds; + const walls = this.host._spaceWalls; + const physical = this.host._physicalBodiesR(); + const base = this.host._baseVb(); + const currentView = this.host._view && this.host._view.w > 0 && this.host._view.h > 0 + ? this.host._view + : { x: base[0], y: base[1], w: base[2], h: base[3] }; + const [cachedW, cachedH] = this.host._lastValidStageSize || [currentView.w, currentView.h]; + const view = { + ...currentView, + stageWidth: Math.max(1, cachedW), + stageHeight: Math.max(1, cachedH), + }; + const space = this.host._spaceModel(); + const cfgSize = this.host._config?.icon_size ?? 2.5; + const iconPct = cfgSize > 8 ? 2.5 : cfgSize; + const iconPx = space + ? iconCqw(iconPct, space, currentView.w, this.host._kiosk ? this.host._kioskScale.icon : 1) + * view.stageWidth / 100 + : 24; + const gearHeightPx = Math.max(10, iconPx * 0.77); + const gearText = this.host._t('room.settings_short'); + // Mirrors the CSS proportions conservatively: icon + gap + two paddings + + // localized text at the button's 0.42*height font size. + const gearWidthPx = gearHeightPx * ( + 0.55 + 0.35 * 0.42 + 0.76 + Math.max(1, gearText.length) * 0.66 * 0.42 + ); + for (const id of ids) { + const poly = res.polys[id] || rooms.find((r) => r.id === id)!.poly; + // The preview is already the active render model. Reuse the same shared + // masonry union + contour cache that the following render consumes; + // rebuilding both independently here doubled one Resize frame. + const floor = walls.length && space + ? (this.host._innerRoomContour(space, id) || poly) + : poly; + const m2 = physical.length + ? geometryArea(floorMinusBodies(floor, physical)) + * Math.pow(this.host._cellCm / this.host._gridPitch, 2) / 1e4 + : areaM2(floor, this.host._gridPitch, this.host._cellCm); + const text = formatArea(m2, imperial); + const placement = placeResizeAreaLabel({ + poly, + edge: plan.edgeByRoom[id], + text, + view, + gearCenter: poleOfInaccessibility(poly), + gearWidthPx, + gearHeightPx, + }); + labels.push({ + kind: 'area', roomId: id, + x: placement.anchor[0], y: placement.anchor[1], text, placement, + }); + } + return labels; + } + +public _rszInnerSpanCms( + roomId: string, own: number[][], polys: Record, + ): number[] | null { + const rooms = Object.keys(polys).length + ? Object.entries(polys).map(([id, poly]) => ({ id, poly })) + : this.host._spaceModel()?.rooms; + if (!rooms?.length) return null; + const walls = this.host._spaceWalls; + if (!walls.length) return null; + const openCuts = this.host._openCuts(); + const offsets = ownEdgeOffsets( + rooms, roomId, walls, openCuts, + this.host._wallKeyPitch, this.host._cellCm, this.host._gridPitch, NORM_W, + ); + if (!offsets || offsets.length !== own.length) return null; + const perUnitCm = this.host._cellCm / this.host._gridPitch; + return own.map((_, edge) => innerEdgeSpan(own, edge, offsets) * perUnitCm); + } + +public _renderResizeMeasurements(): TemplateResult | typeof nothing { + const live = this.host._resize.liveLabels; + if (!live?.length) return nothing; + const lengths = live.filter((label) => label.kind === 'length'); + const areas = live.filter((label) => label.kind === 'area'); + return svg``; + } + +public _renderResizeLayer(view: { x: number; y: number; w: number; h: number }): TemplateResult { + const hr = Math.max(view.w * 0.013, 5); // finger-sized HIT radius on touch, like .vacfithandle + // Wall-handle glyph: half the old circle — a wall segment with two arrows + // pointing perpendicular to it (the directions the wall can be dragged). + // Drawn in local coords (wall along X), rotated per edge at render time. + // The invisible circle above keeps the full finger-sized hit area and the + // HP-1550-04 hit-test priority over openings. + const s = hr / 2, f = (v: number) => v.toFixed(1); + const iconD = + `M ${f(-0.7 * s)} 0 H ${f(0.7 * s)}` + + ` M 0 ${f(-0.22 * s)} V ${f(-s)} M ${f(-0.32 * s)} ${f(-0.6 * s)} L 0 ${f(-s)} L ${f(0.32 * s)} ${f(-0.6 * s)}` + + ` M 0 ${f(0.22 * s)} V ${f(s)} M ${f(-0.32 * s)} ${f(0.6 * s)} L 0 ${f(s)} L ${f(0.32 * s)} ${f(0.6 * s)}`; + const parts: TemplateResult[] = []; + const rooms = this._rszRooms(); + // A geometry snapshot is a deep clone/serialization. It is the cache key + // for every handle in this frame, so compute it once per layer render — + // never once per room edge. During a drag the immutable gesture snapshot + // remains authoritative inside _rszResolution. + const renderSnapshot = this.host._resize.snapshotIdentity || this._rszSnapshot(); + for (const r of rooms) { + for (let i = 0; i < r.poly.length; i++) { + const a = r.poly[i], b = r.poly[(i + 1) % r.poly.length]; + if (Math.hypot(b[0] - a[0], b[1] - a[1]) < this.host._gridPitch) continue; + const mx = f((a[0] + b[0]) / 2), my = f((a[1] + b[1]) / 2); + const ang = f(Math.atan2(b[1] - a[1], b[0] - a[0]) * 180 / Math.PI); + const resolution = this._rszResolution(r.id, i, renderSnapshot); + const disabled = !resolution.enabled; + const reason = disabled ? this._rszReasonText(resolution.reason) : this.host._t('title.markup_resize'); + parts.push(svg` this._rszEdgeDown(e, r.id, i)} + @pointermove=${(e: PointerEvent) => this._rszMove(e)} + @pointerup=${(e: PointerEvent) => this._rszUp(e)} + @pointercancel=${(e: PointerEvent) => this._rszPointerCancel(e)} + @lostpointercapture=${(e: PointerEvent) => this._rszPointerCancel(e)} + @click=${disabled ? (e: Event) => this._rszDisabledActivate(e, resolution.reason) : null} + @keydown=${disabled ? (e: KeyboardEvent) => this._rszDisabledKey(e, resolution.reason) : null}> + ${reason} + `); + parts.push(svg``); + } + } + return svg`${parts}`; + } + +public _partitionOpeningCuts( + space: SpaceModel | undefined = this.host._spaceModel(), + accept: (opening: OpeningCfg) => boolean = () => true, + ): PartitionOpeningCut[] { + if (!space) return []; + const config = this.host._curSpaceCfg?.id === space.id ? this.host._curSpaceCfg : null; + const openings = geometryOpenings( + config, space, this.host._cellCm, this.host._gridPitch, NORM_W, + ); + return geometryPartitionOpeningCuts(openings, accept); + } + +public _decorSnapGeometry(excludeId?: string): SnapGeometry { + const space = this.host._spaceModel(); + if (!space) return { points: [], segments: [] }; + const cacheKey = excludeId || ''; + const cached = this.host._decorSnapCache; + if (cached && cached.epoch === this.host._cfgEpoch && cached.space === this.host._space + && cached.height === this.host._decorH && cached.exclude === cacheKey) { + return cached.geometry; + } + const parts: SnapGeometry[] = []; + for (const shape of this.host._decorList) { + if (shape.id === excludeId) continue; + if (shape.kind === 'line') { + const a = [shape.x1 * NORM_W, shape.y1 * this.host._decorH]; + const b = [shape.x2 * NORM_W, shape.y2 * this.host._decorH]; + parts.push({ + points: [a, b, [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]], + segments: [{ a, b }], + }); + } else if (shape.kind === 'text') { + parts.push({ points: [[shape.x * NORM_W, shape.y * this.host._decorH]], segments: [] }); + } else { + const box = this.host._decorBoxOf(shape); + if (box) parts.push(boxAnchors(box)); + } + } + for (const room of space.rooms) { + const poly = roomPoly(room); + if (!poly?.length) continue; + parts.push({ + points: poly.flatMap((p, i) => { + const q = poly[(i + 1) % poly.length]; + return [p, [(p[0] + q[0]) / 2, (p[1] + q[1]) / 2]]; + }), + segments: poly.map((p, i) => ({ a: p, b: poly[(i + 1) % poly.length] })), + }); + } + const geometry = mergeSnapGeometry(parts); + this.host._decorSnapCache = { + epoch: this.host._cfgEpoch, space: this.host._space, height: this.host._decorH, + exclude: cacheKey, geometry, + }; + return geometry; + } + +public _decorSnap(raw: number[], pointerType = 'mouse', excludeId?: string): number[] { + const stage = this.host._stageEl; + const view = this.host._viewOr(this.host._baseVb()); + const px = pointerType === 'touch' || pointerType === 'pen' ? 14 : 8; + const tolerance = stage ? (view.w / Math.max(1, stage.clientWidth)) * px : this.host._gridPitch; + return snapDecorPoint( + raw, this._decorSnapGeometry(excludeId), tolerance, + (point) => this._snap(point), + ).point; + } + +public _replaceDecor(id: string, patch: Partial): void { + const sp = this.host._curSpaceCfg; + if (!sp) return; + sp.decor = this.host._decorList.map((shape) => shape.id === id ? { ...shape, ...patch } : shape); + // The edited shape is excluded from the gesture's magnet candidates, so + // keep the cached geometry of every *other* object across pointermoves. + // A different exclude id/cache key rebuilds it when the next gesture starts. + this.host.requestUpdate(); + } + +public _cancelDecorGesture(): void { + const before = this.host._decorMove?.before || this.host._dtDrag?.before || this.host._bdDrag?.before; + const sp = before && this.host._serverCfg?.spaces.find((space) => space.id === before.spaceId); + if (before && sp) { + const copy = (value: T): T => JSON.parse(JSON.stringify(value)); + if (before.decor !== undefined) sp.decor = copy(before.decor); + else delete sp.decor; + for (const key of ['plan_x', 'plan_y', 'plan_scale', 'plan_scale_x', 'plan_scale_y', 'plan_angle'] as const) + delete sp[key]; + Object.assign(sp, copy(before.plan_transform || {})); + this.host._cfgEpoch++; + } + this.host._decorMove = null; + this.host._dtDrag = null; + this.host._bdDrag = null; + this.host.requestUpdate(); + } + +public _decorPointerDown(ev: PointerEvent): boolean { + const t = this.host._decorTool; + // A DRAWING tool owns the whole canvas. Pressing on top of an existing + // shape must start a NEW figure at that very point — otherwise a line can + // never begin at the end of another line, because the old line grabs the + // press first (owner, 2026-08-04). Only select/erase talk to shapes, and + // only for them does the shape's own handler get to deal with the event. + const onShape = (t === 'select' || t === 'erase') + ? ((ev.target as HTMLElement).closest?.('.dshape') as SVGElement | null) + : null; + if (onShape) return true; // the shape's own handler deals with it + if (t === 'line' || t === 'rect' || t === 'ellipse') { + ev.preventDefault(); + const p = this._decorSnap(this._svgPoint(ev), ev.pointerType); + this.host._decorDraft = { kind: t, a: p, b: p, pid: ev.pointerId }; + capturePointer(ev); + return true; + } + if (t === 'text') { + // …and the press did NOT land on an existing label: those are the one + // exception to the inertness above (see _decorShapeDown). + const p = this._decorSnap(this._svgPoint(ev), ev.pointerType); + this.host._decorTextDialog = { + x: clampCanvasN(p[0] / NORM_W), y: clampCanvasN(p[1] / this.host._decorH), + text: '', color: this.host._decorStyle.color, opacity: this.host._decorStyle.opacity, + angle: '0', sizeCm: decorUnitsToCm(DECOR_TEXT_BASE, this.host._cellCm, this.host._gridPitch), + }; + this.host._decorTextSelection = { start: 0, end: 0 }; + return true; + } + if (t === 'furniture') { + // The furniture tool is a STAMP: the palette arms a symbol, the press + // puts it down at its real size and the editor goes back to `select` + // with the new piece selected (owner: «сразу выделен»). Without an armed + // symbol the press does nothing but keep the pan — pressing the canvas + // must not silently place whatever was chosen last week. + if (!this.host._furnPalette) return false; + ev.preventDefault(); + this._furnPlace(this._svgPoint(ev), ev.shiftKey); + return true; + } + // Empty-space selection belongs only to Select. Erase is an object + // command: a miss must be a true no-op. With SVG text its painted glyphs + // do not cover the whole logical label box, so clearing here made a click + // between letters appear to erase only the selection outline. + if (t === 'select') this.host._decorSel = null; + // …and under its own tool the picture is grabbable by its body + // (docs/BACKDROP.md §2). Only INSIDE the image rect: press beside the + // picture and the plane still pans with one finger. + if (this.host._bdMovable) { + const r = this.host._bdRect!; + const p = this._svgPoint(ev); + const cx = r.x + r.w / 2, cy = r.y + r.h / 2; + const a = (-normalizeAngle(r.angle) * Math.PI) / 180; + const dx = p[0] - cx, dy = p[1] - cy; + const local = [cx + dx * Math.cos(a) - dy * Math.sin(a), cy + dx * Math.sin(a) + dy * Math.cos(a)]; + if (local[0] >= r.x && local[0] <= r.x + r.w && local[1] >= r.y && local[1] <= r.y + r.h) { + ev.preventDefault(); + return this._bdStart(ev); + } + } + return false; // pan is allowed + } + +public _decorCommitDraft(): void { + const d = this.host._decorDraft; + this.host._decorDraft = null; + if (!d) return; + const min = this.host._gridPitch * 0.5; + if (!validDecorDraft(d.kind, d.a, d.b, min)) return; + const W = NORM_W, H = this.host._decorH; + const st = this.host._decorStyle; + const before = this._geometrySnapshot(); + const id = 'dc' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5); + // creation had no canvas guard at all while the MOVE did — a draft could be + // born outside the range the mover then refused to leave. One limit, both + // ends of the gesture (docs/CANVAS.md §9). + const cn = clampCanvasN; + let shape: DecorShape; + if (d.kind === 'line') { + shape = { id, kind: 'line', x1: cn(d.a[0] / W), y1: cn(d.a[1] / H), + x2: cn(d.b[0] / W), y2: cn(d.b[1] / H), ...decorStylePatch(st, false) } as DecorShape; + } else { + const x = cn(Math.min(d.a[0], d.b[0]) / W), y = cn(Math.min(d.a[1], d.b[1]) / H); + const w = Math.abs(d.b[0] - d.a[0]) / W, h = Math.abs(d.b[1] - d.a[1]) / H; + shape = { id, kind: d.kind, x, y, w, h, ...decorStylePatch(st, true) } as DecorShape; + } + const sp = this.host._curSpaceCfg; + sp.decor = [...this.host._decorList, shape]; + this.host._decorSel = id; + this._recordGeometry(this.host._t('history.decor_add'), before); + this._saveConfig(); + this.host.requestUpdate(); + } + +public _decorShapeDown(ev: PointerEvent, shape: DecorShape): void { + if (this.host._mode !== 'decor') return; + // Under any other tool the shape is not a target at all: the press has to + // reach the stage, where the drawing tool starts a new figure (or the + // backdrop tool grabs the picture). Swallowing it here was the bug — the + // click on a line end did nothing but keep the old selection alive. + const t = this.host._decorTool; + // ONE exception (owner, 2026-08-04): under the TEXT tool an existing LABEL + // is a target again, and pressing it opens its editor instead of starting + // a new label on top of it. Everything else stays inert — a press on a + // line or a rectangle under the text tool still reaches the stage and + // creates a new label there (the CSS keeps them pointer-inert, this is + // the belt to that pair of braces). + if (t === 'text') { + if (shape.kind !== 'text') return; + ev.stopPropagation(); + ev.preventDefault(); + this._decorOpenText(shape); + return; + } + if (t !== 'select' && t !== 'erase') return; + ev.stopPropagation(); + ev.preventDefault(); + if (t === 'erase') { + this.host._decorEraseConfirm = { id: shape.id, kind: shape.kind }; + return; + } + this.host._decorSel = shape.id; + this.host._decorMove = { + id: shape.id, start: this._svgPoint(ev), orig: JSON.parse(JSON.stringify(shape)), + pid: ev.pointerId, moved: false, before: this._geometrySnapshot(), + }; + capturePointer(ev); + } + +public _decorMoveUpdate(ev: PointerEvent): void { + const m = this.host._decorMove!; + // Furniture is dragged by its CENTRE and has a magnet of its own + // (docs/FURNITURE.md §5) — a different rule, not a different gesture. + if (m.orig?.kind === 'furniture') { this._furnMoveUpdate(ev); return; } + const p = this._svgPoint(ev); + const o0: any = m.orig; + // The delta used to be what got snapped, which preserves whatever off-grid + // offset the shape already had: a legacy shape at 0.3013 stayed at 0.3013 + // for ever, one step at a time. Snap the RESULTING ANCHOR instead, so one + // drag is enough to put any shape on the grid (docs/CANVAS.md §9). + const ax0 = (o0.kind === 'line' ? o0.x1 : o0.x) * NORM_W; + const ay0 = (o0.kind === 'line' ? o0.y1 : o0.y) * this.host._decorH; + const anchor = this._decorSnap( + [ax0 + (p[0] - m.start[0]), ay0 + (p[1] - m.start[1])], + ev.pointerType, m.id, + ); + let dx = (anchor[0] - ax0) / NORM_W; + let dy = (anchor[1] - ay0) / this.host._decorH; + // audit follow-up L4 gave decor a bounds clamp of -0.25..1.25 — the plan + // was a sheet with edges then. It is not any more (docs/CANVAS.md): the + // clamp is now the same garbage limit the backend enforces, so decor can + // follow a plan that lives at 2.7 and still cannot be flung to 1e100. + const o: any = m.orig; + const curX = o.kind === 'line' ? Math.min(o.x1, o.x2) : o.x; + const curY = o.kind === 'line' ? Math.min(o.y1, o.y2) : o.y; + const w = o.kind === 'line' ? Math.abs(o.x2 - o.x1) : (o.w || 0); + const h = o.kind === 'line' ? Math.abs(o.y2 - o.y1) : (o.h || 0); + const lim = CANVAS_LIMIT; + dx = Math.max(-lim - curX, Math.min(lim - curX - w, dx)); + dy = Math.max(-lim - curY, Math.min(lim - curY - h, dy)); + if (dx || dy) m.moved = true; + const sp = this.host._curSpaceCfg; + sp.decor = this.host._decorList.map((x) => { + if (x.id !== m.id) return x; + const o: any = m.orig; + if (x.kind === 'line') return { ...x, x1: o.x1 + dx, y1: o.y1 + dy, x2: o.x2 + dx, y2: o.y2 + dy }; + return { ...x, x: o.x + dx, y: o.y + dy }; + }); + this.host.requestUpdate(); + } + +public _decorShapeDbl(ev: MouseEvent, shape: DecorShape): void { + if (this.host._mode !== 'decor' || this.host._decorTool !== 'select') return; + ev.preventDefault(); + ev.stopPropagation(); + this.host._decorMove = null; + this.host._decorSel = shape.id; + this._openDecorProperties(shape); + } + +public _openDecorProperties(shape: DecorShape): void { + if (this.host._mode !== 'decor' || this.host._decorTool !== 'select') return; + if (shape.kind === 'text') { + this._decorOpenText(shape); + return; + } + if (!['line', 'rect', 'ellipse', 'furniture'].includes(shape.kind)) return; + const style = this.host._decorResolvedStyle(shape); + const line = shape.kind === 'line' ? shape : null; + const box = this.host._decorBoxOf(shape); + this.host._decorShapeDialog = { + id: shape.id, + kind: shape.kind as 'line' | 'rect' | 'ellipse' | 'furniture', + color: style.color, opacity: style.opacity, widthCm: style.widthCm, + angle: this.host._angleField(line + ? normalizeAngle(segmentAngle( + [line.x1 * NORM_W, line.y1 * this.host._decorH], + [line.x2 * NORM_W, line.y2 * this.host._decorH], + )) + : normalizeAngle((shape as any).angle)), + ...(line ? { + lengthCm: decorUnitsToCm( + Math.hypot((line.x2 - line.x1) * NORM_W, (line.y2 - line.y1) * this.host._decorH), + this.host._cellCm, this.host._gridPitch, + ), + lineStyle: line.line_style === 'dashed' ? 'dashed' : 'solid', + } : {}), + ...(box ? { + sizeWCm: decorUnitsToCm(box.w, this.host._cellCm, this.host._gridPitch), + sizeHCm: decorUnitsToCm(box.h, this.host._cellCm, this.host._gridPitch), + } : {}), + ...(shape.kind === 'furniture' ? { symbol: shape.symbol } : {}), + ...(shape.kind === 'rect' || shape.kind === 'ellipse' ? { + fill: style.fill, fillColor: style.fillColor, fillOpacity: style.fillOpacity, + } : {}), + }; + } + +public _decorOpenText(shape: DecorShape): void { + if (shape.kind !== 'text') return; + let text = String(shape.text ?? ''); + const hasInline = [...text.matchAll(/\{([^{}\r\n]+)\}/g)] + .some((m) => !!liveTextReference(m[1])); + const explicitUnit = String(shape.unit ?? '').trim(); + // Old dialogs represented the entity state as attr="state"; inline text + // represents that same value with a bare `{entity}` token. + const legacyAttr = String(shape.attr ?? '').trim().toLowerCase() === 'state' + ? null + : shape.attr; + const legacyToken = !hasInline && !explicitUnit + ? liveTextToken(shape.entity, legacyAttr) + : ''; + const preserveLegacy = !!String(shape.entity ?? '').trim() && !hasInline + && (!legacyToken || !!explicitUnit); + if (legacyToken && !preserveLegacy) { + const slot = text.indexOf('{}'); + text = slot >= 0 + ? text.slice(0, slot) + legacyToken + text.slice(slot + 2) + : `${text}${text ? ' ' : ''}${legacyToken}`; + } + this.host._decorTextDialog = { + id: shape.id, x: shape.x, y: shape.y, text, + color: safeStoredColor(shape.color, this.host._decorStyle.color), + opacity: clamp01(shape.opacity, this.host._decorStyle.opacity), + angle: this.host._angleField(shape.angle), + sizeCm: this.host._decorTextSizeCm(shape), + pickerEntity: shape.entity || '', + preserveLegacy: preserveLegacy || undefined, + }; + this.host._decorTextSelection = { start: text.length, end: text.length }; + } + +public _decorRememberTextSelection(el: HTMLTextAreaElement): void { + this.host._decorTextSelection = { + start: el.selectionStart ?? el.value.length, + end: el.selectionEnd ?? el.value.length, + }; + } + +public _decorInsertLiveVariable(attr: string | null): void { + const d = this.host._decorTextDialog; + if (!d) return; + const token = liveTextToken(d.pickerEntity, attr); + if (!token) return; + const old = d.text; + const start = Math.max(0, Math.min(old.length, this.host._decorTextSelection.start)); + const end = Math.max(start, Math.min(old.length, this.host._decorTextSelection.end)); + if (old.length - (end - start) + token.length > 200) return; + const text = old.slice(0, start) + token + old.slice(end); + const caret = start + token.length; + this.host._decorTextSelection = { start: caret, end: caret }; + // Inserting a new inline reference is the explicit replacement of any + // unrepresentable legacy link; the stale side fields can now be dropped. + this.host._decorTextDialog = { ...d, text, preserveLegacy: undefined }; + this.host.updateComplete.then(() => { + const el = this.host.renderRoot.querySelector('textarea.dtarea'); + if (!el) return; + el.focus(); + el.setSelectionRange(caret, caret); + }); + } + +public _decorSaveText(): void { + const d = this.host._decorTextDialog; + // The user's own line breaks are kept; the surrounding whitespace is not. + const text = String(d?.text ?? '').replace(/\r\n?/g, '\n').trim(); + if (!d || !text) { this.host._decorTextDialog = null; return; } + const before = this._geometrySnapshot(); + const sp = this.host._curSpaceCfg; + const textStyle = { + color: d.color, opacity: clamp01(d.opacity), + size_cm: Number(Math.max(0.1, Math.min(DECOR_TEXT_CM_MAX, d.sizeCm)).toFixed(4)), + ...(normalizeAngle(d.angle) ? { angle: normalizeAngle(d.angle) } : {}), + }; + if (d.id) { + sp.decor = this.host._decorList.map((x) => { + if (x.id !== d.id) return x; + if (x.kind !== 'text') return x; + // A legacy unit or non-canonical attribute cannot be represented by + // the inline grammar. Keep those fields until the user explicitly + // replaces the binding; ordinary representable links still migrate. + if (d.preserveLegacy) { + const { angle: _angle, size: _size, scale: _scale, ...straight } = x; + return { ...straight, text, ...textStyle }; + } + // beta.9 and earlier stored one live link beside the text. Saving in + // the new editor migrates it to inline tokens and drops the old fields. + const { entity, attr, unit, ...rest } = x; + const { angle: _angle, size: _size, scale: _scale, ...straight } = rest; + return { ...straight, text, ...textStyle }; + }); + } else { + const id = 'dc' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5); + sp.decor = [...this.host._decorList, { id, kind: 'text', x: d.x, y: d.y, + text, ...textStyle }]; + this.host._decorSel = id; + } + this.host._decorTextDialog = null; + this._recordGeometry(this.host._t(d.id ? 'history.decor_edit' : 'history.decor_add'), before); + this._saveConfig(); + this.host.requestUpdate(); + } + +public _decorSaveShape(): void { + const d = this.host._decorShapeDialog; + if (!d) return; + const before = this._geometrySnapshot(); + const style: DecorStyle = { + color: d.color, opacity: clamp01(d.opacity), + widthCm: Math.max(0.1, Math.min(100, Number(d.widthCm) || 0.1)), + fill: !!d.fill, fillColor: d.fillColor || d.color, + fillOpacity: clamp01(d.fillOpacity, 0.25), + }; + const sp = this.host._curSpaceCfg; + sp.decor = this.host._decorList.map((shape) => { + if (shape.id !== d.id) return shape; + const fillable = d.kind === 'rect' || d.kind === 'ellipse'; + const visual = decorStylePatch(style, fillable); + if (shape.kind === 'line') { + const cx = ((shape.x1 + shape.x2) / 2) * NORM_W; + const cy = ((shape.y1 + shape.y2) / 2) * this.host._decorH; + const length = decorCmToUnits(Math.max(0.1, Number(d.lengthCm) || 0.1), this.host._cellCm, this.host._gridPitch); + const rad = (normalizeAngle(d.angle) * Math.PI) / 180; + const dx = Math.cos(rad) * length / 2, dy = Math.sin(rad) * length / 2; + const a = this._snap([cx - dx, cy - dy]); + const b = this._snap([cx + dx, cy + dy]); + const { width: _legacyWidth, line_style: _oldLineStyle, ...rest } = shape; + return { ...rest, ...visual, + x1: clampCanvasN(a[0] / NORM_W), y1: clampCanvasN(a[1] / this.host._decorH), + x2: clampCanvasN(b[0] / NORM_W), y2: clampCanvasN(b[1] / this.host._decorH), + ...(d.lineStyle === 'dashed' ? { line_style: 'dashed' as const } : {}), + } as DecorShape; + } + if (shape.kind === 'rect' || shape.kind === 'ellipse' || shape.kind === 'furniture') { + const oldW = shape.w * NORM_W, oldH = shape.h * this.host._decorH; + const w = Math.max(this.host._gridPitch, snapToGrid( + decorCmToUnits(Number(d.sizeWCm), this.host._cellCm, this.host._gridPitch), this.host._gridPitch, + )); + const h = Math.max(this.host._gridPitch, snapToGrid( + decorCmToUnits(Number(d.sizeHCm), this.host._cellCm, this.host._gridPitch), this.host._gridPitch, + )); + const cx = shape.x * NORM_W + oldW / 2, cy = shape.y * this.host._decorH + oldH / 2; + const angle = normalizeAngle(d.angle); + const topLeft = resizedBoxTopLeft( + { x: cx - w / 2, y: cy - h / 2 }, angle, + (point) => this._snap(point), + ); + const { width: _legacyWidth, angle: _oldAngle, ...rest } = shape; + return { ...rest, ...visual, + x: clampCanvasN(topLeft[0] / NORM_W), y: clampCanvasN(topLeft[1] / this.host._decorH), + w: w / NORM_W, h: h / this.host._decorH, + ...(shape.kind === 'furniture' && d.symbol ? { symbol: d.symbol } : {}), + ...(angle ? { angle } : {}), + } as DecorShape; + } + return shape; + }); + this.host._decorStyle = { ...style, + fill: d.kind === 'rect' || d.kind === 'ellipse' ? style.fill : this.host._decorStyle.fill, + fillColor: d.kind === 'rect' || d.kind === 'ellipse' ? style.fillColor : this.host._decorStyle.fillColor, + fillOpacity: d.kind === 'rect' || d.kind === 'ellipse' ? style.fillOpacity : this.host._decorStyle.fillOpacity, + }; + this.host._decorShapeDialog = null; + this._recordGeometry(this.host._t('history.decor_edit'), before); + this._saveConfig(); + this.host.requestUpdate(); + } + +public _dtPivot(sh: DecorShape): number[] { + if (sh.kind === 'line') return [ + ((sh.x1 + sh.x2) / 2) * NORM_W, + ((sh.y1 + sh.y2) / 2) * this.host._decorH, + ]; + if (sh.kind === 'furniture' || sh.kind === 'rect' || sh.kind === 'ellipse') + return [(sh.x + sh.w / 2) * NORM_W, (sh.y + sh.h / 2) * this.host._decorH]; + return [sh.x * NORM_W, sh.y * this.host._decorH]; + } + +public _dtApply(id: string, patch: { textSizeCm?: number; angle?: number }): void { + const sp = this.host._curSpaceCfg; + if (!sp) return; + sp.decor = this.host._decorList.map((x) => { + if (x.id !== id) return x; + const rest: any = { ...x }; + if (x.kind === 'text' && patch.textSizeCm !== undefined) { + delete rest.size; + delete rest.scale; + } + const out: any = { ...rest }; + if (patch.textSizeCm !== undefined) + out.size_cm = Number(Math.max(0.1, Math.min(DECOR_TEXT_CM_MAX, patch.textSizeCm)).toFixed(4)); + if (patch.angle !== undefined) { + if (patch.angle) out.angle = Number(patch.angle.toFixed(2)); + else delete out.angle; // straight again = the field goes away + } + return out; + }); + this.host._cfgEpoch++; + this.host.requestUpdate(); + } + +public _dtStart( + ev: PointerEvent, kind: 'scale' | 'rotate', corner?: number[], lineEnd?: 0 | 1, + ): void { + const sh = this.host._dtSel; + if (!sh) return; + ev.stopPropagation(); + ev.preventDefault(); + const [ax, ay] = this._dtPivot(sh); + const p = this._svgPoint(ev); + const box = this.host._decorBoxOf(sh); + this.host._dtDrag = { + id: sh.id, kind, pid: ev.pointerId, ax, ay, + r0: Math.hypot(p[0] - ax, p[1] - ay), + a0: (Math.atan2(p[1] - ay, p[0] - ax) * 180) / Math.PI, + textSizeCm0: sh.kind === 'text' ? this.host._decorTextSizeCm(sh) : 1, + angle0: sh.kind === 'line' ? 0 : Number(sh.angle) || 0, + // which corner is being pulled, and the box it started from: a piece of + // furniture is resized about the OPPOSITE corner, so both have to be + // remembered — a label, scaled about its anchor, needs neither + sgx: corner?.[0], sgy: corner?.[1], + orig: box || undefined, + origShape: JSON.parse(JSON.stringify(sh)), + before: this._geometrySnapshot(), + lineEnd, + moved: false, + }; + capturePointer(ev); + } + +public _dtMove(ev: PointerEvent): void { + const d = this.host._dtDrag; + if (!d) return; + const p = this._svgPoint(ev); + if (d.lineEnd !== undefined && d.origShape.kind === 'line') { + const at = this._decorSnap(p, ev.pointerType, d.id); + const nx = clampCanvasN(at[0] / NORM_W), ny = clampCanvasN(at[1] / this.host._decorH); + const old = d.origShape; + const ox = d.lineEnd === 0 ? old.x1 : old.x2; + const oy = d.lineEnd === 0 ? old.y1 : old.y2; + if (Math.abs(nx - ox) > 1e-9 || Math.abs(ny - oy) > 1e-9) d.moved = true; + this._replaceDecor(d.id, d.lineEnd === 0 ? { x1: nx, y1: ny } : { x2: nx, y2: ny }); + return; + } + if (d.kind === 'scale' && d.orig) { + // Box geometry preserves ratio by default and separates axes with Shift. + // Each dimension lands on a whole cell. + const box = resizeDecorBox( + d.orig, d.sgx ?? 1, d.sgy ?? 1, p[0], p[1], + !ev.shiftKey, this.host._gridPitch, this.host._gridPitch, + ); + const changed = Math.abs(box.x - d.orig.x) > 1e-6 || Math.abs(box.y - d.orig.y) > 1e-6 + || Math.abs(box.w - d.orig.w) > 1e-6 || Math.abs(box.h - d.orig.h) > 1e-6; + if (!changed && !d.moved) return; + d.moved ||= changed; + this._decorApplyBox(d.id, box); + return; + } + if (d.kind === 'scale') { + // uniform, about the anchor: the distance from the anchor is invariant + // under the block's own rotation, so a rotated block scales the same way + const r = Math.hypot(p[0] - d.ax, p[1] - d.ay); + if (d.r0 < 1e-6) return; + const sizeCm = Math.max(0.1, Math.min(DECOR_TEXT_CM_MAX, d.textSizeCm0 * (r / d.r0))); + const changed = Math.abs(sizeCm - d.textSizeCm0) > 1e-6; + if (!changed && !d.moved) return; + d.moved ||= changed; + this._dtApply(d.id, { textSizeCm: sizeCm }); + return; + } + const a = (Math.atan2(p[1] - d.ay, p[0] - d.ax) * 180) / Math.PI; + let ang = d.angle0 + (a - d.a0); + // 5° rotation steps; Shift remains an angle-only precision modifier. + if (!ev.shiftKey) ang = Math.round(ang / DT_ANGLE_STEP) * DT_ANGLE_STEP; + ang = ((ang % 360) + 360) % 360; + if (ang > 180) ang -= 360; + const changed = Math.abs(ang - d.angle0) > 1e-6; + if (!changed && !d.moved) return; + d.moved ||= changed; + this._dtApply(d.id, { angle: ang }); + } + +public _dtUp(): void { + const d = this.host._dtDrag; + this.host._dtDrag = null; + if (d?.moved) { + this._recordGeometry(this.host._t('history.decor_transform'), d.before); + this._saveConfig(); + } + this.host.requestUpdate(); + } + +public _dtMeasure(): void { + const sh = this.host._dtSel; + if (!sh) { + if (this.host._dtBox) { this.host._dtBox = null; this.host.requestUpdate(); } + return; + } + let box: { id: string; x: number; y: number; w: number; h: number }; + if (sh.kind === 'line') { + const x1 = sh.x1 * NORM_W, y1 = sh.y1 * this.host._decorH; + const x2 = sh.x2 * NORM_W, y2 = sh.y2 * this.host._decorH; + box = { id: sh.id, x: Math.min(x1, x2), y: Math.min(y1, y2), + w: Math.abs(x2 - x1), h: Math.abs(y2 - y1) }; + } else if (sh.kind === 'furniture' || sh.kind === 'rect' || sh.kind === 'ellipse') { + // …and furniture needs no measuring at all: its box IS the config. The + // frame therefore appears in the SAME frame as the selection, not one + // after it, and a resize can never be a render behind the shape. + box = { id: sh.id, x: sh.x * NORM_W, y: sh.y * this.host._decorH, + w: sh.w * NORM_W, h: sh.h * this.host._decorH }; + } else { + const el = this.host.renderRoot.querySelector(`text.dtext[data-id="${sh.id}"]`) as SVGGraphicsElement | null; + if (!el || typeof (el as any).getBBox !== 'function') return; + let b: DOMRect; + try { b = el.getBBox(); } catch { return; } // not rendered yet (hidden card) + if (!b || (!b.width && !b.height)) return; + box = { id: sh.id, x: b.x, y: b.y, w: b.width, h: b.height }; + } + const cur = this.host._dtBox; + const same = cur && cur.id === box.id && Math.abs(cur.x - box.x) < 0.01 + && Math.abs(cur.y - box.y) < 0.01 && Math.abs(cur.w - box.w) < 0.01 + && Math.abs(cur.h - box.h) < 0.01; + if (same) return; + this.host._dtBox = box; + this.host.requestUpdate(); + } + +public _deleteDecor(id: string): void { + if (!this.host._decorList.some((shape) => shape.id === id)) return; + const before = this._geometrySnapshot(); + const sp = this.host._curSpaceCfg; + sp.decor = this.host._decorList.filter((shape) => shape.id !== id); + if (this.host._decorSel === id) this.host._decorSel = null; + this._recordGeometry(this.host._t('history.decor_delete'), before); + this._saveConfig(); + this.host.requestUpdate(); + } + +public _decorDeleteSel(): void { + if (this.host._decorSel) this._deleteDecor(this.host._decorSel); + } + +public _confirmDecorErase(): void { + const pending = this.host._decorEraseConfirm; + this.host._decorEraseConfirm = null; + if (pending) this._deleteDecor(pending.id); + } + +public _furnFieldValue(cm: number): number { + return Math.round((this.host._imperial ? cm / 30.48 : cm / 100) * 100) / 100; + } + +public _furnFieldToCm(v: number): number { + return clampFurnCm(this.host._imperial ? v * 30.48 : v * 100); + } + +public _furnPick(symbol: string): void { + const d = furnitureDefaultCm(symbol); + this.host._furnPalette = { symbol, w: d.w, h: d.h }; + } + +public _furnPlace(raw: number[], free = false): void { + const pal = this.host._furnPalette; + const sp = this.host._curSpaceCfg; + if (!pal || !sp) return; + 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 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, + ...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)); + 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._recordGeometry(this.host._t('history.decor_add'), before); + this._saveConfig(); + this.host.requestUpdate(); + } + +public _furnMoveUpdate(ev: PointerEvent): void { + const m = this.host._decorMove!; + if (m.orig.kind !== 'furniture') return; + const o: any = m.orig; + const sp = this.host._curSpaceCfg; + if (!sp) return; + const W = NORM_W, H = this.host._decorH; + const p = this._svgPoint(ev); + const rawCx = (o.x + o.w / 2) * W + (p[0] - m.start[0]); + const rawCy = (o.y + o.h / 2) * H + (p[1] - m.start[1]); + let x: number, y: number; + let angle = Number(o.angle) || 0; + const snap = ev.shiftKey ? null : snapFurnitureToWall( + rawCx, rawCy, o.h * H, this.host._furnWalls, this.host._furnWallReach, this.host._gridPitch); + if (snap) { + x = snap.cx / W - o.w / 2; + y = snap.cy / H - o.h / 2; + angle = snap.angle; + } else { + const a = this._decorSnap( + [rawCx - (o.w / 2) * W, rawCy - (o.h / 2) * H], ev.pointerType, m.id, + ); + x = a[0] / W; + y = a[1] / H; + } + x = clampCanvasN(x); y = clampCanvasN(y); + if (Math.abs(x - o.x) > 1e-9 || Math.abs(y - o.y) > 1e-9 + || Math.abs(angle - (Number(o.angle) || 0)) > 1e-9) m.moved = true; + sp.decor = this.host._decorList.map((s) => { + if (s.id !== m.id) return s; + const out: any = { ...s, x, y }; + if (angle) out.angle = Number(angle.toFixed(2)); + else delete out.angle; + return out; + }); + this.host.requestUpdate(); + } + +public _decorApplyBox(id: string, box: { x: number; y: number; w: number; h: number }): void { + const sp = this.host._curSpaceCfg; + if (!sp) return; + const W = NORM_W, H = this.host._decorH; + sp.decor = this.host._decorList.map((s) => { + if (s.id !== id) return s; + const topLeft = resizedBoxTopLeft( + box, (s as any).angle, (point) => this._snap(point), + ); + return { + ...s, + x: clampCanvasN(topLeft[0] / W), y: clampCanvasN(topLeft[1] / H), + w: Math.max(this.host._gridPitch / W, Math.min(CANVAS_LIMIT * 2, box.w / W)), + h: Math.max(this.host._gridPitch / H, Math.min(CANVAS_LIMIT * 2, box.h / H)), + } as DecorShape; + }); + this.host._cfgEpoch++; + this.host.requestUpdate(); + } + +public _renderFurnPalette(): TemplateResult { + const pal = this.host._furnPalette; + // the unit the fields are read in — the same two words the glow radius + // already uses, because a plan has one unit system, not one per control + const unit = this.host._t(this.host._imperial ? 'gs.unit_ft' : 'gs.unit_m'); + const preview = (id: string): TemplateResult => { + const s = furnitureSymbol(id)!; + // fit the symbol into a 40×40 box keeping its real proportions, so a + // sofa reads as a sofa and a toilet does not become a square + const k = 36 / Math.max(s.w, s.h); + const w = s.w * k, h = s.h * k; + return svg`` as unknown as TemplateResult; + }; + return html`
e.stopPropagation()}> +
+ ${this.host._t('furn.title')} + + +
+
+ ${FURNITURE_GROUPS.map((g) => html` +
${this.host._t(`furn.group_${g}` as any)}
+
+ ${furnitureOfGroup(g as FurnitureGroup).map((s) => html``)} +
`)} +
+ ${pal ? html`
+ + (this.host._furnPalette = { + ...pal, w: this._furnFieldToCm(Number((e.target as HTMLInputElement).value)) })} /> + + (this.host._furnPalette = { + ...pal, h: this._furnFieldToCm(Number((e.target as HTMLInputElement).value)) })} /> + ${this.host._t('furn.place_hint')} +
` : html`
${this.host._t('furn.pick_hint')}
`} +
`; + } + +public _openBackdropDialog(ev?: Event): void { + if (!this.host._bdMovable || !this.host._bdRect) return; + ev?.preventDefault(); + ev?.stopPropagation(); + this.host._bdDrag = null; + const r = this.host._bdRect; + this.host._backdropDialog = { + widthCm: decorUnitsToCm(r.w, this.host._cellCm, this.host._gridPitch), + heightCm: decorUnitsToCm(r.h, this.host._cellCm, this.host._gridPitch), + angle: this.host._angleField(r.angle), + }; + } + +public _saveBackdropDialog(): void { + const d = this.host._backdropDialog; + const base = this.host._bdBase, current = this.host._bdRect; + if (!d || !base || !current) return; + const angle = normalizeAngle(d.angle); + const before = this._geometrySnapshot(); + const w = Math.min(base.w * PLAN_SCALE_MAX, Math.max( + base.w * PLAN_SCALE_MIN, snapToGrid( + decorCmToUnits(d.widthCm, this.host._cellCm, this.host._gridPitch), this.host._gridPitch, + ), + )); + const h = Math.min(base.h * PLAN_SCALE_MAX, Math.max( + base.h * PLAN_SCALE_MIN, snapToGrid( + decorCmToUnits(d.heightCm, this.host._cellCm, this.host._gridPitch), this.host._gridPitch, + ), + )); + const cx = current.x + current.w / 2, cy = current.y + current.h / 2; + const topLeft = resizedBoxTopLeft( + { x: cx - w / 2, y: cy - h / 2 }, angle, + (point) => this._snap(point), + ); + this._bdApply((topLeft[0] - base.x) / NORM_W, (topLeft[1] - base.y) / NORM_W, + w / base.w, h / base.h, angle); + this.host._backdropDialog = null; + this._recordGeometry(this.host._t('history.backdrop_transform'), before); + this._saveConfig(); + } + +public _bdApply(dx: number, dy: number, sx: number, sy: number, angle: number): void { + const sp = this.host._curSpaceCfg; + if (!sp) return; + // 6 decimals: the same precision the rest of the normalised config keeps, + // and enough for a 1000-unit canvas to be exact to a thousandth of a pixel + sp.plan_x = Number(clampCanvasN(dx).toFixed(6)); + sp.plan_y = Number(clampCanvasN(dy).toFixed(6)); + delete sp.plan_scale; + sp.plan_scale_x = Number(Math.min(PLAN_SCALE_MAX, Math.max(PLAN_SCALE_MIN, sx)).toFixed(6)); + sp.plan_scale_y = Number(Math.min(PLAN_SCALE_MAX, Math.max(PLAN_SCALE_MIN, sy)).toFixed(6)); + const a = normalizeAngle(angle); + if (a) sp.plan_angle = Number(a.toFixed(2)); + else delete sp.plan_angle; + this.host._cfgEpoch++; + this.host.requestUpdate(); + } + +public _bdStart(ev: PointerEvent, corner?: number[], rotate = false): boolean { + const base = this.host._bdBase, r = this.host._bdRect; + if (!base || !r) return false; + const p = this._svgPoint(ev); + const sgx = corner ? corner[0] : 0; + const sgy = corner ? corner[1] : 0; + // the corner that stays put is the OPPOSITE one + const fx = sgx > 0 ? r.x : r.x + r.w; + const fy = sgy > 0 ? r.y : r.y + r.h; + this.host._bdDrag = { + kind: rotate ? 'rotate' : corner ? 'scale' : 'move', + pid: ev.pointerId, + sx: p[0], sy: p[1], + base, p0: this.host._bdParams, + fx, fy, sgx, sgy, + rect0: { x: r.x, y: r.y, w: r.w, h: r.h, angle: r.angle }, + before: this._geometrySnapshot(), + moved: false, + }; + capturePointer(ev); + return true; + } + +public _bdMove(ev: PointerEvent): void { + const d = this.host._bdDrag; + if (!d) return; + const p = this._svgPoint(ev); + const b = d.base; + if (d.kind === 'move') { + const x0 = d.rect0.x, y0 = d.rect0.y; + const at = this._snap([x0 + (p[0] - d.sx), y0 + (p[1] - d.sy)]); + const changed = Math.abs(at[0] - x0) > 1e-9 || Math.abs(at[1] - y0) > 1e-9; + if (!changed && !d.moved) return; + d.moved ||= changed; + this._bdApply((at[0] - b.x) / NORM_W, (at[1] - b.y) / NORM_W, + d.p0.sx, d.p0.sy, d.p0.angle); + return; + } + if (d.kind === 'rotate') { + const cx = d.rect0.x + d.rect0.w / 2, cy = d.rect0.y + d.rect0.h / 2; + const a0 = Math.atan2(d.sy - cy, d.sx - cx) * 180 / Math.PI; + let angle = d.p0.angle + (Math.atan2(p[1] - cy, p[0] - cx) * 180 / Math.PI - a0); + if (!ev.shiftKey) angle = Math.round(angle / DT_ANGLE_STEP) * DT_ANGLE_STEP; + angle = normalizeAngle(angle); + const changed = Math.abs(angle - d.p0.angle) > 1e-9; + if (!changed && !d.moved) return; + d.moved ||= changed; + this._bdApply(d.p0.dx, d.p0.dy, d.p0.sx, d.p0.sy, angle); + return; + } + const box = resizeDecorBox( + d.rect0, d.sgx, d.sgy, p[0], p[1], !ev.shiftKey, + this.host._gridPitch, Math.min(b.w, b.h) * PLAN_SCALE_MIN, + ); + const sx = box.w / Math.max(1e-9, b.w), sy = box.h / Math.max(1e-9, b.h); + const changed = Math.abs(sx - d.p0.sx) > 1e-9 || Math.abs(sy - d.p0.sy) > 1e-9 + || Math.abs(box.x - d.rect0.x) > 1e-9 || Math.abs(box.y - d.rect0.y) > 1e-9; + if (!changed && !d.moved) return; + d.moved ||= changed; + const topLeft = resizedBoxTopLeft( + box, d.p0.angle, (point) => this._snap(point), + ); + this._bdApply((topLeft[0] - b.x) / NORM_W, (topLeft[1] - b.y) / NORM_W, + sx, sy, d.p0.angle); + } + +public _bdReset(): void { + const sp = this.host._curSpaceCfg; + if (!sp) return; + const before = this._geometrySnapshot(); + delete sp.plan_x; delete sp.plan_y; delete sp.plan_scale; + delete sp.plan_scale_x; delete sp.plan_scale_y; delete sp.plan_angle; + this.host._bdDrag = null; + this._recordGeometry(this.host._t('history.backdrop_transform'), before); + this._saveConfig(); + this.host._showToast(this.host._t('decor.backdrop_reset_done')); + this.host.requestUpdate(); + } + +public _bdUp(): void { + const d = this.host._bdDrag; + this.host._bdDrag = null; + if (d?.moved) { + this._recordGeometry(this.host._t('history.backdrop_transform'), d.before); + this._saveConfig(); + } + this.host.requestUpdate(); + } + +public _renderBackdropFrame(view: { x: number; y: number; w: number; h: number }): TemplateResult | typeof nothing { + const r = this.host._bdRect; + if (!this.host._bdActive || !r) return nothing; + // Two radii, one gesture — the split the text frame uses (docs/LIVE-TEXT.md + // §3) and, since 2026-08-05, every corner handle in the card. `hr` is the + // HIT radius: a fraction of the visible view, so the target stays + // finger-sized at any zoom. `kr` is what you SEE — a quarter of it, because + // a blob the size of a room is not a handle, it is an occlusion (owner: + // «уменьшить в 4 раза… они постоянно гигантские»). The clickable area is + // unchanged; only the ink shrank. + const hr = Math.max(view.w, view.h) * 0.02; + const kr = hr / 4; + const cx = r.x + r.w / 2, cy = r.y + r.h / 2; + const angle = normalizeAngle(r.angle); + const arm = hr * 2.2; + const corners: [number, number, string][] = [ + [-1, -1, 'nwse'], [1, -1, 'nesw'], [1, 1, 'nwse'], [-1, 1, 'nesw'], + ]; + return svg` + + + { + e.stopPropagation(); e.preventDefault(); this._bdStart(e, undefined, true); + }}> + + ${corners.map(([sx, sy, cur]) => { + const cx = sx < 0 ? r.x : r.x + r.w; + const cy = sy < 0 ? r.y : r.y + r.h; + return svg` { + e.stopPropagation(); e.preventDefault(); this._bdStart(e, [sx, sy]); + }}>`; + })} + ` as unknown as TemplateResult; + } + +public _renderEditorGroupLauncher(group: EditorToolbarGroup): TemplateResult { + return this.host._editorSecondary.renderGroupLauncher( + group, + this.host._editorToolbarGroups, + this.host._editorSecondaryCopy, + ); + } + +public _runEditorContext(contextId: string, action: () => void): void { + this.host._editorSecondary.runContext(contextId, this.host._editorSecondaryContextId, action); + } + +public _renderEditorGroupModel(group: EditorToolbarGroup): EditorSecondaryModel { + return this.host._editorSecondary.renderGroupModel( + group, + this.host._editorSecondaryContextId, + this.host._editorSecondaryCopy, + ); + } + +public _renderDrawWallControl(): TemplateResult { + const min = this.host._tool === 'column' ? 1 : 0; + return html``; + } + +public _renderPlanSecondary(): EditorSecondaryModel | null { + const contextId = this.host._editorSecondaryContextId; + const sel = this.host._physicalSel; + if (sel) { + const exists = sel.kind === 'partition' + ? !!this.host._curSpaceCfg.partitions?.some((item: PartitionCfg) => item.id === sel.id) + : sel.kind === 'column' + ? !!this.host._curSpaceCfg.wall_columns?.some((item: WallColumnCfg) => item.id === sel.id) + : !!this.host._curSpaceCfg.room_drafts?.some((item: RoomDraftCfg) => item.id === sel.id); + if (!exists) return null; + const label = sel.kind === 'partition' ? this.host._t('markup.partition') + : sel.kind === 'column' ? this.host._t('markup.column') : this.host._t('markup.add'); + return { + contextId, + kind: 'selection', + ariaLabel: this.host._t('editor.context_actions', { object: label }), + visibleLabel: label, + content: html` + + `, + }; + } + + const hasThickness = this.host._tool === 'draw' || this.host._tool === 'column'; + const hintKey = this.host._tool === 'column' ? 'markup.hint_column' + : this.host._tool === 'resize' ? 'markup.hint_resize' + : this.host._tool === 'wallthick' ? 'markup.hint_wallthick' + : null; + const drawHint = this.host._tool === 'draw' + ? this.host._t(this.host._path.length ? 'markup.hint_points' : 'markup.hint_start', + this.host._path.length ? { n: this.host._path.length } : undefined) + : ''; + if (!hasThickness && !hintKey && !drawHint) return null; + const operation = this.host._tool === 'draw' && this.host._path.length > 0; + return { + contextId, + kind: operation ? 'operation' : 'tool', + ariaLabel: this.host._t('editor.tool_options', { + tool: this.host._t((this.host._tool === 'draw' ? 'markup.add' + : this.host._tool === 'column' ? 'markup.column' + : this.host._tool === 'resize' ? 'markup.resize' + : 'markup.wallthick') as any), + }), + content: html` + ${hasThickness ? this._renderDrawWallControl() : nothing} + ${drawHint ? html`${drawHint}` : nothing} + ${hintKey ? html`${this.host._t(hintKey as any)}` : nothing} + ${this.host._tool === 'draw' && this.host._path.length + ? html`` : nothing}`, + }; + } + +public _renderDecorSecondary(): EditorSecondaryModel | null { + const contextId = this.host._editorSecondaryContextId; + if (this.host._decorTool === 'furniture') { + return { + contextId, + kind: 'palette', + ariaLabel: this.host._t('editor.palette', { tool: this.host._t('decor.furniture') }), + launcherId: 'furniture', + dismissPolicy: this.host._furnPalette ? 'stay-open-on-canvas' : 'outside', + dismiss: () => { + if (this.host._decorTool !== 'furniture') return; + this.host._furnPalette = null; + this.host._decorTool = 'select'; + this.host.requestUpdate(); + }, + content: this._renderFurnPalette(), + }; + } + const selected = this.host._dtSel; + if (selected) { + const label = this.host._t((`decor.${selected.kind}`) as any); + return { + contextId, + kind: 'selection', + ariaLabel: this.host._t('editor.context_actions', { object: label }), + visibleLabel: label, + content: html` + + `, + }; + } + + const draws = this.host._decorTool === 'line' || this.host._decorTool === 'rect' || this.host._decorTool === 'ellipse'; + const canFill = this.host._decorTool === 'rect' || this.host._decorTool === 'ellipse'; + const backdrop = this.host._decorTool === 'backdrop' && !!this.host._bdRect; + if (!draws && !backdrop) return null; + return { + contextId, + kind: 'tool', + ariaLabel: this.host._t('editor.tool_options', { + tool: this.host._t((draws ? `decor.${this.host._decorTool}` : 'decor.backdrop') as any), + }), + content: html` + ${draws ? html` + ) => + (this.host._decorStyle = { ...this.host._decorStyle, ...e.detail })}> + + ${canFill ? html` + ) => + (this.host._decorStyle = { ...this.host._decorStyle, + fillColor: e.detail.color, fillOpacity: e.detail.opacity })}>` : nothing} + ` : nothing} + ${backdrop ? html`${this.host._t('decor.backdrop_hint')}` : nothing}`, + }; + } + +public _withBackdropReset(model: EditorSecondaryModel | null): EditorSecondaryModel | null { + if (!this.host._bdMoved || this.host._bdDrag || model?.kind === 'palette') return model; + const contextId = this.host._editorSecondaryContextId; + const reset = html``; + if (!model) { + return { + contextId, + kind: 'tool', + ariaLabel: this.host._t('editor.tool_options', { tool: this.host._t('decor.backdrop') }), + content: reset, + }; + } + return { + ...model, + kind: model.kind === 'selection' ? 'mixed' : model.kind, + content: html`${model.content}${reset}`, + }; + } + +public _renderEditorSecondary(): TemplateResult | typeof nothing { + if (!this.host._editing) return nothing; + const group = this.host._editorSecondary.activeGroup(this.host._editorToolbarGroups); + const model = group ? this._renderEditorGroupModel(group) + : this.host._mode === 'plan' ? this._renderPlanSecondary() + : this.host._mode === 'decor' ? this._withBackdropReset(this._renderDecorSecondary()) + : null; + return this.host._editorSecondary.render(model, this.host._editorSecondaryDialogBlocked); + } + +public _renderDecorBar(): TemplateResult { + const tools = [ + ['select', 'mdi:cursor-default-outline', 'decor.select'], + // moving the picture is a TOOL (docs/BACKDROP.md §2) — offered only when + // there IS a picture, so a hand-drawn space's bar is unchanged + ...(this.host._bdRect ? [['backdrop', 'mdi:image-move', 'decor.backdrop'] as const] : []), + ['line', 'mdi:vector-line', 'decor.line'], + ['rect', 'mdi:rectangle-outline', 'decor.rect'], + ['ellipse', 'mdi:ellipse-outline', 'decor.ellipse'], + ['text', 'mdi:format-text', 'decor.text'], + // the library sits next to the shapes it belongs with (docs/FURNITURE.md) + ['furniture', 'mdi:sofa-outline', 'decor.furniture'], + ['erase', 'mdi:eraser', 'decor.erase'], + ] as const; + const undoName = this.host._geometryHistory.undoName; + const redoName = this.host._geometryHistory.redoName; + return html`
+
+ ${tools.map( + ([t, ic, k]) => html``, + )} + ${this.host._editorToolbarGroups.map((group) => this._renderEditorGroupLauncher(group))} + + +
+
+ +
+
`; + } + +public _renderDecorEraseConfirm(): TemplateResult { + const pending = this.host._decorEraseConfirm!; + const kind = this.host._t(`decor.${pending.kind}` as any); + return html` (this.host._decorEraseConfirm = null)}> +

${this.host._t('confirm.erase_decor', { kind })}

+
+ + + +
+
`; + } + +public _renderDecorTextDialog(): TemplateResult { + const d = this.host._decorTextDialog!; + const ent = (d.pickerEntity || '').trim(); + const st = ent ? this.host.hass?.states?.[ent] : null; + return html` (this.host._decorTextDialog = null)}> +
+ + ${''/* a textarea, not an input: the user's own line breaks are kept + and rendered (centred). Enter is a NEW LINE here, so saving + moved to Ctrl/Cmd+Enter and the button. */} + + ) => + (this.host._decorTextDialog = { ...d, ...e.detail })}> + +
(this.host._decorTextDialog = { ...d, + sizeCm: this.host._decorTextCm(Number((e.target as HTMLInputElement).value)) })} /> + ${this.host._t(this.host._imperial ? 'wallthick.unit_in' : 'wallthick.unit_cm')}
+ + (this.host._decorTextDialog = { ...d, + angle: (e.target as HTMLInputElement).value })} /> + + + (this.host._decorTextDialog = { + ...d, pickerEntity: (e.target as HTMLInputElement).value, + })} /> + + ${Object.keys(this.host.hass?.states || {}).map((id) => html``)} + + ${ent ? html` + + + ` : nothing} +
+
+ + + +
+
`; + } + +public _renderDecorShapeDialog(): TemplateResult { + const d = this.host._decorShapeDialog!; + const canFill = d.kind === 'rect' || d.kind === 'ellipse'; + const kindLabel = this.host._t(('decor.' + d.kind) as any); + const unit = this.host._t(this.host._imperial ? 'gs.unit_ft' : 'gs.unit_m'); + return html` (this.host._decorShapeDialog = null)}> +
+ ${d.kind === 'furniture' ? html` + + ` : nothing} + ) => + (this.host._decorShapeDialog = { ...d, ...e.detail })}> + +
(this.host._decorShapeDialog = { + ...d, widthCm: this.host._decorSmallCm(Number((e.target as HTMLInputElement).value)), + })} />${this.host._t(this.host._imperial ? 'wallthick.unit_in' : 'wallthick.unit_cm')}
+ ${d.kind === 'line' ? html` + +
+ + +
+ +
(this.host._decorShapeDialog = { ...d, + lengthCm: this.host._decorLargeCm(Number((e.target as HTMLInputElement).value)) })} /> + ${unit}
` : html` + +
(this.host._decorShapeDialog = { ...d, + sizeWCm: this.host._decorLargeCm(Number((e.target as HTMLInputElement).value)) })} /> + × (this.host._decorShapeDialog = { ...d, + sizeHCm: this.host._decorLargeCm(Number((e.target as HTMLInputElement).value)) })} /> + ${unit}
`} + + (this.host._decorShapeDialog = { ...d, + angle: (e.target as HTMLInputElement).value })} /> + ${canFill ? html` + ) => + (this.host._decorShapeDialog = { ...d, + fillColor: e.detail.color, fillOpacity: e.detail.opacity })}>` : nothing} +
+
+ + + +
+
`; + } + +public _renderBackdropDialog(): TemplateResult { + const d = this.host._backdropDialog!; + const unit = this.host._t(this.host._imperial ? 'gs.unit_ft' : 'gs.unit_m'); + return html` (this.host._backdropDialog = null)}> +
+ +
(this.host._backdropDialog = { ...d, + widthCm: this.host._decorLargeCm(Number((e.target as HTMLInputElement).value)) })} /> + × (this.host._backdropDialog = { ...d, + heightCm: this.host._decorLargeCm(Number((e.target as HTMLInputElement).value)) })} /> + ${unit}
+ + (this.host._backdropDialog = { ...d, + angle: (e.target as HTMLInputElement).value })} /> +
+
+ + +
+
`; + } + +public _cssPxToRender(px: number): number { + const stage = this.host._stageEl; + const view = this.host._viewOr(this.host._baseVb()); + if (!stage?.clientWidth || !stage.clientHeight) return (this.host._gridPitch / 8) * px; + return Math.max(view.w / stage.clientWidth, view.h / stage.clientHeight) * px; + } + +public _deleteRoomClick(raw: number[]): void { + const space = this.host._spaceModel(); + if (!space) return; + const room = [...space.rooms].reverse().find((r) => this._pointInRoom(raw, r)); + if (!room) { + this.host._showToast(this.host._t('toast.delete_room_pick')); + return; + } + if (!room.id) return; + this.host._roomDeleteDialog = { roomId: room.id, name: room.name }; + } + +public _confirmRoomDelete = (keepWalls: boolean): void => { + const dialog = this.host._roomDeleteDialog; + const sp = this.host._curSpaceCfg; + const space = this.host._spaceModel(); + if (!dialog || !sp || !space) return; + const room = space.rooms.find((candidate) => candidate.id === dialog.roomId); + if (!room) { this.host._roomDeleteDialog = null; return; } + const openCuts = this.host._openCuts(); + const allIntervals = wallIntervals( + space.rooms, this.host._spaceWalls, openCuts, + this.host._wallKeyPitch, this.host._cellCm, this.host._gridPitch, NORM_W, + ); + const roomIntervals = allIntervals.filter((interval) => interval.roomId === room.id); + const plan = planRoomDeletion( + roomIntervals, + space.partitions, + this.host._openingsR.map((opening) => ({ + ...opening, x: opening.rx, y: opening.ry, + })), + this.host._gridPitch * 0.02, + ); + const newCount = new Set(plan.materialize + .filter((item) => !item.reusePartitionId) + .map((item) => item.interval.key)).size; + if (keepWalls && (sp.partitions || []).length + newCount > MAX_PARTITIONS) { + this.host._showToast(this.host._t('toast.physical_limit')); + return; + } + const before = this._geometrySnapshot(); + const materializedWalls = materializeWallIntervals( + space.rooms, this.host._spaceWalls, openCuts, + this.host._wallKeyPitch, this.host._cellCm, this.host._gridPitch, NORM_W, + ); + if (keepWalls) { + sp.partitions ||= []; + const targetByInterval = new Map(); + const createdByKey = new Map(); + const sourceWallId = (interval: { a: readonly number[]; b: readonly number[] }): string | null => { + const match = (sp.wall_segments || []).find((segment: any) => { + const a = [Number(segment.a?.[0]) * NORM_W, Number(segment.a?.[1]) * NORM_W]; + const b = [Number(segment.b?.[0]) * NORM_W, Number(segment.b?.[1]) * NORM_W]; + return (this._samePt(interval.a, a) && this._samePt(interval.b, b)) + || (this._samePt(interval.a, b) && this._samePt(interval.b, a)); + }); + return typeof match?.id === 'string' && match.id ? match.id : null; + }; + const seed = Date.now().toString(36); + for (const item of plan.materialize) { + let target = item.reusePartitionId + ? sp.partitions.find((partition: PartitionCfg) => partition.id === item.reusePartitionId) + : createdByKey.get(item.interval.key); + if (!target) { + target = { + id: sourceWallId(item.interval) || `partition-room-${seed}-${createdByKey.size}`, + a: [item.interval.a[0] / NORM_W, item.interval.a[1] / NORM_W], + b: [item.interval.b[0] / NORM_W, item.interval.b[1] / NORM_W], + cm: item.interval.cm, + }; + sp.partitions.push(target); + createdByKey.set(item.interval.key, target); + } + targetByInterval.set(item.interval.key, target); + } + for (const opening of sp.openings || []) { + const intervalKey = plan.openingIntervals.get(opening.id); + const target = intervalKey ? targetByInterval.get(intervalKey) : null; + const rendered = this.host._openingsR.find((candidate) => candidate.id === opening.id); + if (!target || !rendered) continue; + const renderTarget = { + a: [target.a[0] * NORM_W, target.a[1] * NORM_W], + b: [target.b[0] * NORM_W, target.b[1] * NORM_W], + }; + opening.host = { + kind: 'partition', id: target.id, + t: parameterOnPartition([rendered.rx, rendered.ry], renderTarget), + }; + } + } else if (plan.removeOpeningIds.length) { + const remove = new Set(plan.removeOpeningIds); + sp.openings = (sp.openings || []).filter((opening: OpeningCfg) => !remove.has(opening.id)); + if (!sp.openings.length) delete sp.openings; + } + sp.rooms = sp.rooms.filter((r: any) => r.id !== room.id); + this.host._cfgEpoch++; + const normalized = this.host._normalizeWalls(materializedWalls, this.host._openCuts()); + if (normalized.length) sp.walls = normalized; + else delete sp.walls; + this.host._roomDeleteDialog = null; + this._commitPhysicalGeometry(this.host._t( + keepWalls ? 'history.delete_room_keep_walls' : 'history.delete_room_with_walls', + ), before); + this.host._regSignature = ''; + this.host._maybeRebuildDevices(); + this.host.requestUpdate(); + }; + +public _wallThickHit(raw: number[]): WallThickHit | null { + const space = this.host._spaceModel(); + if (!space) return null; + const pull = this.host._gridPitch * 6; + const cuts = this.host._openCuts(); + type Hit = WallThickHit; + // #313: the tool serves every wall — room intervals AND independent + // masonry. On an exact overlap (the #308 duplicate) the independent wall + // wins: it owns the hit zone, exactly like the select tool, and it is + // the body the eye actually sees. + let best: { hit: Hit; d: number; independent: boolean } | null = null; + const offer = (hit: Hit, d: number, independent: boolean): void => { + if (d > pull) return; + if (!best || d < best.d - 1e-9 + || (independent && !best.independent && d <= best.d + 1e-9)) { + best = { hit, d, independent }; + } + }; + for (const iv of wallIntervals( + space.rooms, this.host._spaceWalls, cuts, + this.host._wallKeyPitch, this.host._cellCm, this.host._gridPitch, NORM_W, + )) { + offer({ + a: iv.a, b: iv.b, roomId: iv.roomId, + segs: [[iv.a[0], iv.a[1], iv.b[0], iv.b[1]]], + open: iv.open, cm: iv.cm, source: { kind: 'room' }, + }, distToSegment(raw, [iv.a[0], iv.a[1], iv.b[0], iv.b[1]]), false); + } + for (const partition of space.partitions || []) { + const a = [partition.a[0], partition.a[1]]; + const b = [partition.b[0], partition.b[1]]; + offer({ + a, b, roomId: '', segs: [[a[0], a[1], b[0], b[1]]], + open: false, cm: Number(partition.cm) || 0, + source: { kind: 'partition', id: partition.id }, + }, distToSegment(raw, [a[0], a[1], b[0], b[1]]), true); + } + for (const draft of space.room_drafts || []) { + // The active chain belongs to the draw tool; like the snap geometry, + // Thickness only sees saved drafts. + if (draft.id === this.host._activeDraftId) continue; + for (let i = 0; i + 1 < draft.points.length; i++) { + const a = [draft.points[i][0], draft.points[i][1]]; + const b = [draft.points[i + 1][0], draft.points[i + 1][1]]; + // Zero is a legitimate stored value («Empty / 0 leaves the new wall + // thin», docs/WALL-THICKNESS.md §6) — only a MISSING record falls + // back to the drawing default. `|| 15` would silently turn a stored + // 0 into 15 on Apply (CODE-REVIEW-313-r1 High). + const rawCm = Number(draft.segments[i]?.cm); + offer({ + a, b, roomId: '', segs: [[a[0], a[1], b[0], b[1]]], + open: false, cm: Number.isFinite(rawCm) ? rawCm : 15, + source: { kind: 'draft', id: draft.id, segment: i }, + }, distToSegment(raw, [a[0], a[1], b[0], b[1]]), true); + } + } + return best ? (best as { hit: Hit }).hit : null; + } + +public _wallThickClick(raw: number[]): void { + const hit = this._wallThickHit(raw); + if (!hit) { + this.host._showToast(this.host._t('toast.wallthick_pick')); + return; + } + const cm = hit.cm; + const view = this.host._viewOr(this.host._baseVb()); + const mx = (hit.a[0] + hit.b[0]) / 2, my = (hit.a[1] + hit.b[1]) / 2; + this.host._wallDialog = { + a: hit.a, b: hit.b, + value: cmToField(cm, this.host._imperial), + roomId: hit.roomId, + source: hit.source, + sx: ((mx - view.x) / view.w) * 100, + sy: ((my - view.y) / view.h) * 100, + }; + } + +public _wallThickApply(allRoom: boolean): void { + const d = this.host._wallDialog; + if (!d) return; + const sp = this.host._curSpaceCfg; + const space = this.host._spaceModel(); + if (!sp || !space) return; + const text = d.value.trim(); + const raw = text ? strictNumber(text) : null; + if (raw == null) { this._showPhysicalRange(100); return; } + const cmRaw = this.host._imperial ? raw * 2.54 : raw; + if (!Number.isFinite(cmRaw) || cmRaw < 0 || cmRaw > 100) { + this._showPhysicalRange(100); + return; + } + // Independent walls and saved drafts own their records. Zero is the same + // canonical bodyless state here as it is for a contour atom (#306). + if (d.source.kind !== 'room') { + const before = this._geometrySnapshot(); + if (d.source.kind === 'partition') { + const partition = (sp.partitions || []) + .find((item: PartitionCfg) => item.id === (d.source as { id: string }).id); + if (!partition) return; + if (cmRaw === 0 && zeroWallHasOpening(sp.openings, { + kind: 'partition', id: partition.id, + })) { + this.host._showToast(this.host._t('toast.zero_wall_opening_conflict')); + return; + } + partition.cm = cmRaw; + } else { + const draft = (sp.room_drafts || []) + .find((item) => item.id === (d.source as { id: string }).id); + const segment = draft?.segments?.[(d.source as { segment: number }).segment]; + if (!segment) return; + segment.cm = cmRaw; + } + this.host._wallDialog = null; + const committed = this._commitPhysicalGeometry( + this.host._t('history.wall_thickness'), before, + ); + if (committed) this.host._showToast(this.host._t('toast.wallthick_set')); + this.host.requestUpdate(); + return; + } + const before = this._geometrySnapshot(); + const cm = cmRaw > 0 ? cmRaw : null; + if (cmRaw === 0) { + const eps = this.host._gridPitch * 0.02; + const target = [d.a[0], d.a[1], d.b[0], d.b[1]]; + const room = (sp.rooms || []).find((item) => item.id === d.roomId); + const ids = new Set(allRoom && room && Array.isArray(room.wall_ids) + ? room.wall_ids + : (sp.wall_segments || []).filter((segment: any) => { + const a = [Number(segment.a?.[0]) * NORM_W, Number(segment.a?.[1]) * NORM_W]; + const b = [Number(segment.b?.[0]) * NORM_W, Number(segment.b?.[1]) * NORM_W]; + return distToSegment(a, target) <= eps && distToSegment(b, target) <= eps; + }).map((segment: any) => String(segment.id))); + const blocked = (sp.openings || []).some((opening: OpeningCfg) => { + if (opening.host?.kind === 'partition') return false; + if (opening.host?.kind === 'wall' && ids.has(opening.host.id)) return true; + if (opening.host) return false; + const point = [Number(opening.x) * NORM_W, Number(opening.y) * NORM_W]; + return point.every(Number.isFinite) && distToSegment(point, target) <= eps; + }); + if (blocked) { + this.host._showToast(this.host._t('toast.zero_wall_opening_conflict')); + return; + } + } + let openCuts = this.host._openCuts(); + if (cmRaw > 0 && openCuts.length) { + // The selected zero axis is a cut only in the PRE-edit body profile. + // Remove exactly the carriers being restored before the compatibility + // wall normalizer runs; otherwise it discards the new positive record as + // if the segment were still a zero interval (#306). + const targets: number[][] = []; + if (allRoom && d.roomId) { + const room = (sp.rooms || []).find((item) => item.id === d.roomId); + const ids = new Set(Array.isArray(room?.wall_ids) ? room.wall_ids : []); + for (const segment of sp.wall_segments || []) { + if (!ids.has(segment.id)) continue; + targets.push([ + Number(segment.a?.[0]) * NORM_W, Number(segment.a?.[1]) * NORM_W, + Number(segment.b?.[0]) * NORM_W, Number(segment.b?.[1]) * NORM_W, + ]); + } + // A pre-v9 room has no catalogue IDs yet. Its current zero axes still + // come from the rendered room outline, so use that outline as the + // carrier set for this first explicit positive-thickness edit. The + // common v9 barrier below will materialise stable IDs atomically. + if (!targets.length) { + const renderedRoom = space.rooms.find((item) => item.id === d.roomId); + const poly = renderedRoom ? roomPoly(renderedRoom) : null; + for (let index = 0; poly && index < poly.length; index++) { + const a = poly[index], b = poly[(index + 1) % poly.length]; + targets.push([a[0], a[1], b[0], b[1]]); + } + } + } else targets.push([d.a[0], d.a[1], d.b[0], d.b[1]]); + const eps = this.host._gridPitch * 0.02; + openCuts = openCuts.filter((cut) => !targets.some((target) => ( + distToSegment([cut[0], cut[1]], target) <= eps + && distToSegment([cut[2], cut[3]], target) <= eps + && distToSegment([target[0], target[1]], cut) <= eps + && distToSegment([target[2], target[3]], cut) <= eps + ))); + } + let next: WallEntry[]; + if (allRoom && d.roomId) { + next = setWallThicknessForRoom( + sp.walls, space.rooms, d.roomId, cm, this.host._wallKeyPitch, openCuts, NORM_W, + ); + } else { + next = setWallThickness(sp.walls, d.a, d.b, cm, this.host._wallKeyPitch, NORM_W); + } + next = this.host._normalizeWalls(next, openCuts); + // ADR 282 Stage 1: this writer knows that absence from `next` is an + // explicit zero chosen by the user, not a missing compatibility record. + // Mirror that intent into the canonical catalogue before the common + // barrier; otherwise its lineage fallback would resurrect the old cm. + for (const segment of sp.wall_segments || []) { + segment.cm = thicknessCmAt( + next, segment.a, segment.b, GRID_STEP_N, 1, + ); + } + if (next.length) sp.walls = next; + else delete sp.walls; + this.host._wallDialog = null; + if (this._commitPhysicalGeometry(this.host._t('history.wall_thickness'), before)) + this.host._showToast(this.host._t(cmRaw === 0 ? 'toast.wallthick_cleared' : 'toast.wallthick_set')); + this.host.requestUpdate(); + } + +public _wallHatchDefs(color: string): TemplateResult { + if (!this.host._spaceWalls.length && !this.host._physicalBodiesR().length && !this.host._markup) + return svg`` as unknown as TemplateResult; + // The step is a physical distance now — 9.6 cm of plan, whatever the grid + // scale — so it must NOT be compensated for zoom: a wall that changes its + // hatching as you zoom is exactly what #230 is about (spec §4.2). + const step = wallHatchStepUnits(this.host._cellCm); + const stripe = 2 * (step / HATCH_BASE_STEP_UNITS); + // Bake the wall colour into the pattern — CSS vars on content + // do not inherit from the filled path, so var(--room-stroke) fell back + // to grey while the wall outline used the real border colour. + const stroke = color || '#607d8b'; + return svg` + + + + ` as unknown as TemplateResult; + } + +public _renderWallThickUi(): TemplateResult { + const hover = this.host._wallThickHover; + if (!hover || !hover.d) return svg`` as unknown as TemplateResult; + return svg`` as unknown as TemplateResult; + } + +public _renderWallThickDialog(): TemplateResult { + const d = this.host._wallDialog; + if (!d) return html``; + return html`
e.stopPropagation()}> +
+ + { + this.host._wallDialog = { ...d, value: (e.target as HTMLInputElement).value }; + }} + @keydown=${(e: KeyboardEvent) => { + if (e.key === 'Enter') { e.preventDefault(); this._wallThickApply(false); } + }} /> + ${this.host._t(this.host._imperial ? 'wallthick.unit_in' : 'wallthick.unit_cm')} +
+
+ ${d.source.kind === 'room' ? html`` : nothing} + + +
+
`; + } + +public _openingAt(raw: readonly number[]): RenderOpening | null { + if (!this.host._openingsR.length) return null; + const roughHits = this.host._openingsR.flatMap((o) => { + const rad = o.angle * Math.PI / 180; + const dx = raw[0] - o.rx, dy = raw[1] - o.ry; + const localX = dx * Math.cos(rad) + dy * Math.sin(rad); + if (Math.abs(localX) > o.rlen / 2 + gridVisualUnits(12, this.host._cellCm)) return []; + return [{ o, localY: -dx * Math.sin(rad) + dy * Math.cos(rad) }]; + }); + if (!roughHits.length) return null; + const space = this.host._spaceModel(); + if (!space) return null; + const openCuts = this.host._openCuts(); + const wallIndex = this.host._openingWallIndexFor(space, openCuts).value; + return roughHits.find(({ o, localY }) => { + const faceFlipV = o.type === 'gate' ? !o.flip_v : o.flip_v; + const face = o.partitionHost || this.host._spaceWalls.length || o.type === 'gate' + ? this.host._openingFace(o, wallIndex, !!faceFlipV) + : { ox: 0, oy: 0, cm: 0, side: -1 as -1 | 1 }; + const metrics = openingVisibleMetrics({ + type: o.type, + length: o.rlen, + angle: o.angle, + amount: this.host._openingAmt(o), + flipH: !!o.flip_h, + flipV: !!o.flip_v, + base: '', tone: '', + cellCm: this.host._cellCm, + gridPitch: this.host._gridPitch, + face, + }); + // Match the committed `.op-hit` rectangle exactly. The old radial test + // made a 3 m gate suppress placement in a huge circular area far away + // from its visible/hit geometry. + return Math.abs(localY) <= metrics.hitHalf; + })?.o || null; + } + +public _resolveOpeningPlacement(raw: readonly number[]): OpeningPlacementCandidate | null { + const preset = this.host._openingPreset; + if (this.host._tool !== 'opening' || !preset) return null; + const space = this.host._spaceModel(); + if (!space) return null; + const openCuts = this.host._openCuts(); + const wallIndex = this.host._openingWallIndexFor(space, openCuts); + const placementKey = `${wallIndex.key}|partitions:${this.host._cfgEpoch}`; + if (!this.host._openingPlacementIntervalsCache + || this.host._openingPlacementIntervalsCache.key !== placementKey) { + this.host._openingPlacementIntervalsCache = { + key: placementKey, + value: [ + ...wallIntervals( + space.rooms, this.host._spaceWalls, openCuts, + this.host._wallKeyPitch, this.host._cellCm, this.host._gridPitch, NORM_W, + ), + ...partitionPlacementIntervals(space.partitions, this.host._cellCm, this.host._gridPitch), + ], + }; + } + if (!this.host._openingDimensionContextCache + || this.host._openingDimensionContextCache.key !== placementKey) { + this.host._openingDimensionContextCache = { + key: placementKey, + value: buildOpeningDimensionContext({ + rooms: space.rooms, + walls: this.host._spaceWalls, + openCuts, + partitions: space.partitions, + roomOpenings: this.host._roomWallOpeningInputs(), + partitionCuts: this._partitionOpeningCuts(space), + pitch: this.host._wallKeyPitch, + cellCm: this.host._cellCm, + gridPitch: this.host._gridPitch, + coordScale: NORM_W, + epsilon: this.host._gridPitch * 0.0002, + }), + }; + } + const resolution = resolveOpeningPlacementResult({ + pointer: [raw[0], raw[1]], + preset, + geometryRevision: this.host._cfgEpoch, + renderedLength: this.host._cmToUnits(preset.lengthCm), + intervals: this.host._openingPlacementIntervalsCache.value, + baseTolerance: this.host._gridPitch * 1.5, + bodyPointerPadding: this._cssPxToRender( + this.host._pointerModality.modality === 'touch' || this.host._pointerModality.modality === 'pen' ? 10 : 6, + ), + gridStep: this.host._gridPitch, + }); + this.host._openingJambBlockCm = resolution.jambBlockedTarget + ? (resolution.jambBlockedTarget.physicalHalfWidth / this.host._gridPitch) * this.host._cellCm + : null; + const core = resolution.candidate; + if (!core) return null; + const faceFlipV = core.type === 'gate' ? !core.flipV : core.flipV; + let face: OpeningFaceOffset; + if (core.host) { + const hosted = resolvePartitionOpeningCompat({ + id: 'preview', type: core.type, + x: core.x / NORM_W, y: core.y / NORM_W, + angle: core.angle, length: core.renderedLength / NORM_W, + host: core.host, + }, space.partitions, NORM_W, this.host._cellCm, this.host._gridPitch).resolved; + face = hosted + ? partitionOpeningFace(hosted, faceFlipV) + : { ox: 0, oy: 0, cm: 0, side: -1 }; + } else { + face = openingInnerFaceOffsetFromIndex( + wallIndex.value, + { x: core.x, y: core.y, angle: core.angle, length: core.renderedLength, flip_v: faceFlipV }, + ); + } + const imperial = this.host.hass?.config?.unit_system?.length === 'mi'; + // #238: placement labels are physical dimensions. They consume the ONE + // resolved candidate above and never re-run pointer snap; existing-opening + // drag deliberately keeps `_opRuler`/`openingShoulders` below. + const dimensions = resolveOpeningDimensions( + core, this.host._openingDimensionContextCache.value, + ); + const labels = dimensions.map((dimension) => ({ + x: dimension.label[0], + y: dimension.label[1], + text: formatLength((dimension.distance / this.host._gridPitch) * this.host._cellCm, imperial), + dimension, + })); + return { ...core, face, measure: { labels, guide: core.measure.guide } }; + } + +public _activateOpeningPlacement(type: OpeningPlacementType): void { + this._activateMarkupTool('opening'); + if (this.host._tool !== 'opening') return; + this.host._openingPreset = openingPlacementPreset(type, ++this.host._openingPresetRevision); + this.host._openingHoverCandidate = null; + this.host._cursorPt = null; + } + +public _clearOpeningPlacement(clearPreset: boolean): void { + this.host._openingHoverCandidate = null; + this.host._openingJambBlockCm = null; + if (clearPreset) { + this.host._openingPreset = null; + this.host._openingRebindId = null; + } + } + +public _openingClick(raw: number[]): void { + const space = this.host._spaceModel(); + if (!space) return; + // Rebind is an explicit one-shot placement: the orphan's stale fallback + // position must not win hit testing when the replacement wall crosses it. + const hit = this.host._openingRebindId ? null : this._openingAt(raw); + if (hit) { + this._editOpening(hit); + return; + } + const preset = this.host._openingPreset; + if (!preset) return; + const cached = this.host._openingHoverCandidate; + const place = cached && sameOpeningPlacementInput( + cached, [raw[0], raw[1]], preset.revision, this.host._cfgEpoch, + ) ? cached : this._resolveOpeningPlacement(raw); + if (!place) { + if (this.host._openingJambBlockCm != null) { + this.host._showToast(this.host._t('opening.partition_jamb_margin', { + distance: formatLength(this.host._openingJambBlockCm, this.host._imperial), + })); + return; + } + const eps = this.host._gridPitch * 1.5; + const snap = snapToWall(raw, space.rooms, eps); + if (snap && pointOnOpenCut(snap.x, snap.y, snap.angle, this.host._openCuts(), eps)) { + this.host._showToast(this.host._t('toast.opening_on_zero_wall')); + return; + } + this.host._showToast(this.host._t('toast.opening_no_wall')); + return; + } + const rebound = this.host._openingRebindId + ? this.host._curSpaceCfg?.openings?.find((item: OpeningCfg) => item.id === this.host._openingRebindId) + : null; + this.host._openingDialog = { + ...(rebound ? { + id: rebound.id, + type: rebound.type, + lengthCm: Math.round((rebound.length * NORM_W / this.host._gridPitch) * this.host._cellCm), + lengthTouched: false, + contact: rebound.contact || '', lock: rebound.lock || '', + invert: !!rebound.invert, flipH: !!rebound.flip_h, flipV: !!rebound.flip_v, + } : { + type: place.type, lengthCm: place.lengthCm, contact: '', lock: '', + invert: false, flipH: place.flipH, flipV: place.flipV, + }), + ...(place.host ? { host: place.host } : {}), + x: place.x, y: place.y, angle: place.angle, + }; + this.host._openingRebindId = null; + // rulers, tick and ghost live only through the placement gesture + this.host._openingHoverCandidate = null; + this.host._cursorPt = null; + } + +public _editOpening(o: RenderOpening): void { + this.host._openingDialog = { + id: o.id, + type: o.type, + lengthCm: Math.round((o.rlen / this.host._gridPitch) * this.host._cellCm), + lengthTouched: false, + contact: o.contact || '', + lock: o.lock || '', + invert: !!o.invert, + flipH: !!o.flip_h, + flipV: !!o.flip_v, + ...(o.host?.kind === 'partition' ? { host: { ...o.host } } : {}), + x: o.rx, y: o.ry, angle: o.angle, + }; + } + +public _opPointerDown(ev: PointerEvent, o: OpeningCfg): void { + if (this.host._mode !== 'plan' || !this.host._spaceModel()) return; + // HP-1550-04: in the resize tool the wall handles own the geometry — a door + // in the middle of a wall must neither swallow the handle nor start its own + // drag (it travels with the wall through the resize pipeline instead) + if (this.host._tool === 'resize') return; + ev.preventDefault(); + ev.stopPropagation(); + try { + capturePointer(ev); + } catch { + /* an inactive pointerId (synthetic events, some browsers) must not kill the drag */ + } + this.host._opDrag = { + id: o.id, moved: false, sx: ev.clientX, sy: ev.clientY, dirty: false, + before: this._geometrySnapshot(), + }; + } + +public _opPointerMove(ev: PointerEvent, o: OpeningCfg): void { + if (!this.host._opDrag || this.host._opDrag.id !== o.id) return; + // audit L4: the other drag pipelines require 3 px before calling it a drag. + // Without it every tap counted as a drag: the properties dialog never + // opened and an unchanged config was written (which then broadcast the + // event behind the L2 data loss). + if (Math.abs(ev.clientX - this.host._opDrag.sx) + Math.abs(ev.clientY - this.host._opDrag.sy) <= 3) return; + const space = this.host._spaceModel(); + if (!space) return; + const raw = this._svgPoint(ev); + const sp = this.host._curSpaceCfg; + const cfg = sp?.openings?.find((x: OpeningCfg) => x.id === o.id); + if (!cfg) return; + if (cfg.host?.kind === 'partition') { + const partition = space.partitions.find((item) => item.id === cfg.host!.id); + if (!partition) return; + const dx = partition.b[0] - partition.a[0], dy = partition.b[1] - partition.a[1]; + const length = Math.hypot(dx, dy); + if (!(length > 1e-9)) return; + const ux = dx / length, uy = dy / length; + const perpendicular = Math.abs( + (raw[0] - partition.a[0]) * uy - (raw[1] - partition.a[1]) * ux, + ); + if (perpendicular > this.host._gridPitch * 4) return; + const half = cfg.length * NORM_W / 2; + const jamb = partitionOpeningJambMargin(partition, this.host._cellCm, this.host._gridPitch); + const shoulder = half + jamb; + let along = (raw[0] - partition.a[0]) * ux + (raw[1] - partition.a[1]) * uy; + along = Math.round(along / this.host._gridPitch) * this.host._gridPitch; + along = Math.max(shoulder, Math.min(length - shoulder, along)); + if (length < shoulder * 2 - 1e-9) return; + const t = along / length; + const nx = (partition.a[0] + ux * along) / NORM_W; + const ny = (partition.a[1] + uy * along) / this.host._spaceH; + let angle = Math.atan2(dy, dx) * 180 / Math.PI; + if (angle >= 90) angle -= 180; + else if (angle < -90) angle += 180; + const changed = cfg.host.t !== t || cfg.x !== nx || cfg.y !== ny || cfg.angle !== angle; + this.host._opDrag.moved = true; + if (changed) this.host._opDrag.dirty = true; + cfg.host = { ...cfg.host, t }; + cfg.x = nx; cfg.y = ny; cfg.angle = angle; + this.host._opMeasure = null; + if (changed) this.host._cfgEpoch++; + this.host.requestUpdate(); + return; + } + const snap = snapToWall(raw, space.rooms, this.host._gridPitch * 4); + if (!snap) return; // too far from any wall: the opening stays where it was + this.host._opDrag.moved = true; + // ruler badges on both shoulders + soft magnet to the wall's center + // (owner 2026-08-03) — the very same helper the PLACEMENT preview uses + const r = this._opRuler(snap, cfg.length * NORM_W); + this.host._opMeasure = r.measure; + const nx = r.x / NORM_W; + const ny = r.y / this.host._spaceH; + const geometryChanged = cfg.x !== nx || cfg.y !== ny || cfg.angle !== snap.angle; + if (geometryChanged) this.host._opDrag.dirty = true; + cfg.x = nx; + cfg.y = ny; + cfg.angle = snap.angle; + if (Number(this.host._serverCfg?.model_version || 0) >= 9) { + const host = resolveRoomOpeningHost(cfg, sp?.wall_segments || []); + if (host) cfg.host = host; + else delete cfg.host; + } + // The wall cut and room-coloured tunnel are geometry caches too. Advance + // the preview epoch so both follow the live opening instead of remaining at + // its pre-drag location until pointerup/save. + if (geometryChanged) this.host._cfgEpoch++; + this.host.requestUpdate(); + } + +public _opRuler( + snap: { x: number; y: number; angle: number }, + rlen: number, + ): { x: number; y: number; angle: number; measure: OpMeasure | null } { + const rooms = this.host._spaceModel()?.rooms || []; + const tol = this.host._gridPitch / 2; + let cx = snap.x, cy = snap.y; + let sh = openingShoulders([cx, cy], snap.angle, rlen, rooms, tol); + if (sh && sh.centered && (cx !== sh.wallCenter[0] || cy !== sh.wallCenter[1])) { + [cx, cy] = sh.wallCenter; + sh = openingShoulders([cx, cy], snap.angle, rlen, rooms, tol); + } else if (sh) { + // Not centred: quantise the offset ALONG the wall to the grid step + // (docs/CANVAS.md §9.3). Grid-BOUND would lift the opening off a diagonal + // wall, so the wall stays the master and the grid only says WHERE on it. + // The magnet is consulted first on purpose — a wall whose middle is not a + // node must still be able to hold a centred window. + const [ax, ay] = sh.wallA, [bx, by] = sh.wallB; + const wx = bx - ax, wy = by - ay; + const len = Math.hypot(wx, wy); + if (len > 0) { + const g = this.host._gridPitch; + const half = Math.min(rlen / 2, len / 2); + let along = Math.round((((cx - ax) * wx + (cy - ay) * wy) / len) / g) * g; + along = Math.max(half, Math.min(len - half, along)); + cx = ax + (along / len) * wx; + cy = ay + (along / len) * wy; + sh = openingShoulders([cx, cy], snap.angle, rlen, rooms, tol) || sh; + } + } + if (!sh) return { x: cx, y: cy, angle: snap.angle, measure: null }; + const imperial = this.host.hass?.config?.unit_system?.length === 'mi'; + const lbl = (d: number, m: number[]) => + ({ x: m[0], y: m[1], text: formatLength((d / this.host._gridPitch) * this.host._cellCm, imperial) }); + return { + x: cx, y: cy, angle: snap.angle, + measure: { + labels: [lbl(sh.sideA, sh.midA), lbl(sh.sideB, sh.midB)], + guide: sh.centered + ? { x: sh.wallCenter[0], y: sh.wallCenter[1], angle: snap.angle } + : null, + }, + }; + } + +public _opPointerUp(ev: PointerEvent, o: OpeningCfg): void { + if (!this.host._opDrag || this.host._opDrag.id !== o.id) return; + const drag = this.host._opDrag; + const moved = drag.moved; + this.host._opMeasure = null; // badges and the center tick live only through the drag + // only write when the geometry actually changed (audit L4) + if (moved && drag.dirty) { + this._commitPhysicalGeometry(this.host._t('history.move_opening'), drag.before); + } + // keep the flag until the click event that follows pointerup, then let it go + if (moved) window.setTimeout(() => (this.host._opDrag = null), 0); + else this.host._opDrag = null; + } + +public _opClick(ev: MouseEvent, o: RenderOpening): void { + // HP-1550-04: in the resize tool a click over an opening falls through to + // the stage (room picking) instead of opening the editor dialog + if (this.host._mode === 'plan' && this.host._tool === 'resize') return; + ev.stopPropagation(); + if (this.host._opDrag?.moved) return; // that click was the tail of a drag + // openings are inert outside Plan mode (owner's decision: View must not + // interact with them at all); in Plan any click on an opening edits it + if (this.host._mode === 'plan') this._editOpening(o); + } + +public _saveOpening(): void { + const d = this.host._openingDialog; + const sp = this.host._curSpaceCfg; + const model = this.host._spaceModel(); + if (!d || !sp || !model) return; + const before = this._geometrySnapshot(); + const H = this.host._spaceH; + const previous = (sp.openings || []).find((item: OpeningCfg) => item.id === d.id); + const o: OpeningCfg = { + ...(previous || {}), + id: d.id || 'o' + Date.now().toString(36), + type: d.type, + x: d.x / NORM_W, + y: d.y / H, + angle: d.angle, + length: previous && !d.lengthTouched + ? previous.length + : this.host._cmToUnits(Math.max(20, d.lengthCm)) / NORM_W, + ...(d.host ? { host: { ...d.host } } : {}), + }; + if (o.host?.kind === 'partition') { + const strict = partitionOpeningNeedsStrictValidation(previous, o); + const resolution = strict + ? resolvePartitionOpeningStrict( + o, model.partitions, NORM_W, this.host._cellCm, this.host._gridPitch, + ) + : resolvePartitionOpeningCompat( + o, model.partitions, NORM_W, this.host._cellCm, this.host._gridPitch, + ); + if (!resolution.resolved) { + const partition = model.partitions.find((item) => item.id === o.host!.id); + if (resolution.reason === 'does-not-fit-jamb' && partition) { + const margin = partitionOpeningJambMargin( + partition, this.host._cellCm, this.host._gridPitch, + ); + this.host._showToast(this.host._t('opening.partition_jamb_margin', { + distance: formatLength( + (margin / this.host._gridPitch) * this.host._cellCm, this.host._imperial, + ), + })); + } else { + this.host._showToast(this.host._t('opening.partition_orphan')); + } + return; + } + const siblings = (sp.openings || []).flatMap((item: OpeningCfg) => { + if (!item.host || item.id === o.id) return []; + const sibling = resolvePartitionOpeningCompat( + item, model.partitions, NORM_W, this.host._cellCm, this.host._gridPitch, + ).resolved; + return sibling ? [sibling] : []; + }); + if (hostedOpeningIntervalsOverlap(resolution.resolved, siblings)) { + this.host._showToast(this.host._t('toast.opening_no_wall')); + return; + } + Object.assign(o, materializePartitionOpening(o, resolution.resolved, NORM_W)); + } else if (Number(this.host._serverCfg?.model_version || 0) >= 9) { + const host = resolveRoomOpeningHost(o, sp.wall_segments || []); + if (!host) { + this.host._showToast(this.host._t('toast.opening_no_wall')); + return; + } + o.host = host; + } else { + delete o.host; + } + if (d.type === 'passage') { + // The canonical passage record contains geometry only. Delete keys + // instead of writing null/false so imports and old broken records have + // one unambiguous representation after an explicit edit. + delete o.contact; + delete o.lock; + delete o.invert; + delete o.flip_h; + delete o.flip_v; + } else { + o.contact = d.contact || null; + o.lock = d.type === 'door' || d.type === 'gate' ? d.lock || null : null; + o.invert = d.invert || undefined; + o.flip_h = d.type !== 'gate' && d.flipH || undefined; + o.flip_v = d.flipV || undefined; + } + sp.openings = sp.openings || []; + const i = sp.openings.findIndex((x: OpeningCfg) => x.id === o.id); + if (i >= 0) sp.openings[i] = o; + else sp.openings.push(o); + this._commitPhysicalGeometry( + this.host._t(d.id ? 'history.edit_opening' : 'history.add_opening'), before, + ); + this.host._openingDialog = null; + this.host.requestUpdate(); + } + +public _deleteOpening(): void { + const d = this.host._openingDialog; + const sp = this.host._curSpaceCfg; + if (!d?.id || !sp?.openings) return; + const before = this._geometrySnapshot(); + sp.openings = sp.openings.filter((x: OpeningCfg) => x.id !== d.id); + this._commitPhysicalGeometry(this.host._t('history.delete_opening'), before); + this.host._openingDialog = null; + this.host.requestUpdate(); + } + +public _rebindPartitionOpening = (): void => { + const d = this.host._openingDialog; + if (!d?.id) return; + const openingId = d.id; + this._activateMarkupTool('opening'); + if (this.host._tool !== 'opening') return; + // Tool activation clears every stale placement session. Publish the + // rebind identity only after that gate so the next click cannot degrade + // into creating a second opening. + this.host._openingRebindId = openingId; + this.host._openingPreset = { + type: d.type, + lengthCm: d.lengthCm, + flipH: d.flipH, + flipV: d.flipV, + revision: ++this.host._openingPresetRevision, + }; + this.host._openingDialog = null; + this.host._openingHoverCandidate = null; + }; + +public _contactCandidates(): { value: string; label: string }[] { + const out: [string, string, number][] = []; + for (const eid of Object.keys(this.host.hass.states)) { + if (!this.host._openingEntityAvailable(eid)) continue; + const dom = eid.split('.')[0]; + if (dom !== 'binary_sensor' && dom !== 'cover') continue; + const st = this.host.hass.states[eid]; + const dc = st?.attributes?.device_class || ''; + const doorish = ['door', 'window', 'opening', 'garage_door', 'garage'].includes(dc); + if (dom === 'cover' && !doorish) continue; + out.push([eid, st?.attributes?.friendly_name || eid, doorish ? 0 : 1]); + } + return out + .sort((a, b) => a[2] - b[2] || a[1].localeCompare(b[1])) + .map(([value, label]) => ({ value, label })); + } + +public _lockCandidates(): { value: string; label: string }[] { + return Object.keys(this.host.hass.states) + .filter((eid) => eid.startsWith('lock.') && this.host._openingEntityAvailable(eid)) + .map((eid) => ({ value: eid, label: this.host.hass.states[eid]?.attributes?.friendly_name || eid })) + .sort((a, b) => a.label.localeCompare(b.label)); + } + +public _toggleOpeningEntityPicker(kind: 'contact' | 'lock'): void { + const d = this.host._openingDialog; + if (!d) return; + const nextOpen = kind === 'contact' ? !d.contactOpen : !d.lockOpen; + this.host._openingDialog = { + ...d, + contactOpen: kind === 'contact' ? nextOpen : false, + lockOpen: kind === 'lock' ? nextOpen : false, + }; + } + +public _filterOpeningEntities(kind: 'contact' | 'lock', value: string): void { + const d = this.host._openingDialog; + if (!d) return; + this.host._openingDialog = kind === 'contact' + ? { ...d, contactFilter: value } + : { ...d, lockFilter: value }; + } + +public _selectOpeningEntity(kind: 'contact' | 'lock', value: string): void { + const d = this.host._openingDialog; + if (!d) return; + this.host._openingDialog = kind === 'contact' + ? { ...d, contact: value, contactOpen: false, contactFilter: '' } + : { ...d, lock: value, lockOpen: false, lockFilter: '' }; + } + +public _mergeClick(raw: number[]): void { + const space = this.host._spaceModel(); + if (!space) return; + const rooms = space.rooms; + const hit = [...rooms].reverse().find((r) => this._pointInRoom(raw, r)); + if (!hit?.id) return; + const hitId = hit.id; + if (!this.host._mergeSel || this.host._mergeSel === hitId) { + this.host._mergeSel = this.host._mergeSel === hitId ? null : hitId; // click again = deselect + return; + } + const a = rooms.find((r) => r.id === this.host._mergeSel); + const pa = a ? roomPoly(a) : null; + const pb = roomPoly(hit); + const merged = pa && pb ? mergeRooms(pa, pb) : null; + if (!merged) { + // only rooms sharing a wall collapse into one outline (see mergeRooms) + this.host._showToast(this.host._t('toast.merge_not_adjacent')); + this.host._mergeSel = null; + return; + } + this.host._mergeDialog = { aId: this.host._mergeSel, bId: hitId, poly: merged, pick: 'a' }; + this.host._mergeSel = null; + } + +public _commitMerge(): void { + const d = this.host._mergeDialog; + const sp = this.host._curSpaceCfg; + if (!d || !sp || !this.host._spaceModel()) return; + const before = this._geometrySnapshot(); + const H = this.host._spaceH; + const keepId = d.pick === 'a' ? d.aId : d.bId; + const dropId = d.pick === 'a' ? d.bId : d.aId; + const keep = sp.rooms.find((r: any) => r.id === keepId); + if (!keep) { + this.host._mergeDialog = null; + return; + } + // the kept room keeps its id, so its label position and devices stay put + keep.poly = d.poly.map((p) => [p[0] / NORM_W, p[1] / H]); + delete keep.x; delete keep.y; delete keep.w; delete keep.h; // a merged room is never a rect + sp.rooms = sp.rooms.filter((r: any) => r.id !== dropId); + const committed = this._commitPhysicalGeometry(this.host._t('history.merge_rooms'), before); + this.host._mergeDialog = null; + this.host._regSignature = ''; + this.host._maybeRebuildDevices(); + if (committed) this.host._showToast(this.host._t('toast.rooms_merged', { name: keep.name || '' })); + } + +public _splitClick(raw: number[]): void { + const space = this.host._spaceModel(); + if (!space) return; + const rooms = space.rooms; + if (!this.host._splitSel) { + const hit = [...rooms].reverse().find((r) => this._pointInRoom(raw, r)); + if (!hit?.id) return; + this.host._splitSel = { roomId: hit.id, pts: [] }; + return; + } + const room = rooms.find((r) => r.id === this.host._splitSel!.roomId); + const poly = room ? roomPoly(room) : null; + if (!room || !poly) { + this.host._splitSel = null; + return; + } + // A split point lands on the room's nearest wall — the user aims at a wall, + // and rooms need not be grid-aligned (imported/legacy polygons), so snapping + // to the grid would miss the outline. The pull is capped: a click far from + // any wall (e.g. an accidental one in the middle of the room) is a miss and + // gets the toast, not a wall the user never meant. splitRoom() still rejects + // any cut that is not a clean wall-to-wall chord. + // …and it is still QUANTISED: the offset ALONG the wall moves in whole grid + // steps (docs/CANVAS.md §9), which on the axis-aligned, grid-drawn walls the + // editor itself makes IS a grid node. There is no free-position bypass. + const eps = this.host._gridPitch * 0.02; + const pull = this.host._gridPitch * 6; // ≈2.5% of the plan width — generous but intentional + const raw0 = closestPointOnBoundary(raw, poly); + const near = raw0 ? (snapPointAlongPoly(raw0, poly, this.host._gridPitch) || raw0) : raw0; + const wallPt = raw0 && near && Math.hypot(raw0[0] - raw[0], raw0[1] - raw[1]) <= pull ? near : null; + const onWall = !!wallPt && pointOnBoundary(wallPt, poly, eps); + const cur = this.host._splitSel.pts; + if (!cur.length) { + // the cut starts on a wall + if (!onWall) { + this.host._showToast(this.host._t('toast.split_pick_wall')); + return; + } + this.host._splitSel = { ...this.host._splitSel, pts: [wallPt!] }; + return; + } + if (!onWall) { + // an interior click adds an intermediate vertex of the cut path + const mid = this._snap(raw); + if (!ptInside(mid, poly, eps)) { + this.host._showToast(this.host._t('toast.split_pick_inside')); + return; + } + this.host._splitSel = { ...this.host._splitSel, pts: [...cur, mid] }; + return; + } + // a wall point finishes the cut + const parts = splitRoomPath(poly, [...cur, wallPt!], eps); + if (!parts) { + this.host._showToast(this.host._t('toast.split_bad_cut')); + return; + } + this._resetRoomDialogFields(); + // the bigger part stays the room it was — name, area and devices go with it + const [p1, p2] = parts; + const main = polygonArea(p1) >= polygonArea(p2) ? p1 : p2; + const fresh = main === p1 ? p2 : p1; + this.host._pendingSplit = { roomId: room.id!, mainPoly: main, newPoly: fresh }; + this.host._cursorPt = null; + this.host._nameSel = ''; + this.host._areaSel = ''; + this.host._roomDialog = true; + } + +public _markupMove(ev: MouseEvent): void { + if (!this.host._markup) return; + if (this.host._tool === 'column') { + this.host._cursorPt = this._snap(this._svgPoint(ev)); + return; + } + if (this.host._tool === 'opening' || this.host._tool === 'wallthick') { + // hover preview: raw cursor point; snapping happens in the preview getters + this.host._cursorPt = this._svgPoint(ev); + return; + } + const architectural = this.host._tool === 'draw' && !this.host._contourClosed; + const cutting = this.host._tool === 'split' && !!this.host._splitSel?.pts?.length; + if (!architectural && !cutting) return; + const raw = this._svgPoint(ev); + if (architectural) { + const resolved = this._resolvePlanDrawPoint(raw, ev.shiftKey); + this.host._planSnapHover = { + contextKey: resolved.contextKey, + candidate: resolved.candidate, + conflicts: resolved.conflicts, + }; + this._syncPlanSnapConflictMarkers(resolved.conflicts); + // Before the first click there is no rubber-band to repaint. Updating the + // dedicated marker avoids walking the complete large-house Lit tree for + // every mouse move while click still resolves from the event coordinate. + if (!this.host._path.length) { + this._syncPlanSnapActiveMarker(resolved.candidate); + return; + } + this.host._cursorPt = resolved.point; + return; + } + this.host._cursorPt = this._snap(raw); + } + +public _saveRoom(): void { + if (!this.host._areaSel && !this.host._nameSel.trim()) return; + if (this.host._wallFaceBatch) { + this._decideWallFace(true); + return; + } + this._commitRoom(); + } + +public _decideWallFace(create: boolean): void { + const batch = this.host._wallFaceBatch; + if (!batch) return; + const candidate = batch.candidates[batch.index]; + if (!candidate) return; + if (!create && (candidate.existing || candidate.repair) && batch.candidates.length === 1) { + this.host._wallFaceBatch = null; + this.host._roomDialog = false; + this.host._nameSel = ''; + this.host._areaSel = ''; + this.host.requestUpdate(); + return; + } + const decision: WallFaceDecision = create ? { + candidate, + create: true, + name: this.host._nameSel.trim(), + area: this.host._areaSel || null, + settings: this._roomSettingsFromDialog() + ? JSON.parse(JSON.stringify(this._roomSettingsFromDialog())) : null, + } : { candidate, create: false }; + const decisions = [...batch.decisions, decision]; + const nextIndex = batch.index + 1; + if (nextIndex < batch.candidates.length) { + this.host._wallFaceBatch = { ...batch, decisions, index: nextIndex }; + this.host._nameSel = ''; + this.host._areaSel = ''; + this._resetRoomDialogFields(); + this.host.requestUpdate(); + return; + } + this.host._wallFaceBatch = { ...batch, decisions }; + this._applyWallFaceBatch(); + } + +public _wallSourceCmAt( + point: number[], activePath: number[][], activeCms: number[], + ): number { + const validCm = (value: unknown): number | null => + typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null; + const epsilon = this.host._gridPitch * 0.02; + const model = this.host._spaceModel(); + if (!model) return DRAW_WALL_DEFAULT_CM; + // Already-saved independent masonry is authoritative when the new chain + // traces it. Room-owned walls are preserved by the canonical wall helpers. + for (const segment of this._planSnapGeometrySnapshot().value.segments) { + if (segment.sourceKind === 'room' + || distToSegment(point, [segment.a[0], segment.a[1], segment.b[0], segment.b[1]]) > epsilon) continue; + if (segment.sourceKind === 'partition') { + return validCm(model.partitions.find((item) => item.id === segment.sourceId)?.cm) + ?? DRAW_WALL_DEFAULT_CM; + } + const separator = segment.sourceId.lastIndexOf(':'); + const draftId = separator >= 0 ? segment.sourceId.slice(0, separator) : segment.sourceId; + const index = separator >= 0 ? Number(segment.sourceId.slice(separator + 1)) : -1; + return validCm(model.room_drafts.find((item) => item.id === draftId)?.segments[index]?.cm) + ?? DRAW_WALL_DEFAULT_CM; + } + // Толщина отрезка активной цепочки решается тем же резолвером (#234): + // именно это значение подсвечивает инструмент «Толщина», и расхождение с + // записью здесь было тем способом, которым дефект и обнаружился. + const resolved = chainSegmentCms( + Math.max(0, activePath.length - 1), activeCms, + this.host._drawWallCm, DRAW_WALL_DEFAULT_CM, + ); + for (let i = 0; i + 1 < activePath.length; i++) { + const a = activePath[i], b = activePath[i + 1]; + if (distToSegment(point, [a[0], a[1], b[0], b[1]]) <= epsilon) { + return resolved[i] ?? DRAW_WALL_DEFAULT_CM; + } + } + return DRAW_WALL_DEFAULT_CM; + } + +public _activePathWithRepair( + path: number[][], proposal: WallFaceRepairProposal | undefined, + ): number[][] { + const result = path.map((point) => [...point]); + if (!proposal?.sourceKey.startsWith('active:')) return result; + const match = /:(\d+)$/.exec(proposal.sourceKey); + const index = match ? Number(match[1]) + (proposal.endpoint === 'b' ? 1 : 0) : -1; + if (index >= 0 && index < result.length) result[index] = [...proposal.to]; + return result; + } + +public _validateWallRepair( + proposal: WallFaceRepairProposal, activePath: number[][], + ): boolean { + const epsilon = this.host._gridPitch * 0.0002; + const sources = this._wallGraphSources(activePath); + const source = sources.find((item) => item.key === proposal.sourceKey); + const target = sources.find((item) => item.key === proposal.targetSourceKey); + if (!source || !target) return false; + const current = proposal.endpoint === 'a' ? source.a : source.b; + if (Math.hypot(current[0] - proposal.from[0], current[1] - proposal.from[1]) > epsilon) return false; + if (distToSegment(proposal.to, [target.a[0], target.a[1], target.b[0], target.b[1]]) > epsilon) { + return false; + } + if (repairMovesHostedPartition( + proposal, (this.host._curSpaceCfg as any)?.openings || [], + )) return false; + return true; + } + +public _applyWallRepair( + proposal: WallFaceRepairProposal, batch: WallFaceBatch, + ): boolean { + const sp = this.host._curSpaceCfg as any; + if (!sp || !this._validateWallRepair(proposal, batch.activePath)) return false; + const write = (point: number[]): void => { + point[0] = proposal.to[0] / NORM_W; + point[1] = proposal.to[1] / NORM_W; + }; + if (proposal.sourceKey.startsWith('active:')) { + const match = /:(\d+)$/.exec(proposal.sourceKey); + const index = match ? Number(match[1]) + (proposal.endpoint === 'b' ? 1 : 0) : -1; + if (index < 0 || index >= batch.activePath.length) return false; + batch.activePath[index] = [...proposal.to]; + const draft = (sp.room_drafts || []).find((item) => item.id === batch.activeDraftId); + if (draft?.points?.[index]) write(draft.points[index]); + return true; + } + if (!proposal.sourceKey.startsWith('static:')) return false; + const [kind, sourceId] = proposal.sourceKey.slice('static:'.length).split('|'); + if (kind === 'partition') { + const partition = (sp.partitions || []).find((item: PartitionCfg) => item.id === sourceId); + if (!partition) return false; + const renderA = [partition.a[0] * NORM_W, partition.a[1] * NORM_W]; + const renderB = [partition.b[0] * NORM_W, partition.b[1] * NORM_W]; + const point = Math.hypot(renderA[0] - proposal.from[0], renderA[1] - proposal.from[1]) + <= this.host._gridPitch * 0.0002 ? partition.a + : Math.hypot(renderB[0] - proposal.from[0], renderB[1] - proposal.from[1]) + <= this.host._gridPitch * 0.0002 ? partition.b : null; + if (!point) return false; + write(point); + return true; + } + if (kind === 'draft') { + const separator = sourceId.lastIndexOf(':'); + const draftId = separator >= 0 ? sourceId.slice(0, separator) : ''; + const segment = separator >= 0 ? Number(sourceId.slice(separator + 1)) : -1; + const draft = (sp.room_drafts || []).find((item) => item.id === draftId); + if (!draft?.points?.[segment] || !draft?.points?.[segment + 1]) return false; + const candidates = [segment, segment + 1]; + const index = candidates.find((candidate) => Math.hypot( + draft.points[candidate][0] * NORM_W - proposal.from[0], + draft.points[candidate][1] * NORM_W - proposal.from[1], + ) <= this.host._gridPitch * 0.0002); + if (index == null) return false; + write(draft.points[index]); + return true; + } + return false; + } + +public _applyWallFaceBatch(): void { + const batch = this.host._wallFaceBatch; + const sp = this.host._curSpaceCfg as any; + const model = this.host._spaceModel(); + if (!batch || !sp || !model) return; + const abort = (message: string, vars?: Record): void => { + this.host._showToast(this.host._t(message as any, vars)); + this._roomDialogCancel(); + }; + const accepted = batch.decisions.filter((decision) => decision.create); + const acceptedRings = accepted.map((decision) => decision.candidate.ring); + const existingRooms = model.rooms; + for (let i = 0; i < acceptedRings.length; i++) { + if (this._contourSelfIntersects(acceptedRings[i]) || polygonArea(acceptedRings[i]) <= 1e-6) { + abort('toast.contour_cannot_close'); + return; + } + for (let j = i + 1; j < acceptedRings.length; j++) { + if (roomsOverlap(acceptedRings[i], acceptedRings[j])) { + abort('toast.contour_cannot_close'); + return; + } + } + const ownSplit = accepted[i].candidate.split?.roomId; + const clash = existingRooms.find((room) => room.id !== ownSplit + && !!roomPoly(room) && roomsOverlap(acceptedRings[i], roomPoly(room)!)); + if (clash) { + abort('toast.room_overlap', { name: clash.name || '' }); + return; + } + } + if ((sp.rooms || []).length + accepted.length > MAX_ROOMS) { + abort('toast.physical_limit'); + return; + } + if (accepted.some((decision) => decision.candidate.split + && !sp.rooms.some((room) => room.id === decision.candidate.split!.roomId))) { + abort('toast.contour_cannot_close'); + return; + } + + const repairs = accepted + .map((decision) => decision.candidate.repair) + .filter((repair): repair is WallFaceRepairProposal => !!repair); + if (repairs.length > 1 || (repairs[0] && !this._validateWallRepair(repairs[0], batch.activePath))) { + abort('toast.wall_repair_changed'); + return; + } + const effectiveActivePath = this._activePathWithRepair(batch.activePath, repairs[0]); + + const epsilon = this.host._gridPitch * 0.0002; + const activeSourceCms = new Map(); + for (let i = 0; i < batch.activeCms.length; i++) + activeSourceCms.set(this._activeWallSourceKey(i), batch.activeCms[i]); + const partitions: Array<{ id?: string; a: number[]; b: number[]; cm: number }> = []; + if (!accepted.length) { + partitions.push(...wallChainSegments( + effectiveActivePath, + chainSegmentCms( + effectiveActivePath.length - 1, batch.activeCms, + this.host._drawWallCm, DRAW_WALL_DEFAULT_CM, + ), + )); + } else { + const consumed = new Set(accepted.flatMap((decision) => decision.candidate.atomKeys)); + const consumeAll = accepted.some((decision) => decision.candidate.consumeAllActive); + for (const atom of atomizeWallSegments(this._wallGraphSources(effectiveActivePath), epsilon)) { + const activeKey = atom.sourceKeys.find((key) => activeSourceCms.has(key)); + if (!activeKey || consumeAll || consumed.has(atom.key)) continue; + partitions.push({ a: atom.a, b: atom.b, cm: activeSourceCms.get(activeKey)! }); + } + } + if ((sp.partitions || []).length + partitions.length > MAX_PARTITIONS) { + abort('toast.physical_limit'); + return; + } + + // A face decision may split one persisted draft segment between a room + // contour and leftover partitions. Pick the single physical child that + // inherits the old ID by the same midpoint/old-a rule as the catalogue. + const roomLineage = accepted.map((decision) => + decision.candidate.ring.map(() => '')); + const activeDraft = batch.activeDraftId + ? (sp.room_drafts || []).find((draft) => draft.id === batch.activeDraftId) + : null; + const draftLineage = this._draftSegmentsForPath( + batch.activePath, activeDraft, batch.activeCms, + ); + const geometryKey = (a: readonly number[], b: readonly number[]): string => { + const ka = `${a[0].toFixed(9)},${a[1].toFixed(9)}`; + const kb = `${b[0].toFixed(9)},${b[1].toFixed(9)}`; + return ka <= kb ? `${ka}|${kb}` : `${kb}|${ka}`; + }; + for (let sourceIndex = 0; sourceIndex < draftLineage.length; sourceIndex++) { + const id = draftLineage[sourceIndex]?.id; + if (!id) continue; + const sourceA = batch.activePath[sourceIndex]; + const sourceB = batch.activePath[sourceIndex + 1]; + const sourceSpan = [sourceA[0], sourceA[1], sourceB[0], sourceB[1]]; + const candidates: Array<{ + kind: 'room' | 'partition'; owner: number; edge: number; a: number[]; b: number[]; + }> = []; + accepted.forEach((decision, owner) => decision.candidate.ring.forEach((a, edge) => { + const b = decision.candidate.ring[(edge + 1) % decision.candidate.ring.length]; + if (distToSegment(a, sourceSpan) <= epsilon && distToSegment(b, sourceSpan) <= epsilon) + candidates.push({ kind: 'room', owner, edge, a, b }); + })); + partitions.forEach((partition, owner) => { + if (distToSegment(partition.a, sourceSpan) <= epsilon + && distToSegment(partition.b, sourceSpan) <= epsilon) + candidates.push({ kind: 'partition', owner, edge: 0, a: partition.a, b: partition.b }); + }); + const midpoint = [(sourceA[0] + sourceB[0]) / 2, (sourceA[1] + sourceB[1]) / 2]; + candidates.sort((left, right) => { + const leftSpan = [left.a[0], left.a[1], left.b[0], left.b[1]]; + const rightSpan = [right.a[0], right.a[1], right.b[0], right.b[1]]; + return (distToSegment(midpoint, leftSpan) <= epsilon ? 0 : 1) + - (distToSegment(midpoint, rightSpan) <= epsilon ? 0 : 1) + || (distToSegment(sourceA, leftSpan) <= epsilon ? 0 : 1) + - (distToSegment(sourceA, rightSpan) <= epsilon ? 0 : 1) + || geometryKey(left.a, left.b).localeCompare(geometryKey(right.a, right.b)); + }); + const winner = candidates[0]; + if (!winner) continue; + const winnerKey = geometryKey(winner.a, winner.b); + for (const candidate of candidates) { + if (geometryKey(candidate.a, candidate.b) !== winnerKey) continue; + if (candidate.kind === 'room') roomLineage[candidate.owner][candidate.edge] = id; + else partitions[candidate.owner].id = id; + } + } + + const before = this._geometrySnapshot(); + if (repairs[0] && !this._applyWallRepair(repairs[0], batch)) { + abort('toast.wall_repair_changed'); + return; + } + const hasSplit = accepted.some((decision) => !!decision.candidate.split); + const splitWalls = hasSplit + ? materializeWallIntervals( + model.rooms, sp.walls, this.host._openCuts(), + this.host._wallKeyPitch, this.host._cellCm, this.host._gridPitch, NORM_W, + ) + : null; + const wallsBeforeSplit = hasSplit && Array.isArray(sp.walls) ? sp.walls : null; + for (const decision of accepted) { + const split = decision.candidate.split; + if (!split) continue; + const main = sp.rooms.find((room) => room.id === split.roomId); + if (!main) { + abort('toast.contour_cannot_close'); + return; + } + main.poly = split.mainPoly.map((point) => [point[0] / NORM_W, point[1] / this.host._spaceH]); + delete main.x; delete main.y; delete main.w; delete main.h; + } + + const seed = Date.now().toString(36); + const newRooms: Array<{ room: any; decision: WallFaceDecision }> = []; + accepted.forEach((decision, index) => { + const areaName = decision.area ? this.host.hass.areas[decision.area]?.name : ''; + const room = { + id: `r${seed}-${index}`, + name: decision.name || areaName || this.host._t('room.default_name'), + area: decision.area || null, + poly: decision.candidate.ring.map((point) => + [point[0] / NORM_W, point[1] / this.host._spaceH]), + ...(roomLineage[index].some(Boolean) ? { wall_ids: roomLineage[index] } : {}), + ...(decision.settings ? { settings: JSON.parse(JSON.stringify(decision.settings)) } : {}), + }; + sp.rooms.push(room); + newRooms.push({ room, decision }); + }); + + if (batch.activeDraftId && Array.isArray(sp.room_drafts)) { + sp.room_drafts = sp.room_drafts.filter((draft) => draft.id !== batch.activeDraftId); + if (!sp.room_drafts.length) delete sp.room_drafts; + } + if (partitions.length) { + sp.partitions ||= []; + partitions.forEach((partition, index) => sp.partitions.push({ + id: partition.id || `partition-${seed}-${index}`, + a: [partition.a[0] / NORM_W, partition.a[1] / NORM_W], + b: [partition.b[0] / NORM_W, partition.b[1] / NORM_W], + cm: partition.cm, + })); + } + + if (hasSplit) { + const next = this.host._normalizeWalls(splitWalls, this.host._openCuts()); + if (next.length) sp.walls = next; + else if (!wallsBeforeSplit?.length) delete sp.walls; + else sp.walls = wallsBeforeSplit; + } + if (newRooms.length) { + // Make the new model visible to the canonical wall helpers before they + // key intervals. This is still inside the one history/save transaction. + this.host._cfgEpoch++; + const openCuts = this.host._openCuts(); + const updatedModel = this.host._spaceModel(); + if (!updatedModel) return; + let walls = sp.walls; + for (const { room, decision } of newRooms) { + const ring = decision.candidate.ring; + const fallback = this._wallSourceCmAt( + ring[0], batch.activePath, batch.activeCms, + ); + walls = applyWallThicknessToNewRoom( + walls, updatedModel.rooms, room.id, fallback, + this.host._wallKeyPitch, openCuts, NORM_W, + ); + for (const interval of wallIntervals( + updatedModel.rooms, walls, openCuts, + this.host._wallKeyPitch, this.host._cellCm, this.host._gridPitch, NORM_W, + )) { + if (interval.roomId !== room.id || interval.kind !== 'outer') continue; + const midpoint = [ + (interval.a[0] + interval.b[0]) / 2, + (interval.a[1] + interval.b[1]) / 2, + ]; + walls = setWallThickness( + walls, interval.a, interval.b, + this._wallSourceCmAt(midpoint, batch.activePath, batch.activeCms), + this.host._wallKeyPitch, NORM_W, + ); + } + } + const normalized = this.host._normalizeWalls(walls, openCuts); + if (normalized.length) sp.walls = normalized; + else delete sp.walls; + } + + if (!this._commitPhysicalGeometry(this.host._t('history.wall_face_batch'), before)) return; + delete this.host._resumeDraftBySpace[this.host._space]; + this.host._path = []; + this.host._activeDraftId = null; + this.host._draftSegmentCms = []; + this.host._closingWallCm = null; + this.host._wallFaceBatch = null; + this.host._roomDialog = false; + this.host._nameSel = ''; + this.host._areaSel = ''; + this.host._regSignature = ''; + this.host._maybeRebuildDevices(); + // Match the ordinary room-save contract: newly area-bound devices are + // auto-added and their initial positions are pinned immediately, while a + // clean split leaves the surviving parent's existing layout untouched. + const boundAreas = new Set(accepted + .map((decision) => decision.area) + .filter((area): area is string => !!area)); + if (boundAreas.size) { + const next = { ...this.host._layout }; + for (const device of this.host._devices) { + if (!boundAreas.has(device.area || '') || device.space !== this.host._space) continue; + if (this.host._layout[device.id]) continue; + const position = this.host._defPos[device.id]; + if (!position) continue; + next[device.id] = { + s: this.host._space, x: position.x / NORM_W, y: position.y / NORM_W, + }; + this.host._dirtyPos.add(device.id); + } + this.host._layout = next; + this.host._persistLayout(); + } + this.host._showToast(this.host._t(accepted.length ? 'toast.wall_rooms_saved' : 'toast.wall_chain_saved', { + n: accepted.length, + })); + } + +public _commitRoom(): void { + const sp = this.host._curSpaceCfg; + const space = this.host._spaceModel(); + if (!sp || !space) return; + const before = this._geometrySnapshot(); + const H = this.host._spaceH; + const wasSplit = !!this.host._pendingSplit; + // Split changes the two source-wall edges into shorter children. Preserve + // the effective old profile while the original outline still proves the + // full extent of legacy midpoint-only wall keys; after the mutation that + // information is irrecoverable and one child would normalise to 0 cm. + const splitWalls = wasSplit + ? materializeWallIntervals( + space.rooms, sp.walls, this.host._openCuts(), + this.host._wallKeyPitch, this.host._cellCm, this.host._gridPitch, NORM_W, + ) + : null; + const wallsBeforeSplit = wasSplit && Array.isArray(sp.walls) ? sp.walls : null; + let verts: number[][]; + if (this.host._pendingSplit) { + // apply the cut now: the bigger part keeps the original room, this dialog names the rest + const main = sp.rooms.find((r: any) => r.id === this.host._pendingSplit!.roomId); + if (!main) { + this.host._pendingSplit = null; + this.host._splitSel = null; + this.host._roomDialog = false; + return; + } + main.poly = this.host._pendingSplit.mainPoly.map((p) => [p[0] / NORM_W, p[1] / H]); + delete main.x; delete main.y; delete main.w; delete main.h; + verts = this.host._pendingSplit.newPoly; + } else { + if (!this.host._contourClosed) return; + verts = this.host._path.slice(0, -1); // without the duplicated closing vertex + } + const areaName = this.host._areaSel ? this.host.hass.areas[this.host._areaSel]?.name : ''; + const newRoom: any = { + id: 'r' + Date.now().toString(36), + name: this.host._nameSel || areaName || this.host._t('room.default_name'), + area: this.host._areaSel || null, + poly: verts.map((p) => [p[0] / NORM_W, p[1] / H]), + ...(this._roomSettingsFromDialog() ? { settings: this._roomSettingsFromDialog() } : {}), + }; + if (!wasSplit && this.host._activeDraftId) { + const activeDraft = Array.isArray((sp as any).room_drafts) + ? (sp as any).room_drafts.find((draft) => draft.id === this.host._activeDraftId) + : null; + const cms = chainSegmentCms( + verts.length, + [...this.host._draftSegmentCms, this.host._closingWallCm ?? undefined], + this.host._drawWallCm, DRAW_WALL_DEFAULT_CM, + ); + const lineage = this._draftSegmentsForPath(this.host._path, activeDraft, cms); + if (lineage.length === verts.length && lineage.every((segment) => !!segment.id)) { + newRoom.wall_ids = lineage.map((segment) => segment.id!); + } + } + sp.rooms.push(newRoom); + // Closing a saved draft promotes it into a room in the same transaction. + if (!wasSplit && this.host._activeDraftId && Array.isArray((sp as any).room_drafts)) { + (sp as any).room_drafts = (sp as any).room_drafts.filter((d: any) => d.id !== this.host._activeDraftId); + if (!(sp as any).room_drafts.length) delete (sp as any).room_drafts; + } + // A Split rewrites the parent outline and adds a child, so a span that used + // to sit between the parent and a neighbour may now belong to the child. + // Geometry first, then spans and the derived open_to — otherwise border + // trimming reads the new geometry while glow reads the old connectivity + // (AUD-159B6-02). + if (wasSplit) { + const next = this.host._normalizeWalls(splitWalls, this.host._openCuts()); + if (next.length) sp.walls = next; + else if (!wallsBeforeSplit?.length) delete sp.walls; + else sp.walls = wallsBeforeSplit; + } + // Draw-session wall thickness: apply to new edges only; keep neighbour cm + // on shared stretches. Split naming does not use the Draw field. + if (!wasSplit) { + const edgeCms = chainSegmentCms( + verts.length, + [...this.host._draftSegmentCms, this.host._closingWallCm ?? undefined], + this.host._drawWallCm, DRAW_WALL_DEFAULT_CM, + ); + const cm = edgeCms[0]; + if (cm != null) { + this.host._cfgEpoch++; // the new room must be in the model before keying + const openCuts = this.host._openCuts(); + const updatedSpace = this.host._spaceModel(); + if (!updatedSpace) return; + let next = applyWallThicknessToNewRoom( + sp.walls, updatedSpace.rooms, newRoom.id, cm, + this.host._wallKeyPitch, openCuts, NORM_W, + ); + // The room may have been drawn while the toolbar thickness changed. + // Shared stretches keep the already-existing physical wall; every + // outer atomic stretch receives the value of its source draft edge. + for (const iv of wallIntervals( + updatedSpace.rooms, next, openCuts, + this.host._wallKeyPitch, this.host._cellCm, this.host._gridPitch, NORM_W, + )) { + if (iv.roomId !== newRoom.id || iv.kind !== 'outer') continue; + const mid = [(iv.a[0] + iv.b[0]) / 2, (iv.a[1] + iv.b[1]) / 2]; + const source = verts.findIndex((a, i) => { + const b = verts[(i + 1) % verts.length]; + return distToSegment(mid, [a[0], a[1], b[0], b[1]]) <= this.host._gridPitch * 0.02; + }); + if (source >= 0) next = setWallThickness( + next, iv.a, iv.b, edgeCms[source], + this.host._wallKeyPitch, NORM_W, + ); + } + next = this.host._normalizeWalls(next, openCuts); + if (next.length) sp.walls = next; + else delete sp.walls; + } + } + const committed = this._commitPhysicalGeometry( + this.host._t(wasSplit ? 'history.split_room' : 'history.add_room'), before, + ); + this.host._path = []; + delete this.host._resumeDraftBySpace[this.host._space]; + this.host._activeDraftId = null; + this.host._draftSegmentCms = []; + this.host._closingWallCm = null; + this.host._pendingSplit = null; + this.host._splitSel = null; + const boundArea = this.host._areaSel; + this.host._areaSel = ''; + this.host._nameSel = ''; + this.host._roomDialog = false; + this.host._regSignature = ''; + this.host._maybeRebuildDevices(); + if (!committed) return; + // auto-add the area's device icons + PIN their positions in the layout, + // so that icons do not get reshuffled when the order in the HA registry changes. + let added = 0; + if (boundArea) { + const H2 = NORM_W; + const next = { ...this.host._layout }; + for (const d of this.host._devices) { + if (d.area !== boundArea || d.space !== this.host._space) continue; + added++; + if (this.host._layout[d.id]) continue; // placed manually — leave it alone + const dp = this.host._defPos[d.id]; + if (!dp) continue; + next[d.id] = { s: this.host._space, x: dp.x / NORM_W, y: dp.y / H2 }; + this.host._dirtyPos.add(d.id); + } + this.host._layout = next; + this.host._persistLayout(); + } + const roomsN = this.host._model.find((s) => s.id === this.host._space)?.rooms.length || 0; + this.host._showToast( + boundArea + ? this.host._t('toast.room_saved', { n: roomsN, added }) + : this.host._t('toast.room_saved_no_area', { n: roomsN }), + ); + } + +public _cancelPath(): void { + this.host._wallRepairDiagnostic = null; + if (this.host._activeDraftId) this.host._resumeDraftBySpace[this.host._space] = this.host._activeDraftId; + this.host._path = []; + this.host._activeDraftId = null; + this.host._draftSegmentCms = []; + this.host._closingWallCm = null; + this._clearPlanSnapHover(); + this.host._roomDialog = false; + this.host._pendingSplit = null; + this.host._wallFaceBatch = null; + this.host._splitSel = null; + this.host._mergeSel = null; + this.host._mergeDialog = null; + this.host._physicalSel = null; + this.host._physicalDrag = null; + this.host._physicalRotate = null; + this._clearOpeningPlacement(true); + } + +public _resumeLastDraft(): void { + const id = this.host._resumeDraftBySpace[this.host._space]; + if (!id) return; + const draft = this.host._spaceModel()?.room_drafts.find((d) => d.id === id); + if (!draft) { delete this.host._resumeDraftBySpace[this.host._space]; return; } + this.host._activeDraftId = id; + this.host._path = draft.points.map((p) => [...p]); + this.host._draftSegmentCms = this._adoptDraftCms( + this.host._path, draft.segments.map((s: any) => s.cm), draft.id, + ); + this._clearPlanSnapHover(); + } + +public _roomDialogCancel(): void { + this.host._roomDialog = false; + if (this.host._roomEditId) { + this.host._roomEditId = null; + this.host._nameSel = ''; + this.host._areaSel = ''; + return; + } + if (this.host._wallFaceBatch) { + const batch = this.host._wallFaceBatch; + this.host._wallFaceBatch = null; + this.host._path = batch.activePath.map((point) => [...point]); + this.host._draftSegmentCms = [...batch.activeCms]; + this.host._activeDraftId = batch.activeDraftId; + this.host._nameSel = ''; + this.host._areaSel = ''; + this.host.requestUpdate(); + return; + } + if (this.host._pendingSplit) { + // nothing was applied yet — drop the cut entirely, the room stays whole + this.host._pendingSplit = null; + this.host._splitSel = null; + return; + } + this.host._undoPoint(); + } + +public _openDeviceInbox(): void { + this.host._deviceInboxReturn = null; + this.host._deviceInbox = this.host._deviceInbox || { + tab: 'on_plan', search: '', showEntities: false, onlyNew: false, limit: 100, + }; + } + +public _closeMarkerDialog(): void { + this.host._markerDialog = null; + if (this.host._deviceInboxReturn) { + const restored = { ...this.host._deviceInboxReturn }; + this.host._deviceInbox = restored; + this.host._deviceInboxReturn = null; + if (restored.anchor) { + void this.host.updateComplete.then(() => requestAnimationFrame(() => { + const selector = `.device-inbox-row[data-binding="${CSS.escape(restored.anchor!)}"]`; + this.host.renderRoot.querySelector(selector)?.scrollIntoView({ block: 'nearest' }); + })); + } + } + } + +public _deviceInboxCandidates(showEntities: boolean) { + return bindingCandidates({ + hass: this.host._planHass, + devices: this.host._devices, + markers: this.host._markers, + showEntities, + labels: { + device: this.host._t('marker.sub_device'), + z2mGroup: this.host._t('marker.sub_z2m_group'), + group: this.host._t('marker.sub_group'), + helper: this.host._t('marker.sub_helper'), + entity: this.host._t('marker.sub_entity'), + }, + }); + } + +public _deviceInboxRows(): DeviceInboxRow[] { + const dialog = this.host._deviceInbox || this.host._deviceInboxReturn; + const showEntities = !!dialog?.showEntities; + const key = [ + this.host._haRegistry.revision, this.host._cfgRev, this.host._cfgEpoch, this.host._regSignature, + this.host._newSyncKey, showEntities ? 1 : 0, this.host._showAll ? 1 : 0, + langOf(this.host.hass, this.host._config?.language), + ].join('|'); + if (this.host._deviceInboxMemo?.key === key) return this.host._deviceInboxMemo.rows; + const candidates = this._deviceInboxCandidates(showEntities); + const bindings = new Set(candidates.map((item) => item.value)); + for (const marker of this.host._markers) if (marker.binding && marker.binding !== 'virtual') bindings.add(marker.binding); + for (const device of this.host._devices) { + if (device.bindingKind && device.bindingKind !== 'virtual' && device.bindingRef) { + bindings.add(`${device.bindingKind}:${device.bindingRef}`); + } + } + const statuses = new Map(); + for (const binding of bindings) statuses.set(binding, this.host._bindingStatus(binding)); + + const areaMap = this.host._areaToSpace; + const areaNames: Record = {}; + for (const [id, area] of Object.entries(this.host.hass?.areas || {})) { + areaNames[id] = areaMap[id]?.room?.name || area?.name || id; + } + // A room may keep a valid HA area reference even when the current user has + // only a limited area-registry snapshot. Its plan-visible name is still + // authoritative for the catalog and search. + for (const [id, target] of Object.entries(areaMap)) { + areaNames[id] = target.room.name || areaNames[id] || id; + } + const spaceNames = Object.fromEntries(this.host._model.map((space) => [space.id, space.title])); + const spaceByArea = Object.fromEntries( + Object.entries(areaMap).map(([area, value]) => [area, value.space]), + ); + const integrationByBinding: Record = {}; + const devicePlatforms = new Map>(); + for (const [entityId, entity] of Object.entries(this.host._fullRegistryHass.entities || {})) { + const platform = String(entity?.platform || '').trim(); + if (platform) integrationByBinding[`entity:${entityId}`] = platform; + if (platform && entity?.device_id) { + const set = devicePlatforms.get(entity.device_id) || new Set(); + set.add(platform); + devicePlatforms.set(entity.device_id, set); + } + } + for (const [deviceId, platforms] of devicePlatforms) { + integrationByBinding[`device:${deviceId}`] = [...platforms].sort().join(', '); + } + const reasonByBinding: Record = {}; + for (const [deviceId, device] of Object.entries(this.host._fullRegistryHass.devices || {})) { + const binding = `device:${deviceId}`; + const platforms = devicePlatforms.get(deviceId) || new Set(); + const identifierDomain = Array.isArray(device?.identifiers?.[0]) + ? String(device.identifiers[0][0] || '') : ''; + const excluded = [identifierDomain, ...platforms].some((domain) => this.host._excluded.has(domain)); + if (device?.entry_type === 'service') reasonByBinding[binding] = 'service_entry'; + else if (excluded) reasonByBinding[binding] = 'excluded_integration'; + else if (device?.model === 'Group') reasonByBinding[binding] = 'grouped_light'; + else if (/scene/i.test(device?.model || '')) reasonByBinding[binding] = 'excluded_domain'; + else if (/bridge/i.test(`${device?.model || ''}${device?.name || ''}`) + || (identifierDomain === 'myheat' && device?.via_device_id)) { + reasonByBinding[binding] = 'represented_by_parent'; + } + } + const rows = buildDeviceInbox({ + devices: this.host._devices, + markers: this.host._markers, + candidates, + statuses, + newDeviceIds: this.host._newIds, + showHiddenOnPlan: this.host._showAll, + areaNames, spaceNames, spaceByArea, integrationByBinding, reasonByBinding, + }); + this.host._deviceInboxMemo = { key, rows }; + return rows; + } + +public _deviceForInboxRow(row: DeviceInboxRow): DevItem | null { + const runtime = row.deviceId ? this.host._devices.find((item) => item.id === row.deviceId) : null; + if (runtime) return runtime; + const marker = row.markerId ? this.host._markers.find((item) => item.id === row.markerId) : null; + if (!marker || marker.removed) return null; + return { + id: marker.id, + name: row.name, + model: row.model, + area: row.areaId, + space: row.spaceId || this.host._space, + hidden: marker.hidden === true || row.status.kind === 'ha_disabled', + userHidden: marker.hidden === true, + bindingStatus: row.status, + icon: row.icon, + entities: row.status.kind === 'active' ? row.status.enabledEntityIds : [], + allEntities: row.status.allEntityIds, + primary: row.status.kind === 'active' ? row.status.enabledEntityIds[0] : undefined, + marker, + bindingKind: row.kind, + bindingRef: row.binding.slice(row.binding.indexOf(':') + 1), + pdfs: marker.pdfs || [], + }; + } + +public _openInboxMarker(row: DeviceInboxRow, add = false): void { + const snapshot = this.host._deviceInbox; + if (!snapshot) return; + this.host._deviceInboxReturn = { ...snapshot, anchor: row.key }; + this.host._deviceInbox = null; + if (!add) { + const device = this._deviceForInboxRow(row); + if (device) this._openMarkerDialog(device); + else this._closeMarkerDialog(); + return; + } + this._openMarkerDialog(); + if (!this.host._markerDialog) { + this._closeMarkerDialog(); + return; + } + this.host._markerDialog = { + ...this.host._markerDialog, + bindingMode: 'ha', binding: row.binding, bindingOpen: false, + showEntities: row.kind === 'entity', name: '', + }; + } + +public async _setInboxHidden(row: DeviceInboxRow, hidden: boolean): Promise { + const dialog = this.host._deviceInbox; + const cfg = this.host._serverCfg; + if (!dialog || !cfg || dialog.busy || row.status.kind !== 'active') return; + const previous = cfg.markers || []; + const live = previous.find((marker) => !marker.removed && marker.binding === row.binding); + if (!hidden && !live) return; + const id = live?.id || markerIdForBinding( + row.binding, row.markerId, () => `m_${Date.now().toString(36)}`, + ); + const next: Marker = live + ? { ...live, hidden } + : { id, binding: row.binding, hidden: true }; + cfg.markers = [ + ...previous.filter((marker) => marker.id !== id + && (marker.binding !== row.binding || marker.removed === true)), + next, + ]; + this.host._deviceInbox = { ...dialog, busy: row.key, anchor: row.key }; + try { + await this._saveConfigNow(); + this.host._regSignature = ''; + this.host._deviceInboxMemo = null; + this.host._maybeRebuildDevices(); + if (this.host._deviceInbox) this.host._deviceInbox = { ...this.host._deviceInbox, busy: undefined }; + this.host._showToast(this.host._t('device_inbox.saved' as any)); + } catch (error: any) { + if (this.host._serverCfg === cfg) cfg.markers = previous; + if (this.host._deviceInbox) this.host._deviceInbox = { ...this.host._deviceInbox, busy: undefined }; + this.host._showToast(this.host._t('toast.error', { err: this.host._errText(error) })); + } + } + +public _findInboxDevice(row: DeviceInboxRow): void { + if (!row.canFind) return; + const device = this._deviceForInboxRow(row); + if (!device) return; + this.host._deviceInbox = null; + if (device.space && device.space !== this.host._space) { + if (!this.host._commitSpace(device.space)) return; + this.host._restoreZoom(); + } + const focus = () => { + const current = this.host._devices.find((item) => item.id === device.id) || device; + const point = this.host._pos(current); + this.host._applyView(this.host._zoom, point.x, point.y); + this.host._selId = current.id; + this.host.requestUpdate(); + window.setTimeout(() => { + if (this.host._selId === current.id) { + this.host._selId = null; + this.host.requestUpdate(); + } + }, 1500); + }; + requestAnimationFrame(() => requestAnimationFrame(focus)); + } + +public _deviceInboxTabKey(event: KeyboardEvent): void { + if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return; + const dialog = this.host._deviceInbox; + if (!dialog) return; + const tabs: DeviceInboxCategory[] = ['on_plan', 'available', 'hidden', 'readd']; + const offset = event.key === 'ArrowRight' ? 1 : -1; + const index = (tabs.indexOf(dialog.tab) + offset + tabs.length) % tabs.length; + this.host._deviceInbox = { ...dialog, tab: tabs[index], limit: 100, onlyNew: false }; + event.preventDefault(); + } + +public _openMarkerDialog(d?: DevItem): void { + // A global-camera snapshot belongs to one explicit expansion only; never + // carry it across device-dialog sessions. + this.host._vacAllCamerasFor = null; + this.host._vacAllCameraCache = null; + if (d) this.host._ackNewDevice(d.id); + if (!this.host._norm) { + this.host._showToast(this.host._t('toast.marker_needs_server')); + return; + } + if (d) { + const marker = d.marker; + const hasIsLight = Object.prototype.hasOwnProperty.call(marker || {}, 'is_light'); + const hasLightEntity = Object.prototype.hasOwnProperty.call(marker || {}, 'light_entity'); + const hasGlowColor = Object.prototype.hasOwnProperty.call(marker || {}, 'glow_color'); + const hasValueBadge = Object.prototype.hasOwnProperty.call(marker || {}, 'value_badge'); + const hasTapAction = Object.prototype.hasOwnProperty.call(marker || {}, 'tap_action'); + const hasToggleEntity = Object.prototype.hasOwnProperty.call(marker || {}, 'toggle_entity'); + const glowOverride = normalizeGlowColorOverride(marker?.glow_color); + const currentBadge = this.host._devicePresentation(d, true).valueBadge; + const badgeCandidates = valueBadgeCandidates(this.host._planHass, d, this.host._devices); + const recommendedBadge = recommendedValueBadgeSource(this.host._planHass, d, badgeCandidates); + this.host._markerDialog = { + devId: d.id, + name: d.name, + binding: d.bindingKind === 'virtual' ? 'virtual' : d.bindingKind + ':' + d.bindingRef, + bindingMode: d.bindingKind === 'virtual' ? 'virtual' : 'ha', + bindingOpen: false, + // a marker bound to an ENTITY of a device only shows up with the box on + showEntities: d.bindingKind === 'entity' && !!this.host._fullRegistryHass.entities[d.bindingRef || '']?.device_id, + bindingFilter: '', + icon: d.marker?.icon || '', + autoIcon: d.icon || '', + display: normalizeDeviceDisplay(d.marker?.display), + rippleColor: safeStoredColor(d.marker?.ripple_color, ''), + rippleSize: Number(d.marker?.ripple_size) > 0 ? Number(d.marker!.ripple_size) : 1.5, + size: Number(d.marker?.size) > 0 ? Number(d.marker!.size) : 1, + angle: Number(d.marker?.angle) || 0, + tapAction: projectedTapAction(d.marker?.tap_action, d.primary?.split('.')[0]), + tapActionTouched: false, + originalHasTapAction: hasTapAction, + originalTapAction: d.marker?.tap_action, + tapHintAnnouncement: '', + toggleEntity: marker?.toggle_entity || '', + toggleEntityTouched: false, + originalHasToggleEntity: hasToggleEntity, + originalToggleEntity: marker?.toggle_entity, + tapTarget: d.marker?.tap_target || '', + tapConfirm: d.marker?.tap_confirm === true, + runFilter: '', + // Keep unknown, temporarily inactive and duplicate external targets + // byte-for-byte across Open → Save. Runtime uses the filtered + // effective projection; a legacy self-reference is not a light source. + controls: persistedExternalControls(d.marker?.binding, d.marker?.controls, d.entities), + controlsFilter: '', + lightRole: marker?.is_light === true ? 'always' : marker?.is_light === false ? 'never' : 'auto', + lightRoleTouched: false, + originalHasIsLight: hasIsLight, + originalIsLight: marker?.is_light, + lightEntity: marker?.light_entity || '', + lightEntityTouched: false, + originalHasLightEntity: hasLightEntity, + originalLightEntity: marker?.light_entity, + glowMode: glowOverride?.bri != null ? 'fixed' : glowOverride ? 'color' : 'auto', + glowColor: glowOverride?.c || this.host._fillColors.glow_light.c, + glowBrightness: Math.max(1, Math.round((glowOverride?.bri ?? 1) * 100)), + glowColorDrafted: !!glowOverride, + glowBrightnessDrafted: glowOverride?.bri != null, + glowTouched: false, + originalHasGlowColor: hasGlowColor && (!!glowOverride || marker?.glow_color === null), + originalGlowColor: glowOverride || (marker?.glow_color === null ? null : undefined), + valueBadgeEnabled: hasValueBadge ? marker?.value_badge?.enabled === true : !!currentBadge, + valueBadgeSource: marker?.value_badge?.source || currentBadge?.source || recommendedBadge, + valueBadgePosition: marker?.value_badge?.position || currentBadge?.position || 'right', + valueBadgeTouched: false, + originalHasValueBadge: hasValueBadge, + originalValueBadge: marker?.value_badge, + useClimateTemp: d.marker?.use_climate_temp === true, + glowRadius: Number(d.marker?.glow_radius_cm) > 0 + ? String(this.host._imperial + ? Math.round((Number(d.marker!.glow_radius_cm) / 30.48) * 10) / 10 + : Math.round(Number(d.marker!.glow_radius_cm)) / 100) + : '', + model: d.model || '', + link: d.link || '', + description: d.description || '', + pdfs: [...(d.pdfs || [])], + room: d.marker?.room_id + ? d.space + '#@' + d.marker.room_id + : d.space && d.area ? d.space + '#' + d.area : '', + hideFromPlan: d.marker?.hidden === true, + busy: false, + }; + } else { + this.host._markerDialog = { + name: '', binding: 'virtual', bindingMode: 'virtual', bindingOpen: false, + showEntities: false, bindingFilter: '', icon: '', autoIcon: '', + display: 'badge', rippleColor: '', rippleSize: 1.5, size: 1, angle: 0, + tapAction: 'info', tapActionTouched: false, + originalHasTapAction: false, originalTapAction: undefined, tapHintAnnouncement: '', + toggleEntity: '', toggleEntityTouched: false, + originalHasToggleEntity: false, originalToggleEntity: undefined, + tapTarget: '', tapConfirm: false, runFilter: '', + controls: [], controlsFilter: '', + lightRole: 'auto', lightRoleTouched: false, + originalHasIsLight: false, originalIsLight: undefined, + lightEntity: '', lightEntityTouched: false, + originalHasLightEntity: false, originalLightEntity: undefined, + glowMode: 'auto', glowColor: this.host._fillColors.glow_light.c, glowBrightness: 100, + glowColorDrafted: false, glowBrightnessDrafted: false, glowTouched: false, + originalHasGlowColor: false, originalGlowColor: undefined, + valueBadgeEnabled: false, valueBadgeSource: null, valueBadgePosition: 'right', + valueBadgeTouched: false, originalHasValueBadge: false, originalValueBadge: undefined, + useClimateTemp: false, glowRadius: '', model: '', + link: '', description: '', pdfs: [], room: '', hideFromPlan: false, busy: false, + uploadId: 'up_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6), + }; + } + } + +public _runCandidates(): { value: string; label: string; sub: string }[] { + const out: { value: string; label: string; sub: string }[] = []; + for (const dom of RUN_TARGET_DOMAINS) { + for (const [eid, st] of Object.entries(this.host.hass.states)) { + if (!eid.startsWith(dom + '.')) continue; + if (!this.host._planEntityAvailable(eid)) continue; + out.push({ + value: eid, + label: st?.attributes?.friendly_name || eid, + sub: this.host._t(('run.' + dom) as any), + }); + } + } + return out.sort((a, b) => a.sub.localeCompare(b.sub) || a.label.localeCompare(b.label)); + } + +public _bindingCandidates(): { value: string; label: string; sub: string }[] { + const list = bindingCandidates({ + hass: this.host._planHass, + devices: this.host._devices, + markers: this.host._markers, + showEntities: !!this.host._markerDialog?.showEntities, + currentBinding: this.host._markerDialog?.binding, + currentDeviceId: this.host._markerDialog?.devId, + labels: { + device: this.host._t('marker.sub_device'), + z2mGroup: this.host._t('marker.sub_z2m_group'), + group: this.host._t('marker.sub_group'), + helper: this.host._t('marker.sub_helper'), + entity: this.host._t('marker.sub_entity'), + }, + }); + const f = (this.host._markerDialog?.bindingFilter || '').toLowerCase().trim(); + const filtered = f + ? list.filter((o) => (o.label + ' ' + o.sub + ' ' + o.value).toLowerCase().includes(f)) + : list; + filtered.sort((a, b) => a.label.localeCompare(b.label)); + return filtered.slice(0, 200); + } + +public _keepClosedAsPartitions = (): void => { + if (this.host._wallFaceBatch) { + this._decideWallFace(false); + return; + } + if (!this.host._contourClosed || this.host._pendingSplit || !this.host._curSpaceCfg) return; + const sp = this.host._curSpaceCfg as any; + const verts = this.host._path.slice(0, -1); + if ((sp.partitions || []).length + verts.length > MAX_PARTITIONS) { + this.host._showToast(this.host._t('toast.physical_limit')); + return; + } + const before = this._geometrySnapshot(); + // Замкнутый контур: отрезков столько же, сколько вершин, и последний — + // закрывающий. Его известное значение подаётся резолверу как запись, всё + // остальное решает единое правило (#234). + const cms = chainSegmentCms( + verts.length, + [...this.host._draftSegmentCms, this.host._closingWallCm ?? undefined], + this.host._drawWallCm, DRAW_WALL_DEFAULT_CM, + ); + sp.partitions ||= []; + const seed = Date.now().toString(36); + const activeDraft = this.host._activeDraftId + ? (sp.room_drafts || []).find((draft) => draft.id === this.host._activeDraftId) + : null; + const lineage = this._draftSegmentsForPath(this.host._path, activeDraft, cms); + for (let i = 0; i < verts.length; i++) { + const a = verts[i], b = verts[(i + 1) % verts.length]; + sp.partitions.push({ + id: lineage[i]?.id || `partition-${seed}-${i}`, + a: [a[0] / NORM_W, a[1] / NORM_W], + b: [b[0] / NORM_W, b[1] / NORM_W], + cm: cms[i], + }); + } + if (this.host._activeDraftId && Array.isArray(sp.room_drafts)) { + sp.room_drafts = sp.room_drafts.filter((d: any) => d.id !== this.host._activeDraftId); + if (!sp.room_drafts.length) delete sp.room_drafts; + } + this._commitPhysicalGeometry(this.host._t('history.contour_to_partitions'), before); + this.host._roomDialog = false; + this.host._path = []; + delete this.host._resumeDraftBySpace[this.host._space]; + this.host._activeDraftId = null; + this.host._draftSegmentCms = []; + this.host._closingWallCm = null; + }; + +public _backupErrorText(e: any): string { + const code = e?.code ?? e?.error; + if (typeof code === 'string') { + const key = `backup.error.${code}`; + const translated = this.host._t(key as I18nKey); + if (translated !== key) return translated; + } + return this.host._errText(e); + } + +public async _pickMarkerFiles(ev: Event): Promise { + const input = ev.target as HTMLInputElement; + const files = input.files ? [...input.files] : []; + input.value = ''; + if (!files.length || !this.host._markerDialog) return; + const mid = this.host._markerDialog.uploadId || this.host._markerDialog.devId || 'new'; + const uploaded: PdfRef[] = []; + for (const file of files) { + try { + const fd = new FormData(); + fd.append('marker_id', mid); + fd.append('file', file, file.name); + // fetchWithAuth refreshes a stale access_token itself; the fallback is the raw token + const resp: Response = this.host.hass?.fetchWithAuth + ? await this.host.hass.fetchWithAuth('/api/houseplan/upload', { method: 'POST', body: fd }) + : await fetch('/api/houseplan/upload', { + method: 'POST', + body: fd, + headers: this.host.hass?.auth?.data?.access_token + ? { authorization: `Bearer ${this.host.hass.auth.data.access_token}` } + : {}, + }); + const json = await resp.json().catch(() => ({})); + if (!resp.ok || json.error) { + const map: Record = { + too_large: this.host._t('err.too_large', { mb: json.max_mb || 50 }), + bad_ext: this.host._t('err.bad_ext'), + unauthorized: this.host._t('err.unauthorized'), + }; + throw new Error(map[json.error] || json.error || 'HTTP ' + resp.status); + } + uploaded.push({ name: json.name || file.name, url: json.url }); + } catch (e: any) { + this.host._showToast(this.host._t('toast.file_failed', { name: file.name, err: this.host._errText(e) })); + } + } + // the dialog might have closed during the upload — add only if it is still open + if (uploaded.length && this.host._markerDialog) { + this.host._markerDialog = { ...this.host._markerDialog, pdfs: [...this.host._markerDialog.pdfs, ...uploaded] }; + this.host._showToast(this.host._t('toast.files_attached', { n: uploaded.length })); + } + } + +public _removeMarkerPdf(url: string): void { + if (!this.host._markerDialog) return; + this.host._markerDialog = { + ...this.host._markerDialog, + pdfs: this.host._markerDialog.pdfs.filter((p) => p.url !== url), + }; + } + +public _markerLightFields(d: NonNullable): Partial { + const fields: Partial = {}; + if (!d.lightRoleTouched) { + if (d.originalHasIsLight) fields.is_light = d.originalIsLight ?? null; + } else if (d.lightRole === 'always') fields.is_light = true; + else if (d.lightRole === 'never') fields.is_light = false; + + if (!d.lightEntityTouched) { + if (d.originalHasLightEntity) fields.light_entity = d.originalLightEntity ?? null; + } else if (d.lightEntity) { + fields.light_entity = d.lightEntity; + } + + if (!d.glowTouched) { + if (d.originalHasGlowColor) fields.glow_color = d.originalGlowColor ?? null; + } else if (d.glowMode !== 'auto') { + const glow: NonNullable = { + c: safeStoredColor(d.glowColor, this.host._fillColors.glow_light.c), + }; + if (d.glowMode === 'fixed') { + glow.bri = Math.max(0.01, Math.min(1, Math.round(d.glowBrightness) / 100)); + } + fields.glow_color = glow; + } + return fields; + } + +public _markerTapActionFields( + d: NonNullable, + ): Pick | Record { + if (!d.tapActionTouched) { + return d.originalHasTapAction ? { tap_action: d.originalTapAction ?? null } : {}; + } + return { tap_action: d.tapAction || null }; + } + +public _markerToggleEntityFields( + d: NonNullable, + ): Pick | Record { + return toggleEntityWriteFields({ + touched: d.toggleEntityTouched, + originalHas: d.originalHasToggleEntity, + original: d.originalToggleEntity, + value: d.toggleEntity, + }); + } + +public async _saveMarker(): Promise { + const dlg = this.host._markerDialog; + if (!dlg || dlg.busy) return; + const effectiveTapAction = this._effectiveMarkerTapAction(dlg); + if (dlg.bindingMode === 'ha' && (!dlg.binding || dlg.binding === 'virtual')) return; + if (dlg.binding === 'virtual' && !dlg.name.trim()) { + this.host._showToast(this.host._t('toast.virtual_name_required')); + return; + } + if (effectiveTapAction === 'run' && !dlg.tapTarget) { + this.host._showToast(this.host._t('toast.run_target_required')); + return; + } + if (dlg.valueBadgeTouched && dlg.valueBadgeEnabled && !dlg.valueBadgeSource) { + this.host._showToast(this.host._t('toast.value_badge_source_required')); + return; + } + if (dlg.bindingMode === 'ha') { + const status = this.host._bindingStatus(dlg.binding); + const previous = dlg.devId + ? this.host._markers.find((marker) => marker.id === dlg.devId) : null; + const replacingWithInactive = !previous || previous.binding !== dlg.binding; + if (status.kind !== 'active' && replacingWithInactive) { + this.host._showToast(this.host._t(status.kind === 'ha_disabled' + ? 'toast.ha_disabled_add' : 'toast.ha_binding_unverified')); + return; + } + // Saving metadata for an already-disabled binding is allowed, but a + // blocked Show attempt may never erase an older explicit user hidden. + if (status.kind === 'ha_disabled' && previous?.hidden === true && !dlg.hideFromPlan) { + this.host._markerDialog = { ...dlg, hideFromPlan: true }; + this.host._showToast(this.host._t(status.reason === 'entity' + ? 'toast.ha_disabled_show_entity' : 'toast.ha_disabled_show_device')); + return; + } + } + const cfg = this.host._serverCfg; + if (!cfg) return; + const markers = cfg.markers || []; + const roomRef = parseRoomRef(dlg.room); + let space: string | null = roomRef?.space || null; + const area: string | null = roomRef?.area || null; + const roomId: string | null = roomRef?.roomId || null; + const id = markerIdForBinding(dlg.binding, dlg.devId, () => 'v_' + Date.now().toString(36)); + const oldId = dlg.devId; + const prevDev = oldId ? this.host._devices.find((x) => x.id === oldId) : null; + const explicitSpaceId = roomRef?.space || prevDev?.space || null; + const targetSpaceModel = explicitSpaceId + ? this.host._spaceModelById(explicitSpaceId) + : this.host._spaceModel(); + if (!targetSpaceModel) return; + const targetSpaceId = targetSpaceModel.id; + if (dlg.binding === 'virtual' && !space) space = targetSpaceId; + this.host._markerDialog = { ...dlg, busy: true }; + try { + const replacedRemovedIds = dlg.binding === 'virtual' + ? [] + : markers + .filter((m) => m.removed && m.binding === dlg.binding) + .map((m) => m.id); + const replacingRemoved = replacedRemovedIds.length > 0; + const controls = persistedExternalControls( + dlg.binding, dlg.controls, this.host._bindingEntities(dlg.binding), + ); + // the vacuum block is edited live outside the dialog transaction — + // the rebuild below must carry it over, not erase it + const prevVac = markers.find((m0: Marker) => m0.id === id || m0.id === oldId)?.vacuum || null; + const marker: Marker = { + id, + vacuum: prevVac, + binding: dlg.binding, + name: dlg.name.trim() || null, + icon: dlg.icon || null, + display: dlg.display !== 'badge' ? dlg.display : null, + ripple_color: dlg.display === 'icon_ripple' && dlg.rippleColor ? dlg.rippleColor : null, + ripple_size: dlg.display === 'icon_ripple' && dlg.rippleSize !== 1.5 ? dlg.rippleSize : null, + size: dlg.size !== 1 ? dlg.size : null, + angle: dlg.angle ? dlg.angle : null, + ...this._markerTapActionFields(dlg), + ...this._markerToggleEntityFields(dlg), + tap_target: effectiveTapAction === 'run' ? dlg.tapTarget || null : null, + tap_confirm: dlg.tapConfirm ? true : null, + controls: controls.length ? controls : null, + // pdfs may be rewritten below when rebinding changes the marker id + ...this._markerLightFields(dlg), + ...this._markerValueBadgeFields(dlg), + use_climate_temp: dlg.useClimateTemp ? true : null, + glow_radius_cm: (() => { + const v = strictNumber(dlg.glowRadius); + if (v == null || v <= 0) return null; + return Math.round(this.host._imperial ? v * 30.48 : v * 100); + })(), + model: dlg.model.trim() || null, + link: dlg.link.trim() || null, + description: dlg.description.trim() || null, + pdfs: dlg.pdfs, + // Explicit false, not absence: a marker of any kind is what tells the + // seeder "the user decided" — unticking must not invite a re-hide + // (docs/FILTERING.md). + hidden: dlg.hideFromPlan ? true : false, + }; + // save the room choice (always for virtual ones; for bound ones — if chosen) + if (dlg.binding === 'virtual' || dlg.room) { + marker.space = space; + marker.area = area; + marker.room_id = roomId; + } + // the room changed → move the icon to its center + const prevRoomId = prevDev?.marker?.room_id ?? null; + const roomChanged = !!dlg.room && prevDev != null + && (prevDev.space !== space || prevDev.area !== area || prevRoomId !== roomId); + // Rebinding changes the marker id, so the uploaded files must follow. + // Order matters (review CR-2): COPY first, save the config, and only then + // delete the old folder. If the save is rejected, the old urls in the + // stored config still resolve — the files never left. A failed copy + // leaves the urls untouched and tells the user (review CR-3). + let cleanupOldFiles = false; + // a new icon uploaded into its own staging folder; an edited one into its + // own id. Either way the files move to the final id here. + const fileSrc = dlg.uploadId || oldId; + if (fileSrc && fileSrc !== id && marker.pdfs?.length) { + try { + const res: any = await this.host.hass.callWS({ + type: 'houseplan/files/migrate', from_id: fileSrc, to_id: id, + }); + const mapping = res?.mapping || {}; + marker.pdfs = migratePdfUrls(marker.pdfs, fileSrc, id, mapping); + cleanupOldFiles = Object.keys(mapping).length > 0; + } catch (e: any) { + this.host._showToast(this.host._t('toast.files_migrate_failed', { err: this.host._errText(e) })); + } + } + // Rebinding changes source identity. Rewrite every marker:* edge in the + // same config transaction before replacing the marker itself. + cfg.markers = markers; + if (oldId && oldId !== id) cfg.markers = rewriteMarkerControlReferences(cfg.markers, oldId, id); + if (oldId && oldId !== id && marker.value_badge?.source?.kind === 'derived_marker_state' + && marker.value_badge.source.ref === `marker:${oldId}`) { + marker.value_badge.source = { kind: 'derived_marker_state', ref: `marker:${id}` }; + } + // remove the previous marker (by the old id and by the new id) + cfg.markers = cfg.markers.filter( + (m) => m.id !== id && m.id !== oldId + && (marker.binding === 'virtual' || m.binding !== marker.binding), + ); + cfg.markers.push(marker); + // Position rule (owner's decision, v1.33.4): editing an existing icon — + // rebinding it to another HA device/entity or to another room — must NOT + // move it. Its current position (saved or the ephemeral auto one) is + // migrated to the new marker id. Only two cases still center the icon: + // a truly NEW icon, and a move to a room in a DIFFERENT space (keeping + // the old coordinates there would be meaningless). + // Write POINT-WISE (layout/update), not the whole layout — a full layout/set + // overwrites positions changed in other windows (the v1.4.4 incident). + let newPos: { s: string; x: number; y: number } | null = null; + const prevRec = oldId ? this.host._layout[oldId] : null; + const prevPos = prevRec + ? { s: prevRec.s || prevDev?.space || this.host._space, x: prevRec.x, y: prevRec.y } + : oldId && prevDev && this.host._defPos[oldId] + ? this.host._normPos(prevDev.space, this.host._defPos[oldId].x, this.host._defPos[oldId].y) + : null; + if (!replacingRemoved && prevPos && prevPos.s === targetSpaceId) { + // stays in place; pin it under the (possibly new) id + if (id !== oldId || !this.host._layout[id] || roomChanged) { + newPos = { s: prevPos.s, x: prevPos.x, y: prevPos.y }; + this.host._layout = { ...this.host._layout, [id]: newPos }; + } + } else if (replacingRemoved || !this.host._layout[id] || roomChanged) { + let cx = targetSpaceModel.vb[0] + targetSpaceModel.vb[2] / 2; + let cy = targetSpaceModel.vb[1] + targetSpaceModel.vb[3] / 2; + const room = roomId + ? targetSpaceModel.rooms.find((r) => r.id === roomId) + : area + ? targetSpaceModel.rooms.find((r) => r.area === area) + : undefined; + if (room) [cx, cy] = this.host._roomCenter(room); + newPos = this.host._normPos(targetSpaceId, cx, cy); + this.host._layout = { ...this.host._layout, [id]: newPos }; + } + await this._saveConfigNow(); + if (newPos) { + const pos = canonicalizePosition(newPos); + this.host._layout = { ...this.host._layout, [id]: pos }; + this.host._noteLayoutRev(await this.host.hass.callWS({ + type: 'houseplan/layout/update', device_id: id, pos, + })); + } + const obsoleteIds = new Set(replacedRemovedIds); + if (oldId && oldId !== id) obsoleteIds.add(oldId); + obsoleteIds.delete(id); + for (const obsoleteId of obsoleteIds) { + // Rebinding or replacing a legacy tombstone changed the icon id. + delete this.host._layout[obsoleteId]; + await this.host.hass.callWS({ type: 'houseplan/layout/delete', device_id: obsoleteId }) + .then((r: any) => this.host._noteLayoutRev(r)).catch(() => undefined); + } + // the config is committed — now it is safe to drop the old folder + if (cleanupOldFiles && fileSrc) { + await this.host.hass + .callWS({ type: 'houseplan/files/cleanup', marker_id: fileSrc }) + .catch(() => undefined); // leftovers are harmless; broken links are not + } + this._closeMarkerDialog(); + this.host._regSignature = ''; + this.host._maybeRebuildDevices(); + this.host._showToast(this.host._t('toast.marker_saved')); + } catch (e: any) { + // audit L3: the dialog may have been closed (Esc) while the save was + // in flight — spreading null yields a truthy husk and the renderer + // then crashes, blanking the whole card. The toast below is the + // only remaining signal, so it must still fire. + if (this.host._markerDialog) this.host._markerDialog = { ...this.host._markerDialog, busy: false }; + this.host._showToast(this.host._t('toast.error', { err: this.host._errText(e) })); + } + } + +public async _deleteMarker(): Promise { + const dlg = this.host._markerDialog; + if (!dlg || dlg.busy || !dlg.devId) return; + const d = dlg.devId ? this.host._devices.find((x) => x.id === dlg.devId) : null; + const persisted = this.host._markers.find((marker) => marker.id === dlg.devId); + if (!d && !persisted) return; + const label = dlg.name || this.host._t('device.fallback'); + if (!confirm(this.host._t('confirm.remove_marker', { name: label }))) return; + const cfg = this.host._serverCfg!; + cfg.markers = cfg.markers || []; + const previousMarkers = cfg.markers; + const targetId = d?.id || persisted!.id; + const binding = d + ? d.bindingKind === 'virtual' ? 'virtual' + : d.bindingKind && d.bindingRef ? `${d.bindingKind}:${d.bindingRef}` : '' + : persisted!.binding; + if (!binding) return; + const deletion = deletePlanMarkerRecords( + cfg.markers, targetId, binding, binding === 'virtual', + ); + cfg.markers = removeMarkerControlReferences(deletion.markers, deletion.cleanupIds); + const cleanupIds = deletion.cleanupIds; + this.host._markerDialog = { ...dlg, busy: true }; + try { + await this._saveConfigNow(); + // Housekeeping follows the durable config write. Every call is + // idempotent and best-effort: the device is already deleted even if an + // old integration cannot yet serve one of the cleanup commands. + for (const id of cleanupIds) { + delete this.host._layout[id]; + delete this.host._defPos[id]; + this.host._dirtyPos.delete(id); + this.host._sentPos.delete(id); + const activity = this.host._activityRt.get(id); + if (activity) clearTimeout(activity.timer); + this.host._activityRt.delete(id); + this.host._vacRt.delete(id); + delete this.host._vacSrvTrails[id]; + await this.host.hass.callWS({ type: 'houseplan/layout/delete', device_id: id }) + .then((r: any) => this.host._noteLayoutRev(r)).catch(() => undefined); + await this.host.hass.callWS({ type: 'houseplan/files/cleanup', marker_id: id }) + .catch(() => undefined); + await this.host.hass.callWS({ type: 'houseplan/trail/delete', marker_id: id }) + .catch(() => undefined); + } + if (this.host._deviceInboxReturn) { + this.host._deviceInboxReturn = { ...this.host._deviceInboxReturn, tab: 'readd', anchor: binding }; + } + this._closeMarkerDialog(); + if (this.host._infoCard?.id === targetId) this.host._closeInfoCard(); + if (this.host._selId === targetId) this.host._selId = null; + if (this.host._drag?.id === targetId) this.host._drag = null; + this.host._regSignature = ''; + this.host._maybeRebuildDevices(); + this.host._showToast(this.host._t('toast.marker_removed')); + } catch (e: any) { + if (this.host._serverCfg === cfg) cfg.markers = previousMarkers; + if (this.host._markerDialog) this.host._markerDialog = { ...this.host._markerDialog, busy: false }; + this.host._showToast(this.host._t('toast.error', { err: this.host._errText(e) })); + } + } + +public _openSpaceDialog(mode: 'edit' | 'create', spaceId?: string): void { + if (!this.host._serverStorage || !this.host._serverCfg) { + this.host._showToast(this.host._t('toast.integration_missing')); + return; + } + if (mode === 'edit') { + const sp = this.host._serverCfg!.spaces.find((x: any) => x.id === spaceId); + if (!sp) return; + const disp = spaceDisplayOf(sp); + const storedCustom = sp.settings?.custom_fill && typeof sp.settings.custom_fill === 'object' + ? customFillOf(sp.settings.custom_fill) : null; + // Space-level None is no longer a separate UI choice. Project it to the + // same custom colour at zero opacity, so merely opening/saving settings + // cannot change either the visible floor or the Glow darkness. + const dialogCustom = disp.fill === 'none' + ? { ...(storedCustom || DEFAULT_CUSTOM_FILL), a: 0 } + : storedCustom; + this.host._spaceDialog = { + mode, spaceId, title: sp.title, planUrl: sp.plan_url || null, planFile: null, + source: sp.plan_url ? 'file' : 'draw', + showBorders: disp.showBorders, showNames: disp.showNames, + zeroWallStyle: zeroWallStyleOf(sp), + displayTouched: true, + hideDecor: disp.hideDecor, hideOpenings: disp.hideOpenings, + roomColor: disp.color, roomOpacity: disp.opacity, + // `none` is a legacy space token. The current editor represents its + // exact appearance as a transparent user colour. + fillMode: disp.fill === 'none' ? 'custom' : disp.fill, + customFill: dialogCustom, + glowEnabled: disp.glow, + bgColor: disp.bgColor, + bgMode: sp.settings?.bg_mode === 'static' || sp.settings?.bg_mode === 'daynight' ? sp.settings.bg_mode : null, + northDeg: northDegOf({}, sp.settings), + sunRays: typeof sp.settings?.sun_rays === 'boolean' ? sp.settings.sun_rays : null, + tempMin: disp.tempMin, tempMax: disp.tempMax, + showLqi: disp.showLqi ?? this.host._config?.show_signal ?? true, + cardFontScale: disp.cardFontScale, + labelTemp: disp.labelTemp, labelHum: disp.labelHum, + labelLqi: disp.labelLqi, labelLight: disp.labelLight, + cellCm: Number(sp.cell_cm) > 0 ? Number(sp.cell_cm) : 5, + cellCmInput: gridCellFieldValue( + Number(sp.cell_cm) > 0 ? Number(sp.cell_cm) : 5, this.host._imperial, + ), + cellCmTouched: false, + busy: false, + }; + } else { + this.host._spaceDialog = { + mode, title: '', planUrl: null, planFile: null, + ...initialSpaceDisplayDraft(), + hideDecor: false, hideOpenings: false, zeroWallStyle: 'dashed', + roomColor: DEFAULT_ROOM_COLOR, roomOpacity: DEFAULT_ROOM_OPACITY, fillMode: 'custom', + // `custom` replaces the old `none` choice in the editor, but creating + // a space must preserve the old no-floor visual by default. The user + // can make this colour visible explicitly with the opacity control. + customFill: { ...DEFAULT_CUSTOM_FILL, a: 0 }, + glowEnabled: true, + bgColor: null, + bgMode: 'daynight', northDeg: null, sunRays: null, + tempMin: DEFAULT_TEMP_MIN, tempMax: DEFAULT_TEMP_MAX, + showLqi: this.host._config?.show_signal ?? true, + cardFontScale: 1, + labelTemp: false, labelHum: false, labelLqi: false, labelLight: false, + cellCm: newSpaceCellCm(this.host._imperial), + cellCmInput: gridCellFieldValue(newSpaceCellCm(this.host._imperial), this.host._imperial), + cellCmTouched: false, + busy: false, + }; + } + } + +public async _pickPlanFile(ev: Event): Promise { + const input = ev.target as HTMLInputElement; + const file = input.files?.[0]; + if (!file || !this.host._spaceDialog) return; + const extMap: Record = { + 'image/svg+xml': 'svg', 'image/png': 'png', 'image/jpeg': 'jpg', 'image/webp': 'webp', + }; + const ext = extMap[file.type] || (file.name.toLowerCase().endsWith('.svg') ? 'svg' : ''); + if (!ext) { + this.host._showToast(this.host._t('toast.plan_formats')); + return; + } + const buf = new Uint8Array(await file.arrayBuffer()); + let bin = ''; + for (let i = 0; i < buf.length; i += 32768) bin += String.fromCharCode(...buf.subarray(i, i + 32768)); + const b64 = btoa(bin); + // aspect ratio: render into an Image + const url = URL.createObjectURL(file); + const aspect = await new Promise((res) => { + const img = new Image(); + img.onload = () => res(img.naturalWidth && img.naturalHeight ? img.naturalWidth / img.naturalHeight : 1.414); + img.onerror = () => res(1.414); + img.src = url; + }); + URL.revokeObjectURL(url); + this.host._spaceDialog = { ...this.host._spaceDialog, planFile: { ext, b64, aspect, name: file.name } }; + } + +public _toggleServerPlans = async (): Promise => { + const d = this.host._spaceDialog; + if (!d) return; + if (d.pickSaved) { + this.host._spaceDialog = { ...d, pickSaved: false }; + return; + } + this.host._spaceDialog = { ...d, pickSaved: true, savedBusy: true }; + try { + const r: any = await this.host.hass.callWS({ type: 'houseplan/plans/list' }); + const cur = this.host._spaceDialog; + if (cur) this.host._spaceDialog = { ...cur, saved: r?.plans || [], savedBusy: false }; + } catch (e: any) { + const cur = this.host._spaceDialog; + if (cur) this.host._spaceDialog = { ...cur, saved: [], savedBusy: false }; + this.host._showToast(this.host._t('toast.plans_list_failed', { err: this.host._errText(e) })); + } + }; + +public _useServerPlan(url: string): void { + const d = this.host._spaceDialog; + if (!d) return; + // Attach immediately — the click should not wait for anything. The OLD + // file's proportions go right away: they describe the previous image, and + // a Save racing the read must get "unknown", never "the wrong shape". + this.host._spaceDialog = { ...d, planUrl: url, planFile: null, pickSaved: false, savedAspect: undefined }; + this.host._aspectJob = this._readPlanAspect(url); + } + +public async _readPlanAspect(url: string): Promise { + for (let i = 0; i < 40; i++) { // ~6 s, then give up quietly + const src = this.host._display(url); + if (src) { + const ratio = await new Promise((res) => { + const img = new Image(); + img.onload = () => res(img.naturalWidth && img.naturalHeight + ? img.naturalWidth / img.naturalHeight : 0); + img.onerror = () => res(0); + img.src = src; + }); + const cur = this.host._spaceDialog; + if (cur && cur.planUrl === url && Number.isFinite(ratio) && ratio > 0) { + this.host._spaceDialog = { ...cur, savedAspect: ratio }; + return ratio; + } + return 0; + } + await new Promise((r) => setTimeout(r, 150)); + if (this.host._spaceDialog?.planUrl !== url) return 0; // the user moved on + } + return 0; + } + +public async _deleteServerPlan(name: string): Promise { + if (!confirm(this.host._t('confirm.delete_plan', { name }))) return; + try { + await this.host.hass.callWS({ type: 'houseplan/plans/delete', name }); + const d = this.host._spaceDialog; + if (d?.saved) this.host._spaceDialog = { ...d, saved: d.saved.filter((p) => p.name !== name) }; + } catch (e: any) { + this.host._showToast(this.host._t('toast.plan_delete_failed', { err: this.host._errText(e) })); + } + } + +public _renderServerPlans(d: NonNullable): TemplateResult { + if (d.savedBusy) return html`
${this.host._t('space.loading')}
`; + const list = d.saved || []; + if (!list.length) return html`
${this.host._t('space.no_saved')}
`; + const kb = (n: number) => (n >= 1048576 ? (n / 1048576).toFixed(1) + ' MB' : Math.round(n / 1024) + ' KB'); + return html`
+ ${list.map((p) => html` +
+ +
+ ${p.name} + ${kb(p.size)}${p.used_by.length + ? ' · ' + this.host._t('space.used_by', { list: p.used_by.join(', ') }) + : ''} +
+ + +
`)} +
`; + } + +public async _saveSpaceDialog(): Promise { + const d = this.host._spaceDialog; + if (!d || d.busy || !d.title.trim()) return; + if (d.source === 'file' && !d.planFile && !d.planUrl) { + this.host._showToast(this.host._t('toast.plan_required')); + return; + } + const wasFirst = d.mode === 'create' && (this.host._serverCfg?.spaces.length || 0) === 0; + this.host._spaceDialog = { ...d, busy: true }; + try { + const spaceId = d.mode === 'create' ? 's' + Date.now().toString(36) : d.spaceId!; + + /* Upload BEFORE touching the config, and never hold a reference to a + config object across an await. `houseplan/config/get` runs on every + `houseplan_config_updated` event and REPLACES `_serverCfg`; a space + object captured before the upload is then detached, so plan_url, + aspect and settings were written into an orphan and the save shipped + the untouched config. Symptom: the file lands on disk, the plan never + appears, and re-saving does not help (owner's install, 2026-07-27). */ + let uploaded: { url: string; aspect: number } | null = null; + if (d.source === 'file' && d.planFile) { + const resp = await this.host.hass.callWS({ + type: 'houseplan/plan/set', space_id: spaceId, ext: d.planFile.ext, data: d.planFile.b64, + }); + uploaded = { url: resp.url, aspect: d.planFile.aspect }; + } + + // A plan picked from the server list reads its proportions from the + // image, and Save used to outrun that read: the snapshot still carried + // the PREVIOUS file's ratio and a wide plan came out at the old shape + // for good (HP-1490-04). Wait for the read — it is bounded (~6 s) and + // the dialog is already busy. Unknown stays unknown, never the old + // value: a square fallback is honest, an inherited ratio is not. + let pickedAspect: number | null = d.savedAspect || null; + if (!uploaded && d.source === 'file' && d.planUrl && !pickedAspect && this.host._aspectJob) { + pickedAspect = (await this.host._aspectJob) || null; + } + + // from here on: no awaits until the save, so `sp` cannot be orphaned + const cfg = this.host._serverCfg!; + let sp: any; + if (d.mode === 'create') { + sp = createEmptySpaceConfig(spaceId, d.title.trim()); + cfg.spaces.push(sp); + } else { + sp = cfg.spaces.find((x: any) => x.id === spaceId); + if (!sp) throw new Error('space ' + spaceId + ' is gone from the config'); + sp.title = d.title.trim(); + } + if (uploaded) { + sp.plan_url = uploaded.url; + // the image's own proportions, so it can be centred before it loads + sp.plan_aspect = uploaded.aspect; + } else if (d.source === 'file' && d.planUrl && d.planUrl !== sp.plan_url) { + // picked from the server list: no upload, just a reference — and the + // previous image's proportions never survive the switch + sp.plan_url = d.planUrl; + sp.plan_aspect = pickedAspect; + } + // switching an existing space to "draw" detaches its background image + // (the uploaded file stays on disk; only the reference is cleared). + // Its transform goes with it — there is nothing left for plan_x/plan_y/ + // plan_scale to describe, and a stale one would silently apply to the + // NEXT picture uploaded here (docs/BACKDROP.md §1). + if (d.source === 'draw') { + sp.plan_url = null; sp.plan_aspect = null; + delete sp.plan_x; delete sp.plan_y; delete sp.plan_scale; + delete sp.plan_scale_x; delete sp.plan_scale_y; delete sp.plan_angle; + } + // Persist exactly what the dialog showed. Source defaults are projected + // visibly while editing the create state, never hidden here at Save. + sp.settings = { + ...(sp.settings || {}), + show_borders: d.showBorders, + show_names: d.showNames, + // written only when ON: a plan that never hid anything stores nothing + hide_decor: d.hideDecor || undefined, + hide_openings: d.hideOpenings || undefined, + room_color: d.roomColor, + room_opacity: d.roomOpacity, + bg_color: d.bgColor || undefined, // empty = inherit the general setting + bg_mode: d.bgMode || undefined, // sun on the plan (docs/SUN.md); empty = inherit + north_deg: d.northDeg ?? undefined, + sun_rays: d.sunRays ?? undefined, + fill_mode: d.fillMode, + custom_fill: d.customFill || undefined, + // Always materialize the independent value. In particular, editing a + // legacy fill_mode:'glow' space preserves its appearance atomically + // while replacing the old token with the selected data fill. + glow_enabled: d.glowEnabled, + temp_min: Number.isFinite(d.tempMin) ? Math.min(d.tempMin, d.tempMax) : DEFAULT_TEMP_MIN, + temp_max: Number.isFinite(d.tempMax) ? Math.max(d.tempMin, d.tempMax) : DEFAULT_TEMP_MAX, + show_lqi: d.showLqi, + card_font_scale: d.cardFontScale !== 1 ? d.cardFontScale : undefined, + label_temp: d.labelTemp, + label_hum: d.labelHum, + label_lqi: d.labelLqi, + label_light: d.labelLight, + }; + // This is a geometry policy of the space, not a visual room setting. + sp.zero_wall_style = d.zeroWallStyle; + sp.cell_cm = Number.isFinite(d.cellCm) && d.cellCm > 0 + ? Math.max(CELL_CM_MIN, Math.min(CELL_CM_MAX, d.cellCm)) : 5; + // Nothing to clean up from here: the backend collects the superseded + // file inside the same locked transaction that accepted this config + // (review R3-1). A cleanup driven from the client could not be ordered + // against another client's commit and deleted its freshly saved plan. + await this._saveConfigNow(); + this.host._spaceDialog = null; + if (d.mode === 'create') this.host._commitSpace(sp.id); + this.host._regSignature = ''; + this.host._maybeRebuildDevices(); + if (this.host._importQueue.length) { + // floors-import wizard: proceed to the next floor + this._openNextImport(); + } else if (wasFirst || this.host._importTotal > 0) { + // guide the user onward: straight into room markup mode + this.host._importTotal = 0; + this.host._commitSpace(this.host._serverCfg!.spaces[0]?.id || this.host._space); + this._setMode('plan'); + this.host._tool = 'draw'; + this.host._path = []; + this.host._cursorPt = null; + this._primeDrawWallField(); + this.host._showToast(this.host._t(wasFirst && !this.host._importTotal ? 'toast.space_added_onboard' : 'import.done')); + } else { + this.host._showToast(d.mode === 'create' ? this.host._t('toast.space_added') : this.host._t('toast.space_saved')); + // A freshly created space is empty — View has nothing to show. Open + // the plan editor (draw tool) so the user can mark rooms immediately, + // same as the first-space onboard path above. + if (d.mode === 'create') { + if (this.host._mode !== 'plan') this._setMode('plan'); + else { + this.host._tool = 'draw'; + this.host._path = []; + this.host._cursorPt = null; + this._primeDrawWallField(); + this.host._saveNav(); + } + } + } + } catch (e: any) { + // The dialog edits the reactive config optimistically before config/set. + // A rejected create used to leave that unsaved space on screen; the next + // delete then flushed the same malformed write and repeated its error. + // Re-adopt server truth for every failed space transaction. Conflict is + // already reloaded by _saveConfigNow; do not issue the same read twice. + // A lost response is safe too: if the server did commit, config/get + // returns it. + if (e?.code !== 'conflict') await this.host._reloadConfigOnly(true); + // audit L3: the dialog may have been closed (Esc) while the save was + // in flight — spreading null yields a truthy husk and the renderer + // then crashes, blanking the whole card. The toast below is the + // only remaining signal, so it must still fire. + if (this.host._spaceDialog) this.host._spaceDialog = { ...this.host._spaceDialog, busy: false }; + this.host._showToast(this.host._t('toast.error', { err: this.host._errText(e) })); + } + } + +public async _deleteSpace(): Promise { + const d = this.host._spaceDialog; + if (!d || d.mode !== 'edit') return; + const serverCfg = this.host._serverCfg; + if (!serverCfg) return; + const sp = serverCfg.spaces.find((x: any) => x.id === d.spaceId); + const dependencies = collectSpaceMarkerDependencies( + serverCfg, this.host._layout || {}, d.spaceId || '', + ); + const deletingLastSpace = serverCfg.spaces.length === 1 + && serverCfg.spaces[0]?.id === d.spaceId; + if (dependencies.count && !deletingLastSpace) { + this.host._spaceDialog = { ...d, deleteBlockers: dependencies.count }; + return; + } + if (!confirm(this.host._t('confirm.delete_space', { title: sp.title }))) return; + this.host._spaceDialog = { ...d, deleteBlockers: 0, busy: true }; + try { + if (this.host._saveConfigDebounced.pending()) this.host._saveConfigDebounced.flush(); + if (this.host._persistLayout.pending()) this.host._persistLayout.flush(); + await this.host._writeChain; + const response: any = await this.host.hass.callWS({ + type: 'houseplan/space/delete', + space_id: d.spaceId, + expected_config_rev: this.host._cfgRev, + expected_layout_rev: this.host._layoutRev, + }); + const [configResponse, layoutResponse] = await Promise.all([ + this.host.hass.callWS({ type: 'houseplan/config/get' }), + this.host.hass.callWS({ type: 'houseplan/layout/get' }), + ]); + this.host._adoptStructuralResponses(configResponse, layoutResponse); + this.host._cfgRev = response?.config_rev ?? this.host._cfgRev; + this.host._layoutRev = response?.layout_rev ?? this.host._layoutRev; + this.host._spaceDialog = null; + if (this.host._space === d.spaceId) this.host._commitSpace(this.host._serverCfg!.spaces[0]?.id || ''); + this.host._regSignature = ''; + this.host._maybeRebuildDevices(); + this.host._showToast(this.host._t('toast.space_deleted')); + } catch (e: any) { + if (e?.code === 'conflict' || e?.code === 'space_in_use') { + await Promise.all([this.host._reloadConfigOnly(true), this.host._reloadLayoutOnly()]); + } + const refreshedConfig = this.host._serverCfg; + if (this.host._spaceDialog && refreshedConfig) { + const refreshed = collectSpaceMarkerDependencies( + refreshedConfig, this.host._layout || {}, d.spaceId || '', + ); + const stillLastSpace = refreshedConfig.spaces.length === 1 + && refreshedConfig.spaces[0]?.id === d.spaceId; + this.host._spaceDialog = { + ...this.host._spaceDialog, + busy: false, + deleteBlockers: stillLastSpace ? 0 : refreshed.count, + }; + } + this.host._showToast(this.host._t('toast.delete_failed', { err: this.host._errText(e) })); + } + } + +public async _saveConfigNow(): Promise { + this.host._cfgEpoch++; + try { + // same queue as the debounced writer: a dialog saving while a background + // write is still out must not race it into a self-inflicted conflict + await this._writeConfig(); + } catch (e: any) { + if (e?.physicalGeometryRolledBack) await this._reloadRejectedPhysicalWrite(); + else if (e?.code === 'conflict') await this.host._reloadConfigOnly(); + throw e; + } + } + +public _startImport(): void { + const dlg = this.host._importDialog; + if (!dlg) return; + const titles = dlg.floors.filter((f) => f.checked).map((f) => f.name); + this.host._importDialog = null; + if (!titles.length) { + this._openSpaceDialog('create'); + return; + } + this.host._importQueue = titles; + this.host._importTotal = titles.length; + this._openNextImport(); + } + +public _openNextImport(): void { + const title = this.host._importQueue.shift(); + if (title === undefined) return; + this.host._spaceDialog = { + mode: 'create', title, planUrl: null, planFile: null, + ...initialSpaceDisplayDraft(), + hideDecor: false, hideOpenings: false, zeroWallStyle: 'dashed', + roomColor: DEFAULT_ROOM_COLOR, roomOpacity: DEFAULT_ROOM_OPACITY, fillMode: 'custom', + customFill: null, + glowEnabled: true, + bgColor: null, + bgMode: 'daynight', northDeg: null, sunRays: null, + tempMin: DEFAULT_TEMP_MIN, tempMax: DEFAULT_TEMP_MAX, + showLqi: this.host._config?.show_signal ?? true, + cardFontScale: 1, + labelTemp: false, labelHum: false, labelLqi: false, labelLight: false, + cellCm: newSpaceCellCm(this.host._imperial), + cellCmInput: gridCellFieldValue(newSpaceCellCm(this.host._imperial), this.host._imperial), + cellCmTouched: false, + busy: false, + }; + } + +public _skipImport(): void { + this.host._spaceDialog = null; + if (this.host._importQueue.length) this._openNextImport(); + else if (this.host._importTotal > 0 && this.host._model.length) { + this.host._importTotal = 0; + this.host._commitSpace(this.host._serverCfg!.spaces[0]?.id || this.host._space); + this._setMode('plan'); + this.host._showToast(this.host._t('import.done')); + } + } + +public _renderImportDialog(): TemplateResult { + const d = this.host._importDialog!; + const n = d.floors.filter((f) => f.checked).length; + return html` (this.host._importDialog = null)}> +
+
${this.host._t('import.hint')}
+ ${d.floors.map( + (f, i) => html``, + )} +
+
+ + + +
+
`; + } + +public _openSettingsDialog = (): void => { + if (!this.host._norm) return; + // deep copy so the dialog edits do not leak into the live palette + const cm = this.host._glowRadiusCm; + const glowRadius = this.host._imperial + ? Math.round((cm / 30.48) * 10) / 10 + : Math.round(cm) / 100; + this.host._settingsDialog = { + colors: JSON.parse(JSON.stringify(this.host._fillColors)), + glowRadius, + bgColor: stageBgOf(this.host._settings, { bgColor: null }) || null, + northDeg: northDegOf(this.host._settings, {}), + bgMode: bgModeOf(this.host._settings, {}), + sunRays: sunRaysOn(this.host._settings, {}), + busy: false, + }; + }; + +public _preflightDiagnostics( + preflight: OptimizeGeometryPreflightResult, + candidate: ServerConfig | null, + ): object { + // CODE-REVIEW-295-r1 M1: hash the CANDIDATE spaces the preflight judged, + // not the already-saved config — a saved-config hash is exactly what a + // space export would reproduce, and the block promises what the export + // does not carry. + const spacesById = new Map(((candidate as any)?.spaces || []) + .map((space) => [String(space?.id || ''), space])); + return { + kind: 'houseplan-optimize-preflight', + origin: 'runtime', + cardVersion: CARD_VERSION, + checkedAt: new Date().toISOString(), + preflightFingerprint: preflight.fingerprint, + failures: preflight.failures.map((failure) => ({ + spaceId: failure.spaceId, + displayName: failure.displayName, + reason: failure.reason, + detail: failure.detail ?? null, + spaceGeometryFingerprint: spacesById.has(failure.spaceId) + ? spacePhysicalGeometryFingerprint(spacesById.get(failure.spaceId)) + : null, + })), + }; + } + +public _reportPreflightFailure( + preflight: OptimizeGeometryPreflightResult, + candidate: ServerConfig | null, + ): void { + if (preflight.ok || preflight.fingerprint === this.host._reportedPreflightFingerprint) return; + this.host._reportedPreflightFingerprint = preflight.fingerprint; + // eslint-disable-next-line no-console + console.warn('[houseplan] optimize preflight failed', this._preflightDiagnostics(preflight, candidate)); + } + +public _preflightVersionsDiffer(): boolean { + const integration = this.host._haIntegrationVersion; + return typeof integration === 'string' && integration.length > 0 + && integration !== CARD_VERSION; + } + +public async _copyPreflightDiagnostics(): Promise { + const preflight = this.host._alignDialog?.preflight; + if (!preflight || preflight.ok) return; + const text = JSON.stringify( + this._preflightDiagnostics(preflight, this.host._alignDialog?.config ?? null), null, 2, + ); + try { + await navigator.clipboard.writeText(text); + this.host._preflightClipboardFallback = null; + this.host._showToast(this.host._t('gs.preflight_copied')); + } catch { + // Insecure context / embedded webview: surface the block inline so the + // owner can select and copy it by hand. + this.host._preflightClipboardFallback = text; + } + } + +public _checkOptimizeGeometry(config: ServerConfig): OptimizeGeometryPreflightResult { + return checkOptimizeGeometry(config, { + fallbackSpaceName: (index) => this.host._t('gs.align_preflight_space', { + n: String(index), + }), + }); + } + +public _checkSpacePhysicalGeometry( + config: ServerConfig, + spaceId: string, + captureWallGeometry?: ( + geometry: ReturnType, + ) => void, + ) { + return checkSpacePhysicalGeometry(config, spaceId, { + fallbackSpaceName: (index) => this.host._t('gs.align_preflight_space', { n: String(index) }), + captureWallGeometry: captureWallGeometry + ? (_input, geometry) => captureWallGeometry(geometry) + : undefined, + }); + } + +public _optimizeReferenceContext( + removeLiveMissingPositions: boolean, + ): SpaceReferenceRepairContext { + const registry = this.host._haRegistry; + const full = this.host._fullRegistryHass; + const names: Record = {}; + const humanName = (...values: unknown[]): string => { + for (const value of values) { + const name = String(value || '').trim(); + if (name) return name; + } + return ''; + }; + for (const [deviceId, device] of Object.entries(registry.devices || {})) { + names[deviceId] = humanName(device?.name_by_user, device?.name, device?.model); + } + for (const [entityId, entity] of Object.entries(registry.entities || {})) { + names[`lg_${entityId}`] = humanName( + this.host.hass?.states?.[entityId]?.attributes?.friendly_name, + entity?.name, + entity?.original_name, + ); + } + for (const device of this.host._devices) names[device.id] = humanName(device.name, names[device.id]); + for (const marker of this.host._serverCfg?.markers || []) { + const separator = String(marker.binding || '').indexOf(':'); + const kind = separator > 0 ? marker.binding.slice(0, separator) : ''; + const ref = separator > 0 ? marker.binding.slice(separator + 1) : ''; + const bound = kind === 'device' + ? full?.devices?.[ref] + : kind === 'entity' ? full?.entities?.[ref] : null; + names[marker.id] = humanName( + marker.name, + kind === 'device' ? bound?.name_by_user : null, + bound?.name, + kind === 'entity' ? this.host.hass?.states?.[ref]?.attributes?.friendly_name : null, + names[marker.id], + ); + } + return { + effectiveAreaByMarker: Object.fromEntries( + this.host._devices + .filter((device) => !device.virtual && !!device.area) + .map((device) => [device.id, device.area]), + ), + ownerRoster: { + authoritative: registry.authoritative, + deviceIds: Object.keys(registry.devices || {}), + // State-only YAML entities are positive existence evidence even though + // an authoritative Entity Registry cannot list them. + entityIds: [...new Set([ + ...Object.keys(registry.entities || {}), + ...Object.keys(this.host.hass?.states || {}), + ])], + names, + }, + removeLiveMissingPositions, + }; + } + +public _previewAlignDialog(removeLiveMissingPositions: boolean): void { + if (!this.host._norm || !this.host._serverCfg) return; + const spaces = this.host._serverCfg.spaces || []; + let r; + try { + r = optimizePlans( + this.host._serverCfg, + this.host._layout || {}, + this._optimizeReferenceContext(removeLiveMissingPositions), + ); + } catch (error) { + this._showWallModelMigrationBlocked(error); + return; + } + const preflight = r.changed ? this._checkOptimizeGeometry(r.config) : null; + if (preflight) this._reportPreflightFailure(preflight, r.config); + // The maximum geometry shift is an UPPER BOUND, not a sample. The run + // measured every element in the centimetres of ITS OWN space — converting + // one normalised maximum through the first space's `cell_cm` understated + // a two-scale plan twentyfold (AUD-158B1-01) — and the last tenth is + // rounded UP, so the promise can never be smaller than the deed. + const cm = Math.ceil(r.report.maxShiftCm * 10) / 10; + const sp = spaces.find((x: any) => x?.id != null && String(x.id) === r.report.maxSpace); + const where = spaces.length > 1 && sp ? String(sp.title || sp.id) : ''; + // CODE-REVIEW-295-r1 M2: the inline clipboard fallback belongs to one + // dialog showing — a reopened dialog must not display the previous + // refusal's JSON while the visible reasons already describe a new one. + this.host._preflightClipboardFallback = null; + this.host._alignDialog = { + report: r.report, config: r.config, layout: r.layout, cm, where, + preflight, changed: r.changed, busy: false, removeLiveMissingPositions, + }; + } + +public _openAlignDialog = (): void => this._previewAlignDialog(false); + +public _toggleOptimizeLivePositions = (): void => { + const dialog = this.host._alignDialog; + if (!dialog || dialog.busy || !dialog.report.liveMissingPositions.length) return; + this._previewAlignDialog(!dialog.removeLiveMissingPositions); + }; + +public async _runAlignToGrid(): Promise { + let d = this.host._alignDialog; + if (!d || d.busy || !this.host._serverCfg || !d.changed || !d.preflight?.ok) return; + const fingerprint = contentFingerprint(d.config); + if (d.preflight.fingerprint !== fingerprint) { + const preflight = this._checkOptimizeGeometry(d.config); + this._reportPreflightFailure(preflight, d.config); + d = { ...d, preflight }; + this.host._alignDialog = d; + if (!preflight.ok) return; + } + this._clearGeometryGesture(); + this.host._alignDialog = { ...d, busy: true }; + try { + if (this.host._saveConfigDebounced.pending()) this.host._saveConfigDebounced.flush(); + await this.host._writeChain; + const resp = await this.host.hass.callWS({ + type: 'houseplan/plan/optimize', + config: d.config, + layout: d.layout, + expected_config_rev: this.host._cfgRev, + expected_layout_rev: this.host._layoutRev, + }); + this.host._serverCfg = d.config; + this.host._layout = d.layout; + this.host._geometryHistory.clear(); + this.host._cfgRev = resp?.config_rev ?? this.host._cfgRev + 1; + this.host._layoutRev = resp?.layout_rev ?? this.host._layoutRev + 1; + this.host._canOptimizeUndo = !!resp?.can_undo; + this.host._undoKind = resp?.can_undo ? 'optimize' : null; + this.host._dirtyPos.clear(); + this.host._sentPos.clear(); + this.host._cfgEpoch++; + this.host._modelCache = null; + this.host._frame = null; + this.host._regSignature = ''; + this.host._maybeRebuildDevices(); + this.host._cacheSnapshot(); + this.host._alignDialog = null; + this.host._preflightClipboardFallback = null; + this.host.requestUpdate(); + this.host._showToast(this.host._t('gs.align_done', { + n: String(d.report.moved), + m: String(d.report.migrated + d.report.canonicalized + + d.report.coordsCanonicalized + d.report.latticeCoordinatesCanonicalized + + d.report.wallsMerged + d.report.spansMerged + + d.report.partitionsMerged + d.report.partitionsReconciled + + d.report.openingsRehosted + d.report.wallsStraightened), + r: String(d.report.spaceRefsRemapped + d.report.roomRefsRemapped + + d.report.positionsRemapped + d.report.markersDetached + + d.report.orphanRoomLabelsRemoved + d.report.orphanDevicePositionsRemoved + + d.report.orphanGroupPositionsRemoved), + })); + } catch (e: any) { + if (this.host._alignDialog) this.host._alignDialog = { ...this.host._alignDialog, busy: false }; + if (e?.code === 'wall_model_client_outdated') { + this.host._showToast(this.host._t('toast.wall_model_client_outdated')); + return; + } + if (e?.code === 'conflict') { + await Promise.all([this.host._reloadConfigOnly(true), this.host._reloadLayoutOnly()]); + } + this.host._showToast(this.host._t('toast.error', { err: this.host._errText(e) })); + } + } + +public async _undoPlanOptimization(): Promise { + if (!this.host._canOptimizeUndo || this.host._optimizeUndoBusy) return; + const undoKind = this.host._undoKind; + this._clearGeometryGesture(); + this.host._optimizeUndoBusy = true; + this.host.requestUpdate(); + try { + await this.host.hass.callWS({ + type: 'houseplan/plan/optimize_undo', + expected_config_rev: this.host._cfgRev, + expected_layout_rev: this.host._layoutRev, + }); + const [cfgResp, layResp] = await Promise.all([ + this.host.hass.callWS({ type: 'houseplan/config/get' }), + this.host.hass.callWS({ type: 'houseplan/layout/get' }), + ]); + this.host._serverCfg = cfgResp?.config || this.host._serverCfg; + this.host._cfgRev = cfgResp?.rev ?? this.host._cfgRev; + this.host._layout = layResp?.layout || this.host._layout; + this.host._geometryHistory.clear(); + this.host._layoutRev = layResp?.rev ?? this.host._layoutRev; + this.host._canOptimizeUndo = false; + this.host._undoKind = null; + this.host._cfgEpoch++; + this.host._modelCache = null; + this.host._frame = null; + this.host._regSignature = ''; + this.host._maybeRebuildDevices(); + this.host._cacheSnapshot(); + this.host.requestUpdate(); + this.host._showToast(this.host._t(undoKind === 'import' ? 'backup.import_undone' : 'gs.optimize_undone')); + } catch (e: any) { + this.host._canOptimizeUndo = false; + this.host._undoKind = null; + this.host._showToast(this.host._t('toast.error', { err: this.host._errText(e) })); + } finally { + this.host._optimizeUndoBusy = false; + this.host.requestUpdate(); + } + } + +public _openBackupExport = (): void => { + this.host._settingsDialog = null; + this.host._backupExportDialog = { kind: 'full', planOnly: false, busy: false, error: '' }; + }; + +public async _runBackupExport(): Promise { + const d = this.host._backupExportDialog; + if (!d || d.busy) return; + this.host._backupExportDialog = { ...d, busy: true, error: '' }; + try { + const response: any = await this.host.hass.callWS({ + type: 'houseplan/export/create', + kind: d.kind, + space_id: d.kind === 'space' ? this.host._space : undefined, + ...(d.kind === 'space' && d.planOnly ? { plan_only: true } : {}), + card_version: CARD_VERSION, + }); + const blob = new Blob([JSON.stringify(response.document, null, 2) + '\n'], { + type: 'application/json;charset=utf-8', + }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement('a'); + anchor.href = url; + anchor.download = response.filename || `houseplan-${d.kind}.json`; + anchor.style.display = 'none'; + document.body.append(anchor); + anchor.click(); + anchor.remove(); + window.setTimeout(() => URL.revokeObjectURL(url), 0); + this.host._backupExportDialog = null; + this.host._showToast(this.host._t('backup.export_done')); + } catch (error: any) { + if (this.host._backupExportDialog) { + this.host._backupExportDialog = { ...this.host._backupExportDialog, busy: false, error: this._backupErrorText(error) }; + } + } + } + +public async _pickBackupImport(ev: Event): Promise { + const input = ev.target as HTMLInputElement; + const file = input.files?.[0]; + input.value = ''; + if (!file) return; + this.host._settingsDialog = null; + this.host._backupImportDialog = { + filename: file.name, size: file.size, token: '', preview: null, + expectedConfigRev: this.host._cfgRev, expectedLayoutRev: this.host._layoutRev, + duplicatePolicy: 'skip', confirmMissing: false, busy: true, error: '', + }; + try { + if (this.host._saveConfigDebounced.pending()) this.host._saveConfigDebounced.flush(); + await this.host._writeChain; + if (this.host._persistLayout.pending()) this.host._persistLayout.flush(); + const path = '/api/houseplan/import/preview?duplicate_policy=skip'; + const init = { method: 'POST', body: file, headers: { 'content-type': 'application/json' } }; + const response: Response = this.host.hass?.fetchWithAuth + ? await this.host.hass.fetchWithAuth(path, init) + : await fetch(path, { + ...init, + headers: { + ...init.headers, + ...(this.host.hass?.auth?.data?.access_token + ? { authorization: `Bearer ${this.host.hass.auth.data.access_token}` } : {}), + }, + }); + const data = await response.json().catch(() => ({})); + if (!response.ok || data.error) throw Object.assign(new Error(data.message || data.error || `HTTP ${response.status}`), { code: data.error }); + if (this.host._backupImportDialog) { + this.host._backupImportDialog = { + ...this.host._backupImportDialog, + token: data.token, + preview: data.preview, + expectedConfigRev: data.expected_config_rev, + expectedLayoutRev: data.expected_layout_rev, + busy: false, + }; + } + } catch (error: any) { + if (this.host._backupImportDialog) { + this.host._backupImportDialog = { ...this.host._backupImportDialog, busy: false, error: this._backupErrorText(error) }; + } + } + } + +public async _setBackupDuplicatePolicy(policy: 'skip' | 'virtual'): Promise { + const d = this.host._backupImportDialog; + if (!d || d.busy || !d.token || policy === d.duplicatePolicy) return; + this.host._backupImportDialog = { ...d, duplicatePolicy: policy, busy: true, error: '' }; + try { + const response: any = await this.host.hass.callWS({ + type: 'houseplan/import/revalidate', token: d.token, duplicate_policy: policy, + }); + if (this.host._backupImportDialog) { + this.host._backupImportDialog = { + ...this.host._backupImportDialog, + preview: { ...d.preview, ...response.preview }, + expectedConfigRev: response.expected_config_rev, + expectedLayoutRev: response.expected_layout_rev, + confirmMissing: false, + busy: false, + }; + } + } catch (error: any) { + if (this.host._backupImportDialog) { + this.host._backupImportDialog = { + ...this.host._backupImportDialog, + duplicatePolicy: d.duplicatePolicy, + preview: d.preview, + confirmMissing: d.confirmMissing, + busy: false, + error: this._backupErrorText(error), + }; + } + } + } + +public async _applyBackupImport(): Promise { + const d = this.host._backupImportDialog; + if (!d || d.busy || !d.token || !d.preview) return; + if (d.preview.confirmation_required && !d.confirmMissing) return; + this.host._backupImportDialog = { ...d, busy: true, error: '' }; + const previousSpace = this.host._space; + try { + const result: any = await this.host.hass.callWS({ + type: 'houseplan/import/apply', + token: d.token, + expected_config_rev: d.expectedConfigRev, + expected_layout_rev: d.expectedLayoutRev, + duplicate_policy: d.duplicatePolicy, + confirm_missing_content: d.confirmMissing, + }); + const [configResponse, layoutResponse] = await Promise.all([ + this.host.hass.callWS({ type: 'houseplan/config/get' }), + this.host.hass.callWS({ type: 'houseplan/layout/get' }), + ]); + this.host._adoptStructuralResponses(configResponse, layoutResponse); + this.host._geometryHistory.clear(); + this.host._dirtyPos.clear(); + this.host._sentPos.clear(); + this.host._defPos = {}; + this.host._cfgEpoch++; + this.host._modelCache = null; + this.host._frame = null; + this.host._visibleDeviceSnapshot = null; + this.host._candidateDeviceSnapshot = null; + this.host._stagedDeviceSnapshotToken = -1; + this.host._capturedSnapshotConfigEpoch = -1; + this.host._regSignature = ''; + this.host._signer.invalidate(this.host.hass); + this.host._resign(); + this.host._maybeRebuildDevices(); + const spaces = this.host._serverCfg?.spaces || []; + const nextSpace = result.kind === 'space' && result.space_id + ? result.space_id + : spaces.some((space) => space.id === previousSpace) + ? previousSpace : spaces[0]?.id || this.host._space; + if (this.host._hasFixedFloor) this.host._adoptInitialSpace(this.host._model, true); + else this.host._commitSpace(nextSpace); + this.host._backupImportDialog = null; + this.host._cacheSnapshot(); + this.host.requestUpdate(); + const outcomeCounts = result.kind === 'space' ? d.preview.counts : result.counts; + this.host._showToast(this.host._t(result.kind === 'space' ? 'backup.space_done' : 'backup.full_done', { + spaces: String(outcomeCounts?.spaces || 0), rooms: String(outcomeCounts?.rooms || 0), + markers: String(outcomeCounts?.markers || 0), + refs: String(result.repaired_target_refs || 0), + })); + } catch (error: any) { + if (error?.code === 'conflict' && this.host._backupImportDialog?.token) { + try { + const current = this.host._backupImportDialog; + const refreshed: any = await this.host.hass.callWS({ + type: 'houseplan/import/revalidate', token: current.token, + duplicate_policy: current.duplicatePolicy, + }); + this.host._backupImportDialog = { + ...current, + preview: { ...current.preview, ...refreshed.preview }, + expectedConfigRev: refreshed.expected_config_rev, + expectedLayoutRev: refreshed.expected_layout_rev, + confirmMissing: false, + busy: false, + error: this.host._t('backup.revalidated'), + }; + return; + } catch (refreshError: any) { + error = refreshError; + } + } + if (this.host._backupImportDialog) { + this.host._backupImportDialog = { ...this.host._backupImportDialog, busy: false, error: this._backupErrorText(error) }; + } + } + } + +public _renderBackupExportDialog(): TemplateResult { + const d = this.host._backupExportDialog!; + const currentSpace = (this.host._serverCfg?.spaces || []).find((space) => space.id === this.host._space); + return html` (this.host._backupExportDialog = null)}> +
+
${this.host._t('backup.export_hint')}
+ + + ${d.kind === 'space' && currentSpace ? html`` : nothing} +
${this.host._t('backup.privacy_warning')}
+ ${d.error ? html`` : nothing} +
+
+ + + +
+
`; + } + +public _renderBackupImportDialog(): TemplateResult { + const d = this.host._backupImportDialog!; + const p = d.preview; + const counts = p?.counts || {}; + const report = p?.reference_report || {}; + const sum = (values: any): number => Object.values(values || {}).reduce( + (total: number, value: any) => total + (Number(value) || 0), 0, + ); + const allReportRows: Array<[ + 'incoming_remapped' | 'target_repaired' | 'preserved_unresolved' + | 'collisions' | 'dropped_links' | 'bounded_lineages', + number, + ]> = [ + ['incoming_remapped', sum(report.remapped?.incoming)], + ['target_repaired', sum(report.remapped?.target)], + ['preserved_unresolved', sum(report.preservedUnresolved)], + ['collisions', sum(report.collisions)], + ['dropped_links', sum(report.droppedIncomingLinks)], + ['bounded_lineages', Number(report.boundedLineages) || 0], + ]; + const reportRows = allReportRows.filter(([, value]) => value > 0); + return html` (this.host._backupImportDialog = null)}> +
+
${d.filename}${(d.size / 1024).toFixed(1)} KB
+ ${d.busy && !p ? html`
${this.host._t('backup.reading')}
` : nothing} + ${d.error ? html`` : nothing} + ${p ? html` +
+ ${this.host._t(p.kind === 'full' ? 'backup.full' : 'backup.current_space')} + ${p.plan_only ? html`${this.host._t('backup.plan_only_preview')}` : nothing} + ${this.host._t(p.source === 'same' ? 'backup.same_source' : 'backup.foreign_source')} + ${this.host._t('backup.created', { value: p.created_at || '—' })} + ${this.host._t('backup.versions', { + card: p.card_version || '—', integration: p.integration_version || '—', + model: String(p.model_version ?? '—'), + })} +
+
+ ${this.host._t('backup.count_spaces', { n: p.kind === 'full' + ? `${p.current_counts?.spaces || 0} → ${counts.spaces || 0}` : String(counts.spaces || 0) })} + ${this.host._t('backup.count_rooms', { n: p.kind === 'full' + ? `${p.current_counts?.rooms || 0} → ${counts.rooms || 0}` : String(counts.rooms || 0) })} + ${this.host._t('backup.count_walls', { n: p.kind === 'full' + ? `${p.current_counts?.walls || 0} → ${counts.walls || 0}` : String(counts.walls || 0) })} + ${this.host._t('backup.count_openings', { n: p.kind === 'full' + ? `${p.current_counts?.openings || 0} → ${counts.openings || 0}` : String(counts.openings || 0) })} + ${this.host._t('backup.count_decor', { n: p.kind === 'full' + ? `${p.current_counts?.decor || 0} → ${counts.decor || 0}` : String(counts.decor || 0) })} + ${this.host._t('backup.count_markers', { n: p.kind === 'full' + ? `${p.current_counts?.markers || 0} → ${counts.markers || 0}` : String(counts.markers || 0) })} + ${this.host._t('backup.count_layout', { n: p.kind === 'full' + ? `${p.current_counts?.layout || 0} → ${counts.layout || 0}` : String(counts.layout || 0) })} +
+ ${p.bindings ? html`
${this.host._t('backup.bindings', { + device: String(p.bindings.device || 0), entity: String(p.bindings.entity || 0), + virtual: String(p.bindings.virtual || 0), legacy: String(p.legacy_positions || 0), + })}
${this.host._t('backup.binding_status', { + active: String(p.bindings.active || 0), disabled: String(p.bindings.disabled || 0), + missing: String(p.bindings.missing || 0), + })}
` : nothing} + ${p.missing_areas?.length ? html`
${this.host._t('backup.missing_areas', { + areas: p.missing_areas.join(', '), + })}
` : nothing} + ${p.dropped_marker_links ? html`
${this.host._t('backup.dropped_marker_links', { + n: String(p.dropped_marker_links), + })}
` : nothing} + ${p.repaired_target_refs ? html`
${this.host._t('backup.repaired_target_refs', { + n: String(p.repaired_target_refs), + })}
` : nothing} + ${p.preserved_unresolved_refs ? html` +
${this.host._t('backup.preserved_unresolved_refs', { + n: String(p.preserved_unresolved_refs), + })}
${this.host._t('backup.preserved_unresolved_hint')}
+ ` : nothing} + ${reportRows.length ? html`
+ ${this.host._t('backup.import_details')} +
+ ${reportRows.map(([key, value]) => html`${this.host._t( + `backup.import_detail.${key}`, { n: String(value) }, + )}`)} + ${(report.examples || []).slice(0, 8).map((item) => html` + ${item.owner} → ${item.reference} + `)} +
+
` : nothing} + ${p.kind === 'full' ? html` +
${this.host._t('backup.replace_warning')}
+ ${p.source === 'foreign' ? html`
${this.host._t('backup.foreign_bookkeeping')}
` : nothing}` : html` +
${this.host._t('backup.final_name')}${p.space_title}
+
${this.host._t('backup.target_settings')}
+ ${p.duplicates ? html`
${this.host._t('backup.duplicates', { n: String(p.duplicates) })} + + +
` : nothing}`} + ${p.content?.length ? html`
+ ${this.host._t('backup.content')} + ${p.content.map((item) => html`${item.url} · ${this.host._t( + item.state === 'available' ? 'backup.content_available' + : item.state === 'external' ? 'backup.content_external' + : 'backup.content_detach_required', + )}`)} +
` : nothing} + ${p.confirmation_required ? html`` : nothing} + ` : nothing} +
+
+ + + ${p ? html`` : nothing} +
+
`; + } + +public _setFillColor(key: keyof FillColors, patch: Partial<{ c: string; a: number }>): void { + const d = this.host._settingsDialog!; + this.host._settingsDialog = { ...d, colors: { ...d.colors, [key]: { ...d.colors[key], ...patch } } }; + } + +public async _saveSettingsDialog(): Promise { + const d = this.host._settingsDialog; + if (!d || d.busy) return; + this.host._settingsDialog = { ...d, busy: true }; + try { + const cfg = this.host._serverCfg!; + const isDefault = JSON.stringify(d.colors) === JSON.stringify(DEFAULT_FILL_COLORS); + const settings: any = { ...cfg.settings }; + if (isDefault) delete settings.fill_colors; + else settings.fill_colors = d.colors; + const cm = this.host._imperial ? d.glowRadius * 30.48 : d.glowRadius * 100; + if (Number.isFinite(cm) && cm > 0 && Math.round(cm) !== 300) settings.glow_radius_cm = Math.round(cm); + else delete settings.glow_radius_cm; + if (d.bgColor) settings.bg_color = d.bgColor; + else delete settings.bg_color; + // Sun background compatibility is explicit: legacy absence means static, + // while new installations and spaces materialise daynight. + if (d.northDeg !== null && Number.isInteger(d.northDeg) && d.northDeg >= 0 && d.northDeg <= 359) + settings.north_deg = d.northDeg; + else delete settings.north_deg; + settings.bg_mode = d.bgMode; + if (d.sunRays) settings.sun_rays = true; + else delete settings.sun_rays; + // Legacy compatibility: old configs may still contain this accepted + // field, but weather no longer affects sunlight and the UI no longer + // exposes it. Saving general settings cleans the obsolete value up. + delete settings.weather_entity; + this.host._serverCfg = { ...cfg, settings }; + await this._saveConfigNow(); + this.host._settingsDialog = null; + this.host.requestUpdate(); + this.host._showToast(this.host._t('gs.saved')); + } catch (e: any) { + // audit L3: the dialog may have been closed (Esc) while the save was + // in flight — spreading null yields a truthy husk and the renderer + // then crashes, blanking the whole card. The toast below is the + // only remaining signal, so it must still fire. + if (this.host._settingsDialog) this.host._settingsDialog = { ...this.host._settingsDialog, busy: false }; + this.host._showToast(this.host._t('toast.error', { err: this.host._errText(e) })); + } + } + +public _boolInput(checked: boolean, onChange: (v: boolean) => void, disabled = false): TemplateResult { + const h = (e: Event) => onChange(!!(e.target as HTMLInputElement).checked); + return customElements.get('ha-switch') + ? html`` + : html``; + } + +public _rangeInput( + min: number, max: number, step: number, value: number, + onInput: (v: number) => void, disabled = false, ariaLabel?: string, + ): TemplateResult { + const h = (e: Event) => { + const n = Number((e.target as HTMLInputElement).value); + if (Number.isFinite(n)) onInput(n); + }; + return customElements.get('ha-slider') + ? html`` + : html``; + } + +public _renderColorRow(key: keyof FillColors, labelKey: string): TemplateResult { + const d = this.host._settingsDialog!; + const v = d.colors[key]; + return html`
+ ) => { + this._setFillColor(key, { c: e.detail.color, a: e.detail.opacity }); + }}> +
`; + } + +public _renderAlignDialog(): TemplateResult { + const d = this.host._alignDialog!; + const r = d.report; + const failed = d.changed && !d.preflight?.ok; + const failures = d.preflight?.failures || []; + const visibleNames = failures.slice(0, 3).map((failure) => failure.displayName); + const spaces = visibleNames.length + ? visibleNames.join(', ') + : this.host._t('gs.align_preflight_space', { n: '1' }); + const remaining = Math.max(0, failures.length - visibleNames.length); + const more = remaining + ? this.host._t('gs.align_preflight_more', { n: String(remaining) }) + : ''; + const repaired = r.spaceRefsRemapped + r.roomRefsRemapped + + r.positionsRemapped + r.markersDetached; + const modelMaintenance = r.migrated + r.canonicalized + r.coordsCanonicalized + + r.wallSegmentsMigrated + + r.wallsMerged + r.spansMerged + r.partitionsMerged + + r.partitionsReconciled + r.openingsRehosted + r.redundantDraftsRemoved; + const gridWarning = r.moved + r.rotated + r.removedDrafts + + r.coordsCanonicalized + r.wallsStraightened; + const straightenCm = Math.ceil(r.maxStraightenShiftCm * 10) / 10; + const straightenSpace = (this.host._serverCfg?.spaces || []).find( + (space) => String(space?.id || '') === r.maxStraightenSpace, + ); + const straightenWhere = (this.host._serverCfg?.spaces || []).length > 1 && straightenSpace + ? String(straightenSpace.title || straightenSpace.id) : ''; + const removed = r.orphanRoomLabelsRemoved + r.orphanDevicePositionsRemoved + + r.orphanGroupPositionsRemoved; + const liveNames = r.liveMissingPositions.map((item) => item.name).filter(Boolean); + const visibleLiveNames = liveNames.slice(0, 3).join(', '); + const remainingLiveNames = Math.max(0, liveNames.length - 3); + const liveNamesText = visibleLiveNames + ? this.host._t('gs.optimize_live_names', { + names: visibleLiveNames, + more: remainingLiveNames + ? this.host._t('gs.optimize_reference_more', { n: String(remainingLiveNames) }) : '', + }) + : ''; + const registryLimited = r.unverifiedPositions.some( + (item) => item.reason === 'registry_unavailable', + ); + const detailStatus = (item: typeof r.removedPositions[number]): string => { + if (r.removedPositions.some((removedItem) => removedItem.id === item.id)) { + return this.host._t('gs.optimize_detail_removed'); + } + if (r.liveMissingPositions.some((liveItem) => liveItem.id === item.id)) { + return this.host._t('gs.optimize_detail_live'); + } + return this.host._t('gs.optimize_detail_unverified'); + }; + const detailKind = (kind: typeof r.removedPositions[number]['kind']): string => this.host._t( + kind === 'room_label' ? 'gs.optimize_detail_room_label' + : kind === 'group' ? 'gs.optimize_detail_group' + : kind === 'device' ? 'gs.optimize_detail_device' + : 'gs.optimize_detail_unknown', + ); + const referenceDetails = [ + ...r.removedPositions, + ...r.liveMissingPositions.filter((item) => ( + !r.removedPositions.some((removedItem) => removedItem.id === item.id) + )), + ...r.unverifiedPositions, + ]; + const visibleDetails = referenceDetails.slice(0, 10); + const remainingDetails = Math.max(0, referenceDetails.length - visibleDetails.length); + return html` { this.host._alignDialog = null; this.host._preflightClipboardFallback = null; }}> +
+ ${failed + ? html` +

${this.host._t('gs.align_preflight_failed', { spaces, more })}

+ ${failures.slice(0, 10).map((failure) => html`

+ ${failure.displayName}: ${this.host._t(`gs.preflight_reason_${failure.reason}` as any)} +

`)} + ${failures.length > 10 ? html`

+ ${this.host._t('gs.align_preflight_more', { n: String(failures.length - 10) })} +

` : nothing} +
${this.host._t('gs.align_preflight_hint')}
+ ${this._preflightVersionsDiffer() ? html` +
${this.host._t('gs.preflight_update_hint')}
` : nothing} +
+ +
+ ${this.host._preflightClipboardFallback ? html`
+ ${this.host._t('gs.preflight_copy')} +
${this.host._preflightClipboardFallback}
+
` : nothing}` + : !d.changed + ? html`

${this.host._t( + r.liveMissingPositions.length || r.unverifiedPositions.length + || r.nestedRefsUnresolved + ? 'gs.optimize_no_automatic_changes' : 'gs.align_none', + )}

` + : html` + ${r.moved ? html`

${this.host._t('gs.align_count', { + n: String(r.moved), total: String(r.total), cm: String(d.cm), + })}

` : nothing} + ${r.latticeCoordinatesCanonicalized ? html` +

${this.host._t('gs.optimize_lattice_summary', { + n: String(r.latticeCoordinatesCanonicalized), + cm: formatLatticeShiftCm(r.latticeMaxShiftCm), + })}

+ ${r.latticeSpaces.map((space) => html`

${this.host._t( + 'gs.optimize_lattice_space', { + space: space.space, + n: String(space.canonicalized), + far: String(space.far), + }, + )}

`)} + ` : nothing} + ${d.where + ? html`

${this.host._t('gs.align_where', { s: d.where })}

` + : nothing} + ${r.rotated + ? html`

${this.host._t('gs.align_turned', { n: String(r.rotated) })}

` + : nothing} + ${r.removedDrafts + ? html`

${this.host._t('gs.align_removed_drafts', { + n: String(r.removedDrafts), + })}

` + : nothing} + ${r.redundantDraftsRemoved + ? html`

${this.host._t('gs.optimize_redundant_drafts', { + n: String(r.redundantDraftsRemoved), + })}

` + : nothing} + ${r.wallSegmentsMigrated ? html`

${this.host._t( + 'gs.wall_segments_migrated', { n: String(r.wallSegmentsMigrated) }, + )}

` : nothing} + ${r.legacyZeroWallsMigrated ? html`

${this.host._t( + 'gs.zero_walls_migrated', { n: String(r.legacyZeroWallsMigrated) }, + )}

` : nothing} + ${modelMaintenance ? html`

${this.host._t('gs.optimize_changes', { + m: String(r.migrated), c: String(r.canonicalized), + p: String(r.coordsCanonicalized), w: String(r.wallsMerged), + s: String(r.spansMerged), i: String(r.partitionsMerged), + })}

` : nothing} + ${r.partitionsReconciled ? html`

${this.host._t( + 'gs.optimize_coincident_partitions', { n: String(r.partitionsReconciled) }, + )}

` : nothing} + ${r.openingsRehosted ? html`

${this.host._t( + 'gs.optimize_openings_rehosted', { n: String(r.openingsRehosted) }, + )}

` : nothing} + ${r.wallsStraightened ? html`

${this.host._t( + 'gs.optimize_walls_straightened', { + n: String(r.wallsStraightened), cm: String(straightenCm), + }, + )}

` : nothing} + ${straightenWhere ? html`

${this.host._t( + 'gs.optimize_walls_straightened_where', { s: straightenWhere }, + )}

` : nothing} + ${r.glowSpacesMigrated || r.glowRoomsMigrated + ? html`

${this.host._t('gs.optimize_glow_migration', { + spaces: String(r.glowSpacesMigrated), + rooms: String(r.glowRoomsMigrated), + })}

` + : nothing} + ${gridWarning ? html`
${this.host._t('gs.align_warn')}
` : nothing}`} + ${!failed && r.wallsStraightenSkipped ? html`

${this.host._t( + 'gs.optimize_walls_straighten_skipped', { + n: String(r.wallsStraightenSkipped), + }, + )}

` : nothing} + ${repaired + ? html`

${this.host._t('gs.optimize_references', { + spaces: String(r.spaceRefsRemapped), rooms: String(r.roomRefsRemapped), + positions: String(r.positionsRemapped), detached: String(r.markersDetached), + })}

` + : nothing} + ${removed + ? html`

${this.host._t('gs.optimize_orphans_removed', { + total: String(removed), + rooms: String(r.orphanRoomLabelsRemoved), + devices: String(r.orphanDevicePositionsRemoved), + groups: String(r.orphanGroupPositionsRemoved), + })}

` + : nothing} + ${r.liveMissingPositions.length + ? html`
+

${this.host._t(d.removeLiveMissingPositions + ? 'gs.optimize_live_positions_remove' : 'gs.optimize_live_positions', { + n: String(r.liveMissingPositions.length), names: liveNamesText, + })}

+ + ${d.removeLiveMissingPositions + ? html`
+ ${this.host._t('gs.optimize_live_selected')} +
` + : nothing} +
` + : nothing} + ${r.unverifiedPositions.length + ? html`` + : nothing} + ${r.nestedRefsUnresolved + ? html`` + : nothing} + ${referenceDetails.length + ? html`
+ ${this.host._t('gs.optimize_details')} +
    + ${visibleDetails.map((item) => html`
  • ${this.host._t('gs.optimize_detail_item', { + status: detailStatus(item), kind: detailKind(item.kind), + id: item.id, space: item.spaceId, + })}
  • `)} +
+ ${remainingDetails + ? html`
${this.host._t('gs.optimize_details_more', { + n: String(remainingDetails), + })}
` + : nothing} +
` + : nothing} +
+
+ + + ${!d.changed || !d.preflight?.ok ? nothing : html` + `} +
+
`; + } + +public _renderSettingsDialog(): TemplateResult { + return html` (this.host._settingsDialog = null)}> +
+
${this.host._t('gs.hint')}
+ + ${this._renderColorRow('light_on', 'gs.light_on')} + ${this._renderColorRow('light_off', 'gs.light_off')} + ${this._renderColorRow('light_none', 'gs.light_none')} + + ${this._renderColorRow('temp_cold', 'gs.temp_cold')} + ${this._renderColorRow('temp_ok', 'gs.temp_ok')} + ${this._renderColorRow('temp_hot', 'gs.temp_hot')} + + ${this._renderColorRow('lqi_low', 'gs.lqi_low')} + ${this._renderColorRow('lqi_high', 'gs.lqi_high')} + + ${this._renderColorRow('glow_base', 'gs.glow_base')} + ${this._renderColorRow('glow_light', 'gs.glow_light')} +
+ ${this.host._t('gs.glow_radius')} + { + const v = strictNumber((e.target as HTMLInputElement).value); + if (v != null && v > 0) + this.host._settingsDialog = { ...this.host._settingsDialog!, glowRadius: v }; + }} /> + ${this.host._imperial ? this.host._t('gs.unit_ft') : this.host._t('gs.unit_m')} +
+ + ${this._renderColorRow('wall_fill', 'gs.wall_fill')} + +
+ ${this.host._t('gs.bg_mode')} + +
+ ${this.host._settingsDialog!.bgMode === 'static' + ? html`
+ ) => { + this.host._settingsDialog = { ...this.host._settingsDialog!, bgColor: e.detail.color }; + }}> + ${this.host._settingsDialog!.bgColor + ? html`` + : html`${this.host._t('gs.bg_theme')}`} +
` + : html`
${this.host._t('gs.bg_daynight_hint')}
`} + + ${!sunStateOf(this.host.hass) + ? html`
${this.host._t('gs.sun_missing')}
` + : nothing} +
+ ${this.host._renderCompass()} +
+ ${this.host._t('gs.north')} +
+ { + const raw = (e.target as HTMLInputElement).value.trim(); + const n = raw === '' ? null : Math.round(Number(raw)); + this.host._settingsDialog = { + ...this.host._settingsDialog!, + northDeg: n !== null && Number.isFinite(n) ? Math.min(359, Math.max(0, n)) : null, + }; + }} /> + ${this.host._settingsDialog!.northDeg !== null + ? html`` + : nothing} +
+ ${this.host._settingsDialog!.northDeg === null + ? html`
${this.host._t('gs.north_hint')}
` + : nothing} +
+
+ + ${this.host._canEdit ? html` + +
${this.host._t('gs.backup_hint')}
+
+ + + + + + ${this.host._canOptimizeUndo && this.host._undoKind === 'import' ? html` + ` : nothing} +
` : nothing} + +
${this.host._t('gs.grid_hint')}
+
+ +
+ ${this.host._canOptimizeUndo && this.host._undoKind !== 'import' ? html`
+ +
` : nothing} + +
${this.host._t('gs.about_version', { v: CARD_VERSION })}
+ + ${this.host._t('gs.about_github')} + + ${this.host._t('gs.about_telegram')} +
+
+ + + + +
+
`; + } + +public _openRulesDialog = (): void => { + if (!this.host._norm) return; + const custom = this.host._settings.icon_rules; + const rules = (custom && custom.length ? custom : DEFAULT_ICON_RULES).map((r) => ({ ...r })); + this.host._rulesDialog = { rules, test: '', busy: false }; + }; + +public _rulesSet(rules: IconRule[]): void { + this.host._rulesDialog = { ...this.host._rulesDialog!, rules }; + } + +public async _saveRules(): Promise { + const dlg = this.host._rulesDialog; + if (!dlg || dlg.busy) return; + const cleaned = dlg.rules.filter((r) => r.pattern.trim() && r.icon.trim()); + this.host._rulesDialog = { ...dlg, busy: true }; + try { + const cfg = this.host._serverCfg!; + const isDefault = JSON.stringify(cleaned) === JSON.stringify(DEFAULT_ICON_RULES); + const settings: any = { ...cfg.settings }; + if (isDefault) delete settings.icon_rules; + else settings.icon_rules = cleaned; + this.host._serverCfg = { ...cfg, settings }; + await this._saveConfigNow(); + this.host._rulesDialog = null; + this.host._regSignature = ''; + this.host._maybeRebuildDevices(); + this.host._showToast(this.host._t('rules.saved')); + } catch (e: any) { + // audit L3: the dialog may have been closed (Esc) while the save was + // in flight — spreading null yields a truthy husk and the renderer + // then crashes, blanking the whole card. The toast below is the + // only remaining signal, so it must still fire. + if (this.host._rulesDialog) this.host._rulesDialog = { ...this.host._rulesDialog, busy: false }; + this.host._showToast(this.host._t('toast.error', { err: this.host._errText(e) })); + } + } + +public _renderRulesDialog(): TemplateResult { + const d = this.host._rulesDialog!; + const compiled = compileIconRules(d.rules); + const testIcon = d.test.trim() ? iconFor(d.test, '', compiled) : null; + const move = (i: number, delta: number) => { + const r = [...d.rules]; + const j = i + delta; + if (j < 0 || j >= r.length) return; + [r[i], r[j]] = [r[j], r[i]]; + this._rulesSet(r); + }; + return html` (this.host._rulesDialog = null)}> +
+
${this.host._t('rules.hint')}
+
+ (this.host._rulesDialog = { ...d, test: (e.target as HTMLInputElement).value })} /> + ${testIcon ? html`${testIcon}` : nothing} +
+ ${d.rules.map((r, i) => { + const bad = r.pattern.trim() !== '' && !isValidPattern(r.pattern); + return html`
+ { + const rules = [...d.rules]; + rules[i] = { ...r, pattern: (e.target as HTMLInputElement).value }; + this._rulesSet(rules); + }} /> + { + const rules = [...d.rules]; + rules[i] = { ...r, icon: (e.target as HTMLInputElement).value }; + this._rulesSet(rules); + }} /> + + move(i, -1)}> + move(i, 1)}> + this._rulesSet(d.rules.filter((_, j) => j !== i))}> +
`; + })} + +
+
+ + + + +
+
`; + } + +public _saveKioskScale(patch: Partial<{ icon: number; font: number }>): void { + this.host._kioskScale = { ...this.host._kioskScale, ...patch }; + try { + localStorage.setItem(LS_KIOSK, JSON.stringify(this.host._kioskScale)); + } catch { + /* ignore */ + } + this.host.requestUpdate(); + } + +public _renderKioskDialog(): TemplateResult { + const k = this.host._kioskScale; + const row = (key: 'icon' | 'font', label: string) => html` +
+ ${this._rangeInput(50, 300, 5, Math.round(k[key] * 100), (n) => this._saveKioskScale({ [key]: n / 100 }))} + ${Math.round(k[key] * 100)}% +
`; + return html` (this.host._kioskDialog = false)}> +
+
${this.host._t('kiosk.hint')}
+ ${row('icon', this.host._t('kiosk.icon_scale'))} + ${row('font', this.host._t('kiosk.font_scale'))} +
+
+ + + +
+
`; + } + +public _renderVacSection(dlg: any): TemplateResult | typeof nothing { + const dev = this.host._devices.find((x) => x.id === dlg.devId); + if (!dev || !this.host._isVacDev(dev)) return nothing; + const v = dev.marker?.vacuum || {}; + const includeAllCameras = this.host._vacAllCamerasFor === dev.id; + const resolution = this.host._vacSourceResolution(dev, includeAllCameras); + const src = resolution.entityId; + const selected = resolution.candidates.find((candidate) => candidate.entityId === src) || null; + const tele = src ? readVacTelemetry(this.host.hass?.states[src]?.attributes) : null; + const canUseSource = v.live !== false + && (resolution.status === 'ok' || resolution.status === 'unsupported'); + const planRooms = this._vacPlanRoomAnchors(dev.space); + const roomMatches = tele + ? vacRoomNameMatchCount(tele.rooms, planRooms.map((room) => room.name)) : 0; + const tierA = !!(canUseSource && tele && roomMatches >= 3); + const cals = Object.keys(v.calibration || {}); + const setVac = (patch: Record) => { + // HP-1540-01: materialise the marker first — find() alone silently + // dropped every edit for a vacuum that had never been saved + const m = this.host._vacEnsureMarker(dev); + if (!m) return; + m.vacuum = { ...(m.vacuum || {}), ...patch }; + this.host._regSignature = ''; + // First-use markers are materialised above, so the dialog must rebuild + // immediately instead of waiting for an unrelated HA state tick. + this.host._maybeRebuildDevices(); + this._saveConfig(); + this.host.requestUpdate(); + }; + const sameDevice = new Set(dev.entities || []); + const primaryCandidates = resolution.candidates.filter((candidate) => + sameDevice.has(candidate.entityId) || candidate.entityId === v.source); + const globalCandidates = includeAllCameras + ? resolution.candidates.filter((candidate) => candidate.entityId.startsWith('camera.') + && !primaryCandidates.some((entry) => entry.entityId === candidate.entityId)) + : []; + const capability = (candidate: VacSourceCandidate) => [ + candidate.hasPosition ? this.host._t('vac.cap_position') : '', + candidate.hasRooms ? this.host._t('vac.cap_rooms_short') : '', + candidate.hasPath ? this.host._t('vac.cap_path') : '', + candidate.hasMapId ? this.host._t('vac.cap_map') : '', + ].filter(Boolean).join(' · ') || this.host._t('vac.cap_none'); + const candidateButton = (candidate: VacSourceCandidate) => html` + `; + const knownSameDeviceXcme = resolution.candidates.some((candidate) => + sameDevice.has(candidate.entityId) && candidate.category === 'known_xcme_incomplete'); + const showXcmeHint = knownSameDeviceXcme + || (!!resolution.pinned && !!src?.startsWith('camera.') && !selected?.hasPosition); + const openPicker = () => { + const picker = [...this.host.renderRoot.querySelectorAll('details.vacpicker')] + .find((details) => details.dataset.devId === dev.id); + if (!picker) return; + picker.open = true; + picker.querySelector('summary')?.focus(); + }; + return html` + +
+
+
${this.host._t('vac.diag_source')}${src || this.host._t('vac.source_none')}
+ ${selected?.platform ? html`
${this.host._t('vac.diag_platform')}${selected.platform}
` : nothing} +
${this.host._t('vac.diag_status')}${this.host._t((`vac.source_status_${resolution.status}`) as any)}
+
${this.host._t('vac.diag_position')}${tele?.pos ? this.host._t('common.yes') : this.host._t('common.no')}
+
${this.host._t('vac.diag_rooms')}${subst(this.host._t('vac.diag_rooms_value'), { + total: String(tele?.rooms.length || 0), matched: String(roomMatches), + readiness: this.host._t(roomMatches >= 3 ? 'vac.autocal_ready' : 'vac.autocal_not_ready'), + })}
+
${this.host._t('vac.diag_path')}${tele?.path.length ? this.host._t('common.yes') : this.host._t('common.no')}
+
${this.host._t('vac.diag_map')}${tele?.mapId ?? 'default'}
+
+ ${(resolution.status === 'missing' || resolution.status === 'disabled' || resolution.status === 'unverified') + ? html`
+ ${this.host._t((`vac.source_banner_${resolution.status}`) as any)} + +
` : nothing} + ${showXcmeHint ? html`
+ ${this.host._t('vac.xcme_hint')} +
attributes:
+  - vacuum_position
+  - rooms
+  - path
+  - map_name
+
` : nothing} +
+ ${this.host._t('vac.choose_source')} +
+ + ${primaryCandidates.map(candidateButton)} +
{ + const open = (event.currentTarget as HTMLDetailsElement).open; + if (open) this.host._vacOpenAllCameras(dev); + else { + this.host._vacAllCamerasFor = null; + this.host._vacAllCameraCache = null; + } + }}> + ${this.host._t('vac.all_cameras')} +
${this.host._t('vac.all_cameras_warn')}
+ ${includeAllCameras + ? (globalCandidates.length ? globalCandidates.map(candidateButton) + : html`
${this.host._t('vac.all_cameras_empty')}
`) + : nothing} +
+
+
+
+ ${tierA ? html`` : nothing} + ${canUseSource + ? html`` + : nothing} + ${this.host._t('vac.documentation')} +
+ ${tele ? html` + + + + ${cals.length ? html`
${subst(this.host._t('vac.cal_maps'), { maps: cals.join(', ') })}
` : nothing} + ` : nothing} +
`; + } + +public _vacMapId(d: DevItem, tele: { mapId: string }, planHass = this.host._planHass): string { + // HP-1541-01: nullish, not truthy — selected_map: 0 is a real map id and + // must equal what trails.py resolve_map_id stores server-side. + const ve = this.host._vacEntity(d); + const sel = ve ? planHass?.states?.[ve]?.attributes?.selected_map : null; + return vacMapIdWithFallback(tele.mapId, sel); + } + +public _vacSaveMatrix(markerId: string, source: string, mapId: string, matrix: Affine): boolean { + // HP-1540-01: a first-use vacuum has no marker yet — materialise it + const dev = this.host._devices.find((x) => x.id === markerId); + const m = dev ? this.host._vacEnsureMarker(dev) + : this.host._serverCfg?.markers?.find((x: Marker) => x.id === markerId); + if (!m) return false; + const v = { ...(m.vacuum || {}) }; + v.source = source; + v.calibration = { ...(v.calibration || {}), [mapId]: matrix.map((n) => Number(n.toFixed(6))) }; + m.vacuum = v; + this.host._regSignature = ''; + this.host._maybeRebuildDevices(); + this._saveConfig(); + this.host.requestUpdate(); + return true; + } + +public _vacPlanRoomAnchors(spaceId: string | null | undefined): Array<{ + name: string; cx: number; cy: number; + }> { + return (this.host._spaceModelById(spaceId)?.rooms || []) + // HP-1540-04: legacy rectangle rooms (x/y/w/h) are still first-class; + // roomPoly() gives the same outline the renderer uses for them. + .map((room) => ({ room, poly: roomPoly(room) })) + .filter(({ room, poly }: any) => room.name && poly) + .map(({ room, poly }: any) => { + const centroid = areaCentroid(poly); + return centroid ? { name: String(room.name), cx: centroid[0], cy: centroid[1] } : null; + }) + .filter(Boolean) as Array<{ name: string; cx: number; cy: number }>; + } + +public _vacAutoCalibrate(d: DevItem): void { + const src = this.host._vacSource(d); + const tele = src ? readVacTelemetry(this.host.hass?.states[src]?.attributes) : null; + if (!src || !tele || tele.rooms.length < 3) { + this.host._showToast(this.host._t('vac.autocal_no_rooms')); + return; + } + const planRooms = this._vacPlanRoomAnchors(d.space); + const res = autoCalibrate(tele.rooms, planRooms); + if (!res) { + this.host._showToast(this.host._t('vac.autocal_no_match')); + return; + } + const rawSpace = this.host._serverCfg?.spaces?.find((space) => space.id === d.space); + const rawCellCm = Number(rawSpace?.cell_cm); + const cellCm = Number.isFinite(rawCellCm) && rawCellCm > 0 ? rawCellCm : 5; + const residualCm = vacCalibrationResidualCm(res.residual, this.host._gridPitch, cellCm); + const mapId = this._vacMapId(d, tele); + if (residualCm > VAC_CALIBRATION_WARN_CM) { + this.host._vacCalConfirm = { + markerId: d.id, source: src, mapId, matrix: res.matrix, + rooms: res.matched.length, + error: formatLength(residualCm, this.host.hass?.config?.unit_system?.length === 'mi'), + }; + return; + } + if (!this._vacSaveMatrix(d.id, src, mapId, res.matrix)) return; + this.host._showToast(subst(this.host._t('vac.autocal_done'), { rooms: String(res.matched.length) })); + } + +public _vacApplyCalibrationProposal(manual: boolean): void { + const proposal = this.host._vacCalConfirm; + if (!proposal) return; + this.host._vacCalConfirm = null; + if (manual) { + const dev = this.host._devices.find((candidate) => candidate.id === proposal.markerId); + const fit = fitFromMatrix(proposal.matrix); + if (!dev || !fit) return; + this.host._markerDialog = null; + if (dev.space !== this.host._space && !this.host._commitSpace(dev.space)) return; + this.host._vacFit = { + markerId: proposal.markerId, source: proposal.source, + mapId: proposal.mapId, p: fit, drag: null, + }; + return; + } + if (this._vacSaveMatrix( + proposal.markerId, proposal.source, proposal.mapId, proposal.matrix, + )) this.host._showToast(subst(this.host._t('vac.autocal_done'), { rooms: String(proposal.rooms) })); + } + +public _vacStartFit(d: DevItem): void { + const src = this.host._vacSource(d); + const tele = src ? readVacTelemetry(this.host.hass?.states[src]?.attributes) : null; + if (!src || !tele) { + this.host._showToast(this.host._t('vac.cal_need_pos')); + return; + } + const mapId = this._vacMapId(d, tele); + const existing = d.marker?.vacuum?.calibration?.[mapId] as Affine | undefined; + const sp = this.host._spaceModelById(d.space); + if (!sp) return; + const vb = sp.vb as [number, number, number, number]; + const p = (existing && existing.length === 6 && fitFromMatrix(existing)) + || initialFit(tele.rooms, vb); + this.host._markerDialog = null; + if (d.space !== this.host._space && !this.host._commitSpace(d.space)) return; + this.host._vacFit = { markerId: d.id, source: src, mapId, p, drag: null }; + } + +public _vacFitSave(): void { + const f = this.host._vacFit; + if (!f) return; + // HP-1540-01: no success toast for a save that did not happen + const ok = this._vacSaveMatrix(f.markerId, f.source, f.mapId, fitMatrix(f.p)); + this.host._vacFit = null; + if (ok) this.host._showToast(this.host._t('vac.cal_done')); + } + +public _vacFitTurn(patch: Partial): void { + const f = this.host._vacFit; + if (!f) return; + const tele = readVacTelemetry(this.host.hass?.states[f.source]?.attributes); + const c = this._vacGhostCentre(tele?.rooms || []); + const next = { ...f.p, ...patch } as FitParams; + this.host._vacFit = { ...f, p: reanchorFit(next, f.p, c[0], c[1]) }; + } + +public _vacGhostCentre(rooms: VacRoom[]): VacPt { + const xs: number[] = [], ys: number[] = []; + for (const r of rooms) { + xs.push(r.x0 ?? r.cx, r.x1 ?? r.cx); + ys.push(r.y0 ?? r.cy, r.y1 ?? r.cy); + } + if (!xs.length) return [0, 0]; + return [(Math.min(...xs) + Math.max(...xs)) / 2, (Math.min(...ys) + Math.max(...ys)) / 2]; + } + +public _vacDelta(view: { w: number; h: number }, dxPx: number, dyPx: number): VacPt { + const st = this.host._stageEl; + const w = st?.clientWidth || 1, h = st?.clientHeight || 1; + return [(dxPx / w) * view.w, (dyPx / h) * view.h]; + } + +public _vacFitPointer(ev: PointerEvent, view: { x: number; y: number; w: number; h: number }): void { + const f = this.host._vacFit; + if (!f) return; + ev.stopPropagation(); + if (ev.type === 'pointerdown') { + const t = ev.target as HTMLElement; + const corner = t.getAttribute?.('data-corner'); + try { + (ev.currentTarget as HTMLElement).setPointerCapture?.(ev.pointerId); + } catch { /* synthetic pointers (tests) have no active id — capture is a nicety */ } + this.host._vacFit = { ...f, drag: corner + ? { kind: 'scale', sx: ev.clientX, sy: ev.clientY, p0: { ...f.p }, + fx: Number(corner.split(',')[0]), fy: Number(corner.split(',')[1]) } + : { kind: 'move', sx: ev.clientX, sy: ev.clientY, p0: { ...f.p }, fx: 0, fy: 0 } }; + return; + } + const d = f.drag; + if (!d) return; + if (ev.type === 'pointermove') { + const [dx, dy] = this._vacDelta(view, ev.clientX - d.sx, ev.clientY - d.sy); + if (d.kind === 'move') { + this.host._vacFit = { ...f, p: { ...d.p0, ox: d.p0.ox + dx, oy: d.p0.oy + dy } }; + } else { + // corner-stretch: uniform scale about the OPPOSITE corner (fx, fy — + // the fixed corner in robot coords), like a graphics-editor frame + const tele = readVacTelemetry(this.host.hass?.states[f.source]?.attributes); + const c = this._vacGhostCentre(tele?.rooms || []); + const m0 = fitMatrix(d.p0); + const [gx, gy] = applyAffine(m0, c[0], c[1]); + const [px0, py0] = applyAffine(m0, d.fx, d.fy); + const span0 = Math.hypot(gx - px0, gy - py0) || 1; + // distance change of the dragged corner (opposite of fixed) from centre + const [cx0, cy0] = [2 * gx - px0, 2 * gy - py0]; + const span1 = Math.hypot(cx0 + dx * 2 - px0, cy0 + dy * 2 - py0) / 2; + const k = Math.max(0.05, span1 / span0); + const next = { ...d.p0, s: d.p0.s * k } as FitParams; + this.host._vacFit = { ...f, p: reanchorFit(next, d.p0, d.fx, d.fy) }; + } + return; + } + if (ev.type === 'pointerup' || ev.type === 'pointercancel') { + this.host._vacFit = { ...f, drag: null }; + } + } + +public _renderVacFit(view: { x: number; y: number; w: number; h: number }): TemplateResult | typeof nothing { + const f = this.host._vacFit; + if (!f) return nothing; + const tele = readVacTelemetry(this.host._renderPlanHass?.states?.[f.source]?.attributes); + if (!tele) return nothing; + const m = fitMatrix(f.p); + const rects: TemplateResult[] = []; + const xs: number[] = [], ys: number[] = []; + for (const r of tele.rooms) { + if (r.x0 == null) continue; + const cs = [[r.x0!, r.y0!], [r.x1!, r.y0!], [r.x1!, r.y1!], [r.x0!, r.y1!]] + .map(([x, y]) => applyAffine(m, x, y)); + cs.forEach(([x, y]) => { xs.push(x); ys.push(y); }); + const [lx, ly] = applyAffine(m, r.cx, r.cy); + rects.push(svg` + ${r.name}`); + } + let dot: TemplateResult | typeof nothing = nothing; + if (tele.pos) { + const [dx2, dy2] = applyAffine(m, tele.pos.x, tele.pos.y); + dot = svg``; + } + // corner handles on the ghost bbox; data-corner carries the FIXED corner + // (the opposite one) in robot coordinates + const handles: TemplateResult[] = []; + if (xs.length) { + const inv = ((): ((x: number, y: number) => VacPt) => { + const det = m[0] * m[4] - m[1] * m[3]; + return (x, y) => [ + (m[4] * (x - m[2]) - m[1] * (y - m[5])) / det, + (-m[3] * (x - m[2]) + m[0] * (y - m[5])) / det, + ]; + })(); + const x0 = Math.min(...xs), x1 = Math.max(...xs); + const y0 = Math.min(...ys), y1 = Math.max(...ys); + // hit radius (finger-sized: these are grabbed on tablets) and the bead + // you actually see, a quarter of it — same split as .bdhandle/.bdknob + const r = view.w * 0.022; + const kr = r / 4; + for (const [hx, hy, ox2, oy2] of [[x0, y0, x1, y1], [x1, y0, x0, y1], [x1, y1, x0, y0], [x0, y1, x1, y0]] as number[][]) { + const fixed = inv(ox2, oy2); + handles.push(svg` + `); + } + } + return html` this._vacFitPointer(e, view)} + @pointermove=${(e: PointerEvent) => this._vacFitPointer(e, view)} + @pointerup=${(e: PointerEvent) => this._vacFitPointer(e, view)} + @pointercancel=${(e: PointerEvent) => this._vacFitPointer(e, view)}>${rects}${dot}${handles}`; + } + +public _resetRoomDialogFields(): void { + this.host._roomEditId = null; + this.host._roomFill = ''; + this.host._roomCustomFill = null; + this.host._roomTempSrc = ''; + this.host._roomHumSrc = ''; + this.host._roomSrcOpen = null; + this.host._roomSrcFilter = ''; + this.host._roomNameScale = 1; + this.host._roomLabelScale = 1; + } + +public _openRoomEdit(r: RoomCfg): void { + if (!r.id) return; + this.host._roomEditId = r.id; + this.host._nameSel = r.name || ''; + this.host._areaSel = r.area || ''; + this.host._roomFill = r.settings?.fill_mode === 'glow' + ? '' + : ((r.settings?.fill_mode as any) || ''); + const rawCustom = r.settings?.custom_fill; + this.host._roomCustomFill = rawCustom && typeof rawCustom === 'object' + ? customFillOf(rawCustom, spaceDisplayOf(this.host._curSpaceCfg).customFill) + : null; + this.host._roomTempSrc = r.settings?.temp_source || ''; + this.host._roomHumSrc = r.settings?.hum_source || ''; + this.host._roomNameScale = clampScale(r.settings?.name_scale); + this.host._roomLabelScale = clampScale(r.settings?.label_scale); + this.host._roomSrcOpen = null; + this.host._roomSrcFilter = ''; + this.host._roomDialog = true; + } + +public _roomSettingsFromDialog(): RoomCfg['settings'] { + const st: any = {}; + if (this.host._roomFill) st.fill_mode = this.host._roomFill; + if (this.host._roomCustomFill) st.custom_fill = this.host._roomCustomFill; + if (this.host._roomTempSrc) st.temp_source = this.host._roomTempSrc; + if (this.host._roomHumSrc) st.hum_source = this.host._roomHumSrc; + if (this.host._roomNameScale !== 1) st.name_scale = this.host._roomNameScale; + if (this.host._roomLabelScale !== 1) st.label_scale = this.host._roomLabelScale; + return Object.keys(st).length ? st : null; + } + +public _saveRoomEdit(): void { + const sp = this.host._curSpaceCfg; + const room = sp?.rooms.find((x: any) => x.id === this.host._roomEditId); + if (!room) { + this.host._roomDialog = false; + this.host._roomEditId = null; + return; + } + room.name = this.host._nameSel.trim() || room.name; + room.area = this.host._areaSel || null; + // Preserve unknown/future room settings. If this dialog replaces a legacy + // fill_mode:'glow', materialize its effective Glow in the same write so a + // seemingly unrelated room edit cannot switch the light overlay off. + const previous = room.settings || {}; + const next: any = { ...previous }; + if (this.host._roomFill) next.fill_mode = this.host._roomFill; + else delete next.fill_mode; + if (this.host._roomCustomFill) next.custom_fill = this.host._roomCustomFill; + else delete next.custom_fill; + if (previous.fill_mode === 'glow' && typeof previous.glow !== 'boolean') next.glow = true; + if (this.host._roomTempSrc) next.temp_source = this.host._roomTempSrc; + else delete next.temp_source; + if (this.host._roomHumSrc) next.hum_source = this.host._roomHumSrc; + else delete next.hum_source; + if (this.host._roomNameScale !== 1) next.name_scale = this.host._roomNameScale; + else delete next.name_scale; + if (this.host._roomLabelScale !== 1) next.label_scale = this.host._roomLabelScale; + else delete next.label_scale; + if (Object.keys(next).length) room.settings = next; + else delete room.settings; + this._saveConfig(); + this.host._roomDialog = false; + this.host._roomEditId = null; + this.host._nameSel = ''; + this.host._areaSel = ''; + this.host._regSignature = ''; + this.host._maybeRebuildDevices(); + this.host.requestUpdate(); + this.host._showToast(this.host._t('toast.room_updated')); + } + +public _roomSrcCandidates(): { value: string; label: string; sub: string }[] { + const h = this.host._planHass; + const removed = removedPlanBindings(this.host._markers); + const q = this.host._roomSrcFilter.trim().toLowerCase(); + const list: { value: string; label: string; sub: string }[] = []; + for (const dev of Object.values(h.devices)) { + if (dev.entry_type === 'service') continue; + if (removed.devices.has(dev.id)) continue; + const name = (dev.name_by_user || dev.name || dev.id).trim(); + if (q && !name.toLowerCase().includes(q)) continue; + list.push({ value: 'device:' + dev.id, label: name, sub: dev.model || this.host._t('marker.sub_device') }); + } + for (const [eid, reg] of Object.entries(h.entities)) { + if (!eid.startsWith('sensor.') || reg.hidden) continue; + if (isRemovedPlanEntity(h, eid, removed)) continue; + const label = reg.name || h.states[eid]?.attributes?.friendly_name || eid; + if (q && !(label + ' ' + eid).toLowerCase().includes(q)) continue; + list.push({ value: 'entity:' + eid, label, sub: eid }); + } + list.sort((a, b) => a.label.localeCompare(b.label)); + return list.slice(0, 200); + } + +public _roomSrcLabel(src: string): string { + const i = src.indexOf(':'); + const k = src.slice(0, i); + const ref = src.slice(i + 1); + if (k === 'device') { + return this.host._fullRegistryHass.devices[ref]?.name_by_user + || this.host._fullRegistryHass.devices[ref]?.name || ref; + } + return this.host._fullRegistryHass.entities[ref]?.name + || this.host.hass.states[ref]?.attributes?.friendly_name || ref; + } + +public _labelPos(r: RoomCfg, spaceId: string): { x: number; y: number } { + const saved = this.host._layout['rl_' + (r.id || '')]; + if (saved && saved.s === spaceId) { + return { x: saved.x * NORM_W, y: saved.y * NORM_W }; + } + // a label nobody has dragged sits at the room's CENTROID, which is not a + // node for an odd-sized or polygonal room — put it on the nearest one + const c = this._snap(this.host._roomCenter(r)); + return { x: c[0], y: c[1] }; + } + +public _labelDown(ev: PointerEvent, r: RoomCfg, spaceId: string): void { + if (this.host._mode !== 'plan') return; + ev.preventDefault(); + ev.stopPropagation(); + const p = this._labelPos(r, spaceId); + this.host._drag = { id: 'rl_' + (r.id || ''), sx: ev.clientX, sy: ev.clientY, ox: p.x, oy: p.y, moved: false }; + capturePointer(ev); + this.host._tip = null; + } + +public _labelMove(ev: PointerEvent, r: RoomCfg, spaceId: string): void { + const id = 'rl_' + (r.id || ''); + if (!this.host._drag || this.host._drag.id !== id) return; + const stage = this.host._stageEl; + if (!stage) return; + const space = this.host._spaceModelById(spaceId); + if (!space) return; + const vb = space.vb; + const rect = stage.getBoundingClientRect(); + const v = this.host._viewOr(vb); + const dx = ((ev.clientX - this.host._drag.sx) / rect.width) * v.w; + const dy = ((ev.clientY - this.host._drag.sy) / rect.height) * v.h; + if (Math.abs(ev.clientX - this.host._drag.sx) + Math.abs(ev.clientY - this.host._drag.sy) > 3) this.host._drag.moved = true; + // DEV-B58-01, and worse than the marker's: this clamped to the space's + // STORED view_box, which for every existing plan is the old unit square. + // A room drawn at 2.5 had a name that could not be dragged to its own room. + const nx = clampCanvasR(this.host._drag.ox + dx); + const ny = clampCanvasR(this.host._drag.oy + dy); + this.host._savePos({ id, space: spaceId } as DevItem, nx, ny); + } + +public _labelUp(r: RoomCfg): void { + const id = 'rl_' + (r.id || ''); + if (!this.host._drag || this.host._drag.id !== id) return; + const moved = this.host._drag.moved; + this.host._drag = moved ? this.host._drag : null; + if (moved) window.setTimeout(() => (this.host._drag = null), 0); + } + +public _labelScale(r: RoomCfg): number { + const k = (this.host._layout['rl_' + (r.id || '')] as any)?.k; + return typeof k === 'number' && Number.isFinite(k) ? Math.min(3, Math.max(0.5, k)) : 1; + } + +public _rlResizeDown(ev: PointerEvent, r: RoomCfg, spaceId: string): void { + if (this.host._mode !== 'plan') return; + ev.preventDefault(); + ev.stopPropagation(); + const card = (ev.target as HTMLElement).closest('.roomlabel') as HTMLElement | null; + if (!card) return; + const b = card.getBoundingClientRect(); + const cx = b.left + b.width / 2; + const cy = b.top + b.height / 2; + const d0 = Math.max(8, Math.hypot(ev.clientX - cx, ev.clientY - cy)); + this.host._rlResize = { id: 'rl_' + (r.id || ''), space: spaceId, k0: this._labelScale(r), cx, cy, d0 }; + capturePointer(ev); + } + +public _rlResizeMove(ev: PointerEvent): void { + const rs = this.host._rlResize; + if (!rs) return; + ev.stopPropagation(); + const dist = Math.max(8, Math.hypot(ev.clientX - rs.cx, ev.clientY - rs.cy)); + const k = Math.min(3, Math.max(0.5, rs.k0 * (dist / rs.d0))); + const rec: any = this.host._layout[rs.id]; + if (!rec) { + // the card was never dragged: pin its current default position first + const roomId = rs.id.slice(3); + const sp = this.host._spaceModelById(rs.space); + if (!sp) return; + const room = sp.rooms.find((x) => x.id === roomId); + if (!room) return; + const p = this._labelPos(room, rs.space); + + this.host._layout = { + ...this.host._layout, + [rs.id]: { s: rs.space, x: p.x / NORM_W, y: p.y / NORM_W, k }, + }; + } else { + this.host._layout = { ...this.host._layout, [rs.id]: { ...rec, k } }; + } + this.host._dirtyPos.add(rs.id); + } + +public _rlResizeUp(): void { + if (!this.host._rlResize) return; + this.host._rlResize = null; + this.host._persistLayout(); + } + +public _renderRoomGear( + r: RoomCfg, space: SpaceModel, view: { x: number; y: number; w: number; h: number }, + ): TemplateResult | typeof nothing { + if (!r.id) return nothing; + let c: number[] | null = null; + if (r.poly) { + // the VISUAL centre (largest inscribed circle) — interiorPoint only + // promises "inside", which sat visibly off-centre on an L-shaped room. + // The model is memoized, so the poly array is a stable cache key. + c = this.host._gearPtCache.get(r.poly) || null; + if (!c) { c = poleOfInaccessibility(r.poly); this.host._gearPtCache.set(r.poly, c); } + } else if (r.x != null && r.y != null) { + c = [r.x + (r.w || 0) / 2, r.y + (r.h || 0) / 2]; + } + if (!c) return nothing; + const left = ((c[0] - view.x) / view.w) * 100; + const top = ((c[1] - view.y) / view.h) * 100; + return html``; + } + +public _alignCandidates(): number[][] { + const out: number[][] = []; + const spm = this.host._spaceModel(); + if (this.host._markup) { + if (!spm) return out; + if (this.host._drag?.id.startsWith('rl_')) { + // room-card drag: centers of the OTHER room cards + const dragged = this.host._drag.id.slice(3); + for (const r of spm.rooms) { + if (!r.name || r.id === dragged) continue; + const p = this._labelPos(r, this.host._space); + out.push([p.x, p.y]); + } + return out; + } + // drawing: room vertices + current path/split points + for (const r of spm.rooms) { + const poly = roomPoly(r); + if (poly) for (const p of poly) out.push(p); + } + if (this.host._tool === 'draw') for (const p of this.host._path) out.push(p); + if (this.host._tool === 'split' && this.host._splitSel?.pts) for (const p of this.host._splitSel.pts) out.push(p); + return out; + } + if (this.host._mode === 'devices') { + // other icons of this space only (owner's decision) + for (const d of this.host._devices) { + if (d.space !== this.host._space || d.id === this.host._drag?.id + || d.bindingStatus?.kind === 'ha_disabled') continue; + const p = this.host._pos(d); + out.push([p.x, p.y]); + } + return out; + } + if (this.host._mode === 'decor') { + const movingId = this.host._decorMove?.id; + out.push(...this._decorSnapGeometry(movingId).points); + if (this.host._decorDraft) out.push(this.host._decorDraft.a); + return out; + } + return out; + } + +public _renderAlignGuides(): TemplateResult { + const pt = this.host._alignPoint; + if (!pt) return svg`` as unknown as TemplateResult; + // exact node match for grid-snapped things; half a cell for free-moving cards + const tol = this.host._drag?.id.startsWith('rl_') ? this.host._gridPitch * 0.5 : this.host._gridPitch * 0.05; + const guides = alignGuides(pt, this._alignCandidates(), tol); + if (!guides.length) return svg`` as unknown as TemplateResult; + const g = this.host._gridPitch; + const over = g * 1.5; // extend a little past the point + return svg` + ${guides.map((gd: AlignGuide) => { + const [x1, y1, x2, y2] = gd.axis === 'x' + ? [gd.at, gd.from[1], gd.at, pt[1] + Math.sign(pt[1] - gd.from[1]) * over] + : [gd.from[0], gd.at, pt[0] + Math.sign(pt[0] - gd.from[0]) * over, gd.at]; + return svg` + `; + })} + ` as unknown as TemplateResult; + } + +public _renderOpeningCenterTick(gd: { x: number; y: number; angle: number }): TemplateResult { + const rad = ((gd.angle + 90) * Math.PI) / 180; + const half = gridVisualUnits(2.5 * 6, this.host._cellCm); + return svg`` as unknown as TemplateResult; + } + +public _renderOpeningDimensionGuides(measure: OpMeasure): TemplateResult { + const dimensions = measure.labels.flatMap((label) => label.dimension ? [label.dimension] : []); + if (!dimensions.length) return svg`` as unknown as TemplateResult; + const tickHalf = this._cssPxToRender(4); + return svg`` as unknown as TemplateResult; + } + +public _renderOpeningPlacementPreview(): TemplateResult { + const candidate = this.host._openingPreview; + if (!candidate) return svg``; + const visibleSpec: OpeningVisibleSpec = { + type: candidate.type, + length: candidate.renderedLength, + angle: candidate.angle, + amount: openingAmount(candidate.type, null), + flipH: candidate.flipH, + flipV: candidate.flipV, + base: 'var(--hp-open)', + tone: 'var(--hp-open)', + cellCm: this.host._cellCm, + gridPitch: this.host._gridPitch, + face: candidate.face, + }; + const passageGeometry = candidate.type === 'passage' + ? passagePlacementPreviewGeometry( + candidate, gridVisualUnits(this.host._gridPitch, this.host._cellCm), + ) + : null; + return svg` + ` as unknown as TemplateResult; + } + +public _renderOpeningDialog(): TemplateResult { + const d = this.host._openingDialog!; + const partitions = this.host._spaceModel()?.partitions || []; + const previous = d.id + ? this.host._curSpaceCfg?.openings?.find((item: OpeningCfg) => item.id === d.id) + : null; + const dialogOpening: OpeningCfg = { + id: d.id || 'preview', type: d.type, + x: d.x / NORM_W, y: d.y / this.host._spaceH, + angle: d.angle, + length: previous && !d.lengthTouched + ? previous.length + : this.host._cmToUnits(Math.max(20, d.lengthCm)) / NORM_W, + ...(d.host ? { host: d.host } : {}), + }; + const strict = partitionOpeningNeedsStrictValidation(previous, dialogOpening); + const hostResolution = d.host + ? strict + ? resolvePartitionOpeningStrict( + dialogOpening, partitions, NORM_W, this.host._cellCm, this.host._gridPitch, + ) + : resolvePartitionOpeningCompat( + dialogOpening, partitions, NORM_W, this.host._cellCm, this.host._gridPitch, + ) + : null; + const jambInvalid = hostResolution?.reason === 'does-not-fit-jamb'; + const hostPartition = d.host + ? partitions.find((partition) => partition.id === d.host!.id) + : null; + const jambDistance = hostPartition + ? formatLength(hostPartition.cm / 2, this.host._imperial) + : ''; + const orphan = !!d.host && !hostResolution?.resolved && !jambInvalid; + const icon = d.type === 'gate' ? 'mdi:gate' + : d.type === 'window' ? 'mdi:window-closed-variant' + : d.type === 'passage' ? 'mdi:arch' : 'mdi:door'; + const picker = (kind: 'contact' | 'lock', list: { value: string; label: string }[]) => { + const cur = kind === 'contact' ? d.contact : d.lock; + const open = kind === 'contact' ? !!d.contactOpen : !!d.lockOpen; + const filter = kind === 'contact' ? d.contactFilter || '' : d.lockFilter || ''; + const selected = list.find((candidate) => candidate.value === cur); + const selectedLabel = selected?.label + || this.host.hass.states[cur]?.attributes?.friendly_name + || this.host._fullRegistryHass.entities[cur]?.name + || cur; + const filtered = filterOpeningEntityCandidates(list, filter); + return html` + + ${open + ? html`
+ this._filterOpeningEntities( + kind, (e.target as HTMLInputElement).value, + )} /> +
+ + ${filtered.map((candidate) => html` + `)} + ${!filtered.length + ? html`
${this.host._t('marker.nothing_found')}
` + : nothing} +
+
` + : nothing}`; + }; + return html` (this.host._openingDialog = null)}> +
+ ${d.host ? html` +
+ ${orphan || jambInvalid ? html`` : nothing} + ${orphan + ? this.host._t('opening.partition_orphan') + : jambInvalid + ? this.host._t('opening.partition_jamb_margin', { distance: jambDistance }) + : this.host._t('opening.host_partition')} +
` : nothing} + + + + + + + + { + const n = strictNumber((e.target as HTMLInputElement).value); + if (n != null) this.host._openingDialog = { ...d, lengthCm: n, lengthTouched: true }; + }} /> + + ${d.type === 'passage' && (d.contact || d.lock) + ? html`
+ + ${this.host._t('opening.passage_binding_warning')} +
` + : nothing} + + ${d.type !== 'passage' + ? html` + ${picker('contact', this._contactCandidates())} + ${d.contact + ? html`` + : nothing}` + : nothing} + + ${d.type === 'door' || d.type === 'gate' + ? html` + ${picker('lock', this._lockCandidates())}` + : nothing} + + ${d.type !== 'gate' && d.type !== 'passage' + ? html`` + : nothing} + ${d.type !== 'passage' + ? html`` + : nothing} +
+ +
`; + } + +public _gridLevels(): { fine: number; coarse: number } | null { + const stage = this.host._stageEl; + const v = this.host._viewOr(this.host._baseVb()); + const px = stage && stage.clientWidth && v.w ? stage.clientWidth / v.w : 1; + return gridLevels(this.host._gridPitch, px); + } + +public _renderMarkupDefs(_vb: number[]): TemplateResult { + const lv = this._gridLevels(); + if (!lv) return svg`` as unknown as TemplateResult; + const g = this.host._gridPitch * lv.fine; + const G = this.host._gridPitch * lv.coarse; + const dotR = this.host._gridPitch * lv.fine * 0.14; + // Two patterns, CAD-style: the fine dots thin out as you zoom away + // (gridLevels drops whole decades of them) while every coarse node keeps + // a bigger, darker dot, so the eye never loses the scale reference. + return svg` + + + + + + + + + + + + + + `; + } + +public _renderPhysicalEditorLayer(): TemplateResult { + const space = this.host._spaceModel(); + if (!space) return svg`` as unknown as TemplateResult; + const g = this.host._gridPitch; + const view = this.host._viewOr(this.host._baseVb()); + const stage = this.host._stageEl; + const unitsPerPx = stage?.clientWidth ? view.w / stage.clientWidth : g / 8; + const touchStroke = 24; + const handleR = Math.max(g * 0.22, unitsPerPx * 8); + const rotateHandleR = Math.max(handleR, unitsPerPx * 12); + const path = (poly: number[][]) => `M ${poly.map((p) => `${p[0]} ${p[1]}`).join(' L ')} Z`; + const selected = (kind: string, id: string) => + (this.host._physicalSel?.kind === kind && this.host._physicalSel.id === id) + || (kind === 'column' && this.host._duplicateColumnId === id); + const draftSegs = (space.room_drafts || []).flatMap((d) => d.points.slice(0, -1).map((a, i) => { + const b = d.points[i + 1]; + const storedCm = Number(d.segments[i]?.cm); + const depth = wallCmToUnits( + Number.isFinite(storedCm) ? storedCm : 15, this.host._cellCm, this.host._gridPitch, + ); + return svg` { + this.host._physicalSel = { kind: 'draft', id: d.id, segment: i }; + this._registerPhysicalTap('draft', d.id, i); + }}>`; + })); + const partitions = (space.partitions || []).map((p) => { + const body = partitionBody(p.a, p.b, p.cm, this.host._cellCm, this.host._gridPitch); + return body + ? svg` this._physicalDown(e, 'partition', p.id)} + @pointermove=${(e: PointerEvent) => this._physicalMove(e)} + @pointerup=${(e: PointerEvent) => this._physicalUp(e)}>` + : svg` this._physicalDown(e, 'partition', p.id)} + @pointermove=${(e: PointerEvent) => this._physicalMove(e)} + @pointerup=${(e: PointerEvent) => this._physicalUp(e)}>`; + }); + const columns = (space.wall_columns || []).map((c) => { + const shown = this.host._physicalRotate?.id === c.id + ? { ...c, angle: this.host._physicalRotate.angle } as WallColumnCfg : c; + const body = columnBody(shown, this.host._cellCm, this.host._gridPitch); + return svg` this._physicalDown(e, 'column', c.id)} + @pointermove=${(e: PointerEvent) => this._physicalMove(e)} + @pointerup=${(e: PointerEvent) => this._physicalUp(e)}>`; + }); + const drag = this.host._physicalDrag; + const ghost = (() => { + if (!drag?.moved) return nothing; + if (drag.kind === 'partition' && Number((drag.base as PartitionCfg).cm) === 0) { + const partition = drag.base as PartitionCfg; + return svg``; + } + const poly = drag.kind === 'partition' + ? partitionBody((drag.base as PartitionCfg).a, (drag.base as PartitionCfg).b, + (drag.base as PartitionCfg).cm, this.host._cellCm, this.host._gridPitch) + : columnBody(drag.base as WallColumnCfg, this.host._cellCm, this.host._gridPitch); + return poly ? svg`` : nothing; + })(); + const chrome = (() => { + const sel = this.host._physicalSel; + if (!sel) return nothing; + if (sel.kind === 'draft') { + const d = space.room_drafts.find((x) => x.id === sel.id); + if (!d) return nothing; + return svg` + p.join(',')).join(' ')}> + ${d.points.map((p) => svg``)} + `; + } + if (sel.kind === 'partition') { + const p = space.partitions.find((x) => x.id === sel.id); + if (!p) return nothing; + const body = partitionBody(p.a, p.b, p.cm, this.host._cellCm, this.host._gridPitch); + const mx = (p.a[0] + p.b[0]) / 2, my = (p.a[1] + p.b[1]) / 2; + return svg` + ${body + ? svg`` + : svg``} + + + + `; + } + const base = space.wall_columns.find((x) => x.id === sel.id); + if (!base) return nothing; + const c = this.host._physicalRotate?.id === base.id + ? { ...base, angle: this.host._physicalRotate.angle } as WallColumnCfg : base; + const body = columnBody(c, this.host._cellCm, this.host._gridPitch); + if (c.shape !== 'square') return svg` + + + `; + const size = wallCmToUnits(c.cm, this.host._cellCm, this.host._gridPitch); + const angle = canonicalColumnAngle(c.angle) * Math.PI / 180; + const ux = Math.sin(angle), uy = -Math.cos(angle); + const edge = [c.center[0] + ux * size / 2, c.center[1] + uy * size / 2]; + const handle = [edge[0] + ux * Math.max(g, unitsPerPx * 24), + edge[1] + uy * Math.max(g, unitsPerPx * 24)]; + return svg` + + + + this._physicalRotateDown(e, base)} + @pointermove=${(e: PointerEvent) => this._physicalRotateMove(e)} + @pointerup=${(e: PointerEvent) => this._physicalRotateUp(e)}> + `; + })(); + return svg`${draftSegs}${partitions}${columns}${ghost}${chrome}`; + } + +public _renderHiddenWallDiagnosticOverlay(): TemplateResult { + if (!this.host._markup) return svg`` as unknown as TemplateResult; + const geometry = this._hiddenWallDiagnosticSnapshot().value; + if (!geometry.segments.length) return svg`` as unknown as TemplateResult; + const radius = wallCmToUnits(5, this.host._cellCm, this.host._gridPitch); + return svg`` as unknown as TemplateResult; + } + +public _renderPlanSnapOverlay(): TemplateResult { + if (!this.host._markup) { + return svg`` as unknown as TemplateResult; + } + const geometry = this._planSnapGeometrySnapshot().value; + const active = this.host._activePlanSnapCandidate; + const conflictKeys = new Set(this.host._activePlanSnapConflicts.map((item) => item.key)); + const staticRadius = wallCmToUnits(5, this.host._cellCm, this.host._gridPitch); + const activeRadius = wallCmToUnits(10, this.host._cellCm, this.host._gridPitch); + return svg`` as unknown as TemplateResult; + } + +public _syncPlanSnapActiveMarker(candidate: PlanSnapCandidate | null): void { + const marker = this.host.renderRoot?.querySelector( + '[data-hp="plan-snap-active-marker"]', + ); + if (!marker) return; + marker.setAttribute('class', `plan-snap-node${candidate ? ' active' : ''}${ + candidate?.kind === 'line' ? ' dynamic' : '' + }`); + marker.setAttribute('data-active', candidate ? 'true' : 'false'); + marker.setAttribute('visibility', candidate ? 'visible' : 'hidden'); + if (!candidate) { + marker.removeAttribute('data-kind'); + marker.removeAttribute('data-key'); + return; + } + marker.setAttribute('data-kind', candidate.kind); + marker.setAttribute('data-key', candidate.key); + marker.setAttribute('cx', String(candidate.point[0])); + marker.setAttribute('cy', String(candidate.point[1])); + marker.setAttribute('r', String(wallCmToUnits(10, this.host._cellCm, this.host._gridPitch))); + } + +public _syncPlanSnapConflictMarkers(conflicts: readonly PlanSnapEndpoint[]): void { + const keys = new Set(conflicts.map((item) => item.key)); + for (const marker of this.host.renderRoot?.querySelectorAll( + '.plan-snap-node[data-kind="endpoint"]', + ) || []) { + marker.setAttribute('class', `plan-snap-node${keys.has(marker.dataset.key || '') ? ' conflict' : ''}`); + } + } + +public _planSnapPhysicalSegment(segment: PlanSnapSegment): LinearWallSegment | null { + let cm = 0; + const space = this.host._spaceModel(); + if (!space) return null; + if (segment.sourceKind === 'partition') { + cm = Number(space.partitions.find((item) => item.id === segment.sourceId)?.cm) || 0; + } else if (segment.sourceKind === 'draft') { + const match = /^(.*):(\d+)$/.exec(segment.sourceId); + const draft = match ? space.room_drafts.find((item) => item.id === match[1]) : null; + cm = draft && match ? Number(draft.segments[Number(match[2])]?.cm) || 0 : 0; + } else { + cm = intervalCmAt( + space.rooms, this.host._spaceWalls, this.host._openCuts(), + [segment.a[0], segment.a[1], segment.b[0], segment.b[1]], + this.host._wallKeyPitch, this.host._cellCm, this.host._gridPitch, NORM_W, + ); + } + if (!(cm > 0)) return null; + return { + a: [...segment.a], b: [...segment.b], + halfDepth: wallCmToUnits(cm, this.host._cellCm, this.host._gridPitch) / 2, + }; + } + +public _drawPreviewJoinPatchD( + points: number[][], halfDepths: number[], + ): string { + if (points.length < 2) return ''; + const preview: LinearWallSegment[] = []; + for (let i = 0; i + 1 < points.length; i++) { + if (!(halfDepths[i] > 0)) continue; + preview.push({ a: points[i], b: points[i + 1], halfDepth: halfDepths[i] }); + } + if (!preview.length) return ''; + const eps = this.host._gridPitch * 0.0002; + const touching = this._planSnapGeometrySnapshot().value.segments + .filter((segment) => points.some((point) => distToSegment(point, [ + segment.a[0], segment.a[1], segment.b[0], segment.b[1], + ]) <= eps)) + .map((segment) => this._planSnapPhysicalSegment(segment)) + .filter((segment): segment is LinearWallSegment => !!segment); + const patches = linearWallJoinPatches([...preview, ...touching], eps); + return patches.map((patch) => + `M ${patch.map((point) => `${point[0]} ${point[1]}`).join(' L ')} Z`).join(' '); + } + +public _renderMarkupLayer(vb: number[]): TemplateResult { + // Derived axes omit zero and positive-body stretches; their dedicated + // layers paint those segments with the correct style and z-order. + const openCuts = this.host._openCuts(); + const thickCuts = this.host._thickWallCuts(); + const allCuts = openCuts.concat(thickCuts); + const segs = allCuts.length + ? cutSegments(this.host._segments, allCuts, this.host._gridPitch * 0.02) + : this.host._segments; + const path = this.host._path; + const g = this.host._gridPitch; + const view = this.host._viewOr(this.host._baseVb()); + const drawCm = this.host._tool === 'draw' ? this.host._drawWallCm : null; + const previewPts = (() => { + if (this.host._tool !== 'draw' || !path.length || drawCm == null) return null; + if (this.host._contourClosed) return path; + if (this.host._cursorPt) return [...path, this.host._cursorPt]; + return path.length >= 2 ? path : null; + })(); + // Превью берёт толщины из того же резолвера, что и запись (#234). Раньше + // здесь была вторая формула, и расходились они ровно на пропуске: на экране + // текущее поле, в конфиге — 15 см. + const previewHalfDepths = previewPts + ? chainSegmentCms( + previewPts.length - 1, + this.host._contourClosed + ? [...this.host._draftSegmentCms, this.host._closingWallCm ?? undefined] + : this.host._draftSegmentCms, + drawCm, DRAW_WALL_DEFAULT_CM, + ).map((cm) => wallCmToUnits(cm, this.host._cellCm, this.host._gridPitch) / 2) + : []; + const previewD = previewPts + ? drawWallPreviewD( + previewPts, + wallCmToUnits(drawCm!, this.host._cellCm, this.host._gridPitch) / 2, + this.host._contourClosed, + previewHalfDepths, + ) + : ''; + const previewJoinPatchD = previewPts + ? this._drawPreviewJoinPatchD(previewPts, previewHalfDepths) + : ''; + const repairPreview = this.host._wallFaceBatch?.candidates[this.host._wallFaceBatch.index]?.repair + || this.host._wallRepairDiagnostic; + return svg` + ${this._gridLevels() + ? svg`` + : nothing} + ${segs.map((s) => svg``)} + ${this._renderPhysicalEditorLayer()} + ${this.host._tool === 'column' && this.host._cursorPt && this.host._drawWallCm + ? svg` { + const c: WallColumnCfg = { id: 'preview', shape: 'square', center: this.host._cursorPt!, cm: this.host._drawWallCm! }; + const body = columnBody(c, this.host._cellCm, this.host._gridPitch); + return `M ${body.map((p) => `${p[0]} ${p[1]}`).join(' L ')} Z`; + })()}>` + : nothing} + ${previewD + ? svg` + ` + : nothing} + ${previewPts && drawCm === 0 + ? svg` point.join(',')).join(' ')}>` + : nothing} + ${previewJoinPatchD + ? svg` + ` + : nothing} + ${repairPreview ? svg`` : nothing} + ${this.host._tool === 'split' && this.host._splitSel?.pts?.length + ? svg`${this.host._splitSel.pts.length > 1 + ? svg`` + : nothing} + ${this.host._splitSel.pts.map((p, i) => svg``)} + ${this.host._cursorPt + ? svg`` + : nothing}` + : nothing} + `; + } + +public _renderActiveChainInk(): TemplateResult { + const path = this.host._path; + const g = this.host._gridPitch; + return svg` + ${path.length > 1 + ? svg`` + : nothing} + ${path.length && this.host._cursorPt && this.host._tool === 'draw' && !this.host._contourClosed + ? svg` + ${!this.host._activePlanSnapCandidate && !this.host._activePlanSnapConflicts.length + ? svg`` : nothing}` + : nothing} + ${path.map((p, i) => svg``)} + ` as unknown as TemplateResult; + } + +public _renderPartitionDeleteDialog(): TemplateResult { + const dialog = this.host._partitionDeleteDialog!; + const imperial = this.host.hass?.config?.unit_system?.length === 'mi'; + return html` (this.host._partitionDeleteDialog = null)}> +
+

${this.host._t('confirm.delete_partition_openings_body', { + count: dialog.openings.length, + })}

+
    + ${dialog.openings.map((opening) => html`
  • ${this.host._t( + 'confirm.delete_partition_openings_item', { + type: this.host._t(`opening.${opening.type}` as I18nKey), + length: formatLength( + (opening.length * NORM_W / this.host._gridPitch) * this.host._cellCm, + imperial, + ), + }, + )}
  • `)} +
+
+
+ + + +
+
`; + } + +public _renderRoomDeleteDialog(): TemplateResult { + const dialog = this.host._roomDeleteDialog!; + return html` (this.host._roomDeleteDialog = null)}> +
+

${this.host._t('confirm.delete_room_body')}

+
+
+ + + + +
+
`; + } + +public _renderPhysicalDialog(): TemplateResult { + const d = this.host._physicalDialog!; + const column = d.kind === 'column'; + return html` (this.host._physicalDialog = null)}> +
+ ${column ? html` + ` : nothing} + +
(this.host._physicalDialog = { + ...d, cm: (e.target as HTMLInputElement).value, + })} /> + ${this.host._t(this.host._imperial ? 'wallthick.unit_in' : 'wallthick.unit_cm')}
+ ${column && d.shape === 'square' ? html` + + (this.host._physicalDialog = { + ...d, angle: (e.target as HTMLInputElement).value, + })} />` : nothing} + ${d.length ? html`
${this.host._t('physical.length')}: ${d.length}
` : nothing} +
+ +
`; + } + +public _renderMarkupBar(): TemplateResult { + const undoName = this.host._geometryHistory.undoName; + const redoName = this.host._geometryHistory.redoName; + const undoTitle = undoName + ? this.host._t('history.undo_named', { name: undoName }) + : this.host._t('history.undo_empty'); + const redoTitle = redoName + ? this.host._t('history.redo_named', { name: redoName }) + : this.host._t('history.redo_empty'); + return html`
+
+ + + + + + + + + + ${this.host._editorToolbarGroups.map((group) => this._renderEditorGroupLauncher(group))} + + + + +
+
+ +
+
`; + } + +public _renderDevicesBar(): TemplateResult { + return html`
+
+ + + + ${this.host._editorToolbarGroups.map((group) => this._renderEditorGroupLauncher(group))} +
+
+ +
+
`; + } + +public _renderDeviceInbox(): TemplateResult { + const dialog = this.host._deviceInbox!; + const rows = this._deviceInboxRows(); + const counts = Object.fromEntries((['on_plan', 'available', 'hidden', 'readd'] as const) + .map((category) => [category, rows.filter((row) => row.category === category).length])); + const filtered = filterDeviceInbox( + rows, dialog.tab, dialog.search, dialog.tab === 'on_plan' && dialog.onlyNew, + ); + const visible = filtered.slice(0, dialog.limit); + const tabLabel = (tab: DeviceInboxCategory) => this.host._t(`device_inbox.tab_${tab}` as any); + const emptyKey = `device_inbox.empty_${dialog.tab}` as any; + const openVirtual = () => { + this.host._deviceInboxReturn = { ...dialog }; + this.host._deviceInbox = null; + this._openMarkerDialog(); + if (!this.host._markerDialog) this._closeMarkerDialog(); + }; + return html` (this.host._deviceInbox = null)}> +
+
+ (this.host._deviceInbox = { + ...dialog, search: (event.target as HTMLInputElement).value, limit: 100, + })} /> + +
+
+ ${(['on_plan', 'available', 'hidden', 'readd'] as DeviceInboxCategory[]).map((tab) => html` + `)} +
+
+ ${dialog.tab === 'on_plan' ? html`` : nothing} + ${dialog.tab === 'available' ? html`` : nothing} + +
+
+ ${visible.length ? visible.map((row) => { + const primary = row.category === 'on_plan' + ? row.canFind ? html`` + : html`` + : row.category === 'hidden' + ? html`` + : html``; + const status = row.status.kind === 'active' ? '' + : this.host._t(`device_inbox.status_${row.status.kind}` as any); + return html`
+ +
+
${row.name} + ${row.isNew ? html`${this.host._t('device_inbox.new' as any)}` : nothing} +
+
+ ${[row.model, row.integration, row.spaceName, row.areaName].filter(Boolean).join(' · ')} +
+
+ ${this.host._t(`device_inbox.reason_${row.reason}` as any)} + ${status ? html`${status}` : nothing} +
+ ${row.binding} +
+
+ ${primary} + ${row.canEdit || row.canHide || row.category === 'available' + || row.category === 'hidden' || this.host._bindingHasHaPage(row.binding) ? html` +
+ + + +
+ ${row.canEdit && !(row.category === 'on_plan' && !row.canFind) + ? html`` : nothing} + ${row.canHide ? html`` : nothing} + ${row.category === 'available' ? html`` : nothing} + ${row.category === 'hidden' ? html`` : nothing} + ${this.host._bindingHasHaPage(row.binding) ? html`` : nothing} +
+
` : nothing} +
+
`; + }) : html`
${this.host._t(emptyKey)}
`} +
+ ${filtered.length > visible.length ? html`` : nothing} +
+
+ +
+
`; + } + +public _markerValueBadgeFields( + d: NonNullable, + ): Pick | Record { + return valueBadgeWriteFields({ + touched: d.valueBadgeTouched, + originalHas: d.originalHasValueBadge, + original: d.originalValueBadge, + enabled: d.valueBadgeEnabled, + source: d.valueBadgeSource, + position: d.valueBadgePosition, + }); + } + +public _markerDraft(d: NonNullable): Marker | null { + if (d.bindingMode === 'ha' && (!d.binding || d.binding === 'virtual')) return null; + const roomRef = parseRoomRef(d.room); + const id = markerIdForBinding(d.binding, d.devId, () => '__hp_device_preview__'); + const previous = this.host._markers.find((marker) => marker.id === id || marker.id === d.devId); + const controls = persistedExternalControls( + d.binding, d.controls, this.host._bindingEntities(d.binding), + ); + const effectiveTapAction = this._effectiveStoredTapAction(d); + const marker: Marker = { + id, + binding: d.binding, + name: d.name.trim() || null, + icon: d.icon || null, + display: d.display !== 'badge' ? d.display : null, + ripple_color: d.display === 'icon_ripple' && d.rippleColor ? d.rippleColor : null, + ripple_size: d.display === 'icon_ripple' && d.rippleSize !== 1.5 ? d.rippleSize : null, + size: d.size !== 1 ? d.size : null, + angle: d.angle || null, + ...this._markerTapActionFields(d), + ...this._markerToggleEntityFields(d), + tap_target: effectiveTapAction === 'run' ? d.tapTarget || null : null, + tap_confirm: d.tapConfirm ? true : null, + controls: controls.length ? controls : null, + ...this._markerLightFields(d), + ...this._markerValueBadgeFields(d), + use_climate_temp: d.useClimateTemp ? true : null, + glow_radius_cm: (() => { + const value = strictNumber(d.glowRadius); + if (value == null || value <= 0) return null; + return Math.round(this.host._imperial ? value * 30.48 : value * 100); + })(), + model: d.model.trim() || null, + link: d.link.trim() || null, + description: d.description.trim() || null, + pdfs: d.pdfs, + hidden: d.hideFromPlan, + vacuum: previous?.vacuum || null, + }; + if (d.binding === 'virtual' || d.room) { + marker.space = roomRef?.space || (d.binding === 'virtual' ? this.host._space : null); + marker.area = roomRef?.area || null; + marker.room_id = roomRef?.roomId || null; + } + return marker; + } + +public _markerPreviewDevice(d: NonNullable): DevItem | null { + const marker = this._markerDraft(d); + if (!marker) return null; + const key = `${this.host._haRegistry.revision}\n${contentFingerprint(this.host._markers)}\n${JSON.stringify(marker)}`; + if (this.host._markerPreviewMemo?.key === key) return this.host._markerPreviewMemo.device; + const device = deviceFromMarkerDraft({ + hass: this.host.hass, + registry: this.host._haRegistry, + areaToSpace: Object.fromEntries( + Object.entries(this.host._areaToSpace).map(([area, value]) => [area, value.space]), + ), + marker, + siblingMarkers: this.host._markers, + settings: this.host._settings, + excluded: this.host._excluded, + showAll: this.host._showAll, + firstSpaceId: this.host._model[0]?.id || this.host._space, + loc: (key) => this.host._t(key), + iconRules: this.host._iconRules, + }); + this.host._markerPreviewMemo = { key, device }; + return device; + } + +public _markerPreviewDevices(preview: DevItem): readonly DevItem[] { + const memo = this.host._markerPreviewDevicesMemo; + if (memo?.base === this.host._devices && memo.preview === preview) return memo.devices; + const devices = [...this.host._devices.filter((item) => item.id !== preview.id), preview]; + this.host._markerPreviewDevicesMemo = { base: this.host._devices, preview, devices }; + return devices; + } + +public _toggleIntent( + device: DevItem, + devices: readonly DevItem[] = this.host._devices, + ): ResolvedToggleIntent | null { + return resolveToggleIntent({ + hass: this.host._planHass, + registryHass: this.host._fullRegistryHass, + devices, + device, + virtualLights: this.host._virtualLights, + }); + } + +public _toggleIntentForDialog( + d: NonNullable, + ): ResolvedToggleIntent | null { + const preview = this._markerPreviewDevice(d); + if (!preview) return null; + const devices = this._markerPreviewDevices(preview); + return this._toggleIntent(preview, devices); + } + +public _toggleStateText(entityId: string, fallback: string): string { + const state = this.host._planHass?.states?.[entityId] || this.host.hass?.states?.[entityId]; + try { + return state && typeof this.host.hass?.formatEntityState === 'function' + ? this.host.hass.formatEntityState(state) : fallback; + } catch { + return fallback; + } + } + +public _toggleConfirmationStateText(target: ResolvedToggleTarget): string { + const raw = String(target.state || 'unknown'); + const formatted = target.entityId ? this._toggleStateText(target.entityId, raw) : raw; + if (formatted.trim().toLocaleLowerCase() !== raw.trim().toLocaleLowerCase()) return formatted; + const known: Partial> = { + on: 'confirm.state_on', + off: 'confirm.state_off', + open: 'confirm.state_open', + closed: 'confirm.state_closed', + opening: 'confirm.state_opening', + closing: 'confirm.state_closing', + unknown: 'confirm.state_unknown', + }; + const key = known[raw]; + if (key) return this.host._t(key); + // A future integration-specific token stays honest and readable even in a + // harness without HA's formatter; underscores are never exposed as UI. + return raw.replaceAll('_', ' ').replaceAll('-', ' '); + } + +public _toggleConfirmationLines(intent: ResolvedToggleIntent): string[] { + const effectKeys: Record, I18nKey> = { + 'turn-on': 'confirm.state_on', + 'turn-off': 'confirm.state_off', + open: 'confirm.state_open', + close: 'confirm.state_closed', + stop: 'confirm.state_stopped', + }; + return formatToggleConfirmation(intent, { + state: (target) => this._toggleConfirmationStateText(target), + current: (state) => this.host._t('confirm.current_state', { state }), + expected: (state) => this.host._t('confirm.expected_state', { state }), + groupCurrent: (on, total) => this.host._t('confirm.group_current', { on, total }), + groupAllOn: () => this.host._t('confirm.group_all_on'), + groupAllOff: () => this.host._t('confirm.group_all_off'), + unavailable: (count) => this.host._t('confirm.unavailable_targets', { count }), + effect: (effect) => this.host._t(effectKeys[effect]), + expectedByHa: () => this.host._t('confirm.expected_by_ha'), + }); + } + +public _toggleHintLines(intent: ResolvedToggleIntent | null): string[] { + if (!intent) return []; + const effect = (value: ToggleNextEffect): string => + this.host._t((`marker.toggle_effect_${value.replace('-', '_')}`) as any); + const skipReason = (value: ToggleSkipReason): string => + this.host._t((`marker.toggle_skip_${value.replace('-', '_')}`) as any); + return formatToggleIntent(intent, { + single: (target) => { + if ('via' in target && target.via === 'virtual-light') { + return this.host._t('marker.virtual_light_target', { name: target.name }); + } + const entityId = target.entityId || ('ref' in target ? target.ref : ''); + const name = target.name || entityId; + return this.host._t('marker.toggle_hint_single', { name, id: entityId }); + }, + group: (targets) => this.host._t('marker.toggle_hint_group', { + count: targets.length, + names: targets.map((target) => `${target.name} (${target.entityId})`).join(', '), + }), + currentNext: (target, next) => target.via === 'virtual-light' + ? this.host._t('marker.virtual_light_current', { + state: this.host._t(target.state === 'on' + ? 'marker.virtual_light_state_on' : 'marker.virtual_light_state_off'), + effect: effect(next), + }) + : this.host._t('marker.toggle_hint_current', { + state: this._toggleStateText(target.entityId, target.state), + effect: effect(next), + }), + groupCurrentNext: (targets, next) => this.host._t('marker.toggle_hint_group_current', { + on: targets.filter((target) => target.state === 'on').length, + count: targets.length, + effect: effect(next), + }), + skipped: (targets) => this.host._t('marker.toggle_hint_skipped', { + count: targets.length, + targets: targets.map((target) => { + const id = target.entityId || target.ref; + return `${target.name || id} (${id}: ${skipReason(target.reason)})`; + }).join(', '), + }), + none: (reason: ToggleNoneReason) => + this.host._t((`marker.toggle_none_${reason.replaceAll('-', '_')}`) as any), + }); + } + +public _effectiveStoredTapAction( + d: NonNullable, primaryDomain?: string, + ): string { + return d.tapActionTouched + ? d.tapAction + : projectedTapAction( + d.originalHasTapAction ? d.originalTapAction : null, + primaryDomain, + ); + } + +public _effectiveMarkerTapAction( + d: NonNullable, + preview = this._markerPreviewDevice(d), + ): string { + return this._effectiveStoredTapAction(d, preview?.primary?.split('.')[0]); + } + +public _announceToggleDraft( + d: NonNullable, + ): NonNullable { + const preview = this._markerPreviewDevice(d); + const next = !d.tapActionTouched + ? { ...d, tapAction: this._effectiveMarkerTapAction(d, preview) } + : d; + const text = next.tapAction === 'toggle' + ? this._toggleHintLines(this._toggleIntentForDialog(next)).join(' ') + : ''; + return { ...next, tapHintAnnouncement: text }; + } + +public _valueBadgeForBinding( + d: NonNullable, binding: string, + ): Pick, + 'valueBadgeEnabled' | 'valueBadgeSource' | 'valueBadgeTouched'> { + const draft = { + ...d, binding, + valueBadgeEnabled: false, valueBadgeSource: null, valueBadgeTouched: true, + }; + const device = this._markerPreviewDevice(draft); + if (!device) return { valueBadgeEnabled: false, valueBadgeSource: null, valueBadgeTouched: true }; + const lightDevices = [...this.host._devices.filter((item) => item.id !== device.id), device]; + const candidates = valueBadgeCandidates(this.host._planHass, device, lightDevices); + const source = recommendedValueBadgeSource(this.host._planHass, device, candidates); + return { + valueBadgeEnabled: d.valueBadgeEnabled && !!source, + valueBadgeSource: source, + valueBadgeTouched: true, + }; + } + +public _markerSpatialSource(d: NonNullable) { + const device = this._markerPreviewDevice(d); + if (!device) return null; + const preview = { ...device, hidden: false }; + return selectSpatialGlowSource(resolvedLightSources(this.host._planHass, [ + ...this.host._devices.filter((item) => item.id !== preview.id), preview, + ], null, this.host._virtualLights).filter((source) => source.device.id === preview.id)); + } + +public _markerAutoHasSpatialSource(d: NonNullable): boolean { + const autoDraft = { ...d, lightRole: 'auto' as const, lightRoleTouched: true }; + const device = this._markerPreviewDevice(autoDraft); + return !!device && hasOwnSpatialSource(this.host._planHass, { ...device, hidden: false }); + } + +public _setMarkerLightRole(role: 'auto' | 'always' | 'never'): void { + const d = this.host._markerDialog; + if (!d) return; + this.host._markerDialog = { ...d, lightRole: role, lightRoleTouched: true }; + } + +public _controlRefInfo(ref: string): { label: string; sub: string; icon: string; warning: boolean } { + if (!ref.startsWith('marker:')) { + return { + label: this.host.hass.states[ref]?.attributes?.friendly_name || ref, + sub: ref, + icon: ref.startsWith('light.') ? 'mdi:lightbulb' : 'mdi:toggle-switch', + warning: !this.host._planEntityAvailable(ref), + }; + } + const id = ref.slice('marker:'.length); + const marker = this.host._markers.find((item) => item.id === id); + const device = this.host._devices.find((item) => item.id === id); + if (!marker || marker.removed || marker.is_light !== true) { + return { + label: this.host._t('marker.control_missing_label'), + sub: `${this.host._t('marker.control_broken')} (${ref})`, + icon: 'mdi:alert-outline', warning: true, + }; + } + const space = this.host._serverCfg?.spaces.find((item) => item.id === (device?.space || marker.space)); + const stateful = !!device && ownControllableEntities(device).length > 0; + return { + label: marker.name || device?.name || id, + sub: [space?.title || device?.space || marker.space, marker.room_id || device?.area, + stateful ? '' : this.host._t('marker.control_passive')].filter(Boolean).join(' · '), + icon: marker.icon || device?.icon || 'mdi:lightbulb-outline', + warning: marker.hidden === true || device?.hidden === true, + }; + } + +public _valueBadgeCandidateLabel(candidate: ValueBadgeCandidate): string { + const source = candidate.source; + if (source.kind === 'derived_lqi') return this.host._t('marker.value_badge_lqi'); + if (source.kind === 'derived_marker_state') { + return this.host._t('marker.value_badge_marker_state', { name: candidate.label }); + } + if (source.kind === 'entity_state') { + return this.host._t('marker.value_badge_state', { name: candidate.label }); + } + const entityName = this.host.hass.states[source.entity_id]?.attributes?.friendly_name + || this.host._fullRegistryHass.entities[source.entity_id]?.name || source.entity_id; + return this.host._t((`marker.value_badge_attr_${source.attribute}`) as any, { name: entityName }); + } + +public _controlCandidates(d: NonNullable): { + value: string; label: string; sub: string; icon: string; + }[] { + const currentId = this._markerDraft(d)?.id || d.devId || ''; + const plan: { value: string; label: string; sub: string; icon: string; search: string }[] = []; + const coveredEntities = new Set(); + for (const marker of this.host._markers) { + if (marker.id === currentId || marker.removed || marker.hidden || marker.is_light !== true) continue; + if (marker.binding !== 'virtual' && this.host._bindingStatus(marker.binding).kind !== 'active') continue; + const device = this.host._devices.find((item) => item.id === marker.id); + if (!device || device.hidden) continue; + const info = this._controlRefInfo(`marker:${marker.id}`); + for (const eid of ownControllableEntities(device)) coveredEntities.add(eid); + plan.push({ + value: `marker:${marker.id}`, label: info.label, sub: info.sub, icon: info.icon, + search: `${info.label} ${info.sub} ${marker.id} ${ownControllableEntities(device).join(' ')}`.toLowerCase(), + }); + } + const ha = Object.keys(this.host.hass.states || {}) + .filter((eid) => isControllable(eid) && !coveredEntities.has(eid) + && effectiveMarkerControls(d.binding, [eid], this.host._bindingEntities(d.binding)).length > 0 + && this.host._planEntityAvailable(eid)) + .map((eid) => { + const info = this._controlRefInfo(eid); + return { value: eid, label: info.label, sub: eid, icon: info.icon, + search: `${info.label} ${eid}`.toLowerCase() }; + }); + const q = d.controlsFilter.trim().toLowerCase(); + return [...plan, ...ha] + .filter((item) => !d.controls.includes(item.value) && (!q || item.search.includes(q))) + .slice(0, 12) + .map(({ search: _search, ...item }) => item); + } + +public _addControlRef(d: NonNullable, ref: string): void { + if (ref.startsWith('marker:')) { + const controllerId = this._markerDraft(d)?.id || d.devId || ''; + if (!controllerId) { + this.host._showToast(this.host._t('toast.marker_binding_required')); + return; + } + const targetId = ref.slice('marker:'.length); + const draft = this._markerDraft(d); + const graph = draft + ? [...this.host._markers.filter((marker) => marker.id !== controllerId), draft] + : this.host._markers; + if (markerControlWouldCycle(graph, controllerId, targetId)) { + this.host._showToast(this.host._t('toast.marker_control_cycle')); + return; + } + } + this.host._markerDialog = this._announceToggleDraft({ + ...d, controls: [...d.controls, ref], controlsFilter: '', + }); + } + +public _setMarkerGlowMode(mode: 'auto' | 'color' | 'fixed'): void { + const d = this.host._markerDialog; + if (!d) return; + if (mode === 'auto') { + this.host._markerDialog = { ...d, glowMode: mode, glowTouched: true }; + return; + } + const source = this._markerSpatialSource(d); + const values = resolveGlowValues( + source ? this.host._planHass.states[source.eid] : undefined, + null, + this.host._fillColors.glow_light.c, + ); + const needColor = !d.glowColorDrafted; + const needBrightness = mode === 'fixed' && !d.glowBrightnessDrafted; + this.host._markerDialog = { + ...d, + glowMode: mode, + glowColor: needColor ? values.c : d.glowColor, + glowBrightness: needBrightness ? Math.max(1, Math.round(values.bri * 100)) : d.glowBrightness, + glowColorDrafted: true, + glowBrightnessDrafted: d.glowBrightnessDrafted || mode === 'fixed', + glowTouched: true, + }; + } + +public _renderMarkerDialog(): TemplateResult { + const d = this.host._markerDialog!; + const isVirtual = d.bindingMode === 'virtual'; + const cands = this._bindingCandidates(); + const ownEntities = this.host._bindingEntities(d.binding); + const bindingStatus = isVirtual ? null : this.host._bindingStatus(d.binding); + const canOpenBindingInHa = !isVirtual && this.host._bindingHasHaPage(d.binding); + const previewDevice = this._markerPreviewDevice(d); + // Untouched defaults are projections, not stored values. Re-resolve the + // select from the current preview on every render: HA may reveal a more + // meaningful leading entity (most visibly `light.*`) after the dialog was + // opened. Runtime already uses that current primary, so keeping the stale + // draft value here made the select say "Device card" while a tap toggled + // the lamp. An explicit user choice remains authoritative and stable. + const effectiveTapAction = this._effectiveMarkerTapAction(d, previewDevice); + const previewSpaceDisplay = previewDevice + ? spaceDisplayOf(this.host._serverCfg?.spaces.find((space) => space.id === previewDevice.space)) + : null; + const previewLightDevices = previewDevice + ? this._markerPreviewDevices(previewDevice) + : this.host._devices; + const toggleIntent = effectiveTapAction === 'toggle' && previewDevice + ? this._toggleIntent(previewDevice, previewLightDevices) : null; + const toggleHintLines = this._toggleHintLines(toggleIntent); + const previewPresentation = previewDevice + ? resolveDevicePresentation(this.host._planHass, previewDevice, { + liveStates: this.host._config?.live_states !== false, + showTemperature: this.host._config?.show_temperature !== false, + showSignal: previewSpaceDisplay?.showLqi ?? (this.host._config?.show_signal !== false), + designPreview: true, + activityRuntime: this.host._activityRt.get(previewDevice.id), + lightDevices: previewLightDevices, + registryHass: this.host._fullRegistryHass, + reducedMotion: this.host._reducedMotion, + }) + : null; + const badgeCandidates = previewDevice + ? valueBadgeCandidates(this.host._planHass, previewDevice, previewLightDevices) : []; + const badgeRecommendation = previewDevice + ? recommendedValueBadgeSource(this.host._planHass, previewDevice, badgeCandidates) : null; + const effectiveBadgeEnabled = d.valueBadgeTouched + ? d.valueBadgeEnabled : !!previewPresentation?.valueBadge; + const effectiveBadgeSource = d.valueBadgeTouched + ? d.valueBadgeSource : previewPresentation?.valueBadge?.source || d.valueBadgeSource; + const effectiveBadgePosition = d.valueBadgeTouched + ? d.valueBadgePosition : previewPresentation?.valueBadge?.position || d.valueBadgePosition; + const badgeSourceKey = valueBadgeSourceKey(effectiveBadgeSource); + const badgeSourceMissing = !!effectiveBadgeSource + && !badgeCandidates.some((item) => item.key === badgeSourceKey); + const selectedBadgeCandidate = badgeCandidates.find((item) => item.key === badgeSourceKey); + const innerValueSourceKey = previewPresentation?.valueSource + ? previewPresentation.valueSource.attribute + ? `attr:${previewPresentation.valueSource.eid}:${previewPresentation.valueSource.attribute}` + : `state:${previewPresentation.valueSource.eid}` + : ''; + const autoHasSpatialSource = this._markerAutoHasSpatialSource(d); + const statefulSource = !!previewDevice + && hasOwnStatefulLightSource(this.host._planHass, { ...previewDevice, hidden: false }); + const lightSettings = resolveDeviceLightSettings( + d.lightRole, autoHasSpatialSource, statefulSource, d.glowMode, + ); + const glowSourceDisabled = !lightSettings.sourceExists; + const liveGlowDisabled = !lightSettings.fromSourceEnabled; + const passiveSource = lightSettings.passive; + const displayedGlowMode = lightSettings.effectiveMode; + const glowDisabledHint = d.lightRole === 'never' + ? this.host._t('marker.glow_disabled_never') + : d.lightRole === 'auto' && !autoHasSpatialSource + ? this.host._t('marker.glow_disabled_auto') + : passiveSource ? this.host._t('marker.glow_passive_hint') + : this.host._t('marker.glow_disabled_no_entity'); + const leadingEntities = previewDevice ? ownControllableEntities(previewDevice) : []; + // Keep the fallback text aligned with the production resolver. In + // particular, an entity binding or the resolved primary may precede the + // registry order used for the remaining candidates. + const effectiveLeading = previewDevice ? forcedLightEntityOf(previewDevice) || '' : ''; + const staleLeading = !!d.lightEntity && !leadingEntities.includes(d.lightEntity); + const toggleEntities = previewDevice ? toggleEntityCandidates(previewDevice) : []; + const staleToggleEntity = !!d.toggleEntity && !toggleEntities.includes(d.toggleEntity); + const automaticToggleIntent = effectiveTapAction === 'toggle' + ? this._toggleIntentForDialog({ ...d, toggleEntity: '', toggleEntityTouched: true }) + : null; + const automaticToggleTarget = automaticToggleIntent + ? [...automaticToggleIntent.targets, ...automaticToggleIntent.skippedTargets] + .map((target) => target.entityId || ('ref' in target ? target.ref : '')) + .filter(Boolean) + .join(', ') + : ''; + const curLabel = (() => { + if (isVirtual) return null; + const found = cands.find((c) => c.value === d.binding); + if (found) return found.label; + const [k, ref] = d.binding.split(':'); + if (k === 'device') return this.host._fullRegistryHass.devices[ref]?.name_by_user || this.host._fullRegistryHass.devices[ref]?.name || ref; + return this.host._fullRegistryHass.entities[ref]?.name || this.host.hass.states[ref]?.attributes?.friendly_name || ref; + })(); + return html` +
+ ${bindingStatus?.kind === 'ha_disabled' + ? html`
+ + ${this.host._t((`marker.ha_disabled_${bindingStatus.reason}`) as any)} + ${canOpenBindingInHa + ? html`` + : nothing} +
` + : bindingStatus?.kind === 'unverified' && !!d.binding + ? html`
+ + ${this.host._t('marker.ha_registry_limited')} +
` + : nothing} + + (this.host._markerDialog = { ...d, name: (e.target as HTMLInputElement).value })} /> + + +
+ +
+ + +
+ ${d.bindingMode === 'ha' + ? html` + ${d.bindingOpen + ? html`
+ (this.host._markerDialog = { ...d, bindingFilter: (e.target as HTMLInputElement).value })} /> +
+ ${cands.map( + (c) => html`
{ + const next = { + ...d, binding: c.value, bindingOpen: false, + controls: persistedExternalControls( + c.value, d.controls, this.host._bindingEntities(c.value), + ), + autoIcon: this.host._autoIconForBinding(c.value), + }; + this.host._markerDialog = this._announceToggleDraft({ + ...next, ...this._valueBadgeForBinding(next, c.value), + }); + }}> + ${c.label}${c.sub} +
`, + )} + ${!cands.length ? html`
${this.host._t('marker.nothing_found')}
` : nothing} +
+
` + : nothing}` + : nothing} +
+ + + + + ${this._renderVacSection(d)} + + + + ${effectiveTapAction === 'toggle' + && (toggleEntities.length > 1 || staleToggleEntity) + ? html`
+
+ + ${this._help('marker.toggle_entity.help')} +
+ + ${staleToggleEntity ? html`

+ + ${this.host._t('marker.toggle_entity_missing', { + entity: d.toggleEntity, + fallback: automaticToggleTarget || this.host._t('marker.toggle_entity_none'), + })} +

` : nothing} +
` + : nothing} + ${effectiveTapAction === 'toggle' + ? html`
+ ${toggleHintLines.map((line) => html`
${line}
`)} +
+
${d.tapHintAnnouncement}
` + : nothing} + ${effectiveTapAction === 'run' + ? (() => { + const q = d.runFilter.trim().toLowerCase(); + const cands = this._runCandidates().filter( + (c) => !q || c.label.toLowerCase().includes(q) || c.value.includes(q), + ); + const cur = d.tapTarget ? this._runCandidates().find((c) => c.value === d.tapTarget) : null; + return html` + + ${d.tapTarget && !cur + ? html`
${this.host._t('marker.run_target_gone', { id: d.tapTarget })}
` + : nothing} + { (e.target as HTMLInputElement).select(); }} + @input=${(e: Event) => (this.host._markerDialog = { ...d, runFilter: (e.target as HTMLInputElement).value, tapTarget: '' })} /> + ${!cur + ? html`
+ ${cands.slice(0, 40).map( + (c) => html`
(this.host._markerDialog = { ...d, tapTarget: c.value, runFilter: '' })}> + ${c.label}${c.sub} +
`, + )} + ${!cands.length ? html`
${this.host._t('marker.nothing_found')}
` : nothing} +
` + : nothing}`; + })() + : nothing} + ${effectiveTapAction === 'run' || effectiveTapAction === 'toggle' + ? html`` + : nothing} + + +
${this.host._t('marker.controls_hint')}
+ ${d.controls.length + ? html`
+ ${d.controls.map((eid) => { + const info = this._controlRefInfo(eid); + return html` + ${info.label} + + (this.host._markerDialog = this._announceToggleDraft({ + ...d, controls: d.controls.filter((x) => x !== eid), + }))}> + `; + })} +
` + : nothing} + (this.host._markerDialog = { ...d, controlsFilter: (e.target as HTMLInputElement).value })} /> + ${d.controlsFilter.trim() + ? html`
+ ${this._controlCandidates(d).map((candidate) => html``)} +
` + : nothing} + + ${this.host._bindingHasClimate(d.binding) + ? html`` + : nothing} +
+ ${this.host._t('marker.light_role_label')}${this._help('marker.light_role.help')} +
+ + + +
+
+ + ${d.lightRole === 'always' && (leadingEntities.length > 1 || staleLeading) + ? html`
+
+ + ${this._help('marker.light_entity.help')} +
+ + ${staleLeading ? html`

+ + ${this.host._t('marker.light_entity_missing', { + entity: d.lightEntity, fallback: effectiveLeading || '—', + })} +

` : nothing} +
` + : nothing} + +
+ ${this.host._t('marker.glow_color_label')}${this._help('marker.glow_mode.help')} +
+ + + +
+ ${displayedGlowMode !== 'auto' ? html`
+ ) => { + this.host._markerDialog = { + ...d, glowMode: displayedGlowMode, + glowColor: e.detail.color, glowColorDrafted: true, glowTouched: true, + }; + }}> + ${displayedGlowMode === 'fixed' ? html` + ${this.host._t('marker.glow_brightness')} + ${this._rangeInput(1, 100, 1, d.glowBrightness, (n) => { + this.host._markerDialog = { + ...d, glowMode: displayedGlowMode, + glowBrightness: n, glowBrightnessDrafted: true, glowTouched: true, + }; + }, glowSourceDisabled, this.host._t('marker.glow_brightness'))} + ${Math.round(d.glowBrightness)}%` : nothing} +
` : nothing} +
+
+
+ + ${this._help('marker.glow_radius.help')} +
+
+ (this.host._markerDialog = { ...d, glowRadius: (e.target as HTMLInputElement).value })} /> + ${this.host._imperial ? this.host._t('gs.unit_ft') : this.host._t('gs.unit_m')} +
+
+ ${glowSourceDisabled || passiveSource + ? html`

+ ${glowDisabledHint} +

` + : nothing} + + + ${customElements.get('ha-icon-picker') + // Feed the effective icon to HA's picker so its field renders both + // the glyph and the mdi:* label. autoIcon is presentation-only: + // untouched dialogs still save d.icon as an empty auto override. + ? html` { + const icon = e.detail.value || ''; + // Some picker versions announce an assigned value. Do not + // let that turn the display-only auto icon into an override. + if (!d.icon && icon === d.autoIcon) return; + this.host._markerDialog = { ...d, icon }; + }}>` + : html` (this.host._markerDialog = { ...d, icon: (e.target as HTMLInputElement).value })} />`} + ${!d.icon && d.autoIcon + ? html`

+ ${this.host._t('marker.icon_auto', { icon: d.autoIcon })} +

` + : nothing} + + + +

${this.host._t(DISPLAY_HINT_KEYS[d.display])}

+ ${d.display === 'static_icon' && this.host._bindingHasAlarm(d.binding) + ? html`
+ + ${this.host._t('marker.static_alarm_warning')} +
` + : nothing} +
+ ${this.host._t('marker.value_badge_title')}${this._help('marker.value_badge.help')} + + ${d.display === 'static_icon' + ? html`

+ ${this.host._t('marker.value_badge_static')} +

` + : !badgeCandidates.length && !d.valueBadgeSource + ? html`

+ ${this.host._t('marker.value_badge_empty')} +

` + : nothing} + ${effectiveBadgeEnabled ? html` +
+ + ${this._help('marker.value_badge_source.help')} +
+ + ${selectedBadgeCandidate + ? html`

${selectedBadgeCandidate.technical}

` + : nothing} + ${badgeSourceMissing ? html`

+ ${this.host._t('marker.value_badge_missing_hint')} +

` : nothing} + ${d.display === 'value' && badgeSourceKey === innerValueSourceKey + ? html`

+ ${this.host._t('marker.value_badge_duplicate')} +

` : nothing} +
+ + ${this._help('marker.value_badge_position.help')} +
+ + ` : nothing} +
+ ${previewPresentation + ? html` + ` + : html`
+ + ${this.host._t('marker.preview.select_source')} +
`} + ${d.display === 'icon_ripple' + ? html`
+ ) => { + this.host._markerDialog = { ...d, rippleColor: e.detail.color }; + }}> +
+
+ ${this.host._t('marker.ripple_size')} + ${this._rangeInput(1, 8, 0.5, d.rippleSize, (n) => (this.host._markerDialog = { ...d, rippleSize: n }))} + ×${d.rippleSize} +
+

${this.host._t('marker.activity_alarm_note')}

` + : nothing} + + +
+ ${this._rangeInput(0.5, 3, 0.1, d.size, (n) => (this.host._markerDialog = { ...d, size: n }))} + ×${d.size.toFixed(1)} + ${this.host._t('marker.angle_label')} + ${''/* 5 degrees, not 10 (owner 2026-08-03): a marker often has to + line up with a wall that is not on a 10-degree grid. */} + ${this._rangeInput(0, 355, 5, d.angle, (n) => (this.host._markerDialog = { ...d, angle: n }))} + ${d.angle}° +
+ + + (this.host._markerDialog = { ...d, model: (e.target as HTMLInputElement).value })} /> + + + (this.host._markerDialog = { ...d, link: (e.target as HTMLInputElement).value })} /> + + + + + +
+ ${d.pdfs.map( + (p) => html` + ${p.name} + this._removeMarkerPdf(p.url)}>`, + )} + + + this._pickMarkerFiles(e)} /> + +
+
+
+
+ ${d.devId + ? html`` + : nothing} + ${d.devId + ? html`` + : nothing} +
+
+ + +
+
+
`; + } + +public _renderSpaceDialog(): TemplateResult { + const d = this.host._spaceDialog!; + const progress = this.host._importTotal > 0 && d.mode === 'create' + ? this.host._t('import.progress', { + i: this.host._importTotal - this.host._importQueue.length, + n: this.host._importTotal, + }) + : ''; + const close = () => { + this.host._spaceDialog = null; + this.host._importQueue = []; + this.host._importTotal = 0; + }; + return html` +
+ + (this.host._spaceDialog = { ...d, title: (e.target as HTMLInputElement).value })} /> + + + ${d.source === 'file' + ? html`
+ ${d.planFile + ? html`${d.planFile.name}` + : d.planUrl + ? html`${this.host._t('space.plan_alt')}` + : html`${this.host._t('space.no_plan')}`} + + + this._pickPlanFile(e)} /> + + +
+ ${d.pickSaved ? this._renderServerPlans(d) : nothing}` + : nothing} + + + +
+ { + const raw = (e.target as HTMLInputElement).value; + const n = strictNumber(raw); + const canonical = n == null ? null : gridCellFieldToCm(n, this.host._imperial); + this.host._spaceDialog = { + ...d, + cellCmInput: raw, + cellCmTouched: true, + cellCm: canonical != null && canonical > 0 + ? Math.max(CELL_CM_MIN, Math.min(CELL_CM_MAX, canonical)) : d.cellCm, + }; + }} /> + ${this.host._t( + this.host._imperial ? 'space.scale_unit_imperial' : 'space.scale_unit', + )} +
+ + + + + +
${this.host._t('space.zero_wall_help')}
+ + + ${''/* the two "draw less" switches (owner 2026-08-05). They only + hide: the shapes and the openings stay in the config and + stay visible in the editor that owns them, so nothing is + lost and nothing becomes uneditable. */} + +
${this.host._t('space.hide_decor_tip')}
+ +
${this.host._t('space.hide_openings_tip')}
+ + ${([['labelTemp', 'space.label_temp'], ['labelHum', 'space.label_hum'], + ['labelLqi', 'space.label_lqi'], ['labelLight', 'space.label_light']] as const).map( + ([f, k]) => html``, + )} + +
+ ${this._rangeInput(50, 300, 5, Math.round(d.cardFontScale * 100), (n) => (this.host._spaceDialog = { ...d, cardFontScale: n / 100 }))} + ${Math.round(d.cardFontScale * 100)}% +
+ ${this.host._renderCardPreview(d.cardFontScale, 1, 1)} +
+ ) => { + this.host._spaceDialog = { + ...d, roomColor: e.detail.color, roomOpacity: e.detail.opacity, + }; + }}> +
+ + + ${(d.bgMode ?? bgModeOf(this.host._settings, {})) === 'static' + ? html`
+ ) => { + this.host._spaceDialog = { ...d, bgColor: e.detail.color }; + }}> + ${d.bgColor + ? html`` + : html`${this.host._t('space.bg_inherited')}`} +
` + : nothing} + +
+ { + const raw = (e.target as HTMLInputElement).value.trim(); + const n = raw === '' ? null : Math.round(Number(raw)); + this.host._spaceDialog = { ...d, northDeg: n !== null && Number.isFinite(n) ? Math.min(359, Math.max(0, n)) : null }; + }} /> + ${d.northDeg === null + ? this.host._t('space.north_inherited', { + v: northDegOf(this.host._settings, {}) === null ? '—' : String(northDegOf(this.host._settings, {})) + '°', + }) + : '°'} +
+ + + + ${SPACE_FILL_UI_MODES.map((v) => [v, 'fill.' + v] as const).map( + ([v, k]) => html` + ${v === 'custom' && d.fillMode === 'custom' + ? html`
+ ${this.host._t('space.custom_fill')} + ) => { + this.host._spaceDialog = { ...d, customFill: { c: e.detail.color, a: e.detail.opacity } }; + }}> + ${d.customFill + ? html`` + : nothing} +
` + : nothing}`, + )} + + ${d.deleteBlockers + ? html`` + : nothing} +
+ +
`; + } + +public _renderMergeDialog(): TemplateResult { + const d = this.host._mergeDialog!; + const rooms = this.host._spaceModel()?.rooms || []; + const opt = (id: string, key: 'a' | 'b') => { + const r = rooms.find((x) => x.id === id); + const area = r?.area ? this.host.hass.areas[r.area]?.name : null; + return html``; + }; + return html` (this.host._mergeDialog = null)}> +
+

${this.host._t('merge.hint')}

+ + ${opt(d.aId, 'a')} + ${opt(d.bId, 'b')} +
+
+ + + +
+
`; + } + +public _renderRoomSource(kind: 'temp' | 'hum'): TemplateResult { + const val = kind === 'temp' ? this.host._roomTempSrc : this.host._roomHumSrc; + const setVal = (v: string) => { + if (kind === 'temp') this.host._roomTempSrc = v; + else this.host._roomHumSrc = v; + this.host.requestUpdate(); + }; + const open = this.host._roomSrcOpen === kind; + return html` + + + + ${val || open + ? html` + ${open + ? html`
+ { this.host._roomSrcFilter = (e.target as HTMLInputElement).value; this.host.requestUpdate(); }} /> +
+ ${this._roomSrcCandidates().map( + (c) => html`
{ setVal(c.value); this.host._roomSrcOpen = null; }}> + ${c.label}${c.sub} +
`, + )} +
+
` + : nothing}` + : nothing}`; + } + +public _renderRoomDialog(): TemplateResult { + const edit = !!this.host._roomEditId; + const faceBatch = !edit ? this.host._wallFaceBatch : null; + const batchProgress = faceBatch && faceBatch.candidates.length > 1 + ? this.host._t('room.queue_progress', { + current: faceBatch.index + 1, total: faceBatch.candidates.length, + }) + : ''; + const canSaveNew = !!this.host._areaSel || !!this.host._nameSel.trim(); + const spaceDisplay = spaceDisplayOf(this.host._curSpaceCfg); + const effectiveFill = this.host._roomFill || spaceDisplay.fill; + const customFill = this.host._roomCustomFill || spaceDisplay.customFill; + // the free-areas list must include the edited room's CURRENT area + const areas = [...this.host._freeAreas]; + if (edit && this.host._areaSel && !areas.some((a) => a.area_id === this.host._areaSel)) { + const cur = this.host.hass.areas[this.host._areaSel]; + if (cur) areas.unshift(cur); + } + return html` +
+ ${batchProgress ? html`

+ ${batchProgress} +

` : nothing} + + (this.host._nameSel = (e.target as HTMLInputElement).value)} /> + + + + + + ${([['', 'fill.inherit'], ...ROOM_FILL_MODES.map((v) => [v, 'fill.' + v])] as const).map( + ([v, k]) => html``, + )} + ${effectiveFill === 'custom' + ? html`
+ ${this.host._roomCustomFill + ? this.host._t('room.custom_fill_own') : this.host._t('room.custom_fill_space')} + ) => { + this.host._roomCustomFill = { c: e.detail.color, a: e.detail.opacity }; + }}> + ${this.host._roomCustomFill + ? html`` + : nothing} +
` + : nothing} + ${this._renderRoomSource('temp')} + ${this._renderRoomSource('hum')} + + + +
+ ${this._rangeInput(50, 300, 5, Math.round(this.host._roomNameScale * 100), (n) => { this.host._roomNameScale = n / 100; this.host.requestUpdate(); })} + ${Math.round(this.host._roomNameScale * 100)}% +
+ +
+ ${this._rangeInput(50, 300, 5, Math.round(this.host._roomLabelScale * 100), (n) => { this.host._roomLabelScale = n / 100; this.host.requestUpdate(); })} + ${Math.round(this.host._roomLabelScale * 100)}% +
+ ${this.host._renderCardPreview( + spaceDisplayOf(this.host._curSpaceCfg).cardFontScale, + this.host._roomNameScale, + this.host._roomLabelScale, + )} +
+
+ + + ${edit + ? html`` + : html`${!this.host._pendingSplit ? html`` : nothing} + `} +
+
`; + } +} diff --git a/src/houseplan-onboarding-runtime.ts b/src/houseplan-onboarding-runtime.ts new file mode 100644 index 00000000..5da5ab98 --- /dev/null +++ b/src/houseplan-onboarding-runtime.ts @@ -0,0 +1,843 @@ +import { html, nothing, type TemplateResult } from 'lit'; + +import { + DEFAULT_CUSTOM_FILL, + DEFAULT_ROOM_COLOR, + DEFAULT_ROOM_OPACITY, + DEFAULT_TEMP_MAX, + DEFAULT_TEMP_MIN, + SPACE_FILL_UI_MODES, + customFillOf, + spaceDisplayOf, + stageBgOf, +} from './logic'; +import { bgModeOf, northDegOf } from './sun'; +import { + createEmptySpaceConfig, + initialSpaceDisplayDraft, + switchSpacePlanSource, + touchSpaceDisplay, +} from './space-dialog'; +import { collectSpaceMarkerDependencies } from './space-deletion'; +import { + gridCellFieldToCm, + gridCellFieldValue, + newSpaceCellCm, +} from './grid-scale'; +import { zeroWallStyleOf } from './zero-walls'; +import type { HouseplanEditorHostPort } from './houseplan-editor-runtime'; + +const BUILD_FINGERPRINT = '__HOUSEPLAN_SOURCE_FINGERPRINT__'; +const CELL_CM_MIN = 0.1; +const CELL_CM_MAX = 1000; + +const strictNumber = (value: string): number | null => { + if (!value.trim()) return null; + const parsed = Number(value.replace(',', '.')); + return Number.isFinite(parsed) ? parsed : null; +}; + +/** + * Space creation/import is part of empty-install onboarding, not an editor. + * It therefore has its own lazy boundary: normal View downloads neither this + * module nor the editor, while a new empty installation can create its first + * space without ever requesting the editor chunk. + */ +export const ONBOARDING_RUNTIME_FINGERPRINT = BUILD_FINGERPRINT; + +export class HouseplanOnboardingRuntime { + public constructor(public readonly host: HouseplanEditorHostPort) {} + + public _openSpaceDialog(mode: 'edit' | 'create', spaceId?: string): void { + if (!this.host._serverStorage || !this.host._serverCfg) { + this.host._showToast(this.host._t('toast.integration_missing')); + return; + } + if (mode === 'edit') { + const sp = this.host._serverCfg.spaces.find((space) => space.id === spaceId); + if (!sp) return; + const disp = spaceDisplayOf(sp); + const storedCustom = sp.settings?.custom_fill && typeof sp.settings.custom_fill === 'object' + ? customFillOf(sp.settings.custom_fill) : null; + const dialogCustom = disp.fill === 'none' + ? { ...(storedCustom || DEFAULT_CUSTOM_FILL), a: 0 } + : storedCustom; + this.host._spaceDialog = { + mode, spaceId, title: sp.title, planUrl: sp.plan_url || null, planFile: null, + source: sp.plan_url ? 'file' : 'draw', + showBorders: disp.showBorders, showNames: disp.showNames, + zeroWallStyle: zeroWallStyleOf(sp), + displayTouched: true, + hideDecor: disp.hideDecor, hideOpenings: disp.hideOpenings, + roomColor: disp.color, roomOpacity: disp.opacity, + fillMode: disp.fill === 'none' ? 'custom' : disp.fill, + customFill: dialogCustom, + glowEnabled: disp.glow, + bgColor: disp.bgColor, + bgMode: sp.settings?.bg_mode === 'static' || sp.settings?.bg_mode === 'daynight' + ? sp.settings.bg_mode : null, + northDeg: northDegOf({}, sp.settings), + sunRays: typeof sp.settings?.sun_rays === 'boolean' ? sp.settings.sun_rays : null, + tempMin: disp.tempMin, tempMax: disp.tempMax, + showLqi: disp.showLqi ?? this.host._config?.show_signal ?? true, + cardFontScale: disp.cardFontScale, + labelTemp: disp.labelTemp, labelHum: disp.labelHum, + labelLqi: disp.labelLqi, labelLight: disp.labelLight, + cellCm: Number(sp.cell_cm) > 0 ? Number(sp.cell_cm) : 5, + cellCmInput: gridCellFieldValue( + Number(sp.cell_cm) > 0 ? Number(sp.cell_cm) : 5, this.host._imperial, + ), + cellCmTouched: false, + busy: false, + }; + return; + } + const cellCm = newSpaceCellCm(this.host._imperial); + this.host._spaceDialog = { + mode, title: '', planUrl: null, planFile: null, + ...initialSpaceDisplayDraft(), + hideDecor: false, hideOpenings: false, zeroWallStyle: 'dashed', + roomColor: DEFAULT_ROOM_COLOR, roomOpacity: DEFAULT_ROOM_OPACITY, fillMode: 'custom', + customFill: { ...DEFAULT_CUSTOM_FILL, a: 0 }, + glowEnabled: true, + bgColor: null, + bgMode: 'daynight', northDeg: null, sunRays: null, + tempMin: DEFAULT_TEMP_MIN, tempMax: DEFAULT_TEMP_MAX, + showLqi: this.host._config?.show_signal ?? true, + cardFontScale: 1, + labelTemp: false, labelHum: false, labelLqi: false, labelLight: false, + cellCm, + cellCmInput: gridCellFieldValue(cellCm, this.host._imperial), + cellCmTouched: false, + busy: false, + }; + } + + public async _pickPlanFile(ev: Event): Promise { + const input = ev.target as HTMLInputElement; + const file = input.files?.[0]; + if (!file || !this.host._spaceDialog) return; + const extMap: Record = { + 'image/svg+xml': 'svg', 'image/png': 'png', 'image/jpeg': 'jpg', 'image/webp': 'webp', + }; + const ext = extMap[file.type] || (file.name.toLowerCase().endsWith('.svg') ? 'svg' : ''); + if (!ext) { + this.host._showToast(this.host._t('toast.plan_formats')); + return; + } + const buf = new Uint8Array(await file.arrayBuffer()); + let bin = ''; + for (let i = 0; i < buf.length; i += 32768) bin += String.fromCharCode(...buf.subarray(i, i + 32768)); + const b64 = btoa(bin); + const url = URL.createObjectURL(file); + const aspect = await new Promise((resolve) => { + const image = new Image(); + image.onload = () => resolve(image.naturalWidth && image.naturalHeight + ? image.naturalWidth / image.naturalHeight : 1.414); + image.onerror = () => resolve(1.414); + image.src = url; + }); + URL.revokeObjectURL(url); + this.host._spaceDialog = { + ...this.host._spaceDialog, planFile: { ext, b64, aspect, name: file.name }, + }; + } + + public _toggleServerPlans = async (): Promise => { + const dialog = this.host._spaceDialog; + if (!dialog) return; + if (dialog.pickSaved) { + this.host._spaceDialog = { ...dialog, pickSaved: false }; + return; + } + this.host._spaceDialog = { ...dialog, pickSaved: true, savedBusy: true }; + try { + const response: { plans?: NonNullable } = await this.host.hass.callWS({ + type: 'houseplan/plans/list', + }); + const current = this.host._spaceDialog; + if (current) this.host._spaceDialog = { + ...current, saved: response?.plans || [], savedBusy: false, + }; + } catch (error: unknown) { + const current = this.host._spaceDialog; + if (current) this.host._spaceDialog = { ...current, saved: [], savedBusy: false }; + this.host._showToast(this.host._t('toast.plans_list_failed', { + err: this.host._errText(error), + })); + } + }; + + public _useServerPlan(url: string): void { + const dialog = this.host._spaceDialog; + if (!dialog) return; + this.host._spaceDialog = { + ...dialog, planUrl: url, planFile: null, pickSaved: false, savedAspect: undefined, + }; + this.host._aspectJob = this._readPlanAspect(url); + } + + public async _readPlanAspect(url: string): Promise { + for (let i = 0; i < 40; i++) { + const src = this.host._display(url); + if (src) { + const ratio = await new Promise((resolve) => { + const image = new Image(); + image.onload = () => resolve(image.naturalWidth && image.naturalHeight + ? image.naturalWidth / image.naturalHeight : 0); + image.onerror = () => resolve(0); + image.src = src; + }); + const current = this.host._spaceDialog; + if (current && current.planUrl === url && Number.isFinite(ratio) && ratio > 0) { + this.host._spaceDialog = { ...current, savedAspect: ratio }; + return ratio; + } + return 0; + } + await new Promise((resolve) => setTimeout(resolve, 150)); + if (this.host._spaceDialog?.planUrl !== url) return 0; + } + return 0; + } + + public async _deleteServerPlan(name: string): Promise { + if (!confirm(this.host._t('confirm.delete_plan', { name }))) return; + try { + await this.host.hass.callWS({ type: 'houseplan/plans/delete', name }); + const dialog = this.host._spaceDialog; + if (dialog?.saved) this.host._spaceDialog = { + ...dialog, saved: dialog.saved.filter((plan) => plan.name !== name), + }; + } catch (error: unknown) { + this.host._showToast(this.host._t('toast.plan_delete_failed', { + err: this.host._errText(error), + })); + } + } + + public _renderServerPlans( + dialog: NonNullable, + ): TemplateResult { + if (dialog.savedBusy) return html`
${this.host._t('space.loading')}
`; + const list = dialog.saved || []; + if (!list.length) return html`
${this.host._t('space.no_saved')}
`; + const kb = (size: number) => (size >= 1048576 + ? `${(size / 1048576).toFixed(1)} MB` : `${Math.round(size / 1024)} KB`); + return html`
+ ${list.map((plan) => html` +
+ +
+ ${plan.name} + ${kb(plan.size)}${plan.used_by.length + ? ` · ${this.host._t('space.used_by', { list: plan.used_by.join(', ') })}` + : ''} +
+ + +
`)} +
`; + } + + private async _saveConfigNow(): Promise { + this.host._cfgEpoch++; + try { + await this.host._writeConfig(); + } catch (error: unknown) { + const failure = error as { physicalGeometryRolledBack?: boolean; code?: string }; + if (failure?.physicalGeometryRolledBack) await this.host._reloadRejectedPhysicalWrite(); + else if (failure?.code === 'conflict') await this.host._reloadConfigOnly(); + throw error; + } + } + + public async _saveSpaceDialog(): Promise { + const dialog = this.host._spaceDialog; + if (!dialog || dialog.busy || !dialog.title.trim()) return; + if (dialog.source === 'file' && !dialog.planFile && !dialog.planUrl) { + this.host._showToast(this.host._t('toast.plan_required')); + return; + } + const wasFirst = dialog.mode === 'create' && (this.host._serverCfg?.spaces.length || 0) === 0; + this.host._spaceDialog = { ...dialog, busy: true }; + try { + const spaceId = dialog.mode === 'create' + ? `s${Date.now().toString(36)}` : dialog.spaceId!; + let uploaded: { url: string; aspect: number } | null = null; + if (dialog.source === 'file' && dialog.planFile) { + const response: { url: string } = await this.host.hass.callWS({ + type: 'houseplan/plan/set', + space_id: spaceId, + ext: dialog.planFile.ext, + data: dialog.planFile.b64, + }); + uploaded = { url: response.url, aspect: dialog.planFile.aspect }; + } + let pickedAspect: number | null = dialog.savedAspect || null; + if (!uploaded && dialog.source === 'file' && dialog.planUrl + && !pickedAspect && this.host._aspectJob) { + pickedAspect = (await this.host._aspectJob) || null; + } + + const config = this.host._serverCfg!; + let space = config.spaces.find((candidate) => candidate.id === spaceId); + if (dialog.mode === 'create') { + space = createEmptySpaceConfig(spaceId, dialog.title.trim()); + config.spaces.push(space); + } else { + if (!space) throw new Error(`space ${spaceId} is gone from the config`); + space.title = dialog.title.trim(); + } + if (!space) throw new Error(`space ${spaceId} is unavailable`); + if (uploaded) { + space.plan_url = uploaded.url; + space.plan_aspect = uploaded.aspect; + } else if (dialog.source === 'file' && dialog.planUrl && dialog.planUrl !== space.plan_url) { + space.plan_url = dialog.planUrl; + space.plan_aspect = pickedAspect; + } + if (dialog.source === 'draw') { + space.plan_url = null; + space.plan_aspect = null; + delete space.plan_x; + delete space.plan_y; + delete space.plan_scale; + delete space.plan_scale_x; + delete space.plan_scale_y; + delete space.plan_angle; + } + space.settings = { + ...(space.settings || {}), + show_borders: dialog.showBorders, + show_names: dialog.showNames, + hide_decor: dialog.hideDecor || undefined, + hide_openings: dialog.hideOpenings || undefined, + room_color: dialog.roomColor, + room_opacity: dialog.roomOpacity, + bg_color: dialog.bgColor || undefined, + bg_mode: dialog.bgMode || undefined, + north_deg: dialog.northDeg ?? undefined, + sun_rays: dialog.sunRays ?? undefined, + fill_mode: dialog.fillMode, + custom_fill: dialog.customFill || undefined, + glow_enabled: dialog.glowEnabled, + temp_min: Number.isFinite(dialog.tempMin) + ? Math.min(dialog.tempMin, dialog.tempMax) : DEFAULT_TEMP_MIN, + temp_max: Number.isFinite(dialog.tempMax) + ? Math.max(dialog.tempMin, dialog.tempMax) : DEFAULT_TEMP_MAX, + show_lqi: dialog.showLqi, + card_font_scale: dialog.cardFontScale !== 1 ? dialog.cardFontScale : undefined, + label_temp: dialog.labelTemp, + label_hum: dialog.labelHum, + label_lqi: dialog.labelLqi, + label_light: dialog.labelLight, + }; + space.zero_wall_style = dialog.zeroWallStyle; + space.cell_cm = Number.isFinite(dialog.cellCm) && dialog.cellCm > 0 + ? Math.max(CELL_CM_MIN, Math.min(CELL_CM_MAX, dialog.cellCm)) : 5; + await this._saveConfigNow(); + this.host._spaceDialog = null; + if (dialog.mode === 'create') this.host._commitSpace(space.id); + this.host._regSignature = ''; + this.host._maybeRebuildDevices(); + if (this.host._importQueue.length) { + this._openNextImport(); + } else if (wasFirst || this.host._importTotal > 0) { + const wasImport = this.host._importTotal > 0; + this.host._importTotal = 0; + this.host._commitSpace(this.host._serverCfg!.spaces[0]?.id || this.host._space); + await this.host._requestMode('plan'); + this.host._tool = 'draw'; + this.host._path = []; + this.host._cursorPt = null; + this.host._primeDrawWallField(); + this.host._showToast(this.host._t(wasFirst && !wasImport + ? 'toast.space_added_onboard' : 'import.done')); + } else { + this.host._showToast(this.host._t(dialog.mode === 'create' + ? 'toast.space_added' : 'toast.space_saved')); + if (dialog.mode === 'create') { + if (this.host._mode !== 'plan') await this.host._requestMode('plan'); + else { + this.host._tool = 'draw'; + this.host._path = []; + this.host._cursorPt = null; + this.host._primeDrawWallField(); + this.host._saveNav(); + } + } + } + } catch (error: unknown) { + const failure = error as { code?: string }; + if (failure?.code !== 'conflict') await this.host._reloadConfigOnly(true); + if (this.host._spaceDialog) this.host._spaceDialog = { + ...this.host._spaceDialog, busy: false, + }; + this.host._showToast(this.host._t('toast.error', { err: this.host._errText(error) })); + } + } + + public async _deleteSpace(): Promise { + const dialog = this.host._spaceDialog; + if (!dialog || dialog.mode !== 'edit') return; + const serverConfig = this.host._serverCfg; + if (!serverConfig) return; + const space = serverConfig.spaces.find((candidate) => candidate.id === dialog.spaceId); + if (!space) return; + const dependencies = collectSpaceMarkerDependencies( + serverConfig, this.host._layout || {}, dialog.spaceId || '', + ); + const deletingLastSpace = serverConfig.spaces.length === 1 + && serverConfig.spaces[0]?.id === dialog.spaceId; + if (dependencies.count && !deletingLastSpace) { + this.host._spaceDialog = { ...dialog, deleteBlockers: dependencies.count }; + return; + } + if (!confirm(this.host._t('confirm.delete_space', { title: space.title }))) return; + this.host._spaceDialog = { ...dialog, deleteBlockers: 0, busy: true }; + try { + if (this.host._saveConfigDebounced.pending()) this.host._saveConfigDebounced.flush(); + if (this.host._persistLayout.pending()) this.host._persistLayout.flush(); + await this.host._writeChain; + const response: { config_rev?: number; layout_rev?: number } = await this.host.hass.callWS({ + type: 'houseplan/space/delete', + space_id: dialog.spaceId, + expected_config_rev: this.host._cfgRev, + expected_layout_rev: this.host._layoutRev, + }); + const [configResponse, layoutResponse] = await Promise.all([ + this.host.hass.callWS({ type: 'houseplan/config/get' }), + this.host.hass.callWS({ type: 'houseplan/layout/get' }), + ]); + this.host._adoptStructuralResponses(configResponse, layoutResponse); + this.host._cfgRev = response?.config_rev ?? this.host._cfgRev; + this.host._layoutRev = response?.layout_rev ?? this.host._layoutRev; + this.host._spaceDialog = null; + if (this.host._space === dialog.spaceId) { + this.host._commitSpace(this.host._serverCfg!.spaces[0]?.id || ''); + } + this.host._regSignature = ''; + this.host._maybeRebuildDevices(); + this.host._showToast(this.host._t('toast.space_deleted')); + } catch (error: unknown) { + const failure = error as { code?: string }; + if (failure?.code === 'conflict' || failure?.code === 'space_in_use') { + await Promise.all([ + this.host._reloadConfigOnly(true), this.host._reloadLayoutOnly(), + ]); + } + const refreshedConfig = this.host._serverCfg; + if (this.host._spaceDialog && refreshedConfig) { + const refreshed = collectSpaceMarkerDependencies( + refreshedConfig, this.host._layout || {}, dialog.spaceId || '', + ); + const stillLastSpace = refreshedConfig.spaces.length === 1 + && refreshedConfig.spaces[0]?.id === dialog.spaceId; + this.host._spaceDialog = { + ...this.host._spaceDialog, + busy: false, + deleteBlockers: stillLastSpace ? 0 : refreshed.count, + }; + } + this.host._showToast(this.host._t('toast.delete_failed', { + err: this.host._errText(error), + })); + } + } + + public _startImport(): void { + const dialog = this.host._importDialog; + if (!dialog) return; + const titles = dialog.floors.filter((floor) => floor.checked).map((floor) => floor.name); + this.host._importDialog = null; + if (!titles.length) { + this._openSpaceDialog('create'); + return; + } + this.host._importQueue = titles; + this.host._importTotal = titles.length; + this._openNextImport(); + } + + public _openNextImport(): void { + const title = this.host._importQueue.shift(); + if (title === undefined) return; + const cellCm = newSpaceCellCm(this.host._imperial); + this.host._spaceDialog = { + mode: 'create', title, planUrl: null, planFile: null, + ...initialSpaceDisplayDraft(), + hideDecor: false, hideOpenings: false, zeroWallStyle: 'dashed', + roomColor: DEFAULT_ROOM_COLOR, roomOpacity: DEFAULT_ROOM_OPACITY, fillMode: 'custom', + customFill: null, + glowEnabled: true, + bgColor: null, + bgMode: 'daynight', northDeg: null, sunRays: null, + tempMin: DEFAULT_TEMP_MIN, tempMax: DEFAULT_TEMP_MAX, + showLqi: this.host._config?.show_signal ?? true, + cardFontScale: 1, + labelTemp: false, labelHum: false, labelLqi: false, labelLight: false, + cellCm, + cellCmInput: gridCellFieldValue(cellCm, this.host._imperial), + cellCmTouched: false, + busy: false, + }; + } + + public _skipImport(): void { + this.host._spaceDialog = null; + if (this.host._importQueue.length) this._openNextImport(); + else if (this.host._importTotal > 0 && this.host._model.length) { + this.host._importTotal = 0; + this.host._commitSpace(this.host._serverCfg!.spaces[0]?.id || this.host._space); + void this.host._requestMode('plan').then(() => { + this.host._showToast(this.host._t('import.done')); + }); + } + } + + public _renderImportDialog(): TemplateResult { + const dialog = this.host._importDialog!; + const selected = dialog.floors.filter((floor) => floor.checked).length; + return html` (this.host._importDialog = null)}> +
+
${this.host._t('import.hint')}
+ ${dialog.floors.map((floor, index) => html``)} +
+
+ + + +
+
`; + } + + private _boolInput(value: boolean, change: (checked: boolean) => void): TemplateResult { + return html` change((event.target as HTMLInputElement).checked)} />`; + } + + private _rangeInput( + min: number, max: number, step: number, value: number, change: (next: number) => void, + ): TemplateResult { + return html` change(Number((event.target as HTMLInputElement).value))} />`; + } + + public _renderSpaceDialog(): TemplateResult { + return this._renderSpaceDialogBody(this.host._spaceDialog!); + } + + private _renderSpaceDialogBody( + dialog: NonNullable, + ): TemplateResult { + const progress = this.host._importTotal > 0 && dialog.mode === 'create' + ? this.host._t('import.progress', { + i: this.host._importTotal - this.host._importQueue.length, + n: this.host._importTotal, + }) : ''; + const close = () => { + this.host._spaceDialog = null; + this.host._importQueue = []; + this.host._importTotal = 0; + }; + return html` +
+ + (this.host._spaceDialog = { + ...dialog, title: (event.target as HTMLInputElement).value, + })} /> + + + ${dialog.source === 'file' ? html`
+ ${dialog.planFile + ? html`${dialog.planFile.name}` + : dialog.planUrl + ? html`${this.host._t('space.plan_alt')}` + : html`${this.host._t('space.no_plan')}`} + + + this._pickPlanFile(event)} /> + + +
+ ${dialog.pickSaved ? this._renderServerPlans(dialog) : nothing}` : nothing} + + + +
+ { + const raw = (event.target as HTMLInputElement).value; + const parsed = strictNumber(raw); + const canonical = parsed == null ? null + : gridCellFieldToCm(parsed, this.host._imperial); + this.host._spaceDialog = { + ...dialog, + cellCmInput: raw, + cellCmTouched: true, + cellCm: canonical != null && canonical > 0 + ? Math.max(CELL_CM_MIN, Math.min(CELL_CM_MAX, canonical)) : dialog.cellCm, + }; + }} /> + ${this.host._t(this.host._imperial + ? 'space.scale_unit_imperial' : 'space.scale_unit')} +
+ + + + + +
${this.host._t('space.zero_wall_help')}
+ + + +
${this.host._t('space.hide_decor_tip')}
+ +
${this.host._t('space.hide_openings_tip')}
+ + ${([ + ['labelTemp', 'space.label_temp'], ['labelHum', 'space.label_hum'], + ['labelLqi', 'space.label_lqi'], ['labelLight', 'space.label_light'], + ] as const).map(([field, key]) => html``)} + +
+ ${this._rangeInput(50, 300, 5, Math.round(dialog.cardFontScale * 100), (value) => { + this.host._spaceDialog = { ...dialog, cardFontScale: value / 100 }; + })} + ${Math.round(dialog.cardFontScale * 100)}% +
+ ${this.host._renderCardPreview(dialog.cardFontScale, 1, 1)} +
+ ) => { + this.host._spaceDialog = { + ...dialog, roomColor: event.detail.color, roomOpacity: event.detail.opacity, + }; + }}> +
+ + + ${(dialog.bgMode ?? bgModeOf(this.host._settings, {})) === 'static' ? html` +
+ ) => { + this.host._spaceDialog = { ...dialog, bgColor: event.detail.color }; + }}> + ${dialog.bgColor ? html`` + : html`${this.host._t('space.bg_inherited')}`} +
` : nothing} + +
+ { + const raw = (event.target as HTMLInputElement).value.trim(); + const value = raw === '' ? null : Math.round(Number(raw)); + this.host._spaceDialog = { + ...dialog, + northDeg: value !== null && Number.isFinite(value) + ? Math.min(359, Math.max(0, value)) : null, + }; + }} /> + ${dialog.northDeg === null + ? this.host._t('space.north_inherited', { + v: northDegOf(this.host._settings, {}) === null + ? '—' : `${northDegOf(this.host._settings, {})}°`, + }) : '°'} +
+ + + + ${SPACE_FILL_UI_MODES.map((value) => [value, `fill.${value}`] as const).map( + ([value, key]) => html` + ${value === 'custom' && dialog.fillMode === 'custom' ? html` +
+ ${this.host._t('space.custom_fill')} + ) => { + this.host._spaceDialog = { + ...dialog, customFill: { c: event.detail.color, a: event.detail.opacity }, + }; + }}> + ${dialog.customFill ? html`` : nothing} +
` : nothing}`, + )} + + ${dialog.deleteBlockers ? html`` : nothing} +
+ +
`; + } +} diff --git a/src/i18n/en.json b/src/i18n/en.json index a7ed2354..ddce9004 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -5,6 +5,10 @@ "editor.open_group": "Tool group: {group}", "editor.group_active": "{group} — active: {item}", "editor.disabled_action": "{action} is unavailable: {reason}", + "editor.loading": "Loading editor…", + "editor.loading_aria": "The plan editor is loading", + "editor.load_failed": "Could not load the editor.", + "editor.refresh_advice": "Refresh the page and try again.", "color_picker.title": "Color picker", "color_picker.hue": "Hue", "color_picker.saturation": "Saturation", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index e3e6d42e..5440935f 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -5,6 +5,10 @@ "editor.open_group": "Группа инструментов: {group}", "editor.group_active": "{group} — активно: {item}", "editor.disabled_action": "{action} недоступно: {reason}", + "editor.loading": "Загружаем редактор…", + "editor.loading_aria": "Редактор плана загружается", + "editor.load_failed": "Не удалось загрузить редактор.", + "editor.refresh_advice": "Обновите страницу и повторите попытку.", "color_picker.title": "Выбор цвета", "color_picker.hue": "Оттенок", "color_picker.saturation": "Насыщенность", diff --git a/src/resize.ts b/src/resize.ts index 3c88cc67..05d2b29a 100644 --- a/src/resize.ts +++ b/src/resize.ts @@ -15,6 +15,7 @@ import { polygonArea, segmentsProperlyCross, polyContainsPoly, roomsOverlap, } from './logic'; import { classifyNearAxisSegment } from './near-axis'; +export { areaM2, formatArea } from './area-format'; /** Minimal room dimension in centimetres (owner: «мин. габарит ~30 см»). */ export const MIN_ROOM_CM = 30; @@ -1347,17 +1348,3 @@ export function clampRoomScale( } return good; } - -// ---------------- live numbers ---------------- - -/** Room area in m² from render units via the grid scale. */ -export function areaM2(poly: number[][], gridPitch: number, cellCm: number): number { - const cmPerUnit = cellCm / gridPitch; - return (polygonArea(poly) * cmPerUnit * cmPerUnit) / 1e4; -} - -/** "12.4 m²" or "133 ft²" per the HA unit system. */ -export function formatArea(m2: number, imperial: boolean): string { - if (imperial) return `${Math.round(m2 * 10.7639)} ft²`; - return `${(Math.round(m2 * 10) / 10).toFixed(1)} m²`; -} diff --git a/src/styles/dialogs.styles.ts b/src/styles/dialogs.styles.ts index 173cbd4c..d3efb259 100644 --- a/src/styles/dialogs.styles.ts +++ b/src/styles/dialogs.styles.ts @@ -114,9 +114,41 @@ export const dialogsStyles = css` --mdc-icon-size: 44px; color: var(--hp-accent); } + .editorloading { + position: absolute; + z-index: 74; + left: 50%; + top: 50%; + display: flex; + align-items: center; + gap: var(--sp-3); + padding: var(--sp-3) var(--sp-5); + border: 1px solid color-mix(in srgb, var(--hp-accent) 45%, transparent); + border-radius: 999px; + background: color-mix(in srgb, + var(--ha-card-background, var(--card-background-color, #111)) 92%, transparent); + color: var(--primary-text-color); + box-shadow: 0 6px 22px rgb(0 0 0 / 18%); + transform: translate(-50%, -50%); + pointer-events: none; + animation: editor-loading-in 0.15s ease both; + } + .editorloading ha-icon { + --mdc-icon-size: 22px; + color: var(--hp-accent); + animation: editor-loading-spin 0.9s linear infinite; + } + @keyframes editor-loading-in { + from { opacity: 0; transform: translate(-50%, calc(-50% + 4px)); } + to { opacity: 1; transform: translate(-50%, -50%); } + } + @keyframes editor-loading-spin { to { transform: rotate(360deg); } } @media (prefers-reduced-motion: reduce) { - .recoveryoverlay { + .recoveryoverlay, + .editorloading, + .editorloading ha-icon { transition: none; + animation: none; } } .oplock { diff --git a/src/wall-tool-compat.ts b/src/wall-tool-compat.ts new file mode 100644 index 00000000..2b71f20e --- /dev/null +++ b/src/wall-tool-compat.ts @@ -0,0 +1,4 @@ +/** Compatibility projection for a session token written by the old toolbar. */ +export function normalizeUnifiedWallTool(value: unknown): unknown { + return value === 'partition' ? 'draw' : value; +} diff --git a/test/bundle-assets.test.mjs b/test/bundle-assets.test.mjs new file mode 100644 index 00000000..adfd9bd0 --- /dev/null +++ b/test/bundle-assets.test.mjs @@ -0,0 +1,135 @@ +import assert from 'node:assert/strict'; +import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import test from 'node:test'; + +import { + buildBundleManifest, buildFingerprintPlugin, editorRuntimeRetryUrlPlugin, +} from '../scripts/bundle-manifest.mjs'; +import { assertBundleBudget } from '../scripts/bundle-budget.mjs'; +import { compareBundleTrees, sha256Bytes, verifyBundleTree } from '../scripts/bundle-tree.mjs'; +import { + minifyCssText, minifyStaticCssTemplates, +} from '../scripts/css-template-minifier.mjs'; + +test('CSS template minifier preserves semantic whitespace, strings and functions', () => { + const css = ` + /* owner note */ + .a .b, .c > .d { --pair: 1 2; width: calc(100% - 2px); } + .quoted { content: "a b /* text */"; background: url("a b.png"); } + .joined/**/.state { color: red; } + `; + assert.equal( + minifyCssText(css), + '.a .b,.c>.d{--pair:1 2;width:calc(100% - 2px);}.quoted{content:"a b /* text */";background:url("a b.png");}.joined.state{color:red;}', + ); + assert.equal( + minifyStaticCssTemplates('const s = css` .a { content: "\\`"; } `;', 'fixture.ts'), + 'const s = css`.a{content:"\\`";}`;', + ); +}); + +test('CSS template minifier fails closed on interpolation and malformed input', () => { + assert.throws( + () => minifyStaticCssTemplates('const s = css`color:${value}`;', 'fixture.ts'), + /interpolated css template/, + ); + assert.throws(() => minifyCssText('.a{/* nope'), /unclosed CSS comment/); + assert.throws(() => minifyCssText('.a{content:"nope}'), /unclosed CSS string/); +}); + +test('bundle manifest separates static initial graph from dynamic editor graph', () => { + const manifest = buildBundleManifest({ + 'houseplan-card.js': { + type: 'chunk', fileName: 'houseplan-card.js', code: 'entry', isEntry: true, + imports: ['shared.js'], dynamicImports: [ + 'houseplan-assets/editor.js', 'houseplan-assets/houseplan-onboarding-runtime-HASH.js', + ], + }, + 'shared.js': { + type: 'chunk', fileName: 'shared.js', code: 'shared', isEntry: false, + imports: [], dynamicImports: [], + }, + 'houseplan-assets/editor.js': { + type: 'chunk', fileName: 'houseplan-assets/editor.js', code: 'editor', isEntry: false, + imports: ['shared.js'], dynamicImports: [], + }, + 'houseplan-assets/houseplan-onboarding-runtime-HASH.js': { + type: 'chunk', fileName: 'houseplan-assets/houseplan-onboarding-runtime-HASH.js', + code: 'onboarding', isEntry: false, + imports: ['shared.js'], dynamicImports: [], + }, + }, 'fingerprint'); + assert.deepEqual(manifest.initialViewFiles, ['houseplan-card.js', 'shared.js']); + assert.deepEqual(manifest.lazyEditorFiles, ['houseplan-assets/editor.js']); + assert.deepEqual(manifest.lazyOnboardingFiles, [ + 'houseplan-assets/houseplan-onboarding-runtime-HASH.js', + ]); + assert.deepEqual(manifest.lazyFiles, [ + 'houseplan-assets/editor.js', 'houseplan-assets/houseplan-onboarding-runtime-HASH.js', + ]); + assert.doesNotThrow(() => assertBundleBudget(manifest, 1_000_000)); + assert.throws(() => assertBundleBudget(manifest, 1), /exceeds/); +}); + +test('build fingerprint is embedded for Windows and POSIX source ids', () => { + const plugin = buildFingerprintPlugin('exact-build'); + for (const id of ['C:\\repo\\src\\houseplan-card.ts', '/repo/src/houseplan-editor-runtime.ts']) { + const transformed = plugin.transform( + 'export const fingerprint = "__HOUSEPLAN_SOURCE_FINGERPRINT__";', id, + ); + assert.match(transformed.code, /"exact-build"/); + } +}); + +test('retry URL points at the content-hashed runtime chunk after naming', () => { + const plugin = editorRuntimeRetryUrlPlugin(); + const bundle = { + 'houseplan-assets/houseplan-card.js': { + type: 'chunk', fileName: 'houseplan-assets/houseplan-card.js', + code: 'new URL("__HOUSEPLAN_EDITOR_RETRY_ASSET__", import.meta.url);' + + 'new URL("__HOUSEPLAN_ONBOARDING_RETRY_ASSET__", import.meta.url)', modules: {}, + }, + 'houseplan-assets/houseplan-editor-runtime-HASH.js': { + type: 'chunk', fileName: 'houseplan-assets/houseplan-editor-runtime-HASH.js', code: '', + modules: { '/repo/src/houseplan-editor-runtime.ts': {} }, + }, + 'houseplan-assets/houseplan-onboarding-runtime-HASH.js': { + type: 'chunk', fileName: 'houseplan-assets/houseplan-onboarding-runtime-HASH.js', code: '', + modules: { '/repo/src/houseplan-onboarding-runtime.ts': {} }, + }, + }; + plugin.generateBundle({}, bundle); + assert.equal( + bundle['houseplan-assets/houseplan-card.js'].code, + 'new URL("./houseplan-editor-runtime-HASH.js", import.meta.url);' + + 'new URL("./houseplan-onboarding-runtime-HASH.js", import.meta.url)', + ); +}); + +test('bundle tree verification fails for a missing or tampered manifest asset', () => { + const temp = mkdtempSync(join(tmpdir(), 'houseplan-bundle-tree-')); + const source = join(temp, 'source'); + const target = join(temp, 'target'); + try { + for (const root of [source, target]) { + mkdirSync(join(root, 'houseplan-assets'), { recursive: true }); + writeFileSync(join(root, 'houseplan-card.js'), 'entry'); + writeFileSync(join(root, 'houseplan-assets', 'editor-hash.js'), 'editor'); + const files = ['houseplan-card.js', 'houseplan-assets/editor-hash.js'].map((path) => ({ + path, sha256: sha256Bytes(readFileSync(join(root, path))), + })); + writeFileSync(join(root, 'houseplan-assets.json'), `${JSON.stringify({ + schema: 1, fingerprint: 'fixture', entry: 'houseplan-card.js', files, + })}\n`); + } + assert.doesNotThrow(() => compareBundleTrees(source, target)); + writeFileSync(join(target, 'houseplan-assets', 'editor-hash.js'), 'tampered'); + assert.throws(() => verifyBundleTree(target), /manifest hash mismatch/); + rmSync(join(target, 'houseplan-assets', 'editor-hash.js')); + assert.throws(() => verifyBundleTree(target), /manifest asset is missing/); + } finally { + rmSync(temp, { recursive: true, force: true }); + } +}); diff --git a/test/bundle-freshness.test.mjs b/test/bundle-freshness.test.mjs index a8771224..0055ef51 100644 --- a/test/bundle-freshness.test.mjs +++ b/test/bundle-freshness.test.mjs @@ -1,4 +1,5 @@ import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { resolve } from 'node:path'; @@ -44,6 +45,35 @@ test('bundle freshness rejects a missing or stale fingerprint', async () => { } }); +test('bundle freshness verifies every manifest-listed demo asset', async () => { + const root = fixtureRoot(); + try { + const expected = sourceFingerprint(root); + const assets = resolve(root, 'demo/srv/assets'); + mkdirSync(resolve(assets, 'houseplan-assets'), { recursive: true }); + const files = [ + ['houseplan-card.js', 'entry'], + ['houseplan-assets/editor-HASH.js', 'editor'], + ]; + for (const [name, contents] of files) writeFileSync(resolve(assets, name), contents, 'utf8'); + writeFileSync(resolve(assets, 'houseplan-assets.json'), JSON.stringify({ + schema: 1, + fingerprint: expected, + files: files.map(([path, contents]) => ({ + path, + sha256: createHash('sha256').update(contents).digest('hex'), + })), + }), 'utf8'); + const page = { evaluate: async () => expected }; + assert.equal(await assertFreshDemoBundle(page, root), expected); + + writeFileSync(resolve(assets, 'houseplan-assets/editor-HASH.js'), 'tampered', 'utf8'); + await assert.rejects(assertFreshDemoBundle(page, root), /hash mismatch/); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + test('bundle freshness uses the target tree fingerprint contract', async () => { const root = fixtureRoot(); try { diff --git a/test/color-picker.test.mjs b/test/color-picker.test.mjs index 48b53c7c..4304c2ef 100644 --- a/test/color-picker.test.mjs +++ b/test/color-picker.test.mjs @@ -5,6 +5,7 @@ import { hexToRgb, hsvToHex, hsvToRgb, normalizeHexColor, normalizeHue, rgbToHex, rgbToHsv, } from '../test-build/color-picker.js'; import { readAllStylesSource } from './styles-source.mjs'; +import { readHouseplanProductionSource } from './houseplan-source.mjs'; test('hex drafts accept only three or six digits and normalize on commit', () => { assert.equal(normalizeHexColor('#AbC'), '#aabbcc'); @@ -40,7 +41,7 @@ test('HSV helpers wrap hue, clamp finite dimensions and keep grayscale achromati test('the shared component keeps its API and contains no nested native color picker', () => { const component = readFileSync(new URL('../src/hp-color-opacity.ts', import.meta.url), 'utf8'); - const card = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8'); + const card = readHouseplanProductionSource(); const sourceFiles = (directory) => readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { const target = new URL(entry.name + (entry.isDirectory() ? '/' : ''), directory); return entry.isDirectory() ? sourceFiles(target) : entry.name.endsWith('.ts') ? [target] : []; @@ -80,7 +81,7 @@ test('the hue range exposes one cyclic spectrum without restyling other ranges', }); test('activity color and ripple size keep independent readable rows', () => { - const card = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8'); + const card = readHouseplanProductionSource(); const styles = readAllStylesSource(); const start = card.indexOf("d.display === 'icon_ripple'"); const end = card.indexOf("marker.activity_alarm_note", start); diff --git a/test/coordinate-canonicalization.test.mjs b/test/coordinate-canonicalization.test.mjs index c1d30b48..032fb04d 100644 --- a/test/coordinate-canonicalization.test.mjs +++ b/test/coordinate-canonicalization.test.mjs @@ -1,6 +1,7 @@ import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import test from 'node:test'; +import { readHouseplanProductionSource } from './houseplan-source.mjs'; import { COORDINATE_DECIMALS, @@ -110,11 +111,15 @@ test('one position changes only x/y and preserves future metadata (#224)', () => }); test('frontend write paths adopt canonical candidates before persistence (#224)', () => { - const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8'); + const source = readHouseplanProductionSource(); assert.match(source, /enqueueSerializedWrite\(this\._writeChain, async \(\) =>/); assert.match( source, - /const candidate = canonicalizeConfigGeometry\(this\._serverCfg\);[\s\S]*config: candidate/, + /const candidate = canonicalizeConfigGeometry\(this\._serverCfg\);/, + ); + assert.match( + source, + /const canonicalCandidate = canonicalizeConfigGeometry\(candidate\);[\s\S]*config: canonicalCandidate/, ); assert.match( source, diff --git a/test/device-marker-polish-contract.test.mjs b/test/device-marker-polish-contract.test.mjs index 77c7396e..d03b6140 100644 --- a/test/device-marker-polish-contract.test.mjs +++ b/test/device-marker-polish-contract.test.mjs @@ -3,11 +3,14 @@ import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { readAllStylesSource } from './styles-source.mjs'; +import { readHouseplanProductionSource } from './houseplan-source.mjs'; // #266: styles.ts is being split into surface files — style greps must // see the whole sheet, other sources keep the direct read. const source = (name) => name === 'styles.ts' ? readAllStylesSource() + : name === 'houseplan-card.ts' + ? readHouseplanProductionSource() : readFileSync(new URL(`../src/${name}`, import.meta.url), 'utf8'); test('issue 213 resolves the effective base before the face without a late visual factor', () => { diff --git a/test/editor-runtime-loader.test.mjs b/test/editor-runtime-loader.test.mjs new file mode 100644 index 00000000..d86a3bc2 --- /dev/null +++ b/test/editor-runtime-loader.test.mjs @@ -0,0 +1,105 @@ +import assert from 'node:assert/strict'; +import { readFileSync, readdirSync, statSync } from 'node:fs'; +import test from 'node:test'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { EditorRuntimeLoader } from '../test-build/editor-runtime-loader.js'; + +const repoRoot = dirname(dirname(fileURLToPath(import.meta.url))); + +const deferred = () => { + let resolve; + let reject; + const promise = new Promise((ok, fail) => { resolve = ok; reject = fail; }); + return { promise, resolve, reject }; +}; + +test('editor runtime loader deduplicates concurrent intent and installs atomically', async () => { + const pending = deferred(); + const states = []; + const installed = []; + let loads = 0; + const loader = new EditorRuntimeLoader({ + expectedFingerprint: 'same', + load: async () => { loads++; return pending.promise; }, + install: (runtime) => installed.push(runtime), + stateChanged: (state) => states.push(state), + }); + + const first = loader.ensure(); + const second = loader.ensure(); + assert.equal(loads, 1); + assert.equal(loader.state, 'loading'); + assert.equal(installed.length, 0); + + pending.resolve({ fingerprint: 'same', create: () => ({ ready: true }) }); + assert.deepEqual(await Promise.all([first, second]), [true, true]); + assert.equal(loads, 1); + assert.deepEqual(installed, [{ ready: true }]); + assert.deepEqual(states, ['loading', 'ready']); +}); + +test('editor runtime loader retries the first failure exactly once', async () => { + const attempts = []; + const loader = new EditorRuntimeLoader({ + expectedFingerprint: 'same', + load: async (attempt) => { + attempts.push(attempt); + if (attempt === 0) throw new Error('404'); + return { fingerprint: 'same', create: () => 'runtime' }; + }, + install: () => {}, + }); + + assert.equal(await loader.ensure(), true); + assert.deepEqual(attempts, [0, 1]); + assert.equal(await loader.ensure(), true); + assert.deepEqual(attempts, [0, 1], 'a ready loader never imports again'); +}); + +test('fingerprint mismatch fails closed after one retry and remains terminal', async () => { + const attempts = []; + const installed = []; + const failures = []; + const loader = new EditorRuntimeLoader({ + expectedFingerprint: 'entry', + load: async (attempt) => { + attempts.push(attempt); + return { fingerprint: 'other', create: () => 'mixed-runtime' }; + }, + install: (runtime) => installed.push(runtime), + failed: (error) => failures.push(String(error)), + }); + + assert.equal(await loader.ensure(), false); + assert.deepEqual(attempts, [0, 1]); + assert.deepEqual(installed, []); + assert.equal(loader.state, 'failed'); + assert.match(failures[0], /fingerprint mismatch/); + + assert.equal(await loader.ensure(), false); + assert.deepEqual(attempts, [0, 1], 'terminal failure requires a page refresh'); +}); + +test('lazy runtime boundary keeps the eager entry independent and does not grow any usage', () => { + const card = readFileSync(join(repoRoot, 'src', 'houseplan-card.ts'), 'utf8'); + assert.doesNotMatch( + card, + /^import(?!\s+type\b)[^;]*houseplan-editor-runtime[^;]*;/m, + 'the eager card must not statically import the editor runtime', + ); + const sources = []; + const visit = (directory) => { + for (const name of readdirSync(directory)) { + const path = join(directory, name); + if (statSync(path).isDirectory()) visit(path); + else if (/\.ts$/.test(name)) sources.push(readFileSync(path, 'utf8')); + } + }; + visit(join(repoRoot, 'src')); + const anyCount = sources.reduce( + (total, source) => total + (source.match(/\bany\b/g)?.length || 0), 0, + ); + assert.ok(anyCount <= 1123, `src any count grew from 1123 to ${anyCount}`); +}); diff --git a/test/fixed-floor-contract.test.mjs b/test/fixed-floor-contract.test.mjs index 83aca6f8..59796ecd 100644 --- a/test/fixed-floor-contract.test.mjs +++ b/test/fixed-floor-contract.test.mjs @@ -1,8 +1,9 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; +import { readHouseplanProductionSource } from './houseplan-source.mjs'; -const card = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8'); +const card = readHouseplanProductionSource(); const editor = readFileSync(new URL('../src/editor.ts', import.meta.url), 'utf8'); test('active-space writes go through the fixed-floor mutation boundary', () => { diff --git a/test/golden-matrix.test.mjs b/test/golden-matrix.test.mjs index 1a9caf2a..4e9e9595 100644 --- a/test/golden-matrix.test.mjs +++ b/test/golden-matrix.test.mjs @@ -6,6 +6,7 @@ import { GOLDEN_MATRIX_VERSION, GOLDEN_SCENARIOS, OPENING_SYMBOL_EXISTING_GOLDEN_IMPACT, } from '../demo/golden/matrix.mjs'; import { fixtureWallKey } from '../demo/fixtures/visual-matrix.mjs'; +import { readHouseplanProductionSource } from './houseplan-source.mjs'; test('golden matrix has stable unique ids and bounded comparison thresholds', () => { assert.equal(Number.isInteger(GOLDEN_MATRIX_VERSION) && GOLDEN_MATRIX_VERSION > 0, true); @@ -739,7 +740,7 @@ test('golden overrides fail closed on misspelled fixture references', () => { }); test('a light source paints exactly one region: the floor it can see', () => { - const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8'); + const source = readHouseplanProductionSource(); // One region per source, and it is the visibility polygon clipped to floor. assert.match(source, /visibilityPolygon\(\[pos\.x, pos\.y\], R, occluders/); assert.match(source, @@ -772,7 +773,7 @@ test('a light source paints exactly one region: the floor it can see', () => { }); test('all destructive editor dialogs use the medium shell and shared responsive footer groups', () => { - const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8'); + const source = readHouseplanProductionSource(); for (const method of ['_renderOpeningDialog', '_renderPhysicalDialog', '_renderSpaceDialog']) { const start = source.indexOf(`private ${method}`); assert.notEqual(start, -1, method); diff --git a/test/houseplan-source.mjs b/test/houseplan-source.mjs new file mode 100644 index 00000000..d8efbc3e --- /dev/null +++ b/test/houseplan-source.mjs @@ -0,0 +1,55 @@ +import { readFileSync } from 'node:fs'; +import ts from 'typescript'; + +/** + * Source-contract tests historically inspected one monolithic card file. + * Editor implementations are now lazy, so expose the same logical production + * surface with the implementation before its typed host stubs. + */ +export function readHouseplanProductionSource() { + const runtime = readFileSync( + new URL('../src/houseplan-editor-runtime.ts', import.meta.url), + 'utf8', + ); + const card = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8'); + const parse = (name, source) => ts.createSourceFile( + name, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS, + ); + const runtimeFile = parse('houseplan-editor-runtime.ts', runtime); + const cardFile = parse('houseplan-card.ts', card); + const classOf = (file, name) => file.statements.find( + (statement) => ts.isClassDeclaration(statement) && statement.name?.text === name, + ); + const runtimeClass = classOf(runtimeFile, 'HouseplanEditorRuntime'); + const cardClass = classOf(cardFile, 'HouseplanCard'); + if (!runtimeClass || !cardClass) throw new Error('Houseplan source classes are missing'); + const nameOf = (member, file) => member.name?.getText(file); + const implementations = new Map(runtimeClass.members + .map((member) => [nameOf(member, runtimeFile), member]) + .filter(([name]) => name)); + const replacements = []; + for (const member of cardClass.members) { + const name = nameOf(member, cardFile); + const implementation = implementations.get(name); + if (!implementation) continue; + const baseText = card.slice(member.getStart(cardFile), member.end); + if (!baseText.includes('_editorRuntime')) continue; + const visibility = /^(private|protected|public)\b/.exec(baseText)?.[1] || 'private'; + const implementationText = runtime + .slice(implementation.getStart(runtimeFile), implementation.end) + .replace(/^public\b/, visibility) + .replaceAll('this.host.', 'this.'); + replacements.push({ + from: member.getStart(cardFile), + to: member.end, + text: implementationText, + }); + } + let reconstructed = card; + for (const replacement of replacements.sort((a, b) => b.from - a.from)) { + reconstructed = reconstructed.slice(0, replacement.from) + + replacement.text + + reconstructed.slice(replacement.to); + } + return reconstructed; +} diff --git a/test/i18n.test.mjs b/test/i18n.test.mjs index d64e5956..40f94d35 100644 --- a/test/i18n.test.mjs +++ b/test/i18n.test.mjs @@ -12,13 +12,14 @@ import { resolveLanguageCode, } from '../test-build/i18n/registry.js'; import { langOf } from '../test-build/i18n.js'; +import { readHouseplanProductionSource } from './houseplan-source.mjs'; const dictionaries = new Map( LANGUAGE_REGISTRY.map(({ code, dictionary }) => [code, dictionary]), ); const en = dictionaries.get('en'); const ru = dictionaries.get('ru'); -const cardSource = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8'); +const cardSource = readHouseplanProductionSource(); test('i18n: registry codes and English fallback are valid', () => { const codes = LANGUAGE_REGISTRY.map(({ code }) => code); diff --git a/test/junction-limits.test.mjs b/test/junction-limits.test.mjs index b7fc00cf..242a8d74 100644 --- a/test/junction-limits.test.mjs +++ b/test/junction-limits.test.mjs @@ -9,6 +9,7 @@ import { cmToUnits, newViolations, } from '../test-build/junction-limits.js'; import { GRID_STEP_N } from '../test-build/space-geometry.js'; +import { readHouseplanProductionSource } from './houseplan-source.mjs'; // #329, решения владельца 2026-08-27. Пороги абсолютные: 15°, 6 стен, // max(20 см, толщина), 5 см, 25 см². cell_cm на них не влияет. @@ -383,9 +384,7 @@ test('#330 AC4: кэш baseline инвалидируется по конфиг- // приёмом, каким #293 пинит обвязку смока. Поведенческая половина AC4 // (N move → N+1 вычислений в реальном жесте) живёт в // demo/smoke_junction_limits.mjs (resizeBaselineCachedPerGesture). - const source = readFileSync( - new URL('../src/houseplan-card.ts', import.meta.url), 'utf8', - ); + const source = readHouseplanProductionSource(); const method = source.slice( source.indexOf('private _junctionLimitsIntroduced('), source.indexOf('private _commitPhysicalGeometry('), diff --git a/test/native-select-contract.test.mjs b/test/native-select-contract.test.mjs index 14f60c04..8fdee3df 100644 --- a/test/native-select-contract.test.mjs +++ b/test/native-select-contract.test.mjs @@ -3,9 +3,10 @@ import { readdirSync, readFileSync } from 'node:fs'; import { join, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; import test from 'node:test'; +import { readHouseplanProductionSource } from './houseplan-source.mjs'; const srcRoot = fileURLToPath(new URL('../src/', import.meta.url)); -const source = readFileSync(join(srcRoot, 'houseplan-card.ts'), 'utf8'); +const source = readHouseplanProductionSource(); const walkTypescript = (directory) => readdirSync(directory, { withFileTypes: true }) .flatMap((entry) => { const path = join(directory, entry.name); @@ -27,7 +28,7 @@ test('native selects explicitly project selection unless they are command menus' // This is a command menu, not persisted state: it intentionally resets to // its empty first row after inserting an HA variable into decorative text. - assert.deepEqual(unsafe, ['houseplan-card.ts:decor-live-attribute']); + assert.deepEqual(unsafe, ['houseplan-editor-runtime.ts:decor-live-attribute']); }); test('persisted dynamic marker selects project their selected option explicitly', () => { diff --git a/test/near-axis.test.mjs b/test/near-axis.test.mjs index 8bbdee6e..5c168b5c 100644 --- a/test/near-axis.test.mjs +++ b/test/near-axis.test.mjs @@ -12,6 +12,7 @@ import { import { optimizePlans } from '../test-build/plan-optimizer.js'; import { MULTI_WALL_NEAR_ORTHOGONAL_MAX_DEGREES } from '../test-build/wall-thickness.js'; import { nearAxisProfile } from '../scripts/model-invariants.mjs'; +import { readHouseplanProductionSource } from './houseplan-source.mjs'; const fixture = JSON.parse(readFileSync( new URL('./fixtures/279-near-orthogonal-junction.json', import.meta.url), 'utf8', @@ -21,7 +22,7 @@ const sourceOf = (file) => readFileSync(new URL(`../src/${file}`, import.meta.ur test('#290 authoring, Optimize and renderer use one near-axis threshold source', () => { const renderer = sourceOf('wall-thickness.ts'); - const authoring = sourceOf('houseplan-card.ts'); + const authoring = readHouseplanProductionSource(); const optimizer = sourceOf('plan-optimizer.ts'); assert.match(renderer, /import \{ NEAR_AXIS_MAX_DEGREES \} from '\.\/near-axis';/); diff --git a/test/open-passage-contract.test.mjs b/test/open-passage-contract.test.mjs index fa9d7c50..c0d24b22 100644 --- a/test/open-passage-contract.test.mjs +++ b/test/open-passage-contract.test.mjs @@ -2,8 +2,9 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { readAllStylesSource } from './styles-source.mjs'; +import { readHouseplanProductionSource } from './houseplan-source.mjs'; -const card = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8'); +const card = readHouseplanProductionSource(); const styles = readAllStylesSource(); const staticRender = readFileSync(new URL('../src/space-render.ts', import.meta.url), 'utf8'); const backend = readFileSync(new URL('../custom_components/houseplan/websocket_api.py', import.meta.url), 'utf8'); diff --git a/test/optional-space-model-contract.test.mjs b/test/optional-space-model-contract.test.mjs index 3adb92f6..0f8d8539 100644 --- a/test/optional-space-model-contract.test.mjs +++ b/test/optional-space-model-contract.test.mjs @@ -1,8 +1,9 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; +import { readHouseplanProductionSource } from './houseplan-source.mjs'; -const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8'); +const source = readHouseplanProductionSource(); const methodBody = (name) => { const start = source.search(new RegExp(`(?:private|protected)\\s+(?:get\\s+|async\\s+)?${name}(?:\\s*=)?(?:\\(|:)`)); diff --git a/test/performance-contract.test.mjs b/test/performance-contract.test.mjs index dc5b3eb0..4bb489d6 100644 --- a/test/performance-contract.test.mjs +++ b/test/performance-contract.test.mjs @@ -6,6 +6,7 @@ import { GLOW_CARD_CONTRACT, LARGE_HOUSE_CARD_CONTRACT, } from '../demo/performance/card-contract.mjs'; +import { readHouseplanProductionSource } from './houseplan-source.mjs'; const directCardMembers = (relativePath) => { const source = readFileSync(new URL(`../${relativePath}`, import.meta.url), 'utf8'); @@ -38,10 +39,10 @@ test('Glow benchmark declares every private card member it consumes', () => { }); test('performance contracts reference real production members', () => { - const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8'); + const source = readHouseplanProductionSource(); for (const contract of [LARGE_HOUSE_CARD_CONTRACT, GLOW_CARD_CONTRACT]) { for (const name of declaredMembers(contract)) { - assert.match(source, new RegExp(`\\b(?:private\\s+(?:get\\s+)?|get\\s+)${name}\\b`), + assert.match(source, new RegExp(`\\b(?:private\\s+(?:declare\\s+|get\\s+)?|get\\s+)${name}\\b`), `${contract.label} declares missing production member ${name}`); } } @@ -66,7 +67,7 @@ test('contract accepts recent optional fields only when their runtime type is va }); test('view render resolves structural wall cuts once per frame, not once per room', () => { - const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8'); + const source = readHouseplanProductionSource(); const start = source.indexOf('const allZeroCuts = this._openCuts();'); const end = source.indexOf('${this._renderOpeningTunnelFills(space, roomFills)}', start); assert.ok(start >= 0 && end > start, 'view room-render block is present'); @@ -87,7 +88,7 @@ test('view render resolves structural wall cuts once per frame, not once per roo }); test('room inner faces are structurally cached and shared by both fill layers', () => { - const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8'); + const source = readHouseplanProductionSource(); const helperStart = source.indexOf('private _innerRoomContour('); const helperEnd = source.indexOf('\n /**', helperStart); assert.ok(helperStart >= 0 && helperEnd > helperStart, 'inner-contour cache helper is present'); @@ -113,7 +114,7 @@ test('room inner faces are structurally cached and shared by both fill layers', }); test('wall and light geometry reuse bounded caches before structural work', () => { - const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8'); + const source = readHouseplanProductionSource(); const unionStart = source.indexOf('private _wallUnionGeometry()'); const unionEnd = source.indexOf('\n /** Thick-wall spans', unionStart); diff --git a/test/render-device-snapshot.test.mjs b/test/render-device-snapshot.test.mjs index 801ef5c0..a03c14ff 100644 --- a/test/render-device-snapshot.test.mjs +++ b/test/render-device-snapshot.test.mjs @@ -5,6 +5,7 @@ import { readFileSync } from 'node:fs'; import { createRenderDeviceSnapshot, presentationSnapshotKey, renderDeviceSnapshotPositions, } from '../test-build/render-device-snapshot.js'; +import { readHouseplanProductionSource } from './houseplan-source.mjs'; test('snapshot positions skip resolution until a renderable plan exists', () => { const devices = [{ id: 'one' }, { id: 'two' }]; @@ -71,7 +72,7 @@ const methodBody = (source, name) => { }; test('atomic plan render paths do not bypass RenderDeviceSnapshot with this.hass', () => { - const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8'); + const source = readHouseplanProductionSource(); for (const name of [ '_roomLqi', '_resolvedRoomFills', '_sunNow', '_renderSunRays', '_renderGlowLayer', '_renderVacuums', '_renderVacFit', '_renderDevice', '_roomTemp', '_roomHum', '_openingAmt', @@ -82,7 +83,7 @@ test('atomic plan render paths do not bypass RenderDeviceSnapshot with this.hass }); test('the card gates snapshot positions on the render model', () => { - const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8'); + const source = readHouseplanProductionSource(); const capture = methodBody(source, '_captureRenderDeviceSnapshot'); assert.match( capture, @@ -97,7 +98,7 @@ test('the card gates snapshot positions on the render model', () => { }); test('opening references use their own availability policy without weakening plan tombstones', () => { - const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8'); + const source = readHouseplanProductionSource(); for (const name of [ '_contactCandidates', '_lockCandidates', '_openingAmt', '_renderOpenings', '_renderOpeningLocks', '_renderOpeningInfoCard', '_lockAction', @@ -125,7 +126,7 @@ test('opening references use their own availability policy without weakening pla }); test('lock actuation remains guarded inside the one sanctioned opening-card method', () => { - const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8'); + const source = readHouseplanProductionSource(); const action = methodBody(source, '_lockAction'); const guardAt = action.indexOf('_openingEntityAvailable(entityId)'); const confirmAt = action.indexOf('confirm('); @@ -136,7 +137,7 @@ test('lock actuation remains guarded inside the one sanctioned opening-card meth }); test('marker delete/re-add and opening save remain separate config transactions', () => { - const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8'); + const source = readHouseplanProductionSource(); const saveOpening = methodBody(source, '_saveOpening'); const saveMarker = methodBody(source, '_saveMarker'); assert.match(saveOpening, /sp\.openings/); diff --git a/test/resize-production-path.test.mjs b/test/resize-production-path.test.mjs index ade4b287..37f22a42 100644 --- a/test/resize-production-path.test.mjs +++ b/test/resize-production-path.test.mjs @@ -2,10 +2,12 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import { readAllStylesSource } from './styles-source.mjs'; +import { readHouseplanProductionSource } from './houseplan-source.mjs'; -const card = fs.readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8'); +const card = readHouseplanProductionSource(); const resize = fs.readFileSync(new URL('../src/resize.ts', import.meta.url), 'utf8'); const controller = fs.readFileSync(new URL('../src/resize-controller.ts', import.meta.url), 'utf8'); +const editorRuntime = fs.readFileSync(new URL('../src/houseplan-editor-runtime.ts', import.meta.url), 'utf8'); const wallInvariant = fs.readFileSync(new URL('../src/wall-record-preservation.ts', import.meta.url), 'utf8'); const invariantCli = fs.readFileSync(new URL('../scripts/model-invariants.mjs', import.meta.url), 'utf8'); const styles = readAllStylesSource(); @@ -99,7 +101,7 @@ test('#264 Resize controller is the sole mutable gesture owner', () => { for (const mirror of ['_rszSel', '_rszDrag', '_rszPreview', '_rszLive', '_rszEligibilityCache']) { assert.equal(card.includes(`private ${mirror}`), false, `${mirror} remains a root state mirror`); } - assert.match(card, /new ResizeController { diff --git a/test/wall-segment-model.test.mjs b/test/wall-segment-model.test.mjs index 2015da0e..50900257 100644 --- a/test/wall-segment-model.test.mjs +++ b/test/wall-segment-model.test.mjs @@ -16,6 +16,7 @@ import { } from '../test-build/wall-segment-model.js'; import { wallKey } from '../test-build/wall-thickness.js'; import { GRID_STEP_N } from '../test-build/space-geometry.js'; +import { readHouseplanProductionSource } from './houseplan-source.mjs'; const rectangle = (id, x1 = 0, y1 = 0, x2 = 1, y2 = 1) => ({ id, @@ -377,7 +378,7 @@ test('initial migration resolves a reserved deterministic id with the documented }); test('every frontend structural transaction crosses the wall identity barrier', () => { - const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8'); + const source = readHouseplanProductionSource(); const optimizer = readFileSync(new URL('../src/plan-optimizer.ts', import.meta.url), 'utf8'); const methodSource = (name) => { const start = source.indexOf(`private ${name}(`); diff --git a/test/wall-union-isolation.test.mjs b/test/wall-union-isolation.test.mjs index 60525909..decbebf7 100644 --- a/test/wall-union-isolation.test.mjs +++ b/test/wall-union-isolation.test.mjs @@ -10,6 +10,7 @@ import { import { spaceModels } from '../test-build/space-geometry.js'; import { wallBodiesGeometry, wallBodiesUnionPath } from '../test-build/wall-thickness.js'; import { checkPhysicalGeometry } from '../scripts/model-invariants.mjs'; +import { readHouseplanProductionSource } from './houseplan-source.mjs'; const fixture = JSON.parse(readFileSync( new URL('./fixtures/278-wall-union-isolation.json', import.meta.url), 'utf8', @@ -98,7 +99,7 @@ test('#278 physical fingerprint ignores decor but covers every strict writer fie }); test('#278 production source routes physical writers through one barrier and decor around it', () => { - const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8'); + const source = readHouseplanProductionSource(); for (const historyKey of [ 'draft_segment', 'wall_chain_finish', 'column_add', 'physical_edit', 'physical_delete', 'physical_move', 'resize_room', 'wall_thickness', diff --git a/tests_backend/test_frontend_assets.py b/tests_backend/test_frontend_assets.py new file mode 100644 index 00000000..a1ed056b --- /dev/null +++ b/tests_backend/test_frontend_assets.py @@ -0,0 +1,68 @@ +"""Pure path/manifest guards for public frontend chunks (#337).""" +from __future__ import annotations + +import json +import importlib.util +from pathlib import Path + +_PATH = Path(__file__).parents[1] / "custom_components" / "houseplan" / "frontend_asset_manifest.py" +_SPEC = importlib.util.spec_from_file_location("hp_frontend_asset_manifest", _PATH) +assert _SPEC and _SPEC.loader +_MODULE = importlib.util.module_from_spec(_SPEC) +_SPEC.loader.exec_module(_MODULE) +resolve_frontend_asset = _MODULE.resolve_frontend_asset + + +def _root(tmp_path: Path) -> Path: + root = tmp_path / "frontend" + assets = root / "houseplan-assets" + assets.mkdir(parents=True) + (assets / "editor-abc.js").write_text("export {};", encoding="utf-8") + (assets / "stale.js").write_text("export {};", encoding="utf-8") + (root / "houseplan-assets.json").write_text(json.dumps({ + "schema": 1, + "files": [{"path": "houseplan-assets/editor-abc.js"}], + }), encoding="utf-8") + return root + + +def test_frontend_asset_requires_current_manifest_entry(tmp_path: Path) -> None: + root = _root(tmp_path) + assert resolve_frontend_asset(root, "editor-abc.js") == ( + root / "houseplan-assets" / "editor-abc.js" + ).resolve() + assert resolve_frontend_asset(root, "stale.js") is None + assert resolve_frontend_asset(root, "missing.js") is None + assert resolve_frontend_asset(root, "houseplan-assets.json") is None + + +def test_frontend_asset_refuses_traversal_and_malformed_manifest(tmp_path: Path) -> None: + root = _root(tmp_path) + for value in ( + "../editor-abc.js", "..\\editor-abc.js", "nested/editor-abc.js", + "..%2feditor-abc.js", "editor-abc.css", "..", "", + ): + assert resolve_frontend_asset(root, value) is None + (root / "houseplan-assets.json").write_text("not json", encoding="utf-8") + assert resolve_frontend_asset(root, "editor-abc.js") is None + + +def test_frontend_asset_refuses_wrong_schema_and_symlink_escape(tmp_path: Path) -> None: + root = _root(tmp_path) + manifest = root / "houseplan-assets.json" + manifest.write_text(json.dumps({ + "schema": 2, "files": [{"path": "houseplan-assets/editor-abc.js"}], + }), encoding="utf-8") + assert resolve_frontend_asset(root, "editor-abc.js") is None + + outside = tmp_path / "outside.js" + outside.write_text("outside", encoding="utf-8") + link = root / "houseplan-assets" / "linked.js" + try: + link.symlink_to(outside) + except OSError: + return # Windows without Developer Mode; containment is covered elsewhere. + manifest.write_text(json.dumps({ + "schema": 1, "files": [{"path": "houseplan-assets/linked.js"}], + }), encoding="utf-8") + assert resolve_frontend_asset(root, "linked.js") is None diff --git a/tsconfig.test.json b/tsconfig.test.json index 1809a5af..cfd276de 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -24,7 +24,7 @@ "src/space-geometry.ts", "src/junction-limits.ts", "src/space-order.ts", "src/card-editor-validation.ts", "src/signing.ts", "src/initial-load.ts", "src/space-model-selection.ts", "src/space-dialog.ts", - "src/visual-continuity.ts", "src/mode-transition.ts", "src/pointer-modality.ts", + "src/visual-continuity.ts", "src/mode-transition.ts", "src/editor-runtime-loader.ts", "src/pointer-modality.ts", "src/render-device-snapshot.ts", "src/command-stack.ts", "src/coordinate-canonicalization.ts",