Files
gods-eye-view/scripts/qa-transit-scenes.mjs
T

1051 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 { captureQaScreenshot } from './qa-chrome.mjs';
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 captureQaScreenshot(page, {
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 captureQaScreenshot(page, {
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 captureQaScreenshot(page, {
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 captureQaScreenshot(page, {
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 captureQaScreenshot(page, {
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 captureQaScreenshot(page, {
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(),
);
}