@tldraw/editor 3.13.0-canary.fd867adaa211 → 3.14.0-canary.50ad892ef195

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 (147) hide show
  1. package/CHANGELOG.md +180 -0
  2. package/dist-cjs/index.d.ts +99 -99
  3. package/dist-cjs/index.js +7 -22
  4. package/dist-cjs/index.js.map +2 -2
  5. package/dist-cjs/lib/components/Shape.js +12 -8
  6. package/dist-cjs/lib/components/Shape.js.map +2 -2
  7. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +31 -8
  8. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  9. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +17 -11
  10. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
  11. package/dist-cjs/lib/editor/Editor.js +85 -24
  12. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  13. package/dist-cjs/lib/editor/managers/SnapManager/HandleSnaps.js.map +2 -2
  14. package/dist-cjs/lib/editor/managers/TextManager.js +10 -0
  15. package/dist-cjs/lib/editor/managers/TextManager.js.map +2 -2
  16. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +1 -1
  17. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  18. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +0 -3
  19. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +2 -2
  20. package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js.map +2 -2
  21. package/dist-cjs/lib/exports/getSvgJsx.js +12 -3
  22. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  23. package/dist-cjs/lib/hooks/useEditorComponents.js +1 -2
  24. package/dist-cjs/lib/hooks/useEditorComponents.js.map +2 -2
  25. package/dist-cjs/lib/primitives/Box.js +16 -0
  26. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  27. package/dist-cjs/lib/primitives/Mat.js +1 -1
  28. package/dist-cjs/lib/primitives/Mat.js.map +2 -2
  29. package/dist-cjs/lib/primitives/Vec.js +20 -0
  30. package/dist-cjs/lib/primitives/Vec.js.map +2 -2
  31. package/dist-cjs/lib/primitives/geometry/Arc2d.js +2 -2
  32. package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
  33. package/dist-cjs/lib/primitives/geometry/Circle2d.js +1 -1
  34. package/dist-cjs/lib/primitives/geometry/Circle2d.js.map +2 -2
  35. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js +1 -1
  36. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
  37. package/dist-cjs/lib/primitives/geometry/CubicSpline2d.js.map +2 -2
  38. package/dist-cjs/lib/primitives/geometry/Edge2d.js +1 -1
  39. package/dist-cjs/lib/primitives/geometry/Edge2d.js.map +2 -2
  40. package/dist-cjs/lib/primitives/geometry/Ellipse2d.js.map +2 -2
  41. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +91 -20
  42. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  43. package/dist-cjs/lib/primitives/geometry/Group2d.js +55 -2
  44. package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
  45. package/dist-cjs/lib/primitives/geometry/Point2d.js.map +2 -2
  46. package/dist-cjs/lib/primitives/geometry/Polyline2d.js.map +2 -2
  47. package/dist-cjs/lib/primitives/geometry/Stadium2d.js.map +2 -2
  48. package/dist-cjs/lib/utils/areShapesContentEqual.js +25 -0
  49. package/dist-cjs/lib/utils/areShapesContentEqual.js.map +7 -0
  50. package/dist-cjs/lib/utils/debug-flags.js +5 -2
  51. package/dist-cjs/lib/utils/debug-flags.js.map +2 -2
  52. package/dist-cjs/lib/utils/nearestMultiple.js +34 -0
  53. package/dist-cjs/lib/utils/nearestMultiple.js.map +7 -0
  54. package/dist-cjs/version.js +3 -3
  55. package/dist-cjs/version.js.map +1 -1
  56. package/dist-esm/index.d.mts +99 -99
  57. package/dist-esm/index.mjs +9 -41
  58. package/dist-esm/index.mjs.map +2 -2
  59. package/dist-esm/lib/components/Shape.mjs +12 -8
  60. package/dist-esm/lib/components/Shape.mjs.map +2 -2
  61. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +31 -8
  62. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  63. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +17 -11
  64. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
  65. package/dist-esm/lib/editor/Editor.mjs +85 -24
  66. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  67. package/dist-esm/lib/editor/managers/SnapManager/HandleSnaps.mjs.map +2 -2
  68. package/dist-esm/lib/editor/managers/TextManager.mjs +10 -0
  69. package/dist-esm/lib/editor/managers/TextManager.mjs.map +2 -2
  70. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +1 -1
  71. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  72. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +0 -3
  73. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +2 -2
  74. package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs.map +2 -2
  75. package/dist-esm/lib/exports/getSvgJsx.mjs +12 -3
  76. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  77. package/dist-esm/lib/hooks/useEditorComponents.mjs +1 -4
  78. package/dist-esm/lib/hooks/useEditorComponents.mjs.map +2 -2
  79. package/dist-esm/lib/primitives/Box.mjs +16 -0
  80. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  81. package/dist-esm/lib/primitives/Mat.mjs +1 -1
  82. package/dist-esm/lib/primitives/Mat.mjs.map +2 -2
  83. package/dist-esm/lib/primitives/Vec.mjs +20 -0
  84. package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
  85. package/dist-esm/lib/primitives/geometry/Arc2d.mjs +2 -2
  86. package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
  87. package/dist-esm/lib/primitives/geometry/Circle2d.mjs +1 -1
  88. package/dist-esm/lib/primitives/geometry/Circle2d.mjs.map +2 -2
  89. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +1 -1
  90. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
  91. package/dist-esm/lib/primitives/geometry/CubicSpline2d.mjs.map +2 -2
  92. package/dist-esm/lib/primitives/geometry/Edge2d.mjs +1 -1
  93. package/dist-esm/lib/primitives/geometry/Edge2d.mjs.map +2 -2
  94. package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs.map +2 -2
  95. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +92 -21
  96. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  97. package/dist-esm/lib/primitives/geometry/Group2d.mjs +55 -2
  98. package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
  99. package/dist-esm/lib/primitives/geometry/Point2d.mjs.map +2 -2
  100. package/dist-esm/lib/primitives/geometry/Polyline2d.mjs.map +2 -2
  101. package/dist-esm/lib/primitives/geometry/Stadium2d.mjs.map +2 -2
  102. package/dist-esm/lib/utils/areShapesContentEqual.mjs +5 -0
  103. package/dist-esm/lib/utils/areShapesContentEqual.mjs.map +7 -0
  104. package/dist-esm/lib/utils/debug-flags.mjs +5 -2
  105. package/dist-esm/lib/utils/debug-flags.mjs.map +2 -2
  106. package/dist-esm/lib/utils/nearestMultiple.mjs +14 -0
  107. package/dist-esm/lib/utils/nearestMultiple.mjs.map +7 -0
  108. package/dist-esm/version.mjs +3 -3
  109. package/dist-esm/version.mjs.map +1 -1
  110. package/editor.css +36 -4
  111. package/package.json +7 -7
  112. package/src/index.ts +16 -31
  113. package/src/lib/components/Shape.tsx +14 -10
  114. package/src/lib/components/default-components/DefaultCanvas.tsx +32 -8
  115. package/src/lib/components/default-components/DefaultShapeIndicator.tsx +17 -8
  116. package/src/lib/editor/Editor.test.ts +1 -1
  117. package/src/lib/editor/Editor.ts +96 -24
  118. package/src/lib/editor/managers/SnapManager/HandleSnaps.ts +0 -1
  119. package/src/lib/editor/managers/TextManager.ts +12 -0
  120. package/src/lib/editor/shapes/ShapeUtil.ts +10 -2
  121. package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +0 -4
  122. package/src/lib/editor/shapes/shared/getPerfectDashProps.ts +9 -9
  123. package/src/lib/exports/getSvgJsx.tsx +16 -7
  124. package/src/lib/hooks/useEditorComponents.tsx +2 -5
  125. package/src/lib/primitives/Box.ts +20 -0
  126. package/src/lib/primitives/Mat.ts +5 -4
  127. package/src/lib/primitives/Vec.ts +23 -0
  128. package/src/lib/primitives/geometry/Arc2d.ts +5 -5
  129. package/src/lib/primitives/geometry/Circle2d.ts +4 -4
  130. package/src/lib/primitives/geometry/CubicBezier2d.ts +4 -4
  131. package/src/lib/primitives/geometry/CubicSpline2d.ts +3 -3
  132. package/src/lib/primitives/geometry/Edge2d.ts +3 -3
  133. package/src/lib/primitives/geometry/Ellipse2d.ts +3 -3
  134. package/src/lib/primitives/geometry/Geometry2d.test.ts +42 -0
  135. package/src/lib/primitives/geometry/Geometry2d.ts +123 -35
  136. package/src/lib/primitives/geometry/Group2d.ts +70 -7
  137. package/src/lib/primitives/geometry/Point2d.ts +2 -2
  138. package/src/lib/primitives/geometry/Polyline2d.ts +3 -3
  139. package/src/lib/primitives/geometry/Stadium2d.ts +3 -3
  140. package/src/lib/test/currentToolIdMask.test.ts +1 -1
  141. package/src/lib/test/user.test.ts +1 -1
  142. package/src/lib/utils/areShapesContentEqual.ts +4 -0
  143. package/src/lib/utils/debug-flags.ts +7 -2
  144. package/src/lib/utils/nearestMultiple.ts +13 -0
  145. package/src/lib/utils/sync/LocalIndexedDb.test.ts +1 -1
  146. package/src/lib/utils/sync/TLLocalSyncClient.test.ts +1 -1
  147. package/src/version.ts +3 -3
