@tldraw/editor 4.1.0-canary.4f9226a36e7e → 4.1.0-canary.533dd6427b33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist-cjs/index.d.ts +1 -0
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/lib/components/MenuClickCapture.js +30 -15
  4. package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
  5. package/dist-cjs/lib/editor/Editor.js +1 -1
  6. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  7. package/dist-cjs/lib/editor/derivations/parentsToChildren.js +3 -7
  8. package/dist-cjs/lib/editor/derivations/parentsToChildren.js.map +2 -2
  9. package/dist-cjs/lib/editor/managers/SnapManager/HandleSnaps.js +67 -2
  10. package/dist-cjs/lib/editor/managers/SnapManager/HandleSnaps.js.map +2 -2
  11. package/dist-cjs/lib/hooks/useCanvasEvents.js +2 -1
  12. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  13. package/dist-cjs/lib/license/LicenseManager.js +2 -0
  14. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  15. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +5 -0
  16. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  17. package/dist-cjs/version.js +3 -3
  18. package/dist-cjs/version.js.map +1 -1
  19. package/dist-esm/index.d.mts +1 -0
  20. package/dist-esm/index.mjs +1 -1
  21. package/dist-esm/lib/components/MenuClickCapture.mjs +30 -15
  22. package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
  23. package/dist-esm/lib/editor/Editor.mjs +2 -1
  24. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  25. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs +3 -7
  26. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs.map +2 -2
  27. package/dist-esm/lib/editor/managers/SnapManager/HandleSnaps.mjs +67 -2
  28. package/dist-esm/lib/editor/managers/SnapManager/HandleSnaps.mjs.map +2 -2
  29. package/dist-esm/lib/hooks/useCanvasEvents.mjs +2 -1
  30. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  31. package/dist-esm/lib/license/LicenseManager.mjs +2 -0
  32. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  33. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +5 -0
  34. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  35. package/dist-esm/version.mjs +3 -3
  36. package/dist-esm/version.mjs.map +1 -1
  37. package/package.json +7 -7
  38. package/src/lib/components/MenuClickCapture.tsx +35 -17
  39. package/src/lib/editor/Editor.ts +2 -1
  40. package/src/lib/editor/derivations/parentsToChildren.ts +4 -10
  41. package/src/lib/editor/managers/SnapManager/HandleSnaps.ts +91 -4
  42. package/src/lib/hooks/useCanvasEvents.ts +8 -1
  43. package/src/lib/license/LicenseManager.ts +8 -0
  44. package/src/lib/primitives/geometry/Geometry2d.ts +6 -0
  45. package/src/version.ts +3 -3
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/version.ts"],
4
- "sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '4.1.0-canary.4f9226a36e7e'\nexport const publishDates = {\n\tmajor: '2025-09-18T14:39:22.803Z',\n\tminor: '2025-10-04T12:12:04.189Z',\n\tpatch: '2025-10-04T12:12:04.189Z',\n}\n"],
4
+ "sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '4.1.0-canary.533dd6427b33'\nexport const publishDates = {\n\tmajor: '2025-09-18T14:39:22.803Z',\n\tminor: '2025-10-15T10:04:14.590Z',\n\tpatch: '2025-10-15T10:04:14.590Z',\n}\n"],
5
5
  "mappings": "AAGO,MAAM,UAAU;AAChB,MAAM,eAAe;AAAA,EAC3B,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACR;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/editor",
3
3
  "description": "tldraw infinite canvas SDK (editor).",
4
- "version": "4.1.0-canary.4f9226a36e7e",
4
+ "version": "4.1.0-canary.533dd6427b33",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -50,12 +50,12 @@
50
50
  "@tiptap/core": "^2.9.1",
51
51
  "@tiptap/pm": "^2.9.1",
52
52
  "@tiptap/react": "^2.9.1",
