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).
Rebase #1142 onto current main, keeping both sides of the changelog and runtime documentation conflicts. Retain unwrapped containment and circular query coordinates.
Address review feedback on perimeterAnchorDegrees: use circular longitude for the selected outer ring, excluding its closing duplicate. Compare multipart areas along short longitude edges so a small dateline part cannot steal the card anchor from a larger polygon.
Three new card regressions fail before the fix and pass afterward. Coverage includes reversed/rotated rings, vertices at +/-180 and both multipart selection directions. Local validation: 43 focused tests; full suite 7,636 passed, one skipped, zero failures; production build, formatting, module boundaries and whitespace checks pass on Node 24.19.0. Browser tracking validation follows on the QA branch.
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.
- i18n scanner: lastError sink, no false sinks on defaults and ternaries
- shared src/i18n/marker.js for modules that only mark strings
- wind field messages move to wind/fields.js; CITY_POIS imports from cityPois.js
- drop review IDs, change history and stale references from comments and docs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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).
keep typed and tool-follow-up responses attached to their original requests
reject duplicate events before they affect newer work
preserve newer input and Radio handoffs during old callback cleanup
integrate the tested main snapshot without dropping existing history
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.
Keep USB reads in a bounded ordered pipeline, acknowledge and cap decoder
work, terminate stopped workers, and buffer short playback jitter. Throttle
display telemetry and compute spectra only while seeking.
Preserve PR #1174's wrapped seeking and fresh-spectrum tagging. Add opt-in
bounded timing diagnostics and decoder/playback lifecycle regressions.
Co-authored-by: Aditya <[email protected]>
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).
- Status pieces translate in one place (src/ui/statusText.js): a Local
ADS-B error with a single piece now translates, and the analyst card
splits empty-layer notes into pieces (Transit, Local ADS-B).
- Translate the ALPR retry chip text, the ECCC error on the Air quality
card, "Upstream response too large" and the "entry {n}" feed label.
- The unmarked-text check also sees error/loadingLabel/statusMessage/
status assignments, `status:` properties and showToast?.( /
this._showToast( calls; the scene import preview marks its pack
statuses, data and model-facing text carry i18n-ignore with a reason.
- Tests check the real code: the HUD locality formatter moves to
src/hudLocalityFormat.js, the unsaved-keys test pins the teardown.
- One copy each: city names come from src/cityPois.js, the GNSS HTTP
template and wind "field unavailable" texts; dead fallbacks removed.
- Leftovers of the reverted vessel-label change and the reformatting of
detectionHost.test.mjs / keySetup.test.mjs are back to main.
Catalogs: es/ru 2617 messages each, complete.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.