From fd65288101a37c108e87471405dad5c90cd0cf0c Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Sat, 7 Nov 2026 22:24:46 +0400 Subject: [PATCH] Cover raised imagery shells on the headless Cesium engine Exercise the real shell primitives and materials over the headless engine. Cover raised rectangle geometry, alpha and depth policy, camera lift, shell ordering, upload readiness, hidden frames, detail replacement and moved windows, render idling and resource cleanup. Drive the public imagery API through detail bounds and staged source settlement as well. Supply only the geometry and texture completion signals that require a renderer. Preserve Cesium's reference behavior without changing runtime code. Navara and the fake engine are unchanged. Include the new test in the format scope. All eleven coupling counts remain unchanged; none fell or grew. Renderer lines remain 0 and 189, external Cesium import files 3, viewer lines 891, Navara kind checks 1, engine-sniff calls 0, extension reads 3, ground passes 1, SDK loads 3, screen-handler files 4 and engine-name lines 32. --- scripts/format-scope.json | 1 + src/globe/cesium/imageryShell.test.mjs | 329 +++++++++++++++++++++++++ 2 files changed, 330 insertions(+) create mode 100644 src/globe/cesium/imageryShell.test.mjs diff --git a/scripts/format-scope.json b/scripts/format-scope.json index b221331b0..01930d24b 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -1643,6 +1643,7 @@ "scripts/qa-weather-compare.mjs", "src/globe/cesium/imagery.js", "src/globe/cesium/imagery.test.mjs", + "src/globe/cesium/imageryShell.test.mjs", "src/globe/imagery.js", "src/globe/imagery.test.mjs", "src/globe/navara/imagery/index.js", diff --git a/src/globe/cesium/imageryShell.test.mjs b/src/globe/cesium/imageryShell.test.mjs new file mode 100644 index 000000000..88205efc5 --- /dev/null +++ b/src/globe/cesium/imageryShell.test.mjs @@ -0,0 +1,329 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import * as Cesium from 'cesium'; +import { createCesiumMoversEngine } from '../../testSupport/cesiumMoversEngine.mjs'; +import { createImageryShell } from './imageryShell.js'; + +const BOX = { west: -100, south: 30, east: -90, north: 40 }; +const WINDOW = { west: 0.1, south: 0.2, east: 0.7, north: 0.8 }; +const image = (width = 8, height = 4) => ({ width, height }); +const items = (collection) => + Array.from({ length: collection.length }, (_, i) => collection.get(i)); + +function fixture(t) { + const f = createCesiumMoversEngine(); + const scene = f.viewer.scene; + const shells = []; + let requests = 0; + scene.requestRender = () => requests++; + f.viewer.imageryLayers = new Cesium.ImageryLayerCollection(); + scene.globe.show = true; + t.after(() => { + for (const shell of shells) shell.destroy(); + f.engine.destroy(); + }); + return { + ...f, + scene, + requests: () => requests, + frames(count = 1) { + for (let i = 0; i < count; i++) f.frame(); + }, + shell(options = {}) { + const before = items(scene.primitives); + let settled = 0; + const shell = createImageryShell({ + scene, + rectangle: BOX, + height: 600, + onSettled: () => settled++, + ...options, + }); + shells.push(shell); + const primitive = items(scene.primitives).find( + (item) => !before.includes(item), + ); + return { + shell, + primitive, + material: primitive.appearance.material, + settled: () => settled, + }; + }, + }; +} + +// The headless scene never uploads textures or builds asynchronous geometry. +// Supply only those GPU completion signals; materials and primitives are real. +function uploaded(primitive, name = 'image') { + primitive._ready = true; + const uniforms = primitive.appearance.material.uniforms; + const value = uniforms[name]; + uniforms[`${name}Dimensions`] = new Cesium.Cartesian2( + value.width, + value.height, + ); +} + +test('a raised shell keeps its rectangle, height and alpha without hiding or picking other content', (t) => { + const f = fixture(t); + const { shell, primitive, material } = f.shell(); + assert.ok(primitive instanceof Cesium.Primitive); + assert.ok(material instanceof Cesium.Material); + const geometry = primitive.geometryInstances.geometry; + assert.ok(geometry instanceof Cesium.RectangleGeometry); + assert.ok( + Cesium.Rectangle.equalsEpsilon( + geometry.rectangle, + Cesium.Rectangle.fromDegrees(-100, 30, -90, 40), + 1e-12, + ), + ); + assert.equal(geometry._surfaceHeight, 600); + assert.equal(primitive.allowPicking, false); + assert.equal(primitive.asynchronous, true); + assert.equal(primitive.appearance.flat, true); + assert.equal(primitive.appearance.isTranslucent(), false); + assert.deepEqual(primitive.appearance.getRenderState(), { + depthTest: { enabled: true }, + depthMask: false, + blending: Cesium.BlendingState.ALPHA_BLEND, + }); + shell.setAlpha(0.35); + assert.equal(material.uniforms.alpha, 0.35); + const requests = f.requests(); + shell.setAlpha(0.35); + assert.equal(f.requests(), requests, 'unchanged alpha needs no frame'); +}); + +test('shell lift follows camera height uniformly and returns to the ellipsoid', (t) => { + const f = fixture(t); + const a = f.shell({ height: 600 }); + const b = f.shell({ height: 900 }); + for (const [height, lift] of [ + [10_000, 0], + [1_000_000, 4000], + [40_000_000, 60_000], + [1000, 0], + ]) { + f.scene.camera.setView({ + destination: Cesium.Cartesian3.fromDegrees(-95, 35, height), + }); + f.frames(); + const expected = Cesium.Matrix4.fromUniformScale( + 1 + lift / Cesium.Ellipsoid.WGS84.maximumRadius, + ); + assert.ok(Cesium.Matrix4.equals(a.primitive.modelMatrix, expected)); + assert.ok(Cesium.Matrix4.equals(b.primitive.modelMatrix, expected)); + } +}); + +test('shell rank puts all shells before other primitives and preserves equal-rank order', (t) => { + const f = fixture(t); + const other = f.scene.primitives.add(new Cesium.PrimitiveCollection()); + t.after(() => f.scene.primitives.remove(other)); + const high = f.shell({ rank: 8 }); + const low = f.shell({ rank: -2 }); + const equal = f.shell({ rank: 8 }); + assert.deepEqual(items(f.scene.primitives), [ + low.primitive, + high.primitive, + equal.primitive, + other, + ]); + high.shell.setRank(-3); + assert.deepEqual(items(f.scene.primitives), [ + high.primitive, + low.primitive, + equal.primitive, + other, + ]); + f.scene.primitives.raiseToTop(low.primitive); + low.shell.setRank(-2); + assert.equal(f.scene.primitives.get(1), low.primitive); + assert.equal(f.scene.primitives.get(3), other); +}); + +test('a shell waits for geometry, texture dimensions and shown frames, then idles', (t) => { + const f = fixture(t); + const { shell, primitive, material, settled } = f.shell(); + const before = f.scene.postRender.numberOfListeners; + shell.setImage(image()); + material.uniforms.imageDimensions = new Cesium.Cartesian2(1, 1); + f.frames(4); + assert.equal(shell.drawn(), false, 'geometry is not ready'); + assert.equal(settled(), 0); + assert.equal(f.scene.postRender.numberOfListeners, before + 1); + primitive._ready = true; + f.frames(3); + assert.equal(shell.drawn(), false, 'the placeholder texture is not ready'); + assert.equal(settled(), 0); + uploaded(primitive); + f.frames(); + assert.equal(shell.drawn(), true); + assert.equal(settled(), 1); + assert.equal(f.scene.postRender.numberOfListeners, before); + const requests = f.requests(); + shell.setImage(shell.image()); + f.frames(3); + assert.equal(f.requests(), requests, 'the settled image needs no frames'); + assert.equal(settled(), 1); + + shell.setImage(image()); + shell.setShow(false); + f.frames(5); + assert.equal(shell.drawn(), false, 'hidden frames do not upload'); + assert.equal(f.scene.postRender.numberOfListeners, before); + shell.setShow(true); + f.frames(2); + assert.equal(shell.drawn(), false); + f.frames(); + assert.equal(shell.drawn(), true); + assert.equal(settled(), 2); +}); + +test('detail waits for its upload, keeps the same window and hides a moved window until ready', (t) => { + const f = fixture(t); + const { shell, primitive, material, settled } = f.shell(); + shell.setImage(image()); + uploaded(primitive); + f.frames(3); + shell.setDetail(image(16, 8), WINDOW); + material.uniforms.detailDimensions = new Cesium.Cartesian2(1, 1); + f.frames(3); + assert.equal(shell.drawn(), false); + assert.equal(shell.diagnostics().detail.window, null); + assert.equal(settled(), 1); + uploaded(primitive, 'detail'); + f.frames(); + assert.deepEqual(shell.diagnostics().detail.window, WINDOW); + assert.equal(shell.drawn(), true); + assert.equal(settled(), 2); + + shell.setDetail(image(16, 8), { ...WINDOW }); + assert.deepEqual(shell.diagnostics().detail.window, WINDOW); + f.frames(2); + assert.equal(shell.drawn(), false, 'same-sized uploads still need frames'); + f.frames(); + assert.equal(shell.drawn(), true); + assert.equal(settled(), 3); + + const moved = { west: 0.2, south: 0.3, east: 0.6, north: 0.7 }; + shell.setDetail(image(32, 16), moved); + assert.equal(shell.diagnostics().detail.window, null); + uploaded(primitive, 'detail'); + f.frames(2); + assert.equal(shell.diagnostics().detail.window, null); + f.frames(); + assert.deepEqual(shell.diagnostics().detail.window, moved); + assert.equal(settled(), 4); + + shell.setDetail(image(64, 32), WINDOW); + shell.setDetail(null); + assert.equal(shell.detail(), null); + assert.equal(material.uniforms.detail, Cesium.Material.DefaultImageId); + assert.equal(shell.diagnostics().detail.window, null); + f.frames(4); + assert.equal(shell.diagnostics().detail.window, null, 'no stale window'); + assert.equal(shell.drawn(), true); +}); + +test('destroy releases the primitive, material, images and frame listeners once', (t) => { + const f = fixture(t); + const before = [ + f.scene.preRender.numberOfListeners, + f.scene.postRender.numberOfListeners, + ]; + const { shell, primitive, material, settled } = f.shell(); + shell.setImage(image()); + shell.setDetail(image(16, 8), WINDOW); + assert.equal(f.scene.preRender.numberOfListeners, before[0] + 1); + assert.equal(f.scene.postRender.numberOfListeners, before[1] + 1); + shell.destroy(); + assert.equal(f.scene.primitives.contains(primitive), false); + assert.equal(primitive.isDestroyed(), true); + assert.equal(material.isDestroyed(), true); + assert.equal(shell.image(), null); + assert.equal(shell.detail(), null); + assert.deepEqual( + [f.scene.preRender.numberOfListeners, f.scene.postRender.numberOfListeners], + before, + ); + const requests = f.requests(); + shell.destroy(); + shell.setImage(image()); + shell.setDetail(image(), WINDOW); + shell.setShow(true); + shell.setAlpha(1); + shell.setRank(4); + f.frames(4); + assert.equal(f.requests(), requests); + assert.equal(settled(), 0); +}); + +test('destroy also releases the material when the scene already destroyed the primitive', (t) => { + const f = fixture(t); + const { shell, primitive, material } = f.shell(); + shell.setImage(image()); + f.scene.primitives.remove(primitive); + assert.equal(primitive.isDestroyed(), true); + assert.equal(material.isDestroyed(), false); + shell.destroy(); + assert.equal(material.isDestroyed(), true); +}); + +test('engine imagery mounts real shells, converts detail bounds and settles staged images', async (t) => { + const f = fixture(t); + const source = { kind: 'image', image: image(), rectangle: BOX }; + const layer = f.engine.imagery.createLayer({ + id: 'raised', + source, + raise: 600, + alpha: 0.6, + }); + const primitive = f.scene.primitives.get(0); + assert.ok(primitive instanceof Cesium.Primitive); + assert.equal(f.viewer.imageryLayers.length, 0, 'no draped imagery layer'); + assert.equal(primitive.appearance.material.uniforms.alpha, 0.6); + uploaded(primitive); + f.frames(3); + assert.equal(layer.ready(), true); + let detailed = false; + const detail = layer.setDetail({ + image: image(16, 8), + rectangle: { west: -99, south: 32, east: -93, north: 38 }, + }); + detail.then((value) => (detailed = value)); + uploaded(primitive, 'detail'); + f.frames(2); + await Promise.resolve(); + assert.equal(detailed, false); + f.frames(); + assert.equal(await detail, true); + assert.deepEqual( + primitive.appearance.material.uniforms.window, + new Cesium.Cartesian4(0.1, 0.2, 0.7, 0.8), + ); + await layer.setDetail(null); + + const next = { ...source, image: image(32, 16) }; + const staged = layer.setSource(next, { staged: true }); + assert.equal(layer.staging(), true); + await new Promise((resolve) => setImmediate(resolve)); + assert.equal( + f.scene.primitives.get(0), + primitive, + 'same rectangle reuses the shell', + ); + uploaded(primitive); + f.frames(2); + assert.equal(layer.staging(), true); + f.frames(); + assert.equal(await staged, true); + assert.equal(layer.source().image, next.image); + const material = primitive.appearance.material; + layer.destroy(); + assert.equal(primitive.isDestroyed(), true); + assert.equal(material.isDestroyed(), true); + assert.deepEqual(f.engine.imagery.list(), []); +});