53
- "@tldraw/state": "4.1.0-canary.4f9226a36e7e",
54
- "@tldraw/state-react": "4.1.0-canary.4f9226a36e7e",
55
- "@tldraw/store": "4.1.0-canary.4f9226a36e7e",
56
- "@tldraw/tlschema": "4.1.0-canary.4f9226a36e7e",
57
- "@tldraw/utils": "4.1.0-canary.4f9226a36e7e",
58
- "@tldraw/validate": "4.1.0-canary.4f9226a36e7e",
53
+ "@tldraw/state": "4.1.0-canary.533dd6427b33",
54
+ "@tldraw/state-react": "4.1.0-canary.533dd6427b33",
55
+ "@tldraw/store": "4.1.0-canary.533dd6427b33",
56
+ "@tldraw/tlschema": "4.1.0-canary.533dd6427b33",
57
+ "@tldraw/utils": "4.1.0-canary.533dd6427b33",
58
+ "@tldraw/validate": "4.1.0-canary.533dd6427b33",
59
59
  "@types/core-js": "^2.5.8",
60
60
  "@use-gesture/react": "^10.3.1",
61
61
  "classnames": "^2.5.1",
@@ -3,6 +3,7 @@ import { PointerEvent, useCallback, useRef, useState } from 'react'
3
3
  import { useCanvasEvents } from '../hooks/useCanvasEvents'
4
4
  import { useEditor } from '../hooks/useEditor'
5
5
  import { Vec } from '../primitives/Vec'
6
+ import { getPointerInfo } from '../utils/getPointerInfo'
6
7
 
