@jscad/modeling 2.7.2 → 2.9.1

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 (238) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/jscad-modeling.min.js +443 -398
  3. package/package.json +2 -2
  4. package/src/curves/bezier/tangentAt.test.js +1 -1
  5. package/src/curves/bezier/valueAt.test.js +1 -1
  6. package/src/geometries/geom2/index.d.ts +1 -0
  7. package/src/geometries/geom2/index.js +12 -1
  8. package/src/geometries/geom2/isA.js +2 -2
  9. package/src/geometries/geom2/toCompactBinary.js +4 -4
  10. package/src/geometries/geom2/toString.js +1 -1
  11. package/src/geometries/geom2/transform.test.js +1 -1
  12. package/src/geometries/geom2/validate.d.ts +3 -0
  13. package/src/geometries/geom2/validate.js +36 -0
  14. package/src/geometries/geom3/fromCompactBinary.js +1 -1
  15. package/src/geometries/geom3/index.d.ts +1 -0
  16. package/src/geometries/geom3/index.js +19 -1
  17. package/src/geometries/geom3/isA.js +2 -2
  18. package/src/geometries/geom3/toCompactBinary.js +4 -4
  19. package/src/geometries/geom3/toString.js +1 -1
  20. package/src/geometries/geom3/transform.test.js +1 -1
  21. package/src/geometries/geom3/validate.d.ts +3 -0
  22. package/src/geometries/geom3/validate.js +62 -0
  23. package/src/geometries/index.js +8 -1
  24. package/src/geometries/path2/eachPoint.js +3 -3
  25. package/src/geometries/path2/index.d.ts +1 -0
  26. package/src/geometries/path2/index.js +13 -1
  27. package/src/geometries/path2/isA.js +2 -2
  28. package/src/geometries/path2/reverse.js +4 -4
  29. package/src/geometries/path2/toCompactBinary.js +6 -6
  30. package/src/geometries/path2/toString.js +1 -1
  31. package/src/geometries/path2/transform.test.js +1 -1
  32. package/src/geometries/path2/validate.d.ts +3 -0
  33. package/src/geometries/path2/validate.js +41 -0
  34. package/src/geometries/poly2/arePointsInside.js +0 -35
  35. package/src/geometries/poly2/arePointsInside.test.js +1 -1
  36. package/src/geometries/poly2/index.js +6 -0
  37. package/src/geometries/poly3/index.d.ts +1 -0
  38. package/src/geometries/poly3/index.js +9 -2
  39. package/src/geometries/poly3/invert.js +7 -1
  40. package/src/geometries/poly3/isA.js +2 -2
  41. package/src/geometries/poly3/isConvex.js +2 -2
  42. package/src/geometries/poly3/measureArea.js +4 -4
  43. package/src/geometries/poly3/measureArea.test.js +16 -16
  44. package/src/geometries/poly3/measureBoundingBox.js +2 -2
  45. package/src/geometries/poly3/measureBoundingSphere.js +2 -2
  46. package/src/geometries/poly3/measureBoundingSphere.test.js +8 -8
  47. package/src/geometries/poly3/measureSignedVolume.js +4 -4
  48. package/src/geometries/poly3/toPoints.js +2 -2
  49. package/src/geometries/poly3/toString.js +2 -2
  50. package/src/geometries/poly3/transform.js +2 -2
  51. package/src/geometries/poly3/validate.d.ts +4 -0
  52. package/src/geometries/poly3/validate.js +50 -0
  53. package/src/maths/index.js +1 -1
  54. package/src/maths/line2/equals.js +2 -2
  55. package/src/maths/line2/fromValues.js +2 -2
  56. package/src/maths/line2/intersectPointOfLines.js +1 -1
  57. package/src/maths/line2/intersectPointOfLines.test.js +1 -1
  58. package/src/maths/line2/reverse.test.js +1 -1
  59. package/src/maths/line2/transform.test.js +1 -1
  60. package/src/maths/line3/equals.js +2 -2
  61. package/src/maths/line3/reverse.test.js +1 -1
  62. package/src/maths/line3/transform.test.js +1 -1
  63. package/src/maths/mat4/fromVectorRotation.js +1 -1
  64. package/src/maths/mat4/fromVectorRotation.test.js +1 -1
  65. package/src/maths/mat4/identity.test.js +1 -1
  66. package/src/maths/mat4/invert.js +18 -18
  67. package/src/maths/mat4/isIdentity.js +1 -1
  68. package/src/maths/mat4/isMirroring.js +4 -4
  69. package/src/maths/mat4/isMirroring.test.js +1 -1
  70. package/src/maths/mat4/leftMultiplyVec3.js +2 -2
  71. package/src/maths/mat4/toString.js +2 -2
  72. package/src/maths/mat4/translate.test.js +1 -1
  73. package/src/maths/plane/flip.test.js +1 -1
  74. package/src/maths/plane/fromPoints.d.ts +1 -1
  75. package/src/maths/plane/fromPoints.js +1 -3
  76. package/src/maths/plane/signedDistanceToPoint.js +1 -1
  77. package/src/maths/plane/transform.test.js +1 -1
  78. package/src/maths/utils/aboutEqualNormals.js +2 -2
  79. package/src/maths/vec2/abs.d.ts +1 -1
  80. package/src/maths/vec2/add.test.js +1 -1
  81. package/src/maths/vec2/angleDegrees.d.ts +1 -1
  82. package/src/maths/vec2/angleRadians.d.ts +1 -1
  83. package/src/maths/vec2/create.js +1 -1
  84. package/src/maths/vec2/cross.test.js +1 -1
  85. package/src/maths/vec2/divide.test.js +1 -1
  86. package/src/maths/vec2/fromAngleDegrees.js +1 -1
  87. package/src/maths/vec2/fromScalar.js +1 -1
  88. package/src/maths/vec2/length.d.ts +1 -1
  89. package/src/maths/vec2/length.js +1 -1
  90. package/src/maths/vec2/lerp.test.js +1 -1
  91. package/src/maths/vec2/multiply.test.js +1 -1
  92. package/src/maths/vec2/negate.test.js +1 -1
  93. package/src/maths/vec2/normal.js +1 -1
  94. package/src/maths/vec2/normalize.d.ts +1 -1
  95. package/src/maths/vec2/normalize.test.js +1 -1
  96. package/src/maths/vec2/rotate.test.js +1 -1
  97. package/src/maths/vec2/squaredLength.d.ts +1 -1
  98. package/src/maths/vec2/squaredLength.js +3 -3
  99. package/src/maths/vec2/subtract.test.js +1 -1
  100. package/src/maths/vec2/toString.js +1 -1
  101. package/src/maths/vec2/transform.test.js +1 -1
  102. package/src/maths/vec3/abs.d.ts +1 -1
  103. package/src/maths/vec3/add.test.js +1 -1
  104. package/src/maths/vec3/cross.test.js +1 -1
  105. package/src/maths/vec3/divide.test.js +1 -1
  106. package/src/maths/vec3/fromScalar.js +1 -1
  107. package/src/maths/vec3/fromVec2.d.ts +1 -1
  108. package/src/maths/vec3/fromVec2.js +3 -3
  109. package/src/maths/vec3/length.d.ts +1 -1
  110. package/src/maths/vec3/length.js +4 -4
  111. package/src/maths/vec3/lerp.test.js +1 -1
  112. package/src/maths/vec3/multiply.test.js +1 -1
  113. package/src/maths/vec3/negate.d.ts +1 -1
  114. package/src/maths/vec3/negate.test.js +1 -1
  115. package/src/maths/vec3/normalize.d.ts +1 -1
  116. package/src/maths/vec3/normalize.test.js +1 -1
  117. package/src/maths/vec3/rotateX.test.js +1 -1
  118. package/src/maths/vec3/rotateY.test.js +1 -1
  119. package/src/maths/vec3/rotateZ.test.js +1 -1
  120. package/src/maths/vec3/scale.test.js +1 -1
  121. package/src/maths/vec3/squaredLength.d.ts +1 -1
  122. package/src/maths/vec3/squaredLength.js +4 -4
  123. package/src/maths/vec3/subtract.test.js +1 -1
  124. package/src/maths/vec3/toString.js +1 -1
  125. package/src/maths/vec3/transform.test.js +1 -1
  126. package/src/maths/vec4/toString.js +1 -1
  127. package/src/maths/vec4/transform.test.js +1 -1
  128. package/src/measurements/measureBoundingSphere.js +4 -4
  129. package/src/measurements/measureCenterOfMass.js +1 -1
  130. package/src/measurements/measureCenterOfMass.test.js +2 -2
  131. package/src/operations/booleans/intersect.test.js +8 -0
  132. package/src/operations/booleans/mayOverlap.js +3 -3
  133. package/src/operations/booleans/retessellate.js +2 -2
  134. package/src/operations/booleans/scission.js +1 -1
  135. package/src/operations/booleans/scission.test.js +4 -4
  136. package/src/operations/booleans/subtract.js +1 -1
  137. package/src/operations/booleans/subtract.test.js +8 -0
  138. package/src/operations/booleans/trees/Node.js +10 -16
  139. package/src/operations/booleans/trees/PolygonTreeNode.js +13 -14
  140. package/src/operations/booleans/trees/Tree.js +1 -2
  141. package/src/operations/booleans/trees/splitPolygonByPlane.js +2 -3
  142. package/src/operations/booleans/union.test.js +28 -1
  143. package/src/operations/booleans/unionGeom3Sub.js +1 -1
  144. package/src/operations/expansions/expand.js +2 -2
  145. package/src/operations/expansions/expand.test.js +32 -55
  146. package/src/operations/expansions/expandShell.js +24 -18
  147. package/src/operations/expansions/offset.js +1 -1
  148. package/src/operations/expansions/offset.test.js +50 -89
  149. package/src/operations/expansions/offsetFromPoints.js +11 -6
  150. package/src/operations/extrusions/earcut/assignHoles.js +91 -0
  151. package/src/operations/extrusions/earcut/assignHoles.test.js +74 -0
  152. package/src/operations/extrusions/earcut/eliminateHoles.js +131 -0
  153. package/src/operations/extrusions/earcut/index.js +252 -0
  154. package/src/operations/extrusions/earcut/linkedList.js +58 -0
  155. package/src/operations/extrusions/earcut/linkedListSort.js +54 -0
  156. package/src/operations/extrusions/earcut/linkedPolygon.js +197 -0
  157. package/src/operations/extrusions/earcut/polygonHierarchy.js +64 -0
  158. package/src/operations/extrusions/earcut/triangle.js +16 -0
  159. package/src/operations/extrusions/extrudeFromSlices.js +10 -3
  160. package/src/operations/extrusions/extrudeFromSlices.test.js +47 -31
  161. package/src/operations/extrusions/extrudeLinear.js +10 -5
  162. package/src/operations/extrusions/extrudeLinear.test.js +91 -35
  163. package/src/operations/extrusions/extrudeLinearGeom2.js +5 -2
  164. package/src/operations/extrusions/extrudeLinearPath2.js +24 -0
  165. package/src/operations/extrusions/extrudeRectangular.js +1 -1
  166. package/src/operations/extrusions/extrudeRectangular.test.js +22 -15
  167. package/src/operations/extrusions/extrudeRotate.test.js +31 -27
  168. package/src/operations/extrusions/project.js +1 -1
  169. package/src/operations/extrusions/project.test.js +5 -5
  170. package/src/operations/extrusions/slice/calculatePlane.js +7 -4
  171. package/src/operations/extrusions/slice/isA.js +2 -2
  172. package/src/operations/extrusions/slice/repairSlice.js +47 -0
  173. package/src/operations/extrusions/slice/toPolygons.js +24 -60
  174. package/src/operations/hulls/hull.test.js +25 -2
  175. package/src/operations/hulls/hullChain.js +1 -1
  176. package/src/operations/hulls/hullChain.test.js +6 -4
  177. package/src/operations/hulls/hullGeom2.js +1 -1
  178. package/src/operations/hulls/hullPath2.js +6 -4
  179. package/src/operations/hulls/hullPath2.test.js +16 -0
  180. package/src/operations/hulls/hullPoints2.test.js +1 -1
  181. package/src/operations/modifiers/edges.js +1 -1
  182. package/src/operations/modifiers/generalize.js +1 -1
  183. package/src/operations/modifiers/generalize.test.js +6 -0
  184. package/src/operations/modifiers/snap.test.js +3 -3
  185. package/src/operations/transforms/align.d.ts +1 -1
  186. package/src/operations/transforms/align.test.js +12 -0
  187. package/src/operations/transforms/center.js +17 -17
  188. package/src/operations/transforms/center.test.js +12 -0
  189. package/src/operations/transforms/mirror.js +12 -12
  190. package/src/operations/transforms/mirror.test.js +16 -0
  191. package/src/operations/transforms/rotate.js +12 -12
  192. package/src/operations/transforms/rotate.test.js +10 -0
  193. package/src/operations/transforms/scale.js +19 -19
  194. package/src/operations/transforms/scale.test.js +15 -0
  195. package/src/operations/transforms/transform.js +3 -3
  196. package/src/operations/transforms/transform.test.js +5 -0
  197. package/src/operations/transforms/translate.js +14 -14
  198. package/src/operations/transforms/translate.test.js +16 -0
  199. package/src/primitives/arc.js +1 -1
  200. package/src/primitives/arc.test.js +11 -0
  201. package/src/primitives/circle.test.js +15 -9
  202. package/src/primitives/cube.test.js +3 -0
  203. package/src/primitives/cuboid.test.js +9 -24
  204. package/src/primitives/cylinder.test.js +7 -4
  205. package/src/primitives/cylinderElliptic.js +13 -6
  206. package/src/primitives/cylinderElliptic.test.js +72 -52
  207. package/src/primitives/ellipse.js +3 -1
  208. package/src/primitives/ellipse.test.js +14 -8
  209. package/src/primitives/ellipsoid.js +7 -5
  210. package/src/primitives/ellipsoid.test.js +84 -82
  211. package/src/primitives/geodesicSphere.d.ts +0 -1
  212. package/src/primitives/geodesicSphere.test.js +3 -0
  213. package/src/primitives/line.test.js +1 -0
  214. package/src/primitives/polygon.test.js +15 -10
  215. package/src/primitives/polyhedron.js +1 -1
  216. package/src/primitives/polyhedron.test.js +14 -42
  217. package/src/primitives/rectangle.test.js +3 -0
  218. package/src/primitives/roundedCuboid.test.js +5 -0
  219. package/src/primitives/roundedCylinder.js +6 -4
  220. package/src/primitives/roundedCylinder.test.js +40 -36
  221. package/src/primitives/roundedRectangle.test.js +5 -0
  222. package/src/primitives/sphere.test.js +52 -73
  223. package/src/primitives/square.test.js +3 -0
  224. package/src/primitives/star.test.js +6 -0
  225. package/src/primitives/torus.d.ts +0 -1
  226. package/src/primitives/torus.test.js +8 -1
  227. package/src/primitives/triangle.js +1 -1
  228. package/src/primitives/triangle.test.js +7 -0
  229. package/src/text/vectorText.js +2 -2
  230. package/src/utils/areAllShapesTheSameType.js +2 -2
  231. package/src/utils/areAllShapesTheSameType.test.js +17 -0
  232. package/src/utils/index.d.ts +1 -0
  233. package/src/utils/index.js +3 -1
  234. package/src/utils/padArrayToLength.js +1 -1
  235. package/src/utils/trigonometry.d.ts +2 -0
  236. package/src/utils/trigonometry.js +35 -0
  237. package/src/utils/trigonometry.test.js +25 -0
  238. package/test/helpers/nearlyEqual.js +4 -1
