diff --git a/src/globe/cesium/clock.js b/src/globe/cesium/clock.js index 483919a18..902fbbe32 100644 --- a/src/globe/cesium/clock.js +++ b/src/globe/cesium/clock.js @@ -5,8 +5,15 @@ // (including those the app added to the scene before the engine existed) // reads the same frame time. The viewer clock itself is never changed: the // app does not animate it (shouldAnimate stays false), and its time still -// drives Cesium's own lighting as before. The tick subscription appears on -// first use (a ticked listener or a frameTime read) and destroy removes it. +// drives Cesium's own lighting as before. +// +// The latch is attached when the engine is created (the app creates it with +// the scene, before cameraVerbs and the layers add their own onTick +// listeners), so it runs first in each tick; an onTick listener added before +// the engine existed would read the previous frame's time. destroy removes +// it. A viewer without a clock (tests, a bare scene) latches on the scene's +// preUpdate instead, attached on first use so creating the engine adds no +// scene listener. import { createEngineEvent } from '../contract.js'; import { appClock as pageClock } from '../../time/clock.js'; @@ -29,6 +36,7 @@ export function createCesiumClock(viewer, { appClock = pageClock } = {}) { if (!removeTick && !destroyed) removeTick = tickSource.addEventListener(onTick); }; + if (viewer.clock?.onTick) attach(); const add = ticked.addEventListener; ticked.addEventListener = (listener) => { const remove = add(listener); diff --git a/src/globe/cesium/clock.test.mjs b/src/globe/cesium/clock.test.mjs index a8f5a0724..055645783 100644 --- a/src/globe/cesium/clock.test.mjs +++ b/src/globe/cesium/clock.test.mjs @@ -28,12 +28,19 @@ function cesiumFrame(viewer, { draw = true } = {}) { viewer.scene.postRender.raiseEvent(viewer.scene, time); } -test('the clock reads the app clock and subscribes to nothing at creation', () => { +test('the clock reads the app clock and latches on the viewer clock from creation', () => { const viewer = createFakeViewer(); const wall = manualAppClock(); const { clock } = createCesiumClock(viewer, { appClock: wall.appClock }); assert.ok(isEngineEvent(clock.ticked)); - assert.equal(viewer.clock.onTick.numberOfListeners, 0); + // Attached at creation, so it runs before onTick listeners added later + // (cameraVerbs installs its own after the engine exists). + assert.equal(viewer.clock.onTick.numberOfListeners, 1); + const order = []; + viewer.clock.onTick.addEventListener(() => order.push(clock.frameTime())); + wall.advance(9); + viewer.clock.onTick.raiseEvent(viewer.clock); + assert.deepEqual(order, [wall.appClock.now()]); assert.equal(clock.now(), wall.appClock.now()); wall.advance(5); assert.equal(clock.now(), wall.appClock.now()); @@ -67,7 +74,7 @@ test('frameTime is latched on the viewer clock tick, before preUpdate', () => { ]); }); -test('frameTime alone starts the latch; destroy removes it', () => { +test('frameTime reads the latch; destroy removes it', () => { const viewer = createFakeViewer(); const wall = manualAppClock(); const { clock, destroy } = createCesiumClock(viewer, { diff --git a/src/globe/cesium/createCesiumEngine.js b/src/globe/cesium/createCesiumEngine.js index 1c05c4285..69a06e43d 100644 --- a/src/globe/cesium/createCesiumEngine.js +++ b/src/globe/cesium/createCesiumEngine.js @@ -1,7 +1,9 @@ // The GlobeEngine on an existing Cesium Viewer. Creating it changes nothing -// in the viewer: Cesium subscriptions, the input handler and render holds +// in the scene: Cesium subscriptions, the input handler and render holds // appear only when the shell uses them, and destroy() removes them again -// (the viewer itself stays owned by the app). +// (the viewer itself stays owned by the app). The one exception is the +// engine clock's frame latch on viewer.clock.onTick (clock.js), attached at +// creation so it runs before onTick listeners added later. import { createCesiumCamera } from './camera.js'; import { createCesiumClock } from './clock.js'; diff --git a/src/globe/contract.js b/src/globe/contract.js index 98035b975..69926e2d0 100644 --- a/src/globe/contract.js +++ b/src/globe/contract.js @@ -214,8 +214,22 @@ * The app's own time (src/time/clock.js), in milliseconds since the Unix * epoch: the one time source for layers and the engine's own time-driven * drawing (Navara's sun). By default it is the wall clock (Date.now); QA - * and tests may freeze, offset or speed it up on the app clock. Cesium's - * viewer.clock is not driven by it (the app never animates that clock). + * and tests may freeze, offset or speed it up on the app clock: now() + * follows at once, frameTime() and ticked from the engine's next frame, and + * in request-render mode nothing draws until a render is requested, so a + * QA helper that moves the clock calls render.request() after it. + * Not on this clock yet (each moves when its layer or code moves): + * - Cesium's viewer.clock: the app never animates it (shouldAnimate false, + * its time frozen at boot); Cesium's sun and lighting and every entity + * property read at viewer.clock.currentTime stay on that time; + * - launches: the replay timeline (replay.js, Date.now/performance.now), + * the mission orbit's GMST frame (orbitRendering.js new Date()), launch + * pad and panel throttles (phase 3 Task 15a); + * - satellites: the point cloud's SGP4 propagation and orbit bake + * (rendering.js new Date()/Date.now) (Task 14a); only the tracked + * satellite reads the app clock; + * - wall-clock throttles and feed receipt stamps everywhere (they measure + * real elapsed time, not scene time). * @property {() => number} now The current app time. * @property {() => number} frameTime The app time latched at the start of the * engine's current frame: every reader in one frame gets the same value; it diff --git a/src/time/clock.js b/src/time/clock.js index edb1a9434..dff07526b 100644 --- a/src/time/clock.js +++ b/src/time/clock.js @@ -22,7 +22,9 @@ function finite(value, label) { * @property {() => number} beginFrame Latches and returns the frame time; the * globe engine calls it once at the start of each of its frames. * @property {(ms?: number) => void} freeze Holds the time at `ms` (default: - * the current time). QA and tests only. + * the current time). QA and tests only. Like offset and rate, it changes + * now() at once and frameTime() from the next beginFrame (the engine's + * next frame); request a render after it in request-render mode. * @property {() => void} resume Runs on from the frozen time. * @property {(ms: number) => void} offset Sets the time to the wall clock * plus `ms`, running on from it at the current rate (it ends a freeze).