modeling/src/primitives/roundedCuboid.js

  1. const { EPS, TAU } = require('../maths/constants')
  2. const vec2 = require('../maths/vec2')
  3. const vec3 = require('../maths/vec3')
  4. const geom3 = require('../geometries/geom3')
  5. const poly3 = require('../geometries/poly3')
  6. const { sin, cos } = require('../maths/utils/trigonometry')
  7. const { isGTE, isNumberArray } = require('./commonChecks')
  8. const cuboid = require('./cuboid')
  9. const createCorners = (center, size, radius, segments, slice, positive) => {
  10. const pitch = (TAU / 4) * slice / segments
  11. const cospitch = cos(pitch)
  12. const sinpitch = sin(pitch)
  13. const layersegments = segments - slice
  14. let layerradius = radius * cospitch
  15. let layeroffset = size[2] - (radius - (radius * sinpitch))
  16. if (!positive) layeroffset = (radius - (radius * sinpitch)) - size[2]
  17. layerradius = layerradius > EPS ? layerradius : 0
  18. const corner0 = vec3.add(vec3.create(), center, [size[0] - radius, size[1] - radius, layeroffset])
  19. const corner1 = vec3.add(vec3.create(), center, [radius - size[0], size[1] - radius, layeroffset])
  20. const corner2 = vec3.add(vec3.create(), center, [radius - size[0], radius - size[1], layeroffset])
  21. const corner3 = vec3.add(vec3.create(), center, [size[0] - radius, radius - size[1], layeroffset])
  22. const corner0Points = []
  23. const corner1Points = []
  24. const corner2Points = []
  25. const corner3Points = []
  26. for (let i = 0; i <= layersegments; i++) {
  27. const radians = layersegments > 0 ? TAU / 4 * i / layersegments : 0
  28. const point2d = vec2.fromAngleRadians(vec2.create(), radians)
  29. vec2.scale(point2d, point2d, layerradius)
  30. const point3d = vec3.fromVec2(vec3.create(), point2d)
  31. corner0Points.push(vec3.add(vec3.create(), corner0, point3d))
  32. vec3.rotateZ(point3d, point3d, [0, 0, 0], TAU / 4)
  33. corner1Points.push(vec3.add(vec3.create(), corner1, point3d))
  34. vec3.rotateZ(point3d, point3d, [0, 0, 0], TAU / 4)
  35. corner2Points.push(vec3.add(vec3.create(), corner2, point3d))
  36. vec3.rotateZ(point3d, point3d, [0, 0, 0], TAU / 4)
  37. corner3Points.push(vec3.add(vec3.create(), corner3, point3d))
  38. }
  39. if (!positive) {
  40. corner0Points.reverse()
  41. corner1Points.reverse()
  42. corner2Points.reverse()
  43. corner3Points.reverse()
  44. return [corner3Points, corner2Points, corner1Points, corner0Points]
  45. }
  46. return [corner0Points, corner1Points, corner2Points, corner3Points]
  47. }
  48. const stitchCorners = (previousCorners, currentCorners) => {
  49. const polygons = []
  50. for (let i = 0; i < previousCorners.length; i++) {
  51. const previous = previousCorners[i]
  52. const current = currentCorners[i]
  53. for (let j = 0; j < (previous.length - 1); j++) {
  54. polygons.push(poly3.create([previous[j], previous[j + 1], current[j]]))
  55. if (j < (current.length - 1)) {
  56. polygons.push(poly3.create([current[j], previous[j + 1], current[j + 1]]))
  57. }
  58. }
  59. }
  60. return polygons
  61. }
  62. const stitchWalls = (previousCorners, currentCorners) => {
  63. const polygons = []
  64. for (let i = 0; i < previousCorners.length; i++) {
  65. let previous = previousCorners[i]
  66. let current = currentCorners[i]
  67. const p0 = previous[previous.length - 1]
  68. const c0 = current[current.length - 1]
  69. const j = (i + 1) % previousCorners.length
  70. previous = previousCorners[j]
  71. current = currentCorners[j]
  72. const p1 = previous[0]
  73. const c1 = current[0]
  74. polygons.push(poly3.create([p0, p1, c1, c0]))
  75. }
  76. return polygons
  77. }
  78. const stitchSides = (bottomCorners, topCorners) => {
  79. // make a copy and reverse the bottom corners
  80. bottomCorners = [bottomCorners[3], bottomCorners[2], bottomCorners[1], bottomCorners[0]]
  81. bottomCorners = bottomCorners.map((corner) => corner.slice().reverse())
  82. const bottomPoints = []
  83. bottomCorners.forEach((corner) => {
  84. corner.forEach((point) => bottomPoints.push(point))
  85. })
  86. const topPoints = []
  87. topCorners.forEach((corner) => {
  88. corner.forEach((point) => topPoints.push(point))
  89. })
  90. const polygons = []
  91. for (let i = 0; i < topPoints.length; i++) {
  92. const j = (i + 1) % topPoints.length
  93. polygons.push(poly3.create([bottomPoints[i], bottomPoints[j], topPoints[j], topPoints[i]]))
  94. }
  95. return polygons
  96. }
  97. /**
  98. * Construct an axis-aligned solid cuboid in three dimensional space with rounded corners.
  99. * @param {Object} [options] - options for construction
  100. * @param {Array} [options.center=[0,0,0]] - center of rounded cube
  101. * @param {Array} [options.size=[2,2,2]] - dimension of rounded cube; width, depth, height
  102. * @param {Number} [options.roundRadius=0.2] - radius of rounded edges
  103. * @param {Number} [options.segments=32] - number of segments to create per full rotation
  104. * @returns {geom3} new 3D geometry
  105. * @alias module:modeling/primitives.roundedCuboid
  106. *
  107. * @example
  108. * let mycube = roundedCuboid({size: [10, 20, 10], roundRadius: 2, segments: 16})
  109. */
  110. const roundedCuboid = (options) => {
  111. const defaults = {
  112. center: [0, 0, 0],
  113. size: [2, 2, 2],
  114. roundRadius: 0.2,
  115. segments: 32
  116. }
  117. let { center, size, roundRadius, segments } = Object.assign({}, defaults, options)
  118. if (!isNumberArray(center, 3)) throw new Error('center must be an array of X, Y and Z values')
  119. if (!isNumberArray(size, 3)) throw new Error('size must be an array of X, Y and Z values')
  120. if (!size.every((n) => n >= 0)) throw new Error('size values must be positive')
  121. if (!isGTE(roundRadius, 0)) throw new Error('roundRadius must be positive')
  122. if (!isGTE(segments, 4)) throw new Error('segments must be four or more')
  123. // if any size is zero return empty geometry
  124. if (size[0] === 0 || size[1] === 0 || size[2] === 0) return geom3.create()
  125. // if roundRadius is zero, return cuboid
  126. if (roundRadius === 0) return cuboid({ center, size })
  127. size = size.map((v) => v / 2) // convert to radius
  128. if (roundRadius > (size[0] - EPS) ||
  129. roundRadius > (size[1] - EPS) ||
  130. roundRadius > (size[2] - EPS)) throw new Error('roundRadius must be smaller than the radius of all dimensions')
  131. segments = Math.floor(segments / 4)
  132. let prevCornersPos = null
  133. let prevCornersNeg = null
  134. let polygons = []
  135. for (let slice = 0; slice <= segments; slice++) {
  136. const cornersPos = createCorners(center, size, roundRadius, segments, slice, true)
  137. const cornersNeg = createCorners(center, size, roundRadius, segments, slice, false)
  138. if (slice === 0) {
  139. polygons = polygons.concat(stitchSides(cornersNeg, cornersPos))
  140. }
  141. if (prevCornersPos) {
  142. polygons = polygons.concat(stitchCorners(prevCornersPos, cornersPos),
  143. stitchWalls(prevCornersPos, cornersPos))
  144. }
  145. if (prevCornersNeg) {
  146. polygons = polygons.concat(stitchCorners(prevCornersNeg, cornersNeg),
  147. stitchWalls(prevCornersNeg, cornersNeg))
  148. }
  149. if (slice === segments) {
  150. // add the top
  151. let points = cornersPos.map((corner) => corner[0])
  152. polygons.push(poly3.create(points))
  153. // add the bottom
  154. points = cornersNeg.map((corner) => corner[0])
  155. polygons.push(poly3.create(points))
  156. }
  157. prevCornersPos = cornersPos
  158. prevCornersNeg = cornersNeg
  159. }
  160. return geom3.create(polygons)
  161. }
  162. module.exports = roundedCuboid