mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-06 22:49:16 +00:00
fix: refine hidden 2.5D presentation (#583)
Issue: #583 User-Visible: no
This commit is contained in:
+56
-18
@@ -25,6 +25,8 @@ 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.deepEqual(basis.leaves[0].hinge, basis.face.selectedStart,
|
||||
'the derived volume pivots on the selected physical wall face, not inside masonry');
|
||||
assert.equal(basis.leaves[0].top, ISO_WALL_HEIGHT * 0.92);
|
||||
assert.equal(basis.leaves[0].turnDeg, -50);
|
||||
});
|
||||
@@ -151,7 +153,7 @@ 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(ISO_OPENING_GEOMETRY_POLICY.revision, 2);
|
||||
assert.equal(ISO_OPENING_GEOMETRY_POLICY.revision, 3);
|
||||
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]);
|
||||
@@ -180,26 +182,29 @@ test('paired window leaves open toward their resolved exterior face in every ori
|
||||
}
|
||||
});
|
||||
|
||||
test('isometric symbols keep one centre while flips change only direction', () => {
|
||||
const centred = buildIsoOpeningBasis(opening());
|
||||
test('isometric door and gate volumes follow the selected host face without changing saved axes', () => {
|
||||
const selected = buildIsoOpeningBasis(opening());
|
||||
const oppositeResolvedFace = buildIsoOpeningBasis(opening({
|
||||
face: { ox: 0, oy: -5, side: -1 },
|
||||
}));
|
||||
assert.deepEqual(oppositeResolvedFace.leaves, centred.leaves,
|
||||
'default door ignores which physical room face was resolved');
|
||||
assert.deepEqual(selected.axis, oppositeResolvedFace.axis,
|
||||
'the saved centreline axis remains canonical');
|
||||
assert.deepEqual(selected.leaves[0].hinge, selected.face.selectedStart);
|
||||
assert.deepEqual(oppositeResolvedFace.leaves[0].hinge,
|
||||
oppositeResolvedFace.face.selectedStart);
|
||||
assert.notDeepEqual(oppositeResolvedFace.leaves, selected.leaves,
|
||||
'the derived volume moves to the resolved physical face');
|
||||
|
||||
const flippedPositive = buildIsoOpeningBasis(opening({ flipV: true }));
|
||||
const flippedNegative = buildIsoOpeningBasis(opening({
|
||||
flipV: true, face: { ox: 0, oy: -5, side: -1 },
|
||||
}));
|
||||
assert.deepEqual(flippedNegative.leaves, flippedPositive.leaves,
|
||||
'resolved room face cannot translate or redirect the saved flip');
|
||||
assert.deepEqual(flippedPositive.leaves[0].hinge, centred.leaves[0].hinge,
|
||||
'flip keeps the exact centreline hinge');
|
||||
assert.deepEqual(flippedPositive.leaves[0].closedVector, centred.leaves[0].closedVector);
|
||||
assert.deepEqual(flippedPositive.leaves[0].hinge, flippedPositive.face.selectedStart);
|
||||
assert.deepEqual(flippedNegative.leaves[0].hinge, flippedNegative.face.selectedStart);
|
||||
assert.deepEqual(flippedPositive.leaves[0].closedVector, selected.leaves[0].closedVector);
|
||||
assert.equal(
|
||||
flippedPositive.leaves[0].quarterVector[1],
|
||||
-centred.leaves[0].quarterVector[1],
|
||||
-selected.leaves[0].quarterVector[1],
|
||||
'flip mirrors the opening direction without moving its origin',
|
||||
);
|
||||
|
||||
@@ -207,17 +212,50 @@ test('isometric symbols keep one centre while flips change only direction', () =
|
||||
const gateFlipped = buildIsoOpeningBasis(opening({
|
||||
type: 'gate', flipV: true, face: { ox: 0, oy: -5, side: -1 },
|
||||
}));
|
||||
assert.equal(gate.leaves[0].hinge[1], 80);
|
||||
assert.equal(gateFlipped.leaves[0].hinge[1], 80);
|
||||
assert.equal(gate.leaves[0].hinge[1], 85);
|
||||
assert.equal(gateFlipped.leaves[0].hinge[1], 75);
|
||||
assert.deepEqual(gate.leaves.map((leaf) => leaf.turnDeg), [10, -10]);
|
||||
assert.deepEqual(gateFlipped.leaves.map((leaf) => leaf.turnDeg), [-10, 10]);
|
||||
assert.notDeepEqual(
|
||||
projectIsoOpening(gate, 1).map((panel) => panel.d),
|
||||
projectIsoOpening(gateFlipped, 1).map((panel) => panel.d),
|
||||
'flip changes the gate turn without translating its centred origin',
|
||||
'flip changes both the selected face and outward turn',
|
||||
);
|
||||
});
|
||||
|
||||
test('door and gate face matrix keeps every live state on the selected physical hinge', () => {
|
||||
for (const type of ['door', 'gate']) {
|
||||
for (const angle of [0, 90, 37]) {
|
||||
const radians = angle * Math.PI / 180;
|
||||
const normal = [-Math.sin(radians), Math.cos(radians)];
|
||||
for (const side of [-1, 1]) {
|
||||
for (const flipH of [false, true]) {
|
||||
const basis = buildIsoOpeningBasis(opening({
|
||||
type, angle, flipH,
|
||||
face: { ox: normal[0] * 5 * side, oy: normal[1] * 5 * side, side },
|
||||
}));
|
||||
for (const leaf of basis.leaves) {
|
||||
const expected = flipH
|
||||
? leaf.leaf === 0 ? basis.face.selectedEnd : basis.face.selectedStart
|
||||
: leaf.leaf === 0 ? basis.face.selectedStart : basis.face.selectedEnd;
|
||||
assert.deepEqual(leaf.hinge, expected,
|
||||
`${type} angle=${angle} side=${side} flipH=${flipH} leaf=${leaf.leaf}`);
|
||||
}
|
||||
for (const amount of [0, 0.5, 1]) {
|
||||
const panels = projectIsoOpening(basis, amount);
|
||||
assert.equal(panels.length, basis.leaves.length);
|
||||
assert.ok(panels.every((panel) => panel.surfaces.length === 5
|
||||
&& panel.surfaces.every((surface) => Number.isFinite(surface.depth)
|
||||
&& Number.isFinite(surface.cameraDepth)
|
||||
&& !/NaN|Infinity/.test(surface.d))),
|
||||
`${type} angle=${angle} side=${side} flipH=${flipH} amount=${amount}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('passage keeps the wall cut but never creates an isometric panel', () => {
|
||||
const passageBasis = buildIsoOpeningBasis(opening({ type: 'passage' }));
|
||||
assert.deepEqual(passageBasis.leaves, []);
|
||||
@@ -257,16 +295,16 @@ test('flipH and flipV independently mirror their exact structural axes', () => {
|
||||
};
|
||||
};
|
||||
assert.deepEqual(signature(normal), {
|
||||
hinge: [70, 80], closedVector: [60, 0], quarterVector: [0, 60],
|
||||
hinge: [70, 85], closedVector: [60, 0], quarterVector: [0, 60],
|
||||
});
|
||||
assert.deepEqual(signature(horizontal), {
|
||||
hinge: [130, 80], closedVector: [-60, 0], quarterVector: [0, 60],
|
||||
hinge: [130, 85], closedVector: [-60, 0], quarterVector: [0, 60],
|
||||
});
|
||||
assert.deepEqual(signature(vertical), {
|
||||
hinge: [70, 80], closedVector: [60, 0], quarterVector: [0, -60],
|
||||
hinge: [70, 85], closedVector: [60, 0], quarterVector: [0, -60],
|
||||
});
|
||||
assert.deepEqual(signature(both), {
|
||||
hinge: [130, 80], closedVector: [-60, 0], quarterVector: [0, -60],
|
||||
hinge: [130, 85], closedVector: [-60, 0], quarterVector: [0, -60],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ import {
|
||||
ISO_OVERLAY_MAX_NUDGE_CSS_PX,
|
||||
ISO_OVERLAY_SAFETY_GAP_CSS_PX,
|
||||
buildIsoFootprintPolygon,
|
||||
isoOverlayCollisionKey,
|
||||
isoOverlayPlane,
|
||||
isoRoomSafePoint,
|
||||
resolveIsoOverlayCollisions,
|
||||
resolveIsoOverlayOwner,
|
||||
resolveIsoOverlayPlacement,
|
||||
} from '../test-build/iso-overlays.js';
|
||||
@@ -53,6 +55,72 @@ test('the exact Stage 4 overlay matrix keeps only the three interactive roots on
|
||||
assert.equal(isoOverlayPlane(kind, false), 'floor', `${kind} without borders`);
|
||||
});
|
||||
|
||||
test('group collision separates a solvable dense set independently of input order', () => {
|
||||
const make = (id) => ({
|
||||
id,
|
||||
kind: id === 'lock' ? 'opening-lock' : 'device',
|
||||
placement: placement({
|
||||
floorAnchor: [100, 100],
|
||||
rooms: [square('room', 0, 0, 200, 200, [100, 100])],
|
||||
visualOffset: 0,
|
||||
camera: identityCamera,
|
||||
}),
|
||||
screenHalfSize: [8, 8],
|
||||
});
|
||||
const solve = (items) => resolveIsoOverlayCollisions({
|
||||
items,
|
||||
rooms: [square('room', 0, 0, 200, 200, [100, 100])],
|
||||
wallSilhouettes: [],
|
||||
sceneUnitsPerCssPixel: 1,
|
||||
visualOffset: 0,
|
||||
camera: identityCamera,
|
||||
});
|
||||
const normal = solve([make('a'), make('b'), make('lock')]);
|
||||
const reversed = solve([make('lock'), make('b'), make('a')]);
|
||||
assert.deepEqual(normal.residualPairs, []);
|
||||
assert.deepEqual(reversed.residualPairs, []);
|
||||
const keys = [
|
||||
isoOverlayCollisionKey('device', 'a'),
|
||||
isoOverlayCollisionKey('device', 'b'),
|
||||
isoOverlayCollisionKey('opening-lock', 'lock'),
|
||||
];
|
||||
const centers = keys.map((key) => normal.placements.get(key).visualScene);
|
||||
for (let index = 0; index < centers.length; index++) {
|
||||
assert.deepEqual(reversed.placements.get(keys[index]).visualScene, centers[index]);
|
||||
assert.ok(normal.placements.get(keys[index]).nudgeDistanceCss <= 48);
|
||||
for (let other = 0; other < index; other++) {
|
||||
assert.ok(Math.abs(centers[index][0] - centers[other][0]) >= 20
|
||||
|| Math.abs(centers[index][1] - centers[other][1]) >= 20,
|
||||
`pair ${index}/${other} must clear its complete roots plus the 4px gap`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(normal.placements.get(keys[0]).visualScene, [100, 100],
|
||||
'the stable first item stays at its zero-deviation anchor');
|
||||
});
|
||||
|
||||
test('group collision reports a deterministic residual without exceeding the absolute cap', () => {
|
||||
const room = square('tight', 0, 0, 1, 1, [0.5, 0.5]);
|
||||
const make = (id) => ({
|
||||
id,
|
||||
kind: 'device',
|
||||
placement: placement({
|
||||
floorAnchor: [0.5, 0.5], rooms: [room], preferredRoomId: 'tight',
|
||||
visualOffset: 0, camera: identityCamera,
|
||||
}),
|
||||
screenHalfSize: [30, 30],
|
||||
});
|
||||
const result = resolveIsoOverlayCollisions({
|
||||
items: [make('a'), make('b')], rooms: [room], wallSilhouettes: [],
|
||||
sceneUnitsPerCssPixel: 1, visualOffset: 0, camera: identityCamera,
|
||||
});
|
||||
const second = result.placements.get(isoOverlayCollisionKey('device', 'b'));
|
||||
assert.equal(result.residualPairs.length, 1);
|
||||
assert.equal(second.status, 'degraded');
|
||||
assert.equal(second.reason, 'overlay-collision');
|
||||
assert.ok(second.nudgeDistanceCss <= 48,
|
||||
'the search radius is total displacement, not a budget added per collision');
|
||||
});
|
||||
|
||||
test('owner resolution honours bindings, then canonical minimum area and stable id', () => {
|
||||
const rooms = [
|
||||
square('wide', 0, 0, 100, 100, [50, 50]),
|
||||
|
||||
@@ -239,6 +239,33 @@ test('shared painter queue puts elevated window glass over its rear sill only in
|
||||
wallSlots, 'window-local occlusion cannot move unrelated wall slots');
|
||||
});
|
||||
|
||||
test('shared painter queue keeps rotating door prism faces in physical camera order', () => {
|
||||
const geometry = buildIsoWallGeometry([[[
|
||||
[0, 100], [100, 100], [100, 200], [0, 200], [0, 100],
|
||||
]]]);
|
||||
const basis = buildIsoOpeningBasis({
|
||||
id: 'door-depth', sourceIndex: 3, type: 'door', x: 50, y: 100,
|
||||
angle: 0, length: 45, flipH: false, flipV: false,
|
||||
face: { ox: 0, oy: -5, cm: 20, side: -1 },
|
||||
});
|
||||
const surfaces = [
|
||||
...projectIsoOpeningStructure(basis),
|
||||
...projectIsoOpening(basis, 0.5).flatMap((panel) => panel.surfaces),
|
||||
].map((surface, index) => ({
|
||||
...surface, id: basis.id, sourceIndex: basis.sourceIndex,
|
||||
type: 'door', leaf: index,
|
||||
}));
|
||||
const queue = buildIsoWallDepthQueue(geometry, [...surfaces].reverse());
|
||||
const doorEntries = queue.filter((entry) => entry.layer === 'opening');
|
||||
assert.deepEqual(doorEntries.map((entry) => entry.surface.cameraDepth),
|
||||
doorEntries.map((entry) => entry.surface.cameraDepth).toSorted((a, b) => a - b),
|
||||
'all faces of one live door reuse its queue slots in physical camera-depth order');
|
||||
const wallSlots = queue.flatMap((entry, index) => entry.layer === 'opening' ? [] : [index]);
|
||||
const natural = buildIsoWallDepthQueue(geometry, surfaces);
|
||||
assert.deepEqual(natural.flatMap((entry, index) => entry.layer === 'opening' ? [] : [index]),
|
||||
wallSlots, 'door-local ordering 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);
|
||||
@@ -439,6 +466,66 @@ test('Stage 4 reuses pure overlay placements and fit probes skip collision searc
|
||||
'view scale invalidates the placement signature');
|
||||
});
|
||||
|
||||
test('render scene separates device roots without moving labels and caches permutations', () => {
|
||||
const owner = {
|
||||
...room('owner', 0, 0, 400, 400),
|
||||
name: 'Owner label', area: 'living', settings: {},
|
||||
};
|
||||
const space = {
|
||||
id: 'floor', title: 'Floor', cellCm: 5, vb: [0, 0, 400, 400], bg: null,
|
||||
rooms: [owner], wall_segments: [], room_drafts: [], partitions: [], wall_columns: [],
|
||||
};
|
||||
const walls = [];
|
||||
const devices = ['b', 'a', 'c'].map((id) => ({
|
||||
id, space: 'floor', marker: { room_id: 'owner', x: 200, y: 200 },
|
||||
}));
|
||||
const input = {
|
||||
space, devices, openings: [],
|
||||
view: { x: 0, y: 0, w: 400, h: 400 },
|
||||
display: { showNames: true, cardFontScale: 1 },
|
||||
layers: { structural: true, shadows: true },
|
||||
wallSilhouettes: walls,
|
||||
iconPct: 3.4, deviceBasePct: 3.4, showLqi: false, cellCm: 5,
|
||||
kioskIconScale: 1, kioskFontScale: 1,
|
||||
stageSize: { width: 200, height: 200 },
|
||||
positionOf: (device) => ({ x: device.marker.x, y: device.marker.y }),
|
||||
presentationOf: () => ({
|
||||
scale: 1, valueText: null, valueFullText: '', valueBadge: null,
|
||||
tempText: null, humText: null, lqiText: null,
|
||||
pulse: { animated: false, diameterScale: 1 },
|
||||
}),
|
||||
labelPositionOf: () => ({ x: 200, y: 200 }),
|
||||
labelScaleOf: () => 1,
|
||||
openingEntityAvailable: () => false,
|
||||
openingWallIndex: () => ({ adjacencyEps: 0.1, edges: [] }),
|
||||
};
|
||||
const scene = buildIsoOverlayRenderScene(input);
|
||||
assert.deepEqual(scene.residualPairs, [], 'the roomy fixture must fully separate all roots');
|
||||
const deviceEntries = scene.entries.filter((entry) => entry.kind === 'device');
|
||||
for (let index = 0; index < deviceEntries.length; index++) {
|
||||
assert.ok(deviceEntries[index].placement.nudgeDistanceCss <= 48);
|
||||
for (let other = 0; other < index; other++) {
|
||||
const a = deviceEntries[index], b = deviceEntries[other];
|
||||
const dx = Math.abs(a.placement.visualScene[0] - b.placement.visualScene[0]);
|
||||
const dy = Math.abs(a.placement.visualScene[1] - b.placement.visualScene[1]);
|
||||
assert.ok(dx >= a.screenHalfSize[0] + b.screenHalfSize[0] + 8 - 1e-7
|
||||
|| dy >= a.screenHalfSize[1] + b.screenHalfSize[1] + 8 - 1e-7,
|
||||
`device roots ${a.id}/${b.id} must not overlap after the 4 CSS px gap`);
|
||||
}
|
||||
}
|
||||
const label = scene.entries.find((entry) => entry.kind === 'room-label');
|
||||
assert.ok(label, 'fixture includes a room label at the same projected anchor');
|
||||
assert.equal(label.placement.nudgeDistanceCss, 0,
|
||||
'device collisions do not push the room label');
|
||||
|
||||
const permuted = buildIsoOverlayRenderScene({ ...input, devices: [...devices].reverse() });
|
||||
assert.strictEqual(permuted, scene,
|
||||
'HA registry permutations reuse the same immutable group layout snapshot');
|
||||
const fit = buildIsoOverlayRenderScene({ ...input, resolveCollisions: false });
|
||||
assert.deepEqual(fit.devices.get('a').visualScene, fit.devices.get('b').visualScene,
|
||||
'fit probing deliberately skips live group displacement');
|
||||
});
|
||||
|
||||
test('visible wall side quads participate in overlay collision', () => {
|
||||
const walls = [[[[45, 20], [55, 20], [55, 80], [45, 80]]]];
|
||||
const scene = resolveIsoScene({
|
||||
@@ -615,7 +702,7 @@ test('throwing decoration capability probes keep Iso structural geometry on the
|
||||
}
|
||||
});
|
||||
|
||||
test('shadow presentation failure retries the same Iso frame as solid geometry', () => {
|
||||
test('removed contact shadows are never read while ambient shadow capability remains enabled', () => {
|
||||
const previousCss = globalThis.CSS;
|
||||
const previousMatchMedia = globalThis.matchMedia;
|
||||
try {
|
||||
@@ -639,11 +726,13 @@ test('shadow presentation failure retries the same Iso frame as solid geometry',
|
||||
},
|
||||
openings: [], amountOf: () => 0, overlays: () => null, cellCm: 5,
|
||||
});
|
||||
assert.equal(contactReads, 1);
|
||||
assert.equal(contactReads, 0,
|
||||
'the deprecated contact path must not be touched by presentation rendering');
|
||||
assert.equal(frame.layers.structural, true);
|
||||
assert.equal(frame.layers.panels, true);
|
||||
assert.equal(frame.layers.shadows, false);
|
||||
assert.equal(frame.layers.materialNuance, false);
|
||||
assert.equal(frame.layers.shadows, true,
|
||||
'the remaining building ambient shadow still follows filter capability');
|
||||
assert.equal(frame.layers.materialNuance, true);
|
||||
assert.equal(frame.overlays, null);
|
||||
} finally {
|
||||
if (previousCss === undefined) delete globalThis.CSS;
|
||||
|
||||
@@ -166,7 +166,10 @@ test('Stage 4 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: 2/);
|
||||
assert.match(openings, /ISO_OPENING_GEOMETRY_POLICY[\s\S]*?revision: 3/);
|
||||
assert.match(openings,
|
||||
/input\.type === 'door' \|\| input\.type === 'gate'[\s\S]*?\? input\.face[\s\S]*?: openingSymbolOffset/,
|
||||
'door/gate volume uses the selected physical face while Flat/window geometry stays canonical');
|
||||
assert.match(source,
|
||||
/buildIsoOpeningBasis\(\{ \.\.\.opening, face \}, wallHeight, openingGeometryPolicy\)/);
|
||||
assert.match(openings, /policy\.gateTurnDeg[\s\S]*?policy\.gateTopRatio/);
|
||||
@@ -279,7 +282,7 @@ test('low-plane DOM roots expose one floor/visual identity and preserve existing
|
||||
assert.match(styles, /\.iso-overlays-svg,[\s\S]*?pointer-events:\s*none/);
|
||||
});
|
||||
|
||||
test('Stage 4 materials and shadows are bounded, theme-aware and capability-safe', () => {
|
||||
test('Stage 4 materials and ambient shadow 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,
|
||||
@@ -295,8 +298,18 @@ test('Stage 4 materials and shadows are bounded, theme-aware and capability-safe
|
||||
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, 3,
|
||||
'ambient, contact and opening shadows share one fixed-light vector');
|
||||
assert.equal([...sceneRender.matchAll(/transform=\$\{isoFixedLightTransform\(cellCm\)\}/g)].length, 1,
|
||||
'only the approved building ambient shadow uses the fixed-light vector');
|
||||
assert.doesNotMatch(sceneRender, /hp-iso-contact-shadow|hp-iso-leaf-shadow|iso-contact-shadow|iso-leaf-shadow/);
|
||||
assert.doesNotMatch(styles, /\.iso-contact-shadow|\.iso-leaf-shadow/);
|
||||
assert.match(styles, /\.iso-opening-panel\.iso-material-matte-leaf\s*\{[\s\S]*?stroke:\s*none/);
|
||||
assert.match(styles, /\.stage\.theme-dark \.iso-opening-panel\.iso-material-matte-leaf\s*\{[^}]*stroke:\s*none/);
|
||||
assert.match(styles, /\.stage:not\(\.theme-light\) \.iso-opening-panel\.iso-material-matte-leaf\s*\{[^}]*stroke:\s*none/);
|
||||
assert.match(styles, /iso-material-glass-side[\s\S]*?stroke:\s*#8fb4c7/,
|
||||
'window glass keeps its thin border');
|
||||
assert.match(styles,
|
||||
/\.stage\.projection-iso\.mode-view \.dev,[\s\S]*?\.stage\.projection-iso\.mode-view \.oplock\s*\{\s*z-index:\s*2/,
|
||||
'interactive overlays remain above room labels');
|
||||
assert.doesNotMatch(rendering, /sunState|_renderSun|Date\.now|Math\.random/);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user