mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 16:38:31 +00:00
- HP-1500-02: the stage budget was the absolute document coordinate, so any tall dashboard content before the card was billed as header and the stage collapsed to 0px. Measure our own chrome relative to the card plus a bounded (<=120px) allowance for what the viewport keeps above us; re-measure on window resize, remove the listener in disconnectedCallback. - HP-1500-03, both layers: contentBounds opens a near-zero axis (< ~an icon) up to a 200-unit floor and ignores extra points outside a canvas envelope (-25%..125%) for FRAMING purposes only; the server bounds layout coordinates to +-4 — any finite float used to pass, and one 1e100 hid the plan from every viewer. A thin real room keeps its tight frame; the gate sensor past the edge still stretches it. - HP-1500-01: no automatic double-transform — a correct layout and a stranded one are indistinguishable, and guessing wrong corrupts good data. Explicit admin command houseplan/geometry/repair: dry_run previews, the backup rides the same store write, undo restores, and routine layout writes now preserve unrelated store keys instead of eating the backup. Tests: contentBounds guards (unit), layout coordinate bounds + repair lifecycle (harness), card-below-content smoke. Inventory: 139 / 49 / 42 / 64.
82 lines
3.7 KiB
JavaScript
82 lines
3.7 KiB
JavaScript
// v1.49.x: zoom goes below the base fit, the editor does not shift the plan.
|
|
// - the stage height follows the MEASURED header, not a hard-coded 118px, so
|
|
// entering an editor keeps the plan inside the viewport;
|
|
// - zoom < 1 centres the content instead of pinning it to a corner;
|
|
// - the content frame (default zoom) includes devices standing outside rooms.
|
|
import { launch, checkAll, finish } from './serve.mjs';
|
|
const { page, browser } = await launch();
|
|
const out = {};
|
|
|
|
// -- editor entry keeps the stage inside the viewport --------------------
|
|
const stageBox = () => page.evaluate(() => {
|
|
const sr = window.__card.shadowRoot || window.__card.renderRoot;
|
|
const b = sr.querySelector('.stage').getBoundingClientRect();
|
|
return { top: Math.round(b.top), bottom: Math.round(b.bottom) };
|
|
});
|
|
const vh = await page.evaluate(() => window.innerHeight);
|
|
const inView = await stageBox();
|
|
await page.evaluate(() => window.__card._setMode('plan'));
|
|
await page.waitForTimeout(400);
|
|
const inPlan = await stageBox();
|
|
out.viewFitsViewport = inView.bottom <= vh + 2;
|
|
out.editorFitsViewport = inPlan.bottom <= vh + 2; // used to overflow by ~90px
|
|
out.editorStageShrinks = inPlan.top > inView.top && inPlan.bottom <= inView.bottom + 2;
|
|
await page.evaluate(() => window.__card._setMode('view'));
|
|
await page.waitForTimeout(300);
|
|
|
|
// -- zoom out below the base fit -----------------------------------------
|
|
out.zoomOut = await page.evaluate(() => {
|
|
const c = window.__card;
|
|
c._resetZoom();
|
|
const fit = { ...c._viewOr(c._baseVb()) };
|
|
const stage = (c.shadowRoot || c.renderRoot).querySelector('.stage');
|
|
c._zoomAt(stage.clientWidth / 2, stage.clientHeight / 2, 0.5);
|
|
const v = { ...c._view };
|
|
const base = c._baseVb();
|
|
const cx = v.x + v.w / 2, cy = v.y + v.h / 2;
|
|
return {
|
|
zoom: c._zoom,
|
|
wider: v.w > fit.w * 1.9, // actually zoomed out
|
|
centredX: Math.abs(cx - (base[0] + base[2] / 2)) < 1, // not pinned to a corner
|
|
centredY: Math.abs(cy - (base[1] + base[3] / 2)) < 1,
|
|
};
|
|
});
|
|
out.zoomOutWorks = out.zoomOut.zoom === 0.5 && out.zoomOut.wider
|
|
&& out.zoomOut.centredX && out.zoomOut.centredY;
|
|
delete out.zoomOut;
|
|
out.floorIsHalf = await page.evaluate(() => { window.__card._resetZoom(); const c = window.__card;
|
|
c._applyView(0.1); return c._zoom; }) === 0.4; // clamped at the floor
|
|
await page.evaluate(() => window.__card._resetZoom());
|
|
|
|
// -- devices outside rooms stretch the default frame ---------------------
|
|
out.devicesStretchFrame = await page.evaluate(() => {
|
|
const c = window.__card;
|
|
const cfg = JSON.parse(JSON.stringify(c._serverCfg));
|
|
cfg.spaces[0].plan_url = null; cfg.spaces[0].plan_aspect = null;
|
|
c._serverCfg = cfg; c._model = null;
|
|
const before = c._baseVb();
|
|
// walk one lamp far outside every room
|
|
c._layout = { ...c._layout, d_lamp: { s: 'f1', x: 0.99, y: 0.5 } };
|
|
const after = c._baseVb();
|
|
return after[0] + after[2] > before[0] + before[2] + 20; // right edge follows the lamp
|
|
});
|
|
|
|
// -- a card BELOW other dashboard content still gets a stage (HP-1500-02) --
|
|
out.stageSurvivesContentAbove = await page.evaluate(async () => {
|
|
const spacer = document.createElement('div');
|
|
spacer.style.height = '900px';
|
|
document.body.insertBefore(spacer, document.body.firstChild);
|
|
window.dispatchEvent(new Event('resize'));
|
|
await new Promise((r) => setTimeout(r, 120));
|
|
const c = window.__card;
|
|
const sr = c.shadowRoot || c.renderRoot;
|
|
const h = sr.querySelector('.stage').getBoundingClientRect().height;
|
|
spacer.remove();
|
|
window.dispatchEvent(new Event('resize'));
|
|
await new Promise((r) => setTimeout(r, 120));
|
|
// the old code billed the 900px spacer as "header" and left a 0px stage
|
|
return h > 300;
|
|
});
|
|
|
|
await finish(browser, checkAll(out));
|