@@ -12,6 +12,7 @@ test('offset: offsetting a straight line produces expected geometry', (t) => {
12
12
  // offset it by 2.
13
13
  let offsetLinePath2 = offset({ delta: 2, corners: 'edge', segments: 8 }, linePath2)
14
14
  let offsetPoints = path2.toPoints(offsetLinePath2)
15
+ t.notThrows(() => path2.validate(offsetLinePath2))
15
16
  t.is(offsetPoints.length, 2)
16
17
  let boundingBox = measureBoundingBox(offsetLinePath2)
17
18
  t.true(comparePoints(boundingBox, [[2, 0, 0], [2, 10, 0]]), 'Unexpected bounding box: ' + JSON.stringify(boundingBox))
@@ -19,6 +20,7 @@ test('offset: offsetting a straight line produces expected geometry', (t) => {
19
20
  // offset it by -2.
20
21
  offsetLinePath2 = offset({ delta: -2, corners: 'edge', segments: 8 }, linePath2)
21
22
  offsetPoints = path2.toPoints(offsetLinePath2)
23
+ t.notThrows(() => path2.validate(offsetLinePath2))
22
24
  t.is(offsetPoints.length, 2)
23
25
  boundingBox = measureBoundingBox(offsetLinePath2)
24
26
  t.true(comparePoints(boundingBox, [[-2, 0, 0], [-2, 10, 0]]), 'Unexpected bounding box: ' + JSON.stringify(boundingBox))
@@ -27,6 +29,7 @@ test('offset: offsetting a straight line produces expected geometry', (t) => {
27
29
  linePath2 = path2.fromPoints({ closed: false }, points.reverse())
28
30
  offsetLinePath2 = offset({ delta: 2, corners: 'edge', segments: 8 }, linePath2)
29
31
  offsetPoints = path2.toPoints(offsetLinePath2)
32
+ t.notThrows(() => path2.validate(offsetLinePath2))
30
33
  t.is(offsetPoints.length, 2)
31
34
  boundingBox = measureBoundingBox(offsetLinePath2)
32
35
  t.true(comparePoints(boundingBox, [[-2, 0, 0], [-2, 10, 0]]), 'Unexpected bounding box: ' + JSON.stringify(boundingBox))
@@ -39,6 +42,7 @@ test('offset: offsetting a bent line produces expected geometry', (t) => {
39
42
  // offset it by 2.
40
43
  let offsetLinePath2 = offset({ delta: 2, corners: 'edge', segments: 8 }, linePath2)
41
44
  let offsetPoints = path2.toPoints(offsetLinePath2)
45
+ t.notThrows(() => path2.validate(offsetLinePath2))
42
46
  t.is(offsetPoints.length, 5)
43
47
  let boundingBox = measureBoundingBox(offsetLinePath2)
44
48
  t.true(comparePoints(boundingBox, [[2, 0, 0], [10, 8, 0]]), 'Unexpected bounding box: ' + JSON.stringify(boundingBox))
@@ -46,7 +50,8 @@ test('offset: offsetting a bent line produces expected geometry', (t) => {
46
50
  // offset it by -2.
47
51
  offsetLinePath2 = offset({ delta: -2, corners: 'edge', segments: 8 }, linePath2)
48
52
  offsetPoints = path2.toPoints(offsetLinePath2)
49
- t.is(offsetPoints.length, 7)
53
+ t.notThrows(() => path2.validate(offsetLinePath2))
54
+ t.is(offsetPoints.length, 5)
50
55
  boundingBox = measureBoundingBox(offsetLinePath2)
51
56
  t.true(comparePoints(boundingBox, [[-2, 0, 0], [10, 12, 0]]), 'Unexpected bounding box: ' + JSON.stringify(boundingBox))
52
57
  })
@@ -56,6 +61,7 @@ test('offset: offsetting a 2 segment straight line produces expected geometry',
56
61
  const linePath2 = path2.fromPoints({ closed: false }, points)
57
62
  const offsetLinePath2 = offset({ delta: 2, corners: 'edge', segments: 8 }, linePath2)
58
63
  const offsetPoints = path2.toPoints(offsetLinePath2)
64
+ t.notThrows(() => path2.validate(offsetLinePath2))
59
65
  t.is(offsetPoints.length, 3)
60
66
  const boundingBox = measureBoundingBox(offsetLinePath2)
61
67
  t.true(comparePoints(boundingBox, [[2, 0, 0], [2, 10, 0]]), 'Unexpected bounding box: ' + JSON.stringify(boundingBox))
@@ -71,6 +77,7 @@ test('offset (corners: chamfer): offset of a path2 produces expected offset path
71
77
  let pts = path2.toPoints(obs)
72
78
  let exp = [
73
79
  ]
80
+ t.notThrows(() => path2.validate(obs))
74
81
  t.true(comparePoints(pts, exp))
75
82
 
76
83
  // expand +
@@ -82,6 +89,7 @@ test('offset (corners: chamfer): offset of a path2 produces expected offset path
82
89
  [5.707106781186548, 0.7071067811865475],
83
90
  [0.7071067811865475, 5.707106781186548]
84
91
  ]
92
+ t.notThrows(() => path2.validate(obs))
85
93
  t.true(comparePoints(pts, exp))
86
94
 
87
95
  obs = offset({ delta: 1, corners: 'chamfer' }, closeline)
@@ -94,6 +102,7 @@ test('offset (corners: chamfer): offset of a path2 produces expected offset path
94
102
  [-1, 5],
95
103
  [-1, 6.123233995736766e-17]
96
104
  ]
105
+ t.notThrows(() => path2.validate(obs))
97
106
  t.true(comparePoints(pts, exp))
98
107
 
99
108
  // contract -
@@ -104,6 +113,7 @@ test('offset (corners: chamfer): offset of a path2 produces expected offset path
104
113
  [2.5857864376269046, 1],
105
114
  [-0.7071067811865475, 4.292893218813452]
106
115
  ]
116
+ t.notThrows(() => path2.validate(obs))
107
117
  t.true(comparePoints(pts, exp))
108
118
 
109
119
  obs = offset({ delta: -1, corners: 'chamfer' }, closeline)
@@ -113,6 +123,7 @@ test('offset (corners: chamfer): offset of a path2 produces expected offset path
113
123
  [2.5857864376269046, 1],
114
124
  [0.9999999999999996, 2.585786437626905]
115
125
  ]
126
+ t.notThrows(() => path2.validate(obs))
116
127
  t.true(comparePoints(pts, exp))
117
128
  })
118
129
 
@@ -124,48 +135,30 @@ test('offset (corners: edge): offset of a path2 produces expected offset path2',
124
135
  let pts = path2.toPoints(obs)
125
136
  let exp = [
126
137
  [-5, -6],
127
- [5, -6],
128
138
  [6, -6],
129
- [6, -5],
130
- [6, 5],
131
139
  [6, 6],
132
- [5, 6],
133
- [3, 6],
134
140
  [2, 6],
135
- [2, 5],
136
141
  [2, 1],
137
142
  [-2, 1],
138
- [-2, 5],
139
143
  [-1.9999999999999996, 6],
140
- [-3, 6],
141
144
  [-5, 6]
142
145
  ]
146
+ t.notThrows(() => path2.validate(obs))
143
147
  t.true(comparePoints(pts, exp))
144
148
 
145
149
  obs = offset({ delta: 1, corners: 'edge' }, closeline)
146
150
  pts = path2.toPoints(obs)
147
151
  exp = [
148
- [-6, -6],
149
- [-5, -6],
150
- [5, -6],
151
152
  [6, -6],
152
- [6, -5],
153
- [6, 5],
154
153
  [6, 6],
155
- [5, 6],
156
- [3, 6],
157
154
  [2, 6],
158
- [2, 5],
159
155
  [2, 1],
160
156
  [-2, 1],
161
- [-2, 5],
162
157
  [-1.9999999999999996, 6],
163
- [-3, 6],
164
- [-5, 6],
165
158
  [-6, 6],
166
- [-6, 5],
167
- [-6, -5]
159
+ [-6, -6]
168
160
  ]
161
+ t.notThrows(() => path2.validate(obs))
169
162
  t.true(comparePoints(pts, exp))
170
163
 
171
164
  obs = offset({ delta: -0.5, corners: 'edge' }, openline)
@@ -175,15 +168,12 @@ test('offset (corners: edge): offset of a path2 produces expected offset path2',
175
168
  [4.5, -4.5],
176
169
  [4.5, 4.5],
177
170
  [3.5, 4.5],
178
- [3.5, -3.061616997868383e-17],
179
171
  [3.4999999999999996, -0.5],
180
- [3, -0.5],
181
- [-3, -0.5],
182
172
  [-3.5, -0.4999999999999996],
183
- [-3.5, 3.061616997868383e-17],
184
173
  [-3.5, 4.5],
185
174
  [-5, 4.5]
186
175
  ]
176
+ t.notThrows(() => path2.validate(obs))
187
177
  t.true(comparePoints(pts, exp))
188
178
 
189
179
  obs = offset({ delta: -0.5, corners: 'edge' }, closeline)
@@ -193,15 +183,12 @@ test('offset (corners: edge): offset of a path2 produces expected offset path2',
193
183
  [4.5, -4.5],
194
184
  [4.5, 4.5],
195
185
  [3.5, 4.5],
196
- [3.5, -3.061616997868383e-17],
197
186
  [3.4999999999999996, -0.5],
198
- [3, -0.5],
199
- [-3, -0.5],
200
187
  [-3.5, -0.4999999999999996],
201
- [-3.5, 3.061616997868383e-17],
202
188
  [-3.5, 4.5],
203
189
  [-4.5, 4.5]
204
190
  ]
191
+ t.notThrows(() => path2.validate(obs))
205
192
  t.true(comparePoints(pts, exp))
206
193
  })
207
194
 
@@ -237,6 +224,7 @@ test('offset (corners: round): offset of a path2 produces expected offset path2'
237
224
  [-3, 6],
238
225
  [-5, 6]
239
226
  ]
227
+ t.notThrows(() => path2.validate(obs))
240
228
  t.true(comparePoints(pts, exp))
241
229
 
242
230
  obs = offset({ delta: 1, corners: 'round', segments: 16 }, closeline)
@@ -275,6 +263,7 @@ test('offset (corners: round): offset of a path2 produces expected offset path2'
275
263
  [-6, 5],
276
264
  [-6, -5]
277
265
  ]
266
+ t.notThrows(() => path2.validate(obs))
278
267
  t.true(comparePoints(pts, exp))
279
268
  })
280
269
 
@@ -317,6 +306,7 @@ test('offset (corners: round): offset of a CW path2 produces expected offset pat
317
306
  [5, -6],
318
307
  [-5, -6]
319
308
  ]
309
+ t.notThrows(() => path2.validate(obs))
320
310
  t.true(comparePoints(pts, exp))
321
311
  })
322
312
 
@@ -329,6 +319,7 @@ test('offset (options): offsetting of a simple geom2 produces expected offset ge
329
319
  let pts = geom2.toPoints(obs)
330
320
  let exp = [
331
321
  ]
322
+ t.notThrows(() => geom2.validate(obs))
332
323
  t.true(comparePoints(pts, exp))
333
324
 
334
325
  // expand +
@@ -350,6 +341,7 @@ test('offset (options): offsetting of a simple geom2 produces expected offset ge
350
341
  [-6, 5],
351
342
  [-6, -5]
352
343
  ]
344
+ t.notThrows(() => geom2.validate(obs))
353
345
  t.true(comparePoints(pts, exp))
354
346
 
355
347
  // contract -
@@ -367,33 +359,23 @@ test('offset (options): offsetting of a simple geom2 produces expected offset ge
367
359
  [-3.5, 4.5],
368
360
  [-4.5, 4.5]
369
361
  ]
362
+ t.notThrows(() => geom2.validate(obs))
370
363
  t.true(comparePoints(pts, exp))
371
364
 
372
365
  // segments 1 - sharp points at corner
373
366
  obs = offset({ delta: 1, corners: 'edge' }, geometry)
374
367
  pts = geom2.toPoints(obs)
375
368
  exp = [
376
- [-6, -6],
377
- [-5, -6],
378
- [5, -6],
379
369
  [6, -6],
380
- [6, -5],
381
- [6, 5],
382
370
  [6, 6],
383
- [5, 6],
384
- [3, 6],
385
371
  [2, 6],
386
- [2, 5],
387
372
  [2, 1],
388
373
  [-2, 1],
389
- [-2, 5],
390
374
  [-1.9999999999999996, 6],
391
- [-3, 6],
392
- [-5, 6],
393
375
  [-6, 6],
394
- [-6, 5],
395
- [-6, -5]
376
+ [-6, -6]
396
377
  ]
378
+ t.notThrows(() => geom2.validate(obs))
397
379
  t.true(comparePoints(pts, exp))
398
380
 
399
381
  // segments 16 - rounded corners
@@ -417,83 +399,61 @@ test('offset (options): offsetting of a simple geom2 produces expected offset ge
417
399
  [-3.5, 4.5],
418
400
  [-4.5, 4.5]
419
401
  ]
402
+ t.notThrows(() => geom2.validate(obs))
420
403
  t.true(comparePoints(pts, exp))
421
404
  })
422
405
 
423
406
  test('offset (options): offsetting of a complex geom2 produces expected offset geom2', (t) => {
424
407
  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]]
408
+ [[-75, 75], [-75, -75]],
409
+ [[-75, -75], [75, -75]],
410
+ [[75, -75], [75, 75]],
411
+ [[-40, 75], [-75, 75]],
412
+ [[75, 75], [40, 75]],
413
+ [[40, 75], [40, 0]],
414
+ [[40, 0], [-40, 0]],
415
+ [[-40, 0], [-40, 75]],
416
+ [[15, -10], [15, -40]],
417
+ [[-15, -10], [15, -10]],
418
+ [[-15, -40], [-15, -10]],
419
+ [[-8, -40], [-15, -40]],
420
+ [[15, -40], [8, -40]],
421
+ [[-8, -25], [-8, -40]],
422
+ [[8, -25], [-8, -25]],
423
+ [[8, -40], [8, -25]],
424
+ [[-2, -15], [-2, -19]],
425
+ [[-2, -19], [2, -19]],
426
+ [[2, -19], [2, -15]],
427
+ [[2, -15], [-2, -15]]
445
428
  ])
