Tracks color indication, slope calculations

+Added option to color the tracks by type or their slope
*Slightly refactored TrackRenderer component
*Fixed points calculations in StandardTrack to include track's slope
This commit is contained in:
izawartka 2025-06-01 14:36:42 +02:00
parent 5c631c071b
commit 0b77a5a782
11 changed files with 164 additions and 63 deletions

View File

@ -4,6 +4,7 @@ import Constants from "../helpers/constants";
export default function SettingsManager(props) {
const [ layers, setLayers ] = useState([]);
const [ trackColorMode, setTrackColorMode ] = useState(Constants.trackColorModes[0].id);
const [ settingsLoaded, setSettingsLoaded ] = useState(false);
const loadLayersSettings = useCallback(() => {
@ -22,7 +23,8 @@ export default function SettingsManager(props) {
return (
<SettingsContext.Provider value={{
layers, setLayers
layers, setLayers,
trackColorMode, setTrackColorMode
}}>
{ props.children }
</SettingsContext.Provider>

View File

@ -5,7 +5,6 @@
.map svg path.track {
stroke-width: 1.44px;
stroke: #aaa;
}
.map svg text {

View File

@ -1,67 +1,101 @@
import { useContext } from "react";
import SettingsContext from "../../../contexts/SettingsContext";
import Constants from "../../../helpers/constants";
export default function TrackRenderer(props) {
const { object } = props;
const { object } = props;
const { trackColorMode } = useContext(SettingsContext);
const path = getTrackPath(object);
const color = getTrackColor(object, trackColorMode);
const defs = getTrackDefs(object, trackColorMode);
return (
<g>
{defs}
<path
d={path}
id={`track-${object.id}`}
stroke={color}
className="track"
/>
</g>
);
}
function getTrackPath(object) {
switch (object.type) {
case "StandardTrack":
case "PointTrack":
return <StandardTrackR {...props} />;
const [x1, y1] = object.points.start.toSVGCoords();
const [x2, y2] = object.points.end.toSVGCoords();
const ra = Math.abs(object.r);
if (object.r < 0) {
return `M${x1},${y1} A${ra} ${ra} 0 0 1 ${x2},${y2}`;
} else if (object.r > 0) {
return `M${x2},${y2} A${ra} ${ra} 0 0 1 ${x1},${y1}`;
} else {
return `M${x1},${y1} L${x2},${y2}`;
}
case "BezierTrack":
return <BezierTrackR {...props} />;
const [bx1, by1] = object.points.start.toSVGCoords();
const [bcx1, bcy1] = object.points.control1.toSVGCoords();
const [bx2, by2] = object.points.end.toSVGCoords();
const [bcx2, bcy2] = object.points.control2.toSVGCoords();
return `M${bx1},${by1} C${bcx1},${bcy1} ${bcx2},${bcy2} ${bx2},${by2}`;
default:
console.warn(`No renderer component for track type: ${object.type}`);
return null;
console.warn(`TrackRenderer: Unsupported track type: ${object.type}`);
}
}
function StandardTrackR(props) {
const { object } = props;
function getTrackColor(object, trackColorMode) {
switch (trackColorMode) {
case "none":
default:
return "#aaa";
case "type":
switch (object.type) {
case "StandardTrack":
default:
return "#00a";
case "PointTrack":
return "#0a0";
case "BezierTrack":
return "#aa8";
}
case "slope":
return `url(#track-slope-${object.id})`;
}
}
function getTrackDefs(object, trackColorMode) {
if (trackColorMode !== "slope") return null;
const [x1, y1] = object.points.start.toSVGCoords();
const [x2, y2] = object.points.end.toSVGCoords();
const ra = Math.abs(object.r);
let d;
if (object.r < 0) {
d = `M${x1},${y1} A${ra} ${ra} 0 0 1 ${x2},${y2}`;
} else if (object.r > 0) {
d = `M${x2},${y2} A${ra} ${ra} 0 0 1 ${x1},${y1}`;
} else {
d = `M${x1},${y1} L${x2},${y2}`;
}
const color = Constants.map.useTrackColors ? (
object.r === 0 ? "rgb(0, 255, 255)" : "rgb(0, 0, 255)"
) : undefined;
const gradId = `track-slope-${object.id}`;
const startColor = getSlopeColor(object.start_slope);
const endColor = getSlopeColor(object.end_slope);
return (
<path
d={d}
id={`track-${object.id}`}
stroke={color}
className="track"
/>
<defs>
<linearGradient
id={gradId}
gradientUnits="userSpaceOnUse"
x1={x1}
y1={y1}
x2={x2}
y2={y2}
>
<stop offset="0%" stopColor={startColor} />
<stop offset="100%" stopColor={endColor} />
</linearGradient>
</defs>
);
}
function BezierTrackR(props) {
const { object } = props;
const [x1, y1] = object.points.start.toSVGCoords();
const [cx1, cy1] = object.points.control1.toSVGCoords();
const [x2, y2] = object.points.end.toSVGCoords();
const [cx2, cy2] = object.points.control2.toSVGCoords();
const d = `M${x1},${y1} C${cx1},${cy1} ${cx2},${cy2} ${x2},${y2}`;
const color = Constants.map.useTrackColors ? "rgb(0, 255, 0)" : undefined;
return (
<path
d={d}
id={`btrack-${object.id}`}
stroke={color}
className="track"
/>
);
function getSlopeColor(slope) {
const blue = Math.min(255, Math.max(0, 128 + Math.abs(slope * Constants.map.trackSlopeScale)));
return `rgb(128, 128, ${blue})`;
}

View File

@ -0,0 +1,28 @@
import { useContext } from "react";
import SettingsContext from "../../contexts/SettingsContext";
import Constants from "../../helpers/constants";
export default function SelTrackColorMode(props) {
const { trackColorMode, setTrackColorMode } = useContext(SettingsContext);
const handleColorModeChange = (event) => {
setTrackColorMode(event.target.value);
};
return (
<div className="track-color-mode-select">
<label htmlFor="track-color-mode">Track Color Mode:</label><br />
<select
id="track-color-mode"
value={trackColorMode}
onChange={handleColorModeChange}
>
{Constants.trackColorModes.map((mode) => (
<option key={mode.id} value={mode.id}>
{mode.name}
</option>
))}
</select>
</div>
);
}

View File

@ -2,6 +2,7 @@ import {useContext} from 'react';
import MainContext from '../../contexts/MainContext';
import './SideMenu.css';
import LayersMenu from './LayersMenu';
import SelTrackColorMode from './SelTrackColorMode';
export default function SideMenu() {
const { sideMenuOpen } = useContext(MainContext);
@ -14,6 +15,7 @@ export default function SideMenu() {
<div className='side-menu-wrapper'>
<div className='side-menu'>
<LayersMenu />
<SelTrackColorMode />
</div>
</div>
);

View File

@ -1,7 +1,7 @@
const Constants = {
map: {
useTrackColors: false,
zoomSensitivity: 0.002,
trackSlopeScale: 10,
},
warnings: {
all: false, // enable all warnings
@ -41,6 +41,20 @@ const Constants = {
name: 'Routes',
default: true,
}
],
trackColorModes: [
{
id: 'none',
name: 'None',
},
{
id: 'type',
name: 'Type',
},
{
id: 'slope',
name: 'Slope'
}
]
};

View File

@ -11,8 +11,8 @@ export default class BezierTrack extends Track
control2: Vector3.zero()
};
constructor(id, start, control1, end, control2, rot, len, r, previd, nextid, id_station, id_isolation, maxspeed, derailspeed) {
super(id, start, rot, len, r, previd, nextid, id_station, id_isolation, maxspeed, derailspeed);
constructor(id, start, control1, end, control2, rot, len, r, previd, nextid, id_station, start_slope, end_slope, id_isolation, maxspeed, derailspeed) {
super(id, start, rot, len, r, previd, nextid, id_station, start_slope, end_slope, id_isolation, maxspeed, derailspeed);
Object.assign(this.points, {
start,
@ -37,6 +37,7 @@ export default class BezierTrack extends Track
values[15], // previd
values[16], // nextid
values[17], // id_station
...Track.slopesFromText(values[18]), // start_slope, end_slope
values[21], // id_isolation
parseFloat(values[24]), // maxspeed
parseFloat(values[25]) // derailspeed

View File

@ -10,12 +10,12 @@ export default class PointTrack extends Track
end: Vector3.zero()
};
constructor(id, start, end, r, previd, nextid, id_station, id_isolation, maxspeed, derailspeed) {
constructor(id, start, end, r, previd, nextid, id_station, start_slope, end_slope, id_isolation, maxspeed, derailspeed) {
const ry = AngleHelper.radToDeg(start.atanY(end));
const rot = new Vector3(0, ry, 0);
// TODO: fix rx and rz = 0
// TODO: calculate length
super(id, start, rot, null, r, previd, nextid, id_station, id_isolation, maxspeed, derailspeed);
super(id, start, rot, null, r, previd, nextid, id_station, start_slope, end_slope, id_isolation, maxspeed, derailspeed);
Object.assign(this.points, {
start, end

View File

@ -11,8 +11,8 @@ export default class StandardTrack extends Track
circleCenter: Vector3.zero()
};
constructor(id, start, rot, len, r, previd, nextid, id_station, id_isolation, maxspeed, derailspeed) {
super(id, start, rot, len, r, previd, nextid, id_station, id_isolation, maxspeed, derailspeed);
constructor(id, start, rot, len, r, previd, nextid, id_station, start_slope, end_slope, id_isolation, maxspeed, derailspeed) {
super(id, start, rot, len, r, previd, nextid, id_station, start_slope, end_slope, id_isolation, maxspeed, derailspeed);
this._calcPoints();
}
@ -27,6 +27,7 @@ export default class StandardTrack extends Track
values[11], // previd
values[12], // nextid
values[13], // id_station
...Track.slopesFromText(values[14]), // start_slope, end_slope
values[17], // id_isolation
parseFloat(values[20]), // maxspeed
parseFloat(values[21]) // derailspeed
@ -48,5 +49,13 @@ 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;
const endHeightDiff = this.end_slope * this.len / 1000;
this.points.end.y += startHeightDiff + (endHeightDiff - startHeightDiff) / 2;
}
}
}

View File

@ -122,6 +122,8 @@ export default class Switch extends SceneryObject {
from,
to,
this.id_switch,
0, // start_slope
0, // end_slope
this.id_isolation,
this.maxspeed,
this.derailspeed

View File

@ -9,17 +9,27 @@ export default class Track extends SceneryObject {
derailspeed;
category = "tracks";
type = "Track";
outs = [];
previd;
nextid;
prevTrack = null;
nextTrack = null;
start_slope;
end_slope;
constructor(id, pos, rot, len, r, previd, nextid, id_station, id_isolation, maxspeed, derailspeed) {
constructor(id, pos, rot, len, r, previd, nextid, id_station, start_slope, end_slope, id_isolation, maxspeed, derailspeed) {
super(id, pos, rot);
Object.assign(this, {
len, r,
id_station, id_isolation,
previd, nextid,
id_station,
start_slope, end_slope,
id_isolation,
maxspeed, derailspeed
});
}
this.outs = [previd, nextid];
static slopesFromText(text) {
return text.split(",", 2);
}
getCloserEndPos(pos) {