squirreling 0.9.4 → 0.10.0

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.
@@ -1,205 +1,11 @@
1
- /**
2
- * @import { Relation, SimpleGeometry } from './geometry.js'
3
- */
4
-
5
- export const EPSILON = 1e-10
6
- export const EPSILON_SQ = EPSILON * EPSILON
7
-
8
- /**
9
- * @typedef {{ minX: number, minY: number, maxX: number, maxY: number }} BBox
10
- */
11
-
12
- /** @type {WeakMap<SimpleGeometry, BBox>} */
13
- const bboxCache = new WeakMap()
14
-
15
- /**
16
- * Compute the axis-aligned bounding box of a simple geometry.
17
- * Results are cached per geometry object.
18
- *
19
- * @param {SimpleGeometry} geom
20
- * @returns {BBox}
21
- */
22
- export function bbox(geom) {
23
- let b = bboxCache.get(geom)
24
- if (b) return b
25
- if (geom.type === 'Point') {
26
- const [x, y] = geom.coordinates
27
- b = { minX: x, minY: y, maxX: x, maxY: y }
28
- } else {
29
- /** @type {number[][]} */
30
- const points = geom.type === 'LineString'
31
- ? geom.coordinates
32
- : geom.coordinates[0] // outer ring
33
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
34
- for (const p of points) {
35
- if (p[0] < minX) minX = p[0]
36
- if (p[1] < minY) minY = p[1]
37
- if (p[0] > maxX) maxX = p[0]
38
- if (p[1] > maxY) maxY = p[1]
39
- }
40
- b = { minX, minY, maxX, maxY }
41
- }
42
- bboxCache.set(geom, b)
43
- return b
44
- }
45
-
46
- /**
47
- * Test whether two bounding boxes overlap.
48
- *
49
- * @param {BBox} a
50
- * @param {BBox} b
51
- * @returns {boolean}
52
- */
53
- export function bboxOverlap(a, b) {
54
- return a.minX <= b.maxX && a.maxX >= b.minX && a.minY <= b.maxY && a.maxY >= b.minY
55
- }
56
-
57
- /**
58
- * Compute the squared distance between two 2D points.
59
- *
60
- * @param {number[]} a
61
- * @param {number[]} b
62
- * @returns {number}
63
- */
64
- export function distSq(a, b) {
65
- const dx = a[0] - b[0]
66
- const dy = a[1] - b[1]
67
- return dx * dx + dy * dy
68
- }
69
-
70
- /**
71
- * Squared minimum distance from point p to line segment [a, b].
72
- *
73
- * @param {number[]} p
74
- * @param {number[]} a
75
- * @param {number[]} b
76
- * @returns {number}
77
- */
78
- export function pointToSegmentDistSq(p, a, b) {
79
- const dx = b[0] - a[0]
80
- const dy = b[1] - a[1]
81
- const lenSq = dx * dx + dy * dy
82
- if (lenSq === 0) return distSq(p, a)
83
- let t = ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / lenSq
84
- t = Math.max(0, Math.min(1, t))
85
- const ddx = p[0] - a[0] - t * dx
86
- const ddy = p[1] - a[1] - t * dy
87
- return ddx * ddx + ddy * ddy
88
- }
89
-
90
- /**
91
- * Test whether two line segments [p1,p2] and [p3,p4] intersect.
92
- * Returns true if they share any point (including endpoints).
93
- *
94
- * @param {number[]} p1
95
- * @param {number[]} p2
96
- * @param {number[]} p3
97
- * @param {number[]} p4
98
- * @returns {boolean}
99
- */
100
- function segmentsIntersect(p1, p2, p3, p4) {
101
- const d1 = cross(p3, p4, p1)
102
- const d2 = cross(p3, p4, p2)
103
- const d3 = cross(p1, p2, p3)
104
- const d4 = cross(p1, p2, p4)
105
-
106
- if ((d1 > 0 && d2 < 0 || d1 < 0 && d2 > 0) &&
107
- (d3 > 0 && d4 < 0 || d3 < 0 && d4 > 0)) {
108
- return true
109
- }
110
-
111
- if (Math.abs(d1) < EPSILON && onSegment(p3, p4, p1)) return true
112
- if (Math.abs(d2) < EPSILON && onSegment(p3, p4, p2)) return true
113
- if (Math.abs(d3) < EPSILON && onSegment(p1, p2, p3)) return true
114
- if (Math.abs(d4) < EPSILON && onSegment(p1, p2, p4)) return true
115
-
116
- return false
117
- }
118
-
119
- /**
120
- * Cross product of vectors (b-a) and (c-a).
121
- *
122
- * @param {number[]} a
123
- * @param {number[]} b
124
- * @param {number[]} c
125
- * @returns {number}
126
- */
127
- function cross(a, b, c) {
128
- return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
129
- }
130
-
131
- /**
132
- * Check if point c lies on segment [a, b], assuming collinearity.
133
- *
134
- * @param {number[]} a
135
- * @param {number[]} b
136
- * @param {number[]} c
137
- * @returns {boolean}
138
- */
139
- function onSegment(a, b, c) {
140
- return Math.min(a[0], b[0]) - c[0] <= EPSILON && c[0] - Math.max(a[0], b[0]) <= EPSILON &&
141
- Math.min(a[1], b[1]) - c[1] <= EPSILON && c[1] - Math.max(a[1], b[1]) <= EPSILON
142
- }
143
-
144
- /**
145
- * Classify a point relative to a ring: 'OUTSIDE', 'BOUNDARY', or 'INSIDE'.
146
- * Combines ray casting with boundary distance check in a single pass.
147
- * ring is an array of [x, y] coords (closed ring, first = last).
148
- *
149
- * @param {number[]} point
150
- * @param {number[][]} ring
151
- * @returns {Relation}
152
- */
153
- function pointInRing(point, ring) {
154
- const [px, py] = point
155
- let inside = false
156
- for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
157
- if (pointToSegmentDistSq(point, ring[j], ring[i]) < EPSILON_SQ) {
158
- return 'BOUNDARY'
159
- }
160
- const [xi, yi] = ring[i]
161
- const [xj, yj] = ring[j]
162
- if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi) {
163
- inside = !inside
164
- }
165
- }
166
- return inside ? 'INSIDE' : 'OUTSIDE'
167
- }
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'
168
5
 
