Merge branch 'main' into denis/cockpit-subject-providers

This commit is contained in:
Denis Groshev committed 2026-11-13 16:38:13 +01:00
commit 546ac22708
217 files changed
+8983 -2470

No files matched your search

+151 -3
View File
@@ -26,6 +26,22 @@ import {
UNSELECTED_TRAIN_ALPHA,
} from './policy.js';
import { placeOnPath } from './pathBinding.js';
import {
fleetModelSet,
loadFleetModelSet,
transitModelSpec,
} from '../../data/fleetModels.js';
import { createFleetModelLayer } from '../../data/fleetModelInstances.js';
/** How often the trains near enough for a model are gathered again (wall ms). */
const MODEL_CANDIDATES_MS = 500;
/** Each category's colour, painted where a train model marks it. */
const CATEGORY_TINTS = Object.fromEntries(
Object.entries(RAIL_CATEGORY_COLORS).map(([category, css]) => [
category,
Cesium.Color.fromCssColorString(css),
]),
);
const SPRITE_PX = 64;
/** Billboard tints; the setter copies, so sharing one instance is safe. */
@@ -508,12 +524,16 @@ export function createRendering({ state, services, parts }) {
}
/**
* Whether the train's marker shows: its category is on, and no
* Whether a train's icon is drawn: shown, not drawn by its model, and no
* first-person view rides it (cockpitSubject.js). A held train keeps its
* playback; only its marker hides.
* playback; only its icon hides.
*/
function markerShown(entry) {
return categoryShown(entry) && entry.key !== state._cockpitSubjectKey;
return (
categoryShown(entry) &&
!entry.modelOwnsVisual &&
entry.key !== state._cockpitSubjectKey
);
}
/**
@@ -683,6 +703,132 @@ export function createRendering({ state, services, parts }) {
function onPreRender() {
advanceMoving();
if (state._enabled && state._selectedKey !== null) cullSelected();
if (state._enabled) updateModels();
}
// Under the DISPLAY rail's 3D toggle, trains near the camera are drawn as
// the project's procedural fleet models, chosen by category and mode,
// painted in their category colour where the model marks it, on the
// aircraft layers' icon-to-model rules (services.fleet). The fleet set
// loads once, up front, when the layer starts.
let fleetModels = null;
/** Items drawn last frame, and the map the next frame's are gathered into. */
let modelItems = new Map();
let spareItems = new Map();
let modelCandidatesAtMs = -Infinity;
/** Load the fleet set and create the layer's models (on enable). */
function startModels() {
const fleet = services.fleet;
if (!fleet?.policy || !state._viewer) return;
loadFleetModelSet({ resolveAsset: fleet.resolveAsset }).catch(() => {
/* models stay off; trains keep their icons */
});
fleetModels ||= createFleetModelLayer({
viewer: state._viewer,
display: fleet.display,
policy: fleet.policy,
resolveAsset: fleet.resolveAsset,
});
}
/** Hand a train's visual back to its icon. */
function handBack(entry) {
if (!entry.modelOwnsVisual) return;
entry.modelOwnsVisual = false;
if (entry.marker) entry.marker.show = markerShown(entry);
}
function modelItem(entry, set) {
let item = modelItems.get(entry.key);
if (!item || item.entry !== entry)
item = {
entry,
pickId: entry.key,
modelOwnsVisual: false,
// Markers are drawn this far above the track; a model stands on it.
groundLiftM: GROUND_FLOOR_LIFT_M,
};
const specKey = `${entry.category}|${entry.railMode}`;
if (item.specKey !== specKey) {
item.specKey = specKey;
item.spec = transitModelSpec(set, {
railCategory: entry.category,
railMode: entry.railMode,
});
item.tint = CATEGORY_TINTS[entry.category] || CATEGORY_TINTS.local;
}
item.position = entry.marker.position;
item.headingDeg = Number.isFinite(entry.courseDeg) ? entry.courseDeg : 0;
item.show = categoryShown(entry);
return item;
}
function updateModels() {
if (!fleetModels) return;
if (!fleetModels.active() || state._trains.size === 0) {
if (modelItems.size) {
for (const item of modelItems.values()) handBack(item.entry);
modelItems.clear();
fleetModels.frame(modelItems, performance.now());
}
return;
}
const set = fleetModelSet();
const now = performance.now();
const next = spareItems;
next.clear();
if (now - modelCandidatesAtMs >= MODEL_CANDIDATES_MS) {
modelCandidatesAtMs = now;
const camera = state._viewer?.camera;
const policy = services.fleet.policy;
const keepM =
services.fleet.display?.()?.models3dMode === 'all'
? policy.keepAllM
: policy.keepM;
const limitSq = keepM * keepM * 1.44;
if (camera?.positionWC)
for (const entry of state._trains.values()) {
if (!entry.marker || !categoryShown(entry)) continue;
if (
Cesium.Cartesian3.distanceSquared(
camera.positionWC,
entry.marker.position,
) > limitSq
)
continue;
next.set(entry.key, modelItem(entry, set));
}
} else {
for (const [key, item] of modelItems)
if (state._trains.get(key) === item.entry && item.entry.marker)
next.set(key, modelItem(item.entry, set));
}
for (const [key, item] of modelItems)
if (next.get(key) !== item) handBack(item.entry);
spareItems = modelItems;
modelItems = next;
fleetModels.frame(modelItems, now);
for (const item of modelItems.values()) {
const entry = item.entry;
if (item.modelOwnsVisual) {
entry.modelOwnsVisual = true;
entry.marker.show = false;
} else handBack(entry);
}
if (fleetModels.settling) governorRequestRender('rail-models');
}
/** Drop every model (layer disabled); `destroy` also removes the collection. */
function stopModels(destroy = false) {
for (const item of modelItems.values()) item.entry.modelOwnsVisual = false;
modelItems.clear();
spareItems.clear();
modelCandidatesAtMs = -Infinity;
if (destroy) {
fleetModels?.destroy();
fleetModels = null;
} else fleetModels?.clear();
}
function advanceMoving() {
@@ -737,5 +883,7 @@ export function createRendering({ state, services, parts }) {
applyCategoryVisibility,
categoryShown,
reset,
startModels,
stopModels,
};
}