mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 03:38:47 +00:00
@@ -1,111 +1,111 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"fingerprint": "e6c51f2467ae4e22a792e536ae954291ab5a46915e728b216c8c45bc6816b209",
|
||||
"fingerprint": "817110bad40d29ee0aa8e4ef2dadfda9e310dfa6346ddb5d2725ecafb359f073",
|
||||
"entry": "houseplan-card.js",
|
||||
"initialViewFiles": [
|
||||
"houseplan-assets/houseplan-card-DI__VUPO.js",
|
||||
"houseplan-assets/houseplan-card-BrUCyMoE.js",
|
||||
"houseplan-card.js"
|
||||
],
|
||||
"initialViewGzipBytes": 272469,
|
||||
"initialViewGzipBytes": 271841,
|
||||
"lazyFiles": [
|
||||
"houseplan-assets/de-gTJXGK7s.js",
|
||||
"houseplan-assets/editor-LGCA8WjH.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Cu2NFP8N.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-B4PXZ4um.js",
|
||||
"houseplan-assets/space-deletion-Do1Uuvoz.js"
|
||||
"houseplan-assets/de-CndLXoON.js",
|
||||
"houseplan-assets/editor-H80bjnWS.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-LLPkWHY0.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BkWWzfSR.js",
|
||||
"houseplan-assets/space-deletion-CLA5ezlL.js"
|
||||
],
|
||||
"lazyGzipBytes": 166314,
|
||||
"lazyGzipBytes": 165904,
|
||||
"lazyEditorFiles": [
|
||||
"houseplan-assets/editor-LGCA8WjH.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Cu2NFP8N.js",
|
||||
"houseplan-assets/space-deletion-Do1Uuvoz.js"
|
||||
"houseplan-assets/editor-H80bjnWS.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-LLPkWHY0.js",
|
||||
"houseplan-assets/space-deletion-CLA5ezlL.js"
|
||||
],
|
||||
"lazyEditorGzipBytes": 137196,
|
||||
"lazyEditorGzipBytes": 136806,
|
||||
"lazyOnboardingFiles": [
|
||||
"houseplan-assets/houseplan-onboarding-runtime-B4PXZ4um.js",
|
||||
"houseplan-assets/space-deletion-Do1Uuvoz.js"
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BkWWzfSR.js",
|
||||
"houseplan-assets/space-deletion-CLA5ezlL.js"
|
||||
],
|
||||
"lazyOnboardingGzipBytes": 7204,
|
||||
"lazyOnboardingGzipBytes": 7193,
|
||||
"lazyLocaleFiles": [
|
||||
"houseplan-assets/de-gTJXGK7s.js"
|
||||
"houseplan-assets/de-CndLXoON.js"
|
||||
],
|
||||
"lazyLocaleGzipBytes": 22468,
|
||||
"lazyLocaleGzipBytes": 22454,
|
||||
"files": [
|
||||
{
|
||||
"path": "houseplan-assets/de-gTJXGK7s.js",
|
||||
"sha256": "2ed2a8aad10d9eaa16fe3fe2e591c2d736d948b2fd35aca01fcffb33264e1284",
|
||||
"path": "houseplan-assets/de-CndLXoON.js",
|
||||
"sha256": "c3fb30be483664a92a555e2a916d2d6201a5d950d77385a34869445625732be1",
|
||||
"rawBytes": 73919,
|
||||
"gzipBytes": 22468,
|
||||
"gzipBytes": 22454,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/editor-LGCA8WjH.js",
|
||||
"sha256": "0f4ce2555703ec01ee188cf84badfdee661587e849c1b604ec88354c3f94894f",
|
||||
"path": "houseplan-assets/editor-H80bjnWS.js",
|
||||
"sha256": "bcd9f271360a94dfbf63d5bcdf3c965aed1e1b17d0c543ac0b554d5527b90250",
|
||||
"rawBytes": 3831,
|
||||
"gzipBytes": 1585,
|
||||
"gzipBytes": 1580,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-DI__VUPO.js"
|
||||
"houseplan-assets/houseplan-card-BrUCyMoE.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-card-DI__VUPO.js",
|
||||
"sha256": "35a537b289b388f34a5f6efc01a95254207be9880fa9fd931723ccfa258ad602",
|
||||
"rawBytes": 960284,
|
||||
"gzipBytes": 271715,
|
||||
"path": "houseplan-assets/houseplan-card-BrUCyMoE.js",
|
||||
"sha256": "bdcf3f7e9306428b2583f1e662541bebebcf2f106b48a53df117c6c9c54c5cde",
|
||||
"rawBytes": 958245,
|
||||
"gzipBytes": 271090,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/de-gTJXGK7s.js",
|
||||
"houseplan-assets/editor-LGCA8WjH.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Cu2NFP8N.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-B4PXZ4um.js"
|
||||
"houseplan-assets/de-CndLXoON.js",
|
||||
"houseplan-assets/editor-H80bjnWS.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-LLPkWHY0.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BkWWzfSR.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-Cu2NFP8N.js",
|
||||
"sha256": "4eb656ee069026534dace297817908e97d0e82e63ba1059d0c3d6c283d913344",
|
||||
"rawBytes": 522188,
|
||||
"gzipBytes": 135057,
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-LLPkWHY0.js",
|
||||
"sha256": "e49282b51d104baff861b3c21261e66dcbfd58807e0db444b04f1f171499e24d",
|
||||
"rawBytes": 520292,
|
||||
"gzipBytes": 134677,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-DI__VUPO.js",
|
||||
"houseplan-assets/space-deletion-Do1Uuvoz.js"
|
||||
"houseplan-assets/houseplan-card-BrUCyMoE.js",
|
||||
"houseplan-assets/space-deletion-CLA5ezlL.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-B4PXZ4um.js",
|
||||
"sha256": "73ad6471b2395b982d233754598fcb6fb296c29b01a188cce724c2861e044187",
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-BkWWzfSR.js",
|
||||
"sha256": "5446043fbf2a383f84df3751b1fd97ecac72d8bcf3c254da80ad4176538c91da",
|
||||
"rawBytes": 26356,
|
||||
"gzipBytes": 6650,
|
||||
"gzipBytes": 6644,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-DI__VUPO.js",
|
||||
"houseplan-assets/space-deletion-Do1Uuvoz.js"
|
||||
"houseplan-assets/houseplan-card-BrUCyMoE.js",
|
||||
"houseplan-assets/space-deletion-CLA5ezlL.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/space-deletion-Do1Uuvoz.js",
|
||||
"sha256": "258cd7b7338ca746122d6afcfc63f81d34be23a7bd31f1599ed8d45ec0daa5b6",
|
||||
"rawBytes": 911,
|
||||
"gzipBytes": 554,
|
||||
"path": "houseplan-assets/space-deletion-CLA5ezlL.js",
|
||||
"sha256": "ec0519abd52ab649e2ed8f5744cc8284d96d8a83f3c9517df7f5070be63a66e9",
|
||||
"rawBytes": 906,
|
||||
"gzipBytes": 549,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-card.js",
|
||||
"sha256": "031ef9710a85647066f82b62cd7a294c752493c0391cf59b1688380f7cac2a18",
|
||||
"sha256": "9bb7a0cb60837b9a006d1c0d125c820ab083b270a9c6130147cd5c8c8b47cff1",
|
||||
"rawBytes": 1089,
|
||||
"gzipBytes": 754,
|
||||
"gzipBytes": 751,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-DI__VUPO.js"
|
||||
"houseplan-assets/houseplan-card-BrUCyMoE.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="817110bad40d29ee0aa8e4ef2dadfda9e310dfa6346ddb5d2725ecafb359f073";import{c as e,l as o,t,c_ as s,A as a,c$ as i,d0 as l,d1 as n,E as r,b as c}from"./houseplan-card-BrUCyMoE.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._formData}
|
||||
.schema=${e}
|
||||
.computeLabel=${e=>h[e.name]||e.name}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>`;return c`
|
||||
${d(f.slice(0,3))}
|
||||
${_?c`<div class="default-floor-error" role="alert"
|
||||
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
|
||||
${t(s,"editor.default_floor_missing",{id:_})}
|
||||
</div>`:a}
|
||||
${d(f.slice(3))}
|
||||
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",h);
|
||||
File diff suppressed because one or more lines are too long
+2591
File diff suppressed because one or more lines are too long
+217
File diff suppressed because one or more lines are too long
@@ -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 a(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),a=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),t=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&a.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:t,count:t.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="817110bad40d29ee0aa8e4ef2dadfda9e310dfa6346ddb5d2725ecafb359f073";export{a,e as c,o as i,r as s,s as t};
|
||||
@@ -1 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="e6c51f2467ae4e22a792e536ae954291ab5a46915e728b216c8c45bc6816b209";try{await import("./houseplan-assets/houseplan-card-DI__VUPO.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="817110bad40d29ee0aa8e4ef2dadfda9e310dfa6346ddb5d2725ecafb359f073";try{await import("./houseplan-assets/houseplan-card-BrUCyMoE.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
|
||||
|
||||
@@ -1274,41 +1274,6 @@ export async function prepareGoldenScenario(page, scenario) {
|
||||
if (!palette.querySelector(selector))
|
||||
throw new Error(`golden furniture palette level is empty: ${scenario.id}`);
|
||||
}
|
||||
if (scenario.furniturePlacementPreview) {
|
||||
const { symbol, widthCm, depthCm, pointer, free = false } = scenario.furniturePlacementPreview;
|
||||
if (typeof symbol !== 'string' || !(widthCm > 0) || !(depthCm > 0)
|
||||
|| !Array.isArray(pointer) || pointer.length !== 2 || !pointer.every(Number.isFinite)) {
|
||||
throw new Error(`invalid golden furniturePlacementPreview: ${scenario.id}`);
|
||||
}
|
||||
card._decorSel = null;
|
||||
card._decorTool = 'furniture';
|
||||
card._furnCategory = null;
|
||||
card._furnPalette = { symbol, w: widthCm, h: depthCm };
|
||||
card.requestUpdate();
|
||||
await card.updateComplete;
|
||||
await frame();
|
||||
const svgRoot = card.renderRoot.querySelector('.stage svg');
|
||||
const stageEl = card.renderRoot.querySelector('.stage');
|
||||
const screen = new DOMPoint(pointer[0] * 1000, pointer[1] * card._spaceH)
|
||||
.matrixTransform(svgRoot.getScreenCTM());
|
||||
stageEl.dispatchEvent(new PointerEvent('pointermove', {
|
||||
bubbles: true, composed: true, pointerId: 993, pointerType: 'mouse',
|
||||
clientX: screen.x, clientY: screen.y, shiftKey: free,
|
||||
}));
|
||||
await card.updateComplete;
|
||||
await frame();
|
||||
const preview = card.renderRoot.querySelector('.furniture-placement-preview');
|
||||
const wall = card.renderRoot.querySelector('[data-hp="wall"]');
|
||||
const previewBeforeWall = !!wall
|
||||
&& !!(preview?.compareDocumentPosition(wall) & Node.DOCUMENT_POSITION_FOLLOWING);
|
||||
if (!preview || preview.getAttribute('data-symbol') !== symbol
|
||||
|| preview.getAttribute('aria-hidden') !== 'true'
|
||||
|| getComputedStyle(preview).pointerEvents !== 'none'
|
||||
|| Math.abs(Number(getComputedStyle(preview).opacity) - 0.55) > 1e-6
|
||||
|| !preview.closest('.decorlayer') || !previewBeforeWall) {
|
||||
throw new Error(`golden furniture placement preview contract is incomplete: ${scenario.id}`);
|
||||
}
|
||||
}
|
||||
if (scenario.hoverRoom) {
|
||||
const room = card._spaceModel().rooms.find((item) => item.id === scenario.hoverRoom);
|
||||
if (!room) throw new Error(`golden hover room missing: ${scenario.hoverRoom}`);
|
||||
|
||||
@@ -418,9 +418,6 @@ export const GOLDEN_SCENARIOS = Object.freeze([
|
||||
{ id: 'furniture-variants-light', fixture: 'visual', space: 'golden-geometry', mode: 'decor',
|
||||
furniturePalette: 'variants', furnitureCategory: 'coffee_table', language: 'ru', theme: 'light',
|
||||
viewport: { width: 900, height: 820 }, ...page },
|
||||
{ id: 'furniture-placement-preview-light', fixture: 'visual', space: 'golden-geometry', mode: 'decor',
|
||||
furniturePlacementPreview: { symbol: 'sofa', widthCm: 180, depthCm: 90, pointer: [0.35, 0.90] },
|
||||
language: 'ru', theme: 'light', viewport: { width: 1000, height: 900 }, ...stage },
|
||||
{ id: 'furniture-plan-art-dark', fixture: 'visual', space: 'golden-geometry', mode: 'view',
|
||||
decorOverride: [
|
||||
...decorLayerFixture.filter((shape) => shape.kind !== 'furniture'),
|
||||
|
||||
+18
-115
@@ -4,14 +4,12 @@
|
||||
// 2) выбранный символ ставится кликом по плану в РЕАЛЬНОМ размере — через
|
||||
// cell_cm пространства, и сразу оказывается выделенным в «выбрать»;
|
||||
// 3) поля ширины/глубины в палитре меняют размер ДО размещения;
|
||||
// 4) мышь показывает точный полупрозрачный будущий символ без записи в
|
||||
// конфиг; touch/pen не рисуют hover и сохраняют только чистый tap;
|
||||
// 5) магнит к стене: предмет прижимается к ближайшей стене и доворачивается
|
||||
// по ней; Shift временно обходит магнит, сохраняя grid snap;
|
||||
// 6) у выделенного предмета та же рамка, что у текстового блока, — угловые
|
||||
// 4) магнит к стене: предмет прижимается к ближайшей стене и доворачивается
|
||||
// по ней; Shift не отключает обязательную привязку (docs/CANVAS.md §9.4);
|
||||
// 5) у выделенного предмета та же рамка, что у текстового блока, — угловые
|
||||
// ручки (НЕзависимые ширина и глубина) и ручка поворота с шагом 5°;
|
||||
// 7) во время растягивания угла показываются живые плашки размеров;
|
||||
// 8) фигура пишется в конфиг и переживает пересборку, хуки card-mod на месте.
|
||||
// 6) во время растягивания угла показываются живые плашки размеров;
|
||||
// 7) фигура пишется в конфиг и переживает пересборку, хуки card-mod на месте.
|
||||
// ПАДАЕТ на сборке до этой задачи: инструмента «Мебель» нет, палитры нет,
|
||||
// kind:'furniture' не рисуется, магнита и плашек не существует. Смок при этом
|
||||
// null-safe — он показывает СПИСОК провалов, а не одно исключение.
|
||||
@@ -61,16 +59,6 @@ const res = await page.evaluate(async () => {
|
||||
await c.updateComplete;
|
||||
out.toolIsSelected = c._decorTool === 'furniture';
|
||||
const pal = () => sr().querySelector('.furnpalette');
|
||||
const openFurniture = async () => {
|
||||
if (c._decorTool !== 'furniture') {
|
||||
c._editorSecondary.openPalette();
|
||||
c._decorTool = 'furniture';
|
||||
c._furnPalette = null;
|
||||
c._furnCategory = null;
|
||||
c.requestUpdate();
|
||||
await c.updateComplete;
|
||||
}
|
||||
};
|
||||
out.paletteOpens = !!pal();
|
||||
const groups = [...(pal()?.querySelectorAll('.furngroup') || [])]
|
||||
.map((g) => g.getAttribute('data-group'));
|
||||
@@ -138,67 +126,9 @@ const res = await page.evaluate(async () => {
|
||||
out.fieldsShowMetres = +(wIn()?.value ?? NaN) === 1.8 && +(hIn()?.value ?? NaN) === 0.9;
|
||||
out.selectedItemIsMarked = !!item('sofa')?.classList.contains('on');
|
||||
|
||||
// ================= 3. точное превью без мутации ==========================
|
||||
const ghost = () => sr().querySelector('.furniture-placement-preview');
|
||||
const beforePreview = {
|
||||
decor: c._decorList.length,
|
||||
epoch: c._cfgEpoch,
|
||||
undo: c._geometryHistory.undoName,
|
||||
redo: c._geometryHistory.redoName,
|
||||
};
|
||||
ev('pointermove', stageEl(), 300, 300, { pointerType: 'mouse' });
|
||||
await c.updateComplete;
|
||||
const firstPreview = c._furniturePreviewPlacement
|
||||
? JSON.parse(JSON.stringify(c._furniturePreviewPlacement)) : null;
|
||||
const firstTransform = ghost()?.getAttribute('transform');
|
||||
out.mouseHoverShowsRealSymbol = ghost()?.getAttribute('data-symbol') === 'sofa'
|
||||
&& (ghost()?.getAttribute('d') || '').length > 20;
|
||||
out.previewIsTransientAndInert = ghost()?.getAttribute('aria-hidden') === 'true'
|
||||
&& getComputedStyle(ghost()).pointerEvents === 'none'
|
||||
&& near(Number(getComputedStyle(ghost()).opacity), 0.55, 1e-6);
|
||||
out.previewDoesNotMutateConfigOrHistory = c._decorList.length === beforePreview.decor
|
||||
&& c._cfgEpoch === beforePreview.epoch
|
||||
&& c._geometryHistory.undoName === beforePreview.undo
|
||||
&& c._geometryHistory.redoName === beforePreview.redo;
|
||||
if (wIn()) {
|
||||
wIn().value = '1.85';
|
||||
wIn().dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
await c.updateComplete;
|
||||
out.sizeUpdatesPreviewWithoutPointerMove = c._furniturePreviewPlacement?.w > firstPreview?.w
|
||||
&& ghost()?.getAttribute('transform') !== firstTransform;
|
||||
if (wIn()) {
|
||||
wIn().value = '1.8';
|
||||
wIn().dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
await c.updateComplete;
|
||||
stageEl().dispatchEvent(new PointerEvent('pointerleave', {
|
||||
bubbles: true, composed: true, pointerId: 11, pointerType: 'mouse',
|
||||
}));
|
||||
await c.updateComplete;
|
||||
out.pointerLeaveClearsPreview = !ghost() && c._furnPreviewInput === null;
|
||||
|
||||
// Unknown/stale symbols fail dark: neither an invisible ghost nor a decor
|
||||
// record is allowed. Restoring a valid selection resumes the same session.
|
||||
c._furnPalette = { symbol: 'future_unknown_symbol', w: 180, h: 90 };
|
||||
await c.updateComplete;
|
||||
ev('pointermove', stageEl(), 300, 300, { pointerType: 'mouse' });
|
||||
await c.updateComplete;
|
||||
const beforeUnknown = c._decorList.length;
|
||||
ev('pointerdown', stageEl(), 300, 300, { pointerType: 'mouse' });
|
||||
await c.updateComplete;
|
||||
out.unknownSymbolFailsDark = !ghost() && c._decorList.length === beforeUnknown
|
||||
&& c._decorTool === 'furniture';
|
||||
c._furnPalette = { symbol: 'sofa', w: 180, h: 90 };
|
||||
await c.updateComplete;
|
||||
|
||||
// ================= 4. размещение в реальном размере ======================
|
||||
// ================= 3. размещение в реальном размере ======================
|
||||
// середина комнаты r1 (40..550 × 140..580) — до стен дальше порога магнита
|
||||
ev('pointermove', stageEl(), 300, 300, { pointerType: 'mouse' });
|
||||
await c.updateComplete;
|
||||
const committedPreview = c._furniturePreviewPlacement
|
||||
? JSON.parse(JSON.stringify(c._furniturePreviewPlacement)) : null;
|
||||
ev('pointerdown', stageEl(), 300, 300, { pointerType: 'mouse' });
|
||||
ev('pointerdown', stageEl(), 300, 300);
|
||||
await c.updateComplete;
|
||||
const sofa = c._decorList.find((s) => s.kind === 'furniture');
|
||||
const sofaId = sofa?.id ?? null;
|
||||
@@ -210,10 +140,6 @@ const res = await page.evaluate(async () => {
|
||||
&& near((sofa.x + sofa.w / 2) * 1000, 300, 1e-6)
|
||||
&& near((sofa.y + sofa.h / 2) * 1000, 300, 1e-6);
|
||||
out.noAngleWhenStraight = !!sofa && sofa.angle === undefined;
|
||||
out.previewAndCommitAreIdentical = !!sofa && !!committedPreview
|
||||
&& ['x', 'y', 'w', 'h'].every((k) => near(sofa[k], committedPreview[k], 1e-12))
|
||||
&& (sofa.angle || 0) === committedPreview.angle;
|
||||
out.previewClearsAfterCommit = !ghost() && c._furnPreviewInput === null;
|
||||
out.selectedRightAway = !!sofaId && c._decorSel === sofaId;
|
||||
out.toolWentBackToSelect = c._decorTool === 'select';
|
||||
out.paletteDisarmed = c._furnPalette === null;
|
||||
@@ -228,8 +154,8 @@ const res = await page.evaluate(async () => {
|
||||
&& /scale\(/.test(attr(sofaId, 'transform') || '')
|
||||
&& attr(sofaId, 'vector-effect') === 'non-scaling-stroke';
|
||||
|
||||
// ================= 5. свой размер до размещения ==========================
|
||||
await openFurniture();
|
||||
// ================= 4. свой размер до размещения ==========================
|
||||
c._decorTool = 'furniture'; await c.updateComplete;
|
||||
category('toilet')?.click(); await c.updateComplete;
|
||||
pick('toilet'); await c.updateComplete;
|
||||
if (wIn()) {
|
||||
@@ -244,8 +170,8 @@ const res = await page.evaluate(async () => {
|
||||
out.typedSizeIsUsed = near((wc?.w ?? NaN) * 1000, cmToUnits(50), 1e-6)
|
||||
&& near((wc?.h ?? NaN) * 1000, cmToUnits(70), 1e-6);
|
||||
|
||||
// ================= 6. магнит к стене =====================================
|
||||
await openFurniture();
|
||||
// ================= 5. магнит к стене =====================================
|
||||
c._decorTool = 'furniture'; await c.updateComplete;
|
||||
category('bed')?.click(); await c.updateComplete;
|
||||
pick('bed_double'); await c.updateComplete;
|
||||
ev('pointerdown', stageEl(), 300, 150); // 10 единиц от стены y=140
|
||||
@@ -258,42 +184,19 @@ const res = await page.evaluate(async () => {
|
||||
out.wallMagnetQuantisesAlongTheWall = !!bed
|
||||
&& Math.abs((bed.x + bed.w / 2) * 1000 - 300) <= PITCH + 1e-6;
|
||||
|
||||
// Shift обходит магнит, но оставляет обычную привязку декора к сетке.
|
||||
await openFurniture();
|
||||
// …Shift больше не отключает магнит
|
||||
c._decorTool = 'furniture'; await c.updateComplete;
|
||||
category('bed')?.click(); await c.updateComplete;
|
||||
pick('bed_single'); await c.updateComplete;
|
||||
ev('pointerdown', stageEl(), 301.7, 151.3, { shiftKey: true });
|
||||
await c.updateComplete;
|
||||
const free = c._decorList.find((s) => s.symbol === 'bed_single');
|
||||
out.shiftBypassesTheWallMagnet = !!free
|
||||
out.shiftKeepsTheMagnet = !!free
|
||||
&& !near((free.y + free.h / 2) * 1000, 151.3, 1e-6)
|
||||
&& free.angle === undefined;
|
||||
|
||||
// Coarse pointers have no hover. A cancelled/dragged/second-contact gesture
|
||||
// must not become an accidental furniture save. Keep this after the palette
|
||||
// click checks because these synthetic pointer streams deliberately have no
|
||||
// browser-generated compatibility click tail.
|
||||
await openFurniture();
|
||||
category('chair')?.click(); await c.updateComplete;
|
||||
pick('chair'); await c.updateComplete;
|
||||
const beforeTouch = c._decorList.length;
|
||||
ev('pointerdown', stageEl(), 330, 330, { pointerType: 'touch', pointerId: 21 });
|
||||
ev('pointermove', stageEl(), 350, 350, { pointerType: 'touch', pointerId: 21 });
|
||||
ev('pointerup', stageEl(), 350, 350, { pointerType: 'touch', pointerId: 21 });
|
||||
ev('pointerdown', stageEl(), 330, 330, { pointerType: 'touch', pointerId: 22 });
|
||||
ev('pointercancel', stageEl(), 330, 330, { pointerType: 'touch', pointerId: 22 });
|
||||
ev('pointerdown', stageEl(), 330, 330, { pointerType: 'touch', pointerId: 23 });
|
||||
ev('pointerdown', stageEl(), 340, 340, { pointerType: 'touch', pointerId: 24, isPrimary: false });
|
||||
ev('pointerup', stageEl(), 330, 330, { pointerType: 'touch', pointerId: 23 });
|
||||
ev('pointerup', stageEl(), 340, 340, { pointerType: 'touch', pointerId: 24, isPrimary: false });
|
||||
await c.updateComplete;
|
||||
out.touchCancelMoveAndSecondContactDoNotSave = c._decorList.length === beforeTouch
|
||||
&& !ghost() && c._furnTouchPending === null;
|
||||
|
||||
// …магнит работает и при ПЕРЕТАСКИВАНИИ: тянем диван к левой стене (x=40)
|
||||
c._editorRuntime._clearFurniturePreview();
|
||||
c._decorTool = 'select'; c._furnPalette = null; c._furnCategory = null;
|
||||
c._decorSel = sofaId; c.requestUpdate();
|
||||
c._decorTool = 'select'; c._decorSel = sofaId; c.requestUpdate();
|
||||
await c.updateComplete; await sleep(40); await c.updateComplete;
|
||||
ev('pointerdown', el(sofaId), 300, 300);
|
||||
out.dragStarted = !!sofaId && c._decorMove?.id === sofaId;
|
||||
@@ -310,7 +213,7 @@ const res = await page.evaluate(async () => {
|
||||
ev('pointerup', stageEl(), 300, 300, { shiftKey: true });
|
||||
await c.updateComplete;
|
||||
|
||||
// ================= 7. рамка, углы, плашки, поворот =======================
|
||||
// ================= 6. рамка, углы, плашки, поворот =======================
|
||||
c._decorSel = sofaId; c.requestUpdate();
|
||||
await c.updateComplete; await sleep(60); await c.updateComplete;
|
||||
const frame = () => sr().querySelector('.dtframe');
|
||||
@@ -382,7 +285,7 @@ const res = await page.evaluate(async () => {
|
||||
await c.updateComplete;
|
||||
out.rotationIsRendered = /rotate\(/.test(attr(sofaId, 'transform') || '');
|
||||
|
||||
// ================= 8. конфиг и пересборка ================================
|
||||
// ================= 7. конфиг и пересборка ================================
|
||||
const stored = JSON.parse(JSON.stringify(sofaNow()));
|
||||
out.storedShapeIsComplete = stored.kind === 'furniture' && typeof stored.symbol === 'string'
|
||||
&& ['x', 'y', 'w', 'h'].every((k) => Number.isFinite(stored[k]));
|
||||
@@ -402,7 +305,7 @@ const res = await page.evaluate(async () => {
|
||||
)?.value === stored.symbol;
|
||||
c._decorShapeDialog = null;
|
||||
|
||||
// ================= 9. инертность и удаление ==============================
|
||||
// ================= 8. инертность и удаление ==============================
|
||||
c._decorTool = 'furniture'; c._furnPalette = null; await c.updateComplete;
|
||||
out.inertUnderTheStamp = pe(sofaId) === 'none';
|
||||
const n0 = c._decorList.length;
|
||||
|
||||
Vendored
+51
-51
@@ -1,111 +1,111 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"fingerprint": "e6c51f2467ae4e22a792e536ae954291ab5a46915e728b216c8c45bc6816b209",
|
||||
"fingerprint": "817110bad40d29ee0aa8e4ef2dadfda9e310dfa6346ddb5d2725ecafb359f073",
|
||||
"entry": "houseplan-card.js",
|
||||
"initialViewFiles": [
|
||||
"houseplan-assets/houseplan-card-DI__VUPO.js",
|
||||
"houseplan-assets/houseplan-card-BrUCyMoE.js",
|
||||
"houseplan-card.js"
|
||||
],
|
||||
"initialViewGzipBytes": 272469,
|
||||
"initialViewGzipBytes": 271841,
|
||||
"lazyFiles": [
|
||||
"houseplan-assets/de-gTJXGK7s.js",
|
||||
"houseplan-assets/editor-LGCA8WjH.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Cu2NFP8N.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-B4PXZ4um.js",
|
||||
"houseplan-assets/space-deletion-Do1Uuvoz.js"
|
||||
"houseplan-assets/de-CndLXoON.js",
|
||||
"houseplan-assets/editor-H80bjnWS.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-LLPkWHY0.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BkWWzfSR.js",
|
||||
"houseplan-assets/space-deletion-CLA5ezlL.js"
|
||||
],
|
||||
"lazyGzipBytes": 166314,
|
||||
"lazyGzipBytes": 165904,
|
||||
"lazyEditorFiles": [
|
||||
"houseplan-assets/editor-LGCA8WjH.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Cu2NFP8N.js",
|
||||
"houseplan-assets/space-deletion-Do1Uuvoz.js"
|
||||
"houseplan-assets/editor-H80bjnWS.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-LLPkWHY0.js",
|
||||
"houseplan-assets/space-deletion-CLA5ezlL.js"
|
||||
],
|
||||
"lazyEditorGzipBytes": 137196,
|
||||
"lazyEditorGzipBytes": 136806,
|
||||
"lazyOnboardingFiles": [
|
||||
"houseplan-assets/houseplan-onboarding-runtime-B4PXZ4um.js",
|
||||
"houseplan-assets/space-deletion-Do1Uuvoz.js"
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BkWWzfSR.js",
|
||||
"houseplan-assets/space-deletion-CLA5ezlL.js"
|
||||
],
|
||||
"lazyOnboardingGzipBytes": 7204,
|
||||
"lazyOnboardingGzipBytes": 7193,
|
||||
"lazyLocaleFiles": [
|
||||
"houseplan-assets/de-gTJXGK7s.js"
|
||||
"houseplan-assets/de-CndLXoON.js"
|
||||
],
|
||||
"lazyLocaleGzipBytes": 22468,
|
||||
"lazyLocaleGzipBytes": 22454,
|
||||
"files": [
|
||||
{
|
||||
"path": "houseplan-assets/de-gTJXGK7s.js",
|
||||
"sha256": "2ed2a8aad10d9eaa16fe3fe2e591c2d736d948b2fd35aca01fcffb33264e1284",
|
||||
"path": "houseplan-assets/de-CndLXoON.js",
|
||||
"sha256": "c3fb30be483664a92a555e2a916d2d6201a5d950d77385a34869445625732be1",
|
||||
"rawBytes": 73919,
|
||||
"gzipBytes": 22468,
|
||||
"gzipBytes": 22454,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/editor-LGCA8WjH.js",
|
||||
"sha256": "0f4ce2555703ec01ee188cf84badfdee661587e849c1b604ec88354c3f94894f",
|
||||
"path": "houseplan-assets/editor-H80bjnWS.js",
|
||||
"sha256": "bcd9f271360a94dfbf63d5bcdf3c965aed1e1b17d0c543ac0b554d5527b90250",
|
||||
"rawBytes": 3831,
|
||||
"gzipBytes": 1585,
|
||||
"gzipBytes": 1580,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-DI__VUPO.js"
|
||||
"houseplan-assets/houseplan-card-BrUCyMoE.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-card-DI__VUPO.js",
|
||||
"sha256": "35a537b289b388f34a5f6efc01a95254207be9880fa9fd931723ccfa258ad602",
|
||||
"rawBytes": 960284,
|
||||
"gzipBytes": 271715,
|
||||
"path": "houseplan-assets/houseplan-card-BrUCyMoE.js",
|
||||
"sha256": "bdcf3f7e9306428b2583f1e662541bebebcf2f106b48a53df117c6c9c54c5cde",
|
||||
"rawBytes": 958245,
|
||||
"gzipBytes": 271090,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/de-gTJXGK7s.js",
|
||||
"houseplan-assets/editor-LGCA8WjH.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Cu2NFP8N.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-B4PXZ4um.js"
|
||||
"houseplan-assets/de-CndLXoON.js",
|
||||
"houseplan-assets/editor-H80bjnWS.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-LLPkWHY0.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BkWWzfSR.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-Cu2NFP8N.js",
|
||||
"sha256": "4eb656ee069026534dace297817908e97d0e82e63ba1059d0c3d6c283d913344",
|
||||
"rawBytes": 522188,
|
||||
"gzipBytes": 135057,
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-LLPkWHY0.js",
|
||||
"sha256": "e49282b51d104baff861b3c21261e66dcbfd58807e0db444b04f1f171499e24d",
|
||||
"rawBytes": 520292,
|
||||
"gzipBytes": 134677,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-DI__VUPO.js",
|
||||
"houseplan-assets/space-deletion-Do1Uuvoz.js"
|
||||
"houseplan-assets/houseplan-card-BrUCyMoE.js",
|
||||
"houseplan-assets/space-deletion-CLA5ezlL.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-B4PXZ4um.js",
|
||||
"sha256": "73ad6471b2395b982d233754598fcb6fb296c29b01a188cce724c2861e044187",
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-BkWWzfSR.js",
|
||||
"sha256": "5446043fbf2a383f84df3751b1fd97ecac72d8bcf3c254da80ad4176538c91da",
|
||||
"rawBytes": 26356,
|
||||
"gzipBytes": 6650,
|
||||
"gzipBytes": 6644,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-DI__VUPO.js",
|
||||
"houseplan-assets/space-deletion-Do1Uuvoz.js"
|
||||
"houseplan-assets/houseplan-card-BrUCyMoE.js",
|
||||
"houseplan-assets/space-deletion-CLA5ezlL.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/space-deletion-Do1Uuvoz.js",
|
||||
"sha256": "258cd7b7338ca746122d6afcfc63f81d34be23a7bd31f1599ed8d45ec0daa5b6",
|
||||
"rawBytes": 911,
|
||||
"gzipBytes": 554,
|
||||
"path": "houseplan-assets/space-deletion-CLA5ezlL.js",
|
||||
"sha256": "ec0519abd52ab649e2ed8f5744cc8284d96d8a83f3c9517df7f5070be63a66e9",
|
||||
"rawBytes": 906,
|
||||
"gzipBytes": 549,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-card.js",
|
||||
"sha256": "031ef9710a85647066f82b62cd7a294c752493c0391cf59b1688380f7cac2a18",
|
||||
"sha256": "9bb7a0cb60837b9a006d1c0d125c820ab083b270a9c6130147cd5c8c8b47cff1",
|
||||
"rawBytes": 1089,
|
||||
"gzipBytes": 754,
|
||||
"gzipBytes": 751,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-DI__VUPO.js"
|
||||
"houseplan-assets/houseplan-card-BrUCyMoE.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
}
|
||||
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
+14
@@ -0,0 +1,14 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="817110bad40d29ee0aa8e4ef2dadfda9e310dfa6346ddb5d2725ecafb359f073";import{c as e,l as o,t,c_ as s,A as a,c$ as i,d0 as l,d1 as n,E as r,b as c}from"./houseplan-card-BrUCyMoE.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._formData}
|
||||
.schema=${e}
|
||||
.computeLabel=${e=>h[e.name]||e.name}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>`;return c`
|
||||
${d(f.slice(0,3))}
|
||||
${_?c`<div class="default-floor-error" role="alert"
|
||||
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
|
||||
${t(s,"editor.default_floor_missing",{id:_})}
|
||||
</div>`:a}
|
||||
${d(f.slice(3))}
|
||||
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",h);
|
||||
+5247
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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 a(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),a=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),t=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&a.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:t,count:t.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="817110bad40d29ee0aa8e4ef2dadfda9e310dfa6346ddb5d2725ecafb359f073";export{a,e as c,o as i,r as s,s as t};
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="e6c51f2467ae4e22a792e536ae954291ab5a46915e728b216c8c45bc6816b209";try{await import("./houseplan-assets/houseplan-card-DI__VUPO.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="817110bad40d29ee0aa8e4ef2dadfda9e310dfa6346ddb5d2725ecafb359f073";try{await import("./houseplan-assets/houseplan-card-BrUCyMoE.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
|
||||
|
||||
@@ -2,12 +2,6 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Furniture placement in the Background editor now shows a translucent mouse
|
||||
preview with the exact future size, rotation and wall snap. It follows live
|
||||
size edits and disappears without a config change on cancel; on touch,
|
||||
movement, cancellation or a second contact cannot create an accidental item
|
||||
([#359](https://github.com/Matysh/houseplan-card/issues/359)).
|
||||
|
||||
- Robot-vacuum trails now follow the same recorded points with softly rounded
|
||||
corners instead of a visibly segmented polyline. Current and previous runs
|
||||
use the same bounded curve, preserve telemetry gaps and never deviate by more
|
||||
|
||||
@@ -8,12 +8,6 @@
|
||||
|
||||
## Не выпущено
|
||||
|
||||
- При размещении мебели в редакторе подложки мышь теперь показывает на плане
|
||||
полупрозрачный символ с точным будущим размером, поворотом и привязкой к
|
||||
стене. Предпросмотр обновляется вместе с полями размера и исчезает без
|
||||
изменений при отмене; на touch движение, отмена или второй палец не создают
|
||||
случайный предмет ([#359](https://github.com/Matysh/houseplan-card/issues/359)).
|
||||
|
||||
- След пылесоса теперь повторяет те же записанные точки с мягко скруглёнными
|
||||
поворотами вместо заметной ломаной. Текущая и предыдущая уборки используют
|
||||
одну ограниченную кривую, сохраняют разрывы телеметрии и не отклоняются от
|
||||
|
||||
+3
-15
@@ -53,21 +53,9 @@ Furniture is a two-level non-modal palette in the editor context tray:
|
||||
3. The second level shows the real top-view drawings. Back returns to the
|
||||
category list and clears any armed symbol.
|
||||
4. Picking a variant arms one placement and reveals editable Width and Depth
|
||||
in the Home Assistant length unit. With a mouse, moving over the plan shows
|
||||
the real top-view symbol at its exact future size, wall magnet, rotation and
|
||||
canvas clamp. Editing Width or Depth updates that ghost immediately without
|
||||
requiring another pointer move.
|
||||
5. Clicking the plan places exactly the previewed object and returns to Select.
|
||||
Shift keeps the established free-placement behaviour. The ghost is a
|
||||
transient, 55%-opaque rendering aid: it is never saved and never enters
|
||||
Undo history.
|
||||
6. Pointer leave, Escape, palette/tool/editor/space changes and remount clear
|
||||
the ghost. Unknown or stale symbol ids show and save nothing.
|
||||
|
||||
Touch and pen do not emulate hover or show a placement ghost. Their best-effort
|
||||
editor path saves one object only after a clean tap; movement, pointer cancel
|
||||
or a second contact cancels that pending stamp so pinch/cancel cannot create
|
||||
furniture accidentally. View and kiosk touch guarantees remain unchanged.
|
||||
in the Home Assistant length unit.
|
||||
5. Clicking the plan places the object and returns to Select. Shift keeps the
|
||||
established free-placement behaviour.
|
||||
|
||||
The properties dialog remains a flat native select, grouped by category. Its
|
||||
optgroup label includes both the parent group and category because HTML selects
|
||||
|
||||
@@ -1302,11 +1302,7 @@ Power=`off`/`unavailable` подавляет даже устаревший ак
|
||||
|
||||
### Мебель
|
||||
|
||||
Библиотека содержит группы мебели, бытовой техники, сантехники и прочих символов. Размер можно задать в метрах или десятичных футах. После выбора символа наведите мышь на план: полупрозрачное изображение заранее показывает точное будущее положение, размер и поворот предмета. Изменение ширины или глубины сразу обновляет предпросмотр. Клик размещает предмет именно в показанной позиции; выход курсора с плана, `Esc`, смена инструмента, пространства или редактора убирают предпросмотр без изменения плана.
|
||||
|
||||
На сенсорных экранах предпросмотр не показывается: чистый тап размещает предмет, а движение, отмена касания или второй палец отменяют ожидающее размещение. Редактирование на touch-устройствах по-прежнему поддерживается по остаточному принципу; для полноценной работы используйте desktop.
|
||||
|
||||
При размещении рядом со стеной предмет:
|
||||
Библиотека содержит группы мебели, бытовой техники, сантехники и прочих символов. Размер можно задать в метрах или десятичных футах. При размещении рядом со стеной предмет:
|
||||
|
||||
- прижимается к ней и поворачивается параллельно;
|
||||
- всегда прижимается к ближайшей стене в радиусе магнита, иначе размещается по сетке;
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
# CODE-REVIEW-359-r1
|
||||
|
||||
Issue: [#359](https://github.com/Matysh/houseplan-card/issues/359) — Предпросмотр мебели на плане перед размещением
|
||||
SHA: `8b66d67d` (`issue/359-furniture-placement-preview`)
|
||||
ТЗ: `docs/specs/359-furniture-placement-preview.md`, spec review r2 — зелёный
|
||||
Заход: r1 · блокирующих циклов израсходовано 0 из 4 (r1 — первый заход code review, разбор полный)
|
||||
|
||||
## Скоуп проверки
|
||||
|
||||
Диапазон `origin/dev..HEAD` — 5 коммитов, из них один продуктовый:
|
||||
`8b66d67d feat: preview furniture placement` (Issue: #359, User-Visible: yes).
|
||||
Остальные четыре — спек и спек-ревью документы (r1/r2), уже принятые до этого
|
||||
этапа.
|
||||
|
||||
Диф продуктового кода: `src/furniture.ts`, `src/houseplan-card.ts`,
|
||||
`src/houseplan-editor-runtime.ts`, `src/styles/plan.styles.ts`. Плюс тесты
|
||||
(`test/furniture.test.mjs`, `test/golden-matrix.test.mjs`,
|
||||
`demo/smoke_furniture.mjs`, `demo/golden/harness.mjs`, `demo/golden/matrix.mjs`),
|
||||
документация (`docs/FURNITURE.md`, `docs/USER-GUIDE.ru.md`, оба CHANGELOG) и
|
||||
бандл/дистрибутив (класс D, обновлён `bundle:sync`).
|
||||
|
||||
Диф не трогает геометрию комнат/толщину/`layout`/`marker.space`/`open_spans`:
|
||||
`_furnWalls` используется только на чтение для магнита мебели, схема decor не
|
||||
меняется. `npm run invariants` не требуется — подтверждено чтением диффа
|
||||
(нет изменений в space-geometry/wall-degrade/rekey путях кроме одного нового
|
||||
импорта `clampCanvasN`, который уже существует и не меняет поведение).
|
||||
|
||||
## Как проверялось
|
||||
|
||||
Дешёвые гейты прогнаны лично на этом SHA (зелёного Validate на нём нет):
|
||||
|
||||
| Гейт | Команда | Результат |
|
||||
|---|---|---|
|
||||
| typecheck | `npx tsc --noEmit` | чисто, без вывода |
|
||||
| unit | `npm test` | `tests 1511 · pass 1510 · fail 0 · skipped 1` |
|
||||
| build | `npm run build` | `created dist in 15.3s` |
|
||||
| bundle sync | `npm run bundle:sync` | три копии синхронны (`custom_components`, `demo/srv/assets`) |
|
||||
| bundle budget | `npm run bundle:budget` | View 272469 B / 282000 B (headroom 9531 B), editor 137196 B |
|
||||
| furniture pack | `npm run furniture:check` | `Furniture pack OK: 44 plan symbols, 33 menu icons` |
|
||||
| **docs fingerprint** | `node scripts/check-docs.mjs` | **`ERROR screenshot source fingerprint is stale`** — см. находку ниже |
|
||||
| docs пересборка (диагностика находки) | `node demo/docs/capture.mjs` | пересобрал скриншоты/фингерпринт локально, подтвердил причину; изменения отменены (`git checkout -- docs/images/`), не коммитил |
|
||||
| browser smoke | `node demo/smoke_furniture.mjs` | `OK`, все 60+ именованных ассертов true |
|
||||
| golden (новый сценарий) | `node demo/golden/run.mjs --mode=capture --scenario=furniture-placement-preview-light` | `missing-baseline` — ожидаемо, приёмка эталона требует полного Linux CI (AC10, `demo/golden/README.md`) |
|
||||
| golden (регрессия) | `npm run golden:verify` | все существующие сценарии `passed`, кроме нового (`missing-baseline`, ожидаемо) — новая композиция decor-слоя не сдвинула ни один принятый эталон |
|
||||
| smoke-select (широкий охват) | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | 4 файла `src/**`, 44 изменённых символа-проекта; 31 «прямое совпадение», включая `demo/smoke_furniture.mjs` (уже прогнан). Остальные «прямые» — общие декор-символы (`_curSpaceCfg`, `_decorTool`, `_svgPoint`, `NORM_W`, `_editorRuntime`, `_pointers`) на диффах в `_stagePointerMove/_stagePointerLeave/_stagePointerUp`, которые новый код только оборачивает веткой `_decorTool === 'furniture'`, не меняя остальные ветки — прочитаны построчно (см. ниже), не прогонялись целиком: точечный per-tool guard, не общая decor-логика |
|
||||
|
||||
Инвариант «один источник числа»: preview и commit используют один и тот же
|
||||
чистый `resolveFurniturePlacement` (`src/furniture.ts:418`) — подтверждено
|
||||
и unit-тестом (`preview и commit равны для одного input`), и smoke
|
||||
(`previewAndCommitAreIdentical`), и чтением кода (`_furnPlace` больше не
|
||||
пересчитывает привязку отдельно, а берёт результат `_resolveFurniturePlacement`
|
||||
целиком). Второй показываемый параметр — ширина/глубина в полях палитры — тот
|
||||
же `pal.w/pal.h`, что уходит в резолвер; отдельного пути нет.
|
||||
|
||||
## Находки
|
||||
|
||||
### High — 1
|
||||
|
||||
**docs-гейт на этом SHA красный: скриншот-фингерпринт устарел, а PNG не
|
||||
пересобраны.**
|
||||
|
||||
- Файл: `docs/images/screenshots.json`
|
||||
- Воспроизведение: `node scripts/check-docs.mjs` →
|
||||
`ERROR screenshot source fingerprint is stale; run npm run build && node demo/docs/capture.mjs`.
|
||||
Причина — числами: записанный в коммите `sourceFingerprint` —
|
||||
`c94b78394be8a5a7dc2973652aa9aba4b6896cfd4c7f44c0038fadf911e62eb4` (тот же,
|
||||
что и на `origin/dev`, `git show origin/dev:docs/images/screenshots.json`
|
||||
и `git show HEAD^:docs/images/screenshots.json` совпадают — на `dev` гейт
|
||||
был зелёным). Диф трогает `src/furniture.ts`, `src/houseplan-card.ts`,
|
||||
`src/houseplan-editor-runtime.ts`, `src/styles/plan.styles.ts` — фингерпринт
|
||||
считается по всему `src/**`, значит любая правка делает его устаревшим.
|
||||
Пересчитанный локально фингерпринт — `5e7ddb2ac8885e8e6edf2113a1f35551ce3338234da9324d0874e3f861205502`;
|
||||
`docs/images/*.png` и `docs/images/screenshots.json` в коммите
|
||||
`8b66d67d` не обновлены.
|
||||
- Последствие: это ровно `docs` job из `validate.yml` — «реальный блокер»
|
||||
(AGENTS.md). На этом SHA он красный детерминированно, не по вкусу CI-раннера.
|
||||
Слияние в `dev` без починки повторит #230/#234: `dev` останется с красным
|
||||
`docs` до следующей задачи (#237).
|
||||
- Это не про содержание документации — `docs/FURNITURE.md` и
|
||||
`docs/USER-GUIDE.ru.md` обновлены корректно и по делу (см. «Проверено и
|
||||
корректно»). Дело исключительно в шаге релиз-артефактов «пересобрать
|
||||
скриншоты», который ТЗ не назвало явно (в разделе «Release-артефакты» ТЗ
|
||||
вообще не упомянут скриншотный гейт), но `AGENTS.md`/`PROCESS.md` требуют
|
||||
его для любой правки `src/**`, а хендофф автора его не называет и не
|
||||
прогонял.
|
||||
- Почин: `npm run build && node demo/docs/capture.mjs`, затем
|
||||
`node scripts/check-docs.mjs` до зелёного, закоммитить обновлённые
|
||||
`docs/images/*.png` и `docs/images/screenshots.json` вместе с остальным
|
||||
диффом (или отдельным коммитом класса C/D с тем же issue-трейлером).
|
||||
|
||||
Находка в скоупе задачи (сама задача внесла правку в `src/**`, из-за которой
|
||||
гейт стал красным) — чинится в этом же issue, отдельный issue не заводится.
|
||||
|
||||
## Проверено и корректно
|
||||
|
||||
- **AC1 (появление)** — `mouseHoverShowsRealSymbol` в smoke: реальный path,
|
||||
`data-symbol` совпадает с выбранным символом.
|
||||
- **AC2 (геометрический паритет)** — `resolveFurniturePlacement`
|
||||
(`src/furniture.ts:418`) — чистая функция, вызывается один раз и для превью
|
||||
(`_resolveFurniturePlacement` → `_furniturePreviewPlacement`), и для коммита
|
||||
(`_furnPlace`). Unit-тест сравнивает результат `deepEqual` для одинакового
|
||||
входа; отдельный тест покрывает `free`/`Shift` и canvas-guard
|
||||
(`clampCanvasN`, лимит 5000) и `null` для неизвестного символа. Smoke
|
||||
`previewAndCommitAreIdentical` сравнивает реально размещённый `sofa` с
|
||||
зафиксированным значением превью перед кликом — `x/y/w/h` с точностью
|
||||
`1e-12`, `angle` точно.
|
||||
- **AC3 (живые размеры)** — `sizeUpdatesPreviewWithoutPointerMove`: изменение
|
||||
поля Width без нового движения мыши меняет `w` превью и `transform`.
|
||||
Работает потому, что рендер геометрии превью читает текущий `_furnPalette`
|
||||
на каждый рендер (`_furniturePreviewPlacement` — геттер, не кэш).
|
||||
- **AC4 (чистота)** — `previewDoesNotMutateConfigOrHistory`: `_decorList.length`,
|
||||
`_cfgEpoch`, undo/redo-имена не меняются на `pointermove`. Прочитано в коде:
|
||||
`_furnPointerMove` пишет только `_furnPreviewInput` (`@state`, не часть
|
||||
конфига) и ничего не передаёт в `_saveConfig`/`_recordGeometry`.
|
||||
- **AC5 (очистка)** — `pointerLeaveClearsPreview`,
|
||||
`previewClearsAfterCommit` в smoke; остальные точки очистки (Escape,
|
||||
кнопки «закрыть»/«назад»/категория палитры, переключение инструмента,
|
||||
`_setMode`, смена `_space`, `_stagePointerCancel`,
|
||||
`_clearGeometryGesture`) не покрыты ни отдельным smoke-шагом, ни
|
||||
source-contract тестом (ТЗ допускал последний опционально, «если нельзя
|
||||
надёжно доказать одним smoke» — план его не потребовал письменно, но и не
|
||||
реализовал). **Проверено чтением, не исполнением**: все девять точек вызова
|
||||
`_clearFurniturePreview()` (`src/houseplan-editor-runtime.ts:1178, 2207,
|
||||
2238, 4722, 4746, 4867, 4878, 4902, 5222, 5345, 5357, 5361` и
|
||||
`src/houseplan-card.ts:1450, 2691, 2797`) расположены до любого early-return,
|
||||
который мог бы их обойти — прослежено построчно для `_setMode` (в самом
|
||||
начале функции, до проверки «тот же режим»), для Escape-ветки (до сброса
|
||||
`_furnPalette`), для кнопок палитры и переключателя инструментов в
|
||||
`_renderDecorBar`/`_renderFurnPalette`/`_renderDecorSecondary`. Пропусков не
|
||||
нашёл.
|
||||
- **AC6 (ввод)** — `shiftBypassesTheWallMagnet` (было
|
||||
`shiftKeepsTheMagnet` — переименовано согласно новому контракту `Shift`,
|
||||
смок обновлён консистентно с CANVAS.md §9.4 и ТЗ п.3);
|
||||
`touchCancelMoveAndSecondContactDoNotSave` — четыре синтетических
|
||||
touch-сценария (move>8px, cancel, второй contact, `isPrimary:false`) не
|
||||
создают запись. Прочитано: `_furnPointerMove` помечает `pending.cancelled`
|
||||
при движении больше 8px (тот же порог, что уже использует `_stagePointerMove`
|
||||
для pan-lock, `src/houseplan-card.ts:6220` — не новое магическое число), а
|
||||
`_decorPointerDown` игнорирует второй контакт, не создавая для него
|
||||
собственный `pending`.
|
||||
- **AC7 (визуальный контракт)** — `previewIsTransientAndInert`: `aria-hidden`,
|
||||
`pointer-events: none` (вычисленный computed style), `opacity: 0.55` с
|
||||
допуском `1e-6`. CSS-правило `.decorlayer .furniture-placement-preview`
|
||||
(`src/styles/plan.styles.ts`) не задевает никакой другой класс.
|
||||
- **AC8 (совместимость)** — весь остальной furniture-flow (drag/resize/rotate,
|
||||
erase, View-рендер) прогнан тем же расширенным smoke и остаётся зелёным;
|
||||
`npm test` без регрессий (1510 pass, 0 fail); `npm run golden:verify` —
|
||||
все ранее принятые сценарии `passed`, ни один не сдвинут новым кодом
|
||||
decor-слоя.
|
||||
- **AC9 (неизвестный символ)** — unit-тест
|
||||
`resolveFurniturePlacement({..., symbol: 'future_unknown_symbol'})` → `null`;
|
||||
smoke `unknownSymbolFailsDark` форсирует невалидный `_furnPalette`, проверяет
|
||||
отсутствие ghost и записи, и что инструмент остаётся вооружён для восстановления
|
||||
валидного выбора.
|
||||
- **AC10 (композиция)** — golden-сценарий `furniture-placement-preview-light`
|
||||
зарегистрирован в `matrix.mjs` и `test/golden-matrix.test.mjs`; harness
|
||||
(`demo/golden/harness.mjs`) программно вооружает предмет, ставит курсор и
|
||||
проверяет DOM-контракт (символ, `aria-hidden`, `pointer-events`, opacity,
|
||||
порядок относительно `[data-hp="wall"]`) до самого растрового захвата —
|
||||
капчур прошёл (`missing-baseline`, эталон не принят локально — верно по
|
||||
правилам `demo/golden/README.md` и по `npm run golden:verify`, который не
|
||||
нашёл ни одной существующей регрессии).
|
||||
- Трейлеры: `Issue: #359`, `User-Visible: yes` на продуктовом коммите; оба
|
||||
CHANGELOG (`docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`) правлены в том же
|
||||
коммите `8b66d67d`. Формулировки соответствуют реальному поведению (сверено
|
||||
построчно с кодом), терминология `docs/USER-GUIDE.ru.md` и `docs/FURNITURE.md`
|
||||
не придумана заново, согласована с существующим разделом «Мебель».
|
||||
- Ветка/процесс: один продуктовый коммит на весь диф, класс A корректно
|
||||
отделён от класса D (бандл) — все три копии бандла синхронны
|
||||
(`bundle:sync`), бюджет в пределах (`bundle:budget`).
|
||||
|
||||
## Чего не проверял
|
||||
|
||||
- **`python -m pytest tests_backend`** — не прогонялся: диф не касается
|
||||
`custom_components/**/*.py`.
|
||||
- **Performance-профили** — не прогонялись: не названы в AC, диф не трогает
|
||||
чувствительные к перфу пути (рендер одного дополнительного SVG-path на
|
||||
pointermove — цена признана в ТЗ и не требует профиля).
|
||||
- **`npm run invariants`** — не прогонялся: диф не меняет рёбра комнат, записи
|
||||
толщины, `layout`, `marker.space`, `open_spans`; `_furnWalls` используется
|
||||
только на чтение существующим (не новым) резолвером стен.
|
||||
- **Полный `demo/smoke_*.mjs` набор** — не прогонялся целиком; выбор сужен
|
||||
инструментом `smoke-select.mjs` (см. таблицу выше) до
|
||||
`smoke_furniture.mjs`, который прогнан. Остальные «прямые совпадения» разобраны
|
||||
чтением как точечные ветки существующей decor pointer-логики, не общие пути.
|
||||
- **`docs/images/*.png` визуально** — не сверял пиксель-в-пиксель новые
|
||||
скриншоты с прежними (не требуется: находка в том, что фингерпринт вообще
|
||||
не пересобран в коммите, а не в содержимом самих кадров).
|
||||
|
||||
## Вердикт
|
||||
|
||||
Функционально фича сделана верно и полно: единый resolver, паритет
|
||||
preview/commit, полная очистка по всем перечисленным в контракте границам
|
||||
(включая девять точек, проверенных чтением), touch/pen fail-safe, unknown-symbol
|
||||
fail-dark, новый golden-сценарий и расширенный smoke — всё воспроизводимо и
|
||||
падение тестов проверяемо (пробовал ломать резолвер мысленно на canvas-guard —
|
||||
тест зафиксировал бы `-5000`, а не что-то другое; unknown-symbol тест ловит
|
||||
регрессию `furnitureGraphic` напрямую).
|
||||
|
||||
Единственная находка — не в продуктовой логике, а в release-артефакте:
|
||||
скриншотный гейт документации остался красным на этом SHA. Это High по
|
||||
формальному эффекту (гарантированно красный обязательный job `docs` в
|
||||
`validate.yml` при слиянии) и по прецеденту (#230/#234/#237), поэтому
|
||||
вердикт — красный, а не жёлтый с отложенным High.
|
||||
|
||||
Вердикт: красный · заход r1 · блокирующих циклов 0/4 · High: 1 · Medium: 0 → в задаче
|
||||
+1
-65
@@ -28,7 +28,7 @@
|
||||
* colour, so a plan stays a drawing (owner: «символы рисуются линейно»).
|
||||
*/
|
||||
|
||||
import { NORM_W, GRID_PITCH, CANVAS_LIMIT, clampCanvasN } from './space-geometry';
|
||||
import { NORM_W, GRID_PITCH, CANVAS_LIMIT } from './space-geometry';
|
||||
import { GENERATED_FURNITURE_PLAN } from './furniture-plan-art.generated';
|
||||
|
||||
/** Groups the palette shows, in the order it shows them. */
|
||||
@@ -395,33 +395,6 @@ export interface FurnitureSnap {
|
||||
/** Wall segments (`[x1,y1,x2,y2]`) in whatever units the caller works in. */
|
||||
export type WallSeg = number[];
|
||||
|
||||
/** Final normalised box shared by the placement ghost and the saved decor
|
||||
* record. Keeping this result pure prevents a preview/commit jump when wall
|
||||
* magnetism or the canvas guard participates (#359). */
|
||||
export interface FurniturePlacement {
|
||||
symbol: string;
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
angle: number;
|
||||
}
|
||||
|
||||
export interface FurniturePlacementInput {
|
||||
symbol: string;
|
||||
widthCm: number;
|
||||
depthCm: number;
|
||||
/** Already resolved decor/grid snap point, in render units. */
|
||||
point: readonly [number, number];
|
||||
canvasW: number;
|
||||
canvasH: number;
|
||||
cellCm: number;
|
||||
gridPitch: number;
|
||||
walls: WallSeg[];
|
||||
wallReach: number;
|
||||
free?: boolean;
|
||||
}
|
||||
|
||||
const norm180 = (a: number): number => {
|
||||
let x = ((a % 360) + 360) % 360;
|
||||
if (x > 180) x -= 360;
|
||||
@@ -485,43 +458,6 @@ export function snapFurnitureToWall(
|
||||
return best;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the exact geometry one furniture stamp will own.
|
||||
*
|
||||
* The caller owns the higher-level decor/room/grid snap because that geometry
|
||||
* is editor state. Everything after that point is pure and therefore shared
|
||||
* byte-for-byte by hover preview and commit. Unknown symbols fail dark: a
|
||||
* newer config or a stale palette cannot create invisible furniture.
|
||||
*/
|
||||
export function resolveFurniturePlacement(input: FurniturePlacementInput): FurniturePlacement | null {
|
||||
const {
|
||||
symbol, widthCm, depthCm, point, canvasW, canvasH, cellCm, gridPitch,
|
||||
walls, wallReach, free = false,
|
||||
} = input;
|
||||
if (!furnitureGraphic(symbol) || !(canvasW > 0) || !(canvasH > 0)
|
||||
|| !Number.isFinite(point[0]) || !Number.isFinite(point[1])) return null;
|
||||
const w = clampFurnSize(cmToNorm(widthCm, cellCm, gridPitch, canvasW));
|
||||
const h = clampFurnSize(cmToNorm(depthCm, cellCm, gridPitch, canvasW));
|
||||
let cx = point[0], cy = point[1];
|
||||
let angle = 0;
|
||||
const snap = free ? null : snapFurnitureToWall(
|
||||
cx, cy, h * canvasH, walls, wallReach, gridPitch,
|
||||
);
|
||||
if (snap) {
|
||||
cx = snap.cx;
|
||||
cy = snap.cy;
|
||||
angle = snap.angle;
|
||||
}
|
||||
return {
|
||||
symbol,
|
||||
x: clampCanvasN(cx / canvasW - w / 2),
|
||||
y: clampCanvasN(cy / canvasH - h / 2),
|
||||
w,
|
||||
h,
|
||||
angle: Number(angle.toFixed(2)),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The four corners of a rotated box, world units, in the order the frame
|
||||
* draws them: NW, NE, SE, SW. Pure, so the frame, the gestures and the tests
|
||||
|
||||
+3
-53
@@ -63,8 +63,7 @@ import { dayCycleStageVars, renderDayCycleEnvironment } from './day-cycle-render
|
||||
import {
|
||||
FURNITURE_GROUPS, furnitureOfGroup, furnitureSymbol, furnitureDefaultCm,
|
||||
furnitureGraphic, furnitureCorners, snapFurnitureToWall,
|
||||
cmToNorm, clampFurnSize, clampFurnCm, FURN_WALL_CELLS,
|
||||
type FurnitureGroup, type FurniturePlacement,
|
||||
cmToNorm, clampFurnSize, clampFurnCm, FURN_WALL_CELLS, type FurnitureGroup,
|
||||
} from './furniture';
|
||||
import {
|
||||
degradeWalls, rekeyWallsAfterMoveChecked, wallRecordCarrierViolations,
|
||||
@@ -1038,15 +1037,6 @@ export class HouseplanCard extends LitElement {
|
||||
private _furnPalette: { symbol: string; w: number; h: number } | null = null;
|
||||
/** Category currently open in the two-level furniture palette. */
|
||||
private _furnCategory: string | null = null;
|
||||
/** Last fine-pointer location while one furniture stamp is armed. The final
|
||||
* geometry is deliberately resolved on render from the live palette so a
|
||||
* Width/Depth edit updates the ghost without another mouse move (#359). */
|
||||
private _furnPreviewInput: { raw: [number, number]; free: boolean } | null = null;
|
||||
/** Touch/pen has no hover. Delay its one-shot stamp until a clean pointerup
|
||||
* so pointercancel or a second contact cannot save accidental geometry. */
|
||||
private _furnTouchPending: {
|
||||
pid: number; sx: number; sy: number; pointerType: string; cancelled: boolean;
|
||||
} | null = null;
|
||||
/** The selected shape's own unrotated frame (text is measured from SVG). */
|
||||
private _dtBox: { id: string; x: number; y: number; w: number; h: number } | null = null;
|
||||
/**
|
||||
@@ -1447,7 +1437,6 @@ export class HouseplanCard extends LitElement {
|
||||
if (id !== this._space) {
|
||||
this._clearTransientHover(true);
|
||||
this._cancelDevicePressFeedback();
|
||||
this._editorRuntime?._clearFurniturePreview();
|
||||
}
|
||||
this._space = id;
|
||||
return true;
|
||||
@@ -2442,7 +2431,6 @@ export class HouseplanCard extends LitElement {
|
||||
_backdropDialog: { state: true },
|
||||
_furnPalette: { state: true },
|
||||
_furnCategory: { state: true },
|
||||
_furnPreviewInput: { state: true },
|
||||
_bdDrag: { state: true },
|
||||
_dtBox: { state: true },
|
||||
_dtDrag: { state: true },
|
||||
@@ -2688,7 +2676,6 @@ export class HouseplanCard extends LitElement {
|
||||
this._warmRelease();
|
||||
this._clearPlanSnapHover();
|
||||
this._clearOpeningPlacement(true);
|
||||
this._editorRuntime?._clearFurniturePreview();
|
||||
this._touchContacts.clear();
|
||||
this._touchSequenceMultitouch = false;
|
||||
this._touchClickBlockUntil = 0;
|
||||
@@ -2794,7 +2781,6 @@ export class HouseplanCard extends LitElement {
|
||||
// The palette is one explicit surface: Escape closes it and returns
|
||||
// to Select in one step, regardless of whether a symbol was armed.
|
||||
else if (this._decorTool === 'furniture') {
|
||||
this._editorRuntime?._clearFurniturePreview();
|
||||
this._furnPalette = null;
|
||||
this._furnCategory = null;
|
||||
this._decorTool = 'select';
|
||||
@@ -6170,10 +6156,6 @@ export class HouseplanCard extends LitElement {
|
||||
this._decorMoveUpdate(ev);
|
||||
return;
|
||||
}
|
||||
if (this._mode === 'decor' && this._decorTool === 'furniture' && this._editorRuntime) {
|
||||
this._notePointer(ev);
|
||||
if (this._editorRuntime._furnPointerMove(ev, this._pointerModality.hoverEnabled)) return;
|
||||
}
|
||||
if (!this._pointers.has(ev.pointerId)) {
|
||||
this._markupMove(ev);
|
||||
return;
|
||||
@@ -6238,8 +6220,7 @@ export class HouseplanCard extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _stagePointerLeave(ev: PointerEvent): void {
|
||||
if (this._mode === 'decor') this._editorRuntime?._furnPointerLeave(ev);
|
||||
private _stagePointerLeave(_ev: PointerEvent): void {
|
||||
if (!this._markup) return;
|
||||
if (this._tool === 'opening') {
|
||||
this._cursorPt = null;
|
||||
@@ -6316,7 +6297,6 @@ export class HouseplanCard extends LitElement {
|
||||
this._decorMove = null;
|
||||
return;
|
||||
}
|
||||
if (this._editorRuntime?._furnPointerUp(ev)) return;
|
||||
const viewportGestureEnded = !!this._pinchStart || !!this._panStart;
|
||||
this._pointers.delete(ev.pointerId);
|
||||
if (this._pointers.size < 2) this._pinchStart = null;
|
||||
@@ -8038,36 +8018,6 @@ export class HouseplanCard extends LitElement {
|
||||
</g>` as unknown as TemplateResult;
|
||||
}
|
||||
|
||||
private get _furniturePreviewPlacement(): FurniturePlacement | null {
|
||||
const input = this._furnPreviewInput;
|
||||
if (!input || !this._editorRuntime || this._mode !== 'decor'
|
||||
|| this._decorTool !== 'furniture' || !this._furnPalette
|
||||
|| !this._pointerModality.hoverEnabled) return null;
|
||||
return this._editorRuntime._resolveFurniturePlacement(input.raw, input.free, 'mouse');
|
||||
}
|
||||
|
||||
/** One real furniture path, in the same decor composition group as saved
|
||||
* shapes. It paints after them (so it is visible) and the whole decor layer
|
||||
* still paints below physical walls (#359). */
|
||||
private _renderFurniturePlacementPreview(): TemplateResult | typeof nothing {
|
||||
const placement = this._furniturePreviewPlacement;
|
||||
if (!placement) return nothing;
|
||||
const art = furnitureGraphic(placement.symbol);
|
||||
if (!art) return nothing;
|
||||
const W = NORM_W, H = this._decorH;
|
||||
const W2 = placement.w * W, H2 = placement.h * H;
|
||||
const cx = placement.x * W + W2 / 2, cy = placement.y * H + H2 / 2;
|
||||
const transform = `${placement.angle ? `rotate(${placement.angle} ${cx} ${cy}) ` : ''}`
|
||||
+ `translate(${placement.x * W} ${placement.y * H}) scale(${W2 / art.viewW} ${H2 / art.viewH})`;
|
||||
const style = this._decorStyle;
|
||||
return svg`<path class="furniture-placement-preview dfurn"
|
||||
data-symbol=${placement.symbol} d=${art.d} transform=${transform}
|
||||
stroke=${style.color} stroke-opacity=${style.opacity}
|
||||
stroke-width=${decorCmToUnits(style.widthCm, this._cellCm, this._gridPitch)}
|
||||
fill="none" stroke-linecap="round" stroke-linejoin="round"
|
||||
vector-effect="non-scaling-stroke" aria-hidden="true" pointer-events="none"></path>`;
|
||||
}
|
||||
|
||||
private _renderDecorLayer(): TemplateResult {
|
||||
const W = NORM_W, H = this._decorH;
|
||||
const editing = this._mode === 'decor';
|
||||
@@ -8187,7 +8137,7 @@ export class HouseplanCard extends LitElement {
|
||||
stroke="${st.color}" stroke-opacity="${st.opacity}" stroke-width="${sw}" fill="${st.fill ? st.fillColor : 'none'}" fill-opacity="${st.fill ? st.fillOpacity : 0}"></ellipse>`;
|
||||
}
|
||||
}
|
||||
return svg`<g class="decorlayer">${shapes}${draft}${this._renderFurniturePlacementPreview()}</g>` as unknown as TemplateResult;
|
||||
return svg`<g class="decorlayer">${shapes}${draft}</g>` as unknown as TemplateResult;
|
||||
}
|
||||
|
||||
// ================= shared editor secondary surface =================
|
||||
|
||||
+16
-122
@@ -65,9 +65,7 @@ import { dayCycleStageVars, renderDayCycleEnvironment } from './day-cycle-render
|
||||
import {
|
||||
FURNITURE_GROUPS, furnitureOfGroup, furnitureDefaultCm,
|
||||
furnitureGraphic, furnitureCorners, snapFurnitureToWall,
|
||||
resolveFurniturePlacement,
|
||||
clampFurnCm, FURN_WALL_CELLS,
|
||||
type FurnitureGroup, type FurniturePlacement,
|
||||
cmToNorm, clampFurnSize, clampFurnCm, FURN_WALL_CELLS, type FurnitureGroup,
|
||||
} from './furniture';
|
||||
import { GENERATED_FURNITURE_MENU } from './furniture-menu-art.generated';
|
||||
import {
|
||||
@@ -919,10 +917,6 @@ export interface HouseplanEditorHostPort {
|
||||
_fullRegistryHass: any;
|
||||
_furnPalette: { symbol: string; w: number; h: number; } | null;
|
||||
_furnCategory: string | null;
|
||||
_furnPreviewInput: { raw: [number, number]; free: boolean; } | null;
|
||||
_furnTouchPending: {
|
||||
pid: number; sx: number; sy: number; pointerType: string; cancelled: boolean;
|
||||
} | null;
|
||||
_furnWallReach: number;
|
||||
_furnWalls: number[][];
|
||||
_gearPtCache: WeakMap<number[][], number[]>;
|
||||
@@ -1175,7 +1169,6 @@ public _help(key: Extract<I18nKey, `${string}.help`>): TemplateResult | typeof n
|
||||
|
||||
public _setMode(mode: 'view' | 'plan' | 'devices' | 'decor', animate = true): void {
|
||||
this.host._endTabDrag();
|
||||
this._clearFurniturePreview();
|
||||
// A mode command is newer than the editor remembered by a same-route warm
|
||||
// remount. Clear it before the same-mode early return: while can_write is
|
||||
// pending, Lit may still be presenting the previous editor DOM even though
|
||||
@@ -2204,7 +2197,6 @@ public _commitPhysicalGeometry(
|
||||
}
|
||||
|
||||
public _clearGeometryGesture(): void {
|
||||
this._clearFurniturePreview();
|
||||
this.host._path = [];
|
||||
this._clearPlanSnapHover();
|
||||
this._clearOpeningPlacement(false);
|
||||
@@ -2234,12 +2226,6 @@ public _clearGeometryGesture(): void {
|
||||
public _stagePointerCancel(ev: PointerEvent): void {
|
||||
clearTimeout(this.host._kioskHoldTimer);
|
||||
if (this.host._swipeStart?.id === ev.pointerId) this.host._swipeStart = null;
|
||||
if (this.host._furnTouchPending?.pid === ev.pointerId) {
|
||||
this._clearFurniturePreview();
|
||||
this.host._pointers.delete(ev.pointerId);
|
||||
this.host.requestUpdate();
|
||||
return;
|
||||
}
|
||||
if (this.host._physicalDrag?.pid === ev.pointerId || this.host._physicalRotate?.pid === ev.pointerId) {
|
||||
this._cancelPhysicalGesture();
|
||||
return;
|
||||
@@ -4109,29 +4095,7 @@ public _decorPointerDown(ev: PointerEvent): boolean {
|
||||
// must not silently place whatever was chosen last week.
|
||||
if (!this.host._furnPalette) return false;
|
||||
ev.preventDefault();
|
||||
// A mouse has already shown the exact result and keeps the traditional
|
||||
// one-press stamp. Touch/pen has no hover, so commit only after a clean
|
||||
// pointerup; pointercancel, movement and a second contact stay fail-dark.
|
||||
const pointerType = ev.pointerType || 'mouse';
|
||||
if (pointerType === 'mouse') {
|
||||
this._furnPlace(this._svgPoint(ev), ev.shiftKey, pointerType);
|
||||
return true;
|
||||
}
|
||||
const pending = this.host._furnTouchPending;
|
||||
if (pending && pending.pid !== ev.pointerId) {
|
||||
pending.cancelled = true;
|
||||
this.host._furnPreviewInput = null;
|
||||
return true;
|
||||
}
|
||||
this.host._furnTouchPending = {
|
||||
pid: ev.pointerId,
|
||||
sx: ev.clientX,
|
||||
sy: ev.clientY,
|
||||
pointerType,
|
||||
cancelled: false,
|
||||
};
|
||||
this.host._furnPreviewInput = null;
|
||||
capturePointer(ev);
|
||||
this._furnPlace(this._svgPoint(ev), ev.shiftKey);
|
||||
return true;
|
||||
}
|
||||
// Empty-space selection belongs only to Select. Erase is an object
|
||||
@@ -4675,104 +4639,41 @@ public _furnPick(symbol: string): void {
|
||||
this.host._furnPalette = { symbol, w: d.w, h: d.h };
|
||||
}
|
||||
|
||||
public _resolveFurniturePlacement(
|
||||
raw: number[], free = false, pointerType = 'mouse',
|
||||
): FurniturePlacement | null {
|
||||
public _furnPlace(raw: number[], free = false): void {
|
||||
const pal = this.host._furnPalette;
|
||||
const sp = this.host._curSpaceCfg;
|
||||
if (!pal || !sp) return null;
|
||||
if (!pal || !sp) return;
|
||||
const W = NORM_W, H = this.host._decorH;
|
||||
const snapped = this._decorSnap(raw, pointerType);
|
||||
return resolveFurniturePlacement({
|
||||
symbol: pal.symbol,
|
||||
widthCm: pal.w,
|
||||
depthCm: pal.h,
|
||||
point: [snapped[0], snapped[1]],
|
||||
canvasW: W,
|
||||
canvasH: H,
|
||||
cellCm: this.host._cellCm,
|
||||
gridPitch: this.host._gridPitch,
|
||||
walls: this.host._furnWalls,
|
||||
wallReach: this.host._furnWallReach,
|
||||
free,
|
||||
});
|
||||
}
|
||||
|
||||
public _furnPlace(raw: number[], free = false, pointerType = 'mouse'): void {
|
||||
const placement = this._resolveFurniturePlacement(raw, free, pointerType);
|
||||
if (!placement) return;
|
||||
const sp = this.host._curSpaceCfg;
|
||||
if (!sp) return;
|
||||
const 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: placement.symbol,
|
||||
x: placement.x, y: placement.y, w: placement.w, h: placement.h,
|
||||
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 (placement.angle) shape.angle = placement.angle;
|
||||
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.host._furnCategory = null;
|
||||
this._clearFurniturePreview();
|
||||
this._recordGeometry(this.host._t('history.decor_add'), before);
|
||||
this._saveConfig();
|
||||
this.host.requestUpdate();
|
||||
}
|
||||
|
||||
public _clearFurniturePreview(): void {
|
||||
this.host._furnPreviewInput = null;
|
||||
this.host._furnTouchPending = null;
|
||||
}
|
||||
|
||||
/** Track only real fine-pointer hover. Touch/pen own a delayed one-tap
|
||||
* transaction below and must never resurrect a compatibility mouse ghost. */
|
||||
public _furnPointerMove(ev: PointerEvent, hoverAllowed: boolean): boolean {
|
||||
const pending = this.host._furnTouchPending;
|
||||
if (pending?.pid === ev.pointerId) {
|
||||
if (Math.abs(ev.clientX - pending.sx) + Math.abs(ev.clientY - pending.sy) > 8) {
|
||||
pending.cancelled = true;
|
||||
}
|
||||
this.host._furnPreviewInput = null;
|
||||
return true;
|
||||
}
|
||||
if (this.host._mode !== 'decor' || this.host._decorTool !== 'furniture'
|
||||
|| !this.host._furnPalette) {
|
||||
this._clearFurniturePreview();
|
||||
return false;
|
||||
}
|
||||
if (ev.pointerType !== 'mouse' || !hoverAllowed) {
|
||||
this.host._furnPreviewInput = null;
|
||||
return false;
|
||||
}
|
||||
const raw = this._svgPoint(ev);
|
||||
this.host._furnPreviewInput = { raw: [raw[0], raw[1]], free: ev.shiftKey };
|
||||
return true;
|
||||
}
|
||||
|
||||
public _furnPointerLeave(ev: PointerEvent): void {
|
||||
if (this.host._furnTouchPending?.pid === ev.pointerId) {
|
||||
this.host._furnTouchPending.cancelled = true;
|
||||
}
|
||||
this.host._furnPreviewInput = null;
|
||||
}
|
||||
|
||||
public _furnPointerUp(ev: PointerEvent): boolean {
|
||||
const pending = this.host._furnTouchPending;
|
||||
if (!pending || pending.pid !== ev.pointerId) return false;
|
||||
this.host._furnTouchPending = null;
|
||||
if (!pending.cancelled && this.host._mode === 'decor'
|
||||
&& this.host._decorTool === 'furniture' && this.host._furnPalette) {
|
||||
this._furnPlace(this._svgPoint(ev), ev.shiftKey, pending.pointerType);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public _furnMoveUpdate(ev: PointerEvent): void {
|
||||
const m = this.host._decorMove!;
|
||||
if (m.orig.kind !== 'furniture') return;
|
||||
@@ -4864,7 +4765,6 @@ public _renderFurnPalette(): TemplateResult {
|
||||
<span class="spacer"></span>
|
||||
<button class="btn furnclose" title=${this.host._t('btn.close')}
|
||||
@click=${() => {
|
||||
this._clearFurniturePreview();
|
||||
this.host._furnPalette = null;
|
||||
this.host._furnCategory = null;
|
||||
this.host._decorTool = 'select';
|
||||
@@ -4875,7 +4775,6 @@ public _renderFurnPalette(): TemplateResult {
|
||||
<div class="furnbody">
|
||||
${category ? html`
|
||||
<button class="btn ghost furnback" @click=${() => {
|
||||
this._clearFurniturePreview();
|
||||
this.host._furnPalette = null;
|
||||
this.host._furnCategory = null;
|
||||
}}>
|
||||
@@ -4899,7 +4798,6 @@ public _renderFurnPalette(): TemplateResult {
|
||||
class="furnitem furncategory" data-category=${item.id}
|
||||
title=${this.host._t(`furn.cat_${item.id}` as I18nKey)}
|
||||
@click=${() => {
|
||||
this._clearFurniturePreview();
|
||||
this.host._furnPalette = null;
|
||||
this.host._furnCategory = item.id;
|
||||
}}>
|
||||
@@ -5219,7 +5117,6 @@ public _renderDecorSecondary(): EditorSecondaryModel | null {
|
||||
dismissPolicy: this.host._furnPalette ? 'stay-open-on-canvas' : 'outside',
|
||||
dismiss: () => {
|
||||
if (this.host._decorTool !== 'furniture') return;
|
||||
this._clearFurniturePreview();
|
||||
this.host._furnPalette = null;
|
||||
this.host._furnCategory = null;
|
||||
this.host._decorTool = 'select';
|
||||
@@ -5342,7 +5239,6 @@ public _renderDecorBar(): TemplateResult {
|
||||
data-editor-palette=${t === 'furniture' ? 'furniture' : nothing}
|
||||
@click=${() => {
|
||||
if (t === 'furniture' && this.host._decorTool === 'furniture') {
|
||||
this._clearFurniturePreview();
|
||||
this.host._furnPalette = null;
|
||||
this.host._furnCategory = null;
|
||||
this.host._decorTool = 'select';
|
||||
@@ -5354,11 +5250,9 @@ public _renderDecorBar(): TemplateResult {
|
||||
// the palette belongs to its tool and to nothing else: leaving the
|
||||
// tool disarms whatever was chosen, so no later click can stamp it
|
||||
if (t !== 'furniture') {
|
||||
this._clearFurniturePreview();
|
||||
this.host._furnPalette = null;
|
||||
this.host._furnCategory = null;
|
||||
} else {
|
||||
this._clearFurniturePreview();
|
||||
this.host._furnCategory = null;
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -737,10 +737,6 @@ export const planStyles = css`
|
||||
calc(5px * var(--hp-cell-visual-scale, 1));
|
||||
pointer-events: none;
|
||||
}
|
||||
.decorlayer .furniture-placement-preview {
|
||||
opacity: 0.55;
|
||||
pointer-events: none;
|
||||
}
|
||||
.decorlayer text {
|
||||
font-weight: 600;
|
||||
user-select: none;
|
||||
|
||||
+1
-34
@@ -6,8 +6,7 @@ import assert from 'node:assert/strict';
|
||||
import {
|
||||
FURNITURE, FURNITURE_GROUPS, furnitureSymbol, furnitureOfGroup,
|
||||
furnitureDefaultCm, furniturePathD, furnitureGraphic, furnitureCorners, furnitureResize,
|
||||
snapFurnitureToWall, resolveFurniturePlacement,
|
||||
cmToNorm, normToCm, clampFurnSize, clampFurnCm,
|
||||
snapFurnitureToWall, cmToNorm, normToCm, clampFurnSize, clampFurnCm,
|
||||
FURN_MIN_N, FURN_MIN_CM, FURN_MAX_CM, FURN_WALL_CELLS,
|
||||
} from '../test-build/furniture.js';
|
||||
import { roomEdges } from '../test-build/logic.js';
|
||||
@@ -186,38 +185,6 @@ test('the default reach is six cells — thirty centimetres on a default plan',
|
||||
closeTo(GRID_PITCH * FURN_WALL_CELLS * (5 / GRID_PITCH), 30); // cells x cell_cm
|
||||
});
|
||||
|
||||
test('preview and commit share one deterministic furniture placement resolver', () => {
|
||||
const input = {
|
||||
symbol: 'sofa', widthCm: 180, depthCm: 90,
|
||||
point: [300, 112], canvasW: 1000, canvasH: 1000,
|
||||
cellCm: 5, gridPitch: GRID_PITCH, walls: EDGES,
|
||||
wallReach: 30,
|
||||
};
|
||||
const preview = resolveFurniturePlacement(input);
|
||||
const commit = resolveFurniturePlacement({ ...input });
|
||||
assert.deepEqual(preview, commit);
|
||||
assert.equal(preview.symbol, 'sofa');
|
||||
closeTo((preview.x + preview.w / 2) * 1000, 300, GRID_PITCH);
|
||||
closeTo((preview.y + preview.h / 2) * 1000, 100 + preview.h * 500);
|
||||
assert.equal(preview.angle, 0);
|
||||
});
|
||||
|
||||
test('the shared placement resolver supports Shift/free, canvas guards and unknown symbols', () => {
|
||||
const base = {
|
||||
symbol: 'sofa', widthCm: 180, depthCm: 90,
|
||||
point: [300, 112], canvasW: 1000, canvasH: 1000,
|
||||
cellCm: 5, gridPitch: GRID_PITCH, walls: EDGES,
|
||||
wallReach: 30,
|
||||
};
|
||||
const free = resolveFurniturePlacement({ ...base, free: true });
|
||||
closeTo((free.x + free.w / 2) * 1000, 300);
|
||||
closeTo((free.y + free.h / 2) * 1000, 112);
|
||||
const guarded = resolveFurniturePlacement({ ...base, point: [-1e9, -1e9], free: true });
|
||||
assert.equal(guarded.x, -5000);
|
||||
assert.equal(guarded.y, -5000);
|
||||
assert.equal(resolveFurniturePlacement({ ...base, symbol: 'future_unknown_symbol' }), null);
|
||||
});
|
||||
|
||||
// ------------------------------- the frame ----------------------------------
|
||||
|
||||
test('the corners of an unrotated box are the box; a rotated one turns about its centre', () => {
|
||||
|
||||
@@ -835,17 +835,6 @@ test('issue 159 goldens cover both palette levels and every plan-art compatibili
|
||||
new Set(['sofa', 'fridge', 'sofa_corner_right']));
|
||||
});
|
||||
|
||||
test('issue 359 has one deterministic light-theme furniture placement preview golden', () => {
|
||||
const scenario = GOLDEN_SCENARIOS.find((item) => item.id === 'furniture-placement-preview-light');
|
||||
assert.ok(scenario);
|
||||
assert.equal(scenario.mode, 'decor');
|
||||
assert.equal(scenario.theme, 'light');
|
||||
assert.equal(scenario.capture, 'stage');
|
||||
assert.deepEqual(scenario.furniturePlacementPreview, {
|
||||
symbol: 'sofa', widthCm: 180, depthCm: 90, pointer: [0.35, 0.90],
|
||||
});
|
||||
});
|
||||
|
||||
test('device dialog goldens expose the complete light-source controls at desktop and mobile widths', () => {
|
||||
const dialogs = GOLDEN_SCENARIOS.filter((scenario) => scenario.id.startsWith('device-dialog-'));
|
||||
assert.equal(dialogs.length, 3);
|
||||
|
||||
Reference in New Issue
Block a user