fix(traffic): hold rendering only for animated content

This commit is contained in:
Said committed 2026-10-20 01:21:53 +01:00
1 parent de2138e84c
commit 122b511d1e
8 files changed
+86 -6

No files matched your search

+2
View File
@@ -1,5 +1,7 @@
# God's Eye View Current State # God's Eye View Current State
Traffic holds continuous rendering only while enabled with animated dots or a pulsing jam heat-line. Empty, failed, and high-altitude views release the hold; population admission reacquires it and resets animation time. Clear, disable, and destroy also release it. Retained fading dots keep animation alive until they are removed.
Shared distance checks use `src/geoDistance.js` with `{ lat, lon }` points and Shared distance checks use `src/geoDistance.js` with `{ lat, lon }` points and
the IUGG mean spherical Earth radius, 6371.0088 km. `greatCircleKm` is the IUGG mean spherical Earth radius, 6371.0088 km. `greatCircleKm` is
canonical; `greatCircleMeters` is its exact metre conversion. Missing, canonical; `greatCircleMeters` is its exact metre conversion. Missing,
+25 -1
View File
@@ -20,6 +20,20 @@ export function createAnimation({
parts, parts,
source, source,
}) { }) {
function syncRenderDemand() {
const needed = Boolean(
layerState._enabled &&
(layerState._dots.length || layerState._heatJamPrim),
);
if (needed === Boolean(layerState._renderHeld)) return;
layerState._renderHeld = needed;
if (needed) {
layerState._lastAnimTime = 0;
services.render.holdContinuousRender('traffic');
layerState._viewer?.scene?.requestRender?.();
} else services.render.releaseContinuousRender('traffic');
}
/** /**
* Spawn animated dot primitives along a single road. * Spawn animated dot primitives along a single road.
* *
@@ -188,6 +202,7 @@ export function createAnimation({
: null, : null,
}); });
} }
syncRenderDemand();
} }
/** /**
@@ -206,6 +221,8 @@ export function createAnimation({
*/ */
function animate() { function animate() {
syncRenderDemand();
if (!layerState._renderHeld) return;
const now = Date.now(); const now = Date.now();
// Delta time in seconds, capped to avoid jumps when returning from background tab // Delta time in seconds, capped to avoid jumps when returning from background tab
const dt = layerState._lastAnimTime const dt = layerState._lastAnimTime
@@ -339,6 +356,13 @@ export function createAnimation({
layerState._count = 0; layerState._count = 0;
layerState._bucketCounts = { free: 0, slow: 0, jam: 0, sim: 0 }; layerState._bucketCounts = { free: 0, slow: 0, jam: 0, sim: 0 };
layerState._closedRoads = 0; layerState._closedRoads = 0;
syncRenderDemand();
} }
return { spawnDotsForRoad, animate, maybeStopLight, clearDots }; return {
spawnDotsForRoad,
animate,
maybeStopLight,
clearDots,
syncRenderDemand,
};
} }
+2 -3
View File
@@ -12,7 +12,6 @@ export function createLifecycle({
parts, parts,
source, source,
}) { }) {
const { holdContinuousRender, releaseContinuousRender } = services.render;
const { resetFlowTileCache } = source; const { resetFlowTileCache } = source;
const methods = { const methods = {
@@ -81,7 +80,7 @@ export function createLifecycle({
layerState._enabled = true; layerState._enabled = true;
layerState._surfaceFrameRemover = observeTrafficSurface(viewer.scene); layerState._surfaceFrameRemover = observeTrafficSurface(viewer.scene);
if (layerState._roadMode !== 'tomtom') source.prefetch?.(); if (layerState._roadMode !== 'tomtom') source.prefetch?.();
holdContinuousRender('traffic'); // per-frame animator (perf wave 2) parts.animation.syncRenderDemand();
layerState._lastAnimTime = 0; layerState._lastAnimTime = 0;
layerState._pointCollection.show = true; layerState._pointCollection.show = true;
@@ -142,7 +141,7 @@ export function createLifecycle({
disable(viewer) { disable(viewer) {
layerState._enabled = false; layerState._enabled = false;
services.credits?.hideOsmCredit?.(layerState._viewer, 'traffic'); services.credits?.hideOsmCredit?.(layerState._viewer, 'traffic');
releaseContinuousRender('traffic'); parts.animation.syncRenderDemand();
clearTimeout(layerState._fetchTimeout); clearTimeout(layerState._fetchTimeout);
clearInterval(layerState._enableKickTimer); clearInterval(layerState._enableKickTimer);
layerState._enableKickTimer = null; layerState._enableKickTimer = null;
+8 -1
View File
@@ -37,7 +37,14 @@ function setup(t) {
visibleRoadsForAltitude: (roads) => roads, visibleRoadsForAltitude: (roads) => roads,
}, },
}; };
const ctx = { state, services: {}, parts, source: {} }; const ctx = {
state,
services: {
render: { holdContinuousRender() {}, releaseContinuousRender() {} },
},
parts,
source: {},
};
parts.style = createStyle(ctx); parts.style = createStyle(ctx);
parts.model = createModel(ctx); parts.model = createModel(ctx);
parts.animation = createAnimation(ctx); parts.animation = createAnimation(ctx);
+45
View File
@@ -0,0 +1,45 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createAnimation } from './animation.js';
test('traffic render demand follows animated population and jam lines across empty, disable and re-enable', () => {
const calls = [];
const state = { _enabled: true, _dots: [], _motion: { rebuilds: 0 } };
const animation = createAnimation({
state,
services: {
render: {
holdContinuousRender: (id) => calls.push(['hold', id]),
releaseContinuousRender: (id) => calls.push(['release', id]),
},
},
parts: {
retention: { clear() {} },
rendering: {
removeHeatLines() {
state._heatJamPrim = null;
},
},
},
});
animation.syncRenderDemand();
assert.deepEqual(calls, []);
state._dots.push({});
animation.syncRenderDemand();
animation.syncRenderDemand();
assert.deepEqual(calls, [['hold', 'traffic']]);
animation.clearDots();
assert.deepEqual(calls.at(-1), ['release', 'traffic']);
state._heatJamPrim = {};
animation.syncRenderDemand();
assert.deepEqual(calls.at(-1), ['hold', 'traffic']);
state._enabled = false;
animation.syncRenderDemand();
assert.deepEqual(calls.at(-1), ['release', 'traffic']);
state._enabled = true;
animation.clearDots();
assert.equal(state._renderHeld, false);
state._dots.push({});
animation.syncRenderDemand();
assert.equal(state._renderHeld, true);
});
+2
View File
@@ -50,6 +50,7 @@ export function createRendering({
layerState._heatSlowPrim = null; layerState._heatSlowPrim = null;
} }
layerState._heatLineCount = 0; layerState._heatLineCount = 0;
parts.animation.syncRenderDemand?.();
} }
/** /**
@@ -152,6 +153,7 @@ export function createRendering({
} }
layerState._heatLineCount = kept.length; layerState._heatLineCount = kept.length;
parts.animation.syncRenderDemand?.();
if (candidates.length > kept.length) { if (candidates.length > kept.length) {
console.log( console.log(
`[Data:Traffic] Heat-lines capped at ${HEAT_LINE_CAP} (${candidates.length} congested roads in view)`, `[Data:Traffic] Heat-lines capped at ${HEAT_LINE_CAP} (${candidates.length} congested roads in view)`,
+1
View File
@@ -125,6 +125,7 @@ export function createRetention({ state, parts }) {
count++; count++;
} }
state._count = count; state._count = count;
parts.animation.syncRenderDemand?.();
} }
function reconcile(roads, height, replace) { function reconcile(roads, height, replace) {
altitude = height; altitude = height;
+1 -1
View File
@@ -3576,7 +3576,7 @@ test('actual Traffic, Bikeshare and Transit lifecycles retain sensitivity in eve
source: {}, source: {},
parts: { parts: {
flow: { ensureFlowStatus: noop }, flow: { ensureFlowStatus: noop },
animation: { animate: noop, clearDots: noop }, animation: { animate: noop, clearDots: noop, syncRenderDemand: noop },
viewport: { onCameraChanged: noop }, viewport: { onCameraChanged: noop },
ingestion: { cancelActiveFetch: noop }, ingestion: { cancelActiveFetch: noop },
}, },