feat: refine hidden isometric Stage 4 (#570)

Issue: #570
User-Visible: no
This commit is contained in:
Sergey Matyunin
2026-09-14 15:29:57 +03:00
parent 807d9917c0
commit 258896cb6f
74 changed files with 2588 additions and 2494 deletions
+14 -12
View File
@@ -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
View File
@@ -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
View File
@@ -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', () => {
+12 -12
View File
@@ -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,
}));
+51 -15
View File
@@ -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],
+38 -22
View File
@@ -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/);
}
+4 -4
View File
@@ -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),
+7 -7
View File
@@ -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');