mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 07:42:23 +00:00
Add indexed world triangles to the required globe contract on Cesium, Navara and the fake engine. Own geometry, textures, visibility, depth tests, pick identities and teardown inside each engine. Reused canvas images request another frame until their new pixels upload. Move obstruction rays and owned mesh, line and mover exclusions into the Cesium surface adapter. Draw CCTV monitor planes and viewshed volumes through shared mesh handles. Draw data-pack points, straight lines, raised polygons, image rectangles and media captions through movers, lines and meshes. Delete the feature renderers for each engine and retain decoded assets across scene rebinds. Register the modules, dependency, format scope and reduced coupling baseline. Cesium is the reference for geometry, colours, image placement, marker sizes, caption offsets, picking and resource ownership. Navara now uses straight pack lines and per-position polygon heights without edge densification. Navara meshes draw in the transparent pass after the effects, with the scene-depth test and the view's exposure undone, as the pack meshes did: in the scene pass the landscape look's exposure washed a red pack image to white. The scene-depth helper moves into the Navara engine, and the stems helper that only its test still used is deleted. The mesh picker takes the globe when Navara's depth read answers with the camera itself, which it does right after another click and which hid a polygon from a quick second click. The 256 px media caption is not pickable: its transparent square took clicks meant for nearby pack features, and no action targets it. Annotation drapes remain on their existing renderers because bounded building classification, animated fills and wire cages need a separate contract extension to preserve Cesium behaviour. Coupling falls from 344 to 210 Cesium layer-renderer lines, 1294 to 794 Navara layer-renderer lines, 9 to 7 external Cesium import files, 1026 to 1005 viewer lines, 9 to 6 extension reads and 42 to 39 engine-name lines. Other counts do not grow. The full unit suite, import directions, package boundaries, formatting and coupling checks pass. No user-visible strings change. In the browser, keyless: the contract suite passes on Navara and on Cesium apart from the camera-events check that also fails intermittently before this change; CCTV v2 (58 checks), the CCTV and director engine switches pass, with the pack image at the same pixel as before on both engines. The mesh contract fixture now sits at 3 km, above the terrain Navara draws there. The director interactions script starts at its view in the link so the app's arrival flight no longer lands after the shot, and waits for the click's asynchronous pick; its last step, the next-shot action, fails the same way before this change.
417 lines
15 KiB
JavaScript
417 lines
15 KiB
JavaScript
import { execFileSync } from 'node:child_process';
|
|
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
import { builtinModules } from 'node:module';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { analyzeModule } from './module-analysis.mjs';
|
|
|
|
const builtins = new Set(
|
|
builtinModules.flatMap((name) => [name, `node:${name}`]),
|
|
);
|
|
const code = /\.[mc]?js$/;
|
|
const entry = (file) =>
|
|
file === 'src/main.js' || file.startsWith('src/standalone/');
|
|
const tests = (file) =>
|
|
file.endsWith('.test.mjs') ||
|
|
file.startsWith('src/testSupport/') ||
|
|
file.startsWith('src/tooling/') ||
|
|
file === 'src/overlays/worldOverlayAllocation.worker.mjs';
|
|
const source = (file) =>
|
|
file.startsWith('src/sources/') ||
|
|
/^src\/layers\/[^/]+\/(?:source|bundledSource|flowSource)\.js$/.test(file);
|
|
// Engine-neutral geometry ported from Cesium; every runtime file is a portable root.
|
|
const geo = (file) => file.startsWith('src/geo/');
|
|
// The app clock (src/time): plain numbers, no engine, no DOM.
|
|
const time = (file) => file.startsWith('src/time/');
|
|
// The engine-neutral globe contract and its test support; engines live below it.
|
|
const globeContract = (file) =>
|
|
/^src\/globe\/[^/]+$/.test(file) || file.startsWith('src/globe/testing/');
|
|
const globeEngine = (file) => /^src\/globe\/(?:cesium|navara)\//.test(file);
|
|
const cesiumPackage = /^(?:cesium(?:\/|$)|@cesium\/)/;
|
|
// Navara renders through three.js and its postprocessing peer.
|
|
const navaraPackage = /^(?:(?:three|postprocessing)(?:\/|$)|@navaramap\/)/;
|
|
// Layers built on layer renderers keep each engine in their renderers:
|
|
// cesium only in render/cesium*.js, three and Navara only in
|
|
// render/navara*.js. The list grows with each layer moved onto renderers.
|
|
const RENDERED_LAYER_DIRS = [
|
|
'src/layers/streetLevel/',
|
|
'src/layers/traffic/',
|
|
'src/layers/directions/',
|
|
'src/layers/infrastructure/',
|
|
'src/layers/installations/',
|
|
'src/layers/awareness/',
|
|
'src/layers/droneAirspace/',
|
|
'src/layers/cyclones/',
|
|
'src/layers/ghostMaps/',
|
|
'src/layers/submarineCables/',
|
|
'src/layers/alpr/',
|
|
'src/layers/cctv/',
|
|
'src/layers/transit/',
|
|
'src/layers/airQuality/',
|
|
'src/layers/gnss/',
|
|
'src/layers/perimeters/',
|
|
'src/layers/radio/',
|
|
'src/layers/bikeshare/',
|
|
'src/layers/earthquakes/',
|
|
'src/layers/firms/',
|
|
'src/layers/flights/',
|
|
'src/layers/military/',
|
|
'src/layers/localAdsb/',
|
|
'src/layers/vessels/',
|
|
'src/layers/satellites/',
|
|
'src/layers/launches/',
|
|
'src/layers/aircraft/',
|
|
'src/layers/shared/',
|
|
];
|
|
const W1_SHARED_FILES = new Set(
|
|
[
|
|
'trackedCamera',
|
|
'trailRenderer',
|
|
'contactTrailRenderer',
|
|
'focusDeemphasis',
|
|
'spriteOrder',
|
|
'iconOrientation',
|
|
'modelVisualAnchor',
|
|
'trackingClickGesture',
|
|
].map((name) => `src/data/${name}.js`),
|
|
);
|
|
const renderedLayer = (file) =>
|
|
W1_SHARED_FILES.has(file) ||
|
|
file.startsWith('src/data/render/') ||
|
|
file === 'src/celestialRing.js' ||
|
|
file.startsWith('src/celestialRing/') ||
|
|
RENDERED_LAYER_DIRS.some((dir) => file.startsWith(dir));
|
|
const cesiumLayerRenderer = (file) =>
|
|
/\/render\/cesium(?:[^/]*|\/[^/]+)\.js$/.test(file);
|
|
// Navara renderers outside a layer's render/ directory: like those, they
|
|
// reach the app only through the Navara mode's import().
|
|
const NAVARA_SCENE_RENDERERS = new Set();
|
|
// The modules that load Navara renderers, each through import().
|
|
const NAVARA_RENDERER_LOADERS = new Set([
|
|
'src/app/navaraMode.js',
|
|
'src/app/layers/renderers.js',
|
|
]);
|
|
const navaraLayerRenderer = (file) =>
|
|
/\/render\/navara[^/]*\.js$/.test(file) || NAVARA_SCENE_RENDERERS.has(file);
|
|
const renderer = (file) =>
|
|
/^src\/(?:ui|app|standalone|overlays)\//.test(file) ||
|
|
/^src\/layers\/[^/]+\/(?:index|rendering|snapshotRenderer|overlay|presentation|cards|controls)\.js$/.test(
|
|
file,
|
|
);
|
|
// One compatibility composition entry intentionally selects standalone defaults.
|
|
const compatibilityEdge = (from, to) =>
|
|
from === 'server/providers/local.js' &&
|
|
to === 'server/standalone/key-setup.js';
|
|
const portableExport = (key) =>
|
|
key.startsWith('./sources/') ||
|
|
/\/source$/.test(key) ||
|
|
/^\.\/layers\/(?:flights|military|vessels)\/(?:records|ingestion)$/.test(
|
|
key,
|
|
) ||
|
|
[
|
|
'./director',
|
|
'./tools',
|
|
'./tools/mcp',
|
|
'./tools/services',
|
|
'./view',
|
|
'./voice/action-schemas',
|
|
'./voice/session',
|
|
'./data/lifecycle',
|
|
].includes(key);
|
|
|
|
/** Check every runtime file, plus transitive portable/source graphs, independently of bundler reachability. */
|
|
export function checkImportDirections(root) {
|
|
root = realpathSync(root);
|
|
const pkg = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
const files = [
|
|
...new Set(
|
|
execFileSync(
|
|
'git',
|
|
[
|
|
'ls-files',
|
|
'-z',
|
|
'--cached',
|
|
'--others',
|
|
'--exclude-standard',
|
|
'--',
|
|
'src',
|
|
'server',
|
|
],
|
|
{ cwd: root, encoding: 'utf8' },
|
|
).split('\0'),
|
|
),
|
|
].filter(
|
|
(file) =>
|
|
code.test(file) && !tests(file) && existsSync(path.join(root, file)),
|
|
);
|
|
const errors = new Set();
|
|
const modules = new Map();
|
|
const relative = (file) =>
|
|
path.relative(root, file).split(path.sep).join('/');
|
|
const report = (file, message) => errors.add(`${file}: ${message}`);
|
|
function load(file) {
|
|
if (modules.has(file)) return modules.get(file);
|
|
const resolved = realpathSync(path.join(root, file));
|
|
if (relative(resolved) !== file) {
|
|
report(
|
|
file,
|
|
'Runtime modules must not alias other owners through symlinks',
|
|
);
|
|
return null;
|
|
}
|
|
let analysis;
|
|
try {
|
|
analysis = analyzeModule(readFileSync(resolved, 'utf8'), {
|
|
dynamic: true,
|
|
});
|
|
} catch (error) {
|
|
report(file, error.message);
|
|
return null;
|
|
}
|
|
const record = { ...analysis, edges: [], external: [] };
|
|
modules.set(file, record);
|
|
for (const specifier of analysis.imports) {
|
|
let target;
|
|
if (specifier.startsWith('.'))
|
|
target = path.resolve(path.dirname(resolved), specifier.split('?')[0]);
|
|
else if (specifier === pkg.name || specifier.startsWith(`${pkg.name}/`)) {
|
|
const key =
|
|
specifier === pkg.name ? '.' : '.' + specifier.slice(pkg.name.length);
|
|
const exported = pkg.exports?.[key];
|
|
if (typeof exported !== 'string') {
|
|
report(file, `Unsupported self import: ${specifier}`);
|
|
continue;
|
|
}
|
|
target = path.resolve(root, exported);
|
|
} else {
|
|
record.external.push(specifier);
|
|
if (renderedLayer(file) && !tests(file)) {
|
|
if (cesiumPackage.test(specifier) && !cesiumLayerRenderer(file))
|
|
report(
|
|
file,
|
|
`cesium only in the layer's render/cesium*.js: ${specifier}`,
|
|
);
|
|
if (navaraPackage.test(specifier) && !navaraLayerRenderer(file))
|
|
report(
|
|
file,
|
|
`three and Navara only in the layer's render/navara*.js: ${specifier}`,
|
|
);
|
|
}
|
|
if (
|
|
file.startsWith('src/') &&
|
|
(builtins.has(specifier) || specifier.startsWith('node:'))
|
|
)
|
|
report(file, `Node builtin in browser module: ${specifier}`);
|
|
if (
|
|
specifier.startsWith('/') ||
|
|
(/^[a-z]+:/i.test(specifier) && !specifier.startsWith('node:'))
|
|
)
|
|
report(file, `Use repository or package imports: ${specifier}`);
|
|
continue;
|
|
}
|
|
if (!existsSync(target)) {
|
|
report(file, `Missing import: ${specifier}`);
|
|
continue;
|
|
}
|
|
const to = relative(realpathSync(target));
|
|
if (to.startsWith('../') || path.isAbsolute(to)) {
|
|
report(file, `Import escapes repository: ${specifier}`);
|
|
continue;
|
|
}
|
|
record.edges.push(to);
|
|
// Navara stays out of the Cesium-mode bundle: outside its own directory
|
|
// it is reached only through import() (src/app/scene.js), which the
|
|
// bundler splits into a chunk loaded on ?engine=navara.
|
|
if (
|
|
to.startsWith('src/globe/navara/') &&
|
|
!file.startsWith('src/globe/navara/') &&
|
|
!analysis.dynamicImports.includes(specifier)
|
|
)
|
|
report(
|
|
file,
|
|
`The Navara engine loads only through a dynamic import(): ${to}`,
|
|
);
|
|
// Like the engine, a layer's Navara renderer reaches the app only
|
|
// through an import() of the Navara mode (src/app/navaraMode.js) or
|
|
// of the layer manifest (src/app/layers/renderers.js).
|
|
if (
|
|
navaraLayerRenderer(to) &&
|
|
!navaraLayerRenderer(file) &&
|
|
!(
|
|
NAVARA_RENDERER_LOADERS.has(file) &&
|
|
analysis.dynamicImports.includes(specifier)
|
|
)
|
|
)
|
|
report(
|
|
file,
|
|
`A Navara layer renderer loads only through the Navara mode's dynamic import(): ${to}`,
|
|
);
|
|
if (file.startsWith('src/') && to.startsWith('server/'))
|
|
report(file, `Browser imports server: ${to}`);
|
|
if (
|
|
!compatibilityEdge(file, to) &&
|
|
file.startsWith('server/providers/') &&
|
|
(to.startsWith('server/standalone/') || renderer(to))
|
|
)
|
|
report(file, `Provider imports application/rendering: ${to}`);
|
|
if (
|
|
!compatibilityEdge(file, to) &&
|
|
file.startsWith('src/') &&
|
|
!entry(file) &&
|
|
entry(to)
|
|
)
|
|
report(file, `Reusable module imports standalone setup: ${to}`);
|
|
if (source(file) && renderer(to))
|
|
report(file, `Source imports rendering/application: ${to}`);
|
|
if (file === 'src/voice/gevActions.js' && to === 'src/data/manager.js')
|
|
report(
|
|
file,
|
|
'Actions must consume feed state without the manager facade',
|
|
);
|
|
}
|
|
return record;
|
|
}
|
|
for (const file of files) load(file);
|
|
// A helper outside src must not smuggle Node or server code into a browser graph.
|
|
const browserSeen = new Set();
|
|
function browser(file) {
|
|
if (browserSeen.has(file) || !code.test(file)) return;
|
|
browserSeen.add(file);
|
|
const info = load(file);
|
|
if (!info) return;
|
|
for (const specifier of info.external) {
|
|
if (builtins.has(specifier) || specifier.startsWith('node:'))
|
|
report(file, `Browser graph reaches Node builtin: ${specifier}`);
|
|
}
|
|
for (const to of info.edges) {
|
|
if (to.startsWith('server/') || tests(to))
|
|
report(file, `Browser graph reaches non-runtime owner: ${to}`);
|
|
else browser(to);
|
|
}
|
|
}
|
|
for (const file of files.filter((file) => file.startsWith('src/')))
|
|
browser(file);
|
|
const roots = new Map(files.filter(source).map((file) => [file, 'source']));
|
|
for (const file of files.filter(geo)) roots.set(file, 'geo');
|
|
for (const file of files.filter(time)) roots.set(file, 'time');
|
|
for (const [key, value] of Object.entries(pkg.exports || {})) {
|
|
if (portableExport(key) && typeof value === 'string')
|
|
roots.set(value.replace(/^\.\//, ''), key);
|
|
}
|
|
roots.set('src/data/feedState.js', 'feed state');
|
|
for (const [start, label] of roots) {
|
|
if (!existsSync(path.join(root, start))) {
|
|
report(start, `Missing portable entry: ${label}`);
|
|
continue;
|
|
}
|
|
const seen = new Set();
|
|
function visit(file) {
|
|
if (seen.has(file) || !code.test(file)) return;
|
|
seen.add(file);
|
|
const info = load(file);
|
|
if (!info) return;
|
|
if (info.browser.length)
|
|
report(
|
|
file,
|
|
`${label} reaches browser globals: ${info.browser.join(', ')}`,
|
|
);
|
|
for (const specifier of info.external) {
|
|
if (
|
|
builtins.has(specifier) ||
|
|
specifier.startsWith('node:') ||
|
|
/^(?:cesium|vite)(?:\/|$)/.test(specifier) ||
|
|
// Geo must not reach Cesium through its workspace packages either.
|
|
(label === 'geo' && /^@cesium\//.test(specifier)) ||
|
|
// Neither engine reaches the app clock.
|
|
(label === 'time' &&
|
|
(cesiumPackage.test(specifier) || navaraPackage.test(specifier)))
|
|
)
|
|
report(
|
|
file,
|
|
`${label} reaches platform/rendering dependency: ${specifier}`,
|
|
);
|
|
}
|
|
for (const to of info.edges) {
|
|
if (renderer(to) || to.startsWith('server/'))
|
|
report(file, `${label} reaches application/rendering: ${to}`);
|
|
else visit(to);
|
|
}
|
|
}
|
|
visit(start);
|
|
}
|
|
// The globe contract reaches no engine, directly or through helpers; each
|
|
// engine implementation imports only its own engine's packages.
|
|
const globeSeen = new Set();
|
|
function globe(file) {
|
|
if (globeSeen.has(file) || !code.test(file)) return;
|
|
globeSeen.add(file);
|
|
const info = load(file);
|
|
if (!info) return;
|
|
for (const specifier of info.external) {
|
|
if (cesiumPackage.test(specifier) || navaraPackage.test(specifier))
|
|
report(file, `globe contract reaches engine dependency: ${specifier}`);
|
|
}
|
|
for (const to of info.edges) {
|
|
if (globeEngine(to))
|
|
report(file, `globe contract imports engine implementation: ${to}`);
|
|
else globe(to);
|
|
}
|
|
}
|
|
for (const file of files.filter(globeContract)) globe(file);
|
|
// Each engine graph stays on its own engine, through helpers too.
|
|
for (const start of files.filter(globeEngine)) {
|
|
const navara = start.startsWith('src/globe/navara/');
|
|
const label = navara ? 'Navara globe engine' : 'Cesium globe engine';
|
|
const foreignPackage = navara ? cesiumPackage : navaraPackage;
|
|
const foreignEngine = navara ? 'src/globe/cesium/' : 'src/globe/navara/';
|
|
const seen = new Set();
|
|
const visit = (file) => {
|
|
if (seen.has(file) || !code.test(file)) return;
|
|
seen.add(file);
|
|
const info = load(file);
|
|
if (!info) return;
|
|
for (const specifier of info.external)
|
|
if (foreignPackage.test(specifier))
|
|
report(file, `${label} reaches ${specifier}`);
|
|
for (const to of info.edges) {
|
|
if (to.startsWith(foreignEngine))
|
|
report(file, `${label} reaches ${to}`);
|
|
else visit(to);
|
|
}
|
|
};
|
|
visit(start);
|
|
}
|
|
// Common voice controls may use their DOM view, but never a protocol implementation.
|
|
const voiceSeen = new Set();
|
|
function voice(file) {
|
|
if (voiceSeen.has(file)) return;
|
|
voiceSeen.add(file);
|
|
const info = load(file);
|
|
for (const to of info?.edges || []) {
|
|
if (/^src\/voice\/realtime/.test(to))
|
|
report(file, `Common voice controls import protocol: ${to}`);
|
|
else if (code.test(to)) voice(to);
|
|
}
|
|
}
|
|
if (existsSync(path.join(root, 'src/voice/sessionCommands.js')))
|
|
voice('src/voice/sessionCommands.js');
|
|
if (errors.size) throw new Error([...errors].join('\n'));
|
|
return { modules: files.length, portableEntries: roots.size };
|
|
}
|
|
|
|
if (
|
|
process.argv[1] &&
|
|
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
) {
|
|
try {
|
|
console.log(
|
|
JSON.stringify(
|
|
checkImportDirections(fileURLToPath(new URL('../', import.meta.url))),
|
|
),
|
|
);
|
|
} catch (error) {
|
|
console.error(error.message);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|