@@ -0,0 +1,42 @@
1
+ import { Mat } from '../Mat'
2
+ import { Vec, VecLike } from '../Vec'
3
+ import { Geometry2dFilters } from './Geometry2d'
4
+ import { Rectangle2d } from './Rectangle2d'
5
+
6
+ describe('TransformedGeometry2d', () => {
7
+ const rect = new Rectangle2d({ width: 100, height: 50, isFilled: true }).transform(
8
+ Mat.Translate(50, 100).scale(2, 2)
9
+ )
10
+
11
+ test('getVertices', () => {
12
+ expect(rect.getVertices(Geometry2dFilters.INCLUDE_ALL)).toMatchObject([
13
+ { x: 50, y: 100, z: 1 },
14
+ { x: 250, y: 100, z: 1 },
15
+ { x: 250, y: 200, z: 1 },
16
+ { x: 50, y: 200, z: 1 },
17
+ ])
18
+ })
19
+
20
+ test('nearestPoint', () => {
21
+ expectApproxMatch(rect.nearestPoint(new Vec(100, 300)), { x: 100, y: 200 })
22
+ })
23
+
24
+ test('hitTestPoint', () => {
25
+ // basic case - no margin / scaling:
26
+ expect(rect.hitTestPoint(new Vec(0, 0), 0, true)).toBe(false)
27
+ expect(rect.hitTestPoint(new Vec(50, 100), 0, true)).toBe(true)
28
+ expect(rect.hitTestPoint(new Vec(49, 100), 0, true)).toBe(false)
29
+ expect(rect.hitTestPoint(new Vec(100, 150), 0, true)).toBe(true)
30
+
31
+ // with margin:
32
+ // move away 8 px and test with 10px margin:
33
+ expect(rect.hitTestPoint(new Vec(42, 100), 10, true)).toBe(true)
34
+ // move away 12 px and test with 10px margin:
35
+ expect(rect.hitTestPoint(new Vec(38, 100), 10, true)).toBe(false)
36
+ })
37
+ })
38
+
39
+ function expectApproxMatch(a: VecLike, b: VecLike) {
40
+ expect(a.x).toBeCloseTo(b.x, 0.0001)
41
+ expect(a.y).toBeCloseTo(b.y, 0.0001)
42
+ }
@@ -1,4 +1,4 @@
1
- import { assert } from '@tldraw/utils'
1
+ import { assert, invLerp } from '@tldraw/utils'
2
2
  import { Box } from '../Box'
