From db273d24201317ecbb2f23a1c7841a20c039ef82 Mon Sep 17 00:00:00 2001 From: dominik-korsa <29484605+dominik-korsa@users.noreply.github.com> Date: Sat, 12 Jul 2025 00:22:38 +0200 Subject: [PATCH 01/24] Change switch prefab definition format --- .gitignore | 5 +- src/helpers/constants.js | 7 +- src/model/defs/defined-switches.js | 659 +++--------------- .../switch-prefab-track.js | 37 + .../switch-descriptions/switch-prefab.js | 176 +++++ src/model/switch.js | 240 +++---- 6 files changed, 405 insertions(+), 719 deletions(-) create mode 100644 src/model/switch-descriptions/switch-prefab-track.js create mode 100644 src/model/switch-descriptions/switch-prefab.js 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/helpers/constants.js b/src/helpers/constants.js index 5665913..b36cca1 100644 --- a/src/helpers/constants.js +++ b/src/helpers/constants.js @@ -8,8 +8,6 @@ const Constants = { forcePointerEvents: false }, parser: { - forceAutoSwitches: false, - logNewAutoSwitches: true, logSceneryAfterFinished: true, sceneryInfoVersion: 29, alwaysShowLogDialog: false, @@ -35,9 +33,10 @@ const Constants = { trackAliasAlreadyExists: true, trackAliasNoTrack: true, switchUndefinedModel: true, - switchAutoDefFailed: true, switchInvalidDataFormat: true, switchNoModel: true, + switchMissingTrackId: true, + switchInvalidInternalConnection: true, signalBoxUndefinedPrefabName: true, invalidSceneryInfoVersion: true, signUndefinedPrefabName: true, @@ -175,4 +174,4 @@ const Constants = { } }; -export default Constants; \ No newline at end of file +export default Constants; diff --git a/src/model/defs/defined-switches.js b/src/model/defs/defined-switches.js index a9ea06f..f8f1c25 100644 --- a/src/model/defs/defined-switches.js +++ b/src/model/defs/defined-switches.js @@ -1,555 +1,110 @@ -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-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": [ + // 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) + // ], + // Rkp 60E1-190-1_9 ab + // Rkp 60E1-190-1_9 ba + // Rkpd 60E1-190-1_9 + "Rld 60E1-1200_600-1_15 L": SwitchPrefab.fork(600.0, -1200.0, 39.95566, 0.0), + "Rld 60E1-1200_600-1_15 R": SwitchPrefab.fork(-600.0, 1200.0, 39.95566, 0.0), + "Rld 60E1-1200_900-1_18.5 L": SwitchPrefab.fork(900.0, -1200.0, 48.61317, 0.0), + "Rld 60E1-1200_900-1_18.5 R": SwitchPrefab.fork(-900.0, 1200.0, 48.61317, 0.0), + "Rld 60E1-1800_300-1_9 L": SwitchPrefab.fork(300.0, -1800.0, 33.23108, 0.0), + "Rld 60E1-1800_300-1_9 R": SwitchPrefab.fork(-300.0, 1800.0, 33.23108, 0.0), + "Rld 60E1-1800_600-1_14 L": SwitchPrefab.fork(600.0, -1800.0, 42.80262, 0.0), + "Rld 60E1-1800_600-1_14 R": SwitchPrefab.fork(-600.0, 1800.0, 42.80262, 0.0), + "Rld 60E1-1800_900-1_18.5 L": SwitchPrefab.fork(900.0, -1800.0, 48.61317, 0.0), + "Rld 60E1-1800_900-1_18.5 R": SwitchPrefab.fork(-900.0, 1800.0, 48.61317, 0.0), + "Rld 60E1-2500_1200-1_22 L": SwitchPrefab.fork(1200.0, -2500.0, 54.51731, 0.0), + "Rld 60E1-2500_1200-1_22 R": SwitchPrefab.fork(-1200.0, 2500.0, 54.51731, 0.0), + "Rld 60E1-2500_250-1_8.5 L": SwitchPrefab.fork(250.0, -2500.0, 29.31069, 0.0), + "Rld 60E1-2500_250-1_8.5 R": SwitchPrefab.fork(-250.0, 2500.0, 29.31069, 0.0), + "Rld 60E1-2500_400-1_10.5 L": SwitchPrefab.fork(400.0, -2500.0, 38.00925, 0.0), + "Rld 60E1-2500_400-1_10.5 R": SwitchPrefab.fork(-400.0, 2500.0, 38.00925, 0.0), + "Rld 60E1-2500_600-1_14 L": SwitchPrefab.fork(600.0, -2500.0, 42.80262, 0.0), + "Rld 60E1-2500_600-1_14 R": SwitchPrefab.fork(-600.0, 2500.0, 42.80262, 0.0), + "Rld 60E1-2500_900-1_17 L": SwitchPrefab.fork(900.0, -2500.0, 52.14928, 0.0), + "Rld 60E1-2500_900-1_17 R": SwitchPrefab.fork(-900.0, 2500.0, 52.14928, 0.0), + "Rld 60E1-600_300-1_9 L": SwitchPrefab.fork(300.0, -600.0, 29.92537, 0.0), + "Rld 60E1-600_300-1_9 R": SwitchPrefab.fork(-300.0, 600.0, 29.92537, 0.0), + "Rld 60E1-650_450-1_15 L": SwitchPrefab.fork(450.0, -650.0, 29.96674, 0.0), + "Rld 60E1-650_450-1_15 R": SwitchPrefab.fork(-450.0, 650.0, 29.96674, 0.0), + "Rld 60E1-700_190-1_7.5 L": SwitchPrefab.fork(190.0, -700.0, 25.22173, 0.0), + "Rld 60E1-700_190-1_7.5 R": SwitchPrefab.fork(-190.0, 700.0, 25.22173, 0.0), + "Rld 60E1-700_300-1_10 L": SwitchPrefab.fork(300.0, -700.0, 29.38637, 0.0), + "Rld 60E1-700_300-1_10 R": SwitchPrefab.fork(-300.0, 700.0, 29.38637, 0.0), + "Rld 60E1-700_500-1_14 L": SwitchPrefab.fork(500.0, -700.0, 35.66885, 0.0), + "Rld 60E1-700_500-1_14 R": SwitchPrefab.fork(-500.0, 700.0, 35.66885, 0.0), + "Rld 60E1-900_300-1_9 L": SwitchPrefab.fork(300.0, -900.0, 33.23108, 0.0), + "Rld 60E1-900_300-1_9 R": SwitchPrefab.fork(-300.0, 900.0, 33.23108, 0.0), + "Rld 60E1-900_450-1_12 L": SwitchPrefab.fork(450.0, -900.0, 37.43512, 0.0), + "Rld 60E1-900_450-1_12 R": SwitchPrefab.fork(-450.0, 900.0, 37.43512, 0.0), + "Rld 60E1-900_600-1_15 L": SwitchPrefab.fork(600.0, -900.0, 39.95566, 0.0), + "Rld 60E1-900_600-1_15 R": SwitchPrefab.fork(-600.0, 900.0, 39.95566, 0.0), + "Rlds 60E1-1000-1000-1_23": SwitchPrefab.fork(1000.0, -1000.0, 43.45773, 0.0), + "Rlds 60E1-190-190-1_9": SwitchPrefab.fork(190.0, -190.0, 21.23, 0.0), + "Rlds 60E1-600-600-1_18.5": SwitchPrefab.fork(599.9205, -600.0, 33.22262, 0.0), + "Rlj 60E1-1200_300-1_7 L": SwitchPrefab.fork(300.0, 1200.0, 42.64069, 0.0), + "Rlj 60E1-1200_300-1_7 R": SwitchPrefab.fork(-300.0, -1200.0, 42.64069, 0.0), + "Rlj 60E1-1200_600-1_9 L": SwitchPrefab.fork(600.0, 1200.0, 68.0, 0.0), + "Rlj 60E1-1200_600-1_9 R": SwitchPrefab.fork(-600.0, -1200.0, 68.0, 0.0), + "Rlj 60E1-1500_450-1_9 L": SwitchPrefab.fork(450.0, 1500.0, 49.84663, 0.0), + "Rlj 60E1-1500_450-1_9 R": SwitchPrefab.fork(-450.0, -1500.0, 49.84663, 0.0), + "Rlj 60E1-1800_300-1_7.5 L": SwitchPrefab.fork(300.0, 1800.0, 39.0, 0.0), + "Rlj 60E1-1800_300-1_7.5 R": SwitchPrefab.fork(-300.0, -1800.0, 39.0, 0.0), + "Rlj 60E1-1800_450-1_9 L": SwitchPrefab.fork(450.0, 1800.0, 49.84663, 0.0), + "Rlj 60E1-1800_450-1_9 R": SwitchPrefab.fork(-450.0, -1800.0, 49.84663, 0.0), + "Rlj 60E1-1800_600-1_10 L": SwitchPrefab.fork(599.48096, 1800.0, 59.85075, 0.0), + "Rlj 60E1-1800_600-1_10 R": SwitchPrefab.fork(-599.48096, -1800.0, 59.85075, 0.0), + "Rlj 60E1-600_300-1_6 L": SwitchPrefab.fork(300.0, 600.0, 48.0, 0.0), + "Rlj 60E1-600_300-1_6 R": SwitchPrefab.fork(-300.0, -600.0, 48.0, 0.0), + "Rlj 60E1-650_190-1_20 L": SwitchPrefab.fork(190.0, 650.0, 32.47971, 0.0), + "Rlj 60E1-650_190-1_20 R": SwitchPrefab.fork(-190.0, -650.0, 32.47971, 0.0), + "Rlj 60E1-750_190-1_6 L": SwitchPrefab.fork(190.0, 750.0, 31.44976, 0.0), + "Rlj 60E1-750_190-1_6 R": SwitchPrefab.fork(-190.0, -750.0, 31.44976, 0.0), + "Rlj 60E1-800_250-1_6.5 L": SwitchPrefab.fork(250.0, 800.0, 38.23661, 0.0), + "Rlj 60E1-800_250-1_6.5 R": SwitchPrefab.fork(-250.0, -800.0, 38.23661, 0.0), + "Rlj 60E1-900_300-1_7.5 L": SwitchPrefab.fork(300.0, 900.0, 39.82378, 0.0), + "Rlj 60E1-900_300-1_7.5 R": SwitchPrefab.fork(-300.0, -900.0, 39.82378, 0.0), + "Rlj 60E1-900_300-1_9 L": SwitchPrefab.fork(300.0, 900.0, 49.84663, 0.0), + "Rlj 60E1-900_300-1_9 R": SwitchPrefab.fork(-300.0, -900.0, 49.84663, 0.0), + "Rlj 60E1-900_450-1_7.5 L": SwitchPrefab.fork(450.0, 900.0, 59.0, 0.0), + "Rlj 60E1-900_450-1_7.5 R": SwitchPrefab.fork(-450.0, -900.0, 59.0, 0.0), + "Rz 60E1-1200-1_18.5 L": SwitchPrefab.fork(1200.0, 0.0, 64.81756, 0.0), + "Rz 60E1-1200-1_18.5 R": SwitchPrefab.fork(-1200.0, 0.0, 64.81756, 0.0), + "Rz 60E1-190-1_7.5 L": SwitchPrefab.fork(190.0, 0.0, 25.221731, 0.0), + "Rz 60E1-190-1_7.5 R": SwitchPrefab.fork(-190.0, 0.0, 25.221731, 0.0), + "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, 0.0), + "Rz 60E1-2500-1_26.5 R": SwitchPrefab.fork(-2500.0, 0.0, 94.30607, 0.0), + "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, 0.0), + "Rz 60E1-300-1_9 R": SwitchPrefab.fork(-300.0, 0.0, 33.231083, 0.0), + "Rz 60E1-500-1_12 L": SwitchPrefab.fork(500.0, 0.0, 41.59458, 0.0), + "Rz 60E1-500-1_12 R": SwitchPrefab.fork(-500.0, 0.0, 41.59458, 0.0), + "Rz 60E1-760-1_14 L": SwitchPrefab.fork(760.0, 0.0, 54.21665, 0.0), + "Rz 60E1-760-1_14 R": SwitchPrefab.fork(-760.0, 0.0, 54.21665, 0.0), +}; -export default DefinedSwitches; \ No newline at end of file +export default DefinedSwitches; 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..61808be --- /dev/null +++ b/src/model/switch-descriptions/switch-prefab-track.js @@ -0,0 +1,37 @@ +export const SwitchTrackConnectionEnd = { + PREV: Symbol('SwitchTrackConnectionEnd.PREV'), + NEXT: Symbol('SwitchTrackConnectionEnd.NEXT'), +}; + +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..0791b76 --- /dev/null +++ b/src/model/switch-descriptions/switch-prefab.js @@ -0,0 +1,176 @@ +import Vector3 from "../vector3"; +import SwitchPrefabTrack, {SwitchTrackConnectionEnd, SwitchTrackConnectionType} from "./switch-prefab-track"; +import SceneryParserLog from "../scenery-parser-log"; + +export default class SwitchPrefab { + tracks = {}; + + constructor(tracks) { + Object.assign(this, { + tracks, + }); + 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 not 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) { + const tracks = { + start: new SwitchPrefabTrack( + Vector3.zero(), + Vector3.zero(), + radiusA, + 0, + [ + { + type: SwitchTrackConnectionType.EXTERNAL, + end: SwitchTrackConnectionEnd.PREV, + }, + { + type: SwitchTrackConnectionType.INTERNAL, + end: SwitchTrackConnectionEnd.NEXT, + otherEnd: SwitchTrackConnectionEnd.PREV, + internalId: 'curve_a', + }, + { + type: SwitchTrackConnectionType.INTERNAL, + end: SwitchTrackConnectionEnd.NEXT, + otherEnd: SwitchTrackConnectionEnd.PREV, + internalId: 'curve_b', + }, + ], + ), + ...SwitchPrefab._createSwitchTracks( + radiusA, + curveLength, + addedLength, + 'a', + ), + ...SwitchPrefab._createSwitchTracks( + radiusB, + curveLength, + addedLength, + 'b', + ), + }; + + return new SwitchPrefab(tracks); + } + + static _createSwitchTracks(radius, curveLength, addedLength, side) { + let curveEnd; + let endAngle; + if (radius === 0) { + curveEnd = new Vector3(0, 0, curveLength); + endAngle = 0; + } else { + const circleCenter = new Vector3(-radius, 0, 0); + endAngle = -curveLength / radius; + const centerToEnd = Vector3.fromAngleY(endAngle + Math.sign(radius) * Math.PI / 2, Math.abs(radius)); + curveEnd = circleCenter.add(centerToEnd); + } + + const tracks = { + [`curve_${side}`]: new SwitchPrefabTrack( + Vector3.zero(), + curveEnd, + radius, + side === 'a' ? 1 : 2, + [ + { + type: SwitchTrackConnectionType.INTERNAL, + end: SwitchTrackConnectionEnd.PREV, + otherEnd: SwitchTrackConnectionEnd.NEXT, + internalId: 'start', + }, + { + type: SwitchTrackConnectionType.INTERNAL, + end: SwitchTrackConnectionEnd.NEXT, + otherEnd: SwitchTrackConnectionEnd.PREV, + 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: SwitchTrackConnectionEnd.PREV, + otherEnd: SwitchTrackConnectionEnd.NEXT, + internalId: `curve_${side}`, + }, + { + type: SwitchTrackConnectionType.INTERNAL, + end: SwitchTrackConnectionEnd.NEXT, + otherEnd: SwitchTrackConnectionEnd.PREV, + internalId: `end_${side}`, + }, + ], + ); + } + + tracks[`end_${side}`] = SwitchPrefabTrack.point( + end, + addedLength > 0 ? + (side === 'a' ? 5 : 6) : + (side === 'a' ? 3 : 4), + [ + { + type: SwitchTrackConnectionType.INTERNAL, + end: SwitchTrackConnectionEnd.PREV, + otherEnd: SwitchTrackConnectionEnd.NEXT, + internalId: addedLength > 0 ? `added_${side}` : `curve_${side}`, + }, + { + type: SwitchTrackConnectionType.EXTERNAL, + end: SwitchTrackConnectionEnd.NEXT, + }, + ] + ); + + return tracks; + } +} diff --git a/src/model/switch.js b/src/model/switch.js index b19cc39..22c8797 100644 --- a/src/model/switch.js +++ b/src/model/switch.js @@ -3,7 +3,7 @@ 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 {SwitchTrackConnectionEnd, SwitchTrackConnectionType} from "./switch-descriptions/switch-prefab-track"; export default class Switch extends SceneryObject { model; @@ -17,8 +17,7 @@ export default class Switch extends SceneryObject { type = "Switch"; applied = false; track_prefab_name; - trackA; - trackB; + tracks = []; def = null; constructor(id, model, pos, rot, data, id_isolation, id_switch, maxspeed, derailspeed, track_prefab_name) { @@ -64,188 +63,105 @@ 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]); + _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}`); } - - exclude.forEach(alias => { - aliases.delete(alias); - }); - - return Array.from(aliases); + return ids; } - _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 = []) { + _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 endIds = { + [SwitchTrackConnectionEnd.PREV]: [], + [SwitchTrackConnectionEnd.NEXT]: [], + }; + 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) { + endIds[connection.end].push(otherTrackId); + } + } else if (connection.type === SwitchTrackConnectionType.EXTERNAL) { + const otherTrackId = connection.end === SwitchTrackConnectionEnd.PREV ? prevId : nextId; + if (otherTrackId) { + endIds[connection.end].push(otherTrackId); + } + } + }); 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, + endIds[SwitchTrackConnectionEnd.PREV][0], // TODO: Pass the array + endIds[SwitchTrackConnectionEnd.NEXT][0], // TODO: --||-- + 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); - }); + scenery.addTrackAlias(trackId, trackId); 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); - } } From b79530ca71bbe7c1400eaa3dc3dde92bb963804f Mon Sep 17 00:00:00 2001 From: dominik-korsa <29484605+dominik-korsa@users.noreply.github.com> Date: Sat, 12 Jul 2025 01:19:55 +0200 Subject: [PATCH 02/24] Change purpose of TrackConnection --- .../object-renderers/IsolationIdRenderer.js | 8 +-- src/helpers/constants.js | 2 - src/model/connect-tracks.js | 70 ------------------- src/model/scenery-parser.js | 8 +-- .../switch-prefab-track.js | 5 -- .../switch-descriptions/switch-prefab.js | 35 +++++----- src/model/switch.js | 19 +++-- src/model/track-connection.js | 21 +++--- src/model/track-objects/track-object.js | 3 +- src/model/tracks-connection-test.js | 70 +++++++++---------- src/model/tracks/bezier-track.js | 11 +-- src/model/tracks/point-track.js | 4 +- src/model/tracks/standard-track.js | 13 ++-- src/model/tracks/track.js | 21 +++--- 14 files changed, 106 insertions(+), 184 deletions(-) delete mode 100644 src/model/connect-tracks.js diff --git a/src/components/map/object-renderers/IsolationIdRenderer.js b/src/components/map/object-renderers/IsolationIdRenderer.js index 3a6088f..acb6cba 100644 --- a/src/components/map/object-renderers/IsolationIdRenderer.js +++ b/src/components/map/object-renderers/IsolationIdRenderer.js @@ -1,6 +1,6 @@ import { useMemo } from "react"; import SimpleLabelText from "../text/SimpleLabelText"; -import { TrackConnectionType } from "../../../model/track-connection"; +import { TrackConnectionEnd } from "../../../model/track-connection"; export default function IsolationIdRenderer(props) { const { object } = props; @@ -15,7 +15,7 @@ export default function IsolationIdRenderer(props) { text={text} x={x} y={y} - textProps={{ + textProps={{ className: "isolation-id" }} />} @@ -29,7 +29,7 @@ function IsolationEndMarker(props) { const isShown = useMemo(() => { return object.connections.some(conn => - (isStart ? conn.type === TrackConnectionType.START : conn.type === TrackConnectionType.END) && + (isStart ? conn.type === TrackConnectionEnd.START : conn.type === TrackConnectionEnd.END) && conn.otherTrack.id_isolation !== object.id_isolation ); }, [object, isStart]); @@ -46,5 +46,5 @@ function IsolationEndMarker(props) { return + /> } diff --git a/src/helpers/constants.js b/src/helpers/constants.js index b36cca1..1e66345 100644 --- a/src/helpers/constants.js +++ b/src/helpers/constants.js @@ -21,7 +21,6 @@ const Constants = { attachSignsMaxDistanceX: 0.2, attachSignsGridSize: 10, logAttachedSigns: false, - connectTracks: true, }, warnings: { all: false, // enable all warnings @@ -51,7 +50,6 @@ const Constants = { attachSignsOutOfBounds: true, signalElemsUnknownSign: true, signalElemsUnknownSignText: true, - connectTracksFailed: true, }, errors: { invalidSceneryInfo: true diff --git a/src/model/connect-tracks.js b/src/model/connect-tracks.js deleted file mode 100644 index 477da55..0000000 --- a/src/model/connect-tracks.js +++ /dev/null @@ -1,70 +0,0 @@ -import SceneryParserLog from "./scenery-parser-log"; -import TrackConnection, { TrackConnectionType } from "./track-connection"; - -export function connectTracks(scenery) { - const tracks = scenery.objects['tracks'] || {}; - - Object.values(tracks).forEach(track => { - connectTrack(scenery, track, false); - connectTrack(scenery, track, true); - }); -} - -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})`); - 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 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/scenery-parser.js b/src/model/scenery-parser.js index 51292a0..dbeb723 100644 --- a/src/model/scenery-parser.js +++ b/src/model/scenery-parser.js @@ -19,7 +19,6 @@ 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'; /* TODO: add support for WorldRotation and WorldTranslation @@ -36,7 +35,7 @@ export default class SceneryParser { 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 @@ -46,7 +45,6 @@ export default class SceneryParser { scenery.applyObjects(); 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,7 +64,7 @@ export default class SceneryParser { } return SceneryInfo.fromText(text); - } + } static _parseObject(text) { const type = text.split(";", 2)[0]; @@ -149,4 +147,4 @@ export default class SceneryParser { return Misc.fromText(Scenery.nextMiscId++, text); } } -} \ 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 index 61808be..7dc8c40 100644 --- a/src/model/switch-descriptions/switch-prefab-track.js +++ b/src/model/switch-descriptions/switch-prefab-track.js @@ -1,8 +1,3 @@ -export const SwitchTrackConnectionEnd = { - PREV: Symbol('SwitchTrackConnectionEnd.PREV'), - NEXT: Symbol('SwitchTrackConnectionEnd.NEXT'), -}; - export const SwitchTrackConnectionType = { INTERNAL: Symbol('SwitchTrackConnectionType.INTERNAL'), EXTERNAL: Symbol('SwitchTrackConnectionType.EXTERNAL'), diff --git a/src/model/switch-descriptions/switch-prefab.js b/src/model/switch-descriptions/switch-prefab.js index 0791b76..ace1e1b 100644 --- a/src/model/switch-descriptions/switch-prefab.js +++ b/src/model/switch-descriptions/switch-prefab.js @@ -1,6 +1,7 @@ import Vector3 from "../vector3"; -import SwitchPrefabTrack, {SwitchTrackConnectionEnd, SwitchTrackConnectionType} from "./switch-prefab-track"; +import SwitchPrefabTrack, {SwitchTrackConnectionType} from "./switch-prefab-track"; import SceneryParserLog from "../scenery-parser-log"; +import {TrackConnectionEnd} from "../track-connection"; export default class SwitchPrefab { tracks = {}; @@ -58,18 +59,18 @@ export default class SwitchPrefab { [ { type: SwitchTrackConnectionType.EXTERNAL, - end: SwitchTrackConnectionEnd.PREV, + end: TrackConnectionEnd.START, }, { type: SwitchTrackConnectionType.INTERNAL, - end: SwitchTrackConnectionEnd.NEXT, - otherEnd: SwitchTrackConnectionEnd.PREV, + end: TrackConnectionEnd.END, + otherEnd: TrackConnectionEnd.START, internalId: 'curve_a', }, { type: SwitchTrackConnectionType.INTERNAL, - end: SwitchTrackConnectionEnd.NEXT, - otherEnd: SwitchTrackConnectionEnd.PREV, + end: TrackConnectionEnd.END, + otherEnd: TrackConnectionEnd.START, internalId: 'curve_b', }, ], @@ -113,14 +114,14 @@ export default class SwitchPrefab { [ { type: SwitchTrackConnectionType.INTERNAL, - end: SwitchTrackConnectionEnd.PREV, - otherEnd: SwitchTrackConnectionEnd.NEXT, + end: TrackConnectionEnd.START, + otherEnd: TrackConnectionEnd.END, internalId: 'start', }, { type: SwitchTrackConnectionType.INTERNAL, - end: SwitchTrackConnectionEnd.NEXT, - otherEnd: SwitchTrackConnectionEnd.PREV, + end: TrackConnectionEnd.END, + otherEnd: TrackConnectionEnd.START, internalId: addedLength > 0 ? `added_${side}` : `end_${side}`, }, ], @@ -138,14 +139,14 @@ export default class SwitchPrefab { [ { type: SwitchTrackConnectionType.INTERNAL, - end: SwitchTrackConnectionEnd.PREV, - otherEnd: SwitchTrackConnectionEnd.NEXT, + end: TrackConnectionEnd.START, + otherEnd: TrackConnectionEnd.END, internalId: `curve_${side}`, }, { type: SwitchTrackConnectionType.INTERNAL, - end: SwitchTrackConnectionEnd.NEXT, - otherEnd: SwitchTrackConnectionEnd.PREV, + end: TrackConnectionEnd.END, + otherEnd: TrackConnectionEnd.START, internalId: `end_${side}`, }, ], @@ -160,13 +161,13 @@ export default class SwitchPrefab { [ { type: SwitchTrackConnectionType.INTERNAL, - end: SwitchTrackConnectionEnd.PREV, - otherEnd: SwitchTrackConnectionEnd.NEXT, + end: TrackConnectionEnd.START, + otherEnd: TrackConnectionEnd.END, internalId: addedLength > 0 ? `added_${side}` : `curve_${side}`, }, { type: SwitchTrackConnectionType.EXTERNAL, - end: SwitchTrackConnectionEnd.NEXT, + end: TrackConnectionEnd.END, }, ] ); diff --git a/src/model/switch.js b/src/model/switch.js index 22c8797..1352265 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 {SwitchTrackConnectionEnd, SwitchTrackConnectionType} from "./switch-descriptions/switch-prefab-track"; +import {SwitchTrackConnectionType} from "./switch-descriptions/switch-prefab-track"; +import TrackConnection, {TrackConnectionEnd} from "./track-connection"; export default class Switch extends SceneryObject { model; @@ -76,7 +77,7 @@ export default class Switch extends SceneryObject { if (!trackIds) return; const tracks = Object.entries(def.tracks).map(([key, track]) => { - return this._createSwitchTrackFromDef(scenery, def, track, trackIds) + return this._createSwitchTrackFromDef(scenery, def, track, trackIds); }); if (tracks.some(track => !track)) return; @@ -109,10 +110,7 @@ export default class Switch extends SceneryObject { } const [trackId, prevId, nextId] = ids[trackDef.dataIndex]; - const endIds = { - [SwitchTrackConnectionEnd.PREV]: [], - [SwitchTrackConnectionEnd.NEXT]: [], - }; + const connections = []; trackDef.connections.forEach((connection) => { if (connection.type === SwitchTrackConnectionType.INTERNAL) { const otherTrackDef = switchDef.tracks[connection.internalId]; @@ -132,12 +130,12 @@ export default class Switch extends SceneryObject { } const otherTrackId = ids[otherTrackDef.dataIndex][0]; if (otherTrackId) { - endIds[connection.end].push(otherTrackId); + connections.push(new TrackConnection(otherTrackId, connection.end)); } } else if (connection.type === SwitchTrackConnectionType.EXTERNAL) { - const otherTrackId = connection.end === SwitchTrackConnectionEnd.PREV ? prevId : nextId; + const otherTrackId = connection.end === TrackConnectionEnd.START ? prevId : nextId; if (otherTrackId) { - endIds[connection.end].push(otherTrackId); + connections.push(new TrackConnection(otherTrackId, connection.end)); } } }); @@ -147,8 +145,7 @@ export default class Switch extends SceneryObject { this.pos.add(trackDef.startPos.rotate(rotRad)), this.pos.add(trackDef.endPos.rotate(rotRad)), trackDef.radius, - endIds[SwitchTrackConnectionEnd.PREV][0], // TODO: Pass the array - endIds[SwitchTrackConnectionEnd.NEXT][0], // TODO: --||-- + connections, this.id_switch, 0, // start_slope 0, // end_slope diff --git a/src/model/track-connection.js b/src/model/track-connection.js index 7aab742..2949714 100644 --- a/src/model/track-connection.js +++ b/src/model/track-connection.js @@ -1,20 +1,15 @@ -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; + end; - constructor(track, otherTrack, type, otherType) { + constructor(otherTrackId, end) { Object.assign(this, { - track, - otherTrack, - type, - otherType + otherTrackId, end, }); } } diff --git a/src/model/track-objects/track-object.js b/src/model/track-objects/track-object.js index 051fc34..2070be2 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,6 +38,7 @@ export default class TrackObject extends SceneryObject { if(this.applied) return; // already applied this.applied = true; + // TODO: Is the scenery.getTrackIdByAlias needed? const trackId = scenery.getTrackIdByAlias(this.track_id); const track = scenery.getObject('tracks', trackId); if (!track) { diff --git a/src/model/tracks-connection-test.js b/src/model/tracks-connection-test.js index dec8fc9..62dc1db 100644 --- a/src/model/tracks-connection-test.js +++ b/src/model/tracks-connection-test.js @@ -1,52 +1,50 @@ import SceneryParserLog from './scenery-parser-log'; +import {TrackConnectionEnd} from "./track-connection"; 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})`); +function trackEndConnectionTest(scenery, track, connection) { + const otherTrack = scenery.getObject('tracks', connection.otherTrackId); + if (!otherTrack) { + SceneryParserLog.warn('tracksConnectionTest', `Track ${track.id} has a non-existing ${connection.end === TrackConnectionEnd.START ? 'previous' : 'next'} track: ${connection.otherTrackId}`); 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})`); + const reverseConnections = otherTrack.connections.filter( + (otherConnection) => otherConnection.type === otherConnection.otherTrackId === track.id, + ); + if (reverseConnections.length > 1) { + SceneryParserLog.warn( + 'tracksConnectionTest', + `Track ${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 not reverse connection`, + ); return; } + const reverseConnection = reverseConnections[0]; + + const ownPosition = track.getEndPos(connection.end); + const otherPosition = 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)}`, + ); + } } export function tracksConnectionTest(scenery) { const tracks = scenery.objects['tracks'] || []; Object.values(tracks).forEach(track => { - trackEndConnectionTest(scenery, track, false); - trackEndConnectionTest(scenery, track, true); + track.connections.forEach((connection) => { + trackEndConnectionTest(scenery, track, connection); + }); }); -} \ No newline at end of file +} diff --git a/src/model/tracks/bezier-track.js b/src/model/tracks/bezier-track.js index 33abd81..bcd66fd 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 { @@ -11,8 +12,8 @@ export default class BezierTrack extends Track control2: 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, @@ -42,8 +43,10 @@ export default class BezierTrack extends Track Vector3.zero(), 0, // len ?? 0, // r ?? - values[15], // nextid - values[16], // previd + [ + new TrackConnection(values[15], TrackConnectionEnd.END), + new TrackConnection(values[16], TrackConnectionEnd.START), + ], values[17], // id_station ...Track.slopesFromText(values[18]), // start_slope, end_slope values[21], // id_isolation 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/standard-track.js b/src/model/tracks/standard-track.js index 27c64b2..3600dce 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 { @@ -11,8 +12,8 @@ export default class StandardTrack extends Track circleCenter: 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(); } @@ -38,8 +39,10 @@ export default class StandardTrack extends Track Vector3.fromValuesArray(values, 6), // rot parseFloat(values[9]), // len parseFloat(values[10]), // r - values[11], // nextid - values[12], // previd + [ + new TrackConnection(values[11], TrackConnectionEnd.END), + new TrackConnection(values[12], TrackConnectionEnd.START), + ], values[13], // id_station ...Track.slopesFromText(values[14]), // start_slope, end_slope values[17], // id_isolation @@ -64,7 +67,7 @@ export default class StandardTrack extends Track 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; diff --git a/src/model/tracks/track.js b/src/model/tracks/track.js index b209c29..5f91868 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,8 +11,7 @@ export default class Track extends SceneryObject { derailspeed; category = "tracks"; type = "Track"; - nextid; - previd; + connections = []; start_slope; end_slope; prefab_name; @@ -20,18 +20,17 @@ export default class Track extends SceneryObject { 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, }); } @@ -47,6 +46,10 @@ export default class Track extends SceneryObject { 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 +59,7 @@ export default class Track extends SceneryObject { return this.points.end; } } - + getRenderBounds() { return Object.values(this.points); } From 5d7cef846fc069fd028d9d7e54921228d2089298 Mon Sep 17 00:00:00 2001 From: dominik-korsa <29484605+dominik-korsa@users.noreply.github.com> Date: Sat, 12 Jul 2025 02:02:53 +0200 Subject: [PATCH 03/24] Run track connection test and resolve some errors --- src/model/electrification-resolver.js | 34 +++++++------------ .../switch-descriptions/switch-prefab.js | 6 ++-- src/model/tracks-connection-test.js | 4 +-- src/model/tracks/bezier-track.js | 10 +++--- src/model/tracks/standard-track.js | 10 +++--- 5 files changed, 30 insertions(+), 34 deletions(-) diff --git a/src/model/electrification-resolver.js b/src/model/electrification-resolver.js index dea0702..b3bf5d2 100644 --- a/src/model/electrification-resolver.js +++ b/src/model/electrification-resolver.js @@ -26,14 +26,15 @@ export default class ElectrificationResolver { routeTracks.forEach(routeTrack => { this._propagate( - routeTrack.track, - [], + routeTrack.track, + [], routeTrack.electrified ? ElectrificationStatus.ELECTRIFIED : ElectrificationStatus.NON_ELECTRIFIED ); }); this._runResolution(); } catch (error) { + // TODO: There is no such field, should it be RESOLVING_ERROR? scenery.electrificationResolved = ElectrificationResolutionStatus.ERROR; SceneryParserLog.warn('electrificationResolverError', `Error resolving electrification: ${error.message}`); return; @@ -89,7 +90,7 @@ export default class ElectrificationResolver { if(results.length < routePoints.length) { ElectrificationResolver._passWarn( - 'electrificationMissingRouteTracks', + 'electrificationMissingRouteTracks', `While resolving electrification, found ${results.length} tracks connected to routes, but expected ${routePoints.length}` ); } @@ -103,7 +104,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; @@ -124,7 +125,7 @@ export default class ElectrificationResolver { ElectrificationResolver.propagationQueue.push({ track, skipTrackIds, status }); } - static _resolveTrack(track, skipTrackIds, status) { + static _resolveTrack(scenery, track, skipTrackIds, status) { if (track.electrificationStatus === status) return; // already set if (Constants.parser.skipElectrificationErrorsPropagation && status === ElectrificationStatus.CONFLICT) { return; @@ -137,38 +138,29 @@ 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 propagation = track.connections + .map(conn => scenery.getObject('tracks', conn.otherTrackId)) + .filter((track) => !!track); 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); }); - - // 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/switch-descriptions/switch-prefab.js b/src/model/switch-descriptions/switch-prefab.js index ace1e1b..7469fcd 100644 --- a/src/model/switch-descriptions/switch-prefab.js +++ b/src/model/switch-descriptions/switch-prefab.js @@ -33,7 +33,7 @@ export default class SwitchPrefab { if (!reverseConnection) { SceneryParserLog.warn( 'switchInvalidInternalConnection', - `In a switch prefab the internal track ${internalId} is connected to ${connection.internalId} but there is not reverse connection`, + `In a switch prefab the internal track ${internalId} is connected to ${connection.internalId} but there is no reverse connection`, ); return; } @@ -156,8 +156,8 @@ export default class SwitchPrefab { tracks[`end_${side}`] = SwitchPrefabTrack.point( end, addedLength > 0 ? - (side === 'a' ? 5 : 6) : - (side === 'a' ? 3 : 4), + (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, diff --git a/src/model/tracks-connection-test.js b/src/model/tracks-connection-test.js index 62dc1db..87fb2a7 100644 --- a/src/model/tracks-connection-test.js +++ b/src/model/tracks-connection-test.js @@ -11,7 +11,7 @@ function trackEndConnectionTest(scenery, track, connection) { } const reverseConnections = otherTrack.connections.filter( - (otherConnection) => otherConnection.type === otherConnection.otherTrackId === track.id, + (otherConnection) => otherConnection.otherTrackId === track.id, ); if (reverseConnections.length > 1) { SceneryParserLog.warn( @@ -22,7 +22,7 @@ function trackEndConnectionTest(scenery, track, connection) { if (reverseConnections.length === 0) { SceneryParserLog.warn( 'tracksConnectionTest', - `Track ${track.id} is connected to ${connection.otherTrackId} but there is not reverse connection`, + `Track ${track.id} is connected to ${connection.otherTrackId} but there is no reverse connection`, ); return; } diff --git a/src/model/tracks/bezier-track.js b/src/model/tracks/bezier-track.js index bcd66fd..ff79db7 100644 --- a/src/model/tracks/bezier-track.js +++ b/src/model/tracks/bezier-track.js @@ -33,6 +33,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 @@ -43,10 +48,7 @@ export default class BezierTrack extends Track Vector3.zero(), 0, // len ?? 0, // r ?? - [ - new TrackConnection(values[15], TrackConnectionEnd.END), - new TrackConnection(values[16], TrackConnectionEnd.START), - ], + connections, values[17], // id_station ...Track.slopesFromText(values[18]), // start_slope, end_slope values[21], // id_isolation diff --git a/src/model/tracks/standard-track.js b/src/model/tracks/standard-track.js index 3600dce..81cc799 100644 --- a/src/model/tracks/standard-track.js +++ b/src/model/tracks/standard-track.js @@ -33,16 +33,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 - [ - new TrackConnection(values[11], TrackConnectionEnd.END), - new TrackConnection(values[12], TrackConnectionEnd.START), - ], + connections, values[13], // id_station ...Track.slopesFromText(values[14]), // start_slope, end_slope values[17], // id_isolation From 4342bb41e8a896c736138476c65a348945752e15 Mon Sep 17 00:00:00 2001 From: dominik-korsa <29484605+dominik-korsa@users.noreply.github.com> Date: Sat, 12 Jul 2025 10:48:06 +0200 Subject: [PATCH 04/24] Add crossing prefab definitions --- src/model/defs/defined-switches.js | 20 +------- .../switch-descriptions/switch-prefab.js | 47 +++++++++++++++++-- 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/src/model/defs/defined-switches.js b/src/model/defs/defined-switches.js index f8f1c25..e917203 100644 --- a/src/model/defs/defined-switches.js +++ b/src/model/defs/defined-switches.js @@ -1,24 +1,8 @@ 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) - // ], + "Crossing": SwitchPrefab.crossing(33.2294, 9.0), + "Crossing4.444": SwitchPrefab.crossing(20.0, 4.444), // Rkp 60E1-190-1_9 ab // Rkp 60E1-190-1_9 ba // Rkpd 60E1-190-1_9 diff --git a/src/model/switch-descriptions/switch-prefab.js b/src/model/switch-descriptions/switch-prefab.js index 7469fcd..4b0ef2e 100644 --- a/src/model/switch-descriptions/switch-prefab.js +++ b/src/model/switch-descriptions/switch-prefab.js @@ -75,13 +75,13 @@ export default class SwitchPrefab { }, ], ), - ...SwitchPrefab._createSwitchTracks( + ...SwitchPrefab._createForkSwitchTracks( radiusA, curveLength, addedLength, 'a', ), - ...SwitchPrefab._createSwitchTracks( + ...SwitchPrefab._createForkSwitchTracks( radiusB, curveLength, addedLength, @@ -92,7 +92,7 @@ export default class SwitchPrefab { return new SwitchPrefab(tracks); } - static _createSwitchTracks(radius, curveLength, addedLength, side) { + static _createForkSwitchTracks(radius, curveLength, addedLength, side) { let curveEnd; let endAngle; if (radius === 0) { @@ -174,4 +174,45 @@ export default class SwitchPrefab { 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); + } } From d93eae6985ecbdf30929a525685637958edcaf54 Mon Sep 17 00:00:00 2001 From: dominik-korsa <29484605+dominik-korsa@users.noreply.github.com> Date: Sat, 12 Jul 2025 13:12:27 +0200 Subject: [PATCH 05/24] Add slip switch prefab definitions --- src/model/defs/defined-switches.js | 162 +++++------ .../switch-descriptions/switch-prefab.js | 266 +++++++++++++++++- 2 files changed, 337 insertions(+), 91 deletions(-) diff --git a/src/model/defs/defined-switches.js b/src/model/defs/defined-switches.js index e917203..48a2260 100644 --- a/src/model/defs/defined-switches.js +++ b/src/model/defs/defined-switches.js @@ -3,92 +3,94 @@ import SwitchPrefab from "../switch-descriptions/switch-prefab"; const DefinedSwitches = { "Crossing": SwitchPrefab.crossing(33.2294, 9.0), "Crossing4.444": SwitchPrefab.crossing(20.0, 4.444), - // Rkp 60E1-190-1_9 ab - // Rkp 60E1-190-1_9 ba - // Rkpd 60E1-190-1_9 - "Rld 60E1-1200_600-1_15 L": SwitchPrefab.fork(600.0, -1200.0, 39.95566, 0.0), - "Rld 60E1-1200_600-1_15 R": SwitchPrefab.fork(-600.0, 1200.0, 39.95566, 0.0), - "Rld 60E1-1200_900-1_18.5 L": SwitchPrefab.fork(900.0, -1200.0, 48.61317, 0.0), - "Rld 60E1-1200_900-1_18.5 R": SwitchPrefab.fork(-900.0, 1200.0, 48.61317, 0.0), - "Rld 60E1-1800_300-1_9 L": SwitchPrefab.fork(300.0, -1800.0, 33.23108, 0.0), - "Rld 60E1-1800_300-1_9 R": SwitchPrefab.fork(-300.0, 1800.0, 33.23108, 0.0), - "Rld 60E1-1800_600-1_14 L": SwitchPrefab.fork(600.0, -1800.0, 42.80262, 0.0), - "Rld 60E1-1800_600-1_14 R": SwitchPrefab.fork(-600.0, 1800.0, 42.80262, 0.0), - "Rld 60E1-1800_900-1_18.5 L": SwitchPrefab.fork(900.0, -1800.0, 48.61317, 0.0), - "Rld 60E1-1800_900-1_18.5 R": SwitchPrefab.fork(-900.0, 1800.0, 48.61317, 0.0), - "Rld 60E1-2500_1200-1_22 L": SwitchPrefab.fork(1200.0, -2500.0, 54.51731, 0.0), - "Rld 60E1-2500_1200-1_22 R": SwitchPrefab.fork(-1200.0, 2500.0, 54.51731, 0.0), - "Rld 60E1-2500_250-1_8.5 L": SwitchPrefab.fork(250.0, -2500.0, 29.31069, 0.0), - "Rld 60E1-2500_250-1_8.5 R": SwitchPrefab.fork(-250.0, 2500.0, 29.31069, 0.0), - "Rld 60E1-2500_400-1_10.5 L": SwitchPrefab.fork(400.0, -2500.0, 38.00925, 0.0), - "Rld 60E1-2500_400-1_10.5 R": SwitchPrefab.fork(-400.0, 2500.0, 38.00925, 0.0), - "Rld 60E1-2500_600-1_14 L": SwitchPrefab.fork(600.0, -2500.0, 42.80262, 0.0), - "Rld 60E1-2500_600-1_14 R": SwitchPrefab.fork(-600.0, 2500.0, 42.80262, 0.0), - "Rld 60E1-2500_900-1_17 L": SwitchPrefab.fork(900.0, -2500.0, 52.14928, 0.0), - "Rld 60E1-2500_900-1_17 R": SwitchPrefab.fork(-900.0, 2500.0, 52.14928, 0.0), - "Rld 60E1-600_300-1_9 L": SwitchPrefab.fork(300.0, -600.0, 29.92537, 0.0), - "Rld 60E1-600_300-1_9 R": SwitchPrefab.fork(-300.0, 600.0, 29.92537, 0.0), - "Rld 60E1-650_450-1_15 L": SwitchPrefab.fork(450.0, -650.0, 29.96674, 0.0), - "Rld 60E1-650_450-1_15 R": SwitchPrefab.fork(-450.0, 650.0, 29.96674, 0.0), - "Rld 60E1-700_190-1_7.5 L": SwitchPrefab.fork(190.0, -700.0, 25.22173, 0.0), - "Rld 60E1-700_190-1_7.5 R": SwitchPrefab.fork(-190.0, 700.0, 25.22173, 0.0), - "Rld 60E1-700_300-1_10 L": SwitchPrefab.fork(300.0, -700.0, 29.38637, 0.0), - "Rld 60E1-700_300-1_10 R": SwitchPrefab.fork(-300.0, 700.0, 29.38637, 0.0), - "Rld 60E1-700_500-1_14 L": SwitchPrefab.fork(500.0, -700.0, 35.66885, 0.0), - "Rld 60E1-700_500-1_14 R": SwitchPrefab.fork(-500.0, 700.0, 35.66885, 0.0), - "Rld 60E1-900_300-1_9 L": SwitchPrefab.fork(300.0, -900.0, 33.23108, 0.0), - "Rld 60E1-900_300-1_9 R": SwitchPrefab.fork(-300.0, 900.0, 33.23108, 0.0), - "Rld 60E1-900_450-1_12 L": SwitchPrefab.fork(450.0, -900.0, 37.43512, 0.0), - "Rld 60E1-900_450-1_12 R": SwitchPrefab.fork(-450.0, 900.0, 37.43512, 0.0), - "Rld 60E1-900_600-1_15 L": SwitchPrefab.fork(600.0, -900.0, 39.95566, 0.0), - "Rld 60E1-900_600-1_15 R": SwitchPrefab.fork(-600.0, 900.0, 39.95566, 0.0), - "Rlds 60E1-1000-1000-1_23": SwitchPrefab.fork(1000.0, -1000.0, 43.45773, 0.0), - "Rlds 60E1-190-190-1_9": SwitchPrefab.fork(190.0, -190.0, 21.23, 0.0), - "Rlds 60E1-600-600-1_18.5": SwitchPrefab.fork(599.9205, -600.0, 33.22262, 0.0), - "Rlj 60E1-1200_300-1_7 L": SwitchPrefab.fork(300.0, 1200.0, 42.64069, 0.0), - "Rlj 60E1-1200_300-1_7 R": SwitchPrefab.fork(-300.0, -1200.0, 42.64069, 0.0), - "Rlj 60E1-1200_600-1_9 L": SwitchPrefab.fork(600.0, 1200.0, 68.0, 0.0), - "Rlj 60E1-1200_600-1_9 R": SwitchPrefab.fork(-600.0, -1200.0, 68.0, 0.0), - "Rlj 60E1-1500_450-1_9 L": SwitchPrefab.fork(450.0, 1500.0, 49.84663, 0.0), - "Rlj 60E1-1500_450-1_9 R": SwitchPrefab.fork(-450.0, -1500.0, 49.84663, 0.0), - "Rlj 60E1-1800_300-1_7.5 L": SwitchPrefab.fork(300.0, 1800.0, 39.0, 0.0), - "Rlj 60E1-1800_300-1_7.5 R": SwitchPrefab.fork(-300.0, -1800.0, 39.0, 0.0), - "Rlj 60E1-1800_450-1_9 L": SwitchPrefab.fork(450.0, 1800.0, 49.84663, 0.0), - "Rlj 60E1-1800_450-1_9 R": SwitchPrefab.fork(-450.0, -1800.0, 49.84663, 0.0), - "Rlj 60E1-1800_600-1_10 L": SwitchPrefab.fork(599.48096, 1800.0, 59.85075, 0.0), - "Rlj 60E1-1800_600-1_10 R": SwitchPrefab.fork(-599.48096, -1800.0, 59.85075, 0.0), - "Rlj 60E1-600_300-1_6 L": SwitchPrefab.fork(300.0, 600.0, 48.0, 0.0), - "Rlj 60E1-600_300-1_6 R": SwitchPrefab.fork(-300.0, -600.0, 48.0, 0.0), - "Rlj 60E1-650_190-1_20 L": SwitchPrefab.fork(190.0, 650.0, 32.47971, 0.0), - "Rlj 60E1-650_190-1_20 R": SwitchPrefab.fork(-190.0, -650.0, 32.47971, 0.0), - "Rlj 60E1-750_190-1_6 L": SwitchPrefab.fork(190.0, 750.0, 31.44976, 0.0), - "Rlj 60E1-750_190-1_6 R": SwitchPrefab.fork(-190.0, -750.0, 31.44976, 0.0), - "Rlj 60E1-800_250-1_6.5 L": SwitchPrefab.fork(250.0, 800.0, 38.23661, 0.0), - "Rlj 60E1-800_250-1_6.5 R": SwitchPrefab.fork(-250.0, -800.0, 38.23661, 0.0), - "Rlj 60E1-900_300-1_7.5 L": SwitchPrefab.fork(300.0, 900.0, 39.82378, 0.0), - "Rlj 60E1-900_300-1_7.5 R": SwitchPrefab.fork(-300.0, -900.0, 39.82378, 0.0), - "Rlj 60E1-900_300-1_9 L": SwitchPrefab.fork(300.0, 900.0, 49.84663, 0.0), - "Rlj 60E1-900_300-1_9 R": SwitchPrefab.fork(-300.0, -900.0, 49.84663, 0.0), - "Rlj 60E1-900_450-1_7.5 L": SwitchPrefab.fork(450.0, 900.0, 59.0, 0.0), - "Rlj 60E1-900_450-1_7.5 R": SwitchPrefab.fork(-450.0, -900.0, 59.0, 0.0), - "Rz 60E1-1200-1_18.5 L": SwitchPrefab.fork(1200.0, 0.0, 64.81756, 0.0), - "Rz 60E1-1200-1_18.5 R": SwitchPrefab.fork(-1200.0, 0.0, 64.81756, 0.0), - "Rz 60E1-190-1_7.5 L": SwitchPrefab.fork(190.0, 0.0, 25.221731, 0.0), - "Rz 60E1-190-1_7.5 R": SwitchPrefab.fork(-190.0, 0.0, 25.221731, 0.0), + + "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, 0.0), - "Rz 60E1-2500-1_26.5 R": SwitchPrefab.fork(-2500.0, 0.0, 94.30607, 0.0), + "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, 0.0), - "Rz 60E1-300-1_9 R": SwitchPrefab.fork(-300.0, 0.0, 33.231083, 0.0), - "Rz 60E1-500-1_12 L": SwitchPrefab.fork(500.0, 0.0, 41.59458, 0.0), - "Rz 60E1-500-1_12 R": SwitchPrefab.fork(-500.0, 0.0, 41.59458, 0.0), - "Rz 60E1-760-1_14 L": SwitchPrefab.fork(760.0, 0.0, 54.21665, 0.0), - "Rz 60E1-760-1_14 R": SwitchPrefab.fork(-760.0, 0.0, 54.21665, 0.0), + "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/switch-descriptions/switch-prefab.js b/src/model/switch-descriptions/switch-prefab.js index 4b0ef2e..a6161b3 100644 --- a/src/model/switch-descriptions/switch-prefab.js +++ b/src/model/switch-descriptions/switch-prefab.js @@ -49,7 +49,7 @@ export default class SwitchPrefab { }); } - static fork(radiusA, radiusB, curveLength, addedLength) { + static fork(radiusA, radiusB, curveLength, addedLength = 0.0) { const tracks = { start: new SwitchPrefabTrack( Vector3.zero(), @@ -92,18 +92,20 @@ export default class SwitchPrefab { return new SwitchPrefab(tracks); } - static _createForkSwitchTracks(radius, curveLength, addedLength, side) { - let curveEnd; - let endAngle; + static _calculateCurveEnd(startPos, startAngle, radius, curveLength) { if (radius === 0) { - curveEnd = new Vector3(0, 0, curveLength); - endAngle = 0; - } else { - const circleCenter = new Vector3(-radius, 0, 0); - endAngle = -curveLength / radius; - const centerToEnd = Vector3.fromAngleY(endAngle + Math.sign(radius) * Math.PI / 2, Math.abs(radius)); - curveEnd = circleCenter.add(centerToEnd); + return { endPos: startPos, 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( @@ -215,4 +217,246 @@ export default class SwitchPrefab { return new SwitchPrefab(tracks); } + + 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); + } } From 0b6514a6f820bc23f77de57fc3e3c3beaa3f05d8 Mon Sep 17 00:00:00 2001 From: dominik-korsa <29484605+dominik-korsa@users.noreply.github.com> Date: Sat, 12 Jul 2025 13:19:54 +0200 Subject: [PATCH 06/24] Fix straight switch tracks --- src/model/switch-descriptions/switch-prefab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/switch-descriptions/switch-prefab.js b/src/model/switch-descriptions/switch-prefab.js index a6161b3..6d2f623 100644 --- a/src/model/switch-descriptions/switch-prefab.js +++ b/src/model/switch-descriptions/switch-prefab.js @@ -94,7 +94,7 @@ export default class SwitchPrefab { static _calculateCurveEnd(startPos, startAngle, radius, curveLength) { if (radius === 0) { - return { endPos: startPos, endAngle: startAngle }; + 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()); From c4eac723ba42636889a0baa59f8fdc367aaf1439 Mon Sep 17 00:00:00 2001 From: dominik-korsa <29484605+dominik-korsa@users.noreply.github.com> Date: Sat, 12 Jul 2025 14:00:50 +0200 Subject: [PATCH 07/24] Add route parsing --- src/helpers/constants.js | 1 + src/model/route.js | 9 ++++-- src/model/scenery-parser.js | 58 ++++++++++++++++++++++++++++++++----- 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/helpers/constants.js b/src/helpers/constants.js index 1e66345..ac8e816 100644 --- a/src/helpers/constants.js +++ b/src/helpers/constants.js @@ -50,6 +50,7 @@ const Constants = { attachSignsOutOfBounds: true, signalElemsUnknownSign: true, signalElemsUnknownSignText: true, + routeInvalidSegment: true, }, errors: { invalidSceneryInfo: true diff --git a/src/model/route.js b/src/model/route.js index 511692a..11676af 100644 --- a/src/model/route.js +++ b/src/model/route.js @@ -9,6 +9,7 @@ export default class Route extends SceneryObject { category = "routes"; type = "Route"; points = []; + segments = []; constructor(prefab_name, pos, rot, track_count, route_name, track_offset, electrified) { super(route_name, pos, rot); @@ -24,7 +25,7 @@ export default class Route extends SceneryObject { this.points = this._getConnectionPoints(); } - static fromText(text) { + static fromText(text) { const values = text.split(";"); const route = new Route( values[2], // prefab_name @@ -39,6 +40,10 @@ export default class Route extends SceneryObject { return route; } + addSegment(length, radius, trackIds) { + this.segments.push({ length, radius, trackIds }); + } + _getConnectionPoints() { if (this.track_count === 1) { return [this.pos.clone()]; @@ -53,4 +58,4 @@ export default class Route extends SceneryObject { this.pos.add(leftTrackOffset) ]; } -} \ No newline at end of file +} diff --git a/src/model/scenery-parser.js b/src/model/scenery-parser.js index dbeb723..0e940cd 100644 --- a/src/model/scenery-parser.js +++ b/src/model/scenery-parser.js @@ -29,6 +29,8 @@ 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); @@ -37,9 +39,21 @@ export default class SceneryParser { } 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(); @@ -69,7 +83,8 @@ export default class SceneryParser { static _parseObject(text) { const type = text.split(";", 2)[0]; - if(type.indexOf("Forest") !== -1 || type.indexOf("Empty") !== -1) { + if (type === '' || 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; } @@ -88,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': @@ -101,12 +115,11 @@ export default class SceneryParser { case 'SSPController': case 'TerrainGroup': case 'EndTerrainGroup': - case '': return null; default: SceneryParserLog.warn('unknownObjectType', `Unknown object type: ${type}`); return null; - }; + } } static _parseTrack(text) { @@ -147,4 +160,35 @@ export default class SceneryParser { return Misc.fromText(Scenery.nextMiscId++, text); } } + + // Returns true if EndRoute was found, false otherwise + static _parseRouteLine(text, route) { + if (text === 'EndRoute') return true; + const fields = text.split(';'); + if (fields.length < 4) { + SceneryParserLog.warn('routeInvalidSegment', `Invalid route segment: "${text}", not enough fields`); + return; + } + const trackIds = fields[3] + .split(',') + .map(segment => { + const id = segment.split(':')[0]; + if (!id) return null; + return parseInt(id); + }); + if (trackIds.some(id => id === null || isNaN(id))) { + 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 + trackIds, + ); + return false; + } } From e78c1ccde50941917ae94e771db21f8c0616bac8 Mon Sep 17 00:00:00 2001 From: dominik-korsa <29484605+dominik-korsa@users.noreply.github.com> Date: Sat, 12 Jul 2025 15:36:13 +0200 Subject: [PATCH 08/24] Add route track rendering --- .../map/object-renderers/TrackRenderer.js | 13 ++- .../track-hover-info/TrackHoverInfoConsts.js | 1 + src/helpers/constants.js | 1 + src/model/connect-routes.js | 24 ++++ src/model/route.js | 103 +++++++++++++++--- src/model/scenery-parser.js | 6 +- src/model/tracks/route-track.js | 42 +++++++ 7 files changed, 166 insertions(+), 24 deletions(-) create mode 100644 src/model/connect-routes.js create mode 100644 src/model/tracks/route-track.js 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; + } +} From 60e2f1f8fa2197f24a39f3809333f8933f6d2b6f Mon Sep 17 00:00:00 2001 From: dominik-korsa <29484605+dominik-korsa@users.noreply.github.com> Date: Sat, 12 Jul 2025 16:04:58 +0200 Subject: [PATCH 09/24] Properly connect route tracks --- src/model/route.js | 17 +++++++++++++++-- src/model/scenery-parser.js | 6 +++--- src/model/tracks/route-track.js | 2 +- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/model/route.js b/src/model/route.js index 119c263..522b92b 100644 --- a/src/model/route.js +++ b/src/model/route.js @@ -2,6 +2,7 @@ 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"; export default class Route extends SceneryObject { track_count; @@ -101,18 +102,30 @@ export default class Route extends SceneryObject { 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.id, TrackConnectionEnd.START)); + prevTrack.connections.push(new TrackConnection(track.id, TrackConnectionEnd.END)); + }); + } + this.segments.push({ length, radius, trackIds, tracks }); } // This method is future-proofed for more than 2 tracks _getOffsets() { - const leftmost_offset = -(this.track_count - 1) * 2 + this.track_offset; + const 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); + result.push(leftmost_offset - i * 4); } return result; } diff --git a/src/model/scenery-parser.js b/src/model/scenery-parser.js index 9aad6de..3ce7890 100644 --- a/src/model/scenery-parser.js +++ b/src/model/scenery-parser.js @@ -174,9 +174,9 @@ export default class SceneryParser { const trackIds = fields[3] .split(',') .map(segment => { - const id = segment.split(':')[0]; - if (!id) return null; - return parseInt(id); + 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]}`); diff --git a/src/model/tracks/route-track.js b/src/model/tracks/route-track.js index b4236d9..ac93dfa 100644 --- a/src/model/tracks/route-track.js +++ b/src/model/tracks/route-track.js @@ -7,7 +7,7 @@ export default class RouteTrack extends Track { points; route; - constructor(id, start, end, rot, len, r, connections, route, electrified) { + constructor(id, start, end, rot, len, r, connections, electrified, route) { super( id, start, rot, len, r, connections, null, // id_station, From 68f4504a4ade040ce22f1aec75f98e112002a4b8 Mon Sep 17 00:00:00 2001 From: dominik-korsa <29484605+dominik-korsa@users.noreply.github.com> Date: Sat, 12 Jul 2025 16:47:55 +0200 Subject: [PATCH 10/24] Update electrification resolution --- src/helpers/constants.js | 1 - src/model/electrification-resolver.js | 94 ++++++++------------------- 2 files changed, 27 insertions(+), 68 deletions(-) diff --git a/src/helpers/constants.js b/src/helpers/constants.js index 18e3837..467c8d5 100644 --- a/src/helpers/constants.js +++ b/src/helpers/constants.js @@ -42,7 +42,6 @@ const Constants = { tracksConnectionTest: true, electrificationNevpNotApplied: false, electrificationConflict: false, - electrificationMissingRouteTracks: true, electrificationResolverError: true, electrificationResolverWarnings: true, signalElemsUnknownPrefab: true, diff --git a/src/model/electrification-resolver.js b/src/model/electrification-resolver.js index b3bf5d2..34f0037 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,22 +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(); + this._runResolution(scenery); } catch (error) { - // TODO: There is no such field, should it be RESOLVING_ERROR? - scenery.electrificationResolved = ElectrificationResolutionStatus.ERROR; + scenery.electrificationResolved = ElectrificationResolutionStatus.RESOLVING_ERROR; SceneryParserLog.warn('electrificationResolverError', `Error resolving electrification: ${error.message}`); + console.warn(error); return; } @@ -54,48 +44,19 @@ 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) => { + const otherTrack = scenery.getObject('tracks', connection.otherTrackId); + if (!otherTrack || otherTrack.type === 'RouteTrack') return null; + return { track: 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) { @@ -114,18 +75,18 @@ export default class ElectrificationResolver { }); } - static _runResolution() { + static _runResolution(scenery) { 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(scenery, 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(scenery, track, skipTrackIds, status) { + static _resolveTrack(scenery, track, skipTrackId, status) { if (track.electrificationStatus === status) return; // already set if (Constants.parser.skipElectrificationErrorsPropagation && status === ElectrificationStatus.CONFLICT) { return; @@ -156,11 +117,10 @@ export default class ElectrificationResolver { const propagation = track.connections .map(conn => scenery.getObject('tracks', conn.otherTrackId)) .filter((track) => !!track); - const nextSkipIds = [track.id]; propagation.forEach(nextTrack => { - if (skipTrackIds.includes(nextTrack.id)) return; - this._propagate(nextTrack, nextSkipIds, track.electrificationStatus); + if (skipTrackId === nextTrack.id) return; + this._propagate(nextTrack, track.id, track.electrificationStatus); }); } }; From d6c0582c8448427db809cf96f66c3164e639a7f9 Mon Sep 17 00:00:00 2001 From: dominik-korsa <29484605+dominik-korsa@users.noreply.github.com> Date: Sat, 12 Jul 2025 16:58:17 +0200 Subject: [PATCH 11/24] Remove track aliases --- src/helpers/constants.js | 2 -- src/model/scenery.js | 26 ++----------------------- src/model/switch.js | 1 - src/model/track-objects/track-object.js | 7 ++----- src/model/tracks/track.js | 1 - 5 files changed, 4 insertions(+), 33 deletions(-) diff --git a/src/helpers/constants.js b/src/helpers/constants.js index 467c8d5..4785357 100644 --- a/src/helpers/constants.js +++ b/src/helpers/constants.js @@ -29,8 +29,6 @@ const Constants = { unknownObjectType: true, unknownTrackType: true, spawnWithoutSpawnInfo: true, - trackAliasAlreadyExists: true, - trackAliasNoTrack: true, switchUndefinedModel: true, switchInvalidDataFormat: true, switchNoModel: true, diff --git a/src/model/scenery.js b/src/model/scenery.js index 08737da..5a4eb40 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,7 +34,7 @@ export default class Scenery this._updateBounds(object.getRenderBounds()); } } - + applyObjects() { Object.keys(this.objects).forEach(category => { Object.values(this.objects[category]).forEach(object => { @@ -52,26 +50,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; @@ -83,4 +61,4 @@ export default class Scenery } ); } -} \ No newline at end of file +} diff --git a/src/model/switch.js b/src/model/switch.js index 1352265..0950a52 100644 --- a/src/model/switch.js +++ b/src/model/switch.js @@ -157,7 +157,6 @@ export default class Switch extends SceneryObject { trackObj.switch = this; scenery.addObject(trackObj); - scenery.addTrackAlias(trackId, trackId); return trackObj; } diff --git a/src/model/track-objects/track-object.js b/src/model/track-objects/track-object.js index 2070be2..5a60440 100644 --- a/src/model/track-objects/track-object.js +++ b/src/model/track-objects/track-object.js @@ -38,14 +38,11 @@ export default class TrackObject extends SceneryObject { if(this.applied) return; // already applied this.applied = true; - // TODO: Is the scenery.getTrackIdByAlias needed? - 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/track.js b/src/model/tracks/track.js index 5f91868..71c35ca 100644 --- a/src/model/tracks/track.js +++ b/src/model/tracks/track.js @@ -16,7 +16,6 @@ export default class Track extends SceneryObject { end_slope; prefab_name; hide_isolation = false; - aliases = []; switch = null; electrificationStatus = ElectrificationStatus.NOT_CHECKED; hasNEVP = false; From f9ef9de7ef1b4335cbeda9ddf20d4276c69ab226 Mon Sep 17 00:00:00 2001 From: dominik-korsa <29484605+dominik-korsa@users.noreply.github.com> Date: Sat, 12 Jul 2025 17:07:40 +0200 Subject: [PATCH 12/24] Do not render 0-length tracks --- src/components/map/object-renderers/TrackRenderer.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/map/object-renderers/TrackRenderer.js b/src/components/map/object-renderers/TrackRenderer.js index cc8045a..c027890 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); }; From 5eca3d4558708ad433f06836b8440e251cd57c72 Mon Sep 17 00:00:00 2001 From: dominik-korsa <29484605+dominik-korsa@users.noreply.github.com> Date: Sat, 12 Jul 2025 17:13:19 +0200 Subject: [PATCH 13/24] Change position of the route arrow --- src/components/map/object-renderers/RouteRenderer.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/map/object-renderers/RouteRenderer.js b/src/components/map/object-renderers/RouteRenderer.js index 474ead5..a208bb9 100644 --- a/src/components/map/object-renderers/RouteRenderer.js +++ b/src/components/map/object-renderers/RouteRenderer.js @@ -3,13 +3,14 @@ import AlwaysUpText from "../text/AlwaysUpText"; export default function RouteRenderer(props) { const { object } = props; - const [x, y] = object.pos.toSVGCoords(); + const [x, y] = object.end_center.toSVGCoords(); + const angle = object.end_angle_rad * 180 / Math.PI; const offY = object.track_count === 2 ? -22 : -20; const arrowOffset = - object.track_offset - 37.8; return ( - + Date: Sat, 12 Jul 2025 18:19:55 +0200 Subject: [PATCH 14/24] Store reference to otherTrack in TrackConnection --- .../object-renderers/IsolationIdRenderer.js | 25 +++--- src/helpers/constants.js | 1 + src/model/connect-routes.js | 24 ------ src/model/connect-tracks.js | 83 +++++++++++++++++++ src/model/electrification-resolver.js | 25 +++--- src/model/route.js | 4 +- src/model/scenery-parser.js | 6 +- src/model/track-connection.js | 18 +++- src/model/tracks-connection-test.js | 50 ----------- src/model/tracks/track.js | 5 ++ 10 files changed, 132 insertions(+), 109 deletions(-) delete mode 100644 src/model/connect-routes.js create mode 100644 src/model/connect-tracks.js delete mode 100644 src/model/tracks-connection-test.js diff --git a/src/components/map/object-renderers/IsolationIdRenderer.js b/src/components/map/object-renderers/IsolationIdRenderer.js index acb6cba..d802f65 100644 --- a/src/components/map/object-renderers/IsolationIdRenderer.js +++ b/src/components/map/object-renderers/IsolationIdRenderer.js @@ -19,26 +19,31 @@ export default function IsolationIdRenderer(props) { className: "isolation-id" }} />} - - + + ); } function IsolationEndMarker(props) { - const { object, isStart = false } = props; + const { object, end = false } = props; const isShown = useMemo(() => { - return object.connections.some(conn => - (isStart ? conn.type === TrackConnectionEnd.START : conn.type === TrackConnectionEnd.END) && - conn.otherTrack.id_isolation !== object.id_isolation - ); - }, [object, isStart]); + 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 = isStart ? object.points.start : object.points.end; + const pos = object.getEndPos(end); const [x, y] = pos.toSVGCoords(); - const angle = isStart ? object.getStartAngleXZ() : object.getEndAngleXZ(); + const angle = object.getAngleXZForEnd(end); const cos = Math.cos(angle); const sin = Math.sin(angle); diff --git a/src/helpers/constants.js b/src/helpers/constants.js index 4785357..e5d2ad4 100644 --- a/src/helpers/constants.js +++ b/src/helpers/constants.js @@ -47,6 +47,7 @@ const Constants = { attachSignsOutOfBounds: true, signalElemsUnknownSign: true, signalElemsUnknownSignText: true, + connectTracksFailed: true, routeInvalidSegment: true, }, errors: { diff --git a/src/model/connect-routes.js b/src/model/connect-routes.js deleted file mode 100644 index 12cbadd..0000000 --- a/src/model/connect-routes.js +++ /dev/null @@ -1,24 +0,0 @@ -import TrackConnection, {TrackConnectionEnd} from "./track-connection"; - -export 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.id, TrackConnectionEnd.START)); - }); - }); -} diff --git a/src/model/connect-tracks.js b/src/model/connect-tracks.js new file mode 100644 index 0000000..94961de --- /dev/null +++ b/src/model/connect-tracks.js @@ -0,0 +1,83 @@ +import TrackConnection, {TrackConnectionEnd} from "./track-connection"; +import SceneryParserLog from "./scenery-parser-log"; +import Constants from "../helpers/constants"; + +const connectionThresholdDistanceSq = 0.05; + +export function connectTracks(scenery) { + _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 _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 ${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 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)}`, + ); + } +} diff --git a/src/model/electrification-resolver.js b/src/model/electrification-resolver.js index 34f0037..9cebb88 100644 --- a/src/model/electrification-resolver.js +++ b/src/model/electrification-resolver.js @@ -22,7 +22,7 @@ export default class ElectrificationResolver { ); }); - this._runResolution(scenery); + this._runResolution(); } catch (error) { scenery.electrificationResolved = ElectrificationResolutionStatus.RESOLVING_ERROR; SceneryParserLog.warn('electrificationResolverError', `Error resolving electrification: ${error.message}`); @@ -50,11 +50,10 @@ export default class ElectrificationResolver { return route.segments[0].tracks.flatMap((routeTrack) => { return routeTrack.connections .map((connection) => { - const otherTrack = scenery.getObject('tracks', connection.otherTrackId); - if (!otherTrack || otherTrack.type === 'RouteTrack') return null; - return { track: otherTrack, routeTrack }; + if (connection.otherTrack.type === 'RouteTrack') return null; + return { track: connection.otherTrack, routeTrack }; }) - .filter((adjacent) => adjacent !== null) + .filter((adjacent) => adjacent !== null); }); }); } @@ -75,10 +74,10 @@ export default class ElectrificationResolver { }); } - static _runResolution(scenery) { + static _runResolution() { while(ElectrificationResolver.propagationQueue.length > 0) { const { track, skipTrackId, status } = ElectrificationResolver.propagationQueue.pop(); - ElectrificationResolver._resolveTrack(scenery, track, skipTrackId, status); + ElectrificationResolver._resolveTrack(track, skipTrackId, status); } } @@ -86,7 +85,7 @@ export default class ElectrificationResolver { ElectrificationResolver.propagationQueue.push({ track, skipTrackId, status }); } - static _resolveTrack(scenery, track, skipTrackId, status) { + static _resolveTrack(track, skipTrackId, status) { if (track.electrificationStatus === status) return; // already set if (Constants.parser.skipElectrificationErrorsPropagation && status === ElectrificationStatus.CONFLICT) { return; @@ -114,13 +113,9 @@ export default class ElectrificationResolver { return; } - const propagation = track.connections - .map(conn => scenery.getObject('tracks', conn.otherTrackId)) - .filter((track) => !!track); - - propagation.forEach(nextTrack => { - if (skipTrackId === nextTrack.id) return; - this._propagate(nextTrack, track.id, track.electrificationStatus); + track.connections.forEach(connection => { + if (skipTrackId === connection.otherTrackId) return; + this._propagate(connection.otherTrack, track.id, track.electrificationStatus); }); } }; diff --git a/src/model/route.js b/src/model/route.js index 522b92b..7136391 100644 --- a/src/model/route.js +++ b/src/model/route.js @@ -112,8 +112,8 @@ export default class Route extends SceneryObject { const newSegment = this.segments[this.segments.length - 1]; newSegment.tracks.forEach((track, index) => { const prevTrack = prevSegment.tracks[index]; - track.connections.push(new TrackConnection(prevTrack.id, TrackConnectionEnd.START)); - prevTrack.connections.push(new TrackConnection(track.id, TrackConnectionEnd.END)); + track.connections.push(new TrackConnection(prevTrack, TrackConnectionEnd.START)); + prevTrack.connections.push(new TrackConnection(track, TrackConnectionEnd.END)); }); } diff --git a/src/model/scenery-parser.js b/src/model/scenery-parser.js index 3ce7890..3ca046e 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 {connectRoutes} from "./connect-routes"; +import {connectTracks} from "./connect-tracks"; /* TODO: add support for WorldRotation and WorldTranslation @@ -58,9 +57,8 @@ export default class SceneryParser { }); scenery.applyObjects(); - connectRoutes(scenery); + connectTracks(scenery); - if(Constants.parser.runTracksConnectionTest) tracksConnectionTest(scenery); if(Constants.parser.resolveElectrification) ElectrificationResolver.resolveScenery(scenery); if(Constants.parser.attachSigns) attachSigns(scenery); if(Constants.parser.logSceneryAfterFinished) console.log(scenery); diff --git a/src/model/track-connection.js b/src/model/track-connection.js index 2949714..4f21817 100644 --- a/src/model/track-connection.js +++ b/src/model/track-connection.js @@ -5,11 +5,21 @@ export const TrackConnectionEnd = { export default class TrackConnection { otherTrackId; + otherTrack = null; end; - constructor(otherTrackId, end) { - Object.assign(this, { - otherTrackId, end, - }); + /** + * 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/tracks-connection-test.js b/src/model/tracks-connection-test.js deleted file mode 100644 index 87fb2a7..0000000 --- a/src/model/tracks-connection-test.js +++ /dev/null @@ -1,50 +0,0 @@ -import SceneryParserLog from './scenery-parser-log'; -import {TrackConnectionEnd} from "./track-connection"; - -const connectionThresholdDistanceSq = 0.05; - -function trackEndConnectionTest(scenery, track, connection) { - const otherTrack = scenery.getObject('tracks', connection.otherTrackId); - if (!otherTrack) { - SceneryParserLog.warn('tracksConnectionTest', `Track ${track.id} has a non-existing ${connection.end === TrackConnectionEnd.START ? 'previous' : 'next'} track: ${connection.otherTrackId}`); - return; - } - - const reverseConnections = otherTrack.connections.filter( - (otherConnection) => otherConnection.otherTrackId === track.id, - ); - if (reverseConnections.length > 1) { - SceneryParserLog.warn( - 'tracksConnectionTest', - `Track ${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 reverseConnection = reverseConnections[0]; - - const ownPosition = track.getEndPos(connection.end); - const otherPosition = 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)}`, - ); - } -} - -export function tracksConnectionTest(scenery) { - const tracks = scenery.objects['tracks'] || []; - - Object.values(tracks).forEach(track => { - track.connections.forEach((connection) => { - trackEndConnectionTest(scenery, track, connection); - }); - }); -} diff --git a/src/model/tracks/track.js b/src/model/tracks/track.js index 71c35ca..711517e 100644 --- a/src/model/tracks/track.js +++ b/src/model/tracks/track.js @@ -41,6 +41,11 @@ 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); } From b137af2843d62d5b3699b766f61e60bdad74a4c7 Mon Sep 17 00:00:00 2001 From: dominik-korsa <29484605+dominik-korsa@users.noreply.github.com> Date: Sat, 12 Jul 2025 18:28:57 +0200 Subject: [PATCH 15/24] Don't mark route tracks as derailing tracks --- src/components/map/object-renderers/TrackRenderer.js | 3 ++- src/helpers/constants.js | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/map/object-renderers/TrackRenderer.js b/src/components/map/object-renderers/TrackRenderer.js index c027890..b9fa747 100644 --- a/src/components/map/object-renderers/TrackRenderer.js +++ b/src/components/map/object-renderers/TrackRenderer.js @@ -130,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); diff --git a/src/helpers/constants.js b/src/helpers/constants.js index e5d2ad4..4e0f6f7 100644 --- a/src/helpers/constants.js +++ b/src/helpers/constants.js @@ -167,6 +167,7 @@ const Constants = { }, options: { 'derail': ['#f22', 'Derail track'], + 'unknown': ['#aaa', 'Unknown speed'], } } } From 5546601b67094544aab0097892bc18d36f0b7758 Mon Sep 17 00:00:00 2001 From: dominik-korsa <29484605+dominik-korsa@users.noreply.github.com> Date: Sat, 12 Jul 2025 19:07:45 +0200 Subject: [PATCH 16/24] Implement category application order --- src/model/scenery.js | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/model/scenery.js b/src/model/scenery.js index 5a4eb40..0a79104 100644 --- a/src/model/scenery.js +++ b/src/model/scenery.js @@ -36,12 +36,29 @@ export default class Scenery } applyObjects() { + const categories_order = [ + 'switches', 'routes', // switches and routes create tracks + 'tracks', // tracks create isolation ids + 'isolation-ids', + '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); + } }); } @@ -58,7 +75,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; - } - ); + }); } } From 3ae1827d10fd4b5f3e6a672657536b32f48766aa Mon Sep 17 00:00:00 2001 From: dominik-korsa <29484605+dominik-korsa@users.noreply.github.com> Date: Sat, 12 Jul 2025 19:58:35 +0200 Subject: [PATCH 17/24] Create IsolationId class, split IsolationEndRenderer and IsolationIdRenderer --- src/components/map/ObjectsRenderQueue.js | 11 +++- .../object-renderers/IsolationEndRenderer.js | 40 +++++++++++++ .../object-renderers/IsolationIdRenderer.js | 58 ++++--------------- src/model/isolation-id.js | 15 +++++ .../switch-descriptions/switch-prefab.js | 13 +++-- src/model/switch.js | 6 ++ src/model/tracks/bezier-track.js | 24 +++++++- src/model/tracks/standard-track.js | 21 +++++-- src/model/tracks/track.js | 1 - 9 files changed, 128 insertions(+), 61 deletions(-) create mode 100644 src/components/map/object-renderers/IsolationEndRenderer.js create mode 100644 src/model/isolation-id.js diff --git a/src/components/map/ObjectsRenderQueue.js b/src/components/map/ObjectsRenderQueue.js index a4f84bf..8f98b70 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,14 @@ const ObjectRendererQueue = [ 'pointerEvents': true }, { - 'name': 'isolations-ids', + 'name': 'isolations-ends', 'category': 'tracks', + 'renderer': IsolationEndRenderer, + 'cond': (layers) => layers['isolations-ids'] + }, + { + 'name': 'isolations-ids', + 'category': 'isolation-ids', 'renderer': IsolationIdRenderer, 'cond': (layers) => layers['isolations-ids'] }, @@ -92,4 +99,4 @@ const ObjectRendererQueue = [ } ] -export default ObjectRendererQueue; \ No newline at end of file +export default ObjectRendererQueue; 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 d802f65..881585b 100644 --- a/src/components/map/object-renderers/IsolationIdRenderer.js +++ b/src/components/map/object-renderers/IsolationIdRenderer.js @@ -1,55 +1,17 @@ -import { useMemo } from "react"; import SimpleLabelText from "../text/SimpleLabelText"; -import { TrackConnectionEnd } 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 [x, y] = object.pos.toSVGCoords(); const text = object.id_isolation ?? ''; + if (!text) return null; - const showText = (!object.hide_isolation && !!text); - - return (<> - {showText && } - - - ); -} - -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 + return (); } diff --git a/src/model/isolation-id.js b/src/model/isolation-id.js new file mode 100644 index 0000000..feefa5d --- /dev/null +++ b/src/model/isolation-id.js @@ -0,0 +1,15 @@ +import SceneryObject from "./scenery-object"; +import Vector3 from "./vector3"; + +export default class IsolationId extends SceneryObject { + category = "isolation-ids"; + type = "IsolationId"; + id_isolation; + + constructor(object_type, object_id, pos, id_isolation) { + super(`${object_type}:${object_id}`, pos, Vector3.zero()); + Object.assign(this, { + id_isolation, + }); + } +} diff --git a/src/model/switch-descriptions/switch-prefab.js b/src/model/switch-descriptions/switch-prefab.js index 6d2f623..3bfa580 100644 --- a/src/model/switch-descriptions/switch-prefab.js +++ b/src/model/switch-descriptions/switch-prefab.js @@ -5,10 +5,12 @@ import {TrackConnectionEnd} from "../track-connection"; export default class SwitchPrefab { tracks = {}; + isolation_id_offset; - constructor(tracks) { + constructor(tracks, isolation_id_offset) { Object.assign(this, { tracks, + isolation_id_offset, }); this._verifyConnections(); } @@ -89,7 +91,10 @@ export default class SwitchPrefab { ), }; - return new SwitchPrefab(tracks); + 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) { @@ -215,7 +220,7 @@ export default class SwitchPrefab { ), }; - return new SwitchPrefab(tracks); + return new SwitchPrefab(tracks, Vector3.zero()); } static slip(totalLength, outerLength, transitionLength, radius, tangentInv, leftSlipEnabled, rightSlipEnabled) { @@ -457,6 +462,6 @@ export default class SwitchPrefab { addSideConnection('left', 'a', 'b', leftSlipEnabled); addSideConnection('right', 'b', 'a', rightSlipEnabled); - return new SwitchPrefab(tracks); + return new SwitchPrefab(tracks, Vector3.zero()); } } diff --git a/src/model/switch.js b/src/model/switch.js index 0950a52..27412e6 100644 --- a/src/model/switch.js +++ b/src/model/switch.js @@ -5,6 +5,7 @@ import Vector3 from "./vector3.js"; import DefinedSwitches from "./defs/defined-switches.js"; import {SwitchTrackConnectionType} from "./switch-descriptions/switch-prefab-track"; import TrackConnection, {TrackConnectionEnd} from "./track-connection"; +import IsolationId from "./isolation-id"; export default class Switch extends SceneryObject { model; @@ -84,6 +85,11 @@ export default class Switch extends SceneryObject { this.tracks = tracks; this.def = def; + + const rotRad = this.rot.multiply(Math.PI / 180); + const isolationIdPos = this.pos.add(def.isolation_id_offset.rotate(rotRad)); + super.applyObject(scenery); + scenery.addObject(new IsolationId(this.category, this.id, isolationIdPos, this.id_isolation)); } _getTrackIds() { diff --git a/src/model/tracks/bezier-track.js b/src/model/tracks/bezier-track.js index ff79db7..96b9cc2 100644 --- a/src/model/tracks/bezier-track.js +++ b/src/model/tracks/bezier-track.js @@ -1,6 +1,7 @@ import Track from "./track"; import Vector3 from "../vector3"; import TrackConnection, {TrackConnectionEnd} from "../track-connection"; +import IsolationId from "../isolation-id"; export default class BezierTrack extends Track { @@ -9,7 +10,8 @@ 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, connections, id_station, start_slope, end_slope, id_isolation, prefab_name, maxspeed, derailspeed) { @@ -19,8 +21,21 @@ export default class BezierTrack extends Track 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() { @@ -59,4 +74,9 @@ export default class BezierTrack extends Track return track; } + + applyObject(scenery) { + super.applyObject(scenery); + scenery.addObject(new IsolationId(this.category, this.id, this.points.middle, this.id_isolation)); + } } diff --git a/src/model/tracks/standard-track.js b/src/model/tracks/standard-track.js index 81cc799..2d0d263 100644 --- a/src/model/tracks/standard-track.js +++ b/src/model/tracks/standard-track.js @@ -2,6 +2,7 @@ import AngleHelper from "../../helpers/angleHelper"; import Track from "./track"; import Vector3 from "../vector3"; import TrackConnection, {TrackConnectionEnd} from "../track-connection"; +import IsolationId from "../isolation-id"; export default class StandardTrack extends Track { @@ -9,7 +10,8 @@ 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, connections, id_station, start_slope, end_slope, id_isolation, prefab_name, maxspeed, derailspeed) { @@ -60,12 +62,18 @@ 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)); } @@ -78,4 +86,9 @@ export default class StandardTrack extends Track this.points.end.y += startHeightDiff + (endHeightDiff - startHeightDiff) / 2; } } + + applyObject(scenery) { + super.applyObject(scenery); + scenery.addObject(new IsolationId(this.category, this.id, this.points.middle, this.id_isolation)); + } } diff --git a/src/model/tracks/track.js b/src/model/tracks/track.js index 711517e..926425b 100644 --- a/src/model/tracks/track.js +++ b/src/model/tracks/track.js @@ -15,7 +15,6 @@ export default class Track extends SceneryObject { start_slope; end_slope; prefab_name; - hide_isolation = false; switch = null; electrificationStatus = ElectrificationStatus.NOT_CHECKED; hasNEVP = false; From 7d36835b28bf7dae519382fc526ef8d08cecef42 Mon Sep 17 00:00:00 2001 From: dominik-korsa <29484605+dominik-korsa@users.noreply.github.com> Date: Sat, 12 Jul 2025 21:43:26 +0200 Subject: [PATCH 18/24] Don't remove route tracks in scenery processor script --- tools/scp.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 From b31277e614c7214fc4f05c55d8a24c8f8a8b0fdb Mon Sep 17 00:00:00 2001 From: dominik-korsa <29484605+dominik-korsa@users.noreply.github.com> Date: Sun, 13 Jul 2025 21:28:17 +0200 Subject: [PATCH 19/24] Fix undefined reference in connect-tracks.js --- src/model/connect-tracks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/connect-tracks.js b/src/model/connect-tracks.js index 94961de..7a0a22c 100644 --- a/src/model/connect-tracks.js +++ b/src/model/connect-tracks.js @@ -59,7 +59,7 @@ function _checkConnection(scenery, track, connection) { if (reverseConnections.length > 1) { SceneryParserLog.warn( 'tracksConnectionTest', - `Track ${otherTrack.id} has multiple connections to ${track.id}`, + `Track ${connection.otherTrack.id} has multiple connections to ${track.id}`, ); } if (reverseConnections.length === 0) { From d394e843d7ed6b83811d889cedb0ac63ead7debd Mon Sep 17 00:00:00 2001 From: dominik-korsa <29484605+dominik-korsa@users.noreply.github.com> Date: Sun, 13 Jul 2025 21:30:28 +0200 Subject: [PATCH 20/24] Revert showing warning for the empty object type --- src/model/scenery-parser.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/model/scenery-parser.js b/src/model/scenery-parser.js index 3ca046e..77136ea 100644 --- a/src/model/scenery-parser.js +++ b/src/model/scenery-parser.js @@ -83,7 +83,7 @@ export default class SceneryParser { static _parseObject(text) { const type = text.split(";", 2)[0]; - if (type === '' || type === 'EndRoute' || 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; } @@ -115,6 +115,7 @@ export default class SceneryParser { case 'SSPController': case 'TerrainGroup': case 'EndTerrainGroup': + case '': return null; default: SceneryParserLog.warn('unknownObjectType', `Unknown object type: ${type}`); From b0e0c85c6aef978bb9ff5c603a0d63f1551670ff Mon Sep 17 00:00:00 2001 From: dominik-korsa <29484605+dominik-korsa@users.noreply.github.com> Date: Sun, 13 Jul 2025 21:43:26 +0200 Subject: [PATCH 21/24] Remove unused maxRouteConnectionDistance config field --- src/helpers/constants.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/helpers/constants.js b/src/helpers/constants.js index 4e0f6f7..47b9d03 100644 --- a/src/helpers/constants.js +++ b/src/helpers/constants.js @@ -14,7 +14,6 @@ const Constants = { runTracksConnectionTest: false, resolveElectrification: true, skipElectrificationErrorsPropagation: false, - maxRouteConnectionDistance: 0.2, attachSigns: true, attachSignsNeedsSameTrack: false, attachSignsMaxDistanceZ: 1.0, From 94554d5200142cee11e1fc09d57d3475506471e3 Mon Sep 17 00:00:00 2001 From: dominik-korsa <29484605+dominik-korsa@users.noreply.github.com> Date: Sun, 13 Jul 2025 22:38:11 +0200 Subject: [PATCH 22/24] Remove isolation-id objects from model and modify isolation id renderer --- src/components/map/ObjectsRenderQueue.js | 5 +++-- src/components/map/SceneryLayer.js | 22 +++++++++++-------- .../object-renderers/IsolationIdRenderer.js | 5 ++++- src/model/isolation-id.js | 15 ------------- src/model/scenery.js | 3 +-- src/model/switch.js | 5 ++--- src/model/tracks/bezier-track.js | 2 -- src/model/tracks/standard-track.js | 2 -- 8 files changed, 23 insertions(+), 36 deletions(-) delete mode 100644 src/model/isolation-id.js diff --git a/src/components/map/ObjectsRenderQueue.js b/src/components/map/ObjectsRenderQueue.js index 8f98b70..738951c 100644 --- a/src/components/map/ObjectsRenderQueue.js +++ b/src/components/map/ObjectsRenderQueue.js @@ -10,7 +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"; +import IsolationEndRenderer from './object-renderers/IsolationEndRenderer'; const ObjectRendererQueue = [ { @@ -31,7 +31,8 @@ const ObjectRendererQueue = [ }, { 'name': 'isolations-ids', - 'category': 'isolation-ids', + 'category': ['tracks', 'switches'], + 'types': ['StandardTrack', 'BezierTrack', 'Switch'], 'renderer': IsolationIdRenderer, 'cond': (layers) => layers['isolations-ids'] }, diff --git a/src/components/map/SceneryLayer.js b/src/components/map/SceneryLayer.js index 5ac1b08..3ea50f5 100644 --- a/src/components/map/SceneryLayer.js +++ b/src/components/map/SceneryLayer.js @@ -8,19 +8,23 @@ 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 categoryList = typeof category === 'string' ? [category] : category; + return categoryList.flatMap((category) => { + const categoryObjs = scenery.objects[category]; + if (!categoryObjs) return []; + return Object.values(categoryObjs); + }); }, [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 +57,4 @@ function StatelessSceneryLayer({ name, Renderer, objects, type, types, pointerEv ))} ); -} \ No newline at end of file +} diff --git a/src/components/map/object-renderers/IsolationIdRenderer.js b/src/components/map/object-renderers/IsolationIdRenderer.js index 881585b..f1be890 100644 --- a/src/components/map/object-renderers/IsolationIdRenderer.js +++ b/src/components/map/object-renderers/IsolationIdRenderer.js @@ -2,10 +2,13 @@ import SimpleLabelText from "../text/SimpleLabelText"; export default function IsolationIdRenderer(props) { const { object } = props; - const [x, y] = object.pos.toSVGCoords(); + const text = object.id_isolation ?? ''; if (!text) return null; + let pos = (object.type === 'Switch' ? object.isolation_id_pos : object.points.middle) ?? object.pos; + const [x, y] = pos.toSVGCoords(); + return ( Date: Sun, 13 Jul 2025 22:58:55 +0200 Subject: [PATCH 23/24] Place route name label at the start of route --- src/components/map/Map.css | 4 +- .../map/object-renderers/RouteRenderer.js | 37 +++++++++++-------- src/model/route.js | 5 ++- 3 files changed, 26 insertions(+), 20 deletions(-) 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/object-renderers/RouteRenderer.js b/src/components/map/object-renderers/RouteRenderer.js index a208bb9..e42de3c 100644 --- a/src/components/map/object-renderers/RouteRenderer.js +++ b/src/components/map/object-renderers/RouteRenderer.js @@ -1,28 +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.end_center.toSVGCoords(); - const angle = object.end_angle_rad * 180 / Math.PI; + + 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/model/route.js b/src/model/route.js index 7136391..c3a4324 100644 --- a/src/model/route.js +++ b/src/model/route.js @@ -3,6 +3,7 @@ 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; @@ -57,7 +58,7 @@ export default class Route extends SceneryObject { return; } const startAngleRad = this.end_angle_rad; - const start_rot = new Vector3(0, startAngleRad * 180 / Math.PI, 0); + const startRot = new Vector3(0, AngleHelper.radToDeg(startAngleRad), 0); const startPoints = this.end_points; this.end_points = []; @@ -98,7 +99,7 @@ export default class Route extends SceneryObject { trackId, startPoints[index], this.end_points[index], - start_rot, + startRot, lengths[index], radii[index], [], // connections From 55312b48ae225237d70d2df6cbcb9bdb633fffe3 Mon Sep 17 00:00:00 2001 From: dominik-korsa <29484605+dominik-korsa@users.noreply.github.com> Date: Sun, 13 Jul 2025 23:06:09 +0200 Subject: [PATCH 24/24] Don't use flapMap for a single category in SceneryLayer --- src/components/map/SceneryLayer.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/map/SceneryLayer.js b/src/components/map/SceneryLayer.js index 3ea50f5..ee26395 100644 --- a/src/components/map/SceneryLayer.js +++ b/src/components/map/SceneryLayer.js @@ -13,12 +13,15 @@ export default function SceneryLayer(props) { const objects = useMemo(() => { if (!scenery || !isVisible) return []; - const categoryList = typeof category === 'string' ? [category] : category; - return categoryList.flatMap((category) => { + + 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;