mapv10 Viewer
The mapv10 viewer renders generated continent artifacts from a manifest URL into a 3D strategic map. It is a Vite + plain TypeScript + Three.js application; the renderer core is decoupled from any UI framework. For the broader workspace context, see architecture.md § Viewer And Renderer and § Renderer/UI Boundary. For the wave protocol that governs every code change here, see wave-protocol.md.
Stack and Build
-
Three.js
^0.181.2(viewer/package.json). -
Vite
^7.1.12(viewer/package.json). -
TypeScript
^5.9.3(viewer/package.json);tsconfig.jsontargetsES2022. -
Plain HTTP. The dev server listens on port
5443and the preview server on port4279(viewer/vite.config.tsserverandpreviewconfig). Both arehost: "0.0.0.0"withstrictPort: falseso an occupied port falls through to the next free one rather than failing the boot. -
@vitejs/plugin-basic-sslis a dev dependency but the active config does not install it. HTTPS is not part of the viewer's current contract. -
The Three.js bundle is pinned to its own chunk via
manualChunksinviewer/vite.config.ts. Three.js is around 400 kB minified and changes rarely between iterations; isolating it lets the browser cache absorb its weight while only the small main bundle re-downloads on viewer-side edits. -
Generated artifacts are served by an in-process middleware mounted on both the dev (
configureServer) and preview (configurePreviewServer) Vite servers. The mount lives atviewer/server/mapv10ArtifactMiddleware.ts:MAPV10_ARTIFACT_PREFIX = "/mapv10/runs"DEFAULT_ARTIFACT_RUN_ID = "continent-lod6"DEFAULT_ARTIFACT_MANIFEST_URL = "/mapv10/runs/continent-lod6/manifest.json"
The middleware serves typed JSON and binary assets out of
viewer/public/rather than letting Vite fall through to the SPA index. Per-request byte ranges and content types are enforced so a corrupt artifact surfaces as a load error instead of a silent zero-byte read. -
Generated artifact run directories under
viewer/public/are excluded from Vite's watcher inviewer/vite.config.ts. The middleware still serves them normally, but Vite must not treat the full continent fixture as source input: z0-z7 tile artifacts create hundreds of thousands of files and can exhaust Node's heap during browser verifier startup if watched.
npm scripts
The full set of scripts in viewer/package.json:
| Script | Role |
|---|---|
dev | Run the dev server on port 5443 with the artifact middleware. |
build | tsc --noEmit then vite build; produces a static bundle in dist/. |
typecheck | tsc --noEmit only; no bundle output. |
test | Run the Vitest suite once. |
test:watch | Run Vitest in watch mode. |
scenarios | Run scripted-camera scenarios via scripts/run-scenarios.mjs. |
fixture:continent | Bootstrap the continent fixture into public/continent-lod6. |
fixture:continent:validate | Validate-only run of the fixture bootstrap (no regeneration). |
validate:valenar-export | Validate a Valenar export against the run-root structure. |
verify:browser | Run the Playwright verifier against the dev URL. |
verify:browser:continent | Run the m10-lod6-continuity-proof verifier scenario. |
verify:browser:baseline:update | Capture a fresh visual baseline. |
verify:browser:baseline | Compare against the captured baseline. |
verify:browser:all | Chain verify:browser then verify:browser:continent. |
Renderer Architecture
Everything visible originates in a single class, Mapv10ThreeRenderer, defined
in viewer/src/renderer/Mapv10ThreeRenderer.ts. The class owns the
THREE.WebGLRenderer, the scene-graph, all caches, the request scheduler, the
worker pool, the camera, the controls, the keyboard/pointer state, and the
debug-state surface. Lifecycle:
mount → loadRun → tickFrame (rAF) → dispose.
mount(canvas: HTMLCanvasElement)
Sequence at the referenced source location:
- Create the
THREE.WebGLRendereragainst the supplied canvas. - Probe the GL sampler budget (
SamplerProbe.ts) and assert that the unified terrain shader's binding count fits. - Set the scene background colour and the
THREE.Fognear/far envelope atMapv10ThreeRenderer.ts. - Set the camera default pose (
position(0, 96, 120),target(0, 0, 0)). - Name the seven scene-graph groups and add them to the scene
(
Mapv10ThreeRenderer.ts). - Construct the
OrbitControlsagainst the canvas with the configuration detailed in Camera and Controls. - Install keyboard navigation (
installKeyboardNavigation), pointer tracking (installPointerTracking), and thePerformanceObserverlong-task hook. - Sync the terrain visual config to the shared shader uniforms.
- Install the directional + ambient lights (
installLights). - Mark the first frame dirty and schedule the rAF.
loadRun(manifestUrl: string)
Defined at Mapv10ThreeRenderer.ts. The phases match the
load-progress events emitted to UI handlers
(Mapv10ThreeRenderer.ts):
manifest: download and parse the run manifest vialoadMapv10Run. This fetches the run envelope, product manifests, the generatedtile-coordinate-index.json, and the small z-row shard indexes for raster/vector/semantic/mesh families. It does not fetch the family shard payloads or tile binaries until streaming requests them.renderer1/4:prepareStrategicMapResources— semantic maps, color LUTs, continent-wide ID raster.renderer2/4:prepareTerrainStreaming— tile caches, theTileRequestScheduler, theDecoderWorkerPool, and the planner/resolver.renderer3/4:frameRun— camera framed to the run's world bounds.renderer4/4:buildGeneratedLayers— routes, labels, water, markers.initial-residency:awaitInitialResidencyblocks the promise until the first VisibilitySet for the framed camera reportsauxiliaryReady === true.ready:run-loadedevent emitted with the run id.
A throw at any phase emits tile-load-state failed=1 and an error event,
then re-throws so the caller (the shell) can surface a fatal status.
tickFrame(now: number)
Defined at Mapv10ThreeRenderer.ts. One frame's body, in order:
- Update controls (OrbitControls damping). Update keyboard navigation
integration via
applyKeyboardNavigation. - Update DESP smoothing for the predictive-preload snapshot.
- If the frame is dirty (camera, layer state, mode, run, or input changed),
compute a fresh
VisibilitySetfrom the currentCameraSnapshot. - Run the
TileStreamingPlanner.applypass. The planner reconciles the scheduler against the desired set, fans out cache requests, and returns thePlannedSelection(terrain keys, runtime keys, auxiliary keys). - Drive the
RenderResolverto produce aRenderCommit(the final per-frame list of terrain mesh slots, including ancestor and previous-frame fallbacks). - Run the spatial-LRU cache
maintainpass against the live view centre. - Update label visibility (zoom band filter, collision avoidance, fade ramp).
- Step every active fade animation (terrain tiles, auxiliary batches, labels).
- Drain the per-frame
FrameBudget(texture creates, geometry merges, mesh disposes) withinFRAME_BUDGET_TOTAL_MS = 4. - Render via
WebGLRenderer.render(scene, camera). - Emit a
camera-changedevent if the camera state diverged from the last emit (debounced). - Sample the frame time into the rolling p50/p95/p99 buffer.
- Decide whether to re-queue another rAF (see below).
Scene-graph passes
Seven THREE.Groups named and added by Mapv10ThreeRenderer.mount:
| Group name | Purpose |
|---|---|
mapv10-terrain-pass | Per-tile terrain meshes (the unified terrain ShaderMaterial). |
mapv10-water-pass | Per-water-body batched lake / ocean / river meshes. |
mapv10-route-pass | Route ribbons and crossings. |
mapv10-overlay-pass | Per-tile overlay meshes (state-field overlay material). |
mapv10-marker-pass | Sample-point markers. |
mapv10-label-pass | Per-label sprites driven by LabelTextureCache. |
mapv10-picking-debug-pass | Optional picking-overlay diagnostics; off by default. |
The pass groups are added in this order so the GPU draw order matches the required compositing: terrain underneath, water on top of terrain, routes on top of water, overlay on top of routes, markers and labels above overlays, debug last.
On-demand frame loop
requestAnimationFrame is not driven on a free-running clock. Three primitives
control re-queueing (Mapv10ThreeRenderer.ts):
-
markFrameDirty()setsframeDirty = true, flags the label-layout dirty bit, and callsrequestNextFrame(). -
requestNextFrame()is idempotent. If a frame is already pending it returns immediately. Otherwise it setsframePending = trueand schedules the rAF body, which clears the bit before invokingtickFrame. -
scheduleNextFrameIfNeeded()runs at the end of everytickFrame. It re-queues another frame if any of these conditions hold (Mapv10ThreeRenderer.ts):frameDirtyis set.controlsDampingActive()is true (OrbitControls is bleeding off momentum).- At least one keyboard navigation key is held.
- The request scheduler has in-flight or pending tile requests.
- The frame budget has pending work units.
- A pending terrain render commit is queued, or terrain texture/disposal work is deferred, or auxiliary disposal work is deferred.
- Any actor is mid-fade (
fadesActive()checks terrain tiles, auxiliary batches, and labels). - The initial-residency awaiter is still unresolved.
When none of these hold the loop parks. CPU and GPU usage drop to zero until
external input, a cache callback, or a resize fires markFrameDirty() again.
Materials and Shading
Every fadeable terrain surface uses a single THREE.ShaderMaterial returned by
createFadeableTerrainMaterial (viewer/src/renderer/MaterialFade.ts).
One GL program serves every map mode; mode dispatch happens in the fragment
shader through an int uModeId uniform. Switching map modes is a single
uniform write — not a program recompile, not a material swap.
Sampler bindings
The unified terrain shader binds twelve samplers, every one declared in
MaterialFade.ts:
| # | Uniform | Format | Filter | Purpose |
|---|---|---|---|---|
| 1 | uMaterialWeightsA | RGBA8 | linear | Per-tile splat-map weights, primary band: grass, rock, forest, snow-or-wetland (MaterialFade.ts). |
| 2 | uMaterialWeightsB | RGBA8 | linear | Per-tile splat-map weights, secondary band: sand/coast, bare earth, ice/glacier, riverbank mud (MaterialFade.ts). |
| 3 | uBiomeMap | R8 | nearest | Per-tile biome enum used to switch the snow-vs-wetland palette (MaterialFade.ts). |
| 4 | uWaterMask | R8 | linear | Per-tile dry/sea/lake/river mask (MaterialFade.ts). |
| 5 | uRoughnessMap | R8 | linear | Per-tile Toksvig sidecar; replaces the Three.js <roughnessmap_fragment> chunk (MaterialFade.ts). |
| 6 | uGlobalIdMap | RGBA8 | nearest | Continent-wide RGB-packed locationId raster sampled by world-space UV (MaterialFade.ts). |
| 7 | uColorMapLut | RGBA8 | nearest | dim×dim per-locationId LUT authored from provinceColorSeeds.json (MaterialFade.ts). |
| 8 | uBorderSdf | RGBA8 | linear | Per-tile JFA SDF + nearest-id; the fragment shader smoothsteps the distance channel for crisp borders (MaterialFade.ts). |
| 9 | uStateField | RGBA8 | linear | World-space overlay RT. The producer transport is a future wave; the binding ensures the texture() read has a valid GL texture even before the producer exists (MaterialFade.ts). |
| 10 | uSelectionMask | R8 | nearest | Per-locationId selection state (MaterialFade.ts). |
| 11 | uTerrainMicroReliefNormalMap | RGBA8 | linear | Per-tile generated closeDetailNormal product sampled in tile UV space and faded by semantic band (MaterialFade.ts). |
| 12 | uInfluenceMaskCorruption | R8 | linear | Per-tile generated corruption intensity sliced from influenceMask.corruption, driving the organic world-space influence blend (MaterialFade.ts). |
Mode dispatch
The seven terrain-mode constants are exported from MaterialFade.ts:
| Mode | Constant | Value |
|---|---|---|
| Geography | TERRAIN_MODE_GEOGRAPHY | 0 |
| Height | TERRAIN_MODE_HEIGHT | 1 |
| Slope | TERRAIN_MODE_SLOPE | 2 |
| Normal | TERRAIN_MODE_NORMAL | 3 |
| Political | TERRAIN_MODE_POLITICAL | 4 |
| Routes | TERRAIN_MODE_ROUTES | 5 |
| Influence | TERRAIN_MODE_INFLUENCE | 6 |
The fragment program switches on uModeId to select the colour-derivation
path. Selection / hover composition (uSelectionMask, uHoveredId,
uBorderSdf, uStateField) runs in every mode. Influence mode keeps
the effective material blend visible and emphasizes generated corruption
territory from uInfluenceMaskCorruption.
Per-tile fade
LOD_FADE_MS = 320 (MaterialFade.ts). A new tile entering the active
commit ramps uTileOpacity from 0 to 1 over 320 ms; a tile leaving ramps
back to 0. The ramp value is computed by computeFadeProgress
(MaterialFade.ts), a pure function with no side effects.
Per-mesh uniform delivery is the architecturally interesting part. A shared
THREE.ShaderMaterial only re-uploads its uniforms once per material
activation in WebGLRenderer.setProgram. Writing uTileOpacity from
mesh.onBeforeRender is therefore subject to last-write-wins across meshes
sharing the material. The viewer works around this by setting
material.uniformsNeedUpdate = true on every per-mesh write so the renderer's
per-mesh refresh path takes effect (Mapv10ThreeRenderer.ts documents the
shape; the onBeforeRender hook in applyFadeOpacity performs the write).
Terrain meshes themselves do not alpha-fade. Cross-fading two terrain LOD rasters blends different splat / water / SDF masks and produces a temporal shoreline shimmer that is much worse than a hard pop. Coverage is controlled by the resolver's previous-frame fallback ("kicking") plus opaque child commits. The 320 ms fade window is reused unchanged for auxiliary batches and labels, where the visual gain outweighs the fade artefact.
Other material factories
All defined in MaterialFade.ts:
| Factory | Symbol | Purpose |
|---|---|---|
createFadeableTerrainMaterial | function export | Unified terrain shader. |
createFadeableOverlayMaterial | function export | Per-tile overlay layer. |
createFadeableLakeMaterial | function export | Lake water surface. |
createFadeableOceanMaterial | function export | Ocean water surface. |
createFadeableRiverMaterial | function export | River ribbon material. |
createFadeableBorderMaterial | function export | Border-line material (used only by debug overlays now that borders render in the terrain shader). |
createFadeableRouteMaterial | function export | Route ribbon material. |
createFadeableCrossingMaterial | function export | Crossing-marker material. |
createFadeableMarkerMaterial | function export | Sample-point marker material. |
createFadeableFootprintMaterial | function export | Selection-footprint material. |
Lighting
installLights() at Mapv10ThreeRenderer.ts adds three lights to the
scene:
- One
THREE.AmbientLight(0xf0e8d7, 1.0)— warm hemispheric fill. - One
THREE.DirectionalLight(0xfff2d0, 1.0)— key sun, cool-warm direction driven byterrainSunDirectionfromTerrainVisualConfig. - One
THREE.DirectionalLight(0x9cc7b8, 1.0)— fill light from the opposite hemisphere.
There is no IBL and no shadow-map pass.
Atmosphere
Two independent layers:
- Scene-level
THREE.Fog(aerialPerspectiveColorHex, 190, 470)set inMapv10ThreeRenderer.ts. Three.js uses the linear fog ramp through the standard<fog_fragment>chunk for materials that opt in. - A per-fragment exponential haze in the unified terrain shader. The two layers composite independently so the strategic-map look does not depend on Three.js fog being lit consistently across all materials.
Micro-relief
uTerrainMicroReliefNormalMap is built in MaterialFade.ts at the referenced source location by
createMicroReliefNormalTexture(). It is sampled in world-UV space (so the
detail does not "swim" with mesh seams) and distance-faded — at continent
zoom the relief contribution is forced to identity so the close-up texture
does not bleed into a coarse strategic view.
Toksvig roughness composition
The per-tile R8 sidecar uRoughnessMap carries a Toksvig-method roughness
authored offline. The shader composes it in α²-space against the per-class
authored roughness values, then routes the result through the standard
roughness chunk. This replaces the Three.js <roughnessmap_fragment> chunk
verbatim so the lit look matches MeshStandardMaterial.
Physical LOD Selection
Physical LOD controls generated product residency only: terrain mesh tiles, raster sidecars, semantic ID tiles, vector tiles, auxiliary mesh sidecars, preloads, cache priority, and resolver fallback. It is not the semantic zoom policy for labels, routes, borders, markers, water visibility, hover, selection, or influence overlays.
Terrain LOD selection is the Cesium 3D Tiles screen-space-error formula:
sse_pixels = (geometricErrorKm * viewportHeight) / (2 * tan(fov/2) * distanceToCameraKm)
The selector walks the LOD pyramid coarse-to-fine and picks the first level whose measured tile geometric error projects below the target.
Dual screen-space-error targets
mapv10's "terrain" is a coupled product (mesh displacement plus per-tile water
masks, splat weights, roughness, close-detail normals, and border SDFs). Geometry SSE alone is not
enough — the coarse continent root has acceptable geometric error at realm
scale, but its 18.75 km raster sample projects to many pixels and crawls when
the camera moves. Two thresholds therefore drive selection
(viewer/src/renderer/lod/VisibilitySet.ts terrain SSE constants):
TERRAIN_GEOMETRIC_ERROR_TARGET_PX = 1.5— geometry SSE target.TERRAIN_RASTER_SAMPLE_ERROR_TARGET_PX = 6.0— generated raster/detail sample footprint target. z6/z7 report their generatedrasterSampleSpacingKmfromcloseDetailScale = 8, so close views are judged against the detail product rather than merely smaller tile windows.
Semantic Display Policy
The generated semanticDisplayPolicy product maps camera distance and map
mode to the first-class semantic bands continent, realm, province,
location, and close. Mapv10ThreeRenderer consumes this policy through
SemanticDisplayPolicy.ts before applying layer visibility, label
eligibility, max label counts, water/route planner fan-out, and influence
requirements. lodBand remains useful debug metadata for product residency,
but it must not be the only source of display visibility.
The policy also carries influence requirements. corruption rendering
requires the generated influenceMask.corruption product before the
viewer may render, and the manifest loader validates that requirement
against influenceTypes.json, influenceSources.json,
influenceRules.json, and the derived effective products. Missing
required influence truth is a load-time failure.
Full-coverage limit
FULL_COVERAGE_PRIMARY_TILE_LIMIT = 64 (VisibilitySet.ts). When the
selected level contains 64 or fewer tiles, render the entire level rather than
a target-centred subset. Coarse strategic LODs are cheap enough to keep whole,
and doing so prevents camera pans at constant distance from swapping a visible
z2 footprint against the z0 context surface.
Focal-footprint pruning
Inside the focal disk:
PRIMARY_LOD_VIEWPORT_FRACTION = 0.45— fraction of viewport height treated as the focal disk (VisibilitySet.ts).VIEW_FOOTPRINT_TILE_MARGIN = 0.6— margin used to expand the focal-pixel rectangle when collecting primary tiles (VisibilitySet.ts). One value replaces the prior enter/retire pair; theRenderResolver's previous-frame set absorbs the visual transition without a second threshold.
Within-band priority
Tiles inside the same band (Urgent / Current / Preload) are ordered by a
continuous priority scalar (VisibilitySet.ts):
priority = SCREEN_AREA_WEIGHT * screenAreaPx
- DEPTH_PENALTY_PER_LEVEL * (primaryZ - tile.z)
+ FOREGROUND_BIAS * foreground
+ FOVEATION_BIAS * foveation
With:
SCREEN_AREA_WEIGHT = 1.0DEPTH_PENALTY_PER_LEVEL = 250FOREGROUND_BIAS = 200FOVEATION_BIAS = 100
Foveation
The pointer-weighted bias takes the cursor's NDC position from the last
mousemove (or screen centre when the pointer has not moved or has left the
canvas). Falloff is linear with radius FOVEATION_RADIUS_NDC = 0.4
(VisibilitySet.ts). Tiles outside the radius get zero bonus. The
coefficient (100) is intentionally smaller than FOREGROUND_BIAS (200) so
foveation acts as a tie-breaker between similarly-framed tiles, not as a
primary priority axis.
Three-stage flow
A frame's selection runs through three modules:
VisibilitySet(viewer/src/renderer/lod/VisibilitySet.ts) — pure function over(camera, run, metrics, predictedSnapshot?)returns the desiredprimary[],underlay[],preload[]slot lists, the per-key band-tagged priority map, and a canonicalsignaturestring for cheap equality.TileStreamingPlanner.apply(viewer/src/renderer/lod/TileStreamingPlanner.ts) — turns the desired set into scheduler reconcile entries plus cache request fan-out acrossTerrainTileCache,RuntimeTileCache, the water mesh cache, and the route mesh cache.RenderResolver.resolve(viewer/src/renderer/lod/RenderResolver.ts) — produces the finalRenderCommit: which tile slots draw this frame, with what fallback metadata. Holds the only legitimate cross-frame state in the new selection model.
Ancestor-fallback "kicking"
When a primary tile's mesh has not yet streamed in, the resolver "kicks" — it
keeps the previous-frame rendered tile visible until the replacement lands.
The previous-frame slot is held in RenderResolver.previousRenderedSlots
(RenderResolver.ts). Without this, deeper-LOD refinements would produce a
visible hole during the streaming window.
Ancestor selection is a structural-parent walk, not bounds containment.
The resolver climbs the tile pyramid one parent at a time using the recorded
parentId chain in the run's tile pyramid, and at each step asks
isFullyResident (RenderResolver.ts) whether the candidate has both CPU
and GPU residency (typed-array data decoded into the tile cache AND the GPU
texture/mesh upload completed). A tile that contains the primary in 2D bounds
but is not resident is never promoted as a fallback. The walk stops at
MAX_ANCESTOR_Z_DISTANCE = 1 (RenderResolver.ts): the resolver refuses to
promote any ancestor more than one z step coarser than the primary. When no
qualifying ancestor exists within that cap, the resolver falls back to the
previous-rendered slot if one is fully resident; otherwise the slot is
recorded as omitted (omittedSlotCountMax gate, scenarioTypes.ts) and
the primary is left empty for that frame rather than substituting fabricated
or stale data.
For the accepted and now implemented z6/z7 physical LOD ladder tied to raster
SSE target coverage, see
wave-3b-lod-ladder-camera-decision.md.
DESP predictive preload
PRELOAD_LOOKAHEAD_MS = 250 and DESP_ALPHA = 0.5 (TilePriority.ts DESP preload constants).
The renderer applies double-exponential smoothing to the camera position over
recent frames, projects the smoothed velocity 250 ms forward, and constructs
a predicted CameraSnapshot. The visibility set diff (predicted minus
current) becomes the Preload band. Three safeguards suppress speculation
when it would burn bandwidth without a payoff:
- A velocity floor (
PRELOAD_VELOCITY_FLOOR_TILES_PER_LOOKAHEAD = 0.5,TilePriority.ts) — a settling camera generates no preloads. - A predicted/current overlap threshold
(
PREDICTED_OVERLAP_SKIP_RATIO = 0.7,TilePriority.ts) — if 70 % of the predicted set is already in the current set, the preload band is empty for this frame. - A direction-change commitment window (
PRELOAD_COMMITMENT_WINDOW_MS = 100,TilePriority.ts) — preload requests in flight for less than 100 ms are not cancelled even when they leave the predicted set.
Streaming and Scheduler
The TileRequestScheduler is constructed at
Mapv10ThreeRenderer.ts with MAX_IN_FLIGHT_TILE_REQUESTS = 24
(Mapv10ThreeRenderer.ts). Per-host concurrency is capped at
DEFAULT_MAX_PER_HOST = 8 (viewer/src/renderer/TileCache.ts). Within
each host, the preload band reserves a fraction of the slots:
floor(PRELOAD_HOST_CONCURRENCY_FRACTION * 8) = floor(0.3 * 8) = 2
(TileCache.ts).
Heap ordering
The scheduler heap is a binary max-heap over the lex tuple (band, withinBandPriority, enqueuedAtFrame). compareBandedPriority
(TilePriority.ts) is the single-line comparator: smaller band wins, then
larger within-band priority wins. Frame number breaks priority ties so older
requests do not starve.
Bands
TileBand is a categorical enum (TilePriority.ts):
Urgent (0)— a coarse ancestor is missing while a descendant renders in its place.Current (1)— in the current-frame visibility set.Preload (2)— in the predicted-frame set only.
Bands are categorical, not multiplicative weights. A high-priority Preload tile never preempts a low-priority Current tile.
Reconcile flow
Each TileStreamingPlanner.apply call hands the scheduler a fresh
Iterable<ReconcileEntry>. The scheduler:
- Cancels in-flight requests that no longer appear in the desired set, with
exceptions: a Preload request inside
PRELOAD_COMMITMENT_WINDOW_MS = 100is held; a request that has been absent from BOTH the current and predicted sets forPRELOAD_STALE_FRAMES = 3frames is aborted. - Refreshes priorities on requests that survived reconcile.
- Enqueues new desired keys onto the heap.
- Pumps the heap up to
maxInFlight - inFlighttimes subject to per-host limits.
Preload pin and stale eviction
After a Preload-band request completes, the cache pins it as non-evictable for
PRELOAD_CACHE_PIN_MS = 250 ms (TilePriority.ts). This stops the
"preload, then evict before use" loop when the camera arrives at the predicted
location.
Coalescing
Re-enqueue of a key already in the heap updates its priority in place. The heap re-sifts the entry without producing a duplicate request.
No retry
A transient HTTP error permanently fails the tile in this version. See Known Limitations.
Cache Architecture
Three caches carry the streaming payload, each owned by Mapv10ThreeRenderer:
| Cache | Family | What it stores |
|---|---|---|
TerrainTileCache (viewer/src/renderer/TileCache.ts) | terrain | Per-tile mesh assets and per-tile sidecars. |
RuntimeTileCache (viewer/src/renderer/RuntimeTileCache.ts) | raster + semantic | Per-tile decoded raster sidecars (base/effective splat, biome, water, influence mask, roughness, close-detail normal, ID, border SDF) and semantic data. |
MeshAssetCache (viewer/src/renderer/TileCache.ts) | vector | Auxiliary meshes (water, route ribbons) keyed independently of terrain tiles. |
A single CacheCoordinator (viewer/src/renderer/RuntimeTileCache.ts,
Mapv10ThreeRenderer.ts) wires them together. Each cache reports its
byte usage and family classification into the spatial-LRU evictor.
RuntimeTileCache consumes sharded tile family manifests. It owns
ShardedTileManifestStore instances for raster, vector, and semantic
families; each store resolves a requested tile key to the generated z/y row
shard, fetches and byte-checks that shard once, attaches the tile coordinate
from tile-coordinate-index.json, then lets the worker pool fetch the actual
raster/vector payloads. This keeps boot readiness bounded to manifest indexes
and visible tile shards rather than the full continent raster manifest.
Family floors
Family floors are SOFT — they bias the eviction order rather than excluding
entries from the candidate pool. From viewer/src/config/CacheConfig.ts:
| Family | Default floor (MiB) |
|---|---|
terrain | 64 |
raster | 32 |
vector | 8 |
semantic | 8 |
When a family is below its floor, its entries get
cacheFloorProtectionBias = 200 subtracted from their eviction score. They
remain ELIGIBLE so the global cacheCeilingBytes invariant always holds even
when every family sits below its floor.
Eviction score
Every non-pinned, non-Urgent entry is scored by a single scalar
(viewer/src/renderer/SpatialLruEviction.ts):
score = recencyAge + bandPenalty * 1000 + spatialDistance * 10
+ (familyBelowFloor ? -floorProtectionBias : 0)
Higher score evicts earlier. bandPenalty is 0 for Current and 50 for
Preload, so the band-scaled term contributes either 0 or 50_000. Recency
is in frames; spatial distance is in tile-grid units to the live view centre.
Pinned entries and Urgent-band entries return Number.NEGATIVE_INFINITY,
guaranteeing the candidate pool never picks them.
maxEvictPerFrame
maxEvictPerFrame = 16 (viewer/src/config/CacheConfig.ts). The
maintain pass evicts at most 16 entries per call, so a large pressure event
drains across multiple frames instead of stalling the WebGL driver with a
hundred geometry/texture disposes in one go.
Pin lifetime
A cache entry can be pinned in two ways:
- Refcount:
pin(key)/unpin(key)increments and decrementspinCount. An entry withpinCount > 0is never evicted. - Time-based:
pinUntil(key, untilMs)holds the entry resident until wall clock passesuntilMs. Used by the predictive-preload path withPRELOAD_CACHE_PIN_MS = 250.
safeDisposeTexture
safeDisposeTexture (viewer/src/renderer/SpatialLruEviction.ts)
disposes a Three.js texture AND closes its underlying ImageBitmap. The
default Texture.dispose() does not (Three.js issue #23953); without the
ImageBitmap close, decoded raster bytes stay GPU-resident forever and the
cache's byte accounting diverges from real GPU memory.
Per-preset cache budgets
Mapv10ThreeRenderer.ts defines CACHE_BUDGETS:
| Preset | terrainBytes | runtimeBytes |
|---|---|---|
province-slice | 24 MiB | 32 MiB |
regional-slice | 24 MiB | 32 MiB |
realm-slice | 64 MiB | 96 MiB |
continent | 128 MiB | 192 MiB |
The active preset is selected from the run manifest's scale hint and applied
via setCacheBudgets.
Decoder Worker Pool
DecoderWorkerPool (viewer/src/workers/WorkerPool.ts) owns N dedicated
Worker instances and dispatches decode requests round-robin via
nextWorkerIndex. Pool size:
min(6, max(2, hardwareConcurrency - 1))
(WorkerPool.ts). On a 12-core machine this gives 6 workers; on a
2-core machine, 2.
Init protocol
Each worker is spawned with name: "mapv10-decoder-N" and immediately
receives an init message containing the world dimensions and exaggeration
(WorkerPool.ts):
{ kind: "init", worldWidthKm, worldHeightKm, exaggeration }
Worker capabilities
A single worker handles HTTP fetch, byte-length verification against the
manifest, typed-array decode (raster and mesh), mesh world-transform,
computeVertexNormals, worldKm sidecar generation, and vector-tile JSON
parsing. Decoded buffers are returned to the main thread as Transferable
objects (zero-copy).
Abort protocol
The pool's dispatch method registers an abort listener on the caller's
AbortSignal. On abort, the pool posts { kind: "abort", requestId } to the
worker (WorkerPool.ts). The worker responds with an error whose
name === "AbortError".
Crash recovery
handleWorkerCrash (WorkerPool.ts) starts a replacement worker, hooks
its message, error, and messageerror listeners, replays the init message
containing world dimensions and exaggeration, and rejects every pending job that
was dispatched to the failed worker.
Camera and Controls
The camera is a THREE.PerspectiveCamera(50, 1, 0.1, 2000)
(Mapv10ThreeRenderer.ts) — 50° vertical FOV, 0.1 km near, 2000 km far.
Default pose (Mapv10ThreeRenderer.ts): position (0, 96, 120), target
(0, 0, 0). The aspect ratio is updated on every resize.
OrbitControls configuration
From Mapv10ThreeRenderer.ts:
| Property | Value |
|---|---|
enableDamping | true |
dampingFactor | 0.12 |
rotateSpeed | 0.68 |
panSpeed | 0.72 |
zoomSpeed | 0.82 |
screenSpacePanning | false |
minDistance | 35 |
maxDistance | 260 |
maxPolarAngle | Math.PI * 0.48 (≈86°) |
| target | (0, 0, 0) |
screenSpacePanning = false keeps the pan motion in the world plane so panning
along an angled view does not drift the target into the air. The polar-angle
ceiling at 86° prevents the camera from going below the strategic plane.
Distance limits
minDistance = 35 and maxDistance = 260 are the pre-load defaults. After
loadRun finishes, maxDistance is recomputed against maxExtent * 1.8
(Mapv10ThreeRenderer.ts) so a continent-scale run permits a camera
that frames the entire run from above without the orbit controls clamping it
mid-zoom.
Zoom bands
The camera state classifies into one of four zoom bands
(Mapv10ThreeRenderer.ts): continent (0), realm (1),
province (2), location (3). The band drives label visibility, mode-active
texture sampling, and a few HUD details.
Scripted camera bypass
__mapv10SetCameraPose(positionKm, targetKm, fovDeg, options) accepts a
bypassInteractiveLimits flag. When set, the renderer relaxes the distance
clamps for the duration of the script, allowing verifier scenarios to frame
poses outside the interactive envelope.
Frame APIs
frameSelection(target: MapEntityRef)(Mapv10ThreeRenderer.ts) — frame the camera to the bounding sphere of an entity reference; falls back toframeRun()if the run is not yet loaded.frameWorld(pointKm, distanceKm?)(Mapv10ThreeRenderer.ts) — frame to a world-km point at an optional distance. Heuristic — picks a pose relative to a target.frameRun()(Mapv10ThreeRenderer.ts) — frame the camera to the active run's world bounds.
Input
Pointer drag-vs-click guard
The shell holds a dragClickGuard with a clickMovementThresholdSq = 6 * 6
px² (viewer/src/ui/shell.ts drag-click guard). On pointerdown the start coordinates
are recorded; on pointermove while the same pointerId is held, the maximum
squared distance is tracked; on pointerup, if the distance exceeded the
threshold, the next click is suppressed via suppressNextClick.
WASD / Arrow keyboard navigation
NAVIGATION_KEYS (Mapv10ThreeRenderer.ts) is KeyW, KeyA,
KeyS, KeyD, ArrowUp, ArrowLeft, ArrowDown, ArrowRight,
ShiftLeft, ShiftRight. Holding a Shift key multiplies translation speed
by 1.8 (Mapv10ThreeRenderer.ts). The handler at
handleKeyboardNavigationDown ignores key events whose target is an editable
element (isEditableKeyboardTarget test).
Pointer foveation throttle
handlePointerMove (Mapv10ThreeRenderer.ts) throttles pointer-move
priority recomputes to once per 50 ms. Pointer motion alone does NOT call
markFrameDirty() — only the next frame that runs for unrelated reasons
picks up the new foveation pointer position.
HUD toggle
F3 toggles the performance HUD; Shift+F3 toggles the verbose HUD readout. Both shortcuts are scoped to non-editable targets.
UI Shell
The viewer's first screen is the map, not a dashboard. Persistent UI is limited to compact top controls, a layer panel, an inspector, a scale ruler, the optional HUD, and a debug drawer hidden by default.
Entry
viewer/src/main.ts is a 10-line bootstrap that imports the stylesheet, locates
the #app root, and calls createMapv10Shell(root) from ui/shell.ts.
Top bar
The top bar holds the current run chip, the map-mode tabs, and a Products toggle that surfaces the debug drawer.
Loading panel
A loading section reports the active load phase, a progress bar, and any
fatal error text. The phases mirror the loadRun event sequence
(Mapv10ThreeRenderer.ts).
Layer panel
Eight toggles plus an overlay slider (shell.ts):
| Toggle | Default |
|---|---|
| Terrain | on |
| Water | on |
| Borders | on |
| Routes | on |
| Influence | on |
| Markers | off |
| Labels | on |
| Mesh (wireframe) | off |
| Overlay slider | 0 |
A stream sub-panel below the toggles shows phase, summary, network counter, and per-family progress. A status line below that surfaces fatal status messages.
Inspector
A right-hand inspector aside lists the current selection; before any selection it shows "click the map" placeholder text.
Scale ruler
A bottom-left ruler bar shows the current camera-distance scale and the world
target the ruler is anchored to. The snapshot is recomputed on every HUD
refresh tick and on demand via __mapv10ScaleRulerProbe.
Performance HUD
A bottom-right data-hud panel shows FPS, frame time, p50/p95/p99
percentiles, the current LOD level, tile counts, byte budgets, cache state,
band counts, pin counts, network counts, and within-band priority channels. F3
toggles its visibility; Shift+F3 toggles verbose mode.
The HUD refresh runs on a window.setInterval at 250 ms
(viewer/src/ui/shell.ts); the scale ruler also refreshes every 250 ms
(shell.ts). HUD-affecting renderer events (run-loaded, error,
camera-changed) trigger an immediate refreshHud() outside the interval so
the HUD does not lag a quarter-second behind state changes.
Debug drawer
A hidden-by-default drawer with two sections: Products (the manifest's
declared artifact list) and Previews (per-product PNG thumbnails). Toggle
state lives on [data-debug-drawer].
GL error surfacing
shell.ts intercepts console.error and console.warn, applies a
regex (WebGL|THREE|texSubImage2D|...|GPU stall) to the formatted message
text, and pushes matches into __mapv10Ready.errors. The verifier (and
external checkers) treat any captured warning as a readiness error so a
silent driver-level upload failure cannot pass acceptance.
Map Modes
The unified terrain shader serves seven map modes in a single GL program. Mode
swap is a single uModeId uniform write — no GL program recompile, no
material clone.
| Mode | uModeId | Description |
|---|---|---|
| Geography | 0 | Splat-map blend across the eight active material classes (band A grass/rock/forest/snow-or-wetland; band B sand-coast/bare-earth/ice-glacier/riverbank-mud), Toksvig roughness composition, altitude tinting, aerial haze, micro-relief. |
| Height | 1 | Per-vertex vColor height ramp (CPU pre-computed per tile). |
| Slope | 2 | Per-vertex vColor slope ramp. |
| Normal | 3 | Per-vertex vColor world-normal ramp. |
| Political | 4 | uColorMapLut keyed by RGB-unpacked uGlobalIdMap sample at world-space UV. The continent-wide ID raster avoids the tile-grid checkerboard that per-tile ID rasters produce at coarse LODs (mode-downsampling each tile to one dominant id per texel). |
| Routes | 5 | Desaturated splat blend; route ribbons handle the foreground. |
| Influence | 6 | Effective splat blend with generated corruption intensity from uInfluenceMaskCorruption emphasized for strategic territory readability. |
Selection / hover composition (uSelectionMask, uHoveredId, uBorderSdf,
uStateField) runs in every mode regardless of the dispatch branch. Layer
toggles compose orthogonally — disabling Borders zeroes uBorderOpacity
without affecting the rest of the shader.
The viewer does not expose "Scalar overlay" or "Debug" as first-class modes.
The scalar overlay is setOverlayState writing to the uStateField sampler
binding (the producer transport is a future wave; see
Known Limitations). Debug is the Products drawer plus
the F3 HUD, not a render mode.
Layer Toggles
Eight layer flags live in MapLayerState (Mapv10ThreeRenderer.ts):
| Layer | Default | How it renders |
|---|---|---|
terrain | on | The terrain pass group. |
water | on | The water pass group; lake / ocean / river batches. |
borders | on | NOT a separate mesh layer — uBorderOpacity uniform in the unified terrain shader. |
routes | on | The route pass group; ribbon meshes plus crossings. |
influence | on | Generated corruption/influence blend in the unified terrain shader; influence mode forces it visible. |
markers | off | The marker pass group; sample-point sprites. |
labels | on | The label pass group; sprite-per-label. |
wireframe | off | A THREE.WireframeGeometry overlay added per terrain mesh; renders as LineSegments. |
Markers are off by default to keep a fresh load uncluttered. Borders are
NOT a THREE.Group — disabling the layer animates uBorderOpacity to zero
in the terrain shader so the band fades cleanly without a draw-call swap.
Interaction, Picking, and Selection
The viewer uses a CPU THREE.Raycaster against the active primary terrain
meshes (Mapv10ThreeRenderer.ts interaction methods). There is no GPU
ID-buffer readback. The renderer owns semantic interaction resolution and
scene state; the shell passes canvas pixels to renderer commands and renders
the typed payloads it receives back.
inspectAt, hoverAt, and selectAt
Defined at Mapv10ThreeRenderer.ts. Sequence:
- Convert canvas pixels to NDC via the canvas bounding-rect mapping (Y is flipped because canvas-Y grows downward and NDC-Y grows upward).
- Raycast against the active primary terrain meshes.
- Convert the hit point from scene space to world km.
- Resolve the semantic target from generated products and visible vector ownership.
- Build a
MapInteractionPayloadwith state, target, world km, screen px, display name, ancestry, source product keys, cursor hint, clear reason when relevant, and inspector fields.
pick(screenX, screenY) remains a compatibility/debug helper. It delegates to
the same resolver and returns the selected target plus payload when a target is
present.
Semantic ID lookup
The resolver samples the per-cell raster at the hit world-km coordinate from
the resident provinceId.u32.bin and locationId.u32.bin sidecars, walks
visible-rendered-entity overrides, resolves water meshes, route centerlines,
map features, and terrain-cell sidecars, and returns a MapEntityRef
(viewer/src/events/types.ts):
| { kind: "continent"; id: string }
| { kind: "realm"; id: string }
| { kind: "province"; id: string }
| { kind: "location"; id: string }
| { kind: "water"; id: string }
| { kind: "route"; id: string }
| { kind: "feature"; id: string }
| { kind: "terrain-cell"; id: string }
Hover and selection
Hover and selection emit hover-changed and selection-changed events carrying
MapInteractionPayload. Location/province hover writes uHoveredId and
location/province selection drives the SelectionMask texture
(viewer/src/renderer/SelectionMask.ts), an R8 per-locationId texture sampled
in the unified shader. Route, water, feature, and label emphasis is kept inside
the renderer through auxiliary meshes, scale/color/render-order changes, and
label entityId userdata.
Terrain-cell inspector payloads expose tile id, z/x/y, row/column/index, height,
slope, water class, base/effective biome and material weights, base/effective
forest/wetland fields, and influence fields from generated influence products.
Base forestMask and wetlandMask remain preserved generated products; the
runtime tile cache loads their per-tile sidecars for inspection while the
manifest loader leaves the full-resolution root rasters unfetched on default
run load. The renderer continues to consume the generated effective tile products.
Influence inspection reports active type key/id, intensity, mask value,
available source/rule ids, effective product keys, and the marker that base
terrain products are preserved rather than mutated.
The shell handles UX commands only: pointer move calls hoverAt, click calls
selectAt, empty click clears selection with empty-click, Escape clears with
escape, and pointer leave clears hover with pointer-leave. The renderer
provides cursor hints and the shell renders the hover tooltip, selected
inspector, and breadcrumb from the payload.
raycastSurfaceAtCanvasPx
Mapv10ThreeRenderer.ts raycasts at a canvas pixel against the active
terrain meshes; if every terrain ray misses, it falls back to an analytical
sea-level plane. Used by the scale ruler so the world-target readout stays
sensible even when the camera frames open ocean.
Labels
Generator-side payload
The generator emits per-label records as part of each runtime tile
(viewer/src/data/types.ts):
{ id, text, entityKind, entityId, point, priority,
minZoomBand, maxZoomBand, category }
Viewer-side handling
The renderer:
- Filters labels by zoom band against
[minZoomBand, maxZoomBand]. - Runs collision avoidance in screen space, dropping lower-priority labels that overlap higher-priority labels.
- Fades labels in and out reusing
uTileOpacityoverLOD_FADE_MS = 320. - Highlights hovered and selected entities through per-sprite tinting.
LabelTextureCache
viewer/src/renderer/LabelTextureCache.ts rasterises label text via Canvas2D
and memoises the result, keyed by text string. A label whose text repeats
(e.g. "Forest") gets one shared THREE.Texture rather than one per sprite.
Layout cadence
Two constants control label work
(Mapv10ThreeRenderer.ts label cadence constants):
LABEL_LAYOUT_INTERVAL_MS = 100— minimum interval between collision recomputes.LABEL_WORK_ITEMS_PER_FRAME = 16— cap on label updates per frame inside the FrameBudget so a heavy label region cannot burn the budget alone.
The label sprites live in the labelGroup (mapv10-label-pass), one
THREE.Sprite per visible label.
URL Params
The viewer reads exactly one URL parameter: ?manifest=<value>.
normalizeManifestUrl(value) (viewer/src/ui/manifestUrl.ts) applies these
rules:
- Empty / null / whitespace →
DEFAULT_MANIFEST_URL(/mapv10/runs/continent-lod6/manifest.json). - Aliases
continent,continent-lod,continent-lod6→ default manifest URL (manifestUrl.ts). - Absolute URL (
http://orhttps://): preserved, with the legacy one-segment / two-segment run-path normalisation applied if relevant. - Root-relative path: parsed against
https://mapv10.local, then aliases and legacy-run paths apply, then.jsonis appended if absent.
There is no camera-pose, mode, or layer URL deep-link. Two viewers visiting
the same ?manifest= value start at the same default pose.
Debug and Scripting Hooks
The shell installs the following globals on window so verifier scenarios
and external checkers can drive the viewer without going through the DOM.
All assignments live in createMapv10Shell (viewer/src/ui/shell.ts); the
renderer itself exposes typed methods which the shell re-binds as globals.
| Global | Role |
|---|---|
__mapv10Ready | { loaded, errors } readiness state; verifier polls until loaded and errors.length === 0. |
__mapv10PickAtCenter() | Pick at viewport centre; returns MapPickResult | null. |
__mapv10PickAtCanvasPoint(xRatio, yRatio) | Pick at fractional canvas coords. |
__mapv10HoverAtCanvasPoint(xRatio, yRatio) | Run renderer-owned hover resolution at fractional canvas coords. |
__mapv10SelectAtCanvasPoint(xRatio, yRatio) | Run renderer-owned selection resolution at fractional canvas coords. |
__mapv10ClearHover() | Clear hover through the renderer with the pointer-leave reason. |
__mapv10ClearSelection(reason?) | Clear selection through the renderer, defaulting to scenario-clear. |
__mapv10SetMode(mode) | Set the current map mode. |
__mapv10SetLayerState(partial) | Patch one or more layer flags. |
__mapv10FrameEntity(target) | Frame a MapEntityRef. |
__mapv10FrameWorld(pointKm, distanceKm?) | Frame a world-km point. |
__mapv10CameraState() | Snapshot the live camera state. |
__mapv10SetCameraPose(positionKm, targetKm, fovDeg, options) | Scripted camera pose; honours bypassInteractiveLimits. |
__mapv10WaitForTilesSettled(timeoutMs?) | Resolve when the scheduler has zero in-flight and zero pending. |
__mapv10WaitForFrame() | Resolve after one stable on-screen render. |
__mapv10TerrainGeometryProbe() | Inspect resident terrain meshes' triangle counts and bounds. |
__mapv10TerrainShadingProbe() | Inspect terrain shader uniforms and visual config. |
__mapv10TerrainSelectionProbe() | Inspect selection-mask and resolved RenderCommit. |
__mapv10ScaleRulerProbe() | Snapshot the scale-ruler state. |
__mapv10RunScenario(id) | Run a scripted scenario by id. |
__mapv10ListScenarios() | List the registered scenario ids. |
__mapv10SetCacheConfig(partial) | Patch the cache config at runtime. |
__mapv10SetTerrainVisualConfig(partial) | Patch the terrain visual config at runtime. |
__mapv10TerrainVisualConfig() | Read the active terrain visual config. |
__mapv10StartZoomTrace(options?) | Start a zoom-trace session. |
__mapv10MarkZoomTrace(label, detail?) | Mark a zoom-trace event. |
__mapv10StopZoomTrace() | Stop and return the report. |
__mapv10ZoomTraceReport() | Read the latest zoom-trace report. |
__mapv10DebugState() | Snapshot of HUD-relevant state used by the HUD renderer itself. |
The Playwright verifier scripts under viewer/scripts/ are the primary
consumer; the verifier polls __mapv10Ready, drives camera poses through
__mapv10SetCameraPose, and asserts on probes.
Known Limitations
-
State-field producer unwired.
uStateFieldis bound to a world-space RGBA8 render target so the shader'stexture()call is always valid. The shader samples it with continent/world UV derived from per-fragment world km anduWorldBoundsKm; the producer transport that writes live overlay data into that render target is a future wave. -
JFA ping-pong unconnected.
FBOPool.acquirePingPongaccepts the"jfa"spec name (FBOPool.tsFBOSpecandacquirePingPong), but no caller writes to or reads from the JFA ping-pong pair. Borders render exclusively from the per-tile static SDF rasters baked offline. -
Hardcoded constants without
CacheConfigentries. The following numeric thresholds are baked into source files rather than living on a UI-exposed config field:MAX_IN_FLIGHT_TILE_REQUESTS = 24(Mapv10ThreeRenderer.ts)TERRAIN_NODE_CREATES_PER_FRAME = 24(Mapv10ThreeRenderer.ts)TERRAIN_NODE_DISPOSES_PER_FRAME = 19(Mapv10ThreeRenderer.ts)TERRAIN_TEXTURE_CREATES_PER_FRAME = 96(Mapv10ThreeRenderer.ts)AUXILIARY_NODE_DISPOSES_PER_FRAME = 64(Mapv10ThreeRenderer.ts)LOD_FADE_MS = 320(MaterialFade.ts)TERRAIN_RASTER_SAMPLE_ERROR_TARGET_PX = 6.0(VisibilitySet.ts)MAX_MISSING_ROUTE_ASSETS_PER_FRAME = 64(TileStreamingPlanner.ts)ROUTE_REQUEST_THROTTLE_THRESHOLD = 128(TileStreamingPlanner.ts)
The
TERRAIN_RASTER_SAMPLE_ERROR_TARGET_PXvalue remains locked. Seewave-3b-lod-ladder-camera-decision.mdfor the accepted and implemented z6/z7 ladder context before changing physical LOD products. -
Oversized route aggregate keys are a string-literal workaround.
OVERSIZED_ROUTE_AGGREGATE_KEYS = new Set(["route.aggregate.z2", "route.aggregate.z3"])(TileStreamingPlanner.ts) hard-codes the aggregate-key list rather than deriving it from the manifest. -
No HTTP retry. A transient HTTP error on a tile permanently fails the tile in this version of the scheduler.
-
Markers off by default and undocumented in-app. The default
markers: false(Mapv10ThreeRenderer.ts) silently hides the sample-point markers; nothing in the UI calls out that the layer exists. -
No keyboard shortcut for mode switching. The map-mode tabs in the top bar are mouse-only; there is no key binding for cycling modes.
-
Only
?manifest=deep-link. The URL does not encode the camera pose, map mode, or layer state. Two visitors to the same URL share the run but not the view.
Source / Symbol Map
A pointer index for navigation:
| Path | What it owns |
|---|---|
viewer/src/main.ts | Bootstrap. |
viewer/src/ui/shell.ts | DOM, top bar, layer panel, inspector, HUD, debug drawer, __mapv10* globals. |
viewer/src/ui/manifestUrl.ts | URL param normalisation. |
viewer/src/ui/loadErrorPresentation.ts | Fatal-error formatting for the loading panel. |
viewer/src/renderer/Mapv10ThreeRenderer.ts | Renderer core. |
viewer/src/renderer/MaterialFade.ts | Shared material factories and the unified terrain shader. |
viewer/src/renderer/lod/VisibilitySet.ts | Pure visibility-set selection. |
viewer/src/renderer/lod/TileStreamingPlanner.ts | Cache fan-out and scheduler reconcile. |
viewer/src/renderer/lod/RenderResolver.ts | Per-frame render commit and previous-frame fallback. |
viewer/src/renderer/lod/TilePriority.ts | Bands, banded priority, DESP and preload constants. |
viewer/src/renderer/TileCache.ts | TerrainTileCache, MeshAssetCache, TileRequestScheduler. |
viewer/src/renderer/RuntimeTileCache.ts | RuntimeTileCache, the CacheCoordinator. |
viewer/src/renderer/SpatialLruEviction.ts | Eviction-score formula and safeDisposeTexture. |
viewer/src/renderer/SamplerProbe.ts | GL sampler-budget probe. |
viewer/src/renderer/FBOPool.ts | Render-target pool (state-field RT, JFA ping-pong reservation). |
viewer/src/renderer/SelectionMask.ts | Per-locationId selection texture. |
viewer/src/renderer/ColorMapLut.ts | Province-color-seeds LUT builder. |
viewer/src/renderer/LabelTextureCache.ts | Canvas2D label rasterisation memoiser. |
viewer/src/renderer/FrameBudget.ts | Per-frame work scheduler with lane fairness. |
viewer/src/renderer/SpatialLruEviction.ts | Eviction policy. |
viewer/src/workers/WorkerPool.ts | DecoderWorkerPool. |
viewer/src/workers/decoderWorker.ts | Worker body. |
viewer/src/workers/decoderProtocol.ts | Wire-format types. |
viewer/src/data/types.ts | Run / tile / label data shapes. |
viewer/src/data/manifestLoader.ts | Manifest parsing and validation. |
viewer/src/data/meshLoader.ts | Mesh decoding entry point. |
viewer/src/data/rasterLoader.ts | Raster decoding entry point. |
viewer/src/events/types.ts | MapEntityRef, renderer events. |
viewer/src/config/CacheConfig.ts | Cache budget config. |
viewer/src/config/LodVisualConfig.ts | Per-zoom-band visual config. |
viewer/src/config/TerrainVisualConfig.ts | Terrain visual config (sun direction, atmosphere, relief). |
viewer/src/config/ScaleRulerConfig.ts | Ruler tick spacing. |
viewer/server/mapv10ArtifactMiddleware.ts | Vite artifact middleware. |
viewer/scripts/ | Verifier and bootstrap scripts. |
For complementary documentation see ./generator.md (generator pipeline and stage reference), ./export-contract.md (export schema for Stage 15 consumers), scenarios.md (scripted camera scenarios), extending.md (adding map modes, layers, or label categories), and wave-protocol.md (the four-step wave protocol that gates every change).