mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 07:42:23 +00:00
The layers that still had one Cesium and one Navara implementation, or
drew on Cesium only, now draw through engine.movers, engine.lines and
engine.camera on both engines, and application code holds the engine
instead of guessing it from a viewer.
- Aircraft family: flights, military and local ADS-B share one display
(src/layers/aircraft/render/display.js). The Cesium and Navara
renderers are gone. Where they differed, the display follows Cesium:
- attach and visibility tracking, live disableDepthTestDistance
changes, the tracked glyph's distance scale, model ids that change,
local ADS-B markers hidden until shown and its UAT ring as a point;
- the trail head's hidden alpha is 0.45, the contract's
depthFailAlpha scaled by the head's 0.9.
The tracked glyph stays over the fleet, as on Navara. The movers
contract reports readiness per model and reads model axes as the
glTF's own on every engine. Cesium's follow takes any native target
through a proxy entity in the ENU frame and mirrors its readout fields.
- Wildlife Sightings draws badges and the density glow as sprite sets
and its privacy cells as line outlines (no fill yet). It takes clicks
from engine.input and its card anchors to the engine's canvas and
camera, so the layer is in both engines' catalogs again and a switch
keeps the session's layer set.
- Cockpit: cockpitHost takes the engine only. The ridden aircraft is the
engine follow's target, hidden through its sprite; only the Cesium
Viewer's stock double-click is still routed around.
- About forty camera, navigation, share-link, HUD, voice, detection and
layer-lifecycle callers take the engine. isGlobeEngine and
engineFromViewer calls outside src/globe fall from 51 to 14.
The coupling baseline drops accordingly: layer renderer lines from
5239/12331 to 4546/11106 (Cesium/Navara), `viewer` lines from 2424 to
2209, Cesium-importing files from 69 to 66.
334 lines
11 KiB
JavaScript
334 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
|
/** Real-app zoom and tilt regression. Start a dev server, then run this file. */
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import puppeteer from 'puppeteer';
|
|
|
|
const origin = process.env.QA_BASE_URL || 'http://localhost:4173';
|
|
const browser = await puppeteer.launch({
|
|
headless: true,
|
|
args: [
|
|
'--no-sandbox',
|
|
...(process.platform === 'darwin'
|
|
? ['--use-angle=metal', '--enable-gpu']
|
|
: [
|
|
'--use-gl=angle',
|
|
'--use-angle=swiftshader',
|
|
'--enable-unsafe-swiftshader',
|
|
]),
|
|
],
|
|
protocolTimeout: 120_000,
|
|
});
|
|
const errors = [];
|
|
try {
|
|
const page = await browser.newPage();
|
|
page.on('pageerror', (error) => errors.push(error.message));
|
|
await page.setViewport({ width: 1200, height: 800 });
|
|
await page.goto(`${origin}/?welcome=0`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForFunction(
|
|
() =>
|
|
window.__godsEyeView?.viewer &&
|
|
window.__godsEyeView.viewer.scene.screenSpaceCameraController
|
|
.enableInputs &&
|
|
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
|
{ timeout: 90_000 },
|
|
);
|
|
const renderer = await page.evaluate(async () => {
|
|
const C = await import('/node_modules/cesium/Build/Cesium/index.js');
|
|
const { maxPitchForHeight } = await import('/src/app/zoomOutGuard.js');
|
|
const { readCameraTargetFrame } =
|
|
await import('/src/ui/cameraOrientationControls.js');
|
|
const { viewer, engine } = window.__godsEyeView;
|
|
viewer.camera.cancelFlight();
|
|
viewer.trackedEntity = undefined;
|
|
// Explicit real renders make each gesture independent of host frame rate.
|
|
viewer.useDefaultRenderLoop = false;
|
|
const controller = viewer.scene.screenSpaceCameraController;
|
|
controller.inertiaZoom = 0;
|
|
controller.inertiaSpin = 0;
|
|
const original = viewer.camera.setView.bind(viewer.camera);
|
|
const qa = {
|
|
viewer,
|
|
guardWrites: 0,
|
|
targetPitch: () => readCameraTargetFrame(engine, viewer)?.pitch,
|
|
history: [],
|
|
frame() {
|
|
viewer.scene.requestRender();
|
|
viewer.render();
|
|
qa.history.push({
|
|
height: viewer.camera._positionCartographic.height,
|
|
guardWrites: qa.guardWrites,
|
|
position: { ...viewer.camera.position },
|
|
});
|
|
if (qa.history.length > 80) qa.history.shift();
|
|
},
|
|
pose() {
|
|
return {
|
|
height: viewer.camera.positionCartographic.height,
|
|
pitch: viewer.camera.pitch,
|
|
maxPitch: maxPitchForHeight(
|
|
viewer.camera.positionCartographic.height,
|
|
),
|
|
guardWrites: qa.guardWrites,
|
|
};
|
|
},
|
|
reset(height = 1_000_000, pitch = -35) {
|
|
viewer.camera.cancelFlight();
|
|
viewer.camera.lookAtTransform(C.Matrix4.IDENTITY);
|
|
original({
|
|
destination: C.Cartesian3.fromDegrees(0, 0, height),
|
|
orientation: { heading: 0, pitch: C.Math.toRadians(pitch), roll: 0 },
|
|
});
|
|
qa.frame();
|
|
qa.guardWrites = 0;
|
|
},
|
|
};
|
|
viewer.camera.setView = (options) => {
|
|
// Cesium itself also calls setView while processing touch movement.
|
|
if (new Error().stack.includes('/src/app/zoomOutGuard.js'))
|
|
qa.guardWrites++;
|
|
return original(options);
|
|
};
|
|
window.__zoomGuardQa = qa;
|
|
const gl = viewer.scene.context._gl;
|
|
const debug = gl.getExtension('WEBGL_debug_renderer_info');
|
|
return gl.getParameter(debug ? debug.UNMASKED_RENDERER_WEBGL : gl.RENDERER);
|
|
});
|
|
console.log(`[INFO] WebGL renderer: ${renderer}`);
|
|
const frame = (count = 1) =>
|
|
page.evaluate(async (n) => {
|
|
for (let i = 0; i < n; i++) {
|
|
window.__zoomGuardQa.frame();
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
}
|
|
return window.__zoomGuardQa.pose();
|
|
}, count);
|
|
const reset = (height, pitch) =>
|
|
page.evaluate((h, p) => window.__zoomGuardQa.reset(h, p), height, pitch);
|
|
const session = await page.createCDPSession();
|
|
for (const viewport of [
|
|
{ width: 1200, height: 800 },
|
|
{ width: 390, height: 844 },
|
|
]) {
|
|
await page.setViewport(viewport);
|
|
await page.evaluate(() => window.__zoomGuardQa.viewer.resize());
|
|
const x = Math.round(viewport.width / 2);
|
|
const y = Math.round(viewport.height / 2);
|
|
await reset(1_000_000, -25);
|
|
await page.mouse.move(x, y);
|
|
await page.evaluate(
|
|
({ x, y }) => {
|
|
window.__zoomGuardQa.viewer.canvas.dispatchEvent(
|
|
new WheelEvent('wheel', {
|
|
deltaY: 2400,
|
|
clientX: x,
|
|
clientY: y,
|
|
bubbles: true,
|
|
cancelable: true,
|
|
}),
|
|
);
|
|
},
|
|
{ x, y },
|
|
);
|
|
const first = await frame();
|
|
console.log(`[INFO] ${viewport.width}px wheel delivered`);
|
|
const settled = await frame(40);
|
|
assert.ok(first.height > 1_000_000, 'wheel must move the real camera');
|
|
assert.ok(settled.guardWrites > 0, 'zoom guard must run');
|
|
assert.ok(
|
|
settled.pitch <= settled.maxPitch + 1e-6,
|
|
`stationary pitch must finish easing: ${JSON.stringify(settled)}`,
|
|
);
|
|
for (let i = 0; i < 12; i++) {
|
|
await page.evaluate(
|
|
({ x, y }) => {
|
|
window.__zoomGuardQa.viewer.canvas.dispatchEvent(
|
|
new WheelEvent('wheel', {
|
|
deltaY: 2400,
|
|
clientX: x,
|
|
clientY: y,
|
|
bubbles: true,
|
|
cancelable: true,
|
|
}),
|
|
);
|
|
},
|
|
{ x, y },
|
|
);
|
|
await frame();
|
|
}
|
|
const ceiling = await frame(40);
|
|
assert.ok(ceiling.height <= 40_000_001, 'user zoom respects the ceiling');
|
|
assert.ok(
|
|
ceiling.height > 39_000_000,
|
|
'exercise the ceiling, not a low orbit',
|
|
);
|
|
assert.ok(
|
|
ceiling.pitch <= ceiling.maxPitch + 1e-6,
|
|
`ceiling must settle: ${JSON.stringify(await page.evaluate(() => window.__zoomGuardQa.history))}`,
|
|
);
|
|
console.log(
|
|
`[PASS] ${viewport.width}px wheel, stationary easing and ceiling`,
|
|
);
|
|
if (process.env.QA_SCREENSHOT_DIR) {
|
|
fs.mkdirSync(process.env.QA_SCREENSHOT_DIR, { recursive: true });
|
|
await page.screenshot({
|
|
path: path.join(
|
|
process.env.QA_SCREENSHOT_DIR,
|
|
`zoom-ceiling-${viewport.width}.png`,
|
|
),
|
|
});
|
|
}
|
|
|
|
await reset();
|
|
await page.mouse.move(x, y);
|
|
await page.keyboard.down('Control');
|
|
await page.mouse.down({ button: 'right' });
|
|
for (let i = 1; i <= 6; i++) {
|
|
await page.mouse.move(x, y + i * 3);
|
|
await frame();
|
|
}
|
|
await page.mouse.up({ button: 'right' });
|
|
await page.keyboard.up('Control');
|
|
const tilted = await frame(3);
|
|
assert.equal(
|
|
tilted.guardWrites,
|
|
0,
|
|
'Ctrl+right tilt must not trigger guard',
|
|
);
|
|
assert.ok(Math.abs(tilted.height - 1_000_000) > 1, 'tilt must move camera');
|
|
|
|
await reset();
|
|
const touches = (dy) => [
|
|
{ x: x - 60, y: y + dy, id: 1 },
|
|
{ x: x + 60, y: y + dy, id: 2 },
|
|
];
|
|
await session.send('Input.dispatchTouchEvent', {
|
|
type: 'touchStart',
|
|
touchPoints: touches(0),
|
|
});
|
|
await frame();
|
|
for (let i = 1; i <= 6; i++) {
|
|
await session.send('Input.dispatchTouchEvent', {
|
|
type: 'touchMove',
|
|
touchPoints: touches(i * 3),
|
|
});
|
|
await frame();
|
|
}
|
|
await session.send('Input.dispatchTouchEvent', {
|
|
type: 'touchEnd',
|
|
touchPoints: [],
|
|
});
|
|
const translated = await frame(3);
|
|
assert.equal(
|
|
translated.guardWrites,
|
|
0,
|
|
'parallel two-finger tilt must not trigger guard',
|
|
);
|
|
assert.ok(
|
|
Math.abs(translated.height - 1_000_000) > 1,
|
|
'touch tilt must move camera',
|
|
);
|
|
console.log(
|
|
`[PASS] ${viewport.width}px Ctrl+right and parallel touch tilt`,
|
|
);
|
|
await reset(1_000_000, -35);
|
|
await page.evaluate(
|
|
({ x, y }) => {
|
|
window.__zoomGuardQa.viewer.canvas.dispatchEvent(
|
|
new WheelEvent('wheel', {
|
|
deltaY: 120,
|
|
clientX: x,
|
|
clientY: y,
|
|
bubbles: true,
|
|
cancelable: true,
|
|
}),
|
|
);
|
|
},
|
|
{ x, y },
|
|
);
|
|
const beforeTilt = await frame();
|
|
assert.ok(
|
|
beforeTilt.guardWrites > 0,
|
|
'wheel correction must be pending before UI tilt',
|
|
);
|
|
const targetPitchBeforeClick = await page.evaluate(() =>
|
|
window.__zoomGuardQa.targetPitch(),
|
|
);
|
|
assert.ok(
|
|
Number.isFinite(targetPitchBeforeClick),
|
|
'UI tilt fixture must have a ground target under the canvas center',
|
|
);
|
|
// Programmatic fixture resets precede moveEnd, so the button's published
|
|
// aria state may still describe the previous view. The action toggles the
|
|
// current target frame: angles shallower than -60 degrees are tilted.
|
|
const wasTilted = targetPitchBeforeClick > -Math.PI / 3;
|
|
await page.click('#tilt-map-view');
|
|
const uiTilt = await page.evaluate(
|
|
async (desiredDegrees) => {
|
|
const qa = window.__zoomGuardQa;
|
|
const desired = (desiredDegrees * Math.PI) / 180;
|
|
const started = performance.now();
|
|
const deadline = started + 3000;
|
|
while (performance.now() < deadline) {
|
|
qa.frame();
|
|
// The 650 ms animator can be numerically near its destination
|
|
// before it releases preUpdate. Finish its lifetime before resetting.
|
|
if (
|
|
performance.now() - started >= 700 &&
|
|
Math.abs(qa.targetPitch() - desired) < 1e-5
|
|
)
|
|
return { settled: true, guardWrites: qa.guardWrites };
|
|
await new Promise((resolve) => setTimeout(resolve, 16));
|
|
}
|
|
return {
|
|
settled: false,
|
|
guardWrites: qa.guardWrites,
|
|
pitch: qa.targetPitch(),
|
|
};
|
|
},
|
|
wasTilted ? -89 : -35,
|
|
);
|
|
assert.ok(
|
|
uiTilt.settled,
|
|
`UI tilt must reach its destination: ${JSON.stringify(uiTilt)}`,
|
|
);
|
|
assert.equal(
|
|
uiTilt.guardWrites,
|
|
beforeTilt.guardWrites,
|
|
'UI navigation handoff must cancel pending zoom correction',
|
|
);
|
|
console.log(
|
|
`[PASS] ${viewport.width}px UI tilt takes over a pending wheel correction`,
|
|
);
|
|
|
|
await reset(1_000_000, -25);
|
|
await session.send('Input.dispatchTouchEvent', {
|
|
type: 'touchStart',
|
|
touchPoints: touches(0),
|
|
});
|
|
await frame();
|
|
await session.send('Input.dispatchTouchEvent', {
|
|
type: 'touchMove',
|
|
touchPoints: [
|
|
{ x: x - 35, y, id: 1 },
|
|
{ x: x + 35, y, id: 2 },
|
|
],
|
|
});
|
|
await frame();
|
|
await session.send('Input.dispatchTouchEvent', {
|
|
type: 'touchEnd',
|
|
touchPoints: [],
|
|
});
|
|
const pinched = await frame(40);
|
|
assert.ok(pinched.height > 1_000_000, 'pinch-out must move camera outward');
|
|
assert.ok(pinched.guardWrites > 0, 'pinch-out must trigger the guard');
|
|
assert.ok(pinched.pitch <= pinched.maxPitch + 1e-6);
|
|
console.log(`[PASS] ${viewport.width}px pinch-out settles below the limb`);
|
|
}
|
|
assert.deepEqual(errors, [], 'no JavaScript page errors');
|
|
console.log('[PASS] no JavaScript page errors');
|
|
} finally {
|
|
await browser.close();
|
|
}
|