feat(cockpit): an application can add its own cockpit subjects; the Rail and Transit layers expose the read and hold APIs such a subject needs

GEV itself registers no new subject: with no provider, the Cockpit offers
aircraft, satellites and vessels as before, except that the most recently
selected of them now wins entry.

- services.cockpitSubjects: providers ({kind, layerId, entryFace, priority,
  isEntryAllowed, read, hold, release, hud}) are checked once by the
  coordinator; frames and HUD views on every update; a throwing provider
  ends its view or keeps the last HUD.
- Entry: the most recently selected subject wins, across all kinds.
- Camera: a frame may turn the ground clamp off, anchor exactly on the
  subject and update every frame.
- A generic HUD: kicker, title, cards, always-visible source rows, banner,
  a veil with light streaks (off under reduced motion) and a bottom
  progress line whose close ticks merge.
- Text comes from the provider, already in the page's language
  (./i18n/locale: getLocale, onLocaleChange).
- Rail and Transit read and hold APIs (getSelectedTrain/Vehicle,
  getCockpitSubject, setCockpitSubject, focusTrain/Vehicle); a held
  train's marker, card and route line are hidden, and its feed keeps
  polling while held.
- ./layers/rail/paths is exported.
This commit is contained in:
Denis Groshev committed 2026-11-12 10:50:26 +01:00
1 parent 10a788db78
commit f75179eea0
51 files changed
+4059 -129

No files matched your search

+13 -4
View File
@@ -507,6 +507,15 @@ export function createRendering({ state, services, parts }) {
);
}
/**
* Whether the train's marker shows: its category is on, and no
* first-person view rides it (cockpitSubject.js). A held train keeps its
* playback; only its marker hides.
*/
function markerShown(entry) {
return categoryShown(entry) && entry.key !== state._cockpitSubjectKey;
}
/**
* A train bound to its railway line is drawn on the line, at the arc its
* playback has reached; any other train (or a bound train whose reports do
@@ -536,7 +545,7 @@ export function createRendering({ state, services, parts }) {
undefined,
scratchPosition,
);
entry.marker.show = categoryShown(entry);
entry.marker.show = markerShown(entry);
}
/** While a train is selected, every other train is drawn see-through. */
@@ -554,7 +563,7 @@ export function createRendering({ state, services, parts }) {
entry.marker.width = size;
entry.marker.height = size;
entry.marker.color = tintFor(entry);
entry.marker.show = categoryShown(entry);
entry.marker.show = markerShown(entry);
}
/** Re-tint every train after the selection appeared, moved or cleared. */
@@ -658,7 +667,7 @@ export function createRendering({ state, services, parts }) {
function cullSelected() {
const entry = state._trains.get(state._selectedKey);
if (!entry?.marker) return;
const shown = categoryShown(entry) && facesCamera(entry.marker.position);
const shown = markerShown(entry) && facesCamera(entry.marker.position);
if (entry.marker.show !== shown) entry.marker.show = shown;
parts.routes.cullStops(facesCamera);
}
@@ -697,7 +706,7 @@ export function createRendering({ state, services, parts }) {
function applyCategoryVisibility() {
for (const entry of state._trains.values()) {
if (!entry.marker) continue;
entry.marker.show = categoryShown(entry);
entry.marker.show = markerShown(entry);
schedulePlayback(entry);
}
syncRenderHold();