mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-28 19:01:34 +00:00
213 lines
8.6 KiB
JavaScript
213 lines
8.6 KiB
JavaScript
/** Issues #132/#185/#186: hosted-opening lifecycle, continuity and jamb margin. */
|
|
import { launch, checkAll, finish } from './serve.mjs';
|
|
|
|
const { page, browser } = await launch({ width: 960, 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 pointIn = (point, body) => {
|
|
let inside = false;
|
|
for (let i = 0, j = body.length - 1; i < body.length; j = i++) {
|
|
const a = body[i], b = body[j];
|
|
if (((a[1] > point[1]) !== (b[1] > point[1]))
|
|
&& point[0] < ((b[0] - a[0]) * (point[1] - a[1])) / ((b[1] - a[1]) || 1e-12) + a[0])
|
|
inside = !inside;
|
|
}
|
|
return inside;
|
|
};
|
|
|
|
card._serverCfg = {
|
|
spaces: [{
|
|
id: 'partition-openings', title: 'Partition openings', cell_cm: 5,
|
|
view_box: [0, 0, 1, 1],
|
|
rooms: [{
|
|
id: 'room', name: 'Room', area: null,
|
|
poly: [[0.1, 0.1], [0.9, 0.1], [0.9, 0.9], [0.1, 0.9]],
|
|
}],
|
|
partitions: [{ id: 'host', a: [0.25, 0.5], b: [0.75, 0.5], cm: 15 }],
|
|
}],
|
|
markers: [], settings: {},
|
|
};
|
|
card._layout = {};
|
|
card._space = 'partition-openings';
|
|
card._modelCache = null;
|
|
card._cfgEpoch++;
|
|
let writes = 0;
|
|
card._saveConfig = () => {
|
|
writes++;
|
|
card._cfgEpoch++;
|
|
card._modelCache = null;
|
|
card._physicalBodiesCache = null;
|
|
card._planSnapGeometryCache = null;
|
|
card.requestUpdate();
|
|
};
|
|
await card._requestMode('plan');
|
|
card._activateOpeningPlacement('door');
|
|
await update();
|
|
|
|
const centre = [500, 500];
|
|
const candidate = card._resolveOpeningPlacement(centre);
|
|
result.partitionCandidateCarriesStableHost = candidate?.host?.kind === 'partition'
|
|
&& candidate.host.id === 'host' && Math.abs(candidate.host.t - 0.5) < 1e-9;
|
|
card._openingClick(centre);
|
|
card._saveOpening();
|
|
await update();
|
|
let space = card._curSpaceCfg;
|
|
const saved = space.openings?.[0];
|
|
result.saveMaterializesHostAndCompatibilityGeometry = saved?.host?.id === 'host'
|
|
&& Math.abs(saved.host.t - 0.5) < 1e-9
|
|
&& Math.abs(saved.x - 0.5) < 1e-9
|
|
&& Math.abs(saved.y - 0.5) < 1e-9
|
|
&& !!root().querySelector(`[data-hp="opening"][data-id="${saved.id}"]`);
|
|
|
|
const endpointCandidate = card._resolveOpeningPlacement([250, 500]);
|
|
const endpointJamb = endpointCandidate?.target?.physicalHalfWidth || 0;
|
|
const endpointX = 250 + (endpointCandidate?.renderedLength || 0) / 2 + endpointJamb;
|
|
result.placementClampsToHalfThicknessJamb = !!endpointCandidate
|
|
&& Math.abs(endpointCandidate.x - endpointX) < 1e-6
|
|
&& Math.abs(endpointCandidate.host.t - (endpointX - 250) / 500) < 1e-9
|
|
&& !!endpointCandidate.measure.labels[0].text;
|
|
|
|
const runtime = card._editorRuntime;
|
|
const originalSvgPoint = runtime._svgPoint;
|
|
card._opDrag = {
|
|
id: saved.id, moved: false, sx: 0, sy: 0, dirty: false,
|
|
before: card._geometrySnapshot(),
|
|
};
|
|
runtime._svgPoint = () => [250, 500];
|
|
card._opPointerMove(new PointerEvent('pointermove', {
|
|
pointerId: 186, clientX: 20, clientY: 0, bubbles: true,
|
|
}), saved);
|
|
await Promise.resolve();
|
|
runtime._svgPoint = originalSvgPoint;
|
|
card._opPointerUp(new PointerEvent('pointerup', { pointerId: 186, bubbles: true }), saved);
|
|
await update();
|
|
result.directDragStopsAtSameJambBoundary = Math.abs(saved.host.t - (endpointX - 250) / 500) < 1e-9
|
|
&& Math.abs(saved.x - endpointX / 1000) < 1e-9;
|
|
|
|
card._editOpening(card._openingsR.find((opening) => opening.id === saved.id));
|
|
card._openingDialog = { ...card._openingDialog, lengthCm: 600, lengthTouched: true };
|
|
const beforeRejectedDialog = JSON.stringify(space.openings);
|
|
const writesBeforeRejectedDialog = writes;
|
|
const historyBeforeRejectedDialog = card._geometryHistory.size;
|
|
await update();
|
|
const dialogGuidance = root().querySelector('.habindingbanner')?.textContent || '';
|
|
result.dialogShowsJambSpecificGuidance = dialogGuidance.includes('Leave at least')
|
|
|| dialogGuidance.includes('Оставьте');
|
|
card._saveOpening();
|
|
result.invalidLengthWritesNeitherConfigNorHistory = JSON.stringify(space.openings)
|
|
=== beforeRejectedDialog
|
|
&& writes === writesBeforeRejectedDialog
|
|
&& card._geometryHistory.size === historyBeforeRejectedDialog;
|
|
card._openingDialog = null;
|
|
|
|
saved.host.t = 0.5;
|
|
saved.x = 0.5;
|
|
saved.y = 0.5;
|
|
card._saveConfig();
|
|
space.partitions.push({ id: 'short', a: [0.2, 0.7], b: [0.27, 0.7], cm: 15 });
|
|
card._cfgEpoch++;
|
|
card._modelCache = null;
|
|
card._openingPlacementIntervalsCache = null;
|
|
const previousToast = card._showToast;
|
|
let jambToast = '';
|
|
card._showToast = (message) => { jambToast = message; };
|
|
const shortPoint = [235, 700];
|
|
const shortCandidate = card._resolveOpeningPlacement(shortPoint);
|
|
card._openingClick(shortPoint);
|
|
card._showToast = previousToast;
|
|
result.tooShortPartitionHasNoPreviewAndExplainsWhy = shortCandidate === null
|
|
&& (jambToast.includes('Leave at least') || jambToast.includes('Оставьте'));
|
|
space.partitions = space.partitions.filter((partition) => partition.id !== 'short');
|
|
card._cfgEpoch++;
|
|
card._modelCache = null;
|
|
card._openingPlacementIntervalsCache = null;
|
|
|
|
card._physicalBodiesCache = null;
|
|
const bodies = card._physicalBodiesR();
|
|
result.hostBodyHasFullDepthOpeningGap = !bodies.some((body) => pointIn(centre, body));
|
|
|
|
const base = card._spaceModel().partitions.find((partition) => partition.id === 'host');
|
|
card._physicalDrag = {
|
|
pid: 132, kind: 'partition', id: 'host', start: [...base.a],
|
|
startClient: [0, 0], before: card._geometrySnapshot(), moved: true,
|
|
base: JSON.parse(JSON.stringify(base)), delta: [50, 25],
|
|
};
|
|
card._physicalUp(new PointerEvent('pointerup', { pointerId: 132, bubbles: true }));
|
|
await update();
|
|
space = card._curSpaceCfg;
|
|
const movedOpening = space.openings?.[0];
|
|
result.rigidHostMoveKeepsTAndMovesProjectionAtomically = !!movedOpening
|
|
&& Math.abs(movedOpening.host.t - 0.5) < 1e-9
|
|
&& Math.abs(movedOpening.x - 0.55) < 1e-9
|
|
&& Math.abs(movedOpening.y - 0.525) < 1e-9;
|
|
|
|
card._physicalSel = { kind: 'partition', id: 'host' };
|
|
card._deletePhysicalSelection();
|
|
await update();
|
|
result.deleteRequiresAccessibleListDialog = !!card._partitionDeleteDialog
|
|
&& root().querySelectorAll('hp-dialog li').length === 1
|
|
&& !!space.partitions?.length && !!space.openings?.length;
|
|
card._partitionDeleteDialog = null;
|
|
await update();
|
|
result.deleteCancelIsMutationFree = !!space.partitions?.length && !!space.openings?.length;
|
|
card._physicalSel = { kind: 'partition', id: 'host' };
|
|
card._deletePhysicalSelection();
|
|
card._confirmPartitionDelete();
|
|
await update();
|
|
result.deleteConfirmCascadesHostAndOpening = !space.partitions && !space.openings;
|
|
card._undoGeometry();
|
|
await update();
|
|
space = card._curSpaceCfg;
|
|
result.deleteUndoRestoresHostAndOpening = space.partitions?.[0]?.id === 'host'
|
|
&& space.openings?.[0]?.host?.id === 'host';
|
|
card._redoGeometry();
|
|
await update();
|
|
space = card._curSpaceCfg;
|
|
result.deleteRedoCascadesAgain = !space.partitions && !space.openings;
|
|
|
|
space.partitions = [{ id: 'replacement', a: [0.3, 0.6], b: [0.7, 0.6], cm: 15 }];
|
|
space.openings = [{
|
|
id: 'orphan', type: 'door', x: 0.5, y: 0.6, angle: 0, length: 0.1,
|
|
host: { kind: 'partition', id: 'missing', t: 0.5 },
|
|
}];
|
|
card._saveConfig();
|
|
await update();
|
|
card._editOpening(card._openingsR[0]);
|
|
card._rebindPartitionOpening();
|
|
result.orphanRebindSessionKeepsOriginalIdentity = card._openingRebindId === 'orphan';
|
|
card._openingClick([500, 600]);
|
|
card._saveOpening();
|
|
await update();
|
|
result.orphanRebindReplacesHostWithoutDuplicate = space.openings.length === 1
|
|
&& space.openings[0].id === 'orphan'
|
|
&& space.openings[0].host?.id === 'replacement';
|
|
|
|
// Production-bundle regression for #185: a legacy room-wall opening does
|
|
// not cut the structural segment consumed by #173 room-face detection.
|
|
space.rooms = [{
|
|
id: 'room', name: 'Room', area: null,
|
|
poly: [[0.1, 0.1], [0.9, 0.1], [0.9, 0.9], [0.1, 0.9]],
|
|
}];
|
|
delete space.partitions;
|
|
space.openings = [{
|
|
id: 'legacy-door', type: 'door', x: 0.5, y: 0.1, angle: 0, length: 0.2,
|
|
}];
|
|
card._saveConfig();
|
|
await update();
|
|
const structural = card._planStructuralGeometrySnapshot().value;
|
|
result.openingKeepsRoomFaceAxisContinuous = structural.segments.some((segment) =>
|
|
segment.sourceKind === 'room'
|
|
&& Math.min(segment.a[0], segment.b[0]) <= 100 + 1e-6
|
|
&& Math.max(segment.a[0], segment.b[0]) >= 900 - 1e-6
|
|
&& Math.abs(segment.a[1] - 100) < 1e-6
|
|
&& Math.abs(segment.b[1] - 100) < 1e-6);
|
|
|
|
return result;
|
|
});
|
|
|
|
await finish(browser, checkAll(out));
|