tldraw
Version:
A tiny little drawing editor.
198 lines (160 loc) • 4.67 kB
text/typescript
import {
StateNode,
TLPointerEventInfo,
TLShapeId,
TLTextShape,
Vec,
createShapeId,
isShapeId,
maybeSnapToGrid,
toRichText,
} from '@tldraw/editor'
import { startEditingShapeWithRichText } from '../../../tools/SelectTool/selectHelpers'
export class Pointing extends StateNode {
static override id = 'pointing'
shape?: TLTextShape
markId = ''
enterTime = 0
override onEnter(): void {
this.enterTime = Date.now()
}
override onExit() {
this.editor.setHintingShapes([])
}
override onPointerMove(info: TLPointerEventInfo) {
// Create a fixed width shape if the user wants to do that.
// Don't create a fixed width shape unless the the drag is a little larger,
// otherwise you get a vertical column of single characters if you accidentally
// drag a bit unintentionally.
// If the user hasn't been pointing for more than 150ms, don't create a fixed width shape
if (Date.now() - this.enterTime < 150) return
const { editor } = this
const isPointing = editor.inputs.getIsPointing()
if (!isPointing) return
const originPagePoint = editor.inputs.getOriginPagePoint()
const currentPagePoint = editor.inputs.getCurrentPagePoint()
const currentDragDist = Math.abs(originPagePoint.x - currentPagePoint.x)
const baseMinDragDistForFixedWidth = Math.sqrt(
editor.getInstanceState().isCoarsePointer
? editor.options.coarseDragDistanceSquared
: editor.options.dragDistanceSquared
)
// Ten times the base drag distance for fixed width
const minSquaredDragDist = (baseMinDragDistForFixedWidth * 6) / editor.getZoomLevel()
if (currentDragDist > minSquaredDragDist) {
const id = createShapeId()
this.markId = editor.markHistoryStoppingPoint(`creating_text:${id}`)
// create the initial shape with the width that we've dragged
const shape = this.createTextShape(id, originPagePoint, false, currentDragDist)
if (!shape) {
this.cancel()
return
}
// Now save the fresh reference
this.shape = editor.getShape(shape)
editor.select(id)
const scale = this.editor.user.getIsDynamicResizeMode() ? 1 / this.editor.getZoomLevel() : 1
editor.setCurrentTool('select.resizing', {
...info,
target: 'selection',
handle: 'right',
isCreating: true,
creatingMarkId: this.markId,
// Make sure the cursor offset takes into account how far we've already dragged
creationCursorOffset: { x: currentDragDist * scale, y: 1 },
onInteractionEnd: 'text',
onCreate: () => {
startEditingShapeWithRichText(editor, shape.id)
},
})
}
}
override onPointerUp() {
this.complete()
}
override onComplete() {
this.cancel()
}
override onCancel() {
this.cancel()
}
override onInterrupt() {
this.cancel()
}
private complete() {
this.editor.markHistoryStoppingPoint('creating text shape')
const id = createShapeId()
const originPagePoint = this.editor.inputs.getOriginPagePoint()
const shape = this.createTextShape(id, originPagePoint, true, 20)
if (!shape) return
this.editor.select(id)
startEditingShapeWithRichText(this.editor, id)
}
private cancel() {
this.parent.transition('idle')
this.editor.bailToMark(this.markId)
}
private createTextShape(id: TLShapeId, point: Vec, autoSize: boolean, width: number) {
this.editor.createShape({
id,
type: 'text',
x: point.x,
y: point.y,
props: {
richText: toRichText(''),
autoSize,
w: width,
scale: this.editor.user.getIsDynamicResizeMode() ? 1 / this.editor.getZoomLevel() : 1,
},
})
const shape = this.editor.getShape<TLTextShape>(id)
if (!shape) {
this.cancel()
return
}
const bounds = this.editor.getShapePageBounds(shape)!
const delta = new Vec()
if (autoSize) {
switch (shape.props.textAlign) {
case 'start': {
delta.x = 0
break
}
case 'middle': {
delta.x = -bounds.width / 2
break
}
case 'end': {
delta.x = -bounds.width
break
}
}
} else {
delta.x = 0
}
delta.y = -bounds.height / 2
if (isShapeId(shape.parentId)) {
const transform = this.editor.getShapeParentTransform(shape)
delta.rot(-transform.rotation())
}
const shapeX = shape.x + delta.x
const shapeY = shape.y + delta.y
if (this.editor.getInstanceState().isGridMode) {
const topLeft = new Vec(shapeX, shapeY)
const gridSnappedPoint = maybeSnapToGrid(topLeft, this.editor)
const gridDelta = Vec.Sub(topLeft, gridSnappedPoint)
this.editor.updateShape({
...shape,
x: shapeX - gridDelta.x,
y: shapeY - gridDelta.y,
})
} else {
this.editor.updateShape({
...shape,
x: shapeX,
y: shapeY,
})
}
return shape
}
}