feat(globe): bring the Navara engine up to the shell contract

- camera.livePosition: one engine-owned Cartesian3, refreshed on read
  (the model replaces its state on every move); camera.height()
- render.request(reason) takes the diagnostic reason and ignores it
- maps.current() returns the sources as passed: the base imagery on the
  globe (none while tiles replace it) and the tiles shown
- maps.releasePhotorealTiles forgets a source and takes its tiles away
  if they show
- base imagery and photoreal share one switch order; tiles off brings
  back the last base imagery with its credit; imagery chosen while
  tiles show waits hidden, credit included
- Navara QA runs the contract's maps checks with a Google 3D tiles
  source on the dev server's browser key: 58/58
This commit is contained in:
German Khokhlov committed 2026-10-15 19:08:47 +04:00
1 parent a4ac0b9b0d
commit 840e34a881
5 files changed
+130 -10

No files matched your search

+8
View File
@@ -266,7 +266,15 @@ export function createCameraModel({
}
}
// camera.livePosition: one owned object, refreshed on read (place() and
// sync() replace `state`, so it is copied rather than aliased).
const livePosition = cartesian3.create();
const camera = {
get livePosition() {
return cartesian3.clone(state.position, livePosition);
},
height: () => cartographic.fromCartesian(state.position, WGS84).height,
pose() {
const current = view();
const angles = headingPitchRoll(
+18
View File
@@ -439,3 +439,21 @@ test('rounding noise in the drawn camera is no movement, so moveEnd still fires'
assert.equal(clock.time >= 960, true);
assert.equal(ends, 1, 'moveEnd did not fire while Navara redrew the pose');
});
test('livePosition is one engine-owned object holding the current position; height() reads it', () => {
const { camera, port, frame } = setup();
const live = camera.livePosition;
const destination = cartesian3.fromDegrees(20, 10, 12345);
camera.setView({ destination });
assert.equal(camera.livePosition, live, 'not the same object');
near(cartesian3.distance(live, destination), 0, 1e-9, 'follows setView');
near(camera.height(), 12345, 1e-6, 'height');
frame();
const moved = cartesian3.fromDegrees(21, 11, 9e5);
port.drawn = { position: moved, ...orientationVectors(moved, 0.3, -1, 0) };
frame();
near(cartesian3.distance(camera.livePosition, moved), 0, 1e-9, 'follows Navara');
near(camera.height(), 9e5, 1e-6, 'height after a user move');
const pose = camera.pose();
assert.notEqual(pose.position, live, 'pose shares the live object');
});
@@ -115,6 +115,8 @@ test('render requests are forceUpdate and continuous mode is Navara animation',
const { engine, view } = setup();
engine.render.request();
assert.deepEqual(view.calls.at(-1), ['forceUpdate']);
engine.render.request('world-overlay');
assert.deepEqual(view.calls.at(-1), ['forceUpdate'], 'the reason is ignored');
engine.render.setContinuous(true);
assert.equal(view.animation, true);
assert.equal(engine.render.isContinuous(), true);
+47 -10
View File
@@ -67,7 +67,9 @@ export function createNavaraMaps(
{ fetch: fetchJson = globalThis.fetch } = {},
) {
const slots = { imagery: null, terrain: null, photoreal: null };
const generations = { imagery: 0, terrain: 0, photoreal: 0 };
// Base imagery and photoreal tiles share one switch order (display): the
// call made last decides what shows.
const generations = { display: 0, terrain: 0 };
let destroyed = false;
function retire(slot) {
@@ -95,7 +97,14 @@ export function createNavaraMaps(
);
retire(previous);
if (shown.length) view.attribution?.add(shown);
slots[name] = { key, source, layer, layerDesc, credits: shown };
slots[name] = {
key,
source,
layer,
layerDesc,
credits: shown,
hidden: false,
};
view.forceUpdate();
}
@@ -146,15 +155,26 @@ export function createNavaraMaps(
}
}
// What current() reports: the base imagery on the globe (none while tiles
// replace it) and the tiles shown, as the objects passed in.
let baseSource = null;
let currentBase = null;
let tilesSource = null;
const knownTiles = new Set();
const maps = {
async setBaseImagery(source) {
const generation = ++generations.imagery;
const generation = ++generations.display;
const { url, credit } = imageryFor(source);
if (destroyed || generation !== generations.imagery) return;
if (slots.imagery?.key === url) return setImageryShown(true);
show('imagery', url, { type: 'raster-tile', url }, { type: 'raster' }, [
credit,
]);
if (destroyed || generation !== generations.display) return;
baseSource = source;
currentBase = source;
if (slots.imagery?.key !== url)
show('imagery', url, { type: 'raster-tile', url }, { type: 'raster' }, [
credit,
]);
// Under tiles the base imagery waits hidden, credit included.
setImageryShown(slots.photoreal == null);
},
async setTerrain(source) {
const generation = ++generations.terrain;
@@ -169,9 +189,15 @@ export function createNavaraMaps(
show('terrain', description.url, description, { type: 'terrain' });
},
async setPhotorealTiles(source) {
const generation = ++generations.photoreal;
const generation = ++generations.display;
if (source == null) {
clear('photoreal');
tilesSource = null;
// The globe comes back with the last base imagery and its credit.
if (slots.imagery) {
setImageryShown(true);
currentBase = baseSource;
}
view.forceUpdate();
return;
}
@@ -180,9 +206,12 @@ export function createNavaraMaps(
throw new Error(
'Google 3D tiles on Navara need a browser key (googleApiKey)',
);
if (destroyed || generation !== generations.photoreal) return;
if (destroyed || generation !== generations.display) return;
const url = `${GOOGLE_TILES_ROOT}?key=${encodeURIComponent(key)}`;
setImageryShown(false);
currentBase = null;
tilesSource = source;
knownTiles.add(source);
if (slots.photoreal?.key === url) return;
show(
'photoreal',
@@ -201,7 +230,15 @@ export function createNavaraMaps(
// them; Navara lists them in its attribution card, collapsed by default.
view.attribution?.show();
},
releasePhotorealTiles(source) {
if (!knownTiles.delete(source)) return;
if (tilesSource !== source) return;
tilesSource = null;
clear('photoreal');
view.forceUpdate();
},
photorealActive: () => slots.photoreal != null,
current: () => ({ baseImagery: currentBase, photoreal: tilesSource }),
};
return {
+55
View File
@@ -223,3 +223,58 @@ test('destroy removes every layer, source and credit it added', async () => {
assert.deepEqual(view.sources, []);
assert.deepEqual(view.shown, []);
});
test('current() returns the passed sources: base while the globe shows, tiles while they show', async () => {
const view = fakeView();
const { maps } = createNavaraMaps(view);
const osm = { kind: 'osm' };
const esri = { kind: 'esri-imagery' };
const tiles = { kind: 'photoreal', googleApiKey: 'k' };
assert.deepEqual(maps.current(), { baseImagery: null, photoreal: null });
await maps.setBaseImagery(osm);
assert.deepEqual(maps.current(), { baseImagery: osm, photoreal: null });
await maps.setPhotorealTiles(tiles);
assert.equal(maps.current().baseImagery, null, 'tiles replace the base');
assert.equal(maps.current().photoreal, tiles);
// Imagery chosen while the tiles show goes on the hidden globe.
await maps.setBaseImagery(esri);
const [raster] = layersOf(view, 'raster');
assert.match(raster.desc.source.desc.url, /World_Imagery/);
assert.equal(raster.desc.raster?.show, false, 'the new layer shows over tiles');
assert.ok(
!view.shown.some((item) => /esri/i.test(JSON.stringify(item))),
'its credit shows over tiles',
);
assert.equal(maps.current().baseImagery, esri);
assert.equal(maps.current().photoreal, tiles);
await maps.setPhotorealTiles(null);
assert.deepEqual(maps.current(), { baseImagery: esri, photoreal: null });
assert.notEqual(raster.desc.raster?.show, false, 'base not back');
assert.ok(view.shown.some((item) => /esri/i.test(JSON.stringify(item))), 'credit not back');
});
test('the same tiles under a new source object count as that source', async () => {
const view = fakeView();
const { maps } = createNavaraMaps(view);
const first = { kind: 'photoreal', googleApiKey: 'k' };
const second = { kind: 'photoreal', googleApiKey: 'k' };
await maps.setPhotorealTiles(first);
await maps.setPhotorealTiles(second);
assert.equal(maps.current().photoreal, second);
assert.equal(layersOf(view, '3d-tiles').length, 1);
});
test('releasePhotorealTiles forgets the source and takes its tiles away if shown', async () => {
const view = fakeView();
const { maps } = createNavaraMaps(view);
const tiles = { kind: 'photoreal', googleApiKey: 'k' };
await maps.setPhotorealTiles(tiles);
maps.releasePhotorealTiles({ kind: 'photoreal', googleApiKey: 'k' });
assert.equal(maps.photorealActive(), true, 'another object released it');
maps.releasePhotorealTiles(tiles);
maps.releasePhotorealTiles(tiles);
assert.equal(maps.photorealActive(), false);
assert.equal(maps.current().photoreal, null);
assert.equal(layersOf(view, '3d-tiles').length, 0);
});