mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 07:42:23 +00:00
Read QA drawing evidence through the globe contract
Port the layer probes in twenty-six browser scripts to engine primitive lists, drawn rows and layer diagnostics. Preserve Cesium as the reference and keep the supported Navara paths on the same observations. This changes QA observation only, with no application behaviour change. Review the interrupted infrastructure group and retain its readiness, drape, selection, label and floor checks. Match both installation outline buffers. Frame each CCTV city before reading its visible coverage. Read Radio colours and selected identities from actual mover writes. Retain transit's GPU upload budgets using engine-owned line primitives rather than substituting rebuild counts. Keep native inspection where a check specifically measures GPU uploads, loaded meshes or ground readiness. The preceding combined B/C commit owns all shared manifests, docs, locale catalogs and the coupling baseline; this commit changes only its own twenty-six QA scripts. Coupling remains unchanged at 344 Cesium renderer lines, 1294 Navara renderer lines, 9 Cesium import files, 1026 viewer lines, 2 Navara kind checks, 0 engine sniff calls, 9 extension reads, 1 ground pass, 3 SDK loads, 4 screen-handler files and 42 engine-name lines. The full unit suite and the boundary, format and coupling checks pass. Browser validation remains to be run. Browser checks to run GOOGLE_MAPS_API_KEY= CESIUM_ION_TOKEN= npm run dev -- --port 4291 node scripts/qa-alpr-journey.mjs http://localhost:4291 node scripts/qa-alpr-journey.mjs 'http://localhost:4291/?engine=navara' node scripts/qa-cables-overlay.mjs --url http://localhost:4291 node scripts/qa-cctv-v2.mjs --url http://localhost:4291 --fixture --terrain-globe node scripts/qa-context-arrivals.mjs http://localhost:4291 node scripts/qa-directions.mjs --url http://localhost:4291 QA_BASE_URL=http://localhost:4291 node scripts/qa-draw-tool.mjs QA_BASE_URL=http://localhost:4291 node scripts/qa-floor-hold.mjs node scripts/qa-ghostmaps.mjs --url http://localhost:4291 node scripts/qa-gnss.mjs --url http://localhost:4291 node scripts/qa-height-datum.mjs --url http://localhost:4291 node scripts/qa-infra-lod.mjs --url http://localhost:4291 node scripts/qa-installation-navigation.mjs http://localhost:4291 node scripts/qa-military-names.mjs http://localhost:4291 --no-video node scripts/qa-overpass-offload.mjs --url http://localhost:4291 node scripts/qa-radio.mjs --url http://localhost:4291 node scripts/qa-traffic-motion.mjs http://localhost:4291 --mode=osm QA_BASE_URL=http://localhost:4291 node scripts/qa-transit-recovery.mjs QA_BASE_URL=http://localhost:4291 node scripts/qa-transit.mjs node scripts/qa-weather-teardown.mjs --url http://localhost:4291 --headless for engine in cesium navara; do node scripts/qa-enrich-ambient.mjs --url http://localhost:4291 --engine=$engine QA_BASE_URL=http://localhost:4291 node scripts/qa-floor-verify.mjs --engine=$engine node scripts/qa-heading-b3.mjs --url http://localhost:4291 --engine=$engine node scripts/qa-overlay-baseline.mjs --url http://localhost:4291 --engine=$engine node scripts/qa-sprites-b5.mjs --url http://localhost:4291 --engine=$engine node scripts/qa-vessel-datum.mjs --url 'http://localhost:4291/?qaFeed=vessels:fixture' --engine $engine done The military-names driver exercises installation-polish; the transit driver exercises transit-scenes. Use only the keyless server. Cases that require photoreal tiles or unavailable providers remain unverified.
This commit is contained in:
1 parent
97c4dc76ce
commit
3907b7084e
26 files changed
+1429
-1075
No files matched your search
+169
-69
@@ -286,24 +286,106 @@ function installRenderFramesHelper(page) {
|
||||
});
|
||||
}
|
||||
|
||||
// In-page helper (stringified and re-defined inside each evaluate that needs
|
||||
// it, since functions can't cross the puppeteer boundary): serializes the
|
||||
// 5 frustum polylines + plane position/orientation/dimensions for a camera
|
||||
// into plain arrays of numbers, using only the camera's own clock time (no
|
||||
// Cesium namespace needed — Property.getValue just needs *a* JulianDate,
|
||||
// and the viewer's own clock.currentTime is a valid live instance of one).
|
||||
// In-page helpers (stringified and re-defined inside each evaluate that needs
|
||||
// them, since functions can't cross the puppeteer boundary).
|
||||
//
|
||||
// The frustum wireframe (four corner rays and the far cap) and the gizmo draw
|
||||
// through the globe contract (src/layers/cctv/render/display.js): engine
|
||||
// lines named `cctv-<camera>-<role>` and `cctv-gizmo-<part>`, and the gizmo's
|
||||
// handles in the `cctv-gizmo-points` point set. engine.lines.list() names the
|
||||
// live lines (id, points, shown); their positions are what the layer's own
|
||||
// line handles hold (record.coverageEntities). The monitor plane is still a
|
||||
// Cesium plane Entity (render/cesiumMeshes.js), read as before.
|
||||
const CONTRACT_SRC = `
|
||||
function cctvRecord(camId) {
|
||||
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
||||
return (
|
||||
mod._cctvRecordsForTest().find((r) => r.camera && r.camera.id === camId) ||
|
||||
null
|
||||
);
|
||||
}
|
||||
function liveLines() {
|
||||
return new Map(
|
||||
window.__godsEyeView.engine.lines.list().map((line) => [line.id, line]),
|
||||
);
|
||||
}
|
||||
// The positions of camera camId's coverage line 'role', when the engine
|
||||
// draws that line with the handle's points; null otherwise.
|
||||
function coverageLinePositions(camId, role, lines = liveLines()) {
|
||||
const id = 'cctv-' + camId + '-' + role;
|
||||
const line = lines.get(id);
|
||||
const record = cctvRecord(camId);
|
||||
const handle = ((record && record.coverageEntities) || []).find(
|
||||
(h) => h && h.id === id,
|
||||
);
|
||||
const positions = handle && handle.polyline && handle.polyline.positions;
|
||||
if (!line || !positions || line.points !== positions.length) return null;
|
||||
return Array.from(positions, (p) => [p.x, p.y, p.z]);
|
||||
}
|
||||
// A Cesium mover set's instance by pickId (the contract's own primitives:
|
||||
// a collection tagged gevMoverSet in scene.primitives).
|
||||
function moverInstance(setId, pickId) {
|
||||
const walk = (collection) => {
|
||||
if (!collection || typeof collection.get !== 'function') return null;
|
||||
if (collection.gevMoverSet === setId) {
|
||||
for (let i = 0; i < collection.length; i++) {
|
||||
const item = collection.get(i);
|
||||
if (item && item.id === pickId) return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
for (let i = 0; i < collection.length; i++) {
|
||||
const found = walk(collection.get(i));
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return walk(window.__godsEyeView.viewer.scene.primitives);
|
||||
}
|
||||
// 'shown' | 'hidden' | 'missing' for one gizmo part: its line (rings and
|
||||
// arrow shafts) and its point (arrow tips and handles), as many as it has.
|
||||
function gizmoPartState(part) {
|
||||
const id = 'cctv-gizmo-' + part;
|
||||
const line = liveLines().get(id);
|
||||
const point = moverInstance('cctv-gizmo-points', id);
|
||||
if (!line && !point) return 'missing';
|
||||
const shown = [line && line.shown, point && point.show].filter(
|
||||
(value) => value !== undefined && value !== null,
|
||||
);
|
||||
return shown.every(Boolean) ? 'shown' : 'hidden';
|
||||
}
|
||||
// The viewshed volumes the layer holds (record.viewshedPrimitive), with
|
||||
// their Cesium Primitive, only where that primitive is in the scene.
|
||||
function viewshedVolumes() {
|
||||
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
||||
const primitives = window.__godsEyeView.viewer.scene.primitives;
|
||||
const out = [];
|
||||
for (const record of mod._cctvRecordsForTest()) {
|
||||
const volume = record.viewshedPrimitive;
|
||||
const primitive = volume && !volume.destroyed && volume.native
|
||||
? volume.native.primitive
|
||||
: null;
|
||||
if (primitive && primitives.contains(primitive))
|
||||
out.push({ camera: record.camera.id, volume, primitive });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
`;
|
||||
|
||||
// serializeGeom: the 5 frustum polylines + plane position/orientation/
|
||||
// dimensions for a camera as plain arrays of numbers. The plane's properties
|
||||
// are read at the viewer's own clock.currentTime (Property.getValue just needs
|
||||
// *a* JulianDate, and that is a valid live instance of one).
|
||||
const SERIALIZE_GEOM_SRC = `
|
||||
${CONTRACT_SRC}
|
||||
function serializeGeom(camId) {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const time = viewer.clock.currentTime;
|
||||
const roles = ['ray-tl', 'ray-tr', 'ray-br', 'ray-bl', 'cap'];
|
||||
const lines = liveLines();
|
||||
const poly = {};
|
||||
for (const role of roles) {
|
||||
const ent = viewer.entities.getById('cctv-' + camId + '-' + role);
|
||||
poly[role] = ent && ent.polyline
|
||||
? ent.polyline.positions.getValue(time).map((p) => [p.x, p.y, p.z])
|
||||
: null;
|
||||
}
|
||||
for (const role of roles)
|
||||
poly[role] = coverageLinePositions(camId, role, lines);
|
||||
const planeEnt = viewer.entities.getById('cctv-' + camId + '-plane');
|
||||
let plane = null;
|
||||
if (planeEnt) {
|
||||
@@ -529,6 +611,8 @@ async function main() {
|
||||
),
|
||||
{ timeoutMs: 60000 },
|
||||
);
|
||||
if (!appReady) throw new Error('app did not initialise within 60s');
|
||||
await installRenderFramesHelper(page);
|
||||
await page.evaluate(async () => {
|
||||
const app = window.__godsEyeView;
|
||||
await app.styleManager.initialRestorePromise;
|
||||
@@ -790,14 +874,18 @@ async function main() {
|
||||
(await waitForPredicate(
|
||||
page,
|
||||
(expected, base) => {
|
||||
const { viewer, dataManager } = window.__godsEyeView;
|
||||
const camera = dataManager.layers
|
||||
.get('cctv')
|
||||
.module.getUIState().activeCamera;
|
||||
const ray = viewer.entities.getById(`cctv-${camera.id}-ray-tl`);
|
||||
const mount = ray?.polyline?.positions.getValue(
|
||||
viewer.clock.currentTime,
|
||||
)?.[0];
|
||||
const { viewer, dataManager, engine } = window.__godsEyeView;
|
||||
const module = dataManager.layers.get('cctv').module;
|
||||
const camera = module.getUIState().activeCamera;
|
||||
// The corner ray is an engine line; its first point (the mount)
|
||||
// is what the layer's line handle gave it.
|
||||
const id = `cctv-${camera.id}-ray-tl`;
|
||||
const drawn = engine.lines.list().some((line) => line.id === id);
|
||||
const ray = module
|
||||
._cctvRecordsForTest()
|
||||
.find((record) => record.camera?.id === camera.id)
|
||||
?.coverageEntities?.find((handle) => handle?.id === id);
|
||||
const mount = drawn ? ray?.polyline?.positions?.[0] : undefined;
|
||||
const height =
|
||||
mount &&
|
||||
viewer.scene.globe.ellipsoid.cartesianToCartographic(mount)
|
||||
@@ -961,7 +1049,7 @@ async function main() {
|
||||
'cap',
|
||||
].every((r) => Array.isArray(geomInfo.poly[r]));
|
||||
record(
|
||||
'all 5 frustum polyline entities exist for the active camera',
|
||||
'all 5 frustum lines are drawn for the active camera (engine lines)',
|
||||
allPolylinesPresent,
|
||||
Object.keys(geomInfo.poly)
|
||||
.map((k) => `${k}=${geomInfo.poly[k] ? 'ok' : 'MISSING'}`)
|
||||
@@ -1775,35 +1863,51 @@ async function main() {
|
||||
console.log(
|
||||
'Checking viewshed mode (coverage tri-state + color-coded volumes)...',
|
||||
);
|
||||
// The volumes are the layer's (record.viewshedPrimitive), each a Cesium
|
||||
// Primitive in scene.primitives (render/cesiumMeshes.js); they carry no
|
||||
// tag of their own any more.
|
||||
const countVolumes = () =>
|
||||
page.evaluate(() => {
|
||||
const prims = window.__godsEyeView.viewer.scene.primitives;
|
||||
let n = 0;
|
||||
for (let i = 0; i < prims.length; i++) {
|
||||
if (prims.get(i) && prims.get(i)._gevViewshed) n += 1;
|
||||
}
|
||||
return n;
|
||||
});
|
||||
page.evaluate(`(function(){
|
||||
${CONTRACT_SRC}
|
||||
return viewshedVolumes().length;
|
||||
})()`);
|
||||
|
||||
const cBeforeViewshed = await readCounters();
|
||||
await page.evaluate(() => {
|
||||
await page.evaluate(`(function(){
|
||||
${CONTRACT_SRC}
|
||||
const primitives = window.__godsEyeView.viewer.scene.primitives;
|
||||
const events = [];
|
||||
window.__qaViewshedTrace = events;
|
||||
// A volume is a synchronous Primitive the pick pass skips; which
|
||||
// camera it belongs to is looked up when the trace is read.
|
||||
const isVolume = (primitive) =>
|
||||
primitive &&
|
||||
primitive.allowPicking === false &&
|
||||
primitive.asynchronous === false &&
|
||||
typeof primitive.get !== 'function';
|
||||
for (const method of ['add', 'remove']) {
|
||||
const original = primitives[method].bind(primitives);
|
||||
primitives[method] = (primitive, ...args) => {
|
||||
if (primitive?._gevViewshed)
|
||||
if (isVolume(primitive))
|
||||
events.push({
|
||||
method,
|
||||
camera: primitive._gevViewshed,
|
||||
primitive,
|
||||
at: performance.now(),
|
||||
stack: new Error().stack,
|
||||
});
|
||||
return original(primitive, ...args);
|
||||
};
|
||||
}
|
||||
});
|
||||
window.__qaViewshedTraceRead = () => {
|
||||
const owners = new Map(
|
||||
viewshedVolumes().map((entry) => [entry.primitive, entry.camera]),
|
||||
);
|
||||
return events.map(({ primitive, ...event }) => ({
|
||||
...event,
|
||||
camera: owners.get(primitive) ?? null,
|
||||
}));
|
||||
};
|
||||
})()`);
|
||||
const modeAfterSet = await page.evaluate(() => {
|
||||
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
||||
mod.setParams({ coverageMode: 'viewshed' });
|
||||
@@ -1866,29 +1970,22 @@ async function main() {
|
||||
terrainRequests.size === 0 && Date.now() - terrainActivityAt >= 250;
|
||||
record('footprint requests settle before viewshed idle', terrainSettled);
|
||||
if (!terrainSettled) throw new Error('Footprint requests did not settle');
|
||||
await page.evaluate(() => {
|
||||
const prims = window.__godsEyeView.viewer.scene.primitives;
|
||||
window.__qaViewshedRefs = [];
|
||||
for (let i = 0; i < prims.length; i++) {
|
||||
if (prims.get(i) && prims.get(i)._gevViewshed)
|
||||
window.__qaViewshedRefs.push(prims.get(i));
|
||||
}
|
||||
});
|
||||
await page.evaluate(`(function(){
|
||||
${CONTRACT_SRC}
|
||||
window.__qaViewshedRefs = viewshedVolumes().map((entry) => entry.primitive);
|
||||
})()`);
|
||||
await sleep(8000);
|
||||
const viewshedIdle = await page.evaluate(() => {
|
||||
const prims = window.__godsEyeView.viewer.scene.primitives;
|
||||
const now = [];
|
||||
for (let i = 0; i < prims.length; i++) {
|
||||
if (prims.get(i) && prims.get(i)._gevViewshed) now.push(prims.get(i));
|
||||
}
|
||||
const viewshedIdle = await page.evaluate(`(function(){
|
||||
${CONTRACT_SRC}
|
||||
const now = viewshedVolumes().map((entry) => entry.primitive);
|
||||
const before = window.__qaViewshedRefs || [];
|
||||
const sameCount = now.length === before.length;
|
||||
const sameIdentity = sameCount && before.every((p) => now.includes(p));
|
||||
return { sameCount, sameIdentity, count: now.length };
|
||||
});
|
||||
})()`);
|
||||
const cAfterViewshed = await readCounters();
|
||||
evidence.viewshedTrace = await page.evaluate(
|
||||
() => window.__qaViewshedTrace,
|
||||
evidence.viewshedTrace = await page.evaluate(() =>
|
||||
window.__qaViewshedTraceRead?.(),
|
||||
);
|
||||
const lateFloorSamples =
|
||||
cAfterViewshed.sampleHeight - cBeforeViewshed.sampleHeight;
|
||||
@@ -1931,15 +2028,12 @@ async function main() {
|
||||
'handle-fov-l',
|
||||
'handle-fov-r',
|
||||
];
|
||||
const gizmoStates = await page.evaluate((parts) => {
|
||||
const gizmoStates = await page.evaluate(`(function(parts){
|
||||
${CONTRACT_SRC}
|
||||
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
||||
mod.setParams({ calibrationMode: true });
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
return parts.map((p) => {
|
||||
const e = viewer.entities.getById('cctv-gizmo-' + p);
|
||||
return e ? (e.show ? 'shown' : 'hidden') : 'missing';
|
||||
});
|
||||
}, GIZMO_PARTS);
|
||||
return parts.map(gizmoPartState);
|
||||
})(${JSON.stringify(GIZMO_PARTS)})`);
|
||||
record(
|
||||
'all 8 gizmo parts exist and show in ADJUST mode',
|
||||
gizmoStates.every((s) => s === 'shown'),
|
||||
@@ -1949,11 +2043,20 @@ async function main() {
|
||||
// Synthetic CDP drag on the EAST arrow — the owner's field-test
|
||||
// regression. Sample effective elevation after every mouse move: it must
|
||||
// stay frozen until release, with no transient sampleHeight calls.
|
||||
const eastDrag = await page.evaluate(() => {
|
||||
// The E arrow's shaft runs from the active camera's mount to its tip,
|
||||
// the `cctv-gizmo-move-east` point in the gizmo's point set.
|
||||
const eastDrag = await page.evaluate(`(function(){
|
||||
${CONTRACT_SRC}
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const e = viewer.entities.getById('cctv-gizmo-move-east');
|
||||
if (!e) return null;
|
||||
const pts = e.polyline.positions.getValue(viewer.clock.currentTime);
|
||||
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
||||
const active = mod.getUIState().activeCameraId;
|
||||
const mount = cctvRecord(active)?.frustumPositions?.mount;
|
||||
const tip = moverInstance('cctv-gizmo-points', 'cctv-gizmo-move-east');
|
||||
if (!mount || !tip || gizmoPartState('move-east') !== 'shown') return null;
|
||||
const pts = [
|
||||
new tip.position.constructor(mount.x, mount.y, mount.z),
|
||||
tip.position,
|
||||
];
|
||||
const scene = viewer.scene;
|
||||
const w = scene.canvas.clientWidth;
|
||||
const h = scene.canvas.clientHeight;
|
||||
@@ -1982,7 +2085,7 @@ async function main() {
|
||||
}
|
||||
if (!candidates.length) return null;
|
||||
return { ...candidates[0], candidates, ux: dx / mag, uy: dy / mag };
|
||||
});
|
||||
})()`);
|
||||
|
||||
const calBeforeDrag = await page.evaluate(() => {
|
||||
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
||||
@@ -2227,16 +2330,13 @@ async function main() {
|
||||
);
|
||||
|
||||
// ADJUST off: parts hidden; leave a clean calibration for the next group.
|
||||
const gizmoOff = await page.evaluate((parts) => {
|
||||
const gizmoOff = await page.evaluate(`(function(parts){
|
||||
${CONTRACT_SRC}
|
||||
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
||||
mod.setParams({ calibrationMode: false });
|
||||
mod.setParams({ calibration: { reset: true } });
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
return parts.map((p) => {
|
||||
const e = viewer.entities.getById('cctv-gizmo-' + p);
|
||||
return e ? (e.show ? 'shown' : 'hidden') : 'missing';
|
||||
});
|
||||
}, GIZMO_PARTS);
|
||||
return parts.map(gizmoPartState);
|
||||
})(${JSON.stringify(GIZMO_PARTS)})`);
|
||||
record(
|
||||
'gizmo parts hide when ADJUST mode turns off',
|
||||
gizmoOff.every((s) => s === 'hidden'),
|
||||
|
||||
Reference in new issue
Block a user