Commit Graph
220 Commits
Author SHA1 Message Date
German Khokhlov 4baefeb8f9 refactor(launches): Cesium rendering behind a renderer interface 2026-10-23 00:32:10 +04:00
German Khokhlov 1839d62160 refactor(launches): geodesics on src/geo
Port native Vincenty geodesics and launch-path math with exact Cesium oracles.
Keep all existing launch path-test expectations unchanged.
2026-10-22 23:48:50 +04:00
German Khokhlov 6bdb995fe3 feat(satellites): render on Navara
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.
2026-10-22 23:41:14 +04:00
German Khokhlov 000ef99464 refactor(satellites): Cesium rendering behind a renderer interface
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.
2026-10-22 22:37:09 +04:00
German Khokhlov 69a78710cc feat(vessels): render on Navara (WIP: browser parity not yet run)
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).
2026-10-17 14:06:11 +04:00
German Khokhlov 1fe8267550 refactor(vessels): Cesium rendering behind a renderer interface
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).
2026-10-17 12:47:14 +04:00
German Khokhlov ea1e2f9c04 feat(localAdsb): render on Navara
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).
2026-10-17 06:50:15 +04:00
German Khokhlov 0b8cb855fa refactor(localAdsb): Cesium rendering behind a renderer interface
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.
2026-10-17 05:50:30 +04:00
German Khokhlov 9dc446bd24 feat(military): render on Navara
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).
2026-10-17 05:05:40 +04:00
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 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 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 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 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 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 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 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 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 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 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 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 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 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
German Khokhlov 86aa0bc9ba feat(globe): dynamic lines on both engines
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).
2026-10-16 17:24:36 +04:00
German Khokhlov 0109097440 feat(globe): instanced movers on both engines
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).
2026-10-16 17:15:24 +04:00
German Khokhlov 4623c8b616 test(qa): a comparable W1 baseline from recorded feeds and a frozen clock
Review of Task 0: the live baseline could not be compared between
commits. qa-w1-baseline now records every /api response of each layer's
run (--record=DIR, DIR/<layer>.json), replays them through puppeteer
request interception (--replay=DIR; an /api request outside the
recording answers 503) with the page clock frozen at the recording's time
(evaluateOnNewDocument: Date.now and new Date(), hence JulianDate.now and
the app clock), and diffs two runs (--compare=A,B: counts, known objects'
positions within 1e-7° and 5 cm, screen points within 0.5 px, tracking
poses within 1e-6 relative). The rules live in src/tooling/w1Baseline.js
with tests.

