@jscad/modeling 2.8.0 → 2.9.2

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 (100) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/jscad-modeling.min.js +433 -391
  3. package/package.json +2 -2
  4. package/src/geometries/geom2/index.d.ts +1 -0
  5. package/src/geometries/geom2/index.js +2 -1
  6. package/src/geometries/geom2/validate.d.ts +3 -0
  7. package/src/geometries/geom2/validate.js +36 -0
  8. package/src/geometries/geom3/index.d.ts +1 -0
  9. package/src/geometries/geom3/index.js +2 -1
  10. package/src/geometries/geom3/isA.js +1 -1
  11. package/src/geometries/geom3/validate.d.ts +3 -0
  12. package/src/geometries/geom3/validate.js +62 -0
  13. package/src/geometries/path2/index.d.ts +1 -0
  14. package/src/geometries/path2/index.js +2 -1
  15. package/src/geometries/path2/validate.d.ts +3 -0
  16. package/src/geometries/path2/validate.js +41 -0
  17. package/src/geometries/poly2/arePointsInside.js +0 -35
  18. package/src/geometries/poly3/index.d.ts +1 -0
  19. package/src/geometries/poly3/index.js +2 -1
  20. package/src/geometries/poly3/invert.js +7 -1
  21. package/src/geometries/poly3/measureArea.test.js +16 -16
  22. package/src/geometries/poly3/measureBoundingSphere.test.js +8 -8
  23. package/src/geometries/poly3/validate.d.ts +4 -0
  24. package/src/geometries/poly3/validate.js +50 -0
  25. package/src/measurements/measureCenterOfMass.test.js +2 -2
  26. package/src/operations/booleans/intersect.test.js +8 -0
  27. package/src/operations/booleans/scission.test.js +4 -4
  28. package/src/operations/booleans/subtract.test.js +8 -0
  29. package/src/operations/booleans/trees/Node.js +10 -16
  30. package/src/operations/booleans/trees/PolygonTreeNode.js +13 -14
  31. package/src/operations/booleans/trees/Tree.js +1 -2
  32. package/src/operations/booleans/trees/splitPolygonByPlane.js +2 -3
  33. package/src/operations/booleans/union.test.js +27 -0
  34. package/src/operations/expansions/expand.test.js +30 -21
  35. package/src/operations/expansions/expandShell.js +2 -2
  36. package/src/operations/expansions/offset.test.js +25 -0
  37. package/src/operations/extrusions/earcut/assignHoles.js +91 -0
  38. package/src/operations/extrusions/earcut/assignHoles.test.js +74 -0
  39. package/src/operations/extrusions/earcut/eliminateHoles.js +131 -0
  40. package/src/operations/extrusions/earcut/index.js +252 -0
  41. package/src/operations/extrusions/earcut/linkedList.js +58 -0
  42. package/src/operations/extrusions/earcut/linkedListSort.js +54 -0
  43. package/src/operations/extrusions/earcut/linkedPolygon.js +197 -0
  44. package/src/operations/extrusions/earcut/polygonHierarchy.js +64 -0
  45. package/src/operations/extrusions/earcut/triangle.js +16 -0
  46. package/src/operations/extrusions/extrudeFromSlices.js +10 -3
  47. package/src/operations/extrusions/extrudeFromSlices.test.js +47 -31
  48. package/src/operations/extrusions/extrudeLinear.js +4 -3
  49. package/src/operations/extrusions/extrudeLinear.test.js +69 -37
  50. package/src/operations/extrusions/extrudeLinearGeom2.js +5 -2
  51. package/src/operations/extrusions/extrudeRectangular.test.js +22 -15
  52. package/src/operations/extrusions/extrudeRotate.test.js +31 -27
  53. package/src/operations/extrusions/project.test.js +5 -5
  54. package/src/operations/extrusions/slice/calculatePlane.js +7 -4
  55. package/src/operations/extrusions/slice/repairSlice.js +47 -0
  56. package/src/operations/extrusions/slice/toPolygons.js +24 -60
  57. package/src/operations/hulls/hull.test.js +24 -1
  58. package/src/operations/hulls/hullChain.test.js +6 -4
  59. package/src/operations/hulls/hullPath2.test.js +1 -1
  60. package/src/operations/modifiers/generalize.test.js +6 -0
  61. package/src/operations/transforms/align.test.js +12 -0
  62. package/src/operations/transforms/center.test.js +12 -0
  63. package/src/operations/transforms/mirror.test.js +16 -0
  64. package/src/operations/transforms/rotate.test.js +10 -0
  65. package/src/operations/transforms/scale.test.js +15 -0
  66. package/src/operations/transforms/transform.test.js +5 -0
  67. package/src/operations/transforms/translate.test.js +16 -0
  68. package/src/primitives/arc.test.js +11 -0
  69. package/src/primitives/circle.test.js +15 -9
  70. package/src/primitives/cube.test.js +3 -0
  71. package/src/primitives/cuboid.test.js +9 -24
  72. package/src/primitives/cylinder.test.js +7 -4
  73. package/src/primitives/cylinderElliptic.js +13 -6
  74. package/src/primitives/cylinderElliptic.test.js +72 -50
  75. package/src/primitives/ellipse.js +3 -1
  76. package/src/primitives/ellipse.test.js +14 -8
  77. package/src/primitives/ellipsoid.js +6 -4
  78. package/src/primitives/ellipsoid.test.js +84 -80
  79. package/src/primitives/geodesicSphere.test.js +3 -0
  80. package/src/primitives/line.test.js +1 -0
  81. package/src/primitives/polygon.test.js +15 -10
  82. package/src/primitives/polyhedron.test.js +14 -42
  83. package/src/primitives/rectangle.test.js +3 -0
  84. package/src/primitives/roundedCuboid.test.js +5 -0
  85. package/src/primitives/roundedCylinder.js +6 -4
  86. package/src/primitives/roundedCylinder.test.js +40 -36
  87. package/src/primitives/roundedRectangle.test.js +5 -0
  88. package/src/primitives/sphere.test.js +52 -73
  89. package/src/primitives/square.test.js +3 -0
  90. package/src/primitives/star.test.js +6 -0
  91. package/src/primitives/torus.test.js +8 -1
  92. package/src/primitives/triangle.test.js +7 -0
  93. package/src/utils/areAllShapesTheSameType.js +2 -2
  94. package/src/utils/areAllShapesTheSameType.test.js +17 -0
  95. package/src/utils/index.d.ts +1 -0
  96. package/src/utils/index.js +3 -1
  97. package/src/utils/trigonometry.d.ts +2 -0
  98. package/src/utils/trigonometry.js +35 -0
  99. package/src/utils/trigonometry.test.js +25 -0
  100. package/test/helpers/nearlyEqual.js +4 -1