7
8
  /**
8
9
  * When a menu is open, this component prevents the user from interacting with the canvas.
@@ -39,35 +40,51 @@ export function MenuClickCapture() {
39
40
  isDragging: false,
40
41
  start: new Vec(e.clientX, e.clientY),
41
42
  }
43
+ rDidAPointerDownAndDragWhileMenuWasOpen.current = false
42
44
  }
43
45
  editor.menus.clearOpenMenus()
44
46
  },
45
47
  [editor]
46
48
  )
47
49
 
50
+ const rDidAPointerDownAndDragWhileMenuWasOpen = useRef(false)
51
+
48
52
  const handlePointerMove = useCallback(
49
53
  (e: PointerEvent) => {
50
54
  // Do nothing unless we're pointing
51
55
  if (!rPointerState.current.isDown) return
52
56
 
53
- if (
54
- // We're pointing, but are we dragging?
55
- Vec.Dist2(rPointerState.current.start, new Vec(e.clientX, e.clientY)) >
56
- editor.options.dragDistanceSquared
57
- ) {
58
- // Wehaddaeventitsadrag
59
- rPointerState.current = {
60
- ...rPointerState.current,
61
- isDown: true,
62
- isDragging: true,
57
+ // call the onPointerDown with the original pointer position
58
+ const { x, y } = rPointerState.current.start
59
+
60
+ if (!rDidAPointerDownAndDragWhileMenuWasOpen.current) {
61
+ if (
62
+ // We're pointing, but are we dragging?
63
+ Vec.Dist2(rPointerState.current.start, new Vec(e.clientX, e.clientY)) >
64
+ editor.options.dragDistanceSquared
65
+ ) {
66
+ rDidAPointerDownAndDragWhileMenuWasOpen.current = true
67
+ // Wehaddaeventitsadrag
68
+ rPointerState.current = {
69
+ ...rPointerState.current,
70
+ isDown: true,
71
+ isDragging: true,
72
+ }
73
+ canvasEvents.onPointerDown?.({
74
+ ...e,
75
+ clientX: x,
76
+ clientY: y,
77
+ button: 0,
78
+ })
63
79
  }
64
- // call the onPointerDown with the original pointer position
65
- const { x, y } = rPointerState.current.start
66
- canvasEvents.onPointerDown?.({
67
- ...e,
68
- clientX: x,
69
- clientY: y,
70
- button: 0,
80
+ }
81
+
82
+ if (rDidAPointerDownAndDragWhileMenuWasOpen.current) {
83
+ editor.dispatch({
84
+ type: 'pointer',
85
+ target: 'canvas',
86
+ name: 'pointer_move',
87
+ ...getPointerInfo(editor, e),
71
88
  })
72
89
  }
73
90
  },
@@ -86,6 +103,7 @@ export function MenuClickCapture() {
86
103
  isDragging: false,
87
104
  start: new Vec(e.clientX, e.clientY),
88
105
  }
106
+ rDidAPointerDownAndDragWhileMenuWasOpen.current = false
89
107
  },
90
108
  [canvasEvents]
91
109
  )
@@ -69,6 +69,7 @@ import {
69
69
  JsonObject,
70
70
  PerformanceTracker,
71
71
  Result,
72
+ ZERO_INDEX_KEY,
72
73
  annotateError,
73
74
  assert,
74
75
  assertExists,
@@ -5663,7 +5664,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5663
5664
  const children = this._parentIdsToChildIds.get()[parentId]
5664
5665
 
5665
5666
  if (!children || children.length === 0) {
5666
- return 'a1' as IndexKey
5667
+ return getIndexAbove(ZERO_INDEX_KEY)
5667
5668
  }
5668
5669
  const shape = this.getShape(children[children.length - 1])!
5669
5670
  return getIndexAbove(shape.index)
@@ -1,6 +1,6 @@
1
1
  import { Computed, computed, isUninitialized, RESET_VALUE } from '@tldraw/state'
2
2
  import { CollectionDiff, RecordsDiff } from '@tldraw/store'
3
- import { isShape, TLParentId, TLRecord, TLShape, TLShapeId, TLStore } from '@tldraw/tlschema'
3
+ import { isShape, TLParentId, TLRecord, TLShapeId, TLStore } from '@tldraw/tlschema'
4
4
  import { compact, sortByIndex } from '@tldraw/utils'
5
5
 
6
6
  type ParentShapeIdsToChildShapeIds = Record<TLParentId, TLShapeId[]>
@@ -11,17 +11,11 @@ function fromScratch(
11
11
  ) {
12
12
  const result: ParentShapeIdsToChildShapeIds = {}
13
13
  const shapeIds = shapeIdsQuery.get()
14
- const shapes = Array(shapeIds.size) as TLShape[]
15
- shapeIds.forEach((id) => shapes.push(store.get(id)!))
16
-
17
- // Sort the shapes by index
18
- shapes.sort(sortByIndex)
14
+ const sortedShapes = Array.from(shapeIds, (id) => store.get(id)!).sort(sortByIndex)
19
15
 
20
16
  // Populate the result object with an array for each parent.
21
- shapes.forEach((shape) => {
22
- if (!result[shape.parentId]) {
23
- result[shape.parentId] = []
24
- }
17
+ sortedShapes.forEach((shape) => {
18
+ result[shape.parentId] ??= []
25
19
  result[shape.parentId].push(shape.id)
26
20
  })
27
21
 
@@ -4,7 +4,7 @@ import { assertExists, uniqueId } from '@tldraw/utils'
4
4
  import { Vec } from '../../../primitives/Vec'
5
5
  import { Geometry2d } from '../../../primitives/geometry/Geometry2d'
6
6
  import { Editor } from '../../Editor'
7
- import { SnapData, SnapManager } from './SnapManager'
7
+ import { PointsSnapIndicator, SnapData, SnapManager } from './SnapManager'
8
8
 
9
9
  /**
10
10
  * When dragging a handle, users can snap the handle to key geometry on other nearby shapes.
@@ -43,6 +43,11 @@ export interface HandleSnapGeometry {
43
43
  getSelfSnapPoints?(handle: TLHandle): VecModel[]
44
44
  }
45
45
 
46
+ interface AlignPointsSnap {
47
+ snaps: PointsSnapIndicator[]
48
+ nudge: Vec
49
+ }
50
+
46
51
  const defaultGetSelfSnapOutline = () => null
47
52
  const defaultGetSelfSnapPoints = () => []
48
53
  /** @public */
@@ -171,6 +176,67 @@ export class HandleSnaps {
171
176
  return null
172
177
  }
173
178
 
