@jscad/modeling 3.0.2-alpha.0 → 3.0.4-alpha.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 (232) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/jscad-modeling.es.js +2 -7
  3. package/dist/jscad-modeling.min.js +2 -7
  4. package/package.json +6 -7
  5. package/rollup.config.js +8 -4
  6. package/src/colors/colorize.test.js +1 -1
  7. package/src/curves/bezier/arcLengthToT.js +1 -1
  8. package/src/curves/bezier/create.js +1 -1
  9. package/src/curves/bezier/index.js +7 -7
  10. package/src/curves/bezier/length.js +1 -1
  11. package/src/curves/bezier/lengths.js +2 -1
  12. package/src/curves/bezier/tangentAt.js +1 -1
  13. package/src/curves/bezier/valueAt.js +1 -1
  14. package/src/curves/index.js +3 -3
  15. package/src/geometries/geom2/applyTransforms.js +3 -1
  16. package/src/geometries/geom2/clone.js +5 -1
  17. package/src/geometries/geom2/create.js +4 -14
  18. package/src/geometries/geom2/fromSides.js +4 -2
  19. package/src/geometries/geom2/index.d.ts +0 -2
  20. package/src/geometries/geom2/index.js +21 -7
  21. package/src/geometries/geom2/isA.js +5 -1
  22. package/src/geometries/geom2/reverse.js +4 -2
  23. package/src/geometries/geom2/toOutlines.js +2 -1
  24. package/src/geometries/geom2/toPoints.js +5 -2
  25. package/src/geometries/geom2/toSides.js +4 -3
  26. package/src/geometries/geom2/toString.js +3 -2
  27. package/src/geometries/geom2/transform.js +4 -2
  28. package/src/geometries/geom2/validate.js +6 -2
  29. package/src/geometries/geom3/applyTransforms.test.js +2 -2
  30. package/src/geometries/geom3/clone.js +5 -1
  31. package/src/geometries/geom3/clone.test.js +2 -2
  32. package/src/geometries/geom3/create.js +6 -28
  33. package/src/geometries/geom3/{fromPoints.d.ts → fromVertices.d.ts} +1 -1
  34. package/src/geometries/geom3/{fromPoints.js → fromVertices.js} +15 -2
  35. package/src/geometries/geom3/{fromPoints.test.js → fromVertices.test.js} +6 -6
  36. package/src/geometries/geom3/{fromPointsConvex.d.ts → fromVerticesConvex.d.ts} +1 -1
  37. package/src/geometries/geom3/fromVerticesConvex.js +25 -0
  38. package/src/geometries/geom3/{fromPointsConvex.test.js → fromVerticesConvex.test.js} +3 -3
  39. package/src/geometries/geom3/index.d.ts +4 -5
  40. package/src/geometries/geom3/index.js +29 -9
  41. package/src/geometries/geom3/invert.js +5 -1
  42. package/src/geometries/geom3/invert.test.js +2 -2
  43. package/src/geometries/geom3/isA.js +5 -1
  44. package/src/geometries/geom3/isA.test.js +2 -2
  45. package/src/geometries/geom3/isConvex.d.ts +3 -0
  46. package/src/geometries/geom3/isConvex.js +65 -0
  47. package/src/geometries/geom3/isConvex.test.js +44 -0
  48. package/src/geometries/geom3/toPolygons.js +4 -2
  49. package/src/geometries/geom3/toString.js +3 -2
  50. package/src/geometries/geom3/toString.test.js +2 -2
  51. package/src/geometries/geom3/{toPoints.d.ts → toVertices.d.ts} +1 -1
  52. package/src/geometries/geom3/toVertices.js +20 -0
  53. package/src/geometries/geom3/{toPoints.test.js → toVertices.test.js} +4 -4
  54. package/src/geometries/geom3/transform.js +5 -2
  55. package/src/geometries/geom3/transform.test.js +2 -2
  56. package/src/geometries/geom3/validate.js +6 -2
  57. package/src/geometries/geom3/validate.test.js +4 -4
  58. package/src/geometries/index.d.ts +1 -0
  59. package/src/geometries/index.js +10 -7
  60. package/src/geometries/path2/appendArc.js +7 -5
  61. package/src/geometries/path2/appendArc.test.js +11 -15
  62. package/src/geometries/path2/appendBezier.js +6 -4
  63. package/src/geometries/path2/appendPoints.js +4 -2
  64. package/src/geometries/path2/applyTransforms.js +3 -0
  65. package/src/geometries/path2/clone.js +5 -1
  66. package/src/geometries/path2/close.js +5 -1
  67. package/src/geometries/path2/concat.js +3 -2
  68. package/src/geometries/path2/create.js +5 -25
  69. package/src/geometries/path2/equals.js +12 -7
  70. package/src/geometries/path2/fromPoints.js +5 -3
  71. package/src/geometries/path2/index.d.ts +0 -2
  72. package/src/geometries/path2/index.js +21 -6
  73. package/src/geometries/path2/isA.js +5 -1
  74. package/src/geometries/path2/reverse.js +4 -2
  75. package/src/geometries/path2/toPoints.js +5 -3
  76. package/src/geometries/path2/toString.js +3 -2
  77. package/src/geometries/path2/transform.js +4 -2
  78. package/src/geometries/path2/validate.js +5 -1
  79. package/src/geometries/path3/applyTransforms.js +22 -0
  80. package/src/geometries/path3/applyTransforms.test.js +28 -0
  81. package/src/geometries/path3/close.d.ts +3 -0
  82. package/src/geometries/path3/close.js +33 -0
  83. package/src/geometries/path3/close.test.js +43 -0
  84. package/src/geometries/path3/concat.d.ts +3 -0
  85. package/src/geometries/path3/concat.js +35 -0
  86. package/src/geometries/path3/concat.test.js +35 -0
  87. package/src/geometries/path3/create.d.ts +4 -0
  88. package/src/geometries/path3/create.js +14 -0
  89. package/src/geometries/path3/create.test.js +8 -0
  90. package/src/geometries/path3/equals.d.ts +3 -0
  91. package/src/geometries/path3/equals.js +50 -0
  92. package/src/geometries/path3/equals.test.js +38 -0
  93. package/src/geometries/path3/fromVertices.d.ts +8 -0
  94. package/src/geometries/path3/fromVertices.js +44 -0
  95. package/src/geometries/path3/fromVertices.test.js +33 -0
  96. package/src/geometries/path3/index.d.ts +13 -0
  97. package/src/geometries/path3/index.js +37 -0
  98. package/src/geometries/path3/isA.d.ts +3 -0
  99. package/src/geometries/path3/isA.js +22 -0
  100. package/src/geometries/path3/isA.test.js +19 -0
  101. package/src/geometries/path3/reverse.d.ts +3 -0
  102. package/src/geometries/path3/reverse.js +18 -0
  103. package/src/geometries/path3/reverse.test.js +9 -0
  104. package/src/geometries/path3/toString.d.ts +3 -0
  105. package/src/geometries/path3/toString.js +23 -0
  106. package/src/geometries/path3/toVertices.d.ts +4 -0
  107. package/src/geometries/path3/toVertices.js +15 -0
  108. package/src/geometries/path3/toVertices.test.js +13 -0
  109. package/src/geometries/path3/transform.d.ts +4 -0
  110. package/src/geometries/path3/transform.js +20 -0
  111. package/src/geometries/path3/transform.test.js +50 -0
  112. package/src/geometries/path3/type.d.ts +10 -0
  113. package/src/geometries/path3/validate.d.ts +1 -0
  114. package/src/geometries/path3/validate.js +44 -0
  115. package/src/geometries/poly2/arePointsInside.js +4 -1
  116. package/src/geometries/poly2/clone.js +4 -1
  117. package/src/geometries/poly2/create.js +3 -15
  118. package/src/geometries/poly2/index.js +16 -4
  119. package/src/geometries/poly2/isA.js +5 -1
  120. package/src/geometries/poly2/isConvex.js +5 -1
  121. package/src/geometries/poly2/isSimple.js +5 -1
  122. package/src/geometries/poly2/measureArea.js +4 -1
  123. package/src/geometries/poly2/measureBoundingBox.js +6 -1
  124. package/src/geometries/poly2/reverse.js +4 -1
  125. package/src/geometries/poly2/toPoints.js +6 -1
  126. package/src/geometries/poly2/toString.js +5 -1
  127. package/src/geometries/poly2/transform.js +5 -1
  128. package/src/geometries/poly2/validate.js +6 -2
  129. package/src/geometries/poly3/clone.js +4 -1
  130. package/src/geometries/poly3/create.js +4 -17
  131. package/src/geometries/poly3/fromVerticesAndPlane.js +3 -1
  132. package/src/geometries/poly3/index.js +19 -4
  133. package/src/geometries/poly3/invert.js +4 -1
  134. package/src/geometries/poly3/isA.js +5 -1
  135. package/src/geometries/poly3/isConvex.js +5 -1
  136. package/src/geometries/poly3/measureArea.js +5 -1
  137. package/src/geometries/poly3/measureBoundingBox.js +4 -1
  138. package/src/geometries/poly3/measureBoundingSphere.js +4 -3
  139. package/src/geometries/poly3/measureSignedVolume.js +6 -1
  140. package/src/geometries/poly3/plane.js +6 -0
  141. package/src/geometries/poly3/toString.js +5 -1
  142. package/src/geometries/poly3/toVertices.js +6 -1
  143. package/src/geometries/poly3/transform.js +5 -1
  144. package/src/geometries/poly3/validate.js +6 -2
  145. package/src/geometries/slice/calculatePlane.js +3 -3
  146. package/src/geometries/slice/clone.js +4 -1
  147. package/src/geometries/slice/create.js +5 -10
  148. package/src/geometries/slice/equals.js +5 -1
  149. package/src/geometries/slice/fromGeom2.js +1 -1
  150. package/src/geometries/slice/fromVertices.js +3 -3
  151. package/src/geometries/slice/index.js +19 -4
  152. package/src/geometries/slice/isA.js +5 -1
  153. package/src/geometries/slice/reverse.js +5 -2
  154. package/src/geometries/slice/toEdges.js +5 -3
  155. package/src/geometries/slice/toPolygons.js +5 -1
  156. package/src/geometries/slice/toString.js +5 -1
  157. package/src/geometries/slice/toVertices.js +5 -3
  158. package/src/geometries/slice/transform.js +4 -3
  159. package/src/geometries/slice/validate.js +3 -2
  160. package/src/index.d.ts +1 -0
  161. package/src/index.js +4 -0
  162. package/src/maths/constants.js +11 -7
  163. package/src/maths/index.js +2 -1
  164. package/src/maths/mat4/isOnlyTransformScale.js +1 -1
  165. package/src/operations/booleans/index.js +2 -0
  166. package/src/operations/booleans/intersect.js +0 -1
  167. package/src/operations/booleans/intersectGeom3.test.js +4 -4
  168. package/src/operations/booleans/scission.js +0 -1
  169. package/src/operations/booleans/subtractGeom3.test.js +4 -4
  170. package/src/operations/booleans/trees/splitLineSegmentByPlane.js +1 -4
  171. package/src/operations/booleans/trees/splitPolygonByPlane.test.js +138 -0
  172. package/src/operations/booleans/unionGeom3.test.js +40 -5
  173. package/src/operations/extrusions/extrudeFromSlices.js +15 -5
  174. package/src/operations/extrusions/extrudeFromSlices.test.js +6 -6
  175. package/src/operations/extrusions/extrudeLinear.test.js +8 -8
  176. package/src/operations/extrusions/extrudeRotate.js +2 -1
  177. package/src/operations/extrusions/extrudeRotate.test.js +46 -12
  178. package/src/operations/extrusions/extrudeWalls.test.js +60 -0
  179. package/src/operations/hulls/hull.test.js +5 -5
  180. package/src/operations/hulls/hullChain.test.js +5 -5
  181. package/src/operations/hulls/toUniquePoints.js +2 -2
  182. package/src/operations/minkowski/index.d.ts +1 -0
  183. package/src/operations/minkowski/index.js +15 -0
  184. package/src/operations/minkowski/minkowskiSum.d.ts +4 -0
  185. package/src/operations/minkowski/minkowskiSum.js +223 -0
  186. package/src/operations/minkowski/minkowskiSum.test.js +199 -0
  187. package/src/operations/modifiers/generalize.test.js +6 -6
  188. package/src/operations/modifiers/insertTjunctions.test.js +2 -2
  189. package/src/operations/modifiers/reTesselateCoplanarPolygons.js +10 -3
  190. package/src/operations/modifiers/reTesselateCoplanarPolygons.test.js +36 -1
  191. package/src/operations/modifiers/retessellate.js +4 -2
  192. package/src/operations/modifiers/retessellate.test.js +10 -10
  193. package/src/operations/modifiers/snap.test.js +28 -19
  194. package/src/operations/offsets/offsetGeom3.test.js +9 -11
  195. package/src/operations/transforms/center.test.js +7 -7
  196. package/src/operations/transforms/mirror.test.js +7 -7
  197. package/src/operations/transforms/rotate.test.js +7 -7
  198. package/src/operations/transforms/scale.test.js +7 -7
  199. package/src/operations/transforms/transform.test.js +2 -2
  200. package/src/operations/transforms/translate.test.js +7 -7
  201. package/src/primitives/arc.js +2 -2
  202. package/src/primitives/arc.test.js +104 -113
  203. package/src/primitives/cube.test.js +4 -4
  204. package/src/primitives/cuboid.test.js +4 -4
  205. package/src/primitives/cylinder.test.js +5 -5
  206. package/src/primitives/cylinderElliptic.test.js +9 -9
  207. package/src/primitives/ellipsoid.test.js +5 -5
  208. package/src/primitives/geodesicSphere.test.js +4 -4
  209. package/src/primitives/polyhedron.test.js +2 -2
  210. package/src/primitives/roundedCuboid.test.js +7 -7
  211. package/src/primitives/roundedCylinder.test.js +9 -9
  212. package/src/primitives/sphere.test.js +5 -5
  213. package/src/primitives/torus.test.js +4 -4
  214. package/src/utils/flatten.js +1 -1
  215. package/src/utils/flatten.test.js +94 -0
  216. package/src/geometries/geom2/fromCompactBinary.d.ts +0 -3
  217. package/src/geometries/geom2/fromCompactBinary.js +0 -40
  218. package/src/geometries/geom2/fromToCompactBinary.test.js +0 -100
  219. package/src/geometries/geom2/toCompactBinary.d.ts +0 -3
  220. package/src/geometries/geom2/toCompactBinary.js +0 -56
  221. package/src/geometries/geom3/fromCompactBinary.d.ts +0 -3
  222. package/src/geometries/geom3/fromCompactBinary.js +0 -42
  223. package/src/geometries/geom3/fromPointsConvex.js +0 -25
  224. package/src/geometries/geom3/fromToCompactBinary.test.js +0 -139
  225. package/src/geometries/geom3/toCompactBinary.d.ts +0 -3
  226. package/src/geometries/geom3/toCompactBinary.js +0 -66
  227. package/src/geometries/geom3/toPoints.js +0 -15
  228. package/src/geometries/path2/fromCompactBinary.d.ts +0 -3
  229. package/src/geometries/path2/fromCompactBinary.js +0 -31
  230. package/src/geometries/path2/fromToCompactBinary.test.js +0 -114
  231. package/src/geometries/path2/toCompactBinary.d.ts +0 -3
  232. package/src/geometries/path2/toCompactBinary.js +0 -50
