mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
262 lines
13 KiB
JavaScript
262 lines
13 KiB
JavaScript
// Panning at ANY zoom (owner's report 2026-08-04: «добавь возможность таскать
|
|
// план при любом масштабе, а не только при более 100%»).
|
|
//
|
|
// Until this fix `_stagePointerMove` moved the view only while `_zoom > 1`:
|
|
// the rule was inherited from the old bounded canvas, where a plan smaller
|
|
// than the scene had nowhere to go. The infinite canvas removed that edge —
|
|
// there is always somewhere to go, and an editor extending a plan outwards
|
|
// needs exactly that. So a drag on empty scene now pans at 100 %, at 50 % and
|
|
// at 800 %, in view mode and in every editor; how far you may walk is decided
|
|
// by `_clampView` (PAN_SLACK: about a screen past the content) and not by the
|
|
// zoom. The tools keep the pointer they already owned — a resize handle, a
|
|
// device badge, an opening — and the kiosk keeps its floor swipe from the
|
|
// active 48 px edge while central horizontal drags remain pan.
|
|
import { launch, check, checkAll, finish } from './serve.mjs';
|
|
const { page, browser } = await launch();
|
|
const out = {};
|
|
|
|
// ---- a drag on empty scene pans, at every zoom and in every mode ---------
|
|
Object.assign(out, await page.evaluate(async () => {
|
|
const c = window.__card;
|
|
const o = {};
|
|
const pd = (id, x, y) => c._stagePointerDown({ pointerId: id, clientX: x, clientY: y, target: c._stageEl, preventDefault() {} });
|
|
const pm = (id, x, y) => c._stagePointerMove({ pointerId: id, clientX: x, clientY: y });
|
|
const pu = (id, x, y) => c._stagePointerUp({ pointerId: id, clientX: x, clientY: y });
|
|
let pid = 100;
|
|
/** Drag 80x60 px across the middle of the scene; did the view follow? */
|
|
const drag = async (zoom) => {
|
|
// always start from the plan in the middle: the clamp is a real limit, and
|
|
// a dozen drags in a row would otherwise walk into it
|
|
const vb = c._baseVb();
|
|
c._applyView(zoom, vb[0] + vb[2] / 2, vb[1] + vb[3] / 2);
|
|
await c.updateComplete;
|
|
const v0 = { ...c._viewOr(c._baseVb()) };
|
|
const id = ++pid;
|
|
pd(id, 300, 300);
|
|
pm(id, 340, 330);
|
|
pm(id, 380, 360);
|
|
const v1 = { ...c._viewOr(c._baseVb()) };
|
|
pu(id, 380, 360);
|
|
// dragging down-right pulls the viewport up-left, and the zoom is untouched
|
|
return Math.abs(v1.x - v0.x) > 1 && Math.abs(v1.y - v0.y) > 1
|
|
&& v1.x < v0.x && v1.y < v0.y && Math.abs(c._zoom - zoom) < 1e-9;
|
|
};
|
|
const inMode = async (name, setup) => {
|
|
await setup();
|
|
await c.updateComplete;
|
|
o['pan_' + name + '_at100'] = await drag(1);
|
|
o['pan_' + name + '_at50'] = await drag(0.5);
|
|
o['pan_' + name + '_at33'] = await drag(1 / 3);
|
|
};
|
|
|
|
await inMode('view', async () => { c._setMode('view'); });
|
|
for (const tool of ['draw', 'merge', 'split', 'resize', 'opening', 'wallthick', 'delroom']) {
|
|
await inMode('plan_' + tool, async () => { c._setMode('plan'); c._tool = tool; });
|
|
}
|
|
await inMode('devices', async () => { c._setMode('devices'); });
|
|
await inMode('decor', async () => { c._setMode('decor'); c._decorTool = 'select'; });
|
|
c._setMode('view');
|
|
c._applyView(1);
|
|
await c.updateComplete;
|
|
return o;
|
|
}));
|
|
|
|
// ---- 400 % still pans (the behaviour that already worked) ----------------
|
|
out.panStillWorksZoomedIn = await page.evaluate(async () => {
|
|
const c = window.__card;
|
|
c._setMode('view'); c._applyView(4); await c.updateComplete;
|
|
const v0 = { ...c._view };
|
|
c._stagePointerDown({ pointerId: 7, clientX: 300, clientY: 300, target: c._stageEl, preventDefault() {} });
|
|
c._stagePointerMove({ pointerId: 7, clientX: 380, clientY: 360 });
|
|
const moved = Math.abs(c._view.x - v0.x) > 1;
|
|
c._stagePointerUp({ pointerId: 7, clientX: 380, clientY: 360 });
|
|
c._applyView(1);
|
|
return moved;
|
|
});
|
|
|
|
// ---- the clamp: you may walk about a screen off, never further ------------
|
|
Object.assign(out, await page.evaluate(async () => {
|
|
const c = window.__card;
|
|
const sr = c.shadowRoot || c.renderRoot;
|
|
const o = {};
|
|
c._setMode('view');
|
|
const vb = c._baseVb();
|
|
c._applyView(1, vb[0] + vb[2] / 2, vb[1] + vb[3] / 2); // at 100 % the view IS fitView
|
|
await c.updateComplete;
|
|
const fit = { ...c._view };
|
|
c._stagePointerDown({ pointerId: 8, clientX: 700, clientY: 400, target: c._stageEl, preventDefault() {} });
|
|
c._stagePointerMove({ pointerId: 8, clientX: -40000, clientY: 400 }); // shove it into the void
|
|
const far = { ...c._view };
|
|
c._stagePointerUp({ pointerId: 8, clientX: -40000, clientY: 400 });
|
|
o.panLeavesTheContent = far.x > fit.x + fit.w * 0.5;
|
|
o.panStopsAtTheSlack = far.x <= fit.x + fit.w + 1;
|
|
c.requestUpdate(); await c.updateComplete;
|
|
o.homeArrowOffersTheWayBack = !!sr.querySelector('.homearrow');
|
|
c._applyView(1, vb[0] + vb[2] / 2, vb[1] + vb[3] / 2);
|
|
c.requestUpdate(); await c.updateComplete;
|
|
// the same at 50 %: the screen holds more, the walk is still finite
|
|
const fit2 = { ...c._view };
|
|
c._applyView(0.5, vb[0] + vb[2] / 2, vb[1] + vb[3] / 2); await c.updateComplete;
|
|
c._stagePointerDown({ pointerId: 9, clientX: 700, clientY: 400, target: c._stageEl, preventDefault() {} });
|
|
c._stagePointerMove({ pointerId: 9, clientX: -40000, clientY: 400 });
|
|
const far2 = { ...c._view };
|
|
c._stagePointerUp({ pointerId: 9, clientX: -40000, clientY: 400 });
|
|
o.panStopsAtTheSlackZoomedOut = far2.x <= fit2.x + fit2.w + 1 && far2.x > fit2.x;
|
|
c._applyView(1, vb[0] + vb[2] / 2, vb[1] + vb[3] / 2); await c.updateComplete;
|
|
return o;
|
|
}));
|
|
|
|
// ---- the tools still own their pointer ------------------------------------
|
|
const ev = (type, id, x, y) => ({ type, id, x, y });
|
|
Object.assign(out, await page.evaluate(async () => {
|
|
const c = window.__card;
|
|
const sr = c.shadowRoot || c.renderRoot;
|
|
const o = {};
|
|
const fire = (el, type, id, x, y) => el.dispatchEvent(new PointerEvent(type, {
|
|
bubbles: true, composed: true, cancelable: true, pointerId: id, clientX: x, clientY: y,
|
|
}));
|
|
|
|
// a) resize handle: the wall moves, the view does not
|
|
c._setMode('plan'); c._tool = 'resize'; c._applyView(1); await c.updateComplete;
|
|
const h = sr.querySelector('.rszhandle[aria-disabled="false"]');
|
|
o.resizeHandlePresent = !!h;
|
|
if (h) {
|
|
const v0 = { ...c._viewOr(c._baseVb()) };
|
|
const r = h.getBoundingClientRect();
|
|
fire(h, 'pointerdown', 21, r.left + r.width / 2, r.top + r.height / 2);
|
|
o.resizeHandleTakesThePointer = c._resize?.dragging === true && c._panStart === null;
|
|
fire(h, 'pointermove', 21, r.left + r.width / 2 + 40, r.top + r.height / 2 + 40);
|
|
await Promise.resolve();
|
|
const v1 = { ...c._viewOr(c._baseVb()) };
|
|
o.resizeHandleDoesNotPan = Math.abs(v1.x - v0.x) < 0.5 && Math.abs(v1.y - v0.y) < 0.5;
|
|
o.resizeHandleResizes = c._resize?.dragging === true && c._resize.moved === true;
|
|
c._rszCancelDrag?.();
|
|
fire(h, 'pointerup', 21, r.left + r.width / 2 + 40, r.top + r.height / 2 + 40);
|
|
}
|
|
c._resize?.selectRoom(null);
|
|
|
|
// b) a device badge in the devices editor drags the DEVICE
|
|
c._setMode('devices'); c._applyView(1); await c.updateComplete;
|
|
const dev = sr.querySelector('.devlayer .dev');
|
|
o.devicePresent = !!dev;
|
|
if (dev) {
|
|
const v0 = { ...c._viewOr(c._baseVb()) };
|
|
const r = dev.getBoundingClientRect();
|
|
fire(dev, 'pointerdown', 22, r.left + r.width / 2, r.top + r.height / 2);
|
|
o.deviceTakesThePointer = !!c._deviceDrag && c._panStart === null;
|
|
fire(dev, 'pointermove', 22, r.left + r.width / 2 + 40, r.top + r.height / 2 + 30);
|
|
await Promise.resolve();
|
|
c._stagePointerMove({ pointerId: 22, clientX: r.left + 60, clientY: r.top + 50 });
|
|
const v1 = { ...c._viewOr(c._baseVb()) };
|
|
o.deviceDragDoesNotPan = Math.abs(v1.x - v0.x) < 0.5 && Math.abs(v1.y - v0.y) < 0.5;
|
|
o.deviceActuallyMoved = !!c._deviceDrag && c._deviceDrag.moved === true;
|
|
fire(dev, 'pointerup', 22, r.left + r.width / 2 + 40, r.top + r.height / 2 + 30);
|
|
// _pointerUp releases the device drag before its serial final write, and _stagePointerDown
|
|
// refuses to start a gesture while a device drag is live
|
|
await new Promise((r2) => setTimeout(r2, 20));
|
|
o.deviceDragReleased = c._deviceDrag === null;
|
|
await c.updateComplete;
|
|
}
|
|
|
|
// c) an opening drags along its wall, the view stays put
|
|
const cfg = JSON.parse(JSON.stringify(c._serverCfg));
|
|
cfg.spaces[0].openings = [{ id: 'o_pan', type: 'window', x: 0.3, y: 0.14, angle: 0, length: 0.1 }];
|
|
c._serverCfg = cfg; c._modelCache = null; c._frame = null;
|
|
c._setMode('plan'); c._tool = 'draw'; c._applyView(1);
|
|
c.requestUpdate(); await c.updateComplete;
|
|
const op = sr.querySelector('.op-hit');
|
|
o.openingPresent = !!op;
|
|
if (op) {
|
|
const v0 = { ...c._viewOr(c._baseVb()) };
|
|
const r = op.getBoundingClientRect();
|
|
fire(op, 'pointerdown', 23, r.left + r.width / 2, r.top + r.height / 2);
|
|
o.openingTakesThePointer = !!c._opDrag && c._panStart === null;
|
|
fire(op, 'pointermove', 23, r.left + r.width / 2 + 50, r.top + r.height / 2);
|
|
const v1 = { ...c._viewOr(c._baseVb()) };
|
|
o.openingDragDoesNotPan = Math.abs(v1.x - v0.x) < 0.5 && Math.abs(v1.y - v0.y) < 0.5;
|
|
fire(op, 'pointerup', 23, r.left + r.width / 2 + 50, r.top + r.height / 2);
|
|
await c.updateComplete;
|
|
}
|
|
|
|
// d) a drawing tool is still a drawing tool: two fingers pinch, they do not pan
|
|
c._setMode('plan'); c._tool = 'draw'; c._applyView(1); await c.updateComplete;
|
|
const z0 = c._zoom;
|
|
c._stagePointerDown({ pointerId: 31, clientX: 300, clientY: 300, target: c._stageEl, preventDefault() {} });
|
|
c._stagePointerDown({ pointerId: 32, clientX: 400, clientY: 300, target: c._stageEl, preventDefault() {} });
|
|
c._stagePointerMove({ pointerId: 31, clientX: 250, clientY: 300 });
|
|
c._stagePointerMove({ pointerId: 32, clientX: 450, clientY: 300 });
|
|
o.pinchStillZooms = c._zoom > z0 * 1.5;
|
|
c._stagePointerUp({ pointerId: 31, clientX: 250, clientY: 300 });
|
|
c._stagePointerUp({ pointerId: 32, clientX: 450, clientY: 300 });
|
|
c._path = [];
|
|
c._setMode('view'); c._applyView(1); await c.updateComplete;
|
|
return o;
|
|
}));
|
|
|
|
// ---- the kiosk keeps its edge-owned floor swipe ----------------------------
|
|
Object.assign(out, await page.evaluate(async () => {
|
|
const c0 = window.__card;
|
|
const o = {};
|
|
const k = document.createElement('houseplan-card');
|
|
k.setConfig({ type: 'custom:houseplan-card', kiosk: true, cycle: 0 });
|
|
k.hass = c0.hass;
|
|
document.body.appendChild(k);
|
|
k.style.cssText = 'position:fixed;left:0;top:0;width:900px;height:700px;z-index:99';
|
|
await new Promise((r) => setTimeout(r, 500));
|
|
k.hass = { ...c0.hass };
|
|
await k.updateComplete;
|
|
const sr = k.shadowRoot || k.renderRoot;
|
|
const stage = sr.querySelector('.stage');
|
|
const fire = (type, id, x, y) => stage.dispatchEvent(new PointerEvent(type, {
|
|
bubbles: true, composed: true, cancelable: true, pointerId: id,
|
|
pointerType: 'touch', isPrimary: true, button: 0,
|
|
buttons: type === 'pointerup' ? 0 : 1, clientX: x, clientY: y,
|
|
}));
|
|
k._applyView(1); await k.updateComplete;
|
|
const rect = stage.getBoundingClientRect();
|
|
const rightEdgeX = rect.right - 20;
|
|
const centerY = rect.top + rect.height / 2;
|
|
const s0 = k._space;
|
|
const v0 = { ...k._viewOr(k._baseVb()) };
|
|
// A real floor swipe starts inside the active right-edge strip and moves
|
|
// inward. Several moves prove that the plan never pans under the gesture.
|
|
fire('pointerdown', 41, rightEdgeX, centerY);
|
|
fire('pointermove', 41, rightEdgeX - 60, centerY + 2);
|
|
fire('pointermove', 41, rightEdgeX - 120, centerY + 5);
|
|
fire('pointermove', 41, rightEdgeX - 150, centerY + 5);
|
|
const midPan = Math.abs(k._viewOr(k._baseVb()).x - v0.x);
|
|
fire('pointerup', 41, rightEdgeX - 150, centerY + 5);
|
|
await k.updateComplete;
|
|
o.kioskSwipeStillSwitchesFloors = k._space !== s0;
|
|
o.kioskHorizontalDragIsNotAPan = midPan < 1;
|
|
// …and the plan is not left hanging half-panned after the switch
|
|
o.kioskViewCenteredAfterSwipe = Math.abs(k._viewOr(k._baseVb()).x - v0.x) < 2000;
|
|
// a VERTICAL drag is nobody's swipe, so it pans like everywhere else
|
|
const s1 = k._space;
|
|
const v1 = { ...k._viewOr(k._baseVb()) };
|
|
fire('pointerdown', 42, 450, 200);
|
|
fire('pointermove', 42, 452, 260);
|
|
fire('pointermove', 42, 454, 330);
|
|
const panned = Math.abs(k._viewOr(k._baseVb()).y - v1.y) > 1;
|
|
fire('pointerup', 42, 454, 330);
|
|
await k.updateComplete;
|
|
o.kioskVerticalDragPans = panned;
|
|
o.kioskVerticalDragKeepsFloor = k._space === s1;
|
|
// zoomed in, a horizontal drag pans (no swipe up there — unchanged rule)
|
|
k._applyView(2); await k.updateComplete;
|
|
const v2 = { ...k._view };
|
|
const s2 = k._space;
|
|
fire('pointerdown', 43, 600, 300);
|
|
fire('pointermove', 43, 540, 302);
|
|
fire('pointermove', 43, 480, 305);
|
|
const pannedZoomed = Math.abs(k._view.x - v2.x) > 1;
|
|
fire('pointerup', 43, 480, 305);
|
|
await k.updateComplete;
|
|
o.kioskZoomedHorizontalPans = pannedZoomed;
|
|
o.kioskZoomedDoesNotSwipe = k._space === s2;
|
|
k.remove();
|
|
return o;
|
|
}));
|
|
|
|
await finish(browser, checkAll(out));
|