Merge branch 'main' into prefab-parser
# Conflicts: # src/components/map/object-renderers/TrackRenderer.js # src/components/map/track-hover-info/TrackHoverInfoPopup.js # src/helpers/angleHelper.js
This commit is contained in:
commit
9e5e6ab7cb
@ -2,7 +2,8 @@ import React, { useRef, useEffect, useCallback } from 'react';
|
||||
import './ZoomPanWrapper.css';
|
||||
import Constants from '../../helpers/constants';
|
||||
import { useZoomPanSubscriber, viewBox$, clientRect$, camera$ } from '../../hooks/useZoomPubSub';
|
||||
import { mapRotation$ } from '../../services/mapRotationService';
|
||||
import {mapRotation$} from '../../services/mapRotationService';
|
||||
import AngleHelper from "../../helpers/angleHelper";
|
||||
|
||||
export default function ZoomPanWrapper({children}) {
|
||||
const wrapperRef = useRef(null);
|
||||
@ -18,13 +19,13 @@ export default function ZoomPanWrapper({children}) {
|
||||
const { x, y, w, h } = viewBoxRef.current;
|
||||
return `${x} ${y} ${w} ${h}`;
|
||||
};
|
||||
|
||||
|
||||
const updateViewBox = useCallback((viewBox) => {
|
||||
viewBoxRef.current = viewBox;
|
||||
svgRef.current.setAttribute('viewBox', getViewBoxString());
|
||||
viewBox$.next(viewBoxRef.current);
|
||||
}, []);
|
||||
|
||||
|
||||
const getCameraTransformString = () => {
|
||||
const { x, y, zoom, rotation } = cameraRef.current;
|
||||
|
||||
@ -42,7 +43,7 @@ export default function ZoomPanWrapper({children}) {
|
||||
mapRotation$.next(cameraRef.current.rotation);
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
const handleResize = useCallback(() => {
|
||||
const rect = wrapperRef.current?.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
@ -66,13 +67,13 @@ export default function ZoomPanWrapper({children}) {
|
||||
const cy = e.movementY / cameraRef.current.zoom;
|
||||
const cos = Math.cos(cameraRef.current.rotation * Math.PI / 180);
|
||||
const sin = Math.sin(cameraRef.current.rotation * Math.PI / 180);
|
||||
|
||||
|
||||
cameraRef.current.x -= cx * cos + cy * sin;
|
||||
cameraRef.current.y -= cy * cos - cx * sin;
|
||||
|
||||
scheduleCameraUpdate();
|
||||
};
|
||||
|
||||
|
||||
// update viewbox when the window resizes
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', handleResize);
|
||||
@ -81,16 +82,27 @@ export default function ZoomPanWrapper({children}) {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, [handleResize]);
|
||||
|
||||
|
||||
const centerOn = useCallback((cx, cy) => {
|
||||
cameraRef.current.x = cx;
|
||||
cameraRef.current.y = cy;
|
||||
|
||||
scheduleCameraUpdate();
|
||||
}, [scheduleCameraUpdate]);
|
||||
|
||||
// Subscribe to any external "center" calls
|
||||
useZoomPanSubscriber(centerOn);
|
||||
|
||||
const alignView = (angleDeg) => {
|
||||
// Find such relative rotation in the range [-45, 45] degrees that aligns the track
|
||||
// with y rotation angleDeg to the X or Y screen axis.
|
||||
const angleDifference = -angleDeg - cameraRef.current.rotation;
|
||||
let deltaAngle = AngleHelper.normalizeDegAngle(angleDifference, 90);
|
||||
if (deltaAngle > 45) deltaAngle -= 90;
|
||||
|
||||
cameraRef.current.rotation += deltaAngle;
|
||||
scheduleCameraUpdate();
|
||||
};
|
||||
|
||||
// Subscribe to any external `center` and `alignView` calls
|
||||
useZoomPanSubscriber(centerOn, alignView);
|
||||
|
||||
const onWheel = (e) => {
|
||||
const { left, top } = clientRectRef.current;
|
||||
@ -125,7 +137,7 @@ export default function ZoomPanWrapper({children}) {
|
||||
|
||||
scheduleCameraUpdate();
|
||||
};
|
||||
|
||||
|
||||
const onMouseDown = (e) => {
|
||||
e.preventDefault();
|
||||
isMouseDownRef.current = true;
|
||||
@ -146,7 +158,7 @@ export default function ZoomPanWrapper({children}) {
|
||||
e.preventDefault();
|
||||
isMouseDownRef.current = false;
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="zoom-pan-wrapper" ref={wrapperRef}>
|
||||
<svg
|
||||
|
||||
@ -1,64 +1,79 @@
|
||||
import React, { useContext } from "react";
|
||||
import React, {useContext} from "react";
|
||||
import SettingsContext from "../../../contexts/SettingsContext";
|
||||
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 GradientsContext from "../../../contexts/GradientsContext";
|
||||
import { TrackSource } from "../../../model/tracks/track";
|
||||
import {useZoomPanEmitter} from "../../../hooks/useZoomPubSub";
|
||||
|
||||
export default function TrackRenderer(props) {
|
||||
const { object } = props;
|
||||
const { trackColorMode } = useContext(SettingsContext);
|
||||
const { gradientDefs } = useContext(GradientsContext);
|
||||
const { object } = props;
|
||||
const { trackColorMode } = useContext(SettingsContext);
|
||||
const { gradientDefs } = useContext(GradientsContext);
|
||||
const { alignView } = useZoomPanEmitter();
|
||||
|
||||
return (
|
||||
<MemoizedTrackRenderer
|
||||
object={object}
|
||||
trackColorMode={trackColorMode}
|
||||
gradientDef={gradientDefs[trackColorMode] ?? null}
|
||||
/>
|
||||
);
|
||||
const onAlign = () => {
|
||||
alignView(object.rot.y);
|
||||
};
|
||||
|
||||
return (
|
||||
<MemoizedTrackRenderer
|
||||
object={object}
|
||||
trackColorMode={trackColorMode}
|
||||
gradientDef={gradientDefs[trackColorMode] ?? null}
|
||||
onAlign={onAlign}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const MemoizedTrackRenderer = React.memo(StatelessTrackRenderer);
|
||||
|
||||
function StatelessTrackRenderer(props) {
|
||||
const { object, trackColorMode, gradientDef } = props;
|
||||
const {object, trackColorMode, gradientDef, onAlign} = props;
|
||||
|
||||
if (object.points.start.distanceSq(object.points.end) < 0.001) {
|
||||
return null;
|
||||
}
|
||||
if (object.points.start.distanceSq(object.points.end) < 0.001) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onMouseEnter = () => {
|
||||
setHoveredTrack(object);
|
||||
};
|
||||
const onMouseEnter = () => {
|
||||
setHoveredTrack(object);
|
||||
};
|
||||
|
||||
const onMouseLeave = () => {
|
||||
unsetHoveredTrack(null);
|
||||
};
|
||||
const onMouseLeave = () => {
|
||||
unsetHoveredTrack(null);
|
||||
};
|
||||
|
||||
const path = getTrackPath(object);
|
||||
const color = getTrackColor(object, trackColorMode, gradientDef);
|
||||
const defs = getTrackDefs(object, trackColorMode, gradientDef);
|
||||
const onClick = (event) => {
|
||||
if (object.type !== 'StandardTrack' || object.r !== 0) return;
|
||||
if (event.detail === 2) {
|
||||
event.preventDefault();
|
||||
onAlign(event);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<g id={`track-${object.id}`}>
|
||||
{defs}
|
||||
<path
|
||||
d={path}
|
||||
stroke={color}
|
||||
className="track-unscaled"
|
||||
/>
|
||||
<path
|
||||
d={path}
|
||||
stroke={color}
|
||||
className="track"
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
const path = getTrackPath(object);
|
||||
const color = getTrackColor(object, trackColorMode, gradientDef);
|
||||
const defs = getTrackDefs(object, trackColorMode, gradientDef);
|
||||
|
||||
return (
|
||||
<g id={`track-${object.id}`}>
|
||||
{defs}
|
||||
<path
|
||||
d={path}
|
||||
stroke={color}
|
||||
className="track-unscaled"
|
||||
/>
|
||||
<path
|
||||
d={path}
|
||||
stroke={color}
|
||||
className="track"
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onClick={onClick}/>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
function getTrackPath(object) {
|
||||
@ -99,57 +114,57 @@ function getGradientValues(object, trackColorMode) {
|
||||
}
|
||||
|
||||
function getTrackColor(object, trackColorMode, gradientDef) {
|
||||
const modeDef = Constants.trackColorModes[trackColorMode];
|
||||
const modeDef = Constants.trackColorModes[trackColorMode];
|
||||
|
||||
switch (trackColorMode) {
|
||||
case "standard":
|
||||
default:
|
||||
if(object.prefab_name && object.prefab_name.includes('trans-mat'))
|
||||
return modeDef.options['invisible'][0];
|
||||
|
||||
return modeDef.options[modeDef.optionDefault][0];
|
||||
|
||||
case "electrification":
|
||||
switch(object.electrificationStatus) {
|
||||
case ElectrificationStatus.NOT_CHECKED:
|
||||
return modeDef.options['not-checked'][0];
|
||||
case ElectrificationStatus.NON_ELECTRIFIED:
|
||||
return modeDef.options['non-electrified'][0];
|
||||
case ElectrificationStatus.ELECTRIFIED:
|
||||
return modeDef.options['electrified'][0];
|
||||
case ElectrificationStatus.CONFLICT:
|
||||
return modeDef.options['conflict'][0];
|
||||
switch (trackColorMode) {
|
||||
case "standard":
|
||||
default:
|
||||
return modeDef.options[modeDef.optionDefault][0];
|
||||
}
|
||||
if (object.prefab_name && object.prefab_name.includes('trans-mat'))
|
||||
return modeDef.options['invisible'][0];
|
||||
|
||||
case "type":
|
||||
switch (object.source) {
|
||||
case TrackSource.STANDARD:
|
||||
return modeDef.options['standard-track'][0];
|
||||
case TrackSource.SWITCH:
|
||||
return modeDef.options['point-track'][0];
|
||||
case TrackSource.ROUTE:
|
||||
return modeDef.options['route-track'][0];
|
||||
case TrackSource.BEZIER:
|
||||
return modeDef.options['bezier-track'][0];
|
||||
default:
|
||||
return modeDef.options[modeDef.optionDefault][0];
|
||||
}
|
||||
return modeDef.options[modeDef.optionDefault][0];
|
||||
|
||||
case "max-speed":
|
||||
if (!object.maxspeed) {
|
||||
if (object.type === 'RouteTrack') return modeDef.options['unknown'][0];
|
||||
return modeDef.options['derail'][0];
|
||||
}
|
||||
return MiscHelper.getTrackGradientColor(gradientDef, object.maxspeed);
|
||||
case "electrification":
|
||||
switch (object.electrificationStatus) {
|
||||
case ElectrificationStatus.NOT_CHECKED:
|
||||
return modeDef.options['not-checked'][0];
|
||||
case ElectrificationStatus.NON_ELECTRIFIED:
|
||||
return modeDef.options['non-electrified'][0];
|
||||
case ElectrificationStatus.ELECTRIFIED:
|
||||
return modeDef.options['electrified'][0];
|
||||
case ElectrificationStatus.CONFLICT:
|
||||
return modeDef.options['conflict'][0];
|
||||
default:
|
||||
return modeDef.options[modeDef.optionDefault][0];
|
||||
}
|
||||
|
||||
case "elevation":
|
||||
case "slope":
|
||||
const [startValue, endValue] = getGradientValues(object, trackColorMode);
|
||||
if (startValue === endValue) return MiscHelper.getTrackGradientColor(gradientDef, startValue);
|
||||
return `url(#track-${trackColorMode}-${object.id})`;
|
||||
}
|
||||
case "type":
|
||||
switch (object.source) {
|
||||
case TrackSource.STANDARD:
|
||||
return modeDef.options['standard-track'][0];
|
||||
case TrackSource.SWITCH:
|
||||
return modeDef.options['point-track'][0];
|
||||
case TrackSource.ROUTE:
|
||||
return modeDef.options['route-track'][0];
|
||||
case TrackSource.BEZIER:
|
||||
return modeDef.options['bezier-track'][0];
|
||||
default:
|
||||
return modeDef.options[modeDef.optionDefault][0];
|
||||
}
|
||||
|
||||
case "max-speed":
|
||||
if (!object.maxspeed) {
|
||||
if (object.type === 'RouteTrack') return modeDef.options['unknown'][0];
|
||||
return modeDef.options['derail'][0];
|
||||
}
|
||||
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 getGradientDefs(object, trackColorMode, gradientDef, startValue, endValue) {
|
||||
@ -170,16 +185,16 @@ function getGradientDefs(object, trackColorMode, gradientDef, startValue, endVal
|
||||
x2={x2}
|
||||
y2={y2}
|
||||
>
|
||||
<stop offset="0%" stopColor={startColor} />
|
||||
<stop offset="100%" stopColor={endColor} />
|
||||
<stop offset="0%" stopColor={startColor}/>
|
||||
<stop offset="100%" stopColor={endColor}/>
|
||||
</linearGradient>
|
||||
</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);
|
||||
const gradientVals = getGradientValues(object, trackColorMode);
|
||||
if (gradientVals === null) return null;
|
||||
const [startValue, endValue] = gradientVals;
|
||||
return getGradientDefs(object, trackColorMode, gradientDef, startValue, endValue);
|
||||
}
|
||||
|
||||
@ -12,8 +12,18 @@
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.track-hover-info-popup table {
|
||||
border-spacing: 2px;
|
||||
}
|
||||
|
||||
.track-hover-info-popup th {
|
||||
text-align: start;
|
||||
padding-right: 2em;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.track-hover-info-popup__align {
|
||||
margin-top: 1.5em;
|
||||
padding: 2px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ export default function TrackHoverInfo() {
|
||||
const { showTrackHoverInfo } = useContext(SettingsContext);
|
||||
|
||||
if (!showTrackHoverInfo) return null;
|
||||
|
||||
|
||||
return (
|
||||
<InnerTrackHoverInfo />
|
||||
);
|
||||
|
||||
@ -24,6 +24,9 @@ export default function TrackHoverInfoPopup(props) {
|
||||
<InfoPopupShapeItems track={track} />
|
||||
</tbody>
|
||||
</table>
|
||||
{track.type === 'StandardTrack' && track.r === 0 && <div className="track-hover-info-popup__align">
|
||||
Double click to align view
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -24,11 +24,11 @@ function rotationRadToDeg(vector) {
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeDegAngle(angle) {
|
||||
function normalizeDegAngle(angle, max = 360) {
|
||||
if (angle < 0) {
|
||||
return angle + 360 * Math.ceil(Math.abs(angle) / 360);
|
||||
} else if (angle >= 360) {
|
||||
return angle - 360 * Math.floor(angle / 360);
|
||||
return angle + max * Math.ceil(Math.abs(angle) / max);
|
||||
} else if (angle >= max) {
|
||||
return angle - max * Math.floor(angle / max);
|
||||
} else {
|
||||
return angle;
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import { useEffect } from 'react';
|
||||
import { Subject, BehaviorSubject } from 'rxjs';
|
||||
|
||||
const zoomCenter$ = new Subject();
|
||||
const viewAlign$ = new Subject();
|
||||
|
||||
// BehaviorSubjects to store current viewBox, clientRect and camera transform
|
||||
export const viewBox$ = new BehaviorSubject(null);
|
||||
@ -32,17 +33,27 @@ export function getCurrentCamera() {
|
||||
/**
|
||||
* useZoomPanSubscriber
|
||||
*
|
||||
* Registers a callback (onCenter) that will be invoked whenever
|
||||
* Registers an `onCenter` callback that will be invoked whenever
|
||||
* someone calls `center(x, y)` via the emitter. You should pass
|
||||
* a function that takes (x, y) and recenters your viewBox accordingly.
|
||||
*
|
||||
* Similarly, registers an `onAlign` callback that will be invoked
|
||||
* when someone calls `alignView(angleDeg)`.
|
||||
*/
|
||||
export function useZoomPanSubscriber(onCenter) {
|
||||
export function useZoomPanSubscriber(onCenter, onAlign) {
|
||||
useEffect(() => {
|
||||
const sub = zoomCenter$.subscribe(({ x, y }) => {
|
||||
onCenter(x, y);
|
||||
});
|
||||
return () => sub.unsubscribe();
|
||||
}, [onCenter]);
|
||||
|
||||
useEffect(() => {
|
||||
const sub = viewAlign$.subscribe((angleDeg) => {
|
||||
onAlign(angleDeg);
|
||||
});
|
||||
return () => sub.unsubscribe();
|
||||
}, [onAlign]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -57,5 +68,8 @@ export function useZoomPanEmitter() {
|
||||
center: (x, y) => {
|
||||
zoomCenter$.next({ x, y });
|
||||
},
|
||||
alignView: (angleDeg) => {
|
||||
viewAlign$.next(angleDeg);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user