sampleHeight collects hidden tiles only after the shown ones miss, with
no per-node allocation, and picks the finest by tile extent (Navara
exposes no geometric error), not per-mesh radius. The ground floor
resamples on a move only after 100 ms, and reports its own ground and
margin (engine.groundFloor, Navara-only diagnostics).
- The takram uniform paths move to internals.js (forEachLuminanceUniform,
loud in dev when a path is gone); look.js sets each factor to scale x
the recorded base value, so shared or copied vectors are never scaled
twice, and the stars keep the base value (own vector when shared).
Stars at intensity 120: a starfield shows from space.
- Both night-side shader variants compile on the first two frames.
- The clouds hide once the sun sets at the camera (dark puffs at dusk).
- No allocation on look frames that change nothing (frozen updates, a
scratch camera place from livePosition).
Gallery regenerated.
The clock keeps its numbers in object fields (stored in place) instead of
closure variables, and its default wall clock is one module-level
function. Micro-measure (Node, --expose-gc, 200k calls): the flights
render-time arithmetic allocates about 25-35 B per call with the app
clock against 66-102 B with JulianDate.now/addSeconds; an engine tick
(beginFrame + ticked) about 20-30 B.
Flights and military keep their fix history in plain milliseconds
(timeMs, whole ms as the Date the JulianDate came from held) and read
"now" from the app clock: the render-delay time, bracketing,
extrapolation, the warm-up check and the trail seed are number
arithmetic (motion.js, tracking.js, snapshotRenderer.js), as is the turn
rate and the forward synthetic fix in data/motionModel.js, which no
longer imports Cesium. The JulianDate scratch fields in state.js and the
unused JulianDate.now() in queries.js go. Launches (mission frame, panel,
selection), the tracked satellite's SGP4 epoch and the celestial ring's
sun and moon read the app clock; where a Cesium property still takes a
JulianDate it is built from the app clock's now (as JulianDate.now did
from Date.now). Wall-clock throttles and feed receipt stamps stay on
Date.now; property reads at viewer.clock.currentTime (render, frozen at
boot) move with the render code in the *a tasks.
Tests that built JulianDate history fixtures now build ms ones with the
same instants (motionModel.test, flights.test, militaryFlights.test); new
tests show the military delayed position and the tracked satellite follow
the app clock (both fail on the previous code).
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.
scripts/check-main-chunk.mjs (npm run check:main-chunk) finds the main
chunk of a build (the Vite manifest's entry, or the module script
dist/index.html loads), follows its static imports and fails when any of
them carries three or Navara markers; it prints the sizes. Base build:
main chunk 2,850,229 bytes (gzip 881,783), no static imports, clean.
scripts/qa-w1-baseline.mjs records Cesium mode for the W1 layers (flights,
military, local ADS-B, vessels, satellites, launches): counts, five known
objects with positions and screen points, JPEGs (path, size, sha256) at
the global / New York (JFK) / orbit poses, a tracked object's camera pose
after 2 s and what its share link restores. Live sources until the
fixture sources of Task 8.
VisualEffects sends its sharpen state to engine.render.setSharpen when a
viewer exists too; the Cesium engine keeps it as state, and the Cesium
stage itself is unchanged.
qa-globe-contract checks the noon sky facing the horizon (daylight blue,
no star pixels) and stars at midnight; qa-engine-shell compares
sampleHeight at the Austin startup pose across engines and reports the
wheel floor's lowest frame. QA pages load OSM and keyless terrain tiles
through a disk cache with backoff on 429. Report updated.
The ground under a tilted camera is outside the frustum, where Navara
hides its tiles, so the ray found nothing. When the shown tiles miss,
the loaded hidden ones answer, the finest first (within 1.5 times its
radius, which keeps out the quadtree parent). At the Austin startup
pose it agrees with Cesium within 0.12-1.04 m.
The baseline and the style presets name ENGINE_SHARPEN_DEFAULT, which
resolves to 49 % on Cesium and 80 % on Navara. Share links and explicit
calls restore their value unchanged.
The atmosphere draws physical radiance scaled for an exposure near 10,
while the baked Google tiles need exposure 1, so the noon sky came out
dark navy with the stars showing through. The cinematic look now scales
the atmosphere's radiance-to-luminance factors (sky, aerial perspective,
clouds) by 3, leaving the tiles alone, and shows its stars only from
space or with the sun below -6 degrees at the camera. Gallery
regenerated.
The contract gains render.setSharpen({ enabled, amount }) and
render.sharpen(). Navara draws it as a custom EffectDesc: Cesium's unsharp
mask (3x3 box blur, gamma 2.2 round trip so one amount matches Cesium)
right after tone mapping and before SMAA/FXAA, skipping sky and cloud
pixels by scene depth. The Cesium engine keeps the state only; Cesium mode
still sharpens with its own post stage, unchanged. The fake engine records
it and the contract suite checks it.
Without a viewer the Sharpen slider and toggle drive the engine with the
same slider-to-amount mapping. On Navara the defaults, presets and share
links read Cesium's 49 % as 80 %.
navaraScenePlan gains a pure `cinematic` plan, now the default of the app's
Navara mode (`?look=photoreal|minimal` picks the others): photoreal plus
volumetric clouds (low quality, layers from 1,500 m, no ground shadows),
ACES tone mapping, brighter stars and no sun shadow maps. From space, at
rest, the aerial perspective lights the globe from the atmosphere for a
real night side and terminator; it switches off below 1,000 km so the
Google tiles keep their baked light.
look.js builds the plan and keeps it current on frames Navara draws
anyway: the atmosphere's sun date follows the wall clock (the HUD's SUN
elevation), and the clouds hide above 4 km or over high ground in view,
parking their last image so the aerial perspective shows no ghosts.
The Navara engine now builds its own photoreal scene (aerial perspective,
tone mapping, SMAA) instead of the minimal sky-and-sun look. It costs about
5 ms a frame (S5); minimal stays available for benches.
Prettier on the 13 new test files that failed --check; format-scope.json
now lists every file phase 2 added (src/globe/**, engineChoice, navaraMode,
cameraAngles, the QA scripts and pages, build/navaraAssets and tests).
Two engine-neutral checks: pickPosition then worldToWindow lands within
1.5 px of the screen point (over New York, straight down and tilted;
points with no surface are skipped, at least one must hit), and
maps.setTerrain resolves for keyless terrain and rejects an unknown kind.
The fake engine now rejects unknown terrain kinds, as both engines do.
The Navara contract page shows OSM on keyless terrain before the suite,
since depth picks need a drawn surface.
Navara's attribution list is capped at 120 px, and the shell's data
credits register after the map's, so Google's copyright lines come first
and stay visible while the card no longer covers the left panels.
qa-engine-shell polls engine.surface.tilesLoaded() (true for 1.5 s, up to
90 s) before judging the picture and prints the load time per engine;
"wheel zoom stops above the ground" is a hard check on both engines
(height >= 20 m; on Navara >= 20 m above the sampled surface), and the
Navara known gap is gone.
chunkFileNames names the chunk holding src/globe/navara
navara-engine-[hash].js; chunking is unchanged (manualChunks would pull
modules shared with the Cesium entry into it and make the entry import
it). The Cesium main chunk stays 2,849,217 B.
Navara 0.2.1's camera controller has no terrain or tile collision; its
only zoom limit, minimumZoomDistance, is a distance from the Earth's
centre, so a wheel zoom near the ground went through it (to -5.9 km).
groundFloor.js runs on the engine's preRender after the camera model
reads Navara's camera. While the camera is free (no flight, no lookAt
target) and below 20 km it samples the ground under it (tiles, else
terrain, else the ellipsoid; at most every 250 ms or 20 m), feeds
Navara's clamp (minimumZoomDistance = geocentric radius of the ground
point + 30 m, sent on changes over 10 m, reset to Navara's default
during flights and above 20 km) so the wheel stops smoothly, and lifts a
camera still below ground + 30 m straight up without camera.commanded.
It only lifts, so it cannot oscillate with the shell's ground guard,
whose lift is a flight the floor stands aside for.
The page's engine is ?engine=cesium|navara, else localStorage gev.engine,
else Cesium (src/app/engineChoice.js). Cesium mode is unchanged.
In Navara mode (src/app/navaraMode.js, loaded with import() only then,
and loading the Navara globe the same way) the scene builds the Navara
globe on the app container with no viewer; map stacks go through
engine.maps (Google 3D from the browser key, else Esri imagery); only the
shell's own data credits are registered. The layer catalog is empty and
the controls get empty layer stand-ins. The shell does not create its
viewer-bound parts (cockpit view, celestial ring, detection, tracked
readout, post-process stages); the tools install the render governor on
the engine, skip the director, annotations, drawing, imagery box, scope
mask, voice and embedded views, skip the first-run welcome, and show
"Experimental Navara engine: layers, cockpit, director and voice are
unavailable". window.__godsEyeView.viewer is null.
Navara's attribution card is kept clear of the location tray. A few
shell paths became null-safe for the missing viewer, layers or stages
without changing Cesium behaviour.
scripts/qa-engine-shell.mjs checks both modes headless: boot, viewer,
console errors, map drawn, search and fly, north/tilt/zoom/reset, orbit,
HUD, share link restore and map stacks.
The import-direction check now tells dynamic imports apart
(analyzeModule(source, { dynamic: true })) and rejects any static import
or re-export of src/globe/navara/** from outside that directory, so the
Navara engine can only be a lazily loaded chunk of the app.
Navara's prebuilt workers load their wasm by file name next to
themselves, which the bundler cannot see; a build-only Rollup plugin
copies the missing ones (draco decoder, font worker) into assets/ when
the bundle includes Navara, and adds nothing to a build without it.
createDefaultMapSources takes the engine kind. On Navara, Google 3D is
available from the browser key alone (Navara builds the tiles itself and
has no ion route to them), so it is the default and the recovery stack
when a key is set; without one the default is Esri imagery. Bing (Cesium
ion imagery) has no Navara path and is unavailable with a reason. World
terrain still needs an ion token and falls back to the keyless terrain.
MapStackController passes the kind of the engine it is given; Cesium
sources are unchanged.
The render governor installs on a non-Cesium globe engine as its backend:
owner-counted holds drive engine.render.setContinuous and requests go to
engine.render.request, so orbit and every other per-frame owner keep
Navara drawing while held and let it idle once the last hold goes. Cesium
stays on the viewer path its engine delegates to.
A user gesture on Navara now ends a flight without raising
camera.commanded (the user is not a new camera owner). Headless Navara QA
checks both.
Camera-gesture listeners attach to the engine's canvas and the
interaction availability check tolerates a missing viewer, so a director
built on an engine alone places, flies and stops scene cameras.
Scene flights, authored placement, flight cancellation and arrival
ownership run on the engine the composition hands the director (or the
one registered for its viewer). Tracking, interactions and data packs keep
the viewer until phase 1c.
The application shell keeps the page's globe engine and hands it to the
navigation controller, location navigation (search, city and POI flights,
orbit, globe reset), the tilt and north-up controls, share-link save and
restore, share restoration's first-gesture listeners and the HUD. The
viewer is passed only where entity tracking still lives on it (phase 1c)
and to the HUD's basemap label context (voice); every such use is null-safe.
Share links and captured shots write heading and roll as Cesium's camera
reports them (src/cameraAngles.js): the contract's [0, 2π) pose maps an
exact 0 back to Cesium's 2π, except the roll of a vertical view, so
links stay byte-identical (heading=360, roll=360) on every engine.
Source-reading tests follow the moved code with the same checks.
The startup fly-in, orbit, world-target focus, the arrival ground guard,
arrival ownership, camera sensitivity claims, landmark/viewport/globe
flights and place search (with its view bias and near-view recovery) take
the globe engine. Callers that still hold a viewer (voice, layers until
phase 1c) reach the same engine through engineFromViewer, and arrival
ownership is keyed by the engine whichever one a caller names.
The ground guard yields on camera.commanded instead of patching the
viewer camera itself; on Cesium that event hooks the same command methods
and tracking changes, so direct Cesium callers still end it. Landmark
framing reads the frustum fov from the pose's fovy and aspectRatio.
Tests that built viewer doubles now run on the fake engine (or on the
Cesium engine over the same double), with the same expectations.
Shell navigation needs to know when a new owner takes the camera (the
arrival ground guard), where the globe surface is under a window position
(orientation pivot, view-centred place search), the frustum aspect ratio
(landmark framing) and whether the engine is gone. Each member has a
contract suite check and is implemented by the Cesium engine, the Navara
engine and the fake. On Cesium, commanded hooks the viewer camera's
command methods and trackedEntityChanged only while it has listeners, so
direct Cesium calls from layers still announce a new owner.
- camera.livePosition: one engine-owned Cartesian3, refreshed on read
(the model replaces its state on every move); camera.height()
- render.request(reason) takes the diagnostic reason and ignores it
- maps.current() returns the sources as passed: the base imagery on the
globe (none while tiles replace it) and the tiles shown
- maps.releasePhotorealTiles forgets a source and takes its tiles away
if they show
- base imagery and photoreal share one switch order; tiles off brings
back the last base imagery with its credit; imagery chosen while
tiles show waits hidden, credit included
- Navara QA runs the contract's maps checks with a Google 3D tiles
source on the dev server's browser key: 58/58
- maps: photoreal hides the base imagery layer instead of deleting it,
and a new base layer goes in before the old one goes: deleting the
last raster layer stops Navara 0.2.1 loading terrain at the current
view, which left terrainHeight undefined under the tiles
- Google 3D tiles open Navara's attribution card so the tiles'
copyright lines are on screen, as Cesium shows them; onScreen
credits do the same
- setCameraInputs merges into the camera options set through the
engine (new setCameraOptions extension) instead of resetting them
- createNavaraGlobe disposes the view and canvas if the engine throws
- the pin test checks the installed Navara version and catches
bracketed private access
- QA: draw-match checks at the nadir clamp, after a Navara flight and
after a mouse drag; terrain under photoreal set right away; the
copyright lines on screen; create/destroy/create leaves no canvas,
frames, animation frames, workers or console errors (54/54)
src/globe/navara builds the GlobeEngine on a Navara 0.2.1 ThreeView. The
engine keeps the camera pose in src/geo values and pushes it to Navara's
setCamera in degrees (Cesium's heading/pitch/roll convention, S4);
whatever Navara moves itself (user input, its flyTo arc) is read back
from the drawn camera each frame. Projection, picks against the
ellipsoid and the view rectangle are geo math on that pose, with
Navara's fov kept on Cesium's 60° across the wider side.
- flights: Navara flyTo in ms with mapped easings, cancelFlight via an
empty setCamera, user input cancels a flight as in Cesium
- events: moveStart/moveEnd (500 ms still)/changed from the pose
- render: forceUpdate and animation (render on demand, S5)
- surface: S2 CPU ray over 3D-tile meshes, Navara terrain fallback
- maps: OSM/Esri raster, Re:Earth or ion quantized-mesh terrain,
Google 3D tiles with a browser key and Google's logo
- input: DOM pointer events with ScreenSpaceEventHandler semantics
- the three private Navara members used live in internals.js, pinned
to 0.2.1 by a test
index.js builds the product view (MSAA 4, native pixel ratio, preserved
drawing buffer, effects=minimal) for a dynamic import. The contract
QA script gains --engine=navara on a dev-only harness page: 46/46
contract and 4/4 Navara checks; Cesium stays 53/53.
Pins @navaramap/three and @navaramap/three-default-plugin 0.2.1 with
their peers three 0.185.1 and postprocessing 6.39.4 (mapillary-js keeps
its own nested three 0.134). The dev server leaves the Navara packages
and their wasm workers out of dependency pre-bundling so wasm-bindgen
resolves its .wasm next to the package. Nothing imports Navara yet: the
Cesium-mode main chunk is byte-identical (2,837,992 bytes, same hash).
The map source controller always hands engine.maps an engine-neutral
description of the registry source: imagery kind/style/accessToken and
credit, photoreal kind and credentials, terrain kind/accessToken. The
default registry carries those beside its Cesium factories. Only a
Cesium engine also gets the prebuilt provider, tileset and cached terrain
(Cesium-mode behaviour, ordering and same-provider guard unchanged); for
any other engine the controller builds nothing, falls back by description
when a setter rejects, and has no tile-failure watch.
The contract suite gains maps checks (opt-in with mapSources: two imagery
sources and a photoreal one the engine can show): setPhotorealTiles(null)
brings back the last base imagery, the last imagery or photoreal call
decides what shows, and a released photoreal source no longer shows or
counts. They observe the new maps.current(), the source objects on screen
as passed. The fake engine follows the same semantics; qa-globe-contract
runs the checks on the app engine with the startup tileset and reapplies
the app's map stack afterwards. The contract documents map sources as
engine-neutral descriptions whose prebuilt members only the engine kind
that built them uses.
The controls pass the scene engine to the shell, which initializes the
world overlay on it; initWorldOverlay takes only a globe engine (a viewer
or null is refused), so the shell no longer needs a viewer for its
overlay. engineFromViewer only returns the engine registered for a viewer
(the scene engine until teardown) and never builds one, so no engine can
outlive its viewer without a destroy path. Overlay and detection tests
and the allocation worker build a Cesium engine on their mock viewers and
destroy it with them; frame allocation is unchanged.
MapSourceController (and MapStackController) take the page's globe
engine: a globe stack becomes engine.maps.setBaseImagery with the
controller's ready provider and credit, then setPhotorealTiles(null),
then setTerrain with the cached terrain; a 3D stack becomes
setPhotorealTiles with its tileset and credit; owned tilesets are
released through maps.releasePhotorealTiles before disposal. The
controller keeps its provider/terrain/tileset caches, construction and
tile-failure fallbacks, switch generations and error watch. The scene
builds its map stack controller with its engine; without an engine the
controller applies sources to the viewer as before. The photoreal-to-
globe sequence (imagery, credit, globe, tileset) is tested end to end on
the Cesium engine.
Map credits (maps/credits.js) are shown through a credits capability
(engine.credits.addStatic) instead of the scene's credit display; the
shared map scene takes the engine's credits, so the Cesium engine's maps
and its credits part share one owner. Data credits (registerDataCredits,
registerDynamicCredit, show/hideOsmCredit) take the globe engine and keep
accepting a viewer from layers that still hold one; the scene registers
the data credits on its engine. The Cesium credits part reads
Viewer.creditDisplay, else the scene frame state display it aliases.
groundSnap.heightFor and meshFloorSampler.sampleMeshFloorCells take the
globe engine: engine.surface.sampleHeight for the probe,
engine.surface.tilesLoaded (and, for mesh cells, maps.photorealActive)
for readiness, camera.height for the mesh sampler's altitude gate. Callers
that still hold a viewer or scene (the layers until phase 1c) keep the
unchanged Cesium path.
pickGroundPosition takes the globe engine (picker.pickPosition, then
camera.pickEllipsoid) and still accepts a Cesium viewer from callers that
hold one (layers until phase 1c), with one shared ground-or-ellipsoid
rule. isSurfacePick classifies engine pick results exactly as the Cesium
picks they wrap.
The world overlay host takes its frames (frames.postRender), camera events
(camera.moveEnd), canvas size (host.size), projection matrix
(projector.viewProjection), horizon test (projector.horizonVisible) and
render requests (render.request('world-overlay')) from the globe engine.
initWorldOverlay accepts an engine or, for callers that still hold a
viewer, resolves the viewer's engine through engineFromViewer.
Contract: camera.position(result) becomes camera.livePosition, one
engine-owned object holding the current position, so per-frame readers
and paint lanes keep a live camera position without allocating; engine
events gain raiseValue for argument-list-free frame dispatch. Steady-state
frame allocation is unchanged (worker probe, 30 chunks: 2696/9536/38593
B/frame before and after; detection 473749 -> 408085).
Contract: camera.position(result?) and camera.height() for per-frame
readers, render.request(reason?), maps.releasePhotorealTiles, sources
with a caller-owned ready provider, isGlobeEngine. engine.maps now hides
every tileset it has shown, lets the last imagery/photoreal call win, and
setPhotorealTiles(null) restores the last base imagery and its credit
instead of leaving the photoreal credit on a bare globe.
Cesium engine: reads the Viewer camera/canvas aliases, detaches bridged
events through the remover Cesium returns, raises frame timestamps
without allocating, re-aims the horizon occluder only when the camera
moved, and renders the scene directly when the governor governs another
viewer (or none yet). engineFromViewer returns the registered scene
engine of a viewer, else builds and caches one. The render governor stays
the Cesium render backend under engine.render and gains
isRenderGovernorInstalled.
The scene builds the page's Cesium engine right after the viewer
(src/app/globeEngine.js), names the startup Google tileset to it once
loaded, returns it beside the viewer and destroys it before the viewer.
The debug handle exposes it as window.__godsEyeView.engine. Nothing
consumes it yet; render holds taken before the governor is installed
apply at install.
qa-globe-contract now runs on the app's engine (self-built only when the
app has none); a throwaway engine checks creation and destroy leave the
scene untouched.
Map sources are applied through one src/maps/sceneApply.js helper shared by MapSourceController and engine.maps (live layer kept for an unchanged provider, providers cached per source). The contract gains flyTo endTransform and rectangle destinations, flyToSphere easing, drillPick width/height and pick content, with Cesium's own flyToSphere range. Input skips listeners removed during dispatch and stays inert after destroy.
createCesiumEngine wraps an existing viewer without changing it: camera, picker, projector, frames, render (through the render governor), surface, input, credits and maps, with geo values out and Cesium values in at the boundary. Map and surface code is shared with src/maps and services/groundSnap instead of copied. scripts/qa-globe-contract.mjs runs the contract suite against the engine in headless Chrome; the camera-events check now waits for the previous move to end, as Cesium's moveStart marks a move from rest.
FlightOptions and setView now state Cesium's defaults for missing angles
(heading 0, pitch -pi/2, roll 0), enforced by a contract-suite check. The
suite also checks a horizon-bounded view rectangle and an undefined one
when the camera looks away from the Earth.
Engine events get their own documented semantics (single registration,
branded instances); the fake follows Cesium's roll sign, angle ranges,
synchronous zero-duration flights, flyToSphere defaults and
computeViewRectangle. The contract adds viewProjection, horizonVisible,
flightActive, lookAtTransform/transform, zoomIn/zoomOut, input modifiers,
terrainHeight and clampToHeight, with geometric suite checks. Engine
import rules now follow each engine graph transitively.
src/globe/contract.js defines the engine-neutral GlobeEngine capabilities
(JSDoc), createEngineEvent with Cesium.Event semantics and
assertGlobeEngine. src/globe/testing adds a geo-based fake engine for shell
tests and an engine-agnostic contract suite that the fake passes in Node.
check-import-directions keeps the contract free of cesium, three and
@navaramap, and each engine directory free of the other engine.
The draped coverage line is about one pixel wide and can sit on a pixel
boundary: the probe hit at a fractional x while the click at the integer
pixel missed. Wait for tiles, click only at an integer pixel that picks the
line right before the click (off the centre column first), retry up to 4x.