pcb-scene3d-viewer 1.1.47 → 1.1.49

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,5 +1,7 @@
1
1
  import earcut from 'earcut'
2
2
  import { PcbAssemblyFillGeometryResolver } from './PcbAssemblyFillGeometryResolver.mjs'
3
+ import { PcbScene3dCopperFillAreaClipper } from './PcbScene3dCopperFillAreaClipper.mjs'
4
+ import { PcbScene3dCopperFillPolygonBoolean } from './PcbScene3dCopperFillPolygonBoolean.mjs'
3
5
  import { PcbScene3dCopperOcclusionClipper } from './PcbScene3dCopperOcclusionClipper.mjs'
4
6
 
5
7
  /**
@@ -18,6 +20,7 @@ export class PcbScene3dCopperFillMeshBuilder {
18
20
  * @param {boolean} mirrorY Whether to mirror underside Y coordinates.
19
21
  * @param {any} material Copper material.
20
22
  * @param {{ x: number, y: number }[][]} [cutouts] Optional normalized overlay cutouts.
23
+ * @param {{ surfaceOnly?: boolean, clipContainedFillOverlaps?: boolean }} [options] Optional mesh options.
21
24
  * @returns {any | null}
22
25
  */
23
26
  static build(
@@ -28,22 +31,41 @@ export class PcbScene3dCopperFillMeshBuilder {
28
31
  normalizeBoardPoint,
29
32
  mirrorY,
30
33
  material,
31
- cutouts = []
34
+ cutouts = [],
35
+ options = {}
32
36
  ) {
33
37
  const positions = []
34
38
  const halfThickness = Math.max(Number(thickness || 0), 0.001) / 2
35
39
  const bottomZ = Number(z || 0) - halfThickness
36
40
  const topZ = Number(z || 0) + halfThickness
37
41
 
38
- for (const fill of fills || []) {
39
- PcbScene3dCopperFillMeshBuilder.#appendFillPositions(
42
+ if (
43
+ PcbScene3dCopperFillMeshBuilder.#shouldClipContainedFillOverlaps(
44
+ options
45
+ )
46
+ ) {
47
+ PcbScene3dCopperFillMeshBuilder.#appendClippedFillPositions(
48
+ THREE,
40
49
  positions,
41
- fill,
50
+ fills,
42
51
  bottomZ,
43
52
  topZ,
44
53
  normalizeBoardPoint,
45
- mirrorY
54
+ mirrorY,
55
+ options
46
56
  )
57
+ } else {
58
+ for (const fill of fills || []) {
59
+ PcbScene3dCopperFillMeshBuilder.#appendFillPositions(
60
+ positions,
61
+ fill,
62
+ bottomZ,
63
+ topZ,
64
+ normalizeBoardPoint,
65
+ mirrorY,
66
+ options
67
+ )
68
+ }
47
69
  }
48
70
 