@@ -0,0 +1,197 @@
1
+ const { Node, insertNode, removeNode } = require('./linkedList')
2
+ const { area } = require('./triangle')
3
+
4
+ /*
5
+ * create a circular doubly linked list from polygon points in the specified winding order
6
+ */
7
+ const linkedPolygon = (data, start, end, dim, clockwise) => {
8
+ let last
9
+
10
+ if (clockwise === (signedArea(data, start, end, dim) > 0)) {
11
+ for (let i = start; i < end; i += dim) {
12
+ last = insertNode(i, data[i], data[i + 1], last)
13
+ }
14
+ } else {
15
+ for (let i = end - dim; i >= start; i -= dim) {
16
+ last = insertNode(i, data[i], data[i + 1], last)
17
+ }
18
+ }
19
+
20
+ if (last && equals(last, last.next)) {
21
+ removeNode(last)
22
+ last = last.next
23
+ }
24
+
25
+ return last
26
+ }
27
+
28
+ /*
29
+ * eliminate colinear or duplicate points
30
+ */
31
+ const filterPoints = (start, end) => {
32
+ if (!start) return start
33
+ if (!end) end = start
34
+
35
+ let p = start
36
+ let again
37
+ do {
38
+ again = false
39
+
40
+ if (!p.steiner && (equals(p, p.next) || area(p.prev, p, p.next) === 0)) {
41
+ removeNode(p)
42
+ p = end = p.prev
43
+ if (p === p.next) break
44
+ again = true
45
+ } else {
46
+ p = p.next
47
+ }
48
+ } while (again || p !== end)
49
+
50
+ return end
51
+ }
52
+
53
+ /*
54
+ * go through all polygon nodes and cure small local self-intersections
55
+ */
56
+ const cureLocalIntersections = (start, triangles, dim) => {
57
+ let p = start
58
+ do {
59
+ const a = p.prev
60
+ const b = p.next.next
61
+
62
+ if (!equals(a, b) && intersects(a, p, p.next, b) && locallyInside(a, b) && locallyInside(b, a)) {
63
+ triangles.push(a.i / dim)
64
+ triangles.push(p.i / dim)
65
+ triangles.push(b.i / dim)
66
+
67
+ // remove two nodes involved
68
+ removeNode(p)
69
+ removeNode(p.next)
70
+
71
+ p = start = b
72
+ }
73
+
74
+ p = p.next
75
+ } while (p !== start)
76
+
77
+ return filterPoints(p)
78
+ }
79
+
80
+ /*
81
+ * check if a polygon diagonal intersects any polygon segments
82
+ */
83
+ const intersectsPolygon = (a, b) => {
84
+ let p = a
85
+ do {
86
+ if (p.i !== a.i && p.next.i !== a.i && p.i !== b.i && p.next.i !== b.i &&
87
+ intersects(p, p.next, a, b)) return true
88
+ p = p.next
89
+ } while (p !== a)
90
+
91
+ return false
92
+ }
93
+
94
+ /*
95
+ * check if a polygon diagonal is locally inside the polygon
96
+ */
97
+ const locallyInside = (a, b) => area(a.prev, a, a.next) < 0
98
+ ? area(a, b, a.next) >= 0 && area(a, a.prev, b) >= 0
99
+ : area(a, b, a.prev) < 0 || area(a, a.next, b) < 0
100
+
101
+ /*
102
+ * check if the middle point of a polygon diagonal is inside the polygon
103
+ */
104
+ const middleInside = (a, b) => {
105
+ let p = a
106
+ let inside = false
107
+ const px = (a.x + b.x) / 2
108
+ const py = (a.y + b.y) / 2
109
+ do {
110
+ if (((p.y > py) !== (p.next.y > py)) && p.next.y !== p.y &&
111
+ (px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x)) { inside = !inside }
112
+ p = p.next
113
+ } while (p !== a)
114
+
115
+ return inside
116
+ }
117
+
118
+ /*
119
+ * link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two
120
+ * if one belongs to the outer ring and another to a hole, it merges it into a single ring
121
+ */
122
+ const splitPolygon = (a, b) => {
123
+ const a2 = new Node(a.i, a.x, a.y)
124
+ const b2 = new Node(b.i, b.x, b.y)
125
+ const an = a.next
126
+ const bp = b.prev
127
+
128
+ a.next = b
129
+ b.prev = a
130
+
131
+ a2.next = an
132
+ an.prev = a2
133
+
134
+ b2.next = a2
135
+ a2.prev = b2
136
+
137
+ bp.next = b2
138
+ b2.prev = bp
139
+
140
+ return b2
141
+ }
142
+
143
+ /*
144
+ * check if a diagonal between two polygon nodes is valid (lies in polygon interior)
145
+ */
146
+ const isValidDiagonal = (a, b) => a.next.i !== b.i &&
147
+ a.prev.i !== b.i &&
148
+ !intersectsPolygon(a, b) && // doesn't intersect other edges
149
+ (
150
+ locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b) && // locally visible
151
+ (area(a.prev, a, b.prev) || area(a, b.prev, b)) || // does not create opposite-facing sectors
152
+ equals(a, b) && area(a.prev, a, a.next) > 0 && area(b.prev, b, b.next) > 0
153
+ )
154
+
155
+ /*
156
+ * check if two segments intersect
157
+ */
158
+ const intersects = (p1, q1, p2, q2) => {
159
+ const o1 = Math.sign(area(p1, q1, p2))
160
+ const o2 = Math.sign(area(p1, q1, q2))
161
+ const o3 = Math.sign(area(p2, q2, p1))
162
+ const o4 = Math.sign(area(p2, q2, q1))
163
+
164
+ if (o1 !== o2 && o3 !== o4) return true // general case
165
+
166
+ if (o1 === 0 && onSegment(p1, p2, q1)) return true // p1, q1 and p2 are colinear and p2 lies on p1q1
167
+ if (o2 === 0 && onSegment(p1, q2, q1)) return true // p1, q1 and q2 are colinear and q2 lies on p1q1
168
+ if (o3 === 0 && onSegment(p2, p1, q2)) return true // p2, q2 and p1 are colinear and p1 lies on p2q2
169
+ if (o4 === 0 && onSegment(p2, q1, q2)) return true // p2, q2 and q1 are colinear and q1 lies on p2q2
170
+
171
+ return false
172
+ }
173
+
174
+ /*
175
+ * for colinear points p, q, r, check if point q lies on segment pr
176
+ */
177
+ const onSegment = (p, q, r) => q.x <= Math.max(p.x, r.x) &&
178
+ q.x >= Math.min(p.x, r.x) &&
179
+ q.y <= Math.max(p.y, r.y) &&
180
+ q.y >= Math.min(p.y, r.y)
181
+
182
+ const signedArea = (data, start, end, dim) => {
183
+ let sum = 0
184
+ for (let i = start, j = end - dim; i < end; i += dim) {
185
+ sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1])
186
+ j = i
187
+ }
188
+
189
+ return sum
190
+ }
191
+
192
+ /*
193
+ * check if two points are equal
194
+ */
195
+ const equals = (p1, p2) => p1.x === p2.x && p1.y === p2.y
196
+
197
+ module.exports = { cureLocalIntersections, filterPoints, isValidDiagonal, linkedPolygon, locallyInside, splitPolygon }
@@ -0,0 +1,64 @@
1
+ const geom2 = require('../../../geometries/geom2')
2
+ const plane = require('../../../maths/plane')
3
+ const vec2 = require('../../../maths/vec2')
4
+ const vec3 = require('../../../maths/vec3')
5
+ const calculatePlane = require('../slice/calculatePlane')
6
+ const assignHoles = require('./assignHoles')
7
+
8
+ /*
9
+ * Constructs a polygon hierarchy which associates holes with their outer solids.
10
+ * This class maps a 3D polygon onto a 2D space using an orthonormal basis.
11
+ * It tracks the mapping so that points can be reversed back to 3D losslessly.
12
+ */
13
+ class PolygonHierarchy {
14
+ constructor (slice) {
15
+ this.plane = calculatePlane(slice)
16
+
17
+ // create an orthonormal basis
18
+ // choose an arbitrary right hand vector, making sure it is somewhat orthogonal to the plane normal
19
+ const rightvector = vec3.orthogonal(vec3.create(), this.plane)
20
+ const perp = vec3.cross(vec3.create(), this.plane, rightvector)
21
+ this.v = vec3.normalize(perp, perp)
22
+ this.u = vec3.cross(vec3.create(), this.v, this.plane)
23
+
24
+ // map from 2D to original 3D points
25
+ this.basisMap = new Map()
26
+
27
+ // project slice onto 2D plane
28
+ const projected = slice.edges.map((e) => e.map((v) => this.to2D(v)))
29
+
30
+ // compute polygon hierarchies, assign holes to solids
31
+ const geometry = geom2.create(projected)
32
+ this.roots = assignHoles(geometry)
33
+ }
34
+
35
+ /*
36
+ * project a 3D point onto the 2D plane
37
+ */
38
+ to2D (vector3) {
39
+ const vector2 = vec2.fromValues(vec3.dot(vector3, this.u), vec3.dot(vector3, this.v))
40
+ this.basisMap.set(vector2, vector3)
41
+ return vector2
42
+ }
43
+
44
+ /*
45
+ * un-project a 2D point back into 3D
46
+ */
47
+ to3D (vector2) {
48
+ // use a map to get the original 3D, no floating point error
49
+ const original = this.basisMap.get(vector2)
50
+ if (original) {
51
+ return original
52
+ } else {
53
+ console.log('Warning: point not in original slice')
54
+ const v1 = vec3.scale(vec3.create(), this.u, vector2[0])
55
+ const v2 = vec3.scale(vec3.create(), this.v, vector2[1])
56
+
57
+ const planeOrigin = vec3.scale(vec3.create(), plane, plane[3])
58
+ const v3 = vec3.add(v1, v1, planeOrigin)
59
+ return vec3.add(v2, v2, v3)
60
+ }
61
+ }
62
+ }
63
+
64
+ module.exports = PolygonHierarchy
@@ -0,0 +1,16 @@
1
+
2
+ /*
3
+ * check if a point lies within a convex triangle
4
+ */
5
+ const pointInTriangle = (ax, ay, bx, by, cx, cy, px, py) => (
6
+ (cx - px) * (ay - py) - (ax - px) * (cy - py) >= 0 &&
7
+ (ax - px) * (by - py) - (bx - px) * (ay - py) >= 0 &&
8
+ (bx - px) * (cy - py) - (cx - px) * (by - py) >= 0
9
+ )
10
+
11
+ /*
12
+ * signed area of a triangle
13
+ */
14
+ const area = (p, q, r) => (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y)
15
+
16
+ module.exports = { area, pointInTriangle }
@@ -5,6 +5,7 @@ const geom3 = require('../../geometries/geom3')
5
5
  const poly3 = require('../../geometries/poly3')
