moves to div renderer
This commit is contained in:
parent
bc96414bf7
commit
e7987ca451
42 changed files with 1104 additions and 852 deletions
|
@ -1,7 +1,9 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import type { TLBounds } from '+types'
|
import type { TLBounds } from '+types'
|
||||||
import { Utils } from '+utils'
|
import { useBoundsEvents, usePosition } from '+hooks'
|
||||||
import { useBoundsEvents } from '+hooks'
|
import { Container } from '+components/container'
|
||||||
|
import { SVGContainer } from '+components/svg-container'
|
||||||
|
|
||||||
interface BoundsBgProps {
|
interface BoundsBgProps {
|
||||||
bounds: TLBounds
|
bounds: TLBounds
|
||||||
|
@ -11,20 +13,11 @@ interface BoundsBgProps {
|
||||||
export function BoundsBg({ bounds, rotation }: BoundsBgProps): JSX.Element {
|
export function BoundsBg({ bounds, rotation }: BoundsBgProps): JSX.Element {
|
||||||
const events = useBoundsEvents()
|
const events = useBoundsEvents()
|
||||||
|
|
||||||
const { width, height } = bounds
|
|
||||||
|
|
||||||
const center = Utils.getBoundsCenter(bounds)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<rect
|
<Container bounds={bounds} rotation={rotation}>
|
||||||
className="tl-bounds-bg"
|
<SVGContainer>
|
||||||
width={Math.max(1, width)}
|
<rect className="tl-bounds-bg" width={bounds.width} height={bounds.height} {...events} />
|
||||||
height={Math.max(1, height)}
|
</SVGContainer>
|
||||||
transform={`
|
</Container>
|
||||||
rotate(${rotation * (180 / Math.PI)},${center})
|
|
||||||
translate(${bounds.minX},${bounds.minY})
|
|
||||||
rotate(${(bounds.rotation || 0) * (180 / Math.PI)}, 0, 0)`}
|
|
||||||
{...events}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { TLBoundsEdge, TLBoundsCorner, TLBounds } from '+types'
|
import { TLBoundsEdge, TLBoundsCorner, TLBounds } from '+types'
|
||||||
import { Utils } from '+utils'
|
|
||||||
import { CenterHandle } from './center-handle'
|
import { CenterHandle } from './center-handle'
|
||||||
import { RotateHandle } from './rotate-handle'
|
import { RotateHandle } from './rotate-handle'
|
||||||
import { CornerHandle } from './corner-handle'
|
import { CornerHandle } from './corner-handle'
|
||||||
import { EdgeHandle } from './edge-handle'
|
import { EdgeHandle } from './edge-handle'
|
||||||
|
import { usePosition } from '+hooks'
|
||||||
|
import { Container } from '+components/container'
|
||||||
|
import { SVGContainer } from '+components/svg-container'
|
||||||
|
|
||||||
interface BoundsProps {
|
interface BoundsProps {
|
||||||
zoom: number
|
zoom: number
|
||||||
|
@ -14,6 +17,27 @@ interface BoundsProps {
|
||||||
viewportWidth: number
|
viewportWidth: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// function setTransform(elm: SVGSVGElement, padding: number, bounds: TLBounds, rotation: number) {
|
||||||
|
// const center = Utils.getBoundsCenter(bounds)
|
||||||
|
// const transform = `
|
||||||
|
// rotate(${rotation * (180 / Math.PI)},${center})
|
||||||
|
// translate(${bounds.minX - padding},${bounds.minY - padding})
|
||||||
|
// rotate(${(bounds.rotation || 0) * (180 / Math.PI)},0,0)`
|
||||||
|
// elm.setAttribute('transform', transform)
|
||||||
|
// elm.setAttribute('width', bounds.width + padding * 2 + 'px')
|
||||||
|
// elm.setAttribute('height', bounds.height + padding * 2 + 'px')
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function setTransform(elm: HTMLDivElement, bounds: TLBounds, rotation = 0) {
|
||||||
|
// const transform = `
|
||||||
|
// translate(calc(${bounds.minX}px - var(--tl-padding)),calc(${bounds.minY}px - var(--tl-padding)))
|
||||||
|
// rotate(${rotation + (bounds.rotation || 0)}rad)
|
||||||
|
// `
|
||||||
|
// elm.style.setProperty('transform', transform)
|
||||||
|
// elm.style.setProperty('width', `calc(${bounds.width}px + (var(--tl-padding) * 2))`)
|
||||||
|
// elm.style.setProperty('height', `calc(${bounds.height}px + (var(--tl-padding) * 2))`)
|
||||||
|
// }
|
||||||
|
|
||||||
export function Bounds({
|
export function Bounds({
|
||||||
zoom,
|
zoom,
|
||||||
bounds,
|
bounds,
|
||||||
|
@ -23,65 +47,65 @@ export function Bounds({
|
||||||
}: BoundsProps): JSX.Element {
|
}: BoundsProps): JSX.Element {
|
||||||
const targetSize = (viewportWidth < 768 ? 16 : 8) / zoom // Touch target size
|
const targetSize = (viewportWidth < 768 ? 16 : 8) / zoom // Touch target size
|
||||||
const size = 8 / zoom // Touch target size
|
const size = 8 / zoom // Touch target size
|
||||||
const center = Utils.getBoundsCenter(bounds)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g
|
<Container className="tl-bounds" bounds={bounds} rotation={rotation}>
|
||||||
pointerEvents="all"
|
<SVGContainer>
|
||||||
transform={`
|
<CenterHandle bounds={bounds} isLocked={isLocked} />
|
||||||
rotate(${rotation * (180 / Math.PI)},${center})
|
{!isLocked && (
|
||||||
translate(${bounds.minX},${bounds.minY})
|
<>
|
||||||
rotate(${(bounds.rotation || 0) * (180 / Math.PI)}, 0, 0)`}
|
<EdgeHandle
|
||||||
>
|
targetSize={targetSize}
|
||||||
<CenterHandle bounds={bounds} isLocked={isLocked} />
|
size={size}
|
||||||
{!isLocked && (
|
bounds={bounds}
|
||||||
<>
|
edge={TLBoundsEdge.Top}
|
||||||
<EdgeHandle targetSize={targetSize} size={size} bounds={bounds} edge={TLBoundsEdge.Top} />
|
/>
|
||||||
<EdgeHandle
|
<EdgeHandle
|
||||||
targetSize={targetSize}
|
targetSize={targetSize}
|
||||||
size={size}
|
size={size}
|
||||||
bounds={bounds}
|
bounds={bounds}
|
||||||
edge={TLBoundsEdge.Right}
|
edge={TLBoundsEdge.Right}
|
||||||
/>
|
/>
|
||||||
<EdgeHandle
|
<EdgeHandle
|
||||||
targetSize={targetSize}
|
targetSize={targetSize}
|
||||||
size={size}
|
size={size}
|
||||||
bounds={bounds}
|
bounds={bounds}
|
||||||
edge={TLBoundsEdge.Bottom}
|
edge={TLBoundsEdge.Bottom}
|
||||||
/>
|
/>
|
||||||
<EdgeHandle
|
<EdgeHandle
|
||||||
targetSize={targetSize}
|
targetSize={targetSize}
|
||||||
size={size}
|
size={size}
|
||||||
bounds={bounds}
|
bounds={bounds}
|
||||||
edge={TLBoundsEdge.Left}
|
edge={TLBoundsEdge.Left}
|
||||||
/>
|
/>
|
||||||
<CornerHandle
|
<CornerHandle
|
||||||
targetSize={targetSize}
|
targetSize={targetSize}
|
||||||
size={size}
|
size={size}
|
||||||
bounds={bounds}
|
bounds={bounds}
|
||||||
corner={TLBoundsCorner.TopLeft}
|
corner={TLBoundsCorner.TopLeft}
|
||||||
/>
|
/>
|
||||||
<CornerHandle
|
<CornerHandle
|
||||||
targetSize={targetSize}
|
targetSize={targetSize}
|
||||||
size={size}
|
size={size}
|
||||||
bounds={bounds}
|
bounds={bounds}
|
||||||
corner={TLBoundsCorner.TopRight}
|
corner={TLBoundsCorner.TopRight}
|
||||||
/>
|
/>
|
||||||
<CornerHandle
|
<CornerHandle
|
||||||
targetSize={targetSize}
|
targetSize={targetSize}
|
||||||
size={size}
|
size={size}
|
||||||
bounds={bounds}
|
bounds={bounds}
|
||||||
corner={TLBoundsCorner.BottomRight}
|
corner={TLBoundsCorner.BottomRight}
|
||||||
/>
|
/>
|
||||||
<CornerHandle
|
<CornerHandle
|
||||||
targetSize={targetSize}
|
targetSize={targetSize}
|
||||||
size={size}
|
size={size}
|
||||||
bounds={bounds}
|
bounds={bounds}
|
||||||
corner={TLBoundsCorner.BottomLeft}
|
corner={TLBoundsCorner.BottomLeft}
|
||||||
/>
|
/>
|
||||||
<RotateHandle targetSize={targetSize} size={size} bounds={bounds} />
|
<RotateHandle targetSize={targetSize} size={size} bounds={bounds} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</g>
|
</SVGContainer>
|
||||||
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as React from 'react'
|
||||||
import type { TLBounds } from '+types'
|
import type { TLBounds } from '+types'
|
||||||
|
|
||||||
export class BrushUpdater {
|
export class BrushUpdater {
|
||||||
ref = React.createRef<SVGRectElement>()
|
ref = React.createRef<SVGSVGElement>()
|
||||||
|
|
||||||
isControlled = false
|
isControlled = false
|
||||||
|
|
||||||
|
@ -18,8 +18,7 @@ export class BrushUpdater {
|
||||||
if (!elm) return
|
if (!elm) return
|
||||||
|
|
||||||
elm.setAttribute('opacity', '1')
|
elm.setAttribute('opacity', '1')
|
||||||
elm.setAttribute('x', bounds.minX.toString())
|
elm.setAttribute('transform', `translate(${bounds.minX.toString()}, ${bounds.minY.toString()})`)
|
||||||
elm.setAttribute('y', bounds.minY.toString())
|
|
||||||
elm.setAttribute('width', bounds.width.toString())
|
elm.setAttribute('width', bounds.width.toString())
|
||||||
elm.setAttribute('height', bounds.height.toString())
|
elm.setAttribute('height', bounds.height.toString())
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,5 +4,9 @@ import { BrushUpdater } from './BrushUpdater'
|
||||||
export const brushUpdater = new BrushUpdater()
|
export const brushUpdater = new BrushUpdater()
|
||||||
|
|
||||||
export const Brush = React.memo((): JSX.Element | null => {
|
export const Brush = React.memo((): JSX.Element | null => {
|
||||||
return <rect ref={brushUpdater.ref} className="tl-brush" x={0} y={0} width={0} height={0} />
|
return (
|
||||||
|
<svg ref={brushUpdater.ref} opacity={0}>
|
||||||
|
<rect className="tl-brush" x={0} y={0} width="100%" height="100%" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import {
|
import {
|
||||||
usePreventNavigation,
|
usePreventNavigation,
|
||||||
|
@ -18,24 +19,24 @@ function resetError() {
|
||||||
void null
|
void null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CanvasProps<T extends TLShape> {
|
interface CanvasProps<T extends TLShape, M extends Record<string, unknown>> {
|
||||||
page: TLPage<T, TLBinding>
|
page: TLPage<T, TLBinding>
|
||||||
pageState: TLPageState
|
pageState: TLPageState
|
||||||
hideBounds?: boolean
|
hideBounds?: boolean
|
||||||
hideHandles?: boolean
|
hideHandles?: boolean
|
||||||
hideIndicators?: boolean
|
hideIndicators?: boolean
|
||||||
meta?: Record<string, unknown>
|
meta?: M
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Canvas<T extends TLShape>({
|
export function Canvas<T extends TLShape, M extends Record<string, unknown>>({
|
||||||
page,
|
page,
|
||||||
pageState,
|
pageState,
|
||||||
meta,
|
meta,
|
||||||
hideHandles = false,
|
hideHandles = false,
|
||||||
hideBounds = false,
|
hideBounds = false,
|
||||||
hideIndicators = false,
|
hideIndicators = false,
|
||||||
}: CanvasProps<T>): JSX.Element {
|
}: CanvasProps<T, M>): JSX.Element {
|
||||||
const rCanvas = React.useRef<SVGSVGElement>(null)
|
const rCanvas = React.useRef<HTMLDivElement>(null)
|
||||||
const rContainer = React.useRef<HTMLDivElement>(null)
|
const rContainer = React.useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
useResizeObserver(rCanvas)
|
useResizeObserver(rCanvas)
|
||||||
|
@ -48,14 +49,14 @@ export function Canvas<T extends TLShape>({
|
||||||
|
|
||||||
const events = useCanvasEvents()
|
const events = useCanvasEvents()
|
||||||
|
|
||||||
const rGroup = useCameraCss(rContainer, pageState)
|
const rLayer = useCameraCss(rContainer, pageState)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tl-container" ref={rContainer}>
|
<div className="tl-container" ref={rContainer}>
|
||||||
<svg id="canvas" className="tl-canvas" ref={rCanvas} {...events}>
|
<div id="canvas" className="tl-absolute tl-canvas" ref={rCanvas} {...events}>
|
||||||
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={resetError}>
|
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={resetError}>
|
||||||
<Defs zoom={pageState.camera.zoom} />
|
{/* <Defs zoom={pageState.camera.zoom} /> */}
|
||||||
<g ref={rGroup} id="tl-shapes">
|
<div ref={rLayer} className="tl-absolute tl-layer">
|
||||||
<Page
|
<Page
|
||||||
page={page}
|
page={page}
|
||||||
pageState={pageState}
|
pageState={pageState}
|
||||||
|
@ -65,9 +66,9 @@ export function Canvas<T extends TLShape>({
|
||||||
meta={meta}
|
meta={meta}
|
||||||
/>
|
/>
|
||||||
<Brush />
|
<Brush />
|
||||||
</g>
|
</div>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</svg>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
23
packages/core/src/components/container/container.tsx
Normal file
23
packages/core/src/components/container/container.tsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
import type { TLBounds } from '+types'
|
||||||
|
import { usePosition } from '+hooks'
|
||||||
|
|
||||||
|
interface ContainerProps {
|
||||||
|
bounds: TLBounds
|
||||||
|
rotation?: number
|
||||||
|
id?: string
|
||||||
|
className?: string
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Container = React.memo(
|
||||||
|
({ id, bounds, rotation = 0, className, children }: ContainerProps) => {
|
||||||
|
const rBounds = usePosition(bounds, rotation)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id={id} ref={rBounds} className={className + ' tl-positioned'}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
1
packages/core/src/components/container/index.ts
Normal file
1
packages/core/src/components/container/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './container'
|
|
@ -1,24 +1,41 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useHandleEvents } from '+hooks'
|
import { useHandleEvents } from '+hooks'
|
||||||
|
import { Container } from '+components/container'
|
||||||
|
import Utils from '+utils'
|
||||||
|
import { SVGContainer } from '+components/svg-container'
|
||||||
|
|
||||||
interface HandleProps {
|
interface HandleProps {
|
||||||
id: string
|
id: string
|
||||||
point: number[]
|
point: number[]
|
||||||
zoom: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Handle = React.memo(({ id, point, zoom }: HandleProps) => {
|
export const Handle = React.memo(({ id, point }: HandleProps) => {
|
||||||
const events = useHandleEvents(id)
|
const events = useHandleEvents(id)
|
||||||
|
|
||||||
|
const bounds = React.useMemo(
|
||||||
|
() =>
|
||||||
|
Utils.translateBounds(
|
||||||
|
{
|
||||||
|
minX: 0,
|
||||||
|
minY: 0,
|
||||||
|
maxX: 32,
|
||||||
|
maxY: 32,
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
},
|
||||||
|
point
|
||||||
|
),
|
||||||
|
[point]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g className="tl-handles" transform={`translate(${point})`} {...events}>
|
<Container bounds={bounds}>
|
||||||
<circle
|
<SVGContainer>
|
||||||
id="handle-bg"
|
<g className="tl-handles" {...events}>
|
||||||
className="tl-handle-bg"
|
<circle className="tl-handle-bg" pointerEvents="all" />
|
||||||
pointerEvents="all"
|
<circle className="tl-counter-scaled tl-handle" pointerEvents="none" r={4} />
|
||||||
r={20 / Math.max(1, zoom)}
|
</g>
|
||||||
/>
|
</SVGContainer>
|
||||||
<use href="#handle" />
|
</Container>
|
||||||
</g>
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,6 +4,6 @@ import { Handles } from './handles'
|
||||||
|
|
||||||
describe('handles', () => {
|
describe('handles', () => {
|
||||||
test('mounts component without crashing', () => {
|
test('mounts component without crashing', () => {
|
||||||
renderWithContext(<Handles zoom={1} shape={mockUtils.box.create({})} />)
|
renderWithContext(<Handles shape={mockUtils.box.create({ id: 'box' })} />)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,35 +1,26 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Vec } from '+utils'
|
import { Vec } from '+utils'
|
||||||
import type { TLShape } from '+types'
|
import type { TLShape } from '+types'
|
||||||
import { useTLContext } from '+hooks'
|
|
||||||
import { Handle } from './handle'
|
import { Handle } from './handle'
|
||||||
|
|
||||||
interface HandlesProps {
|
interface HandlesProps {
|
||||||
shape: TLShape
|
shape: TLShape
|
||||||
zoom: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toAngle = 180 / Math.PI
|
export const Handles = React.memo(({ shape }: HandlesProps): JSX.Element | null => {
|
||||||
|
|
||||||
export const Handles = React.memo(({ shape, zoom }: HandlesProps): JSX.Element | null => {
|
|
||||||
const { shapeUtils } = useTLContext()
|
|
||||||
|
|
||||||
const center = shapeUtils[shape.type].getCenter(shape)
|
|
||||||
|
|
||||||
if (shape.handles === undefined) {
|
if (shape.handles === undefined) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g transform={`rotate(${(shape.rotation || 0) * toAngle},${center})`}>
|
<>
|
||||||
{Object.values(shape.handles).map((handle) => (
|
{Object.values(shape.handles).map((handle) => (
|
||||||
<Handle
|
<Handle
|
||||||
key={shape.id + '_' + handle.id}
|
key={shape.id + '_' + handle.id}
|
||||||
id={handle.id}
|
id={handle.id}
|
||||||
point={Vec.add(handle.point, shape.point)}
|
point={Vec.add(handle.point, shape.point)}
|
||||||
zoom={zoom}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</g>
|
</>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import type { TLBinding, TLPage, TLPageState, TLShape } from '+types'
|
import type { TLBinding, TLPage, TLPageState, TLShape } from '+types'
|
||||||
import { useSelection, useShapeTree, useHandles, useRenderOnResize, useTLContext } from '+hooks'
|
import { useSelection, useShapeTree, useHandles, useRenderOnResize, useTLContext } from '+hooks'
|
||||||
|
@ -7,26 +8,26 @@ import { Handles } from '+components/handles'
|
||||||
import { ShapeNode } from '+components/shape'
|
import { ShapeNode } from '+components/shape'
|
||||||
import { ShapeIndicator } from '+components/shape-indicator'
|
import { ShapeIndicator } from '+components/shape-indicator'
|
||||||
|
|
||||||
interface PageProps<T extends TLShape> {
|
interface PageProps<T extends TLShape, M extends Record<string, unknown>> {
|
||||||
page: TLPage<T, TLBinding>
|
page: TLPage<T, TLBinding>
|
||||||
pageState: TLPageState
|
pageState: TLPageState
|
||||||
hideBounds: boolean
|
hideBounds: boolean
|
||||||
hideHandles: boolean
|
hideHandles: boolean
|
||||||
hideIndicators: boolean
|
hideIndicators: boolean
|
||||||
meta?: Record<string, unknown>
|
meta?: M
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Page component renders the current page.
|
* The Page component renders the current page.
|
||||||
*/
|
*/
|
||||||
export function Page<T extends TLShape>({
|
export function Page<T extends TLShape, M extends Record<string, unknown>>({
|
||||||
page,
|
page,
|
||||||
pageState,
|
pageState,
|
||||||
hideBounds,
|
hideBounds,
|
||||||
hideHandles,
|
hideHandles,
|
||||||
hideIndicators,
|
hideIndicators,
|
||||||
meta,
|
meta,
|
||||||
}: PageProps<T>): JSX.Element {
|
}: PageProps<T, M>): JSX.Element {
|
||||||
const { callbacks, shapeUtils, inputs } = useTLContext()
|
const { callbacks, shapeUtils, inputs } = useTLContext()
|
||||||
|
|
||||||
const shapeTree = useShapeTree(page, pageState, shapeUtils, inputs.size, meta, callbacks.onChange)
|
const shapeTree = useShapeTree(page, pageState, shapeUtils, inputs.size, meta, callbacks.onChange)
|
||||||
|
@ -69,7 +70,7 @@ export function Page<T extends TLShape>({
|
||||||
variant="hovered"
|
variant="hovered"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!hideHandles && shapeWithHandles && <Handles shape={shapeWithHandles} zoom={zoom} />}
|
{!hideHandles && shapeWithHandles && <Handles shape={shapeWithHandles} />}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,12 +13,15 @@ import { Canvas } from '../canvas'
|
||||||
import { Inputs } from '../../inputs'
|
import { Inputs } from '../../inputs'
|
||||||
import { useTLTheme, TLContext, TLContextType } from '../../hooks'
|
import { useTLTheme, TLContext, TLContextType } from '../../hooks'
|
||||||
|
|
||||||
export interface RendererProps<T extends TLShape, M extends Record<string, unknown>>
|
export interface RendererProps<
|
||||||
extends Partial<TLCallbacks> {
|
T extends TLShape,
|
||||||
|
E extends HTMLElement | SVGElement,
|
||||||
|
M extends Record<string, unknown>
|
||||||
|
> extends Partial<TLCallbacks> {
|
||||||
/**
|
/**
|
||||||
* An object containing instances of your shape classes.
|
* An object containing instances of your shape classes.
|
||||||
*/
|
*/
|
||||||
shapeUtils: TLShapeUtils<T>
|
shapeUtils: TLShapeUtils<T, E>
|
||||||
/**
|
/**
|
||||||
* The current page, containing shapes and bindings.
|
* The current page, containing shapes and bindings.
|
||||||
*/
|
*/
|
||||||
|
@ -63,7 +66,11 @@ export interface RendererProps<T extends TLShape, M extends Record<string, unkno
|
||||||
* @param props
|
* @param props
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function Renderer<T extends TLShape, M extends Record<string, unknown>>({
|
export function Renderer<
|
||||||
|
T extends TLShape,
|
||||||
|
E extends SVGElement | HTMLElement,
|
||||||
|
M extends Record<string, unknown>
|
||||||
|
>({
|
||||||
shapeUtils,
|
shapeUtils,
|
||||||
page,
|
page,
|
||||||
pageState,
|
pageState,
|
||||||
|
@ -73,17 +80,18 @@ export function Renderer<T extends TLShape, M extends Record<string, unknown>>({
|
||||||
hideIndicators = false,
|
hideIndicators = false,
|
||||||
hideBounds = false,
|
hideBounds = false,
|
||||||
...rest
|
...rest
|
||||||
}: RendererProps<T, M>): JSX.Element {
|
}: RendererProps<T, E, M>): JSX.Element {
|
||||||
useTLTheme(theme)
|
useTLTheme(theme)
|
||||||
|
|
||||||
const rScreenBounds = React.useRef<TLBounds>(null)
|
const rScreenBounds = React.useRef<TLBounds>(null)
|
||||||
|
|
||||||
const rPageState = React.useRef<TLPageState>(pageState)
|
const rPageState = React.useRef<TLPageState>(pageState)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
rPageState.current = pageState
|
rPageState.current = pageState
|
||||||
}, [pageState])
|
}, [pageState])
|
||||||
|
|
||||||
const [context] = React.useState<TLContextType>(() => ({
|
const [context] = React.useState<TLContextType<T, E>>(() => ({
|
||||||
callbacks: rest,
|
callbacks: rest,
|
||||||
shapeUtils,
|
shapeUtils,
|
||||||
rScreenBounds,
|
rScreenBounds,
|
||||||
|
@ -92,7 +100,7 @@ export function Renderer<T extends TLShape, M extends Record<string, unknown>>({
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TLContext.Provider value={context}>
|
<TLContext.Provider value={context as TLContextType<T, E>}>
|
||||||
<Canvas
|
<Canvas
|
||||||
page={page}
|
page={page}
|
||||||
pageState={pageState}
|
pageState={pageState}
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { ShapeIndicator } from './shape-indicator'
|
||||||
|
|
||||||
describe('shape indicator', () => {
|
describe('shape indicator', () => {
|
||||||
test('mounts component without crashing', () => {
|
test('mounts component without crashing', () => {
|
||||||
renderWithSvg(<ShapeIndicator shape={mockUtils.box.create({})} variant={'selected'} />)
|
renderWithSvg(
|
||||||
|
<ShapeIndicator shape={mockUtils.box.create({ id: 'box1' })} variant={'selected'} />
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,20 +1,25 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import type { TLShape } from '+types'
|
import type { TLShape } from '+types'
|
||||||
import { useTLContext } from '+hooks'
|
import { usePosition, useTLContext } from '+hooks'
|
||||||
|
|
||||||
export const ShapeIndicator = React.memo(
|
export const ShapeIndicator = React.memo(
|
||||||
({ shape, variant }: { shape: TLShape; variant: 'selected' | 'hovered' }) => {
|
({ shape, variant }: { shape: TLShape; variant: 'selected' | 'hovered' }) => {
|
||||||
const { shapeUtils } = useTLContext()
|
const { shapeUtils } = useTLContext()
|
||||||
const utils = shapeUtils[shape.type]
|
const utils = shapeUtils[shape.type]
|
||||||
|
const bounds = utils.getBounds(shape)
|
||||||
const center = utils.getCenter(shape)
|
const rBounds = usePosition(bounds, shape.rotation)
|
||||||
const rotation = (shape.rotation || 0) * (180 / Math.PI)
|
|
||||||
const transform = `rotate(${rotation}, ${center}) translate(${shape.point})`
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g className={variant === 'selected' ? 'tl-selected' : 'tl-hovered'} transform={transform}>
|
<div
|
||||||
{shapeUtils[shape.type].renderIndicator(shape)}
|
ref={rBounds}
|
||||||
</g>
|
className={
|
||||||
|
'tl-indicator tl-absolute ' + (variant === 'selected' ? 'tl-selected' : 'tl-hovered')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<svg width="100%" height="100%">
|
||||||
|
<g className="tl-centered-g">{utils.renderIndicator(shape)}</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,13 +2,15 @@ import { useTLContext } from '+hooks'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import type { TLShapeUtil, TLRenderInfo, TLShape } from '+types'
|
import type { TLShapeUtil, TLRenderInfo, TLShape } from '+types'
|
||||||
|
|
||||||
interface EditingShapeProps<T extends TLShape> extends TLRenderInfo {
|
interface EditingShapeProps<T extends TLShape, E extends HTMLElement | SVGElement>
|
||||||
|
extends TLRenderInfo {
|
||||||
shape: T
|
shape: T
|
||||||
utils: TLShapeUtil<T>
|
utils: TLShapeUtil<T, E>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EditingTextShape({
|
export function EditingTextShape<T extends TLShape, E extends HTMLElement | SVGElement>({
|
||||||
shape,
|
shape,
|
||||||
|
events,
|
||||||
utils,
|
utils,
|
||||||
isEditing,
|
isEditing,
|
||||||
isBinding,
|
isBinding,
|
||||||
|
@ -16,12 +18,12 @@ export function EditingTextShape({
|
||||||
isSelected,
|
isSelected,
|
||||||
isCurrentParent,
|
isCurrentParent,
|
||||||
meta,
|
meta,
|
||||||
}: EditingShapeProps<TLShape>) {
|
}: EditingShapeProps<T, E>) {
|
||||||
const {
|
const {
|
||||||
callbacks: { onTextChange, onTextBlur, onTextFocus, onTextKeyDown, onTextKeyUp },
|
callbacks: { onTextChange, onTextBlur, onTextFocus, onTextKeyDown, onTextKeyUp },
|
||||||
} = useTLContext()
|
} = useTLContext()
|
||||||
|
|
||||||
const ref = React.useRef<HTMLElement>(null)
|
const ref = React.useRef<E>(null)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// Firefox fix?
|
// Firefox fix?
|
||||||
|
@ -32,18 +34,22 @@ export function EditingTextShape({
|
||||||
}, 0)
|
}, 0)
|
||||||
}, [shape.id])
|
}, [shape.id])
|
||||||
|
|
||||||
return utils.render(shape, {
|
return utils.render({
|
||||||
ref,
|
ref,
|
||||||
|
shape,
|
||||||
isEditing,
|
isEditing,
|
||||||
isHovered,
|
isHovered,
|
||||||
isSelected,
|
isSelected,
|
||||||
isCurrentParent,
|
isCurrentParent,
|
||||||
isBinding,
|
isBinding,
|
||||||
onTextChange,
|
events: {
|
||||||
onTextBlur,
|
...events,
|
||||||
onTextFocus,
|
onTextChange,
|
||||||
onTextKeyDown,
|
onTextBlur,
|
||||||
onTextKeyUp,
|
onTextFocus,
|
||||||
|
onTextKeyDown,
|
||||||
|
onTextKeyUp,
|
||||||
|
},
|
||||||
meta,
|
meta,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import type { TLShapeUtil, TLRenderInfo, TLShape } from '+types'
|
import type { TLShapeUtil, TLRenderInfo, TLShape } from '+types'
|
||||||
|
|
||||||
interface RenderedShapeProps<T extends TLShape> extends TLRenderInfo {
|
|
||||||
shape: T
|
|
||||||
utils: TLShapeUtil<T>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RenderedShape = React.memo(
|
export const RenderedShape = React.memo(
|
||||||
function RenderedShape({
|
<T extends TLShape, E extends SVGElement | HTMLElement, M extends Record<string, unknown>>({
|
||||||
shape,
|
shape,
|
||||||
utils,
|
utils,
|
||||||
isEditing,
|
isEditing,
|
||||||
|
@ -15,16 +11,29 @@ export const RenderedShape = React.memo(
|
||||||
isHovered,
|
isHovered,
|
||||||
isSelected,
|
isSelected,
|
||||||
isCurrentParent,
|
isCurrentParent,
|
||||||
|
events,
|
||||||
meta,
|
meta,
|
||||||
}: RenderedShapeProps<TLShape>) {
|
}: TLRenderInfo<M, E> & {
|
||||||
return utils.render(shape, {
|
shape: T
|
||||||
isEditing,
|
utils: TLShapeUtil<T, E>
|
||||||
isBinding,
|
}) => {
|
||||||
isHovered,
|
const ref = utils.getRef(shape)
|
||||||
isSelected,
|
|
||||||
isCurrentParent,
|
return (
|
||||||
meta,
|
<utils.render
|
||||||
})
|
ref={ref}
|
||||||
|
{...{
|
||||||
|
shape,
|
||||||
|
isEditing,
|
||||||
|
isBinding,
|
||||||
|
isHovered,
|
||||||
|
isSelected,
|
||||||
|
isCurrentParent,
|
||||||
|
meta,
|
||||||
|
events,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
(prev, next) => {
|
(prev, next) => {
|
||||||
// If these have changed, then definitely render
|
// If these have changed, then definitely render
|
||||||
|
|
|
@ -3,7 +3,7 @@ import type { IShapeTreeNode, TLShape, TLShapeUtils } from '+types'
|
||||||
import { Shape } from './shape'
|
import { Shape } from './shape'
|
||||||
|
|
||||||
export const ShapeNode = React.memo(
|
export const ShapeNode = React.memo(
|
||||||
<M extends Record<string, unknown>>({
|
({
|
||||||
shape,
|
shape,
|
||||||
utils,
|
utils,
|
||||||
children,
|
children,
|
||||||
|
@ -13,7 +13,7 @@ export const ShapeNode = React.memo(
|
||||||
isSelected,
|
isSelected,
|
||||||
isCurrentParent,
|
isCurrentParent,
|
||||||
meta,
|
meta,
|
||||||
}: { utils: TLShapeUtils<TLShape> } & IShapeTreeNode<M>) => {
|
}: { utils: TLShapeUtils<TLShape, HTMLElement | SVGElement> } & IShapeTreeNode<TLShape, any>) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Shape
|
<Shape
|
||||||
|
|
|
@ -6,7 +6,7 @@ describe('shape', () => {
|
||||||
test('mounts component without crashing', () => {
|
test('mounts component without crashing', () => {
|
||||||
renderWithSvg(
|
renderWithSvg(
|
||||||
<Shape
|
<Shape
|
||||||
shape={mockUtils.box.create({})}
|
shape={mockUtils.box.create({ id: 'box' })}
|
||||||
utils={mockUtils[mockUtils.box.type]}
|
utils={mockUtils[mockUtils.box.type]}
|
||||||
isEditing={false}
|
isEditing={false}
|
||||||
isBinding={false}
|
isBinding={false}
|
||||||
|
@ -17,3 +17,6 @@ describe('shape', () => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// { shape: TLShape; ref: ForwardedRef<HTMLElement | SVGElement>; } & TLRenderInfo<any, any> & RefAttributes<HTMLElement | SVGElement>
|
||||||
|
// { shape: BoxShape; ref: ForwardedRef<any>; } & TLRenderInfo<any, any> & RefAttributes<any>'
|
||||||
|
|
|
@ -1,10 +1,27 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useShapeEvents } from '+hooks'
|
import { usePosition, useShapeEvents } from '+hooks'
|
||||||
import type { IShapeTreeNode, TLShape, TLShapeUtil } from '+types'
|
import type { IShapeTreeNode, TLBounds, TLShape, TLShapeUtil } from '+types'
|
||||||
import { RenderedShape } from './rendered-shape'
|
import { RenderedShape } from './rendered-shape'
|
||||||
import { EditingTextShape } from './editing-text-shape'
|
import { EditingTextShape } from './editing-text-shape'
|
||||||
|
import { Container } from '+components/container'
|
||||||
|
import { SVGContainer } from '+components/svg-container'
|
||||||
|
|
||||||
export const Shape = <M extends Record<string, unknown>>({
|
// function setTransform(elm: HTMLDivElement, bounds: TLBounds, rotation = 0) {
|
||||||
|
// const transform = `
|
||||||
|
// translate(calc(${bounds.minX}px - var(--tl-padding)),calc(${bounds.minY}px - var(--tl-padding)))
|
||||||
|
// rotate(${rotation + (bounds.rotation || 0)}rad)
|
||||||
|
// `
|
||||||
|
// elm.style.setProperty('transform', transform)
|
||||||
|
// elm.style.setProperty('width', `calc(${bounds.width}px + (var(--tl-padding) * 2))`)
|
||||||
|
// elm.style.setProperty('height', `calc(${bounds.height}px + (var(--tl-padding) * 2))`)
|
||||||
|
// }
|
||||||
|
|
||||||
|
export const Shape = <
|
||||||
|
T extends TLShape,
|
||||||
|
E extends SVGElement | HTMLElement,
|
||||||
|
M extends Record<string, unknown>
|
||||||
|
>({
|
||||||
shape,
|
shape,
|
||||||
utils,
|
utils,
|
||||||
isEditing,
|
isEditing,
|
||||||
|
@ -13,42 +30,46 @@ export const Shape = <M extends Record<string, unknown>>({
|
||||||
isSelected,
|
isSelected,
|
||||||
isCurrentParent,
|
isCurrentParent,
|
||||||
meta,
|
meta,
|
||||||
}: { utils: TLShapeUtil<TLShape> } & IShapeTreeNode<M>) => {
|
}: IShapeTreeNode<T, M> & {
|
||||||
|
utils: TLShapeUtil<T, E>
|
||||||
|
}) => {
|
||||||
|
const bounds = utils.getBounds(shape)
|
||||||
const events = useShapeEvents(shape.id, isCurrentParent)
|
const events = useShapeEvents(shape.id, isCurrentParent)
|
||||||
const center = utils.getCenter(shape)
|
|
||||||
const rotation = (shape.rotation || 0) * (180 / Math.PI)
|
|
||||||
const transform = `rotate(${rotation}, ${center}) translate(${shape.point})`
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g
|
<Container
|
||||||
className={isCurrentParent ? 'tl-shape-group tl-current-parent' : 'tl-shape-group'}
|
|
||||||
id={shape.id}
|
id={shape.id}
|
||||||
transform={transform}
|
className={'tl-shape' + (isCurrentParent ? 'tl-current-parent' : '')}
|
||||||
{...events}
|
bounds={bounds}
|
||||||
|
rotation={shape.rotation}
|
||||||
>
|
>
|
||||||
{isEditing && utils.isEditableText ? (
|
<SVGContainer>
|
||||||
<EditingTextShape
|
{isEditing && utils.isEditableText ? (
|
||||||
shape={shape}
|
<EditingTextShape
|
||||||
isBinding={false}
|
shape={shape}
|
||||||
isCurrentParent={false}
|
isBinding={false}
|
||||||
isEditing={true}
|
isCurrentParent={false}
|
||||||
isHovered={isHovered}
|
isEditing={true}
|
||||||
isSelected={isSelected}
|
isHovered={isHovered}
|
||||||
utils={utils}
|
isSelected={isSelected}
|
||||||
meta={meta}
|
utils={utils}
|
||||||
/>
|
meta={meta}
|
||||||
) : (
|
events={events}
|
||||||
<RenderedShape
|
/>
|
||||||
shape={shape}
|
) : (
|
||||||
utils={utils}
|
<RenderedShape
|
||||||
isBinding={isBinding}
|
shape={shape}
|
||||||
isCurrentParent={isCurrentParent}
|
utils={utils as any}
|
||||||
isEditing={isEditing}
|
isBinding={isBinding}
|
||||||
isHovered={isHovered}
|
isCurrentParent={isCurrentParent}
|
||||||
isSelected={isSelected}
|
isEditing={isEditing}
|
||||||
meta={meta}
|
isHovered={isHovered}
|
||||||
/>
|
isSelected={isSelected}
|
||||||
)}
|
meta={meta as any}
|
||||||
</g>
|
events={events}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</SVGContainer>
|
||||||
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
1
packages/core/src/components/svg-container/index.ts
Normal file
1
packages/core/src/components/svg-container/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './svg-container'
|
13
packages/core/src/components/svg-container/svg-container.tsx
Normal file
13
packages/core/src/components/svg-container/svg-container.tsx
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
interface SvgContainerProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SVGContainer = React.memo(({ children }: SvgContainerProps) => {
|
||||||
|
return (
|
||||||
|
<svg className="tl-positioned-svg">
|
||||||
|
<g className="tl-centered-g">{children}</g>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
})
|
|
@ -14,3 +14,4 @@ export * from './useHandleEvents'
|
||||||
export * from './useHandles'
|
export * from './useHandles'
|
||||||
export * from './usePreventNavigation'
|
export * from './usePreventNavigation'
|
||||||
export * from './useBoundsEvents'
|
export * from './useBoundsEvents'
|
||||||
|
export * from './usePosition'
|
||||||
|
|
|
@ -3,21 +3,29 @@ import * as React from 'react'
|
||||||
import type { TLPageState } from '+types'
|
import type { TLPageState } from '+types'
|
||||||
|
|
||||||
export function useCameraCss(ref: React.RefObject<HTMLDivElement>, pageState: TLPageState) {
|
export function useCameraCss(ref: React.RefObject<HTMLDivElement>, pageState: TLPageState) {
|
||||||
const rGroup = React.useRef<SVGGElement>(null)
|
const rLayer = React.useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
// Update the tl-zoom CSS variable when the zoom changes
|
// Update the tl-zoom CSS variable when the zoom changes
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
ref.current!.style.setProperty('--tl-zoom', pageState.camera.zoom.toString())
|
ref.current!.style.setProperty('--tl-zoom', pageState.camera.zoom.toString())
|
||||||
}, [pageState.camera.zoom])
|
}, [pageState.camera.zoom])
|
||||||
|
|
||||||
// Update the group's position when the camera moves or zooms
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const {
|
ref.current!.style.setProperty('--tl-camera-x', pageState.camera.point[0] + 'px')
|
||||||
zoom,
|
ref.current!.style.setProperty('--tl-camera-y', pageState.camera.point[1] + 'px')
|
||||||
point: [x = 0, y = 0],
|
}, [pageState.camera.point])
|
||||||
} = pageState.camera
|
|
||||||
rGroup.current?.setAttribute('transform', `scale(${zoom}) translate(${x} ${y})`)
|
|
||||||
}, [pageState.camera])
|
|
||||||
|
|
||||||
return rGroup
|
// Update the group's position when the camera moves or zooms
|
||||||
|
// React.useEffect(() => {
|
||||||
|
// const {
|
||||||
|
// zoom,
|
||||||
|
// point: [x = 0, y = 0],
|
||||||
|
// } = pageState.camera
|
||||||
|
// rLayer.current?.style.setProperty(
|
||||||
|
// 'transform',
|
||||||
|
// `scale(${zoom},${zoom}) translate(${x}px,${y}px)`
|
||||||
|
// )
|
||||||
|
// }, [pageState.camera])
|
||||||
|
|
||||||
|
return rLayer
|
||||||
}
|
}
|
||||||
|
|
20
packages/core/src/hooks/usePosition.ts
Normal file
20
packages/core/src/hooks/usePosition.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
|
import * as React from 'react'
|
||||||
|
import type { TLBounds } from '+types'
|
||||||
|
|
||||||
|
export function usePosition(bounds: TLBounds, rotation = 0) {
|
||||||
|
const rBounds = React.useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const elm = rBounds.current!
|
||||||
|
const transform = `
|
||||||
|
translate(calc(${bounds.minX}px - var(--tl-padding)),calc(${bounds.minY}px - var(--tl-padding)))
|
||||||
|
rotate(${rotation + (bounds.rotation || 0)}rad)
|
||||||
|
`
|
||||||
|
elm.style.setProperty('transform', transform)
|
||||||
|
elm.style.setProperty('width', `calc(${bounds.width}px + (var(--tl-padding) * 2))`)
|
||||||
|
elm.style.setProperty('height', `calc(${bounds.height}px + (var(--tl-padding) * 2))`)
|
||||||
|
}, [rBounds, bounds, rotation])
|
||||||
|
|
||||||
|
return rBounds
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
export function usePreventNavigation(rCanvas: React.RefObject<SVGGElement>): void {
|
export function usePreventNavigation(rCanvas: React.RefObject<HTMLDivElement>): void {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const preventGestureNavigation = (event: TouchEvent) => {
|
const preventGestureNavigation = (event: TouchEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
|
@ -6,10 +6,10 @@ function canvasToScreen(point: number[], camera: TLPageState['camera']): number[
|
||||||
return [(point[0] + camera.point[0]) * camera.zoom, (point[1] + camera.point[1]) * camera.zoom]
|
return [(point[0] + camera.point[0]) * camera.zoom, (point[1] + camera.point[1]) * camera.zoom]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSelection<T extends TLShape>(
|
export function useSelection<T extends TLShape, E extends HTMLElement | SVGElement>(
|
||||||
page: TLPage<T, TLBinding>,
|
page: TLPage<T, TLBinding>,
|
||||||
pageState: TLPageState,
|
pageState: TLPageState,
|
||||||
shapeUtils: TLShapeUtils<T>
|
shapeUtils: TLShapeUtils<T, E>
|
||||||
) {
|
) {
|
||||||
const { rScreenBounds } = useTLContext()
|
const { rScreenBounds } = useTLContext()
|
||||||
const { selectedIds } = pageState
|
const { selectedIds } = pageState
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import type {
|
import type {
|
||||||
|
@ -13,8 +14,8 @@ import type {
|
||||||
import { Utils, Vec } from '+utils'
|
import { Utils, Vec } from '+utils'
|
||||||
|
|
||||||
function addToShapeTree<T extends TLShape, M extends Record<string, unknown>>(
|
function addToShapeTree<T extends TLShape, M extends Record<string, unknown>>(
|
||||||
shape: TLShape,
|
shape: T,
|
||||||
branch: IShapeTreeNode<M>[],
|
branch: IShapeTreeNode<T, M>[],
|
||||||
shapes: TLPage<T, TLBinding>['shapes'],
|
shapes: TLPage<T, TLBinding>['shapes'],
|
||||||
pageState: {
|
pageState: {
|
||||||
bindingTargetId?: string
|
bindingTargetId?: string
|
||||||
|
@ -27,7 +28,7 @@ function addToShapeTree<T extends TLShape, M extends Record<string, unknown>>(
|
||||||
},
|
},
|
||||||
meta?: M
|
meta?: M
|
||||||
) {
|
) {
|
||||||
const node: IShapeTreeNode<M> = {
|
const node: IShapeTreeNode<T, M> = {
|
||||||
shape,
|
shape,
|
||||||
isCurrentParent: pageState.currentParentId === shape.id,
|
isCurrentParent: pageState.currentParentId === shape.id,
|
||||||
isEditing: pageState.editingId === shape.id,
|
isEditing: pageState.editingId === shape.id,
|
||||||
|
@ -37,7 +38,7 @@ function addToShapeTree<T extends TLShape, M extends Record<string, unknown>>(
|
||||||
(shape.children ? shape.children.includes(pageState.hoveredId) : false)
|
(shape.children ? shape.children.includes(pageState.hoveredId) : false)
|
||||||
: false,
|
: false,
|
||||||
isBinding: pageState.bindingTargetId === shape.id,
|
isBinding: pageState.bindingTargetId === shape.id,
|
||||||
meta,
|
meta: meta as any,
|
||||||
}
|
}
|
||||||
|
|
||||||
branch.push(node)
|
branch.push(node)
|
||||||
|
@ -54,14 +55,18 @@ function addToShapeTree<T extends TLShape, M extends Record<string, unknown>>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function shapeIsInViewport(shape: TLShape, bounds: TLBounds, viewport: TLBounds) {
|
function shapeIsInViewport(bounds: TLBounds, viewport: TLBounds) {
|
||||||
return Utils.boundsContain(viewport, bounds) || Utils.boundsCollide(viewport, bounds)
|
return Utils.boundsContain(viewport, bounds) || Utils.boundsCollide(viewport, bounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useShapeTree<T extends TLShape, M extends Record<string, unknown>>(
|
export function useShapeTree<
|
||||||
|
T extends TLShape,
|
||||||
|
E extends SVGElement | HTMLElement,
|
||||||
|
M extends Record<string, unknown>
|
||||||
|
>(
|
||||||
page: TLPage<T, TLBinding>,
|
page: TLPage<T, TLBinding>,
|
||||||
pageState: TLPageState,
|
pageState: TLPageState,
|
||||||
shapeUtils: TLShapeUtils<T>,
|
shapeUtils: TLShapeUtils<T, E>,
|
||||||
size: number[],
|
size: number[],
|
||||||
meta?: M,
|
meta?: M,
|
||||||
onChange?: TLCallbacks['onChange']
|
onChange?: TLCallbacks['onChange']
|
||||||
|
@ -100,7 +105,7 @@ export function useShapeTree<T extends TLShape, M extends Record<string, unknown
|
||||||
// Don't hide selected shapes (this breaks certain drag interactions)
|
// Don't hide selected shapes (this breaks certain drag interactions)
|
||||||
if (
|
if (
|
||||||
selectedIds.includes(shape.id) ||
|
selectedIds.includes(shape.id) ||
|
||||||
shapeIsInViewport(shape, shapeUtils[shape.type as T['type']].getBounds(shape), viewport)
|
shapeIsInViewport(shapeUtils[shape.type as T['type']].getBounds(shape), viewport)
|
||||||
) {
|
) {
|
||||||
if (shape.parentId === page.id) {
|
if (shape.parentId === page.id) {
|
||||||
shapesIdsToRender.add(shape.id)
|
shapesIdsToRender.add(shape.id)
|
||||||
|
@ -132,7 +137,7 @@ export function useShapeTree<T extends TLShape, M extends Record<string, unknown
|
||||||
|
|
||||||
// Populate the shape tree
|
// Populate the shape tree
|
||||||
|
|
||||||
const tree: IShapeTreeNode<M>[] = []
|
const tree: IShapeTreeNode<T, M>[] = []
|
||||||
|
|
||||||
const info = { ...pageState, bindingTargetId }
|
const info = { ...pageState, bindingTargetId }
|
||||||
|
|
||||||
|
|
|
@ -111,6 +111,9 @@ const tlcss = css`
|
||||||
.tl-container {
|
.tl-container {
|
||||||
--tl-zoom: 1;
|
--tl-zoom: 1;
|
||||||
--tl-scale: calc(1 / var(--tl-zoom));
|
--tl-scale: calc(1 / var(--tl-zoom));
|
||||||
|
--tl-camera-x: 0px;
|
||||||
|
--tl-camera-y: 0px;
|
||||||
|
--tl-padding: calc(32px * var(--tl-scale));
|
||||||
position: relative;
|
position: relative;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -127,6 +130,37 @@ const tlcss = css`
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tl-absolute {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
transform-origin: center center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-positioned {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
transform-origin: center center;
|
||||||
|
pointer-events: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-positioned-svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-layer {
|
||||||
|
transform: scale(var(--tl-zoom), var(--tl-zoom))
|
||||||
|
translate(var(--tl-camera-x), var(--tl-camera-y));
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.tl-counter-scaled {
|
.tl-counter-scaled {
|
||||||
transform: scale(var(--tl-scale));
|
transform: scale(var(--tl-scale));
|
||||||
}
|
}
|
||||||
|
@ -190,6 +224,10 @@ const tlcss = css`
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tl-bounds {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.tl-bounds-center {
|
.tl-bounds-center {
|
||||||
fill: transparent;
|
fill: transparent;
|
||||||
stroke: var(--tl-selectStroke);
|
stroke: var(--tl-selectStroke);
|
||||||
|
@ -210,10 +248,7 @@ const tlcss = css`
|
||||||
}
|
}
|
||||||
|
|
||||||
.tl-canvas {
|
.tl-canvas {
|
||||||
position: absolute;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
top: 0px;
|
|
||||||
left: 0px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
|
@ -256,6 +291,7 @@ const tlcss = css`
|
||||||
fill: transparent;
|
fill: transparent;
|
||||||
stroke: none;
|
stroke: none;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
|
r: calc(20 / max(1, var(--tl-zoom)));
|
||||||
}
|
}
|
||||||
|
|
||||||
.tl-binding-indicator {
|
.tl-binding-indicator {
|
||||||
|
@ -264,18 +300,22 @@ const tlcss = css`
|
||||||
stroke: var(--tl-selected);
|
stroke: var(--tl-selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tl-shape-group {
|
.tl-shape {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tl-shape-group > *[data-shy='true'] {
|
.tl-shape > *[data-shy='true'] {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tl-shape-group:hover > *[data-shy='true'] {
|
.tl-shape:hover > *[data-shy='true'] {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tl-centered-g {
|
||||||
|
transform: translate(var(--tl-padding), var(--tl-padding));
|
||||||
|
}
|
||||||
|
|
||||||
.tl-current-parent > *[data-shy='true'] {
|
.tl-current-parent > *[data-shy='true'] {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,16 +2,18 @@ import * as React from 'react'
|
||||||
import type { Inputs } from '+inputs'
|
import type { Inputs } from '+inputs'
|
||||||
import type { TLCallbacks, TLShape, TLBounds, TLPageState, TLShapeUtils } from '+types'
|
import type { TLCallbacks, TLShape, TLBounds, TLPageState, TLShapeUtils } from '+types'
|
||||||
|
|
||||||
export interface TLContextType {
|
export interface TLContextType<T extends TLShape, E extends HTMLElement | SVGElement> {
|
||||||
id?: string
|
id?: string
|
||||||
callbacks: Partial<TLCallbacks>
|
callbacks: Partial<TLCallbacks>
|
||||||
shapeUtils: TLShapeUtils<TLShape>
|
shapeUtils: TLShapeUtils<T, E>
|
||||||
rPageState: React.MutableRefObject<TLPageState>
|
rPageState: React.MutableRefObject<TLPageState>
|
||||||
rScreenBounds: React.MutableRefObject<TLBounds | null>
|
rScreenBounds: React.MutableRefObject<TLBounds | null>
|
||||||
inputs: Inputs
|
inputs: Inputs
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TLContext = React.createContext<TLContextType>({} as TLContextType)
|
export const TLContext = React.createContext<TLContextType<any, any>>(
|
||||||
|
{} as TLContextType<TLShape, HTMLElement | SVGElement>
|
||||||
|
)
|
||||||
|
|
||||||
export function useTLContext() {
|
export function useTLContext() {
|
||||||
const context = React.useContext(TLContext)
|
const context = React.useContext(TLContext)
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { TLShapeUtil, TLShape, TLBounds, TLRenderInfo, TLTransformInfo } from '+types'
|
import { TLShapeUtil, TLShape, TLShapeProps, TLBounds, TLRenderInfo, TLTransformInfo } from '+types'
|
||||||
import Utils, { Intersect } from '+utils'
|
import Utils, { Intersect } from '+utils'
|
||||||
|
|
||||||
export interface BoxShape extends TLShape {
|
export interface BoxShape extends TLShape {
|
||||||
size: number[]
|
size: number[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Box extends TLShapeUtil<BoxShape> {
|
export class Box extends TLShapeUtil<BoxShape, SVGGElement> {
|
||||||
type = 'box'
|
type = 'box'
|
||||||
|
|
||||||
defaultProps = {
|
defaultProps = {
|
||||||
|
@ -21,13 +21,15 @@ export class Box extends TLShapeUtil<BoxShape> {
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
create(props: Partial<BoxShape>) {
|
render = React.forwardRef<SVGGElement, TLShapeProps<BoxShape, SVGGElement>>(
|
||||||
return { ...this.defaultProps, ...props }
|
({ shape, events }, ref) => {
|
||||||
}
|
return (
|
||||||
|
<g ref={ref} {...events}>
|
||||||
render(shape: BoxShape, info: TLRenderInfo): JSX.Element {
|
<rect width={shape.size[0]} height={shape.size[1]} fill="none" stroke="black" />
|
||||||
return <rect width={100} height={100} fill="none" stroke="black" />
|
</g>
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
renderIndicator(shape: BoxShape) {
|
renderIndicator(shape: BoxShape) {
|
||||||
return <rect width={100} height={100} />
|
return <rect width={100} height={100} />
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { TLShapeUtils } from '+types'
|
import type { TLShapeUtils } from '+types'
|
||||||
import { Box, BoxShape } from './box'
|
import { Box, BoxShape } from './box'
|
||||||
|
|
||||||
export const mockUtils: TLShapeUtils<BoxShape> = {
|
export const mockUtils: TLShapeUtils<BoxShape, SVGGElement> = {
|
||||||
box: new Box(),
|
box: new Box(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* --------------------- Primary -------------------- */
|
/* --------------------- Primary -------------------- */
|
||||||
|
|
||||||
|
import React, { ForwardedRef } from 'react'
|
||||||
|
|
||||||
export type Patch<T> = Partial<{ [P in keyof T]: T | Partial<T> | Patch<T[P]> }>
|
export type Patch<T> = Partial<{ [P in keyof T]: T | Partial<T> | Patch<T[P]> }>
|
||||||
|
|
||||||
export interface TLPage<T extends TLShape, B extends TLBinding> {
|
export interface TLPage<T extends TLShape, B extends TLBinding> {
|
||||||
|
@ -54,21 +56,35 @@ export interface TLShape {
|
||||||
isAspectRatioLocked?: boolean
|
isAspectRatioLocked?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TLShapeUtils<T extends TLShape> = Record<string, TLShapeUtil<T>>
|
export type TLShapeUtils<T extends TLShape, E extends SVGElement | HTMLElement> = Record<
|
||||||
|
string,
|
||||||
|
TLShapeUtil<T, E>
|
||||||
|
>
|
||||||
|
|
||||||
export interface TLRenderInfo<M = any, T extends SVGElement | HTMLElement = any> {
|
export interface TLRenderInfo<M = any, E = any> {
|
||||||
ref?: React.RefObject<T>
|
|
||||||
isEditing: boolean
|
isEditing: boolean
|
||||||
isBinding: boolean
|
isBinding: boolean
|
||||||
isHovered: boolean
|
isHovered: boolean
|
||||||
isSelected: boolean
|
isSelected: boolean
|
||||||
isCurrentParent: boolean
|
isCurrentParent: boolean
|
||||||
onTextChange?: TLCallbacks['onTextChange']
|
|
||||||
onTextBlur?: TLCallbacks['onTextBlur']
|
|
||||||
onTextFocus?: TLCallbacks['onTextFocus']
|
|
||||||
onTextKeyDown?: TLCallbacks['onTextKeyDown']
|
|
||||||
onTextKeyUp?: TLCallbacks['onTextKeyUp']
|
|
||||||
meta: M extends any ? M : never
|
meta: M extends any ? M : never
|
||||||
|
events: {
|
||||||
|
onPointerDown: (e: React.PointerEvent<E>) => void
|
||||||
|
onPointerUp: (e: React.PointerEvent<E>) => void
|
||||||
|
onPointerEnter: (e: React.PointerEvent<E>) => void
|
||||||
|
onPointerMove: (e: React.PointerEvent<E>) => void
|
||||||
|
onPointerLeave: (e: React.PointerEvent<E>) => void
|
||||||
|
onTextChange?: TLCallbacks['onTextChange']
|
||||||
|
onTextBlur?: TLCallbacks['onTextBlur']
|
||||||
|
onTextFocus?: TLCallbacks['onTextFocus']
|
||||||
|
onTextKeyDown?: TLCallbacks['onTextKeyDown']
|
||||||
|
onTextKeyUp?: TLCallbacks['onTextKeyUp']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TLShapeProps<T extends TLShape, E = any, M = any> extends TLRenderInfo<M, E> {
|
||||||
|
ref: ForwardedRef<E>
|
||||||
|
shape: T
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TLTool {
|
export interface TLTool {
|
||||||
|
@ -261,18 +277,26 @@ export interface TLBezierCurveSegment {
|
||||||
/* Shape Utility */
|
/* Shape Utility */
|
||||||
/* -------------------------------------------------- */
|
/* -------------------------------------------------- */
|
||||||
|
|
||||||
export abstract class TLShapeUtil<T extends TLShape> {
|
export abstract class TLShapeUtil<T extends TLShape, E extends HTMLElement | SVGElement> {
|
||||||
|
refMap = new Map<string, React.RefObject<E>>()
|
||||||
|
|
||||||
boundsCache = new WeakMap<TLShape, TLBounds>()
|
boundsCache = new WeakMap<TLShape, TLBounds>()
|
||||||
|
|
||||||
isEditableText = false
|
isEditableText = false
|
||||||
|
|
||||||
isAspectRatioLocked = false
|
isAspectRatioLocked = false
|
||||||
|
|
||||||
canEdit = false
|
canEdit = false
|
||||||
|
|
||||||
canBind = false
|
canBind = false
|
||||||
|
|
||||||
abstract type: T['type']
|
abstract type: T['type']
|
||||||
|
|
||||||
abstract defaultProps: T
|
abstract defaultProps: T
|
||||||
|
|
||||||
abstract render(shape: T, info: TLRenderInfo): JSX.Element | null
|
abstract render: React.ForwardRefExoticComponent<
|
||||||
|
{ shape: T; ref: React.ForwardedRef<E> } & TLRenderInfo & React.RefAttributes<E>
|
||||||
|
>
|
||||||
|
|
||||||
abstract renderIndicator(shape: T): JSX.Element | null
|
abstract renderIndicator(shape: T): JSX.Element | null
|
||||||
|
|
||||||
|
@ -303,6 +327,14 @@ export abstract class TLShapeUtil<T extends TLShape> {
|
||||||
return [bounds.width / 2, bounds.height / 2]
|
return [bounds.width / 2, bounds.height / 2]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getRef(shape: T): React.RefObject<E> {
|
||||||
|
if (!this.refMap.has(shape.id)) {
|
||||||
|
this.refMap.set(shape.id, React.createRef<E>())
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
return this.refMap.get(shape.id)!
|
||||||
|
}
|
||||||
|
|
||||||
getBindingPoint(
|
getBindingPoint(
|
||||||
shape: T,
|
shape: T,
|
||||||
fromShape: TLShape,
|
fromShape: TLShape,
|
||||||
|
@ -315,7 +347,8 @@ export abstract class TLShapeUtil<T extends TLShape> {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
create(props: Partial<T>): T {
|
create(props: { id: string } & Partial<T>): T {
|
||||||
|
this.refMap.set(props.id, React.createRef<E>())
|
||||||
return { ...this.defaultProps, ...props }
|
return { ...this.defaultProps, ...props }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -380,15 +413,15 @@ export abstract class TLShapeUtil<T extends TLShape> {
|
||||||
|
|
||||||
/* -------------------- Internal -------------------- */
|
/* -------------------- Internal -------------------- */
|
||||||
|
|
||||||
export interface IShapeTreeNode<M extends Record<string, unknown>> {
|
export interface IShapeTreeNode<T extends TLShape, M extends Record<string, unknown>> {
|
||||||
shape: TLShape
|
shape: T
|
||||||
children?: IShapeTreeNode<M>[]
|
children?: IShapeTreeNode<TLShape, M>[]
|
||||||
isEditing: boolean
|
isEditing: boolean
|
||||||
isBinding: boolean
|
isBinding: boolean
|
||||||
isHovered: boolean
|
isHovered: boolean
|
||||||
isSelected: boolean
|
isSelected: boolean
|
||||||
isCurrentParent: boolean
|
isCurrentParent: boolean
|
||||||
meta?: M
|
meta?: M extends any ? M : never
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------------- */
|
/* -------------------------------------------------- */
|
||||||
|
|
|
@ -8,18 +8,31 @@ export const tldrawShapeUtils: TLDrawShapeUtils = {
|
||||||
[TLDrawShapeType.Arrow]: new Arrow(),
|
[TLDrawShapeType.Arrow]: new Arrow(),
|
||||||
[TLDrawShapeType.Text]: new Text(),
|
[TLDrawShapeType.Text]: new Text(),
|
||||||
[TLDrawShapeType.Group]: new Group(),
|
[TLDrawShapeType.Group]: new Group(),
|
||||||
}
|
} as TLDrawShapeUtils
|
||||||
|
|
||||||
export type ShapeByType<T extends keyof TLDrawShapeUtils> = TLDrawShapeUtils[T]
|
export type ShapeByType<T extends keyof TLDrawShapeUtils> = TLDrawShapeUtils[T]
|
||||||
|
|
||||||
export function getShapeUtilsByType<T extends TLDrawShape>(shape: T): TLDrawShapeUtil<T> {
|
export function getShapeUtilsByType<T extends TLDrawShape>(
|
||||||
return tldrawShapeUtils[shape.type as T['type']] as TLDrawShapeUtil<T>
|
shape: T
|
||||||
|
): TLDrawShapeUtil<T, HTMLElement | SVGElement> {
|
||||||
|
return tldrawShapeUtils[shape.type as T['type']] as unknown as TLDrawShapeUtil<
|
||||||
|
T,
|
||||||
|
HTMLElement | SVGElement
|
||||||
|
>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getShapeUtils<T extends TLDrawShape>(shape: T): TLDrawShapeUtil<T> {
|
export function getShapeUtils<T extends TLDrawShape>(
|
||||||
return tldrawShapeUtils[shape.type as T['type']] as TLDrawShapeUtil<T>
|
shape: T
|
||||||
|
): TLDrawShapeUtil<T, HTMLElement | SVGElement> {
|
||||||
|
return tldrawShapeUtils[shape.type as T['type']] as unknown as TLDrawShapeUtil<
|
||||||
|
T,
|
||||||
|
HTMLElement | SVGElement
|
||||||
|
>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createShape<TLDrawShape>(type: TLDrawShapeType, props: Partial<TLDrawShape>) {
|
export function createShape<TLDrawShape>(
|
||||||
|
type: TLDrawShapeType,
|
||||||
|
props: { id: string } & Partial<TLDrawShape>
|
||||||
|
) {
|
||||||
return tldrawShapeUtils[type].create(props)
|
return tldrawShapeUtils[type].create(props)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
Intersect,
|
Intersect,
|
||||||
TLHandle,
|
TLHandle,
|
||||||
TLPointerInfo,
|
TLPointerInfo,
|
||||||
|
TLShapeProps,
|
||||||
} from '@tldraw/core'
|
} from '@tldraw/core'
|
||||||
import getStroke from 'perfect-freehand'
|
import getStroke from 'perfect-freehand'
|
||||||
import { defaultStyle, getPerfectDashProps, getShapeStyle } from '~shape/shape-styles'
|
import { defaultStyle, getPerfectDashProps, getShapeStyle } from '~shape/shape-styles'
|
||||||
|
@ -22,7 +23,7 @@ import {
|
||||||
TLDrawRenderInfo,
|
TLDrawRenderInfo,
|
||||||
} from '~types'
|
} from '~types'
|
||||||
|
|
||||||
export class Arrow extends TLDrawShapeUtil<ArrowShape> {
|
export class Arrow extends TLDrawShapeUtil<ArrowShape, SVGGElement> {
|
||||||
type = TLDrawShapeType.Arrow as const
|
type = TLDrawShapeType.Arrow as const
|
||||||
toolType = TLDrawToolType.Handle
|
toolType = TLDrawToolType.Handle
|
||||||
canStyleFill = false
|
canStyleFill = false
|
||||||
|
@ -70,62 +71,130 @@ export class Arrow extends TLDrawShapeUtil<ArrowShape> {
|
||||||
return next.handles !== prev.handles || next.style !== prev.style
|
return next.handles !== prev.handles || next.style !== prev.style
|
||||||
}
|
}
|
||||||
|
|
||||||
render = (shape: ArrowShape, { meta }: TLDrawRenderInfo) => {
|
render = React.forwardRef<SVGGElement, TLShapeProps<ArrowShape, SVGGElement>>(
|
||||||
const {
|
({ shape, meta, events }) => {
|
||||||
handles: { start, bend, end },
|
const {
|
||||||
decorations = {},
|
handles: { start, bend, end },
|
||||||
style,
|
decorations = {},
|
||||||
} = shape
|
style,
|
||||||
|
} = shape
|
||||||
|
|
||||||
const isDraw = style.dash === DashStyle.Draw
|
const isDraw = style.dash === DashStyle.Draw
|
||||||
|
|
||||||
// TODO: Improve drawn arrows
|
// TODO: Improve drawn arrows
|
||||||
|
|
||||||
const isStraightLine = Vec.dist(bend.point, Vec.round(Vec.med(start.point, end.point))) < 1
|
const isStraightLine = Vec.dist(bend.point, Vec.round(Vec.med(start.point, end.point))) < 1
|
||||||
|
|
||||||
const styles = getShapeStyle(style, meta.isDarkMode)
|
const styles = getShapeStyle(style, meta.isDarkMode)
|
||||||
|
|
||||||
const { strokeWidth } = styles
|
const { strokeWidth } = styles
|
||||||
|
|
||||||
const arrowDist = Vec.dist(start.point, end.point)
|
const arrowDist = Vec.dist(start.point, end.point)
|
||||||
|
|
||||||
const arrowHeadLength = Math.min(arrowDist / 3, strokeWidth * 8)
|
const arrowHeadLength = Math.min(arrowDist / 3, strokeWidth * 8)
|
||||||
|
|
||||||
let shaftPath: JSX.Element | null
|
let shaftPath: JSX.Element | null
|
||||||
let startArrowHead: { left: number[]; right: number[] } | undefined
|
let startArrowHead: { left: number[]; right: number[] } | undefined
|
||||||
let endArrowHead: { left: number[]; right: number[] } | undefined
|
let endArrowHead: { left: number[]; right: number[] } | undefined
|
||||||
|
|
||||||
if (isStraightLine) {
|
if (isStraightLine) {
|
||||||
const sw = strokeWidth * (isDraw ? 1.25 : 1.618)
|
const sw = strokeWidth * (isDraw ? 1.25 : 1.618)
|
||||||
|
|
||||||
const path = Utils.getFromCache(this.pathCache, shape, () =>
|
const path = Utils.getFromCache(this.pathCache, shape, () =>
|
||||||
isDraw
|
isDraw
|
||||||
? renderFreehandArrowShaft(shape)
|
? renderFreehandArrowShaft(shape)
|
||||||
: 'M' + Vec.round(start.point) + 'L' + Vec.round(end.point)
|
: 'M' + Vec.round(start.point) + 'L' + Vec.round(end.point)
|
||||||
)
|
)
|
||||||
|
|
||||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||||
arrowDist,
|
arrowDist,
|
||||||
sw,
|
sw,
|
||||||
shape.style.dash,
|
shape.style.dash,
|
||||||
2
|
2
|
||||||
)
|
)
|
||||||
|
|
||||||
if (decorations.start) {
|
if (decorations.start) {
|
||||||
startArrowHead = getStraightArrowHeadPoints(start.point, end.point, arrowHeadLength)
|
startArrowHead = getStraightArrowHeadPoints(start.point, end.point, arrowHeadLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (decorations.end) {
|
if (decorations.end) {
|
||||||
endArrowHead = getStraightArrowHeadPoints(end.point, start.point, arrowHeadLength)
|
endArrowHead = getStraightArrowHeadPoints(end.point, start.point, arrowHeadLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Straight arrow path
|
// Straight arrow path
|
||||||
shaftPath =
|
shaftPath =
|
||||||
arrowDist > 2 ? (
|
arrowDist > 2 ? (
|
||||||
|
<>
|
||||||
|
<path
|
||||||
|
d={path}
|
||||||
|
fill="none"
|
||||||
|
strokeWidth={Math.max(8, strokeWidth * 2)}
|
||||||
|
strokeDasharray="none"
|
||||||
|
strokeDashoffset="none"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
pointerEvents="stroke"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d={path}
|
||||||
|
fill={styles.stroke}
|
||||||
|
stroke={styles.stroke}
|
||||||
|
strokeWidth={sw}
|
||||||
|
strokeDasharray={strokeDasharray}
|
||||||
|
strokeDashoffset={strokeDashoffset}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
pointerEvents="stroke"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null
|
||||||
|
} else {
|
||||||
|
const circle = getCtp(shape)
|
||||||
|
|
||||||
|
const sw = strokeWidth * (isDraw ? 1.25 : 1.618)
|
||||||
|
|
||||||
|
const path = Utils.getFromCache(this.pathCache, shape, () =>
|
||||||
|
isDraw
|
||||||
|
? renderCurvedFreehandArrowShaft(shape, circle)
|
||||||
|
: getArrowArcPath(start, end, circle, shape.bend)
|
||||||
|
)
|
||||||
|
|
||||||
|
const { center, radius, length } = getArrowArc(shape)
|
||||||
|
|
||||||
|
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||||
|
length - 1,
|
||||||
|
sw,
|
||||||
|
shape.style.dash,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
|
||||||
|
if (decorations.start) {
|
||||||
|
startArrowHead = getCurvedArrowHeadPoints(
|
||||||
|
start.point,
|
||||||
|
arrowHeadLength,
|
||||||
|
center,
|
||||||
|
radius,
|
||||||
|
length < 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decorations.end) {
|
||||||
|
endArrowHead = getCurvedArrowHeadPoints(
|
||||||
|
end.point,
|
||||||
|
arrowHeadLength,
|
||||||
|
center,
|
||||||
|
radius,
|
||||||
|
length >= 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Curved arrow path
|
||||||
|
shaftPath = (
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
d={path}
|
d={path}
|
||||||
fill="none"
|
fill="none"
|
||||||
|
stroke="transparent"
|
||||||
strokeWidth={Math.max(8, strokeWidth * 2)}
|
strokeWidth={Math.max(8, strokeWidth * 2)}
|
||||||
strokeDasharray="none"
|
strokeDasharray="none"
|
||||||
strokeDashoffset="none"
|
strokeDashoffset="none"
|
||||||
|
@ -135,7 +204,7 @@ export class Arrow extends TLDrawShapeUtil<ArrowShape> {
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d={path}
|
d={path}
|
||||||
fill={styles.stroke}
|
fill={isDraw ? styles.stroke : 'none'}
|
||||||
stroke={styles.stroke}
|
stroke={styles.stroke}
|
||||||
strokeWidth={sw}
|
strokeWidth={sw}
|
||||||
strokeDasharray={strokeDasharray}
|
strokeDasharray={strokeDasharray}
|
||||||
|
@ -145,110 +214,46 @@ export class Arrow extends TLDrawShapeUtil<ArrowShape> {
|
||||||
pointerEvents="stroke"
|
pointerEvents="stroke"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : null
|
|
||||||
} else {
|
|
||||||
const circle = getCtp(shape)
|
|
||||||
|
|
||||||
const sw = strokeWidth * (isDraw ? 1.25 : 1.618)
|
|
||||||
|
|
||||||
const path = Utils.getFromCache(this.pathCache, shape, () =>
|
|
||||||
isDraw
|
|
||||||
? renderCurvedFreehandArrowShaft(shape, circle)
|
|
||||||
: getArrowArcPath(start, end, circle, shape.bend)
|
|
||||||
)
|
|
||||||
|
|
||||||
const { center, radius, length } = getArrowArc(shape)
|
|
||||||
|
|
||||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
|
||||||
length - 1,
|
|
||||||
sw,
|
|
||||||
shape.style.dash,
|
|
||||||
2
|
|
||||||
)
|
|
||||||
|
|
||||||
if (decorations.start) {
|
|
||||||
startArrowHead = getCurvedArrowHeadPoints(
|
|
||||||
start.point,
|
|
||||||
arrowHeadLength,
|
|
||||||
center,
|
|
||||||
radius,
|
|
||||||
length < 0
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (decorations.end) {
|
const sw = strokeWidth * 1.618
|
||||||
endArrowHead = getCurvedArrowHeadPoints(
|
|
||||||
end.point,
|
|
||||||
arrowHeadLength,
|
|
||||||
center,
|
|
||||||
radius,
|
|
||||||
length >= 0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Curved arrow path
|
return (
|
||||||
shaftPath = (
|
<g {...events}>
|
||||||
<>
|
<g pointerEvents="none">
|
||||||
<path
|
{shaftPath}
|
||||||
d={path}
|
{startArrowHead && (
|
||||||
fill="none"
|
<path
|
||||||
stroke="transparent"
|
d={`M ${startArrowHead.left} L ${start.point} ${startArrowHead.right}`}
|
||||||
strokeWidth={Math.max(8, strokeWidth * 2)}
|
fill="none"
|
||||||
strokeDasharray="none"
|
stroke={styles.stroke}
|
||||||
strokeDashoffset="none"
|
strokeWidth={sw}
|
||||||
strokeLinecap="round"
|
strokeDashoffset="none"
|
||||||
strokeLinejoin="round"
|
strokeDasharray="none"
|
||||||
pointerEvents="stroke"
|
strokeLinecap="round"
|
||||||
/>
|
strokeLinejoin="round"
|
||||||
<path
|
pointerEvents="stroke"
|
||||||
d={path}
|
/>
|
||||||
fill={isDraw ? styles.stroke : 'none'}
|
)}
|
||||||
stroke={styles.stroke}
|
{endArrowHead && (
|
||||||
strokeWidth={sw}
|
<path
|
||||||
strokeDasharray={strokeDasharray}
|
d={`M ${endArrowHead.left} L ${end.point} ${endArrowHead.right}`}
|
||||||
strokeDashoffset={strokeDashoffset}
|
fill="none"
|
||||||
strokeLinecap="round"
|
stroke={styles.stroke}
|
||||||
strokeLinejoin="round"
|
strokeWidth={sw}
|
||||||
pointerEvents="stroke"
|
strokeDashoffset="none"
|
||||||
/>
|
strokeDasharray="none"
|
||||||
</>
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
pointerEvents="stroke"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
const sw = strokeWidth * 1.618
|
|
||||||
|
|
||||||
return (
|
|
||||||
<g pointerEvents="none">
|
|
||||||
{shaftPath}
|
|
||||||
{startArrowHead && (
|
|
||||||
<path
|
|
||||||
d={`M ${startArrowHead.left} L ${start.point} ${startArrowHead.right}`}
|
|
||||||
fill="none"
|
|
||||||
stroke={styles.stroke}
|
|
||||||
strokeWidth={sw}
|
|
||||||
strokeDashoffset="none"
|
|
||||||
strokeDasharray="none"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
pointerEvents="stroke"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{endArrowHead && (
|
|
||||||
<path
|
|
||||||
d={`M ${endArrowHead.left} L ${end.point} ${endArrowHead.right}`}
|
|
||||||
fill="none"
|
|
||||||
stroke={styles.stroke}
|
|
||||||
strokeWidth={sw}
|
|
||||||
strokeDashoffset="none"
|
|
||||||
strokeDasharray="none"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
pointerEvents="stroke"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</g>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
renderIndicator(shape: ArrowShape) {
|
renderIndicator(shape: ArrowShape) {
|
||||||
const path = Utils.getFromCache(this.simplePathCache, shape.handles, () => getArrowPath(shape))
|
const path = Utils.getFromCache(this.simplePathCache, shape.handles, () => getArrowPath(shape))
|
||||||
|
|
|
@ -1,17 +1,10 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { TLBounds, Utils, Vec, TLTransformInfo, Intersect } from '@tldraw/core'
|
import { TLBounds, Utils, Vec, TLTransformInfo, Intersect, TLShapeProps } from '@tldraw/core'
|
||||||
import getStroke, { getStrokePoints } from 'perfect-freehand'
|
import getStroke, { getStrokePoints } from 'perfect-freehand'
|
||||||
import { defaultStyle, getShapeStyle } from '~shape/shape-styles'
|
import { defaultStyle, getShapeStyle } from '~shape/shape-styles'
|
||||||
import {
|
import { DrawShape, DashStyle, TLDrawShapeUtil, TLDrawShapeType, TLDrawToolType } from '~types'
|
||||||
DrawShape,
|
|
||||||
DashStyle,
|
|
||||||
TLDrawShapeUtil,
|
|
||||||
TLDrawShapeType,
|
|
||||||
TLDrawToolType,
|
|
||||||
TLDrawRenderInfo,
|
|
||||||
} from '~types'
|
|
||||||
|
|
||||||
export class Draw extends TLDrawShapeUtil<DrawShape> {
|
export class Draw extends TLDrawShapeUtil<DrawShape, SVGGElement> {
|
||||||
type = TLDrawShapeType.Draw as const
|
type = TLDrawShapeType.Draw as const
|
||||||
toolType = TLDrawToolType.Draw
|
toolType = TLDrawToolType.Draw
|
||||||
|
|
||||||
|
@ -37,118 +30,122 @@ export class Draw extends TLDrawShapeUtil<DrawShape> {
|
||||||
return next.points !== prev.points || next.style !== prev.style
|
return next.points !== prev.points || next.style !== prev.style
|
||||||
}
|
}
|
||||||
|
|
||||||
render(shape: DrawShape, { meta, isEditing }: TLDrawRenderInfo): JSX.Element {
|
render = React.forwardRef<SVGGElement, TLShapeProps<DrawShape, SVGGElement>>(
|
||||||
const { points, style } = shape
|
({ shape, meta, events, isEditing }) => {
|
||||||
|
const { points, style } = shape
|
||||||
|
|
||||||
const styles = getShapeStyle(style, meta.isDarkMode)
|
const styles = getShapeStyle(style, meta.isDarkMode)
|
||||||
|
|
||||||
const strokeWidth = styles.strokeWidth
|
const strokeWidth = styles.strokeWidth
|
||||||
|
|
||||||
// For very short lines, draw a point instead of a line
|
// For very short lines, draw a point instead of a line
|
||||||
const bounds = this.getBounds(shape)
|
const bounds = this.getBounds(shape)
|
||||||
|
|
||||||
const verySmall = bounds.width < strokeWidth / 2 && bounds.height < strokeWidth / 2
|
const verySmall = bounds.width < strokeWidth / 2 && bounds.height < strokeWidth / 2
|
||||||
|
|
||||||
if (!isEditing && verySmall) {
|
if (!isEditing && verySmall) {
|
||||||
const sw = strokeWidth * 0.618
|
const sw = strokeWidth * 0.618
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<circle
|
<g {...events}>
|
||||||
r={strokeWidth * 0.618}
|
<circle
|
||||||
fill={styles.stroke}
|
r={strokeWidth * 0.618}
|
||||||
stroke={styles.stroke}
|
fill={styles.stroke}
|
||||||
strokeWidth={sw}
|
stroke={styles.stroke}
|
||||||
pointerEvents="all"
|
strokeWidth={sw}
|
||||||
/>
|
pointerEvents="all"
|
||||||
)
|
/>
|
||||||
}
|
</g>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const shouldFill =
|
const shouldFill =
|
||||||
style.isFilled &&
|
style.isFilled &&
|
||||||
points.length > 3 &&
|
points.length > 3 &&
|
||||||
Vec.dist(points[0], points[points.length - 1]) < +styles.strokeWidth * 2
|
Vec.dist(points[0], points[points.length - 1]) < +styles.strokeWidth * 2
|
||||||
|
|
||||||
// For drawn lines, draw a line from the path cache
|
// For drawn lines, draw a line from the path cache
|
||||||
|
|
||||||
if (shape.style.dash === DashStyle.Draw) {
|
if (shape.style.dash === DashStyle.Draw) {
|
||||||
const polygonPathData = Utils.getFromCache(this.polygonCache, points, () =>
|
const polygonPathData = Utils.getFromCache(this.polygonCache, points, () =>
|
||||||
getFillPath(shape)
|
getFillPath(shape)
|
||||||
)
|
)
|
||||||
|
|
||||||
const drawPathData = isEditing
|
const drawPathData = isEditing
|
||||||
? getDrawStrokePath(shape, true)
|
? getDrawStrokePath(shape, true)
|
||||||
: Utils.getFromCache(this.drawPathCache, points, () => getDrawStrokePath(shape, false))
|
: Utils.getFromCache(this.drawPathCache, points, () => getDrawStrokePath(shape, false))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<g {...events}>
|
||||||
{shouldFill && (
|
{shouldFill && (
|
||||||
|
<path
|
||||||
|
d={polygonPathData}
|
||||||
|
stroke="none"
|
||||||
|
fill={styles.fill}
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeLinecap="round"
|
||||||
|
pointerEvents="fill"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<path
|
<path
|
||||||
d={polygonPathData}
|
d={drawPathData}
|
||||||
stroke="none"
|
fill={styles.stroke}
|
||||||
fill={styles.fill}
|
stroke={styles.stroke}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
pointerEvents="fill"
|
pointerEvents="all"
|
||||||
/>
|
/>
|
||||||
)}
|
</g>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For solid, dash and dotted lines, draw a regular stroke path
|
||||||
|
|
||||||
|
const strokeDasharray = {
|
||||||
|
[DashStyle.Draw]: 'none',
|
||||||
|
[DashStyle.Solid]: `none`,
|
||||||
|
[DashStyle.Dotted]: `${strokeWidth / 10} ${strokeWidth * 3}`,
|
||||||
|
[DashStyle.Dashed]: `${strokeWidth * 3} ${strokeWidth * 3}`,
|
||||||
|
}[style.dash]
|
||||||
|
|
||||||
|
const strokeDashoffset = {
|
||||||
|
[DashStyle.Draw]: 'none',
|
||||||
|
[DashStyle.Solid]: `none`,
|
||||||
|
[DashStyle.Dotted]: `-${strokeWidth / 20}`,
|
||||||
|
[DashStyle.Dashed]: `-${strokeWidth}`,
|
||||||
|
}[style.dash]
|
||||||
|
|
||||||
|
const path = Utils.getFromCache(this.simplePathCache, points, () => getSolidStrokePath(shape))
|
||||||
|
|
||||||
|
const sw = strokeWidth * 1.618
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g {...events}>
|
||||||
<path
|
<path
|
||||||
d={drawPathData}
|
d={path}
|
||||||
fill={styles.stroke}
|
fill={shouldFill ? styles.fill : 'none'}
|
||||||
stroke={styles.stroke}
|
stroke="transparent"
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={Math.min(4, strokeWidth * 2)}
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
pointerEvents="all"
|
pointerEvents={shouldFill ? 'all' : 'stroke'}
|
||||||
/>
|
/>
|
||||||
</>
|
<path
|
||||||
|
d={path}
|
||||||
|
fill="transparent"
|
||||||
|
stroke={styles.stroke}
|
||||||
|
strokeWidth={sw}
|
||||||
|
strokeDasharray={strokeDasharray}
|
||||||
|
strokeDashoffset={strokeDashoffset}
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeLinecap="round"
|
||||||
|
pointerEvents="stroke"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
// For solid, dash and dotted lines, draw a regular stroke path
|
|
||||||
|
|
||||||
const strokeDasharray = {
|
|
||||||
[DashStyle.Draw]: 'none',
|
|
||||||
[DashStyle.Solid]: `none`,
|
|
||||||
[DashStyle.Dotted]: `${strokeWidth / 10} ${strokeWidth * 3}`,
|
|
||||||
[DashStyle.Dashed]: `${strokeWidth * 3} ${strokeWidth * 3}`,
|
|
||||||
}[style.dash]
|
|
||||||
|
|
||||||
const strokeDashoffset = {
|
|
||||||
[DashStyle.Draw]: 'none',
|
|
||||||
[DashStyle.Solid]: `none`,
|
|
||||||
[DashStyle.Dotted]: `-${strokeWidth / 20}`,
|
|
||||||
[DashStyle.Dashed]: `-${strokeWidth}`,
|
|
||||||
}[style.dash]
|
|
||||||
|
|
||||||
const path = Utils.getFromCache(this.simplePathCache, points, () => getSolidStrokePath(shape))
|
|
||||||
|
|
||||||
const sw = strokeWidth * 1.618
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<path
|
|
||||||
d={path}
|
|
||||||
fill={shouldFill ? styles.fill : 'none'}
|
|
||||||
stroke="transparent"
|
|
||||||
strokeWidth={Math.min(4, strokeWidth * 2)}
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeLinecap="round"
|
|
||||||
pointerEvents={shouldFill ? 'all' : 'stroke'}
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d={path}
|
|
||||||
fill="transparent"
|
|
||||||
stroke={styles.stroke}
|
|
||||||
strokeWidth={sw}
|
|
||||||
strokeDasharray={strokeDasharray}
|
|
||||||
strokeDashoffset={strokeDashoffset}
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeLinecap="round"
|
|
||||||
pointerEvents="stroke"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
renderIndicator(shape: DrawShape): JSX.Element {
|
renderIndicator(shape: DrawShape): JSX.Element {
|
||||||
const { points } = shape
|
const { points } = shape
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Utils, TLTransformInfo, TLBounds, Intersect, Vec } from '@tldraw/core'
|
import { Utils, TLTransformInfo, TLBounds, Intersect, TLShapeProps, Vec } from '@tldraw/core'
|
||||||
import {
|
import {
|
||||||
ArrowShape,
|
ArrowShape,
|
||||||
DashStyle,
|
DashStyle,
|
||||||
|
@ -15,7 +15,7 @@ import getStroke from 'perfect-freehand'
|
||||||
// TODO
|
// TODO
|
||||||
// [ ] Improve indicator shape for drawn shapes
|
// [ ] Improve indicator shape for drawn shapes
|
||||||
|
|
||||||
export class Ellipse extends TLDrawShapeUtil<EllipseShape> {
|
export class Ellipse extends TLDrawShapeUtil<EllipseShape, SVGGElement> {
|
||||||
type = TLDrawShapeType.Ellipse as const
|
type = TLDrawShapeType.Ellipse as const
|
||||||
toolType = TLDrawToolType.Bounds
|
toolType = TLDrawToolType.Bounds
|
||||||
pathCache = new WeakMap<EllipseShape, string>([])
|
pathCache = new WeakMap<EllipseShape, string>([])
|
||||||
|
@ -37,32 +37,79 @@ export class Ellipse extends TLDrawShapeUtil<EllipseShape> {
|
||||||
return next.radius !== prev.radius || next.style !== prev.style
|
return next.radius !== prev.radius || next.style !== prev.style
|
||||||
}
|
}
|
||||||
|
|
||||||
render(shape: EllipseShape, { meta, isBinding }: TLDrawRenderInfo) {
|
render = React.forwardRef<SVGGElement, TLShapeProps<EllipseShape, SVGGElement>>(
|
||||||
const {
|
({ shape, meta, isBinding, events }) => {
|
||||||
radius: [radiusX, radiusY],
|
const {
|
||||||
style,
|
radius: [radiusX, radiusY],
|
||||||
} = shape
|
style,
|
||||||
|
} = shape
|
||||||
|
|
||||||
const styles = getShapeStyle(style, meta.isDarkMode)
|
const styles = getShapeStyle(style, meta.isDarkMode)
|
||||||
const strokeWidth = +styles.strokeWidth
|
const strokeWidth = +styles.strokeWidth
|
||||||
|
|
||||||
const rx = Math.max(0, radiusX - strokeWidth / 2)
|
const rx = Math.max(0, radiusX - strokeWidth / 2)
|
||||||
const ry = Math.max(0, radiusY - strokeWidth / 2)
|
const ry = Math.max(0, radiusY - strokeWidth / 2)
|
||||||
|
|
||||||
if (style.dash === DashStyle.Draw) {
|
if (style.dash === DashStyle.Draw) {
|
||||||
const path = Utils.getFromCache(this.pathCache, shape, () =>
|
const path = Utils.getFromCache(this.pathCache, shape, () =>
|
||||||
renderPath(shape, this.getCenter(shape))
|
renderPath(shape, this.getCenter(shape))
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g {...events}>
|
||||||
|
{isBinding && (
|
||||||
|
<ellipse
|
||||||
|
className="tl-binding-indicator"
|
||||||
|
cx={radiusX}
|
||||||
|
cy={radiusY}
|
||||||
|
rx={rx + 2}
|
||||||
|
ry={ry + 2}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<ellipse
|
||||||
|
cx={radiusX}
|
||||||
|
cy={radiusY}
|
||||||
|
rx={rx}
|
||||||
|
ry={ry}
|
||||||
|
stroke="none"
|
||||||
|
fill={style.isFilled ? styles.fill : 'none'}
|
||||||
|
pointerEvents="all"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d={path}
|
||||||
|
fill={styles.stroke}
|
||||||
|
stroke={styles.stroke}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
pointerEvents="all"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const h = Math.pow(rx - ry, 2) / Math.pow(rx + ry, 2)
|
||||||
|
|
||||||
|
const perimeter = Math.PI * (rx + ry) * (1 + (3 * h) / (10 + Math.sqrt(4 - 3 * h)))
|
||||||
|
|
||||||
|
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||||
|
perimeter,
|
||||||
|
strokeWidth * 1.618,
|
||||||
|
shape.style.dash,
|
||||||
|
4
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const sw = strokeWidth * 1.618
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<g {...events}>
|
||||||
{isBinding && (
|
{isBinding && (
|
||||||
<ellipse
|
<ellipse
|
||||||
className="tl-binding-indicator"
|
className="tl-binding-indicator"
|
||||||
cx={radiusX}
|
cx={radiusX}
|
||||||
cy={radiusY}
|
cy={radiusY}
|
||||||
rx={rx + 2}
|
rx={rx + 32}
|
||||||
ry={ry + 2}
|
ry={ry + 32}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ellipse
|
<ellipse
|
||||||
|
@ -70,64 +117,19 @@ export class Ellipse extends TLDrawShapeUtil<EllipseShape> {
|
||||||
cy={radiusY}
|
cy={radiusY}
|
||||||
rx={rx}
|
rx={rx}
|
||||||
ry={ry}
|
ry={ry}
|
||||||
stroke="none"
|
fill={styles.fill}
|
||||||
fill={style.isFilled ? styles.fill : 'none'}
|
|
||||||
pointerEvents="all"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d={path}
|
|
||||||
fill={styles.stroke}
|
|
||||||
stroke={styles.stroke}
|
stroke={styles.stroke}
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={sw}
|
||||||
|
strokeDasharray={strokeDasharray}
|
||||||
|
strokeDashoffset={strokeDashoffset}
|
||||||
pointerEvents="all"
|
pointerEvents="all"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
/>
|
/>
|
||||||
</>
|
</g>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
const h = Math.pow(rx - ry, 2) / Math.pow(rx + ry, 2)
|
|
||||||
|
|
||||||
const perimeter = Math.PI * (rx + ry) * (1 + (3 * h) / (10 + Math.sqrt(4 - 3 * h)))
|
|
||||||
|
|
||||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
|
||||||
perimeter,
|
|
||||||
strokeWidth * 1.618,
|
|
||||||
shape.style.dash,
|
|
||||||
4
|
|
||||||
)
|
|
||||||
|
|
||||||
const sw = strokeWidth * 1.618
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{isBinding && (
|
|
||||||
<ellipse
|
|
||||||
className="tl-binding-indicator"
|
|
||||||
cx={radiusX}
|
|
||||||
cy={radiusY}
|
|
||||||
rx={rx + 32}
|
|
||||||
ry={ry + 32}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<ellipse
|
|
||||||
cx={radiusX}
|
|
||||||
cy={radiusY}
|
|
||||||
rx={rx}
|
|
||||||
ry={ry}
|
|
||||||
fill={styles.fill}
|
|
||||||
stroke={styles.stroke}
|
|
||||||
strokeWidth={sw}
|
|
||||||
strokeDasharray={strokeDasharray}
|
|
||||||
strokeDashoffset={strokeDashoffset}
|
|
||||||
pointerEvents="all"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
renderIndicator(shape: EllipseShape) {
|
renderIndicator(shape: EllipseShape) {
|
||||||
const {
|
const {
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { TLBounds, Utils, Vec, TLTransformInfo, Intersect } from '@tldraw/core'
|
import { TLBounds, Utils, Vec, Intersect, TLShapeProps } from '@tldraw/core'
|
||||||
import { defaultStyle, getPerfectDashProps } from '~shape/shape-styles'
|
import { defaultStyle, getPerfectDashProps } from '~shape/shape-styles'
|
||||||
import {
|
import {
|
||||||
GroupShape,
|
GroupShape,
|
||||||
TLDrawShapeUtil,
|
TLDrawShapeUtil,
|
||||||
TLDrawShapeType,
|
TLDrawShapeType,
|
||||||
TLDrawToolType,
|
TLDrawToolType,
|
||||||
TLDrawRenderInfo,
|
|
||||||
ColorStyle,
|
ColorStyle,
|
||||||
DashStyle,
|
DashStyle,
|
||||||
ArrowShape,
|
ArrowShape,
|
||||||
|
@ -15,7 +14,7 @@ import {
|
||||||
// TODO
|
// TODO
|
||||||
// [ ] - Find bounds based on common bounds of descendants
|
// [ ] - Find bounds based on common bounds of descendants
|
||||||
|
|
||||||
export class Group extends TLDrawShapeUtil<GroupShape> {
|
export class Group extends TLDrawShapeUtil<GroupShape, SVGGElement> {
|
||||||
type = TLDrawShapeType.Group as const
|
type = TLDrawShapeType.Group as const
|
||||||
toolType = TLDrawToolType.Bounds
|
toolType = TLDrawToolType.Bounds
|
||||||
canBind = true
|
canBind = true
|
||||||
|
@ -39,59 +38,68 @@ export class Group extends TLDrawShapeUtil<GroupShape> {
|
||||||
return next.size !== prev.size || next.style !== prev.style
|
return next.size !== prev.size || next.style !== prev.style
|
||||||
}
|
}
|
||||||
|
|
||||||
render(shape: GroupShape, { isBinding, isHovered, isSelected }: TLDrawRenderInfo) {
|
render = React.forwardRef<SVGGElement, TLShapeProps<GroupShape, SVGGElement>>(
|
||||||
const { id, size } = shape
|
({ shape, isBinding, isHovered, isSelected, events }) => {
|
||||||
|
const { id, size } = shape
|
||||||
|
|
||||||
const sw = 2
|
const sw = 2
|
||||||
const w = Math.max(0, size[0] - sw / 2)
|
const w = Math.max(0, size[0] - sw / 2)
|
||||||
const h = Math.max(0, size[1] - sw / 2)
|
const h = Math.max(0, size[1] - sw / 2)
|
||||||
|
|
||||||
const strokes: [number[], number[], number][] = [
|
const strokes: [number[], number[], number][] = [
|
||||||
[[sw / 2, sw / 2], [w, sw / 2], w - sw / 2],
|
[[sw / 2, sw / 2], [w, sw / 2], w - sw / 2],
|
||||||
[[w, sw / 2], [w, h], h - sw / 2],
|
[[w, sw / 2], [w, h], h - sw / 2],
|
||||||
[[w, h], [sw / 2, h], w - sw / 2],
|
[[w, h], [sw / 2, h], w - sw / 2],
|
||||||
[[sw / 2, h], [sw / 2, sw / 2], h - sw / 2],
|
[[sw / 2, h], [sw / 2, sw / 2], h - sw / 2],
|
||||||
]
|
]
|
||||||
|
|
||||||
const paths = strokes.map(([start, end, length], i) => {
|
const paths = strokes.map(([start, end, length], i) => {
|
||||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||||
length,
|
length,
|
||||||
sw,
|
sw,
|
||||||
DashStyle.Dotted
|
DashStyle.Dotted
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<line
|
||||||
|
key={id + '_' + i}
|
||||||
|
x1={start[0]}
|
||||||
|
y1={start[1]}
|
||||||
|
x2={end[0]}
|
||||||
|
y2={end[1]}
|
||||||
|
stroke={ColorStyle.Black}
|
||||||
|
strokeWidth={isHovered || isSelected ? sw : 0}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeDasharray={strokeDasharray}
|
||||||
|
strokeDashoffset={strokeDashoffset}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<line
|
<g {...events}>
|
||||||
key={id + '_' + i}
|
{isBinding && (
|
||||||
x1={start[0]}
|
<rect
|
||||||
y1={start[1]}
|
className="tl-binding-indicator"
|
||||||
x2={end[0]}
|
x={-32}
|
||||||
y2={end[1]}
|
y={-32}
|
||||||
stroke={ColorStyle.Black}
|
width={size[0] + 64}
|
||||||
strokeWidth={isHovered || isSelected ? sw : 0}
|
height={size[1] + 64}
|
||||||
strokeLinecap="round"
|
/>
|
||||||
strokeDasharray={strokeDasharray}
|
)}
|
||||||
strokeDashoffset={strokeDashoffset}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{isBinding && (
|
|
||||||
<rect
|
<rect
|
||||||
className="tl-binding-indicator"
|
x={0}
|
||||||
x={-32}
|
y={0}
|
||||||
y={-32}
|
width={size[0]}
|
||||||
width={size[0] + 64}
|
height={size[1]}
|
||||||
height={size[1] + 64}
|
fill="transparent"
|
||||||
|
pointerEvents="all"
|
||||||
/>
|
/>
|
||||||
)}
|
<g pointerEvents="stroke">{paths}</g>
|
||||||
<rect x={0} y={0} width={size[0]} height={size[1]} fill="transparent" pointerEvents="all" />
|
</g>
|
||||||
<g pointerEvents="stroke">{paths}</g>
|
)
|
||||||
</>
|
}
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
renderIndicator(shape: GroupShape) {
|
renderIndicator(shape: GroupShape) {
|
||||||
const [width, height] = shape.size
|
const [width, height] = shape.size
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { TLBounds, Utils, Vec, TLTransformInfo, Intersect } from '@tldraw/core'
|
import { TLBounds, Utils, Vec, TLTransformInfo, Intersect, TLShapeProps } from '@tldraw/core'
|
||||||
import getStroke from 'perfect-freehand'
|
import getStroke from 'perfect-freehand'
|
||||||
import { getPerfectDashProps, defaultStyle, getShapeStyle } from '~shape/shape-styles'
|
import { getPerfectDashProps, defaultStyle, getShapeStyle } from '~shape/shape-styles'
|
||||||
import {
|
import {
|
||||||
|
@ -8,18 +8,16 @@ import {
|
||||||
TLDrawShapeUtil,
|
TLDrawShapeUtil,
|
||||||
TLDrawShapeType,
|
TLDrawShapeType,
|
||||||
TLDrawToolType,
|
TLDrawToolType,
|
||||||
TLDrawRenderInfo,
|
|
||||||
ArrowShape,
|
ArrowShape,
|
||||||
} from '~types'
|
} from '~types'
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
// [ ] - Make sure that fill does not extend drawn shape at corners
|
// [ ] - Make sure that fill does not extend drawn shape at corners
|
||||||
|
|
||||||
export class Rectangle extends TLDrawShapeUtil<RectangleShape> {
|
export class Rectangle extends TLDrawShapeUtil<RectangleShape, SVGGElement> {
|
||||||
type = TLDrawShapeType.Rectangle as const
|
type = TLDrawShapeType.Rectangle as const
|
||||||
toolType = TLDrawToolType.Bounds
|
toolType = TLDrawToolType.Bounds
|
||||||
canBind = true
|
canBind = true
|
||||||
|
|
||||||
pathCache = new WeakMap<number[], string>([])
|
pathCache = new WeakMap<number[], string>([])
|
||||||
|
|
||||||
defaultProps: RectangleShape = {
|
defaultProps: RectangleShape = {
|
||||||
|
@ -38,105 +36,111 @@ export class Rectangle extends TLDrawShapeUtil<RectangleShape> {
|
||||||
return next.size !== prev.size || next.style !== prev.style
|
return next.size !== prev.size || next.style !== prev.style
|
||||||
}
|
}
|
||||||
|
|
||||||
render(shape: RectangleShape, { isBinding, meta }: TLDrawRenderInfo) {
|
render = React.forwardRef<SVGGElement, TLShapeProps<RectangleShape, SVGGElement>>(
|
||||||
const { id, size, style } = shape
|
({ shape, isBinding, meta, events }, ref) => {
|
||||||
const styles = getShapeStyle(style, meta.isDarkMode)
|
const { id, size, style } = shape
|
||||||
const strokeWidth = +styles.strokeWidth
|
const styles = getShapeStyle(style, meta.isDarkMode)
|
||||||
|
const strokeWidth = +styles.strokeWidth
|
||||||
|
|
||||||
if (style.dash === DashStyle.Draw) {
|
React.useEffect(() => {
|
||||||
const pathData = Utils.getFromCache(this.pathCache, shape.size, () => renderPath(shape))
|
console.log(this.refMap.get(shape.id))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (style.dash === DashStyle.Draw) {
|
||||||
|
const pathData = Utils.getFromCache(this.pathCache, shape.size, () => renderPath(shape))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g ref={ref} {...events}>
|
||||||
|
{isBinding && (
|
||||||
|
<rect
|
||||||
|
className="tl-binding-indicator"
|
||||||
|
x={strokeWidth / 2 - 32}
|
||||||
|
y={strokeWidth / 2 - 32}
|
||||||
|
width={Math.max(0, size[0] - strokeWidth / 2) + 64}
|
||||||
|
height={Math.max(0, size[1] - strokeWidth / 2) + 64}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<rect
|
||||||
|
x={+styles.strokeWidth / 2}
|
||||||
|
y={+styles.strokeWidth / 2}
|
||||||
|
width={Math.max(0, size[0] - strokeWidth)}
|
||||||
|
height={Math.max(0, size[1] - strokeWidth)}
|
||||||
|
fill={style.isFilled ? styles.fill : 'none'}
|
||||||
|
stroke="none"
|
||||||
|
pointerEvents="all"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d={pathData}
|
||||||
|
fill={styles.stroke}
|
||||||
|
stroke={styles.stroke}
|
||||||
|
strokeWidth={styles.strokeWidth}
|
||||||
|
pointerEvents="all"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sw = strokeWidth * 1.618
|
||||||
|
|
||||||
|
const w = Math.max(0, size[0] - sw / 2)
|
||||||
|
const h = Math.max(0, size[1] - sw / 2)
|
||||||
|
|
||||||
|
const strokes: [number[], number[], number][] = [
|
||||||
|
[[sw / 2, sw / 2], [w, sw / 2], w - sw / 2],
|
||||||
|
[[w, sw / 2], [w, h], h - sw / 2],
|
||||||
|
[[w, h], [sw / 2, h], w - sw / 2],
|
||||||
|
[[sw / 2, h], [sw / 2, sw / 2], h - sw / 2],
|
||||||
|
]
|
||||||
|
|
||||||
|
const paths = strokes.map(([start, end, length], i) => {
|
||||||
|
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||||
|
length,
|
||||||
|
sw,
|
||||||
|
shape.style.dash
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<line
|
||||||
|
key={id + '_' + i}
|
||||||
|
x1={start[0]}
|
||||||
|
y1={start[1]}
|
||||||
|
x2={end[0]}
|
||||||
|
y2={end[1]}
|
||||||
|
stroke={styles.stroke}
|
||||||
|
strokeWidth={sw}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeDasharray={strokeDasharray}
|
||||||
|
strokeDashoffset={strokeDashoffset}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<g ref={ref} {...events}>
|
||||||
{isBinding && (
|
{isBinding && (
|
||||||
<rect
|
<rect
|
||||||
className="tl-binding-indicator"
|
className="tl-binding-indicator"
|
||||||
x={strokeWidth / 2 - 32}
|
x={sw / 2 - 32}
|
||||||
y={strokeWidth / 2 - 32}
|
y={sw / 2 - 32}
|
||||||
width={Math.max(0, size[0] - strokeWidth / 2) + 64}
|
width={w + 64}
|
||||||
height={Math.max(0, size[1] - strokeWidth / 2) + 64}
|
height={h + 64}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<rect
|
<rect
|
||||||
x={+styles.strokeWidth / 2}
|
x={sw / 2}
|
||||||
y={+styles.strokeWidth / 2}
|
y={sw / 2}
|
||||||
width={Math.max(0, size[0] - strokeWidth)}
|
width={w}
|
||||||
height={Math.max(0, size[1] - strokeWidth)}
|
height={h}
|
||||||
fill={style.isFilled ? styles.fill : 'none'}
|
fill={styles.fill}
|
||||||
stroke="none"
|
stroke="transparent"
|
||||||
|
strokeWidth={sw}
|
||||||
pointerEvents="all"
|
pointerEvents="all"
|
||||||
/>
|
/>
|
||||||
<path
|
<g pointerEvents="stroke">{paths}</g>
|
||||||
d={pathData}
|
</g>
|
||||||
fill={styles.stroke}
|
|
||||||
stroke={styles.stroke}
|
|
||||||
strokeWidth={styles.strokeWidth}
|
|
||||||
pointerEvents="all"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
const sw = strokeWidth * 1.618
|
|
||||||
|
|
||||||
const w = Math.max(0, size[0] - sw / 2)
|
|
||||||
const h = Math.max(0, size[1] - sw / 2)
|
|
||||||
|
|
||||||
const strokes: [number[], number[], number][] = [
|
|
||||||
[[sw / 2, sw / 2], [w, sw / 2], w - sw / 2],
|
|
||||||
[[w, sw / 2], [w, h], h - sw / 2],
|
|
||||||
[[w, h], [sw / 2, h], w - sw / 2],
|
|
||||||
[[sw / 2, h], [sw / 2, sw / 2], h - sw / 2],
|
|
||||||
]
|
|
||||||
|
|
||||||
const paths = strokes.map(([start, end, length], i) => {
|
|
||||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
|
||||||
length,
|
|
||||||
sw,
|
|
||||||
shape.style.dash
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<line
|
|
||||||
key={id + '_' + i}
|
|
||||||
x1={start[0]}
|
|
||||||
y1={start[1]}
|
|
||||||
x2={end[0]}
|
|
||||||
y2={end[1]}
|
|
||||||
stroke={styles.stroke}
|
|
||||||
strokeWidth={sw}
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeDasharray={strokeDasharray}
|
|
||||||
strokeDashoffset={strokeDashoffset}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{isBinding && (
|
|
||||||
<rect
|
|
||||||
className="tl-binding-indicator"
|
|
||||||
x={sw / 2 - 32}
|
|
||||||
y={sw / 2 - 32}
|
|
||||||
width={w + 64}
|
|
||||||
height={h + 64}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<rect
|
|
||||||
x={sw / 2}
|
|
||||||
y={sw / 2}
|
|
||||||
width={w}
|
|
||||||
height={h}
|
|
||||||
fill={styles.fill}
|
|
||||||
stroke="transparent"
|
|
||||||
strokeWidth={sw}
|
|
||||||
pointerEvents="all"
|
|
||||||
/>
|
|
||||||
<g pointerEvents="stroke">{paths}</g>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
renderIndicator(shape: RectangleShape) {
|
renderIndicator(shape: RectangleShape) {
|
||||||
const {
|
const {
|
||||||
|
@ -162,6 +166,7 @@ export class Rectangle extends TLDrawShapeUtil<RectangleShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
getBounds(shape: RectangleShape) {
|
getBounds(shape: RectangleShape) {
|
||||||
|
console.log(this.refMap.get(shape.id))
|
||||||
const bounds = Utils.getFromCache(this.boundsCache, shape, () => {
|
const bounds = Utils.getFromCache(this.boundsCache, shape, () => {
|
||||||
const [width, height] = shape.size
|
const [width, height] = shape.size
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,14 +1,7 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { TLBounds, Utils, Vec, TLTransformInfo, Intersect } from '@tldraw/core'
|
import { TLBounds, Utils, Vec, TLTransformInfo, Intersect, TLShapeProps } from '@tldraw/core'
|
||||||
import { getShapeStyle, getFontSize, getFontStyle, defaultStyle } from '~shape/shape-styles'
|
import { getShapeStyle, getFontSize, getFontStyle, defaultStyle } from '~shape/shape-styles'
|
||||||
import {
|
import { TextShape, TLDrawShapeUtil, TLDrawShapeType, TLDrawToolType, ArrowShape } from '~types'
|
||||||
TextShape,
|
|
||||||
TLDrawShapeUtil,
|
|
||||||
TLDrawShapeType,
|
|
||||||
TLDrawRenderInfo,
|
|
||||||
TLDrawToolType,
|
|
||||||
ArrowShape,
|
|
||||||
} from '~types'
|
|
||||||
import styled from '~styles'
|
import styled from '~styles'
|
||||||
import TextAreaUtils from './text-utils'
|
import TextAreaUtils from './text-utils'
|
||||||
|
|
||||||
|
@ -56,7 +49,7 @@ if (typeof window !== 'undefined') {
|
||||||
melm = getMeasurementDiv()
|
melm = getMeasurementDiv()
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Text extends TLDrawShapeUtil<TextShape> {
|
export class Text extends TLDrawShapeUtil<TextShape, SVGGElement> {
|
||||||
type = TLDrawShapeType.Text as const
|
type = TLDrawShapeType.Text as const
|
||||||
toolType = TLDrawToolType.Text
|
toolType = TLDrawToolType.Text
|
||||||
isAspectRatioLocked = true
|
isAspectRatioLocked = true
|
||||||
|
@ -90,156 +83,144 @@ export class Text extends TLDrawShapeUtil<TextShape> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
render(
|
render = React.forwardRef<SVGGElement, TLShapeProps<TextShape, SVGGElement>>(
|
||||||
shape: TextShape,
|
({ shape, meta, isEditing, isBinding, events }, ref) => {
|
||||||
{
|
const rInput = React.useRef<HTMLTextAreaElement>(null)
|
||||||
ref,
|
const { id, text, style } = shape
|
||||||
meta,
|
const styles = getShapeStyle(style, meta.isDarkMode)
|
||||||
isEditing,
|
const font = getFontStyle(shape.style)
|
||||||
isBinding,
|
const bounds = this.getBounds(shape)
|
||||||
onTextBlur,
|
|
||||||
onTextChange,
|
|
||||||
onTextFocus,
|
|
||||||
onTextKeyDown,
|
|
||||||
onTextKeyUp,
|
|
||||||
}: TLDrawRenderInfo
|
|
||||||
): JSX.Element {
|
|
||||||
const { id, text, style } = shape
|
|
||||||
const styles = getShapeStyle(style, meta.isDarkMode)
|
|
||||||
const font = getFontStyle(shape.style)
|
|
||||||
const bounds = this.getBounds(shape)
|
|
||||||
|
|
||||||
function handleChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
|
function handleChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
|
||||||
onTextChange?.(id, normalizeText(e.currentTarget.value))
|
events.onTextChange?.(id, normalizeText(e.currentTarget.value))
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
|
function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
|
||||||
onTextKeyDown?.(id, e.key)
|
events.onTextKeyDown?.(id, e.key)
|
||||||
|
|
||||||
if (e.key === 'Escape') return
|
if (e.key === 'Escape') return
|
||||||
|
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
if (e.key === 'Tab') {
|
if (e.key === 'Tab') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
TextAreaUtils.unindent(e.currentTarget)
|
TextAreaUtils.unindent(e.currentTarget)
|
||||||
} else {
|
} else {
|
||||||
TextAreaUtils.indent(e.currentTarget)
|
TextAreaUtils.indent(e.currentTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
events.onTextChange?.(id, normalizeText(e.currentTarget.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyUp(e: React.KeyboardEvent<HTMLTextAreaElement>) {
|
||||||
|
events.onTextKeyUp?.(id, e.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBlur(e: React.FocusEvent<HTMLTextAreaElement>) {
|
||||||
|
if (isEditing) {
|
||||||
|
e.currentTarget.focus()
|
||||||
|
e.currentTarget.select()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
onTextChange?.(id, normalizeText(e.currentTarget.value))
|
setTimeout(() => {
|
||||||
}
|
events.onTextBlur?.(id)
|
||||||
}
|
}, 0)
|
||||||
|
|
||||||
function handleKeyUp(e: React.KeyboardEvent<HTMLTextAreaElement>) {
|
|
||||||
onTextKeyUp?.(id, e.key)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleBlur(e: React.FocusEvent<HTMLTextAreaElement>) {
|
|
||||||
if (isEditing) {
|
|
||||||
e.currentTarget.focus()
|
|
||||||
e.currentTarget.select()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
function handleFocus(e: React.FocusEvent<HTMLTextAreaElement>) {
|
||||||
onTextBlur?.(id)
|
if (document.activeElement === e.currentTarget) {
|
||||||
}, 0)
|
e.currentTarget.select()
|
||||||
}
|
events.onTextFocus?.(id)
|
||||||
|
}
|
||||||
function handleFocus(e: React.FocusEvent<HTMLTextAreaElement>) {
|
|
||||||
if (document.activeElement === e.currentTarget) {
|
|
||||||
e.currentTarget.select()
|
|
||||||
onTextFocus?.(id)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function handlePointerDown() {
|
function handlePointerDown() {
|
||||||
if (ref && ref.current.selectionEnd !== 0) {
|
const elm = rInput.current
|
||||||
ref.current.selectionEnd = 0
|
if (!elm) return
|
||||||
|
if (elm.selectionEnd !== 0) {
|
||||||
|
elm.selectionEnd = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const fontSize = getFontSize(shape.style.size) * (shape.style.scale || 1)
|
const fontSize = getFontSize(shape.style.size) * (shape.style.scale || 1)
|
||||||
|
|
||||||
const lineHeight = fontSize * 1.3
|
const lineHeight = fontSize * 1.3
|
||||||
|
|
||||||
|
if (!isEditing) {
|
||||||
|
return (
|
||||||
|
<g ref={ref} {...events}>
|
||||||
|
{isBinding && (
|
||||||
|
<rect
|
||||||
|
className="tl-binding-indicator"
|
||||||
|
x={-16}
|
||||||
|
y={-16}
|
||||||
|
width={bounds.width + 32}
|
||||||
|
height={bounds.height + 32}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{text.split('\n').map((str, i) => (
|
||||||
|
<text
|
||||||
|
key={i}
|
||||||
|
x={4}
|
||||||
|
y={4 + fontSize / 2 + i * lineHeight}
|
||||||
|
fontFamily="Caveat Brush"
|
||||||
|
fontStyle="normal"
|
||||||
|
fontWeight="500"
|
||||||
|
letterSpacing={LETTER_SPACING}
|
||||||
|
fontSize={fontSize}
|
||||||
|
width={bounds.width}
|
||||||
|
height={bounds.height}
|
||||||
|
fill={styles.stroke}
|
||||||
|
color={styles.stroke}
|
||||||
|
stroke="none"
|
||||||
|
xmlSpace="preserve"
|
||||||
|
dominantBaseline="mathematical"
|
||||||
|
alignmentBaseline="mathematical"
|
||||||
|
>
|
||||||
|
{str}
|
||||||
|
</text>
|
||||||
|
))}
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (!isEditing) {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<foreignObject
|
||||||
{isBinding && (
|
width={bounds.width}
|
||||||
<rect
|
height={bounds.height}
|
||||||
className="tl-binding-indicator"
|
pointerEvents="none"
|
||||||
x={-16}
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
y={-16}
|
>
|
||||||
width={bounds.width + 32}
|
<StyledTextArea
|
||||||
height={bounds.height + 32}
|
ref={rInput}
|
||||||
/>
|
style={{
|
||||||
)}
|
font,
|
||||||
{text.split('\n').map((str, i) => (
|
color: styles.stroke,
|
||||||
<text
|
}}
|
||||||
key={i}
|
name="text"
|
||||||
x={4}
|
defaultValue={text}
|
||||||
y={4 + fontSize / 2 + i * lineHeight}
|
tabIndex={-1}
|
||||||
fontFamily="Caveat Brush"
|
autoComplete="false"
|
||||||
fontStyle="normal"
|
autoCapitalize="false"
|
||||||
fontWeight="500"
|
autoCorrect="false"
|
||||||
letterSpacing={LETTER_SPACING}
|
autoSave="false"
|
||||||
fontSize={fontSize}
|
placeholder=""
|
||||||
width={bounds.width}
|
color={styles.stroke}
|
||||||
height={bounds.height}
|
autoFocus={true}
|
||||||
fill={styles.stroke}
|
onFocus={handleFocus}
|
||||||
color={styles.stroke}
|
onBlur={handleBlur}
|
||||||
stroke="none"
|
onKeyDown={handleKeyDown}
|
||||||
xmlSpace="preserve"
|
onKeyUp={handleKeyUp}
|
||||||
dominantBaseline="mathematical"
|
onChange={handleChange}
|
||||||
alignmentBaseline="mathematical"
|
onPointerDown={handlePointerDown}
|
||||||
>
|
/>
|
||||||
{str}
|
</foreignObject>
|
||||||
</text>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
if (ref === undefined) {
|
|
||||||
throw Error('This component should receive a ref when editing.')
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<foreignObject
|
|
||||||
width={bounds.width}
|
|
||||||
height={bounds.height}
|
|
||||||
pointerEvents="none"
|
|
||||||
onPointerDown={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<StyledTextArea
|
|
||||||
ref={ref as React.RefObject<HTMLTextAreaElement>}
|
|
||||||
style={{
|
|
||||||
font,
|
|
||||||
color: styles.stroke,
|
|
||||||
}}
|
|
||||||
name="text"
|
|
||||||
defaultValue={text}
|
|
||||||
tabIndex={-1}
|
|
||||||
autoComplete="false"
|
|
||||||
autoCapitalize="false"
|
|
||||||
autoCorrect="false"
|
|
||||||
autoSave="false"
|
|
||||||
placeholder=""
|
|
||||||
color={styles.stroke}
|
|
||||||
autoFocus={true}
|
|
||||||
onFocus={handleFocus}
|
|
||||||
onBlur={handleBlur}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onKeyUp={handleKeyUp}
|
|
||||||
onChange={handleChange}
|
|
||||||
onPointerDown={handlePointerDown}
|
|
||||||
/>
|
|
||||||
</foreignObject>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
renderIndicator(): JSX.Element | null {
|
renderIndicator(): JSX.Element | null {
|
||||||
return null
|
return null
|
||||||
|
|
|
@ -13,7 +13,9 @@ import type {
|
||||||
} from '~types'
|
} from '~types'
|
||||||
|
|
||||||
export class TLDR {
|
export class TLDR {
|
||||||
static getShapeUtils<T extends TLDrawShape>(shape: T | T['type']): TLDrawShapeUtil<T> {
|
static getShapeUtils<T extends TLDrawShape>(
|
||||||
|
shape: T | T['type']
|
||||||
|
): TLDrawShapeUtil<T, HTMLElement | SVGElement> {
|
||||||
return getShapeUtils(typeof shape === 'string' ? ({ type: shape } as T) : shape)
|
return getShapeUtils(typeof shape === 'string' ? ({ type: shape } as T) : shape)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -72,7 +72,7 @@ const defaultState: Data = {
|
||||||
settings: {
|
settings: {
|
||||||
isPenMode: false,
|
isPenMode: false,
|
||||||
isDarkMode: false,
|
isDarkMode: false,
|
||||||
isZoomSnap: true,
|
isZoomSnap: false,
|
||||||
isDebugMode: process.env.NODE_ENV === 'development',
|
isDebugMode: process.env.NODE_ENV === 'development',
|
||||||
isReadonlyMode: false,
|
isReadonlyMode: false,
|
||||||
nudgeDistanceLarge: 10,
|
nudgeDistanceLarge: 10,
|
||||||
|
|
|
@ -199,11 +199,17 @@ export type TLDrawShape =
|
||||||
| TextShape
|
| TextShape
|
||||||
| GroupShape
|
| GroupShape
|
||||||
|
|
||||||
export abstract class TLDrawShapeUtil<T extends TLDrawShape> extends TLShapeUtil<T> {
|
export abstract class TLDrawShapeUtil<
|
||||||
|
T extends TLDrawShape,
|
||||||
|
E extends HTMLElement | SVGElement
|
||||||
|
> extends TLShapeUtil<T, E> {
|
||||||
abstract toolType: TLDrawToolType
|
abstract toolType: TLDrawToolType
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TLDrawShapeUtils = Record<TLDrawShapeType, TLDrawShapeUtil<TLDrawShape>>
|
export type TLDrawShapeUtils = Record<
|
||||||
|
TLDrawShapeType,
|
||||||
|
TLDrawShapeUtil<TLDrawShape, HTMLElement | SVGElement>
|
||||||
|
>
|
||||||
|
|
||||||
export interface ArrowBinding extends TLBinding {
|
export interface ArrowBinding extends TLBinding {
|
||||||
type: 'arrow'
|
type: 'arrow'
|
||||||
|
|
Loading…
Reference in a new issue