/** * The two "draw less" space options and the zero-wall rule (#306), plus the * handle beads that came with them. * * • «Скрыть декоративный слой» hides decor everywhere EXCEPT its own editor; * • «Скрыть проёмы» hides doors/windows everywhere EXCEPT the plan editor, * and does not touch what an opening MEANS (the light still gets through); * • a space that does not draw room borders draws no zero-thickness walls * in View; every editor still does, because the complete centreline span * must remain visible while editing; * • both switches survive a round trip through the dialog; * • every corner handle paints a bead a quarter of its hit radius, and the * hit radius itself is unchanged. */ import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; const c = window.__card; const sr = () => c.shadowRoot || c.renderRoot; const upd = async () => { c._cfgEpoch++; c.requestUpdate(); await c.updateComplete; }; const sp = () => c._serverCfg.spaces.find((s) => s.id === c._space); const set = async (patch) => { const s = sp(); s.settings = { ...(s.settings || {}), ...patch }; await upd(); }; const mode = async (m) => { c._setMode(m); await upd(); }; const n = (sel) => sr().querySelectorAll(sel).length; const setThickness = async (point, value) => { c._setMode('plan'); c._tool = 'wallthick'; await upd(); c._wallThickClick(point); await c.updateComplete; if (!c._wallDialog) return false; c._wallDialog = { ...c._wallDialog, value: String(value) }; c._wallThickApply(false); await c.updateComplete; return true; }; // a shape and an opening to look for, on the space we are on const s0 = sp(); s0.decor = [{ id: 'hl1', kind: 'rect', x: 0.3, y: 0.3, w: 0.1, h: 0.1, color: '#ff0000', width: 3 }]; s0.openings = [{ id: 'hl2', type: 'door', x: 0.3, y: 0.2, angle: 0, length: 0.09 }]; await mode('view'); await set({ hide_decor: undefined, hide_openings: undefined, show_borders: true }); // ---- 1) the baseline: both layers are drawn --------------------------- out.decorDrawnByDefault = n('.decorlayer .dshape') > 0; out.openingDrawnByDefault = n('[data-hp="opening"]') > 0; // ---- 2) hide the decor layer ------------------------------------------ await set({ hide_decor: true }); out.decorHiddenInView = n('.decorlayer .dshape') === 0; await mode('devices'); out.decorHiddenInDevices = n('.decorlayer .dshape') === 0; // …but its own editor still shows it, or it could not be edited await mode('decor'); out.decorVisibleInItsEditor = n('.decorlayer .dshape') > 0; await mode('view'); // the shape itself was never touched out.decorShapeKept = (sp().decor || []).length === 1; await set({ hide_decor: undefined }); out.decorBackAfterOff = n('.decorlayer .dshape') > 0; // ---- 3) hide the openings --------------------------------------------- const opCount = () => n('[data-hp="opening"]'); const before = opCount(); await set({ hide_openings: true }); out.openingsHiddenInView = opCount() === 0 && before > 0; await mode('plan'); c._tool = 'opening'; await upd(); out.openingsVisibleInPlanEditor = opCount() > 0; await mode('view'); // the opening is still an opening: it stayed in the config untouched out.openingKept = (sp().openings || []).length === 1; await set({ hide_openings: undefined }); out.openingsBackAfterOff = opCount() === before; // ---- 4) zero walls follow the borders switch --------------------------- // The demo's first two rooms share x=550. Apply zero through the public // Thickness workflow so the smoke exercises the canonical v9 catalogue. // The synthetic unhosted opening above has finished its own assertions and // must not participate in the structural migration exercised here. delete sp().openings; await upd(); out.zeroWallCreated = await setThickness([550, 250], 0); sp().zero_wall_style = 'dashed'; await upd(); await mode('view'); await set({ show_borders: true }); out.zeroWallsDrawnWithBorders = n('.zero-walls .zero-wall') > 0; await set({ show_borders: false }); out.zeroWallsGoneWithoutBorders = n('.zero-walls .zero-wall') === 0; // Every editor still draws them. The dash deliberately reaches the physical // centreline there, even if show_borders is off; only View hides it. await mode('plan'); await upd(); out.zeroWallsVisibleInPlanEditor = n('.zero-walls .zero-wall') > 0; await mode('devices'); out.zeroWallsVisibleInDevicesEditor = n('.zero-walls .zero-wall') > 0; await mode('decor'); out.zeroWallsVisibleInDecorEditor = n('.zero-walls .zero-wall') > 0; await mode('view'); await set({ show_borders: true }); // ---- 5) the dialog carries both switches round trip -------------------- c._openSpaceDialog('edit', c._space); await upd(); const rows = [...sr().querySelectorAll('hp-dialog .srcrow')].map((r) => r.textContent.trim()); out.dialogOffersHideDecor = rows.some((t) => /декорат|decorative/i.test(t)); out.dialogOffersHideOpenings = rows.some((t) => /проём|проем|openings|doors and windows/i.test(t)); out.dialogReadsOff = c._spaceDialog.hideDecor === false && c._spaceDialog.hideOpenings === false; c._spaceDialog = { ...c._spaceDialog, hideDecor: true, hideOpenings: true }; await c._saveSpaceDialog(); await upd(); out.savedBoth = sp().settings.hide_decor === true && sp().settings.hide_openings === true; c._openSpaceDialog('edit', c._space); await upd(); out.dialogReadsOn = c._spaceDialog.hideDecor === true && c._spaceDialog.hideOpenings === true; c._spaceDialog = { ...c._spaceDialog, hideDecor: false, hideOpenings: false }; await c._saveSpaceDialog(); await upd(); // switched off = nothing is stored (undefined, which JSON drops on the way // to the server), not a stored `false` — the same rule bg_color follows out.clearedNotStored = sp().settings.hide_decor === undefined && sp().settings.hide_openings === undefined && !('hide_decor' in JSON.parse(JSON.stringify(sp().settings))); c._spaceDialog = null; await upd(); return out; }); checkAll(res); // ---- 6) the beads: a quarter of the hit radius, and the hit is unchanged -- const rad = await page.evaluate(async () => { const c = window.__card; const sr = () => c.shadowRoot || c.renderRoot; c._setMode('decor'); c._decorTool = 'backdrop'; c._cfgEpoch++; c.requestUpdate(); await c.updateComplete; const hit = sr().querySelector('.bdframe .bdhandle'); const knob = sr().querySelector('.bdframe .bdknob'); const view = c._viewOr(c._baseVb()); const o = { hasBoth: !!hit && !!knob, hitR: hit ? Number(hit.getAttribute('r')) : 0, knobR: knob ? Number(knob.getAttribute('r')) : 0, expectHit: Math.max(view.w, view.h) * 0.02, knobInert: knob ? getComputedStyle(knob).pointerEvents === 'none' : false, // the bead sits exactly on the handle sameCentre: hit && knob && hit.getAttribute('cx') === knob.getAttribute('cx') && hit.getAttribute('cy') === knob.getAttribute('cy'), }; // #277 removed the Resize corner scale frame entirely; Backdrop keeps its // own independent handle/bead contract above. c._setMode('plan'); c._tool = 'resize'; c._resize.selectRoom((c._curSpaceCfg.rooms || [])[0]?.id || null); c._cfgEpoch++; c.requestUpdate(); await c.updateComplete; o.rszLegacyCornersAbsent = !sr().querySelector('.rszcorner, .rszknob, .rszframe'); c._setMode('view'); return o; }); checkAll({ handleAndBead: rad.hasBoth, hitRadiusUnchanged: Math.abs(rad.hitR - rad.expectHit) < 0.15, beadIsAQuarter: Math.abs(rad.knobR * 4 - rad.hitR) < 0.05, beadTakesNoPointer: rad.knobInert, beadOnTheHandle: rad.sameCentre, resizeLegacyCornerScaleAbsent: rad.rszLegacyCornersAbsent, }); // ---- 7) every Background session starts on Select; image has its own tool -- const armed = await page.evaluate(async () => { const c = window.__card; c._setMode('view'); await c.updateComplete; c._setMode('decor'); await c.updateComplete; const o = { startsOnSelect: c._decorTool === 'select', inertUntilPictureTool: !c._bdMovable, hasPicture: !!c._curSpaceCfg.plan_url, }; c._decorTool = 'backdrop'; c.requestUpdate(); await c.updateComplete; o.movable = c._bdMovable; // and the picture really moves under it: press inside the body, drag, release const r = c._bdRect; const before = { x: c._curSpaceCfg.plan_x ?? 0, y: c._curSpaceCfg.plan_y ?? 0 }; const pt = [r.x + r.w / 2, r.y + r.h / 2]; const ev = (x, y) => ({ pointerId: 42, clientX: x, clientY: y, target: c._stageEl, preventDefault() {}, stopPropagation() {}, shiftKey: false }); // _svgPoint reads clientX/Y — go through the stage geometry const stage = (c.shadowRoot || c.renderRoot).querySelector('.stage'); const box = stage.getBoundingClientRect(); const vb = stage.querySelector('svg').getAttribute('viewBox').split(' ').map(Number); const toScreen = (x, y) => [box.left + ((x - vb[0]) / vb[2]) * box.width, box.top + ((y - vb[1]) / vb[3]) * box.height]; const [sx, sy] = toScreen(pt[0], pt[1]); const [ex, ey] = toScreen(pt[0] + vb[2] * 0.1, pt[1]); c._stagePointerDown(ev(sx, sy)); c._stagePointerMove(ev(ex, ey)); c._stagePointerUp(ev(ex, ey)); await c.updateComplete; o.moved = (c._curSpaceCfg.plan_x ?? 0) !== before.x; c._setMode('view'); return o; }); checkAll({ backgroundStartsOnSelect: armed.startsOnSelect, pictureInertUntilOwnTool: armed.inertUntilPictureTool, pictureMovableUnderOwnTool: armed.movable, dragMovedThePicture: armed.moved, }); await finish(browser);