tldraw
Version:
A tiny little drawing editor.
186 lines (152 loc) • 4.72 kB
text/typescript
import {
Group2d,
StateNode,
TLArrowShape,
TLPointerEventInfo,
TLShapeId,
Vec,
} from '@tldraw/editor'
import { ArrowShapeUtil } from '../../../shapes/arrow/ArrowShapeUtil'
import {
getArrowBodyGeometry,
getArrowLabelDefaultPosition,
} from '../../../shapes/arrow/arrowLabel'
import { startEditingShapeWithRichText } from '../selectHelpers'
export class PointingArrowLabel extends StateNode {
static override id = 'pointing_arrow_label'
shapeId = '' as TLShapeId
markId = ''
wasAlreadySelected = false
didDrag = false
didCtrlOnEnter = false
private info = {} as TLPointerEventInfo & {
shape: TLArrowShape
onInteractionEnd?: string | (() => void)
isCreating: boolean
}
private updateCursor() {
this.editor.setCursor({ type: 'grabbing', rotation: 0 })
}
override onEnter(
info: TLPointerEventInfo & {
shape: TLArrowShape
onInteractionEnd?: string | (() => void)
isCreating: boolean
}
) {
const { shape } = info
if (typeof info.onInteractionEnd === 'string') {
this.parent.setCurrentToolIdMask(info.onInteractionEnd)
}
this.info = info
this.shapeId = shape.id
this.didDrag = false
this.didCtrlOnEnter = info.accelKey
this.wasAlreadySelected = this.editor.getOnlySelectedShapeId() === shape.id
this.updateCursor()
const geometry = this.editor.getShapeGeometry<Group2d>(shape)
const labelGeometry = geometry.children[1]
if (!labelGeometry) {
throw Error(`Expected to find an arrow label geometry for shape: ${shape.id}`)
}
const currentPagePoint = this.editor.inputs.getCurrentPagePoint()
const pointInShapeSpace = this.editor.getPointInShapeSpace(shape, currentPagePoint)
this._labelDragOffset = Vec.Sub(labelGeometry.center, pointInShapeSpace)
this.markId = this.editor.markHistoryStoppingPoint('label-drag start')
const additiveSelectionKey = info.shiftKey || info.accelKey
if (additiveSelectionKey) {
const selectedShapeIds = this.editor.getSelectedShapeIds()
this.editor.setSelectedShapes([...selectedShapeIds, this.shapeId])
return
}
this.editor.setSelectedShapes([this.shapeId])
}
override onExit() {
this.parent.setCurrentToolIdMask(undefined)
this.editor.setCursor({ type: 'default', rotation: 0 })
}
private _labelDragOffset = new Vec(0, 0)
override onPointerMove() {
const isDragging = this.editor.inputs.getIsDragging()
if (!isDragging) return
if (this.didCtrlOnEnter) {
this.parent.transition('brushing', this.info)
return
}
const shape = this.editor.getShape<TLArrowShape>(this.shapeId)
if (!shape) return
const options = this.editor.getShapeUtil<ArrowShapeUtil>('arrow').options
const geometry = getArrowBodyGeometry(this.editor, shape)
const transform = this.editor.getShapePageTransform(shape.id)
const pointInShapeSpace = this.editor
.getPointInShapeSpace(shape, this.editor.inputs.getCurrentPagePoint())
.add(this._labelDragOffset)
const defaultLabelPosition = getArrowLabelDefaultPosition(this.editor, shape)
let nextLabelPosition = geometry.uninterpolateAlongEdge(pointInShapeSpace)
if (isNaN(nextLabelPosition)) {
nextLabelPosition = defaultLabelPosition
}
const nextLabelPoint = transform.applyToPoint(geometry.interpolateAlongEdge(nextLabelPosition))
const labelDefaultPoint = transform.applyToPoint(
geometry.interpolateAlongEdge(defaultLabelPosition)
)
if (
Vec.DistMin(
nextLabelPoint,
labelDefaultPoint,
options.labelCenterSnapDistance / this.editor.getZoomLevel()
)
) {
nextLabelPosition = defaultLabelPosition
}
this.didDrag = true
this.editor.updateShape({
id: shape.id,
type: shape.type,
props: { labelPosition: nextLabelPosition },
})
}
override onPointerUp() {
const shape = this.editor.getShape<TLArrowShape>(this.shapeId)
if (!shape) return
if (this.didDrag || !this.wasAlreadySelected) {
this.complete()
} else if (this.editor.canEditShape(shape)) {
startEditingShapeWithRichText(this.editor, shape.id)
}
}
override onCancel() {
this.cancel()
}
override onComplete() {
this.cancel()
}
override onInterrupt() {
this.cancel()
}
private complete() {
const { onInteractionEnd } = this.info
if (onInteractionEnd) {
if (typeof onInteractionEnd === 'string') {
this.editor.setCurrentTool(onInteractionEnd, {})
} else {
onInteractionEnd()
}
return
}
this.parent.transition('idle')
}
private cancel() {
this.editor.bailToMark(this.markId)
const { onInteractionEnd } = this.info
if (onInteractionEnd) {
if (typeof onInteractionEnd === 'string') {
this.editor.setCurrentTool(onInteractionEnd, {})
} else {
onInteractionEnd()
}
return
}
this.parent.transition('idle')
}
}