@tldraw/editor 3.13.0-canary.ce8e6cffa809 → 3.13.0-canary.d6fe56e14603

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 (134) hide show
  1. package/dist-cjs/index.d.ts +95 -98
  2. package/dist-cjs/index.js +7 -22
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/components/Shape.js +12 -8
  5. package/dist-cjs/lib/components/Shape.js.map +2 -2
  6. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +21 -2
  7. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  8. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +17 -11
  9. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
  10. package/dist-cjs/lib/editor/Editor.js +33 -14
  11. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  12. package/dist-cjs/lib/editor/managers/SnapManager/HandleSnaps.js.map +2 -2
  13. package/dist-cjs/lib/editor/managers/TextManager.js +10 -0
  14. package/dist-cjs/lib/editor/managers/TextManager.js.map +2 -2
  15. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  16. package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js.map +2 -2
  17. package/dist-cjs/lib/exports/getSvgJsx.js +12 -3
  18. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  19. package/dist-cjs/lib/primitives/Box.js +16 -0
  20. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  21. package/dist-cjs/lib/primitives/Mat.js +1 -1
  22. package/dist-cjs/lib/primitives/Mat.js.map +2 -2
  23. package/dist-cjs/lib/primitives/Vec.js +20 -0
  24. package/dist-cjs/lib/primitives/Vec.js.map +2 -2
  25. package/dist-cjs/lib/primitives/geometry/Arc2d.js +2 -2
  26. package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
  27. package/dist-cjs/lib/primitives/geometry/Circle2d.js +1 -1
  28. package/dist-cjs/lib/primitives/geometry/Circle2d.js.map +2 -2
  29. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js +1 -1
  30. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
  31. package/dist-cjs/lib/primitives/geometry/CubicSpline2d.js.map +2 -2
  32. package/dist-cjs/lib/primitives/geometry/Edge2d.js +1 -1
  33. package/dist-cjs/lib/primitives/geometry/Edge2d.js.map +2 -2
  34. package/dist-cjs/lib/primitives/geometry/Ellipse2d.js.map +2 -2
  35. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +91 -20
  36. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  37. package/dist-cjs/lib/primitives/geometry/Group2d.js +55 -2
  38. package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
  39. package/dist-cjs/lib/primitives/geometry/Point2d.js.map +2 -2
  40. package/dist-cjs/lib/primitives/geometry/Polyline2d.js.map +2 -2
  41. package/dist-cjs/lib/primitives/geometry/Stadium2d.js.map +2 -2
  42. package/dist-cjs/lib/utils/areShapesContentEqual.js +25 -0
  43. package/dist-cjs/lib/utils/areShapesContentEqual.js.map +7 -0
  44. package/dist-cjs/lib/utils/debug-flags.js +5 -2
  45. package/dist-cjs/lib/utils/debug-flags.js.map +2 -2
  46. package/dist-cjs/lib/utils/nearestMultiple.js +34 -0
  47. package/dist-cjs/lib/utils/nearestMultiple.js.map +7 -0
  48. package/dist-cjs/version.js +3 -3
  49. package/dist-cjs/version.js.map +1 -1
  50. package/dist-esm/index.d.mts +95 -98
  51. package/dist-esm/index.mjs +9 -41
  52. package/dist-esm/index.mjs.map +2 -2
  53. package/dist-esm/lib/components/Shape.mjs +12 -8
  54. package/dist-esm/lib/components/Shape.mjs.map +2 -2
  55. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +21 -2
  56. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  57. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +17 -11
  58. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
  59. package/dist-esm/lib/editor/Editor.mjs +33 -14
  60. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  61. package/dist-esm/lib/editor/managers/SnapManager/HandleSnaps.mjs.map +2 -2
  62. package/dist-esm/lib/editor/managers/TextManager.mjs +10 -0
  63. package/dist-esm/lib/editor/managers/TextManager.mjs.map +2 -2
  64. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  65. package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs.map +2 -2
  66. package/dist-esm/lib/exports/getSvgJsx.mjs +12 -3
  67. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  68. package/dist-esm/lib/primitives/Box.mjs +16 -0
  69. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  70. package/dist-esm/lib/primitives/Mat.mjs +1 -1
  71. package/dist-esm/lib/primitives/Mat.mjs.map +2 -2
  72. package/dist-esm/lib/primitives/Vec.mjs +20 -0
  73. package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
  74. package/dist-esm/lib/primitives/geometry/Arc2d.mjs +2 -2
  75. package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
  76. package/dist-esm/lib/primitives/geometry/Circle2d.mjs +1 -1
  77. package/dist-esm/lib/primitives/geometry/Circle2d.mjs.map +2 -2
  78. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +1 -1
  79. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
  80. package/dist-esm/lib/primitives/geometry/CubicSpline2d.mjs.map +2 -2
  81. package/dist-esm/lib/primitives/geometry/Edge2d.mjs +1 -1
  82. package/dist-esm/lib/primitives/geometry/Edge2d.mjs.map +2 -2
  83. package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs.map +2 -2
  84. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +92 -21
  85. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  86. package/dist-esm/lib/primitives/geometry/Group2d.mjs +55 -2
  87. package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
  88. package/dist-esm/lib/primitives/geometry/Point2d.mjs.map +2 -2
  89. package/dist-esm/lib/primitives/geometry/Polyline2d.mjs.map +2 -2
  90. package/dist-esm/lib/primitives/geometry/Stadium2d.mjs.map +2 -2
  91. package/dist-esm/lib/utils/areShapesContentEqual.mjs +5 -0
  92. package/dist-esm/lib/utils/areShapesContentEqual.mjs.map +7 -0
  93. package/dist-esm/lib/utils/debug-flags.mjs +5 -2
  94. package/dist-esm/lib/utils/debug-flags.mjs.map +2 -2
  95. package/dist-esm/lib/utils/nearestMultiple.mjs +14 -0
  96. package/dist-esm/lib/utils/nearestMultiple.mjs.map +7 -0
  97. package/dist-esm/version.mjs +3 -3
  98. package/dist-esm/version.mjs.map +1 -1
  99. package/editor.css +29 -4
  100. package/package.json +7 -7
  101. package/src/index.ts +16 -31
  102. package/src/lib/components/Shape.tsx +14 -10
  103. package/src/lib/components/default-components/DefaultCanvas.tsx +21 -2
  104. package/src/lib/components/default-components/DefaultShapeIndicator.tsx +17 -8
  105. package/src/lib/editor/Editor.test.ts +1 -1
  106. package/src/lib/editor/Editor.ts +33 -14
  107. package/src/lib/editor/managers/SnapManager/HandleSnaps.ts +0 -1
  108. package/src/lib/editor/managers/TextManager.ts +12 -0
  109. package/src/lib/editor/shapes/ShapeUtil.ts +22 -2
  110. package/src/lib/editor/shapes/shared/getPerfectDashProps.ts +9 -9
  111. package/src/lib/exports/getSvgJsx.tsx +16 -7
  112. package/src/lib/primitives/Box.ts +20 -0
  113. package/src/lib/primitives/Mat.ts +5 -4
  114. package/src/lib/primitives/Vec.ts +23 -0
  115. package/src/lib/primitives/geometry/Arc2d.ts +5 -5
  116. package/src/lib/primitives/geometry/Circle2d.ts +4 -4
  117. package/src/lib/primitives/geometry/CubicBezier2d.ts +4 -4
  118. package/src/lib/primitives/geometry/CubicSpline2d.ts +3 -3
  119. package/src/lib/primitives/geometry/Edge2d.ts +3 -3
  120. package/src/lib/primitives/geometry/Ellipse2d.ts +3 -3
  121. package/src/lib/primitives/geometry/Geometry2d.test.ts +42 -0
  122. package/src/lib/primitives/geometry/Geometry2d.ts +123 -35
  123. package/src/lib/primitives/geometry/Group2d.ts +70 -7
  124. package/src/lib/primitives/geometry/Point2d.ts +2 -2
  125. package/src/lib/primitives/geometry/Polyline2d.ts +3 -3
  126. package/src/lib/primitives/geometry/Stadium2d.ts +3 -3
  127. package/src/lib/test/currentToolIdMask.test.ts +1 -1
  128. package/src/lib/test/user.test.ts +1 -1
  129. package/src/lib/utils/areShapesContentEqual.ts +4 -0
  130. package/src/lib/utils/debug-flags.ts +7 -2
  131. package/src/lib/utils/nearestMultiple.ts +13 -0
  132. package/src/lib/utils/sync/LocalIndexedDb.test.ts +1 -1
  133. package/src/lib/utils/sync/TLLocalSyncClient.test.ts +1 -1
  134. package/src/version.ts +3 -3
