2022-08-02 13:56:12 +00:00
import { TLShapeUtil , Utils } from '@tldraw/core'
import type { TLBounds , TLPointerInfo } from '@tldraw/core'
2021-11-16 16:01:29 +00:00
import {
intersectLineSegmentBounds ,
intersectLineSegmentPolyline ,
intersectRayBounds ,
} from '@tldraw/intersect'
2021-10-27 15:15:01 +00:00
import { Vec } from '@tldraw/vec'
import * as React from 'react'
2021-12-09 22:29:09 +00:00
import { BINDING_DISTANCE } from '~constants'
2022-08-02 13:56:12 +00:00
import type { ShapesWithProp , TDBinding , TDMeta , TDShape , TransformInfo } from '~types'
2021-12-28 11:23:17 +00:00
import { getFontStyle , getShapeStyle } from './shared'
2022-08-02 13:56:12 +00:00
import { getTextLabelSize } from './shared/getTextSize'
import { getTextSvgElement } from './shared/getTextSvgElement'
2021-10-27 15:15:01 +00:00
2021-11-16 16:01:29 +00:00
export abstract class TDShapeUtil < T extends TDShape , E extends Element = any > extends TLShapeUtil <
T ,
E ,
TDMeta
> {
2021-10-27 15:15:01 +00:00
abstract type : T [ 'type' ]
2021-10-28 16:50:58 +00:00
canBind = false
canEdit = false
canClone = false
isAspectRatioLocked = false
2021-11-16 16:01:29 +00:00
hideResizeHandles = false
2021-12-09 22:29:09 +00:00
bindingDistance = BINDING_DISTANCE
2021-10-27 15:15:01 +00:00
abstract getShape : ( props : Partial < T > ) = > T
2021-11-16 16:01:29 +00:00
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
}
2021-10-27 15:15:01 +00:00
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 ) )
}
2021-12-09 22:29:09 +00:00
getExpandedBounds = ( shape : T ) = > {
return Utils . expandBounds ( this . getBounds ( shape ) , this . bindingDistance )
}
2021-11-16 16:01:29 +00:00
getBindingPoint = < K extends TDShape > (
2021-10-27 15:15:01 +00:00
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 )
2021-12-09 22:29:09 +00:00
const expandedBounds = this . getExpandedBounds ( shape )
2021-10-27 15:15:01 +00:00
// The point must be inside of the expanded bounding box
if ( ! Utils . pointInBounds ( point , expandedBounds ) ) return
2021-12-09 22:29:09 +00:00
const intersections = intersectRayBounds ( origin , direction , expandedBounds )
. filter ( ( int ) = > int . didIntersect )
. map ( ( int ) = > int . points [ 0 ] )
2021-10-27 15:15:01 +00:00
2021-12-09 22:29:09 +00:00
if ( ! intersections . length ) return
2021-10-27 15:15:01 +00:00
2021-12-09 22:29:09 +00:00
// The center of the shape
const center = this . getCenter ( shape )
2021-10-27 15:15:01 +00:00
2021-12-09 22:29:09 +00:00
// 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 ]
2021-10-27 15:15:01 +00:00
2021-12-09 22:29:09 +00:00
// The point between the handle and the intersection
const middlePoint = Vec . med ( point , intersection )
2021-10-27 15:15:01 +00:00
2021-12-09 22:29:09 +00:00
// The anchor is the point in the shape where the arrow will be pointing
let anchor : number [ ]
2021-10-27 15:15:01 +00:00
2021-12-09 22:29:09 +00:00
// 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
2021-10-27 15:15:01 +00:00
} else {
2021-12-09 22:29:09 +00:00
// Otherwise, the anchor is the middle point between the handle and the intersection
anchor = middlePoint
2021-10-27 15:15:01 +00:00
}
if ( Utils . pointInBounds ( point , bounds ) ) {
2021-12-09 22:29:09 +00:00
// If the point is inside of the shape, use the shape's binding distance
distance = this . bindingDistance
2021-10-27 15:15:01 +00:00
} else {
2021-12-09 22:29:09 +00:00
// Otherwise, use the actual distance from the handle point to nearest edge
2021-10-27 15:15:01 +00:00
distance = Math . max (
2021-12-09 22:29:09 +00:00
this . bindingDistance ,
2021-10-27 15:15:01 +00:00
Utils . getBoundsSides ( bounds )
. map ( ( side ) = > Vec . distanceToLineSegment ( side [ 1 ] [ 0 ] , side [ 1 ] [ 1 ] , point ) )
. sort ( ( a , b ) = > a - b ) [ 0 ]
)
}
}
2021-12-09 22:29:09 +00:00
// 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 ,
] )
2021-10-27 15:15:01 +00:00
return {
point : Vec.clampV ( bindingPoint , 0 , 1 ) ,
distance ,
}
}
mutate = ( shape : T , props : Partial < T > ) : Partial < T > = > {
return props
}
2021-11-16 16:01:29 +00:00
transform = ( shape : T , bounds : TLBounds , info : TransformInfo < T > ) : Partial < T > = > {
2021-10-27 15:15:01 +00:00
return { . . . shape , point : [ bounds . minX , bounds . minY ] }
}
2021-11-16 16:01:29 +00:00
transformSingle = ( shape : T , bounds : TLBounds , info : TransformInfo < T > ) : Partial < T > | void = > {
2021-10-27 15:15:01 +00:00
return this . transform ( shape , bounds , info )
}
2021-11-16 16:01:29 +00:00
updateChildren ? : < K extends TDShape > ( shape : T , children : K [ ] ) = > Partial < K > [ ] | void
2021-10-27 15:15:01 +00:00
2021-11-16 16:01:29 +00:00
onChildrenChange ? : ( shape : T , children : TDShape [ ] ) = > Partial < T > | void
2021-10-27 15:15:01 +00:00
2022-01-30 21:13:57 +00:00
onHandleChange ? : ( shape : T , handles : Partial < T [ ' handles ' ] > ) = > Partial < T > | void
2021-10-27 15:15:01 +00:00
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
2021-11-22 16:15:51 +00:00
2022-05-14 11:13:37 +00:00
getSvgElement = ( shape : T , isDarkMode : boolean ) : SVGElement | void = > {
2021-12-28 11:23:17 +00:00
const elm = document . getElementById ( shape . id + '_svg' ) ? . cloneNode ( true ) as SVGElement
if ( ! elm ) return // possibly in test mode
2022-05-11 13:25:08 +00:00
if ( 'label' in shape && ( shape as any ) . label ) {
2022-01-10 15:13:52 +00:00
const s = shape as TDShape & { label : string }
2021-12-28 11:23:17 +00:00
const g = document . createElementNS ( 'http://www.w3.org/2000/svg' , 'g' )
const bounds = this . getBounds ( shape )
2022-01-10 15:13:52 +00:00
const labelElm = getTextSvgElement ( s [ 'label' ] , shape . style , bounds )
2022-05-14 11:13:37 +00:00
labelElm . setAttribute ( 'fill' , getShapeStyle ( shape . style , isDarkMode ) . stroke )
2021-12-28 11:23:17 +00:00
labelElm . setAttribute ( 'transform-origin' , 'top left' )
2022-05-11 13:25:08 +00:00
g . setAttribute ( 'text-align' , 'center' )
g . setAttribute ( 'text-anchor' , 'middle' )
2021-12-28 11:23:17 +00:00
g . appendChild ( elm )
g . appendChild ( labelElm )
return g
}
return elm
2021-11-22 16:15:51 +00:00
}
2021-10-27 15:15:01 +00:00
}