169
6
  /**
170
- * Classify a point relative to a polygon: 'OUTSIDE', 'BOUNDARY', or 'INSIDE'.
171
- * First ring is exterior, rest are holes.
172
- *
173
- * @param {number[]} point
174
- * @param {number[][][]} rings
175
- * @returns {Relation}
176
- */
177
- export function pointInPolygon(point, rings) {
178
- const rel = pointInRing(point, rings[0])
179
- if (rel === 'OUTSIDE') return 'OUTSIDE'
180
- if (rel === 'BOUNDARY') return 'BOUNDARY'
181
- for (let i = 1; i < rings.length; i++) {
182
- const holeRel = pointInRing(point, rings[i])
183
- if (holeRel === 'INSIDE') return 'OUTSIDE'
184
- if (holeRel === 'BOUNDARY') return 'BOUNDARY'
185
- }
186
- return 'INSIDE'
187
- }
188
-
189
- /**
190
- * Test if a line segment intersects a ring boundary.
191
- *
192
- * @param {number[]} a
193
- * @param {number[]} b
194
- * @param {number[][]} ring
195
- * @returns {boolean}
7
+ * @import { Relation, SimpleGeometry } from './geometry.js'
196
8
  */
197
- function segmentIntersectsRing(a, b, ring) {
198
- for (let i = 0; i < ring.length - 1; i++) {
199
- if (segmentsIntersect(a, b, ring[i], ring[i + 1])) return true
200
- }
201
- return false
202
- }
203
9
 
204
10
  /**
205
11
  * Test if a linestring intersects a polygon.
@@ -209,11 +15,11 @@ function segmentIntersectsRing(a, b, ring) {
209
15
  * @returns {boolean}
210
16
  */