@@ -57,6 +57,11 @@ export class Box {
57
57
  this.x = n
58
58
  }
59
59
 
60
+ // eslint-disable-next-line no-restricted-syntax
61
+ get left() {
62
+ return this.x
63
+ }
64
+
60
65
  // eslint-disable-next-line no-restricted-syntax
61
66
  get midX() {
62
67
  return this.x + this.w / 2
@@ -67,6 +72,11 @@ export class Box {
67
72
  return this.x + this.w
68
73
  }
69
74
 
75
+ // eslint-disable-next-line no-restricted-syntax
76
+ get right() {
77
+ return this.x + this.w
78
+ }
79
+
70
80
  // eslint-disable-next-line no-restricted-syntax
71
81
  get minY() {
72
82
  return this.y
@@ -77,6 +87,11 @@ export class Box {
77
87
  this.y = n
78
88
  }
79
89
 
90
+ // eslint-disable-next-line no-restricted-syntax
91
+ get top() {
92
+ return this.y
93
+ }
94
+
80
95
  // eslint-disable-next-line no-restricted-syntax
81
96
  get midY() {
82
97
  return this.y + this.h / 2
@@ -87,6 +102,11 @@ export class Box {
87
102
  return this.y + this.h
88
103
  }
89
104
 
105
+ // eslint-disable-next-line no-restricted-syntax
106
+ get bottom() {
107
+ return this.y + this.h
108
+ }
109
+
90
110
  // eslint-disable-next-line no-restricted-syntax
91
111
  get width() {
92
112
  return this.w
@@ -157,12 +157,13 @@ export class Mat {
157
157
  return Mat.Compose(Mat.Translate(cx, cy!), rotationMatrix, Mat.Translate(-cx, -cy!))
158
158
  }
159
159
 
160
- static Scale(x: number, y: number): MatModel
161
- static Scale(x: number, y: number, cx: number, cy: number): MatModel
162
- static Scale(x: number, y: number, cx?: number, cy?: number): MatModel {
160
+ static Scale(x: number, y: number): Mat
161
+ static Scale(x: number, y: number, cx: number, cy: number): Mat
162
+ static Scale(x: number, y: number, cx?: number, cy?: number): Mat {
163
163
  const scaleMatrix = new Mat(x, 0, 0, y, 0, 0)
164
164
  if (cx === undefined) return scaleMatrix
165
- return Mat.Compose(Mat.Translate(cx, cy!), scaleMatrix, Mat.Translate(-cx, -cy!))
165
+
166
+ return Mat.Translate(cx, cy!).multiply(scaleMatrix).translate(-cx, -cy!)
166
167
  }
167
168
  static Multiply(m1: MatModel, m2: MatModel): MatModel {
168
169
  return {
@@ -319,6 +319,11 @@ export class Vec {
319
319
  return ((A.y - B.y) ** 2 + (A.x - B.x) ** 2) ** 0.5
320
320
  }
321
321
 
322
+ // Get the Manhattan distance between two points.
323
+ static ManhattanDist(A: VecLike, B: VecLike): number {
324
+ return Math.abs(A.x - B.x) + Math.abs(A.y - B.y)
325
+ }
326
+
322
327
  // Get whether a distance between two points is less than a number. This is faster to calulate than using `Vec.Dist(a, b) < n`.
323
328
  static DistMin(A: VecLike, B: VecLike, n: number): boolean {
324
329
  return (A.x - B.x) * (A.x - B.x) + (A.y - B.y) * (A.y - B.y) < n ** 2
@@ -465,10 +470,28 @@ export class Vec {
465
470
  return isNaN(A.x) || isNaN(A.y)
466
471
  }
467
472
 
473
+ /**
474
+ * Get the angle from position A to position B.
475
+ */
468
476
  static Angle(A: VecLike, B: VecLike): number {
469
477
  return Math.atan2(B.y - A.y, B.x - A.x)
470
478
  }
471
479
 
480
+ /**
481
+ * Get the angle between vector A and vector B. This will return the smallest angle between the
482
+ * two vectors, between -π and π. The sign indicates direction of angle.
483
+ */
484
+ static AngleBetween(A: VecLike, B: VecLike): number {
485
+ const p = A.x * B.x + A.y * B.y
486
+ const n = Math.sqrt(
487
+ (Math.pow(A.x, 2) + Math.pow(A.y, 2)) * (Math.pow(B.x, 2) + Math.pow(B.y, 2))
488
+ )
489
+ const sign = A.x * B.y - A.y * B.x < 0 ? -1 : 1
490
+ const angle = sign * Math.acos(p / n)
491
+
492
+ return angle
493
+ }
494
+
472
495
  /**
473
496
  * Linearly interpolate between two points.
474
497
  * @param A - The first point.
@@ -1,4 +1,4 @@
1
- import { Vec } from '../Vec'
1
+ import { Vec, VecLike } from '../Vec'
2
2
  import { intersectLineSegmentCircle } from '../intersect'
3
3
  import { getArcMeasure, getPointInArcT, getPointOnCircle } from '../utils'
4
4
  import { Geometry2d, Geometry2dOptions } from './Geometry2d'
@@ -44,14 +44,14 @@ export class Arc2d extends Geometry2d {
44
44
  this._center = center
45
45
  }
46
46
 
47
- nearestPoint(point: Vec): Vec {
47
+ nearestPoint(point: VecLike): Vec {
48
48
  const { _center, measure, radius, angleEnd, angleStart, start: A, end: B } = this
49
49
  const t = getPointInArcT(measure, angleStart, angleEnd, _center.angle(point))
50
50
  if (t <= 0) return A
51
51
  if (t >= 1) return B
52
52
 
53
53
  // Get the point (P) on the arc, then pick the nearest of A, B, and P
54
- const P = _center.clone().add(point.clone().sub(_center).uni().mul(radius))
54
+ const P = Vec.Sub(point, _center).uni().mul(radius).add(_center)
55
55
 
56
56
  let nearest: Vec | undefined
57
57
  let dist = Infinity
@@ -67,7 +67,7 @@ export class Arc2d extends Geometry2d {
67
67
  return nearest
68
68
  }
69
69
 
70
- hitTestLineSegment(A: Vec, B: Vec): boolean {
70
+ hitTestLineSegment(A: VecLike, B: VecLike): boolean {
71
71
  const { _center, radius, measure, angleStart, angleEnd } = this
72
72
  const intersection = intersectLineSegmentCircle(A, B, _center, radius)
73
73
  if (intersection === null) return false
@@ -95,6 +95,6 @@ export class Arc2d extends Geometry2d {
95
95
  }
96
96
 
97
97
  override getLength() {
98
- return this.measure * this.radius
98
+ return Math.abs(this.measure * this.radius)
99
99
  }
100
100
  }
@@ -1,5 +1,5 @@
1
1
  import { Box } from '../Box'
2
- import { Vec } from '../Vec'
2
+ import { Vec, VecLike } from '../Vec'
3
3
  import { intersectLineSegmentCircle } from '../intersect'
4
4
  import { PI2, getPointOnCircle } from '../utils'
5
5
  import { Geometry2d, Geometry2dOptions } from './Geometry2d'
@@ -43,13 +43,13 @@ export class Circle2d extends Geometry2d {
43
43
  return vertices
44
44
  }
45
45
 
46
- nearestPoint(point: Vec): Vec {
46
+ nearestPoint(point: VecLike): Vec {
47
47
  const { _center, radius } = this
48
48
  if (_center.equals(point)) return Vec.AddXY(_center, radius, 0)
49
- return _center.clone().add(point.clone().sub(_center).uni().mul(radius))
49
+ return Vec.Sub(point, _center).uni().mul(radius).add(_center)
50
50
  }
51
51
 
52
- hitTestLineSegment(A: Vec, B: Vec, distance = 0): boolean {
52
+ hitTestLineSegment(A: VecLike, B: VecLike, distance = 0): boolean {
53
53
  const { _center, radius } = this
54
54
  return intersectLineSegmentCircle(A, B, _center, radius + distance) !== null
55
55
  }
@@ -1,5 +1,5 @@
1
- import { Vec } from '../Vec'
2
- import { Geometry2dOptions } from './Geometry2d'
1
+ import { Vec, VecLike } from '../Vec'
2
+ import { Geometry2dFilters, Geometry2dOptions } from './Geometry2d'
3
3
  import { Polyline2d } from './Polyline2d'
4
4
 
5
5
  /** @public */
@@ -52,7 +52,7 @@ export class CubicBezier2d extends Polyline2d {
52
52
  return CubicBezier2d.GetAtT(this, 0.5)
53
53
  }
54
54
 
55
- nearestPoint(A: Vec): Vec {
55
+ nearestPoint(A: VecLike): Vec {
56
56
  let nearest: Vec | undefined
57
57
  let dist = Infinity
58
58
  let d: number
@@ -89,7 +89,7 @@ export class CubicBezier2d extends Polyline2d {
89
89
  )
90
90
  }
91
91
 
92
- override getLength(precision = 32) {
92
+ override getLength(filters?: Geometry2dFilters, precision = 32) {
93
93
  let n1: Vec,
94
94
  p1 = this.a,
95
95
  length = 0
@@ -1,4 +1,4 @@
1
- import { Vec } from '../Vec'
1
+ import { Vec, VecLike } from '../Vec'
2
2
  import { CubicBezier2d } from './CubicBezier2d'
3
3
  import { Geometry2d, Geometry2dOptions } from './Geometry2d'
4
4
 
@@ -58,7 +58,7 @@ export class CubicSpline2d extends Geometry2d {
58
58
  return vertices
59
59
  }
60
60
 
61
- nearestPoint(A: Vec): Vec {
61
+ nearestPoint(A: VecLike): Vec {
62
62
  let nearest: Vec | undefined
63
63
  let dist = Infinity
64
64
  let d: number
@@ -75,7 +75,7 @@ export class CubicSpline2d extends Geometry2d {
75
75
  return nearest
76
76
  }
77
77
 
78
- hitTestLineSegment(A: Vec, B: Vec): boolean {
78
+ hitTestLineSegment(A: VecLike, B: VecLike): boolean {
79
79
  return this.segments.some((segment) => segment.hitTestLineSegment(A, B))
80
80
  }
81
81
 
@@ -1,5 +1,5 @@
1
- import { Vec } from '../Vec'
2
1
  import { linesIntersect } from '../intersect'
2
+ import { Vec, VecLike } from '../Vec'
3
3
  import { Geometry2d } from './Geometry2d'
4
4
 
5
5
  /** @public */
@@ -34,7 +34,7 @@ export class Edge2d extends Geometry2d {
34
34
  return [this.start, this.end]
35
35
  }
36
36
 
37
- override nearestPoint(point: Vec): Vec {
37
+ override nearestPoint(point: VecLike): Vec {
38
38
  const { start, end, d, u, ul: l } = this
39
39
  if (d.len() === 0) return start // start and end are the same
40
40
  if (l === 0) return start // no length in the unit vector
@@ -48,7 +48,7 @@ export class Edge2d extends Geometry2d {
48
48
  return new Vec(cx, cy)
49
49
  }
50
50
 
51
- override hitTestLineSegment(A: Vec, B: Vec, distance = 0): boolean {
51
+ override hitTestLineSegment(A: VecLike, B: VecLike, distance = 0): boolean {
52
52
  return (
53
53
  linesIntersect(A, B, this.start, this.end) || this.distanceToLineSegment(A, B) <= distance
54
54
  )
@@ -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, PI2, perimeterOfEllipse } from '../utils'
4
4
  import { Edge2d } from './Edge2d'
5
5
  import { Geometry2d, Geometry2dOptions } from './Geometry2d'
@@ -73,7 +73,7 @@ export class Ellipse2d extends Geometry2d {
73
73
  return vertices
74
74
  }
75
75
 
76
- nearestPoint(A: Vec): Vec {
76
+ nearestPoint(A: VecLike): Vec {
77
77
  let nearest: Vec | undefined
78
78
  let dist = Infinity
79
79
  let d: number
@@ -90,7 +90,7 @@ export class Ellipse2d extends Geometry2d {
90
90
  return nearest
91
91
  }
92
92
 
93
- hitTestLineSegment(A: Vec, B: Vec): boolean {
93
+ hitTestLineSegment(A: VecLike, B: VecLike): boolean {
94
94
  return this.edges.some((edge) => edge.hitTestLineSegment(A, B))
95
95
  }
96
96
 
@@ -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 {