mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-28 19:01:34 +00:00
155 lines
5.7 KiB
JavaScript
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));
|