From 2b94c0efaae53c5535c3d404a8af00a5daaceb63 Mon Sep 17 00:00:00 2001
From: dominik-korsa <29484605+dominik-korsa@users.noreply.github.com>
Date: Fri, 18 Jul 2025 00:23:44 +0200
Subject: [PATCH 1/3] Align the view to a straight track on double click
This feature will be useful when exporting the visualization to an SVG file
---
src/components/map/ZoomPanWrapper.js | 66 ++++++++++++-------
.../map/object-renderers/TrackRenderer.js | 22 +++++--
.../map/track-hover-info/TrackHoverInfo.css | 10 +++
.../map/track-hover-info/TrackHoverInfo.js | 2 +-
.../track-hover-info/TrackHoverInfoPopup.js | 7 +-
src/contexts/ZoomPanContext.js | 4 ++
src/helpers/angleHelper.js | 10 +--
7 files changed, 84 insertions(+), 37 deletions(-)
create mode 100644 src/contexts/ZoomPanContext.js
diff --git a/src/components/map/ZoomPanWrapper.js b/src/components/map/ZoomPanWrapper.js
index 4997ab8..b28b95b 100644
--- a/src/components/map/ZoomPanWrapper.js
+++ b/src/components/map/ZoomPanWrapper.js
@@ -2,7 +2,9 @@ import React, { useRef, useEffect, useCallback } from 'react';
import './ZoomPanWrapper.css';
import Constants from '../../helpers/constants';
import { useZoomPanSubscriber, viewBox$, clientRect$, camera$ } from '../../hooks/useZoomPubSub';
-import { mapRotation$ } from '../../services/mapRotationService';
+import {mapRotation$} from '../../services/mapRotationService';
+import ZoomPanContext from "../../contexts/ZoomPanContext";
+import AngleHelper from "../../helpers/angleHelper";
export default function ZoomPanWrapper({children}) {
const wrapperRef = useRef(null);
@@ -18,13 +20,13 @@ export default function ZoomPanWrapper({children}) {
const { x, y, w, h } = viewBoxRef.current;
return `${x} ${y} ${w} ${h}`;
};
-
+
const updateViewBox = useCallback((viewBox) => {
viewBoxRef.current = viewBox;
svgRef.current.setAttribute('viewBox', getViewBoxString());
viewBox$.next(viewBoxRef.current);
}, []);
-
+
const getCameraTransformString = () => {
const { x, y, zoom, rotation } = cameraRef.current;
@@ -42,7 +44,7 @@ export default function ZoomPanWrapper({children}) {
mapRotation$.next(cameraRef.current.rotation);
});
}, []);
-
+
const handleResize = useCallback(() => {
const rect = wrapperRef.current?.getBoundingClientRect();
if (!rect) return;
@@ -66,13 +68,13 @@ export default function ZoomPanWrapper({children}) {
const cy = e.movementY / cameraRef.current.zoom;
const cos = Math.cos(cameraRef.current.rotation * Math.PI / 180);
const sin = Math.sin(cameraRef.current.rotation * Math.PI / 180);
-
+
cameraRef.current.x -= cx * cos + cy * sin;
cameraRef.current.y -= cy * cos - cx * sin;
scheduleCameraUpdate();
};
-
+
// update viewbox when the window resizes
useEffect(() => {
window.addEventListener('resize', handleResize);
@@ -81,14 +83,14 @@ export default function ZoomPanWrapper({children}) {
window.removeEventListener('resize', handleResize);
};
}, [handleResize]);
-
+
const centerOn = useCallback((cx, cy) => {
cameraRef.current.x = cx;
cameraRef.current.y = cy;
scheduleCameraUpdate();
}, [scheduleCameraUpdate]);
-
+
// Subscribe to any external "center" calls
useZoomPanSubscriber(centerOn);
@@ -125,7 +127,7 @@ export default function ZoomPanWrapper({children}) {
scheduleCameraUpdate();
};
-
+
const onMouseDown = (e) => {
e.preventDefault();
isMouseDownRef.current = true;
@@ -146,22 +148,36 @@ export default function ZoomPanWrapper({children}) {
e.preventDefault();
isMouseDownRef.current = false;
};
-
+
+ // `event` could be used in the future to perform the rotation around the click position
+ const alignView = (angleDeg, _event) => {
+ // Find such relative rotation in the range [-45, 45] degrees that aligns the track
+ // with y rotation angleDeg to the X or Y screen axis.
+ const angleDifference = -angleDeg - cameraRef.current.rotation;
+ let deltaAngle = AngleHelper.normalizeDegAngle(angleDifference, 90);
+ if (deltaAngle > 45) deltaAngle -= 90;
+
+ cameraRef.current.rotation += deltaAngle;
+ scheduleCameraUpdate();
+ };
+
return (
-
-
-
+
+
+
+
+
);
}
diff --git a/src/components/map/object-renderers/TrackRenderer.js b/src/components/map/object-renderers/TrackRenderer.js
index b9fa747..6f9c9c8 100644
--- a/src/components/map/object-renderers/TrackRenderer.js
+++ b/src/components/map/object-renderers/TrackRenderer.js
@@ -4,23 +4,29 @@ import Constants from "../../../helpers/constants";
import { ElectrificationStatus } from "../../../model/electrification-status";
import MiscHelper from "../../../helpers/miscHelper";
import { setHoveredTrack, unsetHoveredTrack } from "../../../services/trackHoverInfoService";
+import ZoomPanContext from "../../../contexts/ZoomPanContext";
export default function TrackRenderer(props) {
const { object } = props;
const { trackColorMode } = useContext(SettingsContext);
+ const {alignView} = useContext(ZoomPanContext);
+
+ const onAlign = (event) => {
+ alignView(object.rot.y, event);
+ };
return (
+ onAlign={onAlign}/>
);
}
const MemoizedTrackRenderer = React.memo(StatelessTrackRenderer);
function StatelessTrackRenderer(props) {
- const { object, trackColorMode } = props;
+ const { object, trackColorMode , onAlign} = props;
if (object.points.start.distanceSq(object.points.end) < 0.001) {
return null;
@@ -34,7 +40,15 @@ function StatelessTrackRenderer(props) {
unsetHoveredTrack(null);
};
- const path = getTrackPath(object);
+ const onClick = (event) => {
+ if (object.type !== 'StandardTrack' || object.r !== 0) return;
+ if (event.detail === 2) {
+ event.preventDefault();
+ onAlign(event);
+ }
+ };
+
+ const path = getTrackPath(object);
const color = getTrackColor(object, trackColorMode);
const defs = getTrackDefs(object, trackColorMode);
@@ -52,7 +66,7 @@ function StatelessTrackRenderer(props) {
className="track"
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
- />
+ onClick={onClick}/>
);
}
diff --git a/src/components/map/track-hover-info/TrackHoverInfo.css b/src/components/map/track-hover-info/TrackHoverInfo.css
index 451660b..c02ce3f 100644
--- a/src/components/map/track-hover-info/TrackHoverInfo.css
+++ b/src/components/map/track-hover-info/TrackHoverInfo.css
@@ -12,8 +12,18 @@
padding: 1em;
}
+.track-hover-info-popup table {
+ border-spacing: 2px;
+}
+
.track-hover-info-popup th {
text-align: start;
padding-right: 2em;
color: #888;
}
+
+.track-hover-info-popup__align {
+ margin-top: 1.5em;
+ padding: 2px;
+ color: #888;
+}
diff --git a/src/components/map/track-hover-info/TrackHoverInfo.js b/src/components/map/track-hover-info/TrackHoverInfo.js
index 7ea2233..eb215f6 100644
--- a/src/components/map/track-hover-info/TrackHoverInfo.js
+++ b/src/components/map/track-hover-info/TrackHoverInfo.js
@@ -8,7 +8,7 @@ export default function TrackHoverInfo() {
const { showTrackHoverInfo } = useContext(SettingsContext);
if (!showTrackHoverInfo) return null;
-
+
return (
);
diff --git a/src/components/map/track-hover-info/TrackHoverInfoPopup.js b/src/components/map/track-hover-info/TrackHoverInfoPopup.js
index 8d17120..b73385b 100644
--- a/src/components/map/track-hover-info/TrackHoverInfoPopup.js
+++ b/src/components/map/track-hover-info/TrackHoverInfoPopup.js
@@ -4,7 +4,7 @@ export default function TrackHoverInfoPopup(props) {
const { track } = props;
const slopeArr = Array.from(new Set([
- Math.abs(track['start_slope']),
+ Math.abs(track['start_slope']),
Math.abs(track['end_slope'])
]));
const slopeOptions = {join: ' / ', subOptions: {suffix: ' ‰'}};
@@ -23,6 +23,9 @@ export default function TrackHoverInfoPopup(props) {
+ {track.type === 'StandardTrack' && track.r === 0 &&
+ Double click to align view
+
}
);
}
@@ -64,4 +67,4 @@ function InfoPopupItem({ label, value, options = {} }) {
{textValue} |
);
-}
\ No newline at end of file
+}
diff --git a/src/contexts/ZoomPanContext.js b/src/contexts/ZoomPanContext.js
new file mode 100644
index 0000000..98c8cfe
--- /dev/null
+++ b/src/contexts/ZoomPanContext.js
@@ -0,0 +1,4 @@
+import { createContext } from 'react';
+
+const ZoomPanContext = createContext();
+export default ZoomPanContext;
diff --git a/src/helpers/angleHelper.js b/src/helpers/angleHelper.js
index 2dbda25..8b53af8 100644
--- a/src/helpers/angleHelper.js
+++ b/src/helpers/angleHelper.js
@@ -8,11 +8,11 @@ function radToDeg(angle) {
return angle * (180 / Math.PI);
}
-function normalizeDegAngle(angle) {
+function normalizeDegAngle(angle, max = 360) {
if (angle < 0) {
- return angle + 360 * Math.ceil(Math.abs(angle) / 360);
- } else if (angle >= 360) {
- return angle - 360 * Math.floor(angle / 360);
+ return angle + max * Math.ceil(Math.abs(angle) / max);
+ } else if (angle >= max) {
+ return angle - max * Math.floor(angle / max);
} else {
return angle;
}
@@ -33,4 +33,4 @@ const AngleHelper = {
normalizeDegVector
}
-export default AngleHelper;
\ No newline at end of file
+export default AngleHelper;
From 2e1947a7d4136babc750c9a4717b5e94a743921e Mon Sep 17 00:00:00 2001
From: dominik-korsa <29484605+dominik-korsa@users.noreply.github.com>
Date: Wed, 23 Jul 2025 00:24:29 +0200
Subject: [PATCH 2/3] Reindent TrackRenderer.js to use 4 spaces
---
.../map/object-renderers/TrackRenderer.js | 218 +++++++++---------
1 file changed, 109 insertions(+), 109 deletions(-)
diff --git a/src/components/map/object-renderers/TrackRenderer.js b/src/components/map/object-renderers/TrackRenderer.js
index c5b99a7..9c67e4a 100644
--- a/src/components/map/object-renderers/TrackRenderer.js
+++ b/src/components/map/object-renderers/TrackRenderer.js
@@ -1,78 +1,78 @@
-import React, { useContext } from "react";
+import React, {useContext} from "react";
import SettingsContext from "../../../contexts/SettingsContext";
import Constants from "../../../helpers/constants";
-import { ElectrificationStatus } from "../../../model/electrification-status";
-import { setHoveredTrack, unsetHoveredTrack } from "../../../services/trackHoverInfoService";
+import {ElectrificationStatus} from "../../../model/electrification-status";
+import {setHoveredTrack, unsetHoveredTrack} from "../../../services/trackHoverInfoService";
import ZoomPanContext from "../../../contexts/ZoomPanContext";
import GradientsContext from "../../../contexts/GradientsContext";
import MiscHelper from "../../../helpers/miscHelper";
export default function TrackRenderer(props) {
- const { object } = props;
- const { trackColorMode } = useContext(SettingsContext);
- const { gradientDefs } = useContext(GradientsContext);
- const {alignView} = useContext(ZoomPanContext);
+ const {object} = props;
+ const {trackColorMode} = useContext(SettingsContext);
+ const {gradientDefs} = useContext(GradientsContext);
+ const {alignView} = useContext(ZoomPanContext);
- const onAlign = (event) => {
- alignView(object.rot.y, event);
- };
+ const onAlign = (event) => {
+ alignView(object.rot.y, event);
+ };
- return (
-
- );
+ return (
+
+ );
}
const MemoizedTrackRenderer = React.memo(StatelessTrackRenderer);
function StatelessTrackRenderer(props) {
- const { object, trackColorMode, gradientDef, onAlign } = props;
+ const {object, trackColorMode, gradientDef, onAlign} = props;
- if (object.points.start.distanceSq(object.points.end) < 0.001) {
- return null;
- }
-
- const onMouseEnter = () => {
- setHoveredTrack(object);
- };
-
- const onMouseLeave = () => {
- unsetHoveredTrack(null);
- };
-
- const onClick = (event) => {
- if (object.type !== 'StandardTrack' || object.r !== 0) return;
- if (event.detail === 2) {
- event.preventDefault();
- onAlign(event);
+ if (object.points.start.distanceSq(object.points.end) < 0.001) {
+ return null;
}
- };
- const path = getTrackPath(object);
- const color = getTrackColor(object, trackColorMode, gradientDef);
- const defs = getTrackDefs(object, trackColorMode, gradientDef);
+ const onMouseEnter = () => {
+ setHoveredTrack(object);
+ };
- return (
-
- {defs}
-
-
-
- );
+ const onMouseLeave = () => {
+ unsetHoveredTrack(null);
+ };
+
+ const onClick = (event) => {
+ if (object.type !== 'StandardTrack' || object.r !== 0) return;
+ if (event.detail === 2) {
+ event.preventDefault();
+ onAlign(event);
+ }
+ };
+
+ const path = getTrackPath(object);
+ const color = getTrackColor(object, trackColorMode, gradientDef);
+ const defs = getTrackDefs(object, trackColorMode, gradientDef);
+
+ return (
+
+ {defs}
+
+
+
+ );
}
function getTrackPath(object) {
@@ -113,57 +113,57 @@ function getGradientValues(object, trackColorMode) {
}
function getTrackColor(object, trackColorMode, gradientDef) {
- const modeDef = Constants.trackColorModes[trackColorMode];
+ const modeDef = Constants.trackColorModes[trackColorMode];
- switch (trackColorMode) {
- case "standard":
- default:
- if(object.prefab_name && object.prefab_name.includes('trans-mat'))
- return modeDef.options['invisible'][0];
-
- return modeDef.options[modeDef.optionDefault][0];
-
- case "electrification":
- switch(object.electrificationStatus) {
- case ElectrificationStatus.NOT_CHECKED:
- return modeDef.options['not-checked'][0];
- case ElectrificationStatus.NON_ELECTRIFIED:
- return modeDef.options['non-electrified'][0];
- case ElectrificationStatus.ELECTRIFIED:
- return modeDef.options['electrified'][0];
- case ElectrificationStatus.CONFLICT:
- return modeDef.options['conflict'][0];
+ switch (trackColorMode) {
+ case "standard":
default:
- return modeDef.options[modeDef.optionDefault][0];
- }
+ if (object.prefab_name && object.prefab_name.includes('trans-mat'))
+ return modeDef.options['invisible'][0];
- case "type":
- switch (object.type) {
- case "StandardTrack":
- return modeDef.options['standard-track'][0];
- case "PointTrack":
- return modeDef.options['point-track'][0];
- case "RouteTrack":
- return modeDef.options['route-track'][0];
- case "BezierTrack":
- return modeDef.options['bezier-track'][0];
- default:
- return modeDef.options[modeDef.optionDefault][0];
- }
+ return modeDef.options[modeDef.optionDefault][0];
- case "max-speed":
- if (!object.maxspeed) {
- if (object.type === 'RouteTrack') return modeDef.options['unknown'][0];
- return modeDef.options['derail'][0];
- }
- return MiscHelper.getTrackGradientColor(gradientDef, object.maxspeed);
+ case "electrification":
+ switch (object.electrificationStatus) {
+ case ElectrificationStatus.NOT_CHECKED:
+ return modeDef.options['not-checked'][0];
+ case ElectrificationStatus.NON_ELECTRIFIED:
+ return modeDef.options['non-electrified'][0];
+ case ElectrificationStatus.ELECTRIFIED:
+ return modeDef.options['electrified'][0];
+ case ElectrificationStatus.CONFLICT:
+ return modeDef.options['conflict'][0];
+ default:
+ return modeDef.options[modeDef.optionDefault][0];
+ }
- case "elevation":
- case "slope":
- const [startValue, endValue] = getGradientValues(object, trackColorMode);
- if (startValue === endValue) return MiscHelper.getTrackGradientColor(gradientDef, startValue);
- return `url(#track-${trackColorMode}-${object.id})`;
- }
+ case "type":
+ switch (object.type) {
+ case "StandardTrack":
+ return modeDef.options['standard-track'][0];
+ case "PointTrack":
+ return modeDef.options['point-track'][0];
+ case "RouteTrack":
+ return modeDef.options['route-track'][0];
+ case "BezierTrack":
+ return modeDef.options['bezier-track'][0];
+ default:
+ return modeDef.options[modeDef.optionDefault][0];
+ }
+
+ case "max-speed":
+ if (!object.maxspeed) {
+ if (object.type === 'RouteTrack') return modeDef.options['unknown'][0];
+ return modeDef.options['derail'][0];
+ }
+ return MiscHelper.getTrackGradientColor(gradientDef, object.maxspeed);
+
+ case "elevation":
+ case "slope":
+ const [startValue, endValue] = getGradientValues(object, trackColorMode);
+ if (startValue === endValue) return MiscHelper.getTrackGradientColor(gradientDef, startValue);
+ return `url(#track-${trackColorMode}-${object.id})`;
+ }
}
function getGradientDefs(object, trackColorMode, gradientDef, startValue, endValue) {
@@ -184,16 +184,16 @@ function getGradientDefs(object, trackColorMode, gradientDef, startValue, endVal
x2={x2}
y2={y2}
>
-
-
+
+
);
}
function getTrackDefs(object, trackColorMode, gradientDef) {
- const gradientVals = getGradientValues(object, trackColorMode);
- if (gradientVals === null) return null;
- const [startValue, endValue] = gradientVals;
- return getGradientDefs(object, trackColorMode, gradientDef, startValue, endValue);
+ const gradientVals = getGradientValues(object, trackColorMode);
+ if (gradientVals === null) return null;
+ const [startValue, endValue] = gradientVals;
+ return getGradientDefs(object, trackColorMode, gradientDef, startValue, endValue);
}
From de6d3aa5dc0ca81484cd21e54f9da516298deb1a Mon Sep 17 00:00:00 2001
From: dominik-korsa <29484605+dominik-korsa@users.noreply.github.com>
Date: Wed, 23 Jul 2025 00:44:13 +0200
Subject: [PATCH 3/3] Use ZoomPanEmitter/Subscriber instead of a context for
aligning the view
---
src/components/map/ZoomPanWrapper.js | 60 +++++++++----------
.../map/object-renderers/TrackRenderer.js | 8 +--
src/contexts/ZoomPanContext.js | 4 --
src/hooks/useZoomPubSub.js | 18 +++++-
4 files changed, 48 insertions(+), 42 deletions(-)
delete mode 100644 src/contexts/ZoomPanContext.js
diff --git a/src/components/map/ZoomPanWrapper.js b/src/components/map/ZoomPanWrapper.js
index b28b95b..2a5295e 100644
--- a/src/components/map/ZoomPanWrapper.js
+++ b/src/components/map/ZoomPanWrapper.js
@@ -3,7 +3,6 @@ import './ZoomPanWrapper.css';
import Constants from '../../helpers/constants';
import { useZoomPanSubscriber, viewBox$, clientRect$, camera$ } from '../../hooks/useZoomPubSub';
import {mapRotation$} from '../../services/mapRotationService';
-import ZoomPanContext from "../../contexts/ZoomPanContext";
import AngleHelper from "../../helpers/angleHelper";
export default function ZoomPanWrapper({children}) {
@@ -91,8 +90,19 @@ export default function ZoomPanWrapper({children}) {
scheduleCameraUpdate();
}, [scheduleCameraUpdate]);
- // Subscribe to any external "center" calls
- useZoomPanSubscriber(centerOn);
+ const alignView = (angleDeg) => {
+ // Find such relative rotation in the range [-45, 45] degrees that aligns the track
+ // with y rotation angleDeg to the X or Y screen axis.
+ const angleDifference = -angleDeg - cameraRef.current.rotation;
+ let deltaAngle = AngleHelper.normalizeDegAngle(angleDifference, 90);
+ if (deltaAngle > 45) deltaAngle -= 90;
+
+ cameraRef.current.rotation += deltaAngle;
+ scheduleCameraUpdate();
+ };
+
+ // Subscribe to any external `center` and `alignView` calls
+ useZoomPanSubscriber(centerOn, alignView);
const onWheel = (e) => {
const { left, top } = clientRectRef.current;
@@ -149,35 +159,21 @@ export default function ZoomPanWrapper({children}) {
isMouseDownRef.current = false;
};
- // `event` could be used in the future to perform the rotation around the click position
- const alignView = (angleDeg, _event) => {
- // Find such relative rotation in the range [-45, 45] degrees that aligns the track
- // with y rotation angleDeg to the X or Y screen axis.
- const angleDifference = -angleDeg - cameraRef.current.rotation;
- let deltaAngle = AngleHelper.normalizeDegAngle(angleDifference, 90);
- if (deltaAngle > 45) deltaAngle -= 90;
-
- cameraRef.current.rotation += deltaAngle;
- scheduleCameraUpdate();
- };
-
return (
-
-
-
-
-
+
+
+
);
}
diff --git a/src/components/map/object-renderers/TrackRenderer.js b/src/components/map/object-renderers/TrackRenderer.js
index 9c67e4a..6a7496b 100644
--- a/src/components/map/object-renderers/TrackRenderer.js
+++ b/src/components/map/object-renderers/TrackRenderer.js
@@ -3,18 +3,18 @@ import SettingsContext from "../../../contexts/SettingsContext";
import Constants from "../../../helpers/constants";
import {ElectrificationStatus} from "../../../model/electrification-status";
import {setHoveredTrack, unsetHoveredTrack} from "../../../services/trackHoverInfoService";
-import ZoomPanContext from "../../../contexts/ZoomPanContext";
import GradientsContext from "../../../contexts/GradientsContext";
import MiscHelper from "../../../helpers/miscHelper";
+import {useZoomPanEmitter} from "../../../hooks/useZoomPubSub";
export default function TrackRenderer(props) {
const {object} = props;
const {trackColorMode} = useContext(SettingsContext);
const {gradientDefs} = useContext(GradientsContext);
- const {alignView} = useContext(ZoomPanContext);
+ const { alignView } = useZoomPanEmitter();
- const onAlign = (event) => {
- alignView(object.rot.y, event);
+ const onAlign = () => {
+ alignView(object.rot.y);
};
return (
diff --git a/src/contexts/ZoomPanContext.js b/src/contexts/ZoomPanContext.js
deleted file mode 100644
index 98c8cfe..0000000
--- a/src/contexts/ZoomPanContext.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import { createContext } from 'react';
-
-const ZoomPanContext = createContext();
-export default ZoomPanContext;
diff --git a/src/hooks/useZoomPubSub.js b/src/hooks/useZoomPubSub.js
index fa500be..acaf8c3 100644
--- a/src/hooks/useZoomPubSub.js
+++ b/src/hooks/useZoomPubSub.js
@@ -2,6 +2,7 @@ import { useEffect } from 'react';
import { Subject, BehaviorSubject } from 'rxjs';
const zoomCenter$ = new Subject();
+const viewAlign$ = new Subject();
// BehaviorSubjects to store current viewBox, clientRect and camera transform
export const viewBox$ = new BehaviorSubject(null);
@@ -32,17 +33,27 @@ export function getCurrentCamera() {
/**
* useZoomPanSubscriber
*
-* Registers a callback (onCenter) that will be invoked whenever
+* Registers an `onCenter` callback that will be invoked whenever
* someone calls `center(x, y)` via the emitter. You should pass
* a function that takes (x, y) and recenters your viewBox accordingly.
+ *
+ * Similarly, registers an `onAlign` callback that will be invoked
+ * when someone calls `alignView(angleDeg)`.
*/
-export function useZoomPanSubscriber(onCenter) {
+export function useZoomPanSubscriber(onCenter, onAlign) {
useEffect(() => {
const sub = zoomCenter$.subscribe(({ x, y }) => {
onCenter(x, y);
});
return () => sub.unsubscribe();
}, [onCenter]);
+
+ useEffect(() => {
+ const sub = viewAlign$.subscribe((angleDeg) => {
+ onAlign(angleDeg);
+ });
+ return () => sub.unsubscribe();
+ }, [onAlign]);
}
/**
@@ -57,5 +68,8 @@ export function useZoomPanEmitter() {
center: (x, y) => {
zoomCenter$.next({ x, y });
},
+ alignView: (angleDeg) => {
+ viewAlign$.next(angleDeg);
+ },
};
}