modeling/src/primitives/arc.js

  1. const { EPS, TAU } = require('../maths/constants')
  2. const vec2 = require('../maths/vec2')
  3. const path2 = require('../geometries/path2')
  4. const { isGT, isGTE, isNumberArray } = require('./commonChecks')
  5. /**
  6. * Construct an arc in two dimensional space where all points are at the same distance from the center.
  7. * @param {Object} [options] - options for construction
  8. * @param {Array} [options.center=[0,0]] - center of arc
  9. * @param {Number} [options.radius=1] - radius of arc
  10. * @param {Number} [options.startAngle=0] - starting angle of the arc, in radians
  11. * @param {Number} [options.endAngle=TAU] - ending angle of the arc, in radians
  12. * @param {Number} [options.segments=32] - number of segments to create per full rotation
  13. * @param {Boolean} [options.makeTangent=false] - adds line segments at both ends of the arc to ensure that the gradients at the edges are tangent
  14. * @returns {path2} new 2D path
  15. * @alias module:modeling/primitives.arc
  16. * @example
  17. * let myshape = arc({ center: [-1, -1], radius: 2, endAngle: (TAU / 4)})
  18. */
  19. const arc = (options) => {
  20. const defaults = {
  21. center: [0, 0],
  22. radius: 1,
  23. startAngle: 0,
  24. endAngle: TAU,
  25. makeTangent: false,
  26. segments: 32
  27. }
  28. let { center, radius, startAngle, endAngle, makeTangent, segments } = Object.assign({}, defaults, options)
  29. if (!isNumberArray(center, 2)) throw new Error('center must be an array of X and Y values')
  30. if (!isGT(radius, 0)) throw new Error('radius must be greater than zero')
  31. if (!isGTE(startAngle, 0)) throw new Error('startAngle must be positive')
  32. if (!isGTE(endAngle, 0)) throw new Error('endAngle must be positive')
  33. if (!isGTE(segments, 4)) throw new Error('segments must be four or more')
  34. startAngle = startAngle % TAU
  35. endAngle = endAngle % TAU
  36. let rotation = TAU
  37. if (startAngle < endAngle) {
  38. rotation = endAngle - startAngle
  39. }
  40. if (startAngle > endAngle) {
  41. rotation = endAngle + (TAU - startAngle)
  42. }
  43. const minangle = Math.acos(((radius * radius) + (radius * radius) - (EPS * EPS)) / (2 * radius * radius))
  44. const centerv = vec2.clone(center)
  45. let point
  46. const pointArray = []
  47. if (rotation < minangle) {
  48. // there is no rotation, just a single point
  49. point = vec2.fromAngleRadians(vec2.create(), startAngle)
  50. vec2.scale(point, point, radius)
  51. vec2.add(point, point, centerv)
  52. pointArray.push(point)
  53. } else {
  54. // note: add one additional step to acheive full rotation
  55. const numsteps = Math.max(1, Math.floor(segments * (rotation / TAU))) + 1
  56. let edgestepsize = numsteps * 0.5 / rotation // step size for half a degree
  57. if (edgestepsize > 0.25) edgestepsize = 0.25
  58. const totalsteps = makeTangent ? (numsteps + 2) : numsteps
  59. for (let i = 0; i <= totalsteps; i++) {
  60. let step = i
  61. if (makeTangent) {
  62. step = (i - 1) * (numsteps - 2 * edgestepsize) / numsteps + edgestepsize
  63. if (step < 0) step = 0
  64. if (step > numsteps) step = numsteps
  65. }
  66. const angle = startAngle + (step * (rotation / numsteps))
  67. point = vec2.fromAngleRadians(vec2.create(), angle)
  68. vec2.scale(point, point, radius)
  69. vec2.add(point, point, centerv)
  70. pointArray.push(point)
  71. }
  72. }
  73. return path2.fromPoints({ closed: false }, pointArray)
  74. }
  75. module.exports = arc