mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-06 14:39:22 +00:00
feat(iso): the 2.5D floor is the Flat plane, one wall-top rise for tiles (#713)
- Vertical oblique projection: the floor matrix is the identity and a height rises straight up by z·sin 20°, so the on-screen wall height is unchanged and the cos 20° foreshortening of the plan, decor and anchors is gone. - Device tiles and lock badges stand on the wall-top plane with one common shift; the #651 placement search no longer runs in the live scene (its removal is #714). Room names keep their Flat floor point. - The 2.5D fit no longer reserves the 48 CSS px nudge budget. - Switching projection keeps the camera when the previous projection was on screen: saving the setting, entering an editor from 2.5D and adopting a warm memo from the other projection re-read only the scalar zoom. A cold 2.5D start still opens the 2.5D home. - Opening faces are ordered along the oblique projector (s·y + z). Witness: demo/smoke_iso_flat_parity.mjs (AC2–AC5, AC11) is red on the old code. Issue: #713 User-Visible: yes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
@@ -446,7 +446,7 @@ test('sun-ray golden requires browser-painted light from a state-only sun entity
|
||||
assert.ok(scenario);
|
||||
const fixture = prepareGoldenFixture(scenario);
|
||||
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
|
||||
assert.equal(GOLDEN_MATRIX_VERSION, 68);
|
||||
assert.equal(GOLDEN_MATRIX_VERSION, 69);
|
||||
assert.equal(space.settings.sun_rays, true);
|
||||
assert.equal(scenario.northDeg, 90,
|
||||
'the sign-sensitive golden must keep a non-zero north direction');
|
||||
@@ -940,7 +940,7 @@ test('issue 570 Stage 4 reuses the historical iso goldens for visual handoff cov
|
||||
|
||||
test('#673 Stage 6 designer acceptance scenes are canonical golden entries', () => {
|
||||
const ids = STAGE6_ACCEPTANCE_SCENARIOS.map((scenario) => scenario.id);
|
||||
assert.equal(GOLDEN_MATRIX_VERSION, 68);
|
||||
assert.equal(GOLDEN_MATRIX_VERSION, 69);
|
||||
assert.deepEqual(ids, [
|
||||
'isometric-stage6-light-lightfloor',
|
||||
'isometric-stage6-light-darkfloor',
|
||||
|
||||
@@ -347,3 +347,18 @@ test('decoration degradation never removes structure or creates floating panels'
|
||||
structural: true, panels: false, shadows: true, materialNuance: true, floorSymbols: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('#713 AC7: opening depth runs along the oblique projector (s·y + z)', () => {
|
||||
const s = Math.sin(20 * Math.PI / 180);
|
||||
const basis = buildIsoOpeningBasis(opening());
|
||||
const leaf = basis.leaves[0];
|
||||
const top = projectIsoOpening(basis, 0)[0].surfaces.find((surface) => surface.kind === 'leaf-top');
|
||||
assert.ok(top, 'the closed door leaf has a top face');
|
||||
// The four top corners straddle the leaf axis symmetrically, so their mean
|
||||
// plan point is the middle of the closed leaf.
|
||||
const meanY = leaf.hinge[1] + leaf.closedVector[1] / 2;
|
||||
assert.ok(Math.abs(top.cameraDepth - (s * meanY + leaf.top)) < 1e-9,
|
||||
`${top.cameraDepth} != s·y + z = ${s * meanY + leaf.top}`);
|
||||
assert.ok(Math.abs(top.cameraDepth - (s * meanY + leaf.top * Math.cos(20 * Math.PI / 180))) > 1e-3,
|
||||
'the former orthographic key y·sin 20° + z·cos 20° is gone');
|
||||
});
|
||||
|
||||
@@ -10,20 +10,35 @@ import {
|
||||
const close = (actual, expected, epsilon = 1e-9) =>
|
||||
assert.ok(Math.abs(actual - expected) <= epsilon, `${actual} != ${expected}`);
|
||||
|
||||
test('Stage 4 camera is the exact fixed 0°/20° orthographic camera', () => {
|
||||
test('Stage 4 camera is the exact fixed zero-yaw camera with the 20° wall rise', () => {
|
||||
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 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, 4);
|
||||
});
|
||||
|
||||
test('#713 AC1: the 2.5D floor is the Flat plane and height rises straight up', () => {
|
||||
const s = Math.sin(20 * Math.PI / 180);
|
||||
assert.deepEqual(isoPlaneMatrix(0).map((value) => value + 0), [1, 0, 0, 1, 0, 0],
|
||||
'the floor matrix is the identity: no cos 20° foreshortening');
|
||||
for (const point of [[0, 0], [250, 750], [-3000, 4000], [1000, 1000]]) {
|
||||
assert.deepEqual(projectPlanPoint(point, 0), point, 'floor points keep their Flat position');
|
||||
assert.deepEqual(unprojectFloorPoint(point), point, 'floor hit testing is the identity too');
|
||||
for (const z of [ISO_WALL_HEIGHT, 7, 168]) {
|
||||
const lifted = projectPlanPoint(point, z);
|
||||
close(lifted[0], point[0]);
|
||||
close(lifted[1], point[1] - z * s);
|
||||
}
|
||||
}
|
||||
const wall = [300, 400];
|
||||
close(projectPlanPoint(wall, 0)[1] - projectPlanPoint(wall, ISO_WALL_HEIGHT)[1],
|
||||
ISO_WALL_HEIGHT * 0.3420201433256687, 1e-12);
|
||||
close(ISO_WALL_HEIGHT * s, 28.729692039353627, 1e-9);
|
||||
});
|
||||
|
||||
test('floor projection round-trips across the infinite canvas contract', () => {
|
||||
for (const point of [[-5000, -5000], [0, 0], [500, 500], [5000, 5000], [-1234.5, 4321.25]]) {
|
||||
const roundTrip = unprojectFloorPoint(projectPlanPoint(point, 0));
|
||||
@@ -102,7 +117,8 @@ test('client coordinates map through the current scene view', () => {
|
||||
|
||||
test('degenerate cameras and frames throw instead of mixing projections', () => {
|
||||
assert.throws(() => projectPlanPoint([0, 0], 0, { ...ISO_CAMERA, xyScale: 0 }));
|
||||
assert.throws(() => unprojectFloorPoint([0, 0], { ...ISO_CAMERA, tiltDeg: 90 }));
|
||||
assert.throws(() => unprojectFloorPoint([0, 0], { ...ISO_CAMERA, xyScale: 0 }));
|
||||
assert.throws(() => unprojectFloorPoint([Number.NaN, 0]));
|
||||
assert.throws(() => clientToScenePoint([0, 0], { left: 0, top: 0, width: 0, height: 1 },
|
||||
{ x: 0, y: 0, w: 1, h: 1 }));
|
||||
assert.throws(() => isoOverlayVisualHeight(-1));
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
ISO_OVERLAY_VISUAL_OFFSET,
|
||||
ISO_RAISED_OVERLAY_HEIGHT,
|
||||
ISO_WALL_HEIGHT,
|
||||
projectPlanPoint,
|
||||
unprojectFloorPoint,
|
||||
} from '../test-build/iso-projection.js';
|
||||
|
||||
@@ -140,7 +141,7 @@ test('overlay bounds use final screen footprint and canonical owner filtering',
|
||||
assert.deepEqual(isoOverlaySceneBounds(scene), { x: 15, y: 27, w: 195, h: 281 });
|
||||
});
|
||||
|
||||
test('stable overlay fit contains a maximum final nudge without zoom feedback', () => {
|
||||
test('#713 K8: overlay fit is the structure plus visible tiles, no #651 nudge reserve', () => {
|
||||
const entry = {
|
||||
id: 'edge', kind: 'device', groundRadius: 1, screenHalfSize: [10, 8],
|
||||
placement: {
|
||||
@@ -163,17 +164,9 @@ test('stable overlay fit contains a maximum final nudge without zoom feedback',
|
||||
baseBounds: { x: 0, y: 0, w: 100, h: 100 }, entries: [entry], stageSize, targetView,
|
||||
});
|
||||
assert.ok(fitted);
|
||||
const unitsPerPixel = Math.max(
|
||||
fitted.view.w / stageSize.width, fitted.view.h / stageSize.height,
|
||||
);
|
||||
const dx = 48 * unitsPerPixel;
|
||||
const actual = { entries: [{ ...entry, placement: {
|
||||
...entry.placement, visualScene: [95 + dx, 50], nudgeScene: [dx, 0],
|
||||
footprint: entry.placement.footprint.map((point) => [point[0] + dx, point[1]]),
|
||||
} }] };
|
||||
const final = isoOverlaySceneBounds(actual);
|
||||
assert.ok(final.x >= fitted.bounds.x - 1e-7
|
||||
&& final.x + final.w <= fitted.bounds.x + fitted.bounds.w + 1e-7);
|
||||
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,
|
||||
});
|
||||
@@ -400,7 +393,11 @@ test('opening lock without a canonical host owner never guesses from point conta
|
||||
const placement = scene.locks.get('partition-door');
|
||||
assert.equal(placement?.owner, null);
|
||||
assert.equal(placement?.nudged, false);
|
||||
assert.equal(placement?.reason, 'missing-owner');
|
||||
// #713: no placement search runs, so a missing owner is no longer a search
|
||||
// failure; the badge simply stands on the wall-top plane above its anchor.
|
||||
assert.equal(placement?.reason, null);
|
||||
assert.equal(placement?.plane, 'raised');
|
||||
assert.deepEqual(placement?.visualScene, projectPlanPoint(placement.floorAnchor, ISO_WALL_HEIGHT));
|
||||
assert.equal(placement?.tether.visible, false);
|
||||
});
|
||||
|
||||
@@ -446,8 +443,11 @@ test('Stage 4 reuses pure overlay placements and fit probes skip collision searc
|
||||
'an unchanged frame reuses the exact render-scene snapshot for Lit guards');
|
||||
assert.strictEqual(repeated.devices.get('device'), live.devices.get('device'),
|
||||
'unchanged HA/render passes reuse the exact pure placement result');
|
||||
assert.equal(live.devices.get('device')?.nearWallBefore, true);
|
||||
assert.equal(live.devices.get('device')?.cleared, true);
|
||||
// #713: the tile stands on the wall-top plane right above its anchor even
|
||||
// next to a wall; no wall test and no nudge in the live scene.
|
||||
assert.equal(live.devices.get('device')?.nearWallBefore, false);
|
||||
assert.equal(live.devices.get('device')?.nudged, false);
|
||||
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 },
|
||||
@@ -458,8 +458,8 @@ test('Stage 4 reuses pure overlay placements and fit probes skip collision searc
|
||||
const fit = buildIsoOverlayRenderScene({ ...input, resolveCollisions: false });
|
||||
assert.equal(fit.devices.get('device')?.nearWallBefore, false,
|
||||
'fit envelope uses unnudged bounds without running wall collision search');
|
||||
assert.notStrictEqual(fit.devices.get('device'), live.devices.get('device'),
|
||||
'fit and live placements use separate bounded cache entries');
|
||||
assert.deepEqual(fit.devices.get('device')?.visualScene, live.devices.get('device')?.visualScene,
|
||||
'#713: fit and live placements agree — there is no search to differ by');
|
||||
|
||||
const zoomed = buildIsoOverlayRenderScene({ ...input, view: { x: -10, y: -10, w: 120, h: 120 } });
|
||||
assert.strictEqual(zoomed, live,
|
||||
@@ -940,46 +940,49 @@ test('#570 supersedes #473 W1: selection reuses the cue-free low placement', ()
|
||||
assert.strictEqual(again, selected, 'clearing selection keeps the same immutable placement');
|
||||
});
|
||||
|
||||
test('#473 W2: кэш размещений привязан к идентичности массива силуэтов', () => {
|
||||
test('#473 W2 after #713: walls no longer move a tile, the cache still follows the silhouette array', () => {
|
||||
const { input } = perfFixture();
|
||||
const withWall = buildIsoOverlayRenderScene(input).devices.get('device');
|
||||
assert.equal(withWall.nearWallBefore, true, 'фикстура: стена рядом с плитой');
|
||||
// Новая геометрия — новый массив. Кэш, ключуемый константой, отдал бы
|
||||
// размещение «у стены» для плана, в котором стены больше нет.
|
||||
const noWalls = buildIsoOverlayRenderScene({ ...input, wallSilhouettes: [] }).devices.get('device');
|
||||
assert.equal(noWalls.nearWallBefore, false, 'без стен плита не у стены');
|
||||
assert.notStrictEqual(noWalls, withWall);
|
||||
assert.notStrictEqual(noWalls, withWall, 'a new geometry array is a new cache slot');
|
||||
assert.deepEqual(noWalls.visualScene, withWall.visualScene,
|
||||
'the tile position does not depend on nearby walls');
|
||||
assert.equal(withWall.nearWallBefore, false);
|
||||
assert.equal(withWall.nudged, false);
|
||||
});
|
||||
|
||||
test('#651 supersedes #473 W3: live zoom never recomputes scene placement', () => {
|
||||
test('#713 AC3: live zoom never recomputes placement and every tile gets one straight-up shift', () => {
|
||||
const { input } = perfFixture();
|
||||
// Далёкая плита: зум внутрь переиспользует доказанно безопасное размещение.
|
||||
const far = { ...input, positionOf: () => ({ x: 60, y: 20 }) };
|
||||
const farLive = buildIsoOverlayRenderScene(far).devices.get('device');
|
||||
assert.equal(farLive.nearWallBefore, false);
|
||||
const farZoomed = buildIsoOverlayRenderScene({ ...far, view: { x: 0, y: 0, w: 80, h: 80 } })
|
||||
.devices.get('device');
|
||||
assert.strictEqual(farZoomed, farLive, 'не у стены — переиспользуется');
|
||||
// Плита у стены, которую не удалось очистить, также остаётся в тех же
|
||||
// координатах сцены. Иначе экранный zoom снова становится layout-событием
|
||||
// и возвращает пользовательский дрейф #651.
|
||||
const pinned = {
|
||||
...input,
|
||||
// упор со всех сторон: узкая комната не даёт места для nudge
|
||||
space: { ...input.space, rooms: [room('owner', 0, 44, 12, 56)] },
|
||||
positionOf: () => ({ x: 5, y: 50 }),
|
||||
};
|
||||
const pinnedLive = buildIsoOverlayRenderScene(pinned).devices.get('device');
|
||||
assert.equal(pinnedLive.nearWallBefore, true);
|
||||
if (pinnedLive.cleared) {
|
||||
// Фикстура не смогла создать неочищенную плиту — тест обязан сказать об
|
||||
// этом честно, а не пройти молча (правило после #426).
|
||||
assert.fail('фикстура «у стены, не очищена» не построилась: cleared=true');
|
||||
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');
|
||||
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');
|
||||
}
|
||||
}
|
||||
const pinnedZoomed = buildIsoOverlayRenderScene({ ...pinned, view: { x: 0, y: 0, w: 80, h: 80 } })
|
||||
.devices.get('device');
|
||||
assert.strictEqual(pinnedZoomed, pinnedLive,
|
||||
'у стены и не очищена — тот же структурный layout, без zoom-feedback');
|
||||
});
|
||||
|
||||
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({
|
||||
...input,
|
||||
space: { ...input.space, rooms: [owner] },
|
||||
display: { showNames: true, cardFontScale: 1 },
|
||||
labelPositionOf: () => ({ x: 30, y: 40 }),
|
||||
});
|
||||
const label = scene.rooms.get(owner);
|
||||
assert.ok(label, 'the room label is placed');
|
||||
assert.deepEqual(label.visualScene, [30, 40], 'the name keeps its Flat position');
|
||||
assert.equal(label.nudged, false);
|
||||
const device = scene.devices.get('device');
|
||||
assert.deepEqual(device.visualScene, projectPlanPoint([5, 50], ISO_WALL_HEIGHT));
|
||||
});
|
||||
|
||||
test('#473 W4: AABB-отсечение учитывает зазор безопасности', () => {
|
||||
|
||||
@@ -147,11 +147,15 @@ test('Stage 4 keeps device, room and lock roots on one corrected low visual plan
|
||||
'_renderSunRays(space)', '_renderOpenings(disp)',
|
||||
'_renderVacuums(this._renderVacuumDevices, view, space.id)',
|
||||
]) assert.ok(card.includes(renderer), `missing ${renderer}`);
|
||||
assert.match(card, /const point = isoPlacement\?\.visualScene \?\? this\._scenePoint\(\[pos\.x, pos\.y\]\)/);
|
||||
assert.match(card, /const point = isoPlacement\?\.visualScene \?\? this\._scenePoint\(\[p\.x, p\.y\]\)/);
|
||||
assert.match(card, /const point = isoPlacement\?\.visualScene \?\? this\._scenePoint\(floorAnchor\)/);
|
||||
// #713: the 2.5D floor is the Flat plane, so a floor anchor is its own scene point.
|
||||
assert.match(card, /const point = isoPlacement\?\.visualScene \?\? \[pos\.x, pos\.y\]/);
|
||||
assert.match(card, /const point = isoPlacement\?\.visualScene \?\? \[p\.x, p\.y\]/);
|
||||
assert.match(card, /const point = isoPlacement\?\.visualScene \?\? floorAnchor/);
|
||||
assert.doesNotMatch(card, /_scenePoint/);
|
||||
assert.doesNotMatch(readFileSync(new URL('../src/live-editor.ts', import.meta.url), 'utf8'), /_scenePoint/,
|
||||
'the live editor paints a dragged marker at its Flat point too');
|
||||
const vacuum = section(card, 'private _renderVacuums(', 'private _renderDevice(');
|
||||
assert.match(vacuum, /const point = this\._scenePoint\(\[cx, cy\]\)/);
|
||||
assert.match(vacuum, /const point = \[cx, cy\]/);
|
||||
assert.doesNotMatch(vacuum, /visualScene|raisedScene|resolveIsoOverlayPlacement/);
|
||||
for (const kind of ['device', 'room-label', 'opening-lock'])
|
||||
assert.match(card, new RegExp(`data-hp-iso-overlay-kind=\\$\\{isoPlacement\\?\\.plane === 'raised' \\? '${kind}'`));
|
||||
@@ -162,7 +166,7 @@ test('Stage 4 structural cache fingerprints geometry/camera/heights and excludes
|
||||
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: 5/);
|
||||
assert.match(sceneRender, /camera: ISO_CAMERA,[\s\S]*?wallHeight,[\s\S]*?raisedHeight,[\s\S]*?floorEdgeHeight,[\s\S]*?algorithm: 6/);
|
||||
const source = section(sceneRender, 'export function createIsoStructuralSource', 'const unknownArray');
|
||||
const roomProjection = section(sceneRender,
|
||||
'export function isoStructuralRoomGeometry', 'export type IsoStructuralOpeningHost');
|
||||
@@ -204,16 +208,18 @@ test('Stage 4 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 zero-yaw floor matrix and removes every volume cue', () => {
|
||||
test('show_borders:false keeps the Flat floor plane and removes every volume cue', () => {
|
||||
assert.match(card, /isoLayers && !isoLayers\.floorSymbols/);
|
||||
assert.match(card, /<svg class="plan-svg"[\s\S]*?data-hp-live-viewbox=\$\{iso \? 'camera' : 'floor'\}/);
|
||||
assert.match(card, /transform=\$\{iso \? isoFloorMatrixCss\(\) : nothing\}/);
|
||||
// #713: the floor group carries no transform in either branch.
|
||||
assert.match(card, /<g class=\$\{iso \? 'iso-floor-scene' : nothing\}>/);
|
||||
assert.doesNotMatch(card, /isoFloorMatrixCss|unprojectFloorPoint/);
|
||||
assert.match(card,
|
||||
/\$\{litCache\(iso && isoLayers\?\.structural \? svg`<svg class="iso-shadows-svg"/);
|
||||
assert.match(card, /if \(!runtime \|\| !layers\?\.structural \|\| !structural\) return null/);
|
||||
const baseView = section(card, 'private _baseVb(', '/** How many objects');
|
||||
assert.match(baseView, /if \(!this\._spaceDisplayForRender\(\)\.showBorders\)[\s\S]*?projectedFrame\(\{[\s\S]*?wallHeight:\s*0/);
|
||||
const effective = section(card, 'private _effectiveProjection()', 'private _scenePoint');
|
||||
const effective = section(card, 'private _effectiveProjection()', 'private _floorView');
|
||||
assert.ok(effective.indexOf("if (!this._spaceDisplayForRender().showBorders)")
|
||||
< effective.indexOf('const source = this._isoSource()'));
|
||||
assert.doesNotMatch(baseView, /ISO_WALL_HEIGHT|raisedHeight|floorDepth/);
|
||||
@@ -221,7 +227,7 @@ test('show_borders:false keeps the exact zero-yaw floor matrix and removes every
|
||||
});
|
||||
|
||||
test('one frame resolves one structural source and latches late topology/projection failures', () => {
|
||||
const effective = section(card, 'private _effectiveProjection()', 'private _scenePoint');
|
||||
const effective = section(card, 'private _effectiveProjection()', 'private _floorView');
|
||||
assert.equal([...effective.matchAll(/this\._isoSource\(\)/g)].length, 1);
|
||||
assert.match(effective, /this\._isoScene\(source\)/);
|
||||
assert.match(effective, /this\._isoFallback\.has\(key\)[\s\S]*?try \{/,
|
||||
|
||||
Reference in New Issue
Block a user