3
3
  import { Mat, MatModel } from '../Mat'
4
4
  import { Vec, VecLike } from '../Vec'
@@ -81,9 +81,9 @@ export abstract class Geometry2d {
81
81
 
82
82
  abstract getVertices(filters: Geometry2dFilters): Vec[]
83
83
 
84
- abstract nearestPoint(point: Vec, _filters?: Geometry2dFilters): Vec
84
+ abstract nearestPoint(point: VecLike, _filters?: Geometry2dFilters): Vec
85
85
 
86
- hitTestPoint(point: Vec, margin = 0, hitInside = false, _filters?: Geometry2dFilters) {
86
+ hitTestPoint(point: VecLike, margin = 0, hitInside = false, _filters?: Geometry2dFilters) {
87
87
  // First check whether the point is inside
88
88
  if (this.isClosed && (this.isFilled || hitInside) && pointInPolygon(point, this.vertices)) {
89
89
  return true
@@ -92,17 +92,17 @@ export abstract class Geometry2d {
92
92
  return Vec.Dist2(point, this.nearestPoint(point)) <= margin * margin
93
93
  }
94
94
 
95
- distanceToPoint(point: Vec, hitInside = false, filters?: Geometry2dFilters) {
95
+ distanceToPoint(point: VecLike, hitInside = false, filters?: Geometry2dFilters) {
96
96
  return (
97
- point.dist(this.nearestPoint(point, filters)) *
97
+ Vec.Dist(point, this.nearestPoint(point, filters)) *
98
98
  (this.isClosed && (this.isFilled || hitInside) && pointInPolygon(point, this.vertices)
99
99
  ? -1
100
100
  : 1)
101
101
  )
102
102
  }
103
103
 
104
- distanceToLineSegment(A: Vec, B: Vec, filters?: Geometry2dFilters) {
105
- if (A.equals(B)) return this.distanceToPoint(A, false, filters)
104
+ distanceToLineSegment(A: VecLike, B: VecLike, filters?: Geometry2dFilters) {
105
+ if (Vec.Equals(A, B)) return this.distanceToPoint(A, false, filters)
106
106
  const { vertices } = this
107
107
  let nearest: Vec | undefined
108
108
  let dist = Infinity
@@ -120,7 +120,7 @@ export abstract class Geometry2d {
120
120
  return this.isClosed && this.isFilled && pointInPolygon(nearest, this.vertices) ? -dist : dist
121
121
  }
122
122
 
123
- hitTestLineSegment(A: Vec, B: Vec, distance = 0, filters?: Geometry2dFilters): boolean {
123
+ hitTestLineSegment(A: VecLike, B: VecLike, distance = 0, filters?: Geometry2dFilters): boolean {
124
124
  return this.distanceToLineSegment(A, B, filters) <= distance
125
125
  }
126
126
 
@@ -148,8 +148,76 @@ export abstract class Geometry2d {
148
148
  return intersectPolys(polyline, this.vertices, false, this.isClosed)
149
149
  }
150
150
 
151
+ /**
152
+ * Find a point along the edge of the geometry that is a fraction `t` along the entire way round.
153
+ */
154
+ interpolateAlongEdge(t: number, _filters?: Geometry2dFilters): Vec {
155
+ const { vertices } = this
156
+
157
+ if (t <= 0) return vertices[0]
158
+
159
+ const distanceToTravel = t * this.length
160
+ let distanceTraveled = 0
161
+
162
+ for (let i = 0; i < (this.isClosed ? vertices.length : vertices.length - 1); i++) {
163
+ const curr = vertices[i]
164
+ const next = vertices[(i + 1) % vertices.length]
165
+ const dist = Vec.Dist(curr, next)
166
+ const newDistanceTraveled = distanceTraveled + dist
167
+ if (newDistanceTraveled >= distanceToTravel) {
168
+ const p = Vec.Lrp(
169
+ curr,
170
+ next,
171
+ invLerp(distanceTraveled, newDistanceTraveled, distanceToTravel)
172
+ )
173
+ return p
174
+ }
175
+ distanceTraveled = newDistanceTraveled
176
+ }
177
+
178
+ return this.isClosed ? vertices[0] : vertices[vertices.length - 1]
179
+ }
180
+
181
+ /**
182
+ * Take `point`, find the closest point to it on the edge of the geometry, and return how far
183
+ * along the edge it is as a fraction of the total length.
184
+ */
185
+ uninterpolateAlongEdge(point: VecLike, _filters?: Geometry2dFilters): number {
186
+ const { vertices, length } = this
187
+ let closestSegment = null
188
+ let closestDistance = Infinity
189
+ let distanceTraveled = 0
190
+
191
+ for (let i = 0; i < (this.isClosed ? vertices.length : vertices.length - 1); i++) {
192
+ const curr = vertices[i]
193
+ const next = vertices[(i + 1) % vertices.length]
194
+
195
+ const nearestPoint = Vec.NearestPointOnLineSegment(curr, next, point, true)
196
+ const distance = Vec.Dist(nearestPoint, point)
197
+
198
+ if (distance < closestDistance) {
199
+ closestDistance = distance
200
+ closestSegment = {
201
+ start: curr,
202
+ end: next,
203
+ nearestPoint,
204
+ distanceToStart: distanceTraveled,
205
+ }
206
+ }
207
+
208
+ distanceTraveled += Vec.Dist(curr, next)
209
+ }
210
+
211
+ assert(closestSegment)
212
+
213
+ const distanceAlongRoute =
214
+ closestSegment.distanceToStart + Vec.Dist(closestSegment.start, closestSegment.nearestPoint)
215
+
216
+ return distanceAlongRoute / length
217
+ }
218
+
151
219
  /** @deprecated Iterate the vertices instead. */
152
- nearestPointOnLineSegment(A: Vec, B: Vec): Vec {
220
+ nearestPointOnLineSegment(A: VecLike, B: VecLike): Vec {
153
221
  const { vertices } = this
154
222
  let nearest: Vec | undefined
155
223
  let dist = Infinity
@@ -167,7 +235,7 @@ export abstract class Geometry2d {
167
235
  return nearest
168
236
  }
169
237
 
170
- isPointInBounds(point: Vec, margin = 0) {
238
+ isPointInBounds(point: VecLike, margin = 0) {
171
239
  const { bounds } = this
172
240
  return !(
173
241
  point.x < bounds.minX - margin ||
@@ -261,21 +329,24 @@ export abstract class Geometry2d {
261
329
  // eslint-disable-next-line no-restricted-syntax
262
330
  get length() {
263
331
  if (this._length) return this._length
264
- this._length = this.getLength()
332
+ this._length = this.getLength(Geometry2dFilters.EXCLUDE_LABELS)
265
333
  return this._length
266
334
  }
267
335
 
268
- getLength() {
269
- const { vertices } = this
270
- let n1: Vec,
271
- p1 = vertices[0],
272
- length = 0
336
+ getLength(_filters?: Geometry2dFilters) {
337
+ const vertices = this.getVertices(_filters ?? Geometry2dFilters.EXCLUDE_LABELS)
338
+ if (vertices.length === 0) return 0
339
+ let prev = vertices[0]
340
+ let length = 0
273
341
  for (let i = 1; i < vertices.length; i++) {
274
- n1 = vertices[i]
275
- length += Vec.Dist2(p1, n1)
276
- p1 = n1
342
+ const next = vertices[i]
343
+ length += Vec.Dist(prev, next)
344
+ prev = next
345
+ }
346
+ if (this.isClosed) {
347
+ length += Vec.Dist(vertices[vertices.length - 1], vertices[0])
277
348
  }
278
- return Math.sqrt(length)
349
+ return length
279
350
  }
280
351
 
281
352
  abstract getSvgPathData(first: boolean): string
@@ -317,7 +388,7 @@ export class TransformedGeometry2d extends Geometry2d {
317
388
  return this.geometry.getVertices(filters).map((v) => Mat.applyToPoint(this.matrix, v))
318
389
  }
319
390
 
320
- nearestPoint(point: Vec, filters?: Geometry2dFilters): Vec {
391
+ nearestPoint(point: VecLike, filters?: Geometry2dFilters): Vec {
321
392
  return Mat.applyToPoint(
322
393
  this.matrix,
323
394
  this.geometry.nearestPoint(Mat.applyToPoint(this.inverse, point), filters)
@@ -325,7 +396,7 @@ export class TransformedGeometry2d extends Geometry2d {
325
396
  }
326
397
 
327
398
  override hitTestPoint(
328
- point: Vec,
399
+ point: VecLike,
329
400
  margin = 0,
330
401
  hitInside?: boolean,
331
402
  filters?: Geometry2dFilters
@@ -338,14 +409,14 @@ export class TransformedGeometry2d extends Geometry2d {
338
409
  )
339
410
  }
340
411
 
341
- override distanceToPoint(point: Vec, hitInside = false, filters?: Geometry2dFilters) {
412
+ override distanceToPoint(point: VecLike, hitInside = false, filters?: Geometry2dFilters) {
342
413
  return (
343
414
  this.geometry.distanceToPoint(Mat.applyToPoint(this.inverse, point), hitInside, filters) *
344
415
  this.decomposed.scaleX
345
416
  )
346
417
  }
347
418
 
348
- override distanceToLineSegment(A: Vec, B: Vec, filters?: Geometry2dFilters) {
419
+ override distanceToLineSegment(A: VecLike, B: VecLike, filters?: Geometry2dFilters) {
349
420
  return (
350
421
  this.geometry.distanceToLineSegment(
351
422
  Mat.applyToPoint(this.inverse, A),
@@ -355,7 +426,12 @@ export class TransformedGeometry2d extends Geometry2d {
355
426
  )
356
427
  }
357
428
 
358
- override hitTestLineSegment(A: Vec, B: Vec, distance = 0, filters?: Geometry2dFilters): boolean {
429
+ override hitTestLineSegment(
430
+ A: VecLike,
431
+ B: VecLike,
432
+ distance = 0,
433
+ filters?: Geometry2dFilters
434
+ ): boolean {
359
435
  return this.geometry.hitTestLineSegment(
360
436
  Mat.applyToPoint(this.inverse, A),
361
437
  Mat.applyToPoint(this.inverse, B),
@@ -365,27 +441,39 @@ export class TransformedGeometry2d extends Geometry2d {
365
441
  }
366
442
 
367
443
  override intersectLineSegment(A: VecLike, B: VecLike, filters?: Geometry2dFilters) {
368
- return this.geometry.intersectLineSegment(
369
- Mat.applyToPoint(this.inverse, A),
370
- Mat.applyToPoint(this.inverse, B),
371
- filters
444
+ return Mat.applyToPoints(
445
+ this.matrix,
446
+ this.geometry.intersectLineSegment(
447
+ Mat.applyToPoint(this.inverse, A),
448
+ Mat.applyToPoint(this.inverse, B),
449
+ filters
450
+ )
372
451
  )
373
452
  }
374
453
 
375
454
  override intersectCircle(center: VecLike, radius: number, filters?: Geometry2dFilters) {
376
- return this.geometry.intersectCircle(
377
- Mat.applyToPoint(this.inverse, center),
378
- radius / this.decomposed.scaleX,
379
- filters
455
+ return Mat.applyToPoints(
456
+ this.matrix,
457
+ this.geometry.intersectCircle(
458
+ Mat.applyToPoint(this.inverse, center),
459
+ radius / this.decomposed.scaleX,
460
+ filters
461
+ )
380
462
  )
381
463
  }
382
464
 
383
465
  override intersectPolygon(polygon: VecLike[], filters?: Geometry2dFilters): VecLike[] {
384
- return this.geometry.intersectPolygon(Mat.applyToPoints(this.inverse, polygon), filters)
466
+ return Mat.applyToPoints(
467
+ this.matrix,
468
+ this.geometry.intersectPolygon(Mat.applyToPoints(this.inverse, polygon), filters)
469
+ )
385
470
  }
386
471
 
387
472
  override intersectPolyline(polyline: VecLike[], filters?: Geometry2dFilters): VecLike[] {
388
- return this.geometry.intersectPolyline(Mat.applyToPoints(this.inverse, polyline), filters)
473
+ return Mat.applyToPoints(
474
+ this.matrix,
475
+ this.geometry.intersectPolyline(Mat.applyToPoints(this.inverse, polyline), filters)
476
+ )
389
477
  }
390
478
 
391
479
  override transform(transform: MatModel, opts?: TransformedGeometry2dOptions): Geometry2d {
@@ -1,4 +1,5 @@
1
1
  import { EMPTY_ARRAY } from '@tldraw/state'
2
+ import { assert, invLerp, lerp } from '@tldraw/utils'
2
3
  import { Box } from '../Box'
3
4
  import { Mat } from '../Mat'
4
5
  import { Vec, VecLike } from '../Vec'
@@ -34,7 +35,7 @@ export class Group2d extends Geometry2d {
34
35
  .flatMap((c) => c.getVertices(filters))
35
36
  }
36
37
 
37
- override nearestPoint(point: Vec, filters?: Geometry2dFilters): Vec {
38
+ override nearestPoint(point: VecLike, filters?: Geometry2dFilters): Vec {
38
39
  let dist = Infinity
39
40
  let nearest: Vec | undefined
40
41
 
@@ -59,7 +60,7 @@ export class Group2d extends Geometry2d {
59
60
  return nearest
60
61
  }
61
62
 
62
- override distanceToPoint(point: Vec, hitInside = false, filters?: Geometry2dFilters) {
63
+ override distanceToPoint(point: VecLike, hitInside = false, filters?: Geometry2dFilters) {
63
64
  let smallestDistance = Infinity
64
65
  for (const child of this.children) {
65
66
  if (child.isExcludedByFilter(filters)) continue
@@ -72,7 +73,7 @@ export class Group2d extends Geometry2d {
72
73
  }
73
74
 
74
75
  override hitTestPoint(
75
- point: Vec,
76
+ point: VecLike,
76
77
  margin: number,
77
78
  hitInside: boolean,
78
79
  filters = Geometry2dFilters.EXCLUDE_LABELS
@@ -83,8 +84,8 @@ export class Group2d extends Geometry2d {
83
84
  }
84
85
 
85
86
  override hitTestLineSegment(
86
- A: Vec,
87
- B: Vec,
87
+ A: VecLike,
88
+ B: VecLike,
88
89
  zoom: number,
89
90
  filters = Geometry2dFilters.EXCLUDE_LABELS
90
91
  ): boolean {
@@ -121,6 +122,63 @@ export class Group2d extends Geometry2d {
121
122
  })
122
123
  }
123
124
 
125
+ override interpolateAlongEdge(t: number, filters?: Geometry2dFilters): Vec {
126
+ const totalLength = this.getLength(filters)
127
+
128
+ const distanceToTravel = t * totalLength
129
+ let distanceTraveled = 0
130
+ for (const child of this.children) {
131
+ if (child.isExcludedByFilter(filters)) continue
132
+ const childLength = child.length
133
+ const newDistanceTraveled = distanceTraveled + childLength
134
+ if (newDistanceTraveled >= distanceToTravel) {
135
+ return child.interpolateAlongEdge(
136
+ invLerp(distanceTraveled, newDistanceTraveled, distanceToTravel),
137
+ filters
138
+ )
139
+ }
140
+ distanceTraveled = newDistanceTraveled
141
+ }
142
+
143
+ return this.children[this.children.length - 1].interpolateAlongEdge(1, filters)
144
+ }
145
+
146
+ override uninterpolateAlongEdge(point: VecLike, filters?: Geometry2dFilters): number {
147
+ const totalLength = this.getLength(filters)
148
+
149
+ let closestChild = null
150
+ let closestDistance = Infinity
151
+ let distanceTraveled = 0
152
+
153
+ for (const child of this.children) {
154
+ if (child.isExcludedByFilter(filters)) continue
155
+ const childLength = child.getLength(filters)
156
+ const newDistanceTraveled = distanceTraveled + childLength
157
+
158
+ const distance = child.distanceToPoint(point, false, filters)
159
+ if (distance < closestDistance) {
160
+ closestDistance = distance
161
+ closestChild = {
162
+ startLength: distanceTraveled,
163
+ endLength: newDistanceTraveled,
164
+ child,
165
+ }
166
+ }
167
+
168
+ distanceTraveled = newDistanceTraveled
169
+ }
170
+
171
+ assert(closestChild)
172
+
173
+ const normalizedDistanceInChild = closestChild.child.uninterpolateAlongEdge(point, filters)
174
+ const childTLength = lerp(
175
+ closestChild.startLength,
176
+ closestChild.endLength,
177
+ normalizedDistanceInChild
178
+ )
179
+ return childTLength / totalLength
180
+ }
181
+
124
182
  override transform(transform: Mat): Geometry2d {
125
183
  return new Group2d({
126
184
  children: this.children.map((c) => c.transform(transform)),
@@ -160,8 +218,13 @@ export class Group2d extends Geometry2d {
160
218
  return path
161
219
  }
162
220
 
163
- getLength(): number {
164
- return this.children.reduce((a, c) => (c.isLabel ? a : a + c.length), 0)
221
+ getLength(filters?: Geometry2dFilters): number {
222
+ let length = 0
223
+ for (const child of this.children) {
224
+ if (child.isExcludedByFilter(filters)) continue
225
+ length += child.length
226
+ }
227
+ return length
165
228
  }
166
229
 
167
230
  getSvgPathData(): string {
@@ -1,4 +1,4 @@
1
- import { Vec } from '../Vec'
1
+ import { Vec, VecLike } from '../Vec'
2
2
  import { Geometry2d, Geometry2dOptions } from './Geometry2d'
3
3
 
4
4
  /** @public */
@@ -22,7 +22,7 @@ export class Point2d extends Geometry2d {
22
22
  return this.point
23
23
  }
24
24
 
25
- hitTestLineSegment(A: Vec, B: Vec, margin: number): boolean {
25
+ hitTestLineSegment(A: VecLike, B: VecLike, margin: number): boolean {
26
26
  return Vec.DistanceToLineSegment(A, B, this.point) < margin
27
27
  }
28
28
 
@@ -1,4 +1,4 @@
1
- import { Vec } from '../Vec'
1
+ import { Vec, VecLike } from '../Vec'
2
2
  import { Edge2d } from './Edge2d'
3
3
  import { Geometry2d, Geometry2dOptions } from './Geometry2d'
4
4
 
@@ -41,7 +41,7 @@ export class Polyline2d extends Geometry2d {
41
41
  return this.points
42
42
  }
43
43
 
44
- nearestPoint(A: Vec): Vec {
44
+ nearestPoint(A: VecLike): Vec {
45
45
  const { segments } = this
46
46
  let nearest = this.points[0]
47
47
  let dist = Infinity
@@ -59,7 +59,7 @@ export class Polyline2d extends Geometry2d {
59
59
  return nearest
60
60
  }
61
61
 
62
- hitTestLineSegment(A: Vec, B: Vec, distance = 0): boolean {
62
+ hitTestLineSegment(A: VecLike, B: VecLike, distance = 0): boolean {
63
63
  const { segments } = this
64
64
  for (let i = 0, n = segments.length; i < n; i++) {
65
65
  if (segments[i].hitTestLineSegment(A, B, distance)) {
@@ -1,5 +1,5 @@
1
1
  import { Box } from '../Box'
2
- import { Vec } from '../Vec'
2
+ import { Vec, VecLike } from '../Vec'
3
3
  import { PI } from '../utils'
4
4
  import { Arc2d } from './Arc2d'
5
5
  import { Edge2d } from './Edge2d'
@@ -65,7 +65,7 @@ export class Stadium2d extends Geometry2d {
65
65
  }
66
66
  }
67
67
 
68
- nearestPoint(A: Vec): Vec {
68
+ nearestPoint(A: VecLike): Vec {
69
69
  let nearest: Vec | undefined
70
70
  let dist = Infinity
71
71
  let _d: number
@@ -84,7 +84,7 @@ export class Stadium2d extends Geometry2d {
84
84
  return nearest
85
85
  }
86
86
 
87
- hitTestLineSegment(A: Vec, B: Vec): boolean {
87
+ hitTestLineSegment(A: VecLike, B: VecLike): boolean {
88
88
  const { a, b, c, d } = this
89
89
  return [a, b, c, d].some((edge) => edge.hitTestLineSegment(A, B))
90
90
  }
@@ -27,7 +27,7 @@ beforeEach(() => {
27
27
  shapeUtils: [],
28
28
  bindingUtils: [],
29
29
  tools: [A, B, C],
30
- store: createTLStore({ shapeUtils: [] }),
30
+ store: createTLStore({ shapeUtils: [], bindingUtils: [] }),
31
31
  getContainer: () => document.body,
32
32
  })
33
33
  })
@@ -8,7 +8,7 @@ beforeEach(() => {
8
8
  shapeUtils: [],
9
9
  bindingUtils: [],
10
10
  tools: [],
11
- store: createTLStore({ shapeUtils: [] }),
11
+ store: createTLStore({ shapeUtils: [], bindingUtils: [] }),
12
12
  getContainer: () => document.body,
13
13
  })
14
14
  })
@@ -0,0 +1,4 @@
1
+ import { TLShape } from '@tldraw/tlschema'
2
+
3
+ export const areShapesContentEqual = (a: TLShape, b: TLShape) =>
4
+ a.props === b.props && a.meta === b.meta
@@ -54,6 +54,7 @@ export const debugFlags = {
54
54
  hideShapes: createDebugValue('hideShapes', { defaults: { all: false } }),
55
55
  editOnType: createDebugValue('editOnType', { defaults: { all: false } }),
56
56
  a11y: createDebugValue('a11y', { defaults: { all: false } }),
57
+ debugElbowArrows: createDebugValue('debugElbowArrows', { defaults: { all: false } }),
57
58
  } as const
58
59
 
59
60
  declare global {
@@ -149,7 +150,9 @@ function createDebugValueBase<T>(def: DebugFlagDef<T>): DebugFlag<T> {
149
150
  })
150
151
  }
151
152
 
152
- return Object.assign(valueAtom, def)
153
+ return Object.assign(valueAtom, def, {
154
+ reset: () => valueAtom.set(defaultValue),
155
+ })
153
156
  }
154
157
 
155
158
  function getStoredInitialValue(name: string) {
@@ -206,4 +209,6 @@ export interface DebugFlagDef<T> {
206
209
  }
207
210
 
208
211
  /** @internal */
209
- export type DebugFlag<T> = DebugFlagDef<T> & Atom<T>
212
+ export interface DebugFlag<T> extends DebugFlagDef<T>, Atom<T> {
213
+ reset(): void
214
+ }
@@ -0,0 +1,13 @@
1
+ // Euclidean algorithm to find the GCD
2
+ function gcd(a: number, b: number): number {
3
+ return b === 0 ? a : gcd(b, a % b)
4
+ }
5
+
6
+ // Returns the lowest value that the given number can be multiplied by to reach an integer
7
+ export function nearestMultiple(float: number) {
8
+ const decimal = float.toString().split('.')[1]
9
+ if (!decimal) return 1
10
+ const denominator = Math.pow(10, decimal.length)
11
+ const numerator = parseInt(decimal, 10)
12
+ return denominator / gcd(numerator, denominator)
13
+ }
@@ -3,7 +3,7 @@ import { openDB } from 'idb'
3
3
  import { hardReset } from './hardReset'
4
4
  import { getAllIndexDbNames, LocalIndexedDb } from './LocalIndexedDb'
5
5
 
6
- const schema = createTLSchema({ shapes: {} })
6
+ const schema = createTLSchema({ shapes: {}, bindings: {} })
7
7
  describe('LocalIndexedDb', () => {
8
8
  beforeEach(() => {
9
9
  jest.useRealTimers()
@@ -19,7 +19,7 @@ class BroadcastChannelMock {
19
19
  }
20
20
 
21
21
  function testClient(channel = new BroadcastChannelMock('test')) {
22
- const store = createTLStore({ shapeUtils: [] })
22
+ const store = createTLStore({ shapeUtils: [], bindingUtils: [] })
23
23
  const onLoad = jest.fn(() => {
24
24
  return
25
25
  })
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 = '3.13.0-canary.fd867adaa211'
4
+ export const version = '3.14.0-canary.50ad892ef195'
5
5
  export const publishDates = {
6
6
  major: '2024-09-13T14:36:29.063Z',
7
- minor: '2025-04-30T12:33:09.282Z',
8
- patch: '2025-04-30T12:33:09.282Z',
7
+ minor: '2025-05-23T08:41:15.515Z',
8
+ patch: '2025-05-23T08:41:15.515Z',
9
9
  }