Improved camera hook
+When the scenery loads, the view is now rotated to match the orientation of the main SignalBox +When the scenery loads, the zoom is set to the default value (5) +When clicking on a SignalBox, the camera rotation is set according to its orientation *Renamed the onCenter hook to setCamera and added rot and zoom options
This commit is contained in:
parent
607599479e
commit
7578202e18
@ -1,11 +1,11 @@
|
||||
import { useZoomPanEmitter } from '../../hooks/useZoomPubSub';
|
||||
|
||||
export default function ClickableLocation(props) {
|
||||
const { name, pos } = props;
|
||||
const { center } = useZoomPanEmitter();
|
||||
const { name, pos, rot } = props;
|
||||
const { setCamera } = useZoomPanEmitter();
|
||||
|
||||
const handleClick = () => {
|
||||
center(pos[0], pos[1]);
|
||||
setCamera(pos[0], pos[1], rot);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -10,9 +10,10 @@ export default function LocList(props) {
|
||||
if (!scenery?.signalBoxes) return null;
|
||||
return scenery.signalBoxes.map(signalBox => {
|
||||
const pos = signalBox.pos.toSVGCoords();
|
||||
const rot = -signalBox.getFinalRotation();
|
||||
const name = signalBox.getPrintableSignalBoxName();
|
||||
return (
|
||||
<ClickableLocation name={name} pos={pos} key={signalBox.id} />
|
||||
<ClickableLocation name={name} pos={pos} rot={rot} key={signalBox.id} />
|
||||
);
|
||||
});
|
||||
}, [scenery?.signalBoxes]);
|
||||
|
||||
@ -5,6 +5,10 @@ import { useZoomPanSubscriber, viewBox$, clientRect$, camera$ } from '../../hook
|
||||
import {mapRotation$} from '../../services/mapRotationService';
|
||||
import AngleHelper from "../../helpers/angleHelper";
|
||||
|
||||
function boundZoom(zoom) {
|
||||
return Math.max(Constants.map.zoomMin, Math.min(Constants.map.zoomMax, zoom));
|
||||
}
|
||||
|
||||
export default function ZoomPanWrapper({children}) {
|
||||
const wrapperRef = useRef(null);
|
||||
const svgRef = useRef(null);
|
||||
@ -83,9 +87,11 @@ export default function ZoomPanWrapper({children}) {
|
||||
};
|
||||
}, [handleResize]);
|
||||
|
||||
const centerOn = useCallback((cx, cy) => {
|
||||
const setCamera = useCallback((cx, cy, rot, zoom) => {
|
||||
cameraRef.current.x = cx;
|
||||
cameraRef.current.y = cy;
|
||||
if (rot || rot === 0) cameraRef.current.rotation = AngleHelper.normalizeDegAngle(rot);
|
||||
if (zoom) cameraRef.current.zoom = boundZoom(zoom);
|
||||
|
||||
scheduleCameraUpdate();
|
||||
}, [scheduleCameraUpdate]);
|
||||
@ -101,8 +107,8 @@ export default function ZoomPanWrapper({children}) {
|
||||
scheduleCameraUpdate();
|
||||
};
|
||||
|
||||
// Subscribe to any external `center` and `alignView` calls
|
||||
useZoomPanSubscriber(centerOn, alignView);
|
||||
// Subscribe to any external `setCamera` and `alignView` calls
|
||||
useZoomPanSubscriber(setCamera, alignView);
|
||||
|
||||
const onWheel = (e) => {
|
||||
const { left, top } = clientRectRef.current;
|
||||
@ -110,7 +116,7 @@ export default function ZoomPanWrapper({children}) {
|
||||
const { w, h } = viewBoxRef.current;
|
||||
|
||||
const newZoomCalc = cameraRef.current.zoom * (1.0 - (e.deltaY * Constants.map.zoomSensitivity));
|
||||
const newZoom = Math.max(Constants.map.zoomMin, Math.min(Constants.map.zoomMax, newZoomCalc));
|
||||
const newZoom = boundZoom(newZoomCalc);
|
||||
cameraRef.current.zoom = newZoom;
|
||||
|
||||
// screen space cursor pos
|
||||
|
||||
@ -6,14 +6,14 @@ import { resetHoveredTracksStack } from "../../services/trackHoverInfoService";
|
||||
|
||||
export default function CustomFileSelect() {
|
||||
const {setScenery, setIsLoading} = useContext(MainContext);
|
||||
const { center } = useZoomPanEmitter();
|
||||
const { setCamera } = useZoomPanEmitter();
|
||||
|
||||
const handleFileChange = async (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
setIsLoading(true);
|
||||
const scenery = await SceneryFilesHelper.loadCustom(file, center);
|
||||
const scenery = await SceneryFilesHelper.loadCustom(file, setCamera);
|
||||
setScenery(scenery);
|
||||
resetHoveredTracksStack();
|
||||
setIsLoading(false);
|
||||
|
||||
@ -9,13 +9,13 @@ import { resetHoveredTracksStack } from '../../services/trackHoverInfoService';
|
||||
export default function SceneriesList(props) {
|
||||
const {setScenery, setIsLoading} = useContext(MainContext);
|
||||
const [ sceneriesListData, setSceneriesListData ] = useState(null);
|
||||
const { center } = useZoomPanEmitter();
|
||||
const { setCamera } = useZoomPanEmitter();
|
||||
|
||||
const handleSelectChange = async (event) => {
|
||||
const name = event.target.value;
|
||||
|
||||
setIsLoading(true);
|
||||
const scenery = await SceneryFilesHelper.load(name, center);
|
||||
const scenery = await SceneryFilesHelper.load(name, setCamera);
|
||||
setScenery(scenery);
|
||||
resetHoveredTracksStack();
|
||||
setIsLoading(false);
|
||||
|
||||
@ -4,6 +4,7 @@ const Constants = {
|
||||
zoomSensitivity: 0.002,
|
||||
zoomMin: 0.03,
|
||||
zoomMax: 200.0,
|
||||
zoomDefault: 5,
|
||||
rotationSensitivity: 0.2,
|
||||
forcePointerEvents: false,
|
||||
platformMaxTilt: 0.5
|
||||
|
||||
@ -30,7 +30,7 @@ export default class SceneryFilesHelper {
|
||||
return await this.fetch(url, false);
|
||||
}
|
||||
|
||||
static async load(name, centerFn = null) {
|
||||
static async load(name, setCameraFn = null) {
|
||||
let file;
|
||||
try {
|
||||
file = await this.getScenery(name);
|
||||
@ -43,10 +43,10 @@ export default class SceneryFilesHelper {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await this.loadCustom(file, centerFn);
|
||||
return await this.loadCustom(file, setCameraFn);
|
||||
}
|
||||
|
||||
static async loadCustom(file, centerFn = null) {
|
||||
static async loadCustom(file, setCameraFn = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
SceneryParserLog.clear();
|
||||
const reader = new FileReader();
|
||||
@ -56,8 +56,13 @@ export default class SceneryFilesHelper {
|
||||
let loadingError = null;
|
||||
try {
|
||||
scenery = SceneryParser.fromText(e.target?.result);
|
||||
const mainSignalBoxPos = scenery.signalBoxes?.[0]?.pos || null;
|
||||
if (mainSignalBoxPos) centerFn(...mainSignalBoxPos.toSVGCoords());
|
||||
const mainSignalBox = scenery.signalBoxes?.[0];
|
||||
if(mainSignalBox) {
|
||||
const sbPos = mainSignalBox.pos.toSVGCoords();
|
||||
const sbRot = -mainSignalBox.getFinalRotation();
|
||||
setCameraFn(...sbPos, sbRot, Constants.map.zoomDefault);
|
||||
}
|
||||
else setCameraFn(0, 0, 0, Constants.map.zoomDefault);
|
||||
} catch (error) {
|
||||
scenery = null;
|
||||
loadingError = error;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Subject, BehaviorSubject } from 'rxjs';
|
||||
|
||||
const zoomCenter$ = new Subject();
|
||||
const setCamera$ = new Subject();
|
||||
const viewAlign$ = new Subject();
|
||||
|
||||
// BehaviorSubjects to store current viewBox, clientRect and camera transform
|
||||
@ -21,13 +21,13 @@ export function getCurrentCamera() {
|
||||
return camera$.getValue();
|
||||
}
|
||||
|
||||
export function useZoomPanSubscriber(onCenter, onAlign) {
|
||||
export function useZoomPanSubscriber(setCamera, onAlign) {
|
||||
useEffect(() => {
|
||||
const sub = zoomCenter$.subscribe(({ x, y }) => {
|
||||
onCenter(x, y);
|
||||
const sub = setCamera$.subscribe(({ x, y, rot, zoom }) => {
|
||||
setCamera(x, y, rot, zoom);
|
||||
});
|
||||
return () => sub.unsubscribe();
|
||||
}, [onCenter]);
|
||||
}, [setCamera]);
|
||||
|
||||
useEffect(() => {
|
||||
const sub = viewAlign$.subscribe((angleDeg) => {
|
||||
@ -39,8 +39,8 @@ export function useZoomPanSubscriber(onCenter, onAlign) {
|
||||
|
||||
export function useZoomPanEmitter() {
|
||||
return {
|
||||
center: (x, y) => {
|
||||
zoomCenter$.next({ x, y });
|
||||
setCamera: (x, y, rot = null, zoom = null) => {
|
||||
setCamera$.next({ x, y, rot, zoom });
|
||||
},
|
||||
alignView: (angleDeg) => {
|
||||
viewAlign$.next(angleDeg);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user