mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
Revert hiding vessel detection callouts under ambient cards
The vessels layer hid the detection callout for every vessel whose card it published to the world overlay. The overlay paints only part of the published cards (shared budget, collisions, UI chrome), so vessels whose card was dropped lost all their text: with 60 vessels and Detection on, 21 showed only an arrow. Back to main's behaviour: the callout always draws. Hiding it only under a painted card is HAL-628. The translated speed unit in the callout stays. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
3adce33cc6
commit
a2463ca2d2
7 files changed
+433
-250
No files matched your search
@@ -1450,9 +1450,6 @@ function _drawOverlay(frame) {
|
||||
if (bracketAlpha > 0) protectedVisibleCount++;
|
||||
continue;
|
||||
}
|
||||
// The source shows its own card for this object (e.g. an ambient vessel
|
||||
// card): keep the bracket, draw no callout over the card.
|
||||
if (obj.hideLabel) continue;
|
||||
|
||||
const primary = String(obj.id || '');
|
||||
const micro = String(obj.metric || '');
|
||||
|
||||
+431
-200
@@ -27,17 +27,25 @@ 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);
|
||||
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(); }
|
||||
constructor() {
|
||||
this.listeners = new Set();
|
||||
}
|
||||
|
||||
addEventListener(listener) {
|
||||
this.listeners.add(listener);
|
||||
@@ -69,62 +77,127 @@ function mockContext(target, trace) {
|
||||
return {
|
||||
calls,
|
||||
font: '',
|
||||
get filter() { return filter; },
|
||||
set filter(value) { filter = value; record('filter', value); },
|
||||
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); },
|
||||
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); },
|
||||
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'); },
|
||||
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); },
|
||||
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 = '' } = {}) {
|
||||
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 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;
|
||||
} },
|
||||
value: {
|
||||
now: () => {
|
||||
const value = currentTime;
|
||||
currentTime += performanceStep;
|
||||
return value;
|
||||
},
|
||||
},
|
||||
});
|
||||
Date.now = () => currentTime;
|
||||
globalThis.Path2D = MockPath2D;
|
||||
@@ -167,7 +240,9 @@ function installEnvironment({ width = 800, height = 600, dpr = 2, search = '' }
|
||||
register(this);
|
||||
}
|
||||
|
||||
getBoundingClientRect() { return this._rect; }
|
||||
getBoundingClientRect() {
|
||||
return this._rect;
|
||||
}
|
||||
getContext() {
|
||||
if (this.tagName !== 'CANVAS') return null;
|
||||
return this.id === 'world-overlay-detection-surface' ? detectionCtx : ctx;
|
||||
@@ -180,13 +255,17 @@ function installEnvironment({ width = 800, height = 600, dpr = 2, search = '' }
|
||||
|
||||
remove() {
|
||||
if (this.parentElement) {
|
||||
this.parentElement.children = this.parentElement.children.filter((child) => child !== this);
|
||||
this.parentElement.children = this.parentElement.children.filter(
|
||||
(child) => child !== this,
|
||||
);
|
||||
}
|
||||
unregister(this);
|
||||
this.parentElement = null;
|
||||
}
|
||||
|
||||
get nextSibling() { return null; }
|
||||
get nextSibling() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function register(element) {
|
||||
@@ -200,14 +279,26 @@ function installEnvironment({ width = 800, height = 600, dpr = 2, search = '' }
|
||||
}
|
||||
|
||||
const body = new MockElement('body');
|
||||
body.classList = { contains() { return false; } };
|
||||
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; },
|
||||
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;
|
||||
const element = selector.startsWith('#')
|
||||
? byId.get(selector.slice(1))
|
||||
: null;
|
||||
return element ? [element] : [];
|
||||
},
|
||||
};
|
||||
@@ -216,10 +307,18 @@ function installEnvironment({ width = 800, height = 600, dpr = 2, search = '' }
|
||||
location: { search },
|
||||
addEventListener() {},
|
||||
removeEventListener() {},
|
||||
getComputedStyle() { return { display: 'block', visibility: 'visible', opacity: '1' }; },
|
||||
getComputedStyle() {
|
||||
return { display: 'block', visibility: 'visible', opacity: '1' };
|
||||
},
|
||||
};
|
||||
globalThis.ResizeObserver = class {
|
||||
observe() {}
|
||||
disconnect() {}
|
||||
};
|
||||
globalThis.MutationObserver = class {
|
||||
observe() {}
|
||||
disconnect() {}
|
||||
};
|
||||
globalThis.ResizeObserver = class { observe() {} disconnect() {} };
|
||||
globalThis.MutationObserver = class { observe() {} disconnect() {} };
|
||||
globalThis.document = document;
|
||||
globalThis.window = window;
|
||||
|
||||
@@ -241,7 +340,9 @@ function installEnvironment({ width = 800, height = 600, dpr = 2, search = '' }
|
||||
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) },
|
||||
frustum: {
|
||||
projectionMatrix: Cesium.Matrix4.clone(Cesium.Matrix4.IDENTITY),
|
||||
},
|
||||
moveEnd: new MockEvent(),
|
||||
},
|
||||
scene: { postRender, requestRender() {} },
|
||||
@@ -254,8 +355,12 @@ function installEnvironment({ width = 800, height = 600, dpr = 2, search = '' }
|
||||
ctx,
|
||||
detectionCtx,
|
||||
paintTrace,
|
||||
advance(ms) { currentTime += ms; },
|
||||
setPerformanceStep(ms) { performanceStep = ms; },
|
||||
advance(ms) {
|
||||
currentTime += ms;
|
||||
},
|
||||
setPerformanceStep(ms) {
|
||||
performanceStep = ms;
|
||||
},
|
||||
cleanup() {
|
||||
destroyDetection();
|
||||
destroyWorldOverlay();
|
||||
@@ -266,7 +371,8 @@ function installEnvironment({ width = 800, height = 600, dpr = 2, search = '' }
|
||||
delete globalThis.window;
|
||||
delete globalThis.ResizeObserver;
|
||||
delete globalThis.MutationObserver;
|
||||
if (originalPerformance) Object.defineProperty(globalThis, 'performance', originalPerformance);
|
||||
if (originalPerformance)
|
||||
Object.defineProperty(globalThis, 'performance', originalPerformance);
|
||||
else delete globalThis.performance;
|
||||
},
|
||||
};
|
||||
@@ -295,14 +401,16 @@ 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',
|
||||
}];
|
||||
return [
|
||||
{
|
||||
position: new Cesium.Cartesian3(0, 0, 6_356_752),
|
||||
sourceId: 'military-pin',
|
||||
id: 'MIL-PIN',
|
||||
metric: 'FL450',
|
||||
type: 'AIR',
|
||||
tier: 'military',
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -358,7 +466,9 @@ test('detection lifecycle re-hosts unchanged painters behind the sole host liste
|
||||
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 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);
|
||||
@@ -381,19 +491,36 @@ test('detection lifecycle re-hosts unchanged painters behind the sole host liste
|
||||
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');
|
||||
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.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');
|
||||
@@ -409,8 +536,16 @@ test('detection lifecycle re-hosts unchanged painters behind the sole host liste
|
||||
// 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'));
|
||||
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);
|
||||
@@ -420,7 +555,10 @@ test('detection lifecycle re-hosts unchanged painters behind the sole host liste
|
||||
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');
|
||||
assert.ok(
|
||||
env.detectionCtx.calls.some(([name]) => name === 'arc'),
|
||||
'sparse focus ring remains available',
|
||||
);
|
||||
|
||||
setMode('BALANCED');
|
||||
env.postRender.raise();
|
||||
@@ -429,9 +567,12 @@ test('detection lifecycle re-hosts unchanged painters behind the sole host liste
|
||||
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 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);
|
||||
@@ -448,9 +589,12 @@ test('detection lifecycle re-hosts unchanged painters behind the sole host liste
|
||||
// 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))',
|
||||
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');
|
||||
@@ -476,17 +620,28 @@ test('detection lifecycle re-hosts unchanged painters behind the sole host liste
|
||||
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'));
|
||||
assert.ok(
|
||||
modes.includes('SPARSE') &&
|
||||
modes.includes('BALANCED') &&
|
||||
modes.includes('DENSE'),
|
||||
);
|
||||
|
||||
destroyDetection();
|
||||
assert.equal(env.postRender.listeners.size, 1, 'detection never owns the listener');
|
||||
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);
|
||||
assert.equal(
|
||||
env.document.getElementById('world-overlay-detection-surface'),
|
||||
null,
|
||||
);
|
||||
} finally {
|
||||
env.cleanup();
|
||||
}
|
||||
@@ -499,16 +654,22 @@ test('callouts stop painting the moment the last detectable object goes away', (
|
||||
// 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',
|
||||
}];
|
||||
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 }], () => {});
|
||||
initDetection(
|
||||
env.viewer,
|
||||
[{ id: 'flights', getDetectableObjects: () => objects }],
|
||||
() => {},
|
||||
);
|
||||
setMode('DENSE');
|
||||
env.advance(250);
|
||||
env.postRender.raise();
|
||||
@@ -517,7 +678,9 @@ test('callouts stop painting the moment the last detectable object goes away', (
|
||||
env.advance(250);
|
||||
env.postRender.raise();
|
||||
assert.ok(
|
||||
env.ctx.calls.some(([name, text]) => name === 'fillText' && text === 'LASTONE'),
|
||||
env.ctx.calls.some(
|
||||
([name, text]) => name === 'fillText' && text === 'LASTONE',
|
||||
),
|
||||
'the callout paints on the shared canvas while the contact exists',
|
||||
);
|
||||
|
||||
@@ -528,7 +691,9 @@ test('callouts stop painting the moment the last detectable object goes away', (
|
||||
env.advance(250);
|
||||
env.postRender.raise();
|
||||
assert.ok(
|
||||
!env.ctx.calls.some(([name, text]) => name === 'fillText' && text === 'LASTONE'),
|
||||
!env.ctx.calls.some(
|
||||
([name, text]) => name === 'fillText' && text === 'LASTONE',
|
||||
),
|
||||
'an empty field must drop the replay buffer, not strand the last callsign',
|
||||
);
|
||||
} finally {
|
||||
@@ -595,12 +760,24 @@ test('space-tier callouts take the heavier plate while air contacts keep the lig
|
||||
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 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');
|
||||
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();
|
||||
}
|
||||
@@ -617,11 +794,16 @@ test('surveillance military tier reaches the dedicated render target as shipped
|
||||
env.postRender.raise();
|
||||
|
||||
assert.ok(
|
||||
env.detectionCtx.calls.some(([name, value]) => name === 'strokeStyle' && value === '#ff5a47'),
|
||||
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));
|
||||
assert.ok(
|
||||
env.detectionCtx.calls.some(
|
||||
([name, path]) => name === 'stroke' && path instanceof MockPath2D,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
env.cleanup();
|
||||
}
|
||||
@@ -633,44 +815,71 @@ test('pathological detection paint holds alternate frames without freezing share
|
||||
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',
|
||||
}]);
|
||||
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;
|
||||
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(
|
||||
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(
|
||||
env.detectionCtx.calls.filter(([name]) => name === 'clearRect').length,
|
||||
detectionClears + 1,
|
||||
'the next even frame repaints detection',
|
||||
);
|
||||
assert.equal(getDetectionDiagnostics().throttleSkipCount, 1);
|
||||
} finally {
|
||||
env.cleanup();
|
||||
@@ -678,9 +887,14 @@ test('pathological detection paint holds alternate frames without freezing share
|
||||
});
|
||||
|
||||
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');
|
||||
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['"]/);
|
||||
@@ -690,11 +904,20 @@ test('detection cannot resurrect a private canvas, listener, matrix, resize, cle
|
||||
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,
|
||||
/_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,
|
||||
/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.
|
||||
@@ -711,7 +934,10 @@ test('detection cannot resurrect a private canvas, listener, matrix, resize, cle
|
||||
3,
|
||||
'CRT, NVG, and FLIR retain their Dense auto-enable defaults',
|
||||
);
|
||||
assert.match(uiSource, /preset\.detection && !this\._detectionUserOverridden/);
|
||||
assert.match(
|
||||
uiSource,
|
||||
/preset\.detection && !this\._detectionUserOverridden/,
|
||||
);
|
||||
});
|
||||
|
||||
// ── Developer telemetry gate (2026-08-20 QA hunt) ───────────────────────────
|
||||
@@ -725,15 +951,22 @@ test('detectionDebugRequested parses the query-string gate and nothing else', ()
|
||||
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('?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 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 });
|
||||
@@ -745,7 +978,9 @@ test('the mode banner is absent by default and present behind the flag', () => {
|
||||
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),
|
||||
env.detectionCtx.calls.some(
|
||||
([name, path]) => name === 'stroke' && path instanceof MockPath2D,
|
||||
),
|
||||
'detection still paints its contacts regardless of the debug gate',
|
||||
);
|
||||
return bannerPaints(env);
|
||||
@@ -756,8 +991,15 @@ test('the mode banner is absent by default and present behind the flag', () => {
|
||||
}
|
||||
};
|
||||
|
||||
assert.equal(painted(''), 0, 'users must never see the engine telemetry banner');
|
||||
assert.ok(painted('?detectDebug=1') > 0, 'the flag brings the telemetry back');
|
||||
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', () => {
|
||||
@@ -794,7 +1036,11 @@ test('civilian and military AIR brackets cover front, left, and right at Sparse
|
||||
type: 'AIR',
|
||||
}));
|
||||
initWorldOverlay(env.viewer);
|
||||
initDetection(env.viewer, [{ id: layerId, getDetectableObjects: () => objects }], () => {});
|
||||
initDetection(
|
||||
env.viewer,
|
||||
[{ id: layerId, getDetectableObjects: () => objects }],
|
||||
() => {},
|
||||
);
|
||||
setDetectionTuning({ densityPct: 25 });
|
||||
setMode('SPARSE');
|
||||
settleFrame(env);
|
||||
@@ -807,8 +1053,10 @@ test('civilian and military AIR brackets cover front, left, and right at Sparse
|
||||
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');
|
||||
assert.ok(
|
||||
diagnostics.selectedCount <= diagnostics.collectiveLabelBudget,
|
||||
'Sparse keeps its documented budget instead of silently becoming Dense',
|
||||
);
|
||||
} finally {
|
||||
destroyDetection();
|
||||
destroyWorldOverlay();
|
||||
@@ -833,11 +1081,17 @@ function backdropLayer() {
|
||||
return [
|
||||
{
|
||||
position: new Cesium.Cartesian3(-0.4, 0.2, 6_356_752),
|
||||
sourceId: 'ground-1', id: 'GROUNDED', metric: 'FL100', type: 'AIR',
|
||||
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',
|
||||
sourceId: 'sky-1',
|
||||
id: 'SKYBACK',
|
||||
metric: 'FL400',
|
||||
type: 'AIR',
|
||||
},
|
||||
];
|
||||
},
|
||||
@@ -859,8 +1113,14 @@ test('the backdrop feather reaches the canvas as a lighter plate against sky', (
|
||||
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 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
|
||||
@@ -871,7 +1131,10 @@ test('the backdrop feather reaches the canvas as a lighter plate against sky', (
|
||||
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');
|
||||
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,
|
||||
@@ -937,9 +1200,23 @@ test('a transit vehicle is a contact: box, route, mode and operator on the share
|
||||
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'));
|
||||
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');
|
||||
@@ -955,52 +1232,6 @@ test('a transit vehicle is a contact: box, route, mode and operator on the share
|
||||
}
|
||||
});
|
||||
|
||||
test('hideLabel keeps the bracket but paints no callout over the source card', () => {
|
||||
// C6: an ambient vessel card already names the vessel with its speed; the
|
||||
// callout drawn beside it covered that card's speed and course.
|
||||
const env = installEnvironment();
|
||||
const objects = [
|
||||
{
|
||||
position: new Cesium.Cartesian3(0, 0, 6_356_752),
|
||||
sourceId: '200',
|
||||
id: 'GOLDEN BEAR',
|
||||
type: 'VEH',
|
||||
metric: '10 kn',
|
||||
hideLabel: true,
|
||||
},
|
||||
{
|
||||
position: new Cesium.Cartesian3(0.5, 0.5, 6_356_752),
|
||||
sourceId: '300',
|
||||
id: 'BAY PILOT',
|
||||
type: 'VEH',
|
||||
metric: '4 kn',
|
||||
},
|
||||
];
|
||||
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]) => String(text));
|
||||
assert.ok(painted.includes('BAY PILOT'), `painted ${JSON.stringify(painted)}`);
|
||||
assert.ok(
|
||||
!painted.some((text) => text.includes('GOLDEN BEAR')),
|
||||
`no callout under the card; painted ${JSON.stringify(painted)}`,
|
||||
);
|
||||
} 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');
|
||||
|
||||
Reference in new issue
Block a user