UNPKG

23 kBJavaScriptView Raw
1const CSG = require('./CSG')
2const {parseOption, parseOptionAs3DVector, parseOptionAs2DVector, parseOptionAs3DVectorList, parseOptionAsFloat, parseOptionAsInt} = require('./optionParsers')
3const {defaultResolution3D, defaultResolution2D, EPS} = require('./constants')
4const Vector3D = require('./math/Vector3')
5const Vertex = require('./math/Vertex3')
6const Polygon = require('./math/Polygon3')
7const {Connector} = require('./connectors')
8const Properties = require('./Properties')
9
10/** Construct an axis-aligned solid cuboid.
11 * @param {Object} [options] - options for construction
12 * @param {Vector3D} [options.center=[0,0,0]] - center of cube
13 * @param {Vector3D} [options.radius=[1,1,1]] - radius of cube, single scalar also possible
14 * @returns {CSG} new 3D solid
15 *
16 * @example
17 * let cube = CSG.cube({
18 * center: [5, 5, 5],
19 * radius: 5, // scalar radius
20 * });
21 */
22const cube = function (options) {
23 let c
24 let r
25 let corner1
26 let corner2
27 options = options || {}
28 if (('corner1' in options) || ('corner2' in options)) {
29 if (('center' in options) || ('radius' in options)) {
30 throw new Error('cube: should either give a radius and center parameter, or a corner1 and corner2 parameter')
31 }
32 corner1 = parseOptionAs3DVector(options, 'corner1', [0, 0, 0])
33 corner2 = parseOptionAs3DVector(options, 'corner2', [1, 1, 1])
34 c = corner1.plus(corner2).times(0.5)
35 r = corner2.minus(corner1).times(0.5)
36 } else {
37 c = parseOptionAs3DVector(options, 'center', [0, 0, 0])
38 r = parseOptionAs3DVector(options, 'radius', [1, 1, 1])
39 }
40 r = r.abs() // negative radii make no sense
41 let result = CSG.fromPolygons([
42 [
43 [0, 4, 6, 2],
44 [-1, 0, 0]
45 ],
46 [
47 [1, 3, 7, 5],
48 [+1, 0, 0]
49 ],
50 [
51 [0, 1, 5, 4],
52 [0, -1, 0]
53 ],
54 [
55 [2, 6, 7, 3],
56 [0, +1, 0]
57 ],
58 [
59 [0, 2, 3, 1],
60 [0, 0, -1]
61 ],
62 [
63 [4, 5, 7, 6],
64 [0, 0, +1]
65 ]
66 ].map(function (info) {
67 let vertices = info[0].map(function (i) {
68 let pos = new Vector3D(
69 c.x + r.x * (2 * !!(i & 1) - 1), c.y + r.y * (2 * !!(i & 2) - 1), c.z + r.z * (2 * !!(i & 4) - 1))
70 return new Vertex(pos)
71 })
72 return new Polygon(vertices, null /* , plane */)
73 }))
74 result.properties.cube = new Properties()
75 result.properties.cube.center = new Vector3D(c)
76 // add 6 connectors, at the centers of each face:
77 result.properties.cube.facecenters = [
78 new Connector(new Vector3D([r.x, 0, 0]).plus(c), [1, 0, 0], [0, 0, 1]),
79 new Connector(new Vector3D([-r.x, 0, 0]).plus(c), [-1, 0, 0], [0, 0, 1]),
80 new Connector(new Vector3D([0, r.y, 0]).plus(c), [0, 1, 0], [0, 0, 1]),
81 new Connector(new Vector3D([0, -r.y, 0]).plus(c), [0, -1, 0], [0, 0, 1]),
82 new Connector(new Vector3D([0, 0, r.z]).plus(c), [0, 0, 1], [1, 0, 0]),
83 new Connector(new Vector3D([0, 0, -r.z]).plus(c), [0, 0, -1], [1, 0, 0])
84 ]
85 return result
86}
87
88/** Construct a solid sphere
89 * @param {Object} [options] - options for construction
90 * @param {Vector3D} [options.center=[0,0,0]] - center of sphere
91 * @param {Number} [options.radius=1] - radius of sphere
92 * @param {Number} [options.resolution=defaultResolution3D] - number of polygons per 360 degree revolution
93 * @param {Array} [options.axes] - an array with 3 vectors for the x, y and z base vectors
94 * @returns {CSG} new 3D solid
95 *
96 *
97 * @example
98 * let sphere = CSG.sphere({
99 * center: [0, 0, 0],
100 * radius: 2,
101 * resolution: 32,
102 * });
103*/
104const sphere = function (options) {
105 options = options || {}
106 let center = parseOptionAs3DVector(options, 'center', [0, 0, 0])
107 let radius = parseOptionAsFloat(options, 'radius', 1)
108 let resolution = parseOptionAsInt(options, 'resolution', defaultResolution3D)
109 let xvector, yvector, zvector
110 if ('axes' in options) {
111 xvector = options.axes[0].unit().times(radius)
112 yvector = options.axes[1].unit().times(radius)
113 zvector = options.axes[2].unit().times(radius)
114 } else {
115 xvector = new Vector3D([1, 0, 0]).times(radius)
116 yvector = new Vector3D([0, -1, 0]).times(radius)
117 zvector = new Vector3D([0, 0, 1]).times(radius)
118 }
119 if (resolution < 4) resolution = 4
120 let qresolution = Math.round(resolution / 4)
121 let prevcylinderpoint
122 let polygons = []
123 for (let slice1 = 0; slice1 <= resolution; slice1++) {
124 let angle = Math.PI * 2.0 * slice1 / resolution
125 let cylinderpoint = xvector.times(Math.cos(angle)).plus(yvector.times(Math.sin(angle)))
126 if (slice1 > 0) {
127 // cylinder vertices:
128 let vertices = []
129 let prevcospitch, prevsinpitch
130 for (let slice2 = 0; slice2 <= qresolution; slice2++) {
131 let pitch = 0.5 * Math.PI * slice2 / qresolution
132 let cospitch = Math.cos(pitch)
133 let sinpitch = Math.sin(pitch)
134 if (slice2 > 0) {
135 vertices = []
136 vertices.push(new Vertex(center.plus(prevcylinderpoint.times(prevcospitch).minus(zvector.times(prevsinpitch)))))
137 vertices.push(new Vertex(center.plus(cylinderpoint.times(prevcospitch).minus(zvector.times(prevsinpitch)))))
138 if (slice2 < qresolution) {
139 vertices.push(new Vertex(center.plus(cylinderpoint.times(cospitch).minus(zvector.times(sinpitch)))))
140 }
141 vertices.push(new Vertex(center.plus(prevcylinderpoint.times(cospitch).minus(zvector.times(sinpitch)))))
142 polygons.push(new Polygon(vertices))
143 vertices = []
144 vertices.push(new Vertex(center.plus(prevcylinderpoint.times(prevcospitch).plus(zvector.times(prevsinpitch)))))
145 vertices.push(new Vertex(center.plus(cylinderpoint.times(prevcospitch).plus(zvector.times(prevsinpitch)))))
146 if (slice2 < qresolution) {
147 vertices.push(new Vertex(center.plus(cylinderpoint.times(cospitch).plus(zvector.times(sinpitch)))))
148 }
149 vertices.push(new Vertex(center.plus(prevcylinderpoint.times(cospitch).plus(zvector.times(sinpitch)))))
150 vertices.reverse()
151 polygons.push(new Polygon(vertices))
152 }
153 prevcospitch = cospitch
154 prevsinpitch = sinpitch
155 }
156 }
157 prevcylinderpoint = cylinderpoint
158 }
159 let result = CSG.fromPolygons(polygons)
160 result.properties.sphere = new Properties()
161 result.properties.sphere.center = new Vector3D(center)
162 result.properties.sphere.facepoint = center.plus(xvector)
163 return result
164}
165
166/** Construct a solid cylinder.
167 * @param {Object} [options] - options for construction
168 * @param {Vector} [options.start=[0,-1,0]] - start point of cylinder
169 * @param {Vector} [options.end=[0,1,0]] - end point of cylinder
170 * @param {Number} [options.radius=1] - radius of cylinder, must be scalar
171 * @param {Number} [options.resolution=defaultResolution3D] - number of polygons per 360 degree revolution
172 * @returns {CSG} new 3D solid
173 *
174 * @example
175 * let cylinder = CSG.cylinder({
176 * start: [0, -10, 0],
177 * end: [0, 10, 0],
178 * radius: 10,
179 * resolution: 16
180 * });
181 */
182const cylinder = function (options) {
183 let s = parseOptionAs3DVector(options, 'start', [0, -1, 0])
184 let e = parseOptionAs3DVector(options, 'end', [0, 1, 0])
185 let r = parseOptionAsFloat(options, 'radius', 1)
186 let rEnd = parseOptionAsFloat(options, 'radiusEnd', r)
187 let rStart = parseOptionAsFloat(options, 'radiusStart', r)
188 let alpha = parseOptionAsFloat(options, 'sectorAngle', 360)
189 alpha = alpha > 360 ? alpha % 360 : alpha
190
191 if ((rEnd < 0) || (rStart < 0)) {
192 throw new Error('Radius should be non-negative')
193 }
194 if ((rEnd === 0) && (rStart === 0)) {
195 throw new Error('Either radiusStart or radiusEnd should be positive')
196 }
197
198 let slices = parseOptionAsInt(options, 'resolution', defaultResolution2D) // FIXME is this 3D?
199 let ray = e.minus(s)
200 let axisZ = ray.unit() //, isY = (Math.abs(axisZ.y) > 0.5);
201 let axisX = axisZ.randomNonParallelVector().unit()
202
203 // let axisX = new Vector3D(isY, !isY, 0).cross(axisZ).unit();
204 let axisY = axisX.cross(axisZ).unit()
205 let start = new Vertex(s)
206 let end = new Vertex(e)
207 let polygons = []
208
209 function point (stack, slice, radius) {
210 let angle = slice * Math.PI * alpha / 180
211 let out = axisX.times(Math.cos(angle)).plus(axisY.times(Math.sin(angle)))
212 let pos = s.plus(ray.times(stack)).plus(out.times(radius))
213 return new Vertex(pos)
214 }
215 if (alpha > 0) {
216 for (let i = 0; i < slices; i++) {
217 let t0 = i / slices
218 let t1 = (i + 1) / slices
219 if (rEnd === rStart) {
220 polygons.push(new Polygon([start, point(0, t0, rEnd), point(0, t1, rEnd)]))
221 polygons.push(new Polygon([point(0, t1, rEnd), point(0, t0, rEnd), point(1, t0, rEnd), point(1, t1, rEnd)]))
222 polygons.push(new Polygon([end, point(1, t1, rEnd), point(1, t0, rEnd)]))
223 } else {
224 if (rStart > 0) {
225 polygons.push(new Polygon([start, point(0, t0, rStart), point(0, t1, rStart)]))
226 polygons.push(new Polygon([point(0, t0, rStart), point(1, t0, rEnd), point(0, t1, rStart)]))
227 }
228 if (rEnd > 0) {
229 polygons.push(new Polygon([end, point(1, t1, rEnd), point(1, t0, rEnd)]))
230 polygons.push(new Polygon([point(1, t0, rEnd), point(1, t1, rEnd), point(0, t1, rStart)]))
231 }
232 }
233 }
234 if (alpha < 360) {
235 polygons.push(new Polygon([start, end, point(0, 0, rStart)]))
236 polygons.push(new Polygon([point(0, 0, rStart), end, point(1, 0, rEnd)]))
237 polygons.push(new Polygon([start, point(0, 1, rStart), end]))
238 polygons.push(new Polygon([point(0, 1, rStart), point(1, 1, rEnd), end]))
239 }
240 }
241 let result = CSG.fromPolygons(polygons)
242 result.properties.cylinder = new Properties()
243 result.properties.cylinder.start = new Connector(s, axisZ.negated(), axisX)
244 result.properties.cylinder.end = new Connector(e, axisZ, axisX)
245 let cylCenter = s.plus(ray.times(0.5))
246 let fptVec = axisX.rotate(s, axisZ, -alpha / 2).times((rStart + rEnd) / 2)
247 let fptVec90 = fptVec.cross(axisZ)
248 // note this one is NOT a face normal for a cone. - It's horizontal from cyl perspective
249 result.properties.cylinder.facepointH = new Connector(cylCenter.plus(fptVec), fptVec, axisZ)
250 result.properties.cylinder.facepointH90 = new Connector(cylCenter.plus(fptVec90), fptVec90, axisZ)
251 return result
252}
253
254/** Construct a cylinder with rounded ends.
255 * @param {Object} [options] - options for construction
256 * @param {Vector3D} [options.start=[0,-1,0]] - start point of cylinder
257 * @param {Vector3D} [options.end=[0,1,0]] - end point of cylinder
258 * @param {Number} [options.radius=1] - radius of rounded ends, must be scalar
259 * @param {Vector3D} [options.normal] - vector determining the starting angle for tesselation. Should be non-parallel to start.minus(end)
260 * @param {Number} [options.resolution=defaultResolution3D] - number of polygons per 360 degree revolution
261 * @returns {CSG} new 3D solid
262 *
263 * @example
264 * let cylinder = CSG.roundedCylinder({
265 * start: [0, -10, 0],
266 * end: [0, 10, 0],
267 * radius: 2,
268 * resolution: 16
269 * });
270 */
271const roundedCylinder = function (options) {
272 let p1 = parseOptionAs3DVector(options, 'start', [0, -1, 0])
273 let p2 = parseOptionAs3DVector(options, 'end', [0, 1, 0])
274 let radius = parseOptionAsFloat(options, 'radius', 1)
275 let direction = p2.minus(p1)
276 let defaultnormal
277 if (Math.abs(direction.x) > Math.abs(direction.y)) {
278 defaultnormal = new Vector3D(0, 1, 0)
279 } else {
280 defaultnormal = new Vector3D(1, 0, 0)
281 }
282 let normal = parseOptionAs3DVector(options, 'normal', defaultnormal)
283 let resolution = parseOptionAsInt(options, 'resolution', defaultResolution3D)
284 if (resolution < 4) resolution = 4
285 let polygons = []
286 let qresolution = Math.floor(0.25 * resolution)
287 let length = direction.length()
288 if (length < EPS) {
289 return sphere({
290 center: p1,
291 radius: radius,
292 resolution: resolution
293 })
294 }
295 let zvector = direction.unit().times(radius)
296 let xvector = zvector.cross(normal).unit().times(radius)
297 let yvector = xvector.cross(zvector).unit().times(radius)
298 let prevcylinderpoint
299 for (let slice1 = 0; slice1 <= resolution; slice1++) {
300 let angle = Math.PI * 2.0 * slice1 / resolution
301 let cylinderpoint = xvector.times(Math.cos(angle)).plus(yvector.times(Math.sin(angle)))
302 if (slice1 > 0) {
303 // cylinder vertices:
304 let vertices = []
305 vertices.push(new Vertex(p1.plus(cylinderpoint)))
306 vertices.push(new Vertex(p1.plus(prevcylinderpoint)))
307 vertices.push(new Vertex(p2.plus(prevcylinderpoint)))
308 vertices.push(new Vertex(p2.plus(cylinderpoint)))
309 polygons.push(new Polygon(vertices))
310 let prevcospitch, prevsinpitch
311 for (let slice2 = 0; slice2 <= qresolution; slice2++) {
312 let pitch = 0.5 * Math.PI * slice2 / qresolution
313 // let pitch = Math.asin(slice2/qresolution);
314 let cospitch = Math.cos(pitch)
315 let sinpitch = Math.sin(pitch)
316 if (slice2 > 0) {
317 vertices = []
318 vertices.push(new Vertex(p1.plus(prevcylinderpoint.times(prevcospitch).minus(zvector.times(prevsinpitch)))))
319 vertices.push(new Vertex(p1.plus(cylinderpoint.times(prevcospitch).minus(zvector.times(prevsinpitch)))))
320 if (slice2 < qresolution) {
321 vertices.push(new Vertex(p1.plus(cylinderpoint.times(cospitch).minus(zvector.times(sinpitch)))))
322 }
323 vertices.push(new Vertex(p1.plus(prevcylinderpoint.times(cospitch).minus(zvector.times(sinpitch)))))
324 polygons.push(new Polygon(vertices))
325 vertices = []
326 vertices.push(new Vertex(p2.plus(prevcylinderpoint.times(prevcospitch).plus(zvector.times(prevsinpitch)))))
327 vertices.push(new Vertex(p2.plus(cylinderpoint.times(prevcospitch).plus(zvector.times(prevsinpitch)))))
328 if (slice2 < qresolution) {
329 vertices.push(new Vertex(p2.plus(cylinderpoint.times(cospitch).plus(zvector.times(sinpitch)))))
330 }
331 vertices.push(new Vertex(p2.plus(prevcylinderpoint.times(cospitch).plus(zvector.times(sinpitch)))))
332 vertices.reverse()
333 polygons.push(new Polygon(vertices))
334 }
335 prevcospitch = cospitch
336 prevsinpitch = sinpitch
337 }
338 }
339 prevcylinderpoint = cylinderpoint
340 }
341 let result = CSG.fromPolygons(polygons)
342 let ray = zvector.unit()
343 let axisX = xvector.unit()
344 result.properties.roundedCylinder = new Properties()
345 result.properties.roundedCylinder.start = new Connector(p1, ray.negated(), axisX)
346 result.properties.roundedCylinder.end = new Connector(p2, ray, axisX)
347 result.properties.roundedCylinder.facepoint = p1.plus(xvector)
348 return result
349}
350
351/** Construct an elliptic cylinder.
352 * @param {Object} [options] - options for construction
353 * @param {Vector3D} [options.start=[0,-1,0]] - start point of cylinder
354 * @param {Vector3D} [options.end=[0,1,0]] - end point of cylinder
355 * @param {Vector2D} [options.radius=[1,1]] - radius of rounded ends, must be two dimensional array
356 * @param {Vector2D} [options.radiusStart=[1,1]] - OPTIONAL radius of rounded start, must be two dimensional array
357 * @param {Vector2D} [options.radiusEnd=[1,1]] - OPTIONAL radius of rounded end, must be two dimensional array
358 * @param {Number} [options.resolution=defaultResolution2D] - number of polygons per 360 degree revolution
359 * @returns {CSG} new 3D solid
360 *
361 * @example
362 * let cylinder = CSG.cylinderElliptic({
363 * start: [0, -10, 0],
364 * end: [0, 10, 0],
365 * radiusStart: [10,5],
366 * radiusEnd: [8,3],
367 * resolution: 16
368 * });
369 */
370
371const cylinderElliptic = function (options) {
372 let s = parseOptionAs3DVector(options, 'start', [0, -1, 0])
373 let e = parseOptionAs3DVector(options, 'end', [0, 1, 0])
374 let r = parseOptionAs2DVector(options, 'radius', [1, 1])
375 let rEnd = parseOptionAs2DVector(options, 'radiusEnd', r)
376 let rStart = parseOptionAs2DVector(options, 'radiusStart', r)
377
378 if ((rEnd._x < 0) || (rStart._x < 0) || (rEnd._y < 0) || (rStart._y < 0)) {
379 throw new Error('Radius should be non-negative')
380 }
381 if ((rEnd._x === 0 || rEnd._y === 0) && (rStart._x === 0 || rStart._y === 0)) {
382 throw new Error('Either radiusStart or radiusEnd should be positive')
383 }
384
385 let slices = parseOptionAsInt(options, 'resolution', defaultResolution2D) // FIXME is this correct?
386 let ray = e.minus(s)
387 let axisZ = ray.unit() //, isY = (Math.abs(axisZ.y) > 0.5);
388 let axisX = axisZ.randomNonParallelVector().unit()
389
390 // let axisX = new Vector3D(isY, !isY, 0).cross(axisZ).unit();
391 let axisY = axisX.cross(axisZ).unit()
392 let start = new Vertex(s)
393 let end = new Vertex(e)
394 let polygons = []
395
396 function point (stack, slice, radius) {
397 let angle = slice * Math.PI * 2
398 let out = axisX.times(radius._x * Math.cos(angle)).plus(axisY.times(radius._y * Math.sin(angle)))
399 let pos = s.plus(ray.times(stack)).plus(out)
400 return new Vertex(pos)
401 }
402 for (let i = 0; i < slices; i++) {
403 let t0 = i / slices
404 let t1 = (i + 1) / slices
405
406 if (rEnd._x === rStart._x && rEnd._y === rStart._y) {
407 polygons.push(new Polygon([start, point(0, t0, rEnd), point(0, t1, rEnd)]))
408 polygons.push(new Polygon([point(0, t1, rEnd), point(0, t0, rEnd), point(1, t0, rEnd), point(1, t1, rEnd)]))
409 polygons.push(new Polygon([end, point(1, t1, rEnd), point(1, t0, rEnd)]))
410 } else {
411 if (rStart._x > 0) {
412 polygons.push(new Polygon([start, point(0, t0, rStart), point(0, t1, rStart)]))
413 polygons.push(new Polygon([point(0, t0, rStart), point(1, t0, rEnd), point(0, t1, rStart)]))
414 }
415 if (rEnd._x > 0) {
416 polygons.push(new Polygon([end, point(1, t1, rEnd), point(1, t0, rEnd)]))
417 polygons.push(new Polygon([point(1, t0, rEnd), point(1, t1, rEnd), point(0, t1, rStart)]))
418 }
419 }
420 }
421 let result = CSG.fromPolygons(polygons)
422 result.properties.cylinder = new Properties()
423 result.properties.cylinder.start = new Connector(s, axisZ.negated(), axisX)
424 result.properties.cylinder.end = new Connector(e, axisZ, axisX)
425 result.properties.cylinder.facepoint = s.plus(axisX.times(rStart))
426 return result
427}
428
429/** Construct an axis-aligned solid rounded cuboid.
430 * @param {Object} [options] - options for construction
431 * @param {Vector3D} [options.center=[0,0,0]] - center of rounded cube
432 * @param {Vector3D} [options.radius=[1,1,1]] - radius of rounded cube, single scalar is possible
433 * @param {Number} [options.roundradius=0.2] - radius of rounded edges
434 * @param {Number} [options.resolution=defaultResolution3D] - number of polygons per 360 degree revolution
435 * @returns {CSG} new 3D solid
436 *
437 * @example
438 * let cube = CSG.roundedCube({
439 * center: [2, 0, 2],
440 * radius: 15,
441 * roundradius: 2,
442 * resolution: 36,
443 * });
444 */
445const roundedCube = function (options) {
446 let minRR = 1e-2 // minroundradius 1e-3 gives rounding errors already
447 let center
448 let cuberadius
449 let corner1
450 let corner2
451 options = options || {}
452 if (('corner1' in options) || ('corner2' in options)) {
453 if (('center' in options) || ('radius' in options)) {
454 throw new Error('roundedCube: should either give a radius and center parameter, or a corner1 and corner2 parameter')
455 }
456 corner1 = parseOptionAs3DVector(options, 'corner1', [0, 0, 0])
457 corner2 = parseOptionAs3DVector(options, 'corner2', [1, 1, 1])
458 center = corner1.plus(corner2).times(0.5)
459 cuberadius = corner2.minus(corner1).times(0.5)
460 } else {
461 center = parseOptionAs3DVector(options, 'center', [0, 0, 0])
462 cuberadius = parseOptionAs3DVector(options, 'radius', [1, 1, 1])
463 }
464 cuberadius = cuberadius.abs() // negative radii make no sense
465 let resolution = parseOptionAsInt(options, 'resolution', defaultResolution3D)
466 if (resolution < 4) resolution = 4
467 if (resolution % 2 === 1 && resolution < 8) resolution = 8 // avoid ugly
468 let roundradius = parseOptionAs3DVector(options, 'roundradius', [0.2, 0.2, 0.2])
469 // slight hack for now - total radius stays ok
470 roundradius = Vector3D.Create(Math.max(roundradius.x, minRR), Math.max(roundradius.y, minRR), Math.max(roundradius.z, minRR))
471 let innerradius = cuberadius.minus(roundradius)
472 if (innerradius.x < 0 || innerradius.y < 0 || innerradius.z < 0) {
473 throw new Error('roundradius <= radius!')
474 }
475 let res = sphere({radius: 1, resolution: resolution})
476 res = res.scale(roundradius)
477 innerradius.x > EPS && (res = res.stretchAtPlane([1, 0, 0], [0, 0, 0], 2 * innerradius.x))
478 innerradius.y > EPS && (res = res.stretchAtPlane([0, 1, 0], [0, 0, 0], 2 * innerradius.y))
479 innerradius.z > EPS && (res = res.stretchAtPlane([0, 0, 1], [0, 0, 0], 2 * innerradius.z))
480 res = res.translate([-innerradius.x + center.x, -innerradius.y + center.y, -innerradius.z + center.z])
481 res = res.reTesselated()
482 res.properties.roundedCube = new Properties()
483 res.properties.roundedCube.center = new Vertex(center)
484 res.properties.roundedCube.facecenters = [
485 new Connector(new Vector3D([cuberadius.x, 0, 0]).plus(center), [1, 0, 0], [0, 0, 1]),
486 new Connector(new Vector3D([-cuberadius.x, 0, 0]).plus(center), [-1, 0, 0], [0, 0, 1]),
487 new Connector(new Vector3D([0, cuberadius.y, 0]).plus(center), [0, 1, 0], [0, 0, 1]),
488 new Connector(new Vector3D([0, -cuberadius.y, 0]).plus(center), [0, -1, 0], [0, 0, 1]),
489 new Connector(new Vector3D([0, 0, cuberadius.z]).plus(center), [0, 0, 1], [1, 0, 0]),
490 new Connector(new Vector3D([0, 0, -cuberadius.z]).plus(center), [0, 0, -1], [1, 0, 0])
491 ]
492 return res
493}
494
495/** Create a polyhedron using Openscad style arguments.
496 * Define face vertices clockwise looking from outside.
497 * @param {Object} [options] - options for construction
498 * @returns {CSG} new 3D solid
499 */
500const polyhedron = function (options) {
501 options = options || {}
502 if (('points' in options) !== ('faces' in options)) {
503 throw new Error("polyhedron needs 'points' and 'faces' arrays")
504 }
505 let vertices = parseOptionAs3DVectorList(options, 'points', [
506 [1, 1, 0],
507 [1, -1, 0],
508 [-1, -1, 0],
509 [-1, 1, 0],
510 [0, 0, 1]
511 ])
512 .map(function (pt) {
513 return new Vertex(pt)
514 })
515 let faces = parseOption(options, 'faces', [
516 [0, 1, 4],
517 [1, 2, 4],
518 [2, 3, 4],
519 [3, 0, 4],
520 [1, 0, 3],
521 [2, 1, 3]
522 ])
523 // Openscad convention defines inward normals - so we have to invert here
524 faces.forEach(function (face) {
525 face.reverse()
526 })
527 let polygons = faces.map(function (face) {
528 return new Polygon(face.map(function (idx) {
529 return vertices[idx]
530 }))
531 })
532
533 // TODO: facecenters as connectors? probably overkill. Maybe centroid
534 // the re-tesselation here happens because it's so easy for a user to
535 // create parametrized polyhedrons that end up with 1-2 dimensional polygons.
536 // These will create infinite loops at CSG.Tree()
537 return CSG.fromPolygons(polygons).reTesselated()
538}
539
540module.exports = {
541 cube,
542 sphere,
543 roundedCube,
544 cylinder,
545 roundedCylinder,
546 cylinderElliptic,
547 polyhedron
548}