kopia lustrzana https://github.com/Tldraw/Tldraw
Improves pan and zoom gestures
rodzic
8154ed5a2a
commit
b00e0d3a95
File diff suppressed because one or more lines are too long
|
@ -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"
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ describe('shape', () => {
|
|||
renderWithSvg(
|
||||
<Shape
|
||||
shape={mockUtils.box.create({})}
|
||||
utils={mockUtils[mockUtils.box.type]}
|
||||
isEditing={false}
|
||||
isBinding={false}
|
||||
isHovered={false}
|
||||
|
|
|
@ -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})`
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 },
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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 = {}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"',
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 />
|
||||
}
|
||||
|
|
|
@ -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(() => {
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
@ -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')
|
||||
)
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue