mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
feat: refine hidden isometric Stage 4 (#570)
Issue: #570 User-Visible: no
This commit is contained in:
+14
-12
@@ -810,7 +810,7 @@ test('opening symbol goldens lock room, diagonal, flip-pair and hidden Iso contr
|
||||
}
|
||||
});
|
||||
|
||||
test('issue 160 Stage 3 goldens cover raised ownership, openings and solid fallbacks', () => {
|
||||
test('issue 570 Stage 4 reuses the historical iso goldens for visual handoff coverage', () => {
|
||||
const scenarios = GOLDEN_SCENARIOS.filter((item) => item.id.startsWith('isometric-stage3-'));
|
||||
assert.deepEqual(scenarios.map((item) => item.id), [
|
||||
'isometric-stage3-overlays-light',
|
||||
@@ -825,8 +825,8 @@ test('issue 160 Stage 3 goldens cover raised ownership, openings and solid fallb
|
||||
assert.deepEqual(new Set(overlays.map((item) => item.theme)), new Set(['light', 'dark']));
|
||||
assert.equal(overlays.every((item) => item.stage3Golden.requireNudged
|
||||
&& item.stage3Golden.requireDenseFacets
|
||||
&& item.stage3Golden.requireTetherCues
|
||||
&& item.stage3Golden.requireGrounding
|
||||
&& item.stage3Golden.requireNoTetherCues
|
||||
&& item.stage3Golden.requireNoGrounding
|
||||
&& item.stage3Golden.requiredKinds.includes('device')
|
||||
&& item.stage3Golden.requiredKinds.includes('room-label')
|
||||
&& item.stage3Golden.requiredKinds.includes('opening-lock')), true);
|
||||
@@ -866,8 +866,10 @@ test('issue 160 Stage 3 goldens cover raised ownership, openings and solid fallb
|
||||
const noFilter = scenarios.find((item) => item.disableIsoFilters);
|
||||
assert.equal(forced.stage3Golden.requireNoMaterialDefs, true);
|
||||
assert.equal(noFilter.stage3Golden.requireNoMaterialDefs, true);
|
||||
assert.equal(forced.stage3Golden.requireTetherCues, true);
|
||||
assert.equal(noFilter.stage3Golden.requireTetherCues, true);
|
||||
assert.equal(forced.stage3Golden.requireNoTetherCues, true);
|
||||
assert.equal(noFilter.stage3Golden.requireNoTetherCues, true);
|
||||
assert.equal(forced.stage3Golden.requireNoGrounding, true);
|
||||
assert.equal(noFilter.stage3Golden.requireNoGrounding, true);
|
||||
const noBorders = GOLDEN_SCENARIOS.find((item) => item.id === 'isometric-no-borders-dark');
|
||||
assert.equal(noBorders.stage3Golden.noBorders, true);
|
||||
|
||||
@@ -875,20 +877,20 @@ test('issue 160 Stage 3 goldens cover raised ownership, openings and solid fallb
|
||||
assert.match(harness,
|
||||
/\[data-hp-iso-overlay-kind\]\[data-hp-iso-floor\]\[data-hp-iso-visual\]/);
|
||||
assert.match(harness, /forcedColors: scenario\.forcedColors \? 'active' : 'none'/);
|
||||
assert.match(harness, /Stage 3 golden raised a floor-bound vacuum/);
|
||||
assert.match(harness, /Stage 3 no-borders golden retained raised overlays/);
|
||||
assert.match(harness, /Stage 4 golden raised a floor-bound vacuum/);
|
||||
assert.match(harness, /Stage 4 no-borders golden retained low overlays/);
|
||||
assert.match(harness, /!scenario\.stage3Golden\?\.noBorders[\s\S]*?data-hp-iso-stage/,
|
||||
'no-borders remains true Iso without requiring the intentionally absent volume stage');
|
||||
assert.match(harness,
|
||||
/\.\.\.\(scenario\.stage3Golden[\s\S]*?\? \{ themes: \{ darkMode: scenario\.theme === 'dark' \} \}[\s\S]*?: \{\}\)/,
|
||||
'explicit HA darkMode must be scoped to Stage 3 golden scenarios');
|
||||
'explicit HA darkMode must be scoped to isometric golden scenarios');
|
||||
assert.doesNotMatch(harness,
|
||||
/locale: \{ language: scenario\.language \|\| 'en' \},\s*themes:/,
|
||||
'legacy Flat and Iso golden inputs must not gain an unconditional HA themes field');
|
||||
assert.match(harness, /Stage 3 golden did not use HA darkMode/);
|
||||
assert.match(harness, /Stage 3 dense golden lacks/);
|
||||
assert.match(harness, /Stage 3 combined golden lacks/);
|
||||
assert.ok(harness.lastIndexOf('Stage 3 combined golden lacks')
|
||||
assert.match(harness, /Stage 4 golden did not use HA darkMode/);
|
||||
assert.match(harness, /Stage 4 dense golden lacks/);
|
||||
assert.match(harness, /Stage 4 combined golden lacks/);
|
||||
assert.ok(harness.lastIndexOf('Stage 4 combined golden lacks')
|
||||
> harness.lastIndexOf("await until(() => !!card.renderRoot.querySelector('.vactrail > path.case'))"),
|
||||
'combined live-layer preflight must run after the async vacuum fixture settles');
|
||||
});
|
||||
|
||||
+52
-16
@@ -1,12 +1,13 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
ISO_OPENING_FRAME_THICKNESS_RATIO, ISO_OPENING_LEAF_THICKNESS_RATIO,
|
||||
ISO_OPENING_FRAME_THICKNESS_RATIO, ISO_OPENING_GEOMETRY_POLICY,
|
||||
ISO_OPENING_LEAF_THICKNESS_RATIO,
|
||||
buildIsoOpeningBasis, isoOpeningBounds, projectIsoOpening,
|
||||
projectIsoOpeningStructure, resolveIsoDecoration,
|
||||
} from '../test-build/iso-openings.js';
|
||||
import { openingAmount } from '../test-build/logic.js';
|
||||
import { projectPlanPoint } from '../test-build/iso-projection.js';
|
||||
import { ISO_WALL_HEIGHT, projectPlanPoint } from '../test-build/iso-projection.js';
|
||||
|
||||
const opening = (patch = {}) => ({
|
||||
id: 'op-1', sourceIndex: 0, type: 'door', x: 100, y: 80,
|
||||
@@ -24,10 +25,11 @@ test('door basis is immutable and live amount preserves the exact jamb anchor',
|
||||
assert.match(closed.d, new RegExp(`^M ${hinge.replace('.', '\\.')}`));
|
||||
assert.match(open.d, new RegExp(`^M ${hinge.replace('.', '\\.')}`));
|
||||
assert.notEqual(open.d, closed.d);
|
||||
assert.equal(basis.leaves[0].top, 64 * 0.92);
|
||||
assert.equal(basis.leaves[0].top, ISO_WALL_HEIGHT * 0.92);
|
||||
assert.equal(basis.leaves[0].turnDeg, -50);
|
||||
});
|
||||
|
||||
test('Stage 3 basis freezes the real axis, physical face depth and jamb endpoints', () => {
|
||||
test('Stage 4 basis freezes the real axis, physical face depth and jamb endpoints', () => {
|
||||
const basis = buildIsoOpeningBasis(opening());
|
||||
assert.equal(Object.isFrozen(basis), true);
|
||||
assert.equal(Object.isFrozen(basis.axis), true);
|
||||
@@ -55,8 +57,8 @@ test('Stage 3 basis freezes the real axis, physical face depth and jamb endpoint
|
||||
{ jamb: 0, center: [70, 80], selected: [70, 85], opposite: [70, 75] },
|
||||
{ jamb: 1, center: [130, 80], selected: [130, 85], opposite: [130, 75] },
|
||||
]);
|
||||
assert.equal(basis.leafThickness, 64 * ISO_OPENING_LEAF_THICKNESS_RATIO);
|
||||
assert.equal(basis.frameThickness, 64 * ISO_OPENING_FRAME_THICKNESS_RATIO);
|
||||
assert.equal(basis.leafThickness, ISO_WALL_HEIGHT * ISO_OPENING_LEAF_THICKNESS_RATIO);
|
||||
assert.equal(basis.frameThickness, ISO_WALL_HEIGHT * ISO_OPENING_FRAME_THICKNESS_RATIO);
|
||||
|
||||
const rotated = buildIsoOpeningBasis(opening({
|
||||
angle: 90,
|
||||
@@ -102,14 +104,22 @@ test('door and gate expose matte fixed-thickness leaf prisms plus real jamb reve
|
||||
}
|
||||
});
|
||||
|
||||
test('window has only light inserts, frame and sill surfaces and no dark glass material', () => {
|
||||
test('window separates neutral fixed/sash frames from blue glass above the sill', () => {
|
||||
const basis = buildIsoOpeningBasis(opening({ type: 'window' }));
|
||||
assert.deepEqual(basis.windowProfile, {
|
||||
frameBottom: ISO_WALL_HEIGHT * 0.38,
|
||||
frameTop: ISO_WALL_HEIGHT,
|
||||
sashBottom: ISO_WALL_HEIGHT * 0.40,
|
||||
sashTop: ISO_WALL_HEIGHT * 0.98,
|
||||
glassBottom: ISO_WALL_HEIGHT * 0.45,
|
||||
glassTop: ISO_WALL_HEIGHT * 0.93,
|
||||
member: ISO_WALL_HEIGHT * 0.05,
|
||||
});
|
||||
const structure = projectIsoOpeningStructure(basis);
|
||||
assert.equal(structure.filter((surface) => surface.kind === 'jamb-reveal').length, 2);
|
||||
assert.equal(structure.filter((surface) => surface.kind === 'window-frame-side').length, 4);
|
||||
assert.equal(structure.filter((surface) => surface.kind === 'window-frame-top').length, 2);
|
||||
assert.equal(structure.filter((surface) => surface.kind === 'window-sill').length, 1);
|
||||
assert.equal(structure.some((surface) => /glass|dark/i.test(surface.material)), false);
|
||||
assert.equal(structure.filter((surface) => surface.kind.startsWith('window-'))
|
||||
.every((surface) => ['light-frame', 'light-sill'].includes(surface.material)), true);
|
||||
|
||||
@@ -117,12 +127,16 @@ test('window has only light inserts, frame and sill surfaces and no dark glass m
|
||||
assert.equal(panels.length, 2);
|
||||
assert.equal(panels.every((panel) => panel.material === 'light-window'
|
||||
&& panel.thickness === 0
|
||||
&& panel.surfaces.length === 1
|
||||
&& panel.surfaces[0].kind === 'window-insert'
|
||||
&& panel.surfaces[0].material === 'light-window'), true);
|
||||
&& panel.surfaces.length === 7
|
||||
&& panel.surfaces.filter((surface) => surface.material === 'light-frame').length === 4
|
||||
&& panel.surfaces.filter((surface) => surface.material === 'glass-side').length === 2
|
||||
&& panel.surfaces.filter((surface) => surface.material === 'glass-top').length === 1), true);
|
||||
assert.equal(panels.flatMap((panel) => panel.surfaces)
|
||||
.filter((surface) => surface.material.startsWith('glass'))
|
||||
.every((surface) => ['window-glass', 'window-glass-top'].includes(surface.kind)), true);
|
||||
});
|
||||
|
||||
test('live projection is O(leaves)-only and cannot mutate the structural Stage 3 basis', () => {
|
||||
test('live projection is O(leaves)-only and cannot mutate the structural Stage 4 basis', () => {
|
||||
const basis = buildIsoOpeningBasis(opening({ type: 'gate', flipV: true }));
|
||||
const snapshot = structuredClone(basis);
|
||||
const structure = projectIsoOpeningStructure(basis);
|
||||
@@ -133,17 +147,39 @@ test('live projection is O(leaves)-only and cannot mutate the structural Stage 3
|
||||
assert.deepEqual(basis, snapshot);
|
||||
});
|
||||
|
||||
test('window and gate retain two leaves, fixed height bounds and gate 10 degree turn', () => {
|
||||
test('window and gate retain two leaves with reviewed height and turn policies', () => {
|
||||
const windowBasis = buildIsoOpeningBasis(opening({ type: 'window' }));
|
||||
const gateBasis = buildIsoOpeningBasis(opening({ type: 'gate', flipV: true }));
|
||||
assert.equal(windowBasis.leaves.length, 2);
|
||||
assert.equal(windowBasis.leaves.every((leaf) => leaf.bottom === 64 * 0.27
|
||||
&& leaf.top === 64 * 0.78), true);
|
||||
assert.equal(ISO_OPENING_GEOMETRY_POLICY.revision, 2);
|
||||
assert.equal(windowBasis.leaves.every((leaf) => leaf.bottom === ISO_WALL_HEIGHT * 0.40
|
||||
&& leaf.top === ISO_WALL_HEIGHT * 0.98 && Math.abs(leaf.turnDeg) === 65), true);
|
||||
assert.deepEqual(gateBasis.leaves.map((leaf) => Math.abs(leaf.turnDeg)), [10, 10]);
|
||||
assert.equal(gateBasis.leaves.every((leaf) => leaf.top === 64 * 0.88), true);
|
||||
assert.equal(gateBasis.leaves.every((leaf) => leaf.top === ISO_WALL_HEIGHT * 0.88), true);
|
||||
assert.equal(projectIsoOpening(gateBasis, 1).length, 2);
|
||||
});
|
||||
|
||||
test('paired window leaves open toward their resolved exterior face in every orientation', () => {
|
||||
for (const angle of [0, 90, 180, 270, 37]) {
|
||||
const radians = angle * Math.PI / 180;
|
||||
const normal = [-Math.sin(radians), Math.cos(radians)];
|
||||
for (const flipV of [false, true]) for (const flipH of [false, true]) {
|
||||
const side = flipV ? 1 : -1;
|
||||
const basis = buildIsoOpeningBasis(opening({
|
||||
type: 'window', angle, flipV, flipH,
|
||||
face: { ox: normal[0] * side * 5, oy: normal[1] * side * 5, cm: 20, side },
|
||||
}));
|
||||
for (const leaf of basis.leaves) {
|
||||
const turn = leaf.turnDeg * Math.PI / 180;
|
||||
const swingNormal = (leaf.quarterVector[0] * Math.sin(turn)) * normal[0]
|
||||
+ (leaf.quarterVector[1] * Math.sin(turn)) * normal[1];
|
||||
assert.ok(swingNormal * side > 0,
|
||||
`angle=${angle} flipH=${flipH} flipV=${flipV} leaf=${leaf.leaf}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('isometric symbols keep one centre while flips change only direction', () => {
|
||||
const centred = buildIsoOpeningBasis(opening());
|
||||
const oppositeResolvedFace = buildIsoOpeningBasis(opening({
|
||||
|
||||
+20
-20
@@ -11,7 +11,6 @@ import {
|
||||
} from '../test-build/iso-overlays.js';
|
||||
import {
|
||||
ISO_RAISED_OVERLAY_HEIGHT,
|
||||
ISO_WALL_HEIGHT,
|
||||
projectPlanPoint,
|
||||
} from '../test-build/iso-projection.js';
|
||||
|
||||
@@ -43,7 +42,7 @@ const placement = (overrides = {}) => resolveIsoOverlayPlacement({
|
||||
...overrides,
|
||||
});
|
||||
|
||||
test('the exact D2 overlay matrix keeps only the three accepted roots raised', () => {
|
||||
test('the exact Stage 4 overlay matrix keeps only the three interactive roots on the low plane', () => {
|
||||
for (const kind of ['device', 'room-label', 'opening-lock'])
|
||||
assert.equal(isoOverlayPlane(kind, true), 'raised', kind);
|
||||
for (const kind of [
|
||||
@@ -124,7 +123,7 @@ test('a degenerate room has no safe point and placement degrades without throwin
|
||||
const result = placement({
|
||||
rooms: [degenerate],
|
||||
preferredRoomId: degenerate.id,
|
||||
wallSilhouettes: [{ outer: buildIsoFootprintPolygon([50, 50], [10, 10], ISO_WALL_HEIGHT) }],
|
||||
wallSilhouettes: [{ outer: buildIsoFootprintPolygon([50, 50], [10, 10], ISO_RAISED_OVERLAY_HEIGHT) }],
|
||||
});
|
||||
assert.equal(result.owner, null);
|
||||
assert.equal(result.status, 'degraded');
|
||||
@@ -140,11 +139,12 @@ test('footprint corners use the same affine camera on the raised plane', () => {
|
||||
close(footprint[index][0], projected[0]);
|
||||
close(footprint[index][1], projected[1]);
|
||||
});
|
||||
assert.notEqual(footprint[0][1], footprint[1][1],
|
||||
'the +4° footprint is a projected parallelogram');
|
||||
close(footprint[0][1], footprint[1][1]);
|
||||
assert.ok(footprint[2][1] > footprint[1][1],
|
||||
'zero yaw keeps the screen-facing footprint aligned with the plan axes');
|
||||
});
|
||||
|
||||
test('free raised overlay separates floor, raised, footprint and optional tether geometry', () => {
|
||||
test('free low overlay separates the immutable floor anchor from invisible collision geometry', () => {
|
||||
const normal = placement();
|
||||
assert.equal(normal.plane, 'raised');
|
||||
assert.deepEqual(normal.floorAnchor, [50, 50]);
|
||||
@@ -152,19 +152,19 @@ test('free raised overlay separates floor, raised, footprint and optional tether
|
||||
assert.deepEqual(normal.raisedScene, projectPlanPoint([50, 50], ISO_RAISED_OVERLAY_HEIGHT));
|
||||
assert.deepEqual(normal.visualScene, normal.raisedScene);
|
||||
assert.equal(normal.footprint.length, 4);
|
||||
assert.equal(normal.grounding.visible, true);
|
||||
assert.equal(normal.grounding.visible, false);
|
||||
assert.equal(normal.tether.visible, false);
|
||||
assert.equal(normal.status, 'ok');
|
||||
|
||||
for (const state of ['hovered', 'focused', 'selected'])
|
||||
assert.equal(placement({ [state]: true }).tether.visible, true, state);
|
||||
assert.equal(placement({ [state]: true }).tether.visible, false, state);
|
||||
assert.equal(placement({ filtersSupported: false }).grounding.visible, false,
|
||||
'unsupported filters remove the soft grounding shadow only');
|
||||
});
|
||||
|
||||
test('wall-aware nudge is deterministic, minimal, inward and never changes the floor anchor', () => {
|
||||
const wall = {
|
||||
outer: buildIsoFootprintPolygon([0, 50], [4, 60], ISO_WALL_HEIGHT),
|
||||
outer: buildIsoFootprintPolygon([0, 50], [4, 60], ISO_RAISED_OVERLAY_HEIGHT),
|
||||
};
|
||||
const input = {
|
||||
floorAnchor: [5, 50],
|
||||
@@ -188,12 +188,12 @@ test('wall-aware nudge is deterministic, minimal, inward and never changes the f
|
||||
const towardSafe = projectPlanPoint([50, 50], ISO_RAISED_OVERLAY_HEIGHT);
|
||||
const safeVector = [towardSafe[0] - first.raisedScene[0], towardSafe[1] - first.raisedScene[1]];
|
||||
assert.ok(first.nudgeScene[0] * safeVector[0] + first.nudgeScene[1] * safeVector[1] > 0);
|
||||
assert.equal(first.tether.visible, true);
|
||||
assert.equal(first.tether.visible, false);
|
||||
});
|
||||
|
||||
test('shared-wall and corner fixtures nudge only toward the selected room', () => {
|
||||
const verticalWall = {
|
||||
outer: buildIsoFootprintPolygon([50, 50], [3, 60], ISO_WALL_HEIGHT),
|
||||
outer: buildIsoFootprintPolygon([50, 50], [3, 60], ISO_RAISED_OVERLAY_HEIGHT),
|
||||
};
|
||||
const shared = placement({
|
||||
floorAnchor: [49, 50],
|
||||
@@ -212,14 +212,14 @@ test('shared-wall and corner fixtures nudge only toward the selected room', () =
|
||||
rooms: [square('corner', 0, 0, 100, 100, [50, 50])],
|
||||
preferredRoomId: 'corner',
|
||||
wallSilhouettes: [
|
||||
{ outer: buildIsoFootprintPolygon([0, 50], [4, 60], ISO_WALL_HEIGHT) },
|
||||
{ outer: buildIsoFootprintPolygon([50, 0], [60, 4], ISO_WALL_HEIGHT) },
|
||||
{ outer: buildIsoFootprintPolygon([0, 50], [4, 60], ISO_RAISED_OVERLAY_HEIGHT) },
|
||||
{ outer: buildIsoFootprintPolygon([50, 0], [60, 4], ISO_RAISED_OVERLAY_HEIGHT) },
|
||||
],
|
||||
});
|
||||
assert.equal(corner.nearWallBefore, true);
|
||||
assert.ok(corner.nudgeScene[0] > 0 && corner.nudgeScene[1] > 0,
|
||||
'corner marker follows the projected inward diagonal');
|
||||
assert.equal(corner.tether.visible, true);
|
||||
assert.equal(corner.tether.visible, false);
|
||||
});
|
||||
|
||||
test('nudge never crosses an island hole or a concave-room boundary', () => {
|
||||
@@ -275,9 +275,9 @@ test('nudge never crosses an island hole or a concave-room boundary', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('cap and ambiguous ownership fail safe with a visible tether', () => {
|
||||
test('cap and ambiguous ownership fail safe without bringing back debug cues', () => {
|
||||
const largeWall = {
|
||||
outer: buildIsoFootprintPolygon([25, 50], [30, 30], ISO_WALL_HEIGHT),
|
||||
outer: buildIsoFootprintPolygon([25, 50], [30, 30], ISO_RAISED_OVERLAY_HEIGHT),
|
||||
};
|
||||
const capped = placement({
|
||||
floorAnchor: [25, 50], wallSilhouettes: [largeWall], maxNudgeCssPx: 2,
|
||||
@@ -287,19 +287,19 @@ test('cap and ambiguous ownership fail safe with a visible tether', () => {
|
||||
assert.equal(capped.cleared, false);
|
||||
assert.equal(capped.status, 'degraded');
|
||||
assert.equal(capped.reason, 'nudge-cap');
|
||||
assert.equal(capped.tether.visible, true);
|
||||
assert.equal(capped.tether.visible, false);
|
||||
|
||||
const ownerless = placement({
|
||||
floorAnchor: [150, 50], rooms: [], preferredRoomId: null,
|
||||
wallSilhouettes: [{
|
||||
outer: buildIsoFootprintPolygon([150, 50], [10, 10], ISO_WALL_HEIGHT),
|
||||
outer: buildIsoFootprintPolygon([150, 50], [10, 10], ISO_RAISED_OVERLAY_HEIGHT),
|
||||
}],
|
||||
});
|
||||
assert.equal(ownerless.owner, null);
|
||||
assert.equal(ownerless.nudged, false);
|
||||
assert.equal(ownerless.status, 'degraded');
|
||||
assert.equal(ownerless.reason, 'missing-owner');
|
||||
assert.equal(ownerless.tether.visible, true);
|
||||
assert.equal(ownerless.tether.visible, false);
|
||||
});
|
||||
|
||||
test('malformed collision input degrades without a guessed move', () => {
|
||||
@@ -309,7 +309,7 @@ test('malformed collision input degrades without a guessed move', () => {
|
||||
assert.equal(result.status, 'degraded');
|
||||
assert.equal(result.reason, 'invalid-wall-geometry');
|
||||
assert.equal(result.nudged, false);
|
||||
assert.equal(result.tether.visible, true);
|
||||
assert.equal(result.tether.visible, false);
|
||||
});
|
||||
|
||||
test('show_borders:false is exact no-volume: floor anchor, no footprint/nudge/cues', () => {
|
||||
|
||||
@@ -3,24 +3,25 @@ import assert from 'node:assert/strict';
|
||||
import {
|
||||
ISO_CAMERA, ISO_OVERLAY_VISUAL_OFFSET, ISO_RAISED_OVERLAY_HEIGHT,
|
||||
ISO_WALL_HEIGHT, applyIsoMatrix, clientToScenePoint, isoFloorMatrix,
|
||||
isoPlaneMatrix, isoPlaneMatrixCss, isoRaisedOverlayHeight, projectPlanPoint,
|
||||
isoOverlayVisualHeight, isoPlaneMatrix, isoPlaneMatrixCss, projectPlanPoint,
|
||||
projectedFrame, unprojectFloorPoint,
|
||||
} from '../test-build/iso-projection.js';
|
||||
|
||||
const close = (actual, expected, epsilon = 1e-9) =>
|
||||
assert.ok(Math.abs(actual - expected) <= epsilon, `${actual} != ${expected}`);
|
||||
|
||||
test('Stage 3 camera is the exact fixed +4°/20° orthographic camera', () => {
|
||||
assert.equal(ISO_CAMERA.rotDeg, 4);
|
||||
test('Stage 4 camera is the exact fixed 0°/20° orthographic camera', () => {
|
||||
assert.equal(ISO_CAMERA.rotDeg, 0);
|
||||
assert.equal(ISO_CAMERA.tiltDeg, 20);
|
||||
const origin = projectPlanPoint([500, 500], 0);
|
||||
assert.deepEqual(origin, [500, 500]);
|
||||
const x = projectPlanPoint([600, 500], 0);
|
||||
const y = projectPlanPoint([500, 600], 0);
|
||||
assert.ok(x[0] > 500 && x[1] > 500, 'positive plan X follows the +4° yaw');
|
||||
assert.ok(y[0] < 500 && y[1] > 500, 'positive plan Y follows the +4° yaw');
|
||||
assert.ok(x[0] > 500 && x[1] === 500, 'positive plan X stays horizontal at zero yaw');
|
||||
assert.ok(y[0] === 500 && y[1] > 500, 'positive plan Y follows the tilted vertical axis');
|
||||
assert.equal(ISO_WALL_HEIGHT, 84);
|
||||
assert.equal(ISO_OVERLAY_VISUAL_OFFSET, 4);
|
||||
assert.equal(ISO_RAISED_OVERLAY_HEIGHT, 68);
|
||||
assert.equal(ISO_RAISED_OVERLAY_HEIGHT, 4);
|
||||
});
|
||||
|
||||
test('floor projection round-trips across the infinite canvas contract', () => {
|
||||
@@ -52,7 +53,7 @@ test('one canonical affine helper projects floor and raised planes', () => {
|
||||
assert.match(isoPlaneMatrixCss(z), /^matrix\([-0-9.e ]+\)$/);
|
||||
}
|
||||
assert.deepEqual(isoPlaneMatrix(0), isoFloorMatrix());
|
||||
assert.equal(isoRaisedOverlayHeight(64, 4), 68);
|
||||
assert.equal(isoOverlayVisualHeight(4), 4);
|
||||
});
|
||||
|
||||
test('projected frame includes raised wall tops and is view-state independent', () => {
|
||||
@@ -75,7 +76,7 @@ test('Stage 2 frame includes opening tops and the structural floor edge, not blu
|
||||
assert.deepEqual(Object.keys(stage2).sort(), ['h', 'w', 'x', 'y']);
|
||||
});
|
||||
|
||||
test('Stage 3 frame includes the final raised plane and diagonal camera corners', () => {
|
||||
test('Stage 4 frame already contains the low overlay plane at zero yaw', () => {
|
||||
const rect = { x: 100, y: 200, w: 400, h: 300 };
|
||||
const wallFrame = projectedFrame({ rect, wallHeight: ISO_WALL_HEIGHT });
|
||||
const raisedFrame = projectedFrame({
|
||||
@@ -83,15 +84,14 @@ test('Stage 3 frame includes the final raised plane and diagonal camera corners'
|
||||
wallHeight: ISO_WALL_HEIGHT,
|
||||
raisedHeight: ISO_RAISED_OVERLAY_HEIGHT,
|
||||
});
|
||||
assert.ok(raisedFrame.y < wallFrame.y, 'raised overlay top is not clipped');
|
||||
assert.ok(raisedFrame.h > wallFrame.h, 'raised overlay height participates in fit bounds');
|
||||
assert.deepEqual(raisedFrame, wallFrame, 'the wall top encloses the low overlay plane');
|
||||
const projectedFloor = [
|
||||
[rect.x, rect.y], [rect.x + rect.w, rect.y],
|
||||
[rect.x + rect.w, rect.y + rect.h], [rect.x, rect.y + rect.h],
|
||||
].map((point) => projectPlanPoint(point, 0));
|
||||
const projectedWidth = Math.max(...projectedFloor.map((point) => point[0]))
|
||||
- Math.min(...projectedFloor.map((point) => point[0]));
|
||||
assert.ok(projectedWidth > rect.w, 'the +4° camera expands the frame diagonally');
|
||||
close(projectedWidth, rect.w);
|
||||
});
|
||||
|
||||
test('client coordinates map through the current scene view', () => {
|
||||
@@ -105,7 +105,7 @@ test('degenerate cameras and frames throw instead of mixing projections', () =>
|
||||
assert.throws(() => unprojectFloorPoint([0, 0], { ...ISO_CAMERA, tiltDeg: 90 }));
|
||||
assert.throws(() => clientToScenePoint([0, 0], { left: 0, top: 0, width: 0, height: 1 },
|
||||
{ x: 0, y: 0, w: 1, h: 1 }));
|
||||
assert.throws(() => isoRaisedOverlayHeight(64, -1));
|
||||
assert.throws(() => isoOverlayVisualHeight(-1));
|
||||
assert.throws(() => projectedFrame({
|
||||
rect: { x: 0, y: 0, w: 1, h: 1 }, wallHeight: 64, raisedHeight: -1,
|
||||
}));
|
||||
|
||||
@@ -19,7 +19,12 @@ import {
|
||||
resolveIsoOverlayFitEnvelope,
|
||||
resolveIsoScene,
|
||||
} from '../test-build/iso-scene-render.js';
|
||||
import { ISO_OPENING_GEOMETRY_POLICY } from '../test-build/iso-openings.js';
|
||||
import {
|
||||
ISO_OPENING_GEOMETRY_POLICY,
|
||||
buildIsoOpeningBasis,
|
||||
projectIsoOpening,
|
||||
projectIsoOpeningStructure,
|
||||
} from '../test-build/iso-openings.js';
|
||||
import { buildIsoWallGeometry } from '../test-build/iso-walls.js';
|
||||
import { wallKey } from '../test-build/wall-thickness.js';
|
||||
import {
|
||||
@@ -40,7 +45,7 @@ const room = (id, x0, y0, x1, y1) => ({
|
||||
poly: [[x0, y0], [x1, y0], [x1, y1], [x0, y1]],
|
||||
});
|
||||
|
||||
test('every Stage 3 shadow plane uses the same scale-aware fixed-light vector', () => {
|
||||
test('every Stage 4 shadow plane uses the same scale-aware fixed-light vector', () => {
|
||||
assert.equal(isoFixedLightTransform(5), 'translate(4 8)');
|
||||
assert.equal(isoFixedLightTransform(1), 'translate(20 40)');
|
||||
});
|
||||
@@ -188,7 +193,7 @@ test('one painter queue paints a nearer wall after an unrelated rear opening', (
|
||||
const rearOpening = {
|
||||
id: 'rear-door', sourceIndex: 4, type: 'door', leaf: 0,
|
||||
kind: 'leaf-front', material: 'matte-leaf', d: 'M 0 0 L 1 0 L 1 1 Z',
|
||||
depth: nearWall.depth - 10,
|
||||
depth: nearWall.depth - 10, cameraDepth: nearWall.depth - 10,
|
||||
};
|
||||
const queue = buildIsoWallDepthQueue(geometry, [rearOpening]);
|
||||
const rearIndex = queue.findIndex((entry) => entry.layer === 'opening');
|
||||
@@ -200,6 +205,40 @@ test('one painter queue paints a nearer wall after an unrelated rear opening', (
|
||||
'the combined wall/opening order is deterministic');
|
||||
});
|
||||
|
||||
test('shared painter queue puts elevated window glass over its rear sill only in window slots', () => {
|
||||
const geometry = buildIsoWallGeometry([[[
|
||||
[0, 100], [100, 100], [100, 200], [0, 200], [0, 100],
|
||||
]]]);
|
||||
const basis = buildIsoOpeningBasis({
|
||||
id: 'window-overlap', sourceIndex: 2, type: 'window', x: 50, y: 100,
|
||||
angle: 0, length: 60, flipH: false, flipV: false,
|
||||
face: { ox: 0, oy: -5, cm: 20, side: -1 },
|
||||
});
|
||||
const surfaces = [
|
||||
...projectIsoOpeningStructure(basis),
|
||||
...projectIsoOpening(basis, 0).flatMap((panel) => panel.surfaces),
|
||||
].map((surface, index) => ({
|
||||
...surface, id: basis.id, sourceIndex: basis.sourceIndex,
|
||||
type: 'window', leaf: index,
|
||||
}));
|
||||
const queue = buildIsoWallDepthQueue(geometry, surfaces);
|
||||
const windowEntries = queue.filter((entry) => entry.layer === 'opening');
|
||||
assert.deepEqual(windowEntries.map((entry) => entry.surface.cameraDepth),
|
||||
windowEntries.map((entry) => entry.surface.cameraDepth).toSorted((a, b) => a - b),
|
||||
'one window reuses its shared queue slots in physical camera-depth order');
|
||||
const sillIndex = queue.findIndex((entry) => entry.layer === 'opening'
|
||||
&& entry.surface.kind === 'window-sill');
|
||||
const glassIndices = queue.flatMap((entry, index) => entry.layer === 'opening'
|
||||
&& entry.surface.material.startsWith('glass') ? [index] : []);
|
||||
assert.ok(sillIndex >= 0 && glassIndices.length === 6
|
||||
&& glassIndices.some((index) => index > sillIndex),
|
||||
'elevated glass must paint after the rear sill projection');
|
||||
const wallSlots = queue.flatMap((entry, index) => entry.layer === 'opening' ? [] : [index]);
|
||||
const reversed = buildIsoWallDepthQueue(geometry, [...surfaces].reverse());
|
||||
assert.deepEqual(reversed.flatMap((entry, index) => entry.layer === 'opening' ? [] : [index]),
|
||||
wallSlots, 'window-local occlusion cannot move unrelated wall slots');
|
||||
});
|
||||
|
||||
test('production overlay rooms preserve direct island holes and cache safe points', () => {
|
||||
const outer = room('outer', 0, 0, 100, 100);
|
||||
const island = room('island', 40, 40, 60, 60);
|
||||
@@ -335,10 +374,10 @@ test('opening lock without a canonical host owner never guesses from point conta
|
||||
assert.equal(placement?.owner, null);
|
||||
assert.equal(placement?.nudged, false);
|
||||
assert.equal(placement?.reason, 'missing-owner');
|
||||
assert.equal(placement?.tether.visible, true);
|
||||
assert.equal(placement?.tether.visible, false);
|
||||
});
|
||||
|
||||
test('Stage 3 reuses pure overlay placements and fit probes skip collision search', () => {
|
||||
test('Stage 4 reuses pure overlay placements and fit probes skip collision search', () => {
|
||||
const owner = room('owner', 0, 0, 100, 100);
|
||||
const space = {
|
||||
id: 'floor', title: 'Floor', cellCm: 5, vb: [0, 0, 100, 100], bg: null,
|
||||
@@ -458,7 +497,7 @@ test('visible wall side quads participate in overlay collision', () => {
|
||||
'the same raised footprint intersects a rendered vertical side');
|
||||
});
|
||||
|
||||
test('orphan hosted openings never become phantom Stage 3 volumes', () => {
|
||||
test('orphan hosted openings never become phantom Stage 4 volumes', () => {
|
||||
const base = {
|
||||
type: 'door', rx: 20, ry: 30, rlen: 40, angle: 0,
|
||||
flip_h: false, flip_v: false,
|
||||
@@ -746,19 +785,16 @@ const perfFixture = () => {
|
||||
return { input, wallSilhouettes };
|
||||
};
|
||||
|
||||
test('#473 W1: выделение входит в подпись кэша размещений', () => {
|
||||
test('#570 supersedes #473 W1: selection reuses the cue-free low placement', () => {
|
||||
const { input } = perfFixture();
|
||||
const plain = buildIsoOverlayRenderScene(input).devices.get('device');
|
||||
const selected = buildIsoOverlayRenderScene({ ...input, selectedDeviceId: 'device' })
|
||||
.devices.get('device');
|
||||
// Подпись без `selected` вернула бы тот же кэшированный объект — плита
|
||||
// выделенного устройства осталась бы без своего размещения.
|
||||
assert.notStrictEqual(selected, plain, 'выделение обязано дать своё размещение, а не кэш');
|
||||
// Кэш держит одну запись на (режим, вид, id): снятие выделения пересчитывает
|
||||
// заново, и это правильно — подпись изменилась. Тождества с `plain` здесь
|
||||
// быть не должно, проверяется только отсутствие подмены.
|
||||
// Stage 4 removed the selected/hover tether and ground cue. Selection no
|
||||
// longer changes collision geometry and therefore must not churn the cache.
|
||||
assert.strictEqual(selected, plain, 'selection is presentation-only after cue removal');
|
||||
const again = buildIsoOverlayRenderScene(input).devices.get('device');
|
||||
assert.notStrictEqual(again, selected, 'снятие выделения не отдаёт выделенное размещение');
|
||||
assert.strictEqual(again, selected, 'clearing selection keeps the same immutable placement');
|
||||
});
|
||||
|
||||
test('#473 W2: кэш размещений привязан к идентичности массива силуэтов', () => {
|
||||
@@ -808,7 +844,7 @@ test('#473 W4: AABB-отсечение учитывает зазор безоп
|
||||
// до точного теста, и плита легла бы вплотную к стене.
|
||||
// Стена — компактный силуэт: у длинной стены AABB в изометрии накрывает плиту
|
||||
// при любом сдвиге вдоль оси, и отсечение не участвует в решении.
|
||||
const wall = { outer: buildIsoFootprintPolygon([0, 50], [2, 2], ISO_WALL_HEIGHT) };
|
||||
const wall = { outer: buildIsoFootprintPolygon([0, 50], [2, 2], 0) };
|
||||
const owner = { id: 'owner', outer: [[0, 0], [100, 0], [100, 100], [0, 100]], holes: [], safePoint: [50, 50] };
|
||||
const placeAt = (x, safetyGapCssPx) => resolveIsoOverlayPlacement({
|
||||
kind: 'device', floorAnchor: [x, 50], footprintHalfSize: [2, 2],
|
||||
|
||||
@@ -42,7 +42,7 @@ test('Labs iso is presentation-only and absent from the secondary space card', (
|
||||
assert.doesNotMatch(labs, /CARD_VERSION|houseplan_card_labs_v1|params\.getAll\('hp-labs'\)/);
|
||||
});
|
||||
|
||||
test('Stage 3 code is loaded only through the hidden alpha runtime boundary', () => {
|
||||
test('Stage 4 code is loaded only through the hidden alpha runtime boundary', () => {
|
||||
assert.equal([...card.matchAll(/await import\('\.\/iso-scene-render'\)/g)].length, 1);
|
||||
for (const module of ['iso-scene-render', 'iso-walls', 'iso-openings', 'iso-overlays']) {
|
||||
const valueImport = new RegExp(`import\\s+(?!type\\b)[^;]+from './${module}';`, 's');
|
||||
@@ -56,14 +56,14 @@ test('Stage 3 code is loaded only through the hidden alpha runtime boundary', ()
|
||||
assert.match(card, /if \(this\._labsIso\) void this\._ensureIsoSceneRuntime\(\)/);
|
||||
});
|
||||
|
||||
test('Stage 3 browser harnesses await the lazy runtime and cold alpha-off skips its chunk', () => {
|
||||
test('Stage 4 browser harnesses await the lazy runtime and cold alpha-off skips its chunk', () => {
|
||||
for (const [label, source] of [
|
||||
['contract smoke', contractSmoke],
|
||||
['touch smoke', touchSmoke],
|
||||
['golden harness', goldenHarness],
|
||||
]) {
|
||||
assert.match(source, /await card\._ensureIsoSceneRuntime\(\)/,
|
||||
`${label} does not await the Stage 3 runtime`);
|
||||
`${label} does not await the Stage 4 runtime`);
|
||||
}
|
||||
assert.match(contractSmoke,
|
||||
/cleanAlphaOffSkipsIsoRuntimeRequest = isoRuntimeRequests\(\) === 0/);
|
||||
@@ -93,7 +93,7 @@ test('every browser smoke that explicitly selects Iso waits for its lazy runtime
|
||||
'the shared helper must await load and a painted settled frame');
|
||||
});
|
||||
|
||||
test('Stage 3 composes ordered inert SVG surfaces below screen-facing HTML', () => {
|
||||
test('Stage 4 composes ordered inert SVG surfaces below screen-facing HTML', () => {
|
||||
assert.match(card, /<svg class="iso-underlay-svg"/);
|
||||
assert.match(card, /<svg class="iso-shadows-svg"/);
|
||||
assert.match(card, /<svg class="iso-walls-svg"/);
|
||||
@@ -103,8 +103,8 @@ test('Stage 3 composes ordered inert SVG surfaces below screen-facing HTML', ()
|
||||
assert.match(sceneRender, /class="iso-opening-panel iso-\$\{surface\.type\} iso-opening-\$\{surface\.kind\} iso-material-\$\{surface\.material\}"/);
|
||||
assert.doesNotMatch(sceneRender, /iso-overlay-plate|hp-iso-overlay-texture/,
|
||||
'the invisible safety footprint must never become painted SVG geometry');
|
||||
assert.match(sceneRender, /class="iso-overlay-tether"/);
|
||||
assert.match(sceneRender, /class="iso-overlay-ground"/);
|
||||
assert.doesNotMatch(sceneRender, /class="iso-overlay-tether"|class="iso-overlay-ground"/,
|
||||
'the reviewed near-floor treatment has no debug tethers or ground dots');
|
||||
assert.match(sceneRender, /const depthQueue = buildIsoWallDepthQueue\(scene\.geometry, openingSurfaces\)/);
|
||||
assert.match(sceneRender, /depthQueue\.map\(\(entry\) =>/);
|
||||
assert.match(sceneRender, /entry\.layer === 'wall-top'[\s\S]*?fill-rule="evenodd"/,
|
||||
@@ -125,13 +125,12 @@ test('Stage 3 composes ordered inert SVG surfaces below screen-facing HTML', ()
|
||||
< frameResolver.indexOf('raised: renderIsoRaisedOverlays'));
|
||||
const grounds = section(sceneRender, 'export function renderIsoOverlayGrounds',
|
||||
'export function renderIsoRaisedOverlays');
|
||||
assert.match(grounds, /renderIsoDefs\(layers, 'overlays', cellCm\)/);
|
||||
assert.match(grounds, /class="iso-overlay-ground"[\s\S]*?transform=\$\{isoFixedLightTransform\(cellCm\)\}/);
|
||||
assert.match(grounds, /return emptySvg\(\)/);
|
||||
assert.doesNotMatch(styles, /perspective\s*:|preserve-3d|rotateX\(|rotateZ\(/);
|
||||
assert.doesNotMatch(sceneRender, /window-light|iso-window-light|iso-glow|iso-sun/);
|
||||
});
|
||||
|
||||
test('Stage 3 raises only device, room and lock roots while floor/live layers stay floor-bound', () => {
|
||||
test('Stage 4 keeps device, room and lock roots on one corrected low visual plane', () => {
|
||||
for (const renderer of [
|
||||
'_renderDecorLayer(undefined, view)', '_renderRoomHoverFill(roomHover)', '_renderGlowLayer(space, disp, view)',
|
||||
'_renderSunRays(space)', '_renderOpenings(disp)',
|
||||
@@ -147,12 +146,12 @@ test('Stage 3 raises only device, room and lock roots while floor/live layers st
|
||||
assert.match(card, new RegExp(`data-hp-iso-overlay-kind=\\$\\{isoPlacement\\?\\.plane === 'raised' \\? '${kind}'`));
|
||||
});
|
||||
|
||||
test('Stage 3 structural cache fingerprints geometry/camera/heights and excludes live paint', () => {
|
||||
test('Stage 4 structural cache fingerprints geometry/camera/heights and excludes live paint', () => {
|
||||
assert.match(sceneRender, /flipH: !!opening\.flip_h/);
|
||||
assert.match(sceneRender, /flipV: !!opening\.flip_v/);
|
||||
assert.match(sceneRender, /const floorEdgeHeight = gridVisualUnits\(ISO_FLOOR_EDGE_HEIGHT, input\.cellCm\)/);
|
||||
assert.match(sceneRender, /const raisedHeight = gridVisualUnits\(ISO_RAISED_OVERLAY_HEIGHT, input\.cellCm\)/);
|
||||
assert.match(sceneRender, /camera: ISO_CAMERA,[\s\S]*?wallHeight,[\s\S]*?raisedHeight,[\s\S]*?floorEdgeHeight,[\s\S]*?algorithm: 4/);
|
||||
assert.match(sceneRender, /camera: ISO_CAMERA,[\s\S]*?wallHeight,[\s\S]*?raisedHeight,[\s\S]*?floorEdgeHeight,[\s\S]*?algorithm: 5/);
|
||||
const source = section(sceneRender, 'export function createIsoStructuralSource', 'const unknownArray');
|
||||
const roomProjection = section(sceneRender,
|
||||
'export function isoStructuralRoomGeometry', 'export type IsoStructuralOpeningHost');
|
||||
@@ -167,11 +166,14 @@ test('Stage 3 structural cache fingerprints geometry/camera/heights and excludes
|
||||
'export function isoStructuralOpeningHost', 'export interface IsoStructuralSourceInput');
|
||||
for (const field of ['hostId: resolved.host.id', 't: resolved.t', 'depth: resolved.depth',
|
||||
'face: partitionOpeningFace']) assert.ok(hostProjection.includes(field), `missing ${field}`);
|
||||
assert.match(openings, /ISO_OPENING_GEOMETRY_POLICY[\s\S]*?revision: 1/);
|
||||
assert.match(openings, /ISO_OPENING_GEOMETRY_POLICY[\s\S]*?revision: 2/);
|
||||
assert.match(source,
|
||||
/buildIsoOpeningBasis\(\{ \.\.\.opening, face \}, wallHeight, openingGeometryPolicy\)/);
|
||||
assert.match(openings, /policy\.gateTurnDeg[\s\S]*?policy\.gateTopRatio/);
|
||||
assert.match(openings, /policy\.windowBottomRatio[\s\S]*?policy\.windowTopRatio/);
|
||||
assert.match(openings, /policy\.windowSashBottomRatio[\s\S]*?policy\.windowSashTopRatio/);
|
||||
assert.match(openings, /policy\.windowGlassBottomRatio[\s\S]*?policy\.windowGlassTopRatio/);
|
||||
assert.match(openings, /policy\.doorTurnDeg/);
|
||||
assert.match(openings, /policy\.windowTurnDeg/);
|
||||
assert.match(openings, /policy\.doorTopRatio/);
|
||||
assert.match(openings, /policy\.leafThicknessRatio[\s\S]*?policy\.frameThicknessRatio/);
|
||||
assert.match(contractSmoke,
|
||||
@@ -188,7 +190,7 @@ test('Stage 3 structural cache fingerprints geometry/camera/heights and excludes
|
||||
assert.match(sceneRender, /wallSilhouettes:\s*Object\.freeze\(\[[\s\S]*?\.\.\.wallTops,[\s\S]*?\.\.\.geometry\.sides\.map\(\(face\) => \(\{ outer: face\.points \}\)\),[\s\S]*?\]\)/);
|
||||
});
|
||||
|
||||
test('show_borders:false keeps the exact +4° floor matrix and removes every volume cue', () => {
|
||||
test('show_borders:false keeps the exact zero-yaw floor matrix and removes every volume cue', () => {
|
||||
assert.match(card, /isoLayers && !isoLayers\.floorSymbols/);
|
||||
assert.match(card, /<svg class=\$\{iso \? 'plan-svg' : nothing\}[\s\S]*?data-hp-live-viewbox=\$\{iso \? 'camera' : 'floor'\}/);
|
||||
assert.match(card, /transform=\$\{iso \? isoFloorMatrixCss\(\) : nothing\}/);
|
||||
@@ -235,7 +237,7 @@ test('one frame resolves one structural source and latches late topology/project
|
||||
const resolver = section(sceneRender, 'export function resolveIsoFramePresentation', 'const emptySvg');
|
||||
for (const step of ['resolveIsoDecorationLayers', 'resolveIsoOpeningPanels', 'renderIsoUnderlay',
|
||||
'renderIsoShadows', 'renderIsoWalls', 'renderIsoOverlayGrounds', 'renderIsoRaisedOverlays'])
|
||||
assert.ok(resolver.includes(step), `late Stage 3 step escaped the frame boundary: ${step}`);
|
||||
assert.ok(resolver.includes(step), `late Stage 4 step escaped the frame boundary: ${step}`);
|
||||
});
|
||||
|
||||
test('raised footprints participate in global and room fit without entering the structural LRU', () => {
|
||||
@@ -257,8 +259,8 @@ test('raised footprints participate in global and room fit without entering the
|
||||
assert.doesNotMatch(structural, /overlayFitEntries|resolveIsoOverlayFitEnvelope|buildIsoOverlayRenderScene/);
|
||||
});
|
||||
|
||||
test('raised DOM roots expose one floor/visual identity and preserve existing actions', () => {
|
||||
assert.match(card, /data-hp-iso-stage=\$\{iso \? '3' : nothing\}/);
|
||||
test('low-plane DOM roots expose one floor/visual identity and preserve existing actions', () => {
|
||||
assert.match(card, /data-hp-iso-stage=\$\{iso \? '4' : nothing\}/);
|
||||
for (const token of [
|
||||
'data-hp-iso-overlay-kind', 'data-hp-iso-raised', 'data-hp-iso-nudged',
|
||||
'data-hp-iso-floor', 'data-hp-iso-visual',
|
||||
@@ -277,7 +279,7 @@ test('raised DOM roots expose one floor/visual identity and preserve existing ac
|
||||
assert.match(styles, /\.iso-overlays-svg,[\s\S]*?pointer-events:\s*none/);
|
||||
});
|
||||
|
||||
test('Stage 3 materials and shadows are bounded, theme-aware and capability-safe', () => {
|
||||
test('Stage 4 materials and shadows are bounded, theme-aware and capability-safe', () => {
|
||||
const materialIds = [...sceneRender.matchAll(/id="(hp-iso-[^"]+)" data-hp-iso-material-def/g)]
|
||||
.map((match) => match[1]);
|
||||
assert.ok(materialIds.length >= 5 && materialIds.length <= 12,
|
||||
@@ -287,15 +289,29 @@ test('Stage 3 materials and shadows are bounded, theme-aware and capability-safe
|
||||
assert.match(styles, /\.stage:not\(\.theme-light\) \.iso-wall-side/);
|
||||
assert.doesNotMatch(styles, /iso-overlay-plate|hp-iso-overlay-texture/,
|
||||
'themes and forced colors must not recreate a painted footprint');
|
||||
assert.match(styles, /@supports not \(filter: blur\(1px\)\)[\s\S]*?\.iso-overlay-ground[\s\S]*?display:\s*none/);
|
||||
assert.doesNotMatch(styles, /iso-overlay-ground|iso-overlay-tether/,
|
||||
'themes and capability fallbacks must not recreate removed debug cues');
|
||||
assert.match(styles, /iso-material-glass-side[\s\S]*?fill:\s*#c9e4f3/);
|
||||
assert.match(styles, /iso-material-glass-top[\s\S]*?fill:\s*#e3f2fa/);
|
||||
const rendering = section(sceneRender, 'function renderIsoDefs(', 'export function resolveIsoDecorationLayers');
|
||||
assert.match(sceneRender, /return `translate\(\$\{gridVisualUnits\(4, cellCm\)\} \$\{gridVisualUnits\(8, cellCm\)\}\)`/);
|
||||
assert.equal([...sceneRender.matchAll(/transform=\$\{isoFixedLightTransform\(cellCm\)\}/g)].length, 4,
|
||||
'ambient, contact, opening and overlay grounding shadows share one fixed-light vector');
|
||||
assert.equal([...sceneRender.matchAll(/transform=\$\{isoFixedLightTransform\(cellCm\)\}/g)].length, 3,
|
||||
'ambient, contact and opening shadows share one fixed-light vector');
|
||||
assert.doesNotMatch(rendering, /sunState|_renderSun|Date\.now|Math\.random/);
|
||||
});
|
||||
|
||||
test('Stage 3 adds no schema, dependency, storage, network or HA action surface', () => {
|
||||
test('Stage 4 room labels override Flat inline colors with the approved clean palette', () => {
|
||||
const lightLabel = styleBodiesFor('projection-iso', 'mode-view', '.roomlabel');
|
||||
const darkLabel = styleBodiesFor('projection-iso', 'theme-dark', 'mode-view', '.roomlabel');
|
||||
assert.match(lightLabel, /text-shadow:\s*none/);
|
||||
assert.match(lightLabel, /filter:\s*none/);
|
||||
assert.match(lightLabel, /-webkit-text-stroke:\s*0 transparent/);
|
||||
assert.match(lightLabel, /color:\s*#303936\s*!important/,
|
||||
'Iso palette must win over the room color written inline for Flat');
|
||||
assert.match(darkLabel, /color:\s*#f2f0e8\s*!important/);
|
||||
});
|
||||
|
||||
test('Stage 4 adds no schema, dependency, storage, network or HA action surface', () => {
|
||||
for (const source of [openings, overlays, projection, sceneRender]) {
|
||||
assert.doesNotMatch(source, /localStorage\s*\.|fetch\(|XMLHttpRequest|WebSocket|callService\(|Math\.random/);
|
||||
}
|
||||
|
||||
@@ -98,17 +98,17 @@ test('performance budget refuses incomparable runtime profiles', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('isometric reports fail closed on exact SHA, effective projection and Stage 3 metadata', () => {
|
||||
test('isometric reports fail closed on exact SHA, effective projection and current Stage 4 metadata', () => {
|
||||
const isoBudgets = { ...budgets, profile: 'isometric-stage3-dense-v1' };
|
||||
const valid = report();
|
||||
Object.assign(valid, {
|
||||
profile: isoBudgets.profile,
|
||||
effectiveProjection: ['iso'],
|
||||
isoStageRevision: ['3'],
|
||||
isoStageRevision: ['4'],
|
||||
stage3Required: true,
|
||||
});
|
||||
valid.rows.forEach((row) => Object.assign(row, {
|
||||
effectiveProjection: 'iso', isoStageRevision: '3',
|
||||
effectiveProjection: 'iso', isoStageRevision: '4',
|
||||
isoStructuralBuilds: {
|
||||
supported: true, initial: 1, beforeHaUpdate: 2, afterHaUpdate: 2, haUpdateDelta: 0,
|
||||
},
|
||||
@@ -135,7 +135,7 @@ test('isometric reports fail closed on exact SHA, effective projection and Stage
|
||||
mutate(candidate);
|
||||
assert.throws(() => evaluatePerformanceBudget({
|
||||
baseline, candidate, budgets: isoBudgets,
|
||||
}), /sourceSha|effectiveProjection|Stage 3|structural build count/);
|
||||
}), /sourceSha|effectiveProjection|Stage 4|structural build count/);
|
||||
}
|
||||
assert.throws(() => evaluatePerformanceBudget({
|
||||
baseline, candidate: valid, budgets: isoBudgets, candidateSha: 'f'.repeat(40),
|
||||
|
||||
@@ -126,7 +126,7 @@ test('#160 Stage 3 budget preserves every historical common ceiling', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('#160 Stage 3 runner fails closed on the agreed observable DOM contract', () => {
|
||||
test('#570 current Stage 4 runner fails closed on the agreed observable DOM contract', () => {
|
||||
const runner = readFileSync(new URL('../demo/benchmark_large_house.mjs', import.meta.url), 'utf8');
|
||||
for (const contract of [
|
||||
'data-hp-iso-stage',
|
||||
@@ -143,7 +143,7 @@ test('#160 Stage 3 runner fails closed on the agreed observable DOM contract', (
|
||||
'no shared texture pattern',
|
||||
'no shared shadow filter',
|
||||
'no observable',
|
||||
'Stage 3 roots',
|
||||
'Stage 4 roots',
|
||||
'opening surfaces',
|
||||
'entered Flat fallback',
|
||||
'structural build counter is absent',
|
||||
@@ -151,21 +151,21 @@ test('#160 Stage 3 runner fails closed on the agreed observable DOM contract', (
|
||||
'haUpdateDelta',
|
||||
'performed a structural rebuild for an HA-only state update',
|
||||
'requires an exact git source SHA',
|
||||
]) assert.ok(runner.includes(contract), `missing Stage 3 runner contract: ${contract}`);
|
||||
]) assert.ok(runner.includes(contract), `missing Stage 4 runner contract: ${contract}`);
|
||||
assert.match(runner,
|
||||
/\[data-hp-iso-overlay-kind\]\[data-hp-iso-floor\]\[data-hp-iso-visual\]/,
|
||||
'Stage 3 counts must use interactive roots, not duplicated inert SVG diagnostics');
|
||||
'Stage 4 counts must use interactive roots, not duplicated inert SVG diagnostics');
|
||||
assert.match(runner, /raisedVacuumCount/,
|
||||
'floor-bound vacuum must have an independent negative raised-state check');
|
||||
assert.match(runner, /stage3 !== total/,
|
||||
'every rendered device, room label and lock root must participate in Stage 3');
|
||||
'every rendered device, room label and lock root must participate in Stage 4');
|
||||
assert.match(runner, /expectedOpeningKinds\.map/,
|
||||
'door, window and gate surfaces must contribute to the measured Stage 3 scene');
|
||||
'door, window and gate surfaces must contribute to the measured Stage 4 scene');
|
||||
assert.match(runner, /pulseDeviceIdsBySpace\?\.\[card\._space\]/,
|
||||
'every switched floor must retain its own observable pulse witness');
|
||||
assert.match(runner,
|
||||
/if \(snapshot\.effectiveProjection !== 'iso'\) failures\.push\('effective projection is not iso'\);/,
|
||||
'a dense Stage 3 report must reject Flat fallback instead of timing it as success');
|
||||
'a dense Stage 4 report must reject Flat fallback instead of timing it as success');
|
||||
assert.match(runner,
|
||||
/requiresIsoStructuralBuildCounter[\s\S]*?afterStateIsoStructuralBuilds !== steadyIsoStructuralBuilds/,
|
||||
'both isometric profiles must reject a structural rebuild on the HA-only window');
|
||||
|
||||
Reference in New Issue
Block a user