Draw civil flights from an icon set moved on the GPU

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.
This commit is contained in:
Sameh Khamis committed 2026-11-03 19:40:39 -08:00
1 parent 5f36eb0e0d
commit 2c4c66c710
33 files changed
+1598 -986

No files matched your search

+12
View File
@@ -1315,6 +1315,12 @@
"src/layers/flights/policy.js",
"src/layers/flights/queries.js",
"src/layers/flights/rendering.js",
"src/layers/flights/icons.js",
"src/data/iconSetPrimitive.js",
"src/data/iconSet.js",
"src/data/iconSegment.js",
"src/data/iconAtlas.js",
"src/data/aircraftRecession.js",
"src/data/modelMirror.js",
"src/data/billboardMirror.js",
"src/layers/aircraft/visualState.js",
@@ -2249,6 +2255,7 @@
"./standalone/catalog"
],
"modules": [
"src/layers/flights/icons.js",
"src/sources/adsbFields.js",
"src/data/headingDegrees.js",
"src/data/distanceFormat.js",
@@ -3248,6 +3255,11 @@
"application-layer-construction": {
"exports": ["./application/layers"],
"modules": [
"src/layers/flights/icons.js",
"src/data/iconSetPrimitive.js",
"src/data/iconSet.js",
"src/data/iconSegment.js",
"src/data/iconAtlas.js",
"src/sources/adsbFields.js",
"src/data/headingDegrees.js",
"src/data/distanceFormat.js",
+2
View File
@@ -252,6 +252,8 @@ const _b64 = (s) =>
* NEAR the display size lets the browser's SVG AA do the work: crisp AND
* smooth. 64 covers the 40–58 px fleet band with ≤1.6× minification. */
const FLEET_RASTER_PX = 64;
/** Raster size of the default fleet glyph. */
export const FLEET_ICON_PX = FLEET_RASTER_PX;
/** Tracked raster: the tracked billboard is the one SUSTAINED large 2D glyph
* (close-zoom fleet flybys hand off to 3D models). Keep the 192 px texture so
* it stays crisp at its biggest on-screen sizes. */
+34
View File
@@ -243,6 +243,40 @@ export function applyAircraftBillboardTreatment({
return _treatmentResult;
}
/**
* The alpha an aircraft draws with: base × max(combined floor, focus × limb
* haze). Icon sets compose the same thing in their shader; models need it here.
* @param {object} input
* @param {number} input.baseAlpha
* @param {number} input.focusFactor
* @param {number} input.cameraDistanceM
* @param {number} input.cameraHeightM
* @param {Partial<typeof DEFAULT_AIRCRAFT_RECESSION_PARAMS>} [input.params]
* @returns {number}
*/
export function aircraftTreatmentAlpha({
baseAlpha,
focusFactor,
cameraDistanceM,
cameraHeightM,
params,
}) {
const tuning = resolvedParams(params);
const factors = aircraftRecessionFactorsResolved(
cameraDistanceM,
cameraHeightM,
tuning,
_scratchFactors,
);
return (
baseAlpha *
Math.max(
tuning.combinedAlphaFloor,
clamp(focusFactor, 0, 1) * factors.alpha,
)
);
}
/**
* Apply layer-owned hue and composed aircraft alpha to an ambient glTF model without
* disturbing Cesium's existing MIX/colorBlendAmount presentation semantics.
+6 -6
View File
@@ -83,24 +83,24 @@ for (const layer of LAYERS) {
test(`${layer.name}: near AIR state is independent from model admission`, () => {
assert.match(source, /nextCockpitNearContacts\(/,
'Cockpit derives a separate near-contact hysteresis set');
assert.match(source, /isCockpitContact && !isCockpitNear[\s\S]*cockpitContactDotImage\(\)/,
assert.match(source, /isCockpitContact && !isCockpitNear[\s\S]*cockpitContactDotImage\(\)|!flightState\._cockpitNearContacts\.has\(icao24\);\s*if \(cockpitDot\) \{\s*const dot = iconAtlas\.slot\(cockpitContactDotImage\(\)/,
'only out-of-range Cockpit contacts become dots');
// `_iconKind` is identity for every unconverted contact (see
// tr3bRegistry.test.mjs) — it only swaps the glyph for a contact the
// operator explicitly converted into a TR-3B.
assert.match(source, /bb\.image\s*=\s*aircraftIcon\(\s*_iconKind\(\s*icao24,\s*meta\?\.klass,?\s*\)(,\s*aircraftVisuals\.read\(bb\)\?\.iconLarge\s*\?\s*TRACKED_ICON_PX\s*:\s*undefined)?,?\s*\)/,
assert.match(source, /bb\.image\s*=\s*aircraftIcon\(\s*_iconKind\(\s*icao24,\s*meta\?\.klass,?\s*\)(,\s*aircraftVisuals\.read\(bb\)\?\.iconLarge\s*\?\s*TRACKED_ICON_PX\s*:\s*undefined)?,?\s*\)|aircraftIcon\(\s*parts\.rendering\._iconKind\(icao24, info\?\.klass\),?\s*\)/,
'near contacts and model fallbacks retain the class-derived aircraft silhouette');
assert.match(source, /setBillboard\(\s*bb,\s*'rotation',\s*0\s*\);/,
assert.match(source, /setBillboard\(\s*bb,\s*'rotation',\s*0\s*\);|set\.setFlag\(row, ICON_ROTATES, false\);/,
'far dots are reset to a rotation-free presentation');
assert.match(source, /\(\s*!(?:flightState\.)?_cockpitContactMode\s*\|\|\s*isCockpitNear,?\s*\)\s*&&\s*\(\s*doRotations\s*\|\|\s*revealed,?\s*\)/,
assert.match(source, /\(\s*!(?:flightState\.)?_cockpitContactMode\s*\|\|\s*isCockpitNear,?\s*\)\s*&&\s*\(\s*doRotations\s*\|\|\s*revealed,?\s*\)|set\.setFlag\(row, ICON_ROTATES, true\);/,
'near 2D silhouettes continue to receive projected course');
assert.match(source, /if\s*\(\s*billboardState\(bb\)\.show,?\s*\)\s*setBillboard\(\s*bb,\s*'show',\s*false\s*\);\s*\/\/\s*hand\s*off\s*ONLY\s*once\s*the\s*model\s*renders/,
assert.match(source, /if\s*\(\s*billboardState\(bb\)\.show,?\s*\)\s*setBillboard\(\s*bb,\s*'show',\s*false\s*\);\s*\/\/\s*hand\s*off\s*ONLY\s*once\s*the\s*model\s*renders|icons\.setShown\(row, false\);\s*\/\/\s*hand\s*off\s*ONLY\s*once\s*the\s*model\s*renders/,
'the gap-proof billboard-to-model handoff remains intact');
});
test(`${layer.name}: Cockpit exit clears near state before restoring map presentation`, () => {
const setMode = /^([ \t]*)function _setCockpitContactMode\b[\s\S]*?\n\1\}/m.exec(source)?.[0];
assert.match(setMode, /else\s*(?:flightState\.)?_cockpitNearContacts\s*=\s*new\s*Set\(\s*,?\s*\);/);
assert.match(setMode, /for\s*\(\s*const\s*\[icao24,\s*bb\]\s*of\s*(?:flightState\.)?_billboards,?\s*\)\s*(?:parts\.\w+\.)?_applyFleetBillboardPresentation\(\s*icao24,\s*bb,?\s*\);/);
assert.match(setMode, /for\s*\(\s*const\s*\[icao24,\s*bb\]\s*of\s*(?:flightState\.)?_billboards,?\s*\)\s*(?:parts\.\w+\.)?_applyFleetBillboardPresentation\(\s*icao24,\s*bb,?\s*\);|for \(let row = 0; row < \(icons\?\.count \?\? 0\); row\+\+\)\s*parts\.icons\._applyIconPresentation\(icons\.idAt\(row\)\);/);
});
}
+4
View File
@@ -40,5 +40,9 @@ export const _driveFleetModelHandoffForTest =
flightsLayer.testing._driveFleetModelHandoffForTest;
export const _ensureFleetModelForTest =
flightsLayer.testing._ensureFleetModelForTest;
export const _iconForTest = flightsLayer.testing._iconForTest;
export const _setIconForTest = flightsLayer.testing._setIconForTest;
export const _floorGroundedIconsForTest =
flightsLayer.testing._floorGroundedIconsForTest;
export const mapAnalystRecord = flightsLayer.mapAnalystRecord;
export default flightsLayer;
+101 -28
View File
@@ -14,6 +14,8 @@ import flightsLayer, {
_setTrackedFlightRefreshStateForTest,
_floorGroundedDisplayPositionForTest,
_clearDisplayFloorStateForTest,
_iconForTest,
_floorGroundedIconsForTest,
mapAnalystRecord,
} from './flights.js';
import { setMilitaryLayerActive } from './militaryRegistry.js';
@@ -225,18 +227,9 @@ test('nonempty OpenSky payload with zero usable rows cannot prove share target a
test('a newly seen aircraft appears at its delayed position, not ahead of it', async () => {
// The fleet renders RENDER_DELAY_SEC behind real time, so a fresh contact's
// first displayed position is its fix projected back to that delayed time.
// Creating the billboard at the raw fix drew it ahead until the next fleet
// Creating the icon at the raw fix drew it ahead until the next fleet
// tick, which then jumped it back about one delay of travel.
const added = [];
const billboardCollection = {
show: false,
add(options) {
const billboard = { ...options };
added.push(billboard);
return billboard;
},
remove() {},
};
const billboardCollection = { show: false };
const viewer = { camera: { positionCartographic: null }, scene: {} };
_setTrackedFlightRefreshStateForTest({
icao24: 'seed00',
@@ -293,9 +286,9 @@ test('a newly seen aircraft appears at its delayed position, not ahead of it', a
};
try {
await flightsLayer.update(viewer);
const billboard = added.find((entry) => entry.id === icao24);
assert.ok(billboard, 'the new contact gets a billboard');
const shown = Cesium.Cartographic.fromCartesian(billboard.position);
const icon = _iconForTest(icao24);
assert.ok(icon, 'the new contact gets an icon');
const shown = Cesium.Cartographic.fromCartesian(icon.position);
const fix = Cesium.Cartesian3.fromDegrees(fixLon, fixLat, 10_700);
const behindM = Cesium.Cartesian3.distance(
Cesium.Cartesian3.fromRadians(shown.longitude, shown.latitude, 10_700),
@@ -1066,21 +1059,12 @@ const _floorCarto = (pos) =>
test('a newly seen taxiing aircraft starts on the floor under its delayed position', async () => {
// A grounded contact is drawn at its delayed position lifted onto the floor
// under it. Creating the billboard without that lift started it on the
// under it. Creating the icon without that lift started it on the
// floor of its reported fix, then the next fleet tick raised it.
_clearDisplayFloorStateForTest();
_clearMeshFloorCellsForTest();
setMeshFloorPreferred(true);
const added = [];
const billboardCollection = {
show: false,
add(options) {
const billboard = { ...options };
added.push(billboard);
return billboard;
},
remove() {},
};
const billboardCollection = { show: false };
const viewer = { camera: { positionCartographic: null }, scene: {} };
_setTrackedFlightRefreshStateForTest({
icao24: 'seed00',
@@ -1142,9 +1126,9 @@ test('a newly seen taxiing aircraft starts on the floor under its delayed positi
};
try {
await flightsLayer.update(viewer);
const billboard = added.find((entry) => entry.id === icao24);
assert.ok(billboard, 'the new contact gets a billboard');
const shown = _floorCarto(billboard.position);
const icon = _iconForTest(icao24);
assert.ok(icon, 'the new contact gets an icon');
const shown = _floorCarto(icon.position);
assert.ok(
Cesium.Math.toDegrees(shown.longitude) < fixLon,
'drawn behind the fix',
@@ -1160,6 +1144,95 @@ test('a newly seen taxiing aircraft starts on the floor under its delayed positi
}
});
test('far grounded contacts take turns on the floor; near ones refloor every tick', async () => {
// The floor pass is the fleet tick's only per-contact CPU work. Past
// FLOOR_NEAR_M a contact is refloored once per FLOOR_FAR_EVERY_TICKS.
_clearDisplayFloorStateForTest();
_clearMeshFloorCellsForTest();
setMeshFloorPreferred(true);
const viewer = { camera: { positionCartographic: null }, scene: {} };
_setTrackedFlightRefreshStateForTest({
icao24: 'seed00',
entity: null,
billboard: {
show: false,
position: Cesium.Cartesian3.fromDegrees(0, 0, 0),
},
billboardCollection: { show: false },
viewer,
meta: { rawLat: 0, rawLon: 0, onGround: false },
tracked: false,
});
const icao24 = 'taxi02';
const fixLon = -97.66;
const fixLat = 30.2;
for (let k = -40; k <= 40; k += 1) {
reportMeshFloorCell(fixLat, fixLon + k * 0.0001, 131.5);
}
const nowSec = Math.floor(Date.now() / 1000);
const realFetch = globalThis.fetch;
globalThis.fetch = async (url) => {
if (!String(url).startsWith('/api/flights')) {
return { ok: true, status: 200, json: async () => ({ ac: [] }) };
}
return {
ok: true,
status: 200,
headers: { get: () => null },
json: async () => ({
time: nowSec,
states: [
[
icao24,
'TAXI02 ',
'United States',
nowSec - 2,
nowSec - 2,
fixLon,
fixLat,
null,
true,
10,
90,
0,
null,
null,
null,
null,
null,
5,
],
],
}),
};
};
try {
await flightsLayer.update(viewer);
const start = Cesium.JulianDate.now();
const at = (s) =>
Cesium.JulianDate.addSeconds(start, s, new Cesium.JulianDate());
const far = Cesium.Cartesian3.fromDegrees(fixLon, fixLat, 3_000_000);
const near = Cesium.Cartesian3.fromDegrees(fixLon, fixLat, 10_000);
let last = _iconForTest(icao24).position;
const moves = (camera, from, to) => {
let count = 0;
for (let tick = from; tick <= to; tick += 1) {
_floorGroundedIconsForTest(at(tick), camera);
const now = _iconForTest(icao24).position;
if (Cesium.Cartesian3.distance(now, last) > 1) count += 1;
last = now;
}
return count;
};
assert.equal(moves(far, 1, 24), 2, 'far: once per 12 ticks');
assert.equal(moves(near, 25, 30), 6, 'near: every tick');
} finally {
globalThis.fetch = realFetch;
_clearMeshFloorCellsForTest();
_clearDisplayFloorStateForTest();
}
});
test('display floor: a grounded contact that drifted onto a higher cell is lifted', () => {
_clearDisplayFloorStateForTest();
_clearMeshFloorCellsForTest();
+18 -2
View File
@@ -45,7 +45,7 @@ const COURSE0 = 19; // deg
const COURSE_SLOPE = 20; // lerp: deg per unit fraction
const SLEW_OFFSET = 21; // deg still to glide at SLEW_T0
const SLEW_T0 = 22; // s
const SLEW_RATE = 23; // deg/s
const SLEW_RATE = 23; // deg/s; negative holds the course at the glide's start
const DEG = Math.PI / 180;
const STRAIGHT_BELOW_RAD_PER_SEC = 1e-4;
@@ -140,6 +140,8 @@ export function writeArcSegment(out, row, segment) {
/**
* Glide the displayed course from `fromCourseDeg` onto the segment's own course,
* at most `rateDps`, starting at `t0` (the rate limit at a segment boundary).
* A rate of zero or less holds `fromCourseDeg` for the whole segment instead
* (a hover: the nose keeps its heading however the track moves).
* Call after writing the segment.
* @param {Float32Array} out
* @param {number} row
@@ -153,7 +155,7 @@ export function writeCourseGlide(out, row, fromCourseDeg, t0, rateDps) {
const target = targetCourse(out, o, t0);
out[o + SLEW_OFFSET] = norm180(fromCourseDeg - target);
out[o + SLEW_T0] = t0;
out[o + SLEW_RATE] = Math.max(0, rateDps);
out[o + SLEW_RATE] = rateDps > 0 ? rateDps : -1;
}
/**
@@ -195,6 +197,8 @@ export function segmentCourse(seg, row, tau) {
const o = row * SEGMENT_STRIDE;
const course = targetCourse(seg, o, tau);
const offset = seg[o + SLEW_OFFSET];
if (seg[o + SLEW_RATE] < 0)
return norm360(targetCourse(seg, o, seg[o + SLEW_T0]) + offset);
if (offset === 0) return norm360(course);
// limitCourseStep: the displayed course turns at the rate toward the segment's
// own (possibly turning) course until it reaches it, then follows it. The
@@ -207,6 +211,17 @@ export function segmentCourse(seg, row, tau) {
return norm360(turned < gap ? start + offset + toward * turned : course);
}
/**
* The course the row glides toward at `tau` (its segment's own course).
* @param {Float32Array} seg
* @param {number} row
* @param {number} tau
* @returns {number}
*/
export function segmentTargetCourse(seg, row, tau) {
return norm360(targetCourse(seg, row * SEGMENT_STRIDE, tau));
}
/** The segment's own course at `tau`, before any glide. */
function targetCourse(seg, o, tau) {
const kind = seg[o + KIND];
@@ -310,6 +325,7 @@ float iconTargetCourse(vec4 seg3, vec4 seg4, vec4 seg5, float tau) {
float iconSegmentCourse(vec4 seg3, vec4 seg4, vec4 seg5, float tau) {
float course = iconTargetCourse(seg3, seg4, seg5, tau);
float offset = seg5.y;
if (seg5.w < 0.0) return iconTargetCourse(seg3, seg4, seg5, seg5.z) + offset;
if (offset == 0.0) return course;
float toward = -sign(offset);
float start = iconTargetCourse(seg3, seg4, seg5, seg5.z);
+32
View File
@@ -213,3 +213,35 @@ test('the course glide never outruns the rate while its target turns', () => {
'then it follows the turn',
);
});
test('a zero-rate glide holds the course however the segment turns', () => {
const seg = new Float32Array(SEGMENT_STRIDE);
const to = Cesium.Cartesian3.fromDegrees(-97.04, 32.90001, 10_500);
for (const from of [0, 30]) {
writeLerpSegment(seg, 0, {
from: fix,
to,
t0: 0,
t1: 30,
course0Deg: 0,
courseSlopeDeg: 90,
});
writeCourseGlide(seg, 0, from, 0, 0);
for (const tau of [0, 10, 20, 30, 60])
assert.ok(
courseGap(segmentCourse(seg, 0, tau), from) < 1e-4,
`from ${from}° at τ=${tau}`,
);
}
writeArcSegment(seg, 0, {
fix,
t0: 0,
minDt: 0,
maxDt: 300,
speedMps: 1,
trackDeg: 90,
turnRateDps: 3,
});
writeCourseGlide(seg, 0, 90, 0, 0);
assert.ok(courseGap(segmentCourse(seg, 0, 20), 90) < 1e-4, 'arc turn held');
});
+25 -1
View File
@@ -43,8 +43,14 @@ export const ICON_SHOWN = 1;
export const ICON_ROTATES = 2;
/** Fade and shrink toward the globe's limb (aircraft recession). */
export const ICON_RECEDES = 4;
/** Lift the row 12 m along the normal before the horizon test (fixes near the ground). */
/**
* Judge the row at no lower than ICON_CULL_HEIGHT_M above the ellipsoid in the
* horizon test. A fix near or below the ellipsoid (a grounded contact at geoid
* −31 m before its floor warms) otherwise reads as beyond the limb.
*/
export const ICON_CULL_LIFT = 8;
/** The ellipsoid height a CULL_LIFT row is raised to for the horizon test. */
export const ICON_CULL_HEIGHT_M = 12;
/** Move the epoch forward once render times pass this many seconds. */
const REBASE_AFTER_SEC = 1800;
@@ -422,6 +428,24 @@ export function createIconSet({ capacity = 1024, epoch } = {}) {
course(row, tau) {
return segmentCourse(segments, row, tau);
},
/** The point the horizon test judges for a row drawn at `position`. */
cullPoint(row, position, result = new Cesium.Cartesian3()) {
if (!(style[row * STYLE_STRIDE + FLAGS] & ICON_CULL_LIFT))
return Cesium.Cartesian3.clone(position, result);
// Height above the ellipsoid along the geocentric ray, as the shader
// computes it: exact to millimetres this close to the surface.
const radius = Cesium.Cartesian3.magnitude(position);
Cesium.Cartesian3.multiplyComponents(
position,
Cesium.Ellipsoid.WGS84.oneOverRadii,
result,
);
const height = radius * (1 - 1 / Cesium.Cartesian3.magnitude(result));
const lift = Math.max(0, ICON_CULL_HEIGHT_M - height);
Cesium.Cartesian3.normalize(position, result);
Cesium.Cartesian3.multiplyByScalar(result, lift, result);
return Cesium.Cartesian3.add(position, result, result);
},
};
function markRow(row) {
+24
View File
@@ -4,6 +4,8 @@ import * as Cesium from 'cesium';
import {
createIconSet,
DirtyRows,
ICON_CULL_HEIGHT_M,
ICON_CULL_LIFT,
ICON_ROTATES,
ICON_SHOWN,
} from './iconSet.js';
@@ -225,3 +227,25 @@ test('rebasing moves the epoch without moving any row', () => {
assert.equal(set.expires(row), 1100);
assert.deepEqual(set.dirty.segments.runs(set.count), [[0, 0]]);
});
test('a cull-lift row is judged at no lower than the cull height above the ellipsoid', () => {
const set = createIconSet();
const row = set.add('a');
const height = (p) => Cesium.Cartographic.fromCartesian(p).height;
const at = (h) => Cesium.Cartesian3.fromDegrees(-84.43, 33.64, h);
assert.ok(
Math.abs(height(set.cullPoint(row, at(-31))) - -31) < 1e-6,
'unflagged: as drawn',
);
set.setFlag(row, ICON_CULL_LIFT, true);
for (const h of [-31, 0, 5]) {
assert.ok(
Math.abs(height(set.cullPoint(row, at(h))) - ICON_CULL_HEIGHT_M) < 0.01,
`${h} m is judged at ${ICON_CULL_HEIGHT_M} m`,
);
}
assert.ok(
Math.abs(height(set.cullPoint(row, at(131))) - 131) < 1e-6,
'a contact floored higher keeps its own height',
);
});
+9 -4
View File
@@ -16,7 +16,7 @@
*/
import * as Cesium from 'cesium';
import { ICON_SEGMENT_GLSL, SEGMENT_STRIDE } from './iconSegment.js';
import { ICON_CULL_LIFT, STYLE_STRIDE } from './iconSet.js';
import { ICON_CULL_HEIGHT_M, ICON_CULL_LIFT, STYLE_STRIDE } from './iconSet.js';
import { iconAtlas, MAX_ICON_SLOTS } from './iconAtlas.js';
import {
cameraLimbDistanceM,
@@ -25,8 +25,6 @@ import {
/** On-screen device pixels above which a row draws its near (large) image. */
const NEAR_IMAGE_PX = 76;
/** Rows lifted this far before the horizon test when flagged (low fixes). */
const CULL_LIFT_M = 12;
const ATTRIBUTE_LOCATIONS = Object.freeze({
corner: 0,
@@ -80,6 +78,13 @@ bool iconFlag(float flags, float bit) {
return mod(floor(flags / bit), 2.0) == 1.0;
}
// Raise a near-ground point to at least the cull height (iconSet.cullPoint).
vec3 iconCullPoint(vec3 world) {
float radius = length(world);
float height = radius * (1.0 - 1.0 / length(world * czm_ellipsoidInverseRadii));
return world + world / radius * max(0.0, ${ICON_CULL_HEIGHT_M.toFixed(1)} - height);
}
bool occludedByEllipsoid(vec3 world) {
vec3 cv = u_horizon.xyz;
float vh2 = u_horizon.w;
@@ -101,7 +106,7 @@ void main() {
vec3 rte = iconSegmentPosition(seg0, seg1, seg2, seg3, seg4, u_tau);
vec3 world = u_cameraPosition + rte;
vec3 cullPoint = iconFlag(flags, ${ICON_CULL_LIFT.toFixed(1)}) ? world + normalize(world) * ${CULL_LIFT_M.toFixed(1)} : world;
vec3 cullPoint = iconFlag(flags, ${ICON_CULL_LIFT.toFixed(1)}) ? iconCullPoint(world) : world;
if (occludedByEllipsoid(cullPoint)) return;
vec4 positionEC = czm_modelViewRelativeToEye * vec4(rte, 1.0);
+1 -1
View File
@@ -1660,7 +1660,7 @@ test('hasContact declines while a layer is disabled, whatever its maps still hol
[
'flights',
readLayerSource(new URL('./flights.js', import.meta.url)),
/hasContact\(\s*icao24,?\s*\)\s*\{\s*\n\s*if\s*\(\s*!(?:flightState\.)?_billboardCollection\s*\|\|\s*!(?:flightState\.)?_billboardCollection\.show\s*\|\|\s*(?:flightState\.)?_billboards\.size\s*===\s*0,?\s*\)\s*return\s*null;/,
/hasContact\(\s*icao24,?\s*\)\s*\{\s*\n\s*const icons = flightState\._icons;\s*\n\s*if\s*\(\s*!icons\s*\|\|\s*!icons\.show\s*\|\|\s*icons\.count\s*===\s*0,?\s*\)\s*return\s*null;/,
],
[
'militaryFlights',
+10 -4
View File
@@ -10,7 +10,10 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import flightsLayer, { _setTrackedFlightRefreshStateForTest } from './flights.js';
import flightsLayer, {
_iconForTest,
_setTrackedFlightRefreshStateForTest,
} from './flights.js';
import militaryFlightsLayer, { _setTrackedMilitaryRefreshStateForTest } from './militaryFlights.js';
const ICAO = 'abc123';
@@ -39,6 +42,8 @@ const LAYERS = [
name: 'commercial',
layer: flightsLayer,
visualCenterM: Cesium.Cartesian3.ZERO,
// Where the icon is drawn: the seeded position as the GPU encodes it.
spritePosition: () => _iconForTest(ICAO).position,
seed({ billboardShow, models = [] }) {
_setTrackedFlightRefreshStateForTest({
icao24: ICAO,
@@ -61,6 +66,7 @@ const LAYERS = [
name: 'military',
layer: militaryFlightsLayer,
visualCenterM: Cesium.Cartesian3.ZERO,
spritePosition: () => contactBillboard(true).position,
seed({ billboardShow, models = [] }) {
_setTrackedMilitaryRefreshStateForTest({
icao24: ICAO,
@@ -149,8 +155,8 @@ for (const fixture of LAYERS) {
const spritePosition = detectionPosition(fixture.layer);
assert.ok(spritePosition, 'the 2D sprite publishes a detection candidate');
assert.ok(
Cesium.Cartesian3.equalsEpsilon(spritePosition, contactBillboard(true).position, 0, 1e-6),
'the 2D bracket stays welded to the billboard position',
Cesium.Cartesian3.equalsEpsilon(spritePosition, fixture.spritePosition(), 0, 1e-6),
'the 2D bracket stays welded to the drawn sprite position',
);
const modelMatrix = Cesium.Matrix4.fromRotationTranslation(MODEL_ROTATION, MODEL_POSITION);
@@ -195,7 +201,7 @@ for (const fixture of LAYERS) {
const loadingPosition = detectionPosition(fixture.layer);
assert.ok(loadingPosition, 'the billboard remains detectable while its model loads');
assert.ok(
Cesium.Cartesian3.equalsEpsilon(loadingPosition, contactBillboard(true).position, 0, 1e-6),
Cesium.Cartesian3.equalsEpsilon(loadingPosition, fixture.spritePosition(), 0, 1e-6),
'a shown-but-unready zero-scale model cannot steal the detection anchor',
);
});
+9 -17
View File
@@ -3,7 +3,6 @@ import test from 'node:test';
import * as Cesium from 'cesium';
import { routePlausible } from './routePlausible.js';
import { createTracking } from '../layers/flights/tracking.js';
import { addBillboard } from './billboardMirror.js';
const SFO = { lat: 37.6188, lon: -122.3754, code: 'SFO' };
const LAX = { lat: 33.9416, lon: -118.4085, code: 'LAX' };
@@ -116,15 +115,14 @@ test('unjudgeable endpoint geometry and missing coordinates keep the loose contr
assert.equal(plausible(beyond, null, null), true);
});
test('the actual native tracked label hides the wrong leg and keeps a healthy route', (t) => {
const collection = new Cesium.BillboardCollection();
t.after(() => collection.destroy());
test('the actual native tracked label hides the wrong leg and keeps a healthy route', () => {
const flightState = {
records: { data: new Map(), missingPolls: new Map() },
feed: { _backoff: false },
_billboards: new Map(),
_scratchCarto: new Cesium.Cartographic(),
};
// Where each contact's icon is drawn.
const drawn = new Map();
// Only application host services are supplied. Route admission, WGS84
// billboard conversion and tracked-label formatting are the actual owners.
const services = Object.fromEntries(
@@ -141,7 +139,10 @@ test('the actual native tracked label hides the wrong leg and keeps a healthy ro
].map((name) => [name, {}]),
);
services.aircraftPresentation = { isTr3b: () => false };
const parts = { queries: { _contactLabel: (_, info) => info.callsign } };
const parts = {
queries: { _contactLabel: (_, info) => info.callsign },
icons: { _iconPosition: (id) => drawn.get(id) ?? null },
};
const tracking = createTracking({ flightState, services, parts });
for (const [position, expected] of [
[beyond, false],
@@ -155,18 +156,9 @@ test('the actual native tracked label hides the wrong leg and keeps a healthy ro
verticalRate: 0,
route,
});
flightState._billboards.set(
drawn.set(
id,
addBillboard(collection, {
id,
width: 20,
height: 20,
position: Cesium.Cartesian3.fromDegrees(
position.lon,
position.lat,
10000,
),
}),
Cesium.Cartesian3.fromDegrees(position.lon, position.lat, 10000),
);
assert.equal(
tracking._trackedLabelText(id).includes('SFO → LAX'),
+3 -3
View File
@@ -233,7 +233,7 @@ test('both flight layers keep a converted contact 2D and visible (render invaria
// 1. The 3D model handoff is SUPPRESSED for a converted contact — there is
// no TR-3B GLB, so the triangle billboard stays the visual.
assert.match(source, /modelEligible\.has\(icao24\) && !isTr3b\(icao24\)/,
assert.match(source, /modelEligible\.has\(icao24\) && !isTr3b\(icao24\)|for \(const icao24 of modelEligible\) \{\s*if \(isTr3b\(icao24\)\) continue;/,
`${name}: fleet model handoff skips converted contacts`);
// The tracked regime became default-on and camera-distance driven
// (2026-08-19), so the suppression moved from a conjunct on
@@ -245,7 +245,7 @@ test('both flight layers keep a converted contact 2D and visible (render invaria
// 2. The billboard is never hidden by that suppression — it must keep
// satisfying the getNearby/getDetectableObjects visibility guards, so a
// converted contact still works in Contacts and Cockpit.
assert.match(source, /if\s*\(\s*(bb|modelled)\s*&&\s*id\s*!==\s*(?:flightState\.)?_trackedIcao,?\s*\)\s*setBillboard\(\s*\1,\s*'show',\s*true,?\s*\);/,
assert.match(source, /if\s*\(\s*(bb|modelled)\s*&&\s*id\s*!==\s*(?:flightState\.)?_trackedIcao,?\s*\)\s*setBillboard\(\s*\1,\s*'show',\s*true,?\s*\);|if\s*\(\s*row >= 0\s*&&\s*id\s*!==\s*flightState\._trackedIcao\s*\)\s*flightState\._icons\.setShown\(row, true\);/,
`${name}: converting restores the billboard the model handoff had hidden`);
// 3. Every aircraftIcon() CALL SITE routes through the kind resolver, so no
@@ -411,7 +411,7 @@ test('a converted contact never consumes a 3D model CAP SLOT', async () => {
const source = readLayerSource(new URL(`./${name}`, import.meta.url));
// Anchor on the MODEL-eligibility loop (keepDistSq), not the unrelated
// ambient-enrichment candidate loop that also builds a `cand`.
const loop = /const\s*cand\s*=\s*\[\];\s*\n\s*for\s*\(\s*const\s*\[icao,\s*bb\]\s*of\s*(?:flightState\.)?_billboards,?\s*\)[\s\S]*?cand\.push\(\s*/.exec(source)?.[0];
const loop = /const\s*cand\s*=\s*\[\];\s*\n\s*for\s*\((?:\s*const\s*\[icao,\s*bb\]\s*of\s*(?:flightState\.)?_billboards,?\s*|let row = 0; row < icons\.count; row\+\+)\)[\s\S]*?cand\.push\(\s*/.exec(source)?.[0];
assert.ok(loop, `${name}: the model-eligibility candidate loop is present`);
assert.match(loop, /keepDistSq/, `${name}: matched the model-eligibility loop`);
assert.match(loop, /if \(isTr3b\(icao\)\) continue;/,
+22 -1
View File
@@ -34,6 +34,8 @@ import flightsLayer, {
_trackedBillboardColorForTest as flightsTrackedBillboardColor,
_driveFleetModelHandoffForTest as flightsDriveFleetModel,
_ensureFleetModelForTest as flightsEnsureFleetModel,
_iconForTest as flightsIcon,
_setIconForTest as flightsSetIcon,
} from './flights.js';
import militaryFlightsLayer, {
_setTrackedMilitaryRefreshStateForTest,
@@ -186,6 +188,25 @@ function contactBillboard() {
};
}
/** A flights contact's icon row, read and written the way these tests read a
* billboard: where it is drawn and whether the layer shows it. */
function flightIconView(icao24) {
return {
get position() {
return flightsIcon(icao24)?.position;
},
set position(position) {
flightsSetIcon(icao24, { position });
},
get show() {
return flightsIcon(icao24)?.shown === true;
},
set show(shown) {
flightsSetIcon(icao24, { shown });
},
};
}
/** Records what the driver hands to the scene, so a test can assert that a
* failing loader never put a model up. */
function recordingModelCollection() {
@@ -216,7 +237,7 @@ const LAYERS = [
modelCollection,
meta: { callsign: 'TEST123 ', altitude: 10_668, klass: 'airliner', onGround },
});
return billboard;
return flightIconView(icao24);
},
},
{
+12 -10
View File
@@ -9,7 +9,6 @@ import {
ENRICH_AMBIENT_REFILL_WINDOW_MS,
ENRICH_AMBIENT_PER_SWEEP,
} from './policy.js';
import { billboardState } from '../../data/billboardMirror.js';
export function createEnrichment({
flightState,
@@ -94,9 +93,7 @@ export function createEnrichment({
});
if (klass !== meta.klass) {
meta.klass = klass;
const bb = flightState._billboards.get(icao24);
if (bb)
parts.rendering._applyFleetBillboardPresentation(icao24, bb);
parts.icons._applyIconPresentation(icao24);
// Hangar fleet: the class's GLB/scale may have changed — resync the
// live model, any in-flight load, and the tracked standalone model.
parts.rendering._syncModelToClass(icao24);
@@ -182,8 +179,7 @@ export function createEnrichment({
if (
flightState._enrichAmbientBudget <= 0 ||
!flightState._viewer ||
!flightState._billboardCollection ||
!flightState._billboardCollection.show
!flightState._icons?.show
)
return;
try {
@@ -196,13 +192,19 @@ export function createEnrichment({
camera.upWC,
);
const cand = [];
for (const [icao24, bb] of flightState._billboards) {
const icons = flightState._icons;
const tau = parts.icons._displayTau();
for (let row = 0; row < icons.count; row++) {
const icao24 = icons.idAt(row);
if (flightState._enrichSeen.has(`t:${icao24}`)) continue; // answered / queued / negative this session
if (!/^[0-9a-f]{6}$/i.test(icao24)) continue; // adsbdb keys are 6-char hex only
if (flightState.records.data.get(icao24)?.onGround) continue; // ground traffic never spends ambient budget (click-to-enrich still works)
const { position } = billboardState(bb);
if (!position || !occluder.isPointVisible(position)) continue; // beyond the limb
Cesium.Cartesian3.clone(position, flightState._scratchModelBS.center);
const position = icons.position(
row,
tau,
flightState._scratchModelBS.center,
);
if (!occluder.isPointVisible(position)) continue; // beyond the limb
if (
cull.computeVisibility(flightState._scratchModelBS) ===
Cesium.Intersect.OUTSIDE
+55 -50
View File
@@ -1,11 +1,11 @@
import * as Cesium from 'cesium';
import { aircraftIcon } from '../../data/aircraftIcons.js';
import { FOCUS_EVIDENCE_DEV } from './policy.js';
import { writeHoldSegment } from '../../data/iconSegment.js';
import {
addBillboard,
billboardState,
setBillboard,
} from '../../data/billboardMirror.js';
aircraftRecessionFactors,
aircraftTreatmentAlpha,
} from '../../data/aircraftRecession.js';
import { horizonOccluder } from '../../data/iconOrientation.js';
export function createEvidence({
flightState,
@@ -44,17 +44,14 @@ export function createEvidence({
/** Replace the real fleet with deterministic explicit-position contacts. */
function _setFocusEvidenceAircraft(records = []) {
if (
!FOCUS_EVIDENCE_DEV ||
!flightState._billboardCollection ||
!flightState._viewer
)
if (!FOCUS_EVIDENCE_DEV || !flightState._icons || !flightState._viewer)
return { ok: false, count: 0 };
if (flightState._trackedIcao) parts.tracking._clearTracking();
parts.rendering._releaseModels();
for (const bb of flightState._billboards.values())
flightState._billboardCollection.remove(bb);
flightState._billboards.clear();
const icons = flightState._icons;
icons.clear();
flightState._grounded.clear();
flightState._floorPinned.clear();
flightState.records.data.clear();
flightState._positionHistory.clear();
flightState._displayCourse.clear();
@@ -86,32 +83,19 @@ export function createEvidence({
rawLon: record.longitude ?? null,
};
flightState.records.data.set(id, meta);
flightState._cullPositions.delete(id);
flightState._focusEvidenceIds.add(id);
const bb = addBillboard(flightState._billboardCollection, {
position,
image: aircraftIcon(parts.rendering._iconKind(id, klass)),
width: 20,
height: 20,
scale: parts.rendering._fleetBillboardScale(id, klass),
rotation: 0,
alignedAxis: Cesium.Cartesian3.ZERO,
color: parts.rendering._fleetBillboardColor(id),
sizeInMeters: false,
scaleByDistance: parts.rendering._normalBillboardScaleByDistance(),
disableDepthTestDistance: parts.rendering._groundDepthDistance(),
id,
show: true,
});
flightState._billboards.set(id, bb);
parts.icons._addIcon(id);
const row = icons.row(id);
writeHoldSegment(icons.segments, row, position, meta.true_track);
icons.segmentWritten(row);
}
flightState.feed._count = flightState._billboards.size;
flightState.feed._count = icons.count;
flightState._lastFleetTickMs = 0;
flightState._viewer.scene.requestRender?.();
return { ok: true, count: flightState.feed._count };
}
/** Update explicit evidence positions without rebuilding billboards. */
/** Update explicit evidence positions without rebuilding rows. */
function _moveFocusEvidenceAircraft(records = []) {
if (!FOCUS_EVIDENCE_DEV) return { ok: false, moved: 0 };
@@ -122,15 +106,21 @@ export function createEvidence({
.toLowerCase();
if (!flightState._focusEvidenceIds.has(id)) continue;
const position = _focusEvidencePosition(record);
const bb = flightState._billboards.get(id);
if (!position || !bb) continue;
setBillboard(bb, 'position', position);
const row = flightState._icons?.row(id) ?? -1;
if (!position || row < 0) continue;
const meta = flightState.records.data.get(id);
if (meta) {
if (Number.isFinite(record.trackDeg)) meta.true_track = record.trackDeg;
if (Number.isFinite(record.velocityMps))
meta.velocity = record.velocityMps;
}
writeHoldSegment(
flightState._icons.segments,
row,
position,
meta?.true_track ?? 0,
);
flightState._icons.segmentWritten(row);
moved += 1;
}
flightState._lastFleetTickMs = 0;
@@ -142,27 +132,42 @@ export function createEvidence({
function _focusEvidenceSnapshot() {
if (!FOCUS_EVIDENCE_DEV || !flightState._viewer) return [];
const icons = flightState._icons;
const { scene, camera } = flightState._viewer;
const occluder = horizonOccluder(camera);
const cameraHeightM = camera.positionCartographic?.height;
return [...flightState._focusEvidenceIds].map((id) => {
const bb = flightState._billboards.get(id);
const screen = billboardState(bb)?.position
? Cesium.SceneTransforms.worldToWindowCoordinates(
flightState._viewer.scene,
billboardState(bb).position,
)
const row = icons?.row(id) ?? -1;
const position = row < 0 ? null : parts.icons._iconPosition(id);
const cameraDistanceM = position
? Cesium.Cartesian3.distance(camera.positionWC, position)
: null;
const screen = position
? Cesium.SceneTransforms.worldToWindowCoordinates(scene, position)
: null;
// What the icon shader composes: base scale × limb taper, and base
// alpha × max(floor, focus × limb haze).
const factors = position
? aircraftRecessionFactors({ cameraDistanceM, cameraHeightM })
: null;
return {
id,
show: billboardState(bb)?.show === true,
scale: billboardState(bb)?.scale ?? null,
alpha: bb?.color?.alpha ?? null,
show:
!!position &&
icons.shown(row) &&
parts.icons._aboveHorizon(row, position, occluder),
scale: factors ? icons.scale(row) * factors.scale : null,
alpha: position
? aircraftTreatmentAlpha({
baseAlpha: icons.alpha(row),
focusFactor: icons.focus(row),
cameraDistanceM,
cameraHeightM,
})
: null,
x: screen?.x ?? null,
y: screen?.y ?? null,
cameraDistanceM: billboardState(bb)?.position
? Cesium.Cartesian3.distance(
flightState._viewer.camera.positionWC,
billboardState(bb).position,
)
: null,
cameraDistanceM,
};
});
}
+471
View File
@@ -0,0 +1,471 @@
import * as Cesium from 'cesium';
import {
COURSE_HOLD_SPEED_MPS,
courseBetweenCartesians,
courseSlewCapDps,
norm180,
speedRamp,
staleCoastLimitSeconds,
} from '../../data/motionModel.js';
import {
createIconSet,
ICON_CULL_LIFT,
ICON_RECEDES,
ICON_ROTATES,
} from '../../data/iconSet.js';
import {
segmentCourse,
segmentTargetCourse,
writeArcSegment,
writeCourseGlide,
writeHoldSegment,
writeLerpSegment,
} from '../../data/iconSegment.js';
import { IconSetPrimitive } from '../../data/iconSetPrimitive.js';
import { iconAtlas } from '../../data/iconAtlas.js';
import {
aircraftIcon,
FLEET_ICON_PX,
TRACKED_ICON_PX,
} from '../../data/aircraftIcons.js';
import { cockpitContactDotImage } from '../../data/cockpitContactDot.js';
import { horizonOccluder } from '../../data/iconOrientation.js';
import {
COCKPIT_CONTACT_SIZE_PX,
COURSE_MAX_DPS,
FLOOR_FAR_EVERY_TICKS,
FLOOR_NEAR_M,
RENDER_DELAY_SEC,
} from './policy.js';
/** Raster size of the cockpit contact pip (cockpitContactDot.js draws 16 px). */
const COCKPIT_DOT_PX = 16;
/** Pinned grounded rows rewrite only past this displayed move (m²) or turn (°). */
const PIN_MOVE_SQ_M = 1;
const PIN_TURN_DEG = 0.1;
/**
* The flights layer's contacts as one icon set: each row's motion segment
* mirrors `_deadReckon`'s regime for the delayed display time, so the GPU
* moves every untracked contact between polls with no per-frame CPU pass.
*/
export function createIcons({ flightState, services, parts }) {
const { focusPassIsNeeded } = services.focus;
const scratchTime = new Cesium.JulianDate();
const scratchPosition = new Cesium.Cartesian3();
const scratchCull = new Cesium.Cartesian3();
const scratchScaleByDistance = new Cesium.NearFarScalar();
let normalScaleByDistance = null;
let cockpitScaleByDistance = null;
/** The delayed instant every untracked contact is drawn at. */
function _displayTime(result = scratchTime) {
return Cesium.JulianDate.addSeconds(
Cesium.JulianDate.now(result),
-RENDER_DELAY_SEC,
result,
);
}
function _displayTau() {
return flightState._icons.seconds(_displayTime());
}
/** Create the set and its primitive for one layer lifetime. */
function _createIcons(viewer) {
flightState._icons = createIconSet({ epoch: _displayTime() });
flightState._iconPrimitive = new IconSetPrimitive({
set: flightState._icons,
renderTime: () => _displayTime(flightState._scratchIconRenderTime),
});
viewer.scene.primitives.add(flightState._iconPrimitive);
return flightState._iconPrimitive;
}
/** Where a contact is drawn right now, or null when it has no row. */
function _iconPosition(icao24, result = new Cesium.Cartesian3()) {
const set = flightState._icons;
const row = set.row(icao24);
return row < 0 ? null : set.position(row, _displayTau(), result);
}
/** The nose direction a contact is drawn with right now. */
function _iconCourse(icao24) {
const set = flightState._icons;
const row = set.row(icao24);
return row < 0 ? null : set.course(row, _displayTau());
}
/** Whether the layer draws this contact's icon (not tracked, not model-owned). */
function _iconShown(icao24) {
const set = flightState._icons;
const row = set.row(icao24);
return row >= 0 && set.shown(row);
}
/** The camera's horizon test, or null when there is no placed camera. */
function _occluder() {
const camera = flightState._viewer?.camera;
return camera?.positionWC ? horizonOccluder(camera) : null;
}
/** Whether a drawn row at `position` clears the camera's horizon, judged
* exactly as the icon shader judges it. No camera, no horizon. */
function _aboveHorizon(row, position, occluder) {
return (
!occluder ||
occluder.isPointVisible(
flightState._icons.cullPoint(row, position, scratchCull),
)
);
}
/**
* Write the segment `_deadReckon` would follow from now on: interpolation
* between the bracketing fixes, the warm-up arc back from the oldest fix, or
* the coast arc past the newest. The displayed nose glides from where it is
* onto the new course at the fleet's slew cap; a hovering contact keeps its
* nose. The row expires when the regime would change.
* @param {string} icao24
* @param {{fromCourseDeg?: number|null}} [options] Nose to glide from when
* the row's own segment is not what was on screen (tracking handoff).
*/
function _planIcon(icao24, { fromCourseDeg } = {}) {
const set = flightState._icons;
const row = set.row(icao24);
if (row < 0) return;
if (flightState._focusEvidenceIds.has(icao24)) return;
if (flightState._floorPinned.has(icao24)) return;
const history = flightState._positionHistory.get(icao24);
if (!history?.length) return;
const info = flightState.records.data.get(icao24);
const renderTime = _displayTime();
const tau = set.seconds(renderTime);
const seg = set.segments;
const shownCourse =
fromCourseDeg ??
(set.expires(row) === -Infinity ? null : set.course(row, tau));
const regime = parts.motion._motionRegime(history, info, renderTime);
let speed;
let expiresAt = Infinity;
if (regime.kind === 'between') {
const { a, b, span, trackFrom, trackTo, chordCourse } = regime;
speed = regime.speed;
const blend = chordCourse == null ? 0 : regime.blend;
const t0 = set.seconds(a.time);
if (span > 0) {
writeLerpSegment(seg, row, {
from: a.position,
to: b.position,
t0,
t1: set.seconds(b.time),
course0Deg:
trackFrom + blend * norm180((chordCourse ?? 0) - trackFrom),
courseSlopeDeg: norm180(trackTo - trackFrom) * (1 - blend),
});
} else {
writeHoldSegment(
seg,
row,
b.position,
trackTo + blend * norm180((chordCourse ?? 0) - trackTo),
);
}
expiresAt = set.seconds(b.time);
} else {
const { fix, heading } = regime;
speed = regime.speed;
const t0 = set.seconds(fix.time);
if (speed === 0) {
writeHoldSegment(seg, row, fix.position, heading);
} else {
writeArcSegment(seg, row, {
fix: fix.position,
t0,
minDt: regime.minDt,
maxDt: regime.maxDt,
speedMps: speed,
trackDeg: heading,
turnRateDps: regime.turnRateDps,
});
}
// A warm-up's arc ends at its fix; a coast's runs to its limit.
if (regime.maxDt === 0) expiresAt = t0;
}
// Below hover speed the nose holds what is on screen (or, for a new row,
// where this segment starts); above it, it glides on at the slew cap.
const hold = speed < COURSE_HOLD_SPEED_MPS;
if (hold || shownCourse != null) {
writeCourseGlide(
seg,
row,
shownCourse ?? segmentCourse(seg, row, tau),
tau,
hold ? 0 : courseSlewCapDps(speed, COURSE_MAX_DPS),
);
}
set.segmentWritten(row, expiresAt);
}
/** Re-plan every row whose regime ended before `tau`. */
function _replanExpired(tau) {
const set = flightState._icons;
const due = [];
for (let row = 0; row < set.count; row++)
if (set.expires(row) <= tau) due.push(set.idAt(row));
for (const icao24 of due) _planIcon(icao24);
}
/**
* Grounded contacts stand on the floor under where they are DRAWN, which the
* GPU cannot sample. While the floor lifts one, the CPU pins its row to a
* hold at the floored dead-reckoned position each tick (rewritten only when
* it moved); once nothing lifts it the row goes back to its planned segment.
*/
function _floorGroundedIcons(renderTime, tau, nowMs, cameraPosition) {
const set = flightState._icons;
// Past FLOOR_NEAR_M a taxiing contact moves well under a pixel per tick,
// so far rows take turns: each refreshes every FLOOR_FAR_EVERY_TICKS. Their
// eases onto a lower floor advance per call, so they run that much slower
// out there too — sub-pixel at that range, and full rate once near.
const turn = flightState._floorTick++ % FLOOR_FAR_EVERY_TICKS;
const nearSq = FLOOR_NEAR_M * FLOOR_NEAR_M;
for (const icao24 of flightState._grounded) {
if (icao24 === flightState._trackedIcao) continue;
const row = set.row(icao24);
if (row < 0) continue;
if (
row % FLOOR_FAR_EVERY_TICKS !== turn &&
cameraPosition &&
Cesium.Cartesian3.distanceSquared(
cameraPosition,
set.position(row, tau, scratchPosition),
) > nearSq
)
continue;
_floorIcon(icao24, renderTime, tau, nowMs);
}
}
/** One grounded contact's floor pass (see `_floorGroundedIcons`). */
function _floorIcon(icao24, renderTime, tau, nowMs) {
const set = flightState._icons;
const row = set.row(icao24);
if (row < 0) return;
const info = flightState.records.data.get(icao24);
const raw = parts.motion._deadReckon(
icao24,
flightState._scratchFleetPos,
renderTime,
);
const rawCourse =
flightState._drCourseDeg ?? (info && info.true_track) ?? 0;
const hold = flightState._drCourseHold;
const speed = flightState._drSpeedMps ?? info?.velocity ?? NaN;
const display = parts.motion._floorGroundedDisplayPosition(
icao24,
info,
raw,
parts.rendering._modelOwnsVisual(icao24),
nowMs,
);
if (!display || display === raw) {
if (flightState._floorPinned.delete(icao24))
_planIcon(icao24, { fromCourseDeg: set.course(row, tau) });
return;
}
const planned = set.expires(row) !== -Infinity;
const prev = planned ? set.course(row, tau) : null;
// The pin holds the position; the GPU glides the course onto the target,
// so the rate stays exact however seldom the pin is rewritten.
const target = hold && prev != null ? prev : rawCourse;
if (
flightState._floorPinned.has(icao24) &&
Cesium.Cartesian3.distanceSquared(
display,
set.position(row, tau, scratchPosition),
) <= PIN_MOVE_SQ_M &&
Math.abs(norm180(target - segmentTargetCourse(set.segments, row, tau))) <=
PIN_TURN_DEG
)
return;
flightState._floorPinned.add(icao24);
writeHoldSegment(set.segments, row, display, target);
if (prev != null)
writeCourseGlide(
set.segments,
row,
prev,
tau,
courseSlewCapDps(speed, COURSE_MAX_DPS),
);
set.segmentWritten(row);
}
/**
* Advance focus de-emphasis for every drawn row (and every row a fleet model
* draws for, whose alpha reads it), only while a focus target exists or some
* row is still relaxing back to full emphasis.
*/
function _focusIcons(tau, scene, camera, nowMs, target) {
if (!focusPassIsNeeded(target, flightState._iconFocusActive)) return;
const { advanceProjectedSpriteFocus, nearFarScalarValueAtDistance } =
services.focus;
const set = flightState._icons;
let active = 0;
for (let row = 0; row < set.count; row++) {
if (!set.shown(row) && !flightState._models.has(set.idAt(row))) continue;
const position = set.position(row, tau, scratchPosition);
const distanceScale = nearFarScalarValueAtDistance(
set.scaleByDistance(row, scratchScaleByDistance),
Cesium.Cartesian3.distance(camera.positionWC, position),
);
const half = set.scale(row) * distanceScale * 0.5;
const focus = advanceProjectedSpriteFocus(
set.metaAt(row),
position,
scene,
camera,
nowMs,
target,
undefined,
set.width(row) * half,
set.height(row) * half,
);
set.setFocus(row, focus.factor);
if (focus.active || focus.transitioning) active++;
}
flightState._iconFocusActive = active;
}
/** Image, size, scale, tint and freshness for one row under the current
* normal/cockpit contract. Motion and rotation stay with the segment. */
function _applyIconPresentation(icao24) {
const set = flightState._icons;
const row = set.row(icao24);
if (row < 0) return;
const info = flightState.records.data.get(icao24);
const tracked = icao24 === flightState._trackedIcao;
const cockpitDot =
flightState._cockpitContactMode &&
!tracked &&
!flightState._cockpitNearContacts.has(icao24);
if (cockpitDot) {
const dot = iconAtlas.slot(cockpitContactDotImage(), COCKPIT_DOT_PX);
set.setImage(row, dot, dot);
set.setSize(row, COCKPIT_CONTACT_SIZE_PX, COCKPIT_CONTACT_SIZE_PX);
set.setScale(row, 1);
cockpitScaleByDistance ??=
parts.rendering._cockpitBillboardScaleByDistance();
set.setScaleByDistance(row, cockpitScaleByDistance);
set.setColor(row, parts.rendering._cockpitContactColor(icao24, info));
set.setFlag(row, ICON_ROTATES, false);
} else {
set.setImage(
row,
iconAtlas.slot(
aircraftIcon(parts.rendering._iconKind(icao24, info?.klass)),
FLEET_ICON_PX,
),
iconAtlas.slot(
aircraftIcon(
parts.rendering._iconKind(icao24, info?.klass),
TRACKED_ICON_PX,
),
TRACKED_ICON_PX,
),
);
const size = tracked ? 24 : 20;
set.setSize(row, size, size);
set.setScale(
row,
parts.rendering._fleetBillboardScale(icao24, info?.klass),
);
normalScaleByDistance ??=
parts.rendering._normalBillboardScaleByDistance();
set.setScaleByDistance(row, normalScaleByDistance);
set.setColor(row, parts.rendering._fleetBillboardColor(icao24, info));
set.setFlag(row, ICON_ROTATES, true);
}
set.setFlag(row, ICON_RECEDES, true);
set.setFlag(
row,
ICON_CULL_LIFT,
Number.isFinite(info?.renderAltitudeM) && info.renderAltitudeM < 10,
);
set.setAlpha(row, flightState.records.missingPolls.get(icao24) ? 0.45 : 1);
}
/** Add a row for a new contact, hidden while it is the tracked one. */
function _addIcon(icao24) {
const set = flightState._icons;
const row = set.add(icao24, { layer: 'flights', id: icao24 });
_applyIconPresentation(icao24);
_settleIcon(icao24);
set.setShown(row, icao24 !== flightState._trackedIcao);
return row;
}
/**
* Plan a row afresh, dropping any floor pin (a pin stops at the last floor
* pass, which skips a tracked contact), and let a dimmed row relax. A grounded contact goes straight onto
* the floor under where it is drawn, not its fix's floor until the next fleet
* tick raises it.
* @param {string} icao24
* @param {{fromCourseDeg?: number}} [options] The course on screen to glide from.
*/
function _settleIcon(icao24, options) {
const set = flightState._icons;
flightState._floorPinned.delete(icao24);
// A row that sat out the focus pass (tracked) may come back dimmed after
// the pass went idle; count it so the pass runs and relaxes it.
if (set.focus(set.row(icao24)) < 1) flightState._iconFocusActive += 1;
_planIcon(icao24, options);
if (flightState._grounded.has(icao24)) {
const renderTime = _displayTime();
_floorIcon(icao24, renderTime, set.seconds(renderTime), Date.now());
}
}
/** Drop a contact and everything the layer keeps for it. */
function _forgetContact(icao24) {
flightState._icons?.remove(icao24);
parts.rendering._releaseModel(icao24);
flightState.records.forget(icao24);
flightState._positionHistory.delete(icao24);
flightState._displayCourse.delete(icao24);
flightState._groundSnap.forget(icao24);
flightState._displayFloorState.delete(icao24);
flightState._grounded.delete(icao24);
flightState._floorPinned.delete(icao24);
}
/** Icon ids, so ray samples never land on an aircraft instead of the ground. */
function _iconIds() {
const set = flightState._icons;
const ids = new Array(set?.count ?? 0);
for (let row = 0; row < ids.length; row++) ids[row] = set.idAt(row);
return ids;
}
return {
_displayTime,
_displayTau,
_createIcons,
_iconPosition,
_iconCourse,
_iconShown,
_occluder,
_aboveHorizon,
_planIcon,
_settleIcon,
_replanExpired,
_floorGroundedIcons,
_focusIcons,
_applyIconPresentation,
_addIcon,
_forgetContact,
_iconIds,
};
}
+120
View File
@@ -0,0 +1,120 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import { createFlightState } from './state.js';
import { createIcons } from './icons.js';
import { createIconSet } from '../../data/iconSet.js';
import { writeHoldSegment } from '../../data/iconSegment.js';
function services(focus = {}) {
return {
focus: { focusPassIsNeeded: () => true, ...focus },
groundSnap: { createGroundSnap: () => ({ clear() {}, forget() {} }) },
};
}
function setup({ focus, motion = {}, rendering = {} } = {}) {
const svc = services(focus);
const flightState = createFlightState({ services: svc });
flightState._icons = createIconSet();
const parts = {
motion,
rendering: { _modelOwnsVisual: () => false, ...rendering },
};
const icons = createIcons({ flightState, services: svc, parts });
return { flightState, icons, set: flightState._icons };
}
function hold(set, id, position, course = 0) {
const row = set.add(id, { layer: 'flights', id });
writeHoldSegment(set.segments, row, position, course);
set.segmentWritten(row);
return row;
}
const at = (lon, h = 1000) => Cesium.Cartesian3.fromDegrees(lon, 30, h);
test('focus advances for drawn rows and for rows a fleet model draws for', () => {
const advanced = [];
const { flightState, icons, set } = setup({
focus: {
nearFarScalarValueAtDistance: () => 1,
advanceProjectedSpriteFocus(meta) {
advanced.push(meta.id);
return { factor: 0.4, active: true };
},
},
});
set.setShown(hold(set, 'drawn', at(0)), true);
const modeled = hold(set, 'modeled', at(0.1));
hold(set, 'hidden', at(0.2));
flightState._models.set('modeled', {});
icons._focusIcons(0, {}, { positionWC: at(0, 50_000) }, 0, {});
assert.deepEqual(advanced, ['drawn', 'modeled']);
assert.ok(
Math.abs(set.focus(modeled) - 0.4) < 1e-6,
"the model's alpha reads it",
);
assert.equal(flightState._iconFocusActive, 2);
});
test('settling a pinned grounded contact re-floors it where it is now, nose kept', () => {
const taxied = at(0.01, 100);
const floored = at(0.01, 131.5);
const { flightState, icons, set } = setup({
motion: {
_motionRegime: () => ({
kind: 'fix',
fix: { position: taxied, time: Cesium.JulianDate.now() },
speed: 0,
heading: 200,
turnRateDps: 0,
minDt: -60,
maxDt: 60,
}),
_deadReckon(icao24, result) {
flightState._drCourseDeg = 200;
flightState._drSpeedMps = 0;
flightState._drCourseHold = true;
return Cesium.Cartesian3.clone(taxied, result);
},
_floorGroundedDisplayPosition: () => floored,
},
});
// Pinned before it was tracked, 1 km back, facing 10°.
const row = hold(set, 'g', at(0, 131.5), 10);
flightState._floorPinned.add('g');
flightState._grounded.add('g');
flightState.records.data.set('g', { onGround: true });
flightState._positionHistory.set('g', [{}]);
icons._settleIcon('g', { fromCourseDeg: 123 });
const tau = icons._displayTau();
assert.ok(Cesium.Cartesian3.distance(set.position(row, tau), floored) < 0.01);
assert.ok(Math.abs(set.course(row, tau) - 123) < 1e-3, 'tracked nose kept');
assert.ok(flightState._floorPinned.has('g'), 'pinned on the new floor');
});
test('a contact that comes back dimmed re-arms the idle focus pass', () => {
const advanced = [];
const { flightState, icons, set } = setup({
focus: {
focusPassIsNeeded: (target, active) => target != null || active > 0,
nearFarScalarValueAtDistance: () => 1,
advanceProjectedSpriteFocus(meta) {
advanced.push(meta.id);
return { factor: 1, active: false, transitioning: false };
},
},
motion: { _motionRegime: () => null },
});
const row = hold(set, 'was-tracked', at(0));
set.setFocus(row, 0.2);
flightState._iconFocusActive = 0;
icons._settleIcon('was-tracked');
set.setShown(row, true);
icons._focusIcons(0, {}, { positionWC: at(0, 50_000) }, 0, null);
assert.deepEqual(advanced, ['was-tracked']);
assert.equal(set.focus(row), 1);
});
+3
View File
@@ -1,6 +1,7 @@
import { createFlightSnapshotRenderer } from './snapshotRenderer.js';
import { createFlightState } from './state.js';
import { createRendering } from './rendering.js';
import { createIcons } from './icons.js';
import { createMotion } from './motion.js';
import { createTracking } from './tracking.js';
import { createController } from './controller.js';
@@ -21,6 +22,7 @@ export function createCivilFlightLayer({
const layer = {};
const context = { flightState, services, parts, layer, resolveAsset };
parts.rendering = createRendering(context);
parts.icons = createIcons(context);
parts.motion = createMotion(context);
parts.tracking = createTracking(context);
parts.controller = createController(context);
@@ -38,6 +40,7 @@ export function createCivilFlightLayer({
rendering: parts.rendering,
tracking: parts.tracking,
motion: parts.motion,
icons: parts.icons,
enrichment: parts.enrichment,
queries: parts.queries,
});
+24 -32
View File
@@ -34,7 +34,7 @@ export function createLifecycle({
/**
* Initialize the flights layer.
* Creates the BillboardCollection, resets all state, and installs the
* Creates the icon set and its primitive, resets all state, and installs the
* click-to-track handler on the scene canvas.
* @param {Cesium.Viewer} viewer - The CesiumJS viewer instance.
*/
@@ -48,9 +48,7 @@ export function createLifecycle({
clearFocusTarget('flights');
flightState._focusEvidenceIds.clear();
flightState._viewer = viewer;
flightState._billboardCollection = new Cesium.BillboardCollection();
viewer.scene.primitives.add(flightState._billboardCollection);
registerSpriteCollection('flights', flightState._billboardCollection);
registerSpriteCollection('flights', parts.icons._createIcons(viewer));
flightState._modelCollection = new Cesium.PrimitiveCollection();
viewer.scene.primitives.add(flightState._modelCollection);
// Warm the glTF cache so the tracked plane's model instantiates instantly when first needed
@@ -78,8 +76,9 @@ export function createLifecycle({
/* tracked plane just stays a billboard a beat longer */
});
}
flightState._billboards = new Map();
flightState._cullPositions.clear();
flightState._grounded.clear();
flightState._floorPinned.clear();
flightState._iconFocusActive = 0;
flightState._detectionObjects = new Map();
flightState.records.data = new Map();
flightState._positionHistory = new Map();
@@ -126,16 +125,15 @@ export function createLifecycle({
restoreSpriteOrder(viewer);
console.log('[Data:Flights] Initialized with billboard icons');
console.log('[Data:Flights] Initialized with GPU icons');
},
/**
* Show the billboard collection and re-install the click handler.
* Show the icons and re-install the click handler.
* @param {Cesium.Viewer} viewer
*/
enable(viewer) {
if (flightState._billboardCollection)
flightState._billboardCollection.show = true;
if (flightState._icons) flightState._icons.show = true;
holdContinuousRender('flights'); // per-frame animator (perf wave 2)
if (flightState._modelCollection)
flightState._modelCollection.show = true;
@@ -156,11 +154,10 @@ export function createLifecycle({
});
}
parts.tracking._installClickHandler(viewer);
registerPickOwner('flights', (pickedId) =>
flightState._billboards.has(pickedId),
registerPickOwner(
'flights',
(pickedId) => flightState._icons?.has(pickedId) === true,
);
// Force a fresh rotation pass on the first tick after re-enable
flightState._lastCamPoseSig = '';
if (!flightState._preRenderRemove && viewer?.scene) {
flightState._preRenderRemove = viewer.scene.preRender.addEventListener(
parts.rendering._fleetTick,
@@ -173,18 +170,11 @@ export function createLifecycle({
);
}
if (!flightState._moveEndRemove && viewer?.camera) {
// Arrival polish (field test 2026-07-03: "planes look weird when you first
// come to them"): when a camera move SETTLES (voice fly-to, fast pan
// release), force a full rotation pass on the very next frame. The
// pose-signature gate alone can eat the settle — the final easing frames
// of a flight land inside one quantization bucket (10 m / 0.06°), leaving
// every icon wearing its last mid-flight rotation for up to
// ROTATION_REFRESH_MS. Zeroing the tick throttle too means the pass runs
// on the next preRender, not up to FLEET_DR_INTERVAL_MS later. Cost: one
// extra rotation pass per completed camera gesture — nothing per-frame.
// A settled camera move (voice fly-to, fast pan release) runs the fleet
// pass on the very next preRender rather than up to FLEET_DR_INTERVAL_MS
// later, so the model handoff and focus land with the camera.
flightState._moveEndRemove = viewer.camera.moveEnd.addEventListener(
() => {
flightState._lastCamPoseSig = '';
flightState._lastFleetTickMs = 0;
},
);
@@ -193,15 +183,14 @@ export function createLifecycle({
},
/**
* Hide all flight billboards and tear down click/keyboard handlers.
* Hide all flight icons and tear down click/keyboard handlers.
* Also clears any active flight tracking so the camera is released.
* @param {Cesium.Viewer} viewer
*/
disable(viewer) {
parts.controller._abortActiveUpdates();
parts.tracking._cancelPendingTrackingRestore();
if (flightState._billboardCollection)
flightState._billboardCollection.show = false;
if (flightState._icons) flightState._icons.show = false;
releaseContinuousRender('flights');
parts.rendering._releaseModels();
if (flightState._modelCollection)
@@ -282,10 +271,12 @@ export function createLifecycle({
flightState._moveEndRemove = null;
}
parts.rendering._releaseModels();
if (flightState._billboardCollection) {
viewer.scene.primitives.remove(flightState._billboardCollection);
flightState._billboardCollection = null;
if (flightState._iconPrimitive) {
viewer.scene.primitives.remove(flightState._iconPrimitive); // removing destroys it
flightState._iconPrimitive = null;
}
flightState._icons?.clear();
flightState._icons = null;
if (flightState._modelCollection) {
viewer.scene.primitives.remove(flightState._modelCollection); // removing destroys it + its models
flightState._modelCollection = null;
@@ -302,8 +293,9 @@ export function createLifecycle({
flightState._preloadModel = null;
}
flightState._planeModelLoaded = false;
flightState._billboards.clear();
flightState._cullPositions.clear();
flightState._grounded.clear();
flightState._floorPinned.clear();
flightState._iconFocusActive = 0;
flightState._detectionObjects.clear();
flightState.records.data.clear();
flightState._positionHistory.clear();
+140 -102
View File
@@ -35,10 +35,23 @@ import {
DISPLAY_CORRIDOR_CELL_BUDGET,
DISPLAY_CORRIDOR_FAIR_SHARE,
} from './policy.js';
import { billboardState, setBillboard } from '../../data/billboardMirror.js';
import { modelState } from '../../data/modelMirror.js';
import { readEntityRecord } from '../../data/entityRecords.js';
/** A fix's own ground speed, else the record's. */
function fixSpeedMps(fix, info) {
return Number.isFinite(fix.velocity)
? fix.velocity
: (info && info.velocity) || 0;
}
/** A fix's own track, else the record's. */
function fixTrackDeg(fix, info) {
return Number.isFinite(fix.track)
? fix.track
: (info && info.true_track) || 0;
}
export function createMotion({
flightState,
services,
@@ -66,6 +79,23 @@ export function createMotion({
// Fixes leave the cache with their history; corrected positions invalidate
// the local frame even when the existing Cartesian is mutated in place.
const fixFrames = new WeakMap();
/** `_motionRegime`'s reused result. */
const regime = {
kind: 'fix',
a: null,
b: null,
span: 0,
speed: 0,
trackFrom: 0,
trackTo: 0,
chordCourse: null,
blend: 0,
fix: null,
heading: 0,
turnRateDps: 0,
minDt: 0,
maxDt: 0,
};
// Round 5: the old `_warmGroundedAircraftSurfaceCache` (global exact-5-decimal
// warm for every grounded contact on Earth) is GONE. Parked-aircraft GPS
@@ -81,7 +111,7 @@ export function createMotion({
* coasting on its final meta — if that fix predated the floor warm, it sat
* frozen underground forever. Runs once per poll over the (bounded) flight
* map; touches only grounded, feed-absent, demonstrably-below-floor
* contacts, and patches the stored fix + billboard in place (zero-velocity
* contacts, and patches the stored fix + icon in place (zero-velocity
* DR renders the patched fix verbatim).
* @param {Set<string>} currentIcaos - Contacts present in THIS poll (already
* floored by the live path — skipped here).
@@ -101,7 +131,6 @@ export function createMotion({
)
continue;
info.renderAltitudeM = lifted;
flightState._cullPositions.delete(icao24); // above the ellipsoid now (or floors say otherwise next poll)
const position = Cesium.Cartesian3.fromDegrees(
info.rawLon,
info.rawLat,
@@ -111,8 +140,9 @@ export function createMotion({
const newest = history?.[history.length - 1];
if (newest)
newest.position = Cesium.Cartesian3.clone(position, newest.position);
const bb = flightState._billboards.get(icao24);
if (bb) setBillboard(bb, 'position', position);
// Above the ellipsoid now (or floors say otherwise next poll).
parts.icons._applyIconPresentation(icao24);
parts.icons._planIcon(icao24);
}
}
@@ -134,9 +164,7 @@ export function createMotion({
function _deadReckon(icao24, result, sampledRenderTime) {
const info = flightState.records.data.get(icao24);
if (FOCUS_EVIDENCE_DEV && flightState._focusEvidenceIds.has(icao24)) {
const position = billboardState(
flightState._billboards.get(icao24),
)?.position;
const position = parts.icons._iconPosition(icao24);
flightState._drCourseDeg = info?.true_track || 0;
flightState._drSpeedMps = info?.velocity || 0;
flightState._drCourseHold =
@@ -166,100 +194,113 @@ export function createMotion({
flightState._scratchRenderTime,
);
// Bracketing pair: interpolate — no extrapolation error, no snap-back.
const regime = _motionRegime(history, info, renderTime);
if (regime.kind === 'between') {
const { a, b, span } = regime;
const t =
span > 0
? Cesium.JulianDate.secondsDifference(renderTime, a.time) / span
: 1.0;
const trackCourse = lerpAngleDeg(regime.trackFrom, regime.trackTo, t);
flightState._drCourseDeg =
regime.chordCourse != null
? lerpAngleDeg(trackCourse, regime.chordCourse, regime.blend)
: trackCourse;
flightState._drSpeedMps = regime.speed;
flightState._drCourseHold = regime.speed < COURSE_HOLD_SPEED_MPS;
flightState._drExtrapolating = false;
return Cesium.Cartesian3.lerp(a.position, b.position, t, out);
}
const dt = Cesium.Math.clamp(
Cesium.JulianDate.secondsDifference(renderTime, regime.fix.time),
regime.minDt,
regime.maxDt,
);
return _extrapolateFix(regime.fix, info, dt, out, regime.turnRateDps);
}
/**
* Which motion a contact's history gives at `renderTime` — the one choice
* `_deadReckon` (CPU) and the icon planner (GPU segment) both draw from.
*
* - `between`: interpolate the bracketing pair `a`→`b`, no extrapolation
* error and no snap-back. The course of the DISPLAYED motion blends the
* time-interpolated reported track toward the chord by `blend`: the chord
* is only trustworthy when the segment covers real ground (at hover its
* direction is GPS jitter; on a slow tight turn it STEPS the whole
* per-segment turn at each boundary), and helicopters never use it
* (rotorcraft chords are noise-dominated at their typical speeds).
* - `fix`: extrapolate `fix` by the elapsed time clamped to [minDt, maxDt].
* Warm-up (renderTime predates ALL history: freshly seen or just tracked,
* before RENDER_DELAY_SEC of history exists) runs the OLDEST fix BACKWARD,
* preserving the 30s-behind invariant, so as history fills the icon glides
* FORWARD into interpolation with no freeze and no backward snap (holding
* the oldest fix froze the icon; extrapolating the newest to wall-clock
* made it jump back a poll interval when interpolation took over).
* Otherwise the newest fix coasts. OpenSky can still be receiving fresh
* contact messages for that aircraft; freezing 60 s after the position
* produced a stop → catch-up → stop cadence, so it coasts through the latest
* real contact plus a bounded grace, with an absolute cap so a stale cached
* feed cannot drift forever.
*
* The returned object is a reused scratch: read it before the next call.
* @param {Array<object>} history Non-empty, oldest first.
* @param {object|undefined} info The contact's record.
* @param {Cesium.JulianDate} renderTime
*/
function _motionRegime(history, info, renderTime) {
for (let i = history.length - 1; i >= 1; i--) {
const a = history[i - 1];
const b = history[i];
if (
Cesium.JulianDate.lessThanOrEquals(a.time, renderTime) &&
Cesium.JulianDate.lessThanOrEquals(renderTime, b.time)
) {
const span = Cesium.JulianDate.secondsDifference(b.time, a.time);
const t =
span > 0
? Cesium.JulianDate.secondsDifference(renderTime, a.time) / span
: 1.0;
// Course of the DISPLAYED motion. The chord is only trustworthy when the
// segment covers real ground (at hover its direction is GPS jitter; on a
// slow tight turn it STEPS the whole per-segment turn at each boundary),
// so it is blended against the reported per-fix track by displayed
// ground speed — and the track is TIME-INTERPOLATED between the fixes so
// a slow turner's nose advances continuously through the segment instead
// of snapping once per poll. Helicopters always use the reported track
// (rotorcraft chords are noise-dominated at their typical speeds).
const chordLenM = Cesium.Cartesian3.distance(a.position, b.position);
const segSpeed =
span > 0 ? chordLenM / span : (info && info.velocity) || 0;
const fallbackTrack = (info && info.true_track) || 0;
const trackFrom = Number.isFinite(a.track) ? a.track : fallbackTrack;
const trackTo = Number.isFinite(b.track) ? b.track : trackFrom;
const trackCourse = lerpAngleDeg(trackFrom, trackTo, t);
const w = info && info.klass === 'helicopter' ? 0 : speedRamp(segSpeed);
const chordCourse =
w > 0 ? courseBetweenCartesians(a.position, b.position) : null;
flightState._drCourseDeg =
chordCourse != null
? lerpAngleDeg(trackCourse, chordCourse, w)
: trackCourse;
flightState._drSpeedMps = segSpeed;
flightState._drCourseHold = segSpeed < COURSE_HOLD_SPEED_MPS;
flightState._drExtrapolating = false;
return Cesium.Cartesian3.lerp(a.position, b.position, t, out);
}
!Cesium.JulianDate.lessThanOrEquals(a.time, renderTime) ||
!Cesium.JulianDate.lessThanOrEquals(renderTime, b.time)
)
continue;
const span = Cesium.JulianDate.secondsDifference(b.time, a.time);
const chordLenM = Cesium.Cartesian3.distance(a.position, b.position);
const speed = span > 0 ? chordLenM / span : (info && info.velocity) || 0;
const fallbackTrack = (info && info.true_track) || 0;
const trackFrom = Number.isFinite(a.track) ? a.track : fallbackTrack;
const blend = info && info.klass === 'helicopter' ? 0 : speedRamp(speed);
regime.kind = 'between';
regime.a = a;
regime.b = b;
regime.span = span;
regime.speed = speed;
regime.trackFrom = trackFrom;
regime.trackTo = Number.isFinite(b.track) ? b.track : trackFrom;
regime.chordCourse =
blend > 0 ? courseBetweenCartesians(a.position, b.position) : null;
regime.blend = blend;
return regime;
}
const newest = history[history.length - 1];
const elapsedSec = Cesium.JulianDate.secondsDifference(
renderTime,
newest.time,
);
if (elapsedSec <= 0) {
// Warm-up: renderTime predates ALL history (freshly seen / just-started-tracking
// aircraft, before RENDER_DELAY_SEC of history has accumulated, so no bracketing
// pair exists yet). Render at the DELAYED renderTime — preserving the 30s-behind
// invariant — by extrapolating the OLDEST fix BACKWARD to renderTime. As history
// fills, renderTime advances toward the first fix and the icon glides FORWARD into
// the bracketing interpolation above with NO freeze and NO backward snap. (Holding
// the oldest fix froze the icon; extrapolating the NEWEST fix to wall-clock now made
// the icon jump back ~one poll interval the instant interpolation took over.)
const oldest = history[0];
const lookbackSec = Cesium.JulianDate.secondsDifference(
oldest.time,
renderTime,
); // ≥ 0
return _extrapolateFix(
oldest,
info,
-Math.min(lookbackSec, 60),
out,
(info && info.turnRateDps) || 0,
);
}
// Newest POSITION is older than renderTime. OpenSky can still be receiving
// fresh contact/kinematic messages for that aircraft; freezing at a hard
// 60 s-after-position boundary produced the visible stop → catch-up → stop
// cadence. Coast through the latest real contact plus a bounded grace
// window, with an absolute cap so a stale cached feed cannot drift forever.
const coastLimitSec = staleCoastLimitSeconds({
// `epochMs` is captured once when the poll is normalized. Avoid allocating
// a Date per aircraft on every 12 Hz fleet tick.
fixEpochMs: Number.isFinite(newest.epochMs)
? newest.epochMs
: Cesium.JulianDate.toDate(newest.time).getTime(),
lastContactEpochMs: info?.lastContactEpochMs,
// Permit one minute of contact grace but cap any cached-feed drift at
// five minutes. Source backoff is exposed separately as a STALE cue.
minimumSec: 60,
maximumSec: 300,
});
return _extrapolateFix(
newest,
info,
Math.min(elapsedSec, coastLimitSec),
out,
(info && info.turnRateDps) || 0,
);
const warming = Cesium.JulianDate.lessThan(renderTime, history[0].time);
regime.kind = 'fix';
regime.fix = warming ? history[0] : newest;
regime.speed = fixSpeedMps(regime.fix, info);
regime.heading = fixTrackDeg(regime.fix, info);
regime.turnRateDps = (info && info.turnRateDps) || 0;
regime.minDt = -60;
// A warm-up ends when real time reaches the oldest fix; a single fix keeps
// coasting forward on the same arc.
regime.maxDt =
warming && history.length > 1
? 0
: staleCoastLimitSeconds({
// `epochMs` is captured once when the poll is normalized. Avoid
// allocating a Date per aircraft on every 12 Hz fleet tick.
fixEpochMs: Number.isFinite(newest.epochMs)
? newest.epochMs
: Cesium.JulianDate.toDate(newest.time).getTime(),
lastContactEpochMs: info?.lastContactEpochMs,
// Permit one minute of contact grace but cap any cached-feed drift
// at five minutes. Source backoff is exposed separately as a STALE cue.
minimumSec: 60,
maximumSec: 300,
});
return regime;
}
/**
@@ -274,12 +315,8 @@ export function createMotion({
*/
function _extrapolateFix(fix, info, dt, out, turnRateDps = 0) {
const speed = Number.isFinite(fix.velocity)
? fix.velocity
: (info && info.velocity) || 0;
const heading = Number.isFinite(fix.track)
? fix.track
: (info && info.true_track) || 0;
const speed = fixSpeedMps(fix, info);
const heading = fixTrackDeg(fix, info);
flightState._drSpeedMps = speed;
flightState._drCourseHold = speed < COURSE_HOLD_SPEED_MPS;
flightState._drExtrapolating = true;
@@ -1114,6 +1151,7 @@ export function createMotion({
return {
_refloorStaleGroundedContacts,
_deadReckon,
_motionRegime,
_extrapolateFix,
_isTrackWarmingUp,
_trackedFocusSizePx,
+2 -1
View File
@@ -59,7 +59,8 @@ test('flight state owns distinct mutable records, scratch objects and floor samp
for (const key of [
'records',
'feed',
'_billboards',
'_grounded',
'_floorPinned',
'_positionHistory',
'_displayFloorState',
'_groundSnap',
+8
View File
@@ -207,6 +207,14 @@ export const COURSE_MAX_DPS = 60;
export const COURSE_SLEW_DT_MAX_SEC = 0.25;
/** Grounded contacts this close to the camera are floored every fleet tick. */
export const FLOOR_NEAR_M = 50_000;
/** Farther grounded contacts are floored once per this many fleet ticks (~1 s). */
export const FLOOR_FAR_EVERY_TICKS = 12;
// ---------------------------------------------------------------------------
// adsbdb enrichment (best-effort, fail-silent). Bounded fan-out: max 4
// concurrent requests, dispatches dripped ≥ENRICH_DISPATCH_GAP_MS apart
+68 -91
View File
@@ -16,7 +16,6 @@ import {
FLEET_DR_INTERVAL_MS,
} from './policy.js';
import { emergencyBroadcastPhrase } from '../../data/aircraftEmergency.js';
import { billboardState, setBillboard } from '../../data/billboardMirror.js';
import { modelState } from '../../data/modelMirror.js';
export function createQueries({
@@ -99,7 +98,7 @@ export function createQueries({
* airframe itself and cannot go stale the way a leg can.
*
* This is a DISPLAY string only. Identity everywhere in this layer is `icao24`
* (the `_billboards`/`_flightData` key, the `sourceId` detection declutter hashes,
* (the `_icons`/`_flightData` key, the `sourceId` detection declutter hashes,
* the `id` that trackById/Context cohorts resolve) — never the label.
* @param {string} icao24 - ICAO 24-bit transponder address (the identity key).
* @param {Object|null|undefined} info - `_flightData` record for this aircraft.
@@ -134,10 +133,8 @@ export function createQueries({
// every visual consumer reads the floored per-frame cache instead (see
// `_trackedDisplayPosition`). If that ever changes, floor this path too.
const info = flightState.records.data.get(icao24);
const bb = flightState._billboards.get(icao24);
const basePos =
parts.motion._deadReckon(icao24) ||
(bb ? billboardState(bb).position : null);
parts.motion._deadReckon(icao24) || parts.icons._iconPosition(icao24);
if (!basePos) return null;
const displayed = displayedKinematics({
derivedSpeedMps: flightState._drSpeedMps,
@@ -334,14 +331,8 @@ export function createQueries({
if (!flightState._models3dEnabled) {
parts.rendering._releaseModels();
parts.tracking._syncTracked2dRotation();
// Restore fleet billboards (the horizon-cull pass re-asserts next tick), but NEVER the
// tracked plane's own fleet billboard — its tracked entity is the visual, so re-showing it
// here would double-image the tracked plane when 3D is turned off mid-track.
if (flightState._billboardCollection)
for (const [icao, bb] of flightState._billboards) {
if (icao !== flightState._trackedIcao)
setBillboard(bb, 'show', true);
}
// (Releasing the models handed each fleet contact back to its icon, never
// the tracked plane's own — its tracked entity is the visual.)
}
}
if (
@@ -424,42 +415,34 @@ export function createQueries({
* @returns {Array<{position: Cesium.Cartesian3, id: string, type: string, skipLabel: boolean}>}
*/
getDetectableObjects(options = {}) {
if (
!flightState._billboardCollection ||
!flightState._billboardCollection.show
)
return [];
// Compute a stride that evenly samples the billboard map.
const icons = flightState._icons;
if (!icons || !icons.show) return [];
// Compute a stride that evenly samples the icon rows.
// seed shifts the starting offset so successive calls can sample
// different aircraft without shuffling the underlying Map order.
// different aircraft without reordering the rows.
const maxCount = Number.isFinite(options.maxCount)
? Math.max(1, Math.floor(options.maxCount))
: flightState._billboards.size;
: icons.count;
const seed = Number.isFinite(options.seed) ? Math.floor(options.seed) : 0;
const stride = Math.max(
1,
Math.ceil(flightState._billboards.size / maxCount),
);
const stride = Math.max(1, Math.ceil(icons.count / maxCount));
const start = seed % stride;
const occluder = parts.icons._occluder();
const tau = parts.icons._displayTau();
const result = [];
let idx = 0;
for (const [icao24, bb] of flightState._billboards) {
const shouldTake = (idx - start) % stride === 0;
idx++;
if (!shouldTake) continue;
for (let row = start; row < icons.count; row += stride) {
const icao24 = icons.idAt(row);
if (
flightState._cockpitContactMode &&
icao24.toLowerCase() === flightState._cockpitSubjectId
)
continue;
const isTracked = icao24 === flightState._trackedIcao;
// Keep planes rendered as a 3D model (billboard hidden) so the detection box
// doesn't vanish on the 2D→3D handoff; bb.position stays current while hidden.
// Keep planes rendered as a 3D model (icon hidden) so the detection box
// doesn't vanish on the 2D→3D handoff.
const model = flightState._models.get(icao24) ?? null;
const modelOwnsVisual = parts.rendering._modelOwnsVisual(icao24, model);
if (!isTracked && !billboardState(bb).show && !modelOwnsVisual)
continue;
if (!isTracked && !icons.shown(row) && !modelOwnsVisual) continue;
const info = flightState.records.data.get(icao24);
let object = flightState._detectionObjects.get(icao24);
if (!object) {
@@ -467,21 +450,24 @@ export function createQueries({
sourceId: icao24,
type: 'AIR',
_weldPos: new Cesium.Cartesian3(),
_iconPos: new Cesium.Cartesian3(),
};
flightState._detectionObjects.set(icao24, object);
}
// WELD: anchor to whatever actually owns the visual. A model-owned contact is
// read straight off the translation the fleet tick already wrote into its
// modelMatrix, so bracket and label sit on the aircraft you can see instead of
// on the buried billboard position, which for a grounded plane is ~100 m below
// on the buried icon position, which for a grounded plane is ~100 m below
// and rises only as the coarse ground-floor cell warms. Zero extra sampling and
// no `_modelDisplayPosition` call from postRender. Sprite-owned contacts keep
// `bb.position` — sprite and bracket are co-located there, so association holds.
// no `_modelDisplayPosition` call from postRender. Icon-owned contacts keep
// the drawn icon position — icon and bracket are co-located there.
const spec = modelOwnsVisual
? parts.rendering._modelSpec(info?.klass)
: null;
const pos = isTracked
? parts.motion._trackedVisualCached() || billboardState(bb).position
? parts.motion._trackedVisualCached() ||
icons.position(row, tau, object._iconPos)
: modelOwnsVisual
? modelVisualAnchor(
modelState(model).modelMatrix,
@@ -491,8 +477,15 @@ export function createQueries({
: spec.scale,
object._weldPos || (object._weldPos = new Cesium.Cartesian3()),
)
: billboardState(bb).position;
if (!pos) continue;
: icons.position(row, tau, object._iconPos);
// An icon past the horizon is not drawn, so it is not detectable.
if (
!pos ||
(!isTracked &&
!modelOwnsVisual &&
!parts.icons._aboveHorizon(row, pos, occluder))
)
continue;
object.position = pos;
object.skipLabel = isTracked;
// Card text only — declutter/cohort identity is `object.sourceId` (icao24).
@@ -517,13 +510,9 @@ export function createQueries({
result.push(object);
if (result.length >= maxCount) break;
}
if (
flightState._detectionObjects.size >
flightState._billboards.size + 512
) {
if (flightState._detectionObjects.size > icons.count + 512) {
for (const icao24 of flightState._detectionObjects.keys()) {
if (!flightState._billboards.has(icao24))
flightState._detectionObjects.delete(icao24);
if (!icons.has(icao24)) flightState._detectionObjects.delete(icao24);
}
}
return result;
@@ -583,12 +572,11 @@ export function createQueries({
* @returns {Array<{id: string, icao24: string, callsign: string|null, position: Cesium.Cartesian3, distance: number, aircraftClass: string|null, altitudeM: number|null, velocityMps: number|null, track: number|null}>}
*/
getNearby(center, range, maxCount = 50, { includeHidden = false } = {}) {
if (
!center ||
!flightState._billboardCollection ||
!flightState._billboardCollection.show
)
return [];
const icons = flightState._icons;
if (!center || !icons || !icons.show) return [];
const occluder = parts.icons._occluder();
const tau = parts.icons._displayTau();
const scratch = flightState._scratchNearbyPos;
const limit = Number.isFinite(maxCount)
? Math.max(1, Math.floor(maxCount))
@@ -598,28 +586,30 @@ export function createQueries({
const nearby = [];
for (const [icao24, bb] of flightState._billboards) {
for (let row = 0; row < icons.count; row++) {
const icao24 = icons.idAt(row);
const isTracked = icao24 === flightState._trackedIcao;
// Keep planes rendered as a 3D model (billboard hidden) so proximity counts
// don't drop to zero on the 2D→3D handoff; bb.position stays current while hidden.
const trackedPos = isTracked
? parts.motion._trackedDisplayCached()
: null; // cached, no recompute (anti-jitter)
const drawn = trackedPos || icons.position(row, tau, scratch);
// Keep planes rendered as a 3D model (icon hidden) so proximity counts
// don't drop to zero on the 2D→3D handoff.
if (
!aircraftIncludedInNearby({
isTracked,
billboardShown: billboardState(bb).show,
billboardShown:
icons.shown(row) &&
parts.icons._aboveHorizon(row, drawn, occluder),
modelRendering: parts.rendering._modelOwnsVisual(icao24),
includeHidden,
})
)
continue;
const trackedPos = isTracked
? parts.motion._trackedDisplayCached()
: null; // cached, no recompute (anti-jitter)
const pos = trackedPos || billboardState(bb).position;
if (!pos) continue;
const distance = Cesium.Cartesian3.distance(center, pos);
const distance = Cesium.Cartesian3.distance(center, drawn);
if (distance > maxRange) continue;
const pos = trackedPos || Cesium.Cartesian3.clone(drawn);
const info = flightState.records.data.get(icao24);
const callsign = info?.callsign?.trim() || null;
@@ -651,8 +641,8 @@ export function createQueries({
/**
* Return id/label/position for up to maxCount currently rendered aircraft.
* Cheap snapshot for voice-tool framing — billboard positions only, no
* dead reckoning and no cloning.
* Cheap snapshot for voice-tool framing — drawn icon positions, no
* extra dead reckoning.
* @param {number} [maxCount=500] - Maximum number of entries to return.
* @returns {Array<{id: string, label: string, position: Cesium.Cartesian3, latitude: number, longitude: number, altitudeM: number}>}
*/
@@ -671,34 +661,25 @@ export function createQueries({
* holds no data and therefore cannot answer.
*/
hasContact(icao24) {
if (
!flightState._billboardCollection ||
!flightState._billboardCollection.show ||
flightState._billboards.size === 0
)
return null;
const icons = flightState._icons;
if (!icons || !icons.show || icons.count === 0) return null;
if (!icao24) return false;
const id = String(icao24).trim();
return (
flightState._billboards.has(id) ||
flightState._billboards.has(id.toLowerCase())
);
return icons.has(id) || icons.has(id.toLowerCase());
},
getAllPositions(maxCount = 500) {
if (
!flightState._billboardCollection ||
flightState._billboards.size === 0
)
return [];
const icons = flightState._icons;
if (!icons || icons.count === 0) return [];
const limit = Number.isFinite(maxCount)
? Math.max(1, Math.floor(maxCount))
: 500;
const tau = parts.icons._displayTau();
const result = [];
for (const [icao24, bb] of flightState._billboards) {
const pos = billboardState(bb).position;
if (!pos) continue;
for (let row = 0; row < icons.count; row++) {
const icao24 = icons.idAt(row);
const pos = icons.position(row, tau);
const carto = Cesium.Cartographic.fromCartesian(
pos,
Cesium.Ellipsoid.WGS84,
@@ -741,11 +722,7 @@ export function createQueries({
* @returns {Array<Object>} See mapAnalystRecord for the record shape.
*/
getAnalystRecords(maxCount = 2000) {
if (
!flightState._billboardCollection ||
!flightState._billboardCollection.show ||
flightState.records.data.size === 0
)
if (!flightState._icons?.show || flightState.records.data.size === 0)
return [];
const limit = Number.isFinite(maxCount)
? Math.max(1, Math.floor(maxCount))
@@ -773,8 +750,8 @@ export function createQueries({
trackById(icao24, { origin = 'programmatic' } = {}) {
if (!icao24) return false;
let id = String(icao24).trim();
if (!flightState._billboards.has(id)) id = id.toLowerCase();
if (!flightState._billboards.has(id)) return false;
if (!flightState._icons?.has(id)) id = id.toLowerCase();
if (!flightState._icons?.has(id)) return false;
if (parts.tracking._isExplicitTrackingOrigin(origin))
parts.tracking._cancelPendingTrackingRestore();
if (flightState._trackedIcao === id)
@@ -844,7 +821,7 @@ export function createQueries({
)
return false;
let id = String(icao24).trim();
if (!flightState._billboards.has(id)) id = id.toLowerCase();
if (!flightState._icons?.has(id)) id = id.toLowerCase();
if (
id !== flightState._trackedIcao ||
!flightState._viewer.entities?.contains?.(flightState._trackedEntity) ||
+31 -20
View File
@@ -3,11 +3,9 @@ import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import { createQueries } from './queries.js';
import { createRendering } from './rendering.js';
import {
billboardState,
mirrorBillboard,
setBillboard,
} from '../../data/billboardMirror.js';
import { createIcons } from './icons.js';
import { createIconSet } from '../../data/iconSet.js';
import { writeHoldSegment } from '../../data/iconSegment.js';
import { mirrorModel, setModel } from '../../data/modelMirror.js';
function fixture() {
@@ -16,14 +14,11 @@ function fixture() {
registration: ' N123 ',
altitude: 1000,
};
const bb = mirrorBillboard({
show: true,
position: new Cesium.Cartesian3(1, 2, 3),
});
const icons = createIconSet();
const flightState = {
feed: {},
_billboardCollection: { show: true },
_billboards: new Map([['abc123', bb]]),
_icons: icons,
_scratchIconRenderTime: new Cesium.JulianDate(),
_models: new Map(),
_detectionObjects: new Map(),
records: { data: new Map([['abc123', info]]) },
@@ -48,8 +43,27 @@ function fixture() {
motion: { _trackedVisualCached: () => null },
};
parts.rendering = createRendering({ flightState, services, parts });
parts.icons = createIcons({ flightState, services, parts });
const queries = createQueries({ flightState, services, parts }).methods;
return { flightState, info, bb, parts, queries, formats: () => formats };
const row = () => icons.row('abc123');
/** Hold the contact's icon at `position`. */
const place = (position) => {
writeHoldSegment(icons.segments, row(), position, 0);
icons.segmentWritten(row());
};
icons.add('abc123', { id: 'abc123' });
place(new Cesium.Cartesian3(1, 2, 3));
icons.setShown(row(), true);
return {
flightState,
info,
parts,
queries,
formats: () => formats,
place,
show: (shown) => icons.setShown(row(), shown),
drawn: () => parts.icons._iconPosition('abc123'),
};
}
test('candidate text follows in-place enrichment without freezing identity or positions', () => {
@@ -57,9 +71,9 @@ test('candidate text follows in-place enrichment without freezing identity or po
const [first] = f.queries.getDetectableObjects();
assert.equal(first.id, 'TEST123');
assert.equal(first.sourceId, 'abc123');
setBillboard(f.bb, 'position', new Cesium.Cartesian3(4, 5, 6));
f.place(new Cesium.Cartesian3(4, 5, 6));
assert.equal(f.queries.getDetectableObjects()[0], first);
assert.equal(first.position, billboardState(f.bb).position);
assert.deepEqual(first.position, f.drawn());
assert.deepEqual(first.position, new Cesium.Cartesian3(4, 5, 6));
assert.equal(f.formats(), 1);
f.info.callsign = ' NEW456 ';
@@ -107,7 +121,7 @@ test('candidate collection resolves models once and keeps handoffs live', (t) =>
scale: 1,
});
f.flightState._models.set('abc123', model);
setBillboard(f.bb, 'show', false);
f.show(false);
assert.deepEqual(f.queries.getDetectableObjects(), []);
model.ready = true;
assert.equal(f.queries.getDetectableObjects()[0], candidate);
@@ -121,11 +135,8 @@ test('candidate collection resolves models once and keeps handoffs live', (t) =>
assert.deepEqual(candidate.position, new Cesium.Cartesian3(40, 50, 60));
setModel(model, 'show', false);
assert.deepEqual(f.queries.getDetectableObjects(), []);
setBillboard(f.bb, 'show', true);
assert.equal(
f.queries.getDetectableObjects()[0].position,
billboardState(f.bb).position,
);
f.show(true);
assert.deepEqual(f.queries.getDetectableObjects()[0].position, f.drawn());
assert.equal(get.mock.callCount(), 6);
setModel(model, 'show', true);
assert.equal(f.parts.rendering._modelOwnsVisual('abc123'), true);
+153 -386
View File
@@ -1,22 +1,14 @@
import * as Cesium from 'cesium';
import { cyberSonarBaseAlpha } from '../../cyberSonar.js';
import { selectModelEligible } from '../../data/modelEligibility.js';
import { civilAircraftModelSpec } from './modelSpec.js';
import { CLASS_SCALE_2D } from '../../data/aircraftClass.js';
import { isEmergency } from '../../data/aircraftEmergency.js';
import { cockpitContactDotImage } from '../../data/cockpitContactDot.js';
import { aircraftIcon, TRACKED_ICON_PX } from '../../data/aircraftIcons.js';
import {
cameraPoseSignature,
horizonOccluder,
screenProjectedRotation,
} from '../../data/iconOrientation.js';
import { limitCourseStep, courseSlewCapDps } from '../../data/motionModel.js';
import { horizonOccluder } from '../../data/iconOrientation.js';
import { aircraftTreatmentAlpha } from '../../data/aircraftRecession.js';
import {
MIL_TINT,
EMERGENCY_TINT,
GROUND_SCALE,
COCKPIT_CONTACT_SIZE_PX,
COCKPIT_CIVILIAN_COLOR,
MODEL_ALT_CEIL_M,
MODEL_MAX_ALL,
@@ -32,14 +24,9 @@ import {
TRACKED_MODEL_MIN_PX,
TRACKED_MODEL_MAX_PX,
FLEET_DR_INTERVAL_MS,
RENDER_DELAY_SEC,
COURSE_SLEW_DT_MAX_SEC,
ROTATION_REFRESH_MS,
COURSE_MAX_DPS,
} from './policy.js';
import { AIRCRAFT_ENVIRONMENT_MAP_OPTIONS } from '../../data/aircraftModelLighting.js';
import { aircraftVisuals } from '../aircraft/visualState.js';
import { billboardState, setBillboard } from '../../data/billboardMirror.js';
import { modelState, setModel } from '../../data/modelMirror.js';
export function createRendering({
@@ -52,14 +39,8 @@ export function createRendering({
const { isMilitaryIcao } = services.militaryRegistry;
const { tr3bIconKind, isTr3b } = services.aircraftPresentation;
const { trackedModelScaleForPixelCap } = services.camera;
const {
focusNowMs,
getFocusTarget,
nearFarScalarValueAtDistance,
advanceProjectedSpriteFocus,
} = services.focus;
const { applyAircraftBillboardTreatment, applyAircraftModelTreatment } =
services.recession;
const { focusNowMs, getFocusTarget } = services.focus;
const { applyAircraftModelTreatment } = services.recession;
/** True while this contact's latest poll broadcast an emergency code. */
@@ -98,22 +79,6 @@ export function createRendering({
);
}
/** Depth-test policy for aircraft billboards. Round 5 (owner directive
* 2026-07-06: "I just want the planes and their lines to ALWAYS be
* visible... evenly applied"): EVERY contact renders depth-test-free at
* every distance — grounded, low, and airborne alike. The photoreal mesh
* writes depth and residual baro/floor error will always leave some sprite
* geometry at or below it; a uniform rule beats the grounded-only /
* low-AGL-only conditions that kept leaving classes of contacts buried
* (2026-07-03 Van Nuys grounded case; 2026-07-06 Austin QNH-below-field
* case). Far-side planes are still removed by the fleet tick's horizon
* occluder, which never depended on depth. Kept as a function so the
* callers' restyle sites stay diff-stable. */
function _groundDepthDistance() {
return Number.POSITIVE_INFINITY;
}
function _modelSpec(klass) {
const cached = flightState._specCache.get(klass);
if (cached) return cached;
@@ -141,53 +106,6 @@ export function createRendering({
const _iconKind = (icao24, klass) =>
tr3bIconKind(icao24, klass, { hot: flightState._irBoost });
/** Apply the current normal/cockpit visual contract to one owned fleet billboard. */
function _applyFleetBillboardPresentation(icao24, bb) {
if (!bb) return;
const limbScale = flightState._billboardLimbScale.get(bb) ?? 1;
const isCockpitContact =
flightState._cockpitContactMode && icao24 !== flightState._trackedIcao;
const isCockpitNear =
isCockpitContact && flightState._cockpitNearContacts.has(icao24);
if (isCockpitContact && !isCockpitNear) {
const freshnessAlpha = cyberSonarBaseAlpha(bb);
bb.image = cockpitContactDotImage();
setBillboard(bb, 'width', COCKPIT_CONTACT_SIZE_PX);
setBillboard(bb, 'height', COCKPIT_CONTACT_SIZE_PX);
setBillboard(bb, 'scale', limbScale);
setBillboard(bb, 'scaleByDistance', _cockpitBillboardScaleByDistance());
bb.color = _cockpitContactColor(icao24).withAlpha(freshnessAlpha);
setBillboard(bb, 'rotation', 0);
return;
}
const meta = flightState.records.data.get(icao24);
bb.image = aircraftIcon(
_iconKind(icao24, meta?.klass),
aircraftVisuals.read(bb)?.iconLarge ? TRACKED_ICON_PX : undefined,
);
setBillboard(bb, 'width', icao24 === flightState._trackedIcao ? 24 : 20);
setBillboard(bb, 'height', icao24 === flightState._trackedIcao ? 24 : 20);
setBillboard(
bb,
'scale',
_fleetBillboardScale(icao24, meta?.klass) * limbScale,
);
setBillboard(bb, 'scaleByDistance', _normalBillboardScaleByDistance());
bb.color = _fleetBillboardColor(icao24).withAlpha(cyberSonarBaseAlpha(bb));
}
/**
* Per-preRender fleet pass at ~12Hz: dead-reckons every untracked billboard,
* horizon-culls billboards beyond the limb (no far-side depth with the globe
* hidden), and refreshes screen-projected icon rotations whenever the camera
* pose changed (plus a 1s drift catch-up while idle). Driven by
* scene.preRender — NOT camera.changed, whose granularity is globally
* degraded by other layers mutating camera.percentageChanged.
* @returns {void}
*/
/** Model tint, mirroring the billboard color rules. */
function _modelColor(icao24) {
@@ -272,12 +190,12 @@ export function createRendering({
/** Everything scene.sampleHeight must NOT hit when snapping a grounded model: the
* vertical pick ray at a plane's own lat/lon otherwise lands on its (or a parked
* neighbor's) billboard/model instead of the tile skin. Cesium's ray-pick exclusion
* matches picked-object IDs, and every billboard AND model in this layer carries its
* matches picked-object IDs, and every icon AND model in this layer carries its
* icao as `id`, so the icao strings cover both; the tracked entity is excluded as the
* object itself. Built lazily — only when a sample actually fires (one-shot). */
function _groundSampleExclusions() {
const out = [...flightState._billboards.keys()];
const out = parts.icons._iconIds();
if (flightState._trackedEntity) out.push(flightState._trackedEntity);
return out;
}
@@ -323,15 +241,23 @@ export function createRendering({
);
}
/** Atomically hand one fleet contact from its billboard to a safely placed,
/** Atomically hand one fleet contact from its icon to a safely placed,
* ready 3D model. Missing/loading models and unresolved terrain always leave
* the billboard owning the visual, so no render frame can hide both.
* the icon owning the visual, so no render frame can hide both.
* `beforeShow` runs only on the committing path — the per-tick model treatment
* belongs to a model that is about to draw, not to one still waiting. */
function _driveFleetModelHandoff(icao24, model, bb, pos, course, beforeShow) {
function _driveFleetModelHandoff(
icao24,
model,
row,
pos,
course,
beforeShow,
) {
const icons = flightState._icons;
if (!model) {
setBillboard(bb, 'show', true);
icons.setShown(row, true);
return false;
}
const displayPos = _modelDisplayPosition(
@@ -341,7 +267,7 @@ export function createRendering({
);
if (!displayPos) {
setModel(model, 'show', false); // no ground evidence → nothing safe to place a depth-tested model at
setBillboard(bb, 'show', true);
icons.setShown(row, true);
return false;
}
// The matrix is written BEFORE the readiness test on purpose: a model can flip
@@ -350,12 +276,12 @@ export function createRendering({
setModel(model, 'modelMatrix', _modelMatrix(displayPos, course));
if (!model.ready) {
setModel(model, 'show', false); // not loaded yet → keep the 2D icon, no half-model flash
setBillboard(bb, 'show', true);
icons.setShown(row, true);
return false;
}
beforeShow?.();
if (!modelState(model).show) setModel(model, 'show', true);
if (billboardState(bb).show) setBillboard(bb, 'show', false); // hand off ONLY once the model renders
icons.setShown(row, false); // hand off ONLY once the model renders
return true;
}
@@ -443,9 +369,6 @@ export function createRendering({
(current && aircraftVisuals.read(current)?.specKey !== key) ||
(!current && flightState._modelPending.has(icao24))
) {
const bb = flightState._billboards.get(icao24);
if (bb && icao24 !== flightState._trackedIcao)
setBillboard(bb, 'show', true);
_releaseModel(icao24);
}
if (
@@ -479,9 +402,6 @@ export function createRendering({
aircraftVisuals.read(model)?.irBoost === flightState._irBoost
)
continue; // already right state
const bb = flightState._billboards.get(icao);
if (bb && icao !== flightState._trackedIcao)
setBillboard(bb, 'show', true);
_releaseModel(icao);
}
if (flightState._irReloadQueue.length === 0)
@@ -624,6 +544,7 @@ export function createRendering({
}
}
flightState._models.delete(icao24);
_showReleasedIcon(icao24);
}
// Do NOT clear _modelPending here — the in-flight load's OWN post-await removes it. Keeping
// it (a) prevents a duplicate load from starting and (b) keeps the bumped gen entry alive so
@@ -664,9 +585,20 @@ export function createRendering({
}
}
}
for (const icao24 of flightState._models.keys()) _showReleasedIcon(icao24);
flightState._models.clear();
}
/** A released fleet model hands the visual straight back to its icon (the
* tracked contact's entity keeps its own). */
function _showReleasedIcon(icao24) {
const icons = flightState._icons;
const row = icons?.row(icao24) ?? -1;
if (row >= 0 && icao24 !== flightState._trackedIcao)
icons.setShown(row, true);
}
/** Destroy the standalone tracked-aircraft model and invalidate any in-flight load. */
function _releaseTrackedModel() {
@@ -709,8 +641,7 @@ export function createRendering({
// readout consumers then reuse that exact cached position.
const pos =
parts.motion._trackedDisplayPosition(flightState._trackedIcao) ||
billboardState(flightState._billboards.get(flightState._trackedIcao))
?.position;
parts.icons._iconPosition(flightState._trackedIcao);
if (!pos) {
if (flightState._trackedModel)
setModel(flightState._trackedModel, 'show', false);
@@ -789,9 +720,7 @@ export function createRendering({
// default here produces a one-frame jump to the Earth's center.
const currentPos =
parts.motion._trackedDisplayCached() ||
billboardState(
flightState._billboards.get(flightState._trackedIcao),
)?.position;
parts.icons._iconPosition(flightState._trackedIcao);
if (currentPos) {
const displayPos = _modelDisplayPosition(
flightState._trackedIcao,
@@ -859,54 +788,40 @@ export function createRendering({
}
}
/**
* ~12 Hz fleet pass. Motion, rotation, the horizon cull, limb recession and
* the two-tier glyph swap all run on the GPU from each row's segment, so this
* only re-plans rows whose regime ended, pins grounded contacts to the floor
* under them, advances focus de-emphasis while a target exists, and drives
* the 3D-model handoff inside the model regime. Driven by scene.preRender.
* @returns {void}
*/
function _fleetTick() {
if (
!flightState._viewer ||
!flightState._billboardCollection ||
!flightState._billboardCollection.show
)
return;
const icons = flightState._icons;
if (!flightState._viewer || !icons || !icons.show) return;
const scene = flightState._viewer.scene;
const camera = flightState._viewer.camera;
const nowMs = focusNowMs(Date.now());
// (The tracked trail head is now the per-frame _trailHeadEntity segment — no 1 Hz
// primitive rebuild needed here anymore.)
if (nowMs - flightState._lastFleetTickMs < FLEET_DR_INTERVAL_MS) return;
const tickDtSec = flightState._lastFleetTickMs
? Math.min(
COURSE_SLEW_DT_MAX_SEC,
(nowMs - flightState._lastFleetTickMs) / 1000,
)
: 0.08;
flightState._lastFleetTickMs = nowMs;
// Every contact in this pass shares the same delayed display instant.
const renderTime = Cesium.JulianDate.addSeconds(
Cesium.JulianDate.now(flightState._scratchFleetRenderTime),
-RENDER_DELAY_SEC,
const renderTime = parts.icons._displayTime(
flightState._scratchFleetRenderTime,
);
const tau = icons.seconds(renderTime);
_drainIrReloadQueue(); // bounded per-tick slice of any pending boost-flip reload
if (flightState._cockpitContactMode)
parts.tracking._refreshCockpitNearContacts();
const poseSig = cameraPoseSignature(camera);
// Only the nearby Cockpit silhouettes need projected course; far dots are
// rotation-free. The per-contact gate below keeps the pip path cheap.
const doRotations =
poseSig !== flightState._lastCamPoseSig ||
nowMs - flightState._lastRotPassMs >= ROTATION_REFRESH_MS;
if (doRotations) {
flightState._lastCamPoseSig = poseSig;
flightState._lastRotPassMs = nowMs;
}
const occluder = horizonOccluder(camera);
parts.icons._replanExpired(tau);
parts.icons._floorGroundedIcons(renderTime, tau, nowMs, camera.positionWC);
const focusTarget = getFocusTarget();
parts.icons._focusIcons(tau, scene, camera, nowMs, focusTarget);
// 3D model regime: only when enabled AND the camera is zoomed in past the altitude ceiling.
// Drop all models the moment we leave it (toggled off / zoomed out) so billboards resume.
// Drop all models the moment we leave it (toggled off / zoomed out) so icons resume.
const useModels = _modelRegimeActive();
// Drop live models AND invalidate in-flight loads on leaving the regime (else a load that
// resolves after zoom-out could briefly add a model outside the 3D-model regime).
@@ -915,6 +830,7 @@ export function createRendering({
(flightState._models.size || flightState._modelPending.size)
)
_releaseModels();
if (!useModels) return;
// 3D-model eligibility: by DISTANCE (the mode's add/keep band), with ON-SCREEN PRIORITY under the
// cap. FOUR passes, visible-first, so the slots are spent on what you can see: (1) KEEP on-screen
@@ -925,273 +841,124 @@ export function createRendering({
// on-screen plane that wants one (passes 1–2) — a single KEEP-everything pass could fill the cap
// with off-screen retained models. Pure-distance let off-screen planes eat the cap; pure-frustum
// filtering dropped near off-screen planes entirely. This does both right.
let modelEligible = null;
if (useModels) {
const cap = _modelCap();
const camPos = camera.positionWC;
const addM = _modelAddDistM();
const addDistSq = addM * addM;
const keepM = _modelKeepDistM();
const keepDistSq = keepM * keepM;
const cull = camera.frustum.computeCullingVolume(
camPos,
camera.directionWC,
camera.upWC,
const cap = _modelCap();
const camPos = camera.positionWC;
const addM = _modelAddDistM();
const addDistSq = addM * addM;
const keepM = _modelKeepDistM();
const keepDistSq = keepM * keepM;
const cull = camera.frustum.computeCullingVolume(
camPos,
camera.directionWC,
camera.upWC,
);
// Candidates = planes within the KEEP radius, nearest first, each tagged with on-screen-ness.
const cand = [];
for (let row = 0; row < icons.count; row++) {
const icao = icons.idAt(row);
if (icao === flightState._trackedIcao) continue;
// A converted TR-3B renders as an icon and can never take a model, so
// it must not occupy a CAP SLOT either — excluded here at selection time,
// not just at the handoff below, or accumulated conversions would starve
// ordinary contacts of 3D models. (The handoff guard stays as defence.)
if (isTr3b(icao)) continue;
// Ground planes compete for model slots like everyone else (owner decision
// 2026-07-03: "3D mode is respected regardless of whether a plane is on the
// ground or in the air — no distinction"). The cap + nearest-first ordering
// below already bound airport clusters; grounded placement is handled by the
// one-shot ground snap in _modelDisplayPosition.
const position = icons.position(
row,
tau,
flightState._scratchModelBS.center,
);
// Candidates = planes within the KEEP radius, nearest first, each tagged with on-screen-ness.
const cand = [];
for (const [icao, bb] of flightState._billboards) {
if (icao === flightState._trackedIcao) continue;
// A converted TR-3B renders as a billboard and can never take a model, so
// it must not occupy a CAP SLOT either — excluded here at selection time,
// not just at the handoff below, or accumulated conversions would starve
// ordinary contacts of 3D models. (The handoff guard stays as defence.)
if (isTr3b(icao)) continue;
// Ground planes compete for model slots like everyone else (owner decision
// 2026-07-03: "3D mode is respected regardless of whether a plane is on the
// ground or in the air — no distinction"). The cap + nearest-first ordering
// below already bound airport clusters; grounded placement is handled by the
// one-shot ground snap in _modelDisplayPosition.
const { position } = billboardState(bb);
const d2 = Cesium.Cartesian3.distanceSquared(camPos, position);
if (d2 > keepDistSq) continue; // beyond the keep radius → never eligible
Cesium.Cartesian3.clone(position, flightState._scratchModelBS.center);
cand.push([
icao,
d2,
cull.computeVisibility(flightState._scratchModelBS) !==
Cesium.Intersect.OUTSIDE,
]);
}
cand.sort((a, b) => a[1] - b[1]); // nearest first
if (cand.length > cap && nowMs - flightState._lastModelCapWarnMs > 5000) {
console.warn(
`[Data:Flights] ${cand.length} planes in 3D range; capped at ${cap} (${flightState._models3dMode}). On-screen planes are prioritized.`,
);
flightState._lastModelCapWarnMs = nowMs;
}
modelEligible = selectModelEligible(cand, {
cap,
addDistSq,
isModeled: (icao) => flightState._models.has(icao),
});
const toRelease = [];
for (const icao of flightState._models.keys()) {
if (icao !== flightState._trackedIcao && !modelEligible.has(icao))
toRelease.push(icao);
}
for (const icao of toRelease) _releaseModel(icao);
const d2 = Cesium.Cartesian3.distanceSquared(camPos, position);
if (d2 > keepDistSq) continue; // beyond the keep radius → never eligible
cand.push([
icao,
d2,
cull.computeVisibility(flightState._scratchModelBS) !==
Cesium.Intersect.OUTSIDE,
]);
}
for (const [icao24, bb] of flightState._billboards) {
if (icao24 === flightState._trackedIcao) continue; // tracked entity owns its own motion
const icon = billboardState(bb);
const info = flightState.records.data.get(icao24);
const dr = parts.motion._deadReckon(
icao24,
flightState._scratchFleetPos,
renderTime,
cand.sort((a, b) => a[1] - b[1]); // nearest first
if (cand.length > cap && nowMs - flightState._lastModelCapWarnMs > 5000) {
console.warn(
`[Data:Flights] ${cand.length} planes in 3D range; capped at ${cap} (${flightState._models3dMode}). On-screen planes are prioritized.`,
);
// The dead-reckoned point drifts away from the fix whose cell supplied the
// height, so a grounded contact's sprite ends up under the mesh it taxied
// (or coasted) over. Re-floor at the DISPLAYED coordinate — read-only, and
// never while a 3D model owns the visual (T7).
const display = parts.motion._floorGroundedDisplayPosition(
icao24,
info,
dr,
_modelOwnsVisual(icao24),
nowMs,
);
// Gate the write — assigning Billboard.position dirties the whole
// collection's vertex buffer, so skip sub-meter moves.
if (
display &&
Cesium.Cartesian3.distanceSquared(display, icon.position) > 1.0
) {
setBillboard(bb, 'position', display);
}
flightState._lastModelCapWarnMs = nowMs;
}
const modelEligible = selectModelEligible(cand, {
cap,
addDistSq,
isModeled: (icao) => flightState._models.has(icao),
});
const toRelease = [];
for (const icao of flightState._models.keys()) {
if (icao !== flightState._trackedIcao && !modelEligible.has(icao))
toRelease.push(icao);
}
for (const icao of toRelease) _releaseModel(icao);
// Round 6: occlusion-test a LIFTED point for contacts rendering below
// (or within a wingspan of) the ellipsoid — EllipsoidalOccluder judges a
// sub-ellipsoid point near the limb "beyond the horizon" and the fleet
// pass would hide a plane that is really just low over high-N terrain
// waiting for its floor to warm (ATL grounded contacts at geoid −31 m).
const beyondHorizon = !occluder.isPointVisible(
flightState._cullPositions.get(icao24) || icon.position,
);
// A billboard flipping INTO view (horizon reveal while the camera idles)
// gets its rotation refreshed THIS tick even without a pose change —
// otherwise it reappears wearing its stale (often creation-north) nose for
// up to ROTATION_REFRESH_MS. (Model-handed-off planes also read show=false
// here; harmless — the model branch below `continue`s past the rotation.)
const revealed = !beyondHorizon && !icon.show;
if (icon.show === beyondHorizon) setBillboard(bb, 'show', !beyondHorizon);
if (beyondHorizon) {
// Also hide any 3D model — otherwise a model that crossed the limb would keep
// rendering through the hidden globe at its last matrix.
// 3D model takes over from the icon for eligible planes. GAP-PROOF: the icon stays shown
// until the model is actually READY to render, so a plane is never both iconless AND
// modelless (the "planes vanish when 3D turns on" bug). Position the model every tick
// regardless so it's framed the instant it becomes ready. Converted TR-3Bs stay 2D on
// purpose: the Easter egg IS the triangle, and there is no GLB for it.
const occluder = horizonOccluder(camera);
for (const icao24 of modelEligible) {
if (isTr3b(icao24)) continue;
const row = icons.row(icao24);
if (row < 0) continue;
const position = icons.position(row, tau, flightState._scratchFleetPos);
if (!parts.icons._aboveHorizon(row, position, occluder)) {
// A model that crossed the limb would keep rendering through the
// hidden globe at its last matrix; the icon's own cull is on the GPU.
const m = flightState._models.get(icao24);
if (m && modelState(m)?.show) setModel(m, 'show', false);
continue;
}
// One order-independent write site composes freshness × focus × limb haze
// for alpha, and base class/ground scale × limb taper for scale. Cesium's
// locked NearFarScalar remains a separate multiplicative stage. This
// narrowly amends always-visible rendering without count culling or zero
// alpha; nearer focus behavior remains tunable rather than universal.
const cameraDistanceM = Cesium.Cartesian3.distance(
camera.positionWC,
icon.position,
_ensureModel(icao24);
const model = flightState._models.get(icao24);
const cameraDistanceM = Cesium.Cartesian3.distance(camPos, position);
_driveFleetModelHandoff(
icao24,
model,
row,
position,
icons.course(row, tau),
() => {
applyAircraftModelTreatment({
model,
// IR boost must survive the per-tick treatment write — otherwise
// any alpha change would repaint the ordinary tint over the hot
// white. Boosted models also skip the recession fade: hot targets
// stay full-strength at any range (icons keep their normal
// fade — full-opacity glyph walls read as overwhelming).
baseColor: flightState._irBoost
? Cesium.Color.WHITE
: _modelColor(icao24),
alpha: flightState._irBoost
? 1
: aircraftTreatmentAlpha({
baseAlpha: icons.alpha(row),
focusFactor: icons.focus(row),
cameraDistanceM,
cameraHeightM: camera.positionCartographic?.height,
}),
});
},
);
const distanceScale = nearFarScalarValueAtDistance(
icon.scaleByDistance,
cameraDistanceM,
);
const focus = advanceProjectedSpriteFocus(
bb,
icon.position,
scene,
camera,
nowMs,
focusTarget,
undefined,
(icon.width || 20) * (icon.scale || 1) * distanceScale * 0.5,
(icon.height || 20) * (icon.scale || 1) * distanceScale * 0.5,
);
const isCockpitNear =
flightState._cockpitContactMode &&
flightState._cockpitNearContacts.has(icao24);
const layerBaseColor =
flightState._cockpitContactMode && !isCockpitNear
? _cockpitContactColor(icao24, info)
: _fleetBillboardColor(icao24, info);
const baseColor = layerBaseColor;
const treatment = applyAircraftBillboardTreatment({
billboard: bb,
baseScale:
flightState._cockpitContactMode && !isCockpitNear
? 1
: _fleetBillboardScale(icao24, info?.klass),
baseAlpha: flightState.records.missingPolls.get(icao24) ? 0.45 : 1,
baseColor,
focusFactor: focus.factor,
cameraDistanceM,
cameraHeightM: camera.positionCartographic?.height,
});
flightState._billboardLimbScale.set(bb, treatment.factors.scale);
// Two-tier glyph raster (owner playtest 2026-08-16): the billboard atlas
// has no mipmaps, so no single texture stays crisp across the ~25–150
// device-px range scaleByDistance produces. Swap between the 64 px fleet
// raster and the 192 px close raster on the billboard's ACTUAL on-screen
// size — post-treatment bb.scale, so focus/limb recession counts — with
// hysteresis so zoom oscillation never thrashes the atlas.
if (!flightState._cockpitContactMode || isCockpitNear) {
const glyphDevPx =
(icon.width || 20) *
(icon.scale || 1) *
distanceScale *
(globalThis.devicePixelRatio || 1);
const visual = aircraftVisuals.write(bb);
const wantLarge = visual.iconLarge ? glyphDevPx > 56 : glyphDevPx > 76;
if (wantLarge !== !!visual.iconLarge) {
visual.iconLarge = wantLarge;
bb.image = aircraftIcon(
_iconKind(icao24, info?.klass),
wantLarge ? TRACKED_ICON_PX : undefined,
);
}
}
// Smoothed display course: the path direction _deadReckon just reported
// for THIS aircraft (nothing else calls _deadReckon in between), rate-
// limited so segment-boundary course steps glide instead of snapping.
// The slew cap eases toward COURSE_MIN_DPS at low speed, and a hovering
// aircraft (hold flag) keeps its previous nose direction outright.
const rawCourse =
flightState._drCourseDeg != null
? flightState._drCourseDeg
: (info && info.true_track) || 0;
const prevCourse = flightState._displayCourse.get(icao24);
const course =
flightState._drCourseHold && prevCourse != null
? prevCourse
: limitCourseStep(
prevCourse,
rawCourse,
courseSlewCapDps(
flightState._drSpeedMps != null
? flightState._drSpeedMps
: ((info && info.velocity) ?? NaN),
COURSE_MAX_DPS,
),
tickDtSec,
);
flightState._displayCourse.set(icao24, course);
// 3D model takes over from the billboard for in-view planes (modelEligible). GAP-PROOF: the
// billboard stays shown until the model is actually READY to render, so a plane is never both
// iconless AND modelless (the "planes vanish when 3D turns on" bug). Position the model every
// tick regardless so it's framed the instant it becomes ready.
// Converted TR-3Bs stay 2D on purpose: the Easter egg IS the triangle, and
// there is no GLB for it, so the model handoff is suppressed rather than
// fed a stand-in mesh. The billboard keeps rendering (and keeps satisfying
// the getNearby/getDetectableObjects `bb.show` visibility guards), so a
// converted contact still works in Contacts and Cockpit.
if (useModels && dr && modelEligible.has(icao24) && !isTr3b(icao24)) {
_ensureModel(icao24);
const model = flightState._models.get(icao24);
const ownsVisual = _driveFleetModelHandoff(
icao24,
model,
bb,
dr,
course,
() => {
applyAircraftModelTreatment({
model,
// IR boost must survive the per-tick treatment write — otherwise
// any alpha change would repaint the ordinary tint over the hot
// white. Boosted models also skip the recession fade: hot targets
// stay full-strength at any range (billboards keep their normal
// fade — full-opacity glyph walls read as overwhelming).
baseColor: flightState._irBoost
? Cesium.Color.WHITE
: _modelColor(icao24),
alpha: flightState._irBoost ? 1 : treatment.alpha,
});
},
);
if (ownsVisual) continue; // skip billboard rotation
}
if (
(!flightState._cockpitContactMode || isCockpitNear) &&
(doRotations || revealed)
) {
const rot = screenProjectedRotation(
scene,
icon.position,
course,
icon.rotation,
);
if (rot !== null && Math.abs(rot - icon.rotation) > 0.002) {
setBillboard(bb, 'rotation', rot);
}
}
}
}
return {
_fleetBillboardColor,
_fleetBillboardScale,
_groundDepthDistance,
_cockpitContactColor,
_modelSpec,
_normalBillboardScaleByDistance,
_cockpitBillboardScaleByDistance,
_iconKind,
_applyFleetBillboardPresentation,
_modelColor,
_modelRegimeActive,
_modelCap,
+28 -99
View File
@@ -1,13 +1,10 @@
import * as Cesium from 'cesium';
import { cyberSonarBaseAlpha } from '../../cyberSonar.js';
import {
turnRateFromFixHistory,
liftRepeatedGroundFix,
synthesizeForwardKinematicsFix,
} from '../../data/motionModel.js';
import { aircraftIcon } from '../../data/aircraftIcons.js';
import { POSITION_HISTORY_LIMIT } from './recordPolicy.js';
import { addBillboard } from '../../data/billboardMirror.js';
/** Apply reconciled aircraft records to Cesium resources and follow state. */
export function createFlightSnapshotRenderer({
@@ -19,6 +16,7 @@ export function createFlightSnapshotRenderer({
rendering,
tracking,
motion,
icons,
enrichment,
queries,
}) {
@@ -53,17 +51,8 @@ export function createFlightSnapshotRenderer({
// which hands off on untrack).
const isMil = isMilitaryIcao(icao24);
if (isMil && tracking._militaryLayerSuppresses(icao24)) {
const dupe = flightState._billboards.get(icao24);
if (dupe) {
flightState._billboardCollection.remove(dupe);
flightState._billboards.delete(icao24);
rendering._releaseModel(icao24); // military-suppression: drop any 3D model too
records.forget(icao24);
flightState._cullPositions.delete(icao24);
flightState._positionHistory.delete(icao24);
flightState._displayCourse.delete(icao24);
flightState._groundSnap.forget(icao24);
}
// Military-suppression: drop the duplicate with any 3D model too.
if (flightState._icons.has(icao24)) icons._forgetContact(icao24);
continue;
}
@@ -84,12 +73,12 @@ export function createFlightSnapshotRenderer({
);
// Either flip retires the previous one-shot ground sample.
if (groundFlipped) flightState._groundSnap.forget(icao24);
flightState._cullPositions.set(
icao24,
meta.renderAltitudeM < 10
? Cesium.Cartesian3.fromDegrees(meta.rawLon, meta.rawLat, 12)
: null,
);
if (meta.onGround) {
flightState._grounded.add(icao24);
} else if (flightState._grounded.delete(icao24)) {
flightState._floorPinned.delete(icao24);
motion._retireDisplayFloorState(icao24, Date.now());
}
const isTracked = icao24 === flightState._trackedIcao;
// Append to position history stamped with the FEED's fix epoch
@@ -153,72 +142,18 @@ export function createFlightSnapshotRenderer({
}
}
if (flightState._billboards.has(icao24)) {
const bb = flightState._billboards.get(icao24);
// Reclassify if the category resolved/changed (first extended poll);
// a ground flip (landing/takeoff) re-scales the SAME billboard in
// place — the transition is a restyle, never a removal.
// Round 5: depth policy is uniform (always depth-test-free, see
// _groundDepthDistance) — nothing to flip on landing/takeoff.
// Position AND rotation are owned by the fleet pass (_fleetTick);
// course changes land on the next rotation pass (forced below).
if (!isTracked) {
// Refresh affiliation hue without clobbering the tick-owned
// freshness × focus × horizon alpha composition.
bb.color = rendering
._fleetBillboardColor(icao24)
.withAlpha(cyberSonarBaseAlpha(bb));
}
if (
prevMeta?.klass !== meta.klass ||
groundFlipped ||
flightState._cockpitContactMode
) {
rendering._applyFleetBillboardPresentation(icao24, bb);
}
if (flightState._icons.has(icao24)) {
// Reclassification, a ground flip (a restyle, never a removal), a
// freshness change and the cockpit contract all land on the row's
// style; motion re-plans from the updated history.
icons._applyIconPresentation(icao24);
icons._planIcon(icao24);
// Poll-path class change (category updates): same model resync rule
// as the enrichment path — the class's GLB/scale may have changed.
if (prevMeta?.klass !== meta.klass) rendering._syncModelToClass(icao24);
} else {
// Start where the fleet pass will draw it: the fleet renders behind
// real time, so a fresh contact's displayed position is its fix
// projected back to that delayed time, and a grounded contact is then
// lifted onto the floor under that delayed position. Creating it at
// the raw fix drew it ahead until the next fleet tick snapped it back;
// skipping the floor started a taxiing contact low and then raised it.
const delayed = motion._deadReckon(icao24, new Cesium.Cartesian3());
const displayed =
(delayed &&
motion._floorGroundedDisplayPosition(
icao24,
flightState.records.data.get(icao24) || meta,
delayed,
rendering._modelOwnsVisual(icao24),
)) ||
position;
const bb = addBillboard(flightState._billboardCollection, {
position: displayed,
image: aircraftIcon(rendering._iconKind(icao24, meta.klass)),
width: isTracked ? 24 : 20,
height: isTracked ? 24 : 20,
scale: rendering._fleetBillboardScale(icao24, meta.klass),
// Screen-projected rotation lands on the next fleet tick.
rotation: 0,
alignedAxis: Cesium.Cartesian3.ZERO,
color: isTracked
? Cesium.Color.CYAN
: rendering._fleetBillboardColor(icao24),
sizeInMeters: false,
scaleByDistance: rendering._normalBillboardScaleByDistance(),
// Grounded/near-surface planes sit at/below the photoreal tile
// surface — render them depth-test-free so they never vanish up
// close (_groundDepthDistance).
disableDepthTestDistance: rendering._groundDepthDistance(),
id: icao24,
show: !isTracked, // hidden if currently tracked (entity replaces it)
});
flightState._billboards.set(icao24, bb);
rendering._applyFleetBillboardPresentation(icao24, bb);
// Hidden while tracked: the tracked entity replaces it.
icons._addIcon(icao24);
}
// Takeoff while TRACKED: ground traffic drew no trail, so start one
@@ -240,8 +175,12 @@ export function createFlightSnapshotRenderer({
// EXCEPTION: likely-landed planes (last fix low + slow) get only
// LANDED_MISSING_POLL_LIMIT — their disappearance means "landed", not a
// feed gap, and the full grace left phantom planes parked at airports.
for (const [icao24, bb] of flightState._billboards) {
if (currentIcaos.has(icao24)) continue;
const absentIds = [];
for (let row = 0; row < flightState._icons.count; row++) {
const icao24 = flightState._icons.idAt(row);
if (!currentIcaos.has(icao24)) absentIds.push(icao24);
}
for (const icao24 of absentIds) {
const absence = records.absence(icao24, {
complete: snapshot.complete,
retainIds: snapshot.retainIds,
@@ -249,8 +188,9 @@ export function createFlightSnapshotRenderer({
});
if (absence === 'retain') continue;
if (absence === 'stale') {
icons._applyIconPresentation(icao24); // the faded freshness alpha
if (icao24 === flightState._trackedIcao && flightState._trackedEntity) {
// Honest readout: the tracked plane has no faded billboard (its
// Honest readout: the tracked plane has no faded icon (its
// entity owns the visual), so refresh the label — with icao24 now
// in _missingPolls, _trackedLabelText appends the STALE cue so
// last-known velocity/altitude aren't presented as live.
@@ -269,20 +209,9 @@ export function createFlightSnapshotRenderer({
tracking._clearTracking(false, { evicted: true });
}
flightState._billboardCollection.remove(bb);
flightState._billboards.delete(icao24);
rendering._releaseModel(icao24); // aged-out aircraft: drop its 3D model (no orphan / cap leak)
records.forget(icao24);
flightState._cullPositions.delete(icao24);
flightState._positionHistory.delete(icao24);
flightState._displayCourse.delete(icao24);
flightState._groundSnap.forget(icao24);
flightState._displayFloorState.delete(icao24);
icons._forgetContact(icao24); // aged out: its 3D model goes too (no orphan / cap leak)
}
// Fresh courses arrived — force a rotation pass on the next fleet tick
flightState._lastCamPoseSig = '';
// Ambient type enrichment: give ON-SCREEN planes real types (bounded
// sweep — see _sweepAmbientEnrichment; internally fail-silent).
enrichment._sweepAmbientEnrichment();
@@ -307,7 +236,7 @@ export function createFlightSnapshotRenderer({
// aircraft instead of the pavement.
sampleMeshFloorCells(flightState._viewer?.scene, floorWarmPoints, {
excludeObjects: [
...flightState._billboards.values(),
...icons._iconIds(),
...flightState._models.values(),
flightState._trackedModel,
].filter(Boolean),
@@ -327,6 +256,6 @@ export function createFlightSnapshotRenderer({
// in place is safe (the DR extrapolates a zero-velocity fix).
motion._refloorStaleGroundedContacts(currentIcaos);
return { count: flightState._billboards.size, ids: acceptedSnapshotIcaos };
return { count: flightState._icons.count, ids: acceptedSnapshotIcaos };
};
}
+26 -16
View File
@@ -11,7 +11,6 @@ export function createFlightState({ source, services }) {
...services.geoid,
...services.groundFloor,
});
flightState._cullPositions = new Map();
/** Per-class model spec. Hangar-fleet classes (CLASS_MODEL_REAL) ship GLBs
* vertex-baked to real-world METERS in the airplane.glb axis convention, so
@@ -91,22 +90,37 @@ export function createFlightState({ source, services }) {
1.0,
);
// frustum-visibility test
/** Last limb taper per billboard, retained across class/ground/cockpit repaints. */
flightState._billboardLimbScale = new WeakMap();
// keep last N positions per aircraft
// ---------------------------------------------------------------------------
// Module-level state: billboard collection and per-aircraft lookup maps
// ---------------------------------------------------------------------------
/** @type {Cesium.BillboardCollection|null} */
/** Every contact's icon row (see icons.js); null outside a layer lifetime. */
flightState._billboardCollection = null;
flightState._icons = null;
/** @type {Map<string, Cesium.Billboard>} icao24 -> billboard primitive */
/** The primitive drawing `_icons`. */
flightState._billboards = new Map();
flightState._iconPrimitive = null;
flightState._scratchIconRenderTime = new Cesium.JulianDate();
/** Contacts whose latest poll put them on the ground. */
flightState._grounded = new Set();
/** Grounded contacts the floor currently lifts; their rows hold CPU positions. */
flightState._floorPinned = new Set();
/** Fleet ticks since start; far grounded rows take their floor turn by it. */
flightState._floorTick = 0;
/** Rows still away from full focus emphasis after the last focus pass. */
flightState._iconFocusActive = 0;
/** Stable lightweight records reused by the detection overlay between polls. */
@@ -204,14 +218,6 @@ export function createFlightState({ source, services }) {
flightState._lastFleetTickMs = 0;
/** @type {string} Camera pose signature at the last rotation pass */
flightState._lastCamPoseSig = '';
/** @type {number} Epoch ms of the last full rotation pass */
flightState._lastRotPassMs = 0;
/** @type {number} Last computed rotation for the tracked entity (radians) */
flightState._lastTrackedRotation = 0;
@@ -250,6 +256,10 @@ export function createFlightState({ source, services }) {
flightState._scratchFleetPos = new Cesium.Cartesian3();
flightState._scratchCockpitPos = new Cesium.Cartesian3();
flightState._scratchNearbyPos = new Cesium.Cartesian3();
flightState._scratchSonarScreen = new Cesium.Cartesian2();
flightState._scratchDrRaw = new Cesium.Cartesian3();
+69 -10
View File
@@ -1,7 +1,8 @@
import * as Cesium from 'cesium';
import { CYAN_TRANSPARENT } from './policy.js';
import { mirrorBillboard } from '../../data/billboardMirror.js';
import { mirrorModel } from '../../data/modelMirror.js';
import { createIconSet } from '../../data/iconSet.js';
import { writeHoldSegment } from '../../data/iconSegment.js';
export function createTesting({
flightState,
@@ -38,6 +39,19 @@ export function createTesting({
flightState._displayFloorState.clear();
}
/** Give a test contact an icon row held at `billboard.position`, shown as
* `billboard.show` says (hidden by default while tracked). */
function _seedIconForTest(icao24, billboard, tracked) {
const icons = flightState._icons;
const row = icons.add(icao24, { layer: 'flights', id: icao24 });
if (billboard?.position)
writeHoldSegment(icons.segments, row, billboard.position, 0);
icons.segmentWritten(row);
icons.setShown(row, billboard?.show ?? !tracked);
return row;
}
/**
* Seed only the mutable state needed to exercise tracked-card refreshes through
* the production poll reconciler. Tests still call `flightsLayer.update()`;
@@ -46,13 +60,14 @@ export function createTesting({
* @param {string} state.icao24
* @param {object} state.entity
* @param {object} state.meta
* @param {object} state.billboard
* @param {object} state.billboardCollection
* @param {{position?: Cesium.Cartesian3, show?: boolean}} [state.billboard]
* Where the contact's icon is held, and whether it is shown.
* @param {{show?: boolean}} [state.billboardCollection] Layer visibility.
* @param {object} state.viewer
* @param {Array<Object>} [state.history=[]]
* @param {boolean} [state.tracked=true]
* @param {Iterable<[string, object]>} [state.models=[]] - Fleet 3D models keyed
* by icao24, for the billboard-hidden/model-shown handoff state.
* by icao24, for the icon-hidden/model-shown handoff state.
*/
function _setTrackedFlightRefreshStateForTest({
@@ -69,9 +84,13 @@ export function createTesting({
}) {
flightState._viewer = viewer;
flightState._modelCollection = modelCollection;
flightState._billboardCollection = billboardCollection;
flightState._billboards = new Map([[icao24, mirrorBillboard(billboard)]]);
flightState._cullPositions.clear();
flightState._icons = createIconSet({
epoch: parts.icons._displayTime(new Cesium.JulianDate()),
});
flightState._icons.show = billboardCollection?.show ?? true;
flightState._grounded.clear();
flightState._floorPinned.clear();
_seedIconForTest(icao24, billboard, tracked);
flightState._models.clear();
for (const [key, model] of models)
flightState._models.set(key, mirrorModel(model));
@@ -120,7 +139,7 @@ export function createTesting({
billboard,
history = [],
}) {
flightState._billboards.set(icao24, mirrorBillboard(billboard));
_seedIconForTest(icao24, billboard, false);
flightState.records.data.set(icao24, meta);
flightState._positionHistory.set(icao24, history);
}
@@ -181,25 +200,64 @@ export function createTesting({
: Cesium.Color.CYAN;
}
/** Drive the exact fleet billboard-to-model handoff used by `_fleetTick`. */
/** Drive the exact fleet icon-to-model handoff used by `_fleetTick`. */
function _driveFleetModelHandoffForTest({ icao24, position, course = 0 }) {
return parts.rendering._driveFleetModelHandoff(
icao24,
flightState._models.get(icao24),
flightState._billboards.get(icao24),
flightState._icons.row(icao24),
position,
course,
);
}
/** Where a contact's icon is drawn now and whether the layer shows it. */
function _iconForTest(icao24) {
const position = parts.icons._iconPosition(icao24);
return position
? {
position,
shown: parts.icons._iconShown(icao24),
course: parts.icons._iconCourse(icao24),
}
: null;
}
/** Exercise the exact asynchronous fleet loader and return its admitted model. */
async function _ensureFleetModelForTest(icao24) {
await parts.rendering._ensureModel(icao24);
return flightState._models.get(icao24) || null;
}
/** Show or hide a contact's icon as a handoff would, or hold it somewhere. */
function _setIconForTest(icao24, { shown, position } = {}) {
const icons = flightState._icons;
const row = icons?.row(icao24) ?? -1;
if (row < 0) return;
if (shown !== undefined) icons.setShown(row, shown);
if (position) {
writeHoldSegment(icons.segments, row, position, 0);
icons.segmentWritten(row);
}
}
/** Run the fleet tick's grounded floor pass once, at `renderTime`. */
function _floorGroundedIconsForTest(renderTime, cameraPosition) {
const icons = flightState._icons;
parts.icons._floorGroundedIcons(
renderTime,
icons.seconds(renderTime),
Date.now(),
cameraPosition,
);
}
return {
_floorGroundedIconsForTest,
_setIconForTest,
_floorGroundedDisplayPositionForTest,
_clearDisplayFloorStateForTest,
_setTrackedFlightRefreshStateForTest,
@@ -214,6 +272,7 @@ export function createTesting({
_updateTrackedModelForTest,
_trackedBillboardColorForTest,
_driveFleetModelHandoffForTest,
_iconForTest,
_ensureFleetModelForTest,
};
}
+75 -101
View File
@@ -27,7 +27,6 @@ import {
} from './policy.js';
import { roundHeadingDegrees } from '../../data/headingDegrees.js';
import { entityRecord, readEntityRecord } from '../../data/entityRecords.js';
import { billboardState, setBillboard } from '../../data/billboardMirror.js';
export function createTracking({
flightState,
@@ -65,9 +64,9 @@ export function createTracking({
}
function _publishTrackedSelection(icao24, origin = 'programmatic') {
const bb = flightState._billboards.get(icao24);
const position = parts.icons._iconPosition(icao24);
const info = flightState.records.data.get(icao24);
if (!billboardState(bb)?.position || !info) return false;
if (!position || !info) return false;
if (flightState._trackedEntity)
entityRecord(flightState._trackedEntity).selectionOrigin = origin;
_emitAwarenessEvent('gev:awareness-subject-selected', {
@@ -78,7 +77,7 @@ export function createTracking({
// a callsign-less contact reached Context as its raw hex even once adsbdb
// had supplied a registration. Identity below stays `icao24`.
label: parts.queries._contactLabel(icao24, info),
position: Cesium.Cartesian3.clone(billboardState(bb).position),
position,
origin,
});
selectTrackedSubjectContext(_contextSubjectMetadata(icao24));
@@ -163,9 +162,12 @@ export function createTracking({
}
const previous = flightState._cockpitNearContacts;
const distancesSquared = [];
for (const [icao24, bb] of flightState._billboards) {
const position = billboardState(bb)?.position;
if (icao24 === flightState._trackedIcao || !position) continue;
const icons = flightState._icons;
const tau = parts.icons._displayTau();
for (let row = 0; row < icons.count; row++) {
const icao24 = icons.idAt(row);
if (icao24 === flightState._trackedIcao) continue;
const position = icons.position(row, tau, flightState._scratchCockpitPos);
distancesSquared.push([
icao24,
Cesium.Cartesian3.distanceSquared(
@@ -181,13 +183,10 @@ export function createTracking({
parts.rendering._modelKeepDistM(),
);
flightState._cockpitNearContacts = next;
let presentationChanged = false;
for (const [icao24, bb] of flightState._billboards) {
if (previous.has(icao24) === next.has(icao24)) continue;
parts.rendering._applyFleetBillboardPresentation(icao24, bb);
presentationChanged = true;
}
if (presentationChanged) flightState._lastCamPoseSig = '';
for (const icao24 of previous)
if (!next.has(icao24)) parts.icons._applyIconPresentation(icao24);
for (const icao24 of next)
if (!previous.has(icao24)) parts.icons._applyIconPresentation(icao24);
}
/** Switch all current and future ambient contacts between silhouettes and cockpit pips. */
@@ -205,9 +204,9 @@ export function createTracking({
if (flightState._modelCollection) flightState._modelCollection.show = true;
flightState._trail?.setVisible(!next);
if (flightState._trailHeadEntity) flightState._trailHeadEntity.show = !next;
for (const [icao24, bb] of flightState._billboards)
parts.rendering._applyFleetBillboardPresentation(icao24, bb);
flightState._lastCamPoseSig = '';
const icons = flightState._icons;
for (let row = 0; row < (icons?.count ?? 0); row++)
parts.icons._applyIconPresentation(icons.idAt(row));
flightState._lastFleetTickMs = 0;
}
@@ -319,8 +318,7 @@ export function createTracking({
if (!flightState._trackedIcao || !flightState._viewer) return;
const pos =
parts.motion._trackedDisplayCached() ||
billboardState(flightState._billboards.get(flightState._trackedIcao))
?.position;
parts.icons._iconPosition(flightState._trackedIcao);
if (!pos) return;
const projected = screenProjectedRotation(
flightState._viewer.scene,
@@ -639,30 +637,9 @@ export function createTracking({
const clearedIcao = flightState._trackedIcao;
clearFocusTarget('flights', clearedIcao);
// Restore the original billboard appearance. The rotation is re-seeded from
// the tracked entity's last rendered rotation and a rotation pass is forced
// (_lastCamPoseSig below): the fleet billboard otherwise reappears with the
// STALE screen rotation it had when tracking began — up to a full
// ROTATION_REFRESH_MS of a wrong (possibly reversed) nose on release.
if (flightState._billboards.has(flightState._trackedIcao)) {
const bb = flightState._billboards.get(flightState._trackedIcao);
setBillboard(bb, 'show', true);
setBillboard(bb, 'width', 20);
setBillboard(bb, 'height', 20);
// Ground/military-aware restore (a plane untracked while taxiing must come
// back muted-gray at ground scale, not white at full scale).
bb.color = parts.rendering._fleetBillboardColor(flightState._trackedIcao);
setBillboard(
bb,
'scale',
parts.rendering._fleetBillboardScale(
flightState._trackedIcao,
flightState.records.data.get(flightState._trackedIcao)?.klass,
),
);
setBillboard(bb, 'rotation', flightState._lastTrackedRotation);
}
flightState._lastCamPoseSig = ''; // force a fleet rotation pass on the next tick
// The nose the icon continues from (the shared _displayCourse entry), so
// release never flips it.
const trackedCourseDeg = flightState._displayCourse.get(clearedIcao);
// Stop tracking and remove the entity. skipViewerUntrack: when ANOTHER layer just grabbed the
// follow-camera, we tear down our own state but must NOT clear viewer.trackedEntity (the new owner
@@ -679,10 +656,14 @@ export function createTracking({
parts.rendering._releaseTrackedModel();
_resetTrackedSelectionState(); // the zoom band + load-failure budget belong to the selection we just dropped
flightState._trackedIcao = null;
parts.rendering._applyFleetBillboardPresentation(
clearedIcao,
flightState._billboards.get(clearedIcao),
);
// Hand the contact back to its icon only now: settling floors a grounded
// contact, and while it still read as tracked its tracked model owned the
// visual, so the floor would have stepped aside.
if (flightState._icons?.has(clearedIcao)) {
flightState._icons.setShown(flightState._icons.row(clearedIcao), true);
parts.icons._settleIcon(clearedIcao, { fromCourseDeg: trackedCourseDeg });
}
parts.icons._applyIconPresentation(clearedIcao);
clearTrackedSubjectContext('flights');
_emitAwarenessEvent('gev:awareness-subject-cleared', {
layerId: 'flights',
@@ -730,10 +711,7 @@ export function createTracking({
pending.generation !== flightState._trackingIntentGeneration
)
return false;
if (
!flightState._billboardCollection?.show ||
!flightState._billboards.has(pending.id)
)
if (!flightState._icons?.show || !flightState._icons.has(pending.id))
return false;
flightState._pendingTrackingRestore = null;
_trackFlight(pending.id, { origin: pending.origin });
@@ -837,42 +815,42 @@ export function createTracking({
// an already-loaded model would otherwise linger with the billboard hidden.
if (flightState._models.has(id) || flightState._modelPending.has(id))
parts.rendering._releaseModel(id);
const bb = flightState._billboards.get(id);
if (bb && id !== flightState._trackedIcao) setBillboard(bb, 'show', true); // horizon cull re-asserts next tick
const row = flightState._icons?.row(id) ?? -1;
if (row >= 0 && id !== flightState._trackedIcao)
flightState._icons.setShown(row, true);
if (id === flightState._trackedIcao) {
parts.rendering._releaseTrackedModel();
_syncTracked2dRotation();
}
}
const bb = flightState._billboards.get(id);
if (bb) parts.rendering._applyFleetBillboardPresentation(id, bb);
parts.icons._applyIconPresentation(id);
if (id === flightState._trackedIcao) {
_syncTrackedBillboardImage();
_updateTrackedLabelModel(id);
}
flightState._viewer?.scene?.requestRender?.();
return flightState._billboards.has(id) || id === flightState._trackedIcao;
return flightState._icons?.has(id) || id === flightState._trackedIcao;
}
/** Re-image every converted contact this layer owns (IR style flip). */
function _refreshTr3bForStyle() {
for (const id of tr3bConvertedIds()) {
if (flightState._billboards.has(id) || id === flightState._trackedIcao)
if (flightState._icons?.has(id) || id === flightState._trackedIcao)
_refreshTr3bContact(id);
}
}
/** Plausibility check anchored to the plane's billboard position (coarse is
/** Plausibility check anchored to the plane's icon position (coarse is
* fine here — this gates a LABEL, and it must not touch the tracked frame
* cache). Missing data → true (never hide what we can't judge). */
function _routeIsPlausible(icao24, route) {
const info = flightState.records.data.get(icao24);
const bb = flightState._billboards.get(icao24);
if (!info || !bb || !billboardState(bb).position) return true;
const position = parts.icons._iconPosition(icao24);
if (!info || !position) return true;
const carto = Cesium.Cartographic.fromCartesian(
billboardState(bb).position,
position,
Cesium.Ellipsoid.WGS84,
flightState._scratchCarto,
);
@@ -907,29 +885,42 @@ export function createTracking({
function _trackFlight(icao24, { origin = 'programmatic' } = {}) {
_clearTracking(false, { origin }); // switching planes — the new follow-camera takes over
const bb = flightState._billboards.get(icao24);
const icons = flightState._icons;
const row = icons?.row(icao24) ?? -1;
const info = flightState.records.data.get(icao24);
if (!bb || !info) return;
if (row < 0 || !info) return;
// The tracked limiter continues from the nose the icon was drawn with, and
// the entity starts with the rotation the icon had on screen.
const tau = parts.icons._displayTau();
const iconPosition = icons.position(row, tau);
flightState._displayCourse.set(icao24, icons.course(row, tau));
flightState._trackedIcao = icao24;
_resetTrackedSelectionState(); // fresh selection: enter at the ENTER ceiling, full load-retry budget
flightState._cachedDRFrame = -1;
flightState._lastTrackedRotation = billboardState(bb).rotation || 0;
flightState._lastTrackedRotation =
screenProjectedRotation(
flightState._viewer.scene,
iconPosition,
icons.course(row, tau),
0,
) ?? 0;
// Drop any fleet 3D model for this aircraft — the tracked entity now owns its visual (its
// own billboard + model graphic), and the fleet tick skips the tracked icao, so a leftover
// fleet model would be orphaned + double-rendered.
parts.rendering._releaseModel(icao24);
// Hide the billboard — the tracked entity replaces it visually
setBillboard(bb, 'show', false);
// Hide the icon — the tracked entity replaces it visually
icons.setShown(row, false);
// Helper: smoothed, per-frame-cached tracked position (see _trackedDisplayPosition —
// one computation shared by the position/alignedAxis/rotation/trail-head callbacks,
// with discontinuity reconciliation). Falls back to the last billboard position when
// with discontinuity reconciliation). Falls back to the icon's position when
// the aircraft has no fix.
const getTrackedPosition = () =>
parts.motion._trackedDisplayPosition(icao24) ||
billboardState(bb).position;
parts.icons._iconPosition(icao24) ||
iconPosition;
// Dead-reckoning position property — smooth continuous motion between API updates.
const positionProperty = new Cesium.CallbackProperty(() => {
@@ -1106,23 +1097,19 @@ export function createTracking({
*/
function _onMilitaryActiveChange(active) {
if (!flightState._viewer || !flightState._billboardCollection) return;
const icons = flightState._icons;
if (!flightState._viewer || !icons) return;
if (active) {
for (const [icao24, bb] of flightState._billboards) {
if (!isMilitaryIcao(icao24) || icao24 === flightState._trackedIcao)
continue;
flightState._billboardCollection.remove(bb);
flightState._billboards.delete(icao24);
parts.rendering._releaseModel(icao24); // military-suppression: drop any 3D model too
flightState.records.data.delete(icao24);
flightState._cullPositions.delete(icao24);
flightState._positionHistory.delete(icao24);
flightState._displayCourse.delete(icao24);
flightState._groundSnap.forget(icao24);
flightState.records.missingPolls.delete(icao24);
const suppressed = [];
for (let row = 0; row < icons.count; row++) {
const icao24 = icons.idAt(row);
if (isMilitaryIcao(icao24) && icao24 !== flightState._trackedIcao)
suppressed.push(icao24);
}
flightState.feed._count = flightState._billboards.size;
} else if (flightState._billboardCollection.show) {
// Military-suppression: drop each duplicate with any 3D model too.
for (const icao24 of suppressed) parts.icons._forgetContact(icao24);
flightState.feed._count = icons.count;
} else if (icons.show) {
// Fire-and-forget refresh; only while the layer is actually enabled.
void flightsLayer.update(flightState._viewer);
}
@@ -1212,25 +1199,12 @@ export function createTracking({
return;
}
// For BillboardCollection picks, the billboard may be at picked.primitive or picked.id
const billboard = picked.primitive;
if (
billboard &&
billboard.id &&
flightState._billboards.has(billboard.id)
) {
// Icon picks carry the icao on picked.id; models carry it on the primitive.
const pickedIcao =
typeof picked.id === 'string' ? picked.id : picked.primitive?.id;
if (pickedIcao && flightState._icons?.has(pickedIcao)) {
_cancelPendingTrackingRestore();
_trackFlight(billboard.id, { origin: 'user' });
return;
}
// Some CesiumJS versions surface the id as a string on picked.id instead
if (
picked.id &&
typeof picked.id === 'string' &&
flightState._billboards.has(picked.id)
) {
_cancelPendingTrackingRestore();
_trackFlight(picked.id, { origin: 'user' });
_trackFlight(pickedIcao, { origin: 'user' });
return;
}
}
+1 -1
View File
@@ -702,7 +702,7 @@ test('the Display selector, voice schema and final stylesheet expose Cyber', ()
assert.doesNotMatch(detection, /sonarActive \? 1 : 0/);
assert.match(
civilRendering,
/alpha: flightState\._irBoost \? 1 : treatment\.alpha/,
/alpha: flightState\._irBoost\s*\?\s*1\s*:\s*aircraftTreatmentAlpha\(/,
);
assert.match(
militaryRendering,