Fade a vessel's callout by its card's painted opacity

The callout now asks for the opacity its own card paints with this frame,
keyhole fade included, and fades out as the card fades in. A card faded out
at the keyhole rim leaves the callout readable, and a card fading out after
leaving its source still hides it. The vessel card id is cached on the
record, and the allocation gate covers the lookup.
This commit is contained in:
German Khokhlov committed 2026-11-05 21:21:02 +04:00
1 parent ee3466dff9
commit da00b096d2
9 files changed
+274 -57

No files matched your search

+1 -1
View File
@@ -5322,7 +5322,7 @@ silently demoting every later lookup for the session.
Mission world text has no native `LabelGraphics`: overview launch markers publish at most 48 ambient candidates for a 24-winner budget, while selecting a mission clears that overview source and publishes its launch-site callout, stage re-entry annotations, live/estimated payload-position readout, and orbit annotation as protected selected-lane entries. The source retains the exact former strings and colors. Static anchors reuse the Cartesian values used to build their mission geometry; the moving payload entry reads the layer's per-frame live-position cache; catalog-backed orbit annotation positions are cached in the same one-second matrix update that realigns the orbit primitive. Keyhole edge fade, horizon culling, final collision placement, and UI exclusion are owned by the shared host. Deselect restores the bounded overview, and refresh, disable, and destroy replace or clear both mission sources.
A selected mission renders its orbit as four repeating tactical sectors, each containing one prominent cyan dot followed by one hundred thin translucent dashes. The bright dots act as orbit anchors while the subdued dash field remains depth-tested against the globe and is shown only for the selected mission.
Close selected-pad views add one static 500 m-radius cyan launch-zone ring with a low-opacity translucent fill over the sampled photoreal launch-site surface. The single scene primitive is created only for the visible selected site and is otherwise dormant. It appears during Focus, sufficiently close manual zoom, and the replay countdown, but is suppressed above 120 km camera altitude, beyond 180 km direct camera-to-pad range, for unselected missions, and whenever Space Missions is inactive. Focus establishes a launch-site-centered camera transform once; subsequent manual heading and pitch changes remain centered on that site without an automated per-frame correction. Surface mission markers and labels use an additional conservative globe-limb margin before the exact ellipsoid occluder boundary, preventing near-horizon visibility from alternating between frames.
- **AIS vessels**: chevron symbology (naval cyan base, type tints), world-space headings, MMSI-keyed reconciliation (selection survives refreshes; pinned 3 refreshes with STALE marker when absent), detection-overlay integration (`type: 'SEA'`; a vessel whose ambient card the shared host paints in the current frame draws no detection callout, because the callout would show through the card's plate, while a vessel whose card was published but not painted (shared budget, collisions, UI chrome) keeps its callout — its detectable object names the card in `cardSourceId`/`cardEntryId`, and the callout lane asks `isOverlayEntryQueued` in `src/overlays/worldOverlay.js`, which answers from the solved paint queue because the detection lane paints before the cards publish their paint rects), contextStore registration for voice Q&A. Empty-space clicks, id-less photorealistic-tile picks, and Escape dismiss the vessel card/HUD/context and clear its trail; picks owned by another layer (including `gev-trail:*`) and raw vessel-record picks without a live MMSI key are no-ops for vessel selection. Click and key handlers detach while the layer is disabled and reinstall on enable. Selecting another vessel replaces the selection and trail, and reconciliation clears a trail if its owning vessel is evicted.
- **AIS vessels**: chevron symbology (naval cyan base, type tints), world-space headings, MMSI-keyed reconciliation (selection survives refreshes; pinned 3 refreshes with STALE marker when absent), detection-overlay integration (`type: 'SEA'`; a vessel's detection callout fades out while its own ambient card paints in the same frame, judged by the card's painted opacity including the keyhole fade, so a card dropped by budget, collisions or UI chrome, or faded out at the keyhole rim, leaves the callout readable), contextStore registration for voice Q&A. Empty-space clicks, id-less photorealistic-tile picks, and Escape dismiss the vessel card/HUD/context and clear its trail; picks owned by another layer (including `gev-trail:*`) and raw vessel-record picks without a live MMSI key are no-ops for vessel selection. Click and key handlers detach while the layer is disabled and reinstall on enable. Selecting another vessel replaces the selection and trail, and reconciliation clears a trail if its owning vessel is evicted.
- **Track trails**: server accumulates per-MMSI ring buffers (`/api/vessels/track?mmsi=`, Float32+Uint32, 64 samples, 30s/25m thinning); aircraft backfill proxies `/api/flights/track` (OAuth, own credit bucket). Military aircraft keep only their locally accumulated trails: `/api/military/track` validates the hex, then answers `503 history_unavailable` without an upstream request.
- Shared `src/data/pickRegistry.js` stops the two flight layers' click handlers from fighting over the camera.
+1
View File
@@ -1821,6 +1821,7 @@ test('detection objects name the vessel card their callout gives way to', () =>
'vessel:unkeyed:NO MMSI:37.80000:-122.40000',
'the same identity the vessel card is published under',
);
assert.equal(keyed.overlayEntryId, 'vessel:200', 'cached on the record for later frames');
} finally {
_setVesselStateForTest({ enabled: false });
}
+19 -14
View File
@@ -22,7 +22,7 @@ import {
keyholeLabelAlphaFromGeometry,
} from '../celestialRing.js';
import {
isOverlayEntryQueued,
getQueuedOverlayEntryAlpha,
registerWorldOverlayPaintLane,
} from '../overlays/worldOverlay.js';
import {
@@ -110,6 +110,11 @@ const THEME_MAP = DETECTION_THEME_MAP;
const SPARSE_ZONE_FRACTION = 0.5;
/** Solve cadence for membership/collision; accepted identities still reproject every frame. */
const LABEL_SOLVE_INTERVAL_MS = 125;
/**
* A contact's own card at this opacity or more hides its callout; below it the
* callout fades in, so a card fading in or out never snaps the callout.
*/
const CARD_ALPHA_HIDING_CALLOUT = 0.5;
/** Alpha bands retain bracket batching while approximating a continuous radial fade. */
const BRACKET_ALPHA_STEPS = 4;
/** Finer bands for Global Context echo brackets, whose fade is animated. */
@@ -579,8 +584,8 @@ export function getDetectionDiagnostics() {
// sprites/strokes. Nothing is copied on the production paint path.
result.calloutRects = _calloutPool
.slice(0, _calloutCount)
.filter((row) => !row.yielded)
.map(({ x, y, w, h, alpha }) => ({ x, y, w, h, alpha }));
.filter((row) => row.paintAlpha > 0.001)
.map(({ x, y, w, h, paintAlpha }) => ({ x, y, w, h, alpha: paintAlpha }));
return result;
}
@@ -1083,7 +1088,7 @@ function _stashCallout(entry, acquireFade, keyhole) {
alpha: 1,
cardSourceId: '',
cardEntryId: '',
yielded: false,
paintAlpha: 1,
};
_calloutPool[_calloutCount] = row;
}
@@ -1112,7 +1117,7 @@ function _stashCallout(entry, acquireFade, keyhole) {
row.alpha = alpha;
row.cardSourceId = candidate.cardSourceId;
row.cardEntryId = candidate.cardEntryId;
row.yielded = false;
row.paintAlpha = alpha;
_calloutCount++;
}
@@ -1144,15 +1149,15 @@ function _paintCalloutLane(frame) {
ctx.textBaseline = 'alphabetic';
for (let i = 0; i < _calloutCount; i++) {
const row = _calloutPool[i];
// The contact's own card (an ambient vessel card) already names it, and
// this callout would show through the card's plate. Checked every frame
// against the cards queued for THIS frame: the host paints only part of
// the published cards, and a contact whose card did not paint keeps its
// callout.
row.yielded =
row.cardEntryId !== '' &&
isOverlayEntryQueued(row.cardSourceId, row.cardEntryId);
if (!row.yielded) paintDetectionCallout(ctx, row, row.alpha);
// Under the contact's own card (an ambient vessel card) the callout would
// show through the plate. Only a card painting this frame counts.
const cardAlpha =
row.cardEntryId === ''
? 0
: getQueuedOverlayEntryAlpha(row.cardSourceId, row.cardEntryId);
row.paintAlpha =
row.alpha * Math.max(0, 1 - cardAlpha / CARD_ALPHA_HIDING_CALLOUT);
if (row.paintAlpha > 0.001) paintDetectionCallout(ctx, row, row.paintAlpha);
}
ctx.globalAlpha = 1;
}
+174
View File
@@ -21,10 +21,17 @@ import {
import {
clearOverlaySource,
destroyWorldOverlay,
getOverlayPaintRect,
initWorldOverlay,
setOverlayEntries,
setOverlaySourceVisible,
} from '../overlays/worldOverlay.js';
import { applyVesselOverlayPolicy } from './vesselLabels.js';
import {
getKeyholeFadeTuning,
getKeyholeGeometry,
keyholeLabelAlphaFromGeometry,
} from '../celestialRing.js';
import { DETECTION_THEME_MAP } from '../overlays/worldOverlayTokens.js';
import {
clearAwarenessEchoField,
@@ -641,6 +648,173 @@ test('a callout gives way to its contact\'s own card only while that card paints
}
});
/** A vessel detection contact that names its ambient card, like the AIS layer does. */
function shipWithCard(mmsi, position) {
return {
position,
sourceId: mmsi,
id: `SHIP-${mmsi}`,
metric: '6 kn',
type: 'SEA',
cardSourceId: 'ais-live-vessels',
cardEntryId: `vessel:${mmsi}`,
};
}
/** The production ambient vessel card (not protected: it goes through the arbiter). */
function ambientVesselCard(mmsi, position) {
return applyVesselOverlayPolicy({
id: `vessel:${mmsi}`,
position,
title: `CARD-${mmsi}`,
details: ['CARGO · 6KT · 80°'],
gapPx: 10,
});
}
test('a callout stays readable when its own card is faded out at the keyhole rim', () => {
// Default keyhole tuning: 7% feather, 1% outside opacity.
assert.deepEqual(getKeyholeFadeTuning(), { fadeRatio: 0.07, outsideOpacity: 0.01 });
const width = 1600;
const height = 900;
const keyhole = getKeyholeGeometry(width, height);
const env = installEnvironment({ width, height, dpr: 1 });
// Just outside the keyhole's left rim: the card sits above or below the
// anchor (outside, 1% alpha), the callout to its right (inside the feather).
const position = new Cesium.Cartesian3(270 / (width / 2) - 1, 1 - 460 / (height / 2), 6_356_752);
const ship = shipWithCard('200', position);
try {
initWorldOverlay(env.viewer);
initDetection(env.viewer, [{ id: 'ais-live-vessels', getDetectableObjects: () => [ship] }], () => {});
setMode('DENSE');
for (let i = 0; i < 4; i++) settleFrame(env);
const [alone] = getDetectionDiagnostics().calloutRects;
assert.ok(alone && alone.alpha >= 0.5, 'without a card the callout is clearly visible');
setOverlayEntries('ais-live-vessels', [ambientVesselCard('200', position)]);
for (let i = 0; i < 4; i++) settleFrame(env);
const rect = getOverlayPaintRect('ais-live-vessels', 'vessel:200');
assert.ok(rect, 'the host paints the card (at the outside opacity)');
const cardAlpha = keyholeLabelAlphaFromGeometry(rect.x + rect.w / 2, rect.y + rect.h / 2, keyhole);
assert.ok(cardAlpha <= 0.05, `the card is invisible at the rim (keyhole alpha ${cardAlpha})`);
const callouts = getDetectionDiagnostics().calloutRects;
assert.equal(callouts.length, 1, 'the callout keeps painting while its card is faded out');
assert.ok(callouts[0].alpha >= 0.4, 'and stays readable');
} finally {
destroyDetection();
destroyWorldOverlay();
env.cleanup();
}
});
test('a callout stays hidden while its card fades out after leaving the published cohort', () => {
const env = installEnvironment({ width: 1600, height: 900, dpr: 1 });
const at = (x, y) => new Cesium.Cartesian3(x, y, 6_356_752);
const ship = shipWithCard('200', at(0.1, 0.1));
const painted = (text) => env.ctx.calls.some(([name, value]) => name === 'fillText' && value === text);
const frame = (ms) => {
env.ctx.calls.length = 0;
env.advance(ms);
env.postRender.raise();
};
try {
initWorldOverlay(env.viewer);
initDetection(env.viewer, [{ id: 'ais-live-vessels', getDetectableObjects: () => [ship] }], () => {});
setMode('DENSE');
setOverlayEntries('ais-live-vessels', [ambientVesselCard('200', ship.position)]);
for (let i = 0; i < 6; i++) frame(250);
frame(16);
assert.ok(painted('CARD-200') && !painted('SHIP-200'), 'steady state: card only');
// The vessel layer republishes its screen-grid cohort after a camera move:
// this vessel's card is replaced by another one, the vessel stays detectable.
setOverlayEntries('ais-live-vessels', [ambientVesselCard('300', at(-0.5, -0.4))]);
frame(16);
assert.ok(getOverlayPaintRect('ais-live-vessels', 'vessel:200'), 'the dropped card is still fading out');
assert.ok(painted('CARD-200'), 'and its text still paints');
assert.ok(!painted('SHIP-200'), 'no full callout under the fading card');
for (let i = 0; i < 4; i++) frame(250);
assert.ok(!painted('CARD-200'), 'the card has faded out');
assert.ok(painted('SHIP-200'), 'and the callout is back');
} finally {
destroyDetection();
destroyWorldOverlay();
env.cleanup();
}
});
test('a vessel whose ambient card loses the collision keeps its callout', () => {
const env = installEnvironment({ width: 900, height: 600, dpr: 1 });
// Four vessels a few pixels apart: their cards collide (a card goes only
// above or below its anchor), so the arbiter paints only some of them.
const ships = [0, 1, 2, 3].map((i) =>
shipWithCard(String(100 + i), new Cesium.Cartesian3(-0.2 + i * 0.005, i * 0.005, 6_356_752)),
);
const painted = (text) => env.ctx.calls.some(([name, value]) => name === 'fillText' && value === text);
try {
initWorldOverlay(env.viewer);
initDetection(env.viewer, [{ id: 'ais-live-vessels', getDetectableObjects: () => ships }], () => {});
setOverlayEntries(
'ais-live-vessels',
ships.map((ship) => ambientVesselCard(ship.sourceId, ship.position)),
);
setMode('DENSE');
for (let i = 0; i < 4; i++) settleFrame(env);
env.ctx.calls.length = 0;
settleFrame(env);
const winners = ships.filter((ship) => getOverlayPaintRect('ais-live-vessels', ship.cardEntryId));
assert.ok(
winners.length >= 1 && winners.length < ships.length,
`the colliding cards leave some unpainted (${winners.length} of ${ships.length} painted)`,
);
for (const ship of ships) {
const carded = winners.includes(ship);
assert.equal(
painted(ship.id),
!carded,
carded ? `${ship.id} draws no callout under its card` : `${ship.id} keeps its callout`,
);
}
} finally {
destroyDetection();
destroyWorldOverlay();
env.cleanup();
}
});
test('a callout fades out while its card fades in instead of vanishing at once', () => {
const env = installEnvironment({ width: 1600, height: 900, dpr: 1 });
const ship = shipWithCard('200', new Cesium.Cartesian3(0.1, 0.1, 6_356_752));
try {
initWorldOverlay(env.viewer);
initDetection(env.viewer, [{ id: 'ais-live-vessels', getDetectableObjects: () => [ship] }], () => {});
setMode('DENSE');
for (let i = 0; i < 4; i++) settleFrame(env);
const [alone] = getDetectionDiagnostics().calloutRects;
assert.ok(alone.alpha >= 0.9, 'the callout starts fully visible');
setOverlayEntries('ais-live-vessels', [ambientVesselCard('200', ship.position)]);
const alphas = [];
for (let i = 0; i < 30; i++) {
env.advance(16);
env.postRender.raise();
alphas.push(getDetectionDiagnostics().calloutRects[0]?.alpha ?? 0);
}
assert.ok(
alphas.some((alpha) => alpha > 0.1 && alpha < alone.alpha - 0.1),
`the callout passes through partial opacity: ${alphas.map((a) => a.toFixed(2)).join(' ')}`,
);
assert.equal(alphas.at(-1), 0, 'and is gone once the card is fully in');
} finally {
destroyDetection();
destroyWorldOverlay();
env.cleanup();
}
});
test('space-tier callouts take the heavier plate while air contacts keep the light one', () => {
// Satellites sit over the high-albedo lit Earth disc far more often than
// aircraft do, so the space tier carries the heavier backing. Losing the
+8
View File
@@ -117,6 +117,14 @@ export function createCards({
/** Stable overlay identity for MMSI-keyed and source-retained unkeyed rows. */
function vesselOverlayEntryId(record) {
// Cached on the record: detection asks for it every frame for every vessel.
if (record?.overlayEntryId) return record.overlayEntryId;
const id = composeVesselOverlayEntryId(record);
if (record) record.overlayEntryId = id;
return id;
}
function composeVesselOverlayEntryId(record) {
const mmsi = String(record?.mmsi || '').trim();
if (mmsi) return `vessel:${mmsi}`;
const name = String(record?.name || 'VESSEL').trim() || 'VESSEL';
+3
View File
@@ -23,6 +23,9 @@ export function normalizeVessel(row) {
lastPositionUtc: String(row.last_position_UTC || ''),
lastPositionEpoch: finiteNumber(row.last_position_epoch),
missedRefreshes: 0,
// Filled on first use by the cards' `vesselOverlayEntryId`. The id reads
// only the MMSI, or for an unkeyed row fields it never updates.
overlayEntryId: null,
};
}
+57 -34
View File
@@ -258,6 +258,10 @@ const _paintQueue = [];
const _paintItemPool = [];
const _paintRectPool = [];
const _paintRectByKey = new Map();
/** @type {Map<string, Map<string, object>>} This frame's paint items by source and entry id. */
const _queuedItemBySource = new Map();
/** Index rows that stop painting are dropped this many frames apart. */
const QUEUED_ITEM_PRUNE_FRAMES = 600;
const _hitRects = [];
const _paintedBySource = Object.create(null);
const _paintedSourceKeys = [];
@@ -1066,24 +1070,26 @@ export function getOverlayPaintRect(sourceId, entryId) {
}
/**
* Whether an entry is queued to paint in the frame being painted. Custom lanes
* run before the host's own entries, so `getOverlayPaintRect` has nothing for
* this frame yet while they paint; this answers from the solved paint queue.
* Opacity an entry will paint with in the frame being painted, or 0 when it is
* not queued. Valid from the custom lanes, which paint before the entries in
* later lanes publish their paint rects. Leaves out only the Cyber sonar fade.
* @param {string} sourceId
* @param {string} entryId
* @returns {boolean}
* @returns {number}
*/
export function isOverlayEntryQueued(sourceId, entryId) {
if (_destroyed) return false;
const record = _records.get(entryKey(String(sourceId), String(entryId)));
if (!record || record.queuedStamp !== _frameStamp) return false;
// Same channels as the paint, minus the per-frame keyhole and sonar fades.
export function getQueuedOverlayEntryAlpha(sourceId, entryId) {
if (_destroyed) return 0;
const item = _queuedItemBySource.get(sourceId)?.get(entryId);
// Pooled items are reassigned every frame: check the frame and the entry.
if (!item || item.stamp !== _frameStamp) return 0;
const record = item.record;
if (record.entry.id !== entryId || record.entry.source !== sourceId) return 0;
return (
record.sourceAlpha *
record.queuedItem.temporalAlpha *
record.distanceAlpha *
record.altitudeAlpha >
0.001
item.temporalAlpha *
record.distanceAlpha *
record.altitudeAlpha *
(_keyhole ? entryKeyholeAlpha(record.entry, item.placement, _keyhole) : 1)
);
}
@@ -1759,8 +1765,6 @@ function getProjectionRecord(entry) {
contentAlpha: 1,
altitudeAlpha: 1,
sourceAlpha: 1,
queuedStamp: 0,
queuedItem: null,
protectedPlacement: null,
distanceOptions: {},
altitudeOptions: {},
@@ -2277,12 +2281,27 @@ function addPaintItem(record, placement, temporalAlpha, selected) {
item.lane = paintLaneForOverlayEntry(record.entry);
item.zIndex = record.entry.zIndex;
item.key = record.key;
item.stamp = _frameStamp;
_paintQueue[_paintCount++] = item;
// Read by `isOverlayEntryQueued` while the custom lanes paint, before this
// item publishes its paint rect. Only a stamp and a pointer: storing the
// item's alpha here would box a double per paint item per frame.
record.queuedStamp = _frameStamp;
record.queuedItem = item;
// Indexed by source and id rather than by `record.key`, so a lookup builds
// no string. A card fading out after it left its source's entries is still
// queued here, though `_records` no longer has it.
let bySource = _queuedItemBySource.get(record.entry.source);
if (!bySource) {
bySource = new Map();
_queuedItemBySource.set(record.entry.source, bySource);
}
bySource.set(record.entry.id, item);
}
/** Drop index rows for entries that stopped painting (bounded, not per frame). */
function pruneQueuedItemIndex() {
for (const [sourceId, bySource] of _queuedItemBySource) {
for (const [entryId, item] of bySource) {
if (item.stamp !== _frameStamp) bySource.delete(entryId);
}
if (bySource.size === 0) _queuedItemBySource.delete(sourceId);
}
}
function sourceCanPaint(sourceId) {
@@ -2354,6 +2373,7 @@ function solveDomains(timestamp) {
solveRevision += domain.arbiter.solveRevision;
}
_paintQueue.length = _paintCount;
if (_frameStamp % QUEUED_ITEM_PRUNE_FRAMES === 0) pruneQueuedItemIndex();
_solveDirty = false;
_diagnostics.solveMs = solveMs;
_diagnostics.selectedCount = selectedCount;
@@ -2482,23 +2502,25 @@ function paintCustomLane(lane) {
}
}
/** Keyhole edge fade of one placed entry (1 for entries that do not fade). */
function entryKeyholeAlpha(entry, placement, keyhole) {
if (entry.edgeFade !== 'keyhole') return 1;
const keyholeX = placement.centerX - keyhole.centerX;
const keyholeY = placement.centerY - keyhole.centerY;
return keyholeX * keyholeX + keyholeY * keyholeY <=
keyhole.radius * keyhole.radius
? 1
: keyholeLabelAlphaFromGeometry(
placement.centerX,
placement.centerY,
keyhole,
);
}
function paintEntryItem(item, keyhole, sonar) {
const { record, placement } = item;
const entry = record.entry;
let keyholeAlpha = 1;
if (entry.edgeFade === 'keyhole') {
const keyholeX = placement.centerX - keyhole.centerX;
const keyholeY = placement.centerY - keyhole.centerY;
keyholeAlpha =
keyholeX * keyholeX + keyholeY * keyholeY <=
keyhole.radius * keyhole.radius
? 1
: keyholeLabelAlphaFromGeometry(
placement.centerX,
placement.centerY,
keyhole,
);
}
const keyholeAlpha = entryKeyholeAlpha(entry, placement, keyhole);
// All five channels are normalized at their source. Keep the multiply on
// the hot paint path so its intermediate doubles remain unboxed; the pure
// `combinedOverlayAlpha` export still specifies/tests the same binding.
@@ -2799,6 +2821,7 @@ export function destroyWorldOverlay() {
_accessibleActivatorByKey.clear();
_accessibilitySignature = '';
_paintRectByKey.clear();
_queuedItemBySource.clear();
_paintCount = 0;
_paintRectCount = 0;
_hitRectCount = 0;
+8 -8
View File
@@ -24,7 +24,7 @@ import {
activateWorldOverlayAction,
initWorldOverlay,
subscribeWorldOverlayActions,
isOverlayEntryQueued,
getQueuedOverlayEntryAlpha,
isOverlayPointVisible,
normalizeOverlayEntry,
paintLaneForOverlayEntry,
@@ -1216,16 +1216,16 @@ test('custom detection lane receives the shared host frame and paints below ordi
test('a detection lane sees which cards this frame will paint before they publish paint rects', () => {
// The detection lane paints first, so a card's paint rectangle for this
// frame does not exist yet when it runs. The queued check is what lets a
// frame does not exist yet when it runs. The queued alpha is what lets a
// contact's callout give way to that contact's own card in the same frame.
const env = installMockEnvironment({ width: 400, height: 300, dpr: 1 });
initWorldOverlay(env.viewer);
const seen = [];
registerWorldOverlayPaintLane('detection', () => {
seen.push({
shown: isOverlayEntryQueued('cards', 'SHOWN'),
hidden: isOverlayEntryQueued('hidden-cards', 'HIDDEN'),
unknown: isOverlayEntryQueued('cards', 'NOT-PUBLISHED'),
shown: getQueuedOverlayEntryAlpha('cards', 'SHOWN'),
hidden: getQueuedOverlayEntryAlpha('hidden-cards', 'HIDDEN'),
unknown: getQueuedOverlayEntryAlpha('cards', 'NOT-PUBLISHED'),
rect: getOverlayPaintRect('cards', 'SHOWN'),
});
}, { id: 'detection', active: true, target: 'detection' });
@@ -1238,16 +1238,16 @@ test('a detection lane sees which cards this frame will paint before they publis
setOverlaySourceVisible('hidden-cards', false);
env.postRender.raise();
assert.deepEqual(seen[0], { shown: true, hidden: false, unknown: false, rect: null },
assert.deepEqual(seen[0], { shown: 1, hidden: 0, unknown: 0, rect: null },
'the card is queued while its paint rect is still unpublished');
assert.ok(getOverlayPaintRect('cards', 'SHOWN'), 'the card did paint after the lane');
assert.equal(getOverlayPaintRect('hidden-cards', 'HIDDEN'), null);
clearOverlaySource('cards');
env.postRender.raise();
assert.equal(seen[1].shown, false, 'a card cleared since the last frame is no longer queued');
assert.equal(seen[1].shown, 0, 'a card cleared since the last frame is no longer queued');
destroyWorldOverlay();
assert.equal(isOverlayEntryQueued('cards', 'SHOWN'), false);
assert.equal(getQueuedOverlayEntryAlpha('cards', 'SHOWN'), 0);
env.cleanup();
});
@@ -1217,6 +1217,9 @@ function buildPhase6DetectionWorkload(count) {
: `FL${String(180 + (index % 220)).padStart(3, '0')}`,
type: ['AIR', 'SAT', 'SEA', 'VEH'][index % 4],
tier: index % 17 === 0 ? 'military' : undefined,
// Vessels name their ambient card, so each callout asks the host for it.
cardSourceId: index % 4 === 2 ? 'ais-live-vessels' : undefined,
cardEntryId: index % 4 === 2 ? `vessel:${index}` : undefined,
});
}
return {