mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
Move Cesium viewer, map, sky, projection, trail and sonar internals into its engine. Drive styles, bloom, sharpening and contact sonar through engine.render. Count camera-input suspensions across all three engines and migrate the feature callers. Preserve Cesium's effects and input behaviour; fix transit lens clicks on Cesium and make zero sharpening on Navara disable sharpening, as it does on Cesium. Parse imported files into plain records and draw them through movers, lines, ground and imagery on both engines. Follow Cesium's KML style precedence, preserve local KMZ images and frame the drawable records. Open overlays use imagery when the engine has no native map-overlay API. Run every imported-display case on fake and headless Cesium engines. The shared import path retains these known differences from Cesium's old DataSources: wall fills, extruded prisms and gx:LatLonQuad are undrawn; raised polygon fills drape while outlines retain altitude; GroundOverlay rotation, altitude and RGB tint are not applied; rhumb lines become great circles. Ground heights use the loaded surface synchronously instead of waiting for most-detailed sampling. Supported KMZ ground overlays now draw on Navara too. Document the limits rather than claiming full parity. Combine batches B and C because their disjoint path manifests cannot place the shared boundary and format files in a later commit while keeping B independently valid. This combined commit owns the shared docs, package boundaries, format scope, locale catalogs and coupling baseline. Remove five obsolete import strings from all eleven catalogs. Make the load-time source scan include new files and skip deleted paths so the same checks work before and after recording the moves. Coupling: Navara renderer lines fall from 1695 to 1294, Cesium import files from 25 to 9, viewer lines from 1177 to 1026, SDK loads from 4 to 3 and engine-name lines from 62 to 42. No count grows. 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-contract-suite.mjs http://localhost:4291 node scripts/qa-navara-visual-styles.mjs http://localhost:4291 node scripts/qa-navara-bloom.mjs http://localhost:4291 node scripts/qa-navara-sonar.mjs http://localhost:4291 QA_BASE_URL=http://localhost:4291 node scripts/qa-visual-effects.mjs QA_BASE_URL=http://localhost:4291 node scripts/qa-zoom-out-guard.mjs node scripts/qa-sky-model-lighting.mjs --url http://localhost:4291 node scripts/qa-geo-import.mjs http://localhost:4291 node scripts/qa-geo-import-regressions.mjs http://localhost:4291 node scripts/qa-geo-import-large.mjs http://localhost:4291 node scripts/qa-navara-geo-import.mjs http://localhost:4291 Use the keyless server for every check. Photoreal-only cases remain unverified; do not enable paid tiles to run them.
335 lines
11 KiB
JavaScript
335 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/globe/cesium/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/globe/cesium/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();
|
|
}
|