|  | @@ -1,8 +1,8 @@
 | 
	
		
			
				|  |  | -import Vec from '@tldraw/vec'
 | 
	
		
			
				|  |  |  import { toJS } from 'mobx'
 | 
	
		
			
				|  |  | -import { TLApp, TLLineShape, TLLineShapeProps, TLShape, TLToolState } from '~lib'
 | 
	
		
			
				|  |  | -import type { TLEventMap, TLBinding, TLStateEvents } from '~types'
 | 
	
		
			
				|  |  | -import { deepMerge, GeomUtils, PointUtils, uniqueId } from '~utils'
 | 
	
		
			
				|  |  | +import type { TLApp, TLLineShape, TLShape } from '~lib'
 | 
	
		
			
				|  |  | +import { TLBaseLineBindingState } from '~lib/TLBaseLineBindingState'
 | 
	
		
			
				|  |  | +import type { TLEventMap } from '~types'
 | 
	
		
			
				|  |  | +import { PointUtils, uniqueId } from '~utils'
 | 
	
		
			
				|  |  |  import type { TLLineTool } from '../TLLineTool'
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  export class CreatingState<
 | 
	
	
		
			
				|  | @@ -11,17 +11,17 @@ export class CreatingState<
 | 
	
		
			
				|  |  |    K extends TLEventMap,
 | 
	
		
			
				|  |  |    R extends TLApp<S, K>,
 | 
	
		
			
				|  |  |    P extends TLLineTool<T, S, K, R>
 | 
	
		
			
				|  |  | -> extends TLToolState<S, K, R, P> {
 | 
	
		
			
				|  |  | +> extends TLBaseLineBindingState<S, T, K, R, P> {
 | 
	
		
			
				|  |  |    static id = 'creating'
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  creatingShape = {} as T
 | 
	
		
			
				|  |  | -  initialShape = {} as T['props']
 | 
	
		
			
				|  |  | -  bindableShapeIds: string[] = []
 | 
	
		
			
				|  |  | -  startBindingShapeId?: string
 | 
	
		
			
				|  |  | -  newStartBindingId = uniqueId()
 | 
	
		
			
				|  |  | -  draggedBindingId = uniqueId()
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |    onEnter = () => {
 | 
	
		
			
				|  |  | +    this.app.history.pause()
 | 
	
		
			
				|  |  | +    this.newStartBindingId = uniqueId()
 | 
	
		
			
				|  |  | +    this.draggedBindingId = uniqueId()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    const page = this.app.currentPage
 | 
	
		
			
				|  |  | +    this.bindableShapeIds = page.getBindableShapes()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      const { Shape } = this.tool
 | 
	
		
			
				|  |  |      const { originPoint } = this.app.inputs
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -33,15 +33,9 @@ export class CreatingState<
 | 
	
		
			
				|  |  |        point: originPoint,
 | 
	
		
			
				|  |  |      })
 | 
	
		
			
				|  |  |      this.initialShape = toJS(shape.props)
 | 
	
		
			
				|  |  | -    this.creatingShape = shape
 | 
	
		
			
				|  |  | +    this.currentShape = shape
 | 
	
		
			
				|  |  |      this.app.currentPage.addShapes(shape)
 | 
	
		
			
				|  |  |      this.app.setSelectedShapes([shape])
 | 
	
		
			
				|  |  | -    this.newStartBindingId = uniqueId()
 | 
	
		
			
				|  |  | -    this.draggedBindingId = uniqueId()
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    const page = this.app.currentPage
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    this.bindableShapeIds = page.getBindableShapes()
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      this.startBindingShapeId = this.bindableShapeIds
 | 
	
		
			
				|  |  |        .map(id => this.app.getShapeById(id))
 | 
	
	
		
			
				|  | @@ -49,178 +43,7 @@ export class CreatingState<
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      if (this.startBindingShapeId) {
 | 
	
		
			
				|  |  |        this.bindableShapeIds.splice(this.bindableShapeIds.indexOf(this.startBindingShapeId), 1)
 | 
	
		
			
				|  |  | -      this.app.setBindingShape(this.startBindingShapeId)
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -  }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  onPointerMove: TLStateEvents<S, K>['onPointerMove'] = () => {
 | 
	
		
			
				|  |  | -    const {
 | 
	
		
			
				|  |  | -      inputs: { shiftKey, previousPoint, originPoint, currentPoint, modKey },
 | 
	
		
			
				|  |  | -      settings: { showGrid },
 | 
	
		
			
				|  |  | -      currentGrid,
 | 
	
		
			
				|  |  | -    } = this.app
 | 
	
		
			
				|  |  | -    // @ts-expect-error just ignore
 | 
	
		
			
				|  |  | -    const shape = this.app.getShapeById<TLLineShape>(this.initialShape.id)
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    const { handles } = this.initialShape
 | 
	
		
			
				|  |  | -    const handleId = 'end'
 | 
	
		
			
				|  |  | -    const otherHandleId = 'start'
 | 
	
		
			
				|  |  | -    if (Vec.isEqual(previousPoint, currentPoint)) return
 | 
	
		
			
				|  |  | -    let delta = Vec.sub(currentPoint, originPoint)
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    if (shiftKey) {
 | 
	
		
			
				|  |  | -      const A = handles[otherHandleId].point
 | 
	
		
			
				|  |  | -      const B = handles[handleId].point
 | 
	
		
			
				|  |  | -      const C = Vec.add(B, delta)
 | 
	
		
			
				|  |  | -      const angle = Vec.angle(A, C)
 | 
	
		
			
				|  |  | -      const adjusted = Vec.rotWith(C, A, GeomUtils.snapAngleToSegments(angle, 24) - angle)
 | 
	
		
			
				|  |  | -      delta = Vec.add(delta, Vec.sub(adjusted, C))
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    const nextPoint = Vec.add(handles[handleId].point, delta)
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    const handleChanges = {
 | 
	
		
			
				|  |  | -      [handleId]: {
 | 
	
		
			
				|  |  | -        ...handles[handleId],
 | 
	
		
			
				|  |  | -        // FIXME Snap not working properly
 | 
	
		
			
				|  |  | -        point: showGrid ? Vec.snap(nextPoint, currentGrid) : Vec.toFixed(nextPoint),
 | 
	
		
			
				|  |  | -        bindingId: undefined,
 | 
	
		
			
				|  |  | -      },
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    let updated = this.creatingShape.getHandlesChange(this.initialShape, handleChanges)
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    // If the handle changed produced no change, bail here
 | 
	
		
			
				|  |  | -    if (!updated) return
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    // If nothing changes, we want these to be the same object reference as
 | 
	
		
			
				|  |  | -    // before. If it does change, we'll redefine this later on. And if we've
 | 
	
		
			
				|  |  | -    // made it this far, the shape should be a new object reference that
 | 
	
		
			
				|  |  | -    // incorporates the changes we've made due to the handle movement.
 | 
	
		
			
				|  |  | -    const next: { shape: TLLineShapeProps; bindings: Record<string, TLBinding> } = {
 | 
	
		
			
				|  |  | -      shape: deepMerge(shape.props, updated),
 | 
	
		
			
				|  |  | -      bindings: {},
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    let draggedBinding: TLBinding | undefined
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    const draggingHandle = next.shape.handles[handleId]
 | 
	
		
			
				|  |  | -    const oppositeHandle = next.shape.handles[otherHandleId]
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    // START BINDING
 | 
	
		
			
				|  |  | -    // If we have a start binding shape id, the recompute the binding
 | 
	
		
			
				|  |  | -    // point based on the current end handle position
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    if (this.startBindingShapeId) {
 | 
	
		
			
				|  |  | -      let nextStartBinding: TLBinding | undefined
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -      const startTarget = this.app.getShapeById(this.startBindingShapeId)
 | 
	
		
			
				|  |  | -      const center = startTarget.getCenter()
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -      const startHandle = next.shape.handles.start
 | 
	
		
			
				|  |  | -      const endHandle = next.shape.handles.end
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -      const rayPoint = Vec.add(startHandle.point, next.shape.point)
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -      if (Vec.isEqual(rayPoint, center)) rayPoint[1]++ // Fix bug where ray and center are identical
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -      const rayOrigin = center
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -      const isInsideShape = startTarget.hitTestPoint(currentPoint)
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -      const rayDirection = Vec.uni(Vec.sub(rayPoint, rayOrigin))
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -      const hasStartBinding = this.app.currentPage.bindings[this.newStartBindingId] !== undefined
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -      // Don't bind the start handle if both handles are inside of the target shape.
 | 
	
		
			
				|  |  | -      if (!modKey && !startTarget.hitTestPoint(Vec.add(next.shape.point, endHandle.point))) {
 | 
	
		
			
				|  |  | -        nextStartBinding = this.findBindingPoint(
 | 
	
		
			
				|  |  | -          shape.props,
 | 
	
		
			
				|  |  | -          startTarget,
 | 
	
		
			
				|  |  | -          'start',
 | 
	
		
			
				|  |  | -          this.newStartBindingId,
 | 
	
		
			
				|  |  | -          center,
 | 
	
		
			
				|  |  | -          rayOrigin,
 | 
	
		
			
				|  |  | -          rayDirection,
 | 
	
		
			
				|  |  | -          isInsideShape
 | 
	
		
			
				|  |  | -        )
 | 
	
		
			
				|  |  | -      }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -      if (nextStartBinding && !hasStartBinding) {
 | 
	
		
			
				|  |  | -        next.bindings[this.newStartBindingId] = nextStartBinding
 | 
	
		
			
				|  |  | -        next.shape.handles.start.bindingId = nextStartBinding.id
 | 
	
		
			
				|  |  | -      } else if (!nextStartBinding && hasStartBinding) {
 | 
	
		
			
				|  |  | -        console.log('removing start binding')
 | 
	
		
			
				|  |  | -        delete next.bindings[this.newStartBindingId]
 | 
	
		
			
				|  |  | -        next.shape.handles.start.bindingId = undefined
 | 
	
		
			
				|  |  | -      }
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    updated = this.creatingShape.getHandlesChange(next.shape, next.shape.handles)
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    if (updated) {
 | 
	
		
			
				|  |  | -      this.creatingShape.update(updated)
 | 
	
		
			
				|  |  | -      this.app.currentPage.updateBindings(next.bindings)
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -  }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  onPointerUp: TLStateEvents<S, K>['onPointerUp'] = () => {
 | 
	
		
			
				|  |  | -    this.tool.transition('idle')
 | 
	
		
			
				|  |  | -    if (this.creatingShape) {
 | 
	
		
			
				|  |  | -      this.app.setSelectedShapes([this.creatingShape])
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -    if (!this.app.settings.isToolLocked) {
 | 
	
		
			
				|  |  | -      this.app.transition('select')
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -    this.app.persist()
 | 
	
		
			
				|  |  | -  }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  onWheel: TLStateEvents<S, K>['onWheel'] = (info, e) => {
 | 
	
		
			
				|  |  | -    this.onPointerMove(info, e)
 | 
	
		
			
				|  |  | -  }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  onExit: TLStateEvents<S, K>['onExit'] = () => {
 | 
	
		
			
				|  |  | -    this.app.clearBindingShape()
 | 
	
		
			
				|  |  | -  }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  onKeyDown: TLStateEvents<S>['onKeyDown'] = (info, e) => {
 | 
	
		
			
				|  |  | -    switch (e.key) {
 | 
	
		
			
				|  |  | -      case 'Escape': {
 | 
	
		
			
				|  |  | -        this.app.deleteShapes([this.creatingShape])
 | 
	
		
			
				|  |  | -        this.tool.transition('idle')
 | 
	
		
			
				|  |  | -        break
 | 
	
		
			
				|  |  | -      }
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -  }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  private findBindingPoint = (
 | 
	
		
			
				|  |  | -    shape: TLLineShapeProps,
 | 
	
		
			
				|  |  | -    target: TLShape,
 | 
	
		
			
				|  |  | -    handleId: 'start' | 'end',
 | 
	
		
			
				|  |  | -    bindingId: string,
 | 
	
		
			
				|  |  | -    point: number[],
 | 
	
		
			
				|  |  | -    origin: number[],
 | 
	
		
			
				|  |  | -    direction: number[],
 | 
	
		
			
				|  |  | -    bindAnywhere: boolean
 | 
	
		
			
				|  |  | -  ) => {
 | 
	
		
			
				|  |  | -    const bindingPoint = target.getBindingPoint(
 | 
	
		
			
				|  |  | -      point, // fix dead center bug
 | 
	
		
			
				|  |  | -      origin,
 | 
	
		
			
				|  |  | -      direction,
 | 
	
		
			
				|  |  | -      bindAnywhere
 | 
	
		
			
				|  |  | -    )
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    // Not all shapes will produce a binding point
 | 
	
		
			
				|  |  | -    if (!bindingPoint) return
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    return {
 | 
	
		
			
				|  |  | -      id: bindingId,
 | 
	
		
			
				|  |  | -      type: 'line',
 | 
	
		
			
				|  |  | -      fromId: shape.id,
 | 
	
		
			
				|  |  | -      toId: target.id,
 | 
	
		
			
				|  |  | -      handleId: handleId,
 | 
	
		
			
				|  |  | -      point: Vec.toFixed(bindingPoint.point),
 | 
	
		
			
				|  |  | -      distance: bindingPoint.distance,
 | 
	
		
			
				|  |  | +      this.app.setBindingShapes([this.startBindingShapeId])
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  }
 |