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';
|
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 (
|
||||||
|
|||||||
@ -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]);
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user