mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
- src/geo drops the ported debug argument checks and the tests that bitExact already covers; package boundaries accept a directory entry, so src/geo/ is listed once per group. - Cesium no longer carries movers and lines nobody draws with: they are optional engine capabilities. The fake engine and the Cesium camera use the shared cameraMath; unused contract tables, clock.ticked, the 'finest' surface detail and Navara click statistics are gone. - ALPR keeps one overlay core and one presentation model for both engines, bikeshare one station bookkeeping, the Navara ground effects one definition helper; image atlases, view centres, mover banks and colours are shared. - The legacy switch route, the clock offset and rate controls and other unused exports are removed. - The acceptance runs the Navara draw, geo import and scope checks; scenarios use the harness switch and page helpers; the main chunk check keeps the size budget only and runs once in CI.
248 lines
8.0 KiB
JavaScript
248 lines
8.0 KiB
JavaScript
// The cockpit weather clouds on Navara: in cockpit mode with
|
|
// the cockpit weather on, Navara's own volumetric clouds take their cover
|
|
// from the weather under the camera (clear: none; overcast: a heavy deck),
|
|
// with no second canvas or WebGL context; leaving the cockpit gives the look
|
|
// its own clouds back. On Cesium the cockpit keeps its own cloud canvas and
|
|
// the engine draws none. The weather route is a fixture, the page clock is
|
|
// shifted to an Austin midday (the look shows clouds only in daylight), and
|
|
// cockpit mode is entered through its event (the cloud controllers listen
|
|
// to it; a real cockpit needs a tracked aircraft). Keyless preview under the
|
|
// strict default-deny network policy.
|
|
//
|
|
// node scripts/qa-navara-cockpit-clouds.mjs http://127.0.0.1:<PORT>/ [--out=DIR]
|
|
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import {
|
|
createStrict,
|
|
localPreview,
|
|
waitForEngine,
|
|
} from './qa-strict-harness.mjs';
|
|
import {
|
|
capture,
|
|
checker,
|
|
diff,
|
|
freeze,
|
|
saveCapture,
|
|
stats,
|
|
visualPageUrl,
|
|
} from './qa/navaraVisual.mjs';
|
|
|
|
const base = localPreview(process.argv[2]);
|
|
const outDir = process.argv
|
|
.find((arg) => arg.startsWith('--out='))
|
|
?.slice('--out='.length);
|
|
const LOW_VIEW =
|
|
'v=2&lat=30.2672&lon=-97.7431&alt=2500&heading=90&pitch=-8&map=osm&l=';
|
|
const MIDDAY_UTC = '2026-07-15T18:00:00Z';
|
|
const WEATHER = {
|
|
clear: { weatherCode: 0, cloudCoverPct: 0, windKph: 6, visibilityM: 20000 },
|
|
overcast: {
|
|
weatherCode: 3,
|
|
cloudCoverPct: 100,
|
|
windKph: 25,
|
|
visibilityM: 15000,
|
|
},
|
|
};
|
|
let weather = WEATHER.clear;
|
|
const asked = [];
|
|
const strict = createStrict({
|
|
origin: base.origin,
|
|
layers: [],
|
|
routes: [
|
|
{
|
|
pattern: `${base.origin}/api/weather-effects*`,
|
|
respond: (request) => {
|
|
asked.push(request.url);
|
|
return {
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ status: 'live', weather }),
|
|
};
|
|
},
|
|
},
|
|
],
|
|
});
|
|
const report = { checks: [] };
|
|
const check = checker(report, assert);
|
|
const save = (name, pixels) => saveCapture(outDir, name, pixels);
|
|
|
|
// Clouds grey the frame: the blue sky and the map's coloured lines go.
|
|
const saturation = (pixels) => stats(pixels).saturation;
|
|
|
|
// A few seconds of continuous frames: the clouds converge over frames.
|
|
async function converged(page, name) {
|
|
await page.evaluate(async () => {
|
|
const { engine } = window.__godsEyeView;
|
|
engine.render.setContinuous(true);
|
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
engine.render.setContinuous(false);
|
|
});
|
|
const pixels = await capture(page);
|
|
save(name, pixels);
|
|
return pixels;
|
|
}
|
|
|
|
const cockpit = (page, active) =>
|
|
page.evaluate((active) => {
|
|
document.body.classList.toggle('cockpit-mode', active);
|
|
window.dispatchEvent(
|
|
new CustomEvent('gev:cockpit-mode-changed', { detail: { active } }),
|
|
);
|
|
}, active);
|
|
const weatherToggle = (page, enabled) =>
|
|
page.evaluate((enabled) => {
|
|
window.dispatchEvent(
|
|
new CustomEvent('gev:cockpit-weather-toggle', { detail: { enabled } }),
|
|
);
|
|
}, enabled);
|
|
const coverage = (page) =>
|
|
page.evaluate(
|
|
() => window.__godsEyeView.engine.render.cloudCover?.().coverage,
|
|
);
|
|
|
|
await strict.run({}, async (browser) => {
|
|
const page = await browser.newPage();
|
|
const errors = [];
|
|
page.on('pageerror', (error) => errors.push(error.message));
|
|
await page.evaluateOnNewDocument((utc) => {
|
|
localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
|
|
const offset = Date.parse(utc) - Date.now();
|
|
const RealDate = Date;
|
|
class ShiftedDate extends RealDate {
|
|
constructor(...args) {
|
|
if (args.length === 0) super(RealDate.now() + offset);
|
|
else super(...args);
|
|
}
|
|
static now() {
|
|
return RealDate.now() + offset;
|
|
}
|
|
}
|
|
globalThis.Date = ShiftedDate;
|
|
}, MIDDAY_UTC);
|
|
await page.setViewport({ width: 1280, height: 800 });
|
|
await page.goto(visualPageUrl(base.origin, 'navara', '', LOW_VIEW), {
|
|
waitUntil: 'domcontentloaded',
|
|
});
|
|
await waitForEngine(page, 'navara');
|
|
await freeze(page);
|
|
|
|
const setup = await page.evaluate(() => {
|
|
const a = window.__godsEyeView;
|
|
return {
|
|
cover: a.engine.render.cloudCover?.() ?? null,
|
|
canvas: Boolean(document.getElementById('cockpit-cloud-effects')),
|
|
canvases: document.querySelectorAll('canvas').length,
|
|
};
|
|
});
|
|
check(
|
|
'Navara takes the cockpit cloud cover itself, no cloud canvas',
|
|
setup.cover?.coverage === null && !setup.canvas,
|
|
setup,
|
|
);
|
|
const lookOwn = await converged(page, 'look-own');
|
|
|
|
await weatherToggle(page, true);
|
|
await cockpit(page, true);
|
|
await page.waitForFunction(
|
|
() => window.__godsEyeView.engine.render.cloudCover().coverage === 0,
|
|
{ timeout: 10000, polling: 100 },
|
|
);
|
|
check('clear weather in the cockpit: no cloud cover', asked.length >= 1, {
|
|
requests: asked.length,
|
|
});
|
|
const clear = await converged(page, 'cockpit-clear');
|
|
|
|
// Overcast 30 km east: the movement makes the weather stale.
|
|
weather = WEATHER.overcast;
|
|
await page.evaluate(() => {
|
|
const { engine } = window.__godsEyeView;
|
|
const place = engine.camera.pose().cartographic;
|
|
const east = place.longitude + 30000 / (6378137 * Math.cos(place.latitude));
|
|
const { sin, cos } = Math;
|
|
// WGS84 Earth-fixed position of (east, latitude, height).
|
|
const a = 6378137;
|
|
const e2 = 0.00669437999014;
|
|
const n = a / Math.sqrt(1 - e2 * sin(place.latitude) ** 2);
|
|
const h = place.height;
|
|
engine.camera.setView({
|
|
destination: {
|
|
x: (n + h) * cos(place.latitude) * cos(east),
|
|
y: (n + h) * cos(place.latitude) * sin(east),
|
|
z: (n * (1 - e2) + h) * sin(place.latitude),
|
|
},
|
|
orientation: { heading: Math.PI / 2, pitch: -0.14, roll: 0 },
|
|
});
|
|
});
|
|
await page.waitForFunction(
|
|
() => window.__godsEyeView.engine.render.cloudCover().coverage > 0.4,
|
|
{ timeout: 15000, polling: 100 },
|
|
);
|
|
const overcastCover = await coverage(page);
|
|
const overcast = await converged(page, 'cockpit-overcast');
|
|
const greyed = {
|
|
lookOwn: saturation(lookOwn),
|
|
clear: saturation(clear),
|
|
overcast: saturation(overcast),
|
|
clearToOvercast: diff(clear, overcast).mean,
|
|
};
|
|
check(
|
|
'overcast weather covers the view with Navara clouds, clear leaves it open',
|
|
overcastCover > 0.4 &&
|
|
greyed.overcast < greyed.clear * 0.6 &&
|
|
greyed.lookOwn < greyed.clear &&
|
|
greyed.clearToOvercast > 15,
|
|
{ overcastCover, greyed, requests: asked.length },
|
|
);
|
|
const contexts = await page.evaluate(() => ({
|
|
canvas: Boolean(document.getElementById('cockpit-cloud-effects')),
|
|
canvases: document.querySelectorAll('canvas').length,
|
|
}));
|
|
check(
|
|
'no second canvas or WebGL context in the cockpit',
|
|
!contexts.canvas && contexts.canvases === setup.canvases,
|
|
{ before: setup.canvases, after: contexts },
|
|
);
|
|
|
|
await cockpit(page, false);
|
|
check(
|
|
'leaving the cockpit gives the look its own clouds back',
|
|
(await coverage(page)) === null,
|
|
);
|
|
|
|
// Cesium keeps its own cockpit cloud canvas; its engine draws none.
|
|
await strict.switchTo(page, 'cesium');
|
|
await freeze(page);
|
|
weather = WEATHER.overcast;
|
|
await cockpit(page, true);
|
|
await page.waitForFunction(
|
|
() =>
|
|
Number(
|
|
document.getElementById('cockpit-cloud-effects')?.dataset.cloudStrength,
|
|
) > 0.3,
|
|
{ timeout: 15000, polling: 100 },
|
|
);
|
|
const cesium = await page.evaluate(() => {
|
|
const canvas = document.getElementById('cockpit-cloud-effects');
|
|
return {
|
|
setCloudCover: typeof window.__godsEyeView.engine.render.setCloudCover,
|
|
strength: Number(canvas.dataset.cloudStrength),
|
|
cockpit: canvas.dataset.cockpit,
|
|
};
|
|
});
|
|
check(
|
|
'Cesium: the cockpit draws its own cloud canvas, the engine none',
|
|
cesium.setCloudCover === 'undefined' && cesium.cockpit === 'true',
|
|
cesium,
|
|
);
|
|
await cockpit(page, false);
|
|
await weatherToggle(page, false);
|
|
|
|
check('no page errors', errors.length === 0, errors);
|
|
if (outDir)
|
|
fs.writeFileSync(
|
|
path.join(outDir, 'cockpit-clouds.json'),
|
|
JSON.stringify(report, null, 2),
|
|
);
|
|
});
|