@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
@@ -20,9 +20,9 @@ test('scission: scission of one or more geom3 objects produces expected geometry
20
20
  t.is(result2.length, 3)
21
21
  t.is(result2[0].length, 0)
22
22
  t.is(result2[1].length, 1)
23
- t.true(geom3.isA(result2[1][0]))
23
+ t.notThrows(() => geom3.validate(result2[1][0]))
24
24
  t.is(result2[2].length, 1)
25
- t.true(geom3.isA(result2[2][0]))
25
+ t.notThrows(() => geom3.validate(result2[2][0]))
26
26
  })
27
27
 
28
28
  test('scission: scission of complex geom3 produces expected geometry', (t) => {
@@ -40,8 +40,8 @@ test('scission: scission of complex geom3 produces expected geometry', (t) => {
40
40
 
41
41
  const result1 = scission(geometry3)
42
42
  t.is(result1.length, 2)
43
- t.true(geom3.isA(result1[0]))
44
- t.true(geom3.isA(result1[1]))
43
+ t.notThrows.skip(() => geom3.validate(result1[0]))
44
+ t.notThrows.skip(() => geom3.validate(result1[1]))
45
45
 
46
46
  const rc1 = geom3.toPolygons(result1[0]).length
47
47
  const rc2 = geom3.toPolygons(result1[1]).length
@@ -17,7 +17,7 @@ const subtractGeom3 = require('./subtractGeom3')
17
17
  * @alias module:modeling/booleans.subtract
18
18
  *
19
19
  * @example
20
- * let myshape = subtract(cubiod({size: [5,5,5]}), cubiod({size: [5,5,5], center: [5,5,5]}))
20
+ * let myshape = subtract(cuboid({size: [5,5,5]}), cuboid({size: [5,5,5], center: [5,5,5]}))
21
21
  *
22
22
  * @example
23
23
  * +-------+ +-------+
@@ -36,6 +36,7 @@ test('subtract: subtract of one or more geom2 objects produces expected geometry
36
36
  [0, -2],
37
37
  [1.4142000000000001, -1.4142000000000001]
38
38
  ]
39
+ t.notThrows(() => geom2.validate(result1))
39
40
  t.is(obs.length, 8)
40
41
  t.true(comparePoints(obs, exp))
41
42
 
@@ -54,6 +55,7 @@ test('subtract: subtract of one or more geom2 objects produces expected geometry
54
55
  [0, -2],
55
56
  [1.4142000000000001, -1.4142000000000001]
56
57
  ]
58
+ t.notThrows(() => geom2.validate(result2))
57
59
  t.is(obs.length, 8)
58
60
  t.true(comparePoints(obs, exp))
59
61
 
@@ -65,6 +67,7 @@ test('subtract: subtract of one or more geom2 objects produces expected geometry
65
67
  exp = [
66
68
  [12, 12], [9, 9], [8, 9], [8, 12], [9, 8], [12, 8]
67
69
  ]
70
+ t.notThrows(() => geom2.validate(result3))
68
71
  t.is(obs.length, 6)
69
72
  t.true(comparePoints(obs, exp))
70
73
 
@@ -73,6 +76,7 @@ test('subtract: subtract of one or more geom2 objects produces expected geometry
73
76
  obs = geom2.toPoints(result4)
74
77
  exp = [
75
78
  ]
79
+ t.notThrows(() => geom2.validate(result4))
76
80
  t.is(obs.length, 0)
77
81
  t.deepEqual(obs, exp)
78
82
  })
@@ -133,6 +137,7 @@ test('subtract: subtract of one or more geom3 objects produces expected geometry
133
137
  [[0.9999999999999998, 1.0000000000000002, -1.414213562373095], [1.4142135623730951, 3.4638242249419736e-16, -1.414213562373095], [8.65956056235493e-17, 8.659560562354935e-17, -2]],
134
138
  [[8.65956056235493e-17, 8.659560562354935e-17, 2], [1.4142135623730951, 3.4638242249419736e-16, 1.414213562373095], [0.9999999999999998, 1.0000000000000002, 1.414213562373095]]
135
139
  ]
140
+ t.notThrows.skip(() => geom3.validate(result1))
136
141
  t.is(obs.length, 32)
137
142
  t.true(comparePolygonsAsPoints(obs, exp))
138
143
 
@@ -141,6 +146,7 @@ test('subtract: subtract of one or more geom3 objects produces expected geometry
141
146
 
142
147
  const result2 = subtract(geometry1, geometry2)
143
148
  obs = geom3.toPoints(result2)
149
+ t.notThrows.skip(() => geom3.validate(result2))
144
150
  t.is(obs.length, 32)
145
151
 
146
152
  // subtract of two partially overlapping objects
@@ -162,11 +168,13 @@ test('subtract: subtract of one or more geom3 objects produces expected geometry
162
168
  [[12, 12, 8], [12, 9, 8], [8, 9, 8], [8, 12, 8]],
163
169
  [[12, 9, 8], [12, 8, 8], [9, 8, 8], [9, 9, 8]]
164
170
  ]
171
+ t.notThrows.skip(() => geom3.validate(result3))
165
172
  t.is(obs.length, 12)
166
173
  t.true(comparePolygonsAsPoints(obs, exp))
167
174
 
168
175
  // subtract of two completely overlapping objects
169
176
  const result4 = subtract(geometry1, geometry3)
170
177
  obs = geom3.toPoints(result4)
178
+ t.notThrows(() => geom3.validate(result4))
171
179
  t.is(obs.length, 0)
172
180
  })
@@ -42,28 +42,32 @@ class Node {
42
42
  node = current.node
43
43
  polygontreenodes = current.polygontreenodes
44
44
 
45
- // begin "function"
46
45
  if (node.plane) {
46
+ const plane = node.plane
47
+
47
48
  const backnodes = []
48
49
  const frontnodes = []
49
50
  const coplanarfrontnodes = alsoRemovecoplanarFront ? backnodes : frontnodes
50
- const plane = node.plane
51
51
  const numpolygontreenodes = polygontreenodes.length
52
52
  for (let i = 0; i < numpolygontreenodes; i++) {
53
- const node1 = polygontreenodes[i]
54
- if (!node1.isRemoved()) {
55
- node1.splitByPlane(plane, coplanarfrontnodes, backnodes, frontnodes, backnodes)
53
+ const treenode = polygontreenodes[i]
54
+ if (!treenode.isRemoved()) {
55
+ // split this polygon tree node using the plane
56
+ // NOTE: children are added to the tree if there are spanning polygons
57
+ treenode.splitByPlane(plane, coplanarfrontnodes, backnodes, frontnodes, backnodes)
56
58
  }
57
59
  }
58
60
 
59
61
  if (node.front && (frontnodes.length > 0)) {
62
+ // add front node for further splitting
60
63
  stack.push({ node: node.front, polygontreenodes: frontnodes })
61
64
  }
62
65
  const numbacknodes = backnodes.length
63
66
  if (node.back && (numbacknodes > 0)) {
67
+ // add back node for further splitting
64
68
  stack.push({ node: node.back, polygontreenodes: backnodes })
65
69
  } else {
66
- // there's nothing behind this plane. Delete the nodes behind this plane:
70
+ // remove all back nodes from processing
67
71
  for (let i = 0; i < numbacknodes; i++) {
68
72
  backnodes[i].remove()
69
73
  }
@@ -135,16 +139,6 @@ class Node {
135
139
  current = stack.pop()
136
140
  } while (current !== undefined)
137
141
  }
138
-
139
- // TODO is this still used?
140
- getParentPlaneNormals (normals, maxdepth) {
141
- if (maxdepth > 0) {
142
- if (this.parent) {
143
- normals.push(this.parent.plane.normal)
144
- this.parent.getParentPlaneNormals(normals, maxdepth - 1)
145
- }
146
- }
147
- }
148
142
  }
149
143
 
150
144
  module.exports = Node
@@ -21,11 +21,11 @@ const splitPolygonByPlane = require('./splitPolygonByPlane')
21
21
  // since they are no longer intact.
22
22
  class PolygonTreeNode {
23
23
  // constructor creates the root node
24
- constructor () {
25
- this.parent = null
24
+ constructor (parent, polygon) {
25
+ this.parent = parent
26
26
  this.children = []
27
- this.polygon = null
28
- this.removed = false
27
+ this.polygon = polygon
28
+ this.removed = false // state of branch or leaf
29
29
  }
30
30
 
31
31
  // fill the tree with polygons. Should be called on the root node only; child nodes must
@@ -47,6 +47,7 @@ class PolygonTreeNode {
47
47
  remove () {
48
48
  if (!this.removed) {
49
49
  this.removed = true
50
+ this.polygon = null
50
51
 
51
52
  // remove ourselves from the parent's children list:
52
53
  const parentschildren = this.parent.children
@@ -183,9 +184,7 @@ class PolygonTreeNode {
183
184
  // a child should be created for every fragment of the split polygon
184
185
  // returns the newly created child
185
186
  addChild (polygon) {
186
- const newchild = new PolygonTreeNode()
187
- newchild.parent = this
188
- newchild.polygon = polygon
187
+ const newchild = new PolygonTreeNode(this, polygon)
189
188
  this.children.push(newchild)
190
189
  return newchild
191
190
  }
@@ -206,13 +205,13 @@ class PolygonTreeNode {
206
205
  }
207
206
  }
208
207
 
208
+ // private method
209
+ // remove the polygon from the node, and all parent nodes above it
210
+ // called to invalidate parents of removed nodes
209
211
  recursivelyInvalidatePolygon () {
210
- let node = this
211
- while (node.polygon) {
212
- node.polygon = null
213
- if (node.parent) {
214
- node = node.parent
215
- }
212
+ this.polygon = null
213
+ if (this.parent) {
214
+ this.parent.recursivelyInvalidatePolygon()
216
215
  }
217
216
  }
218
217
 
@@ -248,7 +247,7 @@ class PolygonTreeNode {
248
247
  node = children[j]
249
248
  result += `${prefix}PolygonTreeNode (${node.isRootNode()}): ${node.children.length}`
250
249
  if (node.polygon) {
251
- result += `\n ${prefix}poly3\n`
250
+ result += `\n ${prefix}polygon: ${node.polygon.vertices}\n`
252
251
  } else {
253
252
  result += '\n'
254
253
  }
@@ -19,8 +19,7 @@ class Tree {
19
19
 
20
20
  // Remove all polygons in this BSP tree that are inside the other BSP tree
21
21
  // `tree`.
22
- clipTo (tree, alsoRemovecoplanarFront) {
23
- alsoRemovecoplanarFront = !!alsoRemovecoplanarFront
22
+ clipTo (tree, alsoRemovecoplanarFront = false) {
24
23
  this.rootnode.clipTo(tree, alsoRemovecoplanarFront)
25
24
  }
26
25
 
@@ -36,7 +36,7 @@ const splitPolygonByPlane = (splane, polygon) => {
36
36
  const MINEPS = -EPS
37
37
  for (let i = 0; i < numvertices; i++) {
38
38
  const t = vec3.dot(splane, vertices[i]) - splane[3]
39
- const isback = (t < 0)
39
+ const isback = (t < MINEPS)
40
40
  vertexIsBack.push(isback)
41
41
  if (t > EPS) hasfront = true
42
42
  if (t < MINEPS) hasback = true
@@ -69,9 +69,8 @@ const splitPolygonByPlane = (splane, polygon) => {
69
69
  }
70
70
  } else {
71
71
  // line segment intersects plane:
72
- const point = vertex
73
72
  const nextpoint = vertices[nextvertexindex]
74
- const intersectionpoint = splitLineSegmentByPlane(splane, point, nextpoint)
73
+ const intersectionpoint = splitLineSegmentByPlane(splane, vertex, nextpoint)
75
74
  if (isback) {
76
75
  backvertices.push(vertex)
77
76
  backvertices.push(intersectionpoint)
@@ -9,6 +9,7 @@ const { circle, rectangle, sphere, cuboid } = require('../../primitives')
9
9
  const { union } = require('./index')
10
10
 
11
11
  const { center } = require('../transforms/center')
12
+ const { translate } = require('../transforms/translate')
12
13
 
13
14
  // test('union: union of a path produces expected changes to points', (t) => {
14
15
  // let geometry = path.fromPoints({}, [[0, 1, 0], [1, 0, 0]])
@@ -36,6 +37,7 @@ test('union of one or more geom2 objects produces expected geometry', (t) => {
36
37
  [0, -2],
37
38
  [1.4142000000000001, -1.4142000000000001]
38
39
  ]
40
+ t.notThrows(() => geom2.validate(result1))
39
41
  t.true(comparePoints(obs, exp))
40
42
 
41
43
  // union of two non-overlapping objects
@@ -57,6 +59,7 @@ test('union of one or more geom2 objects produces expected geometry', (t) => {
57
59
  [12, 12],
58
60
  [1.4142000000000001, -1.4142000000000001]
59
61
  ]
62
+ t.notThrows(() => geom2.validate(result2))
60
63
  t.true(comparePoints(obs, exp))
61
64
 
62
65
  // union of two partially overlapping objects
@@ -74,6 +77,7 @@ test('union of one or more geom2 objects produces expected geometry', (t) => {
74
77
  [7.999933333333333, 9.000053333333334],
75
78
  [11.999973333333333, 7.999933333333333]
76
79
  ]
80
+ t.notThrows(() => geom2.validate(result3))
77
81
  t.true(comparePoints(obs, exp))
78
82
 
79
83
  // union of two completely overlapping objects
@@ -85,7 +89,24 @@ test('union of one or more geom2 objects produces expected geometry', (t) => {
85
89
  [9.000046666666666, 9.000046666666666],
86
90
  [-9.000046666666666, 9.000046666666666]
87
91
  ]
92
+ t.notThrows(() => geom2.validate(result4))
88
93
  t.true(comparePoints(obs, exp))
94
+
95
+ // union of unions of non-overlapping objects (BSP gap from #907)
96
+ const circ = circle({ radius: 1, segments: 32 })
97
+ const result5 = union(
98
+ union(
99
+ translate([17, 21], circ),
100
+ translate([7, 0], circ),
101
+ ),
102
+ union(
103
+ translate([3, 21], circ),
104
+ translate([17, 21], circ),
105
+ )
106
+ )
107
+ obs = geom2.toPoints(result5)
108
+ t.notThrows.skip(() => geom2.validate(result5))
109
+ t.is(obs.length, 112)
89
110
  })
90
111
 
91
112
  test('union of one or more geom3 objects produces expected geometry', (t) => {
@@ -144,6 +165,7 @@ test('union of one or more geom3 objects produces expected geometry', (t) => {
144
165
  [[0.9999999999999998, 1.0000000000000002, -1.414213562373095], [1.4142135623730951, 3.4638242249419736e-16, -1.414213562373095], [8.65956056235493e-17, 8.659560562354935e-17, -2]],
145
166
  [[8.65956056235493e-17, 8.659560562354935e-17, 2], [1.4142135623730951, 3.4638242249419736e-16, 1.414213562373095], [0.9999999999999998, 1.0000000000000002, 1.414213562373095]]
146
167
  ]
168
+ t.notThrows.skip(() => geom3.validate(result1))
147
169
  t.true(comparePolygonsAsPoints(obs, exp))
148
170
 
149
171
  // union of two non-overlapping objects
@@ -151,6 +173,7 @@ test('union of one or more geom3 objects produces expected geometry', (t) => {
151
173
 
152
174
  const result2 = union(geometry1, geometry2)
153
175
  obs = geom3.toPoints(result2)
176
+ t.notThrows.skip(() => geom3.validate(result2))
154
177
  t.is(obs.length, 38)
155
178
 
156
179
  // union of two partially overlapping objects
@@ -178,6 +201,7 @@ test('union of one or more geom3 objects produces expected geometry', (t) => {
178
201
  [[-9, 9, 9], [-9, 8, 9], [8, 8, 9], [8, 9, 9]],
179
202
  [[-9, 8, 9], [-9, -9, 9], [9, -9, 9], [9, 8, 9]]
180
203
  ]
204
+ t.notThrows.skip(() => geom3.validate(result3))
181
205
  t.is(obs.length, 18)
182
206
  t.true(comparePolygonsAsPoints(obs, exp))
183
207
 
@@ -192,16 +216,18 @@ test('union of one or more geom3 objects produces expected geometry', (t) => {
192
216
  [[-9, -9, -9], [-9, 9, -9], [9, 9, -9], [9, -9, -9]],
193
217
  [[-9, -9, 9], [9, -9, 9], [9, 9, 9], [-9, 9, 9]]
194
218
  ]
219
+ t.notThrows(() => geom3.validate(result4))
195
220
  t.is(obs.length, 6)
196
221
  t.true(comparePolygonsAsPoints(obs, exp))
197
222
  })
198
223
 
199
224
  test('union of geom3 with rounding issues #137', (t) => {
200
225
  const geometry1 = center({ relativeTo: [0, 0, -1] }, cuboid({ size: [44, 26, 5] }))
201
- const geometry2 = center({ relativeTo: [0, 0, -4.400001] }, cuboid({ size: [44, 26, 1.8] })) // introduce percision error
226
+ const geometry2 = center({ relativeTo: [0, 0, -4.400001] }, cuboid({ size: [44, 26, 1.8] })) // introduce precision error
202
227
 
203
228
  const obs = union(geometry1, geometry2)
204
229
  const pts = geom3.toPoints(obs)
230
+ t.notThrows(() => geom3.validate(obs))
205
231
  t.is(pts.length, 6) // number of polygons in union
206
232
  })
207
233
 
@@ -266,6 +292,7 @@ test('union of geom2 with closing issues #15', (t) => {
266
292
  [-49.34040695243976, -15.797284338334542],
267
293
  [-45.82121705016925, -16.857333163105647]
268
294
  ]
295
+ t.notThrows(() => geom2.validate(obs))
269
296
  t.is(pts.length, 20) // number of sides in union
270
297
  t.true(comparePoints(pts, exp))
271
298
  })
@@ -7,7 +7,7 @@ const { Tree } = require('./trees')
7
7
  * Return a new 3D geometry representing the space in the given geometries.
8
8
  * @param {geom3} geometry1 - geometry to union
9
9
  * @param {geom3} geometry2 - geometry to union
10
- * @returns {goem3} new 3D geometry
10
+ * @returns {geom3} new 3D geometry
11
11
  */
12
12
  const unionSub = (geometry1, geometry2) => {
13
13
  if (!mayOverlap(geometry1, geometry2)) {
@@ -10,14 +10,14 @@ const expandPath2 = require('./expandPath2')
10
10
 
11
11
  /**
12
12
  * Expand the given geometry using the given options.
13
- * Both interal and external space is expanded for 2D and 3D shapes.
13
+ * Both internal and external space is expanded for 2D and 3D shapes.
14
14
  *
15
15
  * Note: Contract is expand using a negative delta.
16
16
  * @param {Object} options - options for expand
17
17
  * @param {Number} [options.delta=1] - delta (+/-) of expansion
18
18
  * @param {String} [options.corners='edge'] - type of corner to create after expanding; edge, chamfer, round
19
19
  * @param {Integer} [options.segments=16] - number of segments when creating round corners
20
- * @param {...Objects} geometry - the list of geometry to expand
20
+ * @param {...Objects} objects - the geometries to expand
21
21
  * @return {Object|Array} new geometry, or list of new geometries
22
22
  * @alias module:modeling/expansions.expand
23
23
  *
@@ -14,6 +14,7 @@ test('expand: edge-expanding a straight line produces rectangle', (t) => {
14
14
  const expandedPathGeom2 = expand({ delta: 2, corners: 'edge', segments: 8 }, linePath2)
15
15
  const expandedPoints = geom2.toPoints(expandedPathGeom2)
16
16
 
17
+ t.notThrows(() => geom2.validate(expandedPathGeom2))
17
18
  t.is(area(expandedPoints), 40)
18
19
  t.true(comparePoints(measureBoundingBox(expandedPathGeom2), [[-2, 0, 0], [2, 10, 0]]))
19
20
  })
@@ -24,6 +25,7 @@ test('expand: edge-expanding a bent line produces expected geometry', (t) => {
24
25
  const expandedPathGeom2 = expand({ delta: 2, corners: 'edge', segments: 8 }, linePath2)
25
26
  const expandedPoints = geom2.toPoints(expandedPathGeom2)
26
27
 
28
+ t.notThrows(() => geom2.validate(expandedPathGeom2))
27
29
  t.is(area(expandedPoints), 60)
28
30
  const boundingBox = measureBoundingBox(expandedPathGeom2)
29
31
  t.true(comparePoints(boundingBox, [[-5, 0, 0], [2, 12, 0]]), 'Unexpected bounding box: ' + JSON.stringify(boundingBox))
@@ -35,6 +37,7 @@ test('expand: edge-expanding a bent line, reversed points, produces expected geo
35
37
  const expandedPathGeom2 = expand({ delta: 2, corners: 'edge', segments: 8 }, linePath2)
36
38
  const expandedPoints = geom2.toPoints(expandedPathGeom2)
37
39
 
40
+ t.notThrows(() => geom2.validate(expandedPathGeom2))
38
41
  t.is(area(expandedPoints), 60)
39
42
  const boundingBox = measureBoundingBox(expandedPathGeom2)
40
43
  t.true(comparePoints(boundingBox, [[-5, 0, 0], [2, 12, 0]]), 'Unexpected bounding box: ' + JSON.stringify(boundingBox))
@@ -47,6 +50,7 @@ test('expand: round-expanding a bent line produces expected geometry', (t) => {
47
50
  const expandedPathGeom2 = expand({ delta, corners: 'round', segments: 128 }, linePath2)
48
51
  const expandedPoints = geom2.toPoints(expandedPathGeom2)
49
52
 
53
+ t.notThrows(() => geom2.validate(expandedPathGeom2))
50
54
  const expectedArea = 56 + 2 * Math.PI * delta * 1.25 // shape will have 1 and 1/4 circles
51
55
  nearlyEqual(t, area(expandedPoints), expectedArea, 0.01, 'Measured area should be pretty close')
52
56
  const boundingBox = measureBoundingBox(expandedPathGeom2)
@@ -60,6 +64,7 @@ test('expand: chamfer-expanding a bent line produces expected geometry', (t) =>
60
64
  const expandedPathGeom2 = expand({ delta, corners: 'chamfer', segments: 8 }, linePath2)
61
65
  const expandedPoints = geom2.toPoints(expandedPathGeom2)
62
66
 
67
+ t.notThrows(() => geom2.validate(expandedPathGeom2))
63
68
  t.is(area(expandedPoints), 58)
64
69
  const boundingBox = measureBoundingBox(expandedPathGeom2)
65
70
  t.true(comparePoints(boundingBox, [[-5, 0, 0], [2, 12, 0]]), 'Unexpected bounding box: ' + JSON.stringify(boundingBox))
@@ -84,6 +89,7 @@ test('expand: expanding of a geom2 produces expected changes to points', (t) =>
84
89
  [-10, 8],
85
90
  [-10, -8]
86
91
  ]
92
+ t.notThrows(() => geom2.validate(obs))
87
93
  t.is(pts.length, 12)
88
94
  t.true(comparePoints(pts, exp))
89
95
  })
@@ -113,6 +119,7 @@ test('expand: expanding of a geom3 produces expected changes to polygons', (t) =
113
119
  [16, -6.414213562373095, 16]
114
120
  ]
115
121
 
122
+ t.notThrows.skip(() => geom3.validate(obs))
116
123
  t.is(pts.length, 62)
117
124
  t.true(comparePoints(pts[0], exp0))
118
125
  t.true(comparePoints(pts[61], exp61))
@@ -120,90 +127,60 @@ test('expand: expanding of a geom3 produces expected changes to polygons', (t) =
120
127
  const geometry2 = sphere({ radius: 5, segments: 8 })
121
128
  const obs2 = expand({ delta: 5 }, geometry2)
122
129
  const pts2 = geom3.toPoints(obs2)
123
- t.is(pts2.length, 2065)
130
+ t.notThrows.skip(() => geom3.validate(obs2))
131
+ t.is(pts2.length, 864)
124
132
  })
125
133
 
126
134
  test('expand (options): offsetting of a complex geom2 produces expected offset geom2', (t) => {
127
135
  const geometry = geom2.create([
128
- [[-75.00000, 75.00000], [-75.00000, -75.00000]],
129
- [[-75.00000, -75.00000], [75.00000, -75.00000]],
130
- [[75.00000, -75.00000], [75.00000, 75.00000]],
131
- [[-40.00000, 75.00000], [-75.00000, 75.00000]],
132
- [[75.00000, 75.00000], [40.00000, 75.00000]],
133
- [[40.00000, 75.00000], [40.00000, 0.00000]],
134
- [[40.00000, 0.00000], [-40.00000, 0.00000]],
135
- [[-40.00000, 0.00000], [-40.00000, 75.00000]],
136
- [[15.00000, -10.00000], [15.00000, -40.00000]],
137
- [[-15.00000, -10.00000], [15.00000, -10.00000]],
138
- [[-15.00000, -40.00000], [-15.00000, -10.00000]],
139
- [[-8.00000, -40.00000], [-15.00000, -40.00000]],
140
- [[15.00000, -40.00000], [8.00000, -40.00000]],
141
- [[-8.00000, -25.00000], [-8.00000, -40.00000]],
142
- [[8.00000, -25.00000], [-8.00000, -25.00000]],
143
- [[8.00000, -40.00000], [8.00000, -25.00000]],
144
- [[-2.00000, -15.00000], [-2.00000, -19.00000]],
145
- [[-2.00000, -19.00000], [2.00000, -19.00000]],
146
- [[2.00000, -19.00000], [2.00000, -15.00000]],
147
- [[2.00000, -15.00000], [-2.00000, -15.00000]]
136
+ [[-75, 75], [-75, -75]],
137
+ [[-75, -75], [75, -75]],
138
+ [[75, -75], [75, 75]],
139
+ [[-40, 75], [-75, 75]],
140
+ [[75, 75], [40, 75]],
141
+ [[40, 75], [40, 0]],
142
+ [[40, 0], [-40, 0]],
143
+ [[-40, 0], [-40, 75]],
144
+ [[15, -10], [15, -40]],
145
+ [[-15, -10], [15, -10]],
146
+ [[-15, -40], [-15, -10]],
147
+ [[-8, -40], [-15, -40]],
148
+ [[15, -40], [8, -40]],
149
+ [[-8, -25], [-8, -40]],
150
+ [[8, -25], [-8, -25]],
151
+ [[8, -40], [8, -25]],
152
+ [[-2, -15], [-2, -19]],
153
+ [[-2, -19], [2, -19]],
154
+ [[2, -19], [2, -15]],
155
+ [[2, -15], [-2, -15]]
148
156
  ])
149
157
 
150
158
  // expand +
151
159
  const obs = expand({ delta: 2, corners: 'edge' }, geometry)
152
160
  const pts = geom2.toPoints(obs)
153
161
  const exp = [
154
- [-77, -77],
155
- [-75, -77],
156
- [75, -77],
157
162
  [77, -77],
158
- [77, -75],
159
- [77, 75],
160
163
  [77, 77],
161
- [75, 77],
162
- [40, 77],
163
164
  [38, 77],
164
- [38, 75],
165
165
  [38, 2],
166
166
  [-38, 2],
167
- [-38, 75],
168
167
  [-37.99999999999999, 77],
169
- [-40, 77],
170
- [-75, 77],
171
168
  [-77, 77],
172
- [-77, 75],
173
- [17, -40],
174
169
  [16.999999999999996, -42],
175
- [15, -42],
176
- [8, -42],
177
170
  [6, -42],
178
- [6, -40],
179
171
  [6, -27],
180
172
  [-6, -27],
181
- [-6, -40],
182
173
  [-6.000000000000001, -42],
183
- [-8, -42],
184
- [-15, -42],
185
174
  [-17, -42],
186
- [-17, -40],
187
- [-17, -10],
188
175
  [-16.999999999999996, -8],
189
- [-15, -8],
190
- [15, -8],
191
176
  [17, -8.000000000000004],
192
- [17, -10],
193
- [-4, -19],
194
177
  [-4, -21],
195
- [-2, -21],
196
- [1.9999999999999998, -21],
197
178
  [3.9999999999999996, -21],
198
- [4, -19],
199
- [4, -15],
200
179
  [4, -13],
201
- [2, -13],
202
- [-1.9999999999999998, -13],
203
180
  [-4, -13],
204
- [-4, -15],
205
- [-77, -75]
181
+ [-77, -77]
206
182
  ]
207
- t.is(pts.length, 52)
183
+ t.notThrows(() => geom2.validate(obs))
184
+ t.is(pts.length, 20)
208
185
  t.true(comparePoints(pts, exp))
209
186
  })
@@ -15,37 +15,43 @@ const unionGeom3Sub = require('../booleans/unionGeom3Sub')
15
15
 
16
16
  const extrudePolygon = require('./extrudePolygon')
17
17
 
18
+ /*
19
+ * Collect all planes adjacent to each vertex
20
+ */
18
21
  const mapPlaneToVertex = (map, vertex, plane) => {
19
- const i = map.findIndex((item) => vec3.equals(item[0], vertex))
20
- if (i < 0) {
22
+ const key = vertex.toString()
23
+ if (!map.has(key)) {
21
24
  const entry = [vertex, [plane]]
22
- map.push(entry)
23
- return map.length
25
+ map.set(key, entry)
26
+ } else {
27
+ const planes = map.get(key)[1]
28
+ planes.push(plane)
24
29
  }
25
- const planes = map[i][1]
26
- planes.push(plane)
27
- return i
28
30
  }
29
31
 
32
+ /*
33
+ * Collect all planes adjacent to each edge.
34
+ * Combine undirected edges, no need for duplicate cylinders.
35
+ */
30
36
  const mapPlaneToEdge = (map, edge, plane) => {
31
- const i = map.findIndex((item) => (vec3.equals(item[0], edge[0]) && vec3.equals(item[1], edge[1])) || (vec3.equals(item[0], edge[1]) && vec3.equals(item[1], edge[0])))
32
- if (i < 0) {
37
+ const key0 = edge[0].toString()
38
+ const key1 = edge[1].toString()
39
+ // Sort keys to make edges undirected
40
+ const key = key0 < key1 ? `${key0},${key1}` : `${key1},${key0}`
41
+ if (!map.has(key)) {
33
42
  const entry = [edge, [plane]]
34
- map.push(entry)
35
- return map.length
43
+ map.set(key, entry)
44
+ } else {
45
+ const planes = map.get(key)[1]
46
+ planes.push(plane)
36
47
  }
37
- const planes = map[i][1]
38
- planes.push(plane)
39
- return i
40
48
  }
41
49
 
42
50
  const addUniqueAngle = (map, angle) => {
43
51
  const i = map.findIndex((item) => item === angle)
44
52
  if (i < 0) {
45
53
  map.push(angle)
46
- return map.length
47
54
  }
48
- return i
49
55
  }
50
56
 
51
57
  /*
@@ -65,8 +71,8 @@ const expandShell = (options, geometry) => {
65
71
  const { delta, segments } = Object.assign({ }, defaults, options)
66
72
 
67
73
  let result = geom3.create()
68
- const vertices2planes = [] // contents: [vertex, [plane, ...]]
69
- const edges2planes = [] // contents: [[vertex, vertex], [plane, ...]]
74
+ const vertices2planes = new Map() // {vertex: [vertex, [plane, ...]]}
75
+ const edges2planes = new Map() // {edge: [[vertex, vertex], [plane, ...]]}
70
76
 
71
77
  const v1 = vec3.create()
72
78
  const v2 = vec3.create()
@@ -13,7 +13,7 @@ const offsetPath2 = require('./offsetPath2')
13
13
  * @param {Float} [options.delta=1] - delta of offset (+ to exterior, - from interior)
14
14
  * @param {String} [options.corners='edge'] - type of corner to create after offseting; edge, chamfer, round
15
15
  * @param {Integer} [options.segments=16] - number of segments when creating round corners
16
- * @param {...Object} geometry - the list of geometry to offset
16
+ * @param {...Object} objects - the geometries to offset
17
17
  * @return {Object|Array} new geometry, or list of new geometries
18
18
  * @alias module:modeling/expansions.offset
19
19
  *