TLBaseLineBindingState.ts 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. import Vec from '@tldraw/vec'
  2. import { transaction } from 'mobx'
  3. import type { TLBinding, TLEventMap, TLHandle, TLStateEvents } from '../types'
  4. import { deepMerge, GeomUtils } from '../utils'
  5. import type { TLLineShape, TLLineShapeProps, TLShape } from './shapes'
  6. import type { TLApp } from './TLApp'
  7. import type { TLTool } from './TLTool'
  8. import { TLToolState } from './TLToolState'
  9. export class TLBaseLineBindingState<
  10. S extends TLShape,
  11. T extends S & TLLineShape,
  12. K extends TLEventMap,
  13. R extends TLApp<S, K>,
  14. P extends TLTool<S, K, R>
  15. > extends TLToolState<S, K, R, P> {
  16. static id = 'creating'
  17. handle: TLHandle = {} as TLHandle
  18. handleId: 'start' | 'end' = 'end'
  19. currentShape = {} as T
  20. initialShape = {} as T['props']
  21. bindableShapeIds: string[] = []
  22. startBindingShapeId?: string
  23. newStartBindingId = ''
  24. // Seems this value is never assigned to other than the default?
  25. draggedBindingId = ''
  26. onPointerMove: TLStateEvents<S, K>['onPointerMove'] = () => {
  27. const {
  28. inputs: { shiftKey, previousPoint, originPoint, currentPoint, modKey, altKey },
  29. settings: { showGrid },
  30. currentGrid,
  31. } = this.app
  32. // @ts-expect-error just ignore
  33. const shape = this.app.getShapeById<TLLineShape>(this.initialShape.id)!
  34. const { handles } = this.initialShape
  35. const handleId = this.handleId
  36. const otherHandleId = this.handleId === 'start' ? 'end' : 'start'
  37. if (Vec.isEqual(previousPoint, currentPoint)) return
  38. let delta = Vec.sub(currentPoint, originPoint)
  39. if (shiftKey) {
  40. const A = handles[otherHandleId].point
  41. const B = handles[handleId].point
  42. const C = Vec.add(B, delta)
  43. const angle = Vec.angle(A, C)
  44. const adjusted = Vec.rotWith(C, A, GeomUtils.snapAngleToSegments(angle, 24) - angle)
  45. delta = Vec.add(delta, Vec.sub(adjusted, C))
  46. }
  47. const nextPoint = Vec.add(handles[handleId].point, delta)
  48. const handleChanges = {
  49. [handleId]: {
  50. ...handles[handleId],
  51. // FIXME Snap not working properly
  52. // point: showGrid ? Vec.snap(nextPoint, currentGrid) : Vec.toFixed(nextPoint)
  53. point: Vec.toFixed(nextPoint),
  54. bindingId: undefined,
  55. },
  56. }
  57. let updated = this.currentShape.getHandlesChange(this.initialShape, handleChanges)
  58. // If the handle changed produced no change, bail here
  59. if (!updated) return
  60. // If nothing changes, we want these to be the same object reference as
  61. // before. If it does change, we'll redefine this later on. And if we've
  62. // made it this far, the shape should be a new object reference that
  63. // incorporates the changes we've made due to the handle movement.
  64. const next: { shape: TLLineShapeProps; bindings: Record<string, TLBinding> } = {
  65. shape: deepMerge(shape.props, updated),
  66. bindings: {},
  67. }
  68. let draggedBinding: TLBinding | undefined
  69. const draggingHandle = next.shape.handles[handleId]
  70. const oppositeHandle = next.shape.handles[otherHandleId]
  71. // START BINDING
  72. // If we have a start binding shape id, the recompute the binding
  73. // point based on the current end handle position
  74. if (this.startBindingShapeId) {
  75. let nextStartBinding: TLBinding | undefined
  76. const startTarget = this.app.getShapeById(this.startBindingShapeId)
  77. if (startTarget) {
  78. const center = startTarget.getCenter()
  79. const startHandle = next.shape.handles.start
  80. const endHandle = next.shape.handles.end
  81. const rayPoint = Vec.add(startHandle.point, next.shape.point)
  82. if (Vec.isEqual(rayPoint, center)) rayPoint[1]++ // Fix bug where ray and center are identical
  83. const rayOrigin = center
  84. const isInsideShape = startTarget.hitTestPoint(currentPoint)
  85. const rayDirection = Vec.uni(Vec.sub(rayPoint, rayOrigin))
  86. const hasStartBinding = this.app.currentPage.bindings[this.newStartBindingId] !== undefined
  87. // Don't bind the start handle if both handles are inside of the target shape.
  88. if (!modKey && !startTarget.hitTestPoint(Vec.add(next.shape.point, endHandle.point))) {
  89. nextStartBinding = this.findBindingPoint(
  90. shape.props,
  91. startTarget,
  92. 'start',
  93. this.newStartBindingId,
  94. center,
  95. rayOrigin,
  96. rayDirection,
  97. isInsideShape
  98. )
  99. }
  100. if (nextStartBinding && !hasStartBinding) {
  101. next.bindings[this.newStartBindingId] = nextStartBinding
  102. next.shape.handles.start.bindingId = nextStartBinding.id
  103. } else if (!nextStartBinding && hasStartBinding) {
  104. console.log('removing start binding')
  105. delete next.bindings[this.newStartBindingId]
  106. next.shape.handles.start.bindingId = undefined
  107. }
  108. }
  109. }
  110. if (!modKey) {
  111. const rayOrigin = Vec.add(oppositeHandle.point, next.shape.point)
  112. const rayPoint = Vec.add(draggingHandle.point, next.shape.point)
  113. const rayDirection = Vec.uni(Vec.sub(rayPoint, rayOrigin))
  114. const startPoint = Vec.add(next.shape.point, next.shape.handles.start.point)
  115. const endPoint = Vec.add(next.shape.point, next.shape.handles.end.point)
  116. const targets = this.bindableShapeIds
  117. .map(id => this.app.getShapeById(id)!)
  118. .sort((a, b) => b.nonce - a.nonce)
  119. .filter(shape => {
  120. return ![startPoint, endPoint].every(point => shape.hitTestPoint(point))
  121. })
  122. for (const target of targets) {
  123. draggedBinding = this.findBindingPoint(
  124. shape.props,
  125. target,
  126. this.handleId,
  127. this.draggedBindingId,
  128. rayPoint,
  129. rayOrigin,
  130. rayDirection,
  131. altKey
  132. )
  133. if (draggedBinding) break
  134. }
  135. }
  136. if (draggedBinding) {
  137. // Create the dragged point binding
  138. next.bindings[this.draggedBindingId] = draggedBinding
  139. next.shape = deepMerge(next.shape, {
  140. handles: {
  141. [this.handleId]: {
  142. bindingId: this.draggedBindingId,
  143. },
  144. },
  145. })
  146. } else {
  147. // Remove the dragging point binding
  148. const currentBindingId = shape.props.handles[this.handleId].bindingId
  149. if (currentBindingId !== undefined) {
  150. delete next.bindings[currentBindingId]
  151. next.shape = deepMerge(next.shape, {
  152. handles: {
  153. [this.handleId]: {
  154. bindingId: undefined,
  155. },
  156. },
  157. })
  158. }
  159. }
  160. updated = this.currentShape.getHandlesChange(next.shape, next.shape.handles)
  161. transaction(() => {
  162. if (updated) {
  163. this.currentShape.update(updated)
  164. this.app.currentPage.updateBindings(next.bindings)
  165. const bindingShapes = Object.values(updated.handles ?? {})
  166. .map(handle => handle.bindingId!)
  167. .map(id => this.app.currentPage.bindings[id])
  168. .filter(Boolean)
  169. .flatMap(binding => [binding.toId, binding.fromId].filter(Boolean))
  170. this.app.setBindingShapes(bindingShapes)
  171. }
  172. })
  173. }
  174. onPointerUp: TLStateEvents<S, K>['onPointerUp'] = () => {
  175. this.tool.transition('idle')
  176. if (this.currentShape) {
  177. this.app.setSelectedShapes([this.currentShape])
  178. }
  179. if (!this.app.settings.isToolLocked) {
  180. this.app.transition('select')
  181. }
  182. this.app.persist()
  183. }
  184. onWheel: TLStateEvents<S, K>['onWheel'] = (info, e) => {
  185. this.onPointerMove(info, e)
  186. }
  187. onExit: TLStateEvents<S, K>['onExit'] = () => {
  188. this.app.clearBindingShape()
  189. this.app.history.resume()
  190. this.app.persist()
  191. }
  192. onKeyDown: TLStateEvents<S>['onKeyDown'] = (info, e) => {
  193. switch (e.key) {
  194. case 'Escape': {
  195. this.app.deleteShapes([this.currentShape])
  196. this.tool.transition('idle')
  197. break
  198. }
  199. }
  200. }
  201. private findBindingPoint = (
  202. shape: TLLineShapeProps,
  203. target: TLShape,
  204. handleId: 'start' | 'end',
  205. bindingId: string,
  206. point: number[],
  207. origin: number[],
  208. direction: number[],
  209. bindAnywhere: boolean
  210. ) => {
  211. const bindingPoint = target.getBindingPoint(
  212. point, // fix dead center bug
  213. origin,
  214. direction,
  215. bindAnywhere
  216. )
  217. // Not all shapes will produce a binding point
  218. if (!bindingPoint) return
  219. return {
  220. id: bindingId,
  221. type: 'line',
  222. fromId: shape.id,
  223. toId: target.id,
  224. handleId: handleId,
  225. point: Vec.toFixed(bindingPoint.point),
  226. distance: bindingPoint.distance,
  227. }
  228. }
  229. }