Merge pull request #3 from dominik-korsa/switch-tracks

This commit is contained in:
Igor Zawartka 2025-07-13 23:11:21 +02:00 committed by GitHub
commit a0b792fb6f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 1230 additions and 1127 deletions

5
.gitignore vendored
View File

@ -22,4 +22,7 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
public/sceneries/*
public/sceneries/*
# IDEs
.idea

View File

@ -63,11 +63,11 @@
fill: #111;
}
.map svg g.route g {
.map svg g.route-arrow {
fill: #eee;
}
.map svg g.route text {
.map svg g.route-name text {
fill: #eee;
font-size: 72mm;
}

View File

@ -10,6 +10,7 @@ import NEVPRenderer from './object-renderers/NEVPRenderer';
import DerailerRenderer from './object-renderers/DerailerRenderer';
import ElectrificationStatusPopup from './additional-layer-components/ElectrificationStatusPopup';
import SpawnPointRenderer from './object-renderers/SpawnPointRenderer';
import IsolationEndRenderer from './object-renderers/IsolationEndRenderer';
const ObjectRendererQueue = [
{
@ -23,8 +24,15 @@ const ObjectRendererQueue = [
'pointerEvents': true
},
{
'name': 'isolations-ids',
'name': 'isolations-ends',
'category': 'tracks',
'renderer': IsolationEndRenderer,
'cond': (layers) => layers['isolations-ids']
},
{
'name': 'isolations-ids',
'category': ['tracks', 'switches'],
'types': ['StandardTrack', 'BezierTrack', 'Switch'],
'renderer': IsolationIdRenderer,
'cond': (layers) => layers['isolations-ids']
},
@ -92,4 +100,4 @@ const ObjectRendererQueue = [
}
]
export default ObjectRendererQueue;
export default ObjectRendererQueue;

View File

@ -8,19 +8,26 @@ export default function SceneryLayer(props) {
const { name, category, type, types, cond, renderer: Renderer, additionalComponents: AdditionalComponents } = queueItem;
const { scenery } = useContext(MainContext);
const { layers } = useContext(SettingsContext);
const isVisible = (cond ? cond(layers) : true);
const objects = useMemo(() => {
if (!scenery || !isVisible) return [];
const categoryObjs = scenery.objects[category];
return categoryObjs ? Object.values(categoryObjs) : [];
const getCategoryObjects = (category) => {
const categoryObjs = scenery.objects[category];
if (!categoryObjs) return [];
return Object.values(categoryObjs);
}
// flatMap is not used in the case of a single category to avoid an unnecessary array copy
if (typeof category === 'string') return getCategoryObjects(category);
return category.flatMap(getCategoryObjects);
}, [scenery, category, isVisible]);
if (!scenery || !isVisible) return null;
const pointerEvents = Constants.map.forcePointerEvents || queueItem.pointerEvents || false;
return (
<MemoizedSceneryLayer
name={name}
@ -33,13 +40,13 @@ export default function SceneryLayer(props) {
/>
);
}
const MemoizedSceneryLayer = memo(StatelessSceneryLayer);
function StatelessSceneryLayer({ name, Renderer, objects, type, types, pointerEvents = false, AdditionalComponents = [] }) {
return (
<g
<g
className={`scenery-layer-${name}`}
pointerEvents={pointerEvents ? "all" : "none"}
>
@ -53,4 +60,4 @@ function StatelessSceneryLayer({ name, Renderer, objects, type, types, pointerEv
))}
</g>
);
}
}

View File

@ -0,0 +1,40 @@
import { useMemo } from "react";
import { TrackConnectionEnd } from "../../../model/track-connection";
export default function IsolationEndRenderer(props) {
const { object } = props;
return (<>
<IsolationEndMarker object={object} end={TrackConnectionEnd.START} />
<IsolationEndMarker object={object} end={TrackConnectionEnd.END} />
</>);
}
function IsolationEndMarker(props) {
const { object, end = false } = props;
const isShown = useMemo(() => {
return object.connections.some((conn) => {
const isCorrectEnd = conn.end === end;
const isolationIdChanged = conn.otherTrack.id_isolation !== object.id_isolation;
// Prevent the marker from showing twice for both tracks
const isPreferred = object.id < conn.otherTrackId;
return isCorrectEnd && isolationIdChanged && isPreferred;
});
}, [object, end]);
if (!isShown) return null;
const pos = object.getEndPos(end);
const [x, y] = pos.toSVGCoords();
const angle = object.getAngleXZForEnd(end);
const cos = Math.cos(angle);
const sin = Math.sin(angle);
return <path
className="isolation-end-marker"
d={`M ${x - 1.5 * cos} ${y - 1.5 * sin} L ${x + 1.5 * cos} ${y + 1.5 * sin}`}
/>
}

View File

@ -1,50 +1,20 @@
import { useMemo } from "react";
import SimpleLabelText from "../text/SimpleLabelText";
import { TrackConnectionType } from "../../../model/track-connection";
export default function IsolationIdRenderer(props) {
const { object } = props;
const pos = object.points.start.lerp(object.points.end, 0.5);
const [x, y] = pos.toSVGCoords();
const text = object.id_isolation ?? '';
if (!text) return null;
const showText = (!object.hide_isolation && !!text);
return (<>
{showText && <SimpleLabelText
text={text}
x={x}
y={y}
textProps={{
className: "isolation-id"
}}
/>}
<IsolationEndMarker object={object} isStart={true} />
<IsolationEndMarker object={object} />
</>);
}
function IsolationEndMarker(props) {
const { object, isStart = false } = props;
const isShown = useMemo(() => {
return object.connections.some(conn =>
(isStart ? conn.type === TrackConnectionType.START : conn.type === TrackConnectionType.END) &&
conn.otherTrack.id_isolation !== object.id_isolation
);
}, [object, isStart]);
if (!isShown) return null;
const pos = isStart ? object.points.start : object.points.end;
let pos = (object.type === 'Switch' ? object.isolation_id_pos : object.points.middle) ?? object.pos;
const [x, y] = pos.toSVGCoords();
const angle = isStart ? object.getStartAngleXZ() : object.getEndAngleXZ();
const cos = Math.cos(angle);
const sin = Math.sin(angle);
return <path
className="isolation-end-marker"
d={`M ${x - 1.5 * cos} ${y - 1.5 * sin} L ${x + 1.5 * cos} ${y + 1.5 * sin}`}
/>
return (<SimpleLabelText
text={text}
x={x}
y={y}
textProps={{
className: "isolation-id"
}}
/>);
}

View File

@ -1,27 +1,33 @@
import { ReactSVG } from "react-svg";
import AlwaysUpText from "../text/AlwaysUpText";
import AngleHelper from "../../../helpers/angleHelper";
export default function RouteRenderer(props) {
const { object } = props;
const [x, y] = object.pos.toSVGCoords();
const [startX, startY] = object.pos.toSVGCoords();
const startAngle = object.rot.y;
const [endX, endY] = object.end_center.toSVGCoords();
const endAngle = AngleHelper.radToDeg(object.end_angle_rad);
const offY = object.track_count === 2 ? -22 : -20;
const arrowOffset = - object.track_offset - 37.8;
return (
<g className="route" transform={`translate(${x}, ${y}) rotate(${object.rot.y})`}>
<g transform={`rotate(90) translate(-80, ${arrowOffset})`}>
<ReactSVG
src={`${process.env.PUBLIC_URL}/assets/route.svg`}
wrapper='svg'
beforeInjection={(svg) => {
svg.setAttribute('width', '20mm');
svg.setAttribute('height', '20mm');
}}
/>
</g>
return (<>
<g className="route-arrow" transform={`translate(${endX}, ${endY}) rotate(${endAngle + 90}) translate(-80, ${arrowOffset})`}>
<ReactSVG
src={`${process.env.PUBLIC_URL}/assets/route.svg`}
wrapper='svg'
beforeInjection={(svg) => {
svg.setAttribute('width', '20mm');
svg.setAttribute('height', '20mm');
}}
/>
</g>
<g className="route-name" transform={`translate(${startX}, ${startY}) rotate(${startAngle})`}>
<AlwaysUpText
baseRot={object.rot.y}
baseRot={startAngle}
additionalRot={-90}
offsetX={object.track_offset}
reverseAnchor={true}
@ -32,5 +38,5 @@ export default function RouteRenderer(props) {
text={object.route_name}
/>
</g>
);
</>);
}

View File

@ -22,6 +22,10 @@ const MemoizedTrackRenderer = React.memo(StatelessTrackRenderer);
function StatelessTrackRenderer(props) {
const { object, trackColorMode } = props;
if (object.points.start.distanceSq(object.points.end) < 0.001) {
return null;
}
const onMouseEnter = () => {
setHoveredTrack(object);
};
@ -56,7 +60,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 +89,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 +107,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:
@ -123,7 +130,8 @@ function getTrackColor(object, trackColorMode) {
return `url(#track-slope-${object.id})`;
case "max-speed":
if(!object.maxspeed) {
if (!object.maxspeed) {
if (object.type === 'RouteTrack') return modeDef.options['unknown'][0];
return modeDef.options['derail'][0];
}
return MiscHelper.getTrackGradient(modeDef.gradient, object.maxspeed);
@ -133,13 +141,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

@ -8,22 +8,18 @@ const Constants = {
forcePointerEvents: false
},
parser: {
forceAutoSwitches: false,
logNewAutoSwitches: true,
logSceneryAfterFinished: true,
sceneryInfoVersion: 29,
alwaysShowLogDialog: false,
runTracksConnectionTest: false,
resolveElectrification: true,
skipElectrificationErrorsPropagation: false,
maxRouteConnectionDistance: 0.2,
attachSigns: true,
attachSignsNeedsSameTrack: false,
attachSignsMaxDistanceZ: 1.0,
attachSignsMaxDistanceX: 0.2,
attachSignsGridSize: 10,
logAttachedSigns: false,
connectTracks: true,
},
warnings: {
all: false, // enable all warnings
@ -32,19 +28,17 @@ const Constants = {
unknownObjectType: true,
unknownTrackType: true,
spawnWithoutSpawnInfo: true,
trackAliasAlreadyExists: true,
trackAliasNoTrack: true,
switchUndefinedModel: true,
switchAutoDefFailed: true,
switchInvalidDataFormat: true,
switchNoModel: true,
switchMissingTrackId: true,
switchInvalidInternalConnection: true,
signalBoxUndefinedPrefabName: true,
invalidSceneryInfoVersion: true,
signUndefinedPrefabName: true,
tracksConnectionTest: true,
electrificationNevpNotApplied: false,
electrificationConflict: false,
electrificationMissingRouteTracks: true,
electrificationResolverError: true,
electrificationResolverWarnings: true,
signalElemsUnknownPrefab: true,
@ -53,6 +47,7 @@ const Constants = {
signalElemsUnknownSign: true,
signalElemsUnknownSignText: true,
connectTracksFailed: true,
routeInvalidSegment: true,
},
errors: {
invalidSceneryInfo: true
@ -146,6 +141,7 @@ const Constants = {
'standard-track': ['#00a', 'Standard track'],
'point-track': ['#0a0', 'Switch track'],
'bezier-track': ['#aa8', 'Bezier track'],
'route-track': ['#a6d', 'Route track'],
}
},
'slope': {
@ -170,9 +166,10 @@ const Constants = {
},
options: {
'derail': ['#f22', 'Derail track'],
'unknown': ['#aaa', 'Unknown speed'],
}
}
}
};
export default Constants;
export default Constants;

View File

@ -1,70 +1,83 @@
import TrackConnection, {TrackConnectionEnd} from "./track-connection";
import SceneryParserLog from "./scenery-parser-log";
import TrackConnection, { TrackConnectionType } from "./track-connection";
import Constants from "../helpers/constants";
const connectionThresholdDistanceSq = 0.05;
export function connectTracks(scenery) {
const tracks = scenery.objects['tracks'] || {};
Object.values(tracks).forEach(track => {
connectTrack(scenery, track, false);
connectTrack(scenery, track, true);
_connectRoutes(scenery);
_connectRemainingTracks(scenery);
}
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, TrackConnectionEnd.START));
});
});
}
function connectTrack(scenery, track, isPrev) {
const vNext = isPrev ? track.previd : track.nextid;
if (!vNext) return;
const vNextTrackId = scenery.getTrackIdByAlias(vNext);
const vNextTrack = scenery.getObject('tracks', vNextTrackId);
if (!vNextTrack) {
SceneryParserLog.warn('connectTracks', `Track ${track.id} has non-existing ${isPrev ? 'previous' : 'next'} track: ${isPrev ? track.previd : track.nextid} (${vNextTrackId})`);
function _connectRemainingTracks(scenery) {
Object.values(scenery.objects['tracks'] || {}).forEach((track) => {
track.connections.forEach((connection) => {
_checkConnection(scenery, track, connection);
});
track.connections = track.connections.filter((connection) => connection.otherTrack !== null);
});
}
function _checkConnection(scenery, track, connection) {
if (connection.otherTrack === null) {
const otherTrack = scenery.getObject('tracks', connection.otherTrackId);
if (!otherTrack) {
SceneryParserLog.warn('connectTracksFailed', `Track ${track.id} has a non-existing ${connection.end === TrackConnectionEnd.START ? 'previous' : 'next'} track: ${connection.otherTrackId}`);
return;
}
connection.otherTrack = otherTrack;
}
if (!Constants.parser.runTracksConnectionTest) return;
const reverseConnections = connection.otherTrack.connections.filter(
(otherConnection) => otherConnection.otherTrackId === track.id,
);
if (reverseConnections.length > 1) {
SceneryParserLog.warn(
'tracksConnectionTest',
`Track ${connection.otherTrack.id} has multiple connections to ${track.id}`,
);
}
if (reverseConnections.length === 0) {
SceneryParserLog.warn(
'tracksConnectionTest',
`Track ${track.id} is connected to ${connection.otherTrackId} but there is no reverse connection`,
);
return;
}
const vNextTrackNextId = scenery.getTrackIdByAlias(vNextTrack.nextid);
const vNextTrackPrevId = scenery.getTrackIdByAlias(vNextTrack.previd);
const isConnectedAsNext = vNextTrackPrevId === track.id || track.aliases.includes(vNextTrackPrevId);
const isConnectedAsPrev = vNextTrackNextId === track.id || track.aliases.includes(vNextTrackNextId);
let otherConnectionType = isConnectedAsNext ? TrackConnectionType.START : TrackConnectionType.END;
let addReverseConnection = false;
if (!isConnectedAsNext && !isConnectedAsPrev) {
// switch connection
if(!track.switch) {
SceneryParserLog.warn('connectTracksFailed', `${isPrev ? 'Previous' : 'Next'} track ${vNextTrackId} is not connected back to track ${track.id} (${track.previd} / ${track.nextid}). Cannot connect tracks`);
return;
}
const switchId = track.switch.id;
const isConnectedAsSwitchNext = vNextTrackNextId.slice(0, -1) === switchId;
const isConnectedAsSwitchPrev = vNextTrackPrevId.slice(0, -1) === switchId;
if(!isConnectedAsSwitchNext && !isConnectedAsSwitchPrev) {
SceneryParserLog.warn('connectTracksFailed', `${isPrev ? 'Previous' : 'Next'} track ${vNextTrackId} is not connected back to switch track ${track.id} (${track.previd} / ${track.nextid}). Cannot connect tracks`);
return;
}
otherConnectionType = isConnectedAsSwitchNext ? TrackConnectionType.START : TrackConnectionType.END;
addReverseConnection = true;
const reverseConnection = reverseConnections[0];
const ownPosition = track.getEndPos(connection.end);
const otherPosition = connection.otherTrack.getEndPos(reverseConnection.end);
const distSq = ownPosition.distanceSq(otherPosition);
if (distSq > connectionThresholdDistanceSq) {
SceneryParserLog.warn(
'tracksConnectionTest',
`Track ${track.id} with ${connection.end === TrackConnectionEnd.START ? 'start' : 'end'} position ${ownPosition.toString()} is too far from the track ${connection.otherTrackId} with ${connection.end === TrackConnectionEnd.START ? 'start' : 'end'} position ${otherPosition.toString()}. Distance: ${Math.sqrt(distSq).toFixed(3)}`,
);
}
const mainConnectionType = isPrev ? TrackConnectionType.START : TrackConnectionType.END;
track.connections.push(new TrackConnection(
track,
vNextTrack,
mainConnectionType,
otherConnectionType
));
if(!addReverseConnection) return;
vNextTrack.connections.push(new TrackConnection(
vNextTrack,
track,
otherConnectionType,
mainConnectionType
));
}
}

View File

@ -1,573 +1,96 @@
import Vector3 from "../vector3";
import SwitchPrefab from "../switch-descriptions/switch-prefab";
const DefinedSwitches = {
"Crossing": [
0,
0,
[0,1],
new Vector3(-0.919, 0, -16.59),
new Vector3(0.919, 0, -16.59),
new Vector3(0.919, 0, 16.59),
new Vector3(-0.919, 0, 16.59)
],
"Crossing4.444": [
0,
0,
[0,1],
new Vector3(-1.104, 0, -9.939),
new Vector3(1.104, 0, -9.939),
new Vector3(1.104, 0, 9.939),
new Vector3(-1.104, 0, 9.939)
],
"Rz 60E1-300-1_9 R": [
0,
-300,
[0, 0, 3, 4, 2, 1],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0, 0, 33.231),
new Vector3(1.835, 0, 33.129)
],
"Rz 60E1-300-1_9 L": [
0,
300,
[0, 0, 3, 4, 2, 1],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0, 0, 33.231),
new Vector3(-1.835, 0, 33.129)
],
"Rz 60E1-190-1_9 R": [
0,
-190,
[0, 0, 5, 6, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0, 0, 27.139),
new Vector3(1.835, 0, 27.037)
],
"Rz 60E1-190-1_9 L": [
0,
190,
[0, 0, 5, 6, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0, 0, 27.139),
new Vector3(-1.835, 0, 27.037)
],
"Rz 60E1-500-1_12 R": [
0,
-500,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0, 0, 41.595),
new Vector3(1.727, 0, 41.523)
],
"Rz 60E1-500-1_12 L": [
0,
500,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0, 0, 41.595),
new Vector3(-1.727, 0, 41.523)
],
"Rkpd 60E1-190-1_9": [
0,
0,
[0, 1, 2, 3],
new Vector3(-0.918, 0, -16.557),
new Vector3(0.917, 0, -16.557),
new Vector3(0.917, 0, 16.557),
new Vector3(-0.917, 0, 16.557)
],
"Rkp 60E1-190-1_9 ab": [
0,
0,
[0, 1, 2, 3],
new Vector3(-0.918, 0, -16.557),
new Vector3(0.917, 0, -16.556),
new Vector3(0.917, 0, 16.558),
new Vector3(-0.921, 0, 16.558)
],
"Rkp 60E1-190-1_9 ba": [
0,
0,
[0, 1, 2, 3],
new Vector3(-0.918, 0, -16.557),
new Vector3(0.917, 0, -16.556),
new Vector3(0.917, 0, 16.558),
new Vector3(-0.921, 0, 16.558)
],
"Rz 60E1-265-1_10 R": [
0,
-265,
[0, 0, 5, 6],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0, 0, 31.181),
new Vector3(1.789, 0, 31.094)
],
"Rz 60E1-265-1_10 L": [
0,
265,
[0, 0, 5, 6],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0, 0, 31.181),
new Vector3(-1.789, 0, 31.094)
],
"Rz 60E1-760-1_14 R": [
0,
-760,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0, 0, 54.217),
new Vector3(1.931, 0, 54.148)
],
"Rz 60E1-760-1_14 L": [
0,
760,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0, 0, 54.217),
new Vector3(-1.931, 0, 54.148)
],
"Rz 60E1-1200-1_18.5 R": [
0,
-1200,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0, 0, 64.817),
new Vector3(1.749, 0, 64.77)
],
"Rz 60E1-1200-1_18.5 L": [
0,
1200,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0, 0, 64.817),
new Vector3(-1.75, 0, 64.77)
],
"Rld 60E1-2500_250-1_8.5 R": [
2500,
-250,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0.172, 0, 29.31),
new Vector3(-1.716, 0, 29.244)
],
"Rld 60E1-2500_250-1_8.5 L": [
-2500,
250,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(-0.172, 0, 29.31),
new Vector3(1.716, 0, 29.244)
],
"Rld 60E1-700_300-1_10 R": [
700,
-300,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(-0.616, 0, 29.377),
new Vector3(1.438, 0, 29.339)
],
"Rld 60E1-700_300-1_10 L": [
-700,
300,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0.616, 0, 29.377),
new Vector3(-1.438, 0, 29.339)
],
"Rld 60E1-1800_300-1_9 R": [
1800,
-300,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(-0.307, 0, 33.228),
new Vector3(1.838, 0, 33.163)
],
"Rld 60E1-1800_300-1_9 L": [
-1800,
300,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0.307, 0, 33.228),
new Vector3(-1.838, 0, 33.163)
],
"Rld 60E1-2500_1200-1_22 R": [
2500,
-1200,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(-0.586, 0, 54.51),
new Vector3(1.238, 0, 54.499)
],
"Rld 60E1-2500_1200-1_22 L": [
-2500,
1200,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0.586, 0, 54.51),
new Vector3(-1.238, 0, 54.499)
],
"Rld 60E1-650_450-1_15 R": [
650,
-450,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(-0.691, 0, 29.956),
new Vector3(0.997, 0, 29.945)
],
"Rld 60E1-650_450-1_15 L": [
-650,
450,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0.691, 0, 29.956),
new Vector3(-0.997, 0, 29.945)
],
"Rld 60E1-600_300-1_9 R": [
600,
-300,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(-0.749, 0, 29.918),
new Vector3(1.49, 0, 29.881)
],
"Rld 60E1-600_300-1_9 L": [
-600,
300,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0.749, 0, 29.918),
new Vector3(-1.49, 0, 29.881)
],
"Rz 60E1-205-1_9 R": [
0,
-205,
[0, 0, 5, 6],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0, 0, 28.127),
new Vector3(1.853, 0, 28.025)
],
"Rz 60E1-205-1_9 L": [
0,
205,
[0, 0, 5, 6],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0, 0, 28.127),
new Vector3(-1.853, 0, 28.025),
],
"Rz 60E1-190-1_7.5 R": [
0,
-190,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0, 0, 25.221),
new Vector3(1.667, 0, 25.111)
],
"Rz 60E1-190-1_7.5 L": [
0,
190,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0, 0, 25.221),
new Vector3(-1.667, 0, 25.111)
],
"Rz 60E1-2500-1_26.5 R": [
0,
-2500,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0, 0, 94.306),
new Vector3(1.778, 0, 94.273)
],
"Rz 60E1-2500-1_26.5 L": [
0,
2500,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0, 0, 94.306),
new Vector3(-1.778, 0, 94.273)
],
"Rlds 60E1-600-600-1_18.5": [
-600,
600,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0.92, 0, 33.206),
new Vector3(-0.92, 0, 33.206)
],
"Rlds 60E1-190-190-1_9": [
-190,
190,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(1.184, 0, 21.186),
new Vector3(-1.185, 0, 21.186)
],
"Rlj 60E1-650_190-1_20 R": [
-650,
-190,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0.811, 0, 32.466),
new Vector3(2.77, 0, 32.332)
],
"Rlj 60E1-650_190-1_20 L": [
650,
190,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(-0.811, 0, 32.466),
new Vector3(-2.77, 0, 32.332)
],
"Rlj 60E1-1200_300-1_7 R": [
-1200,
-300,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0.758, 0, 42.632),
new Vector3(3.025, 0, 42.498)
],
"Rlj 60E1-1200_300-1_7 L": [
1200,
300,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(-0.758, 0, 42.632),
new Vector3(-3.025, 0, 42.498)
],
"Rlj 60E1-750_190-1_6 R": [
-750,
-190,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0.66, 0, 31.44),
new Vector3(2.597, 0, 31.306)
],
"Rlj 60E1-750_190-1_6 L": [
750,
190,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(-0.66, 0, 31.44),
new Vector3(-2.597, 0, 31.306)
],
"Rld 60E1-1200_600-1_15 R": [
1200,
-600,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(-0.665, 0, 39.948),
new Vector3(1.33, 0, 39.926)
],
"Rld 60E1-1200_600-1_15 L": [
-1200,
600,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0.665, 0, 39.948),
new Vector3(-1.33, 0, 39.926)
],
"Rld 60E1-1200_900-1_18.5 R": [
1200,
-900,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(-0.984, 0, 48.6),
new Vector3(1.313, 0, 48.589)
],
"Rld 60E1-1200_900-1_18.5 L": [
-1200,
900,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0.984, 0, 48.6),
new Vector3(-1.313, 0, 48.589)
],
"Rlj 60E1-900_300-1_7.5 R": [
-900,
-300,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0.881, 0, 39.81),
new Vector3(2.639, 0, 39.706)
],
"Rlj 60E1-900_300-1_7.5 L": [
900,
300,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(-0.881, 0, 39.81),
new Vector3(-2.639, 0, 39.706)
],
"Rlj 60E1-600_300-1_6 R": [
-600,
-300,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(1.919, 0, 47.949),
new Vector3(3.832, 0, 47.795)
],
"Rlj 60E1-600_300-1_6 L": [
600,
300,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(-1.919, 0, 47.949),
new Vector3(-3.832, 0, 47.795)
],
"Rld 60E1-900_450-1_12 R": [
900,
-450,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(-0.778, 0, 37.425),
new Vector3(1.556, 0, 37.392)
],
"Rld 60E1-900_450-1_12 L": [
-900,
450,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0.778, 0, 37.425),
new Vector3(-1.556, 0, 37.392)
],
"Rld 60E1-700_500-1_14 R": [
700,
-500,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(-0.909, 0, 35.653),
new Vector3(1.272, 0, 35.639)
],
"Rld 60E1-700_500-1_14 L": [
-700,
500,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0.909, 0, 35.653),
new Vector3(-1.272, 0, 35.639)
],
"Rld 60E1-2500_400-1_10.5 R": [
2500,
-400,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(-0.289, 0, 38.008),
new Vector3(1.805, 0, 37.952)
],
"Rld 60E1-2500_400-1_10.5 L": [
-2500,
400,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0.289, 0, 38.008),
new Vector3(-1.805, 0, 37.952)
],
"Rlj 60E1-1800_300-1_7.5 R": [
-1800,
-300,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0.423, 0, 38.998),
new Vector3(2.531, 0, 38.89)
],
"Rlj 60E1-1800_300-1_7.5 L": [
1800,
300,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(-0.423, 0, 38.998),
new Vector3(-2.531, 0, 38.89)
],
"Rld 60E1-1800_600-1_14 R": [
1800,
-600,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(-0.509, 0, 42.798),
new Vector3(1.526, 0, 42.767)
],
"Rld 60E1-1800_600-1_14 L": [
-1800,
600,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0.509, 0, 42.798),
new Vector3(-1.526, 0, 42.767)
],
"Rlj 60E1-1800_450-1_9 R": [
-1800,
-450,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(0.69, 0, 49.84),
new Vector3(2.758, 0, 49.744)
],
"Rlj 60E1-1800_450-1_9 L": [
1800,
450,
[0, 0, 3, 4],
new Vector3(0, 0, 0),
new Vector3(0, 0, 0),
new Vector3(-0.69, 0, 49.84),
new Vector3(-2.758, 0, 49.745)
]
}
"Crossing": SwitchPrefab.crossing(33.2294, 9.0),
"Crossing4.444": SwitchPrefab.crossing(20.0, 4.444),
export default DefinedSwitches;
"Rkp 60E1-190-1_9 ab": SwitchPrefab.slip(33.165, 6.06, 7.461676, 190.0, 9.0, false, true),
"Rkp 60E1-190-1_9 ba": SwitchPrefab.slip(33.165, 6.06, 7.461676, 190.0, 9.0, false, true),
"Rkpd 60E1-190-1_9": SwitchPrefab.slip(33.165, 6.06, 7.461676, 190.0, 9.0, true, true),
"Rld 60E1-1200_600-1_15 L": SwitchPrefab.fork(600.0, -1200.0, 39.95566),
"Rld 60E1-1200_600-1_15 R": SwitchPrefab.fork(-600.0, 1200.0, 39.95566),
"Rld 60E1-1200_900-1_18.5 L": SwitchPrefab.fork(900.0, -1200.0, 48.61317),
"Rld 60E1-1200_900-1_18.5 R": SwitchPrefab.fork(-900.0, 1200.0, 48.61317),
"Rld 60E1-1800_300-1_9 L": SwitchPrefab.fork(300.0, -1800.0, 33.23108),
"Rld 60E1-1800_300-1_9 R": SwitchPrefab.fork(-300.0, 1800.0, 33.23108),
"Rld 60E1-1800_600-1_14 L": SwitchPrefab.fork(600.0, -1800.0, 42.80262),
"Rld 60E1-1800_600-1_14 R": SwitchPrefab.fork(-600.0, 1800.0, 42.80262),
"Rld 60E1-1800_900-1_18.5 L": SwitchPrefab.fork(900.0, -1800.0, 48.61317),
"Rld 60E1-1800_900-1_18.5 R": SwitchPrefab.fork(-900.0, 1800.0, 48.61317),
"Rld 60E1-2500_1200-1_22 L": SwitchPrefab.fork(1200.0, -2500.0, 54.51731),
"Rld 60E1-2500_1200-1_22 R": SwitchPrefab.fork(-1200.0, 2500.0, 54.51731),
"Rld 60E1-2500_250-1_8.5 L": SwitchPrefab.fork(250.0, -2500.0, 29.31069),
"Rld 60E1-2500_250-1_8.5 R": SwitchPrefab.fork(-250.0, 2500.0, 29.31069),
"Rld 60E1-2500_400-1_10.5 L": SwitchPrefab.fork(400.0, -2500.0, 38.00925),
"Rld 60E1-2500_400-1_10.5 R": SwitchPrefab.fork(-400.0, 2500.0, 38.00925),
"Rld 60E1-2500_600-1_14 L": SwitchPrefab.fork(600.0, -2500.0, 42.80262),
"Rld 60E1-2500_600-1_14 R": SwitchPrefab.fork(-600.0, 2500.0, 42.80262),
"Rld 60E1-2500_900-1_17 L": SwitchPrefab.fork(900.0, -2500.0, 52.14928),
"Rld 60E1-2500_900-1_17 R": SwitchPrefab.fork(-900.0, 2500.0, 52.14928),
"Rld 60E1-600_300-1_9 L": SwitchPrefab.fork(300.0, -600.0, 29.92537),
"Rld 60E1-600_300-1_9 R": SwitchPrefab.fork(-300.0, 600.0, 29.92537),
"Rld 60E1-650_450-1_15 L": SwitchPrefab.fork(450.0, -650.0, 29.96674),
"Rld 60E1-650_450-1_15 R": SwitchPrefab.fork(-450.0, 650.0, 29.96674),
"Rld 60E1-700_190-1_7.5 L": SwitchPrefab.fork(190.0, -700.0, 25.22173),
"Rld 60E1-700_190-1_7.5 R": SwitchPrefab.fork(-190.0, 700.0, 25.22173),
"Rld 60E1-700_300-1_10 L": SwitchPrefab.fork(300.0, -700.0, 29.38637),
"Rld 60E1-700_300-1_10 R": SwitchPrefab.fork(-300.0, 700.0, 29.38637),
"Rld 60E1-700_500-1_14 L": SwitchPrefab.fork(500.0, -700.0, 35.66885),
"Rld 60E1-700_500-1_14 R": SwitchPrefab.fork(-500.0, 700.0, 35.66885),
"Rld 60E1-900_300-1_9 L": SwitchPrefab.fork(300.0, -900.0, 33.23108),
"Rld 60E1-900_300-1_9 R": SwitchPrefab.fork(-300.0, 900.0, 33.23108),
"Rld 60E1-900_450-1_12 L": SwitchPrefab.fork(450.0, -900.0, 37.43512),
"Rld 60E1-900_450-1_12 R": SwitchPrefab.fork(-450.0, 900.0, 37.43512),
"Rld 60E1-900_600-1_15 L": SwitchPrefab.fork(600.0, -900.0, 39.95566),
"Rld 60E1-900_600-1_15 R": SwitchPrefab.fork(-600.0, 900.0, 39.95566),
"Rlds 60E1-1000-1000-1_23": SwitchPrefab.fork(1000.0, -1000.0, 43.45773),
"Rlds 60E1-190-190-1_9": SwitchPrefab.fork(190.0, -190.0, 21.23),
"Rlds 60E1-600-600-1_18.5": SwitchPrefab.fork(599.9205, -600.0, 33.22262),
"Rlj 60E1-1200_300-1_7 L": SwitchPrefab.fork(300.0, 1200.0, 42.64069),
"Rlj 60E1-1200_300-1_7 R": SwitchPrefab.fork(-300.0, -1200.0, 42.64069),
"Rlj 60E1-1200_600-1_9 L": SwitchPrefab.fork(600.0, 1200.0, 68.0),
"Rlj 60E1-1200_600-1_9 R": SwitchPrefab.fork(-600.0, -1200.0, 68.0),
"Rlj 60E1-1500_450-1_9 L": SwitchPrefab.fork(450.0, 1500.0, 49.84663),
"Rlj 60E1-1500_450-1_9 R": SwitchPrefab.fork(-450.0, -1500.0, 49.84663),
"Rlj 60E1-1800_300-1_7.5 L": SwitchPrefab.fork(300.0, 1800.0, 39.0),
"Rlj 60E1-1800_300-1_7.5 R": SwitchPrefab.fork(-300.0, -1800.0, 39.0),
"Rlj 60E1-1800_450-1_9 L": SwitchPrefab.fork(450.0, 1800.0, 49.84663),
"Rlj 60E1-1800_450-1_9 R": SwitchPrefab.fork(-450.0, -1800.0, 49.84663),
"Rlj 60E1-1800_600-1_10 L": SwitchPrefab.fork(599.48096, 1800.0, 59.85075),
"Rlj 60E1-1800_600-1_10 R": SwitchPrefab.fork(-599.48096, -1800.0, 59.85075),
"Rlj 60E1-600_300-1_6 L": SwitchPrefab.fork(300.0, 600.0, 48.0),
"Rlj 60E1-600_300-1_6 R": SwitchPrefab.fork(-300.0, -600.0, 48.0),
"Rlj 60E1-650_190-1_20 L": SwitchPrefab.fork(190.0, 650.0, 32.47971),
"Rlj 60E1-650_190-1_20 R": SwitchPrefab.fork(-190.0, -650.0, 32.47971),
"Rlj 60E1-750_190-1_6 L": SwitchPrefab.fork(190.0, 750.0, 31.44976),
"Rlj 60E1-750_190-1_6 R": SwitchPrefab.fork(-190.0, -750.0, 31.44976),
"Rlj 60E1-800_250-1_6.5 L": SwitchPrefab.fork(250.0, 800.0, 38.23661),
"Rlj 60E1-800_250-1_6.5 R": SwitchPrefab.fork(-250.0, -800.0, 38.23661),
"Rlj 60E1-900_300-1_7.5 L": SwitchPrefab.fork(300.0, 900.0, 39.82378),
"Rlj 60E1-900_300-1_7.5 R": SwitchPrefab.fork(-300.0, -900.0, 39.82378),
"Rlj 60E1-900_300-1_9 L": SwitchPrefab.fork(300.0, 900.0, 49.84663),
"Rlj 60E1-900_300-1_9 R": SwitchPrefab.fork(-300.0, -900.0, 49.84663),
"Rlj 60E1-900_450-1_7.5 L": SwitchPrefab.fork(450.0, 900.0, 59.0),
"Rlj 60E1-900_450-1_7.5 R": SwitchPrefab.fork(-450.0, -900.0, 59.0),
"Rz 60E1-1200-1_18.5 L": SwitchPrefab.fork(1200.0, 0.0, 64.81756),
"Rz 60E1-1200-1_18.5 R": SwitchPrefab.fork(-1200.0, 0.0, 64.81756),
"Rz 60E1-190-1_7.5 L": SwitchPrefab.fork(190.0, 0.0, 25.221731),
"Rz 60E1-190-1_7.5 R": SwitchPrefab.fork(-190.0, 0.0, 25.221731),
"Rz 60E1-190-1_9 L": SwitchPrefab.fork(0.0, 190.0, 21.046352, 6.0923653),
"Rz 60E1-190-1_9 R": SwitchPrefab.fork(0.0, -190.0, 21.046352, 6.0923653),
"Rz 60E1-205-1_9 L": SwitchPrefab.fork(205.0, 0.0, 22.707907, 5.42 /* added manually */),
"Rz 60E1-205-1_9 R": SwitchPrefab.fork(-205.0, 0.0, 22.707907, 5.42 /* added manually */),
"Rz 60E1-2500-1_26.5 L": SwitchPrefab.fork(2500.0, 0.0, 94.30607),
"Rz 60E1-2500-1_26.5 R": SwitchPrefab.fork(-2500.0, 0.0, 94.30607),
"Rz 60E1-265-1_10 L": SwitchPrefab.fork(0.0, 265.0, 26.434078, 4.75 /* added manually */),
"Rz 60E1-265-1_10 R": SwitchPrefab.fork(0.0, -265.0, 26.434078, 4.75 /* added manually */),
"Rz 60E1-300-1_9 L": SwitchPrefab.fork(300.0, 0.0, 33.231083),
"Rz 60E1-300-1_9 R": SwitchPrefab.fork(-300.0, 0.0, 33.231083),
"Rz 60E1-500-1_12 L": SwitchPrefab.fork(500.0, 0.0, 41.59458),
"Rz 60E1-500-1_12 R": SwitchPrefab.fork(-500.0, 0.0, 41.59458),
"Rz 60E1-760-1_14 L": SwitchPrefab.fork(760.0, 0.0, 54.21665),
"Rz 60E1-760-1_14 R": SwitchPrefab.fork(-760.0, 0.0, 54.21665),
};
export default DefinedSwitches;

