Tldraw/packages/tldraw/src/state/shapes/TDShapeUtil.tsx

197 wiersze
6.7 KiB
TypeScript

import { TLShapeUtil, Utils } from '@tldraw/core'
import type { TLBounds, TLPointerInfo } from '@tldraw/core'
import {
intersectLineSegmentBounds,
intersectLineSegmentPolyline,
intersectRayBounds,
} from '@tldraw/intersect'
import { Vec } from '@tldraw/vec'
import * as React from 'react'
import { BINDING_DISTANCE } from '~constants'
import type { ShapesWithProp, TDBinding, TDMeta, TDShape, TransformInfo } from '~types'
import { getFontStyle, getShapeStyle } from './shared'
import { getTextLabelSize } from './shared/getTextSize'
import { getTextSvgElement } from './shared/getTextSvgElement'
export abstract class TDShapeUtil<T extends TDShape, E extends Element = any> extends TLShapeUtil<
T,
E,
TDMeta
> {
abstract type: T['type']
canBind = false
canEdit = false
canClone = false
isAspectRatioLocked = false
hideResizeHandles = false
bindingDistance = BINDING_DISTANCE
abstract getShape: (props: Partial<T>) => T
hitTestPoint = (shape: T, point: number[]): boolean => {
return Utils.pointInBounds(point, this.getRotatedBounds(shape))
}
hitTestLineSegment = (shape: T, A: number[], B: number[]): boolean => {
const box = Utils.getBoundsFromPoints([A, B])
const bounds = this.getBounds(shape)
return Utils.boundsContain(bounds, box) || shape.rotation
? intersectLineSegmentPolyline(A, B, Utils.getRotatedCorners(this.getBounds(shape)))
.didIntersect
: intersectLineSegmentBounds(A, B, this.getBounds(shape)).length > 0
}
create = (props: { id: string } & Partial<T>) => {
this.refMap.set(props.id, React.createRef())
return this.getShape(props)
}
getCenter = (shape: T) => {
return Utils.getBoundsCenter(this.getBounds(shape))
}
getExpandedBounds = (shape: T) => {
return Utils.expandBounds(this.getBounds(shape), this.bindingDistance)
}
getBindingPoint = <K extends TDShape>(
shape: T,
fromShape: K,
point: number[],
origin: number[],
direction: number[],
bindAnywhere: boolean
) => {
// Algorithm time! We need to find the binding point (a normalized point inside of the shape, or around the shape, where the arrow will point to) and the distance from the binding shape to the anchor.
const bounds = this.getBounds(shape)
const expandedBounds = this.getExpandedBounds(shape)
// The point must be inside of the expanded bounding box
if (!Utils.pointInBounds(point, expandedBounds)) return
const intersections = intersectRayBounds(origin, direction, expandedBounds)
.filter((int) => int.didIntersect)
.map((int) => int.points[0])
if (!intersections.length) return
// The center of the shape
const center = this.getCenter(shape)
// Find furthest intersection between ray from origin through point and expanded bounds. TODO: What if the shape has a curve? In that case, should we intersect the circle-from-three-points instead?
const intersection = intersections.sort((a, b) => Vec.dist(b, origin) - Vec.dist(a, origin))[0]
// The point between the handle and the intersection
const middlePoint = Vec.med(point, intersection)
// The anchor is the point in the shape where the arrow will be pointing
let anchor: number[]
// The distance is the distance from the anchor to the handle
let distance: number
if (bindAnywhere) {
// If the user is indicating that they want to bind inside of the shape, we just use the handle's point
anchor = Vec.dist(point, center) < BINDING_DISTANCE / 2 ? center : point
distance = 0
} else {
if (Vec.distanceToLineSegment(point, middlePoint, center) < BINDING_DISTANCE / 2) {
// If the line segment would pass near to the center, snap the anchor the center point
anchor = center
} else {
// Otherwise, the anchor is the middle point between the handle and the intersection
anchor = middlePoint
}
if (Utils.pointInBounds(point, bounds)) {
// If the point is inside of the shape, use the shape's binding distance
distance = this.bindingDistance
} else {
// Otherwise, use the actual distance from the handle point to nearest edge
distance = Math.max(
this.bindingDistance,
Utils.getBoundsSides(bounds)
.map((side) => Vec.distanceToLineSegment(side[1][0], side[1][1], point))
.sort((a, b) => a - b)[0]
)
}
}
// The binding point is a normalized point indicating the position of the anchor.
// An anchor at the middle of the shape would be (0.5, 0.5). When the shape's bounds
// changes, we will re-recalculate the actual anchor point by multiplying the
// normalized point by the shape's new bounds.
const bindingPoint = Vec.divV(Vec.sub(anchor, [expandedBounds.minX, expandedBounds.minY]), [
expandedBounds.width,
expandedBounds.height,
])
return {
point: Vec.clampV(bindingPoint, 0, 1),
distance,
}
}
mutate = (shape: T, props: Partial<T>): Partial<T> => {
return props
}
transform = (shape: T, bounds: TLBounds, info: TransformInfo<T>): Partial<T> => {
return { ...shape, point: [bounds.minX, bounds.minY] }
}
transformSingle = (shape: T, bounds: TLBounds, info: TransformInfo<T>): Partial<T> | void => {
return this.transform(shape, bounds, info)
}
updateChildren?: <K extends TDShape>(shape: T, children: K[]) => Partial<K>[] | void
onChildrenChange?: (shape: T, children: TDShape[]) => Partial<T> | void
onHandleChange?: (shape: T, handles: Partial<T['handles']>) => Partial<T> | void
onRightPointHandle?: (
shape: T,
handles: Partial<T['handles']>,
info: Partial<TLPointerInfo>
) => Partial<T> | void
onDoubleClickHandle?: (
shape: T,
handles: Partial<T['handles']>,
info: Partial<TLPointerInfo>
) => Partial<T> | void
onDoubleClickBoundsHandle?: (shape: T) => Partial<T> | void
onSessionComplete?: (shape: T) => Partial<T> | void
getSvgElement = (shape: T, isDarkMode: boolean): SVGElement | void => {
const elm = document.getElementById(shape.id + '_svg')?.cloneNode(true) as SVGElement
if (!elm) return // possibly in test mode
if ('label' in shape && (shape as any).label) {
const s = shape as TDShape & { label: string }
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
const bounds = this.getBounds(shape)
const labelElm = getTextSvgElement(s['label'], shape.style, bounds)
labelElm.setAttribute('fill', getShapeStyle(shape.style, isDarkMode).stroke)
labelElm.setAttribute('transform-origin', 'top left')
g.setAttribute('text-align', 'center')
g.setAttribute('text-anchor', 'middle')
g.appendChild(elm)
g.appendChild(labelElm)
return g
}
return elm
}
}