Delete dead render code and consolidate QA helpers

Remove code that nothing in the app, the runners or CI uses:
- the one-off migration compare, look and bench scripts and the vector
  ground prototype they measured, with the bench tables in the docs;
- the unused Navara-named shared render modules (polygons, ground picks,
  mover bank, ground ring);
- the Cesium-shaped aircraft and vessel test renderers, and the trail,
  engineFromViewer and horizon-occluder helpers only they kept alive.
  Their tests now run on the production displays over the fake engine;
- Navara's unread diagnostics, fallback ports, dead exports and
  parameters, stale effect-chain anchors and the unread capability flags.

The QA scripts share one keyless guard and readiness wait, the session
harness reuses the strict harness's browser flags and block list, and the
harness honours QA_ANGLE and QA_RESERVED_PORTS. The bikeshare fixture
keeps only what its scenario reads. The docs name the engine picker where
it is (VISUAL PRESETS) and drop dated run results.

No behaviour changes on either engine. Navara layer renderer lines fall
from 189 to 0 and viewer lines from 891 to 849.
This commit is contained in:
German Khokhlov committed 2026-11-08 17:05:38 +04:00
1 parent 18f69c2699
commit 371d848eec
130 files changed
+633 -4542

No files matched your search

+1 -5
View File
@@ -30,16 +30,12 @@ const globeEngine = (file) => /^src\/globe\/(?:cesium|navara)\//.test(file);
const cesiumPackage = /^(?:cesium(?:\/|$)|@cesium\/)/;
// Navara renders through three.js and its postprocessing peer.
const navaraPackage = /^(?:(?:three|postprocessing)(?:\/|$)|@navaramap\/)/;
// Navara renderers outside a layer's render/ directory: like those, they
// reach the app only through the Navara mode's import().
const NAVARA_SCENE_RENDERERS = new Set();
// The modules that load Navara renderers, each through import().
const NAVARA_RENDERER_LOADERS = new Set([
'src/app/navaraMode.js',
'src/app/layers/renderers.js',
]);
const navaraLayerRenderer = (file) =>
/\/render\/navara[^/]*\.js$/.test(file) || NAVARA_SCENE_RENDERERS.has(file);
const navaraLayerRenderer = (file) => /\/render\/navara[^/]*\.js$/.test(file);
const renderer = (file) =>
/^src\/(?:ui|app|standalone|overlays)\//.test(file) ||
/^src\/layers\/[^/]+\/(?:index|rendering|snapshotRenderer|overlay|presentation|cards|controls)\.js$/.test(
+3 -3
View File
@@ -1,9 +1,9 @@
{
"metrics": {
"cesiumLayerRendererLines": 0,
"navaraLayerRendererLines": 189,
"navaraLayerRendererLines": 0,
"cesiumImportFiles": 3,
"viewerLines": 891,
"viewerLines": 849,
"navaraKindChecks": 1,
"engineSniffCalls": 0,
"engineExtensionReads": 3,
@@ -12,5 +12,5 @@
"screenSpaceHandlerFiles": 4,
"engineNameLines": 32
},
"measuredAt": "Scope render demand to scene attachments and make the Cesium render backend independent"
"measuredAt": "Delete dead render code"
}
+14 -194
View File
@@ -1,25 +1,7 @@
import { cameras as alprCameras, cameraTile } from './alpr.mjs';
import { cameras as cctvCameras, cameraFrame } from './cctv.mjs';
import { stations as radioStations } from './radio.mjs';
import { earthquakePerformanceFixture } from './earthquakes.mjs';
export const COMBINED_EPOCH = Date.parse('2026-10-28T18:00:00Z');
export const COMBINED_ORIGIN = { lat: 42.3554, lon: -71.0605 };
const ORIGIN = { lat: 42.3554, lon: -71.0605 };
const grid = (i, cols, spacing) => ({
lat: COMBINED_ORIGIN.lat + (Math.floor(i / cols) - cols / 2) * spacing,
lon: COMBINED_ORIGIN.lon + ((i % cols) - cols / 2) * spacing,
});
const radio = radioStations.map((r, i) => ({ ...r, ...grid(i, 28, 0.001) }));
const cctv = cctvCameras.map((r, i) => ({
...r,
...grid(i, 5, 0.002),
cityId: 'boston',
city: 'Boston',
groundElevationM: 20,
}));
const alpr = alprCameras.map((r, i) => {
const p = grid(i, 19, 0.001);
return { ...r, latitude: p.lat, longitude: p.lon };
lat: ORIGIN.lat + (Math.floor(i / cols) - cols / 2) * spacing,
lon: ORIGIN.lon + ((i % cols) - cols / 2) * spacing,
});
const bikes = Array.from({ length: 200 }, (_, i) => ({
station_id: `w2-bike-${i}`,
@@ -27,154 +9,24 @@ const bikes = Array.from({ length: 200 }, (_, i) => ({
capacity: 20,
...grid(i, 15, 0.0015),
}));
const earthquakes = earthquakePerformanceFixture(500);
earthquakes.features = earthquakes.features.map((r, i) => {
const p = grid(i, 23, 0.002);
return {
...r,
properties: { ...r.properties, time: COMBINED_EPOCH - 3600000 },
geometry:
i < 5 ? { ...r.geometry, coordinates: [p.lon, p.lat, 5] } : r.geometry,
};
});
const fires = Array.from({ length: 6000 }, (_, i) => ({
...grid(i, 78, 0.001),
frp: 5 + (i % 120),
confidence: ['low', 'nominal', 'high'][i % 3],
brightness: 280 + (i % 90),
daynight: i % 2 ? 'N' : 'D',
acqDate: '2026-10-28',
acqTime: '1730',
instrument: 'VIIRS',
satellite: 'NOAA-20',
}));
const elements = Array.from({ length: 20 }, (_, i) => {
const p = grid(i, 5, 0.006),
d = 0.0007;
return {
type: 'way',
id: 980000 + i,
center: p,
tags: { military: 'airfield', name: `W2 facility ${i}` },
bounds: {
minlat: p.lat - d,
minlon: p.lon - d,
maxlat: p.lat + d,
maxlon: p.lon + d,
},
geometry: [
[p.lon - d, p.lat - d],
[p.lon + d, p.lat - d],
[p.lon + d, p.lat + d],
[p.lon - d, p.lat + d],
[p.lon - d, p.lat - d],
].map(([lon, lat]) => ({ lon, lat })),
};
});
export const combinedPayloads = {
radio,
cctv,
alpr,
bikes,
earthquakes,
fires,
elements,
};
export const combinedPayloads = { bikes };
/** One source transport fixture, shared unchanged by both engines. */
export async function installCombinedFixture(
page,
{ denseQuakes = false } = {},
) {
const quakePayload = denseQuakes
? {
...earthquakes,
features: earthquakes.features.map((r, i) => {
const p = grid(i, 23, 0.002);
return {
...r,
geometry: { ...r.geometry, coordinates: [p.lon, p.lat, 5] },
};
}),
}
: earthquakes;
const frame = await cameraFrame(),
requests = [];
/** Bikeshare and startup feed fixtures shared by both engines. */
export async function installCombinedFixture(page) {
const requests = [];
const session = await page.createCDPSession();
await session.send('Fetch.enable', {
patterns: [
...['127.0.0.1', 'localhost'].map((host) => ({
urlPattern: `http://${host}:*/api/*`,
})),
{ urlPattern: 'https://earthquake.usgs.gov/*all_day.geojson' },
{ urlPattern: 'https://tiles.dontgetflocked.com/*' },
{ urlPattern: '*://*.earthdata.nasa.gov/*' },
],
patterns: ['127.0.0.1', 'localhost'].map((host) => ({
urlPattern: `http://${host}:*/api/*`,
})),
});
let disposed = false;
session.on('Fetch.requestPaused', async (event) => {
try {
const u = new URL(event.request.url);
let body,
type = 'application/json';
let body;
const p = u.pathname;
if (u.hostname === 'earthquake.usgs.gov') body = quakePayload;
else if (u.hostname === 'tiles.dontgetflocked.com') {
if (p.endsWith('.json'))
body = {
tiles: [
'https://tiles.dontgetflocked.com/cameras-us-hourly/{z}/{x}/{y}.mvt',
],
bounds: [-160, 17, -64, 59],
};
else {
const m = p.match(/\/(\d+)\/(\d+)\/(\d+)\.mvt$/);
body = m
? cameraTile(...m.slice(1).map(Number), alpr)
: Buffer.alloc(0);
type = 'application/x-protobuf';
}
} else if (u.hostname === 'cmr.earthdata.nasa.gov')
body = {
hits: 2,
items: ['2026-10-28', '2026-10-27'].map((day) => ({
umm: {
TemporalExtent: {
RangeDateTime: { BeginningDateTime: `${day}T12:00:00Z` },
},
AdditionalAttributes: [{ Name: 'CLOUD_COVERAGE', Values: ['0'] }],
SpatialExtent: {
HorizontalSpatialDomain: {
Geometry: {
BoundingRectangles: [
{
WestBoundingCoordinate: -72,
EastBoundingCoordinate: -70,
SouthBoundingCoordinate: 41,
NorthBoundingCoordinate: 43,
},
],
},
},
},
},
})),
};
else if (u.hostname.endsWith('.earthdata.nasa.gov')) {
body = frame;
type = 'image/png';
} else if (p === '/api/firms')
body = { fires, fetchedAt: COMBINED_EPOCH - 120000, stale: false };
else if (p === '/api/radio/stations')
body = {
stations: radio,
updatedAt: new Date(COMBINED_EPOCH).toISOString(),
stale: false,
degraded: false,
acceptedGeneration: 1,
catalogInstance: 'w2-combined-fixture',
};
else if (p.startsWith('/api/gbfs/'))
if (p.startsWith('/api/gbfs/'))
body = {
version: '3.0',
data: {
@@ -189,40 +41,11 @@ export async function installCombinedFixture(
})),
},
};
else if (p === '/api/military-installations')
body = {
status: 'ready',
retrievedAt: new Date(COMBINED_EPOCH).toISOString(),
saturated: false,
elements,
};
else if (p === '/api/cctv/sources') body = { sources: cctv };
else if (p === '/api/cctv/health')
body = {
cameras: cctv.map(({ id }) => ({
id,
status: 'online',
sourceKind: 'image',
updatedAt: COMBINED_EPOCH,
})),
};
else if (p.startsWith('/api/cctv/frame/')) {
body = frame;
type = 'image/png';
} else if (p.startsWith('/api/transit/'))
body = {
fetchedAt: COMBINED_EPOCH,
vehicles: [],
fixes: [],
epochs: [],
};
else if (p === '/api/flights') body = { states: [] };
else if (p === '/api/military') body = { ac: [] };
else if (p === '/api/vessels') body = { connected: false, vessels: [] };
else if (p === '/api/google/nearby-places') body = { places: [] };
else if (p === '/api/setup/status') body = { ready: true };
else if (p === '/api/openai/hud-summary')
body = { summary: 'W2 performance fixture' };
else if (p === '/api/terrain/heights')
body = {
results: (u.searchParams.get('points') || '')
@@ -244,15 +67,12 @@ export async function installCombinedFixture(
requestId: event.requestId,
responseCode: 200,
responseHeaders: [
{ name: 'Content-Type', value: type },
{ name: 'Content-Type', value: 'application/json' },
{ name: 'Access-Control-Allow-Origin', value: '*' },
{ name: 'Access-Control-Expose-Headers', value: 'Data-Present' },
{ name: 'Data-Present', value: 'true' },
],
body: (Buffer.isBuffer(body)
? body
: Buffer.from(JSON.stringify(body))
).toString('base64'),
body: Buffer.from(JSON.stringify(body)).toString('base64'),
});
} catch (error) {
if (
-83
View File
@@ -1,83 +0,0 @@
// USGS-shaped fixed observations; source parsers remain the production ones.
export const W2_EPOCH_MS = 1792152000000;
export function earthquakeFixture() {
const features = [];
for (const [region, lon, lat] of [
['sf', -122.42, 37.77],
['austin', -97.74, 30.27],
['alaska', -150.4, 61.0],
]) {
for (let i = 0; i < 5; i++) {
features.push({
type: 'Feature',
id: `qa-${region}-${i}`,
geometry: {
type: 'Point',
coordinates: [
lon + (i - 2) * 0.24,
lat + (i % 2) * 0.3,
[8, 70, 300, 301, 45][i],
],
},
properties: {
mag: [2.5, 3, 4, 5, 5.5][i],
place: `W2 ${region} event ${i}`,
time: W2_EPOCH_MS - i * 60000,
},
});
}
}
features.push({
type: 'Feature',
id: 'qa-below',
geometry: { type: 'Point', coordinates: [-122.42, 37.77, 5] },
properties: { mag: 2.4 },
});
features.push({
type: 'Feature',
id: 'qa-missing-mag',
geometry: { type: 'Point', coordinates: [-122.4, 37.7, 5] },
properties: { mag: null },
});
return {
type: 'FeatureCollection',
metadata: { generated: W2_EPOCH_MS },
features,
};
}
/** Dense regional and worldwide USGS-shaped load, separate from the oracle. */
export function earthquakePerformanceFixture(count = 500) {
let seed = 17;
const random = () => {
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0;
return seed / 4294967296;
};
return {
type: 'FeatureCollection',
features: Array.from({ length: count }, (_, i) => ({
type: 'Feature',
id: `perf-quake-${i}`,
properties: {
mag: 2.5 + (i % 7) * 0.5,
place: `Recorded performance event ${i}`,
time: W2_EPOCH_MS - i * 1000,
},
geometry: {
type: 'Point',
coordinates:
i < Math.min(100, count)
? [
-122.42 + (random() - 0.5),
37.77 + (random() - 0.5),
[8, 70, 301][i % 3],
]
: [
-180 + random() * 360,
-70 + random() * 140,
[8, 70, 301][i % 3],
],
},
})),
};
}
+1 -14
View File
@@ -1210,7 +1210,6 @@
"src/layers/military/renderer.test.mjs",
"src/layers/military/policy.test.mjs",
"src/testSupport/aircraftRecordingRenderer.mjs",
"src/testSupport/aircraftViewerRenderer.mjs",
"src/testSupport/cesiumMoversEngine.mjs",
"src/testSupport/cockpitEngine.mjs",
"src/layers/aircraft/render/contract.js",
@@ -1226,7 +1225,6 @@
"src/layers/vessels/testFixtures.mjs",
"src/layers/shared/render/engineView.js",
"src/globe/navara/ground/effect.js",
"src/layers/shared/render/navaraMoverBank.js",
"src/layers/shared/render/moverBank.js",
"src/layers/shared/render/imageAtlas.js",
"src/layers/bikeshare/presentation.js",
@@ -1258,8 +1256,6 @@
"src/globe/cesium/createCesiumEngine.test.mjs",
"src/globe/cesium/movers.test.mjs",
"src/globe/cesium/lines.test.mjs",
"src/globe/cesium/engineFromViewer.js",
"src/globe/cesium/engineFromViewer.test.mjs",
"src/globe/cesium/testing/fakeViewer.mjs",
"src/globe/cesium/testing/cameraViewer.mjs",
"src/app/globeEngine.js",
@@ -1376,7 +1372,6 @@
"src/layers/satellites/interaction.test.mjs",
"src/layers/launches/lifecycleSnapshot.test.mjs",
"src/globe/cesium/sceneProjection.js",
"src/globe/cesium/trail.js",
"src/layers/flights/trackingRefresh.test.mjs",
"scripts/qa-heading-navara.mjs",
"src/layers/aircraft/qaDrawn.test.mjs",
@@ -1389,7 +1384,6 @@
"src/layers/localAdsb/qaDrawn.js",
"src/layers/localAdsb/qaDrawn.test.mjs",
"src/tooling/w1RuntimeEvidence.test.mjs",
"scripts/fixtures/w2/earthquakes.mjs",
"src/layers/firms/rectangle.js",
"src/layers/firms/rectangle.test.mjs",
"src/layers/firms/viewport.test.mjs",
@@ -1561,7 +1555,6 @@
"src/layers/satellites/render/display.test.mjs",
"src/layers/vessels/render/display.js",
"src/layers/vessels/render/display.test.mjs",
"src/testSupport/vesselViewerRenderer.mjs",
"scripts/qa-wildlife-switch.mjs",
"scripts/fixtures/wildlife.mjs",
"src/layers/cctv/render/display.js",
@@ -1588,10 +1581,7 @@
"src/globe/navara/ground/pack.js",
"src/globe/navara/ground/pack.test.mjs",
"src/globe/navara/ground/pass.js",
"src/globe/navara/ground/vector.js",
"scripts/qa-ground-suite.mjs",
"scripts/qa-ground-bench.mjs",
"scripts/qa-ground-switch.mjs",
"docs/GLOBE-PRIMITIVES.md",
"src/layers/airQuality/render/display.js",
"src/layers/airQuality/render/display.test.mjs",
@@ -1639,8 +1629,6 @@
"src/layers/traffic/render/display.test.mjs",
"src/layers/directions/edgeCases.test.mjs",
"src/layers/streetLevel/edgeCases.test.mjs",
"scripts/qa-imagery-compare.mjs",
"scripts/qa-weather-compare.mjs",
"src/globe/cesium/imagery.js",
"src/globe/cesium/imagery.test.mjs",
"src/globe/cesium/imageryShell.test.mjs",
@@ -1654,9 +1642,8 @@
"src/layers/weather/render/display.test.mjs",
"src/layers/wind/render/display.test.mjs",
"src/layers/bhoteKoshi/render/display.test.mjs",
"src/layers/shared/render/navaraGroundOrder.test.mjs",
"src/globe/navara/navaraGroundOrder.test.mjs",
"src/globe/navara/ground/corridors.js",
"scripts/qa-ground-looks.mjs",
"src/globe/cesium/windFlow.js",
"src/testSupport/imageryDisplayEngines.mjs",
"src/data/geoImportDisplay.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"gzipBytes": 2066560,
"allowanceBytes": 20666,
"measuredAt": "HEAD 47d63128 with the layer displays loaded through import() (src/app/layers/displays.js), keyless build (GOOGLE_MAPS_API_KEY and CESIUM_ION_TOKEN empty), entry index-DEjtUM5w.js, 6 322 899 bytes, gzip level 9; HEAD 47d63128 itself measured 2 113 641 and the merge of main 92c8f8d9 2 090 274",
"measuredAt": "Keyless production build with dynamically loaded layer displays",
"allowance": "1% of the baseline; growth past it fails npm run check:main-chunk until the baseline is raised on purpose"
}
+3 -29
View File
@@ -1214,7 +1214,6 @@
"src/ui/stores/globeActions.js",
"src/ui/stores/recording.js",
"src/ui/i18n.js",
"src/globe/cesium/engineFromViewer.js",
"src/cameraAngles.js",
"src/globe/contract.js",
"src/geo/",
@@ -1377,7 +1376,6 @@
"src/ui/stores/sceneInteractions.js",
"src/ui/stores/dataPacks.js",
"src/ui/i18n.js",
"src/globe/cesium/engineFromViewer.js",
"src/globe/contract.js",
"src/geo/"
],
@@ -1466,7 +1464,6 @@
"src/geo/",
"src/time/clock.js",
"src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js",
"src/layers/shared/color.js",
"src/layers/aircraft/qaDrawn.js",
"src/data/bindContextStore.js"
@@ -1536,7 +1533,6 @@
"src/geo/",
"src/time/clock.js",
"src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js",
"src/layers/aircraft/qaDrawn.js",
"src/data/bindContextStore.js"
],
@@ -1574,8 +1570,7 @@
"src/globe/cesium/sceneProjection.js",
"src/globe/cesium/screenEvents.js",
"src/geo/",
"src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js"
"src/globe/contract.js"
],
"external": ["cesium"]
},
@@ -1622,7 +1617,6 @@
"src/globe/cesium/sceneProjection.js",
"src/geo/",
"src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js",
"src/overlays/bindOverlayHost.js",
"src/data/bindContextStore.js"
],
@@ -1797,7 +1791,6 @@
"src/geo/",
"src/time/clock.js",
"src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js",
"src/overlays/bindOverlayHost.js"
],
"external": ["cesium"]
@@ -1848,8 +1841,7 @@
"src/layers/installations/classLabel.js",
"src/layers/shared/color.js",
"src/geo/",
"src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js"
"src/globe/contract.js"
],
"external": ["cesium", "@mapbox/vector-tile", "pbf"]
},
@@ -1903,8 +1895,7 @@
"src/ui/unitLabels.js",
"src/globe/cesium/sceneProjection.js",
"src/geo/",
"src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js"
"src/globe/contract.js"
],
"external": ["cesium"]
},
@@ -1966,7 +1957,6 @@
"src/i18n/locales/zh-Hant.json",
"src/geo/",
"src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js",
"src/layers/traffic/render/display.js"
],
"external": ["pbf", "@mapbox/vector-tile"]
@@ -2007,7 +1997,6 @@
"src/layers/shared/color.js",
"src/geo/",
"src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js",
"src/overlays/bindOverlayHost.js"
],
"external": []
@@ -2652,7 +2641,6 @@
"src/data/trafficFlowStyle.js",
"src/data/trafficPresetStyle.js",
"src/data/trafficQueue.js",
"src/data/trailRenderer.js",
"src/data/vesselLabels.js",
"src/data/vesselTypes.js",
"src/firstRunExperience.js",
@@ -3441,7 +3429,6 @@
"src/globe/cesium/surface.js",
"src/globe/cesium/movers.js",
"src/globe/cesium/lines.js",
"src/globe/cesium/engineFromViewer.js",
"src/globe/cameraMath.js",
"src/globe/navara/cameraModel.js",
"src/globe/navara/createNavaraEngine.js",
@@ -3468,7 +3455,6 @@
"src/annotations/drawSurface.js",
"src/globe/cesium/sceneProjection.js",
"src/globe/cesium/screenEvents.js",
"src/globe/cesium/trail.js",
"src/layers/alpr/overlay.js",
"src/layers/alpr/presentation.js",
"src/layers/bikeshare/selectionModel.js",
@@ -3520,17 +3506,13 @@
"src/layers/shared/render/engineView.js",
"src/globe/cesium/celestial.js",
"src/celestialRing/view.js",
"src/layers/shared/render/navaraPolygons.js",
"src/globe/navara/ground/pulse.js",
"src/globe/navara/sceneDepth.js",
"src/layers/bhoteKoshi/render/story.js",
"src/globe/navara/windFlowMesh.js",
"src/layers/shared/render/navaraGroundPicks.js",
"src/layers/shared/render/groundRing.js",
"src/layers/bikeshare/stationPoints.js",
"src/layers/shared/viewRectangle.js",
"src/layers/firms/rectangle.js",
"src/layers/shared/render/navaraMoverBank.js",
"src/layers/shared/render/moverBank.js",
"src/layers/radio/screenClusters.js",
"src/layers/shared/render/imageAtlas.js",
@@ -3805,7 +3787,6 @@
"src/data/trafficFlowStyle.js",
"src/data/trafficPresetStyle.js",
"src/data/trafficQueue.js",
"src/data/trailRenderer.js",
"src/data/vesselLabels.js",
"src/data/vesselTypes.js",
"src/keylessGeocoder.js",
@@ -4259,7 +4240,6 @@
"src/ui/stores/bhoteKoshiPanel.js",
"src/hudReadouts.js",
"src/ui/i18n.js",
"src/globe/cesium/engineFromViewer.js",
"src/globe/cesium/createCesiumEngine.js",
"src/globe/cesium/maps.js",
"src/globe/cesium/terrain.js",
@@ -4284,7 +4264,6 @@
"src/globe/contract.js",
"src/globe/cesium/sceneProjection.js",
"src/globe/cesium/screenEvents.js",
"src/globe/cesium/trail.js",
"src/layers/alpr/overlay.js",
"src/layers/alpr/presentation.js",
"src/layers/bikeshare/selectionModel.js",
@@ -4492,7 +4471,6 @@
"src/sources/capability.js",
"src/data/cameraArrival.js",
"src/data/cameraFraming.js",
"src/globe/cesium/engineFromViewer.js",
"src/globe/cesium/credits.js",
"src/globe/contract.js",
"src/geo/",
@@ -4524,7 +4502,6 @@
"layer-lifecycle": {
"exports": ["./data/lifecycle"],
"modules": [
"src/globe/cesium/engineFromViewer.js",
"src/globe/contract.js",
"src/data/lifecycle.js",
"src/data/layerSceneBinding.js",
@@ -4552,7 +4529,6 @@
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json",
"src/i18n/index.js",
"src/globe/cesium/engineFromViewer.js",
"src/globe/contract.js"
],
"external": ["cesium"]
@@ -5154,7 +5130,6 @@
"src/globe/cesium/terrain.js",
"src/globe/cesium/maps.js",
"src/globe/cesium/createCesiumEngine.js",
"src/globe/cesium/engineFromViewer.js",
"src/globe/cesium/projector.js",
"src/globe/cesium/render.js",
"src/globe/cesium/credits.js",
@@ -5752,7 +5727,6 @@
"src/globe/cesium/sceneProjection.js",
"src/geo/",
"src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js",
"src/globe/cesium/credits.js",
"src/layers/wildlifeSightings/display.js",
"src/data/spriteOrder.js",
+2
View File
@@ -18,6 +18,7 @@ import assert from 'node:assert/strict';
import puppeteer from 'puppeteer';
import { APPLICATION_LAYER_DIRECTORY } from '../src/app/layers/directory.js';
import {
assertPreviewPortAvailable,
createStrict,
ENGINES,
localPreview,
@@ -147,6 +148,7 @@ if (flag('serve')) {
? resolve(String(flag('scratch')))
: tmpdir();
mkdirSync(scratchRoot, { recursive: true });
await assertPreviewPortAvailable(base.href);
const scratch = mkdtempSync(join(scratchRoot, 'gev-acceptance-build-'));
const worktree = join(scratch, 'checkout');
const dist = join(scratch, 'dist');
+5 -6
View File
@@ -1,3 +1,4 @@
import { localPreview, waitForEngine } from './qa-strict-harness.mjs';
// The application is created once per page: switching engines detaches and
// attaches only the engine-bound parts. This gate runs three Cesium→Navara→
// Cesium round trips and checks that every session object keeps its identity,
@@ -14,15 +15,13 @@ import {
listenerCounts,
listenerDifference,
openSessionPage,
previewBase,
readSession,
requestEngine,
sessionAddress,
switchEngine,
waitReady,
} from './qa/appSessionHarness.mjs';
const base = previewBase(process.argv[2]);
const base = localPreview(process.argv[2]);
const reportOnly = process.argv.includes('--report');
const report = { switches: [], failures: [] };
const check = (condition, message, detail) => {
@@ -139,7 +138,7 @@ try {
browser,
sessionAddress(base, { engine: 'cesium' }),
);
await waitReady(page, 'cesium');
await waitForEngine(page, 'cesium', { timeout: 90_000 });
// Let the first boot's deferred listeners land before counting.
await new Promise((resolve) => setTimeout(resolve, 2000));
const boot = await readSession(page);
@@ -335,7 +334,7 @@ try {
await page.waitForFunction(() => window.__qaRollback === true, {
timeout: 60_000,
});
await waitReady(page, 'cesium');
await waitForEngine(page, 'cesium', { timeout: 90_000 });
const rolledBack = await readSession(page);
const ui = await readUi(page);
report.rollback = {
@@ -388,7 +387,7 @@ try {
await page.waitForFunction(() => window.__qaDataRefused === true, {
timeout: 60_000,
});
await waitReady(page, 'cesium');
await waitForEngine(page, 'cesium', { timeout: 90_000 });
const afterDataFailure = await readSession(page);
report.dataFailure = {
ids: idDifference(boot.ids, afterDataFailure.ids),
+4 -3
View File
@@ -1,3 +1,4 @@
import { assertKeylessPreview } from './qa-strict-harness.mjs';
import assert from 'node:assert/strict';
import { promisify } from 'node:util';
import { execFile } from 'node:child_process';
@@ -55,10 +56,10 @@ await strict.run({}, async (b) => {
window.__godsEyeView?.engine?.kind === 'navara' &&
document.querySelector('#loading-screen.hidden'),
);
await assertKeylessPreview(p);
await p.evaluate(async () => {
const { dataManager, styleManager, tileset } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
throw Error('Expected keyless preview');
const { dataManager, styleManager } = window.__godsEyeView;
await styleManager.initialRestorePromise;
await dataManager.setEnabled('cctv', true, { origin: 'user' });
window.qaCctv = dataManager.layers.get('cctv').module;
+2 -3
View File
@@ -90,9 +90,8 @@ await strict.run({}, async (b) => {
await p.goto(base.href);
await waitForEngine(p, 'navara');
await p.evaluate(async () => {
const { dataManager, styleManager, tileset } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
throw Error('Require keyless preview');
const { dataManager, styleManager } = window.__godsEyeView;
await dataManager.setEnabled('ais-live-vessels', true, { origin: 'user' });
window.qaBefore = [...dataManager.getEnabledLayerIds()].sort();
const outcome = await styleManager.setContextMode('contacts');
+2 -3
View File
@@ -56,9 +56,8 @@ await strict.run({}, async (b) => {
await p.goto(base.href);
await waitForEngine(p, 'navara');
await p.evaluate(async () => {
const { tileset, dataManager } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
throw Error('Keyless preview required');
const { dataManager } = window.__godsEyeView;
window.qaRoute = dataManager.layers.get('directions').module;
await dataManager.setEnabled('directions', true, { origin: 'user' });
});
+3 -2
View File
@@ -1,3 +1,4 @@
import { assertKeylessPreview } from './qa-strict-harness.mjs';
import assert from 'node:assert/strict';
import sharp from 'sharp';
import { createStrict, localPreview } from './qa-strict-harness.mjs';
@@ -181,10 +182,10 @@ await strict.run({}, async (b) => {
window.__godsEyeView?.sceneDirector &&
document.querySelector('#loading-screen.hidden'),
);
await assertKeylessPreview(p);
await p.evaluate(async (project) => {
const a = window.__godsEyeView;
if (window.__GOOGLE_MAPS_API_KEY__ || a.tileset)
throw Error('Expected keyless preview');
await a.styleManager.initialRestorePromise;
window.director = a.sceneDirector;
await window.director.importProjectFile(
+2 -2
View File
@@ -1,3 +1,4 @@
import { assertKeylessPreview } from './qa-strict-harness.mjs';
import assert from 'node:assert/strict';
import sharp from 'sharp';
import { createStrict, localPreview } from './qa-strict-harness.mjs';
@@ -69,10 +70,9 @@ await strict.run({}, async (browser) => {
{ timeout: 90000 },
kind,
);
await assertKeylessPreview(page);
await page.evaluate(async () => {
await window.__godsEyeView.styleManager.initialRestorePromise;
if (window.__GOOGLE_MAPS_API_KEY__ || window.__godsEyeView.tileset)
throw new Error('Keyless preview required');
});
assert.equal(await page.$eval('#globe-engine', (el) => el.value), kind);
}
+4 -3
View File
@@ -1,3 +1,4 @@
import { assertKeylessPreview } from './qa-strict-harness.mjs';
import assert from 'node:assert/strict';
import { installAlprFixture, cameras } from './fixtures/w2/alpr.mjs';
import { firmsFixture, FIRMS_VALID_COUNT } from './fixtures/w2/firms.mjs';
@@ -46,10 +47,10 @@ await strict.run({}, async (b) => {
window.__godsEyeView?.engine?.kind === 'navara' &&
document.querySelector('#loading-screen.hidden'),
);
await assertKeylessPreview(p);
await p.evaluate(async (id) => {
const { dataManager, styleManager, tileset } = window.__godsEyeView;
if (window.__GOOGLE_MAPS_API_KEY__ || tileset)
throw Error('Keyless preview required');
const { dataManager, styleManager } = window.__godsEyeView;
await styleManager.initialRestorePromise;
window.qaGround = dataManager.layers.get(id).module;
await dataManager.setEnabled(id, true, { origin: 'user' });
-558
View File
@@ -1,558 +0,0 @@
#!/usr/bin/env node
/**
* Measure engine.ground at Dubai (1280×800, keyless) with N polygons:
* Cesium (GroundPrimitive), Navara's screen pass (engine.ground,
* src/globe/navara/ground/pass.js) and the Navara vector-layer prototype
* (src/globe/navara/ground/vector.js). For each N: the time from commit to
* the first item drawn, the frame time with the camera still and turning
* (preUpdate to postRender, closed by a 1-pixel readPixels so the GPU work
* of the frame is in it), the JS heap after a GC and the WebGL bytes the
* page allocated (a tally of bufferData, texImage/texStorage and
* renderbuffer storage, less deletions), each against the empty scene; then
* one polygon over the Hajar mountains, obliquely, for the edge quality.
*
* GOOGLE_MAPS_API_KEY= CESIUM_ION_TOKEN= npm run dev -- --port 4271
* node scripts/qa-ground-bench.mjs http://localhost:4271 [--counts=10,1000,10000]
* [--paths=cesium,navara-pass,navara-vector] [--out=DIR]
*/
import fs from 'node:fs';
import path from 'node:path';
import puppeteer from 'puppeteer';
const BLOCKED_HOSTS =
/(^|\.)(googleapis\.com|google\.com|gstatic\.com|cesium\.com)$/;
const args = process.argv.slice(2);
const origin = args.find((arg) => !arg.startsWith('--'));
const option = (name) =>
args.find((arg) => arg.startsWith(`--${name}=`))?.slice(name.length + 3);
if (!origin) {
console.error('Usage: node scripts/qa-ground-bench.mjs <origin> [options]');
process.exit(2);
}
const counts = (option('counts') ?? '10,1000,10000').split(',').map(Number);
// polygons: N small polygons; corridors: N 40-point lines across the view,
// 2.5 px; outline: one polygon of N vertices round the view.
const shape = option('shape') ?? 'polygons';
const paths = (option('paths') ?? 'cesium,navara-pass,navara-vector').split(
',',
);
const out = option('out') ?? null;
if (out) fs.mkdirSync(out, { recursive: true });
// The page's WebGL allocations, before any script of the app runs.
function installGpuTally() {
const proto = WebGL2RenderingContext.prototype;
const sizes = new Map();
let total = 0;
const boundBuffers = new WeakMap();
const boundTextures = new WeakMap();
const boundRenderbuffers = new WeakMap();
const set = (key, bytes) => {
total += bytes - (sizes.get(key) ?? 0);
sizes.set(key, bytes);
};
const drop = (key) => {
total -= sizes.get(key) ?? 0;
sizes.delete(key);
};
const per = (map, gl) => {
if (!map.has(gl)) map.set(gl, new Map());
return map.get(gl);
};
const FLOAT = 0x1406;
const HALF_FLOAT = 0x140b;
const internalBytes = {
0x8814: 16, // RGBA32F
0x881a: 8, // RGBA16F
0x8815: 12, // RGB32F
0x822e: 4, // R32F
0x8230: 8, // RG32F
0x8058: 4, // RGBA8
0x8c43: 4, // SRGB8_ALPHA8
0x88f0: 4, // DEPTH24_STENCIL8
0x8cad: 8, // DEPTH32F_STENCIL8
0x8cac: 4, // DEPTH_COMPONENT32F
0x81a6: 4, // DEPTH_COMPONENT24
0x81a5: 2, // DEPTH_COMPONENT16
0x8229: 1, // R8
0x822b: 2, // RG8
};
const texelBytes = (internal, type) =>
internalBytes[internal] ??
(type === FLOAT ? 16 : type === HALF_FLOAT ? 8 : 4);
const wrap = (name, after) => {
const original = proto[name];
proto[name] = function (...call) {
const result = original.apply(this, call);
try {
after.call(this, call, result);
} catch {
// A tally error never breaks the page.
}
return result;
};
};
wrap('bindBuffer', function ([target, buffer]) {
per(boundBuffers, this).set(target, buffer);
});
wrap('bufferData', function ([target, data]) {
const buffer = per(boundBuffers, this).get(target);
if (buffer)
set(buffer, typeof data === 'number' ? data : (data?.byteLength ?? 0));
});
wrap('deleteBuffer', ([buffer]) => drop(buffer));
wrap('bindTexture', function ([target, texture]) {
per(boundTextures, this).set(target, texture);
});
wrap('texImage2D', function (call) {
const texture = per(boundTextures, this).get(call[0]);
if (!texture || call[1] !== 0) return;
let bytes;
if (call.length >= 8 && typeof call[3] === 'number')
bytes = call[3] * call[4] * texelBytes(call[2], call[7]);
else {
const source = call[5];
bytes = (source?.width ?? 0) * (source?.height ?? 0) * 4;
}
set(texture, bytes);
});
wrap('texStorage2D', function ([target, levels, internal, width, height]) {
const texture = per(boundTextures, this).get(target);
if (texture)
set(
texture,
width * height * texelBytes(internal) * (levels > 1 ? 4 / 3 : 1),
);
});
wrap('texImage3D', function (call) {
const texture = per(boundTextures, this).get(call[0]);
if (texture && call[1] === 0)
set(texture, call[3] * call[4] * call[5] * texelBytes(call[2], call[8]));
});
wrap('texStorage3D', function ([target, , internal, width, height, depth]) {
const texture = per(boundTextures, this).get(target);
if (texture) set(texture, width * height * depth * texelBytes(internal));
});
wrap('deleteTexture', ([texture]) => drop(texture));
wrap('bindRenderbuffer', function ([, renderbuffer]) {
boundRenderbuffers.set(this, renderbuffer);
});
wrap('renderbufferStorage', function ([, internal, width, height]) {
const renderbuffer = boundRenderbuffers.get(this);
if (renderbuffer) set(renderbuffer, width * height * texelBytes(internal));
});
wrap(
'renderbufferStorageMultisample',
function ([, samples, internal, width, height]) {
const renderbuffer = boundRenderbuffers.get(this);
if (renderbuffer)
set(
renderbuffer,
width * height * texelBytes(internal) * Math.max(1, samples),
);
},
);
wrap('deleteRenderbuffer', ([renderbuffer]) => drop(renderbuffer));
window.__gpuBytes = () => total;
}
const DUBAI = { lon: 55.27, lat: 25.15, alt: 70000 };
async function openEngine(browser, kind) {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
page.on('pageerror', (error) => console.log(' page error:', error.message));
await page.evaluateOnNewDocument(installGpuTally);
await page.setRequestInterception(true);
page.on('request', (request) => {
if (BLOCKED_HOSTS.test(new URL(request.url()).hostname)) request.abort();
else request.continue();
});
await page.goto(
`${origin}/?welcome=0&engine=${kind}#v=2&lat=${DUBAI.lat}&lon=${DUBAI.lon}&alt=${DUBAI.alt}&heading=0&pitch=-90&map=osm&l=`,
);
await page.waitForFunction(
(expected) =>
window.__godsEyeView?.engine?.kind === expected &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 300_000, polling: 250 },
kind,
);
// Daylight in Dubai (Navara lights the scene with the app clock's sun).
await page.evaluate(async () => {
const { appClock } = await import('/src/time/clock.js');
appClock.freeze(Date.UTC(2026, 10, 7, 8, 0));
window.__godsEyeView.engine.render.request('ground-bench-daylight');
});
const cdp = await page.createCDPSession();
await cdp.send('HeapProfiler.enable');
// Helpers in the page: polygons, the approach under test, timing.
await page.evaluate(() => {
const { engine } = window.__godsEyeView;
const random = (seed) => () => {
seed = (seed * 1664525 + 1013904223) >>> 0;
return seed / 2 ** 32;
};
const PALETTE = [
[1, 0.2, 0.2],
[1, 0.6, 0],
[1, 1, 0.2],
[0.2, 0.9, 0.3],
[0, 0.8, 1],
[0.3, 0.4, 1],
[0.7, 0.3, 1],
[0.9, 0.9, 0.9],
];
window.__bench = {
engine,
// N polygons over Dubai (deterministic): 10 vertices, 100–400 m;
// item 0 a 3 km magenta polygon at the view centre.
polygons(count) {
const next = random(7);
const items = [];
for (let index = 0; index < count; index++) {
const centre =
index === 0
? [55.27, 25.15]
: [54.95 + next() * 0.65, 24.95 + next() * 0.4];
const radius = index === 0 ? 3000 : 100 + next() * 300;
const ring = [];
for (let k = 0; k < 10; k++) {
const angle = (k / 10) * 2 * Math.PI;
const r = radius * (0.75 + 0.25 * (index === 0 ? 1 : next()));
ring.push([
centre[0] +
(r * Math.cos(angle)) /
(111320 * Math.cos((centre[1] * Math.PI) / 180)),
centre[1] + (r * Math.sin(angle)) / 110540,
]);
}
items.push({
rings: [ring],
// Eight colours, as the layers use a handful (depth bands,
// levels); the vector prototype makes one layer per style.
fill:
index === 0
? [1, 0, 1, 0.9]
: [...PALETTE[Math.floor(next() * PALETTE.length)], 0.35],
outline: { color: [1, 1, 1, 0.9], widthPx: 1 },
pickId: index,
});
}
return items;
},
// N - 1 lines, then the magenta mark on top of them.
corridors(count) {
const next = random(11);
const items = [];
for (let index = 1; index < count; index++) {
let lon = 54.95 + next() * 0.65;
let lat = 24.95 + next() * 0.4;
const heading = next() * 2 * Math.PI;
const positions = [];
for (let k = 0; k < 40; k++) {
positions.push([lon, lat]);
const turn = heading + (next() - 0.5) * 1.2;
lon += 0.004 * Math.cos(turn);
lat += 0.004 * Math.sin(turn);
}
items.push({
positions,
color: [...PALETTE[Math.floor(next() * PALETTE.length)], 0.9],
widthPx: 2.5,
pickId: index,
});
}
items.push(window.__bench.centreDisc());
return items;
},
// The magenta mark of the corridors scenario: a 40 px corridor.
centreDisc: () => ({
positions: [
[55.255, 25.15],
[55.285, 25.15],
],
color: [1, 0, 1, 0.9],
widthPx: 40,
pickId: 0,
}),
outline(count) {
const ring = [];
for (let k = 0; k < count; k++) {
const angle = (k / count) * 2 * Math.PI;
const wobble = 1 + 0.03 * Math.sin(angle * 37);
ring.push([
55.27 + 0.28 * wobble * Math.cos(angle),
25.15 + 0.2 * wobble * Math.sin(angle),
]);
}
return [
{
rings: [ring],
fill: [1, 0, 1, 0.5],
outline: { color: [1, 1, 1, 1], widthPx: 2 },
pickId: 0,
},
];
},
async ground(kind) {
if (kind !== 'navara-vector') return engine.ground;
const { createNavaraVectorGround } =
await import('/src/globe/navara/ground/vector.js');
window.__bench.vector ??= createNavaraVectorGround(
engine.extensions.navara.view,
);
return window.__bench.vector.ground;
},
frame: () =>
new Promise((resolve) => {
const remove = engine.frames.postRender.addEventListener(() => {
remove();
resolve();
});
engine.render.request('ground-bench');
}),
// Frame times in ms (preUpdate → postRender + 1 px readPixels).
async frameTimes(frames, turn = false) {
const gl = engine.host.canvas.getContext('webgl2');
const pixel = new Uint8Array(4);
const times = [];
let start = 0;
const removeStart = engine.frames.preUpdate.addEventListener(() => {
start = performance.now();
});
const removeEnd = engine.frames.postRender.addEventListener(() => {
gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel);
times.push(performance.now() - start);
});
const pose = engine.camera.pose();
try {
for (let index = 0; index < frames; index++) {
if (turn)
engine.camera.setView({
destination: pose.position,
orientation: {
heading: pose.heading + (index % 2 ? 0.002 : -0.002),
pitch: pose.pitch,
roll: 0,
},
});
await window.__bench.frame();
}
} finally {
removeStart();
removeEnd();
engine.camera.setView({
destination: pose.position,
orientation: { heading: pose.heading, pitch: pose.pitch, roll: 0 },
});
}
times.sort((a, b) => a - b);
const mean = times.reduce((a, b) => a + b, 0) / times.length;
return {
mean,
median: times[Math.floor(times.length / 2)],
p95: times[Math.floor(times.length * 0.95)],
};
},
// The centre pixel shows the magenta item.
centreMagenta() {
const canvas = engine.host.canvas;
const c = document.createElement('canvas');
c.width = 4;
c.height = 4;
const context = c.getContext('2d');
context.drawImage(
canvas,
canvas.width / 2 - 2,
canvas.height / 2 - 2,
4,
4,
0,
0,
4,
4,
);
const [r, g, b] = context.getImageData(1, 1, 1, 1).data;
// Lit (the vector layers) or exact (the pass, Cesium).
return r > 150 && b > 150 && g < Math.min(r, b) - 60;
},
};
});
return { page, cdp };
}
async function settleTiles(page) {
await page.evaluate(async () => {
const { engine, frame } = window.__bench;
const started = performance.now();
let loadedFrames = 0;
while (performance.now() - started < 60000 && loadedFrames < 30) {
await frame();
loadedFrames = engine.surface.tilesLoaded() ? loadedFrames + 1 : 0;
}
});
}
async function memory(page, cdp) {
await cdp.send('HeapProfiler.collectGarbage');
await cdp.send('HeapProfiler.collectGarbage');
return page.evaluate(() => ({
js: performance.memory.usedJSHeapSize,
gpu: window.__gpuBytes(),
navara:
window.__bench.engine.extensions?.navara?.view?.memoryStats?.()
?.gpuBytesEst ?? null,
}));
}
const mb = (bytes) => (bytes / 2 ** 20).toFixed(2);
const rows = [];
async function measure(browser, kind) {
const engineKind = kind === 'cesium' ? 'cesium' : 'navara';
const { page, cdp } = await openEngine(browser, engineKind);
const renderer = await page.evaluate(() => {
const gl = window.__bench.engine.host.canvas.getContext('webgl2');
const info = gl.getExtension('WEBGL_debug_renderer_info');
return info ? gl.getParameter(info.UNMASKED_RENDERER_WEBGL) : 'unknown';
});
console.log(`${kind}: ${renderer}`);
await settleTiles(page);
// Warm the approach (shader compile) with one item, then measure empty.
await page.evaluate(async (kind) => {
const ground = await window.__bench.ground(kind);
const set = ground.createSet({ id: 'bench-warm', kind: 'polygon' });
set.replace(window.__bench.polygons(1));
set.commit();
const started = performance.now();
while (
!window.__bench.centreMagenta() &&
performance.now() - started < 30000
)
await window.__bench.frame();
set.destroy();
for (let index = 0; index < 10; index++) await window.__bench.frame();
}, kind);
const emptyFrames = {
still: await page.evaluate(() => window.__bench.frameTimes(90)),
turning: await page.evaluate(() => window.__bench.frameTimes(90, true)),
};
rows.push({ path: kind, shape, count: 0, ...emptyFrames });
for (const count of counts) {
const before = await memory(page, cdp);
const built = await page.evaluate(
async (kind, count, shape) => {
const ground = await window.__bench.ground(kind);
const items =
shape === 'corridors'
? window.__bench.corridors(count)
: shape === 'outline'
? window.__bench.outline(count)
: window.__bench.polygons(count);
const set = ground.createSet({
id: 'bench',
kind: shape === 'corridors' ? 'corridor' : 'polygon',
});
const started = performance.now();
set.replace(items);
set.commit();
const committed = performance.now() - started;
while (
!window.__bench.centreMagenta() &&
performance.now() - started < 120000
)
await window.__bench.frame();
window.__bench.set = set;
const drawnMs = performance.now() - started;
for (let index = 0; index < 10; index++) await window.__bench.frame();
return { commitMs: committed, drawnMs };
},
kind,
count,
shape,
);
// Right after the build, before tiles stream again.
const loaded = await memory(page, cdp);
const passBytes = await page.evaluate(
() => window.__bench.engine.groundDiagnostics?.()?.textureBytes ?? null,
);
await settleTiles(page);
const frames = {
still: await page.evaluate(() => window.__bench.frameTimes(90)),
turning: await page.evaluate(() => window.__bench.frameTimes(90, true)),
};
if (out)
await page.screenshot({
path: path.join(out, `dubai-${shape}-${kind}-${count}.png`),
});
await page.evaluate(async () => {
window.__bench.set.destroy();
for (let index = 0; index < 20; index++) await window.__bench.frame();
});
const released = await memory(page, cdp);
const row = {
path: kind,
shape,
count,
...built,
...frames,
jsMB: mb(loaded.js - before.js),
gpuMB: mb(loaded.gpu - before.gpu),
navaraGpuMB:
loaded.navara == null ? null : mb(loaded.navara - before.navara),
passTexturesMB: passBytes == null ? null : mb(passBytes),
jsAfterDestroyMB: mb(released.js - before.js),
gpuAfterDestroyMB: mb(released.gpu - before.gpu),
};
rows.push(row);
console.log(
` N=${count}: drawn ${row.drawnMs.toFixed(0)} ms (commit ${row.commitMs.toFixed(0)}), frame still ${frames.still.median.toFixed(2)} ms (empty ${emptyFrames.still.median.toFixed(2)}), turning ${frames.turning.median.toFixed(2)} ms (empty ${emptyFrames.turning.median.toFixed(2)}), JS +${row.jsMB} MB, WebGL +${row.gpuMB} MB${row.navaraGpuMB == null ? '' : `, Navara gpuBytesEst +${row.navaraGpuMB} MB`}${row.passTexturesMB == null || kind !== 'navara-pass' ? '' : `, pass textures ${row.passTexturesMB} MB`}; after destroy JS +${row.jsAfterDestroyMB}, WebGL +${row.gpuAfterDestroyMB} MB`,
);
}
// Edge quality over terrain: one polygon on the Hajar mountains, oblique.
if (out && shape === 'polygons') {
await page.evaluate(async (kind) => {
const { engine, frame } = window.__bench;
const ground = await window.__bench.ground(kind);
const geo = await import('/src/geo/index.js');
engine.camera.setView({
destination: geo.cartesian3.fromDegrees(56.05, 24.98, 9000),
orientation: { heading: 0.5, pitch: -0.45, roll: 0 },
});
const set = ground.createSet({ id: 'bench-terrain', kind: 'polygon' });
const ring = [];
for (let k = 0; k < 24; k++) {
const angle = (k / 24) * 2 * Math.PI;
ring.push([
56.12 + 0.03 * Math.cos(angle),
25.07 + 0.022 * Math.sin(angle),
]);
}
set.replace([
{
rings: [ring],
fill: [1, 0.3, 0, 0.45],
outline: { color: [1, 1, 0, 1], widthPx: 2 },
},
]);
set.commit();
for (let index = 0; index < 20; index++) await frame();
}, kind);
await settleTiles(page);
await page.screenshot({ path: path.join(out, `terrain-${kind}.png`) });
}
await page.close();
}
const browser = await puppeteer.launch({ headless: true });
try {
for (const kind of paths) await measure(browser, kind);
} finally {
await browser.close();
}
if (out)
fs.writeFileSync(
path.join(out, `bench-${shape}.json`),
JSON.stringify(rows, null, 2),
);
+1 -2
View File
@@ -218,8 +218,7 @@ await strict.run({}, async (b) => {
await waitForEngine(p, 'navara');
await p.evaluate(() => {
const d = window.__godsEyeView;
if (d.tileset || window.__GOOGLE_MAPS_API_KEY__)
throw Error('Keyless required');
window.qaAir = d.dataManager.layers.get('air-quality').module;
window.qaGnss = d.dataManager.layers.get('gnss-integrity').module;
});
-290
View File
@@ -1,290 +0,0 @@
#!/usr/bin/env node
/**
* Compare the animated ground corridors with the Cesium drawing they
* replace, on the Cesium engine (keyless, headless): Street Traffic's jam
* heat lines (PolylineGlow, glowPower 0.25, alpha 0.55 ± 0.2 on
* sin(now / 260 ms)) and the Directions route (the annotation route's
* GevRouteFlow material, 9 px). One road and one route over Dubai, nadir,
* drawn at fixed page times (performance.now() frozen), so every variant
* shows the same phase of the pulse and the dashes:
*
* --variant=old the old drawing, rebuilt with Cesium directly: a
* GroundPolylinePrimitive with PolylineGlow, its alpha
* set from the frozen time, and the Directions route
* entity with FlowMaterialProperty;
* --variant=engine engine.ground: the jam as the traffic layer gives it
* (glow and pulse when the build has them), the route
* through the Directions display.
*
* Both drape on terrain and tiles ('both'): keyless there are no 3D tiles,
* where the traffic layer drapes its heat lines.
*
* `--engine=navara` draws the engine variant on Navara (daylight app clock;
* its frames at the live page time, 400 ms apart: Navara's frame loop stops
* on a frozen one).
*
* GOOGLE_MAPS_API_KEY= CESIUM_ION_TOKEN= npm run dev -- --port 4271
* node scripts/qa-ground-looks.mjs http://localhost:4271 --variant=old --out=DIR
* node scripts/qa-ground-looks.mjs --compare=DIR_A,DIR_B
*/
import fs from 'node:fs';
import path from 'node:path';
import puppeteer from 'puppeteer';
import sharp from 'sharp';
const args = process.argv.slice(2);
const option = (name) =>
args.find((arg) => arg.startsWith(`--${name}=`))?.slice(name.length + 3);
const BLOCKED_HOSTS =
/(^|\.)(googleapis\.com|google\.com|gstatic\.com|cesium\.com)$/;
// Page times (ms) the frames are drawn at: across one pulse (1.63 s) and
// a few dash cells.
const TIMES = [100_000, 100_400, 100_800, 101_200];
const CROP = { left: 340, top: 200, width: 600, height: 400 };
// Metres over the ground: the road and the route fill the crop.
const ALTITUDE = 6000;
async function compare([a, b]) {
const rows = [];
for (const name of fs
.readdirSync(a)
.filter((file) => file.endsWith('.png'))) {
if (!fs.existsSync(path.join(b, name))) continue;
const read = (dir) =>
sharp(path.join(dir, name))
.removeAlpha()
.raw()
.toBuffer({ resolveWithObject: true });
const [left, right] = await Promise.all([read(a), read(b)]);
let sum = 0;
let over = 0;
const pixels = left.info.width * left.info.height;
for (let i = 0; i < left.data.length; i += 3) {
let most = 0;
for (let k = 0; k < 3; k++)
most = Math.max(most, Math.abs(left.data[i + k] - right.data[i + k]));
sum += most;
if (most > 24) over++;
}
rows.push({
image: name,
meanDiff: +(sum / pixels).toFixed(2),
over24: +(over / pixels).toFixed(4),
});
}
console.table(rows);
}
async function draw(origin, variant, out, engineKind) {
fs.mkdirSync(out, { recursive: true });
const browser = await puppeteer.launch({
headless: true,
protocolTimeout: 600_000,
});
try {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
page.on('pageerror', (error) => console.log('page error:', error.message));
await page.setRequestInterception(true);
page.on('request', (request) => {
if (BLOCKED_HOSTS.test(new URL(request.url()).hostname)) request.abort();
else request.continue();
});
await page.goto(
`${origin}/?welcome=0&engine=${engineKind}#v=2&lat=25.2&lon=55.3&alt=${ALTITUDE}&heading=0&pitch=-90&map=osm&l=`,
);
await page.waitForFunction(
(kind) =>
window.__godsEyeView?.engine?.kind === kind &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 300_000, polling: 250 },
engineKind,
);
await page.evaluate(
async (variant, altitude) => {
const { engine } = window.__godsEyeView;
const { appClock } = await import('/src/time/clock.js');
appClock.freeze(Date.UTC(2026, 10, 7, 8, 0));
const frame = () =>
new Promise((resolve) => {
const remove = engine.frames.postRender.addEventListener(() => {
remove();
resolve();
});
engine.render.request('qa-ground-looks');
});
const geo = await import('/src/geo/index.js');
engine.camera.setView({
destination: geo.cartesian3.fromDegrees(55.3, 25.2, altitude),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
// Tiles in.
const started = performance.now();
let loaded = 0;
while (performance.now() - started < 60_000 && loaded < 30) {
await frame();
loaded = engine.surface.tilesLoaded() ? loaded + 1 : 0;
}
// A jam road across the view, and a route with turns below it.
const road = [
[55.288, 25.204],
[55.296, 25.206],
[55.304, 25.203],
[55.312, 25.205],
];
const route = [
[55.286, 25.196],
[55.294, 25.197],
[55.297, 25.192],
[55.306, 25.193],
[55.314, 25.196],
];
const policy = await import('/src/layers/traffic/policy.js');
const jamRgba = policy.HEAT_JAM_COLOR.rgba();
if (variant === 'old') {
// The app's own Cesium module: the URL the dev server gave the
// engine's import of it.
const served = await (
await fetch('/src/globe/cesium/ground.js')
).text();
const Cesium = await import(
served.match(/from\s+["']([^"']*cesium[^"']*)["']/)[1]
);
const { FlowMaterialProperty, ensureFlowFabricRegistered } =
await import('/src/globe/cesium/annotations/worldAnnotationRenderer.js');
const { viewer } = engine.extensions.cesium;
const color = new Cesium.Color(...jamRgba.slice(0, 3), 0.55);
const jam = viewer.scene.groundPrimitives.add(
new Cesium.GroundPolylinePrimitive({
geometryInstances: [
new Cesium.GeometryInstance({
geometry: new Cesium.GroundPolylineGeometry({
positions: Cesium.Cartesian3.fromDegreesArray(road.flat()),
width: 9,
}),
}),
],
classificationType: Cesium.ClassificationType.BOTH,
appearance: new Cesium.PolylineMaterialAppearance({
material: Cesium.Material.fromType('PolylineGlow', {
color,
glowPower: 0.25,
}),
}),
}),
);
ensureFlowFabricRegistered();
viewer.entities.add({
id: 'qa-route',
polyline: {
positions: Cesium.Cartesian3.fromDegreesArray(route.flat()),
width: 9,
material: new FlowMaterialProperty('#39d0ff'),
clampToGround: true,
classificationType: Cesium.ClassificationType.BOTH,
},
});
// The traffic animation set the jam's alpha every frame.
engine.frames.preRender.addEventListener(() => {
jam.appearance.material.uniforms.color.alpha =
policy.HEAT_JAM_BASE_ALPHA +
policy.HEAT_JAM_PULSE_ALPHA * Math.sin(performance.now() / 260);
});
window.__qaReady = () => jam.ready;
} else {
const { createDirectionsDisplay } =
await import('/src/layers/directions/render/display.js');
const animated = policy.HEAT_JAM_GLOW_POWER !== undefined;
const set = engine.ground.createSet({
id: 'qa-jam',
kind: 'corridor',
drape: 'both',
});
// As src/layers/traffic/rendering.js gives the jam bucket.
set.replace([
{
positions: road,
widthPx: policy.HEAT_LINE_JAM_WIDTH,
...(animated
? {
color: [...jamRgba.slice(0, 3), 1],
glow: { power: policy.HEAT_JAM_GLOW_POWER },
pulse: {
periodMs: policy.HEAT_JAM_PULSE_PERIOD_MS,
min:
policy.HEAT_JAM_BASE_ALPHA -
policy.HEAT_JAM_PULSE_ALPHA,
max:
policy.HEAT_JAM_BASE_ALPHA +
policy.HEAT_JAM_PULSE_ALPHA,
},
}
: {
color: [...jamRgba.slice(0, 3), policy.HEAT_JAM_BASE_ALPHA],
}),
},
]);
set.commit();
const display = createDirectionsDisplay(engine);
display.setRoute(route);
window.__qaReady = () => set.ready();
}
const until = Date.now() + 60_000;
while (!window.__qaReady() && Date.now() < until) await frame();
loaded = 0;
while (Date.now() < until + 60_000 && loaded < 30) {
await frame();
loaded = engine.surface.tilesLoaded() ? loaded + 1 : 0;
}
},
variant,
ALTITUDE,
);
for (const time of TIMES) {
await page.evaluate(async (time) => {
const { engine } = window.__godsEyeView;
// Navara's frame loop stops on a frozen page clock: there the frames
// are drawn at the live time, 400 ms apart.
if (engine.kind === 'navara')
await new Promise((resolve) => setTimeout(resolve, 400));
else performance.now = () => time;
for (let k = 0; k < 4; k++)
await new Promise((resolve) => {
const remove = engine.frames.postRender.addEventListener(() => {
remove();
resolve();
});
engine.render.request('qa-ground-looks');
});
}, time);
await page.screenshot({
path: path.join(out, `looks-${time}.png`),
clip: {
x: CROP.left,
y: CROP.top,
width: CROP.width,
height: CROP.height,
},
});
}
} finally {
await browser.close();
}
}
const pair = option('compare');
if (pair) await compare(pair.split(','));
else {
const origin = args.find((arg) => !arg.startsWith('--'));
if (!origin) {
console.error(
'Usage: node scripts/qa-ground-looks.mjs <origin> --variant=old|engine --out=DIR | --compare=A,B',
);
process.exit(2);
}
const variant = option('variant') ?? 'engine';
// The old drawing is Cesium's own; engine.ground also draws on Navara.
const engineKind =
variant === 'old' ? 'cesium' : (option('engine') ?? 'cesium');
await draw(origin, variant, option('out') ?? 'qa-ground-looks', engineKind);
}
-175
View File
@@ -1,175 +0,0 @@
#!/usr/bin/env node
/**
* Show and hide of ground fills on Navara before and after an engine switch
* (Navara → Cesium → Navara, the parked view taken up again): Navara 0.2.1
* dropped the first `show: false` of a vector layer added right after a
* switch. For engine.ground's screen pass and the vector-layer prototype
* (hidden by layer.update with show false, or by deleting its layers): a
* magenta cell hidden right after its commit must not show, then show, then
* hide again. The pixels are read from the engine canvas.
*
* GOOGLE_MAPS_API_KEY= CESIUM_ION_TOKEN= npm run dev -- --port 4271
* node scripts/qa-ground-switch.mjs http://localhost:4271
*/
import puppeteer from 'puppeteer';
import { pickEngine, waitForEngine } from './qa-strict-harness.mjs';
const BLOCKED_HOSTS =
/(^|\.)(googleapis\.com|google\.com|gstatic\.com|cesium\.com)$/;
const origin = process.argv[2];
if (!origin) {
console.error('Usage: node scripts/qa-ground-switch.mjs <origin>');
process.exit(2);
}
const APPROACHES = ['pass', 'vector-update', 'vector-delete'];
async function trial(page, approach, round) {
return page.evaluate(
async (approach, round) => {
const { engine } = window.__godsEyeView;
const geo = await import('/src/geo/index.js');
const frame = () =>
new Promise((resolve) => {
const remove = engine.frames.postRender.addEventListener(() => {
remove();
resolve();
});
engine.render.request('ground-switch');
});
let ground = engine.ground;
let owner = null;
if (approach !== 'pass') {
const { createNavaraVectorGround } =
await import('/src/globe/navara/ground/vector.js');
owner = createNavaraVectorGround(engine.extensions.navara.view, {
hide: approach === 'vector-update' ? 'update' : 'delete',
});
ground = owner.ground;
}
const lon = 20 + round * 1.5 + APPROACH_INDEX[approach] * 0.5;
const spot = engine.projector.worldToWindow(
geo.cartesian3.fromDegrees(lon + 0.15, 10),
);
const magenta = () => {
const canvas = engine.host.canvas;
const copy = document.createElement('canvas');
copy.width = 16;
copy.height = 16;
const context = copy.getContext('2d');
const scale = canvas.width / canvas.clientWidth;
context.drawImage(
canvas,
(spot.x - 8) * scale,
(spot.y - 8) * scale,
16 * scale,
16 * scale,
0,
0,
16,
16,
);
const data = context.getImageData(0, 0, 16, 16).data;
let lit = 0;
for (let i = 0; i < data.length; i += 4)
if (
data[i] > 150 &&
data[i + 2] > 150 &&
data[i + 1] < Math.min(data[i], data[i + 2]) - 60
)
lit++;
return lit / 256;
};
// Frames until `want` (shown or not) holds, up to 4 s.
const until = async (want) => {
const started = performance.now();
while (performance.now() - started < 4000) {
await frame();
if (magenta() > 0.5 === want) return true;
}
return false;
};
const set = ground.createSet({
id: `switch-${approach}-${round}`,
kind: 'cell',
});
set.replace([
{
west: lon,
south: 9.85,
east: lon + 0.3,
north: 10.15,
fill: [1, 0, 1, 1],
},
]);
set.commit();
set.show(false);
// Hidden right after the commit: never drawn.
let drawnWhileHidden = false;
for (let index = 0; index < 90; index++) {
await frame();
if (magenta() > 0.5) drawnWhileHidden = true;
}
set.show(true);
const shows = await until(true);
set.show(false);
const hides = await until(false);
set.destroy();
owner?.destroy();
return { hiddenAfterCommit: !drawnWhileHidden, shows, hides };
},
approach,
round,
);
}
const browser = await puppeteer.launch({ headless: true });
let failed = false;
try {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
page.on('pageerror', (error) => console.log(' page error:', error.message));
await page.setRequestInterception(true);
page.on('request', (request) => {
if (BLOCKED_HOSTS.test(new URL(request.url()).hostname)) request.abort();
else request.continue();
});
await page.evaluateOnNewDocument((approaches) => {
window.APPROACH_INDEX = Object.fromEntries(
approaches.map((name, index) => [name, index]),
);
}, APPROACHES);
await page.goto(
`${origin}/?welcome=0&engine=navara#v=2&lat=10&lon=21&alt=900000&heading=0&pitch=-90&map=osm&l=`,
);
await waitForEngine(page, 'navara', { timeout: 300_000 });
await page.evaluate(async () => {
const { appClock } = await import('/src/time/clock.js');
appClock.freeze(Date.UTC(2026, 10, 7, 11, 0));
});
const report = (label, result) => {
const ok = result.hiddenAfterCommit && result.shows && result.hides;
if (!ok) failed = true;
console.log(
` ${label}: hidden right after commit ${result.hiddenAfterCommit ? 'yes' : 'NO'}, shows ${result.shows ? 'yes' : 'NO'}, hides again ${result.hides ? 'yes' : 'NO'}`,
);
};
console.log('before a switch:');
for (const approach of APPROACHES)
report(approach, await trial(page, approach, 0));
await pickEngine(page, 'cesium');
await waitForEngine(page, 'cesium', { timeout: 300_000 });
await pickEngine(page, 'navara');
await waitForEngine(page, 'navara', { timeout: 300_000 });
await page.evaluate(async () => {
const geo = await import('/src/geo/index.js');
window.__godsEyeView.engine.camera.setView({
destination: geo.cartesian3.fromDegrees(21, 10, 900000),
});
});
console.log('after Navara → Cesium → Navara:');
for (const approach of APPROACHES)
report(approach, await trial(page, approach, 1));
} finally {
await browser.close();
}
process.exit(failed ? 1 : 0);
-438
View File
@@ -1,438 +0,0 @@
#!/usr/bin/env node
/**
* Draw the rasters that moved to engine.imagery — the Active Fires far-zoom
* heatmap and Recent Imagery — on both real engines, read the canvas back,
* and compare two runs pixel by pixel (before and after a change), and the
* two engines against each other. Run it against a keyless dev server of
* each version, then compare:
*
* GOOGLE_MAPS_API_KEY= CESIUM_ION_TOKEN= npm run dev -- --port 4291
* node scripts/qa-imagery-compare.mjs shoot http://localhost:4291 --out=DIR
* node scripts/qa-imagery-compare.mjs compare BEFORE_DIR AFTER_DIR
*
* Every scene freezes the app clock at local noon (Navara lights the scene
* with the app clock's sun) and waits for the map tiles. The heatmap is a
* fixed set of fires through the layer's own raster builder; Recent Imagery
* draws generated tiles (a gradient per tile, intercepted at qa-tiles.test)
* through the layer's renderer from the registry, falling back to the
* renderers of older versions. Each scene is shot with and without the
* raster: `overlay` is where the raster changed the frame. Google and
* Cesium ion are blocked, so no engine draws photoreal tiles.
*/
import fs from 'node:fs';
import path from 'node:path';
import puppeteer from 'puppeteer';
import sharp from 'sharp';
const BLOCKED_HOSTS =
/(^|\.)(googleapis\.com|google\.com|gstatic\.com|cesium\.com)$/;
const TILE_HOST = 'qa-tiles.test';
const args = process.argv.slice(2);
const [command, first, second] = args.filter((arg) => !arg.startsWith('--'));
const option = (name) =>
args.find((arg) => arg.startsWith(`--${name}=`))?.slice(name.length + 3);
const SIZE = { width: 1280, height: 800 };
// Fires on three continents, a dense cluster and a sparse field each.
const FIRES = [
...Array.from({ length: 400 }, (_, i) => ({
lat: 8 + (i % 20) * 0.08,
lon: 18 + Math.floor(i / 20) * 0.09,
frp: 40 + (i % 7) * 30,
})),
...Array.from({ length: 120 }, (_, i) => ({
lat: -12 + (i % 12) * 0.9,
lon: 22 + Math.floor(i / 12) * 1.1,
frp: 8 + (i % 5) * 6,
})),
...Array.from({ length: 60 }, (_, i) => ({
lat: 4 + (i % 6) * 0.5,
lon: 30 + Math.floor(i / 6) * 0.4,
frp: 200,
})),
];
const SCENES = [
{
id: 'heat-orbit',
kind: 'heat',
view: { lon: 22, lat: 2, height: 12_000_000 },
noonUtc: 10.5,
},
{
id: 'heat-regional',
kind: 'heat',
view: { lon: 19, lat: 8.8, height: 1_500_000 },
noonUtc: 10.5,
},
{
id: 'imagery-box',
kind: 'imagery',
view: { lon: -97.75, lat: 30.25, height: 16_000 },
noonUtc: 18.5,
slots: [{ id: 'a', product: 'S30', alpha: 1, split: 'none' }],
},
{
id: 'imagery-swipe',
kind: 'imagery',
view: { lon: -97.75, lat: 30.25, height: 16_000 },
noonUtc: 18.5,
split: 0.5,
slots: [
{ id: 'a', product: 'S30', alpha: 0.7, split: 'left' },
{ id: 'b', product: 'VIIRS', alpha: 1, split: 'right' },
],
},
];
const BOX = { west: -97.8, south: 30.2, east: -97.7, north: 30.3 };
// A 256 px tile: red across, green down, blue by the tile's parity; the
// VIIRS product (JPEG on GIBS) is a darker variant.
const tileCache = new Map();
async function tile(z, x, y, dark) {
const key = `${z}/${x}/${y}/${dark}`;
if (tileCache.has(key)) return tileCache.get(key);
const data = Buffer.alloc(256 * 256 * 4);
const parity = (x + y) % 2 ? 200 : 40;
for (let row = 0; row < 256; row++)
for (let column = 0; column < 256; column++) {
const i = (row * 256 + column) * 4;
const scale = dark ? 0.6 : 1;
data[i] = column * scale;
data[i + 1] = row * scale;
data[i + 2] = parity * scale;
data[i + 3] = 255;
}
const png = await sharp(data, {
raw: { width: 256, height: 256, channels: 4 },
})
.png()
.toBuffer();
tileCache.set(key, png);
return png;
}
async function shootScene(browser, origin, kind, scene) {
const page = await browser.newPage();
await page.setViewport(SIZE);
const errors = [];
page.on('pageerror', (error) => errors.push(error.message));
await page.setRequestInterception(true);
let tiles = 0;
page.on('request', async (request) => {
const url = new URL(request.url());
if (url.hostname === TILE_HOST) {
tiles++;
const [, product, z, y, x] = url.pathname.match(
/^\/(\w+)\/(\d+)\/(\d+)\/(\d+)\.png$/,
);
await request.respond({
status: 200,
contentType: 'image/png',
headers: { 'Access-Control-Allow-Origin': '*' },
body: await tile(+z, +x, +y, product === 'VIIRS'),
});
} else if (BLOCKED_HOSTS.test(url.hostname)) request.abort();
else request.continue();
});
const { lon, lat, height } = scene.view;
await page.goto(
`${origin}/?welcome=0&engine=${kind}#v=2&lat=${lat}&lon=${lon}&alt=${height}&heading=0&pitch=-90&map=osm&l=`,
);
await page.waitForFunction(
(expected) =>
window.__godsEyeView?.engine?.kind === expected &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 300_000, polling: 250 },
kind,
);
await page.waitForFunction(
() => !window.__godsEyeView.engine.camera.flightActive(),
{ timeout: 60_000, polling: 250 },
);
// The scene: the view, noon, and helpers the shots below use.
await page.evaluate(
async ({ scene, fires, box, tileHost }) => {
const { engine } = window.__godsEyeView;
const geo = await import('/src/geo/index.js');
const { appClock } = await import('/src/time/clock.js');
const noon = Date.UTC(2026, 10, 7) + scene.noonUtc * 3_600_000;
appClock.freeze(noon);
engine.camera.setView({
destination: geo.cartesian3.fromDegrees(
scene.view.lon,
scene.view.lat,
scene.view.height,
),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
// `count` frames, each asked for (a scene at rest draws only on request).
const frames = (count) =>
new Promise((resolve) => {
let left = count;
const remove = engine.frames.postRender.addEventListener(() => {
if (--left > 0) {
setTimeout(() => engine.render.request('qa-imagery'), 0);
return;
}
remove();
resolve();
});
engine.render.request('qa-imagery');
});
const { rendererFor } = await import('/src/app/layers/renderers.js');
const qa = { frames, raster: null };
window.__qaImagery = qa;
if (scene.kind === 'heat') {
const [{ buildHeatRaster }, { HEAT_RASTER }] = await Promise.all([
import('/src/data/firmsHeatRaster.js'),
import('/src/layers/firms/policy.js'),
]);
const display = rendererFor('local-firms', engine, { id: 'qa-firms' });
const heat = display.createHeatImagery();
const raster = buildHeatRaster(fires, HEAT_RASTER);
qa.show = () => {
heat.setRaster(raster);
heat.setOpacity(1);
heat.setShow(true);
};
qa.hide = () => heat.setShow(false);
return;
}
const options = {
tileTemplate: (product) =>
`https://${tileHost}/${product}/{z}/{y}/{x}.png`,
credit: 'QA tiles',
};
let renderer = rendererFor('recent-imagery', engine, options);
const viewer = engine.extensions?.cesium?.viewer;
if (!renderer) {
// Versions before engine.imagery: Cesium's own renderer on the globe.
const { createRecentImageryRenderer } =
await import('/src/layers/recentImagery/rendering.js');
renderer = createRecentImageryRenderer({
...options,
requestRender: () => engine.render.request('qa-imagery'),
});
renderer.rebind({ collection: viewer.imageryLayers, kind: 'globe' });
} else renderer.rebind(renderer.resolveHost());
const candidate = (product) => ({
key: `${product}:2026-10-28`,
product,
day: '2026-10-28',
});
qa.show = () => {
if (scene.split != null) {
renderer.setSplit?.(scene.split);
if (viewer) viewer.scene.splitPosition = scene.split;
}
for (const slot of scene.slots)
renderer.showSlot(slot.id, candidate(slot.product), box, {
alpha: slot.alpha,
splitDirection: slot.split,
});
};
qa.hide = () => {
for (const slot of scene.slots) renderer.hideSlot(slot.id);
};
},
{ scene, fires: FIRES, box: BOX, tileHost: TILE_HOST },
);
// Settle: the map tiles loaded, then tile requests quiet for 2 s.
const settle = async () => {
for (let round = 0; round < 120; round++) {
const before = tiles;
await page.evaluate(() => window.__qaImagery.frames(10));
await new Promise((resolve) => setTimeout(resolve, 500));
const loaded = await page.evaluate(() =>
window.__godsEyeView.engine.surface.tilesLoaded(),
);
if (loaded && tiles === before && round >= 4) break;
}
await page.evaluate(() => window.__qaImagery.frames(20));
};
const read = () =>
page.evaluate(() => {
const { engine } = window.__godsEyeView;
const canvas = engine.host.canvas;
const scratch = document.createElement('canvas');
scratch.width = canvas.clientWidth;
scratch.height = canvas.clientHeight;
const context = scratch.getContext('2d');
context.drawImage(canvas, 0, 0, scratch.width, scratch.height);
return scratch.toDataURL('image/png');
});
await settle();
const without = await read();
await page.evaluate(() => window.__qaImagery.show());
await settle();
const withRaster = await read();
await page.close();
return { without, with: withRaster, errors };
}
async function shoot(origin, out) {
fs.mkdirSync(out, { recursive: true });
const browser = await puppeteer.launch({
headless: true,
protocolTimeout: 600_000,
args: ['--use-angle=metal', '--enable-gpu', '--no-sandbox'],
});
const engines = (option('engine') ?? 'cesium,navara').split(',');
const only = option('only')?.split(',') ?? null;
try {
for (const kind of engines)
for (const scene of SCENES) {
if (only && !only.includes(scene.id)) continue;
const shot = await shootScene(browser, origin, kind, scene);
for (const name of ['without', 'with'])
fs.writeFileSync(
path.join(out, `${kind}-${scene.id}-${name}.png`),
Buffer.from(shot[name].split(',')[1], 'base64'),
);
console.log(
`${kind} ${scene.id}: shot${shot.errors.length ? ` (page errors: ${shot.errors.join('; ')})` : ''}`,
);
}
} finally {
await browser.close();
}
}
const raw = async (file) =>
(await sharp(file).ensureAlpha().raw().toBuffer({ resolveWithObject: true }))
.data;
// Where the raster changed the frame (any channel by more than 6).
function overlayMask(without, withRaster) {
const mask = new Uint8Array(without.length / 4);
for (let p = 0; p < mask.length; p++)
for (let k = 0; k < 3; k++)
if (Math.abs(without[p * 4 + k] - withRaster[p * 4 + k]) > 6) {
mask[p] = 1;
break;
}
return mask;
}
// Per-pixel differences of two frames inside `mask` (or everywhere).
function difference(a, b, mask) {
let count = 0,
sum = 0,
over8 = 0,
over24 = 0,
max = 0;
for (let p = 0; p < a.length / 4; p++) {
if (mask && !mask[p]) continue;
let worst = 0;
for (let k = 0; k < 3; k++)
worst = Math.max(worst, Math.abs(a[p * 4 + k] - b[p * 4 + k]));
count++;
sum += worst;
if (worst > 8) over8++;
if (worst > 24) over24++;
max = Math.max(max, worst);
}
return {
pixels: count,
meanMaxChannel: count ? +(sum / count).toFixed(2) : 0,
over8: count ? +((100 * over8) / count).toFixed(2) : 0,
over24: count ? +((100 * over24) / count).toFixed(2) : 0,
max,
};
}
// The raster's own change of the frame: with minus without, per channel.
function contribution(without, withRaster) {
const out = new Int16Array(without.length);
for (let i = 0; i < without.length; i++) out[i] = withRaster[i] - without[i];
return out;
}
async function compare(beforeDir, afterDir) {
const report = {};
const engines = ['cesium', 'navara'];
for (const scene of SCENES) {
const row = (report[scene.id] = {});
const loaded = {};
for (const engine of engines)
for (const [version, dir] of [
['before', beforeDir],
['after', afterDir],
])
for (const name of ['without', 'with']) {
const file = path.join(dir, `${engine}-${scene.id}-${name}.png`);
if (fs.existsSync(file))
loaded[`${engine}-${version}-${name}`] = await raw(file);
}
for (const engine of engines) {
const before = loaded[`${engine}-before-with`];
const after = loaded[`${engine}-after-with`];
if (!before || !after) continue;
const mask = overlayMask(loaded[`${engine}-before-without`], before);
row[`${engine}: before vs after, where the raster draws`] = difference(
before,
after,
mask,
);
row[`${engine}: before vs after, whole frame`] = difference(
before,
after,
);
row[`${engine}: base map before vs after`] = difference(
loaded[`${engine}-before-without`],
loaded[`${engine}-after-without`],
);
}
// Navara against Cesium: the raster's change of each frame (the base
// maps are drawn differently), where Cesium's raster draws.
const reference = loaded['cesium-after-with'];
const navara = loaded['navara-after-with'];
if (reference && navara) {
const mask = overlayMask(loaded['cesium-after-without'], reference);
const a = contribution(loaded['cesium-after-without'], reference);
const b = contribution(loaded['navara-after-without'], navara);
const shift = (values) =>
Uint8Array.from(values, (value) =>
Math.max(0, Math.min(255, value + 128)),
);
row['navara vs cesium after: the raster’s change of the frame'] =
difference(shift(a), shift(b), mask);
row['navara vs cesium after: frames with the raster'] = difference(
reference,
navara,
mask,
);
const beforeReference = loaded['cesium-before-with'];
const beforeNavara = loaded['navara-before-with'];
if (beforeReference && beforeNavara)
row['navara vs cesium before: frames with the raster'] = difference(
beforeReference,
beforeNavara,
overlayMask(loaded['cesium-before-without'], beforeReference),
);
}
}
return report;
}
if (command === 'shoot' && first) {
await shoot(first, option('out') ?? 'qa-imagery');
} else if (command === 'compare' && first && second) {
const report = await compare(first, second);
const json = option('json');
if (json) fs.writeFileSync(json, `${JSON.stringify(report, null, 2)}\n`);
for (const [scene, rows] of Object.entries(report)) {
console.log(scene);
for (const [label, stats] of Object.entries(rows))
console.log(
` ${label}: ${stats.pixels} px, mean ${stats.meanMaxChannel}, >8 ${stats.over8}%, >24 ${stats.over24}%, max ${stats.max}`,
);
}
} else {
console.error(
'Usage: node scripts/qa-imagery-compare.mjs shoot <origin> [--out=DIR] [--engine=cesium,navara] [--only=scene,...]\n' +
' node scripts/qa-imagery-compare.mjs compare <beforeDir> <afterDir> [--json=FILE]',
);
process.exit(2);
}
+2 -3
View File
@@ -54,9 +54,8 @@ await strict.run({}, async (b) => {
await waitForEngine(p, 'navara');
const ids = ['local-datacenters', 'local-dams'];
const dataset = await p.evaluate(async (ids) => {
const { dataManager, tileset } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
throw Error('Keyless preview required');
const { dataManager } = window.__godsEyeView;
window.qaSites = [];
const records = [];
for (const id of ids) {
+4 -3
View File
@@ -1,3 +1,4 @@
import { assertKeylessPreview } from './qa-strict-harness.mjs';
import assert from 'node:assert/strict';
import { createStrict, localPreview } from './qa-strict-harness.mjs';
@@ -125,10 +126,10 @@ await strict.run({}, async (browser) => {
await page.waitForFunction(
() => window.__godsEyeView.styleManager.getCockpitState().active,
);
await assertKeylessPreview(page);
await page.evaluate(() => {
const { dataManager, styleManager, tileset } = window.__godsEyeView;
if (window.__GOOGLE_MAPS_API_KEY__ || tileset)
throw Error('Keyless preview required');
const { dataManager, styleManager } = window.__godsEyeView;
styleManager.cockpitView.setVisionMode('thermal');
const entry = dataManager.layers.get(window.__qaAircraft.layerId);
window.__handoff = {
+2 -3
View File
@@ -54,9 +54,8 @@ await strict.run({}, async (b) => {
await p.goto(base.href);
await waitForEngine(p, 'navara');
await p.evaluate(async () => {
const { dataManager, tileset } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
throw Error('Keyless preview required');
const { dataManager } = window.__godsEyeView;
const mod = dataManager.layers.get('local-adsb').module;
window.qaAdsb = {
mod,
+15
View File
@@ -115,3 +115,18 @@ export function strictBlockPatterns(
{ urlPattern: '*://*:*/*', block: true },
];
}
/** Paid or live model endpoints no switch gate may reach. */
export const SESSION_BLOCKED_URLS = Object.freeze([
'*://tile.googleapis.com/*',
'*://*.googleapis.com/*3dtiles*',
'*://api.cesium.com/*',
'*://assets.ion.cesium.com/*',
'*://maps.googleapis.com/*',
'*/api/google/*',
'*/api/openai/*',
'*/api/realtime/*',
'*/api/gemini/*',
'*://api.openai.com/*',
'*://generativelanguage.googleapis.com/*',
]);
-7
View File
@@ -46,13 +46,6 @@ await strict.run(
page.on('pageerror', (error) => errors.push(error.message));
await page.goto(base.href);
await waitForEngine(page, 'navara');
assert.equal(
await page.evaluate(() =>
Boolean(window.__godsEyeView.tileset || window.__GOOGLE_MAPS_API_KEY__),
),
false,
'keyless preview required',
);
/** Amber pixels of the globe canvas alone (the SVG board hidden). */
const canvasAmber = async () => {
+2 -7
View File
@@ -8,6 +8,7 @@
// the shapes drawn on either engine stay on the board and draw on both.
//
// node scripts/qa-navara-draw.mjs http://127.0.0.1:<PORT>/
import { assertKeylessPreview } from './qa-strict-harness.mjs';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import sharp from 'sharp';
@@ -58,13 +59,7 @@ await strict.run(
);
};
await ready('navara');
assert.equal(
await page.evaluate(() =>
Boolean(window.__godsEyeView.tileset || window.__GOOGLE_MAPS_API_KEY__),
),
false,
'keyless preview required',
);
await assertKeylessPreview(page);
const tool = () =>
page.evaluate(() => {
const draw = window.__gevDrawTool;
+2 -7
View File
@@ -9,6 +9,7 @@
// outside the preview and the free tile cache).
//
// node scripts/qa-navara-geo-import.mjs http://127.0.0.1:<PORT>/
import { assertKeylessPreview } from './qa-strict-harness.mjs';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
@@ -162,13 +163,7 @@ await strict.run(
);
};
await ready('navara');
assert.equal(
await page.evaluate(() =>
Boolean(window.__godsEyeView.tileset || window.__GOOGLE_MAPS_API_KEY__),
),
false,
'keyless preview required',
);
await assertKeylessPreview(page);
const state = () =>
page.evaluate(() => ({
files: window.__gevGeoImport.list(),
-7
View File
@@ -46,13 +46,6 @@ await strict.run(
page.on('pageerror', (error) => errors.push(error.message));
await page.goto(base.href);
await waitForEngine(page, 'navara');
assert.equal(
await page.evaluate(() =>
Boolean(window.__godsEyeView.tileset || window.__GOOGLE_MAPS_API_KEY__),
),
false,
'keyless preview required',
);
const edgeLuminance = async () => {
await wait(900);
const { data, info } = await sharp(await page.screenshot({ clip: EDGE }))
+1 -2
View File
@@ -77,8 +77,7 @@ await strict.run({}, async (b) => {
const { tileset, dataManager, sceneDirector } = window.__godsEyeView;
if (!sceneDirector?.listScenes().length)
throw Error('No scenes available on Navara');
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
throw Error('Keyless preview required');
const mod = dataManager.layers.get('bhote-koshi-2026').module;
window.qaEvent = mod;
await dataManager.setEnabled(mod.id, true, { origin: 'user' });
+2 -3
View File
@@ -21,9 +21,8 @@ await strict.run({}, async (b) => {
await p.goto(base.href);
await waitForEngine(p, 'navara');
await p.evaluate(async () => {
const { tileset, dataManager } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
throw Error('Keyless preview required');
const { dataManager } = window.__godsEyeView;
window.qaLocator = dataManager.layers.get('bhote-koshi-locator').module;
window.qaLocator.setParams({ presentation: 'bhote-koshi-path-overview' });
await dataManager.setEnabled('bhote-koshi-locator', true, {
+6 -12
View File
@@ -1,27 +1,21 @@
import assert from 'node:assert/strict';
import { stations, installRadioFixture } from './fixtures/w2/radio.mjs';
import { createStrict, localPreview } from './qa-strict-harness.mjs';
import { createStrict, localPreview, openPage, waitForEngine } from './qa-strict-harness.mjs';
const url = localPreview(process.argv[2]);
// The long soak and switch timings live in qa-strict-soak.mjs.
url.search = '?engine=navara&welcome=0';
url.hash = 'v=2&lat=30.27&lon=-97.74&alt=12000&pitch=-90&map=sentinel2&l=';
const strict = createStrict({ origin: url.origin, layers: ['radio'] });
await strict.run({}, async (browser) => {
const page = await browser.newPage();
const errors = [];
page.on('pageerror', (e) => errors.push(e.message));
const { page: page, errors } = await openPage(browser);
await page.setViewport({ width: 800, height: 600 });
await installRadioFixture(page);
await page.goto(url.href);
await page.waitForFunction(
() =>
window.__godsEyeView?.engine?.kind === 'navara' &&
document.querySelector('#loading-screen.hidden'),
);
await waitForEngine(page, 'navara');
await page.evaluate(async (id) => {
const { dataManager, styleManager, tileset } = window.__godsEyeView;
const { dataManager, styleManager } = window.__godsEyeView;
await styleManager.initialRestorePromise;
if (window.__GOOGLE_MAPS_API_KEY__ || tileset)
throw Error('Expected keyless preview');
window.qaLayer = dataManager.layers.get('radio').module;
await dataManager.setEnabled('radio', true, { origin: 'user' });
}, stations[0].id);
+4 -3
View File
@@ -1,3 +1,4 @@
import { assertKeylessPreview } from './qa-strict-harness.mjs';
import assert from 'node:assert/strict';
import { launchFixture } from '../src/bench/launchFixture.js';
import {
@@ -62,10 +63,10 @@ await strict.run({}, async (b) => {
window.__godsEyeView?.engine?.kind === 'navara' &&
document.querySelector('#loading-screen.hidden'),
);
await assertKeylessPreview(p);
await p.evaluate(async (missions) => {
const { dataManager, styleManager, tileset } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
throw Error('Expected keyless preview');
const { dataManager, styleManager } = window.__godsEyeView;
await styleManager.initialRestorePromise;
window.sa = dataManager.layers.get('satellites').module;
window.la = dataManager.layers.get('rocket-launches').module;
+2 -3
View File
@@ -57,9 +57,8 @@ await strict.run({}, async (browser) => {
await p.goto(base.href);
await waitForEngine(p, 'navara');
await p.evaluate(async (provider) => {
const { dataManager, tileset } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
throw Error('Expected keyless preview');
const { dataManager } = window.__godsEyeView;
window.qaStreet = dataManager.layers.get('street-level').module;
await dataManager.setEnabled('street-level', true, { origin: 'user' });
window.qaStreet.setProviderEnabled(provider, true);
+25 -10
View File
@@ -34,18 +34,11 @@ const CHOICES = Object.freeze([...ENGINES, 'navara-hd']);
/** Local ports that belong to the user's own servers: never a QA preview. */
export const RESERVED_PORTS = Object.freeze([
'',
'4173',
'4174',
'4185',
'4186',
'4187',
'4188',
'4189',
'4194',
'4196',
...(process.env.QA_RESERVED_PORTS ?? '4173,4174,4185,4186,4187,4188,4189,4194,4196')
.split(',').map((port) => port.trim()).filter(Boolean),
]);
/** Chrome flags every scenario's browser starts with (the GPU path). */
const CHROME_ARGS = Object.freeze(['--use-angle=metal', '--enable-gpu']);
export const CHROME_ARGS = Object.freeze([`--use-angle=${process.env.QA_ANGLE || 'metal'}`, '--enable-gpu']);
/** The values of --layers=a,b and --evidence=FILE (null when absent). */
export function strictArguments(argv = process.argv) {
@@ -69,6 +62,27 @@ export function localPreview(href) {
return url;
}
/** Refuse to start a preview over an existing local server. */
export async function assertPreviewPortAvailable(href, request = fetch) {
const url = localPreview(href);
let response;
try {
response = await request(url.origin, { signal: AbortSignal.timeout(1000) });
} catch (error) {
if (error.cause?.code === 'ECONNREFUSED') return;
throw error;
}
if (response) throw new Error(`Preview port ${url.port} already answers`);
}
/** Check the running page before any scenario touches its layers. */
export async function assertKeylessPreview(page) {
await page.evaluate(() => {
if (window.__godsEyeView.tileset || window.__GOOGLE_MAPS_API_KEY__)
throw new Error('Keyless preview required');
});
}
/** The URL with no remote IP: fulfilled by a fixture or a cache, or a data URL. */
const live = (response) => Boolean(response.remoteIPAddress);
@@ -567,6 +581,7 @@ export async function waitForEngine(page, kind, { timeout = 120000 } = {}) {
await page.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
await assertKeylessPreview(page);
}
/**
+1 -2
View File
@@ -120,8 +120,7 @@ export async function openApp(
await page.goto(url.href, { waitUntil: 'domcontentloaded' });
await waitForEngine(page, engine);
await page.evaluate(() => {
if (window.__GOOGLE_MAPS_API_KEY__ || window.__godsEyeView.tileset)
throw Error('Keyless preview required');
window.__qaEngines = new Map([
[window.__godsEyeView.engine.kind, window.__godsEyeView.engine],
]);
@@ -42,9 +42,8 @@ await strict.run({}, async (b) => {
await p.goto(base.href);
await waitForEngine(p, 'navara');
await p.evaluate(async () => {
const { dataManager, tileset } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
throw Error('Keyless preview required');
const { dataManager } = window.__godsEyeView;
window.qaCables = dataManager.layers.get(
'telegeography-submarine-cables',
).module;
+2 -3
View File
@@ -75,9 +75,8 @@ await strict.run({}, async (b) => {
await p.goto(base.href);
await waitForEngine(p, 'navara');
await p.evaluate(async () => {
const { tileset, dataManager } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
throw Error('Keyless preview required');
const { dataManager } = window.__godsEyeView;
window.qaTraffic = dataManager.layers.get('traffic').module;
window.qaTraffic.setParams({ jamViz: 'both' });
await dataManager.setEnabled('traffic', true, { origin: 'user' });
+5 -23
View File
@@ -1,6 +1,6 @@
import assert from 'node:assert/strict';
import { fromDegrees } from '../src/geo/cartesian3.js';
import { createStrict, localPreview } from './qa-strict-harness.mjs';
import { createStrict, localPreview, openPage, waitForEngine } from './qa-strict-harness.mjs';
import { installTransitFixture } from './fixtures/w2/transit.mjs';
import {
combinedPayloads,
@@ -22,37 +22,19 @@ const VEHICLES = 12;
const strict = createStrict({
origin: url.origin,
layers: [id],
// installCombinedFixture answers these hosts too.
fixtures: bus
? []
: [
'https://earthquake.usgs.gov/*',
'https://tiles.dontgetflocked.com/*',
'*://*.earthdata.nasa.gov/*',
],
});
await strict.run({}, async (b) => {
const p = await b.newPage();
const { page: p, errors } = await openPage(b);
await p.setViewport({ width: 1280, height: 900 });
const errors = [];
p.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message);
});
const fx = bus
? await installTransitFixture(p)
: await installCombinedFixture(p);
fx.setStep?.(9);
await p.goto(url.href);
await p.waitForFunction(
() =>
window.__godsEyeView?.engine?.kind === 'navara' &&
document.querySelector('#loading-screen.hidden'),
);
await waitForEngine(p, 'navara');
await p.evaluate(async (id) => {
const { dataManager, styleManager, tileset } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
throw Error('Expected keyless preview');
const { dataManager, styleManager } = window.__godsEyeView;
await styleManager.initialRestorePromise;
window.qaUrban = dataManager.layers.get(id).module;
await dataManager.setEnabled(id, true, { origin: 'user' });
+5 -6
View File
@@ -1,3 +1,4 @@
import { localPreview, waitForEngine } from './qa-strict-harness.mjs';
// An active voice session survives engine switches. With a fake microphone
// and a fake Realtime transport (no model is reached: /api/openai, /api/realtime
// and api.openai.com are blocked or answered in the page), this gate starts
@@ -17,16 +18,14 @@ import {
idDifference,
launch,
openSessionPage,
previewBase,
readSession,
readVoiceEvidence,
requestEngine,
sessionAddress,
switchEngine,
waitReady,
} from './qa/appSessionHarness.mjs';
const base = previewBase(process.argv[2]);
const base = localPreview(process.argv[2]);
// Play one server function call on the fake data channel and wait for the
// client's function_call_output.
@@ -117,7 +116,7 @@ try {
browser,
sessionAddress(base, { engine: 'cesium' }),
);
await waitReady(page, 'cesium');
await waitForEngine(page, 'cesium', { timeout: 90_000 });
await page.evaluate(async () => {
const close = document.querySelector('[data-key-setup-close]');
if (close?.offsetParent) close.click();
@@ -184,7 +183,7 @@ try {
{ layerId: 'earthquakes', enabled: true },
{ beforeCall: 'switch-cesium' },
);
await waitReady(page, 'cesium');
await waitForEngine(page, 'cesium', { timeout: 90_000 });
assert.equal(midSwitch.ok, true, JSON.stringify(midSwitch));
assert.equal(midSwitch.engine, 'cesium', JSON.stringify(midSwitch));
results.push('PASS a call made during the switch ran on the incoming engine');
@@ -204,7 +203,7 @@ try {
{ layerId: 'earthquakes', enabled: false },
{ beforeCall: 'switch-navara' },
);
await waitReady(page, 'navara');
await waitForEngine(page, 'navara', { timeout: 90_000 });
await page.evaluate(() => {
window.__godsEyeView.voiceCommands.runner.switchWaitMs = 15_000;
});
-644
View File
@@ -1,644 +0,0 @@
#!/usr/bin/env node
/**
* Draw the rasters that moved to engine.imagery's tile schemes, staged
* sources and raised layers — the rain radar, lightning density, regional
* and global satellite clouds, the wind field and the Bhote Koshi
* before/after split — on both real engines, read the canvas back, and
* compare two runs pixel by pixel (before and after a change), and the two
* engines against each other. Run it against a keyless dev server of each
* version, then compare:
*
* GOOGLE_MAPS_API_KEY= CESIUM_ION_TOKEN= npm run dev -- --port 4291
* node scripts/qa-weather-compare.mjs shoot http://localhost:4291 --out=DIR
* node scripts/qa-weather-compare.mjs compare BEFORE_DIR AFTER_DIR
*
* The page must serve `/qa-cesium.js` (`export * from 'cesium';` at the
* root of the served copy): older versions build their Cesium renderers
* from the SDK. Every scene freezes the app clock at local noon and waits
* for the map tiles. The weather tiles and images are generated here
* (intercepted /api/weather/tile and /api/weather/image) from one field of
* longitude and latitude, so a tile and a whole-extent image of the same
* place agree; the wind is a fixed field, paused. Each scene is shot with
* and without the raster: `overlay` is where the raster changed the frame.
* Google and Cesium ion are blocked, so no engine draws photoreal tiles.
*/
import fs from 'node:fs';
import path from 'node:path';
import puppeteer from 'puppeteer';
import sharp from 'sharp';
const BLOCKED_HOSTS =
/(^|\.)(googleapis\.com|google\.com|gstatic\.com|cesium\.com)$/;
const args = process.argv.slice(2);
const [command, first, second] = args.filter((arg) => !arg.startsWith('--'));
const option = (name) =>
args.find((arg) => arg.startsWith(`--${name}=`))?.slice(name.length + 3);
const SIZE = { width: 1280, height: 800 };
const TIME = '2026-11-07T12:00:00.000Z';
const US = { west: -130, south: 20, east: -60, north: 55 };
const GLOBE = { west: -180, south: -90, east: 180, north: 90 };
const NEPAL = { west: 85.295, south: 28.135, east: 85.379, north: 28.292 };
const SCENES = [
{
id: 'radar',
kind: 'weather',
product: 'radar',
bounds: US,
alpha: 0.8,
view: { lon: -95, lat: 37.5, height: 6_000_000 },
noonUtc: 18,
},
{
id: 'lightning',
kind: 'weather',
product: 'lightning',
bounds: US,
alpha: 0.8,
view: { lon: -95, lat: 37.5, height: 6_000_000 },
noonUtc: 18,
},
{
id: 'clouds-regional',
kind: 'weather',
product: 'clouds-regional',
bounds: US,
alpha: 0.7,
view: { lon: -95, lat: 37.5, height: 6_000_000 },
noonUtc: 18,
},
{
id: 'clouds-global',
kind: 'weather',
product: 'clouds',
bounds: GLOBE,
alpha: 0.7,
view: { lon: -40, lat: 15, height: 14_000_000 },
noonUtc: 14.5,
},
{
id: 'wind',
kind: 'wind',
view: { lon: -40, lat: 20, height: 12_000_000 },
noonUtc: 14.5,
},
{
id: 'nepal-split',
kind: 'nepal',
view: { lon: 85.337, lat: 28.2135, height: 22_000 },
noonUtc: 6.5,
},
];
// The weather field at a longitude and latitude: blobs with soft edges
// (radar, lightning) or a grey sky to mask (the infrared products).
function weatherPixel(product, lon, lat, out, at) {
const v =
0.5 + 0.25 * Math.sin(lon * 0.35) + 0.25 * Math.cos(lat * 0.5 + lon * 0.1);
if (product === 'clouds' || product === 'clouds-regional') {
const grey = Math.round(255 * Math.max(0, Math.min(1, v)));
out[at] = out[at + 1] = out[at + 2] = grey;
out[at + 3] = 255;
return;
}
out[at] = Math.round(255 * Math.max(0, Math.min(1, v)));
out[at + 1] = Math.round(255 * Math.max(0, Math.min(1, 1 - v)));
out[at + 2] = product === 'lightning' ? 220 : 90;
out[at + 3] = Math.round(255 * Math.max(0, Math.min(1, (v - 0.45) * 4)));
}
async function weatherPng(product, box, width, height) {
const data = Buffer.alloc(width * height * 4);
for (let row = 0; row < height; row++) {
const lat = box.north - ((row + 0.5) / height) * (box.north - box.south);
for (let column = 0; column < width; column++) {
const lon = box.west + ((column + 0.5) / width) * (box.east - box.west);
weatherPixel(product, lon, lat, data, (row * width + column) * 4);
}
}
return sharp(data, { raw: { width, height, channels: 4 } })
.png()
.toBuffer();
}
const LARGEST = {
radar: [4096, 2048],
lightning: [4096, 2048],
'clouds-regional': [4096, 2048],
clouds: [2048, 1024],
};
const pngCache = new Map();
// One generated /api/weather/tile or /api/weather/image response, or null.
async function weatherResponse(url) {
const key = url.pathname + url.search;
if (pngCache.has(key)) return pngCache.get(key);
const query = url.searchParams;
const product = query.get('product');
const scene = SCENES.find((entry) => entry.product === product);
if (!scene) return null;
let png;
if (url.pathname === '/api/weather/tile') {
const z = +query.get('z');
const x = +query.get('x');
const y = +query.get('y');
const span = 180 / 2 ** z;
const size = +(query.get('size') ?? 256);
png = await weatherPng(
product,
{
west: -180 + x * span,
east: -180 + (x + 1) * span,
north: 90 - y * span,
south: 90 - (y + 1) * span,
},
size,
size,
);
} else {
const bbox = query.get('bbox');
const [west, south, east, north] = bbox
? bbox.split(',').map(Number)
: [
scene.bounds.west,
scene.bounds.south,
scene.bounds.east,
scene.bounds.north,
];
const [width, height] = query.get('size')
? query.get('size').split('x').map(Number)
: bbox
? [4096, 2048]
: LARGEST[product];
png = await weatherPng(
product,
{ west, south, east, north },
width,
height,
);
}
pngCache.set(key, png);
return png;
}
// A fixed 5° wind field: westerlies, trades and a jet.
function windSnapshot() {
const nx = 72;
const ny = 37;
const u = new Float32Array(nx * ny);
const v = new Float32Array(nx * ny);
for (let j = 0; j < ny; j++) {
const lat = 90 - j * 5;
for (let i = 0; i < nx; i++) {
const lon = i * 5;
u[j * nx + i] =
14 * Math.cos((lat * Math.PI) / 60) +
6 * Math.sin((lon * Math.PI) / 45);
v[j * nx + i] = 5 * Math.sin(((lon + lat) * Math.PI) / 50);
}
}
return {
model: 'gfs',
level: '10m',
units: 'm/s',
cycle: { runIso: TIME, validIso: TIME, forecastHour: 0 },
grid: { nx, ny, lo1: 0, la1: 90, dx: 5, dy: 5 },
u: Array.from(u),
v: Array.from(v),
};
}
async function shootScene(browser, origin, kind, scene) {
const page = await browser.newPage();
await page.setViewport(SIZE);
const errors = [];
page.on('pageerror', (error) => errors.push(error.message));
await page.setRequestInterception(true);
let requests = 0;
page.on('request', async (request) => {
const url = new URL(request.url());
if (url.pathname.startsWith('/api/weather/')) {
requests++;
const png = await weatherResponse(url).catch(() => null);
if (png)
await request.respond({
status: 200,
contentType: 'image/png',
body: png,
});
else await request.respond({ status: 404, body: '' });
} else if (BLOCKED_HOSTS.test(url.hostname)) request.abort();
else request.continue();
});
const { lon, lat, height } = scene.view;
await page.goto(
`${origin}/?welcome=0&engine=${kind}#v=2&lat=${lat}&lon=${lon}&alt=${height}&heading=0&pitch=-90&map=osm&l=`,
);
await page.waitForFunction(
(expected) =>
window.__godsEyeView?.engine?.kind === expected &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 300_000, polling: 250 },
kind,
);
await page.waitForFunction(
() => !window.__godsEyeView.engine.camera.flightActive(),
{ timeout: 60_000, polling: 250 },
);
const version = await page.evaluate(
async ({ scene, time, wind }) => {
const { engine } = window.__godsEyeView;
const geo = await import('/src/geo/index.js');
const { appClock } = await import('/src/time/clock.js');
appClock.freeze(Date.UTC(2026, 10, 7) + scene.noonUtc * 3_600_000);
engine.camera.setView({
destination: geo.cartesian3.fromDegrees(
scene.view.lon,
scene.view.lat,
scene.view.height,
),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
const frames = (count) =>
new Promise((resolve) => {
let left = count;
const remove = engine.frames.postRender.addEventListener(() => {
if (--left > 0) {
setTimeout(() => engine.render.request('qa-weather'), 0);
return;
}
remove();
resolve();
});
engine.render.request('qa-weather');
});
const tryImport = async (url) => {
try {
return await import(url);
} catch {
return null;
}
};
const viewer = engine.extensions?.cesium?.viewer ?? null;
const native = engine.extensions?.navara ?? null;
const cesium = viewer ? await import('/qa-cesium.js') : null;
const qa = { frames };
window.__qaWeather = qa;
if (scene.kind === 'weather') {
const snapshot = {
schemaVersion: 1,
product: scene.product,
bounds: scene.bounds,
times: [time],
latest: time,
tileSize: 256,
maxLevel: 6,
tilingScheme: 'geographic',
};
const display = await tryImport(
'/src/layers/weather/render/display.js',
);
let renderer;
let used;
if (display) {
renderer = display.createWeatherDisplay(engine, {});
used = 'display';
} else if (viewer) {
const { createWeatherRendering } =
await import('/src/layers/weather/rendering.js');
renderer = createWeatherRendering({
viewer,
cesium,
getHost: () => ({
collection: viewer.imageryLayers,
kind: 'globe',
}),
});
used = 'cesium';
} else {
const { createWeatherNavaraRenderer } =
await import('/src/layers/weather/render/navara.js');
renderer = createWeatherNavaraRenderer(engine, native, {
getHost: () => ({ kind: 'globe', collection: engine }),
});
used = 'navara';
}
renderer.setAlpha(scene.alpha);
qa.show = () =>
renderer.setFrame(snapshot, time, {
infrared: scene.product.startsWith('clouds') ? 100 : 'filtered',
});
qa.state = () => renderer.getDiagnostics();
return used;
}
if (scene.kind === 'wind') {
const display = await tryImport('/src/layers/wind/render/display.js');
const { createWindRendering } =
await import('/src/layers/wind/rendering.js');
const cesiumFlow = (options = {}) =>
createWindRendering({
cesium,
container: viewer.container,
getViewer: () => viewer,
getHost: () => ({
collection: viewer.imageryLayers,
kind: 'globe',
}),
...options,
});
let renderer;
let used;
if (display) {
const navaraFlow = native
? (
(await tryImport('/src/globe/navara/windFlow.js')) ??
(await import('/src/layers/wind/render/navaraFlow.js'))
).createWindNavaraFlow
: null;
const createWindFlowMesh = native
? (await import('/src/globe/navara/windFlowMesh.js'))
.createWindFlowMesh
: null;
renderer = display.createWindDisplay(engine, {
flow: viewer
? () => cesiumFlow({ field: false })
: (e) => navaraFlow(e, native, createWindFlowMesh),
});
used = 'display';
} else if (viewer) {
renderer = cesiumFlow();
used = 'cesium';
} else {
const { createWindNavaraRenderer } =
await import('/src/layers/wind/render/navara.js');
renderer = createWindNavaraRenderer(engine, native);
used = 'navara';
}
renderer.attach();
renderer.setOptions({ overlay: 'none', paused: true });
renderer.setField(wind);
renderer.start();
qa.show = () => renderer.setOptions({ overlay: 'speed', paused: true });
qa.state = () => renderer.getDiagnostics();
return used;
}
// The Bhote Koshi before and after images, split at the middle.
const box = {
west: 85.295,
south: 28.135,
east: 85.379,
north: 28.292,
};
const urls = [
'/events/bhote-koshi-2026/pre.webp',
'/events/bhote-koshi-2026/post.webp',
].map((url) => new URL(url, location.href).href);
const display = await tryImport(
'/src/layers/bhoteKoshi/render/display.js',
);
if (display) {
const renderer = display.createBhoteKoshiDisplay(engine);
qa.show = async () => {
await renderer.loadImages(box, urls);
renderer.setSplit(0.5);
};
return 'display';
}
const Cesium = cesium ?? (await import('/qa-cesium.js'));
const rectangle = Cesium.Rectangle.fromDegrees(
box.west,
box.south,
box.east,
box.north,
);
if (viewer) {
qa.show = async () => {
const providers = await Promise.all(
urls.map((url) =>
Cesium.SingleTileImageryProvider.fromUrl(url, { rectangle }),
),
);
providers.forEach((provider, index) => {
const layer = viewer.imageryLayers.addImageryProvider(provider);
layer.splitDirection = index
? Cesium.SplitDirection.RIGHT
: Cesium.SplitDirection.LEFT;
layer.alpha = 0.96;
});
viewer.scene.splitPosition = 0.5;
};
return 'cesium';
}
const { createNepalEventDisplay } =
await import('/src/layers/bhoteKoshi/render/navara.js');
const renderer = createNepalEventDisplay(engine, native);
qa.show = async () => {
const providers = await Promise.all(
urls.map((url) =>
Cesium.SingleTileImageryProvider.fromUrl(url, { rectangle }),
),
);
await renderer.loadImages(box, providers);
renderer.setSplit(0.5);
};
return 'navara';
},
{ scene, time: TIME, wind: windSnapshot() },
);
// Settle: the map tiles loaded, then requests quiet for 2 s.
const settle = async () => {
for (let round = 0; round < 120; round++) {
const before = requests;
await page.evaluate(() => window.__qaWeather.frames(10));
await new Promise((resolve) => setTimeout(resolve, 500));
const loaded = await page.evaluate(() =>
window.__godsEyeView.engine.surface.tilesLoaded(),
);
if (loaded && requests === before && round >= 4) break;
}
await page.evaluate(() => window.__qaWeather.frames(20));
};
const read = () =>
page.evaluate(() => {
const { engine } = window.__godsEyeView;
const canvas = engine.host.canvas;
const scratch = document.createElement('canvas');
scratch.width = canvas.clientWidth;
scratch.height = canvas.clientHeight;
const context = scratch.getContext('2d');
context.drawImage(canvas, 0, 0, scratch.width, scratch.height);
return scratch.toDataURL('image/png');
});
await settle();
const without = await read();
const shown = await page.evaluate(async () => {
const result = await window.__qaWeather.show();
return result === undefined ? null : result;
});
await settle();
const withRaster = await read();
const state = await page.evaluate(() => {
try {
return JSON.parse(JSON.stringify(window.__qaWeather.state?.() ?? null));
} catch {
return null;
}
});
await page.close();
return { without, with: withRaster, errors, version, shown, state };
}
async function shoot(origin, out) {
fs.mkdirSync(out, { recursive: true });
const browser = await puppeteer.launch({
headless: true,
protocolTimeout: 600_000,
args: ['--use-angle=metal', '--enable-gpu', '--no-sandbox'],
});
const engines = (option('engine') ?? 'cesium,navara').split(',');
const only = option('only')?.split(',') ?? null;
try {
for (const kind of engines)
for (const scene of SCENES) {
if (only && !only.includes(scene.id)) continue;
const shot = await shootScene(browser, origin, kind, scene);
for (const name of ['without', 'with'])
fs.writeFileSync(
path.join(out, `${kind}-${scene.id}-${name}.png`),
Buffer.from(shot[name].split(',')[1], 'base64'),
);
console.log(
`${kind} ${scene.id}: ${shot.version}, shown ${JSON.stringify(shot.shown)}, error ${shot.state?.error ?? shot.state?.imageryError ?? null}${shot.errors.length ? ` (page errors: ${shot.errors.join('; ')})` : ''}`,
);
}
} finally {
await browser.close();
}
}
const raw = async (file) =>
(await sharp(file).ensureAlpha().raw().toBuffer({ resolveWithObject: true }))
.data;
// Where the raster changed the frame (any channel by more than 6).
function overlayMask(without, withRaster) {
const mask = new Uint8Array(without.length / 4);
for (let p = 0; p < mask.length; p++)
for (let k = 0; k < 3; k++)
if (Math.abs(without[p * 4 + k] - withRaster[p * 4 + k]) > 6) {
mask[p] = 1;
break;
}
return mask;
}
// Per-pixel differences of two frames inside `mask` (or everywhere).
function difference(a, b, mask) {
let count = 0,
sum = 0,
over8 = 0,
over24 = 0,
max = 0;
for (let p = 0; p < a.length / 4; p++) {
if (mask && !mask[p]) continue;
let worst = 0;
for (let k = 0; k < 3; k++)
worst = Math.max(worst, Math.abs(a[p * 4 + k] - b[p * 4 + k]));
count++;
sum += worst;
if (worst > 8) over8++;
if (worst > 24) over24++;
max = Math.max(max, worst);
}
return {
pixels: count,
meanMaxChannel: count ? +(sum / count).toFixed(2) : 0,
over8: count ? +((100 * over8) / count).toFixed(2) : 0,
over24: count ? +((100 * over24) / count).toFixed(2) : 0,
max,
};
}
// The raster's own change of the frame: with minus without, per channel.
function contribution(without, withRaster) {
const out = new Int16Array(without.length);
for (let i = 0; i < without.length; i++) out[i] = withRaster[i] - without[i];
return out;
}
const shift = (values) =>
Uint8Array.from(values, (value) => Math.max(0, Math.min(255, value + 128)));
async function compare(beforeDir, afterDir) {
const report = {};
const engines = ['cesium', 'navara'];
for (const scene of SCENES) {
const row = (report[scene.id] = {});
const loaded = {};
for (const engine of engines)
for (const [version, dir] of [
['before', beforeDir],
['after', afterDir],
])
for (const name of ['without', 'with']) {
const file = path.join(dir, `${engine}-${scene.id}-${name}.png`);
if (fs.existsSync(file))
loaded[`${engine}-${version}-${name}`] = await raw(file);
}
for (const engine of engines) {
const before = loaded[`${engine}-before-with`];
const after = loaded[`${engine}-after-with`];
if (!before || !after) continue;
const mask = overlayMask(loaded[`${engine}-before-without`], before);
row[`${engine}: before vs after, where the raster draws`] = difference(
before,
after,
mask,
);
// The raster's own change, before and after: the base map drawn
// differently between runs (tiles, lighting) cancels out.
row[`${engine}: the raster's change, before vs after`] = difference(
shift(contribution(loaded[`${engine}-before-without`], before)),
shift(contribution(loaded[`${engine}-after-without`], after)),
mask,
);
row[`${engine}: base map before vs after`] = difference(
loaded[`${engine}-before-without`],
loaded[`${engine}-after-without`],
);
}
// Navara against Cesium, where Cesium's raster draws: each engine's
// change of its own frame (the base maps are drawn differently).
for (const version of ['before', 'after']) {
const reference = loaded[`cesium-${version}-with`];
const navara = loaded[`navara-${version}-with`];
if (!reference || !navara) continue;
const mask = overlayMask(loaded[`cesium-${version}-without`], reference);
row[`navara vs cesium ${version}: the raster's change`] = difference(
shift(contribution(loaded[`cesium-${version}-without`], reference)),
shift(contribution(loaded[`navara-${version}-without`], navara)),
mask,
);
}
}
return report;
}
if (command === 'shoot' && first) {
await shoot(first, option('out') ?? 'qa-weather');
} else if (command === 'compare' && first && second) {
const report = await compare(first, second);
const json = option('json');
if (json) fs.writeFileSync(json, `${JSON.stringify(report, null, 2)}\n`);
for (const [scene, rows] of Object.entries(report)) {
console.log(scene);
for (const [label, value] of Object.entries(rows))
console.log(
` ${label}: ${value.pixels} px, mean ${value.meanMaxChannel}, >8 ${value.over8}%, >24 ${value.over24}%, max ${value.max}`,
);
}
} else {
console.error(
'Usage: node scripts/qa-weather-compare.mjs shoot <origin> [--out=DIR] [--engine=cesium,navara] [--only=scene,...]\n' +
' node scripts/qa-weather-compare.mjs compare <before> <after> [--json=FILE]',
);
process.exit(2);
}
+1 -2
View File
@@ -195,8 +195,7 @@ await strict.run({}, async (b) => {
);
};
await p.evaluate(() => {
if (window.__godsEyeView.tileset || window.__GOOGLE_MAPS_API_KEY__)
throw Error('Keyless preview required');
});
for (const id of [ids[1], ids[0], ids[2]]) {
await p.evaluate(
+2 -3
View File
@@ -62,9 +62,8 @@ await strict.run({}, async (b) => {
await p.goto(base.href);
await waitForEngine(p, 'cesium');
await p.evaluate(async (id) => {
const { dataManager, tileset } = window.__godsEyeView;
if (window.__GOOGLE_MAPS_API_KEY__ || tileset)
throw Error('Keyless preview required');
const { dataManager } = window.__godsEyeView;
await dataManager.setEnabled(id, true, { origin: 'user' });
window.qaWild = dataManager.layers.get(id).module;
// Every selection the layer reports, in order.
+2 -3
View File
@@ -71,9 +71,8 @@ await strict.run({}, async (b) => {
await p.goto(base.href);
await waitForEngine(p, 'navara');
await p.evaluate(async () => {
const { dataManager, tileset } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
throw Error('Keyless preview required');
const { dataManager } = window.__godsEyeView;
const mod = dataManager.layers.get('wind').module;
window.qaWind = mod;
mod.setParams({ overlay: 'temperature', paused: true, units: 'm/s' });
+5 -32
View File
@@ -4,28 +4,8 @@
// endpoint blocked, a fake microphone and Realtime transport, and readers for
// the session ids, listener counts and transport counters.
import puppeteer from 'puppeteer';
import { blockPaidQaMaps } from '../qa-map-policy.mjs';
import { localPreview, waitForEngine } from '../qa-strict-harness.mjs';
/** Paid or live model endpoints no switch gate may reach. */
export const BLOCKED_URLS = Object.freeze([
'*://tile.googleapis.com/*',
'*://*.googleapis.com/*3dtiles*',
'*://api.cesium.com/*',
'*://assets.ion.cesium.com/*',
'*://maps.googleapis.com/*',
'*/api/google/*',
'*/api/openai/*',
'*/api/realtime/*',
'*/api/gemini/*',
'*://api.openai.com/*',
'*://generativelanguage.googleapis.com/*',
]);
/** The preview origin from argv, refusing the ports that belong to the user. */
export function previewBase(argument) {
return localPreview(argument ?? '');
}
import { SESSION_BLOCKED_URLS } from '../qa-map-policy.mjs';
import { CHROME_ARGS, waitForEngine } from '../qa-strict-harness.mjs';
/** The page address: Boston on OSM, no welcome, on the given engine. */
export function sessionAddress(base, { engine = 'cesium', extra = {} } = {}) {
@@ -46,8 +26,7 @@ export function launch() {
return puppeteer.launch({
headless: true,
args: [
'--use-angle=metal',
'--enable-gpu',
...CHROME_ARGS,
'--use-fake-device-for-media-stream',
'--use-fake-ui-for-media-stream',
'--autoplay-policy=no-user-gesture-required',
@@ -216,10 +195,9 @@ export async function openSessionPage(browser, address) {
await browser
.defaultBrowserContext()
.overridePermissions(new URL(address).origin, ['microphone']);
await blockPaidQaMaps(page);
const network = await page.createCDPSession();
await network.send('Network.enable');
await network.send('Network.setBlockedURLs', { urls: [...BLOCKED_URLS] });
await network.send('Network.setBlockedURLs', { urls: [...SESSION_BLOCKED_URLS] });
await page.evaluateOnNewDocument(installFakeVoice);
await page.setViewport({ width: 1440, height: 900 });
const errors = [];
@@ -228,11 +206,6 @@ export async function openSessionPage(browser, address) {
return { page, errors };
}
/** The engine runs, its picker is idle and the loading screen is gone. */
export function waitReady(page, kind) {
return waitForEngine(page, kind, { timeout: 90_000 });
}
/**
* Pick an engine the way the picker does, without moving focus to it, so a
* focused field keeps its focus through the switch.
@@ -247,7 +220,7 @@ export function requestEngine(page, kind) {
export async function switchEngine(page, kind) {
await requestEngine(page, kind);
await waitReady(page, kind);
await waitForEngine(page, kind, { timeout: 90_000 });
}
/** Session object ids and counters (src/app/sessionDiagnostics.js). */
+3 -2
View File
@@ -1,3 +1,4 @@
import { assertKeylessPreview } from '../qa-strict-harness.mjs';
// Shared by the Navara visual QA scenarios (qa-navara-visual-styles,
// qa-navara-bloom, qa-navara-sonar, qa-navara-cockpit-clouds): read the
// engine canvas, compare frames, settle a still view, follow a style
@@ -134,10 +135,10 @@ export const histogramShift = (a, b) =>
) / 2;
export async function freeze(page) {
await assertKeylessPreview(page);
await page.evaluate(async () => {
const a = window.__godsEyeView;
if (window.__GOOGLE_MAPS_API_KEY__ || a.tileset)
throw new Error('Keyless preview required');
a.styleManager.orbitController.stop();
a.engine.camera.cancelFlight();
a.styleManager._setDetectionMode?.('OFF');
+2 -3
View File
@@ -92,13 +92,12 @@ export const ENGINE_COUPLING_METRICS = Object.freeze([
},
{
id: 'engineSniffCalls',
label: 'isGlobeEngine / engineFromViewer calls outside src/globe',
label: 'isGlobeEngine calls outside src/globe',
measure: (files) =>
files
.filter(({ file }) => !inGlobe(file))
.reduce(
(sum, { text }) =>
sum + countMatches(text, /\b(?:isGlobeEngine|engineFromViewer)\(/),
(sum, { text }) => sum + countMatches(text, /\bisGlobeEngine\(/),
0,
),
},