From c9f40a837cd08d025d5c1d58d31d3dff5ef37561 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Fri, 6 Nov 2026 20:20:11 +0400 Subject: [PATCH] Tighten callout fade comments and docs --- docs/CURRENT-STATE.md | 2 +- src/data/detection.js | 9 +++------ src/data/detectionHost.test.mjs | 6 ++---- src/layers/vessels/queries.js | 3 +-- src/layers/vessels/records.js | 3 +-- src/overlays/worldOverlay.js | 11 +++++------ src/overlays/worldOverlay.test.mjs | 14 ++++++-------- 7 files changed, 19 insertions(+), 29 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index c4d66de8a..6d9193078 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -5483,7 +5483,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'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. +- **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 callout fades by the painted opacity of its own ambient card, so a card dropped by budget, collisions or UI chrome, or faded 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. diff --git a/src/data/detection.js b/src/data/detection.js index f4d1ddf3b..b4dc29522 100644 --- a/src/data/detection.js +++ b/src/data/detection.js @@ -110,10 +110,7 @@ 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. - */ +/** Own-card opacity that fully hides a callout; below it the callout fades in. */ const CARD_ALPHA_HIDING_CALLOUT = 0.5; /** Alpha bands retain bracket batching while approximating a continuous radial fade. */ const BRACKET_ALPHA_STEPS = 4; @@ -1149,8 +1146,8 @@ function _paintCalloutLane(frame) { ctx.textBaseline = 'alphabetic'; for (let i = 0; i < _calloutCount; i++) { const row = _calloutPool[i]; - // Under the contact's own card (an ambient vessel card) the callout would - // show through the plate. Only a card painting this frame counts. + // The callout would show through its contact's own card: fade it by the + // opacity that card paints with this frame. const cardAlpha = row.cardEntryId === '' ? 0 diff --git a/src/data/detectionHost.test.mjs b/src/data/detectionHost.test.mjs index 4c1c6dddc..f479869f9 100644 --- a/src/data/detectionHost.test.mjs +++ b/src/data/detectionHost.test.mjs @@ -589,10 +589,8 @@ test('every style paints its OWN plate token through the production callout path }); test('a callout gives way to its contact\'s own card only while that card paints', () => { - // A vessel's ambient card already shows its name, speed and course, and the - // callout drawn beneath it shows through the card's plate. The host paints - // only part of the published cards (shared budget, collisions, UI chrome), - // so a contact whose card was published but not painted keeps its callout. + // The callout shows through its card's plate. The host paints only some + // published cards (budget, collisions, UI chrome); an unpainted one keeps it. const env = installEnvironment({ width: 900, height: 600, dpr: 1 }); try { const ships = [ diff --git a/src/layers/vessels/queries.js b/src/layers/vessels/queries.js index f26ada86b..0be276913 100644 --- a/src/layers/vessels/queries.js +++ b/src/layers/vessels/queries.js @@ -505,8 +505,7 @@ export function createQueries({ id: record.name || record.mmsi || 'VESSEL', type: 'SEA', skipLabel: record === selected, - // The vessel's ambient card: while the overlay paints it, detection - // draws no callout under it. + // Detection hides the callout while this card paints. cardSourceId: VESSEL_OVERLAY_SOURCE_ID, cardEntryId: components.cards.vesselOverlayEntryId(record), klass: record.type diff --git a/src/layers/vessels/records.js b/src/layers/vessels/records.js index aeb2f39bf..f6f581784 100644 --- a/src/layers/vessels/records.js +++ b/src/layers/vessels/records.js @@ -23,8 +23,7 @@ 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. + // Cached by `vesselOverlayEntryId`; the fields it reads never change. overlayEntryId: null, }; } diff --git a/src/overlays/worldOverlay.js b/src/overlays/worldOverlay.js index c569e03b8..71f4b849f 100644 --- a/src/overlays/worldOverlay.js +++ b/src/overlays/worldOverlay.js @@ -1047,9 +1047,9 @@ export function getOverlayPaintRect(sourceId, entryId) { } /** - * 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. + * Opacity an entry paints with this frame, or 0 when it is not queued. Custom + * lanes may call it before later lanes publish paint rects. Leaves out the + * Cyber sonar fade and yield-collision hiding. * @param {string} sourceId * @param {string} entryId * @returns {number} @@ -2253,9 +2253,8 @@ function addPaintItem(record, placement, temporalAlpha, selected) { item.collisionHidden = false; item.stamp = _frameStamp; _paintQueue[_paintCount++] = 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. + // Keyed by source and id, not `record.key`, so a lookup builds no string. + // Also holds cards fading out after they left `_records`. let bySource = _queuedItemBySource.get(record.entry.source); if (!bySource) { bySource = new Map(); diff --git a/src/overlays/worldOverlay.test.mjs b/src/overlays/worldOverlay.test.mjs index dfc82cebd..1b8223bbc 100644 --- a/src/overlays/worldOverlay.test.mjs +++ b/src/overlays/worldOverlay.test.mjs @@ -1224,9 +1224,8 @@ 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 alpha is what lets a - // contact's callout give way to that contact's own card in the same frame. + // The detection lane paints before the cards publish this frame's paint + // rects, so it reads the queued alpha instead. const env = installMockEnvironment({ width: 400, height: 300, dpr: 1 }); initWorldOverlay(env.viewer); const seen = []; @@ -2529,8 +2528,8 @@ test('promoting an ambient entry to selected paints it once with no fading dupli }); test('the queued-card index stays bounded while card identities churn', () => { - // The index is internal: capture the per-source map the same way the - // projection-record test captures its map, through Map.prototype.set. + // The index is internal: capture it through Map.prototype.set, like the + // projection-record test. const originalSet = Map.prototype.set; let queuedIndex = null; Map.prototype.set = function captureQueuedIndex(key, value) { @@ -2546,9 +2545,8 @@ test('the queued-card index stays bounded while card identities churn', () => { initWorldOverlay(env.viewer); const FRAMES = 1300; for (let frame = 0; frame < FRAMES; frame++) { - // One painted card whose identity changes every frame: the same pooled - // paint item serves every id (a vessel cohort republished as the camera - // moves, a CCTV or radio card swapping). + // One card whose id changes every frame reuses one pooled paint item, + // like a vessel cohort republished as the camera moves. setOverlayEntries('cards', [selectedEntry(`churn-card-${frame}`, { position: positionAtScreen(200, 150), })]);