Improves pan and zoom gestures

pull/84/head
Steve Ruiz 2021-09-09 13:32:08 +01:00
rodzic 8154ed5a2a
commit b00e0d3a95
32 zmienionych plików z 148072 dodań i 619 usunięć

147191
.yarn/releases/yarn-1.19.0.cjs vendored 100755

File diff suppressed because one or more lines are too long

5
.yarnrc 100644
Wyświetl plik

@ -0,0 +1,5 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
yarn-path ".yarn/releases/yarn-1.19.0.cjs"

Wyświetl plik

@ -44,8 +44,9 @@
"babel-jest": "^27.1.0",
"eslint": "^7.32.0",
"fake-indexeddb": "^3.1.3",
"init-package-json": "^2.0.4",
"jest": "^27.1.0",
"lerna": "^3.15.0",
"lerna": "^3.22.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"resize-observer-polyfill": "^1.5.1",
@ -115,4 +116,4 @@
"\\+(.*)": "<rootDir>/packages/core/src/$1"
}
}
}
}

Wyświetl plik

@ -55,7 +55,7 @@
"react-dom": "^17.0.2"
},
"dependencies": {
"react-use-gesture": "^9.1.3"
"@use-gesture/react": "^10.0.0-beta.24"
},
"gitHead": "55da8880eb3d8ab5fb62b5eb7853065922c95dcf"
}

Wyświetl plik

