The sea-mark and ski-piste overlays add an XYZ imagery layer through the
globe port instead of building Cesium layers into a host collection. The
port drapes it on the globe on globe stacks and on the Google 3D tiles,
which the map stack now marks as its base tilesets. The row's hidden
reason reads the map stack's new getDrapeSurface, or the viewer's globe
when there is no map stack, and an overlay enabled before its viewer has
a globe port adds its layer once there is one. The overlays no longer
reach Cesium, so they leave the allow-list.
The globe port sets the base (photoreal tiles, terrain, imagery), shows
or hides the globe, and adds imagery layers: XYZ, WMTS, ArcGIS, world,
single-image and canvas sources, with admission, fetch and transform
hooks, a request budget and tile errors. Each layer drapes where its
drapeOn says: on the globe, or on the drawn base tileset, which is the
port's own photoreal tileset or one the map stack marks as its base,
followed every frame so a base switched outside the port moves the
layers. Port layers keep their order above other layers in each
collection. The sky reads the scene and writes through to it, and the
port reports settling and load progress.
The application renderer and the conformance renderer both have it, so
the application renderer now has every member of the contract. A viewer
with no renderer bound gets a globe port through findGlobe. The Cesium
conformance run checks imagery order, the split and the image-hook gate.
camera.controls now maps the contract's CameraControls onto the scene's
ScreenSpaceCameraController: the enable flags, zoom distances, zoom
factor, coast as Cesium inertia (decay seconds, 0 for no coast), the
collision switch, the tilt limit as the upper end of pitchRange, and the
zoom bindings by name. suspend and enabled use the scene's shared input
holds, so they count with the input port's and the dispatcher's.
A follow's zoom hold now composes with the settings: minDistance and
coast.zoom read and set what the follow gives back, a higher floor takes
effect at once, and a follow never lengthens a shorter zoom coast.
The ctrl+wheel binding check reuses the controls' binding helpers, and
the controls checks run in the Cesium conformance set, the settings one
in Node as well.
The Cesium quality port supports pixel ratio, multisampling, shadows,
3D tile and terrain detail and the target frame rate, each applied when
first set and nothing changed before. Detail scales each tileset's and
the globe's own screen-space error and gives it back at full detail;
while tile detail is lowered, tilesets added later follow it. Frame
signals carry each drawn tick's CPU time, size, loop mode and holds.
The application and conformance renderers have the port, and the
conformance page runs the quality checks against Cesium.
The Cesium camera module now loads the credits factory, so the flight,
military and satellite packages own the credits modules too, and the
infrastructure consumer build lists them.
The renderer bridge builds and keeps an effects port over a viewer no
renderer is bound to, with a factory the Cesium effects module provides,
so visual settings on a consumer's own viewer draw their styles, bloom
and sharpen as before and give the scene's bloom back when destroyed.
The application shell loads the factory; a test checks every export
that reaches the effects lookup loads it.
VisualEffects defines each preset on the renderer and keeps its own
records of intensities and parameters, written through to the port, so
crossfades, the anime sky fade, bloom and sharpen behave as before. The
renderer animates the animated styles. Sensor picks read the style
records instead of the renderer's stages. The application renderer now
has the effects port and its capabilities.
The effects port puts each style Core defines on its own post-process
stage, takes the scene's bloom stage and adds a sharpen stage, with the
mappings the application uses today. An animated style holds frames and
advances its time uniform while it draws. A replaced style keeps drawing
until its intensity is 0, so Core can crossfade; the contract, the stub
and a new conformance check say so. The conformance renderer has the
port; the application renderer does not yet.
The street-level provider credits, the map source credit, the shared
inline OpenStreetMap and OpenMapTiles credits and the ALPR provider credit
go through the renderer's credits port instead of Cesium's credit display.
Their markup has one link each and is read as the port's plain data; the
ALPR credit is built as plain data, which the port escapes. src/maps/credits.js
and src/layers/streetLevel/credits.js no longer reach Cesium and leave the
allow-list.
findCredits finds the port as findCamera finds the camera: a Cesium viewer
of a consumer's own, with no renderer bound, gets a credits port over its
own credit display, so the public entries keep their attribution. A port
built that way is released, not disposed, when a renderer binds later, and
a credit let go after its viewer is destroyed leaves the display alone.
The per-layer data credits and the transit and event credits stay on
Cesium's display: their markup carries several links each, which the
contract's credit cannot hold.
The shared Cesium ports gain credits, on both the conformance renderer and
the application's. list() reports what Cesium owes as plain data: its logo,
its static credits and the last drawn frame's imagery, terrain and tile
credits, read from the credit display through internals.js, with the
credits Core adds listed as given where the display shows them. addStatic
shows a credit through Cesium's display, as markup built from the plain
data with every value escaped and only web links kept, so attribution stays
on screen. change fires when Core adds or removes a credit, and after a
drawn frame whose Cesium credits differ from the last one's.
src/render/creditMarkup.js reads markup credits as plain data. The credits
listener check moves out of renderer.listenerRules into its own check, so a
renderer without every port can run it; it and the credits check now run on
Cesium.
Flights and military follow their tracked aircraft with an
entityFollowTarget: framed in ENU from viewFrom, never closer than
150 m, presentation read from the drawn entity's record. Satellites
follow in Cesium's autodetect frame. The layers' own framing
(applyTrackedCameraFrame) goes; the port's follow has the same
close-range rules, and trackedCamera.js no longer imports Cesium.
The port's helper entity draws nothing: its point's outline is
transparent too, and click and voice picks of it count as picks of
the target's drawn entity. The fake camera binder follows when the
fake has Cesium's entity tracking.
The tracked card, screen annotations, north-up, share links and Cockpit
ask data/followed.js what the camera follows, so they work the same when
a layer follows through the camera port. followed.js gains
entityFollowTarget (a FollowTarget whose presentation reads the drawn
entity's record), resumeFollowing and followedPosition.
src/data/followed.js answers what the camera follows: the camera port's
target, or the entity a layer still tracks itself, seen through the same
fields. Ownership checks, render holds, guards, voice actions, the CCTV
pacing and the navigation release paths read it instead of Cesium's
trackedEntity, so they keep working when layers follow through the port.
The release paths stop following, cancel the flight and release the
camera through the port. The rotate refusal names the followed target by
its name or label title.
The Cesium camera port follows a target with Cesium's own entity
tracking, on a hidden helper entity per target, and gains followed,
followFramed, followOffset and followChanged. A target with a minimum
range gets GEV's close-range rules, moved here from trackedCamera.js:
an exact first framing, no zoom coasting, a zoom floor, and an eye that
never crosses through the target. followChanged now fires only when
what is followed changes, on the stub too. No layer follows through the
port yet.
The scene director's shot flights, start pose and stops, the annotation
framings and fly-to, the voice overhead framing and the local GeoJSON
click flight now drive the renderer's camera port. The director's stops
look the camera up with findCamera: teardown can run after the viewer is
gone, and then there is no flight to stop. The local GeoJSON flight,
part of the public infrastructure export, works on a plain Cesium viewer
through the unbound camera, and looks it up before taking its input
hold. Voice zoom stays for the move slice.
The embed view, the celestial-ring focus, the ground-guard lift, the
Bhote Koshi approach and orbit, launch selection and replay flights, and
the cockpit's view and takeover now drive the renderer's camera port.
The contract gains a sineInOut easing, pinned by a conformance trace.
A viewer with no renderer bound, such as one a consumer of a public
export brings, now gets a camera port too, as it already got an input
port: cameraOf builds one over the viewer's own Cesium camera and keeps
it for the viewer's life, until a renderer is bound. The Cesium adapter
provides the factory as it loads; code that reaches Cesium takes cameraOf
from the adapter so the factory comes with it, and findCamera answers
null where a missing camera is not an error (HUD, share link, world
focus, Recent Imagery's ZOOM IN). A test walks every package export and
fails one that can reach the bridge without loading the factory.
A destroyed viewer gets no camera and lets go of a built one, and the
application renderer stays bound until its viewer is destroyed.
flyToBounds takes an easing, as flyTo does: in the contract, the Cesium
port and the reference recorder, pinned by a new conformance trace.
The flights of radio, awareness, CCTV, submarine cables, cyclones, the
launch preview, installations, ALPR, street-level framing, recent
imagery, weather and world focus now go through the renderer's camera
port. Street-level framing cancels its own flight handle rather than
whatever flight is current. worldFocus.js no longer reaches Cesium and
leaves the allow-list.
Layer pose reads stay for each layer's own slice. The local GeoJSON
flight stays on Cesium: gods-eye-view/infrastructure is used with plain
Cesium viewers that have no renderer bound.
The Cesium camera port moves into render/cesium/camera.js, with the
listener list and position conversion shared from common.js. It now
covers flights (flyTo, flyToBounds, cancelFlight, flying), lookAt and
release, command hooks, and a flightEnd event for the flights it starts.
A flight's end that Cesium raises inside one of the port's own calls is
delivered once Cesium's call returns, so its callback sees the flight
let go of. A flight Cesium declines to start, mid scene-mode morph,
ends as cancelled instead of never settling.
Flight traces run in Node on the recorder's headless camera with a
virtual clock; the browser harness keeps the instant traces.
The startup fly-in, landmark and viewport flights, share-link restore
and the shell's camera state now go through the port. camera.js and
sharelink.js leave the Cesium allow-list.
The first camera-family slice. The camera port's events move into a
shared module (render/camera/events.js) that the stub and the Cesium
port both use: listener lists, delivery with errors reported, and the
contract's per-listener `changed` measure.
The Cesium port gains:
- camera.on: `changed` measured per listener on every tick, where Cesium
measures its single threshold (the counts match Cesium's own at each
threshold); moveStart and moveEnd relayed from Cesium's camera;
- camera.viewRectangle, in degrees (now in CESIUM_CHECKS);
- lon and lat in pose, and NaN where Cesium has no cartographic position.
Code that still holds the viewer reaches the renderer through
rendererOf(viewer); the application renderer binds it on create and
unbinds on destroy.
The HUD reads its telemetry from the camera port and no longer imports
Cesium (off the allow-list). The share link writes the hash from the
pose. Its restore path stays on the viewer's camera for a later slice.
Behaviour changes:
- The share link listens to `changed` at its own 0.5 threshold, not at
whatever global threshold layers had set, and also to moveEnd, so the
hash always catches up with where the camera comes to rest.
- The HUD's and share link's camera callbacks run from the port's single
relay, ahead of other moveStart/moveEnd listeners, and a throw in one
is reported to the renderer's error listeners instead of escaping from
Cesium's render.
The Cesium renderer now creates the input port and exposes it as
renderer.input. The input dispatcher becomes an adapter over the port:
one port listener per event type, fanned out to its subscribers with the
ownership check read once per event, keeping the legacy event shapes.
- Ctrl+wheel zoom is switched on through renderer.input.ctrlWheelZoom.
- A dispatcher made for a viewer with no port creates its own port with
takeStock: false, which borrows the stock select and double-click
follow only while they are suppressed.
- The stock actions see the press as it began: a click whose press
started while select or follow was held off never runs them.
- A scene accepts one input port; cesiumInputOf(scene) returns it.
- Releasing one mouse button while another is held no longer gives the
held button a click. Cesium still ends that press.
installTrackpadPinchZoom and its pinch scaling move unchanged from
src/app/viewer.js to src/render/cesium/ctrlWheelZoom.js, where the input
port's ctrlWheelZoom will turn it on and off. The application scene
imports it from there; the application/viewer export no longer carries
this undocumented helper.
The take-over of the camera controller's enableInputs moves, unchanged,
to src/render/cesium/cameraInput.js. acquireCameraInput(scene) gives every
user one set of counted holds per scene, so the renderer's input and
camera ports can hold camera input alongside the dispatcher; the
controller gets its own property back when the last user releases. The
dispatcher acquires its share once its handler is bound. The input guard
names the one module allowed to write the flag, and the new render files
join the format scope.
The stub's lease and press routing moves to src/render/input/router.js,
unchanged, so a real renderer can feed it its own engine's events. The
router also keeps the behaviour holds (telling the renderer when one turns
off or on, unless it lacks it) and ctrlWheelZoom, and dispatch says who an
event went to.
The frame, draw, camera, query and capture ports move out of the
conformance scene renderer into src/render/cesium/ports.js, unchanged.
The app's transitional renderer, now src/render/cesium/appRenderer.js,
builds the same ports over the full viewer; it still leaves out globe,
whenSettled and setTime, which would misreport photoreal tiles and stop
the app clock.
Disposing the ports now settles a pending renderOnce and detaches the
draw sets, so a model that loads after destroy is destroyed rather than
added to a destroyed collection. A viewer whose ports fail to build is
destroyed with its credits.
Checked in a browser: the app boots with one credits element, and all
eleven conformance scenes match their references exactly.
createApplicationScene takes a renderer plugin, defaulting to Cesium,
which builds today's viewer and credits exactly as before. Its renderer
is transitional: it has only the members it really has, with read-only
capabilities, and a test pins the contract members still to come. Code
not yet ported reaches the viewer through cesiumViewerOf, never through
a renderer member. A plugin whose renderer has no viewer is refused and
destroyed, since the application still runs on the viewer. The result
now includes the renderer.
Not yet checked in a browser.
Ports Cesium's axis-angle, product, rotation-matrix and heading-pitch-roll
quaternions, with rotations given as frame axes. They match Cesium bit for
bit, including heading-pitch-roll frames on the polar axis. The model
convention is documented: at heading 0 the forward axis points east.
A port of Cesium's Simon 1994 sun and moon positions, its TEME to
pseudo-fixed rotation and the JulianDate time handling, with epoch
milliseconds as the only input. Results are identical to Cesium's,
tested with exact equality from year -500 to 3000 and across every
leap second. Times outside the model throw a RangeError, where
Cesium's debug build throws.
The world packet converts the eye position with toLonLatH instead of
Cesium's Cartographic, so the agent-world-packet group no longer needs
cesium. Degree conversions use precomputed factors, as Cesium.Math does,
so results stay bit for bit the same.
WGS84, ECEF and geodetic conversions, surface normals and east-north-up
frames, as plain {x, y, z} objects, exported as ./geo. A test checks
them against Cesium over random points and the poles. The stub's maths
re-exports them and keeps only the camera angle conventions.
- lodGroup links items across sets as one thing at different detail;
picks name the member shown and report the group. displayRange is
half-open.
- An orbits capability: the renderer propagates two-line elements with
SGP4 on the set's motion clock. The stub propagates with satellite.js.
- Height queries see draw sets only with includeInHeightQueries (was
excludeFromHeightQueries); HeightSample.source gains 'set'.
- courseGlide is a capability; courseBlendMs eases the course on every
renderer.
- Arc segments climb at climbMps; a negative minDtS stays for the
aircraft warm-up.
- The star catalogue is optional, a hidden column row keeps its motion,
and draw.clampState() reports the drape fallback.
Checks cover the climb and warm-up, lodGroup ranges, orbits against SGP4
(with and without motionSegments), hidden rows resuming, and sets not
answering height queries by default.
The renderer contract is what the application asks of a 3D renderer, so a
renderer other than Cesium can draw the same application. It is nine ports
(frame, globe, draw, camera, query, input, effects, credits, quality) plus
the renderer itself, one strict .d.ts per port in src/render/contract/.
PORTS lists every member at run time, and a TypeScript-compiler test fails
when the list and the types disagree on one member. An optional member
exists only with its capability; a required one answers null or empty
without it. Nothing in the application uses the contract yet.
The stub renderer (src/render/stub/) implements every port in memory and
draws nothing. Its camera is a port of the default camera's maths: views,
lookAt, flights with their height arc and easing, rectangle framing on the
geodesic midpoint, the view rectangle with its horizon and pole cases,
changed thresholds, follows, and flights that finish inside the call.
The conformance suite (src/render/conformance/) runs 59 checks against
any renderer through the contract alone, in Node or in a browser page. The
camera checks compare traces recorded from Cesium's own Camera, headless on
a virtual clock (scripts/record-camera-goldens.mjs), to 1 mm, 1e-9 and
1e-3 px. The other checks cover listener rules on every subscription, the
frame loop, draw sets, picks, motion clocks, heights, leases, imagery,
styles, credits and quality knobs. The stub passes all of them but the
model check, which needs a glTF asset.
Reference scenes draw one scene per primitive family, a city and a
calibration path from orbit to the street, from contract calls only and
with no network. /dev/conformance/ draws them on the Cesium scene renderer
(src/render/cesium/sceneRenderer.js), records references through a
dev-server-only, loopback-only endpoint, and compares captures with
pixelmatch's antialias detection plus a solid-pixel count. Measured on
Cesium: every MSAA setting passes, and a dropped 4 px point, a dropped 2 px
line and a 1 px shift of the view fail. visual.js states the limits.
The package exports ./render/contract, ./render/stub and
./render/conformance. Documented in docs/RENDERER-CONTRACT.md,
docs/CODE-BOUNDARIES.md and docs/CURRENT-STATE.md.
The arrival ground guard replaced camera.flyTo, flyToBoundingSphere,
setView, lookAt and cancelFlight with wrappers so a direct caller would stop
it. It now remembers the pose it started on and, before each retry, yields
if a flight is under way or the camera has moved across the ground, down or
around. A straight rise is not a move: Cesium's terrain collision lifts a
buried eye that way as tiles load, which is the case the guard exists for.
This also catches owners the wrappers missed (a pose written directly,
lookAtTransform), and no camera method is replaced.
Recent imagery and weather overwrote provider.requestImage (weather also
provider.credit) to ration tiles. They now state that policy as a hook with
withTileRequestHook(provider, { requestImage, credit }) from
src/render/cesium/tileRequests.js, which hands Cesium a provider delegating
to the original and leaves the original untouched.
Every private Cesium field Core reaches goes through a named helper in
src/render/cesium/internals.js: the main and offscreen pick views and their
command extents, a billboard's collection, a cluster's point collection,
the widget's render-loop latch, the WebGL context and the current camera
flight. internals.test.mjs fails when the installed Cesium stops declaring
one of them, and a source scan fails on private access anywhere else. The
cyber sonar's primitives.update wrap stays until the effects work.
Live (Austin, keyless globe): a buried eye is lifted to 253 m over a 133 m
surface; a setView or flyTo by another caller makes the guard yield; radar
loads its tiles through the hook. Documented in docs/APPLICATION.md and
docs/CURRENT-STATE.md.
About 20 sites called scene.sampleHeight, globe.getHeight,
scene.clampToHeight or the MostDetailed variants directly, each with its own
idea of a miss (undefined, NaN, a throw). They now ask heightsFor(scene) in
src/queries/heights.js:
- now(point, { against, exclude }) answers from what is loaded, or null;
against is 'surface' (what is drawn) or 'terrain' (loaded terrain only).
- clampNow(position) moves a position onto the drawn surface, or null.
- batch() and clampBatch() wait for the most detailed tiles: null per
missed point, a rejection when the whole query fails.
- supported() reports { surface, clamp } without allocating.
Cesium still answers now() synchronously, so nothing changes on screen and
nothing is slower: in the live app 1,000 calls cost the same through the
service as direct (terrain 0.5-0.7 ms, surface 2-6 ms a call, run-to-run
noise larger than the difference). Every site already treated a miss as
"hold the last value or fall back"; a renderer that can only answer from a
cache or asynchronously can now plug in at one place.
- A Bhote Koshi evidence point whose terrain sample misses stays at its
fallback elevation instead of reading the miss as sea level.
- The mesh floor sampler skips a scene without depth-texture support
instead of spending its budget on probes that throw.
- The world annotation probe's trace reads hit/missed.
- A source scan forbids direct height calls outside the service.
The service is exported as ./queries/heights and documented in
docs/APPLICATION.md.
Every layer and tool bound its own ScreenSpaceEventHandler and picked the
same click again: with the usual layers on, one click ran about 26
scene.pick and 2 drillPick passes (120-215 ms in NYC with 18 layers). Pointer
input now comes in through src/input/dispatcher.js, one handler per scene.
A click is picked lazily and at most once per kind of query, and every
subscriber reads the same ClickEvent (pick, hit, id, ownerId, gesture,
drill, ground). The same click now costs 3 picks and 2 drills, 17-70 ms.
- Ambient onClick subscribers run only while no tool holds the pointer,
checked once per event; tools subscribe with their lease. Moves and
releases always reach ambient subscribers, so hover and drags end cleanly.
- Camera input and the viewer's stock select/track clicks are ref-counted
holds (suspendCamera, suppressStockClicks). The dispatcher owns the
controller's enableInputs, so a Cesium flight that ends no longer turns
input back on under a drag or the cockpit.
- The draw tool, imagery box, CCTV gizmo, cockpit, directions and every
selecting layer use it; bindTrackingClickGesture is gone (the gesture is
on the event). A source scan forbids new handlers, enableInputs writes and
stock-click removal outside the dispatcher.
- The sensor-style pick correction transit had now applies to every layer.
- Radio's 8-pick near-miss ring is one 17x17 windowed drill.
- A missed pick passed to pickContact is no longer picked again, and a
touch tap's contact search shares the click's drill.
- A gizmo drag whose release Cesium never reported ends on the next press
instead of leaving the camera held.
The dispatcher is exported as ./input with the pointer-lease functions, and
documented in docs/APPLICATION.md.
Aircraft draw from GPU icon sets, so the billboard mirror, the billboard
treatment and the aircraft visual-state module had no callers left. 3D models
are now records the layer holds under the contact's id (modelRecord.js):
show and placement are written through showModel/placeModel and read back
from the record; renderer-measured values stay on record.model.
The tracked-model click check only matches when a tracked model exists, so
an id-only pick is never mistaken for a click on the tracked plane.
The flights icon planning and presentation move into
`createAircraftIcons` (src/data/aircraftIconLayer.js), and the motion
regime both layers dead-reckon from into src/data/aircraftMotionRegime.js.
Flights keeps only its grounded-floor pass on top.
Military now draws every contact as a row of that icon set, like
flights: the billboards, the cull positions and the rotation-pass
signature go. Tracking hides the row and untracking re-plans it from
the tracked course. Queries read the drawn row position and judge the
horizon as the shader does. Snapshots that arrive before init or after
destroy are ignored, and per-contact lookups find no contact then
instead of throwing.
With about 120 live military contacts, preRender p95 drops from
0.7-0.8 ms to 0.2 ms on the same views. Render time and frame gaps are
unchanged or slightly lower.
Every flight is now a row in the layer's icon set. Its motion segment
follows the same regime `_deadReckon` picks (interpolate between fixes,
warm up backward from the oldest, coast forward from the newest), and
one shared `_motionRegime` makes that choice for both. The shader moves
every untracked contact between polls, so the 12 Hz fleet tick no
longer touches each billboard. It now only re-plans expired rows,
floors grounded contacts, advances focus while needed, and runs the 3D
model regime.
Grounded contacts the floor lifts are pinned to a hold at the floored
position. Their nose glides on the GPU. Contacts within 50 km re-floor
every tick, and farther ones take turns about once a second. Low
contacts are judged for the horizon at no lower than 12 m above the
ellipsoid, as before. Below hover speed a contact's nose holds. A row
whose fleet model is drawn keeps its focus updated, because the model
reads it. Untracking re-plans and re-floors the icon from the tracked
course.
On the same views with about 6,700 OpenSky flights, frame gaps at p95
drop from 28-40 ms to 17 ms, and the fleet tick from 16-37 ms to about
2 ms at most.
Icon rows live in typed-array columns: one packed motion segment (hold,
lerp or arc, with a course glide), style, colour and pick ids per row.
The primitive draws every row in one instanced call, dead-reckons in the
vertex shader, matches the billboard treatment the fleet layers use, and
picks and drill-picks like a billboard. Cyber sonar covers icon sets.
Earthquake analyst records come from the events the layer drew, gated on
its own enabled flag, instead of reading each entity's position and
property bag back; the discs no longer carry the bag. CCTV coverage rays
and projection planes record their camera id in a layer pick store
instead of a cctvCameraId entity property, with the same ownership checks.
The tracked flight, military and local ADS-B billboards record their size
and distance scaling as the entity record's sprite, which the close-range
camera guard and the screen annotations' tracked footprint now read in
place of the billboard's property getters.
Flights, military and local ADS-B now write their models' show and world
matrix through setModel in src/data/modelMirror.js, and read them back
through modelState: the fleet and tracked handoffs, the tracked visual and
trail anchors, detection anchors and modelAnchorWorld. A matrix is copied
into Core's record and into the model's own matrix, so the shared scratch
can be passed directly and models still never share one matrix object.
What Cesium measures (ready, computedScale) is still read from the model,
and colour stays live as on billboards, because cyber sonar writes it too.
Test seams mirror the models they install, as they do billboards.
The three layers now add their billboards through addBillboard and write
the mirrored fields through setBillboard, so selection, cards, queries,
focus, view thinning and the FIRMS horizon cull read what was pushed from
Core instead of from Cesium. Vessels and FIRMS state their sprites' own
sizes (32 px ship artwork, the glow canvas at twice the core size), which
the mirror needs. A vessel helper, iconPosition, replaces the repeated
mirror-or-visual position lookups, and the vessel visibility pass writes
show only when it changes, as CCTV and FIRMS already did.
A billboard never given a position now has none in the mirror rather than
the origin.
Flights and military now add their billboards through addBillboard and
write show, position, scale, rotation, size and distance scaling through
setBillboard, which keeps Core's copy in src/data/billboardMirror.js.
Their reads go to billboardState instead of the Cesium billboard, so the
layers no longer read back what they pushed to the renderer.
A mirrored billboard needs an explicit width and height, because unset
Cesium reports the loaded image's size. Colour is not mirrored yet: the
cyber-sonar theme writes it too and tells the writers apart by the live
value, so it moves with the effects port.
Submarine cables wrote each pick's info both to a per-load WeakMap and to a
__gevTeleGeography expando, always together; one cablePicks store replaces
both and resolves the same objects. The voice view-target prewarm marks its
viewer in a WeakSet instead of a __gevViewTargetPrewarmInstalled property.
The aircraft layers' model spec key, IR boost and belly offset and the
billboards' large-icon flag move into aircraftVisuals, one store shared by
flights, military and local ADS-B. Installation markers' last drawn height
and selection move into installationMarkers. The CCTV viewshed QA tag is now
the viewshedCameraId record field, which qa-cctv-v2 reads through
entityRecords. Test fixtures drop a placement flag nothing reads.
The ALPR layer's private per-entity fields (record, clamp, canvas and display
anchors, wedge, native appearance, pick position, shown, surface check) move
from gevAlpr* expandos into alprMarkers, a store made by the new
createEntityStore(). The shared entity records are built on the same factory.
Flights, military, satellites, installations, local ADS-B and ALPR hung
gevTrackedId, gevDisplayPosition, gevVisualPosition, gevSelectionOrigin and
gevLabelModel on the Cesium entities they draw, and the tracked readout,
follow camera, cockpit, share links and awareness read them back. They now
live in src/data/entityRecords.js, a Core-owned record per entity: layers
write entityRecord(entity), readers use readEntityRecord(entity). Nothing
changes on screen. This is the first slice of the renderer plan's state
mirror (P1).
The module is exported as ./data/entity-records so hosted code can read the
same records, and window.__godsEyeView.entityRecords gives the QA scripts
the same access from the page.