Files
gods-eye-view/scripts/qa-transit-scenes.mjs
T
Bilawal Sidhu 0bb61f53c6 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.
2026-09-15 13:26:41 -05:00

1050 lines
35 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 {
reduceFleet,
reduceMotion,
reduceBackground,
reduceSensorContrast,
reduceTrailPixels,
} from '../src/layers/transit/qaMetrics.js';
/** CDP includes objects collected during the sample, not just surviving objects. */
export function allocationTotals(profile) {
let frameBytes = 0,
layerFrameBytes = 0,
sharedOverlayBytes = 0,
qaBytes = 0;
const allocators = new Map();
function visit(node, inFrame = false, inQa = false, inOverlay = false) {
const frame = node.callFrame || {},
name = frame.functionName || '',
url = frame.url || '';
const nested =
inFrame ||
name === 'onPreRender' ||
/^render\d*$/.test(name) ||
/^Scene\d*\.render$/.test(name);
// QA wraps the real frame callback: re-enter production attribution at its
// onPreRender boundary, but exclude QA's own Math/Cesium helper descendants.
const qa =
name === 'onPreRender'
? false
: inQa || /\/transit\/testing\.js(?:\?|$)/.test(url);
const overlay = inOverlay || name === '_drawOverlay';
const bytes = node.selfSize || 0;
if (nested) {
if (qa) qaBytes += bytes;
else {
frameBytes += bytes;
const allocator = `${name || '(anonymous)'} ${url.split('?')[0]}:${(frame.lineNumber ?? -1) + 1}`;
allocators.set(allocator, (allocators.get(allocator) || 0) + bytes);
if (overlay) sharedOverlayBytes += bytes;
if (
/\/layers\/transit\/|\/data\/(?:contactPlayback|contactTrailRenderer|transit[^/]*)\.js/.test(
url,
)
)
layerFrameBytes += bytes;
}
}
for (const child of node.children || []) visit(child, nested, qa, overlay);
}
visit(profile.head);
return {
frameBytes,
layerFrameBytes,
sharedOverlayBytes,
qaBytes,
topAllocators: [...allocators]
.sort((a, b) => b[1] - a[1])
.slice(0, 3)
.map(([allocator, bytes]) => ({ allocator, bytes })),
};
}
export async function runFleetBudgets({
page,
check,
wait,
shots,
tag,
detectOn,
selectStyle,
}) {
console.log(
'\n== moving fleet budgets: 1920×1080 CSS, warmed view, DETECT dense ==',
);
await selectStyle(page, 'normal');
await detectOn(page);
const cdp = await page.createCDPSession();
await cdp.send('HeapProfiler.enable');
for (const count of [800, 3000]) {
await page.evaluate(async (count) => {
const app = window.__godsEyeView;
await app.dataManager.setEnabled('transit', true, { source: 'qa' });
const camera = app.viewer.camera,
C = camera.positionCartographic.constructor;
camera.setView({
destination: C.toCartesian(
C.fromDegrees(-71.0605, 42.3554, count === 800 ? 600 : 30000),
),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
const layer = app.dataManager.layers.get('transit').module;
layer._transitPartsForTest().ingestion.abortAllInFlight();
layer._transitStateForTest()._activeFeeds.clear();
for (const key of layer._transitStateForTest()._vehicles.keys())
layer._transitPartsForTest().ingestion.removeVehicle(key);
}, count);
await wait(8000);
// Camera proximity can finish a live poll during tile warm-up. Remove it
// before the heap baseline, or its memory would discount the fixture fleet.
await page.evaluate(() => {
const layer =
window.__godsEyeView.dataManager.layers.get('transit').module;
const state = layer._transitStateForTest(),
parts = layer._transitPartsForTest();
parts.ingestion.abortAllInFlight();
state._activeFeeds.clear();
clearTimeout(state._cameraDebounceTimer);
state._cameraDebounceTimer = null;
for (const key of state._vehicles.keys())
parts.ingestion.removeVehicle(key);
parts.rendering.syncRenderHold();
window.__godsEyeView.viewer.scene.requestRender();
});
await wait(500);
await cdp.send('HeapProfiler.collectGarbage');
const before = await cdp.send('Runtime.getHeapUsage');
const loaded = await page.evaluate(
(count) =>
window.__godsEyeView.dataManager.layers
.get('transit')
.module._loadTransitFleetForTest(
count,
{ lat: 42.3554, lon: -71.0605 },
count === 800 ? 5 : 120,
),
count,
);
await reportTransitVisibility(
page,
`${count} budget visibility after load`,
);
await wait(2000);
await cdp.send('HeapProfiler.collectGarbage');
// The retained delta is conservative: it includes concurrent page growth.
const retained = await cdp.send('Runtime.getHeapUsage');
// Timing/upload acceptance runs without the allocation profiler attached.
const trace = await page.evaluate(() =>
window.__godsEyeView.dataManager.layers
.get('transit')
.module._measureTransitFramesForTest(8000),
);
await page.evaluate(() =>
window.__godsEyeView.dataManager.layers
.get('transit')
.module._resetTransitFixtureClockForTest(),
);
await wait(1500);
await cdp.send('HeapProfiler.startSampling', {
samplingInterval: 128,
includeObjectsCollectedByMajorGC: true,
includeObjectsCollectedByMinorGC: true,
});
await reportTransitVisibility(
page,
`${count} budget visibility before allocation trace`,
);
const allocationTrace = await page.evaluate(() =>
window.__godsEyeView.dataManager.layers
.get('transit')
.module._measureTransitFramesForTest(6000),
);
const { profile } = await cdp.send('HeapProfiler.stopSampling');
const allocations = allocationTotals(profile);
const retainedBytes = retained.usedSize - before.usedSize;
const evidence = reduceFleet(
trace.rows,
count,
allocations.layerFrameBytes / allocationTrace.rows.length,
retainedBytes,
loaded.fixBytes,
);
const details = {
...evidence,
...allocations,
wholeFrameBytesPerFrame:
allocations.frameBytes / allocationTrace.rows.length,
topAllocatorsPerFrame: allocations.topAllocators.map((a) => ({
...a,
bytesPerFrame: a.bytes / allocationTrace.rows.length,
})),
allocationFrames: allocationTrace.rows.length,
loaded,
dpr: trace.dpr,
viewport: [trace.width, trace.height],
heapMethod:
'GC-bracketed whole-page growth after fleet load (conservative)',
allocationMethod:
'CDP 128-byte sampling including collected objects; transit source self allocation gates, whole render stacks reported, QA scaffolding excluded',
};
await writeFile(
`${shots}/${tag}-fleet-${count}.json`,
JSON.stringify(details, null, 2),
);
await writeFile(
`${shots}/${tag}-fleet-${count}-allocations.json`,
JSON.stringify(profile),
);
check(
`${count} moving vehicles meet CPU, frame interval, billboard upload, heap and allocation budgets`,
evidence.pass && trace.width === 1920 && trace.height === 1080,
JSON.stringify(details),
);
check(
`${count}: transit-authored frame allocation <= 8 KiB`,
allocationTrace.rows.length >= 100 &&
reduceMotion(allocationTrace.rows).pass &&
allocationTrace.rows.every((r) => r.moving === count) &&
allocations.layerFrameBytes / allocationTrace.rows.length <= 8192,
`${allocations.layerFrameBytes / allocationTrace.rows.length} transit-authored bytes/frame; whole frame ${allocations.frameBytes / allocationTrace.rows.length}; top three ${JSON.stringify(details.topAllocatorsPerFrame)}; collected objects included. Full profile saved.`,
);
await page.screenshot({
path: `${shots}/${tag}-fleet-${count}.jpg`,
type: 'jpeg',
quality: 85,
});
const settled = await page.evaluate(async () => {
const layer =
window.__godsEyeView.dataManager.layers.get('transit').module;
const state = layer._transitStateForTest();
const { seek } = await import('/src/data/contactPlayback.js');
const { updatePlayback } =
await import('/src/layers/transit/movement.js');
for (const entry of state._vehicles.values()) {
entry.clocks.wallNowMs = Date.now();
entry.clocks.monoNowMs = performance.now();
seek(entry.track, entry.fixes.at(-1).t, entry.clocks);
updatePlayback(entry, Date.now(), performance.now());
layer._transitPartsForTest().rendering.schedulePlayback(entry);
}
layer._transitPartsForTest().rendering.syncRenderHold();
return { moving: state._moving.size, held: state._renderHeld };
});
check(
`${count}: settling releases the transit hold`,
settled.moving === 0 && !settled.held,
JSON.stringify(settled),
);
}
await cdp.detach();
}
export async function runBostonMatrix({
page,
check,
wait,
shots,
tag,
selectStyle,
sampleRendered,
detectOn,
}) {
console.log(
'\n== Boston matrix: scripted Red, 741, 742, Green-E; actual map backgrounds ==',
);
await detectOn(page);
const routes = ['Red', '741', '742', 'Green-E'];
const looks = [
{ name: 'normal', style: 'normal', params: {} },
{ name: 'nvg', style: 'surveillance', params: {} },
{ name: 'noir', style: 'noir', params: {} },
{
name: 'white-hot',
style: 'thermal',
params: { 'WHOT/BHOT': 0, Ironbow: 0 },
},
{
name: 'black-hot',
style: 'thermal',
params: { 'WHOT/BHOT': 1, Ironbow: 0 },
},
{
name: 'ironbow',
style: 'thermal',
params: { 'WHOT/BHOT': 0, Ironbow: 1 },
},
];
// Actual Boston views. Acceptance checks the measured background; a tile/key
// failure or an unsuitable view is explicitly unexercised, never assumed bright/dark.
const backgrounds = [
{
name: 'bright',
candidates: [
{ lat: 42.3458, lon: -71.0464 },
{ lat: 42.3659, lon: -71.018 },
{ lat: 42.3526, lon: -71.0461 },
{ lat: 42.3486, lon: -71.0825 },
],
},
{
name: 'dark',
candidates: [
{ lat: 42.3574, lon: -71.0375 },
{ lat: 42.3554, lon: -71.0655 },
{ lat: 42.3588, lon: -71.0738 },
{ lat: 42.348, lon: -71.032 },
],
},
];
const evidence = [];
for (const group of backgrounds) {
let background, backgroundResult;
const attempts = [];
await selectStyle(page, 'normal');
for (const candidate of group.candidates) {
background = { ...candidate, name: group.name };
await page.evaluate((b) => {
const camera = window.__godsEyeView.viewer.camera,
C = camera.positionCartographic.constructor;
camera.setView({
destination: C.toCartesian(C.fromDegrees(b.lon, b.lat, 600)),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
}, background);
await wait(8000);
await page.evaluate(
({ background, routes }) => {
const layer =
window.__godsEyeView.dataManager.layers.get('transit').module;
layer._loadTransitFleetForTest(8, background, 45, routes);
for (const entry of layer._transitStateForTest()._vehicles.values()) {
entry.track.rate = 0;
layer._transitPartsForTest().rendering.schedulePlayback(entry);
}
},
{ background, routes },
);
await wait(1000);
backgroundResult = reduceBackground(
group.name,
await sampleRendered(page),
);
attempts.push({ background, ...backgroundResult });
if (backgroundResult.pass) break;
}
check(
`Boston ${background.name}: all four routes over measured ${background.name} background`,
backgroundResult.pass,
JSON.stringify({ attempts, chosen: background }),
{ unexercised: !backgroundResult.pass },
);
for (const look of looks) {
await selectStyle(page, look.style, look.params);
await wait(1200);
await reportTransitVisibility(
page,
`Boston ${background.name}/${look.name} visibility`,
);
const pixels = await sampleRendered(page);
const modes = await page.evaluate(() => {
const layer =
window.__godsEyeView.dataManager.layers.get('transit').module;
return [...layer._transitStateForTest()._vehicles.values()].map(
(e) => ({
key: e.key,
route: e.record.routeId,
image: e.marker.image,
width: e.marker.width,
color: e.marker.color.toCssHexString(),
}),
);
});
const contrast =
look.name === 'normal'
? null
: reduceSensorContrast(
pixels,
look.name === 'black-hot' ? 'black' : 'white',
look.style,
);
check(
`Boston ${background.name}/${look.name}: all four silhouettes and mode brackets readable`,
pixels.length >= 6 &&
pixels.every(
(p) =>
p.bracket &&
p.bracketExpected &&
Math.hypot(...p.bracket.map((v, i) => v - p.bracketExpected[i])) <
48,
) &&
(!contrast || contrast.pass),
JSON.stringify({ pixels, contrast }),
{ unexercised: pixels.length < 6 },
);
await page.screenshot({
path: `${shots}/${tag}-matrix-${background.name}-${look.name}-fleet.jpg`,
type: 'jpeg',
quality: 90,
});
for (const target of modes) {
await page.evaluate(
(key) =>
window.__godsEyeView.dataManager.layers
.get('transit')
.module._transitPartsForTest()
.selection.selectVehicle(key),
target.key,
);
await wait(500);
await page.screenshot({
path: `${shots}/${tag}-matrix-${background.name}-${look.name}-${target.route}-selected.jpg`,
type: 'jpeg',
quality: 90,
});
const restored = await page.evaluate((key) => {
const layer =
window.__godsEyeView.dataManager.layers.get('transit').module;
layer._transitPartsForTest().selection.clearSelection();
const e = layer._transitStateForTest()._vehicles.get(key);
return {
image: e.marker.image,
width: e.marker.width,
color: e.marker.color.toCssHexString(),
};
}, target.key);
check(
`Boston ${background.name}/${look.name}/${target.route}: deselect restores fleet styling`,
restored.image === target.image &&
restored.width === target.width &&
restored.color === target.color,
);
}
await wait(300);
await page.screenshot({
path: `${shots}/${tag}-matrix-${background.name}-${look.name}-restored.jpg`,
type: 'jpeg',
quality: 90,
});
evidence.push({
background,
look,
pixels,
routes: modes.map((m) => m.route),
});
}
}
await selectStyle(page, 'normal');
await writeFile(
`${shots}/${tag}-boston-matrix.json`,
JSON.stringify(evidence, null, 2),
);
}
/** Real MBTA observations at oblique street views, separate from matrix fixtures. */
export async function runBostonLive({
page,
check,
wait,
shots,
tag,
detectOn,
selectStyle,
sampleRendered,
}) {
console.log('\n== Boston live oblique streets ==');
await page.evaluate(async () => {
const app = window.__godsEyeView;
await app.dataManager.setEnabled('transit', false, { source: 'qa' });
const camera = app.viewer.camera,
C = camera.positionCartographic.constructor;
camera.setView({
destination: C.toCartesian(C.fromDegrees(-71.0605, 42.3554, 600)),
orientation: { heading: 0, pitch: -Math.PI / 4, roll: 0 },
});
await app.dataManager.setEnabled('transit', true, { source: 'qa' });
const layer = app.dataManager.layers.get('transit').module;
layer._setTransitFixtureFloorsForTest([], 0);
await layer.update();
});
await wait(20000);
await detectOn(page);
const evidence = [];
for (const view of [
{
name: 'white-hot',
style: 'thermal',
altitude: 120,
heading: 0,
pitch: -20,
params: { 'WHOT/BHOT': 0, Ironbow: 0 },
},
{
name: 'nvg',
style: 'surveillance',
altitude: 600,
heading: 30,
pitch: -45,
params: {},
},
{
name: 'noir',
style: 'noir',
altitude: 400,
heading: 60,
pitch: -35,
params: {},
},
]) {
await page.evaluate((view) => {
const app = window.__godsEyeView,
camera = app.viewer.camera,
C = camera.positionCartographic.constructor;
app.dataManager.layers
.get('transit')
.module._transitPartsForTest()
.selection.clearSelection();
camera.setView({
destination: C.toCartesian(
C.fromDegrees(-71.0605, 42.3554, view.altitude),
),
orientation: {
heading: (view.heading * Math.PI) / 180,
pitch: (view.pitch * Math.PI) / 180,
roll: 0,
},
});
}, view);
await selectStyle(page, view.style, view.params);
await wait(8000);
const fleet = await page.evaluate(() => {
const state = window.__godsEyeView.dataManager.layers
.get('transit')
.module._transitStateForTest();
const entries = [...state._vehicles.values()];
return {
total: entries.length,
visible: entries.filter((e) => e.marker?.show).length,
fixtures: entries.filter((e) => e.qaFixture).length,
routes: [
...new Set(
entries.filter((e) => e.marker?.show).map((e) => e.record.routeId),
),
],
};
});
check(
`Boston live/${view.name}: observed vehicles in the oblique street view`,
fleet.total > 0 && fleet.visible > 0 && fleet.fixtures === 0,
JSON.stringify({ view, fleet }),
{ unexercised: fleet.visible === 0 },
);
const pixels = await sampleRendered(page);
const contrast = reduceSensorContrast(pixels, 'white', view.style);
check(
`Boston live/${view.name}: unobscured sensor contrast`,
contrast.pass,
JSON.stringify(contrast),
{ unexercised: contrast.unexercised },
);
const target = pixels[0];
let selection = null;
if (target) {
await page.mouse.click(target.x, target.y);
await wait(250);
selection = await page.evaluate((key) => {
const state = window.__godsEyeView.dataManager.layers
.get('transit')
.module._transitStateForTest();
return {
expected: key,
actual: state._selectedKey,
pick: state._lastPickForTest ?? null,
};
}, target.key);
}
check(
`Boston live/${view.name}: real mouse selects an observed vehicle`,
selection?.actual === target?.key && !!target,
JSON.stringify(selection),
{ unexercised: !target },
);
await page.screenshot({
path: `${shots}/${tag}-boston-live-${view.name}.jpg`,
type: 'jpeg',
quality: 90,
});
evidence.push({ view, fleet, pixels, contrast, selection });
}
await writeFile(
`${shots}/${tag}-boston-live.json`,
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(),
);
}