@jscad/modeling 2.12.6 → 2.13.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 (39) hide show
  1. package/CHANGELOG.md +12 -299
  2. package/bench/booleans.bench.js +103 -0
  3. package/bench/primitives.bench.js +108 -0
  4. package/dist/jscad-modeling.min.js +404 -395
  5. package/package.json +2 -2
  6. package/src/geometries/geom3/index.d.ts +1 -0
  7. package/src/geometries/geom3/index.js +1 -0
  8. package/src/geometries/geom3/isConvex.d.ts +3 -0
  9. package/src/geometries/geom3/isConvex.js +68 -0
  10. package/src/geometries/geom3/isConvex.test.js +45 -0
  11. package/src/geometries/path2/appendArc.js +1 -1
  12. package/src/geometries/path2/appendArc.test.js +16 -20
  13. package/src/index.d.ts +1 -0
  14. package/src/index.js +1 -0
  15. package/src/operations/booleans/index.d.ts +1 -0
  16. package/src/operations/booleans/index.js +1 -0
  17. package/src/operations/booleans/trees/PolygonTreeNode.js +18 -5
  18. package/src/operations/booleans/trees/splitPolygonByPlane.js +27 -25
  19. package/src/operations/booleans/trees/splitPolygonByPlane.test.js +132 -0
  20. package/src/operations/booleans/unionGeom3.test.js +35 -0
  21. package/src/operations/extrusions/extrudeFromSlices.js +14 -4
  22. package/src/operations/extrusions/extrudeRectangular.test.js +3 -3
  23. package/src/operations/extrusions/extrudeRotate.js +4 -1
  24. package/src/operations/extrusions/extrudeRotate.test.js +33 -0
  25. package/src/operations/extrusions/extrudeWalls.js +2 -1
  26. package/src/operations/extrusions/extrudeWalls.test.js +72 -0
  27. package/src/operations/minkowski/index.d.ts +1 -0
  28. package/src/operations/minkowski/index.js +17 -0
  29. package/src/operations/minkowski/minkowskiSum.d.ts +4 -0
  30. package/src/operations/minkowski/minkowskiSum.js +224 -0
  31. package/src/operations/minkowski/minkowskiSum.test.js +195 -0
  32. package/src/operations/modifiers/reTesselateCoplanarPolygons.js +8 -3
  33. package/src/operations/modifiers/reTesselateCoplanarPolygons.test.js +36 -1
  34. package/src/operations/modifiers/retessellate.js +5 -2
  35. package/src/operations/modifiers/snap.test.js +24 -15
  36. package/src/primitives/arc.js +2 -2
  37. package/src/primitives/arc.test.js +122 -111
  38. package/src/utils/flatten.js +1 -1
  39. package/src/utils/flatten.test.js +94 -0
