From 8c48da785b82407fd6a34c39565cd111e9bf4a45 Mon Sep 17 00:00:00 2001 From: dominik-korsa <29484605+dominik-korsa@users.noreply.github.com> Date: Thu, 17 Jul 2025 22:54:39 +0200 Subject: [PATCH 01/10] Add track elevation layer --- src/components/App.js | 12 ++- .../map/object-renderers/TrackRenderer.js | 79 +++++++++++-------- .../track-color-mode/TrackColorModeLegend.js | 16 ++-- src/contexts/GradientsContext.js | 4 + src/helpers/constants.js | 14 +++- src/helpers/miscHelper.js | 10 --- src/hooks/useGradients.js | 68 ++++++++++++++++ src/model/scenery.js | 19 ++++- 8 files changed, 165 insertions(+), 57 deletions(-) create mode 100644 src/contexts/GradientsContext.js delete mode 100644 src/helpers/miscHelper.js create mode 100644 src/hooks/useGradients.js diff --git a/src/components/App.js b/src/components/App.js index 09a5c77..1c8ab96 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -8,21 +8,25 @@ import SettingsManager from './SettingsManager'; import LocList from './loclist/LocList'; import DialogManager from './dialog/DialogManager'; import DistanceMeterContext from '../contexts/DistanceMeterContext'; +import GradientsContext from "../contexts/GradientsContext"; +import {useGradients} from "../hooks/useGradients"; function App() { const [isLoading, setIsLoading] = useState(false); const [scenery, setScenery] = useState(null); const [sideMenuOpen, setSideMenuOpen] = useState(false); const [distancePoints, setDistancePoints] = useState(null); - + const gradients = useGradients(scenery); + return ( - +
@@ -32,11 +36,11 @@ function App() {
+ ); } - + export default App; - \ No newline at end of file diff --git a/src/components/map/object-renderers/TrackRenderer.js b/src/components/map/object-renderers/TrackRenderer.js index b9fa747..3ed98d7 100644 --- a/src/components/map/object-renderers/TrackRenderer.js +++ b/src/components/map/object-renderers/TrackRenderer.js @@ -2,17 +2,19 @@ import React, { useContext } from "react"; import SettingsContext from "../../../contexts/SettingsContext"; import Constants from "../../../helpers/constants"; import { ElectrificationStatus } from "../../../model/electrification-status"; -import MiscHelper from "../../../helpers/miscHelper"; import { setHoveredTrack, unsetHoveredTrack } from "../../../services/trackHoverInfoService"; +import GradientsContext from "../../../contexts/GradientsContext"; export default function TrackRenderer(props) { const { object } = props; const { trackColorMode } = useContext(SettingsContext); + const gradients = useContext(GradientsContext); return ( ); } @@ -20,7 +22,7 @@ export default function TrackRenderer(props) { const MemoizedTrackRenderer = React.memo(StatelessTrackRenderer); function StatelessTrackRenderer(props) { - const { object, trackColorMode } = props; + const { object, trackColorMode, gradients } = props; if (object.points.start.distanceSq(object.points.end) < 0.001) { return null; @@ -35,8 +37,8 @@ function StatelessTrackRenderer(props) { }; const path = getTrackPath(object); - const color = getTrackColor(object, trackColorMode); - const defs = getTrackDefs(object, trackColorMode); + const color = getTrackColor(object, trackColorMode, gradients); + const defs = getTrackDefs(object, trackColorMode, gradients); return ( @@ -83,7 +85,18 @@ function getTrackPath(object) { } } -function getTrackColor(object, trackColorMode) { +function getGradientValues(object, trackColorMode) { + switch (trackColorMode) { + case 'slope': + return [Math.abs(object.start_slope), Math.abs(object.end_slope)]; + case 'elevation': + return [object.points.start.y, object.points.end.y]; + default: + return null; + } +} + +function getTrackColor(object, trackColorMode, gradients) { const modeDef = Constants.trackColorModes[trackColorMode]; switch (trackColorMode) { @@ -121,46 +134,50 @@ function getTrackColor(object, trackColorMode) { default: return modeDef.options[modeDef.optionDefault][0]; } - case "slope": - // do not use track gradient unless the slope is different on both ends - if (object.start_slope === object.end_slope) { - return MiscHelper.getTrackGradient(modeDef.gradient, Math.abs(object.start_slope)); - } - - return `url(#track-slope-${object.id})`; case "max-speed": if (!object.maxspeed) { if (object.type === 'RouteTrack') return modeDef.options['unknown'][0]; return modeDef.options['derail'][0]; } - return MiscHelper.getTrackGradient(modeDef.gradient, object.maxspeed); + return gradients.getTrackGradientColor('max-speed', object.maxspeed); + + case "elevation": + case "slope": + const [startValue, endValue] = getGradientValues(object, trackColorMode); + if (startValue === endValue) return gradients.getTrackGradientColor(trackColorMode, startValue); + return `url(#track-${trackColorMode}-${object.id})`; } } -function getTrackDefs(object, trackColorMode) { - if (trackColorMode !== "slope") return null; - if (object.start_slope === object.end_slope) return null; - +function getGradientDefs(object, trackColorMode, gradients, startValue, endValue) { + if (startValue === endValue) 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)); + const gradId = `track-${trackColorMode}-${object.id}`; + const startColor = gradients.getTrackGradientColor(trackColorMode, startValue); + const endColor = gradients.getTrackGradientColor(trackColorMode, endValue); return ( - - - - + + + + ); } + +function getTrackDefs(object, trackColorMode, gradients) { + const gradientVals = getGradientValues(object, trackColorMode, gradients); + if (gradientVals === null) return null; + const [startValue, endValue] = gradientVals; + return getGradientDefs(object, trackColorMode, gradients, startValue, endValue); +} diff --git a/src/components/sidemenu/track-color-mode/TrackColorModeLegend.js b/src/components/sidemenu/track-color-mode/TrackColorModeLegend.js index 9d4d50d..fd51bfe 100644 --- a/src/components/sidemenu/track-color-mode/TrackColorModeLegend.js +++ b/src/components/sidemenu/track-color-mode/TrackColorModeLegend.js @@ -1,5 +1,6 @@ import Constants from "../../../helpers/constants"; -import MiscHelper from "../../../helpers/miscHelper"; +import {useContext} from "react"; +import GradientsContext from "../../../contexts/GradientsContext"; export default function TrackColorModeLegend(props) { const { trackColorMode } = props; @@ -7,7 +8,7 @@ export default function TrackColorModeLegend(props) { return (
- +
); @@ -22,7 +23,7 @@ function LegendOptions(props) {
    { Object.entries(colorModeDef.options).map(([key, optionDef]) => ( - ))} + ))}
); @@ -39,12 +40,13 @@ function LegendOptionsItem(props) { } function LegendGradient(props) { - const { colorModeDef } = props; - const gradientDef = colorModeDef?.gradient || null; + const gradients = useContext(GradientsContext); + const { trackColorMode } = props; + const gradientDef = gradients.gradientSettings[trackColorMode]; if (!gradientDef) return null; - const from = MiscHelper.getTrackGradient(gradientDef, gradientDef.legendMin); - const to = MiscHelper.getTrackGradient(gradientDef, gradientDef.legendMax); + const from = gradients.getTrackGradientColor(trackColorMode, gradientDef.legendMin); + const to = gradients.getTrackGradientColor(trackColorMode, gradientDef.legendMax); const gradientStyle = `linear-gradient(to right, ${from}, ${to})`; const midVal = (gradientDef.legendMin + gradientDef.legendMax) / 2; diff --git a/src/contexts/GradientsContext.js b/src/contexts/GradientsContext.js new file mode 100644 index 0000000..a8c2001 --- /dev/null +++ b/src/contexts/GradientsContext.js @@ -0,0 +1,4 @@ +import { createContext } from 'react'; + +const GradientsContext = createContext(); +export default GradientsContext; diff --git a/src/helpers/constants.js b/src/helpers/constants.js index 770e5c2..57528f5 100644 --- a/src/helpers/constants.js +++ b/src/helpers/constants.js @@ -147,6 +147,7 @@ const Constants = { }, 'slope': { name: 'Slope', + type: 'gradient', gradient: { base: [128, 0, 128], diff: [0, 25.6, 0], @@ -169,7 +170,18 @@ const Constants = { 'derail': ['#f22', 'Derail track'], 'unknown': ['#aaa', 'Unknown speed'], } - } + }, + 'elevation': { + name: 'Elevation', + type: 'gradient', + dynamicGradient: { + min: [0, 190, 0], + max: [255, 22, 22], + defaultMin: 0, + defaultMax: 1, + unit: 'm', + }, + }, } }; diff --git a/src/helpers/miscHelper.js b/src/helpers/miscHelper.js deleted file mode 100644 index 6caface..0000000 --- a/src/helpers/miscHelper.js +++ /dev/null @@ -1,10 +0,0 @@ -export default class MiscHelper { - static getTrackGradient(gradientDef, value) { - const { base, diff } = gradientDef; - const out = base.map((b, i) => - Math.min(255, Math.max(0, b + diff[i] * value)) - ); - - return `rgb(${out[0]}, ${out[1]}, ${out[2]})`; - } -} diff --git a/src/hooks/useGradients.js b/src/hooks/useGradients.js new file mode 100644 index 0000000..4365982 --- /dev/null +++ b/src/hooks/useGradients.js @@ -0,0 +1,68 @@ +import {useMemo} from 'react'; +import Constants from "../helpers/constants"; + +class Gradients { + gradientSettings; + + constructor(trackElevationBounds) { + this.trackElevationBounds = trackElevationBounds; + this.gradientSettings = Object.fromEntries( + Object.entries(Constants.trackColorModes).map( + ([mode, def]) => [mode, this._createGradientSettings(mode, def)], + ), + ); + } + + _getMinMax(trackColorMode) { + if (trackColorMode === 'elevation') { + if (!this.trackElevationBounds) return null; + if (this.trackElevationBounds.minY > this.trackElevationBounds.maxY) return null; + // Ensure min and max values are even so that the min, max and middle values are integers + return [Math.floor(this.trackElevationBounds.minY/2)*2, Math.ceil(this.trackElevationBounds.maxY/2)*2]; + } + return null; + } + + _createGradientSettings(trackColorMode, def) { + if ('gradient' in def) return def.gradient; + if ('dynamicGradient' in def) { + const gradient = def.dynamicGradient; + const [minValue, maxValue] = this._getMinMax(trackColorMode) ?? [gradient.defaultMin, gradient.defaultMax]; + + const diff = gradient.min.map((min, i) => { + const max = gradient.max[i]; + return (max - min) / (maxValue - minValue); + }); + const base = gradient.min.map((min, i) => { + return min - diff[i] * minValue; + }); + + return { + base, + diff, + legendMin: minValue, + legendMax: maxValue, + unit: gradient.unit, + }; + } + return null; + } + + getTrackGradientColor(trackColorMode, value) { + const gradientDef = this.gradientSettings[trackColorMode]; + if (!gradientDef) return null; + + const { base, diff } = gradientDef; + const out = base.map((b, i) => + Math.min(255, Math.max(0, b + diff[i] * value)) + ); + + return `rgb(${out[0]}, ${out[1]}, ${out[2]})`; + } +} + +export function useGradients(scenery) { + return useMemo(() => { + return new Gradients(scenery?.trackElevationBounds ?? null); + }, [scenery?.trackElevationBounds?.minY, scenery?.trackElevationBounds?.maxY]); +} diff --git a/src/model/scenery.js b/src/model/scenery.js index b3a69d5..f01af04 100644 --- a/src/model/scenery.js +++ b/src/model/scenery.js @@ -6,6 +6,7 @@ export default class Scenery signalBoxes = []; spawnPoints = []; bounds = { minX: Infinity, minZ: Infinity, maxX: -Infinity, maxZ: -Infinity }; + trackElevationBounds = { minY: Infinity, maxY: -Infinity }; electrificationResolved = ElectrificationResolutionStatus.NOT_RESOLVED; static nextMiscId = 1; @@ -33,6 +34,9 @@ export default class Scenery if(object.getRenderBounds) { this._updateBounds(object.getRenderBounds()); } + if (object.category === 'tracks') { + this._updateTrackElevationBounds(object); + } } applyObjects() { @@ -70,10 +74,17 @@ export default class Scenery if(!objBounds || !Array.isArray(objBounds)) return; objBounds.forEach(point => { - if(point.x < this.bounds.minX) this.bounds.minX = point.x; - 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; + if (point.x < this.bounds.minX) this.bounds.minX = point.x; + 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; }); } + + _updateTrackElevationBounds(object) { + [object.points.start, object.points.end].forEach(point => { + if (point.y < this.trackElevationBounds.minY) this.trackElevationBounds.minY = point.y; + if (point.y > this.trackElevationBounds.maxY) this.trackElevationBounds.maxY = point.y; + }) + } } From ec20068309ea8aff55bece8ecd9fae478d894110 Mon Sep 17 00:00:00 2001 From: dominik-korsa <29484605+dominik-korsa@users.noreply.github.com> Date: Sat, 19 Jul 2025 17:54:03 +0200 Subject: [PATCH 02/10] Pass scenery directly to Gradients --- src/hooks/useGradients.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/hooks/useGradients.js b/src/hooks/useGradients.js index 4365982..27b78b2 100644 --- a/src/hooks/useGradients.js +++ b/src/hooks/useGradients.js @@ -4,8 +4,8 @@ import Constants from "../helpers/constants"; class Gradients { gradientSettings; - constructor(trackElevationBounds) { - this.trackElevationBounds = trackElevationBounds; + constructor(scenery) { + this.scenery = scenery; this.gradientSettings = Object.fromEntries( Object.entries(Constants.trackColorModes).map( ([mode, def]) => [mode, this._createGradientSettings(mode, def)], @@ -15,10 +15,10 @@ class Gradients { _getMinMax(trackColorMode) { if (trackColorMode === 'elevation') { - if (!this.trackElevationBounds) return null; - if (this.trackElevationBounds.minY > this.trackElevationBounds.maxY) return null; + if (!this.scenery) return null; + if (this.scenery.trackElevationBounds.minY > this.scenery.trackElevationBounds.maxY) return null; // Ensure min and max values are even so that the min, max and middle values are integers - return [Math.floor(this.trackElevationBounds.minY/2)*2, Math.ceil(this.trackElevationBounds.maxY/2)*2]; + return [Math.floor(this.scenery.trackElevationBounds.minY/2)*2, Math.ceil(this.scenery.trackElevationBounds.maxY/2)*2]; } return null; } @@ -62,7 +62,5 @@ class Gradients { } export function useGradients(scenery) { - return useMemo(() => { - return new Gradients(scenery?.trackElevationBounds ?? null); - }, [scenery?.trackElevationBounds?.minY, scenery?.trackElevationBounds?.maxY]); + return useMemo(() => new Gradients(scenery), [scenery]); } From 5b83f8c8172851b114cd202f52bfcf0c1d6b2d09 Mon Sep 17 00:00:00 2001 From: izawartka <59137928+izawartka@users.noreply.github.com> Date: Sun, 20 Jul 2025 23:31:22 +0200 Subject: [PATCH 03/10] Added platforms renderer +Added an experimental platforms renderer --- src/components/map/Map.css | 4 ++++ src/components/map/ObjectsRenderQueue.js | 8 ++++++++ .../map/object-renderers/PlatformRenderer.js | 20 +++++++++++++++++++ src/helpers/constants.js | 5 +++++ 4 files changed, 37 insertions(+) create mode 100644 src/components/map/object-renderers/PlatformRenderer.js diff --git a/src/components/map/Map.css b/src/components/map/Map.css index 3e72537..ac1b40c 100644 --- a/src/components/map/Map.css +++ b/src/components/map/Map.css @@ -128,3 +128,7 @@ .map svg g.spawn-point path.background { fill: #111; } + +.map svg g.platform rect { + fill: #333; +} diff --git a/src/components/map/ObjectsRenderQueue.js b/src/components/map/ObjectsRenderQueue.js index 738951c..206e379 100644 --- a/src/components/map/ObjectsRenderQueue.js +++ b/src/components/map/ObjectsRenderQueue.js @@ -11,8 +11,16 @@ 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 PlatformRenderer from './object-renderers/PlatformRenderer'; const ObjectRendererQueue = [ + { + 'name': 'platforms', + 'category': 'misc', + 'type': 'Platform', + 'renderer': PlatformRenderer, + 'cond': (layers) => layers['platforms'], + }, { 'name': 'tracks', 'category': 'tracks', diff --git a/src/components/map/object-renderers/PlatformRenderer.js b/src/components/map/object-renderers/PlatformRenderer.js new file mode 100644 index 0000000..b0afcc0 --- /dev/null +++ b/src/components/map/object-renderers/PlatformRenderer.js @@ -0,0 +1,20 @@ +export default function PlatformRenderer(props) { + const { object } = props; + const [x, y] = object.pos.toSVGCoords(); + if(!object.def) return null; + + const [centerX, centerY] = object.def.center.toSVGCoords(); + const sizeX = object.def.size.x; + const sizeY = object.def.size.z; + + return ( + + + + ); +} diff --git a/src/helpers/constants.js b/src/helpers/constants.js index 517c3df..9c3404e 100644 --- a/src/helpers/constants.js +++ b/src/helpers/constants.js @@ -63,6 +63,11 @@ const Constants = { fetchListUrl: `${process.env.PUBLIC_URL}/sceneries/sceneries.json`, }, layers: [ + { + id: 'platforms', + name: 'Platforms (WIP)', + default: true, + }, { id: 'tracks', name: 'Tracks', From 4855787e23b74833c6d5dfa7a56903557703a765 Mon Sep 17 00:00:00 2001 From: izawartka <59137928+izawartka@users.noreply.github.com> Date: Mon, 21 Jul 2025 18:54:04 +0200 Subject: [PATCH 04/10] Added Quaternion class +Added Quaternion class --- src/model/quaternion.js | 58 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/model/quaternion.js diff --git a/src/model/quaternion.js b/src/model/quaternion.js new file mode 100644 index 0000000..79d6024 --- /dev/null +++ b/src/model/quaternion.js @@ -0,0 +1,58 @@ +import Vector3 from "./vector3"; + +export default class Quaternion { + w; + x; + y; + z; + + constructor(w, x, y, z) { + this.w = w; + this.x = x; + this.y = y; + this.z = z; + } + + static fromEulerAngles(vector) { + const { x: rotX, y: rotY, z: rotZ } = vector; + + const cz = Math.cos(rotZ * 0.5); + const sz = Math.sin(rotZ * 0.5); + const cx = Math.cos(rotX * 0.5); + const sx = Math.sin(rotX * 0.5); + const cy = Math.cos(rotY * 0.5); + const sy = Math.sin(rotY * 0.5); + + const w = cx * cy * cz + sx * sy * sz; + const x = sx * cy * cz + cx * sy * sz; + const y = cx * sy * cz - sx * cy * sz; + const z = cx * cy * sz - sx * sy * cz; + + return new Quaternion(w, x, y, z); + } + + rotateVector(v) { + const vecQuat = new Quaternion(0, v.x, v.y, v.z); + const resultQuat = this.multiply(vecQuat).multiply(this.conjugate()); + return new Vector3(resultQuat.x, resultQuat.y, resultQuat.z); + } + + conjugate() { + return new Quaternion(this.w, -this.x, -this.y, -this.z); + } + + normalize() { + const length = Math.sqrt(this.w * this.w + this.x * this.x + this.y * this.y + this.z * this.z); + if (length === 0) return new Quaternion(1, 0, 0, 0); // return identity quaternion if length is zero + return new Quaternion(this.w / length, this.x / length, this.y / length, this.z / length); + } + + multiply(q) { + const w = this.w * q.w - this.x * q.x - this.y * q.y - this.z * q.z; + const x = this.w * q.x + this.x * q.w + this.y * q.z - this.z * q.y; + const y = this.w * q.y - this.x * q.z + this.y * q.w + this.z * q.x; + const z = this.w * q.z + this.x * q.y - this.y * q.x + this.z * q.w; + + return new Quaternion(w, x, y, z); + } +}; \ No newline at end of file From 884652ac8fd85982bf94b2a031d6cb7d9b51e6d2 Mon Sep 17 00:00:00 2001 From: izawartka <59137928+izawartka@users.noreply.github.com> Date: Mon, 21 Jul 2025 18:54:36 +0200 Subject: [PATCH 05/10] Updated scp.py *Updated scp.py to not ignore MiscGroups --- tools/scp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/scp.py b/tools/scp.py index 841f940..b9b463d 100644 --- a/tools/scp.py +++ b/tools/scp.py @@ -5,7 +5,7 @@ import sys # # [ TD2 SCENERY PROCESSOR ] # by masuo -# v1.6 +# v1.7 # BAD_WORDS = ['Fence', 'TerrainPoint', 'Wires'] @@ -16,7 +16,7 @@ def should_exclude(line: str) -> bool: if line.startswith(word): return True - if line.startswith("Misc"): + if line.startswith("Misc") and not line.startswith("MiscGroup"): if re.match(MISC_REGEX, line): return True From f6ca38e0f9a3ccae000ac85559df12aafa9e9c92 Mon Sep 17 00:00:00 2001 From: izawartka <59137928+izawartka@users.noreply.github.com> Date: Mon, 21 Jul 2025 18:59:44 +0200 Subject: [PATCH 06/10] Added Platforms disable option +Added an option to disable Platform objects parsing and rendering --- src/components/map/ObjectsRenderQueue.js | 3 ++- src/components/sidemenu/LayersMenu.js | 2 ++ src/helpers/constants.js | 4 +++- src/model/scenery-parser.js | 4 ++-- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/map/ObjectsRenderQueue.js b/src/components/map/ObjectsRenderQueue.js index 206e379..2c43b76 100644 --- a/src/components/map/ObjectsRenderQueue.js +++ b/src/components/map/ObjectsRenderQueue.js @@ -12,6 +12,7 @@ import ElectrificationStatusPopup from './additional-layer-components/Electrific import SpawnPointRenderer from './object-renderers/SpawnPointRenderer'; import IsolationEndRenderer from './object-renderers/IsolationEndRenderer'; import PlatformRenderer from './object-renderers/PlatformRenderer'; +import Constants from '../../helpers/constants'; const ObjectRendererQueue = [ { @@ -19,7 +20,7 @@ const ObjectRendererQueue = [ 'category': 'misc', 'type': 'Platform', 'renderer': PlatformRenderer, - 'cond': (layers) => layers['platforms'], + 'cond': (layers) => layers['platforms'] && !Constants.parser.skipPlatforms }, { 'name': 'tracks', diff --git a/src/components/sidemenu/LayersMenu.js b/src/components/sidemenu/LayersMenu.js index aa59caf..5f2528d 100644 --- a/src/components/sidemenu/LayersMenu.js +++ b/src/components/sidemenu/LayersMenu.js @@ -36,6 +36,8 @@ export default function LayersMenu(props) { function LayersMenuItem(props) { const { layer, isChecked, onToggle } = props; + if(layer.cond && !layer.cond()) return null; + return (