UNPKG

@jscad/modeling

Version:

Constructive Solid Geometry (CSG) Library for JSCAD

143 lines (127 loc) 6.8 kB
const { TAU } = require('../../maths/constants') const vec2 = require('../../maths/vec2') const fromPoints = require('./fromPoints') const toPoints = require('./toPoints') /** * Append a series of points to the given geometry that represent an arc. * This implementation follows the SVG specifications. * @see http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands * @param {Object} options - options for construction * @param {vec2} options.endpoint - end point of arc (REQUIRED) * @param {vec2} [options.radius=[0,0]] - radius of arc (X and Y) * @param {Number} [options.xaxisrotation=0] - rotation (RADIANS) of the X axis of the arc with respect to the X axis of the coordinate system * @param {Boolean} [options.clockwise=false] - draw an arc clockwise with respect to the center point * @param {Boolean} [options.large=false] - draw an arc longer than TAU / 2 radians * @param {Number} [options.segments=16] - number of segments per full rotation * @param {path2} geometry - the path of which to append the arc * @returns {path2} a new path with the appended points * @alias module:modeling/geometries/path2.appendArc * * @example * let p1 = path2.fromPoints({}, [[27.5,-22.96875]]); * p1 = path2.appendPoints([[27.5,-3.28125]], p1); * p1 = path2.appendArc({endpoint: [12.5, -22.96875], radius: [15, -19.6875]}, p1); */ const appendArc = (options, geometry) => { const defaults = { radius: [0, 0], // X and Y radius xaxisrotation: 0, clockwise: false, large: false, segments: 16 } let { endpoint, radius, xaxisrotation, clockwise, large, segments } = Object.assign({}, defaults, options) // validate the given options if (!Array.isArray(endpoint)) throw new Error('endpoint must be an array of X and Y values') if (endpoint.length < 2) throw new Error('endpoint must contain X and Y values') endpoint = vec2.clone(endpoint) if (!Array.isArray(radius)) throw new Error('radius must be an array of X and Y values') if (radius.length < 2) throw new Error('radius must contain X and Y values') if (segments < 4) throw new Error('segments must be four or more') const decimals = 100000 // validate the given geometry if (geometry.isClosed) { throw new Error('the given path cannot be closed') } const points = toPoints(geometry) if (points.length < 1) { throw new Error('the given path must contain one or more points (as the starting point for the arc)') } let xradius = radius[0] let yradius = radius[1] const startpoint = points[points.length - 1] // round to precision in order to have determinate calculations xradius = Math.round(xradius * decimals) / decimals yradius = Math.round(yradius * decimals) / decimals endpoint = vec2.fromValues(Math.round(endpoint[0] * decimals) / decimals, Math.round(endpoint[1] * decimals) / decimals) const sweepFlag = !clockwise let newpoints = [] if ((xradius === 0) || (yradius === 0)) { // http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes: // If rx = 0 or ry = 0, then treat this as a straight line from (x1, y1) to (x2, y2) and stop newpoints.push(endpoint) } else { xradius = Math.abs(xradius) yradius = Math.abs(yradius) // see http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes : const phi = xaxisrotation const cosphi = Math.cos(phi) const sinphi = Math.sin(phi) const minushalfdistance = vec2.subtract(vec2.create(), startpoint, endpoint) vec2.scale(minushalfdistance, minushalfdistance, 0.5) // F.6.5.1: // round to precision in order to have determinate calculations const x = Math.round((cosphi * minushalfdistance[0] + sinphi * minushalfdistance[1]) * decimals) / decimals const y = Math.round((-sinphi * minushalfdistance[0] + cosphi * minushalfdistance[1]) * decimals) / decimals const startTranslated = vec2.fromValues(x, y) // F.6.6.2: const biglambda = (startTranslated[0] * startTranslated[0]) / (xradius * xradius) + (startTranslated[1] * startTranslated[1]) / (yradius * yradius) if (biglambda > 1.0) { // F.6.6.3: const sqrtbiglambda = Math.sqrt(biglambda) xradius *= sqrtbiglambda yradius *= sqrtbiglambda // round to precision in order to have determinate calculations xradius = Math.round(xradius * decimals) / decimals yradius = Math.round(yradius * decimals) / decimals } // F.6.5.2: let multiplier1 = Math.sqrt((xradius * xradius * yradius * yradius - xradius * xradius * startTranslated[1] * startTranslated[1] - yradius * yradius * startTranslated[0] * startTranslated[0]) / (xradius * xradius * startTranslated[1] * startTranslated[1] + yradius * yradius * startTranslated[0] * startTranslated[0])) if (sweepFlag === large) multiplier1 = -multiplier1 const centerTranslated = vec2.fromValues(xradius * startTranslated[1] / yradius, -yradius * startTranslated[0] / xradius) vec2.scale(centerTranslated, centerTranslated, multiplier1) // F.6.5.3: let center = vec2.fromValues(cosphi * centerTranslated[0] - sinphi * centerTranslated[1], sinphi * centerTranslated[0] + cosphi * centerTranslated[1]) center = vec2.add(center, center, vec2.scale(vec2.create(), vec2.add(vec2.create(), startpoint, endpoint), 0.5)) // F.6.5.5: const vector1 = vec2.fromValues((startTranslated[0] - centerTranslated[0]) / xradius, (startTranslated[1] - centerTranslated[1]) / yradius) const vector2 = vec2.fromValues((-startTranslated[0] - centerTranslated[0]) / xradius, (-startTranslated[1] - centerTranslated[1]) / yradius) const theta1 = vec2.angleRadians(vector1) const theta2 = vec2.angleRadians(vector2) let deltatheta = theta2 - theta1 deltatheta = deltatheta % TAU if ((!sweepFlag) && (deltatheta > 0)) { deltatheta -= TAU } else if ((sweepFlag) && (deltatheta < 0)) { deltatheta += TAU } // Ok, we have the center point and angle range (from theta1, deltatheta radians) so we can create the ellipse let numsteps = Math.ceil(Math.abs(deltatheta) / TAU * segments) + 1 if (numsteps < 1) numsteps = 1 for (let step = 1; step < numsteps; step++) { const theta = theta1 + step / numsteps * deltatheta const costheta = Math.cos(theta) const sintheta = Math.sin(theta) // F.6.3.1: const point = vec2.fromValues(cosphi * xradius * costheta - sinphi * yradius * sintheta, sinphi * xradius * costheta + cosphi * yradius * sintheta) vec2.add(point, point, center) newpoints.push(point) } // ensure end point is precisely what user gave as parameter if (numsteps) newpoints.push(options.endpoint) } newpoints = points.concat(newpoints) const result = fromPoints({}, newpoints) return result } module.exports = appendArc