Recorded once (qa-artifacts/w1-feeds, git-ignored), replayed on
d50eaa28 and on HEAD: the runs agree within the tolerances for flights,
military, satellites (counts, five known objects each, tracking poses)
and launches (count). Local ADS-B has no receiver and vessels no AIS key
here: both stay empty. This is the reference for the extraction tasks.
2026-10-16 16:37:43 +04:00
German Khokhlov 76851ee949 build: guard the initial chunks by module id; main-chunk check in CI
Review of Task 0:
- build/initialChunkGuard.js, a Rollup plugin in the browser build, fails
  the build when an initial chunk (an entry and what it imports
  statically) holds a module from node_modules/three, @navaramap/* or
  postprocessing. Checked by forcing it on satellite.js, which the main
  chunk does hold: the build fails naming the chunk and module.
  navaraAssets.test now pins both build plugins.
- scripts/check-main-chunk.mjs is one script for both main-chunk checks:
  the composition (text markers in the initial chunks, sizes) and, when
  scripts/main-chunk-baseline.json exists, the phase-4 branch's gzip-9
  budget, with its exports (checkMainChunk, measureMainChunk) so the two
  branches merge into this file. The composition test moved to
  src/tooling/mainChunkComposition.test.mjs to leave phase 4's
  mainChunk.test.mjs its name.
- CI runs npm run check:main-chunk after each npm run build (unit,
  browser-gate and Windows jobs).
2026-10-16 16:22:49 +04:00
German Khokhlov 7c7422e281 Merge migration/phase2-engine (4aaf9746) into migration/phase3-layers
The phase-2 review fixes: look internals in internals.js, the groundFloor
resample throttle, tile height selection, the sharpen mirror, QA tile
cache limits, engine.groundFloor diagnostics. In navara/index.js both
sides are kept: the look gets internals.js's luminance uniforms and reads
the sun's time from the engine clock's frame time, so engine.clock stays
the one time source for the sun.
2026-10-16 15:15:24 +04:00
German Khokhlov 5188e8869d fix(globe): the cinematic look's atmosphere scale, warm-up and sunset clouds
- The takram uniform paths move to internals.js (forEachLuminanceUniform,
  loud in dev when a path is gone); look.js sets each factor to scale x
  the recorded base value, so shared or copied vectors are never scaled
  twice, and the stars keep the base value (own vector when shared).
  Stars at intensity 120: a starfield shows from space.
- Both night-side shader variants compile on the first two frames.
- The clouds hide once the sun sets at the camera (dark puffs at dusk).
- No allocation on look frames that change nothing (frozen updates, a
  scratch camera place from livePosition).
Gallery regenerated.
2026-10-16 15:06:15 +04:00
German Khokhlov 27e28373d0 test(qa): W1 baseline and the main-chunk check
scripts/check-main-chunk.mjs (npm run check:main-chunk) finds the main
chunk of a build (the Vite manifest's entry, or the module script
dist/index.html loads), follows its static imports and fails when any of
them carries three or Navara markers; it prints the sizes. Base build:
main chunk 2,850,229 bytes (gzip 881,783), no static imports, clean.

scripts/qa-w1-baseline.mjs records Cesium mode for the W1 layers (flights,
military, local ADS-B, vessels, satellites, launches): counts, five known
objects with positions and screen points, JPEGs (path, size, sha256) at
the global / New York (JFK) / orbit poses, a tracked object's camera pose
after 2 s and what its share link restores. Live sources until the
fixture sources of Task 8.
2026-10-16 14:49:24 +04:00
German Khokhlov d50eaa28af test(qa): noon sky and stars, surface heights across engines, tile cache
qa-globe-contract checks the noon sky facing the horizon (daylight blue,
no star pixels) and stars at midnight; qa-engine-shell compares
sampleHeight at the Austin startup pose across engines and reports the
wheel floor's lowest frame. QA pages load OSM and keyless terrain tiles
through a disk cache with backoff on 429. Report updated.
2026-10-16 14:29:31 +04:00
German Khokhlov bb8c0b83c4 feat(globe): Sharpen on Navara, on at 80 % by default
The contract gains render.setSharpen({ enabled, amount }) and
render.sharpen(). Navara draws it as a custom EffectDesc: Cesium's unsharp
mask (3x3 box blur, gamma 2.2 round trip so one amount matches Cesium)
right after tone mapping and before SMAA/FXAA, skipping sky and cloud
pixels by scene depth. The Cesium engine keeps the state only; Cesium mode
still sharpens with its own post stage, unchanged. The fake engine records
it and the contract suite checks it.

Without a viewer the Sharpen slider and toggle drive the engine with the
same slider-to-amount mapping. On Navara the defaults, presets and share
links read Cesium's 49 % as 80 %.
2026-10-16 13:30:56 +04:00
German Khokhlov 80cc0b536e style: format the phase 2 files and add them to the format scope
Prettier on the 13 new test files that failed --check; format-scope.json
now lists every file phase 2 added (src/globe/**, engineChoice, navaraMode,
cameraAngles, the QA scripts and pages, build/navaraAssets and tests).
2026-10-15 22:34:25 +04:00
German Khokhlov 58a196374f feat(globe): engine members and the viewer bridge for the shell
Contract: camera.position(result?) and camera.height() for per-frame
readers, render.request(reason?), maps.releasePhotorealTiles, sources
with a caller-owned ready provider, isGlobeEngine. engine.maps now hides
every tileset it has shown, lets the last imagery/photoreal call win, and
setPhotorealTiles(null) restores the last base imagery and its credit
instead of leaving the photoreal credit on a bare globe.

Cesium engine: reads the Viewer camera/canvas aliases, detaches bridged
events through the remover Cesium returns, raises frame timestamps
without allocating, re-aims the horizon occluder only when the camera
moved, and renders the scene directly when the governor governs another
viewer (or none yet). engineFromViewer returns the registered scene
engine of a viewer, else builds and caches one. The render governor stays
the Cesium render backend under engine.render and gains
isRenderGovernorInstalled.
2026-10-15 18:18:19 +04:00
German Khokhlov a5c98c6b07 feat(app): construct the globe engine with the viewer
The scene builds the page's Cesium engine right after the viewer
(src/app/globeEngine.js), names the startup Google tileset to it once
loaded, returns it beside the viewer and destroys it before the viewer.
The debug handle exposes it as window.__godsEyeView.engine. Nothing
consumes it yet; render holds taken before the governor is installed
apply at install.

qa-globe-contract now runs on the app's engine (self-built only when the
app has none); a throwaway engine checks creation and destroy leave the
scene untouched.
2026-10-15 18:02:11 +04:00
German Khokhlov ab2368f3f3 fix(globe): tighten the Cesium engine after review
Map sources are applied through one src/maps/sceneApply.js helper shared by MapSourceController and engine.maps (live layer kept for an unchanged provider, providers cached per source). The contract gains flyTo endTransform and rectangle destinations, flyToSphere easing, drillPick width/height and pick content, with Cesium's own flyToSphere range. Input skips listeners removed during dispatch and stays inert after destroy.
2026-10-15 17:55:00 +04:00
German Khokhlov d831cbd23a feat(globe): implement the engine on Cesium
createCesiumEngine wraps an existing viewer without changing it: camera, picker, projector, frames, render (through the render governor), surface, input, credits and maps, with geo values out and Cesium values in at the boundary. Map and surface code is shared with src/maps and services/groundSnap instead of copied. scripts/qa-globe-contract.mjs runs the contract suite against the engine in headless Chrome; the camera-events check now waits for the previous move to end, as Cesium's moveStart marks a move from rest.
2026-10-15 17:43:05 +04:00
German Khokhlov 528ec75f32 feat(globe): add the engine contract and its test suite
src/globe/contract.js defines the engine-neutral GlobeEngine capabilities
(JSDoc), createEngineEvent with Cesium.Event semantics and
assertGlobeEngine. src/globe/testing adds a geo-based fake engine for shell
tests and an engine-agnostic contract suite that the fake passes in Node.
check-import-directions keeps the contract free of cesium, three and
@navaramap, and each engine directory free of the other engine.
2026-10-15 17:01:08 +04:00
Mazeyar Moeini Feizabadi ddd932f0a7 Harden shared GBFS station-name parsing and add regression coverage 2026-10-13 23:46:51 +02:00
Said TurkseverandClaude Opus 5.5 76db4114ff Merge upstream/main into pr/google-street-view
Conflicts with upstream's Street Level provider composition and share-state
codec, resolved:

- Street View's switch moves from its own share option (`g`) to upstream's
  provider-switch field (`r`, e.g. `0.r.google-1`); a link without it keeps
  Street View off. Docs and the browser gate follow.
- createApplicationStreetLevel keeps upstream's injectable provider list;
  its default, once the application supplies its Street Level source, is
  Mapillary plus Google Street View. Without that source there are none, as
  upstream intends.
- The coverage hint keeps upstream's empty-composition message and joins
  every switched-on provider's hint otherwise.
- CHANGELOG, README and SECURITY keep both sides' entries.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 11:32:05 +01:00
Sameh Khamis fb1596d2f1 Merge main into PR #621 (ChatGPT OAuth for Realtime voice)
Resolve conflicts with the voice-core captions, panel resize and MCP
panel key changes by keeping both sides.
2026-10-08 11:00:54 -07:00
Said TurkseverandClaude Opus 5.5 7bfcf177ac fix(street-level): IC8 review of the Street View provider
Google's Maps Platform terms forbid Street View imagery beside non-Google
maps, so the provider declares requiresMapStack: 'photoreal': off Google
3D its chip is dashed with the reason, lookups and opens skip it, and
leaving Google 3D closes its panorama.

- Ground clicks: only a click on the map itself (scenePick.isSurfacePick:
  nothing picked or Google 3D tile content) at street zoom opens Street
  View, and the camera stays put; higher up the hint says to zoom in.
  scenePick.pickGroundPosition replaces the copy in Directions.
- Viewer: poses go out at once and again when the date and address
  arrive, so an open never hangs on that lookup; one lookup per panorama,
  seeded by the nearest lookup; a close settles a pending open; failed
  lookups are asked again; poses follow position changes, not pano_changed.
- Loader: the gm_authFailure handler is installed once, so a retried load
  reports a refusal once.
- Viewer host: a prewarm that lands after another provider took the host
  is released; FIT/FILL availability is set on every open.
- Provider: a refusal while switched off reports no error over another
  photo; SINCE compares against the end of Google's month; messages name
  the referrer and network causes; FLAT says Street View is 360° only.
- The layer row names the key of the providers that are on; the MCP panel
  CSP admits Street View's tiles, control images and photos.
- Tests for each, against fakes that now follow Google's event order; the
  browser gate restores its state, waits instead of sleeping, aims its
  panel clicks and corner drags, and asserts the camera stays put.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 02:59:51 +01:00
Said TurkseverandClaude Opus 5.5 22b0009138 feat(street-level): Google Street View as the second imagery provider
Google Street View registers beside Mapillary as provider `google`. It runs
on the Maps JavaScript API with the browser GOOGLE_MAPS_API_KEY, loaded on
first use (mapsLoader.js), and uses Google's own StreetViewPanorama as its
viewer adapter, emitting provider-neutral poses (address and month in the
caption, a link to Google Maps, steps along Google's arrows, FOLLOW on
Google 3D). Its chip starts off because every panorama is billed to the
key; the share option is `g`.

Google offers no coverage layer for other maps, so the provider draws
none: with it on, a click on bare ground at street zoom opens its nearest
Google-collected outdoor panorama (groundClick.js). A key Google refuses
reads KEY REJECTED and fails a pending open at once; a lookup Google never
answers fails after 10 s.

Core: providers may declare `groundClick` and `defaultOn`; poses may carry
`capturedAtPrecision` and `title`; the coverage line keeps every
provider's hint; opening one provider's image releases another's
prewarmed viewer; FIT/FILL hides for viewers without render modes. CSP
script-src admits https://maps.googleapis.com. The browser gate gains two
Street View steps (the keyed one skips as 'no Google key' in CI).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 02:32:06 +01:00
jibraaan 09cefa6f39 Merge remote-tracking branch 'upstream/main' into fix/mcp-shared-panel-key
# Conflicts:
#	CHANGELOG.md
2026-10-07 23:39:53 +05:00
jibraaanandClaude Opus 5.5 e363b6a14f fix(mcp): share one panel key across an install's stdio servers
Fixes bilawalsidhu/gods-eye-view#927. Each stdio MCP server made its own
random panel key, and panel_request accepts only the key in the panel
page. A client that starts two server processes for one connection and
reads the page from one while sending the page's requests to the other
(seen in Claude Desktop on Windows) therefore always got "Only the God's
Eye View panel may make this request".

The stdio entry now passes a key shared by the install, created once in
.gev-cache/mcp-panel-key. The file is staged 0600 and published with
link(), so processes starting together agree on one key and never read a
half-written one. If it cannot be stored, the process keeps a key of its
own, as before. createLocalMcpServer still makes a fresh key unless given
one, so in-process servers (the /mcp HTTP plugin, tests) are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-07 18:00:38 +05:00