Add route track rendering

This commit is contained in:
dominik-korsa 2025-07-12 15:36:13 +02:00
parent c4eac723ba
commit e78c1ccde5
No known key found for this signature in database
GPG Key ID: 5A24D76EE0A30974
7 changed files with 166 additions and 24 deletions

View File

@ -56,7 +56,8 @@ function StatelessTrackRenderer(props) {
function getTrackPath(object) {
switch (object.type) {
case "StandardTrack":
case "PointTrack":
case "PointTrack":
case "RouteTrack":
const [x1, y1] = object.points.start.toSVGCoords();
const [x2, y2] = object.points.end.toSVGCoords();
const ra = Math.abs(object.r);
@ -84,7 +85,7 @@ function getTrackColor(object, trackColorMode) {
switch (trackColorMode) {
case "standard":
default:
if(object.prefab_name.includes('trans-mat'))
if(object.prefab_name && object.prefab_name.includes('trans-mat'))
return modeDef.options['invisible'][0];
return modeDef.options[modeDef.optionDefault][0];
@ -102,13 +103,15 @@ function getTrackColor(object, trackColorMode) {
default:
return modeDef.options[modeDef.optionDefault][0];
}
case "type":
switch (object.type) {
case "StandardTrack":
return modeDef.options['standard-track'][0];
case "PointTrack":
return modeDef.options['point-track'][0];
case "RouteTrack":
return modeDef.options['route-track'][0];
case "BezierTrack":
return modeDef.options['bezier-track'][0];
default:
@ -133,13 +136,13 @@ function getTrackColor(object, trackColorMode) {
function getTrackDefs(object, trackColorMode) {
if (trackColorMode !== "slope") return null;
if (object.start_slope === object.end_slope) return null;
const [x1, y1] = object.points.start.toSVGCoords();
const [x2, y2] = object.points.end.toSVGCoords();
const gradId = `track-slope-${object.id}`;
const startColor = MiscHelper.getTrackGradient(Constants.trackColorModes['slope'].gradient, Math.abs(object.start_slope));
const endColor = MiscHelper.getTrackGradient(Constants.trackColorModes['slope'].gradient, Math.abs(object.end_slope));
return (
<defs>
<linearGradient

View File

@ -11,6 +11,7 @@ const TrackHoverInfoConsts = {
'StandardTrack': 'Standard track',
'PointTrack': 'Switch track',
'BezierTrack': 'Bezier track',
'RouteTrack': 'Route track',
}
};

View File

@ -144,6 +144,7 @@ const Constants = {
'standard-track': ['#00a', 'Standard track'],
'point-track': ['#0a0', 'Switch track'],
'bezier-track': ['#aa8', 'Bezier track'],
'route-track': ['#a6d', 'Route track'],
}
},
'slope': {

View File

@ -0,0 +1,24 @@
import TrackConnection, {TrackConnectionEnd} from "./track-connection";
export function connectRoutes(scenery) {
const routeTracks = new Map();
Object.values(scenery.objects['routes'] || {}).forEach((route) => {
if (!route.segments[0]) return;
route.segments[0].tracks.forEach((track) => {
routeTracks.set(track.id, track);
});
});
Object.values(scenery.objects['tracks'] || {}).forEach((track) => {
track.connections.forEach((connection) => {
const routeTrack = routeTracks.get(connection.otherTrackId);
if (!routeTrack) return;
const alreadyConnected = routeTrack.connections.some(
(reverseConnection) => reverseConnection.otherTrackId === track.id,
);
if (alreadyConnected) return;
routeTrack.connections.push(new TrackConnection(track.id, TrackConnectionEnd.START));
});
});
}

View File

@ -1,5 +1,7 @@
import SceneryObject from "./scenery-object";
import Vector3 from "./vector3";
import SceneryParserLog from "./scenery-parser-log";
import RouteTrack from "./tracks/route-track";
export default class Route extends SceneryObject {
track_count;
@ -8,8 +10,12 @@ export default class Route extends SceneryObject {
track_offset;
category = "routes";
type = "Route";
points = [];
points;
offsets;
segments = [];
end_center;
end_points;
end_angle_rad;
constructor(prefab_name, pos, rot, track_count, route_name, track_offset, electrified) {
super(route_name, pos, rot);
@ -22,7 +28,11 @@ export default class Route extends SceneryObject {
electrified,
});
this.points = this._getConnectionPoints();
this.offsets = this._getOffsets();
this.end_angle_rad = this.rot.y * Math.PI / 180;
this.end_center = this.pos.clone();
this.end_points = this._getConnectionPoints();
this.points = [...this.end_points];
}
static fromText(text) {
@ -33,7 +43,7 @@ export default class Route extends SceneryObject {
Vector3.fromValuesArray(values, 6), // rot
parseInt(values[9]), // track_count
values[10], // route_name
parseFloat(values[11]) || 0, // track_offset
parseFloat(values[11]) || 0, // center_offset
values[13] === "True" || values[12] === "1" // electrified
);
@ -41,21 +51,84 @@ export default class Route extends SceneryObject {
}
addSegment(length, radius, trackIds) {
this.segments.push({ length, radius, trackIds });
if (trackIds.length !== this.track_count) {
SceneryParserLog.warn('routeInvalidSegment', `Route segment has invalid track count: expected ${this.track_count}, got ${trackIds.length}`);
return;
}
const startAngleRad = this.end_angle_rad;
const start_rot = new Vector3(0, startAngleRad * 180 / Math.PI, 0);
const startPoints = this.end_points;
this.end_points = [];
const radii = [];
const lengths = [];
if (radius === 0) {
this.end_center = this.end_center.add(Vector3.fromAngleY(startAngleRad, length));
this.offsets.forEach(offset => {
radii.push(0);
lengths.push(length);
const end_point = this.end_center
.add(Vector3.fromAngleY(this.end_angle_rad + Math.PI / 2, offset));
this.end_points.push(end_point);
});
} else {
this.end_angle_rad -= length / radius;
const startToCenter = Vector3.fromAngleY(startAngleRad + Math.sign(radius) * Math.PI / 2, Math.abs(radius));
const circleCenter = this.end_center.add(startToCenter.negate());
const centerToEndUnit = Vector3.fromAngleY(this.end_angle_rad + Math.sign(radius) * Math.PI / 2, 1).multiply(Math.sign(radius));
this.end_center = circleCenter.add(centerToEndUnit.multiply(radius));
this.offsets.forEach(offset => {
const trackRadius = radius + offset;
const trackLength = length * trackRadius / radius;
radii.push(trackRadius);
lengths.push(trackLength);
const end_point = circleCenter.add(centerToEndUnit.multiply(trackRadius));
this.end_points.push(end_point);
});
}
this.points.push(...this.end_points);
const tracks = trackIds.map((trackId, index) => {
return new RouteTrack(
trackId,
startPoints[index],
this.end_points[index],
start_rot,
lengths[index],
radii[index],
[], // connections
this, // route
);
});
this.segments.push({ length, radius, trackIds, tracks });
}
// This method is future-proofed for more than 2 tracks
_getOffsets() {
const leftmost_offset = -(this.track_count - 1) * 2 + this.track_offset;
const result = [];
for (let i = 0; i < this.track_count; i++) {
result.push(leftmost_offset + i * 4);
}
return result;
}
_getConnectionPoints() {
if (this.track_count === 1) {
return [this.pos.clone()];
}
return this.offsets.map((offset) => {
const offsetVec = Vector3.fromAngleY(this.end_angle_rad + Math.PI / 2, offset);
return this.pos.add(offsetVec);
});
}
const angle = (this.rot.y + 90) * Math.PI / 180;
const rightTrackOffset = Vector3.fromAngleY(angle, this.track_offset - 2);
const leftTrackOffset = Vector3.fromAngleY(angle, this.track_offset + 2);
return [
this.pos.add(rightTrackOffset),
this.pos.add(leftTrackOffset)
];
applyObject(scenery) {
this.segments.forEach((segment) => {
segment.tracks.forEach((track) => {
scenery.addObject(track);
});
});
}
}

View File

@ -19,6 +19,7 @@ import NEVP from './track-objects/nevp';
import Derailer from './track-objects/derailer';
import SpawnPoint from './track-objects/spawn-point';
import { attachSigns } from './attach-signs';
import {connectRoutes} from "./connect-routes";
/*
TODO: add support for WorldRotation and WorldTranslation
@ -57,6 +58,7 @@ export default class SceneryParser {
});
scenery.applyObjects();
connectRoutes(scenery);
if(Constants.parser.runTracksConnectionTest) tracksConnectionTest(scenery);
if(Constants.parser.resolveElectrification) ElectrificationResolver.resolveScenery(scenery);
@ -180,10 +182,6 @@ export default class SceneryParser {
SceneryParserLog.warn('routeInvalidSegment', `Invalid route segment track IDs: ${fields[3]}`);
return;
}
if (trackIds.length !== route.track_count) {
SceneryParserLog.warn('routeInvalidSegment', `Route segment has invalid track count: expected ${route.track_count}, got ${trackIds.length}`);
return;
}
route.addSegment(
parseInt(fields[1]), // length
parseInt(fields[2]), // radius

View File

@ -0,0 +1,42 @@
import AngleHelper from "../../helpers/angleHelper";
import Track from "./track";
import {ElectrificationStatus} from "../electrification-status";
export default class RouteTrack extends Track {
type = "RouteTrack";
points;
route;
constructor(id, start, end, rot, len, r, connections, route, electrified) {
super(
id, start, rot, len, r, connections,
null, // id_station,
0, // start_slope,
0, // end_slope,
null, // id_isolation
null, // prefab_name
null, // maxspeed,
null, // derailspeed,
);
this.electrificationStatus = electrified ? ElectrificationStatus.ELECTRIFIED : ElectrificationStatus.NON_ELECTRIFIED;
this.points = {
start,
end,
};
this.route = route;
}
getStartAngleXZ() {
return AngleHelper.degToRad(this.rot.y);
}
getEndAngleXZ() {
const startAngle = AngleHelper.degToRad(this.rot.y);
if(this.r === 0) {
return startAngle
}
return startAngle - this.len / this.r;
}
}