mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-06 22:49:16 +00:00
perf(led): avoid redundant tube and barrier calculations
Preserve exact light fields while culling provably distant occluders, reusing physical tube paths across camera updates and excluding LED emitter data from aligned architectural fingerprints. Cover output equivalence, in-place invalidation, resize tags and hyphenated benchmark chunk hashes. Issue: #788 User-Visible: yes
This commit is contained in:
@@ -2,16 +2,108 @@ import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
createGlowRuntimeState, disposeGlowRuntime, readGlowClip,
|
||||
buildLightBarrierScene, createGlowRuntimeState, disposeGlowRuntime,
|
||||
lightGeometryFingerprint, readGlowClip,
|
||||
resolveGlowCandidates, resolveLightBarrierRevision,
|
||||
transitionGlowSource, writeGlowClip,
|
||||
} from '../test-build/glow-scene.js';
|
||||
import { contentFingerprint } from '../test-build/visual-continuity.js';
|
||||
import { wallBodiesGeometry, wallKey } from '../test-build/wall-thickness.js';
|
||||
|
||||
const square = (id, x0, x1) => ({
|
||||
id,
|
||||
poly: [[x0, 0], [x1, 0], [x1, 100], [x0, 100]],
|
||||
});
|
||||
|
||||
test('light geometry excludes only LED sources and never visits or mutates their data', () => {
|
||||
const geometry = Object.freeze({
|
||||
id: 's', rooms: Object.freeze([square('room', 0, 100)]),
|
||||
walls: [], wall_segments: [], openings: [], open_spans: [],
|
||||
partitions: [], wall_columns: [], wall_style: 'zero',
|
||||
future_geometry_key: { x: 1 },
|
||||
});
|
||||
const expected = contentFingerprint([geometry, 5, 20]);
|
||||
assert.equal(lightGeometryFingerprint(geometry, 5, 20), expected);
|
||||
const raw = Object.freeze(Object.defineProperty({ ...geometry }, 'led_strips', {
|
||||
enumerable: true,
|
||||
get() { throw new Error('LED source data must not enter a masonry hash'); },
|
||||
}));
|
||||
assert.equal(lightGeometryFingerprint(raw, 5, 20), expected);
|
||||
assert.equal(raw.rooms, geometry.rooms, 'the caller-owned geometry is not cloned or mutated');
|
||||
for (const led_strips of [[], [{ id: 'a', points: [[0, 0], [100, 1]] }],
|
||||
[{ id: 'b', active: false, entity: 'light.changed', points: [[0, 1]] }]]) {
|
||||
assert.equal(lightGeometryFingerprint({ ...geometry, led_strips }, 5, 20), expected);
|
||||
}
|
||||
for (const key of Object.keys(geometry)) {
|
||||
assert.notEqual(lightGeometryFingerprint({ ...geometry, [key]: ['changed'] }, 5, 20), expected,
|
||||
`all non-LED raw keys still invalidate, including ${key}`);
|
||||
}
|
||||
assert.notEqual(lightGeometryFingerprint(geometry, 10, 20), expected);
|
||||
assert.notEqual(lightGeometryFingerprint(geometry, 5, 40), expected);
|
||||
for (const invalid of [null, undefined, false, 0, 's', [], [{ led_strips: [1] }]]) {
|
||||
assert.equal(lightGeometryFingerprint(invalid, 5, 20), contentFingerprint([invalid, 5, 20]),
|
||||
'non-object and array inputs keep their prior fingerprint semantics');
|
||||
}
|
||||
const mutable = { rooms: [square('room', 0, 100)], led_strips: [] };
|
||||
const before = lightGeometryFingerprint(mutable, 5, 20);
|
||||
mutable.rooms[0].poly[0][0] = 1;
|
||||
assert.notEqual(lightGeometryFingerprint(mutable, 5, 20), before,
|
||||
'in-place architectural edits must not reuse stale masonry');
|
||||
});
|
||||
|
||||
test('LED-only edits retain the aligned shared-masonry recut fast path', () => {
|
||||
const space = {
|
||||
id: 's', rooms: [square('left', 0, 100), square('right', 100, 200)],
|
||||
partitions: [], room_drafts: [], wall_columns: [],
|
||||
};
|
||||
const unique = new Map();
|
||||
for (const room of space.rooms) for (let index = 0; index < room.poly.length; index++) {
|
||||
const a = room.poly[index], b = room.poly[(index + 1) % room.poly.length];
|
||||
const key = wallKey(a, b, 1);
|
||||
if (!unique.has(key)) unique.set(key, { key, a, b, cm: 20 });
|
||||
}
|
||||
const walls = [...unique.values()];
|
||||
const raw = { ...space, walls, led_strips: [{ id: 'led', points: [[0, 0], [100, 0]] }] };
|
||||
const sharedWallGeometry = wallBodiesGeometry(space.rooms, walls, [], [], 1, 5, 5, 1, []);
|
||||
assert.equal(sharedWallGeometry.status, 'ok');
|
||||
Object.defineProperty(sharedWallGeometry, 'sourceFingerprint', {
|
||||
value: lightGeometryFingerprint(raw, 5, 5), enumerable: false,
|
||||
});
|
||||
const revisionFor = (rawSpaceConfig) => resolveLightBarrierRevision({
|
||||
rawSpaceConfig, space, openings: [], cellCm: 5, gridPitch: 5, openingAmount: () => 0,
|
||||
});
|
||||
const revision = revisionFor(raw);
|
||||
raw.led_strips[0].points[1][1] = 90;
|
||||
const edited = revisionFor(raw);
|
||||
assert.equal(edited.geometryFingerprint, sharedWallGeometry.sourceFingerprint);
|
||||
assert.equal(edited.fingerprint, revision.fingerprint);
|
||||
|
||||
let wallIterations = 0;
|
||||
Object.defineProperty(walls, Symbol.iterator, { value() {
|
||||
wallIterations++;
|
||||
return Array.prototype[Symbol.iterator].call(this);
|
||||
} });
|
||||
const input = {
|
||||
space, revision: edited, walls,
|
||||
zeroWalls: { contour: [], barriers: [], transmissive: [] },
|
||||
wallKeyPitch: 1, cellCm: 5, gridPitch: 5, coordScale: 1,
|
||||
physicalBodies: () => [],
|
||||
};
|
||||
const reused = buildLightBarrierScene({ ...input, sharedWallGeometry });
|
||||
assert.equal(wallIterations, 1, 'matching source tag avoids the second wall rebuild traversal');
|
||||
assert.ok(reused.masonryGeometry.length);
|
||||
wallIterations = 0;
|
||||
const rebuilt = buildLightBarrierScene(input);
|
||||
assert.equal(wallIterations, 2, 'a scene without shared geometry must rebuild masonry');
|
||||
assert.deepEqual(reused, rebuilt, 'recut and uncached rebuild retain identical light geometry');
|
||||
const changedRevision = revisionFor({ ...raw, wall_style: 'changed' });
|
||||
wallIterations = 0;
|
||||
buildLightBarrierScene({
|
||||
...input, revision: changedRevision, sharedWallGeometry,
|
||||
});
|
||||
assert.equal(wallIterations, 2, 'architectural changes reject the old shared-masonry tag');
|
||||
});
|
||||
|
||||
test('shared light revision admits only floor-to-floor architectural passages', () => {
|
||||
const space = {
|
||||
id: 's', rooms: [square('left', 0, 100), square('right', 100, 200)],
|
||||
|
||||
@@ -1044,7 +1044,7 @@ test('a light source paints exactly one region: the floor it can see', () => {
|
||||
// edited in place, and a stale barrier set lights straight through a wall.
|
||||
assert.doesNotMatch(glow, /_cfgEpoch/);
|
||||
assert.match(glow,
|
||||
/const geometryFingerprint = contentFingerprint\(\[\s*input\.rawSpaceConfig, input\.cellCm, input\.gridPitch/);
|
||||
/const geometryFingerprint = lightGeometryFingerprint\(\s*input\.rawSpaceConfig, input\.cellCm, input\.gridPitch/);
|
||||
assert.match(glow,
|
||||
/fingerprint: contentFingerprint\(\[geometryFingerprint, openingStateSignature\]\)/);
|
||||
assert.match(glow,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { ledChunkRequestName } from '../demo/performance/led-chunk-request.mjs';
|
||||
|
||||
test('#788: LED request accounting recognises names independently of hash delimiters', () => {
|
||||
for (const kind of ['runtime', 'field', 'editor']) {
|
||||
for (const hash of ['abc123', '-abc123', 'abc-def', 'abc_def', 'a-b_c-d']) {
|
||||
const name = `led-strip-${kind}-${hash}.js`;
|
||||
for (const url of [name, `http://localhost:8123/dist/${name}`,
|
||||
`http://localhost:8123/dist/${name}?retry=1`, `http://localhost:8123/dist/${name}?from=/path`,
|
||||
`http://localhost:8123/dist/${name}#module`]) {
|
||||
assert.equal(ledChunkRequestName(url), `led-strip-${kind}`, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('#788: LED request accounting ignores non-chunks without hiding editor loads', () => {
|
||||
for (const name of ['led-strip-geometry-abc.js', 'prefix-led-strip-field-abc.js',
|
||||
'led-strip-field-abc.js.map', 'led-strip-field-abc.css', 'led-strip-field-.js',
|
||||
'led-strip-field.js', 'houseplan-card.js']) {
|
||||
assert.equal(ledChunkRequestName(`http://localhost:8123/dist/${name}`), null, name);
|
||||
}
|
||||
assert.equal(ledChunkRequestName('http://localhost:8123/dist/led-strip-editor-a-b_c.js'), 'led-strip-editor');
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
// #788: camera renders reuse tube geometry without freezing live state or edits.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { ledFrameFor, ledStripePath, releaseLed } from '../test-build/led-strip-runtime.js';
|
||||
import { LED_THICKNESS_D, pathD, visibleStripPath } from '../test-build/led-strip-geometry.js';
|
||||
|
||||
const strip = () => ({ id: 'led', marker: 'lamp', points: [[2, 1], [6, 1], [6, 4]] });
|
||||
const wallFaces = () => ({
|
||||
faces: [{ a: [0, 1], b: [10, 1] }],
|
||||
inside: ([x, y]) => x > 0 && x < 10 && y < 1,
|
||||
epsilon: 1e-5,
|
||||
});
|
||||
const fresh = (shape, faces, diameter) => {
|
||||
const t = LED_THICKNESS_D * diameter;
|
||||
const path = visibleStripPath(shape.points, faces, t / 2);
|
||||
return { t, path, d: pathD(path) };
|
||||
};
|
||||
|
||||
test('#788: repeated camera reads skip tube classification and reuse identical SVG geometry', () => {
|
||||
const shape = strip(), faces = wallFaces();
|
||||
let classifications = 0;
|
||||
faces.near = () => { classifications++; return faces.faces; };
|
||||
const cached = ledStripePath(shape, faces, 4);
|
||||
assert.equal(classifications, 1);
|
||||
for (let step = 0; step < 100; step++) assert.equal(ledStripePath(shape, faces, 4), cached);
|
||||
assert.equal(classifications, 1, 'camera reads must not rerun face classification');
|
||||
assert.deepEqual(cached, fresh(shape, faces, 4), 'cached outline and hit geometry equal the uncached derivation');
|
||||
});
|
||||
|
||||
test('#788: face context, epsilon, diameter and exact point edits invalidate the tube cache', () => {
|
||||
const shape = strip();
|
||||
let faces = wallFaces(), diameter = 4;
|
||||
let previous = ledStripePath(shape, faces, diameter);
|
||||
const changed = () => {
|
||||
const value = ledStripePath(shape, faces, diameter);
|
||||
assert.notEqual(value, previous);
|
||||
assert.deepEqual(value, fresh(shape, faces, diameter));
|
||||
previous = value;
|
||||
};
|
||||
diameter = 8;
|
||||
changed();
|
||||
faces = null;
|
||||
changed();
|
||||
faces = wallFaces();
|
||||
changed();
|
||||
faces.epsilon *= 2;
|
||||
changed();
|
||||
shape.points[1][0] = 7;
|
||||
changed();
|
||||
shape.points = [[1, 2], [8, 2], [8, 5]];
|
||||
changed();
|
||||
shape.points.push([9, 6]);
|
||||
changed();
|
||||
const replacement = { ...shape, points: shape.points.map((point) => [...point]) };
|
||||
assert.notEqual(ledStripePath(replacement, faces, diameter), previous, 'same id in a new frame owns a new weak cache key');
|
||||
});
|
||||
|
||||
const ownerFixture = () => ({
|
||||
isConnected: true,
|
||||
_renderDevices: [{ id: 'lamp', name: 'Lamp', primary: 'light.led', entities: ['light.led'],
|
||||
space: 's', marker: { id: 'lamp', binding: 'device:lamp' } }],
|
||||
_renderPlanHass: { states: { 'light.led': { state: 'on', attributes: {} } } },
|
||||
_fillColors: { glow_light: { c: '#ffd27b', a: 0.7 } },
|
||||
_cellCm: 5, _gridPitch: 1000 / 240, _config: { icon_size: 3.4 },
|
||||
_mode: 'view', _showAll: false,
|
||||
_pointInRoom: () => false,
|
||||
});
|
||||
const spaceFixture = () => ({ id: 's', vb: [0, 0, 1000, 1000], rooms: [],
|
||||
led_strips: [{ id: 'led', marker: 'lamp', points: [[0.1, 0.1], [0.4, 0.1]] }] });
|
||||
|
||||
test('#788: new HA frames keep live state and evict the old frame geometry; disconnect releases it', () => {
|
||||
const owner = ownerFixture(), space = spaceFixture();
|
||||
const first = ledFrameFor(owner, space, true);
|
||||
const firstStrip = first.views[0].strip;
|
||||
const firstPath = ledStripePath(firstStrip, first.faces, first.d);
|
||||
assert.equal(ledFrameFor(owner, space, true), first, 'unchanged camera inputs reuse the frame');
|
||||
assert.equal(ledStripePath(firstStrip, first.faces, first.d), firstPath);
|
||||
owner._renderPlanHass = { states: { 'light.led': { state: 'off', attributes: {} } } };
|
||||
const second = ledFrameFor(owner, space, true);
|
||||
assert.notEqual(second, first);
|
||||
assert.equal(second.views[0].state, 'off', 'geometry caching must not freeze the device state');
|
||||
assert.notEqual(ledStripePath(firstStrip, first.faces, first.d), firstPath, 'old frame entry was explicitly evicted');
|
||||
const secondStrip = second.views[0].strip;
|
||||
const secondPath = ledStripePath(secondStrip, second.faces, second.d);
|
||||
assert.deepEqual(secondPath, firstPath, 'state changes preserve the physical tube');
|
||||
releaseLed(owner);
|
||||
assert.notEqual(ledStripePath(secondStrip, second.faces, second.d), secondPath, 'disconnect explicitly evicts current frame entries');
|
||||
});
|
||||
@@ -6,7 +6,8 @@ import { ledAnchor, ledStripsByMarker } from '../test-build/led-strip-gate.js';
|
||||
import { faceContext, ledFrame, ledStripView, stripRoom } from '../test-build/led-strip-runtime.js';
|
||||
import { LED_FIELD_BANDS, LedFieldCache, buildFieldGeometry, falloffAt } from '../test-build/led-strip-field.js';
|
||||
import { GLOW_FALLOFF } from '../test-build/glow-scene.js';
|
||||
import { stripAnchor } from '../test-build/led-strip-geometry.js';
|
||||
import { compactPoints, emitterSamples, stripAnchor } from '../test-build/led-strip-geometry.js';
|
||||
import { visibilityPolygon } from '../test-build/light-visibility.js';
|
||||
|
||||
const space = (strips) => ({ id: 's', rooms: [], led_strips: strips });
|
||||
|
||||
@@ -242,6 +243,114 @@ test('#788: a wall crossing the radius contributes exact circle-intersection eve
|
||||
}
|
||||
});
|
||||
|
||||
// Frozen pre-broad-phase reference: visit EVERY scene segment for EVERY
|
||||
// emitter, then use the same public visibility sweep. Exact emitted strings
|
||||
// (not only fan counts/bounds) must survive the candidate-culling speedup.
|
||||
const unfilteredFans = (emitters, radius, segments) => {
|
||||
const coord = (value) => String(Math.round(value * 10_000) / 10_000 || 0);
|
||||
return emitters.flatMap((p) => {
|
||||
const clipped = [];
|
||||
for (const s of segments) {
|
||||
if (!s || s.length < 4) continue;
|
||||
const dx = s[2] - s[0], dy = s[3] - s[1], len2 = dx * dx + dy * dy;
|
||||
if (!(len2 > 0)) continue;
|
||||
const ox = s[0] - p[0], oy = s[1] - p[1];
|
||||
const cross = ox * dy - oy * dx, distance2 = cross * cross / len2;
|
||||
if (distance2 >= radius * radius) continue;
|
||||
const center = -(ox * dx + oy * dy) / len2;
|
||||
const span = Math.sqrt((radius * radius - distance2) / len2);
|
||||
const lo = Math.max(0, center - span), hi = Math.min(1, center + span);
|
||||
if (hi <= lo) continue;
|
||||
clipped.push([s[0] + lo * dx, s[1] + lo * dy, s[0] + hi * dx, s[1] + hi * dy]);
|
||||
}
|
||||
const r = coord(radius);
|
||||
if (!clipped.length) {
|
||||
const left = coord(p[0] - radius), right = coord(p[0] + radius), y = coord(p[1]);
|
||||
return [`M${left} ${y} A${r} ${r} 0 1 1 ${right} ${y} A${r} ${r} 0 1 1 ${left} ${y} Z`];
|
||||
}
|
||||
const ring = visibilityPolygon(p, radius, clipped, 12);
|
||||
if (ring.length < 3) return [];
|
||||
const onRadius = (point) => Math.abs(Math.hypot(point[0] - p[0], point[1] - p[1]) - radius)
|
||||
<= Math.max(1e-9, radius * 1e-7);
|
||||
let path = `M${coord(ring[0][0])} ${coord(ring[0][1])}`;
|
||||
for (let i = 1; i <= ring.length; i++) {
|
||||
const previous = ring[i - 1], point = ring[i % ring.length];
|
||||
path += onRadius(previous) && onRadius(point)
|
||||
? ` A${r} ${r} 0 0 1 ${coord(point[0])} ${coord(point[1])}`
|
||||
: ` L${coord(point[0])} ${coord(point[1])}`;
|
||||
}
|
||||
return [`${path} Z`];
|
||||
});
|
||||
};
|
||||
|
||||
test('#788: strip-wide broad phase preserves every unfiltered fan byte for byte', () => {
|
||||
const faces = {
|
||||
faces: [{ a: [0, 0], b: [8, 0] }], inside: ([, y]) => y < 0, epsilon: 0.5,
|
||||
};
|
||||
const cases = [
|
||||
{ points: [[350, 350], [585, 450]], radius: 25,
|
||||
// Both endpoints are outside the field box, but this wall crosses it.
|
||||
segments: [[595, 100, 595, 700], [-1000, -1000, -900, -900], [590, 470, 610, 450]] },
|
||||
{ points: [[350, 350], [585, 450]], radius: 50,
|
||||
segments: [[595, 700, 595, 100], [600, 100, 600, 700], [0, 900, 1000, 900]] },
|
||||
{ points: [[0, 0], [8, 0]], radius: 1, faces,
|
||||
// This edge is outside the STORED path box, but inside the displaced
|
||||
// emitters' expanded box; an input-point bound would lose its shadow.
|
||||
segments: [[-20, 1.49, 20, 1.49], [-20, -100, 20, -100]] },
|
||||
{ points: [[0, 0], [2.49, 0]], radius: 2,
|
||||
// Boundary tangencies, near-tangencies and degenerate segments.
|
||||
segments: [[4.49, -20, 4.49, 20], [-20, 2, 20, 2], [-2, -20, -2, 20],
|
||||
[-20, 2 - 1e-10, 20, 2 - 1e-10], [1, 1, 1, 1], [-20, -2 - 1e-10, 20, -2 - 1e-10]] },
|
||||
{ points: [[0.13, 0.29], [8.37, 1.26], [8.9, 6.31], [0.32, 7.19], [0.13, 0.29]], radius: 2,
|
||||
segments: [[-100, 3, 100, 3], [-100, 4, 100, 4], [4, -100, 4, 100], [400, 400, 401, 401]] },
|
||||
{ points: [[0, 0], [2.1, 0], [0.2, 0.55], [2.4, 0.8]], radius: 2,
|
||||
segments: [[-10, 2.5, 10, 2.5], [3, -10, 3, 10], [-20, -20, -10, -10]] },
|
||||
];
|
||||
// Deterministic coverage of varied origins, radii, segment directions and
|
||||
// local/far obstacles. No timing assertions or nondeterministic randomness.
|
||||
let seed = 788;
|
||||
const random = () => ((seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0) / 2 ** 32);
|
||||
for (let i = 0; i < 18; i++) {
|
||||
const x = (random() - 0.5) * 10_000, y = (random() - 0.5) * 10_000;
|
||||
const radius = [0.05, 2, 25][i % 3];
|
||||
const points = Array.from({ length: 4 }, () => [x + random() * radius * 4, y + random() * radius * 4]);
|
||||
const segments = Array.from({ length: 12 }, (_, j) => {
|
||||
const scale = j % 3 ? radius * 8 : radius * 1000;
|
||||
return [x + (random() - 0.5) * scale, y + (random() - 0.5) * scale,
|
||||
x + (random() - 0.5) * scale, y + (random() - 0.5) * scale];
|
||||
});
|
||||
cases.push({ points, radius, segments });
|
||||
}
|
||||
for (const [index, fixture] of cases.entries()) {
|
||||
for (const points of [fixture.points, [...fixture.points].reverse()]) {
|
||||
const emitters = emitterSamples(compactPoints(points), fixture.faces ?? null, fixture.radius / 4);
|
||||
const expected = unfilteredFans(emitters, fixture.radius, fixture.segments);
|
||||
const actual = buildFieldGeometry({ points, radius: fixture.radius,
|
||||
scene: { ...scene, occluders: fixture.segments }, polygons: [], faces: fixture.faces ?? null, spaceId: 's' });
|
||||
assert.deepEqual(actual?.pieces.flatMap(pieceFans) ?? [], expected, `fixture ${index}: ${JSON.stringify(points)}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('#788: distant occluders are examined once per strip, not once per emitter', () => {
|
||||
const points = [[0, 0], [80, 0]], radius = 2;
|
||||
let farCoordinateReads = 0;
|
||||
const far = Array.from({ length: 64 }, (_, i) => new Proxy([1000 + i, -100, 1000 + i, 100], {
|
||||
get(target, property, receiver) {
|
||||
if (/^[0-3]$/.test(String(property))) farCoordinateReads++;
|
||||
return Reflect.get(target, property, receiver);
|
||||
},
|
||||
}));
|
||||
const local = [[40, 1, 50, 1]];
|
||||
const geometry = buildFieldGeometry({ points, radius,
|
||||
scene: { ...scene, occluders: [...far, ...local] }, polygons: [], faces: null, spaceId: 's' });
|
||||
assert.deepEqual(geometry.pieces.flatMap(pieceFans), unfilteredFans(emitterSamples(points, null, radius / 4), radius, local));
|
||||
assert.ok(farCoordinateReads <= far.length * 8,
|
||||
`${farCoordinateReads} coordinate reads: a distant segment must not enter per-emitter clipping`);
|
||||
assert.ok(geometry.pieces.reduce((n, piece) => n + piece.sourceCount, 0) > 150,
|
||||
'candidate culling must not thin or drop emitter fans');
|
||||
});
|
||||
|
||||
test('#788: a residual run retains the true free endpoint in both directions', () => {
|
||||
const freeScene = { ...scene, occluders: [], fingerprint: 'free-endpoints' };
|
||||
for (const radius of [0.2, 2, 20]) {
|
||||
|
||||
@@ -41,15 +41,23 @@ test('#375 AC1: the glow scene passes input.devices without a spread', () => {
|
||||
});
|
||||
|
||||
// #375 AC2: the static path attaches the same sourceFingerprint tag the full
|
||||
// card attaches, from the same triple buildLightBarrierRevision fingerprints —
|
||||
// card attaches, through the same lightGeometryFingerprint helper —
|
||||
// proven the same way the full card's recut wiring is proven
|
||||
// (performance-contract.test.mjs): by pinning the source.
|
||||
|
||||
test('#375 AC2: static wall geometry carries the recut fingerprint tag', () => {
|
||||
const render = readFileSync(new URL('../src/space-render.ts', import.meta.url), 'utf8');
|
||||
assert.match(render,
|
||||
/Object\.defineProperty\(built, 'sourceFingerprint', \{\s*\n\s*value: contentFingerprint\(\[spCfg, cellCm, GRID_PITCH\]\),/,
|
||||
'the tag must be the exact revision triple (rawSpaceConfig=spCfg, cellCm, gridPitch=GRID_PITCH)');
|
||||
/Object\.defineProperty\(built, 'sourceFingerprint', \{\s*\n\s*value: lightGeometryFingerprint\(spCfg, cellCm, GRID_PITCH\),/,
|
||||
'the tag must use the same geometry projection and scale as the light revision');
|
||||
const fullCard = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
|
||||
assert.match(fullCard,
|
||||
/Object\.defineProperty\(value, 'sourceFingerprint', \{\s*\n\s*value: lightGeometryFingerprint\(this\._curSpaceCfg, this\._cellCm, this\._gridPitch\),/,
|
||||
'the full-card tag must share that projection and scale as well');
|
||||
const editor = readFileSync(new URL('../src/houseplan-editor-runtime.ts', import.meta.url), 'utf8');
|
||||
assert.match(editor,
|
||||
/Object\.defineProperty\(projected, 'sourceFingerprint', \{\s*\n\s*value: lightGeometryFingerprint\(preview\.sp, this\.host\._cellCm, this\.host\._gridPitch\),/,
|
||||
'accepted resize artifacts must retain the same fast-path tag');
|
||||
assert.ok(
|
||||
render.indexOf("Object.defineProperty(built, 'sourceFingerprint'")
|
||||
< render.indexOf('const contentFrame ='),
|
||||
|
||||
Reference in New Issue
Block a user