Skip to content

Commit

Permalink
Split PanZoom mesh in Pan and Zoom meshes
Browse files Browse the repository at this point in the history
  • Loading branch information
loichuder committed Feb 7, 2022
1 parent 3e05274 commit a03d289
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 158 deletions.
80 changes: 80 additions & 0 deletions packages/lib/src/vis/shared/PanMesh.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { useThree } from '@react-three/fiber';
import type { ThreeEvent } from '@react-three/fiber';
import { useRef, useCallback } from 'react';
import type { Vector3 } from 'three';

import type { ModifierKey } from '../models';
import { noModifierKeyPressed } from '../utils';
import { useMoveCameraTo } from './hooks';

interface Props {
disabled?: boolean;
modifierKey?: ModifierKey;
}

function PanMesh(props: Props) {
const { disabled, modifierKey } = props;

const camera = useThree((state) => state.camera);
const { width, height } = useThree((state) => state.size);

const startOffsetPosition = useRef<Vector3>(); // `useRef` to avoid re-renders

const moveCameraTo = useMoveCameraTo();

const onPointerDown = useCallback(
(evt: ThreeEvent<PointerEvent>) => {
const { sourceEvent, unprojectedPoint } = evt;
const { target, pointerId } = sourceEvent;

if (disabled) {
return;
}

const isPanAllowed = modifierKey
? sourceEvent.getModifierState(modifierKey)
: noModifierKeyPressed(sourceEvent);
if (isPanAllowed) {
(target as Element).setPointerCapture(pointerId); // https://stackoverflow.com/q/28900077/758806
startOffsetPosition.current = unprojectedPoint.clone();
}
},
[disabled, modifierKey]
);

const onPointerUp = useCallback((evt: ThreeEvent<PointerEvent>) => {
const { sourceEvent } = evt;
const { target, pointerId } = sourceEvent;
(target as Element).releasePointerCapture(pointerId); // https://stackoverflow.com/q/28900077/758806

startOffsetPosition.current = undefined;
}, []);

const onPointerMove = useCallback(
(evt: ThreeEvent<PointerEvent>) => {
if (disabled || !startOffsetPosition.current) {
return;
}

// Prevent events from reaching tooltip mesh when panning
evt.stopPropagation();

const delta = startOffsetPosition.current
.clone()
.sub(evt.unprojectedPoint);
const target = camera.position.clone().add(delta);

moveCameraTo(target.x, target.y);
},
[camera, disabled, moveCameraTo]
);

return (
<mesh {...{ onPointerMove, onPointerUp, onPointerDown }}>
<meshBasicMaterial opacity={0} transparent />
<planeGeometry args={[width, height]} />
</mesh>
);
}

export default PanMesh;
165 changes: 7 additions & 158 deletions packages/lib/src/vis/shared/PanZoomMesh.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,6 @@
import { useThree } from '@react-three/fiber';
import type { ThreeEvent } from '@react-three/fiber';
import { clamp } from 'lodash';
import { useRef, useCallback, useEffect } from 'react';
import { Vector2, Vector3 } from 'three';

import { useWheelCapture } from '../hooks';
import type { ModifierKey } from '../models';
import { CAMERA_TOP_RIGHT, noModifierKeyPressed } from '../utils';
import { useAxisSystemContext } from './AxisSystemContext';

const ZOOM_FACTOR = 0.95;
const ONE_VECTOR = new Vector3(1, 1, 1);
import PanMesh from './PanMesh';
import ZoomMesh from './ZoomMesh';