View File

@ -2,16 +2,6 @@ import Constants from "../helpers/constants";
import SceneryParserLog from "./scenery-parser-log";
import {ElectrificationStatus, ElectrificationResolutionStatus} from "./electrification-status";
class RouteTrack {
route;
track;
electrified;
constructor(route, track, electrified) {
Object.assign(this, { route, track, electrified });
}
}
export default class ElectrificationResolver {
static hasWarnings = false;
static propagationQueue = [];
@ -21,21 +11,22 @@ export default class ElectrificationResolver {
ElectrificationResolver.propagationQueue = [];
try {
const routeTracks = this._findRouteTracks(scenery);
const routeTracks = this._findTracksAdjacentToRoutes(scenery);
this._markNEVPTracks(scenery);
routeTracks.forEach(routeTrack => {
routeTracks.forEach(({ track, routeTrack }) => {
this._propagate(
routeTrack.track,
[],
routeTrack.electrified ? ElectrificationStatus.ELECTRIFIED : ElectrificationStatus.NON_ELECTRIFIED
track,
routeTrack.id,
routeTrack.electrificationStatus,
);
});
this._runResolution();
} catch (error) {
scenery.electrificationResolved = ElectrificationResolutionStatus.ERROR;
scenery.electrificationResolved = ElectrificationResolutionStatus.RESOLVING_ERROR;
SceneryParserLog.warn('electrificationResolverError', `Error resolving electrification: ${error.message}`);
console.warn(error);
return;
}
@ -53,48 +44,18 @@ export default class ElectrificationResolver {
SceneryParserLog.warn(type, message);
}
static _checkTrackConnectedToRoute(track, routePoint) {
const mStartDist = track.points.start.mannhattanDistance(routePoint.point);
if(mStartDist <= Constants.parser.maxRouteConnectionDistance) {
const startDistSq = track.points.start.distanceSq(routePoint.point);
if(startDistSq <= Constants.parser.maxRouteConnectionDistance ** 2) {
return true;
}
}
const mEndDist = track.points.end.mannhattanDistance(routePoint.point);
if(mEndDist <= Constants.parser.maxRouteConnectionDistance) {
const endDistSq = track.points.end.distanceSq(routePoint.point);
if(endDistSq <= Constants.parser.maxRouteConnectionDistance ** 2) {
return true;
}
}
return false;
}
static _findRouteTracks(scenery) {
const routePoints = Object.values(scenery.objects.routes || {})
.map(route => route.points.map(point => ({ route, point }))).flat();
const results = [];
Object.values(scenery.objects.tracks || {}).forEach(track => {
routePoints.forEach(routePoint => {
if (this._checkTrackConnectedToRoute(track, routePoint)) {
results.push(new RouteTrack(routePoint.route, track, routePoint.route.electrified));
}
static _findTracksAdjacentToRoutes(scenery) {
return Object.values(scenery.objects.routes || {}).flatMap(route => {
if (route.segments.length === 0) return;
return route.segments[0].tracks.flatMap((routeTrack) => {
return routeTrack.connections
.map((connection) => {
if (connection.otherTrack.type === 'RouteTrack') return null;
return { track: connection.otherTrack, routeTrack };
})
.filter((adjacent) => adjacent !== null);
});
});
if(results.length < routePoints.length) {
ElectrificationResolver._passWarn(
'electrificationMissingRouteTracks',
`While resolving electrification, found ${results.length} tracks connected to routes, but expected ${routePoints.length}`
);
}
return results;
}
static _markNEVPTracks(scenery) {
@ -103,7 +64,7 @@ export default class ElectrificationResolver {
if(!trackObject.track) {
ElectrificationResolver._passWarn(
'electrificationNevpNotApplied',
'electrificationNevpNotApplied',
`NEVP object ${trackObject.name} (${trackObject.id}) at track ${trackObject.track_id} has not been applied. Electrification resolution may fail`
);
return;
@ -115,16 +76,16 @@ export default class ElectrificationResolver {
static _runResolution() {
while(ElectrificationResolver.propagationQueue.length > 0) {
const { track, skipTrackIds, status } = ElectrificationResolver.propagationQueue.shift();
ElectrificationResolver._resolveTrack(track, skipTrackIds, status);
const { track, skipTrackId, status } = ElectrificationResolver.propagationQueue.pop();
ElectrificationResolver._resolveTrack(track, skipTrackId, status);
}
}
static _propagate(track, skipTrackIds, status) {
ElectrificationResolver.propagationQueue.push({ track, skipTrackIds, status });
static _propagate(track, skipTrackId, status) {
ElectrificationResolver.propagationQueue.push({ track, skipTrackId, status });
}
static _resolveTrack(track, skipTrackIds, status) {
static _resolveTrack(track, skipTrackId, status) {
if (track.electrificationStatus === status) return; // already set
if (Constants.parser.skipElectrificationErrorsPropagation && status === ElectrificationStatus.CONFLICT) {
return;
@ -137,38 +98,24 @@ export default class ElectrificationResolver {
const fsne = status === ElectrificationStatus.NON_ELECTRIFIED;
const tse = track.electrificationStatus === ElectrificationStatus.ELECTRIFIED;
const tsne = track.electrificationStatus === ElectrificationStatus.NON_ELECTRIFIED;
track.electrificationStatus = status;
if (track.hasNEVP) {
track.electrificationStatus = ElectrificationStatus.NON_ELECTRIFIED;
if(!fse) return;
} else if ((tse && fsne) || (tsne && fse)) {
track.electrificationStatus = ElectrificationStatus.CONFLICT;
ElectrificationResolver._passWarn(
'electrificationConflict',
'electrificationConflict',
`Track ${track.id} has conflicting electrification status (=>${status})`
);
return;
}
const propagation = track.connections.map(conn => conn.otherTrack);
const nextSkipIds = [track.id];
if (track.switch) {
nextSkipIds.push(track.switch.trackA.id, track.switch.trackB.id);
}
propagation.forEach(nextTrack => {
if (skipTrackIds.includes(nextTrack.id)) return;
this._propagate(nextTrack, nextSkipIds, track.electrificationStatus);
track.connections.forEach(connection => {
if (skipTrackId === connection.otherTrackId) return;
this._propagate(connection.otherTrack, track.id, track.electrificationStatus);
});
// Additional propagation for double switches
if (!track.switch || track.switch.def?.[2]?.length <= 2 || track.hasNEVP) return;
const otherSwitchTrack = track.switch.trackA === track ? track.switch.trackB : track.switch.trackA;
this._propagate(otherSwitchTrack, [track.id, ...propagation.map(track => track.id)], track.electrificationStatus);
}
};
};

View File

@ -1,5 +1,9 @@
import SceneryObject from "./scenery-object";
import Vector3 from "./vector3";
import SceneryParserLog from "./scenery-parser-log";
import RouteTrack from "./tracks/route-track";
import TrackConnection, {TrackConnectionEnd} from "./track-connection";
import AngleHelper from "../helpers/angleHelper";
export default class Route extends SceneryObject {
track_count;
@ -8,7 +12,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);
@ -21,10 +30,14 @@ 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) {
static fromText(text) {
const values = text.split(";");
const route = new Route(
values[2], // prefab_name
@ -32,25 +45,104 @@ 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
);
return route;
}
_getConnectionPoints() {
if (this.track_count === 1) {
return [this.pos.clone()];
addSegment(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 startRot = new Vector3(0, AngleHelper.radToDeg(startAngleRad), 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);
});
}
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);
this.points.push(...this.end_points);
return [
this.pos.add(rightTrackOffset),
this.pos.add(leftTrackOffset)
];
const tracks = trackIds.map((trackId, index) => {
return new RouteTrack(
trackId,
startPoints[index],
this.end_points[index],
startRot,
lengths[index],
radii[index],
[], // connections
this.electrified,
this, // route
);
});
if (this.segments.length >= 2) {
const prevSegment = this.segments[this.segments.length - 2];
const newSegment = this.segments[this.segments.length - 1];
newSegment.tracks.forEach((track, index) => {
const prevTrack = prevSegment.tracks[index];
track.connections.push(new TrackConnection(prevTrack, TrackConnectionEnd.START));
prevTrack.connections.push(new TrackConnection(track, TrackConnectionEnd.END));
});
}
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() {
return this.offsets.map((offset) => {
const offsetVec = Vector3.fromAngleY(this.end_angle_rad + Math.PI / 2, offset);
return this.pos.add(offsetVec);
});
}
applyObject(scenery) {
this.segments.forEach((segment) => {
segment.tracks.forEach((track) => {
scenery.addObject(track);
});
});
}
}

View File

@ -1,5 +1,4 @@
import Scenery from './scenery';
import { tracksConnectionTest } from './tracks-connection-test';
import Switch from './switch';
import StandardTrack from './tracks/standard-track';
import BezierTrack from './tracks/bezier-track';
@ -19,7 +18,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 { connectTracks } from './connect-tracks';
import {connectTracks} from "./connect-tracks";
/*
TODO: add support for WorldRotation and WorldTranslation
@ -30,23 +29,36 @@ export default class SceneryParser {
const lines = scText.split("\n").map(line => line.trim());
const scenery = new Scenery();
let currentRoute = null;
lines.forEach((line, index) => {
if(index === 0) {
const sceneryInfo = SceneryParser._parseSceneryInfo(line);
if(sceneryInfo) scenery.addObject(sceneryInfo);
return;
}
if(!line?.length) return;
if(!line?.length) return;
const object = SceneryParser._parseObject(line);
if(!object) return; // skip null objects
scenery.addObject(object);
if (currentRoute !== null) {
const foundRouteEnd = SceneryParser._parseRouteLine(line, currentRoute);
if (foundRouteEnd) {
scenery.addObject(currentRoute);
currentRoute = null;
}
} else {
const object = SceneryParser._parseObject(line);
if(!object) return; // skip null objects
if (object.type === 'Route') {
currentRoute = object;
} else {
scenery.addObject(object);
}
}
});
scenery.applyObjects();
connectTracks(scenery);
if(Constants.parser.runTracksConnectionTest) tracksConnectionTest(scenery);
if(Constants.parser.connectTracks) connectTracks(scenery);
if(Constants.parser.resolveElectrification) ElectrificationResolver.resolveScenery(scenery);
if(Constants.parser.attachSigns) attachSigns(scenery);
if(Constants.parser.logSceneryAfterFinished) console.log(scenery);
@ -66,12 +78,13 @@ export default class SceneryParser {
}
return SceneryInfo.fromText(text);
}
}
static _parseObject(text) {
const type = text.split(";", 2)[0];
if(type.indexOf("Forest") !== -1 || type.indexOf("Empty") !== -1) {
if (type === 'EndRoute' || type.indexOf("Forest") !== -1 || type.indexOf("Empty") !== -1) {
SceneryParserLog.warn('unknownObjectType', `Unexpected entry of type ${type} without a preceding Route object`);
return null;
}
@ -90,7 +103,6 @@ export default class SceneryParser {
return CameraHome.fromText(text);
case 'MainCamera':
return MainCamera.fromText(text);
case 'EndRoute':
case 'MiscGroup':
case 'EndMiscGroup':
case 'Wires':
@ -108,7 +120,7 @@ export default class SceneryParser {
default:
SceneryParserLog.warn('unknownObjectType', `Unknown object type: ${type}`);
return null;
};
}
}
static _parseTrack(text) {
@ -149,4 +161,31 @@ export default class SceneryParser {
return Misc.fromText(Scenery.nextMiscId++, text);
}
}
}
// Returns true if EndRoute was found, false otherwise
static _parseRouteLine(text, route) {
if (text === 'EndRoute') return true;
const fields = text.split(';');
if (fields.length < 4) {
SceneryParserLog.warn('routeInvalidSegment', `Invalid route segment: "${text}", not enough fields`);
return;
}
const trackIds = fields[3]
.split(',')
.map(segment => {
const id = segment.split(':')[0].trim();
if (id === '') return null;
return id;
});
if (trackIds.some(id => id === null || isNaN(id))) {
SceneryParserLog.warn('routeInvalidSegment', `Invalid route segment track IDs: ${fields[3]}`);
return;
}
route.addSegment(
parseInt(fields[1]), // length
parseInt(fields[2]), // radius
trackIds,
);
return false;
}
}

View File

@ -1,5 +1,4 @@
import { ElectrificationResolutionStatus } from './electrification-status.js';
import SceneryParserLog from './scenery-parser-log.js';
export default class Scenery
{
@ -7,7 +6,6 @@ export default class Scenery
signalBoxes = [];
spawnPoints = [];
bounds = { minX: Infinity, minZ: Infinity, maxX: -Infinity, maxZ: -Infinity };
trackAliases = {};
electrificationResolved = ElectrificationResolutionStatus.NOT_RESOLVED;
static nextMiscId = 1;
@ -36,14 +34,30 @@ export default class Scenery
this._updateBounds(object.getRenderBounds());
}
}
applyObjects() {
const categories_order = [
'switches', 'routes', // switches and routes create tracks
'tracks',
'track-objects', // track objects might refer to tracks
];
categories_order.forEach((category) => this._applyCategory(category));
// Remaining categories
Object.keys(this.objects).forEach(category => {
Object.values(this.objects[category]).forEach(object => {
if(object.applyObject) {
object.applyObject(this);
}
});
if (categories_order.includes(category)) return;
this._applyCategory(category);
});
}
_applyCategory(category) {
if(!this.objects[category]) return;
Object.values(this.objects[category]).forEach(object => {
if (object.applyObject) {
object.applyObject(this);
}
});
}
@ -52,26 +66,6 @@ export default class Scenery
return this.objects[category][id] ?? null;
}
addTrackAlias(id, alias) {
if(this.trackAliases[alias]) {
SceneryParserLog.warn('trackAliasAlreadyExists', `Track alias "${alias}" already exists for track "${this.trackAliases[alias]}". Cannot add alias for "${id}"`);
return;
}
const track = this.getObject('tracks', id);
if(!track) {
SceneryParserLog.warn('trackAliasNoTrack', `Cannot add alias "${alias}" for non-existing track "${id}"`);
return;
}
track.aliases.push(alias);
this.trackAliases[alias] = id;
}
getTrackIdByAlias(idOrAlias) {
return this.trackAliases[idOrAlias] || idOrAlias;
}
_updateBounds(objBounds) {
if(!objBounds || !Array.isArray(objBounds)) return;
@ -80,7 +74,6 @@ export default class Scenery
if(point.z < this.bounds.minZ) this.bounds.minZ = point.z;
if(point.x > this.bounds.maxX) this.bounds.maxX = point.x;
if(point.z > this.bounds.maxZ) this.bounds.maxZ = point.z;
}
);
});
}
}
}

View File

@ -0,0 +1,32 @@
export const SwitchTrackConnectionType = {
INTERNAL: Symbol('SwitchTrackConnectionType.INTERNAL'),
EXTERNAL: Symbol('SwitchTrackConnectionType.EXTERNAL'),
};
export default class SwitchPrefabTrack {
startPos;
endPos;
radius;
dataIndex;
connections;
constructor(startPos, endPos, radius, dataIndex, connections = []) {
Object.assign(this, {
startPos,
endPos,
radius,
dataIndex,
connections,
});
}
static point(pos, dataIndex, connections = []) {
return new SwitchPrefabTrack(
pos,
pos,
0,
dataIndex,
connections,
);
}
}

View File

@ -0,0 +1,467 @@
import Vector3 from "../vector3";
import SwitchPrefabTrack, {SwitchTrackConnectionType} from "./switch-prefab-track";
import SceneryParserLog from "../scenery-parser-log";
import {TrackConnectionEnd} from "../track-connection";
export default class SwitchPrefab {
tracks = {};
isolation_id_offset;
constructor(tracks, isolation_id_offset) {
Object.assign(this, {
tracks,
isolation_id_offset,
});
this._verifyConnections();
}
_verifyConnections() {
// TODO: Should SceneryParserLog be used here?
Object.entries(this.tracks).forEach(([internalId, track]) => {
track.connections.forEach((connection) => {
if (connection.type === SwitchTrackConnectionType.INTERNAL) {
const otherTrack = this.tracks[connection.internalId];
if (!otherTrack) {
SceneryParserLog.warn(
'switchInvalidInternalConnection',
`In a switch prefab the referenced internal track ${connection.internalId} in a connection of track ${internalId} not found`,
);
return;
}
const reverseConnection = otherTrack.connections.find(
(otherConnection) => otherConnection.type === SwitchTrackConnectionType.INTERNAL && otherConnection.internalId === internalId,
);
if (!reverseConnection) {
SceneryParserLog.warn(
'switchInvalidInternalConnection',
`In a switch prefab the internal track ${internalId} is connected to ${connection.internalId} but there is no reverse connection`,
);
return;
}
if (reverseConnection.end !== connection.otherEnd) {
SceneryParserLog.warn(
'switchInvalidInternalConnection',
`In a switch prefab the connection between tracks internal tracks ${internalId} and ${connection.internalId} has mismatched ends`,
);
}
}
});
});
}
static fork(radiusA, radiusB, curveLength, addedLength = 0.0) {
const tracks = {
start: new SwitchPrefabTrack(
Vector3.zero(),
Vector3.zero(),
radiusA,
0,
[
{
type: SwitchTrackConnectionType.EXTERNAL,
end: TrackConnectionEnd.START,
},
{
type: SwitchTrackConnectionType.INTERNAL,
end: TrackConnectionEnd.END,
otherEnd: TrackConnectionEnd.START,
internalId: 'curve_a',
},
{
type: SwitchTrackConnectionType.INTERNAL,
end: TrackConnectionEnd.END,
otherEnd: TrackConnectionEnd.START,
internalId: 'curve_b',
},
],
),
...SwitchPrefab._createForkSwitchTracks(
radiusA,
curveLength,
addedLength,
'a',
),
...SwitchPrefab._createForkSwitchTracks(
radiusB,
curveLength,
addedLength,
'b',
),
};
const midpointA = this._calculateCurveEnd(Vector3.zero(), 0, radiusA, curveLength / 2).endPos;
const midpointB = this._calculateCurveEnd(Vector3.zero(), 0, radiusB, curveLength / 2).endPos;
const isolationIdOffset = midpointA.lerp(midpointB, 0.5);
return new SwitchPrefab(tracks, isolationIdOffset);
}
static _calculateCurveEnd(startPos, startAngle, radius, curveLength) {
if (radius === 0) {
return { endPos: Vector3.fromAngleY(startAngle, curveLength), endAngle: startAngle };
}
const centerToStart = Vector3.fromAngleY(startAngle + Math.sign(radius) * Math.PI / 2, Math.abs(radius));
const circleCenter = startPos.add(centerToStart.negate());
const endAngle = startAngle -curveLength / radius;
const centerToEnd = Vector3.fromAngleY(endAngle + Math.sign(radius) * Math.PI / 2, Math.abs(radius));
const endPos = circleCenter.add(centerToEnd);
return { endPos, endAngle };
}
static _createForkSwitchTracks(radius, curveLength, addedLength, side) {
const { endPos: curveEnd, endAngle } = SwitchPrefab._calculateCurveEnd(Vector3.zero(), 0, radius, curveLength);
const tracks = {
[`curve_${side}`]: new SwitchPrefabTrack(
Vector3.zero(),
curveEnd,
radius,
side === 'a' ? 1 : 2,
[
{
type: SwitchTrackConnectionType.INTERNAL,
end: TrackConnectionEnd.START,
otherEnd: TrackConnectionEnd.END,
internalId: 'start',
},
{
type: SwitchTrackConnectionType.INTERNAL,
end: TrackConnectionEnd.END,
otherEnd: TrackConnectionEnd.START,
internalId: addedLength > 0 ? `added_${side}` : `end_${side}`,
},
],
),
};
let end = curveEnd;
if (addedLength > 0) {
end = end.add(Vector3.fromAngleY(endAngle, addedLength));
tracks[`added_${side}`] = new SwitchPrefabTrack(
curveEnd,
end,
0,
side === 'a' ? 3 : 4,
[
{
type: SwitchTrackConnectionType.INTERNAL,
end: TrackConnectionEnd.START,
otherEnd: TrackConnectionEnd.END,
internalId: `curve_${side}`,
},
{
type: SwitchTrackConnectionType.INTERNAL,
end: TrackConnectionEnd.END,
otherEnd: TrackConnectionEnd.START,
internalId: `end_${side}`,
},
],
);
}
tracks[`end_${side}`] = SwitchPrefabTrack.point(
end,
addedLength > 0 ?
(side === 'a' ? 5 : 6) : // A fork switch with added length has the track A before B
(side === 'a' ? 4 : 3), // and without any added length, track B before A,
[
{
type: SwitchTrackConnectionType.INTERNAL,
end: TrackConnectionEnd.START,
otherEnd: TrackConnectionEnd.END,
internalId: addedLength > 0 ? `added_${side}` : `curve_${side}`,
},
{
type: SwitchTrackConnectionType.EXTERNAL,
end: TrackConnectionEnd.END,
},
]
);
return tracks;
}
static crossing(length, tangentInv) {
const vectorA = Vector3.fromAngleY(Math.atan(1 / tangentInv / 2), length / 2);
const vectorB = Vector3.fromAngleY(-Math.atan(1 / tangentInv / 2), length / 2);
const tracks = {
a: new SwitchPrefabTrack(
vectorA.negate(),
vectorA,
0,
0,
[
{
type: SwitchTrackConnectionType.EXTERNAL,
end: TrackConnectionEnd.START,
},
{
type: SwitchTrackConnectionType.EXTERNAL,
end: TrackConnectionEnd.END,
},
],
),
b: new SwitchPrefabTrack(
vectorB.negate(),
vectorB,
0,
1,
[
{
type: SwitchTrackConnectionType.EXTERNAL,
end: TrackConnectionEnd.START,
},
{
type: SwitchTrackConnectionType.EXTERNAL,
end: TrackConnectionEnd.END,
},
],
),
};
return new SwitchPrefab(tracks, Vector3.zero());
}
static slip(totalLength, outerLength, transitionLength, radius, tangentInv, leftSlipEnabled, rightSlipEnabled) {
const aAngle = Math.atan(1 / tangentInv / 2);
const bAngle = -aAngle;
const unitVectorA = Vector3.fromAngleY(aAngle);
const unitVectorB = Vector3.fromAngleY(bAngle);
const outerStart = totalLength / 2;
const transitionStart = outerStart - outerLength;
const innerStart = transitionStart - transitionLength;
const makeSlipConfig = (radiusSign, unitVector, startAngle) => {
const startPos = unitVector.multiply(transitionStart);
const slipRadius = radiusSign * radius;
const { endPos } = SwitchPrefab._calculateCurveEnd(startPos, startAngle, slipRadius, transitionLength);
return {
startPos,
slipRadius,
endPos,
};
};
const slipPoints = {
a_enter: makeSlipConfig(1, unitVectorA.negate(), aAngle),
a_exit: makeSlipConfig(1, unitVectorA, aAngle + Math.PI),
b_enter: makeSlipConfig(-1, unitVectorB.negate(), bAngle),
b_exit: makeSlipConfig(-1, unitVectorB, bAngle + Math.PI),
};
let dataIndex = 0;
const outerTrack = (unitVector, name) => [
`outer_${name}`,
new SwitchPrefabTrack(
unitVector.multiply(outerStart),
unitVector.multiply(transitionStart),
0,
dataIndex++,
[
{
type: SwitchTrackConnectionType.EXTERNAL,
end: TrackConnectionEnd.START,
},
{
type: SwitchTrackConnectionType.INTERNAL,
end: TrackConnectionEnd.END,
otherEnd: TrackConnectionEnd.START,
internalId: `transition_${name}`,
},
],
),
];
const transitionTrack = (unitVector, path, end) => [
`transition_${path}_${end}`,
new SwitchPrefabTrack(
unitVector.multiply(transitionStart),
unitVector.multiply(innerStart),
0,
dataIndex++,
[
{
type: SwitchTrackConnectionType.INTERNAL,
end: TrackConnectionEnd.START,
otherEnd: TrackConnectionEnd.END,
internalId: `outer_${path}_${end}`,
},
{
type: SwitchTrackConnectionType.INTERNAL,
end: TrackConnectionEnd.END,
otherEnd: end === 'enter' ? TrackConnectionEnd.START : TrackConnectionEnd.END,
internalId: `crossing_${path}`,
},
],
),
];
const slipTransitionTrack = (path, side, end) => {
const { startPos, slipRadius, endPos } = slipPoints[`${path}_${end}`];
return [
`slip_transition_${path}_${end}`,
new SwitchPrefabTrack(
startPos,
endPos,
slipRadius,
dataIndex++,
[
{
type: SwitchTrackConnectionType.INTERNAL,
end: TrackConnectionEnd.START,
otherEnd: TrackConnectionEnd.END,
internalId: `outer_${path}_${end}`,
},
{
type: SwitchTrackConnectionType.INTERNAL,
end: TrackConnectionEnd.END,
otherEnd: end === 'enter' ? TrackConnectionEnd.START : TrackConnectionEnd.END,
internalId: `side_${side}`,
},
],
),
];
};
const crossingTrack = (unitVector, path) => [
`crossing_${path}`,
new SwitchPrefabTrack(
unitVector.multiply(-innerStart),
unitVector.multiply(innerStart),
0,
dataIndex++,
[
{
type: SwitchTrackConnectionType.INTERNAL,
end: TrackConnectionEnd.START,
otherEnd: TrackConnectionEnd.END,
internalId: `transition_${path}_enter`,
},
{
type: SwitchTrackConnectionType.INTERNAL,
end: TrackConnectionEnd.END,
otherEnd: TrackConnectionEnd.END,
internalId: `transition_${path}_exit`,
},
],
),
];
const sideTrack = (unitVectorEnter, unitVectorExit, enterPath, exitPath, side, slipEnabled) => {
if (slipEnabled) {
const enterConfig = slipPoints[`${enterPath}_enter`];
const exitConfig = slipPoints[`${exitPath}_exit`];
return [
`side_${side}`,
new SwitchPrefabTrack(
enterConfig.endPos,
exitConfig.endPos,
enterConfig.slipRadius,
dataIndex++,
[
{
type: SwitchTrackConnectionType.INTERNAL,
end: TrackConnectionEnd.START,
otherEnd: TrackConnectionEnd.END,
internalId: `slip_transition_${enterPath}_enter`,
},
{
type: SwitchTrackConnectionType.INTERNAL,
end: TrackConnectionEnd.END,
otherEnd: TrackConnectionEnd.END,
internalId: `slip_transition_${exitPath}_exit`,
},
]
),
];
} else {
return [
`side_${side}`,
new SwitchPrefabTrack(
unitVectorEnter.multiply(innerStart),
unitVectorExit.multiply(innerStart),
0,
dataIndex++,
[
{
type: SwitchTrackConnectionType.INTERNAL,
end: TrackConnectionEnd.START,
otherEnd: TrackConnectionEnd.END,
internalId: `transition_${enterPath}_enter`,
},
{
type: SwitchTrackConnectionType.INTERNAL,
end: TrackConnectionEnd.END,
otherEnd: TrackConnectionEnd.END,
internalId: `transition_${exitPath}_exit`,
},
]
),
];
}
};
// the order is important, each function call increments dataIndex
const trackList = [
outerTrack(unitVectorA.negate(), 'a_enter'),
outerTrack(unitVectorB.negate(), 'b_enter'),
outerTrack(unitVectorA, 'a_exit'),
outerTrack(unitVectorB, 'b_exit'),
transitionTrack(unitVectorA.negate(), 'a', 'enter'),
transitionTrack(unitVectorB.negate(), 'b', 'enter'),
];
if (leftSlipEnabled) {
trackList.push(slipTransitionTrack('a', 'left', 'enter'));
}
if (rightSlipEnabled) {
trackList.push(slipTransitionTrack('b', 'right', 'enter'));
}
trackList.push(
crossingTrack(unitVectorA, 'a'),
crossingTrack(unitVectorB, 'b'),
sideTrack(unitVectorA.negate(), unitVectorB, 'a', 'b', 'left', leftSlipEnabled),
sideTrack(unitVectorB.negate(), unitVectorA, 'b', 'a', 'right', rightSlipEnabled),
transitionTrack(unitVectorA, 'a', 'exit'),
transitionTrack(unitVectorB, 'b', 'exit'),
);
if (rightSlipEnabled) {
trackList.push(slipTransitionTrack('a', 'right', 'exit'));
}
if (leftSlipEnabled) {
trackList.push(slipTransitionTrack('b', 'left', 'exit'));
}
const tracks = Object.fromEntries(trackList);
const addSideConnection = (side, enter_path, exit_path, slipEnabled) => {
if (slipEnabled) {
tracks[`outer_${enter_path}_enter`].connections.push({
type: SwitchTrackConnectionType.INTERNAL,
end: TrackConnectionEnd.END,
otherEnd: TrackConnectionEnd.START,
internalId: `slip_transition_${enter_path}_enter`,
});
tracks[`outer_${exit_path}_exit`].connections.push({
type: SwitchTrackConnectionType.INTERNAL,
end: TrackConnectionEnd.END,
otherEnd: TrackConnectionEnd.START,
internalId: `slip_transition_${exit_path}_exit`,
});
} else {
tracks[`transition_${enter_path}_enter`].connections.push({
type: SwitchTrackConnectionType.INTERNAL,
end: TrackConnectionEnd.END,
otherEnd: TrackConnectionEnd.START,
internalId: `side_${side}`,
});
tracks[`transition_${exit_path}_exit`].connections.push({
type: SwitchTrackConnectionType.INTERNAL,
end: TrackConnectionEnd.END,
otherEnd: TrackConnectionEnd.END,
internalId: `side_${side}`,
});
}
};
addSideConnection('left', 'a', 'b', leftSlipEnabled);
addSideConnection('right', 'b', 'a', rightSlipEnabled);
return new SwitchPrefab(tracks, Vector3.zero());
}
}

View File

@ -3,7 +3,8 @@ import SceneryObject from "./scenery-object.js";
import SceneryParserLog from "./scenery-parser-log.js";
import Vector3 from "./vector3.js";
import DefinedSwitches from "./defs/defined-switches.js";
import Constants from "../helpers/constants.js";
import {SwitchTrackConnectionType} from "./switch-descriptions/switch-prefab-track";
import TrackConnection, {TrackConnectionEnd} from "./track-connection";
export default class Switch extends SceneryObject {
model;
@ -17,9 +18,9 @@ export default class Switch extends SceneryObject {
type = "Switch";
applied = false;
track_prefab_name;
trackA;
trackB;
tracks = [];
def = null;
isolation_id_pos = null;
constructor(id, model, pos, rot, data, id_isolation, id_switch, maxspeed, derailspeed, track_prefab_name) {
super(id, pos, rot);
@ -64,188 +65,104 @@ export default class Switch extends SceneryObject {
let def = DefinedSwitches[this.bare_model] || null;
if(!def && !Constants.parser.forceAutoSwitches) {
SceneryParserLog.warn('switchUndefinedModel', `Switch ${this.id} has an undefined model "${this.bare_model}", trying to generate auto model definition`);
if (!def) {
SceneryParserLog.warn('switchUndefinedModel', `Switch ${this.id} has an undefined model "${this.bare_model}"`);
return;
}
if(!def || Constants.parser.forceAutoSwitches) {
def = this._tryGetAutoDef(scenery);
}
if(def) {
this._applySwitchFromDef(scenery, def);
} else {
SceneryParserLog.warn('switchAutoDefFailed', `Could not generate auto definition for switch ${this.id} with model "${this.bare_model}"`);
}
this._applySwitchFromDef(scenery, def);
}
_applySwitchFromDef(scenery, def) {
const ids = this._getTrackIds(def[2]);
if(!ids) return;
const trackIds = this._getTrackIds();
if (!trackIds) return;
const tracks = Object.entries(def.tracks).map(([key, track]) => {
return this._createSwitchTrackFromDef(scenery, def, track, trackIds);
});
if (tracks.some(track => !track)) return;
this.tracks = tracks;
this.def = def;
const trackAAliases = this._getTrackAliases(ids);
this.trackA = this._createSwitchTrackFromDef(scenery, this.id+"A", def[0], ids[0][1], ids[2][1], def[3], def[5], trackAAliases);
const trackBAliases = this._getTrackAliases(ids, 1, trackAAliases);
this.trackB = this._createSwitchTrackFromDef(scenery, this.id+"B", def[1], ids[1][1], ids[3][1], def[4], def[6], trackBAliases);
this.trackB.hide_isolation = true; // hide isolation for the second track
}
_getTrackAliases(ids, startingId = 0, exclude = []) {
const aliases = new Set();
for(let i = startingId; i < ids.length; i+=2) {
aliases.add(ids[i][0]);
}
exclude.forEach(alias => {
aliases.delete(alias);
});
return Array.from(aliases);
}
_getTrackIds(outs) {
const dataValues = this.data.split(",");
const isCrossing = outs.length === 2;
if(isCrossing) outs = [outs[0], outs[1], outs[0], outs[1]];
return outs.map((out, index) => {
const parts = dataValues[out]?.split(":");
if(parts.length !== 3) {
SceneryParserLog.warn('switchInvalidDataFormat', `Switch ${this.id} has invalid data: ${this.data}`);
return null;
}
const trackId = parts[0].trim();
let connectedId = parts[1].trim() || parts[2].trim() || null;
if(isCrossing) connectedId = parts[~~(index / 2) + 1].trim();
return [trackId, connectedId];
});
}
_createSwitchTrackFromDef(scenery, id, r, previd, nextid, startPos, endPos, aliases = []) {
const rotRad = this.rot.multiply(Math.PI / 180);
this.isolation_id_pos = this.pos.add(def.isolation_id_offset.rotate(rotRad));
super.applyObject(scenery);
}
_getTrackIds() {
const ids = this.data
.split(",")
.filter(part => part.trim() !== '')
.map(
(value) => value.split(":").map((part) => part.trim()),
);
if (ids.some((part) => part.length !== 3)) {
SceneryParserLog.warn('switchInvalidDataFormat', `Switch ${this.id} has invalid data: ${this.data}`);
}
return ids;
}
_createSwitchTrackFromDef(scenery, switchDef, trackDef, ids) {
const rotRad = this.rot.multiply(Math.PI / 180);
if (trackDef.dataIndex >= ids.length) {
SceneryParserLog.warn(
'switchMissingTrackId',
`Switch ${this.id} with model ${this.bare_model} is missing an id for the track at index ${trackDef.dataIndex}`,
);
return null;
}
const [trackId, prevId, nextId] = ids[trackDef.dataIndex];
const connections = [];
trackDef.connections.forEach((connection) => {
if (connection.type === SwitchTrackConnectionType.INTERNAL) {
const otherTrackDef = switchDef.tracks[connection.internalId];
if (!otherTrackDef) {
SceneryParserLog.warn(
'switchMissingTrackId',
`Switch prefab for model with model ${this.bare_model} of track ${this.id} does not have the internal track ${connection.internalId}`,
);
return;
}
if (otherTrackDef.dataIndex >= ids.length) {
SceneryParserLog.warn(
'switchMissingTrackId',
`Switch ${this.id} with model ${this.bare_model} is missing an id for the track at index ${otherTrackDef.dataIndex}`,
);
return;
}
const otherTrackId = ids[otherTrackDef.dataIndex][0];
if (otherTrackId) {
connections.push(new TrackConnection(otherTrackId, connection.end));
}
} else if (connection.type === SwitchTrackConnectionType.EXTERNAL) {
const otherTrackId = connection.end === TrackConnectionEnd.START ? prevId : nextId;
if (otherTrackId) {
connections.push(new TrackConnection(otherTrackId, connection.end));
}
}
});
const trackObj = new PointTrack(
id,
this.pos.add(startPos.rotate(rotRad)),
this.pos.add(endPos.rotate(rotRad)),
r,
nextid,
previd,
this.id_switch,
trackId,
this.pos.add(trackDef.startPos.rotate(rotRad)),
this.pos.add(trackDef.endPos.rotate(rotRad)),
trackDef.radius,
connections,
this.id_switch,
0, // start_slope
0, // end_slope
this.id_isolation,
this.id_isolation,
this.track_prefab_name,
this.maxspeed,
this.maxspeed,
this.derailspeed
);
trackObj.switch = this;
scenery.addObject(trackObj);
aliases.forEach(alias => {
scenery.addTrackAlias(id, alias);
});
return trackObj;
}
_tryGetAutoDef(scenery) {
const outs = Switch.autoGetOutsFromData(this.data);
if(!outs) return null;
const radiuses = Switch.autoGetRadiusesFromModel(this.bare_model);
if(!radiuses) return null;
const [ra, rb] = radiuses;
const ids = this._getTrackIds(outs);
if(!ids) return null;
const trackPoints = ids.map(idsVal =>
this._autoFindClosestTrackEnd(scenery, idsVal[1])
);
if(trackPoints.length !== 4 || trackPoints.some(point => !point)) return null;
const rotRadNeg = this.rot.multiply(-Math.PI / 180);
const transformedPoints = trackPoints.map(point => point.sub(this.pos).rotate(rotRadNeg).toPrecision(3));
const newDef = [
ra,
rb,
outs,
...transformedPoints
];
if(!DefinedSwitches[this.bare_model]) DefinedSwitches[this.bare_model] = newDef;
if(Constants.parser.logNewAutoSwitches) console.log(this.bare_model, newDef);
return newDef;
}
static autoGetOutsFromData(data) {
const tracks = data.split(",");
if(tracks.length === 3) return [0, 1]; // Crossing
if(tracks.length < 4) {
SceneryParserLog.warn('switchInvalidDataFormat', `Switch data "${data}" is invalid, expected at least 4 tracks`);
return null;
}
const outs = [];
tracks.forEach((ct, i) => {
const ctv = ct.split(":");
outs.push(ctv[1] || ctv[2]);
});
return [
0,
outs[1] ? 1 : 0, // if no second track, use first track
outs[2] ? 2 : (outs[3] ? 3 : 5), // if no third track, use fourth or sixth track
outs[4] ? 4 : (outs[6] ? 6 : 3) // if no fourth track, use sixth or third track
];
}
static autoGetRadiusesFromModel(model) {
const bareModel = (model.split(',')[0] || model).trim();
if(/Rkp|Crossing/.test(bareModel)) return [0, 0];
const right = bareModel.at(-1) === "R";
const [prefix, rText, rldsR2Text] = bareModel.split("-");
const r = parseFloat(rText) || 0;
const [r1, r2] = rText.split('_').map(value => parseFloat(value) || 0);
switch(prefix) {
case 'Rz 60E1':
return [0, right ? -r : r];
case 'Rld 60E1':
return right ? [r1, -r2] : [-r1, r2];
case 'Rlds 60E1':
const rldsR2 = parseFloat(rldsR2Text) || 0;
return [-r1, rldsR2];
case 'Rlj 60E1':
return right ? [-r1, -r2] : [r1, r2];
default:
SceneryParserLog.warn('switchCannotResolveModel', `Switch model ${model} cannot be auto-resolved to radiuses`);
return null;
}
}
_autoFindClosestTrackEnd(scenery, trackId) {
if(!trackId) {
SceneryParserLog.warn('switchInvalidTrackConnection', `Switch ${this.id}, trackId of one of the required connections is empty`);
return null;
}
const track = scenery.getObject("tracks", trackId);
if(!track) {
SceneryParserLog.warn('switchInvalidTrackConnection', `Switch ${this.id}, track ${trackId} that the switch connects to not found`);
return null;
}
return track.getCloserEndPos(this.pos);
}
}

View File

@ -1,20 +1,25 @@
export const TrackConnectionType = {
END: 0,
START: 1,
}
export const TrackConnectionEnd = {
START: Symbol('TrackConnectionEnd.START'),
END: Symbol('TrackConnectionEnd.END'),
};
export default class TrackConnection {
tracks;
otherTrack;
type;
otherType;
otherTrackId;
otherTrack = null;
end;
constructor(track, otherTrack, type, otherType) {
Object.assign(this, {
track,
otherTrack,
type,
otherType
});
/**
* If only an ID is passed as `otherTrack`,
* it needs to be resolved later in the `connectTracks` function.
*/
constructor(otherTrack, end) {
this.end = end;
if (typeof otherTrack === 'string') {
this.otherTrackId = otherTrack;
this.otherTrack = null;
} else {
this.otherTrackId = otherTrack.id;
this.otherTrack = otherTrack;
}
}
}

View File

@ -15,7 +15,7 @@ export default class TrackObject extends SceneryObject {
super(id, pos, rot);
Object.assign(this, {
prefab_name,
track_id,
track_id,
name
});
}
@ -38,13 +38,11 @@ export default class TrackObject extends SceneryObject {
if(this.applied) return; // already applied
this.applied = true;
const trackId = scenery.getTrackIdByAlias(this.track_id);
const track = scenery.getObject('tracks', trackId);
const track = scenery.getObject('tracks', this.track_id);
if (!track) {
SceneryParserLog.warn('trackObjectCannotBeApplied', `TrackObject ${this.id} cannot be applied: track ${trackId} not found`)
SceneryParserLog.warn('trackObjectCannotBeApplied', `TrackObject ${this.id} cannot be applied: track ${this.track_id} not found`)
return;
}
this.track = track;
}
}

View File

@ -1,52 +0,0 @@
import SceneryParserLog from './scenery-parser-log';
const connectionThresholdDistanceSq = 0.05;
function trackEndConnectionTest(scenery, track, isPrev) {
const vNext = isPrev ? track.previd : track.nextid;
if (!vNext) return;
const vEndPos = isPrev ? track.points.start : track.points.end;
const vNextTrackId = scenery.getTrackIdByAlias(vNext);
const vNextTrack = scenery.getObject('tracks', vNextTrackId);
if (!vNextTrack) {
SceneryParserLog.warn('tracksConnectionTest', `Track ${track.id} has non-existing ${isPrev ? 'previous' : 'next'} track: ${isPrev ? track.previd : track.nextid} (${vNextTrackId})`);
return;
}
const [vNextTrackNext, vNextTrackPrev] = isPrev ? [vNextTrack.previd, vNextTrack.nextid] : [vNextTrack.nextid, vNextTrack.previd];
const vNextTrackNextId = scenery.getTrackIdByAlias(vNextTrackNext);
const vNextTrackPrevId = scenery.getTrackIdByAlias(vNextTrackPrev);
const directlyConnected = track.aliases.includes(vNextTrackPrevId) || vNextTrackPrevId === track.id;
const reverseConnected = track.aliases.includes(vNextTrackNextId) || vNextTrackNextId === track.id;
const isSwitchBTrackPrev = isPrev && track.id.at(-1) === 'B';
if (directlyConnected) {
const vNextTrackStartPos = isPrev ? vNextTrack.points.end : vNextTrack.points.start;
const distSq = vEndPos.distanceSq(vNextTrackStartPos);
if (distSq > connectionThresholdDistanceSq) {
SceneryParserLog.warn('tracksConnectionTest', `Track ${track.id} with ${isPrev ? 'start' : 'end'} position ${vEndPos.toString()} is too far from the ${isPrev ? 'previous' : 'next'} track ${vNextTrackId} with ${isPrev ? 'end' : 'start'} position ${vNextTrackStartPos.toString()}. Distance: ${Math.sqrt(distSq).toFixed(3)}`);
return;
}
} else if (reverseConnected) {
const vNextTrackEndPos = isPrev ? vNextTrack.points.start : vNextTrack.points.end;
const distSq = vEndPos.distanceSq(vNextTrackEndPos);
if (distSq > connectionThresholdDistanceSq) {
SceneryParserLog.warn('tracksConnectionTest', `Track ${track.id} with ${isPrev ? 'start' : 'end'} position ${vEndPos.toString()} is too far from the ${isPrev ? 'previous' : 'next'} track ${vNextTrackId} with ${isPrev ? 'start' : 'end'} position ${vNextTrackEndPos.toString()}. Distance: ${Math.sqrt(distSq).toFixed(3)}`);
return;
}
} else if(!isSwitchBTrackPrev) {
SceneryParserLog.warn('tracksConnectionTest', `Track ${track.id} ${isPrev ? 'previous' : 'next'} track ${vNextTrackId} does not match its ${isPrev ? 'next' : 'previous'} track id: ${vNextTrackNext} (${vNextTrackNextId}), ${vNextTrackPrev} (${vNextTrackPrevId})`);
return;
}
}
export function tracksConnectionTest(scenery) {
const tracks = scenery.objects['tracks'] || [];
Object.values(tracks).forEach(track => {
trackEndConnectionTest(scenery, track, false);
trackEndConnectionTest(scenery, track, true);
});
}

View File

@ -1,5 +1,6 @@
import Track from "./track";
import Vector3 from "../vector3";
import TrackConnection, {TrackConnectionEnd} from "../track-connection";
export default class BezierTrack extends Track
{
@ -8,18 +9,32 @@ export default class BezierTrack extends Track
start: Vector3.zero(),
control1: Vector3.zero(),
end: Vector3.zero(),
control2: Vector3.zero()
control2: Vector3.zero(),
middle: Vector3.zero(),
};
constructor(id, start, control1, end, control2, rot, len, r, nextid, previd, id_station, start_slope, end_slope, id_isolation, prefab_name, maxspeed, derailspeed) {
super(id, start, rot, len, r, nextid, previd, id_station, start_slope, end_slope, id_isolation, prefab_name, maxspeed, derailspeed);
constructor(id, start, control1, end, control2, rot, len, r, connections, id_station, start_slope, end_slope, id_isolation, prefab_name, maxspeed, derailspeed) {
super(id, start, rot, len, r, connections, id_station, start_slope, end_slope, id_isolation, prefab_name, maxspeed, derailspeed);
Object.assign(this.points, {
start,
control1: start.add(control1),
end,
control2: end.add(control2)
control2: end.add(control2),
});
// This is not the the actual middle point, but it's close enough
this.points.middle = this._pointAtT(0.5);
}
_pointAtT(t) {
const a1 = this.points.start.lerp(this.points.control1, t);
const a2 = this.points.control1.lerp(this.points.control2, t);
const a3 = this.points.control2.lerp(this.points.end, t);
const b1 = a1.lerp(a2, t);
const b2 = a2.lerp(a3, t);
return b1.lerp(b2, t);
}
getStartAngleXZ() {
@ -32,6 +47,11 @@ export default class BezierTrack extends Track
static fromText(text) {
const values = text.split(";");
const connections = [];
if (values[15]) connections.push(new TrackConnection(values[15], TrackConnectionEnd.END));
if (values[16]) connections.push(new TrackConnection(values[16], TrackConnectionEnd.START));
const track = new BezierTrack(
values[1], // id
Vector3.fromValuesArray(values, 3), // start
@ -42,8 +62,7 @@ export default class BezierTrack extends Track
Vector3.zero(),
0, // len ??
0, // r ??
values[15], // nextid
values[16], // previd
connections,
values[17], // id_station
...Track.slopesFromText(values[18]), // start_slope, end_slope
values[21], // id_isolation
@ -54,4 +73,8 @@ export default class BezierTrack extends Track
return track;
}
applyObject(scenery) {
super.applyObject(scenery);
}
}

View File

@ -10,9 +10,9 @@ export default class PointTrack extends Track
end: Vector3.zero()
};
constructor(id, start, end, r, nextid, previd, id_station, start_slope, end_slope, id_isolation, prefab_name, maxspeed, derailspeed) {
constructor(id, start, end, r, connections, id_station, start_slope, end_slope, id_isolation, prefab_name, maxspeed, derailspeed) {
const [rot, len] = PointTrack._getRotLen(start, end, r);
super(id, start, rot, len, r, nextid, previd, id_station, start_slope, end_slope, id_isolation, prefab_name, maxspeed, derailspeed);
super(id, start, rot, len, r, connections, id_station, start_slope, end_slope, id_isolation, prefab_name, maxspeed, derailspeed);
Object.assign(this.points, {
start, end

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, electrified, route) {
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;
}
}

View File

@ -1,6 +1,7 @@
import AngleHelper from "../../helpers/angleHelper";
import Track from "./track";
import Vector3 from "../vector3";
import TrackConnection, {TrackConnectionEnd} from "../track-connection";
export default class StandardTrack extends Track
{
@ -8,11 +9,12 @@ export default class StandardTrack extends Track
points = {
start: Vector3.zero(),
end: Vector3.zero(),
circleCenter: Vector3.zero()
circleCenter: Vector3.zero(),
middle: Vector3.zero(),
};
constructor(id, start, rot, len, r, nextid, previd, id_station, start_slope, end_slope, id_isolation, prefab_name, maxspeed, derailspeed) {
super(id, start, rot, len, r, nextid, previd, id_station, start_slope, end_slope, id_isolation, prefab_name, maxspeed, derailspeed);
constructor(id, start, rot, len, r, connections, id_station, start_slope, end_slope, id_isolation, prefab_name, maxspeed, derailspeed) {
super(id, start, rot, len, r, connections, id_station, start_slope, end_slope, id_isolation, prefab_name, maxspeed, derailspeed);
this._calcPoints();
}
@ -32,14 +34,18 @@ export default class StandardTrack extends Track
static fromText(text) {
const values = text.split(";");
const connections = [];
if (values[11]) connections.push(new TrackConnection(values[11], TrackConnectionEnd.END));
if (values[12]) connections.push(new TrackConnection(values[12], TrackConnectionEnd.START));
const track = new StandardTrack(
values[1], // id
Vector3.fromValuesArray(values, 3), // start
Vector3.fromValuesArray(values, 6), // rot
parseFloat(values[9]), // len
parseFloat(values[10]), // r
values[11], // nextid
values[12], // previd
connections,
values[13], // id_station
...Track.slopesFromText(values[14]), // start_slope, end_slope
values[17], // id_isolation
@ -55,16 +61,22 @@ export default class StandardTrack extends Track
const rotRad = AngleHelper.degToRad(this.rot.y);
this.points.start = this.pos.clone();
this.points.end = this.pos.add(Vector3.fromAngleY(rotRad, this.len));
this.points.circleCenter = this.pos.clone(); // default circle center
if (this.r !== 0) {
if (this.r === 0) {
this.points.end = this.pos.add(Vector3.fromAngleY(rotRad, this.len));
this.points.middle = this.points.start.lerp(this.points.end, 0.5);
this.points.circleCenter = this.pos.clone(); // default circle center
} else {
const centerAngle = rotRad + Math.PI / 2;
this.points.circleCenter = this.pos.sub(Vector3.fromAngleY(centerAngle, this.r));
const middleAngle = centerAngle - (this.len / this.r) / 2;
this.points.middle = this.points.circleCenter.add(Vector3.fromAngleY(middleAngle, this.r));
const endAngle = centerAngle - this.len / this.r;
this.points.end = this.points.circleCenter.add(Vector3.fromAngleY(endAngle, this.r));
}
// adjust end point by the slope
if (this.end_slope !== 0) {
const startHeightDiff = this.start_slope * this.len / 1000;
@ -73,4 +85,8 @@ export default class StandardTrack extends Track
this.points.end.y += startHeightDiff + (endHeightDiff - startHeightDiff) / 2;
}
}
applyObject(scenery) {
super.applyObject(scenery);
}
}

View File

@ -1,5 +1,6 @@
import SceneryObject from "../scenery-object";
import { ElectrificationStatus } from "../electrification-status";
import {TrackConnectionEnd} from "../track-connection";
export default class Track extends SceneryObject {
len;
@ -10,28 +11,24 @@ export default class Track extends SceneryObject {
derailspeed;
category = "tracks";
type = "Track";
nextid;
previd;
connections = [];
start_slope;
end_slope;
prefab_name;
hide_isolation = false;
aliases = [];
switch = null;
electrificationStatus = ElectrificationStatus.NOT_CHECKED;
hasNEVP = false;
connections = [];
constructor(id, pos, rot, len, r, nextid, previd, id_station, start_slope, end_slope, id_isolation, prefab_name, maxspeed, derailspeed) {
constructor(id, pos, rot, len, r, connections, id_station, start_slope, end_slope, id_isolation, prefab_name, maxspeed, derailspeed) {
super(id, pos, rot);
Object.assign(this, {
len, r,
nextid, previd,
id_station,
connections,
id_station,
start_slope, end_slope,
id_isolation,
prefab_name,
maxspeed, derailspeed
prefab_name,
maxspeed, derailspeed,
});
}
@ -43,10 +40,19 @@ export default class Track extends SceneryObject {
throw new Error("getEndAngleXZ() must be implemented in subclass");
}
getAngleXZForEnd(end) {
if (end === TrackConnectionEnd.START) return this.getStartAngleXZ();
else return this.getEndAngleXZ();
}
static slopesFromText(text) {
return text.split(",", 2);
}
getEndPos(end) {
return end === TrackConnectionEnd.START ? this.points.start : this.points.end;
}
getCloserEndPos(pos) {
const distStartSq = pos.distanceSq(this.points.start);
const distEndSq = pos.distanceSq(this.points.end);
@ -56,7 +62,7 @@ export default class Track extends SceneryObject {
return this.points.end;
}
}
getRenderBounds() {
return Object.values(this.points);
}

View File

@ -7,22 +7,24 @@ import sys
# v1.4
#
BAD_WORDS = ['Empty,Empty', 'Forest Start,Forest Start', 'EndMiscGroup', 'Fence', 'TerrainPoint', 'Wires']
BAD_WORDS = ['EndMiscGroup', 'Fence', 'TerrainPoint', 'Wires']
def should_exclude(line: str) -> bool:
for word in BAD_WORDS:
if line.startswith(word):
return True
if line.startswith("Misc"):
values = line.split(';')
return not values[2].startswith("SignalBox")
return False
def process_file(file_path: str):
if not file_path.endswith(".sc"):
print(f"Skipping non-.sc file: {file_path}")
return
if file_path.endswith(".lite.sc"):
print(f"Skipping already processed file: {file_path}")
return