Files
gods-eye-view/scripts/qa-spatial-tiles.mjs
T

375 lines
10 KiB
JavaScript

/** Synthetic 3D Tiles integration fixture. This is not Google photogrammetry. */
import puppeteer from 'puppeteer';
import fs from 'node:fs/promises';
import sharp from 'sharp';
const out = 'output/spatial-review/tiles';
await fs.mkdir(out, { recursive: true });
// A glTF cube with normals, in Y-up coordinates, packed as a GLB tile.
const faces = [
[
[0, 0, 1],
[
[-25, 0, 25],
[25, 0, 25],
[25, 60, 25],
[-25, 60, 25],
],
],
[
[0, 0, -1],
[
[25, 0, -25],
[-25, 0, -25],
[-25, 60, -25],
[25, 60, -25],
],
],
[
[1, 0, 0],
[
[25, 0, 25],
[25, 0, -25],
[25, 60, -25],
[25, 60, 25],
],
],
[
[-1, 0, 0],
[
[-25, 0, -25],
[-25, 0, 25],
[-25, 60, 25],
[-25, 60, -25],
],
],
[
[0, 1, 0],
[
[-25, 60, 25],
[25, 60, 25],
[25, 60, -25],
[-25, 60, -25],
],
],
[
[0, -1, 0],
[
[-25, 0, -25],
[25, 0, -25],
[25, 0, 25],
[-25, 0, 25],
],
],
];
const positions = [],
normals = [],
indices = [];
for (const [normal, points] of faces) {
const base = positions.length / 3;
positions.push(...points.flat());
normals.push(...Array(4).fill(normal).flat());
indices.push(...[0, 1, 2, 0, 2, 3].map((i) => i + base));
}
const buffers = [
Buffer.from(new Float32Array(positions).buffer),
Buffer.from(new Float32Array(normals).buffer),
Buffer.from(new Uint16Array(indices).buffer),
];
const bin = Buffer.concat(buffers),
gltf = {
asset: { version: '2.0' },
scene: 0,
scenes: [{ nodes: [0] }],
nodes: [{ mesh: 0 }],
meshes: [
{
primitives: [
{ attributes: { POSITION: 0, NORMAL: 1 }, indices: 2, material: 0 },
],
},
],
materials: [
{
pbrMetallicRoughness: {
baseColorFactor: [0.7, 0.7, 0.7, 1],
metallicFactor: 0,
roughnessFactor: 1,
},
doubleSided: true,
},
],
buffers: [{ byteLength: bin.length }],
bufferViews: buffers.map((b, i) => ({
buffer: 0,
byteOffset: buffers.slice(0, i).reduce((n, x) => n + x.length, 0),
byteLength: b.length,
})),
accessors: [
{
bufferView: 0,
componentType: 5126,
count: 24,
type: 'VEC3',
min: [-25, 0, -25],
max: [25, 60, 25],
},
{ bufferView: 1, componentType: 5126, count: 24, type: 'VEC3' },
{ bufferView: 2, componentType: 5123, count: 36, type: 'SCALAR' },
],
};
const json = Buffer.from(
JSON.stringify(gltf).padEnd(
Math.ceil(JSON.stringify(gltf).length / 4) * 4,
' ',
),
);
const header = Buffer.alloc(20);
header.writeUInt32LE(0x46546c67, 0);
header.writeUInt32LE(2, 4);
header.writeUInt32LE(28 + json.length + bin.length, 8);
header.writeUInt32LE(json.length, 12);
header.writeUInt32LE(0x4e4f534a, 16);
const binHeader = Buffer.alloc(8);
binHeader.writeUInt32LE(bin.length, 0);
binHeader.writeUInt32LE(0x004e4942, 4);
const glb = Buffer.concat([header, json, binHeader, bin]).toString('base64');
const browser = await puppeteer.launch({
headless: true,
args: [
'--no-sandbox',
'--use-gl=angle',
'--use-angle=swiftshader',
'--enable-unsafe-swiftshader',
],
});
const page = await browser.newPage(),
errors = [];
page.on('pageerror', (e) => errors.push(e.message));
page.on('console', (m) => {
if (m.type() === 'error' || m.type() === 'warn')
console.log(m.type(), m.text().slice(0, 600));
});
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
try {
await page.setViewport({ width: 1440, height: 900 });
await page.goto(
'http://localhost:4173/?welcome=0#v=2&lat=30.2672&lon=-97.7431&alt=1600&heading=0&pitch=-90&roll=0&style=normal&bloom=0&sharpen=0&bi=0&bv=2&sc=0&map=esri-imagery',
);
await page.waitForFunction(
() =>
window.__godsEyeView?.spatialWorkspace &&
document.querySelector('#loading-screen')?.classList.contains('hidden') &&
window.__godsEyeView.viewer.scene.globe.tilesLoaded,
{ timeout: 60000 },
);
await wait(3000);
const fixture = await page.evaluate(async (glb) => {
const v = window.__godsEyeView.viewer,
C = window.__CESIUM__,
lon = -97.7431,
lat = 30.2672;
const ground =
v.scene.globe.getHeight(C.Cartographic.fromDegrees(lon, lat)) || 0;
const transform = C.Transforms.eastNorthUpToFixedFrame(
C.Cartesian3.fromDegrees(lon, lat, ground),
);
const root = {
asset: { version: '1.1' },
geometricError: 500,
root: {
boundingVolume: { box: [0, 0, 30, 25, 0, 0, 0, 25, 0, 0, 0, 30] },
transform: C.Matrix4.toArray(transform),
geometricError: 0,
refine: 'ADD',
content: { uri: `data:model/gltf-binary;base64,${glb}` },
},
};
const url = URL.createObjectURL(
new Blob([JSON.stringify(root)], { type: 'application/json' }),
);
const tile = await C.Cesium3DTileset.fromUrl(url);
tile.tileFailed.addEventListener((e) =>
console.error('tile failed', e.message),
);
v.scene.primitives.add(tile);
window.__spatialTestTile = tile;
v.camera.lookAt(
C.Cartesian3.fromDegrees(lon, lat, ground + 30),
new C.HeadingPitchRange(0, -0.75, 260),
);
v.camera.lookAtTransform(C.Matrix4.IDENTITY);
const coords = [
[-27, -27],
[27, -27],
[27, 27],
[-27, 27],
[-27, -27],
]
.map(([x, y]) =>
C.Cartographic.fromCartesian(
C.Matrix4.multiplyByPoint(
transform,
new C.Cartesian3(x, y, 0),
new C.Cartesian3(),
),
),
)
.map((c) => ({
lon: C.Math.toDegrees(c.longitude),
lat: C.Math.toDegrees(c.latitude),
}));
const payload = {
elements: [
{
type: 'way',
id: 999999001,
tags: {
building: 'office',
height: '60',
name: 'Synthetic classification fixture',
},
geometry: coords,
},
],
};
const original = window.fetch;
window.fetch = function (input, init) {
return String(input).includes('/api/overpass') &&
String(init?.body).includes('building')
? Promise.resolve(
new Response(JSON.stringify(payload), {
headers: { 'Content-Type': 'application/json' },
}),
)
: original.call(this, input, init);
};
v.scene.requestRender();
return { ground };
}, glb);
await wait(8000);
await page.screenshot({ path: `${out}/01-before.png` });
console.log(
'tile state',
await page.evaluate(() => {
const t = window.__spatialTestTile,
C = window.__CESIUM__;
return {
show: t.show,
center: C.Cartographic.fromCartesian(
t.boundingSphere.center,
).toString(),
radius: t.boundingSphere.radius,
stats: t._statistics,
content: t.root.content?.constructor.name,
};
}),
);
await page.click('.spatial-launch');
await page.click('[data-action="load"]');
await page.waitForFunction(
() => window.__godsEyeView.spatialWorkspace.diagnostics().loaded === 1,
);
await page.click('[data-action="all"]');
await wait(3000);
await page.screenshot({ path: `${out}/02-classified.png` });
const result = await page.evaluate(() => {
const v = window.__godsEyeView.viewer,
C = window.__CESIUM__,
masks = [];
for (let i = 0; i < v.scene.primitives.length; i++) {
const collection = v.scene.primitives.get(i);
if (collection instanceof C.PrimitiveCollection)
for (let j = 0; j < collection.length; j++) {
const p = collection.get(j);
if (p instanceof C.ClassificationPrimitive)
masks.push({ ready: p.ready, show: p.show });
}
}
return {
mode: window.__godsEyeView.spatialWorkspace.diagnostics().mode,
solids: v.dataSources
.getByName('gev-spatial-buildings')[0]
.entities.values.filter((e) => e.polygon).length,
masks,
tileLoaded: window.__spatialTestTile.tilesLoaded,
tileCommands: window.__spatialTestTile._statistics.numberOfCommands,
};
});
const crop = { left: 650, top: 460, width: 120, height: 120 };
const before = (
await sharp(
await sharp(`${out}/01-before.png`).extract(crop).toBuffer(),
).stats()
).channels;
const after = (
await sharp(
await sharp(`${out}/02-classified.png`).extract(crop).toBuffer(),
).stats()
).channels;
const visibleTint =
after[1].mean > before[1].mean + 30 && after[2].mean > before[2].mean + 30;
await page.evaluate(() => {
window.__spatialTestTile.show = false;
window.__godsEyeView.viewer.scene.requestRender();
});
await page.waitForFunction(
() =>
window.__godsEyeView.spatialWorkspace.diagnostics().mode ===
'OSM building volumes',
);
const fallback = await page.evaluate(() => ({
selected: window.__godsEyeView.spatialWorkspace.diagnostics().selected,
solids: window.__godsEyeView.viewer.dataSources
.getByName('gev-spatial-buildings')[0]
.entities.values.filter((e) => e.polygon).length,
}));
await page.evaluate(() => {
window.__spatialTestTile.show = true;
window.__godsEyeView.viewer.scene.requestRender();
});
await page.waitForFunction(
() =>
window.__godsEyeView.spatialWorkspace.diagnostics().mode ===
'Surface tint',
);
const modeSwitch =
fallback.selected === 1 &&
fallback.solids === 1 &&
(await page.evaluate(
() =>
window.__godsEyeView.viewer.dataSources
.getByName('gev-spatial-buildings')[0]
.entities.values.filter((e) => e.polygon).length,
)) === 0;
const passed =
visibleTint &&
modeSwitch &&
result.mode === 'Surface tint' &&
result.solids === 0 &&
result.masks.length === 1 &&
result.masks[0].ready &&
result.tileLoaded &&
result.tileCommands > 0 &&
!errors.length;
await fs.writeFile(
`${out}/report.json`,
JSON.stringify(
{
passed,
fixture,
...result,
visibleTint,
modeSwitch,
errors,
note: 'Synthetic GLB 3D tile integration, not a Google photoreal alignment test.',
},
null,
2,
),
);
console.log(JSON.stringify({ passed, ...result, errors }));
if (!passed) process.exitCode = 1;
} finally {
await browser.close();
}