mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
feat(director): add authored camera directions and anchors
This commit is contained in:
1 parent
0ea10468ea
commit
13c0b8d1bc
24 files changed
+1104
-167
No files matched your search
@@ -733,5 +733,8 @@
|
||||
"src/director/timeline.test.mjs",
|
||||
"src/scenes/packs/registry.test.mjs",
|
||||
"scripts/qa-director-timing.mjs",
|
||||
"src/director/document.test.mjs"
|
||||
"src/director/document.test.mjs",
|
||||
"src/director/camera.test.mjs",
|
||||
"src/scenes/cameraMotion.test.mjs",
|
||||
"scripts/qa-director-camera.mjs"
|
||||
]
|
||||
@@ -618,7 +618,10 @@
|
||||
"src/director/playback.js",
|
||||
"src/director/clock.js",
|
||||
"src/director/timeline.js",
|
||||
"src/director/document.js"
|
||||
"src/director/document.js",
|
||||
"src/director/documentFields.js",
|
||||
"src/director/cameraDocument.js",
|
||||
"src/director/camera.js"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
@@ -648,7 +651,11 @@
|
||||
"src/scenes/packs/defaults.js",
|
||||
"src/scenes/packs/nepal.js",
|
||||
"src/director/document.js",
|
||||
"src/scenes/project.js"
|
||||
"src/scenes/project.js",
|
||||
"src/director/documentFields.js",
|
||||
"src/director/cameraDocument.js",
|
||||
"src/director/camera.js",
|
||||
"src/scenes/cameraMotion.js"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
@@ -1755,7 +1762,11 @@
|
||||
"src/scenes/packs/defaults.js",
|
||||
"src/scenes/packs/nepal.js",
|
||||
"src/director/document.js",
|
||||
"src/scenes/project.js"
|
||||
"src/scenes/project.js",
|
||||
"src/director/documentFields.js",
|
||||
"src/director/cameraDocument.js",
|
||||
"src/director/camera.js",
|
||||
"src/scenes/cameraMotion.js"
|
||||
],
|
||||
"external": [
|
||||
"@mapbox/vector-tile",
|
||||
@@ -2220,7 +2231,11 @@
|
||||
},
|
||||
"ui-navigation": {
|
||||
"exports": ["./ui/navigation"],
|
||||
"modules": ["src/navigationPolicy.js", "src/ui/navigationController.js"],
|
||||
"modules": [
|
||||
"src/navigationPolicy.js",
|
||||
"src/ui/navigationController.js",
|
||||
"src/app/stateChannel.js"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
"ui-share-restoration": {
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
#!/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;
|
||||
Reference in new issue
Block a user