Merge branch 'main' into sameh/flight-photo

This commit is contained in:
Sameh Khamis authored and GitHub committed 2026-11-09 21:13:16 -08:00
commit 9fe27c2d75
6 files changed
+267 -14

No files matched your search

+219
View File
@@ -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`,
);
});
+39 -13
View File
@@ -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,
+4
View File
@@ -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;