feat(flights): render on Navara

Task 10b. src/layers/aircraft/render/navara.js implements the aircraft
renderer contract on the engine contract alone (no three, no Cesium):
- Fleet glyphs: banks of engine.movers sprite sets (one per distance scale
  and depth mode the handles ask for; flights: one, 'always', Cesium's
  NearFarScalar), growing by doubling, from a glyph atlas of every
  aircraftIcon() kind at the fleet (64 px) and tracked (192 px) rasters
  (data/aircraftIcons: AIRCRAFT_ICON_KINDS, aircraftIconCells,
  aircraftIconAtlasLayout, drawAircraftIconAtlas). Sprite handles keep the
  Billboard field vocabulary as setters; a frame's writes commit once at
  the end of the frame listener that made them (a microtask outside one).
- Models: banks of model mover sets per glTF and look; handles report
  ready from MoverSet.ready() and computedScale from minimumPixelSize kept
  on the CPU (radiusM in the spec, the class's nativeRadiusM); poses are
  Cesium's HPR frame (modelMatrix) turned by Navara's glTF axes
  (Z_UP_TO_X_UP: its loader is already Z-up; chosen side by side with
  Cesium among six candidates).
- Tracked glyph: a sprite whose position/colour/rotation callbacks are read
  every frame in preRender; follow = engine.camera.follow with the tracked
  readout's card as its label (gevFollowHandle); current() maps the follow
  back to the tracked handle, also while follow() raises followChanged.
- Trail and live head: engine.lines (alpha 0.85, depth-fail 0.4, geodesic)
  in the gev-trail pick namespace.
- View: camera/scene adapters over engine.camera.pose() (registered with
  the engine so shared helpers project through it), frustum and horizon
  tests, frame number, engine frame events.
flights/render/navara.js registers it for 'navara' from the Navara mode's
import(); the Navara catalog now offers Live Flights.

Also: tracking a contact that is selected but no longer followed follows it
again (a camera command ends a Navara follow; on Cesium a search flight
releases trackedEntity): trackById/select re-follows instead of only
re-publishing. Behaviour change on Cesium only in that case.

Tests: the renderer on the fake engine (contract, atlas, banks, per-frame
commits, growth, tracked callbacks, follow label, model pose, trails) and
the flights logic over it (fleet, click-select, follow, re-follow after a
camera command, release, no set left behind).
This commit is contained in:
German Khokhlov committed 2026-10-17 02:15:04 +04:00
1 parent 20e9d4a0fa
commit b14bf7e013
10 files changed
+1530 -3

No files matched your search

+3
View File
@@ -322,6 +322,9 @@
"src/layers/flights/evidence.js",
"src/layers/flights/ingestion.js",
"src/layers/flights/render/cesium.js",
"src/layers/flights/render/navara.js",
"src/layers/aircraft/render/navara.test.mjs",
"src/layers/aircraft/render/navara.js",
"src/layers/flights/renderer.test.mjs",
"src/layers/aircraft/render/cesium.test.mjs",
"src/layers/aircraft/render/cesium.js",
+3 -1
View File
@@ -2592,7 +2592,9 @@
"src/layers/aircraft/render/cesium.js",
"src/layers/flights/render/cesium.js",
"src/data/selectionDispatcher.js",
"src/layers/shared/color.js"
"src/layers/shared/color.js",
"src/layers/aircraft/render/navara.js",
"src/layers/flights/render/navara.js"
],
"external": [
"hls.js",
+21 -1
View File
@@ -5,7 +5,11 @@
import { DATA_CREDITS, registerDataCredits } from '../data/dataCredits.js';
import { isEmbeddedInline } from './embed.js';
import { createLayerAvailability } from './layers/renderers.js';
import {
createLayerAvailability,
layerAvailableOn,
registerLayerRenderers,
} from './layers/renderers.js';
import {
getRenderGovernorDiagnostics,
governorRequestRender,
@@ -45,6 +49,18 @@ const SHELL_CREDIT_KEYS = new Set([
'reearth-terrain',
]);
/**
* Registers the Navara layer renderers (once per page). Each loads only
* here, through import(), so Cesium mode never fetches them.
*/
export async function registerNavaraLayerRenderers() {
if (layerAvailableOn('flights', 'navara')) return;
const { createFlightsNavaraRenderer } =
await import('../layers/flights/render/navara.js');
if (!layerAvailableOn('flights', 'navara'))
registerLayerRenderers('navara', { flights: createFlightsNavaraRenderer });
}
/**
* The scene on the experimental Navara engine: no viewer and no Cesium
* objects. Navara builds the map sources from the controller's descriptions
@@ -72,6 +88,10 @@ export async function createNavaraScene({
});
defer(() => globe.destroy());
const { engine } = globe;
// The layers drawn on Navara (their render/navara.js), before the catalog
// asks the registry which layers this engine shows.
await registerNavaraLayerRenderers();
signal.throwIfAborted();
// The tools install the governor on this engine; it goes before the engine.
defer(() => uninstallRenderGovernor(engine));
signal.throwIfAborted();
+66
View File
@@ -261,6 +261,10 @@ const TRACKED_RASTER_PX = 192;
* Default size serves the fleet; pass `aircraftIcon(kind, TRACKED_ICON_PX)`
* (re-exported below) for the tracked billboard. */
export const TRACKED_ICON_PX = TRACKED_RASTER_PX;
/** The fleet raster size (aircraftIcon's default). */
export const FLEET_ICON_PX = FLEET_RASTER_PX;
/** Every silhouette kind aircraftIcon draws (an engine's sprite atlas). */
export const AIRCRAFT_ICON_KINDS = Object.freeze(Object.keys(BODIES));
export function aircraftIcon(kind, px = FLEET_RASTER_PX) {
const k = BODIES[kind] ? kind : 'airliner';
const key = `${k}@${px}`;
@@ -272,3 +276,65 @@ export function aircraftIcon(kind, px = FLEET_RASTER_PX) {
}
return uri;
}
// ---------------------------------------------------------------------------
// The glyph atlas (engines that draw sprites from one texture: Navara)
// ---------------------------------------------------------------------------
const ATLAS_PAD = 2;
const atlasCellKey = (kind, px) => `${kind}@${px}`;
/** Every aircraftIcon() data URI → its atlas cell key (`${kind}@${px}`). */
export function aircraftIconCells() {
const cells = new Map();
for (const kind of AIRCRAFT_ICON_KINDS)
for (const px of [FLEET_RASTER_PX, TRACKED_RASTER_PX])
cells.set(aircraftIcon(kind, px), atlasCellKey(kind, px));
return cells;
}
/**
* The atlas layout (pure): the tracked raster in row 0, the fleet raster in
* row 1, one column per kind, ATLAS_PAD px around each cell; cells are
* [u0, v0, u1, v1] in 0..1.
*/
export function aircraftIconAtlasLayout(kinds = AIRCRAFT_ICON_KINDS) {
const big = TRACKED_RASTER_PX + 2 * ATLAS_PAD;
const small = FLEET_RASTER_PX + 2 * ATLAS_PAD;
const width = kinds.length * big;
const height = big + small;
const rects = new Map();
const cells = {};
kinds.forEach((kind, column) => {
for (const [px, row] of [
[TRACKED_RASTER_PX, 0],
[FLEET_RASTER_PX, big],
]) {
const x = column * big + ATLAS_PAD;
const y = row + ATLAS_PAD;
const key = atlasCellKey(kind, px);
rects.set(key, { x, y, size: px });
cells[key] = [x / width, y / height, (x + px) / width, (y + px) / height];
}
});
return { width, height, rects, cells };
}
/** Draws the atlas (browser): a canvas and its cells, once the SVGs decode. */
export async function drawAircraftIconAtlas() {
const layout = aircraftIconAtlasLayout();
const canvas = document.createElement('canvas');
canvas.width = layout.width;
canvas.height = layout.height;
const context = canvas.getContext('2d');
await Promise.all(
[...aircraftIconCells()].map(async ([uri, key]) => {
const image = new Image();
image.src = uri;
await image.decode();
const { x, y, size } = layout.rects.get(key);
context.drawImage(image, x, y, size, size);
}),
);
return { canvas, cells: layout.cells };
}
File diff suppressed because it is too large. Load diff
+291
View File
@@ -0,0 +1,291 @@
// The aircraft renderer on the engine contract (Navara's), over the fake
// engine: the contract's members, sprite banks with the glyph atlas, handle
// writes committed once per frame, the tracked glyph's per-frame callbacks,
// the follow and its readout label, model poses and the trail lines.
import test from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import * as geo from '../../../geo/index.js';
import { createFakeEngine } from '../../../globe/testing/fakeEngine.js';
import { aircraftIcon, TRACKED_ICON_PX } from '../../../data/aircraftIcons.js';
import { CYAN, WHITE, nearFarScale, rgbaFromCss } from '../../shared/color.js';
import { missingAircraftRendererMembers } from './contract.js';
import {
aircraftAtlasLayout,
aircraftIconCells,
createAircraftNavaraRenderer,
} from './navara.js';
const layout = aircraftAtlasLayout();
const drawAtlas = async () => ({
canvas: { width: layout.width, height: layout.height },
cells: layout.cells,
});
async function setup() {
const engine = createFakeEngine();
const renderer = createAircraftNavaraRenderer({
engine,
layerId: 'flights',
order: 8,
headingOffsetDeg: 180,
drawAtlas,
});
await renderer.whenReady();
return { engine, renderer };
}
const setsOf = (engine, prefix) =>
engine.movers.list().filter(({ id }) => id.startsWith(prefix));
test('the Navara aircraft renderer has every member of the contract', async () => {
const { engine, renderer } = await setup();
assert.deepEqual(missingAircraftRendererMembers(renderer), []);
assert.equal(renderer.kind, 'navara');
renderer.destroy();
engine.destroy();
});
test('the atlas has every glyph at the fleet and tracked sizes', () => {
const cells = aircraftIconCells();
for (const key of cells.values())
assert.ok(layout.cells[key], `${key} in the atlas`);
for (const [, [u0, v0, u1, v1]] of Object.entries(layout.cells))
assert.ok(u0 >= 0 && v0 >= 0 && u1 <= 1 && v1 <= 1 && u1 > u0 && v1 > v0);
assert.equal(
cells.get(aircraftIcon('quadjet', TRACKED_ICON_PX)),
`quadjet@${TRACKED_ICON_PX}`,
);
});
test('fleet sprites write the mover store, once per frame', async () => {
const { engine, renderer } = await setup();
const position = geo.cartesian3.fromDegrees(-97.7, 30.2, 3000);
const sprite = renderer.fleet.add({
position,
image: aircraftIcon('helicopter'),
width: 20,
height: 20,
scale: 1.5,
rotation: 0.3,
color: rgbaFromCss('#FFB800'),
scaleByDistance: nearFarScale(1000, 3.0, 8000000, 0.5),
disableDepthTestDistance: Number.POSITIVE_INFINITY,
id: 'abc123',
show: true,
});
await Promise.resolve();
const [listed] = setsOf(engine, 'flights-sprites');
const set = engine.fake.moverSet(listed.id);
assert.equal(set.options.depthTest, 'always');
assert.deepEqual(set.options.sprite.scaleByDistance, [1000, 3, 8000000, 0.5]);
assert.equal(set.options.sprite.sizePx, 20);
assert.equal(set.options.order, 8);
const { store } = set;
assert.equal(store.count, 1);
assert.equal(store.positions[0], position.x);
assert.equal(store.pickIds[0], 'abc123');
assert.equal(store.cellKeys[store.cells[0]], 'helicopter@64');
assert.ok(Math.abs(store.scales[0] - 1.5) < 1e-6);
assert.ok(Math.abs(store.rotations[0] - 0.3) < 1e-6);
assert.ok(Math.abs(store.colors[1] - 184 / 255) < 1e-6);
// Writes inside a frame listener commit at its end, once.
const before = set.commits;
const remove = renderer.view.on('preRender', () => {
sprite.color = WHITE.withAlpha(0.45);
sprite.scale = 2;
sprite.width = 24;
sprite.image = aircraftIcon('helicopter', TRACKED_ICON_PX);
sprite.show = false;
});
engine.fake.frame();
remove();
assert.equal(engine.fake.moverSet(listed.id).commits, before + 1);
assert.ok(Math.abs(store.colors[3] - 0.45) < 1e-6);
assert.ok(Math.abs(store.scales[0] - 2.4) < 1e-6, 'scale × width/20');
assert.equal(store.cellKeys[store.cells[0]], `helicopter@${TRACKED_ICON_PX}`);
assert.equal(store.shows[0], 0);
assert.equal(sprite.color.alpha, 0.45);
// Removal moves the last sprite into the hole.
const other = renderer.fleet.add({
position,
image: aircraftIcon('light'),
id: 'def456',
scaleByDistance: nearFarScale(1000, 3.0, 8000000, 0.5),
disableDepthTestDistance: Number.POSITIVE_INFINITY,
});
renderer.fleet.remove(sprite);
await Promise.resolve();
assert.equal(store.count, 1);
assert.equal(store.pickIds[0], 'def456');
assert.equal(other.gevIndex, 0);
renderer.fleet.setVisible(false);
await Promise.resolve();
assert.equal(store.count, 0);
assert.equal(renderer.fleet.visible(), false);
renderer.destroy();
assert.equal(setsOf(engine, 'flights').length, 0, 'no set left behind');
engine.destroy();
});
test('the bank grows past its capacity without losing sprites', async () => {
const { engine, renderer } = await setup();
const sprites = [];
for (let index = 0; index < 1500; index++)
sprites.push(
renderer.fleet.add({
position: geo.cartesian3.fromDegrees(index * 0.01, 0, 1000),
image: aircraftIcon('airliner'),
id: `a${index}`,
scaleByDistance: nearFarScale(1000, 3.0, 8000000, 0.5),
}),
);
await Promise.resolve();
const sets = setsOf(engine, 'flights-sprites');
assert.equal(sets.length, 1);
assert.equal(sets[0].count, 1500);
assert.ok(sets[0].capacity >= 1500);
const { store } = engine.fake.moverSet(sets[0].id);
assert.equal(store.pickIds[1499], 'a1499');
renderer.destroy();
engine.destroy();
});
test('the tracked glyph reads its callbacks every frame; follow carries its readout label', async () => {
const { engine, renderer } = await setup();
let position = geo.cartesian3.fromDegrees(-97.7, 30.2, 3000);
let color = CYAN;
const handle = renderer.tracked.create({
position: () => position,
image: aircraftIcon('airliner', TRACKED_ICON_PX),
width: 28,
height: 28,
scale: 1,
color: () => color,
scaleByDistance: nearFarScale(1000, 3.0, 8000000, 0.5),
disableDepthTestDistance: Number.POSITIVE_INFINITY,
rotation: () => 0.5,
});
handle.gevTrackedId = 'flights:abc123';
handle.gevLabelModel = { title: 'DAL123' };
handle.gevDisplayPosition = () => position;
renderer.tracked.setViewFrom(handle, { x: 0, y: -2400, z: 1650 });
engine.fake.frame();
const [listed] = setsOf(engine, 'flights-tracked');
const { store } = engine.fake.moverSet(listed.id);
assert.equal(store.pickIds[0], handle.id);
assert.ok(Math.abs(store.rotations[0] - 0.5) < 1e-6);
position = geo.cartesian3.fromDegrees(-97.6, 30.2, 3000);
color = CYAN.withAlpha(0);
engine.fake.frame();
assert.equal(
store.positions[0],
position.x,
'the position callback, per frame',
);
assert.equal(store.colors[3], 0, 'the colour callback, per frame');
const stop = renderer.follow.start(handle);
assert.equal(typeof stop, 'function');
const following = engine.camera.following();
assert.equal(following, handle.gevFollowHandle);
assert.equal(following.label.id, 'flights:abc123');
assert.equal(following.label.model(), handle.gevLabelModel);
assert.equal(renderer.follow.isFollowing(handle), true);
assert.equal(renderer.follow.current(), handle);
renderer.follow.release();
assert.equal(engine.camera.following(), null);
assert.equal(renderer.follow.current(), undefined);
renderer.tracked.remove(handle);
assert.equal(renderer.tracked.isLive(handle), false);
renderer.destroy();
engine.destroy();
});
test("a model pose is the Cesium model frame, turned to Navara's glTF axes", async () => {
const { engine, renderer } = await setup();
const model = await renderer.models.loadFleet({
url: '/models/airplane.glb',
minimumPixelSize: 24,
scale: 1,
radiusM: 34.41,
color: WHITE,
blendAmount: 0.94,
unlit: false,
id: 'abc123',
});
model.show = false;
renderer.models.add(model);
const position = geo.cartesian3.fromDegrees(-97.7, 30.2, 3000);
renderer.models.setPose(model, position, 75);
// modelMatrix is Cesium's (what the trail anchors read).
const expected = Cesium.Transforms.headingPitchRollToFixedFrame(
position,
new Cesium.HeadingPitchRoll(Cesium.Math.toRadians(75 + 180), 0, 0),
Cesium.Ellipsoid.WGS84,
);
for (let index = 0; index < 16; index++)
assert.ok(Math.abs(model.modelMatrix[index] - expected[index]) < 1e-6);
// The instance orientation: modelMatrix × Cesium's Z_UP_TO_X_UP (Navara's
// loader already turned the glTF Z-up).
const corrected = Cesium.Matrix4.multiplyTransformation(
expected,
Cesium.Axis.Z_UP_TO_X_UP,
new Cesium.Matrix4(),
);
const q = Cesium.Quaternion.fromRotationMatrix(
Cesium.Matrix4.getMatrix3(corrected, new Cesium.Matrix3()),
);
await Promise.resolve();
const [listed] = setsOf(engine, 'flights-models');
const set = engine.fake.moverSet(listed.id);
assert.equal(set.options.model.minimumPixelSize, 24);
assert.deepEqual(set.options.model.colorBlend, { mode: 'mix', amount: 0.94 });
const { store } = set;
for (const [k, value] of ['x', 'y', 'z', 'w'].entries())
assert.ok(
Math.abs(store.orientations[k] - q[value]) < 1e-9,
`${value}: ${[...store.orientations.slice(0, 4)]} vs ${[q.x, q.y, q.z, q.w]}`,
);
assert.equal(store.shows[0], 0, 'admitted hidden');
model.show = true;
await Promise.resolve();
assert.equal(store.shows[0], 1);
assert.equal(model.ready, true, 'the fake engine draws at once');
assert.ok(model.computedScale >= model.scale);
renderer.destroy();
engine.destroy();
});
test('trails are engine lines in the trail pick namespace; the head follows each frame', async () => {
const { engine, renderer } = await setup();
const trail = renderer.trails.create({ color: '#00d4ff', width: 2.5 });
const a = geo.cartesian3.fromDegrees(0, 0, 1000);
const b = geo.cartesian3.fromDegrees(0.1, 0, 1000);
trail.setPositions([a, a, b]);
const lines = engine.lines.list();
assert.equal(lines.length, 1);
const line = engine.fake.line(lines[0].id);
assert.match(lines[0].id, /^gev-trail:/);
assert.equal(line.options.pickId, lines[0].id);
assert.ok(Math.abs(line.options.color[3] - 0.85) < 1e-9);
assert.equal(line.options.arc, 'geodesic');
let ends = [a, b];
const head = renderer.trails.createHead({
id: 'gev-trail:fl-head-1',
show: true,
positions: () => ends,
color: '#00d4ff',
width: 2.5,
});
engine.fake.frame();
assert.equal(engine.lines.list().length, 2);
head.show = false;
ends = [];
renderer.trails.removeHead(head);
trail.destroy();
assert.equal(engine.lines.list().length, 0);
renderer.destroy();
engine.destroy();
});
+11 -1
View File
@@ -718,8 +718,18 @@ export function createQueries({
if (!flightState._billboards.has(id)) return false;
if (parts.tracking._isExplicitTrackingOrigin(origin))
parts.tracking._cancelPendingTrackingRestore();
if (flightState._trackedIcao === id)
if (flightState._trackedIcao === id) {
// Selected but no longer followed (a camera command took the camera:
// a search flight, the startup view; on Navara any command ends a
// follow): asking to track it again follows it again.
const entity = flightState._trackedEntity;
if (entity && !flightState._renderer?.follow.isFollowing(entity)) {
flightState._trackedCameraFrameStop?.();
flightState._trackedCameraFrameStop =
flightState._renderer.follow.refocus(entity);
}
return parts.tracking._publishTrackedSelection(id, origin);
}
parts.tracking._trackFlight(id, { origin });
return true;
},
+22
View File
@@ -0,0 +1,22 @@
// The civil flights layer's renderer on the Navara engine: the shared
// aircraft renderer (src/layers/aircraft/render/navara.js) with the flights'
// sprite slot (drawn over the other contact layers, as data/spriteOrder
// orders them on Cesium) and airplane.glb's nose offset. Registered for
// 'navara' by the Navara mode after its import() (src/app/navaraMode.js).
import { createAircraftNavaraRenderer } from '../../aircraft/render/navara.js';
import { SPRITE_LAYER_ORDER } from '../../../data/spriteOrder.js';
import { MODEL_HEADING_OFFSET_DEG } from '../policy.js';
/**
* Registry factory (src/app/layers/renderers.js): `(engine, options)`.
* @param {import('../../../globe/contract.js').GlobeEngine} engine
*/
export function createFlightsNavaraRenderer(engine) {
return createAircraftNavaraRenderer({
engine,
layerId: 'flights',
order: SPRITE_LAYER_ORDER.indexOf('flights'),
headingOffsetDeg: MODEL_HEADING_OFFSET_DEG,
});
}
+82
View File
@@ -247,3 +247,85 @@ test('the flights logic draws, selects and follows only through its renderer', a
}
assert.ok(renderer.calls.some(([name]) => name === 'destroy'));
});
test('the flights logic draws and follows on the Navara renderer (fake engine)', async () => {
const { createAircraftNavaraRenderer, aircraftAtlasLayout } =
await import('../aircraft/render/navara.js');
const layout = aircraftAtlasLayout();
const engine = createFakeEngine();
let renderer = null;
const layer = createApplicationFlights({
surface: defaultSurface,
source: {
label: 'Fixture aircraft',
getSnapshot: async () => snapshotOf(['abc123', 'def456']),
},
militaryRegistry,
renderer: {
create: ({ engine: given }) =>
(renderer = createAircraftNavaraRenderer({
engine: given,
layerId: 'flights',
order: 8,
headingOffsetDeg: 180,
drawAtlas: async () => ({
canvas: { width: layout.width, height: layout.height },
cells: layout.cells,
}),
})),
},
});
layer.init(null, { engine });
layer.enable();
try {
await renderer.whenReady();
await layer.update(null);
await Promise.resolve();
const sprites = engine.movers
.list()
.filter(({ id }) => id.startsWith('flights-sprites'));
assert.equal(sprites.length, 1);
assert.equal(sprites[0].count, 2);
const { store } = engine.fake.moverSet(sprites[0].id);
assert.deepEqual([...store.pickIds.slice(0, 2)].sort(), [
'abc123',
'def456',
]);
// A click on a contact selects it; the engine follows with its label.
engine.fake.script('pick', () => ({ id: 'def456' }));
engine.fake.script('drillPick', () => [{ id: 'def456' }]);
engine.fake.input('down', { position: { x: 10, y: 10 } });
engine.fake.input('up', { position: { x: 10, y: 10 } });
engine.fake.input('click', { position: { x: 10, y: 10 } });
assert.equal(layer.getTrackedInfo()?.icao24, 'def456');
const following = engine.camera.following();
assert.equal(following?.label?.id, 'flights:def456');
engine.fake.frame();
const tracked = engine.movers
.list()
.find(({ id }) => id.startsWith('flights-tracked'));
assert.equal(tracked.count, 1);
assert.equal(layer.getAllPositions().length, 2);
// A camera command ends the engine's follow; the contact stays selected
// (as on Cesium after a search flight), and tracking it again follows.
engine.camera.setView({
destination: geo.cartesian3.fromDegrees(-97.7, 30.2, 50_000),
});
assert.equal(engine.camera.following(), null);
assert.equal(layer.getTrackedInfo()?.icao24, 'def456');
assert.equal(layer.trackById('def456', { origin: 'voice' }), true);
assert.equal(engine.camera.following()?.label?.id, 'flights:def456');
assert.equal(layer.stopTracking(), true);
assert.equal(engine.camera.following(), null);
assert.equal(
engine.movers.list().find(({ id }) => id.startsWith('flights-tracked'))
.count,
0,
);
} finally {
layer.disable();
layer.destroy(null);
}
assert.equal(engine.movers.list().length, 0, 'no mover set left behind');
engine.destroy();
});
+4
View File
@@ -467,6 +467,9 @@ export function createRendering({
url: resolveAsset(spec.url),
minimumPixelSize: MODEL_MIN_PX,
scale: spec.scale,
// Bounding radius per scale unit (an engine that keeps
// minimumPixelSize on the CPU reports the drawn scale with it).
radiusM: spec.nativeRadiusM,
color: flightState._irBoost ? WHITE : _modelColor(icao24),
// Launch presentation keeps the code-side tint dominant for every approved
// model; IR boost removes the remaining diffuse hint with flat UNLIT white.
@@ -635,6 +638,7 @@ export function createRendering({
url: resolveAsset(trackedSpec.url),
minimumPixelSize: TRACKED_MODEL_MIN_PX,
scale: trackedSpec.scale,
radiusM: trackedSpec.nativeRadiusM,
color: flightState._irBoost ? WHITE : CYAN,
// The tracked aircraft uses the same dominant light tint as the fleet;
// IR boost removes the remaining diffuse hint with flat UNLIT white.