Commit Graph
908 Commits
Author SHA1 Message Date
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
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 f7f4e9a45c fix(time): latch the Cesium clock at creation; document the clock's limits
Review of Tasks 0–1:
- The Cesium engine clock attaches its frame latch to viewer.clock.onTick
  when the engine is created, so it runs before onTick listeners added
  later (cameraVerbs installs its own after the engine exists). Before, it
  attached on first use and would have run after them. A viewer without a
  clock still latches lazily on the scene's preUpdate.
- GlobeClock lists what is not on the app clock yet: Cesium's viewer clock
  (its lighting and property reads at currentTime), the launch replay
  timeline and the mission orbit's GMST frame (Task 15a), the satellite
  cloud's SGP4 and orbit bake (Task 14a), and wall-clock throttles. The
  plan's T14a/T15a now carry those moves.
- freeze/offset/rate change now() at once and frameTime() from the next
  frame; QA helpers request a render after moving the clock.
2026-10-16 16:18:08 +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 4aaf974600 docs(migration): the review fixes, their measurements and QA 2026-10-16 15:06:15 +04:00
German Khokhlov ce985768bc test(qa): wheel floor against the floor's own ground; polite tile cache
The Navara wheel check measures clearance against engine.groundFloor's
ground (threshold 29 m: the floor keeps 30 m), so tiles streaming under a
still camera cannot flake it. The tile cache identifies itself with the
repository URL, keeps at most two misses in flight and merges identical
ones.
2026-10-16 15:06:15 +04:00
German Khokhlov da6141be4b perf(globe): lazy hidden tiles by tile extent; floor resample interval
sampleHeight collects hidden tiles only after the shown ones miss, with
no per-node allocation, and picks the finest by tile extent (Navara
exposes no geometric error), not per-mesh radius. The ground floor
resamples on a move only after 100 ms, and reports its own ground and
margin (engine.groundFloor, Navara-only diagnostics).
2026-10-16 15:06:15 +04:00
German Khokhlov 6dad3894bd fix(globe): sharpen sky mask past depth 0.99999, not >= 1
RGBA8 depth packing rounds near the far plane.
2026-10-16 15:06:15 +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 bb4e6bafa4 perf(time): the app clock's frame latch without closure boxing
The clock keeps its numbers in object fields (stored in place) instead of
closure variables, and its default wall clock is one module-level
function. Micro-measure (Node, --expose-gc, 200k calls): the flights
render-time arithmetic allocates about 25-35 B per call with the app
clock against 66-102 B with JulianDate.now/addSeconds; an engine tick
(beginFrame + ticked) about 20-30 B.
2026-10-16 15:03:23 +04:00
German Khokhlov c33ee31efc refactor(layers): W1 timestamps without JulianDate
Flights and military keep their fix history in plain milliseconds
(timeMs, whole ms as the Date the JulianDate came from held) and read
"now" from the app clock: the render-delay time, bracketing,
extrapolation, the warm-up check and the trail seed are number
arithmetic (motion.js, tracking.js, snapshotRenderer.js), as is the turn
rate and the forward synthetic fix in data/motionModel.js, which no
longer imports Cesium. The JulianDate scratch fields in state.js and the
unused JulianDate.now() in queries.js go. Launches (mission frame, panel,
selection), the tracked satellite's SGP4 epoch and the celestial ring's
sun and moon read the app clock; where a Cesium property still takes a
JulianDate it is built from the app clock's now (as JulianDate.now did
from Date.now). Wall-clock throttles and feed receipt stamps stay on
Date.now; property reads at viewer.clock.currentTime (render, frozen at
boot) move with the render code in the *a tasks.

Tests that built JulianDate history fixtures now build ms ones with the
same instants (motionModel.test, flights.test, militaryFlights.test); new
tests show the military delayed position and the tracked satellite follow
the app clock (both fail on the previous code).
2026-10-16 15:01:46 +04:00
German Khokhlov f1a2df5cfe feat(time): the app clock on the engine
src/time/clock.js is the app's own time in ms since the epoch: by default
Date.now exactly (read at each call), with freeze/offset/rate/reset for QA
and tests, and one frame time latched per engine frame (beginFrame). The
contract gains engine.clock { now, frameTime, ticked }, in the fake engine
(a controllable wall clock, fake.advanceTime, fake.appClock), the contract
suite (one frame time for every listener of a frame, latched before
preUpdate, never decreasing), Cesium and Navara.

