UNPKG

@jscad/modeling

Version:

Constructive Solid Geometry (CSG) Library for JSCAD

148 lines (126 loc) 6.11 kB
const { EPS, TAU } = require('../maths/constants') const vec3 = require('../maths/vec3') const geom3 = require('../geometries/geom3') const poly3 = require('../geometries/poly3') const { sin, cos } = require('../maths/utils/trigonometry') const { isGTE, isNumberArray } = require('./commonChecks') const cylinder = require('./cylinder') /** * Construct a Z axis-aligned solid cylinder in three dimensional space with rounded ends. * @param {Object} [options] - options for construction * @param {Array} [options.center=[0,0,0]] - center of cylinder * @param {Number} [options.height=2] - height of cylinder * @param {Number} [options.radius=1] - radius of cylinder * @param {Number} [options.roundRadius=0.2] - radius of rounded edges * @param {Number} [options.segments=32] - number of segments to create per full rotation * @returns {geom3} new 3D geometry * @alias module:modeling/primitives.roundedCylinder * * @example * let myshape = roundedCylinder({ height: 10, radius: 2, roundRadius: 0.5 }) */ const roundedCylinder = (options) => { const defaults = { center: [0, 0, 0], height: 2, radius: 1, roundRadius: 0.2, segments: 32 } const { center, height, radius, roundRadius, segments } = Object.assign({}, defaults, options) if (!isNumberArray(center, 3)) throw new Error('center must be an array of X, Y and Z values') if (!isGTE(height, 0)) throw new Error('height must be positive') if (!isGTE(radius, 0)) throw new Error('radius must be positive') if (!isGTE(roundRadius, 0)) throw new Error('roundRadius must be positive') if (roundRadius > radius) throw new Error('roundRadius must be smaller than the radius') if (!isGTE(segments, 4)) throw new Error('segments must be four or more') // if size is zero return empty geometry if (height === 0 || radius === 0) return geom3.create() // if roundRadius is zero, return cylinder if (roundRadius === 0) return cylinder({ center, height, radius }) const start = [0, 0, -(height / 2)] const end = [0, 0, height / 2] const direction = vec3.subtract(vec3.create(), end, start) const length = vec3.length(direction) if ((2 * roundRadius) > (length - EPS)) throw new Error('height must be larger than twice roundRadius') let defaultnormal if (Math.abs(direction[0]) > Math.abs(direction[1])) { defaultnormal = vec3.fromValues(0, 1, 0) } else { defaultnormal = vec3.fromValues(1, 0, 0) } const zvector = vec3.scale(vec3.create(), vec3.normalize(vec3.create(), direction), roundRadius) const xvector = vec3.scale(vec3.create(), vec3.normalize(vec3.create(), vec3.cross(vec3.create(), zvector, defaultnormal)), radius) const yvector = vec3.scale(vec3.create(), vec3.normalize(vec3.create(), vec3.cross(vec3.create(), xvector, zvector)), radius) vec3.add(start, start, zvector) vec3.subtract(end, end, zvector) const qsegments = Math.floor(0.25 * segments) const fromPoints = (points) => { // adjust the points to center const newpoints = points.map((point) => vec3.add(point, point, center)) return poly3.create(newpoints) } const polygons = [] const v1 = vec3.create() const v2 = vec3.create() let prevcylinderpoint for (let slice1 = 0; slice1 <= segments; slice1++) { const angle = TAU * slice1 / segments const cylinderpoint = vec3.add(vec3.create(), vec3.scale(v1, xvector, cos(angle)), vec3.scale(v2, yvector, sin(angle))) if (slice1 > 0) { // cylinder wall let points = [] points.push(vec3.add(vec3.create(), start, cylinderpoint)) points.push(vec3.add(vec3.create(), start, prevcylinderpoint)) points.push(vec3.add(vec3.create(), end, prevcylinderpoint)) points.push(vec3.add(vec3.create(), end, cylinderpoint)) polygons.push(fromPoints(points)) let prevcospitch, prevsinpitch for (let slice2 = 0; slice2 <= qsegments; slice2++) { const pitch = TAU / 4 * slice2 / qsegments const cospitch = cos(pitch) const sinpitch = sin(pitch) if (slice2 > 0) { // cylinder rounding, start points = [] let point point = vec3.add(vec3.create(), start, vec3.subtract(v1, vec3.scale(v1, prevcylinderpoint, prevcospitch), vec3.scale(v2, zvector, prevsinpitch))) points.push(point) point = vec3.add(vec3.create(), start, vec3.subtract(v1, vec3.scale(v1, cylinderpoint, prevcospitch), vec3.scale(v2, zvector, prevsinpitch))) points.push(point) if (slice2 < qsegments) { point = vec3.add(vec3.create(), start, vec3.subtract(v1, vec3.scale(v1, cylinderpoint, cospitch), vec3.scale(v2, zvector, sinpitch))) points.push(point) } point = vec3.add(vec3.create(), start, vec3.subtract(v1, vec3.scale(v1, prevcylinderpoint, cospitch), vec3.scale(v2, zvector, sinpitch))) points.push(point) polygons.push(fromPoints(points)) // cylinder rounding, end points = [] point = vec3.add(vec3.create(), vec3.scale(v1, prevcylinderpoint, prevcospitch), vec3.scale(v2, zvector, prevsinpitch)) vec3.add(point, point, end) points.push(point) point = vec3.add(vec3.create(), vec3.scale(v1, cylinderpoint, prevcospitch), vec3.scale(v2, zvector, prevsinpitch)) vec3.add(point, point, end) points.push(point) if (slice2 < qsegments) { point = vec3.add(vec3.create(), vec3.scale(v1, cylinderpoint, cospitch), vec3.scale(v2, zvector, sinpitch)) vec3.add(point, point, end) points.push(point) } point = vec3.add(vec3.create(), vec3.scale(v1, prevcylinderpoint, cospitch), vec3.scale(v2, zvector, sinpitch)) vec3.add(point, point, end) points.push(point) points.reverse() polygons.push(fromPoints(points)) } prevcospitch = cospitch prevsinpitch = sinpitch } } prevcylinderpoint = cylinderpoint } const result = geom3.create(polygons) return result } module.exports = roundedCylinder