49
71
  if (!positions.length) {
@@ -93,6 +115,7 @@ export class PcbScene3dCopperFillMeshBuilder {
93
115
  * @param {number} topZ Upper Z.
94
116
  * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
95
117
  * @param {boolean} mirrorY Whether to mirror underside Y coordinates.
118
+ * @param {{ surfaceOnly?: boolean }} options Mesh options.
96
119
  * @returns {void}
97
120
  */
98
121
  static #appendFillPositions(
@@ -101,7 +124,8 @@ export class PcbScene3dCopperFillMeshBuilder {
101
124
  bottomZ,
102
125
  topZ,
103
126
  normalizeBoardPoint,
104
- mirrorY
127
+ mirrorY,
128
+ options
105
129
  ) {
106
130
  for (const loops of PcbAssemblyFillGeometryResolver.resolveAll(fill)) {
107
131
  PcbScene3dCopperFillMeshBuilder.#appendLoopSetPositions(
@@ -110,11 +134,216 @@ export class PcbScene3dCopperFillMeshBuilder {
110
134
  bottomZ,
111
135
  topZ,
112
136
  normalizeBoardPoint,
113
- mirrorY
137
+ mirrorY,
138
+ options
114
139
  )
115
140
  }
116
141
  }
117
142
 
143
+ /**
144
+ * Appends fill positions while removing duplicate overlap surfaces.
145
+ * @param {any} THREE Three.js namespace.
146
+ * @param {number[]} positions Position buffer.
147
+ * @param {object[]} fills Filled copper primitives.
148
+ * @param {number} bottomZ Lower Z.
149
+ * @param {number} topZ Upper Z.
150
+ * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
151
+ * @param {boolean} mirrorY Whether to mirror underside Y coordinates.
152
+ * @param {{ surfaceOnly?: boolean, clipContainedFillOverlaps?: boolean }} options Mesh options.
153
+ * @returns {void}
154
+ */
155
+ static #appendClippedFillPositions(
156
+ THREE,
157
+ positions,
158
+ fills,
159
+ bottomZ,
160
+ topZ,
161
+ normalizeBoardPoint,
162
+ mirrorY,
163
+ options
164
+ ) {
165
+ const loopSets = PcbScene3dCopperFillMeshBuilder.#resolveLoopSets(
166
+ fills,
167
+ normalizeBoardPoint,
168
+ mirrorY
169
+ )
170
+ const emittedPolygons = []
171
+ const emittedLoopSets = []
172
+
173
+ for (const loopSet of loopSets) {
174
+ const remainingLoopSets =
175
+ PcbScene3dCopperFillPolygonBoolean.resolveRemainingLoopSets(
176
+ loopSet,
177
+ emittedPolygons
178
+ )
179
+
180
+ if (remainingLoopSets) {
181
+ for (const remainingLoopSet of remainingLoopSets) {
182
+ PcbScene3dCopperFillMeshBuilder.#appendNormalizedLoopSetPositions(
183
+ positions,
184
+ remainingLoopSet,
185
+ bottomZ,
186
+ topZ,
187
+ options
188
+ )
189
+ }
190
+ } else {
191
+ PcbScene3dCopperFillMeshBuilder.#appendTriangleClippedLoopSet(
192
+ THREE,
193
+ positions,
194
+ loopSet,
195
+ emittedLoopSets,
196
+ bottomZ,
197
+ topZ,
198
+ options
199
+ )
200
+ }
201
+
202
+ emittedLoopSets.push(loopSet)
203
+ emittedPolygons.push(
204
+ ...PcbScene3dCopperFillPolygonBoolean.resolveNormalizedPolygons(
205
+ loopSet
206
+ )
207
+ )
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Appends one loop set using the older triangle clipper fallback.
213
+ * @param {any} THREE Three.js namespace.
214
+ * @param {number[]} positions Position buffer.
215
+ * @param {{ outer: number[][], holes: number[][][], bounds: object }} loopSet Candidate loop set.
216
+ * @param {{ outer: number[][], holes: number[][][], bounds: object }[]} emittedLoopSets Already emitted loop sets.
217
+ * @param {number} bottomZ Lower Z.
218
+ * @param {number} topZ Upper Z.
219
+ * @param {{ surfaceOnly?: boolean, clipContainedFillOverlaps?: boolean }} options Mesh options.
220
+ * @returns {void}
221
+ */
222
+ static #appendTriangleClippedLoopSet(
223
+ THREE,
224
+ positions,
225
+ loopSet,
226
+ emittedLoopSets,
227
+ bottomZ,
228
+ topZ,
229
+ options
230
+ ) {
231
+ if (
232
+ emittedLoopSets.some((emittedLoopSet) =>
233
+ PcbScene3dCopperFillMeshBuilder.#containsLoopSet(
234
+ emittedLoopSet,
235
+ loopSet
236
+ )
237
+ )
238
+ ) {
239
+ return
240
+ }
241
+
242
+ const loopPositions = []
243
+ PcbScene3dCopperFillMeshBuilder.#appendNormalizedLoopSetPositions(
244
+ loopPositions,
245
+ loopSet,
246
+ bottomZ,
247
+ topZ,
248
+ options
249
+ )
250
+ PcbScene3dCopperFillMeshBuilder.#appendPositions(
251
+ positions,
252
+ PcbScene3dCopperFillMeshBuilder.#clipPositionsAgainstLoopSets(
253
+ THREE,
254
+ loopPositions,
255
+ PcbScene3dCopperFillMeshBuilder.#resolveClipLoopSets(
256
+ loopSet,
257
+ emittedLoopSets
258
+ )
259
+ )
260
+ )
261
+ }
262
+
263
+ /**
264
+ * Resolves earlier loop sets that can actually overlap a candidate.
265
+ * @param {{ outer: number[][], holes: number[][][], bounds: object }} candidate Candidate loop set.
266
+ * @param {{ outer: number[][], holes: number[][][], bounds: object }[]} loopSets Emitted loop sets.
267
+ * @returns {{ outer: number[][], holes: number[][][], bounds: object }[]}
268
+ */
269
+ static #resolveClipLoopSets(candidate, loopSets) {
270
+ return loopSets.filter(
271
+ (loopSet) =>
272
+ PcbScene3dCopperFillMeshBuilder.#boundsOverlap(
273
+ candidate.bounds,
274
+ loopSet.bounds
275
+ ) &&
276
+ !PcbScene3dCopperFillMeshBuilder.#isInsideAnyHole(
277
+ candidate,
278
+ loopSet
279
+ )
280
+ )
281
+ }
282
+
283
+ /**
284
+ * Appends a potentially large position array without using argument spread.
285
+ * @param {number[]} target Target position buffer.
286
+ * @param {number[]} source Source positions.
287
+ * @returns {void}
288
+ */
289
+ static #appendPositions(target, source) {
290
+ for (const value of source) {
291
+ target.push(value)
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Clips position triangles against already-emitted fill loop sets.
297
+ * @param {any} THREE Three.js namespace.
298
+ * @param {number[]} positions Candidate triangle positions.
299
+ * @param {{ outer: number[][], holes: number[][][] }[]} loopSets Filled areas already represented.
300
+ * @returns {number[]}
301
+ */
302
+ static #clipPositionsAgainstLoopSets(THREE, positions, loopSets) {
303
+ if (!positions.length || !loopSets.length) {
304
+ return positions
305
+ }
306
+
307
+ const geometry = new THREE.BufferGeometry()
308
+ geometry.setAttribute(
309
+ 'position',
310
+ new THREE.Float32BufferAttribute(positions, 3)
311
+ )
312
+ const mesh = new THREE.Mesh(geometry)
313
+ const clippedMesh = PcbScene3dCopperFillAreaClipper.filter(
314
+ THREE,
315
+ mesh,
316
+ PcbScene3dCopperFillMeshBuilder.#loopSetsToFills(loopSets),
317
+ (x, y) => ({ x, y }),
318
+ false
319
+ )
320
+ if (!clippedMesh) {
321
+ return []
322
+ }
323
+
324
+ return Array.from(clippedMesh.geometry.attributes.position.array)
325
+ }
326
+
327
+ /**
328
+ * Converts normalized loop sets back to fill primitives for shared clipping.
329
+ * @param {{ outer: number[][], holes: number[][][] }[]} loopSets Loop sets.
330
+ * @returns {object[]}
331
+ */
332
+ static #loopSetsToFills(loopSets) {
333
+ return loopSets.map((loopSet) => ({
334
+ points: loopSet.outer.map((point) => ({
335
+ x: point[0],
336
+ y: point[1]
337
+ })),
338
+ holes: (loopSet.holes || []).map((hole) =>
339
+ hole.map((point) => ({
340
+ x: point[0],
341
+ y: point[1]
342
+ }))
343
+ )
344
+ }))
345
+ }
346
+
118
347
  /**
119
348
  * Appends one fill island loop set to a shared position buffer.
120
349
  * @param {number[]} positions Position buffer.
@@ -123,6 +352,7 @@ export class PcbScene3dCopperFillMeshBuilder {
123
352
  * @param {number} topZ Upper Z.
124
353
  * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
125
354
  * @param {boolean} mirrorY Whether to mirror underside Y coordinates.
355
+ * @param {{ surfaceOnly?: boolean }} options Mesh options.
126
356
  * @returns {void}
127
357
  */
