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:
izawartka 2025-07-23 02:53:47 +02:00
parent 607599479e
commit 7578202e18
8 changed files with 37 additions and 24 deletions

View File

@ -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 (

View File

@ -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]);

View File

@ -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

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -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;

View File

@ -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);