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
This commit is contained in:
izawartka 2025-06-03 18:17:09 +02:00
parent 6d4aef606a
commit 9b49f4dc73
5 changed files with 568 additions and 64 deletions

View File

@ -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: [
{

View File

@ -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;

View File

@ -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) {

View File

@ -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);
}
}

View File

@ -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,