Skip to content

Commit

Permalink
Implement useDrag hook
Browse files Browse the repository at this point in the history
  • Loading branch information
axelboc committed Aug 21, 2023
1 parent c209724 commit 41492f3
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 45 deletions.
18 changes: 12 additions & 6 deletions apps/storybook/src/SvgElement.stories.module.css
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
.rect {
.dragCircle {
fill: teal;
fill-opacity: 0.2;
stroke: teal;
fill-opacity: 0.5;
stroke: transparent;
stroke-width: 2;
pointer-events: auto;
cursor: grab;
}

.rect:hover {
fill: darkgoldenrod;
stroke: darkorange;
.dragCircle:hover,
.dragCircle[data-dragging] {
fill: blueviolet;
}

.dragCircle[data-dragging] {
stroke: darkmagenta;
cursor: grabbing;
}
114 changes: 77 additions & 37 deletions apps/storybook/src/SvgElement.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import {
DataToHtml,
Pan,
DefaultInteractions,
ResetZoomButton,
SvgCircle,
SvgElement,
SvgLine,
SvgRect,
useDrag,
VisCanvas,
Zoom,
} from '@h5web/lib';
import type { Meta, StoryObj } from '@storybook/react';
import { useState } from 'react';
import { Vector3 } from 'three';

import FillHeight from './decorators/FillHeight';
Expand All @@ -18,7 +19,19 @@ import styles from './SvgElement.stories.module.css';
const meta = {
title: 'Building Blocks/SvgElement',
component: SvgElement,
decorators: [FillHeight],
decorators: [
(Story) => (
<VisCanvas
abscissaConfig={{ visDomain: [0, 10], showGrid: true }}
ordinateConfig={{ visDomain: [0, 10], showGrid: true }}
>
<DefaultInteractions />
<ResetZoomButton />
<Story />
</VisCanvas>
),
FillHeight,
],
parameters: { layout: 'fullscreen' },
} satisfies Meta<typeof SvgElement>;

Expand All @@ -27,45 +40,72 @@ type Story = StoryObj<typeof meta>;

export const Default = {
render: (args) => (
<VisCanvas
abscissaConfig={{ visDomain: [0, 10], showGrid: true }}
ordinateConfig={{ visDomain: [0, 10], showGrid: true }}
<DataToHtml
points={[
new Vector3(2, 8),
new Vector3(4, 6),
new Vector3(3, 2),
new Vector3(6, 4),
new Vector3(6, 6),
new Vector3(7, 7),
]}
>
<Pan />
<Zoom />
<ResetZoomButton />
{(pt1, pt2, pt3, pt4, pt5, pt6) => (
<SvgElement {...args}>
<SvgRect
coords={[pt1, pt2]}
fill="teal"
fillOpacity={0.2}
stroke="teal"
strokeWidth={2}
/>
<SvgLine
coords={[pt3, pt4]}
stroke="darkblue"
strokeWidth={5}
strokeDasharray={15}
strokeLinecap="round"
/>
<SvgRect
coords={[pt5, pt6]}
fill="none"
stroke="teal"
strokeWidth={10}
strokePosition="outside"
/>
<SvgCircle coords={[pt5, pt6]} fill="none" stroke="lightseagreen" />
</SvgElement>
)}
</DataToHtml>
),
} satisfies Story;

<DataToHtml
points={[
new Vector3(2, 8),
new Vector3(4, 6),
new Vector3(3, 2),
new Vector3(6, 4),
new Vector3(6, 6),
new Vector3(7, 7),
]}
>
{(pt1, pt2, pt3, pt4, pt5, pt6) => (
export const Draggable = {
render: (args) => {
const [center, setCenter] = useState(() => new Vector3(2, 6));

const { delta, isDragging, startDrag } = useDrag({
onDragEnd: (d) => setCenter((c) => c.clone().add(d)),
});

return (
<DataToHtml points={[center.clone().add(delta)]}>
{(htmlCenter) => (
<SvgElement {...args}>
<SvgRect className={styles.rect} coords={[pt1, pt2]} />
<SvgLine
coords={[pt3, pt4]}
stroke="darkblue"
strokeWidth={5}
strokeDasharray={15}
strokeLinecap="round"
<circle
className={styles.dragCircle}
cx={htmlCenter.x}
cy={htmlCenter.y}
r={40}
data-dragging={isDragging || undefined}
onPointerDown={(evt) => {
evt.stopPropagation();
startDrag(evt.nativeEvent);
}}
/>
<SvgRect
coords={[pt5, pt6]}
fill="none"
stroke="teal"
strokeWidth={10}
strokePosition="outside"
/>
<SvgCircle coords={[pt5, pt6]} fill="none" stroke="lightseagreen" />
</SvgElement>
)}
</DataToHtml>
</VisCanvas>
),
);
},
} satisfies Story;
1 change: 1 addition & 0 deletions packages/lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export {
useCanvasEvents,
useInteraction,
useModifierKeyPressed,
useDrag,
} from './interactions/hooks';
export { default as Box } from './interactions/box';

Expand Down
72 changes: 70 additions & 2 deletions packages/lib/src/interactions/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEventListener, useToggle } from '@react-hookz/web';
import { useEventListener, useSyncedRef, useToggle } from '@react-hookz/web';
import { useThree } from '@react-three/fiber';
import { castArray } from 'lodash';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Vector3 } from 'three';

import { useVisCanvasContext } from '../vis/shared/VisCanvasProvider';
Expand Down Expand Up @@ -248,3 +248,71 @@ function useCanvasArea(): HTMLDivElement {
state.gl.domElement.parentElement?.parentElement as HTMLDivElement,
);
}

export function useDrag(opts: { onDragEnd: (delta: Vector3) => void }) {
const { onDragEnd } = opts;

const camera = useThree((state) => state.camera);
const { htmlToData } = useVisCanvasContext();

const htmlStartRef = useRef<Vector3>();
const onDragEndRef = useSyncedRef(onDragEnd);

const [delta, setDelta] = useState<Vector3>();

const startDrag = useCallback((evt: PointerEvent) => {
const { offsetX, offsetY, target, pointerId } = evt;

if (target instanceof Element) {
target.setPointerCapture(pointerId);
}

htmlStartRef.current = new Vector3(offsetX, offsetY);
setDelta(new Vector3());
}, []);

const handlePointerMove = useCallback(
(canvasEvt: CanvasEvent<PointerEvent>) => {
if (!htmlStartRef.current) {
return;
}

const dataStart = htmlToData(camera, htmlStartRef.current);
setDelta(canvasEvt.dataPt.sub(dataStart));
},
[camera, htmlToData],
);

const handlePointerUp = useCallback(
(canvasEvt: CanvasEvent<PointerEvent>) => {
if (!htmlStartRef.current) {
return;
}

const { dataPt, sourceEvent } = canvasEvt;
const { target, pointerId } = sourceEvent;

if (target instanceof Element) {
target.releasePointerCapture(pointerId);
}

const dataStart = htmlToData(camera, htmlStartRef.current);
htmlStartRef.current = undefined;
setDelta(undefined);

onDragEndRef.current?.(dataPt.sub(dataStart));
},
[camera, htmlToData, onDragEndRef],
);

useCanvasEvents({
onPointerMove: handlePointerMove,
onPointerUp: handlePointerUp,
});

return {
delta: delta || new Vector3(),
isDragging: !!delta,
startDrag,
};
}

0 comments on commit 41492f3

Please sign in to comment.