From 7578202e187b6c7345d8e1dae60190efc39ec896 Mon Sep 17 00:00:00 2001
From: izawartka <59137928+izawartka@users.noreply.github.com>
Date: Wed, 23 Jul 2025 02:53:47 +0200
Subject: [PATCH] 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
---
src/components/loclist/ClickableLocation.js | 6 +++---
src/components/loclist/LocList.js | 3 ++-
src/components/map/ZoomPanWrapper.js | 14 ++++++++++----
src/components/toolbar/CustomFileSelect.js | 4 ++--
src/components/toolbar/SceneriesList.js | 4 ++--
src/helpers/constants.js | 1 +
src/helpers/sceneryFilesHelper.js | 15 ++++++++++-----
src/hooks/useZoomPubSub.js | 14 +++++++-------
8 files changed, 37 insertions(+), 24 deletions(-)
diff --git a/src/components/loclist/ClickableLocation.js b/src/components/loclist/ClickableLocation.js
index 12f5261..866c75a 100644
--- a/src/components/loclist/ClickableLocation.js
+++ b/src/components/loclist/ClickableLocation.js
@@ -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 (
diff --git a/src/components/loclist/LocList.js b/src/components/loclist/LocList.js
index 62e5fc4..cfada3c 100644
--- a/src/components/loclist/LocList.js
+++ b/src/components/loclist/LocList.js
@@ -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 (
-
+
);
});
}, [scenery?.signalBoxes]);
diff --git a/src/components/map/ZoomPanWrapper.js b/src/components/map/ZoomPanWrapper.js
index 2a5295e..ec1f861 100644
--- a/src/components/map/ZoomPanWrapper.js
+++ b/src/components/map/ZoomPanWrapper.js
@@ -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
diff --git a/src/components/toolbar/CustomFileSelect.js b/src/components/toolbar/CustomFileSelect.js
index d1fbc90..59678e8 100644
--- a/src/components/toolbar/CustomFileSelect.js
+++ b/src/components/toolbar/CustomFileSelect.js
@@ -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);
diff --git a/src/components/toolbar/SceneriesList.js b/src/components/toolbar/SceneriesList.js
index 61fd586..6f0202e 100644
--- a/src/components/toolbar/SceneriesList.js
+++ b/src/components/toolbar/SceneriesList.js
@@ -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);
diff --git a/src/helpers/constants.js b/src/helpers/constants.js
index 64382d3..f6f044f 100644
--- a/src/helpers/constants.js
+++ b/src/helpers/constants.js
@@ -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
diff --git a/src/helpers/sceneryFilesHelper.js b/src/helpers/sceneryFilesHelper.js
index 928f7b3..5c1e004 100644
--- a/src/helpers/sceneryFilesHelper.js
+++ b/src/helpers/sceneryFilesHelper.js
@@ -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;
diff --git a/src/hooks/useZoomPubSub.js b/src/hooks/useZoomPubSub.js
index f948b73..d4115c5 100644
--- a/src/hooks/useZoomPubSub.js
+++ b/src/hooks/useZoomPubSub.js
@@ -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);