- {gradientDef.legendMin} {gradientDef.unit}
+ {legendMin} {gradientDef.unit}
{midVal} {gradientDef.unit}
- {gradientDef.legendMax} {gradientDef.unit}
+ {legendMax} {gradientDef.unit}
);
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 517c3df..30bbfe1 100644
--- a/src/helpers/constants.js
+++ b/src/helpers/constants.js
@@ -5,7 +5,8 @@ const Constants = {
zoomMin: 0.03,
zoomMax: 200.0,
rotationSensitivity: 0.2,
- forcePointerEvents: false
+ forcePointerEvents: false,
+ platformMaxTilt: 0.5
},
parser: {
logSceneryAfterFinished: true,
@@ -20,7 +21,8 @@ const Constants = {
attachSignsMaxDistanceX: 0.3,
attachSignsGridSize: 10,
logAttachedSigns: false,
- skipBaseMisc: true
+ skipBaseMisc: true,
+ skipPlatforms: false
},
warnings: {
all: false, // enable all warnings
@@ -63,6 +65,12 @@ const Constants = {
fetchListUrl: `${process.env.PUBLIC_URL}/sceneries/sceneries.json`,
},
layers: [
+ {
+ id: 'platforms',
+ name: 'Platforms',
+ default: true,
+ cond: () => !Constants.parser.skipPlatforms,
+ },
{
id: 'tracks',
name: 'Tracks',
@@ -151,12 +159,14 @@ const Constants = {
},
'slope': {
name: 'Slope',
+ type: 'gradient',
gradient: {
base: [128, 0, 128],
diff: [0, 25.6, 0],
legendMin: 0,
legendMax: 10,
unit: '‰',
+ startLegendAt0: false,
}
},
'max-speed': {
@@ -168,12 +178,25 @@ const Constants = {
legendMin: 0,
legendMax: 170,
unit: 'km/h',
+ startLegendAt0: false,
},
options: {
'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: 2,
+ unit: 'm',
+ startLegendAt0: true,
+ },
+ },
}
};
diff --git a/src/helpers/miscHelper.js b/src/helpers/miscHelper.js
index 6caface..acec666 100644
--- a/src/helpers/miscHelper.js
+++ b/src/helpers/miscHelper.js
@@ -1,10 +1,10 @@
export default class MiscHelper {
- static getTrackGradient(gradientDef, value) {
+ static getTrackGradientColor(gradientDef, value) {
const { base, diff } = gradientDef;
- const out = base.map((b, i) =>
- Math.min(255, Math.max(0, b + diff[i] * value))
+ 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/model/misc-group.js b/src/model/misc-group.js
index 9695270..63d6af8 100644
--- a/src/model/misc-group.js
+++ b/src/model/misc-group.js
@@ -1,8 +1,10 @@
import Vector3 from "./vector3";
+import Quaternion from "./quaternion";
export default class MiscGroup {
pos;
rot;
+ quaternion;
constructor(pos, rot) {
this.pos = pos;
@@ -18,4 +20,13 @@ export default class MiscGroup {
return object;
}
+
+ getQuaternion() {
+ if (!this.quaternion) {
+ const rotRad = this.rot.multiply(Math.PI / 180);
+ this.quaternion = Quaternion.fromEulerAngles(rotRad).normalize();
+ }
+
+ return this.quaternion;
+ }
}
diff --git a/src/model/misc/misc.js b/src/model/misc/misc.js
index 93a28bc..fd98aa4 100644
--- a/src/model/misc/misc.js
+++ b/src/model/misc/misc.js
@@ -1,19 +1,22 @@
import AngleHelper from "../../helpers/angleHelper";
+import Quaternion from "../quaternion";
import SceneryObject from "../scenery-object";
import Vector3 from "../vector3";
export default class Misc extends SceneryObject {
prefab_name;
name;
+ yawData;
category = "misc";
type = "Misc";
- constructor(id, misc_id, prefab_name, pos, rot, name) {
+ constructor(id, misc_id, prefab_name, pos, rot, yawData, name) {
super(id, pos, rot);
Object.assign(this, {
misc_id,
prefab_name,
name,
+ yawData
});
}
@@ -35,19 +38,20 @@ export default class Misc extends SceneryObject {
return object;
}
- static applyGroupTransforms(pos, rot, miscGroups) {
- if(!miscGroups) return [ pos, rot ];
-
+ static applyGroupTransforms(localPos, localRot, miscGroups) {
+ const localRotRad = localRot.multiply(Math.PI / 180);
+ let worldQuat = Quaternion.fromEulerAngles(localRotRad).normalize();
+ let worldPos = localPos.clone();
+
for (const group of miscGroups) {
- const rotRad = AngleHelper.degToRad(group.rot.y);
- const rotRadY = new Vector3(0, rotRad, 0);
- pos = pos.rotate(rotRadY).add(group.pos);
- rot = rot.add(group.rot);
- // TODO: Replace this with some fancy quaternion math maybe
+ const groupQuat = group.getQuaternion();
+ worldPos = groupQuat.rotateVector(worldPos).add(group.pos);
+ worldQuat = groupQuat.multiply(worldQuat).normalize();
}
- rot = AngleHelper.normalizeDegVector(rot);
-
- return [ pos, rot ];
+ const worldRot = worldQuat.toEulerAngles().multiply(180 / Math.PI);
+ const yawData = worldQuat.getMiscYawData();
+
+ return [ worldPos, worldRot, yawData ];
}
}
diff --git a/src/model/misc/platform.js b/src/model/misc/platform.js
index 3efd6a3..6fa9d8e 100644
--- a/src/model/misc/platform.js
+++ b/src/model/misc/platform.js
@@ -7,8 +7,8 @@ export default class Platform extends Misc {
type = "Platform";
def = null;
- constructor(id, misc_id, prefab_name, pos, rot, name) {
- super(id, misc_id, prefab_name, pos, rot, name);
+ constructor(id, misc_id, prefab_name, pos, rot, yawData, name) {
+ super(id, misc_id, prefab_name, pos, rot, yawData, name);
this.def = Platform.getDef(name, prefab_name);
}
diff --git a/src/model/misc/signalbox.js b/src/model/misc/signalbox.js
index 16966cf..9a6f696 100644
--- a/src/model/misc/signalbox.js
+++ b/src/model/misc/signalbox.js
@@ -8,8 +8,8 @@ export default class SignalBox extends Misc {
applied = false;
def = null;
- constructor(id, misc_id, prefab_name, pos, rot, name) {
- super(id, misc_id, prefab_name, pos, rot, name);
+ constructor(id, misc_id, prefab_name, pos, rot, yawData, name) {
+ super(id, misc_id, prefab_name, pos, rot, yawData, name);
this.def = SignalBox.getDef(name, prefab_name);
}
diff --git a/src/model/quaternion.js b/src/model/quaternion.js
new file mode 100644
index 0000000..8b9a0b0
--- /dev/null
+++ b/src/model/quaternion.js
@@ -0,0 +1,103 @@
+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 halfZ = vector.z * 0.5;
+ const halfX = vector.x * 0.5;
+ const halfY = vector.y * 0.5;
+
+ const sz = Math.sin(halfZ);
+ const cz = Math.cos(halfZ);
+ const sx = Math.sin(halfX);
+ const cx = Math.cos(halfX);
+ const sy = Math.sin(halfY);
+ const cy = Math.cos(halfY);
+
+ return new Quaternion(
+ sx * sy * sz + cx * cy * cz,
+ sx * cy * cz + cx * sy * sz,
+ cx * sy * cz - sx * cy * sz,
+ cx * cy * sz - sx * sy * cz
+ )
+ }
+
+ toEulerAngles() {
+ const sinr_cosp = 2 * (this.w * this.x + this.y * this.z);
+ const cosr_cosp = 1 - 2 * (this.x * this.x + this.y * this.y);
+ const roll = Math.atan2(sinr_cosp, cosr_cosp);
+
+ const sinp = 2 * (this.w * this.y - this.z * this.x);
+ let pitch;
+ if (Math.abs(sinp) >= 1) {
+ pitch = Math.sign(sinp) * Math.PI / 2; // use 90 degrees if out of range
+ } else {
+ pitch = -Math.asin(sinp);
+ }
+
+ const siny_cosp = 2 * (this.w * this.z + this.x * this.y);
+ const cosy_cosp = 1 - 2 * (this.y * this.y + this.z * this.z);
+ const yaw = Math.atan2(siny_cosp, cosy_cosp);
+
+ return new Vector3(roll, pitch, yaw);
+ }
+
+ rotateVector(v) {
+ const qv = new Quaternion(0, v.x, v.y, v.z);
+ const conjugate = this.conjugate();
+ const rotated = this.multiply(qv).multiply(conjugate);
+ return new Vector3(rotated.x, rotated.y, rotated.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) {
+ return new Quaternion(
+ this.w * q.w - this.x * q.x - this.y * q.y - this.z * q.z,
+ this.w * q.x + this.x * q.w + this.y * q.z - this.z * q.y,
+ this.w * q.y - this.x * q.z + this.y * q.w + this.z * q.x,
+ this.w * q.z + this.x * q.y - this.y * q.x + this.z * q.w
+ );
+ }
+
+ multiplyScalar(scalar) {
+ return new Quaternion(
+ this.w * scalar,
+ this.x * scalar,
+ this.y * scalar,
+ this.z * scalar
+ );
+ }
+
+ getMiscYawData() {
+ const forward = new Vector3(0, 0, -1);
+ const rotatedForward = this.rotateVector(forward);
+
+ const up = new Vector3(0, 1, 0);
+ const rotatedUp = this.rotateVector(up);
+
+ return {
+ yaw: Math.atan2(rotatedForward.x, rotatedForward.z) * 180 / Math.PI,
+ tilt: rotatedUp.x * rotatedUp.x + rotatedUp.z * rotatedUp.z
+ }
+ }
+};
\ No newline at end of file
diff --git a/src/model/scenery-parser.js b/src/model/scenery-parser.js
index f8619ca..6ed5dc3 100644
--- a/src/model/scenery-parser.js
+++ b/src/model/scenery-parser.js
@@ -115,11 +115,11 @@ export default class SceneryParser {
return MainCamera.fromText(text);
case 'MiscGroup':
const group = MiscGroup.fromText(text);
- currentMiscGroups.push(group);
+ currentMiscGroups.unshift(group);
return null;
case 'EndMiscGroup':
- if(!currentMiscGroups.pop()) {
+ if(!currentMiscGroups.shift()) {
SceneryParserLog.warn('endMiscGroupWithoutStart', 'Unexpected EndMiscGroup found without a preceding MiscGroup');
}
@@ -176,12 +176,12 @@ export default class SceneryParser {
const id = SceneryParser.nextMiscId++;
if (Platform.isPlatform(prefabName)) {
+ if(Constants.parser.skipPlatforms) return null;
return Platform.fromText(id, text, miscGroups);
} else if(SignalBox.isSignalBox(prefabName)) {
return SignalBox.fromText(id, text, miscGroups);
- } else if(Constants.parser.skipBaseMisc) {
- return null;
} else {
+ if(Constants.parser.skipBaseMisc) return null;
return Misc.fromText(id, text, miscGroups);
}
}
diff --git a/src/model/scenery.js b/src/model/scenery.js
index f003ff6..392be7f 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;
getBounds () {
@@ -32,6 +33,9 @@ export default class Scenery
if(object.getRenderBounds) {
this._updateBounds(object.getRenderBounds());
}
+ if (object.category === 'tracks') {
+ this._updateTrackElevationBounds(object);
+ }
}
applyObjects() {
@@ -69,10 +73,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;
+ })
+ }
}
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