modeling/src/primitives/ellipse.js

  1. const { EPS, TAU } = require('../maths/constants')
  2. const vec2 = require('../maths/vec2')
  3. const geom2 = require('../geometries/geom2')
  4. const { sin, cos } = require('../maths/utils/trigonometry')
  5. const { isGTE, isNumberArray } = require('./commonChecks')
  6. /**
  7. * Construct an axis-aligned ellipse in two dimensional space.
  8. * @see https://en.wikipedia.org/wiki/Ellipse
  9. * @param {Object} [options] - options for construction
  10. * @param {Array} [options.center=[0,0]] - center of ellipse
  11. * @param {Array} [options.radius=[1,1]] - radius of ellipse, along X and Y
  12. * @param {Number} [options.startAngle=0] - start angle of ellipse, in radians
  13. * @param {Number} [options.endAngle=TAU] - end angle of ellipse, in radians
  14. * @param {Number} [options.segments=32] - number of segments to create per full rotation
  15. * @returns {geom2} new 2D geometry
  16. * @alias module:modeling/primitives.ellipse
  17. * @example
  18. * let myshape = ellipse({radius: [5,10]})
  19. */
  20. const ellipse = (options) => {
  21. const defaults = {
  22. center: [0, 0],
  23. radius: [1, 1],
  24. startAngle: 0,
  25. endAngle: TAU,
  26. segments: 32
  27. }
  28. let { center, radius, startAngle, endAngle, 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 (!isNumberArray(radius, 2)) throw new Error('radius must be an array of X and Y values')
  31. if (!radius.every((n) => n >= 0)) throw new Error('radius values must be positive')
  32. if (!isGTE(startAngle, 0)) throw new Error('startAngle must be positive')
  33. if (!isGTE(endAngle, 0)) throw new Error('endAngle must be positive')
  34. if (!isGTE(segments, 3)) throw new Error('segments must be three or more')
  35. // if any radius is zero return empty geometry
  36. if (radius[0] === 0 || radius[1] === 0) return geom2.create()
  37. startAngle = startAngle % TAU
  38. endAngle = endAngle % TAU
  39. let rotation = TAU
  40. if (startAngle < endAngle) {
  41. rotation = endAngle - startAngle
  42. }
  43. if (startAngle > endAngle) {
  44. rotation = endAngle + (TAU - startAngle)
  45. }
  46. const minradius = Math.min(radius[0], radius[1])
  47. const minangle = Math.acos(((minradius * minradius) + (minradius * minradius) - (EPS * EPS)) /
  48. (2 * minradius * minradius))
  49. if (rotation < minangle) throw new Error('startAngle and endAngle do not define a significant rotation')
  50. segments = Math.floor(segments * (rotation / TAU))
  51. const centerv = vec2.clone(center)
  52. const step = rotation / segments // radians per segment
  53. const points = []
  54. segments = (rotation < TAU) ? segments + 1 : segments
  55. for (let i = 0; i < segments; i++) {
  56. const angle = (step * i) + startAngle
  57. const point = vec2.fromValues(radius[0] * cos(angle), radius[1] * sin(angle))
  58. vec2.add(point, centerv, point)
  59. points.push(point)
  60. }
  61. if (rotation < TAU) points.push(centerv)
  62. return geom2.fromPoints(points)
  63. }
  64. module.exports = ellipse