feat(time): the app clock on the engine

src/time/clock.js is the app's own time in ms since the epoch: by default
Date.now exactly (read at each call), with freeze/offset/rate/reset for QA
and tests, and one frame time latched per engine frame (beginFrame). The
contract gains engine.clock { now, frameTime, ticked }, in the fake engine
(a controllable wall clock, fake.advanceTime, fake.appClock), the contract
suite (one frame time for every listener of a frame, latched before
preUpdate, never decreasing), Cesium and Navara.

Cesium latches on the viewer clock's onTick, which CesiumWidget runs right
before scene.render, so scene listeners the app added before the engine
existed read the same frame time; the subscription appears on first use
and destroy removes it. The viewer clock itself is untouched: in the app
it never animates (shouldAnimate false, multiplier 1, its time frozen at
boot), and its time still drives Cesium's lighting as before. Navara
latches in the engine's handler of the view's preUpdate, and the look
reads the engine clock's frame time for the sun, so the app clock is the
one time source on Navara too.

src/time is a portable root in check-import-directions: no engine
packages, no browser globals.
This commit is contained in:
German Khokhlov committed 2026-10-16 15:01:36 +04:00
1 parent 27e28373d0
commit f1a2df5cfe
20 files changed
+771 -13

No files matched your search

+7 -1
View File
@@ -21,6 +21,8 @@ const source = (file) =>
/^src\/layers\/[^/]+\/(?:source|bundledSource|flowSource)\.js$/.test(file);
// Engine-neutral geometry ported from Cesium; every runtime file is a portable root.
const geo = (file) => file.startsWith('src/geo/');
// The app clock (src/time): plain numbers, no engine, no DOM.
const time = (file) => file.startsWith('src/time/');
// The engine-neutral globe contract and its test support; engines live below it.
const globeContract = (file) =>
/^src\/globe\/[^/]+$/.test(file) || file.startsWith('src/globe/testing/');
@@ -210,6 +212,7 @@ export function checkImportDirections(root) {
browser(file);
const roots = new Map(files.filter(source).map((file) => [file, 'source']));
for (const file of files.filter(geo)) roots.set(file, 'geo');
for (const file of files.filter(time)) roots.set(file, 'time');
for (const [key, value] of Object.entries(pkg.exports || {})) {
if (portableExport(key) && typeof value === 'string')
roots.set(value.replace(/^\.\//, ''), key);
@@ -237,7 +240,10 @@ export function checkImportDirections(root) {
specifier.startsWith('node:') ||
/^(?:cesium|vite)(?:\/|$)/.test(specifier) ||
// Geo must not reach Cesium through its workspace packages either.
(label === 'geo' && /^@cesium\//.test(specifier))
(label === 'geo' && /^@cesium\//.test(specifier)) ||
// Neither engine reaches the app clock.
(label === 'time' &&
(cesiumPackage.test(specifier) || navaraPackage.test(specifier)))
)
report(
file,