211
17
  function lineIntersectsPolygon(line, rings) {
212
- // Check if any point of the line is inside the polygon
213
- for (const pt of line) {
214
- if (pointInPolygon(pt, rings) !== 'OUTSIDE') return true
215
- }
216
- // Check if any segment of the line intersects any ring edge
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.
217
23
  for (let i = 0; i < line.length - 1; i++) {
218
24
  for (const ring of rings) {
219
25
  if (segmentIntersectsRing(line[i], line[i + 1], ring)) return true
@@ -238,6 +44,15 @@ function linesIntersect(line1, line2) {
238
44
  return false
239
45
  }
240
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
+
241
56
  /**
242
57
  * Classify containment of a linestring within a polygon.
243
58
  * Returns 'INSIDE' if entirely in interior, 'BOUNDARY' if inside but
@@ -290,56 +105,6 @@ function polygonContainsPolygon(ringsA, ringsB) {
290
105
  return result
291
106
  }
292
107
 
293
- /**
294
- * Test if point is on a linestring.
295
- *
296
- * @param {number[]} point
297
- * @param {number[][]} line
298
- * @returns {boolean}
299
- */
300
- function pointOnLine(point, line) {
301
- for (let i = 0; i < line.length - 1; i++) {
302
- if (pointToSegmentDistSq(point, line[i], line[i + 1]) < EPSILON_SQ) return true
303
- }
304
- return false
305
- }
306
-
307
- /**
308
- * Compute intersection point of two segments (if they intersect at a single point).
309
- *
310
- * @param {number[]} p1
311
- * @param {number[]} p2
312
- * @param {number[]} p3
313
- * @param {number[]} p4
314
- * @returns {number[] | null}
315
- */
316
- function segmentIntersectionPoint(p1, p2, p3, p4) {
317
- const d1x = p2[0] - p1[0], d1y = p2[1] - p1[1]
318
- const d2x = p4[0] - p3[0], d2y = p4[1] - p3[1]
319
- const denom = d1x * d2y - d1y * d2x
320
- if (Math.abs(denom) < EPSILON) return null // parallel
321
- const t = ((p3[0] - p1[0]) * d2y - (p3[1] - p1[1]) * d2x) / denom
322
- return [p1[0] + t * d1x, p1[1] + t * d1y]
323
- }
324
-
325
- /**
326
- * Classify a point relative to a linestring.
327
- *
328
- * @param {number[]} point
329
- * @param {number[][]} line
330
- * @returns {Relation}
331
- */
332
- export function pointLineRelation(point, line) {
333
- // Check endpoints first
334
- if (distSq(point, line[0]) < EPSILON_SQ) return 'BOUNDARY'
335
- if (distSq(point, line[line.length - 1]) < EPSILON_SQ) return 'BOUNDARY'
336
- // Check if on any segment
337
- for (let i = 0; i < line.length - 1; i++) {
338
- if (pointToSegmentDistSq(point, line[i], line[i + 1]) < EPSILON_SQ) return 'INSIDE'
339
- }
340
- return 'OUTSIDE'
341
- }
342
-
343
108
  /**
344
109
  * Classify the relationship between two linestrings.
345
110
  * Returns INSIDE if interiors share a point, BOUNDARY if they only meet
@@ -353,24 +118,10 @@ function lineLineRelation(line1, line2) {
353
118
  let boundary = false
354
119
  for (let i = 0; i < line1.length - 1; i++) {
355
120
  for (let j = 0; j < line2.length - 1; j++) {
356
- if (!segmentsIntersect(line1[i], line1[i + 1], line2[j], line2[j + 1])) continue
357
- // Segments intersect, check if the intersection is interior to both lines
358
- // Check segment midpoints
359
- const mid1 = [(line1[i][0] + line1[i + 1][0]) / 2, (line1[i][1] + line1[i + 1][1]) / 2]
360
- if (pointLineRelation(mid1, line1) === 'INSIDE' && pointLineRelation(mid1, line2) === 'INSIDE') {
361
- return 'INSIDE'
362
- }
363
- const mid2 = [(line2[j][0] + line2[j + 1][0]) / 2, (line2[j][1] + line2[j + 1][1]) / 2]
364
- if (pointLineRelation(mid2, line1) === 'INSIDE' && pointLineRelation(mid2, line2) === 'INSIDE') {
365
- return 'INSIDE'
366
- }
367
- // Check actual intersection point
368
- const ip = segmentIntersectionPoint(line1[i], line1[i + 1], line2[j], line2[j + 1])
369
- if (ip) {
370
- if (pointLineRelation(ip, line1) === 'INSIDE' && pointLineRelation(ip, line2) === 'INSIDE') {
371
- return 'INSIDE'
372
- }
373
- }
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'
374
125
  boundary = true
375
126
  }
376
127
  }
@@ -475,10 +226,10 @@ function polygonPolygonRelation(rings1, rings2) {
475
226
  * @param {SimpleGeometry[]} partsB
476
227
  * @returns {boolean}
477
228
  */
478
- export function simpleIntersects(partsA, partsB) {
229
+ export function intersects(partsA, partsB) {
479
230
  for (const pa of partsA) {
480
231
  for (const pb of partsB) {
481
- if (simplePairIntersects(pa, pb)) return true
232
+ if (pairIntersects(pa, pb)) return true
482
233
  }
483
234
  }
484
235
  return false
@@ -489,8 +240,8 @@ export function simpleIntersects(partsA, partsB) {
489
240
  * @param {SimpleGeometry} b
490
241
  * @returns {boolean}
491
242
  */
492
- function simplePairIntersects(a, b) {
493
- if (!bboxOverlap(bbox(a), bbox(b))) return false
243
+ function pairIntersects(a, b) {
244
+ if (!bboxOverlap(a, b)) return false
494
245
  const ta = a.type
495
246
  const tb = b.type
496
247
 
@@ -533,8 +284,8 @@ function simplePairIntersects(a, b) {
533
284
  * @param {SimpleGeometry} b
534
285
  * @returns {Relation}
535
286
  */
536
- export function simplePairRelation(a, b) {
537
- if (!bboxOverlap(bbox(a), bbox(b))) return 'OUTSIDE'
287
+ export function pairRelation(a, b) {
288
+ if (!bboxOverlap(a, b)) return 'OUTSIDE'
538
289
  const ta = a.type
539
290
  const tb = b.type
540
291
 
@@ -589,8 +340,8 @@ export function simplePairRelation(a, b) {
589
340
  * @param {SimpleGeometry} b
590
341
  * @returns {Relation}
591
342
  */
592
- export function simplePairContainment(a, b) {
593
- if (!bboxOverlap(bbox(a), bbox(b))) return 'OUTSIDE'
343
+ export function pairContainment(a, b) {
344
+ if (!bboxOverlap(a, b)) return 'OUTSIDE'
594
345
  const ta = a.type
595
346
  const tb = b.type
596
347
 
@@ -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
+ }