modeling/src/primitives/geodesicSphere.js

  1. const mat4 = require('../maths/mat4')
  2. const vec3 = require('../maths/vec3')
  3. const geom3 = require('../geometries/geom3')
  4. const polyhedron = require('./polyhedron')
  5. const { isGTE } = require('./commonChecks')
  6. /**
  7. * Construct a geodesic sphere based on icosahedron symmetry.
  8. * @param {Object} [options] - options for construction
  9. * @param {Number} [options.radius=1] - target radius of sphere
  10. * @param {Number} [options.frequency=6] - subdivision frequency per face, multiples of 6
  11. * @returns {geom3} new 3D geometry
  12. * @alias module:modeling/primitives.geodesicSphere
  13. *
  14. * @example
  15. * let myshape = geodesicSphere({radius: 15, frequency: 18})
  16. */
  17. const geodesicSphere = (options) => {
  18. const defaults = {
  19. radius: 1,
  20. frequency: 6
  21. }
  22. let { radius, frequency } = Object.assign({}, defaults, options)
  23. if (!isGTE(radius, 0)) throw new Error('radius must be positive')
  24. if (!isGTE(frequency, 6)) throw new Error('frequency must be six or more')
  25. // if radius is zero return empty geometry
  26. if (radius === 0) return geom3.create()
  27. // adjust the frequency to base 6
  28. frequency = Math.floor(frequency / 6)
  29. const ci = [ // hard-coded data of icosahedron (20 faces, all triangles)
  30. [0.850651, 0.000000, -0.525731],
  31. [0.850651, -0.000000, 0.525731],
  32. [-0.850651, -0.000000, 0.525731],
  33. [-0.850651, 0.000000, -0.525731],
  34. [0.000000, -0.525731, 0.850651],
  35. [0.000000, 0.525731, 0.850651],
  36. [0.000000, 0.525731, -0.850651],
  37. [0.000000, -0.525731, -0.850651],
  38. [-0.525731, -0.850651, -0.000000],
  39. [0.525731, -0.850651, -0.000000],
  40. [0.525731, 0.850651, 0.000000],
  41. [-0.525731, 0.850651, 0.000000]]
  42. const ti = [[0, 9, 1], [1, 10, 0], [6, 7, 0], [10, 6, 0], [7, 9, 0], [5, 1, 4], [4, 1, 9], [5, 10, 1], [2, 8, 3], [3, 11, 2], [2, 5, 4],
  43. [4, 8, 2], [2, 11, 5], [3, 7, 6], [6, 11, 3], [8, 7, 3], [9, 8, 4], [11, 10, 5], [10, 11, 6], [8, 9, 7]]
  44. const geodesicSubDivide = (p, frequency, offset) => {
  45. const p1 = p[0]
  46. const p2 = p[1]
  47. const p3 = p[2]
  48. let n = offset
  49. const c = []
  50. const f = []
  51. // p3
  52. // /\
  53. // /__\ frequency = 3
  54. // i /\ /\
  55. // /__\/__\ total triangles = 9 (frequency*frequency)
  56. // /\ /\ /\
  57. // 0/__\/__\/__\
  58. // p1 0 j p2
  59. for (let i = 0; i < frequency; i++) {
  60. for (let j = 0; j < frequency - i; j++) {
  61. const t0 = i / frequency
  62. const t1 = (i + 1) / frequency
  63. const s0 = j / (frequency - i)
  64. const s1 = (j + 1) / (frequency - i)
  65. const s2 = frequency - i - 1 ? j / (frequency - i - 1) : 1
  66. const q = []
  67. q[0] = mix3(mix3(p1, p2, s0), p3, t0)
  68. q[1] = mix3(mix3(p1, p2, s1), p3, t0)
  69. q[2] = mix3(mix3(p1, p2, s2), p3, t1)
  70. // -- normalize
  71. for (let k = 0; k < 3; k++) {
  72. const r = vec3.length(q[k])
  73. for (let l = 0; l < 3; l++) {
  74. q[k][l] /= r
  75. }
  76. }
  77. c.push(q[0], q[1], q[2])
  78. f.push([n, n + 1, n + 2]); n += 3
  79. if (j < frequency - i - 1) {
  80. const s3 = frequency - i - 1 ? (j + 1) / (frequency - i - 1) : 1
  81. q[0] = mix3(mix3(p1, p2, s1), p3, t0)
  82. q[1] = mix3(mix3(p1, p2, s3), p3, t1)
  83. q[2] = mix3(mix3(p1, p2, s2), p3, t1)
  84. // -- normalize
  85. for (let k = 0; k < 3; k++) {
  86. const r = vec3.length(q[k])
  87. for (let l = 0; l < 3; l++) {
  88. q[k][l] /= r
  89. }
  90. }
  91. c.push(q[0], q[1], q[2])
  92. f.push([n, n + 1, n + 2]); n += 3
  93. }
  94. }
  95. }
  96. return { points: c, triangles: f, offset: n }
  97. }
  98. const mix3 = (a, b, f) => {
  99. const _f = 1 - f
  100. const c = []
  101. for (let i = 0; i < 3; i++) {
  102. c[i] = a[i] * _f + b[i] * f
  103. }
  104. return c
  105. }
  106. let points = []
  107. let faces = []
  108. let offset = 0
  109. for (let i = 0; i < ti.length; i++) {
  110. const g = geodesicSubDivide([ci[ti[i][0]], ci[ti[i][1]], ci[ti[i][2]]], frequency, offset)
  111. points = points.concat(g.points)
  112. faces = faces.concat(g.triangles)
  113. offset = g.offset
  114. }
  115. let geometry = polyhedron({ points: points, faces: faces, orientation: 'inward' })
  116. if (radius !== 1) geometry = geom3.transform(mat4.fromScaling(mat4.create(), [radius, radius, radius]), geometry)
  117. return geometry
  118. }
  119. module.exports = geodesicSphere