diff --git a/src/components/map/object-renderers/TrackRenderer.js b/src/components/map/object-renderers/TrackRenderer.js index 8f85636..cc8045a 100644 --- a/src/components/map/object-renderers/TrackRenderer.js +++ b/src/components/map/object-renderers/TrackRenderer.js @@ -56,7 +56,8 @@ function StatelessTrackRenderer(props) { function getTrackPath(object) { switch (object.type) { case "StandardTrack": - case "PointTrack": + case "PointTrack": + case "RouteTrack": const [x1, y1] = object.points.start.toSVGCoords(); const [x2, y2] = object.points.end.toSVGCoords(); const ra = Math.abs(object.r); @@ -84,7 +85,7 @@ function getTrackColor(object, trackColorMode) { switch (trackColorMode) { case "standard": default: - if(object.prefab_name.includes('trans-mat')) + if(object.prefab_name && object.prefab_name.includes('trans-mat')) return modeDef.options['invisible'][0]; return modeDef.options[modeDef.optionDefault][0]; @@ -102,13 +103,15 @@ function getTrackColor(object, trackColorMode) { default: return modeDef.options[modeDef.optionDefault][0]; } - + case "type": switch (object.type) { case "StandardTrack": return modeDef.options['standard-track'][0]; case "PointTrack": return modeDef.options['point-track'][0]; + case "RouteTrack": + return modeDef.options['route-track'][0]; case "BezierTrack": return modeDef.options['bezier-track'][0]; default: @@ -133,13 +136,13 @@ function getTrackColor(object, trackColorMode) { function getTrackDefs(object, trackColorMode) { if (trackColorMode !== "slope") return null; if (object.start_slope === object.end_slope) return null; - + const [x1, y1] = object.points.start.toSVGCoords(); const [x2, y2] = object.points.end.toSVGCoords(); const gradId = `track-slope-${object.id}`; const startColor = MiscHelper.getTrackGradient(Constants.trackColorModes['slope'].gradient, Math.abs(object.start_slope)); const endColor = MiscHelper.getTrackGradient(Constants.trackColorModes['slope'].gradient, Math.abs(object.end_slope)); - + return ( { + if (!route.segments[0]) return; + route.segments[0].tracks.forEach((track) => { + routeTracks.set(track.id, track); + }); + }); + + Object.values(scenery.objects['tracks'] || {}).forEach((track) => { + track.connections.forEach((connection) => { + const routeTrack = routeTracks.get(connection.otherTrackId); + if (!routeTrack) return; + const alreadyConnected = routeTrack.connections.some( + (reverseConnection) => reverseConnection.otherTrackId === track.id, + ); + if (alreadyConnected) return; + routeTrack.connections.push(new TrackConnection(track.id, TrackConnectionEnd.START)); + }); + }); +} diff --git a/src/model/route.js b/src/model/route.js index 11676af..119c263 100644 --- a/src/model/route.js +++ b/src/model/route.js @@ -1,5 +1,7 @@ import SceneryObject from "./scenery-object"; import Vector3 from "./vector3"; +import SceneryParserLog from "./scenery-parser-log"; +import RouteTrack from "./tracks/route-track"; export default class Route extends SceneryObject { track_count; @@ -8,8 +10,12 @@ export default class Route extends SceneryObject { track_offset; category = "routes"; type = "Route"; - points = []; + points; + offsets; segments = []; + end_center; + end_points; + end_angle_rad; constructor(prefab_name, pos, rot, track_count, route_name, track_offset, electrified) { super(route_name, pos, rot); @@ -22,7 +28,11 @@ export default class Route extends SceneryObject { electrified, }); - this.points = this._getConnectionPoints(); + this.offsets = this._getOffsets(); + this.end_angle_rad = this.rot.y * Math.PI / 180; + this.end_center = this.pos.clone(); + this.end_points = this._getConnectionPoints(); + this.points = [...this.end_points]; } static fromText(text) { @@ -33,7 +43,7 @@ export default class Route extends SceneryObject { Vector3.fromValuesArray(values, 6), // rot parseInt(values[9]), // track_count values[10], // route_name - parseFloat(values[11]) || 0, // track_offset + parseFloat(values[11]) || 0, // center_offset values[13] === "True" || values[12] === "1" // electrified ); @@ -41,21 +51,84 @@ export default class Route extends SceneryObject { } addSegment(length, radius, trackIds) { - this.segments.push({ length, radius, trackIds }); + if (trackIds.length !== this.track_count) { + SceneryParserLog.warn('routeInvalidSegment', `Route segment has invalid track count: expected ${this.track_count}, got ${trackIds.length}`); + return; + } + const startAngleRad = this.end_angle_rad; + const start_rot = new Vector3(0, startAngleRad * 180 / Math.PI, 0); + + const startPoints = this.end_points; + this.end_points = []; + + const radii = []; + const lengths = []; + + if (radius === 0) { + this.end_center = this.end_center.add(Vector3.fromAngleY(startAngleRad, length)); + this.offsets.forEach(offset => { + radii.push(0); + lengths.push(length); + const end_point = this.end_center + .add(Vector3.fromAngleY(this.end_angle_rad + Math.PI / 2, offset)); + this.end_points.push(end_point); + }); + } else { + this.end_angle_rad -= length / radius; + const startToCenter = Vector3.fromAngleY(startAngleRad + Math.sign(radius) * Math.PI / 2, Math.abs(radius)); + const circleCenter = this.end_center.add(startToCenter.negate()); + const centerToEndUnit = Vector3.fromAngleY(this.end_angle_rad + Math.sign(radius) * Math.PI / 2, 1).multiply(Math.sign(radius)); + this.end_center = circleCenter.add(centerToEndUnit.multiply(radius)); + + this.offsets.forEach(offset => { + const trackRadius = radius + offset; + const trackLength = length * trackRadius / radius; + radii.push(trackRadius); + lengths.push(trackLength); + const end_point = circleCenter.add(centerToEndUnit.multiply(trackRadius)); + this.end_points.push(end_point); + }); + } + + this.points.push(...this.end_points); + + const tracks = trackIds.map((trackId, index) => { + return new RouteTrack( + trackId, + startPoints[index], + this.end_points[index], + start_rot, + lengths[index], + radii[index], + [], // connections + this, // route + ); + }); + this.segments.push({ length, radius, trackIds, tracks }); + } + + // This method is future-proofed for more than 2 tracks + _getOffsets() { + const leftmost_offset = -(this.track_count - 1) * 2 + this.track_offset; + const result = []; + for (let i = 0; i < this.track_count; i++) { + result.push(leftmost_offset + i * 4); + } + return result; } _getConnectionPoints() { - if (this.track_count === 1) { - return [this.pos.clone()]; - } + return this.offsets.map((offset) => { + const offsetVec = Vector3.fromAngleY(this.end_angle_rad + Math.PI / 2, offset); + return this.pos.add(offsetVec); + }); + } - const angle = (this.rot.y + 90) * Math.PI / 180; - const rightTrackOffset = Vector3.fromAngleY(angle, this.track_offset - 2); - const leftTrackOffset = Vector3.fromAngleY(angle, this.track_offset + 2); - - return [ - this.pos.add(rightTrackOffset), - this.pos.add(leftTrackOffset) - ]; + applyObject(scenery) { + this.segments.forEach((segment) => { + segment.tracks.forEach((track) => { + scenery.addObject(track); + }); + }); } } diff --git a/src/model/scenery-parser.js b/src/model/scenery-parser.js index 0e940cd..9aad6de 100644 --- a/src/model/scenery-parser.js +++ b/src/model/scenery-parser.js @@ -19,6 +19,7 @@ import NEVP from './track-objects/nevp'; import Derailer from './track-objects/derailer'; import SpawnPoint from './track-objects/spawn-point'; import { attachSigns } from './attach-signs'; +import {connectRoutes} from "./connect-routes"; /* TODO: add support for WorldRotation and WorldTranslation @@ -57,6 +58,7 @@ export default class SceneryParser { }); scenery.applyObjects(); + connectRoutes(scenery); if(Constants.parser.runTracksConnectionTest) tracksConnectionTest(scenery); if(Constants.parser.resolveElectrification) ElectrificationResolver.resolveScenery(scenery); @@ -180,10 +182,6 @@ export default class SceneryParser { SceneryParserLog.warn('routeInvalidSegment', `Invalid route segment track IDs: ${fields[3]}`); return; } - if (trackIds.length !== route.track_count) { - SceneryParserLog.warn('routeInvalidSegment', `Route segment has invalid track count: expected ${route.track_count}, got ${trackIds.length}`); - return; - } route.addSegment( parseInt(fields[1]), // length parseInt(fields[2]), // radius diff --git a/src/model/tracks/route-track.js b/src/model/tracks/route-track.js new file mode 100644 index 0000000..b4236d9 --- /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, route, electrified) { + super( + id, start, rot, len, r, connections, + null, // id_station, + 0, // start_slope, + 0, // end_slope, + null, // id_isolation + null, // prefab_name + null, // maxspeed, + null, // derailspeed, + ); + this.electrificationStatus = electrified ? ElectrificationStatus.ELECTRIFIED : ElectrificationStatus.NON_ELECTRIFIED; + this.points = { + start, + end, + }; + this.route = route; + } + + getStartAngleXZ() { + return AngleHelper.degToRad(this.rot.y); + } + + getEndAngleXZ() { + const startAngle = AngleHelper.degToRad(this.rot.y); + + if(this.r === 0) { + return startAngle + } + + return startAngle - this.len / this.r; + } +}