@ -37,11 +37,8 @@ export function Canvas<T extends TLShape>({
}: CanvasProps<T>): JSX.Element {
const rCanvas = React.useRef<SVGSVGElement>(null)
const rContainer = React.useRef<HTMLDivElement>(null)
const rGroup = useCameraCss(pageState)
useResizeObserver(rCanvas)
useZoomEvents(rCanvas)
useSafariFocusOutFix()
@ -50,6 +47,8 @@ export function Canvas<T extends TLShape>({
const events = useCanvasEvents()
useResizeObserver(rCanvas)
return (
<div className="tl-container" ref={rContainer}>
<svg id="canvas" className="tl-canvas" ref={rCanvas} {...events}>

Wyświetl plik

@ -27,11 +27,11 @@ export function Page<T extends TLShape>({
hideIndicators,
meta,
}: PageProps<T>): JSX.Element {
const { callbacks, shapeUtils } = useTLContext()
const { callbacks, shapeUtils, inputs } = useTLContext()
useRenderOnResize()
const shapeTree = useShapeTree(page, pageState, shapeUtils, meta, callbacks.onChange)
const shapeTree = useShapeTree(page, pageState, shapeUtils, inputs.size, meta, callbacks.onChange)
const { shapeWithHandles } = useHandles(page, pageState)
@ -47,7 +47,7 @@ export function Page<T extends TLShape>({
<>
{bounds && !hideBounds && <BoundsBg bounds={bounds} rotation={rotation} />}
{shapeTree.map((node) => (
<ShapeNode key={node.shape.id} {...node} />
<ShapeNode key={node.shape.id} utils={shapeUtils} {...node} />
))}
{bounds && !hideBounds && (
<Bounds zoom={zoom} bounds={bounds} isLocked={isLocked} rotation={rotation} />

Wyświetl plik

@ -1,10 +1,11 @@
import * as React from 'react'
import type { IShapeTreeNode } from '+types'
import type { IShapeTreeNode, TLShape, TLShapeUtils } from '+types'
import { Shape } from './shape'
export const ShapeNode = React.memo(
<M extends Record<string, unknown>>({
shape,
utils,
children,
isEditing,
isBinding,
@ -12,7 +13,7 @@ export const ShapeNode = React.memo(
isSelected,
isCurrentParent,
meta,
}: IShapeTreeNode<M>) => {
}: { utils: TLShapeUtils<TLShape> } & IShapeTreeNode<M>) => {
return (
<>
<Shape
@ -22,10 +23,13 @@ export const ShapeNode = React.memo(
isHovered={isHovered}
isSelected={isSelected}
isCurrentParent={isCurrentParent}
utils={utils[shape.type]}
meta={meta}
/>
{children &&
children.map((childNode) => <ShapeNode key={childNode.shape.id} {...childNode} />)}
children.map((childNode) => (
<ShapeNode key={childNode.shape.id} utils={utils} {...childNode} />
))}
</>
)
}

Wyświetl plik

@ -7,6 +7,7 @@ describe('shape', () => {
renderWithSvg(
<Shape
shape={mockUtils.box.create({})}
utils={mockUtils[mockUtils.box.type]}
isEditing={false}
isBinding={false}
isHovered={false}

Wyświetl plik

@ -1,22 +1,20 @@
import * as React from 'react'
import { useShapeEvents, useTLContext } from '+hooks'
import type { IShapeTreeNode } from '+types'
import { useShapeEvents } from '+hooks'
import type { IShapeTreeNode, TLShape, TLShapeUtil } from '+types'
import { RenderedShape } from './rendered-shape'
import { EditingTextShape } from './editing-text-shape'
export const Shape = <M extends Record<string, unknown>>({
shape,
utils,
isEditing,
isBinding,
isHovered,
isSelected,
isCurrentParent,
meta,
}: IShapeTreeNode<M>) => {
const { shapeUtils } = useTLContext()
}: { utils: TLShapeUtil<TLShape> } & IShapeTreeNode<M>) => {
const events = useShapeEvents(shape.id, isCurrentParent)
const utils = shapeUtils[shape.type]
const center = utils.getCenter(shape)
const rotation = (shape.rotation || 0) * (180 / Math.PI)
const transform = `rotate(${rotation}, ${center}) translate(${shape.point})`

Wyświetl plik

@ -7,6 +7,7 @@ export function useBoundsEvents() {
const onPointerDown = React.useCallback(
(e: React.PointerEvent) => {
if (e.button !== 0) return
if (!inputs.pointerIsValid(e)) return
e.stopPropagation()
e.currentTarget?.setPointerCapture(e.pointerId)
const info = inputs.pointerDown(e, 'bounds')
@ -20,6 +21,7 @@ export function useBoundsEvents() {
const onPointerUp = React.useCallback(
(e: React.PointerEvent) => {
if (e.button !== 0) return
if (!inputs.pointerIsValid(e)) return
e.stopPropagation()
const isDoubleClick = inputs.isDoubleClick()
const info = inputs.pointerUp(e, 'bounds')
@ -40,8 +42,7 @@ export function useBoundsEvents() {
const onPointerMove = React.useCallback(
(e: React.PointerEvent) => {
if (inputs.pointer && e.pointerId !== inputs.pointer.pointerId) return
if (!inputs.pointerIsValid(e)) return
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
callbacks.onDragBounds?.(inputs.pointerMove(e, 'bounds'), e)
}
@ -53,6 +54,7 @@ export function useBoundsEvents() {
const onPointerEnter = React.useCallback(
(e: React.PointerEvent) => {
if (!inputs.pointerIsValid(e)) return
callbacks.onHoverBounds?.(inputs.pointerEnter(e, 'bounds'), e)
},
[callbacks, inputs]
@ -60,26 +62,17 @@ export function useBoundsEvents() {
const onPointerLeave = React.useCallback(
(e: React.PointerEvent) => {
if (!inputs.pointerIsValid(e)) return
callbacks.onUnhoverBounds?.(inputs.pointerEnter(e, 'bounds'), e)
},
[callbacks, inputs]
)
const onTouchStart = React.useCallback((e: React.TouchEvent) => {
e.preventDefault()
}, [])
const onTouchEnd = React.useCallback((e: React.TouchEvent) => {
e.preventDefault()
}, [])
return {
onPointerDown,
onPointerUp,
onPointerEnter,
onPointerMove,
onPointerLeave,
onTouchStart,
onTouchEnd,
}
}

Wyświetl plik

@ -8,6 +8,7 @@ export function useBoundsHandleEvents(id: TLBoundsCorner | TLBoundsEdge | 'rotat
const onPointerDown = React.useCallback(
(e: React.PointerEvent) => {
if (e.button !== 0) return
if (!inputs.pointerIsValid(e)) return
e.stopPropagation()
e.currentTarget?.setPointerCapture(e.pointerId)
const info = inputs.pointerDown(e, id)
@ -21,6 +22,7 @@ export function useBoundsHandleEvents(id: TLBoundsCorner | TLBoundsEdge | 'rotat
const onPointerUp = React.useCallback(
(e: React.PointerEvent) => {
if (e.button !== 0) return
if (!inputs.pointerIsValid(e)) return
e.stopPropagation()
const isDoubleClick = inputs.isDoubleClick()
const info = inputs.pointerUp(e, id)
@ -41,6 +43,7 @@ export function useBoundsHandleEvents(id: TLBoundsCorner | TLBoundsEdge | 'rotat
const onPointerMove = React.useCallback(
(e: React.PointerEvent) => {
if (!inputs.pointerIsValid(e)) return
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
callbacks.onDragBoundsHandle?.(inputs.pointerMove(e, id), e)
}
@ -52,6 +55,7 @@ export function useBoundsHandleEvents(id: TLBoundsCorner | TLBoundsEdge | 'rotat
const onPointerEnter = React.useCallback(
(e: React.PointerEvent) => {
if (!inputs.pointerIsValid(e)) return
callbacks.onHoverBoundsHandle?.(inputs.pointerEnter(e, id), e)
},
[inputs, callbacks, id]
@ -59,26 +63,17 @@ export function useBoundsHandleEvents(id: TLBoundsCorner | TLBoundsEdge | 'rotat
const onPointerLeave = React.useCallback(
(e: React.PointerEvent) => {
if (!inputs.pointerIsValid(e)) return
callbacks.onUnhoverBoundsHandle?.(inputs.pointerEnter(e, id), e)
},
[inputs, callbacks, id]
)
const onTouchStart = React.useCallback((e: React.TouchEvent) => {
e.preventDefault()
}, [])
const onTouchEnd = React.useCallback((e: React.TouchEvent) => {
e.preventDefault()
}, [])
return {
onPointerDown,
onPointerUp,
onPointerEnter,
onPointerMove,
onPointerLeave,
onTouchStart,
onTouchEnd,
}
}

Wyświetl plik

@ -7,6 +7,7 @@ export function useCanvasEvents() {
const onPointerDown = React.useCallback(
(e: React.PointerEvent) => {
if (e.button !== 0) return
if (!inputs.pointerIsValid(e)) return
e.currentTarget.setPointerCapture(e.pointerId)
if (e.button === 0) {
@ -20,6 +21,7 @@ export function useCanvasEvents() {
const onPointerMove = React.useCallback(
(e: React.PointerEvent) => {
if (!inputs.pointerIsValid(e)) return
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
const info = inputs.pointerMove(e, 'canvas')
callbacks.onDragCanvas?.(info, e)
@ -33,6 +35,7 @@ export function useCanvasEvents() {
const onPointerUp = React.useCallback(
(e: React.PointerEvent) => {
if (e.button !== 0) return
if (!inputs.pointerIsValid(e)) return
const isDoubleClick = inputs.isDoubleClick()
const info = inputs.pointerUp(e, 'canvas')

Wyświetl plik

@ -7,6 +7,7 @@ export function useHandleEvents(id: string) {
const onPointerDown = React.useCallback(
(e: React.PointerEvent) => {
if (e.button !== 0) return
if (!inputs.pointerIsValid(e)) return
e.stopPropagation()
e.currentTarget?.setPointerCapture(e.pointerId)
@ -20,6 +21,7 @@ export function useHandleEvents(id: string) {
const onPointerUp = React.useCallback(
(e: React.PointerEvent) => {
if (e.button !== 0) return
if (!inputs.pointerIsValid(e)) return
e.stopPropagation()
const isDoubleClick = inputs.isDoubleClick()
const info = inputs.pointerUp(e, id)
@ -40,6 +42,7 @@ export function useHandleEvents(id: string) {
const onPointerMove = React.useCallback(
(e: React.PointerEvent) => {
if (!inputs.pointerIsValid(e)) return
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
const info = inputs.pointerMove(e, id)
callbacks.onDragHandle?.(info, e)
@ -52,6 +55,7 @@ export function useHandleEvents(id: string) {
const onPointerEnter = React.useCallback(
(e: React.PointerEvent) => {
if (!inputs.pointerIsValid(e)) return
const info = inputs.pointerEnter(e, id)
callbacks.onHoverHandle?.(info, e)
},
@ -60,27 +64,18 @@ export function useHandleEvents(id: string) {
const onPointerLeave = React.useCallback(
(e: React.PointerEvent) => {
if (!inputs.pointerIsValid(e)) return
const info = inputs.pointerEnter(e, id)
callbacks.onUnhoverHandle?.(info, e)
},
[inputs, callbacks, id]
)
const onTouchStart = React.useCallback((e: React.TouchEvent) => {
e.preventDefault()
}, [])
const onTouchEnd = React.useCallback((e: React.TouchEvent) => {
e.preventDefault()
}, [])
return {
onPointerDown,
onPointerUp,
onPointerEnter,
onPointerMove,
onPointerLeave,
onTouchStart,
onTouchEnd,
}
}

Wyświetl plik

@ -1,32 +1,37 @@
import { useTLContext } from '+hooks'
import * as React from 'react'
import { Utils } from '+utils'
export function useResizeObserver<T extends HTMLElement | SVGElement>(ref: React.RefObject<T>) {
const { inputs } = useTLContext()
React.useEffect(() => {
function handleScroll() {
const rect = ref.current?.getBoundingClientRect()
if (rect) {
inputs.offset = [rect.left, rect.top]
}
const updateOffsets = React.useCallback(() => {
const rect = ref.current?.getBoundingClientRect()
if (rect) {
inputs.offset = [rect.left, rect.top]
inputs.size = [rect.width, rect.height]
}
}, [ref])
window.addEventListener('scroll', handleScroll)
React.useEffect(() => {
const debouncedUpdateOffsets = Utils.debounce(updateOffsets, 100)
window.addEventListener('scroll', debouncedUpdateOffsets)
window.addEventListener('resize', debouncedUpdateOffsets)
updateOffsets()
return () => {
window.removeEventListener('scroll', handleScroll)
window.removeEventListener('scroll', debouncedUpdateOffsets)
window.removeEventListener('resize', debouncedUpdateOffsets)
}
}, [inputs])
React.useEffect(() => {
const resizeObserver = new ResizeObserver((entries) => {
if (inputs.isPinching) return
if (inputs.isPinching) {
return
}
if (entries[0].contentRect) {
const rect = ref.current?.getBoundingClientRect()
if (rect) {
inputs.offset = [rect.left, rect.top]
}
updateOffsets()
}
})
@ -38,4 +43,10 @@ export function useResizeObserver<T extends HTMLElement | SVGElement>(ref: React
resizeObserver.disconnect()
}
}, [ref, inputs])
React.useEffect(() => {
setTimeout(() => {
updateOffsets()
})
}, [ref])
}

Wyświetl plik

@ -8,6 +8,7 @@ export function useShapeEvents(id: string, disable = false) {
const onPointerDown = React.useCallback(
(e: React.PointerEvent) => {
if (disable) return
if (!inputs.pointerIsValid(e)) return
if (e.button === 2) {
callbacks.onRightPointShape?.(inputs.pointerDown(e, id), e)
@ -43,6 +44,7 @@ export function useShapeEvents(id: string, disable = false) {
const onPointerUp = React.useCallback(
(e: React.PointerEvent) => {
if (e.button !== 0) return
if (!inputs.pointerIsValid(e)) return
if (disable) return
e.stopPropagation()
const isDoubleClick = inputs.isDoubleClick()
@ -64,6 +66,7 @@ export function useShapeEvents(id: string, disable = false) {
const onPointerMove = React.useCallback(
(e: React.PointerEvent) => {
if (!inputs.pointerIsValid(e)) return
if (disable) return
if (inputs.pointer && e.pointerId !== inputs.pointer.pointerId) return
@ -81,6 +84,7 @@ export function useShapeEvents(id: string, disable = false) {
const onPointerEnter = React.useCallback(
(e: React.PointerEvent) => {
if (!inputs.pointerIsValid(e)) return
if (disable) return
const info = inputs.pointerEnter(e, id)
callbacks.onHoverShape?.(info, e)
@ -91,27 +95,18 @@ export function useShapeEvents(id: string, disable = false) {
const onPointerLeave = React.useCallback(
(e: React.PointerEvent) => {
if (disable) return
if (!inputs.pointerIsValid(e)) return
const info = inputs.pointerEnter(e, id)
callbacks.onUnhoverShape?.(info, e)
},
[inputs, callbacks, id, disable]
)
const onTouchStart = React.useCallback((e: React.TouchEvent) => {
e.preventDefault()
}, [])
const onTouchEnd = React.useCallback((e: React.TouchEvent) => {
e.preventDefault()
}, [])
return {
onPointerDown,
onPointerUp,
onPointerEnter,
onPointerMove,
onPointerLeave,
onTouchStart,
onTouchEnd,
}
}

Wyświetl plik

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import * as React from 'react'
import type {
IShapeTreeNode,
@ -7,6 +8,7 @@ import type {
TLShapeUtils,
TLCallbacks,
TLBinding,
TLBounds,
} from '+types'
import { Utils, Vec } from '+utils'
@ -52,28 +54,32 @@ function addToShapeTree<T extends TLShape, M extends Record<string, unknown>>(
}
}
function shapeIsInViewport(shape: TLShape, bounds: TLBounds, viewport: TLBounds) {
return Utils.boundsContain(viewport, bounds) || Utils.boundsCollide(viewport, bounds)
}
export function useShapeTree<T extends TLShape, M extends Record<string, unknown>>(
page: TLPage<T, TLBinding>,
pageState: TLPageState,
shapeUtils: TLShapeUtils<T>,
size: number[],
meta?: M,
onChange?: TLCallbacks['onChange']
) {
const rTimeout = React.useRef<unknown>()
const rPreviousCount = React.useRef(0)
if (typeof window === 'undefined') return []
const rShapesIdsToRender = React.useRef(new Set<string>())
const rShapesToRender = React.useRef(new Set<TLShape>())
const { selectedIds, camera } = pageState
// Find viewport
// Filter the page's shapes down to only those that:
// - are the direct child of the page
// - collide with or are contained by the viewport
// - OR are selected
const [minX, minY] = Vec.sub(Vec.div([0, 0], camera.zoom), camera.point)
const [maxX, maxY] = Vec.sub(
Vec.div([window.innerWidth, window.innerHeight], camera.zoom),
camera.point
)
const [maxX, maxY] = Vec.sub(Vec.div(size, camera.zoom), camera.point)
const viewport = {
minX,
minY,
@ -83,28 +89,43 @@ export function useShapeTree<T extends TLShape, M extends Record<string, unknown
width: maxY - minY,
}
// Filter shapes that are in view, and that are the direct child of
// the page. Other shapes are not visible, or will be rendered as
// the children of groups.
const shapesToRender = rShapesToRender.current
const shapesIdsToRender = rShapesIdsToRender.current
const shapesToRender = Object.values(page.shapes).filter((shape) => {
if (shape.parentId !== page.id) return false
shapesToRender.clear()
shapesIdsToRender.clear()
// Don't hide selected shapes (this breaks certain drag interactions)
if (selectedIds.includes(shape.id)) return true
const shapeBounds = shapeUtils[shape.type as T['type']].getBounds(shape)
return Utils.boundsContain(viewport, shapeBounds) || Utils.boundsCollide(viewport, shapeBounds)
})
Object.values(page.shapes)
.filter((shape) => {
// Don't hide selected shapes (this breaks certain drag interactions)
if (
selectedIds.includes(shape.id) ||
shapeIsInViewport(shape, shapeUtils[shape.type as T['type']].getBounds(shape), viewport)
) {
if (shape.parentId === page.id) {
shapesIdsToRender.add(shape.id)
shapesToRender.add(shape)
} else {
shapesIdsToRender.add(shape.parentId)
shapesToRender.add(page.shapes[shape.parentId])
}
}
})
.sort((a, b) => a.childIndex - b.childIndex)
// Call onChange callback when number of rendering shapes changes
if (shapesToRender.length !== rPreviousCount.current) {
// Use a timeout to clear call stack, in case the onChange handleer
// produces a new state change (React won't like that)
setTimeout(() => onChange?.(shapesToRender.map((shape) => shape.id)), 0)
rPreviousCount.current = shapesToRender.length
if (shapesToRender.size !== rPreviousCount.current) {
// Use a timeout to clear call stack, in case the onChange handler
// produces a new state change, which could cause nested state
// changes, which is bad in React.
if (rTimeout.current) {
clearTimeout(rTimeout.current as number)
}
rTimeout.current = setTimeout(() => {
onChange?.(Array.from(shapesIdsToRender.values()))
}, 100)
rPreviousCount.current = shapesToRender.size
}
const bindingTargetId = pageState.bindingId ? page.bindings[pageState.bindingId].toId : undefined
@ -113,11 +134,9 @@ export function useShapeTree<T extends TLShape, M extends Record<string, unknown
const tree: IShapeTreeNode<M>[] = []
shapesToRender
.sort((a, b) => a.childIndex - b.childIndex)
.forEach((shape) =>
addToShapeTree(shape, tree, page.shapes, { ...pageState, bindingTargetId }, meta)
)
const info = { ...pageState, bindingTargetId }
shapesToRender.forEach((shape) => addToShapeTree(shape, tree, page.shapes, info, meta))
return tree
}

Wyświetl plik

@ -201,6 +201,7 @@ const tlcss = css`
height: 100%;
padding: 0px;
margin: 0px;
touch-action: none;
overscroll-behavior: none;
overscroll-behavior-x: none;
background-color: var(--tl-background);

Wyświetl plik

@ -1,86 +1,99 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import * as React from 'react'
import { useTLContext } from './useTLContext'
import { Vec } from '+utils'
import { useWheel, usePinch } from 'react-use-gesture'
import Utils, { Vec } from '+utils'
import { useGesture } from '@use-gesture/react'
// Capture zoom gestures (pinches, wheels and pans)
export function useZoomEvents<T extends HTMLElement | SVGElement>(ref: React.RefObject<T>) {
const rPinchDa = React.useRef<number[] | undefined>(undefined)
const rOriginPoint = React.useRef<number[] | undefined>(undefined)
const rPinchPoint = React.useRef<number[] | undefined>(undefined)
const rDelta = React.useRef<number[]>([0, 0])
const { inputs, callbacks } = useTLContext()
useWheel(
({ event: e, delta }) => {
const elm = ref.current
if (!(e.target === elm || elm?.contains(e.target as Node))) return
e.preventDefault()
if (Vec.isEqual(delta, [0, 0])) return
const info = inputs.pan(delta, e as WheelEvent)
callbacks.onPan?.(info, e)
},
{
domTarget: window,
eventOptions: { passive: false },
React.useEffect(() => {
const preventGesture = (event: TouchEvent) => {
event.preventDefault()
}
)
usePinch(
({ pinching, da, origin, event: e }) => {
const elm = ref.current
if (!(e.target === elm || elm?.contains(e.target as Node))) return
// @ts-ignore
document.addEventListener('gesturestart', preventGesture)
// @ts-ignore
document.addEventListener('gesturechange', preventGesture)
const info = inputs.pinch(origin, origin)
return () => {
// @ts-ignore
document.removeEventListener('gesturestart', preventGesture)
// @ts-ignore
document.removeEventListener('gesturechange', preventGesture)
}
}, [])
useGesture(
{
onWheel: ({ event: e, delta }) => {
const elm = ref.current
if (!(e.target === elm || elm?.contains(e.target as Node))) return
e.preventDefault()
if (inputs.isPinching) return
if (Vec.isEqual(delta, [0, 0])) return
const info = inputs.pan(delta, e as WheelEvent)
callbacks.onPan?.(info, e)
},
onPinchStart: ({ origin, event }) => {
const elm = ref.current
if (!(event.target === elm || elm?.contains(event.target as Node))) return
const info = inputs.pinch(origin, origin)
inputs.isPinching = true
callbacks.onPinchStart?.(info, event)
rPinchPoint.current = info.point
rOriginPoint.current = info.origin
rDelta.current = [0, 0]
},
onPinchEnd: ({ origin, event }) => {
const elm = ref.current
if (!(event.target === elm || elm?.contains(event.target as Node))) return
const info = inputs.pinch(origin, origin)
if (!pinching) {
inputs.isPinching = false
callbacks.onPinchEnd?.(
info,
e as React.WheelEvent<Element> | WheelEvent | React.TouchEvent<Element> | TouchEvent
)
rPinchDa.current = undefined
callbacks.onPinchEnd?.(info, event)
rPinchPoint.current = undefined
rOriginPoint.current = undefined
return
}
rDelta.current = [0, 0]
},
onPinch: ({ delta, origin, event }) => {
const elm = ref.current
if (!(event.target === elm || elm?.contains(event.target as Node))) return
if (!rOriginPoint.current) throw Error('No origin point!')
if (rPinchPoint.current === undefined) {
inputs.isPinching = true
callbacks.onPinchStart?.(
info,
e as React.WheelEvent<Element> | WheelEvent | React.TouchEvent<Element> | TouchEvent
const info = inputs.pinch(origin, rOriginPoint.current)
const trueDelta = Vec.sub(info.delta, rDelta.current)
rDelta.current = info.delta
callbacks.onPinch?.(
{
...info,
point: info.point,
origin: rOriginPoint.current,
delta: [...trueDelta, -delta[0]],
},
event
)
rPinchDa.current = da
rPinchPoint.current = info.point
rOriginPoint.current = info.point
}
if (!rPinchDa.current) throw Error('No pinch direction!')
if (!rOriginPoint.current) throw Error('No origin point!')
const [distanceDelta] = Vec.sub(rPinchDa.current, da)
callbacks.onPinch?.(
{
...info,
point: origin,
origin: rOriginPoint.current,
delta: [...info.delta, distanceDelta],
},
e as React.WheelEvent<Element> | WheelEvent | React.TouchEvent<Element> | TouchEvent
)
rPinchDa.current = da
rPinchPoint.current = origin
rPinchPoint.current = origin
},
},
{
domTarget: window,
target: ref.current,
eventOptions: { passive: false },
}
)

Wyświetl plik

@ -11,15 +11,32 @@ export class Inputs {
isPinching = false
offset = [0, 0]
size = [10, 10]
pointerUpTime = 0
activePointer?: number
pointerIsValid(e: TouchEvent | React.TouchEvent | PointerEvent | React.PointerEvent) {
if ('pointerId' in e) {
if (this.activePointer && this.activePointer !== e.pointerId) return false
}
if ('touches' in e) {
const touch = e.changedTouches[0]
if (this.activePointer && this.activePointer !== touch.identifier) return false
}
return true
}
touchStart<T extends string>(e: TouchEvent | React.TouchEvent, target: T): TLPointerInfo<T> {
const { shiftKey, ctrlKey, metaKey, altKey } = e
e.preventDefault()
const touch = e.changedTouches[0]
this.activePointer = touch.identifier
const info: TLPointerInfo<T> = {
target,
pointerId: touch.identifier,
@ -38,9 +55,33 @@ export class Inputs {
return info
}
touchEnd<T extends string>(e: TouchEvent | React.TouchEvent, target: T): TLPointerInfo<T> {
const { shiftKey, ctrlKey, metaKey, altKey } = e
const touch = e.changedTouches[0]
const info: TLPointerInfo<T> = {
target,
pointerId: touch.identifier,
origin: Inputs.getPoint(touch),
delta: [0, 0],
point: Inputs.getPoint(touch),
pressure: Inputs.getPressure(touch),
shiftKey,
ctrlKey,
metaKey: Utils.isDarwin() ? metaKey : ctrlKey,
altKey,
}
this.pointer = info
this.activePointer = undefined
return info
}
touchMove<T extends string>(e: TouchEvent | React.TouchEvent, target: T): TLPointerInfo<T> {
const { shiftKey, ctrlKey, metaKey, altKey } = e
e.preventDefault()
const touch = e.changedTouches[0]
@ -74,6 +115,8 @@ export class Inputs {
const point = Inputs.getPoint(e, this.offset)
this.activePointer = e.pointerId
const info: TLPointerInfo<T> = {
target,
pointerId: e.pointerId,
@ -155,6 +198,8 @@ export class Inputs {
const delta = prev?.point ? Vec.sub(point, prev.point) : [0, 0]
this.activePointer = undefined
const info: TLPointerInfo<T> = {
origin: point,
...prev,
@ -277,14 +322,12 @@ export class Inputs {
pinch(point: number[], origin: number[]) {
const { shiftKey, ctrlKey, metaKey, altKey } = this.keys
const prev = this.pointer
const delta = Vec.sub(origin, point)
const info: TLPointerInfo<'pinch'> = {
pointerId: 0,
target: 'pinch',
origin: prev?.origin || Vec.sub(Vec.round(point), this.offset),
origin,
delta: delta,
point: Vec.sub(Vec.round(point), this.offset),
pressure: 0.5,
@ -303,6 +346,7 @@ export class Inputs {
this.pointerUpTime = 0
this.pointer = undefined
this.keyboard = undefined
this.activePointer = undefined
this.keys = {}
}

Wyświetl plik

@ -98,7 +98,13 @@ export type TLWheelEventHandler = (
) => void
export type TLPinchEventHandler = (
info: TLPointerInfo<string>,
e: React.WheelEvent<Element> | WheelEvent | React.TouchEvent<Element> | TouchEvent
e:
| React.WheelEvent<Element>
| WheelEvent
| React.TouchEvent<Element>
| TouchEvent
| React.PointerEvent<Element>
| PointerEventInit
) => void
export type TLPointerEventHandler = (info: TLPointerInfo<string>, e: React.PointerEvent) => void
export type TLCanvasEventHandler = (info: TLPointerInfo<'canvas'>, e: React.PointerEvent) => void

Wyświetl plik

@ -1639,7 +1639,7 @@ left past the initial left edge) then swap points on that axis.
/**
* Debounce a function.
*/
static debounce<T extends (...args: unknown[]) => void>(fn: T, ms = 0) {
static debounce<T extends (...args: any[]) => void>(fn: T, ms = 0) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let timeoutId: number | any
return function (...args: Parameters<T>) {
@ -1655,18 +1655,22 @@ left past the initial left edge) then swap points on that axis.
static getSvgPathFromStroke(stroke: number[][]): string {
if (!stroke.length) return ''
const max = stroke.length - 1
const d = stroke.reduce(
(acc, [x0, y0], i, arr) => {
const [x1, y1] = arr[(i + 1) % arr.length]
if (i === max) return acc
const [x1, y1] = arr[i + 1]
acc.push(` ${x0},${y0} ${(x0 + x1) / 2},${(y0 + y1) / 2}`)
return acc
},
['M ', `${stroke[0][0]},${stroke[0][1]}`, ' Q']
)
d.push(' Z')
return d.join('').replaceAll(/(\s?[A-Z]?,?-?[0-9]*\.[0-9]{0,2})(([0-9]|e|-)*)/g, '$1')
return d
.concat('Z')
.join('')
.replaceAll(/(\s?[A-Z]?,?-?[0-9]*\.[0-9]{0,2})(([0-9]|e|-)*)/g, '$1')
}
/* -------------------------------------------------- */
@ -1702,7 +1706,7 @@ left past the initial left edge) then swap points on that axis.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
lastResult = func.apply(this, ...args)
lastResult = func(...args)
}
return lastResult

Wyświetl plik

@ -9,13 +9,11 @@ if (!fs.existsSync('./dist')) {
fs.mkdirSync('./dist')
}
fs.copyFile('./src/styles.css', './dist/styles.css', (err) => {
if (err) throw err
})
fs.copyFile('./src/index.html', './dist/index.html', (err) => {
if (err) throw err
})
for (const file of ['styles.css', 'index.html']) {
fs.copyFile(`./src/${file}`, './dist/${file}', (err) => {
if (err) throw err
})
}
esbuild
.build({
@ -25,6 +23,7 @@ esbuild
minify: false,
sourcemap: true,
incremental: isDevServer,
platform: 'browser',
target: ['chrome58', 'firefox57', 'safari11', 'edge18'],
define: {
'process.env.NODE_ENV': isDevServer ? '"development"' : '"production"',

Wyświetl plik

@ -21,12 +21,15 @@
"@tldraw/tldraw": "^0.0.85",
"idb": "^6.1.2",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react-dom": "^17.0.2",
"react-router": "^5.2.1",
"react-router-dom": "^5.3.0"
},
"devDependencies": {
"@types/node": "^14.14.35",
"@types/react": "^17.0.3",
"@types/react-dom": "^17.0.2",
"@types/react-router-dom": "^5.1.8",
"concurrently": "6.0.1",
"create-serve": "1.0.1",
"esbuild": "0.11.5",

Wyświetl plik

@ -1,9 +1,50 @@
import * as React from 'react'
import { Switch, Route, Link } from 'react-router-dom'
import Basic from './basic'
import Controlled from './controlled'
import Imperative from './imperative'
import Small from './small'
import Embedded from './embedded'
import ChangingId from './changing-id'
export default function App(): JSX.Element {
return <Small />
return (
<main>
<Switch>
<Route path="/basic">
<Basic />
</Route>
<Route path="/controlled">
<Controlled />
</Route>
<Route path="/imperative">
<Imperative />
</Route>
<Route path="/changing-id">
<ChangingId />
</Route>
<Route path="/embedded">
<Embedded />
</Route>
<Route path="/">
<ul>
<li>
<Link to="/basic">basic</Link>
</li>
<li>
<Link to="/controlled">controlled</Link>
</li>
<li>
<Link to="/imperative">imperative</Link>
</li>
<li>
<Link to="/changing-id">changing id</Link>
</li>
<li>
<Link to="/embedded">embedded</Link>
</li>
</ul>
</Route>
</Switch>
</main>
)
}

Wyświetl plik

@ -1,6 +1,6 @@
import * as React from 'react'
import Editor from './components/editor'
export default function BasicUsage(): JSX.Element {
export default function Basic(): JSX.Element {
return <Editor />
}

Wyświetl plik

@ -1,7 +1,7 @@
import * as React from 'react'
import { TLDraw } from '@tldraw/tldraw'
export default function NewId() {
export default function ChangingId() {
const [id, setId] = React.useState('example')
React.useEffect(() => {

Wyświetl plik

@ -1,7 +1,7 @@
import * as React from 'react'
import Editor from './components/editor'
export default function BasicUsage(): JSX.Element {
export default function Embedded(): JSX.Element {
return (
<div>
<div

Wyświetl plik

@ -2,7 +2,6 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="favicon.ico" />
<link rel="stylesheet" href="styles.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>tldraw</title>

Wyświetl plik

@ -1,10 +1,13 @@
import React from 'react'
import ReactDOM from 'react-dom'
import App from './app'
import { HashRouter } from 'react-router-dom'
ReactDOM.render(
<React.StrictMode>
<App />
<HashRouter>
<App />
</HashRouter>
</React.StrictMode>,
document.getElementById('root')
)

Wyświetl plik

@ -90,7 +90,6 @@ export class TextSession implements Session {
// if (initialShape.text.trim() === '' && shape.text.trim() === '') {
// // delete shape
// console.log('deleting shape')
// return {
// id: 'text',
// before: {

Wyświetl plik

@ -1005,7 +1005,7 @@ export class TLDrawState extends StateManager<Data> {
*/
pinchZoom = (point: number[], delta: number[], zoomDelta: number): this => {
const { camera } = this.pageState
const nextPoint = Vec.add(camera.point, Vec.div(delta, camera.zoom))
const nextPoint = Vec.sub(camera.point, Vec.div(delta, camera.zoom))
const nextZoom = TLDR.getCameraZoom(camera.zoom - zoomDelta * camera.zoom)
const p0 = Vec.sub(Vec.div(point, camera.zoom), nextPoint)
const p1 = Vec.sub(Vec.div(point, nextZoom), nextPoint)
@ -2227,6 +2227,9 @@ export class TLDrawState extends StateManager<Data> {
/* ------------- Renderer Event Handlers ------------ */
onPinchStart: TLPinchEventHandler = () => {
if (this.session) {
this.cancelSession()
}
this.setStatus(TLDrawStatus.Pinching)
}
@ -2236,13 +2239,13 @@ export class TLDrawState extends StateManager<Data> {
// const nextZoom = TLDR.getCameraZoom(i * 0.25)
// this.zoomTo(nextZoom, inputs.pointer?.point)
// }
this.setStatus(this.appState.status.previous)
this.setStatus(TLDrawStatus.Idle)
}
onPinch: TLPinchEventHandler = (info) => {
if (this.appState.status.current !== TLDrawStatus.Pinching) return
this.pinchZoom(info.origin, info.delta, info.delta[2] / 350)
this.pinchZoom(info.point, info.delta, info.delta[2])
this.updateOnPointerMove(info)
}

976
yarn.lock

Plik diff jest za duży Load Diff