179
+ private getHandleSnapData({
180
+ handle,
181
+ currentShapeId,
182
+ }: {
183
+ handle: TLHandle
184
+ currentShapeId: TLShapeId
185
+ }): AlignPointsSnap | null {
186
+ const snapThreshold = this.manager.getSnapThreshold()
187
+ const currentShapeTransform = assertExists(this.editor.getShapePageTransform(currentShapeId))
188
+ const handleInPageSpace = currentShapeTransform.applyToPoint(handle)
189
+
190
+ let nearestXSnap: Vec | null = null
191
+ let nearestYSnap: Vec | null = null
192
+ let minOffsetX = snapThreshold
193
+ let minOffsetY = snapThreshold
194
+
195
+ for (const snapPoint of this.iterateSnapPointsInPageSpace(currentShapeId, handle)) {
196
+ const offsetX = Math.abs(handleInPageSpace.x - snapPoint.x)
197
+ const offsetY = Math.abs(handleInPageSpace.y - snapPoint.y)
198
+ if (offsetX < minOffsetX) {
199
+ minOffsetX = offsetX
200
+ nearestXSnap = snapPoint
201
+ }
202
+ if (offsetY < minOffsetY) {
203
+ minOffsetY = offsetY
204
+ nearestYSnap = snapPoint
205
+ }
206
+ }
207
+
208
+ if (!nearestXSnap && !nearestYSnap) {
209
+ return null
210
+ }
211
+
212
+ const nudge = new Vec(
213
+ nearestXSnap ? nearestXSnap.x - handleInPageSpace.x : 0,
214
+ nearestYSnap ? nearestYSnap.y - handleInPageSpace.y : 0
215
+ )
216
+
217
+ const snappedHandle = Vec.Add(handleInPageSpace, nudge)
218
+ const snaps: PointsSnapIndicator[] = []
219
+
220
+ if (nearestXSnap) {
221
+ const snappedHandleOnX = new Vec(nearestXSnap.x, snappedHandle.y)
222
+ snaps.push({
223
+ id: uniqueId(),
224
+ type: 'points',
225
+ points: [nearestXSnap, snappedHandleOnX],
226
+ })
227
+ }
228
+ if (nearestYSnap) {
229
+ const snappedHandleOnY = new Vec(snappedHandle.x, nearestYSnap.y)
230
+ snaps.push({
231
+ id: uniqueId(),
232
+ type: 'points',
233
+ points: [nearestYSnap, snappedHandleOnY],
234
+ })
235
+ }
236
+
237
+ return { snaps, nudge }
238
+ }
239
+
174
240
  snapHandle({
175
241
  currentShapeId,
176
242
  handle,
@@ -180,10 +246,16 @@ export class HandleSnaps {
180
246
  }): SnapData | null {
181
247
  const currentShapeTransform = assertExists(this.editor.getShapePageTransform(currentShapeId))
182
248
  const handleInPageSpace = currentShapeTransform.applyToPoint(handle)
183
- const snapPosition = this.getHandleSnapPosition({ currentShapeId, handle, handleInPageSpace })
249
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
250
+ const snapType = handle.canSnap ? 'point' : handle.snapType
251
+
252
+ if (snapType === 'point') {
253
+ const snapPosition = this.getHandleSnapPosition({ currentShapeId, handle, handleInPageSpace })
254
+
255
+ if (!snapPosition) {
256
+ return null
257
+ }
184
258
 
185
- // If we found a point, display snap lines, and return the nudge
186
- if (snapPosition) {
187
259
  this.manager.setIndicators([
188
260
  {
189
261
  id: uniqueId(),
@@ -195,6 +267,21 @@ export class HandleSnaps {
195
267
  return { nudge: Vec.Sub(snapPosition, handleInPageSpace) }
196
268
  }
197
269
 
270
+ if (snapType === 'align') {
271
+ const snapData = this.getHandleSnapData({
272
+ handle,
273
+ currentShapeId,
274
+ })
275
+
276
+ if (!snapData) {
277
+ return null
278
+ }
279
+
280
+ this.manager.setIndicators(snapData.snaps)
281
+
282
+ return { nudge: snapData.nudge }
283
+ }
284
+
198
285
  return null
199
286
  }
200
287
  }
@@ -1,6 +1,7 @@
1
1
  import { useValue } from '@tldraw/state-react'
2
2
  import React, { useEffect, useMemo } from 'react'
3
3
  import { RIGHT_MOUSE_BUTTON } from '../constants'
4
+ import { tlenv } from '../globals/environment'
4
5
  import { preventDefault, releasePointerCapture, setPointerCapture } from '../utils/dom'
5
6
  import { getPointerInfo } from '../utils/getPointerInfo'
6
7
  import { useEditor } from './useEditor'
@@ -161,8 +162,14 @@ export function useCanvasEvents() {
161
162
  // For tools that benefit from a higher fidelity of events,
162
163
  // we dispatch the coalesced events.
163
164
  // N.B. Sometimes getCoalescedEvents isn't present on iOS, ugh.
165
+ // Specifically, in local mode (non-https) mode, iOS does not `useCoalescedEvents`
166
+ // so it appears like the ink is working locally, when really it's just that `useCoalescedEvents`
167
+ // is disabled. The intent here is to have `useCoalescedEvents` disabled for iOS.
164
168
  const events =
165
- currentTool.useCoalescedEvents && e.getCoalescedEvents ? e.getCoalescedEvents() : [e]
169
+ !tlenv.isIos && currentTool.useCoalescedEvents && e.getCoalescedEvents
170
+ ? e.getCoalescedEvents()
171
+ : [e]
172
+
166
173
  for (const singleEvent of events) {
167
174
  editor.dispatch({
168
175
  type: 'pointer',
@@ -171,6 +171,14 @@ export class LicenseManager {
171
171
  url.searchParams.set('license_type', trackType)
172
172
  if ('license' in result) {
173
173
  url.searchParams.set('license_id', result.license.id)
174
+ const sku = this.isFlagEnabled(result.license.flags, FLAGS.EVALUATION_LICENSE)
175
+ ? 'evaluation'
176
+ : this.isFlagEnabled(result.license.flags, FLAGS.ANNUAL_LICENSE)
177
+ ? 'annual'
178
+ : this.isFlagEnabled(result.license.flags, FLAGS.PERPETUAL_LICENSE)
179
+ ? 'perpetual'
180
+ : 'unknown'
181
+ url.searchParams.set('sku', sku)
174
182
  }
175
183
  if (process.env.NODE_ENV) {
176
184
  url.searchParams.set('environment', process.env.NODE_ENV)
@@ -120,6 +120,8 @@ export abstract class Geometry2d {
120
120
  distanceToLineSegment(A: VecLike, B: VecLike, filters?: Geometry2dFilters) {
121
121
  if (Vec.Equals(A, B)) return this.distanceToPoint(A, false, filters)
122
122
  const { vertices } = this
123
+ if (vertices.length === 0) throw Error('nearest point not found')
124
+ if (vertices.length === 1) return Vec.Dist(A, vertices[0])
123
125
  let nearest: Vec | undefined
124
126
  let dist = Infinity
125
127
  let d: number, p: Vec, q: Vec
@@ -175,6 +177,8 @@ export abstract class Geometry2d {
175
177
  interpolateAlongEdge(t: number, _filters?: Geometry2dFilters): Vec {
176
178
  const { vertices } = this
177
179
 
180
+ if (vertices.length === 0) return new Vec(0, 0)
181
+ if (vertices.length === 1) return vertices[0]
178
182
  if (t <= 0) return vertices[0]
179
183
 
180
184
  const distanceToTravel = t * this.length
@@ -209,6 +213,8 @@ export abstract class Geometry2d {
209
213
  let closestDistance = Infinity
210
214
  let distanceTraveled = 0
211
215
 
216
+ if (vertices.length === 0 || vertices.length === 1) return 0
217
+
212
218
  for (let i = 0; i < (this.isClosed ? vertices.length : vertices.length - 1); i++) {
213
219
  const curr = vertices[i]
214
220
  const next = vertices[(i + 1) % vertices.length]
package/src/version.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  // This file is automatically generated by internal/scripts/refresh-assets.ts.
2
2
  // Do not edit manually. Or do, I'm a comment, not a cop.
3
3
 
4
- export const version = '4.1.0-canary.4f9226a36e7e'
4
+ export const version = '4.1.0-canary.533dd6427b33'
5
5
  export const publishDates = {
6
6
  major: '2025-09-18T14:39:22.803Z',
7
- minor: '2025-10-04T12:12:04.189Z',
8
- patch: '2025-10-04T12:12:04.189Z',
7
+ minor: '2025-10-15T10:04:14.590Z',
8
+ patch: '2025-10-15T10:04:14.590Z',
9
9
  }