Files
houseplan-card/demo/smoke_room_autoclose.mjs
T
2026-09-06 17:38:47 +03:00

155 lines
5.7 KiB
JavaScript

/** Issue #138: a new room may close along one existing continuous solid wall interval. */
import { launch, checkAll, finish } from './serve.mjs';
const { page, browser } = await launch({ width: 900, height: 820 }, 1);
const out = await page.evaluate(async () => {
const result = {};
const card = window.__card;
const root = () => card.shadowRoot || card.renderRoot;
const update = async () => { card.requestUpdate(); await card.updateComplete; };
const clone = (value) => JSON.parse(JSON.stringify(value));
const baseSpace = {
id: 'autoclose', title: 'Autoclose', cell_cm: 5, view_box: [0, 0, 1, 0.7],
rooms: [{
id: 'existing', name: 'Existing', area: null,
poly: [[0.1, 0.1], [0.5, 0.1], [0.5, 0.5], [0.1, 0.5]],
}],
walls: [{
key: '0.500000,0.300000@1.5708',
a: [0.5, 0.1], b: [0.5, 0.5], cm: 40,
}],
};
const reset = async (spacePatch = {}) => {
card._serverCfg = { spaces: [{ ...clone(baseSpace), ...clone(spacePatch) }], markers: [], settings: {} };
card._layout = {};
card._space = 'autoclose';
card._modelCache = null;
card._frame = null;
card._cfgEpoch++;
card._planSnapGeometryCache = null;
card._path = [];
card._activeWallChainId = null;
card._activeWallChainPartitionIds = [];
card._wallChainSegmentCms = [];
card._wallChainRedo = [];
card._closingWallCm = null;
card._roomDialog = false;
card._wallFaceBatch = null;
card._roomEditId = null;
card._pendingSplit = null;
card._toast = '';
card._drawWallField = '15';
card._setMode('plan');
card._tool = 'draw';
card._clearPlanSnapHover();
await update();
};
await reset();
const stage = root().querySelector('.stage');
const eventAt = (x, y, extra = {}) => {
const rect = stage.getBoundingClientRect();
const view = card._viewOr(card._baseVb());
return new MouseEvent('click', {
clientX: rect.left + ((x - view.x) / view.w) * rect.width,
clientY: rect.top + ((y - view.y) / view.h) * rect.height,
bubbles: true,
...extra,
});
};
const click = async (x, y, extra = {}) => {
card._markupClick(eventAt(x, y, extra));
await update();
};
const close = (a, b, epsilon = 1e-5) => Math.abs(a - b) <= epsilon;
const currentChain = () => card._activeWallChainPartitionIds
.map((id) => card._curSpaceCfg.partitions?.find((partition) => partition.id === id))
.filter(Boolean);
// r1 regression guard: two points on the common wall are still only an open line.
await click(500, 100);
await click(500, 500);
result.secondCommonWallPointStaysOpen = card._path.length === 2
&& !card._roomDialog && !card._toast
&& currentChain().length === 1;
// Endpoint-to-endpoint auto-close after the new room has enough vertices.
await reset();
await click(500, 100);
await click(800, 100);
await click(800, 500);
await click(500, 500);
const successfulChain = clone(currentChain());
result.endpointAutoCloseOpensDialog = card._roomDialog
&& card._wallFaceBatch?.candidates.length === 1
&& card._path.length === 4
&& close(card._path.at(-1)[0], 500)
&& close(card._path.at(-1)[1], 500);
result.terminalSegmentPersistsBeforeDialog = successfulChain.length === 3
&& close(successfulChain.at(-1).b[0], 0.5)
&& close(successfulChain.at(-1).b[1], 0.5)
&& successfulChain.every((segment) => close(segment.cm, 15));
result.roomIsNotCommittedBeforeSave = card._curSpaceCfg.rooms.length === 1;
card._roomDialogCancel();
await update();
result.cancelKeepsTerminalOpenChain = !card._roomDialog
&& card._path.length === 4
&& !card._contourClosed
&& JSON.stringify(currentChain()) === JSON.stringify(successfulChain)
&& close(card._path.at(-1)[0], 500) && close(card._path.at(-1)[1], 500);
// Save promotes the same draft, keeps the existing shared thickness and creates no partition.
await reset();
await click(500, 100);
await click(800, 100);
await click(800, 500);
await click(500, 500);
card._nameSel = 'Adjacent';
card._saveRoom();
await update();
const sharedWall = (card._curSpaceCfg.walls || []).find((wall) => wall.cm === 40
&& wall.a && wall.b
&& close(wall.a[0], 0.5) && close(wall.b[0], 0.5)
&& close(Math.min(wall.a[1], wall.b[1]), 0.1)
&& close(Math.max(wall.a[1], wall.b[1]), 0.5));
result.savePromotesChainWithoutPartition = card._curSpaceCfg.rooms.length === 2
&& card._curSpaceCfg.rooms.some((room) => room.name === 'Adjacent')
&& !('room_drafts' in card._curSpaceCfg)
&& !card._curSpaceCfg.partitions?.length
&& card._path.length === 0;
result.sharedWallKeepsNeighbourThickness = !!sharedWall
&& (card._curSpaceCfg.walls || []).filter((wall) => wall.cm === 15).length >= 3;
// #185: an architectural opening cuts masonry, not the structural wall axis.
await reset({ openings: [{
id: 'door', type: 'door', x: 0.5, y: 0.3, angle: 90, length: 0.1,
}] });
await click(500, 100);
await click(800, 100);
await click(800, 500);
await click(500, 500);
result.openingKeepsStructuralAutoClose = card._roomDialog
&& card._wallFaceBatch?.candidates.length === 1
&& card._path.length === 4
&& currentChain().length === 3;
// #173: a proper X is a derived graph node. The two bounded faces are
// offered, while the terminal draft remains the only persisted mutation.
await reset();
await click(500, 100);
await click(400, 300);
await click(700, 300);
await click(500, 500);
result.xIntersectionOffersFacesWithoutPartialRooms = card._roomDialog
&& card._wallFaceBatch?.candidates.length === 2
&& card._path.length === 4
&& currentChain().length === 3
&& card._curSpaceCfg.rooms.length === 1;
return result;
});
await finish(browser, checkAll(out));