-
Notifications
You must be signed in to change notification settings - Fork 91
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
d65f39f
commit 60556ed
Showing
6 changed files
with
177 additions
and
150 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { MovablePointDisplay } from "./MovablePointDisplay" | ||
|
||
describe("MovablePointDisplay", () => { | ||
it("has a human-readable displayName", () => { | ||
expect(MovablePointDisplay.displayName).toBe("MovablePointDisplay") | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import React from "react" | ||
import { vec } from "../vec" | ||
import { useTransformContext } from "../context/TransformContext" | ||
import { Theme } from "./Theme" | ||
|
||
export interface MovablePointDisplayProps { | ||
color?: string | ||
ringRadiusPx?: number | ||
dragging: boolean | ||
point: vec.Vector2 | ||
} | ||
|
||
export const MovablePointDisplay = React.forwardRef<SVGGElement, MovablePointDisplayProps>( | ||
(props: MovablePointDisplayProps, ref) => { | ||
const { color = Theme.pink, ringRadiusPx = 15, dragging, point } = props | ||
|
||
const { viewTransform, userTransform } = useTransformContext() | ||
|
||
const combinedTransform = React.useMemo( | ||
() => vec.matrixMult(viewTransform, userTransform), | ||
[viewTransform, userTransform], | ||
) | ||
|
||
const [xPx, yPx] = vec.transform(point, combinedTransform) | ||
|
||
return ( | ||
<g | ||
ref={ref} | ||
style={ | ||
{ | ||
"--movable-point-color": color, | ||
"--movable-point-ring-size": `${ringRadiusPx}px`, | ||
} as React.CSSProperties | ||
} | ||
className={`mafs-movable-point ${dragging ? "mafs-movable-point-dragging" : ""}`} | ||
tabIndex={0} | ||
> | ||
<circle className="mafs-movable-point-hitbox" r={30} cx={xPx} cy={yPx}></circle> | ||
<circle | ||
className="mafs-movable-point-focus" | ||
r={ringRadiusPx + 1} | ||
cx={xPx} | ||
cy={yPx} | ||
></circle> | ||
<circle className="mafs-movable-point-ring" r={ringRadiusPx} cx={xPx} cy={yPx}></circle> | ||
<circle className="mafs-movable-point-point" r={6} cx={xPx} cy={yPx}></circle> | ||
</g> | ||
) | ||
}, | ||
) | ||
|
||
MovablePointDisplay.displayName = "MovablePointDisplay" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,7 @@ | ||
import { MovablePoint, MovablePointSVG } from "./MovablePoint" | ||
import { MovablePoint } from "./MovablePoint" | ||
|
||
describe("MovablePoint", () => { | ||
it("has a human-readable displayName", () => { | ||
expect(MovablePoint.displayName).toBe("MovablePoint"); | ||
}) | ||
}) | ||
|
||
describe("MovablePointSVG", () => { | ||
it("has a human-readable displayName", () => { | ||
expect(MovablePointSVG.displayName).toBe("MovablePointSVG"); | ||
}) | ||
it("has a human-readable displayName", () => { | ||
expect(MovablePoint.displayName).toBe("MovablePoint") | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import * as React from "react" | ||
import { useDrag } from "@use-gesture/react" | ||
import invariant from "tiny-invariant" | ||
import { vec } from "../vec" | ||
import { useSpanContext } from "../context/SpanContext" | ||
import { useTransformContext } from "../context/TransformContext" | ||
import { range } from "../math" | ||
|
||
export interface UseMovementInteractionArguments { | ||
gestureTarget: React.RefObject<Element> | ||
onMove: (point: vec.Vector2) => unknown | ||
point: vec.Vector2 | ||
constrain: (point: vec.Vector2) => vec.Vector2 | ||
} | ||
|
||
export interface UseMovementInteraction { | ||
dragging: boolean | ||
} | ||
|
||
export function useMovementInteraction( | ||
args: UseMovementInteractionArguments, | ||
): UseMovementInteraction { | ||
const { gestureTarget: target, onMove, point, constrain } = args | ||
const [dragging, setDragging] = React.useState(false) | ||
const { xSpan, ySpan } = useSpanContext() | ||
const { viewTransform, userTransform } = useTransformContext() | ||
|
||
const inverseViewTransform = vec.matrixInvert(viewTransform) | ||
invariant(inverseViewTransform, "The view transform must be invertible.") | ||
|
||
const inverseTransform = React.useMemo(() => getInverseTransform(userTransform), [userTransform]) | ||
|
||
const pickup = React.useRef<vec.Vector2>([0, 0]) | ||
|
||
useDrag( | ||
(state) => { | ||
const { type, event } = state | ||
event?.stopPropagation() | ||
|
||
const isKeyboard = type.includes("key") | ||
if (isKeyboard) { | ||
event?.preventDefault() | ||
const { direction: yDownDirection, altKey, metaKey, shiftKey } = state | ||
|
||
const direction = [yDownDirection[0], -yDownDirection[1]] as vec.Vector2 | ||
const span = Math.abs(direction[0]) ? xSpan : ySpan | ||
|
||
let divisions = 50 | ||
if (altKey || metaKey) divisions = 200 | ||
if (shiftKey) divisions = 10 | ||
|
||
const min = span / (divisions * 2) | ||
const tests = range(span / divisions, span / 2, span / divisions) | ||
|
||
for (const dx of tests) { | ||
// Transform the test back into the point's coordinate system | ||
const testMovement = vec.scale(direction, dx) | ||
const testPoint = constrain( | ||
vec.transform( | ||
vec.add(vec.transform(point, userTransform), testMovement), | ||
inverseTransform, | ||
), | ||
) | ||
|
||
if (vec.dist(testPoint, point) > min) { | ||
onMove(testPoint) | ||
break | ||
} | ||
} | ||
} else { | ||
const { last, movement: pixelMovement, first } = state | ||
|
||
setDragging(!last) | ||
|
||
if (first) pickup.current = vec.transform(point, userTransform) | ||
if (vec.mag(pixelMovement) === 0) return | ||
|
||
const movement = vec.transform(pixelMovement, inverseViewTransform) | ||
onMove(constrain(vec.transform(vec.add(pickup.current, movement), inverseTransform))) | ||
} | ||
}, | ||
{ target, eventOptions: { passive: false } }, | ||
) | ||
return { dragging } | ||
} | ||
|
||
function getInverseTransform(transform: vec.Matrix) { | ||
const invert = vec.matrixInvert(transform) | ||
invariant( | ||
invert !== null, | ||
"Could not invert transform matrix. Your movable point's transformation matrix might be degenerative (mapping 2D space to a line).", | ||
) | ||
return invert | ||
} |