This repository has been archived on 2023-03-25. You can view files and clone it, but cannot push or open issues or pull requests.
mightyscape-1.1-deprecated/extensions/fablabchemnitz/papercraft/openjscad/node_modules/@jscad/csg/src/primitives3d.js

549 lines
22 KiB
JavaScript

const CSG = require('./CSG')
const {parseOption, parseOptionAs3DVector, parseOptionAs2DVector, parseOptionAs3DVectorList, parseOptionAsFloat, parseOptionAsInt} = require('./optionParsers')
const {defaultResolution3D, defaultResolution2D, EPS} = require('./constants')
const Vector3D = require('./math/Vector3')
const Vertex = require('./math/Vertex3')
const Polygon = require('./math/Polygon3')
const {Connector} = require('./connectors')
const Properties = require('./Properties')
/** Construct an axis-aligned solid cuboid.
* @param {Object} [options] - options for construction
* @param {Vector3D} [options.center=[0,0,0]] - center of cube
* @param {Vector3D} [options.radius=[1,1,1]] - radius of cube, single scalar also possible
* @returns {CSG} new 3D solid
*
* @example
* let cube = CSG.cube({
* center: [5, 5, 5],
* radius: 5, // scalar radius
* });
*/
const cube = function (options) {
let c
let r
let corner1
let corner2
options = options || {}
if (('corner1' in options) || ('corner2' in options)) {
if (('center' in options) || ('radius' in options)) {
throw new Error('cube: should either give a radius and center parameter, or a corner1 and corner2 parameter')
}
corner1 = parseOptionAs3DVector(options, 'corner1', [0, 0, 0])
corner2 = parseOptionAs3DVector(options, 'corner2', [1, 1, 1])
c = corner1.plus(corner2).times(0.5)
r = corner2.minus(corner1).times(0.5)
} else {
c = parseOptionAs3DVector(options, 'center', [0, 0, 0])
r = parseOptionAs3DVector(options, 'radius', [1, 1, 1])
}
r = r.abs() // negative radii make no sense
let result = CSG.fromPolygons([
[
[0, 4, 6, 2],
[-1, 0, 0]
],
[
[1, 3, 7, 5],
[+1, 0, 0]
],
[
[0, 1, 5, 4],
[0, -1, 0]
],
[
[2, 6, 7, 3],
[0, +1, 0]
],
[
[0, 2, 3, 1],
[0, 0, -1]
],
[
[4, 5, 7, 6],
[0, 0, +1]
]
].map(function (info) {
let vertices = info[0].map(function (i) {
let pos = new Vector3D(
c.x + r.x * (2 * !!(i & 1) - 1), c.y + r.y * (2 * !!(i & 2) - 1), c.z + r.z * (2 * !!(i & 4) - 1))
return new Vertex(pos)
})
return new Polygon(vertices, null /* , plane */)
}))
result.properties.cube = new Properties()
result.properties.cube.center = new Vector3D(c)
// add 6 connectors, at the centers of each face:
result.properties.cube.facecenters = [
new Connector(new Vector3D([r.x, 0, 0]).plus(c), [1, 0, 0], [0, 0, 1]),
new Connector(new Vector3D([-r.x, 0, 0]).plus(c), [-1, 0, 0], [0, 0, 1]),
new Connector(new Vector3D([0, r.y, 0]).plus(c), [0, 1, 0], [0, 0, 1]),
new Connector(new Vector3D([0, -r.y, 0]).plus(c), [0, -1, 0], [0, 0, 1]),
new Connector(new Vector3D([0, 0, r.z]).plus(c), [0, 0, 1], [1, 0, 0]),
new Connector(new Vector3D([0, 0, -r.z]).plus(c), [0, 0, -1], [1, 0, 0])
]
return result
}
/** Construct a solid sphere
* @param {Object} [options] - options for construction
* @param {Vector3D} [options.center=[0,0,0]] - center of sphere
* @param {Number} [options.radius=1] - radius of sphere
* @param {Number} [options.resolution=defaultResolution3D] - number of polygons per 360 degree revolution
* @param {Array} [options.axes] - an array with 3 vectors for the x, y and z base vectors
* @returns {CSG} new 3D solid
*
*
* @example
* let sphere = CSG.sphere({
* center: [0, 0, 0],
* radius: 2,
* resolution: 32,
* });
*/
const sphere = function (options) {
options = options || {}
let center = parseOptionAs3DVector(options, 'center', [0, 0, 0])
let radius = parseOptionAsFloat(options, 'radius', 1)
let resolution = parseOptionAsInt(options, 'resolution', defaultResolution3D)
let xvector, yvector, zvector
if ('axes' in options) {
xvector = options.axes[0].unit().times(radius)
yvector = options.axes[1].unit().times(radius)
zvector = options.axes[2].unit().times(radius)
} else {
xvector = new Vector3D([1, 0, 0]).times(radius)
yvector = new Vector3D([0, -1, 0]).times(radius)
zvector = new Vector3D([0, 0, 1]).times(radius)
}
if (resolution < 4) resolution = 4
let qresolution = Math.round(resolution / 4)
let prevcylinderpoint
let polygons = []
for (let slice1 = 0; slice1 <= resolution; slice1++) {
let angle = Math.PI * 2.0 * slice1 / resolution
let cylinderpoint = xvector.times(Math.cos(angle)).plus(yvector.times(Math.sin(angle)))
if (slice1 > 0) {
// cylinder vertices:
let vertices = []
let prevcospitch, prevsinpitch
for (let slice2 = 0; slice2 <= qresolution; slice2++) {
let pitch = 0.5 * Math.PI * slice2 / qresolution
let cospitch = Math.cos(pitch)
let sinpitch = Math.sin(pitch)
if (slice2 > 0) {
vertices = []
vertices.push(new Vertex(center.plus(prevcylinderpoint.times(prevcospitch).minus(zvector.times(prevsinpitch)))))
vertices.push(new Vertex(center.plus(cylinderpoint.times(prevcospitch).minus(zvector.times(prevsinpitch)))))
if (slice2 < qresolution) {
vertices.push(new Vertex(center.plus(cylinderpoint.times(cospitch).minus(zvector.times(sinpitch)))))
}
vertices.push(new Vertex(center.plus(prevcylinderpoint.times(cospitch).minus(zvector.times(sinpitch)))))
polygons.push(new Polygon(vertices))
vertices = []
vertices.push(new Vertex(center.plus(prevcylinderpoint.times(prevcospitch).plus(zvector.times(prevsinpitch)))))
vertices.push(new Vertex(center.plus(cylinderpoint.times(prevcospitch).plus(zvector.times(prevsinpitch)))))
if (slice2 < qresolution) {
vertices.push(new Vertex(center.plus(cylinderpoint.times(cospitch).plus(zvector.times(sinpitch)))))
}
vertices.push(new Vertex(center.plus(prevcylinderpoint.times(cospitch).plus(zvector.times(sinpitch)))))
vertices.reverse()
polygons.push(new Polygon(vertices))
}
prevcospitch = cospitch
prevsinpitch = sinpitch
}
}
prevcylinderpoint = cylinderpoint
}
let result = CSG.fromPolygons(polygons)
result.properties.sphere = new Properties()
result.properties.sphere.center = new Vector3D(center)
result.properties.sphere.facepoint = center.plus(xvector)
return result
}
/** Construct a solid cylinder.
* @param {Object} [options] - options for construction
* @param {Vector} [options.start=[0,-1,0]] - start point of cylinder
* @param {Vector} [options.end=[0,1,0]] - end point of cylinder
* @param {Number} [options.radius=1] - radius of cylinder, must be scalar
* @param {Number} [options.resolution=defaultResolution3D] - number of polygons per 360 degree revolution
* @returns {CSG} new 3D solid
*
* @example
* let cylinder = CSG.cylinder({
* start: [0, -10, 0],
* end: [0, 10, 0],
* radius: 10,
* resolution: 16
* });
*/
const cylinder = function (options) {
let s = parseOptionAs3DVector(options, 'start', [0, -1, 0])
let e = parseOptionAs3DVector(options, 'end', [0, 1, 0])
let r = parseOptionAsFloat(options, 'radius', 1)
let rEnd = parseOptionAsFloat(options, 'radiusEnd', r)
let rStart = parseOptionAsFloat(options, 'radiusStart', r)
let alpha = parseOptionAsFloat(options, 'sectorAngle', 360)
alpha = alpha > 360 ? alpha % 360 : alpha
if ((rEnd < 0) || (rStart < 0)) {
throw new Error('Radius should be non-negative')
}
if ((rEnd === 0) && (rStart === 0)) {
throw new Error('Either radiusStart or radiusEnd should be positive')
}
let slices = parseOptionAsInt(options, 'resolution', defaultResolution2D) // FIXME is this 3D?
let ray = e.minus(s)
let axisZ = ray.unit() //, isY = (Math.abs(axisZ.y) > 0.5);
let axisX = axisZ.randomNonParallelVector().unit()
// let axisX = new Vector3D(isY, !isY, 0).cross(axisZ).unit();
let axisY = axisX.cross(axisZ).unit()
let start = new Vertex(s)
let end = new Vertex(e)
let polygons = []
function point (stack, slice, radius) {
let angle = slice * Math.PI * alpha / 180
let out = axisX.times(Math.cos(angle)).plus(axisY.times(Math.sin(angle)))
let pos = s.plus(ray.times(stack)).plus(out.times(radius))
return new Vertex(pos)
}
if (alpha > 0) {
for (let i = 0; i < slices; i++) {
let t0 = i / slices
let t1 = (i + 1) / slices
if (rEnd === rStart) {
polygons.push(new Polygon([start, point(0, t0, rEnd), point(0, t1, rEnd)]))
polygons.push(new Polygon([point(0, t1, rEnd), point(0, t0, rEnd), point(1, t0, rEnd), point(1, t1, rEnd)]))
polygons.push(new Polygon([end, point(1, t1, rEnd), point(1, t0, rEnd)]))
} else {
if (rStart > 0) {
polygons.push(new Polygon([start, point(0, t0, rStart), point(0, t1, rStart)]))
polygons.push(new Polygon([point(0, t0, rStart), point(1, t0, rEnd), point(0, t1, rStart)]))
}
if (rEnd > 0) {
polygons.push(new Polygon([end, point(1, t1, rEnd), point(1, t0, rEnd)]))
polygons.push(new Polygon([point(1, t0, rEnd), point(1, t1, rEnd), point(0, t1, rStart)]))
}
}
}
if (alpha < 360) {
polygons.push(new Polygon([start, end, point(0, 0, rStart)]))
polygons.push(new Polygon([point(0, 0, rStart), end, point(1, 0, rEnd)]))
polygons.push(new Polygon([start, point(0, 1, rStart), end]))
polygons.push(new Polygon([point(0, 1, rStart), point(1, 1, rEnd), end]))
}
}
let result = CSG.fromPolygons(polygons)
result.properties.cylinder = new Properties()
result.properties.cylinder.start = new Connector(s, axisZ.negated(), axisX)
result.properties.cylinder.end = new Connector(e, axisZ, axisX)
let cylCenter = s.plus(ray.times(0.5))
let fptVec = axisX.rotate(s, axisZ, -alpha / 2).times((rStart + rEnd) / 2)
let fptVec90 = fptVec.cross(axisZ)
// note this one is NOT a face normal for a cone. - It's horizontal from cyl perspective
result.properties.cylinder.facepointH = new Connector(cylCenter.plus(fptVec), fptVec, axisZ)
result.properties.cylinder.facepointH90 = new Connector(cylCenter.plus(fptVec90), fptVec90, axisZ)
return result
}
/** Construct a cylinder with rounded ends.
* @param {Object} [options] - options for construction
* @param {Vector3D} [options.start=[0,-1,0]] - start point of cylinder
* @param {Vector3D} [options.end=[0,1,0]] - end point of cylinder
* @param {Number} [options.radius=1] - radius of rounded ends, must be scalar
* @param {Vector3D} [options.normal] - vector determining the starting angle for tesselation. Should be non-parallel to start.minus(end)
* @param {Number} [options.resolution=defaultResolution3D] - number of polygons per 360 degree revolution
* @returns {CSG} new 3D solid
*
* @example
* let cylinder = CSG.roundedCylinder({
* start: [0, -10, 0],
* end: [0, 10, 0],
* radius: 2,
* resolution: 16
* });
*/
const roundedCylinder = function (options) {
let p1 = parseOptionAs3DVector(options, 'start', [0, -1, 0])
let p2 = parseOptionAs3DVector(options, 'end', [0, 1, 0])
let radius = parseOptionAsFloat(options, 'radius', 1)
let direction = p2.minus(p1)
let defaultnormal
if (Math.abs(direction.x) > Math.abs(direction.y)) {
defaultnormal = new Vector3D(0, 1, 0)
} else {
defaultnormal = new Vector3D(1, 0, 0)
}
let normal = parseOptionAs3DVector(options, 'normal', defaultnormal)
let resolution = parseOptionAsInt(options, 'resolution', defaultResolution3D)
if (resolution < 4) resolution = 4
let polygons = []
let qresolution = Math.floor(0.25 * resolution)
let length = direction.length()
if (length < EPS) {
return sphere({
center: p1,
radius: radius,
resolution: resolution
})
}
let zvector = direction.unit().times(radius)
let xvector = zvector.cross(normal).unit().times(radius)
let yvector = xvector.cross(zvector).unit().times(radius)
let prevcylinderpoint
for (let slice1 = 0; slice1 <= resolution; slice1++) {
let angle = Math.PI * 2.0 * slice1 / resolution
let cylinderpoint = xvector.times(Math.cos(angle)).plus(yvector.times(Math.sin(angle)))
if (slice1 > 0) {
// cylinder vertices:
let vertices = []
vertices.push(new Vertex(p1.plus(cylinderpoint)))
vertices.push(new Vertex(p1.plus(prevcylinderpoint)))
vertices.push(new Vertex(p2.plus(prevcylinderpoint)))
vertices.push(new Vertex(p2.plus(cylinderpoint)))
polygons.push(new Polygon(vertices))
let prevcospitch, prevsinpitch
for (let slice2 = 0; slice2 <= qresolution; slice2++) {
let pitch = 0.5 * Math.PI * slice2 / qresolution
// let pitch = Math.asin(slice2/qresolution);
let cospitch = Math.cos(pitch)
let sinpitch = Math.sin(pitch)
if (slice2 > 0) {
vertices = []
vertices.push(new Vertex(p1.plus(prevcylinderpoint.times(prevcospitch).minus(zvector.times(prevsinpitch)))))
vertices.push(new Vertex(p1.plus(cylinderpoint.times(prevcospitch).minus(zvector.times(prevsinpitch)))))
if (slice2 < qresolution) {
vertices.push(new Vertex(p1.plus(cylinderpoint.times(cospitch).minus(zvector.times(sinpitch)))))
}
vertices.push(new Vertex(p1.plus(prevcylinderpoint.times(cospitch).minus(zvector.times(sinpitch)))))
polygons.push(new Polygon(vertices))
vertices = []
vertices.push(new Vertex(p2.plus(prevcylinderpoint.times(prevcospitch).plus(zvector.times(prevsinpitch)))))
vertices.push(new Vertex(p2.plus(cylinderpoint.times(prevcospitch).plus(zvector.times(prevsinpitch)))))
if (slice2 < qresolution) {
vertices.push(new Vertex(p2.plus(cylinderpoint.times(cospitch).plus(zvector.times(sinpitch)))))
}
vertices.push(new Vertex(p2.plus(prevcylinderpoint.times(cospitch).plus(zvector.times(sinpitch)))))
vertices.reverse()
polygons.push(new Polygon(vertices))
}
prevcospitch = cospitch
prevsinpitch = sinpitch
}
}
prevcylinderpoint = cylinderpoint
}
let result = CSG.fromPolygons(polygons)
let ray = zvector.unit()
let axisX = xvector.unit()
result.properties.roundedCylinder = new Properties()
result.properties.roundedCylinder.start = new Connector(p1, ray.negated(), axisX)
result.properties.roundedCylinder.end = new Connector(p2, ray, axisX)
result.properties.roundedCylinder.facepoint = p1.plus(xvector)
return result
}
/** Construct an elliptic cylinder.
* @param {Object} [options] - options for construction
* @param {Vector3D} [options.start=[0,-1,0]] - start point of cylinder
* @param {Vector3D} [options.end=[0,1,0]] - end point of cylinder
* @param {Vector2D} [options.radius=[1,1]] - radius of rounded ends, must be two dimensional array
* @param {Vector2D} [options.radiusStart=[1,1]] - OPTIONAL radius of rounded start, must be two dimensional array
* @param {Vector2D} [options.radiusEnd=[1,1]] - OPTIONAL radius of rounded end, must be two dimensional array
* @param {Number} [options.resolution=defaultResolution2D] - number of polygons per 360 degree revolution
* @returns {CSG} new 3D solid
*
* @example
* let cylinder = CSG.cylinderElliptic({
* start: [0, -10, 0],
* end: [0, 10, 0],
* radiusStart: [10,5],
* radiusEnd: [8,3],
* resolution: 16
* });
*/
const cylinderElliptic = function (options) {
let s = parseOptionAs3DVector(options, 'start', [0, -1, 0])
let e = parseOptionAs3DVector(options, 'end', [0, 1, 0])
let r = parseOptionAs2DVector(options, 'radius', [1, 1])
let rEnd = parseOptionAs2DVector(options, 'radiusEnd', r)
let rStart = parseOptionAs2DVector(options, 'radiusStart', r)
if ((rEnd._x < 0) || (rStart._x < 0) || (rEnd._y < 0) || (rStart._y < 0)) {
throw new Error('Radius should be non-negative')
}
if ((rEnd._x === 0 || rEnd._y === 0) && (rStart._x === 0 || rStart._y === 0)) {
throw new Error('Either radiusStart or radiusEnd should be positive')
}
let slices = parseOptionAsInt(options, 'resolution', defaultResolution2D) // FIXME is this correct?
let ray = e.minus(s)
let axisZ = ray.unit() //, isY = (Math.abs(axisZ.y) > 0.5);
let axisX = axisZ.randomNonParallelVector().unit()
// let axisX = new Vector3D(isY, !isY, 0).cross(axisZ).unit();
let axisY = axisX.cross(axisZ).unit()
let start = new Vertex(s)
let end = new Vertex(e)
let polygons = []
function point (stack, slice, radius) {
let angle = slice * Math.PI * 2
let out = axisX.times(radius._x * Math.cos(angle)).plus(axisY.times(radius._y * Math.sin(angle)))
let pos = s.plus(ray.times(stack)).plus(out)
return new Vertex(pos)
}
for (let i = 0; i < slices; i++) {
let t0 = i / slices
let t1 = (i + 1) / slices
if (rEnd._x === rStart._x && rEnd._y === rStart._y) {
polygons.push(new Polygon([start, point(0, t0, rEnd), point(0, t1, rEnd)]))
polygons.push(new Polygon([point(0, t1, rEnd), point(0, t0, rEnd), point(1, t0, rEnd), point(1, t1, rEnd)]))
polygons.push(new Polygon([end, point(1, t1, rEnd), point(1, t0, rEnd)]))
} else {
if (rStart._x > 0) {
polygons.push(new Polygon([start, point(0, t0, rStart), point(0, t1, rStart)]))
polygons.push(new Polygon([point(0, t0, rStart), point(1, t0, rEnd), point(0, t1, rStart)]))
}
if (rEnd._x > 0) {
polygons.push(new Polygon([end, point(1, t1, rEnd), point(1, t0, rEnd)]))
polygons.push(new Polygon([point(1, t0, rEnd), point(1, t1, rEnd), point(0, t1, rStart)]))
}
}
}
let result = CSG.fromPolygons(polygons)
result.properties.cylinder = new Properties()
result.properties.cylinder.start = new Connector(s, axisZ.negated(), axisX)
result.properties.cylinder.end = new Connector(e, axisZ, axisX)
result.properties.cylinder.facepoint = s.plus(axisX.times(rStart))
return result
}
/** Construct an axis-aligned solid rounded cuboid.
* @param {Object} [options] - options for construction
* @param {Vector3D} [options.center=[0,0,0]] - center of rounded cube
* @param {Vector3D} [options.radius=[1,1,1]] - radius of rounded cube, single scalar is possible
* @param {Number} [options.roundradius=0.2] - radius of rounded edges
* @param {Number} [options.resolution=defaultResolution3D] - number of polygons per 360 degree revolution
* @returns {CSG} new 3D solid
*
* @example
* let cube = CSG.roundedCube({
* center: [2, 0, 2],
* radius: 15,
* roundradius: 2,
* resolution: 36,
* });
*/
const roundedCube = function (options) {
let minRR = 1e-2 // minroundradius 1e-3 gives rounding errors already
let center
let cuberadius
let corner1
let corner2
options = options || {}
if (('corner1' in options) || ('corner2' in options)) {
if (('center' in options) || ('radius' in options)) {
throw new Error('roundedCube: should either give a radius and center parameter, or a corner1 and corner2 parameter')
}
corner1 = parseOptionAs3DVector(options, 'corner1', [0, 0, 0])
corner2 = parseOptionAs3DVector(options, 'corner2', [1, 1, 1])
center = corner1.plus(corner2).times(0.5)
cuberadius = corner2.minus(corner1).times(0.5)
} else {
center = parseOptionAs3DVector(options, 'center', [0, 0, 0])
cuberadius = parseOptionAs3DVector(options, 'radius', [1, 1, 1])
}
cuberadius = cuberadius.abs() // negative radii make no sense
let resolution = parseOptionAsInt(options, 'resolution', defaultResolution3D)
if (resolution < 4) resolution = 4
if (resolution % 2 === 1 && resolution < 8) resolution = 8 // avoid ugly
let roundradius = parseOptionAs3DVector(options, 'roundradius', [0.2, 0.2, 0.2])
// slight hack for now - total radius stays ok
roundradius = Vector3D.Create(Math.max(roundradius.x, minRR), Math.max(roundradius.y, minRR), Math.max(roundradius.z, minRR))
let innerradius = cuberadius.minus(roundradius)
if (innerradius.x < 0 || innerradius.y < 0 || innerradius.z < 0) {
throw new Error('roundradius <= radius!')
}
let res = sphere({radius: 1, resolution: resolution})
res = res.scale(roundradius)
innerradius.x > EPS && (res = res.stretchAtPlane([1, 0, 0], [0, 0, 0], 2 * innerradius.x))
innerradius.y > EPS && (res = res.stretchAtPlane([0, 1, 0], [0, 0, 0], 2 * innerradius.y))
innerradius.z > EPS && (res = res.stretchAtPlane([0, 0, 1], [0, 0, 0], 2 * innerradius.z))
res = res.translate([-innerradius.x + center.x, -innerradius.y + center.y, -innerradius.z + center.z])
res = res.reTesselated()
res.properties.roundedCube = new Properties()
res.properties.roundedCube.center = new Vertex(center)
res.properties.roundedCube.facecenters = [
new Connector(new Vector3D([cuberadius.x, 0, 0]).plus(center), [1, 0, 0], [0, 0, 1]),
new Connector(new Vector3D([-cuberadius.x, 0, 0]).plus(center), [-1, 0, 0], [0, 0, 1]),
new Connector(new Vector3D([0, cuberadius.y, 0]).plus(center), [0, 1, 0], [0, 0, 1]),
new Connector(new Vector3D([0, -cuberadius.y, 0]).plus(center), [0, -1, 0], [0, 0, 1]),
new Connector(new Vector3D([0, 0, cuberadius.z]).plus(center), [0, 0, 1], [1, 0, 0]),
new Connector(new Vector3D([0, 0, -cuberadius.z]).plus(center), [0, 0, -1], [1, 0, 0])
]
return res
}
/** Create a polyhedron using Openscad style arguments.
* Define face vertices clockwise looking from outside.
* @param {Object} [options] - options for construction
* @returns {CSG} new 3D solid
*/
const polyhedron = function (options) {
options = options || {}
if (('points' in options) !== ('faces' in options)) {
throw new Error("polyhedron needs 'points' and 'faces' arrays")
}
let vertices = parseOptionAs3DVectorList(options, 'points', [
[1, 1, 0],
[1, -1, 0],
[-1, -1, 0],
[-1, 1, 0],
[0, 0, 1]
])
.map(function (pt) {
return new Vertex(pt)
})
let faces = parseOption(options, 'faces', [
[0, 1, 4],
[1, 2, 4],
[2, 3, 4],
[3, 0, 4],
[1, 0, 3],
[2, 1, 3]
])
// Openscad convention defines inward normals - so we have to invert here
faces.forEach(function (face) {
face.reverse()
})
let polygons = faces.map(function (face) {
return new Polygon(face.map(function (idx) {
return vertices[idx]
}))
})
// TODO: facecenters as connectors? probably overkill. Maybe centroid
// the re-tesselation here happens because it's so easy for a user to
// create parametrized polyhedrons that end up with 1-2 dimensional polygons.
// These will create infinite loops at CSG.Tree()
return CSG.fromPolygons(polygons).reTesselated()
}
module.exports = {
cube,
sphere,
roundedCube,
cylinder,
roundedCylinder,
cylinderElliptic,
polyhedron
}