Merge pull request #84 from tldraw/feature-resize-observer
[feature] resize observer, embedded component improvements
This commit is contained in:
commit
1b3084b502
50 changed files with 150273 additions and 912 deletions
147191
.yarn/releases/yarn-1.19.0.cjs
vendored
Executable file
147191
.yarn/releases/yarn-1.19.0.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
5
.yarnrc
Normal file
5
.yarnrc
Normal file
|
@ -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,10 +44,12 @@
|
|||
"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",
|
||||
"ts-jest": "^27.0.5",
|
||||
"tslib": "^2.3.0",
|
||||
"typedoc": "^0.21.9",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ describe('bounds', () => {
|
|||
zoom={1}
|
||||
bounds={{ minX: 0, minY: 0, maxX: 100, maxY: 100, width: 100, height: 100 }}
|
||||
rotation={0}
|
||||
viewportWidth={1000}
|
||||
isLocked={false}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -11,10 +11,18 @@ interface BoundsProps {
|
|||
bounds: TLBounds
|
||||
rotation: number
|
||||
isLocked: boolean
|
||||
viewportWidth: number
|
||||
}
|
||||
|
||||
export function Bounds({ zoom, bounds, rotation, isLocked }: BoundsProps): JSX.Element {
|
||||
const size = (Utils.isMobileSize() ? 10 : 8) / zoom // Touch target size
|
||||
export function Bounds({
|
||||
zoom,
|
||||
bounds,
|
||||
viewportWidth,
|
||||
rotation,
|
||||
isLocked,
|
||||
}: BoundsProps): JSX.Element {
|
||||
const targetSize = (viewportWidth < 768 ? 16 : 8) / zoom // Touch target size
|
||||
const size = 8 / zoom // Touch target size
|
||||
const center = Utils.getBoundsCenter(bounds)
|
||||
|
||||
return (
|
||||
|
@ -28,15 +36,50 @@ export function Bounds({ zoom, bounds, rotation, isLocked }: BoundsProps): JSX.E
|
|||
<CenterHandle bounds={bounds} isLocked={isLocked} />
|
||||
{!isLocked && (
|
||||
<>
|
||||
<EdgeHandle size={size} bounds={bounds} edge={TLBoundsEdge.Top} />
|
||||
<EdgeHandle size={size} bounds={bounds} edge={TLBoundsEdge.Right} />
|
||||
<EdgeHandle size={size} bounds={bounds} edge={TLBoundsEdge.Bottom} />
|
||||
<EdgeHandle size={size} bounds={bounds} edge={TLBoundsEdge.Left} />
|
||||
<CornerHandle size={size} bounds={bounds} corner={TLBoundsCorner.TopLeft} />
|
||||
<CornerHandle size={size} bounds={bounds} corner={TLBoundsCorner.TopRight} />
|
||||
<CornerHandle size={size} bounds={bounds} corner={TLBoundsCorner.BottomRight} />
|
||||
<CornerHandle size={size} bounds={bounds} corner={TLBoundsCorner.BottomLeft} />
|
||||
<RotateHandle size={size} bounds={bounds} />
|
||||
<EdgeHandle targetSize={targetSize} size={size} bounds={bounds} edge={TLBoundsEdge.Top} />
|
||||
<EdgeHandle
|
||||
targetSize={targetSize}
|
||||
size={size}
|
||||
bounds={bounds}
|
||||
edge={TLBoundsEdge.Right}
|
||||
/>
|
||||
<EdgeHandle
|
||||
targetSize={targetSize}
|
||||
size={size}
|
||||
bounds={bounds}
|
||||
edge={TLBoundsEdge.Bottom}
|
||||
/>
|
||||
<EdgeHandle
|
||||
targetSize={targetSize}
|
||||
size={size}
|
||||
bounds={bounds}
|
||||
edge={TLBoundsEdge.Left}
|
||||
/>
|
||||
<CornerHandle
|
||||
targetSize={targetSize}
|
||||
size={size}
|
||||
bounds={bounds}
|
||||
corner={TLBoundsCorner.TopLeft}
|
||||
/>
|
||||
<CornerHandle
|
||||
targetSize={targetSize}
|
||||
size={size}
|
||||
bounds={bounds}
|
||||
corner={TLBoundsCorner.TopRight}
|
||||
/>
|
||||
<CornerHandle
|
||||
targetSize={targetSize}
|
||||
size={size}
|
||||
bounds={bounds}
|
||||
corner={TLBoundsCorner.BottomRight}
|
||||
/>
|
||||
<CornerHandle
|
||||
targetSize={targetSize}
|
||||
size={size}
|
||||
bounds={bounds}
|
||||
corner={TLBoundsCorner.BottomLeft}
|
||||
/>
|
||||
<RotateHandle targetSize={targetSize} size={size} bounds={bounds} />
|
||||
</>
|
||||
)}
|
||||
</g>
|
||||
|
|
|
@ -9,16 +9,15 @@ const cornerBgClassnames = {
|
|||
[TLBoundsCorner.BottomLeft]: 'tl-transparent tl-cursor-nesw',
|
||||
}
|
||||
|
||||
interface CornerHandleProps {
|
||||
size: number
|
||||
targetSize: number
|
||||
bounds: TLBounds
|
||||
corner: TLBoundsCorner
|
||||
}
|
||||
|
||||
export const CornerHandle = React.memo(
|
||||
({
|
||||
size,
|
||||
corner,
|
||||
bounds,
|
||||
}: {
|
||||
size: number
|
||||
bounds: TLBounds
|
||||
corner: TLBoundsCorner
|
||||
}): JSX.Element => {
|
||||
({ size, targetSize, corner, bounds }: CornerHandleProps): JSX.Element => {
|
||||
const events = useBoundsHandleEvents(corner)
|
||||
|
||||
const isTop = corner === TLBoundsCorner.TopLeft || corner === TLBoundsCorner.TopRight
|
||||
|
@ -28,19 +27,20 @@ export const CornerHandle = React.memo(
|
|||
<g>
|
||||
<rect
|
||||
className={cornerBgClassnames[corner]}
|
||||
x={(isLeft ? -1 : bounds.width + 1) - size}
|
||||
y={(isTop ? -1 : bounds.height + 1) - size}
|
||||
width={size * 2}
|
||||
height={size * 2}
|
||||
x={(isLeft ? -1 : bounds.width + 1) - targetSize}
|
||||
y={(isTop ? -1 : bounds.height + 1) - targetSize}
|
||||
width={targetSize * 2}
|
||||
height={targetSize * 2}
|
||||
pointerEvents="all"
|
||||
fill="red"
|
||||
{...events}
|
||||
/>
|
||||
<rect
|
||||
className="tl-corner-handle"
|
||||
x={(isLeft ? -1 : bounds.width + 1) - 4}
|
||||
y={(isTop ? -1 : bounds.height + 1) - 4}
|
||||
width={8}
|
||||
height={8}
|
||||
x={(isLeft ? -1 : bounds.width + 1) - size / 2}
|
||||
y={(isTop ? -1 : bounds.height + 1) - size / 2}
|
||||
width={size}
|
||||
height={size}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
</g>
|
||||
|
|
|
@ -9,24 +9,29 @@ const edgeClassnames = {
|
|||
[TLBoundsEdge.Left]: 'tl-transparent tl-cursor-ew',
|
||||
}
|
||||
|
||||
export const EdgeHandle = React.memo(
|
||||
({ size, bounds, edge }: { size: number; bounds: TLBounds; edge: TLBoundsEdge }): JSX.Element => {
|
||||
const events = useBoundsHandleEvents(edge)
|
||||
interface EdgeHandleProps {
|
||||
targetSize: number
|
||||
size: number
|
||||
bounds: TLBounds
|
||||
edge: TLBoundsEdge
|
||||
}
|
||||
|
||||
const isHorizontal = edge === TLBoundsEdge.Top || edge === TLBoundsEdge.Bottom
|
||||
const isFarEdge = edge === TLBoundsEdge.Right || edge === TLBoundsEdge.Bottom
|
||||
export const EdgeHandle = React.memo(({ size, bounds, edge }: EdgeHandleProps): JSX.Element => {
|
||||
const events = useBoundsHandleEvents(edge)
|
||||
|
||||
const { height, width } = bounds
|
||||
const isHorizontal = edge === TLBoundsEdge.Top || edge === TLBoundsEdge.Bottom
|
||||
const isFarEdge = edge === TLBoundsEdge.Right || edge === TLBoundsEdge.Bottom
|
||||
|
||||
return (
|
||||
<rect
|
||||
className={edgeClassnames[edge]}
|
||||
x={isHorizontal ? size / 2 : (isFarEdge ? width + 1 : -1) - size / 2}
|
||||
y={isHorizontal ? (isFarEdge ? height + 1 : -1) - size / 2 : size / 2}
|
||||
width={isHorizontal ? Math.max(0, width + 1 - size) : size}
|
||||
height={isHorizontal ? size : Math.max(0, height + 1 - size)}
|
||||
{...events}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
const { height, width } = bounds
|
||||
|
||||
return (
|
||||
<rect
|
||||
className={edgeClassnames[edge]}
|
||||
x={isHorizontal ? size / 2 : (isFarEdge ? width + 1 : -1) - size / 2}
|
||||
y={isHorizontal ? (isFarEdge ? height + 1 : -1) - size / 2 : size / 2}
|
||||
width={isHorizontal ? Math.max(0, width + 1 - size) : size}
|
||||
height={isHorizontal ? size : Math.max(0, height + 1 - size)}
|
||||
{...events}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -2,18 +2,23 @@ import * as React from 'react'
|
|||
import { useBoundsHandleEvents } from '+hooks'
|
||||
import type { TLBounds } from '+types'
|
||||
|
||||
interface RotateHandleProps {
|
||||
bounds: TLBounds
|
||||
size: number
|
||||
targetSize: number
|
||||
}
|
||||
|
||||
export const RotateHandle = React.memo(
|
||||
({ bounds, size }: { bounds: TLBounds; size: number }): JSX.Element => {
|
||||
({ bounds, targetSize, size }: RotateHandleProps): JSX.Element => {
|
||||
const events = useBoundsHandleEvents('rotate')
|
||||
|
||||
return (
|
||||
<g cursor="grab">
|
||||
<circle
|
||||
className="tl-transparent"
|
||||
cx={bounds.width / 2}
|
||||
cy={size * -2}
|
||||
r={size * 2}
|
||||
fill="transparent"
|
||||
stroke="none"
|
||||
r={targetSize * 2}
|
||||
pointerEvents="all"
|
||||
{...events}
|
||||
/>
|
||||
|
@ -21,7 +26,7 @@ export const RotateHandle = React.memo(
|
|||
className="tl-rotate-handle"
|
||||
cx={bounds.width / 2}
|
||||
cy={size * -2}
|
||||
r={4}
|
||||
r={size / 2}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
</g>
|
||||
|
|
|
@ -12,6 +12,7 @@ import { ErrorBoundary } from '+components/error-boundary'
|
|||
import { Brush } from '+components/brush'
|
||||
import { Defs } from '+components/defs'
|
||||
import { Page } from '+components/page'
|
||||
import { useResizeObserver } from '+hooks/useResizeObserver'
|
||||
|
||||
function resetError() {
|
||||
void null
|
||||
|
@ -35,10 +36,11 @@ export function Canvas<T extends TLShape>({
|
|||
hideIndicators = false,
|
||||
}: CanvasProps<T>): JSX.Element {
|
||||
const rCanvas = React.useRef<SVGSVGElement>(null)
|
||||
const rContainer = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
const rGroup = useCameraCss(pageState)
|
||||
useResizeObserver(rCanvas)
|
||||
|
||||
useZoomEvents()
|
||||
useZoomEvents(rCanvas)
|
||||
|
||||
useSafariFocusOutFix()
|
||||
|
||||
|
@ -46,8 +48,10 @@ export function Canvas<T extends TLShape>({
|
|||
|
||||
const events = useCanvasEvents()
|
||||
|
||||
const rGroup = useCameraCss(rContainer, pageState)
|
||||
|
||||
return (
|
||||
<div className="tl-container">
|
||||
<div className="tl-container" ref={rContainer}>
|
||||
<svg id="canvas" className="tl-canvas" ref={rCanvas} {...events}>
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={resetError}>
|
||||
<Defs zoom={pageState.camera.zoom} />
|
||||
|
|
|
@ -27,11 +27,9 @@ 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,10 +45,16 @@ 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} />
|
||||
<Bounds
|
||||
zoom={zoom}
|
||||
bounds={bounds}
|
||||
viewportWidth={inputs.size[0]}
|
||||
isLocked={isLocked}
|
||||
rotation={rotation}
|
||||
/>
|
||||
)}
|
||||
{!hideIndicators &&
|
||||
selectedIds
|
||||
|
|
|
@ -10,6 +10,7 @@ import type {
|
|||
TLBinding,
|
||||
} from '../../types'
|
||||
import { Canvas } from '../canvas'
|
||||
import { Inputs } from '../../inputs'
|
||||
import { useTLTheme, TLContext, TLContextType } from '../../hooks'
|
||||
|
||||
export interface RendererProps<T extends TLShape, M extends Record<string, unknown>>
|
||||
|
@ -87,6 +88,7 @@ export function Renderer<T extends TLShape, M extends Record<string, unknown>>({
|
|||
shapeUtils,
|
||||
rScreenBounds,
|
||||
rPageState,
|
||||
inputs: new Inputs(),
|
||||
}))
|
||||
|
||||
return (
|
||||
|
|
|
@ -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})`
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import * as React from 'react'
|
||||
import { inputs } from '+inputs'
|
||||
import { useTLContext } from './useTLContext'
|
||||
|
||||
export function useBoundsEvents() {
|
||||
const { callbacks } = useTLContext()
|
||||
const { callbacks, inputs } = useTLContext()
|
||||
|
||||
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')
|
||||
|
@ -15,12 +15,13 @@ export function useBoundsEvents() {
|
|||
callbacks.onPointBounds?.(info, e)
|
||||
callbacks.onPointerDown?.(info, e)
|
||||
},
|
||||
[callbacks]
|
||||
[callbacks, inputs]
|
||||
)
|
||||
|
||||
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')
|
||||
|
@ -36,51 +37,42 @@ export function useBoundsEvents() {
|
|||
callbacks.onReleaseBounds?.(info, e)
|
||||
callbacks.onPointerUp?.(info, e)
|
||||
},
|
||||
[callbacks]
|
||||
[callbacks, inputs]
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
const info = inputs.pointerMove(e, 'bounds')
|
||||
callbacks.onPointerMove?.(info, e)
|
||||
},
|
||||
[callbacks]
|
||||
[callbacks, inputs]
|
||||
)
|
||||
|
||||
const onPointerEnter = React.useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
if (!inputs.pointerIsValid(e)) return
|
||||
callbacks.onHoverBounds?.(inputs.pointerEnter(e, 'bounds'), e)
|
||||
},
|
||||
[callbacks]
|
||||
[callbacks, inputs]
|
||||
)
|
||||
|
||||
const onPointerLeave = React.useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
if (!inputs.pointerIsValid(e)) return
|
||||
callbacks.onUnhoverBounds?.(inputs.pointerEnter(e, 'bounds'), e)
|
||||
},
|
||||
[callbacks]
|
||||
[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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import * as React from 'react'
|
||||
import { inputs } from '+inputs'
|
||||
import type { TLBoundsEdge, TLBoundsCorner } from '+types'
|
||||
import { useTLContext } from './useTLContext'
|
||||
|
||||
export function useBoundsHandleEvents(id: TLBoundsCorner | TLBoundsEdge | 'rotate') {
|
||||
const { callbacks } = useTLContext()
|
||||
const { callbacks, inputs } = useTLContext()
|
||||
|
||||
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)
|
||||
|
@ -16,12 +16,13 @@ export function useBoundsHandleEvents(id: TLBoundsCorner | TLBoundsEdge | 'rotat
|
|||
callbacks.onPointBoundsHandle?.(info, e)
|
||||
callbacks.onPointerDown?.(info, e)
|
||||
},
|
||||
[callbacks, id]
|
||||
[inputs, callbacks, id]
|
||||
)
|
||||
|
||||
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)
|
||||
|
@ -37,49 +38,42 @@ export function useBoundsHandleEvents(id: TLBoundsCorner | TLBoundsEdge | 'rotat
|
|||
callbacks.onReleaseBoundsHandle?.(info, e)
|
||||
callbacks.onPointerUp?.(info, e)
|
||||
},
|
||||
[callbacks, id]
|
||||
[inputs, callbacks, id]
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
const info = inputs.pointerMove(e, id)
|
||||
callbacks.onPointerMove?.(info, e)
|
||||
},
|
||||
[callbacks, id]
|
||||
[inputs, callbacks, id]
|
||||
)
|
||||
|
||||
const onPointerEnter = React.useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
if (!inputs.pointerIsValid(e)) return
|
||||
callbacks.onHoverBoundsHandle?.(inputs.pointerEnter(e, id), e)
|
||||
},
|
||||
[callbacks, id]
|
||||
[inputs, callbacks, id]
|
||||
)
|
||||
|
||||
const onPointerLeave = React.useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
if (!inputs.pointerIsValid(e)) return
|
||||
callbacks.onUnhoverBoundsHandle?.(inputs.pointerEnter(e, id), e)
|
||||
},
|
||||
[callbacks, id]
|
||||
[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,12 +1,13 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import * as React from 'react'
|
||||
import type { TLPageState } from '+types'
|
||||
|
||||
export function useCameraCss(pageState: TLPageState) {
|
||||
export function useCameraCss(ref: React.RefObject<HTMLDivElement>, pageState: TLPageState) {
|
||||
const rGroup = React.useRef<SVGGElement>(null)
|
||||
|
||||
// Update the tl-zoom CSS variable when the zoom changes
|
||||
React.useEffect(() => {
|
||||
document.documentElement.style.setProperty('--tl-zoom', pageState.camera.zoom.toString())
|
||||
ref.current!.style.setProperty('--tl-zoom', pageState.camera.zoom.toString())
|
||||
}, [pageState.camera.zoom])
|
||||
|
||||
// Update the group's position when the camera moves or zooms
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import * as React from 'react'
|
||||
import { useTLContext } from './useTLContext'
|
||||
import { inputs } from '+inputs'
|
||||
|
||||
export function useCanvasEvents() {
|
||||
const { callbacks } = useTLContext()
|
||||
const { callbacks, inputs } = useTLContext()
|
||||
|
||||
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) {
|
||||
|
@ -16,11 +16,12 @@ export function useCanvasEvents() {
|
|||
callbacks.onPointerDown?.(info, e)
|
||||
}
|
||||
},
|
||||
[callbacks]
|
||||
[callbacks, inputs]
|
||||
)
|
||||
|
||||
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)
|
||||
|
@ -28,12 +29,13 @@ export function useCanvasEvents() {
|
|||
const info = inputs.pointerMove(e, 'canvas')
|
||||
callbacks.onPointerMove?.(info, e)
|
||||
},
|
||||
[callbacks]
|
||||
[callbacks, inputs]
|
||||
)
|
||||
|
||||
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')
|
||||
|
||||
|
@ -47,7 +49,7 @@ export function useCanvasEvents() {
|
|||
callbacks.onReleaseCanvas?.(info, e)
|
||||
callbacks.onPointerUp?.(info, e)
|
||||
},
|
||||
[callbacks]
|
||||
[callbacks, inputs]
|
||||
)
|
||||
|
||||
return {
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import * as React from 'react'
|
||||
import { inputs } from '+inputs'
|
||||
import { useTLContext } from './useTLContext'
|
||||
|
||||
export function useHandleEvents(id: string) {
|
||||
const { callbacks } = useTLContext()
|
||||
const { inputs, callbacks } = useTLContext()
|
||||
|
||||
const onPointerDown = React.useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
if (e.button !== 0) return
|
||||
if (!inputs.pointerIsValid(e)) return
|
||||
e.stopPropagation()
|
||||
e.currentTarget?.setPointerCapture(e.pointerId)
|
||||
|
||||
|
@ -15,12 +15,13 @@ export function useHandleEvents(id: string) {
|
|||
callbacks.onPointHandle?.(info, e)
|
||||
callbacks.onPointerDown?.(info, e)
|
||||
},
|
||||
[callbacks, id]
|
||||
[inputs, callbacks, id]
|
||||
)
|
||||
|
||||
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)
|
||||
|
@ -36,11 +37,12 @@ export function useHandleEvents(id: string) {
|
|||
}
|
||||
callbacks.onPointerUp?.(info, e)
|
||||
},
|
||||
[callbacks]
|
||||
[inputs, callbacks]
|
||||
)
|
||||
|
||||
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)
|
||||
|
@ -48,40 +50,32 @@ export function useHandleEvents(id: string) {
|
|||
const info = inputs.pointerMove(e, id)
|
||||
callbacks.onPointerMove?.(info, e)
|
||||
},
|
||||
[callbacks, id]
|
||||
[inputs, callbacks, id]
|
||||
)
|
||||
|
||||
const onPointerEnter = React.useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
if (!inputs.pointerIsValid(e)) return
|
||||
const info = inputs.pointerEnter(e, id)
|
||||
callbacks.onHoverHandle?.(info, e)
|
||||
},
|
||||
[callbacks, id]
|
||||
[inputs, callbacks, id]
|
||||
)
|
||||
|
||||
const onPointerLeave = React.useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
if (!inputs.pointerIsValid(e)) return
|
||||
const info = inputs.pointerEnter(e, id)
|
||||
callbacks.onUnhoverHandle?.(info, e)
|
||||
},
|
||||
[callbacks, id]
|
||||
[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,
|
||||
}
|
||||
}
|
||||
|
|
55
packages/core/src/hooks/useResizeObserver.ts
Normal file
55
packages/core/src/hooks/useResizeObserver.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
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()
|
||||
const rIsMounted = React.useRef(false)
|
||||
const forceUpdate = React.useReducer((x) => x + 1, 0)[1]
|
||||
|
||||
const updateOffsets = React.useCallback(() => {
|
||||
if (rIsMounted.current) {
|
||||
const rect = ref.current?.getBoundingClientRect()
|
||||
if (rect) {
|
||||
inputs.offset = [rect.left, rect.top]
|
||||
inputs.size = [rect.width, rect.height]
|
||||
forceUpdate()
|
||||
}
|
||||
}
|
||||
rIsMounted.current = true
|
||||
}, [ref, forceUpdate])
|
||||
|
||||
React.useEffect(() => {
|
||||
const debouncedUpdateOffsets = Utils.debounce(updateOffsets, 100)
|
||||
window.addEventListener('scroll', debouncedUpdateOffsets)
|
||||
window.addEventListener('resize', debouncedUpdateOffsets)
|
||||
return () => {
|
||||
window.removeEventListener('scroll', debouncedUpdateOffsets)
|
||||
window.removeEventListener('resize', debouncedUpdateOffsets)
|
||||
}
|
||||
}, [inputs])
|
||||
|
||||
React.useEffect(() => {
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
if (inputs.isPinching) {
|
||||
return
|
||||
}
|
||||
|
||||
if (entries[0].contentRect) {
|
||||
updateOffsets()
|
||||
}
|
||||
})
|
||||
|
||||
if (ref.current) {
|
||||
resizeObserver.observe(ref.current)
|
||||
}
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
}, [ref, inputs])
|
||||
|
||||
React.useEffect(() => {
|
||||
updateOffsets()
|
||||
}, [ref])
|
||||
}
|
|
@ -1,14 +1,14 @@
|
|||
import * as React from 'react'
|
||||
import { inputs } from '+inputs'
|
||||
import { Utils } from '+utils'
|
||||
import { TLContext } from '+hooks'
|
||||
|
||||
export function useShapeEvents(id: string, disable = false) {
|
||||
const { rPageState, rScreenBounds, callbacks } = React.useContext(TLContext)
|
||||
const { rPageState, rScreenBounds, callbacks, inputs } = React.useContext(TLContext)
|
||||
|
||||
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)
|
||||
|
@ -38,12 +38,13 @@ export function useShapeEvents(id: string, disable = false) {
|
|||
callbacks.onPointShape?.(info, e)
|
||||
callbacks.onPointerDown?.(info, e)
|
||||
},
|
||||
[callbacks, id, disable]
|
||||
[inputs, callbacks, id, disable]
|
||||
)
|
||||
|
||||
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()
|
||||
|
@ -60,11 +61,12 @@ export function useShapeEvents(id: string, disable = false) {
|
|||
callbacks.onReleaseShape?.(info, e)
|
||||
callbacks.onPointerUp?.(info, e)
|
||||
},
|
||||
[callbacks, id, disable]
|
||||
[inputs, callbacks, id, disable]
|
||||
)
|
||||
|
||||
const onPointerMove = React.useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
if (!inputs.pointerIsValid(e)) return
|
||||
if (disable) return
|
||||
|
||||
if (inputs.pointer && e.pointerId !== inputs.pointer.pointerId) return
|
||||
|
@ -77,42 +79,34 @@ export function useShapeEvents(id: string, disable = false) {
|
|||
|
||||
callbacks.onPointerMove?.(info, e)
|
||||
},
|
||||
[callbacks, id, disable]
|
||||
[inputs, callbacks, id, disable]
|
||||
)
|
||||
|
||||
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)
|
||||
},
|
||||
[callbacks, id, disable]
|
||||
[inputs, callbacks, id, disable]
|
||||
)
|
||||
|
||||
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)
|
||||
},
|
||||
[callbacks, id, disable]
|
||||
[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
|
||||
}
|
||||
|
|
|
@ -108,151 +108,174 @@ const tlcss = css`
|
|||
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
html,
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
.tl-container {
|
||||
--tl-zoom: 1;
|
||||
--tl-scale: calc(1 / var(--tl-zoom));
|
||||
}
|
||||
.tl-counter-scaled {
|
||||
transform: scale(var(--tl-scale));
|
||||
}
|
||||
.tl-dashed {
|
||||
stroke-dasharray: calc(2px * var(--tl-scale)), calc(2px * var(--tl-scale));
|
||||
}
|
||||
.tl-transparent {
|
||||
fill: transparent;
|
||||
stroke: transparent;
|
||||
}
|
||||
.tl-cursor-ns {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
.tl-cursor-ew {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
.tl-cursor-nesw {
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
.tl-cursor-nwse {
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
.tl-corner-handle {
|
||||
stroke: var(--tl-selectStroke);
|
||||
fill: var(--tl-background);
|
||||
stroke-width: calc(1.5px * var(--tl-scale));
|
||||
}
|
||||
.tl-rotate-handle {
|
||||
stroke: var(--tl-selectStroke);
|
||||
fill: var(--tl-background);
|
||||
stroke-width: calc(1.5px * var(--tl-scale));
|
||||
cursor: grab;
|
||||
}
|
||||
.tl-binding {
|
||||
fill: var(--tl-selectFill);
|
||||
stroke: var(--tl-selectStroke);
|
||||
stroke-width: calc(1px * var(--tl-scale));
|
||||
pointer-events: none;
|
||||
}
|
||||
.tl-selected {
|
||||
fill: transparent;
|
||||
stroke: var(--tl-selectStroke);
|
||||
stroke-width: calc(1.5px * var(--tl-scale));
|
||||
pointer-events: none;
|
||||
}
|
||||
.tl-hovered {
|
||||
fill: transparent;
|
||||
stroke: var(--tl-selectStroke);
|
||||
stroke-width: calc(1.5px * var(--tl-scale));
|
||||
pointer-events: none;
|
||||
}
|
||||
.tl-bounds-center {
|
||||
fill: transparent;
|
||||
stroke: var(--tl-selectStroke);
|
||||
stroke-width: calc(1.5px * var(--tl-scale));
|
||||
}
|
||||
.tl-bounds-bg {
|
||||
stroke: none;
|
||||
fill: var(--tl-selectFill);
|
||||
pointer-events: all;
|
||||
}
|
||||
.tl-brush {
|
||||
fill: var(--tl-brushFill);
|
||||
stroke: var(--tl-brushStroke);
|
||||
stroke-width: calc(1px * var(--tl-scale));
|
||||
pointer-events: none;
|
||||
}
|
||||
.tl-canvas {
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
touch-action: none;
|
||||
z-index: 100;
|
||||
pointer-events: all;
|
||||
}
|
||||
.tl-container {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
touch-action: none;
|
||||
overscroll-behavior: none;
|
||||
overscroll-behavior-x: none;
|
||||
background-color: var(--tl-background);
|
||||
}
|
||||
|
||||
.tl-container * {
|
||||
user-select: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tl-counter-scaled {
|
||||
transform: scale(var(--tl-scale));
|
||||
}
|
||||
|
||||
.tl-dashed {
|
||||
stroke-dasharray: calc(2px * var(--tl-scale)), calc(2px * var(--tl-scale));
|
||||
}
|
||||
|
||||
.tl-transparent {
|
||||
fill: transparent;
|
||||
stroke: transparent;
|
||||
}
|
||||
|
||||
.tl-cursor-ns {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.tl-cursor-ew {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.tl-cursor-nesw {
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.tl-cursor-nwse {
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.tl-corner-handle {
|
||||
stroke: var(--tl-selectStroke);
|
||||
fill: var(--tl-background);
|
||||
stroke-width: calc(1.5px * var(--tl-scale));
|
||||
}
|
||||
|
||||
.tl-rotate-handle {
|
||||
stroke: var(--tl-selectStroke);
|
||||
fill: var(--tl-background);
|
||||
stroke-width: calc(1.5px * var(--tl-scale));
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.tl-binding {
|
||||
fill: var(--tl-selectFill);
|
||||
stroke: var(--tl-selectStroke);
|
||||
stroke-width: calc(1px * var(--tl-scale));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tl-selected {
|
||||
fill: transparent;
|
||||
stroke: var(--tl-selectStroke);
|
||||
stroke-width: calc(1.5px * var(--tl-scale));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tl-hovered {
|
||||
fill: transparent;
|
||||
stroke: var(--tl-selectStroke);
|
||||
stroke-width: calc(1.5px * var(--tl-scale));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tl-bounds-center {
|
||||
fill: transparent;
|
||||
stroke: var(--tl-selectStroke);
|
||||
stroke-width: calc(1.5px * var(--tl-scale));
|
||||
}
|
||||
|
||||
.tl-bounds-bg {
|
||||
stroke: none;
|
||||
fill: var(--tl-selectFill);
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.tl-brush {
|
||||
fill: var(--tl-brushFill);
|
||||
stroke: var(--tl-brushStroke);
|
||||
stroke-width: calc(1px * var(--tl-scale));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tl-canvas {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
touch-action: none;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.tl-dot {
|
||||
fill: var(--tl-background);
|
||||
stroke: var(--tl-foreground);
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.tl-handles {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.tl-handles:hover > .tl-handle-bg {
|
||||
fill: var(--tl-selectFill);
|
||||
}
|
||||
|
||||
.tl-handles:hover > .tl-handle-bg > * {
|
||||
stroke: var(--tl-selectFill);
|
||||
}
|
||||
|
||||
.tl-handles:active > .tl-handle-bg {
|
||||
fill: var(--tl-selectFill);
|
||||
}
|
||||
|
||||
.tl-handles:active > .tl-handle-bg > * {
|
||||
stroke: var(--tl-selectFill);
|
||||
}
|
||||
|
||||
.tl-handle {
|
||||
fill: var(--tl-background);
|
||||
stroke: var(--tl-selectStroke);
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
.tl-handle-bg {
|
||||
fill: transparent;
|
||||
stroke: none;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.tl-binding-indicator {
|
||||
stroke-width: calc(3px * var(--tl-scale));
|
||||
fill: var(--tl-selectFill);
|
||||
stroke: var(--tl-selected);
|
||||
}
|
||||
|
||||
.tl-shape-group {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tl-shape-group > *[data-shy='true'] {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tl-shape-group:hover > *[data-shy='true'] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tl-current-parent > *[data-shy='true'] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import * as React from 'react'
|
||||
import type { Inputs } from '+inputs'
|
||||
import type { TLCallbacks, TLShape, TLBounds, TLPageState, TLShapeUtils } from '+types'
|
||||
|
||||
export interface TLContextType {
|
||||
|
@ -7,6 +8,7 @@ export interface TLContextType {
|
|||
shapeUtils: TLShapeUtils<TLShape>
|
||||
rPageState: React.MutableRefObject<TLPageState>
|
||||
rScreenBounds: React.MutableRefObject<TLBounds | null>
|
||||
inputs: Inputs
|
||||
}
|
||||
|
||||
export const TLContext = React.createContext<TLContextType>({} as TLContextType)
|
||||
|
|
|
@ -1,81 +1,99 @@
|
|||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { useRef } from 'react'
|
||||
import * as React from 'react'
|
||||
import { useTLContext } from './useTLContext'
|
||||
import { Vec } from '+utils'
|
||||
import { useWheel, usePinch } from 'react-use-gesture'
|
||||
import { inputs } from '+inputs'
|
||||
import Utils, { Vec } from '+utils'
|
||||
import { useGesture } from '@use-gesture/react'
|
||||
|
||||
// Capture zoom gestures (pinches, wheels and pans)
|
||||
export function useZoomEvents() {
|
||||
const rPinchDa = useRef<number[] | undefined>(undefined)
|
||||
const rOriginPoint = useRef<number[] | undefined>(undefined)
|
||||
const rPinchPoint = useRef<number[] | undefined>(undefined)
|
||||
export function useZoomEvents<T extends HTMLElement | SVGElement>(ref: React.RefObject<T>) {
|
||||
const rOriginPoint = React.useRef<number[] | undefined>(undefined)
|
||||
const rPinchPoint = React.useRef<number[] | undefined>(undefined)
|
||||
const rDelta = React.useRef<number[]>([0, 0])
|
||||
|
||||
const { callbacks } = useTLContext()
|
||||
const { inputs, callbacks } = useTLContext()
|
||||
|
||||
useWheel(
|
||||
({ event: e, delta }) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (Vec.isEqual(delta, [0, 0])) return
|
||||
|
||||
const info = inputs.pan(delta, e as WheelEvent)
|
||||
|
||||
callbacks.onPan?.(info, e)
|
||||
},
|
||||
{
|
||||
domTarget: typeof document === 'undefined' ? undefined : document.body,
|
||||
eventOptions: { passive: false },
|
||||
React.useEffect(() => {
|
||||
const preventGesture = (event: TouchEvent) => {
|
||||
event.preventDefault()
|
||||
}
|
||||
)
|
||||
|
||||
usePinch(
|
||||
({ pinching, da, origin, event: e }) => {
|
||||
if (!pinching) {
|
||||
// @ts-ignore
|
||||
document.addEventListener('gesturestart', preventGesture)
|
||||
// @ts-ignore
|
||||
document.addEventListener('gesturechange', preventGesture)
|
||||
|
||||
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)
|
||||
callbacks.onPinchEnd?.(
|
||||
info,
|
||||
e as React.WheelEvent<Element> | WheelEvent | React.TouchEvent<Element> | TouchEvent
|
||||
)
|
||||
rPinchDa.current = undefined
|
||||
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)
|
||||
|
||||
inputs.isPinching = false
|
||||
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) {
|
||||
const info = inputs.pinch(origin, origin)
|
||||
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 = origin
|
||||
rOriginPoint.current = origin
|
||||
}
|
||||
|
||||
if (!rPinchDa.current) throw Error('No pinch direction!')
|
||||
if (!rOriginPoint.current) throw Error('No origin point!')
|
||||
|
||||
const [distanceDelta] = Vec.sub(rPinchDa.current, da)
|
||||
|
||||
const info = inputs.pinch(rPinchPoint.current, origin)
|
||||
|
||||
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
|
||||
},
|
||||
},
|
||||
{
|
||||
domTarget: typeof document === 'undefined' ? undefined : document.body,
|
||||
target: window,
|
||||
eventOptions: { passive: false },
|
||||
}
|
||||
)
|
||||
|
|
|
@ -4,19 +4,39 @@ import { Vec, Utils } from './utils'
|
|||
|
||||
const DOUBLE_CLICK_DURATION = 250
|
||||
|
||||
class Inputs {
|
||||
export class Inputs {
|
||||
pointer?: TLPointerInfo<string>
|
||||
keyboard?: TLKeyboardInfo
|
||||
keys: Record<string, boolean> = {}
|
||||
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,
|
||||
|
@ -35,9 +55,33 @@ 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]
|
||||
|
||||
|
@ -69,7 +113,9 @@ class Inputs {
|
|||
pointerDown<T extends string>(e: PointerEvent | React.PointerEvent, target: T): TLPointerInfo<T> {
|
||||
const { shiftKey, ctrlKey, metaKey, altKey } = e
|
||||
|
||||
const point = Inputs.getPoint(e)
|
||||
const point = Inputs.getPoint(e, this.offset)
|
||||
|
||||
this.activePointer = e.pointerId
|
||||
|
||||
const info: TLPointerInfo<T> = {
|
||||
target,
|
||||
|
@ -95,7 +141,7 @@ class Inputs {
|
|||
): TLPointerInfo<T> {
|
||||
const { shiftKey, ctrlKey, metaKey, altKey } = e
|
||||
|
||||
const point = Inputs.getPoint(e)
|
||||
const point = Inputs.getPoint(e, this.offset)
|
||||
|
||||
const info: TLPointerInfo<T> = {
|
||||
target,
|
||||
|
@ -120,7 +166,7 @@ class Inputs {
|
|||
|
||||
const prev = this.pointer
|
||||
|
||||
const point = Inputs.getPoint(e)
|
||||
const point = Inputs.getPoint(e, this.offset)
|
||||
|
||||
const delta = prev?.point ? Vec.sub(point, prev.point) : [0, 0]
|
||||
|
||||
|
@ -148,10 +194,12 @@ class Inputs {
|
|||
|
||||
const prev = this.pointer
|
||||
|
||||
const point = Inputs.getPoint(e)
|
||||
const point = Inputs.getPoint(e, this.offset)
|
||||
|
||||
const delta = prev?.point ? Vec.sub(point, prev.point) : [0, 0]
|
||||
|
||||
this.activePointer = undefined
|
||||
|
||||
const info: TLPointerInfo<T> = {
|
||||
origin: point,
|
||||
...prev,
|
||||
|
@ -182,7 +230,7 @@ class Inputs {
|
|||
origin: this.pointer?.origin || [0, 0],
|
||||
delta: [0, 0],
|
||||
pressure: 0.5,
|
||||
point: Inputs.getPoint(e),
|
||||
point: Inputs.getPoint(e, this.offset),
|
||||
shiftKey,
|
||||
ctrlKey,
|
||||
metaKey,
|
||||
|
@ -203,7 +251,7 @@ class Inputs {
|
|||
|
||||
const prev = this.pointer
|
||||
|
||||
const point = Inputs.getPoint(e)
|
||||
const point = Inputs.getPoint(e, this.offset)
|
||||
|
||||
const info: TLPointerInfo<'wheel'> = {
|
||||
...prev,
|
||||
|
@ -274,16 +322,14 @@ 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.round(point),
|
||||
origin,
|
||||
delta: delta,
|
||||
point: Vec.round(point),
|
||||
point: Vec.sub(Vec.round(point), this.offset),
|
||||
pressure: 0.5,
|
||||
shiftKey,
|
||||
ctrlKey,
|
||||
|
@ -300,13 +346,18 @@ class Inputs {
|
|||
this.pointerUpTime = 0
|
||||
this.pointer = undefined
|
||||
this.keyboard = undefined
|
||||
this.activePointer = undefined
|
||||
this.keys = {}
|
||||
}
|
||||
|
||||
static getPoint(
|
||||
e: PointerEvent | React.PointerEvent | Touch | React.Touch | WheelEvent
|
||||
e: PointerEvent | React.PointerEvent | Touch | React.Touch | WheelEvent,
|
||||
offset = [0, 0]
|
||||
): number[] {
|
||||
return [Number(e.clientX.toPrecision(5)), Number(e.clientY.toPrecision(5))]
|
||||
return [
|
||||
Number(e.clientX.toPrecision(5)) - offset[0],
|
||||
Number(e.clientY.toPrecision(5)) - offset[1],
|
||||
]
|
||||
}
|
||||
|
||||
static getPressure(e: PointerEvent | React.PointerEvent | Touch | React.Touch | WheelEvent) {
|
||||
|
|
|
@ -3,6 +3,7 @@ import type { TLPageState, TLBounds } from '../types'
|
|||
import { mockDocument } from './mockDocument'
|
||||
import { mockUtils } from './mockUtils'
|
||||
import { useTLTheme, TLContext } from '../hooks'
|
||||
import { Inputs } from '+inputs'
|
||||
|
||||
export const ContextWrapper: React.FC = ({ children }) => {
|
||||
useTLTheme()
|
||||
|
@ -14,6 +15,7 @@ export const ContextWrapper: React.FC = ({ children }) => {
|
|||
shapeUtils: mockUtils,
|
||||
rScreenBounds,
|
||||
rPageState,
|
||||
inputs: new Inputs(),
|
||||
}))
|
||||
|
||||
return <TLContext.Provider value={context}>{children}</TLContext.Provider>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -3,7 +3,7 @@ import fs from 'fs'
|
|||
import esbuild from 'esbuild'
|
||||
import serve, { error, log } from 'create-serve'
|
||||
|
||||
const isDevServer = process.argv.includes('--dev')
|
||||
const isDevServer = true
|
||||
|
||||
if (!fs.existsSync('./dist')) {
|
||||
fs.mkdirSync('./dist')
|
||||
|
|
|
@ -21,17 +21,20 @@
|
|||
"@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",
|
||||
"esbuild": "^0.12.26",
|
||||
"rimraf": "3.0.2",
|
||||
"typescript": "4.2.3"
|
||||
},
|
||||
"gitHead": "a7dac0f83ad998e205c2aab58182cb4ba4e099a6"
|
||||
}
|
||||
}
|
|
@ -1,8 +1,51 @@
|
|||
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 Embedded from './embedded'
|
||||
import ChangingId from './changing-id'
|
||||
import './styles.css'
|
||||
|
||||
export default function App(): JSX.Element {
|
||||
return <Basic />
|
||||
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(() => {
|
|
@ -12,5 +12,9 @@ export default function Editor(props: TLDrawProps): JSX.Element {
|
|||
props.onMount?.(state)
|
||||
}, [])
|
||||
|
||||
return <TLDraw id="tldraw" {...props} onMount={handleMount} />
|
||||
return (
|
||||
<div className="tldraw">
|
||||
<TLDraw id="tldraw" {...props} onMount={handleMount} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
30
packages/dev/src/embedded.tsx
Normal file
30
packages/dev/src/embedded.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { TLDraw } from '@tldraw/tldraw'
|
||||
import * as React from 'react'
|
||||
|
||||
export default function Embedded(): JSX.Element {
|
||||
return (
|
||||
<div style={{ padding: '2% 10%', width: 'calc(100% - 100px)' }}>
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: 'auto',
|
||||
height: '500px',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<TLDraw id="small1" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: 'auto',
|
||||
height: '500px',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<TLDraw id="small2" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import * as React from 'react'
|
||||
import { openDB, DBSchema } from 'idb'
|
||||
import type { TLDrawDocument } from '@tldraw/tldraw'
|
||||
|
||||
const VERSION = 5
|
||||
|
||||
interface TLDatabase extends DBSchema {
|
||||
documents: {
|
||||
key: string
|
||||
value: TLDrawDocument
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist a value in indexdb. This hook is designed to be used primarily through
|
||||
* its methods, `setValue` and `forceUpdate`. The `setValue` method will update the
|
||||
* value in the database, howeever it will NOT cause the hook's component to update.
|
||||
* The `forceUpdate` method will cause the component to update with the latest value
|
||||
* in the database.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* const {status, value, setValue, forceUpdate} = usePersistence()
|
||||
*```
|
||||
*/
|
||||
export function usePersistence(id: string, doc: TLDrawDocument) {
|
||||
const [status, setStatus] = React.useState<'loading' | 'ready'>('loading')
|
||||
const [value, _setValue] = React.useState<TLDrawDocument | null>(null)
|
||||
|
||||
// A function that other parts of the program can use to manually update
|
||||
// the state to the latest value in the database.
|
||||
const forceUpdate = React.useCallback(() => {
|
||||
_setValue(null)
|
||||
setStatus('loading')
|
||||
|
||||
openDB<TLDatabase>('db', VERSION).then((db) =>
|
||||
db.get('documents', id).then((v) => {
|
||||
if (!v) throw Error(`Could not find document with id: ${id}`)
|
||||
_setValue(v)
|
||||
setStatus('ready')
|
||||
})
|
||||
)
|
||||
}, [id])
|
||||
|
||||
// A function that other parts of the program can use to manually set the
|
||||
// value in the database.
|
||||
const setValue = React.useCallback(
|
||||
(doc: TLDrawDocument) => {
|
||||
openDB<TLDatabase>('db', VERSION).then((db) => db.put('documents', doc, id))
|
||||
},
|
||||
[id]
|
||||
)
|
||||
|
||||
// Whenever the id or doc changes, save the new value to the database and update
|
||||
// the state.
|
||||
React.useEffect(() => {
|
||||
async function handleLoad() {
|
||||
const db = await openDB<TLDatabase>('db', VERSION, {
|
||||
upgrade(db, _oldVersion, newVersion) {
|
||||
if (newVersion) {
|
||||
if (db.objectStoreNames.contains('documents')) {
|
||||
db.deleteObjectStore('documents')
|
||||
}
|
||||
db.createObjectStore('documents')
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
let savedDoc: TLDrawDocument
|
||||
|
||||
try {
|
||||
const restoredDoc = await db.get('documents', id)
|
||||
if (!restoredDoc) throw Error('No document')
|
||||
savedDoc = restoredDoc
|
||||
restoredDoc.pageStates = Object.fromEntries(
|
||||
Object.entries(restoredDoc.pageStates).map(([pageId, pageState]) => [
|
||||
pageId,
|
||||
{
|
||||
...pageState,
|
||||
hoveredId: undefined,
|
||||
editingId: undefined,
|
||||
},
|
||||
])
|
||||
)
|
||||
} catch (e) {
|
||||
await db.put('documents', doc, id)
|
||||
savedDoc = doc
|
||||
}
|
||||
|
||||
_setValue(savedDoc)
|
||||
setStatus('ready')
|
||||
}
|
||||
|
||||
handleLoad()
|
||||
}, [id, doc])
|
||||
|
||||
return { value, status, setValue, forceUpdate }
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="stylesheet" href="bundle.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>tldraw</title>
|
||||
</head>
|
||||
|
|
|
@ -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')
|
||||
)
|
||||
|
|
20
packages/dev/src/styles.css
Normal file
20
packages/dev/src/styles.css
Normal file
|
@ -0,0 +1,20 @@
|
|||
html,
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
overscroll-behavior: none;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.tldraw {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
1800
packages/dev/tsconfig.tsbuildinfo
Normal file
1800
packages/dev/tsconfig.tsbuildinfo
Normal file
File diff suppressed because it is too large
Load diff
|
@ -5,7 +5,7 @@ import styled from '~styles'
|
|||
/* -------------------------------------------------- */
|
||||
|
||||
export const DialogContent = styled('div', {
|
||||
position: 'fixed',
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import * as React from 'react'
|
||||
import { render } from '@testing-library/react'
|
||||
import { act } from 'react-dom/test-utils'
|
||||
import { TLDraw } from './tldraw'
|
||||
|
||||
describe('tldraw', () => {
|
||||
|
|
|
@ -218,32 +218,19 @@ function InnerTldraw({
|
|||
)
|
||||
}
|
||||
|
||||
const Spacer = styled('div', {
|
||||
flexGrow: 2,
|
||||
})
|
||||
|
||||
const MenuButtons = styled('div', {
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
})
|
||||
|
||||
const Layout = styled('main', {
|
||||
position: 'fixed',
|
||||
const Layout = styled('div', {
|
||||
overflow: 'hidden',
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
position: 'absolute',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
padding: '8px 8px 0 8px',
|
||||
zIndex: 200,
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'flex-start',
|
||||
boxSizing: 'border-box',
|
||||
outline: 'none',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 1,
|
||||
|
||||
'& > *': {
|
||||
pointerEvents: 'all',
|
||||
|
@ -253,5 +240,17 @@ const Layout = styled('main', {
|
|||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
zIndex: 100,
|
||||
},
|
||||
})
|
||||
|
||||
const Spacer = styled('div', {
|
||||
flexGrow: 2,
|
||||
})
|
||||
|
||||
const MenuButtons = styled('div', {
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
})
|
||||
|
|
|
@ -139,7 +139,7 @@ export const ToolsPanel = React.memo((): JSX.Element => {
|
|||
})
|
||||
|
||||
const ToolsPanelContainer = styled('div', {
|
||||
position: 'fixed',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
import '@testing-library/jest-dom/extend-expect'
|
||||
import "fake-indexeddb/auto"
|
||||
import 'fake-indexeddb/auto'
|
||||
global.ResizeObserver = require('resize-observer-polyfill')
|
||||
|
|
Loading…
Reference in a new issue