modeling/src/primitives/roundedRectangle.js

  1. const { EPS, TAU } = require('../maths/constants')
  2. const vec2 = require('../maths/vec2')
  3. const geom2 = require('../geometries/geom2')
  4. const { isGTE, isNumberArray } = require('./commonChecks')
  5. const rectangle = require('./rectangle')
  6. /**
  7. * Construct an axis-aligned rectangle in two dimensional space with rounded corners.
  8. * @param {Object} [options] - options for construction
  9. * @param {Array} [options.center=[0,0]] - center of rounded rectangle
  10. * @param {Array} [options.size=[2,2]] - dimension of rounded rectangle; width and length
  11. * @param {Number} [options.roundRadius=0.2] - round radius of corners
  12. * @param {Number} [options.segments=32] - number of segments to create per full rotation
  13. * @returns {geom2} new 2D geometry
  14. * @alias module:modeling/primitives.roundedRectangle
  15. *
  16. * @example
  17. * let myshape = roundedRectangle({size: [10, 20], roundRadius: 2})
  18. */
  19. const roundedRectangle = (options) => {
  20. const defaults = {
  21. center: [0, 0],
  22. size: [2, 2],
  23. roundRadius: 0.2,
  24. segments: 32
  25. }
  26. let { center, size, roundRadius, segments } = Object.assign({}, defaults, options)
  27. if (!isNumberArray(center, 2)) throw new Error('center must be an array of X and Y values')
  28. if (!isNumberArray(size, 2)) throw new Error('size must be an array of X and Y values')
  29. if (!size.every((n) => n >= 0)) throw new Error('size values must be positive')
  30. if (!isGTE(roundRadius, 0)) throw new Error('roundRadius must be positive')
  31. if (!isGTE(segments, 4)) throw new Error('segments must be four or more')
  32. // if any size is zero return empty geometry
  33. if (size[0] === 0 || size[1] === 0) return geom2.create()
  34. // if roundRadius is zero, return rectangle
  35. if (roundRadius === 0) return rectangle({ center, size })
  36. size = size.map((v) => v / 2) // convert to radius
  37. if (roundRadius > (size[0] - EPS) ||
  38. roundRadius > (size[1] - EPS)) throw new Error('roundRadius must be smaller than the radius of all dimensions')
  39. const cornersegments = Math.floor(segments / 4)
  40. // create sets of points that define the corners
  41. const corner0 = vec2.add(vec2.create(), center, [size[0] - roundRadius, size[1] - roundRadius])
  42. const corner1 = vec2.add(vec2.create(), center, [roundRadius - size[0], size[1] - roundRadius])
  43. const corner2 = vec2.add(vec2.create(), center, [roundRadius - size[0], roundRadius - size[1]])
  44. const corner3 = vec2.add(vec2.create(), center, [size[0] - roundRadius, roundRadius - size[1]])
  45. const corner0Points = []
  46. const corner1Points = []
  47. const corner2Points = []
  48. const corner3Points = []
  49. for (let i = 0; i <= cornersegments; i++) {
  50. const radians = TAU / 4 * i / cornersegments
  51. const point = vec2.fromAngleRadians(vec2.create(), radians)
  52. vec2.scale(point, point, roundRadius)
  53. corner0Points.push(vec2.add(vec2.create(), corner0, point))
  54. vec2.rotate(point, point, vec2.create(), TAU / 4)
  55. corner1Points.push(vec2.add(vec2.create(), corner1, point))
  56. vec2.rotate(point, point, vec2.create(), TAU / 4)
  57. corner2Points.push(vec2.add(vec2.create(), corner2, point))
  58. vec2.rotate(point, point, vec2.create(), TAU / 4)
  59. corner3Points.push(vec2.add(vec2.create(), corner3, point))
  60. }
  61. return geom2.fromPoints(corner0Points.concat(corner1Points, corner2Points, corner3Points))
  62. }
  63. module.exports = roundedRectangle