mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04: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++;
|
if (bracketAlpha > 0) protectedVisibleCount++;
|
||||||
continue;
|
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 primary = String(obj.id || '');
|
||||||
const micro = String(obj.metric || '');
|
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';
|
import { composeLabel, resolveTier } from './detectionDraw.js';
|
||||||
|
|
||||||
test('detection diagnostics count rendered fading rows instead of absent selected identities', () => {
|
test('detection diagnostics count rendered fading rows instead of absent selected identities', () => {
|
||||||
assert.equal(countFadingRenderEntries([
|
assert.equal(
|
||||||
{ selected: true },
|
countFadingRenderEntries([
|
||||||
{ selected: false },
|
{ selected: true },
|
||||||
{ selected: true },
|
{ selected: false },
|
||||||
{ selected: false },
|
{ selected: true },
|
||||||
]), 2);
|
{ selected: false },
|
||||||
assert.equal(countFadingRenderEntries([{ selected: true }, { selected: true }]), 0);
|
]),
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
countFadingRenderEntries([{ selected: true }, { selected: true }]),
|
||||||
|
0,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
class MockEvent {
|
class MockEvent {
|
||||||
constructor() { this.listeners = new Set(); }
|
constructor() {
|
||||||
|
this.listeners = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
addEventListener(listener) {
|
addEventListener(listener) {
|
||||||
this.listeners.add(listener);
|
this.listeners.add(listener);
|
||||||
@@ -69,62 +77,127 @@ function mockContext(target, trace) {
|
|||||||
return {
|
return {
|
||||||
calls,
|
calls,
|
||||||
font: '',
|
font: '',
|
||||||
get filter() { return filter; },
|
get filter() {
|
||||||
set filter(value) { filter = value; record('filter', value); },
|
return filter;
|
||||||
|
},
|
||||||
|
set filter(value) {
|
||||||
|
filter = value;
|
||||||
|
record('filter', value);
|
||||||
|
},
|
||||||
fillStyle: '',
|
fillStyle: '',
|
||||||
get strokeStyle() { return strokeStyle; },
|
get strokeStyle() {
|
||||||
set strokeStyle(value) { strokeStyle = value; record('strokeStyle', value); },
|
return strokeStyle;
|
||||||
|
},
|
||||||
|
set strokeStyle(value) {
|
||||||
|
strokeStyle = value;
|
||||||
|
record('strokeStyle', value);
|
||||||
|
},
|
||||||
globalAlpha: 1,
|
globalAlpha: 1,
|
||||||
get globalCompositeOperation() { return globalCompositeOperation; },
|
get globalCompositeOperation() {
|
||||||
set globalCompositeOperation(value) { globalCompositeOperation = value; record('globalCompositeOperation', value); },
|
return globalCompositeOperation;
|
||||||
|
},
|
||||||
|
set globalCompositeOperation(value) {
|
||||||
|
globalCompositeOperation = value;
|
||||||
|
record('globalCompositeOperation', value);
|
||||||
|
},
|
||||||
lineWidth: 1,
|
lineWidth: 1,
|
||||||
measureText(text) { return { width: String(text).length * 6 }; },
|
measureText(text) {
|
||||||
setTransform(...args) { record('setTransform', ...args); },
|
return { width: String(text).length * 6 };
|
||||||
clearRect(...args) { record('clearRect', ...args); },
|
},
|
||||||
save() { record('save'); },
|
setTransform(...args) {
|
||||||
restore() { record('restore'); },
|
record('setTransform', ...args);
|
||||||
translate(...args) { record('translate', ...args); },
|
},
|
||||||
scale(...args) { record('scale', ...args); },
|
clearRect(...args) {
|
||||||
beginPath() { record('beginPath'); },
|
record('clearRect', ...args);
|
||||||
rect(...args) { record('rect', ...args); },
|
},
|
||||||
clip(...args) { record('clip', ...args); },
|
save() {
|
||||||
moveTo(...args) { record('moveTo', ...args); },
|
record('save');
|
||||||
lineTo(...args) { record('lineTo', ...args); },
|
},
|
||||||
arc(...args) { record('arc', ...args); },
|
restore() {
|
||||||
arcTo(...args) { record('arcTo', ...args); },
|
record('restore');
|
||||||
closePath() { record('closePath'); },
|
},
|
||||||
|
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
|
// 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
|
// 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
|
// set at some point during the frame. globalAlpha rides along for the same
|
||||||
// reason: the backdrop feather is an alpha, not a colour.
|
// reason: the backdrop feather is an alpha, not a colour.
|
||||||
fill(path) { record('fill', path, this.fillStyle, this.globalAlpha); },
|
fill(path) {
|
||||||
stroke(path) { record('stroke', path, this.strokeStyle, this.lineWidth); },
|
record('fill', path, this.fillStyle, this.globalAlpha);
|
||||||
fillRect(...args) { record('fillRect', ...args); },
|
},
|
||||||
fillText(...args) { record('fillText', ...args); },
|
stroke(path) {
|
||||||
drawImage(...args) { record('drawImage', ...args); },
|
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
|
// `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
|
// telemetry gated behind ?detectDebug=1, so any test that uses the banner as its
|
||||||
// "did this frame repaint" probe must opt in explicitly.
|
// "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 byId = new Map();
|
||||||
const paintTrace = [];
|
const paintTrace = [];
|
||||||
const ctx = mockContext('shared', paintTrace);
|
const ctx = mockContext('shared', paintTrace);
|
||||||
const detectionCtx = mockContext('detection', paintTrace);
|
const detectionCtx = mockContext('detection', paintTrace);
|
||||||
const originalPerformance = Object.getOwnPropertyDescriptor(globalThis, 'performance');
|
const originalPerformance = Object.getOwnPropertyDescriptor(
|
||||||
|
globalThis,
|
||||||
|
'performance',
|
||||||
|
);
|
||||||
const originalDateNow = Date.now;
|
const originalDateNow = Date.now;
|
||||||
const originalPath2D = globalThis.Path2D;
|
const originalPath2D = globalThis.Path2D;
|
||||||
let currentTime = 1_000;
|
let currentTime = 1_000;
|
||||||
let performanceStep = 0;
|
let performanceStep = 0;
|
||||||
Object.defineProperty(globalThis, 'performance', {
|
Object.defineProperty(globalThis, 'performance', {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
value: { now: () => {
|
value: {
|
||||||
const value = currentTime;
|
now: () => {
|
||||||
currentTime += performanceStep;
|
const value = currentTime;
|
||||||
return value;
|
currentTime += performanceStep;
|
||||||
} },
|
return value;
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
Date.now = () => currentTime;
|
Date.now = () => currentTime;
|
||||||
globalThis.Path2D = MockPath2D;
|
globalThis.Path2D = MockPath2D;
|
||||||
@@ -167,7 +240,9 @@ function installEnvironment({ width = 800, height = 600, dpr = 2, search = '' }
|
|||||||
register(this);
|
register(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBoundingClientRect() { return this._rect; }
|
getBoundingClientRect() {
|
||||||
|
return this._rect;
|
||||||
|
}
|
||||||
getContext() {
|
getContext() {
|
||||||
if (this.tagName !== 'CANVAS') return null;
|
if (this.tagName !== 'CANVAS') return null;
|
||||||
return this.id === 'world-overlay-detection-surface' ? detectionCtx : ctx;
|
return this.id === 'world-overlay-detection-surface' ? detectionCtx : ctx;
|
||||||
@@ -180,13 +255,17 @@ function installEnvironment({ width = 800, height = 600, dpr = 2, search = '' }
|
|||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
if (this.parentElement) {
|
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);
|
unregister(this);
|
||||||
this.parentElement = null;
|
this.parentElement = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get nextSibling() { return null; }
|
get nextSibling() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function register(element) {
|
function register(element) {
|
||||||
@@ -200,14 +279,26 @@ function installEnvironment({ width = 800, height = 600, dpr = 2, search = '' }
|
|||||||
}
|
}
|
||||||
|
|
||||||
const body = new MockElement('body');
|
const body = new MockElement('body');
|
||||||
body.classList = { contains() { return false; } };
|
body.classList = {
|
||||||
|
contains() {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
};
|
||||||
const document = {
|
const document = {
|
||||||
body,
|
body,
|
||||||
createElement(tagName) { return new MockElement(tagName); },
|
createElement(tagName) {
|
||||||
getElementById(id) { return byId.get(id) || null; },
|
return new MockElement(tagName);
|
||||||
querySelector(selector) { return byId.get(selector.slice(1)) || null; },
|
},
|
||||||
|
getElementById(id) {
|
||||||
|
return byId.get(id) || null;
|
||||||
|
},
|
||||||
|
querySelector(selector) {
|
||||||
|
return byId.get(selector.slice(1)) || null;
|
||||||
|
},
|
||||||
querySelectorAll(selector) {
|
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] : [];
|
return element ? [element] : [];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -216,10 +307,18 @@ function installEnvironment({ width = 800, height = 600, dpr = 2, search = '' }
|
|||||||
location: { search },
|
location: { search },
|
||||||
addEventListener() {},
|
addEventListener() {},
|
||||||
removeEventListener() {},
|
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.document = document;
|
||||||
globalThis.window = window;
|
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),
|
positionWC: new Cesium.Cartesian3(0, 0, 10_000_000),
|
||||||
positionCartographic: { height: 1_000_000 },
|
positionCartographic: { height: 1_000_000 },
|
||||||
viewMatrix: Cesium.Matrix4.clone(Cesium.Matrix4.IDENTITY),
|
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(),
|
moveEnd: new MockEvent(),
|
||||||
},
|
},
|
||||||
scene: { postRender, requestRender() {} },
|
scene: { postRender, requestRender() {} },
|
||||||
@@ -254,8 +355,12 @@ function installEnvironment({ width = 800, height = 600, dpr = 2, search = '' }
|
|||||||
ctx,
|
ctx,
|
||||||
detectionCtx,
|
detectionCtx,
|
||||||
paintTrace,
|
paintTrace,
|
||||||
advance(ms) { currentTime += ms; },
|
advance(ms) {
|
||||||
setPerformanceStep(ms) { performanceStep = ms; },
|
currentTime += ms;
|
||||||
|
},
|
||||||
|
setPerformanceStep(ms) {
|
||||||
|
performanceStep = ms;
|
||||||
|
},
|
||||||
cleanup() {
|
cleanup() {
|
||||||
destroyDetection();
|
destroyDetection();
|
||||||
destroyWorldOverlay();
|
destroyWorldOverlay();
|
||||||
@@ -266,7 +371,8 @@ function installEnvironment({ width = 800, height = 600, dpr = 2, search = '' }
|
|||||||
delete globalThis.window;
|
delete globalThis.window;
|
||||||
delete globalThis.ResizeObserver;
|
delete globalThis.ResizeObserver;
|
||||||
delete globalThis.MutationObserver;
|
delete globalThis.MutationObserver;
|
||||||
if (originalPerformance) Object.defineProperty(globalThis, 'performance', originalPerformance);
|
if (originalPerformance)
|
||||||
|
Object.defineProperty(globalThis, 'performance', originalPerformance);
|
||||||
else delete globalThis.performance;
|
else delete globalThis.performance;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -295,14 +401,16 @@ function militaryLayer() {
|
|||||||
return {
|
return {
|
||||||
id: 'military',
|
id: 'military',
|
||||||
getDetectableObjects() {
|
getDetectableObjects() {
|
||||||
return [{
|
return [
|
||||||
position: new Cesium.Cartesian3(0, 0, 6_356_752),
|
{
|
||||||
sourceId: 'military-pin',
|
position: new Cesium.Cartesian3(0, 0, 6_356_752),
|
||||||
id: 'MIL-PIN',
|
sourceId: 'military-pin',
|
||||||
metric: 'FL450',
|
id: 'MIL-PIN',
|
||||||
type: 'AIR',
|
metric: 'FL450',
|
||||||
tier: 'military',
|
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.postRender.listeners.size, 1);
|
||||||
assert.equal(env.document.getElementById('detection-overlay'), null);
|
assert.equal(env.document.getElementById('detection-overlay'), null);
|
||||||
assert.ok(env.document.getElementById('world-overlay-canvas'));
|
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 root = env.document.getElementById('world-overlay-root');
|
||||||
const mainCanvas = env.document.getElementById('world-overlay-canvas');
|
const mainCanvas = env.document.getElementById('world-overlay-canvas');
|
||||||
assert.ok(surface);
|
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.equal(first.profile, 'DENSE');
|
||||||
assert.ok(Array.isArray(first.calloutRects));
|
assert.ok(Array.isArray(first.calloutRects));
|
||||||
if (first.calloutRects.length) {
|
if (first.calloutRects.length) {
|
||||||
const x=first.calloutRects[0].x;
|
const x = first.calloutRects[0].x;
|
||||||
first.calloutRects[0].x=NaN;
|
first.calloutRects[0].x = NaN;
|
||||||
assert.equal(getDetectionDiagnostics().calloutRects[0].x,x,'QA owns a copy, never a live plate');
|
assert.equal(
|
||||||
|
getDetectionDiagnostics().calloutRects[0].x,
|
||||||
|
x,
|
||||||
|
'QA owns a copy, never a live plate',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
assert.equal(first.observationCount, 2);
|
assert.equal(first.observationCount, 2);
|
||||||
assert.equal(first.didSolve, true);
|
assert.equal(first.didSolve, true);
|
||||||
assert.ok(first.solveRevision > 0);
|
assert.ok(first.solveRevision > 0);
|
||||||
assert.ok(first.collectiveLabelBudget > 0);
|
assert.ok(first.collectiveLabelBudget > 0);
|
||||||
assert.ok(first.bracketOpacityCounts.full + first.bracketOpacityCounts.partial > 0);
|
assert.ok(
|
||||||
assert.ok(env.detectionCtx.calls.some(([name, text]) => name === 'fillText'
|
first.bracketOpacityCounts.full + first.bracketOpacityCounts.partial > 0,
|
||||||
&& String(text).startsWith('DENSE VIS:')));
|
);
|
||||||
assert.ok(env.detectionCtx.calls.some(([name, path]) => name === 'stroke' && path instanceof MockPath2D));
|
assert.ok(
|
||||||
assert.ok(env.detectionCtx.calls.some(([name]) => name === 'fillRect'), 'scanlines and banner still paint');
|
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.globalCompositeOperation, 'source-over');
|
||||||
assert.equal(env.detectionCtx.filter, 'none');
|
assert.equal(env.detectionCtx.filter, 'none');
|
||||||
assert.equal(surface.style.display, 'block');
|
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
|
// 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
|
// 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.
|
// 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(
|
||||||
assert.ok(!env.detectionCtx.calls.some(([name, text]) => name === 'fillText' && text === 'TEST0'));
|
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.advance(1);
|
||||||
env.postRender.raise();
|
env.postRender.raise();
|
||||||
assert.ok(getDetectionDiagnostics().solveRevision > first.solveRevision);
|
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(getMode(), 'SPARSE');
|
||||||
assert.equal(getDetectionDiagnostics().profile, 'SPARSE');
|
assert.equal(getDetectionDiagnostics().profile, 'SPARSE');
|
||||||
assert.equal(getDetectionDiagnostics().allocationStrategy, 'WEIGHTED');
|
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');
|
setMode('BALANCED');
|
||||||
env.postRender.raise();
|
env.postRender.raise();
|
||||||
@@ -429,9 +567,12 @@ test('detection lifecycle re-hosts unchanged painters behind the sole host liste
|
|||||||
env.postRender.raise();
|
env.postRender.raise();
|
||||||
assert.equal(getDetectionDiagnostics().profile, 'DENSE');
|
assert.equal(getDetectionDiagnostics().profile, 'DENSE');
|
||||||
|
|
||||||
const bannerCount = () => env.detectionCtx.calls.filter(([name, text]) => (
|
const bannerCount = () =>
|
||||||
name === 'fillText' && /^(SPARSE|BALANCED|DENSE) VIS:/.test(String(text))
|
env.detectionCtx.calls.filter(
|
||||||
)).length;
|
([name, text]) =>
|
||||||
|
name === 'fillText' &&
|
||||||
|
/^(SPARSE|BALANCED|DENSE) VIS:/.test(String(text)),
|
||||||
|
).length;
|
||||||
const beforeSuspend = bannerCount();
|
const beforeSuspend = bannerCount();
|
||||||
suspendDetection('intercity');
|
suspendDetection('intercity');
|
||||||
assert.equal(isDetectionSuspended(), true);
|
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
|
// These are the exact engine calls made by military style presets after
|
||||||
// their production UI gate chooses CRT/NVG/FLIR defaults.
|
// their production UI gate chooses CRT/NVG/FLIR defaults.
|
||||||
const expectedThemes = {
|
const expectedThemes = {
|
||||||
retro: 'contrast(1.08) saturate(1.04) drop-shadow(0 0 3px rgba(255, 176, 56, 0.45))',
|
retro:
|
||||||
surveillance: 'contrast(1.12) saturate(1.12) drop-shadow(0 0 3px rgba(120, 255, 120, 0.42))',
|
'contrast(1.08) saturate(1.04) drop-shadow(0 0 3px rgba(255, 176, 56, 0.45))',
|
||||||
thermal: 'contrast(1.1) saturate(1.08) drop-shadow(0 0 3px rgba(255, 224, 170, 0.42))',
|
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']) {
|
for (const style of ['retro', 'surveillance', 'thermal']) {
|
||||||
setMode('OFF');
|
setMode('OFF');
|
||||||
@@ -476,17 +620,28 @@ test('detection lifecycle re-hosts unchanged painters behind the sole host liste
|
|||||||
assert.equal(getMode(), 'OFF');
|
assert.equal(getMode(), 'OFF');
|
||||||
assert.equal(surface.style.display, 'none');
|
assert.equal(surface.style.display, 'none');
|
||||||
assert.equal(env.postRender.listeners.size, 1);
|
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();
|
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');
|
setMode('DENSE');
|
||||||
env.postRender.raise();
|
env.postRender.raise();
|
||||||
assert.equal(env.document.getElementById('detection-overlay'), null);
|
assert.equal(env.document.getElementById('detection-overlay'), null);
|
||||||
destroyWorldOverlay();
|
destroyWorldOverlay();
|
||||||
assert.equal(env.postRender.listeners.size, 0);
|
assert.equal(env.postRender.listeners.size, 0);
|
||||||
assert.equal(env.document.getElementById('world-overlay-canvas'), null);
|
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 {
|
} finally {
|
||||||
env.cleanup();
|
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
|
// clear the replay buffer, or the final callsigns stay stranded on the
|
||||||
// shared canvas after the last data layer is switched off.
|
// shared canvas after the last data layer is switched off.
|
||||||
const env = installEnvironment();
|
const env = installEnvironment();
|
||||||
let objects = [{
|
let objects = [
|
||||||
position: new Cesium.Cartesian3(0, 0, 6_356_752),
|
{
|
||||||
sourceId: 'lonely-1',
|
position: new Cesium.Cartesian3(0, 0, 6_356_752),
|
||||||
id: 'LASTONE',
|
sourceId: 'lonely-1',
|
||||||
metric: 'FL120',
|
id: 'LASTONE',
|
||||||
type: 'AIR',
|
metric: 'FL120',
|
||||||
}];
|
type: 'AIR',
|
||||||
|
},
|
||||||
|
];
|
||||||
try {
|
try {
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.viewer);
|
||||||
initDetection(env.viewer, [{ id: 'flights', getDetectableObjects: () => objects }], () => {});
|
initDetection(
|
||||||
|
env.viewer,
|
||||||
|
[{ id: 'flights', getDetectableObjects: () => objects }],
|
||||||
|
() => {},
|
||||||
|
);
|
||||||
setMode('DENSE');
|
setMode('DENSE');
|
||||||
env.advance(250);
|
env.advance(250);
|
||||||
env.postRender.raise();
|
env.postRender.raise();
|
||||||
@@ -517,7 +678,9 @@ test('callouts stop painting the moment the last detectable object goes away', (
|
|||||||
env.advance(250);
|
env.advance(250);
|
||||||
env.postRender.raise();
|
env.postRender.raise();
|
||||||
assert.ok(
|
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',
|
'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.advance(250);
|
||||||
env.postRender.raise();
|
env.postRender.raise();
|
||||||
assert.ok(
|
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',
|
'an empty field must drop the replay buffer, not strand the last callsign',
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -595,12 +760,24 @@ test('space-tier callouts take the heavier plate while air contacts keep the lig
|
|||||||
theme.calloutPlateSpace,
|
theme.calloutPlateSpace,
|
||||||
'the fixture is only meaningful while the two tiers differ',
|
'the fixture is only meaningful while the two tiers differ',
|
||||||
);
|
);
|
||||||
const painted = (text) => env.ctx.calls.some(([name, value]) => name === 'fillText' && value === text);
|
const painted = (text) =>
|
||||||
assert.ok(painted('AIRONE') && painted('SATONE'), 'both tiers reached the frame under test');
|
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);
|
const fills = sharedFillStyles(env);
|
||||||
assert.ok(fills.includes(theme.calloutPlate), 'the air contact keeps the light plate');
|
assert.ok(
|
||||||
assert.ok(fills.includes(theme.calloutPlateSpace), 'the space contact takes the heavier plate');
|
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 {
|
} finally {
|
||||||
env.cleanup();
|
env.cleanup();
|
||||||
}
|
}
|
||||||
@@ -617,11 +794,16 @@ test('surveillance military tier reaches the dedicated render target as shipped
|
|||||||
env.postRender.raise();
|
env.postRender.raise();
|
||||||
|
|
||||||
assert.ok(
|
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',
|
'the production projection/tier/batched-bracket paint path emits surveillance military red',
|
||||||
);
|
);
|
||||||
assert.ok(env.detectionCtx.calls.some(([name, path]) => name === 'stroke'
|
assert.ok(
|
||||||
&& path instanceof MockPath2D));
|
env.detectionCtx.calls.some(
|
||||||
|
([name, path]) => name === 'stroke' && path instanceof MockPath2D,
|
||||||
|
),
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
env.cleanup();
|
env.cleanup();
|
||||||
}
|
}
|
||||||
@@ -633,44 +815,71 @@ test('pathological detection paint holds alternate frames without freezing share
|
|||||||
try {
|
try {
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.viewer);
|
||||||
initDetection(env.viewer, [detectableLayer()], () => {});
|
initDetection(env.viewer, [detectableLayer()], () => {});
|
||||||
setOverlayEntries('valve-card', [{
|
setOverlayEntries('valve-card', [
|
||||||
id: 'ambient-card',
|
{
|
||||||
position: new Cesium.Cartesian3(0, 0, 0),
|
id: 'ambient-card',
|
||||||
variant: 'label',
|
position: new Cesium.Cartesian3(0, 0, 0),
|
||||||
title: 'AMBIENT-CONTINUES',
|
variant: 'label',
|
||||||
selected: true,
|
title: 'AMBIENT-CONTINUES',
|
||||||
protected: true,
|
selected: true,
|
||||||
horizonCull: false,
|
protected: true,
|
||||||
edgeFade: 'none',
|
horizonCull: false,
|
||||||
}]);
|
edgeFade: 'none',
|
||||||
|
},
|
||||||
|
]);
|
||||||
setMode('DENSE');
|
setMode('DENSE');
|
||||||
env.advance(250);
|
env.advance(250);
|
||||||
env.setPerformanceStep(30);
|
env.setPerformanceStep(30);
|
||||||
|
|
||||||
env.postRender.raise();
|
env.postRender.raise();
|
||||||
env.postRender.raise();
|
env.postRender.raise();
|
||||||
const detectionClears = env.detectionCtx.calls.filter(([name]) => name === 'clearRect').length;
|
const detectionClears = env.detectionCtx.calls.filter(
|
||||||
const detectionBanners = env.detectionCtx.calls.filter(([name, text]) => name === 'fillText'
|
([name]) => name === 'clearRect',
|
||||||
&& String(text).startsWith('DENSE VIS:')).length;
|
).length;
|
||||||
const sharedClears = env.ctx.calls.filter(([name]) => name === 'clearRect').length;
|
const detectionBanners = env.detectionCtx.calls.filter(
|
||||||
const sharedCards = env.ctx.calls.filter(([name, text]) => name === 'fillText'
|
([name, text]) =>
|
||||||
&& text === 'AMBIENT-CONTINUES').length;
|
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();
|
env.postRender.raise();
|
||||||
assert.equal(env.detectionCtx.calls.filter(([name]) => name === 'clearRect').length, detectionClears,
|
assert.equal(
|
||||||
'the host preserves detection pixels on the held odd frame');
|
env.detectionCtx.calls.filter(([name]) => name === 'clearRect').length,
|
||||||
assert.equal(env.detectionCtx.calls.filter(([name, text]) => name === 'fillText'
|
detectionClears,
|
||||||
&& String(text).startsWith('DENSE VIS:')).length, detectionBanners);
|
'the host preserves detection pixels on the held odd frame',
|
||||||
assert.equal(env.ctx.calls.filter(([name]) => name === 'clearRect').length, sharedClears + 1,
|
);
|
||||||
'the shared surface still clears');
|
assert.equal(
|
||||||
assert.equal(env.ctx.calls.filter(([name, text]) => name === 'fillText'
|
env.detectionCtx.calls.filter(
|
||||||
&& text === 'AMBIENT-CONTINUES').length, sharedCards + 1,
|
([name, text]) =>
|
||||||
'the shared lane still repaints');
|
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);
|
assert.equal(getDetectionDiagnostics().throttleSkipCount, 1);
|
||||||
|
|
||||||
env.postRender.raise();
|
env.postRender.raise();
|
||||||
assert.equal(env.detectionCtx.calls.filter(([name]) => name === 'clearRect').length,
|
assert.equal(
|
||||||
detectionClears + 1, 'the next even frame repaints detection');
|
env.detectionCtx.calls.filter(([name]) => name === 'clearRect').length,
|
||||||
|
detectionClears + 1,
|
||||||
|
'the next even frame repaints detection',
|
||||||
|
);
|
||||||
assert.equal(getDetectionDiagnostics().throttleSkipCount, 1);
|
assert.equal(getDetectionDiagnostics().throttleSkipCount, 1);
|
||||||
} finally {
|
} finally {
|
||||||
env.cleanup();
|
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', () => {
|
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 source = readFileSync(
|
||||||
const uiSource = readShellSource()
|
new URL('./detection.js', import.meta.url),
|
||||||
+ '\n' + readFileSync(new URL('../ui/visualPresets.js', import.meta.url), 'utf8');
|
'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, /createElement\(\s*['"]canvas['"]\s*\)/);
|
||||||
assert.doesNotMatch(source, /postRender\.addEventListener/);
|
assert.doesNotMatch(source, /postRender\.addEventListener/);
|
||||||
assert.doesNotMatch(source, /['"]detection-overlay['"]/);
|
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, /CALLOUT_OCCLUSION_SELECTORS/);
|
||||||
assert.doesNotMatch(source, /_ctx\.(?:filter|globalCompositeOperation)\s*=/);
|
assert.doesNotMatch(source, /_ctx\.(?:filter|globalCompositeOperation)\s*=/);
|
||||||
assert.match(source, /_hostSurface\.style\.mixBlendMode = _theme\.blend;/);
|
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 LABEL_SOLVE_INTERVAL_MS = 125;/);
|
||||||
assert.match(source, /const _labelArbiter = new LabelArbiter\(\);/);
|
assert.match(source, /const _labelArbiter = new LabelArbiter\(\);/);
|
||||||
assert.match(source, /const clipW = vp3 \* px \+ vp7 \* py \+ vp11 \* pz \+ vp15;/);
|
assert.match(
|
||||||
assert.match(source, /registerWorldOverlayPaintLane\('detection', _paintDetectionLane/);
|
source,
|
||||||
|
/const clipW = vp3 \* px \+ vp7 \* py \+ vp11 \* pz \+ vp15;/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
source,
|
||||||
|
/registerWorldOverlayPaintLane\('detection', _paintDetectionLane/,
|
||||||
|
);
|
||||||
assert.match(source, /target: 'detection'/);
|
assert.match(source, /target: 'detection'/);
|
||||||
assert.match(source, /shouldPaint: _shouldPaintDetectionLane/);
|
assert.match(source, /shouldPaint: _shouldPaintDetectionLane/);
|
||||||
// The three military styles used to carry three copies of these numbers.
|
// 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,
|
3,
|
||||||
'CRT, NVG, and FLIR retain their Dense auto-enable defaults',
|
'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) ───────────────────────────
|
// ── 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(''), false);
|
||||||
assert.equal(detectionDebugRequested('?detectDebug=0'), false);
|
assert.equal(detectionDebugRequested('?detectDebug=0'), false);
|
||||||
assert.equal(detectionDebugRequested('?detectDebug'), 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(undefined), false);
|
||||||
assert.equal(detectionDebugRequested(null), false);
|
assert.equal(detectionDebugRequested(null), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the mode banner is absent by default and present behind the flag', () => {
|
test('the mode banner is absent by default and present behind the flag', () => {
|
||||||
const bannerPaints = (env) => env.detectionCtx.calls.filter(([name, text]) => (
|
const bannerPaints = (env) =>
|
||||||
name === 'fillText' && /^(SPARSE|BALANCED|DENSE) VIS:/.test(String(text))
|
env.detectionCtx.calls.filter(
|
||||||
)).length;
|
([name, text]) =>
|
||||||
|
name === 'fillText' &&
|
||||||
|
/^(SPARSE|BALANCED|DENSE) VIS:/.test(String(text)),
|
||||||
|
).length;
|
||||||
|
|
||||||
const painted = (search) => {
|
const painted = (search) => {
|
||||||
const env = installEnvironment({ 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();
|
env.postRender.raise();
|
||||||
// The overlay is alive either way — brackets still stroke.
|
// The overlay is alive either way — brackets still stroke.
|
||||||
assert.ok(
|
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',
|
'detection still paints its contacts regardless of the debug gate',
|
||||||
);
|
);
|
||||||
return bannerPaints(env);
|
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.equal(
|
||||||
assert.ok(painted('?detectDebug=1') > 0, 'the flag brings the telemetry back');
|
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', () => {
|
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',
|
type: 'AIR',
|
||||||
}));
|
}));
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.viewer);
|
||||||
initDetection(env.viewer, [{ id: layerId, getDetectableObjects: () => objects }], () => {});
|
initDetection(
|
||||||
|
env.viewer,
|
||||||
|
[{ id: layerId, getDetectableObjects: () => objects }],
|
||||||
|
() => {},
|
||||||
|
);
|
||||||
setDetectionTuning({ densityPct: 25 });
|
setDetectionTuning({ densityPct: 25 });
|
||||||
setMode('SPARSE');
|
setMode('SPARSE');
|
||||||
settleFrame(env);
|
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.visibleCount, 3);
|
||||||
assert.equal(diagnostics.densityPct, 25);
|
assert.equal(diagnostics.densityPct, 25);
|
||||||
assert.equal(diagnostics.profile, 'SPARSE');
|
assert.equal(diagnostics.profile, 'SPARSE');
|
||||||
assert.ok(diagnostics.selectedCount <= diagnostics.collectiveLabelBudget,
|
assert.ok(
|
||||||
'Sparse keeps its documented budget instead of silently becoming Dense');
|
diagnostics.selectedCount <= diagnostics.collectiveLabelBudget,
|
||||||
|
'Sparse keeps its documented budget instead of silently becoming Dense',
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
destroyDetection();
|
destroyDetection();
|
||||||
destroyWorldOverlay();
|
destroyWorldOverlay();
|
||||||
@@ -833,11 +1081,17 @@ function backdropLayer() {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
position: new Cesium.Cartesian3(-0.4, 0.2, 6_356_752),
|
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),
|
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;
|
env.ctx.calls.length = 0;
|
||||||
settleFrame(env);
|
settleFrame(env);
|
||||||
|
|
||||||
const painted = (text) => env.ctx.calls.some(([name, value]) => name === 'fillText' && value === text);
|
const painted = (text) =>
|
||||||
assert.ok(painted('GROUNDED') && painted('SKYBACK'), 'both backdrops reached the frame under test');
|
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 plate = DETECTION_THEME_MAP._default.calloutPlate;
|
||||||
const plateAlphas = env.ctx.calls
|
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 heaviest = Math.max(...plateAlphas);
|
||||||
const lightest = Math.min(...plateAlphas);
|
const lightest = Math.min(...plateAlphas);
|
||||||
assert.ok(heaviest > 0, 'the grounded contact still gets a plate');
|
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.
|
// Reverting the feature makes both plates equal, which fails here first.
|
||||||
assert.ok(
|
assert.ok(
|
||||||
lightest < heaviest * 0.5,
|
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')),
|
painted.some((text) => String(text).includes('34 KM/H')),
|
||||||
'and so does what the vehicle is doing',
|
'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(
|
||||||
assert.ok(env.ctx.calls.some(([name, , color, width]) => name === 'stroke' && color === '#05080C' && width === 3.25));
|
env.ctx.calls.some(
|
||||||
assert.ok(!env.detectionCtx.calls.some(([name, , color]) => name === 'stroke' && color === '#5EF08A'));
|
([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
|
// 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.
|
// where `klass` is read; the host's track label does not paint it.
|
||||||
assert.equal(composeLabel(objects[0]).secondary, 'MBTA · BUS 34 KM/H');
|
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', () => {
|
test('transit contacts take the vehicle tier, not the civil-air one', () => {
|
||||||
assert.equal(resolveTier({ type: 'VEH' }), 'vehicle');
|
assert.equal(resolveTier({ type: 'VEH' }), 'vehicle');
|
||||||
assert.equal(resolveTier({ type: 'AIR' }), 'civil');
|
assert.equal(resolveTier({ type: 'AIR' }), 'civil');
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
// C6: a vessel with an ambient card gets no detection callout under it (the
|
// The speed unit of a vessel's detection callout follows the UI language.
|
||||||
// card already shows name, speed and course); the speed unit of a callout
|
|
||||||
// follows the UI language.
|
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
@@ -61,20 +59,6 @@ function detectable(cameraZ) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test('a vessel with a card on screen draws no detection label', () => {
|
|
||||||
const objects = detectable(2_500);
|
|
||||||
const hidden = Object.values(objects).filter((object) => object.hideLabel);
|
|
||||||
assert.equal(hidden.length, 1, 'exactly the carded vessel hides its label');
|
|
||||||
const shown = Object.values(objects).filter((object) => !object.hideLabel);
|
|
||||||
assert.equal(shown.length, 1, 'the vessel without a card keeps its label');
|
|
||||||
assert.equal(hidden[0].skipLabel, false, 'not treated as the tracked vessel');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('past the card fade the detection label comes back', () => {
|
|
||||||
const objects = detectable(4_000_000);
|
|
||||||
assert.ok(Object.values(objects).every((object) => !object.hideLabel));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('ru: the callout speed unit is translated', () => {
|
test('ru: the callout speed unit is translated', () => {
|
||||||
setLocale('ru');
|
setLocale('ru');
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -129,7 +129,6 @@ export function createLifecycle({
|
|||||||
state.lastFocusUpdate = 0;
|
state.lastFocusUpdate = 0;
|
||||||
state.activeFocusCount = 0;
|
state.activeFocusCount = 0;
|
||||||
state.activeLabelCount = 0;
|
state.activeLabelCount = 0;
|
||||||
state.cardedRecords = new Set();
|
|
||||||
state.selectedRecord = null;
|
state.selectedRecord = null;
|
||||||
state.trail = null;
|
state.trail = null;
|
||||||
state.trailPositions = [];
|
state.trailPositions = [];
|
||||||
|
|||||||
@@ -471,7 +471,7 @@ export function createQueries({
|
|||||||
* @param {Object} [options={}] - Options from the detection system.
|
* @param {Object} [options={}] - Options from the detection system.
|
||||||
* @param {number} [options.maxCount] - Maximum objects to return (defaults to all).
|
* @param {number} [options.maxCount] - Maximum objects to return (defaults to all).
|
||||||
* @param {number} [options.seed] - Seed offset for stride sampling.
|
* @param {number} [options.seed] - Seed offset for stride sampling.
|
||||||
* @returns {Array<{position: Cesium.Cartesian3, id: string, type: string, skipLabel: boolean, hideLabel: boolean}>}
|
* @returns {Array<{position: Cesium.Cartesian3, id: string, type: string, skipLabel: boolean}>}
|
||||||
*/
|
*/
|
||||||
getDetectableObjects(options = {}) {
|
getDetectableObjects(options = {}) {
|
||||||
if (
|
if (
|
||||||
@@ -492,7 +492,6 @@ export function createQueries({
|
|||||||
const start = seed % stride;
|
const start = seed % stride;
|
||||||
|
|
||||||
const selected = state.selectedRecord;
|
const selected = state.selectedRecord;
|
||||||
const carded = state.cardedRecords;
|
|
||||||
const result = [];
|
const result = [];
|
||||||
for (let idx = 0; idx < records.length; idx += 1) {
|
for (let idx = 0; idx < records.length; idx += 1) {
|
||||||
if ((idx - start) % stride !== 0) continue;
|
if ((idx - start) % stride !== 0) continue;
|
||||||
@@ -507,9 +506,6 @@ export function createQueries({
|
|||||||
id: record.name || record.mmsi || 'VESSEL',
|
id: record.name || record.mmsi || 'VESSEL',
|
||||||
type: 'SEA',
|
type: 'SEA',
|
||||||
skipLabel: record === selected,
|
skipLabel: record === selected,
|
||||||
// An open ambient card already names the vessel with its speed and
|
|
||||||
// course; a detection label under it would only cover them.
|
|
||||||
hideLabel: carded?.has(record) === true,
|
|
||||||
klass: record.type
|
klass: record.type
|
||||||
? String(record.type).toUpperCase().slice(0, 14)
|
? String(record.type).toUpperCase().slice(0, 14)
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
VESSEL_OVERLAY_SOURCE_ID,
|
VESSEL_OVERLAY_SOURCE_ID,
|
||||||
applyVesselOverlayPolicy,
|
applyVesselOverlayPolicy,
|
||||||
VESSEL_CARD_FADE_DISTANCE_M,
|
VESSEL_CARD_FADE_DISTANCE_M,
|
||||||
VESSEL_CARD_FADE_START_RATIO,
|
|
||||||
} from '../../data/vesselLabels.js';
|
} from '../../data/vesselLabels.js';
|
||||||
import {
|
import {
|
||||||
cameraPoseSignature,
|
cameraPoseSignature,
|
||||||
@@ -367,7 +366,6 @@ export function createRendering({
|
|||||||
|
|
||||||
if (!scene || !records.length || maxLabels <= 0) {
|
if (!scene || !records.length || maxLabels <= 0) {
|
||||||
state.activeLabelCount = entries.length;
|
state.activeLabelCount = entries.length;
|
||||||
state.cardedRecords = new Set();
|
|
||||||
publishVesselOverlayEntries(entries);
|
publishVesselOverlayEntries(entries);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -405,9 +403,6 @@ export function createRendering({
|
|||||||
if (screen) accepted.push({ x: screen.x, y: screen.y });
|
if (screen) accepted.push({ x: screen.x, y: screen.y });
|
||||||
}
|
}
|
||||||
const ranked = [...cells.values()].sort((a, b) => b.score - a.score);
|
const ranked = [...cells.values()].sort((a, b) => b.score - a.score);
|
||||||
// Vessels with an ambient card: the card already shows name, speed and
|
|
||||||
// course, so the detection overlay draws no label of its own under it.
|
|
||||||
const carded = new Set();
|
|
||||||
for (const candidate of ranked) {
|
for (const candidate of ranked) {
|
||||||
if (entries.length >= maxLabels) break;
|
if (entries.length >= maxLabels) break;
|
||||||
if (
|
if (
|
||||||
@@ -420,10 +415,8 @@ export function createRendering({
|
|||||||
continue;
|
continue;
|
||||||
accepted.push({ x: candidate.x, y: candidate.y });
|
accepted.push({ x: candidate.x, y: candidate.y });
|
||||||
entries.push(components.cards.buildVesselCard(candidate.record));
|
entries.push(components.cards.buildVesselCard(candidate.record));
|
||||||
if (cardFullyShown(scene, candidate.record)) carded.add(candidate.record);
|
|
||||||
}
|
}
|
||||||
state.activeLabelCount = entries.length;
|
state.activeLabelCount = entries.length;
|
||||||
state.cardedRecords = carded;
|
|
||||||
publishVesselOverlayEntries(entries);
|
publishVesselOverlayEntries(entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,22 +469,6 @@ export function createRendering({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the host paints this ambient card at full strength: inside the
|
|
||||||
* start of its distance fade (applyVesselOverlayPolicy). Farther out the
|
|
||||||
* card fades away and the detection label stays the vessel's only text.
|
|
||||||
*/
|
|
||||||
function cardFullyShown(scene, record) {
|
|
||||||
const camera = scene.camera?.positionWC;
|
|
||||||
const visual = getVisual(record);
|
|
||||||
const position = visual.billboard?.position || visual.position;
|
|
||||||
if (!camera || !position) return true;
|
|
||||||
return (
|
|
||||||
Cesium.Cartesian3.distance(camera, position) <
|
|
||||||
VESSEL_CARD_FADE_DISTANCE_M * VESSEL_CARD_FADE_START_RATIO
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function labelPriority(record, selected) {
|
function labelPriority(record, selected) {
|
||||||
if (record === selected) return 100000;
|
if (record === selected) return 100000;
|
||||||
let score = 0;
|
let score = 0;
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ export function createVesselState({ source, services }) {
|
|||||||
activeFocusCount: 0,
|
activeFocusCount: 0,
|
||||||
activeLabelCount: 0,
|
activeLabelCount: 0,
|
||||||
/** @type {Set<Object>} Records that have an ambient card this pass. */
|
/** @type {Set<Object>} Records that have an ambient card this pass. */
|
||||||
cardedRecords: new Set(),
|
|
||||||
selectedRecord: null,
|
selectedRecord: null,
|
||||||
/** @type {{setPositions: Function, clear: Function, destroy: Function}|null} Selected-vessel fading trail */
|
/** @type {{setPositions: Function, clear: Function, destroy: Function}|null} Selected-vessel fading trail */
|
||||||
trail: null,
|
trail: null,
|
||||||
|
|||||||
Reference in new issue
Block a user