mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee3466dff9 | ||
|
|
092e7e71a8 | ||
|
|
652f3c8ea1 |
No files matched your search
@@ -0,0 +1,3 @@
|
||||
- A vessel whose ambient card is on the map no longer gets a Detection callout
|
||||
showing through the card and covering its speed and course. A vessel whose
|
||||
card the overlay did not paint keeps its callout (German, @gekh, #1577).
|
||||
@@ -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'`), 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 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.
|
||||
- **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.
|
||||
|
||||
|
||||
@@ -1786,3 +1786,42 @@ test('vessel focus wire skips hidden sprites at rest while a target is tracked',
|
||||
assert.equal(result.activeCount, 1);
|
||||
for (const billboard of hidden) assert.equal(billboard.color.alpha, 1);
|
||||
});
|
||||
|
||||
test('detection objects name the vessel card their callout gives way to', () => {
|
||||
const vessel = (mmsi, name) => ({
|
||||
name,
|
||||
mmsi,
|
||||
type: 'Cargo',
|
||||
destination: '',
|
||||
speed: 9.5,
|
||||
course: 80,
|
||||
heading: 80,
|
||||
lastPositionUtc: '',
|
||||
missedRefreshes: 0,
|
||||
position: POS,
|
||||
billboard: null,
|
||||
});
|
||||
const keyed = vessel('200', 'GOLDEN BEAR');
|
||||
const unkeyed = { ...vessel('', 'NO MMSI'), lat: 37.8, lon: -122.4 };
|
||||
_setVesselStateForTest({
|
||||
viewer: {},
|
||||
records: [keyed, unkeyed],
|
||||
billboardCollection: { show: true, remove() {} },
|
||||
});
|
||||
try {
|
||||
const objects = Object.fromEntries(
|
||||
aisLiveVesselsLayer
|
||||
.getDetectableObjects()
|
||||
.map((object) => [object.id, object]),
|
||||
);
|
||||
assert.equal(objects['GOLDEN BEAR'].cardSourceId, 'ais-live-vessels');
|
||||
assert.equal(objects['GOLDEN BEAR'].cardEntryId, 'vessel:200');
|
||||
assert.equal(
|
||||
objects['NO MMSI'].cardEntryId,
|
||||
'vessel:unkeyed:NO MMSI:37.80000:-122.40000',
|
||||
'the same identity the vessel card is published under',
|
||||
);
|
||||
} finally {
|
||||
_setVesselStateForTest({ enabled: false });
|
||||
}
|
||||
});
|
||||
+22
-2
@@ -21,7 +21,10 @@ import {
|
||||
getKeyholeFadeTuning,
|
||||
keyholeLabelAlphaFromGeometry,
|
||||
} from '../celestialRing.js';
|
||||
import { registerWorldOverlayPaintLane } from '../overlays/worldOverlay.js';
|
||||
import {
|
||||
isOverlayEntryQueued,
|
||||
registerWorldOverlayPaintLane,
|
||||
} from '../overlays/worldOverlay.js';
|
||||
import {
|
||||
DETECTION_STYLE,
|
||||
DETECTION_THEME_MAP,
|
||||
@@ -576,6 +579,7 @@ 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 }));
|
||||
return result;
|
||||
}
|
||||
@@ -1077,6 +1081,9 @@ function _stashCallout(entry, acquireFade, keyhole) {
|
||||
font: FONT,
|
||||
microFont: MICRO_FONT,
|
||||
alpha: 1,
|
||||
cardSourceId: '',
|
||||
cardEntryId: '',
|
||||
yielded: false,
|
||||
};
|
||||
_calloutPool[_calloutCount] = row;
|
||||
}
|
||||
@@ -1103,6 +1110,9 @@ function _stashCallout(entry, acquireFade, keyhole) {
|
||||
row.font = FONT;
|
||||
row.microFont = MICRO_FONT;
|
||||
row.alpha = alpha;
|
||||
row.cardSourceId = candidate.cardSourceId;
|
||||
row.cardEntryId = candidate.cardEntryId;
|
||||
row.yielded = false;
|
||||
_calloutCount++;
|
||||
}
|
||||
|
||||
@@ -1134,7 +1144,15 @@ function _paintCalloutLane(frame) {
|
||||
ctx.textBaseline = 'alphabetic';
|
||||
for (let i = 0; i < _calloutCount; i++) {
|
||||
const row = _calloutPool[i];
|
||||
paintDetectionCallout(ctx, row, row.alpha);
|
||||
// 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);
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
@@ -1211,6 +1229,8 @@ function _materializeCandidate(
|
||||
primary,
|
||||
micro,
|
||||
hasMicro: card.hasMicro,
|
||||
cardSourceId: obj.cardSourceId ? String(obj.cardSourceId) : '',
|
||||
cardEntryId: obj.cardEntryId ? String(obj.cardEntryId) : '',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,11 @@ import {
|
||||
suspendDetection,
|
||||
} from './detection.js';
|
||||
import {
|
||||
clearOverlaySource,
|
||||
destroyWorldOverlay,
|
||||
initWorldOverlay,
|
||||
setOverlayEntries,
|
||||
setOverlaySourceVisible,
|
||||
} from '../overlays/worldOverlay.js';
|
||||
import { DETECTION_THEME_MAP } from '../overlays/worldOverlayTokens.js';
|
||||
import {
|
||||
@@ -579,6 +581,66 @@ 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.
|
||||
const env = installEnvironment({ width: 900, height: 600, dpr: 1 });
|
||||
try {
|
||||
const ships = [
|
||||
{ x: -0.3, sourceId: 'carded', id: 'SHIP-CARDED', card: 'vessel:carded' },
|
||||
{ x: 0.3, sourceId: 'dropped', id: 'SHIP-DROPPED', card: 'vessel:dropped' },
|
||||
].map(({ x, sourceId, id, card }) => ({
|
||||
position: new Cesium.Cartesian3(x, 0, 6_356_752),
|
||||
sourceId,
|
||||
id,
|
||||
metric: '6 kn',
|
||||
type: 'SEA',
|
||||
cardSourceId: card === 'vessel:carded' ? 'vessel-cards' : 'dropped-cards',
|
||||
cardEntryId: card,
|
||||
}));
|
||||
initWorldOverlay(env.viewer);
|
||||
initDetection(env.viewer, [{ id: 'ais-live-vessels', getDetectableObjects: () => ships }], () => {});
|
||||
const card = (id, title, x) => ({
|
||||
id,
|
||||
position: new Cesium.Cartesian3(x, 0, 6_356_752),
|
||||
variant: 'label',
|
||||
title,
|
||||
selected: true,
|
||||
protected: true,
|
||||
horizonCull: false,
|
||||
edgeFade: 'none',
|
||||
});
|
||||
setOverlayEntries('vessel-cards', [card('vessel:carded', 'CARD-CARDED', -0.3)]);
|
||||
// Published, but the host does not paint it this frame.
|
||||
setOverlayEntries('dropped-cards', [card('vessel:dropped', 'CARD-DROPPED', 0.3)]);
|
||||
setOverlaySourceVisible('dropped-cards', false);
|
||||
setMode('DENSE');
|
||||
settleFrame(env);
|
||||
env.ctx.calls.length = 0;
|
||||
settleFrame(env);
|
||||
|
||||
const painted = (text) => env.ctx.calls.some(([name, value]) => name === 'fillText' && value === text);
|
||||
assert.ok(painted('CARD-CARDED'), 'the fixture card reached the frame');
|
||||
assert.ok(!painted('CARD-DROPPED'), 'the hidden source paints no card');
|
||||
assert.ok(!painted('SHIP-CARDED'), 'no callout under the painted card');
|
||||
assert.ok(painted('SHIP-DROPPED'), 'the contact whose card did not paint keeps its callout');
|
||||
assert.equal(getDetectionDiagnostics().calloutRects.length, 1,
|
||||
'only the callout that painted is reported');
|
||||
|
||||
// The card leaves the host: the callout comes back on the next frame.
|
||||
clearOverlaySource('vessel-cards');
|
||||
env.ctx.calls.length = 0;
|
||||
settleFrame(env);
|
||||
assert.ok(painted('SHIP-CARDED'), 'the callout returns once its card stops painting');
|
||||
} 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
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
REFRESH_MS,
|
||||
FOCUS_EVIDENCE_DEV,
|
||||
} from './policy.js';
|
||||
import { VESSEL_OVERLAY_SOURCE_ID } from '../../data/vesselLabels.js';
|
||||
|
||||
export function createQueries({
|
||||
vesselState,
|
||||
@@ -469,7 +470,7 @@ export function createQueries({
|
||||
* @param {Object} [options={}] - Options from the detection system.
|
||||
* @param {number} [options.maxCount] - Maximum objects to return (defaults to all).
|
||||
* @param {number} [options.seed] - Seed offset for stride sampling.
|
||||
* @returns {Array<{position: Cesium.Cartesian3, id: string, type: string, skipLabel: boolean}>}
|
||||
* @returns {Array<{position: Cesium.Cartesian3, id: string, type: string, skipLabel: boolean, cardSourceId: string, cardEntryId: string}>}
|
||||
*/
|
||||
getDetectableObjects(options = {}) {
|
||||
if (
|
||||
@@ -504,6 +505,10 @@ 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.
|
||||
cardSourceId: VESSEL_OVERLAY_SOURCE_ID,
|
||||
cardEntryId: components.cards.vesselOverlayEntryId(record),
|
||||
klass: record.type
|
||||
? String(record.type).toUpperCase().slice(0, 14)
|
||||
: undefined,
|
||||
|
||||
@@ -1065,6 +1065,28 @@ export function getOverlayPaintRect(sourceId, entryId) {
|
||||
return rect && rect.key === key && rect.stamp === _frameStamp ? rect : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param {string} sourceId
|
||||
* @param {string} entryId
|
||||
* @returns {boolean}
|
||||
*/
|
||||
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.
|
||||
return (
|
||||
record.sourceAlpha *
|
||||
record.queuedItem.temporalAlpha *
|
||||
record.distanceAlpha *
|
||||
record.altitudeAlpha >
|
||||
0.001
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the topmost interactive world-overlay entry at CSS-pixel coords.
|
||||
* @param {number} x
|
||||
@@ -1737,6 +1759,8 @@ function getProjectionRecord(entry) {
|
||||
contentAlpha: 1,
|
||||
altitudeAlpha: 1,
|
||||
sourceAlpha: 1,
|
||||
queuedStamp: 0,
|
||||
queuedItem: null,
|
||||
protectedPlacement: null,
|
||||
distanceOptions: {},
|
||||
altitudeOptions: {},
|
||||
@@ -2254,6 +2278,11 @@ function addPaintItem(record, placement, temporalAlpha, selected) {
|
||||
item.zIndex = record.entry.zIndex;
|
||||
item.key = record.key;
|
||||
_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;
|
||||
}
|
||||
|
||||
function sourceCanPaint(sourceId) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
activateWorldOverlayAction,
|
||||
initWorldOverlay,
|
||||
subscribeWorldOverlayActions,
|
||||
isOverlayEntryQueued,
|
||||
isOverlayPointVisible,
|
||||
normalizeOverlayEntry,
|
||||
paintLaneForOverlayEntry,
|
||||
@@ -1213,6 +1214,43 @@ test('custom detection lane receives the shared host frame and paints below ordi
|
||||
env.cleanup();
|
||||
});
|
||||
|
||||
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
|
||||
// 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'),
|
||||
rect: getOverlayPaintRect('cards', 'SHOWN'),
|
||||
});
|
||||
}, { id: 'detection', active: true, target: 'detection' });
|
||||
setOverlayEntries('cards', [selectedEntry('SHOWN', {
|
||||
position: positionAtScreen(200, 150),
|
||||
})]);
|
||||
setOverlayEntries('hidden-cards', [selectedEntry('HIDDEN', {
|
||||
position: positionAtScreen(100, 100),
|
||||
})]);
|
||||
setOverlaySourceVisible('hidden-cards', false);
|
||||
|
||||
env.postRender.raise();
|
||||
assert.deepEqual(seen[0], { shown: true, hidden: false, unknown: false, 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');
|
||||
destroyWorldOverlay();
|
||||
assert.equal(isOverlayEntryQueued('cards', 'SHOWN'), false);
|
||||
env.cleanup();
|
||||
});
|
||||
|
||||
test('custom-lane painter state is bracketed by host save/restore', () => {
|
||||
// A painter that leaks a clip or alpha must not corrupt later frames: the
|
||||
// host wraps every custom-lane painter in save/try/finally-restore. Pin the
|
||||
|
||||
Reference in new issue
Block a user