diff --git a/.gitignore b/.gitignore index 34fe017..830909f 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* -public/sceneries/* \ No newline at end of file +public/sceneries/* + +# IDEs +.idea diff --git a/src/components/map/Map.css b/src/components/map/Map.css index 7c95820..3e72537 100644 --- a/src/components/map/Map.css +++ b/src/components/map/Map.css @@ -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; } diff --git a/src/components/map/ObjectsRenderQueue.js b/src/components/map/ObjectsRenderQueue.js index a4f84bf..738951c 100644 --- a/src/components/map/ObjectsRenderQueue.js +++ b/src/components/map/ObjectsRenderQueue.js @@ -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; \ No newline at end of file +export default ObjectRendererQueue; diff --git a/src/components/map/SceneryLayer.js b/src/components/map/SceneryLayer.js index 5ac1b08..ee26395 100644 --- a/src/components/map/SceneryLayer.js +++ b/src/components/map/SceneryLayer.js @@ -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 ( ); } - + const MemoizedSceneryLayer = memo(StatelessSceneryLayer); function StatelessSceneryLayer({ name, Renderer, objects, type, types, pointerEvents = false, AdditionalComponents = [] }) { return ( - @@ -53,4 +60,4 @@ function StatelessSceneryLayer({ name, Renderer, objects, type, types, pointerEv ))} ); -} \ No newline at end of file +} diff --git a/src/components/map/object-renderers/IsolationEndRenderer.js b/src/components/map/object-renderers/IsolationEndRenderer.js new file mode 100644 index 0000000..12e3e5e --- /dev/null +++ b/src/components/map/object-renderers/IsolationEndRenderer.js @@ -0,0 +1,40 @@ +import { useMemo } from "react"; +import { TrackConnectionEnd } from "../../../model/track-connection"; + +export default function IsolationEndRenderer(props) { + const { object } = props; + return (<> + + + ); +} + +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 +} diff --git a/src/components/map/object-renderers/IsolationIdRenderer.js b/src/components/map/object-renderers/IsolationIdRenderer.js index 3a6088f..f1be890 100644 --- a/src/components/map/object-renderers/IsolationIdRenderer.js +++ b/src/components/map/object-renderers/IsolationIdRenderer.js @@ -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 && } - - - ); -} - -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 + return (); } diff --git a/src/components/map/object-renderers/RouteRenderer.js b/src/components/map/object-renderers/RouteRenderer.js index 474ead5..e42de3c 100644 --- a/src/components/map/object-renderers/RouteRenderer.js +++ b/src/components/map/object-renderers/RouteRenderer.js @@ -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 ( - - - { - svg.setAttribute('width', '20mm'); - svg.setAttribute('height', '20mm'); - }} - /> - + return (<> + + { + svg.setAttribute('width', '20mm'); + svg.setAttribute('height', '20mm'); + }} + /> + + - ); + ); } diff --git a/src/components/map/object-renderers/TrackRenderer.js b/src/components/map/object-renderers/TrackRenderer.js index 8f85636..b9fa747 100644 --- a/src/components/map/object-renderers/TrackRenderer.js +++ b/src/components/map/object-renderers/TrackRenderer.js @@ -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 ( { - 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 - )); -} \ No newline at end of file +} diff --git a/src/model/defs/defined-switches.js b/src/model/defs/defined-switches.js index ca0f3a1..48a2260 100644 --- a/src/model/defs/defined-switches.js +++ b/src/model/defs/defined-switches.js @@ -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; \ No newline at end of file + "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; diff --git a/src/model/electrification-resolver.js b/src/model/electrification-resolver.js index dea0702..9cebb88 100644 --- a/src/model/electrification-resolver.js +++ b/src/model/electrification-resolver.js @@ -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); } -}; \ No newline at end of file +}; diff --git a/src/model/route.js b/src/model/route.js index 511692a..c3a4324 100644 --- a/src/model/route.js +++ b/src/model/route.js @@ -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 }); } -} \ No newline at end of file + + // 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); + }); + }); + } +} diff --git a/src/model/scenery-parser.js b/src/model/scenery-parser.js index 51292a0..77136ea 100644 --- a/src/model/scenery-parser.js +++ b/src/model/scenery-parser.js @@ -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); } } -} \ No newline at end of file + + // 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; + } +} diff --git a/src/model/scenery.js b/src/model/scenery.js index 08737da..b3a69d5 100644 --- a/src/model/scenery.js +++ b/src/model/scenery.js @@ -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; - } - ); + }); } -} \ No newline at end of file +} diff --git a/src/model/switch-descriptions/switch-prefab-track.js b/src/model/switch-descriptions/switch-prefab-track.js new file mode 100644 index 0000000..7dc8c40 --- /dev/null +++ b/src/model/switch-descriptions/switch-prefab-track.js @@ -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, + ); + } +} diff --git a/src/model/switch-descriptions/switch-prefab.js b/src/model/switch-descriptions/switch-prefab.js new file mode 100644 index 0000000..3bfa580 --- /dev/null +++ b/src/model/switch-descriptions/switch-prefab.js @@ -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()); + } +} diff --git a/src/model/switch.js b/src/model/switch.js index b19cc39..2ac3536 100644 --- a/src/model/switch.js +++ b/src/model/switch.js @@ -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); - } } diff --git a/src/model/track-connection.js b/src/model/track-connection.js index 7aab742..4f21817 100644 --- a/src/model/track-connection.js +++ b/src/model/track-connection.js @@ -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; + } } } diff --git a/src/model/track-objects/track-object.js b/src/model/track-objects/track-object.js index 051fc34..5a60440 100644 --- a/src/model/track-objects/track-object.js +++ b/src/model/track-objects/track-object.js @@ -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; } } diff --git a/src/model/tracks-connection-test.js b/src/model/tracks-connection-test.js deleted file mode 100644 index dec8fc9..0000000 --- a/src/model/tracks-connection-test.js +++ /dev/null @@ -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); - }); -} \ No newline at end of file diff --git a/src/model/tracks/bezier-track.js b/src/model/tracks/bezier-track.js index 33abd81..9850b68 100644 --- a/src/model/tracks/bezier-track.js +++ b/src/model/tracks/bezier-track.js @@ -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); + } } diff --git a/src/model/tracks/point-track.js b/src/model/tracks/point-track.js index 6ba7aaa..64f2b38 100644 --- a/src/model/tracks/point-track.js +++ b/src/model/tracks/point-track.js @@ -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 diff --git a/src/model/tracks/route-track.js b/src/model/tracks/route-track.js new file mode 100644 index 0000000..ac93dfa --- /dev/null +++ b/src/model/tracks/route-track.js @@ -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; + } +} diff --git a/src/model/tracks/standard-track.js b/src/model/tracks/standard-track.js index 27c64b2..d359411 100644 --- a/src/model/tracks/standard-track.js +++ b/src/model/tracks/standard-track.js @@ -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); + } } diff --git a/src/model/tracks/track.js b/src/model/tracks/track.js index b209c29..926425b 100644 --- a/src/model/tracks/track.js +++ b/src/model/tracks/track.js @@ -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); } diff --git a/tools/scp.py b/tools/scp.py index bbd814a..8cbc5c5 100644 --- a/tools/scp.py +++ b/tools/scp.py @@ -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