This commit is contained in:
Marshal
2026-07-14 13:10:00 +00:00
parent 6d0cf50b4d
commit b5a97d344a
36 changed files with 1101 additions and 355 deletions

View File

@@ -506,12 +506,19 @@ function TrackBed() {
export function TrainCompositionDiagram({
locomotive,
locomotives,
wagons,
freightType,
trainNumber,
totalLengthMeters,
}: {
locomotive?: { code?: string | null; name?: string | null; maxPullWeightTons?: number | null } | null;
/** Full locomotive set (built trains, ≥2). Takes precedence over `locomotive`. */
locomotives?: Array<{
code?: string | null;
name?: string | null;
maxPullWeightTons?: number | null;
}> | null;
wagons: DiagramWagonInput[];
freightType?: string | null;
trainNumber?: string | null;
@@ -519,6 +526,11 @@ export function TrainCompositionDiagram({
}) {
const { ref, width } = useElementSize();
const locos = useMemo(
() => (locomotives?.length ? locomotives : locomotive ? [locomotive] : []),
[locomotives, locomotive],
);
const normalized = useMemo(
() => wagons.map((w) => normalizeWagon(w, freightType)),
[wagons, freightType],
@@ -533,6 +545,11 @@ export function TrainCompositionDiagram({
// ceiling the allocation engine spends from.
const totalTare = normalized.reduce((s, w) => s + w.tareWeightTons, 0);
const grossWeight = totalWeight + totalTare;
// Weakest locomotive caps the set — same rule the allocation engine applies.
const pullLimits = locos
.map((l) => Number(l.maxPullWeightTons))
.filter((v) => Number.isFinite(v) && v > 0);
const pullLimit = pullLimits.length ? Math.min(...pullLimits) : null;
return {
total: normalized.length,
assigned,
@@ -541,22 +558,25 @@ export function TrainCompositionDiagram({
totalTare: Math.round(totalTare * 100) / 100,
grossWeight: Math.round(grossWeight * 100) / 100,
totalCapacity,
pullUtil:
locomotive?.maxPullWeightTons && locomotive.maxPullWeightTons > 0
? Math.min(100, Math.round((grossWeight / locomotive.maxPullWeightTons) * 100))
: null,
pullLimit,
pullUtil: pullLimit
? Math.min(100, Math.round((grossWeight / pullLimit) * 100))
: null,
};
}, [normalized, locomotive]);
}, [normalized, locos]);
// cars-per-row from measured width; locomotive counts as one car
// cars-per-row from measured width; each locomotive counts as one car
const perRow = Math.max(1, Math.floor((width || CAR_WIDTH) / CAR_WIDTH));
const cars = useMemo(
() => [{ kind: "loco" as const }, ...normalized.map((w) => ({ kind: "wagon" as const, w }))],
[normalized],
() => [
...locos.map((l) => ({ kind: "loco" as const, l })),
...normalized.map((w) => ({ kind: "wagon" as const, w })),
],
[locos, normalized],
);
const rows = useMemo(() => chunk(cars, perRow), [cars, perRow]);
if (!locomotive && !wagons.length) return null;
if (!locos.length && !wagons.length) return null;
return (
<Paper
@@ -658,8 +678,8 @@ export function TrainCompositionDiagram({
<Text size="xs" fw={700} c="edr-green.8">
Locomotive load ·{" "}
{stats.totalTare > 0
? `${stats.grossWeight}T of ${locomotive?.maxPullWeightTons}T (${stats.totalWeight}T cargo + ${stats.totalTare}T tare)`
: `${stats.totalWeight}T of ${locomotive?.maxPullWeightTons}T`}
? `${stats.grossWeight}T of ${stats.pullLimit}T (${stats.totalWeight}T cargo + ${stats.totalTare}T tare)`
: `${stats.totalWeight}T of ${stats.pullLimit}T`}
</Text>
</Group>
<Text size="sm" fw={800} c={stats.pullUtil > 95 ? "red.7" : "edr-green.7"}>
@@ -702,13 +722,11 @@ export function TrainCompositionDiagram({
<Group key={carIndex} gap={0} wrap="nowrap" style={{ flexDirection: reversed ? "row-reverse" : "row" }}>
{carIndex > 0 ? <Coupler /> : null}
{car.kind === "loco" ? (
locomotive ? (
<LocomotiveCar
code={locomotive.code ?? "LOCO"}
name={locomotive.name}
maxPullWeightTons={locomotive.maxPullWeightTons}
/>
) : null
<LocomotiveCar
code={car.l.code ?? "LOCO"}
name={car.l.name}
maxPullWeightTons={car.l.maxPullWeightTons}
/>
) : (
<WagonCar wagon={car.w} />
)}