feat(cockpit): ride the selected vessel from its bridge

With no aircraft tracked, Cockpit can ride the selected AIS vessel. Entry
needs only Live AIS Vessels and a selected vessel; C, double-click and the
entry buttons (reading BRIDGE with a ship icon) enter it, while voice stays
aircraft-only.

The eye sits on the AIS position at a representative bridge height for the
vessel's type family, looks along true heading and moves along course over
ground. The last fix is dead-reckoned for up to 180 s between the layer's
refreshes, and on rivers and lakes the waterline is read from the rendered
surface around the ship rather than the geoid.

The bridge HUD shows speed and course over ground, a straight heading
tape, window mullions, the AIS destination as a reported voyage and a
heading-up radar of nearby ships (3 to 24 NM). The arrow keys look around,
Home or 0 looks ahead again. The vessel layer hides the ridden ship's
sprite and card, ignores scene clicks while held, and returns the camera
to the vessel's map view on exit.
This commit is contained in:
Denis Groshev committed 2026-11-07 19:52:30 +01:00
1 parent 52187c91d1
commit 72112ecb45
58 files changed
+5313 -137

No files matched your search

+9 -3
View File
@@ -286,7 +286,9 @@ export function createRendering({
const visual = getVisual(record);
const visible = isVisible(visual.surfacePosition, occluder);
if (visual.billboard) {
visual.billboard.show = visible;
// The bridge camera sits on this sprite; showing it would fill the view.
visual.billboard.show =
visible && record.mmsi !== state.cockpitSubjectMmsi;
if (visible && doRotations && scene) {
const rot = screenProjectedRotation(
scene,
@@ -422,7 +424,11 @@ export function createRendering({
const viewer = state.viewer;
const scene = viewer?.scene;
const selected = state.selectedRecord;
const entries = selected
// From the bridge, the selected vessel is the viewer; its card would be
// pinned to the camera position.
const selectedCarded =
selected && selected.mmsi !== state.cockpitSubjectMmsi;
const entries = selectedCarded
? [components.cards.buildSelectedVesselCard(selected)]
: [];
const maxLabels = labelRowLimit();
@@ -458,7 +464,7 @@ export function createRendering({
// Greedy min-separation pass over the priority-ranked cell winners: the
// selected card's anchor seeds the accepted set so ambient cards keep clear.
const accepted = [];
if (selected) {
if (selectedCarded) {
const screen = Cesium.SceneTransforms.worldToWindowCoordinates(
scene,
getVisual(selected).billboard?.position || getVisual(selected).position,