Commit Graph
956 Commits
Author SHA1 Message Date
German Khokhlov 3a0a3b0551 test(military): the colour constants equal the Cesium.Color values they replaced 2026-10-17 04:52:33 +04:00
German Khokhlov 7c0891deda refactor(military): Cesium rendering behind a renderer interface
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).
2026-10-17 04:52:15 +04:00
German Khokhlov e007350645 fix(ui): explicit navigation releases flights when an engine leaves out military awareness
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).
2026-10-17 03:59:51 +04:00
German Khokhlov 2da7b6d0d9 fix(ui): north-up and tilt keep an engine's own follow (Navara)
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).
2026-10-17 03:48:12 +04:00
German Khokhlov dcfb1219d5 feat(globe): a follow handle names its target
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).
2026-10-17 03:48:11 +04:00
German Khokhlov ba1191b2e5 test(qa): floor verification on both engines
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.
2026-10-17 03:20:44 +04:00
German Khokhlov 132a941e9a test(qa): sprite and enrichment checks on both engines through the probe
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.
2026-10-17 03:15:12 +04:00
German Khokhlov 03a0ff3a01 test(qa): flights parity checklist on both engines, side by side
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).
2026-10-17 02:35:32 +04:00
German Khokhlov b14bf7e013 feat(flights): render on Navara
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).
2026-10-17 02:15:04 +04:00
German Khokhlov 20e9d4a0fa fix(globe): Navara sprite atlases are sampled as Cesium samples billboards
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.
2026-10-17 02:15:04 +04:00
German Khokhlov 1dd3220812 fix(voice): Navara refusals reach speech, analyst queries and stop_tracking
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.
2026-10-17 01:18:12 +04:00
German Khokhlov 443c581280 refactor(data): focus projects through the engine; readout follows a tracked visual's follow
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.
2026-10-17 01:16:59 +04:00
German Khokhlov 4dfcba8989 feat(globe): mover sets report ready(); Navara scales by distance as Cesium
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.
2026-10-17 01:16:59 +04:00
German Khokhlov d81ebf1268 build: the panel build leaves out the initial-chunk guard
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.
2026-10-17 01:09:58 +04:00
German Khokhlov 2ce1c55f69 build: the QA probe stays out of production builds
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.
2026-10-17 01:09:58 +04:00
German Khokhlov 7f13ef4a87 refactor(flights): Cesium rendering behind a renderer interface
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).
2026-10-17 01:04:25 +04:00
German Khokhlov 8e702f0c3b feat(data): the selection dispatcher treats an unowned pick as empty space
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.
2026-10-17 00:20:18 +04:00
German Khokhlov 9f1a0611fa fix(ui): the "kept in the link" notice is readable on Navara
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.
2026-10-16 23:27:47 +04:00
German Khokhlov c493a8303b build: the QA fixture sources stay out of production builds
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)
2026-10-16 23:18:33 +04:00
German Khokhlov bf46eb6d97 fix(voice): Navara refusals name the feature and the way back to Cesium
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.
2026-10-16 23:17:36 +04:00
German Khokhlov 907cb2092d fix(globe): Navara redraws after a map change while its tiles arrive
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.
2026-10-16 23:00:15 +04:00
German Khokhlov ba039f7974 fix(app): the Navara notice no longer calls voice unavailable
Voice runs on Navara since 18b5475b (the viewer-only actions refuse with
UNAVAILABLE_ON_ENGINE); the notice still listed it as unavailable.
2026-10-16 22:53:35 +04:00
German Khokhlov 18b5475b8a feat(app): voice on the Navara engine
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.
2026-10-16 22:52:21 +04:00
German Khokhlov bc281ab246 refactor(voice): W1 actions on the engine
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().
2026-10-16 22:52:21 +04:00
German Khokhlov ec7dcc944e test(qa): an engine-neutral layer probe
- 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).
2026-10-16 22:52:18 +04:00
German Khokhlov 834d34296f test(overlays): an allocation workload through the Navara projector
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.
2026-10-16 22:26:04 +04:00
German Khokhlov d6fe0f3805 fix(globe): the floor takes no tile coarser than 1 km
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.
2026-10-16 22:19:57 +04:00
German Khokhlov f0ce1b04c7 fix(globe): the Navara follow turns and dollies as Cesium's tracked camera
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.
2026-10-16 22:14:02 +04:00
German Khokhlov b0cbed081b feat(app): layer catalog per engine
- 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.
2026-10-16 22:07:55 +04:00
German Khokhlov e1be2eac19 feat(app): detection and readout on Navara
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.
2026-10-16 21:14:59 +04:00
German Khokhlov 7d5a484796 refactor(overlays): layer labels through the engine
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.
2026-10-16 21:14:52 +04:00
German Khokhlov 311722e6de test(globe): the mover shader test expects decoded sRGB colours
68ff4e1c decodes instance colours in the fragment shader; its shader
test still matched the undecoded line.
2026-10-16 21:04:22 +04:00
German Khokhlov d7193e3b9e fix(globe): the CPU pick radius follows scaleByDistance
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.
2026-10-16 21:00:01 +04:00
German Khokhlov a4e753d550 fix(globe): Cesium picks fall through filtered results; no picks behind the horizon
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).
2026-10-16 20:59:10 +04:00
German Khokhlov 4e195dac6b fix(globe): the Navara follow orbits on drag and zooms to the floor
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.
2026-10-16 20:57:10 +04:00
German Khokhlov 68ff4e1c25 fix(globe): Navara overlay colours are sRGB
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.
2026-10-16 20:51:36 +04:00
German Khokhlov 645f042450 feat(globe): surface height quality
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.
2026-10-16 20:51:29 +04:00
German Khokhlov f640890cf6 feat(globe): follow camera on both engines
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.
2026-10-16 20:26:43 +04:00
German Khokhlov a89719aba9 build: package boundaries own the engine modules added since Task 1
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.
2026-10-16 20:06:12 +04:00
German Khokhlov 39d522c9a9 perf(globe): less churn in movers and lines
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.
2026-10-16 19:33:46 +04:00
German Khokhlov d483e5897e build: the initial-chunk guard covers Navara's whole dependency closure
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).
2026-10-16 19:30:23 +04:00
German Khokhlov ae52695b15 test(qa): seeded synthetic feeds for the W1 baseline; requests compared
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.
2026-10-16 19:28:12 +04:00
German Khokhlov 6436f580d9 test(globe): mover coverage on both engines; pickable: false honoured
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.
2026-10-16 19:20:47 +04:00
German Khokhlov 0957d9712b test(globe): pixel dashes at DPR 1 and 2
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.
2026-10-16 19:14:52 +04:00
German Khokhlov 93866c42fc fix(globe): Navara lines are picked within 8 px of their segments
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.
2026-10-16 19:05:20 +04:00
German Khokhlov e804351205 fix(globe): Navara sprites, points and lines drawn after tone mapping
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.
2026-10-16 19:04:57 +04:00
German Khokhlov 3c2e924363 test(qa): headless by default
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).
2026-10-16 18:59:37 +04:00
German Khokhlov 8812117013 fix(globe): Navara's shadowed sun leaked a light per frame
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.
2026-10-16 18:41:20 +04:00
German Khokhlov c327a25744 feat(data): one selection dispatcher
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).
2026-10-16 18:25:12 +04:00
German Khokhlov b0cb8ce881 feat(globe): picks on input payloads
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.
2026-10-16 18:25:12 +04:00