tldraw/lib/shape-utils/index.tsx

389 lines
8.7 KiB
TypeScript
Raw Normal View History

2021-05-15 13:02:13 +00:00
import {
Bounds,
Shape,
ShapeType,
Corner,
Edge,
2021-05-26 10:34:10 +00:00
ShapeStyles,
2021-05-31 19:13:43 +00:00
ShapeBinding,
2021-06-02 15:58:51 +00:00
Mutable,
2021-06-04 16:08:43 +00:00
ShapeByType,
} from 'types'
import vec from 'utils/vec'
2021-06-02 15:58:51 +00:00
import {
getBoundsCenter,
getBoundsFromPoints,
getRotatedCorners,
} from 'utils/utils'
import {
boundsCollidePolygon,
boundsContainPolygon,
pointInBounds,
} from 'utils/bounds'
import { uniqueId } from 'utils/utils'
2021-06-04 16:08:43 +00:00
import circle from './circle'
import dot from './dot'
import polyline from './polyline'
import rectangle from './rectangle'
import ellipse from './ellipse'
import line from './line'
import ray from './ray'
import draw from './draw'
import arrow from './arrow'
import group from './group'
import text from './text'
2021-06-17 10:43:55 +00:00
import React from 'react'
2021-05-14 12:44:23 +00:00
/*
Shape Utiliies
A shape utility is an object containing utility methods for each type of shape
in the application. While shapes may be very different, each shape must support
a common set of utility methods, such as hit tests or translations, that
Operations throughout the app will call these utility methods
when performing tests (such as hit tests) or mutations, such as translations.
*/
2021-06-02 15:05:44 +00:00
export interface ShapeUtility<K extends Shape> {
2021-05-14 12:44:23 +00:00
// A cache for the computed bounds of this kind of shape.
boundsCache: WeakMap<K, Bounds>
// Whether to show transform controls when this shape is selected.
canTransform: boolean
2021-05-18 08:32:20 +00:00
// Whether the shape's aspect ratio can change.
canChangeAspectRatio: boolean
2021-05-17 21:27:18 +00:00
// Whether the shape's style can be filled.
2021-06-01 21:49:32 +00:00
canStyleFill: boolean
// Whether the shape may be edited in an editing mode
canEdit: boolean
// Whether the shape is a foreign object.
isForeignObject: boolean
// Whether the shape can contain other shapes.
isParent: boolean
// Whether the shape is only shown when on hovered.
isShy: boolean
// Create a new shape.
create(props: Partial<K>): K
2021-05-14 12:44:23 +00:00
2021-06-05 07:42:17 +00:00
// Update a shape's styles
2021-05-26 10:34:10 +00:00
applyStyles(
this: ShapeUtility<K>,
2021-06-02 15:58:51 +00:00
shape: Mutable<K>,
style: Partial<ShapeStyles>
2021-05-26 10:34:10 +00:00
): ShapeUtility<K>
2021-06-04 16:08:43 +00:00
translateBy(
this: ShapeUtility<K>,
shape: Mutable<K>,
point: number[]
): ShapeUtility<K>
translateTo(
this: ShapeUtility<K>,
shape: Mutable<K>,
point: number[]
): ShapeUtility<K>
2021-06-05 07:42:17 +00:00
rotateBy(
this: ShapeUtility<K>,
shape: Mutable<K>,
rotation: number
): ShapeUtility<K>
rotateTo(
this: ShapeUtility<K>,
shape: Mutable<K>,
rotation: number,
delta: number
): ShapeUtility<K>
// Transform to fit a new bounding box when more than one shape is selected.
2021-05-14 21:05:21 +00:00
transform(
this: ShapeUtility<K>,
2021-06-02 15:58:51 +00:00
shape: Mutable<K>,
2021-05-15 13:02:13 +00:00
bounds: Bounds,
info: {
2021-05-23 13:46:04 +00:00
type: Edge | Corner
2021-05-15 13:02:13 +00:00
initialShape: K
scaleX: number
scaleY: number
transformOrigin: number[]
2021-05-15 13:02:13 +00:00
}
): ShapeUtility<K>
2021-05-14 12:44:23 +00:00
// Transform a single shape to fit a new bounding box.
2021-05-19 09:35:00 +00:00
transformSingle(
this: ShapeUtility<K>,
2021-06-02 15:58:51 +00:00
shape: Mutable<K>,
2021-05-19 09:35:00 +00:00
bounds: Bounds,
info: {
2021-05-23 13:46:04 +00:00
type: Edge | Corner
2021-05-19 09:35:00 +00:00
initialShape: K
scaleX: number
scaleY: number
transformOrigin: number[]
2021-05-19 09:35:00 +00:00
}
): ShapeUtility<K>
2021-05-19 09:35:00 +00:00
setProperty<P extends keyof K>(
2021-05-27 17:59:40 +00:00
this: ShapeUtility<K>,
2021-06-02 15:58:51 +00:00
shape: Mutable<K>,
prop: P,
value: K[P]
2021-05-27 17:59:40 +00:00
): ShapeUtility<K>
2021-06-04 16:08:43 +00:00
// Respond when any child of this shape changes.
onChildrenChange(
this: ShapeUtility<K>,
shape: Mutable<K>,
children: Shape[]
): ShapeUtility<K>
2021-05-31 19:13:43 +00:00
// Respond when a user moves one of the shape's bound elements.
2021-06-04 16:08:43 +00:00
onBindingChange(
2021-05-31 19:13:43 +00:00
this: ShapeUtility<K>,
2021-06-02 15:58:51 +00:00
shape: Mutable<K>,
2021-05-31 19:13:43 +00:00
bindings: Record<string, ShapeBinding>
): ShapeUtility<K>
// Respond when a user moves one of the shape's handles.
2021-06-04 16:08:43 +00:00
onHandleChange(
2021-05-31 19:13:43 +00:00
this: ShapeUtility<K>,
2021-06-02 15:58:51 +00:00
shape: Mutable<K>,
2021-05-31 19:13:43 +00:00
handle: Partial<K['handles']>
): ShapeUtility<K>
// Respond when a user double clicks the shape's bounds.
onBoundsReset(this: ShapeUtility<K>, shape: Mutable<K>): ShapeUtility<K>
// Respond when a user double clicks the center of the shape.
onDoubleFocus(this: ShapeUtility<K>, shape: Mutable<K>): ShapeUtility<K>
2021-06-05 14:29:49 +00:00
// Clean up changes when a session ends.
onSessionComplete(this: ShapeUtility<K>, shape: Mutable<K>): ShapeUtility<K>
2021-05-14 12:44:23 +00:00
// Render a shape to JSX.
render(
this: ShapeUtility<K>,
shape: K,
2021-06-17 10:43:55 +00:00
info: {
isEditing: boolean
ref: React.MutableRefObject<HTMLTextAreaElement>
}
): JSX.Element
2021-05-15 13:02:13 +00:00
2021-06-17 10:43:55 +00:00
invalidate(this: ShapeUtility<K>, shape: K): ShapeUtility<K>
// Get the bounds of the a shape.
getBounds(this: ShapeUtility<K>, shape: K): Bounds
2021-05-23 13:46:04 +00:00
// Get the routated bounds of the a shape.
getRotatedBounds(this: ShapeUtility<K>, shape: K): Bounds
// Get the center of the shape
getCenter(this: ShapeUtility<K>, shape: K): number[]
// Test whether a point lies within a shape.
hitTest(this: ShapeUtility<K>, shape: K, test: number[]): boolean
// Test whether bounds collide with or contain a shape.
hitTestBounds(this: ShapeUtility<K>, shape: K, bounds: Bounds): boolean
2021-06-17 10:43:55 +00:00
shouldDelete(this: ShapeUtility<K>, shape: K): boolean
2021-05-14 12:44:23 +00:00
}
// A mapping of shape types to shape utilities.
const shapeUtilityMap: Record<ShapeType, ShapeUtility<Shape>> = {
2021-05-14 12:44:23 +00:00
[ShapeType.Circle]: circle,
[ShapeType.Dot]: dot,
[ShapeType.Polyline]: polyline,
[ShapeType.Rectangle]: rectangle,
[ShapeType.Ellipse]: ellipse,
[ShapeType.Line]: line,
[ShapeType.Ray]: ray,
2021-05-27 17:59:40 +00:00
[ShapeType.Draw]: draw,
2021-05-31 19:13:43 +00:00
[ShapeType.Arrow]: arrow,
[ShapeType.Text]: text,
2021-06-04 16:08:43 +00:00
[ShapeType.Group]: group,
2021-05-14 12:44:23 +00:00
}
/**
* A helper to retrieve a shape utility based on a shape object.
* @param shape
* @returns
*/
export function getShapeUtils<T extends Shape>(shape: T): ShapeUtility<T> {
return shapeUtilityMap[shape?.type] as ShapeUtility<T>
2021-05-14 12:44:23 +00:00
}
2021-06-02 15:58:51 +00:00
function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
return {
boundsCache: new WeakMap(),
canTransform: true,
canChangeAspectRatio: true,
canStyleFill: true,
canEdit: false,
isShy: false,
isParent: false,
isForeignObject: false,
2021-06-02 15:58:51 +00:00
create(props) {
return {
id: uniqueId(),
2021-06-02 15:58:51 +00:00
isGenerated: false,
point: [0, 0],
name: 'Shape',
parentId: 'page1',
2021-06-02 15:58:51 +00:00
childIndex: 0,
rotation: 0,
isAspectRatioLocked: false,
isLocked: false,
isHidden: false,
...props,
} as T
},
render(shape) {
return <circle id={shape.id} />
},
2021-06-04 16:08:43 +00:00
translateBy(shape, delta) {
2021-06-13 13:55:37 +00:00
shape.point = vec.round(vec.add(shape.point, delta))
2021-06-04 16:08:43 +00:00
return this
},
translateTo(shape, point) {
2021-06-13 13:55:37 +00:00
shape.point = vec.round(point)
2021-06-04 16:08:43 +00:00
return this
},
2021-06-05 07:42:17 +00:00
rotateTo(shape, rotation) {
shape.rotation = rotation
return this
},
rotateBy(shape, rotation) {
shape.rotation += rotation
return this
},
2021-06-02 15:58:51 +00:00
transform(shape, bounds) {
shape.point = [bounds.minX, bounds.minY]
return this
},
transformSingle(shape, bounds, info) {
return this.transform(shape, bounds, info)
},
2021-06-04 16:08:43 +00:00
onChildrenChange() {
return this
},
onBindingChange() {
2021-06-02 15:58:51 +00:00
return this
},
2021-06-04 16:08:43 +00:00
onHandleChange() {
2021-06-02 15:58:51 +00:00
return this
},
onDoubleFocus() {
return this
},
onBoundsReset() {
return this
},
2021-06-05 14:29:49 +00:00
onSessionComplete() {
return this
},
2021-06-02 15:58:51 +00:00
getBounds(shape) {
const [x, y] = shape.point
return {
minX: x,
minY: y,
maxX: x + 1,
maxY: y + 1,
width: 1,
height: 1,
}
},
getRotatedBounds(shape) {
return getBoundsFromPoints(
getRotatedCorners(this.getBounds(shape), shape.rotation)
)
},
getCenter(shape) {
return getBoundsCenter(this.getBounds(shape))
},
hitTest(shape, point) {
return pointInBounds(point, this.getBounds(shape))
},
hitTestBounds(shape, brushBounds) {
const rotatedCorners = getRotatedCorners(
this.getBounds(shape),
shape.rotation
)
return (
boundsContainPolygon(brushBounds, rotatedCorners) ||
boundsCollidePolygon(brushBounds, rotatedCorners)
)
},
setProperty(shape, prop, value) {
shape[prop] = value
return this
},
applyStyles(shape, style) {
Object.assign(shape.style, style)
return this
},
2021-06-17 10:43:55 +00:00
shouldDelete(shape) {
return false
},
2021-06-17 10:43:55 +00:00
invalidate(shape) {
this.boundsCache.delete(shape)
return this
},
2021-06-02 15:58:51 +00:00
}
}
2021-05-14 12:44:23 +00:00
/**
* A factory of shape utilities, with typing enforced.
* @param shape
* @returns
*/
2021-06-02 15:58:51 +00:00
export function registerShapeUtils<K extends Shape>(
shapeUtil: Partial<ShapeUtility<K>>
): ShapeUtility<K> {
return Object.freeze({ ...getDefaultShapeUtil<K>(), ...shapeUtil })
2021-05-12 21:11:17 +00:00
}
2021-05-12 22:08:53 +00:00
2021-06-04 16:08:43 +00:00
export function createShape<T extends ShapeType>(
type: T,
props: Partial<ShapeByType<T>>
): ShapeByType<T> {
return shapeUtilityMap[type].create(props) as ShapeByType<T>
}
2021-05-14 12:44:23 +00:00
export default shapeUtilityMap