test: cover finite wall beside an opening

Issue: #271
User-Visible: no
This commit is contained in:
Sergey Matyunin
2026-08-23 21:09:56 +03:00
parent dc94e016ca
commit 8bd910ed84
6 changed files with 282 additions and 182 deletions
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -263,12 +263,12 @@ export const GOLDEN_SCENARIOS = Object.freeze([
{ id: 'junction-patch-resilience-plan-dark', fixture: 'visual',
space: 'golden-junction-patch-resilience', junctionPatchResilience: true, mode: 'plan',
retainedWedgeProbe: [0.8955, 0.556],
absentWallProbes: [[0.420833333, 0.37625], [0.936524285, 0.345833333]],
absentWallProbes: [[0.420833333, 0.37625], [0.92, 0.348]],
theme: 'dark', viewport: { width: 1180, height: 900 }, ...page },
{ id: 'junction-patch-resilience-view-dark', fixture: 'visual',
space: 'golden-junction-patch-resilience', junctionPatchResilience: true, mode: 'view',
retainedWedgeProbe: [0.8955, 0.556],
absentWallProbes: [[0.420833333, 0.37625], [0.936524285, 0.345833333]],
absentWallProbes: [[0.420833333, 0.37625], [0.92, 0.348]],
theme: 'dark', viewport: { width: 1000, height: 900 }, ...stage },
{ id: 'multiwall-junction-bevel-view-dark', fixture: 'visual',
space: 'golden-multiwall-junction', mode: 'view',
+57 -1
View File
@@ -13,6 +13,8 @@ const result = await page.evaluate(async (spaceFixture) => {
const root = () => card.shadowRoot || card.renderRoot;
const retainedWedgeProbe = [895.5, 556];
const finiteRayOutsideProbe = [0.420833333 * 1000, 0.37625 * card._spaceH];
const finiteRayDoorApproachProbe = [0.92 * 1000, 0.348 * card._spaceH];
const finiteDoorSlotProbe = [0.95 * 1000, 0.345833333 * card._spaceH];
const svgContains = (element, point = retainedWedgeProbe) =>
!!element?.isPointInFill?.(new DOMPoint(point[0], point[1]));
const pathDataContains = (d, point) => {
@@ -93,6 +95,7 @@ const result = await page.evaluate(async (spaceFixture) => {
out.paperKeepsFootprint = !!canonical?.paperD && !!root().querySelector('.hp-paper');
out.planRetainsMeasuredWedge = svgContains(planPath);
out.planStopsAtFiniteRayEndpoint = !svgContains(planPath, finiteRayOutsideProbe);
out.planStopsAtLateralFiniteRayProbe = !svgContains(planPath, finiteRayDoorApproachProbe);
out.paperRetainsMeasuredWedge = [...root().querySelectorAll('.hp-paper')]
.some((paper) => svgContains(paper));
@@ -106,6 +109,9 @@ const result = await page.evaluate(async (spaceFixture) => {
out.lightAndSunStopAtFiniteRayEndpoint = !geometryContains(
barriers.masonryGeometry, finiteRayOutsideProbe,
);
out.lightAndSunStopAtLateralFiniteRayProbe = !geometryContains(
barriers.masonryGeometry, finiteRayDoorApproachProbe,
);
const barrierFingerprint = barriers.fingerprint;
const cacheBeforeState = card._wallUnionCache;
@@ -133,6 +139,9 @@ const result = await page.evaluate(async (spaceFixture) => {
out.viewStopsAtFiniteRayEndpoint = !svgContains(
root().querySelector('[data-hp="wall"]'), finiteRayOutsideProbe,
);
out.viewStopsAtLateralFiniteRayProbe = !svgContains(
root().querySelector('[data-hp="wall"]'), finiteRayDoorApproachProbe,
);
card._hoverRoom = { space: spaceFixture.id, room: model.rooms[0] };
const hoverFloor = card._roomHoverPaths(model);
out.cleanFloorConsumerStaysNonEmpty = !!hoverFloor?.fillD && !!hoverFloor.outlineD;
@@ -148,6 +157,11 @@ const result = await page.evaluate(async (spaceFixture) => {
if (!floor?.fillD) return false;
return pathDataContains(floor.fillD, finiteRayOutsideProbe);
});
out.cleanFloorOwnsFiniteDoorApproach = model.rooms.some((room) => {
card._hoverRoom = { space: spaceFixture.id, room };
const floor = card._roomHoverPaths(model);
return !!floor?.fillD && pathDataContains(floor.fillD, finiteRayDoorApproachProbe);
});
card._hoverRoom = null;
const kioskBefore = card._config.kiosk;
@@ -158,6 +172,9 @@ const result = await page.evaluate(async (spaceFixture) => {
out.kioskStopsAtFiniteRayEndpoint = !svgContains(
root().querySelector('[data-hp="wall"]'), finiteRayOutsideProbe,
);
out.kioskStopsAtLateralFiniteRayProbe = !svgContains(
root().querySelector('[data-hp="wall"]'), finiteRayDoorApproachProbe,
);
card._config.kiosk = kioskBefore;
await update(false);
@@ -187,6 +204,9 @@ const result = await page.evaluate(async (spaceFixture) => {
out.staticStopsAtFiniteRayEndpoint = !svgContains(
staticCard.renderRoot?.querySelector('[data-hp="wall"]'), finiteRayOutsideProbe,
);
out.staticStopsAtLateralFiniteRayProbe = !svgContains(
staticCard.renderRoot?.querySelector('[data-hp="wall"]'), finiteRayDoorApproachProbe,
);
staticCard.remove();
const labs = Object.freeze(['iso']);
@@ -201,8 +221,44 @@ const result = await page.evaluate(async (spaceFixture) => {
out.hiddenIsoStopsAtFiniteRayEndpoint = !geometryContains(
isoWalls, finiteRayOutsideProbe,
);
out.hiddenIsoStopsAtLateralFiniteRayProbe = !geometryContains(
isoWalls, finiteRayDoorApproachProbe,
);
out.renderNeverWritesConfig = JSON.stringify(card._serverCfg.spaces[0]) === sourceBefore;
// AC4: add an actual door only after the all-surface structural parity pass,
// so the static-card comparison above remains about one identical model.
// The door begins after the lateral phantom probe: final masonry must cut
// its slot, while the light barrier must still be empty before that slot.
const openingCfg = structuredClone(cfg);
openingCfg.spaces[0].openings = [{
id: 'finite-door', type: 'door', x: 0.95, y: 0.345833333,
angle: 0, length: 0.025,
}];
card._serverCfg = openingCfg;
card._setProjection('flat');
card._setMode('plan');
await update(true);
const openingSource = JSON.stringify(card._serverCfg.spaces[0]);
const openingPlanPath = root().querySelector('[data-hp="wall"]');
out.nearDoorPlanStopsAtFiniteRay = !svgContains(
openingPlanPath, finiteRayDoorApproachProbe,
);
out.nearDoorPlanKeepsSlotEmpty = !svgContains(openingPlanPath, finiteDoorSlotProbe);
out.nearDoorPlanKeepsSymbol = !!root().querySelector('.opening[data-id="finite-door"]');
const openingModel = card._spaceModel();
const openingPolys = openingModel.rooms.map((room) => ({ r: room, poly: room.poly }));
const openingBarriers = card._lightBarriers(openingModel, openingPolys);
out.nearDoorLightStopsAtFiniteRay = !geometryContains(
openingBarriers.masonryGeometry, finiteRayDoorApproachProbe,
);
card._setMode('view');
await update(false);
out.nearDoorViewKeepsSlotEmpty = !svgContains(
root().querySelector('[data-hp="wall"]'), finiteDoorSlotProbe,
);
out.nearDoorViewKeepsSymbol = !!root().querySelector('.opening[data-id="finite-door"]');
out.nearDoorRenderNeverWritesConfig = JSON.stringify(card._serverCfg.spaces[0]) === openingSource;
return out;
}, fixture);
+89 -89
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -374,7 +374,7 @@ test('issue #197 golden keeps the complete junction fixture in Plan and View', (
assert.deepEqual(scenario.retainedWedgeProbe, [0.8955, 0.556]);
assert.deepEqual(
scenario.absentWallProbes,
[[0.420833333, 0.37625], [0.936524285, 0.345833333]],
[[0.420833333, 0.37625], [0.92, 0.348]],
);
const fixture = prepareGoldenFixture(scenario);
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
+44
View File
@@ -914,6 +914,50 @@ test('issue #271 keeps finite co-directional ray supports and never rebuilds pas
);
});
test('issue #271 keeps a nearby door slot and its light-side approach free of a phantom ray', () => {
const fixture = JSON.parse(readFileSync(
new URL('./fixtures/197-junction-patch.json', import.meta.url), 'utf8',
));
const rooms = fixture.rooms.map((room) => ({
...room,
poly: room.poly.map(([x, y]) => [x * NORM_W, y * NORM_W]),
}));
const cuts = resolveOpenCuts(
rooms, fixture.open_spans, NORM_W, GRID_PITCH * 0.02,
);
const opening = {
x: 950, y: 345.8333333333333, angle: 0, length: 25,
};
const geometry = wallBodiesGeometry(
rooms, fixture.walls, cuts, [opening], pitch,
fixture.cell_cm, GRID_PITCH, NORM_W,
);
assert.ok(geometry);
assertProbeInside(
geometry.roomGeom, [950, 345.8333333333333],
'fixture no longer puts the door on physical masonry before the cut',
);
assertProbeOutside(
geometry.geom, [950, 345.8333333333333],
'the final door slot is not empty through the complete masonry',
);
assertProbeOutside(
geometry.roomGeom, [920, 348],
'the short 15 cm ray still paints a lateral phantom before the nearby door',
);
assertProbeOutside(
geometry.geom, [920, 348],
'opening subtraction or final union revived the lateral phantom',
);
assert.ok(
openingTunnelGeometry(
rooms, opening, fixture.walls, cuts, pitch,
fixture.cell_cm, GRID_PITCH, NORM_W,
),
'the associated opening tunnel contract disappeared',
);
});
test('issue #249 node classification is order, direction and scale independent', () => {
const cases = [
{ angles: [0, 30, 200], halves: [5, 5, 5], bevel: true },