446
429
 
447
430
  // expand +
448
431
  const obs = offset({ delta: 2, corners: 'edge' }, geometry)
449
432
  const pts = geom2.toPoints(obs)
450
433
  const exp = [
451
- [-77, -77],
452
- [-75, -77],
453
- [75, -77],
454
434
  [77, -77],
455
- [77, -75],
456
- [77, 75],
457
435
  [77, 77],
458
- [75, 77],
459
- [40, 77],
460
436
  [38, 77],
461
- [38, 75],
462
437
  [38, 2],
463
438
  [-38, 2],
464
- [-38, 75],
465
439
  [-37.99999999999999, 77],
466
- [-40, 77],
467
- [-75, 77],
468
440
  [-77, 77],
469
- [-77, 75],
470
441
  [13, -12],
471
442
  [13, -38],
472
443
  [10, -38],
473
- [10, -25],
474
444
  [10, -23],
475
- [8, -23],
476
- [-8, -23],
477
445
  [-10, -23],
478
- [-10, -25],
479
446
  [-10, -38],
480
447
  [-13, -38],
481
448
  [-13, -12],
482
- [-4, -19],
483
449
  [-4, -21],
484
- [-2, -21],
485
- [1.9999999999999998, -21],
486
450
  [3.9999999999999996, -21],
487
- [4, -19],
488
- [4, -15],
489
451
  [4, -13],
490
- [2, -13],
491
- [-1.9999999999999998, -13],
492
452
  [-4, -13],
493
- [-4, -15],
494
- [-77, -75]
453
+ [-77, -77]
495
454
  ]
