@tldraw/editor 3.15.0-next.f1dfcef63951 → 3.16.0-next.c30b1b5e551a
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 +159 -44
- package/dist-cjs/index.js +20 -16
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/components/SVGContainer.js +1 -1
- package/dist-cjs/lib/components/SVGContainer.js.map +2 -2
- package/dist-cjs/lib/components/Shape.js +4 -26
- package/dist-cjs/lib/components/Shape.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultBrush.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultBrush.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCursor.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultCursor.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultGrid.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultGrid.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultHandles.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultHandles.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultShapeWrapper.js +53 -0
- package/dist-cjs/lib/components/default-components/DefaultShapeWrapper.js.map +7 -0
- package/dist-cjs/lib/components/default-components/DefaultSnapIndictor.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultSnapIndictor.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultSpinner.js +27 -15
- package/dist-cjs/lib/components/default-components/DefaultSpinner.js.map +3 -3
- package/dist-cjs/lib/config/TLUserPreferences.js +7 -1
- package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +88 -43
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +96 -101
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +7 -2
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/tools/StateNode.js +20 -1
- package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
- package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
- package/dist-cjs/lib/hooks/useEditorComponents.js +2 -0
- package/dist-cjs/lib/hooks/useEditorComponents.js.map +2 -2
- package/dist-cjs/lib/license/Watermark.js +2 -2
- package/dist-cjs/lib/license/Watermark.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Arc2d.js +1 -1
- 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 +3 -1
- package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Ellipse2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/Ellipse2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/geometry-constants.js +2 -2
- package/dist-cjs/lib/primitives/geometry/geometry-constants.js.map +2 -2
- package/dist-cjs/lib/primitives/intersect.js +4 -4
- package/dist-cjs/lib/primitives/intersect.js.map +2 -2
- package/dist-cjs/lib/primitives/utils.js +4 -0
- package/dist-cjs/lib/primitives/utils.js.map +2 -2
- package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +0 -1
- package/dist-cjs/lib/utils/sync/TLLocalSyncClient.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 +159 -44
- package/dist-esm/index.mjs +47 -41
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/components/SVGContainer.mjs +1 -1
- package/dist-esm/lib/components/SVGContainer.mjs.map +2 -2
- package/dist-esm/lib/components/Shape.mjs +4 -26
- package/dist-esm/lib/components/Shape.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultBrush.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultBrush.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCursor.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultCursor.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultGrid.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultGrid.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultHandles.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultHandles.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultShapeWrapper.mjs +23 -0
- package/dist-esm/lib/components/default-components/DefaultShapeWrapper.mjs.map +7 -0
- package/dist-esm/lib/components/default-components/DefaultSnapIndictor.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultSnapIndictor.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultSpinner.mjs +17 -15
- package/dist-esm/lib/components/default-components/DefaultSpinner.mjs.map +2 -2
- package/dist-esm/lib/config/TLUserPreferences.mjs +7 -1
- package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +88 -43
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +96 -101
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +7 -2
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/tools/StateNode.mjs +20 -1
- package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
- package/dist-esm/lib/hooks/useEditorComponents.mjs +4 -0
- package/dist-esm/lib/hooks/useEditorComponents.mjs.map +2 -2
- package/dist-esm/lib/license/Watermark.mjs +2 -2
- package/dist-esm/lib/license/Watermark.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 +2 -2
- package/dist-esm/lib/primitives/geometry/Circle2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +3 -1
- package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs +2 -2
- package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/geometry-constants.mjs +2 -2
- package/dist-esm/lib/primitives/geometry/geometry-constants.mjs.map +2 -2
- package/dist-esm/lib/primitives/intersect.mjs +5 -5
- package/dist-esm/lib/primitives/intersect.mjs.map +2 -2
- package/dist-esm/lib/primitives/utils.mjs +4 -0
- package/dist-esm/lib/primitives/utils.mjs.map +2 -2
- package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +0 -1
- package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +21 -27
- package/package.json +9 -8
- package/src/index.ts +68 -62
- package/src/lib/components/SVGContainer.tsx +1 -1
- package/src/lib/components/Shape.tsx +6 -21
- package/src/lib/components/default-components/DefaultBrush.tsx +1 -1
- package/src/lib/components/default-components/DefaultCanvas.tsx +1 -1
- package/src/lib/components/default-components/DefaultCollaboratorHint.tsx +1 -1
- package/src/lib/components/default-components/DefaultCursor.tsx +1 -1
- package/src/lib/components/default-components/DefaultGrid.tsx +1 -1
- package/src/lib/components/default-components/DefaultHandles.tsx +5 -1
- package/src/lib/components/default-components/DefaultShapeIndicator.tsx +1 -1
- package/src/lib/components/default-components/DefaultShapeWrapper.tsx +35 -0
- package/src/lib/components/default-components/DefaultSnapIndictor.tsx +1 -1
- package/src/lib/components/default-components/DefaultSpinner.tsx +12 -12
- package/src/lib/config/TLUserPreferences.ts +7 -0
- package/src/lib/editor/Editor.test.ts +407 -0
- package/src/lib/editor/Editor.ts +106 -44
- package/src/lib/editor/managers/TextManager/TextManager.ts +108 -128
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +21 -0
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +8 -0
- package/src/lib/editor/shapes/ShapeUtil.ts +57 -0
- package/src/lib/editor/tools/StateNode.test.ts +285 -0
- package/src/lib/editor/tools/StateNode.ts +27 -1
- package/src/lib/editor/types/misc-types.ts +19 -0
- package/src/lib/hooks/useEditorComponents.tsx +8 -2
- package/src/lib/license/LicenseManager.test.ts +1 -1
- package/src/lib/license/Watermark.tsx +2 -2
- package/src/lib/primitives/geometry/Arc2d.ts +2 -2
- package/src/lib/primitives/geometry/Circle2d.ts +2 -2
- package/src/lib/primitives/geometry/CubicBezier2d.ts +4 -1
- package/src/lib/primitives/geometry/Ellipse2d.ts +2 -2
- package/src/lib/primitives/geometry/geometry-constants.ts +2 -1
- package/src/lib/primitives/intersect.test.ts +946 -0
- package/src/lib/primitives/intersect.ts +12 -5
- package/src/lib/primitives/utils.ts +11 -0
- package/src/lib/utils/sync/TLLocalSyncClient.ts +0 -1
- package/src/version.ts +3 -3
- package/src/lib/test/currentToolIdMask.test.ts +0 -49
|
@@ -0,0 +1,946 @@
|
|
|
1
|
+
import {
|
|
2
|
+
intersectCircleCircle,
|
|
3
|
+
intersectCirclePolygon,
|
|
4
|
+
intersectCirclePolyline,
|
|
5
|
+
intersectLineSegmentCircle,
|
|
6
|
+
intersectLineSegmentLineSegment,
|
|
7
|
+
intersectLineSegmentPolygon,
|
|
8
|
+
intersectLineSegmentPolyline,
|
|
9
|
+
intersectPolygonPolygon,
|
|
10
|
+
} from './intersect'
|
|
11
|
+
import { Vec, VecLike } from './Vec'
|
|
12
|
+
|
|
13
|
+
describe('intersectLineSegmentLineSegment', () => {
|
|
14
|
+
describe('intersecting segments', () => {
|
|
15
|
+
it('should find intersection when segments cross', () => {
|
|
16
|
+
const a1 = new Vec(0, 0)
|
|
17
|
+
const a2 = new Vec(10, 10)
|
|
18
|
+
const b1 = new Vec(0, 10)
|
|
19
|
+
const b2 = new Vec(10, 0)
|
|
20
|
+
|
|
21
|
+
const result = intersectLineSegmentLineSegment(a1, a2, b1, b2)
|
|
22
|
+
|
|
23
|
+
expect(result).not.toBeNull()
|
|
24
|
+
expect(result!.x).toBeCloseTo(5, 5)
|
|
25
|
+
expect(result!.y).toBeCloseTo(5, 5)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should find intersection when segments cross at an angle', () => {
|
|
29
|
+
const a1 = new Vec(0, 0)
|
|
30
|
+
const a2 = new Vec(8, 6)
|
|
31
|
+
const b1 = new Vec(0, 6)
|
|
32
|
+
const b2 = new Vec(8, 0)
|
|
33
|
+
|
|
34
|
+
const result = intersectLineSegmentLineSegment(a1, a2, b1, b2)
|
|
35
|
+
|
|
36
|
+
expect(result).not.toBeNull()
|
|
37
|
+
expect(result!.x).toBeCloseTo(4, 5)
|
|
38
|
+
expect(result!.y).toBeCloseTo(3, 5)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should find intersection when one segment is vertical', () => {
|
|
42
|
+
const a1 = new Vec(5, 0)
|
|
43
|
+
const a2 = new Vec(5, 10)
|
|
44
|
+
const b1 = new Vec(0, 5)
|
|
45
|
+
const b2 = new Vec(10, 5)
|
|
46
|
+
|
|
47
|
+
const result = intersectLineSegmentLineSegment(a1, a2, b1, b2)
|
|
48
|
+
|
|
49
|
+
expect(result).not.toBeNull()
|
|
50
|
+
expect(result!.x).toBeCloseTo(5, 5)
|
|
51
|
+
expect(result!.y).toBeCloseTo(5, 5)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should find intersection when one segment is horizontal', () => {
|
|
55
|
+
const a1 = new Vec(0, 5)
|
|
56
|
+
const a2 = new Vec(10, 5)
|
|
57
|
+
const b1 = new Vec(5, 0)
|
|
58
|
+
const b2 = new Vec(5, 10)
|
|
59
|
+
|
|
60
|
+
const result = intersectLineSegmentLineSegment(a1, a2, b1, b2)
|
|
61
|
+
|
|
62
|
+
expect(result).not.toBeNull()
|
|
63
|
+
expect(result!.x).toBeCloseTo(5, 5)
|
|
64
|
+
expect(result!.y).toBeCloseTo(5, 5)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('non-intersecting segments', () => {
|
|
69
|
+
it('should return null when segments are parallel', () => {
|
|
70
|
+
const a1 = new Vec(0, 0)
|
|
71
|
+
const a2 = new Vec(10, 0)
|
|
72
|
+
const b1 = new Vec(0, 5)
|
|
73
|
+
const b2 = new Vec(10, 5)
|
|
74
|
+
|
|
75
|
+
const result = intersectLineSegmentLineSegment(a1, a2, b1, b2)
|
|
76
|
+
|
|
77
|
+
expect(result).toBeNull()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should return null when segments are parallel and vertical', () => {
|
|
81
|
+
const a1 = new Vec(0, 0)
|
|
82
|
+
const a2 = new Vec(0, 10)
|
|
83
|
+
const b1 = new Vec(5, 0)
|
|
84
|
+
const b2 = new Vec(5, 10)
|
|
85
|
+
|
|
86
|
+
const result = intersectLineSegmentLineSegment(a1, a2, b1, b2)
|
|
87
|
+
|
|
88
|
+
expect(result).toBeNull()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('should return null when segments do not intersect', () => {
|
|
92
|
+
const a1 = new Vec(0, 0)
|
|
93
|
+
const a2 = new Vec(5, 5)
|
|
94
|
+
const b1 = new Vec(10, 0)
|
|
95
|
+
const b2 = new Vec(15, 5)
|
|
96
|
+
|
|
97
|
+
const result = intersectLineSegmentLineSegment(a1, a2, b1, b2)
|
|
98
|
+
|
|
99
|
+
expect(result).toBeNull()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('should return null when segments are collinear but do not overlap', () => {
|
|
103
|
+
const a1 = new Vec(0, 0)
|
|
104
|
+
const a2 = new Vec(5, 0)
|
|
105
|
+
const b1 = new Vec(10, 0)
|
|
106
|
+
const b2 = new Vec(15, 0)
|
|
107
|
+
|
|
108
|
+
const result = intersectLineSegmentLineSegment(a1, a2, b1, b2)
|
|
109
|
+
|
|
110
|
+
expect(result).toBeNull()
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe('coincident segments', () => {
|
|
115
|
+
it('should return null when segments are identical', () => {
|
|
116
|
+
const a1 = new Vec(0, 0)
|
|
117
|
+
const a2 = new Vec(10, 0)
|
|
118
|
+
const b1 = new Vec(0, 0)
|
|
119
|
+
const b2 = new Vec(10, 0)
|
|
120
|
+
|
|
121
|
+
const result = intersectLineSegmentLineSegment(a1, a2, b1, b2)
|
|
122
|
+
|
|
123
|
+
expect(result).toBeNull()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should return null when segments overlap', () => {
|
|
127
|
+
const a1 = new Vec(0, 0)
|
|
128
|
+
const a2 = new Vec(10, 0)
|
|
129
|
+
const b1 = new Vec(5, 0)
|
|
130
|
+
const b2 = new Vec(15, 0)
|
|
131
|
+
|
|
132
|
+
const result = intersectLineSegmentLineSegment(a1, a2, b1, b2)
|
|
133
|
+
|
|
134
|
+
expect(result).toBeNull()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('should return null when one segment is contained within another', () => {
|
|
138
|
+
const a1 = new Vec(0, 0)
|
|
139
|
+
const a2 = new Vec(10, 0)
|
|
140
|
+
const b1 = new Vec(3, 0)
|
|
141
|
+
const b2 = new Vec(7, 0)
|
|
142
|
+
|
|
143
|
+
const result = intersectLineSegmentLineSegment(a1, a2, b1, b2)
|
|
144
|
+
|
|
145
|
+
expect(result).toBeNull()
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
describe('touching endpoints', () => {
|
|
150
|
+
it('should return null when segments touch at endpoints (coincident case)', () => {
|
|
151
|
+
const a1 = new Vec(0, 0)
|
|
152
|
+
const a2 = new Vec(5, 5)
|
|
153
|
+
const b1 = new Vec(5, 5)
|
|
154
|
+
const b2 = new Vec(10, 0)
|
|
155
|
+
|
|
156
|
+
const result = intersectLineSegmentLineSegment(a1, a2, b1, b2)
|
|
157
|
+
|
|
158
|
+
expect(result).toBeNull() // coincident case
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('should return null when segments touch at one endpoint (coincident case)', () => {
|
|
162
|
+
const a1 = new Vec(0, 0)
|
|
163
|
+
const a2 = new Vec(5, 5)
|
|
164
|
+
const b1 = new Vec(5, 5)
|
|
165
|
+
const b2 = new Vec(10, 10)
|
|
166
|
+
|
|
167
|
+
const result = intersectLineSegmentLineSegment(a1, a2, b1, b2)
|
|
168
|
+
|
|
169
|
+
expect(result).toBeNull() // coincident case
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('should find intersection when segments cross near endpoints', () => {
|
|
173
|
+
const a1 = new Vec(0, 0)
|
|
174
|
+
const a2 = new Vec(5, 5)
|
|
175
|
+
const b1 = new Vec(4.9, 5.1)
|
|
176
|
+
const b2 = new Vec(5.1, 4.9)
|
|
177
|
+
|
|
178
|
+
const result = intersectLineSegmentLineSegment(a1, a2, b1, b2)
|
|
179
|
+
|
|
180
|
+
expect(result).not.toBeNull()
|
|
181
|
+
expect(result!.x).toBeCloseTo(5, 1)
|
|
182
|
+
expect(result!.y).toBeCloseTo(5, 1)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('should find intersection when segments cross at endpoints (floating point error case)', () => {
|
|
186
|
+
const result = intersectLineSegmentLineSegment(
|
|
187
|
+
{ x: 100, y: 100 },
|
|
188
|
+
{ x: 20, y: 20 },
|
|
189
|
+
{ x: 36.141160159025375, y: 31.811740238538057 },
|
|
190
|
+
{ x: 34.14213562373095, y: 34.14213562373095 }
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
expect(result).not.toBeNull()
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
describe('edge cases', () => {
|
|
198
|
+
it('should handle very small segments', () => {
|
|
199
|
+
const a1 = new Vec(0, 0)
|
|
200
|
+
const a2 = new Vec(0.0001, 0.0001)
|
|
201
|
+
const b1 = new Vec(0, 0.0001)
|
|
202
|
+
const b2 = new Vec(0.0001, 0)
|
|
203
|
+
|
|
204
|
+
const result = intersectLineSegmentLineSegment(a1, a2, b1, b2)
|
|
205
|
+
|
|
206
|
+
expect(result).not.toBeNull()
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('should handle segments with very large coordinates', () => {
|
|
210
|
+
const a1 = new Vec(1e10, 1e10)
|
|
211
|
+
const a2 = new Vec(1e11, 1e11)
|
|
212
|
+
const b1 = new Vec(1e10, 1e11)
|
|
213
|
+
const b2 = new Vec(1e11, 1e10)
|
|
214
|
+
|
|
215
|
+
const result = intersectLineSegmentLineSegment(a1, a2, b1, b2)
|
|
216
|
+
|
|
217
|
+
expect(result).not.toBeNull()
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('should return null for zero-length segments (parallel case)', () => {
|
|
221
|
+
const a1 = new Vec(5, 5)
|
|
222
|
+
const a2 = new Vec(5, 5)
|
|
223
|
+
const b1 = new Vec(0, 0)
|
|
224
|
+
const b2 = new Vec(10, 10)
|
|
225
|
+
|
|
226
|
+
const result = intersectLineSegmentLineSegment(a1, a2, b1, b2)
|
|
227
|
+
|
|
228
|
+
expect(result).toBeNull() // parallel case (zero-length segment)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('should handle very short segments that still intersect', () => {
|
|
232
|
+
const a1 = new Vec(5.05, 4.95)
|
|
233
|
+
const a2 = new Vec(4.95, 5.05)
|
|
234
|
+
const b1 = new Vec(0, 0)
|
|
235
|
+
const b2 = new Vec(10, 10)
|
|
236
|
+
|
|
237
|
+
const result = intersectLineSegmentLineSegment(a1, a2, b1, b2)
|
|
238
|
+
|
|
239
|
+
expect(result).not.toBeNull()
|
|
240
|
+
expect(result!.x).toBeCloseTo(5, 1)
|
|
241
|
+
expect(result!.y).toBeCloseTo(5, 1)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('should handle both segments being zero-length at same point', () => {
|
|
245
|
+
const a1 = new Vec(5, 5)
|
|
246
|
+
const a2 = new Vec(5, 5)
|
|
247
|
+
const b1 = new Vec(5, 5)
|
|
248
|
+
const b2 = new Vec(5, 5)
|
|
249
|
+
|
|
250
|
+
const result = intersectLineSegmentLineSegment(a1, a2, b1, b2)
|
|
251
|
+
|
|
252
|
+
expect(result).toBeNull() // coincident case
|
|
253
|
+
})
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
describe('precision and floating point', () => {
|
|
257
|
+
it('should handle floating point precision issues', () => {
|
|
258
|
+
const a1 = new Vec(0.1, 0.1)
|
|
259
|
+
const a2 = new Vec(0.2, 0.2)
|
|
260
|
+
const b1 = new Vec(0.1, 0.2)
|
|
261
|
+
const b2 = new Vec(0.2, 0.1)
|
|
262
|
+
|
|
263
|
+
const result = intersectLineSegmentLineSegment(a1, a2, b1, b2)
|
|
264
|
+
|
|
265
|
+
expect(result).not.toBeNull()
|
|
266
|
+
expect(result!.x).toBeCloseTo(0.15, 5)
|
|
267
|
+
expect(result!.y).toBeCloseTo(0.15, 5)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('should handle segments that are very close but not intersecting', () => {
|
|
271
|
+
const a1 = new Vec(0, 0)
|
|
272
|
+
const a2 = new Vec(1, 0)
|
|
273
|
+
const b1 = new Vec(0, 0.000001)
|
|
274
|
+
const b2 = new Vec(1, 0.000001)
|
|
275
|
+
|
|
276
|
+
const result = intersectLineSegmentLineSegment(a1, a2, b1, b2)
|
|
277
|
+
|
|
278
|
+
expect(result).toBeNull() // parallel
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
describe('VecLike interface compatibility', () => {
|
|
283
|
+
it('should work with VecModel objects', () => {
|
|
284
|
+
const a1 = { x: 0, y: 0 }
|
|
285
|
+
const a2 = { x: 10, y: 10 }
|
|
286
|
+
const b1 = { x: 0, y: 10 }
|
|
287
|
+
const b2 = { x: 10, y: 0 }
|
|
288
|
+
|
|
289
|
+
const result = intersectLineSegmentLineSegment(a1, a2, b1, b2)
|
|
290
|
+
|
|
291
|
+
expect(result).not.toBeNull()
|
|
292
|
+
expect(result!.x).toBeCloseTo(5, 5)
|
|
293
|
+
expect(result!.y).toBeCloseTo(5, 5)
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('should work with mixed Vec and VecModel objects', () => {
|
|
297
|
+
const a1 = new Vec(0, 0)
|
|
298
|
+
const a2 = { x: 10, y: 10 }
|
|
299
|
+
const b1 = new Vec(0, 10)
|
|
300
|
+
const b2 = { x: 10, y: 0 }
|
|
301
|
+
|
|
302
|
+
const result = intersectLineSegmentLineSegment(a1, a2, b1, b2)
|
|
303
|
+
|
|
304
|
+
expect(result).not.toBeNull()
|
|
305
|
+
expect(result!.x).toBeCloseTo(5, 5)
|
|
306
|
+
expect(result!.y).toBeCloseTo(5, 5)
|
|
307
|
+
})
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
describe('intersectLineSegmentCircle', () => {
|
|
312
|
+
it('should return null when segment is completely outside the circle', () => {
|
|
313
|
+
const a1 = new Vec(0, 0)
|
|
314
|
+
const a2 = new Vec(1, 0)
|
|
315
|
+
const c = new Vec(5, 0)
|
|
316
|
+
const r = 1
|
|
317
|
+
const result = intersectLineSegmentCircle(a1, a2, c, r)
|
|
318
|
+
expect(result).toBeNull()
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
it('should return null when segment is tangent to the circle', () => {
|
|
322
|
+
const a1 = new Vec(0, 1)
|
|
323
|
+
const a2 = new Vec(2, 1)
|
|
324
|
+
const c = new Vec(1, 0)
|
|
325
|
+
const r = 1
|
|
326
|
+
const result = intersectLineSegmentCircle(a1, a2, c, r)
|
|
327
|
+
expect(result).toBeNull() // tangent returns null per implementation
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('should return two points when segment passes through the circle', () => {
|
|
331
|
+
const a1 = new Vec(-2, 0)
|
|
332
|
+
const a2 = new Vec(2, 0)
|
|
333
|
+
const c = new Vec(0, 0)
|
|
334
|
+
const r = 1
|
|
335
|
+
const result = intersectLineSegmentCircle(a1, a2, c, r)
|
|
336
|
+
expect(result).not.toBeNull()
|
|
337
|
+
expect(result!.length).toBe(2)
|
|
338
|
+
const sorted = result!.slice().sort((a, b) => a.x - b.x)
|
|
339
|
+
expect(sorted[0].x).toBeCloseTo(-1, 5)
|
|
340
|
+
expect(sorted[1].x).toBeCloseTo(1, 5)
|
|
341
|
+
sorted.forEach((pt) => expect(Math.abs(pt.y)).toBeCloseTo(0, 5))
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it('should return one point when segment starts inside and exits the circle', () => {
|
|
345
|
+
const a1 = new Vec(0, 0)
|
|
346
|
+
const a2 = new Vec(2, 0)
|
|
347
|
+
const c = new Vec(0, 0)
|
|
348
|
+
const r = 1
|
|
349
|
+
const result = intersectLineSegmentCircle(a1, a2, c, r)
|
|
350
|
+
expect(result).not.toBeNull()
|
|
351
|
+
expect(result!.length).toBe(1)
|
|
352
|
+
expect(result![0].x).toBeCloseTo(1, 5)
|
|
353
|
+
expect(result![0].y).toBeCloseTo(0, 5)
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
it('should return one point when segment ends inside and enters the circle', () => {
|
|
357
|
+
const a1 = new Vec(2, 0)
|
|
358
|
+
const a2 = new Vec(0, 0)
|
|
359
|
+
const c = new Vec(0, 0)
|
|
360
|
+
const r = 1
|
|
361
|
+
const result = intersectLineSegmentCircle(a1, a2, c, r)
|
|
362
|
+
expect(result).not.toBeNull()
|
|
363
|
+
expect(result!.length).toBe(1)
|
|
364
|
+
expect(result![0].x).toBeCloseTo(1, 5)
|
|
365
|
+
expect(result![0].y).toBeCloseTo(0, 5)
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
it('should return null when segment is entirely inside the circle', () => {
|
|
369
|
+
const a1 = new Vec(0, 0)
|
|
370
|
+
const a2 = new Vec(0.5, 0)
|
|
371
|
+
const c = new Vec(0, 0)
|
|
372
|
+
const r = 2
|
|
373
|
+
const result = intersectLineSegmentCircle(a1, a2, c, r)
|
|
374
|
+
expect(result).toBeNull()
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
it('should return one point when segment endpoint is exactly on the circle', () => {
|
|
378
|
+
const a1 = new Vec(-1, 0)
|
|
379
|
+
const a2 = new Vec(0, 0)
|
|
380
|
+
const c = new Vec(0, 0)
|
|
381
|
+
const r = 1
|
|
382
|
+
const result = intersectLineSegmentCircle(a1, a2, c, r)
|
|
383
|
+
expect(result).not.toBeNull()
|
|
384
|
+
expect(result!.length).toBe(1)
|
|
385
|
+
expect(result![0].x).toBeCloseTo(-1, 5)
|
|
386
|
+
expect(result![0].y).toBeCloseTo(0, 5)
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
it('should return null for zero-length segment outside the circle', () => {
|
|
390
|
+
const a1 = new Vec(2, 2)
|
|
391
|
+
const a2 = new Vec(2, 2)
|
|
392
|
+
const c = new Vec(0, 0)
|
|
393
|
+
const r = 1
|
|
394
|
+
const result = intersectLineSegmentCircle(a1, a2, c, r)
|
|
395
|
+
expect(result).toBeNull()
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
it('should return null for zero-length segment inside the circle', () => {
|
|
399
|
+
const a1 = new Vec(0.5, 0)
|
|
400
|
+
const a2 = new Vec(0.5, 0)
|
|
401
|
+
const c = new Vec(0, 0)
|
|
402
|
+
const r = 1
|
|
403
|
+
const result = intersectLineSegmentCircle(a1, a2, c, r)
|
|
404
|
+
expect(result).toBeNull()
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
it('should return one point for zero-length segment on the circle', () => {
|
|
408
|
+
const a1 = new Vec(1, 0)
|
|
409
|
+
const a2 = new Vec(1, 0)
|
|
410
|
+
const c = new Vec(0, 0)
|
|
411
|
+
const r = 1
|
|
412
|
+
const result = intersectLineSegmentCircle(a1, a2, c, r)
|
|
413
|
+
expect(result).toBeNull() // tangent returns null per implementation
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
it('should handle floating point precision', () => {
|
|
417
|
+
const a1 = new Vec(-1e-8, 1)
|
|
418
|
+
const a2 = new Vec(1 + 1e-8, 1)
|
|
419
|
+
const c = new Vec(0.5, 1)
|
|
420
|
+
const r = 0.5
|
|
421
|
+
const result = intersectLineSegmentCircle(a1, a2, c, r)
|
|
422
|
+
expect(result).not.toBeNull()
|
|
423
|
+
expect(result!.length).toBe(2)
|
|
424
|
+
const sorted = result!.slice().sort((a, b) => a.x - b.x)
|
|
425
|
+
expect(sorted[0].x).toBeCloseTo(0, 5)
|
|
426
|
+
expect(sorted[1].x).toBeCloseTo(1, 5)
|
|
427
|
+
})
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
describe('intersectLineSegmentPolyline', () => {
|
|
431
|
+
it('should return null when no intersection with polyline', () => {
|
|
432
|
+
const a1 = new Vec(0, 0)
|
|
433
|
+
const a2 = new Vec(1, 0)
|
|
434
|
+
const points = [new Vec(5, 5), new Vec(6, 5), new Vec(6, 6), new Vec(5, 6)]
|
|
435
|
+
const result = intersectLineSegmentPolyline(a1, a2, points)
|
|
436
|
+
expect(result).toBeNull()
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
it('should return single intersection point', () => {
|
|
440
|
+
const a1 = new Vec(0, 0)
|
|
441
|
+
const a2 = new Vec(10, 10)
|
|
442
|
+
const points = [new Vec(0, 10), new Vec(10, 0)]
|
|
443
|
+
const result = intersectLineSegmentPolyline(a1, a2, points)
|
|
444
|
+
expect(result).not.toBeNull()
|
|
445
|
+
expect(result!.length).toBe(1)
|
|
446
|
+
expect(result![0].x).toBeCloseTo(5, 5)
|
|
447
|
+
expect(result![0].y).toBeCloseTo(5, 5)
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
it('should return multiple intersection points', () => {
|
|
451
|
+
const a1 = new Vec(0, 5)
|
|
452
|
+
const a2 = new Vec(10, 5)
|
|
453
|
+
const points = [new Vec(2, 0), new Vec(2, 10), new Vec(8, 10), new Vec(8, 0)]
|
|
454
|
+
const result = intersectLineSegmentPolyline(a1, a2, points)
|
|
455
|
+
expect(result).not.toBeNull()
|
|
456
|
+
expect(result!.length).toBe(2)
|
|
457
|
+
// Should intersect at x=2 and x=8
|
|
458
|
+
const sorted = result!.slice().sort((a, b) => a.x - b.x)
|
|
459
|
+
expect(sorted[0].x).toBeCloseTo(2, 5)
|
|
460
|
+
expect(sorted[1].x).toBeCloseTo(8, 5)
|
|
461
|
+
sorted.forEach((pt) => expect(pt.y).toBeCloseTo(5, 5))
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
it('should return null for empty polyline', () => {
|
|
465
|
+
const a1 = new Vec(0, 0)
|
|
466
|
+
const a2 = new Vec(1, 1)
|
|
467
|
+
const points: VecLike[] = []
|
|
468
|
+
const result = intersectLineSegmentPolyline(a1, a2, points)
|
|
469
|
+
expect(result).toBeNull()
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
it('should return null for single point polyline', () => {
|
|
473
|
+
const a1 = new Vec(0, 0)
|
|
474
|
+
const a2 = new Vec(1, 1)
|
|
475
|
+
const points = [new Vec(5, 5)]
|
|
476
|
+
const result = intersectLineSegmentPolyline(a1, a2, points)
|
|
477
|
+
expect(result).toBeNull()
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it('should handle polyline with duplicate points', () => {
|
|
481
|
+
const a1 = new Vec(0, 0)
|
|
482
|
+
const a2 = new Vec(10, 10)
|
|
483
|
+
const points = [new Vec(0, 10), new Vec(5, 5), new Vec(5, 5), new Vec(10, 0)]
|
|
484
|
+
const result = intersectLineSegmentPolyline(a1, a2, points)
|
|
485
|
+
expect(result).not.toBeNull()
|
|
486
|
+
expect(result!.length).toBe(1)
|
|
487
|
+
expect(result![0].x).toBeCloseTo(5, 5)
|
|
488
|
+
expect(result![0].y).toBeCloseTo(5, 5)
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
it('should handle polyline with zero-length segments', () => {
|
|
492
|
+
const a1 = new Vec(0, 0)
|
|
493
|
+
const a2 = new Vec(10, 10)
|
|
494
|
+
const points = [new Vec(0, 10), new Vec(5, 5), new Vec(5, 5), new Vec(10, 0)]
|
|
495
|
+
const result = intersectLineSegmentPolyline(a1, a2, points)
|
|
496
|
+
expect(result).not.toBeNull()
|
|
497
|
+
expect(result!.length).toBe(1)
|
|
498
|
+
expect(result![0].x).toBeCloseTo(5, 5)
|
|
499
|
+
expect(result![0].y).toBeCloseTo(5, 5)
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
it('should handle polyline that touches segment endpoint', () => {
|
|
503
|
+
const a1 = new Vec(0, 0)
|
|
504
|
+
const a2 = new Vec(5, 5)
|
|
505
|
+
const points = [new Vec(5, 5), new Vec(10, 0)]
|
|
506
|
+
const result = intersectLineSegmentPolyline(a1, a2, points)
|
|
507
|
+
expect(result).toBeNull() // coincident case
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
it('should handle complex polyline with multiple intersections', () => {
|
|
511
|
+
const a1 = new Vec(0, 5)
|
|
512
|
+
const a2 = new Vec(20, 5)
|
|
513
|
+
const points = [
|
|
514
|
+
new Vec(2, 0),
|
|
515
|
+
new Vec(2, 10),
|
|
516
|
+
new Vec(8, 10),
|
|
517
|
+
new Vec(8, 0),
|
|
518
|
+
new Vec(12, 0),
|
|
519
|
+
new Vec(12, 10),
|
|
520
|
+
new Vec(18, 10),
|
|
521
|
+
new Vec(18, 0),
|
|
522
|
+
]
|
|
523
|
+
const result = intersectLineSegmentPolyline(a1, a2, points)
|
|
524
|
+
expect(result).not.toBeNull()
|
|
525
|
+
expect(result!.length).toBe(4)
|
|
526
|
+
// Should intersect at x=2, x=8, x=12, x=18
|
|
527
|
+
const sorted = result!.slice().sort((a, b) => a.x - b.x)
|
|
528
|
+
expect(sorted[0].x).toBeCloseTo(2, 5)
|
|
529
|
+
expect(sorted[1].x).toBeCloseTo(8, 5)
|
|
530
|
+
expect(sorted[2].x).toBeCloseTo(12, 5)
|
|
531
|
+
expect(sorted[3].x).toBeCloseTo(18, 5)
|
|
532
|
+
sorted.forEach((pt) => expect(pt.y).toBeCloseTo(5, 5))
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
// Test cases for vertex intersection issues
|
|
536
|
+
describe('vertex intersection edge cases', () => {
|
|
537
|
+
it('should detect intersection when line segment passes through polyline vertex', () => {
|
|
538
|
+
const a1 = new Vec(0, 5)
|
|
539
|
+
const a2 = new Vec(10, 5)
|
|
540
|
+
const points = [new Vec(5, 0), new Vec(5, 10)] // vertical line at x=5
|
|
541
|
+
const result = intersectLineSegmentPolyline(a1, a2, points)
|
|
542
|
+
expect(result).not.toBeNull()
|
|
543
|
+
expect(result!.length).toBe(1)
|
|
544
|
+
expect(result![0].x).toBeCloseTo(5, 5)
|
|
545
|
+
expect(result![0].y).toBeCloseTo(5, 5)
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
it('should detect intersection when line segment passes through polyline vertex at angle', () => {
|
|
549
|
+
const a1 = new Vec(0, 0)
|
|
550
|
+
const a2 = new Vec(10, 10)
|
|
551
|
+
const points = [new Vec(5, 0), new Vec(5, 10)] // vertical line at x=5
|
|
552
|
+
const result = intersectLineSegmentPolyline(a1, a2, points)
|
|
553
|
+
expect(result).not.toBeNull()
|
|
554
|
+
expect(result!.length).toBe(1)
|
|
555
|
+
expect(result![0].x).toBeCloseTo(5, 5)
|
|
556
|
+
expect(result![0].y).toBeCloseTo(5, 5)
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
it('should detect intersection when line segment passes through polyline vertex at middle', () => {
|
|
560
|
+
const a1 = new Vec(0, 5)
|
|
561
|
+
const a2 = new Vec(10, 5)
|
|
562
|
+
const points = [new Vec(0, 0), new Vec(5, 5), new Vec(10, 0)] // vertex at (5,5)
|
|
563
|
+
const result = intersectLineSegmentPolyline(a1, a2, points)
|
|
564
|
+
expect(result).not.toBeNull()
|
|
565
|
+
expect(result!.length).toBe(1)
|
|
566
|
+
expect(result![0].x).toBeCloseTo(5, 5)
|
|
567
|
+
expect(result![0].y).toBeCloseTo(5, 5)
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
it('should detect intersection when line segment passes through a polyline vertext (floating point error case)', () => {
|
|
571
|
+
const result = intersectLineSegmentPolyline({ x: 100, y: 100 }, { x: 20, y: 20 }, [
|
|
572
|
+
{ x: 36.141160159025375, y: 31.811740238538057 },
|
|
573
|
+
{ x: 34.14213562373095, y: 34.14213562373095 },
|
|
574
|
+
{ x: 31.811740238538057, y: 36.141160159025375 },
|
|
575
|
+
])
|
|
576
|
+
|
|
577
|
+
expect(result).not.toBeNull()
|
|
578
|
+
})
|
|
579
|
+
})
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
describe('intersectLineSegmentPolygon', () => {
|
|
583
|
+
it('should return null when no intersection with polygon', () => {
|
|
584
|
+
const a1 = new Vec(0, 0)
|
|
585
|
+
const a2 = new Vec(1, 0)
|
|
586
|
+
const points = [new Vec(5, 5), new Vec(6, 5), new Vec(6, 6), new Vec(5, 6)]
|
|
587
|
+
const result = intersectLineSegmentPolygon(a1, a2, points)
|
|
588
|
+
expect(result).toBeNull()
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
it('should return multiple intersection points', () => {
|
|
592
|
+
const a1 = new Vec(0, 5)
|
|
593
|
+
const a2 = new Vec(10, 5)
|
|
594
|
+
const points = [new Vec(2, 0), new Vec(2, 10), new Vec(8, 10), new Vec(8, 0)]
|
|
595
|
+
const result = intersectLineSegmentPolygon(a1, a2, points)
|
|
596
|
+
expect(result).not.toBeNull()
|
|
597
|
+
expect(result!.length).toBe(2)
|
|
598
|
+
const sorted = result!.slice().sort((a, b) => a.x - b.x)
|
|
599
|
+
expect(sorted[0].x).toBeCloseTo(2, 5)
|
|
600
|
+
expect(sorted[1].x).toBeCloseTo(8, 5)
|
|
601
|
+
sorted.forEach((pt) => expect(pt.y).toBeCloseTo(5, 5))
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
it('should return null when segment is entirely inside the polygon', () => {
|
|
605
|
+
const a1 = new Vec(3, 3)
|
|
606
|
+
const a2 = new Vec(4, 4)
|
|
607
|
+
const points = [new Vec(2, 2), new Vec(6, 2), new Vec(6, 6), new Vec(2, 6)]
|
|
608
|
+
const result = intersectLineSegmentPolygon(a1, a2, points)
|
|
609
|
+
expect(result).toBeNull()
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
it('should return null when segment is entirely outside the polygon', () => {
|
|
613
|
+
const a1 = new Vec(0, 0)
|
|
614
|
+
const a2 = new Vec(1, 1)
|
|
615
|
+
const points = [new Vec(2, 2), new Vec(6, 2), new Vec(6, 6), new Vec(2, 6)]
|
|
616
|
+
const result = intersectLineSegmentPolygon(a1, a2, points)
|
|
617
|
+
expect(result).toBeNull()
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
it('should return intersection points when segment crosses polygon boundary', () => {
|
|
621
|
+
const a1 = new Vec(0, 5)
|
|
622
|
+
const a2 = new Vec(10, 5)
|
|
623
|
+
const points = [new Vec(2, 0), new Vec(2, 10), new Vec(8, 10), new Vec(8, 0)]
|
|
624
|
+
const result = intersectLineSegmentPolygon(a1, a2, points)
|
|
625
|
+
expect(result).not.toBeNull()
|
|
626
|
+
expect(result!.length).toBe(2)
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
it('should return null for empty polygon', () => {
|
|
630
|
+
const a1 = new Vec(0, 0)
|
|
631
|
+
const a2 = new Vec(1, 1)
|
|
632
|
+
const points: VecLike[] = []
|
|
633
|
+
const result = intersectLineSegmentPolygon(a1, a2, points)
|
|
634
|
+
expect(result).toBeNull()
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
it('should return null for single point polygon', () => {
|
|
638
|
+
const a1 = new Vec(0, 0)
|
|
639
|
+
const a2 = new Vec(1, 1)
|
|
640
|
+
const points = [new Vec(5, 5)]
|
|
641
|
+
const result = intersectLineSegmentPolygon(a1, a2, points)
|
|
642
|
+
expect(result).toBeNull()
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
it('should return single intersection at exit corner', () => {
|
|
646
|
+
const a1 = new Vec(0, 0)
|
|
647
|
+
const a2 = new Vec(10, 10)
|
|
648
|
+
const points = [new Vec(0, 10), new Vec(10, 10), new Vec(10, 0), new Vec(0, 0)]
|
|
649
|
+
const result = intersectLineSegmentPolygon(a1, a2, points)
|
|
650
|
+
expect(result).not.toBeNull()
|
|
651
|
+
expect(result!.length).toBe(1)
|
|
652
|
+
expect(result![0].x).toBeCloseTo(10, 5)
|
|
653
|
+
expect(result![0].y).toBeCloseTo(10, 5)
|
|
654
|
+
})
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
describe('intersectCircleCircle', () => {
|
|
658
|
+
it('should return two points when circles intersect', () => {
|
|
659
|
+
const c1 = new Vec(0, 0)
|
|
660
|
+
const r1 = 5
|
|
661
|
+
const c2 = new Vec(8, 0)
|
|
662
|
+
const r2 = 5
|
|
663
|
+
const result = intersectCircleCircle(c1, r1, c2, r2)
|
|
664
|
+
expect(result).not.toBeNull()
|
|
665
|
+
expect(result.length).toBe(2)
|
|
666
|
+
// Both points should be at x=4, y=+/-3
|
|
667
|
+
const sorted = result.slice().sort((a, b) => a.y - b.y)
|
|
668
|
+
expect(sorted[0].x).toBeCloseTo(4, 5)
|
|
669
|
+
expect(sorted[0].y).toBeCloseTo(-3, 5)
|
|
670
|
+
expect(sorted[1].x).toBeCloseTo(4, 5)
|
|
671
|
+
expect(sorted[1].y).toBeCloseTo(3, 5)
|
|
672
|
+
})
|
|
673
|
+
|
|
674
|
+
it('should return two identical points when circles are tangent', () => {
|
|
675
|
+
const c1 = new Vec(0, 0)
|
|
676
|
+
const r1 = 5
|
|
677
|
+
const c2 = new Vec(10, 0)
|
|
678
|
+
const r2 = 5
|
|
679
|
+
const result = intersectCircleCircle(c1, r1, c2, r2)
|
|
680
|
+
expect(result).not.toBeNull()
|
|
681
|
+
expect(result.length).toBe(2)
|
|
682
|
+
// Both points should be at (5, 0)
|
|
683
|
+
expect(result[0].x).toBeCloseTo(5, 5)
|
|
684
|
+
expect(result[0].y).toBeCloseTo(0, 5)
|
|
685
|
+
expect(result[1].x).toBeCloseTo(5, 5)
|
|
686
|
+
expect(result[1].y).toBeCloseTo(0, 5)
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
it('should return two points when circles intersect at an angle', () => {
|
|
690
|
+
const c1 = new Vec(0, 0)
|
|
691
|
+
const r1 = 5
|
|
692
|
+
const c2 = new Vec(5, 5)
|
|
693
|
+
const r2 = 5
|
|
694
|
+
const result = intersectCircleCircle(c1, r1, c2, r2)
|
|
695
|
+
expect(result).not.toBeNull()
|
|
696
|
+
expect(result.length).toBe(2)
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
it('should return NaN points for concentric circles', () => {
|
|
700
|
+
const c1 = new Vec(0, 0)
|
|
701
|
+
const r1 = 5
|
|
702
|
+
const c2 = new Vec(0, 0)
|
|
703
|
+
const r2 = 3
|
|
704
|
+
const result = intersectCircleCircle(c1, r1, c2, r2)
|
|
705
|
+
expect(result).not.toBeNull()
|
|
706
|
+
result.forEach((pt) => {
|
|
707
|
+
expect(Number.isNaN(pt.x)).toBe(true)
|
|
708
|
+
expect(Number.isNaN(pt.y)).toBe(true)
|
|
709
|
+
})
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
it('should return two points for identical circles', () => {
|
|
713
|
+
const c1 = new Vec(0, 0)
|
|
714
|
+
const r1 = 5
|
|
715
|
+
const c2 = new Vec(0, 0)
|
|
716
|
+
const r2 = 5
|
|
717
|
+
const result = intersectCircleCircle(c1, r1, c2, r2)
|
|
718
|
+
expect(result).not.toBeNull()
|
|
719
|
+
result.forEach((pt) => {
|
|
720
|
+
expect(Number.isNaN(pt.x)).toBe(true)
|
|
721
|
+
expect(Number.isNaN(pt.y)).toBe(true)
|
|
722
|
+
})
|
|
723
|
+
})
|
|
724
|
+
|
|
725
|
+
it('should return two NaN points when one circle is inside another', () => {
|
|
726
|
+
const c1 = new Vec(0, 0)
|
|
727
|
+
const r1 = 5
|
|
728
|
+
const c2 = new Vec(1, 0)
|
|
729
|
+
const r2 = 1
|
|
730
|
+
const result = intersectCircleCircle(c1, r1, c2, r2)
|
|
731
|
+
expect(result).not.toBeNull()
|
|
732
|
+
result.forEach((pt) => {
|
|
733
|
+
expect(Number.isNaN(pt.x)).toBe(true)
|
|
734
|
+
expect(Number.isNaN(pt.y)).toBe(true)
|
|
735
|
+
})
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
it('should return two NaN points when circles are too far apart', () => {
|
|
739
|
+
const c1 = new Vec(0, 0)
|
|
740
|
+
const r1 = 5
|
|
741
|
+
const c2 = new Vec(20, 0)
|
|
742
|
+
const r2 = 5
|
|
743
|
+
const result = intersectCircleCircle(c1, r1, c2, r2)
|
|
744
|
+
expect(result).not.toBeNull()
|
|
745
|
+
result.forEach((pt) => {
|
|
746
|
+
expect(Number.isNaN(pt.x)).toBe(true)
|
|
747
|
+
expect(Number.isNaN(pt.y)).toBe(true)
|
|
748
|
+
})
|
|
749
|
+
})
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
describe('intersectCirclePolygon', () => {
|
|
753
|
+
it('should return null when circle does not intersect polygon', () => {
|
|
754
|
+
const c = new Vec(0, 0)
|
|
755
|
+
const r = 1
|
|
756
|
+
const points = [new Vec(5, 5), new Vec(6, 5), new Vec(6, 6), new Vec(5, 6)]
|
|
757
|
+
const result = intersectCirclePolygon(c, r, points)
|
|
758
|
+
expect(result).toBeNull()
|
|
759
|
+
})
|
|
760
|
+
|
|
761
|
+
it('should return single intersection point (tangent)', () => {
|
|
762
|
+
const c = new Vec(0, 0)
|
|
763
|
+
const r = 5
|
|
764
|
+
const points = [new Vec(5, 0), new Vec(10, 0), new Vec(10, 10), new Vec(5, 10)]
|
|
765
|
+
const result = intersectCirclePolygon(c, r, points)
|
|
766
|
+
expect(result).not.toBeNull()
|
|
767
|
+
expect(result!.length).toBe(1)
|
|
768
|
+
expect(result![0].x).toBeCloseTo(5, 5)
|
|
769
|
+
expect(result![0].y).toBeCloseTo(0, 5)
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
it('should return null when circle is entirely inside polygon', () => {
|
|
773
|
+
const c = new Vec(5, 5)
|
|
774
|
+
const r = 1
|
|
775
|
+
const points = [new Vec(2, 2), new Vec(8, 2), new Vec(8, 8), new Vec(2, 8)]
|
|
776
|
+
const result = intersectCirclePolygon(c, r, points)
|
|
777
|
+
expect(result).toBeNull()
|
|
778
|
+
})
|
|
779
|
+
|
|
780
|
+
it('should return null when circle is entirely outside polygon', () => {
|
|
781
|
+
const c = new Vec(0, 0)
|
|
782
|
+
const r = 1
|
|
783
|
+
const points = [new Vec(5, 5), new Vec(8, 5), new Vec(8, 8), new Vec(5, 8)]
|
|
784
|
+
const result = intersectCirclePolygon(c, r, points)
|
|
785
|
+
expect(result).toBeNull()
|
|
786
|
+
})
|
|
787
|
+
|
|
788
|
+
it('should return null for empty polygon', () => {
|
|
789
|
+
const c = new Vec(0, 0)
|
|
790
|
+
const r = 1
|
|
791
|
+
const points: VecLike[] = []
|
|
792
|
+
const result = intersectCirclePolygon(c, r, points)
|
|
793
|
+
expect(result).toBeNull()
|
|
794
|
+
})
|
|
795
|
+
|
|
796
|
+
it('should return null for complex polygon when circle is inside', () => {
|
|
797
|
+
const c = new Vec(5, 5)
|
|
798
|
+
const r = 3
|
|
799
|
+
const points = [new Vec(2, 2), new Vec(8, 2), new Vec(8, 8), new Vec(2, 8)]
|
|
800
|
+
const result = intersectCirclePolygon(c, r, points)
|
|
801
|
+
expect(result).toBeNull()
|
|
802
|
+
})
|
|
803
|
+
})
|
|
804
|
+
|
|
805
|
+
describe('intersectCirclePolyline', () => {
|
|
806
|
+
it('should return null when circle does not intersect polyline', () => {
|
|
807
|
+
const c = new Vec(0, 0)
|
|
808
|
+
const r = 1
|
|
809
|
+
const points = [new Vec(5, 5), new Vec(6, 5), new Vec(6, 6), new Vec(5, 6)]
|
|
810
|
+
const result = intersectCirclePolyline(c, r, points)
|
|
811
|
+
expect(result).toBeNull()
|
|
812
|
+
})
|
|
813
|
+
|
|
814
|
+
it('should return single intersection point (tangent)', () => {
|
|
815
|
+
const c = new Vec(0, 0)
|
|
816
|
+
const r = 5
|
|
817
|
+
const points = [new Vec(5, 0), new Vec(10, 0)]
|
|
818
|
+
const result = intersectCirclePolyline(c, r, points)
|
|
819
|
+
expect(result).not.toBeNull()
|
|
820
|
+
expect(result!.length).toBe(1)
|
|
821
|
+
expect(result![0].x).toBeCloseTo(5, 5)
|
|
822
|
+
expect(result![0].y).toBeCloseTo(0, 5)
|
|
823
|
+
})
|
|
824
|
+
|
|
825
|
+
it('should return two intersection points', () => {
|
|
826
|
+
const c = new Vec(0, 0)
|
|
827
|
+
const r = 5
|
|
828
|
+
const points = [new Vec(-10, 0), new Vec(10, 0)]
|
|
829
|
+
const result = intersectCirclePolyline(c, r, points)
|
|
830
|
+
expect(result).not.toBeNull()
|
|
831
|
+
expect(result!.length).toBe(2)
|
|
832
|
+
const sorted = result!.slice().sort((a, b) => a.x - b.x)
|
|
833
|
+
expect(sorted[0].x).toBeCloseTo(-5, 5)
|
|
834
|
+
expect(sorted[1].x).toBeCloseTo(5, 5)
|
|
835
|
+
sorted.forEach((pt) => expect(pt.y).toBeCloseTo(0, 5))
|
|
836
|
+
})
|
|
837
|
+
|
|
838
|
+
it('should return null for empty polyline', () => {
|
|
839
|
+
const c = new Vec(0, 0)
|
|
840
|
+
const r = 1
|
|
841
|
+
const points: VecLike[] = []
|
|
842
|
+
const result = intersectCirclePolyline(c, r, points)
|
|
843
|
+
expect(result).toBeNull()
|
|
844
|
+
})
|
|
845
|
+
|
|
846
|
+
it('should return null for single point polyline', () => {
|
|
847
|
+
const c = new Vec(0, 0)
|
|
848
|
+
const r = 1
|
|
849
|
+
const points = [new Vec(5, 5)]
|
|
850
|
+
const result = intersectCirclePolyline(c, r, points)
|
|
851
|
+
expect(result).toBeNull()
|
|
852
|
+
})
|
|
853
|
+
|
|
854
|
+
it('should return null when circle is entirely inside polyline area', () => {
|
|
855
|
+
const c = new Vec(5, 5)
|
|
856
|
+
const r = 1
|
|
857
|
+
const points = [new Vec(2, 2), new Vec(8, 2), new Vec(8, 8), new Vec(2, 8)]
|
|
858
|
+
const result = intersectCirclePolyline(c, r, points)
|
|
859
|
+
expect(result).toBeNull()
|
|
860
|
+
})
|
|
861
|
+
|
|
862
|
+
it('should return null when circle is entirely outside polyline area', () => {
|
|
863
|
+
const c = new Vec(0, 0)
|
|
864
|
+
const r = 1
|
|
865
|
+
const points = [new Vec(5, 5), new Vec(8, 5), new Vec(8, 8), new Vec(5, 8)]
|
|
866
|
+
const result = intersectCirclePolyline(c, r, points)
|
|
867
|
+
expect(result).toBeNull()
|
|
868
|
+
})
|
|
869
|
+
})
|
|
870
|
+
|
|
871
|
+
describe('intersectPolygonPolygon', () => {
|
|
872
|
+
it('should return null for disjoint polygons', () => {
|
|
873
|
+
const polyA = [new Vec(0, 0), new Vec(2, 0), new Vec(2, 2), new Vec(0, 2)]
|
|
874
|
+
const polyB = [new Vec(5, 5), new Vec(7, 5), new Vec(7, 7), new Vec(5, 7)]
|
|
875
|
+
const result = intersectPolygonPolygon(polyA, polyB)
|
|
876
|
+
expect(result).toBeNull()
|
|
877
|
+
})
|
|
878
|
+
|
|
879
|
+
it('should return intersection polygon for overlapping squares', () => {
|
|
880
|
+
const polyA = [new Vec(0, 0), new Vec(4, 0), new Vec(4, 4), new Vec(0, 4)]
|
|
881
|
+
const polyB = [new Vec(2, 2), new Vec(6, 2), new Vec(6, 6), new Vec(2, 6)]
|
|
882
|
+
const result = intersectPolygonPolygon(polyA, polyB)
|
|
883
|
+
expect(result).not.toBeNull()
|
|
884
|
+
expect(result!.length).toBe(4)
|
|
885
|
+
// Should be the square from (2,2) to (4,4)
|
|
886
|
+
const xs = result!.map((pt) => pt.x).sort((a, b) => a - b)
|
|
887
|
+
const ys = result!.map((pt) => pt.y).sort((a, b) => a - b)
|
|
888
|
+
expect(xs[0]).toBeCloseTo(2, 5)
|
|
889
|
+
expect(xs[3]).toBeCloseTo(4, 5)
|
|
890
|
+
expect(ys[0]).toBeCloseTo(2, 5)
|
|
891
|
+
expect(ys[3]).toBeCloseTo(4, 5)
|
|
892
|
+
})
|
|
893
|
+
|
|
894
|
+
it('should return contained polygon when one is inside another', () => {
|
|
895
|
+
const polyA = [new Vec(0, 0), new Vec(10, 0), new Vec(10, 10), new Vec(0, 10)]
|
|
896
|
+
const polyB = [new Vec(2, 2), new Vec(8, 2), new Vec(8, 8), new Vec(2, 8)]
|
|
897
|
+
const result = intersectPolygonPolygon(polyA, polyB)
|
|
898
|
+
expect(result).not.toBeNull()
|
|
899
|
+
expect(result!.length).toBe(4)
|
|
900
|
+
const xs = result!.map((pt) => pt.x).sort((a, b) => a - b)
|
|
901
|
+
const ys = result!.map((pt) => pt.y).sort((a, b) => a - b)
|
|
902
|
+
expect(xs[0]).toBeCloseTo(2, 5)
|
|
903
|
+
expect(xs[3]).toBeCloseTo(8, 5)
|
|
904
|
+
expect(ys[0]).toBeCloseTo(2, 5)
|
|
905
|
+
expect(ys[3]).toBeCloseTo(8, 5)
|
|
906
|
+
})
|
|
907
|
+
|
|
908
|
+
it('should return single point when polygons touch at a point', () => {
|
|
909
|
+
const polyA = [new Vec(0, 0), new Vec(2, 0), new Vec(2, 2), new Vec(0, 2)]
|
|
910
|
+
const polyB = [new Vec(2, 2), new Vec(4, 2), new Vec(4, 4), new Vec(2, 4)]
|
|
911
|
+
const result = intersectPolygonPolygon(polyA, polyB)
|
|
912
|
+
expect(result).not.toBeNull()
|
|
913
|
+
expect(result!.length).toBe(1)
|
|
914
|
+
expect(result![0].x).toBeCloseTo(2, 5)
|
|
915
|
+
expect(result![0].y).toBeCloseTo(2, 5)
|
|
916
|
+
})
|
|
917
|
+
|
|
918
|
+
it('should return shared edge when polygons share an edge', () => {
|
|
919
|
+
const polyA = [new Vec(0, 0), new Vec(2, 0), new Vec(2, 2), new Vec(0, 2)]
|
|
920
|
+
const polyB = [new Vec(2, 0), new Vec(4, 0), new Vec(4, 2), new Vec(2, 2)]
|
|
921
|
+
const result = intersectPolygonPolygon(polyA, polyB)
|
|
922
|
+
expect(result).not.toBeNull()
|
|
923
|
+
expect(result!.length).toBe(2)
|
|
924
|
+
const xs = result!.map((pt) => pt.x).sort((a, b) => a - b)
|
|
925
|
+
const ys = result!.map((pt) => pt.y).sort((a, b) => a - b)
|
|
926
|
+
expect(xs[0]).toBeCloseTo(2, 5)
|
|
927
|
+
expect(xs[1]).toBeCloseTo(2, 5)
|
|
928
|
+
expect(ys[0]).toBeCloseTo(0, 5)
|
|
929
|
+
expect(ys[1]).toBeCloseTo(2, 5)
|
|
930
|
+
})
|
|
931
|
+
|
|
932
|
+
it('should return all points for identical polygons', () => {
|
|
933
|
+
const polyA = [new Vec(0, 0), new Vec(2, 0), new Vec(2, 2), new Vec(0, 2)]
|
|
934
|
+
const polyB = [new Vec(0, 0), new Vec(2, 0), new Vec(2, 2), new Vec(0, 2)]
|
|
935
|
+
const result = intersectPolygonPolygon(polyA, polyB)
|
|
936
|
+
expect(result).not.toBeNull()
|
|
937
|
+
expect(result!.length).toBe(4)
|
|
938
|
+
})
|
|
939
|
+
|
|
940
|
+
it('should return null for degenerate polygons (single point)', () => {
|
|
941
|
+
const polyA = [new Vec(0, 0)]
|
|
942
|
+
const polyB = [new Vec(1, 1), new Vec(2, 2), new Vec(3, 3)]
|
|
943
|
+
const result = intersectPolygonPolygon(polyA, polyB)
|
|
944
|
+
expect(result).toBeNull()
|
|
945
|
+
})
|
|
946
|
+
})
|