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(), ); }