128
358
  static #appendLoopSetPositions(
@@ -131,7 +361,8 @@ export class PcbScene3dCopperFillMeshBuilder {
131
361
  bottomZ,
132
362
  topZ,
133
363
  normalizeBoardPoint,
134
- mirrorY
364
+ mirrorY,
365
+ options
135
366
  ) {
136
367
  const outer = PcbScene3dCopperFillMeshBuilder.#normalizeLoop(
137
368
  loops.outer,
@@ -154,6 +385,33 @@ export class PcbScene3dCopperFillMeshBuilder {
154
385
  return
155
386
  }
156
387
 
388
+ PcbScene3dCopperFillMeshBuilder.#appendNormalizedLoopSetPositions(
389
+ positions,
390
+ { outer, holes },
391
+ bottomZ,
392
+ topZ,
393
+ options
394
+ )
395
+ }
396
+
397
+ /**
398
+ * Appends one already-normalized fill island loop set.
399
+ * @param {number[]} positions Position buffer.
400
+ * @param {{ outer: number[][], holes: number[][][] }} loops Fill loop set.
401
+ * @param {number} bottomZ Lower Z.
402
+ * @param {number} topZ Upper Z.
403
+ * @param {{ surfaceOnly?: boolean, clipContainedFillOverlaps?: boolean }} options Mesh options.
404
+ * @returns {void}
405
+ */
406
+ static #appendNormalizedLoopSetPositions(
407
+ positions,
408
+ loops,
409
+ bottomZ,
410
+ topZ,
411
+ options
412
+ ) {
413
+ const outer = loops.outer
414
+ const holes = loops.holes || []
157
415
  const { points, flat, holeIndexes } =
158
416
  PcbScene3dCopperFillMeshBuilder.#flattenLoops(outer, holes)
159
417
  const triangles = earcut(flat, holeIndexes, 2)
@@ -168,6 +426,10 @@ export class PcbScene3dCopperFillMeshBuilder {
168
426
  topZ,
169
427
  false
170
428
  )
429
+ if (PcbScene3dCopperFillMeshBuilder.#isSurfaceOnly(options)) {
430
+ return
431
+ }
432
+
171
433
  PcbScene3dCopperFillMeshBuilder.#appendSurfaceTriangles(
172
434
  positions,
173
435
  points,
@@ -191,6 +453,132 @@ export class PcbScene3dCopperFillMeshBuilder {
191
453
  }
192
454
  }
