mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-13 06:42:23 +00:00
Enforce engine SDK boundaries and rehearse every layer display
Restrict runtime SDK imports to their engine directories, including package subpaths, re-exports and dynamic imports. Keep three measured, reasoned exceptions for the Bhote Koshi property-record models and reject stale exceptions. Move the Cesium annotation renderers and both wind adapters to their engine folders, preserving their implementations and lazy loading. Load all 35 manifest display entries on a fake engine with an unknown kind and no native extensions. Draw small fixtures and verify resource and subscription cleanup before destroying the engine. Name the three entries without display factories as skips. Update the architecture, infrastructure lifecycle and engine integration documentation to describe the code today. Cesium behavior is unchanged, and Navara keeps its existing behavior. The weather comparison harness supplies the Navara flow-mesh factory and accepts the previous backend location for comparison runs. No user-visible strings change. Cesium layer-renderer lines fall from 14 to 0, Navara layer-renderer lines from 314 to 189, outside-engine Cesium import files from 5 to 3, viewer lines from 933 to 910, extension reads from 4 to 3, and engine-name lines from 36 to 33. No coupling count grows. Targeted checks pass: 845 tests, three named rehearsal skips. Import directions, package boundaries, formatting and syntax checks pass. Checked in the browser, keyless: the contract suite passes on Navara and on Cesium apart from the camera-events check that also fails intermittently before; Navara annotations pass across engine switches; the ground-look comparison is pixel-identical. qa-draw-tool's hidden-line check and the wind engine switch fail the same way before this change.
This commit is contained in:
1 parent
656943641c
commit
52c6f68bd5
31 files changed
+1145
-208
No files matched your search
@@ -211,7 +211,9 @@ test('draped marks classify onto terrain as well as 3D tiles', () => {
|
||||
// globe carries the imagery and there are no tiles to classify against. The
|
||||
// rendered proof is in scripts/qa-draw-tool.mjs, which counts stroke pixels
|
||||
// against a keyless server; this stops the constant being narrowed again.
|
||||
const renderer = read('src/annotations/worldAnnotationRenderer.js');
|
||||
const renderer = read(
|
||||
'src/globe/cesium/annotations/worldAnnotationRenderer.js',
|
||||
);
|
||||
assert.match(renderer, /const CLASSIFY = Cesium\.ClassificationType\.BOTH;/);
|
||||
assert.doesNotMatch(
|
||||
renderer,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { recordWorkSlice } from '../sources/phaseTiming.js';
|
||||
import { createWorldAnnotationRenderer } from './worldAnnotationRenderer.js';
|
||||
import { createScreenAnnotationRenderer } from './screenAnnotationRenderer.js';
|
||||
import { createWorldAnnotationRenderer } from '../globe/cesium/annotations/worldAnnotationRenderer.js';
|
||||
import { createScreenAnnotationRenderer } from '../globe/cesium/annotations/screenAnnotationRenderer.js';
|
||||
|
||||
/**
|
||||
* Hybrid annotation renderer (Direction C — the recommended production blend).
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createApp, nextTick } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import { useScreenAnnotationsStore } from '../ui/stores/screenAnnotations.js';
|
||||
import ScreenAnnotations from '../ui/views/panels/ScreenAnnotations.vue';
|
||||
import { createScreenAnnotationRenderer } from './screenAnnotationRenderer.js';
|
||||
import { createScreenAnnotationRenderer } from '../globe/cesium/annotations/screenAnnotationRenderer.js';
|
||||
|
||||
// happy-dom does not lay out SVG; every element measures as a 40 x 16 box.
|
||||
for (const type of [globalThis.SVGElement, globalThis.SVGGraphicsElement]) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as C from 'cesium';
|
||||
import {
|
||||
FlowMaterialProperty,
|
||||
createWorldAnnotationRenderer,
|
||||
} from './worldAnnotationRenderer.js';
|
||||
} from '../globe/cesium/annotations/worldAnnotationRenderer.js';
|
||||
|
||||
const noop = () => {};
|
||||
function viewer(t) {
|
||||
|
||||
@@ -0,0 +1,715 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { LAYER_MANIFEST, CONTRACT_PRIMITIVES } from './renderers.js';
|
||||
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { rgbaFromCss } from '../../layers/shared/color.js';
|
||||
import { aircraftAtlasLayout } from '../../layers/aircraft/render/display.js';
|
||||
import { vesselAtlasLayout } from '../../layers/vessels/render/display.js';
|
||||
import { transitTestAtlas } from '../../layers/transit/render/testAtlas.mjs';
|
||||
import { transitIcon } from '../../data/transitIcons.js';
|
||||
import { createBikeshareLayer } from '../../layers/bikeshare/index.js';
|
||||
import { createRadioLayer } from '../../layers/radio/index.js';
|
||||
import { createDisplayHarness } from '../../layers/installations/render/displayHarness.mjs';
|
||||
import { BOX } from '../../layers/recentImagery/testDoubles.mjs';
|
||||
|
||||
const noop = () => {};
|
||||
const settle = () => new Promise((resolve) => setImmediate(resolve));
|
||||
const at = (lon = 0, lat = 0, height = 5) =>
|
||||
geo.cartesian3.fromDegrees(lon, lat, height);
|
||||
const color = rgbaFromCss('#5ef08a');
|
||||
const ring = [
|
||||
[0, 0],
|
||||
[1, 0],
|
||||
[1, 1],
|
||||
[0, 0],
|
||||
];
|
||||
const atlas = (layout) => async () => ({
|
||||
canvas: { width: layout.width, height: layout.height },
|
||||
cells: layout.cells,
|
||||
});
|
||||
const story = () => ({
|
||||
name: 'story',
|
||||
entities: {
|
||||
values: [
|
||||
{
|
||||
id: 'point',
|
||||
position: at(85, 28),
|
||||
point: { color, pixelSize: 8 },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
// Small records from the displays' parity tests; transport and image decoding
|
||||
// are supplied so this rehearsal needs neither a DOM canvas nor a network.
|
||||
const cases = {
|
||||
'bhote-koshi-2026': async (make) => {
|
||||
const d = make();
|
||||
await d.loadImages(BOX, ['pre.webp', 'post.webp'], 'Vantor');
|
||||
d.draw(story());
|
||||
return { hide: () => d.setVisible(false) };
|
||||
},
|
||||
'bhote-koshi-locator': (make) => {
|
||||
const data = story();
|
||||
make({ data });
|
||||
return {
|
||||
hide: () => {
|
||||
data.show = false;
|
||||
},
|
||||
};
|
||||
},
|
||||
flights: async (make) => {
|
||||
const d = make({ drawAtlas: atlas(aircraftAtlasLayout()) });
|
||||
await d.whenReady();
|
||||
d.fleet.attach();
|
||||
d.fleet.add({ id: 'flight', position: at(), show: true, color });
|
||||
return { hide: () => d.fleet.setVisible(false) };
|
||||
},
|
||||
military: async (make) => cases.flights(make),
|
||||
'local-adsb': async (make) => {
|
||||
const d = make({ drawAtlas: atlas(aircraftAtlasLayout()) });
|
||||
await d.whenReady();
|
||||
d.attach();
|
||||
d.setVisible(true);
|
||||
d.markers.add({
|
||||
id: 'local',
|
||||
position: () => at(),
|
||||
rotation: () => 0,
|
||||
show: () => true,
|
||||
});
|
||||
return { hide: () => d.setVisible(false), detach: () => d.detach() };
|
||||
},
|
||||
earthquakes: (make) => {
|
||||
const d = make();
|
||||
d.replace([
|
||||
{
|
||||
stableId: 'quake',
|
||||
lon: 0,
|
||||
lat: 0,
|
||||
baseRadius: 1000,
|
||||
color,
|
||||
fillAlpha: 0.4,
|
||||
},
|
||||
]);
|
||||
d.setVisible(true);
|
||||
return { hide: () => d.setVisible(false) };
|
||||
},
|
||||
'air-quality': (make) => {
|
||||
const d = make();
|
||||
d.replace([{ id: 'station', lon: 0, lat: 0, color: '#5ef08a' }], 1000);
|
||||
d.setVisible(true);
|
||||
return { hide: () => d.setVisible(false) };
|
||||
},
|
||||
'fire-perimeters': (make) => {
|
||||
const d = make();
|
||||
d.replace([{ id: 'area', containedPct: 20, polygons: [[ring]] }]);
|
||||
d.onClick(noop);
|
||||
d.setVisible(true);
|
||||
return { hide: () => d.setVisible(false) };
|
||||
},
|
||||
'drone-airspace': (make) => {
|
||||
const d = make();
|
||||
d.replace([{ id: 'zone', typeCode: 'R', polygons: [[ring]] }]);
|
||||
d.onClick(noop);
|
||||
d.setVisible(true);
|
||||
return { hide: () => d.setVisible(false) };
|
||||
},
|
||||
'gnss-integrity': (make) => {
|
||||
const d = make();
|
||||
d.replace(
|
||||
[{ west: 30, south: 50, east: 30.5, north: 50.5, level: 'low' }],
|
||||
{ low: 0.3 },
|
||||
);
|
||||
d.setVisible(true);
|
||||
return { hide: () => d.setVisible(false) };
|
||||
},
|
||||
's2-ghostmaps': async (make) => {
|
||||
const d = make();
|
||||
const paint = d.draw(
|
||||
'kinetic-30d',
|
||||
new Map([
|
||||
[
|
||||
1,
|
||||
{
|
||||
style: { polyColor: [0, 0, 1, 0.6] },
|
||||
geometries: [{ type: 'polygon', outer: ring, holes: [] }],
|
||||
},
|
||||
],
|
||||
]),
|
||||
'#00d4ff',
|
||||
() => '',
|
||||
);
|
||||
await paint.ready;
|
||||
return { hide: () => paint.destroy() };
|
||||
},
|
||||
'alpr-cameras': (make, engine, services) => {
|
||||
const record = { id: 'alpr:0', latitude: 30, longitude: -97, osmId: 0 };
|
||||
const state = {
|
||||
enabled: true,
|
||||
selectedId: null,
|
||||
lastAnchorSampleAt: 0,
|
||||
renderRevision: 0,
|
||||
records: [record],
|
||||
recordById: new Map([[record.id, record]]),
|
||||
};
|
||||
const d = make({
|
||||
state,
|
||||
services,
|
||||
source: {
|
||||
label: 'Fixture',
|
||||
attribution: {
|
||||
name: 'Fixture cameras',
|
||||
text: 'Fixture cameras',
|
||||
href: 'https://example.com/cameras',
|
||||
},
|
||||
},
|
||||
});
|
||||
engine.camera.setView({ destination: at(-97, 30, 2000) });
|
||||
d.init(null, { onMove: noop });
|
||||
d.setVisible(true);
|
||||
d.renderRecords();
|
||||
return { hide: () => d.setVisible(false) };
|
||||
},
|
||||
'street-level': (make) => {
|
||||
const d = make();
|
||||
const lines = d.sequenceLines(
|
||||
[
|
||||
{
|
||||
id: 'a',
|
||||
parts: [
|
||||
[
|
||||
[-121.49, 38.58],
|
||||
[-121.48, 38.58],
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
{
|
||||
id: 'coverage:14/1/2',
|
||||
color: '#00ff00',
|
||||
alpha: 0.92,
|
||||
widthPx: 2.5,
|
||||
pickPrefix: 'mly:seq:',
|
||||
order: 0,
|
||||
},
|
||||
);
|
||||
return { hide: () => lines.destroy() };
|
||||
},
|
||||
satellites: (make) => {
|
||||
const d = make();
|
||||
const points = d.createPoints();
|
||||
points.add({ id: '1', position: at(), color, pixelSize: 5, show: true });
|
||||
const click = d.listenClick(noop);
|
||||
return {
|
||||
hide: () => {
|
||||
points.show = false;
|
||||
},
|
||||
detach: () => click.destroy(),
|
||||
};
|
||||
},
|
||||
'rocket-launches': (make) => {
|
||||
const d = make();
|
||||
d.setVisible(true);
|
||||
d.addPoint({ id: 'launch', position: at(), sizePx: 8, color });
|
||||
d.onClick(noop);
|
||||
return { hide: () => d.setVisible(false) };
|
||||
},
|
||||
traffic: (make) => {
|
||||
const d = make();
|
||||
d.drawDots([
|
||||
{ point: { position: at(), pixelSize: 5, show: true, color } },
|
||||
]);
|
||||
d.setHeat(
|
||||
'jam',
|
||||
[
|
||||
{
|
||||
coords: [
|
||||
[0, 0],
|
||||
[1, 1],
|
||||
],
|
||||
},
|
||||
],
|
||||
{ color: color.rgba(), widthPx: 3 },
|
||||
);
|
||||
return { hide: () => d.show(false) };
|
||||
},
|
||||
cctv: (make) => {
|
||||
const d = make({
|
||||
state: { _recordById: new Map(), _projectionEntities: [] },
|
||||
});
|
||||
d.attach();
|
||||
d.createMarkers();
|
||||
const marker = d.createMarker({ id: 'a' }, at());
|
||||
marker.show = true;
|
||||
d.bindClick(noop);
|
||||
return { hide: () => d.setMarkersVisible(false) };
|
||||
},
|
||||
radio: (make, engine, services) => {
|
||||
let context;
|
||||
const layer = createRadioLayer({
|
||||
services,
|
||||
source: {},
|
||||
rendererFactory(value) {
|
||||
context = value;
|
||||
return make(value);
|
||||
},
|
||||
});
|
||||
layer.attachRenderer(null, { engine });
|
||||
const renderer = context.parts.renderer;
|
||||
const station = {
|
||||
id: '00000000-0000-4000-8000-000000000000',
|
||||
name: 'Station 0',
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
tags: ['news'],
|
||||
};
|
||||
context.state._stationById.set(station.id, station);
|
||||
context.state._renderById.set(
|
||||
station.id,
|
||||
renderer.addStation(station, at(), '#44adff'),
|
||||
);
|
||||
renderer.setVisible(true);
|
||||
return {
|
||||
hide: () => renderer.setVisible(false),
|
||||
destroy: () => layer.detachRenderer(),
|
||||
};
|
||||
},
|
||||
transit: async (make) => {
|
||||
const d = make({ state: {}, services: {}, drawAtlas: transitTestAtlas });
|
||||
d.attach();
|
||||
const [markers] = d.createMarkers();
|
||||
markers.show = true;
|
||||
await d.whenReady();
|
||||
markers.add({
|
||||
id: 'bus',
|
||||
position: at(),
|
||||
image: transitIcon('bus'),
|
||||
width: 20,
|
||||
height: 20,
|
||||
color,
|
||||
rotation: 0,
|
||||
show: true,
|
||||
});
|
||||
return {
|
||||
hide: () => {
|
||||
markers.show = false;
|
||||
},
|
||||
};
|
||||
},
|
||||
bikeshare: (make, engine, services) => {
|
||||
let display;
|
||||
const layer = createBikeshareLayer({
|
||||
services,
|
||||
source: { getStations: async () => ({ data: { stations: [] } }) },
|
||||
rendererFactory(value) {
|
||||
display = make(value);
|
||||
return display;
|
||||
},
|
||||
});
|
||||
layer.init(null, { engine });
|
||||
display.lifecycle.methods.show();
|
||||
display.rendering.ensureCityPoints(
|
||||
'austin-capmetro',
|
||||
new Map([
|
||||
[
|
||||
'central',
|
||||
{
|
||||
stationId: 'central',
|
||||
name: 'Central',
|
||||
lat: 30.2672,
|
||||
lon: -97.7431,
|
||||
capacity: 10,
|
||||
},
|
||||
],
|
||||
]),
|
||||
);
|
||||
return { hide: () => layer.disable(), destroy: () => layer.destroy() };
|
||||
},
|
||||
directions: (make) => {
|
||||
const d = make();
|
||||
d.setMarker('A', { lat: 0, lon: 0 });
|
||||
d.setRoute({
|
||||
type: 'LineString',
|
||||
coordinates: [
|
||||
[0, 0],
|
||||
[1, 1],
|
||||
],
|
||||
});
|
||||
const off = d.onClick(noop);
|
||||
d.show(true);
|
||||
return { hide: () => d.show(false), detach: off };
|
||||
},
|
||||
'recent-imagery': (make) => {
|
||||
const d = make();
|
||||
d.rebind(d.resolveHost());
|
||||
assert.equal(
|
||||
d.showSlot(
|
||||
'a',
|
||||
{ key: 'S30:2026-09-18', product: 'S30', day: '2026-09-18' },
|
||||
BOX,
|
||||
),
|
||||
true,
|
||||
);
|
||||
return { hide: () => d.hideSlot('a') };
|
||||
},
|
||||
'osm-places': (make) => {
|
||||
const d = make();
|
||||
d.replace([{ id: 'node/1', lat: 0, lon: 0, color: '#5ef08a' }]);
|
||||
d.setVisible(true);
|
||||
return { hide: () => d.setVisible(false) };
|
||||
},
|
||||
'ais-live-vessels': async (make) => {
|
||||
const d = make({ drawAtlas: atlas(vesselAtlasLayout()) });
|
||||
await d.whenReady();
|
||||
d.fleet.attach({ show: true });
|
||||
d.fleet.add({
|
||||
id: { mmsi: '123456789' },
|
||||
position: at(),
|
||||
show: true,
|
||||
scale: 0.6,
|
||||
rotation: 0,
|
||||
});
|
||||
return { hide: () => d.fleet.setVisible(false) };
|
||||
},
|
||||
'military-installations': (make, engine, services) => {
|
||||
const h = createDisplayHarness(() => ({ engine, destroy: noop }), {
|
||||
context: services.context,
|
||||
displayFactory: (_engine, options) => make(options),
|
||||
});
|
||||
h.start();
|
||||
h.setRecords([
|
||||
{
|
||||
id: 'site',
|
||||
name: 'Site',
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
category: 'military',
|
||||
sources: [],
|
||||
},
|
||||
]);
|
||||
h.parts.rendering.renderRecords();
|
||||
return {
|
||||
hide: () => h.parts.lifecycle.methods.disable(),
|
||||
destroy: () => h.destroy(),
|
||||
};
|
||||
},
|
||||
'military-awareness': (make) => {
|
||||
const d = make();
|
||||
const rim = d.createRim(at());
|
||||
const pulses = d.createPulses(at());
|
||||
pulses.draw({ radii: [0.5], alphas: [0.5], level: 1 });
|
||||
return {
|
||||
hide: () => pulses.draw({ radii: [], alphas: [], level: 0 }),
|
||||
detach: () => {
|
||||
rim.destroy();
|
||||
pulses.destroy();
|
||||
},
|
||||
};
|
||||
},
|
||||
wind: (make) => {
|
||||
const d = make({
|
||||
createCanvas: ({ width, height }) => ({ width, height }),
|
||||
});
|
||||
d.attach();
|
||||
d.setField({
|
||||
model: 'gfs',
|
||||
grid: { nx: 8, ny: 5, lo1: 0, la1: 90, dx: 45, dy: 45 },
|
||||
u: Array.from({ length: 40 }, (_, i) => 5 + (i % 7)),
|
||||
v: Array.from({ length: 40 }, (_, i) => (i % 3) - 1),
|
||||
});
|
||||
d.setOptions({ overlay: 'speed' });
|
||||
d.start();
|
||||
return { hide: () => d.stop() };
|
||||
},
|
||||
'weather-radar': async (make) => weather(make, 'radar'),
|
||||
'weather-satellite': async (make) => weather(make, 'clouds-regional'),
|
||||
'weather-lightning': async (make) => weather(make, 'lightning'),
|
||||
'weather-cyclones': async (make, engine, services) => {
|
||||
const d = make({ overlayHost: services.overlayHost });
|
||||
await d.setSnapshot({
|
||||
storms: [
|
||||
{
|
||||
id: 'ep152026',
|
||||
name: 'Fifteen-E',
|
||||
position: { longitude: 179, latitude: 15 },
|
||||
forecastPoints: [],
|
||||
track: {
|
||||
type: 'MultiLineString',
|
||||
coordinates: [
|
||||
[
|
||||
[179, 15],
|
||||
[-179, 16],
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
return { hide: () => d.setSnapshot({ storms: [] }) };
|
||||
},
|
||||
'local-datacenters': (make, engine, services) =>
|
||||
infrastructure(make, services, 'local-datacenters'),
|
||||
'local-dams': (make, engine, services) =>
|
||||
infrastructure(make, services, 'local-dams'),
|
||||
'telegeography-submarine-cables': (make, engine, services) => {
|
||||
const d = make({ overlayHost: services.overlayHost });
|
||||
d.show({
|
||||
cables: {
|
||||
features: [
|
||||
{
|
||||
type: 'Feature',
|
||||
id: 'cable',
|
||||
properties: { name: 'Cable' },
|
||||
geometry: {
|
||||
type: 'LineString',
|
||||
coordinates: [
|
||||
[0, 0],
|
||||
[1, 1],
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
landingPoints: { features: [] },
|
||||
});
|
||||
return { hide: () => d.hide() };
|
||||
},
|
||||
'local-firms': (make) => {
|
||||
const d = make();
|
||||
d.addCell({ west: 30, south: 50, east: 30.5, north: 50.5, color });
|
||||
d.setVisible(true);
|
||||
return { hide: () => d.setVisible(false) };
|
||||
},
|
||||
};
|
||||
|
||||
async function weather(make, product) {
|
||||
const d = make();
|
||||
const time = '2026-11-07T12:00:00.000Z';
|
||||
assert.equal(
|
||||
await d.setFrame(
|
||||
{
|
||||
schemaVersion: 1,
|
||||
product,
|
||||
bounds: { west: -130, south: 20, east: -60, north: 55 },
|
||||
times: [time],
|
||||
latest: time,
|
||||
tileSize: 256,
|
||||
maxLevel: 6,
|
||||
tilingScheme: 'geographic',
|
||||
},
|
||||
time,
|
||||
),
|
||||
true,
|
||||
);
|
||||
return { hide: () => d.setHidden(true), destroy: () => d.clear() };
|
||||
}
|
||||
|
||||
function infrastructure(make, services, id) {
|
||||
const d = make({
|
||||
config: {
|
||||
id,
|
||||
name: 'Runtime Dam',
|
||||
source: 'fixture',
|
||||
color: '#5ef08a',
|
||||
labels: false,
|
||||
},
|
||||
services,
|
||||
});
|
||||
const geometry = { type: 'Polygon', coordinates: [ring] };
|
||||
d.show({
|
||||
features: [
|
||||
{
|
||||
type: 'Feature',
|
||||
id: 'real-dam',
|
||||
properties: { name: 'Runtime Dam' },
|
||||
geometry,
|
||||
},
|
||||
],
|
||||
records: [
|
||||
{
|
||||
id: 'real-dam',
|
||||
lon: 0.5,
|
||||
lat: 0.5,
|
||||
properties: { name: 'Runtime Dam' },
|
||||
geometry,
|
||||
},
|
||||
],
|
||||
});
|
||||
return { hide: () => d.hide() };
|
||||
}
|
||||
|
||||
const skips = {
|
||||
'wildlife-sightings':
|
||||
'The layer draws itself through the contract; no manifest display factory.',
|
||||
'sea-marks':
|
||||
'The overlay controller draws imagery itself; no manifest display factory.',
|
||||
'ski-pistes':
|
||||
'The overlay controller draws imagery itself; no manifest display factory.',
|
||||
};
|
||||
|
||||
function listenerCounts(engine) {
|
||||
return [engine.camera, engine.frames].flatMap((part) =>
|
||||
Object.values(part)
|
||||
.filter((value) => value && typeof value.addEventListener === 'function')
|
||||
.map((event) => event.numberOfListeners),
|
||||
);
|
||||
}
|
||||
|
||||
function servicesFixture() {
|
||||
const entries = new Map();
|
||||
const contexts = new Map();
|
||||
const owners = new Map();
|
||||
const overlayHost = {
|
||||
setEntries: (id, value) => entries.set(id, value),
|
||||
setVisible: noop,
|
||||
clearSource: (id) => entries.delete(id),
|
||||
};
|
||||
const context = {
|
||||
getSelectedEntityContext: () => null,
|
||||
clearSelectedEntityContextForLayer: noop,
|
||||
selectEntityContext: noop,
|
||||
registerEntityContext: (_entity, metadata) =>
|
||||
contexts.set(metadata.id, metadata),
|
||||
removeEntityContextsForLayer: (id) => {
|
||||
for (const [key, value] of contexts)
|
||||
if (value.layerId === id) contexts.delete(key);
|
||||
},
|
||||
};
|
||||
return {
|
||||
entries,
|
||||
contexts,
|
||||
owners,
|
||||
context,
|
||||
overlayHost,
|
||||
render: { governorRequestRender: noop },
|
||||
groundFloor: { cachedGroundFloor: () => 150 },
|
||||
ground: { cachedGroundFloor: () => 20, warmGroundFloor: noop },
|
||||
globe: { getKeyholeGeometry: () => ({ radius: 500 }) },
|
||||
overlays: {
|
||||
setOverlayEntries: overlayHost.setEntries,
|
||||
clearOverlaySource: overlayHost.clearSource,
|
||||
setOverlaySourceVisible: noop,
|
||||
},
|
||||
picking: {
|
||||
registerPickOwner: (id, fn) => owners.set(id, fn),
|
||||
unregisterPickOwner: (id) => owners.delete(id),
|
||||
resolvePickId: (pick) => pick?.id,
|
||||
isOwnedByOtherLayer: () => false,
|
||||
},
|
||||
...context,
|
||||
};
|
||||
}
|
||||
|
||||
test('every manifest display draws and releases its resources on a third engine', async (t) => {
|
||||
assert.deepEqual(
|
||||
Object.keys(cases).sort(),
|
||||
Object.entries(LAYER_MANIFEST)
|
||||
.filter(([, entry]) => entry.display)
|
||||
.map(([id]) => id)
|
||||
.sort(),
|
||||
);
|
||||
assert.deepEqual(
|
||||
Object.keys(skips).sort(),
|
||||
Object.entries(LAYER_MANIFEST)
|
||||
.filter(([, entry]) => !entry.display)
|
||||
.map(([id]) => id)
|
||||
.sort(),
|
||||
);
|
||||
const previousWindow = globalThis.window;
|
||||
globalThis.window = new EventTarget();
|
||||
t.after(() => {
|
||||
if (previousWindow === undefined) delete globalThis.window;
|
||||
else globalThis.window = previousWindow;
|
||||
});
|
||||
for (const [id, entry] of Object.entries(LAYER_MANIFEST)) {
|
||||
await t.test(id, { skip: skips[id] }, async () => {
|
||||
const engine = createFakeEngine({ kind: 'rehearsal' });
|
||||
engine.extensions = {};
|
||||
engine.fake.script('sampleHeight', 0);
|
||||
engine.fake.script('terrainHeight', 0);
|
||||
const before = listenerCounts(engine);
|
||||
const credits = [...engine.fake.credits];
|
||||
const inputs = new Set();
|
||||
const on = engine.input.on;
|
||||
engine.input.on = (...args) => {
|
||||
const off = on(...args);
|
||||
const token = {};
|
||||
inputs.add(token);
|
||||
return () => {
|
||||
inputs.delete(token);
|
||||
off();
|
||||
};
|
||||
};
|
||||
const services = servicesFixture();
|
||||
const factory = await entry.display();
|
||||
let display;
|
||||
let lifecycle;
|
||||
const make = (options = {}) => {
|
||||
assert.equal(display, undefined, 'one factory call per manifest entry');
|
||||
return (display = factory(engine, options));
|
||||
};
|
||||
try {
|
||||
lifecycle = (await cases[id](make, engine, services)) ?? {};
|
||||
await settle();
|
||||
engine.fake.frame();
|
||||
assert.ok(
|
||||
CONTRACT_PRIMITIVES.some((name) =>
|
||||
engine[name]
|
||||
.list()
|
||||
.some(
|
||||
(item) =>
|
||||
item.count > 0 ||
|
||||
name === 'imagery' ||
|
||||
name === 'lines' ||
|
||||
item.kind === 'pulse-field',
|
||||
),
|
||||
),
|
||||
'the fixture draws nonempty geometry or imagery',
|
||||
);
|
||||
lifecycle.hide?.();
|
||||
lifecycle.detach?.();
|
||||
if (lifecycle.destroy) lifecycle.destroy();
|
||||
else display.destroy();
|
||||
await settle();
|
||||
engine.fake.frame();
|
||||
for (const name of CONTRACT_PRIMITIVES)
|
||||
assert.deepEqual(
|
||||
engine[name].list(),
|
||||
[],
|
||||
`${name} released before engine destruction`,
|
||||
);
|
||||
assert.deepEqual(
|
||||
listenerCounts(engine),
|
||||
before,
|
||||
'frame and camera listeners released',
|
||||
);
|
||||
assert.equal(inputs.size, 0, 'input subscriptions released');
|
||||
assert.deepEqual(engine.fake.credits, credits, 'credits released');
|
||||
assert.equal(
|
||||
engine.input.cameraInputsEnabled(),
|
||||
true,
|
||||
'camera input restored',
|
||||
);
|
||||
assert.equal(
|
||||
engine.render.isContinuous(),
|
||||
false,
|
||||
'continuous rendering released',
|
||||
);
|
||||
assert.equal(engine.camera.following(), null, 'no retained follow');
|
||||
assert.ok(
|
||||
[...services.entries.values()].every(
|
||||
(entries) => entries.length === 0,
|
||||
),
|
||||
'overlay entries released',
|
||||
);
|
||||
assert.equal(services.contexts.size, 0, 'context records released');
|
||||
assert.equal(services.owners.size, 0, 'pick owners released');
|
||||
} finally {
|
||||
lifecycle?.destroy?.();
|
||||
display?.destroy?.();
|
||||
engine.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -15,7 +15,7 @@
|
||||
// default); the entry's `display` is its loader. `backends` is the
|
||||
// display's engine-specific option, keyed by engine kind: an import()
|
||||
// loader of `{ option: value }` merged into the display's options on that
|
||||
// engine (Global Context's pulses on Navara).
|
||||
// engine (wind streamlines).
|
||||
// - `drawsItself(needs)`: no renderer; the layer draws itself through the
|
||||
// contract primitives in `needs` (Wildlife Sightings).
|
||||
// - `perEngine({ [kind]: loader | DRAWS_ITSELF })`: a layer with no display
|
||||
@@ -122,12 +122,12 @@ const navaraNative = (engine) => engine.extensions.navara;
|
||||
// Wind streamlines retain their engine backends.
|
||||
const cesiumWindFlow = async () => {
|
||||
const { createWindCesiumFlow } =
|
||||
await import('../../layers/wind/render/cesiumFlow.js');
|
||||
await import('../../globe/cesium/windFlow.js');
|
||||
return { flow: createWindCesiumFlow };
|
||||
};
|
||||
const navaraWindFlow = async () => {
|
||||
const [{ createWindNavaraFlow }, { createWindFlowMesh }] = await Promise.all([
|
||||
import('../../layers/wind/render/navaraFlow.js'),
|
||||
import('../../globe/navara/windFlow.js'),
|
||||
import('../../globe/navara/windFlowMesh.js'),
|
||||
]);
|
||||
return {
|
||||
@@ -194,8 +194,8 @@ export const LAYER_MANIFEST = Object.freeze({
|
||||
'weather-satellite': display('createWeatherDisplay', ['imagery']),
|
||||
'weather-lightning': display('createWeatherDisplay', ['imagery']),
|
||||
'weather-cyclones': display('createCyclonesDisplay', ['movers', 'ground']),
|
||||
// The open map overlays drape themselves: Cesium imagery on the viewer,
|
||||
// engine.maps.addOverlay on Navara (src/data/osmOverlays.js).
|
||||
// The open map overlays draw through imagery, or the optional native
|
||||
// maps.addOverlay path (src/data/osmOverlays.js).
|
||||
'sea-marks': perEngine({ cesium: DRAWS_ITSELF, navara: DRAWS_ITSELF }),
|
||||
'ski-pistes': perEngine({ cesium: DRAWS_ITSELF, navara: DRAWS_ITSELF }),
|
||||
'local-datacenters': display('createInfrastructureDisplay', [
|
||||
|
||||
@@ -8,7 +8,7 @@ import { readFileSync } from 'node:fs';
|
||||
import * as Cesium from 'cesium';
|
||||
import { FEATURE_SOURCE_METHODS } from '../sources/featureSource.js';
|
||||
import { createAnnotationResolver } from '../annotations/resolver.js';
|
||||
import { createWorldAnnotationRenderer } from '../annotations/worldAnnotationRenderer.js';
|
||||
import { createWorldAnnotationRenderer } from '../globe/cesium/annotations/worldAnnotationRenderer.js';
|
||||
import { cameraOnlyEngine } from '../testSupport/viewerEngine.mjs';
|
||||
|
||||
const read = (path) => readFileSync(new URL(path, import.meta.url), 'utf8');
|
||||
@@ -25,12 +25,14 @@ const DATA_MODULES = [
|
||||
/** Modules that use Cesium for outlines but must not draw text with it. */
|
||||
const CESIUM_MODULES = [
|
||||
'../annotations/resolver.js',
|
||||
'../annotations/worldAnnotationRenderer.js',
|
||||
'../globe/cesium/annotations/worldAnnotationRenderer.js',
|
||||
'../annotations/hybridAnnotationRenderer.js',
|
||||
];
|
||||
|
||||
/** The DOM overlay: callout elements, never Cesium label primitives. */
|
||||
const OVERLAY_MODULES = ['../annotations/screenAnnotationRenderer.js'];
|
||||
const OVERLAY_MODULES = [
|
||||
'../globe/cesium/annotations/screenAnnotationRenderer.js',
|
||||
];
|
||||
|
||||
const LABEL_PRIMITIVES = [
|
||||
/\bLabelCollection\b/,
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as C from 'cesium';
|
||||
import { showOsmCredit, hideOsmCredit } from './dataCredits.js';
|
||||
import { createInfrastructureLayers } from './infrastructure.js';
|
||||
import { createBhoteKoshiLocatorLayer } from './bhoteKoshiLocator.js';
|
||||
import { createWorldAnnotationRenderer } from '../annotations/worldAnnotationRenderer.js';
|
||||
import { createWorldAnnotationRenderer } from '../globe/cesium/annotations/worldAnnotationRenderer.js';
|
||||
import { createDirectionsLayer } from '../layers/directions/index.js';
|
||||
import { createDirectionsSource } from '../layers/directions/source.js';
|
||||
import { createLifecycle as createCctvLifecycle } from '../layers/cctv/lifecycle.js';
|
||||
|
||||
+7
-7
@@ -1,15 +1,15 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../geo/index.js';
|
||||
import { getOverlayPaintRect } from '../overlays/worldOverlay.js';
|
||||
import * as geo from '../../../geo/index.js';
|
||||
import { getOverlayPaintRect } from '../../../overlays/worldOverlay.js';
|
||||
import {
|
||||
getActiveTrackedReadoutId,
|
||||
TRACKED_OVERLAY_SOURCE_ID,
|
||||
} from '../data/trackedReadout.js';
|
||||
import { t } from '../i18n/index.js';
|
||||
} from '../../../data/trackedReadout.js';
|
||||
import { t } from '../../../i18n/index.js';
|
||||
import { nextTick } from 'vue';
|
||||
import { useScreenAnnotationsStore } from '../ui/stores/screenAnnotations.js';
|
||||
import { applicationPinia } from '../ui/vueRuntime.js';
|
||||
import { isScreenScene } from './screenScene.js';
|
||||
import { useScreenAnnotationsStore } from '../../../ui/stores/screenAnnotations.js';
|
||||
import { applicationPinia } from '../../../ui/vueRuntime.js';
|
||||
import { isScreenScene } from '../../../annotations/screenScene.js';
|
||||
|
||||
/** A label whose detailed outline could not be resolved. */
|
||||
function outlineUnavailableLabel(label) {
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
import { recordWorkSlice } from '../sources/phaseTiming.js';
|
||||
import { ringCentroid } from '../sources/featureGeometry.js';
|
||||
import { recordWorkSlice } from '../../../sources/phaseTiming.js';
|
||||
import { ringCentroid } from '../../../sources/featureGeometry.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import { showOsmCredit, hideOsmCredit } from '../data/dataCredits.js';
|
||||
import { showOsmCredit, hideOsmCredit } from '../../../data/dataCredits.js';
|
||||
|
||||
/**
|
||||
* World-space annotation renderer (Direction A).
|
||||
@@ -69,7 +69,7 @@ const colorOf = (rgba) => new Cesium.Color(rgba[0], rgba[1], rgba[2], rgba[3]);
|
||||
|
||||
// One material for every animated corridor: PolylineGlow's falloff across
|
||||
// the width (st.t) and taper along it (st.s), an alpha pulse, and the
|
||||
// annotation route's flowing dashes (src/annotations/worldAnnotationRenderer.js,
|
||||
// annotation route's flowing dashes (src/globe/cesium/annotations/worldAnnotationRenderer.js,
|
||||
// GevRouteFlow): its diffuse plus emission and alpha. Each part is off at
|
||||
// its zero uniform. Fragments are LDR and premultiplied, as Cesium writes
|
||||
// ground polylines.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Cesium's backend of the wind display: the wind renderer's streamlines
|
||||
// (../rendering.js) over the viewer, read only once a Cesium globe draws.
|
||||
// (src/layers/wind/rendering.js) over the viewer, read only once a Cesium globe draws.
|
||||
import * as Cesium from 'cesium';
|
||||
import { createWindRendering } from '../rendering.js';
|
||||
import { createWindRendering } from '../../layers/wind/rendering.js';
|
||||
|
||||
export function createWindCesiumFlow(engine, { onStatusChange } = {}) {
|
||||
return createWindRendering({
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
WIND_DISPLAY_HEIGHT_METERS,
|
||||
WIND_NARROW_PATH_LIMIT,
|
||||
WIND_PATH_LIMIT,
|
||||
} from '../streamlines.js';
|
||||
import { sameWind } from '../model.js';
|
||||
} from '../../layers/wind/streamlines.js';
|
||||
import { sameWind } from '../../layers/wind/model.js';
|
||||
|
||||
const clamp = (n) => Math.max(0, Math.min(1, n));
|
||||
|
||||
@@ -11,7 +11,7 @@ const rgba = (css, alpha) => {
|
||||
return [red, green, blue, alpha];
|
||||
};
|
||||
|
||||
// The route flow material (src/annotations/worldAnnotationRenderer.js): the
|
||||
// The route flow material (src/globe/cesium/annotations/worldAnnotationRenderer.js): the
|
||||
// route colour at 0.95, 64 dash cells along the route, each lit for its
|
||||
// first 46 % (brightened by 0.9 of its colour) and at 18 % alpha between,
|
||||
// flowing toward the destination at 0.55 cells a second.
|
||||
|
||||
@@ -31,7 +31,12 @@ function cesiumCollection(viewer, id) {
|
||||
*/
|
||||
export function createDisplayHarness(
|
||||
makeEngine,
|
||||
{ context = contextStore, overlayHost = null, warmFloors = null } = {},
|
||||
{
|
||||
context = contextStore,
|
||||
overlayHost = null,
|
||||
warmFloors = null,
|
||||
displayFactory = createInstallationsDisplay,
|
||||
} = {},
|
||||
) {
|
||||
// The context store lives on the window.
|
||||
const ownWindow = globalThis.window === undefined;
|
||||
@@ -90,7 +95,7 @@ export function createDisplayHarness(
|
||||
clearUnavailableRetry() {},
|
||||
};
|
||||
parts.ingestion = { setInstallationStatus() {} };
|
||||
Object.assign(parts, createInstallationsDisplay(h.engine, ctx));
|
||||
Object.assign(parts, displayFactory(h.engine, ctx));
|
||||
const methods = parts.lifecycle.methods;
|
||||
return {
|
||||
...h,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// carry the wind near the ground). The streamlines are the engine's own,
|
||||
// behind one seam: the `flow` backend the application passes in for each
|
||||
// engine (Cesium's wind renderer, rendering.js, on the page's SDK;
|
||||
// Navara's flow mesh, render/navaraFlow.js). Without one the field draws
|
||||
// Navara's flow mesh, src/globe/navara/windFlow.js). Without one the field draws
|
||||
// alone.
|
||||
|
||||
import { createFieldRaster, fieldUnavailableText } from '../fields.js';
|
||||
|
||||
@@ -53,7 +53,7 @@ test('portable sources can share records, and standalone can assemble rendering'
|
||||
write('src/layers/demo/source.js', "export { read } from './records.js';");
|
||||
write('src/layers/demo/records.js', 'export const read = (value) => value;');
|
||||
write('src/standalone/catalog.js', "import '../layers/demo/index.js';");
|
||||
write('src/layers/demo/index.js', "import * as Cesium from 'cesium';");
|
||||
write('src/layers/demo/index.js');
|
||||
assert.equal(checkImportDirections(root).modules, 5);
|
||||
});
|
||||
|
||||
@@ -83,34 +83,34 @@ for (const [name, files, pattern] of [
|
||||
/reaches browser globals/,
|
||||
],
|
||||
[
|
||||
'W1 shared focusDeemphasis keeps native imports in render',
|
||||
'W1 shared focusDeemphasis keeps SDK imports in the engine',
|
||||
{ 'src/data/focusDeemphasis.js': "import 'cesium';" },
|
||||
/cesium only in the layer's render/,
|
||||
/cesium imports belong in src\/globe\/cesium\//,
|
||||
],
|
||||
[
|
||||
'W1 shared trailRenderer keeps native imports in render',
|
||||
'W1 shared trailRenderer keeps SDK imports in the engine',
|
||||
{ 'src/data/trailRenderer.js': "import 'cesium';" },
|
||||
/cesium only in the layer's render/,
|
||||
/cesium imports belong in src\/globe\/cesium\//,
|
||||
],
|
||||
[
|
||||
'W1 shared contactTrailRenderer keeps native imports in render',
|
||||
'W1 shared contactTrailRenderer keeps SDK imports in the engine',
|
||||
{ 'src/data/contactTrailRenderer.js': "import 'cesium';" },
|
||||
/cesium only in the layer's render/,
|
||||
/cesium imports belong in src\/globe\/cesium\//,
|
||||
],
|
||||
[
|
||||
'W1 shared iconOrientation keeps native imports in render',
|
||||
'W1 shared iconOrientation keeps SDK imports in the engine',
|
||||
{ 'src/data/iconOrientation.js': "import 'cesium';" },
|
||||
/cesium only in the layer's render/,
|
||||
/cesium imports belong in src\/globe\/cesium\//,
|
||||
],
|
||||
[
|
||||
'W1 shared modelVisualAnchor keeps native imports in render',
|
||||
'W1 shared modelVisualAnchor keeps SDK imports in the engine',
|
||||
{ 'src/data/modelVisualAnchor.js': "import 'cesium';" },
|
||||
/cesium only in the layer's render/,
|
||||
/cesium imports belong in src\/globe\/cesium\//,
|
||||
],
|
||||
[
|
||||
'W1 shared trackingClickGesture keeps native imports in render',
|
||||
'W1 shared trackingClickGesture keeps SDK imports in the engine',
|
||||
{ 'src/data/trackingClickGesture.js': "import 'cesium';" },
|
||||
/cesium only in the layer's render/,
|
||||
/cesium imports belong in src\/globe\/cesium\//,
|
||||
],
|
||||
[
|
||||
'geo module imports cesium',
|
||||
@@ -326,41 +326,123 @@ test('the app reaches the Navara engine only through a dynamic import', (t) => {
|
||||
);
|
||||
});
|
||||
|
||||
test('layers moved onto renderers keep each engine in its render file', (t) => {
|
||||
const { root, write } = fixture(t);
|
||||
write('src/layers/flights/rendering.js', "import './render/cesium.js';");
|
||||
write('src/layers/flights/render/cesium.js', "import 'cesium';");
|
||||
write(
|
||||
'src/layers/flights/render/navara.js',
|
||||
"import 'three'; import '@navaramap/three';",
|
||||
);
|
||||
write('src/layers/aircraft/render/cesium.js', "import 'cesium';");
|
||||
write('src/layers/bikeshare/render/cesium/lifecycle.js', "import 'cesium';");
|
||||
write('src/layers/flights/render/cesium.test.mjs', "import 'cesium';");
|
||||
write(
|
||||
'src/app/navaraMode.js',
|
||||
"export const load = () => import('../layers/flights/render/navara.js');",
|
||||
);
|
||||
// A layer not moved yet still imports cesium anywhere (until its *a task).
|
||||
write('src/layers/wind/rendering.js', "import 'cesium';");
|
||||
assert.doesNotThrow(() => checkImportDirections(root));
|
||||
for (const specifier of [
|
||||
'cesium',
|
||||
'cesium/Source/Cesium.js',
|
||||
'@cesium/engine',
|
||||
'three',
|
||||
'three/addons/controls.js',
|
||||
'postprocessing',
|
||||
'@navaramap/three',
|
||||
]) {
|
||||
for (const statement of [
|
||||
`import '${specifier}';`,
|
||||
`export * from '${specifier}';`,
|
||||
`const load = () => import('${specifier}');`,
|
||||
]) {
|
||||
test(`SDK ownership rejects ${statement} outside an engine`, (t) => {
|
||||
const { root, write } = fixture(t);
|
||||
write('src/layers/new/render/display.js', statement);
|
||||
assert.throws(
|
||||
() => checkImportDirections(root),
|
||||
/imports belong in src\/globe\//,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
write('src/layers/flights/tracking.js', "import 'cesium';");
|
||||
assert.throws(
|
||||
() => checkImportDirections(root),
|
||||
/src\/layers\/flights\/tracking\.js: cesium only in the layer's render\/cesium\*\.js: cesium/,
|
||||
test('legacy renderer names grant no SDK import exemption', (t) => {
|
||||
const { root, write } = fixture(t);
|
||||
write('src/layers/flights/render/cesium.js', "import 'cesium';");
|
||||
write('src/layers/flights/render/navara.js', "import 'three';");
|
||||
assert.throws(() => checkImportDirections(root), /cesium imports belong/);
|
||||
write('src/layers/flights/render/cesium.js');
|
||||
assert.throws(() => checkImportDirections(root), /three imports belong/);
|
||||
});
|
||||
|
||||
test('remaining Navara layer helpers still load through the manifest or mode', (t) => {
|
||||
const { root, write } = fixture(t);
|
||||
write('src/layers/shared/render/navaraPolygons.js');
|
||||
write(
|
||||
'src/app/layers/renderers.js',
|
||||
"export const load = () => import('../../layers/shared/render/navaraPolygons.js');",
|
||||
);
|
||||
write('src/layers/flights/tracking.js', '');
|
||||
write('src/layers/aircraft/render/cesium.js', "import 'three';");
|
||||
assert.throws(
|
||||
() => checkImportDirections(root),
|
||||
/src\/layers\/aircraft\/render\/cesium\.js: three and Navara only in the layer's render\/navara\*\.js: three/,
|
||||
assert.doesNotThrow(() => checkImportDirections(root));
|
||||
write(
|
||||
'src/app/data.js',
|
||||
"import '../layers/shared/render/navaraPolygons.js';",
|
||||
);
|
||||
write('src/layers/aircraft/render/cesium.js', '');
|
||||
write('src/app/data.js', "import '../layers/flights/render/navara.js';");
|
||||
assert.throws(
|
||||
() => checkImportDirections(root),
|
||||
/src\/app\/data\.js: A Navara layer renderer loads only through the Navara mode's dynamic import\(\): src\/layers\/flights\/render\/navara\.js/,
|
||||
/A Navara layer renderer loads only through/,
|
||||
);
|
||||
});
|
||||
|
||||
test('engine import exceptions are exact, justified and measured', (t) => {
|
||||
const { root, write } = fixture(t);
|
||||
const item = {
|
||||
file: 'src/data/story.js',
|
||||
package: 'cesium',
|
||||
reason: 'Authored story records still use SDK properties.',
|
||||
};
|
||||
const allow = (items) =>
|
||||
write(
|
||||
'scripts/package-boundaries.json',
|
||||
JSON.stringify({ engineImportExceptions: items }),
|
||||
);
|
||||
write(item.file, "import 'cesium';");
|
||||
allow([item]);
|
||||
assert.deepEqual(checkImportDirections(root).engineImportExceptions, [item]);
|
||||
write('src/data/other.js', "import 'cesium';");
|
||||
assert.throws(
|
||||
() => checkImportDirections(root),
|
||||
/other.js: cesium imports belong/,
|
||||
);
|
||||
write('src/data/other.js');
|
||||
write(item.file, "import 'three';");
|
||||
assert.throws(() => checkImportDirections(root), /three imports belong/);
|
||||
write(item.file);
|
||||
assert.throws(
|
||||
() => checkImportDirections(root),
|
||||
/Unused engine import exception/,
|
||||
);
|
||||
write(item.file, "import 'cesium';");
|
||||
allow([{ ...item, reason: '' }]);
|
||||
assert.throws(
|
||||
() => checkImportDirections(root),
|
||||
/Invalid engine import exception/,
|
||||
);
|
||||
allow([item, item]);
|
||||
assert.throws(
|
||||
() => checkImportDirections(root),
|
||||
/Invalid engine import exception/,
|
||||
);
|
||||
allow([{ ...item, file: 'src/missing.js' }]);
|
||||
assert.throws(
|
||||
() => checkImportDirections(root),
|
||||
/Invalid engine import exception/,
|
||||
);
|
||||
});
|
||||
|
||||
test('an exception cannot bypass the contract or engine graph rules', (t) => {
|
||||
const { root, write } = fixture(t);
|
||||
write('src/data/helper.js', "import 'cesium';");
|
||||
write(
|
||||
'scripts/package-boundaries.json',
|
||||
JSON.stringify({
|
||||
engineImportExceptions: [
|
||||
{
|
||||
file: 'src/data/helper.js',
|
||||
package: 'cesium',
|
||||
reason: 'Legacy model.',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
write('src/globe/contract.js', "import '../data/helper.js';");
|
||||
assert.throws(
|
||||
() => checkImportDirections(root),
|
||||
/globe contract reaches engine dependency/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -384,7 +466,7 @@ test('globe engines use their own dependencies, and globe tests may import any e
|
||||
);
|
||||
write(
|
||||
'src/globe/navara/engine.js',
|
||||
"import 'three'; import '@navaramap/three'; import '../contract.js';",
|
||||
"import 'three'; import 'postprocessing'; import '@navaramap/three'; import '../contract.js';",
|
||||
);
|
||||
assert.doesNotThrow(() => checkImportDirections(root));
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { markRaw, shallowRef } from 'vue';
|
||||
|
||||
/**
|
||||
* The screen-space annotation board as its view renders it. The screen
|
||||
* annotation renderer (src/annotations/screenAnnotationRenderer.js) presents
|
||||
* annotation renderer (src/globe/cesium/annotations/screenAnnotationRenderer.js) presents
|
||||
* every mark's SVG parts after each rendered frame; the view hands back the
|
||||
* elements the renderer measures and reports when a draw-on stroke finishes.
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* ── Screen-space annotations (src/annotations/screenAnnotationRenderer.js) ── */
|
||||
/* ── Screen-space annotations (src/globe/cesium/annotations/screenAnnotationRenderer.js) ── */
|
||||
.gev-screen-whiteboard { position: fixed; inset: 0; pointer-events: none; z-index: 90; }
|
||||
.gev-screen-whiteboard-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
|
||||
/* No CSS opacity transition here on purpose: group opacity is driven per-frame in JS
|
||||
|
||||
@@ -3,7 +3,7 @@ import { storeToRefs } from 'pinia';
|
||||
import { useScreenAnnotationsStore } from '../../stores/screenAnnotations.js';
|
||||
|
||||
// The screen-space annotation board over the page, as the screen annotation
|
||||
// renderer (src/annotations/screenAnnotationRenderer.js) presents it.
|
||||
// renderer (src/globe/cesium/annotations/screenAnnotationRenderer.js) presents it.
|
||||
const { board, controls } = storeToRefs(useScreenAnnotationsStore());
|
||||
|
||||
// Each element goes to the renderer it was rendered for, which measures it.
|
||||
|
||||
Reference in new issue
Block a user