@@ -18,7 +18,7 @@ test('extrudeRotate: (defaults) extruding of a geom2 produces an expected geom3'
18
18
  const geometry2 = geom2.create([[[10, 8], [10, -8], [26, -8], [26, 8]]])
19
19
 
20
20
  const geometry3 = extrudeRotate({ }, geometry2)
21
- const pts = geom3.toPoints(geometry3)
21
+ const pts = geom3.toVertices(geometry3)
22
22
  t.notThrows(() => geom3.validate(geometry3))
23
23
  t.is(measureArea(geometry3), 7033.914479497244)
24
24
  t.is(measureVolume(geometry3), 27648.000000000007)
@@ -36,7 +36,7 @@ test('extrudeRotate: (angle) extruding of a geom2 produces an expected geom3', (
36
36
 
37
37
  // test angle
38
38
  let geometry3 = extrudeRotate({ segments: 4, angle: TAU / 8 }, geometry2)
39
- let pts = geom3.toPoints(geometry3)
39
+ let pts = geom3.toVertices(geometry3)
40
40
  const exp = [
41
41
  [[26, 0, 8], [26, 0, -8], [18.38477631085024, 18.384776310850235, -8]],
42
42
  [[26, 0, 8], [18.38477631085024, 18.384776310850235, -8], [18.38477631085024, 18.384776310850235, 8]],
@@ -58,14 +58,14 @@ test('extrudeRotate: (angle) extruding of a geom2 produces an expected geom3', (
58
58
  t.true(comparePolygonsAsPoints(pts, exp))
59
59
 
60
60
  geometry3 = extrudeRotate({ segments: 4, angle: -250 * 0.017453292519943295 }, geometry2)
61
- pts = geom3.toPoints(geometry3)
61
+ pts = geom3.toVertices(geometry3)
62
62
  t.notThrows(() => geom3.validate(geometry3))
63
63
  t.is(measureArea(geometry3), 4525.850393739846)
64
64
  t.is(measureVolume(geometry3), 13730.527057424617)
65
65
  t.is(pts.length, 28)
66
66
 
67
67
  geometry3 = extrudeRotate({ segments: 4, angle: 250 * 0.017453292519943295 }, geometry2)
68
- pts = geom3.toPoints(geometry3)
68
+ pts = geom3.toVertices(geometry3)
69
69
  t.notThrows(() => geom3.validate(geometry3))
70
70
  t.is(measureArea(geometry3), 4525.8503937398455)
71
71
  t.is(measureVolume(geometry3), 13730.527057424617)
@@ -77,7 +77,7 @@ test('extrudeRotate: (startAngle) extruding of a geom2 produces an expected geom
77
77
 
78
78
  // test startAngle
79
79
  let geometry3 = extrudeRotate({ segments: 5, startAngle: TAU / 8 }, geometry2)
80
- let pts = geom3.toPoints(geometry3)
80
+ let pts = geom3.toVertices(geometry3)
81
81
  let exp = [
82
82
  [7.0710678118654755, 7.071067811865475, 8],
83
83
  [18.38477631085024, 18.384776310850235, 8],
@@ -90,7 +90,7 @@ test('extrudeRotate: (startAngle) extruding of a geom2 produces an expected geom
90
90
  t.true(comparePoints(pts[6], exp))
91
91
 
92
92
  geometry3 = extrudeRotate({ segments: 5, startAngle: -TAU / 8 }, geometry2)
93
- pts = geom3.toPoints(geometry3)
93
+ pts = geom3.toVertices(geometry3)
94
94
  exp = [
95
95
  [7.0710678118654755, -7.071067811865475, 8],
96
96
  [18.38477631085024, -18.384776310850235, 8],
@@ -108,14 +108,14 @@ test('extrudeRotate: (segments) extruding of a geom2 produces an expected geom3'
108
108
 
109
109
  // test segments
110
110
  let geometry3 = extrudeRotate({ segments: 4 }, geometry2)
111
- let pts = geom3.toPoints(geometry3)
111
+ let pts = geom3.toVertices(geometry3)
112
112
  t.notThrows(() => geom3.validate(geometry3))
113
113
  t.is(measureArea(geometry3), 5562.34804770761)
114
114
  t.is(measureVolume(geometry3), 18432)
115
115
  t.is(pts.length, 32)
116
116
 
117
117
  geometry3 = extrudeRotate({ segments: 64 }, geometry2)
118
- pts = geom3.toPoints(geometry3)
118
+ pts = geom3.toVertices(geometry3)
119
119
  t.notThrows(() => geom3.validate(geometry3))
120
120
  t.is(measureArea(geometry3), 7230.965353920782)
121
121
  t.is(measureVolume(geometry3), 28906.430888871357)
@@ -124,7 +124,7 @@ test('extrudeRotate: (segments) extruding of a geom2 produces an expected geom3'
124
124
  // test overlapping edges
125
125
  geometry2 = geom2.create([[[0, 0], [2, 1], [1, 2], [1, 3], [3, 4], [0, 5]]])
126
126
  geometry3 = extrudeRotate({ segments: 8 }, geometry2)
127
- pts = geom3.toPoints(geometry3)
127
+ pts = geom3.toVertices(geometry3)
128
128
  t.notThrows(() => geom3.validate(geometry3))
129
129
  t.is(measureArea(geometry3), 84.28200374166053)
130
130
  t.is(measureVolume(geometry3), 33.94112549695427)
@@ -133,7 +133,7 @@ test('extrudeRotate: (segments) extruding of a geom2 produces an expected geom3'
133
133
  // test overlapping edges that produce hollow shape
134
134
  geometry2 = geom2.create([[[30, 0], [30, 60], [0, 60], [0, 50], [10, 40], [10, 30], [0, 20], [0, 10], [10, 0]]])
135
135
  geometry3 = extrudeRotate({ segments: 8 }, geometry2)
136
- pts = geom3.toPoints(geometry3)
136
+ pts = geom3.toVertices(geometry3)
137
137
  t.notThrows(() => geom3.validate(geometry3))
138
138
  t.is(measureArea(geometry3), 17692.315375839215)
139
139
  t.is(measureVolume(geometry3), 147078.2104868019)
@@ -145,7 +145,7 @@ test('extrudeRotate: (overlap +/-) extruding of a geom2 produces an expected geo
145
145
  let geometry = geom2.create([[[-1, 8], [-1, -8], [7, -8], [7, 8]]])
146
146
 
147
147
  let obs = extrudeRotate({ segments: 4, angle: TAU / 4 }, geometry)
148
- let pts = geom3.toPoints(obs)
148
+ let pts = geom3.toVertices(obs)
149
149
  let exp = [
150
150
  [[0, 0, 8], [7, 0, 8], [0, 7, 8]],
151
151
  [[7, 0, 8], [7, 0, -8], [0, 7, -8]],
@@ -166,7 +166,7 @@ test('extrudeRotate: (overlap +/-) extruding of a geom2 produces an expected geo
166
166
  geometry = geom2.create([[[-1, 8], [-2, 4], [-1, -8], [7, -8], [7, 8]]])
167
167
 
168
168
  obs = extrudeRotate({ segments: 8, angle: TAU / 4 }, geometry)
169
- pts = geom3.toPoints(obs)
169
+ pts = geom3.toVertices(obs)
170
170
  exp = [
171
171
  [[2, 0, 4], [1, 0, -8], [0.7071067811865476, 0.7071067811865475, -8]],
172
172
  [[2, 0, 4], [0.7071067811865476, 0.7071067811865475, -8], [1.4142135623730951, 1.414213562373095, 4]],
@@ -194,4 +194,38 @@ test('extrudeRotate: (overlap +/-) extruding of a geom2 produces an expected geo
194
194
  t.true(comparePolygonsAsPoints(pts, exp))
195
195
  })
196
196
 
197
+ // Test for mat4 reuse optimization: verify rotation matrices are computed correctly
198
+ // This ensures the optimization of computing xRotationMatrix once doesn't break anything
199
+ test('extrudeRotate: (mat4 reuse) rotation matrices produce correct geometry', (t) => {
200
+ // Simple rectangle that will be rotated to form a tube-like shape
201
+ const geometry2 = geom2.create([[[6, 1], [5, 1], [5, -1], [6, -1]]])
202
+
203
+ // Full rotation with many segments to test matrix reuse across iterations
204
+ const geometry3 = extrudeRotate({ segments: 32 }, geometry2)
205
+ const pts = geom3.toVertices(geometry3)
206
+
207
+ t.notThrows(() => geom3.validate(geometry3))
208
+ // 32 segments * 8 walls per segment (4 edges * 2 triangles) = 256 polygons
209
+ t.is(pts.length, 256)
210
+
211
+ // Verify the geometry is closed (first and last slices connect properly)
212
+ // This tests the Zrotation rounding error fix at index === segments
213
+ const obs = extrudeRotate({ segments: 16 }, geometry2)
214
+ t.notThrows(() => geom3.validate(obs))
215
+ t.is(measureArea(obs), 204.69587079560992)
216
+ t.is(measureVolume(obs), 67.35228409625583)
217
+ })
218
+
219
+ // Test for mat4 reuse with partial rotation (tests both capped and matrix reuse)
220
+ test('extrudeRotate: (mat4 reuse) partial rotation produces correct caps', (t) => {
221
+ const geometry2 = geom2.create([[[6, 1], [5, 1], [5, -1], [6, -1]]])
222
+
223
+ // Quarter rotation - should have start and end caps
224
+ const obs = extrudeRotate({ segments: 8, angle: TAU / 4 }, geometry2)
225
+
226
+ t.notThrows(() => geom3.validate(obs))
227
+ t.is(measureArea(obs), 53.232491234231944)
228
+ t.is(measureVolume(obs), 15.556349186104049)
229
+ })
230
+
197
231
  // TEST HOLES
@@ -56,3 +56,63 @@ test('extrudeWalls (different shapes)', (t) => {
56
56
  walls = extrudeWalls(slice3, slice.transform(matrix, slice2))
57
57
  t.is(walls.length, 24)
58
58
  })
59
+
60
+ // Test for vec3 reuse optimization in repartitionEdges
61
+ // When shapes have different edge counts, edges are repartitioned using vec3 operations
62
+ test('extrudeWalls (repartitionEdges vec3 reuse)', (t) => {
63
+ const matrix = mat4.fromTranslation(mat4.create(), [0, 0, 5])
64
+
65
+ // Triangle (3 edges)
66
+ const triangle = slice.create([[
67
+ [0, 10, 0], [-8.66, -5, 0], [8.66, -5, 0]
68
+ ]])
69
+
70
+ // Hexagon (6 edges) - LCM with triangle is 6, so triangle edges get split
71
+ const hexagon = slice.create([[
72
+ [0, 10, 0], [-8.66, 5, 0],
73
+ [-8.66, -5, 0],
74
+ [0, -10, 0],
75
+ [8.66, -5, 0],
76
+ [8.66, 5, 0]
77
+ ]])
78
+
79
+ // Triangle to hexagon requires repartitioning (3 -> 6 edges)
80
+ // This exercises the vec3 reuse optimization in repartitionEdges
81
+ const walls = extrudeWalls(triangle, slice.transform(matrix, hexagon))
82
+
83
+ // 6 edges * 2 triangles per edge = 12 wall polygons
84
+ t.is(walls.length, 12)
85
+
86
+ // Verify all walls are valid triangles
87
+ walls.forEach((wall) => {
88
+ t.is(wall.vertices.length, 3)
89
+ })
90
+ })
91
+
92
+ // Test for vec3 reuse with higher repartition multiple
93
+ test('extrudeWalls (repartitionEdges with high multiple)', (t) => {
94
+ const matrix = mat4.fromTranslation(mat4.create(), [0, 0, 10])
95
+
96
+ // Square (4 edges)
97
+ const square = slice.create([[
98
+ [-5, 5, 0], [-5, -5, 0], [5, -5, 0], [5, 5, 0]
99
+ ]])
100
+
101
+ // Octagon (8 edges) - LCM with square is 8, so square edges get doubled
102
+ const octagon = slice.create([[
103
+ [0, 5, 0],
104
+ [-3.54, 3.54, 0],
105
+ [-5, 0, 0],
106
+ [-3.54, -3.54, 0],
107
+ [0, -5, 0],
108
+ [3.54, -3.54, 0],
109
+ [5, 0, 0],
110
+ [3.54, 3.54, 0]
111
+ ]])
112
+
113
+ // Square to octagon requires repartitioning (4 -> 8 edges)
114
+ const walls = extrudeWalls(square, slice.transform(matrix, octagon))
115
+
116
+ // 8 edges * 2 triangles per edge = 16 wall polygons
117
+ t.is(walls.length, 16)
118
+ })
@@ -218,7 +218,7 @@ test('hull (single, geom3)', (t) => {
218
218
  let geometry = geom3.create()
219
219
 
220
220
  let obs = hull(geometry)
221
- let pts = geom3.toPoints(obs)
221
+ let pts = geom3.toVertices(obs)
222
222
 
223
223
  t.notThrows(() => geom3.validate(obs))
224
224
  t.is(pts.length, 0)
@@ -226,7 +226,7 @@ test('hull (single, geom3)', (t) => {
226
226
  geometry = sphere({ radius: 2, segments: 8 })
227
227
 
228
228
  obs = hull(geometry)
229
- pts = geom3.toPoints(obs)
229
+ pts = geom3.toVertices(obs)
230
230
 
231
231
  t.notThrows(() => geom3.validate(obs))
232
232
  t.is(measureArea(obs), 44.05375630658983)
@@ -238,7 +238,7 @@ test('hull (multiple, geom3)', (t) => {
238
238
  const geometry1 = cuboid({ size: [2, 2, 2] })
239
239
 
240
240
  let obs = hull(geometry1, geometry1) // same
241
- let pts = geom3.toPoints(obs)
241
+ let pts = geom3.toVertices(obs)
242
242
  let exp = [
243
243
  [[-1, 1, -1], [-1, 1, 1], [1, 1, 1], [1, 1, -1]],
244
244
  [[-1, 1, -1], [1, 1, -1], [1, -1, -1], [-1, -1, -1]],
@@ -257,7 +257,7 @@ test('hull (multiple, geom3)', (t) => {
257
257
  const geometry2 = center({ relativeTo: [5, 5, 5] }, cuboid({ size: [3, 3, 3] }))
258
258
 
259
259
  obs = hull(geometry1, geometry2)
260
- pts = geom3.toPoints(obs)
260
+ pts = geom3.toVertices(obs)
261
261
  exp = [
262
262
  [[1, -1, -1], [6.5, 3.5, 3.5], [6.5, 3.5, 6.5], [1, -1, 1]],
263
263
  [[-1, -1, 1], [-1, -1, -1], [1, -1, -1], [1, -1, 1]],
@@ -286,7 +286,7 @@ test('hull (multiple, overlapping, geom3)', (t) => {
286
286
  const geometry3 = center({ relativeTo: [-3, -3, -3] }, ellipsoid({ radius: [3, 3, 3], segments: 12 }))
287
287
 
288
288
  const obs = hull(geometry1, geometry2, geometry3)
289
- const pts = geom3.toPoints(obs)
289
+ const pts = geom3.toVertices(obs)
290
290
 
291
291
  t.notThrows(() => geom3.validate(obs))
292
292
  t.is(measureArea(obs), 282.26819685563686)
@@ -61,7 +61,7 @@ test('hullChain (three, geom2)', (t) => {
61
61
  })
62
62
 
63
63
  test('hullChain (three, geom3)', (t) => {
64
- const geometry1 = geom3.fromPoints(
64
+ const geometry1 = geom3.fromVertices(
65
65
  [[[-1, -1, -1], [-1, -1, 1], [-1, 1, 1], [-1, 1, -1]],
66
66
  [[1, -1, -1], [1, 1, -1], [1, 1, 1], [1, -1, 1]],
67
67
  [[-1, -1, -1], [1, -1, -1], [1, -1, 1], [-1, -1, 1]],
@@ -69,7 +69,7 @@ test('hullChain (three, geom3)', (t) => {
69
69
  [[-1, -1, -1], [-1, 1, -1], [1, 1, -1], [1, -1, -1]],
70
70
  [[-1, -1, 1], [1, -1, 1], [1, 1, 1], [-1, 1, 1]]]
71
71
  )
72
- const geometry2 = geom3.fromPoints(
72
+ const geometry2 = geom3.fromVertices(
73
73
  [[[3.5, 3.5, 3.5], [3.5, 3.5, 6.5], [3.5, 6.5, 6.5], [3.5, 6.5, 3.5]],
74
74
  [[6.5, 3.5, 3.5], [6.5, 6.5, 3.5], [6.5, 6.5, 6.5], [6.5, 3.5, 6.5]],
75
75
  [[3.5, 3.5, 3.5], [6.5, 3.5, 3.5], [6.5, 3.5, 6.5], [3.5, 3.5, 6.5]],
@@ -77,7 +77,7 @@ test('hullChain (three, geom3)', (t) => {
77
77
  [[3.5, 3.5, 3.5], [3.5, 6.5, 3.5], [6.5, 6.5, 3.5], [6.5, 3.5, 3.5]],
78
78
  [[3.5, 3.5, 6.5], [6.5, 3.5, 6.5], [6.5, 6.5, 6.5], [3.5, 6.5, 6.5]]]
79
79
  )
80
- const geometry3 = geom3.fromPoints(
80
+ const geometry3 = geom3.fromVertices(
81
81
  [[[-4.5, 1.5, -4.5], [-4.5, 1.5, -1.5], [-4.5, 4.5, -1.5], [-4.5, 4.5, -4.5]],
82
82
  [[-1.5, 1.5, -4.5], [-1.5, 4.5, -4.5], [-1.5, 4.5, -1.5], [-1.5, 1.5, -1.5]],
83
83
  [[-4.5, 1.5, -4.5], [-1.5, 1.5, -4.5], [-1.5, 1.5, -1.5], [-4.5, 1.5, -1.5]],
@@ -88,7 +88,7 @@ test('hullChain (three, geom3)', (t) => {
88
88
 
89
89
  // open
90
90
  let obs = hullChain(geometry1, geometry2, geometry3)
91
- let pts = geom3.toPoints(obs)
91
+ let pts = geom3.toVertices(obs)
92
92
 
93
93
  t.notThrows.skip(() => geom3.validate(obs))
94
94
  t.is(measureArea(obs), 266.1454764345133)
@@ -97,7 +97,7 @@ test('hullChain (three, geom3)', (t) => {
97
97
 
98
98
  // closed
99
99
  obs = hullChain(geometry1, geometry2, geometry3, geometry1)
100
- pts = geom3.toPoints(obs)
100
+ pts = geom3.toVertices(obs)
101
101
 
102
102
  t.notThrows.skip(() => geom3.validate(obs))
103
103
  t.is(measureArea(obs), 272.2887171436021)
@@ -21,8 +21,8 @@ export const toUniquePoints = (geometries) => {
21
21
  if (geom2.isA(geometry)) {
22
22
  geom2.toPoints(geometry).forEach(addPoint)
23
23
  } else if (geom3.isA(geometry)) {
24
- // points are grouped by polygon
25
- geom3.toPoints(geometry).forEach((points) => points.forEach(addPoint))
24
+ // vertices are grouped by polygon
25
+ geom3.toVertices(geometry).forEach((vertices) => vertices.forEach(addPoint))
26
26
  } else if (path2.isA(geometry)) {
27
27
  path2.toPoints(geometry).forEach(addPoint)
28
28
  }
@@ -0,0 +1 @@
1
+ export { minkowskiSum } from './minkowskiSum'
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Minkowski sum operations for 3D geometries.
3
+ *
4
+ * The Minkowski sum of two shapes A and B is the set of all points that are
5
+ * the sum of a point in A and a point in B. This is useful for:
6
+ * - Offsetting/inflating shapes (using a sphere creates rounded edges)
7
+ * - Collision detection (shapes collide iff their Minkowski difference contains origin)
8
+ * - Motion planning and swept volumes
9
+ *
10
+ * @module modeling/minkowski
11
+ * @example
12
+ * import { minkowskiSum } from '@jscad/modeling'
13
+ * const rounded = minkowskiSum(cube, sphere)
14
+ */
15
+ export { minkowskiSum } from './minkowskiSum.js'
@@ -0,0 +1,4 @@
1
+ import type { Geom3 } from '../../geometries/types'
2
+
3
+ export function minkowskiSum(geometryA: Geom3, geometryB: Geom3): Geom3
4
+ export function minkowskiSum(...geometries: Geom3[]): Geom3
@@ -0,0 +1,223 @@
1
+ import { flatten } from '../../utils/flatten.js'
2
+
3
+ import { geom3, poly3 } from '../../geometries/index.js'
4
+
5
+ import { hullPoints3 } from '../hulls/hullPoints3.js'
6
+ import { unionGeom3 } from '../booleans/unionGeom3.js'
7
+
8
+ /**
9
+ * Compute the Minkowski sum of two 3D geometries.
10
+ *
11
+ * The Minkowski sum A ⊕ B is the set of all points a + b where a ∈ A and b ∈ B.
12
+ * Geometrically, this "inflates" geometry A by the shape of geometry B.
13
+ *
14
+ * Common use cases:
15
+ * - Offset a solid by a sphere to round all edges and corners
16
+ * - Offset a solid by a cube to create chamfered edges
17
+ * - Collision detection (if Minkowski sum contains origin, shapes overlap)
18
+ *
19
+ * For best performance, use convex geometries. Non-convex geometries are supported
20
+ * when the second operand is convex, but require decomposition and are slower.
21
+ *
22
+ * @param {...Object} geometries - two geom3 geometries (second should be convex for non-convex first)
23
+ * @returns {geom3} new 3D geometry representing the Minkowski sum
24
+ * @alias module:modeling/minkowski.minkowskiSum
25
+ *
26
+ * @example
27
+ * const { primitives, minkowski } = require('@jscad/modeling')
28
+ * const cube = primitives.cuboid({ size: [10, 10, 10] })
29
+ * const sphere = primitives.sphere({ radius: 2, segments: 16 })
30
+ * const rounded = minkowski.minkowskiSum(cube, sphere)
31
+ */
32
+ export const minkowskiSum = (...geometries) => {
33
+ geometries = flatten(geometries)
34
+
35
+ if (geometries.length !== 2) {
36
+ throw new Error('minkowskiSum requires exactly two geometries')
37
+ }
38
+
39
+ const [geomA, geomB] = geometries
40
+
41
+ if (!geom3.isA(geomA) || !geom3.isA(geomB)) {
42
+ throw new Error('minkowskiSum requires geom3 geometries')
43
+ }
44
+
45
+ const aConvex = geom3.isConvex(geomA)
46
+ const bConvex = geom3.isConvex(geomB)
47
+
48
+ // Fast path: both convex
49
+ if (aConvex && bConvex) {
50
+ return minkowskiSumConvex(geomA, geomB)
51
+ }
52
+
53
+ // Non-convex A + convex B: decompose A into tetrahedra
54
+ if (!aConvex && bConvex) {
55
+ return minkowskiSumNonConvexConvex(geomA, geomB)
56
+ }
57
+
58
+ // Convex A + non-convex B: swap operands (Minkowski sum is commutative)
59
+ if (aConvex && !bConvex) {
60
+ return minkowskiSumNonConvexConvex(geomB, geomA)
61
+ }
62
+
63
+ // Both non-convex: not yet supported
64
+ throw new Error('minkowskiSum of two non-convex geometries is not yet supported')
65
+ }
66
+
67
+ /*
68
+ * Compute Minkowski sum of non-convex A with convex B.
69
+ *
70
+ * Decomposes A into tetrahedra, computes Minkowski sum of each with B,
71
+ * then unions all results.
72
+ */
73
+ const minkowskiSumNonConvexConvex = (geomA, geomB) => {
74
+ const tetrahedra = decomposeIntoTetrahedra(geomA)
75
+
76
+ if (tetrahedra.length === 0) {
77
+ return geom3.create()
78
+ }
79
+
80
+ // Compute Minkowski sum for each tetrahedron
81
+ const parts = tetrahedra.map((tet) => minkowskiSumConvex(tet, geomB))
82
+
83
+ // Union all parts using internal unionGeom3
84
+ if (parts.length === 1) {
85
+ return parts[0]
86
+ }
87
+
88
+ return unionGeom3(parts)
89
+ }
90
+
91
+ /*
92
+ * Decompose a geom3 into tetrahedra using face-local apex points.
93
+ * Each resulting tetrahedron is guaranteed to be convex.
94
+ *
95
+ * Unlike centroid-based decomposition, this approach works correctly for
96
+ * shapes where the centroid is outside the geometry (e.g., torus, U-shapes).
97
+ * Each polygon gets its own apex point, offset inward along its normal.
98
+ */
99
+ const decomposeIntoTetrahedra = (geometry) => {
100
+ const polygons = geom3.toPolygons(geometry)
101
+
102
+ if (polygons.length === 0) {
103
+ return []
104
+ }
105
+
106
+ const tetrahedra = []
107
+
108
+ // For each polygon, compute a face-local apex and create tetrahedra
109
+ for (let i = 0; i < polygons.length; i++) {
110
+ const polygon = polygons[i]
111
+ const vertices = polygon.vertices
112
+
113
+ // Compute polygon center
114
+ let cx = 0
115
+ let cy = 0
116
+ let cz = 0
117
+ for (let k = 0; k < vertices.length; k++) {
118
+ cx += vertices[k][0]
119
+ cy += vertices[k][1]
120
+ cz += vertices[k][2]
121
+ }
122
+ cx /= vertices.length
123
+ cy /= vertices.length
124
+ cz /= vertices.length
125
+
126
+ // Get polygon plane (normal + offset)
127
+ const plane = poly3.plane(polygon)
128
+ const nx = plane[0]
129
+ const ny = plane[1]
130
+ const nz = plane[2]
131
+
132
+ // Offset inward along negative normal to create face-local apex
133
+ // The normal points outward, so we go in the negative direction
134
+ // Use a small offset - the actual distance doesn't matter much
135
+ // as long as the apex is on the interior side of the face
136
+ const offset = 0.1
137
+ const apex = [ // Vertex used as apex in tetrahedron polygons below
138
+ cx - nx * offset,
139
+ cy - ny * offset,
140
+ cz - nz * offset
141
+ ]
142
+
143
+ // Fan triangulate the polygon and create tetrahedra from apex
144
+ for (let j = 1; j < vertices.length - 1; j++) {
145
+ const v0 = vertices[0]
146
+ const v1 = vertices[j]
147
+ const v2 = vertices[j + 1]
148
+
149
+ // Create tetrahedron from apex and triangle
150
+ const tetPolygons = createTetrahedronPolygons(apex, v0, v1, v2)
151
+ tetrahedra.push(geom3.create(tetPolygons))
152
+ }
153
+ }
154
+
155
+ return tetrahedra
156
+ }
157
+
158
+ /*
159
+ * Create the 4 triangular faces of a tetrahedron.
160
+ *
161
+ * Tetrahedron has 4 faces, each a triangle
162
+ */
163
+ const createTetrahedronPolygons = (p0, p1, p2, p3) => [
164
+ poly3.create([p0, p2, p1]), // base seen from p3
165
+ poly3.create([p0, p1, p3]), // face opposite p2
166
+ poly3.create([p1, p2, p3]), // face opposite p0
167
+ poly3.create([p2, p0, p3]) // face opposite p1
168
+ ]
169
+
170
+ /*
171
+ * Compute Minkowski sum of two convex polyhedra.
172
+ *
173
+ * For convex polyhedra, the Minkowski sum equals the convex hull of
174
+ * all pairwise vertex sums. This is O(n*m) for n and m vertices,
175
+ * plus the cost of the convex hull algorithm.
176
+ */
177
+ const minkowskiSumConvex = (geomA, geomB) => {
178
+ const pointsA = extractUniqueVertices(geomA)
179
+ const pointsB = extractUniqueVertices(geomB)
180
+
181
+ if (pointsA.length === 0 || pointsB.length === 0) {
182
+ return geom3.create()
183
+ }
184
+
185
+ // Compute all pairwise sums
186
+ const summedPoints = []
187
+ for (let i = 0; i < pointsA.length; i++) {
188
+ const a = pointsA[i]
189
+ for (let j = 0; j < pointsB.length; j++) {
190
+ const b = pointsB[j]
191
+ summedPoints.push([a[0] + b[0], a[1] + b[1], a[2] + b[2]])
192
+ }
193
+ }
194
+
195
+ // Compute convex hull of the summed points
196
+ const hullPolygons = hullPoints3(summedPoints)
197
+
198
+ return geom3.create(hullPolygons)
199
+ }
200
+
201
+ /*
202
+ * Extract unique vertices from a geom3.
203
+ * Uses a Set with string keys for deduplication.
204
+ */
205
+ const extractUniqueVertices = (geometry) => {
206
+ const found = new Set()
207
+ const unique = []
208
+
209
+ const polygons = geom3.toPolygons(geometry)
210
+ for (let i = 0; i < polygons.length; i++) {
211
+ const vertices = polygons[i].vertices
212
+ for (let j = 0; j < vertices.length; j++) {
213
+ const v = vertices[j]
214
+ const key = `${v[0]},${v[1]},${v[2]}`
215
+ if (!found.has(key)) {
216
+ found.add(key)
217
+ unique.push(v)
218
+ }
219
+ }
220
+ }
221
+
222
+ return unique
223
+ }