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'; import { useZoomPanEmitter } from '../../hooks/useZoomPubSub';
export default function ClickableLocation(props) { export default function ClickableLocation(props) {
const { name, pos } = props; const { name, pos, rot } = props;
const { center } = useZoomPanEmitter(); const { setCamera } = useZoomPanEmitter();
const handleClick = () => { const handleClick = () => {
center(pos[0], pos[1]); setCamera(pos[0], pos[1], rot);
}; };
return ( return (

View File

@ -10,9 +10,10 @@ export default function LocList(props) {
if (!scenery?.signalBoxes) return null; if (!scenery?.signalBoxes) return null;
return scenery.signalBoxes.map(signalBox => { return scenery.signalBoxes.map(signalBox => {
const pos = signalBox.pos.toSVGCoords(); const pos = signalBox.pos.toSVGCoords();
const rot = -signalBox.getFinalRotation();
const name = signalBox.getPrintableSignalBoxName(); const name = signalBox.getPrintableSignalBoxName();
return ( return (
<ClickableLocation name={name} pos={pos} key={signalBox.id} /> <ClickableLocation name={name} pos={pos} rot={rot} key={signalBox.id} />
); );
}); });
}, [scenery?.signalBoxes]); }, [scenery?.signalBoxes]);

View File

@ -5,6 +5,10 @@ import { useZoomPanSubscriber, viewBox$, clientRect$, camera$ } from '../../hook
import {mapRotation$} from '../../services/mapRotationService'; import {mapRotation$} from '../../services/mapRotationService';
import AngleHelper from "../../helpers/angleHelper"; 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}) { export default function ZoomPanWrapper({children}) {
const wrapperRef = useRef(null); const wrapperRef = useRef(null);
const svgRef = useRef(null); const svgRef = useRef(null);
@ -83,9 +87,11 @@ export default function ZoomPanWrapper({children}) {
}; };
}, [handleResize]); }, [handleResize]);
const centerOn = useCallback((cx, cy) => { const setCamera = useCallback((cx, cy, rot, zoom) => {
cameraRef.current.x = cx; cameraRef.current.x = cx;
cameraRef.current.y = cy; cameraRef.current.y = cy;
if (rot || rot === 0) cameraRef.current.rotation = AngleHelper.normalizeDegAngle(rot);
if (zoom) cameraRef.current.zoom = boundZoom(zoom);
scheduleCameraUpdate(); scheduleCameraUpdate();
}, [scheduleCameraUpdate]); }, [scheduleCameraUpdate]);
@ -101,8 +107,8 @@ export default function ZoomPanWrapper({children}) {
scheduleCameraUpdate(); scheduleCameraUpdate();
}; };
// Subscribe to any external `center` and `alignView` calls // Subscribe to any external `setCamera` and `alignView` calls
useZoomPanSubscriber(centerOn, alignView); useZoomPanSubscriber(setCamera, alignView);
const onWheel = (e) => { const onWheel = (e) => {
const { left, top } = clientRectRef.current; const { left, top } = clientRectRef.current;
@ -110,7 +116,7 @@ export default function ZoomPanWrapper({children}) {
const { w, h } = viewBoxRef.current; const { w, h } = viewBoxRef.current;
const newZoomCalc = cameraRef.current.zoom * (1.0 - (e.deltaY * Constants.map.zoomSensitivity)); 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; cameraRef.current.zoom = newZoom;
// screen space cursor pos // screen space cursor pos

View File

@ -6,14 +6,14 @@ import { resetHoveredTracksStack } from "../../services/trackHoverInfoService";
export default function CustomFileSelect() { export default function CustomFileSelect() {
const {setScenery, setIsLoading} = useContext(MainContext); const {setScenery, setIsLoading} = useContext(MainContext);
const { center } = useZoomPanEmitter(); const { setCamera } = useZoomPanEmitter();
const handleFileChange = async (event) => { const handleFileChange = async (event) => {
const file = event.target.files[0]; const file = event.target.files[0];
if (!file) return; if (!file) return;
setIsLoading(true); setIsLoading(true);
const scenery = await SceneryFilesHelper.loadCustom(file, center); const scenery = await SceneryFilesHelper.loadCustom(file, setCamera);
setScenery(scenery); setScenery(scenery);
resetHoveredTracksStack(); resetHoveredTracksStack();
setIsLoading(false); setIsLoading(false);

View File

@ -9,13 +9,13 @@ import { resetHoveredTracksStack } from '../../services/trackHoverInfoService';
export default function SceneriesList(props) { export default function SceneriesList(props) {
const {setScenery, setIsLoading} = useContext(MainContext); const {setScenery, setIsLoading} = useContext(MainContext);
const [ sceneriesListData, setSceneriesListData ] = useState(null); const [ sceneriesListData, setSceneriesListData ] = useState(null);
const { center } = useZoomPanEmitter(); const { setCamera } = useZoomPanEmitter();
const handleSelectChange = async (event) => { const handleSelectChange = async (event) => {
const name = event.target.value; const name = event.target.value;
setIsLoading(true); setIsLoading(true);
const scenery = await SceneryFilesHelper.load(name, center); const scenery = await SceneryFilesHelper.load(name, setCamera);
setScenery(scenery); setScenery(scenery);
resetHoveredTracksStack(); resetHoveredTracksStack();
setIsLoading(false); setIsLoading(false);

View File

@ -4,6 +4,7 @@ const Constants = {
zoomSensitivity: 0.002, zoomSensitivity: 0.002,
zoomMin: 0.03, zoomMin: 0.03,
zoomMax: 200.0, zoomMax: 200.0,
zoomDefault: 5,
rotationSensitivity: 0.2, rotationSensitivity: 0.2,
forcePointerEvents: false, forcePointerEvents: false,
platformMaxTilt: 0.5 platformMaxTilt: 0.5

View File

@ -30,7 +30,7 @@ export default class SceneryFilesHelper {
return await this.fetch(url, false); return await this.fetch(url, false);
} }
static async load(name, centerFn = null) { static async load(name, setCameraFn = null) {
let file; let file;
try { try {
file = await this.getScenery(name); file = await this.getScenery(name);
@ -43,10 +43,10 @@ export default class SceneryFilesHelper {
return null; 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) => { return new Promise((resolve, reject) => {
SceneryParserLog.clear(); SceneryParserLog.clear();
const reader = new FileReader(); const reader = new FileReader();
@ -56,8 +56,13 @@ export default class SceneryFilesHelper {
let loadingError = null; let loadingError = null;
try { try {
scenery = SceneryParser.fromText(e.target?.result); scenery = SceneryParser.fromText(e.target?.result);
const mainSignalBoxPos = scenery.signalBoxes?.[0]?.pos || null; const mainSignalBox = scenery.signalBoxes?.[0];
if (mainSignalBoxPos) centerFn(...mainSignalBoxPos.toSVGCoords()); 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) { } catch (error) {
scenery = null; scenery = null;
loadingError = error; loadingError = error;

View File

@ -1,7 +1,7 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { Subject, BehaviorSubject } from 'rxjs'; import { Subject, BehaviorSubject } from 'rxjs';
const zoomCenter$ = new Subject(); const setCamera$ = new Subject();
const viewAlign$ = new Subject(); const viewAlign$ = new Subject();
// BehaviorSubjects to store current viewBox, clientRect and camera transform // BehaviorSubjects to store current viewBox, clientRect and camera transform
@ -21,13 +21,13 @@ export function getCurrentCamera() {
return camera$.getValue(); return camera$.getValue();
} }
export function useZoomPanSubscriber(onCenter, onAlign) { export function useZoomPanSubscriber(setCamera, onAlign) {
useEffect(() => { useEffect(() => {
const sub = zoomCenter$.subscribe(({ x, y }) => { const sub = setCamera$.subscribe(({ x, y, rot, zoom }) => {
onCenter(x, y); setCamera(x, y, rot, zoom);
}); });
return () => sub.unsubscribe(); return () => sub.unsubscribe();
}, [onCenter]); }, [setCamera]);
useEffect(() => { useEffect(() => {
const sub = viewAlign$.subscribe((angleDeg) => { const sub = viewAlign$.subscribe((angleDeg) => {
@ -39,8 +39,8 @@ export function useZoomPanSubscriber(onCenter, onAlign) {
export function useZoomPanEmitter() { export function useZoomPanEmitter() {
return { return {
center: (x, y) => { setCamera: (x, y, rot = null, zoom = null) => {
zoomCenter$.next({ x, y }); setCamera$.next({ x, y, rot, zoom });
}, },
alignView: (angleDeg) => { alignView: (angleDeg) => {
viewAlign$.next(angleDeg); viewAlign$.next(angleDeg);