mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-11 13:52:19 +00:00
Register the Navara package workarounds and check them against the package
src/globe/navara/WORKAROUNDS.md lists every place the adapter works around @navaramap/three 0.2.1: what the package does wrong or lacks, how it is detected and what to delete once it is fixed. scripts/qa-navara-canaries.mjs confirms three of the bugs on the real package in the browser (batch ids never released, events of a deleted vector layer kept queued, idle reported before a raster tile arrives) and names the workaround to delete when one is gone. The private members the adapter reads are probed when the engine starts, with an error naming the member and the pinned version instead of a silent fallback, and the test that only internals.js reaches private members now scans the adapter's subfolders too.
This commit is contained in:
1 parent
9c2a2d6951
commit
ffbceb977c
4 files changed
+625
-35
No files matched your search
@@ -0,0 +1,80 @@
|
||||
# Navara dependency workarounds
|
||||
|
||||
`@navaramap/three` and its default plugin are pinned to **0.2.1**.
|
||||
Locations below are repository paths; a unit test of our replacement is not
|
||||
an upstream canary. `PASS` means the package bug is confirmed. `BUG GONE`
|
||||
means the fixture controls passed and the bug is absent; only this outcome
|
||||
names a removal target. `FIXTURE ERROR` means the check could not establish a
|
||||
verdict, so keep the workaround. The script exits 0 only when every active
|
||||
canary confirms its bug; either other outcome exits non-zero.
|
||||
|
||||
Run the browser canaries against a keyless Vite dev server on port 4300:
|
||||
|
||||
```sh
|
||||
GOOGLE_MAPS_API_KEY= CESIUM_ION_TOKEN= OPENAI_API_KEY= FIRMS_MAP_KEY= MAPILLARY_CLIENT_TOKEN= TOMTOM_API_KEY= GEMINI_API_KEY= npx vite --port 4300 --strictPort
|
||||
node scripts/qa-navara-canaries.mjs http://localhost:4300
|
||||
```
|
||||
|
||||
Set `PUPPETEER_EXECUTABLE_PATH` to the installed Chrome executable if needed.
|
||||
The script uses a bare real Navara view so adapter cleanup, redraw timers and
|
||||
pick validation cannot hide the defects. All network requests outside the dev
|
||||
origin are blocked; raster responses are local fixtures. A browser run confirmed
|
||||
the batch-ID, deleted-vector-event and raster-idle bugs in 0.2.1. These are the
|
||||
three active canaries; expected output is three `PASS` lines followed by
|
||||
`3 bugs confirmed, 0 bugs gone, 0 fixture errors` (exit 0).
|
||||
|
||||
There is no initial-hide canary: its shared readiness timeout did not identify
|
||||
whether `featureCreated` or a shown/hidden pixel control stalled. The installed
|
||||
SDK supports the inline GeoJSON Feature, vector layer description and event,
|
||||
but the event follows asynchronous geometry creation. The fixture hides the
|
||||
layer immediately, before creation is observed. Code inspection cannot tell
|
||||
which wait failed or why; a browser trace is needed before restoring the check.
|
||||
There is no rapid-click canary: the timing-dependent stale pick was not
|
||||
reproducible headless. Correct IDs from that run do not establish that the bug
|
||||
is fixed. Neither excluded check is a signal to remove a workaround.
|
||||
|
||||
| Location | Package defect or missing API | Seen in | Detection | Remove or replace when fixed |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `src/globe/navara/internals.js:71` | `animation=false` still updates the tile tree on every RAF; no suspend/resume API. | 0.2.1 | No canary: needs an upstream update-loop counter, not just rendered-frame counts. | Replace `_startMainLoop` / `_renderer.setAnimationLoop` access with suspend/resume. |
|
||||
| `src/globe/navara/internals.js:86` | `ModelMesh._releaseGeometryArrays` drops CPU positions and indices after upload, preventing tile ray queries. | 0.2.1 | No canary: needs a loaded synthetic 3D tile and an upload; surface unit tests exercise retained geometry only. | Remove the prototype patch when geometry retention or native ray/height queries are available. |
|
||||
| `src/globe/navara/internals.js:120` | No public enumeration of loaded 3D tile roots. | 0.2.1 | No canary: API gap; startup guard checks `_scenes.mrt.children`. | Replace `tileRoots` private scene traversal with the public query. |
|
||||
| `src/globe/navara/internals.js:131` | Idle is an event edge, with no public current-state getter. | 0.2.1 | No canary: API gap; startup guard checks boolean `_isIdle`. | Replace `isIdle` with a public idle/tiles-loaded getter. |
|
||||
| `src/globe/navara/internals.js:165`, `src/globe/navara/look.js:276` | No independent atmosphere exposure; sky/cloud radiance needs scaling without brightening unlit content. Uniform vectors are shared and materials may be rebuilt. | 0.2.1 | No canary: appearance needs a controlled daylight pixel reference. Startup checks sky/stars `ref.raw.material.uniforms`, aerial `rawEffect.uniforms`, and clouds `rawEffect.cloudsPass.currentMaterial.uniforms`; reads also validate rebuilt materials. | Remove `forEachLuminanceUniform`, the scaler and shared-vector bookkeeping when independent exposure is supported. |
|
||||
| `src/globe/navara/internals.js:209`, `src/globe/navara/cascadedLightCopy.js:49` | Transparent-pass copies clone cascaded shadow lights each frame. | 0.2.1 | `cascadedLightCopy.test.mjs`: “Navara 0.2.1's copy clones new lights every call (the leak)” uses the real CSM package. | Remove `patchCascadedLightCopy`, `keepCascadedLightIds`, and their startup call. |
|
||||
| `src/globe/navara/internals.js:224`, `src/globe/navara/featureQueue.js:15` | Late changes for deleted vector layers remain queued and prevent idle. | 0.2.1 | `deleted-vector-events-retained` (browser, confirmed); startup checks `_eventManager.stacks.renderable_feature_changed` and `removeAt`. | Remove queue access, `dropOrphanFeatureChanges`, and its pre-update hook. |
|
||||
| `src/globe/navara/internals.js:257` | No public ordered pass-name list for diagnostics. | 0.2.1 | No canary: API gap; startup checks `view.viewContext` queries and `renderPassOrchestrator.passMap.list`. A not-yet-created optional effect has no context. | Replace `pipelinePassNames` and the startup context probe with public diagnostics/descriptor queries. |
|
||||
| `src/globe/navara/movers/batchIds.js:11` | Unregistering pickable wrappers does not recycle their global batch IDs. | 0.2.1 | `batch-ids-retained` (browser, confirmed) checks disjoint allocations after unregistering; this measures ID reuse, not WASM byte reclamation. | Remove `createBatchIdPool` and pool wiring in `movers/index.js`. |
|
||||
| `src/globe/navara/ground/index.js:145` | The original native-vector path lost the first `show: false` while a new layer was being built. Current ground rendering uses its own pass, so the hide retry is already absent. | 0.2.1 (original path) | No canary: `initial-vector-hide-dropped` timed out before a verdict; the shared readiness error does not identify the failed wait. Requires browser diagnosis. | No current hide retry to delete. Do not remove the ground pass: it also implements draping and materials. |
|
||||
| `src/globe/navara/maps.js:76` | Idle can be reported before a fetched raster tile arrives after a map switch; the map may keep a stale frame. | 0.2.1 | `idle-before-raster-arrives` (browser, confirmed) holds real raster requests across the idle event, then releases local PNG responses. | Remove the `SETTLE_MS` / `SETTLE_EVERY_MS` redraw timer from `changed`. |
|
||||
| `src/globe/navara/maps.js:145` | Removing the final raster layer interrupts terrain loading at the current view. | 0.2.1 | No canary: needs terrain-loading continuity across final-layer removal; the raster idle fixture does not test terrain. | Remove the add-before-delete constraint and hidden base-raster retention under 3D tiles. |
|
||||
| `src/globe/navara/maps.js:37`, `src/globe/navara/maps.js:209` | Quantized-mesh root URLs without the tile template can panic. | 0.2.1 | No canary: deliberately panicking WASM would invalidate the remaining view checks; no isolated terrain fixture here. | Use the upstream root-URL API instead of manually appending `{z}/{x}/{y}.terrain`. |
|
||||
| `src/globe/navara/maps.js:65` | No ion imagery path. | 0.2.1 | No canary: explicit unsupported source; requires a real ion imagery endpoint. | Replace the ion-imagery rejection with native source support. |
|
||||
| `src/globe/navara/maps.js:209` | No terrain-request authentication retry hook; an expired ion endpoint token cannot be refreshed after a tile 401. | 0.2.1 | No canary: no authenticated fixture or exposed request callback. This is an unresolved gap, not a working retry. | Add retry through the upstream hook; there is no compensating retry code to delete. |
|
||||
| `src/globe/navara/maps.js:230` | Raster tile failures after source creation are not exposed with source/status information for map fallback. | 0.2.1 | No canary: no adapter fallback exists; successful layer construction is not proof of successful tiles. | Wire native errors into map fallback when the package exposes them; nothing currently compensates for this. |
|
||||
| `src/globe/navara/index.js:52`, `src/globe/navara/viewOptions.js:19` | No `preserveDrawingBuffer` option; a default canvas also allocates antialiasing despite rendering through offscreen targets. | 0.2.1 | No canary: missing constructor options; `viewOptions.test.mjs` tests our chosen context attributes. | Replace pre-created WebGL2 context with equivalent native canvas/context options. |
|
||||
| `src/globe/navara/index.js:48` | Default cache budget can retain excessive tile memory; a fixed total also includes screen-size render targets and coarsens HD views. | 0.2.1 | No canary: requires a navigation/resize memory soak. | Remove fixed-target tracking and `TILE_CACHE_ROOM` override if native budgeting preserves the desired tile allowance. |
|
||||
| `src/globe/navara/index.js:289` | Disposing the view leaves shared postprocessing fullscreen geometry listeners holding renderer state. | 0.2.1 | No canary: requires renderer/heap retention measurements after repeated disposal. | Remove `Pass.fullscreenGeometry.dispose()` once upstream releases those listeners. |
|
||||
| `src/globe/navara/frameDescriptor.js:4` | Auto-bound descriptor frame callbacks are not unregistered on deletion. | 0.2.1 | No canary: existing `frameDescriptor.test.mjs` uses a stub base class, not the real descriptor lifecycle. | Remove `withFrameUpdates` and use native descriptor updates. |
|
||||
| `src/globe/navara/frameDescriptor.js:24` | Effect deletion removes the composer pass without disposing its GPU resources. | 0.2.1 | No canary: current tests cover the wrapper's disposal, not native resource ownership. | Remove `withPassDisposal` when native deletion disposes passes exactly once. |
|
||||
| `src/globe/navara/postSamplingPass.js:8` | Every composer target is multisampled, including fullscreen effects that need one sample. | 0.2.1 | No canary: requires HD render-target/sample and pixel checks; unit tests use fake contexts. | Remove `GeometrySamplingPass` and its installation when native sampling separates geometry from effects. |
|
||||
| `src/globe/navara/emptySceneRender.js:15` | Empty MRT/forward scenes still call render and trigger MSAA resolves. | 0.2.1 | No canary: requires instrumenting the real HD pipeline; unit tests cover the skip policy only. | Remove `skipEmptySceneRenders` when native passes skip safe empty renders. |
|
||||
| `src/globe/navara/sceneDepth.js:60` | The transparent pass runs after effects without scene depth, drawing hidden meshes through terrain/tiles. | 0.2.1 | No canary: needs a controlled occluder/depth pixel scene. | Remove shader scene-depth injection when the native transparent target retains correct depth. |
|
||||
| `src/globe/navara/meshes.js:71` | Transparent unlit colours still receive view exposure. | 0.2.1 | No canary: needs colour samples at multiple exposures; current mesh tests exercise adapter state. | Remove reciprocal exposure compensation when native display-space colours stay unchanged. |
|
||||
| `src/globe/navara/groundFloor.js:59` | Camera zoom limits are geocentric distances, without terrain/3D-tile collision. | 0.2.1 | No canary: requires loaded relief plus actual wheel navigation. | Replace `createGroundFloor` and its engine wiring with native collision handling that preserves the contract. |
|
||||
| `src/globe/navara/cameraModel.js:47` | Camera pitch clamps to ±89.99°, perturbing an exact authored pose when read back. | 0.2.1 | No canary: `cameraModel.test.mjs` simulates the clamp, not native round trips. | Remove pushed-pose tolerance once native pose readback is exact. |
|
||||
| `src/globe/navara/cameraModel.js:51` | Flight easing has no sinusoidal-in-out preset. | 0.2.1 | No canary: requires sampling a native flight's time/position curve. | Replace the `quadInOut` approximation with the actual sine easing. |
|
||||
| `src/globe/navara/input.js:193` | Camera controller options have a setter but no getter. | 0.2.1 | No canary: API gap; input unit tests verify retained/merged options. | Replace the local options mirror when options can be read and updated without losing prior settings. |
|
||||
| `src/globe/navara/picking.js:53` | Quick clicks can receive the first gesture's GPU pick. | 0.2.1 | No canary: not reproducible headless. `rapid-click-first-pick` returned correct IDs for spaced and rapid clicks, which does not establish a fix for this timing-dependent bug. | Keep stale-answer CPU validation until rapid clicks reliably return the matching instance in the affected scenario. Preserve CPU paths needed for lines/drill picking. |
|
||||
| `src/globe/navara/createNavaraEngine.js:255` | Depth reads immediately after a click can return the near plane. | 0.2.1 | No canary: requires a terrain/3D-tile depth scene and click timing; the two-quad pick fixture does not establish ground depth. | Replace the CPU ground ray with native depth picking when post-click depth is reliable. |
|
||||
| `src/globe/navara/createNavaraEngine.js:169`, `src/globe/navara/ground/index.js:29` | Frame requests made during rendering are cleared at the end of the current frame. | 0.2.1 | No canary: current engine/ground unit tests emulate the flag reset. | Remove deferred re-request bookkeeping once native requests during a frame schedule the next one. |
|
||||
| `src/globe/navara/viewOptions.js:175` | G-buffer normals for globe/3D tiles are zero, breaking atmosphere lighting. | 0.2.1 | No canary: needs actual G-buffer and lighting pixel checks; Google tiles are not used by these canaries. | Remove `useNormalBuffer: false` when native normals support the lighting path. |
|
||||
| `src/globe/navara/viewOptions.js:191` | Cloud defaults override the low preset's shadow map size and cascade count. | 0.2.1 | No canary: requires inspecting the real cloud preset result; option tests only verify the override. | Remove explicit `shadowMapSize` / `shadowCascadeCount` once the low preset survives native defaults. |
|
||||
| `src/globe/navara/viewOptions.js:93`, `src/globe/navara/look.js:172` | Cloud bases are ellipsoid-relative and do not clear local terrain. | 0.2.1 | No canary: needs sampled relief and cloud-base inspection at local scale. | Remove terrain-dependent cloud offsets if native cloud altitude can be terrain-relative. |
|
||||
| `src/globe/navara/credits.js:13` | Content-based deduplication has no holder reference count. | 0.2.1 | No canary: current credit tests stub attribution; they do not test the plugin. | Remove local counts when native removal retains a credit until its last holder releases it. |
|
||||
| `src/globe/navara/credits.js:28` | No separate persistent on-screen credit strip. | 0.2.1 | No canary: UI/API gap; opening the attribution card is only a partial substitute. | Replace forced `attribution.show()` with native on-screen credits. |
|
||||
| `src/globe/navara/bloom.js:59` | Native selective bloom uses emissive/effect masks before the transparent pass; the contract needs whole-frame bloom after overlays. | 0.2.1 | No canary: capability gap; custom effect tests verify our implementation. | Replace the custom bloom descriptor with a native effect only if ordering and settings match. |
|
||||
|
||||
Private-member checks run through `navaraHooks(view)` before engine construction
|
||||
and fail with the member path and pinned version. The current recursive source
|
||||
scan found no private access in `ground/`, `imagery/`, `lines/`, `movers/`, or
|
||||
other adapter subfolders. No private-access moves were necessary.
|
||||
@@ -10,6 +10,61 @@ import { dropOrphanFeatureChanges } from './featureQueue.js';
|
||||
|
||||
export const NAVARA_INTERNALS_VERSION = '0.2.1';
|
||||
|
||||
function requireMember(name, valid) {
|
||||
if (!valid)
|
||||
throw new Error(
|
||||
`Navara ${NAVARA_INTERNALS_VERSION}: required private member ${name} is missing or incompatible`,
|
||||
);
|
||||
}
|
||||
|
||||
/** Fail at startup, before an absent internal can leave tilesLoaded false forever. */
|
||||
export function assertNavaraInternals(view) {
|
||||
requireMember('view._isIdle', typeof view?._isIdle === 'boolean');
|
||||
requireMember(
|
||||
'view._startMainLoop',
|
||||
typeof view?._startMainLoop === 'function',
|
||||
);
|
||||
requireMember(
|
||||
'view._renderer.setAnimationLoop',
|
||||
typeof view?._renderer?.setAnimationLoop === 'function',
|
||||
);
|
||||
for (const name of ['mrt', 'light'])
|
||||
requireMember(
|
||||
`view._scenes.${name}.children`,
|
||||
Array.isArray(view?._scenes?.[name]?.children),
|
||||
);
|
||||
requireMember(
|
||||
'ModelMesh.prototype._releaseGeometryArrays',
|
||||
typeof ModelMesh.prototype._releaseGeometryArrays === 'function',
|
||||
);
|
||||
requireMember(
|
||||
'view._eventManager.stacks.renderable_feature_changed',
|
||||
Array.isArray(view?._eventManager?.stacks?.renderable_feature_changed),
|
||||
);
|
||||
requireMember(
|
||||
'view._eventManager.removeAt',
|
||||
typeof view?._eventManager?.removeAt === 'function',
|
||||
);
|
||||
const ctx = view?.viewContext;
|
||||
for (const name of ['findMesh', 'findEffect'])
|
||||
requireMember(
|
||||
`view.viewContext.${name}`,
|
||||
typeof ctx?.[name] === 'function',
|
||||
);
|
||||
pipelinePassNames(ctx);
|
||||
const handles = {};
|
||||
for (const name of ['sky', 'stars', 'aerialPerspective', 'clouds']) {
|
||||
const ref =
|
||||
name === 'sky' || name === 'stars'
|
||||
? ctx.findMesh(name)
|
||||
: ctx.findEffect(name);
|
||||
if (!ref) continue; // This look does not use the descriptor.
|
||||
requireMember(`${name}.ref.raw`, ref.raw != null);
|
||||
handles[name] = { ref };
|
||||
}
|
||||
forEachLuminanceUniform(handles, () => {});
|
||||
}
|
||||
|
||||
// Navara 0.2.1's animation=false still updates the tile tree every RAF.
|
||||
// Until ThreeView exposes suspend/resume, stop its underlying Three loop
|
||||
// while this view is parked and reinstall its normal loop when it returns.
|
||||
@@ -32,8 +87,10 @@ export function retainTileGeometry() {
|
||||
if (restoreRelease) return restoreRelease;
|
||||
const proto = ModelMesh.prototype;
|
||||
const release = proto._releaseGeometryArrays;
|
||||
if (typeof release !== 'function')
|
||||
throw new Error('ModelMesh._releaseGeometryArrays not found');
|
||||
requireMember(
|
||||
'ModelMesh.prototype._releaseGeometryArrays',
|
||||
typeof release === 'function',
|
||||
);
|
||||
proto._releaseGeometryArrays = function releaseAllButPositions(mesh) {
|
||||
const geometry = mesh.geometry;
|
||||
const position = geometry?.attributes?.position;
|
||||
@@ -85,24 +142,13 @@ export function isIdle(view) {
|
||||
* (the clouds swap that material when their quality changes).
|
||||
* Calls `visit(key, uniform, scaled)` for each, scaled false for the stars.
|
||||
* A handle whose object is built but has lost a path fails loudly: it
|
||||
* throws in the dev server and warns once in a build. Upstream ask: a sky
|
||||
* throws before scaling. Upstream ask: a sky
|
||||
* luminance or atmosphere exposure that leaves unlit content alone.
|
||||
*/
|
||||
const LUMINANCE_UNIFORMS = Object.freeze([
|
||||
'SUN_SPECTRAL_RADIANCE_TO_LUMINANCE',
|
||||
'SKY_SPECTRAL_RADIANCE_TO_LUMINANCE',
|
||||
]);
|
||||
const DEV = Boolean(import.meta.env?.DEV);
|
||||
const reported = new Set();
|
||||
|
||||
function luminancePathLost(name) {
|
||||
const message = `Navara ${NAVARA_INTERNALS_VERSION}: the ${name} luminance uniforms are not where internals.js expects them`;
|
||||
if (DEV) throw new Error(message);
|
||||
if (reported.has(name)) return;
|
||||
reported.add(name);
|
||||
console.warn(`[globe] ${message}; the atmosphere stays unscaled there`);
|
||||
}
|
||||
|
||||
function visitLuminance(name, raw, uniforms, scaled, visit) {
|
||||
if (!raw) return; // not built yet
|
||||
const get = (key) =>
|
||||
@@ -110,8 +156,7 @@ function visitLuminance(name, raw, uniforms, scaled, visit) {
|
||||
const sun = get(LUMINANCE_UNIFORMS[0]);
|
||||
const sky = get(LUMINANCE_UNIFORMS[1]);
|
||||
if (!sun?.value?.isVector3 || !sky?.value?.isVector3) {
|
||||
luminancePathLost(name);
|
||||
return;
|
||||
requireMember(`${name} luminance uniforms`, false);
|
||||
}
|
||||
visit(LUMINANCE_UNIFORMS[0], sun, scaled);
|
||||
visit(LUMINANCE_UNIFORMS[1], sky, scaled);
|
||||
@@ -119,12 +164,24 @@ function visitLuminance(name, raw, uniforms, scaled, visit) {
|
||||
|
||||
export function forEachLuminanceUniform(handles, visit) {
|
||||
const sky = handles.sky?.ref?.raw;
|
||||
visitLuminance('sky', sky, sky?.material?.uniforms, true, visit);
|
||||
visitLuminance(
|
||||
'sky.ref.raw.material.uniforms',
|
||||
sky,
|
||||
sky?.material?.uniforms,
|
||||
true,
|
||||
visit,
|
||||
);
|
||||
const stars = handles.stars?.ref?.raw;
|
||||
visitLuminance('stars', stars, stars?.material?.uniforms, false, visit);
|
||||
visitLuminance(
|
||||
'stars.ref.raw.material.uniforms',
|
||||
stars,
|
||||
stars?.material?.uniforms,
|
||||
false,
|
||||
visit,
|
||||
);
|
||||
const aerial = handles.aerialPerspective?.ref?.raw;
|
||||
visitLuminance(
|
||||
'aerial perspective',
|
||||
'aerialPerspective.ref.raw.rawEffect.uniforms',
|
||||
aerial,
|
||||
aerial?.rawEffect?.uniforms,
|
||||
true,
|
||||
@@ -132,7 +189,7 @@ export function forEachLuminanceUniform(handles, visit) {
|
||||
);
|
||||
const clouds = handles.clouds?.ref?.raw;
|
||||
visitLuminance(
|
||||
'clouds',
|
||||
'clouds.ref.raw.rawEffect.cloudsPass.currentMaterial.uniforms',
|
||||
clouds,
|
||||
clouds?.rawEffect?.cloudsPass?.currentMaterial?.uniforms,
|
||||
true,
|
||||
@@ -177,24 +234,29 @@ export function featureChangeQueue(view) {
|
||||
}
|
||||
|
||||
/** The hooks createNavaraEngine takes. */
|
||||
export const navaraHooks = (view) => ({
|
||||
retainTileGeometry,
|
||||
tileRoots: () => tileRoots(view),
|
||||
isIdle: () => isIdle(view),
|
||||
dropOrphanFeatureChanges: () =>
|
||||
dropOrphanFeatureChanges(
|
||||
featureChangeQueue(view),
|
||||
(id) => view.findLayerById(id) != null,
|
||||
),
|
||||
});
|
||||
export const navaraHooks = (view) => {
|
||||
assertNavaraInternals(view);
|
||||
return {
|
||||
retainTileGeometry,
|
||||
tileRoots: () => tileRoots(view),
|
||||
isIdle: () => isIdle(view),
|
||||
dropOrphanFeatureChanges: () =>
|
||||
dropOrphanFeatureChanges(
|
||||
featureChangeQueue(view),
|
||||
(id) => view.findLayerById(id) != null,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* QA diagnostics only: the names of the passes in the view's
|
||||
* post-processing pipeline, in draw order, or null when the path is gone.
|
||||
* post-processing pipeline, in draw order.
|
||||
* Private: ctx.renderPassOrchestrator.passMap.list ({ name, pass }).
|
||||
* Upstream ask: a public read of the pipeline order.
|
||||
*/
|
||||
export function pipelinePassNames(ctx) {
|
||||
if (ctx == null) return null; // The optional effect has not been created.
|
||||
const list = ctx?.renderPassOrchestrator?.passMap?.list;
|
||||
return Array.isArray(list) ? list.map((entry) => entry.name) : null;
|
||||
requireMember('renderPassOrchestrator.passMap.list', Array.isArray(list));
|
||||
return list.map((entry) => entry.name);
|
||||
}
|
||||
@@ -1,6 +1,21 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync, readdirSync } from 'node:fs';
|
||||
import { registerHooks } from 'node:module';
|
||||
|
||||
// The package's browser entry needs WebGL; only its prototype matters here.
|
||||
const sdkStub =
|
||||
'data:text/javascript,export class ModelMesh { _releaseGeometryArrays() {} }';
|
||||
const loader = registerHooks({
|
||||
resolve(specifier, context, next) {
|
||||
return specifier === '@navaramap/three'
|
||||
? { url: sdkStub, shortCircuit: true }
|
||||
: next(specifier, context);
|
||||
},
|
||||
});
|
||||
const { navaraHooks, forEachLuminanceUniform } = await import('./internals.js');
|
||||
loader.deregister();
|
||||
const { ModelMesh } = await import(sdkStub);
|
||||
|
||||
const here = new URL('./', import.meta.url);
|
||||
const read = (name) => readFileSync(new URL(name, here), 'utf8');
|
||||
@@ -21,7 +36,8 @@ test('Navara internals are pinned: package.json and internals.js name the same e
|
||||
});
|
||||
|
||||
// `x._member`, `x?._member` and `x['_member']`.
|
||||
const PRIVATE_ACCESS = /\.\s*_[A-Za-z]|\[\s*['"`]_[A-Za-z]/;
|
||||
const PRIVATE_ACCESS =
|
||||
/\.\s*_[A-Za-z]|\[\s*['"`]_[A-Za-z]|(?:\.\s*|\[\s*['"`])(?:viewContext|renderPassOrchestrator|cloudsPass|currentMaterial)\b/;
|
||||
|
||||
test('the private-member pattern catches dotted and bracketed access', () => {
|
||||
for (const code of [
|
||||
@@ -29,14 +45,22 @@ test('the private-member pattern catches dotted and bracketed access', () => {
|
||||
'view?._scenes',
|
||||
"view['_isIdle']",
|
||||
'v[ "_x" ]',
|
||||
'view.viewContext',
|
||||
"ctx['renderPassOrchestrator']",
|
||||
'clouds.rawEffect.cloudsPass.currentMaterial',
|
||||
])
|
||||
assert.ok(PRIVATE_ACCESS.test(code), code);
|
||||
for (const code of ['view.isIdle', 'a_b.c', "map['key']"])
|
||||
for (const code of [
|
||||
'view.isIdle',
|
||||
'a_b.c',
|
||||
"map['key']",
|
||||
'aerial.ref.raw.rawEffect.blendMode.opacity',
|
||||
])
|
||||
assert.ok(!PRIVATE_ACCESS.test(code), code);
|
||||
});
|
||||
|
||||
test('only internals.js reaches private Navara members', () => {
|
||||
const offenders = readdirSync(here).filter(
|
||||
const offenders = readdirSync(here, { recursive: true }).filter(
|
||||
(name) =>
|
||||
name.endsWith('.js') &&
|
||||
name !== 'internals.js' &&
|
||||
@@ -45,6 +69,110 @@ test('only internals.js reaches private Navara members', () => {
|
||||
assert.deepEqual(offenders, []);
|
||||
});
|
||||
|
||||
function stubView() {
|
||||
return {
|
||||
_isIdle: false,
|
||||
_startMainLoop() {},
|
||||
_renderer: { setAnimationLoop() {} },
|
||||
_scenes: { mrt: { children: [] }, light: { children: [] } },
|
||||
_eventManager: {
|
||||
stacks: { renderable_feature_changed: [] },
|
||||
removeAt() {},
|
||||
},
|
||||
viewContext: {
|
||||
renderPassOrchestrator: { passMap: { list: [] } },
|
||||
findMesh() {},
|
||||
findEffect() {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('engine startup probes every required view internal', () => {
|
||||
const view = stubView();
|
||||
const hooks = navaraHooks(view);
|
||||
assert.equal(hooks.isIdle(), false);
|
||||
view._isIdle = true;
|
||||
assert.equal(hooks.isIdle(), true);
|
||||
for (const path of [
|
||||
'_isIdle',
|
||||
'_startMainLoop',
|
||||
'_renderer.setAnimationLoop',
|
||||
'_scenes.mrt.children',
|
||||
'_scenes.light.children',
|
||||
'_eventManager.stacks.renderable_feature_changed',
|
||||
'_eventManager.removeAt',
|
||||
'viewContext.findMesh',
|
||||
'viewContext.findEffect',
|
||||
'viewContext.renderPassOrchestrator.passMap.list',
|
||||
]) {
|
||||
for (const value of [undefined, null, 42]) {
|
||||
const broken = stubView();
|
||||
const keys = path.split('.');
|
||||
const key = keys.pop();
|
||||
const parent = keys.reduce((object, name) => object[name], broken);
|
||||
if (value === undefined) delete parent[key];
|
||||
else parent[key] = value;
|
||||
assert.throws(
|
||||
() => navaraHooks(broken),
|
||||
(error) => {
|
||||
assert.ok(error.message.includes('Navara 0.2.1'));
|
||||
assert.ok(
|
||||
error.message.includes(
|
||||
path.replace(
|
||||
'viewContext.renderPassOrchestrator',
|
||||
'renderPassOrchestrator',
|
||||
),
|
||||
),
|
||||
error.message,
|
||||
);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('engine startup rejects a missing tile geometry release method', () => {
|
||||
const release = ModelMesh.prototype._releaseGeometryArrays;
|
||||
delete ModelMesh.prototype._releaseGeometryArrays;
|
||||
try {
|
||||
assert.throws(
|
||||
() => navaraHooks(stubView()),
|
||||
/Navara 0\.2\.1:.*ModelMesh\.prototype\._releaseGeometryArrays/,
|
||||
);
|
||||
} finally {
|
||||
ModelMesh.prototype._releaseGeometryArrays = release;
|
||||
}
|
||||
});
|
||||
|
||||
test('lazy atmosphere materials reject missing private uniforms in every build', () => {
|
||||
for (const name of ['sky', 'stars', 'aerialPerspective', 'clouds']) {
|
||||
assert.doesNotThrow(() =>
|
||||
forEachLuminanceUniform({ [name]: { ref: {} } }, () => {}),
|
||||
);
|
||||
assert.throws(
|
||||
() => forEachLuminanceUniform({ [name]: { ref: { raw: {} } } }, () => {}),
|
||||
(error) =>
|
||||
error.message.includes('Navara 0.2.1') &&
|
||||
error.message.includes(`${name}.ref.raw.`),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('engine startup checks the active look materials before the first frame', () => {
|
||||
for (const name of ['sky', 'stars', 'aerialPerspective', 'clouds']) {
|
||||
const view = stubView();
|
||||
const find = name === 'sky' || name === 'stars' ? 'findMesh' : 'findEffect';
|
||||
view.viewContext[find] = (key) => (key === name ? { raw: {} } : undefined);
|
||||
assert.throws(
|
||||
() => navaraHooks(view),
|
||||
(error) =>
|
||||
error.message.includes('Navara 0.2.1') &&
|
||||
error.message.includes(`${name}.ref.raw.`),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('feature changes of a deleted vector layer leave the queue; live ones stay', async () => {
|
||||
const { dropOrphanFeatureChanges } = await import('./featureQueue.js');
|
||||
const freed = [];
|
||||
|
||||
Reference in new issue
Block a user