6
6
 
7
7
  const slice = require('./slice')
8
+ const repairSlice = require('./slice/repairSlice')
8
9
 
9
10
  const extrudeWalls = require('./extrudeWalls')
10
11
 
@@ -25,6 +26,7 @@ const defaultCallback = (progress, index, base) => {
25
26
  * @param {Boolean} [options.capStart=true] the solid should have a cap at the start
26
27
  * @param {Boolean} [options.capEnd=true] the solid should have a cap at the end
27
28
  * @param {Boolean} [options.close=false] the solid should have a closing section between start and end
29
+ * @param {Boolean} [options.repair=true] - repair gaps in the geometry
28
30
  * @param {Function} [options.callback] the callback function that generates each slice
29
31
  * @param {Object} base - the base object which is used to create slices (see the example for callback information)
30
32
  * @return {geom3} the extruded shape
@@ -48,12 +50,18 @@ const extrudeFromSlices = (options, base) => {
48
50
  capStart: true,
49
51
  capEnd: true,
50
52
  close: false,
53
+ repair: true,
51
54
  callback: defaultCallback
52
55
  }
53
- const { numberOfSlices, capStart, capEnd, close, callback: generate } = Object.assign({ }, defaults, options)
56
+ const { numberOfSlices, capStart, capEnd, close, repair, callback: generate } = Object.assign({ }, defaults, options)
54
57
 
55
58
  if (numberOfSlices < 2) throw new Error('numberOfSlices must be 2 or more')
56
59
 
60
+ // Repair gaps in the base slice
61
+ if (repair) {
62
+ repairSlice(base)
63
+ }
64
+
57
65
  const sMax = numberOfSlices - 1
58
66
 
59
67
  let startSlice = null
@@ -90,8 +98,7 @@ const extrudeFromSlices = (options, base) => {
90
98
  }
91
99
  if (capStart) {
92
100
  // create a cap at the start
93
- slice.reverse(startSlice, startSlice)
94
- const startPolygons = slice.toPolygons(startSlice)
101
+ const startPolygons = slice.toPolygons(startSlice).map(poly3.invert)
95
102
  polygons = polygons.concat(startPolygons)
96
103
  }
97
104
  if (!capStart && !capEnd) {
@@ -16,25 +16,28 @@ test('extrudeFromSlices (defaults)', (t) => {
16
16
  let geometry3 = extrudeFromSlices({ }, geometry2)
17
17
  let pts = geom3.toPoints(geometry3)
18
18
  const exp = [
19
- [[10.0, -10.0, 0.0], [10.0, 10.0, 0.0], [10.0, 10.0, 1.0]],
20
- [[10.0, -10.0, 0.0], [10.0, 10.0, 1.0], [10.0, -10.0, 1.0]],
21
- [[10.0, 10.0, 0.0], [-10.0, 10.0, 0.0], [-10.0, 10.0, 1.0]],
22
- [[10.0, 10.0, 0.0], [-10.0, 10.0, 1.0], [10.0, 10.0, 1.0]],
23
- [[-10.0, 10.0, 0.0], [-10.0, -10.0, 0.0], [-10.0, -10.0, 1.0]],
24
- [[-10.0, 10.0, 0.0], [-10.0, -10.0, 1.0], [-10.0, 10.0, 1.0]],
25
- [[-10.0, -10.0, 0.0], [10.0, -10.0, 0.0], [10.0, -10.0, 1.0]],
26
- [[-10.0, -10.0, 0.0], [10.0, -10.0, 1.0], [-10.0, -10.0, 1.0]],
27
- [[10, 10, 1], [-10, 10, 1], [-10, -10, 1], [10, -10, 1]],
28
- [[10, -10, 0], [-10, -10, 0], [-10, 10, 0], [10, 10, 0]]
19
+ [[10, -10, 0], [10, 10, 0], [10, 10, 1]],
20
+ [[10, -10, 0], [10, 10, 1], [10, -10, 1]],
21
+ [[10, 10, 0], [-10, 10, 0], [-10, 10, 1]],
22
+ [[10, 10, 0], [-10, 10, 1], [10, 10, 1]],
23
+ [[-10, 10, 0], [-10, -10, 0], [-10, -10, 1]],
24
+ [[-10, 10, 0], [-10, -10, 1], [-10, 10, 1]],
25
+ [[-10, -10, 0], [10, -10, 0], [10, -10, 1]],
26
+ [[-10, -10, 0], [10, -10, 1], [-10, -10, 1]],
27
+ [[-10, -10, 1], [10, -10, 1], [10, 10, 1]],
28
+ [[10, 10, 1], [-10, 10, 1], [-10, -10, 1]],
29
+ [[10, 10, 0], [10, -10, 0], [-10, -10, 0]],
30
+ [[-10, -10, 0], [-10, 10, 0], [10, 10, 0]]
29
31
  ]
30
- t.is(pts.length, 10)
32
+ t.is(pts.length, 12)
31
33
  t.true(comparePolygonsAsPoints(pts, exp))
32
34
 
33
35
  const poly2 = poly3.fromPoints([[10, 10, 0], [-10, 10, 0], [-10, -10, 0], [10, -10, 0]])
34
36
  geometry3 = extrudeFromSlices({ }, poly2)
35
37
  pts = geom3.toPoints(geometry3)
36
38
 
37
- t.is(pts.length, 10)
39
+ t.notThrows(() => geom3.validate(geometry3))
40
+ t.is(pts.length, 12)
38
41
  t.true(comparePolygonsAsPoints(pts, exp))
39
42
  })
40
43
 
@@ -66,6 +69,7 @@ test('extrudeFromSlices (torus)', (t) => {
66
69
  }, hex
67
70
  )
68
71
  const pts = geom3.toPoints(geometry3)
72
+ t.notThrows(() => geom3.validate(geometry3))
69
73
  t.is(pts.length, 96)
70
74
  })
