Tweak controls for mobile devices
- Updated scenery zoom pan wrapper, track hover info and distance meter to work correctly on touch screen / mobile devices - Fixed SideMenu still responding to tab navigation even if closed
This commit is contained in:
parent
a1a27a9ca0
commit
0bedd56cc8
@ -1,6 +1,8 @@
|
|||||||
.zoom-pan-wrapper {
|
.zoom-pan-wrapper {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
touch-action: none;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.zoom-pan-wrapper svg {
|
.zoom-pan-wrapper svg {
|
||||||
|
|||||||
@ -10,6 +10,13 @@ function boundZoom(zoom) {
|
|||||||
return Math.max(Constants.map.zoomMin, Math.min(Constants.map.zoomMax, zoom));
|
return Math.max(Constants.map.zoomMin, Math.min(Constants.map.zoomMax, zoom));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getDistance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
|
||||||
|
const getAngle = (a, b) => Math.atan2(b.y - a.y, b.x - a.x) * 180 / Math.PI;
|
||||||
|
const getMidpoint = (a, b) => ({
|
||||||
|
x: (a.x + b.x) / 2,
|
||||||
|
y: (a.y + b.y) / 2
|
||||||
|
});
|
||||||
|
|
||||||
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);
|
||||||
@ -18,7 +25,9 @@ export default function ZoomPanWrapper({children}) {
|
|||||||
const cameraRef = useRef({ x: 0, y: 0, zoom: 1, rotation: 0 });
|
const cameraRef = useRef({ x: 0, y: 0, zoom: 1, rotation: 0 });
|
||||||
const clientRectRef = useRef(null);
|
const clientRectRef = useRef(null);
|
||||||
const rafScheduledRef = useRef(false);
|
const rafScheduledRef = useRef(false);
|
||||||
const isMouseDownRef = useRef(false);
|
const pointersRef = useRef(new Map());
|
||||||
|
const lastPointerRef = useRef(null);
|
||||||
|
const twoFingerStartRef = useRef(null);
|
||||||
|
|
||||||
const getViewBoxString = () => {
|
const getViewBoxString = () => {
|
||||||
const { x, y, w, h } = viewBoxRef.current;
|
const { x, y, w, h } = viewBoxRef.current;
|
||||||
@ -59,28 +68,157 @@ export default function ZoomPanWrapper({children}) {
|
|||||||
clientRectRef.current = rect;
|
clientRectRef.current = rect;
|
||||||
}, [updateViewBox]);
|
}, [updateViewBox]);
|
||||||
|
|
||||||
const handleRotation = (e) => {
|
const applyScreenPan = useCallback((dxScreen, dyScreen) => {
|
||||||
const deltaAngle = e.movementX * Constants.map.rotationSensitivity;
|
const { zoom, rotation } = cameraRef.current;
|
||||||
if(deltaAngle === 0) return;
|
const dx = dxScreen / zoom;
|
||||||
|
const dy = dyScreen / zoom;
|
||||||
|
const rad = AngleHelper.degToRad(rotation);
|
||||||
|
const cos = Math.cos(rad);
|
||||||
|
const sin = Math.sin(rad);
|
||||||
|
|
||||||
cameraRef.current.rotation = cameraRef.current.rotation + deltaAngle
|
cameraRef.current.x -= dx * cos + dy * sin;
|
||||||
|
cameraRef.current.y -= dy * cos - dx * sin;
|
||||||
|
|
||||||
|
scheduleCameraUpdate();
|
||||||
|
}, [scheduleCameraUpdate]);
|
||||||
|
|
||||||
|
const getScreenPoint = (clientX, clientY) => {
|
||||||
|
const rect = clientRectRef.current;
|
||||||
|
return {
|
||||||
|
x: clientX - rect.left - rect.width / 2,
|
||||||
|
y: clientY - rect.top - rect.height / 2
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const screenPointToWorld = (point, camera) => {
|
||||||
|
const rad = AngleHelper.degToRad(camera.rotation);
|
||||||
|
const cos = Math.cos(rad);
|
||||||
|
const sin = Math.sin(rad);
|
||||||
|
const sx = point.x / camera.zoom;
|
||||||
|
const sy = point.y / camera.zoom;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: camera.x + sx * cos + sy * sin,
|
||||||
|
y: camera.y - sx * sin + sy * cos
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const startTwoFingerGesture = (pointers) => {
|
||||||
|
const [first, second] = pointers;
|
||||||
|
const distance = getDistance(first, second);
|
||||||
|
const angle = getAngle(first, second);
|
||||||
|
const midpoint = getMidpoint(first, second);
|
||||||
|
const screenPoint = getScreenPoint(midpoint.x, midpoint.y);
|
||||||
|
const worldPoint = screenPointToWorld(screenPoint, cameraRef.current);
|
||||||
|
|
||||||
|
twoFingerStartRef.current = {
|
||||||
|
distance,
|
||||||
|
angle,
|
||||||
|
zoom: cameraRef.current.zoom,
|
||||||
|
rotation: cameraRef.current.rotation,
|
||||||
|
worldPoint
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTwoFingerGesture = (pointers) => {
|
||||||
|
const start = twoFingerStartRef.current;
|
||||||
|
if (!start) return;
|
||||||
|
|
||||||
|
const [first, second] = pointers;
|
||||||
|
const distance = getDistance(first, second);
|
||||||
|
const angle = getAngle(first, second);
|
||||||
|
const midpoint = getMidpoint(first, second);
|
||||||
|
|
||||||
|
const newZoom = boundZoom(start.zoom * (distance / start.distance));
|
||||||
|
const deltaAngle = AngleHelper.normalizeAngleDelta(angle - start.angle);
|
||||||
|
const newRotation = AngleHelper.normalizeDegAngle(start.rotation + deltaAngle);
|
||||||
|
|
||||||
|
const screenPoint = getScreenPoint(midpoint.x, midpoint.y);
|
||||||
|
const rad = AngleHelper.degToRad(newRotation);
|
||||||
|
const cos = Math.cos(rad);
|
||||||
|
const sin = Math.sin(rad);
|
||||||
|
const sx = screenPoint.x / newZoom;
|
||||||
|
const sy = screenPoint.y / newZoom;
|
||||||
|
|
||||||
|
cameraRef.current.zoom = newZoom;
|
||||||
|
cameraRef.current.rotation = newRotation;
|
||||||
|
cameraRef.current.x = start.worldPoint.x - (sx * cos + sy * sin);
|
||||||
|
cameraRef.current.y = start.worldPoint.y - (-sx * sin + sy * cos);
|
||||||
|
|
||||||
scheduleCameraUpdate();
|
scheduleCameraUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMove = (e) => {
|
const onPointerDown = (e) => {
|
||||||
const cx = e.movementX / cameraRef.current.zoom;
|
pointersRef.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
||||||
const cy = e.movementY / cameraRef.current.zoom;
|
|
||||||
const cos = Math.cos(AngleHelper.degToRad(cameraRef.current.rotation));
|
|
||||||
const sin = Math.sin(AngleHelper.degToRad(cameraRef.current.rotation));
|
|
||||||
|
|
||||||
cameraRef.current.x -= cx * cos + cy * sin;
|
if (pointersRef.current.size === 1) {
|
||||||
cameraRef.current.y -= cy * cos - cx * sin;
|
lastPointerRef.current = { x: e.clientX, y: e.clientY };
|
||||||
|
twoFingerStartRef.current = null;
|
||||||
scheduleCameraUpdate();
|
} else if (pointersRef.current.size === 2) {
|
||||||
|
startTwoFingerGesture(Array.from(pointersRef.current.values()));
|
||||||
|
lastPointerRef.current = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// update viewbox when the window resizes
|
const onPointerMove = (e) => {
|
||||||
|
if (!pointersRef.current.has(e.pointerId)) return;
|
||||||
|
|
||||||
|
pointersRef.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
||||||
|
|
||||||
|
if (pointersRef.current.size === 1) {
|
||||||
|
const last = lastPointerRef.current;
|
||||||
|
if (!last) return;
|
||||||
|
|
||||||
|
const dx = e.clientX - last.x;
|
||||||
|
const dy = e.clientY - last.y;
|
||||||
|
lastPointerRef.current = { x: e.clientX, y: e.clientY };
|
||||||
|
|
||||||
|
if (e.pointerType === 'mouse' && e.altKey) {
|
||||||
|
const deltaAngle = dx * Constants.map.rotationSensitivity;
|
||||||
|
if (deltaAngle !== 0) {
|
||||||
|
cameraRef.current.rotation += deltaAngle;
|
||||||
|
scheduleCameraUpdate();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
applyScreenPan(dx, dy);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pointersRef.current.size >= 2) {
|
||||||
|
const firstTwo = Array.from(pointersRef.current.values()).slice(0, 2);
|
||||||
|
if (!twoFingerStartRef.current) {
|
||||||
|
startTwoFingerGesture(firstTwo);
|
||||||
|
} else {
|
||||||
|
updateTwoFingerGesture(firstTwo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerUp = (e) => {
|
||||||
|
pointersRef.current.delete(e.pointerId);
|
||||||
|
|
||||||
|
if (pointersRef.current.size === 0) {
|
||||||
|
lastPointerRef.current = null;
|
||||||
|
twoFingerStartRef.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pointersRef.current.size === 1) {
|
||||||
|
const remaining = Array.from(pointersRef.current.values())[0];
|
||||||
|
lastPointerRef.current = { x: remaining.x, y: remaining.y };
|
||||||
|
twoFingerStartRef.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pointersRef.current.size >= 2) {
|
||||||
|
startTwoFingerGesture(Array.from(pointersRef.current.values()).slice(0, 2));
|
||||||
|
lastPointerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerCancel = onPointerUp;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener('resize', handleResize);
|
window.addEventListener('resize', handleResize);
|
||||||
handleResize(); // Initial call to set the viewBox
|
handleResize(); // Initial call to set the viewBox
|
||||||
@ -113,6 +251,8 @@ export default function ZoomPanWrapper({children}) {
|
|||||||
useZoomPanSubscriber(setCamera, alignView);
|
useZoomPanSubscriber(setCamera, alignView);
|
||||||
|
|
||||||
const onWheel = (e) => {
|
const onWheel = (e) => {
|
||||||
|
if (!clientRectRef.current) return;
|
||||||
|
|
||||||
const { left, top } = clientRectRef.current;
|
const { left, top } = clientRectRef.current;
|
||||||
const { x, y, zoom } = cameraRef.current;
|
const { x, y, zoom } = cameraRef.current;
|
||||||
const { w, h } = viewBoxRef.current;
|
const { w, h } = viewBoxRef.current;
|
||||||
@ -146,37 +286,16 @@ export default function ZoomPanWrapper({children}) {
|
|||||||
scheduleCameraUpdate();
|
scheduleCameraUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMouseDown = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
isMouseDownRef.current = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMouseMove = (e) => {
|
|
||||||
if (!isMouseDownRef.current) return;
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if(e.altKey) {
|
|
||||||
handleRotation(e);
|
|
||||||
} else {
|
|
||||||
handleMove(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMouseUp = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
isMouseDownRef.current = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="zoom-pan-wrapper" ref={wrapperRef}>
|
<div className="zoom-pan-wrapper" ref={wrapperRef}>
|
||||||
<svg
|
<svg
|
||||||
ref={svgRef}
|
ref={svgRef}
|
||||||
viewBox={getViewBoxString()}
|
viewBox={getViewBoxString()}
|
||||||
onWheel={onWheel}
|
onWheel={onWheel}
|
||||||
onMouseDown={onMouseDown}
|
onPointerDown={onPointerDown}
|
||||||
onMouseMove={onMouseMove}
|
onPointerMove={onPointerMove}
|
||||||
onMouseUp={onMouseUp}
|
onPointerUp={onPointerUp}
|
||||||
onMouseLeave={onMouseUp}
|
onPointerCancel={onPointerCancel}
|
||||||
>
|
>
|
||||||
<g ref={cameraWrapperRef} transform={getCameraTransformString()}>
|
<g ref={cameraWrapperRef} transform={getCameraTransformString()}>
|
||||||
{children}
|
{children}
|
||||||
@ -184,4 +303,4 @@ export default function ZoomPanWrapper({children}) {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { useContext, useEffect } from "react";
|
import { useContext, useEffect, useRef, useCallback } from "react";
|
||||||
|
import Constants from '../../../helpers/constants';
|
||||||
import SceneryContext from "../../../contexts/SceneryContext";
|
import SceneryContext from "../../../contexts/SceneryContext";
|
||||||
import DistanceMeterContext from "../../../contexts/DistanceMeterContext";
|
import DistanceMeterContext from "../../../contexts/DistanceMeterContext";
|
||||||
import { getSVGCoords } from "./get-svg-coords";
|
import { getSVGCoords } from "./get-svg-coords";
|
||||||
@ -7,8 +8,24 @@ import DistanceMeterView from "./DistanceMeterView";
|
|||||||
export default function DistanceMeter() {
|
export default function DistanceMeter() {
|
||||||
const { distancePoints, setDistancePoints } = useContext(DistanceMeterContext);
|
const { distancePoints, setDistancePoints } = useContext(DistanceMeterContext);
|
||||||
const { scenery } = useContext(SceneryContext);
|
const { scenery } = useContext(SceneryContext);
|
||||||
|
const pointersRef = useRef(new Set());
|
||||||
|
const mainPointerIdRef = useRef(null);
|
||||||
|
const pointerDownPosRef = useRef(null);
|
||||||
|
|
||||||
|
const onPointerUp = useCallback((event) => {
|
||||||
|
const dx = pointerDownPosRef.current?.x - event.clientX;
|
||||||
|
const dy = pointerDownPosRef.current?.y - event.clientY;
|
||||||
|
|
||||||
const onClick = (event) => {
|
pointersRef.current.delete(event.pointerId);
|
||||||
|
if (mainPointerIdRef.current !== event.pointerId) return;
|
||||||
|
mainPointerIdRef.current = null;
|
||||||
|
pointerDownPosRef.current = null;
|
||||||
|
|
||||||
|
if (pointersRef.current.size > 0) return;
|
||||||
|
|
||||||
|
const dist = Math.sqrt(dx*dx + dy*dy);
|
||||||
|
if (dist > Constants.map.distanceMeterMaxMovePx) return;
|
||||||
|
|
||||||
setDistancePoints(oldDistancePoints => {
|
setDistancePoints(oldDistancePoints => {
|
||||||
if(!Array.isArray(oldDistancePoints)) return oldDistancePoints;
|
if(!Array.isArray(oldDistancePoints)) return oldDistancePoints;
|
||||||
const pos = getSVGCoords(event);
|
const pos = getSVGCoords(event);
|
||||||
@ -16,7 +33,26 @@ export default function DistanceMeter() {
|
|||||||
|
|
||||||
return [...oldDistancePoints, pos];
|
return [...oldDistancePoints, pos];
|
||||||
});
|
});
|
||||||
}
|
}, [setDistancePoints]);
|
||||||
|
|
||||||
|
const onPointerDown = useCallback((event) => {
|
||||||
|
pointersRef.current.add(event.pointerId);
|
||||||
|
if(mainPointerIdRef.current !== null) return;
|
||||||
|
|
||||||
|
mainPointerIdRef.current = event.pointerId;
|
||||||
|
pointerDownPosRef.current = {
|
||||||
|
x: event.clientX,
|
||||||
|
y: event.clientY
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onPointerLeave = useCallback((event) => {
|
||||||
|
pointersRef.current.delete(event.pointerId);
|
||||||
|
if (mainPointerIdRef.current !== event.pointerId)
|
||||||
|
|
||||||
|
mainPointerIdRef.current = null;
|
||||||
|
pointerDownPosRef.current = null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDistancePoints([]);
|
setDistancePoints([]);
|
||||||
@ -32,7 +68,10 @@ export default function DistanceMeter() {
|
|||||||
y={-scenery.bounds.maxZ}
|
y={-scenery.bounds.maxZ}
|
||||||
width={scenery.bounds.maxX - scenery.bounds.minX}
|
width={scenery.bounds.maxX - scenery.bounds.minX}
|
||||||
height={scenery.bounds.maxZ - scenery.bounds.minZ}
|
height={scenery.bounds.maxZ - scenery.bounds.minZ}
|
||||||
onClick={onClick}
|
onPointerDown={onPointerDown}
|
||||||
|
onPointerUp={onPointerUp}
|
||||||
|
onPointerCancel={onPointerLeave}
|
||||||
|
onPointerLeave={onPointerLeave}
|
||||||
no-export="true"
|
no-export="true"
|
||||||
/>
|
/>
|
||||||
<DistanceMeterView distancePoints={distancePoints} setDistancePoints={setDistancePoints} />
|
<DistanceMeterView distancePoints={distancePoints} setDistancePoints={setDistancePoints} />
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { useRef } from 'react';
|
import { useRef, useCallback } from 'react';
|
||||||
import { getSVGCoords } from './get-svg-coords';
|
import { getSVGCoords } from './get-svg-coords';
|
||||||
|
|
||||||
export default function DistanceMeterCircle(props) {
|
export default function DistanceMeterCircle(props) {
|
||||||
const { point, index, setDistancePoints } = props;
|
const { point, index, setDistancePoints } = props;
|
||||||
const isDragged = useRef(false);
|
const isDraggingRef = useRef(false);
|
||||||
|
|
||||||
const updatePointPosition = (event) => {
|
const updatePointPosition = useCallback((event) => {
|
||||||
const pos = getSVGCoords(event);
|
const pos = getSVGCoords(event);
|
||||||
if (!pos) return;
|
if (!pos) return;
|
||||||
|
|
||||||
@ -15,57 +15,75 @@ export default function DistanceMeterCircle(props) {
|
|||||||
newPoints[index] = pos;
|
newPoints[index] = pos;
|
||||||
return newPoints;
|
return newPoints;
|
||||||
});
|
});
|
||||||
}
|
}, [setDistancePoints, index]);
|
||||||
|
|
||||||
const removePoint = () => {
|
const removePoint = useCallback(() => {
|
||||||
setDistancePoints(oldPoints => {
|
setDistancePoints(oldPoints => {
|
||||||
if (!Array.isArray(oldPoints)) return oldPoints;
|
if (!Array.isArray(oldPoints)) return oldPoints;
|
||||||
const newPoints = [...oldPoints];
|
const newPoints = [...oldPoints];
|
||||||
newPoints.splice(index, 1);
|
newPoints.splice(index, 1);
|
||||||
return newPoints;
|
return newPoints;
|
||||||
});
|
});
|
||||||
}
|
}, [setDistancePoints, index]);
|
||||||
|
|
||||||
const onMouseDown = (event) => {
|
const onPointerDown = useCallback((event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (isDragged.current) return;
|
event.currentTarget.setPointerCapture(event.pointerId);
|
||||||
isDragged.current = true;
|
|
||||||
|
|
||||||
document.addEventListener('mousemove', onMouseMove);
|
isDraggingRef.current = true;
|
||||||
document.addEventListener('mouseup', onMouseUp, { once: true });
|
}, []);
|
||||||
}
|
|
||||||
|
|
||||||
const onMouseUp = (event) => {
|
const onPointerUp = useCallback((event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (!isDragged.current) return;
|
if (!isDraggingRef.current) return;
|
||||||
isDragged.current = false;
|
|
||||||
|
|
||||||
document.removeEventListener('mousemove', onMouseMove);
|
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
||||||
|
event.currentTarget.releasePointerCapture(event.pointerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
isDraggingRef.current = false;
|
||||||
updatePointPosition(event);
|
updatePointPosition(event);
|
||||||
}
|
}, [updatePointPosition]);
|
||||||
|
|
||||||
const onMouseMove = (event) => {
|
const onPointerMove = useCallback((event) => {
|
||||||
if (!isDragged.current) return;
|
if (!isDraggingRef.current) return;
|
||||||
|
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
updatePointPosition(event);
|
updatePointPosition(event);
|
||||||
}
|
}, [updatePointPosition]);
|
||||||
|
|
||||||
const onContextMenu = (event) => {
|
const onPointerCancel = useCallback((event) => {
|
||||||
|
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
||||||
|
event.currentTarget.releasePointerCapture(event.pointerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
isDraggingRef.current = false;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onContextMenu = useCallback((event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
||||||
|
event.currentTarget.releasePointerCapture(event.pointerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
isDraggingRef.current = false;
|
||||||
removePoint();
|
removePoint();
|
||||||
}
|
}, [removePoint]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g
|
<g
|
||||||
className="distance-meter-circle"
|
className="distance-meter-circle"
|
||||||
onMouseDown={onMouseDown}
|
onPointerDown={onPointerDown}
|
||||||
|
onPointerUp={onPointerUp}
|
||||||
|
onPointerMove={onPointerMove}
|
||||||
|
onPointerCancel={onPointerCancel}
|
||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
>
|
>
|
||||||
<circle
|
<circle
|
||||||
|
|||||||
@ -13,7 +13,7 @@ export default function DistanceMeterLine(props) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const onMouseDown = (event) => {
|
const onPointerDown = (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const pos = getSVGCoords(event);
|
const pos = getSVGCoords(event);
|
||||||
@ -33,7 +33,7 @@ export default function DistanceMeterLine(props) {
|
|||||||
y2={end[1]}
|
y2={end[1]}
|
||||||
stroke="transparent"
|
stroke="transparent"
|
||||||
strokeWidth={3}
|
strokeWidth={3}
|
||||||
onMouseDown={onMouseDown}
|
onPointerDown={onPointerDown}
|
||||||
/>
|
/>
|
||||||
<line
|
<line
|
||||||
x1={start[0]}
|
x1={start[0]}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import SettingsContext from "../../../contexts/SettingsContext";
|
|||||||
import Constants from "../../../helpers/constants";
|
import Constants from "../../../helpers/constants";
|
||||||
import {ElectrificationStatus} from "../../../model/electrification-status";
|
import {ElectrificationStatus} from "../../../model/electrification-status";
|
||||||
import MiscHelper from "../../../helpers/miscHelper";
|
import MiscHelper from "../../../helpers/miscHelper";
|
||||||
import {setHoveredTrack, unsetHoveredTrack} from "../../../services/trackHoverInfoService";
|
import {setHoveredTrack, setTouchClickedTrack, unsetHoveredTrack} from "../../../services/trackHoverInfoService";
|
||||||
import GradientsContext from "../../../contexts/GradientsContext";
|
import GradientsContext from "../../../contexts/GradientsContext";
|
||||||
import { TrackShape, TrackSource } from "../../../model/tracks/track";
|
import { TrackShape, TrackSource } from "../../../model/tracks/track";
|
||||||
import {useZoomPanEmitter} from "../../../hooks/useZoomPubSub";
|
import {useZoomPanEmitter} from "../../../hooks/useZoomPubSub";
|
||||||
@ -36,16 +36,20 @@ function StatelessTrackRenderer(props) {
|
|||||||
const unscaledPathRef = React.useRef(null);
|
const unscaledPathRef = React.useRef(null);
|
||||||
const scaledPathRef = React.useRef(null);
|
const scaledPathRef = React.useRef(null);
|
||||||
|
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
setTouchClickedTrack(object);
|
||||||
|
}, [object]);
|
||||||
|
|
||||||
const onMouseEnter = useCallback(() => {
|
const onMouseEnter = useCallback(() => {
|
||||||
setHoveredTrack(object);
|
setHoveredTrack(object);
|
||||||
}, [object]);
|
}, [object]);
|
||||||
|
|
||||||
const onMouseLeave = useCallback(() => {
|
const onMouseLeave = useCallback(() => {
|
||||||
unsetHoveredTrack(null);
|
unsetHoveredTrack(object);
|
||||||
}, []);
|
}, [object]);
|
||||||
|
|
||||||
const onClick = useCallback((event) => {
|
const onContextMenu = useCallback((event) => {
|
||||||
if (object.shape === TrackShape.STRAIGHT && event.detail === 2) {
|
if (object.shape === TrackShape.STRAIGHT) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
onAlign(event);
|
onAlign(event);
|
||||||
}
|
}
|
||||||
@ -96,6 +100,7 @@ function StatelessTrackRenderer(props) {
|
|||||||
className="track"
|
className="track"
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
|
onContextMenu={onContextMenu}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
export-force-visible="true"
|
export-force-visible="true"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -9,7 +9,7 @@ export default function TrackHoverInfoPopup({track}) {
|
|||||||
<TrackHoverInfoPopupTable track={track} />
|
<TrackHoverInfoPopupTable track={track} />
|
||||||
{showAlignHint && (
|
{showAlignHint && (
|
||||||
<div className="track-hover-info-popup__align">
|
<div className="track-hover-info-popup__align">
|
||||||
Double click to align the view
|
Right click to align the view
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -15,14 +15,14 @@ export default function SideMenu() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='side-menu-wrapper'>
|
<div className='side-menu-wrapper'>
|
||||||
<div className={sideMenuClass}>
|
<aside className={sideMenuClass} inert={!sideMenuOpen}>
|
||||||
<SceneryInfoButton />
|
<SceneryInfoButton />
|
||||||
<DistanceMeterButton />
|
<DistanceMeterButton />
|
||||||
<ExportViewButton />
|
<ExportViewButton />
|
||||||
<LayersMenu />
|
<LayersMenu />
|
||||||
<LayerOptionsMenu />
|
<LayerOptionsMenu />
|
||||||
<InfoFooter />
|
<InfoFooter />
|
||||||
</div>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -42,13 +42,20 @@ function normalizeDegVector(vector) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeAngleDelta(delta) {
|
||||||
|
if (delta > 180) delta -= 360;
|
||||||
|
if (delta < -180) delta += 360;
|
||||||
|
return delta;
|
||||||
|
}
|
||||||
|
|
||||||
const AngleHelper = {
|
const AngleHelper = {
|
||||||
degToRad,
|
degToRad,
|
||||||
radToDeg,
|
radToDeg,
|
||||||
vectorDegToRad,
|
vectorDegToRad,
|
||||||
vectorRadToDeg,
|
vectorRadToDeg,
|
||||||
normalizeDegAngle,
|
normalizeDegAngle,
|
||||||
normalizeDegVector
|
normalizeDegVector,
|
||||||
|
normalizeAngleDelta
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AngleHelper;
|
export default AngleHelper;
|
||||||
|
|||||||
@ -14,7 +14,8 @@ const Constants = {
|
|||||||
platformMaxTilt: 0.5,
|
platformMaxTilt: 0.5,
|
||||||
showDebugTrackIds: false,
|
showDebugTrackIds: false,
|
||||||
mapZoomTrackDetailsThreshold: 1 / 1.44,
|
mapZoomTrackDetailsThreshold: 1 / 1.44,
|
||||||
mapZoomObjectDetailsThreshold: 0.5
|
mapZoomObjectDetailsThreshold: 0.5,
|
||||||
|
distanceMeterMaxMovePx: 10
|
||||||
},
|
},
|
||||||
parser: {
|
parser: {
|
||||||
logSceneryAfterFinished: readEnvBool('VITE_DO_LOG_SCENERY') ?? false,
|
logSceneryAfterFinished: readEnvBool('VITE_DO_LOG_SCENERY') ?? false,
|
||||||
|
|||||||
@ -2,6 +2,11 @@ import { BehaviorSubject } from 'rxjs';
|
|||||||
|
|
||||||
const hoveredTracksStack = [];
|
const hoveredTracksStack = [];
|
||||||
export const hoveredTrack$ = new BehaviorSubject(null);
|
export const hoveredTrack$ = new BehaviorSubject(null);
|
||||||
|
export const touchClickedTrack$ = new BehaviorSubject(null);
|
||||||
|
|
||||||
|
export function setTouchClickedTrack(track) {
|
||||||
|
touchClickedTrack$.next(track);
|
||||||
|
}
|
||||||
|
|
||||||
export function setHoveredTrack(track) {
|
export function setHoveredTrack(track) {
|
||||||
hoveredTracksStack.push(track);
|
hoveredTracksStack.push(track);
|
||||||
@ -16,4 +21,5 @@ export function unsetHoveredTrack(track) {
|
|||||||
export function resetHoveredTracksStack() {
|
export function resetHoveredTracksStack() {
|
||||||
hoveredTracksStack.length = 0;
|
hoveredTracksStack.length = 0;
|
||||||
hoveredTrack$.next(null);
|
hoveredTrack$.next(null);
|
||||||
|
touchClickedTrack$.next(null);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user