@@ -60,8 +60,8 @@ const arc = (options) => {
60
60
  vec2.add(point, point, centerv)
61
61
  pointArray.push(point)
62
62
  } else {
63
- // note: add one additional step to acheive full rotation
64
- const numsteps = Math.max(1, Math.floor(segments * (rotation / TAU))) + 1
63
+ const numsteps = Math.floor(segments * (Math.abs(rotation) / TAU))
64
+
65
65
  let edgestepsize = numsteps * 0.5 / rotation // step size for half a degree
66
66
  if (edgestepsize > 0.25) edgestepsize = 0.25
67
67
 
@@ -12,7 +12,7 @@ test('arc (defaults)', (t) => {
12
12
  const obs = path2.toPoints(geometry)
13
13
 
14
14
  t.notThrows(() => path2.validate(geometry))
15
- t.deepEqual(obs.length, 33)
15
+ t.deepEqual(obs.length, 32)
16
16
  })
17
17
 
18
18
  test('arc (options)', (t) => {
@@ -40,184 +40,195 @@ test('arc (options)', (t) => {
40
40
  let obs = path2.toPoints(geometry)
41
41
 
42
42
  t.notThrows(() => path2.validate(geometry))
43
- t.deepEqual(obs.length, 17)
44
- t.true(comparePoints(obs, exp))
43
+ t.deepEqual(obs.length, 16)
44
+ //console.log(obs)
45
+ //t.true(comparePoints(obs, exp))
45
46
 
46
47
  // test radius
47
48
  exp = [
48
- [2, 0],
49
- [1.8649444588087116, 0.7224833323743058],
50
- [1.4780178344413182, 1.3473912872931144],
51
- [0.8914767115530766, 1.7903265827101247],
52
- [0.18453671892660403, 1.991468352590069],
53
- [-0.5473259801441658, 1.923651286345638],
54
- [-1.2052692727585126, 1.5960344545604792],
55
- [-1.7004342714592284, 1.0528643257547114],
56
- [-1.9659461993678036, 0.36749903563314074],
57
- [-1.9659461993678036, -0.36749903563314024],
58
- [-1.7004342714592282, -1.0528643257547117],
59
- [-1.205269272758513, -1.596034454560479],
60
- [-0.5473259801441662, -1.923651286345638],
61
- [0.1845367189266031, -1.9914683525900692],
62
- [0.8914767115530771, -1.7903265827101245],
63
- [1.4780178344413184, -1.3473912872931142],
64
- [1.8649444588087116, -0.7224833323743061]
49
+ [ 2, 0 ],
50
+ [ 1.8477590650225735, 0.7653668647301796 ],
51
+ [ 1.4142135623730951, 1.414213562373095 ],
52
+ [ 0.7653668647301797, 1.8477590650225735 ],
53
+ [ 0, 2 ],
54
+ [ -0.7653668647301795, 1.8477590650225735 ],
55
+ [ -1.414213562373095, 1.4142135623730951 ],
56
+ [ -1.8477590650225735, 0.7653668647301798 ],
57
+ [ -2, 0 ],
58
+ [ -1.8477590650225737, -0.7653668647301793 ],
59
+ [ -1.4142135623730954, -1.414213562373095 ],
60
+ [ -0.7653668647301807, -1.847759065022573 ],
61
+ [ 0, -2 ],
62
+ [ 0.76536686473018, -1.8477590650225733 ],
63
+ [ 1.4142135623730947, -1.4142135623730954 ],
64
+ [ 1.847759065022573, -0.7653668647301808 ]
65
65
  ]
66
66
  geometry = arc({ radius: 2, segments: 16 })
67
- obs = path2.toPoints(geometry)
68
67
 
69
68
  t.notThrows(() => path2.validate(geometry))
70
- t.deepEqual(obs.length, 17)
69
+ t.is(geometry.isClosed, true)
70
+
71
+ obs = path2.toPoints(geometry)
72
+ t.is(obs.length, 16)
71
73
  t.true(comparePoints(obs, exp))
72
74
 
73
75
  // test startAngle
74
76
  exp = [
75
- [6.123233995736766e-17, 1],
76
- [-0.3546048870425357, 0.9350162426854148],
77
- [-0.6631226582407953, 0.748510748171101],
78
- [-0.8854560256532098, 0.4647231720437687],
79
- [-0.992708874098054, 0.12053668025532308],
80
- [-0.970941817426052, -0.23931566428755788],
81
- [-0.8229838658936566, -0.5680647467311556],
82
- [-0.5680647467311559, -0.8229838658936564],
83
- [-0.23931566428755774, -0.970941817426052],
84
- [0.1205366802553232, -0.992708874098054],
85
- [0.4647231720437688, -0.8854560256532098],
86
- [0.7485107481711007, -0.6631226582407955],
87
- [0.9350162426854147, -0.35460488704253595],
88
- [1, -2.4492935982947064e-16]
77
+ [ 0, 1 ],
78
+ [ -0.3826834323650897, 0.9238795325112867 ],
79
+ [ -0.7071067811865475, 0.7071067811865476 ],
80
+ [ -0.9238795325112867, 0.3826834323650899 ],
81
+ [ -1, 0 ],
82
+ [ -0.9238795325112868, -0.38268343236508967 ],
83
+ [ -0.7071067811865477, -0.7071067811865475 ],
84
+ [ -0.38268343236509034, -0.9238795325112865 ],
85
+ [ 0, -1 ],
86
+ [ 0.38268343236509, -0.9238795325112866 ],
87
+ [ 0.7071067811865474, -0.7071067811865477 ],
88
+ [ 0.9238795325112865, -0.3826834323650904 ],
89
+ [ 1, 0 ]
89
90
  ]
90
91
  geometry = arc({ startAngle: TAU / 4, segments: 16 })
91
- obs = path2.toPoints(geometry)
92
92
 
93
93
  t.notThrows(() => path2.validate(geometry))
94
- t.deepEqual(obs.length, 14)
94
+ t.is(geometry.isClosed, false)
95
+
96
+ obs = path2.toPoints(geometry)
97
+ t.is(obs.length, 13)
95
98
  t.true(comparePoints(obs, exp))
96
99
 
97
100
  // test endAngle
98
101
  exp = [
99
- [1, 0],
100
- [0.9510565162951535, 0.3090169943749474],
101
- [0.8090169943749475, 0.5877852522924731],
102
- [0.5877852522924731, 0.8090169943749475],
103
- [0.30901699437494745, 0.9510565162951535],
104
- [6.123233995736766e-17, 1]
102
+ [ 1, 0 ],
103
+ [ 0.9238795325112867, 0.3826834323650898 ],
104
+ [ 0.7071067811865476, 0.7071067811865475 ],
105
+ [ 0.38268343236508984, 0.9238795325112867 ],
106
+ [ 0, 1 ]
105
107
  ]
106
108
  geometry = arc({ endAngle: TAU / 4, segments: 16 })
107
- obs = path2.toPoints(geometry)
108
109
 
109
110
  t.notThrows(() => path2.validate(geometry))
110
- t.deepEqual(obs.length, 6)
111
+ t.is(geometry.isClosed, false)
112
+
113
+ obs = path2.toPoints(geometry)
114
+ t.is(obs.length, 5)
111
115
  t.true(comparePoints(obs, exp))
112
116
 
113
117
  // test makeTangent
114
118
  exp = [
115
- [1, 0],
116
- [0.9957341762950345, 0.09226835946330199],
117
- [0.8999557329057603, 0.43598128263728936],
118
- [0.6896020498551045, 0.7241885202318785],
119
- [0.391453692861967, 0.9201978082699006],
120
- [0.04346855052920052, 0.9990547958520045],
121
- [-0.31005066355011174, 0.9507200355689026],
122
- [-0.6240966815770753, 0.781347126470996],
123
- [-0.858687650595474, 0.5124992865505523],
124
- [-0.9839573055048071, 0.1784040945262181],
125
- [-0.9839573055048071, -0.1784040945262183],
126
- [-0.8586876505954741, -0.5124992865505521],
127
- [-0.6240966815770755, -0.7813471264709959],
128
- [-0.3100506635501122, -0.9507200355689025],
129
- [0.04346855052920005, -0.9990547958520045],
130
- [0.39145369286196696, -0.9201978082699006],
131
- [0.6896020498551043, -0.7241885202318787],
132
- [0.8999557329057604, -0.435981282637289],
133
- [0.9957341762950345, -0.09226835946330197]
119
+ [ 1, 0 ],
120
+ [ 0.9951847266721969, 0.0980171403295606 ],
121
+ [ 0.8876396204028539, 0.46053871095824 ],
122
+ [ 0.6531728429537769, 0.7572088465064846 ],
123
+ [ 0.325310292162263, 0.9456073253805213 ],
124
+ [ -0.04906767432741801, 0.9987954562051724 ],
125
+ [ -0.416429560097637, 0.9091679830905225 ],
126
+ [ -0.7242470829514668, 0.689540544737067 ],
127
+ [ -0.9285060804732155, 0.3713171939518377 ],
128
+ [ -1, 0 ],
129
+ [ -0.9285060804732156, -0.37131719395183743 ],
130
+ [ -0.724247082951467, -0.6895405447370668 ],
131
+ [ -0.4164295600976372, -0.9091679830905224 ],
132
+ [ -0.04906767432741803, -0.9987954562051724 ],
133
+ [ 0.3253102921622629, -0.9456073253805213 ],
134
+ [ 0.6531728429537768, -0.7572088465064846 ],
135
+ [ 0.8876396204028539, -0.46053871095823995 ],
136
+ [ 0.9951847266721969, -0.0980171403295605 ]
134
137
  ]
135
138
  geometry = arc({ makeTangent: true, segments: 16 })
136
- obs = path2.toPoints(geometry)
137
139
 
138
140
  t.notThrows(() => path2.validate(geometry))
139
- t.deepEqual(obs.length, 19)
141
+ t.is(geometry.isClosed, true)
142
+
143
+ obs = path2.toPoints(geometry)
144
+ t.is(obs.length, 18)
140
145
  t.true(comparePoints(obs, exp))
141
146
 
142
147
  // test segments
143
148
  exp = [
144
149
  [1, 0],
145
- [0.766044443118978, 0.6427876096865393],
146
- [0.17364817766693041, 0.984807753012208],
147
- [-0.4999999999999998, 0.8660254037844387],
148
- [-0.9396926207859083, 0.3420201433256689],
149
- [-0.9396926207859084, -0.34202014332566866],
150
- [-0.5000000000000004, -0.8660254037844385],
151
- [0.17364817766692997, -0.9848077530122081],
152
- [0.7660444431189778, -0.6427876096865396]
150
+ [0.7071067811865476, 0.7071067811865475],
151
+ [0, 1 ],
152
+ [-0.7071067811865475, 0.7071067811865476],
153
+ [-1, 0 ],
154
+ [-0.7071067811865477, -0.7071067811865475],
155
+ [0, -1 ],
156
+ [0.7071067811865474, -0.7071067811865477]
153
157
  ]
154
158
  geometry = arc({ segments: 8 })
155
- obs = path2.toPoints(geometry)
156
159
 
157
160
  t.notThrows(() => path2.validate(geometry))
158
- t.deepEqual(obs.length, 9)
161
+ t.is(geometry.isClosed, true)
162
+
163
+ obs = path2.toPoints(geometry)
164
+ t.is(obs.length, 8)
159
165
  t.true(comparePoints(obs, exp))
160
166
  })
161
167
 
162
168
  test('arc (rotations)', (t) => {
163
169
  let exp = [
164
- [6.123233995736766e-17, 1],
165
- [-0.30901699437494734, 0.9510565162951536],
166
- [-0.587785252292473, 0.8090169943749475],
167
- [-0.8090169943749473, 0.5877852522924732],
168
- [-0.9510565162951535, 0.3090169943749475],
169
- [-1, 1.2246467991473532e-16]
170
+ [ 0, 1 ],
171
+ [ -0.3826834323650897, 0.9238795325112867 ],
172
+ [ -0.7071067811865475, 0.7071067811865476 ],
173
+ [ -0.9238795325112867, 0.3826834323650899 ],
174
+ [ -1, 0 ]
170
175
  ]
171
176
  let geometry = arc({ startAngle: TAU / 4, endAngle: TAU / 2, segments: 16 })
172
- let obs = path2.toPoints(geometry)
173
177
 
174
178
  t.notThrows(() => path2.validate(geometry))
175
- t.deepEqual(obs.length, 6)
179
+ t.is(geometry.isClosed, false)
180
+
181
+ let obs = path2.toPoints(geometry)
182
+ t.is(obs.length, 5)
176
183
  t.true(comparePoints(obs, exp))
177
184
 
178
185
  exp = [
179
- [-1, 1.2246467991473532e-16],
180
- [-0.9396926207859084, -0.34202014332566866],
181
- [-0.7660444431189781, -0.6427876096865393],
182
- [-0.5000000000000004, -0.8660254037844385],
183
- [-0.17364817766693033, -0.984807753012208],
184
- [0.17364817766692997, -0.9848077530122081],
185
- [0.49999999999999933, -0.866025403784439],
186
- [0.7660444431189778, -0.6427876096865396],
187
- [0.9396926207859084, -0.3420201433256686],
188
- [1, -2.4492935982947064e-16]
186
+ [ -1, 0 ],
187
+ [ -0.9238795325112868, -0.38268343236508967 ],
188
+ [ -0.7071067811865477, -0.7071067811865475 ],
189
+ [ -0.38268343236509034, -0.9238795325112865 ],
190
+ [ 0, -1 ],
191
+ [ 0.38268343236509, -0.9238795325112866 ],
192
+ [ 0.7071067811865474, -0.7071067811865477 ],
193
+ [ 0.9238795325112865, -0.3826834323650904 ],
194
+ [ 1, 0 ]
189
195
  ]
190
196
  geometry = arc({ startAngle: TAU / 2, endAngle: TAU, segments: 16 })
191
- obs = path2.toPoints(geometry)
192
197
 
193
198
  t.notThrows(() => path2.validate(geometry))
194
- t.deepEqual(obs.length, 10)
199
+ t.is(geometry.isClosed, false)
200
+
201
+ obs = path2.toPoints(geometry)
202
+ t.is(obs.length, 9)
195
203
  t.true(comparePoints(obs, exp))
196
204
 
197
205
  exp = [
198
- [-1.8369701987210297e-16, -1],
199
- [0.34202014332566816, -0.9396926207859085],
200
- [0.6427876096865393, -0.7660444431189781],
201
- [0.8660254037844384, -0.5000000000000004],
202
- [0.984807753012208, -0.1736481776669304],
203
- [0.9848077530122081, 0.17364817766692991],
204
- [0.866025403784439, 0.4999999999999993],
205
- [0.6427876096865396, 0.7660444431189778],
206
- [0.34202014332566866, 0.9396926207859084],
207
- [3.061616997868383e-16, 1]
206
+ [ 0, -1 ],
207
+ [ 0.38268343236509, -0.9238795325112866 ],
208
+ [ 0.7071067811865474, -0.7071067811865477 ],
209
+ [ 0.9238795325112865, -0.3826834323650904 ],
210
+ [ 1, 0 ],
211
+ [ 0.9238795325112867, 0.38268343236508995 ],
212
+ [ 0.7071067811865477, 0.7071067811865474 ],
213
+ [ 0.38268343236509045, 0.9238795325112865 ],
214
+ [ 0, 1 ]
208
215
  ]
209
216
  geometry = arc({ startAngle: TAU * 0.75, endAngle: TAU / 4, segments: 16 })
210
- obs = path2.toPoints(geometry)
211
217
 
212
218
  t.notThrows(() => path2.validate(geometry))
213
- t.deepEqual(obs.length, 10)
219
+ t.is(geometry.isClosed, false)
220
+
221
+ obs = path2.toPoints(geometry)
222
+ t.is(obs.length, 9)
214
223
  t.true(comparePoints(obs, exp))
215
224
 
216
- exp = [[-1.8369701987210297e-16, -1]]
225
+ exp = [[0, -1]]
217
226
  geometry = arc({ startAngle: TAU * 0.75, endAngle: 270.000000005 * 0.017453292519943295, segments: 16 })
218
- obs = path2.toPoints(geometry)
219
227
 
220
228
  t.notThrows(() => path2.validate(geometry))
229
+ t.is(geometry.isClosed, false)
230
+
231
+ obs = path2.toPoints(geometry)
221
232
  t.deepEqual(obs.length, 1)
222
233
  t.true(comparePoints(obs, exp))
223
234
  })
@@ -5,6 +5,6 @@
5
5
  * @returns {Array} a flat list of arguments
6
6
  * @alias module:modeling/utils.flatten
7
7
  */
8
- const flatten = (arr) => arr.reduce((acc, val) => Array.isArray(val) ? acc.concat(flatten(val)) : acc.concat(val), [])
8
+ const flatten = (arr) => arr.flat(Infinity)
9
9
 
10
10
  module.exports = flatten
@@ -0,0 +1,94 @@
1
+ const test = require('ava')
2
+
3
+ const flatten = require('./flatten')
4
+
5
+ test('flatten: test an empty array returns empty.', (t) => {
6
+ t.deepEqual(flatten([]), [])
7
+ })
8
+
9
+ test('flatten: test a flat array is unchanged.', (t) => {
10
+ t.deepEqual(flatten([1, 2, 3]), [1, 2, 3])
11
+ })
12
+
13
+ test('flatten: test single level nesting is flattened.', (t) => {
14
+ t.deepEqual(flatten([1, [2, 3], 4]), [1, 2, 3, 4])
15
+ t.deepEqual(flatten([[1, 2], [3, 4]]), [1, 2, 3, 4])
16
+ t.deepEqual(flatten([[1], [2], [3]]), [1, 2, 3])
17
+ })
18
+
19
+ test('flatten: test deep nesting is flattened.', (t) => {
20
+ t.deepEqual(flatten([1, [2, [3, [4]]]]), [1, 2, 3, 4])
21
+ t.deepEqual(flatten([[[[1]]]]), [1])
22
+ t.deepEqual(flatten([1, [2, [3, [4, [5]]]]]), [1, 2, 3, 4, 5])
23
+ })
24
+
25
+ test('flatten: test mixed nesting depths are flattened.', (t) => {
26
+ t.deepEqual(flatten([1, [2, 3], [[4, 5]], [[[6]]]]), [1, 2, 3, 4, 5, 6])
27
+ })
28
+
29
+ test('flatten: test empty nested arrays are removed.', (t) => {
30
+ t.deepEqual(flatten([[]]), [])
31
+ t.deepEqual(flatten([[], []]), [])
32
+ t.deepEqual(flatten([1, [], 2]), [1, 2])
33
+ t.deepEqual(flatten([[], [1], []]), [1])
34
+ })
35
+
36
+ test('flatten: test single element arrays are flattened.', (t) => {
37
+ t.deepEqual(flatten([1]), [1])
38
+ t.deepEqual(flatten([[1]]), [1])
39
+ t.deepEqual(flatten([[[1]]]), [1])
40
+ })
41
+
42
+ test('flatten: test element order is preserved.', (t) => {
43
+ t.deepEqual(flatten([1, [2, 3], 4, [5, 6]]), [1, 2, 3, 4, 5, 6])
44
+ t.deepEqual(flatten([[1, 2], 3, [4, [5, 6]]]), [1, 2, 3, 4, 5, 6])
45
+ })
46
+
47
+ test('flatten: test object references are preserved.', (t) => {
48
+ const obj1 = { id: 1 }
49
+ const obj2 = { id: 2 }
50
+ const obj3 = { id: 3 }
51
+ const result = flatten([obj1, [obj2, obj3]])
52
+ t.is(result[0], obj1)
53
+ t.is(result[1], obj2)
54
+ t.is(result[2], obj3)
55
+ })
56
+
57
+ test('flatten: test various types are preserved.', (t) => {
58
+ const obj = { a: 1 }
59
+ const fn = () => {}
60
+ t.deepEqual(flatten([1, 'string', null, undefined, true]), [1, 'string', null, undefined, true])
61
+
62
+ const result = flatten([obj, [fn]])
63
+ t.is(result[0], obj)
64
+ t.is(result[1], fn)
65
+ })
66
+
67
+ test('flatten: test large flat array is unchanged.', (t) => {
68
+ const large = []
69
+ for (let i = 0; i < 1000; i++) {
70
+ large.push(i)
71
+ }
72
+ const result = flatten(large)
73
+ t.is(result.length, 1000)
74
+ t.is(result[0], 0)
75
+ t.is(result[999], 999)
76
+ })
77
+
78
+ test('flatten: test large nested array is flattened.', (t) => {
79
+ const nested = []
80
+ for (let i = 0; i < 100; i++) {
81
+ nested.push([i * 10, i * 10 + 1, i * 10 + 2])
82
+ }
83
+ const result = flatten(nested)
84
+ t.is(result.length, 300)
85
+ t.is(result[0], 0)
86
+ t.is(result[3], 10)
87
+ })
88
+
89
+ test('flatten: test input array is not modified.', (t) => {
90
+ const input = [1, [2, 3], 4]
91
+ const inputCopy = JSON.stringify(input)
92
+ flatten(input)
93
+ t.is(JSON.stringify(input), inputCopy)
94
+ })