feat(cockpit): ride a tracked satellite in an orbital view

With a satellite tracked and no aircraft tracked, Cockpit can ride the
satellite. Entry needs only the Satellites layer; debris can be tracked but
not entered, and voice stays aircraft-only. Double-clicking the tracked
satellite or the entry buttons (reading ORBIT) enter it.

The camera sits on the satellite's per-frame SGP4 position and looks
straight down, turned to the ground track (north-up for a satellite with
none). The optical zoom steps 1x to 8x with a focus pull, the arrows tilt
the view up to 45 degrees off nadir, and Home or 0 returns to nadir. Off
nadir the tilt reads its total angle first and the swath readout becomes an
approximation marked with ≈. While in orbit the map leases flat Esri
imagery, the cloud pass stays off and the view opens in the optical style.

The orbital HUD names the satellite with its owner's flag (from the
flag-icons package, loaded per country) or an organisation badge, its
operator, class, NORAD and COSPAR ids and TLE epoch with the element lines
on request, altitude, orbital speed and the sub-satellite point. CURRENTLY
OVER names the country and its first-level division, or the sea, and from
8x the most prominent populated place nearby, looked up offline from the
bundled Natural Earth packs (populated places are a new pack). The
satellites layer hides the ridden satellite's own marks, dims the other
orbit rings and ignores scene clicks and Escape while held. Exit restores
the camera's field of view and the satellite's follow camera.

C no longer enters or leaves Cockpit, bridge or orbit: it is left to the
app-wide CCTV shortcut, and Escape or the exit button leave first person.
This commit is contained in:
Denis Groshev committed 2026-11-09 00:24:22 +01:00
1 parent a648c9b665
commit 048293fc2a
67 files changed
+6642 -172

No files matched your search

+31 -30
View File
@@ -39,6 +39,8 @@ function subjectWorldPosition(kind) {
) ?? null
);
}
if (kind === 'satellite')
return this.services.satellitesLayer?.getCockpitPosition?.() ?? null;
const info = this.readVesselInfo();
if (!Number.isFinite(info?.latitude) || !Number.isFinite(info?.longitude))
return null;
@@ -194,9 +196,35 @@ function handleLookKey(event) {
return true;
}
/** Zoom keys in orbit: `+` (or `=`, unshifted) in and `-` (or `_`) out. */
const ORBITAL_ZOOM_KEYS = Object.freeze({ '+': 1, '=': 1, '-': -1, _: -1 });
/**
* The Cockpit's capture-phase keydown: Escape and C, and on a bridge the
* look-around keys.
* In an orbital view: tilt off nadir with the arrows (single 5° steps; Shift
* does not take the bridge's 45° step), return to nadir with Home or 0, and
* step the zoom with + and −. The bridge's focus guards apply.
* @returns {boolean} True when the key was taken.
*/
function handleOrbitalKey(event) {
if (!this.active || this.subjectKind !== 'satellite') return false;
if (event.metaKey || event.ctrlKey || event.altKey) return false;
const step = lookKeyStep(event.key);
const reset = !event.repeat && (event.key === 'Home' || event.key === '0');
const zoom = event.repeat ? 0 : ORBITAL_ZOOM_KEYS[event.key] || 0;
if (!step && !reset && !zoom) return false;
if (event.target?.closest?.(LOOK_KEY_OWNERS)) return false;
if (this.isUtilityDisclosureOpen?.()) return false;
event.preventDefault();
if (step) this.turnOrbitalLook(step);
else if (reset) this.orbitalNadir();
else this.stepOrbitalZoom(zoom);
return true;
}
/**
* The Cockpit's capture-phase keydown: Escape, on a bridge the look-around
* keys and in orbit the tilt and zoom keys. C is left to the app-wide CCTV
* shortcut; entry is by double-click or the entry buttons.
* @param {KeyboardEvent} event
*/
export function onKeyDown(event) {
@@ -247,32 +275,5 @@ export function onKeyDown(event) {
if (event.target?.closest?.('input, textarea, select, [contenteditable]'))
return;
if (handleLookKey.call(this, event)) return;
const key = event.key?.toLowerCase();
if (key === 'c' && !event.metaKey && !event.ctrlKey && !event.altKey) {
if (!this.active) {
const cockpitAttempt = !!(
this.readAircraftInfo() && this.viewer.trackedEntity?.position
);
// With no tracked aircraft, C takes the selected vessel's bridge, but
// only when the bridge can be entered: otherwise C stays the CCTV
// shortcut, as it is with nothing selected.
const bridgeAttempt =
!cockpitAttempt &&
!!this.readVesselInfo() &&
this.isBridgeEntryAllowed();
if (!cockpitAttempt && !bridgeAttempt) return;
if (bridgeAttempt) {
event.preventDefault();
event.stopImmediatePropagation();
this.enter({ kind: 'vessel' });
return;
}
}
event.preventDefault();
event.stopImmediatePropagation();
if (!this.active && !this.isEntryAllowed()) return;
if (this.active) this.exit();
else this.enter({ kind: 'aircraft' });
return;
}
handleOrbitalKey.call(this, event);
}