mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 06:12:18 +00:00
Release hidden CCTV geometry and excuse level-of-detail path counts
CCTV kept the coverage and projection geometry of every camera the opening view passed on its way to the restored one; after each visibility update it now releases the coverage of cameras out of view and the planes of inactive cameras, keeping decoded media for reselection. The cold-start comparison accepts a field entry in the ledger allowlist that excuses one field of one set with a reason, and uses it for the infrastructure footprint counts: that level of detail keeps features chosen on the way to a view so they do not flicker while panning, which a cold start cannot reproduce. Presence checks ignore field entries. The fixtures give recent imagery a matching scene and date and traffic its deterministic starts, and traffic counts as drawn without every loaded vehicle visible.
This commit is contained in:
1 parent
ab41c765cc
commit
34b4fdce57
10 files changed
+312
-44
No files matched your search
@@ -1 +1,14 @@
|
|||||||
[]
|
[
|
||||||
|
{
|
||||||
|
"id": "local-dams-footprints",
|
||||||
|
"family": "ground",
|
||||||
|
"field": "count",
|
||||||
|
"reason": "Infrastructure level of detail keeps features chosen on the way to a view (incumbents and eviction grace, so footprints do not flicker while panning); a cold start reaches the view without that path."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "local-datacenters-footprints",
|
||||||
|
"family": "ground",
|
||||||
|
"field": "count",
|
||||||
|
"reason": "Infrastructure level of detail keeps features chosen on the way to a view (incumbents and eviction grace, so footprints do not flicker while panning); a cold start reaches the view without that path."
|
||||||
|
}
|
||||||
|
]
|
||||||
+25
-11
@@ -108,7 +108,7 @@ export function coldStartReady(layers, cold = false) {
|
|||||||
stats.count > 0 &&
|
stats.count > 0 &&
|
||||||
movers.some(
|
movers.some(
|
||||||
(row) =>
|
(row) =>
|
||||||
row.id === 'traffic-fill' && row.count === stats.count && row.shown,
|
row.id === 'traffic-fill' && row.count > 0 && row.shown,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
if (['flights', 'military', 'transit'].includes(id))
|
if (['flights', 'military', 'transit'].includes(id))
|
||||||
@@ -317,16 +317,30 @@ export function compareColdStart(
|
|||||||
const next = cold.ledger.find(
|
const next = cold.ledger.find(
|
||||||
(other) => other.family === row.family && other.id === row.id,
|
(other) => other.family === row.family && other.id === row.id,
|
||||||
);
|
);
|
||||||
if (next)
|
if (next) {
|
||||||
add(
|
// A field entry excuses one field of one set, with its reason.
|
||||||
layerOf(row.family, row.id),
|
const rows = compareProbe(
|
||||||
row.family,
|
{ kind: row.kind, count: row.count },
|
||||||
row.id,
|
{ kind: next.kind, count: next.count },
|
||||||
compareProbe(
|
).filter((difference) => {
|
||||||
{ kind: row.kind, count: row.count },
|
const exception = allow.find(
|
||||||
{ kind: next.kind, count: next.count },
|
(entry) =>
|
||||||
),
|
entry.field === difference.field &&
|
||||||
);
|
entry.id === row.id &&
|
||||||
|
(!entry.family || entry.family === row.family),
|
||||||
|
);
|
||||||
|
if (exception)
|
||||||
|
skipped.push({
|
||||||
|
layer: layerOf(row.family, row.id),
|
||||||
|
family: row.family,
|
||||||
|
id: row.id,
|
||||||
|
...difference,
|
||||||
|
reason: exception.reason,
|
||||||
|
});
|
||||||
|
return !exception;
|
||||||
|
});
|
||||||
|
add(layerOf(row.family, row.id), row.family, row.id, rows);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const layer of new Set([
|
for (const layer of new Set([
|
||||||
...Object.keys(switched.layers),
|
...Object.keys(switched.layers),
|
||||||
|
|||||||
@@ -406,5 +406,47 @@ test('readiness waits for fixture publications, LOD and the locator reveal', ()
|
|||||||
assert.equal(ready('transit'), false);
|
assert.equal(ready('transit'), false);
|
||||||
bus.set(0, { show: true });
|
bus.set(0, { show: true });
|
||||||
assert.equal(ready('transit'), true);
|
assert.equal(ready('transit'), true);
|
||||||
|
entry('traffic');
|
||||||
|
assert.equal(ready('traffic'), false);
|
||||||
|
const cars = engine.movers.createSet({
|
||||||
|
id: 'traffic-fill',
|
||||||
|
kind: 'point',
|
||||||
|
capacity: 10,
|
||||||
|
point: { sizePx: 4 },
|
||||||
|
});
|
||||||
|
cars.setCount(9);
|
||||||
|
assert.equal(ready('traffic'), true, 'fading vehicles need not all draw');
|
||||||
|
for (let i = 0; i < 9; i++) cars.set(i, { show: false });
|
||||||
|
assert.equal(ready('traffic'), false, 'hidden traffic is not ready');
|
||||||
|
for (let i = 0; i < 9; i++) cars.set(i, { show: true });
|
||||||
|
dataManager.layers.get('traffic').module.getStats = () => ({
|
||||||
|
count: 10,
|
||||||
|
loading: true,
|
||||||
|
});
|
||||||
|
assert.equal(ready('traffic'), false, 'population admission must finish');
|
||||||
engine.destroy();
|
engine.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a field entry excuses only that field of that set and never a missing set', () => {
|
||||||
|
const a = snapshot(),
|
||||||
|
b = snapshot();
|
||||||
|
b.ledger[0].count = 2;
|
||||||
|
const allow = [
|
||||||
|
{ id: 'radio-selected', family: 'movers', field: 'count', reason: 'path' },
|
||||||
|
];
|
||||||
|
const excused = compareColdStart(a, b, { ...options, allow });
|
||||||
|
assert.deepEqual(
|
||||||
|
excused.differences.filter((row) => row.family === 'movers'),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
assert.ok(excused.skipped.some((row) => row.reason === 'path'));
|
||||||
|
// The radio marker is declared session state; compare it as plain content.
|
||||||
|
b.ledger = [];
|
||||||
|
assert.ok(
|
||||||
|
compareColdStart(a, b, {
|
||||||
|
...options,
|
||||||
|
allow,
|
||||||
|
sessionState: false,
|
||||||
|
}).differences.some((row) => row.field === 'present'),
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -25,9 +25,11 @@ export async function coldStartFixtures(origin, group) {
|
|||||||
const epoch =
|
const epoch =
|
||||||
group === 'space'
|
group === 'space'
|
||||||
? SATELLITE_FIXTURE_EPOCH
|
? SATELLITE_FIXTURE_EPOCH
|
||||||
: group === 'urban'
|
: group === 'imagery'
|
||||||
? TRANSIT_EPOCH
|
? Date.parse('2026-10-29T12:00:00Z')
|
||||||
: Date.now();
|
: group === 'urban'
|
||||||
|
? TRANSIT_EPOCH
|
||||||
|
: Date.now();
|
||||||
const png = await sharp({
|
const png = await sharp({
|
||||||
create: { width: 256, height: 256, channels: 4, background: '#40506a' },
|
create: { width: 256, height: 256, channels: 4, background: '#40506a' },
|
||||||
})
|
})
|
||||||
@@ -298,6 +300,9 @@ function installFeeds({ group, epoch, groups, ghost, launches, firms }) {
|
|||||||
return Math.floor(now());
|
return Math.floor(now());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// Mid-road starts keep the traffic population visible through birth fades
|
||||||
|
// without random road-end recycling during the comparison.
|
||||||
|
if (group === 'traffic') Math.random = () => 0.5;
|
||||||
localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
|
localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
|
||||||
window.__coldStartUnexpectedApi = [];
|
window.__coldStartUnexpectedApi = [];
|
||||||
const original = window.fetch.bind(window);
|
const original = window.fetch.bind(window);
|
||||||
@@ -315,6 +320,23 @@ function installFeeds({ group, epoch, groups, ghost, launches, firms }) {
|
|||||||
window.fetch = (input, options) => {
|
window.fetch = (input, options) => {
|
||||||
const url = new URL(input.url ?? input, location.href),
|
const url = new URL(input.url ?? input, location.href),
|
||||||
path = url.pathname;
|
path = url.pathname;
|
||||||
|
// The image fixture also needs the snapshot API's explicit pixel proof.
|
||||||
|
if (
|
||||||
|
group === 'imagery' &&
|
||||||
|
url.hostname === 'wvs.earthdata.nasa.gov' &&
|
||||||
|
path.startsWith('/api/v1/snapshot')
|
||||||
|
)
|
||||||
|
return original(input, options).then((response) =>
|
||||||
|
response.ok
|
||||||
|
? new Response(response.body, {
|
||||||
|
status: response.status,
|
||||||
|
headers: {
|
||||||
|
...Object.fromEntries(response.headers),
|
||||||
|
'Data-Present': 'true',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: response,
|
||||||
|
);
|
||||||
if (path === '/api/flights')
|
if (path === '/api/flights')
|
||||||
return json({
|
return json({
|
||||||
time: epoch / 1000,
|
time: epoch / 1000,
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { parseSatcatCsv } from '../../src/layers/satellites/classes.js';
|
|||||||
import { createFakeEngine } from '../../src/globe/testing/fakeEngine.js';
|
import { createFakeEngine } from '../../src/globe/testing/fakeEngine.js';
|
||||||
import { createTrafficDisplay } from '../../src/layers/traffic/render/display.js';
|
import { createTrafficDisplay } from '../../src/layers/traffic/render/display.js';
|
||||||
import { createAnimation } from '../../src/layers/traffic/animation.js';
|
import { createAnimation } from '../../src/layers/traffic/animation.js';
|
||||||
|
import { createRecentImagerySource } from '../../src/layers/recentImagery/source.js';
|
||||||
|
import { createThumbnailLoader } from '../../src/layers/recentImagery/thumbnails.js';
|
||||||
|
|
||||||
const origin = 'http://127.0.0.1:4309';
|
const origin = 'http://127.0.0.1:4309';
|
||||||
test('every group has valid strict fixture routes', async () => {
|
test('every group has valid strict fixture routes', async () => {
|
||||||
@@ -221,48 +223,81 @@ test('traffic uses deterministic road and flow tiles', async () => {
|
|||||||
assert.ok((await (await page.fetch(url)).arrayBuffer()).byteLength > 0);
|
assert.ok((await (await page.fetch(url)).arrayBuffer()).byteLength > 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('traffic dots finish their birth fade with the fixture clock', async (t) => {
|
test('imagery fixtures admit a recent S30 scene through the real thumbnail loader', async (t) => {
|
||||||
|
const page = await installedPage('imagery');
|
||||||
|
const source = createRecentImagerySource({ fetchImpl: page.fetch });
|
||||||
|
const box = { west: -97.77, east: -97.71, south: 30.25, north: 30.29 };
|
||||||
|
const now = new Date(page.fixture.epoch);
|
||||||
|
const { candidates } = await source.searchHls({ box, now });
|
||||||
|
const candidate = candidates.find((row) => row.product === 'S30');
|
||||||
|
assert.ok(candidate);
|
||||||
|
assert.ok(now - Date.parse(candidate.day) < 3 * 86_400_000);
|
||||||
|
const loader = createThumbnailLoader({
|
||||||
|
requestThumbnail: source.getThumbnail,
|
||||||
|
createObjectUrl: () => 'blob:fixture',
|
||||||
|
revokeObjectUrl() {},
|
||||||
|
});
|
||||||
|
t.after(() => loader.destroy());
|
||||||
|
const finished = new Promise((resolve) => {
|
||||||
|
loader.subscribe((key) => {
|
||||||
|
const state = loader.get(key);
|
||||||
|
if (!state.loading && state.status !== 'unknown') resolve(state);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
loader.request(candidate, box);
|
||||||
|
const state = await finished;
|
||||||
|
assert.equal(state.status, 'present');
|
||||||
|
assert.equal(state.objectUrl, 'blob:fixture');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('traffic fixture dots finish their birth fade and stay visible during comparison', async (t) => {
|
||||||
const page = await installedPage('traffic');
|
const page = await installedPage('traffic');
|
||||||
t.mock.method(Date, 'now', () => vm.runInContext('Date.now()', page.context));
|
t.mock.method(Date, 'now', () => vm.runInContext('Date.now()', page.context));
|
||||||
const engine = createFakeEngine();
|
t.mock.method(Math, 'random', () =>
|
||||||
|
vm.runInContext('Math.random()', page.context),
|
||||||
|
);
|
||||||
|
const engine = createFakeEngine({ altitude: 1800 });
|
||||||
const display = createTrafficDisplay(engine);
|
const display = createTrafficDisplay(engine);
|
||||||
t.after(() => {
|
t.after(() => {
|
||||||
display.destroy();
|
display.destroy();
|
||||||
engine.destroy();
|
engine.destroy();
|
||||||
});
|
});
|
||||||
const point = {
|
|
||||||
position: { x: 6378137, y: 0, z: 0 },
|
|
||||||
show: true,
|
|
||||||
pixelSize: 6,
|
|
||||||
color: { red: 1, green: 1, blue: 1, alpha: 1 },
|
|
||||||
outlineWidth: 0,
|
|
||||||
};
|
|
||||||
const dot = {
|
|
||||||
point,
|
|
||||||
born: Date.now(),
|
|
||||||
nominalSize: 6,
|
|
||||||
direction: 1,
|
|
||||||
segIdx: 0,
|
|
||||||
numSegments: 1,
|
|
||||||
t: 0.5,
|
|
||||||
mps: 1,
|
|
||||||
stoppedUntil: 0,
|
|
||||||
segmentDist: [1000],
|
|
||||||
waypoints: [point.position, { x: 6378137, y: 1000, z: 0 }],
|
|
||||||
};
|
|
||||||
const animation = createAnimation({
|
const animation = createAnimation({
|
||||||
state: {
|
state: {
|
||||||
_enabled: true,
|
_enabled: true,
|
||||||
_renderHeld: true,
|
_renderHeld: true,
|
||||||
_engine: engine,
|
_engine: engine,
|
||||||
_display: display,
|
_display: display,
|
||||||
_dots: [dot],
|
_dots: [],
|
||||||
|
_scratchLerp: {},
|
||||||
|
_nextDotId: 1,
|
||||||
|
_speedScale: 1,
|
||||||
|
_fadeScaleFar: 100000,
|
||||||
|
_fadeTransFar: 100000,
|
||||||
|
_bucketCounts: { sim: 0 },
|
||||||
|
_motion: { added: 0, recycled: 0 },
|
||||||
_lastAnimTime: Date.now(),
|
_lastAnimTime: Date.now(),
|
||||||
_animFrame: 0,
|
_animFrame: 0,
|
||||||
},
|
},
|
||||||
services: {},
|
services: {},
|
||||||
parts: { rendering: {}, retention: { easeHeights() {} } },
|
parts: {
|
||||||
|
rendering: {},
|
||||||
|
retention: { easeHeights() {} },
|
||||||
|
style: { baseDotSize: () => 6, activeSizeDelta: () => 0 },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
animation.spawnDotsForRoad(
|
||||||
|
{
|
||||||
|
type: 'primary',
|
||||||
|
segmentDist: [10000],
|
||||||
|
waypoints: [
|
||||||
|
{ x: 6378137, y: 0, z: 0 },
|
||||||
|
{ x: 6378137, y: 10000, z: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
1800,
|
||||||
|
10,
|
||||||
|
);
|
||||||
animation.animate();
|
animation.animate();
|
||||||
animation.drawDots();
|
animation.drawDots();
|
||||||
assert.equal(
|
assert.equal(
|
||||||
@@ -274,6 +309,15 @@ test('traffic dots finish their birth fade with the fixture clock', async (t) =>
|
|||||||
animation.drawDots();
|
animation.drawDots();
|
||||||
assert.equal(
|
assert.equal(
|
||||||
engine.movers.list().find(({ id }) => id === 'traffic-fill').count,
|
engine.movers.list().find(({ id }) => id === 'traffic-fill').count,
|
||||||
1,
|
10,
|
||||||
);
|
);
|
||||||
|
for (let i = 0; i < 300; i++) {
|
||||||
|
page.advance(100);
|
||||||
|
animation.animate();
|
||||||
|
animation.drawDots();
|
||||||
|
assert.equal(
|
||||||
|
engine.movers.list().find(({ id }) => id === 'traffic-fill').count,
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
@@ -42,7 +42,9 @@ export function compareLedgers(before, after, allow = []) {
|
|||||||
if (b.has(key)) return [];
|
if (b.has(key)) return [];
|
||||||
const exception = allow.find(
|
const exception = allow.find(
|
||||||
(entry) =>
|
(entry) =>
|
||||||
entry.id === row.id && (!entry.family || entry.family === row.family),
|
!entry.field &&
|
||||||
|
entry.id === row.id &&
|
||||||
|
(!entry.family || entry.family === row.family),
|
||||||
);
|
);
|
||||||
if (exception) {
|
if (exception) {
|
||||||
allowed.push({
|
allowed.push({
|
||||||
|
|||||||
@@ -818,8 +818,11 @@ test('real active monitor plane owns one protected host label and no native labe
|
|||||||
showProjection: false,
|
showProjection: false,
|
||||||
});
|
});
|
||||||
refreshCoverageStyles();
|
refreshCoverageStyles();
|
||||||
assert.equal(runtime.planeEntity.show, false);
|
assert.equal(runtime.planeEntity, null);
|
||||||
assert.equal(primitive(runtime.planeEntity.id).show, false);
|
assert.equal(
|
||||||
|
engine.lines.list().some((line) => line.id.endsWith('-plane-outline')),
|
||||||
|
false,
|
||||||
|
);
|
||||||
assert.deepEqual(calls.slice(-2), [
|
assert.deepEqual(calls.slice(-2), [
|
||||||
['clear', 'cctv-projection'],
|
['clear', 'cctv-projection'],
|
||||||
['visible', 'cctv-projection', false],
|
['visible', 'cctv-projection', false],
|
||||||
@@ -1210,7 +1213,7 @@ test('CCTV deselect publishes one null state and leaves the complete camera pose
|
|||||||
assert.equal(publications.at(-1).activeCameraId, null);
|
assert.equal(publications.at(-1).activeCameraId, null);
|
||||||
assert.equal(publications.at(-1).activeCamera, null);
|
assert.equal(publications.at(-1).activeCamera, null);
|
||||||
assert.equal(publications.at(-1).enabled, true);
|
assert.equal(publications.at(-1).enabled, true);
|
||||||
assert.equal(runtime.planeEntity.show, false);
|
assert.equal(runtime.planeEntity, null);
|
||||||
assert.equal(record.probeClampRangeM, null);
|
assert.equal(record.probeClampRangeM, null);
|
||||||
assert.equal(record.activationDone, false);
|
assert.equal(record.activationDone, false);
|
||||||
assert.deepEqual(engine.camera.pose(), pose);
|
assert.deepEqual(engine.camera.pose(), pose);
|
||||||
|
|||||||
@@ -744,6 +744,32 @@ export function createGeometry({ state: layerState, services, parts, source }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function releaseInactiveGeometry(activeId, visibleIds) {
|
||||||
|
const removed = new Set();
|
||||||
|
for (const record of layerState._records) {
|
||||||
|
if (record.camera.id === activeId) continue;
|
||||||
|
if (!visibleIds.has(record.camera.id)) {
|
||||||
|
for (const entity of record.coverageEntities || []) {
|
||||||
|
parts.renderer.removeCoverageEntity(entity);
|
||||||
|
removed.add(entity);
|
||||||
|
}
|
||||||
|
record.coverageEntities = [];
|
||||||
|
}
|
||||||
|
const runtime = record.projection;
|
||||||
|
if (runtime?.planeEntity) {
|
||||||
|
// Keep decoded media for reselection, but only the active plane draws.
|
||||||
|
parts.renderer.removePlane(runtime);
|
||||||
|
runtime.planeEntity = runtime.planeMaterial = null;
|
||||||
|
runtime.planeVisible = false;
|
||||||
|
runtime.needsPlane = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (removed.size)
|
||||||
|
layerState._coverageEntities = layerState._coverageEntities.filter(
|
||||||
|
(entity) => !removed.has(entity),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes all coverage entities and projection runtimes from the scene.
|
* Removes all coverage entities and projection runtimes from the scene.
|
||||||
*/
|
*/
|
||||||
@@ -790,6 +816,7 @@ export function createGeometry({ state: layerState, services, parts, source }) {
|
|||||||
registerCoverageEntities,
|
registerCoverageEntities,
|
||||||
ensureActiveCoverageEntities,
|
ensureActiveCoverageEntities,
|
||||||
ensureVisibleCoverageEntities,
|
ensureVisibleCoverageEntities,
|
||||||
|
releaseInactiveGeometry,
|
||||||
destroyCoverageEntities,
|
destroyCoverageEntities,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -426,6 +426,7 @@ export function createRendering({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parts.geometry.releaseInactiveGeometry(activeId, coverageVisible);
|
||||||
parts.renderer?.setMarkersVisible(!!layerState._enabled);
|
parts.renderer?.setMarkersVisible(!!layerState._enabled);
|
||||||
parts.projection.pauseInactiveProjectionFeeds(activeId);
|
parts.projection.pauseInactiveProjectionFeeds(activeId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { createRendering } from './rendering.js';
|
import { createRendering } from './rendering.js';
|
||||||
|
import { createGeometry } from './geometry.js';
|
||||||
import {
|
import {
|
||||||
ACTIVE_CAMERA_COLOR,
|
ACTIVE_CAMERA_COLOR,
|
||||||
IDLE_CAMERA_COLOR,
|
IDLE_CAMERA_COLOR,
|
||||||
@@ -77,3 +78,102 @@ test('focus de-emphasis fades each icon from its own color', () => {
|
|||||||
);
|
);
|
||||||
assert.equal(state._activeFocusStyleCount, 3);
|
assert.equal(state._activeFocusStyleCount, 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('coverage refresh releases departed geometry and preserves media for reselection', () => {
|
||||||
|
const records = ['opening', 'final', 'neighbor'].map((id) => ({
|
||||||
|
camera: { id, headingConfidence: 'high' },
|
||||||
|
frustumGeometry: {},
|
||||||
|
frustumPositions: {},
|
||||||
|
coverageEntities: [],
|
||||||
|
}));
|
||||||
|
const state = {
|
||||||
|
_enabled: true,
|
||||||
|
_coverageMode: 'on',
|
||||||
|
_showProjection: true,
|
||||||
|
_records: records,
|
||||||
|
_coverageEntities: [],
|
||||||
|
};
|
||||||
|
let active = records[0];
|
||||||
|
let visible = new Set(['opening']);
|
||||||
|
const lines = new Set();
|
||||||
|
const planes = new Set();
|
||||||
|
const services = { ground: {}, focus: {} };
|
||||||
|
const parts = {
|
||||||
|
renderer: {
|
||||||
|
ready: true,
|
||||||
|
createCoverage(id) {
|
||||||
|
const line = { id, show: false };
|
||||||
|
lines.add(line);
|
||||||
|
return [line];
|
||||||
|
},
|
||||||
|
removeCoverageEntity: (line) => lines.delete(line),
|
||||||
|
styleLine: (line, { visible }) => {
|
||||||
|
line.show = visible;
|
||||||
|
},
|
||||||
|
removePlane: (runtime) => planes.delete(runtime.planeEntity),
|
||||||
|
setMarkersVisible() {},
|
||||||
|
},
|
||||||
|
selection: { getActiveRecord: () => active },
|
||||||
|
model: { isBrowserOverlayCamera: () => false },
|
||||||
|
projection: {
|
||||||
|
ensureProjectionRuntime(record) {
|
||||||
|
record.projection ??= { canvas: {} };
|
||||||
|
const runtime = record.projection;
|
||||||
|
if (!runtime.planeEntity) {
|
||||||
|
runtime.planeEntity = {};
|
||||||
|
runtime.needsPlane = false;
|
||||||
|
planes.add(runtime.planeEntity);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setPlaneVisible: (runtime, value) => {
|
||||||
|
runtime.planeVisible = value;
|
||||||
|
},
|
||||||
|
pauseInactiveProjectionFeeds() {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
parts.geometry = createGeometry({ state, services, parts });
|
||||||
|
parts.geometry.buildCoverageVisibleSet = () => visible;
|
||||||
|
const rendering = createRendering({ state, services, parts });
|
||||||
|
rendering.refreshCoverageStyles();
|
||||||
|
const openingLine = records[0].coverageEntities[0];
|
||||||
|
const media = records[0].projection.canvas;
|
||||||
|
assert.equal(
|
||||||
|
openingLine.show,
|
||||||
|
true,
|
||||||
|
'startup without a restore draws immediately',
|
||||||
|
);
|
||||||
|
active = records[1];
|
||||||
|
visible = new Set(['final', 'neighbor']);
|
||||||
|
rendering.refreshCoverageStyles();
|
||||||
|
assert.equal(lines.has(openingLine), false);
|
||||||
|
assert.deepEqual(records[0].coverageEntities, []);
|
||||||
|
assert.equal(records[0].projection.planeEntity, null);
|
||||||
|
assert.equal(records[0].projection.needsPlane, true);
|
||||||
|
assert.equal(records[0].projection.canvas, media);
|
||||||
|
assert.deepEqual(
|
||||||
|
[...lines].map((line) => line.id),
|
||||||
|
['final', 'neighbor'],
|
||||||
|
);
|
||||||
|
assert.ok([...lines].every((line) => line.show));
|
||||||
|
assert.deepEqual(state._coverageEntities, [...lines]);
|
||||||
|
assert.equal(planes.size, 1);
|
||||||
|
const finalLine = records[1].coverageEntities[0];
|
||||||
|
rendering.refreshCoverageStyles();
|
||||||
|
assert.equal(
|
||||||
|
records[1].coverageEntities[0],
|
||||||
|
finalLine,
|
||||||
|
'stationary geometry is reused',
|
||||||
|
);
|
||||||
|
active = records[0];
|
||||||
|
visible = new Set();
|
||||||
|
rendering.refreshCoverageStyles();
|
||||||
|
assert.equal(
|
||||||
|
lines.size,
|
||||||
|
1,
|
||||||
|
'active projection keeps its coverage outside the visible cohort',
|
||||||
|
);
|
||||||
|
assert.equal(records[0].coverageEntities[0].show, true);
|
||||||
|
assert.notEqual(records[0].coverageEntities[0], openingLine);
|
||||||
|
assert.equal(records[0].projection.canvas, media);
|
||||||
|
assert.equal(planes.size, 1);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user