Files
houseplan-card/demo/smoke_resize_virtual_thick.mjs
T

203 lines
7.9 KiB
JavaScript

/**
* Regression: resize a shared wall whose middle is virtual and whose solid
* remainders carry thickness. The live overlay, commit and Undo must move one
* geometry transaction. Virtual drawing paints above the real wall body in
* editors, but below it in View so thick-wall jambs mask the dash ends.
*/
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 sp = () => c._serverCfg.spaces.find((s) => s.id === c._space);
const upd = async () => { c._cfgEpoch++; c.requestUpdate(); await c.updateComplete; };
const settleMode = async () => {
const started = performance.now();
do { await new Promise((resolve) => requestAnimationFrame(resolve)); }
while (c._modeTransitionBusy && performance.now() - started < 1500);
await c.updateComplete;
};
sp().settings = { ...(sp().settings || {}), show_borders: true };
delete sp().walls;
delete sp().open_spans;
delete sp().openings;
for (const r of sp().rooms || []) delete r.open_to;
c._setMode('plan');
c._tool = 'wallthick';
await upd(); await settleMode();
c._wallThickClick([550, 250]);
await upd();
if (c._wallDialog) {
c._wallDialog = { ...c._wallDialog, value: '20' };
c._wallThickApply(false);
}
await upd();
out.realBodyReady = !!sr().querySelector('.wallbodies');
// The two-click rubber band used to be below .wallbodies and disappeared
// exactly where it crossed the real T receiver.
c._tool = 'boundary';
c._boundaryClick([550, 200]);
c._cursorPt = [550, 300];
await upd();
const bodyGroup = sr().querySelector('.wallbodies');
const virtualGroup = sr().querySelector('.openwalls');
out.virtualPreviewDrawn = !!sr().querySelector('.openwall-preview');
out.virtualPreviewAboveReal = !!bodyGroup && !!virtualGroup
&& !!(bodyGroup.compareDocumentPosition(virtualGroup) & Node.DOCUMENT_POSITION_FOLLOWING);
c._boundaryClick([550, 300]);
c._cursorPt = null;
await upd();
out.partialSpanCreated = (sp().open_spans || []).length === 1;
const editorBody = sr().querySelector('.wallbodies');
const editorVirtual = sr().querySelector('.openwalls');
out.savedVirtualAboveRealInEditor = !!editorBody && !!editorVirtual
&& !!(editorBody.compareDocumentPosition(editorVirtual) & Node.DOCUMENT_POSITION_FOLLOWING);
c._setMode('view');
await upd(); await settleMode();
const viewBody = sr().querySelector('.wallbodies');
const viewVirtual = sr().querySelector('.openwalls');
out.savedVirtualBelowRealInView = !!viewBody && !!viewVirtual
&& !!(viewVirtual.compareDocumentPosition(viewBody) & Node.DOCUMENT_POSITION_FOLLOWING);
c._setMode('decor');
await upd(); await settleMode();
const decorBody = sr().querySelector('.wallbodies');
const decorVirtual = sr().querySelector('.openwalls');
out.realWallsFadeBehindDecor = !!decorBody
&& getComputedStyle(decorBody).opacity === '0.35';
out.virtualWallsFadeBehindDecor = !!decorVirtual
&& getComputedStyle(decorVirtual).opacity === '0.35';
c._setMode('plan');
await upd(); await settleMode();
out.atomicThicknessBefore = c._intervalCm([550, 145, 550, 195]) === 20
&& c._intervalCm([550, 205, 550, 295]) === 0
&& c._intervalCm([550, 305, 550, 455]) === 20;
const before = JSON.stringify({
rooms: sp().rooms,
walls: sp().walls,
open_spans: sp().open_spans,
});
c._tool = 'resize';
await upd();
const stage = () => sr().querySelector('.stage');
const toScreen = (x, y) => {
const r = stage().getBoundingClientRect();
const v = c._viewOr(c._baseVb());
return {
clientX: r.left + ((x - v.x) / v.w) * r.width,
clientY: r.top + ((y - v.y) / v.h) * r.height,
};
};
const pev = (type, target, x, y) => {
const { clientX, clientY } = toScreen(x, y);
target.dispatchEvent(new PointerEvent(type, {
bubbles: true, composed: true, cancelable: true,
pointerId: 77, clientX, clientY, button: 0, isPrimary: true,
}));
};
const handle = [...sr().querySelectorAll('.rszhandle:not(.rszcorner)')].find((h) => {
const x = +h.getAttribute('cx'), y = +h.getAttribute('cy');
return Math.abs(x - 550) < c._gridPitch * 2 && y > 140 && y < 460;
});
out.resizeHandleFound = !!handle;
if (handle) {
const x = +handle.getAttribute('cx');
const y = +handle.getAttribute('cy');
const step = c._gridPitch;
pev('pointerdown', handle, x, y);
pev('pointermove', handle, x + step, y);
await c.updateComplete;
const liveCut = c._openCuts()[0];
const liveX = liveCut?.[0];
out.liveSpanMoves = Number.isFinite(liveX) && Math.abs(liveX - (550 + step)) < 0.1;
out.liveThicknessStays = c._intervalCm([liveX, 145, liveX, 195]) === 20
&& c._intervalCm([liveX, 205, liveX, 295]) === 0
&& c._intervalCm([liveX, 305, liveX, 455]) === 20;
out.liveBodyStays = !!sr().querySelector('.wallbodies');
pev('pointerup', handle, x + step, y);
await upd();
const finalCut = c._openCuts()[0];
const finalX = finalCut?.[0];
out.commitSpanMoves = Number.isFinite(finalX) && Math.abs(finalX - (550 + step)) < 0.1;
out.commitThicknessStays = c._intervalCm([finalX, 145, finalX, 195]) === 20
&& c._intervalCm([finalX, 205, finalX, 295]) === 0
&& c._intervalCm([finalX, 305, finalX, 455]) === 20;
out.commitKeepsAtomicKeys = (sp().walls || []).filter((w) => w.cm === 20).length >= 2;
c._undoGeometry();
await upd();
out.undoRestoresAll = JSON.stringify({
rooms: sp().rooms,
walls: sp().walls,
open_spans: sp().open_spans,
}) === before;
}
// Canonical storage: legacy/transactional fragments on one room pair become
// one selectable span, and closing it restores the uniformly thick parent
// wall instead of leaving atomic debris in `walls`.
const restoredCut = c._openCuts()[0];
if (restoredCut) {
const mx = (restoredCut[0] + restoredCut[2]) / 2;
const my = (restoredCut[1] + restoredCut[3]) / 2;
c._persistOpenCuts([
[restoredCut[0], restoredCut[1], mx, my],
[mx, my, restoredCut[2], restoredCut[3]],
]);
await upd();
out.adjacentSpansPersistAsOne = (sp().open_spans || []).length === 1
&& c._openCuts().length === 1;
c._closeOpenSpan(c._openCuts()[0]);
await upd();
out.closeRejoinsUniformWall = !sp().open_spans
&& (sp().walls || []).length === 1
&& c._intervalCm(restoredCut) === 20;
} else {
out.adjacentSpansPersistAsOne = false;
out.closeRejoinsUniformWall = false;
}
// The screenshot topology: the two real arms belong to different room
// contours and only meet at the endpoint of B↔C's virtual boundary. Their
// per-room butt caps must be completed into one clean outer mitre.
sp().rooms = [
{ id: 'ta', name: 'A', poly: [[0.1, 0.1], [0.5, 0.1], [0.5, 0.5], [0.1, 0.5]] },
{ id: 'tb', name: 'B', poly: [[0.5, 0.5], [0.9, 0.5], [0.9, 0.9], [0.5, 0.9]] },
{ id: 'tc', name: 'C', poly: [[0.5, 0.1], [0.9, 0.1], [0.9, 0.5], [0.5, 0.5]] },
];
delete sp().walls;
delete sp().open_spans;
c._tool = 'wallthick';
await upd();
for (const p of [[300, 500], [500, 700]]) {
c._wallThickClick(p);
await upd();
if (c._wallDialog) {
c._wallDialog = { ...c._wallDialog, value: '20' };
c._wallThickApply(false);
await upd();
}
}
sp().open_spans = [{ a: [0.5, 0.5], b: [0.9, 0.5] }];
sp().rooms.find((r) => r.id === 'tb').open_to = ['tc'];
sp().rooms.find((r) => r.id === 'tc').open_to = ['tb'];
await upd();
const tPath = sr().querySelector('.wallbody')?.getAttribute('d') || '';
const values = (tPath.match(/-?\d+(?:\.\d+)?/g) || []).map(Number);
const half = (20 / c._cellCm) * c._gridPitch / 2;
out.virtualTJunctionMitred = values.some((x, i) => i % 2 === 0
&& Math.abs(x - (500 + half)) < 1e-6
&& Math.abs(values[i + 1] - (500 - half)) < 1e-6);
return out;
});
checkAll(res);
await finish(browser, res);