@jscad/modeling 2.7.1 → 2.9.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.
Files changed (194) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/jscad-modeling.min.js +174 -144
  3. package/package.json +2 -2
  4. package/src/colors/hslToRgb.js +1 -1
  5. package/src/colors/hueToColorComponent.js +1 -0
  6. package/src/curves/bezier/tangentAt.js +2 -2
  7. package/src/curves/bezier/tangentAt.test.js +1 -1
  8. package/src/curves/bezier/valueAt.test.js +1 -1
  9. package/src/geometries/geom2/index.js +10 -0
  10. package/src/geometries/geom2/isA.js +2 -2
  11. package/src/geometries/geom2/toCompactBinary.js +4 -4
  12. package/src/geometries/geom2/toOutlines.js +6 -11
  13. package/src/geometries/geom2/toString.js +1 -1
  14. package/src/geometries/geom2/transform.test.js +1 -1
  15. package/src/geometries/geom3/fromCompactBinary.js +1 -1
  16. package/src/geometries/geom3/index.js +17 -0
  17. package/src/geometries/geom3/invert.js +2 -2
  18. package/src/geometries/geom3/isA.js +2 -2
  19. package/src/geometries/geom3/toCompactBinary.js +4 -4
  20. package/src/geometries/geom3/toPoints.js +1 -0
  21. package/src/geometries/geom3/toString.js +1 -1
  22. package/src/geometries/geom3/transform.test.js +1 -1
  23. package/src/geometries/index.js +8 -1
  24. package/src/geometries/path2/eachPoint.js +3 -3
  25. package/src/geometries/path2/index.js +11 -0
  26. package/src/geometries/path2/isA.js +2 -2
  27. package/src/geometries/path2/reverse.js +4 -4
  28. package/src/geometries/path2/toCompactBinary.js +6 -6
  29. package/src/geometries/path2/toString.js +1 -1
  30. package/src/geometries/path2/transform.test.js +1 -1
  31. package/src/geometries/poly2/arePointsInside.test.js +1 -1
  32. package/src/geometries/poly2/index.js +6 -0
  33. package/src/geometries/poly3/index.js +7 -1
  34. package/src/geometries/poly3/invert.js +7 -1
  35. package/src/geometries/poly3/isA.js +2 -2
  36. package/src/geometries/poly3/isConvex.js +2 -2
  37. package/src/geometries/poly3/measureArea.js +4 -4
  38. package/src/geometries/poly3/measureBoundingBox.js +2 -2
  39. package/src/geometries/poly3/measureBoundingSphere.js +2 -2
  40. package/src/geometries/poly3/measureSignedVolume.js +4 -4
  41. package/src/geometries/poly3/toPoints.js +2 -2
  42. package/src/geometries/poly3/toString.js +2 -2
  43. package/src/geometries/poly3/transform.js +2 -2
  44. package/src/maths/index.js +1 -1
  45. package/src/maths/line2/equals.js +2 -2
  46. package/src/maths/line2/fromValues.js +2 -2
  47. package/src/maths/line2/intersectPointOfLines.js +1 -1
  48. package/src/maths/line2/intersectPointOfLines.test.js +1 -1
  49. package/src/maths/line2/reverse.test.js +1 -1
  50. package/src/maths/line2/transform.test.js +1 -1
  51. package/src/maths/line3/equals.js +2 -2
  52. package/src/maths/line3/reverse.test.js +1 -1
  53. package/src/maths/line3/transform.test.js +1 -1
  54. package/src/maths/mat4/fromRotation.js +1 -1
  55. package/src/maths/mat4/fromVectorRotation.js +1 -1
  56. package/src/maths/mat4/fromVectorRotation.test.js +1 -1
  57. package/src/maths/mat4/identity.test.js +1 -1
  58. package/src/maths/mat4/invert.js +18 -18
  59. package/src/maths/mat4/isIdentity.js +1 -1
  60. package/src/maths/mat4/isIdentity.test.js +0 -2
  61. package/src/maths/mat4/isMirroring.js +4 -4
  62. package/src/maths/mat4/isMirroring.test.js +1 -1
  63. package/src/maths/mat4/leftMultiplyVec3.js +2 -2
  64. package/src/maths/mat4/rotate.js +1 -1
  65. package/src/maths/mat4/toString.js +2 -2
  66. package/src/maths/mat4/translate.test.js +1 -1
  67. package/src/maths/plane/flip.test.js +1 -1
  68. package/src/maths/plane/fromPoints.d.ts +1 -1
  69. package/src/maths/plane/fromPoints.js +1 -3
  70. package/src/maths/plane/signedDistanceToPoint.js +1 -1
  71. package/src/maths/plane/transform.test.js +1 -1
  72. package/src/maths/utils/aboutEqualNormals.js +2 -2
  73. package/src/maths/vec2/abs.d.ts +1 -1
  74. package/src/maths/vec2/add.test.js +1 -1
  75. package/src/maths/vec2/angleDegrees.d.ts +1 -1
  76. package/src/maths/vec2/angleRadians.d.ts +1 -1
  77. package/src/maths/vec2/create.js +1 -1
  78. package/src/maths/vec2/cross.test.js +1 -1
  79. package/src/maths/vec2/divide.test.js +1 -1
  80. package/src/maths/vec2/fromAngleDegrees.js +1 -1
  81. package/src/maths/vec2/fromScalar.js +1 -1
  82. package/src/maths/vec2/length.d.ts +1 -1
  83. package/src/maths/vec2/length.js +1 -1
  84. package/src/maths/vec2/length.test.js +10 -0
  85. package/src/maths/vec2/lerp.test.js +1 -1
  86. package/src/maths/vec2/multiply.test.js +1 -1
  87. package/src/maths/vec2/negate.test.js +1 -1
  88. package/src/maths/vec2/normal.js +1 -1
  89. package/src/maths/vec2/normalize.d.ts +1 -1
  90. package/src/maths/vec2/normalize.test.js +1 -1
  91. package/src/maths/vec2/rotate.test.js +1 -1
  92. package/src/maths/vec2/squaredLength.d.ts +1 -1
  93. package/src/maths/vec2/squaredLength.js +3 -3
  94. package/src/maths/vec2/subtract.test.js +1 -1
  95. package/src/maths/vec2/toString.js +1 -1
  96. package/src/maths/vec2/transform.test.js +1 -1
  97. package/src/maths/vec3/abs.d.ts +1 -1
  98. package/src/maths/vec3/add.test.js +1 -1
  99. package/src/maths/vec3/angle.js +2 -2
  100. package/src/maths/vec3/angle.test.js +17 -0
  101. package/src/maths/vec3/cross.test.js +1 -1
  102. package/src/maths/vec3/divide.test.js +1 -1
  103. package/src/maths/vec3/fromScalar.js +1 -1
  104. package/src/maths/vec3/fromVec2.d.ts +1 -1
  105. package/src/maths/vec3/fromVec2.js +3 -3
  106. package/src/maths/vec3/length.d.ts +1 -1
  107. package/src/maths/vec3/length.js +4 -4
  108. package/src/maths/vec3/length.test.js +10 -0
  109. package/src/maths/vec3/lerp.test.js +1 -1
  110. package/src/maths/vec3/multiply.test.js +1 -1
  111. package/src/maths/vec3/negate.d.ts +1 -1
  112. package/src/maths/vec3/negate.test.js +1 -1
  113. package/src/maths/vec3/normalize.d.ts +1 -1
  114. package/src/maths/vec3/normalize.test.js +1 -1
  115. package/src/maths/vec3/rotateX.test.js +1 -1
  116. package/src/maths/vec3/rotateY.test.js +1 -1
  117. package/src/maths/vec3/rotateZ.test.js +1 -1
  118. package/src/maths/vec3/scale.test.js +1 -1
  119. package/src/maths/vec3/squaredLength.d.ts +1 -1
  120. package/src/maths/vec3/squaredLength.js +4 -4
  121. package/src/maths/vec3/subtract.test.js +1 -1
  122. package/src/maths/vec3/toString.js +1 -1
  123. package/src/maths/vec3/transform.test.js +1 -1
  124. package/src/maths/vec4/toString.js +1 -1
  125. package/src/maths/vec4/transform.test.js +1 -1
  126. package/src/measurements/measureBoundingSphere.js +4 -4
  127. package/src/measurements/measureCenterOfMass.js +1 -1
  128. package/src/operations/booleans/mayOverlap.js +3 -3
  129. package/src/operations/booleans/retessellate.js +3 -5
  130. package/src/operations/booleans/scission.js +1 -1
  131. package/src/operations/booleans/subtract.js +1 -1
  132. package/src/operations/booleans/union.test.js +1 -1
  133. package/src/operations/booleans/unionGeom3Sub.js +1 -1
  134. package/src/operations/expansions/expand.js +2 -2
  135. package/src/operations/expansions/expand.test.js +3 -35
  136. package/src/operations/expansions/expandShell.js +24 -18
  137. package/src/operations/expansions/offset.js +1 -1
  138. package/src/operations/expansions/offset.test.js +25 -89
  139. package/src/operations/expansions/offsetFromPoints.js +11 -6
  140. package/src/operations/extrusions/earcut/assignHoles.js +87 -0
  141. package/src/operations/extrusions/earcut/assignHoles.test.js +28 -0
  142. package/src/operations/extrusions/earcut/eliminateHoles.js +131 -0
  143. package/src/operations/extrusions/earcut/index.js +252 -0
  144. package/src/operations/extrusions/earcut/linkedList.js +58 -0
  145. package/src/operations/extrusions/earcut/linkedListSort.js +54 -0
  146. package/src/operations/extrusions/earcut/linkedPolygon.js +197 -0
  147. package/src/operations/extrusions/earcut/polygonHierarchy.js +64 -0
  148. package/src/operations/extrusions/earcut/triangle.js +16 -0
  149. package/src/operations/extrusions/extrudeFromSlices.js +10 -3
  150. package/src/operations/extrusions/extrudeFromSlices.test.js +33 -23
  151. package/src/operations/extrusions/extrudeLinear.js +11 -6
  152. package/src/operations/extrusions/extrudeLinear.test.js +77 -27
  153. package/src/operations/extrusions/extrudeLinearGeom2.js +5 -2
  154. package/src/operations/extrusions/extrudeLinearPath2.js +24 -0
  155. package/src/operations/extrusions/extrudeRectangular.js +1 -1
  156. package/src/operations/extrusions/extrudeRectangular.test.js +7 -7
  157. package/src/operations/extrusions/extrudeRotate.test.js +19 -27
  158. package/src/operations/extrusions/project.js +1 -1
  159. package/src/operations/extrusions/slice/calculatePlane.js +7 -4
  160. package/src/operations/extrusions/slice/isA.js +2 -2
  161. package/src/operations/extrusions/slice/repairSlice.js +47 -0
  162. package/src/operations/extrusions/slice/toPolygons.js +24 -60
  163. package/src/operations/hulls/hull.test.js +1 -1
  164. package/src/operations/hulls/hullChain.js +1 -1
  165. package/src/operations/hulls/hullGeom2.js +1 -1
  166. package/src/operations/hulls/hullPath2.js +6 -4
  167. package/src/operations/hulls/hullPath2.test.js +16 -0
  168. package/src/operations/hulls/hullPoints2.test.js +1 -1
  169. package/src/operations/hulls/quickhull/QuickHull.js +2 -2
  170. package/src/operations/modifiers/edges.js +2 -4
  171. package/src/operations/modifiers/generalize.js +4 -7
  172. package/src/operations/modifiers/snap.test.js +3 -3
  173. package/src/operations/transforms/align.d.ts +1 -1
  174. package/src/operations/transforms/center.js +17 -17
  175. package/src/operations/transforms/mirror.js +11 -11
  176. package/src/operations/transforms/rotate.js +12 -12
  177. package/src/operations/transforms/scale.js +19 -19
  178. package/src/operations/transforms/transform.js +3 -3
  179. package/src/operations/transforms/translate.js +14 -14
  180. package/src/primitives/arc.js +1 -1
  181. package/src/primitives/cylinderElliptic.test.js +0 -2
  182. package/src/primitives/ellipsoid.js +1 -1
  183. package/src/primitives/ellipsoid.test.js +0 -2
  184. package/src/primitives/geodesicSphere.d.ts +0 -1
  185. package/src/primitives/geodesicSphere.js +2 -2
  186. package/src/primitives/polyhedron.js +1 -1
  187. package/src/primitives/roundedCylinder.js +1 -1
  188. package/src/primitives/torus.d.ts +0 -1
  189. package/src/primitives/torus.test.js +1 -1
  190. package/src/primitives/triangle.js +1 -1
  191. package/src/text/vectorText.js +2 -2
  192. package/src/utils/padArrayToLength.js +1 -1
  193. package/test/helpers/comparePolygons.js +1 -3
  194. package/test/helpers/nearlyEqual.js +2 -6