496
- t.is(pts.length, 44)
455
+ t.notThrows(() => geom2.validate(obs))
456
+ t.is(pts.length, 20)
497
457
  t.true(comparePoints(pts, exp))
498
458
  })
499
459
 
@@ -537,6 +497,7 @@ test('offset (options): offsetting of round geom2 produces expected offset geom2
537
497
  [6.7105900605102855, -6.710590060510285],
538
498
  [8.767810140100096, -3.6317399864658024]
539
499
  ]
500
+ t.notThrows(() => geom2.validate(obs))
540
501
  t.is(pts.length, 16)
541
502
  t.true(comparePoints(pts, exp))
542
503
  })
@@ -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,91 @@
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
+
35
+ // for each hole, determine what solids it is inside of
36
+ const children = [] // child holes of solid[i]
37
+ const parents = [] // parent solids of hole[i]
38
+ solids.forEach((s, i) => {
39
+ const solid = outlines[s]
40
+ children[i] = []
41
+ holes.forEach((h, j) => {
42
+ const hole = outlines[h]
43
+ // check if a point of hole j is inside solid i
44
+ if (arePointsInside([hole[0]], { vertices: solid })) {
45
+ children[i].push(h)
46
+ if (!parents[j]) parents[j] = []
47
+ parents[j].push(i)
48
+ }
49
+ })
50
+ })
51
+
52
+ // check if holes have multiple parents and choose one with fewest children
53
+ holes.forEach((h, j) => {
54
+ // ensure at least one parent exists
55
+ if (parents[j] && parents[j].length > 1) {
56
+ // the solid directly containing this hole
57
+ const directParent = minIndex(parents[j], (p) => children[p].length)
58
+ parents[j].forEach((p, i) => {
59
+ if (i !== directParent) {
60
+ // Remove hole from skip level parents
61
+ children[p] = children[p].filter((c) => c !== h)
62
+ }
63
+ })
64
+ }
65
+ })
66
+
67
+ // map indices back to points
68
+ return children.map((holes, i) => ({
69
+ solid: outlines[solids[i]],
70
+ holes: holes.map((h) => outlines[h])
71
+ }))
72
+ }
73
+
74
+ /*
75
+ * Find the item in the list with smallest score(item).
76
+ * If the list is empty, return undefined.
77
+ */
78
+ const minIndex = (list, score) => {
79
+ let bestIndex
80
+ let best
81
+ list.forEach((item, index) => {
82
+ const value = score(item)
83
+ if (best === undefined || value < best) {
84
+ bestIndex = index
85
+ best = value
86
+ }
87
+ })
88
+ return bestIndex
89
+ }
90
+
91
+ module.exports = assignHoles
@@ -0,0 +1,74 @@
1
+ const test = require('ava')
2
+
3
+ const { subtract, union } = require('../../../operations/booleans')
4
+ const square = require('../../../primitives/square')
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
+ square({ size: 6 }),
24
+ square({ size: 4 })
25
+ )
26
+ const obs1 = assignHoles(geometry)
27
+ t.deepEqual(obs1, exp1)
28
+ })
29
+
30
+ test('slice: assignHoles() should handle nested holes', (t) => {
31
+ const geometry = union(
32
+ subtract(
33
+ square({ size: 6 }),
34
+ square({ size: 4 })
35
+ ),
36
+ subtract(
37
+ square({ size: 10 }),
38
+ square({ size: 8 })
39
+ )
40
+ )
41
+ const obs1 = assignHoles(geometry)
42
+
43
+ const exp1 = [
44
+ {
45
+ solid: [
46
+ [-3.0000006060444444, -3.0000006060444444],
47
+ [3.0000006060444444, -3.0000006060444444],
48
+ [3.0000006060444444, 3.0000006060444444],
49
+ [-3.0000006060444444, 3.0000006060444444]
50
+ ],
51
+ holes: [[
52
+ [-2.0000248485333336, 2.0000248485333336],
53
+ [2.0000248485333336, 2.0000248485333336],
54
+ [2.0000248485333336, -2.0000248485333336],
55
+ [-2.0000248485333336, -2.0000248485333336]
56
+ ]]
57
+ },
58
+ {
59
+ solid: [
60
+ [-5.000025454577778, -5.000025454577778],
61
+ [5.000025454577778, -5.000025454577778],
62
+ [5.000025454577778, 5.000025454577778],
63
+ [-5.000025454577778, 5.000025454577778]
64
+ ],
65
+ holes: [[
66
+ [-3.9999763635555556, 3.9999763635555556],
67
+ [3.9999763635555556, 3.9999763635555556],
68
+ [3.9999763635555556, -3.9999763635555556],
69
+ [-3.9999763635555556, -3.9999763635555556]
70
+ ]]
71
+ }
72
+ ]
73
+ t.deepEqual(obs1, exp1)
74
+ })
@@ -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