diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 55b7a63ab..109015c4d 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1,5 +1,7 @@ # 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 the IUGG mean spherical Earth radius, 6371.0088 km. `greatCircleKm` is canonical; `greatCircleMeters` is its exact metre conversion. Missing, diff --git a/src/layers/traffic/animation.js b/src/layers/traffic/animation.js index 05afde1c1..4d9f6c4b3 100644 --- a/src/layers/traffic/animation.js +++ b/src/layers/traffic/animation.js @@ -20,6 +20,20 @@ export function createAnimation({ parts, 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. * @@ -188,6 +202,7 @@ export function createAnimation({ : null, }); } + syncRenderDemand(); } /** @@ -206,6 +221,8 @@ export function createAnimation({ */ function animate() { + syncRenderDemand(); + if (!layerState._renderHeld) return; const now = Date.now(); // Delta time in seconds, capped to avoid jumps when returning from background tab const dt = layerState._lastAnimTime @@ -339,6 +356,13 @@ export function createAnimation({ layerState._count = 0; layerState._bucketCounts = { free: 0, slow: 0, jam: 0, sim: 0 }; layerState._closedRoads = 0; + syncRenderDemand(); } - return { spawnDotsForRoad, animate, maybeStopLight, clearDots }; + return { + spawnDotsForRoad, + animate, + maybeStopLight, + clearDots, + syncRenderDemand, + }; } diff --git a/src/layers/traffic/lifecycle.js b/src/layers/traffic/lifecycle.js index 94c40d935..fc13e205d 100644 --- a/src/layers/traffic/lifecycle.js +++ b/src/layers/traffic/lifecycle.js @@ -12,7 +12,6 @@ export function createLifecycle({ parts, source, }) { - const { holdContinuousRender, releaseContinuousRender } = services.render; const { resetFlowTileCache } = source; const methods = { @@ -81,7 +80,7 @@ export function createLifecycle({ layerState._enabled = true; layerState._surfaceFrameRemover = observeTrafficSurface(viewer.scene); if (layerState._roadMode !== 'tomtom') source.prefetch?.(); - holdContinuousRender('traffic'); // per-frame animator (perf wave 2) + parts.animation.syncRenderDemand(); layerState._lastAnimTime = 0; layerState._pointCollection.show = true; @@ -142,7 +141,7 @@ export function createLifecycle({ disable(viewer) { layerState._enabled = false; services.credits?.hideOsmCredit?.(layerState._viewer, 'traffic'); - releaseContinuousRender('traffic'); + parts.animation.syncRenderDemand(); clearTimeout(layerState._fetchTimeout); clearInterval(layerState._enableKickTimer); layerState._enableKickTimer = null; diff --git a/src/layers/traffic/motion.test.mjs b/src/layers/traffic/motion.test.mjs index 3c230d870..6338eb10f 100644 --- a/src/layers/traffic/motion.test.mjs +++ b/src/layers/traffic/motion.test.mjs @@ -37,7 +37,14 @@ function setup(t) { visibleRoadsForAltitude: (roads) => roads, }, }; - const ctx = { state, services: {}, parts, source: {} }; + const ctx = { + state, + services: { + render: { holdContinuousRender() {}, releaseContinuousRender() {} }, + }, + parts, + source: {}, + }; parts.style = createStyle(ctx); parts.model = createModel(ctx); parts.animation = createAnimation(ctx); diff --git a/src/layers/traffic/renderDemand.test.mjs b/src/layers/traffic/renderDemand.test.mjs new file mode 100644 index 000000000..df5965145 --- /dev/null +++ b/src/layers/traffic/renderDemand.test.mjs @@ -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); +}); diff --git a/src/layers/traffic/rendering.js b/src/layers/traffic/rendering.js index 7e148148d..bf0132b98 100644 --- a/src/layers/traffic/rendering.js +++ b/src/layers/traffic/rendering.js @@ -50,6 +50,7 @@ export function createRendering({ layerState._heatSlowPrim = null; } layerState._heatLineCount = 0; + parts.animation.syncRenderDemand?.(); } /** @@ -152,6 +153,7 @@ export function createRendering({ } layerState._heatLineCount = kept.length; + parts.animation.syncRenderDemand?.(); if (candidates.length > kept.length) { console.log( `[Data:Traffic] Heat-lines capped at ${HEAT_LINE_CAP} (${candidates.length} congested roads in view)`, diff --git a/src/layers/traffic/retention.js b/src/layers/traffic/retention.js index 4bfe7bfb6..ec0762b5c 100644 --- a/src/layers/traffic/retention.js +++ b/src/layers/traffic/retention.js @@ -125,6 +125,7 @@ export function createRetention({ state, parts }) { count++; } state._count = count; + parts.animation.syncRenderDemand?.(); } function reconcile(roads, height, replace) { altitude = height; diff --git a/src/layers/transit/lifecycle.test.mjs b/src/layers/transit/lifecycle.test.mjs index 8a2d39edf..9bc5cb5b1 100644 --- a/src/layers/transit/lifecycle.test.mjs +++ b/src/layers/transit/lifecycle.test.mjs @@ -3576,7 +3576,7 @@ test('actual Traffic, Bikeshare and Transit lifecycles retain sensitivity in eve source: {}, parts: { flow: { ensureFlowStatus: noop }, - animation: { animate: noop, clearDots: noop }, + animation: { animate: noop, clearDots: noop, syncRenderDemand: noop }, viewport: { onCameraChanged: noop }, ingestion: { cancelActiveFetch: noop }, },