71
75
 
@@ -84,7 +88,9 @@ test('extrudeFromSlices (same shape, changing dimensions)', (t) => {
84
88
  }, base
85
89
  )
86
90
  const pts = geom3.toPoints(geometry3)
87
- t.is(pts.length, 25)
91
+ // expected to throw because capEnd is false (non-closed geometry)
92
+ t.throws(() => geom3.validate(geometry3))
93
+ t.is(pts.length, 26)
88
94
  })
89
95
 
90
96
  test('extrudeFromSlices (changing shape, changing dimensions)', (t) => {
@@ -101,20 +107,21 @@ test('extrudeFromSlices (changing shape, changing dimensions)', (t) => {
101
107
  }, base
102
108
  )
103
109
  const pts = geom3.toPoints(geometry3)
104
- t.is(pts.length, 298)
110
+ t.notThrows.skip(() => geom3.validate(geometry3))
111
+ t.is(pts.length, 304)
105
112
  })
106
113
 
107
114
  test('extrudeFromSlices (holes)', (t) => {
108
115
  const geometry2 = geom2.create(
109
116
  [
110
- [[-10.0, 10.0], [-10.0, -10.0]],
111
- [[-10.0, -10.0], [10.0, -10.0]],
112
- [[10.0, -10.0], [10.0, 10.0]],
113
- [[10.0, 10.0], [-10.0, 10.0]],
114
- [[-5.0, -5.0], [-5.0, 5.0]],
115
- [[5.0, -5.0], [-5.0, -5.0]],
116
- [[5.0, 5.0], [5.0, -5.0]],
117
- [[-5.0, 5.0], [5.0, 5.0]]
117
+ [[-10, 10], [-10, -10]],
118
+ [[-10, -10], [10, -10]],
119
+ [[10, -10], [10, 10]],
120
+ [[10, 10], [-10, 10]],
121
+ [[-5, -5], [-5, 5]],
122
+ [[5, -5], [-5, -5]],
123
+ [[5, 5], [5, -5]],
124
+ [[-5, 5], [5, 5]]
118
125
  ]
119
126
  )
120
127
  const geometry3 = extrudeFromSlices({ }, geometry2)
@@ -136,15 +143,24 @@ test('extrudeFromSlices (holes)', (t) => {
136
143
  [[5, 5, 0], [5, -5, 1], [5, 5, 1]],
137
144
  [[-5, 5, 0], [5, 5, 0], [5, 5, 1]],
138
145
  [[-5, 5, 0], [5, 5, 1], [-5, 5, 1]],
139
- [[-10, -10, 1], [-5, -10, 1], [-5, 10, 1], [-10, 10, 1]],
140
- [[10, -10, 1], [10, -5, 1], [-5, -5, 1], [-5, -10, 1]],
141
- [[10, 10, 1], [5, 10, 1], [5, -5, 1], [10, -5, 1]],
142
- [[5, 5, 1], [5, 10, 1], [-5, 10, 1], [-5, 5, 1]],
143
- [[-10, 10, 0], [-5, 10, 0], [-5, -10, 0], [-10, -10, 0]],
144
- [[10, -10, 0], [-5, -10, 0], [-5, -5, 0], [10, -5, 0]],
145
- [[10, -5, 0], [5, -5, 0], [5, 10, 0], [10, 10, 0]],
146
- [[5, 10, 0], [5, 5, 0], [-5, 5, 0], [-5, 10, 0]]
146
+ [[10, -10, 1], [10, 10, 1], [5, 5, 1]],
147
+ [[-5, 5, 1], [5, 5, 1], [10, 10, 1]],
148
+ [[10, -10, 1], [5, 5, 1], [5, -5, 1]],
149
+ [[-5, 5, 1], [10, 10, 1], [-10, 10, 1]],
150
+ [[-10, -10, 1], [10, -10, 1], [5, -5, 1]],
151
+ [[-5, -5, 1], [-5, 5, 1], [-10, 10, 1]],
152
+ [[-10, -10, 1], [5, -5, 1], [-5, -5, 1]],
153
+ [[-5, -5, 1], [-10, 10, 1], [-10, -10, 1]],
154
+ [[5, 5, 0], [10, 10, 0], [10, -10, 0]],
155
+ [[10, 10, 0], [5, 5, 0], [-5, 5, 0]],
156
+ [[5, -5, 0], [5, 5, 0], [10, -10, 0]],
157
+ [[-10, 10, 0], [10, 10, 0], [-5, 5, 0]],
158
+ [[5, -5, 0], [10, -10, 0], [-10, -10, 0]],
159
+ [[-10, 10, 0], [-5, 5, 0], [-5, -5, 0]],
160
+ [[-5, -5, 0], [5, -5, 0], [-10, -10, 0]],
161
+ [[-10, -10, 0], [-10, 10, 0], [-5, -5, 0]]
147
162
  ]
148
- t.is(pts.length, 24)
163
+ t.notThrows(() => geom3.validate(geometry3))
164
+ t.is(pts.length, 32)
149
165
  t.true(comparePolygonsAsPoints(pts, exp))
150
166
  })
@@ -25,14 +25,15 @@ const extrudeLinear = (options, ...objects) => {
25
25
  const defaults = {
26
26
  height: 1,
27
27
  twistAngle: 0,
28
- twistSteps: 1
28
+ twistSteps: 1,
29
+ repair: true
29
30
  }
30
- const { height, twistAngle, twistSteps } = Object.assign({ }, defaults, options)
31
+ const { height, twistAngle, twistSteps, repair } = Object.assign({ }, defaults, options)
31
32
 
32
33
  objects = flatten(objects)
33
34
  if (objects.length === 0) throw new Error('wrong number of arguments')
34
35
 
35
- options = { offset: [0, 0, height], twistAngle: twistAngle, twistSteps: twistSteps }
36
+ options = { offset: [0, 0, height], twistAngle, twistSteps, repair }
36
37
 
37
38
  const results = objects.map((object) => {
38
39
  if (path2.isA(object)) return extrudeLinearPath2(options, object)
@@ -20,10 +20,13 @@ test('extrudeLinear (defaults)', (t) => {
20
20
  [[-5, 5, 0], [-5, -5, 1], [-5, 5, 1]],
21
21
  [[-5, -5, 0], [5, -5, 0], [5, -5, 1]],
22
22
  [[-5, -5, 0], [5, -5, 1], [-5, -5, 1]],
23
- [[5, 5, 1], [-5, 5, 1], [-5, -5, 1], [5, -5, 1]],
24
- [[5, -5, 0], [-5, -5, 0], [-5, 5, 0], [5, 5, 0]]
23
+ [[-5, -5, 1], [5, -5, 1], [5, 5, 1]],
24
+ [[5, 5, 1], [-5, 5, 1], [-5, -5, 1]],
25
+ [[5, 5, 0], [5, -5, 0], [-5, -5, 0]],
26
+ [[-5, -5, 0], [-5, 5, 0], [5, 5, 0]]
25
27
  ]
26
- t.is(pts.length, 10)
28
+ t.notThrows(() => geom3.validate(geometry3))
29
+ t.is(pts.length, 12)
27
30
  t.true(comparePolygonsAsPoints(pts, exp))
28
31
  })
29
32
 
@@ -41,10 +44,13 @@ test('extrudeLinear (no twist)', (t) => {
41
44
  [[-5, 5, 0], [-5, -5, 15], [-5, 5, 15]],
42
45
  [[-5, -5, 0], [5, -5, 0], [5, -5, 15]],
43
46
  [[-5, -5, 0], [5, -5, 15], [-5, -5, 15]],
44
- [[5, 5, 15], [-5, 5, 15], [-5, -5, 15], [5, -5, 15]],
45
- [[5, -5, 0], [-5, -5, 0], [-5, 5, 0], [5, 5, 0]]
47
+ [[-5, -5, 15], [5, -5, 15], [5, 5, 15]],
48
+ [[5, 5, 15], [-5, 5, 15], [-5, -5, 15]],
49
+ [[5, 5, 0], [5, -5, 0], [-5, -5, 0]],
50
+ [[-5, -5, 0], [-5, 5, 0], [5, 5, 0]]
46
51
  ]
47
- t.is(pts.length, 10)
52
+ t.notThrows(() => geom3.validate(geometry3))
53
+ t.is(pts.length, 12)
48
54
  t.true(comparePolygonsAsPoints(pts, exp))
49
55
 
50
56
  geometry3 = extrudeLinear({ height: -15 }, geometry2)
@@ -58,10 +64,13 @@ test('extrudeLinear (no twist)', (t) => {
58
64
  [[-5, -5, 0], [-5, 5, -15], [-5, -5, -15]],
59
65
  [[5, -5, 0], [-5, -5, 0], [-5, -5, -15]],
60
66
  [[5, -5, 0], [-5, -5, -15], [5, -5, -15]],
61
- [[5, -5, -15], [-5, -5, -15], [-5, 5, -15], [5, 5, -15]],
62
- [[5, 5, 0], [-5, 5, 0], [-5, -5, 0], [5, -5, 0]]
67
+ [[-5, 5, -15], [5, 5, -15], [5, -5, -15]],
68
+ [[5, -5, -15], [-5, -5, -15], [-5, 5, -15]],
69
+ [[5, -5, 0], [5, 5, 0], [-5, 5, 0]],
70
+ [[-5, 5, 0], [-5, -5, 0], [5, -5, 0]]
63
71
  ]
64
- t.is(pts.length, 10)
72
+ t.notThrows(() => geom3.validate(geometry3))
73
+ t.is(pts.length, 12)
65
74
  t.true(comparePolygonsAsPoints(pts, exp))
66
75
  })
67
76
 
@@ -79,11 +88,21 @@ test('extrudeLinear (twist)', (t) => {
79
88
  [[-5, 5, 0], [-7.0710678118654755, -4.440892098500626e-16, 15], [-4.440892098500626e-16, 7.0710678118654755, 15]],
80
89
  [[-5, -5, 0], [5, -5, 0], [4.440892098500626e-16, -7.0710678118654755, 15]],
81
90
  [[-5, -5, 0], [4.440892098500626e-16, -7.0710678118654755, 15], [-7.0710678118654755, -4.440892098500626e-16, 15]],
82
- [[7.071067811865477, 8.881784197001252e-16, 15], [1.7763568394002505e-15, 7.071067811865477, 15],
83
- [-7.071067811865475, 0, 15], [0, -7.0710678118654755, 15]],
84
- [[5, -5, 0], [-5, -5, 0], [-5, 5, 0], [5, 5, 0]]
91
+ [
92
+ [-7.0710678118654755, -4.440892098500626e-16, 15],
93
+ [4.440892098500626e-16, -7.0710678118654755, 15],
94
+ [7.0710678118654755, 4.440892098500626e-16, 15]
95
+ ],
96
+ [
97
+ [7.0710678118654755, 4.440892098500626e-16, 15],
98
+ [-4.440892098500626e-16, 7.0710678118654755, 15],
99
+ [-7.0710678118654755, -4.440892098500626e-16, 15]
100
+ ],
101
+ [[5, 5, 0], [5, -5, 0], [-5, -5, 0]],
102
+ [[-5, -5, 0], [-5, 5, 0], [5, 5, 0]]
85
103
  ]
86
- t.is(pts.length, 10)
104
+ t.notThrows(() => geom3.validate(geometry3))
105
+ t.is(pts.length, 12)
87
106
  t.true(comparePolygonsAsPoints(pts, exp))
88
107
 
89
108
  geometry3 = extrudeLinear({ height: 15, twistAngle: Math.PI / 2, twistSteps: 3 }, geometry2)
@@ -113,27 +132,30 @@ test('extrudeLinear (twist)', (t) => {
113
132
  [[-6.830127018922193, -1.8301270189221923, 10], [5, -5, 15], [-5, -5, 15]],
114
133
  [[1.8301270189221923, -6.830127018922193, 10], [6.830127018922193, 1.8301270189221923, 10], [5, 5, 15]],
115
134
  [[1.8301270189221923, -6.830127018922193, 10], [5, 5, 15], [5, -5, 15]],
116
- [[-5, 5, 15], [-5, -5, 15], [5, -5, 15], [5, 5, 15]],
117
- [[5, -5, 0], [-5, -5, 0], [-5, 5, 0], [5, 5, 0]]
135
+ [[5, -5, 15], [5, 5, 15], [-5, 5, 15]],
136
+ [[-5, 5, 15], [-5, -5, 15], [5, -5, 15]],
137
+ [[5, 5, 0], [5, -5, 0], [-5, -5, 0]],
138
+ [[-5, -5, 0], [-5, 5, 0], [5, 5, 0]]
118
139
  ]
119
- t.is(pts.length, 26)
140
+ t.is(pts.length, 28)
120
141
  t.true(comparePolygonsAsPoints(pts, exp))
121
142
 
122
143
  geometry3 = extrudeLinear({ height: 15, twistAngle: Math.PI / 2, twistSteps: 30 }, geometry2)
123
144
  pts = geom3.toPoints(geometry3)
124
- t.is(pts.length, 242)
145
+ t.notThrows(() => geom3.validate(geometry3))
146
+ t.is(pts.length, 244)
125
147
  })
126
148
 
127
149
  test('extrudeLinear (holes)', (t) => {
128
150
  const geometry2 = geom2.create([
129
- [[-5.00000, 5.00000], [-5.00000, -5.00000]],
130
- [[-5.00000, -5.00000], [5.00000, -5.00000]],
131
- [[5.00000, -5.00000], [5.00000, 5.00000]],
132
- [[5.00000, 5.00000], [-5.00000, 5.00000]],
133
- [[-2.00000, -2.00000], [-2.00000, 2.00000]],
134
- [[2.00000, -2.00000], [-2.00000, -2.00000]],
135
- [[2.00000, 2.00000], [2.00000, -2.00000]],
136
- [[-2.00000, 2.00000], [2.00000, 2.00000]]
151
+ [[-5, 5], [-5, -5]],
152
+ [[-5, -5], [5, -5]],
153
+ [[5, -5], [5, 5]],
154
+ [[5, 5], [-5, 5]],
155
+ [[-2, -2], [-2, 2]],
156
+ [[2, -2], [-2, -2]],
157
+ [[2, 2], [2, -2]],
158
+ [[-2, 2], [2, 2]]
137
159
  ])
138
160
  const geometry3 = extrudeLinear({ height: 15 }, geometry2)
139
161
  const pts = geom3.toPoints(geometry3)
@@ -154,23 +176,32 @@ test('extrudeLinear (holes)', (t) => {
154
176
  [[2, 2, 0], [2, -2, 15], [2, 2, 15]],
155
177
  [[-2, 2, 0], [2, 2, 0], [2, 2, 15]],
156
178
  [[-2, 2, 0], [2, 2, 15], [-2, 2, 15]],
157
- [[-5, -5, 15], [-2, -5, 15], [-2, 5, 15], [-5, 5, 15]],
158
- [[5, -5, 15], [5, -2, 15], [-2, -2, 15], [-2, -5, 15]],
159
- [[5, 5, 15], [2, 5, 15], [2, -2, 15], [5, -2, 15]],
160
- [[2, 2, 15], [2, 5, 15], [-2, 5, 15], [-2, 2, 15]],
161
- [[-5, 5, 0], [-2, 5, 0], [-2, -5, 0], [-5, -5, 0]],
162
- [[5, -5, 0], [-2, -5, 0], [-2, -2, 0], [5, -2, 0]],
163
- [[5, -2, 0], [2, -2, 0], [2, 5, 0], [5, 5, 0]],
164
- [[2, 5, 0], [2, 2, 0], [-2, 2, 0], [-2, 5, 0]]
179
+ [[5, -5, 15], [5, 5, 15], [2, 2, 15]],
180
+ [[-2, 2, 15], [2, 2, 15], [5, 5, 15]],
181
+ [[5, -5, 15], [2, 2, 15], [2, -2, 15]],
182
+ [[-2, 2, 15], [5, 5, 15], [-5, 5, 15]],
183
+ [[-5, -5, 15], [5, -5, 15], [2, -2, 15]],
184
+ [[-2, -2, 15], [-2, 2, 15], [-5, 5, 15]],
185
+ [[-5, -5, 15], [2, -2, 15], [-2, -2, 15]],
186
+ [[-2, -2, 15], [-5, 5, 15], [-5, -5, 15]],
187
+ [[2, 2, 0], [5, 5, 0], [5, -5, 0]],
188
+ [[5, 5, 0], [2, 2, 0], [-2, 2, 0]],
189
+ [[2, -2, 0], [2, 2, 0], [5, -5, 0]],
190
+ [[-5, 5, 0], [5, 5, 0], [-2, 2, 0]],
191
+ [[2, -2, 0], [5, -5, 0], [-5, -5, 0]],
192
+ [[-5, 5, 0], [-2, 2, 0], [-2, -2, 0]],
193
+ [[-2, -2, 0], [2, -2, 0], [-5, -5, 0]],
194
+ [[-5, -5, 0], [-5, 5, 0], [-2, -2, 0]]
165
195
  ]
166
- t.is(pts.length, 24)
196
+ t.notThrows(() => geom3.validate(geometry3))
197
+ t.is(pts.length, 32)
167
198
  t.true(comparePolygonsAsPoints(pts, exp))
168
199
  })
169
200
 
170
201
  test('extrudeLinear (path2)', (t) => {
171
202
  const geometry2 = path2.fromPoints({ closed: true }, [[0, 0], [12, 0], [6, 10]])
172
203
  const geometry3 = extrudeLinear({ height: 15 }, geometry2)
173
- t.true(geom3.isA(geometry3))
204
+ t.notThrows(() => geom3.validate(geometry3))
174
205
  const pts = geom3.toPoints(geometry3)
175
206
  const exp = [
176
207
  [[6, 10, 0], [0, 0, 0], [0, 0, 15]],
@@ -179,9 +210,10 @@ test('extrudeLinear (path2)', (t) => {
179
210
  [[0, 0, 0], [12, 0, 15], [0, 0, 15]],
180
211
  [[12, 0, 0], [6, 10, 0], [6, 10, 15]],
181
212
  [[12, 0, 0], [6, 10, 15], [12, 0, 15]],
182
- [[0, 0, 15], [11.999999999999998, 0, 15], [6, 10, 15]],
183
- [[6.000000000000001, 10, 0], [12, 0, 0], [8.881784197001252e-16, 0, 0]]
213
+ [[12, 0, 15], [6, 10, 15], [0, 0, 15]],
214
+ [[0, 0, 0], [6, 10, 0], [12, 0, 0]]
184
215
  ]
216
+
185
217
  t.is(pts.length, 8)
186
218
  t.true(comparePolygonsAsPoints(pts, exp))
187
219
  })
@@ -14,6 +14,7 @@ const extrudeFromSlices = require('./extrudeFromSlices')
14
14
  * @param {Array} [options.offset] - the direction of the extrusion as a 3D vector
15
15
  * @param {Number} [options.twistAngle] - the final rotation (RADIANS) about the origin
16
16
  * @param {Integer} [options.twistSteps] - the number of steps created to produce the twist (if any)
17
+ * @param {Boolean} [options.repair] - repair gaps in the geometry
17
18
  * @param {geom2} geometry - the geometry to extrude
18
19
  * @returns {geom3} the extruded 3D geometry
19
20
  */
@@ -21,9 +22,10 @@ const extrudeGeom2 = (options, geometry) => {
21
22
  const defaults = {
22
23
  offset: [0, 0, 1],
23
24
  twistAngle: 0,
24
- twistSteps: 12
25
+ twistSteps: 12,
26
+ repair: true
25
27
  }
26
- let { offset, twistAngle, twistSteps } = Object.assign({ }, defaults, options)
28
+ let { offset, twistAngle, twistSteps, repair } = Object.assign({ }, defaults, options)
27
29
 
28
30
  if (twistSteps < 1) throw new Error('twistSteps must be 1 or more')
29
31
 
@@ -53,6 +55,7 @@ const extrudeGeom2 = (options, geometry) => {
53
55
  numberOfSlices: twistSteps + 1,
54
56
  capStart: true,
55
57
  capEnd: true,
58
+ repair,
56
59
  callback: createTwist
57
60
  }
58
61
  return extrudeFromSlices(options, baseSlice)