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

+466
View File
@@ -1,3 +1,12 @@
import { getRegisteredTransitFeed } from '../src/data/transitFeeds.js';
// Not registered or active: camera discovery and live polls never own this feed.
/** CapMetro metadata under a private QA identity for selected-trail pixels. */
export const TRAIL_VISIBILITY_FEED = Object.freeze({
...getRegisteredTransitFeed('capmetro-austin'),
id: 'qa-trail-capmetro',
});
import { reportTransitVisibility } from './qa-transit-browser.mjs';
import { writeFile } from 'node:fs/promises';
import {
@@ -5,6 +14,7 @@ import {
reduceMotion,
reduceBackground,
reduceSensorContrast,
reduceTrailPixels,
} from '../src/layers/transit/qaMetrics.js';
/** CDP includes objects collected during the sample, not just surviving objects. */
@@ -581,3 +591,459 @@ export async function runBostonLive({
JSON.stringify(evidence, null, 2),
);
}
/** Google 3D proof: paired framebuffer reads reject coincidental road colours. */
export async function runTrailVisibility({
page,
check,
wait,
shots,
tag,
selectStyle,
altitude = 600,
pitch = 45,
}) {
const name = 'Google 3D selected trail visible at 6 of 8 projected samples';
await selectStyle(page, 'normal');
await page.evaluate(
async ({ altitude, pitch }) => {
const app = window.__godsEyeView;
await app.dataManager.setEnabled('transit', true, { source: 'qa' });
if (app.mapStackController.isStackAvailable('photoreal'))
await app.mapStackController.setStack('photoreal');
const camera = app.viewer.camera,
C = camera.positionCartographic.constructor;
camera.setView({
destination: C.toCartesian(
C.fromDegrees(
-97.7431,
30.26725 - altitude / Math.tan((pitch * Math.PI) / 180) / 111320,
altitude,
),
),
orientation: { heading: 0, pitch: (-pitch * Math.PI) / 180, roll: 0 },
});
},
{ altitude, pitch },
);
await wait(8000);
const setup = await page.evaluate(
async ({ altitude, pitch, feed }) => {
const app = window.__godsEyeView,
scene = app.viewer.scene;
const layer = app.dataManager.layers.get('transit').module;
const state = layer._transitStateForTest(),
parts = layer._transitPartsForTest();
const google =
!!app.tileset &&
app.tileset.show !== false &&
app.mapStackController.getActiveId() === 'photoreal';
if (!google)
return { google: false, reason: 'visible Google 3D tileset required' };
const C = scene.camera.positionCartographic.constructor;
const centreLat = 30.267 + 90 / 111320;
const floor = scene.sampleHeight(C.fromDegrees(-97.7431, centreLat));
if (Number.isFinite(floor))
scene.camera.setView({
destination: C.toCartesian(
C.fromDegrees(
-97.7431,
centreLat - altitude / Math.tan((pitch * Math.PI) / 180) / 111320,
floor + altitude,
),
),
orientation: { heading: 0, pitch: (-pitch * Math.PI) / 180, roll: 0 },
});
parts.selection.clearSelection();
parts.ingestion.abortAllInFlight();
state._activeFeeds.clear();
for (const key of state._vehicles.keys())
parts.ingestion.removeVehicle(key);
clearTimeout(state._cameraDebounceTimer);
state._cameraDebounceTimer = null;
const { seek, setRate } = await import('/src/data/contactPlayback.js');
const now = Date.now();
// Nine straight street fixes, 30 m apart. Use the production surface path.
for (let i = 0; i < 9; i++) {
const timestamp = (now - 120000 + i * 15000) / 1000;
parts.ingestion.applySnapshot(
feed,
{
fetchedAt: timestamp * 1000,
vehicles: [
{
id: 'trail-visible',
routeId: '1',
tripId: 'trail-visible',
lat: 30.267 + (i * 30) / 111320,
lon: -97.7431,
timestamp,
timestampSource: 'vehicle',
bearing: 0,
},
],
},
{ stale: false },
);
}
const entry = state._vehicles.get(`${feed.id}:trail-visible`);
entry.qaFixture = true;
const clocks = { wallNowMs: now, monoNowMs: performance.now() };
seek(entry.track, now - 45000, clocks);
setRate(entry.track, 1, clocks);
parts.rendering.sampleIdle(entry);
parts.height.anchorFloors();
parts.rendering.refreshVisibility();
parts.rendering.maintainPresentation();
parts.selection.selectVehicle(entry.key);
return {
google,
key: entry.key,
fixes: entry.track.count,
startDisplayT: now - 45000,
startLat: entry.sample.lat,
};
},
{ altitude, pitch, feed: TRAIL_VISIBILITY_FEED },
);
if (!setup.google) {
check(name, false, JSON.stringify(setup), { unexercised: true });
return;
}
const cost = await page.evaluate(async (key) => {
const app = window.__godsEyeView,
scene = app.viewer.scene;
const layer = app.dataManager.layers.get('transit').module;
const parts = layer._transitPartsForTest(),
entry = layer._transitStateForTest()._vehicles.get(key);
const gl = scene.context._gl,
originals = new Map(),
wrapped = new Map();
let owner = null,
headBytes = 0,
bodyBytes = 0,
headUploadMax = 0,
bodyUploadMax = 0,
frames = 0;
const headCpu = [];
const wrap = (primitive, kind) => {
if (!primitive || wrapped.has(primitive)) return;
const update = primitive.update;
wrapped.set(primitive, update);
primitive.update = function (frameState) {
const before = performance.now();
owner = kind;
try {
return update.call(this, frameState);
} finally {
owner = null;
if (kind === 'head') headCpu.push(performance.now() - before);
}
};
};
for (const name of ['bufferData', 'bufferSubData']) {
const original = gl[name];
originals.set(name, original);
gl[name] = function (...args) {
if (owner) {
const value = args[name === 'bufferData' ? 1 : 2],
offset = args[3] || 0,
length = args[4];
const bytes =
typeof value === 'number'
? value
: value
? length !== undefined
? length * (value.BYTES_PER_ELEMENT || 1)
: value.byteLength - offset * (value.BYTES_PER_ELEMENT || 1)
: 0;
if (owner === 'head') headBytes += bytes;
else bodyBytes += bytes;
}
return original.apply(this, args);
};
}
const pre = scene.preRender.addEventListener(() => {
headBytes = bodyBytes = 0;
const trail = parts.trails.diagnostics();
wrap(trail?.body, 'body');
wrap(trail?.headPrimitive || trail?.head?._polylineCollection, 'head');
});
const post = scene.postRender.addEventListener(() => {
frames++;
headUploadMax = Math.max(headUploadMax, headBytes);
bodyUploadMax = Math.max(bodyUploadMax, bodyBytes);
});
try {
// Include a full history upload, even if the first selection already rendered.
entry.pathRevision = -1;
parts.trails.prepareEntry(entry);
scene.requestRender();
await new Promise((resolve) => setTimeout(resolve, 20000));
headCpu.sort((a, b) => a - b);
return {
frames,
headUploadMax,
bodyUploadMax,
headCpuP95Ms: headCpu[Math.floor(headCpu.length * 0.95)] ?? null,
headCpuMaxMs: headCpu.at(-1) ?? null,
};
} finally {
pre();
post();
for (const [name, original] of originals) gl[name] = original;
for (const [primitive, update] of wrapped)
if (!primitive.isDestroyed()) primitive.update = update;
}
}, setup.key);
const result = await page.evaluate(async (setup) => {
const app = window.__godsEyeView,
scene = app.viewer.scene;
const layer = app.dataManager.layers.get('transit').module;
const state = layer._transitStateForTest(),
parts = layer._transitPartsForTest();
const entry = state._vehicles.get(setup.key);
const trail = parts.trails.diagnostics();
if (!entry || !trail)
return { ...setup, present: 0, reason: 'selection disappeared' };
const { setRate } = await import('/src/data/contactPlayback.js');
setRate(entry.track, 0, {
wallNowMs: Date.now(),
monoNowMs: performance.now(),
});
parts.rendering.sampleIdle(entry);
parts.rendering.schedulePlayback(entry);
parts.rendering.syncRenderHold();
const ready = () =>
app.tileset?.show !== false &&
app.tileset?.tilesLoaded === true &&
(scene.globe?.show === false || scene.globe?.tilesLoaded === true) &&
trail.body?.ready === true &&
(!trail.headPrimitive || trail.headPrimitive.ready === true);
// Require sustained readiness after freezing playback, before projecting
// onto the refined surface. Repeated captures still check pixel stability.
const tilesReady = await new Promise((resolve) => {
const started = performance.now();
let readySince = null;
const timer = setInterval(() => {
const now = performance.now();
readySince = ready() ? (readySince ?? now) : null;
scene.requestRender();
if (
(readySince !== null && now - readySince >= 500) ||
now - started >= 30000
) {
clearInterval(timer);
resolve(readySince !== null && now - readySince >= 500);
}
}, 80);
});
if (!tilesReady)
return {
...setup,
present: 0,
reason: 'tiles or trail geometry did not become ready',
};
const canvas = scene.canvas,
gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
const scale = canvas.width / canvas.clientWidth;
const points = entry.trailSegments
.filter((s) => s.toT <= entry.sample.displayT)
.flatMap((s) => s.positions);
if (trail.head?.show) points.push(...trail.head.positions);
if (points.length < 2)
return { ...setup, present: 0, reason: 'no completed path' };
const C = scene.camera.positionCartographic.constructor;
const V = entry.marker.position.constructor;
const lengths = [0];
for (let i = 1; i < points.length; i++)
lengths.push(lengths.at(-1) + V.distance(points[i - 1], points[i]));
const mul = (m, v) =>
[0, 1, 2, 3].map(
(i) =>
m[i] * v[0] + m[i + 4] * v[1] + m[i + 8] * v[2] + m[i + 12] * v[3],
);
const samples = Array.from({ length: 8 }, (_, i) => {
const d = (lengths.at(-1) * (i + 1)) / 9;
let j = 1;
while (j < lengths.length - 1 && lengths[j] < d) j++;
const p = V.lerp(
points[j - 1],
points[j],
(d - lengths[j - 1]) / Math.max(0.001, lengths[j] - lengths[j - 1]),
new V(),
);
const cart = C.fromCartesian(p);
const height = scene.sampleHeight(
cart,
[entry.marker, trail.body, trail.headPrimitive].filter(Boolean),
);
if (Number.isFinite(height)) cart.height = height;
const surface = C.toCartesian(cart);
const q = mul(
scene.camera.frustum.projectionMatrix,
mul(scene.camera.viewMatrix, [surface.x, surface.y, surface.z, 1]),
);
return {
x: ((q[0] / q[3]) * 0.5 + 0.5) * canvas.clientWidth,
y: (0.5 - (q[1] / q[3]) * 0.5) * canvas.clientHeight,
inFront: q[3] > 0,
radius: (Math.SQRT2 * Math.ceil(3 * scale)) / scale,
};
});
const marker = entry.marker.position;
const markerClip = mul(
scene.camera.frustum.projectionMatrix,
mul(scene.camera.viewMatrix, [marker.x, marker.y, marker.z, 1]),
);
const sprite = {
x: ((markerClip[0] / markerClip[3]) * 0.5 + 0.5) * canvas.clientWidth,
y: (0.5 - (markerClip[1] / markerClip[3]) * 0.5) * canvas.clientHeight,
// Include the padded, rotated sprite and its selection bracket.
radius: Math.hypot(entry.marker.width, entry.marker.height) / 2 + 4,
};
const read = () =>
samples.map(({ x, y, inFront }) => {
const r = Math.ceil(3 * scale),
px = Math.round(x * scale),
py = Math.round(canvas.height - 1 - y * scale);
if (
!inFront ||
px < r ||
py < r ||
px + r >= canvas.width ||
py + r >= canvas.height
)
return null;
const data = new Uint8Array((2 * r + 1) ** 2 * 4);
gl.readPixels(
px - r,
py - r,
2 * r + 1,
2 * r + 1,
gl.RGBA,
gl.UNSIGNED_BYTE,
data,
);
return data;
});
const frame = () =>
new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
remove();
reject(new Error('Trail framebuffer did not render within 5 s'));
}, 5000);
const remove = scene.postRender.addEventListener(() => {
remove();
clearTimeout(timeout);
resolve(read());
});
scene.requestRender();
});
const bodyShow = trail.body?.show,
headShow = trail.head?.show,
headPrimitiveShow = trail.headPrimitive?.show,
backingShow = trail.backing?.show;
let showTrail = false;
const applyVisibility = () => {
if (trail.body) trail.body.show = showTrail && bodyShow;
if (trail.headPrimitive)
trail.headPrimitive.show = showTrail && headPrimitiveShow;
if (trail.head) trail.head.show = showTrail && headShow;
if (trail.backing) trail.backing.show = showTrail && backingShow;
};
// Override the layer's frame update in both states; selection stays fixed.
const removeVisibility = scene.preRender.addEventListener(applyVisibility);
const captures = [];
let captureReady = true;
try {
for (const visible of [false, true, false, true]) {
showTrail = visible;
// Let post-processing settle before each measured framebuffer.
for (let i = 0; i < 3; i++) await frame();
captures.push(await frame());
const current = parts.trails.diagnostics();
captureReady &&=
ready() &&
current?.body === trail.body &&
current?.headPrimitive === trail.headPrimitive;
}
} finally {
removeVisibility();
if (trail.body) trail.body.show = bodyShow;
if (trail.headPrimitive) trail.headPrimitive.show = headPrimitiveShow;
if (trail.head) trail.head.show = headShow;
if (trail.backing) trail.backing.show = backingShow;
await frame();
}
const [off, on, offAgain, onAgain] = captures.map((patches) =>
patches.map((p) => (p ? Array.from(p) : null)),
);
return {
...setup,
fixes: entry.track.count,
selected: state._selectedKey === setup.key,
pathMetres: lengths.at(-1),
samples,
on,
off,
onAgain,
offAgain,
sprite,
tilesReady: captureReady,
elapsedDisplayMs: entry.sample.displayT - setup.startDisplayT,
movedMetres: Math.abs(entry.sample.lat - setup.startLat) * 111320,
body: trail.body?.constructor.name,
headStrategy: trail.headStrategy || 'PolylineCollection',
headRebuilds: trail.headRebuilds ?? null,
};
}, setup);
const pixels = reduceTrailPixels(result.on, result.off, result);
result.cost = cost;
result.altitude = altitude;
result.pitch = pitch;
result.present = pixels.present;
result.total = pixels.total;
result.samples = result.samples?.map((s, i) => ({
...s,
hit: pixels.hits[i],
stable: pixels.stable[i],
eligible: pixels.eligible[i],
}));
await page.screenshot({
path: `${shots}/${tag}-${altitude}m-trail-visible.jpg`,
type: 'jpeg',
quality: 95,
});
await writeFile(
`${shots}/${tag}-${altitude}m-trail-visible.json`,
JSON.stringify(result, null, 2),
);
for (const key of ['on', 'off', 'onAgain', 'offAgain']) delete result[key];
check(
name,
result.google &&
result.selected &&
result.fixes >= 5 &&
result.pathMetres > 15 &&
result.elapsedDisplayMs >= 19000 &&
result.movedMetres >= 5 &&
pixels.pass,
JSON.stringify(result),
);
check(
'selected trail stays within 2 MiB rebuild and 2 KiB/frame head uploads',
cost.frames > 30 &&
cost.bodyUploadMax > 0 &&
cost.bodyUploadMax <= 2 * 1024 * 1024 &&
cost.headUploadMax > 0 &&
cost.headUploadMax <= 2048,
JSON.stringify({ altitude, ...cost }),
);
await page.evaluate(() =>
window.__godsEyeView.dataManager.layers
.get('transit')
.module._transitPartsForTest()
.selection.clearSelection(),
);
}