mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
Supersedes #218 by @spcpza, ported onto the current component layout with fixes from review. Verified before merge: format, package boundaries, unit suite, build, and the browser tracking suite; a browser harness for the change where one applies; adversarial review applied. Contributor: @spcpza. His original commit is the base of this branch, and his own re-port onto main (2026-09-15, `5b69011`) was consulted while finishing the height work. That port reads every vehicle's surface from the app's shared ground-floor system (`cachedGroundFloor` / `warmGroundFloor` / `GROUND_FLOOR_LIFT_M`) rather than sampling the scene per layer, keeps a vehicle hidden until its cell answers, warms a bounded batch per poll on a timer instead of per frame, and renders at the ellipsoid above the altitude gate. That is the sounder primitive — one floor system for the app, with the rooftop-versus-street question already settled against a datum-correct DEM prior — and this branch's height work was rebuilt on it, replacing a bespoke cell cache, ground prior and rooftop rule written here. Credit for that direction is his. Future work (not in this PR), both written up in docs/CURRENT-STATE.md: - Readable route names. The realtime feeds carry only `route_id`; `route_short_name` / `route_long_name` live in each operator's static GTFS `routes.txt`, and headsigns in `trips.txt`. Doing it properly wants a server-side metadata loader beside the transit proxy with fixed registered static sources, bounded indexes built once and persisted, a slow conditional refresh, and a versioned route dictionary the browser caches. Shipped here: ids are never rewritten, and a cut one carries an ellipsis instead of reading as a typo. - Below-ground rendering. Subway vehicles are drawn on the surface directly above themselves, because the feeds carry no depth and a train nobody can see is not worth drawing; the card says so plainly. Real treatment wants OSM tunnel geometry per city and a ghosted sprite tethered to the surface. The owner recorded it and left it. Round four and after (2026-09-15). The owner tested round three under FLIR and NVG, and then asked for the design to be taken further: elegant in performance, beautiful on camera, trails with a cache for public transport, Cesium-native, and a core other moving layers can adopt. Codex Astra reviewed the plan (it confirmed the speed fault: a glide started from the drawn position at receipt time but spanned the gap between report times, so with MBTA reporting every ~10 s against a 15 s poll every second glide ran at 1.5x; it rejected a queued-segment patch), then wrote the design, then implemented it in commit-sized passes with adversarial review between them and real-browser harness runs by the integrator. What this PR now contains beyond the contributor's original port: - Delayed playback in a contact-agnostic core (`src/data/contactPlayback.js`): each vehicle keeps a typed, bounded fix history and a display clock that runs at one second per second through it, a feed-derived lag behind real time, holding at its newest fix rather than extrapolating and never sprinting to catch up. Marker, course, motion word, card and trail all read the same displayed sample. The clock has an explicit time-offset and seek API so a rewind is a later feature, not a rewrite; flights and vessels are untouched. - A selected-vehicle trail (`src/data/contactTrailRenderer.js`, `src/layers/transit/trails.js`): the retained history drawn with batched Cesium primitives, clipped at display time so the head is exactly the sprite, faded by age, in the mode colour (white under sensor presets), on the same surface path as the marker. - Bounded proxy-side history (`server/providers/transitHistory.js`): fifteen minutes and at most 128 fixes per vehicle with hard byte ceilings, MBTA only for now (opt-in per feed), served by a small endpoint that never fetches upstream, so a selected trail survives a browser reload while the server keeps running. - Scene work bounded to what is visible and moving, an occluder, cached DETECT candidates, holds on the render loop only while something visible moves. - Sensor presets: white-hot haloed sprites under NVG/FLIR/noir (black-hot inverts), saturated mode colours under CRT, per-mode bracket tiers in every theme, the selected card keeping its mode colour. - A discriminating browser harness (`scripts/qa-transit.mjs`) that samples postRender frames, reads the real card anchor, and measures fleet budgets at 800 and 3,000 vehicles. Not built, recorded as follow-ups: curated route display names (lowest priority per the owner), fleet-wide tails, route snapping, a rewind UI, adoption of the playback core by flights and vessels. Verified at merge (2026-09-15 morning): unit suite 3,896 passing, build clean, format and boundary gates clean, tracking regression 109/109, browser harness keyless 200 pass / 0 fail (13 sensor checks unexercised because the live view offered fewer than six isolated sprites; every judged sprite passed), keyed on Google 3D tiles 198 pass with the per-frame allocation gate as the only failure. Follow-ups recorded: the keyed build's playback sampler still allocates per frame (CPU and frame time stay within budget; the 3,000-vehicle metro view sits at a 27.6 ms frame interval against a 25 ms budget), the shared detection overlay's per-paint allocation, friendlier route names, fleet-wide tails, a rewind UI, and adopting the playback core for vessels and flights. Co-authored-by: spcpza <[email protected]>
963 lines
36 KiB
JavaScript
963 lines
36 KiB
JavaScript
import { readShellSource } from '../testSupport/readShellSource.mjs';
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import * as Cesium from 'cesium';
|
|
import {
|
|
countFadingRenderEntries,
|
|
destroyDetection,
|
|
detectionDebugRequested,
|
|
getDetectionDiagnostics,
|
|
getDetectionTheme,
|
|
getMode,
|
|
initDetection,
|
|
isDetectionSuspended,
|
|
resumeDetection,
|
|
setDetectionStyle,
|
|
setDetectionTuning,
|
|
setMode,
|
|
suspendDetection,
|
|
} from './detection.js';
|
|
import {
|
|
destroyWorldOverlay,
|
|
initWorldOverlay,
|
|
setOverlayEntries,
|
|
} from '../overlays/worldOverlay.js';
|
|
import { DETECTION_THEME_MAP } from '../overlays/worldOverlayTokens.js';
|
|
import { composeLabel, resolveTier } from './detectionDraw.js';
|
|
|
|
test('detection diagnostics count rendered fading rows instead of absent selected identities', () => {
|
|
assert.equal(countFadingRenderEntries([
|
|
{ selected: true },
|
|
{ selected: false },
|
|
{ selected: true },
|
|
{ selected: false },
|
|
]), 2);
|
|
assert.equal(countFadingRenderEntries([{ selected: true }, { selected: true }]), 0);
|
|
});
|
|
|
|
class MockEvent {
|
|
constructor() { this.listeners = new Set(); }
|
|
|
|
addEventListener(listener) {
|
|
this.listeners.add(listener);
|
|
return () => this.listeners.delete(listener);
|
|
}
|
|
|
|
raise() {
|
|
for (const listener of [...this.listeners]) listener();
|
|
}
|
|
}
|
|
|
|
class MockPath2D {
|
|
moveTo() {}
|
|
lineTo() {}
|
|
roundRect() {}
|
|
arcTo() {}
|
|
closePath() {}
|
|
}
|
|
|
|
function mockContext(target, trace) {
|
|
const calls = [];
|
|
const record = (...call) => {
|
|
calls.push(call);
|
|
trace.push([target, ...call]);
|
|
};
|
|
let filter = 'none';
|
|
let strokeStyle = '';
|
|
let globalCompositeOperation = 'source-over';
|
|
return {
|
|
calls,
|
|
font: '',
|
|
get filter() { return filter; },
|
|
set filter(value) { filter = value; record('filter', value); },
|
|
fillStyle: '',
|
|
get strokeStyle() { return strokeStyle; },
|
|
set strokeStyle(value) { strokeStyle = value; record('strokeStyle', value); },
|
|
globalAlpha: 1,
|
|
get globalCompositeOperation() { return globalCompositeOperation; },
|
|
set globalCompositeOperation(value) { globalCompositeOperation = value; record('globalCompositeOperation', value); },
|
|
lineWidth: 1,
|
|
measureText(text) { return { width: String(text).length * 6 }; },
|
|
setTransform(...args) { record('setTransform', ...args); },
|
|
clearRect(...args) { record('clearRect', ...args); },
|
|
save() { record('save'); },
|
|
restore() { record('restore'); },
|
|
translate(...args) { record('translate', ...args); },
|
|
scale(...args) { record('scale', ...args); },
|
|
beginPath() { record('beginPath'); },
|
|
rect(...args) { record('rect', ...args); },
|
|
clip(...args) { record('clip', ...args); },
|
|
moveTo(...args) { record('moveTo', ...args); },
|
|
lineTo(...args) { record('lineTo', ...args); },
|
|
arc(...args) { record('arc', ...args); },
|
|
arcTo(...args) { record('arcTo', ...args); },
|
|
closePath() { record('closePath'); },
|
|
// The style is recorded AT fill time, not on assignment, so a pin can prove
|
|
// which plate token actually reached the canvas rather than which one was
|
|
// set at some point during the frame. globalAlpha rides along for the same
|
|
// reason: the backdrop feather is an alpha, not a colour.
|
|
fill(path) { record('fill', path, this.fillStyle, this.globalAlpha); },
|
|
stroke(path) { record('stroke', path, this.strokeStyle, this.lineWidth); },
|
|
fillRect(...args) { record('fillRect', ...args); },
|
|
fillText(...args) { record('fillText', ...args); },
|
|
drawImage(...args) { record('drawImage', ...args); },
|
|
};
|
|
}
|
|
|
|
// `search` seeds window.location.search. The detection mode banner is developer
|
|
// telemetry gated behind ?detectDebug=1, so any test that uses the banner as its
|
|
// "did this frame repaint" probe must opt in explicitly.
|
|
function installEnvironment({ width = 800, height = 600, dpr = 2, search = '' } = {}) {
|
|
const byId = new Map();
|
|
const paintTrace = [];
|
|
const ctx = mockContext('shared', paintTrace);
|
|
const detectionCtx = mockContext('detection', paintTrace);
|
|
const originalPerformance = Object.getOwnPropertyDescriptor(globalThis, 'performance');
|
|
const originalDateNow = Date.now;
|
|
const originalPath2D = globalThis.Path2D;
|
|
let currentTime = 1_000;
|
|
let performanceStep = 0;
|
|
Object.defineProperty(globalThis, 'performance', {
|
|
configurable: true,
|
|
value: { now: () => {
|
|
const value = currentTime;
|
|
currentTime += performanceStep;
|
|
return value;
|
|
} },
|
|
});
|
|
Date.now = () => currentTime;
|
|
globalThis.Path2D = MockPath2D;
|
|
|
|
class MockElement {
|
|
constructor(tagName) {
|
|
this.tagName = tagName.toUpperCase();
|
|
this.id = '';
|
|
this.children = [];
|
|
this.parentElement = null;
|
|
this.style = {};
|
|
this.dataset = {};
|
|
this.hidden = false;
|
|
this.width = 0;
|
|
this.height = 0;
|
|
this.clientWidth = width;
|
|
this.clientHeight = height;
|
|
this._rect = { left: 0, top: 0, width, height };
|
|
}
|
|
|
|
appendChild(child) {
|
|
child.parentElement = this;
|
|
this.children.push(child);
|
|
register(child);
|
|
return child;
|
|
}
|
|
|
|
insertBefore(child, before) {
|
|
child.parentElement = this;
|
|
const index = this.children.indexOf(before);
|
|
if (index < 0) this.children.push(child);
|
|
else this.children.splice(index, 0, child);
|
|
register(child);
|
|
return child;
|
|
}
|
|
|
|
setAttribute(name, value) {
|
|
if (name === 'id') this.id = String(value);
|
|
else this[name] = String(value);
|
|
register(this);
|
|
}
|
|
|
|
getBoundingClientRect() { return this._rect; }
|
|
getContext() {
|
|
if (this.tagName !== 'CANVAS') return null;
|
|
return this.id === 'world-overlay-detection-surface' ? detectionCtx : ctx;
|
|
}
|
|
|
|
querySelector(selector) {
|
|
const wanted = selector.slice(1);
|
|
return this.children.find((child) => child.id === wanted) || null;
|
|
}
|
|
|
|
remove() {
|
|
if (this.parentElement) {
|
|
this.parentElement.children = this.parentElement.children.filter((child) => child !== this);
|
|
}
|
|
unregister(this);
|
|
this.parentElement = null;
|
|
}
|
|
|
|
get nextSibling() { return null; }
|
|
}
|
|
|
|
function register(element) {
|
|
if (element.id) byId.set(element.id, element);
|
|
for (const child of element.children) register(child);
|
|
}
|
|
|
|
function unregister(element) {
|
|
if (element.id) byId.delete(element.id);
|
|
for (const child of element.children) unregister(child);
|
|
}
|
|
|
|
const body = new MockElement('body');
|
|
body.classList = { contains() { return false; } };
|
|
const document = {
|
|
body,
|
|
createElement(tagName) { return new MockElement(tagName); },
|
|
getElementById(id) { return byId.get(id) || null; },
|
|
querySelector(selector) { return byId.get(selector.slice(1)) || null; },
|
|
querySelectorAll(selector) {
|
|
const element = selector.startsWith('#') ? byId.get(selector.slice(1)) : null;
|
|
return element ? [element] : [];
|
|
},
|
|
};
|
|
const window = {
|
|
devicePixelRatio: dpr,
|
|
location: { search },
|
|
addEventListener() {},
|
|
removeEventListener() {},
|
|
getComputedStyle() { return { display: 'block', visibility: 'visible', opacity: '1' }; },
|
|
};
|
|
globalThis.ResizeObserver = class { observe() {} disconnect() {} };
|
|
globalThis.MutationObserver = class { observe() {} disconnect() {} };
|
|
globalThis.document = document;
|
|
globalThis.window = window;
|
|
|
|
const container = new MockElement('div');
|
|
container.id = 'cesiumContainer';
|
|
body.appendChild(container);
|
|
const root = new MockElement('div');
|
|
root.id = 'world-overlay-root';
|
|
body.appendChild(root);
|
|
const canvas = new MockElement('canvas');
|
|
canvas.id = 'world-overlay-canvas';
|
|
root.appendChild(canvas);
|
|
|
|
const postRender = new MockEvent();
|
|
const viewer = {
|
|
container,
|
|
canvas: { clientWidth: width, clientHeight: height },
|
|
camera: {
|
|
positionWC: new Cesium.Cartesian3(0, 0, 10_000_000),
|
|
positionCartographic: { height: 1_000_000 },
|
|
viewMatrix: Cesium.Matrix4.clone(Cesium.Matrix4.IDENTITY),
|
|
frustum: { projectionMatrix: Cesium.Matrix4.clone(Cesium.Matrix4.IDENTITY) },
|
|
moveEnd: new MockEvent(),
|
|
},
|
|
scene: { postRender, requestRender() {} },
|
|
};
|
|
|
|
return {
|
|
viewer,
|
|
postRender,
|
|
document,
|
|
ctx,
|
|
detectionCtx,
|
|
paintTrace,
|
|
advance(ms) { currentTime += ms; },
|
|
setPerformanceStep(ms) { performanceStep = ms; },
|
|
cleanup() {
|
|
destroyDetection();
|
|
destroyWorldOverlay();
|
|
Date.now = originalDateNow;
|
|
if (originalPath2D === undefined) delete globalThis.Path2D;
|
|
else globalThis.Path2D = originalPath2D;
|
|
delete globalThis.document;
|
|
delete globalThis.window;
|
|
delete globalThis.ResizeObserver;
|
|
delete globalThis.MutationObserver;
|
|
if (originalPerformance) Object.defineProperty(globalThis, 'performance', originalPerformance);
|
|
else delete globalThis.performance;
|
|
},
|
|
};
|
|
}
|
|
|
|
function detectableLayer() {
|
|
const positions = [
|
|
new Cesium.Cartesian3(0, 0, 6_356_752),
|
|
new Cesium.Cartesian3(10_000, 0, 6_356_740),
|
|
];
|
|
return {
|
|
id: 'flights',
|
|
getDetectableObjects() {
|
|
return positions.map((position, index) => ({
|
|
position,
|
|
sourceId: `flight-${index}`,
|
|
id: `TEST${index}`,
|
|
metric: `FL${300 + index}`,
|
|
type: 'AIR',
|
|
}));
|
|
},
|
|
};
|
|
}
|
|
|
|
function militaryLayer() {
|
|
return {
|
|
id: 'military',
|
|
getDetectableObjects() {
|
|
return [{
|
|
position: new Cesium.Cartesian3(0, 0, 6_356_752),
|
|
sourceId: 'military-pin',
|
|
id: 'MIL-PIN',
|
|
metric: 'FL450',
|
|
type: 'AIR',
|
|
tier: 'military',
|
|
}];
|
|
},
|
|
};
|
|
}
|
|
|
|
// One air contact and one space contact, both on screen. The mock camera
|
|
// projects through an identity view-projection, so a position's x/y ARE its
|
|
// normalized device coordinates; these two land ~320px apart and both survive
|
|
// the declutter.
|
|
function mixedTierLayer() {
|
|
return {
|
|
id: 'mixed',
|
|
getDetectableObjects() {
|
|
return [
|
|
{
|
|
position: new Cesium.Cartesian3(-0.4, 0.2, 6_356_752),
|
|
sourceId: 'air-1',
|
|
id: 'AIRONE',
|
|
metric: 'FL350',
|
|
type: 'AIR',
|
|
},
|
|
{
|
|
position: new Cesium.Cartesian3(0.4, -0.2, 6_356_752),
|
|
sourceId: 'sat-1',
|
|
id: 'SATONE',
|
|
metric: '412KM',
|
|
type: 'SAT',
|
|
},
|
|
];
|
|
},
|
|
};
|
|
}
|
|
|
|
/** Fill styles that actually reached the shared normal-blend canvas. */
|
|
function sharedFillStyles(env) {
|
|
return env.ctx.calls
|
|
.filter(([name]) => name === 'fill')
|
|
.map(([, , fillStyle]) => fillStyle);
|
|
}
|
|
|
|
/** Settle a frame so the arbiter has promoted its identities to render entries. */
|
|
function settleFrame(env) {
|
|
env.advance(250);
|
|
env.postRender.raise();
|
|
}
|
|
|
|
test('detection lifecycle re-hosts unchanged painters behind the sole host listener', () => {
|
|
// Opts into the telemetry banner: this test reads it as the repaint probe.
|
|
const env = installEnvironment({ search: '?detectDebug=1' });
|
|
const modes = [];
|
|
try {
|
|
initWorldOverlay(env.viewer);
|
|
initDetection(env.viewer, [detectableLayer()], (mode) => modes.push(mode));
|
|
assert.equal(env.postRender.listeners.size, 1);
|
|
assert.equal(env.document.getElementById('detection-overlay'), null);
|
|
assert.ok(env.document.getElementById('world-overlay-canvas'));
|
|
const surface = env.document.getElementById('world-overlay-detection-surface');
|
|
const root = env.document.getElementById('world-overlay-root');
|
|
const mainCanvas = env.document.getElementById('world-overlay-canvas');
|
|
assert.ok(surface);
|
|
// The surface must sit in the Cesium container (no stacking context
|
|
// between it and the WebGL canvas) or its `screen` blend is discarded;
|
|
// ordering under the card canvas is carried by z-index, not siblinghood.
|
|
assert.equal(surface.parentElement, env.viewer.container);
|
|
assert.deepEqual(root.children, [mainCanvas]);
|
|
assert.equal(surface.style.mixBlendMode, 'screen');
|
|
assert.equal(
|
|
surface.style.filter,
|
|
'contrast(1.05) saturate(1.05) drop-shadow(0 0 3px rgba(0, 244, 255, 0.4))',
|
|
);
|
|
assert.equal(surface.style.display, 'none');
|
|
|
|
setMode('DENSE');
|
|
env.advance(250);
|
|
env.postRender.raise();
|
|
const first = getDetectionDiagnostics();
|
|
assert.equal(first.profile, 'DENSE');
|
|
assert.ok(Array.isArray(first.calloutRects));
|
|
if (first.calloutRects.length) {
|
|
const x=first.calloutRects[0].x;
|
|
first.calloutRects[0].x=NaN;
|
|
assert.equal(getDetectionDiagnostics().calloutRects[0].x,x,'QA owns a copy, never a live plate');
|
|
}
|
|
assert.equal(first.observationCount, 2);
|
|
assert.equal(first.didSolve, true);
|
|
assert.ok(first.solveRevision > 0);
|
|
assert.ok(first.collectiveLabelBudget > 0);
|
|
assert.ok(first.bracketOpacityCounts.full + first.bracketOpacityCounts.partial > 0);
|
|
assert.ok(env.detectionCtx.calls.some(([name, text]) => name === 'fillText'
|
|
&& String(text).startsWith('DENSE VIS:')));
|
|
assert.ok(env.detectionCtx.calls.some(([name, path]) => name === 'stroke' && path instanceof MockPath2D));
|
|
assert.ok(env.detectionCtx.calls.some(([name]) => name === 'fillRect'), 'scanlines and banner still paint');
|
|
assert.equal(env.detectionCtx.globalCompositeOperation, 'source-over');
|
|
assert.equal(env.detectionCtx.filter, 'none');
|
|
assert.equal(surface.style.display, 'block');
|
|
const canvas = env.document.getElementById('world-overlay-canvas');
|
|
assert.equal(Number(canvas.dataset.solveRevision), first.solveRevision);
|
|
assert.equal(canvas.dataset.profile, 'DENSE');
|
|
|
|
// The existing 125 ms arbiter cadence remains authoritative.
|
|
env.advance(124);
|
|
env.postRender.raise();
|
|
assert.equal(getDetectionDiagnostics().solveRevision, first.solveRevision);
|
|
// Callsigns paint on the SHARED normal-blend canvas, never on the
|
|
// screen-blended sensor surface: `screen` can only lighten, so a callout's
|
|
// dark backing plate is a no-op over sunlit ground and the text dissolves
|
|
// into it. Brackets, banner and scanlines stay on the sensor surface.
|
|
assert.ok(env.ctx.calls.some(([name, text]) => name === 'fillText' && text === 'TEST0'));
|
|
assert.ok(!env.detectionCtx.calls.some(([name, text]) => name === 'fillText' && text === 'TEST0'));
|
|
env.advance(1);
|
|
env.postRender.raise();
|
|
assert.ok(getDetectionDiagnostics().solveRevision > first.solveRevision);
|
|
|
|
setDetectionTuning({ densityPct: 25, allocationStrategy: 'WEIGHTED' });
|
|
env.postRender.raise();
|
|
assert.equal(getMode(), 'SPARSE');
|
|
assert.equal(getDetectionDiagnostics().profile, 'SPARSE');
|
|
assert.equal(getDetectionDiagnostics().allocationStrategy, 'WEIGHTED');
|
|
assert.ok(env.detectionCtx.calls.some(([name]) => name === 'arc'), 'sparse focus ring remains available');
|
|
|
|
setMode('BALANCED');
|
|
env.postRender.raise();
|
|
assert.equal(getDetectionDiagnostics().profile, 'BALANCED');
|
|
setMode('DENSE');
|
|
env.postRender.raise();
|
|
assert.equal(getDetectionDiagnostics().profile, 'DENSE');
|
|
|
|
const bannerCount = () => env.detectionCtx.calls.filter(([name, text]) => (
|
|
name === 'fillText' && /^(SPARSE|BALANCED|DENSE) VIS:/.test(String(text))
|
|
)).length;
|
|
const beforeSuspend = bannerCount();
|
|
suspendDetection('intercity');
|
|
assert.equal(isDetectionSuspended(), true);
|
|
assert.equal(surface.style.opacity, '0');
|
|
env.postRender.raise();
|
|
assert.equal(bannerCount(), beforeSuspend);
|
|
assert.equal(env.postRender.listeners.size, 1);
|
|
resumeDetection();
|
|
env.postRender.raise();
|
|
assert.equal(isDetectionSuspended(), false);
|
|
assert.equal(surface.style.opacity, '1');
|
|
assert.ok(bannerCount() > beforeSuspend);
|
|
|
|
// These are the exact engine calls made by military style presets after
|
|
// their production UI gate chooses CRT/NVG/FLIR defaults.
|
|
const expectedThemes = {
|
|
retro: 'contrast(1.08) saturate(1.04) drop-shadow(0 0 3px rgba(255, 176, 56, 0.45))',
|
|
surveillance: 'contrast(1.12) saturate(1.12) drop-shadow(0 0 3px rgba(120, 255, 120, 0.42))',
|
|
thermal: 'contrast(1.1) saturate(1.08) drop-shadow(0 0 3px rgba(255, 224, 170, 0.42))',
|
|
};
|
|
for (const style of ['retro', 'surveillance', 'thermal']) {
|
|
setMode('OFF');
|
|
setDetectionStyle(style);
|
|
assert.equal(surface.style.mixBlendMode, 'screen');
|
|
assert.equal(surface.style.filter, expectedThemes[style]);
|
|
setDetectionTuning({ densityPct: 75 });
|
|
setMode('DENSE');
|
|
env.postRender.raise();
|
|
assert.equal(getDetectionDiagnostics().profile, 'DENSE');
|
|
assert.ok(getDetectionTheme().line);
|
|
assert.equal(env.postRender.listeners.size, 1);
|
|
}
|
|
|
|
setMode('OFF');
|
|
setDetectionStyle('normal');
|
|
assert.equal(surface.style.mixBlendMode, 'screen');
|
|
assert.equal(
|
|
surface.style.filter,
|
|
'contrast(1.05) saturate(1.05) drop-shadow(0 0 3px rgba(0, 244, 255, 0.4))',
|
|
);
|
|
env.postRender.raise();
|
|
assert.equal(getMode(), 'OFF');
|
|
assert.equal(surface.style.display, 'none');
|
|
assert.equal(env.postRender.listeners.size, 1);
|
|
assert.ok(modes.includes('SPARSE') && modes.includes('BALANCED') && modes.includes('DENSE'));
|
|
|
|
destroyDetection();
|
|
assert.equal(env.postRender.listeners.size, 1, 'detection never owns the listener');
|
|
setMode('DENSE');
|
|
env.postRender.raise();
|
|
assert.equal(env.document.getElementById('detection-overlay'), null);
|
|
destroyWorldOverlay();
|
|
assert.equal(env.postRender.listeners.size, 0);
|
|
assert.equal(env.document.getElementById('world-overlay-canvas'), null);
|
|
assert.equal(env.document.getElementById('world-overlay-detection-surface'), null);
|
|
} finally {
|
|
env.cleanup();
|
|
}
|
|
});
|
|
|
|
test('callouts stop painting the moment the last detectable object goes away', () => {
|
|
// The callout lane replays the previous solve on frames the sensor lane
|
|
// skips under load, which is what keeps plates from strobing against
|
|
// persistent brackets. The cost of that design is that an empty field MUST
|
|
// clear the replay buffer, or the final callsigns stay stranded on the
|
|
// shared canvas after the last data layer is switched off.
|
|
const env = installEnvironment();
|
|
let objects = [{
|
|
position: new Cesium.Cartesian3(0, 0, 6_356_752),
|
|
sourceId: 'lonely-1',
|
|
id: 'LASTONE',
|
|
metric: 'FL120',
|
|
type: 'AIR',
|
|
}];
|
|
try {
|
|
initWorldOverlay(env.viewer);
|
|
initDetection(env.viewer, [{ id: 'flights', getDetectableObjects: () => objects }], () => {});
|
|
setMode('DENSE');
|
|
env.advance(250);
|
|
env.postRender.raise();
|
|
// The first frame solves; the arbiter hands the identity back as a render
|
|
// entry on the frame after that.
|
|
env.advance(250);
|
|
env.postRender.raise();
|
|
assert.ok(
|
|
env.ctx.calls.some(([name, text]) => name === 'fillText' && text === 'LASTONE'),
|
|
'the callout paints on the shared canvas while the contact exists',
|
|
);
|
|
|
|
objects = [];
|
|
env.ctx.calls.length = 0;
|
|
env.advance(250);
|
|
env.postRender.raise();
|
|
env.advance(250);
|
|
env.postRender.raise();
|
|
assert.ok(
|
|
!env.ctx.calls.some(([name, text]) => name === 'fillText' && text === 'LASTONE'),
|
|
'an empty field must drop the replay buffer, not strand the last callsign',
|
|
);
|
|
} finally {
|
|
env.cleanup();
|
|
}
|
|
});
|
|
|
|
test('every style paints its OWN plate token through the production callout path', () => {
|
|
// The token table and the direct painter are both pinned elsewhere, but a
|
|
// pin on either one still passes if the runtime stops consulting the active
|
|
// theme and freezes on a single plate. This drives the real stash → replay
|
|
// path once per style and reads the fill style the shared canvas received.
|
|
const env = installEnvironment();
|
|
try {
|
|
initWorldOverlay(env.viewer);
|
|
initDetection(env.viewer, [mixedTierLayer()], () => {});
|
|
setMode('DENSE');
|
|
settleFrame(env);
|
|
settleFrame(env);
|
|
|
|
for (const style of ['surveillance', 'retro', 'thermal', 'normal']) {
|
|
setDetectionStyle(style);
|
|
settleFrame(env);
|
|
env.ctx.calls.length = 0;
|
|
settleFrame(env);
|
|
|
|
const theme = DETECTION_THEME_MAP[style] || DETECTION_THEME_MAP._default;
|
|
const fills = sharedFillStyles(env);
|
|
assert.ok(
|
|
fills.includes(theme.calloutPlate),
|
|
`${style} must paint its own callout plate (${theme.calloutPlate})`,
|
|
);
|
|
for (const [otherName, other] of Object.entries(DETECTION_THEME_MAP)) {
|
|
if (other.calloutPlate === theme.calloutPlate) continue;
|
|
assert.ok(
|
|
!fills.includes(other.calloutPlate),
|
|
`${style} must not paint the ${otherName} plate`,
|
|
);
|
|
}
|
|
}
|
|
} finally {
|
|
env.cleanup();
|
|
}
|
|
});
|
|
|
|
test('space-tier callouts take the heavier plate while air contacts keep the light one', () => {
|
|
// Satellites sit over the high-albedo lit Earth disc far more often than
|
|
// aircraft do, so the space tier carries the heavier backing. Losing the
|
|
// tier split is invisible to the token table — only a frame carrying BOTH
|
|
// tiers at once can show it.
|
|
const env = installEnvironment();
|
|
try {
|
|
initWorldOverlay(env.viewer);
|
|
initDetection(env.viewer, [mixedTierLayer()], () => {});
|
|
setDetectionStyle('surveillance');
|
|
setMode('DENSE');
|
|
settleFrame(env);
|
|
env.ctx.calls.length = 0;
|
|
settleFrame(env);
|
|
|
|
const theme = DETECTION_THEME_MAP.surveillance;
|
|
assert.notEqual(
|
|
theme.calloutPlate,
|
|
theme.calloutPlateSpace,
|
|
'the fixture is only meaningful while the two tiers differ',
|
|
);
|
|
const painted = (text) => env.ctx.calls.some(([name, value]) => name === 'fillText' && value === text);
|
|
assert.ok(painted('AIRONE') && painted('SATONE'), 'both tiers reached the frame under test');
|
|
|
|
const fills = sharedFillStyles(env);
|
|
assert.ok(fills.includes(theme.calloutPlate), 'the air contact keeps the light plate');
|
|
assert.ok(fills.includes(theme.calloutPlateSpace), 'the space contact takes the heavier plate');
|
|
} finally {
|
|
env.cleanup();
|
|
}
|
|
});
|
|
|
|
test('surveillance military tier reaches the dedicated render target as shipped red', () => {
|
|
const env = installEnvironment();
|
|
try {
|
|
initWorldOverlay(env.viewer);
|
|
initDetection(env.viewer, [militaryLayer()], () => {});
|
|
setDetectionStyle('surveillance');
|
|
setMode('DENSE');
|
|
env.advance(250);
|
|
env.postRender.raise();
|
|
|
|
assert.ok(
|
|
env.detectionCtx.calls.some(([name, value]) => name === 'strokeStyle' && value === '#ff5a47'),
|
|
'the production projection/tier/batched-bracket paint path emits surveillance military red',
|
|
);
|
|
assert.ok(env.detectionCtx.calls.some(([name, path]) => name === 'stroke'
|
|
&& path instanceof MockPath2D));
|
|
} finally {
|
|
env.cleanup();
|
|
}
|
|
});
|
|
|
|
test('pathological detection paint holds alternate frames without freezing shared lanes', () => {
|
|
// Opts into the telemetry banner: the held-frame assertions count banner paints.
|
|
const env = installEnvironment({ search: '?detectDebug=1' });
|
|
try {
|
|
initWorldOverlay(env.viewer);
|
|
initDetection(env.viewer, [detectableLayer()], () => {});
|
|
setOverlayEntries('valve-card', [{
|
|
id: 'ambient-card',
|
|
position: new Cesium.Cartesian3(0, 0, 0),
|
|
variant: 'label',
|
|
title: 'AMBIENT-CONTINUES',
|
|
selected: true,
|
|
protected: true,
|
|
horizonCull: false,
|
|
edgeFade: 'none',
|
|
}]);
|
|
setMode('DENSE');
|
|
env.advance(250);
|
|
env.setPerformanceStep(30);
|
|
|
|
env.postRender.raise();
|
|
env.postRender.raise();
|
|
const detectionClears = env.detectionCtx.calls.filter(([name]) => name === 'clearRect').length;
|
|
const detectionBanners = env.detectionCtx.calls.filter(([name, text]) => name === 'fillText'
|
|
&& String(text).startsWith('DENSE VIS:')).length;
|
|
const sharedClears = env.ctx.calls.filter(([name]) => name === 'clearRect').length;
|
|
const sharedCards = env.ctx.calls.filter(([name, text]) => name === 'fillText'
|
|
&& text === 'AMBIENT-CONTINUES').length;
|
|
|
|
env.postRender.raise();
|
|
assert.equal(env.detectionCtx.calls.filter(([name]) => name === 'clearRect').length, detectionClears,
|
|
'the host preserves detection pixels on the held odd frame');
|
|
assert.equal(env.detectionCtx.calls.filter(([name, text]) => name === 'fillText'
|
|
&& String(text).startsWith('DENSE VIS:')).length, detectionBanners);
|
|
assert.equal(env.ctx.calls.filter(([name]) => name === 'clearRect').length, sharedClears + 1,
|
|
'the shared surface still clears');
|
|
assert.equal(env.ctx.calls.filter(([name, text]) => name === 'fillText'
|
|
&& text === 'AMBIENT-CONTINUES').length, sharedCards + 1,
|
|
'the shared lane still repaints');
|
|
assert.equal(getDetectionDiagnostics().throttleSkipCount, 1);
|
|
|
|
env.postRender.raise();
|
|
assert.equal(env.detectionCtx.calls.filter(([name]) => name === 'clearRect').length,
|
|
detectionClears + 1, 'the next even frame repaints detection');
|
|
assert.equal(getDetectionDiagnostics().throttleSkipCount, 1);
|
|
} finally {
|
|
env.cleanup();
|
|
}
|
|
});
|
|
|
|
test('detection cannot resurrect a private canvas, listener, matrix, resize, clear, or UI inventory', () => {
|
|
const source = readFileSync(new URL('./detection.js', import.meta.url), 'utf8');
|
|
const uiSource = readShellSource()
|
|
+ '\n' + readFileSync(new URL('../ui/visualPresets.js', import.meta.url), 'utf8');
|
|
assert.doesNotMatch(source, /createElement\(\s*['"]canvas['"]\s*\)/);
|
|
assert.doesNotMatch(source, /postRender\.addEventListener/);
|
|
assert.doesNotMatch(source, /['"]detection-overlay['"]/);
|
|
assert.doesNotMatch(source, /devicePixelRatio/);
|
|
assert.doesNotMatch(source, /clearRect\(/);
|
|
assert.doesNotMatch(source, /Matrix4\.multiply/);
|
|
assert.doesNotMatch(source, /CALLOUT_OCCLUSION_SELECTORS/);
|
|
assert.doesNotMatch(source, /_ctx\.(?:filter|globalCompositeOperation)\s*=/);
|
|
assert.match(source, /_hostSurface\.style\.mixBlendMode = _theme\.blend;/);
|
|
assert.match(source, /_hostSurface\.style\.filter = `\$\{_theme\.filter\} drop-shadow\(0 0 \$\{GLOW_PX\}px \$\{_theme\.glow\}\)`;/);
|
|
assert.match(source, /const LABEL_SOLVE_INTERVAL_MS = 125;/);
|
|
assert.match(source, /const _labelArbiter = new LabelArbiter\(\);/);
|
|
assert.match(source, /const clipW = vp3 \* px \+ vp7 \* py \+ vp11 \* pz \+ vp15;/);
|
|
assert.match(source, /registerWorldOverlayPaintLane\('detection', _paintDetectionLane/);
|
|
assert.match(source, /target: 'detection'/);
|
|
assert.match(source, /shouldPaint: _shouldPaintDetectionLane/);
|
|
// The three military styles used to carry three copies of these numbers.
|
|
// They now share ONE frozen object, which Cockpit's force-on reuses too, so
|
|
// the pin moved from "three identical literals" to "one preset, referenced
|
|
// three times" — same guarantee, and the copies can no longer drift.
|
|
assert.match(
|
|
uiSource,
|
|
/const MILITARY_DETECTION_PRESET = Object\.freeze\(\{\s*mode: 'dense',\s*densityPct: 75,?\s*\}\);/,
|
|
'the tactical detection default is still Dense @ 75%',
|
|
);
|
|
assert.equal(
|
|
uiSource.match(/detection: MILITARY_DETECTION_PRESET,/g)?.length,
|
|
3,
|
|
'CRT, NVG, and FLIR retain their Dense auto-enable defaults',
|
|
);
|
|
assert.match(uiSource, /preset\.detection && !this\._detectionUserOverridden/);
|
|
});
|
|
|
|
// ── Developer telemetry gate (2026-08-20 QA hunt) ───────────────────────────
|
|
// The orange mode banner ("DENSE VIS:15 SRC:1036 DENS:100% ELASTIC 0.4ms")
|
|
// painted for every user: CRT/NVG/FLIR auto-enable detection, so engine
|
|
// telemetry was the first thing a visitor saw, overlapping the cockpit callsign
|
|
// block. It is kept as a debug affordance, but must default OFF.
|
|
test('detectionDebugRequested parses the query-string gate and nothing else', () => {
|
|
assert.equal(detectionDebugRequested('?detectDebug=1'), true);
|
|
assert.equal(detectionDebugRequested('?foo=bar&detectDebug=1'), true);
|
|
assert.equal(detectionDebugRequested(''), false);
|
|
assert.equal(detectionDebugRequested('?detectDebug=0'), false);
|
|
assert.equal(detectionDebugRequested('?detectDebug'), false);
|
|
assert.equal(detectionDebugRequested('?detectdebug=1'), false, 'the flag is case-sensitive');
|
|
assert.equal(detectionDebugRequested(undefined), false);
|
|
assert.equal(detectionDebugRequested(null), false);
|
|
});
|
|
|
|
test('the mode banner is absent by default and present behind the flag', () => {
|
|
const bannerPaints = (env) => env.detectionCtx.calls.filter(([name, text]) => (
|
|
name === 'fillText' && /^(SPARSE|BALANCED|DENSE) VIS:/.test(String(text))
|
|
)).length;
|
|
|
|
const painted = (search) => {
|
|
const env = installEnvironment({ search });
|
|
try {
|
|
initWorldOverlay(env.viewer);
|
|
initDetection(env.viewer, [detectableLayer()], () => {});
|
|
setMode('DENSE');
|
|
env.advance(250);
|
|
env.postRender.raise();
|
|
// The overlay is alive either way — brackets still stroke.
|
|
assert.ok(
|
|
env.detectionCtx.calls.some(([name, path]) => name === 'stroke' && path instanceof MockPath2D),
|
|
'detection still paints its contacts regardless of the debug gate',
|
|
);
|
|
return bannerPaints(env);
|
|
} finally {
|
|
destroyDetection();
|
|
destroyWorldOverlay();
|
|
env.cleanup();
|
|
}
|
|
};
|
|
|
|
assert.equal(painted(''), 0, 'users must never see the engine telemetry banner');
|
|
assert.ok(painted('?detectDebug=1') > 0, 'the flag brings the telemetry back');
|
|
});
|
|
|
|
test('detection telemetry stays reachable programmatically with the banner hidden', () => {
|
|
// Hiding the banner must not remove the numbers — getDetectionDiagnostics()
|
|
// is the supported way to read them, and is what the QA harnesses use.
|
|
const env = installEnvironment();
|
|
try {
|
|
initWorldOverlay(env.viewer);
|
|
initDetection(env.viewer, [detectableLayer()], () => {});
|
|
setMode('DENSE');
|
|
env.advance(250);
|
|
env.postRender.raise();
|
|
const diagnostics = getDetectionDiagnostics();
|
|
assert.equal(diagnostics.profile, 'DENSE');
|
|
assert.equal(typeof diagnostics.observationCount, 'number');
|
|
assert.equal(typeof diagnostics.visibleCount, 'number');
|
|
assert.equal(typeof diagnostics.densityPct, 'number');
|
|
} finally {
|
|
destroyDetection();
|
|
destroyWorldOverlay();
|
|
env.cleanup();
|
|
}
|
|
});
|
|
|
|
test('civilian and military AIR brackets cover front, left, and right at Sparse density', () => {
|
|
for (const layerId of ['flights', 'military']) {
|
|
const env = installEnvironment({ width: 900, height: 600, dpr: 1 });
|
|
try {
|
|
const objects = [-0.82, 0, 0.82].map((x, index) => ({
|
|
position: new Cesium.Cartesian3(x, 0, 6_356_752),
|
|
sourceId: `${layerId}-${index}`,
|
|
id: `${layerId.toUpperCase()}-${index}`,
|
|
metric: 'FL120',
|
|
type: 'AIR',
|
|
}));
|
|
initWorldOverlay(env.viewer);
|
|
initDetection(env.viewer, [{ id: layerId, getDetectableObjects: () => objects }], () => {});
|
|
setDetectionTuning({ densityPct: 25 });
|
|
setMode('SPARSE');
|
|
settleFrame(env);
|
|
const diagnostics = getDetectionDiagnostics();
|
|
assert.deepEqual(
|
|
diagnostics.aircraftBracketSectors,
|
|
{ left: 1, front: 1, right: 1 },
|
|
`${layerId} side brackets must not be starved by the central keyhole`,
|
|
);
|
|
assert.equal(diagnostics.visibleCount, 3);
|
|
assert.equal(diagnostics.densityPct, 25);
|
|
assert.equal(diagnostics.profile, 'SPARSE');
|
|
assert.ok(diagnostics.selectedCount <= diagnostics.collectiveLabelBudget,
|
|
'Sparse keeps its documented budget instead of silently becoming Dense');
|
|
} finally {
|
|
destroyDetection();
|
|
destroyWorldOverlay();
|
|
env.cleanup();
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Two air contacts at the same screen radius, one with the planet behind it and
|
|
* one with sky. The mock camera sits at (0, 0, 10 000 km) and projects through
|
|
* an identity view-projection, so a position's x/y ARE its NDC and z is free:
|
|
* the pole-surface contact looks straight down the axis into the planet, while
|
|
* its mirror sits 10 000 km FURTHER out, so the view ray through it escapes.
|
|
* Equal screen radius keeps the keyhole's radial fade identical for both, which
|
|
* makes the two painted plate alphas directly comparable.
|
|
*/
|
|
function backdropLayer() {
|
|
return {
|
|
id: 'flights',
|
|
getDetectableObjects() {
|
|
return [
|
|
{
|
|
position: new Cesium.Cartesian3(-0.4, 0.2, 6_356_752),
|
|
sourceId: 'ground-1', id: 'GROUNDED', metric: 'FL100', type: 'AIR',
|
|
},
|
|
{
|
|
position: new Cesium.Cartesian3(0.4, -0.2, 20_000_000),
|
|
sourceId: 'sky-1', id: 'SKYBACK', metric: 'FL400', type: 'AIR',
|
|
},
|
|
];
|
|
},
|
|
};
|
|
}
|
|
|
|
test('the backdrop feather reaches the canvas as a lighter plate against sky', () => {
|
|
// The discriminator and the painter are pinned in their own modules, but both
|
|
// pins still pass if detection stops carrying the factor between them. This
|
|
// drives the real collect → solve → stash → replay path and reads the alpha
|
|
// the shared canvas actually received for each plate.
|
|
const env = installEnvironment();
|
|
try {
|
|
initWorldOverlay(env.viewer);
|
|
initDetection(env.viewer, [backdropLayer()], () => {});
|
|
setDetectionStyle('normal');
|
|
setMode('DENSE');
|
|
settleFrame(env);
|
|
env.ctx.calls.length = 0;
|
|
settleFrame(env);
|
|
|
|
const painted = (text) => env.ctx.calls.some(([name, value]) => name === 'fillText' && value === text);
|
|
assert.ok(painted('GROUNDED') && painted('SKYBACK'), 'both backdrops reached the frame under test');
|
|
|
|
const plate = DETECTION_THEME_MAP._default.calloutPlate;
|
|
const plateAlphas = env.ctx.calls
|
|
.filter(([name, , fillStyle]) => name === 'fill' && fillStyle === plate)
|
|
.map(([, , , globalAlpha]) => globalAlpha);
|
|
assert.equal(plateAlphas.length, 2, 'exactly one plate per contact');
|
|
|
|
const heaviest = Math.max(...plateAlphas);
|
|
const lightest = Math.min(...plateAlphas);
|
|
assert.ok(heaviest > 0, 'the grounded contact still gets a plate');
|
|
assert.ok(lightest > 0, 'the sky contact keeps a whisper rather than vanishing');
|
|
// Reverting the feature makes both plates equal, which fails here first.
|
|
assert.ok(
|
|
lightest < heaviest * 0.5,
|
|
`sky plate ${lightest} must be markedly lighter than ground plate ${heaviest}`,
|
|
);
|
|
} finally {
|
|
env.cleanup();
|
|
}
|
|
});
|
|
|
|
test('a transit vehicle is a contact: box, route, mode and operator on the shared canvas', () => {
|
|
// The owner's finding was that buses were "green dots moving around
|
|
// clumsily" — the layer rendered them and the detection overlay ignored
|
|
// them, because transit published no detectable objects at all. This drives
|
|
// the real host with the shape the layer now returns.
|
|
const env = installEnvironment();
|
|
const objects = [
|
|
{
|
|
position: new Cesium.Cartesian3(0, 0, 6_356_752),
|
|
sourceId: 'mbta:y1276',
|
|
tier: 'transit_bus',
|
|
id: '111',
|
|
type: 'VEH',
|
|
klass: 'MBTA',
|
|
metric: 'BUS 34 KM/H',
|
|
},
|
|
{
|
|
position: new Cesium.Cartesian3(0, 900, 6_356_752),
|
|
sourceId: 'mbta:R-548BBB14',
|
|
id: 'RED',
|
|
type: 'VEH',
|
|
klass: 'MBTA',
|
|
metric: 'METRO STOPPED',
|
|
skipLabel: true,
|
|
},
|
|
];
|
|
try {
|
|
initWorldOverlay(env.viewer);
|
|
initDetection(
|
|
env.viewer,
|
|
[{ id: 'transit', getDetectableObjects: () => objects }],
|
|
() => {},
|
|
);
|
|
setMode('DENSE');
|
|
env.advance(250);
|
|
env.postRender.raise();
|
|
env.advance(250);
|
|
env.postRender.raise();
|
|
const painted = env.ctx.calls
|
|
.filter(([name]) => name === 'fillText')
|
|
.map(([, text]) => text);
|
|
assert.ok(
|
|
painted.includes('111'),
|
|
`the route is the bright line; painted ${JSON.stringify(painted)}`,
|
|
);
|
|
// The host paints a single-line track label — bright id, one dim micro
|
|
// field — so the mode and what the vehicle is doing share that field.
|
|
assert.ok(
|
|
painted.some((text) => String(text).includes('BUS')),
|
|
`the mode rides along on the micro line; painted ${JSON.stringify(painted)}`,
|
|
);
|
|
assert.ok(
|
|
painted.some((text) => String(text).includes('34 KM/H')),
|
|
'and so does what the vehicle is doing',
|
|
);
|
|
assert.ok(env.ctx.calls.some(([name, , color, width]) => name === 'stroke' && color === '#5EF08A' && width === 1.25));
|
|
assert.ok(env.ctx.calls.some(([name, , color, width]) => name === 'stroke' && color === '#05080C' && width === 3.25));
|
|
assert.ok(!env.detectionCtx.calls.some(([name, , color]) => name === 'stroke' && color === '#5EF08A'));
|
|
// The operator travels on the object for the two-tier card form, which is
|
|
// where `klass` is read; the host's track label does not paint it.
|
|
assert.equal(composeLabel(objects[0]).secondary, 'MBTA · BUS 34 KM/H');
|
|
// The selected vehicle carries its own fuller card, so detection must not
|
|
// label it a second time.
|
|
assert.equal(
|
|
painted.includes('RED'),
|
|
false,
|
|
'skipLabel keeps the selected vehicle from being labelled twice',
|
|
);
|
|
} finally {
|
|
env.cleanup();
|
|
}
|
|
});
|
|
|
|
test('transit contacts take the vehicle tier, not the civil-air one', () => {
|
|
assert.equal(resolveTier({ type: 'VEH' }), 'vehicle');
|
|
assert.equal(resolveTier({ type: 'AIR' }), 'civil');
|
|
assert.equal(resolveTier({ type: 'SEA' }), 'sea');
|
|
});
|