From 9b49f4dc73b7a192a12a01625fe7894fc60b5f35 Mon Sep 17 00:00:00 2001 From: izawartka <59137928+izawartka@users.noreply.github.com> Date: Tue, 3 Jun 2025 18:17:09 +0200 Subject: [PATCH] Added defined switches, fixed bugs +Added pre-defined switches so they dont have to be generated. Unrecognised switches models are still automatically resolved and their new definitions are saved for later use +Added .multiply, .toPrecission and .rotate functions in the Vector3 class *Improved switches to normal tracks connections *Improved switches-related error handling --- src/helpers/constants.js | 8 + src/model/defined-switches.js | 348 ++++++++++++++++++++++++++++++++++ src/model/scenery.js | 14 ++ src/model/switch.js | 221 ++++++++++++++------- src/model/vector3.js | 41 ++++ 5 files changed, 568 insertions(+), 64 deletions(-) create mode 100644 src/model/defined-switches.js diff --git a/src/helpers/constants.js b/src/helpers/constants.js index c51caaf..7005557 100644 --- a/src/helpers/constants.js +++ b/src/helpers/constants.js @@ -3,6 +3,9 @@ const Constants = { zoomSensitivity: 0.002, trackSlopeScale: 10, }, + parser: { + forceAutoSwitches: false + }, warnings: { all: false, // enable all warnings trackObjectCannotBeApplied: false, @@ -10,6 +13,11 @@ const Constants = { unknownObjectType: true, unknownTrackType: true, signalWithoutSpawnInfo: true, + trackAliasAlreadyExists: true, + switchUndefinedModel: true, + switchAutoDefFailed: true, + switchInvalidDataFormat: true, + switchNoModel: true, }, layers: [ { diff --git a/src/model/defined-switches.js b/src/model/defined-switches.js new file mode 100644 index 0000000..2ff5285 --- /dev/null +++ b/src/model/defined-switches.js @@ -0,0 +1,348 @@ +import Vector3 from "./vector3"; + +const DefinedSwitches = { + "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], + 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], + 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], + 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], + 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) + ], + "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-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-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) + ], + "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) + ], + "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-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_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) + ] +} + +export default DefinedSwitches; \ No newline at end of file diff --git a/src/model/scenery.js b/src/model/scenery.js index 629cd2d..9072a5e 100644 --- a/src/model/scenery.js +++ b/src/model/scenery.js @@ -13,6 +13,7 @@ export default class Scenery signalBoxes = []; spawnSignals = []; bounds = { minX: Infinity, minZ: Infinity, maxX: -Infinity, maxZ: -Infinity }; + trackAliases = {}; static nextMiscId = 1; getBounds () { @@ -72,6 +73,19 @@ 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; + } + + this.trackAliases[alias] = id; + } + + getTrackIdByAlias(idOrAlias) { + return this.trackAliases[idOrAlias] || idOrAlias; + } + static _parseObject(text) { const type = text.split(";", 2)[0]; switch(type) { diff --git a/src/model/switch.js b/src/model/switch.js index cd8a60a..f3574f8 100644 --- a/src/model/switch.js +++ b/src/model/switch.js @@ -2,6 +2,8 @@ import PointTrack from "./point-track.js"; import SceneryObject from "./scenery-object.js"; import SceneryParserLog from "./scenery-parser-log.js"; import Vector3 from "./vector3.js"; +import DefinedSwitches from "./defined-switches.js"; +import Constants from "../helpers/constants.js"; export default class Switch extends SceneryObject { model; @@ -12,7 +14,6 @@ export default class Switch extends SceneryObject { derailspeed; category = "switches"; type = "Switch"; - outs = []; applied = false; trackA; trackB; @@ -25,6 +26,8 @@ export default class Switch extends SceneryObject { id_switch, id_isolation, maxspeed, derailspeed }); + + this.bare_model = model.split(',')[0]?.trim() || model.trim(); } static fromText(text) { @@ -49,80 +52,71 @@ export default class Switch extends SceneryObject { if(this.applied) return; // already applied this.applied = true; - this.outs = Switch.getOutsFromData(this.data); - const r = Switch.getRadiusFromModel(this.model); - - this.trackA = this._createGetSwitchTrack(scenery, this.id+"A", this.outs[0], this.outs[2], 0); - if(this.trackA) { - scenery.addObject(this.trackA); + if(!this.bare_model) { + SceneryParserLog.warn('switchNoModel', `Switch ${this.id} has no model defined, apply`); + return null; } - - this.trackB = this._createGetSwitchTrack(scenery, this.id+"B", this.outs[1], this.outs[3], r); - if(this.trackB) { - this.trackB.hide_isolation = true; // hide isolation for the second track - scenery.addObject(this.trackB); + + let def = DefinedSwitches[this.bare_model] || null; + + if(!def && !Constants.parser.forceAutoSwitches) { + SceneryParserLog.warn('switchUndefinedModel', `Switch ${this.id} has undefined model "${this.bare_model}", trying to generate auto model definition`); } - } - static getOutsFromData(data) { - const cts = data.split(","); - - const tou = []; - cts.forEach((ct, i) => { - cts[i] = ct.split(":"); - if(cts[i][1] === "") cts[i][1] = cts[i][2]; - tou.push(cts[i][1]); - }); - - return [ - tou[0], - tou[1] || tou[0], - tou[2] || tou[3] || tou[5], - tou[4] || tou[6] || tou[3] - ]; - } - - static getRadiusFromModel(model) { - if(/Rkpd|Crossing/.test(model)) return 0; + if(!def || Constants.parser.forceAutoSwitches) { + def = this._tryGetAutoDef(scenery); + } - const radiusText = model.replace(/(Rz 60E1-)|-1_.*/g, ""); - const radius = parseInt(radiusText) || 0; - const side = model.split(",")[0]; - const isRight = side[side.length-1] === "R"; - - return isRight ? -radius : radius; + if(def) { + this._applySwitchFromDef(scenery, def); + } else { + SceneryParserLog.warn('switchAutoDefFailed', `Could not generate auto definition for switch ${this.id} with model "${this.bare_model}"`); + } } - _createGetSwitchTrack(scenery, name, from, to, r) { - if(!from) { - SceneryParserLog.warn('switchInvalidTrackConnection', `Switch track ${name} has no 'from' track set`); - return null; - } else if(!to) { - SceneryParserLog.warn('switchInvalidTrackConnection', `Switch track ${name} has no 'to' track set`); - return null; - } + _applySwitchFromDef(scenery, def) { + const ids = this._getTrackIds(def[2]); + if(!ids) return; - const startTrack = scenery.getObject("tracks", from); - const endTrack = scenery.getObject("tracks", to); + const trackAAliases = [ids[0][0], ids[2][0]]; + this.trackA = this._createSwitchTrackFromDef(scenery, this.id+"A", def[0], ids[0][1], ids[2][1], def[3], def[5], trackAAliases); + scenery.addObject(this.trackA); - if(!startTrack) { - SceneryParserLog.warn('switchInvalidTrackConnection', `Switch track ${name}, 'from' track ${from} not found`); - return null; - } else if(!endTrack) { - SceneryParserLog.warn('switchInvalidTrackConnection', `Switch track ${name}, 'to' track ${to} not found`); - return null; - } + const trackBAliases = [ids[0][0], ids[2][0]].filter(alias => trackAAliases.includes(alias) === false); + 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 + scenery.addObject(this.trackB); + } - const startTrackEnd = startTrack.getCloserEndPos(this.pos); - const endTrackEnd = endTrack.getCloserEndPos(this.pos); + _getTrackIds(outs) { + const dataValues = this.data.split(","); + const isCrossing = outs.length === 2; + if(isCrossing) outs = [outs[0], outs[0], outs[1], outs[1]]; - let trackObj = new PointTrack( - name, - startTrackEnd, - endTrackEnd, + 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); + + const trackObj = new PointTrack( + id, + this.pos.add(startPos.rotate(rotRad)), + this.pos.add(endPos.rotate(rotRad)), r, - from, - to, + previd, + nextid, this.id_switch, 0, // start_slope 0, // end_slope @@ -131,6 +125,105 @@ export default class Switch extends SceneryObject { this.derailspeed ); + 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; + // console.log(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] = 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': + return [-r1, r2]; + 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/vector3.js b/src/model/vector3.js index a8a2152..190cba5 100644 --- a/src/model/vector3.js +++ b/src/model/vector3.js @@ -36,6 +36,47 @@ export default class Vector3 { ); } + rotate(rot) { + const [ax, ay, az] = rot.toArray(); + + const cosX = Math.cos(ax); + const sinX = Math.sin(ax); + const cosY = Math.cos(ay); + const sinY = Math.sin(ay); + const cosZ = Math.cos(az); + const sinZ = Math.sin(az); + + // Rotate around Y + let x = this.x * cosY + this.z * sinY; + let z = -this.x * sinY + this.z * cosY; + + // Rotate around X + let y = this.y * cosX - z * sinX; + z = this.y * sinX + z * cosX; + + // Rotate around Z + x = x * cosZ - y * sinZ; + y = x * sinZ + y * cosZ; + + return new Vector3(x, y, z); + } + + toPrecision(precision = 3) { + return new Vector3( + parseFloat(this.x.toFixed(precision)), + parseFloat(this.y.toFixed(precision)), + parseFloat(this.z.toFixed(precision)) + ); + } + + multiply(scalar) { + return new Vector3( + this.x * scalar, + this.y * scalar, + this.z * scalar + ); + } + lerp(other, t) { return new Vector3( this.x + (other.x - this.x) * t,