Cesium latches on the viewer clock's onTick, which CesiumWidget runs right
before scene.render, so scene listeners the app added before the engine
existed read the same frame time; the subscription appears on first use
and destroy removes it. The viewer clock itself is untouched: in the app
it never animates (shouldAnimate false, multiplier 1, its time frozen at
boot), and its time still drives Cesium's lighting as before. Navara
latches in the engine's handler of the view's preUpdate, and the look
reads the engine clock's frame time for the sun, so the app clock is the
one time source on Navara too.

src/time is a portable root in check-import-directions: no engine
packages, no browser globals.
2026-10-16 15:01:36 +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 26968f4903 fix(ui): the Cesium engine's render.sharpen() mirrors the sharpen stage
VisualEffects sends its sharpen state to engine.render.setSharpen when a
viewer exists too; the Cesium engine keeps it as state, and the Cesium
stage itself is unchanged.
2026-10-16 14:34:17 +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 1cca89adf2 fix(globe): Navara sampleHeight under a camera looking ahead
The ground under a tilted camera is outside the frustum, where Navara
hides its tiles, so the ray found nothing. When the shown tiles miss,
the loaded hidden ones answer, the finest first (within 1.5 times its
radius, which keeps out the quadtree parent). At the Austin startup
pose it agrees with Cesium within 0.12-1.04 m.
2026-10-16 14:29:31 +04:00
German Khokhlov a8a2ad5582 refactor(ui): engine-specific sharpen defaults instead of reading 49 as 80
The baseline and the style presets name ENGINE_SHARPEN_DEFAULT, which
resolves to 49 % on Cesium and 80 % on Navara. Share links and explicit
calls restore their value unchanged.
2026-10-16 14:29:31 +04:00
German Khokhlov abf16ae8af fix(globe): a daylight sky and no daytime stars in the cinematic look
The atmosphere draws physical radiance scaled for an exposure near 10,
while the baked Google tiles need exposure 1, so the noon sky came out
dark navy with the stars showing through. The cinematic look now scales
the atmosphere's radiance-to-luminance factors (sky, aerial perspective,
clouds) by 3, leaving the tiles alone, and shows its stars only from
space or with the sun below -6 degrees at the camera. Gallery
regenerated.
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 ac081a008d feat(globe): Navara's cinematic look by default
navaraScenePlan gains a pure `cinematic` plan, now the default of the app's
Navara mode (`?look=photoreal|minimal` picks the others): photoreal plus
volumetric clouds (low quality, layers from 1,500 m, no ground shadows),
ACES tone mapping, brighter stars and no sun shadow maps. From space, at
rest, the aerial perspective lights the globe from the atmosphere for a
real night side and terminator; it switches off below 1,000 km so the
Google tiles keep their baked light.

look.js builds the plan and keeps it current on frames Navara draws
anyway: the atmosphere's sun date follows the wall clock (the HUD's SUN
elevation), and the clouds hide above 4 km or over high ground in view,
parking their last image so the aerial perspective shows no ghosts.
2026-10-16 02:29:46 +04:00
German Khokhlov 177446c0be feat(globe): Navara's photoreal look by default
The Navara engine now builds its own photoreal scene (aerial perspective,
tone mapping, SMAA) instead of the minimal sky-and-sun look. It costs about
5 ms a frame (S5); minimal stays available for benches.
2026-10-15 22:55:31 +04:00
German Khokhlov 179c846f3e docs(globe): pickPosition reads the last drawn frame
Also corrects the separate drape pass cost in the phase 2 report to
0.8–0.95 ms, as measured in the spike.
2026-10-15 22:36:00 +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 312ed416e9 test(globe): pick round-trip and setTerrain checks in the contract suite
Two engine-neutral checks: pickPosition then worldToWindow lands within
1.5 px of the screen point (over New York, straight down and tilted;
points with no surface are skipped, at least one must hit), and
maps.setTerrain resolves for keyless terrain and rejects an unknown kind.
The fake engine now rejects unknown terrain kinds, as both engines do.
The Navara contract page shows OSM on keyless terrain before the suite,
since depth picks need a drawn surface.
2026-10-15 22:34:25 +04:00
German Khokhlov b65dbab709 fix(app): Navara attributions clear of the panels; QA waits for tiles
Navara's attribution list is capped at 120 px, and the shell's data
credits register after the map's, so Google's copyright lines come first
and stay visible while the card no longer covers the left panels.

