mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-11 13:52:19 +00:00
Merge branch 'main' into sameh/flight-photo
This commit is contained in:
6 files changed
+267
-14
No files matched your search
@@ -0,0 +1,219 @@
|
||||
// Street Traffic road lines build one set at a time: rebuilds asked for while
|
||||
// a set is building wait, and only the newest of them runs.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as Cesium from 'cesium';
|
||||
import { createRendering } from './rendering.js';
|
||||
|
||||
// Cesium tests a material's uniforms against these browser classes.
|
||||
for (const name of [
|
||||
'HTMLCanvasElement',
|
||||
'HTMLImageElement',
|
||||
'ImageBitmap',
|
||||
'OffscreenCanvas',
|
||||
])
|
||||
globalThis[name] ??= class {};
|
||||
// A line primitive's render state is checked against the context's limits,
|
||||
// which only a WebGL context sets.
|
||||
Cesium.ContextLimits._minimumAliasedLineWidth = 1;
|
||||
Cesium.ContextLimits._maximumAliasedLineWidth = 1;
|
||||
|
||||
/** A road with flow; `level` picks its bucket (0.1 jam, 0.6 slow, 1 free). */
|
||||
function road(level = 1) {
|
||||
return {
|
||||
flow: { level, closure: false },
|
||||
segmentDist: [100],
|
||||
waypoints: Cesium.Cartesian3.fromDegreesArray([
|
||||
-97.744, 30.267, -97.742, 30.267,
|
||||
]),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendering over a scene that records its ground primitives. Each added
|
||||
* primitive is replaced by a stand-in whose `ready` the test sets, as Cesium
|
||||
* sets it once the asynchronous geometry is built.
|
||||
*/
|
||||
function harness() {
|
||||
const scene = [];
|
||||
const added = [];
|
||||
let linesOn = true;
|
||||
const state = {
|
||||
_viewer: {
|
||||
scene: {
|
||||
globe: { show: true },
|
||||
groundPrimitives: {
|
||||
add(primitive) {
|
||||
const line = {
|
||||
ready: false,
|
||||
roads: primitive.geometryInstances.length,
|
||||
};
|
||||
scene.push(line);
|
||||
added.push(line);
|
||||
return line;
|
||||
},
|
||||
remove(line) {
|
||||
const index = scene.indexOf(line);
|
||||
if (index >= 0) scene.splice(index, 1);
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
_liveMode: true,
|
||||
_showLines: true,
|
||||
_heatSupported: true,
|
||||
_heatJamPrim: null,
|
||||
_heatSlowPrim: null,
|
||||
_heatFreePrim: null,
|
||||
_heatRetired: [],
|
||||
_heatPendingRoads: null,
|
||||
_heatLineCount: 0,
|
||||
};
|
||||
const rendering = createRendering({
|
||||
state,
|
||||
services: {},
|
||||
source: {},
|
||||
parts: {
|
||||
style: { heatlineOn: () => linesOn },
|
||||
animation: { syncRenderDemand() {} },
|
||||
},
|
||||
});
|
||||
return {
|
||||
state,
|
||||
rendering,
|
||||
scene,
|
||||
added,
|
||||
setLines(on) {
|
||||
linesOn = on;
|
||||
state._showLines = on;
|
||||
},
|
||||
/** Finish every build in the scene, then run a frame. */
|
||||
frame({ built = true } = {}) {
|
||||
if (built) for (const line of scene) line.ready = true;
|
||||
rendering.retireHeatLines();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const roads = (count) => Array.from({ length: count }, () => road());
|
||||
|
||||
test('rebuilds asked for while lines build do not start more builds', () => {
|
||||
const { rendering, scene, added, frame } = harness();
|
||||
rendering.rebuildHeatLines(roads(1));
|
||||
assert.equal(added.length, 1);
|
||||
// Progressive paints while the first set builds.
|
||||
for (const count of [2, 3, 4]) rendering.rebuildHeatLines(roads(count));
|
||||
frame({ built: false });
|
||||
assert.equal(added.length, 1, 'no build started during a build');
|
||||
assert.equal(scene.length, 1);
|
||||
assert.deepEqual(scene[0].roads, 1);
|
||||
});
|
||||
|
||||
test('the newest rebuild asked for during a build is the one that runs', () => {
|
||||
const { state, rendering, scene, added, frame } = harness();
|
||||
rendering.rebuildHeatLines(roads(1));
|
||||
for (const count of [2, 3, 4]) rendering.rebuildHeatLines(roads(count));
|
||||
frame();
|
||||
// One build for the three requests, of the last one's roads.
|
||||
assert.deepEqual(
|
||||
added.map((line) => line.roads),
|
||||
[1, 4],
|
||||
);
|
||||
assert.equal(state._heatLineCount, 4);
|
||||
assert.equal(state._heatPendingRoads, null);
|
||||
// The set it replaces stays drawn while it builds, then goes.
|
||||
assert.deepEqual(
|
||||
scene.map((line) => line.roads),
|
||||
[1, 4],
|
||||
);
|
||||
frame();
|
||||
assert.deepEqual(
|
||||
scene.map((line) => line.roads),
|
||||
[4],
|
||||
);
|
||||
// Nothing is waiting, so later frames start nothing.
|
||||
frame();
|
||||
assert.equal(added.length, 2);
|
||||
});
|
||||
|
||||
test('a rebuild with nothing building starts at once', () => {
|
||||
const { rendering, added, frame } = harness();
|
||||
rendering.rebuildHeatLines(roads(1));
|
||||
frame();
|
||||
rendering.rebuildHeatLines(roads(2));
|
||||
assert.deepEqual(
|
||||
added.map((line) => line.roads),
|
||||
[1, 2],
|
||||
);
|
||||
});
|
||||
|
||||
test('turning lines off during a build removes them at once and drops the waiting rebuild', () => {
|
||||
const { state, rendering, scene, added, setLines, frame } = harness();
|
||||
rendering.rebuildHeatLines(roads(1));
|
||||
frame();
|
||||
rendering.rebuildHeatLines(roads(2));
|
||||
rendering.rebuildHeatLines(roads(3));
|
||||
assert.equal(scene.length, 2, 'the drawn set and the building one');
|
||||
setLines(false);
|
||||
rendering.rebuildHeatLines(roads(3));
|
||||
assert.deepEqual(scene, [], 'not deferred behind the build');
|
||||
assert.equal(state._heatPendingRoads, null);
|
||||
assert.equal(state._heatLineCount, 0);
|
||||
frame();
|
||||
assert.equal(added.length, 2, 'the waiting rebuild never runs');
|
||||
assert.deepEqual(scene, []);
|
||||
});
|
||||
|
||||
test('a rebuild right after a build finishes does not keep the lines that build replaced', () => {
|
||||
const { rendering, scene } = harness();
|
||||
rendering.rebuildHeatLines(roads(1));
|
||||
for (const line of scene) line.ready = true;
|
||||
rendering.rebuildHeatLines(roads(2));
|
||||
// The second build finishes, and a third is asked for before a frame ran.
|
||||
for (const line of scene) line.ready = true;
|
||||
rendering.rebuildHeatLines(roads(3));
|
||||
assert.deepEqual(
|
||||
scene.map((line) => line.roads),
|
||||
[2, 3],
|
||||
);
|
||||
});
|
||||
|
||||
test('removing the lines drops the waiting rebuild', () => {
|
||||
const { state, rendering, scene, added, frame } = harness();
|
||||
rendering.rebuildHeatLines(roads(1));
|
||||
rendering.rebuildHeatLines(roads(2));
|
||||
rendering.removeHeatLines();
|
||||
assert.deepEqual(scene, []);
|
||||
assert.equal(state._heatPendingRoads, null);
|
||||
frame();
|
||||
assert.equal(added.length, 1);
|
||||
});
|
||||
|
||||
test('a stream of rebuilds never leaves more than two sets in the scene, and ends with one', () => {
|
||||
const { rendering, scene, added, frame } = harness();
|
||||
let most = 0;
|
||||
const lines = [road(0.1), road(0.6), road(1)];
|
||||
// Three primitives per set: one per flow bucket.
|
||||
for (let paint = 0; paint < 40; paint += 1) {
|
||||
rendering.rebuildHeatLines(lines);
|
||||
most = Math.max(most, scene.length);
|
||||
// A build takes several paints.
|
||||
if (paint % 5 === 4) frame();
|
||||
else frame({ built: false });
|
||||
most = Math.max(most, scene.length);
|
||||
}
|
||||
frame();
|
||||
frame();
|
||||
assert.equal(most <= 6, true, `${most} primitives at once`);
|
||||
assert.equal(scene.length, 3, 'only the current set remains');
|
||||
assert.equal(
|
||||
scene.every((line) => line.ready),
|
||||
true,
|
||||
);
|
||||
// Without coalescing every paint builds: 40 sets, 120 primitives.
|
||||
assert.equal(
|
||||
added.length <= 3 * 10,
|
||||
true,
|
||||
`${added.length} primitives built`,
|
||||
);
|
||||
});
|
||||
@@ -59,24 +59,37 @@ export function createRendering({
|
||||
/** Remove the heat-line / flow-line ground primitives from the scene. */
|
||||
|
||||
function removeHeatLines() {
|
||||
layerState._heatPendingRoads = null;
|
||||
removePrimitives(takeHeatLines());
|
||||
removePrimitives(layerState._heatRetired || []);
|
||||
layerState._heatRetired = [];
|
||||
parts.animation.syncRenderDemand?.();
|
||||
}
|
||||
|
||||
/** @returns {boolean} Whether a line primitive's geometry is still building. */
|
||||
|
||||
function linesBuilding() {
|
||||
return LINE_KEYS.some((key) => layerState[key] && !layerState[key].ready);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaced lines stay drawn until their successors finish building
|
||||
* (GroundPolylinePrimitive geometry is asynchronous), so a rebuild never
|
||||
* blinks. Called every held frame.
|
||||
* blinks. Once they have, the replaced lines go and the newest rebuild
|
||||
* asked for in the meantime starts. Called every held frame.
|
||||
*/
|
||||
|
||||
function retireHeatLines() {
|
||||
if (!layerState._heatRetired?.length) return;
|
||||
if (LINE_KEYS.some((key) => layerState[key] && !layerState[key].ready))
|
||||
return;
|
||||
removePrimitives(layerState._heatRetired);
|
||||
layerState._heatRetired = [];
|
||||
if (linesBuilding()) return;
|
||||
if (layerState._heatRetired?.length) {
|
||||
removePrimitives(layerState._heatRetired);
|
||||
layerState._heatRetired = [];
|
||||
}
|
||||
const pending = layerState._heatPendingRoads;
|
||||
if (pending) {
|
||||
layerState._heatPendingRoads = null;
|
||||
rebuildHeatLines(pending);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,24 +108,37 @@ export function createRendering({
|
||||
* the globe is hidden (photoreal) and on terrain or tiles otherwise. No-op in
|
||||
* sim mode, when both looks are off, or without ground-primitive support.
|
||||
*
|
||||
* One build runs at a time. A city loads in progressive paints, each asking
|
||||
* for a rebuild, and every build drapes up to the cap of polylines
|
||||
* asynchronously. A rebuild asked for while lines are building only replaces
|
||||
* the one waiting, which retireHeatLines starts when the build finishes, so
|
||||
* the scene never holds more than the lines drawn and the lines building.
|
||||
* Turning the lines off is never deferred.
|
||||
*
|
||||
* @param {Array} roads - Road objects visible in the current render.
|
||||
*/
|
||||
|
||||
function rebuildHeatLines(roads) {
|
||||
const wanted =
|
||||
layerState._viewer && layerState._liveMode && parts.style.heatlineOn();
|
||||
if (wanted && linesBuilding()) {
|
||||
layerState._heatPendingRoads = roads;
|
||||
return;
|
||||
}
|
||||
layerState._heatPendingRoads = null;
|
||||
// No build is left for replaced lines to wait for (it finished, or lines
|
||||
// are going off), so they go first and never pile up behind this rebuild.
|
||||
removePrimitives(layerState._heatRetired || []);
|
||||
layerState._heatRetired = [];
|
||||
const previous = takeHeatLines();
|
||||
const finish = () => {
|
||||
// Nothing new to wait for: the old lines go now.
|
||||
if (LINE_KEYS.some((key) => layerState[key]))
|
||||
(layerState._heatRetired ||= []).push(...previous);
|
||||
// Nothing new to wait for: the old lines go now.
|
||||
else removePrimitives(previous);
|
||||
parts.animation.syncRenderDemand?.();
|
||||
};
|
||||
if (
|
||||
!layerState._viewer ||
|
||||
!layerState._liveMode ||
|
||||
!parts.style.heatlineOn()
|
||||
)
|
||||
return finish();
|
||||
if (!wanted) return finish();
|
||||
if (layerState._heatSupported === null) {
|
||||
layerState._heatSupported = Cesium.GroundPolylinePrimitive.isSupported(
|
||||
layerState._viewer.scene,
|
||||
|
||||
@@ -200,6 +200,10 @@ export function createState({ services, source }) {
|
||||
|
||||
layerState._heatRetired = [];
|
||||
|
||||
/** @type {Array|null} Roads of the newest rebuild asked for while the lines were building. */
|
||||
|
||||
layerState._heatPendingRoads = null;
|
||||
|
||||
/** @type {number} Heat-lines in the current render (stats). */
|
||||
|
||||
layerState._heatLineCount = 0;
|
||||
|
||||
Reference in new issue
Block a user