Keep four-sample geometry and depth while using single-sample postprocessing buffers. Preserve forward and overlay coverage, skip empty transfers, and add matched pixel and regression checks.
Batch consecutive disjoint installation outlines to avoid three stencil draws
per feature. Preserve overlapping volumes, material order and local precision.
Delete the public effect handle correctly during teardown.
Keep imagery below ground classifications and glyphs in every activation order,
including minimal mode without tone mapping. Exclude fully offscreen ground
volumes from the screen candidate grid.
Full unit/allocation, boundaries, formatting and keyless builds pass. Browser
checks cover contour coverage, bounded colour differences, a missing-contour
negative control, real pass order, sprite alpha and FIRMS glows. Whole-wave
performance acceptance remains separate from this implementation checkpoint.
Clear the display-resource signature used by static sprite reuse when the WebGL context is lost or restored. Cover both events and verify that caching resumes after the redraw.
Port camera markers, coverage, media planes, volumes and calibration to the
Navara renderer. Preserve shared source/media policy and add exact plane
picking, owned resource cleanup and idle hold handling.
Validate paired keyless dev/preview media and physical drags, native legacy,
engine contracts and full CPU/build gates. Record remaining shell, viewshed
browser and W2 performance gates; defer Dioxus and plan the transit port.
Reuse retained tile triangles for CCTV's upcoming activation probe, with finite range, exclusions and honest misses. Correct nearest-hit ordering when the ray starts inside a mesh bound.
Fifteen focused surface tests and all full keyless unit/allocation, boundary, format and build gates pass. CCTV renderer integration and existing acceptance gaps remain documented.
Keep shared CCTV logic independent of Cesium, release the original renderer's resources, and guard retired initialization/media work. Preserve native marker, plane, volume, calibration and input behavior.
Record keyless dev/preview validation and retain unresolved shell idle/terrain and photoreal acceptance gaps. Routine QA builds use empty Google and Cesium credentials and block paid maps before navigation.
Record keyless launch instructions without changing app defaults.
Block Google tile and token fallback requests in routine CCTV checks;
add a guarded startup check for both engines.
Move native gizmo entities and input into an owned renderer. Restore the
captured camera-input flag, handle cancellation and duplicate presses, and
reject foreign handle identities. Preserve native geometry and picking.
Avoid no-op sync-chip mutations and let unchanged CCTV polls complete
without publishing scene changes. Keep health/UI polling and real geometry,
auto-hop and media updates. Extend browser idle samples across two polls.
Validated full Node24 unit/allocation, boundaries, scoped format, production,
panel and QA builds; native legacy parity, real media/GPU picks on dev and
preview, engine contracts and production shell. Retain failed evidence.
Prove native geometry, real image and video pixels on dev and preview before renderer extraction. Scope fixture interception to API routes so geometry workers remain live.
Separate native scene ownership from shared ALPR source and selection state,
and add bounded Navara sprites with the existing direction/card overlay.
Preserve marker artwork, picking, floor retries and source attribution.
Fix lost post-render wakeups and redundant mobile rail mutations, and keep
HUD typewriter dimensions stable so static views settle without extra frames.
Add parser-fed dev/preview and legacy evidence, including retained failures.
Common W2 terrain and combined performance gates remain open.
Preserve native clustering, selection and direct-action playback through renderer ports. Record both-engine dev/preview, legacy, contract, shell and build evidence with remaining W2 limits.
Keep fire normalization, LOD, anchors, cards and selection portable while the native renderer owns ground cells, sprites, input and scene disposal. Cancel superseded/removed settle timers and unregister the original sprite collection.
Preserve all ten native raster pairs exactly in dev and preview; record source/click/focus/lifecycle, legacy Metal and full regression evidence with strict comparator sensitivity checks.
Match native ground footprints, encoded alpha and source ordering on real terrain and photoreal tiles while excluding forward meshes. Cache static screen data and reduce large-disc edge work without changing GPU pixels.
Add deterministic lifecycle, source/share, foreground/horizon and three-context performance proof; restore credits for available Navara feeds and retain failure history in migration evidence.
Keep refresh, cancellation and label policy engine-neutral; inject the original native entities at composition. Prove static geometry, analyst records, owner disposal and last-good refresh behavior. Native dev and QA-preview pass 18 checks each; all eight baseline PNGs remain byte-identical.
Add the sequential W2/completion plan and deterministic earthquake ground-raster, geometry, lifecycle and strict runtime QA. Prove the comparator rejects incomplete evidence and release deleted overlay entries from the collision arbiter. Record native dev/preview acceptance and full checkpoint gates.
Render satellite fleets and selected targets through mover banks and orbit lines.
Enable the celestial HUD on Navara and keep native follow explicitly in ENU.
Add deterministic interaction, share, orbit, motion and cleanup acceptance QA.
Record complete targeted acceptance and remaining shared cross-shell tile failures.
Move native points, synchronous orbit primitives, tracked entities, follow and picking into the Cesium renderer. Use appClock for satellite physical epochs and isolate the unchanged CelestialRing ephemeris. Synthetic core/dense and ISS/MEO/GEO snapshots match the baseline exactly; all gates pass. The pointer-ownership source assertion follows scene.pick into render/cesium.js; registry and import-boundary assertions now reflect the migrated satellite layer.
Minimal inherited the sun light default of castShadow=true although the photoreal tiles carry baked lighting. Use the same no-shadow sun configuration as cinematic. The focused 3-run city gate improved from C4.6/N5.1 ms (1.1087) to C4.6/N4.9 ms (1.0652) with 12000 vessels. Full Node 24 tests and allocation gates, boundaries, format, build and panel build pass.
Fix delayed and growing mover bank commits, reject stale hidden model picks, and route world-focus through the engine. Strengthen vessel parity and performance checks without changing thresholds.
render/navara.js: one always-on-top engine.movers chevron bank (atlas of
every chevron icon at 32 px), MMSI pick ids, trail on engine.lines,
camera reads through the shared Navara layer view (extracted from the
aircraft renderer to src/layers/shared/render/navaraView.js, + camera
pickEllipsoid and canvas clientWidth). Registered in navaraMode.
QA: vessel qaDrawn; probe selected() falls back to getSelectedInfo;
qa-w1-layers vessel section; qa-vessels-engines side by side; --engine
for qa-vessel-cards/datum; qa-flights-engines military near pose 100 km.
Unit tests on the fake engine pass; browser runs NOT done (see progress).
T11 review follow-up to ed9ea7e6. CPU hits rank as the engine draws them:
sets without a depth test over depth-tested ones, then by set order
(the tracked bank over the fleet, flights over local ADS-B), then depth —
so the CPU's top hit that checks a GPU answer is the instance drawn on
top. A model hit is no longer taken as answered: the model mover keeps
each instance's drawn diameter (after minimumPixelSize/maximumScale) and
the CPU checks the hit against its projected position within that radius
(+3 px); the CPU pick sees models at that radius too.
src/layers/vessels/render/cesium.js holds the Cesium objects the layer
built itself, one for one: the translucent BillboardCollection of
chevrons (centred, screen-aligned, never depth-tested), the horizon
occluder, window projection, canvas size, viewer.trackedEntityChanged,
and the trail (data/trailRenderer's entity polyline, kept on Cesium: the
engine.lines Primitive path did not draw below the photoreal mesh in
round 6 — the plan's T13/T15 blocker; flights does the same).
The logic imports no Cesium: geo math, Rgba WHITE, the chevron artwork in
icons.js, clicks through the engine's selection dispatcher (own-rule
owner, same rule). Registered as CESIUM_LAYER_RENDERERS['ais-live-vessels'];
src/layers/vessels joins the boundary rule.
W1 replay vs 7c7422e2: vessels/flights/military/localAdsb counts, known
objects, screen points, requests and shot sha256 identical.
qa-vessel-cards --data synthetic 2/2 and qa-vessel-datum (?qaFeed=
vessels:fixture) 4/4, the same as 6eec8f6c.
Tests moved with the code (same meaning): cameraHandoff (the click body
is handleClick), inputOwnership (the guard precedes the click rule's
read of the pick), renderers.test (vessels in the extracted list),
importDirections (an unmoved example layer is satellites), the
ownership services fixture (renderer service).
Vessels' click rule (sibling ownership, cyclone background, card hit
test, unkeyed records) does not map onto the plain or look-under rules:
an owner with onClick(ctx) gets every click with the top pick, no gesture
rule (its old LEFT_CLICK handler), and is another layer to the rest.
getNearby returns the contact's label as `id` (the callsign when it has
one) and its key as `icao24`; the drawn rows are keyed by icao24, so the
model lookup missed and every contact was measured at its sprite. Under a
3D model that sprite is hidden and keeps the pre-floor height by design
(T7): the "buried at -27 m" contacts on Cesium (the geoid at AUS) were
those hidden sprites, at HEAD and on 7c7422e2, whose script measured every
sprite. The Cesium report also gives each contact's model state and the
floor cell the layer reads.
AUS, 100 s after enabling flights, both engines: 3 low contacts, 0 buried;
N3752 / DAL848 drawn by their models at 130.2 / 135.4 m over a 123.5 /
128.6 m mesh, the DEM-prior cell warm (123.5 / 128.6 m).
The instance colour (sRGB, as Cesium takes colours: the military amber
#FFB800) was mixed into the linear material colour as is, so a tinted
model drew paler and brighter than Cesium's: the military footprint gate
read 124 % (more pixels cleared the contrast threshold over water) and
the median luminance +9 %. White (flights) is the same either way, which
is why the flights gate passed. The tint is now sRGBTransferEOTF-decoded,
as the sprite sets already decode theirs.
qa-flights-engines' model gate adds the highlights: the p90 luminance of
the model pixels within ±10 % of Cesium, with the near-white pixel count
reported (10b review low 2).
Models on, near pose (computed scale ratio ~1.0000 on both):
- military (from 100 km): footprint 9,902 vs 8,610 px (115 %), median
luminance 158.9 vs 155.3, p90 190.9 vs 184.5 — 18/18;
- flights: footprint 104 %, median 189.8 vs 196.7, p90 223.1 vs 247.8
(near-white px 83 vs 510: Cesium's white models saturate where Navara's
tone mapping rolls them off) — 20/20.
Models off: flights 16/16, military 15/15. Side-by-sides regenerated.
- The DEM prior column read a fresh import of src/data/groundFloor.js, a
module the app does not use for its layers (they read the scene's
surface services), so it was null for every contact. It now reads
window.__godsEyeView.surfaceServices.groundFloor. The terrain proxy
answers 200 and the AUS cells resolve within one or two polls (123-128 m);
the "proxy 503" reading in the task-11 report was this misread.
- A contact a 3D model draws is measured at the model's translation (the
probe's drawn() reports it now): its sprite is hidden and keeps the
pre-floor height by design (T7), so measuring the sprite reported a
buried aircraft that is not drawn.
Navara 0.2.1 can answer a click with the previous gesture's pick. The
check let that answer through when its instance was drawn within twice its
pick radius of the click — the followed aircraft at the centre beside the
clicked one (the 20/24 flights checklist run: a click selected the
previously followed aircraft). A GPU answer for a sprite or point set now
has to be drawn under the click (no slack) and be the instance the CPU
pick finds on top there (lines left out: they are not in the GPU pick);
otherwise the CPU pick answers. Models, which the CPU pick does not see,
are still taken as answered. The CPU drill takes `includeLines`.
Test: 20 gestures alternating a click on A and a click on its neighbour B
answered with A's stale pick — B every time. In the app, 40 scripted
click/follow/release gestures on Navara selected the clicked aircraft
every time with and without the fix (the stale answer is rare).
Task 12b. src/layers/localAdsb/render/navara.js implements the Local
ADS-B renderer interface of 12a on Navara:
- markers: the shared aircraft renderer's glyph sets in magenta #ff4fd8
(class silhouette and scale, ×3 near / ×0.5 far, always on top), drawn
under every sprite layer as Cesium draws the entities; each frame writes
what Cesium's CallbackProperties gave (position, screen rotation,
visibility) and the 2,000 km display distance;
- the 978 MHz UAT ring: its own sprite set, a white ring atlas tinted
#ffb3ef, Cesium's 24 px point plus its 1.5 px outline (27 px), the same
distance scale and distance, pickable as its aircraft;
- models (the Flights GLBs, magenta MIX tint), trail and head: the shared
aircraft renderer's; the camera never follows a local aircraft.
Registered by registerNavaraLayerRenderers (import()); the Navara catalog
now offers flights, military and local ADS-B.
- ?qaFeed=local-adsb:fixture: the local receivers' answer in the server's
format (src/bench/w1Feeds syntheticLocalReceivers: 40 aircraft near JFK,
10 on 978 MHz UAT) through the layer's own decoder-feed session
(sources.localAdsbFeeds, null by default: the layer makes its own).
- qa-w1-layers --layer=local-adsb: a click selects a marker and shows its
card with the receiver line, the camera does not follow, Escape leaves
the selection (it never answered Escape), empty map releases it.
qa-w1-layers local-adsb: Cesium 12/12, Navara 13/13, --compare parity
(global pose: the 2.27 px camera offset only). qa-flights-engines
--layer=local-adsb: 14/14 (fills exact; UAT rings side by side in
docs/migration/phase3/localAdsb).
A sprite/point set uploads its instances by update range. When the camera
went far enough to move the set's origin, update() rewrote every instance
(relative to the new origin) — and a commit later in the same frame
cleared the ranges and kept only its own, so the GPU never got the
rewrite: the instances not committed again drew offset by the origin move
(a military glyph at the orbit pose drew 45 px from its point after the
global → city → orbit sequence; the CPU picks, the probe and Cesium had it
in place). Ranges now add up until three uploads them (it clears them
after the upload).
Found by the military side-by-side (11b); flights' sprites had the same
exposure since 10b.
Task 12a. The Local ADS-B logic (index.js, models.js) no longer imports
Cesium. src/layers/localAdsb/render/cesium.js holds what it built, one
for one: the CustomDataSource of marker entities (billboard with
per-frame position/rotation/visibility CallbackProperties, the 2,000 km
DistanceDisplayCondition, the 978 MHz UAT ring as a PointGraphics), the
model PrimitiveCollection (models load, pose and leave through the shared
aircraft renderer's model part, adopting that collection), the selected
aircraft's trail (data/trailRenderer, as before) and live head segment,
and the camera reads. Registered for 'local-adsb' in the renderer
registry, created at init(viewer, {engine}).
- Clicks go through the engine's selection dispatcher (drill 8) instead of
the layer's ScreenSpaceEventHandler, with the layer's own rule kept as is
— a new `lookUnder` owner kind: its marker on top, or under picks no
layer owns and under trail lines, is selected (and re-selected);
another layer's contact on top, or empty map, releases only its own
selection; a trail line alone does nothing; no gesture rule. To the
other owners it is another layer, as its pick-owner registration made
it before. (The dispatcher's `drillThrough` would have taken the marker
from under another layer's contact, which the old handler never did.)
- models.js reads the camera through the renderer's view (frustum and
horizon tests, ground snap owner) and draws through renderer.models
(loadFleet/add/remove/destroy/setPose: the same MIX-tinted glTF options,
the same HPR frame with the Flights nose offset). The shared aircraft
renderer gains a loadModel seam (default Cesium.Model.fromGltfAsync).
- Cesium math → src/geo; the model tint is an Rgba equal to the old
Cesium.Color; check-import-directions: src/layers/localAdsb/ joins the
rendered layers.
Tests moved with the code, same meaning:
- index.test: the services fixture creates the layer's Cesium renderer and
the selection dispatcher; the click harness scripts the picks on a fake
engine registered for the fake viewer (was a fake ScreenSpaceEventHandler)
and the app's pick owners (trails, a Flights contact) are registered in
the pick registry (was an injected picking service).
- cockpitModels.test: the GLB loads are the aircraft renderer's loadModel
calls (Cesium.Model.fromGltfAsync by default), each asynchronous:false.
- trackingClickGesture.test: the dispatcher's empty click goes to the
plain owners (filter(plain)).
- renderers.test: local-adsb has an extracted Cesium renderer.
New: the dispatcher's look-under rule beside a plain owner (7 click cases).
W1 replay (flights, military, localAdsb) vs a fresh 7c7422e2 run: counts,
known objects, screen points, tracking poses, requests and every shot's
sha256 identical.
On Navara each contact row now carries the surface probe's source and tile
size and the coarse DEM floor (cachedGroundFloor). The buried contacts
(SWA4871 60.5 m and AAL2592 -27 m against a 121-126 m mesh) have no DEM
prior (null: the terrain proxy answers 503 here) and the mesh floor sampler
latches nothing without one, so their sprite heights stay the barometric
fallback; the same rows bury on Cesium when the same contacts are low.
The near-pose finding (Navara models faint, Cesium ~23 bright aircraft)
came from the side-by-side's frozen page clock: the fleet tick ran once per
pose, so Cesium never handed its aircraft to their models and drew 3×
glyphs, against real models on Navara. With the hand-off driven (the move
ends every 10 frames while a pose loads), both engines draw models, and
the comparison showed three real differences, all fixed:
- Orientation: Navara's glTF loader is three's GLTFLoader, which keeps
the scene Y-up; the instance turn is now Cesium's whole axis correction
(Y_UP_TO_Z_UP × Z_UP_TO_X_UP), not Z_UP_TO_X_UP alone. Every fleet model
drew 90° off its heading.
- Size: minimumPixelSize is taken at Cesium's distance (Camera
distanceToBoundingSphere: view depth less the radius at the model's own
scale), in the mover and in ModelHandle.computedScale; the straight-line
distance drew off-centre models up to 16 % larger.
- Colour: the instance colour tints by the set's blend amount (Cesium's
colorBlendAmount) and its alpha is the model's opacity (Cesium: a
model colour's alpha makes it translucent), not a blend weight; a model
is translucent only while faded. An ambient share of the tinted surface
(MODEL_AMBIENT 0.5, Cesium's image-based lighting) keeps a tinted
aircraft near-white at a low sun (median luminance 139 → 190; Cesium 197).
qa-flights-engines (now --layer=flights|military) gates the near pose
with --models=on: computed scale within ±5 % per model (ratio 1.000007),
footprint against the same view with the layer off within ±20 % (1553 vs
1496 px, 104 %), median luminance within ±10 % (189.8 vs 196.7). The probe's
drawn() reports a model's computedScale. Flights models on: 19/19.
Task 11b. src/layers/military/render/navara.js: the shared Navara aircraft
renderer with the military's sprite slot (SPRITE_LAYER_ORDER 'military',
under flights) and jet.glb's nose offset; registered by
registerNavaraLayerRenderers (now a table of lazily imported factories,
flights and military), so the Navara catalog offers military and the main
chunk stays without three/Navara.
- The follow passes between aircraft layers on Navara as on Cesium: the
follow-handle → tracked-handle map and the "follow being started" handle
are shared by every aircraft renderer of the page, so a layer whose follow
another layer took reads the new owner's tracked handle (and its
gevSelectionOrigin) — before, it read a bare follow handle and reported
'programmatic'.
- trackById on a selected-but-not-followed contact follows it again (as
flights since 10b; on Navara any camera command ends a follow).
- qaDrawn (the probe's drawn()) for military, shared with flights
(src/layers/aircraft/qaDrawn.js).
- ?qaFeed=military:synthetic[:count]: seeded readsb rows (1 000 by
default, 150 near JFK, 12 on the ground, 8 types), the first 25 hexes
shared with the synthetic civil flights.
- qa-w1-layers: sections 4–6 read the --layer's module (were 'flights');
section 5 keeps the queries beside the voice answers (the voice block
overwrote them, so --compare never compared queries); for military: the
follow passes military → flights → military, and flights leaves the 25
shared aircraft to military while it is on (9 975 / 10 000 / 9 975).
qa-w1-layers --layer=military: Cesium 25/25, Navara 26/26, --compare parity
(queries included; the known 2.3 px camera offset at the global pose only).
Task 11a. The military flights logic no longer imports Cesium: it draws
through the shared aircraft renderer (src/layers/aircraft/render/cesium.js)
with the military's parameters in src/layers/military/render/cesium.js
(sprite-order slot 'military', jet.glb's 180° nose offset), registered for
'cesium' in the renderer registry and created at init(viewer, {engine}).
The same pure move as flights in 10a:
- Fleet: the BillboardCollection and its sprite-order slot (enable keeps
restoreSpriteOrder, as before); sprites stay Billboard handles.
- Models: fleet/tracked/preload glTF loads (asynchronous:false, MIX tint at
MODEL_COLOR_BLEND_AMOUNT, IR boost as `unlit` → the UNLIT CustomShader),
the model collection, setPose. The per-class specs keep their own nose
offset: setPose takes an optional offsetDeg (default the layer's), on
both aircraft renderers (same arithmetic, test added).
- Tracked: the ENU billboard Entity with its position/colour/rotation
callbacks (#FFD166, transparent amber once the model owns the visual),
viewFrom, followed through viewer.trackedEntity + the one exact tracked
frame; refocusTrackedById through follow.refocus.
- Trail: the entity trail and the floored live head segment.
- View: camera/scene reads, frame number, frustum and horizon tests,
render requests, frame events.
- Clicks and Escape go through the engine's selection dispatcher (the
ScreenSpaceEventHandler and keydown listener are gone); the cross-layer
untrack watches the renderer's follow. Same rules: cockpit clicks are
inert, the tracked visual/model is a no-op, another layer's pick is not
empty space, an unowned pick is.
- Colours are engine-neutral Rgba (MIL_ICON_COLOR #FFB800,
TRACKED_ICON_COLOR #FFD166, AMBER_TRANSPARENT), NearFarScalar →
nearFarScale; Cesium math → src/geo.
- check-import-directions: src/layers/military/ joins the rendered layers
(cesium only in render/cesium*.js).
- The military registry and the flights' duplicate suppression are
untouched.
Source-text tests updated with the moved code, same meaning:
- militaryAwareness.test: military's hasContact guard reads fleet.visible().
- trackingClickGesture.test: the military gesture rules are the
dispatcher's (selection gesture before select, click classifier before
the empty-space clear), military answers the empty click with a
user-origin clear — the same assertion flights already had.
- ownership.test: the services fixture names selection and renderer; the
app-composed services build the layer's Cesium renderer.
- renderers.test: flights and military have extracted Cesium renderers.
New tests: the military logic over a plain-data renderer and a fake engine
(amber fleet, click-select, #FFD166 tracked glyph, no-op on the tracked
visual, empty-click and Escape release, the keyboard left on destroy); the
plain-data renderer moved to src/testSupport for both aircraft layers;
setPose with a per-model offset.
W1 replay vs 7c7422e2 (the 10a reference run): every layer's counts, known
objects, screen points, tracking pose after 2 s, requests and the
city/global/orbit shots (sha256) identical. qa-military-names (it checks
the installations layer): the same verdict as 7c7422e2 (the zoom-markers
assertion fails on both). Main chunk 2,890,763 B (−2.9 KB).
On Navara the military-awareness module has no controls, so its
releaseCameraOwnership was skipped silently and a city pill, voice
fly_to_location or reset ended the follow but kept the aircraft selected.
The trackers it would release are released directly instead, as on
Cesium (selection and follow both cleared).
Inside a Navara follow the orientation controls frame the follow's target
and retarget its viewFrom instead of moving the camera, and the release
cancels only a running flight (a cancel is a camera command, which ends
the follow). Cesium's trackedEntity path is unchanged. Updated test:
cameraOrientationControls.test.mjs (orientation navigation starts a
flight before expecting its cancel; no flight, no cancel).
FollowHandle.target() returns the followed point, checked by the contract
on both engines. The contract's model check now waits for ready() before
looking for the model's pixels: the magenta point and sprites could fill
the region before the glTF loaded (qa-globe-contract cesium 73/73,
navara 79/79).
qa-floor-verify --engine=cesium|navara: on an engine without a viewer the
camera pin, the tile readiness, the mesh probes (engine.surface
sampleHeightDetailed, tiles only, excluding the layer's mover sets) and the
sprite census (the probe's drawn()) go through the engine. The Cesium path
is unchanged. Live AUS traffic: Cesium FAIL as on 7c7422e2 (13 contacts at
the -27 m geoid fallback); Navara FAIL with 1 of 15 low contacts under the
mesh (68.6 m against 121.4 m). Task 10b, checklist item 7.
The QA probe gains drawn(layerId): what a layer draws per object (glyph
image, scale, shown, screen rotation, alpha, position; its 3D model's
scale, shown, ready), from the flights layer's qaDrawn(), which reads the
renderer's handles, so it is the same on every engine.
qa-sprites-b5 and qa-enrich-ambient take --engine=cesium|navara: without a
viewer they read the layer through the probe and move the engine camera;
the military rows/cluster are skipped where the engine has no military
layer. Cesium verdicts unchanged (sprites 9/9; enrich 9/2, the same E8/E10
failures as on 7c7422e2); Navara: sprites 7/7 (flights rows and 3D cluster),
enrich 9/2 (the same two). Task 10b, checklist item 7.
Task 10b:
- qa-w1-layers (flights): 3 focus (the selected object is the focus target,
at the followed centre ≤ 3 px), 4 tracking (held ≤ 3 px for 5 s; wheeled
in, ≥ 149 m), 5 the layer's queries (tracked info, contacts, nearby,
detectable; compared by --compare), 6 share links (the followed object
restored on the same and on the other engine). Clicks only on points the
canvas takes (the Navara attribution card covered the old empty point).
--compare reports the global pose's 2.3 px as the cameras' (Cesium
projects its own nadir 2.27 px off centre at 15,000 km).
Cesium 19/19, Navara 20/20, parity.
- qa-flights-engines (new): the same frozen synthetic fleet at the global,
city, orbit and near (JFK, oblique) poses on both engines, UI hidden;
side-by-side JPEGs (docs/migration/phase3/flights, models off and on),
the same objects in the frame, screen points ≤ 2 px, glyph fill colours
(brightest pixel at the glyph) within 3. 12/12 with models off and on.
- qa-labels: on an engine with flights but not military/satellites, the
deterministic 12,000 / 5,200 field is all aircraft, so detection is
gated on Navara too: 14/14 (Cesium 14/14 unchanged).
Task 10b. src/layers/aircraft/render/navara.js implements the aircraft
renderer contract on the engine contract alone (no three, no Cesium):
- Fleet glyphs: banks of engine.movers sprite sets (one per distance scale
and depth mode the handles ask for; flights: one, 'always', Cesium's
NearFarScalar), growing by doubling, from a glyph atlas of every
aircraftIcon() kind at the fleet (64 px) and tracked (192 px) rasters
(data/aircraftIcons: AIRCRAFT_ICON_KINDS, aircraftIconCells,
aircraftIconAtlasLayout, drawAircraftIconAtlas). Sprite handles keep the
Billboard field vocabulary as setters; a frame's writes commit once at
the end of the frame listener that made them (a microtask outside one).
- Models: banks of model mover sets per glTF and look; handles report
ready from MoverSet.ready() and computedScale from minimumPixelSize kept
on the CPU (radiusM in the spec, the class's nativeRadiusM); poses are
Cesium's HPR frame (modelMatrix) turned by Navara's glTF axes
(Z_UP_TO_X_UP: its loader is already Z-up; chosen side by side with
Cesium among six candidates).
- Tracked glyph: a sprite whose position/colour/rotation callbacks are read
every frame in preRender; follow = engine.camera.follow with the tracked
readout's card as its label (gevFollowHandle); current() maps the follow
back to the tracked handle, also while follow() raises followChanged.
- Trail and live head: engine.lines (alpha 0.85, depth-fail 0.4, geodesic)
in the gev-trail pick namespace.
- View: camera/scene adapters over engine.camera.pose() (registered with
the engine so shared helpers project through it), frustum and horizon
tests, frame number, engine frame events.
flights/render/navara.js registers it for 'navara' from the Navara mode's
import(); the Navara catalog now offers Live Flights.
Also: tracking a contact that is selected but no longer followed follows it
again (a camera command ends a Navara follow; on Cesium a search flight
releases trackedEntity): trackById/select re-follows instead of only
re-publishing. Behaviour change on Cesium only in that case.
Tests: the renderer on the fake engine (contract, atlas, banks, per-frame
commits, growth, tracked callbacks, follow label, model pose, trails) and
the flights logic over it (fleet, click-select, follow, re-follow after a
camera command, release, no set left behind).
Linear, without mipmaps: Cesium's billboard atlas has none, and a mipmapped
atlas greyed small glyphs Cesium draws at full colour (a 5 px aircraft at
15,000 km: 239 against Cesium's 255; now 255 on both). Task 10b colour
parity.
Review of 10a (low):
- The spoken answer to set_layer_visibility on a layer this engine lacks is
the refusal's own sentence (why, and the switch to ?engine=cesium), not
only "Couldn't turn on".
- analyst_query naming layers the catalog left out (no viewer) refuses with
UNAVAILABLE_ON_ENGINE and the layer ids instead of answering zero.
- stop_tracking skips layers the engine does not have, so with nothing
tracked it is a no-op success on Navara (it reported the absent military
and satellites layers as failures).
Tests for each (the old code fails the last two); Cesium unchanged.
Task 10b foundation.
- focusDeemphasis projects through the engine registered for the scene it
is given (engineFromViewer(scene).projector), as iconOrientation already
does, else Cesium's SceneTransforms. On Cesium the engine projector is
SceneTransforms, so nothing changes there; on Navara the aircraft
renderer's scene adapter is registered with the engine. Test with a fake
engine.
- refreshTrackedReadout accepts a layer's tracked visual that names its
follow handle (gevFollowHandle): engines without tracked entities
republish the card through it. Test.
Task 10b foundation.
- MoverSet.ready(): whether what the set draws has loaded (Navara: the
model set's glTF, a URL atlas; Cesium: every instance's Model, the cropped
atlas; parts without it draw at once). The aircraft renderer keeps the 2D
glyph until a model set is ready, as the Cesium path waits on Model.ready.
Contract suite: a drawn model set is ready() (both engines); unit test of
the neutral store.
- Navara sprites and points interpolate scaleByDistance as Cesium's
NearFarScalar (over the squared camera distance, eased by ^0.2) instead
of linearly in distance, so aircraft glyphs have Cesium's size at every
range; the CPU pick radius follows (distanceScale, tested equal to
focusDeemphasis' port of czm_nearFarScalar). The contract text says so.
The MCP panel build inlines every dynamic import into its one script, the
Navara mode included, so the guard that keeps Navara out of the app's
initial chunks failed it (npm run build:panel was red since d483e589). The
panel config filters the guard out, as phase 4 does; a test pins it.
build:panel is green and joins the gates. Phase-4 merge review item 2.
src/app/qaProbe.js was statically imported by standalone/application.js
and shipped in the main chunk (+639 B gzip). It now loads with import()
behind import.meta.env.DEV at the import site: production builds drop it
(main chunk 2,890,982 -> 2,889,432 B), the dev server still installs
window.__godsEyeView.layers (checked headless). Phase-4 merge review item 1.
Task 10a. The flights logic no longer imports Cesium: every Cesium object it
built moved one for one into the shared aircraft renderer
(src/layers/aircraft/render/cesium.js; contract in render/contract.js), with
the flights' parameters in src/layers/flights/render/cesium.js, registered
for 'cesium' in the renderer registry and created at init(viewer, {engine}).
- Fleet: BillboardCollection + sprite-order slot; sprites stay Billboard
handles (field vocabulary documented for the Navara renderer).
- Models: fleet/tracked/preload glTF loads (asynchronous:false, MIX tint, IR
boost as `unlit` → the UNLIT CustomShader), the model collection, setPose
(same HPR frame with airplane.glb's 180° offset).
- Tracked: the ENU billboard Entity with its position/colour/rotation
callbacks, viewFrom, followed through viewer.trackedEntity + the one exact
tracked frame (kept as is; engine.camera.follow on Cesium would also end
the follow on any camera command, a behaviour change).
- Trail: the entity trail (data/trailRenderer, unchanged: its Primitive
predecessor failed below the mesh) and the live head segment.
- View: camera/scene reads, frame number, frustum and horizon tests,
projection, render requests, frame events, evidence render-loop hold.
- Clicks and Escape go through the engine's selection dispatcher (the
ScreenSpaceEventHandler and keydown listener are gone); the cross-layer
untrack watches the renderer's follow.
- Colours are engine-neutral Rgba (src/layers/shared/color.js), bit-equal to
the Cesium.Color constants they replace; NearFarScalar → nearFarScale.
- Cesium math in the logic → src/geo.
- check-import-directions: cesium only in render/cesium*.js and three/Navara
only in render/navara*.js for flights, aircraft and shared; a layer's
Navara renderer only through navaraMode's import().
Source-text tests updated with the moved code, same meaning:
- readLayerSource reads a layer's render/*.js and, for the aircraft layers,
aircraft/render/*.js (GLB-load, tracked-entity, CallbackProperty checks).
- modelScale.test: _normalBillboardScaleByDistance reads nearFarScale(...).
- spriteOrder.test: flights' enable calls fleet.restoreOrderOnEnable(); the
renderer wires it to restoreSpriteOrderOnEnable('flights', viewer).
- trackingClickGesture.test: the flights gesture rules are the dispatcher's
(selection gesture before select, click classifier before the empty-space
clear); flights answers the empty click with a user-origin clear.
- militaryAwareness.test: flights' hasContact guard reads fleet.visible().
- ownership.test: the services fixture names selection and renderer.
New tests: color, the Cesium aircraft renderer (contract, collections, pose,
loads, frustum/horizon, tracked entity, follow, trail head, adopted
collections), the flights logic over a plain-data renderer and a fake engine
(fleet, click-select, no-op on the tracked visual, empty-click release), the
boundary rule.
W1 replay vs 7c7422e2: counts, known objects, screen points (0 px), tracking
poses (0 m), requests and shots (same sha256) identical. Main chunk
2,890,982 B (+6.2 KB, +0.21 %: the renderer indirection).
Task 10a moves flights onto the dispatcher; its old handler deselected on a
pick nobody owns (an unclaimed entity), and only another layer's pick was not
empty space. The dispatcher now does the same (after drill-through owners had
their chance), and selectionDispatcherFor(engine) gives the application one
dispatcher per engine. Tests for both.
On Navara the engine's persistent notice came up right after the share
restore's NOT ON THIS ENGINE notice and replaced it at once, so the names
of the linked layers this engine lacks were never seen (and a later finite
notice would in turn have wiped the engine notice for good).
- ShellFeedback keeps a persistent notice that a finite one covers and
brings it back when the finite one ends; a persistent notice that arrives
under a live finite one waits for it.
- Notices take a dwellMs; "Kept in the link: …" stays 15 s.
- scripts/qa-engine-notice.mjs (headless): a Cesium share link with a layer
Navara lacks, opened with ?engine=navara: notice shown with the layer
name, still up 14 s later, then the engine notice is back. 4/4 (before
the fix the notice never appeared).
T8-T9 review item 2.
The ?qaFeed import is now gated by import.meta.env.DEV at its import site,
so Rollup drops the qaSources chunk (4.7 KB) from production builds instead
of shipping it unreachable. Dev behaviour is unchanged. (T8-T9 review item 3)
Review of T8-T9:
- The UNAVAILABLE_ON_ENGINE refusal now names the feature (UNAVAILABLE_FEATURES)
and suggests the Cesium engine (?engine=cesium): "The cockpit is not
available on the Navara engine yet. Switch to the Cesium engine
(?engine=cesium) to use it." New fields: feature, suggestion.
- set_visual_style and set_post_processing refuse without a viewer
(CESIUM_LOOK_ACTIONS): Navara has no visual styles, so they used to answer
ok, change nothing and record a style get_current_view_state then reported.
- set_layer_visibility on a known layer the engine's catalog left out refuses
with the same structured result instead of throwing "Unknown data layer".
A name no engine knows still throws.
- Removed the dead summarizeEntity and the two helpers only it used.
Cesium (viewer present) is unchanged; tests cover each refusal and that the
style still applies with a viewer.
Phase-4 QA: after a map-stack switch at the whole-globe view Navara
sometimes showed the new map only seconds later. Navara 0.2.1 reports idle
and stops drawing before a raster tile it fetched arrives, so the last
frame stays stale until something else asks for one. Every engine.maps
change (base imagery, terrain, photoreal tiles, their release) now asks for
a frame and keeps asking every 250 ms for 6 s, then the engine idles again.
scripts/qa-map-switch.mjs switches the stack (photoreal / Esri / OSM) at
18 000 km on each engine, waits for the tiles, hashes the picture, forces
frames and hashes again: a change means a stale picture. Before: Navara 3/8
switches drew the new picture by themselves; after: Navara 8/8, Cesium 8/8.
createViewerlessTools creates the voice commands with viewer: null on the
engine (tools.js passes it the voice options and place search), so
window.__gevVoiceCommands exists in ?engine=navara: the W1 tools drive the
engine and the viewer-only tools answer UNAVAILABLE_ON_ENGINE. The Cesium
path hands its engine to initGevVoiceCommands explicitly.
viewerlessTools.test.mjs: the Navara tools now return voice commands
(created without DOM through injected control and session stand-ins),
whose runner answers get_current_view_state on the engine and refuses
control_scene; the debug handle carries them and teardown removes them.
The voice runner drives the globe engine (an `engine` option, by default
the one registered for the viewer) instead of the Cesium viewer:
track_entity, stop_tracking (camera.following().stop()), frame_overhead
(camera.flyToSphere with the pose heading), fly_to_location, zoom_to_globe
(follow released through camera.following()), adjust_camera_zoom,
next_iss_pass / next_satellite_pass, select_nearest_aircraft and the
analyst view context, get_current_view_state, get_entity_context
(projector.worldToWindow), the view target (picker.pickPosition, then
camera.pickEllipsoid, then picker.pickGlobe) and its moveEnd prewarm, and
the viewport samples. getBasemapLabelContext takes the engine or a viewer
with a registered engine (the HUD).
Without a viewer the tools that still need one (VIEWER_ONLY_ACTIONS:
control_scene, annotate_map, clear_annotations, fly_route, move_camera,
control_cockpit, set_context_mode, control_radio, control_cctv) answer a
structured refusal (code UNAVAILABLE_ON_ENGINE) instead of throwing.
Point-and-ask picks with engine.picker and keys the camera pose from
engine.camera.pose(); the viewport capture reads engine.host.canvas and
waits for the engine's postRender (both engines keep their drawing
buffer: Cesium's contextOptions, Navara's own WebGL2 canvas, S4).
Tests: gevActionsEngine.test.mjs runs each moved action on the fake
engine. gevActions.test.mjs keeps its expectations; its runner helper now
registers an engine for the test viewer (the Cesium engine over it, or a
fake engine at its positionCartographic) and the two harness cameras gain
roll and frustum for camera.pose().
- src/app/qaProbe.js → window.__godsEyeView.layers: list() (with
`available`), unavailable(), available(id), enable/disable(id) (a user
toggle through the layer manager), objects(id) → [{ id, label, position,
screen }], screenOf(id, objectId), selected(id), following(), movers(),
framesDrawn() - all through the engine and the catalog's availability, the
same on Cesium and Navara. Installed after the tools.
- src/app/layers/qaSources.js: `?qaFeed=flights:synthetic:10000,
vessels:fixture` (dev server only, allowQaRegistration) replaces those
sources with seeded synthetic answers served through the live sources'
own parsers (same snapshot shape; loaded with import() only then). The
pure generators moved from src/tooling/w1Feeds.js to src/bench/w1Feeds.js
(the browser graph may not reach src/tooling); the W1 recordings
re-export them.
- scripts/qa-w1-layers.mjs: the parity checklist's frame, --layer
--engine=cesium|navara with the fixture feeds, JSON per run and
--compare (objects on screen per pose, 5 known objects within 2 px).
Filled: 1 view, 8 idle, 9 errors/no leftover mover sets after 10
on/off; 2-7 report TODO for the layer tasks.
- qa-perf, qa-labels, qa-overlay-baseline and qa-label-readability take
--engine= and use the engine (camera, frames, render requests, tiles) and
the probe (layers, following, movers) instead of the viewer; checks whose
layers an engine lacks are SKIPPED with the reason (Navara today), as are
qa-perf's animated-style checks there (Cesium post stages).
Verdicts, Cesium, before → after: qa-perf 24/24 → 24/24, qa-labels 14/14 →
14/14, qa-overlay-baseline (datacenters, firms, detection-50) 2 measured +
1 skipped → the same. Navara: qa-perf 14 passed, 3 skipped; qa-labels 3/3,
1 skipped; qa-overlay-baseline 3 skipped; qa-w1-layers flights: Cesium 5/5
(10 000 synthetic aircraft), Navara skipped (no flights renderer yet).
Review of Task 7: every allocation workload ran the Cesium engine on a mock
viewer with identity matrices and the horizon test off, so the per-label
horizon test and the Navara projector were never in the gate. The new
`navara-projector` profile gives the host the Navara engine's projector
(cameraMath worldToWindow, viewProjection, horizonVisible, as
createNavaraEngine wires it) over a real WGS84 camera 2 000 km up, with 250
moving labels on a lon/lat grid under it and the horizon test on.
Node 24.14 (the calibrated runtime; CI's 24.14.0 matrix leg runs the gate
with GEV_REQUIRE_ALLOCATION_GATE=1): 10 905 B/frame median (generic 250
labels: 9 681), 43.6 B per candidate; budget 13 000 B/frame as the generic
saturated workload. All 14 workloads pass on 24.14.
Review of Task 6: from 15 km with cold tiles Navara answers from 3.5-4 km
tiles (13.9 km at AUS), below the 4 km floor limit, 6-16 m off; the mesh
floor samples up to 25 km, so those would latch. MAX_FLOOR_TILE_SIZE_M is
now 1 km: from 800 m the answering tiles are 112-314 m. The 1.8 km tile
that read the Austin ground right under a 600 m camera is now a miss too,
which only leaves the DEM prior in place.
qa-surface-heights runs the 15 km pass first in a fresh page (cold), then
the 800 m pass in another, and keeps every answer the floor rule would
take while the tiles stream. Measured: 800 m 19/20 within 2 m of Cesium;
15 km cold 7/20 (the plan's gate of 15 assumed warm tiles - lod 'finest'
can only use what Navara has loaded, and at 15 km it loads 3.5-14 km
tiles), and the floor rule takes 0/20 of those; while streaming it took
answers more than 2 m off at 3 places: the Empire State spire (39 m, also
the settled answer) and two street points 2.2-2.3 m off.
Review of Tasks 6-7: a 240 px drag right turned Navara's follow by -67.5°
where Cesium turns +119.5°, and a right drag did nothing (Cesium dollies,
1.95 Mm → 0.91 Mm). followGestures.js now follows Cesium's
ScreenSpaceCameraController for a tracked camera:
- left drag (rotate3D): heading += 1.77 × dx / width × 2π (clockwise for a
drag right; 1.77 is Cesium's maximum rotate rate, which a tracked camera
beyond a few metres always reaches) and the camera rises by
1.77 × -dy / height × π; each move counts at most a tenth of the canvas
(maximumMovementRatio);
- right drag (zoom3D): follow.js dollyOffset moves 5 × dy / height of the
distance above the floor (closing) or of the range (opening), never
inside it; no context menu while following.
The contract follow check turns the camera with a 240 px drag right and
asserts the heading turn against Cesium's rate (±15 %; the harness holds the
pointer before releasing it so Cesium adds no inertia), and dollies with a
right drag down (range below 80 %, never under 149 m). Cesium 73/73, Navara
79/79.
- src/app/layers/renderers.js: the layer renderer registry
(registerLayerRenderers(engineKind, { [layerId]: factory }),
rendererFor(layerId, engine), layerAvailableOn(layerId, engineKind)).
Every application layer is registered for Cesium as CESIUM_BUILTIN (it
still draws itself until its render/cesium.js); Navara factories come with
the layer tasks. On it, the stable availability API phase 4 injects into
its layer list: createLayerAvailability(engineKind) →
{ engine, isAvailable(id), available(), unavailable() } with { id, name }
rows (src/app/layers/directory.js holds the ids and names in catalog
order; a test keeps it equal to the constructed catalog).
- createApplicationCatalog({ engineKind, availability }) constructs only the
layers the engine can show, in the same order (shared instances made
once, on demand); the catalog carries `availability` and the others'
metadata (`unavailableMetadata`). Cesium builds the same 30 layers in the
same order as before; Navara builds none yet (no Navara renderers) - the
standalone app now asks the catalog for it instead of making an empty one.
- catalogControlServices: a role whose layer the engine leaves out gets an
empty stand-in (absentLayerControlServices folded in); any other missing
layer is still a wiring error.
- LayerLifecycle(viewer, { engine }): every module call (init, enable,
update, disable, destroy) gets { signal, engine } as its second argument;
viewer is null on Navara.
- Links: a link that turns on layers this engine leaves out keeps them in
the durable state (re-saved links carry them; back on Cesium they turn
on) and activates nothing; a notice names them ("NOT ON THIS ENGINE · Kept
in the link: …"). The Navara notice lists the engine's data layers from
the registry.
- The dev launcher's CCTV wiring check matches the new catalog form.
The shell creates detection and the tracked readout on the engine, so the
Navara mode has them too (before: not created without a viewer). Detection
reads the camera height from the engine. The tracked readout follows
engine.camera.following(): a follow's plain-data label
(FollowOptions.label: { id, model, position }, the readout card's model
and anchor) or, on Cesium, the followed Entity and its gevLabelModel as
before (Cesium layers keep publishing the same). refreshTrackedReadout
takes a follow handle too. A bare viewer without a registered engine
(tests) is still read for its trackedEntity.
In the app on Navara: "[Detection] Initialized", "[TrackedReadout]
Initialized", and a follow with a label paints its card.
The world overlay host already projects through engine.projector and tests
the horizon with projector.horizonVisible; the last Cesium value on its
path, the snapshot of an entry's cullPosition, is now a geo Cartesian3.
Navara's horizonVisible is Cesium's EllipsoidalOccluder.isPointVisible:
tested against Cesium's own on 8 000 eye/point pairs (600 m to 20 000 km,
points under, on and over the ellipsoid; no disagreement) and at the limb
(a label 40 m under the ellipsoid just past the horizon is hidden, with its
cullPosition lifted 100 m it is shown). It ran per label per frame with
three new vectors each call; it now uses scratch vectors. Micro-measure
(500 labels per frame, no scavenge during the run): 7 941 B per frame
before, 51 after.
The W1 sources that projected with Cesium's SceneTransforms themselves -
the icon orientation's exact projection and the launch replay overlay - go
through the engine's projector when their scene or viewer has a registered
engine. Satellites, vessel labels and aircraft recession project nothing
themselves (the host does).
scripts/qa-overlay-engines.mjs publishes the same labels on both engines at
a city, an orbital and a limb pose and compares the painted rectangles:
worst 0 / 2 / 0 px; on both, the far-side label is not painted, the lifted
limb label is and the buried one is not.
Review of Task 4: the CPU picks (Navara hover, drill, the stale-answer
check) took a sprite or point at its full sizePx however far it was, so a
set shrunk with scaleByDistance was picked well off its drawn edge. The
radius now scales by the set's scaleByDistance at the instance's camera
distance, linear between near and far as the Navara shader draws it.
Review of Task 4:
- a Cesium pick whose top result is filtered (a pickable: false set, the
follow camera's proxy entity) took nothing; it now drills and answers the
first result under it with an id (an object), else the first kept one
(map tiles);
- billboards and points drawn without a depth test reach Cesium's GPU pick
through the globe: mover instances behind the horizon (projector
horizonVisible on the instance position) are left out of picks and
drills, as Navara's CPU drill already did;
- the follow proxy entity (1 px point, cesium/follow.js) carries
gevFollowProxy and is never a pick (review item 6).
Review of Task 5: while following on Navara a left drag panned the globe
about the ground point under the cursor (range 2,154 m → 54,136 m) and the
wheel zoomed toward that point (462 m after 40 notches, Cesium stops at
the 150 m floor). While a follow runs, Navara's spin, zoom and tilt are
now suspended (input.suspendCameraInputs, which gives the app's own
setCameraInputs choice back after the release) and followGestures.js turns
a drag into heading and pitch of the ENU offset (a canvas width is one
turn, its height half a turn; elevation kept within 2°-89°) and a wheel
notch into 20 % of its range, never inside the minimum range, as Cesium's
EntityView orbits the target. The offset is no longer read back from a
camera the follow alone moves.
Also (review item 7): a follow requests a render only when the target, the
offset or a gesture changed, so a still target lets Navara idle.
The contract follow check drags (range within ±5 %, target within 3 px)
and turns the wheel 40 notches (never below 149 m, ends within 5 % of
150 m) on both engines with real input (qa-globe-contract's pointer gains
drag and wheel); after stop it waits for Cesium's gesture inertia to end
before checking that the camera is free. Contract: Cesium 73/73, Navara
79/79.
Review of Tasks 4-5: the overlay pass encodes linear to sRGB, and the
instance and line colours went in as if linear: grey 0.5 drew 187 (Cesium
128) and [1, 0.6, 0.2] drew 255/203/124 (Cesium 255/153/52). The sprite,
point and line shaders now decode them (sRGBTransferEOTF), as the sRGB
atlas texture already was.
A contract check draws mid-tone sprites, points and lines (grey 0.5 and
[1, 0.6, 0.2]) and reads their centres: within 3 per channel on every
engine. Navara 79/79.
engine.surface.sampleHeightDetailed(place, { exclude, lod }) answers
{ height, source: 'tiles' | 'terrain', tileSizeM? } (src/globe/surfaceDetail.js).
On Navara tileSizeM is the extent of the tile that answered and
lod 'finest' takes the finest loaded tile, drawn or not; Cesium answers
tiles without a size, else the globe's terrain. acceptedSurfaceHeight()
turns an answer into a height or a miss: tiles larger than
MAX_FLOOR_TILE_SIZE_M (4 km) are too coarse, terrain is not the mesh.
The mesh floor sampler and the ground snap are engine-only now (a viewer
or scene reaches its engine through engineFromViewer, which also registers
the scene) and treat a coarse answer as a miss, as they treat undefined.
Measured on Navara at Austin: while the tiles stream, a 27.9 km tile reads
143.2 m where the ground is 122.5 m (a 1.8 km tile).
`exclude` takes the engine's own mover sets (MoverSet.id): Cesium hides a
set's primitive for the synchronous sample (Cesium matches excluded objects
per billboard, not per collection); Navara's ray only meets tiles.
"sampleHeight undefined in the app on Navara" did not reproduce: the
startup Austin sample is 121.9 m (tile 889 m) on Navara and 122.4 m on
Cesium; one ModelMesh class is patched.
S2 QA (scripts/qa-surface-heights.mjs, 20 places at JFK, SFO, AUS, roofs
and streets): Navara within 2 m of Cesium at 19/20 from 800 m and 18/20
from 15 km with lod 'finest' (gates 18 and 15); offsets per region
-0.21 / +0.4 / +0.1 m; the Empire State roof differs by 39 m (spire
geometry). Navara tile sizes at 800 m: 112-314 m.
camera.follow({ position, viewFrom, minRange = 150, native? }) keeps a
moving target centred at an east-north-up offset the user may turn and
zoom; camera.following() and camera.followChanged report it; stop() frees
the camera and a camera command (setView, flyTo, flyToSphere, lookAt,
cancelFlight) ends it.
Probe ruling: Navara 0.2.1's cameraFollow holds the target but ignores the
user's drag and wheel, the aircraft sits ~13 px off centre with 7.6 px of
spread, and calling it without an offset each frame gives a NaN camera.
So Navara (and the fake engine) follow with src/globe/follow.js: each
frame, before Navara draws, the offset is re-read from where the user put
the camera relative to the previous target, kept outside the minimum
range and carried to the new target.
Cesium follows with viewer.trackedEntity: the layer's own Entity
(`native`) or a proxy Entity at position() with a 1 px transparent point,
since EntityView moves the camera only for an entity with a bounding
sphere. applyTrackedCameraFrame and the zoom limits moved to
src/globe/cesium/follow.js; src/data/trackedCamera.js keeps the pure parts
(the range clamp now lives in src/globe/follow.js). following() also
reports an entity the app tracked itself. Camera sensitivity claims made
on a viewer's Cesium camera now share the engine camera's ledger, and the
orientation controls and the world jump read the follow from the engine.
Contract (both engines): a target flying at 250 m/s stays within 3 px of
the centre for 2 s, zoomIn through it stops at 150 m, stop() and setView
end the follow. Cesium 71/71, Navara 77/77; shell 23/25/4.
check:boundaries failed from 76851ee9 on: vite-build did not own the
initial-chunk guard, and the layer and application packages did not own
src/time/clock.js and the movers, lines, picks and Navara overlay modules
they now reach.
Review of Tasks 2–3:
- the mover store's instance checks no longer build arrays or closures
per set() (orientation and colour checked field by field), and dirty()
returns one reused range object;
- Cesium models get their matrix and colour through scratch objects
(Model.modelMatrix and Model.color copy what they are given) instead
of a new Matrix4 and Color per placement;
- a destroyed MoverSet releases its store (count 0, empty instance
arrays, no pick ids) so a set object a layer still holds keeps no
memory, and the Cesium parts drop their billboard, point and model
lists;
- (with the overlay commit) the Navara line viewport is updated in
place.
Node micro-measure, 10 000 instances set + packed per frame: 0.27-0.29
ms, about 1.7 KB per frame (0.17 B per instance), as before. Contract:
Cesium 70/70, Navara 76/76.
Review of Task 0:
- build/navaraDependencies.js reads the dependency closure of
@navaramap/three and @navaramap/three-default-plugin from node_modules
(dependencies and peers) and drops what the app's other bundled
dependencies reach too (Cesium is served as a script, so draco3d, which
only Cesium shares, stays engine-only), plus three and postprocessing:
88 packages today (the @navaramap/* packages, @takram/*, @react-three/
fiber, @sparkjsdev/spark, astronomy-engine, d3*, draco3d, n8ao,
three-stdlib, …). The browser build's initialChunkGuard fails on a
module from any of them in an initial chunk. Tested on a fixture tree.
- check-main-chunk's budget measures the entry the page loads (the same
chunk the composition check reads), falling back to phase 4's largest
index-*.js only for a dist without a page.
Build clean: no Navara-only module in the initial chunks; main chunk
2,869,565 bytes (gzip 889,041).
Review of Task 0: the recorded live feeds left vessels (no AIS key here)
and local ADS-B (no receiver) empty, and flights held one adsb.lol
viewport answer. src/tooling/w1Feeds.js builds seeded answers relative to
one frozen instant (2026-10-16T12:00Z): a global /api/flights OpenSky
snapshot (8 000 aircraft, 2 000 within 60 km of JFK), /api/vessels with
12 000 AIS rows worldwide and the rows within the city area (the area
answer first, the global one last as the replay's fallback), and
/api/local-receivers/aircraft with 40 aircraft near JFK on 1090 MHz and
978 MHz UAT. Tests read them back through the app's own parsers.
qa-w1-baseline --synthesize=DIR writes them beside the recorded
military, satellites and launches; a replay now logs every feed request
and how it was answered (exact / as another key / none) into
summary.json, and --compare diffs those too.
Replayed on d50eaa28 and HEAD: flights 8 000, military 154, local ADS-B
40, vessels 12 000, satellites 824, launches 24; the runs agree (counts,
known objects, screen points, tracking poses, requests). The Cesium
reference is re-recorded this way before Tasks 10a, 12a and 13a.
Review of Tasks 2 and 4:
- A contract check draws, on both engines: an atlas cell switch, a
rotation of π (a half-white cell flips from above to below the
centre), scale 2 (about four times the area), scaleByDistance past
`far` (about a quarter), setCount shrinking the set, depth modes for
sprites 30 km under the ground ('always' and a disableWithinM past the
camera draw), a point with its dark outline, and a glTF model
(airplane.glb, minimumPixelSize 80, colorBlend replace) in magenta.
'normal' and a 10 m disableWithinM hide the buried sprite on Navara;
Cesium's billboards are not occluded by the app's photoreal tiles
(globe hidden; measured), so those two are asserted on Navara only.
- pickable: false: Cesium's GPU pick sees every primitive, so the
picker drops picks (and drill entries) of a set created with
pickable: false; Navara only registers pickable sets. The contract's
click check now clicks a pickable: false sprite on both engines.
- Navara resolves a GPU answer's batch id to the pick id when Navara
answers (the pick's own frame), not when the engine's click consumes
it: a set that reuses the instance in between still yields the
instance that was under the pointer.
Contract: Cesium 70/70, Navara 76/76.
Review of Task 3 (DPI-independent dashes; the shader change landed with
the overlay commit): qa-globe-contract --dpr=2 runs the suite on a
high-DPI page, and a new check measures a 16/16 px dashed line along its
middle row in CSS pixels: the dashes must be regular, and on Navara
16/16 within 2 px. The harness prints what each engine draws.
Measured: Navara 16/16 (period 32) at DPR 1 and 2. Cesium draws
16 on / 16 off (PolylineDash dashLength 32, pattern 0xff00, uniforms
checked on the primitive) as 14 on / 2 off with a 16 px period at both
ratios — a Cesium-side difference to look at before a layer relies on
pixel dashes (none does yet). At DPR 2 the Navara harness's
stars-at-midnight check counts 4 star pixels (its detector is tuned for
DPR 1); every suite check passes at both ratios.
Review of Task 4: the CPU line pick (shared with the sprite drill, hover
and the synchronous pick) took half the drawn width + 3 px, so a 1-2 px
trail needed a 3.5-4 px aim. Now within 8 px of the projected segments,
or half the width + 3 px for wider lines. Tested at 7.5 and 8.5 px from
a 1 px line; the contract suite's line click passes on both engines.
Review of Tasks 2–3: on Navara the overlays went through the look's
tone mapping and grading (magenta came out as 247/40/228, green as
147/228/89 in the cinematic look), where Cesium draws billboards and
polylines in the colours asked for.
src/globe/navara/overlay.js: a public Navara EffectDesc (gevOverlay)
inserted after toneMapping and before the gev sharpening, SMAA/FXAA and
final, whose pass renders one three scene over the tone-mapped input
buffer (in Navara's own Pass wrapper: Navara inserts a bare pass only
when it is an instance of its own postprocessing copy); it costs nothing
while the scene is empty. The chain has no depth buffer, so each overlay
material tests the scene depth itself (overlayDepth.js): its log depth
against Navara's MRT depth (RGBA packed), modes always / normal /
depth-fail, disableWithinM as a per-vertex escape. Sprites and points
(moverMesh.js) and line ribbons (lineMesh.js) live in that scene with
their per-frame work as overlay hooks; models stay Navara meshes, lit
and graded like the tiles. The sharpening now sits after the overlay
(Cesium's sharpen stage sharpens billboards too).
Also (review): Navara pixel dashes are measured in CSS pixels
(gl_FragCoord over uGevPixelRatio), and the line viewport uniform is
updated in place.
Measured: a magenta sprite draws 255/0/255. The contract suite's colour
checks are now within 40 of the asked colour on every channel on both
engines (were: dominant channels); Cesium 68/68, Navara 74/74 (its
sharpen-order check accepts the overlay between tone mapping and the
sharpening). Shell: cesium 23/23, cross 4/4; navara 23/25 — the two map
stack pictures from space fail on stddev 7.2/7.6 against 8, as on the
Task 3 commit run at the same hour (the cinematic sun follows the clock
into night over the reset view), not from this change.
qa-directions, qa-transit, qa-transit-recovery and qa-wind-canvas
launched a visible Chrome window. They now run headless unless
QA_HEADFUL=1 (`headless: process.env.QA_HEADFUL === '1' ? false : true`,
the same change as on the phase-4 branch).
Navara 0.2.1's transparent pass refreshes its copy of every scene light
each frame with copy(light, true). For the sun's cascaded shadow lights
(three-csm CascadedDirectionalLights) that copy clones every cascade, so
each frame brought new DirectionalLights with new ids and three's
WebGLLights uniforms cache (keyed by id) grew by about 1.2 KB a frame in
the looks whose sun casts shadows (minimal, photoreal).
src/globe/navara/cascadedLightCopy.js patches the copy to update the
cascades it has in place (pose, colour, intensity, target, shadow
settings and camera) when the counts match, and keeps the rebuild
otherwise; internals.js finds the class among the view's scene lights
(private view._scenes.light) and applies it when the globe is built.
Tests on three-csm's own class: the original copy changes the light ids,
the patched one keeps the lights and takes the values.
Post-GC JS heap (CDP HeapProfiler.collectGarbage every 10 s, the app on
?engine=navara rendering continuously at 60 fps): minimal 4.33 → 0.20
MB/min over 70 s and 0.06 MB/min over 180 s; photoreal 4.34 → 0.19
MB/min; cinematic (no shadow) 0.34. Added to the upstream asks.
src/data/selectionDispatcher.js: one engine click subscription with
picks (pick, drill 8), the press gesture (travel and duration, as
bindTrackingClickGesture) and one Escape listener for the layers that
move onto the engine. Owners register owns(id) (also in pickRegistry, so
layers still on their own handlers see the ownership) and onSelect /
onEmptyClick / onEscape. Rules of the existing handlers: another layer's
pick is not empty space; passThrough picks (trails) never take the
click; a drillThrough owner (local ADS-B) takes its object from under
another layer's; a drag selects nothing; deselection needs a clean click;
a pointer owned by a tool selects nothing. Tested on the fake engine:
own / foreign / empty / trail clicks, drill-through, drag and long press,
Escape, an owner without handlers, unregister and destroy. No layer uses
it yet (the *a tasks move them).
input.on('click'|'doubleClick'|'move', fn, { pick, drill }) delivers
{ …event, picked, drilled }: PickResult { id, owner, instance, primitive
(Cesium) } names the mover set (and instance) or line hit. One pick (and
one drill, as deep as the deepest listener) per event, shared by its
listeners; moves with picks are coalesced to one delivery per frame with
the latest position (src/globe/pickDelivery.js). Option checks in the
contract (normalizeInputOptions).
- Cesium: scene.pick / drillPick, and the picker names the owner: movers
map billboards, points and models to their set and instance, lines
their primitives to the line.
- Navara: a click with pick takes Navara's GPU pick of the same gesture
(featureClick; its pick helper answers on the same pointerup, usually
before the engine's click, so a fresh answer waits for it; otherwise
the click waits up to 250 ms, then the CPU pick). Navara 0.2.1 answers
clicks in quick succession with the first click's pick: a GPU answer
whose instance is not drawn near the click is stale and the CPU pick
answers. Hover, drill and the synchronous picker.pick/drillPick are CPU
picks (src/globe/cpuPicks.js) of pickable mover sets and pickable lines
(within half the width + 3 px), behind the globe excluded, nearest
first; lines are picked on the CPU after an empty GPU answer.
picker.clickStats() (Navara only) counts the answers and latencies.
- The fake engine picks its own movers and lines on the CPU unless
scripted.
- The contract suite clicks with real pointer events (puppeteer) on both
engines: a sprite is picked with its owner and instance, the map picks
nothing, two sprites in one place drill both, a line is picked, a
hover picks the sprite under it. Navara, 20 clicks: payload p50 2.8 ms,
p95 4.1 ms after the click (puppeteer dispatch included), 20/20 GPU
answers. The Cesium harness keeps the first-run card closed.
engine.lines.createLine({ id, widthPx, color, depthFailAlpha, dash
({ lengthPx, gapPx } | 'tactical'), arc ('none'|'geodesic'), modelMatrix,
pickId }) → a Line: setPositions (Cartesian3[] or a flat Float64Array),
setColor, setModelMatrix, show, destroy; lines.list() for QA. Options,
flat positions, geodesic densification (great circles of the geodetic
normals, height interpolated, one step per degree) and the registry are
engine-neutral (src/globe/lines.js); the fake engine keeps every line.
- Cesium: a Primitive of a PolylineGeometry (synchronous, rebuilt when
the positions change) with a PolylineMaterialAppearance in an
engine-owned collection; depthFailAlpha is depthFailAppearance with the
alpha scaled; pixel dashes are PolylineDash (16-bit pattern); the
tactical dash is the launches' selected-orbit shader under its own type
(launches keep theirs until Task 15a moves them onto engine.lines).
- Navara: a public MeshDesc (transparent pass, unlit) holding a ribbon
of one quad per segment, widened to CSS pixels in the vertex shader;
a second copy drawn with GreaterDepth and the alpha scaled is the
depth-fail part; dashes are cut in the fragment shader (pixels along
the screen direction, or the tactical pattern over the fraction along
the line); positions relative to the first point, the model matrix on
the group. Geodesic arcs are densified on the CPU. Not pickable yet.
- The contract suite draws a three-point line on both engines: magenta
along both segments, nothing off it, 1° east once turned by its model
matrix, gone when hidden; a line 30 km under the ground shows only its
depth-fail copy, dimmer than the line.
Main chunk 2,867,499 bytes (+7,197, +0.25 %: the Cesium lines).
engine.movers.createSet({ id, kind: sprite|point|model, capacity, order,
depthTest, sprite|point|model, pickable }) → a MoverSet: set(index,
fields) stores an instance (position, screen rotation, orientation, rgba,
scale, atlas cell, show, pickId) in flat arrays, commit() uploads the
changed range once and requests one render, positionsView() reads the
positions, destroy() removes it; movers.list() for QA. The options, the
store and the registry are engine-neutral (src/globe/movers.js); the fake
engine keeps them for layer tests.
- Cesium: one primitive per set in an engine-owned PrimitiveCollection
added with the first set, in `order`: sprites a BillboardCollection
(one image per atlas cell, NearFarScalar, disableDepthTestDistance),
points a PointPrimitiveCollection, models a Model per instance.
- Navara: a public MeshDesc per set. Sprites and points are one
InstancedMesh of a quad, faced to the screen and sized in CSS pixels in
the vertex shader from uniforms (the pick pass draws the same
footprint), atlas uv, rotation, colour and scale per instance, drawn in
the transparent pass unlit; offsets from an origin that follows the
camera past max(1 km, 10 % of its height). Models: the glTF once, one
InstancedMesh per mesh, minimumPixelSize/maximumScale on the CPU per
drawn frame, colorBlend in the shader. Pickable sets register a
PickableInstancedMeshWrapper; pickIds.js maps batch ids back to
instances (used by Task 4).
- The contract suite draws three 24 px sprites on both engines: centred
within 2 px of worldToWindow, hidden by show:false, recoloured, gone on
destroy, and no frames added once the commit has drawn.
- qa-engine-bench --scenario=movers: 10 000 synthetic aircraft
(src/bench/syntheticFlights.js, moved from the Navara spike with its
test), every instance moved and committed every frame. p95 Navara
(look=minimal) / Cesium: global 5.2/7.0 ms, city 5.6/9.9, orbit 6.0/7.4
(0.74, 0.57, 0.81; gate 1.1). Cesium is measured without its 60 fps cap.
Main chunk 2,860,302 bytes (+9,706, +0.34 %: the Cesium movers).