qa-engine-shell polls engine.surface.tilesLoaded() (true for 1.5 s, up to
90 s) before judging the picture and prints the load time per engine;
"wheel zoom stops above the ground" is a hard check on both engines
(height >= 20 m; on Navara >= 20 m above the sampled surface), and the
Navara known gap is gone.
2026-10-15 21:27:57 +04:00
German Khokhlov 8270172086 build: name the lazy Navara engine chunk navara-engine
chunkFileNames names the chunk holding src/globe/navara
navara-engine-[hash].js; chunking is unchanged (manualChunks would pull
modules shared with the Cesium entry into it and make the entry import
it). The Cesium main chunk stays 2,849,217 B.
2026-10-15 21:27:57 +04:00
German Khokhlov 64d3297301 fix(globe): keep the Navara camera above the ground
Navara 0.2.1's camera controller has no terrain or tile collision; its
only zoom limit, minimumZoomDistance, is a distance from the Earth's
centre, so a wheel zoom near the ground went through it (to -5.9 km).

groundFloor.js runs on the engine's preRender after the camera model
reads Navara's camera. While the camera is free (no flight, no lookAt
target) and below 20 km it samples the ground under it (tiles, else
terrain, else the ellipsoid; at most every 250 ms or 20 m), feeds
Navara's clamp (minimumZoomDistance = geocentric radius of the ground
point + 30 m, sent on changes over 10 m, reset to Navara's default
during flights and above 20 km) so the wheel stops smoothly, and lifts a
camera still below ground + 30 m straight up without camera.commanded.
It only lifts, so it cannot oscillate with the shell's ground guard,
whose lift is a flight the floor stands aside for.
2026-10-15 21:27:57 +04:00
German Khokhlov 4ded3fbf15 feat(app): run gev on Navara behind ?engine=navara
The page's engine is ?engine=cesium|navara, else localStorage gev.engine,
else Cesium (src/app/engineChoice.js). Cesium mode is unchanged.

In Navara mode (src/app/navaraMode.js, loaded with import() only then,
and loading the Navara globe the same way) the scene builds the Navara
globe on the app container with no viewer; map stacks go through
engine.maps (Google 3D from the browser key, else Esri imagery); only the
shell's own data credits are registered. The layer catalog is empty and
the controls get empty layer stand-ins. The shell does not create its
viewer-bound parts (cockpit view, celestial ring, detection, tracked
readout, post-process stages); the tools install the render governor on
the engine, skip the director, annotations, drawing, imagery box, scope
mask, voice and embedded views, skip the first-run welcome, and show
"Experimental Navara engine: layers, cockpit, director and voice are
unavailable". window.__godsEyeView.viewer is null.

Navara's attribution card is kept clear of the location tray. A few
shell paths became null-safe for the missing viewer, layers or stages
without changing Cesium behaviour.