193
455
 
456
+ /**
457
+ * Resolves normalized fill loop sets.
458
+ * @param {object[]} fills Filled copper primitives.
459
+ * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
460
+ * @param {boolean} mirrorY Whether to mirror underside Y coordinates.
461
+ * @returns {{ outer: number[][], holes: number[][][], bounds: object }[]}
462
+ */
463
+ static #resolveLoopSets(fills, normalizeBoardPoint, mirrorY) {
464
+ return (fills || []).flatMap((fill) =>
465
+ PcbAssemblyFillGeometryResolver.resolveAll(fill)
466
+ .map((loops) =>
467
+ PcbScene3dCopperFillMeshBuilder.#normalizeLoopSet(
468
+ loops,
469
+ normalizeBoardPoint,
470
+ mirrorY
471
+ )
472
+ )
473
+ .filter(Boolean)
474
+ )
475
+ }
476
+
477
+ /**
478
+ * Normalizes one fill loop set.
479
+ * @param {{ outer: number[][], holes: number[][][] }} loops Source loops.
480
+ * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
481
+ * @param {boolean} mirrorY Whether to mirror underside Y coordinates.
482
+ * @returns {{ outer: number[][], holes: number[][][], bounds: object } | null}
483
+ */
484
+ static #normalizeLoopSet(loops, normalizeBoardPoint, mirrorY) {
485
+ const outer = PcbScene3dCopperFillMeshBuilder.#normalizeLoop(
486
+ loops.outer,
487
+ normalizeBoardPoint,
488
+ mirrorY
489
+ )
490
+ if (!PcbScene3dCopperFillMeshBuilder.#isValidLoop(outer)) {
491
+ return null
492
+ }
493
+
494
+ const holes = (loops.holes || [])
495
+ .map((loop) =>
496
+ PcbScene3dCopperFillMeshBuilder.#normalizeLoop(
497
+ loop,
498
+ normalizeBoardPoint,
499
+ mirrorY
500
+ )
501
+ )
502
+ .filter((loop) =>
503
+ PcbScene3dCopperFillMeshBuilder.#isValidLoop(loop)
504
+ )
505
+
506
+ return {
507
+ outer,
508
+ holes,
509
+ bounds: PcbScene3dCopperFillMeshBuilder.#loopBounds(outer)
510
+ }
511
+ }
512
+
513
+ /**
514
+ * Resolves later fill loops fully contained by an earlier loop set.
515
+ * @param {{ outer: number[][], holes: number[][][], bounds: object }} loopSet Current loop set.
516
+ * @param {{ outer: number[][], holes: number[][][], bounds: object }[]} loopSets All loop sets.
517
+ * @param {number} currentIndex Current loop-set index.
518
+ * @returns {number[][][]}
519
+ */
520
+ static #containedLaterLoops(loopSet, loopSets, currentIndex) {
521
+ return loopSets
522
+ .slice(currentIndex + 1)
523
+ .filter((candidate) =>
524
+ PcbScene3dCopperFillMeshBuilder.#containsLoopSet(
525
+ loopSet,
526
+ candidate
527
+ )
528
+ )
529
+ .map((candidate) => candidate.outer)
530
+ }
531
+
532
+ /**
533
+ * Checks whether one loop set contains another fill loop.
534
+ * @param {{ outer: number[][], holes: number[][][], bounds: object }} outerSet Outer loop set.
535
+ * @param {{ outer: number[][], bounds: object }} candidate Candidate loop set.
536
+ * @returns {boolean}
537
+ */
538
+ static #containsLoopSet(outerSet, candidate) {
539
+ const representative =
540
+ PcbScene3dCopperFillMeshBuilder.#representativePoint(
541
+ candidate.outer
542
+ )
543
+
544
+ return (
545
+ PcbScene3dCopperFillMeshBuilder.#boundsContainBounds(
546
+ outerSet.bounds,
547
+ candidate.bounds
548
+ ) &&
549
+ candidate.outer.every((point) =>
550
+ PcbScene3dCopperFillMeshBuilder.#pointInPolygon(
551
+ point,
552
+ outerSet.outer
553
+ )
554
+ ) &&
555
+ !outerSet.holes.some((hole) =>
556
+ PcbScene3dCopperFillMeshBuilder.#pointInPolygon(
557
+ representative,
558
+ hole
559
+ )
560
+ )
561
+ )
562
+ }
563
+
564
+ /**
565
+ * Checks whether a fill should be represented by only its visible face.
566
+ * @param {{ surfaceOnly?: boolean } | undefined} options Mesh options.
567
+ * @returns {boolean}
568
+ */
569
+ static #isSurfaceOnly(options) {
570
+ return options?.surfaceOnly === true
571
+ }
572
+
573
+ /**
574
+ * Checks whether contained fill overlaps should be removed before meshing.
575
+ * @param {{ clipContainedFillOverlaps?: boolean } | undefined} options Mesh options.
576
+ * @returns {boolean}
577
+ */
578
+ static #shouldClipContainedFillOverlaps(options) {
579
+ return options?.clipContainedFillOverlaps === true
580
+ }
581
+
194
582
  /**
195
583
  * Normalizes one loop into local copper-side coordinates.
196
584
  * @param {number[][]} loop Source loop.
@@ -376,6 +764,140 @@ export class PcbScene3dCopperFillMeshBuilder {
376
764
  )
377
765
  }
378
766
 
767
+ /**
768
+ * Computes axis-aligned bounds for one loop.
769
+ * @param {number[][]} loop Candidate loop.
770
+ * @returns {{ minX: number, minY: number, maxX: number, maxY: number }}
771
+ */
772
+ static #loopBounds(loop) {
773
+ return (loop || []).reduce(
774
+ (bounds, point) => ({
775
+ minX: Math.min(bounds.minX, Number(point?.[0])),
776
+ minY: Math.min(bounds.minY, Number(point?.[1])),
777
+ maxX: Math.max(bounds.maxX, Number(point?.[0])),
778
+ maxY: Math.max(bounds.maxY, Number(point?.[1]))
779
+ }),
780
+ {
781
+ minX: Infinity,
782
+ minY: Infinity,
783
+ maxX: -Infinity,
784
+ maxY: -Infinity
785
+ }
786
+ )
787
+ }
788
+
789
+ /**
790
+ * Checks whether one bounds fully contains another.
791
+ * @param {{ minX: number, minY: number, maxX: number, maxY: number }} outer Outer bounds.
792
+ * @param {{ minX: number, minY: number, maxX: number, maxY: number }} inner Inner bounds.
793
+ * @returns {boolean}
794
+ */
795
+ static #boundsContainBounds(outer, inner) {
796
+ return (
797
+ Number.isFinite(
798
+ outer.minX +
799
+ outer.minY +
800
+ outer.maxX +
801
+ outer.maxY +
802
+ inner.minX +
803
+ inner.minY +
804
+ inner.maxX +
805
+ inner.maxY
806
+ ) &&
807
+ inner.minX >= outer.minX - 0.001 &&
808
+ inner.maxX <= outer.maxX + 0.001 &&
809
+ inner.minY >= outer.minY - 0.001 &&
810
+ inner.maxY <= outer.maxY + 0.001
811
+ )
812
+ }
813
+
814
+ /**
815
+ * Checks whether one candidate loop set is fully inside a hole.
816
+ * @param {{ outer: number[][], bounds: object }} candidate Candidate loop set.
817
+ * @param {{ holes: number[][][] }} loopSet Previous loop set.
818
+ * @returns {boolean}
819
+ */
820
+ static #isInsideAnyHole(candidate, loopSet) {
821
+ return (loopSet.holes || []).some((hole) => {
822
+ const holeBounds = PcbScene3dCopperFillMeshBuilder.#loopBounds(hole)
823
+
824
+ return (
825
+ PcbScene3dCopperFillMeshBuilder.#boundsContainBounds(
826
+ holeBounds,
827
+ candidate.bounds
828
+ ) &&
829
+ candidate.outer.every((point) =>
830
+ PcbScene3dCopperFillMeshBuilder.#pointInPolygon(point, hole)
831
+ )
832
+ )
833
+ })
834
+ }
835
+
836
+ /**
837
+ * Checks whether two bounds overlap.
838
+ * @param {{ minX: number, minY: number, maxX: number, maxY: number }} left Left bounds.
839
+ * @param {{ minX: number, minY: number, maxX: number, maxY: number }} right Right bounds.
840
+ * @returns {boolean}
841
+ */
842
+ static #boundsOverlap(left, right) {
843
+ return !(
844
+ left.maxX < right.minX - 0.001 ||
845
+ left.minX > right.maxX + 0.001 ||
846
+ left.maxY < right.minY - 0.001 ||
847
+ left.minY > right.maxY + 0.001
848
+ )
849
+ }
850
+
851
+ /**
852
+ * Resolves a representative point for one loop.
853
+ * @param {number[][]} loop Candidate loop.
854
+ * @returns {number[]}
855
+ */
856
+ static #representativePoint(loop) {
857
+ const total = (loop || []).reduce(
858
+ (sum, point) => [
859
+ sum[0] + Number(point?.[0]),
860
+ sum[1] + Number(point?.[1])
861
+ ],
862
+ [0, 0]
863
+ )
864
+ const count = Math.max((loop || []).length, 1)
865
+ return [total[0] / count, total[1] / count]
866
+ }
867
+
868
+ /**
869
+ * Checks whether a point is inside a polygon loop.
870
+ * @param {number[]} point Candidate point.
871
+ * @param {number[][]} polygon Polygon loop.
872
+ * @returns {boolean}
873
+ */
874
+ static #pointInPolygon(point, polygon) {
875
+ let inside = false
876
+
877
+ for (
878
+ let index = 0, previousIndex = polygon.length - 1;
879
+ index < polygon.length;
880
+ previousIndex = index, index += 1
881
+ ) {
882
+ const current = polygon[index]
883
+ const previous = polygon[previousIndex]
884
+ const currentY = Number(current?.[1])
885
+ const previousY = Number(previous?.[1])
886
+ const pointY = Number(point?.[1])
887
+ const intersects =
888
+ currentY > pointY !== previousY > pointY &&
889
+ Number(point?.[0]) <
890
+ ((Number(previous?.[0]) - Number(current?.[0])) *
891
+ (pointY - currentY)) /
892
+ (previousY - currentY) +
893
+ Number(current?.[0])
894
+
895
+ if (intersects) inside = !inside
896
+ }
897
+
898
+ return inside
899
+ }
900
+
379
901
  /**
380
902
  * Computes signed loop area.
381
903
  * @param {number[][]} loop Candidate loop.