Merge branch 'refs/heads/main' into align-track
# Conflicts: # src/components/map/object-renderers/TrackRenderer.js
This commit is contained in:
commit
a653a5ac2e
@ -8,20 +8,22 @@ import SettingsManager from './SettingsManager';
|
|||||||
import LocList from './loclist/LocList';
|
import LocList from './loclist/LocList';
|
||||||
import DialogManager from './dialog/DialogManager';
|
import DialogManager from './dialog/DialogManager';
|
||||||
import DistanceMeterContext from '../contexts/DistanceMeterContext';
|
import DistanceMeterContext from '../contexts/DistanceMeterContext';
|
||||||
|
import {GradientsManager} from "./GradientsManager";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [scenery, setScenery] = useState(null);
|
const [scenery, setScenery] = useState(null);
|
||||||
const [sideMenuOpen, setSideMenuOpen] = useState(false);
|
const [sideMenuOpen, setSideMenuOpen] = useState(false);
|
||||||
const [distancePoints, setDistancePoints] = useState(null);
|
const [distancePoints, setDistancePoints] = useState(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsManager>
|
<SettingsManager>
|
||||||
|
<GradientsManager scenery={scenery}>
|
||||||
<DistanceMeterContext.Provider value={{distancePoints, setDistancePoints}}>
|
<DistanceMeterContext.Provider value={{distancePoints, setDistancePoints}}>
|
||||||
<MainContext.Provider value={{
|
<MainContext.Provider value={{
|
||||||
scenery, setScenery,
|
scenery, setScenery,
|
||||||
isLoading, setIsLoading,
|
isLoading, setIsLoading,
|
||||||
sideMenuOpen, setSideMenuOpen
|
sideMenuOpen, setSideMenuOpen,
|
||||||
}}>
|
}}>
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<Toolbar />
|
<Toolbar />
|
||||||
@ -34,9 +36,9 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</MainContext.Provider>
|
</MainContext.Provider>
|
||||||
</DistanceMeterContext.Provider>
|
</DistanceMeterContext.Provider>
|
||||||
|
</GradientsManager>
|
||||||
</SettingsManager>
|
</SettingsManager>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
||||||
68
src/components/GradientsManager.js
Normal file
68
src/components/GradientsManager.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import Constants from "../helpers/constants";
|
||||||
|
import {useMemo} from "react";
|
||||||
|
import GradientsContext from "../contexts/GradientsContext";
|
||||||
|
|
||||||
|
function getMinMax(trackColorMode, scenery) {
|
||||||
|
if (trackColorMode === 'elevation') {
|
||||||
|
if (!scenery) return null;
|
||||||
|
if (scenery.trackElevationBounds.minY > scenery.trackElevationBounds.maxY) return null;
|
||||||
|
|
||||||
|
const min = scenery.trackElevationBounds.minY;
|
||||||
|
// Make sure diff > 0 even on flat sceneries
|
||||||
|
let diff = Math.max(scenery.trackElevationBounds.maxY - scenery.trackElevationBounds.minY, 1);
|
||||||
|
// Ensure `diff` is even so that the min, max and middle legend values are integers
|
||||||
|
diff = Math.ceil(diff / 2)*2;
|
||||||
|
const max = min + diff;
|
||||||
|
|
||||||
|
return [min, max];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGradientSettings(trackColorMode, scenery, def) {
|
||||||
|
if ('gradient' in def) return def.gradient;
|
||||||
|
if ('dynamicGradient' in def) {
|
||||||
|
const gradient = def.dynamicGradient;
|
||||||
|
const [minValue, maxValue] = getMinMax(trackColorMode, scenery) ?? [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,
|
||||||
|
startLegendAt0: gradient.startLegendAt0 ?? false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGradientDefs(scenery) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(Constants.trackColorModes).map(
|
||||||
|
([mode, def]) => [mode, createGradientSettings(mode, scenery, def)],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GradientsManager(props) {
|
||||||
|
const { children, scenery } = props;
|
||||||
|
|
||||||
|
const gradientDefs = useMemo(() => createGradientDefs(scenery), [scenery]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GradientsContext.Provider value={{
|
||||||
|
gradientDefs,
|
||||||
|
}}>
|
||||||
|
{ children }
|
||||||
|
</GradientsContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -128,3 +128,7 @@
|
|||||||
.map svg g.spawn-point path.background {
|
.map svg g.spawn-point path.background {
|
||||||
fill: #111;
|
fill: #111;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.map svg g.platform rect {
|
||||||
|
fill: #444;
|
||||||
|
}
|
||||||
|
|||||||
@ -11,8 +11,17 @@ import DerailerRenderer from './object-renderers/DerailerRenderer';
|
|||||||
import ElectrificationStatusPopup from './additional-layer-components/ElectrificationStatusPopup';
|
import ElectrificationStatusPopup from './additional-layer-components/ElectrificationStatusPopup';
|
||||||
import SpawnPointRenderer from './object-renderers/SpawnPointRenderer';
|
import SpawnPointRenderer from './object-renderers/SpawnPointRenderer';
|
||||||
import IsolationEndRenderer from './object-renderers/IsolationEndRenderer';
|
import IsolationEndRenderer from './object-renderers/IsolationEndRenderer';
|
||||||
|
import PlatformRenderer from './object-renderers/PlatformRenderer';
|
||||||
|
import Constants from '../../helpers/constants';
|
||||||
|
|
||||||
const ObjectRendererQueue = [
|
const ObjectRendererQueue = [
|
||||||
|
{
|
||||||
|
'name': 'platforms',
|
||||||
|
'category': 'misc',
|
||||||
|
'type': 'Platform',
|
||||||
|
'renderer': PlatformRenderer,
|
||||||
|
'cond': (layers) => layers['platforms'] && !Constants.parser.skipPlatforms
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'name': 'tracks',
|
'name': 'tracks',
|
||||||
'category': 'tracks',
|
'category': 'tracks',
|
||||||
|
|||||||
23
src/components/map/object-renderers/PlatformRenderer.js
Normal file
23
src/components/map/object-renderers/PlatformRenderer.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import Constants from "../../../helpers/constants";
|
||||||
|
|
||||||
|
export default function PlatformRenderer(props) {
|
||||||
|
const { object } = props;
|
||||||
|
const [x, y] = object.pos.toSVGCoords();
|
||||||
|
const tiltNotOk = object.yawData.tilt > Constants.map.platformMaxTilt;
|
||||||
|
if(!object.def || tiltNotOk) return null;
|
||||||
|
|
||||||
|
const [centerX, centerY] = object.def.center.toSVGCoords();
|
||||||
|
const sizeX = object.def.size.x;
|
||||||
|
const sizeY = object.def.size.z;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g className="platform" transform={`translate(${x}, ${y}) rotate(${object.yawData.yaw})`}>
|
||||||
|
<rect
|
||||||
|
x={-sizeX / 2 + centerX}
|
||||||
|
y={-sizeY / 2 + centerY}
|
||||||
|
width={sizeX}
|
||||||
|
height={sizeY}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -6,8 +6,9 @@ export default function SignalBoxRenderer(props) {
|
|||||||
const [x, y] = object.pos.toSVGCoords();
|
const [x, y] = object.pos.toSVGCoords();
|
||||||
const text = object.getPrintableSignalBoxName();
|
const text = object.getPrintableSignalBoxName();
|
||||||
|
|
||||||
const rotDiff = object.def.undef ? Math.round(object.rot.y / 90) * (-90) : object.def.rot * 90;
|
const yaw = object.yawData?.yaw || object.rot.y;
|
||||||
const rot = object.rot.y + rotDiff;
|
const rotDiff = object.def.undef ? Math.round(yaw / 90) * (-90) : object.def.rot * 90;
|
||||||
|
const rot = yaw + rotDiff;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g className="signalbox" transform={`translate(${x}, ${y}) rotate(${rot})`}>
|
<g className="signalbox" transform={`translate(${x}, ${y}) rotate(${rot})`}>
|
||||||
|
|||||||
@ -2,31 +2,35 @@ import React, { useContext } from "react";
|
|||||||
import SettingsContext from "../../../contexts/SettingsContext";
|
import SettingsContext from "../../../contexts/SettingsContext";
|
||||||
import Constants from "../../../helpers/constants";
|
import Constants from "../../../helpers/constants";
|
||||||
import { ElectrificationStatus } from "../../../model/electrification-status";
|
import { ElectrificationStatus } from "../../../model/electrification-status";
|
||||||
import MiscHelper from "../../../helpers/miscHelper";
|
|
||||||
import { setHoveredTrack, unsetHoveredTrack } from "../../../services/trackHoverInfoService";
|
import { setHoveredTrack, unsetHoveredTrack } from "../../../services/trackHoverInfoService";
|
||||||
import ZoomPanContext from "../../../contexts/ZoomPanContext";
|
import ZoomPanContext from "../../../contexts/ZoomPanContext";
|
||||||
|
import GradientsContext from "../../../contexts/GradientsContext";
|
||||||
|
import MiscHelper from "../../../helpers/miscHelper";
|
||||||
|
|
||||||
export default function TrackRenderer(props) {
|
export default function TrackRenderer(props) {
|
||||||
const { object } = props;
|
const { object } = props;
|
||||||
const { trackColorMode } = useContext(SettingsContext);
|
const { trackColorMode } = useContext(SettingsContext);
|
||||||
const {alignView} = useContext(ZoomPanContext);
|
const { gradientDefs } = useContext(GradientsContext);
|
||||||
|
const {alignView} = useContext(ZoomPanContext);
|
||||||
|
|
||||||
const onAlign = (event) => {
|
const onAlign = (event) => {
|
||||||
alignView(object.rot.y, event);
|
alignView(object.rot.y, event);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MemoizedTrackRenderer
|
<MemoizedTrackRenderer
|
||||||
object={object}
|
object={object}
|
||||||
trackColorMode={trackColorMode}
|
trackColorMode={trackColorMode}
|
||||||
onAlign={onAlign}/>
|
gradientDef={gradientDefs[trackColorMode] ?? null}
|
||||||
|
onAlign={onAlign}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const MemoizedTrackRenderer = React.memo(StatelessTrackRenderer);
|
const MemoizedTrackRenderer = React.memo(StatelessTrackRenderer);
|
||||||
|
|
||||||
function StatelessTrackRenderer(props) {
|
function StatelessTrackRenderer(props) {
|
||||||
const { object, trackColorMode , onAlign} = props;
|
const { object, trackColorMode, gradientDef, onAlign } = props;
|
||||||
|
|
||||||
if (object.points.start.distanceSq(object.points.end) < 0.001) {
|
if (object.points.start.distanceSq(object.points.end) < 0.001) {
|
||||||
return null;
|
return null;
|
||||||
@ -41,16 +45,16 @@ function StatelessTrackRenderer(props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onClick = (event) => {
|
const onClick = (event) => {
|
||||||
if (object.type !== 'StandardTrack' || object.r !== 0) return;
|
if (object.type !== 'StandardTrack' || object.r !== 0) return;
|
||||||
if (event.detail === 2) {
|
if (event.detail === 2) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
onAlign(event);
|
onAlign(event);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const path = getTrackPath(object);
|
const path = getTrackPath(object);
|
||||||
const color = getTrackColor(object, trackColorMode);
|
const color = getTrackColor(object, trackColorMode, gradientDef);
|
||||||
const defs = getTrackDefs(object, trackColorMode);
|
const defs = getTrackDefs(object, trackColorMode, gradientDef);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g id={`track-${object.id}`}>
|
<g id={`track-${object.id}`}>
|
||||||
@ -97,7 +101,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, gradientDef) {
|
||||||
const modeDef = Constants.trackColorModes[trackColorMode];
|
const modeDef = Constants.trackColorModes[trackColorMode];
|
||||||
|
|
||||||
switch (trackColorMode) {
|
switch (trackColorMode) {
|
||||||
@ -135,46 +150,50 @@ function getTrackColor(object, trackColorMode) {
|
|||||||
default:
|
default:
|
||||||
return modeDef.options[modeDef.optionDefault][0];
|
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":
|
case "max-speed":
|
||||||
if (!object.maxspeed) {
|
if (!object.maxspeed) {
|
||||||
if (object.type === 'RouteTrack') return modeDef.options['unknown'][0];
|
if (object.type === 'RouteTrack') return modeDef.options['unknown'][0];
|
||||||
return modeDef.options['derail'][0];
|
return modeDef.options['derail'][0];
|
||||||
}
|
}
|
||||||
return MiscHelper.getTrackGradient(modeDef.gradient, object.maxspeed);
|
return MiscHelper.getTrackGradientColor(gradientDef, object.maxspeed);
|
||||||
|
|
||||||
|
case "elevation":
|
||||||
|
case "slope":
|
||||||
|
const [startValue, endValue] = getGradientValues(object, trackColorMode);
|
||||||
|
if (startValue === endValue) return MiscHelper.getTrackGradientColor(gradientDef, startValue);
|
||||||
|
return `url(#track-${trackColorMode}-${object.id})`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTrackDefs(object, trackColorMode) {
|
function getGradientDefs(object, trackColorMode, gradientDef, startValue, endValue) {
|
||||||
if (trackColorMode !== "slope") return null;
|
if (startValue === endValue) return null;
|
||||||
if (object.start_slope === object.end_slope) return null;
|
|
||||||
|
|
||||||
const [x1, y1] = object.points.start.toSVGCoords();
|
const [x1, y1] = object.points.start.toSVGCoords();
|
||||||
const [x2, y2] = object.points.end.toSVGCoords();
|
const [x2, y2] = object.points.end.toSVGCoords();
|
||||||
const gradId = `track-slope-${object.id}`;
|
const gradId = `track-${trackColorMode}-${object.id}`;
|
||||||
const startColor = MiscHelper.getTrackGradient(Constants.trackColorModes['slope'].gradient, Math.abs(object.start_slope));
|
const startColor = MiscHelper.getTrackGradientColor(gradientDef, startValue);
|
||||||
const endColor = MiscHelper.getTrackGradient(Constants.trackColorModes['slope'].gradient, Math.abs(object.end_slope));
|
const endColor = MiscHelper.getTrackGradientColor(gradientDef, endValue);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id={gradId}
|
id={gradId}
|
||||||
gradientUnits="userSpaceOnUse"
|
gradientUnits="userSpaceOnUse"
|
||||||
x1={x1}
|
x1={x1}
|
||||||
y1={y1}
|
y1={y1}
|
||||||
x2={x2}
|
x2={x2}
|
||||||
y2={y2}
|
y2={y2}
|
||||||
>
|
>
|
||||||
<stop offset="0%" stopColor={startColor} />
|
<stop offset="0%" stopColor={startColor} />
|
||||||
<stop offset="100%" stopColor={endColor} />
|
<stop offset="100%" stopColor={endColor} />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTrackDefs(object, trackColorMode, gradientDef) {
|
||||||
|
const gradientVals = getGradientValues(object, trackColorMode);
|
||||||
|
if (gradientVals === null) return null;
|
||||||
|
const [startValue, endValue] = gradientVals;
|
||||||
|
return getGradientDefs(object, trackColorMode, gradientDef, startValue, endValue);
|
||||||
|
}
|
||||||
|
|||||||
@ -36,6 +36,8 @@ export default function LayersMenu(props) {
|
|||||||
function LayersMenuItem(props) {
|
function LayersMenuItem(props) {
|
||||||
const { layer, isChecked, onToggle } = props;
|
const { layer, isChecked, onToggle } = props;
|
||||||
|
|
||||||
|
if(layer.cond && !layer.cond()) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
<label>
|
<label>
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import Constants from "../../../helpers/constants";
|
import Constants from "../../../helpers/constants";
|
||||||
|
import {useContext} from "react";
|
||||||
|
import GradientsContext from "../../../contexts/GradientsContext";
|
||||||
import MiscHelper from "../../../helpers/miscHelper";
|
import MiscHelper from "../../../helpers/miscHelper";
|
||||||
|
|
||||||
export default function TrackColorModeLegend(props) {
|
export default function TrackColorModeLegend(props) {
|
||||||
@ -7,7 +9,7 @@ export default function TrackColorModeLegend(props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="track-color-mode-legend">
|
<div className="track-color-mode-legend">
|
||||||
<LegendGradient colorModeDef={colorModeDef} />
|
<LegendGradient trackColorMode={trackColorMode} />
|
||||||
<LegendOptions colorModeDef={colorModeDef} />
|
<LegendOptions colorModeDef={colorModeDef} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -22,7 +24,7 @@ function LegendOptions(props) {
|
|||||||
<ul>
|
<ul>
|
||||||
{ Object.entries(colorModeDef.options).map(([key, optionDef]) => (
|
{ Object.entries(colorModeDef.options).map(([key, optionDef]) => (
|
||||||
<LegendOptionsItem key={key} optionDef={optionDef} />
|
<LegendOptionsItem key={key} optionDef={optionDef} />
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -39,23 +41,30 @@ function LegendOptionsItem(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function LegendGradient(props) {
|
function LegendGradient(props) {
|
||||||
const { colorModeDef } = props;
|
const { gradientDefs } = useContext(GradientsContext);
|
||||||
const gradientDef = colorModeDef?.gradient || null;
|
const { trackColorMode } = props;
|
||||||
|
const gradientDef = gradientDefs[trackColorMode];
|
||||||
if (!gradientDef) return null;
|
if (!gradientDef) return null;
|
||||||
|
|
||||||
const from = MiscHelper.getTrackGradient(gradientDef, gradientDef.legendMin);
|
const from = MiscHelper.getTrackGradientColor(gradientDef, gradientDef.legendMin);
|
||||||
const to = MiscHelper.getTrackGradient(gradientDef, gradientDef.legendMax);
|
const to = MiscHelper.getTrackGradientColor(gradientDef, gradientDef.legendMax);
|
||||||
const gradientStyle = `linear-gradient(to right, ${from}, ${to})`;
|
const gradientStyle = `linear-gradient(to right, ${from}, ${to})`;
|
||||||
|
|
||||||
const midVal = (gradientDef.legendMin + gradientDef.legendMax) / 2;
|
let { legendMin, legendMax } = gradientDef;
|
||||||
|
if (gradientDef.startLegendAt0) {
|
||||||
|
legendMax -= legendMin;
|
||||||
|
legendMin = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const midVal = (legendMin + legendMax) / 2;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="gradient">
|
<div className="gradient">
|
||||||
<div className="gradient-bar" style={{ background: gradientStyle}}></div>
|
<div className="gradient-bar" style={{ background: gradientStyle}}></div>
|
||||||
<div className="gradient-labels">
|
<div className="gradient-labels">
|
||||||
<span className="gradient-label">{gradientDef.legendMin} {gradientDef.unit}</span>
|
<span className="gradient-label">{legendMin} {gradientDef.unit}</span>
|
||||||
<span className="gradient-label">{midVal} {gradientDef.unit}</span>
|
<span className="gradient-label">{midVal} {gradientDef.unit}</span>
|
||||||
<span className="gradient-label">{gradientDef.legendMax} {gradientDef.unit}</span>
|
<span className="gradient-label">{legendMax} {gradientDef.unit}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
4
src/contexts/GradientsContext.js
Normal file
4
src/contexts/GradientsContext.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
const GradientsContext = createContext();
|
||||||
|
export default GradientsContext;
|
||||||
@ -5,7 +5,8 @@ const Constants = {
|
|||||||
zoomMin: 0.03,
|
zoomMin: 0.03,
|
||||||
zoomMax: 200.0,
|
zoomMax: 200.0,
|
||||||
rotationSensitivity: 0.2,
|
rotationSensitivity: 0.2,
|
||||||
forcePointerEvents: false
|
forcePointerEvents: false,
|
||||||
|
platformMaxTilt: 0.5
|
||||||
},
|
},
|
||||||
parser: {
|
parser: {
|
||||||
logSceneryAfterFinished: true,
|
logSceneryAfterFinished: true,
|
||||||
@ -20,7 +21,8 @@ const Constants = {
|
|||||||
attachSignsMaxDistanceX: 0.3,
|
attachSignsMaxDistanceX: 0.3,
|
||||||
attachSignsGridSize: 10,
|
attachSignsGridSize: 10,
|
||||||
logAttachedSigns: false,
|
logAttachedSigns: false,
|
||||||
skipBaseMisc: true
|
skipBaseMisc: true,
|
||||||
|
skipPlatforms: false
|
||||||
},
|
},
|
||||||
warnings: {
|
warnings: {
|
||||||
all: false, // enable all warnings
|
all: false, // enable all warnings
|
||||||
@ -63,6 +65,12 @@ const Constants = {
|
|||||||
fetchListUrl: `${process.env.PUBLIC_URL}/sceneries/sceneries.json`,
|
fetchListUrl: `${process.env.PUBLIC_URL}/sceneries/sceneries.json`,
|
||||||
},
|
},
|
||||||
layers: [
|
layers: [
|
||||||
|
{
|
||||||
|
id: 'platforms',
|
||||||
|
name: 'Platforms',
|
||||||
|
default: true,
|
||||||
|
cond: () => !Constants.parser.skipPlatforms,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'tracks',
|
id: 'tracks',
|
||||||
name: 'Tracks',
|
name: 'Tracks',
|
||||||
@ -151,12 +159,14 @@ const Constants = {
|
|||||||
},
|
},
|
||||||
'slope': {
|
'slope': {
|
||||||
name: 'Slope',
|
name: 'Slope',
|
||||||
|
type: 'gradient',
|
||||||
gradient: {
|
gradient: {
|
||||||
base: [128, 0, 128],
|
base: [128, 0, 128],
|
||||||
diff: [0, 25.6, 0],
|
diff: [0, 25.6, 0],
|
||||||
legendMin: 0,
|
legendMin: 0,
|
||||||
legendMax: 10,
|
legendMax: 10,
|
||||||
unit: '‰',
|
unit: '‰',
|
||||||
|
startLegendAt0: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'max-speed': {
|
'max-speed': {
|
||||||
@ -168,12 +178,25 @@ const Constants = {
|
|||||||
legendMin: 0,
|
legendMin: 0,
|
||||||
legendMax: 170,
|
legendMax: 170,
|
||||||
unit: 'km/h',
|
unit: 'km/h',
|
||||||
|
startLegendAt0: false,
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
'derail': ['#f22', 'Derail track'],
|
'derail': ['#f22', 'Derail track'],
|
||||||
'unknown': ['#aaa', 'Unknown speed'],
|
'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,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
export default class MiscHelper {
|
export default class MiscHelper {
|
||||||
static getTrackGradient(gradientDef, value) {
|
static getTrackGradientColor(gradientDef, value) {
|
||||||
const { base, diff } = gradientDef;
|
const { base, diff } = gradientDef;
|
||||||
const out = base.map((b, i) =>
|
const out = base.map((b, i) =>
|
||||||
Math.min(255, Math.max(0, b + diff[i] * value))
|
Math.min(255, Math.max(0, b + diff[i] * value))
|
||||||
);
|
);
|
||||||
|
|
||||||
return `rgb(${out[0]}, ${out[1]}, ${out[2]})`;
|
return `rgb(${out[0]}, ${out[1]}, ${out[2]})`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import Vector3 from "./vector3";
|
import Vector3 from "./vector3";
|
||||||
|
import Quaternion from "./quaternion";
|
||||||
|
|
||||||
export default class MiscGroup {
|
export default class MiscGroup {
|
||||||
pos;
|
pos;
|
||||||
rot;
|
rot;
|
||||||
|
quaternion;
|
||||||
|
|
||||||
constructor(pos, rot) {
|
constructor(pos, rot) {
|
||||||
this.pos = pos;
|
this.pos = pos;
|
||||||
@ -18,4 +20,13 @@ export default class MiscGroup {
|
|||||||
|
|
||||||
return object;
|
return object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getQuaternion() {
|
||||||
|
if (!this.quaternion) {
|
||||||
|
const rotRad = this.rot.multiply(Math.PI / 180);
|
||||||
|
this.quaternion = Quaternion.fromEulerAngles(rotRad).normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.quaternion;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,22 @@
|
|||||||
import AngleHelper from "../../helpers/angleHelper";
|
import AngleHelper from "../../helpers/angleHelper";
|
||||||
|
import Quaternion from "../quaternion";
|
||||||
import SceneryObject from "../scenery-object";
|
import SceneryObject from "../scenery-object";
|
||||||
import Vector3 from "../vector3";
|
import Vector3 from "../vector3";
|
||||||
|
|
||||||
export default class Misc extends SceneryObject {
|
export default class Misc extends SceneryObject {
|
||||||
prefab_name;
|
prefab_name;
|
||||||
name;
|
name;
|
||||||
|
yawData;
|
||||||
category = "misc";
|
category = "misc";
|
||||||
type = "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);
|
super(id, pos, rot);
|
||||||
Object.assign(this, {
|
Object.assign(this, {
|
||||||
misc_id,
|
misc_id,
|
||||||
prefab_name,
|
prefab_name,
|
||||||
name,
|
name,
|
||||||
|
yawData
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,19 +38,20 @@ export default class Misc extends SceneryObject {
|
|||||||
return object;
|
return object;
|
||||||
}
|
}
|
||||||
|
|
||||||
static applyGroupTransforms(pos, rot, miscGroups) {
|
static applyGroupTransforms(localPos, localRot, miscGroups) {
|
||||||
if(!miscGroups) return [ pos, rot ];
|
const localRotRad = localRot.multiply(Math.PI / 180);
|
||||||
|
let worldQuat = Quaternion.fromEulerAngles(localRotRad).normalize();
|
||||||
|
let worldPos = localPos.clone();
|
||||||
|
|
||||||
for (const group of miscGroups) {
|
for (const group of miscGroups) {
|
||||||
const rotRad = AngleHelper.degToRad(group.rot.y);
|
const groupQuat = group.getQuaternion();
|
||||||
const rotRadY = new Vector3(0, rotRad, 0);
|
worldPos = groupQuat.rotateVector(worldPos).add(group.pos);
|
||||||
pos = pos.rotate(rotRadY).add(group.pos);
|
worldQuat = groupQuat.multiply(worldQuat).normalize();
|
||||||
rot = rot.add(group.rot);
|
|
||||||
// TODO: Replace this with some fancy quaternion math maybe
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rot = AngleHelper.normalizeDegVector(rot);
|
const worldRot = worldQuat.toEulerAngles().multiply(180 / Math.PI);
|
||||||
|
const yawData = worldQuat.getMiscYawData();
|
||||||
return [ pos, rot ];
|
|
||||||
|
return [ worldPos, worldRot, yawData ];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,8 +7,8 @@ export default class Platform extends Misc {
|
|||||||
type = "Platform";
|
type = "Platform";
|
||||||
def = null;
|
def = null;
|
||||||
|
|
||||||
constructor(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, name);
|
super(id, misc_id, prefab_name, pos, rot, yawData, name);
|
||||||
|
|
||||||
this.def = Platform.getDef(name, prefab_name);
|
this.def = Platform.getDef(name, prefab_name);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,8 +8,8 @@ export default class SignalBox extends Misc {
|
|||||||
applied = false;
|
applied = false;
|
||||||
def = null;
|
def = null;
|
||||||
|
|
||||||
constructor(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, name);
|
super(id, misc_id, prefab_name, pos, rot, yawData, name);
|
||||||
|
|
||||||
this.def = SignalBox.getDef(name, prefab_name);
|
this.def = SignalBox.getDef(name, prefab_name);
|
||||||
}
|
}
|
||||||
|
|||||||
103
src/model/quaternion.js
Normal file
103
src/model/quaternion.js
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -115,11 +115,11 @@ export default class SceneryParser {
|
|||||||
return MainCamera.fromText(text);
|
return MainCamera.fromText(text);
|
||||||
case 'MiscGroup':
|
case 'MiscGroup':
|
||||||
const group = MiscGroup.fromText(text);
|
const group = MiscGroup.fromText(text);
|
||||||
currentMiscGroups.push(group);
|
currentMiscGroups.unshift(group);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
case 'EndMiscGroup':
|
case 'EndMiscGroup':
|
||||||
if(!currentMiscGroups.pop()) {
|
if(!currentMiscGroups.shift()) {
|
||||||
SceneryParserLog.warn('endMiscGroupWithoutStart', 'Unexpected EndMiscGroup found without a preceding MiscGroup');
|
SceneryParserLog.warn('endMiscGroupWithoutStart', 'Unexpected EndMiscGroup found without a preceding MiscGroup');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,12 +176,12 @@ export default class SceneryParser {
|
|||||||
const id = SceneryParser.nextMiscId++;
|
const id = SceneryParser.nextMiscId++;
|
||||||
|
|
||||||
if (Platform.isPlatform(prefabName)) {
|
if (Platform.isPlatform(prefabName)) {
|
||||||
|
if(Constants.parser.skipPlatforms) return null;
|
||||||
return Platform.fromText(id, text, miscGroups);
|
return Platform.fromText(id, text, miscGroups);
|
||||||
} else if(SignalBox.isSignalBox(prefabName)) {
|
} else if(SignalBox.isSignalBox(prefabName)) {
|
||||||
return SignalBox.fromText(id, text, miscGroups);
|
return SignalBox.fromText(id, text, miscGroups);
|
||||||
} else if(Constants.parser.skipBaseMisc) {
|
|
||||||
return null;
|
|
||||||
} else {
|
} else {
|
||||||
|
if(Constants.parser.skipBaseMisc) return null;
|
||||||
return Misc.fromText(id, text, miscGroups);
|
return Misc.fromText(id, text, miscGroups);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ export default class Scenery
|
|||||||
signalBoxes = [];
|
signalBoxes = [];
|
||||||
spawnPoints = [];
|
spawnPoints = [];
|
||||||
bounds = { minX: Infinity, minZ: Infinity, maxX: -Infinity, maxZ: -Infinity };
|
bounds = { minX: Infinity, minZ: Infinity, maxX: -Infinity, maxZ: -Infinity };
|
||||||
|
trackElevationBounds = { minY: Infinity, maxY: -Infinity };
|
||||||
electrificationResolved = ElectrificationResolutionStatus.NOT_RESOLVED;
|
electrificationResolved = ElectrificationResolutionStatus.NOT_RESOLVED;
|
||||||
|
|
||||||
getBounds () {
|
getBounds () {
|
||||||
@ -32,6 +33,9 @@ export default class Scenery
|
|||||||
if(object.getRenderBounds) {
|
if(object.getRenderBounds) {
|
||||||
this._updateBounds(object.getRenderBounds());
|
this._updateBounds(object.getRenderBounds());
|
||||||
}
|
}
|
||||||
|
if (object.category === 'tracks') {
|
||||||
|
this._updateTrackElevationBounds(object);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
applyObjects() {
|
applyObjects() {
|
||||||
@ -69,10 +73,17 @@ export default class Scenery
|
|||||||
if(!objBounds || !Array.isArray(objBounds)) return;
|
if(!objBounds || !Array.isArray(objBounds)) return;
|
||||||
|
|
||||||
objBounds.forEach(point => {
|
objBounds.forEach(point => {
|
||||||
if(point.x < this.bounds.minX) this.bounds.minX = point.x;
|
if (point.x < this.bounds.minX) this.bounds.minX = point.x;
|
||||||
if(point.z < this.bounds.minZ) this.bounds.minZ = point.z;
|
if (point.z < this.bounds.minZ) this.bounds.minZ = point.z;
|
||||||
if(point.x > this.bounds.maxX) this.bounds.maxX = point.x;
|
if (point.x > this.bounds.maxX) this.bounds.maxX = point.x;
|
||||||
if(point.z > this.bounds.maxZ) this.bounds.maxZ = point.z;
|
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;
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import sys
|
|||||||
#
|
#
|
||||||
# [ TD2 SCENERY PROCESSOR ]
|
# [ TD2 SCENERY PROCESSOR ]
|
||||||
# by masuo
|
# by masuo
|
||||||
# v1.6
|
# v1.7
|
||||||
#
|
#
|
||||||
|
|
||||||
BAD_WORDS = ['Fence', 'TerrainPoint', 'Wires']
|
BAD_WORDS = ['Fence', 'TerrainPoint', 'Wires']
|
||||||
@ -16,7 +16,7 @@ def should_exclude(line: str) -> bool:
|
|||||||
if line.startswith(word):
|
if line.startswith(word):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if line.startswith("Misc"):
|
if line.startswith("Misc") and not line.startswith("MiscGroup"):
|
||||||
if re.match(MISC_REGEX, line):
|
if re.match(MISC_REGEX, line):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user