scripts/qa-engine-shell.mjs checks both modes headless: boot, viewer,
console errors, map drawn, search and fly, north/tilt/zoom/reset, orbit,
HUD, share link restore and map stacks.
2026-10-15 20:56:16 +04:00
German Khokhlov 306e3e9467 build: reach Navara only through import() and ship its worker wasm
The import-direction check now tells dynamic imports apart
(analyzeModule(source, { dynamic: true })) and rejects any static import
or re-export of src/globe/navara/** from outside that directory, so the
Navara engine can only be a lazily loaded chunk of the app.

Navara's prebuilt workers load their wasm by file name next to
themselves, which the bundler cannot see; a build-only Rollup plugin
copies the missing ones (draco decoder, font worker) into assets/ when
the bundle includes Navara, and adds nothing to a build without it.
2026-10-15 20:56:06 +04:00
German Khokhlov 72fec292d4 feat(maps): map sources for the Navara engine
createDefaultMapSources takes the engine kind. On Navara, Google 3D is
available from the browser key alone (Navara builds the tiles itself and
has no ion route to them), so it is the default and the recovery stack
when a key is set; without one the default is Esri imagery. Bing (Cesium
ion imagery) has no Navara path and is unavailable with a reason. World
terrain still needs an ion token and falls back to the keyless terrain.
MapStackController passes the kind of the engine it is given; Cesium
sources are unchanged.
2026-10-15 20:55:59 +04:00
German Khokhlov d112ecf296 fix(globe): render holds and commanded on Navara
The render governor installs on a non-Cesium globe engine as its backend:
owner-counted holds drive engine.render.setContinuous and requests go to
engine.render.request, so orbit and every other per-frame owner keep
Navara drawing while held and let it idle once the last hold goes. Cesium
stays on the viewer path its engine delegates to.

A user gesture on Navara now ends a flight without raising
camera.commanded (the user is not a new camera owner). Headless Navara QA
checks both.
2026-10-15 20:06:13 +04:00
German Khokhlov c6e1b65c67 refactor(scenes): the director needs no viewer for its camera
Camera-gesture listeners attach to the engine's canvas and the
interaction availability check tolerates a missing viewer, so a director
built on an engine alone places, flies and stops scene cameras.
2026-10-15 19:49:24 +04:00
German Khokhlov 0b9deddd70 refactor(scenes): director flights use the globe engine
Scene flights, authored placement, flight cancellation and arrival
ownership run on the engine the composition hands the director (or the
one registered for its viewer). Tracking, interactions and data packs keep
the viewer until phase 1c.
2026-10-15 19:47:51 +04:00
German Khokhlov ec38e6ef16 refactor(ui): navigation, orientation, share links and HUD use the globe engine
The application shell keeps the page's globe engine and hands it to the
navigation controller, location navigation (search, city and POI flights,
orbit, globe reset), the tilt and north-up controls, share-link save and
restore, share restoration's first-gesture listeners and the HUD. The
viewer is passed only where entity tracking still lives on it (phase 1c)
and to the HUD's basemap label context (voice); every such use is null-safe.

Share links and captured shots write heading and roll as Cesium's camera
reports them (src/cameraAngles.js): the contract's [0, 2π) pose maps an
exact 0 back to Cesium's 2π, except the roll of a vertical view, so
links stay byte-identical (heading=360, roll=360) on every engine.

Source-reading tests follow the moved code with the same checks.
2026-10-15 19:47:51 +04:00
German Khokhlov 16e63f4c50 refactor(camera): use the globe engine
The startup fly-in, orbit, world-target focus, the arrival ground guard,
arrival ownership, camera sensitivity claims, landmark/viewport/globe
flights and place search (with its view bias and near-view recovery) take
the globe engine. Callers that still hold a viewer (voice, layers until
phase 1c) reach the same engine through engineFromViewer, and arrival
ownership is keyed by the engine whichever one a caller names.

The ground guard yields on camera.commanded instead of patching the
viewer camera itself; on Cesium that event hooks the same command methods
and tracking changes, so direct Cesium callers still end it. Landmark
framing reads the frustum fov from the pose's fovy and aspectRatio.

Tests that built viewer doubles now run on the fake engine (or on the
Cesium engine over the same double), with the same expectations.
2026-10-15 19:47:43 +04:00
German Khokhlov a3e929870d feat(globe): camera.commanded, picker.pickGlobe, pose.aspectRatio and isDestroyed
Shell navigation needs to know when a new owner takes the camera (the
arrival ground guard), where the globe surface is under a window position
(orientation pivot, view-centred place search), the frustum aspect ratio
(landmark framing) and whether the engine is gone. Each member has a
contract suite check and is implemented by the Cesium engine, the Navara
engine and the fake. On Cesium, commanded hooks the viewer camera's
command methods and trackedEntityChanged only while it has listeners, so
direct Cesium calls from layers still announce a new owner.
2026-10-15 19:19:41 +04:00
German Khokhlov 840e34a881 feat(globe): bring the Navara engine up to the shell contract
- camera.livePosition: one engine-owned Cartesian3, refreshed on read
  (the model replaces its state on every move); camera.height()
- render.request(reason) takes the diagnostic reason and ignores it
- maps.current() returns the sources as passed: the base imagery on the
  globe (none while tiles replace it) and the tiles shown
- maps.releasePhotorealTiles forgets a source and takes its tiles away
  if they show
- base imagery and photoreal share one switch order; tiles off brings
  back the last base imagery with its credit; imagery chosen while
  tiles show waits hidden, credit included
- Navara QA runs the contract's maps checks with a Google 3D tiles
  source on the dev server's browser key: 58/58
2026-10-15 19:08:47 +04:00
German Khokhlov a4ac0b9b0d fix(globe): harden the Navara engine after review
- maps: photoreal hides the base imagery layer instead of deleting it,
  and a new base layer goes in before the old one goes: deleting the
  last raster layer stops Navara 0.2.1 loading terrain at the current
  view, which left terrainHeight undefined under the tiles
- Google 3D tiles open Navara's attribution card so the tiles'
  copyright lines are on screen, as Cesium shows them; onScreen
  credits do the same
- setCameraInputs merges into the camera options set through the
  engine (new setCameraOptions extension) instead of resetting them
- createNavaraGlobe disposes the view and canvas if the engine throws
- the pin test checks the installed Navara version and catches
  bracketed private access
- QA: draw-match checks at the nadir clamp, after a Navara flight and
  after a mouse drag; terrain under photoreal set right away; the
  copyright lines on screen; create/destroy/create leaves no canvas,
  frames, animation frames, workers or console errors (54/54)
2026-10-15 19:04:07 +04:00
German Khokhlov 90c8e7763a feat(globe): implement the engine on Navara
src/globe/navara builds the GlobeEngine on a Navara 0.2.1 ThreeView. The
engine keeps the camera pose in src/geo values and pushes it to Navara's
setCamera in degrees (Cesium's heading/pitch/roll convention, S4);
whatever Navara moves itself (user input, its flyTo arc) is read back
from the drawn camera each frame. Projection, picks against the
ellipsoid and the view rectangle are geo math on that pose, with
Navara's fov kept on Cesium's 60° across the wider side.

- flights: Navara flyTo in ms with mapped easings, cancelFlight via an
  empty setCamera, user input cancels a flight as in Cesium
- events: moveStart/moveEnd (500 ms still)/changed from the pose
- render: forceUpdate and animation (render on demand, S5)
- surface: S2 CPU ray over 3D-tile meshes, Navara terrain fallback
- maps: OSM/Esri raster, Re:Earth or ion quantized-mesh terrain,
  Google 3D tiles with a browser key and Google's logo
- input: DOM pointer events with ScreenSpaceEventHandler semantics
- the three private Navara members used live in internals.js, pinned
  to 0.2.1 by a test

index.js builds the product view (MSAA 4, native pixel ratio, preserved
drawing buffer, effects=minimal) for a dynamic import. The contract
QA script gains --engine=navara on a dev-only harness page: 46/46
contract and 4/4 Navara checks; Cesium stays 53/53.
2026-10-15 19:03:58 +04:00
German Khokhlov b6ac0e9c99 build: add Navara for the experimental engine
Pins @navaramap/three and @navaramap/three-default-plugin 0.2.1 with
their peers three 0.185.1 and postprocessing 6.39.4 (mapillary-js keeps
its own nested three 0.134). The dev server leaves the Navara packages
and their wasm workers out of dependency pre-bundling so wasm-bindgen
resolves its .wasm next to the package. Nothing imports Navara yet: the
Cesium-mode main chunk is byte-identical (2,837,992 bytes, same hash).
2026-10-15 19:03:32 +04:00
German Khokhlov 6dbd9c983e refactor(maps): describe map sources to the engine
The map source controller always hands engine.maps an engine-neutral
description of the registry source: imagery kind/style/accessToken and
credit, photoreal kind and credentials, terrain kind/accessToken. The
default registry carries those beside its Cesium factories. Only a
Cesium engine also gets the prebuilt provider, tileset and cached terrain
(Cesium-mode behaviour, ordering and same-provider guard unchanged); for
any other engine the controller builds nothing, falls back by description
when a setter rejects, and has no tile-failure watch.
2026-10-15 19:01:00 +04:00
German Khokhlov ab0acb0e09 feat(globe): engine-agnostic maps checks and maps.current
The contract suite gains maps checks (opt-in with mapSources: two imagery
sources and a photoreal one the engine can show): setPhotorealTiles(null)
brings back the last base imagery, the last imagery or photoreal call
decides what shows, and a released photoreal source no longer shows or
counts. They observe the new maps.current(), the source objects on screen
as passed. The fake engine follows the same semantics; qa-globe-contract
runs the checks on the app engine with the startup tileset and reapplies
the app's map stack afterwards. The contract documents map sources as
engine-neutral descriptions whose prebuilt members only the engine kind
that built them uses.
2026-10-15 19:01:00 +04:00
German Khokhlov 12c9651a67 refactor(overlays): hand the shell's overlay host the engine, not the viewer
The controls pass the scene engine to the shell, which initializes the
world overlay on it; initWorldOverlay takes only a globe engine (a viewer
or null is refused), so the shell no longer needs a viewer for its
overlay. engineFromViewer only returns the engine registered for a viewer
(the scene engine until teardown) and never builds one, so no engine can
outlive its viewer without a destroy path. Overlay and detection tests
and the allocation worker build a Cesium engine on their mock viewers and
destroy it with them; frame allocation is unchanged.
2026-10-15 18:53:57 +04:00
German Khokhlov 3e3aa0bd9f refactor(maps): use the globe engine
MapSourceController (and MapStackController) take the page's globe
engine: a globe stack becomes engine.maps.setBaseImagery with the
controller's ready provider and credit, then setPhotorealTiles(null),
then setTerrain with the cached terrain; a 3D stack becomes
setPhotorealTiles with its tileset and credit; owned tilesets are
released through maps.releasePhotorealTiles before disposal. The
controller keeps its provider/terrain/tileset caches, construction and
tile-failure fallbacks, switch generations and error watch. The scene
builds its map stack controller with its engine; without an engine the
controller applies sources to the viewer as before. The photoreal-to-
globe sequence (imagery, credit, globe, tileset) is tested end to end on
the Cesium engine.
2026-10-15 18:40:19 +04:00
German Khokhlov 83c13f330d refactor(credits): use the globe engine
Map credits (maps/credits.js) are shown through a credits capability
(engine.credits.addStatic) instead of the scene's credit display; the
shared map scene takes the engine's credits, so the Cesium engine's maps
and its credits part share one owner. Data credits (registerDataCredits,
registerDynamicCredit, show/hideOsmCredit) take the globe engine and keep
accepting a viewer from layers that still hold one; the scene registers
the data credits on its engine. The Cesium credits part reads
Viewer.creditDisplay, else the scene frame state display it aliases.
2026-10-15 18:37:02 +04:00
German Khokhlov be56144d75 refactor(services): use the globe engine
groundSnap.heightFor and meshFloorSampler.sampleMeshFloorCells take the
globe engine: engine.surface.sampleHeight for the probe,
engine.surface.tilesLoaded (and, for mesh cells, maps.photorealActive)
for readiness, camera.height for the mesh sampler's altitude gate. Callers
that still hold a viewer or scene (the layers until phase 1c) keep the
unchanged Cesium path.
2026-10-15 18:34:39 +04:00