@tldraw/editor 3.13.0-canary.712bc0b8b5bc → 3.13.0-canary.7ec2a895a616
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.
- package/dist-cjs/index.d.ts +99 -99
- package/dist-cjs/index.js +7 -22
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/components/Shape.js +2 -1
- package/dist-cjs/lib/components/Shape.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js +10 -6
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +17 -11
- package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +112 -23
- package/dist-cjs/lib/editor/Editor.js.map +3 -3
- package/dist-cjs/lib/editor/managers/SnapManager/HandleSnaps.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TextManager.js +10 -0
- package/dist-cjs/lib/editor/managers/TextManager.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js +1 -1
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +0 -3
- package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js.map +2 -2
- package/dist-cjs/lib/hooks/useEditorComponents.js +1 -2
- package/dist-cjs/lib/hooks/useEditorComponents.js.map +2 -2
- package/dist-cjs/lib/primitives/Box.js +16 -0
- package/dist-cjs/lib/primitives/Box.js.map +2 -2
- package/dist-cjs/lib/primitives/Mat.js +1 -1
- package/dist-cjs/lib/primitives/Mat.js.map +2 -2
- package/dist-cjs/lib/primitives/Vec.js +20 -0
- package/dist-cjs/lib/primitives/Vec.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Arc2d.js +2 -2
- package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Circle2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/Circle2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/CubicSpline2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Edge2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/Edge2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Ellipse2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js +91 -20
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Group2d.js +55 -2
- package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Point2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Polyline2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Stadium2d.js.map +2 -2
- package/dist-cjs/lib/utils/areShapesContentEqual.js +25 -0
- package/dist-cjs/lib/utils/areShapesContentEqual.js.map +7 -0
- package/dist-cjs/lib/utils/debug-flags.js +5 -2
- package/dist-cjs/lib/utils/debug-flags.js.map +2 -2
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +99 -99
- package/dist-esm/index.mjs +9 -41
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/components/Shape.mjs +2 -1
- package/dist-esm/lib/components/Shape.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +10 -6
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +17 -11
- package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +112 -23
- package/dist-esm/lib/editor/Editor.mjs.map +3 -3
- package/dist-esm/lib/editor/managers/SnapManager/HandleSnaps.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TextManager.mjs +10 -0
- package/dist-esm/lib/editor/managers/TextManager.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +1 -1
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +0 -3
- package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs.map +2 -2
- package/dist-esm/lib/hooks/useEditorComponents.mjs +1 -4
- package/dist-esm/lib/hooks/useEditorComponents.mjs.map +2 -2
- package/dist-esm/lib/primitives/Box.mjs +16 -0
- package/dist-esm/lib/primitives/Box.mjs.map +2 -2
- package/dist-esm/lib/primitives/Mat.mjs +1 -1
- package/dist-esm/lib/primitives/Mat.mjs.map +2 -2
- package/dist-esm/lib/primitives/Vec.mjs +20 -0
- package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Arc2d.mjs +2 -2
- package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Circle2d.mjs +1 -1
- package/dist-esm/lib/primitives/geometry/Circle2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +1 -1
- package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/CubicSpline2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Edge2d.mjs +1 -1
- package/dist-esm/lib/primitives/geometry/Edge2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +92 -21
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Group2d.mjs +55 -2
- package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Point2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Polyline2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Stadium2d.mjs.map +2 -2
- package/dist-esm/lib/utils/areShapesContentEqual.mjs +5 -0
- package/dist-esm/lib/utils/areShapesContentEqual.mjs.map +7 -0
- package/dist-esm/lib/utils/debug-flags.mjs +5 -2
- package/dist-esm/lib/utils/debug-flags.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +36 -4
- package/package.json +7 -7
- package/src/index.ts +16 -31
- package/src/lib/components/Shape.tsx +2 -4
- package/src/lib/components/default-components/DefaultCanvas.tsx +11 -6
- package/src/lib/components/default-components/DefaultShapeIndicator.tsx +17 -8
- package/src/lib/editor/Editor.test.ts +1 -1
- package/src/lib/editor/Editor.ts +125 -23
- package/src/lib/editor/managers/SnapManager/HandleSnaps.ts +0 -1
- package/src/lib/editor/managers/TextManager.ts +12 -0
- package/src/lib/editor/shapes/ShapeUtil.ts +10 -2
- package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +0 -4
- package/src/lib/editor/shapes/shared/getPerfectDashProps.ts +9 -9
- package/src/lib/hooks/useEditorComponents.tsx +2 -5
- package/src/lib/primitives/Box.ts +20 -0
- package/src/lib/primitives/Mat.ts +5 -4
- package/src/lib/primitives/Vec.ts +23 -0
- package/src/lib/primitives/geometry/Arc2d.ts +5 -5
- package/src/lib/primitives/geometry/Circle2d.ts +4 -4
- package/src/lib/primitives/geometry/CubicBezier2d.ts +4 -4
- package/src/lib/primitives/geometry/CubicSpline2d.ts +3 -3
- package/src/lib/primitives/geometry/Edge2d.ts +3 -3
- package/src/lib/primitives/geometry/Ellipse2d.ts +3 -3
- package/src/lib/primitives/geometry/Geometry2d.test.ts +42 -0
- package/src/lib/primitives/geometry/Geometry2d.ts +123 -35
- package/src/lib/primitives/geometry/Group2d.ts +70 -7
- package/src/lib/primitives/geometry/Point2d.ts +2 -2
- package/src/lib/primitives/geometry/Polyline2d.ts +3 -3
- package/src/lib/primitives/geometry/Stadium2d.ts +3 -3
- package/src/lib/test/currentToolIdMask.test.ts +1 -1
- package/src/lib/test/user.test.ts +1 -1
- package/src/lib/utils/areShapesContentEqual.ts +4 -0
- package/src/lib/utils/debug-flags.ts +7 -2
- package/src/lib/utils/sync/LocalIndexedDb.test.ts +1 -1
- package/src/lib/utils/sync/TLLocalSyncClient.test.ts +1 -1
- package/src/version.ts +3 -3
|
@@ -4,15 +4,15 @@ import { TLDefaultDashStyle } from '@tldraw/tlschema'
|
|
|
4
4
|
export function getPerfectDashProps(
|
|
5
5
|
totalLength: number,
|
|
6
6
|
strokeWidth: number,
|
|
7
|
-
opts
|
|
8
|
-
style
|
|
9
|
-
snap
|
|
10
|
-
end
|
|
11
|
-
start
|
|
12
|
-
lengthRatio
|
|
13
|
-
closed
|
|
14
|
-
forceSolid
|
|
15
|
-
}
|
|
7
|
+
opts: {
|
|
8
|
+
style?: TLDefaultDashStyle
|
|
9
|
+
snap?: number
|
|
10
|
+
end?: 'skip' | 'outset' | 'none'
|
|
11
|
+
start?: 'skip' | 'outset' | 'none'
|
|
12
|
+
lengthRatio?: number
|
|
13
|
+
closed?: boolean
|
|
14
|
+
forceSolid?: boolean
|
|
15
|
+
} = {}
|
|
16
16
|
): {
|
|
17
17
|
strokeDasharray: string
|
|
18
18
|
strokeDashoffset: string
|
|
@@ -19,10 +19,7 @@ import { DefaultHandle, TLHandleProps } from '../components/default-components/D
|
|
|
19
19
|
import { DefaultHandles, TLHandlesProps } from '../components/default-components/DefaultHandles'
|
|
20
20
|
import { DefaultLoadingScreen } from '../components/default-components/DefaultLoadingScreen'
|
|
21
21
|
import { DefaultScribble, TLScribbleProps } from '../components/default-components/DefaultScribble'
|
|
22
|
-
import {
|
|
23
|
-
DefaultSelectionBackground,
|
|
24
|
-
TLSelectionBackgroundProps,
|
|
25
|
-
} from '../components/default-components/DefaultSelectionBackground'
|
|
22
|
+
import { TLSelectionBackgroundProps } from '../components/default-components/DefaultSelectionBackground'
|
|
26
23
|
import {
|
|
27
24
|
DefaultSelectionForeground,
|
|
28
25
|
TLSelectionForegroundProps,
|
|
@@ -113,7 +110,7 @@ export function EditorComponentsProvider({
|
|
|
113
110
|
OnTheCanvas: null,
|
|
114
111
|
Overlays: null,
|
|
115
112
|
Scribble: DefaultScribble,
|
|
116
|
-
SelectionBackground:
|
|
113
|
+
SelectionBackground: null,
|
|
117
114
|
SelectionForeground: DefaultSelectionForeground,
|
|
118
115
|
ShapeIndicator: DefaultShapeIndicator,
|
|
119
116
|
ShapeIndicators: DefaultShapeIndicators,
|
|
@@ -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):
|
|
161
|
-
static Scale(x: number, y: number, cx: number, cy: number):
|
|
162
|
-
static Scale(x: number, y: number, cx?: number, cy?: number):
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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:
|
|
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:
|
|
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
|
|
49
|
+
return Vec.Sub(point, _center).uni().mul(radius).add(_center)
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
hitTestLineSegment(A:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
84
|
+
abstract nearestPoint(point: VecLike, _filters?: Geometry2dFilters): Vec
|
|
85
85
|
|
|
86
|
-
hitTestPoint(point:
|
|
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:
|
|
95
|
+
distanceToPoint(point: VecLike, hitInside = false, filters?: Geometry2dFilters) {
|
|
96
96
|
return (
|
|
97
|
-
|
|
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:
|
|
105
|
-
if (
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
275
|
-
length += Vec.
|
|
276
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
|
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
|
|
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 {
|