@@ -46,7 +46,7 @@ test('offset: offsetting a bent line produces expected geometry', (t) => {
46
46
  // offset it by -2.
47
47
  offsetLinePath2 = offset({ delta: -2, corners: 'edge', segments: 8 }, linePath2)
48
48
  offsetPoints = path2.toPoints(offsetLinePath2)
49
- t.is(offsetPoints.length, 7)
49
+ t.is(offsetPoints.length, 5)
50
50
  boundingBox = measureBoundingBox(offsetLinePath2)
51
51
  t.true(comparePoints(boundingBox, [[-2, 0, 0], [10, 12, 0]]), 'Unexpected bounding box: ' + JSON.stringify(boundingBox))
52
52
  })
@@ -124,20 +124,12 @@ test('offset (corners: edge): offset of a path2 produces expected offset path2',
124
124
  let pts = path2.toPoints(obs)
125
125
  let exp = [
126
126
  [-5, -6],
127
- [5, -6],
128
127
  [6, -6],
129
- [6, -5],
130
- [6, 5],
131
128
  [6, 6],
132
- [5, 6],
133
- [3, 6],
134
129
  [2, 6],
135
- [2, 5],
136
130
  [2, 1],
137
131
  [-2, 1],
138
- [-2, 5],
139
132
  [-1.9999999999999996, 6],
140
- [-3, 6],
141
133
  [-5, 6]
142
134
  ]
143
135
  t.true(comparePoints(pts, exp))
@@ -145,26 +137,14 @@ test('offset (corners: edge): offset of a path2 produces expected offset path2',
145
137
  obs = offset({ delta: 1, corners: 'edge' }, closeline)
146
138
  pts = path2.toPoints(obs)
147
139
  exp = [
148
- [-6, -6],
149
- [-5, -6],
150
- [5, -6],
151
140
  [6, -6],
152
- [6, -5],
153
- [6, 5],
154
141
  [6, 6],
155
- [5, 6],
156
- [3, 6],
157
142
  [2, 6],
158
- [2, 5],
159
143
  [2, 1],
160
144
  [-2, 1],
161
- [-2, 5],
162
145
  [-1.9999999999999996, 6],
163
- [-3, 6],
164
- [-5, 6],
165
146
  [-6, 6],
166
- [-6, 5],
167
- [-6, -5]
147
+ [-6, -6]
168
148
  ]
169
149
  t.true(comparePoints(pts, exp))
170
150
 
@@ -175,12 +155,8 @@ test('offset (corners: edge): offset of a path2 produces expected offset path2',
175
155
  [4.5, -4.5],
176
156
  [4.5, 4.5],
177
157
  [3.5, 4.5],
178
- [3.5, -3.061616997868383e-17],
179
158
  [3.4999999999999996, -0.5],
180
- [3, -0.5],
181
- [-3, -0.5],
182
159
  [-3.5, -0.4999999999999996],
183
- [-3.5, 3.061616997868383e-17],
184
160
  [-3.5, 4.5],
185
161
  [-5, 4.5]
186
162
  ]
@@ -193,12 +169,8 @@ test('offset (corners: edge): offset of a path2 produces expected offset path2',
193
169
  [4.5, -4.5],
194
170
  [4.5, 4.5],
195
171
  [3.5, 4.5],
196
- [3.5, -3.061616997868383e-17],
197
172
  [3.4999999999999996, -0.5],
198
- [3, -0.5],
199
- [-3, -0.5],
200
173
  [-3.5, -0.4999999999999996],
201
- [-3.5, 3.061616997868383e-17],
202
174
  [-3.5, 4.5],
203
175
  [-4.5, 4.5]
204
176
  ]
@@ -373,26 +345,14 @@ test('offset (options): offsetting of a simple geom2 produces expected offset ge
373
345
  obs = offset({ delta: 1, corners: 'edge' }, geometry)
374
346
  pts = geom2.toPoints(obs)
375
347
  exp = [
376
- [-6, -6],
377
- [-5, -6],
378
- [5, -6],
379
348
  [6, -6],
380
- [6, -5],
381
- [6, 5],
382
349
  [6, 6],
383
- [5, 6],
384
- [3, 6],
385
350
  [2, 6],
386
- [2, 5],
387
351
  [2, 1],
388
352
  [-2, 1],
389
- [-2, 5],
390
353
  [-1.9999999999999996, 6],
391
- [-3, 6],
392
- [-5, 6],
393
354
  [-6, 6],
394
- [-6, 5],
395
- [-6, -5]
355
+ [-6, -6]
396
356
  ]
397
357
  t.true(comparePoints(pts, exp))
398
358
 
@@ -422,78 +382,54 @@ test('offset (options): offsetting of a simple geom2 produces expected offset ge
422
382
 
423
383
  test('offset (options): offsetting of a complex geom2 produces expected offset geom2', (t) => {
424
384
  const geometry = geom2.create([
425
- [[-75.00000, 75.00000], [-75.00000, -75.00000]],
426
- [[-75.00000, -75.00000], [75.00000, -75.00000]],
427
- [[75.00000, -75.00000], [75.00000, 75.00000]],
428
- [[-40.00000, 75.00000], [-75.00000, 75.00000]],
429
- [[75.00000, 75.00000], [40.00000, 75.00000]],
430
- [[40.00000, 75.00000], [40.00000, 0.00000]],
431
- [[40.00000, 0.00000], [-40.00000, 0.00000]],
432
- [[-40.00000, 0.00000], [-40.00000, 75.00000]],
433
- [[15.00000, -10.00000], [15.00000, -40.00000]],
434
- [[-15.00000, -10.00000], [15.00000, -10.00000]],
435
- [[-15.00000, -40.00000], [-15.00000, -10.00000]],
436
- [[-8.00000, -40.00000], [-15.00000, -40.00000]],
437
- [[15.00000, -40.00000], [8.00000, -40.00000]],
438
- [[-8.00000, -25.00000], [-8.00000, -40.00000]],
439
- [[8.00000, -25.00000], [-8.00000, -25.00000]],
440
- [[8.00000, -40.00000], [8.00000, -25.00000]],
441
- [[-2.00000, -15.00000], [-2.00000, -19.00000]],
442
- [[-2.00000, -19.00000], [2.00000, -19.00000]],
443
- [[2.00000, -19.00000], [2.00000, -15.00000]],
444
- [[2.00000, -15.00000], [-2.00000, -15.00000]]
385
+ [[-75, 75], [-75, -75]],
386
+ [[-75, -75], [75, -75]],
387
+ [[75, -75], [75, 75]],
388
+ [[-40, 75], [-75, 75]],
389
+ [[75, 75], [40, 75]],
390
+ [[40, 75], [40, 0]],
391
+ [[40, 0], [-40, 0]],
392
+ [[-40, 0], [-40, 75]],
393
+ [[15, -10], [15, -40]],
394
+ [[-15, -10], [15, -10]],
395
+ [[-15, -40], [-15, -10]],
396
+ [[-8, -40], [-15, -40]],
397
+ [[15, -40], [8, -40]],
398
+ [[-8, -25], [-8, -40]],
399
+ [[8, -25], [-8, -25]],
400
+ [[8, -40], [8, -25]],
401
+ [[-2, -15], [-2, -19]],
402
+ [[-2, -19], [2, -19]],
403
+ [[2, -19], [2, -15]],
404
+ [[2, -15], [-2, -15]]
445
405
  ])
446
406
 
447
407
  // expand +
448
408
  const obs = offset({ delta: 2, corners: 'edge' }, geometry)
449
409
  const pts = geom2.toPoints(obs)
450
410
  const exp = [
451
- [-77, -77],
452
- [-75, -77],
453
- [75, -77],
454
411
  [77, -77],
455
- [77, -75],
456
- [77, 75],
457
412
  [77, 77],
458
- [75, 77],
459
- [40, 77],
460
413
  [38, 77],
461
- [38, 75],
462
414
  [38, 2],
463
415
  [-38, 2],
464
- [-38, 75],
465
416
  [-37.99999999999999, 77],
466
- [-40, 77],
467
- [-75, 77],
468
417
  [-77, 77],
469
- [-77, 75],
470
418
  [13, -12],
471
419
  [13, -38],
472
420
  [10, -38],
473
- [10, -25],
474
421
  [10, -23],
475
- [8, -23],
476
- [-8, -23],
477
422
  [-10, -23],
478
- [-10, -25],
479
423
  [-10, -38],
480
424
  [-13, -38],
481
425
  [-13, -12],
482
- [-4, -19],
483
426
  [-4, -21],
484
- [-2, -21],
485
- [1.9999999999999998, -21],
486
427
  [3.9999999999999996, -21],
487
- [4, -19],
488
- [4, -15],
489
428
  [4, -13],
490
- [2, -13],
491
- [-1.9999999999999998, -13],
492
429
  [-4, -13],
493
- [-4, -15],
494
- [-77, -75]
430
+ [-77, -77]
495
431
  ]
496
- t.is(pts.length, 44)
432
+ t.is(pts.length, 20)
497
433
  t.true(comparePoints(pts, exp))
498
434
  })
499
435
 
@@ -34,7 +34,7 @@ const offsetFromPoints = (options, points) => {
34
34
  delta = Math.abs(delta) // sign is no longer required
35
35
 
36
36
  let previousSegment = null
37
- const newPoints = []
37
+ let newPoints = []
38
38
  const newCorners = []
39
39
  const of = vec2.create()
40
40
  const n = points.length
@@ -94,6 +94,10 @@ const offsetFromPoints = (options, points) => {
94
94
  // generate corners if necessary
95
95
 
96
96
  if (corners === 'edge') {
97
+ // map for fast point index lookup
98
+ const pointIndex = new Map() // {point: index}
99
+ newPoints.forEach((point, index) => pointIndex.set(point, index))
100
+
97
101
  // create edge corners
98
102
  const line0 = line2.create()
99
103
  const line1 = line2.create()
@@ -103,16 +107,17 @@ const offsetFromPoints = (options, points) => {
103
107
  const ip = line2.intersectPointOfLines(line0, line1)
104
108
  if (Number.isFinite(ip[0]) && Number.isFinite(ip[1])) {
105
109
  const p0 = corner.s0[1]
106
- let i = newPoints.findIndex((point) => vec2.equals(p0, point))
107
- i = (i + 1) % newPoints.length
108
- newPoints.splice(i, 0, ip)
110
+ const i = pointIndex.get(p0)
111
+ newPoints[i] = ip
112
+ newPoints[(i + 1) % newPoints.length] = undefined
109
113
  } else {
110
114
  // paralell segments, drop one
111
115
  const p0 = corner.s1[0]
112
- const i = newPoints.findIndex((point) => vec2.equals(p0, point))
113
- newPoints.splice(i, 1)
116
+ const i = pointIndex.get(p0)
117
+ newPoints[i] = undefined
114
118
  }
115
119
  })
120
+ newPoints = newPoints.filter((p) => p !== undefined)
116
121
  }
117
122
 
118
123
  if (corners === 'round') {
@@ -0,0 +1,87 @@
1
+ const { area } = require('../../../maths/utils')
2
+ const { toOutlines } = require('../../../geometries/geom2')
3
+ const { arePointsInside } = require('../../../geometries/poly2')
4
+
5
+ /*
6
+ * Constructs a polygon hierarchy of solids and holes.
7
+ * The hierarchy is represented as a forest of trees. All trees shall be depth at most 2.
8
+ * If a solid exists inside the hole of another solid, it will be split out as its own root.
9
+ *
10
+ * @param {geom2} geometry
11
+ * @returns {Array} an array of polygons with associated holes
12
+ * @alias module:modeling/geometries/geom2.toTree
13
+ *
14
+ * @example
15
+ * const geometry = subtract(rectangle({size: [5, 5]}), rectangle({size: [3, 3]}))
16
+ * console.log(assignHoles(geometry))
17
+ * [{
18
+ * "solid": [[-2.5,-2.5],[2.5,-2.5],[2.5,2.5],[-2.5,2.5]],
19
+ * "holes": [[[-1.5,1.5],[1.5,1.5],[1.5,-1.5],[-1.5,-1.5]]]
20
+ * }]
21
+ */
22
+ const assignHoles = (geometry) => {
23
+ const outlines = toOutlines(geometry)
24
+ const solids = [] // solid indices
25
+ const holes = [] // hole indices
26
+ outlines.forEach((outline, i) => {
27
+ const a = area(outline)
28
+ if (a < 0) {
29
+ holes.push(i)
30
+ } else if (a > 0) {
31
+ solids.push(i)
32
+ }
33
+ })
34
+ // for each hole, determine what solids it is inside of
35
+ const children = [] // child holes of solid[i]
36
+ const parents = [] // parent solids of hole[i]
37
+ solids.forEach((s, i) => {
38
+ const solid = outlines[s]
39
+ children[i] = []
40
+ holes.forEach((h, j) => {
41
+ const hole = outlines[h]
42
+ // check if a point of hole j is inside solid i
43
+ if (arePointsInside([hole[0]], { vertices: solid })) {
44
+ children[i].push(h)
45
+ if (!parents[j]) parents[j] = []
46
+ parents[j].push(i)
47
+ }
48
+ })
49
+ })
50
+ // check if holes have multiple parents and choose one with fewest children
51
+ holes.forEach((h, j) => {
52
+ // ensure at least one parent exists
53
+ if (parents[j] && parents[j].length > 1) {
54
+ const parent = minIndex(parents[j], (p) => p.length)
55
+ parents[j].forEach((p, i) => {
56
+ if (i !== parent) {
57
+ // Remove hole from skip level parents
58
+ children[p] = children[p].filter((c) => c !== j)
59
+ }
60
+ })
61
+ }
62
+ })
63
+ // map indices back to points
64
+ return children.map((holes, i) => ({
65
+ solid: outlines[solids[i]],
66
+ holes: holes.map((h) => outlines[h])
67
+ }))
68
+ }
69
+
70
+ /*
71
+ * Find the item in the list with smallest score(item).
72
+ * If the list is empty, return undefined.
73
+ */
74
+ const minIndex = (list, score) => {
75
+ let bestIndex
76
+ let best
77
+ list.forEach((item, index) => {
78
+ const value = score(item)
79
+ if (best === undefined || value < best) {
80
+ bestIndex = index
81
+ best = value
82
+ }
83
+ })
84
+ return bestIndex
85
+ }
86
+
87
+ module.exports = assignHoles
@@ -0,0 +1,28 @@
1
+ const test = require('ava')
2
+
3
+ const { subtract } = require('../../../operations/booleans')
4
+ const rectangle = require('../../../primitives/rectangle')
5
+ const assignHoles = require('./assignHoles')
6
+
7
+ test('slice: assignHoles() should return a polygon hierarchy', (t) => {
8
+ const exp1 = [{
9
+ solid: [
10
+ [-3.000013333333334, -3.000013333333334],
11
+ [3.000013333333334, -3.000013333333334],
12
+ [3.000013333333334, 3.000013333333334],
13
+ [-3.000013333333334, 3.000013333333334]
14
+ ],
15
+ holes: [[
16
+ [-1.9999933333333335, 1.9999933333333335],
17
+ [1.9999933333333335, 1.9999933333333335],
18
+ [1.9999933333333335, -1.9999933333333335],
19
+ [-1.9999933333333335, -1.9999933333333335]
20
+ ]]
21
+ }]
22
+ const geometry = subtract(
23
+ rectangle({ size: [6, 6] }),
24
+ rectangle({ size: [4, 4] })
25
+ )
26
+ const obs1 = assignHoles(geometry)
27
+ t.deepEqual(obs1, exp1)
28
+ })
@@ -0,0 +1,131 @@
1
+ const { filterPoints, linkedPolygon, locallyInside, splitPolygon } = require('./linkedPolygon')
2
+ const { area, pointInTriangle } = require('./triangle')
3
+
4
+ /*
5
+ * link every hole into the outer loop, producing a single-ring polygon without holes
6
+ *
7
+ * Original source from https://github.com/mapbox/earcut
8
+ * Copyright (c) 2016 Mapbox
9
+ */
10
+ const eliminateHoles = (data, holeIndices, outerNode, dim) => {
11
+ const queue = []
12
+
13
+ for (let i = 0, len = holeIndices.length; i < len; i++) {
14
+ const start = holeIndices[i] * dim
15
+ const end = i < len - 1 ? holeIndices[i + 1] * dim : data.length
16
+ const list = linkedPolygon(data, start, end, dim, false)
17
+ if (list === list.next) list.steiner = true
18
+ queue.push(getLeftmost(list))
19
+ }
20
+
21
+ queue.sort((a, b) => a.x - b.x) // compare X
22
+
23
+ // process holes from left to right
24
+ for (let i = 0; i < queue.length; i++) {
25
+ outerNode = eliminateHole(queue[i], outerNode)
26
+ outerNode = filterPoints(outerNode, outerNode.next)
27
+ }
28
+
29
+ return outerNode
30
+ }
31
+
32
+ /*
33
+ * find a bridge between vertices that connects hole with an outer ring and link it
34
+ */
35
+ const eliminateHole = (hole, outerNode) => {
36
+ const bridge = findHoleBridge(hole, outerNode)
37
+ if (!bridge) {
38
+ return outerNode
39
+ }
40
+
41
+ const bridgeReverse = splitPolygon(bridge, hole)
42
+
43
+ // filter colinear points around the cuts
44
+ const filteredBridge = filterPoints(bridge, bridge.next)
45
+ filterPoints(bridgeReverse, bridgeReverse.next)
46
+
47
+ // Check if input node was removed by the filtering
48
+ return outerNode === bridge ? filteredBridge : outerNode
49
+ }
50
+
51
+ /*
52
+ * David Eberly's algorithm for finding a bridge between hole and outer polygon
53
+ */
54
+ const findHoleBridge = (hole, outerNode) => {
55
+ let p = outerNode
56
+ const hx = hole.x
57
+ const hy = hole.y
58
+ let qx = -Infinity
59
+ let m
60
+
61
+ // find a segment intersected by a ray from the hole's leftmost point to the left
62
+ // segment's endpoint with lesser x will be potential connection point
63
+ do {
64
+ if (hy <= p.y && hy >= p.next.y && p.next.y !== p.y) {
65
+ const x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y)
66
+ if (x <= hx && x > qx) {
67
+ qx = x
68
+ if (x === hx) {
69
+ if (hy === p.y) return p
70
+ if (hy === p.next.y) return p.next
71
+ }
72
+
73
+ m = p.x < p.next.x ? p : p.next
74
+ }
75
+ }
76
+
77
+ p = p.next
78
+ } while (p !== outerNode)
79
+
80
+ if (!m) return null
81
+
82
+ if (hx === qx) return m // hole touches outer segment; pick leftmost endpoint
83
+
84
+ // look for points inside the triangle of hole point, segment intersection and endpoint
85
+ // if there are no points found, we have a valid connection
86
+ // otherwise choose the point of the minimum angle with the ray as connection point
87
+
88
+ const stop = m
89
+ const mx = m.x
90
+ const my = m.y
91
+ let tanMin = Infinity
92
+
93
+ p = m
94
+
95
+ do {
96
+ if (hx >= p.x && p.x >= mx && hx !== p.x &&
97
+ pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y)) {
98
+ const tan = Math.abs(hy - p.y) / (hx - p.x) // tangential
99
+
100
+ if (locallyInside(p, hole) && (tan < tanMin || (tan === tanMin && (p.x > m.x || (p.x === m.x && sectorContainsSector(m, p)))))) {
101
+ m = p
102
+ tanMin = tan
103
+ }
104
+ }
105
+
106
+ p = p.next
107
+ } while (p !== stop)
108
+
109
+ return m
110
+ }
111
+
112
+ /*
113
+ * whether sector in vertex m contains sector in vertex p in the same coordinates
114
+ */
115
+ const sectorContainsSector = (m, p) => area(m.prev, m, p.prev) < 0 && area(p.next, m, m.next) < 0
116
+
117
+ /*
118
+ * find the leftmost node of a polygon ring
119
+ */
120
+ const getLeftmost = (start) => {
121
+ let p = start
122
+ let leftmost = start
123
+ do {
124
+ if (p.x < leftmost.x || (p.x === leftmost.x && p.y < leftmost.y)) leftmost = p
125
+ p = p.next
126
+ } while (p !== start)
127
+
128
+ return leftmost
129
+ }
130
+
131
+ module.exports = eliminateHoles