From ba1191b2e53b981323894a2bf8d9001a509aa4be Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Sat, 17 Oct 2026 03:20:44 +0400 Subject: [PATCH] test(qa): floor verification on both engines qa-floor-verify --engine=cesium|navara: on an engine without a viewer the camera pin, the tile readiness, the mesh probes (engine.surface sampleHeightDetailed, tiles only, excluding the layer's mover sets) and the sprite census (the probe's drawn()) go through the engine. The Cesium path is unchanged. Live AUS traffic: Cesium FAIL as on 7c7422e2 (13 contacts at the -27 m geoid fallback); Navara FAIL with 1 of 15 low contacts under the mesh (68.6 m against 121.4 m). Task 10b, checklist item 7. --- scripts/qa-floor-verify.mjs | 55 ++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/scripts/qa-floor-verify.mjs b/scripts/qa-floor-verify.mjs index 97b1483dd..756f4cfe3 100644 --- a/scripts/qa-floor-verify.mjs +++ b/scripts/qa-floor-verify.mjs @@ -10,7 +10,12 @@ import fs from 'node:fs'; // QA_BASE_URL matches the sibling harnesses (qa-height-datum / qa-cctv-v2) so a // secondary checkout can verify against its own dev server instead of the default :4173. -const APP_URL = process.env.QA_BASE_URL || 'http://localhost:4173'; +const BASE_URL = process.env.QA_BASE_URL || 'http://localhost:4173'; +// --engine=cesium|navara (phase 3): on Navara the camera, the mesh probes and +// the sprite census go through the engine and the QA probe. +const ENGINE = (process.argv.find((arg) => arg.startsWith('--engine=')) ?? '--engine=cesium').slice(9); +const APP_URL = + ENGINE === 'cesium' ? BASE_URL : `${BASE_URL.replace(/\/$/, '')}/?engine=${ENGINE}`; // CLI: --lat --lon --floor-min --floor-max (defaults: Austin airport) const argv = Object.fromEntries(process.argv.slice(2).map((a) => a.split('=')).filter((x) => x.length === 2).map(([k, v]) => [k.replace(/^--/, ''), Number(v)])); const SITE = { @@ -38,7 +43,7 @@ const page = await browser.newPage(); await page.setViewport({ width: 1400, height: 900 }); page.on('pageerror', (e) => console.log('[pageerror]', e.message)); await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 120000 }); -await page.waitForFunction(() => window.__godsEyeView?.dataManager && window.__godsEyeView?.viewer, { timeout: 150000 }); +await page.waitForFunction(() => window.__godsEyeView?.dataManager && window.__godsEyeView?.engine, { timeout: 150000 }); await sleep(2000); // Let the app's boot fly-to fully settle FIRST (it was overriding an early @@ -46,8 +51,16 @@ await sleep(2000); // coarse global tiles at ~25 m and the oracle was garbage). await sleep(12000); await page.evaluate((site) => { window.__QA_SITE = site; }, SITE); -const camPin = async () => page.evaluate(() => { +const camPin = async () => page.evaluate(async () => { const v = window.__godsEyeView.viewer; + if (!v) { + const geo = await import('/src/geo/index.js'); + const { camera } = window.__godsEyeView.engine; + camera.cancelFlight(); + camera.setView({ destination: geo.cartesian3.fromDegrees(window.__QA_SITE.lon, window.__QA_SITE.lat, 3000) }); + const c = camera.pose().cartographic; + return { lat: +(c.latitude * 180 / Math.PI).toFixed(3), lon: +(c.longitude * 180 / Math.PI).toFixed(3), h: Math.round(c.height) }; + } const C = v.camera.positionCartographic.constructor; v.camera.cancelFlight(); v.camera.setView({ destination: v.scene.globe.ellipsoid.cartographicToCartesian(C.fromDegrees(window.__QA_SITE.lon, window.__QA_SITE.lat, 3000)) }); @@ -63,6 +76,7 @@ await sleep(100000); console.log('camera at probe time:', JSON.stringify(await camPin())); const ready = await page.evaluate(() => { const v = window.__godsEyeView.viewer; + if (!v) return { tilesLoaded: window.__godsEyeView.engine.surface.tilesLoaded() }; const prims = v.scene.primitives; for (let i = 0; i < prims.length; i++) { const p = prims.get(i); @@ -73,11 +87,44 @@ const ready = await page.evaluate(() => { console.log('tileset ready:', JSON.stringify(ready)); await sleep(8000); -const report = await page.evaluate(() => { +const report = await page.evaluate(async () => { const gev = window.__godsEyeView; const v = gev.viewer; const layer = gev.dataManager.layers.get('flights')?.module; if (!layer) return { err: 'no flights module' }; + if (!v) { + // The engine path: mesh probes through engine.surface, excluding the + // layer's own mover sets; the census from the probe's drawn(). + const geo = await import('/src/geo/index.js'); + const { engine } = gev; + const center = geo.cartesian3.fromDegrees(window.__QA_SITE.lon, window.__QA_SITE.lat, 200); + const nearby = layer.getNearby(center, 15000, 60); + const exclude = engine.movers.list().map(({ id }) => id).filter((id) => id.startsWith('flights')); + const out = []; + for (const p of nearby) { + const c = geo.cartographic.fromCartesian(p.position); + const answer = engine.surface.sampleHeightDetailed( + geo.cartographic.fromRadians(c.longitude, c.latitude, 0), + { exclude }, + ); + const meshH = answer?.source === 'tiles' && Number.isFinite(answer.height) ? answer.height : null; + out.push({ + id: p.id, + renderAltM: +c.height.toFixed(1), + meshM: meshH != null ? +meshH.toFixed(1) : null, + aboveMeshM: meshH != null ? +(c.height - meshH).toFixed(1) : null, + }); + } + let shown = 0, hidden = 0; + for (const row of gev.layers.drawn('flights') ?? []) { + if (!row.position) continue; + const cc = geo.cartographic.fromCartesian(row.position); + const la = cc.latitude * 180 / Math.PI, lo = cc.longitude * 180 / Math.PI; + if (Math.abs(la - window.__QA_SITE.lat) > 0.08 || Math.abs(lo - window.__QA_SITE.lon) > 0.10) continue; + if (row.show) shown++; else hidden++; + } + return { ausContacts: out.length, contacts: out.slice(0, 16), spritesShown: shown, spritesHidden: hidden }; + } const ell = v.scene.globe.ellipsoid; const C = v.camera.positionCartographic.constructor; const center = ell.cartographicToCartesian(C.fromDegrees(window.__QA_SITE.lon, window.__QA_SITE.lat, 200));