mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-11 13:52:19 +00:00
Merge branch 'german/refactor-e6-tiles-heights' into german/navara-refactor
This commit is contained in:
commit
560b72c3f7
10 files changed
+430
-97
No files matched your search
@@ -23,6 +23,8 @@ import {
|
||||
fixtureRoutes,
|
||||
observeSurfaceMovers,
|
||||
settledPositions,
|
||||
assertTileHeight,
|
||||
waitForSurfaceHeights,
|
||||
} from './qa/surfaceHeights.mjs';
|
||||
|
||||
export function compareHeights(before, after, { tiles = false } = {}) {
|
||||
@@ -82,7 +84,7 @@ export function compareHeights(before, after, { tiles = false } = {}) {
|
||||
!missing.length &&
|
||||
!extra.length &&
|
||||
!invalid.length &&
|
||||
maxAbsM <= 0.1 + 1e-9,
|
||||
maxAbsM <= (tiles ? 0.5 : 0.1) + 1e-9,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -237,32 +239,7 @@ async function main(argv) {
|
||||
},
|
||||
);
|
||||
await whenSettled(page);
|
||||
await page.waitForFunction(
|
||||
({ points, tiles }) => {
|
||||
const { engine } = window.__godsEyeView;
|
||||
engine.render.request('qa-surface-heights');
|
||||
return (
|
||||
engine.surface.tilesLoaded() &&
|
||||
engine.maps.photorealActive() === tiles &&
|
||||
points.every((p) => {
|
||||
const place = {
|
||||
longitude: (p.lon * Math.PI) / 180,
|
||||
latitude: (p.lat * Math.PI) / 180,
|
||||
height: 0,
|
||||
};
|
||||
const height = tiles
|
||||
? engine.surface.sampleHeight(place)
|
||||
: engine.surface.terrainHeight(place);
|
||||
return (
|
||||
Number.isFinite(height) &&
|
||||
(tiles ? Math.abs(height - 50) < 0.1 : height > 1)
|
||||
);
|
||||
})
|
||||
);
|
||||
},
|
||||
{ timeout: 90000 },
|
||||
{ points, tiles },
|
||||
);
|
||||
await waitForSurfaceHeights(page, kind, tiles);
|
||||
await page.evaluate(observeSurfaceMovers);
|
||||
snapshot.engines[kind] = {};
|
||||
for (const layer of layers) {
|
||||
@@ -285,9 +262,10 @@ async function main(argv) {
|
||||
snapshot.engines[kind][layer] = values;
|
||||
if (tiles)
|
||||
for (const [id, p] of Object.entries(values))
|
||||
assert.ok(
|
||||
Math.abs(p.heightM - (50 + offsets[layer])) <= 0.1,
|
||||
`${kind}/${layer}/${id}: ${p.heightM} m, expected ${50 + offsets[layer]} m`,
|
||||
assertTileHeight(
|
||||
p.heightM,
|
||||
50 + offsets[layer],
|
||||
`${kind}/${layer}/${id}`,
|
||||
);
|
||||
await strict.passed(
|
||||
page,
|
||||
@@ -296,7 +274,7 @@ async function main(argv) {
|
||||
values,
|
||||
);
|
||||
console.log(
|
||||
`PASS ${kind}/${layer}: ${rows.length} points${tiles ? ` at ${50 + offsets[layer]} m (±0.1 m)` : ''}`,
|
||||
`PASS ${kind}/${layer}: ${rows.length} points${tiles ? ` at ${50 + offsets[layer]} m (±0.5 m)` : ''}`,
|
||||
);
|
||||
await unloadDisplay(page, layer);
|
||||
}
|
||||
@@ -316,7 +294,7 @@ async function main(argv) {
|
||||
);
|
||||
assert.ok(
|
||||
comparison.ok,
|
||||
'Surface heights differ by more than 0.1 m or the point sets changed',
|
||||
`Surface heights differ by more than ${tiles ? 0.5 : 0.1} m or the point sets changed`,
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
|
||||
@@ -6,6 +6,9 @@ import {
|
||||
observeSurfaceMovers,
|
||||
fixtureRoutes,
|
||||
center,
|
||||
points,
|
||||
assertTileHeight,
|
||||
waitForSurfaceHeights,
|
||||
} from './qa/surfaceHeights.mjs';
|
||||
import { createFakeEngine } from '../src/globe/testing/fakeEngine.js';
|
||||
import { decodeAlprTile } from '../src/layers/alpr/tileRecords.js';
|
||||
@@ -94,6 +97,76 @@ test('tiles require explicit opt-in and cannot be compared with terrain', () =>
|
||||
assert.equal(compareHeights(tiles, tiles, { tiles: true }).ok, true);
|
||||
});
|
||||
|
||||
test('tile heights and tile comparisons use 0.5 m while terrain keeps 0.1 m', () => {
|
||||
const before = { ...snapshot(), surface: 'tiles' };
|
||||
for (const delta of [0.118, 0.5, -0.5]) {
|
||||
assertTileHeight(50 + delta, 50, 'navara/surface-0');
|
||||
const after = { ...snapshot(50 + delta), surface: 'tiles' };
|
||||
assert.equal(compareHeights(before, after, { tiles: true }).ok, true);
|
||||
assert.equal(compareHeights(snapshot(), snapshot(50 + delta)).ok, false);
|
||||
}
|
||||
for (const height of [50.5001, 49.4999, null, NaN, Infinity])
|
||||
assert.throws(
|
||||
() => assertTileHeight(height, 50, 'navara/surface-1'),
|
||||
/navara\/surface-1: .* m, expected 50 m \(±0.5 m\)/,
|
||||
);
|
||||
const after = { ...snapshot(50.5001), surface: 'tiles' };
|
||||
assert.equal(compareHeights(before, after, { tiles: true }).ok, false);
|
||||
});
|
||||
|
||||
test('loaded incorrect tile heights fail immediately with the point and measured height', async (t) => {
|
||||
const previous = globalThis.window;
|
||||
t.after(() => {
|
||||
if (previous === undefined) delete globalThis.window;
|
||||
else globalThis.window = previous;
|
||||
});
|
||||
const engine = createFakeEngine();
|
||||
t.after(() => engine.destroy());
|
||||
globalThis.window = { __godsEyeView: { engine } };
|
||||
engine.maps.photorealActive = () => true;
|
||||
let height = 50.118;
|
||||
engine.surface.sampleHeight = () => height;
|
||||
const dispose = t.mock.fn();
|
||||
const page = {
|
||||
async waitForFunction(fn, options, args) {
|
||||
const state = fn(args);
|
||||
assert.ok(
|
||||
state,
|
||||
'finite loaded values return without waiting for tolerance',
|
||||
);
|
||||
return { jsonValue: async () => state, dispose };
|
||||
},
|
||||
};
|
||||
await waitForSurfaceHeights(page, 'navara', true);
|
||||
height = 51.25;
|
||||
await assert.rejects(
|
||||
waitForSurfaceHeights(page, 'navara', true),
|
||||
/navara\/surface-0 sampleHeight: 51.25 m, expected 50 m/,
|
||||
);
|
||||
assert.equal(dispose.mock.callCount(), 2);
|
||||
});
|
||||
|
||||
test('unavailable surface timeout reports all points and readiness state', async () => {
|
||||
const cause = new Error('timeout');
|
||||
const page = {
|
||||
waitForFunction: async () => {
|
||||
throw cause;
|
||||
},
|
||||
evaluate: async () => ({
|
||||
loaded: false,
|
||||
photoreal: true,
|
||||
points: points.map((p) => ({ id: p.id, height: null })),
|
||||
}),
|
||||
};
|
||||
await assert.rejects(waitForSurfaceHeights(page, 'navara', true), (error) => {
|
||||
assert.equal(error.cause, cause);
|
||||
for (const p of points)
|
||||
assert.ok(error.message.includes(`${p.id}: null m`));
|
||||
assert.match(error.message, /loaded=false, photoreal=true/);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
test('the probe reads committed contract positions, snapshots mutable inputs and drops destroyed sets', () => {
|
||||
const engine = createFakeEngine();
|
||||
const original = globalThis.window;
|
||||
|
||||
+120
-58
@@ -13,10 +13,11 @@ import {
|
||||
} from './qa-strict-harness.mjs';
|
||||
|
||||
const base = localPreview(process.argv[2]);
|
||||
const report = process.argv.includes('--report');
|
||||
const { extras: fixture } = JSON.parse(
|
||||
readFileSync(new URL('./fixtures/tiles3d/tileset.json', import.meta.url)),
|
||||
);
|
||||
base.search = `?engine=navara&welcome=0&qaTileset=${encodeURIComponent(TILES_FIXTURE_PATH)}`;
|
||||
base.search = `?engine=cesium&welcome=0&qaTileset=${encodeURIComponent(TILES_FIXTURE_PATH)}`;
|
||||
base.hash = `v=2&lat=${fixture.origin.lat}&lon=${fixture.origin.lon}&alt=1100&heading=0&pitch=-90&roll=0&map=photoreal&l=`;
|
||||
let releaseTiles;
|
||||
let pendingTiles;
|
||||
@@ -51,8 +52,8 @@ await strict.run({}, async (browser) => {
|
||||
await page.setCacheEnabled(false);
|
||||
let readsBefore = { ...strict.ledger.routes };
|
||||
await page.goto(base.href, { waitUntil: 'domcontentloaded' });
|
||||
for (const kind of ['navara', 'cesium']) {
|
||||
if (kind === 'cesium') {
|
||||
for (const kind of ['cesium', 'navara']) {
|
||||
if (kind === 'navara') {
|
||||
readsBefore = { ...strict.ledger.routes };
|
||||
holdTiles();
|
||||
await strict.pick(page, kind, { settle: false });
|
||||
@@ -95,60 +96,93 @@ await strict.run({}, async (browser) => {
|
||||
);
|
||||
await loaded(page);
|
||||
await waitForCameraAtRest(page);
|
||||
for (const route of routes.slice(0, 5))
|
||||
assert.ok(
|
||||
strict.ledger.routes[route.pattern] > (readsBefore[route.pattern] ?? 0),
|
||||
`${kind} intercepted ${new URL(route.pattern).pathname}`,
|
||||
);
|
||||
if (!report)
|
||||
for (const route of routes.slice(0, 5))
|
||||
assert.ok(
|
||||
strict.ledger.routes[route.pattern] >
|
||||
(readsBefore[route.pattern] ?? 0),
|
||||
`${kind} intercepted ${new URL(route.pattern).pathname}`,
|
||||
);
|
||||
await strict.passed(
|
||||
page,
|
||||
'osm-places',
|
||||
'tilesLoaded becomes true; all five files served by same-origin routes',
|
||||
report
|
||||
? 'fixture tiles ready for measurement'
|
||||
: 'tilesLoaded becomes true; all five files served by same-origin routes',
|
||||
);
|
||||
|
||||
const heights = await page.evaluate(
|
||||
({ probes, clampStart }) => {
|
||||
const readings = await page.evaluate(
|
||||
({ probes, clampStarts }) => {
|
||||
const { surface } = window.__godsEyeView.engine;
|
||||
return {
|
||||
sampled: Object.fromEntries(
|
||||
Object.entries(probes).map(([id, p]) => [
|
||||
id,
|
||||
surface.sampleHeight(p.cartographic),
|
||||
]),
|
||||
),
|
||||
clamped: surface.clampToHeight(clampStart),
|
||||
};
|
||||
return Object.fromEntries(
|
||||
Object.entries(probes).map(([id, p]) => [
|
||||
id,
|
||||
{
|
||||
sampled: surface.sampleHeight(p.cartographic),
|
||||
clamped: surface.clampToHeight(clampStarts[id]),
|
||||
},
|
||||
]),
|
||||
);
|
||||
},
|
||||
{
|
||||
probes: fixture.probes,
|
||||
clampStart: geo.cartesian3.fromDegrees(
|
||||
fixture.probes.roof.lon,
|
||||
fixture.probes.roof.lat,
|
||||
fixture.probes.roof.height + 300,
|
||||
clampStarts: Object.fromEntries(
|
||||
Object.entries(fixture.probes).map(([id, p]) => [
|
||||
id,
|
||||
geo.cartesian3.fromDegrees(p.lon, p.lat, p.height + 300),
|
||||
]),
|
||||
),
|
||||
},
|
||||
);
|
||||
for (const [id, p] of Object.entries(fixture.probes))
|
||||
assert.ok(
|
||||
Math.abs(heights.sampled[id] - p.height) <= 0.5,
|
||||
`${kind} ${id}: ${JSON.stringify(heights)}`,
|
||||
const heights = Object.fromEntries(
|
||||
Object.entries(readings).map(([id, reading]) => {
|
||||
const p = fixture.probes[id];
|
||||
return [
|
||||
id,
|
||||
{
|
||||
expectedHeightM: p.height,
|
||||
sampledHeightM: reading.sampled ?? null,
|
||||
clampedHeightM: reading.clamped
|
||||
? geo.cartographic.fromCartesian(reading.clamped).height
|
||||
: null,
|
||||
clampDistanceM: reading.clamped
|
||||
? geo.cartesian3.distance(reading.clamped, p.position)
|
||||
: null,
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
console.log(
|
||||
`${report ? 'REPORT' : 'HEIGHTS'} ${kind} ${JSON.stringify(heights)}`,
|
||||
);
|
||||
if (kind === 'navara')
|
||||
console.log(
|
||||
'NOTE navara: the default 3D Tiles height offset is a known package limitation; the adapter sets model.height=0 (height tolerance ±0.5 m).',
|
||||
);
|
||||
assert.ok(
|
||||
Math.abs(heights.sampled.block50 - heights.sampled.plate - 50) <= 0.5,
|
||||
);
|
||||
assert.ok(
|
||||
heights.clamped &&
|
||||
geo.cartesian3.distance(
|
||||
heights.clamped,
|
||||
fixture.probes.roof.position,
|
||||
if (!report) {
|
||||
for (const [id, p] of Object.entries(heights)) {
|
||||
assert.ok(
|
||||
Number.isFinite(p.sampledHeightM) &&
|
||||
Math.abs(p.sampledHeightM - p.expectedHeightM) <= 0.5,
|
||||
`${kind} ${id}: ${JSON.stringify(p)}`,
|
||||
);
|
||||
assert.ok(
|
||||
Number.isFinite(p.clampDistanceM) && p.clampDistanceM <= 0.5,
|
||||
`${kind} ${id} clamp: ${JSON.stringify(p)}`,
|
||||
);
|
||||
}
|
||||
assert.ok(
|
||||
Math.abs(
|
||||
heights.block50.sampledHeightM - heights.plate.sampledHeightM - 50,
|
||||
) <= 0.5,
|
||||
);
|
||||
await strict.passed(
|
||||
page,
|
||||
'osm-places',
|
||||
'known plate, 50 m, 200 m and sloping roof heights; clamp lands on roof',
|
||||
heights,
|
||||
);
|
||||
);
|
||||
await strict.passed(
|
||||
page,
|
||||
'osm-places',
|
||||
'known plate, 50 m, 200 m and sloping roof heights; clamps land on each surface',
|
||||
heights,
|
||||
);
|
||||
}
|
||||
|
||||
const drapes = [];
|
||||
for (const surface of ['tiles', 'terrain']) {
|
||||
@@ -159,6 +193,7 @@ await strict.run({}, async (browser) => {
|
||||
await engine.maps.setTerrain({ kind: 'ellipsoid' });
|
||||
});
|
||||
await loaded(page);
|
||||
await waitForCameraAtRest(page);
|
||||
}
|
||||
const p = fixture.probes.block50;
|
||||
const position = geo.cartesian3.fromDegrees(
|
||||
@@ -166,7 +201,6 @@ await strict.run({}, async (browser) => {
|
||||
p.lat,
|
||||
surface === 'tiles' ? p.height : 0,
|
||||
);
|
||||
const baseline = await pixels(page, position);
|
||||
for (const drape of ['tiles', 'terrain']) {
|
||||
await page.evaluate(
|
||||
({ lon, lat, drape }) => {
|
||||
@@ -199,19 +233,41 @@ await strict.run({}, async (browser) => {
|
||||
},
|
||||
{ timeout: 30000 },
|
||||
);
|
||||
// Build first, then compare the same ready primitive hidden/shown.
|
||||
// Map imagery and classification shaders may still be arriving before ready.
|
||||
await page.evaluate(() => window.qaDrape.show(false));
|
||||
await loaded(page);
|
||||
const baseline = await pixels(page, position);
|
||||
await page.evaluate(() => window.qaDrape.show(true));
|
||||
const rgb = await pixels(page, position);
|
||||
await page.evaluate(() => window.qaDrape.show(false));
|
||||
const restored = await pixels(page, position);
|
||||
const drawn = rgb[0] > 180 && rgb[1] < 80 && rgb[2] > 180;
|
||||
assert.equal(
|
||||
drawn,
|
||||
surface === drape,
|
||||
`${kind} ${drape} on ${surface}: ${rgb}`,
|
||||
const sample = { surface, drape, baseline, rgb, restored, drawn };
|
||||
drapes.push(sample);
|
||||
console.log(
|
||||
`${report ? 'REPORT' : 'DRAPE'} ${kind} ${JSON.stringify(sample)}`,
|
||||
);
|
||||
if (surface !== drape)
|
||||
if (!report) {
|
||||
assert.ok(
|
||||
rgb.every((v, i) => Math.abs(v - baseline[i]) < 15),
|
||||
'other surface stays unchanged',
|
||||
!(baseline[0] > 180 && baseline[1] < 80 && baseline[2] > 180),
|
||||
`${kind} hidden drape is already magenta: ${JSON.stringify(sample)}`,
|
||||
);
|
||||
drapes.push({ surface, drape, baseline, rgb });
|
||||
assert.ok(
|
||||
baseline.every((v, i) => Math.abs(v - restored[i]) < 15),
|
||||
`${kind} unstable drape baseline: ${JSON.stringify(sample)}`,
|
||||
);
|
||||
assert.equal(
|
||||
drawn,
|
||||
surface === drape,
|
||||
`${kind} ${drape} on ${surface}: ${JSON.stringify(sample)}`,
|
||||
);
|
||||
if (surface !== drape)
|
||||
assert.ok(
|
||||
rgb.every((v, i) => Math.abs(v - baseline[i]) < 15),
|
||||
`${kind} ${drape} on ${surface} changes the other surface: ${JSON.stringify(sample)}`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await page.evaluate(() => {
|
||||
window.qaDrape.destroy();
|
||||
@@ -223,21 +279,26 @@ await strict.run({}, async (browser) => {
|
||||
await strict.passed(
|
||||
page,
|
||||
'osm-places',
|
||||
'drape tiles and terrain draw only on their respective surface (pixels)',
|
||||
drapes,
|
||||
report
|
||||
? 'diagnostic height and drape measurements collected; values not asserted'
|
||||
: 'drape tiles and terrain draw only on their respective surface (pixels)',
|
||||
{ heights, drapes },
|
||||
);
|
||||
await page.evaluate(() =>
|
||||
window.__godsEyeView.mapStackController.setStack('photoreal'),
|
||||
);
|
||||
await loaded(page);
|
||||
await waitForCameraAtRest(page);
|
||||
await checkPoints(page, kind);
|
||||
if (!report) await checkPoints(page, kind);
|
||||
await assertKeylessPreview(page);
|
||||
console.log(
|
||||
`PASS ${kind} fixture heights, clamp, drapes, tilesLoaded and surface-points budget`,
|
||||
report
|
||||
? `REPORT ${kind} complete (measurements only)`
|
||||
: `PASS ${kind} fixture heights, clamp, drapes, tilesLoaded and surface-points budget`,
|
||||
);
|
||||
}
|
||||
assert.deepEqual(errors, []);
|
||||
if (report) console.log(`REPORT browser errors ${JSON.stringify(errors)}`);
|
||||
else assert.deepEqual(errors, []);
|
||||
assert.deepEqual(
|
||||
Object.keys(strict.ledger.external).filter((host) =>
|
||||
/google|gstatic|cesium\.com/i.test(host),
|
||||
@@ -270,6 +331,7 @@ async function pixels(page, position) {
|
||||
off();
|
||||
const point = engine.projector.worldToWindow(position);
|
||||
const scale = engine.host.canvas.width / engine.host.size().width;
|
||||
context.clearRect(0, 0, 5, 5);
|
||||
context.drawImage(
|
||||
engine.host.canvas,
|
||||
Math.round(point.x * scale) - 2,
|
||||
@@ -384,7 +446,7 @@ async function checkPoints(page, kind) {
|
||||
engine.movers.createSet = createSet;
|
||||
target[method] = render;
|
||||
};
|
||||
layer.attachRenderer(viewer, { engine });
|
||||
layer.attachRenderer({ engine });
|
||||
layer.setResults(
|
||||
points.map((p, i) => ({ id: String(i), lon: p.lon, lat: p.lat })),
|
||||
{ kind: 'fixture', label: 'Roof points', area: 'Austin' },
|
||||
|
||||
@@ -2,14 +2,17 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { Cesium3DTileset } from 'cesium';
|
||||
import { Core, initSync } from '@navaramap/engine';
|
||||
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||
import { Raycaster, Vector3 } from 'three';
|
||||
import { Group, Raycaster, Vector3 } from 'three';
|
||||
import * as geo from '../src/geo/index.js';
|
||||
import { buildTilesFixture } from './build-tiles3d-fixture.mjs';
|
||||
import { localTilesetPath, qaTilesetUrl } from '../src/maps/qaTileset.js';
|
||||
import { createDefaultMapSources } from '../src/maps/defaultSources.js';
|
||||
import { MapSourceController } from '../src/maps/controller.js';
|
||||
import { createFakeEngine } from '../src/globe/testing/fakeEngine.js';
|
||||
import { createNavaraMaps } from '../src/globe/navara/maps.js';
|
||||
import { createNavaraSurface } from '../src/globe/navara/surface.js';
|
||||
import {
|
||||
assertKeylessPreview,
|
||||
TILES_FIXTURE_PATH,
|
||||
@@ -78,6 +81,153 @@ test('independent glTF loader and triangle rays recover the authored heights', a
|
||||
}
|
||||
});
|
||||
|
||||
// Exercise the installed WASM tile loader, then the same glTF transforms
|
||||
// consumed by Three's renderModel/setTransform path, without WebGL or fetch.
|
||||
async function loadedNavaraTiles(t, adapter) {
|
||||
initSync({
|
||||
module: readFileSync(
|
||||
new URL(
|
||||
'../node_modules/@navaramap/engine/navara_wasm_bg.wasm',
|
||||
import.meta.url,
|
||||
),
|
||||
),
|
||||
});
|
||||
const files = buildTilesFixture();
|
||||
const fixture = JSON.parse(files.get('tileset.json'));
|
||||
const core = new Core('tiles-fixture');
|
||||
t.after(() => core.free());
|
||||
core.resize(1280, 800, 1);
|
||||
core.setFrustum(60, 1, 1e8);
|
||||
core.start();
|
||||
core.changeCamera(
|
||||
new Float64Array([
|
||||
fixture.extras.origin.lon,
|
||||
fixture.extras.origin.lat,
|
||||
1100,
|
||||
]),
|
||||
-90,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
const view = {
|
||||
addSource: (desc) => {
|
||||
const id = core.addSource(desc);
|
||||
return { id, delete: () => core.deleteSource(id) };
|
||||
},
|
||||
addLayer: (desc) => {
|
||||
const id = core.addLayer({ ...desc, source: desc.source.id });
|
||||
return { id, delete: () => core.deleteLayer(id) };
|
||||
},
|
||||
forceUpdate() {},
|
||||
};
|
||||
if (adapter) {
|
||||
globals(t, { location: new URL('http://localhost:4303') });
|
||||
const owner = createNavaraMaps(view, {
|
||||
setInterval: () => 1,
|
||||
clearInterval() {},
|
||||
});
|
||||
await owner.maps.setPhotorealTiles({ tilesetUrl: TILES_FIXTURE_PATH });
|
||||
} else {
|
||||
const source = view.addSource({
|
||||
type: '3d-tiles',
|
||||
url: 'http://localhost:4303/tileset.json',
|
||||
});
|
||||
view.addLayer({ type: '3d-tiles', source, model: { maxSse: 16 } });
|
||||
}
|
||||
const roots = [];
|
||||
for (let frame = 0; frame < 60 && roots.length < 4; frame++) {
|
||||
core.update(frame * 16);
|
||||
const events = core.readEvents();
|
||||
if (!events) continue;
|
||||
for (const request of events.take_data_requested()) {
|
||||
const bytes = files.get(new URL(request.url).pathname.split('/').at(-1));
|
||||
assert.ok(bytes, `unexpected fixture request: ${request.url}`);
|
||||
core.setBufferU8(request.handle, request.bits, bytes.length, (buffer) =>
|
||||
buffer.set(bytes),
|
||||
);
|
||||
}
|
||||
for (const event of events.take_renderable_feature_added()) {
|
||||
const model = event.feature.model;
|
||||
if (!model) continue;
|
||||
const bytes = core.getBufferU8(model.bin).slice();
|
||||
const gltf = await new GLTFLoader().parseAsync(bytes.buffer, '');
|
||||
const root = new Group();
|
||||
const m = model.transform;
|
||||
root.position.set(m.tx, m.ty, m.tz);
|
||||
root.quaternion.set(m.qx, m.qy, m.qz, m.qw);
|
||||
root.scale.set(m.sx, m.sy, m.sz);
|
||||
root.add(gltf.scene);
|
||||
root.updateMatrixWorld(true);
|
||||
roots.push(root);
|
||||
core.markFeatureIsRendered('model', event.bits);
|
||||
}
|
||||
}
|
||||
assert.equal(roots.length, 4, 'all four real tile contents loaded');
|
||||
const { surface } = createNavaraSurface(
|
||||
{ sampleTerrainHeight: () => undefined },
|
||||
{ tileRoots: () => roots, isIdle: () => true, retainTileGeometry() {} },
|
||||
);
|
||||
t.after(() => {
|
||||
for (const root of roots)
|
||||
root.traverse((object) => {
|
||||
object.geometry?.dispose();
|
||||
object.material?.dispose();
|
||||
});
|
||||
});
|
||||
return { fixture, roots, surface };
|
||||
}
|
||||
|
||||
test('Navara default tile height shifts the rendered transform by one ECEF metre', async (t) => {
|
||||
const { fixture, roots, surface } = await loadedNavaraTiles(t, false);
|
||||
for (const root of roots) {
|
||||
assert.equal(root.position.x, fixture.root.transform[12] - 1);
|
||||
assert.equal(root.position.y, fixture.root.transform[13]);
|
||||
assert.equal(root.position.z, fixture.root.transform[14]);
|
||||
}
|
||||
for (const [id, expected] of Object.entries({
|
||||
plate: 0.11864,
|
||||
block50: 50.11837,
|
||||
block200: 200.11864,
|
||||
roof: 83.32621,
|
||||
}))
|
||||
assert.ok(
|
||||
Math.abs(
|
||||
surface.sampleHeight(fixture.extras.probes[id].cartographic) - expected,
|
||||
) < 0.001,
|
||||
id,
|
||||
);
|
||||
});
|
||||
|
||||
test('Navara map source preserves authored rendered vertices, sampled heights and clamps', async (t) => {
|
||||
const { fixture, roots, surface } = await loadedNavaraTiles(t, true);
|
||||
for (const root of roots)
|
||||
assert.deepEqual(
|
||||
root.position.toArray(),
|
||||
fixture.root.transform.slice(12, 15),
|
||||
);
|
||||
for (const p of [
|
||||
...Object.values(fixture.extras.probes),
|
||||
...fixture.extras.points,
|
||||
]) {
|
||||
const normal = geo.ellipsoid.WGS84.geodeticSurfaceNormalCartographic(
|
||||
p.cartographic,
|
||||
{},
|
||||
);
|
||||
const origin = geo.cartesian3.fromDegrees(p.lon, p.lat, p.height + 300);
|
||||
const ray = new Raycaster(
|
||||
new Vector3(origin.x, origin.y, origin.z),
|
||||
new Vector3(-normal.x, -normal.y, -normal.z),
|
||||
);
|
||||
const hit = ray.intersectObjects(roots, true)[0];
|
||||
assert.ok(hit, 'rendered mesh is under the authored probe');
|
||||
assert.ok(geo.cartesian3.distance(hit.point, p.position) < 1e-6);
|
||||
assert.ok(Math.abs(surface.sampleHeight(p.cartographic) - p.height) < 1e-6);
|
||||
assert.ok(
|
||||
geo.cartesian3.distance(surface.clampToHeight(origin), p.position) < 1e-6,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('QA URL switch is inert by default and rejects nonlocal or malformed paths', (t) => {
|
||||
const location = new URL('http://localhost:4303/');
|
||||
globals(t, { location });
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { cameraTile } from '../fixtures/layers/alpr.mjs';
|
||||
import { tilesFixtureRoutes } from '../qa-strict-harness.mjs';
|
||||
@@ -36,6 +37,64 @@ export const counts = {
|
||||
story: 1,
|
||||
};
|
||||
|
||||
export function assertTileHeight(height, expected, label) {
|
||||
assert.ok(
|
||||
Number.isFinite(height) && Math.abs(height - expected) <= 0.5 + 1e-9,
|
||||
`${label}: ${height} m, expected ${expected} m (±0.5 m)`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function waitForSurfaceHeights(page, kind, tiles) {
|
||||
const handle = await page
|
||||
.waitForFunction(
|
||||
({ points, tiles }) => {
|
||||
const { engine } = window.__godsEyeView;
|
||||
engine.render.request('qa-surface-heights');
|
||||
const state = {
|
||||
loaded: engine.surface.tilesLoaded(),
|
||||
photoreal: engine.maps.photorealActive(),
|
||||
points: points.map((p) => {
|
||||
const place = {
|
||||
longitude: (p.lon * Math.PI) / 180,
|
||||
latitude: (p.lat * Math.PI) / 180,
|
||||
height: 0,
|
||||
};
|
||||
const height = tiles
|
||||
? engine.surface.sampleHeight(place)
|
||||
: engine.surface.terrainHeight(place);
|
||||
return { id: p.id, height: height ?? null };
|
||||
}),
|
||||
};
|
||||
window.qaSurfaceHeights = state;
|
||||
// Readiness must not wait for an incorrect height to become correct.
|
||||
return state.loaded &&
|
||||
state.photoreal === tiles &&
|
||||
state.points.every(
|
||||
(p) => Number.isFinite(p.height) && (tiles || p.height > 1),
|
||||
)
|
||||
? state
|
||||
: false;
|
||||
},
|
||||
{ timeout: 90000 },
|
||||
{ points, tiles },
|
||||
)
|
||||
.catch(async (cause) => {
|
||||
const state = await page.evaluate(() => window.qaSurfaceHeights);
|
||||
throw new Error(
|
||||
`${kind} surface readiness failed: ${state?.points.map((p) => `${p.id}: ${p.height} m`).join(', ') ?? 'no samples'}; loaded=${state?.loaded}, photoreal=${state?.photoreal}`,
|
||||
{ cause },
|
||||
);
|
||||
});
|
||||
try {
|
||||
const state = await handle.jsonValue();
|
||||
if (tiles)
|
||||
for (const p of state.points)
|
||||
assertTileHeight(p.height, 50, `${kind}/${p.id} sampleHeight`);
|
||||
} finally {
|
||||
await handle.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export const features = points.map(({ id, lon, lat }) => ({
|
||||
type: 'Feature',
|
||||
id,
|
||||
|
||||
@@ -46,6 +46,7 @@ is fixed. Neither excluded check is a signal to remove a workaround.
|
||||
| `src/globe/navara/movers/batchIds.js:11` | Unregistering pickable wrappers does not recycle their global batch IDs. | 0.2.1 | `batch-ids-retained` (browser, confirmed) checks disjoint allocations after unregistering; this measures ID reuse, not WASM byte reclamation. | Remove `createBatchIdPool` and pool wiring in `movers/index.js`. |
|
||||
| `src/globe/navara/ground/index.js:145` | The original native-vector path lost the first `show: false` while a new layer was being built. Current ground rendering uses its own pass, so the hide retry is already absent. | 0.2.1 (original path) | No canary: `initial-vector-hide-dropped` timed out before a verdict; the shared readiness error does not identify the failed wait. Requires browser diagnosis. | No current hide retry to delete. Do not remove the ground pass: it also implements draping and materials. |
|
||||
| `src/globe/navara/maps.js:76` | Idle can be reported before a fetched raster tile arrives after a map switch; the map may keep a stale frame. | 0.2.1 | `idle-before-raster-arrives` (browser, confirmed) holds real raster requests across the idle event, then releases local PNG responses. | Remove the `SETTLE_MS` / `SETTLE_EVERY_MS` redraw timer from `changed`. |
|
||||
| `src/globe/navara/maps.js` (`setPhotorealTiles`) | The default `model.height=1` displaces transformed 3D Tiles by −1 m in ECEF X on the synthetic ENU fixture; both rendered meshes and CPU queries inherit it. Set `model.height: 0` to preserve the tileset transform. | 0.2.1 | `scripts/qa-tiles-fixture.test.mjs` loads the real WASM package: default transform reproduces the measured flat-top +0.118 m / roof −0.676 m errors; the adapter restores all probes and 40 roof points within 1 µm against independent triangle rays. `qa-tiles-fixture --report` prints browser samples without asserting them. | Remove the explicit zero only when the package's default preserves authored tile positions; retain the authored-geometry regression test. |
|
||||
| `src/globe/navara/maps.js:145` | Removing the final raster layer interrupts terrain loading at the current view. | 0.2.1 | No canary: needs terrain-loading continuity across final-layer removal; the raster idle fixture does not test terrain. | Remove the add-before-delete constraint and hidden base-raster retention under 3D tiles. |
|
||||
| `src/globe/navara/maps.js:37`, `src/globe/navara/maps.js:209` | Quantized-mesh root URLs without the tile template can panic. | 0.2.1 | No canary: deliberately panicking WASM would invalidate the remaining view checks; no isolated terrain fixture here. | Use the upstream root-URL API instead of manually appending `{z}/{x}/{y}.terrain`. |
|
||||
| `src/globe/navara/maps.js:65` | No ion imagery path. | 0.2.1 | No canary: explicit unsupported source; requires a real ion imagery endpoint. | Replace the ion-imagery rejection with native source support. |
|
||||
|
||||
@@ -299,7 +299,8 @@ export function createNavaraMaps(
|
||||
'photoreal',
|
||||
url,
|
||||
{ type: '3d-tiles', url },
|
||||
{ type: '3d-tiles', model: { maxSse: PHOTOREAL_MAX_SSE } },
|
||||
// Navara's default height shifts transformed tiles along ECEF X.
|
||||
{ type: '3d-tiles', model: { maxSse: PHOTOREAL_MAX_SSE, height: 0 } },
|
||||
tilesetUrl
|
||||
? []
|
||||
: [
|
||||
|
||||
@@ -193,7 +193,7 @@ test('setPhotorealTiles shows Google 3D tiles with a browser key and the Google
|
||||
tiles.desc.source.desc.url,
|
||||
'https://tile.googleapis.com/v1/3dtiles/root.json?key=browser%20key',
|
||||
);
|
||||
assert.deepEqual(tiles.desc.model, { maxSse: 16 });
|
||||
assert.deepEqual(tiles.desc.model, { maxSse: 16, height: 0 });
|
||||
// Hidden, not deleted: Navara 0.2.1 stops loading terrain at the current
|
||||
// view when the last raster layer is deleted.
|
||||
const [base] = layersOf(view, 'raster');
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// straight down the ellipsoid normal against the 3D-tile meshes Navara shows.
|
||||
// Where the shown tiles do not reach (outside the frustum), the loaded
|
||||
// tiles Navara hides answer, the finest first. Tile positions are float32
|
||||
// relative to the tile centre, which sits in the
|
||||
// float64 matrixWorld with no rotation, so a ray taken into a mesh's local
|
||||
// frame gives Earth-fixed metres. Navara frees the CPU arrays after upload;
|
||||
// in the mesh's local frame; its float64 matrixWorld maps hits back into
|
||||
// Earth-fixed metres, including any tileset rotation or translation.
|
||||
// Navara frees the CPU arrays after upload;
|
||||
// retainTileGeometry (internals.js) keeps positions and indices. Without a
|
||||
// tile hit, the answer is Navara's terrain height, as Cesium's sampleHeight
|
||||
// also samples the globe. Navara internals come in through `hooks`, so this
|
||||
@@ -37,7 +37,10 @@ export function surfaceRay(place) {
|
||||
RAY_START_HEIGHT,
|
||||
WGS84,
|
||||
);
|
||||
const normal = WGS84.geodeticSurfaceNormal(origin, cartesian3.create());
|
||||
const normal = WGS84.geodeticSurfaceNormalCartographic(
|
||||
place,
|
||||
cartesian3.create(),
|
||||
);
|
||||
return {
|
||||
origin,
|
||||
direction: cartesian3.negate(normal, cartesian3.create()),
|
||||
|
||||
@@ -84,6 +84,12 @@ test('surfaceRay runs down the ellipsoid normal from 12 km', () => {
|
||||
cartesian3.create(),
|
||||
);
|
||||
near(cartographic.fromCartesian(below, WGS84).height, 0, 1e-6, 'ground');
|
||||
near(
|
||||
cartesian3.distance(below, cartesian3.fromDegrees(20, 10, 0)),
|
||||
0,
|
||||
1e-6,
|
||||
'the ray keeps longitude and latitude at the surface',
|
||||
);
|
||||
assert.equal(heightAlongRay(11950), 50);
|
||||
assert.equal(heightAlongRay(null), undefined);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user