fix(transit): keep the real silhouettes under sensor presets, make the selected trail visible, put Transit in Movement (#596)

## Changes

Follow-up to #587 after the owner's first look at the merged transit layer on Google 3D tiles.

- **Sensor presets keep the real silhouettes.** Under NVG, thermal (white-hot, black-hot, Ironbow) and noir, vehicles are the same silhouettes as in normal view, filled opaque white with a 2 px dark halo, at the CRT display sizes (26 px fleet, 39 px selected). The solid rounded "signature" bodies are gone.
- **The selected vehicle's trail is visible.** The retained history is now draped onto 3D tiles and terrain (`GroundPolylinePrimitive`, `ClassificationType.BOTH`, the way the traffic heat-lines are drawn), 3 px core over a 5 px dark backing, per-segment age colour, a short draped head clipped to the sprite. Two defects kept it off screen before: Cesium's per-instance attribute getters return copies, so the renderer's show/colour edits were discarded, and a marker did not recover after the camera jumped to it, which hid the trail with it. Trail geometry is now prepared on selection independent of floor resolution and marker visibility; sparse long intervals (rail) keep a bounded marker corridor instead of dropping the interval; classification survives map-source changes.
- **Transit sits in the Movement panel group** between Street Traffic and Bike Share (the panel orders by `PANEL_GROUPS`, not by catalog order; the catalog is moved too).

## How it was verified

- `npm run format:check` (854 files), `npm run check:boundaries`, `npm test` (4,062 passing), `npm run build` clean; tracking regression 109/109 in a real browser on Google 3D tiles.
- Browser harness (`scripts/qa-transit.mjs`), keyed on Google 3D tiles and keyless: keyed 194 pass, with the trail pixel check passing at 300, 600 and 900 m; the remaining keyed failures are the per-frame allocation gate already recorded as a follow-up from #587 (CPU and frame time within budget), four readability lines at threshold edges, and the card-anchor check now counting frames where the selected marker is legitimately off screen; keyless 193 pass with the remaining misses being threshold edges on the thin tram silhouette (centre 0.83–0.88 against 0.85) and one bus ring 0.28 against 0.25 on a pure-white roof.
- New harness checks: `Google 3D selected trail visible at 6 of 8 projected samples` (repeated off/on framebuffer captures at 300/600/900 m, samples outside the sprite footprint, a background-change negative control; fails on main, passes here), a camera-arrival check (select a moving vehicle, `setView` onto it, marker shown and trail body drawn within 2 s), `PANEL_ORDER` pinned, raster tests comparing every sensor raster's mask and aspect ratio to the normal silhouette.
- Direct look on a keyed build: a live CapMetro bus at 30 m/s selected through the layer API draws its draped trail along the highway lane (harness capture `keyed-600m-trail-visible.jpg`).
- Adversarial review by Codex Astra before the last two fix passes (stack-switch classification, marker corridor under the vertex cap, pixel-gate loophole), all three fixed with regressions.

Docs: `docs/CURRENT-STATE.md` and `CHANGELOG.md` updated in this PR. No data-source changes.
This commit is contained in:
Bilawal Sidhu authored and GitHub committed 2026-09-15 13:26:41 -05:00
1 parent cd22ca3559
commit 0bb61f53c6
24 files changed
+1945 -205

No files matched your search

+192 -7
View File
@@ -2178,7 +2178,7 @@ test('under FLIR and NVG every sprite is white-hot with a dark halo, and selecti
assert.notEqual(entry.marker.image, plainImage, 'a haloed raster');
assert.match(
Buffer.from(entry.marker.image.split(',')[1], 'base64').toString(),
/#000000/,
/#05080C/,
'with the dark ring',
);
assert.ok(entry.marker.width > plainWidth * 1.2, 'and larger');
@@ -2547,7 +2547,7 @@ test('selected trail and marker share revised start floors, with unknown history
assert.ok(
Cesium.Cartographic.fromCartesian(entry.marker.position).height > 30,
);
assert.ok(entry.trailVertices <= 2048);
assert.ok(entry.trailVertices <= 640);
parts.selection.clearSelection();
assert.equal(parts.trails.diagnostics(), null);
});
@@ -2659,7 +2659,7 @@ test('a long selected history shortens its oldest geometry before sacrificing th
parts.rendering.sampleIdle(entry);
parts.selection.selectVehicle(entry.key);
assert.equal(entry.trailTruncated, true);
assert.ok(entry.trailVertices <= 2048);
assert.ok(entry.trailVertices <= 640);
assert.ok(
entry.displayPaths.has(entry.sample.fromSeq),
'active path is prepared first',
@@ -2671,6 +2671,63 @@ test('a long selected history shortens its oldest geometry before sacrificing th
);
});
test('sparse rail reports preserve a bounded marker corridor with and without selection', async (t) => {
const app = harness(t);
const parts = app.layer._transitPartsForTest();
const { seek } = await import('../../data/contactPlayback.js');
let latitude = 42.36 - 16000 / 111320;
let timestamp = reported();
app.serve('mbta', () => ({
status: 200,
body: snapshot(
'mbta',
'MBTA',
[vehicle('sparse', latitude, -71.06, timestamp, { routeId: 'CR-1' })],
{ fetchedAt: Date.now() },
),
}));
app.layer.enable(app.viewer);
await app.layer.update();
app.settle();
app.advance(180000);
latitude = 42.36;
timestamp += 180;
await app.layer.update();
app.settle();
const entry = app.vehicles()[0];
assert.equal(entry.mode, 'rail');
assert.equal(entry.track.count, 2, 'both 88.9 m/s reports pass admission');
assert.equal(entry.fixes[1].t - entry.fixes[0].t, 180000);
for (let i = 0; i < 1500; i++)
app.ground._warm.set(app.ground._key(42.2 + i * 0.0002, -71.06), 12);
seek(entry.track, entry.fixes[0].t + 90000, {
wallNowMs: Date.now(),
monoNowMs: performance.now(),
});
parts.rendering.sampleIdle(entry);
for (const selected of [false, true, false]) {
if (selected) parts.selection.selectVehicle(entry.key);
else parts.selection.clearSelection();
parts.trails.prepareEntry(entry);
const path = entry.displayPaths.get(entry.sample.fromSeq);
assert.ok(path, 'active corridor survives the body cap');
assert.ok(path.positions.length <= 640);
const position = parts.trails.samplePosition(
entry,
entry.sample,
new Cesium.Cartesian3(),
true,
);
assert.ok(
position && [position.x, position.y, position.z].every(Number.isFinite),
);
parts.rendering.placeSample(entry, true);
assert.equal(entry.marker.show, true);
assert.ok(Cesium.Cartesian3.equals(entry.marker.position, position));
assert.ok(entry.trailVertices <= 640);
}
});
test('missing rectangle still rejects the far side, and frustum rejection wins', async (t) => {
const app = harness(t, { altitude: 100_000 });
app.viewer.camera.computeViewRectangle = () => undefined;
@@ -2783,13 +2840,13 @@ test('every styling path retains the mode palette and exact unpadded size', asyn
parts.rendering.paintMode(entry, 'subway');
check('#FF4538', 30);
app.style('thermal');
check('#FFFFFF', 36);
check('#FFFFFF', 39);
parts.selection.clearSelection();
check('#FFFFFF', 30);
check('#FFFFFF', 26);
app.vision('nvg', true);
check('#FFFFFF', 30);
check('#FFFFFF', 26);
parts.selection.selectVehicle(entry.key);
check('#FFFFFF', 36);
check('#FFFFFF', 39);
app.vision('normal', false);
check('#FF4538', 30);
parts.selection.clearSelection();
@@ -3673,3 +3730,131 @@ test('transit polls the supplied source and cancels it when disabled', async (t)
await app.layer.update();
assert.equal(calls.length, before);
});
test('selected trail QA fixture survives repeated live CapMetro snapshots', async (t) => {
const { TRAIL_VISIBILITY_FEED } =
await import('../../../scripts/qa-transit-scenes.mjs');
const { getRegisteredTransitFeed } =
await import('../../data/transitFeeds.js');
const app = harness(t, { at: { lat: 30.267, lon: -97.7431 } });
app.layer.enable(app.viewer);
const { ingestion, selection } = app.layer._transitPartsForTest();
const now = Date.now();
ingestion.applySnapshot(
TRAIL_VISIBILITY_FEED,
{
fetchedAt: now,
vehicles: [vehicle('trail-visible', 30.267, -97.7431, now / 1000)],
},
{ stale: false },
);
const key = `${TRAIL_VISIBILITY_FEED.id}:trail-visible`;
selection.selectVehicle(key);
assert.equal(app.state()._selectedKey, key);
for (let poll = 0; poll <= MISSED_POLLS_TO_DROP; poll++) {
ingestion.applySnapshot(
getRegisteredTransitFeed('capmetro-austin'),
{
fetchedAt: now + poll,
vehicles: [vehicle('live-bus', 30.268, -97.7431, now / 1000)],
},
{ stale: false },
);
}
assert.ok(
app.state()._vehicles.has(key),
'live polls must not evict the selected fixture',
);
assert.equal(app.state()._selectedKey, key);
assert.ok(
app.state()._vehicles.has('capmetro-austin:live-bus'),
'live ingestion still runs',
);
});
for (const jump of ['setView', 'flyTo']) {
test(`${jump} recovers a cold hidden vehicle within one visibility interval`, async (t) => {
const app = harness(t, { floorAt: () => undefined });
let floor;
app.viewer.scene.globe.show = true;
app.viewer.scene.globe.getHeight = () => floor;
app.serve('mbta', () => ({
status: 200,
body: snapshot('mbta', 'MBTA', [
vehicle('cold', 42.36, -71.06, reported()),
]),
}));
app.layer.enable(app.viewer);
await app.layer.update();
const entry = app.vehicles()[0];
assert.equal(entry.marker.show, false);
assert.equal(entry.heightPending, true);
// Surface tiles are now loaded at the camera destination. No poll, floor
// timer or preRender is allowed to bootstrap the visibility recovery.
clearTimeout(app.state()._floorTimer);
app.state()._floorTimer = null;
app.viewer.camera[jump] = () => {
floor = 12;
app.viewer.camera.changed.raiseEvent();
};
app.viewer.camera[jump]();
app.advance(250);
assert.equal(entry.marker.show, true);
assert.equal(entry.heightPending, false);
assert.equal(entry.surfaceReady, true);
assert.equal(entry.visibility.heightPending, false);
assert.equal(entry.visibility.surfaceReady, true);
});
}
test('selected history is prepared while its head is hidden and remains shown outside the head frustum', async (t) => {
const app = harness(t);
app.layer.enable(app.viewer);
app.layer._loadTransitFleetForTest(1, BOSTON);
const entry = app.vehicles()[0],
parts = app.layer._transitPartsForTest();
entry.marker.show = false;
app.state()._visible.delete(entry);
app.state()._moving.delete(entry);
parts.selection.selectVehicle(entry.key);
const trail = parts.trails.diagnostics();
assert.ok(trail.segments > 0);
assert.ok(trail.body);
assert.equal(trail.body.show, true);
parts.trails.update();
assert.equal(trail.body.show, true);
app.state()._vehicles.delete(entry.key);
parts.trails.update();
assert.equal(trail.body.show, false);
app.state()._vehicles.set(entry.key, entry);
});
test('selected draped history prepares geometry before surface heights resolve', async (t) => {
const app = harness(t, { floorAt: () => undefined });
app.layer.enable(app.viewer);
app.layer._loadTransitFleetForTest(1, BOSTON);
const entry = app.vehicles()[0],
parts = app.layer._transitPartsForTest();
delete entry.qaFloorM;
entry.marker.show = false;
entry.heightPending = true;
// Exercise the real GroundPolylinePrimitive branch, without a WebGL context.
const width = Cesium.ContextLimits._maximumAliasedLineWidth;
Cesium.ContextLimits._maximumAliasedLineWidth = 1;
t.after(() => {
Cesium.ContextLimits._maximumAliasedLineWidth = width;
});
app.viewer.scene.frameState = { context: { depthTexture: true } };
app.viewer.scene.groundPrimitives = app.viewer.scene.primitives;
parts.selection.selectVehicle(entry.key);
const trail = parts.trails.diagnostics();
assert.equal(trail.ground, true);
assert.ok(trail.body instanceof Cesium.GroundPolylinePrimitive);
assert.ok(trail.segments > 0);
assert.equal(trail.body.show, true);
assert.equal(
entry.displayPaths.size,
0,
'unresolved marker corridors remain unavailable',
);
});
+72
View File
@@ -329,3 +329,75 @@ export function reduceTrailHead(anchor, target) {
selection: anchor?.selection ?? null,
};
}
/** Repeated off/on patches isolate stable, trail-correlated framebuffer changes. */
export function reduceTrailPixels(on, off, evidence = {}) {
const { onAgain, offAgain, samples = [], sprite, tilesReady } = evidence;
const finitePoint = (p) =>
p && [p.x, p.y, p.radius].every(Number.isFinite) && p.radius > 0;
const stable = [],
eligible = [];
const hits = Array.from({ length: 8 }, (_, i) => {
const point = samples[i];
eligible[i] = !!(
tilesReady === true &&
finitePoint(point) &&
point.inFront === true &&
finitePoint(sprite) &&
Math.hypot(point.x - sprite.x, point.y - sprite.y) >
point.radius + sprite.radius &&
samples
.slice(0, i)
.every(
(prior) =>
!finitePoint(prior) ||
Math.hypot(point.x - prior.x, point.y - prior.y) >
point.radius + prior.radius,
)
);
const a = on?.[i],
b = off?.[i],
c = onAgain?.[i],
d = offAgain?.[i];
stable[i] = false;
if (
!eligible[i] ||
!a?.length ||
a.length % 4 ||
[b, c, d].some((p) => !p || p.length !== a.length)
)
return false;
let matches = 0;
const contrast = (lit, baseline, k) => {
const delta = Math.hypot(
lit[k] - baseline[k],
lit[k + 1] - baseline[k + 1],
lit[k + 2] - baseline[k + 2],
);
const green = lit[k + 1] > lit[k] + 30 && lit[k + 1] > lit[k + 2] + 18;
const dark =
lit[k] + lit[k + 1] + lit[k + 2] <
0.6 * (baseline[k] + baseline[k + 1] + baseline[k + 2]);
return delta >= 40 && (green || dark);
};
for (let k = 0; k < a.length; k += 4) {
for (let channel = 0; channel < 3; channel++) {
const j = k + channel;
if (
![a[j], b[j], c[j], d[j]].every(
(v) => Number.isFinite(v) && v >= 0 && v <= 255,
) ||
Math.abs(b[j] - d[j]) > 8 ||
Math.abs(a[j] - c[j]) > 8 ||
Math.abs(a[j] - b[j] - (c[j] - d[j])) > 8
)
return false;
}
if (contrast(a, b, k) && contrast(c, d, k)) matches++;
}
stable[i] = true;
return matches >= 2;
});
const present = hits.filter(Boolean).length;
return { hits, stable, eligible, present, total: 8, pass: present >= 6 };
}
+103
View File
@@ -328,3 +328,106 @@ test('trail-head acceptance names every missing and failed condition', async ()
false,
);
});
const trailPatch = (rgb) =>
Array.from({ length: 49 }, () => [...rgb, 255]).flat();
const trailEvidence = (on, off) => ({
onAgain: on,
offAgain: off,
tilesReady: true,
samples: Array.from({ length: 8 }, (_, i) => ({
x: i * 20 + 20,
y: 20,
radius: 3,
inFront: true,
})),
sprite: { x: 300, y: 20, radius: 24 },
});
test('trail pixels require repeatable contrast at six distinct samples outside the sprite', async () => {
const { reduceTrailPixels } = await import('./qaMetrics.js');
const off = Array.from({ length: 8 }, () => trailPatch([150, 150, 150]));
const on = off.map((p, i) => (i < 6 ? trailPatch([94, 240, 138]) : p));
const evidence = trailEvidence(on, off);
assert.equal(reduceTrailPixels(on, off, evidence).pass, true);
const five = on.map((p, i) => (i === 5 ? off[i] : p));
assert.equal(
reduceTrailPixels(five, off, trailEvidence(five, off)).pass,
false,
);
for (const rgb of [
[5, 8, 12],
[126, 126, 126],
]) {
const patches = off.map(() => trailPatch(rgb));
assert.equal(
reduceTrailPixels(patches, off, trailEvidence(patches, off)).present,
rgb[0] === 5 ? 8 : 0,
);
assert.equal(
reduceTrailPixels(patches, patches, trailEvidence(patches, patches))
.present,
0,
);
}
assert.equal(
reduceTrailPixels(on, off).pass,
false,
'two reads are insufficient',
);
assert.equal(
reduceTrailPixels(on, off, { ...evidence, tilesReady: false }).pass,
false,
);
assert.equal(
reduceTrailPixels(on, off, {
...evidence,
samples: Array(8).fill(evidence.samples[0]),
}).pass,
false,
);
assert.equal(
reduceTrailPixels(on, off, {
...evidence,
sprite: { x: 80, y: 20, radius: 200 },
}).pass,
false,
);
assert.equal(reduceTrailPixels([], [], trailEvidence([], [])).pass, false);
});
test('background refinement without a trail cannot pass repeated off/on acceptance', async () => {
const { reduceTrailPixels } = await import('./qaMetrics.js');
const grey = (n) => Array.from({ length: 8 }, () => trailPatch([n, n, n]));
const off = grey(100),
on = grey(30);
const evidence = trailEvidence(on, off);
assert.equal(
reduceTrailPixels(on, off, { ...evidence, offAgain: grey(30) }).pass,
false,
'one background transition is not a trail',
);
assert.equal(
reduceTrailPixels(on, off, {
...evidence,
offAgain: grey(170),
onAgain: grey(100),
}).pass,
false,
'equal deltas on a drifting baseline are not a trail',
);
assert.equal(
reduceTrailPixels(on, off, { ...evidence, onAgain: off }).pass,
false,
'contrast must repeat',
);
const first = off.map((p) => p.map((v, i) => (i < 8 ? on[0][i] : v)));
const second = off.map((p) =>
p.map((v, i) => (i >= 8 && i < 16 ? on[0][i] : v)),
);
assert.equal(
reduceTrailPixels(first, off, { ...evidence, onAgain: second }).pass,
false,
'the same pixels must change twice',
);
});
+7
View File
@@ -97,6 +97,7 @@ export function createRendering({ state, services, parts }) {
nearGround,
);
entry.surfaceReady = !!known;
entry.heightPending = !known;
entry.marker.show = !!known && vehicleInView(entry);
if (known) {
entry.marker.position = state._scratchCartesian;
@@ -523,6 +524,10 @@ export function createRendering({ state, services, parts }) {
else state._visible.delete(entry);
if (visible && entry.track?.count) {
if (!wasVisible && !entry.qaFixture) syncPlayback(entry, now);
// Re-entry can advance into an unprepared corridor; loaded tiles can
// also resolve a cold path without a poll or a render-loop wakeup.
if (!wasVisible || entry.surfaceReady === false || entry.heightPending)
parts.trails.prepareEntry(entry);
// Surface completion must recover while requestRenderMode is idle.
// Otherwise surfaceReady=false prevents the very frame that clears it.
if (!state._moving.has(entry)) {
@@ -532,6 +537,8 @@ export function createRendering({ state, services, parts }) {
}
entry.marker.show =
visible && !entry.heightPending && entry.surfaceReady !== false;
visibility.heightPending = !!entry.heightPending;
visibility.surfaceReady = entry.surfaceReady ?? null;
schedulePlayback(entry);
changed ||= visible !== wasVisible || wasShown !== entry.marker.show;
}
+33 -12
View File
@@ -18,7 +18,9 @@ const MODE_COLOR = {
ferry: '#5FD6FF',
unknown: '#D8DDE5',
};
export const TRAIL_VERTEX_LIMIT = 2048;
// Final Cesium batching uses 3,152 bytes per two-lane edge.
// The 1,278-instance body plus 64 bytes/instance reserve stays below 2 MiB.
export const TRAIL_VERTEX_LIMIT = 640;
const STEP_M = 25;
/** Shared prepared paths. Floors are aligned to work with Google 3D tiles. */
@@ -63,6 +65,7 @@ export function createTrails({ state, services, parts, source }) {
entry.trailSegments = [];
entry.trailVertices = 0;
}
const draped = entry === selected && ensureRenderer()?.diagnostics().ground;
const paths = new Map(),
all = [],
a = {},
@@ -113,16 +116,28 @@ export function createTrails({ state, services, parts, source }) {
if (entry === selected)
for (let i = activeIndex - 1; i >= 0; i--) order.push(i);
entry.trailTruncated = false;
let markerVertices = 0;
let markerIntervals = Math.max(0, entry.track.count - 1 - activeIndex);
for (const i of order) {
readFix(entry.track, i, a);
readFix(entry.track, i + 1, b);
const markerCorridor = i >= activeIndex;
if (markerCorridor) markerIntervals--;
if (a.epoch !== b.epoch || b.flags & FIX_FLAGS.BREAK) continue;
if (entry !== selected && b.t < entry.sample.displayT) continue;
const n = Math.max(1, Math.ceil(fixDistanceM(a, b) / STEP_M));
if (vertices + n + 1 > TRAIL_VERTEX_LIMIT) {
entry.trailTruncated = true;
continue;
}
const desired = Math.max(1, Math.ceil(fixDistanceM(a, b) / STEP_M));
// Reserve endpoints for every future marker interval. Sparse reports
// coarsen the active path instead of losing the vehicle to the body cap.
const n = markerCorridor
? Math.min(
desired,
TRAIL_VERTEX_LIMIT - markerVertices - 2 * markerIntervals - 1,
)
: desired;
const retainBody = vertices + n + 1 <= TRAIL_VERTEX_LIMIT;
if (!retainBody || n < desired) entry.trailTruncated = true;
if (!markerCorridor && !retainBody) continue;
if (markerCorridor) markerVertices += n + 1;
const positions = [],
heights = [];
let valid = true;
@@ -136,17 +151,21 @@ export function createTrails({ state, services, parts, source }) {
heights.push(h);
if (h === null) {
valid = false;
positions.push(null);
// Ground polylines consume longitude/latitude and drape themselves.
// Unknown floors still disqualify the marker corridor.
positions.push(
draped ? Cesium.Cartesian3.fromDegrees(point.lon, point.lat) : null,
);
} else
positions.push(
Cesium.Cartesian3.fromDegrees(point.lon, point.lat, h),
);
}
signature += `${a.seq}/${b.seq}:${heights.join(',')};`;
vertices += n + 1;
if (!valid) continue;
paths.set(a.seq, { positions, toSeq: b.seq });
if (entry === selected && fixDistanceM(a, b) > 0) {
if (retainBody) vertices += n + 1;
if (valid) paths.set(a.seq, { positions, toSeq: b.seq });
if (!valid && !draped) continue;
if (entry === selected && retainBody && fixDistanceM(a, b) > 0) {
for (let k = 0; k < n; k++)
all.push({
fromSeq: a.seq,
@@ -216,7 +235,9 @@ export function createTrails({ state, services, parts, source }) {
}
function update() {
if (!selected || !renderer) return;
renderer.setVisible(state._enabled && selected.marker?.show !== false);
renderer.setVisible(
state._enabled && state._vehicles.get(selected.key) === selected,
);
renderer.setDisplaySample(selected.sample, selected.marker?.position);
}
function select(entry) {