interface Props {
pan?: boolean;
Expand All @@ -23,154 +13,13 @@ interface Props {
}

function PanZoomMesh(props: Props) {
const {
pan = true,
zoom = true,
xZoom = false,
yZoom = false,
panKey,
xZoomKey = 'Alt',
yZoomKey = 'Shift',
} = props;
const { abscissaScale, ordinateScale, visSize } = useAxisSystemContext();
const { width: visWidth, height: visHeight } = visSize;

const camera = useThree((state) => state.camera);
const { width, height } = useThree((state) => state.size);
const invalidate = useThree((state) => state.invalidate);

const startOffsetPosition = useRef<Vector3>(); // `useRef` to avoid re-renders
const viewportCenter = useRef<Vector2>();

const moveCameraTo = useCallback(
(x: number, y: number) => {
/* Save mesh coordinates at requested camera position so we can keep this point
in the centre of the viewport on resize. */
viewportCenter.current = new Vector2(
abscissaScale.invert(x),
ordinateScale.invert(y)
);
const { position } = camera;

// Unproject from normalized camera space (-1, -1) to (1, 1) to world space and subtract camera position to get bounds
const cameraLocalBounds = CAMERA_TOP_RIGHT.clone()
.unproject(camera)
.sub(position);

const xBound = Math.max(visWidth / 2 - cameraLocalBounds.x, 0);
const yBound = Math.max(visHeight / 2 - cameraLocalBounds.y, 0);

position.set(
clamp(x, -xBound, xBound),
clamp(y, -yBound, yBound),
position.z
);

camera.updateMatrixWorld();
invalidate();
},
[abscissaScale, ordinateScale, camera, visWidth, visHeight, invalidate]
);

const onPointerDown = useCallback(
(evt: ThreeEvent<PointerEvent>) => {
const { sourceEvent, unprojectedPoint } = evt;
const { target, pointerId } = sourceEvent;

if (!pan) {
return;
}

const isPanAllowed = panKey
? sourceEvent.getModifierState(panKey)
: noModifierKeyPressed(sourceEvent);
if (isPanAllowed) {
(target as Element).setPointerCapture(pointerId); // https://stackoverflow.com/q/28900077/758806
startOffsetPosition.current = unprojectedPoint.clone();
}
},
[pan, panKey]
);

const onPointerUp = useCallback((evt: ThreeEvent<PointerEvent>) => {
const { sourceEvent } = evt;
const { target, pointerId } = sourceEvent;
(target as Element).releasePointerCapture(pointerId); // https://stackoverflow.com/q/28900077/758806

startOffsetPosition.current = undefined;
}, []);

const onPointerMove = useCallback(
(evt: ThreeEvent<PointerEvent>) => {
if (!startOffsetPosition.current) {
return;
}

// Prevent events from reaching tooltip mesh when panning
evt.stopPropagation();

const delta = startOffsetPosition.current
.clone()
.sub(evt.unprojectedPoint);
const target = camera.position.clone().add(delta);

moveCameraTo(target.x, target.y);
},
[camera, moveCameraTo]
);

const onWheel = useCallback(
(evt: ThreeEvent<WheelEvent>) => {
const { sourceEvent, unprojectedPoint } = evt;

if (!zoom) {
return;
}

const factor = sourceEvent.deltaY > 0 ? ZOOM_FACTOR : 1 / ZOOM_FACTOR;

const noKeyPressed = noModifierKeyPressed(sourceEvent);
const zoomVector = new Vector3(
noKeyPressed || (xZoom && sourceEvent.getModifierState(xZoomKey))
? 1 / factor
: 1,
noKeyPressed || (yZoom && sourceEvent.getModifierState(yZoomKey))
? 1 / factor
: 1,
1
);
camera.scale.multiply(zoomVector).min(ONE_VECTOR);

camera.updateProjectionMatrix();
camera.updateMatrixWorld();

const oldPosition = unprojectedPoint.clone();
// Scale the change in position according to the zoom
const delta = camera.position
.clone()
.sub(oldPosition)
.multiply(zoomVector);
const scaledPosition = oldPosition.add(delta);
moveCameraTo(scaledPosition.x, scaledPosition.y);
},
[zoom, xZoom, xZoomKey, yZoom, yZoomKey, camera, moveCameraTo]
);

useEffect(() => {
if (viewportCenter.current) {
// On resize, move camera to the latest saved viewport center coordinates
const { x, y } = viewportCenter.current;
moveCameraTo(abscissaScale(x), ordinateScale(y));
}
}, [abscissaScale, viewportCenter, moveCameraTo, ordinateScale]);

useWheelCapture();
const { pan = true, panKey, zoom = true, ...zoomProps } = props;

return (
<mesh {...{ onPointerMove, onPointerUp, onPointerDown, onWheel }}>
<meshBasicMaterial opacity={0} transparent />
<planeGeometry args={[width, height]} />
</mesh>
<>
<PanMesh disabled={!pan} modifierKey={panKey} />
<ZoomMesh disabled={!zoom} {...zoomProps} />
</>
);
}

Expand Down
29 changes: 29 additions & 0 deletions packages/lib/src/vis/shared/ViewportCenterer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useFrame, useThree } from '@react-three/fiber';
import { useEffect, useRef } from 'react';
import type { Vector2 } from 'three';

