mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 20:29:00 +00:00
refactor(iso): remove the empty overlay renderers and dead fixture fields (#732)
After #714 and #724 the 2.5D overlays still carried stubs: - renderIsoOverlayGrounds and renderIsoRaisedOverlays returned an empty SVG on every frame. They go with IsoFramePresentation.grounds/raised and the two bindings in the card. The iso-overlays-svg element itself stays, now empty: it is the inert camera-viewBox layer the contract and live-touch smokes measure screen-facing HTML against, so the 2.5D DOM keeps its elements. - IsoOverlayRenderEntry.groundRadius was computed for every device, room label and lock and read only by the snapshot comparison that compared it. The overlay test fixtures passed view, referenceView, stageSize and layers (and one test selectedDeviceId), which IsoOverlaySceneInput does not have, and asserted that changing them keeps the placement - a claim the signature makes by itself. Those fields are gone from every fixture. The zoom/resize asserts of "Stage 4 reuses pure overlay placements" and "#713 AC3" (renamed to what it still checks) and the "#570 supersedes #473 W1" selection test go; the #724 AC2 test now zooms the way production does, through the live frame of resolveIsoScene, and checks that the structural geometry and so the overlay scene are reused. The #713 K8 fixture no longer passes stageSize, which resolveIsoOverlayFitEnvelope does not read. test/iso-overlay-fixture-types.test.mjs typechecks the overlay test files with the TypeScript compiler: their fixture types (OverlaySceneFixture, OverlayEntryFixture) are the keys of the production types with deliberately loose values, so a partial fixture is fine and a field the type lacks is an excess-property error. Three checks: no excess property in the fixture files; a probe shows the fixture types resolve to the real inputs and reject view, referenceView, stageSize, layers, selectedDeviceId and groundRadius; every call of the scene builder gets its argument through a checked type (a literal in overlayScene or a declaration of the fixture type). Each check is red when a dead field is put back into a declared fixture, an override literal or an entry, when a literal goes straight into the builder, when a fixture loses its annotation, and when groundRadius returns to the entry type. isometric-contract now asserts that nothing renders into the overlay surface and that the removed renderers and groundRadius stay gone. No mutant is anchored on the removed code; mutation-gate --check is unchanged (3 warnings). The 19 2.5D golden scenes pass in capture on the accepted baselines. Issue: #732 User-Visible: no Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
@@ -11136,10 +11136,7 @@ export class HouseplanCard extends LitElement {
|
||||
${isoFrame?.walls ?? nothing}
|
||||
</svg>
|
||||
<svg class="iso-overlays-svg" data-hp-live-viewbox="camera" viewBox="${view.x} ${view.y} ${view.w} ${view.h}"
|
||||
preserveAspectRatio="xMidYMid meet" aria-hidden="true" pointer-events="none">
|
||||
${isoFrame?.grounds ?? nothing}
|
||||
${isoFrame?.raised ?? nothing}
|
||||
</svg>` : nothing)}
|
||||
preserveAspectRatio="xMidYMid meet" aria-hidden="true" pointer-events="none"></svg>` : nothing)}
|
||||
${''/* docs/CANVAS.md §6: an icon is a percentage of the PLAN and
|
||||
scales with it when you zoom — the behaviour the card always
|
||||
had, restored by the owner. `iconCqw` is `iconPct * iconUnit
|
||||
|
||||
+1
-23
@@ -131,7 +131,6 @@ export type IsoOverlayRenderEntry = {
|
||||
id: string;
|
||||
kind: IsoRaisedOverlayKind;
|
||||
placement: IsoOverlayPlacement;
|
||||
groundRadius: number;
|
||||
/** Screen-facing HTML footprint around visualScene, in scene units. */
|
||||
screenHalfSize: PlanPoint;
|
||||
/**
|
||||
@@ -727,7 +726,7 @@ function sameOverlayEntries(
|
||||
return previous.length === next.length && next.every((entry, index) => {
|
||||
const before = previous[index];
|
||||
return before.id === entry.id && before.kind === entry.kind
|
||||
&& before.placement === entry.placement && before.groundRadius === entry.groundRadius
|
||||
&& before.placement === entry.placement
|
||||
&& before.screenHalfSize[0] === entry.screenHalfSize[0]
|
||||
&& before.screenHalfSize[1] === entry.screenHalfSize[1];
|
||||
});
|
||||
@@ -843,7 +842,6 @@ export function buildIsoOverlayRenderScene(input: IsoOverlaySceneInput): IsoOver
|
||||
id: device.id,
|
||||
kind: 'device',
|
||||
placement,
|
||||
groundRadius: Math.max(core * 0.32, 2),
|
||||
screenHalfSize: halfSize,
|
||||
layoutHalfSize,
|
||||
});
|
||||
@@ -866,7 +864,6 @@ export function buildIsoOverlayRenderScene(input: IsoOverlaySceneInput): IsoOver
|
||||
id: overlayRoom.id,
|
||||
kind: 'room-label',
|
||||
placement,
|
||||
groundRadius: Math.max(font * 0.75, 2),
|
||||
screenHalfSize: halfSize,
|
||||
});
|
||||
}
|
||||
@@ -891,7 +888,6 @@ export function buildIsoOverlayRenderScene(input: IsoOverlaySceneInput): IsoOver
|
||||
id: String(opening.id),
|
||||
kind: 'opening-lock',
|
||||
placement,
|
||||
groundRadius: Math.max(size * 0.28, 2),
|
||||
screenHalfSize: halfSize,
|
||||
});
|
||||
}
|
||||
@@ -1038,8 +1034,6 @@ export interface IsoFramePresentation {
|
||||
underlay: TemplateResult;
|
||||
shadows: TemplateResult;
|
||||
walls: TemplateResult;
|
||||
grounds: TemplateResult;
|
||||
raised: TemplateResult;
|
||||
}
|
||||
|
||||
/** Resolve every lazy Stage 4 artifact inside the card's single failure boundary. */
|
||||
@@ -1062,8 +1056,6 @@ export function resolveIsoFramePresentation(input: {
|
||||
underlay: renderIsoUnderlay(renderLayers, input.scene?.floor, input.cellCm),
|
||||
shadows: renderIsoShadows(renderLayers, panels, input.scene?.geometry, input.cellCm),
|
||||
walls: renderIsoWalls(input.projection, renderLayers, input.scene, panels, input.cellCm),
|
||||
grounds: renderIsoOverlayGrounds(overlays, renderLayers, input.cellCm),
|
||||
raised: renderIsoRaisedOverlays(overlays),
|
||||
});
|
||||
try { return render(layers); } catch (error) {
|
||||
// A material/shadow presentation failure loses only decorative nuance.
|
||||
@@ -1115,20 +1107,6 @@ function renderIsoDefs(
|
||||
</defs>` as unknown as TemplateResult;
|
||||
}
|
||||
|
||||
export function renderIsoOverlayGrounds(
|
||||
_overlays: IsoOverlayRenderScene | null,
|
||||
_layers: IsoDecorationLayers,
|
||||
_cellCm: number,
|
||||
): TemplateResult {
|
||||
return emptySvg();
|
||||
}
|
||||
|
||||
export function renderIsoRaisedOverlays(
|
||||
_overlays: IsoOverlayRenderScene | null,
|
||||
): TemplateResult {
|
||||
return emptySvg();
|
||||
}
|
||||
|
||||
export function renderIsoUnderlay(
|
||||
layers: IsoDecorationLayers,
|
||||
floor: IsoFloorGeometry | undefined,
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { basename, dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import ts from 'typescript';
|
||||
|
||||
// #732 AC2. Фикстуры тестов оверлеев 2.5D передавали поля, которых никто не
|
||||
// читает: `view`, `referenceView`, `stageSize`, `layers`, `selectedDeviceId`
|
||||
// у входа сцены и `groundRadius` у записи. Тесты оставались зелёными — и
|
||||
// «проверяли», что зум, размер сцены или выделение не пересчитывают раскладку,
|
||||
// хотя сигнатура их вообще не принимает.
|
||||
//
|
||||
// Здесь файлы с такими фикстурами проходят через компилятор TypeScript. Тип
|
||||
// фикстуры (`OverlaySceneFixture`, `OverlayEntryFixture`) — ключи боевого типа
|
||||
// входа с намеренно свободными значениями: фикстура частична, как и нужно
|
||||
// тесту, но поле, которого у типа нет, — ошибка «лишнее свойство». Прочие
|
||||
// диагностики этих файлов не судятся: сами тесты исполняют `test-build`, и
|
||||
// частичность значений — их право.
|
||||
|
||||
const ROOT = fileURLToPath(new URL('../', import.meta.url));
|
||||
const TEST_DIR = join(ROOT, 'test');
|
||||
const FIXTURE_FILES = ['iso-scene-render.test.mjs', 'iso-stage6.test.mjs'].map((name) => join(TEST_DIR, name));
|
||||
const PROBE = join(TEST_DIR, '__iso-overlay-fixture-probe.mjs');
|
||||
/** «Object literal may only specify known properties…» — с подсказкой и без. */
|
||||
const EXCESS_PROPERTY = new Set([2353, 2561]);
|
||||
/** Входы сцены и поле записи, которых у боевых типов нет (#714, #724, #732). */
|
||||
const DEAD_SCENE_FIELDS = ['view', 'referenceView', 'stageSize', 'layers', 'selectedDeviceId'];
|
||||
const DEAD_ENTRY_FIELDS = ['groundRadius'];
|
||||
const SCENE_BUILDERS = new Set(['buildIsoOverlayRenderScene', 'overlayScene']);
|
||||
const tagOf = (file) => basename(file).replace(/\W/g, '_');
|
||||
|
||||
/** Зонд: каждое мёртвое поле — отдельный литерал (TypeScript называет одно лишнее поле на литерал). */
|
||||
function probeSource() {
|
||||
const lines = [];
|
||||
const typed = (type, name, body) => lines.push(`/** @type {${type}} */`, `export const ${name} = { ${body} };`);
|
||||
for (const file of FIXTURE_FILES) {
|
||||
const scene = `import('./${basename(file)}').OverlaySceneFixture`;
|
||||
const tag = tagOf(file);
|
||||
typed(scene, `${tag}_known`, 'space: null, structure: null, positionOf: null');
|
||||
DEAD_SCENE_FIELDS.forEach((field, index) => typed(scene, `${tag}_scene${index}`, `${field}: null`));
|
||||
}
|
||||
const entry = "import('./iso-scene-render.test.mjs').OverlayEntryFixture";
|
||||
typed(entry, 'entry_known', 'id: null, placement: null, screenHalfSize: null');
|
||||
DEAD_ENTRY_FIELDS.forEach((field, index) => typed(entry, `entry${index}`, `${field}: null`));
|
||||
return `${lines.join('\n')}\n`;
|
||||
}
|
||||
|
||||
function fixtureProgram() {
|
||||
const config = ts.readConfigFile(join(ROOT, 'tsconfig.json'), ts.sys.readFile).config;
|
||||
const { options: base } = ts.parseJsonConfigFileContent(config, ts.sys, ROOT);
|
||||
const options = {
|
||||
...base, allowJs: true, checkJs: true, noEmit: true, incremental: false, types: [],
|
||||
};
|
||||
const host = ts.createCompilerHost(options, true);
|
||||
const probe = probeSource();
|
||||
const getSourceFile = host.getSourceFile.bind(host);
|
||||
host.getSourceFile = (name, languageVersion, ...rest) => (name === PROBE
|
||||
? ts.createSourceFile(name, probe, languageVersion, true)
|
||||
: getSourceFile(name, languageVersion, ...rest));
|
||||
const fileExists = host.fileExists.bind(host);
|
||||
host.fileExists = (name) => name === PROBE || fileExists(name);
|
||||
// Тесты исполняют `../test-build/*.js` и `node:*`; для типов фикстур нужны
|
||||
// только JSDoc-импорты `../src/*` и файлы фикстур для зонда. Остальное не
|
||||
// разрешается — компилятор не тянет test-build и @types/node.
|
||||
host.resolveModuleNameLiterals = (literals, containing, _redirect, opts) => literals.map((literal) => {
|
||||
const spec = literal.text;
|
||||
if (dirname(containing) === TEST_DIR && !spec.startsWith('../src/') && !spec.startsWith('./')) {
|
||||
return { resolvedModule: undefined };
|
||||
}
|
||||
return ts.resolveModuleName(spec, containing, opts, host);
|
||||
});
|
||||
return ts.createProgram([...FIXTURE_FILES, PROBE], options, host);
|
||||
}
|
||||
|
||||
const program = fixtureProgram();
|
||||
const checker = program.getTypeChecker();
|
||||
const where = (diagnostic) => {
|
||||
const { line } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
|
||||
return `${diagnostic.file.fileName.slice(ROOT.length)}:${line + 1}`;
|
||||
};
|
||||
const message = (diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, ' ');
|
||||
|
||||
test('#732 AC2: фикстуры оверлеев не передают полей, которых нет у боевых типов', () => {
|
||||
const excess = FIXTURE_FILES.flatMap((file) => program.getSemanticDiagnostics(program.getSourceFile(file)))
|
||||
.filter((diagnostic) => EXCESS_PROPERTY.has(diagnostic.code))
|
||||
.map((diagnostic) => `${where(diagnostic)} ${message(diagnostic)}`);
|
||||
assert.deepEqual(excess, [], 'поле, которого никто не читает, фикстура не передаёт');
|
||||
});
|
||||
|
||||
test('#732 AC2: тип фикстуры — ключи боевого входа; зум, размер сцены, слои, выделение и радиус земли — не входы', () => {
|
||||
const probe = program.getSourceFile(PROBE);
|
||||
const rejected = new Map();
|
||||
for (const diagnostic of program.getSemanticDiagnostics(probe)) {
|
||||
const name = probe.text.slice(0, diagnostic.start).match(/export const (\w+) = [^\n]*$/)?.[1];
|
||||
assert.ok(EXCESS_PROPERTY.has(diagnostic.code), `${where(diagnostic)} ${message(diagnostic)}`);
|
||||
rejected.set(name, message(diagnostic));
|
||||
}
|
||||
for (const file of FIXTURE_FILES) {
|
||||
const tag = tagOf(file);
|
||||
assert.ok(!rejected.has(`${tag}_known`), `${tag}: поля боевого входа принимаются`);
|
||||
DEAD_SCENE_FIELDS.forEach((field, index) => {
|
||||
assert.match(rejected.get(`${tag}_scene${index}`) || '', new RegExp(`'${field}' does not exist in type`),
|
||||
`${file.slice(ROOT.length)}: OverlaySceneFixture отвергает ${field} — тип разрешился в боевой вход, а не в any`);
|
||||
});
|
||||
}
|
||||
assert.ok(!rejected.has('entry_known'), 'поля записи принимаются');
|
||||
DEAD_ENTRY_FIELDS.forEach((field, index) => {
|
||||
assert.match(rejected.get(`entry${index}`) || '', new RegExp(`'${field}' does not exist in type`));
|
||||
});
|
||||
});
|
||||
|
||||
test('#732 AC2: каждая фикстура сцены доходит до построителя через проверяемый тип', () => {
|
||||
const aliasOf = (type) => type?.aliasSymbol?.name ?? null;
|
||||
const unchecked = [];
|
||||
let calls = 0;
|
||||
for (const file of FIXTURE_FILES) {
|
||||
const source = program.getSourceFile(file);
|
||||
const visit = (node) => {
|
||||
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && SCENE_BUILDERS.has(node.expression.text)) {
|
||||
calls += 1;
|
||||
const [arg] = node.arguments;
|
||||
// Литерал проверяется типом параметра (`overlayScene`), прочее — своим
|
||||
// объявленным типом; литерал прямо в построитель из test-build не
|
||||
// проверяется ничем.
|
||||
const type = arg && ts.isObjectLiteralExpression(arg)
|
||||
? checker.getContextualType(arg) : arg && checker.getTypeAtLocation(arg);
|
||||
if (aliasOf(type) !== 'OverlaySceneFixture') {
|
||||
const { line } = source.getLineAndCharacterOfPosition(node.getStart());
|
||||
unchecked.push(`${file.slice(ROOT.length)}:${line + 1} ${node.getText().slice(0, 80)}`);
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(source);
|
||||
}
|
||||
assert.ok(calls >= 20, `нашлись вызовы построителя сцены (${calls})`);
|
||||
assert.deepEqual(unchecked, [], 'фикстура сцены — литерал в overlayScene или объявление типа OverlaySceneFixture');
|
||||
});
|
||||
@@ -43,6 +43,21 @@ import {
|
||||
const structureOf = (walls = []) => buildIsoWallGeometry(walls);
|
||||
const wallRect = (x0, y0, x1, y1) => [[[[x0, y0], [x1, y0], [x1, y1], [x0, y1]]]];
|
||||
|
||||
/**
|
||||
* #732: an overlay fixture names only fields of the production input type; the
|
||||
* values stay as partial as each test needs. test/iso-overlay-fixture-types.test.mjs
|
||||
* typechecks this file: a field nothing reads — a zoom view, a stage size,
|
||||
* decoration layers, a selection, a ground radius — fails there instead of
|
||||
* pretending to be an input. Every scene fixture reaches the builder through
|
||||
* `overlayScene` or a declaration of this type.
|
||||
*
|
||||
* @typedef {{ [K in keyof import('../src/iso-scene-render.js').IsoOverlaySceneInput]?: unknown }} OverlaySceneFixture
|
||||
* @typedef {{ [K in keyof import('../src/iso-scene-render.js').IsoOverlayRenderEntry]?: unknown }} OverlayEntryFixture
|
||||
*/
|
||||
|
||||
/** @param {OverlaySceneFixture} input */
|
||||
const overlayScene = (input) => buildIsoOverlayRenderScene(input);
|
||||
|
||||
const room = (id, x0, y0, x1, y1) => ({
|
||||
id,
|
||||
poly: [[x0, y0], [x1, y0], [x1, y1], [x0, y1]],
|
||||
@@ -132,19 +147,19 @@ test('overlay bounds use final screen footprint and canonical owner filtering',
|
||||
footprint: [[center[0] - 2, center[1] - 1], [center[0] + 2, center[1] - 1],
|
||||
[center[0] + 2, center[1] + 1], [center[0] - 2, center[1] + 1]],
|
||||
});
|
||||
/** @type {{ entries: OverlayEntryFixture[] }} */
|
||||
const scene = { entries: [
|
||||
{ id: 'one', kind: 'device', placement: placement('r1', [20, 30]),
|
||||
groundRadius: 1, screenHalfSize: [5, 3] },
|
||||
{ id: 'two', kind: 'device', placement: placement('r2', [200, 300]),
|
||||
groundRadius: 1, screenHalfSize: [10, 8] },
|
||||
{ id: 'one', kind: 'device', placement: placement('r1', [20, 30]), screenHalfSize: [5, 3] },
|
||||
{ id: 'two', kind: 'device', placement: placement('r2', [200, 300]), screenHalfSize: [10, 8] },
|
||||
] };
|
||||
assert.deepEqual(isoOverlaySceneBounds(scene, 'r1'), { x: 15, y: 27, w: 10, h: 6 });
|
||||
assert.deepEqual(isoOverlaySceneBounds(scene), { x: 15, y: 27, w: 195, h: 281 });
|
||||
});
|
||||
|
||||
test('#713 K8: overlay fit is the structure plus visible tiles, no #651 nudge reserve', () => {
|
||||
/** @type {OverlayEntryFixture} */
|
||||
const entry = {
|
||||
id: 'edge', kind: 'device', groundRadius: 1, screenHalfSize: [10, 8],
|
||||
id: 'edge', kind: 'device', screenHalfSize: [10, 8],
|
||||
placement: {
|
||||
owner: { id: 'room' }, floorScene: [95, 50],
|
||||
visualScene: [95, 50],
|
||||
@@ -162,18 +177,18 @@ test('#713 K8: overlay fit is the structure plus visible tiles, no #651 nudge re
|
||||
return { x: bounds.x - (w - bounds.w) / 2, y: bounds.y, w, h: bounds.h };
|
||||
};
|
||||
const fitted = resolveIsoOverlayFitEnvelope({
|
||||
baseBounds: { x: 0, y: 0, w: 100, h: 100 }, entries: [entry], stageSize, targetView,
|
||||
baseBounds: { x: 0, y: 0, w: 100, h: 100 }, entries: [entry], targetView,
|
||||
});
|
||||
assert.ok(fitted);
|
||||
assert.deepEqual(fitted.bounds, { x: 0, y: 0, w: 105, h: 100 },
|
||||
'the tile edge at x=105 is the only growth: no 48 CSS px reserve around it');
|
||||
assert.deepEqual(fitted.view, targetView(fitted.bounds));
|
||||
const repeated = resolveIsoOverlayFitEnvelope({
|
||||
baseBounds: { x: 0, y: 0, w: 100, h: 100 }, entries: [entry], stageSize, targetView,
|
||||
baseBounds: { x: 0, y: 0, w: 100, h: 100 }, entries: [entry], targetView,
|
||||
});
|
||||
assert.deepEqual(repeated, fitted, 'the canonical envelope is deterministic');
|
||||
const otherRoom = resolveIsoOverlayFitEnvelope({
|
||||
baseBounds: { x: 0, y: 0, w: 100, h: 100 }, entries: [entry], stageSize,
|
||||
baseBounds: { x: 0, y: 0, w: 100, h: 100 }, entries: [entry],
|
||||
targetView, ownerId: 'other-room',
|
||||
});
|
||||
assert.deepEqual(otherRoom.bounds, { x: 0, y: 0, w: 100, h: 100 });
|
||||
@@ -324,13 +339,11 @@ test('opening-lock scene keeps physical host ownership when spatial fallback poi
|
||||
});
|
||||
assert.equal(spatialOwner?.id, 'decoy', 'fixture proves point containment would pick the wrong room');
|
||||
|
||||
const scene = buildIsoOverlayRenderScene({
|
||||
const scene = overlayScene({
|
||||
space,
|
||||
devices: [],
|
||||
openings: [opening],
|
||||
view: { x: 0, y: 0, w: 100, h: 100 },
|
||||
display: { showNames: false, cardFontScale: 1 },
|
||||
layers: { shadows: true },
|
||||
structure: structureOf(),
|
||||
iconPct: 100,
|
||||
deviceBasePct: 100,
|
||||
@@ -338,7 +351,6 @@ test('opening-lock scene keeps physical host ownership when spatial fallback poi
|
||||
cellCm: 5,
|
||||
kioskIconScale: 1,
|
||||
kioskFontScale: 1,
|
||||
stageSize: { width: 100, height: 100 },
|
||||
positionOf: () => ({ x: 0, y: 0 }),
|
||||
presentationOf: () => ({ scale: 1 }),
|
||||
labelPositionOf: () => ({ x: 0, y: 0 }),
|
||||
@@ -365,13 +377,11 @@ test('opening lock without a canonical host owner never guesses from point conta
|
||||
partition: { cm: 10 },
|
||||
},
|
||||
};
|
||||
const scene = buildIsoOverlayRenderScene({
|
||||
const scene = overlayScene({
|
||||
space,
|
||||
devices: [],
|
||||
openings: [opening],
|
||||
view: { x: 0, y: 0, w: 100, h: 100 },
|
||||
display: { showNames: false, cardFontScale: 1 },
|
||||
layers: { shadows: true },
|
||||
structure: structureOf(wallRect(0, 45, 100, 55)),
|
||||
iconPct: 100,
|
||||
deviceBasePct: 100,
|
||||
@@ -379,7 +389,6 @@ test('opening lock without a canonical host owner never guesses from point conta
|
||||
cellCm: 5,
|
||||
kioskIconScale: 1,
|
||||
kioskFontScale: 1,
|
||||
stageSize: { width: 100, height: 100 },
|
||||
positionOf: () => ({ x: 0, y: 0 }),
|
||||
presentationOf: () => ({ scale: 1 }),
|
||||
labelPositionOf: () => ({ x: 0, y: 0 }),
|
||||
@@ -402,14 +411,12 @@ test('Stage 4 reuses pure overlay placements, and fit and the live frame share o
|
||||
rooms: [owner], wall_segments: [], room_drafts: [], partitions: [], wall_columns: [],
|
||||
};
|
||||
const structure = structureOf(wallRect(-4, -10, 4, 110));
|
||||
/** @type {OverlaySceneFixture} */
|
||||
const input = {
|
||||
space,
|
||||
devices: [{ id: 'device', space: 'floor', marker: { room_id: 'owner' } }],
|
||||
openings: [],
|
||||
view: { x: 0, y: 0, w: 100, h: 100 },
|
||||
referenceView: { x: 0, y: 0, w: 100, h: 100 },
|
||||
display: { showNames: false, cardFontScale: 1 },
|
||||
layers: { structural: true, shadows: true },
|
||||
structure,
|
||||
iconPct: 3.4,
|
||||
deviceBasePct: 3.4,
|
||||
@@ -417,7 +424,6 @@ test('Stage 4 reuses pure overlay placements, and fit and the live frame share o
|
||||
cellCm: 5,
|
||||
kioskIconScale: 1,
|
||||
kioskFontScale: 1,
|
||||
stageSize: { width: 100, height: 100 },
|
||||
positionOf: () => ({ x: 5, y: 50 }),
|
||||
presentationOf: () => ({
|
||||
scale: 1, valueText: null, valueFullText: '', valueBadge: null,
|
||||
@@ -429,8 +435,8 @@ test('Stage 4 reuses pure overlay placements, and fit and the live frame share o
|
||||
openingEntityAvailable: () => false,
|
||||
openingWallIndex: () => ({ adjacencyEps: 0.1, edges: [] }),
|
||||
};
|
||||
const live = buildIsoOverlayRenderScene(input);
|
||||
const repeated = buildIsoOverlayRenderScene(input);
|
||||
const live = overlayScene(input);
|
||||
const repeated = overlayScene(input);
|
||||
assert.strictEqual(repeated, live,
|
||||
'an unchanged frame reuses the exact render-scene snapshot for Lit guards');
|
||||
assert.strictEqual(repeated.devices.get('device'), live.devices.get('device'),
|
||||
@@ -439,23 +445,13 @@ test('Stage 4 reuses pure overlay placements, and fit and the live frame share o
|
||||
// next to a wall; no wall test and no nudge in the live scene.
|
||||
assert.deepEqual(live.devices.get('device')?.visualScene, projectPlanPoint([5, 50], ISO_WALL_HEIGHT));
|
||||
|
||||
const zoomedIn = buildIsoOverlayRenderScene({
|
||||
...input, view: { x: 0, y: 0, w: 80, h: 80 },
|
||||
});
|
||||
assert.strictEqual(zoomedIn, live, 'zooming in reuses the immutable scene placement');
|
||||
const resized = buildIsoOverlayRenderScene({ ...input, stageSize: { width: 40, height: 40 } });
|
||||
assert.strictEqual(resized, live, '#714: a stage resize is not a layout event either');
|
||||
|
||||
// #724: the fit envelope and the live frame ask with the same inputs and read
|
||||
// one snapshot — there is no search for them to differ by (#713).
|
||||
const fit = buildIsoOverlayRenderScene({ ...input });
|
||||
// one snapshot — there is no search for them to differ by (#713). Zoom, pan
|
||||
// and a stage resize are no input of the overlay scene at all (#714, #732):
|
||||
// the fixture typecheck rejects such a field, and the production-path zoom is
|
||||
// the #724 AC2 test below.
|
||||
const fit = overlayScene({ ...input });
|
||||
assert.strictEqual(fit, live, 'fit and live share one render-scene snapshot');
|
||||
|
||||
const zoomed = buildIsoOverlayRenderScene({ ...input, view: { x: -10, y: -10, w: 120, h: 120 } });
|
||||
assert.strictEqual(zoomed, live,
|
||||
'zooming out and panning reuse the same immutable scene placement');
|
||||
assert.strictEqual(zoomed.devices.get('device'), live.devices.get('device'),
|
||||
'live view is absent from the placement signature');
|
||||
});
|
||||
|
||||
test('render scene keeps coincident devices coincident, leaves labels alone and caches permutations', () => {
|
||||
@@ -470,15 +466,13 @@ test('render scene keeps coincident devices coincident, leaves labels alone and
|
||||
const devices = ['b', 'a', 'c'].map((id) => ({
|
||||
id, space: 'floor', marker: { room_id: 'owner', x: 200, y: 200 },
|
||||
}));
|
||||
/** @type {OverlaySceneFixture} */
|
||||
const input = {
|
||||
space, devices, openings: [],
|
||||
view: { x: 0, y: 0, w: 400, h: 400 },
|
||||
display: { showNames: true, cardFontScale: 1 },
|
||||
layers: { structural: true, shadows: true },
|
||||
structure: structureOf(),
|
||||
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,
|
||||
@@ -490,7 +484,7 @@ test('render scene keeps coincident devices coincident, leaves labels alone and
|
||||
openingEntityAvailable: () => false,
|
||||
openingWallIndex: () => ({ adjacencyEps: 0.1, edges: [] }),
|
||||
};
|
||||
const scene = buildIsoOverlayRenderScene(input);
|
||||
const scene = overlayScene(input);
|
||||
const deviceEntries = scene.entries.filter((entry) => entry.kind === 'device');
|
||||
for (let index = 0; index < deviceEntries.length; index++) {
|
||||
assert.deepEqual(deviceEntries[index].placement.visualScene,
|
||||
@@ -506,10 +500,10 @@ test('render scene keeps coincident devices coincident, leaves labels alone and
|
||||
assert.deepEqual(label.placement.visualScene, [200, 200],
|
||||
'coincident devices do not push the room label');
|
||||
|
||||
const permuted = buildIsoOverlayRenderScene({ ...input, devices: [...devices].reverse() });
|
||||
const permuted = overlayScene({ ...input, devices: [...devices].reverse() });
|
||||
assert.strictEqual(permuted, scene,
|
||||
'HA registry permutations reuse the same immutable group layout snapshot');
|
||||
const fit = buildIsoOverlayRenderScene({ ...input });
|
||||
const fit = overlayScene({ ...input });
|
||||
assert.strictEqual(fit.devices.get('a'), scene.devices.get('a'),
|
||||
'fit probing and the live scene read the same placement');
|
||||
});
|
||||
@@ -532,15 +526,13 @@ test('#711 состояние устройства не двигает знач
|
||||
: null,
|
||||
pulse: { animated: false, diameterScale: 1 },
|
||||
});
|
||||
/** @type {OverlaySceneFixture} */
|
||||
const input = {
|
||||
space, devices, openings: [],
|
||||
view: { x: 0, y: 0, w: 400, h: 400 },
|
||||
display: { showNames: false, cardFontScale: 1 },
|
||||
layers: { structural: true, shadows: true },
|
||||
structure,
|
||||
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,
|
||||
labelPositionOf: () => ({ x: 0, y: 0 }),
|
||||
@@ -548,9 +540,9 @@ test('#711 состояние устройства не двигает знач
|
||||
openingEntityAvailable: () => false,
|
||||
openingWallIndex: () => ({ adjacencyEps: 0.1, edges: [] }),
|
||||
};
|
||||
const off = buildIsoOverlayRenderScene(input);
|
||||
const off = overlayScene(input);
|
||||
lampOn = true;
|
||||
const on = buildIsoOverlayRenderScene(input);
|
||||
const on = overlayScene(input);
|
||||
for (const id of ['lamp', 'plug', 'sensor']) {
|
||||
assert.strictEqual(on.devices.get(id), off.devices.get(id), `${id}: включение лампы не пересчитывает раскладку`);
|
||||
}
|
||||
@@ -560,7 +552,7 @@ test('#711 состояние устройства не двигает знач
|
||||
assert.deepEqual(lampOnEntry.layoutHalfSize, lampOff.layoutHalfSize, 'раскладка видит плитку без состояния');
|
||||
assert.ok(isoOverlaySceneBounds(on).w >= isoOverlaySceneBounds(off).w, 'границы сцены учитывают бейдж');
|
||||
lampOn = false;
|
||||
const offAgain = buildIsoOverlayRenderScene(input);
|
||||
const offAgain = overlayScene(input);
|
||||
for (const id of ['lamp', 'plug', 'sensor']) {
|
||||
assert.deepEqual(offAgain.devices.get(id).visualScene, off.devices.get(id).visualScene, `${id}: выключение возвращает то же место`);
|
||||
}
|
||||
@@ -831,18 +823,15 @@ const perfFixture = () => {
|
||||
rooms: [owner], wall_segments: [], room_drafts: [], partitions: [], wall_columns: [],
|
||||
};
|
||||
const structure = structureOf(wallRect(-4, -10, 4, 110));
|
||||
/** @type {OverlaySceneFixture} */
|
||||
const input = {
|
||||
space,
|
||||
devices: [{ id: 'device', space: 'floor', marker: { room_id: 'owner' } }],
|
||||
openings: [],
|
||||
view: { x: 0, y: 0, w: 100, h: 100 },
|
||||
referenceView: { x: 0, y: 0, w: 100, h: 100 },
|
||||
display: { showNames: false, cardFontScale: 1 },
|
||||
layers: { structural: true, shadows: true },
|
||||
structure,
|
||||
iconPct: 3.4, deviceBasePct: 3.4, showLqi: false, cellCm: 5,
|
||||
kioskIconScale: 1, kioskFontScale: 1,
|
||||
stageSize: { width: 100, height: 100 },
|
||||
positionOf: () => ({ x: 5, y: 50 }),
|
||||
presentationOf: () => ({
|
||||
scale: 1, valueText: null, valueFullText: '', valueBadge: null,
|
||||
@@ -857,22 +846,16 @@ const perfFixture = () => {
|
||||
return { input, structure };
|
||||
};
|
||||
|
||||
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');
|
||||
// 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.strictEqual(again, selected, 'clearing selection keeps the same immutable placement');
|
||||
});
|
||||
// #570 superseded #473 W1: selection is presentation-only since Stage 4
|
||||
// removed the selected/hover tether and ground cue. It is no input of the
|
||||
// overlay scene at all — the fixture typecheck rejects `selectedDeviceId`
|
||||
// (test/iso-overlay-fixture-types.test.mjs, #732); a test passing it proved
|
||||
// nothing the signature does not.
|
||||
|
||||
test('#473 W2 after #724: walls never move a tile, the cache follows the structural wall geometry', () => {
|
||||
const { input } = perfFixture();
|
||||
const withWall = buildIsoOverlayRenderScene(input).devices.get('device');
|
||||
const noWalls = buildIsoOverlayRenderScene({ ...input, structure: structureOf() }).devices.get('device');
|
||||
const withWall = overlayScene(input).devices.get('device');
|
||||
const noWalls = overlayScene({ ...input, structure: structureOf() }).devices.get('device');
|
||||
assert.notStrictEqual(noWalls, withWall, 'a new wall geometry is a new cache slot');
|
||||
assert.deepEqual(noWalls.visualScene, withWall.visualScene,
|
||||
'the tile position does not depend on nearby walls');
|
||||
@@ -884,14 +867,16 @@ test('#473 W2 after #724: walls never move a tile, the cache follows the structu
|
||||
const unhostedWalls = (poly, cm) => poly.map((a, index) => ({
|
||||
key: wallKey(a, poly[(index + 1) % poly.length], 1), cm,
|
||||
}));
|
||||
/**
|
||||
* @param {OverlaySceneFixture} [overrides]
|
||||
* @returns {OverlaySceneFixture}
|
||||
*/
|
||||
const overlayInput = (space, structure, overrides = {}) => ({
|
||||
space, structure, openings: [],
|
||||
devices: [{ id: 'device', space: space.id }],
|
||||
view: { x: 0, y: 0, w: 100, h: 100 },
|
||||
display: { showNames: false, cardFontScale: 1 },
|
||||
iconPct: 3.4, deviceBasePct: 3.4, showLqi: false, cellCm: 5,
|
||||
kioskIconScale: 1, kioskFontScale: 1,
|
||||
stageSize: { width: 100, height: 100 },
|
||||
positionOf: () => ({ x: 50, y: 50 }),
|
||||
presentationOf: () => ({
|
||||
scale: 1, valueText: null, valueFullText: '', valueBadge: null,
|
||||
@@ -908,25 +893,28 @@ const overlayInput = (space, structure, overrides = {}) => ({
|
||||
test('#724 AC2: the overlay scene survives zoom, resize and HA state, and is rebuilt when a wall changes', () => {
|
||||
const square = cacheRoom({ wall_ids: [] });
|
||||
const cache = new Map();
|
||||
const structural = (cm) => {
|
||||
const structural = (cm, liveFrame = { x: 0, y: 0, w: 100, h: 100 }) => {
|
||||
const input = structuralInput({ room: square, walls: unhostedWalls(square.poly, cm), coordinateScale: 1 });
|
||||
return { space: input.space, scene: resolveIsoScene({
|
||||
source: createIsoStructuralSource(input), cache, cellCm: 5, liveFrame: { x: 0, y: 0, w: 100, h: 100 },
|
||||
source: createIsoStructuralSource(input), cache, cellCm: 5, liveFrame,
|
||||
}) };
|
||||
};
|
||||
const thin = structural(20);
|
||||
assert.ok(thin.scene.geometry.sides.length > 0, 'fixture has real wall bodies');
|
||||
const live = buildIsoOverlayRenderScene(overlayInput(thin.space, thin.scene.geometry));
|
||||
const live = overlayScene(overlayInput(thin.space, thin.scene.geometry));
|
||||
|
||||
// Same walls: the structural LRU hands out the same geometry, and neither
|
||||
// zoom, a stage resize nor an HA state change is a layout event.
|
||||
// zoom, a stage resize nor an HA state change is a layout event. Zoom and
|
||||
// resize reach 2.5D only as the live frame of the structural scene (#732:
|
||||
// the overlay scene has no view or stage size input).
|
||||
const zoomedIn = structural(20, { x: 20, y: 20, w: 40, h: 40 });
|
||||
assert.strictEqual(zoomedIn.scene.geometry, thin.scene.geometry);
|
||||
assert.notDeepEqual(zoomedIn.scene.frame, thin.scene.frame, 'the fixture really changes the live frame');
|
||||
const zoomed = overlayScene(overlayInput(zoomedIn.space, zoomedIn.scene.geometry));
|
||||
assert.strictEqual(zoomed, live, 'zoom and resize reuse the render scene');
|
||||
const again = structural(20);
|
||||
assert.strictEqual(again.scene.geometry, thin.scene.geometry);
|
||||
const zoomed = buildIsoOverlayRenderScene(overlayInput(again.space, again.scene.geometry, {
|
||||
view: { x: 20, y: 20, w: 40, h: 40 }, stageSize: { width: 320, height: 180 },
|
||||
}));
|
||||
assert.strictEqual(zoomed, live, 'zoom and resize reuse the render scene');
|
||||
const lit = buildIsoOverlayRenderScene(overlayInput(again.space, again.scene.geometry, {
|
||||
const lit = overlayScene(overlayInput(again.space, again.scene.geometry, {
|
||||
presentationOf: () => ({
|
||||
scale: 1, valueText: null, valueFullText: '', tempText: null, humText: null, lqiText: null,
|
||||
valueBadge: { configured: true, enabled: true, text: '100 %', fullText: '100 %', position: 'right', tone: 'default' },
|
||||
@@ -940,7 +928,7 @@ test('#724 AC2: the overlay scene survives zoom, resize and HA state, and is reb
|
||||
const thick = structural(30);
|
||||
assert.notEqual(thick.scene.key, thin.scene.key);
|
||||
assert.notStrictEqual(thick.scene.geometry, thin.scene.geometry);
|
||||
const rebuilt = buildIsoOverlayRenderScene(overlayInput(thick.space, thick.scene.geometry));
|
||||
const rebuilt = overlayScene(overlayInput(thick.space, thick.scene.geometry));
|
||||
assert.notStrictEqual(rebuilt, live, 'a wall edit rebuilds the overlay scene');
|
||||
assert.notStrictEqual(rebuilt.devices.get('device'), live.devices.get('device'),
|
||||
'and its placements');
|
||||
@@ -964,36 +952,33 @@ test('#724 AC2: a room edit that changes the owner is a new structure — no own
|
||||
};
|
||||
const at = { positionOf: () => ({ x: 90, y: 50 }) };
|
||||
const before = structural(100);
|
||||
const west = buildIsoOverlayRenderScene(overlayInput(before.space, before.scene.geometry, at));
|
||||
const west = overlayScene(overlayInput(before.space, before.scene.geometry, at));
|
||||
assert.equal(west.devices.get('device').owner?.id, 'west');
|
||||
const after = structural(80);
|
||||
assert.notEqual(after.scene.key, before.scene.key, 'room geometry is structural');
|
||||
const east = buildIsoOverlayRenderScene(overlayInput(after.space, after.scene.geometry, at));
|
||||
const east = overlayScene(overlayInput(after.space, after.scene.geometry, at));
|
||||
assert.equal(east.devices.get('device').owner?.id, 'east',
|
||||
'the tile at the same point now belongs to the room that grew over it');
|
||||
});
|
||||
|
||||
test('#713 AC3: live zoom never recomputes placement and every tile gets one straight-up shift', () => {
|
||||
test('#713 AC3: every tile gets one straight-up shift from its Flat anchor', () => {
|
||||
// Live zoom is no overlay input (#732): the fixture typecheck rejects a view,
|
||||
// and the #724 AC2 test zooms the structural scene on the production path.
|
||||
const { input } = perfFixture();
|
||||
const lift = projectPlanPoint([0, 0], 0)[1] - projectPlanPoint([0, 0], ISO_WALL_HEIGHT)[1];
|
||||
for (const position of [{ x: 60, y: 20 }, { x: 5, y: 50 }]) {
|
||||
const at = { ...input, positionOf: () => position };
|
||||
const live = buildIsoOverlayRenderScene(at).devices.get('device');
|
||||
const live = overlayScene({ ...input, positionOf: () => position }).devices.get('device');
|
||||
assert.deepEqual(live.floorScene, [position.x, position.y], 'the floor anchor is the Flat point');
|
||||
assert.equal(live.visualScene[0], position.x, 'no horizontal displacement');
|
||||
assert.ok(Math.abs(live.floorScene[1] - live.visualScene[1] - lift) < 1e-9,
|
||||
'the vertical displacement is the wall-top rise for every tile');
|
||||
for (const view of [{ x: 0, y: 0, w: 80, h: 80 }, { x: -20, y: -10, w: 140, h: 140 }]) {
|
||||
const zoomed = buildIsoOverlayRenderScene({ ...at, view }).devices.get('device');
|
||||
assert.strictEqual(zoomed, live, 'zoom and pan are not layout events');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('#713 K5: room names stay on the Flat floor point while devices share the wall-top rise', () => {
|
||||
const { input } = perfFixture();
|
||||
const owner = { ...input.space.rooms[0], name: 'Owner' };
|
||||
const scene = buildIsoOverlayRenderScene({
|
||||
const scene = overlayScene({
|
||||
...input,
|
||||
space: { ...input.space, rooms: [owner] },
|
||||
display: { showNames: true, cardFontScale: 1 },
|
||||
|
||||
@@ -186,6 +186,12 @@ test('#649 п.2 beams: same exterior windows as Flat, parallelogram along the su
|
||||
assert.equal(Math.round(faced.a[1]), 390);
|
||||
});
|
||||
|
||||
/**
|
||||
* #732: an overlay fixture names only fields of the production input type;
|
||||
* test/iso-overlay-fixture-types.test.mjs typechecks this file.
|
||||
* @typedef {{ [K in keyof import('../src/iso-scene-render.js').IsoOverlaySceneInput]?: unknown }} OverlaySceneFixture
|
||||
*/
|
||||
|
||||
test('#649 AC3 layout and collisions see the 2.5D tile size (× ISO_ICON_SCALE)', async () => {
|
||||
const { buildIsoOverlayRenderScene, isoRaisedOverlayHalfSize } = await import('../test-build/iso-scene-render.js');
|
||||
const { iconUnit } = await import('../test-build/space-geometry.js');
|
||||
@@ -195,15 +201,17 @@ test('#649 AC3 layout and collisions see the 2.5D tile size (× ISO_ICON_SCALE)'
|
||||
rooms: [{ id: 'r', poly: [[0, 0], [1000, 0], [1000, 1000], [0, 1000]] }],
|
||||
wall_segments: [], room_drafts: [], partitions: [], wall_columns: [],
|
||||
};
|
||||
const scene = buildIsoOverlayRenderScene({
|
||||
space, devices: [{ id: 'd1' }], openings: [], view: { x: 0, y: 0, w: 1000, h: 1000 },
|
||||
display: { showNames: false, cardFontScale: 1 }, layers: { shadows: true },
|
||||
/** @type {OverlaySceneFixture} */
|
||||
const input = {
|
||||
space, devices: [{ id: 'd1' }], openings: [],
|
||||
display: { showNames: false, cardFontScale: 1 },
|
||||
structure: { topPath: '', topFaces: [], sides: [], contactPath: '', edgeCount: 0 },
|
||||
iconPct: 3, deviceBasePct: 3, showLqi: false, cellCm: 5, kioskIconScale: 1, kioskFontScale: 1,
|
||||
stageSize: { width: 1000, height: 1000 }, positionOf: () => ({ x: 500, y: 500 }),
|
||||
positionOf: () => ({ x: 500, y: 500 }),
|
||||
presentationOf: () => presentation, labelPositionOf: () => ({ x: 0, y: 0 }), labelScaleOf: () => 1,
|
||||
openingEntityAvailable: () => true, openingWallIndex: () => ({ adjacencyEps: 0.1, edges: [] }),
|
||||
});
|
||||
};
|
||||
const scene = buildIsoOverlayRenderScene(input);
|
||||
const footprint = scene.devices.get('d1').footprint;
|
||||
const half = (Math.max(...footprint.map((p) => p[0])) - Math.min(...footprint.map((p) => p[0]))) / 2;
|
||||
const base = 3 * iconUnit(space) / 100;
|
||||
|
||||
@@ -124,16 +124,15 @@ test('Stage 4 composes ordered inert SVG surfaces below screen-facing HTML', ()
|
||||
assert.match(styles, /\.iso-walls-svg\s*\{[\s\S]*?z-index:\s*4/);
|
||||
assert.match(styles, /\.iso-overlays-svg\s*\{\s*z-index:\s*5/);
|
||||
assert.match(styles, /\.devlayer\s*\{[\s\S]*?z-index:\s*6/);
|
||||
const shadowSurface = section(card, '<svg class="iso-shadows-svg"', '<svg class="iso-walls-svg"');
|
||||
const overlaySurface = section(card, '<svg class="iso-overlays-svg"', '</svg>` : nothing}');
|
||||
assert.doesNotMatch(shadowSurface, /renderIsoOverlayGrounds/);
|
||||
assert.ok(overlaySurface.indexOf('isoFrame?.grounds') < overlaySurface.indexOf('isoFrame?.raised'));
|
||||
// #732: the overlay surface stays as the inert camera-viewBox layer the
|
||||
// screen-facing HTML is measured against; nothing renders into it, and the
|
||||
// two renderers that returned an empty SVG are gone with the ground radius.
|
||||
const overlaySurface = section(card, '<svg class="iso-overlays-svg"', '</svg>');
|
||||
assert.match(overlaySurface, /pointer-events="none">$/);
|
||||
assert.doesNotMatch(overlaySurface, /\$\{isoFrame/);
|
||||
const frameResolver = section(sceneRender, 'export function resolveIsoFramePresentation', 'const emptySvg');
|
||||
assert.ok(frameResolver.indexOf('grounds: renderIsoOverlayGrounds')
|
||||
< frameResolver.indexOf('raised: renderIsoRaisedOverlays'));
|
||||
const grounds = section(sceneRender, 'export function renderIsoOverlayGrounds',
|
||||
'export function renderIsoRaisedOverlays');
|
||||
assert.match(grounds, /return emptySvg\(\)/);
|
||||
assert.doesNotMatch(frameResolver, /grounds|raised/);
|
||||
assert.doesNotMatch(sceneRender, /renderIsoOverlayGrounds|renderIsoRaisedOverlays|groundRadius/);
|
||||
assert.doesNotMatch(styles, /perspective\s*:|preserve-3d|rotateX\(|rotateZ\(/);
|
||||
// #649: the 2.5D window light is its own module re-exported through this lazy
|
||||
// graph; the structural renderer itself still paints no light.
|
||||
@@ -261,7 +260,7 @@ test('one frame resolves one structural source and latches late topology/project
|
||||
assert.match(renderBody, /this\._view = null;[\s\S]*?_baseVb\('flat', null\)/);
|
||||
const resolver = section(sceneRender, 'export function resolveIsoFramePresentation', 'const emptySvg');
|
||||
for (const step of ['resolveIsoDecorationLayers', 'resolveIsoOpeningPanels', 'renderIsoUnderlay',
|
||||
'renderIsoShadows', 'renderIsoWalls', 'renderIsoOverlayGrounds', 'renderIsoRaisedOverlays'])
|
||||
'renderIsoShadows', 'renderIsoWalls'])
|
||||
assert.ok(resolver.includes(step), `late Stage 4 step escaped the frame boundary: ${step}`);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user