1
0

TLBaseLineBindingState.ts 8.2 KB

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