mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
Add spatial building conversations and inspectable evidence
This commit is contained in:
1 parent
c9f9896b48
commit
85a187f561
26 files changed
+3837
-7
No files matched your search
@@ -0,0 +1,374 @@
|
||||
/** 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();
|
||||
}
|
||||
Reference in new issue
Block a user