mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
375 lines
10 KiB
JavaScript
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();
|
|
}
|