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