mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
211 lines
7.2 KiB
JavaScript
211 lines
7.2 KiB
JavaScript
#!/usr/bin/env node
|
|
/** Verify imported camera directions through real rendering and camera authority handoffs. */
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import puppeteer from 'puppeteer';
|
|
const output = path.resolve('qa-shots/director-camera');
|
|
fs.mkdirSync(output, { recursive: true });
|
|
const browser = await puppeteer.launch({
|
|
headless: true,
|
|
args: ['--no-sandbox', '--disable-dev-shm-usage'],
|
|
});
|
|
const page = await browser.newPage();
|
|
const errors = [];
|
|
page.on('pageerror', (error) => errors.push(error.message));
|
|
let failed = 0;
|
|
function check(name, passed) {
|
|
console.log(`[${passed ? 'PASS' : 'FAIL'}] ${name}`);
|
|
if (!passed) failed++;
|
|
}
|
|
try {
|
|
await page.setViewport({ width: 1440, height: 900 });
|
|
await page.goto(
|
|
`${process.env.QA_BASE_URL || 'http://localhost:4173'}/?welcome=0`,
|
|
{ waitUntil: 'domcontentloaded' },
|
|
);
|
|
await page.waitForFunction(
|
|
() =>
|
|
window.__godsEyeView?.sceneDirector &&
|
|
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
|
{ timeout: 60000 },
|
|
);
|
|
const result = await page.evaluate(async () => {
|
|
const { sceneDirector: d, styleManager: style } = window.__godsEyeView;
|
|
const file = {
|
|
version: 4,
|
|
scenes: [
|
|
{
|
|
id: 'directions',
|
|
title: 'Camera directions',
|
|
anchors: [
|
|
{
|
|
id: 'start',
|
|
title: 'Austin west',
|
|
lat: 30.2672,
|
|
lon: -97.76,
|
|
alt: 6000,
|
|
altitudeReference: 'ellipsoid',
|
|
},
|
|
{
|
|
id: 'end',
|
|
title: 'Austin east',
|
|
lat: 30.2672,
|
|
lon: -97.73,
|
|
alt: 7000,
|
|
altitudeReference: 'ellipsoid',
|
|
},
|
|
],
|
|
shots: [
|
|
{
|
|
id: 'move',
|
|
title: 'Across Austin',
|
|
durationSec: 1,
|
|
holdSec: 0.2,
|
|
camera: { anchorId: 'end', heading: 10, pitch: -45, roll: 0 },
|
|
move: {
|
|
from: { anchorId: 'start', heading: 350, pitch: -45, roll: 0 },
|
|
easing: 'linear',
|
|
},
|
|
layers: {},
|
|
visual: { style: 'normal' },
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
await d.importProjectFile(
|
|
new File([JSON.stringify(file)], 'directions.json'),
|
|
);
|
|
const scene = d._project.scenes[0];
|
|
const shot = scene.shots[0];
|
|
const imported =
|
|
scene.anchors.length === 2 && shot.camera.anchorId === 'end';
|
|
await d.seekScene(scene.id, 0.5 / 1.2);
|
|
const middle = style.getCameraState();
|
|
const seekMidpoint =
|
|
Math.abs(middle.lon - -97.745) < 1e-6 &&
|
|
Math.abs(middle.alt - 6500) < 0.01;
|
|
await d.seekScene(scene.id, 0);
|
|
const back = style.getCameraState();
|
|
const backward = Math.abs(back.lon - -97.76) < 1e-6;
|
|
const replay = await d.replayShot(scene.id, shot.id);
|
|
const end = style.getCameraState();
|
|
const replayEnd =
|
|
replay?.started &&
|
|
Math.abs(end.lon - -97.73) < 1e-6 &&
|
|
Math.abs(end.alt - 7000) < 0.01;
|
|
d.stopScene();
|
|
await d.startScene(scene.id, { single: true, preview: false });
|
|
const completed =
|
|
!d.running && d.getPlaybackTimingState().activeTimers === 0;
|
|
const settled = Math.abs(style.getCameraState().lon - -97.73) < 1e-6;
|
|
// Updating the target creates an explicit inline pose while retaining the authored start.
|
|
d.updateSelectedShot();
|
|
const saved = JSON.parse(
|
|
localStorage.getItem('godsEyeView.sceneProject.v2'),
|
|
);
|
|
const updated =
|
|
saved.scenes[0].shots[0].camera.altitudeReference === 'ellipsoid' &&
|
|
saved.scenes[0].shots[0].move.from.anchorId === 'start';
|
|
shot.durationSec = 30;
|
|
const run = d.startScene(scene.id, { single: true, preview: false });
|
|
const deadline = Date.now() + 10000;
|
|
while (!d._cameraMotion.active && Date.now() < deadline)
|
|
await new Promise((r) => setTimeout(r, 20));
|
|
const started = d._cameraMotion.active;
|
|
style.runImmediateNavigation('location', () => true);
|
|
await run;
|
|
const revoked =
|
|
started &&
|
|
!d.running &&
|
|
!d._cameraMotion.active &&
|
|
d.getPlaybackTimingState().activeTimers === 0;
|
|
const stoppedPose = style.getCameraState();
|
|
await new Promise((r) => setTimeout(r, 150));
|
|
const stable =
|
|
Math.abs(style.getCameraState().lon - stoppedPose.lon) < 1e-7;
|
|
return {
|
|
imported,
|
|
seekMidpoint,
|
|
backward,
|
|
replayEnd,
|
|
completed,
|
|
settled,
|
|
updated,
|
|
revoked,
|
|
stable,
|
|
};
|
|
});
|
|
for (const [name, passed] of Object.entries(result)) check(name, passed);
|
|
await new Promise((r) => setTimeout(r, 6000));
|
|
await page.screenshot({ path: path.join(output, 'austin.png') });
|
|
await page.evaluate(() => {
|
|
const d = window.__godsEyeView.sceneDirector;
|
|
const pose = d.styleManager.getCameraState();
|
|
d._setCameraView({ ...pose, heading: pose.heading + 25, pitch: -60 });
|
|
d.viewer.scene.requestRender();
|
|
});
|
|
await new Promise((r) => setTimeout(r, 3000));
|
|
await page.screenshot({ path: path.join(output, 'angle.png') });
|
|
const inputStarted = await page.evaluate(async () => {
|
|
const d = window.__godsEyeView.sceneDirector;
|
|
const scene = d._project.scenes[0];
|
|
window.__qaCameraRun = d.startScene(scene.id, {
|
|
single: true,
|
|
preview: false,
|
|
});
|
|
const deadline = Date.now() + 10000;
|
|
while (!d._cameraMotion.active && Date.now() < deadline)
|
|
await new Promise((r) => setTimeout(r, 20));
|
|
return d._cameraMotion.active;
|
|
});
|
|
const canvas =
|
|
(await page.$('canvas.cesium-widget-canvas')) ||
|
|
(await page.$('.cesium-widget canvas'));
|
|
const rect = await canvas.boundingBox();
|
|
await page.mouse.move(rect.x + rect.width / 2, rect.y + rect.height / 2);
|
|
await page.mouse.down();
|
|
await page.mouse.up();
|
|
const input =
|
|
inputStarted &&
|
|
(await page.evaluate(async () => {
|
|
await window.__qaCameraRun;
|
|
const d = window.__godsEyeView.sceneDirector;
|
|
return !d.running && d.getPlaybackTimingState().activeTimers === 0;
|
|
}));
|
|
check('manual canvas input revokes the move and releases clocks', input);
|
|
const hold = await page.evaluate(async () => {
|
|
const { sceneDirector: d, styleManager: style } = window.__godsEyeView;
|
|
const scene = d._project.scenes[0];
|
|
const shot = scene.shots[0];
|
|
shot.durationSec = 0.2;
|
|
shot.holdSec = 30;
|
|
const run = d.startScene(scene.id, { single: true, preview: false });
|
|
const deadline = Date.now() + 10000;
|
|
while (
|
|
(!d._activeSceneTravel || !d.running || d._cameraMotion.active) &&
|
|
Date.now() < deadline
|
|
)
|
|
await new Promise((r) => setTimeout(r, 20));
|
|
const holding = d.running && !d._cameraMotion.active;
|
|
const began = performance.now();
|
|
style.runImmediateNavigation('location', () => true);
|
|
await run;
|
|
return (
|
|
holding &&
|
|
performance.now() - began < 1000 &&
|
|
d.getPlaybackTimingState().activeTimers === 0
|
|
);
|
|
});
|
|
check('navigation during an authored hold settles immediately', hold);
|
|
|
|
if (errors.length)
|
|
console.error(
|
|
errors.map((message) => message.replace(/https?:\/\/\S+/g, '[URL]')),
|
|
);
|
|
check('no uncaught browser errors', errors.length === 0);
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
if (failed) process.exitCode = 1;
|