mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 07:42:23 +00:00
Give a viewer with no renderer an effects port, as cameras get
The renderer bridge builds and keeps an effects port over a viewer no renderer is bound to, with a factory the Cesium effects module provides, so visual settings on a consumer's own viewer draw their styles, bloom and sharpen as before and give the scene's bloom back when destroyed. The application shell loads the factory; a test checks every export that reaches the effects lookup loads it.
This commit is contained in:
1 parent
d0ef3a902b
commit
910a691494
8 files changed
+333
-64
No files matched your search
@@ -1000,6 +1000,7 @@
|
|||||||
"src/styles/surveillance.js",
|
"src/styles/surveillance.js",
|
||||||
"src/styles/thermal.js",
|
"src/styles/thermal.js",
|
||||||
"src/ui/applicationShell.js",
|
"src/ui/applicationShell.js",
|
||||||
|
"src/render/cesium/effects.js",
|
||||||
"src/data/followed.js",
|
"src/data/followed.js",
|
||||||
"src/render/cesium/camera.js",
|
"src/render/cesium/camera.js",
|
||||||
"src/render/cesium/credits.js",
|
"src/render/cesium/credits.js",
|
||||||
|
|||||||
Vendored
+68
-9
@@ -22,7 +22,8 @@
|
|||||||
* in its own shader, not the frame.
|
* in its own shader, not the frame.
|
||||||
*/
|
*/
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { listenerList } from './common.js';
|
import { provideUnboundEffects } from '../rendererOf.js';
|
||||||
|
import { listenerList, uncaught } from './common.js';
|
||||||
|
|
||||||
/** Below this a style, bloom or sharpen draws nothing, so it runs no pass. */
|
/** Below this a style, bloom or sharpen draws nothing, so it runs no pass. */
|
||||||
const OFF = 0.001;
|
const OFF = 0.001;
|
||||||
@@ -135,6 +136,9 @@ export function createCesiumEffects(
|
|||||||
let disposed = false;
|
let disposed = false;
|
||||||
const started = performance.now();
|
const started = performance.now();
|
||||||
|
|
||||||
|
// Disposed, or the scene went first and took its stages with it.
|
||||||
|
const gone = () => disposed || scene.isDestroyed?.() === true;
|
||||||
|
|
||||||
const drawing = (record) =>
|
const drawing = (record) =>
|
||||||
(record.id === current || fading.has(record.id)) && record.intensity > OFF;
|
(record.id === current || fading.has(record.id)) && record.intensity > OFF;
|
||||||
|
|
||||||
@@ -197,7 +201,7 @@ export function createCesiumEffects(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function changed(reason) {
|
function changed(reason) {
|
||||||
if (disposed) return;
|
if (gone()) return;
|
||||||
apply();
|
apply();
|
||||||
frame.requestRender(reason);
|
frame.requestRender(reason);
|
||||||
listeners.fire(port.diagnostics());
|
listeners.fire(port.diagnostics());
|
||||||
@@ -217,7 +221,7 @@ export function createCesiumEffects(
|
|||||||
: [],
|
: [],
|
||||||
defineStyle(definition) {
|
defineStyle(definition) {
|
||||||
if (
|
if (
|
||||||
disposed ||
|
gone() ||
|
||||||
!capabilities.has('styles') ||
|
!capabilities.has('styles') ||
|
||||||
typeof definition?.shader?.glsl !== 'string'
|
typeof definition?.shader?.glsl !== 'string'
|
||||||
)
|
)
|
||||||
@@ -260,7 +264,7 @@ export function createCesiumEffects(
|
|||||||
return () => {
|
return () => {
|
||||||
if (removed) return;
|
if (removed) return;
|
||||||
removed = true;
|
removed = true;
|
||||||
if (disposed || styles.get(id) !== record) return;
|
if (gone() || styles.get(id) !== record) return;
|
||||||
styles.delete(id);
|
styles.delete(id);
|
||||||
stages.remove(record.stage);
|
stages.remove(record.stage);
|
||||||
fading.delete(id);
|
fading.delete(id);
|
||||||
@@ -269,7 +273,7 @@ export function createCesiumEffects(
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
setStyle(id, params) {
|
setStyle(id, params) {
|
||||||
if (disposed || !capabilities.has('styles')) return;
|
if (gone() || !capabilities.has('styles')) return;
|
||||||
const next = id === null || id === undefined ? null : String(id);
|
const next = id === null || id === undefined ? null : String(id);
|
||||||
if (next !== null && !styles.has(next)) return;
|
if (next !== null && !styles.has(next)) return;
|
||||||
if (next !== current) {
|
if (next !== current) {
|
||||||
@@ -282,19 +286,19 @@ export function createCesiumEffects(
|
|||||||
changed('effects-style');
|
changed('effects-style');
|
||||||
},
|
},
|
||||||
setStyleIntensity(id, intensity) {
|
setStyleIntensity(id, intensity) {
|
||||||
if (disposed || !known(id)) return;
|
if (gone() || !known(id)) return;
|
||||||
const record = styles.get(id);
|
const record = styles.get(id);
|
||||||
record.intensity = clamp01(intensity);
|
record.intensity = clamp01(intensity);
|
||||||
if (record.intensity <= OFF) fading.delete(id);
|
if (record.intensity <= OFF) fading.delete(id);
|
||||||
changed('effects-style');
|
changed('effects-style');
|
||||||
},
|
},
|
||||||
setStyleParams(id, params) {
|
setStyleParams(id, params) {
|
||||||
if (disposed || !known(id)) return;
|
if (gone() || !known(id)) return;
|
||||||
assignParams(styles.get(id), params);
|
assignParams(styles.get(id), params);
|
||||||
changed('effects-style');
|
changed('effects-style');
|
||||||
},
|
},
|
||||||
bloom({ enabled, strength } = {}) {
|
bloom({ enabled, strength } = {}) {
|
||||||
if (disposed || !capabilities.has('bloom')) return;
|
if (gone() || !capabilities.has('bloom')) return;
|
||||||
const bloom = scene.postProcessStages.bloom;
|
const bloom = scene.postProcessStages.bloom;
|
||||||
if (!bloomTaken) {
|
if (!bloomTaken) {
|
||||||
bloomTaken = {
|
bloomTaken = {
|
||||||
@@ -311,7 +315,7 @@ export function createCesiumEffects(
|
|||||||
changed('effects-bloom');
|
changed('effects-bloom');
|
||||||
},
|
},
|
||||||
sharpen({ enabled, amount } = {}) {
|
sharpen({ enabled, amount } = {}) {
|
||||||
if (disposed || !capabilities.has('sharpen')) return;
|
if (gone() || !capabilities.has('sharpen')) return;
|
||||||
sharpenEnabled = Boolean(enabled);
|
sharpenEnabled = Boolean(enabled);
|
||||||
if (amount !== undefined) sharpenAmount = clamp01(amount);
|
if (amount !== undefined) sharpenAmount = clamp01(amount);
|
||||||
if (!sharpenStage) makeSharpen();
|
if (!sharpenStage) makeSharpen();
|
||||||
@@ -352,6 +356,13 @@ export function createCesiumEffects(
|
|||||||
disposed = true;
|
disposed = true;
|
||||||
stopClock();
|
stopClock();
|
||||||
listeners.clear();
|
listeners.clear();
|
||||||
|
// A destroyed scene took its stages with it.
|
||||||
|
if (scene.isDestroyed?.()) {
|
||||||
|
styles.clear();
|
||||||
|
sharpenStage = null;
|
||||||
|
bloomTaken = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (const record of styles.values()) stages.remove(record.stage);
|
for (const record of styles.values()) stages.remove(record.stage);
|
||||||
styles.clear();
|
styles.clear();
|
||||||
if (sharpenStage) stages.remove(sharpenStage);
|
if (sharpenStage) stages.remove(sharpenStage);
|
||||||
@@ -365,3 +376,51 @@ export function createCesiumEffects(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The frame port's members the effects port uses, straight over a scene no
|
||||||
|
* renderer drives: a hold asks for the next frame after each one drawn.
|
||||||
|
* @param {object} scene
|
||||||
|
*/
|
||||||
|
function sceneFrame(scene) {
|
||||||
|
return {
|
||||||
|
on: (phase, cb) =>
|
||||||
|
scene[phase].addEventListener(() => cb({ now: performance.now() })),
|
||||||
|
requestRender: () => scene.requestRender(),
|
||||||
|
holdContinuous() {
|
||||||
|
const off = scene.postRender.addEventListener(() =>
|
||||||
|
scene.requestRender(),
|
||||||
|
);
|
||||||
|
scene.requestRender();
|
||||||
|
let released = false;
|
||||||
|
return () => {
|
||||||
|
if (released) return;
|
||||||
|
released = true;
|
||||||
|
off();
|
||||||
|
if (!scene.isDestroyed?.()) scene.requestRender();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Cesium viewer of a consumer's own, with no renderer bound to it, gets an
|
||||||
|
// effects port over its scene when code first asks `findEffects` for one.
|
||||||
|
provideUnboundEffects((viewer) => {
|
||||||
|
const scene = viewer.scene;
|
||||||
|
// By shape, not `instanceof`: a consumer's viewer may come from another
|
||||||
|
// copy of Cesium.
|
||||||
|
if (
|
||||||
|
typeof scene?.postProcessStages?.add !== 'function' ||
|
||||||
|
typeof scene.postProcessStages.remove !== 'function' ||
|
||||||
|
!scene.postProcessStages.bloom ||
|
||||||
|
typeof scene.preRender?.addEventListener !== 'function' ||
|
||||||
|
typeof scene.postRender?.addEventListener !== 'function' ||
|
||||||
|
typeof scene.requestRender !== 'function'
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
return createCesiumEffects(scene, {
|
||||||
|
capabilities: new Set(['styles', 'bloom', 'sharpen']),
|
||||||
|
frame: sceneFrame(scene),
|
||||||
|
report: uncaught,
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import { bloomUniforms, createCesiumEffects } from './effects.js';
|
import { bloomUniforms, createCesiumEffects } from './effects.js';
|
||||||
|
import { findEffects, releaseUnboundEffects } from '../rendererOf.js';
|
||||||
|
|
||||||
const ALL = new Set(['styles', 'bloom', 'sharpen']);
|
const ALL = new Set(['styles', 'bloom', 'sharpen']);
|
||||||
|
|
||||||
@@ -300,3 +301,90 @@ test('dispose removes every stage and releases the clock', () => {
|
|||||||
f.effects.setStyle('anim');
|
f.effects.setStyle('anim');
|
||||||
assert.equal(f.stages.length, 0);
|
assert.equal(f.stages.length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a viewer with no renderer gets an effects port over its scene', () => {
|
||||||
|
const events = () => {
|
||||||
|
const cbs = new Set();
|
||||||
|
return {
|
||||||
|
cbs,
|
||||||
|
addEventListener(cb) {
|
||||||
|
cbs.add(cb);
|
||||||
|
return () => cbs.delete(cb);
|
||||||
|
},
|
||||||
|
raise: () => [...cbs].forEach((cb) => cb()),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const stages = [];
|
||||||
|
let requests = 0;
|
||||||
|
const scene = {
|
||||||
|
postProcessStages: {
|
||||||
|
bloom: { enabled: false, uniforms: {} },
|
||||||
|
add: (stage) => stages.push(stage),
|
||||||
|
remove: (stage) => stages.splice(stages.indexOf(stage), 1),
|
||||||
|
},
|
||||||
|
preRender: events(),
|
||||||
|
postRender: events(),
|
||||||
|
requestRender: () => (requests += 1),
|
||||||
|
};
|
||||||
|
const viewer = { scene };
|
||||||
|
const effects = findEffects(viewer);
|
||||||
|
assert.ok(effects, 'built');
|
||||||
|
assert.equal(findEffects(viewer), effects, 'kept');
|
||||||
|
assert.equal(findEffects({ scene: {} }), null, 'not a Cesium scene');
|
||||||
|
effects.bloom({ enabled: true, strength: 1 });
|
||||||
|
assert.equal(scene.postProcessStages.bloom.enabled, true);
|
||||||
|
// An animated style draws every frame while it shows.
|
||||||
|
effects.defineStyle(style('anim', { animated: true }));
|
||||||
|
effects.setStyle('anim');
|
||||||
|
assert.equal(scene.postRender.cbs.size, 1, 'held');
|
||||||
|
const before = requests;
|
||||||
|
scene.postRender.raise();
|
||||||
|
assert.equal(requests, before + 1, 'the next frame is asked for');
|
||||||
|
effects.setStyleIntensity('anim', 0);
|
||||||
|
assert.equal(scene.postRender.cbs.size, 0, 'released');
|
||||||
|
assert.equal(scene.preRender.cbs.size, 0);
|
||||||
|
releaseUnboundEffects(viewer, {});
|
||||||
|
assert.ok(stages.length > 0, 'another port is not let go of');
|
||||||
|
releaseUnboundEffects(viewer, effects);
|
||||||
|
assert.equal(stages.length, 0, 'let go: its passes are gone');
|
||||||
|
assert.equal(
|
||||||
|
scene.postProcessStages.bloom.enabled,
|
||||||
|
false,
|
||||||
|
'bloom given back',
|
||||||
|
);
|
||||||
|
assert.notEqual(findEffects(viewer), effects, 'a later lookup builds anew');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a destroyed scene is let go of without touching it', () => {
|
||||||
|
let destroyed = false;
|
||||||
|
const scene = {
|
||||||
|
isDestroyed: () => destroyed,
|
||||||
|
postProcessStages: {
|
||||||
|
bloom: { enabled: false, uniforms: {} },
|
||||||
|
add() {},
|
||||||
|
remove() {
|
||||||
|
if (destroyed) throw new Error('destroyed');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
preRender: { addEventListener: () => () => {} },
|
||||||
|
postRender: { addEventListener: () => () => {} },
|
||||||
|
requestRender() {
|
||||||
|
if (destroyed) throw new Error('destroyed');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const viewer = { scene, isDestroyed: () => destroyed };
|
||||||
|
const effects = findEffects(viewer);
|
||||||
|
effects.defineStyle(style('anim', { animated: true }));
|
||||||
|
effects.setStyle('anim');
|
||||||
|
effects.bloom({ enabled: true, strength: 1 });
|
||||||
|
effects.sharpen({ enabled: true, amount: 1 });
|
||||||
|
const off = effects.defineStyle(style('plain'));
|
||||||
|
destroyed = true;
|
||||||
|
// Core's own teardown after the scene went: nothing throws.
|
||||||
|
off();
|
||||||
|
effects.bloom({ enabled: false });
|
||||||
|
effects.sharpen({ enabled: false });
|
||||||
|
effects.setStyle(null);
|
||||||
|
releaseUnboundEffects(viewer, effects);
|
||||||
|
assert.equal(findEffects(viewer), null);
|
||||||
|
});
|
||||||
+98
-45
@@ -9,15 +9,59 @@
|
|||||||
const renderers = new WeakMap();
|
const renderers = new WeakMap();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Camera ports built for scene objects no renderer is bound to (a viewer of
|
* Ports built for scene objects no renderer is bound to (a viewer of a
|
||||||
* a consumer's own, say), each `{port, dispose}` and kept for as long as its
|
* consumer's own, say), per port: each `{port, dispose}` kept for as long
|
||||||
* scene object lives, or until a renderer is bound to it.
|
* as its scene object lives, or until a renderer is bound to it. The
|
||||||
|
* renderer adapter provides the factory each is built with as it loads.
|
||||||
*/
|
*/
|
||||||
const unboundCameras = new WeakMap();
|
function unboundPorts(member) {
|
||||||
let buildUnboundCamera = null;
|
const built = new WeakMap();
|
||||||
/** Credits ports built likewise, each `{port, dispose, release}`. */
|
let build = null;
|
||||||
const unboundCredits = new WeakMap();
|
return {
|
||||||
let buildUnboundCredits = null;
|
provide(factory) {
|
||||||
|
build = factory;
|
||||||
|
},
|
||||||
|
drop(host, port) {
|
||||||
|
const entry = built.get(host);
|
||||||
|
if (!entry || (port !== undefined && entry.port !== port)) return;
|
||||||
|
built.delete(host);
|
||||||
|
entry.dispose();
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* A renderer bound to `host` supersedes its built port: released when
|
||||||
|
* it can be (it stops working for new callers but keeps what it did),
|
||||||
|
* else disposed.
|
||||||
|
*/
|
||||||
|
supersede(host) {
|
||||||
|
const entry = built.get(host);
|
||||||
|
if (!entry) return;
|
||||||
|
built.delete(host);
|
||||||
|
if (entry.release) entry.release();
|
||||||
|
else entry.dispose();
|
||||||
|
},
|
||||||
|
find(host) {
|
||||||
|
const port = rendererOf(host)?.[member];
|
||||||
|
if (port) return port;
|
||||||
|
if (!host || typeof host !== 'object') return null;
|
||||||
|
if (host.isDestroyed?.()) {
|
||||||
|
// Gone with its scene: let the built port go with it.
|
||||||
|
this.drop(host);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let entry = built.get(host);
|
||||||
|
if (!entry && build) {
|
||||||
|
entry = build(host);
|
||||||
|
if (entry) built.set(host, entry);
|
||||||
|
}
|
||||||
|
return entry?.port ?? null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const unboundCameras = unboundPorts('camera');
|
||||||
|
const unboundEffects = unboundPorts('effects');
|
||||||
|
/** Credits ports are `{port, dispose, release}`: see `bindRenderer`. */
|
||||||
|
const unboundCredits = unboundPorts('credits');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provide the factory `cameraOf` builds a camera port with for a scene
|
* Provide the factory `cameraOf` builds a camera port with for a scene
|
||||||
@@ -26,7 +70,7 @@ let buildUnboundCredits = null;
|
|||||||
* @param {(host: object) => ({port: object, dispose: () => void} | null)} build
|
* @param {(host: object) => ({port: object, dispose: () => void} | null)} build
|
||||||
*/
|
*/
|
||||||
export function provideUnboundCamera(build) {
|
export function provideUnboundCamera(build) {
|
||||||
buildUnboundCamera = build;
|
unboundCameras.provide(build);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,27 +81,45 @@ export function provideUnboundCamera(build) {
|
|||||||
* release: () => void} | null)} build
|
* release: () => void} | null)} build
|
||||||
*/
|
*/
|
||||||
export function provideUnboundCredits(build) {
|
export function provideUnboundCredits(build) {
|
||||||
buildUnboundCredits = build;
|
unboundCredits.provide(build);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bind a renderer to its scene object. A camera `cameraOf` built for it
|
* Provide the factory `findEffects` builds an effects port with for a
|
||||||
* while it had no renderer is disposed: its listeners hear nothing more,
|
* scene object no renderer is bound to, as `provideUnboundCamera` does.
|
||||||
* and `cameraOf` answers with the renderer's camera from then on.
|
* @param {(host: object) => ({port: object, dispose: () => void} | null)} build
|
||||||
|
*/
|
||||||
|
export function provideUnboundEffects(build) {
|
||||||
|
unboundEffects.provide(build);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Let go of `port`, if it is the effects port `findEffects` built for
|
||||||
|
* `host`: its passes go and the scene's own settings come back. A
|
||||||
|
* renderer's own port, or one built later, is left alone.
|
||||||
|
* @param {object|null|undefined} host
|
||||||
|
* @param {object|null|undefined} port
|
||||||
|
*/
|
||||||
|
export function releaseUnboundEffects(host, port) {
|
||||||
|
if (host && typeof host === 'object' && port) unboundEffects.drop(host, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind a renderer to its scene object. A port `cameraOf` or `findEffects`
|
||||||
|
* built for it while it had no renderer is disposed: its listeners hear
|
||||||
|
* nothing more, and they answer with the renderer's ports from then on.
|
||||||
|
* A credits port `findCredits` built is released, not disposed: the
|
||||||
|
* credits added through it stay shown until their own Off.
|
||||||
* @param {object} host The scene object callers hold.
|
* @param {object} host The scene object callers hold.
|
||||||
* @param {object} renderer
|
* @param {object} renderer
|
||||||
* @returns {() => void} Unbinds, if the binding is still this one.
|
* @returns {() => void} Unbinds, if the binding is still this one.
|
||||||
*/
|
*/
|
||||||
export function bindRenderer(host, renderer) {
|
export function bindRenderer(host, renderer) {
|
||||||
if (renderers.has(host)) throw new Error('This scene already has a renderer');
|
if (renderers.has(host)) throw new Error('This scene already has a renderer');
|
||||||
// The renderer's camera supersedes one built while there was none.
|
// The renderer's ports supersede ones built while there was none.
|
||||||
unboundCameras.get(host)?.dispose();
|
unboundCameras.drop(host);
|
||||||
unboundCameras.delete(host);
|
unboundEffects.drop(host);
|
||||||
// Its credits port does too for credits added from now on; one built
|
unboundCredits.supersede(host);
|
||||||
// before is released, not disposed: the credits added through it stay
|
|
||||||
// shown until their own Off.
|
|
||||||
unboundCredits.get(host)?.release?.();
|
|
||||||
unboundCredits.delete(host);
|
|
||||||
renderers.set(host, renderer);
|
renderers.set(host, renderer);
|
||||||
return () => {
|
return () => {
|
||||||
if (renderers.get(host) === renderer) renderers.delete(host);
|
if (renderers.get(host) === renderer) renderers.delete(host);
|
||||||
@@ -79,10 +141,20 @@ export const rendererOf = (host) => (host && renderers.get(host)) || null;
|
|||||||
* has no renderer and no camera can be built for it, or is destroyed.
|
* has no renderer and no camera can be built for it, or is destroyed.
|
||||||
*/
|
*/
|
||||||
export function findCamera(host) {
|
export function findCamera(host) {
|
||||||
return (
|
return unboundCameras.find(host);
|
||||||
rendererOf(host)?.camera ??
|
}
|
||||||
findUnbound(host, unboundCameras, buildUnboundCamera)
|
|
||||||
);
|
/**
|
||||||
|
* The effects port of the renderer bound to `host`; with no renderer bound,
|
||||||
|
* one built over `host` by the provided factory and kept with it while
|
||||||
|
* `host` lives.
|
||||||
|
* @param {object|null|undefined} host
|
||||||
|
* @returns {import('./contract/effects').EffectsPort | null} Null when
|
||||||
|
* `host` has no renderer and no effects can be built for it, or is
|
||||||
|
* destroyed.
|
||||||
|
*/
|
||||||
|
export function findEffects(host) {
|
||||||
|
return unboundEffects.find(host);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -93,26 +165,7 @@ export function findCamera(host) {
|
|||||||
* @returns {import('./contract/credits').CreditsPort | null}
|
* @returns {import('./contract/credits').CreditsPort | null}
|
||||||
*/
|
*/
|
||||||
export function findCredits(host) {
|
export function findCredits(host) {
|
||||||
return (
|
return unboundCredits.find(host);
|
||||||
rendererOf(host)?.credits ??
|
|
||||||
findUnbound(host, unboundCredits, buildUnboundCredits)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function findUnbound(host, built, build) {
|
|
||||||
if (!host || typeof host !== 'object') return null;
|
|
||||||
if (host.isDestroyed?.()) {
|
|
||||||
// Gone with its scene: let the built port go with it.
|
|
||||||
built.get(host)?.dispose();
|
|
||||||
built.delete(host);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
let part = built.get(host);
|
|
||||||
if (!part && build) {
|
|
||||||
part = build(host);
|
|
||||||
if (part) built.set(host, part);
|
|
||||||
}
|
|
||||||
return part?.port ?? null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import {
|
|||||||
cameraOf,
|
cameraOf,
|
||||||
findCamera,
|
findCamera,
|
||||||
findCredits,
|
findCredits,
|
||||||
|
findEffects,
|
||||||
provideUnboundCamera,
|
provideUnboundCamera,
|
||||||
provideUnboundCredits,
|
provideUnboundCredits,
|
||||||
|
provideUnboundEffects,
|
||||||
rendererOf,
|
rendererOf,
|
||||||
} from './rendererOf.js';
|
} from './rendererOf.js';
|
||||||
|
|
||||||
@@ -130,3 +132,36 @@ test('with no renderer bound, findCredits builds one port per scene and keeps it
|
|||||||
assert.equal(findCredits(gone), null);
|
assert.equal(findCredits(gone), null);
|
||||||
assert.equal(built.find((entry) => entry.port === port).disposed, 1);
|
assert.equal(built.find((entry) => entry.port === port).disposed, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('with no renderer bound, findEffects builds one effects port per scene until a renderer is bound', (t) => {
|
||||||
|
t.after(() => provideUnboundEffects(null));
|
||||||
|
const built = [];
|
||||||
|
provideUnboundEffects((host) => {
|
||||||
|
if (!host.scene) return null;
|
||||||
|
const entry = { port: { id: built.length }, disposed: 0 };
|
||||||
|
entry.dispose = () => (entry.disposed += 1);
|
||||||
|
built.push(entry);
|
||||||
|
return entry;
|
||||||
|
});
|
||||||
|
const viewer = { scene: {} };
|
||||||
|
assert.equal(findEffects(viewer), built[0].port);
|
||||||
|
assert.equal(findEffects(viewer), built[0].port, 'kept, not rebuilt');
|
||||||
|
assert.equal(findEffects({}), null, 'nothing to build over');
|
||||||
|
assert.equal(findEffects(null), null);
|
||||||
|
const effects = { styles() {} };
|
||||||
|
const unbind = bindRenderer(viewer, { effects });
|
||||||
|
assert.equal(built[0].disposed, 1, 'the renderer supersedes the built port');
|
||||||
|
assert.equal(findEffects(viewer), effects);
|
||||||
|
unbind();
|
||||||
|
assert.equal(
|
||||||
|
findEffects(viewer),
|
||||||
|
built[1].port,
|
||||||
|
'unbound again: a fresh one',
|
||||||
|
);
|
||||||
|
let destroyed = false;
|
||||||
|
const gone = { scene: {}, isDestroyed: () => destroyed };
|
||||||
|
findEffects(gone);
|
||||||
|
destroyed = true;
|
||||||
|
assert.equal(findEffects(gone), null);
|
||||||
|
assert.equal(built[2].disposed, 1, 'let go with its scene');
|
||||||
|
});
|
||||||
@@ -5,6 +5,8 @@
|
|||||||
* provides as `render/cesium/camera.js` loads, so every such entry must load
|
* provides as `render/cesium/camera.js` loads, so every such entry must load
|
||||||
* that module too. It loads `render/cesium/credits.js`, whose factory
|
* that module too. It loads `render/cesium/credits.js`, whose factory
|
||||||
* builds `findCredits`' port for such a viewer the same way.
|
* builds `findCredits`' port for such a viewer the same way.
|
||||||
|
* Likewise an entry that reaches a caller of `findEffects` must load
|
||||||
|
* `render/cesium/effects.js`, which provides the effects factory.
|
||||||
*
|
*
|
||||||
* The graph is every static import, re-export and literal dynamic import of
|
* The graph is every static import, re-export and literal dynamic import of
|
||||||
* the entry's module and those it reaches, read by the parsers the Cesium
|
* the entry's module and those it reaches, read by the parsers the Cesium
|
||||||
@@ -24,26 +26,26 @@ const root = path.resolve(
|
|||||||
);
|
);
|
||||||
const BRIDGE = 'src/render/rendererOf.js';
|
const BRIDGE = 'src/render/rendererOf.js';
|
||||||
const FACTORY = 'src/render/cesium/camera.js';
|
const FACTORY = 'src/render/cesium/camera.js';
|
||||||
|
const EFFECTS_FACTORY = 'src/render/cesium/effects.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entries that take their Cesium code injected: the module named here is
|
* Entries that take their Cesium code injected: the module named here is
|
||||||
* GEV's Cesium side for them, and must load the factory. Given another, the
|
* GEV's Cesium side for them, and must load the factories. Given another, the
|
||||||
* entry's flight finds no camera (`findCamera`) and does nothing.
|
* entry's flight finds no camera (`findCamera`) and does nothing. Visual
|
||||||
|
* settings are given their viewer by the application shell, which loads
|
||||||
|
* both factories; without it, their styles keep records and draw nothing.
|
||||||
*/
|
*/
|
||||||
const INJECTED = {
|
const INJECTED = {
|
||||||
'./layers/recent-imagery': 'src/layers/recentImagery/rendering.js',
|
'./layers/recent-imagery': 'src/layers/recentImagery/rendering.js',
|
||||||
|
'./ui/visual-settings': 'src/ui/applicationShell.js',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modules whose use of the bridge holds up without the factory: with no
|
* Modules whose use of the bridge holds up without the factory: with no
|
||||||
* port they read Cesium's `trackedEntity` instead, which is all an unbound
|
* port they read Cesium's `trackedEntity` instead, which is all an unbound
|
||||||
* viewer can follow by; visual settings ask for the renderer's effects
|
* viewer can follow by.
|
||||||
* only, never a camera, and keep their own records without one.
|
|
||||||
*/
|
*/
|
||||||
const FALLS_BACK = new Set([
|
const FALLS_BACK = new Set(['src/data/followed.js']);
|
||||||
'src/data/followed.js',
|
|
||||||
'src/ui/visualSettings.js',
|
|
||||||
]);
|
|
||||||
|
|
||||||
const pkg = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'));
|
const pkg = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'));
|
||||||
|
|
||||||
@@ -163,3 +165,29 @@ test('every public entry that reaches the renderer bridge loads the unbound came
|
|||||||
`These entries can reach cameraOf without ${FACTORY}; import it where the entry first reaches the bridge.`,
|
`These entries can reach cameraOf without ${FACTORY}; import it where the entry first reaches the bridge.`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('every public entry that reaches a findEffects caller loads the unbound effects factory', () => {
|
||||||
|
const missing = [];
|
||||||
|
let reaching = 0;
|
||||||
|
const callsFindEffects = (file) =>
|
||||||
|
!file.startsWith('src/render/') &&
|
||||||
|
/\.(m?js|vue)$/.test(file) &&
|
||||||
|
readFileSync(path.join(root, file), 'utf8').includes('findEffects(');
|
||||||
|
for (const [key, declaration] of Object.entries(pkg.exports)) {
|
||||||
|
const target = browserTarget(declaration);
|
||||||
|
if (typeof target !== 'string' || !target.startsWith('./src/')) continue;
|
||||||
|
const entry = path.normalize(target.slice(2));
|
||||||
|
if (![...reachable(entry)].some(callsFindEffects)) continue;
|
||||||
|
reaching += 1;
|
||||||
|
const carrier = INJECTED[key];
|
||||||
|
const loads = (file) =>
|
||||||
|
reachable(file, { eager: true }).has(EFFECTS_FACTORY);
|
||||||
|
if (!loads(entry) && !(carrier && loads(carrier))) missing.push(key);
|
||||||
|
}
|
||||||
|
assert.ok(reaching > 0, 'no entry reaches findEffects: the walk is broken');
|
||||||
|
assert.deepEqual(
|
||||||
|
missing,
|
||||||
|
[],
|
||||||
|
`These entries can reach findEffects without ${EFFECTS_FACTORY}; import it where the entry first reaches the lookup.`,
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -32,6 +32,9 @@ import {
|
|||||||
|
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { cameraOf } from '../render/cesium/camera.js';
|
import { cameraOf } from '../render/cesium/camera.js';
|
||||||
|
// Loads the effects factory, so visual settings on a viewer with no renderer
|
||||||
|
// bound still get an effects port.
|
||||||
|
import '../render/cesium/effects.js';
|
||||||
|
|
||||||
import { aircraftTrackingTarget } from '../cockpitTracking.js';
|
import { aircraftTrackingTarget } from '../cockpitTracking.js';
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useDisplayStore } from './stores/display.js';
|
|||||||
import { applicationPinia } from './vueRuntime.js';
|
import { applicationPinia } from './vueRuntime.js';
|
||||||
import { onLocaleChange, t } from '../i18n/index.js';
|
import { onLocaleChange, t } from '../i18n/index.js';
|
||||||
import { UiLifetime } from './uiLifetime.js';
|
import { UiLifetime } from './uiLifetime.js';
|
||||||
import { rendererOf } from '../render/rendererOf.js';
|
import { findEffects, releaseUnboundEffects } from '../render/rendererOf.js';
|
||||||
import {
|
import {
|
||||||
VisualEffects,
|
VisualEffects,
|
||||||
STYLES,
|
STYLES,
|
||||||
@@ -196,7 +196,7 @@ export class VisualSettings {
|
|||||||
setStyleParameter: (name, value) => this.setStyleParameter(name, value),
|
setStyleParameter: (name, value) => this.setStyleParameter(name, value),
|
||||||
});
|
});
|
||||||
this._visualEffects = new VisualEffects({
|
this._visualEffects = new VisualEffects({
|
||||||
effects: rendererOf(viewer)?.effects ?? null,
|
effects: findEffects(viewer),
|
||||||
viewer,
|
viewer,
|
||||||
requestRender: services.governorRequestRender,
|
requestRender: services.governorRequestRender,
|
||||||
holdRender: services.holdContinuousRender,
|
holdRender: services.holdContinuousRender,
|
||||||
@@ -1621,5 +1621,7 @@ export class VisualSettings {
|
|||||||
this.stop();
|
this.stop();
|
||||||
this.releaseIrBoost();
|
this.releaseIrBoost();
|
||||||
this._visualEffects.destroy();
|
this._visualEffects.destroy();
|
||||||
|
// On a viewer of a consumer's own, its bloom comes back as it was.
|
||||||
|
releaseUnboundEffects(this.viewer, this._visualEffects.effects);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user