modeling/src/primitives/roundedCylinder.js

  1. const { EPS, TAU } = require('../maths/constants')
  2. const vec3 = require('../maths/vec3')
  3. const geom3 = require('../geometries/geom3')
  4. const poly3 = require('../geometries/poly3')
  5. const { sin, cos } = require('../maths/utils/trigonometry')
  6. const { isGTE, isNumberArray } = require('./commonChecks')
  7. const cylinder = require('./cylinder')
  8. /**
  9. * Construct a Z axis-aligned solid cylinder in three dimensional space with rounded ends.
  10. * @param {Object} [options] - options for construction
  11. * @param {Array} [options.center=[0,0,0]] - center of cylinder
  12. * @param {Number} [options.height=2] - height of cylinder
  13. * @param {Number} [options.radius=1] - radius of cylinder
  14. * @param {Number} [options.roundRadius=0.2] - radius of rounded edges
  15. * @param {Number} [options.segments=32] - number of segments to create per full rotation
  16. * @returns {geom3} new 3D geometry
  17. * @alias module:modeling/primitives.roundedCylinder
  18. *
  19. * @example
  20. * let myshape = roundedCylinder({ height: 10, radius: 2, roundRadius: 0.5 })
  21. */
  22. const roundedCylinder = (options) => {
  23. const defaults = {
  24. center: [0, 0, 0],
  25. height: 2,
  26. radius: 1,
  27. roundRadius: 0.2,
  28. segments: 32
  29. }
  30. const { center, height, radius, roundRadius, segments } = Object.assign({}, defaults, options)
  31. if (!isNumberArray(center, 3)) throw new Error('center must be an array of X, Y and Z values')
  32. if (!isGTE(height, 0)) throw new Error('height must be positive')
  33. if (!isGTE(radius, 0)) throw new Error('radius must be positive')
  34. if (!isGTE(roundRadius, 0)) throw new Error('roundRadius must be positive')
  35. if (roundRadius > radius) throw new Error('roundRadius must be smaller than the radius')
  36. if (!isGTE(segments, 4)) throw new Error('segments must be four or more')
  37. // if size is zero return empty geometry
  38. if (height === 0 || radius === 0) return geom3.create()
  39. // if roundRadius is zero, return cylinder
  40. if (roundRadius === 0) return cylinder({ center, height, radius })
  41. const start = [0, 0, -(height / 2)]
  42. const end = [0, 0, height / 2]
  43. const direction = vec3.subtract(vec3.create(), end, start)
  44. const length = vec3.length(direction)
  45. if ((2 * roundRadius) > (length - EPS)) throw new Error('height must be larger than twice roundRadius')
  46. let defaultnormal
  47. if (Math.abs(direction[0]) > Math.abs(direction[1])) {
  48. defaultnormal = vec3.fromValues(0, 1, 0)
  49. } else {
  50. defaultnormal = vec3.fromValues(1, 0, 0)
  51. }
  52. const zvector = vec3.scale(vec3.create(), vec3.normalize(vec3.create(), direction), roundRadius)
  53. const xvector = vec3.scale(vec3.create(), vec3.normalize(vec3.create(), vec3.cross(vec3.create(), zvector, defaultnormal)), radius)
  54. const yvector = vec3.scale(vec3.create(), vec3.normalize(vec3.create(), vec3.cross(vec3.create(), xvector, zvector)), radius)
  55. vec3.add(start, start, zvector)
  56. vec3.subtract(end, end, zvector)
  57. const qsegments = Math.floor(0.25 * segments)
  58. const fromPoints = (points) => {
  59. // adjust the points to center
  60. const newpoints = points.map((point) => vec3.add(point, point, center))
  61. return poly3.create(newpoints)
  62. }
  63. const polygons = []
  64. const v1 = vec3.create()
  65. const v2 = vec3.create()
  66. let prevcylinderpoint
  67. for (let slice1 = 0; slice1 <= segments; slice1++) {
  68. const angle = TAU * slice1 / segments
  69. const cylinderpoint = vec3.add(vec3.create(), vec3.scale(v1, xvector, cos(angle)), vec3.scale(v2, yvector, sin(angle)))
  70. if (slice1 > 0) {
  71. // cylinder wall
  72. let points = []
  73. points.push(vec3.add(vec3.create(), start, cylinderpoint))
  74. points.push(vec3.add(vec3.create(), start, prevcylinderpoint))
  75. points.push(vec3.add(vec3.create(), end, prevcylinderpoint))
  76. points.push(vec3.add(vec3.create(), end, cylinderpoint))
  77. polygons.push(fromPoints(points))
  78. let prevcospitch, prevsinpitch
  79. for (let slice2 = 0; slice2 <= qsegments; slice2++) {
  80. const pitch = TAU / 4 * slice2 / qsegments
  81. const cospitch = cos(pitch)
  82. const sinpitch = sin(pitch)
  83. if (slice2 > 0) {
  84. // cylinder rounding, start
  85. points = []
  86. let point
  87. point = vec3.add(vec3.create(), start, vec3.subtract(v1, vec3.scale(v1, prevcylinderpoint, prevcospitch), vec3.scale(v2, zvector, prevsinpitch)))
  88. points.push(point)
  89. point = vec3.add(vec3.create(), start, vec3.subtract(v1, vec3.scale(v1, cylinderpoint, prevcospitch), vec3.scale(v2, zvector, prevsinpitch)))
  90. points.push(point)
  91. if (slice2 < qsegments) {
  92. point = vec3.add(vec3.create(), start, vec3.subtract(v1, vec3.scale(v1, cylinderpoint, cospitch), vec3.scale(v2, zvector, sinpitch)))
  93. points.push(point)
  94. }
  95. point = vec3.add(vec3.create(), start, vec3.subtract(v1, vec3.scale(v1, prevcylinderpoint, cospitch), vec3.scale(v2, zvector, sinpitch)))
  96. points.push(point)
  97. polygons.push(fromPoints(points))
  98. // cylinder rounding, end
  99. points = []
  100. point = vec3.add(vec3.create(), vec3.scale(v1, prevcylinderpoint, prevcospitch), vec3.scale(v2, zvector, prevsinpitch))
  101. vec3.add(point, point, end)
  102. points.push(point)
  103. point = vec3.add(vec3.create(), vec3.scale(v1, cylinderpoint, prevcospitch), vec3.scale(v2, zvector, prevsinpitch))
  104. vec3.add(point, point, end)
  105. points.push(point)
  106. if (slice2 < qsegments) {
  107. point = vec3.add(vec3.create(), vec3.scale(v1, cylinderpoint, cospitch), vec3.scale(v2, zvector, sinpitch))
  108. vec3.add(point, point, end)
  109. points.push(point)
  110. }
  111. point = vec3.add(vec3.create(), vec3.scale(v1, prevcylinderpoint, cospitch), vec3.scale(v2, zvector, sinpitch))
  112. vec3.add(point, point, end)
  113. points.push(point)
  114. points.reverse()
  115. polygons.push(fromPoints(points))
  116. }
  117. prevcospitch = cospitch
  118. prevsinpitch = sinpitch
  119. }
  120. }
  121. prevcylinderpoint = cylinderpoint
  122. }
  123. const result = geom3.create(polygons)
  124. return result
  125. }
  126. module.exports = roundedCylinder