import { useAxisSystemContext } from './AxisSystemContext';
import { useMoveCameraTo } from './hooks';

function ViewportCenterer() {
const { dataToWorld, worldToData } = useAxisSystemContext();
const viewportCenter = useRef<Vector2>();
const { position } = useThree((state) => state.camera);

const moveCameraTo = useMoveCameraTo();

useFrame(() => {
viewportCenter.current = worldToData(position);
});

useEffect(() => {
if (viewportCenter.current) {
// On resize, move camera to the latest saved viewport center coordinates
const { x, y } = dataToWorld(viewportCenter.current);
moveCameraTo(x, y);
}
}, [viewportCenter, moveCameraTo, dataToWorld, position.x, position.y]);
return null;
}

export default ViewportCenterer;
2 changes: 2 additions & 0 deletions packages/lib/src/vis/shared/VisCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { AxisConfig } from '../models';
import { getSizeToFit, getAxisOffsets } from '../utils';
import AxisSystem from './AxisSystem';
import AxisSystemProvider from './AxisSystemProvider';
import ViewportCenterer from './ViewportCenterer';
import styles from './VisCanvas.module.css';

interface Props {
Expand Down Expand Up @@ -70,6 +71,7 @@ function VisCanvas(props: PropsWithChildren<Props>) {
>
<AxisSystem axisOffsets={axisOffsets} title={title} />
{children}
<ViewportCenterer />
</AxisSystemProvider>
</Canvas>
</div>
Expand Down
54 changes: 54 additions & 0 deletions packages/lib/src/vis/shared/ZoomMesh.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useThree } from '@react-three/fiber';
import { Vector3 } from 'three';

import type { ModifierKey } from '../models';
import { noModifierKeyPressed } from '../utils';
import { useZoomOnWheel } from './hooks';

const ZOOM_FACTOR = 0.95;

interface Props {
disabled?: boolean;
xZoom?: boolean;
yZoom?: boolean;
xZoomKey?: ModifierKey;
yZoomKey?: ModifierKey;
}

function ZoomMesh(props: Props) {
const {
disabled,
xZoom = false,
yZoom = false,
xZoomKey = 'Alt',
yZoomKey = 'Shift',
} = props;

const { width, height } = useThree((state) => state.size);

const zoomVector = (sourceEvent: WheelEvent) => {
const factor = sourceEvent.deltaY > 0 ? ZOOM_FACTOR : 1 / ZOOM_FACTOR;

const noKeyPressed = noModifierKeyPressed(sourceEvent);
return new Vector3(
noKeyPressed || (xZoom && sourceEvent.getModifierState(xZoomKey))
? 1 / factor
: 1,
noKeyPressed || (yZoom && sourceEvent.getModifierState(yZoomKey))
? 1 / factor
: 1,
1
);
};

const onWheel = useZoomOnWheel(zoomVector, disabled);

return (
<mesh onWheel={onWheel}>
<meshBasicMaterial opacity={0} transparent />
<planeGeometry args={[width, height]} />
</mesh>
);
}

export default ZoomMesh;
Loading

0 comments on commit a03d289

Please sign in to comment.