mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
refactor(sky): cut what the sky does not need
- qa-anime-sky keeps only the Milky Way case; the satellites and keyless cases checked an anime shader this branch no longer changes. - Drop qa-sky-sun-moon.mjs: the viewer-options unit test covers it. - VisualEffects sets the sky's visibility directly: the viewer always carries a StarrySky and never outlives the effects. - Drop the unused star intensity knob, the malformed-row tolerance for our own pack, the test-only Transforms parameter and a private-name test. - DATA_SOURCES points to sky/README.md instead of repeating it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
d94cc35c81
commit
b00c7c12c8
10 files changed
+131
-515
No files matched your search
@@ -1077,6 +1077,5 @@
|
||||
"src/sky/starrySky.test.mjs",
|
||||
"src/tooling/skyAssets.test.mjs",
|
||||
"src/tooling/skyAssets.paintOut.test.mjs",
|
||||
"scripts/qa-sky-sun-moon.mjs",
|
||||
"scripts/qa-anime-sky.mjs"
|
||||
]
|
||||
+85
-284
@@ -1,81 +1,26 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* qa-anime-sky — the anime style must cel-shade the Earth, and leave the sky
|
||||
* without bands.
|
||||
* qa-anime-sky — the anime style must leave the sky without bands.
|
||||
*
|
||||
* Earlier the anime shader exempted the far plane, judged from the
|
||||
* post-process depth, from its luminance bands. That depth cannot tell the
|
||||
* sky from far geometry, so the Earth lost its bands. The shader now matches
|
||||
* main, and src/ui/visualEffects.js fades the Milky Way out with the style.
|
||||
* Each case runs headless against a Vite dev server:
|
||||
* Anime's luminance bands turn the faint Milky Way into blotches, so
|
||||
* src/ui/visualEffects.js fades the glow out with the style. Headless against
|
||||
* a Vite dev server, the real anime style looks away from the Earth at the
|
||||
* Galactic Centre: at most 0.5 % of the sky (the stars) may reach the first
|
||||
* band.
|
||||
*
|
||||
* satellites whole globe from 20 000 km with the satellites layer on
|
||||
* (near clamps to 0.1 m): no Earth pixel may skip the bands.
|
||||
* keyless keyless stack, Denver 1700 m looking west: no terrain pixel
|
||||
* above the ellipsoid horizon may skip the bands.
|
||||
* milkyway the real anime style, looking away from the Earth at the
|
||||
* Galactic Centre: at most 0.5 % of the sky (the stars) may
|
||||
* reach the first band.
|
||||
*
|
||||
* The first two cases compile the checkout's anime shader with a debug
|
||||
* output (R = pixel skipped the bands, G = raw depth 1.0) as an extra stage.
|
||||
*
|
||||
* Usage: node scripts/qa-anime-sky.mjs [--url http://localhost:4333/] [--case satellites|keyless|milkyway|all]
|
||||
* Exit: 0 all pass, 1 a case failed, 2 a scenario was not reached or the case is unknown.
|
||||
* Usage: node scripts/qa-anime-sky.mjs [--url http://localhost:4333/]
|
||||
* Exit: 0 pass, 1 fail, 2 the anime style was not reached.
|
||||
*/
|
||||
import puppeteer from 'puppeteer';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const opt = (name, fallback) => {
|
||||
const i = process.argv.indexOf(`--${name}`);
|
||||
return i > 0 && process.argv[i + 1] ? process.argv[i + 1] : fallback;
|
||||
};
|
||||
const URL_ = opt('url', 'http://localhost:4333/');
|
||||
const ALL_CASES = ['satellites', 'keyless', 'milkyway'];
|
||||
const requested = opt('case', 'all');
|
||||
const CASES = requested === 'all' ? ALL_CASES : [requested];
|
||||
if (!CASES.every((c) => ALL_CASES.includes(c))) {
|
||||
console.error(
|
||||
`unknown case ${requested}; expected ${ALL_CASES.join('|')}|all`,
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
const i = process.argv.indexOf('--url');
|
||||
const URL_ =
|
||||
i > 0 && process.argv[i + 1] ? process.argv[i + 1] : 'http://localhost:4333/';
|
||||
|
||||
let fs = readFileSync(
|
||||
fileURLToPath(new URL('../src/styles/anime.js', import.meta.url)),
|
||||
'utf8',
|
||||
);
|
||||
fs = fs
|
||||
.slice(fs.indexOf('/* glsl */ `') + 12, fs.lastIndexOf('`'))
|
||||
.replace('uniform float intensity;', 'const float intensity = 1.0;')
|
||||
.replace('uniform float saturation;', 'const float saturation = 1.0;')
|
||||
.replace('uniform float edgeThick;', 'const float edgeThick = 0.5;');
|
||||
if (!/uniform\s+sampler2D\s+depthTexture/.test(fs))
|
||||
fs = fs.replace(
|
||||
'uniform sampler2D colorTexture;',
|
||||
'uniform sampler2D colorTexture;\n uniform sampler2D depthTexture;',
|
||||
);
|
||||
if (!/float bands\b/.test(fs))
|
||||
throw new Error(
|
||||
'anime.js has no `bands` variable: adapt the debug line of this script',
|
||||
);
|
||||
fs = fs
|
||||
.replace(
|
||||
/out_FragColor = [^;]+;\s*}\s*$/,
|
||||
`out_FragColor = mode < 0.5 ? texture(colorTexture, uv)
|
||||
: vec4(bands > 255.0 ? 1.0 : 0.0, texture(depthTexture, uv).r >= 1.0 ? 1.0 : 0.0, 0.0, 1.0);
|
||||
}`,
|
||||
)
|
||||
.replace('void main()', 'uniform float mode;\n void main()');
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
args: ['--no-sandbox', '--ignore-gpu-blocklist'],
|
||||
});
|
||||
let failed = false,
|
||||
unreached = false;
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
page.on('pageerror', (e) => console.log('PAGEERR', String(e).slice(0, 300)));
|
||||
@@ -87,8 +32,8 @@ try {
|
||||
await page.waitForFunction(() => window.__godsEyeView?.viewer?.scene, {
|
||||
timeout: 120000,
|
||||
});
|
||||
await sleep(12000);
|
||||
await page.evaluate(async (fs) => {
|
||||
await new Promise((r) => setTimeout(r, 12000));
|
||||
const r = await page.evaluate(async () => {
|
||||
const entry = performance
|
||||
.getEntriesByType('resource')
|
||||
.map((e) => e.name)
|
||||
@@ -97,224 +42,80 @@ try {
|
||||
throw new Error(
|
||||
'run against the Vite dev server (needs .vite/deps/cesium.js)',
|
||||
);
|
||||
const C = (window.__C = await import(entry));
|
||||
window.__dbg = window.__godsEyeView.viewer.scene.postProcessStages.add(
|
||||
new C.PostProcessStage({ fragmentShader: fs, uniforms: { mode: 1 } }),
|
||||
const C = await import(entry),
|
||||
g = window.__godsEyeView,
|
||||
v = g.viewer,
|
||||
s = v.scene;
|
||||
g.styleManager.setStyle('anime', { userInitiated: true });
|
||||
const t = v.clock.currentTime;
|
||||
await C.Transforms.preloadIcrfFixed(
|
||||
new C.TimeInterval({
|
||||
start: C.JulianDate.addDays(t, -1, new C.JulianDate()),
|
||||
stop: C.JulianDate.addDays(t, 1, new C.JulianDate()),
|
||||
}),
|
||||
);
|
||||
}, fs);
|
||||
const setView = (v) =>
|
||||
page.evaluate((v) => {
|
||||
const C = window.__C,
|
||||
s = window.__godsEyeView.viewer.scene;
|
||||
s.camera.cancelFlight();
|
||||
s.camera.setView({
|
||||
destination: C.Cartesian3.fromDegrees(v.lon, v.lat, v.h),
|
||||
orientation: {
|
||||
heading: C.Math.toRadians(v.heading ?? 0),
|
||||
pitch: C.Math.toRadians(v.pitch),
|
||||
roll: 0,
|
||||
},
|
||||
});
|
||||
s.requestRender();
|
||||
}, v);
|
||||
// returns per-pixel {exempt, raw1, ground?} counts
|
||||
const measure = (truth) =>
|
||||
page.evaluate((truth) => {
|
||||
const s = window.__godsEyeView.viewer.scene;
|
||||
const read = () => {
|
||||
s.requestRender();
|
||||
s.render();
|
||||
const gl = s.context._gl,
|
||||
w = gl.drawingBufferWidth,
|
||||
h = gl.drawingBufferHeight,
|
||||
px = new Uint8Array(w * h * 4);
|
||||
gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, px);
|
||||
return px;
|
||||
};
|
||||
window.__dbg.uniforms.mode = 1;
|
||||
const px = read();
|
||||
let g;
|
||||
if (truth) {
|
||||
const keep = [s.skyAtmosphere, s.skyBox, s.sun, s.moon].map(
|
||||
(o) => o && o.show,
|
||||
);
|
||||
[s.skyAtmosphere, s.skyBox, s.sun, s.moon].forEach(
|
||||
(o) => o && (o.show = false),
|
||||
);
|
||||
window.__dbg.uniforms.mode = 0;
|
||||
g = read();
|
||||
[s.skyAtmosphere, s.skyBox, s.sun, s.moon].forEach(
|
||||
(o, i) => o && (o.show = keep[i]),
|
||||
);
|
||||
window.__dbg.uniforms.mode = 1;
|
||||
}
|
||||
const r = {
|
||||
total: px.length / 4,
|
||||
near: s._view.frustumCommandsList[0]?.near ?? null,
|
||||
frusta: s._view.frustumCommandsList.length,
|
||||
geom: 0,
|
||||
geomExempt: 0,
|
||||
raw1: 0,
|
||||
raw1Exempt: 0,
|
||||
ground: 0,
|
||||
groundRaw1: 0,
|
||||
groundExempt: 0,
|
||||
clearGlobeDepth: s._environmentState.clearGlobeDepth,
|
||||
};
|
||||
for (let i = 0; i < px.length; i += 4) {
|
||||
const ex = px[i] > 127,
|
||||
raw1 = px[i + 1] > 127;
|
||||
if (raw1) {
|
||||
r.raw1++;
|
||||
if (ex) r.raw1Exempt++;
|
||||
} else {
|
||||
r.geom++;
|
||||
if (ex) r.geomExempt++;
|
||||
}
|
||||
if (g && g[i] + g[i + 1] + g[i + 2] > 6) {
|
||||
r.ground++;
|
||||
if (raw1) r.groundRaw1++;
|
||||
if (ex) r.groundExempt++;
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}, truth);
|
||||
const verdict = (name, ok, reached, msg, r) => {
|
||||
const tag = !reached ? 'UNREACHED' : ok ? 'PASS' : 'FAIL';
|
||||
if (!reached) unreached = true;
|
||||
else if (!ok) failed = true;
|
||||
console.log(`${tag} ${name}: ${msg}\n ${JSON.stringify(r)}`);
|
||||
};
|
||||
|
||||
for (const c of CASES) {
|
||||
if (c === 'satellites') {
|
||||
await page.evaluate(() =>
|
||||
window.__godsEyeView.dataManager.setEnabled('satellites', true, {
|
||||
origin: 'user',
|
||||
}),
|
||||
);
|
||||
await sleep(20000);
|
||||
await setView({ lon: 10, lat: 20, h: 2.0e7, pitch: -90 });
|
||||
await sleep(5000);
|
||||
const r = await measure(false);
|
||||
verdict(
|
||||
'satellites',
|
||||
r.geomExempt <= 0.01 * r.geom,
|
||||
r.frusta > 0 && r.near !== null && r.near < 1 && r.geom > 50000,
|
||||
`${r.geomExempt} of ${r.geom} Earth pixels exempt from cel-shading (near ${r.near} m)`,
|
||||
r,
|
||||
);
|
||||
await page.evaluate(() =>
|
||||
window.__godsEyeView.dataManager.setEnabled('satellites', false, {
|
||||
origin: 'user',
|
||||
}),
|
||||
);
|
||||
await sleep(5000);
|
||||
} else if (c === 'keyless') {
|
||||
await setView({
|
||||
lon: -104.99,
|
||||
lat: 39.74,
|
||||
h: 1700,
|
||||
heading: 270,
|
||||
pitch: 0,
|
||||
});
|
||||
await sleep(25000);
|
||||
const r = await measure(true);
|
||||
verdict(
|
||||
'keyless',
|
||||
r.groundExempt <= 0.01 * r.ground,
|
||||
r.clearGlobeDepth && r.groundRaw1 > 1000,
|
||||
`${r.groundExempt} of ${r.ground} terrain pixels exempt (${r.groundRaw1} terrain pixels have raw depth 1.0)`,
|
||||
r,
|
||||
);
|
||||
} else if (c === 'milkyway') {
|
||||
const r = await page.evaluate(async () => {
|
||||
const C = window.__C,
|
||||
g = window.__godsEyeView,
|
||||
v = g.viewer,
|
||||
s = v.scene;
|
||||
window.__dbg.enabled = false;
|
||||
g.styleManager.setStyle('anime', { userInitiated: true });
|
||||
const t = v.clock.currentTime;
|
||||
await C.Transforms.preloadIcrfFixed(
|
||||
new C.TimeInterval({
|
||||
start: C.JulianDate.addDays(t, -1, new C.JulianDate()),
|
||||
stop: C.JulianDate.addDays(t, 1, new C.JulianDate()),
|
||||
}),
|
||||
);
|
||||
const m = C.Transforms.computeIcrfToFixedMatrix(t);
|
||||
const ra = C.Math.toRadians(266.4),
|
||||
dec = C.Math.toRadians(-29);
|
||||
const look = C.Matrix3.multiplyByVector(
|
||||
m,
|
||||
new C.Cartesian3(
|
||||
Math.cos(dec) * Math.cos(ra),
|
||||
Math.cos(dec) * Math.sin(ra),
|
||||
Math.sin(dec),
|
||||
),
|
||||
new C.Cartesian3(),
|
||||
);
|
||||
const north = C.Matrix3.multiplyByVector(
|
||||
m,
|
||||
C.Cartesian3.UNIT_Z,
|
||||
new C.Cartesian3(),
|
||||
);
|
||||
const right = C.Cartesian3.normalize(
|
||||
C.Cartesian3.cross(look, north, new C.Cartesian3()),
|
||||
new C.Cartesian3(),
|
||||
);
|
||||
s.camera.cancelFlight();
|
||||
s.camera.setView({
|
||||
destination: C.Cartesian3.multiplyByScalar(
|
||||
look,
|
||||
3e7,
|
||||
new C.Cartesian3(),
|
||||
),
|
||||
orientation: {
|
||||
direction: look,
|
||||
up: C.Cartesian3.cross(right, look, new C.Cartesian3()),
|
||||
},
|
||||
});
|
||||
await new Promise((res) => setTimeout(res, 2500));
|
||||
s.requestRender();
|
||||
s.render();
|
||||
const gl = s.context._gl,
|
||||
w = gl.drawingBufferWidth,
|
||||
h = gl.drawingBufferHeight,
|
||||
px = new Uint8Array(w * h * 4);
|
||||
gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, px);
|
||||
let banded = 0;
|
||||
for (let i = 0; i < px.length; i += 4)
|
||||
if (0.299 * px[i] + 0.587 * px[i + 1] + 0.114 * px[i + 2] >= 51)
|
||||
banded++;
|
||||
const out = {
|
||||
total: px.length / 4,
|
||||
banded,
|
||||
visibility: s.skyBox.milkyWayVisibility,
|
||||
animeOn: (() => {
|
||||
const stages = s.postProcessStages;
|
||||
for (let i = 0; i < stages.length; i++) {
|
||||
const stage = stages.get(i);
|
||||
if (stage.name === 'godsEyeView_anime')
|
||||
return stage.enabled && stage.uniforms.intensity > 0.99;
|
||||
}
|
||||
return false;
|
||||
})(),
|
||||
};
|
||||
g.styleManager.setStyle('normal', { userInitiated: true });
|
||||
window.__dbg.enabled = true;
|
||||
return out;
|
||||
});
|
||||
verdict(
|
||||
'milkyway',
|
||||
r.banded <= 0.005 * r.total,
|
||||
r.total > 0 && r.animeOn,
|
||||
`${r.banded} of ${r.total} sky pixels at or above the first cel band (luma >= 0.2)`,
|
||||
r,
|
||||
);
|
||||
const m = C.Transforms.computeIcrfToFixedMatrix(t);
|
||||
const ra = C.Math.toRadians(266.4),
|
||||
dec = C.Math.toRadians(-29);
|
||||
const look = C.Matrix3.multiplyByVector(
|
||||
m,
|
||||
new C.Cartesian3(
|
||||
Math.cos(dec) * Math.cos(ra),
|
||||
Math.cos(dec) * Math.sin(ra),
|
||||
Math.sin(dec),
|
||||
),
|
||||
new C.Cartesian3(),
|
||||
);
|
||||
const north = C.Matrix3.multiplyByVector(
|
||||
m,
|
||||
C.Cartesian3.UNIT_Z,
|
||||
new C.Cartesian3(),
|
||||
);
|
||||
const right = C.Cartesian3.normalize(
|
||||
C.Cartesian3.cross(look, north, new C.Cartesian3()),
|
||||
new C.Cartesian3(),
|
||||
);
|
||||
s.camera.cancelFlight();
|
||||
s.camera.setView({
|
||||
destination: C.Cartesian3.multiplyByScalar(look, 3e7, new C.Cartesian3()),
|
||||
orientation: {
|
||||
direction: look,
|
||||
up: C.Cartesian3.cross(right, look, new C.Cartesian3()),
|
||||
},
|
||||
});
|
||||
await new Promise((res) => setTimeout(res, 2500));
|
||||
s.requestRender();
|
||||
s.render();
|
||||
const gl = s.context._gl,
|
||||
w = gl.drawingBufferWidth,
|
||||
h = gl.drawingBufferHeight,
|
||||
px = new Uint8Array(w * h * 4);
|
||||
gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, px);
|
||||
let banded = 0;
|
||||
for (let i = 0; i < px.length; i += 4)
|
||||
if (0.299 * px[i] + 0.587 * px[i + 1] + 0.114 * px[i + 2] >= 51) banded++;
|
||||
let animeOn = false;
|
||||
for (let i = 0; i < s.postProcessStages.length; i++) {
|
||||
const stage = s.postProcessStages.get(i);
|
||||
if (stage.name === 'godsEyeView_anime')
|
||||
animeOn = stage.enabled && stage.uniforms.intensity > 0.99;
|
||||
}
|
||||
}
|
||||
return {
|
||||
total: px.length / 4,
|
||||
banded,
|
||||
visibility: s.skyBox.milkyWayVisibility,
|
||||
animeOn,
|
||||
};
|
||||
});
|
||||
const tag = !r.animeOn
|
||||
? 'UNREACHED'
|
||||
: r.banded <= 0.005 * r.total
|
||||
? 'PASS'
|
||||
: 'FAIL';
|
||||
console.log(
|
||||
`${tag}: ${r.banded} of ${r.total} sky pixels at or above the first cel band (luma >= 0.2)\n ${JSON.stringify(r)}`,
|
||||
);
|
||||
process.exitCode = tag === 'PASS' ? 0 : tag === 'FAIL' ? 1 : 2;
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
process.exitCode = failed ? 1 : unreached ? 2 : 0;
|
||||
@@ -1,96 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* qa-sky-sun-moon — the custom sky must keep Cesium's Sun and Moon.
|
||||
*
|
||||
* CesiumWidget creates `scene.sun` and `scene.moon` only when the `skyBox`
|
||||
* option is not `false`, so replacing the sky box the wrong way silently
|
||||
* removes the Sun disc and its bloom. Points the camera at the Sun from
|
||||
* 22 000 km and fails unless both bodies exist; saves a screenshot.
|
||||
*
|
||||
* Usage: node scripts/qa-sky-sun-moon.mjs [--url http://localhost:4317] [--out qa-shots/sky-sun.png]
|
||||
*/
|
||||
import puppeteer from 'puppeteer';
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const argv = process.argv;
|
||||
const option = (name, fallback) =>
|
||||
argv.includes(name) ? argv[argv.indexOf(name) + 1] : fallback;
|
||||
const url = option('--url', 'http://localhost:4317');
|
||||
const out = option('--out', 'qa-shots/sky-sun.png');
|
||||
mkdirSync(path.dirname(out), { recursive: true });
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
args: ['--no-sandbox', '--ignore-gpu-blocklist'],
|
||||
});
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1280, height: 800 });
|
||||
await page.evaluateOnNewDocument(() =>
|
||||
localStorage.setItem('gev:first-run-mission:v1', 'suppressed'),
|
||||
);
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForFunction(() => window.__godsEyeView?.viewer?.scene, {
|
||||
timeout: 90_000,
|
||||
});
|
||||
await new Promise((r) => setTimeout(r, 10_000));
|
||||
const info = await page.evaluate(async () => {
|
||||
// The page's own Cesium instance, as Vite served it.
|
||||
const entry = performance
|
||||
.getEntriesByType('resource')
|
||||
.map((e) => e.name)
|
||||
.find((n) => /\.vite\/deps\/cesium\.js/.test(n));
|
||||
const C = await import(entry);
|
||||
const { viewer } = window.__godsEyeView;
|
||||
const scene = viewer.scene;
|
||||
scene.camera.cancelFlight();
|
||||
const time = viewer.clock.currentTime;
|
||||
const sun = C.Cartesian3.normalize(
|
||||
C.Matrix3.multiplyByVector(
|
||||
C.Transforms.computeTemeToPseudoFixedMatrix(time),
|
||||
C.Simon1994PlanetaryPositions.computeSunPositionInEarthInertialFrame(
|
||||
time,
|
||||
),
|
||||
new C.Cartesian3(),
|
||||
),
|
||||
new C.Cartesian3(),
|
||||
);
|
||||
// Off to the side of the Earth, looking straight at the Sun.
|
||||
const side = C.Cartesian3.normalize(
|
||||
C.Cartesian3.cross(sun, C.Cartesian3.UNIT_Z, new C.Cartesian3()),
|
||||
new C.Cartesian3(),
|
||||
);
|
||||
const destination = C.Cartesian3.add(
|
||||
C.Cartesian3.multiplyByScalar(side, 2.2e7, new C.Cartesian3()),
|
||||
C.Cartesian3.multiplyByScalar(sun, -0.6e7, new C.Cartesian3()),
|
||||
new C.Cartesian3(),
|
||||
);
|
||||
const up = C.Cartesian3.normalize(
|
||||
C.Cartesian3.cross(
|
||||
C.Cartesian3.cross(sun, C.Cartesian3.UNIT_Z, new C.Cartesian3()),
|
||||
sun,
|
||||
new C.Cartesian3(),
|
||||
),
|
||||
new C.Cartesian3(),
|
||||
);
|
||||
scene.camera.setView({ destination, orientation: { direction: sun, up } });
|
||||
scene.requestRender();
|
||||
return {
|
||||
sun: Boolean(scene.sun),
|
||||
moon: Boolean(scene.moon),
|
||||
skyBox: scene.skyBox?.constructor?.name,
|
||||
};
|
||||
});
|
||||
await new Promise((r) => setTimeout(r, 4_000));
|
||||
await page.evaluate(() => window.__godsEyeView.viewer.scene.requestRender());
|
||||
await new Promise((r) => setTimeout(r, 1_500));
|
||||
await page.screenshot({ path: out });
|
||||
console.log(JSON.stringify({ url, ...info, screenshot: out }));
|
||||
if (!info.sun || !info.moon) {
|
||||
console.error('FAIL: scene.sun and scene.moon must both exist');
|
||||
process.exitCode = 1;
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
Reference in new issue
Block a user