squirreling 0.9.3 → 0.9.5

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.
@@ -0,0 +1,371 @@
1
+ import { bboxOverlap } from './bbox.js'
2
+ import { pointInPolygon, pointLineRelation, pointOnLine } from './pointRelations.js'
3
+ import { EPSILON_SQ, distSq } from './primitives.js'
4
+ import { segmentIntersectsRing, segmentTouchPoint, segmentsIntersect } from './segments.js'
5
+
6
+ /**
7
+ * @import { Relation, SimpleGeometry } from './geometry.js'
8
+ */
9
+
10
+ /**
11
+ * Test if a linestring intersects a polygon.
12
+ *
13
+ * @param {number[][]} line
14
+ * @param {number[][][]} rings
15
+ * @returns {boolean}
16
+ */
17
+ function lineIntersectsPolygon(line, rings) {
18
+ // Fast path for common containment queries: if one point is inside or on
19
+ // boundary, the line intersects.
20
+ if (pointInPolygon(line[0], rings) !== 'OUTSIDE') return true
21
+
22
+ // Otherwise, detect crossings/touches against polygon boundaries.
23
+ for (let i = 0; i < line.length - 1; i++) {
24
+ for (const ring of rings) {
25
+ if (segmentIntersectsRing(line[i], line[i + 1], ring)) return true
26
+ }
27
+ }
28
+ return false
29
+ }
30
+
31
+ /**
32
+ * Test if two linestrings share any point (intersect).
33
+ *
34
+ * @param {number[][]} line1
35
+ * @param {number[][]} line2
36
+ * @returns {boolean}
37
+ */
38
+ function linesIntersect(line1, line2) {
39
+ for (let i = 0; i < line1.length - 1; i++) {
40
+ for (let j = 0; j < line2.length - 1; j++) {
41
+ if (segmentsIntersect(line1[i], line1[i + 1], line2[j], line2[j + 1])) return true
42
+ }
43
+ }
44
+ return false
45
+ }
46
+
47
+ /**
48
+ * @param {number[]} point
49
+ * @param {number[][]} line
50
+ * @returns {boolean}
51
+ */
52
+ function isLineEndpoint(point, line) {
53
+ return distSq(point, line[0]) < EPSILON_SQ || distSq(point, line[line.length - 1]) < EPSILON_SQ
54
+ }
55
+
56
+ /**
57
+ * Classify containment of a linestring within a polygon.
58
+ * Returns 'INSIDE' if entirely in interior, 'BOUNDARY' if inside but
59
+ * touches boundary, 'OUTSIDE' if any part is outside.
60
+ *
61
+ * @param {number[][]} line
62
+ * @param {number[][][]} rings
63
+ * @returns {Relation}
64
+ */
65
+ function polygonContainsLine(line, rings) {
66
+ /** @type {Relation} */
67
+ let result = 'INSIDE'
68
+ for (const pt of line) {
69
+ const rel = pointInPolygon(pt, rings)
70
+ if (rel === 'OUTSIDE') return 'OUTSIDE'
71
+ if (rel === 'BOUNDARY') result = 'BOUNDARY'
72
+ }
73
+ for (let i = 0; i < line.length - 1; i++) {
74
+ const mid = [(line[i][0] + line[i + 1][0]) / 2, (line[i][1] + line[i + 1][1]) / 2]
75
+ const rel = pointInPolygon(mid, rings)
76
+ if (rel === 'OUTSIDE') return 'OUTSIDE'
77
+ if (rel === 'BOUNDARY') result = 'BOUNDARY'
78
+ }
79
+ return result
80
+ }
81
+
82
+ /**
83
+ * Classify containment of polygon B within polygon A.
84
+ * Returns 'INSIDE' if entirely in interior, 'BOUNDARY' if inside but
85
+ * touches boundary, 'OUTSIDE' if any part is outside.
86
+ *
87
+ * @param {number[][][]} ringsA
88
+ * @param {number[][][]} ringsB
89
+ * @returns {Relation}
90
+ */
91
+ function polygonContainsPolygon(ringsA, ringsB) {
92
+ /** @type {Relation} */
93
+ let result = 'INSIDE'
94
+ for (const pt of ringsB[0]) {
95
+ const rel = pointInPolygon(pt, ringsA)
96
+ if (rel === 'OUTSIDE') return 'OUTSIDE'
97
+ if (rel === 'BOUNDARY') result = 'BOUNDARY'
98
+ }
99
+ for (let i = 0; i < ringsB[0].length - 1; i++) {
100
+ const mid = [(ringsB[0][i][0] + ringsB[0][i + 1][0]) / 2, (ringsB[0][i][1] + ringsB[0][i + 1][1]) / 2]
101
+ const rel = pointInPolygon(mid, ringsA)
102
+ if (rel === 'OUTSIDE') return 'OUTSIDE'
103
+ if (rel === 'BOUNDARY') result = 'BOUNDARY'
104
+ }
105
+ return result
106
+ }
107
+
108
+ /**
109
+ * Classify the relationship between two linestrings.
110
+ * Returns INSIDE if interiors share a point, BOUNDARY if they only meet
111
+ * at endpoints, OUTSIDE if disjoint.
112
+ *
113
+ * @param {number[][]} line1
114
+ * @param {number[][]} line2
115
+ * @returns {Relation}
116
+ */
117
+ function lineLineRelation(line1, line2) {
118
+ let boundary = false
119
+ for (let i = 0; i < line1.length - 1; i++) {
120
+ for (let j = 0; j < line2.length - 1; j++) {
121
+ const touchPoint = segmentTouchPoint(line1[i], line1[i + 1], line2[j], line2[j + 1])
122
+ if (touchPoint === 'OUTSIDE') continue
123
+ if (touchPoint === 'INSIDE') return 'INSIDE'
124
+ if (!isLineEndpoint(touchPoint, line1) && !isLineEndpoint(touchPoint, line2)) return 'INSIDE'
125
+ boundary = true
126
+ }
127
+ }
128
+ return boundary ? 'BOUNDARY' : 'OUTSIDE'
129
+ }
130
+
131
+ /**
132
+ * Classify the relationship between a linestring and a polygon.
133
+ * Returns INSIDE if line interior enters polygon interior, BOUNDARY if
134
+ * they only share boundary points, OUTSIDE if disjoint.
135
+ *
136
+ * @param {number[][]} line
137
+ * @param {number[][][]} rings
138
+ * @returns {Relation}
139
+ */
140
+ function linePolygonRelation(line, rings) {
141
+ let boundary = false
142
+ // Check segment midpoints and interior vertices
143
+ for (let i = 0; i < line.length - 1; i++) {
144
+ const mid = [(line[i][0] + line[i + 1][0]) / 2, (line[i][1] + line[i + 1][1]) / 2]
145
+ const midRel = pointInPolygon(mid, rings)
146
+ if (midRel === 'INSIDE') return 'INSIDE'
147
+ if (midRel === 'BOUNDARY') boundary = true
148
+ }
149
+ // Check interior vertices of the line
150
+ for (let i = 1; i < line.length - 1; i++) {
151
+ const rel = pointInPolygon(line[i], rings)
152
+ if (rel === 'INSIDE') return 'INSIDE'
153
+ if (rel === 'BOUNDARY') boundary = true
154
+ }
155
+ // Check line endpoints
156
+ for (const pt of [line[0], line[line.length - 1]]) {
157
+ const rel = pointInPolygon(pt, rings)
158
+ if (rel === 'INSIDE') return 'INSIDE'
159
+ if (rel === 'BOUNDARY') boundary = true
160
+ }
161
+ // Check if any edge of the polygon rings intersects the line
162
+ if (!boundary) {
163
+ for (let i = 0; i < line.length - 1; i++) {
164
+ for (const ring of rings) {
165
+ if (segmentIntersectsRing(line[i], line[i + 1], ring)) {
166
+ boundary = true
167
+ }
168
+ }
169
+ }
170
+ }
171
+ return boundary ? 'BOUNDARY' : 'OUTSIDE'
172
+ }
173
+
174
+ /**
175
+ * Classify the relationship between two polygons.
176
+ * Returns INSIDE if interiors share area, BOUNDARY if they only share
177
+ * boundary points/edges, OUTSIDE if disjoint.
178
+ *
179
+ * @param {number[][][]} rings1
180
+ * @param {number[][][]} rings2
181
+ * @returns {Relation}
182
+ */
183
+ function polygonPolygonRelation(rings1, rings2) {
184
+ let boundary = false
185
+ // Check vertices of polygon1 against polygon2
186
+ for (const pt of rings1[0]) {
187
+ const rel = pointInPolygon(pt, rings2)
188
+ if (rel === 'INSIDE') return 'INSIDE'
189
+ if (rel === 'BOUNDARY') boundary = true
190
+ }
191
+ // Check vertices of polygon2 against polygon1
192
+ for (const pt of rings2[0]) {
193
+ const rel = pointInPolygon(pt, rings1)
194
+ if (rel === 'INSIDE') return 'INSIDE'
195
+ if (rel === 'BOUNDARY') boundary = true
196
+ }
197
+ // Check edge midpoints of polygon1 against polygon2
198
+ for (let i = 0; i < rings1[0].length - 1; i++) {
199
+ const mid = [(rings1[0][i][0] + rings1[0][i + 1][0]) / 2, (rings1[0][i][1] + rings1[0][i + 1][1]) / 2]
200
+ const rel = pointInPolygon(mid, rings2)
201
+ if (rel === 'INSIDE') return 'INSIDE'
202
+ if (rel === 'BOUNDARY') boundary = true
203
+ }
204
+ // Check edge midpoints of polygon2 against polygon1
205
+ for (let i = 0; i < rings2[0].length - 1; i++) {
206
+ const mid = [(rings2[0][i][0] + rings2[0][i + 1][0]) / 2, (rings2[0][i][1] + rings2[0][i + 1][1]) / 2]
207
+ const rel = pointInPolygon(mid, rings1)
208
+ if (rel === 'INSIDE') return 'INSIDE'
209
+ if (rel === 'BOUNDARY') boundary = true
210
+ }
211
+ // Check edge-edge intersections
212
+ if (!boundary) {
213
+ for (let i = 0; i < rings1[0].length - 1; i++) {
214
+ for (let j = 0; j < rings2[0].length - 1; j++) {
215
+ if (segmentsIntersect(rings1[0][i], rings1[0][i + 1], rings2[0][j], rings2[0][j + 1])) {
216
+ boundary = true
217
+ }
218
+ }
219
+ }
220
+ }
221
+ return boundary ? 'BOUNDARY' : 'OUTSIDE'
222
+ }
223
+
224
+ /**
225
+ * @param {SimpleGeometry[]} partsA
226
+ * @param {SimpleGeometry[]} partsB
227
+ * @returns {boolean}
228
+ */
229
+ export function intersects(partsA, partsB) {
230
+ for (const pa of partsA) {
231
+ for (const pb of partsB) {
232
+ if (pairIntersects(pa, pb)) return true
233
+ }
234
+ }
235
+ return false
236
+ }
237
+
238
+ /**
239
+ * @param {SimpleGeometry} a
240
+ * @param {SimpleGeometry} b
241
+ * @returns {boolean}
242
+ */
243
+ function pairIntersects(a, b) {
244
+ if (!bboxOverlap(a, b)) return false
245
+ const ta = a.type
246
+ const tb = b.type
247
+
248
+ if (ta === 'Point' && tb === 'Point') {
249
+ return distSq(a.coordinates, b.coordinates) < EPSILON_SQ
250
+ }
251
+ if (ta === 'Point' && tb === 'LineString') {
252
+ return pointOnLine(a.coordinates, b.coordinates)
253
+ }
254
+ if (ta === 'LineString' && tb === 'Point') {
255
+ return pointOnLine(b.coordinates, a.coordinates)
256
+ }
257
+ if (ta === 'Point' && tb === 'Polygon') {
258
+ return pointInPolygon(a.coordinates, b.coordinates) !== 'OUTSIDE'
259
+ }
260
+ if (ta === 'Polygon' && tb === 'Point') {
261
+ return pointInPolygon(b.coordinates, a.coordinates) !== 'OUTSIDE'
262
+ }
263
+ if (ta === 'LineString' && tb === 'LineString') {
264
+ return linesIntersect(a.coordinates, b.coordinates)
265
+ }
266
+ if (ta === 'LineString' && tb === 'Polygon') {
267
+ return lineIntersectsPolygon(a.coordinates, b.coordinates)
268
+ }
269
+ if (ta === 'Polygon' && tb === 'LineString') {
270
+ return lineIntersectsPolygon(b.coordinates, a.coordinates)
271
+ }
272
+ if (ta === 'Polygon' && tb === 'Polygon') {
273
+ return polygonPolygonRelation(a.coordinates, b.coordinates) !== 'OUTSIDE'
274
+ }
275
+ return false
276
+ }
277
+
278
+ /**
279
+ * Classify the relationship between two simple geometries.
280
+ * Returns 'INSIDE' if interiors intersect, 'BOUNDARY' if they
281
+ * intersect only at boundaries, or 'OUTSIDE' if they don't intersect.
282
+ *
283
+ * @param {SimpleGeometry} a
284
+ * @param {SimpleGeometry} b
285
+ * @returns {Relation}
286
+ */
287
+ export function pairRelation(a, b) {
288
+ if (!bboxOverlap(a, b)) return 'OUTSIDE'
289
+ const ta = a.type
290
+ const tb = b.type
291
+
292
+ // Point / Point
293
+ if (ta === 'Point' && tb === 'Point') {
294
+ return distSq(a.coordinates, b.coordinates) < EPSILON_SQ ? 'INSIDE' : 'OUTSIDE'
295
+ }
296
+
297
+ // Point / LineString
298
+ if (ta === 'Point' && tb === 'LineString') {
299
+ return pointLineRelation(a.coordinates, b.coordinates)
300
+ }
301
+ if (ta === 'LineString' && tb === 'Point') {
302
+ return pointLineRelation(b.coordinates, a.coordinates)
303
+ }
304
+
305
+ // Point / Polygon
306
+ if (ta === 'Point' && tb === 'Polygon') {
307
+ return pointInPolygon(a.coordinates, b.coordinates)
308
+ }
309
+ if (ta === 'Polygon' && tb === 'Point') {
310
+ return pointInPolygon(b.coordinates, a.coordinates)
311
+ }
312
+
313
+ // LineString / LineString
314
+ if (ta === 'LineString' && tb === 'LineString') {
315
+ return lineLineRelation(a.coordinates, b.coordinates)
316
+ }
317
+
318
+ // LineString / Polygon
319
+ if (ta === 'LineString' && tb === 'Polygon') {
320
+ return linePolygonRelation(a.coordinates, b.coordinates)
321
+ }
322
+ if (ta === 'Polygon' && tb === 'LineString') {
323
+ return linePolygonRelation(b.coordinates, a.coordinates)
324
+ }
325
+
326
+ // Polygon / Polygon
327
+ if (ta === 'Polygon' && tb === 'Polygon') {
328
+ return polygonPolygonRelation(a.coordinates, b.coordinates)
329
+ }
330
+
331
+ return 'OUTSIDE'
332
+ }
333
+
334
+ /**
335
+ * Classify containment of b within a.
336
+ * Returns 'INSIDE' if b is strictly in a's interior, 'BOUNDARY' if b is
337
+ * inside a but touches a's boundary, 'OUTSIDE' if any part of b is outside a.
338
+ *
339
+ * @param {SimpleGeometry} a
340
+ * @param {SimpleGeometry} b
341
+ * @returns {Relation}
342
+ */
343
+ export function pairContainment(a, b) {
344
+ if (!bboxOverlap(a, b)) return 'OUTSIDE'
345
+ const ta = a.type
346
+ const tb = b.type
347
+
348
+ if (ta === 'Point' && tb === 'Point') {
349
+ return distSq(a.coordinates, b.coordinates) < EPSILON_SQ ? 'BOUNDARY' : 'OUTSIDE'
350
+ }
351
+ if (ta === 'LineString' && tb === 'Point') {
352
+ return pointLineRelation(b.coordinates, a.coordinates)
353
+ }
354
+ if (ta === 'Polygon' && tb === 'Point') {
355
+ return pointInPolygon(b.coordinates, a.coordinates)
356
+ }
357
+ if (ta === 'Polygon' && tb === 'LineString') {
358
+ return polygonContainsLine(b.coordinates, a.coordinates)
359
+ }
360
+ if (ta === 'Polygon' && tb === 'Polygon') {
361
+ return polygonContainsPolygon(a.coordinates, b.coordinates)
362
+ }
363
+ if (ta === 'LineString' && tb === 'LineString') {
364
+ // Line A contains line B if every point of B is on A
365
+ for (const pt of b.coordinates) {
366
+ if (!pointOnLine(pt, a.coordinates)) return 'OUTSIDE'
367
+ }
368
+ return 'BOUNDARY'
369
+ }
370
+ return 'OUTSIDE'
371
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * @import { Relation } from './geometry.js'
3
+ */
4
+
5
+ import { EPSILON_SQ, distSq } from './primitives.js'
6
+
7
+ /**
8
+ * Classify a point relative to a ring: 'OUTSIDE', 'BOUNDARY', or 'INSIDE'.
9
+ * Combines ray casting with boundary distance check in a single pass.
10
+ * ring is an array of [x, y] coords (closed ring, first = last).
11
+ *
12
+ * @param {number[]} point
13
+ * @param {number[][]} ring
14
+ * @returns {Relation}
15
+ */
16
+ function pointInRing(point, ring) {
17
+ const [px, py] = point
18
+ let inside = false
19
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
20
+ if (pointToSegmentDistSq(point, ring[j], ring[i]) < EPSILON_SQ) {
21
+ return 'BOUNDARY'
22
+ }
23
+ const [xi, yi] = ring[i]
24
+ const [xj, yj] = ring[j]
25
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi) {
26
+ inside = !inside
27
+ }
28
+ }
29
+ return inside ? 'INSIDE' : 'OUTSIDE'
30
+ }
31
+
32
+ /**
33
+ * Classify a point relative to a polygon: 'OUTSIDE', 'BOUNDARY', or 'INSIDE'.
34
+ * First ring is exterior, rest are holes.
35
+ *
36
+ * @param {number[]} point
37
+ * @param {number[][][]} rings
38
+ * @returns {Relation}
39
+ */
40
+ export function pointInPolygon(point, rings) {
41
+ const rel = pointInRing(point, rings[0])
42
+ if (rel === 'OUTSIDE') return 'OUTSIDE'
43
+ if (rel === 'BOUNDARY') return 'BOUNDARY'
44
+ for (let i = 1; i < rings.length; i++) {
45
+ const holeRel = pointInRing(point, rings[i])
46
+ if (holeRel === 'INSIDE') return 'OUTSIDE'
47
+ if (holeRel === 'BOUNDARY') return 'BOUNDARY'
48
+ }
49
+ return 'INSIDE'
50
+ }
51
+
52
+ /**
53
+ * Test if point is on a linestring.
54
+ *
55
+ * @param {number[]} point
56
+ * @param {number[][]} line
57
+ * @returns {boolean}
58
+ */
59
+ export function pointOnLine(point, line) {
60
+ for (let i = 0; i < line.length - 1; i++) {
61
+ if (pointToSegmentDistSq(point, line[i], line[i + 1]) < EPSILON_SQ) return true
62
+ }
63
+ return false
64
+ }
65
+
66
+ /**
67
+ * Classify a point relative to a linestring.
68
+ *
69
+ * @param {number[]} point
70
+ * @param {number[][]} line
71
+ * @returns {Relation}
72
+ */
73
+ export function pointLineRelation(point, line) {
74
+ // Check endpoints first
75
+ if (distSq(point, line[0]) < EPSILON_SQ) return 'BOUNDARY'
76
+ if (distSq(point, line[line.length - 1]) < EPSILON_SQ) return 'BOUNDARY'
77
+ // Check if on any segment
78
+ for (let i = 0; i < line.length - 1; i++) {
79
+ if (pointToSegmentDistSq(point, line[i], line[i + 1]) < EPSILON_SQ) return 'INSIDE'
80
+ }
81
+ return 'OUTSIDE'
82
+ }
83
+
84
+ /**
85
+ * Squared minimum distance from point p to line segment [a, b].
86
+ *
87
+ * @param {number[]} p
88
+ * @param {number[]} a
89
+ * @param {number[]} b
90
+ * @returns {number}
91
+ */
92
+ export function pointToSegmentDistSq(p, a, b) {
93
+ const dx = b[0] - a[0]
94
+ const dy = b[1] - a[1]
95
+ const lenSq = dx * dx + dy * dy
96
+ if (lenSq === 0) return distSq(p, a)
97
+ let t = ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / lenSq
98
+ t = Math.max(0, Math.min(1, t))
99
+ const ddx = p[0] - a[0] - t * dx
100
+ const ddy = p[1] - a[1] - t * dy
101
+ return ddx * ddx + ddy * ddy
102
+ }
@@ -0,0 +1,27 @@
1
+ export const EPSILON = 1e-10
2
+ export const EPSILON_SQ = EPSILON * EPSILON
3
+
4
+ /**
5
+ * Compute the squared distance between two 2D points.
6
+ *
7
+ * @param {number[]} a
8
+ * @param {number[]} b
9
+ * @returns {number}
10
+ */
11
+ export function distSq(a, b) {
12
+ const dx = a[0] - b[0]
13
+ const dy = a[1] - b[1]
14
+ return dx * dx + dy * dy
15
+ }
16
+
17
+ /**
18
+ * Cross product of vectors (b-a) and (c-a).
19
+ *
20
+ * @param {number[]} a
21
+ * @param {number[]} b
22
+ * @param {number[]} c
23
+ * @returns {number}
24
+ */
25
+ export function cross(a, b, c) {
26
+ return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
27
+ }
@@ -0,0 +1,139 @@
1
+ import { EPSILON, EPSILON_SQ, cross, distSq } from './primitives.js'
2
+
3
+ /**
4
+ * Test whether two line segments [p1,p2] and [p3,p4] intersect.
5
+ * Returns true if they share any point (including endpoints).
6
+ *
7
+ * @param {number[]} p1
8
+ * @param {number[]} p2
9
+ * @param {number[]} p3
10
+ * @param {number[]} p4
11
+ * @returns {boolean}
12
+ */
13
+ export function segmentsIntersect(p1, p2, p3, p4) {
14
+ const d1 = cross(p3, p4, p1)
15
+ const d2 = cross(p3, p4, p2)
16
+ const d3 = cross(p1, p2, p3)
17
+ const d4 = cross(p1, p2, p4)
18
+
19
+ if ((d1 > 0 && d2 < 0 || d1 < 0 && d2 > 0) &&
20
+ (d3 > 0 && d4 < 0 || d3 < 0 && d4 > 0)) {
21
+ return true
22
+ }
23
+
24
+ if (Math.abs(d1) < EPSILON && onSegment(p3, p4, p1)) return true
25
+ if (Math.abs(d2) < EPSILON && onSegment(p3, p4, p2)) return true
26
+ if (Math.abs(d3) < EPSILON && onSegment(p1, p2, p3)) return true
27
+ if (Math.abs(d4) < EPSILON && onSegment(p1, p2, p4)) return true
28
+
29
+ return false
30
+ }
31
+
32
+ /**
33
+ * Test if a line segment intersects a ring boundary.
34
+ *
35
+ * @param {number[]} a
36
+ * @param {number[]} b
37
+ * @param {number[][]} ring
38
+ * @returns {boolean}
39
+ */
40
+ export function segmentIntersectsRing(a, b, ring) {
41
+ for (let i = 0; i < ring.length - 1; i++) {
42
+ if (segmentsIntersect(a, b, ring[i], ring[i + 1])) return true
43
+ }
44
+ return false
45
+ }
46
+
47
+ /**
48
+ * Compute intersection point of two segments (if they intersect at a single point).
49
+ *
50
+ * @param {number[]} p1
51
+ * @param {number[]} p2
52
+ * @param {number[]} p3
53
+ * @param {number[]} p4
54
+ * @returns {number[] | null}
55
+ */
56
+ export function segmentIntersectionPoint(p1, p2, p3, p4) {
57
+ const d1x = p2[0] - p1[0], d1y = p2[1] - p1[1]
58
+ const d2x = p4[0] - p3[0], d2y = p4[1] - p3[1]
59
+ const denom = d1x * d2y - d1y * d2x
60
+ if (Math.abs(denom) < EPSILON) return null // parallel
61
+ const t = ((p3[0] - p1[0]) * d2y - (p3[1] - p1[1]) * d2x) / denom
62
+ return [p1[0] + t * d1x, p1[1] + t * d1y]
63
+ }
64
+
65
+ /**
66
+ * Check if point p lies on segment [a, b].
67
+ *
68
+ * @param {number[]} a
69
+ * @param {number[]} b
70
+ * @param {number[]} p
71
+ * @returns {boolean}
72
+ */
73
+ export function pointOnSegment(a, b, p) {
74
+ if (Math.abs(cross(a, b, p)) > EPSILON) return false
75
+ return p[0] >= Math.min(a[0], b[0]) - EPSILON &&
76
+ p[0] <= Math.max(a[0], b[0]) + EPSILON &&
77
+ p[1] >= Math.min(a[1], b[1]) - EPSILON &&
78
+ p[1] <= Math.max(a[1], b[1]) + EPSILON
79
+ }
80
+
81
+ /**
82
+ * Returns the single endpoint touch point for two segments, 'INSIDE' when
83
+ * they intersect at a non-endpoint/proper crossing or overlap by length,
84
+ * and 'OUTSIDE' when they do not intersect.
85
+ *
86
+ * @param {number[]} a1
87
+ * @param {number[]} a2
88
+ * @param {number[]} b1
89
+ * @param {number[]} b2
90
+ * @returns {'INSIDE' | 'OUTSIDE' | number[]}
91
+ */
92
+ export function segmentTouchPoint(a1, a2, b1, b2) {
93
+ const d1 = cross(b1, b2, a1)
94
+ const d2 = cross(b1, b2, a2)
95
+ const d3 = cross(a1, a2, b1)
96
+ const d4 = cross(a1, a2, b2)
97
+
98
+ if ((d1 > 0 && d2 < 0 || d1 < 0 && d2 > 0) &&
99
+ (d3 > 0 && d4 < 0 || d3 < 0 && d4 > 0)) {
100
+ return 'INSIDE'
101
+ }
102
+
103
+ /** @type {number[] | undefined} */
104
+ let point
105
+ let hasSecondPoint = false
106
+
107
+ /**
108
+ * @param {number[]} candidate
109
+ */
110
+ function addPoint(candidate) {
111
+ if (!point) {
112
+ point = candidate
113
+ return
114
+ }
115
+ if (distSq(point, candidate) >= EPSILON_SQ) hasSecondPoint = true
116
+ }
117
+
118
+ if (Math.abs(d1) < EPSILON && onSegment(b1, b2, a1)) addPoint(a1)
119
+ if (Math.abs(d2) < EPSILON && onSegment(b1, b2, a2)) addPoint(a2)
120
+ if (Math.abs(d3) < EPSILON && onSegment(a1, a2, b1)) addPoint(b1)
121
+ if (Math.abs(d4) < EPSILON && onSegment(a1, a2, b2)) addPoint(b2)
122
+
123
+ if (!point) return 'OUTSIDE'
124
+
125
+ return hasSecondPoint ? 'INSIDE' : point
126
+ }
127
+
128
+ /**
129
+ * Check if point c lies on segment [a, b], assuming collinearity.
130
+ *
131
+ * @param {number[]} a
132
+ * @param {number[]} b
133
+ * @param {number[]} c
134
+ * @returns {boolean}
135
+ */
136
+ function onSegment(a, b, c) {
137
+ return Math.min(a[0], b[0]) - c[0] <= EPSILON && c[0] - Math.max(a[0], b[0]) <= EPSILON &&
138
+ Math.min(a[1], b[1]) - c[1] <= EPSILON && c[1] - Math.max(a[1], b[1]) <= EPSILON
139
+ }