549 lines
22 KiB
JavaScript
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
|
|
}
|